From f38b68a590d53c41ad0a2550d048e808da3bc127 Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sun, 25 Nov 2018 03:50:26 +0100 Subject: [PATCH 0001/6909] Add action to pick random skin --- .../Overlays/Settings/Sections/SkinSection.cs | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 938e2ca2c3..9b190c6862 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -1,6 +1,8 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE +using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -24,7 +26,12 @@ namespace osu.Game.Overlays.Settings.Sections private readonly Bindable dropdownBindable = new Bindable(); private readonly Bindable configBindable = new Bindable(); + private static readonly SkinInfo randomSkinInfo = new RandomSkinInfo(); + private SkinManager skins; + private SkinInfo[] usableSkins; + + private Random random = new Random(); [BackgroundDependencyLoader] private void load(OsuConfigManager config, SkinManager skins) @@ -59,15 +66,32 @@ namespace osu.Game.Overlays.Settings.Sections config.BindWith(OsuSetting.Skin, configBindable); + usableSkins = skins.GetAllUsableSkins().ToArray(); + skinDropdown.Bindable = dropdownBindable; - skinDropdown.Items = skins.GetAllUsableSkins().ToArray(); + skinDropdown.Items = usableSkins.Concat(new[] { randomSkinInfo }); // Todo: This should not be necessary when OsuConfigManager is databased if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) configBindable.Value = 0; configBindable.BindValueChanged(v => dropdownBindable.Value = skinDropdown.Items.Single(s => s.ID == v), true); - dropdownBindable.BindValueChanged(v => configBindable.Value = v.ID); + dropdownBindable.BindValueChanged(v => + { + if (v == randomSkinInfo) + randomizeSkin(); + else + configBindable.Value = v.ID; + }); + } + + private void randomizeSkin() + { + int n = usableSkins.Count(); + if (n > 1) + configBindable.Value = (configBindable.Value + random.Next(n - 1) + 1) % n; // make sure it's always a different one + else + configBindable.Value = 0; } private void itemRemoved(SkinInfo s) => Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != s.ID).ToArray()); @@ -98,5 +122,16 @@ namespace osu.Game.Overlays.Settings.Sections protected override string GenerateItemText(SkinInfo item) => item.ToString(); } } + + private class RandomSkinInfo : SkinInfo + { + public RandomSkinInfo() + { + Name = ""; + ID = -1; + } + + public override string ToString() => Name; + } } } From 6a9187ece061aebac08d949c19b8f95feeb477e5 Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sun, 25 Nov 2018 04:01:30 +0100 Subject: [PATCH 0002/6909] Fixed style warnings --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 9b190c6862..0802db821e 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -2,7 +2,6 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; @@ -26,12 +25,12 @@ namespace osu.Game.Overlays.Settings.Sections private readonly Bindable dropdownBindable = new Bindable(); private readonly Bindable configBindable = new Bindable(); - private static readonly SkinInfo randomSkinInfo = new RandomSkinInfo(); + private static readonly SkinInfo random_skin_info = new RandomSkinInfo(); private SkinManager skins; private SkinInfo[] usableSkins; - private Random random = new Random(); + private readonly Random random = new Random(); [BackgroundDependencyLoader] private void load(OsuConfigManager config, SkinManager skins) @@ -69,7 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections usableSkins = skins.GetAllUsableSkins().ToArray(); skinDropdown.Bindable = dropdownBindable; - skinDropdown.Items = usableSkins.Concat(new[] { randomSkinInfo }); + skinDropdown.Items = usableSkins.Concat(new[] { random_skin_info }); // Todo: This should not be necessary when OsuConfigManager is databased if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) @@ -78,7 +77,7 @@ namespace osu.Game.Overlays.Settings.Sections configBindable.BindValueChanged(v => dropdownBindable.Value = skinDropdown.Items.Single(s => s.ID == v), true); dropdownBindable.BindValueChanged(v => { - if (v == randomSkinInfo) + if (v == random_skin_info) randomizeSkin(); else configBindable.Value = v.ID; @@ -87,7 +86,7 @@ namespace osu.Game.Overlays.Settings.Sections private void randomizeSkin() { - int n = usableSkins.Count(); + int n = usableSkins.Length; if (n > 1) configBindable.Value = (configBindable.Value + random.Next(n - 1) + 1) % n; // make sure it's always a different one else From edb45e4e47ff8d6f76969425ceefd80366223d99 Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sun, 25 Nov 2018 14:23:53 +0100 Subject: [PATCH 0003/6909] Only show random skin button with more than one skin --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 0802db821e..cd109706cc 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -68,7 +68,10 @@ namespace osu.Game.Overlays.Settings.Sections usableSkins = skins.GetAllUsableSkins().ToArray(); skinDropdown.Bindable = dropdownBindable; - skinDropdown.Items = usableSkins.Concat(new[] { random_skin_info }); + if (usableSkins.Length > 1) + skinDropdown.Items = usableSkins.Concat(new[] { random_skin_info }); + else + skinDropdown.Items = usableSkins; // Todo: This should not be necessary when OsuConfigManager is databased if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) From 2b05a618066b4bdb50104011a2944173ef256d5f Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sun, 25 Nov 2018 14:24:20 +0100 Subject: [PATCH 0004/6909] Fix crash when reseting skin while in dropdown --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index cd109706cc..46810184d7 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.Settings.Sections if (v == random_skin_info) randomizeSkin(); else - configBindable.Value = v.ID; + configBindable.Value = v?.ID ?? 0; }); } From 17a11212e802d0659d3a5e4904609624be19f3b3 Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Sun, 25 Nov 2018 14:41:39 +0100 Subject: [PATCH 0005/6909] Style fixes --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 46810184d7..56f61efa2a 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -68,10 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections usableSkins = skins.GetAllUsableSkins().ToArray(); skinDropdown.Bindable = dropdownBindable; - if (usableSkins.Length > 1) - skinDropdown.Items = usableSkins.Concat(new[] { random_skin_info }); - else - skinDropdown.Items = usableSkins; + skinDropdown.Items = usableSkins.Length > 1 ? usableSkins.Concat(new[] { random_skin_info }) : usableSkins; // Todo: This should not be necessary when OsuConfigManager is databased if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) From c4c2191500b81bed98918ed00b0536373253dc1f Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Wed, 28 Nov 2018 12:36:21 +0100 Subject: [PATCH 0006/6909] Apply requested changes --- .../Overlays/Settings/Sections/SkinSection.cs | 32 +++++++++++++------ osu.Game/Skinning/SkinManager.cs | 4 +-- 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 56f61efa2a..225e8024e3 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -2,10 +2,12 @@ // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.MathUtils; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -28,9 +30,7 @@ namespace osu.Game.Overlays.Settings.Sections private static readonly SkinInfo random_skin_info = new RandomSkinInfo(); private SkinManager skins; - private SkinInfo[] usableSkins; - - private readonly Random random = new Random(); + private List usableSkins; [BackgroundDependencyLoader] private void load(OsuConfigManager config, SkinManager skins) @@ -65,10 +65,10 @@ namespace osu.Game.Overlays.Settings.Sections config.BindWith(OsuSetting.Skin, configBindable); - usableSkins = skins.GetAllUsableSkins().ToArray(); + usableSkins = skins.GetAllUsableSkins(); skinDropdown.Bindable = dropdownBindable; - skinDropdown.Items = usableSkins.Length > 1 ? usableSkins.Concat(new[] { random_skin_info }) : usableSkins; + resetSkinButtons(); // Todo: This should not be necessary when OsuConfigManager is databased if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) @@ -86,15 +86,29 @@ namespace osu.Game.Overlays.Settings.Sections private void randomizeSkin() { - int n = usableSkins.Length; + int n = usableSkins.Count(); if (n > 1) - configBindable.Value = (configBindable.Value + random.Next(n - 1) + 1) % n; // make sure it's always a different one + configBindable.Value = (configBindable.Value + RNG.Next(n - 1) + 1) % n; // make sure it's always a different one else configBindable.Value = 0; } - 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()); + private void itemRemoved(SkinInfo s) => Schedule(() => + { + usableSkins.RemoveAll(i => i.ID == s.ID); + resetSkinButtons(); + }); + + private void itemAdded(SkinInfo s) => Schedule(() => + { + usableSkins.Add(s); + resetSkinButtons(); + }); + + private void resetSkinButtons() + { + skinDropdown.Items = usableSkins.Count() > 1 ? usableSkins.Concat(new[] { random_skin_info }) : usableSkins; + } protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index bd694e443a..5ea205d2f3 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -31,7 +31,7 @@ namespace osu.Game.Skinning /// /// Returns a list of all usable s. Includes the special default skin plus all skins from . /// - /// A list of available . + /// A newly allocated list of available . public List GetAllUsableSkins() { var userSkins = GetAllUserSkins(); @@ -42,7 +42,7 @@ namespace osu.Game.Skinning /// /// Returns a list of all usable s that have been loaded by the user. /// - /// A list of available . + /// A newly allocated list of available . public List GetAllUserSkins() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo From 89ded824b3a6554d30b14149947f0a28969d010e Mon Sep 17 00:00:00 2001 From: WebFreak001 Date: Wed, 28 Nov 2018 12:49:17 +0100 Subject: [PATCH 0007/6909] Style fixes --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 225e8024e3..3073e2067e 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -1,7 +1,6 @@ // Copyright (c) 2007-2018 ppy Pty Ltd . // Licensed under the MIT Licence - https://raw.githubusercontent.com/ppy/osu/master/LICENCE -using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; @@ -86,7 +85,7 @@ namespace osu.Game.Overlays.Settings.Sections private void randomizeSkin() { - int n = usableSkins.Count(); + int n = usableSkins.Count; if (n > 1) configBindable.Value = (configBindable.Value + RNG.Next(n - 1) + 1) % n; // make sure it's always a different one else @@ -107,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections private void resetSkinButtons() { - skinDropdown.Items = usableSkins.Count() > 1 ? usableSkins.Concat(new[] { random_skin_info }) : usableSkins; + skinDropdown.Items = usableSkins.Count > 1 ? usableSkins.Concat(new[] { random_skin_info }) : usableSkins; } protected override void Dispose(bool isDisposing) From b51a457e5a7231795708e1060c1557cc3809233a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 6 Mar 2019 14:36:30 +0900 Subject: [PATCH 0008/6909] 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 0009/6909] 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 0010/6909] 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 0011/6909] 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 0012/6909] 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 0013/6909] 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 0014/6909] 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 0015/6909] 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 0016/6909] 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 0017/6909] 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 0018/6909] 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 0019/6909] 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 0020/6909] 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 0021/6909] 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 0022/6909] 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 0023/6909] 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 d467dd57472ebc001ee28eb6f01d6b468702e735 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Mon, 29 Jul 2019 06:51:44 +0300 Subject: [PATCH 0024/6909] Add simple implementation of in-game leaderboard --- osu.Game/Screens/Play/InGameLeaderboard.cs | 42 ++++ osu.Game/Screens/Play/InGameScoreContainer.cs | 206 ++++++++++++++++++ 2 files changed, 248 insertions(+) create mode 100644 osu.Game/Screens/Play/InGameLeaderboard.cs create mode 100644 osu.Game/Screens/Play/InGameScoreContainer.cs diff --git a/osu.Game/Screens/Play/InGameLeaderboard.cs b/osu.Game/Screens/Play/InGameLeaderboard.cs new file mode 100644 index 0000000000..c8f5cf5fd7 --- /dev/null +++ b/osu.Game/Screens/Play/InGameLeaderboard.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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Users; + +namespace osu.Game.Screens.Play +{ + public class InGameLeaderboard : CompositeDrawable + { + protected readonly InGameScoreContainer ScoresContainer; + + public readonly BindableDouble PlayerCurrentScore = new BindableDouble(); + + private bool playerItemCreated; + private User playerUser; + + public User PlayerUser + { + get => playerUser; + set + { + playerUser = value; + + if (playerItemCreated) + return; + + ScoresContainer.AddRealTimePlayer(PlayerCurrentScore, playerUser); + playerItemCreated = true; + } + } + + public InGameLeaderboard() + { + AutoSizeAxes = Axes.Y; + + InternalChild = ScoresContainer = new InGameScoreContainer(); + } + } +} diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/InGameScoreContainer.cs new file mode 100644 index 0000000000..eb4a0a2f38 --- /dev/null +++ b/osu.Game/Screens/Play/InGameScoreContainer.cs @@ -0,0 +1,206 @@ +// Copyright (c) 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 Humanizer; +using osu.Framework.Allocation; +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.Scoring; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Screens.Play +{ + public class InGameScoreContainer : FillFlowContainer + { + /// + /// Whether to declare a new position for un-positioned players. + /// Must be disabled for online leaderboards with top 50 scores only. + /// + public bool DeclareNewPosition = true; + + public InGameScoreContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + Spacing = new Vector2(2.5f); + LayoutDuration = 500; + LayoutEasing = Easing.OutQuint; + } + + public void AddRealTimePlayer(BindableDouble currentScore, User user = null) + { + if (currentScore == null) + return; + + var scoreItem = addScore(currentScore.Value, user); + currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue; + } + + public void AddScore(ScoreInfo score, int? position = null) + { + if (score != null) + addScore(score.TotalScore, score.User, position); + } + + private int maxPosition => this.Where(i => i.ScorePosition.HasValue).Max(i => i.ScorePosition) ?? 0; + + private InGameScoreItem addScore(double totalScore, User user = null, int? position = null) + { + var scoreItem = new InGameScoreItem + { + User = user, + TotalScore = totalScore, + ScorePosition = position, + OnScoreChange = updateScores, + }; + + Add(scoreItem); + SetLayoutPosition(scoreItem, position ?? maxPosition + 1); + + updateScores(); + + return scoreItem; + } + + private void updateScores() + { + var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); + var orderedPositions = this.OrderByDescending(i => i.ScorePosition.HasValue).ThenBy(i => i.ScorePosition).Select(i => i.ScorePosition).ToList(); + + for (int i = 0; i < Count; i++) + { + int newPosition = orderedPositions[i] ?? maxPosition + 1; + + SetLayoutPosition(orderedByScore[i], newPosition); + orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; + } + } + } + + public class InGameScoreItem : CompositeDrawable + { + private readonly OsuSpriteText positionText, positionSymbol, userString; + private readonly GlowingSpriteText scoreText; + + public Action OnScoreChange; + + private int? scorePosition; + + public int? ScorePosition + { + get => scorePosition; + set + { + scorePosition = value; + + if (scorePosition.HasValue) + positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}"; + + positionText.FadeTo(scorePosition.HasValue ? 1 : 0, 100); + positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0, 100); + } + } + + private double totalScore; + + public double TotalScore + { + get => totalScore; + set + { + totalScore = value; + scoreText.Text = totalScore.ToString("N0"); + + OnScoreChange?.Invoke(); + } + } + + private User user; + + public User User + { + get => user; + set + { + user = value; + userString.Text = user?.Username; + } + } + + public InGameScoreItem() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new Container + { + Masking = true, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Right = 2.5f }, + Spacing = new Vector2(2.5f), + Children = new[] + { + positionText = new OsuSpriteText + { + Alpha = 0, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), + }, + positionSymbol = new OsuSpriteText + { + Alpha = 0, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + Text = ">", + }, + } + }, + new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Left = 2.5f }, + Spacing = new Vector2(2.5f), + Children = new Drawable[] + { + userString = new OsuSpriteText + { + Size = new Vector2(80, 16), + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true), + }, + scoreText = new GlowingSpriteText + { + GlowColour = OsuColour.FromHex(@"83ccfa"), + Font = OsuFont.Numeric.With(size: 14), + } + } + }, + }, + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + positionText.Colour = colours.YellowLight; + positionSymbol.Colour = colours.Yellow; + } + } +} From 6233dc22e0b76bda53722e0aca270d67e0e39c8c Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Mon, 29 Jul 2019 06:52:42 +0300 Subject: [PATCH 0025/6909] Add simple tests for current implementation --- .../Gameplay/TestSceneInGameLeaderboard.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs new file mode 100644 index 0000000000..36ed8f9b4c --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Screens.Play; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [TestFixture] + public class TestSceneInGameLeaderboard : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(InGameLeaderboard), + typeof(InGameScoreContainer), + }; + + private readonly TestInGameLeaderboard leaderboard; + private readonly BindableDouble playerScore; + + public TestSceneInGameLeaderboard() + { + Add(leaderboard = new TestInGameLeaderboard + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(2), + RelativeSizeAxes = Axes.X, + PlayerCurrentScore = { BindTarget = playerScore = new BindableDouble(1222333) } + }); + + AddStep("add player user", () => leaderboard.PlayerUser = new User { Username = "You" }); + AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); + } + + [Test] + public void TestPlayerScore() + { + var player2Score = new BindableDouble(1234567); + var player3Score = new BindableDouble(1111111); + + AddStep("add player 2", () => leaderboard.AddDummyPlayer(player2Score, "Player 2")); + AddStep("add player 3", () => leaderboard.AddDummyPlayer(player3Score, "Player 3")); + + AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); + AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); + AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); + + AddStep("set score above player 3", () => player2Score.Value = playerScore.Value - 500); + AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); + AddAssert("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2)); + AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); + + AddStep("set score below players", () => player2Score.Value = playerScore.Value - 123456); + AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); + AddAssert("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2)); + AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); + } + + private class TestInGameLeaderboard : InGameLeaderboard + { + public bool CheckPositionByUsername(string username, int? estimatedPosition) + { + var scoreItem = ScoresContainer.Where(i => i.User.Username == username).FirstOrDefault(); + + return scoreItem != null && scoreItem.ScorePosition == estimatedPosition; + } + + public void AddDummyPlayer(BindableDouble currentScore, string username) => ScoresContainer.AddRealTimePlayer(currentScore, new User { Username = username }); + } + } +} From 4874746525615cb5aaffbe6195d8038b7766543b Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 30 Jul 2019 12:32:12 +0300 Subject: [PATCH 0026/6909] Add OnScoreChange event For use in scrolling system on a later PR --- osu.Game/Screens/Play/InGameScoreContainer.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/InGameScoreContainer.cs index eb4a0a2f38..985e867510 100644 --- a/osu.Game/Screens/Play/InGameScoreContainer.cs +++ b/osu.Game/Screens/Play/InGameScoreContainer.cs @@ -18,6 +18,12 @@ namespace osu.Game.Screens.Play { public class InGameScoreContainer : FillFlowContainer { + /// + /// Called once an item's score has changed. + /// Useful for doing calculations on what score to show or hide next. (scrolling system) + /// + public event Action OnScoreChange; + /// /// Whether to declare a new position for un-positioned players. /// Must be disabled for online leaderboards with top 50 scores only. @@ -64,12 +70,12 @@ namespace osu.Game.Screens.Play Add(scoreItem); SetLayoutPosition(scoreItem, position ?? maxPosition + 1); - updateScores(); + reorderPositions(); return scoreItem; } - private void updateScores() + private void reorderPositions() { var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); var orderedPositions = this.OrderByDescending(i => i.ScorePosition.HasValue).ThenBy(i => i.ScorePosition).Select(i => i.ScorePosition).ToList(); @@ -82,6 +88,13 @@ namespace osu.Game.Screens.Play orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; } } + + private void updateScores() + { + reorderPositions(); + + OnScoreChange?.Invoke(); + } } public class InGameScoreItem : CompositeDrawable From 32498d5d5088f4b009eec5d7304ea10f96dbb515 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 30 Jul 2019 12:35:49 +0300 Subject: [PATCH 0027/6909] Add xmldocs and return score items. Return for usage in later PR --- osu.Game/Screens/Play/InGameScoreContainer.cs | 24 +++++++++++++------ 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/InGameScoreContainer.cs index 985e867510..0ebf475e18 100644 --- a/osu.Game/Screens/Play/InGameScoreContainer.cs +++ b/osu.Game/Screens/Play/InGameScoreContainer.cs @@ -40,20 +40,30 @@ namespace osu.Game.Screens.Play LayoutEasing = Easing.OutQuint; } - public void AddRealTimePlayer(BindableDouble currentScore, User user = null) + /// + /// Adds a real-time player score item whose score is updated via a . + /// + /// The bindable current score of the player. + /// The player user. + /// Returns the drawable score item of that player. + public InGameScoreItem AddRealTimePlayer(BindableDouble currentScore, User user = null) { if (currentScore == null) - return; + return null; var scoreItem = addScore(currentScore.Value, user); currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue; + + return scoreItem; } - public void AddScore(ScoreInfo score, int? position = null) - { - if (score != null) - addScore(score.TotalScore, score.User, position); - } + /// + /// Adds a score item based off a with an initial position. + /// + /// The score info to use for this item. + /// The initial position of this item. + /// Returns the drawable score item of that player. + public InGameScoreItem AddScore(ScoreInfo score, int? position = null) => score != null ? addScore(score.TotalScore, score.User, position) : null; private int maxPosition => this.Where(i => i.ScorePosition.HasValue).Max(i => i.ScorePosition) ?? 0; From 77aa3a9fe56437c3b4aca91b12e87ee5880a8b39 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 30 Jul 2019 12:37:01 +0300 Subject: [PATCH 0028/6909] Clear previous score items on SetUp --- osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs index 36ed8f9b4c..4a861428de 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs @@ -40,6 +40,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } + [SetUp] + public void SetUp() => leaderboard.ClearScores(); + [Test] public void TestPlayerScore() { @@ -66,6 +69,8 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestInGameLeaderboard : InGameLeaderboard { + public void ClearScores() => ScoresContainer.RemoveAll(s => s.User.Username != PlayerUser.Username); + public bool CheckPositionByUsername(string username, int? estimatedPosition) { var scoreItem = ScoresContainer.Where(i => i.User.Username == username).FirstOrDefault(); From 7675c679db84f0fa422b6f9034407527f36484af Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Wed, 31 Jul 2019 16:09:40 +0300 Subject: [PATCH 0029/6909] Fix reordering not declaring new position properly Small bug fix --- osu.Game/Screens/Play/InGameScoreContainer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/InGameScoreContainer.cs index 0ebf475e18..edccf6c45b 100644 --- a/osu.Game/Screens/Play/InGameScoreContainer.cs +++ b/osu.Game/Screens/Play/InGameScoreContainer.cs @@ -90,9 +90,11 @@ namespace osu.Game.Screens.Play var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); var orderedPositions = this.OrderByDescending(i => i.ScorePosition.HasValue).ThenBy(i => i.ScorePosition).Select(i => i.ScorePosition).ToList(); + var newDeclaredPosition = maxPosition + 1; + for (int i = 0; i < Count; i++) { - int newPosition = orderedPositions[i] ?? maxPosition + 1; + int newPosition = orderedPositions[i] ?? newDeclaredPosition; SetLayoutPosition(orderedByScore[i], newPosition); orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; From 5e4e15033025258a2ec6212c782c8cd72380d5f9 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sun, 4 Aug 2019 18:20:45 +0300 Subject: [PATCH 0030/6909] Use normal Action --- osu.Game/Screens/Play/InGameScoreContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/InGameScoreContainer.cs index edccf6c45b..2d45158095 100644 --- a/osu.Game/Screens/Play/InGameScoreContainer.cs +++ b/osu.Game/Screens/Play/InGameScoreContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Play /// Called once an item's score has changed. /// Useful for doing calculations on what score to show or hide next. (scrolling system) /// - public event Action OnScoreChange; + public Action OnScoreChange; /// /// Whether to declare a new position for un-positioned players. From 9325c024bb08dcd468cc41a0f70d3df7f09f5d78 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sun, 4 Aug 2019 18:26:01 +0300 Subject: [PATCH 0031/6909] Add InitialPosition field --- osu.Game/Screens/Play/InGameScoreContainer.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/InGameScoreContainer.cs index 2d45158095..36dbec7fce 100644 --- a/osu.Game/Screens/Play/InGameScoreContainer.cs +++ b/osu.Game/Screens/Play/InGameScoreContainer.cs @@ -61,19 +61,18 @@ namespace osu.Game.Screens.Play /// Adds a score item based off a with an initial position. /// /// The score info to use for this item. - /// The initial position of this item. + /// The initial position of this item. /// Returns the drawable score item of that player. - public InGameScoreItem AddScore(ScoreInfo score, int? position = null) => score != null ? addScore(score.TotalScore, score.User, position) : null; + public InGameScoreItem AddScore(ScoreInfo score, int? initialPosition = null) => score != null ? addScore(score.TotalScore, score.User, initialPosition) : null; private int maxPosition => this.Where(i => i.ScorePosition.HasValue).Max(i => i.ScorePosition) ?? 0; private InGameScoreItem addScore(double totalScore, User user = null, int? position = null) { - var scoreItem = new InGameScoreItem + var scoreItem = new InGameScoreItem(position) { User = user, TotalScore = totalScore, - ScorePosition = position, OnScoreChange = updateScores, }; @@ -117,6 +116,7 @@ namespace osu.Game.Screens.Play public Action OnScoreChange; private int? scorePosition; + public int? InitialPosition; public int? ScorePosition { @@ -159,7 +159,7 @@ namespace osu.Game.Screens.Play } } - public InGameScoreItem() + public InGameScoreItem(int? initialPosition) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -219,6 +219,8 @@ namespace osu.Game.Screens.Play }, }, }; + + InitialPosition = ScorePosition = initialPosition; } [BackgroundDependencyLoader] From 91f35dde58a3815ba25d731c6ed5fad8da1e98ab Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sun, 4 Aug 2019 18:27:49 +0300 Subject: [PATCH 0032/6909] Actual fix of position declaring bug --- osu.Game/Screens/Play/InGameScoreContainer.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/InGameScoreContainer.cs index 36dbec7fce..3949307dca 100644 --- a/osu.Game/Screens/Play/InGameScoreContainer.cs +++ b/osu.Game/Screens/Play/InGameScoreContainer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play /// Returns the drawable score item of that player. public InGameScoreItem AddScore(ScoreInfo score, int? initialPosition = null) => score != null ? addScore(score.TotalScore, score.User, initialPosition) : null; - private int maxPosition => this.Where(i => i.ScorePosition.HasValue).Max(i => i.ScorePosition) ?? 0; + private int maxPosition => this.Max(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition) ?? 0; private InGameScoreItem addScore(double totalScore, User user = null, int? position = null) { @@ -87,13 +87,11 @@ namespace osu.Game.Screens.Play private void reorderPositions() { var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); - var orderedPositions = this.OrderByDescending(i => i.ScorePosition.HasValue).ThenBy(i => i.ScorePosition).Select(i => i.ScorePosition).ToList(); - - var newDeclaredPosition = maxPosition + 1; + var orderedPositions = this.Select(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition).OrderByDescending(p => p.HasValue).ThenBy(p => p).ToList(); for (int i = 0; i < Count; i++) { - int newPosition = orderedPositions[i] ?? newDeclaredPosition; + int newPosition = orderedPositions[i] ?? maxPosition + 1; SetLayoutPosition(orderedByScore[i], newPosition); orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; From f5daf98aa530173f9e83dfc5a1b90b1fb2fd1591 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sun, 4 Aug 2019 18:28:40 +0300 Subject: [PATCH 0033/6909] Reset player state on setup --- .../Visual/Gameplay/TestSceneInGameLeaderboard.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs index 4a861428de..096b29d496 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs @@ -41,7 +41,12 @@ namespace osu.Game.Tests.Visual.Gameplay } [SetUp] - public void SetUp() => leaderboard.ClearScores(); + public void SetUp() + { + leaderboard.ClearScores(); + leaderboard.PlayerPosition = 1; + playerScore.Value = 1222333; + } [Test] public void TestPlayerScore() From 598c02f8b9f266c1dbd3d0d2ee196a2ee7a28d73 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sun, 4 Aug 2019 18:31:32 +0300 Subject: [PATCH 0034/6909] Shorten Where().FirstOrDefault() --- osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs index 096b29d496..24b8033fbc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Gameplay public bool CheckPositionByUsername(string username, int? estimatedPosition) { - var scoreItem = ScoresContainer.Where(i => i.User.Username == username).FirstOrDefault(); + var scoreItem = ScoresContainer.FirstOrDefault(i => i.User.Username == username); return scoreItem != null && scoreItem.ScorePosition == estimatedPosition; } From fdd00c0820355b10b8450e449512cd8067fb77ff Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sun, 4 Aug 2019 18:35:53 +0300 Subject: [PATCH 0035/6909] Remove position fading Unnecessary effect --- osu.Game/Screens/Play/InGameScoreContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/InGameScoreContainer.cs index 3949307dca..f548b3de3f 100644 --- a/osu.Game/Screens/Play/InGameScoreContainer.cs +++ b/osu.Game/Screens/Play/InGameScoreContainer.cs @@ -126,8 +126,8 @@ namespace osu.Game.Screens.Play if (scorePosition.HasValue) positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}"; - positionText.FadeTo(scorePosition.HasValue ? 1 : 0, 100); - positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0, 100); + positionText.FadeTo(scorePosition.HasValue ? 1 : 0); + positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0); } } From ca7a812e1cc822152ceff303d1323a608b6e141a Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sun, 4 Aug 2019 18:39:42 +0300 Subject: [PATCH 0036/6909] Add PlayerPosition property --- osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs index 24b8033fbc..cd211d06ec 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs @@ -75,6 +75,11 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestInGameLeaderboard : InGameLeaderboard { public void ClearScores() => ScoresContainer.RemoveAll(s => s.User.Username != PlayerUser.Username); + public int? PlayerPosition + { + get => PlayerScoreItem.ScorePosition; + set => PlayerScoreItem.ScorePosition = value; + } public bool CheckPositionByUsername(string username, int? estimatedPosition) { From 472af16015c40745f59fa759e362f637f57e0f44 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sun, 4 Aug 2019 19:17:45 +0300 Subject: [PATCH 0037/6909] Revert unintended change --- .../Visual/Gameplay/TestSceneInGameLeaderboard.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs index cd211d06ec..0019212dfa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs @@ -44,7 +44,6 @@ namespace osu.Game.Tests.Visual.Gameplay public void SetUp() { leaderboard.ClearScores(); - leaderboard.PlayerPosition = 1; playerScore.Value = 1222333; } @@ -75,11 +74,6 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestInGameLeaderboard : InGameLeaderboard { public void ClearScores() => ScoresContainer.RemoveAll(s => s.User.Username != PlayerUser.Username); - public int? PlayerPosition - { - get => PlayerScoreItem.ScorePosition; - set => PlayerScoreItem.ScorePosition = value; - } public bool CheckPositionByUsername(string username, int? estimatedPosition) { From ee1c3d42d884167d1657027ca9dad17704df7231 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 20 Aug 2019 21:11:26 +0300 Subject: [PATCH 0038/6909] Add spinner tick judgement --- .../Judgements/OsuSpinnerTickJudgement.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs new file mode 100644 index 0000000000..f9cac7a2c1 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuSpinnerTickJudgement : OsuJudgement + { + internal bool HasBonusPoints; + + public override bool AffectsCombo => false; + + protected override int NumericResultFor(HitResult result) => 100 + (HasBonusPoints ? 1000 : 0); + + protected override double HealthIncreaseFor(HitResult result) => 0; + } +} From bb4178fa037a2b9a4d361b7a89715958d773db3e Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 20 Aug 2019 21:17:27 +0300 Subject: [PATCH 0039/6909] Add drawable spinner ticks implementation --- .../Objects/Drawables/DrawableSpinnerTick.cs | 49 +++++++++++++++++++ osu.Game.Rulesets.Osu/Objects/Spinner.cs | 11 +++++ osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 19 +++++++ .../Replays/OsuAutoGeneratorBase.cs | 2 +- 4 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs create mode 100644 osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs new file mode 100644 index 0000000000..9c316591a9 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.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.Audio; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public class DrawableSpinnerTick : DrawableOsuHitObject + { + private readonly BindableDouble bonusSampleVolume = new BindableDouble(); + + private bool hasBonusPoints; + + /// + /// Whether this judgement has a bonus of 1,000 points additional to the numeric result. + /// Should be set when a spin occured after the spinner has completed. + /// + public bool HasBonusPoints + { + get => hasBonusPoints; + internal set + { + hasBonusPoints = value; + + bonusSampleVolume.Value = value ? 1 : 0; + ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints = value; + } + } + + public override bool DisplayResult => false; + + public DrawableSpinnerTick(SpinnerTick spinnerTick) + : base(spinnerTick) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Samples.AddAdjustment(AdjustableProperty.Volume, bonusSampleVolume); + } + + public void TriggerResult(HitResult result) => ApplyResult(r => r.Type = result); + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 8a2fd3b7aa..c32ec7be1c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -7,6 +7,8 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Osu.Replays; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects { @@ -30,6 +32,15 @@ namespace osu.Game.Rulesets.Osu.Objects SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6); } + protected override void CreateNestedHitObjects() + { + base.CreateNestedHitObjects(); + + var maximumSpins = OsuAutoGeneratorBase.SPIN_RADIUS * (Duration / 1000) / MathHelper.TwoPi; + for (int i = 0; i < maximumSpins; i++) + AddNested(new SpinnerTick()); + } + public override Judgement CreateJudgement() => new OsuJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs new file mode 100644 index 0000000000..18a3dc771b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.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.Audio; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class SpinnerTick : OsuHitObject + { + public SpinnerTick() + { + Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); + } + + public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); + } +} diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index 9ab358ee12..3356a0fbe0 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Replays /// protected static readonly Vector2 SPINNER_CENTRE = OsuPlayfield.BASE_SIZE / 2; - protected const float SPIN_RADIUS = 50; + public const float SPIN_RADIUS = 50; /// /// The time in ms between each ReplayFrame. From 07795c9922cc4b3ce5197010b03fc53e0b1f565b Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 20 Aug 2019 21:50:49 +0300 Subject: [PATCH 0040/6909] Add logic to gain bonus score from spinner ticks --- .../Objects/Drawables/DrawableSpinner.cs | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a0bd301fdb..d166d6b845 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -12,7 +12,9 @@ using osu.Game.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Sprites; using osu.Game.Screens.Ranking; using osu.Game.Rulesets.Scoring; @@ -22,6 +24,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { protected readonly Spinner Spinner; + private readonly Container ticks; + private readonly OsuSpriteText bonusCounter; + public readonly SpinnerDisc Disc; public readonly SpinnerTicks Ticks; private readonly SpinnerSpmCounter spmCounter; @@ -58,6 +63,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { + ticks = new Container(), circleContainer = new Container { AutoSizeAxes = Axes.Both, @@ -115,8 +121,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre, Y = 120, Alpha = 0 + }, + bonusCounter = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Y = -120, + Font = OsuFont.Numeric.With(size: 24), + Alpha = 0, } }; + + foreach (var tick in Spinner.NestedHitObjects.OfType()) + { + var drawableTick = new DrawableSpinnerTick(tick); + + ticks.Add(drawableTick); + AddNested(drawableTick); + } } [BackgroundDependencyLoader] @@ -182,6 +204,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.Update(); } + private int currentSpins; + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -190,6 +214,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Ticks.Rotation = Disc.Rotation; spmCounter.SetRotation(Disc.RotationAbsolute); + var newSpins = (int)(Disc.RotationAbsolute / 360) - currentSpins; + + for (int i = currentSpins; i < currentSpins + newSpins; i++) + { + if (i < 0 || i >= ticks.Count) + break; + + var tick = ticks[i]; + + tick.HasBonusPoints = Progress >= 1 && i > Spinner.SpinsRequired; + + tick.TriggerResult(HitResult.Great); + } + + currentSpins += newSpins; + float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint); @@ -232,6 +272,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables break; } + if (state != ArmedState.Idle) + Schedule(() => NestedHitObjects.Where(t => !t.IsHit).OfType().ForEach(t => t.TriggerResult(HitResult.Miss))); + Expire(); } } From e4179fe4403232aa5663c80c7ee21800a20bd204 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 20 Aug 2019 21:51:32 +0300 Subject: [PATCH 0041/6909] Show bonus text if contains bonus points (1,000) --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index d166d6b845..b97f4e0a57 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -225,6 +225,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables tick.HasBonusPoints = Progress >= 1 && i > Spinner.SpinsRequired; + if (tick.HasBonusPoints) + bonusCounter + .TransformTextTo($"{(i - Spinner.SpinsRequired) * 1000}") + .FadeOutFromOne(1500) + .ScaleTo(1.5f).ScaleTo(1f, 1000, Easing.OutQuint); + tick.TriggerResult(HitResult.Great); } From dbf4884cbc64c736b16d334a2ed29e3f7780ce5b Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 20 Aug 2019 21:52:13 +0300 Subject: [PATCH 0042/6909] Adjust test spinner rotation --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 3ed3f3e981..6e0745d125 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestDrawableSpinner : DrawableSpinner { - private bool auto; + private readonly bool auto; public TestDrawableSpinner(Spinner s, bool auto) : base(s) @@ -74,12 +74,8 @@ namespace osu.Game.Rulesets.Osu.Tests protected override void CheckForResult(bool userTriggered, double timeOffset) { - 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; - auto = false; - } + if (auto && !userTriggered && Time.Current > Spinner.StartTime) + Disc.RotationAbsolute += Progress >= 1 ? 10 : (float)(Spinner.Duration / 120); base.CheckForResult(userTriggered, timeOffset); } From 6b7cb46ddaf9518e9f876535a86f385ed0db1a26 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sat, 7 Sep 2019 17:27:02 +0300 Subject: [PATCH 0043/6909] Add null hit windows --- osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index 18a3dc771b..c2104e68ee 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -4,6 +4,7 @@ using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { @@ -15,5 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects } public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); + + protected override HitWindows CreateHitWindows() => null; } } From 33f4a6897cd315ba7e3790378a586a9adf424b1d Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Sat, 7 Sep 2019 18:01:15 +0300 Subject: [PATCH 0044/6909] Assign to the text property directly --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index fc1e410d5f..62cec0f124 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -226,10 +226,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables tick.HasBonusPoints = Progress >= 1 && i > Spinner.SpinsRequired; if (tick.HasBonusPoints) + { + bonusCounter.Text = $"{(i - Spinner.SpinsRequired) * 1000}"; bonusCounter - .TransformTextTo($"{(i - Spinner.SpinsRequired) * 1000}") .FadeOutFromOne(1500) .ScaleTo(1.5f).ScaleTo(1f, 1000, Easing.OutQuint); + } tick.TriggerResult(HitResult.Great); } From 812d33f850c1f83351f8a6c17376e11f178f7c22 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 1 Oct 2019 08:09:01 +0300 Subject: [PATCH 0045/6909] Add ExpandNumberPiece configuration with OsuLegacySkinTransformer --- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 4 ++++ osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs | 1 + 2 files changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 479c250eab..c9ed313593 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -123,6 +123,10 @@ namespace osu.Game.Rulesets.Osu.Skinning return SkinUtils.As(new BindableFloat(legacy_circle_radius)); break; + + case OsuSkinConfiguration.ExpandNumberPiece: + string legacyVersion = source.GetConfig("Version")?.Value ?? "1"; + return SkinUtils.As(new BindableBool(double.TryParse(legacyVersion, out double version) && version < 2.0)); } break; diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 98219cafe8..85a7f5b0cd 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -7,6 +7,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { HitCirclePrefix, HitCircleOverlap, + ExpandNumberPiece, SliderBorderSize, SliderPathRadius, AllowSliderBallTint, From 6ba1bc381c07d68d7af35d3e631dbe6cac7b9b93 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 1 Oct 2019 08:14:15 +0300 Subject: [PATCH 0046/6909] Add version value to the DefaultSkinConfiguration dictionary --- osu.Game/Skinning/DefaultSkinConfiguration.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Skinning/DefaultSkinConfiguration.cs b/osu.Game/Skinning/DefaultSkinConfiguration.cs index f52fac6077..481360beb9 100644 --- a/osu.Game/Skinning/DefaultSkinConfiguration.cs +++ b/osu.Game/Skinning/DefaultSkinConfiguration.cs @@ -19,6 +19,8 @@ namespace osu.Game.Skinning new Color4(204, 102, 0, 255), new Color4(121, 9, 13, 255) }); + + ConfigDictionary.Add(@"Version", "latest"); } } } From 9e314cd664991c1836d0cb258405b6652e5f4a03 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 1 Oct 2019 08:15:48 +0300 Subject: [PATCH 0047/6909] Add expand number piece bindable to hit circle --- .../Objects/Drawables/DrawableHitCircle.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index bb227d76df..15c768fff9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -10,9 +10,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Scoring; -using osuTK; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -20,6 +21,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public ApproachCircle ApproachCircle { get; } + public IBindable ExpandNumberPiece => expandNumberPiece; + + private readonly BindableBool expandNumberPiece = new BindableBool(); + private readonly IBindable positionBindable = new Bindable(); private readonly IBindable stackHeightBindable = new Bindable(); private readonly IBindable scaleBindable = new Bindable(); @@ -106,6 +111,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + protected override void ApplySkin(ISkinSource skin, bool allowFallback) + { + base.ApplySkin(skin, allowFallback); + + expandNumberPiece.Value = skin.GetConfig(OsuSkinConfiguration.ExpandNumberPiece).Value; + } + protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); From 5aa85968c232927d42a35344437a5f3376fe3321 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 1 Oct 2019 08:23:41 +0300 Subject: [PATCH 0048/6909] Expand number piece for old skins in legacy circle pieces --- .../Skinning/LegacyMainCirclePiece.cs | 40 ++++++++++++++----- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 93ae0371df..d93d0506c3 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -10,9 +10,12 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; +using System.Collections.Generic; +using System.Linq; namespace osu.Game.Rulesets.Osu.Skinning { @@ -23,17 +26,24 @@ namespace osu.Game.Rulesets.Osu.Skinning Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } + private Sprite hitCircleSprite; + private SkinnableSpriteText hitCircleText; + + private List scalables; + private readonly IBindable state = new Bindable(); private readonly Bindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); + private readonly IBindable expandNumberPiece = new BindableBool(); + + [Resolved] + private DrawableHitObject drawableObject { get; set; } [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject, ISkinSource skin) + private void load(ISkinSource skin) { OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; - - Sprite hitCircleSprite; - SkinnableSpriteText hitCircleText; + DrawableHitCircle drawableCircle = (DrawableHitCircle)drawableObject; InternalChildren = new Drawable[] { @@ -58,13 +68,25 @@ namespace osu.Game.Rulesets.Osu.Skinning }; state.BindTo(drawableObject.State); - state.BindValueChanged(updateState, true); - accentColour.BindTo(drawableObject.AccentColour); - accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); - indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable); + expandNumberPiece.BindTo(drawableCircle.ExpandNumberPiece); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + state.BindValueChanged(updateState, true); + accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); + expandNumberPiece.BindValueChanged(expand => + { + scalables = InternalChildren.ToList(); + + if (!expand.NewValue) + scalables.Remove(hitCircleText); + }, true); } private void updateState(ValueChangedEvent state) @@ -75,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { case ArmedState.Hit: this.FadeOut(legacy_fade_duration, Easing.Out); - this.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + scalables.ForEach(d => d.ScaleTo(1.4f, legacy_fade_duration, Easing.Out)); break; } } From ef8f9aa276f9359b1c1f81df3644f52917b7dbfb Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 1 Oct 2019 08:43:03 +0300 Subject: [PATCH 0049/6909] Fix possible nullref exception --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 15c768fff9..bd4cb1f112 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.ApplySkin(skin, allowFallback); - expandNumberPiece.Value = skin.GetConfig(OsuSkinConfiguration.ExpandNumberPiece).Value; + expandNumberPiece.Value = skin.GetConfig(OsuSkinConfiguration.ExpandNumberPiece)?.Value ?? false; } protected override void CheckForResult(bool userTriggered, double timeOffset) From 957bbee3e4eb4569310eacf11c59f7e167795efe Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Thu, 3 Oct 2019 05:58:20 +0300 Subject: [PATCH 0050/6909] Scale pieces individually and use skin source directly --- .../Objects/Drawables/DrawableHitCircle.cs | 11 ------- .../Skinning/LegacyMainCirclePiece.cs | 30 ++++++++----------- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index bd4cb1f112..c37535521d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -21,10 +21,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public ApproachCircle ApproachCircle { get; } - public IBindable ExpandNumberPiece => expandNumberPiece; - - private readonly BindableBool expandNumberPiece = new BindableBool(); - private readonly IBindable positionBindable = new Bindable(); private readonly IBindable stackHeightBindable = new Bindable(); private readonly IBindable scaleBindable = new Bindable(); @@ -111,13 +107,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected override void ApplySkin(ISkinSource skin, bool allowFallback) - { - base.ApplySkin(skin, allowFallback); - - expandNumberPiece.Value = skin.GetConfig(OsuSkinConfiguration.ExpandNumberPiece)?.Value ?? false; - } - protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index d93d0506c3..8ba21d9f89 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -14,8 +14,6 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -using System.Collections.Generic; -using System.Linq; namespace osu.Game.Rulesets.Osu.Skinning { @@ -26,21 +24,21 @@ namespace osu.Game.Rulesets.Osu.Skinning Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } - private Sprite hitCircleSprite; + private Sprite hitCircleSprite, hitCircleOverlay; private SkinnableSpriteText hitCircleText; - private List scalables; - private readonly IBindable state = new Bindable(); private readonly Bindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); - private readonly IBindable expandNumberPiece = new BindableBool(); [Resolved] private DrawableHitObject drawableObject { get; set; } + [Resolved] + private ISkinSource skin { get; set; } + [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load() { OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; DrawableHitCircle drawableCircle = (DrawableHitCircle)drawableObject; @@ -59,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, }, confineMode: ConfineMode.NoScaling), - new Sprite + hitCircleOverlay = new Sprite { Texture = skin.GetTexture("hitcircleoverlay"), Anchor = Anchor.Centre, @@ -70,7 +68,6 @@ namespace osu.Game.Rulesets.Osu.Skinning state.BindTo(drawableObject.State); accentColour.BindTo(drawableObject.AccentColour); indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable); - expandNumberPiece.BindTo(drawableCircle.ExpandNumberPiece); } protected override void LoadComplete() @@ -80,13 +77,6 @@ namespace osu.Game.Rulesets.Osu.Skinning state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); - expandNumberPiece.BindValueChanged(expand => - { - scalables = InternalChildren.ToList(); - - if (!expand.NewValue) - scalables.Remove(hitCircleText); - }, true); } private void updateState(ValueChangedEvent state) @@ -97,7 +87,13 @@ namespace osu.Game.Rulesets.Osu.Skinning { case ArmedState.Hit: this.FadeOut(legacy_fade_duration, Easing.Out); - scalables.ForEach(d => d.ScaleTo(1.4f, legacy_fade_duration, Easing.Out)); + + hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + + if (skin.GetConfig(OsuSkinConfiguration.ExpandNumberPiece).Value) + hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + break; } } From 023c4d64d811e1abc49655ba37b766568117333f Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Thu, 3 Oct 2019 06:00:22 +0300 Subject: [PATCH 0051/6909] Remove redundant using directive --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index c37535521d..4579e5cb59 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK; From a73f6c6a5a3dfc12518a85442732a4c2469d0535 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Thu, 3 Oct 2019 06:46:13 +0300 Subject: [PATCH 0052/6909] Specify version of osu!classic skin --- osu.Game/Skinning/DefaultLegacySkin.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 4b6eea6b6e..8370b5e8ee 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -20,6 +20,8 @@ namespace osu.Game.Skinning new Color4(18, 124, 255, 255), new Color4(242, 24, 57, 255), }); + + Configuration.ConfigDictionary["Version"] = "2"; } public static SkinInfo Info { get; } = new SkinInfo From 89075c5655618c70b2d05f4943a11dca18350b89 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Thu, 3 Oct 2019 06:48:07 +0300 Subject: [PATCH 0053/6909] Set version of not-configured skins to latest only --- osu.Game/Skinning/DefaultSkinConfiguration.cs | 9 +++++++-- osu.Game/Skinning/LegacySkin.cs | 2 +- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/DefaultSkinConfiguration.cs b/osu.Game/Skinning/DefaultSkinConfiguration.cs index 481360beb9..1d22ce8c1d 100644 --- a/osu.Game/Skinning/DefaultSkinConfiguration.cs +++ b/osu.Game/Skinning/DefaultSkinConfiguration.cs @@ -10,7 +10,7 @@ namespace osu.Game.Skinning /// public class DefaultSkinConfiguration : SkinConfiguration { - public DefaultSkinConfiguration() + public DefaultSkinConfiguration(string version) { ComboColours.AddRange(new[] { @@ -20,7 +20,12 @@ namespace osu.Game.Skinning new Color4(121, 9, 13, 255) }); - ConfigDictionary.Add(@"Version", "latest"); + ConfigDictionary["Version"] = version; + } + + public DefaultSkinConfiguration() + : this("1") + { } } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 0b1076be01..0f80aade1e 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -38,7 +38,7 @@ namespace osu.Game.Skinning using (StreamReader reader = new StreamReader(stream)) Configuration = new LegacySkinDecoder().Decode(reader); else - Configuration = new DefaultSkinConfiguration(); + Configuration = new DefaultSkinConfiguration("latest"); if (storage != null) { From 3fe56117005f375a8dcbea223cd48a0365d0af85 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Thu, 3 Oct 2019 06:48:59 +0300 Subject: [PATCH 0054/6909] Retrieve numeric version value from legacy configuration --- .../Skinning/OsuLegacySkinTransformer.cs | 3 +-- osu.Game/Skinning/LegacySkin.cs | 13 +++++++++++++ osu.Game/Skinning/LegacySkinConfiguration.cs | 10 ++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Skinning/LegacySkinConfiguration.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index c9ed313593..539b3d7d34 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -125,8 +125,7 @@ namespace osu.Game.Rulesets.Osu.Skinning break; case OsuSkinConfiguration.ExpandNumberPiece: - string legacyVersion = source.GetConfig("Version")?.Value ?? "1"; - return SkinUtils.As(new BindableBool(double.TryParse(legacyVersion, out double version) && version < 2.0)); + return SkinUtils.As(new BindableBool(source.GetConfig(LegacySkinConfiguration.LegacyVersion).Value < 2.0)); } break; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 0f80aade1e..90265ec066 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -70,6 +70,19 @@ namespace osu.Game.Skinning case GlobalSkinColour colour: return SkinUtils.As(getCustomColour(colour.ToString())); + case LegacySkinConfiguration legacy: + switch (legacy) + { + case LegacySkinConfiguration.LegacyVersion: + var versionString = GetConfig("Version").Value; + if (!double.TryParse(versionString, out double version)) + version = versionString == "latest" ? 2.7 : 1; + + return SkinUtils.As(new BindableDouble(version)); + } + + break; + case SkinCustomColourLookup customColour: return SkinUtils.As(getCustomColour(customColour.Lookup.ToString())); diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs new file mode 100644 index 0000000000..2e75313d0c --- /dev/null +++ b/osu.Game/Skinning/LegacySkinConfiguration.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.Skinning +{ + public enum LegacySkinConfiguration + { + LegacyVersion, + } +} From dabc22403009b8d976ce4971c309ca4fea709cea Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Thu, 3 Oct 2019 06:49:32 +0300 Subject: [PATCH 0055/6909] Fix hit circle positioning --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 8ba21d9f89..705828131d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -56,7 +56,11 @@ namespace osu.Game.Rulesets.Osu.Skinning { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, - }, confineMode: ConfineMode.NoScaling), + }, confineMode: ConfineMode.NoScaling) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, hitCircleOverlay = new Sprite { Texture = skin.GetTexture("hitcircleoverlay"), From 2d7acef0800903e3d6f52ce98de724576f107bbf Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Thu, 3 Oct 2019 11:06:38 +0300 Subject: [PATCH 0056/6909] Fix CI issues --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 4 +--- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 3 ++- osu.Game/Skinning/LegacySkin.cs | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 705828131d..89c8ea9d6c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -10,7 +10,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -41,7 +40,6 @@ namespace osu.Game.Rulesets.Osu.Skinning private void load() { OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; - DrawableHitCircle drawableCircle = (DrawableHitCircle)drawableObject; InternalChildren = new Drawable[] { @@ -95,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - if (skin.GetConfig(OsuSkinConfiguration.ExpandNumberPiece).Value) + if (skin.GetConfig(OsuSkinConfiguration.ExpandNumberPiece)?.Value ?? true) hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); break; diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 539b3d7d34..1303e2cace 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -125,7 +125,8 @@ namespace osu.Game.Rulesets.Osu.Skinning break; case OsuSkinConfiguration.ExpandNumberPiece: - return SkinUtils.As(new BindableBool(source.GetConfig(LegacySkinConfiguration.LegacyVersion).Value < 2.0)); + double legacyVersion = source.GetConfig(LegacySkinConfiguration.LegacyVersion)?.Value ?? 1.0; + return SkinUtils.As(new BindableBool(legacyVersion < 2.0)); } break; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 90265ec066..af5309eecd 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -74,9 +74,9 @@ namespace osu.Game.Skinning switch (legacy) { case LegacySkinConfiguration.LegacyVersion: - var versionString = GetConfig("Version").Value; + var versionString = GetConfig("Version")?.Value ?? "1"; if (!double.TryParse(versionString, out double version)) - version = versionString == "latest" ? 2.7 : 1; + version = versionString == "latest" ? 2.7 : 1.0; return SkinUtils.As(new BindableDouble(version)); } From 5d2fe8733997295bbbecee0cdbc947440e305d06 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Mon, 14 Oct 2019 00:38:45 +0300 Subject: [PATCH 0057/6909] Use empty hit windows for spinner ticks --- osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index c2104e68ee..318e8e71a2 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -17,6 +17,6 @@ namespace osu.Game.Rulesets.Osu.Objects public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); - protected override HitWindows CreateHitWindows() => null; + protected override HitWindows CreateHitWindows() => HitWindows.Empty; } } From 68e370ce7cd72c51a7eda6f9863ed37b0f86b3d5 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Mon, 14 Oct 2019 00:39:20 +0300 Subject: [PATCH 0058/6909] Set spinner tick start time to allow result reverting --- .../Objects/Drawables/DrawableSpinnerTick.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 9c316591a9..21cf7b3acb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -44,6 +44,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Samples.AddAdjustment(AdjustableProperty.Volume, bonusSampleVolume); } - public void TriggerResult(HitResult result) => ApplyResult(r => r.Type = result); + public void TriggerResult(HitResult result) + { + HitObject.StartTime = Time.Current; + ApplyResult(r => r.Type = result); + } } } From a75ae14cb20efca1673d863001736361c29c07f8 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Mon, 14 Oct 2019 00:40:36 +0300 Subject: [PATCH 0059/6909] Use foreach loop to avoid too long lines --- .../Objects/Drawables/DrawableSpinner.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 08e64b7ecf..965303ba7a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Scoring; @@ -279,7 +278,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } if (state != ArmedState.Idle) - Schedule(() => NestedHitObjects.Where(t => !t.IsHit).OfType().ForEach(t => t.TriggerResult(HitResult.Miss))); + { + Schedule(() => + { + foreach (var tick in ticks.Where(t => !t.IsHit)) + tick.TriggerResult(HitResult.Miss); + }); + } } } } From a8514ecd0f220f39c214e13cd89409f1a6694c3e Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Mon, 14 Oct 2019 00:43:46 +0300 Subject: [PATCH 0060/6909] Add tests ensuring correct spinner ticks score results --- .../TestSceneSpinnerRotation.cs | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index cded7f0e95..b03788a7d6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -15,6 +15,8 @@ using osu.Game.Tests.Visual; using osuTK; using System.Collections.Generic; using System.Linq; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play; using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; namespace osu.Game.Rulesets.Osu.Tests @@ -28,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Tests protected override bool Autoplay => true; + protected override Player CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer(); + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap) { var working = new ClockBackedTestWorkingBeatmap(beatmap, new FramedClock(new ManualClock { Rate = 1 }), audioManager); @@ -69,6 +73,32 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("is rotation absolute almost same", () => Precision.AlmostEquals(drawableSpinner.Disc.RotationAbsolute, estimatedRotation, 100)); } + [Test] + public void TestSpinnerNormalBonusRewinding() + { + addSeekStep(1000); + + AddAssert("player score matching expected bonus score", () => + { + // multipled by 2 to nullify the score multiplier. (autoplay mod selected) + var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; + return totalScore == (int)(drawableSpinner.Disc.RotationAbsolute / 360) * 100; + }); + + addSeekStep(0); + + AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0); + } + + [Test] + public void TestSpinnerCompleteBonusRewinding() + { + addSeekStep(2500); + addSeekStep(0); + + AddAssert("player score is 0", () => ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value == 0); + } + private void addSeekStep(double time) { AddStep($"seek to {time}", () => track.Seek(time)); @@ -85,12 +115,17 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(256, 192), EndTime = 5000, }, - // placeholder object to avoid hitting the results screen - new HitObject - { - StartTime = 99999, - } } }; + + private class ScoreExposedPlayer : TestPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + public ScoreExposedPlayer() + : base(false, false) + { + } + } } } From f54eb448fa245c2fde8e5b23b3f526f25560122a Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 15 Oct 2019 22:00:34 +0300 Subject: [PATCH 0061/6909] Revert skin legacy version changes --- osu.Game/Skinning/DefaultLegacySkin.cs | 2 -- osu.Game/Skinning/DefaultSkinConfiguration.cs | 9 +-------- osu.Game/Skinning/LegacySkin.cs | 15 +-------------- osu.Game/Skinning/LegacySkinConfiguration.cs | 10 ---------- 4 files changed, 2 insertions(+), 34 deletions(-) delete mode 100644 osu.Game/Skinning/LegacySkinConfiguration.cs diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 8370b5e8ee..4b6eea6b6e 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -20,8 +20,6 @@ namespace osu.Game.Skinning new Color4(18, 124, 255, 255), new Color4(242, 24, 57, 255), }); - - Configuration.ConfigDictionary["Version"] = "2"; } public static SkinInfo Info { get; } = new SkinInfo diff --git a/osu.Game/Skinning/DefaultSkinConfiguration.cs b/osu.Game/Skinning/DefaultSkinConfiguration.cs index 8c87e1d054..cd5975edac 100644 --- a/osu.Game/Skinning/DefaultSkinConfiguration.cs +++ b/osu.Game/Skinning/DefaultSkinConfiguration.cs @@ -10,7 +10,7 @@ namespace osu.Game.Skinning /// public class DefaultSkinConfiguration : SkinConfiguration { - public DefaultSkinConfiguration(string version) + public DefaultSkinConfiguration() { ComboColours.AddRange(new[] { @@ -19,13 +19,6 @@ namespace osu.Game.Skinning new Color4(18, 124, 255, 255), new Color4(242, 24, 57, 255), }); - - ConfigDictionary["Version"] = version; - } - - public DefaultSkinConfiguration() - : this("1") - { } } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index cbebdd9bd6..fea15458e4 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -39,7 +39,7 @@ namespace osu.Game.Skinning using (LineBufferedReader reader = new LineBufferedReader(stream)) Configuration = new LegacySkinDecoder().Decode(reader); else - Configuration = new DefaultSkinConfiguration("latest"); + Configuration = new DefaultSkinConfiguration(); if (storage != null) { @@ -71,19 +71,6 @@ namespace osu.Game.Skinning case GlobalSkinColour colour: return SkinUtils.As(getCustomColour(colour.ToString())); - case LegacySkinConfiguration legacy: - switch (legacy) - { - case LegacySkinConfiguration.LegacyVersion: - var versionString = GetConfig("Version")?.Value ?? "1"; - if (!double.TryParse(versionString, out double version)) - version = versionString == "latest" ? 2.7 : 1.0; - - return SkinUtils.As(new BindableDouble(version)); - } - - break; - case SkinCustomColourLookup customColour: return SkinUtils.As(getCustomColour(customColour.Lookup.ToString())); diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs deleted file mode 100644 index 2e75313d0c..0000000000 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ /dev/null @@ -1,10 +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.Skinning -{ - public enum LegacySkinConfiguration - { - LegacyVersion, - } -} From 9dcbef49d300458c897f3f40c77c9dddaf3bb5b4 Mon Sep 17 00:00:00 2001 From: iiSaLMaN Date: Tue, 15 Oct 2019 22:28:50 +0300 Subject: [PATCH 0062/6909] Resolve DHO inside load() --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 89c8ea9d6c..f9e6400b18 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -30,14 +30,11 @@ namespace osu.Game.Rulesets.Osu.Skinning private readonly Bindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); - [Resolved] - private DrawableHitObject drawableObject { get; set; } - [Resolved] private ISkinSource skin { get; set; } [BackgroundDependencyLoader] - private void load() + private void load(DrawableHitObject drawableObject) { OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; From 10e1e512fd45abf199bea01c8d70ebfa2337df4c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 12 Dec 2019 15:15:16 +0300 Subject: [PATCH 0063/6909] Update the nested hitobject logic inline with new implementation --- .../Objects/Drawables/DrawableSpinner.cs | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 39330f08c3..2c21b4244a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -15,6 +15,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; @@ -131,16 +132,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Alpha = 0, } }; + } - foreach (var tick in Spinner.NestedHitObjects.OfType()) + protected override void AddNestedHitObject(DrawableHitObject hitObject) + { + base.AddNestedHitObject(hitObject); + + switch (hitObject) { - var drawableTick = new DrawableSpinnerTick(tick); - - ticks.Add(drawableTick); - AddNestedHitObject(drawableTick); + case DrawableSpinnerTick tick: + ticks.Add(tick); + break; } } + protected override void ClearNestedHitObjects() + { + base.ClearNestedHitObjects(); + ticks.Clear(); + } + + protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) + { + switch (hitObject) + { + case SpinnerTick tick: + return new DrawableSpinnerTick(tick); + } + + return base.CreateNestedHitObject(hitObject); + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { From d6fb2283385c9cec966d01dcf684cb402ab1e7e1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 12 Dec 2019 16:02:53 +0300 Subject: [PATCH 0064/6909] Update version retrieval logic in-line with new implementation --- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 1303e2cace..1c716b3ee9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Skinning; using osuTK; +using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Osu.Skinning { @@ -125,8 +126,8 @@ namespace osu.Game.Rulesets.Osu.Skinning break; case OsuSkinConfiguration.ExpandNumberPiece: - double legacyVersion = source.GetConfig(LegacySkinConfiguration.LegacyVersion)?.Value ?? 1.0; - return SkinUtils.As(new BindableBool(legacyVersion < 2.0)); + decimal legacyVersion = source.GetConfig(LegacySetting.Version)?.Value ?? 1.0m; + return SkinUtils.As(new BindableBool(legacyVersion < 2.0m)); } break; From 41ca084fa591253d6406253c76cef99cc1196305 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 17 Dec 2019 22:00:21 +0300 Subject: [PATCH 0065/6909] Simplify expand number check --- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 1c716b3ee9..5926332ea5 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -126,8 +126,8 @@ namespace osu.Game.Rulesets.Osu.Skinning break; case OsuSkinConfiguration.ExpandNumberPiece: - decimal legacyVersion = source.GetConfig(LegacySetting.Version)?.Value ?? 1.0m; - return SkinUtils.As(new BindableBool(legacyVersion < 2.0m)); + bool expand = source.GetConfig(LegacySetting.Version)?.Value < 2.0m; + return SkinUtils.As(new BindableBool(expand)); } break; From 121ce2c3df5b95f48fec3546caee2e0d257b1d8b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 19 Dec 2019 14:44:52 +0300 Subject: [PATCH 0066/6909] Fix checking for expand incorrectly --- 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 5926332ea5..3e41dc08d7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Skinning break; case OsuSkinConfiguration.ExpandNumberPiece: - bool expand = source.GetConfig(LegacySetting.Version)?.Value < 2.0m; + bool expand = !(source.GetConfig(LegacySetting.Version)?.Value >= 2.0m); return SkinUtils.As(new BindableBool(expand)); } From 949ab4e0d3889e4ea88850b49715c1e3f8cc46d2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 25 Dec 2019 05:34:12 +0300 Subject: [PATCH 0067/6909] Move spinner bonus scoring to it's own component class Also fixes counter rewinding issue and does optimizations. --- .../Objects/Drawables/DrawableSpinner.cs | 42 +-------- .../Objects/Drawables/DrawableSpinnerTick.cs | 10 ++- .../Drawables/Pieces/SpinnerBonusComponent.cs | 90 +++++++++++++++++++ 3 files changed, 100 insertions(+), 42 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index f7f4275d2a..86e8840425 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -26,11 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected readonly Spinner Spinner; private readonly Container ticks; - private readonly OsuSpriteText bonusCounter; public readonly SpinnerDisc Disc; public readonly SpinnerTicks Ticks; public readonly SpinnerSpmCounter SpmCounter; + private readonly SpinnerBonusComponent bonusComponent; private readonly Container mainContainer; @@ -123,13 +123,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Y = 120, Alpha = 0 }, - bonusCounter = new OsuSpriteText + bonusComponent = new SpinnerBonusComponent(this, ticks) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Y = -120, - Font = OsuFont.Numeric.With(size: 24), - Alpha = 0, } }; } @@ -226,8 +224,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.Update(); } - private int currentSpins; - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -235,30 +231,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables circle.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation; SpmCounter.SetRotation(Disc.RotationAbsolute); - - var newSpins = (int)(Disc.RotationAbsolute / 360) - currentSpins; - - for (int i = currentSpins; i < currentSpins + newSpins; i++) - { - if (i < 0 || i >= ticks.Count) - break; - - var tick = ticks[i]; - - tick.HasBonusPoints = Progress >= 1 && i > Spinner.SpinsRequired; - - if (tick.HasBonusPoints) - { - bonusCounter.Text = $"{(i - Spinner.SpinsRequired) * 1000}"; - bonusCounter - .FadeOutFromOne(1500) - .ScaleTo(1.5f).ScaleTo(1f, 1000, Easing.OutQuint); - } - - tick.TriggerResult(HitResult.Great); - } - - currentSpins += newSpins; + bonusComponent.UpdateRotation(Disc.RotationAbsolute); float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; Disc.ScaleTo(relativeCircleScale + (1 - relativeCircleScale) * Progress, 200, Easing.OutQuint); @@ -299,15 +272,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables sequence.ScaleTo(Scale * 0.8f, 320, Easing.In); break; } - - if (state != ArmedState.Idle) - { - Schedule(() => - { - foreach (var tick in ticks.Where(t => !t.IsHit)) - tick.TriggerResult(HitResult.Miss); - }); - } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 21cf7b3acb..6512a9526e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Whether this judgement has a bonus of 1,000 points additional to the numeric result. - /// Should be set when a spin occured after the spinner has completed. + /// Set when a spin occured after the spinner has completed. /// public bool HasBonusPoints { @@ -44,10 +44,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Samples.AddAdjustment(AdjustableProperty.Volume, bonusSampleVolume); } - public void TriggerResult(HitResult result) + /// + /// Apply a judgement result. + /// + /// Whether to apply a result, otherwise. + internal void TriggerResult(bool hit) { HitObject.StartTime = Time.Current; - ApplyResult(r => r.Type = result); + ApplyResult(r => r.Type = hit ? HitResult.Great : HitResult.Miss); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs new file mode 100644 index 0000000000..5c96751b3a --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.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.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + /// + /// A component that tracks spinner spins and add bonus score for it. + /// + public class SpinnerBonusComponent : CompositeDrawable + { + private readonly DrawableSpinner drawableSpinner; + private readonly Container ticks; + private readonly OsuSpriteText bonusCounter; + + public SpinnerBonusComponent(DrawableSpinner drawableSpinner, Container ticks) + { + this.drawableSpinner = drawableSpinner; + this.ticks = ticks; + + drawableSpinner.OnNewResult += onNewResult; + + AutoSizeAxes = Axes.Both; + InternalChild = bonusCounter = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Numeric.With(size: 24), + Alpha = 0, + }; + } + + private int currentSpins; + + public void UpdateRotation(double rotation) + { + if (ticks.Count == 0) + return; + + int spinsRequired = ((Spinner)drawableSpinner.HitObject).SpinsRequired; + + int newSpins = Math.Clamp((int)(rotation / 360), 0, ticks.Count - 1); + int direction = Math.Sign(newSpins - currentSpins); + + while (currentSpins != newSpins) + { + var tick = ticks[currentSpins]; + + if (direction >= 0) + { + tick.HasBonusPoints = currentSpins > spinsRequired; + tick.TriggerResult(true); + } + + if (tick.HasBonusPoints) + { + bonusCounter.Text = $"{1000 * (currentSpins - spinsRequired)}"; + bonusCounter.FadeOutFromOne(1500); + bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); + } + + currentSpins += direction; + } + } + + private void onNewResult(DrawableHitObject hitObject, JudgementResult result) + { + if (!result.HasResult || hitObject != drawableSpinner) + return; + + // Trigger a miss result for remaining ticks to avoid infinite gameplay. + foreach (var tick in ticks.Where(t => !t.IsHit)) + tick.TriggerResult(false); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + drawableSpinner.OnNewResult -= onNewResult; + } + } +} From b7565f5943f05247b6469491f052dd6287c95db3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 25 Dec 2019 05:36:58 +0300 Subject: [PATCH 0068/6909] Remove unnecessary using directive --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 86e8840425..edcaa947ac 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -14,7 +14,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; From d773eb2c22c804e97977298fb57bc3cd265a7530 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 5 Feb 2020 14:05:12 +0800 Subject: [PATCH 0069/6909] 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 0070/6909] 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 fa53bd96a0b308a52ce5aa43f8a58bfab894786b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 7 Feb 2020 23:14:46 +0300 Subject: [PATCH 0071/6909] Merge dependency --- osu.Game/Users/User.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 5d0ffd5a67..c573fdd089 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -203,6 +203,21 @@ namespace osu.Game.Users public int ID; } + [JsonProperty("monthly_playcounts")] + public UserHistoryCount[] MonthlyPlaycounts; + + [JsonProperty("replays_watched_counts")] + public UserHistoryCount[] ReplaysWatchedCounts; + + public class UserHistoryCount + { + [JsonProperty("start_date")] + public DateTime Date; + + [JsonProperty("count")] + public long Count; + } + public override string ToString() => Username; /// From 84b7dfb3d6d0031873ba4b964ca5bdf4a4aa9294 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 7 Feb 2020 23:26:35 +0300 Subject: [PATCH 0072/6909] Implement UserGraph component An abstraction for RankGraph --- .../Profile/Header/Components/RankGraph.cs | 281 +++--------------- osu.Game/Overlays/Profile/UserGraph.cs | 234 +++++++++++++++ 2 files changed, 270 insertions(+), 245 deletions(-) create mode 100644 osu.Game/Overlays/Profile/UserGraph.cs diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index ffc060b3f1..917b086f04 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -4,307 +4,98 @@ 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.Cursor; -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.Users; -using osuTK; namespace osu.Game.Overlays.Profile.Header.Components { - public class RankGraph : Container, IHasCustomTooltip + public class RankGraph : UserGraph { - private const float secondary_textsize = 13; - private const float padding = 10; - private const float fade_duration = 150; private const int ranked_days = 88; - private readonly RankChartLineGraph graph; - private readonly OsuSpriteText placeholder; - - private KeyValuePair[] ranks; - private int dayIndex; public readonly Bindable Statistics = new Bindable(); + private readonly OsuSpriteText placeholder; + public RankGraph() { - Padding = new MarginPadding { Vertical = padding }; - Children = new Drawable[] + Add(placeholder = new OsuSpriteText { - placeholder = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "No recent plays", - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular) - }, - graph = new RankChartLineGraph - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.Both, - Y = -secondary_textsize, - Alpha = 0, - } - }; + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "No recent plays", + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular) + }); - graph.OnBallMove += i => dayIndex = i; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - graph.LineColour = colours.Yellow; + Graph.Alpha = 0; } protected override void LoadComplete() { base.LoadComplete(); - Statistics.BindValueChanged(statistics => updateStatistics(statistics.NewValue), true); } private void updateStatistics(UserStatistics statistics) { - placeholder.FadeIn(fade_duration, Easing.Out); + placeholder.FadeIn(FADE_DURATION, Easing.Out); if (statistics?.Ranks.Global == null) { - graph.FadeOut(fade_duration, Easing.Out); - ranks = null; + Graph.FadeOut(FADE_DURATION, Easing.Out); + Data = null; return; } int[] userRanks = statistics.RankHistory?.Data ?? new[] { statistics.Ranks.Global.Value }; - ranks = userRanks.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); + Data = userRanks.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); - if (ranks.Length > 1) + if (Data.Length > 1) { - placeholder.FadeOut(fade_duration, Easing.Out); + placeholder.FadeOut(FADE_DURATION, Easing.Out); - graph.DefaultValueCount = ranks.Length; - graph.Values = ranks.Select(x => -MathF.Log(x.Value)); + Graph.DefaultValueCount = Data.Length; + Graph.Values = Data.Select(x => -MathF.Log(x.Value)); } - graph.FadeTo(ranks.Length > 1 ? 1 : 0, fade_duration, Easing.Out); + Graph.FadeTo(Data.Length > 1 ? 1 : 0, FADE_DURATION, Easing.Out); } - protected override bool OnHover(HoverEvent e) + protected override object GetTooltipContent() { - if (ranks?.Length > 1) - { - graph.UpdateBallPosition(e.MousePosition.X); - graph.ShowBar(); - } + if (Statistics.Value?.Ranks.Global == null) + return null; - return base.OnHover(e); + var days = ranked_days - Data[DataIndex].Key + 1; + + return new TooltipDisplayContent + { + Rank = $"#{Data[DataIndex].Value:#,##0}", + Time = days == 0 ? "now" : $"{days} days ago" + }; } - protected override bool OnMouseMove(MouseMoveEvent e) + protected override UserGraphTooltip GetTooltip() => new RankGraphTooltip(); + + private class RankGraphTooltip : UserGraphTooltip { - if (ranks?.Length > 1) - graph.UpdateBallPosition(e.MousePosition.X); - - return base.OnMouseMove(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - if (ranks?.Length > 1) - { - graph.HideBar(); - } - - base.OnHoverLost(e); - } - - private class RankChartLineGraph : LineGraph - { - private readonly CircularContainer movingBall; - private readonly Container bar; - private readonly Box ballBg; - private readonly Box line; - - public Action OnBallMove; - - public RankChartLineGraph() - { - Add(bar = new Container - { - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Alpha = 0, - RelativePositionAxes = Axes.Both, - Children = new Drawable[] - { - line = new Box - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Width = 1.5f, - }, - movingBall = new CircularContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - Size = new Vector2(18), - Masking = true, - BorderThickness = 4, - RelativePositionAxes = Axes.Y, - Child = ballBg = new Box { RelativeSizeAxes = Axes.Both } - } - } - }); - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider, OsuColour colours) - { - ballBg.Colour = colourProvider.Background5; - movingBall.BorderColour = line.Colour = colours.Yellow; - } - - public void UpdateBallPosition(float mouseXPosition) - { - const int duration = 200; - int index = calculateIndex(mouseXPosition); - Vector2 position = calculateBallPosition(index); - movingBall.MoveToY(position.Y, duration, Easing.OutQuint); - bar.MoveToX(position.X, duration, Easing.OutQuint); - OnBallMove.Invoke(index); - } - - public void ShowBar() => bar.FadeIn(fade_duration); - - public void HideBar() => bar.FadeOut(fade_duration); - - private int calculateIndex(float mouseXPosition) => (int)MathF.Round(mouseXPosition / DrawWidth * (DefaultValueCount - 1)); - - private Vector2 calculateBallPosition(int index) - { - float y = GetYPosition(Values.ElementAt(index)); - return new Vector2(index / (float)(DefaultValueCount - 1), y); - } - } - - public object TooltipContent - { - get - { - if (Statistics.Value?.Ranks.Global == null) - return null; - - var days = ranked_days - ranks[dayIndex].Key + 1; - - return new TooltipDisplayContent - { - Rank = $"#{ranks[dayIndex].Value:#,##0}", - Time = days == 0 ? "now" : $"{days} days ago" - }; - } - } - - public ITooltip GetCustomTooltip() => new RankGraphTooltip(); - - private class RankGraphTooltip : VisibilityContainer, ITooltip - { - private readonly OsuSpriteText globalRankingText, timeText; - private readonly Box background; - public RankGraphTooltip() + : base(@"Global Ranking") { - AutoSizeAxes = Axes.Both; - Masking = true; - CornerRadius = 10; - - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Padding = new MarginPadding(10), - Children = new Drawable[] - { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = "Global Ranking " - }, - globalRankingText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - } - } - }, - timeText = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), - } - } - } - }; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - // Temporary colour since it's currently impossible to change it without bugs (see https://github.com/ppy/osu-framework/issues/3231) - // If above is fixed, this should use OverlayColourProvider - background.Colour = colours.Gray1; - } - - public bool SetContent(object content) + public override bool SetContent(object content) { if (!(content is TooltipDisplayContent info)) return false; - globalRankingText.Text = info.Rank; - timeText.Text = info.Time; + Counter.Text = info.Rank; + BottomText.Text = info.Time; return true; } - - private bool instantMove = true; - - public void Move(Vector2 pos) - { - if (instantMove) - { - Position = pos; - instantMove = false; - } - else - this.MoveTo(pos, 200, Easing.OutQuint); - } - - protected override void PopIn() - { - instantMove |= !IsPresent; - this.FadeIn(200, Easing.OutQuint); - } - - protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); } private class TooltipDisplayContent diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs new file mode 100644 index 0000000000..d0816fd4c6 --- /dev/null +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -0,0 +1,234 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Overlays.Profile +{ + public abstract class UserGraph : Container, IHasCustomTooltip + { + protected const float FADE_DURATION = 150; + + protected readonly RankChartLineGraph Graph; + protected KeyValuePair[] Data; + protected int DataIndex; + + protected UserGraph() + { + Add(Graph = new RankChartLineGraph + { + RelativeSizeAxes = Axes.Both, + }); + + Graph.OnBallMove += i => DataIndex = i; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Graph.LineColour = colours.Yellow; + } + + protected override bool OnHover(HoverEvent e) + { + if (Data?.Length > 1) + { + Graph.UpdateBallPosition(e.MousePosition.X); + Graph.ShowBar(); + } + + return base.OnHover(e); + } + + protected override bool OnMouseMove(MouseMoveEvent e) + { + if (Data?.Length > 1) + Graph.UpdateBallPosition(e.MousePosition.X); + + return base.OnMouseMove(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + if (Data?.Length > 1) + Graph.HideBar(); + + base.OnHoverLost(e); + } + + public ITooltip GetCustomTooltip() => GetTooltip(); + + public object TooltipContent => GetTooltipContent(); + + protected abstract UserGraphTooltip GetTooltip(); + + protected abstract object GetTooltipContent(); + + protected class RankChartLineGraph : LineGraph + { + private readonly CircularContainer movingBall; + private readonly Container bar; + private readonly Box ballBg; + private readonly Box line; + + public Action OnBallMove; + + public RankChartLineGraph() + { + Add(bar = new Container + { + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Alpha = 0, + RelativePositionAxes = Axes.Both, + Children = new Drawable[] + { + line = new Box + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Width = 1.5f, + }, + movingBall = new CircularContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Size = new Vector2(18), + Masking = true, + BorderThickness = 4, + RelativePositionAxes = Axes.Y, + Child = ballBg = new Box { RelativeSizeAxes = Axes.Both } + } + } + }); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, OsuColour colours) + { + ballBg.Colour = colourProvider.Background5; + movingBall.BorderColour = line.Colour = colours.Yellow; + } + + public void UpdateBallPosition(float mouseXPosition) + { + const int duration = 200; + int index = calculateIndex(mouseXPosition); + Vector2 position = calculateBallPosition(index); + movingBall.MoveToY(position.Y, duration, Easing.OutQuint); + bar.MoveToX(position.X, duration, Easing.OutQuint); + OnBallMove.Invoke(index); + } + + public void ShowBar() => bar.FadeIn(FADE_DURATION); + + public void HideBar() => bar.FadeOut(FADE_DURATION); + + private int calculateIndex(float mouseXPosition) => (int)MathF.Round(mouseXPosition / DrawWidth * (DefaultValueCount - 1)); + + private Vector2 calculateBallPosition(int index) + { + float y = GetYPosition(Values.ElementAt(index)); + return new Vector2(index / (float)(DefaultValueCount - 1), y); + } + } + + protected abstract class UserGraphTooltip : VisibilityContainer, ITooltip + { + protected readonly OsuSpriteText Counter, BottomText; + private readonly Box background; + + protected UserGraphTooltip(string topText) + { + AutoSizeAxes = Axes.Both; + Masking = true; + CornerRadius = 10; + + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Text = $"{topText} " + }, + Counter = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + } + } + }, + BottomText = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + // Temporary colour since it's currently impossible to change it without bugs (see https://github.com/ppy/osu-framework/issues/3231) + // If above is fixed, this should use OverlayColourProvider + background.Colour = colours.Gray1; + } + + public abstract bool SetContent(object content); + + private bool instantMove = true; + + public void Move(Vector2 pos) + { + if (instantMove) + { + Position = pos; + instantMove = false; + } + else + this.MoveTo(pos, 200, Easing.OutQuint); + } + + protected override void PopIn() + { + instantMove |= !IsPresent; + this.FadeIn(200, Easing.OutQuint); + } + + protected override void PopOut() => this.FadeOut(200, Easing.OutQuint); + } + } +} From b325725c4533c93e4fb7ebac7b864df82cf49db5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 8 Feb 2020 00:10:17 +0300 Subject: [PATCH 0073/6909] Implement UserHistoryGraph component --- .../Online/TestSceneUserHistoryGraph.cs | 112 ++++++++++++++++++ .../Profile/Header/Components/RankGraph.cs | 2 - .../Sections/Historical/UserHistoryGraph.cs | 90 ++++++++++++++ osu.Game/Overlays/Profile/UserGraph.cs | 1 + 4 files changed, 203 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs create mode 100644 osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs new file mode 100644 index 0000000000..88bb002fc2 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.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.Collections.Generic; +using osu.Game.Overlays.Profile.Sections.Historical; +using osu.Game.Overlays.Profile; +using osu.Framework.Graphics; +using static osu.Game.Users.User; +using osu.Game.Overlays; +using osu.Framework.Allocation; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneUserHistoryGraph : OsuTestScene + { + public override IReadOnlyList RequiredTypes => new[] + { + typeof(UserHistoryGraph), + typeof(UserGraph<,>), + }; + + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + public TestSceneUserHistoryGraph() + { + UserHistoryGraph graph; + + Add(graph = new UserHistoryGraph("Counter Name") + { + RelativeSizeAxes = Axes.X, + Height = 200, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + + var values = new[] + { + new UserHistoryCount + { + Date = new DateTime(2000, 1, 1), + Count = 10, + }, + new UserHistoryCount + { + Date = new DateTime(2000, 2, 1), + Count = 20, + }, + new UserHistoryCount + { + Date = new DateTime(2000, 3, 1), + Count = 100, + }, + new UserHistoryCount + { + Date = new DateTime(2000, 4, 1), + Count = 15, + }, + new UserHistoryCount + { + Date = new DateTime(2000, 5, 1), + Count = 30, + } + }; + + var moreValues = new[] + { + new UserHistoryCount + { + Date = new DateTime(2010, 5, 1), + Count = 1000, + }, + new UserHistoryCount + { + Date = new DateTime(2010, 6, 1), + Count = 20, + }, + new UserHistoryCount + { + Date = new DateTime(2010, 7, 1), + Count = 20000, + }, + new UserHistoryCount + { + Date = new DateTime(2010, 8, 1), + Count = 30, + }, + new UserHistoryCount + { + Date = new DateTime(2010, 9, 1), + Count = 50, + }, + new UserHistoryCount + { + Date = new DateTime(2010, 10, 1), + Count = 2000, + }, + new UserHistoryCount + { + Date = new DateTime(2010, 11, 1), + Count = 2100, + } + }; + + AddStep("Set fake values", () => graph.Values = values); + AddStep("Set more values", () => graph.Values = moreValues); + AddStep("Set null values", () => graph.Values = null); + AddStep("Set empty values", () => graph.Values = Array.Empty()); + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index 917b086f04..2a571e46d1 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -29,8 +29,6 @@ namespace osu.Game.Overlays.Profile.Header.Components Text = "No recent plays", Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular) }); - - Graph.Alpha = 0; } protected override void LoadComplete() diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs new file mode 100644 index 0000000000..5129ce872f --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.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 osu.Framework.Graphics; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class UserHistoryGraph : UserGraph + { + private UserHistoryCount[] values; + + public UserHistoryCount[] Values + { + get => values; + set + { + values = value; + updateValues(value); + } + } + + private readonly string tooltipCounterName; + + public UserHistoryGraph(string tooltipCounterName) + { + this.tooltipCounterName = tooltipCounterName; + } + + private void updateValues(UserHistoryCount[] values) + { + if (values == null || !values.Any()) + { + Graph.FadeOut(FADE_DURATION, Easing.Out); + Data = null; + return; + } + + Data = values.Select(v => new KeyValuePair(v.Date, v.Count)).ToArray(); + + if (values.Length > 1) + { + Graph.DefaultValueCount = Data.Length; + Graph.Values = Data.Select(x => (float)x.Value); + Graph.FadeIn(FADE_DURATION, Easing.Out); + } + } + + protected override object GetTooltipContent() + { + if (!Data?.Any() ?? true) + return null; + + return new TooltipDisplayContent + { + Count = Data[DataIndex].Value.ToString("N0"), + Date = Data[DataIndex].Key.ToString("MMMM yyyy") + }; + } + + protected override UserGraphTooltip GetTooltip() => new HistoryGraphTooltip(tooltipCounterName); + + private class HistoryGraphTooltip : UserGraphTooltip + { + public HistoryGraphTooltip(string topText) + : base(topText) + { + } + + public override bool SetContent(object content) + { + if (!(content is TooltipDisplayContent info)) + return false; + + Counter.Text = info.Count; + BottomText.Text = info.Date; + return true; + } + } + + private class TooltipDisplayContent + { + public string Count; + public string Date; + } + } +} diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index d0816fd4c6..13ea347032 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -30,6 +30,7 @@ namespace osu.Game.Overlays.Profile Add(Graph = new RankChartLineGraph { RelativeSizeAxes = Axes.Both, + Alpha = 0 }); Graph.OnBallMove += i => DataIndex = i; From 5a6a77b609dcf4e9b10fd9c6975b5a8ac964d1f0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 8 Feb 2020 00:13:26 +0300 Subject: [PATCH 0074/6909] Fix usings order --- osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs index 88bb002fc2..bf77e5d60a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs @@ -6,9 +6,9 @@ using System.Collections.Generic; using osu.Game.Overlays.Profile.Sections.Historical; using osu.Game.Overlays.Profile; using osu.Framework.Graphics; -using static osu.Game.Users.User; using osu.Game.Overlays; using osu.Framework.Allocation; +using static osu.Game.Users.User; namespace osu.Game.Tests.Visual.Online { From 25a930c43877007279a2503e34b4fa7702860f21 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 8 Feb 2020 08:59:35 +0800 Subject: [PATCH 0075/6909] 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 0076/6909] 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 0077/6909] 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 0078/6909] 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 0079/6909] 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 0080/6909] 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 0081/6909] 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 0082/6909] 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 0083/6909] 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 0084/6909] 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 0085/6909] 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 9e5da60614f150af5077da96db092134fa7c136a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 9 Feb 2020 00:28:38 +0300 Subject: [PATCH 0086/6909] Rename RankChartLineGraph to UserLineGraph --- osu.Game/Overlays/Profile/UserGraph.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index 13ea347032..f3d4f824f7 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -21,13 +21,13 @@ namespace osu.Game.Overlays.Profile { protected const float FADE_DURATION = 150; - protected readonly RankChartLineGraph Graph; + protected readonly UserLineGraph Graph; protected KeyValuePair[] Data; protected int DataIndex; protected UserGraph() { - Add(Graph = new RankChartLineGraph + Add(Graph = new UserLineGraph { RelativeSizeAxes = Axes.Both, Alpha = 0 @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Profile protected abstract object GetTooltipContent(); - protected class RankChartLineGraph : LineGraph + protected class UserLineGraph : LineGraph { private readonly CircularContainer movingBall; private readonly Container bar; @@ -86,7 +86,7 @@ namespace osu.Game.Overlays.Profile public Action OnBallMove; - public RankChartLineGraph() + public UserLineGraph() { Add(bar = new Container { From e2ecef732cfafa97619e2918572c06b6f5a536ed Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 9 Feb 2020 00:36:41 +0300 Subject: [PATCH 0087/6909] Make TooltipCounterName an abstract property --- .../Visual/Online/TestSceneUserHistoryGraph.cs | 14 ++++++++++++-- .../Profile/Header/Components/RankGraph.cs | 5 +---- .../Sections/Historical/UserHistoryGraph.cs | 18 ++---------------- osu.Game/Overlays/Profile/UserGraph.cs | 6 ++++-- 4 files changed, 19 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs index bf77e5d60a..164d719a00 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs @@ -25,9 +25,9 @@ namespace osu.Game.Tests.Visual.Online public TestSceneUserHistoryGraph() { - UserHistoryGraph graph; + TestGraph graph; - Add(graph = new UserHistoryGraph("Counter Name") + Add(graph = new TestGraph { RelativeSizeAxes = Axes.X, Height = 200, @@ -108,5 +108,15 @@ namespace osu.Game.Tests.Visual.Online AddStep("Set null values", () => graph.Values = null); AddStep("Set empty values", () => graph.Values = Array.Empty()); } + + private class TestGraph : UserHistoryGraph + { + protected override UserGraphTooltip GetTooltip() => new TestTooltip(); + + private class TestTooltip : HistoryGraphTooltip + { + protected override string TooltipCounterName => "Test Counter"; + } + } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index 2a571e46d1..b62864364b 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -80,10 +80,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private class RankGraphTooltip : UserGraphTooltip { - public RankGraphTooltip() - : base(@"Global Ranking") - { - } + protected override string TooltipCounterName => @"Global Ranking"; public override bool SetContent(object content) { diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index 5129ce872f..2e389d3ac6 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -9,7 +9,7 @@ using static osu.Game.Users.User; namespace osu.Game.Overlays.Profile.Sections.Historical { - public class UserHistoryGraph : UserGraph + public abstract class UserHistoryGraph : UserGraph { private UserHistoryCount[] values; @@ -23,13 +23,6 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } } - private readonly string tooltipCounterName; - - public UserHistoryGraph(string tooltipCounterName) - { - this.tooltipCounterName = tooltipCounterName; - } - private void updateValues(UserHistoryCount[] values) { if (values == null || !values.Any()) @@ -61,15 +54,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical }; } - protected override UserGraphTooltip GetTooltip() => new HistoryGraphTooltip(tooltipCounterName); - - private class HistoryGraphTooltip : UserGraphTooltip + protected abstract class HistoryGraphTooltip : UserGraphTooltip { - public HistoryGraphTooltip(string topText) - : base(topText) - { - } - public override bool SetContent(object content) { if (!(content is TooltipDisplayContent info)) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index f3d4f824f7..122a6ded36 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -153,7 +153,9 @@ namespace osu.Game.Overlays.Profile protected readonly OsuSpriteText Counter, BottomText; private readonly Box background; - protected UserGraphTooltip(string topText) + protected abstract string TooltipCounterName { get; } + + protected UserGraphTooltip() { AutoSizeAxes = Axes.Both; Masking = true; @@ -181,7 +183,7 @@ namespace osu.Game.Overlays.Profile new OsuSpriteText { Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = $"{topText} " + Text = $"{TooltipCounterName} " }, Counter = new OsuSpriteText { From 8e20e641f440d4a2dff2c49b60816b0021a1cd55 Mon Sep 17 00:00:00 2001 From: mcendu Date: Sun, 9 Feb 2020 13:33:43 +0800 Subject: [PATCH 0088/6909] 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 0089/6909] 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 0090/6909] 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 0091/6909] 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 0092/6909] 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 0093/6909] 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 0094/6909] 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 0095/6909] 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 0096/6909] 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 0097/6909] 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 0098/6909] 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 2b0bdd1db5deee1efcae3d7388aae3c4a03e2d76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2020 19:15:37 +0100 Subject: [PATCH 0099/6909] Refactor tooltip construction --- .../Profile/Header/Components/RankGraph.cs | 9 +++------ .../Sections/Historical/UserHistoryGraph.cs | 9 +++------ osu.Game/Overlays/Profile/UserGraph.cs | 20 ++++++++++++++----- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index b62864364b..097b68f3aa 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -62,16 +62,13 @@ namespace osu.Game.Overlays.Profile.Header.Components Graph.FadeTo(Data.Length > 1 ? 1 : 0, FADE_DURATION, Easing.Out); } - protected override object GetTooltipContent() + protected override object GetTooltipContent(int index, int rank) { - if (Statistics.Value?.Ranks.Global == null) - return null; - - var days = ranked_days - Data[DataIndex].Key + 1; + var days = ranked_days - index + 1; return new TooltipDisplayContent { - Rank = $"#{Data[DataIndex].Value:#,##0}", + Rank = $"#{rank:#,##0}", Time = days == 0 ? "now" : $"{days} days ago" }; } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index 2e389d3ac6..d37454d607 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -42,15 +42,12 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } } - protected override object GetTooltipContent() + protected override object GetTooltipContent(DateTime date, long playCount) { - if (!Data?.Any() ?? true) - return null; - return new TooltipDisplayContent { - Count = Data[DataIndex].Value.ToString("N0"), - Date = Data[DataIndex].Key.ToString("MMMM yyyy") + Count = playCount.ToString("N0"), + Date = date.ToString("MMMM yyyy") }; } diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index 122a6ded36..f1f5b50cd3 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Profile protected readonly UserLineGraph Graph; protected KeyValuePair[] Data; - protected int DataIndex; + private int dataIndex; protected UserGraph() { @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Profile Alpha = 0 }); - Graph.OnBallMove += i => DataIndex = i; + Graph.OnBallMove += i => dataIndex = i; } [BackgroundDependencyLoader] @@ -71,11 +71,21 @@ namespace osu.Game.Overlays.Profile public ITooltip GetCustomTooltip() => GetTooltip(); - public object TooltipContent => GetTooltipContent(); - protected abstract UserGraphTooltip GetTooltip(); - protected abstract object GetTooltipContent(); + public object TooltipContent + { + get + { + if (Data == null || Data.Length == 0) + return null; + + var (key, value) = Data[dataIndex]; + return GetTooltipContent(key, value); + } + } + + protected abstract object GetTooltipContent(TKey key, TValue value); protected class UserLineGraph : LineGraph { From 9edddbaf4691ee04f2b8af0a9263dc673c1072ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2020 20:19:20 +0100 Subject: [PATCH 0100/6909] Encapsulate base graph further --- .../Profile/Header/Components/RankGraph.cs | 24 ++++---- .../Sections/Historical/UserHistoryGraph.cs | 32 +++------- osu.Game/Overlays/Profile/UserGraph.cs | 61 ++++++++++++++----- 3 files changed, 66 insertions(+), 51 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index 097b68f3aa..77edfe2746 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -39,27 +39,29 @@ namespace osu.Game.Overlays.Profile.Header.Components private void updateStatistics(UserStatistics statistics) { - placeholder.FadeIn(FADE_DURATION, Easing.Out); + int[] userRanks = statistics?.RankHistory?.Data; - if (statistics?.Ranks.Global == null) + if (userRanks == null) { - Graph.FadeOut(FADE_DURATION, Easing.Out); Data = null; return; } - int[] userRanks = statistics.RankHistory?.Data ?? new[] { statistics.Ranks.Global.Value }; Data = userRanks.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); + } - if (Data.Length > 1) - { - placeholder.FadeOut(FADE_DURATION, Easing.Out); + protected override float GetDataPointHeight(int rank) => -MathF.Log(rank); - Graph.DefaultValueCount = Data.Length; - Graph.Values = Data.Select(x => -MathF.Log(x.Value)); - } + protected override void ShowGraph() + { + base.ShowGraph(); + placeholder.FadeOut(FADE_DURATION, Easing.Out); + } - Graph.FadeTo(Data.Length > 1 ? 1 : 0, FADE_DURATION, Easing.Out); + protected override void HideGraph() + { + base.HideGraph(); + placeholder.FadeIn(FADE_DURATION, Easing.Out); } protected override object GetTooltipContent(int index, int rank) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index d37454d607..ccc286d423 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -4,43 +4,27 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics; using static osu.Game.Users.User; namespace osu.Game.Overlays.Profile.Sections.Historical { public abstract class UserHistoryGraph : UserGraph { - private UserHistoryCount[] values; - public UserHistoryCount[] Values { - get => values; set { - values = value; - updateValues(value); + if (value == null) + { + Data = null; + return; + } + + Data = value.Select(v => new KeyValuePair(v.Date, v.Count)).ToArray(); } } - private void updateValues(UserHistoryCount[] values) - { - if (values == null || !values.Any()) - { - Graph.FadeOut(FADE_DURATION, Easing.Out); - Data = null; - return; - } - - Data = values.Select(v => new KeyValuePair(v.Date, v.Count)).ToArray(); - - if (values.Length > 1) - { - Graph.DefaultValueCount = Data.Length; - Graph.Values = Data.Select(x => (float)x.Value); - Graph.FadeIn(FADE_DURATION, Easing.Out); - } - } + protected override float GetDataPointHeight(long playCount) => playCount; protected override object GetTooltipContent(DateTime date, long playCount) { diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index f1f5b50cd3..86e405c8f0 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -21,54 +21,83 @@ namespace osu.Game.Overlays.Profile { protected const float FADE_DURATION = 150; - protected readonly UserLineGraph Graph; - protected KeyValuePair[] Data; + private readonly UserLineGraph graph; + private KeyValuePair[] data; private int dataIndex; protected UserGraph() { - Add(Graph = new UserLineGraph + data = Array.Empty>(); + + Add(graph = new UserLineGraph { RelativeSizeAxes = Axes.Both, Alpha = 0 }); - Graph.OnBallMove += i => dataIndex = i; + graph.OnBallMove += i => dataIndex = i; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - Graph.LineColour = colours.Yellow; + graph.LineColour = colours.Yellow; } protected override bool OnHover(HoverEvent e) { - if (Data?.Length > 1) - { - Graph.UpdateBallPosition(e.MousePosition.X); - Graph.ShowBar(); - } + if (data.Length <= 1) + return base.OnHover(e); + + graph.UpdateBallPosition(e.MousePosition.X); + graph.ShowBar(); return base.OnHover(e); } protected override bool OnMouseMove(MouseMoveEvent e) { - if (Data?.Length > 1) - Graph.UpdateBallPosition(e.MousePosition.X); + if (data.Length > 1) + graph.UpdateBallPosition(e.MousePosition.X); return base.OnMouseMove(e); } protected override void OnHoverLost(HoverLostEvent e) { - if (Data?.Length > 1) - Graph.HideBar(); + if (data.Length > 1) + graph.HideBar(); base.OnHoverLost(e); } + protected KeyValuePair[] Data + { + set + { + value ??= Array.Empty>(); + data = value; + redrawGraph(); + } + } + + private void redrawGraph() + { + if (data.Length == 0) + { + HideGraph(); + return; + } + + graph.DefaultValueCount = data.Length; + graph.Values = data.Select(pair => GetDataPointHeight(pair.Value)).ToArray(); + ShowGraph(); + } + + protected abstract float GetDataPointHeight(TValue value); + protected virtual void ShowGraph() => graph.FadeIn(FADE_DURATION, Easing.Out); + protected virtual void HideGraph() => graph.FadeOut(FADE_DURATION, Easing.Out); + public ITooltip GetCustomTooltip() => GetTooltip(); protected abstract UserGraphTooltip GetTooltip(); @@ -77,10 +106,10 @@ namespace osu.Game.Overlays.Profile { get { - if (Data == null || Data.Length == 0) + if (data.Length == 0) return null; - var (key, value) = Data[dataIndex]; + var (key, value) = data[dataIndex]; return GetTooltipContent(key, value); } } From 60a1dad67d8f51ce21e5bd40cc64792043e6a18e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 12 Feb 2020 20:35:31 +0100 Subject: [PATCH 0101/6909] Explicitly handle hover event --- osu.Game/Overlays/Profile/UserGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index 86e405c8f0..de3d91f088 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Profile graph.UpdateBallPosition(e.MousePosition.X); graph.ShowBar(); - return base.OnHover(e); + return true; } protected override bool OnMouseMove(MouseMoveEvent e) 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 0102/6909] 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 0103/6909] 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 0104/6909] 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 0105/6909] 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 0106/6909] 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 0107/6909] 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 0108/6909] 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 0109/6909] 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 0110/6909] 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 e2ea92e21f7d3a6fad4459a8cfdaadbbcfead410 Mon Sep 17 00:00:00 2001 From: mcendu Date: Wed, 4 Mar 2020 13:51:12 +0800 Subject: [PATCH 0111/6909] 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 d3937acfe9f8ed0d7ca91fbbeae293761ccddcfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Mar 2020 20:11:14 +0100 Subject: [PATCH 0112/6909] Fix rank graph tooltip display --- osu.Game/Overlays/Profile/Header/Components/RankGraph.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index 77edfe2746..39fa0ca251 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Humanizer; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics; @@ -70,8 +71,8 @@ namespace osu.Game.Overlays.Profile.Header.Components return new TooltipDisplayContent { - Rank = $"#{rank:#,##0}", - Time = days == 0 ? "now" : $"{days} days ago" + Rank = $"#{rank:N0}", + Time = days == 0 ? "now" : $"{"day".ToQuantity(days)} ago" }; } From b1de47a6afb6089c31f047b95414b7b11c3a1537 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 5 Mar 2020 20:34:33 +0100 Subject: [PATCH 0113/6909] Adjust graph sizings to match web --- osu.Game/Overlays/Profile/UserGraph.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index de3d91f088..48671b8a70 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -141,13 +141,13 @@ namespace osu.Game.Overlays.Profile Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, - Width = 1.5f, + Width = 2, }, movingBall = new CircularContainer { Anchor = Anchor.TopCentre, Origin = Anchor.Centre, - Size = new Vector2(18), + Size = new Vector2(20), Masking = true, BorderThickness = 4, RelativePositionAxes = Axes.Y, From 1318f242c1cdd710b52ace6de9ce881ec24cb1fb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 6 Mar 2020 02:12:30 +0300 Subject: [PATCH 0114/6909] 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 0115/6909] 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 b77bd08925461bcbd8b24129570c8cdae17c6fc0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 9 Mar 2020 19:20:06 +0300 Subject: [PATCH 0116/6909] Simplify null values handling --- .../Profile/Sections/Historical/UserHistoryGraph.cs | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index ccc286d423..6de1b8e0f0 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -12,16 +12,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { public UserHistoryCount[] Values { - set - { - if (value == null) - { - Data = null; - return; - } - - Data = value.Select(v => new KeyValuePair(v.Date, v.Count)).ToArray(); - } + set => Data = value?.Select(v => new KeyValuePair(v.Date, v.Count)).ToArray(); } protected override float GetDataPointHeight(long playCount) => playCount; From bea2b7094879d36165f179ae130c482b64bf3b02 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 9 Mar 2020 19:22:03 +0300 Subject: [PATCH 0117/6909] Adjust OnHover syntax --- osu.Game/Overlays/Profile/UserGraph.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index 48671b8a70..e2db79024c 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -46,13 +46,15 @@ namespace osu.Game.Overlays.Profile protected override bool OnHover(HoverEvent e) { - if (data.Length <= 1) - return base.OnHover(e); + if (data.Length > 1) + { + graph.UpdateBallPosition(e.MousePosition.X); + graph.ShowBar(); - graph.UpdateBallPosition(e.MousePosition.X); - graph.ShowBar(); + return true; + } - return true; + return base.OnHover(e); } protected override bool OnMouseMove(MouseMoveEvent e) From 432c52bf276747248acc91e8e155a36294ed6a10 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 9 Mar 2020 19:26:15 +0300 Subject: [PATCH 0118/6909] Simplify test scene --- .../Online/TestSceneUserHistoryGraph.cs | 72 ++++--------------- 1 file changed, 12 insertions(+), 60 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs index 164d719a00..26f6ac199b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs @@ -37,70 +37,22 @@ namespace osu.Game.Tests.Visual.Online var values = new[] { - new UserHistoryCount - { - Date = new DateTime(2000, 1, 1), - Count = 10, - }, - new UserHistoryCount - { - Date = new DateTime(2000, 2, 1), - Count = 20, - }, - new UserHistoryCount - { - Date = new DateTime(2000, 3, 1), - Count = 100, - }, - new UserHistoryCount - { - Date = new DateTime(2000, 4, 1), - Count = 15, - }, - new UserHistoryCount - { - Date = new DateTime(2000, 5, 1), - Count = 30, - } + new UserHistoryCount { Date = new DateTime(2000, 1, 1), Count = 10 }, + new UserHistoryCount { Date = new DateTime(2000, 2, 1), Count = 20 }, + new UserHistoryCount { Date = new DateTime(2000, 3, 1), Count = 100 }, + new UserHistoryCount { Date = new DateTime(2000, 4, 1), Count = 15 }, + new UserHistoryCount { Date = new DateTime(2000, 5, 1), Count = 30 } }; var moreValues = new[] { - new UserHistoryCount - { - Date = new DateTime(2010, 5, 1), - Count = 1000, - }, - new UserHistoryCount - { - Date = new DateTime(2010, 6, 1), - Count = 20, - }, - new UserHistoryCount - { - Date = new DateTime(2010, 7, 1), - Count = 20000, - }, - new UserHistoryCount - { - Date = new DateTime(2010, 8, 1), - Count = 30, - }, - new UserHistoryCount - { - Date = new DateTime(2010, 9, 1), - Count = 50, - }, - new UserHistoryCount - { - Date = new DateTime(2010, 10, 1), - Count = 2000, - }, - new UserHistoryCount - { - Date = new DateTime(2010, 11, 1), - Count = 2100, - } + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1000 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 20 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 20000 }, + new UserHistoryCount { Date = new DateTime(2010, 8, 1), Count = 30 }, + new UserHistoryCount { Date = new DateTime(2010, 9, 1), Count = 50 }, + new UserHistoryCount { Date = new DateTime(2010, 10, 1), Count = 2000 }, + new UserHistoryCount { Date = new DateTime(2010, 11, 1), Count = 2100 } }; AddStep("Set fake values", () => graph.Values = values); From 06855c09c741bba8dd1bc0696c5bfccd08273621 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 9 Mar 2020 19:42:35 +0300 Subject: [PATCH 0119/6909] Make data nullable --- .../Overlays/Profile/Header/Components/RankGraph.cs | 8 +------- osu.Game/Overlays/Profile/UserGraph.cs | 12 +++++------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index 39fa0ca251..13fbaa7f85 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -42,13 +42,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { int[] userRanks = statistics?.RankHistory?.Data; - if (userRanks == null) - { - Data = null; - return; - } - - Data = userRanks.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); + Data = userRanks?.Select((x, index) => new KeyValuePair(index, x)).Where(x => x.Value != 0).ToArray(); } protected override float GetDataPointHeight(int rank) => -MathF.Log(rank); diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index e2db79024c..64f988b0c1 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -27,8 +27,6 @@ namespace osu.Game.Overlays.Profile protected UserGraph() { - data = Array.Empty>(); - Add(graph = new UserLineGraph { RelativeSizeAxes = Axes.Both, @@ -46,7 +44,7 @@ namespace osu.Game.Overlays.Profile protected override bool OnHover(HoverEvent e) { - if (data.Length > 1) + if (data?.Length > 1) { graph.UpdateBallPosition(e.MousePosition.X); graph.ShowBar(); @@ -59,7 +57,7 @@ namespace osu.Game.Overlays.Profile protected override bool OnMouseMove(MouseMoveEvent e) { - if (data.Length > 1) + if (data?.Length > 1) graph.UpdateBallPosition(e.MousePosition.X); return base.OnMouseMove(e); @@ -67,7 +65,7 @@ namespace osu.Game.Overlays.Profile protected override void OnHoverLost(HoverLostEvent e) { - if (data.Length > 1) + if (data?.Length > 1) graph.HideBar(); base.OnHoverLost(e); @@ -85,7 +83,7 @@ namespace osu.Game.Overlays.Profile private void redrawGraph() { - if (data.Length == 0) + if (!data?.Any() ?? true) { HideGraph(); return; @@ -108,7 +106,7 @@ namespace osu.Game.Overlays.Profile { get { - if (data.Length == 0) + if (!data?.Any() ?? true) return null; var (key, value) = data[dataIndex]; From d6adc06f6e0f4334d571e4f3e806bbabf19f8157 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 9 Mar 2020 20:13:59 +0300 Subject: [PATCH 0120/6909] Add xmldoc --- osu.Game/Overlays/Profile/UserGraph.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index 64f988b0c1..07346a3e45 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -17,6 +17,11 @@ using osuTK; namespace osu.Game.Overlays.Profile { + /// + /// Graph which is used in to present changes in user statistics over time. + /// + /// Type of data to be used for X-axis of the graph. + /// Type of data to be used for Y-axis of the graph. public abstract class UserGraph : Container, IHasCustomTooltip { protected const float FADE_DURATION = 150; @@ -192,6 +197,9 @@ namespace osu.Game.Overlays.Profile protected readonly OsuSpriteText Counter, BottomText; private readonly Box background; + /// + /// Text which will be shown near the . + /// protected abstract string TooltipCounterName { get; } protected UserGraphTooltip() From f6461dc5f8381d3f62523e155f6f5e7646abb788 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 10 Mar 2020 00:19:28 +0300 Subject: [PATCH 0121/6909] Add more consistency to data null checks --- osu.Game/Overlays/Profile/UserGraph.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index 07346a3e45..cea4600523 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -88,7 +88,7 @@ namespace osu.Game.Overlays.Profile private void redrawGraph() { - if (!data?.Any() ?? true) + if (!(data?.Length > 1)) { HideGraph(); return; @@ -111,7 +111,7 @@ namespace osu.Game.Overlays.Profile { get { - if (!data?.Any() ?? true) + if (!(data?.Length > 1)) return null; var (key, value) = data[dataIndex]; From 2f441baeacc8e9421d4914a7d5a69eba9f87793d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 10 Mar 2020 00:50:12 +0300 Subject: [PATCH 0122/6909] Make UserHistoryGraph non-abstract --- .../Visual/Online/TestSceneUserHistoryGraph.cs | 15 +++------------ .../Profile/Header/Components/RankGraph.cs | 5 ++++- .../Sections/Historical/UserHistoryGraph.cs | 16 ++++++++++++++-- osu.Game/Overlays/Profile/UserGraph.cs | 10 +++------- 4 files changed, 24 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs index 26f6ac199b..83607bea6a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserHistoryGraph.cs @@ -25,14 +25,15 @@ namespace osu.Game.Tests.Visual.Online public TestSceneUserHistoryGraph() { - TestGraph graph; + UserHistoryGraph graph; - Add(graph = new TestGraph + Add(graph = new UserHistoryGraph { RelativeSizeAxes = Axes.X, Height = 200, Anchor = Anchor.Centre, Origin = Anchor.Centre, + TooltipCounterName = "Test" }); var values = new[] @@ -60,15 +61,5 @@ namespace osu.Game.Tests.Visual.Online AddStep("Set null values", () => graph.Values = null); AddStep("Set empty values", () => graph.Values = Array.Empty()); } - - private class TestGraph : UserHistoryGraph - { - protected override UserGraphTooltip GetTooltip() => new TestTooltip(); - - private class TestTooltip : HistoryGraphTooltip - { - protected override string TooltipCounterName => "Test Counter"; - } - } } } diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index 13fbaa7f85..73ae91e345 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -74,7 +74,10 @@ namespace osu.Game.Overlays.Profile.Header.Components private class RankGraphTooltip : UserGraphTooltip { - protected override string TooltipCounterName => @"Global Ranking"; + public RankGraphTooltip() + : base(@"Global Ranking") + { + } public override bool SetContent(object content) { diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index 6de1b8e0f0..5f6f6cc3e4 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -8,15 +8,22 @@ using static osu.Game.Users.User; namespace osu.Game.Overlays.Profile.Sections.Historical { - public abstract class UserHistoryGraph : UserGraph + public class UserHistoryGraph : UserGraph { public UserHistoryCount[] Values { set => Data = value?.Select(v => new KeyValuePair(v.Date, v.Count)).ToArray(); } + /// + /// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the + /// + public string TooltipCounterName { get; set; } = @"Plays"; + protected override float GetDataPointHeight(long playCount) => playCount; + protected override UserGraphTooltip GetTooltip() => new HistoryGraphTooltip(TooltipCounterName); + protected override object GetTooltipContent(DateTime date, long playCount) { return new TooltipDisplayContent @@ -26,8 +33,13 @@ namespace osu.Game.Overlays.Profile.Sections.Historical }; } - protected abstract class HistoryGraphTooltip : UserGraphTooltip + protected class HistoryGraphTooltip : UserGraphTooltip { + public HistoryGraphTooltip(string tooltipCounterName) + : base(tooltipCounterName) + { + } + public override bool SetContent(object content) { if (!(content is TooltipDisplayContent info)) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index cea4600523..c19844960b 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -197,12 +197,7 @@ namespace osu.Game.Overlays.Profile protected readonly OsuSpriteText Counter, BottomText; private readonly Box background; - /// - /// Text which will be shown near the . - /// - protected abstract string TooltipCounterName { get; } - - protected UserGraphTooltip() + protected UserGraphTooltip(string tooltipCounterName) { AutoSizeAxes = Axes.Both; Masking = true; @@ -225,12 +220,13 @@ namespace osu.Game.Overlays.Profile { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, + Spacing = new Vector2(3, 0), Children = new Drawable[] { new OsuSpriteText { Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = $"{TooltipCounterName} " + Text = tooltipCounterName }, Counter = new OsuSpriteText { From d2b4856d134d5e6792b40aca2dd1e49b51d2d97f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 10 Mar 2020 01:02:09 +0300 Subject: [PATCH 0123/6909] Add more xmldoc --- osu.Game/Overlays/Profile/UserGraph.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index c19844960b..aee464dbf9 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -76,6 +76,9 @@ namespace osu.Game.Overlays.Profile base.OnHoverLost(e); } + /// + /// Set of values which will be used to create a graph. + /// protected KeyValuePair[] Data { set @@ -99,7 +102,13 @@ namespace osu.Game.Overlays.Profile ShowGraph(); } + /// + /// Function used to convert point to it's Y-axis position on the graph. + /// + /// Value to convert. + /// protected abstract float GetDataPointHeight(TValue value); + protected virtual void ShowGraph() => graph.FadeIn(FADE_DURATION, Easing.Out); protected virtual void HideGraph() => graph.FadeOut(FADE_DURATION, Easing.Out); From 742698acab9ba2034ebae716bcd8cc1634d5a13e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Mar 2020 15:30:24 +0900 Subject: [PATCH 0124/6909] 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 1aacd1aaa2568eecb1f00ecc1b77cb0ae406082f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 16 Mar 2020 20:43:02 +0100 Subject: [PATCH 0125/6909] 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 8c611a981f0fc35ab89fb8012157dc7c62cecb00 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 16 Mar 2020 21:48:28 +0100 Subject: [PATCH 0126/6909] 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 0127/6909] 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 0128/6909] 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 0129/6909] 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 17bae532bd91e782ac9be727843b4e8f57456df9 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 17 Mar 2020 23:09:50 +0100 Subject: [PATCH 0130/6909] 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 a1274a9eb0ca927c4d07cb94aaba0fa101745a4a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 18 Mar 2020 08:17:41 +0100 Subject: [PATCH 0131/6909] 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 0132/6909] 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 1d680b7a0073b783cee638e64c31d90c966f9deb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 18 Mar 2020 19:13:25 +0900 Subject: [PATCH 0133/6909] 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 e9f224b5e8c3ae93098848ee3ee2146d47e7146e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 18 Mar 2020 21:16:54 +0100 Subject: [PATCH 0134/6909] 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 0135/6909] 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 e59d7fee26728e6c281d03bf67ee004c127973de Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Wed, 18 Mar 2020 23:42:14 -0400 Subject: [PATCH 0136/6909] 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 0137/6909] 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 0138/6909] 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 f285b43a74afd66c6c2ec1dcbe63e4f66f007314 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Mar 2020 17:44:32 +0900 Subject: [PATCH 0139/6909] 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 12a48d2774dd0e4aa19cdd989b34c7022343ff1e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Mar 2020 19:16:24 +0900 Subject: [PATCH 0140/6909] 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 5a6d8f1932715d9fc7479d8cb5614e0b2efe4025 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Fri, 20 Mar 2020 12:47:17 -0400 Subject: [PATCH 0141/6909] 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 0142/6909] 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 0143/6909] 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 0144/6909] 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 0145/6909] 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 0146/6909] 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 0147/6909] 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 0148/6909] 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 0149/6909] 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 0150/6909] 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 0151/6909] 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 0152/6909] 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 0153/6909] 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 299ea236121d0fd47fc56aa2508dcac3290bc843 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Mar 2020 14:26:49 +0100 Subject: [PATCH 0154/6909] Clean up xmldocs --- .../Overlays/Profile/Sections/Historical/UserHistoryGraph.cs | 2 +- osu.Game/Overlays/Profile/UserGraph.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index 5f6f6cc3e4..5009c13512 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } /// - /// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the + /// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the . /// public string TooltipCounterName { get; set; } = @"Plays"; diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index aee464dbf9..651e9ba8b3 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -106,7 +106,6 @@ namespace osu.Game.Overlays.Profile /// Function used to convert point to it's Y-axis position on the graph. /// /// Value to convert. - /// protected abstract float GetDataPointHeight(TValue value); protected virtual void ShowGraph() => graph.FadeIn(FADE_DURATION, Easing.Out); From ce4761747639e4830bdc415af6513a91bd1f839d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Mar 2020 14:28:23 +0100 Subject: [PATCH 0155/6909] Trim unnecessary raw string prefixes --- osu.Game/Overlays/Profile/Header/Components/RankGraph.cs | 2 +- .../Overlays/Profile/Sections/Historical/UserHistoryGraph.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index 73ae91e345..7d094b3be1 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private class RankGraphTooltip : UserGraphTooltip { public RankGraphTooltip() - : base(@"Global Ranking") + : base("Global Ranking") { } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index 5009c13512..b690c2051c 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical /// /// Text describing the value being plotted on the graph, which will be displayed as a prefix to the value in the . /// - public string TooltipCounterName { get; set; } = @"Plays"; + public string TooltipCounterName { get; set; } = "Plays"; protected override float GetDataPointHeight(long playCount) => playCount; From d167e0c8b9ca107eef6e1bdffd20ece0562932ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Mar 2020 14:35:04 +0100 Subject: [PATCH 0156/6909] Mark properties as [CanBeNull] --- .../Overlays/Profile/Sections/Historical/UserHistoryGraph.cs | 2 ++ osu.Game/Overlays/Profile/UserGraph.cs | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs index b690c2051c..b1e8c8f0ca 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/UserHistoryGraph.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using static osu.Game.Users.User; namespace osu.Game.Overlays.Profile.Sections.Historical { public class UserHistoryGraph : UserGraph { + [CanBeNull] public UserHistoryCount[] Values { set => Data = value?.Select(v => new KeyValuePair(v.Date, v.Count)).ToArray(); diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index 651e9ba8b3..5ef42bc3fa 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -79,11 +80,11 @@ namespace osu.Game.Overlays.Profile /// /// Set of values which will be used to create a graph. /// + [CanBeNull] protected KeyValuePair[] Data { set { - value ??= Array.Empty>(); data = value; redrawGraph(); } From af7d6d0a4e4f6b62d042c2a4a301dbf1011db3a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Mar 2020 14:45:32 +0100 Subject: [PATCH 0157/6909] Invert data length checks for consistency --- osu.Game/Overlays/Profile/UserGraph.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Profile/UserGraph.cs b/osu.Game/Overlays/Profile/UserGraph.cs index 5ef42bc3fa..95f8e0c923 100644 --- a/osu.Game/Overlays/Profile/UserGraph.cs +++ b/osu.Game/Overlays/Profile/UserGraph.cs @@ -92,15 +92,15 @@ namespace osu.Game.Overlays.Profile private void redrawGraph() { - if (!(data?.Length > 1)) + if (data?.Length > 1) { - HideGraph(); + graph.DefaultValueCount = data.Length; + graph.Values = data.Select(pair => GetDataPointHeight(pair.Value)).ToArray(); + ShowGraph(); return; } - graph.DefaultValueCount = data.Length; - graph.Values = data.Select(pair => GetDataPointHeight(pair.Value)).ToArray(); - ShowGraph(); + HideGraph(); } /// @@ -120,11 +120,13 @@ namespace osu.Game.Overlays.Profile { get { - if (!(data?.Length > 1)) - return null; + if (data?.Length > 1) + { + var (key, value) = data[dataIndex]; + return GetTooltipContent(key, value); + } - var (key, value) = data[dataIndex]; - return GetTooltipContent(key, value); + return null; } } From 63e9b2a299cf6496ec535315a4787ad9327cb044 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 18:49:45 -0400 Subject: [PATCH 0158/6909] 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 67667b3d22c0f0e389cd0717c1ed717704dfb11c Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 21:22:46 -0400 Subject: [PATCH 0159/6909] 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 cb6e6025567179280b52d2e58aa099af6b2ef5d9 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:06:35 -0400 Subject: [PATCH 0160/6909] 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 0161/6909] 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 0162/6909] 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 afe7397d891237271902caa4b3f31f8b189b770b Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 22:50:52 -0400 Subject: [PATCH 0163/6909] 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 0164/6909] 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 0165/6909] 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 0166/6909] 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 64fc116d673500e6b4e55639e7e7daf7d7fad640 Mon Sep 17 00:00:00 2001 From: Liam DeVoe Date: Sun, 22 Mar 2020 23:08:00 -0400 Subject: [PATCH 0167/6909] 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 997ce397efa6c64e6218fdd342cc0ea738a47b40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 12:48:05 +0900 Subject: [PATCH 0168/6909] 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 a6b153673e2fa61dc5c7c674a5fb6b964008063d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 14:58:02 +0900 Subject: [PATCH 0169/6909] 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 0170/6909] 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 232c2559867ceebfb8b61e0a979096c07c67c406 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 17:33:02 +0900 Subject: [PATCH 0171/6909] 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 467066112f3845393623fa9dc0b2d470a7260935 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Mar 2020 18:50:16 +0900 Subject: [PATCH 0172/6909] 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 0173/6909] 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 0174/6909] 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 0175/6909] 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 0176/6909] 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 0177/6909] 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 e5f4d8686e04b610ac560b3e62cd60c3f7425445 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 10:38:24 +0900 Subject: [PATCH 0178/6909] 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 0179/6909] 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 0180/6909] 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 022465f546ba5cfb93f21f9792365cf6010d3a2f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Mar 2020 14:13:46 +0900 Subject: [PATCH 0181/6909] 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 0182/6909] 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 0183/6909] 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 0184/6909] 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 0185/6909] 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 0186/6909] 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 0187/6909] 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 0188/6909] 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 0189/6909] 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 0190/6909] 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 0191/6909] 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 0192/6909] 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 0193/6909] 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 0194/6909] 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 87db1ba48703d07371f12f822d4d2eb765c1adfb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Mar 2020 14:58:49 +0900 Subject: [PATCH 0195/6909] 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 0196/6909] 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 8a2aac5f8377a27ceb33e22eeb5fa8dbb4432c9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Mar 2020 20:21:34 +0900 Subject: [PATCH 0197/6909] 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 0198/6909] 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 0199/6909] 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 0200/6909] 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 0201/6909] 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 0202/6909] 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 0203/6909] 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 0204/6909] 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 0205/6909] 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 0206/6909] 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 0207/6909] 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 0208/6909] 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 0209/6909] 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 0210/6909] 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 0211/6909] 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 0212/6909] 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 0213/6909] 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 0214/6909] 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 0215/6909] 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 0216/6909] 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 0217/6909] 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 0218/6909] 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 0219/6909] 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 0220/6909] 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 0221/6909] 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 0222/6909] 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 0223/6909] 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 0224/6909] 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 0225/6909] 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 0226/6909] 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 0227/6909] 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 0228/6909] 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 0229/6909] 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 0230/6909] 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 0231/6909] 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 0232/6909] 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 0233/6909] 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 0234/6909] 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 0235/6909] 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 0236/6909] 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 0237/6909] 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 0238/6909] 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 0239/6909] 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 0240/6909] 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 0241/6909] 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 0242/6909] 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 0243/6909] 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 0244/6909] 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 0245/6909] 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 0246/6909] 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 0247/6909] 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 0248/6909] 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 0249/6909] 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 0250/6909] 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 0251/6909] 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 0252/6909] 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 0253/6909] 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 0254/6909] 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 0255/6909] 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 0256/6909] 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 0257/6909] 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 0258/6909] 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 0259/6909] 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 0260/6909] 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 0261/6909] 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 0262/6909] 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 0263/6909] 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 0264/6909] 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 0265/6909] 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 0266/6909] 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 0267/6909] 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 0268/6909] 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 0269/6909] 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 0270/6909] 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 0271/6909] 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 0272/6909] 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 0273/6909] 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 0274/6909] 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 0275/6909] 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 0276/6909] 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 0277/6909] 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 0278/6909] 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 0279/6909] 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 0280/6909] 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 0281/6909] 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 0282/6909] 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 0283/6909] 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 0284/6909] 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 0285/6909] 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 0286/6909] 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 0287/6909] 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 0288/6909] 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 0289/6909] 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 0290/6909] 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 0291/6909] 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 0292/6909] 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 0293/6909] 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 0294/6909] 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 0295/6909] 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 0296/6909] 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 0297/6909] 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 0298/6909] 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 0299/6909] 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 0300/6909] 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 0301/6909] 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 0302/6909] 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 0303/6909] 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 0304/6909] 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 0305/6909] 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 0306/6909] 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 0307/6909] 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 0308/6909] 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 0309/6909] 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 0310/6909] 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 0311/6909] 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 0312/6909] 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 0313/6909] 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 0314/6909] 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 0315/6909] 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 0316/6909] 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 0317/6909] 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 0318/6909] 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 0319/6909] 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 0320/6909] 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 0321/6909] 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 0322/6909] 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 0323/6909] 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 0324/6909] 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 0325/6909] 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 0326/6909] 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 0327/6909] 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 0328/6909] 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 0329/6909] 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 0330/6909] 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 0331/6909] 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 0332/6909] 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 0333/6909] 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 0334/6909] 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 0335/6909] 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 0336/6909] 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 0337/6909] 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 0338/6909] 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 0339/6909] 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 0340/6909] 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 0341/6909] 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 0342/6909] 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 0343/6909] 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 0344/6909] 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 0345/6909] 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 0346/6909] 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 0347/6909] 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 0348/6909] 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 0349/6909] 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 0350/6909] 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 0351/6909] 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 0352/6909] 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 0353/6909] 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 0354/6909] 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 0355/6909] 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 0356/6909] 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 0357/6909] 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 0358/6909] 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 0359/6909] 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 0360/6909] 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 0361/6909] 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 0362/6909] 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 0363/6909] 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 0364/6909] 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 0365/6909] 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 0366/6909] 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 0367/6909] 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 0368/6909] 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 0369/6909] 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 0370/6909] 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 0371/6909] 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 0372/6909] 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 0373/6909] 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 0374/6909] 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 0375/6909] 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 0376/6909] 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 0377/6909] 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 0378/6909] 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 0379/6909] 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 0380/6909] 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 0381/6909] 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 0382/6909] 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 0383/6909] 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 0384/6909] 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 0385/6909] 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 0386/6909] 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 0387/6909] 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 0388/6909] 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 0389/6909] 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 0390/6909] 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 0391/6909] 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 0392/6909] 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 0393/6909] 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 0394/6909] 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 0395/6909] 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 0396/6909] 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 0397/6909] 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 0398/6909] 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 0399/6909] 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 0400/6909] 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 0401/6909] 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 0402/6909] 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 0403/6909] 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 0404/6909] 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 0405/6909] 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 0406/6909] 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 0407/6909] 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 0408/6909] 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 0409/6909] 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 0410/6909] 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 0411/6909] 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 0412/6909] 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 0413/6909] 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 0414/6909] 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 0415/6909] 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 0416/6909] 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 0417/6909] 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 0418/6909] 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 0419/6909] 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 0420/6909] 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 0421/6909] 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 0422/6909] 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 0423/6909] 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 0424/6909] 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 0425/6909] 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 0426/6909] 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 0427/6909] 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 0428/6909] 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 0429/6909] 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 0430/6909] 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 0431/6909] 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 0432/6909] 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 0433/6909] 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 0434/6909] 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 0435/6909] 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 0436/6909] 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 0437/6909] 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 0438/6909] 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 0439/6909] 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 0440/6909] 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 0441/6909] 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 0442/6909] 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 0443/6909] 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 0444/6909] 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 0445/6909] 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 0446/6909] 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 0447/6909] 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 0448/6909] 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 0449/6909] 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 0450/6909] 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 0451/6909] 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 0452/6909] 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 0453/6909] 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 0454/6909] 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 0455/6909] 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 0456/6909] 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 0457/6909] 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 0458/6909] 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 0459/6909] 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 0460/6909] 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 0461/6909] 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 0462/6909] 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 0463/6909] 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 0464/6909] 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 0465/6909] 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 0466/6909] 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 0467/6909] 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 0468/6909] 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 0469/6909] 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 0470/6909] 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 0471/6909] 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 0472/6909] 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 0473/6909] 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 0474/6909] 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 0475/6909] 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 0476/6909] 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 0477/6909] 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 0478/6909] 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 0479/6909] 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 0480/6909] 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 0481/6909] 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 0482/6909] 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 0483/6909] 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 0484/6909] 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 0485/6909] 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 0486/6909] 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 0487/6909] =?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 0488/6909] 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 0489/6909] 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 0490/6909] 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 0491/6909] 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 0492/6909] 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 0493/6909] 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 0494/6909] 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 0495/6909] 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 0496/6909] 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 0497/6909] 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 0498/6909] 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 0499/6909] 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 0500/6909] 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 0501/6909] 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 0502/6909] 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 0503/6909] 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 0504/6909] 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 0505/6909] 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 0506/6909] =?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 0507/6909] 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 0508/6909] 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 0509/6909] 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 0510/6909] 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 0511/6909] 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 0512/6909] 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 0513/6909] 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 0514/6909] 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 0515/6909] 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 0516/6909] 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 0517/6909] 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 0518/6909] 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 0519/6909] 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 0520/6909] 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 0521/6909] 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 0522/6909] 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 0523/6909] 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 0524/6909] 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 0525/6909] 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 0526/6909] 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 0527/6909] 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 0528/6909] 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 0529/6909] 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 0530/6909] 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 0531/6909] 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 0532/6909] 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 0533/6909] 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 0534/6909] 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 0535/6909] 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 0536/6909] 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 0537/6909] 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 0538/6909] 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 0539/6909] 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 0540/6909] 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 0541/6909] 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 0542/6909] 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 0543/6909] 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 0544/6909] 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 0545/6909] 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 0546/6909] 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 0547/6909] 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 0548/6909] 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 0549/6909] 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 0550/6909] 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 0551/6909] 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 0552/6909] 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 0553/6909] 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 0554/6909] 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 0555/6909] 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 0556/6909] 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 0557/6909] 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 0558/6909] 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 0559/6909] 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 0560/6909] 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 0561/6909] 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 0562/6909] 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 0563/6909] 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 0564/6909] 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 0565/6909] 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 0566/6909] 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 0567/6909] 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 0568/6909] 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 0569/6909] 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 0570/6909] 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 0571/6909] 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 0572/6909] 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 0573/6909] 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 0574/6909] 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 0575/6909] 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 0576/6909] 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 0577/6909] 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 0578/6909] 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 0579/6909] 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 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 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 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 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 0580/6909] 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 0581/6909] 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 0582/6909] 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 0583/6909] 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 0584/6909] 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 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 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 0585/6909] 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 0586/6909] 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 0587/6909] 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 0588/6909] 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 0589/6909] 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 0590/6909] 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 0591/6909] 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 0592/6909] 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 0593/6909] 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 0594/6909] 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 0595/6909] 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 0596/6909] 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 0597/6909] 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 0598/6909] 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 0599/6909] 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 0600/6909] 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 0601/6909] 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 0602/6909] 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 0603/6909] 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 0604/6909] 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 0605/6909] 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 0606/6909] 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 0607/6909] 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 0608/6909] 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 0609/6909] =?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 0610/6909] 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 0611/6909] 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 0612/6909] 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 0613/6909] 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 0614/6909] 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 0615/6909] 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 0616/6909] 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 0617/6909] 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 0618/6909] 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 0619/6909] 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 0620/6909] 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 0621/6909] 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 0622/6909] 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 0623/6909] 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 0624/6909] 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 0625/6909] 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 0626/6909] 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 0627/6909] 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 0628/6909] 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 0629/6909] 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 0630/6909] 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 0631/6909] 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 0632/6909] 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 0633/6909] 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 0634/6909] 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 0635/6909] 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 0636/6909] 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 0637/6909] 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 0638/6909] 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 0639/6909] 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 0640/6909] 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 0641/6909] 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 0642/6909] 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 0643/6909] 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 0644/6909] 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 0645/6909] 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 0646/6909] 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 0647/6909] 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 0648/6909] 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 0649/6909] 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 0650/6909] 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 0651/6909] 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 0652/6909] 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 0653/6909] 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 0654/6909] 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 0655/6909] 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 0656/6909] 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 0657/6909] 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 0658/6909] 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 0659/6909] 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 0660/6909] 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 0661/6909] 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 0662/6909] 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 0663/6909] 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 0664/6909] 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 0665/6909] 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 0666/6909] 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 0667/6909] 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 0668/6909] 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 0669/6909] 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 0670/6909] 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 0671/6909] 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 0672/6909] 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 0673/6909] 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 0674/6909] 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 0675/6909] 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 0676/6909] 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 dd6c9173da8b4d44f001cb057efa67c71e2d14a6 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 14 Apr 2020 18:42:00 +0300 Subject: [PATCH 0677/6909] Move DifficultyRecommender to OsuGameBase --- osu.Game/OsuGameBase.cs | 5 +++++ osu.Game/Screens/Select/SongSelect.cs | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5487bd9320..4b79e9f24c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -33,6 +33,7 @@ using osu.Game.Resources; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; +using osu.Game.Screens.Select; using osu.Game.Skinning; using osuTK.Input; @@ -240,6 +241,10 @@ namespace osu.Game dependencies.Cache(previewTrackManager = new PreviewTrackManager()); Add(previewTrackManager); + DifficultyRecommender difficultyRecommender; + dependencies.Cache(difficultyRecommender = new DifficultyRecommender()); + Add(difficultyRecommender); + Ruleset.BindValueChanged(onRulesetChanged); } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index f164056ede..7f35011379 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -81,8 +81,6 @@ namespace osu.Game.Screens.Select protected BeatmapCarousel Carousel { get; private set; } - private DifficultyRecommender recommender; - private BeatmapInfoWedge beatmapInfoWedge; private DialogOverlay dialogOverlay; @@ -104,14 +102,13 @@ namespace osu.Game.Screens.Select private MusicController music { get; set; } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores) + private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, DifficultyRecommender recommender) { // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). transferRulesetValue(); AddRangeInternal(new Drawable[] { - recommender = new DifficultyRecommender(), new ResetScrollContainer(() => Carousel.ScrollToSelected()) { RelativeSizeAxes = Axes.Y, From 00d1cf1ce2b693348633919d649a15388f955072 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 14 Apr 2020 18:42:18 +0300 Subject: [PATCH 0678/6909] Recommend from all rulesets --- .../Screens/Select/DifficultyRecommender.cs | 37 +++++++++++++++++-- 1 file changed, 34 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 20cdca858a..76b1188298 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -44,16 +44,27 @@ namespace osu.Game.Screens.Select /// The recommended difficulty, or null if a recommendation could not be provided. public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps) { - if (recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars)) + if (!recommendedStarDifficulty.Any()) + return null; + + BeatmapInfo beatmap = null; + + foreach (var r in getBestRulesetOrder()) { - return beatmaps.OrderBy(b => + if (!recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars)) + break; + + beatmap = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => { var difference = b.StarDifficulty - stars; return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder }).FirstOrDefault(); + + if (beatmap != null) + break; } - return null; + return beatmap; } private void calculateRecommendedDifficulties() @@ -72,6 +83,26 @@ namespace osu.Game.Screens.Select }); } + private IEnumerable bestRulesetOrder; + + private IEnumerable getBestRulesetOrder() + { + if (bestRulesetOrder != null) + return bestRulesetOrder; + + var otherRulesets = recommendedStarDifficulty.ToList() + .Where(pair => !pair.Key.Equals(ruleset.Value)) + .OrderBy(pair => pair.Value) + .Select(pair => pair.Key) + .Reverse(); + + var rulesetList = new List(new[] { ruleset.Value }); + rulesetList.AddRange(otherRulesets); + + bestRulesetOrder = rulesetList; + return rulesetList; + } + public void APIStateChanged(IAPIProvider api, APIState state) { switch (state) From bbef94b4df15fd3e1b2f0f51f5c16e2243920a45 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 14 Apr 2020 18:56:20 +0300 Subject: [PATCH 0679/6909] Reset best order on ruleset change --- osu.Game/Screens/Select/DifficultyRecommender.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 76b1188298..bdc81ad066 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Select private void load() { api.Register(this); + + ruleset.ValueChanged += _ => bestRulesetOrder = null; } /// From 872551733f3b4c9e8fe4c7f21a4d0b8edf4ae3bb Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 14 Apr 2020 19:39:14 +0300 Subject: [PATCH 0680/6909] Present recommended beatmaps --- osu.Game/OsuGame.cs | 14 +++++++++----- osu.Game/OsuGameBase.cs | 7 ++++--- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5e93d760e3..64fe0f6733 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -322,7 +322,7 @@ namespace osu.Game /// public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate difficultyCriteria = null) { - difficultyCriteria ??= b => b.Ruleset.Equals(Ruleset.Value); + difficultyCriteria ??= _ => true; var databasedSet = beatmap.OnlineBeatmapSetID != null ? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID) @@ -346,11 +346,15 @@ namespace osu.Game return; } - // Find first beatmap that matches our predicate. - var first = databasedSet.Beatmaps.Find(difficultyCriteria) ?? databasedSet.Beatmaps.First(); + // Find beatmaps that match our predicate. + var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria(b)); + if (!beatmaps.Any()) + beatmaps = databasedSet.Beatmaps; - Ruleset.Value = first.Ruleset; - Beatmap.Value = BeatmapManager.GetWorkingBeatmap(first); + var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps); + + Ruleset.Value = selection.Ruleset; + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); }, validScreens: new[] { typeof(PlaySongSelect) }); } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4b79e9f24c..0c86017974 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -100,6 +100,9 @@ namespace osu.Game public bool IsDeployedBuild => AssemblyVersion.Major > 0; + [Cached] + protected readonly DifficultyRecommender DifficultyRecommender = new DifficultyRecommender(); + public virtual string Version { get @@ -241,9 +244,7 @@ namespace osu.Game dependencies.Cache(previewTrackManager = new PreviewTrackManager()); Add(previewTrackManager); - DifficultyRecommender difficultyRecommender; - dependencies.Cache(difficultyRecommender = new DifficultyRecommender()); - Add(difficultyRecommender); + Add(DifficultyRecommender); Ruleset.BindValueChanged(onRulesetChanged); } From 80949e89b934f0eaee7705da8e8f62f0dc47b6b3 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 14 Apr 2020 19:49:42 +0300 Subject: [PATCH 0681/6909] Offline fallback and commenting --- osu.Game/OsuGame.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 64fe0f6733..e64ca3ad87 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -316,8 +316,8 @@ namespace osu.Game /// /// The beatmap to select. /// - /// 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. + /// Optional predicate used to filter which difficulties to select. + /// If omitted, this will try to present a recommended 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) @@ -351,7 +351,10 @@ namespace osu.Game if (!beatmaps.Any()) beatmaps = databasedSet.Beatmaps; - var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps); + var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps) ?? ( + // fallback if a difficulty can't be recommended, maybe we are offline + databasedSet.Beatmaps.Find(b => b.Ruleset.Equals(Ruleset.Value)) ?? databasedSet.Beatmaps.First() + ); Ruleset.Value = selection.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); From 58e122a7cb1c0b47365e1fe28c16f24d85ce0681 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 14 Apr 2020 19:56:41 +0300 Subject: [PATCH 0682/6909] Better fallback logic --- osu.Game/OsuGame.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e64ca3ad87..fea89155f5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -322,8 +322,6 @@ namespace osu.Game /// public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate difficultyCriteria = null) { - difficultyCriteria ??= _ => true; - var databasedSet = beatmap.OnlineBeatmapSetID != null ? BeatmapManager.QueryBeatmapSet(s => s.OnlineBeatmapSetID == beatmap.OnlineBeatmapSetID) : BeatmapManager.QueryBeatmapSet(s => s.Hash == beatmap.Hash); @@ -347,14 +345,20 @@ namespace osu.Game } // Find beatmaps that match our predicate. - var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria(b)); + var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true); if (!beatmaps.Any()) beatmaps = databasedSet.Beatmaps; - var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps) ?? ( - // fallback if a difficulty can't be recommended, maybe we are offline - databasedSet.Beatmaps.Find(b => b.Ruleset.Equals(Ruleset.Value)) ?? databasedSet.Beatmaps.First() - ); + var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps); + + // fallback if a difficulty can't be recommended, maybe we are offline + if (selection == null) + { + if (difficultyCriteria != null) + selection = beatmaps.First(); + else + selection = databasedSet.Beatmaps.Find(b => b.Ruleset.Equals(Ruleset.Value)) ?? databasedSet.Beatmaps.First(); + } Ruleset.Value = selection.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); From d47e414fb142e7aa504814494d496a3d08528a46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Apr 2020 12:35:43 +0900 Subject: [PATCH 0683/6909] 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 0684/6909] 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 0685/6909] 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 0686/6909] 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 0687/6909] 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 0688/6909] 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 0689/6909] 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 0690/6909] 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 0691/6909] 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 0692/6909] 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 0693/6909] 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 0694/6909] 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 0695/6909] 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 0696/6909] 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 47187ec14cbbcfe9ebb8157150cae30e1346c2aa Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Wed, 15 Apr 2020 18:04:23 +0300 Subject: [PATCH 0697/6909] Simplify recommended beatmap presenting --- osu.Game/OsuGame.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fea89155f5..68bf9c822f 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -346,19 +346,18 @@ namespace osu.Game // Find beatmaps that match our predicate. var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true); + + // Use all beatmaps if predicate matched nothing if (!beatmaps.Any()) beatmaps = databasedSet.Beatmaps; - var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps); - - // fallback if a difficulty can't be recommended, maybe we are offline - if (selection == null) - { - if (difficultyCriteria != null) - selection = beatmaps.First(); - else - selection = databasedSet.Beatmaps.Find(b => b.Ruleset.Equals(Ruleset.Value)) ?? databasedSet.Beatmaps.First(); - } + // Try to select recommended beatmap + // This should give us a beatmap from current ruleset if there are any in our matched beatmaps + var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps) ?? ( + // Fallback if a difficulty can't be recommended, maybe we are offline + // First try to find a beatmap in current ruleset, otherwise use first beatmap + beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) ?? beatmaps.First() + ); Ruleset.Value = selection.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); From b5c1752f0a40a731bdefc4c328c2b9cb05a953bf Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Wed, 15 Apr 2020 18:14:51 +0300 Subject: [PATCH 0698/6909] Calculate best ruleset order only once --- .../Screens/Select/DifficultyRecommender.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index bdc81ad066..e7536db356 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -32,8 +32,6 @@ namespace osu.Game.Screens.Select private void load() { api.Register(this); - - ruleset.ValueChanged += _ => bestRulesetOrder = null; } /// @@ -90,19 +88,22 @@ namespace osu.Game.Screens.Select private IEnumerable getBestRulesetOrder() { if (bestRulesetOrder != null) - return bestRulesetOrder; + return moveCurrentRulesetToFirst(); - var otherRulesets = recommendedStarDifficulty.ToList() - .Where(pair => !pair.Key.Equals(ruleset.Value)) - .OrderBy(pair => pair.Value) - .Select(pair => pair.Key) - .Reverse(); + bestRulesetOrder = recommendedStarDifficulty.ToList() + .OrderBy(pair => pair.Value) + .Select(pair => pair.Key) + .Reverse(); - var rulesetList = new List(new[] { ruleset.Value }); - rulesetList.AddRange(otherRulesets); + return moveCurrentRulesetToFirst(); + } - bestRulesetOrder = rulesetList; - return rulesetList; + private IEnumerable moveCurrentRulesetToFirst() + { + var orderedRulesets = bestRulesetOrder.ToList(); + orderedRulesets.Remove(ruleset.Value); + orderedRulesets.Insert(0, ruleset.Value); + return orderedRulesets; } public void APIStateChanged(IAPIProvider api, APIState state) From da9bd74e2eef2f08b85e6e8e15501f70299c4218 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Wed, 15 Apr 2020 20:19:17 +0300 Subject: [PATCH 0699/6909] Very basic testing --- .../TestSceneBeatmapRecommendations.cs | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs new file mode 100644 index 0000000000..80a00ac9a1 --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -0,0 +1,130 @@ +// Copyright (c) ppy Pty Ltd . 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.Logging; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Select; +using osu.Game.Tests.Visual.Navigation; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public class TestSceneBeatmapRecommendations : OsuGameTestScene + { + [Resolved] + private DifficultyRecommender recommender { get; set; } + + [SetUpSteps] + public new void SetUpSteps() + { + AddStep("register request handling", () => + { + Logger.Log($"Registering request handling for {(DummyAPIAccess)API}"); + ((DummyAPIAccess)API).HandleRequest = req => + { + Logger.Log($"New request {req}"); + + switch (req) + { + case GetUserRequest userRequest: + userRequest.TriggerSuccess(new User + { + Username = @"Dummy", + Id = 1001, + Statistics = new UserStatistics + { + PP = 928 // Expected recommended star difficulty is 2.999 + } + }); + break; + } + }; + // Force recommender to calculate its star ratings again + recommender.APIStateChanged(API, APIState.Online); + }); + } + + [Test] + public void TestPresentedBeatmapIsRecommended() + { + var importFunctions = importBeatmaps(5); + + for (int i = 0; i < 5; i++) + { + presentAndConfirm(importFunctions[i], i); + } + } + + private List> importBeatmaps(int amount, RulesetInfo ruleset = null) + { + var importFunctions = new List>(); + + for (int i = 0; i < amount; i++) + { + importFunctions.Add(importBeatmap(i, ruleset)); + } + + return importFunctions; + } + + private Func importBeatmap(int i, RulesetInfo ruleset = null) + { + BeatmapSetInfo imported = null; + AddStep($"import beatmap {i * 1000}", () => + { + var difficulty = new BeatmapDifficulty(); + var metadata = new BeatmapMetadata + { + Artist = "SomeArtist", + AuthorString = "SomeAuthor", + Title = $"import {i * 1000}" + }; + + var beatmaps = new List(); + + for (int j = 1; j <= 5; j++) + { + beatmaps.Add(new BeatmapInfo + { + OnlineBeatmapID = j * 1024 + i * 5, + Metadata = metadata, + BaseDifficulty = difficulty, + Ruleset = ruleset ?? new OsuRuleset().RulesetInfo, + StarDifficulty = j, + }); + } + + imported = Game.BeatmapManager.Import(new BeatmapSetInfo + { + Hash = Guid.NewGuid().ToString(), + OnlineBeatmapSetID = i, + Metadata = metadata, + Beatmaps = beatmaps, + }).Result; + }); + + AddAssert($"import {i * 1000} succeeded", () => imported != null); + + return () => imported; + } + + private void presentAndConfirm(Func getImport, int importedID) + { + AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); + + AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect); + AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID * 5 + 1024 * 3); + AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID); + } + } +} From f36477e39dd1bbd055d345997f91c99701ffa208 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Apr 2020 10:04:09 +0900 Subject: [PATCH 0700/6909] 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 0701/6909] 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 0702/6909] 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 0703/6909] 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 0704/6909] 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 0705/6909] 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 0706/6909] 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 0707/6909] 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 0708/6909] 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 0709/6909] 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 0710/6909] 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 0711/6909] 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 0712/6909] 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 0713/6909] 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 0714/6909] 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 0715/6909] 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 0716/6909] 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 0717/6909] 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 0718/6909] 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 0719/6909] 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 0720/6909] 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 0721/6909] 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 0722/6909] 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 0723/6909] 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 0724/6909] 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 0725/6909] 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 0726/6909] 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 0727/6909] 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 0728/6909] 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 0729/6909] 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 0730/6909] 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 0731/6909] 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 0732/6909] 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 0733/6909] 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 0734/6909] 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 0735/6909] 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 0736/6909] 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 0737/6909] 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 1ac9ee599088d893211bd6dbcdba16662275e745 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 17 Apr 2020 18:15:11 +0300 Subject: [PATCH 0738/6909] Optimize recommender (for custom rulesets) --- .../Screens/Select/DifficultyRecommender.cs | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index e7536db356..07dfc3a85e 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -51,8 +51,7 @@ namespace osu.Game.Screens.Select foreach (var r in getBestRulesetOrder()) { - if (!recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars)) - break; + recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars); beatmap = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => { @@ -75,6 +74,7 @@ namespace osu.Game.Screens.Select req.Success += result => { + bestRulesetOrder = null; // 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; }; @@ -87,22 +87,30 @@ namespace osu.Game.Screens.Select private IEnumerable getBestRulesetOrder() { - if (bestRulesetOrder != null) - return moveCurrentRulesetToFirst(); - - bestRulesetOrder = recommendedStarDifficulty.ToList() - .OrderBy(pair => pair.Value) - .Select(pair => pair.Key) - .Reverse(); + bestRulesetOrder ??= recommendedStarDifficulty.ToList() + .OrderBy(pair => pair.Value) + .Select(pair => pair.Key) + .Reverse(); return moveCurrentRulesetToFirst(); } private IEnumerable moveCurrentRulesetToFirst() { - var orderedRulesets = bestRulesetOrder.ToList(); - orderedRulesets.Remove(ruleset.Value); - orderedRulesets.Insert(0, ruleset.Value); + List orderedRulesets = null; + + if (bestRulesetOrder.Contains(ruleset.Value)) + { + orderedRulesets = bestRulesetOrder.ToList(); + orderedRulesets.Remove(ruleset.Value); + orderedRulesets.Insert(0, ruleset.Value); + } + else + { + orderedRulesets = new List { ruleset.Value }; + orderedRulesets.AddRange(bestRulesetOrder); + } + return orderedRulesets; } From 90fa58b3b65c7221990bfbc12237577a489c4741 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 17 Apr 2020 19:55:51 +0300 Subject: [PATCH 0739/6909] More testing --- .../TestSceneBeatmapRecommendations.cs | 96 +++++++++++++------ .../Online/API/Requests/GetUserRequest.cs | 6 +- 2 files changed, 70 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 80a00ac9a1..ec5fe65fdd 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -6,44 +6,53 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Logging; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; -using osu.Game.Rulesets.Osu; using osu.Game.Screens.Select; using osu.Game.Tests.Visual.Navigation; using osu.Game.Users; namespace osu.Game.Tests.Visual.SongSelect { + [HeadlessTest] public class TestSceneBeatmapRecommendations : OsuGameTestScene { [Resolved] private DifficultyRecommender recommender { get; set; } + [Resolved] + private RulesetStore rulesets { get; set; } + [SetUpSteps] public new void SetUpSteps() { AddStep("register request handling", () => { - Logger.Log($"Registering request handling for {(DummyAPIAccess)API}"); ((DummyAPIAccess)API).HandleRequest = req => { - Logger.Log($"New request {req}"); - switch (req) { case GetUserRequest userRequest: + + decimal pp = userRequest.Ruleset.ID switch + { + 0 => 336, // Expected recommended star difficulty 2* + 1 => 928, // Expected recommended star difficulty 3* + 2 => 1905, // Expected recommended star difficulty 4* + 3 => 3329, // Expected recommended star difficulty 5* + _ => 0 + }; + userRequest.TriggerSuccess(new User { Username = @"Dummy", Id = 1001, Statistics = new UserStatistics { - PP = 928 // Expected recommended star difficulty is 2.999 + PP = pp } }); break; @@ -57,74 +66,103 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestPresentedBeatmapIsRecommended() { - var importFunctions = importBeatmaps(5); + var importFunctions = new List>(); for (int i = 0; i < 5; i++) { - presentAndConfirm(importFunctions[i], i); + importFunctions.Add(importBeatmap(i, new List { null, null, null, null, null })); } - } - private List> importBeatmaps(int amount, RulesetInfo ruleset = null) - { - var importFunctions = new List>(); - - for (int i = 0; i < amount; i++) + for (int i = 0; i < 5; i++) { - importFunctions.Add(importBeatmap(i, ruleset)); + presentAndConfirm(importFunctions[i], i, 2); } - - return importFunctions; } - private Func importBeatmap(int i, RulesetInfo ruleset = null) + [Test] + public void TestBestRulesetIsRecommended() + { + var osuRuleset = rulesets.AvailableRulesets.First(r => r.ID == 0); + var taikoRuleset = rulesets.AvailableRulesets.First(r => r.ID == 1); + var catchRuleset = rulesets.AvailableRulesets.First(r => r.ID == 2); + var maniaRuleset = rulesets.AvailableRulesets.First(r => r.ID == 3); + + var osuImport = importBeatmap(0, new List { osuRuleset }); + var mixedImport = importBeatmap(1, new List { taikoRuleset, catchRuleset, maniaRuleset }); + + // Make sure we are on standard ruleset + presentAndConfirm(osuImport, 0, 1); + + // Present mixed difficulty set, expect ruleset with highest star difficulty + presentAndConfirm(mixedImport, 1, 3); + } + + [Test] + public void TestSecondBestRulesetIsRecommended() + { + var osuRuleset = rulesets.AvailableRulesets.First(r => r.ID == 0); + var taikoRuleset = rulesets.AvailableRulesets.First(r => r.ID == 1); + var catchRuleset = rulesets.AvailableRulesets.First(r => r.ID == 2); + + var osuImport = importBeatmap(0, new List { osuRuleset }); + var mixedImport = importBeatmap(1, new List { taikoRuleset, catchRuleset, taikoRuleset }); + + // Make sure we are on standard ruleset + presentAndConfirm(osuImport, 0, 1); + + // Present mixed difficulty set, expect ruleset with highest star difficulty + presentAndConfirm(mixedImport, 1, 2); + } + + private Func importBeatmap(int importID, List rulesets) { BeatmapSetInfo imported = null; - AddStep($"import beatmap {i * 1000}", () => + AddStep($"import beatmap {importID}", () => { var difficulty = new BeatmapDifficulty(); var metadata = new BeatmapMetadata { Artist = "SomeArtist", AuthorString = "SomeAuthor", - Title = $"import {i * 1000}" + Title = $"import {importID}" }; var beatmaps = new List(); + int difficultyID = 1; - for (int j = 1; j <= 5; j++) + foreach (RulesetInfo r in rulesets) { beatmaps.Add(new BeatmapInfo { - OnlineBeatmapID = j * 1024 + i * 5, + OnlineBeatmapID = importID + 1024 * difficultyID, Metadata = metadata, BaseDifficulty = difficulty, - Ruleset = ruleset ?? new OsuRuleset().RulesetInfo, - StarDifficulty = j, + Ruleset = r ?? rulesets.First(), + StarDifficulty = difficultyID, }); + difficultyID++; } imported = Game.BeatmapManager.Import(new BeatmapSetInfo { Hash = Guid.NewGuid().ToString(), - OnlineBeatmapSetID = i, + OnlineBeatmapSetID = importID, Metadata = metadata, Beatmaps = beatmaps, }).Result; }); - AddAssert($"import {i * 1000} succeeded", () => imported != null); + AddAssert($"import {importID} succeeded", () => imported != null); return () => imported; } - private void presentAndConfirm(Func getImport, int importedID) + private void presentAndConfirm(Func getImport, int importedID, int expextedDiff) { AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect); - AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID * 5 + 1024 * 3); - AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Beatmaps.First().Ruleset.ID); + AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID + 1024 * expextedDiff); } } } diff --git a/osu.Game/Online/API/Requests/GetUserRequest.cs b/osu.Game/Online/API/Requests/GetUserRequest.cs index 31b7e95b39..42aad6f9eb 100644 --- a/osu.Game/Online/API/Requests/GetUserRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRequest.cs @@ -9,14 +9,14 @@ namespace osu.Game.Online.API.Requests public class GetUserRequest : APIRequest { private readonly long? userId; - private readonly RulesetInfo ruleset; + public readonly RulesetInfo Ruleset; public GetUserRequest(long? userId = null, RulesetInfo ruleset = null) { this.userId = userId; - this.ruleset = ruleset; + Ruleset = ruleset; } - protected override string Target => userId.HasValue ? $@"users/{userId}/{ruleset?.ShortName}" : $@"me/{ruleset?.ShortName}"; + protected override string Target => userId.HasValue ? $@"users/{userId}/{Ruleset?.ShortName}" : $@"me/{Ruleset?.ShortName}"; } } From 4aaa00e3219611c387dcdf20dc4f04103bb7af07 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 17 Apr 2020 20:33:12 +0300 Subject: [PATCH 0740/6909] Fix CI complaints --- 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 07dfc3a85e..4d48cc3fe7 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -97,7 +97,7 @@ namespace osu.Game.Screens.Select private IEnumerable moveCurrentRulesetToFirst() { - List orderedRulesets = null; + List orderedRulesets; if (bestRulesetOrder.Contains(ruleset.Value)) { From 61e3491e603daf0a497ec988318043001a4e068a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Apr 2020 12:57:09 +0900 Subject: [PATCH 0741/6909] 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 0742/6909] 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 0743/6909] 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 0744/6909] 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 0745/6909] 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 0746/6909] 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 0747/6909] 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 0748/6909] 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 0749/6909] 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 f893d523f52554b57b390b3f4ee25d302bef9831 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sun, 19 Apr 2020 12:23:41 +0300 Subject: [PATCH 0750/6909] User setting for always playing first combo break --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ .../Settings/Sections/Gameplay/GeneralSettings.cs | 6 ++++++ osu.Game/Screens/Play/ComboEffects.cs | 14 +++++++++++--- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 9d31bc9bba..10d11f967e 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -91,6 +91,7 @@ namespace osu.Game.Configuration Set(OsuSetting.FadePlayfieldWhenHealthLow, true); Set(OsuSetting.KeyOverlay, false); Set(OsuSetting.PositionalHitSounds, true); + Set(OsuSetting.AlwaysPlayComboBreak, false); Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth); Set(OsuSetting.FloatingComments, false); @@ -180,6 +181,7 @@ namespace osu.Game.Configuration ShowStoryboard, KeyOverlay, PositionalHitSounds, + AlwaysPlayComboBreak, ScoreMeter, FloatingComments, ShowInterface, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 93a02ea0e4..f3534e4625 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -67,6 +67,12 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Positional hitsounds", Bindable = config.GetBindable(OsuSetting.PositionalHitSounds) }, + new SettingsCheckbox + { + LabelText = "Always play first combo break sound", + Keywords = new[] { "regardless", "combobreak.wav" }, + Bindable = config.GetBindable(OsuSetting.AlwaysPlayComboBreak) + }, new SettingsEnumDropdown { LabelText = "Score meter type", diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index 1c4ac921f0..c56ee35cec 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Audio; +using osu.Game.Configuration; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; @@ -16,27 +17,34 @@ namespace osu.Game.Screens.Play private SkinnableSound comboBreakSample; + private Bindable alwaysPlay; + private bool firstTime = true; + public ComboEffects(ScoreProcessor processor) { this.processor = processor; } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { InternalChild = comboBreakSample = new SkinnableSound(new SampleInfo("combobreak")); + alwaysPlay = config.GetBindable(OsuSetting.AlwaysPlayComboBreak); } protected override void LoadComplete() { base.LoadComplete(); - processor.Combo.BindValueChanged(onComboChange, true); + processor.Combo.BindValueChanged(onComboChange); } private void onComboChange(ValueChangedEvent combo) { - if (combo.NewValue == 0 && combo.OldValue > 20) + if (combo.NewValue == 0 && (combo.OldValue > 20 || alwaysPlay.Value && firstTime)) + { comboBreakSample?.Play(); + firstTime = false; + } } } } 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 0751/6909] 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 0752/6909] 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 0753/6909] 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 0754/6909] 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 0755/6909] 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 0756/6909] 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 0757/6909] 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 0758/6909] 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 0759/6909] 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 0760/6909] 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 0761/6909] 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 0762/6909] 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 0763/6909] 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 0764/6909] 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 0765/6909] 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 0766/6909] 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 0767/6909] 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 0768/6909] 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 0769/6909] 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 0770/6909] 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 0771/6909] 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 0772/6909] 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 0773/6909] 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 0774/6909] 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 0775/6909] 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 0776/6909] 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 0777/6909] 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 0778/6909] 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 0779/6909] 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 0780/6909] 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 0781/6909] 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 0782/6909] 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 0783/6909] 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 0784/6909] 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 0785/6909] 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 0786/6909] 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 0787/6909] 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 0788/6909] 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 0789/6909] 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 0790/6909] 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 0791/6909] 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 0792/6909] 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 0793/6909] 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 0794/6909] 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 0795/6909] 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 0796/6909] 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 0797/6909] 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 0798/6909] 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 0799/6909] 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 0800/6909] 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 0801/6909] 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 0802/6909] 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 0803/6909] 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 0804/6909] 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 0805/6909] 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 0806/6909] 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 zcmch7c|25Y|L~bHn5>f}WE;C=tb?(SrL5T^L|MjIV(fdCv4xZ@p`s8X3CUWvBx|-1 zk;qPjY-4{<-OqhLzu)J5-tC{)$IP5_uJ68G*Y&+lype$xE!9~n003xpv^7lt00Le@ z05}EsW9?n!2>zV%)VA~i0BXkH9|-U$ixmJUmR(FOd@U~NE8si`Vs;K5_IR;Cf+q+K z07|NXo_07_yf4xo@8sgH%)9osi5KbOpv-G7b4lWorv~2HMLXCVe>vE|6c_A@lXu`% zRY58RDu4tCcwakYAi>SuM-abDyf5MNhiUbWv0krtPXkQyG|c%+P& zED9%y#vtY7#Uy3X64J7wNGSlEO#XP*7#3kkBK^juxQc@@o0_79r?rRr_a`)l;n}R0Z2j}hL z>FeU*j{Hs0&fde%SD6=-^e+_%o|i8DL)hKtUxorrCLU<#DK04{Ax- z-(vhnXdlxcPrSGZ-p9kw8;1wM`TmW3!5`%OFCG3bkT3Y--F^Q-2TlLKNB*0S1MVL< zPd{(BKW1{kiR0by1dyH&sFUPB3VAwv_1I^b^(LN&P~$; z=l7eWj;1m%I3niY;-FwJAtNUxD}zDFV$e7g+Fo83g^|Qzz%O|T2}xNz&R$abZ#w@K z|F1Z5@W6pI{^6=1g}0M-lybC3VeDmPQ1WQBJj!0$Q4%F9ZznC`C@Ui)fyew!O~>8G z*UlY>|63XtkdmV#UJh?B<$#j2mzF@GB|wod@^TU=DG7NwJB*BkJRXPtgRh|Bjkojl z@HX}EaQi!UjsCU`QbXfUtRjUk+2LH=e;e%Ff5F0Q+WF#@d8H(zBv2A)l%$-gBw7Ld zmy$RyA+I1I@fYYN4+j^=p#KgkC21-NLi~pQZ=hf*IoSEy{U3oHa0-qd-UK@^s$2+m zPIz%ocPC!tzs;xN;pX8DG6tmsjsEZFIvN^A-X4xFZs35AiIzH2=Yoc;q`a&wN=i)f zcjElzXHC4fi$C7sqPGVD`6u!fT>g`drKRoU&^S31P6kXsw7nw^WrvfNMoCFJ+DS{= zOUZ#&=KU8w=-+=L=0AJfPDa+w0T23FLJlkocqz0r%H9F=9R`h-#yHx^I5@~T@{0c> zqPB|<=(V7KRg24buYX;+xquwObW^ay{my()O*>GN-(viJ_@C53J38R)@fb&xjI0y} zg~rHBqU z7MK4XQh%Z0obBA5@L(+#|D)*dqV@l-&i`HH|HbhCt2*Mp_4&(D;(vwNzkT>;?7tLj z(3yW8fwREx%YUXH@aCTh4DSwF${U=X!s#g<1As6}M^nu-aBTHjP!{uWnxl0bPn*}F zAIuaR$*Lm=qqw8VCrrT)JtcYeelkYN&hue5&(nTU+Pf1_kx!SRA|rc566pF%WZ{I@ z=zgk5_FNZdBYVwv2J1+zrEO9KA1T;x9&dNu$#3xe682yJb_q5 z9(^V2!3fOT2E+`fAbaWY(0d&4zT>-ovdGjxh7{VR8-t6qqI^2>eWYAZ#qM z@vwDcO=3RqJ7%}w{(-;vF0&LF1Q7E|)-*hx{+W#Nr{hx+*tpx;&S1rVMO#0e_1s30 zWkYQs+b)V-?*LKkLO{wLNIBJYONa_qNo>KWd6n=L6DwpfoSvn|O`c1_^h-K6( zj_=|>1LNHp{8F6V{A&AVf@rjEu+C&CQ8vY@5CId;1)Z&wQBkjovp5XWn14eq{>?ee z@f)RTGo>pBB*R}{x2EeNdqF^`@*Z);LFVQxf#HkHO6O(Hj9;UwCG&S?`1>pk7wO?h zwt-tszCv!dLoKEW@duJxPT#|xWWKMKSA6I5g~A60h3|(dJ$|rO<|uxWsOj-B8Mier z|2ms zRDCkz{ban%?McE~xu}E|l2}XRa8RiTP2SO=Y5&?v`#z}Gk1eFN;@u$;2vL*UiF_@- zN#mQ&TnGi-DX$fMHEXl&^>^r*W9b*&EZ_xKGmRv7HZF7kY$j$V{fi*vm-;&x|i&Ru$J z|618YbtPHDxRsHZj`L@D!y6Djved@w*Jry?&Q&>Mr=JV=PYyN>t}X5kC5{dHECy{^ z2dgy1elx#pQxGcAAlNfb{I$KAS$0Q+&#-ltrK%kq+_%@Qb)+(I2Nw+Gb*k-5G zl-{_`9zZR5S*+~fIJ@ew(c5#oZFnh&Xa8gRElc5ynS%6zf+6FnnZUB|q$L;6|vok`U92b?^EslqBqdSp!71rZr+P5EE-Z1E6aJSj7|QNzBq0AEAnK(w|^Ddlc{nKT~>dC_ufTudKc-cELd7t30w_` ze~*_<7s}qCxPG?Lv_scBdcbgFeC}$%f@=W9U~{AaK!pxR-$Q5bqgoiLdBdm=l5UzW1X38;#!^n$%Q%8N8vHas7hGQXQSr1(+uU+iCPTFC{vBA3MCquv zf(g!9WbsH@mtbW=2jQDPthPg;5hSIu_R`wpl^TaAG zQh58$!sEcNu_H|l&lv^k@&aqSyt^ z#{pp~EmYn*{&_kAP$OiWk?`wZ68v?{83|P04T;7q6y*lbIqspbz&mxPo)4+oQ=eIB z!x10e7>dM;n9eGA=uUG4V1eFp_YB1DrG&(Lv%4lbvy<~5ig2ry^fXIX%@J6M^&J0E zEj6x7IP|g1>Xb%ZAf=xAR?IhUmP z(ZiCkTQ!_J!-?v}BUU}j zRpCgbi|;DhDo0#x_SmR+mR4TGWB)J*&J3t;(}Vd6u=fy)MtfXW^De zB8hk8e0W%wijCd2pq4JHYG0ToZ5h*nBlH&RL{!i z2v`o9*yI~<%$1BTX8t7^0ZfMGq~<{t{Bo zsY5nAs5TBcf6^FF$)5y)u2VGBW7bpEC$w|~&N^xu?nNa%AcG<9X!0+o6Sw$kgr_fl z?TLzdcO6L_9aP9poGUh@lDgYT#}zF`pA5r9X7?3FCJ{r?)TO@=8T*p=b4hhg$SVcu07#os5fR~(x^f& ztJcQ^J#x5m$;cT&l-Kq!WyLIA-k_6wk7vqNXcwV?WW9|}HR)}?#u`q&|Lgor;d0gN zJT;yIQcJ#cueeb^xayhp9naj6d#=IC3@{^Q1sA&p?lGpcrURr#s}O223iUg^GtpH=}^V=8{VTK?mGFZkNB!ElqI`U%ddoDRR1*dhEC; zihAZme|M?P1&r|PHDpWV$+NsKtvRTQo$3+(Q0Kzc1E)HPTbY}*%v9FDaEHek%|>uappTDDi!Ki@>kTV}B8fvK*dgCq1ofnJFT|6G-*hZS zV5xg1YMOW;Aj#@k?N7qifYZ*G?fsAg{Dj zAIPqKKF=pqcBq!Pwua%l4TEZ5PdfI#?e)E)t6!*77f?o@dE47e5;CHC7Myj*YwV)% zX*e=SZC`Iwe_yGtRd{&9@6l1E!A0okf?yN(#9qLYW=i49IaqKWi|x9s6O20fI&y*Q z-G$DJ!2U?ntB8#z-R*;-Yp++deVLiSMfes{Suk{d8g0Y=nb{~_=4^A4F31)pmErBSZ6QnpM{Z+- zFZq1)XvElA@Ldj~l+~qxjxG!KjEp^gTEXT-VvG42h9okz3ik?V2RC}ZHBQTYC|XvZ z!-(bU`LT9As_Y~)ip4T@(2j$N9G3J3dGstO!z%JJ$SxL>W?}m~{su<86zhbF>m6%l zB>P@Qrhq#bmi$TdTK-zhcq<2)qUA>^J$ey{b!%1s-IUM{9m}IusH=&4>>#+4fz>{K z9L4p}wUTP_qtw9zy?d02z(r5WCMg$lurRrI*_?QNFrq_Xm!r0kOBAOntvObdCcNy-<(l9_uW)oK>H}=f0hu^=R)18>k1uAxzDhb2Zi7rpV zQ~lL=?G^GCylVQ{3IMa}s~X#Vhq!0zUGUCXd08Ysl=d#{@mlGsDEB`M+AfCfS{fs`9_= z*rx}ry{qWi0x}70^`70MgaH0U=)fbExI7A3i(YTHM{wu^r(o`Ll)+fqtxh~b0S*{- z1S_9KIN3(+iMDGossUZQ;deA}qL#LW0u(ptT9ST;!R(o!N{#WeA1gT6 z4563~@zB=qBjKa>X+KYm&13sPT7s1`@OMt%;sW>NUwyE7Ejscc0T3JBImy8A69bO- z9}Ulcmq$p!fud6)7oYXHwZDQ);w-rqDnl9>vD{(b1r&)r}6(#u8106>b`J4hWs;4racx(~C`UFx@QlI_!vRoBr z0?{lm2C?Ogg&DoueZ*S>Z~2Q)e6DJ9%+;gjwLKkRLZ*+ekdmlnHq8C?1Pq3SW57Y~ zS6`Fd$B)A;hR_Ec;?F(D=4t8t+>~e{L39JP9ra%ijh9JC&s#3ejqFgPPIw09aDQ^! zLVv<)}lFmPCZ0GU_p-?kn00rmia6;nOL6%yv%sww*S+2D*Y;6dbypf_VV#w7fG zd`}O)&VM%wpdx)=!&o27ahh}V1kgo90)TW8?9IK8erE#mLh}#y)c|(V_ZYfUIWx#c zXoxLE^eK>yn!kqB8;?g8EBzM-FTEyvh-jt@3vASYF1}1*-^!%71F2k2nn=3yGmHP5 zzjI!LzT41!Kt;xmMV|Ow`B@7k*gUJfP+iQtUXSx=b{8Fis=}vSQ+Z+DfcOjH=6d@- z!t&nh>mQrDsO0&0)LAjz^%Xa49i@8c9t$hiEP!a7@&qJeML54XR?ZZ^YqDhz9ifL} zI_XcUG@!!n+m$lF8@gyLq?yKN^3_V=UR+1}Nc>${*}I=*c!-078*SczhfH@hMfofl zoY4wsW^$0@&vVVb6A}V4(@6iJ6^6hHsMaSBnofy4+oQb?0jojjshlOxc9_w6#sb9_i#&|b za(4eq;j%)+*W*yHiY<)h({a%0CvR*pq1t*B;VXd^pnmaYJge_*t9O-XIYv}{_eCh?IGBV#IA$^ zBQ*CMKI_)6ea?(PWu|upo}TKQ#9M_p^LEFdm*b7u|BjC%xC3-as-@NQ%+Q=9zL_z#|dB6kV}Y5`Uysq|w=rwV1`?ijEtQDu^0D zq%ImGa?GZ#Pv^LIMU`pzfyKd=&O2LZ56v}Ciu!RgX#MJ;XRwYFSYB73Nq)u_eUtwy zdskv+2fgo;x^n+Yzt(4$PT6ud@ptn;4qvu2k)U8V7spLh*Hc;DY?Y0z{_+t22Fzk% znV@)%j;g3Vk@9~hdFR5t)YcbtgiWvXC1=%BwohFu2cl&@n@tgd5F()cs47@e`i=%U zCztwcAMR|W0&h0;5)}xqx5zdGxE-RkHeiz zwm6g6Lik+|Ff1JgnA35C>_lqICqjF&&2msA$4rKwpEkhwwp{D7`{`(~EcKFCFqTps z#%p9Ap6lM!xO~%J19-cKpW$R^<|Lykfowk^{~ieteIiz(=>Zn`%FdgXPhQWc0n~f= z%*WN8HXfX?Z&_r2gE(y3NVnQ>jE%CUFW@$iz{~?;mEs50GFCTQ-DA~g(2+L)u%H(l z=X7YjuC|c`ULl$b#H=Hos%Exq#nZ4Izo(b_+TIksA5N_-V%9|Q+msLl-7h_Ee{Eou zE0lHt?2b1t9XEBuoZ*SX7pwKCfKB>aiS_| zX1^7*BHTj7l!GC|X(;yE!P_Ts=HM_vrtj-odny3W-t2D25cnur&x!lDQzK;IK)FW2 zKoj20A!fDp*-uCL#-E=PaXiGJ!(W^R;~fIMpZBATB$!~psIm2RGphO&^HBr0Wqhi= z@^wZmWmw%{OH3tgt&M=fNA)o9kiaOWWaLFLkoLA{*o(jm&pE^1WDK`&V{m0B>c0b1 zglgN)E#pVv-sB^Et@Ii8#5CaBcY_i+IQ_SxYRGwGS&rWe6zt~lfq85-IuidN63UXO9F{B1*pyca8v+LbXAI?+kId~AY=<=w4fQU8*I3P=wQ+i}mno$mS6kIxb9iz79k>vu0T<%9!yB$Y z*=d<$X?`6~yptMt_52N7^g)mD-384{r7*rK3I%2QIRKKcqTLsCOvyUhY40xGua~*~ z2so=_`Y4z}EW}VLubA?#B{?|+k}sy+-S^x6!wZ}9tENS%8OQ||4Oq|Iyl8AsEDLL4(FxK{1tGW$~Lu(DK$kF~| zrJhzp;=s2EB++zh-&IM}8~byM$%grRu20pKPEDYSB-k9toOB)0>B$@Rh%l!U^@|9k zaNa+uu&jPHl6|~wvy$<6Uk6aGGHdn|B&4Obh7RT^fjd6RCCQ#`Kg($XnIb89XWQfP z9Z;TV%2z%4*W8nZi`!k^KO;m#BY+f^{HEkOgL|a4%n-PZ16$AT;Kd_BV(FMjPM*%V zY6HhP|2S7Q1lA;KD`R2bh3v?qi_r0Urx4t)MoV#hY!_Ue_c+}UuF~?T{-97J84Gk$ zezNM_c|BR~-DjO{*f_bvMh|s<{t~*+&IuR0BS`zOiJ6KVs?C+uc%xHgjD7LQqmstT zJRb^KG}fooANPM~OU{~`#R^A0#O~{Ej*E^51uEg>yB#f*+@!C=lG>i=qswCi2|}fe z_hdU;uK=*55WN!$w$ts^;nMxPjiMR%_?Pn;Ac%`DVt4&6jqN-{T`7Lqe8)r>Bw33* z8cIKIlI-^>-m-em;Xjifo*!#H z5m28i_ChH z?@x(qyfJnh!N&X6jv|)MFh)HRm}Z?;th({#if z&@6b3etPalwkXUf!Kc9NO($QGr=Gh5IZr98XsfGL4miQDVfTIKriz2u7t~eF!kRqz z!x)&}QZqmnAsW%`*D6)Z2#IqJLxHT`a3WBjuqqTfEuU#`)MK98kUqIauU1i)a~g|k zJYy`=KVDaN-A#II%)nZi={$JSvtkqWi4P@rvL{A&blkZbOWE@+QX5!s>D0VeTQ68= zkqi(hJp}=%XH;EgpT$m}r0lSs!BWM=rQ0 zTtJ2nROel`R`?-0y;GnjzomBuinQ*tG``Dtd-+9tW?P*2eI(eKwg0O#+e2!9d+6QN zvbw8l&1|YjEd?pc$>iR^qqX}@-;s&j15EHh`^Tnd3j^(KjYHD#-K2@@=i=4JZ8 zJa1%=Xnx4ka82myrg4BR|M@ss2P%+zlCTHLvX@_2@$|km-}0IoE`evKlZGT^nL!QPEa-;3^0ZRXGvUZD zE9clw5t9}^yhCPFi@u3`tSnsMW3fyZod#$(5`2p34if7+zMD_9$1y~F%e+&4zXXQu z_^fT~bf<55O!U0>FZM@Nns)qv*{fJ(%@Z!%+{E*%x^E{1UT_LXLl6|s{OiVdEX~A| zts)Ig$$8%hSdhfvNM@|JY2CqS*{uBg`Xhhy?Jr|yHn9%Xxeh{L=jYMu{sn6*<@AN^ zreNpcO%s0^#2ILL=r%oT`j3hksrZ7c3T2ro6JaJ10Iy!)OOkMb)eZKU&9gNe0*yv5 zA`)j}9#o%(CFvuBlD}4#g*Dpc_P+L2KR@+KsBM)D-b`89PnFmo*4RYZw=xoF;VaXR zyN!6!tp=!3bjHr+w*}1EZqO&T-@8B`_Fm1UnApzz++OavCJ@~%Pf~A9vFvDEah1Lr zVj7UM{V`ivpcS`?1=L`jF|+T54l8*>u1+6t?LYaLN>UT~qF;QPsE{8t=#P0Y|ITO4 zS>>sSM{ccO2_o+bSZ7iSGG=o-vqV;86O6x2j?f(#yjI+A%74_5&kI@HAWOT-C0l0T z8fz`B6pYtC__%N-0$_-ce9=3)@PgS!h~7cpGDNrXeWRkuuz|@#z*PQM-lNF+3X5UyQnz$@ET16M8=6`A*{%{5BbIXfnDnV0i}({`gj zh_!&zG)C>c`SlN{ht`=Njs;q5-NBNdB}WO_^`9b&tdBzdhqz9faO=|tr7*5yBd7Ng zUo=O-CFmL>wM=g6TC+2^JFiqG9gEg|>C@k3>q0n4=xl>!i}ZEWV=c zyF+3+7K`t>3cVL0T5}ai^qew4ZA#>-KW(I6t2482XjDe}K0&soc1H_~(aS;lNiJN@ zDqDNW-G@ikVckD8Y7?p$u0`Kf>l>$pt_R3S*eJ3i7m{OC8WQO4U$mCC<}fw5K*dy) zQfC_XVJ$WaNDpGV5QBT{vv!7ii>oY;Jq={4`tZ=I&S$px znSbGkDZTYA3MhVYDNy|~llS?bi<^q)I%Z>jRJ1}~QuVM!_uouM)Ll?#a@V7S=7gDq zKc94BtM~H=k2}A5C}dNxFHD zPS)liALBkLf)LJKTJ59W09SX{L*}i1(lIw6fnf25?0vPP+?RKBYi}HMWxJtG>-kPj zbmDF5N-A)XFDgy1-Kaa!d=%s&j=;tZMAA-%(K7c*^y^_%k9b`yY;0s^KSpVCnl@DT zIP<*GM?4+T=h^RJnSQR`?V4CRH1Bri>6CJ}b3%ZrA+WW(wYH_WuUwV4v&9k2rJV7^ zaBZM??#gb=iOEh{`j;bq_bt-cPwQjW;Pu{f2IFSj`!rC`1OpBu2cgI_r2+zYbr*rt3sKUSP({R1VD~h||>i=apl)`!Cgf5gELW)TbDfiXAtR$5p_8_x1m%6X6aVR_uAGHU8 zw37P2qIhImmb5?$`3RnObmYWU(D*A2 zG+7sL1o|q^FN*eODRnpQwanM2o~{;F2KNd%GUs==GVQ~g&CRc2-W!e1&+#_OW;Ofp zxan9unX8MUivVACV(JQxYGsX|7`A?o%3$pph+-)$ik*9TC}VTIhZL~+wNgj#-E1bf zHBJm)h$=D5%I$DRWMUGG>5o@qbkNn*b$!Z#G?~S0+;{7=c{z7?ki_yR^%xH)n;>16 zuv&_>wCGyIX)l5f$ z;zK0r>VAoW(RL<7VtMym*>boSt0?~BF}cv^qV|CR7BiprFt)r?Gb~?s0uD=j!*)2r zMpPT~RE6sLUI)^=9IWpgr3UwC31cfuJ5DOdJl6eHmn#bAlaU+?Dxz~LM-LC;iWCt5 zLhFQ#D!f3qYImI_BKZ6 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 zcmZvCc|4Tu_y0ZnVrC2`hAe|H$(nu3J(g6YQ9_oQvD8G#AQCZSkW|tpNx56pRH|u1 zRK`}?M~^L~q)n0~i4ya>d7jVnc|OnY`v>##GS_uq*E#R=KIgp8y^HPZMJ8$x0RSK` zUE;A4{uIpo;IZ)UhDF>*@CSMWYdH%5t~A?W@;<|Vt0#H}C9c}CJ~5da9|f=?w}eMA zmTu;5h*}xNjZEP`h;jx1qoJi9EdOm?;lJf_*9Ip27Qg_;6AO?Qc$D9vB~2BoLm9ps zV?6h#bA7ozQ8w3`^OS%gcY0>gchv|VZFz5##PQR5 z{Od_2I(EIGzgubf)Vm`qnV)>-8z}F;5)$n)z~;T0epU6!dE zfE2__sMOsey}5l!F&vNJxVSiRe0+Q!5s#NVXl}L`9v!tB9v+4`?8XE>U$}Ao`t=rA zVQq#y(~^luoBToc$@uvVgUR?kvH$IZgGMBsMQIabQXvOOM{K}@es<>_ zHhf%%?%eWqk%bYmR`Os&S!;_!$J{3c!(j3K`c?4iATxgB9X{9zZmv{{4t z-MFztC4%(6q$SvWKITsHnM3TD9i!7d@voSThu3u<{Zv7{vQCYo1d1FG7OXFP0;3(h zcstrc?~>3VBzWDrQkI)rCM!6&nC0WMhvnskD*-?y%EQl+P)a$rCmr=uB`8Z|nM-bv z*iuQN@0>qy3$(y81d}CDmL6UZNMCzrd(-gu&#Q9O%-33k-rlC`l#PapcXRTPVj};K0s{L7l z$Nj_{5(%5E?5}~=Of`{(I2Jd;y*0P+WgVHo+XXBz5VR3c;!A+B14wVgy+E8|^@DFQ z-c(8B@We<&j`tBWOXuz1bKefUxDNIAnp;?gV!CQ5eV31eF@vm&xvrx52msBg*s~eP zVKIWrzG1CntxJhcwy&t|WG20L?5F4H9QQ;IO%3S8Q*RMD$1HQs-z;AFFf`$5b>L2>-N$u- z&U@B5W=iBlNazDFyi{XzwdR*a#t6ZzAR!(@bBcq@{%8=(4nf+s- zJ@gJ6D&wQsAs(b63m?J$O=usqqxAVMopvl@^>RkVwQCEjvXBP;>~LVUl5{Pq;mI+( zx~~|vTj$LfwT978&(;A`*GNUwLe!Hxn8s4ph$WQ(Fa%g*gaEk)o7*kCwLq;%WbN$Y zq7Gh4VK>G^M`!A5`zto&jXNa|3KHzaK`uLfS&MV0aOKJ7K*B^42V9FR9;4-!y6^5V zx9&S;<*tGf7au@}Z{*asp$e%dq(5>IlDzPRHa3{RKuFom(-X6Hxu8o2%8Hx%wKdKS z7}KuU@`hqu7Yj_;sq@->d4b=I?x+AD(Bgy;#A~=x=K{Ms!Z}w5-+uVeM0)rc`lZ!c zjhk~7%Ef(1V*62u7VO5MH?=yiWXO*vjz}+eVmW4Su$NYpvJ&j?guL*PDI;5L+JHjJ zTB#WikkC|cHf${|tvUkY5X4%I5&HuMX!aaTzp0>$03G1Itk|3(Y-^%$Y}yp8KOw0Z zXO64``HEEVyAJ@}y{##`%;30`Dn7PcIXykylWS*X6#=N8?*WlEXF(-JB7?m@Ww?jM z2B#2qyq`h*a0C_EJ$4I$ymje?KlIh^ocloR{9AU~#$T5(Z z4FIZpyH1`w*|Vgwq5`|GOZclBQgvbWUJ0wg=1(MidOC$({M(k(_$+(z?~8SdF65kU zR1q0wMRLxhs;jGC6)9&H7OL9o3bs=qK0(&k+j82gZWm{)zYV%DXe`;xMzp4C20x!V zy&4JN8Nr4C`(m&LFUel=CKT&4Rj;I^)DyZTJRJFS3`(~W&txBd&W_0+mL<+9-*hC3 zwK`oEj!qu>+JSBsSetESXU=FodGh3FNl6La#MHFz4o+5u6kFRTd?Zy_2VD)vh-qmG zq*61^@A53$IJry@W<%;FWuW+ha!(?SF|4p9>`p-!4q_S0su1FNKsH%o zJ%QO|{S@&d{wy%oU6!;SXmSC8*sn!c4jp@nr@NIrr@_L&`$S%|k7L-7w@_wA7Ny3A z8$BGU+eytlF*ttYV%X91gN4Zz&X`ADC)O`sU6xxg^rZ47pr{}7jF_=5GBncA2^+-8 zBrgu)=4j;vg;#H0XDaXl{NP~1vVDXnULrPqkYBlcdAc@z>X%&VIOu9IN3@YMHJ$P8 z{R6qMyH+lZWr%Oi#XeClC+XuPZd00AB8nX(fSK5~nTsJhvFVxh%CxhPBs<5lO&Jc< z_yIWP=;%{@7O!>Z96opITR`r@&F$Pwg*OlJgKoeUf>zLwK; z#`JIX0VU4GAM)z0U#9QhyO+6gbjAAh9~ZGJ3yROD;8ARPpK#{5kOeY(4)Za{#Sz`v zL7DZm`AS7APdm;U-A^AOVR`*XlImr2Tn>}hrv3aThiOEv^VxowWtSw) zwF#dnRhI8QeHz#4{Dd(*O3b8S_8|uVnk@^GAhXq(10WagjfNzcY)fYONwWKLo@T*u z06^v*Km!`@_v4br&SnN*fbJ2+cad|}NSbjsV|S#bO)DrUh<;tYODcF#>nG3=)Hkcq z)(o31-fH2(!dJ=L3fAkBk z4{IMhevEdaw(mgLtOdu^#CXxi-!0AOOq854|6UC=VH_fuvkVrm42a)y>mEEvX?py4 zKwiG)x)h}*xvVY5C`31cU((*dTi7d2{0C)Ql~`N~F_o<;=ta_u&e#%JGjx0gW`?lS z$W4OsRjXD_zUk-~mpATG>1kQOzJMO%vbyQK7+2nnr{{u?G4L~Boi*)0H-f5k%UpoG zQ~@y1by5jnkkBkg$S-)$QuB>SjFZ2Vvuknp+yj#@pSgb;ogWskuao~WQ0$)>`E6lB zKwIaQ)5kCTQCeANVbP=-QEqvEE%sW$u1&F({Uuod?~Ml9+y^0>G$<+*3dh|jLRpt87FVD> zr$^r1*U6ataVZaR?cvxEFU?O&Q2$+(xrfCi6BF2-qF#We2H7K;$jp3@@40DGfgvc$ zH8E%fcdLU6_N-evS*!&q9PjI_t$$384|xmveJ#Plwfm4=0K4-9GTVmvn8j7hL$O1p zy%?GoRE2C(w={4DfuygUSb&-%)U^C_)57}K6eLHXNBTMeT`9Ax22nK6l=7frK+ z?2%1#m`_1IF3&w}GXlovOG`3ff%jxtx7IF7Ebvxe+F{0dOP8m*Nc}z&ZL-PnrcL>l z=eqBHPP&quV0t%fA*U{jtRL{W({j(Tm-_;dxrBXH%DmmSyd!2zPoai7q-6@txd_p4 z!IjT|HPurI*4kK-pnlrqe+>aup_+7=X8a}_5O})`NP`;G!O2n9d_wEBtPkn3BvV&S{pPU$g^U<682JQM@R))Pw8-d8D{_xWDhiF*1vlK(J)>O~1 zH@g9O=o)N+lOZ=s_vQSAP|sL=RGvONRM<fH?^A@t3e?So}u<+tnm0*hc{IiFkdU zHQ`V^SQEhVB=q@c%dVP02Oa2)omP7?>j>JJ|2Q`H+!aBu>JGRC5y(^o_I+qyneS?u zr4jUp38+Gb)Qx0Eon;YgUhm5pg)l>`|6p|1(&(o7%vg2CTy_tf6%Zy-#jWC*F)vv9_l)ieglaqn(!VrMc_umP&dY?}L*t=X?Kdy5e9^^SD7 z_KHVR(Z{RFP8Z0n`_N>jl)}x@ZnAa!Y&B75#}2J~YL1mf2hEs4|0F+PFH8#KlFPp`3*}E*4HTOfBEl+wU=BXoA@k{Nj8+B7v6Gtk z+Zgp8{nXFGS{gDF^Z&Ici%;5&&+Nd%moh=bi7u}}kTnFpmFPBg2(=qL7c1&l>U&Xh z`pZ-fWdiwk$Uyd(CR1iUi%*WhBWG3)3kN%eTGpMQCp!xOIf%N(!~ zYy2>@4w7tn4IC#WKo+;-lHe&By;TQ{c?^E~x=&T^eq94-(i*r%ntetw{FRx0kwXwD zB;M3mqMUf!0WVbs`mSZSOv`1Oitu+$W(Z4_#tyXtpUpTY2-$<~zKE*{Cbw<|v^G3|e99=EY$9ly(3Jv^OvSWoJQ-Nb8-Xl_bX|jWbG#_bCm1zvXPS zD~pe-nNOaim1O`>u}wCp#Cgl0=`Rqnp9IG7jFSN5W3riVM*5~uNE){s3q;>&`tGNOhxP)6(1svbB^ zh~nKdo%L+Zpeh=ixo6OM3Jk&ADOB|Q`oTXueF2%0eqqb7W;{UK;M`0AekLUb)lg=h z-eV^&!`Jw$pP{#|M7Pa>zWA#zZjYigU?iD;LK8l1kaNTKq*=4MvQB$KYxE2B*cXJ| zr^wabM}!Xp5AQ)%14(}|_5L?NroLN>eW000|o{G?vaQ6A|OXp>kgnzpCKwLD*C?o zJ}PIiT<3q2>Yqgaxj+#nU|(RJa7WYWEw69?M0G7k8ZQJ@2oMcjFU`p^LKV8_EUtr4 zEB~D(=rdwzT{w zMRoyMTSifM#&^f556?y8mw_xB2e^C8nFsMlld+7pD%;_u;%UgbmEbc&(CyyX<*3zzJP^)#`!ef1*FFoIpu2UjIY_w-?k+~Ax! zLy;t2s`E@aR7pw1PWj2^uadmG&oc7O{B;O=cTYgx)7fGvta~wz`?EC1IK7=S!%uK> zhYfmnW+8H$f{Aeeu2II=T56jy!r`?n8|ppl4S0kThQnVOnffYCl(Chd7R^>>ef`5StW zebZU>nQ~IFLN1rT`T6n5u?>(vx_Te>Mgi6O8ro5$%_H#a_}_T*EqL)63z@#qI9m7) zuIc|udFpYqH>iSd@Q2ieqaQvhMB$_jxo}PDl&%>}6x>7a@k&LbUVpR0Her~)ocoia znU-L7IqJPOxBTTKY{)n7pVnl^r!Q?A>1~Q(FRM_CUS$xbL*FGC2rDbxu8K(VV13;EGve$gw-A!GvFl+BA%%QPj9+OBUX_)|4 zjrG}k4I299wSWM-C?xZL8m?JmSb~&^d$(%lG6eYLLw7M}Ax{|28HnPg$i0TbUxiS> zSy*Bd-MZzVqNB4;{|3vxIyE;Pptb+{Ke`>LDuNLwH39*pWdQWmy+epKM}-AaH2eWu zV<}t)%zyge!LGXcdbjMuhiz2EzKPar`>-DW+ejn+0{YiqZ(UxY7=qk2JtNvh2MT9I z6|6WarN4E4oIc9Bg|l(v1J35ndf;0o=w#p zvd>)mId!)%_h1-+lq6tnfJO$D_D4oGq=*X=&y!t^I&k()`e4eEBHelO6vh)8PurX6 zxFiJ(9g9znI$Qn7e{~-I=+W(@{kFOe-#1|Iniu)f8#F#j?jy|NGUsZ(?YB>@euK>I zv`L5jzoCnsZ=V^0kq$l2*LESAC4pgz9Cviu1{aA__|wOaO91}`3pLjHEXtfnXn434Z5VkiFq)2|~vJr;&ke^MYcQMMXwl6ZVG+i!!XA zim@Q~Gm#j=ri<$Tzn0drAg){o1ol@y9M_k0295vt`f~5Zz$@5sI~LrFZT5oZ8ndSS z*xl&zBWHm&N0j9d@0A}3tJs~H^>kW0WP!_eVNyUoB2Q(eSIVU|AmBUO0tQc*4XuQ5)M`E!Wl6l?zQ*$)4-KX*q#tGEHAME5p$$ zAPek2lmJ0*may@G1N#+RygCp7TjGa?XV2t@R#v0(Yf+D;QL7l8+V&7yF0Ee~Ekl-qoVOQbS=z-_f%}2m}`T`VmKSbDN)?I(16^rQ?cR zKJv~>f`a8fAPL&HPm!tQtiBu_={2qLx4!vL$;hJM)@DW8IIQn2MFKZ<-XAS0I=gJ? z(y3V`b?cXQIUIn~^6{ZJa#)!`B(H{c^~}HFNZ+hN4g55jSx@>~QDboxh!KBjYNP_s z>c51!_Y-!*W06vShYxKR1ADy-O$a$!p2X<$nZf6Ko-TM_%#SYP8}+B#>jnJAFTx-D$<%b{zJ<-%2FI zjz|t#vnH9jV1azgrcJ+VV>&L&rDg>Cw#+;;_7o<%5eT{Vr`$Q-kxx-QN%@ks-n2(xP(SUOa&wYB}>Dp43! z2{Qi9@*;Wd+^frla6}bY7_Z0E0wh=+CW*x*>`t4(Zn2Q4!2ff@Wq`%LqB><(SeRDT z-Ao%2tNX~;mzDA>V)2AxxgMwCp0ECGKK@&c3eyDSB8hX^^t$Tpv(hKiWWQc%^r-2q z(im+B1nF5|=J-ilNwMXE;BtwXUhX2;LvnavE&4=3V=y|1ww;z>z?{8j#?a+#*PX(8 zfTjvrAYfG`NLWvyOOW~tkROj2iKh=SE&oV z?9zX1%@A}@a&mnPSGUh;r&H9F{En^}Og|f9wP5w$FwMH`bq73+9=Jbg%}1yfaYRZ1 zMckdbg_WNwKbogZ8VKlhXnp;gE`;b3CMrbk;O6EwAD*+!P9HpXS65f}SjB+H=D<|1 z`1&R?y8DDZzxV066{Y>cU-?#Gl7v(l6Ww|8)@$V(2WIuWvx>#fG1+8ZIL^94QR6jc z?^jX%dU(!K`}$6*9j>dZ_K_ee%(ZN$q2nEf6^quUL)c<1%J;aV@Utw$A*kTWSa{G< z-Jp7#uXJOxdZ%Y(XXO?h@$ON@qF>|gpRTiT>UU06oWlWvg(%+7+m<|p5s$3HQ&-~| zDDo_EyhS>RdlYy9iq=VXP=%DAOOE@(i%RCl?k!X4+IkgV_vcdh$60|D_EC2GW7z?UwaG=)jDjX6bX~;dZG%O)0$0|UGw-u4Z_MmZA6WO+!B-#sDVyKz z^_@BLpZTT4Sg7L|A4D2JrT{hIW2gi{WbD|nWB67U^v5NZ5WNf!y+Szl#L1_pdJdWi z2yYE6j%=EMRa{$abMg2FS&7@w?n_1RNKni@hj2WKO0-AuG8X+QEb{Pb{ttzE+J~xG z$85nbKe)I!!7@jg#$A{bhXj}3BTPR|EHSSY^lCEz^9`F&79?by?n?GNlysiDgS*V* zeAxQE9v`WN*TB$ih*4+rM)@WnZDaiIO8q?^9u5gB+kRVEJ-P}|JB3yn!7&8%tILDF z_irO|t%Ba-52qbwhkNW{y#KV*YyS^13W#w3eVo;4~FzZ8mr5xsjwD-<<#`ILLa z^E+E_eyLsF!rj_iG(hse+fSTR`xzgwBQQEPjrGT`Yh~B#EgdBT+P^OlN>)92yP)kB zXAL6(O8PbWAjZOZ+5O>78xsd)H(1?wy(`bZ#=MzM+YD@y6Tw$nGz7i@q_s&AN>#bY zd7Qbr48e}$iCCP=R(fpYF>CP06`Hg*%-?k%{_V28&IcEcU(j$Hel0DIx704bvbzyV zXr^c&*~YnGPC)^N2HZ!73!tC45p1k|Sqa@N=P)1&l*@)UCe6#4$*2^#V z94U)8ct|N-l!lh>eY9(>?8P;zaRL_6(Gfz9E^f2B+@|7`TpCfxa%h@X5!`aBLQt_M zEpG2rsoFE1KZri=zNp^&6{xtp|76`ERhjCvtBGKI$nWEriD&uBAFFEy;%+=Huf`eq zN*dDa6wcmuLI=O{xWFB{Gr49RTb;TXd?;!ptKZ}s7Z-15eaJcjov-w6A)0?Vs52V7 z=cm3XO*J~UucGEaskvuJ;o!XJ*YD0Qx<67}>jiuq?8buhoKw7c(RO>lM~u%eGO+8V zxRH5_mnfC()|c;YOg6wxY{{W`gJ>CF?*jqnSN9&0TeU`ZTABzJn07TLJLs``1P$?gZ5IaX=%(H$*! z@4M00{@RX~aeRR0OPko;3lJBy1rDZ2x_Y~`kwNY^mF7z@A3WHph(bE(pjya))YjFz zC_EZt*pl2Y0`@tFv3?sLLy~hwF=+n zg78%#B8QrT=)>R=>QWYc)K=RD$?tqzh~b=E%8OsJ1uJ!R-_{lX1lbTs+7Z*UY(H)s z0H(R0!-KD#rUYpw?fiP{V2PS%GuIP!PLu19QW>%2>wpfxITl!ZLAZ(#F+O~$;_T#) z@6QMJ0UE{x;En>}f*!bl7b%FX#2VriI3wl6Jv=|o6(PCR7iepfIBMpwb^!s73?pPv zV?(^dK$2-BDSCyVKUtPC3F&vRUy;H|3M^}y3b6f6$=t5p z)SPg2j#4bCv0tibj%kD*scaJ#Y2_YeZFMTQ7Q8-1j-}PdyTC6$-Eki=>VwA-7;nfa z3G65bw{&eoKIz_a^3uc=5zphmtc2_Cg6N6%h#OZRIjXBTVQD8ot7@;Qgq%%r;k3@M;`Fy2*1$gF|i4+NHa`g&Y; 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 zcmV;J5ohj+P)(_`g8%^e{{R4h=>PzAFaQARU;qF*m;eA5Z<1fd zMgRa29!W$&RCwC#oquedRT;-Wy>0K-t!uZAjs2o*V|6%w%J?mFfU+Q<$ggN1GZ=}9 zi9~~m(MU8B4MZX#F)`7YNF)-8MubR2CkVQM3~?KS;l^ZRTeogqyOy{``(>z@}~FR_q})T&htIb^L?Ik-g7Fl*(_y%3ZPO)wR5DMzxH*Nu1o0v z7}PNU^f^aHN1yYzU&kPj_{OVBxWTJ{X~1-#2AJs`Gn}JF*IFW2!el+;XMkRy2RPvz z-Ogb@>(%xB`hGSLpma#-e$$SzX90D<932hL>)AlP&es984iVChCqTyQ2TsbUCx9;C zL!eVf2XIVByZ(0Q+;I`ZCB$GLKyi#-DTB>$j6D}P1DFq-2`m5>>R15GE7AQN0a}1V zI$D6k`ur%+CSn{Hi83NrY{^g&Qw?Bad5vTA`7-blpwT(bmN%FR90xuC_UYIU91uB< zIArKico_%;7~Sw`x$D{L-HT=D<-iJHC2-ale|LwB&;m3&pYPGX+30-MC=XJre`_q* z3+w`R1MiCz2OTnWJ6<6Wz!P-V@D0Es_3S3#Twqm!#@GpLl)?AN5Zfnse;12*%XA;t z=>2GTSNHK>-N#;ehISF*WE_Yv)dZ-NGOtx!U#y1yC~%<~f9?q1-vev_UIJcG51$My z2G#;M0M~mS#78Tz6?jXe*ds60E}=Q49$~5oU}OAD$M7qF^MH>5t4E0IyMZTxC*)$| zfEB=c;NB6)@FDOfumyMr_@BZwDhaEK35|IA5n!Z8>m(jDEEUko4=JYXR;9p6B$&oxsmk0CVxG>KmJ-6Ar6x&>v?4RI8}7ST27J@aa4;?vhGs_`#>NBK{C)|heETWjR(S*0<=>;;A22*_27c{@gihd(YP+ot z2{Li;0BQC7vn2Lz0B*z7_+53oPJ$+2WIV6X`Db!v<$B;Q*()WFdjL~j?wd*i*qCpP zzYe&=``yjJEto-(fWolDA;BtrHbZS&p}K&b$~GZQP&GmF)H*T$Q^4)sZ*I_MUBRZ~ z(gkaPXEB8{R9Cs3+$7dPd4h3g2dN1dexa$7RVM=ljQ+Yx`}z1ar?EvXNivDJtM&vl>Wrgqh9|fr)X5GHt)q$3GH;V6O3_4 zc{~J2V@B>)C~v(9Gkwd=@xKs^UsPn2RlSTEJq0tcc9T5p2@#=Rq$pVku(^ICaETbX z#5uoRY|R9lmBbU=qKwMoY}w!v*<_X%hKuR}Dy2jhD)y|!Oysa=##H?}gRM%2R<-e~ z**x@mvRl=*J1(skcgq4L7phCU>48@bxdE8B? z^T!Vv;G16dwi@%`jnx7L5ulRXIRAN=Qlndd2*NMkf^f6*{!+|{?XVd^J_4jM<;0~# zYyCF%2gDoS_k{d~vSr@7Kq}As8JOi*niT&nUh&u!5Kp`yon~>7LV4cmU}~7uZwY2) zj%l9A2uO|~w5XRY7sP4jFy)EAnQ|595MAlw z$)Nlbmk~VWykCJCADFKYpUOvoD!GeAn9?sE0RoZ*-)2pi$vlyvVwV8Z<^CHLGb~>7 z3Jd|sz?+!o3oPvNdUM<`kn+_3Jf#foV@XehwoEKs+btru2I5dQkaFDrEX9bW&X)#& z--mWgv>0@;R5l(OAhHCgCbxLgGSB_DhjvUX_7PnubGbx*y=*>2fa#dI{R`zPEJEs+ zd_3=YfQ7R4G)sUKxpn&%j_~DxWWt>{SRotFu{yx6!EaaWbP3SusmOq2Vl&YPbu0m9 z_ylP1-2ZEY;!ioCM=7C8fCkyT+OFeQo5usZ7TT_?yns)DT1x=CT6Y7{hY7Zab}Jif zbzaZ(J%GDTKY;*c3E!4;Wb?G^0cJZFWC-i|loNJ(?`O*cOtT(frbmE1K>(H&x`+-B z*URQZ9$+S>66g{jpuFG?=+!9=Oj{37gBc632rGD&A6%QxlFf%gz;vQpw1nsV$`9>c zCNSM*0q*vzfdHq62e8}dR#^g6=MfD7soEt~hH#DpF%O_&$j ztE>soC7Wkbm}SPhG5gP0EDP;bc39!OK8}`{cL~rD2vF7;fE(63MS#q31h8ua1(Z+k zcgy`93IiFd0lGN#5wHtFyOj-mTOL!V4+J<(aIy3Hq--7W0LS&AJ7#=&Xt%O~yVhfi zLx4V82k6FZIdags@S4zWWr0efn^GT?t-CD&28JQP0xxw4NUr;L*Lgl9573QD!GlT( zj_IU}8$$af9=;6^DGTUDgF#sdgASc^=L6mx+Ar~NZP+XgU?Ujv(Hk-BtutKm^7oBjvv_7sC@wP^5qe+KxM z)c;|n_k&|0fbRwVmVCwK=q8of}SMT3FT;4xc0@%7hw{ppCz{^CRBmA0|dj?DnZd2P^ zbYS*(ehV||*G&P-{T8}LP)7g2|KtTWc;|mbC=*X6R;tZi{7trbU$*Kirc6Ei;W2JlzRTr-Q$Vv0lorNOT-Gqo*_0)LS$_TtR; zEfoQ>m~k&FfX$Z;~zAWSi_*eza;FH!^@J6CcKmHr433 z!@1^r(hQFTyA=fwbLxbgXO)q?qqyJ0I5zpH8s`mJJ2z;}W!h{XHH^N77xrFt(*@Evg6eo+ipOafRERAY9fS`3_znWnT3ILEtoGjI>l zYU1gLRhSB@zbMyEOrdQPxrv=!#qKo4CV++Akoru_z{O?q23LF6*;>J)r=dWs#muUD zfK%sG{6oF|4RUk(yC*96CX)bGU(CSlO1=~`cVG=>$r67);cno5;Dyt~__xXBH+r9U zU`F-+DVM(&Q+4ebZ=OGu1h9pSX-aDr1Lpx(0M`N^&2#$DfdkxuELCq+&E17ya_XXdz->wv%)~hT>exN z08F*e?p8imG4NdA5`~6U!+h@*;7Nsu&X^m$LHYdp5s1BAJ$?&j?*4w9#TzMbGDi{# z;A?|AV7^4+xe5)J=yg8ysEG8W^5eLmQgQuW{T*(m^bUEmw`BMaFe7;-kM(gQfYk;z zEYwRMG%7TlFEWg@V0EiJ!v^4`DX1r`R7P_>re;6VK1 z$B_VTSg6KqUR{qFI66yWaiubc^M(`^H@rZeVvz_^>NO1yV;;O{ zR?j}9&yL98ZRECO>6NM~PfR=%CLeS{yi$ zPyS-A)8$5o%A}Y;f)J24 zk2oP!Xb>?V^*&mr0FY9E1Z8|<)&a870E_-Nuh#%gPQZdCaC008%D7DNg8;@~=|~{i zaR7|yZMYm@D+-hjzlxLx47dSeYt`Svz+X;)TUp;)3HVtLv<=~7R{>BQfLkj(^f`d$ z2UzqlF!%t76o6RaSYPajy&Qjw4b)Uhl~^OAuzavFl+z7UU!RMXbx4_vomjvU#WGcj ztHU>if+v^{Z~0^Z0J7sqz}cSM`3&Kf4-E-K*WsFT?jkWB=wH8HyWbctag_pq6_0?? zdk%OtWsoRFkkfrG(*c^T8FsekMU-_Fo>VQ6y)&zS;`*o?`JC9snVF6C^}i~=WsEKR z^grC&beOm4-#K0dh}~YCEw`>R2J@H(E23R4cMP1WyGVJrBds>BjM@Y7xb;upvH!}Ja6xhf03 zfk)fw1ppj3xwKEPLQ#WkLso{p9}Xq&6tkItAZz7#Hvlk`W8~89tCsAB0)Sk0FxyXQ zs?&CIjy4SHcJ%pn>{|-~gbd^F4jFuzCssk!?iSBWKgzI%4gREIvw%-X(eSlt*@h*! z;&OH9HQO#R$k`exvBjH>k&x}?ZrdiuDjUj9}30i$N*Bbdb!#6m@Ab+FJU{7|i7 z{zY*0oBTD;i%$kh91K&0F+^~Dzi`>#f{cU_vOFw}u_(%w-*v|b#}vk#wHbCeiqiO$ zC>auZXRREnFonNzz}S02*E7~T*Ll~e*6EIopJcq2a{GR%v&yR7XRkuBfxp4Gf${bm zkF;K1s`kQ<99;tb7$cdooI33atxrFQ_&igE2SZ4eRrAYo6UQlS$!ss|Dz-g-5iBIq zzDyp`AM4*y+)my`J@v+U8b%V*9d5%)kw%V5ZbRWsUhz$>fMPs~qFa|+@WtM@trWZz z7d->EIyQfjxV|TeDv8XARBUkFqT+($#bRu|6TN=jm?ATs59JDaygF+|SQAMldc_%< zdpcA)HbtU8J{8;iaM6|g9$Ix{*8 zg-;4e)x|~5%DIXi@~Lu~hRDUb_}yH@XyDy*M_RI`ol>&-mWh_hD~PzpQL(&9g)a(tpY8sbtTC=R{c-w} z$Be;D$E>QptA3EGt=?Ghyg*bqZ5J2X)QPp+>@n`38DMrqv2%8D&gUO=V4m1ci++6JC~NPT6$+JgV!hE2^jX zx$zfcuDs5v?t`VxYciYquJX=)S>hP z$q!wpNjq>GgJJyPzr1U8ME%cy+hypajVs!a*)+ZjISG%7Q2p4?l`O%PM)hZ*r)6Em zC2~6~l?G_Y zY6RQVZZW{$z~l7GrhnGwdlz3=9+F9%OSosPNvw$pF7hlInhyxJ)%Vo*YjnC~nujjQ z1~NQYJ-mMy1qy?hAU3EG!P3FTC^why{ppa=k|J|&&#Pwkr9WpkN|Q*(d@TF8(OyPp z+w)&+8QsO!x1qQpJKRvsfbFRfOv_EnsLlW!me5b3`eBmMuHgohqc4*Ay+`L|-z~{q z%O%MzrrEsE5U$`<;b)U5=CT)?75v5bOY#qwwc)^|L56{{qbjv97hAc2vt#Sh?f%K9 z)r?i_H9xX%@E%iF-<+Y2)6hcFA}&G>kt8X#@Rveg|4HMs{UH6!bWG&U6#c}vgQmlV z9zH5C8va+-gpKy)u@am5`}qIjn)!VhyLj_CHX0EuMX}9lPt_LEL^4TJyl(36vtd@* zyw$fx4o36|CzWKf5AF|C_szu$rXOZK>=SknuEf{nhpFmw(im*El-dhdiMjdCJy|0` zXAgUYQ7YCDS;yuysZp}8pIjc$!u(#{A=s?F?_+x{#9|vdWI6F1Su#oBU)Fh_4XRHreaMvguQf#_zZ)G^dM zOa|P#>BfmknlW)*?U*U{Jw@E^XRY@chu)k|1)l1CN&mCT;40_4zH@=KbV2)6WY+7m zU+D_w_%kmW8ydL1OzLr}LYhwMjqz*aUm61vqf@(!?{5|7p5dkQi5dE;oapx2&F*|0 zPp~;2{+yAR@gacgYTjZlx6W_t&CrrPp3C7ce2=!v~TmyJ{g@?yub(z z8NQD@i@}pck4cH?4|^K6mnkG6EW9e|ez$X5C_8L5%$!MfpL^e5PDTn3xSZUZ9vb~N znv;B-oPc!l&%fJk+oPzM`_u7MVm&x2|sUF)-=>J#?>?tJyYb6h zXoPb|hVHFhwO#ouYIi#PCoG|3e4N;J0)Fv?C^=~fv?y1XgrqX#vDl5|bIR*cR>nzA zhDjWA@d{%Xit$X}BRo^Qtrsc8k?({sWBE^pM2AoOGM5fN*vRE;KSu*m4H2Ij2%o_o z5Ck>=|M3e2Y*LRM5Y7C5bPap#{2vd+K2g|((E2O~Bd-E?7GJpDuXzMAoV~jmmhKP| z6%mQV&n?g)PF2#Mw%rb*Tl)QXSi&lWnc^HCy2 ztxG0HlQl){I-%FffhVO@BVW5)yJ*4&l;S|GTyWYiYromr{h>S{Q}s~8`9!5*j7kkGr{YsGW*T*g*ISy`D;5Tz?E=#o%e@O7Wn zJ^vI$(|`yI??zSK3z0GrhK!Dm#$`@XlTaH!rNIbUF{+*s3$rBmyKYRD7*c~WQFUaq zrV5hdY(R6|5hK)y5e<1Q9E_0m0y%m&juf?nJl5TX)z#N8ZWy~po7}adFVo_5+mvP`MCgyD(@oXRu>2u z6mI43i=*@WV!4QvmLZNbz`<_T2k+P+*54XRDJCJ^YZR*Er$jiN^PJEQ;*fTWA!fr0 zU9EhIg54_&eM-zuZtF11a#^TpvJzteArx(wFyi;39DQSBsUyNT(m!EpRrTZ@4;>C+ zDd0#G6hA3R*3qs`zVk7+w)RXpr2SuZ*Xxw7s#4x$xqkWTLIxa@{aR{-TQH8Z6m$U= zxymapE^Z!~due89IFr_>8^CYmPfe4yDFS#VU9Iz9yBqr2Iy;vOkmH;PM>ow`SL^9) zO)HU%{?|yPuo=OYpKHyoXprPt#o_18miUVoA9|a{8tF$A@%v94gQ7rR{eSFtM%ChB zTp~c@7oo-`)FK+SckfQbaXJU84LItL#msZfsG@D0Oj4gId;LEZpQlI+_CC$UU7RhQ*Cp&DRf{XkmmIAyx+%& z`?`ixkqf`w37o;m%*<5-Yh*=Ya`K_x7-rv@h{pn?r{eI43*GGA_syF({QXtzXwsKl z!za;>3F-%Zk)OD~_Yv43f_0*QayVEUo8h}I9it{mW}{G!*$f^Asl!-Zh452Wj+cBS507CNZR$;mOEot>>qPELmF&$7h- z8Z=4(?d{sEd{E)bmoG8@#E@+(t^*3mE!^_QHefg{y##rq23;q&gjvHE-bX(E zvhwm>xYVgM7ra~|yTI4+!-iz4y}i9CiZI_9``UYkZntyCUc8HPiFxCo4Q3AQjqrAh zY);btFQCr6ot*X`VwJ+9*xA{eT)pgsl6CkOAw9_>?g5J)pN{I<) zBbjg`Nv?Ac((UFIy%xa7&COi`Ziutu;$p$czriZ01mQ=PFn2XreC|3&tLd;fR27#DB5pw?`)AUs*Ch9l^|DkSoilP+@NN}vwQ1wi4WHV zQC(GX_=vr)Q-|&x`73G$jkQg)h+en!Hqa_DF)#}F1qCaf@mdc=|95Dqa!x){gPH9i zV?*@s?CcCa=UDfQ;>({^lRK>E`$Aa8!5j0X-)jQn9K02n>Gh~mM-=~$7n|9(dh5q(xz!}!$cOQx>lSO8&29Qa3sVRl#U`5>d>-R@@cqLW+l z;(d!w?&$VMEF$J~t+gv+8XsV{?8ru2S|d~JC>#duN0fr9rfIA8p7Tl=%Gd3;5j0gl{XL5?BJu6<1x?2-qq(_DB``o0$ZvvMLIO9-U1 zd6d|i9A}^+H8r)kJDR5HRm9&(C(P`7S@DU`^@^L8&J~|i`@^OFubQrXB=Ll=oE|>b z45}!+k*4V9QB6xrWB(IPbMtlHC3?&a)797Fxhk|f<{5m`_ui>+{L1vYiY2LMd}NoY zboL%S!X5F}uzJDy!A(_F)sTZUP2bdXfhpNLe+^eEK^Zu{>b378}kxR2~IUYSK2z(A(l~$p-r=?pVK$uNQ(Rq zRZ+Cxo{=Kjo~1OtgclqpCa(qO7Z9Lid6|A$NByGW2vO^HENWk zTXF@B`YoS&1t?Y_vqom~3;n9`!B-Yc=MWRlqSo8v{=#7vWFtq~fvn216}{qjJ>fh2 zFtexKCtYFglhe~IEZ`XB=mSi#Ail))ksMi#zx!U(U3HS8YwV(pGNhKVZ-fevUUyuD zscL_C5ZdJx$a@hH&)tc^5eVxxgj&R7JFcUnV+&oHJW+KyzL`I{)HLjhLvf6yX2y|L zUTfl{P$Mf^Elt1)^QAB5 zBn_LX4(A&pbD1OhV6Qj;eJ{Z4VJRtRUgN*-N=Ed zm!eQh_+JV#F)>{{y?ElTttQDSys=}X!ylnt;{MyYeo0?n|01nOr|_Ow48TFS{#$N0 zCng~gAL7QzLm!7D*Ta+Uuzfc(3he$^SBnQ6ulE-0j29wNXoAqj563kmTZVT$?gy-R zux8a$9XFttWbXK3@mpN1toWs`_J*UMNiMoBcuwz6oehWBj6v3`lwUvGqcS)O%&9aj zm}QzP=b0^AuWGPvnA?63o^%vQ?p`}eRE(w;7IT>5Fc&x~Zo-8npAR|;AhmRLJ;c8S zs9opyrH-vBxLM?WAwF{yhe>^qG{QB;Xfx&)@P6Bfmpb^`j z4W#fow4?ScpWNMCEUkHnJcOq`X2 zI6QD8e9-!)5RSZcfB0{v+IzbXHC99z6PMZoR3aQr+Vw{k-}Tj`SuBH<_^cyO3bm}P zEKRM9&*DM`ZL?c?_cftG+wW&KQU3t^H^fpO*4RYqla z&qO?IFVqDWYV-w}Ej9%==cJjS9rS5~uViw=rf#k*h4pBJUspnzK#`(YBgZ5lCg;_D zhU)*dez-}})zuxKJ0s8;BYM?;FW(9;Bqb#s6869RM~E1C&yN)pzpLQ9@Qc4ByD5C3 z$y-!KRaNqxNJhfDs{rPn^JL5FJCj}VDs_X~X^n%c{GpcA>CBxsE3c>@9OGGs2&ZzWT%c_H{Vw7jz2Kpb`zf5@pu`nkmrCeBRLt08C!I} zRBbct`S$JG3!4NED>O%uj-DmNWc-fen0qk{dhxJ~De1GP5qLeWrS1*P=mu8LH2-z3 zPgZtHK09Neoaravratl@v%e-2NGo%+u&{U`v;AQE3$-WrdP&%R;zv`t2kHh6`7t}S zvoQS^?$gt>u`cLltUI)%8OO3>xZFqIzwSoW|Xgz_4y>+mdCz zJD;XjAd@$GSplWnCy=r>4j$I%Iap{qY?-afs@dfc>tvHAzsg^#uS!qH>-H0%?RmV)N#m;xP5b5OV2-G&Zx#bJ`SaG zB9JPe3YNHB4iRNzXW#nXbktycnqa|`l36*oFYjL8?q2^<1(Th&qfOOd zeb2jUU8ycC4+fQQxA;c^W?_8e5FGCWuY-9zE)adO-xOl>jt#nh4(1bqtDjq?QszIq?K%a$i5pz46Tg;HEQ5QhlRM~ZFLJW8mnDR;q^i*VA_E_mOa|(l z=K4Uq;x}(yE$miTdu`V)9lb2}X*R4d{RkiTlb;yv@WB3Dy_0}w2PPXX3t`U&A26`K1Rqd@wkS2DHC!%{Mr{nK1p->-cULonVJ7S- z%jT>ZrB=$h9&dUW1->Y(Ww;XrSz-MHJ?@7+lzPXR)o?d5);s+Nb2o5By43j3KUSd9coEPpb+?enY zqjhm~Y_{o7;>^OIgMe>q^2xMOF4;cT_q^*P&^B^qS`Vj86d!L~7K-L%Wo^KwljDMX z8{ABT2MXCXV0DeUJE)_?%8~y}XUnoq4&er&_~36}Ai0%jfxdHp`#k<`Fo+347Hb8Z~@Y7Re9nQ$=_Zptsy z4hsYQq1<1;G8mn}*BRmYNjl0W@F!26l(USXa;K;f@nFH2N*cccXb&awf^erKl3*CVz0ZXmE)IoD9fLk$mdB! z=hjh#bDOE@W{@LM)Ru-xEb=HncLK(rdUqvN+lNq()(7S!Z|D|DwuXEG$ipmoW_n1;WO*kk*FpSh6%BWI_y|tY$}TyW`;=X=X2m>qpI|w@ zN9Z#`m)TJcMHw^$C34;#0u>USSLhG)@@No{T4C(3!VqVs@gRdKDxJaQwA2Wt0g#qd z*7_(-{&KjvMNH8-W>-b>SPlNGXjzuvF?>^N3 zUYyG64cIanoj8C?~I6x7ng2F_P{$dWut$u9ZAzerHH%k*~R~h zRXt{xB|nbFzQwNU=EQ6-l6KBzJqHz4X;7+R|M7X^x5i!5fAFst@a!h?dzZB%pJ&er zvyJWXd)3L;`w0HQgSg$PLMEC!XCNM|S`mr%EA2?2V+6$#)CVp-6c(rCQ{_vk8S%TX zC3HF_(9P}QTY83eYb<)iy}TIh1->*eYI|zJT!E%DMg=pCUcaV^Wf(Cvxt;S73y6lI zfV8OjT6vqjKi;}h072MPvd;L1>7VWd5v);L;{AoD`X&3?RAra7iR;Yx>B0%`bqP$n zX}Yb;YH=?i>ga(C(d5t59~lsufq{V)gVwz4AM(B?kt{!&9(7M0)z0DJVV9>f2ON!p zoP0Z*wb&$DfCd`E@qn@(evNn`B=lFB7=rWkY2)+AUzQD62^~X5Fc8Yr$dS*Qy`-U} zbSIJrLPA138{#n)JJ?f{V2Tq742?t>hI8%JEn&8NMv6EP7($bS^rnQeH)QPwrdab_ z@pK)GXD)w5f#|h!%#e0hOUoRXen;#W-F6Ch4Wr`mByxLi5BnW;{WD`Oyt)e5Mo-AN=-Om_mfOhlze;k-Q{beoX?6AzxUZO`;>t+^)PK_>9N zYhw%K^=M+m&;KgriUg?=;9v~1DFY2`l#kn?2Bvs@eLb`qO;L&l!UH^dL~95X9UlWf zED0+QiF|;KqTxr#Nkl5-J^VQli}C>e!T$|?(!NI@a$xB~hIhR_3JwmA`MIPsAACF{ zddT;MR{Yl!NE;G(K$-JxF>^Uti;B#iU(^}?{FGv#}nNHiq?|f z@0)_W4k{|@oK*^$@ZYOJ!KGWP-HD<=OgreK*Be8cgQ@dFrtfCT96#No{>gCyKI5aH z0KzElXd&OBp)$|ho3xFSMA4YegDC79ai*-6xmvz0Z>iocqg{4g*u>g*!{dk2S> zjEszKx>JZdGp5~rDE0P#|B1EQPqI~*oDBAOE^D&ZP|=-U;Ew^Z42}J9*uIQZwiGS{ zhaer=jGqgLdy%X3#GRje!9l)foDsrd(%{;2coB$8hY38;3wHdwxyynZ)z~T}patQ} zeE9HT?)v5?4_^U*Mg0e504=0*<)6SzH^+s@?&e_HgNWPeZz`A=5Hv74GU9p}!IJrn z^TOA*QDbC${KVW<6aw&^D0rcelav2lKJrOjNkRcq+SiWw@NjS(N6;Y?Q&S2eqN0)? z=bivoVM9YhG2r|de!&0){*zD{WGE&A2s#BtTFBAdu2;#>AV6_#H0hq3o5O3{M+d?b z{rqkq$^D)t`j5hbOok2&AKK1|0hFgQLR z15i3*D4LnGi_2kgDCtx_WVgicb%;bR1q_g6nB+*x;H2MRTP{;xhe}2U6KIP@*8S(b?JS zp|N~b?^r(ONQAk5i-(vC^6q>KBsYIyb)aXnOiWJ)zHnJ=e)xh<&$eRQvH5&*W~Py{ z4h!`Z0hXzvLHn5e&LkOu7L>$kd~WIBu!mHnC%BX*2-+lwkjpJAD|={loTChPI^vGWq44~aZgPA=WJ)^%ZU#~(0vDt zq(Pgjuc>K%1sX`&Y30~=JlD}&`HF^xgR)8kHN*%?Y_tIf^Dppc6e;I>dF(Z&DYRy4 zDnV!ez|Db_3)ygp(x0QXZtuY?$p=W;=j5qADb(OLB2Q1xho^KHFp|d(4HPYNWb7Ni zENILn!&@hVlJ7PL+@Sn`ozP<^YgiC8oihleM-inET)5nVf&vAoF!0>g-roMyM=Z*O zYx;TFmyD^|Akg+(!RjFiZA>AF3|_W?7_Ekbs%rm}{J=4G7c9^#Cc({HJNLaLmq0on zgAS^R4VGJ-oa=9{Hu{qfM|Sx~jA$;a-u4R!^7EI4VtxYbv%!6nS5#!T|9IEoO2n2E zFW_jkD{bCcFcy$HTK8^Ra3F)5H7oB;%Q)Pjkb>Q#`Nnq+L1=&q(?jFCttk$(W}iIT zaz$RM;4#a)>$4LuoZA~rWSs|N=MYx|SoOXY=oV~?9?BnsZ@GYpo$%qS%TZgHT-0B*Xt-+4)0 zU0we7)(KSTGEb>l%k}K>@v$JYda4I60xtDrcW)2xeaoVagM&lN&e64pGPr+FJFwxz zq8UT=g=#6sreT)0(6>?o`0_6dL7%>rnVIPeZWPn^EiV@c+vJdhrAl{%W zg`~iUpU6{EQPqoI%)isvxij+Ac>KrRl8RrCWJ(ORa1~Am6*u+l+?=S9k&yrw=#ZTx zOkB+HK?hZ4Tz?P;inn2bi&7p2l2HL0XYZ%&d>!EUZQKU6_LGRQv9YH_EH9g*wtPut zXhAPHYRmUnVbZd(+4`8suAkXk=rGKV(h&ZXN}$=q(C{h`q&px(%P{Y&1 zqq)_pH(t1sdmBHT+7Vtejz@d5OW}0U>WpYW)&C1 z?G~ZSN`{)n?wbfFemZWOl+vs+W%$@ zgPwpIKA;tU@4??3p(FyP9Ph<4|A4PO>3MeP-wF?DU-tXyLLHCfj|!6b#j7us=<&gmE%Qw57+qX9hx z4pXHR{oDhb3VBCHd80zTjsp4y97%-I_@xSYaXRnCK#X4rGWolQhZl?bl#? zlgk=_u}OmuNYRNs+@8qebgJ1HdeL{gB~o`MzN6Ofs45Lv6Qb&Nvrc;J8@=X}^p+yb z5{FNH93uog*%PUfuU!Q4Eh@|j%38!kM?NN>yIzli`QDeV*Kr0MQ)DsSsFel-&!W1T z!BmWiHbyW!*Jt%=E9)>!^nVAB_@4YN0uT5<9`%QT?f*KA^nX3{d-VYiz>Um*z)H8# 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 0807/6909] 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 0808/6909] 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 0809/6909] 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 0810/6909] 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 0811/6909] 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 0812/6909] 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 0813/6909] 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 0814/6909] 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 0815/6909] 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 0816/6909] 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 0817/6909] 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 0818/6909] 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 0819/6909] 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 0820/6909] 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 0821/6909] 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 0822/6909] 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 0823/6909] 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 0824/6909] 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 0825/6909] 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 0826/6909] 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 0827/6909] 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 0828/6909] 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 0829/6909] 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 0830/6909] 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 0831/6909] 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 0832/6909] 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 0833/6909] 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 0834/6909] 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 0835/6909] 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 0836/6909] 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 0837/6909] 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 0838/6909] 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 0839/6909] 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 0840/6909] 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 0841/6909] 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 0842/6909] 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 0843/6909] 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 0844/6909] 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 0845/6909] 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 0846/6909] 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 0847/6909] 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 0848/6909] 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 0849/6909] 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 0850/6909] 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 0851/6909] 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 0852/6909] 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 0853/6909] 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 0854/6909] 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 0855/6909] 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 0856/6909] 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 0857/6909] 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 0858/6909] 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 0859/6909] 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 0860/6909] 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 0861/6909] 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 0862/6909] 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 0863/6909] 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 0864/6909] 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 0865/6909] 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 0866/6909] 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 0867/6909] 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 0868/6909] 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 0869/6909] 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 0870/6909] 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 0871/6909] 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 0872/6909] 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 0873/6909] 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 0874/6909] 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 0875/6909] 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 0876/6909] 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 0877/6909] 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 0878/6909] 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 0879/6909] 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 0880/6909] 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 0881/6909] 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 0882/6909] 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 0883/6909] 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 0884/6909] 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 0885/6909] 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 0886/6909] 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 ea29f7c34435635b1b9f2a3e5a12acdefce1d100 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 4 Mar 2020 17:01:37 +0100 Subject: [PATCH 0887/6909] Use an OsuAnimatedButton in LoginPlaceholder to get the correct animations. --- .../Online/Placeholders/LoginPlaceholder.cs | 48 +++++++++---------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/osu.Game/Online/Placeholders/LoginPlaceholder.cs b/osu.Game/Online/Placeholders/LoginPlaceholder.cs index 73b0fa27c3..a17fb8f2b1 100644 --- a/osu.Game/Online/Placeholders/LoginPlaceholder.cs +++ b/osu.Game/Online/Placeholders/LoginPlaceholder.cs @@ -4,43 +4,39 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; namespace osu.Game.Online.Placeholders { public sealed class LoginPlaceholder : Placeholder { - [Resolved(CanBeNull = true)] - private LoginOverlay login { get; set; } - public LoginPlaceholder(string actionMessage) { - AddIcon(FontAwesome.Solid.UserLock, cp => + AddArbitraryDrawable(new LoginButton(actionMessage)); + } + + private class LoginButton : OsuAnimatedButton + { + [Resolved(CanBeNull = true)] + private LoginOverlay login { get; set; } + + public LoginButton(string actionMessage) { - cp.Font = cp.Font.With(size: TEXT_SIZE); - cp.Padding = new MarginPadding { Right = 10 }; - }); + AutoSizeAxes = Axes.Both; - AddText(actionMessage); - } + Child = new OsuTextFlowContainer(cp => cp.Font = cp.Font.With(size: TEXT_SIZE)) + .With(t => t.AutoSizeAxes = Axes.Both) + .With(t => t.AddIcon(FontAwesome.Solid.UserLock, icon => + { + icon.Padding = new MarginPadding { Right = 10 }; + })) + .With(t => t.AddText(actionMessage)) + .With(t => t.Margin = new MarginPadding(5)); - protected override bool OnMouseDown(MouseDownEvent e) - { - this.ScaleTo(0.8f, 4000, Easing.OutQuint); - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - this.ScaleTo(1, 1000, Easing.OutElastic); - base.OnMouseUp(e); - } - - protected override bool OnClick(ClickEvent e) - { - login?.Show(); - return base.OnClick(e); + Action = () => login?.Show(); + } } } } From b1b3e01abdc6dc7ccde647f262b1936cc6e7265b Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 4 Mar 2020 22:59:48 +0100 Subject: [PATCH 0888/6909] Apply review suggestion. --- .../Online/Placeholders/LoginPlaceholder.cs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Placeholders/LoginPlaceholder.cs b/osu.Game/Online/Placeholders/LoginPlaceholder.cs index a17fb8f2b1..543c108642 100644 --- a/osu.Game/Online/Placeholders/LoginPlaceholder.cs +++ b/osu.Game/Online/Placeholders/LoginPlaceholder.cs @@ -26,14 +26,20 @@ namespace osu.Game.Online.Placeholders { AutoSizeAxes = Axes.Both; - Child = new OsuTextFlowContainer(cp => cp.Font = cp.Font.With(size: TEXT_SIZE)) - .With(t => t.AutoSizeAxes = Axes.Both) - .With(t => t.AddIcon(FontAwesome.Solid.UserLock, icon => - { - icon.Padding = new MarginPadding { Right = 10 }; - })) - .With(t => t.AddText(actionMessage)) - .With(t => t.Margin = new MarginPadding(5)); + var textFlowContainer = new OsuTextFlowContainer(cp => cp.Font = cp.Font.With(size: TEXT_SIZE)) + { + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding(5) + }; + + Child = textFlowContainer; + + textFlowContainer.AddIcon(FontAwesome.Solid.UserLock, icon => + { + icon.Padding = new MarginPadding { Right = 10 }; + }); + + textFlowContainer.AddText(actionMessage); Action = () => login?.Show(); } From e136ecec5f131087bd2f1f2ba4df3ee79576adde Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 6 Mar 2020 22:12:02 +0100 Subject: [PATCH 0889/6909] Create ClickablePlaceholder and make of use it where applicable. --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 2 +- .../TestSceneDeleteLocalScore.cs | 2 +- osu.Game/Online/Leaderboards/Leaderboard.cs | 5 +- .../RetrievalFailurePlaceholder.cs | 65 ------------------- .../Placeholders/ClickablePlaceholder.cs | 38 +++++++++++ .../Online/Placeholders/LoginPlaceholder.cs | 39 ++--------- 6 files changed, 49 insertions(+), 102 deletions(-) delete mode 100644 osu.Game/Online/Leaderboards/RetrievalFailurePlaceholder.cs create mode 100644 osu.Game/Online/Placeholders/ClickablePlaceholder.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 1198488bda..44c77b1bd3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.SongSelect { typeof(Placeholder), typeof(MessagePlaceholder), - typeof(RetrievalFailurePlaceholder), + typeof(ClickablePlaceholder), typeof(UserTopScoreContainer), typeof(Leaderboard), }; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index a812b4dc79..fdeff7e434 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.UserInterface { typeof(Placeholder), typeof(MessagePlaceholder), - typeof(RetrievalFailurePlaceholder), + typeof(ClickablePlaceholder), typeof(UserTopScoreContainer), typeof(Leaderboard), typeof(LeaderboardScore), diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index e2a817aaff..cb70cfb97f 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Framework.Threading; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; @@ -133,9 +134,9 @@ namespace osu.Game.Online.Leaderboards switch (placeholderState = value) { case PlaceholderState.NetworkFailure: - replacePlaceholder(new RetrievalFailurePlaceholder + replacePlaceholder(new ClickablePlaceholder(@"Couldn't fetch scores!", FontAwesome.Solid.Sync) { - OnRetry = UpdateScores, + Action = UpdateScores, }); break; diff --git a/osu.Game/Online/Leaderboards/RetrievalFailurePlaceholder.cs b/osu.Game/Online/Leaderboards/RetrievalFailurePlaceholder.cs deleted file mode 100644 index d109f28e72..0000000000 --- a/osu.Game/Online/Leaderboards/RetrievalFailurePlaceholder.cs +++ /dev/null @@ -1,65 +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.Sprites; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; -using osu.Game.Online.Placeholders; -using osuTK; - -namespace osu.Game.Online.Leaderboards -{ - public class RetrievalFailurePlaceholder : Placeholder - { - public Action OnRetry; - - public RetrievalFailurePlaceholder() - { - AddArbitraryDrawable(new RetryButton - { - Action = () => OnRetry?.Invoke(), - Padding = new MarginPadding { Right = 10 } - }); - - AddText(@"Couldn't retrieve scores!"); - } - - public class RetryButton : OsuHoverContainer - { - private readonly SpriteIcon icon; - - public new Action Action; - - public RetryButton() - { - AutoSizeAxes = Axes.Both; - - Child = new OsuClickableContainer - { - AutoSizeAxes = Axes.Both, - Action = () => Action?.Invoke(), - Child = icon = new SpriteIcon - { - Icon = FontAwesome.Solid.Sync, - Size = new Vector2(TEXT_SIZE), - Shadow = true, - }, - }; - } - - protected override bool OnMouseDown(MouseDownEvent e) - { - icon.ScaleTo(0.8f, 4000, Easing.OutQuint); - return base.OnMouseDown(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - icon.ScaleTo(1, 1000, Easing.OutElastic); - base.OnMouseUp(e); - } - } - } -} diff --git a/osu.Game/Online/Placeholders/ClickablePlaceholder.cs b/osu.Game/Online/Placeholders/ClickablePlaceholder.cs new file mode 100644 index 0000000000..936ad79c64 --- /dev/null +++ b/osu.Game/Online/Placeholders/ClickablePlaceholder.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Online.Placeholders +{ + public class ClickablePlaceholder : Placeholder + { + public Action Action; + + public ClickablePlaceholder(string actionMessage, IconUsage icon) + { + OsuTextFlowContainer textFlow; + + AddArbitraryDrawable(new OsuAnimatedButton + { + AutoSizeAxes = Framework.Graphics.Axes.Both, + Child = textFlow = new OsuTextFlowContainer(cp => cp.Font = cp.Font.With(size: TEXT_SIZE)) + { + AutoSizeAxes = Framework.Graphics.Axes.Both, + Margin = new Framework.Graphics.MarginPadding(5) + }, + Action = () => Action?.Invoke() + }); + + textFlow.AddIcon(icon, i => + { + i.Padding = new Framework.Graphics.MarginPadding { Right = 10 }; + }); + + textFlow.AddText(actionMessage); + } + } +} diff --git a/osu.Game/Online/Placeholders/LoginPlaceholder.cs b/osu.Game/Online/Placeholders/LoginPlaceholder.cs index 543c108642..f8a326a52e 100644 --- a/osu.Game/Online/Placeholders/LoginPlaceholder.cs +++ b/osu.Game/Online/Placeholders/LoginPlaceholder.cs @@ -2,47 +2,20 @@ // 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.Graphics.Containers; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; namespace osu.Game.Online.Placeholders { - public sealed class LoginPlaceholder : Placeholder + public sealed class LoginPlaceholder : ClickablePlaceholder { + [Resolved(CanBeNull = true)] + private LoginOverlay login { get; set; } + public LoginPlaceholder(string actionMessage) + : base(actionMessage, FontAwesome.Solid.UserLock) { - AddArbitraryDrawable(new LoginButton(actionMessage)); - } - - private class LoginButton : OsuAnimatedButton - { - [Resolved(CanBeNull = true)] - private LoginOverlay login { get; set; } - - public LoginButton(string actionMessage) - { - AutoSizeAxes = Axes.Both; - - var textFlowContainer = new OsuTextFlowContainer(cp => cp.Font = cp.Font.With(size: TEXT_SIZE)) - { - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding(5) - }; - - Child = textFlowContainer; - - textFlowContainer.AddIcon(FontAwesome.Solid.UserLock, icon => - { - icon.Padding = new MarginPadding { Right = 10 }; - }); - - textFlowContainer.AddText(actionMessage); - - Action = () => login?.Show(); - } + Action = () => login?.Show(); } } } From 029d15f2a2f19ed2f88bb61d58af947df3411702 Mon Sep 17 00:00:00 2001 From: Tim Oliver Date: Thu, 23 Apr 2020 20:14:39 +0800 Subject: [PATCH 0890/6909] 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 0891/6909] 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 0892/6909] 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 0893/6909] 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 0894/6909] 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 0895/6909] 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 0896/6909] 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 0897/6909] 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 0898/6909] 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 0899/6909] 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 0900/6909] 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 0901/6909] 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 0902/6909] 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 0903/6909] 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 0904/6909] 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 0905/6909] 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 0906/6909] 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 0907/6909] 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 0908/6909] 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 0909/6909] 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 0910/6909] 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 0911/6909] 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 0912/6909] 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 0913/6909] 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 0914/6909] 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 0915/6909] 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 0916/6909] 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 0917/6909] 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 0918/6909] 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 ce47b7ca932f85b2ac323b0c220da2eaa88dd3ef Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 25 Apr 2020 10:21:01 +0300 Subject: [PATCH 0919/6909] Unnest in SetUpSteps --- .../TestSceneBeatmapRecommendations.cs | 59 ++++++++++++------- 1 file changed, 38 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index ec5fe65fdd..3f2117a4f8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -36,31 +36,48 @@ namespace osu.Game.Tests.Visual.SongSelect switch (req) { case GetUserRequest userRequest: - - decimal pp = userRequest.Ruleset.ID switch - { - 0 => 336, // Expected recommended star difficulty 2* - 1 => 928, // Expected recommended star difficulty 3* - 2 => 1905, // Expected recommended star difficulty 4* - 3 => 3329, // Expected recommended star difficulty 5* - _ => 0 - }; - - userRequest.TriggerSuccess(new User - { - Username = @"Dummy", - Id = 1001, - Statistics = new UserStatistics - { - PP = pp - } - }); + userRequest.TriggerSuccess(getUser(userRequest.Ruleset.ID)); break; } }; - // Force recommender to calculate its star ratings again - recommender.APIStateChanged(API, APIState.Online); }); + + // Force recommender to calculate its star ratings again + AddStep("calculate recommended SRs", () => recommender.APIStateChanged(API, APIState.Online)); + + User getUser(int? rulesetID) + { + return new User + { + Username = @"Dummy", + Id = 1001, + Statistics = new UserStatistics + { + PP = getNecessaryPP(rulesetID) + } + }; + } + + decimal getNecessaryPP(int? rulesetID) + { + switch (rulesetID) + { + case 0: + return 336; + + case 1: + return 928; + + case 2: + return 1905; + + case 3: + return 3329; + + default: + return 0; + } + } } [Test] From 52416ea90a49f2632a2173ab1c55ca458883b4bc Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 25 Apr 2020 10:22:19 +0300 Subject: [PATCH 0920/6909] Use GetRuleset --- .../SongSelect/TestSceneBeatmapRecommendations.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 3f2117a4f8..aed1729d7d 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -99,10 +99,10 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestBestRulesetIsRecommended() { - var osuRuleset = rulesets.AvailableRulesets.First(r => r.ID == 0); - var taikoRuleset = rulesets.AvailableRulesets.First(r => r.ID == 1); - var catchRuleset = rulesets.AvailableRulesets.First(r => r.ID == 2); - var maniaRuleset = rulesets.AvailableRulesets.First(r => r.ID == 3); + var osuRuleset = rulesets.GetRuleset(0); + var taikoRuleset = rulesets.GetRuleset(1); + var catchRuleset = rulesets.GetRuleset(2); + var maniaRuleset = rulesets.GetRuleset(3); var osuImport = importBeatmap(0, new List { osuRuleset }); var mixedImport = importBeatmap(1, new List { taikoRuleset, catchRuleset, maniaRuleset }); @@ -117,9 +117,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestSecondBestRulesetIsRecommended() { - var osuRuleset = rulesets.AvailableRulesets.First(r => r.ID == 0); - var taikoRuleset = rulesets.AvailableRulesets.First(r => r.ID == 1); - var catchRuleset = rulesets.AvailableRulesets.First(r => r.ID == 2); + var osuRuleset = rulesets.GetRuleset(0); + var taikoRuleset = rulesets.GetRuleset(1); + var catchRuleset = rulesets.GetRuleset(2); var osuImport = importBeatmap(0, new List { osuRuleset }); var mixedImport = importBeatmap(1, new List { taikoRuleset, catchRuleset, taikoRuleset }); From e906ec4d92d8d75467684630a80c606053ff7484 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 25 Apr 2020 10:25:07 +0300 Subject: [PATCH 0921/6909] Fix typo --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index aed1729d7d..f3a118572f 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -174,12 +174,12 @@ namespace osu.Game.Tests.Visual.SongSelect return () => imported; } - private void presentAndConfirm(Func getImport, int importedID, int expextedDiff) + private void presentAndConfirm(Func getImport, int importedID, int expectedDiff) { AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect); - AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID + 1024 * expextedDiff); + AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID + 1024 * expectedDiff); } } } From e65acc34018df7f77594ae8fff8c369a62a01ef8 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 25 Apr 2020 10:36:19 +0300 Subject: [PATCH 0922/6909] Other review suggestions --- .../TestSceneBeatmapRecommendations.cs | 26 +++++++++++-------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index f3a118572f..f49dae4033 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -87,12 +87,12 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 5; i++) { - importFunctions.Add(importBeatmap(i, new List { null, null, null, null, null })); + importFunctions.Add(importBeatmap(i, Enumerable.Repeat(rulesets.GetRuleset(0), 5))); } for (int i = 0; i < 5; i++) { - presentAndConfirm(importFunctions[i], i, 2); + presentAndConfirm(importFunctions[i], 2); } } @@ -108,10 +108,10 @@ namespace osu.Game.Tests.Visual.SongSelect var mixedImport = importBeatmap(1, new List { taikoRuleset, catchRuleset, maniaRuleset }); // Make sure we are on standard ruleset - presentAndConfirm(osuImport, 0, 1); + presentAndConfirm(osuImport, 1); // Present mixed difficulty set, expect ruleset with highest star difficulty - presentAndConfirm(mixedImport, 1, 3); + presentAndConfirm(mixedImport, 3); } [Test] @@ -125,13 +125,13 @@ namespace osu.Game.Tests.Visual.SongSelect var mixedImport = importBeatmap(1, new List { taikoRuleset, catchRuleset, taikoRuleset }); // Make sure we are on standard ruleset - presentAndConfirm(osuImport, 0, 1); + presentAndConfirm(osuImport, 1); // Present mixed difficulty set, expect ruleset with highest star difficulty - presentAndConfirm(mixedImport, 1, 2); + presentAndConfirm(mixedImport, 2); } - private Func importBeatmap(int importID, List rulesets) + private Func importBeatmap(int importID, IEnumerable rulesetEnumerable) { BeatmapSetInfo imported = null; AddStep($"import beatmap {importID}", () => @@ -147,14 +147,14 @@ namespace osu.Game.Tests.Visual.SongSelect var beatmaps = new List(); int difficultyID = 1; - foreach (RulesetInfo r in rulesets) + foreach (RulesetInfo r in rulesetEnumerable) { beatmaps.Add(new BeatmapInfo { OnlineBeatmapID = importID + 1024 * difficultyID, Metadata = metadata, BaseDifficulty = difficulty, - Ruleset = r ?? rulesets.First(), + Ruleset = r ?? rulesets.AvailableRulesets.First(), StarDifficulty = difficultyID, }); difficultyID++; @@ -174,12 +174,16 @@ namespace osu.Game.Tests.Visual.SongSelect return () => imported; } - private void presentAndConfirm(Func getImport, int importedID, int expectedDiff) + private void presentAndConfirm(Func getImport, int expectedDiff) { AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); AddUntilStep("wait for song select", () => Game.ScreenStack.CurrentScreen is Screens.Select.SongSelect); - AddUntilStep("recommended beatmap displayed", () => Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == importedID + 1024 * expectedDiff); + AddUntilStep("recommended beatmap displayed", () => + { + int? expectedID = getImport().Beatmaps[expectedDiff - 1].OnlineBeatmapID; + return Game.Beatmap.Value.BeatmapInfo.OnlineBeatmapID == expectedID; + }); } } } From f68a7401b9577ca357ac931129133306b632a159 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 25 Apr 2020 10:37:18 +0300 Subject: [PATCH 0923/6909] Fix comment --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index f49dae4033..a6e3e0c1c6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.SongSelect // Make sure we are on standard ruleset presentAndConfirm(osuImport, 1); - // Present mixed difficulty set, expect ruleset with highest star difficulty + // Present mixed difficulty set, expect ruleset with second highest star difficulty presentAndConfirm(mixedImport, 2); } From cea582992fa9075899d36d0a4792c7fe13db4bce Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 25 Apr 2020 10:47:12 +0300 Subject: [PATCH 0924/6909] Fix early return check --- osu.Game/OsuGame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index eb86ef4116..9707475cc7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -339,7 +339,7 @@ namespace osu.Game menuScreen.LoadToSolo(); // we might even already be at the song - if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && difficultyCriteria(Beatmap.Value.BeatmapInfo)) + if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true)) { return; } From 16f53991a898e4b7aae75c8bbfc4935b6876cf57 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 25 Apr 2020 10:50:00 +0300 Subject: [PATCH 0925/6909] Test presenting same beatmap more than once --- osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index 27f5b29738..eb73fded2f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -55,8 +55,14 @@ namespace osu.Game.Tests.Visual.Navigation var secondimport = importBeatmap(3); presentAndConfirm(secondimport); + // Test presenting same beatmap more than once + presentAndConfirm(secondimport); + presentSecondDifficultyAndConfirm(firstImport, 1); presentSecondDifficultyAndConfirm(secondimport, 3); + + // Test presenting same beatmap more than once + presentSecondDifficultyAndConfirm(secondimport, 3); } [Test] From b50e8471d20fd113db31027692d472524b0d02f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 25 Apr 2020 18:23:09 +0900 Subject: [PATCH 0926/6909] 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 0927/6909] 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 0928/6909] 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 0929/6909] 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 0930/6909] 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 0931/6909] 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 0932/6909] 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 0933/6909] 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 0934/6909] 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 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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/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 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 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 0935/6909] 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 0936/6909] 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 0937/6909] 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 0938/6909] 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 0939/6909] 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 0940/6909] 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 0941/6909] 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 0942/6909] 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 0943/6909] 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 0944/6909] 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 0945/6909] 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 0946/6909] 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 0947/6909] 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 0948/6909] 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 0949/6909] 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 0950/6909] 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 0951/6909] 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 0952/6909] 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 0953/6909] 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 0954/6909] 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 0955/6909] 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 0956/6909] 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 0957/6909] 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 0958/6909] 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 0959/6909] 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 0960/6909] 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 0961/6909] 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 0962/6909] 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 0963/6909] 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 0964/6909] 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 0965/6909] 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 0966/6909] 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 0967/6909] 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 0968/6909] 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 0969/6909] 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 0970/6909] 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 0971/6909] 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 0972/6909] 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 0973/6909] 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 0974/6909] 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 0975/6909] 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 0976/6909] 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 0977/6909] 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 0978/6909] 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 0979/6909] 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 0980/6909] 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 0981/6909] 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 0982/6909] 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 0983/6909] 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 0984/6909] 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 0985/6909] 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 0986/6909] 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 0987/6909] 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 0988/6909] 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 0989/6909] 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 0990/6909] 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 0991/6909] 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 0992/6909] 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 0993/6909] 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 0994/6909] 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 0995/6909] 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 0996/6909] 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 0997/6909] 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 0998/6909] 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 0999/6909] 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 1000/6909] 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 1001/6909] 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 1002/6909] 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 1003/6909] 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 1004/6909] 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 1005/6909] 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 1006/6909] 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 1007/6909] 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 1008/6909] 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 1009/6909] 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 1010/6909] 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 1011/6909] 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 1012/6909] 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 1013/6909] 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 1014/6909] 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 1015/6909] 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 1016/6909] 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 1017/6909] 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 1018/6909] 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 1019/6909] 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 1020/6909] 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 1021/6909] 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 1022/6909] 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 1023/6909] 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 1024/6909] 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 1025/6909] 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 1026/6909] 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 1027/6909] 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 1028/6909] 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 1029/6909] 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 1030/6909] 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 1031/6909] 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 1032/6909] 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 1033/6909] 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 1034/6909] 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 1035/6909] 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 1036/6909] 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 1037/6909] 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 1038/6909] 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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⩔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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 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 1039/6909] 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 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⩔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%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 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

    5};$)8hHQlgnx)m3IOv=u%u;#}^7!mm6_O`JX-W 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 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 1040/6909] 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 1041/6909] 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 1042/6909] 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 1043/6909] 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 1044/6909] 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 2913a8183538f823858aace50834f9e38a74b726 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 May 2020 16:59:45 +0200 Subject: [PATCH 1045/6909] Improve test code quality & safety --- .../Visual/Navigation/OsuGameTestScene.cs | 2 +- .../TestSceneBeatmapRecommendations.cs | 122 +++++++++--------- 2 files changed, 60 insertions(+), 64 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index 31afce86ae..a3ef33b916 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual.Navigation } [SetUpSteps] - public void SetUpSteps() + public virtual void SetUpSteps() { AddStep("Create new game instance", () => { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index a6e3e0c1c6..5fb4e80b51 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -11,23 +11,25 @@ using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Taiko; using osu.Game.Screens.Select; using osu.Game.Tests.Visual.Navigation; using osu.Game.Users; namespace osu.Game.Tests.Visual.SongSelect { - [HeadlessTest] public class TestSceneBeatmapRecommendations : OsuGameTestScene { + protected override bool UseOnlineAPI => false; + [Resolved] private DifficultyRecommender recommender { get; set; } - [Resolved] - private RulesetStore rulesets { get; set; } - [SetUpSteps] - public new void SetUpSteps() + public override void SetUpSteps() { AddStep("register request handling", () => { @@ -42,6 +44,8 @@ namespace osu.Game.Tests.Visual.SongSelect }; }); + base.SetUpSteps(); + // Force recommender to calculate its star ratings again AddStep("calculate recommended SRs", () => recommender.APIStateChanged(API, APIState.Online)); @@ -83,97 +87,89 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestPresentedBeatmapIsRecommended() { - var importFunctions = new List>(); + List beatmapSets = null; + const int import_count = 5; - for (int i = 0; i < 5; i++) + AddStep("import 5 maps", () => { - importFunctions.Add(importBeatmap(i, Enumerable.Repeat(rulesets.GetRuleset(0), 5))); - } + beatmapSets = new List(); - for (int i = 0; i < 5; i++) - { - presentAndConfirm(importFunctions[i], 2); - } + for (int i = 0; i < import_count; ++i) + { + beatmapSets.Add(importBeatmapSet(i, Enumerable.Repeat(new OsuRuleset().RulesetInfo, 5))); + } + }); + + AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(beatmapSets)); + + presentAndConfirm(() => beatmapSets[3], 2); } [Test] public void TestBestRulesetIsRecommended() { - var osuRuleset = rulesets.GetRuleset(0); - var taikoRuleset = rulesets.GetRuleset(1); - var catchRuleset = rulesets.GetRuleset(2); - var maniaRuleset = rulesets.GetRuleset(3); + BeatmapSetInfo osuSet = null, mixedSet = null; - var osuImport = importBeatmap(0, new List { osuRuleset }); - var mixedImport = importBeatmap(1, new List { taikoRuleset, catchRuleset, maniaRuleset }); + AddStep("create osu! beatmapset", () => osuSet = importBeatmapSet(0, new[] { new OsuRuleset().RulesetInfo })); + AddStep("create mixed beatmapset", () => mixedSet = importBeatmapSet(1, + new[] { new TaikoRuleset().RulesetInfo, new CatchRuleset().RulesetInfo, new ManiaRuleset().RulesetInfo })); + + AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { osuSet, mixedSet })); // Make sure we are on standard ruleset - presentAndConfirm(osuImport, 1); + presentAndConfirm(() => osuSet, 1); // Present mixed difficulty set, expect ruleset with highest star difficulty - presentAndConfirm(mixedImport, 3); + presentAndConfirm(() => mixedSet, 3); } [Test] public void TestSecondBestRulesetIsRecommended() { - var osuRuleset = rulesets.GetRuleset(0); - var taikoRuleset = rulesets.GetRuleset(1); - var catchRuleset = rulesets.GetRuleset(2); + BeatmapSetInfo osuSet = null, mixedSet = null; - var osuImport = importBeatmap(0, new List { osuRuleset }); - var mixedImport = importBeatmap(1, new List { taikoRuleset, catchRuleset, taikoRuleset }); + AddStep("create osu! beatmapset", () => osuSet = importBeatmapSet(0, new[] { new OsuRuleset().RulesetInfo })); + AddStep("create mixed beatmapset", () => mixedSet = importBeatmapSet(1, + new[] { new TaikoRuleset().RulesetInfo, new CatchRuleset().RulesetInfo, new TaikoRuleset().RulesetInfo })); + + AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { osuSet, mixedSet })); // Make sure we are on standard ruleset - presentAndConfirm(osuImport, 1); + presentAndConfirm(() => osuSet, 1); // Present mixed difficulty set, expect ruleset with second highest star difficulty - presentAndConfirm(mixedImport, 2); + presentAndConfirm(() => mixedSet, 2); } - private Func importBeatmap(int importID, IEnumerable rulesetEnumerable) + private BeatmapSetInfo importBeatmapSet(int importID, IEnumerable difficultyRulesets) { - BeatmapSetInfo imported = null; - AddStep($"import beatmap {importID}", () => + var metadata = new BeatmapMetadata { - var difficulty = new BeatmapDifficulty(); - var metadata = new BeatmapMetadata - { - Artist = "SomeArtist", - AuthorString = "SomeAuthor", - Title = $"import {importID}" - }; + Artist = "SomeArtist", + AuthorString = "SomeAuthor", + Title = $"import {importID}" + }; - var beatmaps = new List(); - int difficultyID = 1; - - foreach (RulesetInfo r in rulesetEnumerable) + var beatmapSet = new BeatmapSetInfo + { + Hash = Guid.NewGuid().ToString(), + OnlineBeatmapSetID = importID, + Metadata = metadata, + Beatmaps = difficultyRulesets.Select((ruleset, difficultyIndex) => new BeatmapInfo { - beatmaps.Add(new BeatmapInfo - { - OnlineBeatmapID = importID + 1024 * difficultyID, - Metadata = metadata, - BaseDifficulty = difficulty, - Ruleset = r ?? rulesets.AvailableRulesets.First(), - StarDifficulty = difficultyID, - }); - difficultyID++; - } - - imported = Game.BeatmapManager.Import(new BeatmapSetInfo - { - Hash = Guid.NewGuid().ToString(), - OnlineBeatmapSetID = importID, + OnlineBeatmapID = importID * 1024 + difficultyIndex, Metadata = metadata, - Beatmaps = beatmaps, - }).Result; - }); + BaseDifficulty = new BeatmapDifficulty(), + Ruleset = ruleset, + StarDifficulty = difficultyIndex + 1 + }).ToList() + }; - AddAssert($"import {importID} succeeded", () => imported != null); - - return () => imported; + return Game.BeatmapManager.Import(beatmapSet).Result; } + private bool ensureAllBeatmapSetsImported(IEnumerable beatmapSets) => beatmapSets.All(set => set != null); + private void presentAndConfirm(Func getImport, int expectedDiff) { AddStep("present beatmap", () => Game.PresentBeatmap(getImport())); From 9f091f3a5635cdca80c15511a7e888c70f0536f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 May 2020 17:02:28 +0200 Subject: [PATCH 1046/6909] Do not query API for custom rulesets --- osu.Game/Screens/Select/DifficultyRecommender.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 4d48cc3fe7..e9c7f6c464 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -68,7 +68,8 @@ namespace osu.Game.Screens.Select private void calculateRecommendedDifficulties() { - rulesets.AvailableRulesets.ForEach(rulesetInfo => + // only query API for built-in rulesets + rulesets.AvailableRulesets.Where(ruleset => ruleset.ID <= 3).ForEach(rulesetInfo => { var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); From 623611d9dc03e210f75615a260cde554aaef2a3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 1 May 2020 17:15:35 +0200 Subject: [PATCH 1047/6909] Simplify ruleset ordering --- osu.Game/Screens/Select/DifficultyRecommender.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index e9c7f6c464..0dc4ff95ca 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -88,10 +88,9 @@ namespace osu.Game.Screens.Select private IEnumerable getBestRulesetOrder() { - bestRulesetOrder ??= recommendedStarDifficulty.ToList() - .OrderBy(pair => pair.Value) + bestRulesetOrder ??= recommendedStarDifficulty.OrderByDescending(pair => pair.Value) .Select(pair => pair.Key) - .Reverse(); + .ToList(); return moveCurrentRulesetToFirst(); } From 3cf60e6e00483b9fd39c001073e29517f2e2ed9e Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 1 May 2020 19:08:56 +0300 Subject: [PATCH 1048/6909] Add failing test --- .../TestSceneBeatmapRecommendations.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 5fb4e80b51..68f31c5c73 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -141,6 +141,23 @@ namespace osu.Game.Tests.Visual.SongSelect presentAndConfirm(() => mixedSet, 2); } + [Test] + public void TestCorrectStarRatingIsUsed() + { + BeatmapSetInfo osuSet = null, maniaSet = null; + + AddStep("create osu! beatmapset", () => osuSet = importBeatmapSet(0, new[] { new OsuRuleset().RulesetInfo })); + AddStep("create mania beatmapset", () => maniaSet = importBeatmapSet(1, Enumerable.Repeat(new ManiaRuleset().RulesetInfo, 10))); + + AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { osuSet, maniaSet })); + + // Make sure we are on standard ruleset + presentAndConfirm(() => osuSet, 1); + + // Present mania set, expect the difficulty that matches recommended mania star rating + presentAndConfirm(() => maniaSet, 5); + } + private BeatmapSetInfo importBeatmapSet(int importID, IEnumerable difficultyRulesets) { var metadata = new BeatmapMetadata @@ -161,7 +178,8 @@ namespace osu.Game.Tests.Visual.SongSelect Metadata = metadata, BaseDifficulty = new BeatmapDifficulty(), Ruleset = ruleset, - StarDifficulty = difficultyIndex + 1 + StarDifficulty = difficultyIndex + 1, + Version = $"SR{difficultyIndex + 1}" }).ToList() }; From 1c04d58d6e55f2a09a237c53a4a2b700ecf0d74d Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 1 May 2020 19:44:35 +0300 Subject: [PATCH 1049/6909] Fix recommender's incorrect usage of current ruleset --- 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 0dc4ff95ca..0753bbc5bd 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Select foreach (var r in getBestRulesetOrder()) { - recommendedStarDifficulty.TryGetValue(ruleset.Value, out var stars); + recommendedStarDifficulty.TryGetValue(r, out var stars); beatmap = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => { From d30e4061cce9ef69ffeb08b342a9f77109b8935e Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 1 May 2020 19:46:49 +0300 Subject: [PATCH 1050/6909] Add clarifying comment about pp choice --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 68f31c5c73..fc14af3ab5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -67,16 +67,16 @@ namespace osu.Game.Tests.Visual.SongSelect switch (rulesetID) { case 0: - return 336; + return 336; // recommended star rating of 2 case 1: - return 928; + return 928; // SR 3 case 2: - return 1905; + return 1905; // SR 4 case 3: - return 3329; + return 3329; // SR 5 default: return 0; 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 1051/6909] 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 1052/6909] 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 1053/6909] 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 1054/6909] 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 1055/6909] 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 1056/6909] 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 1057/6909] 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 1058/6909] 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 1059/6909] 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 1060/6909] 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 1061/6909] 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 1062/6909] 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 1063/6909] 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 1064/6909] 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 1065/6909] 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 1066/6909] 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 1067/6909] 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 1068/6909] 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 1069/6909] 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 1070/6909] 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 1071/6909] 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 1072/6909] 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 1073/6909] 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 1074/6909] 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 1075/6909] 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 1076/6909] 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 1077/6909] 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 1078/6909] 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 1079/6909] 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 1080/6909] 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 1081/6909] 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 1082/6909] 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 1083/6909] 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 1084/6909] 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 1085/6909] 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 1086/6909] 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 1087/6909] 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 1088/6909] 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 1089/6909] 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 1090/6909] 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 1091/6909] 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 1092/6909] 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 1093/6909] 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 1094/6909] 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 1095/6909] 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 1096/6909] 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 1097/6909] 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 1098/6909] 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 1099/6909] 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 1100/6909] 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 1101/6909] 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 1102/6909] 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 1103/6909] 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 1104/6909] 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 1105/6909] 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 1106/6909] 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 1107/6909] 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 1108/6909] 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 1109/6909] 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 1110/6909] 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 1111/6909] 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 1313/6909] 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 1314/6909] 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 1315/6909] 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 1316/6909] 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 1317/6909] 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 1318/6909] 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 1319/6909] 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 1320/6909] 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 1321/6909] 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 1322/6909] 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 1323/6909] 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 1324/6909] 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 1325/6909] 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 1326/6909] 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 1327/6909] 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 1328/6909] 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 1329/6909] 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 1330/6909] 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 1331/6909] 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 1332/6909] 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 1333/6909] 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 1334/6909] 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 1335/6909] 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 1336/6909] 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 1337/6909] 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 1338/6909] 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 1339/6909] 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 1340/6909] 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 1341/6909] 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 1342/6909] 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 1343/6909] 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 1344/6909] 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 1345/6909] 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 1346/6909] 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 1347/6909] 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 1348/6909] 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 1349/6909] 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 1350/6909] 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 1351/6909] 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 1352/6909] 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 1353/6909] 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 1354/6909] 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 1355/6909] 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 1356/6909] 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 1357/6909] 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 1358/6909] 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 1359/6909] 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 1360/6909] 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 1361/6909] 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 1362/6909] 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 1363/6909] 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 1364/6909] 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 1365/6909] 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 1366/6909] 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 1367/6909] 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 1368/6909] 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 1369/6909] 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 1370/6909] 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 1371/6909] 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 1372/6909] 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 1373/6909] 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 1374/6909] 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 1375/6909] 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 1376/6909] 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 1377/6909] 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 1378/6909] 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 1379/6909] 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 1380/6909] 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 1381/6909] 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 9461097b0070c15f275c69ea67be7df638e80208 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 22 May 2020 20:50:21 +0900 Subject: [PATCH 1382/6909] Update with latest changes --- .../Difficulty/Skills/Colour.cs | 198 ++++++++---------- .../Difficulty/Skills/SpeedInvariantRhythm.cs | 8 +- .../Difficulty/TaikoDifficultyAttributes.cs | 4 + .../Difficulty/TaikoDifficultyCalculator.cs | 28 +-- 4 files changed, 114 insertions(+), 124 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index da255dcdd7..bd94c8aa65 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.IO; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -12,130 +14,108 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public class Colour : Skill { protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.3; + protected override double StrainDecayBase => 0.4; - private ColourSwitch lastColourSwitch = ColourSwitch.None; - private int sameColourCount = 1; + private bool prevIsKat = false; - private readonly int[] previousDonLengths = { 0, 0 }; - private readonly int[] previousKatLengths = { 0, 0 }; + private int currentMonoLength = 1; + private List monoHistory = new List(); + private readonly int mono_history_max_length = 5; + private int monoHistoryLength = 0; - private int sameTypeCount = 1; + private double sameParityPenalty() + { + return 0.0; + } - // TODO: make this smarter (dont initialise with "Don") - private bool previousIsKat; + private double repititionPenalty(int notesSince) + { + double d = notesSince; + return Math.Atan(d / 30) / (Math.PI / 2); + } + + private double patternLengthPenalty(int patternLength) + { + double shortPatternPenalty = Math.Min(0.25 * patternLength, 1.0); + double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); + return Math.Min(shortPatternPenalty, longPatternPenalty); + } protected override double StrainValueOf(DifficultyHitObject current) { - return StrainValueOfNew(current); - } + double objectDifficulty = 0.0; - protected double StrainValueOfNew(DifficultyHitObject current) - { - double returnVal = 0.0; - double returnMultiplier = 1.0; - - if (previousIsKat != ((TaikoDifficultyHitObject)current).IsKat) - { - returnVal = 1.5 - (1.75 / (sameTypeCount + 0.65)); - - if (previousIsKat) - { - if (sameTypeCount % 2 == previousDonLengths[0] % 2) - { - returnMultiplier *= 0.8; - } - - if (previousKatLengths[0] == sameTypeCount) - { - returnMultiplier *= 0.525; - } - - if (previousKatLengths[1] == sameTypeCount) - { - returnMultiplier *= 0.75; - } - - previousKatLengths[1] = previousKatLengths[0]; - previousKatLengths[0] = sameTypeCount; - } - else - { - if (sameTypeCount % 2 == previousKatLengths[0] % 2) - { - returnMultiplier *= 0.8; - } - - if (previousDonLengths[0] == sameTypeCount) - { - returnMultiplier *= 0.525; - } - - if (previousDonLengths[1] == sameTypeCount) - { - returnMultiplier *= 0.75; - } - - previousDonLengths[1] = previousDonLengths[0]; - previousDonLengths[0] = sameTypeCount; - } - - sameTypeCount = 1; - previousIsKat = ((TaikoDifficultyHitObject)current).IsKat; - } - - else - { - sameTypeCount += 1; - } - - return Math.Min(1.25, returnVal) * returnMultiplier; - } - - protected double StrainValueOfOld(DifficultyHitObject current) - { - double addition = 0; - - // We get an extra addition if we are not a slider or spinner if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) { - if (hasColourChange(current)) - addition = 0.75; + + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)current; + + if (currentHO.IsKat == prevIsKat) + { + currentMonoLength += 1; + } + + else + { + + objectDifficulty = 1.0; + + if (monoHistoryLength > 0 && (monoHistory[monoHistoryLength - 1] + currentMonoLength) % 2 == 0) + { + objectDifficulty *= sameParityPenalty(); + } + + monoHistory.Add(currentMonoLength); + monoHistoryLength += 1; + + if (monoHistoryLength > mono_history_max_length) + { + monoHistory.RemoveAt(0); + monoHistoryLength -= 1; + } + + for (int l = 2; l <= mono_history_max_length / 2; l++) + { + for (int start = monoHistoryLength - l - 1; start >= 0; start--) + { + bool samePattern = true; + + for (int i = 0; i < l; i++) + { + if (monoHistory[start + i] != monoHistory[monoHistoryLength - l + i]) + { + samePattern = false; + } + } + + if (samePattern) // Repitition found! + { + int notesSince = 0; + for (int i = start; i < monoHistoryLength; i++) notesSince += monoHistory[i]; + objectDifficulty *= repititionPenalty(notesSince); + break; + } + } + } + + currentMonoLength = 1; + prevIsKat = currentHO.IsKat; + + } + } - else + + /* + string path = @"out.txt"; + using (StreamWriter sw = File.AppendText(path)) { - lastColourSwitch = ColourSwitch.None; - sameColourCount = 1; + if (((TaikoDifficultyHitObject)current).IsKat) sw.WriteLine("k " + Math.Min(1.25, returnVal) * returnMultiplier); + else sw.WriteLine("d " + Math.Min(1.25, returnVal) * returnMultiplier); } + */ - return addition; + return objectDifficulty; } - private bool hasColourChange(DifficultyHitObject current) - { - var taikoCurrent = (TaikoDifficultyHitObject)current; - - if (!taikoCurrent.HasTypeChange) - { - sameColourCount++; - return false; - } - - var oldColourSwitch = lastColourSwitch; - var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd; - - lastColourSwitch = newColourSwitch; - sameColourCount = 1; - - // We only want a bonus if the parity of the color switch changes - return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch; - } - - private enum ColourSwitch - { - None, - Even, - Odd - } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs index 2d99bac7a9..28198612b2 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -49,9 +49,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills // Penalty for notes so slow that alting is not necessary. private double speedPenalty(double noteLengthMS) { + if (noteLengthMS < 80) return 1; - if (noteLengthMS < 160) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); - if (noteLengthMS < 300) return 0.6; + // return Math.Max(0, 1.4 - 0.005 * noteLengthMS); + if (noteLengthMS < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); + if (noteLengthMS < 210) return 0.6; + + currentStrain = 0.0; return 0.0; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 75d3807bba..783f1ba696 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyAttributes : DifficultyAttributes { + public double StaminaStrain; + public double RhythmStrain; + public double ColourStrain; + public double ApproachRate; public double GreatHitWindow; public int MaxCombo; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 6e1fae01ee..2a6fa81a57 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -29,10 +29,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } - private double readingPenalty(double staminaDifficulty) + private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) { - return Math.Max(0, 1 - staminaDifficulty / 14); - // return 1; + return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } private double norm(double p, double v1, double v2, double v3) @@ -48,15 +47,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { if (sr <= 1) return sr; sr -= 1; - sr = 1.5 * Math.Pow(sr, 0.76); + sr = 1.6 * Math.Pow(sr, 0.7); sr += 1; return sr; } - private double combinedDifficulty(Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) + private double combinedDifficulty(double staminaPenalty, Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) { - double staminaRating = (stamina1.DifficultyValue() + stamina2.DifficultyValue()) * staminaSkillMultiplier; - double readingPenalty = this.readingPenalty(staminaRating); double difficulty = 0; double weight = 1; @@ -64,9 +61,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 0; i < colour.StrainPeaks.Count; i++) { - double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier * readingPenalty; + double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier; double rhythmPeak = rhythm.StrainPeaks[i] * rhythmSkillMultiplier; - double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier; + double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier * staminaPenalty; peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } @@ -85,11 +82,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * staminaSkillMultiplier; - double readingPenalty = this.readingPenalty(staminaRating); - - double colourRating = skills[0].DifficultyValue() * colourSkillMultiplier * readingPenalty; + double colourRating = skills[0].DifficultyValue() * colourSkillMultiplier; double rhythmRating = skills[1].DifficultyValue() * rhythmSkillMultiplier; - double combinedRating = combinedDifficulty(skills[0], skills[1], skills[2], skills[3]); + + double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); + staminaRating *= staminaPenalty; + + double combinedRating = combinedDifficulty(staminaPenalty, skills[0], skills[1], skills[2], skills[3]); // Console.WriteLine("colour\t" + colourRating); // Console.WriteLine("rhythm\t" + rhythmRating); @@ -107,6 +106,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { StarRating = starRating, Mods = mods, + StaminaStrain = staminaRating, + RhythmStrain = rhythmRating, + ColourStrain = colourRating, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), From 12d65f305f24cdfd8dc0792d94eb4c6c5fa2f020 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 May 2020 22:11:55 +0900 Subject: [PATCH 1383/6909] 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 1384/6909] 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 1385/6909] 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 1386/6909] 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 1387/6909] 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 1388/6909] 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 1389/6909] 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 1390/6909] 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 1391/6909] 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 1392/6909] 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 1393/6909] 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 5852a37eb7499ac3969def1845fd3e2115f3236d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 24 May 2020 11:48:56 +0900 Subject: [PATCH 1394/6909] Update with latest changes --- .../Difficulty/Skills/SpeedInvariantRhythm.cs | 1 - .../Difficulty/TaikoDifficultyCalculator.cs | 5 ++++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs index 28198612b2..dd90463113 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -49,7 +49,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills // Penalty for notes so slow that alting is not necessary. private double speedPenalty(double noteLengthMS) { - if (noteLengthMS < 80) return 1; // return Math.Max(0, 1.4 - 0.005 * noteLengthMS); if (noteLengthMS < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 2a6fa81a57..dc2b68e0ca 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -31,6 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) { + if (colorDifficulty <= 0) return 0.79 - 0.25; return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } @@ -123,7 +124,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 2; i < beatmap.HitObjects.Count; i++) { - taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, rhythm)); + // Check for negative durations + if (beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime && beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime) + taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, rhythm)); } new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); From c071fe61407440f6035d35f3fef2949036ad402e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 24 May 2020 13:44:11 +0900 Subject: [PATCH 1395/6909] 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 1396/6909] 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 1397/6909] 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 1398/6909] 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 1399/6909] 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 1400/6909] 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 1401/6909] 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 1402/6909] 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 1403/6909] 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 1404/6909] 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 1405/6909] 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 1406/6909] 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 1407/6909] 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 1408/6909] 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 1409/6909] 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 1410/6909] 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 1411/6909] 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 1412/6909] 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 1413/6909] 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 1414/6909] 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 1415/6909] 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 1416/6909] 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 1417/6909] 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 1418/6909] 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 1419/6909] 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 1420/6909] 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 1421/6909] 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 1422/6909] 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 1423/6909] 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 1424/6909] 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 1425/6909] 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 1426/6909] 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 1427/6909] 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 1428/6909] 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 1429/6909] 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 1430/6909] 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 1431/6909] 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 1432/6909] 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 1433/6909] 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 1434/6909] 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 1435/6909] 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 1436/6909] 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 1437/6909] 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 1438/6909] 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 1439/6909] 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 1440/6909] 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 1441/6909] 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 1442/6909] 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 1443/6909] 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 1444/6909] 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 1445/6909] 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 1446/6909] 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 1447/6909] 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 1448/6909] 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 1449/6909] 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 1450/6909] 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 1451/6909] 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 1452/6909] 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 1453/6909] 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 1454/6909] 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 1455/6909] 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 1456/6909] 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 1457/6909] 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 1458/6909] 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 1459/6909] 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 1460/6909] 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 1461/6909] 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 1462/6909] 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 1463/6909] 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 1464/6909] 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 1465/6909] 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 1466/6909] 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 1467/6909] 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 1468/6909] 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 1469/6909] 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 1470/6909] 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 1471/6909] 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 1472/6909] 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 1473/6909] 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 1474/6909] 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 1475/6909] 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 1476/6909] 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 1477/6909] 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 1478/6909] 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 1479/6909] 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 1480/6909] 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 1481/6909] 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 1482/6909] 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 1483/6909] 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 1484/6909] 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 1485/6909] 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 1486/6909] 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 1487/6909] 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 1488/6909] 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 1489/6909] 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 1490/6909] 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 1491/6909] 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 1492/6909] 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 1493/6909] 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 1494/6909] 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 1495/6909] 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 1496/6909] 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 1497/6909] 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 1498/6909] 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 1499/6909] 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 1500/6909] 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 1501/6909] 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 1502/6909] 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 1503/6909] 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 1504/6909] 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 1505/6909] 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 1506/6909] 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 1507/6909] 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 1508/6909] 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 1509/6909] 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 1510/6909] 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 1511/6909] 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 1512/6909] 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 1513/6909] 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 1514/6909] 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 1515/6909] 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 1516/6909] 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 1517/6909] 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 1518/6909] 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 1519/6909] 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 1520/6909] 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 1521/6909] 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 1522/6909] 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 1523/6909] 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 1524/6909] 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 1525/6909] 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 1526/6909] 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 1527/6909] 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 1528/6909] 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 1529/6909] 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 1530/6909] 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 1531/6909] 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 1532/6909] 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 1533/6909] 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 1534/6909] 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 1535/6909] 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 1536/6909] 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 1537/6909] 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 1538/6909] 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 1539/6909] 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 1540/6909] 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 1541/6909] 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 1542/6909] 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 1543/6909] 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 1544/6909] 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 1545/6909] 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 1546/6909] 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 1547/6909] 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 1548/6909] 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 1549/6909] 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 1550/6909] 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 1551/6909] 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 1552/6909] 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 1553/6909] 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 1554/6909] 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 1555/6909] 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 1556/6909] 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 1557/6909] 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 1558/6909] 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 1559/6909] 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 1560/6909] 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 1561/6909] 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 1562/6909] 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 1563/6909] 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 1564/6909] 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 1565/6909] 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 1566/6909] 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 1567/6909] 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 1568/6909] 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 1569/6909] 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 1570/6909] 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 1571/6909] 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 1572/6909] 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 1573/6909] 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 1574/6909] 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 1575/6909] 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 1576/6909] 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 1577/6909] 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 1578/6909] 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 1579/6909] 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 1580/6909] 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 1581/6909] 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 1582/6909] 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 1583/6909] 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 1584/6909] 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 1585/6909] 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 1586/6909] 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 1587/6909] 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 1588/6909] 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 1589/6909] 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 1590/6909] 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 1591/6909] 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 1592/6909] 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 1593/6909] 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 1594/6909] 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 1595/6909] 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 1596/6909] 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 1597/6909] 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 1598/6909] 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 17cd9569ed2875c3fd418fc018b356eb2d092806 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 00:46:40 +0200 Subject: [PATCH 1599/6909] Introduce new storage class and manager --- .../Components/TourneyVideo.cs | 4 +-- osu.Game.Tournament/TournamentGameBase.cs | 12 ++++--- osu.Game.Tournament/TournamentStorage.cs | 34 +++++++++++++++++-- .../TournamentStorageManager.cs | 30 ++++++++++++++++ 4 files changed, 70 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Tournament/TournamentStorageManager.cs diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 317c5f6a56..259cb95035 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -27,9 +27,9 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader] - private void load(TournamentStorage storage) + private void load(NewTournamentStorage storage) { - var stream = storage.GetStream($@"videos/{filename}"); + var stream = storage.VideoStorage.GetStream($@"{filename}"); if (stream != null) { diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 85db9e61fb..427a33f871 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -19,6 +19,8 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.API.Requests; +using osu.Framework.Logging; +using osu.Game.Tournament.Configuration; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osu.Game.Users; @@ -37,7 +39,7 @@ namespace osu.Game.Tournament private Storage storage; - private TournamentStorage tournamentStorage; + private NewTournamentStorage newTournamentStorage; private DependencyContainer dependencies; @@ -52,15 +54,15 @@ namespace osu.Game.Tournament } [BackgroundDependencyLoader] - private void load(Storage storage, FrameworkConfigManager frameworkConfig) + private void load(FrameworkConfigManager frameworkConfig) { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); - dependencies.CacheAs(tournamentStorage = new TournamentStorage(storage)); + dependencies.CacheAs(newTournamentStorage = new NewTournamentStorage(Host)); - Textures.AddStore(new TextureLoaderStore(tournamentStorage)); + Textures.AddStore(new TextureLoaderStore(newTournamentStorage.VideoStorage)); - this.storage = storage; + this.storage = newTournamentStorage; windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); windowSize.BindValueChanged(size => ScheduleAfterChildren(() => diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/TournamentStorage.cs index 139ad3857b..defeceab93 100644 --- a/osu.Game.Tournament/TournamentStorage.cs +++ b/osu.Game.Tournament/TournamentStorage.cs @@ -2,18 +2,46 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Game.IO; +using osu.Game.Tournament.Configuration; namespace osu.Game.Tournament { - internal class TournamentStorage : NamespacedResourceStore + internal class TournamentVideoStorage : NamespacedResourceStore { - public TournamentStorage(Storage storage) - : base(new StorageBackedResourceStore(storage), "tournament") + public TournamentVideoStorage(Storage storage) + : base(new StorageBackedResourceStore(storage), "videos") { AddExtension("m4v"); AddExtension("avi"); AddExtension("mp4"); } } + + internal class NewTournamentStorage : WrappedStorage + { + private readonly GameHost host; + private readonly TournamentStorageManager storageConfig; + public readonly TournamentVideoStorage VideoStorage; + + public NewTournamentStorage(GameHost host) + : base(host.Storage, string.Empty) + { + this.host = host; + + storageConfig = new TournamentStorageManager(host.Storage); + var customTournamentPath = storageConfig.Get(StorageConfig.CurrentTournament); + + if (!string.IsNullOrEmpty(customTournamentPath)) + { + ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory("tournaments/" + customTournamentPath)); + } else { + ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory("tournaments/default")); + } + VideoStorage = new TournamentVideoStorage(this); + Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); + } + } } diff --git a/osu.Game.Tournament/TournamentStorageManager.cs b/osu.Game.Tournament/TournamentStorageManager.cs new file mode 100644 index 0000000000..b1f84ecf44 --- /dev/null +++ b/osu.Game.Tournament/TournamentStorageManager.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.Tournament.Configuration +{ + public class TournamentStorageManager : IniConfigManager + { + protected override string Filename => "tournament.ini"; + + public TournamentStorageManager(Storage storage) + : base(storage) + { + } + + protected override void InitialiseDefaults() + { + base.InitialiseDefaults(); + Set(StorageConfig.CurrentTournament, string.Empty); + } + + } + + public enum StorageConfig + { + CurrentTournament, + } +} \ No newline at end of file From 9a20ffa8a35ac048b7279dec2fd3752061a12987 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 00:47:47 +0200 Subject: [PATCH 1600/6909] Rename to TournamentStorage --- osu.Game.Tournament/Components/TourneyVideo.cs | 2 +- osu.Game.Tournament/TournamentGameBase.cs | 8 ++++---- osu.Game.Tournament/TournamentStorage.cs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 259cb95035..131fa9450d 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader] - private void load(NewTournamentStorage storage) + private void load(TournamentStorage storage) { var stream = storage.VideoStorage.GetStream($@"{filename}"); diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 427a33f871..991c586a56 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tournament private Storage storage; - private NewTournamentStorage newTournamentStorage; + private TournamentStorage tournamentStorage; private DependencyContainer dependencies; @@ -58,11 +58,11 @@ namespace osu.Game.Tournament { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); - dependencies.CacheAs(newTournamentStorage = new NewTournamentStorage(Host)); + dependencies.CacheAs(tournamentStorage = new TournamentStorage(Host)); - Textures.AddStore(new TextureLoaderStore(newTournamentStorage.VideoStorage)); + Textures.AddStore(new TextureLoaderStore(tournamentStorage.VideoStorage)); - this.storage = newTournamentStorage; + this.storage = tournamentStorage; windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); windowSize.BindValueChanged(size => ScheduleAfterChildren(() => diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/TournamentStorage.cs index defeceab93..e5d19831d0 100644 --- a/osu.Game.Tournament/TournamentStorage.cs +++ b/osu.Game.Tournament/TournamentStorage.cs @@ -20,13 +20,13 @@ namespace osu.Game.Tournament } } - internal class NewTournamentStorage : WrappedStorage + internal class TournamentStorage : WrappedStorage { private readonly GameHost host; private readonly TournamentStorageManager storageConfig; public readonly TournamentVideoStorage VideoStorage; - public NewTournamentStorage(GameHost host) + public TournamentStorage(GameHost host) : base(host.Storage, string.Empty) { this.host = host; From ba5a747ac9e1b83ff38b28884d86952895d34ab9 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 03:03:57 +0200 Subject: [PATCH 1601/6909] Implement migration for TournamentStorage --- osu.Game.Tournament/TournamentStorage.cs | 75 ++++++++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/TournamentStorage.cs index e5d19831d0..48eef76a28 100644 --- a/osu.Game.Tournament/TournamentStorage.cs +++ b/osu.Game.Tournament/TournamentStorage.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; +using System.Linq; +using System.Threading; using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.IO; +using System.IO; using osu.Game.Tournament.Configuration; namespace osu.Game.Tournament @@ -32,16 +36,77 @@ namespace osu.Game.Tournament this.host = host; storageConfig = new TournamentStorageManager(host.Storage); - var customTournamentPath = storageConfig.Get(StorageConfig.CurrentTournament); + var currentTournament = storageConfig.Get(StorageConfig.CurrentTournament); - if (!string.IsNullOrEmpty(customTournamentPath)) + if (!string.IsNullOrEmpty(currentTournament)) { - ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory("tournaments/" + customTournamentPath)); - } else { - ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory("tournaments/default")); + ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory("tournaments" + Path.DirectorySeparatorChar + currentTournament)); } + else + { + // Migrating old storage format to the new one. + Migrate(); + Logger.Log("Migrating files from old storage to new."); + } + VideoStorage = new TournamentVideoStorage(this); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } + + private void Migrate() + { + var defaultPath = "tournaments/default"; + var source = new DirectoryInfo(GetFullPath("tournament")); + var destination = new DirectoryInfo(GetFullPath(defaultPath)); + + Directory.CreateDirectory(destination.FullName); + + if (host.Storage.Exists("bracket.json")) + { + Logger.Log("Migrating bracket to default tournament storage."); + var bracketFile = new System.IO.FileInfo(GetFullPath(string.Empty) + Path.DirectorySeparatorChar + GetFiles(string.Empty, "bracket.json").First()); + attemptOperation(() => bracketFile.CopyTo(Path.Combine(destination.FullName, bracketFile.Name), true)); + } + + Logger.Log("Migrating other assets to default tournament storage."); + copyRecursive(source, destination); + ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(defaultPath)); + storageConfig.Set(StorageConfig.CurrentTournament, defaultPath); + storageConfig.Save(); + } + + private void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) + { + // based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo + + foreach (System.IO.FileInfo fi in source.GetFiles()) + { + attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true)); + } + + foreach (DirectoryInfo dir in source.GetDirectories()) + { + copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); + } + } + + private void attemptOperation(Action action, int attempts = 10) + { + while (true) + { + try + { + action(); + return; + } + catch (Exception) + { + if (attempts-- == 0) + throw; + } + + Thread.Sleep(250); + } + } } } From f01a86f5b1c4395e99aec8a37fe9aa7c2563c5dd Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 03:12:37 +0200 Subject: [PATCH 1602/6909] Fix styling issues and move StorageManager to Configuration Folder --- .../TournamentStorageManager.cs | 1 - osu.Game.Tournament/TournamentGameBase.cs | 4 +--- osu.Game.Tournament/TournamentStorage.cs | 16 ++++++++-------- 3 files changed, 9 insertions(+), 12 deletions(-) rename osu.Game.Tournament/{ => Configuration}/TournamentStorageManager.cs (99%) diff --git a/osu.Game.Tournament/TournamentStorageManager.cs b/osu.Game.Tournament/Configuration/TournamentStorageManager.cs similarity index 99% rename from osu.Game.Tournament/TournamentStorageManager.cs rename to osu.Game.Tournament/Configuration/TournamentStorageManager.cs index b1f84ecf44..6ccc2b6308 100644 --- a/osu.Game.Tournament/TournamentStorageManager.cs +++ b/osu.Game.Tournament/Configuration/TournamentStorageManager.cs @@ -20,7 +20,6 @@ namespace osu.Game.Tournament.Configuration base.InitialiseDefaults(); Set(StorageConfig.CurrentTournament, string.Empty); } - } public enum StorageConfig diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 991c586a56..e3d310a497 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -19,8 +19,6 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.API.Requests; -using osu.Framework.Logging; -using osu.Game.Tournament.Configuration; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osu.Game.Users; @@ -62,7 +60,7 @@ namespace osu.Game.Tournament Textures.AddStore(new TextureLoaderStore(tournamentStorage.VideoStorage)); - this.storage = tournamentStorage; + storage = tournamentStorage; windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); windowSize.BindValueChanged(size => ScheduleAfterChildren(() => diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/TournamentStorage.cs index 48eef76a28..d1c8635466 100644 --- a/osu.Game.Tournament/TournamentStorage.cs +++ b/osu.Game.Tournament/TournamentStorage.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tournament else { // Migrating old storage format to the new one. - Migrate(); + migrate(); Logger.Log("Migrating files from old storage to new."); } @@ -53,16 +53,16 @@ namespace osu.Game.Tournament Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } - private void Migrate() + private void migrate() { - var defaultPath = "tournaments/default"; + const string default_path = "tournaments/default"; var source = new DirectoryInfo(GetFullPath("tournament")); - var destination = new DirectoryInfo(GetFullPath(defaultPath)); + var destination = new DirectoryInfo(GetFullPath(default_path)); Directory.CreateDirectory(destination.FullName); - + if (host.Storage.Exists("bracket.json")) - { + { Logger.Log("Migrating bracket to default tournament storage."); var bracketFile = new System.IO.FileInfo(GetFullPath(string.Empty) + Path.DirectorySeparatorChar + GetFiles(string.Empty, "bracket.json").First()); attemptOperation(() => bracketFile.CopyTo(Path.Combine(destination.FullName, bracketFile.Name), true)); @@ -70,8 +70,8 @@ namespace osu.Game.Tournament Logger.Log("Migrating other assets to default tournament storage."); copyRecursive(source, destination); - ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(defaultPath)); - storageConfig.Set(StorageConfig.CurrentTournament, defaultPath); + ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_path)); + storageConfig.Set(StorageConfig.CurrentTournament, default_path); storageConfig.Save(); } From 72ada020a2515a1ed839fecbeb0af52c3ce86abc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jun 2020 13:42:16 +0900 Subject: [PATCH 1603/6909] 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 1604/6909] 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 1605/6909] 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 1606/6909] 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 1607/6909] 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 68027fcc2c46dacfbfda5a64eb5745d40eae6c81 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Jun 2020 16:30:26 +0900 Subject: [PATCH 1608/6909] Update with latest changes --- .../Preprocessing/StaminaCheeseDetector.cs | 37 +++-- .../Preprocessing/TaikoDifficultyHitObject.cs | 28 ++-- .../TaikoDifficultyHitObjectRhythm.cs | 100 +----------- .../Difficulty/Skills/Colour.cs | 153 +++++++++--------- .../Difficulty/Skills/Rhythm.cs | 115 +++++++++++++ .../Difficulty/Skills/SpeedInvariantRhythm.cs | 135 ---------------- .../Difficulty/Skills/Stamina.cs | 79 ++++----- .../Difficulty/TaikoDifficultyCalculator.cs | 71 ++++---- .../Difficulty/TaikoPerformanceCalculator.cs | 4 - 9 files changed, 295 insertions(+), 427 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index 4f645d7e51..b52dad5198 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -14,25 +14,26 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public void FindCheese(List difficultyHitObjects) { - this.hitObjects = difficultyHitObjects; + hitObjects = difficultyHitObjects; findRolls(3); findRolls(4); - findTLTap(0, true); - findTLTap(1, true); - findTLTap(0, false); - findTLTap(1, false); + findTlTap(0, true); + findTlTap(1, true); + findTlTap(0, false); + findTlTap(1, false); } private void findRolls(int patternLength) { List history = new List(); - int repititionStart = 0; + int repetitionStart = 0; for (int i = 0; i < hitObjects.Count; i++) { history.Add(hitObjects[i]); if (history.Count < 2 * patternLength) continue; + if (history.Count > 2 * patternLength) history.RemoveAt(0); bool isRepeat = true; @@ -47,43 +48,41 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (!isRepeat) { - repititionStart = i - 2 * patternLength; + repetitionStart = i - 2 * patternLength; } - int repeatedLength = i - repititionStart; + int repeatedLength = i - repetitionStart; if (repeatedLength >= roll_min_repetitions) { - // Console.WriteLine("Found Roll Cheese.\tStart: " + repititionStart + "\tEnd: " + i); - for (int j = repititionStart; j < i; j++) + for (int j = repetitionStart; j < i; j++) { - (hitObjects[i]).StaminaCheese = true; + hitObjects[i].StaminaCheese = true; } } } } - private void findTLTap(int parity, bool kat) + private void findTlTap(int parity, bool kat) { - int tl_length = -2; + int tlLength = -2; for (int i = parity; i < hitObjects.Count; i += 2) { if (kat == hitObjects[i].IsKat) { - tl_length += 2; + tlLength += 2; } else { - tl_length = -2; + tlLength = -2; } - if (tl_length >= tl_min_repetitions) + if (tlLength >= tl_min_repetitions) { - // Console.WriteLine("Found TL Cheese.\tStart: " + (i - tl_length) + "\tEnd: " + i); - for (int j = i - tl_length; j < i; j++) + for (int j = i - tlLength; j < i; j++) { - (hitObjects[i]).StaminaCheese = true; + hitObjects[i].StaminaCheese = true; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 75b1b3e268..cd45db2119 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.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; +using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -9,38 +12,31 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public class TaikoDifficultyHitObject : DifficultyHitObject { - public readonly bool HasTypeChange; - public readonly bool HasTimingChange; public readonly TaikoDifficultyHitObjectRhythm Rhythm; public readonly bool IsKat; public bool StaminaCheese = false; - public readonly int RhythmID; - public readonly double NoteLength; - public readonly int n; - private int counter = 0; + public readonly int N; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, TaikoDifficultyHitObjectRhythm rhythm) + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int n, IEnumerable commonRhythms) : base(hitObject, lastObject, clockRate) { - var lastHit = lastObject as Hit; var currentHit = hitObject as Hit; NoteLength = DeltaTime; double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - Rhythm = rhythm.GetClosest(NoteLength / prevLength); - RhythmID = Rhythm.ID; - HasTypeChange = lastHit?.Type != currentHit?.Type; - IsKat = lastHit?.Type == HitType.Rim; - HasTimingChange = !rhythm.IsRepeat(RhythmID); + Rhythm = getClosestRhythm(NoteLength / prevLength, commonRhythms); + IsKat = currentHit?.Type == HitType.Rim; - n = counter; - counter++; + N = n; } - public const int CONST_RHYTHM_ID = 0; + private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio, IEnumerable commonRhythms) + { + return commonRhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index 8a6f0e5bfe..0ad885d9bd 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -1,107 +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; - namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public class TaikoDifficultyHitObjectRhythm { - private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms; - private readonly TaikoDifficultyHitObjectRhythm constRhythm; - private int constRhythmID; - - public int ID = 0; public readonly double Difficulty; - private readonly double ratio; + public readonly double Ratio; + public readonly bool IsRepeat; - public bool IsRepeat() + public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty, bool isRepeat) { - return ID == constRhythmID; - } - - public bool IsRepeat(int id) - { - return id == constRhythmID; - } - - public bool IsSpeedup() - { - return ratio < 1.0; - } - - public bool IsLargeSpeedup() - { - return ratio < 0.49; - } - - public TaikoDifficultyHitObjectRhythm() - { - /* - - ALCHYRS CODE - - If (change < 0.48) Then 'sometimes gaps are slightly different due to position rounding - Return 0.65 'This number increases value of anything that more than doubles speed. Affects doubles. - ElseIf (change < 0.52) Then - Return 0.5 'speed doubling - this one affects pretty much every map other than stream maps - ElseIf change <= 0.9 Then - Return 1.0 'This number increases value of 1/4 -> 1/6 and other weird rhythms. - ElseIf change < 0.95 Then - Return 0.25 '.9 - ElseIf change > 1.95 Then - Return 0.3 'half speed or more - this affects pretty much every map - ElseIf change > 1.15 Then - Return 0.425 'in between - this affects (mostly) 1/6 -> 1/4 - ElseIf change > 1.05 Then - Return 0.15 '.9, small speed changes - - */ - - commonRhythms = new[] - { - new TaikoDifficultyHitObjectRhythm(1, 1, 0.1), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), - new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), - new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), - new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) - }; - - for (int i = 0; i < commonRhythms.Length; i++) - { - commonRhythms[i].ID = i; - } - - constRhythmID = 0; - constRhythm = commonRhythms[constRhythmID]; - } - - private TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) - { - this.ratio = ((double)numerator) / ((double)denominator); - this.Difficulty = difficulty; - } - - // Code is inefficient - we are searching exhaustively through the sorted list commonRhythms - public TaikoDifficultyHitObjectRhythm GetClosest(double ratio) - { - TaikoDifficultyHitObjectRhythm closestRhythm = commonRhythms[0]; - double closestDistance = Double.MaxValue; - - foreach (TaikoDifficultyHitObjectRhythm r in commonRhythms) - { - if (Math.Abs(r.ratio - ratio) < closestDistance) - { - closestRhythm = r; - closestDistance = Math.Abs(r.ratio - ratio); - } - } - - return closestRhythm; + Ratio = numerator / (double)denominator; + Difficulty = difficulty; + IsRepeat = isRepeat; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index bd94c8aa65..7c1623c54e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; @@ -16,106 +15,100 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; - private bool prevIsKat = false; + private NoteColour prevNoteColour = NoteColour.None; private int currentMonoLength = 1; - private List monoHistory = new List(); - private readonly int mono_history_max_length = 5; - private int monoHistoryLength = 0; + private readonly List monoHistory = new List(); + private const int mono_history_max_length = 5; private double sameParityPenalty() { return 0.0; } - private double repititionPenalty(int notesSince) + private double repetitionPenalty(int notesSince) { - double d = notesSince; - return Math.Atan(d / 30) / (Math.PI / 2); + double n = notesSince; + return Math.Min(1.0, 0.032 * n); } - private double patternLengthPenalty(int patternLength) + private double repetitionPenalties() { - double shortPatternPenalty = Math.Min(0.25 * patternLength, 1.0); - double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); - return Math.Min(shortPatternPenalty, longPatternPenalty); + double penalty = 1.0; + + monoHistory.Add(currentMonoLength); + + if (monoHistory.Count > mono_history_max_length) + monoHistory.RemoveAt(0); + + for (int l = 2; l <= mono_history_max_length / 2; l++) + { + for (int start = monoHistory.Count - l - 1; start >= 0; start--) + { + bool samePattern = true; + + for (int i = 0; i < l; i++) + { + if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) + { + samePattern = false; + } + } + + if (samePattern) // Repetition found! + { + int notesSince = 0; + for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; + penalty *= repetitionPenalty(notesSince); + break; + } + } + } + + return penalty; } protected override double StrainValueOf(DifficultyHitObject current) { - double objectDifficulty = 0.0; - - if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) + if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) { - - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)current; - - if (currentHO.IsKat == prevIsKat) - { - currentMonoLength += 1; - } - - else - { - - objectDifficulty = 1.0; - - if (monoHistoryLength > 0 && (monoHistory[monoHistoryLength - 1] + currentMonoLength) % 2 == 0) - { - objectDifficulty *= sameParityPenalty(); - } - - monoHistory.Add(currentMonoLength); - monoHistoryLength += 1; - - if (monoHistoryLength > mono_history_max_length) - { - monoHistory.RemoveAt(0); - monoHistoryLength -= 1; - } - - for (int l = 2; l <= mono_history_max_length / 2; l++) - { - for (int start = monoHistoryLength - l - 1; start >= 0; start--) - { - bool samePattern = true; - - for (int i = 0; i < l; i++) - { - if (monoHistory[start + i] != monoHistory[monoHistoryLength - l + i]) - { - samePattern = false; - } - } - - if (samePattern) // Repitition found! - { - int notesSince = 0; - for (int i = start; i < monoHistoryLength; i++) notesSince += monoHistory[i]; - objectDifficulty *= repititionPenalty(notesSince); - break; - } - } - } - - currentMonoLength = 1; - prevIsKat = currentHO.IsKat; - - } - + prevNoteColour = NoteColour.None; + return 0.0; } - /* - string path = @"out.txt"; - using (StreamWriter sw = File.AppendText(path)) - { - if (((TaikoDifficultyHitObject)current).IsKat) sw.WriteLine("k " + Math.Min(1.25, returnVal) * returnMultiplier); - else sw.WriteLine("d " + Math.Min(1.25, returnVal) * returnMultiplier); - } - */ + TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - return objectDifficulty; + double objectStrain = 0.0; + + NoteColour noteColour = hitObject.IsKat ? NoteColour.Ka : NoteColour.Don; + + if (noteColour == NoteColour.Don && prevNoteColour == NoteColour.Ka || + noteColour == NoteColour.Ka && prevNoteColour == NoteColour.Don) + { + objectStrain = 1.0; + + if (monoHistory.Count < 2) + objectStrain = 0.0; + else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) + objectStrain *= sameParityPenalty(); + + objectStrain *= repetitionPenalties(); + currentMonoLength = 1; + } + else + { + currentMonoLength += 1; + } + + prevNoteColour = noteColour; + return objectStrain; } + private enum NoteColour + { + Don, + Ka, + None + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs new file mode 100644 index 0000000000..c3e6ee4d12 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -0,0 +1,115 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + public class Rhythm : Skill + { + protected override double SkillMultiplier => 10; + protected override double StrainDecayBase => 0; + private const double strain_decay = 0.96; + private double currentStrain; + + private readonly List rhythmHistory = new List(); + private const int rhythm_history_max_length = 8; + + private int notesSinceRhythmChange; + + private double repetitionPenalty(int notesSince) + { + return Math.Min(1.0, 0.032 * notesSince); + } + + // Finds repetitions and applies penalties + private double repetitionPenalties(TaikoDifficultyHitObject hitobject) + { + double penalty = 1; + + rhythmHistory.Add(hitobject); + + if (rhythmHistory.Count > rhythm_history_max_length) + rhythmHistory.RemoveAt(0); + + for (int l = 2; l <= rhythm_history_max_length / 2; l++) + { + for (int start = rhythmHistory.Count - l - 1; start >= 0; start--) + { + bool samePattern = true; + + for (int i = 0; i < l; i++) + { + if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - l + i].Rhythm) + { + samePattern = false; + } + } + + if (samePattern) // Repetition found! + { + int notesSince = hitobject.N - rhythmHistory[start].N; + penalty *= repetitionPenalty(notesSince); + break; + } + } + } + + return penalty; + } + + private double patternLengthPenalty(int patternLength) + { + double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); + double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); + return Math.Min(shortPatternPenalty, longPatternPenalty); + } + + // Penalty for notes so slow that alternating is not necessary. + private double speedPenalty(double noteLengthMs) + { + if (noteLengthMs < 80) return 1; + if (noteLengthMs < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMs); + + currentStrain = 0.0; + notesSinceRhythmChange = 0; + return 0.0; + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + if (!(current.BaseObject is Hit)) + { + currentStrain = 0.0; + notesSinceRhythmChange = 0; + return 0.0; + } + + currentStrain *= strain_decay; + + TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current; + notesSinceRhythmChange += 1; + + if (hitobject.Rhythm.IsRepeat) + { + return 0.0; + } + + double objectStrain = hitobject.Rhythm.Difficulty; + + objectStrain *= repetitionPenalties(hitobject); + objectStrain *= patternLengthPenalty(notesSinceRhythmChange); + objectStrain *= speedPenalty(hitobject.NoteLength); + + notesSinceRhythmChange = 0; + + currentStrain += objectStrain; + return currentStrain; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs deleted file mode 100644 index dd90463113..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ /dev/null @@ -1,135 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Skills -{ - public class Rhythm : Skill - { - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0; - private const double strain_decay = 0.96; - private double currentStrain; - - private readonly List ratioObjectHistory = new List(); - private int ratioHistoryLength; - private const int ratio_history_max_length = 8; - - private int rhythmLength; - - // Penalty for repeated sequences of rhythm changes - private double repititionPenalty(double timeSinceRepititionMS) - { - double t = Math.Atan(timeSinceRepititionMS / 3000) / (Math.PI / 2); - return t; - } - - private double repititionPenalty(int notesSince) - { - double t = notesSince * 150; - t = Math.Atan(t / 3000) / (Math.PI / 2); - return t; - } - - // Penalty for short patterns - // Must be low to buff maps like wizodmiot - // Must not be too low for maps like inverse world - private double patternLengthPenalty(int patternLength) - { - double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); - double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); - return Math.Min(shortPatternPenalty, longPatternPenalty); - } - - // Penalty for notes so slow that alting is not necessary. - private double speedPenalty(double noteLengthMS) - { - if (noteLengthMS < 80) return 1; - // return Math.Max(0, 1.4 - 0.005 * noteLengthMS); - if (noteLengthMS < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); - if (noteLengthMS < 210) return 0.6; - - currentStrain = 0.0; - return 0.0; - } - - // Penalty for the first rhythm change in a pattern - private const double first_burst_penalty = 0.1; - private bool prevIsSpeedup = true; - - protected override double StrainValueOf(DifficultyHitObject dho) - { - currentStrain *= strain_decay; - - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)dho; - rhythmLength += 1; - - if (!currentHO.HasTimingChange) - { - return 0.0; - } - - double objectDifficulty = currentHO.Rhythm.Difficulty; - - // find repeated ratios - - ratioObjectHistory.Add(currentHO); - ratioHistoryLength += 1; - - if (ratioHistoryLength > ratio_history_max_length) - { - ratioObjectHistory.RemoveAt(0); - ratioHistoryLength -= 1; - } - - for (int l = 2; l <= ratio_history_max_length / 2; l++) - { - for (int start = ratioHistoryLength - l - 1; start >= 0; start--) - { - bool samePattern = true; - - for (int i = 0; i < l; i++) - { - if (ratioObjectHistory[start + i].RhythmID != ratioObjectHistory[ratioHistoryLength - l + i].RhythmID) - { - samePattern = false; - } - } - - if (samePattern) // Repitition found! - { - int notesSince = currentHO.n - ratioObjectHistory[start].n; - objectDifficulty *= repititionPenalty(notesSince); - break; - } - } - } - - if (currentHO.Rhythm.IsSpeedup()) - { - objectDifficulty *= 1; - if (currentHO.Rhythm.IsLargeSpeedup()) objectDifficulty *= 1; - if (prevIsSpeedup) objectDifficulty *= 1; - - prevIsSpeedup = true; - } - else - { - prevIsSpeedup = false; - } - - objectDifficulty *= patternLengthPenalty(rhythmLength); - objectDifficulty *= speedPenalty(currentHO.NoteLength); - - rhythmLength = 0; - - currentStrain += objectDifficulty; - return currentStrain; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 1ecca886df..29c1c3c322 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -2,91 +2,78 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Stamina : Skill { - private int hand; - private int noteNumber = 0; + private readonly int hand; protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.4; - // i only add strain every second note so its kind of like using 0.16 - private readonly int maxHistoryLength = 2; - private List noteDurationHistory = new List(); - - private List lastHitObjects = new List(); + private const int max_history_length = 2; + private readonly List notePairDurationHistory = new List(); private double offhandObjectDuration = double.MaxValue; // Penalty for tl tap or roll - private double cheesePenalty(double last2NoteDuration) + private double cheesePenalty(double notePairDuration) { - if (last2NoteDuration > 125) return 1; - if (last2NoteDuration < 100) return 0.6; + if (notePairDuration > 125) return 1; + if (notePairDuration < 100) return 0.6; - return 0.6 + (last2NoteDuration - 100) * 0.016; + return 0.6 + (notePairDuration - 100) * 0.016; } - private double speedBonus(double last2NoteDuration) + private double speedBonus(double notePairDuration) { - // note that we are only looking at every 2nd note, so a 300bpm stream has a note duration of 100ms. - if (last2NoteDuration >= 200) return 0; - double bonus = 200 - last2NoteDuration; + if (notePairDuration >= 200) return 0; + + double bonus = 200 - notePairDuration; bonus *= bonus; return bonus / 100000; } protected override double StrainValueOf(DifficultyHitObject current) { - noteNumber += 1; - - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)current; - - if (noteNumber % 2 == hand) + if (!(current.BaseObject is Hit)) { - lastHitObjects.Add(currentHO); - noteDurationHistory.Add(currentHO.NoteLength + offhandObjectDuration); + return 0.0; + } - if (noteNumber == 1) + TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; + + if (hitObject.N % 2 == hand) + { + double objectStrain = 1; + + if (hitObject.N == 1) return 1; - if (noteDurationHistory.Count > maxHistoryLength) - noteDurationHistory.RemoveAt(0); + notePairDurationHistory.Add(hitObject.NoteLength + offhandObjectDuration); - double shortestRecentNote = min(noteDurationHistory); - double bonus = 0; - bonus += speedBonus(shortestRecentNote); + if (notePairDurationHistory.Count > max_history_length) + notePairDurationHistory.RemoveAt(0); - double objectStaminaStrain = 1 + bonus; - if (currentHO.StaminaCheese) objectStaminaStrain *= cheesePenalty(currentHO.NoteLength + offhandObjectDuration); + double shortestRecentNote = notePairDurationHistory.Min(); + objectStrain += speedBonus(shortestRecentNote); - return objectStaminaStrain; + if (hitObject.StaminaCheese) + objectStrain *= cheesePenalty(hitObject.NoteLength + offhandObjectDuration); + + return objectStrain; } - offhandObjectDuration = currentHO.NoteLength; + offhandObjectDuration = hitObject.NoteLength; return 0; } - private static double min(List l) - { - double minimum = double.MaxValue; - - foreach (double d in l) - { - if (d < minimum) - minimum = d; - } - - return minimum; - } - public Stamina(bool rightHand) { hand = 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index dc2b68e0ca..789fd7c63b 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -20,9 +20,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double rhythmSkillMultiplier = 0.15; - private const double colourSkillMultiplier = 0.01; - private const double staminaSkillMultiplier = 0.02; + private const double rhythm_skill_multiplier = 0.014; + private const double colour_skill_multiplier = 0.01; + private const double stamina_skill_multiplier = 0.02; + + private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms = + { + new TaikoDifficultyHitObjectRhythm(1, 1, 0.0, true), + new TaikoDifficultyHitObjectRhythm(2, 1, 0.3, false), + new TaikoDifficultyHitObjectRhythm(1, 2, 0.5, false), + new TaikoDifficultyHitObjectRhythm(3, 1, 0.3, false), + new TaikoDifficultyHitObjectRhythm(1, 3, 0.35, false), + new TaikoDifficultyHitObjectRhythm(3, 2, 0.6, false), + new TaikoDifficultyHitObjectRhythm(2, 3, 0.4, false), + new TaikoDifficultyHitObjectRhythm(5, 4, 0.5, false), + new TaikoDifficultyHitObjectRhythm(4, 5, 0.7, false) + }; public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) @@ -32,6 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) { if (colorDifficulty <= 0) return 0.79 - 0.25; + return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } @@ -46,25 +60,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double rescale(double sr) { - if (sr <= 1) return sr; - sr -= 1; - sr = 1.6 * Math.Pow(sr, 0.7); - sr += 1; - return sr; + if (sr < 0) return sr; + + return 10.43 * Math.Log(sr / 8 + 1); } - private double combinedDifficulty(double staminaPenalty, Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) + private double locallyCombinedDifficulty(double staminaPenalty, Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) { - double difficulty = 0; double weight = 1; List peaks = new List(); for (int i = 0; i < colour.StrainPeaks.Count; i++) { - double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier; - double rhythmPeak = rhythm.StrainPeaks[i] * rhythmSkillMultiplier; - double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier * staminaPenalty; + double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier; + double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier; + double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } @@ -82,21 +93,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; - double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * staminaSkillMultiplier; - double colourRating = skills[0].DifficultyValue() * colourSkillMultiplier; - double rhythmRating = skills[1].DifficultyValue() * rhythmSkillMultiplier; + double colourRating = skills[0].DifficultyValue() * colour_skill_multiplier; + double rhythmRating = skills[1].DifficultyValue() * rhythm_skill_multiplier; + double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * stamina_skill_multiplier; double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); staminaRating *= staminaPenalty; - double combinedRating = combinedDifficulty(staminaPenalty, skills[0], skills[1], skills[2], skills[3]); - - // Console.WriteLine("colour\t" + colourRating); - // Console.WriteLine("rhythm\t" + rhythmRating); - // Console.WriteLine("stamina\t" + staminaRating); + double combinedRating = locallyCombinedDifficulty(staminaPenalty, skills[0], skills[1], skills[2], skills[3]); double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); - // Console.WriteLine("combinedRating\t" + combinedRating); - // Console.WriteLine("separatedRating\t" + separatedRating); double starRating = 1.4 * separatedRating + 0.5 * combinedRating; starRating = rescale(starRating); @@ -111,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty RhythmStrain = rhythmRating, ColourStrain = colourRating, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, + GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; @@ -120,18 +125,23 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List taikoDifficultyHitObjects = new List(); - var rhythm = new TaikoDifficultyHitObjectRhythm(); for (int i = 2; i < beatmap.HitObjects.Count; i++) { // Check for negative durations if (beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime && beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime) - taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, rhythm)); + { + taikoDifficultyHitObjects.Add( + new TaikoDifficultyHitObject( + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i, commonRhythms + ) + ); + } } new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); - for (int i = 0; i < taikoDifficultyHitObjects.Count; i++) - yield return taikoDifficultyHitObjects[i]; + foreach (var hitobject in taikoDifficultyHitObjects) + yield return hitobject; } protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] @@ -149,10 +159,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty new TaikoModEasy(), new TaikoModHardRock(), }; - - /* - protected override DifficultyAttributes VirtualCalculate(IBeatmap beatmap, Mod[] mods, double clockRate) - => taikoCalculate(beatmap, mods, clockRate); - */ } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 9585a6a369..e6dd9f5084 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -78,10 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available strainValue *= Math.Pow(0.985, countMiss); - // Combo scaling - if (Attributes.MaxCombo > 0) - strainValue *= Math.Min(Math.Pow(Score.MaxCombo, 0.5) / Math.Pow(Attributes.MaxCombo, 0.5), 1.0); - if (mods.Any(m => m is ModHidden)) strainValue *= 1.025; From 712fd6a944cc957bcff10721451ffe613c7180c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Jun 2020 17:49:45 +0900 Subject: [PATCH 1609/6909] 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 1610/6909] 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 1611/6909] 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 1612/6909] 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 1613/6909] 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 1614/6909] 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 1615/6909] 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 ce66b723908356476fdce35691e390a1e3a8b2b5 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 18:25:20 +0200 Subject: [PATCH 1616/6909] Refactor paths --- osu.Game.Tournament/TournamentStorage.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/TournamentStorage.cs index d1c8635466..32dd904b2f 100644 --- a/osu.Game.Tournament/TournamentStorage.cs +++ b/osu.Game.Tournament/TournamentStorage.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tournament public readonly TournamentVideoStorage VideoStorage; public TournamentStorage(GameHost host) - : base(host.Storage, string.Empty) + : base(host.Storage.GetStorageForDirectory("tournaments"), string.Empty) { this.host = host; @@ -40,11 +40,10 @@ namespace osu.Game.Tournament if (!string.IsNullOrEmpty(currentTournament)) { - ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory("tournaments" + Path.DirectorySeparatorChar + currentTournament)); + ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(currentTournament)); } else { - // Migrating old storage format to the new one. migrate(); Logger.Log("Migrating files from old storage to new."); } @@ -55,8 +54,8 @@ namespace osu.Game.Tournament private void migrate() { - const string default_path = "tournaments/default"; - var source = new DirectoryInfo(GetFullPath("tournament")); + const string default_path = "default"; + var source = new DirectoryInfo(host.Storage.GetFullPath("tournament")); var destination = new DirectoryInfo(GetFullPath(default_path)); Directory.CreateDirectory(destination.FullName); @@ -64,7 +63,7 @@ namespace osu.Game.Tournament if (host.Storage.Exists("bracket.json")) { Logger.Log("Migrating bracket to default tournament storage."); - var bracketFile = new System.IO.FileInfo(GetFullPath(string.Empty) + Path.DirectorySeparatorChar + GetFiles(string.Empty, "bracket.json").First()); + var bracketFile = new System.IO.FileInfo(host.Storage.GetFullPath("bracket.json")); attemptOperation(() => bracketFile.CopyTo(Path.Combine(destination.FullName, bracketFile.Name), true)); } From d2ae146c1ffb56dcb6eababc9df624008d50a589 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 19:51:44 +0200 Subject: [PATCH 1617/6909] Remove unnecessary parameters and implement delete --- osu.Game.Tournament/TournamentStorage.cs | 42 ++++++++++++++++-------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/TournamentStorage.cs index 32dd904b2f..87a2604d0b 100644 --- a/osu.Game.Tournament/TournamentStorage.cs +++ b/osu.Game.Tournament/TournamentStorage.cs @@ -13,17 +13,6 @@ using osu.Game.Tournament.Configuration; namespace osu.Game.Tournament { - internal class TournamentVideoStorage : NamespacedResourceStore - { - public TournamentVideoStorage(Storage storage) - : base(new StorageBackedResourceStore(storage), "videos") - { - AddExtension("m4v"); - AddExtension("avi"); - AddExtension("mp4"); - } - } - internal class TournamentStorage : WrappedStorage { private readonly GameHost host; @@ -72,9 +61,10 @@ namespace osu.Game.Tournament ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_path)); storageConfig.Set(StorageConfig.CurrentTournament, default_path); storageConfig.Save(); + deleteRecursive(source); } - private void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) + private void copyRecursive(DirectoryInfo source, DirectoryInfo destination) { // based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo @@ -85,10 +75,26 @@ namespace osu.Game.Tournament foreach (DirectoryInfo dir in source.GetDirectories()) { - copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); + copyRecursive(dir, destination.CreateSubdirectory(dir.Name)); } } + private void deleteRecursive(DirectoryInfo target) + { + foreach (System.IO.FileInfo fi in target.GetFiles()) + { + attemptOperation(() => fi.Delete()); + } + + foreach (DirectoryInfo dir in target.GetDirectories()) + { + attemptOperation(() => dir.Delete(true)); + } + + if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) + attemptOperation(target.Delete); + } + private void attemptOperation(Action action, int attempts = 10) { while (true) @@ -108,4 +114,14 @@ namespace osu.Game.Tournament } } } + internal class TournamentVideoStorage : NamespacedResourceStore + { + public TournamentVideoStorage(Storage storage) + : base(new StorageBackedResourceStore(storage), "videos") + { + AddExtension("m4v"); + AddExtension("avi"); + AddExtension("mp4"); + } + } } From 2f15d7fbac96b253cda0b9c1156e660c9c40dee6 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 20:04:38 +0200 Subject: [PATCH 1618/6909] Code styling fixes --- osu.Game.Tournament/TournamentStorage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/TournamentStorage.cs index 87a2604d0b..49f3d69be1 100644 --- a/osu.Game.Tournament/TournamentStorage.cs +++ b/osu.Game.Tournament/TournamentStorage.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using System.Threading; using osu.Framework.IO.Stores; using osu.Framework.Logging; @@ -114,6 +113,7 @@ namespace osu.Game.Tournament } } } + internal class TournamentVideoStorage : NamespacedResourceStore { public TournamentVideoStorage(Storage storage) From e821d787b42993865b165f309d843cea5ae2bd38 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 8 Jun 2020 20:13:02 +0200 Subject: [PATCH 1619/6909] 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 1620/6909] 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 1621/6909] 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 1622/6909] 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 1623/6909] 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 1624/6909] 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 1625/6909] 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 1626/6909] 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 1627/6909] 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 1628/6909] 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 1629/6909] 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 1630/6909] 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 1631/6909] 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 417919320cfa8400146832ca44bcce33ca660e8b Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 9 Jun 2020 17:28:42 +0200 Subject: [PATCH 1632/6909] change namespace to osu.Game.Tournament.IO --- osu.Game.Tournament/Components/TourneyVideo.cs | 1 + osu.Game.Tournament/{ => IO}/TournamentStorage.cs | 2 +- osu.Game.Tournament/TournamentGameBase.cs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) rename osu.Game.Tournament/{ => IO}/TournamentStorage.cs (99%) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 131fa9450d..dcb08464dd 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Video; using osu.Framework.Timing; using osu.Game.Graphics; +using osu.Game.Tournament.IO; namespace osu.Game.Tournament.Components { diff --git a/osu.Game.Tournament/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs similarity index 99% rename from osu.Game.Tournament/TournamentStorage.cs rename to osu.Game.Tournament/IO/TournamentStorage.cs index 49f3d69be1..7690051c7a 100644 --- a/osu.Game.Tournament/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -10,7 +10,7 @@ using osu.Game.IO; using System.IO; using osu.Game.Tournament.Configuration; -namespace osu.Game.Tournament +namespace osu.Game.Tournament.IO { internal class TournamentStorage : WrappedStorage { diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index e3d310a497..ccfbf37d48 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -20,6 +20,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.API.Requests; using osu.Game.Tournament.IPC; +using osu.Game.Tournament.IO; using osu.Game.Tournament.Models; using osu.Game.Users; using osuTK; From e57a2294743e216415aeffa3cfdc12514721dd49 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 9 Jun 2020 20:22:30 +0200 Subject: [PATCH 1633/6909] 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 1634/6909] 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 1635/6909] 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 1636/6909] 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 1637/6909] 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 1638/6909] 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 1639/6909] 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 1640/6909] 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 1641/6909] 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 1642/6909] 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 1643/6909] 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 c2e01e198f1da6682bd8d1601ffa207509ee0dfc Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 13:55:29 +0200 Subject: [PATCH 1644/6909] Rename tournamentStorage to storage --- osu.Game.Tournament/TournamentGameBase.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index ccfbf37d48..4c0c8cc28f 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -15,7 +15,6 @@ 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; @@ -36,9 +35,7 @@ namespace osu.Game.Tournament private LadderInfo ladder; - private Storage storage; - - private TournamentStorage tournamentStorage; + private TournamentStorage storage; private DependencyContainer dependencies; @@ -57,11 +54,9 @@ namespace osu.Game.Tournament { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); - dependencies.CacheAs(tournamentStorage = new TournamentStorage(Host)); + dependencies.CacheAs(storage = new TournamentStorage(Host)); - Textures.AddStore(new TextureLoaderStore(tournamentStorage.VideoStorage)); - - storage = tournamentStorage; + Textures.AddStore(new TextureLoaderStore(storage.VideoStorage)); windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); windowSize.BindValueChanged(size => ScheduleAfterChildren(() => From b69ff307d83beb9aa40d81e00b8d8c16ab0b5b88 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 13:56:16 +0200 Subject: [PATCH 1645/6909] Fixed migration logic --- osu.Game.Tournament/IO/TournamentStorage.cs | 33 ++++++++++++--------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 7690051c7a..a0f07c354b 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -15,15 +15,16 @@ namespace osu.Game.Tournament.IO internal class TournamentStorage : WrappedStorage { private readonly GameHost host; - private readonly TournamentStorageManager storageConfig; public readonly TournamentVideoStorage VideoStorage; + private const string default_tournament = "default"; public TournamentStorage(GameHost host) : base(host.Storage.GetStorageForDirectory("tournaments"), string.Empty) { this.host = host; - storageConfig = new TournamentStorageManager(host.Storage); + TournamentStorageManager storageConfig = new TournamentStorageManager(host.Storage); + var currentTournament = storageConfig.Get(StorageConfig.CurrentTournament); if (!string.IsNullOrEmpty(currentTournament)) @@ -32,35 +33,39 @@ namespace osu.Game.Tournament.IO } else { - migrate(); Logger.Log("Migrating files from old storage to new."); + Migrate(); + storageConfig.Set(StorageConfig.CurrentTournament, default_tournament); + storageConfig.Save(); + ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_tournament)); } VideoStorage = new TournamentVideoStorage(this); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } - private void migrate() + internal void Migrate() { - const string default_path = "default"; var source = new DirectoryInfo(host.Storage.GetFullPath("tournament")); - var destination = new DirectoryInfo(GetFullPath(default_path)); + var destination = new DirectoryInfo(GetFullPath(default_tournament)); - Directory.CreateDirectory(destination.FullName); + if (!destination.Exists) + destination.Create(); if (host.Storage.Exists("bracket.json")) { Logger.Log("Migrating bracket to default tournament storage."); var bracketFile = new System.IO.FileInfo(host.Storage.GetFullPath("bracket.json")); attemptOperation(() => bracketFile.CopyTo(Path.Combine(destination.FullName, bracketFile.Name), true)); + bracketFile.Delete(); } - Logger.Log("Migrating other assets to default tournament storage."); - copyRecursive(source, destination); - ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_path)); - storageConfig.Set(StorageConfig.CurrentTournament, default_path); - storageConfig.Save(); - deleteRecursive(source); + if (source.Exists) + { + Logger.Log("Migrating tournament assets to default tournament storage."); + copyRecursive(source, destination); + deleteRecursive(source); + } } private void copyRecursive(DirectoryInfo source, DirectoryInfo destination) @@ -113,7 +118,7 @@ namespace osu.Game.Tournament.IO } } } - + internal class TournamentVideoStorage : NamespacedResourceStore { public TournamentVideoStorage(Storage storage) From 18a9e5a0a6e52069dba001144f26436c8e240d4a Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 13:57:29 +0200 Subject: [PATCH 1646/6909] Add NonVisual tests for custom tournaments Can test the default directory from a clean instance, it can test a custom directory and can execute migration from an instance using the older directory setup. --- .../NonVisual/CustomTourneyDirectoryTest.cs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs new file mode 100644 index 0000000000..757465d4ad --- /dev/null +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -0,0 +1,157 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Platform; +using osu.Game.Tournament.Configuration; +using osu.Game.Tournament.IO; +using osu.Game.Tests; + +namespace osu.Game.Tournament.Tests.NonVisual +{ + [TestFixture] + public class CustomTourneyDirectoryTest + { + [SetUp] + public void SetUp() + { + } + + [Test] + public void TestDefaultDirectory() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestDefaultDirectory))) + { + try + { + var osu = loadOsu(host); + var storage = osu.Dependencies.Get(); + var defaultStorage = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestDefaultDirectory), "tournaments", "default"); + Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorage)); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestCustomDirectory() + { + using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestCustomDirectory))) + { + string osuDesktopStorage = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestCustomDirectory)); + const string custom_tournament = "custom"; + + // need access before the game has constructed its own storage yet. + Storage storage = new DesktopStorage(osuDesktopStorage, host); + // manual cleaning so we can prepare a config file. + storage.DeleteDirectory(string.Empty); + + using (var storageConfig = new TournamentStorageManager(storage)) + storageConfig.Set(StorageConfig.CurrentTournament, custom_tournament); + + try + { + var osu = loadOsu(host); + + storage = osu.Dependencies.Get(); + + Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(tournamentBasePath(nameof(TestCustomDirectory)), "custom"))); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestMigration() + { + using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) + { + string basePath = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration), "tournament"); + + string videosPath = Path.Combine(basePath, "videos"); + string modsPath = Path.Combine(basePath, "mods"); + string flagsPath = Path.Combine(basePath, "flags"); + + Directory.CreateDirectory(videosPath); + Directory.CreateDirectory(modsPath); + Directory.CreateDirectory(flagsPath); + + string bracketFile = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration), "bracket.json"); + string videoFile = Path.Combine(videosPath, "video.mp4"); + string modFile = Path.Combine(modsPath, "mod.png"); + string flagFile = Path.Combine(flagsPath, "flag.png"); + + File.WriteAllText(bracketFile, "{}"); + + File.WriteAllText(videoFile, "test"); + File.WriteAllText(modFile, "test"); + File.WriteAllText(flagFile, "test"); + + try + { + var osu = loadOsu(host); + + var storage = osu.Dependencies.Get(); + + var migratedPath = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration), "tournaments", "default"); + + videosPath = Path.Combine(migratedPath, "videos"); + modsPath = Path.Combine(migratedPath, "mods"); + flagsPath = Path.Combine(migratedPath, "flags"); + + videoFile = Path.Combine(videosPath, "video.mp4"); + modFile = Path.Combine(modsPath, "mod.png"); + flagFile = Path.Combine(flagsPath, "flag.png"); + + Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath)); + Assert.That(storage.GetFiles(".", "bracket.json").Single(), Is.EqualTo("bracket.json")); + Assert.True(storage.Exists(videoFile)); + Assert.True(storage.Exists(modFile)); + Assert.True(storage.Exists(flagFile)); + } + finally + { + // Cleaning up after ourselves. + host.Storage.Delete("tournament.ini"); + host.Storage.DeleteDirectory("tournaments"); + + host.Exit(); + } + } + } + + private TournamentGameBase loadOsu(GameHost host) + { + var osu = new TournamentGameBase(); + 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 = 90000) + { + Task task = Task.Run(() => + { + while (!result()) Thread.Sleep(200); + }); + + Assert.IsTrue(task.Wait(timeout), failureMessage); + } + + private string oldPath => Path.Combine(RuntimeInfo.StartupDirectory, "tournament"); + private string tournamentBasePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance, "tournaments"); + } +} \ No newline at end of file From a317b85fd823f1988a1514b93b99e59c866ea8ff Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 14:06:03 +0200 Subject: [PATCH 1647/6909] Remove misleading log --- osu.Game.Tournament/IO/TournamentStorage.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index a0f07c354b..2379967125 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -33,7 +33,6 @@ namespace osu.Game.Tournament.IO } else { - Logger.Log("Migrating files from old storage to new."); Migrate(); storageConfig.Set(StorageConfig.CurrentTournament, default_tournament); storageConfig.Save(); From 5d49b709b99f5a8d1da59cfc76e5025423cd5977 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 14:09:21 +0200 Subject: [PATCH 1648/6909] Change access modifier public -> internal --- osu.Game.Tournament/IO/TournamentStorage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 2379967125..c6f314032f 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tournament.IO internal class TournamentStorage : WrappedStorage { private readonly GameHost host; - public readonly TournamentVideoStorage VideoStorage; + internal readonly TournamentVideoStorage VideoStorage; private const string default_tournament = "default"; public TournamentStorage(GameHost host) From 2964b457a01894c559c329d9ea98f895e66a9b55 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 15:05:28 +0200 Subject: [PATCH 1649/6909] Rename VideoStorage to VideoStore --- osu.Game.Tournament/Components/TourneyVideo.cs | 2 +- osu.Game.Tournament/IO/TournamentStorage.cs | 8 ++++---- osu.Game.Tournament/TournamentGameBase.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index dcb08464dd..5a595f4f44 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Components [BackgroundDependencyLoader] private void load(TournamentStorage storage) { - var stream = storage.VideoStorage.GetStream($@"{filename}"); + var stream = storage.VideoStore.GetStream($@"{filename}"); if (stream != null) { diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index c6f314032f..2eb052a2e3 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tournament.IO internal class TournamentStorage : WrappedStorage { private readonly GameHost host; - internal readonly TournamentVideoStorage VideoStorage; + internal readonly TournamentVideoResourceStore VideoStore; private const string default_tournament = "default"; public TournamentStorage(GameHost host) @@ -39,7 +39,7 @@ namespace osu.Game.Tournament.IO ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_tournament)); } - VideoStorage = new TournamentVideoStorage(this); + VideoStore = new TournamentVideoResourceStore(this); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } @@ -118,9 +118,9 @@ namespace osu.Game.Tournament.IO } } - internal class TournamentVideoStorage : NamespacedResourceStore + internal class TournamentVideoResourceStore : NamespacedResourceStore { - public TournamentVideoStorage(Storage storage) + public TournamentVideoResourceStore(Storage storage) : base(new StorageBackedResourceStore(storage), "videos") { AddExtension("m4v"); diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 4c0c8cc28f..9716f0cd5f 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tournament dependencies.CacheAs(storage = new TournamentStorage(Host)); - Textures.AddStore(new TextureLoaderStore(storage.VideoStorage)); + Textures.AddStore(new TextureLoaderStore(storage.VideoStore)); windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); windowSize.BindValueChanged(size => ScheduleAfterChildren(() => From af1bbe78578f7388c7af7b08e42969038d9333e1 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 15:13:19 +0200 Subject: [PATCH 1650/6909] move TournamentVideoResourceStore to separate file --- osu.Game.Tournament/IO/TournamentStorage.cs | 12 ------------ .../IO/TournamentVideoResourceStore.cs | 19 +++++++++++++++++++ 2 files changed, 19 insertions(+), 12 deletions(-) create mode 100644 osu.Game.Tournament/IO/TournamentVideoResourceStore.cs diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 2eb052a2e3..ab7a5f63d2 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -3,7 +3,6 @@ using System; using System.Threading; -using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.IO; @@ -117,15 +116,4 @@ namespace osu.Game.Tournament.IO } } } - - internal class TournamentVideoResourceStore : NamespacedResourceStore - { - public TournamentVideoResourceStore(Storage storage) - : base(new StorageBackedResourceStore(storage), "videos") - { - AddExtension("m4v"); - AddExtension("avi"); - AddExtension("mp4"); - } - } } diff --git a/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs new file mode 100644 index 0000000000..6a44240f65 --- /dev/null +++ b/osu.Game.Tournament/IO/TournamentVideoResourceStore.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.IO +{ + internal class TournamentVideoResourceStore : NamespacedResourceStore + { + public TournamentVideoResourceStore(Storage storage) + : base(new StorageBackedResourceStore(storage), "videos") + { + AddExtension("m4v"); + AddExtension("avi"); + AddExtension("mp4"); + } + } +} \ No newline at end of file From 883185d3497832ab7309bb566f31010c8ef9a883 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 15:18:21 +0200 Subject: [PATCH 1651/6909] Add a comment to describe what's going on before the headless game starts --- .../NonVisual/CustomTourneyDirectoryTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 757465d4ad..867851e06b 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -79,6 +79,7 @@ namespace osu.Game.Tournament.Tests.NonVisual { using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) { + // Recreate the old setup that uses "tournament" as the base path. string basePath = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration), "tournament"); string videosPath = Path.Combine(basePath, "videos"); From 603054f5214beb6b4ae744d62e7b61c5a03e8f14 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 15:47:21 +0200 Subject: [PATCH 1652/6909] Remove unused property and reuse tournamentBasePath --- .../NonVisual/CustomTourneyDirectoryTest.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 867851e06b..7ac4e51711 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tournament.Tests.NonVisual { var osu = loadOsu(host); var storage = osu.Dependencies.Get(); - var defaultStorage = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestDefaultDirectory), "tournaments", "default"); + var defaultStorage = Path.Combine(tournamentBasePath(nameof(TestDefaultDirectory)), "default"); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorage)); } finally @@ -107,7 +107,7 @@ namespace osu.Game.Tournament.Tests.NonVisual var storage = osu.Dependencies.Get(); - var migratedPath = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration), "tournaments", "default"); + var migratedPath = Path.Combine(tournamentBasePath(nameof(TestMigration)), "default"); videosPath = Path.Combine(migratedPath, "videos"); modsPath = Path.Combine(migratedPath, "mods"); @@ -151,8 +151,6 @@ namespace osu.Game.Tournament.Tests.NonVisual Assert.IsTrue(task.Wait(timeout), failureMessage); } - - private string oldPath => Path.Combine(RuntimeInfo.StartupDirectory, "tournament"); private string tournamentBasePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance, "tournaments"); } } \ No newline at end of file From 222ac863042e8367b8e4a582eabafb453a3a0531 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 15:52:14 +0200 Subject: [PATCH 1653/6909] Add newlines at the end of the file --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 1 + .../NonVisual/CustomTourneyDirectoryTest.cs | 2 +- osu.Game.Tournament/Configuration/TournamentStorageManager.cs | 2 +- osu.Game.Tournament/IO/TournamentVideoResourceStore.cs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index f3d54d876a..5f0ca303e3 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -293,3 +293,4 @@ namespace osu.Game.Tests.NonVisual } } } + diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 7ac4e51711..851efb9a3d 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -153,4 +153,4 @@ namespace osu.Game.Tournament.Tests.NonVisual } private string tournamentBasePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance, "tournaments"); } -} \ No newline at end of file +} diff --git a/osu.Game.Tournament/Configuration/TournamentStorageManager.cs b/osu.Game.Tournament/Configuration/TournamentStorageManager.cs index 6ccc2b6308..653ea14352 100644 --- a/osu.Game.Tournament/Configuration/TournamentStorageManager.cs +++ b/osu.Game.Tournament/Configuration/TournamentStorageManager.cs @@ -26,4 +26,4 @@ namespace osu.Game.Tournament.Configuration { CurrentTournament, } -} \ No newline at end of file +} diff --git a/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs index 6a44240f65..1ccd20fe21 100644 --- a/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs +++ b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs @@ -16,4 +16,4 @@ namespace osu.Game.Tournament.IO AddExtension("mp4"); } } -} \ No newline at end of file +} From 1d4d749b539490f295036c00aa9740fb6d2403f4 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 15:56:34 +0200 Subject: [PATCH 1654/6909] Undo blank line removal Was too excited to add blank lines before submitting the PR that I overdid it --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 5f0ca303e3..f3d54d876a 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -293,4 +293,3 @@ namespace osu.Game.Tests.NonVisual } } } - From c9dc17f3d8868679bb99b37ea425dff4624626eb Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 17:51:07 +0200 Subject: [PATCH 1655/6909] Introduce migrations for drawings --- osu.Game.Tournament/IO/TournamentStorage.cs | 23 +++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index ab7a5f63d2..0879d27aac 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -54,8 +54,21 @@ namespace osu.Game.Tournament.IO { Logger.Log("Migrating bracket to default tournament storage."); var bracketFile = new System.IO.FileInfo(host.Storage.GetFullPath("bracket.json")); - attemptOperation(() => bracketFile.CopyTo(Path.Combine(destination.FullName, bracketFile.Name), true)); - bracketFile.Delete(); + moveFile(bracketFile, destination); + } + + if (host.Storage.Exists("drawings.txt")) + { + Logger.Log("Migrating drawings to default tournament storage."); + var drawingsFile = new System.IO.FileInfo(host.Storage.GetFullPath("drawings.txt")); + moveFile(drawingsFile, destination); + } + + if (host.Storage.Exists("drawings_results.txt")) + { + Logger.Log("Migrating drawings results to default tournament storage."); + var drawingsResultsFile = new System.IO.FileInfo(host.Storage.GetFullPath("drawings_results.txt")); + moveFile(drawingsResultsFile, destination); } if (source.Exists) @@ -97,6 +110,12 @@ namespace osu.Game.Tournament.IO attemptOperation(target.Delete); } + private void moveFile(System.IO.FileInfo file, DirectoryInfo destination) + { + attemptOperation(() => file.CopyTo(Path.Combine(destination.FullName, file.Name), true)); + file.Delete(); + } + private void attemptOperation(Action action, int attempts = 10) { while (true) From 327795ba9933f8bcf7447c1b4f62ebfa40e763e0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 18:00:47 +0200 Subject: [PATCH 1656/6909] Switch drawing storage to tournamentstorage --- osu.Game.Tournament/IO/TournamentStorage.cs | 2 +- .../Screens/Drawings/Components/StorageBackedTeamList.cs | 6 +++--- osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 0879d27aac..195448f2d5 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -11,7 +11,7 @@ using osu.Game.Tournament.Configuration; namespace osu.Game.Tournament.IO { - internal class TournamentStorage : WrappedStorage + public class TournamentStorage : WrappedStorage { private readonly GameHost host; internal readonly TournamentVideoResourceStore VideoStore; diff --git a/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs index f96ec01cbb..ecc23181be 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using osu.Framework.Logging; -using osu.Framework.Platform; +using osu.Game.Tournament.IO; using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Screens.Drawings.Components @@ -14,9 +14,9 @@ namespace osu.Game.Tournament.Screens.Drawings.Components { private const string teams_filename = "drawings.txt"; - private readonly Storage storage; + private readonly TournamentStorage storage; - public StorageBackedTeamList(Storage storage) + public StorageBackedTeamList(TournamentStorage storage) { this.storage = storage; } diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs index 8be66ff98c..bf0d6f4871 100644 --- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs +++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs @@ -12,9 +12,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; -using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Tournament.Components; +using osu.Game.Tournament.IO; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Drawings.Components; using osuTK; @@ -36,12 +36,12 @@ namespace osu.Game.Tournament.Screens.Drawings private Task writeOp; - private Storage storage; + private TournamentStorage storage; public ITeamList TeamList; [BackgroundDependencyLoader] - private void load(TextureStore textures, Storage storage) + private void load(TextureStore textures, TournamentStorage storage) { RelativeSizeAxes = Axes.Both; From 32d86d6fab0a34f502de05fc5becec996b0947f0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 18:07:24 +0200 Subject: [PATCH 1657/6909] Create storage for config files of a tournament --- osu.Game.Tournament/IO/TournamentStorage.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 195448f2d5..b658dfdb69 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -15,6 +15,7 @@ namespace osu.Game.Tournament.IO { private readonly GameHost host; internal readonly TournamentVideoResourceStore VideoStore; + internal readonly Storage ConfigurationStorage; private const string default_tournament = "default"; public TournamentStorage(GameHost host) @@ -38,6 +39,8 @@ namespace osu.Game.Tournament.IO ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_tournament)); } + ConfigurationStorage = UnderlyingStorage.GetStorageForDirectory("config"); + VideoStore = new TournamentVideoResourceStore(this); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } From 592e3bf4c91bc6f976a80d1b416309c60503bcac Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 18:21:56 +0200 Subject: [PATCH 1658/6909] Implement migrations for the drawings config file --- osu.Game.Tournament/IO/TournamentStorage.cs | 14 +++++++++++++- .../Screens/Drawings/DrawingsScreen.cs | 2 +- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index b658dfdb69..298d02e6bb 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -17,6 +17,7 @@ namespace osu.Game.Tournament.IO internal readonly TournamentVideoResourceStore VideoStore; internal readonly Storage ConfigurationStorage; private const string default_tournament = "default"; + private const string config_directory = "config"; public TournamentStorage(GameHost host) : base(host.Storage.GetStorageForDirectory("tournaments"), string.Empty) @@ -39,7 +40,7 @@ namespace osu.Game.Tournament.IO ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_tournament)); } - ConfigurationStorage = UnderlyingStorage.GetStorageForDirectory("config"); + ConfigurationStorage = UnderlyingStorage.GetStorageForDirectory(config_directory); VideoStore = new TournamentVideoResourceStore(this); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); @@ -49,9 +50,13 @@ namespace osu.Game.Tournament.IO { var source = new DirectoryInfo(host.Storage.GetFullPath("tournament")); var destination = new DirectoryInfo(GetFullPath(default_tournament)); + var cfgDestination = new DirectoryInfo(GetFullPath(default_tournament + Path.DirectorySeparatorChar + config_directory)); if (!destination.Exists) destination.Create(); + + if (!cfgDestination.Exists) + destination.CreateSubdirectory(config_directory); if (host.Storage.Exists("bracket.json")) { @@ -67,6 +72,13 @@ namespace osu.Game.Tournament.IO moveFile(drawingsFile, destination); } + if (host.Storage.Exists("drawings.ini")) + { + Logger.Log("Migrating drawing configuration to default tournament storage."); + var drawingsConfigFile = new System.IO.FileInfo(host.Storage.GetFullPath("drawings.ini")); + moveFile(drawingsConfigFile, cfgDestination); + } + if (host.Storage.Exists("drawings_results.txt")) { Logger.Log("Migrating drawings results to default tournament storage."); diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs index bf0d6f4871..7f2563c948 100644 --- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs +++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tournament.Screens.Drawings return; } - drawingsConfig = new DrawingsConfigManager(storage); + drawingsConfig = new DrawingsConfigManager(storage.ConfigurationStorage); InternalChildren = new Drawable[] { From 56a40e616b0688a4969577a6b5b540854556b8bc Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 11 Jun 2020 20:11:44 +0200 Subject: [PATCH 1659/6909] Add drawings to the migration test --- .../NonVisual/CustomTourneyDirectoryTest.cs | 40 ++++++++++++++----- osu.Game.Tournament/IO/TournamentStorage.cs | 2 +- 2 files changed, 30 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 851efb9a3d..37f456ae96 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -3,7 +3,6 @@ using System; using System.IO; -using System.Linq; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -48,7 +47,7 @@ namespace osu.Game.Tournament.Tests.NonVisual { using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestCustomDirectory))) { - string osuDesktopStorage = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestCustomDirectory)); + string osuDesktopStorage = basePath(nameof(TestCustomDirectory)); const string custom_tournament = "custom"; // need access before the game has constructed its own storage yet. @@ -80,23 +79,34 @@ namespace osu.Game.Tournament.Tests.NonVisual using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) { // Recreate the old setup that uses "tournament" as the base path. - string basePath = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration), "tournament"); + string osuRoot = basePath(nameof(TestMigration)); - string videosPath = Path.Combine(basePath, "videos"); - string modsPath = Path.Combine(basePath, "mods"); - string flagsPath = Path.Combine(basePath, "flags"); + // Define all the paths for the old scenario + string oldPath = Path.Combine(osuRoot, "tournament"); + string videosPath = Path.Combine(oldPath, "videos"); + string modsPath = Path.Combine(oldPath, "mods"); + string flagsPath = Path.Combine(oldPath, "flags"); Directory.CreateDirectory(videosPath); Directory.CreateDirectory(modsPath); Directory.CreateDirectory(flagsPath); - string bracketFile = Path.Combine(RuntimeInfo.StartupDirectory, "headless", nameof(TestMigration), "bracket.json"); + // Define testing files corresponding to the specific file migrations that are needed + string bracketFile = Path.Combine(osuRoot, "bracket.json"); + + string drawingsConfig = Path.Combine(osuRoot, "drawings.ini"); + string drawingsFile = Path.Combine(osuRoot, "drawings.txt"); + string drawingsResult = Path.Combine(osuRoot, "drawings_results.txt"); + + // Define sample files to test recursive copying string videoFile = Path.Combine(videosPath, "video.mp4"); string modFile = Path.Combine(modsPath, "mod.png"); string flagFile = Path.Combine(flagsPath, "flag.png"); File.WriteAllText(bracketFile, "{}"); - + File.WriteAllText(drawingsConfig, "test"); + File.WriteAllText(drawingsFile, "test"); + File.WriteAllText(drawingsResult, "test"); File.WriteAllText(videoFile, "test"); File.WriteAllText(modFile, "test"); File.WriteAllText(flagFile, "test"); @@ -118,7 +128,13 @@ namespace osu.Game.Tournament.Tests.NonVisual flagFile = Path.Combine(flagsPath, "flag.png"); Assert.That(storage.GetFullPath("."), Is.EqualTo(migratedPath)); - Assert.That(storage.GetFiles(".", "bracket.json").Single(), Is.EqualTo("bracket.json")); + + Assert.True(storage.Exists("bracket.json")); + Assert.True(storage.Exists("drawings.txt")); + Assert.True(storage.Exists("drawings_results.txt")); + + Assert.True(storage.ConfigurationStorage.Exists("drawings.ini")); + Assert.True(storage.Exists(videoFile)); Assert.True(storage.Exists(modFile)); Assert.True(storage.Exists(flagFile)); @@ -128,7 +144,6 @@ namespace osu.Game.Tournament.Tests.NonVisual // Cleaning up after ourselves. host.Storage.Delete("tournament.ini"); host.Storage.DeleteDirectory("tournaments"); - host.Exit(); } } @@ -151,6 +166,9 @@ namespace osu.Game.Tournament.Tests.NonVisual Assert.IsTrue(task.Wait(timeout), failureMessage); } - private string tournamentBasePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance, "tournaments"); + + private string basePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance); + + private string tournamentBasePath(string testInstance) => Path.Combine(basePath(testInstance), "tournaments"); } } diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 298d02e6bb..05ee7a3618 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tournament.IO if (!destination.Exists) destination.Create(); - + if (!cfgDestination.Exists) destination.CreateSubdirectory(config_directory); From fca6a6d69f3724091c2f72b1f4ea7034614633d7 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Fri, 12 Jun 2020 09:46:21 +0930 Subject: [PATCH 1660/6909] 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 5041c74c7a525de4261b2bf800f97222cd4ede4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Jun 2020 11:30:15 +0900 Subject: [PATCH 1661/6909] Fix merge issue --- 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 bc9999381b..a779135345 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tournament } [BackgroundDependencyLoader] - private void load(Storage storage) + private void load() { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); From a48e36fd31d5e4850a3f132883173a794a457c25 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Jun 2020 12:58:33 +0900 Subject: [PATCH 1662/6909] 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 1663/6909] 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 1664/6909] 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 1665/6909] 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 1666/6909] 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 1667/6909] 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 1668/6909] 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 1669/6909] 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 1670/6909] 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 1671/6909] 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 1672/6909] 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 1673/6909] 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 1674/6909] 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 1675/6909] 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 1676/6909] 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 1677/6909] 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 1678/6909] 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 1679/6909] 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 1680/6909] 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 1681/6909] 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 1682/6909] 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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­TX?fFTze1neYbg@!;i(FO?-YPxzrZPyeo-UA72O5f`GDaxoH z>cZsuO33Qp{9d^2$EFWJm7d^x_@%$vOb_2|&OiKm(|_S>ns_}3hRvEC$5x`}TjmZk zGW?MESqr5+U}+O5C^&(q;f{zxS3)u*R1@`(6Iq90X+s<0`l46mhN+S^3f3Ad<0<+P zJbCZAX787@Jtsid6vt|754DntXns;(7ce7`+`!yx8x-_Q_b*Tbg(sR;E zhnzJ;Rr-!3!hP)=WdjXshNu`|hB57sJ$vsJ?G(MCL0V?dG3%2XB!q~FqA4wdPPPbe zQEQ0M;8qN=5EqOQCp_)Xf>7nqh1-J&)li+EvNxHiKlLx=F|cVJ<>8My#Pot7>t51k zEWN~;c1WIluo*n~iDqzeTWL#rX0PO{k%llM3^vAg@$L5Oc43c?j@8Wtk&ysE5^N;G zp^#~+S>-^;9*giwKhhA~hxtJywPTYTv&SMrkyq(qIL(bW=I@^C+O3uD%VnD~=#Ol2 z-we(^)ajyo-2d4<N zt0AY z>bpz1rj5-=s7pC!Vd?M;MY^Oz*3GnsQUty*!?{E#Vln&fpKYG|<9o^tC>Dl+vTAVT zfw0K2o6BWy%C&7em$NRtww>PfZDYH?YGld&^IKmMQnUUd!XZK! zht}mrT?!E#h*+{Sjaema<<42Y(t!FU)TpwN?I%hPAq_R8B#&H2?RGzG^A1i^=b?XW z+&9>S7b#66OP|f^KlmJcao!&1!FHL>?=XiuZS8ls(`SDjR+mltAd-aR=IL@fpKT?Z zw-@&=hW^K7IE-(8MIn-m#R!8LswInYu#gZ@hr<+6AI4{lG#;9p-&-mxmz&!gm9{?) z=Y28DSYN`N2HWXyyL`gz4qKb5`fvZMO;3CsKgM?PUG6uR5%sjiI2@aHVccfgW;e`l z6$5lS+b+jtVQ*`4d&uLqVoz@9IFjyx^+X1VnKgbUdsEqa0_u6o7!T0|$$iSe?AT%Y z28Hin)~z4zP|P+-ua_ZSR$a>NG{0TUP#Q4LBRm=1J8*YzqWOfJj$iOBAfa`Bt< zEJBN`jquZFhzjVQ!P-ZRfrq%*N@nO&orV?b?k$ zw*`Nf>B{dg+jTy}DBH9@Kg9W+Y&$*W;ojD`9bGnI3;2GyOmGBO@H#ddwagI7kfRpd zX~&-;wV_6nATdTR-S7_6Th&K_uR~qBZbQwzwiE7b-y;i#xJxHe&z-ii%UG`|(;wCu zKh-q1b>vLSsJXfn#9=1M;rqCTP2l0IA&b#7ZIl^kEiulBNnKPxR|ZO+QSn|38jRo3 zz%QZK^XsxVm3Cd}Tu!!euX|{lyPc^sFv7-g37JzULzfxvOx3v`TwIic=O|dpt z!AXasTVC>^cFK=klOmF(ZMpBQS;8-({~VGpDV~wm+wFIc_s?xcz?Eb~{bBoj%*RSDCsE`2Eyym_Dn6 z51QD{!~{bNz=TJBG(B3U(kJe;p+<3Bf*!4|Hq<=-Hakq4=5eS?SH0h+@cR9ge!&no zWhVaKe8^O(?^b?!w^f%qr}*LA#O~7PbnVY0^|c74AP~<8)v5IGJr3lHDNeLIiw!ox zB$XRbm>FW%1882s21iFnT|lIDMbM5-W}7rFV^vy*ciDvH@a*1HuH$Q)eyJZ2$Is~2 zKhyPi?^8#C_bOmN_wF#)fyox77p`bvmL!H^w<>1d@*`4#*y(&J3+nw#& zo&A(;f9-a?tL*%Kc4PAx7FxJ&rnj`tE5^pw6JQ3T;{t;?mk806VHY+kQEDsRe47hF0Hfgs>Lw%dl<#@X{ z+1r#`ziaM`8euZxoV=*rnA%WNm{}ooxZ{=&AxsbHP|T1oL$E6ibEex64&>*nXk+TJzZ>9@71&uQzrH~HNU*mEM8+-EQRu2kG-(gtXUEwExOQ8tXRFv(Ig zn}p)hbmc{fYNjY1K{L7aW?aC~Khs7b#ru(S$xsq;`CS}Fcs5RBn>f2|JAIv(@UAeY z$F9rBaoJs$V?TCR84kntLwR=Rc!y(S99Mk1{n8#DQXk->|$zl=`ML!q}3W6($_VZ%dQ`;tNM}7 z9;$n_(MAlBZRdK*O*7^8nYhtGyS7-I$3U?119N165Y{zNuH((S%2jo9vnig0+oYKn zxFUTnW0m_db-7+6i|ra|dVQh8^Z#rv8T5oSW%jSxB#VSXB#0x&E+$DrWK#_vNku|L z&BizXhGK-Ioyk9wJ9fA!=JciCZjRp6jXP=-^<%p|*u1xGId?L-&z#Za;m`FrKHID6 zt;?ym97rYe!FeEyxQ;ZO*Zdjliwe=i6!>r#s9UyT?~*Nwp@B1Yly%*+~vF; zw?c7tFLq-pAGjUTq+mSIEitDb{GI0Ln}2y;c80Ip!Mm(_pRVv6w#rS}InH%GE<5*Y zR~>H4wzkV~di%GrYq;9K1mBnbm%v}djcGf{VWQiIF-rQMjHdd~dSQl~JDwc2>xU1t zekeCL)YR*TwC$Lm%6#l#@fK3jHlIlGn~qyt;XuA?dGZqCYpVLf`k|YH{jxSDz0+%+ zLShJJ-Aaifn(@VoxTITiOYKbTBPk*!>R1KH!$Ws=xfMJg^d69lt|#{LW>Yzv;;DI? zG`WSV(szY-h2^r=x!qT!F|ybOX_$&&pT77%N#XENE~Epk7PiC?hK7WwyFipq>xg28 zAQ9gPU7b(a z=f+`mISwab?vr`OO)@3ALG-_Yd+n)izl^6kpl zSfib$jDtOmfhp3y)Mw;}+DwJNQ%zz^h)(3Ooj;&1$iMu*H~nvZz7xJ(@6r{f!j_EF z*mflTI?QpGwR3syzjbBhG&!&HT+R}E|P(u!0`~7F~U3WTVrvsivLge#JoE_|o z0mk9_bl#9bQ9m_fbbJnX*kx@q;jaAa>T#UyuKa{KzP4AT2by&pVV5x=2L?MoMi$%L z?K%3$Z!49Eq6j016Psy-G%=Hep}`1IiHaExTlY|Yq(u;FK$vW1oiX#ok95JqL3fos z+5cg4@_~OmXNImnx6N+ay^W!-YE$>0{lEZs4U5|E^j&FloZWSP*W-BGPG^6%d57%B zeOW^vV%(Ru-QU0yDYeF6zeZHehE{zw;_TdFnWqQ-^Qz8xFOd^VM!BKD$4hC$2Bh0YHS8x`*-jkF#W`6)@58LyHW2sD^OFK(7ZJL|oxvMz3&K~9@ zNQc=x26ZXL^ZM@}^HDYBZ5_M7Jd`R8b=qmDNp)=-3-YW%aKSM)QlU24#6`y(yqC!X zCU@Vi;ZfL%pKZ%>{dIc#yV~oxY`YmbY?Ir0u40?;WTe4eBMgZ1-pjwPL(FfBQ6!>> zTa55r3@~Xveqe@kMhU|tUio08s9j;O5KNGf%l9U){Es4-tU48f?Dk;S zZB@`#hW=b-a$l1s`!~5Ec$?Df67M#y%H#O!v@^`z*YrK^>EXAU$qWBH;Le2vK{%NZ z0Xvu>zRF~Jcmi36@?mWJ`@)qkk6%T83+=Qdzx<9;B_?#tL+WnQM7ZS$BB z*6G)cm)u6X$$wdsamBcUo$+cS_%_e~Us~oE72F24EpEodRc#@|9Nf*3742SYD+)(q5RA( zd1iZWHItA2qgVrqOOcuNgE-70MHC@~JV=7fCSinoJj^O#fO>*Cwv2Kh#u@7pEiw z7O*WaW>TvMd;~>iGTcWx6jMCXeQh_}9pvmX9mjEtSsb@;n~qz@E4t?tQ%i?! z^pf@ZlJBDD8rBafl-IwaxqHR@J7gb;@#!c_7#JcCQX&j-7vt1-t?s z9sYjzyWROcGm>P2i2J=y{4I5g&c{f4(-zP`o?v1yLLm=Ns8vvv!USQ8vfmT(OoT+t zE@6yMi4hK3#%U%Rgp)6-i-cyVbffMVb=90KnKT zzJ=cdB-?qgHdE+;_P@O(<>mb$8wQCw@hxlnKgEwk1R}L&HBNHyQ&6Ei(+b zA|bY81?iu(NFYq8_y}`vPw@Rye=EkA>$FX~Xd^bpZO6T?e~Igg?+SBStKx{yX4`b+ zA?;<-JD=@b|2A&JHQR=<`_^_Z;QPxew!jzI2AniGwR8xMS~>)OAD}YTW|~+(l!S=7 z^m~+$)(}yb9=DCXPU)~M@;WRJtLrH1F_ytaG}%~8%wb!&GxF4K4-^lQY!hd{(>Z@_ z=VmxAhrJ)&eka;yH@4py+sI;jeDe#<L7l9MvSjxkmGdsCa zt&BCJWLD`1RSObhtR)9qHqcn9ok-2XYu?o}{Z z74Cf5-X(3FZ&lkIe%Z2fJ-F#3jQh%cw+gg3pC1g4@7~iHvc&mJW|3 zA##~F(&0pmklSf6=lTv}HfDx7%%g<_Z>~R~Z1m42s3zAdd%%^XCDR<0+#q2Y+3D?RUBFS3AGI-PjlAi$u- z_R~N8HI8)rliTf~7d&V(>Sw$h6*GOy$?eqXyzV@IhF< zh%Ow0JJET>p{}ms>lb|A;apD~vfu~~s&PKDs&!+|E>3R$z2^MASM{yLpH|Lp1DqHm zypQ3C+op!A!?6wH9JZ}{UAa%PjIO%!``eAplVM*oLSX;smA@@!^+j!Z(ii6i_x@}_ zG($QR%wcvMw+WS+K8nnAsV`WM1hYdy<3u+R9g1LT=xDsq&n=9p9XPXDPUB$w@n3I7 z`hv~yodRF?soPxRzTxF`snG#2Cprbbzue5U zQEDdmGSQt0zA?dRaeJ)EOmnV>OWY51q(=CCqH7q3NQYVzjLkHhL(GklJ4=U-+Gg$0 zX>D)XU3#xgxK3S~`^M$AHr;YXbN7n*x6B@&7twhp&@VopBRBI&QCvcBtu%E9cyN%x zZfttP4}-++Z0Gb!5{59F>?W-CuS)0eb$N1{tGN9RuFL-Xp18XRV(;L7^XSvR)bxix z3hA&xI)ougQeZaeS!7m~hJ@HkhF*N%*O~*n(%7nv$_1tv>Qf{{sa8XpXjP{nb+l?5 z>Ix~5k@)wBzK8k8bGXtLw+q{C&hO)Qq&Kr0|2j&sE{?o5f0AHK z|G015SGoUi8@?yfxJ+L8Eoo+2xPsGy2Y5Y3G8CK+u=h0C(KFOJ;c>fmn5Dy2n`!93 zU8Td<$@Jt5UG)Ai?kM!m*`Q8?8QLHr!VvkqNPH2Y^%GwYe&=zV2ZqD#$M(SSpusl6 z*>+schZ|$Nc6a#+!{j_AWbx}d79p2+xh&h1WH+|oE89jIBMUYk%YFEn|5(zThP;T@ zK!mX{!GbgLAP;KNP?w7Nu>cQT;}KJg?b?MYp0c(GqCr5%lp`br8Tl(J z*-{s(S_m_Dr=EQJuZd{>KXb%(+q3O=^ZVxa$8A%G+wb^Iw$tY_H|1FspUc{|ygHBl z)L}P$f!lpzJNH+HJ8p>O=;MD!Fp<;%>|6N546RPHY7{8~*iS?>83(|TXRzOfxBha) zTOqWR!}=i;E12R;Hy<&^W`lQ|_dfSa`iO9WZ*HUAZol8l?}m->d)MxWOg~Paf;yeK zyv_uVG0=5yvfU@7!%bh=#@z+q@$LUk=MB#SwlUEaBjhLze@Oqp5EzePgu-Vgvj@@u zu(LGQ4%?#^_1VMgmJY8&UD~msM!gc%%YDf9-BvF$)!mmph4=VM zf{5uDhKO5t=keV<0FUV!D1&JazVq<%H<15YCPyO|f3FCG>?n zad1}*kOlaoc5C*D&YB*{e|D;x52uIPa41O;x~Lhb+BcR8Ev^@ox)fQ07E8!Lb6WZp zAnd=>oP6f5HAi3hMXA^RWd)S?A_~=w-;w^V9|g_fL)<#EDZb82y}O!qb!=N+oi6)v z+iy28$K3^cV1Sx*EXlAx(|wj=R4_v%#Mt!<(GFiD0tgvCjmE zn}~F^*Hr4#fpAy3bXRR`r%}EZzJm80zUmC2wxrE>n)g2YOU=O_{X(<}`hPA_d)Z&fQ0Tz0??kw$EJA6Im z-Nt!un?&o=&>inD^SnCuEqw-xvn(JCQba5?2$9&bKh~lHx2$tXH0NOW6bU!48}MB= zZ%DXJoyLT~c8(_x$dJQugN&pn&JW>Ei)}+VCWqm7dfeGKJz;g8oNk>vm*I2{%QkKv zzq@TC4EE^x-)%;p{AF#aes2j`=FD(hRHcwM7~x2g0he=!lpsdPwN7S+%qY3wjTt3o zU-nc?k?(=S6i)^Jp=O^$))dv4o^~Fyp2#|rj-cuz!pcNex$nctgDVq<#*+w&KzzxcKsOU zwhjB<;9>LNQ~$X>JM%?NPRg1gFb`%Zf+ z&RxX4eZf(<+!vf#Tm;7hw#v;CF>MP*sq``Fp;0U}D*ef4{$?}&=D*e4c;he6-vL;f zG15=u$L@L@r|5Y)+?X6!LY9At$58H%!?^ysZijc-gwcJraRXayC_nIh_{qPn%N6t` z8Nt)cu$c(9f-5R)n4uJ=I#SUxL-jUSDxQlGY62?>QcO@d%;WDKKCiMdZ|JdfXl57@ z`dN2psncZJYlj!4!=fWgm$Asr^?FCpa@WOSVEoF}Ix-f<4^3L}nin>vrf zx3TNI_G9NK_kj@xn=jWt)ZyIehySsd+F5L%VVlcLh?1EhYEvGVq1TR3s|>X34Ash0 zvd<+M!VE#211-SQcXPx2d@?Xah~!Y`YxHsMW~|LK>`2u9ocLsrXltYU846QWdI&2- zQ|vdGB6jo2-)|m@fFFJBuQtQ8mjjj%OzQK)`{Jgb-PhTz;CVQ18>hA3w)@ro%h;>R zrmSts?@HsaY~#)keUTsF&civw)7!ryY2YpORZ(NI$%0*_Fv2co2&~vlGt`8Lv!?7g zWI{X8C1CwXxQ>@hUG3WCXf4xw%OEh=}P0YkK3;6&VJnXW7ES9Gkh~{1K-idwSh(#YxK2d z7)ua>FDmQeE;v0`mm(SJK{6Z(wg=i!voBTYP`EwNY$xw5<*0>smL97e*>r8Bsc)s( zbcuH^uerPA+vss0tw=a*2ik^YD{|*xw!U_K@dD2N`uCU1=Y{|9!E)HVtnW6>vx+|v zGGSnfVtLJh7$BbS5VcZNA}8TQkI?>3KaONI8Fqz1uxCSu8zd76zXCB(9*N`X7lgC#?l zAvg)_C)NmPM=?8zCwgpV2U3a3KNRq$Vsz`6BKrjRdO{xCO*+x3Nm(pLs`Q#y~GeMG2NAC;4m?6$kvj`YH zF-E?bCtaHWF7=FTjDZi}F&-d!FgvWaofq42QHXxX&*O004s$%|abs*3U-|6{Tb0)N zR^@SgZPT{eILz(Nc7E@0Yl3m|kzZ{lAO7vwvJ1_Up%3md$+_7!(uC}Wi031zLH7>&MSZ$~DFTe42^ zI_lAUi<>Y|%yP!CvPpf{B5< zW5Fh`7avN?!~MjFj2t^0hyBfY^Wj4ZVZvi?nSpRmEY+>*ZcX-nC`%@D)^_`D0V3!A+nBm zrY9ssn4&NzkU}aMo!%}x*R%_RPtITfzL_C@1_)tBhCE;#?=a`JAKS+Hu^mr(kS&)_ zJjQnMUG#UQS(nyvyV$w?cDwD^wA(iBzSd$-Uiz(O{Ndly`J%V<4h3OgcrZQ~;aE&C z2&KX-9gL+y$!bzdhEEH5YRS-R4cOTLJ^GSK&?C(}&%_j;R>&$zF(eKMrdkP8WQ1`1 z23udpl)b0-!&b=y?TG#O-)$az^tYP+5B`N_|9h`B`wxFI>NBylO{?6p+E&y&4)?Cc z0cLXz+JM*~ zr+Ah|Vr(j0`YZi`{f4bH0b+)HU1dDm3t0CKe$dQ5@oUYyAN_{{_0Kf>_kXV0YvD_u z8@9G-2jPC~$F?DE7qd%x!)2H9Nt^T8ZJ0XVI8AuC1HXUu1=_9p{S;@YL9QT*9+i71n zVQ7&e5K~#gzz{u9j0E|;97kf@7!M?DyCWj~PzL2WKYr3;=SSW!gk@9v9k*+^^R2S| zPL6kb@!O5pLy)JD%}nw@@B?`ufKy5L@2FlTIbmzUgL%kB?h1n`Vz3XCXQYoqAINQ{I1;E~ zer(2IEg5y%S<4g;A;y|8sa7E|Oc6gNp+V3}Xiwu@#C-PB?>1*I{eE+*ud)x``k7|< zqt7+_4?d?!7C9DhlyeSKyRmIn!OZcy+LtY_3)a-_@Ch0Y4918W7P#_=Ff)PE zXw$eOR`6AurXTvB^^sxS!Fl#VFvo%5d-(2W#Q?a%W5FEV-`Lq*YA?SI&tX3H5%Ag?q9y5@+a$JUB%^+0rXR=v0|Yrh7(IPS2*DsBe4<`R2P3&*gdCuUAi@Y^Hz|xz zD(!a#Hy9i6V^R~+p-N|?#myhNg<_EEmC3Ug8Dk?n9z8|vnjEp-q%q;G`m7s*&igP$ zscc4?C_y*}sBzR{2qp}%uXYAA6w&PK+nF=lhu_u#^ZU)Y)WV~g*zUtun%;v~#T!1_ z3?AKXhNmA3CP^y|Gut@~_jQ_F$GUP2*PO;-*`5v_HDjI6oZa}Q>~9I)-_Xv$@2gJ) zE5R-v^{^li6B>1Ji~%G<#(||jU=F+?s!T|O5h_2EAS7|t^?`jbMM;H&cJmbN1NPZ4 zIhu$e4#XHIk`~7>K~+bM;-ylB2FnZh0@b*g3iyiIA{o+lzN8Y99l>{|GdeIt-Cq>8 zYHxan-)Y7l{a!Qr$nU7FV#n_azK;aohac6ADj#e1P6~Y8vf8G-4#WN9G=?W_u-o>m zh3~Y5Z}aqVGd=izJE0K{2tM=^aEpubWykV{Y^42|@XF2ELLZmXj+tfzPAf0W5H)Gd z4A0w5G2Qs@lonktL##K}?5Z}U6Tg4fYhHY7 z)_na?xC=vkq(}Cc-K8Sj0;LGFu6ELPG&X*7z1Lu>^E03QPnsWn?O3Xp=cFUo;9_fJP6eyJW{~V84JK%2*fK)GM6;HSbZR$MC5(}^MUG`4NuIpdZ}wq|BA`Jy zMcd9rIftz@0Yl_rqO_;?VdaYt4G-9C#ryO{Btx2D)Tq*-*kuq<*rvvi!lv3lFuV2T zrl*ZKXdce^w&rxGZamVX){OO>KP|PPaAmr$J`(}P#{XkBGr^SsG#hA52!=EitOf6x zF2@$-k2PSzY9|*SGJj6tZ<1d~B-Ow2D<|%j8RH~u*I9KxzBq{aQ3!oAPrbD(XK0n#-xX^fOYtVVLDsad*;i{IVUA->JYa&jMTidcaOk?H1G?uTR($q_%`*^4 z&J}WV1ri;E5%nk&8tym@phd%Vv`t|czI}-aM#*B6syKR9=@pJ@=AkA?aZXW!9P3P! z%MQf@Nr?}2Cnn5sC}JuOrEmymfj5MDBIY%K&}$5g6h2V;9-D#~2Wm{|EeI%3*Lct~ z(1s5PXK!E3MHd|RH0G2c9=)+77<#Xe+$rH^2C1k6?@F8xKc#|!C! zWQ+^o4V))>E4zVfyd^eoSKvMs?2zmtOz@76^#^41A+i8C= z#Ar9Gn7pIHzBQ+hoNh-YXK_(c7;x-4MKxV|Ru~#rN6)a!=*mxlm?zK$qII zPWe>@9O?N95xKYZj78{%LbRh(0Ye9R`yx)mQz5H})>y!1n6oay*|g}67%T;l02o#= z8Vs5}F+ZVHAV(kw1P|nZo2m>Bena%I3BlOLJ!z2x_mW|>=7dX;t*51Sta+L6)ye8& zoQ_s!L%J{|06}D&HxMTL5DyWSh%dy1-eJbgjXwNAB8UmX2(myRMf~`}{Xj%?BsXU~ zhqBqNNjQuUq6mU9<9tePzDgl3DJfkS{0c$(t%~$i1P~%<^=L3VwGAft7LAs%E$j>b z3;KmQ&KaX7?HKn3+q@y7NQy@y6lWSAPl++IHQ-$DHV~2HkTCBz)O*HSA_PH%a-cDQ zRM2A_$c=^}#zRO83xcUPP;SNmBTIx)nTT#4@f(2QxuCKzN2ZZ$aNMYeX{;$Cm_KCPK6J2orEc^$gr$guw%fGWmf?askFzdDuY1&Tmwt+*UFc?BjxkRx1D_WM?U*An`LP3e$##TZ~avEsoo$ z98{@$(h{9j(&C=#VhYUoW#R*|X1r>)5{z)BS;|b4otc>9R9D~31at74zknt94y;@F z0#kahV0*EH>9PB@f-QY*lVMBPw(xcS$df&C(*;W~Zt+^(EqcT#P4JaFv_E0fGW>#} zU>i(OeS;m$O*VK6tm!M3HDGvS!8n!^TG+A)jS21}rDrk>Y>y;?b8$B~$G#1YS9r}3 zHE9wgwVT2ai+W;9?A_3YUDT$ptE`spMX4YArV#5@>7peS z>`QI>oFqu3#DS3HNB}x}PXtli57p^NV|uS$gkLa4F;D?eR|5;^8cM>reIdiNC4#U} z{y+$>0}BL^T?mr=K>*7OW+GJ(r-bkN%YereAr!K3^A89z7QvZ`5Ym5&P z3?vnXgdifu0bMpupdyV6e&r6SP**txb5mQ*2+ai9rk%}?#0Vp<4Ijc7ktkI?(&9jV zq=>%8#f_)5ZU4M4X`wL1IL{`609;|reZhO8_U(xXOh@V!y~nx6La5nw^$iTNk7htj zu`l9@rl3!_Y$OM?5HLC1NDclA2B=2khZK2CFvqUP3h~+G_#AF`z|@`rN+`A0);2cg zF+e)Q*)T746_|@-$59609pj*dIm|88k`PD5!Pp>_Xbt!S_AtuW#jXAtss9dTGg0Dg z4+RMMA|C`7sr+29kMV?5*fPSK3J=)Rjs#*D*+2E`7OO} zdmGsDUYp?C_3jR%M#DJ6x5T$_l?R9x#tH7Ulb&{x!vv6XxEZH=;IiP#@igPMI(QCa z_F*u|VWJDY({sU=qZMp$jP=4N$PCN-FMh8g9l8|w+y=9|gt)fA zyNcymVY;M5AJRvZJ~}M}lG(;vffx%Zk+npa;<;3(++>2mjE! zm^}(nWf|xETIgV`ME72{8UzDg+_SU8iK-hVIK4-jS-`cCoaMtBsgDx2|Av zvYN0Gk0KbOSJE&pW>0y=AA(rYRFH>TJ)jw(LepSJV(fu5Vt^y12Vo$JT!RNwWah}d zqoP6*+7t$o@N(+QbNOL5tQ8^|#7!0QXQ^-^Vu+LgA&ecusyj2n%mjbXw%gVR67hsN zf_!t+s!`t&RfX`dEXq0|(&C;7=9yj`0u3>M@F6jxJVQb_Q(MLUV#5yvbF3Ruuz~-i z9S@8NNDVNDLBjZgcrqr$9D^y!jlm6(q?6Zum@@>+h!qZ~alkzOTA0t&M_h!@tudV8HO!d^#W(=Iaw7@y$9feK_E2w&^gR<{ zL_LZ`=pCd3Nrg;!P>;VWz~BzQ>JOCAa&=6IqP5FE&poGoE-A>25r+6ka1BPNNzYUh z;hTbWtosQ@{u>F-Fh(_A_^sOHr(>pq=|DCU940!zI+jM{?hEf@xdcoHwyg~uXL_Im zdf|Yp;RalIfO(g3x(uVPbPl7GJc)I<^9Ov}p+BHMK|eu~cqT&er|?07?^HXHCW3EF zYK6-q#TmZf^q~r20z9K1;}zRThTCqUaerX5|Kx(Xy9l|raBg&UL$5PiZ_YGhwxl$P6m{ls;Jq|R?l=(p zZt>K&*7!_feQOP=Z?(79z!1Ojs@9Ig^bU{`KOpx54Qy@Cet4t}7P{;5TpJ_y_C@4` zFsuuW_3WJ&dngVtksqXpbweRUAGag|h@eJeVTKy~gnHD0 z={aXb@v_&*a)Bzgd7XBi&Si@5*+BeESRwhq3Gf#f@7+Ic8jFeC#t86xI{L3|syn zm@P9zT4X#BrddI;X$=0> zphLkLwRdb-VR-_-o>;3B-1#KeBQeqVWEZy$Da`P;RLD56dff2$yD<*BjMt4DF-G8t zFbtsh(uss)Chk|PlI18pD!7(N~vYF7323#M*3>6mel{;VxEYS|M z@Eo@`_vRXgb9ik5-+9`2gYAEo;Mitc1z*x~DHJyFQ{xL9#rW(doJpRI+G zoCcp`Gn8yJKEM<$&)b6OQM;2A9L^VZ(_CN6wly=9uv41{(*D(E)kd0-4(~2f-&;7F zJ6mupW9ppQrTmq1TT)+TyS(>xWqoa3cB@Tk*-(`9?Y84sr(oOtxuisiftsx@RHl7( zcwaLa)B~~GP6&a@ERJOe8BnPVMerbk%mzg|?p5tk-NwG_FMg+%^*tMpUw_ z+pjYbjR<42Dt((KkU|CLy5nV6JxZc*tNi(93-uRB!V?GBaVsp4 z1vf~9WGx~Ig9O1EsZ7*4I#4ZQsESap^`%0IyGS*&oT#ryt%{7b!XSvPdh{72do=~- zs12iy8DY%;OKTUubvzPAFiDjIa}=K~M$)3@v&~UvjG8zhQJe*1RC@(WON?id9Qe?z zUQX|&$&}RXNQ^UfA1iLC@xWMMEjgq|2&DW$FjYmWeUFP zojBICFSv6ya41HIc3>*lq8gtwLdBU8PAmcyGZK^Mc^{j`8G{)Z+?g zGZkvrXLNHB3~?$Ic`!zeTNoo64m1J4h-2n`mOjMffbk6G3cdqhv;k}`0QZ4q3u`_s zZH71i_hqT?8_zTSA zRRvGuxM0e}W-5Q|xK^5w`W3dXiA{VYLGD9v7&uEygjD#h;L2JdYs|5MQMio6ZWZa{ zV210~4HvyqT7LO^=nIgW0SlHrjD9es|Wq@U)1v zwrZa!;)Vc!B9+P>=g_oPfJy;k$NEu^nXcs3#Q*>xB}qgXnS%Bq(VRH;1=rs-zJc&4*|u zLpqjGUBbv92wSD514isQnDqjrsP*fPe@3TRvwZ2-(s@P#o7etjfI zEw%7P4b-W)XR+f>lO86&Oo#Bue3ly9N|fZUYT%t&dj_wuu?;qAI-;l#WtY8-s9lilvx!RDLPp zTF*Vj-l%~|t`%AxBZzCDI0x02 zYV};|m8V2VVT`DP&&3cSj6H}}jCc)vxnpZJ>ye7XP@f0Bi4uc;+{Tlp(^Ska(lDxJ zS}T|z{=GEt7m>usi1^LZmk>2~fhZA_5)f0aE-Q&ycW~;MO{y+^!Y=9z#KEufdSZa0 z2;rU+L;OOHMvQMLDe;y{LMnjqSt4A=2$|?35tkiMCCT{$toaSX|21Lv- zFNv{YkQ#~GMq=cc1_Tu1$@dmnUpza+F3b@k3IPp?Pvc{su_7XhA4Z6zu?NA_SfO_H zSYeiO2ZK}`2FW6f#*i_|+w}=gpqk&-QZiip&1rM&9R4XmIhIgTb;4XxG2sE;dN`{Y z@Yf^xg;1U4Sb;wGFOE!Wh4;d4Ks%?J|!Sc#%bV$iAgOs zFl{HZq>cQ7rx~E}{!DO#31TA+#>7^G_YC|NT)8I}iO`G?$sU*kyYC3+e58We0xHrs z6mAu%oEw>8*YS!jW=L5!H8Z>}>CokGVH?a01Q4Y~@GWxQ(J;CPp}2(Uk`i4^Y_9oY zulee$P4g*9fFDu>s?+Cg&z_awDB$B20{GD}yT6re%^{vq%_t7*)E25DHvM zuXMRc2pde(#%V(x(gu=-JbaM#Ob+arHz?eM5E2lKQDN91$&rYm6fltnf2hVZCT{AP z2o4~Q*NC!22$MpsVrdXY2r_NU0NZ9STfsmJA;=68=4i%924=FXGd^>o-Ho!3L@d$O z#;#QPS!*1&1T-^87-KL+5y4YE;noPyTx!lFA)1Gp6>;1LypT7>1PqhGLK#}l3S#Ot z--uJYJa+O+mAA(;zXL1K@VSb$7jERqI1tI@@Vte+k5YKkQx_AoWKUbRF|~R;!`E=Nnmn%gQ=z~Yi98r$Xaqd* z5iDckBcujQ&@*rz>0bf!2@t`WZ~;Eus%gXoPbC>QahZY^n(#14w4us_rrBT64z^+jom& z+s!qvDY-uh6}wG8En@z#*L+JGY;G$K26$haZ1!XiHJArai{R-dlOTSQ7O_vx!~nI< z5QqX|C=98P!QIn{KgF%*Om5CQp2>ZPt$1vH&J~u8iCbotIev*_Ek!Im9`r$_6zqY# zNKv^&%^+wt>(Z0%AQ4O4++V_vk%}f~_Qozc%7;%?m=dZOtBP(pBc=B0XBQ%35Pcje<31z5z#Ye0WpLQelh- zZ^XH!wJHvTFBqhbawyG7D?u_=Fc4411*Q+Rl!-CZhT#taizAtOoW!_F@!L2v>`C7R zf5VgbIm~fu?G5%LoEfaVu02VkhjYi zes!ETSj{EJ4!C;KL*5Kq&6oqfSHYIAY&@kr*oNhi9CJ8tfGzc9_+AFKl#$`v1=|{@ zff;5MY}es)ENuwJ*n!jSdOx_F@tFHaFn8mD#0(48ik~q-*v>2@ z#O>0rPIXUFcUDJVh^eK-*Ofx!IgF*m^G_Gm=`l=E>brBW(3qlq+owp1B8V_c7~=j7 z&30wO^d7Y{e>OzGBrzl`b$Ah|zaQ{g@4~CCRnJ zw}mZb8@@G8gYMw>D%f`6Gn-^?wwj&ucGJ_#g5z77;lm7pVc?AzA$@F|^I9S8=~9sr z-o*^#XoWOOwOOSmwPaYQpZo95T~~7KzznJ9Y7!zPTt!MGe?Ersn*5d$iR+RSzoS9V zaSTkB7Jmd|)Pw5oT}cKU)c{E_tT{ZA8-{E9JxK%+*D}0tx9Ni-Mr`?E9SQ@3oEyZe zH3nj}WG)1hSZ3(8iK17O4};6b$?9^L5!xUDV37Vaj|j=n5+LDO9g2TfNzlkd-X~?| z4AK>fz(Q1CYWr}cz|)*z)07boVF{S z;n)@Dunc>r&v3WIM|#5?x5ok!zu_LX2xFHNNFGmsaAzsd3@|f4(p;-)VM&>Wr^~P5 zO1QNGxM7fCx43*Au%*1m4c`n~>I1$RwuT{bhHV!<8?JG&h0=E6GB9LW17qB+86k1U z{K!}`gb`xu>)5`aux&TckUulSUnL-!`MTlVMc}=Kv)NWvYHa+mGDO;aCpEsgTRf}O zro?83i2pS`W{O|exx~*9`01iP#c37lcl2e9eZ_xRVO$7~8nt;*2q8DjF$iGG7|92N z3>!p&>dBx|wK1a%F+qX&Yhwv^9u5WCV6;bI*=&)A3=HznFwa&Ask~QD!%St5Ue(D)B%`;ec-x zmPwM<6Df!(24;z|A9rS)_`8hNvt4oP!ng}`1-R$gHo@I6H-U_dE8tGtMh4gt(0Kzo z91`AlQjJr@Tf)mqSnMcSLDhuEiMrtWf+9WnAze&nGy(D&Bw>c-WnoJhj|;x5V7m=I z*SOq;#~FXOVT8Qv`+*tKjwi|xsn8p1E|N21yUle4`V#ExeDK^XzV+M&H=N%E)=OR>0q-B!AmF7W`28a%cp^a~oZ?-a5+MK$i zP#zEToW%q!ht+Q9L&Z5aB6^rSqr{)jhvA0+VyprSkA8vycB}_xByw8YnfYyEdd_nh zJ2T8pjIw5sT?Dm@n9{DApfXVp82AE*t2KJau z(%-tVP)fL--?hxL<)T?DK&`;khSe2c?m$=#SHk9!KSZE4+rpOQj|0Bo^(NS6eD1>K zdLmoH%$sF+2UMe&#-6+~cP;kC_=#%d*NeLU!C(L!QhK@!#Rc z=5BG+QX-+BFHviy#5PG|rih=-HW)L;=C-y9Kk79Q%=;~KwB*=)PzQt%q7gXsAa?VmPQ8^B?Kfk@ zAI$M~i7|7;x5OZyUDPi=*}Rn)Wyz@-BT{9?7Dvd2c`li^mwdZ>QhpEku0k>r)bTZw zbNqG9_IG7|Ml!eKx@^{tw=rgrq}{|MNwbo;t|6@C!(?fsgsdDoj=q$=3jV8LPbJ&J zpR|`{d>$K=VRM;DQid7jYy&sfm+iF@B?*&MiMM8$gxPe#mbew9!P$1UcY*C3rXA|* zRWPiXAPqE*9;eo3iICt`DpKP9KgtYwwbcxf*o7Q|Z@syylt}5@FvXw!*{1p84;RGI zYE-u_Ge*K-ju<1I86;sJ{*cxmwG**6tC(e-uJ$8+t}@T*rk%?gBZBjXN(&Fuzzt>< zqI6};1a2F`X2j|$>$ofMl3}-3x3<3X83N4*Xv+uSasoV#@A!NknApI z*d-ONQjt>BWh6uL#U`4&xEAK#!Wph@Riz|01>SKpjcdHf5TnrRR-01jU4C|x5=sA> z9&fV2A4W{^C^1FCvb2bQ6=NjM2v##k;<^|l;WjW!OqX<7V)Xm{xof4P6A@OW-oW_Q zA;_vCjX1=2Ax~Gnj8HlLID6K(Ys9OQ0<&$+?G*_hCzGVgtr_Kt_C2oP%Z!2CmOLH^ z-*!yoGH|aNBXv6aw}qF}XWMv8*i{%M`QMLl?NWQv*4h|$t6-L4a-1^P__$6gT*VAm zF+!?Yx0{ss_Zc&!N!P~^DQ&HkNP@c;86v^!RHwwhre_xTw?NKkf=_!)~vM%Te3HFXy=`EOS}7 z-;c0099O|F!|Awsp&CcmF+#HLsv;%NlV*n0>FM+(rNbzqGw)?~no`;}43Tv8S|VYO ziz!mJ3F#)rNZOh?5{8*G$>NQp9Rc4o)d{&i_!%Z+9w(U4@`mXWd zIPBx>c}(uxtIPeCV5t4&yBWiiT-OED+=9mm-we-nu*`5f&W)4%0=_0(ye<_fRsIQO zhBUofLX4P8s7=ZDxR~N=6fSc?QdC&I*@kqr3Y9Pu&)az72ghQa5qCx!{HquwaqCRY zK)<~z@ydu*8J5GhMU)@92-OirMy)7(rwhK7F z<=NL<#&x@Qfoq20aYsYCU}HR8g^#PGLW*CdCe4hHbeGvpO1@pz3`y^4W~X#Vg6+-q zGeqjnOmXQ5h8uRbIBH388!3^T+cL#kg<5K@7^4GkFEd65*T_QnWtn7@c}WJC5rlHq z%@U)Y^pg=L1+OJ|T}YJk;x>0L%UpIU&1KTcG9_L2O-3hA=5+Oyb zS}P=O6(b}rGsBt@65n+L4dM6rNoH4ONY*FH5UFt2{gIg=lK%ozyi4fiRj1_bWQrW4 z_%yN0W)ov1_3h;>;R_^Mp_r)=B{F<#xsYv;r$&fe!Y)kD`MQvWxML^iuse~bO82Ch zUse4|?`4?eCgOU#qqfTuREp{xD;;$ZbQzfEqE^A*X^!n!V{bDaa`vvVk>hu9Z!^Kk zaEYmHR4ibx}_OzBOErU3q19!7jsR)r55&BczJ|A235+bG3vRv6obvlJ{{k zMaq9oPZv`p?r2BGNS=&Xw+UEbt^OsS$}0vu;VMMyT77WBt$LK%lDSN|$?+uAq||dw%#fnC zV~9~HKRcR@QX=KuErr(WiG+T>M8$7?Xx~w<$+3zllI8*-y##ZloOOtFn*@*99B*0v zI%IJX8EFVxhd9nt+aMIC>z4Zx^Z0)+z;??jI|=K$*-=~Wx-pQezMVZ+bRX}Ke;e4( zi|d#K5qFufFfa4RxP&zXou0li##rg}!kfEjmk?p5;xle$Y{faX%&;~zz(_>oUk zYe^FCJh)=~6|t3gTChz0)hO19Qkw{&V^4+wtz)@(GK90~8{&J!=XXTN-aoFv9F&ns z5%y9yM!Z)OBVO?GN|YrDlBb&D-=m&RMf zfz&yoU4tk2r{PNPMkUJ@*m^K*kqEIzo?9Wt*Cs>g$bQrO0VC4d3)D zB8DxWu_Xk%1CeTzAGU`dcuLL!78gKmZ}gI*UZD4!~B2UP;2+cE9Xdn=BV*wg!KuW>1#rYk8;wYbxGb0nJO* ztzsZ$qxZW-7-)qlSKEv0gl`LM3Coo`o{%MPV@VJy_8TD@0gq#8eJh0a!O8G#()`Jk zmfzyjDe00)5&ddn3^XI27-dq#4C}$UeAM?K5>ykJK4y|t3u43!cnPEFktEW_pf5$P z*qm6uhOIW%inlg*VV%9=fSDLB6812>Hnwh!35(ep45j`>bUY2$5==F$Qe6EbiI5`2 zcSGD~b-XFUhME-dBH$5Eig>3To?enIQ1mKyB~S@bwiO?r8hQAL6p{G2Ub2R@wB^N~ z&tF*MF0s%Gd$#n$|A@`|N6R%>a$gtTc*%Fbwgy*?$2soqi7<%G`ZRmo2%-9fTOnUy zeL{q`AFi@M)^3VWQ9CkfnJnU7vkBMsM)BM={ZwyfdKeJY6o zK7Zkjk31qKB;GK+e(blXaxuVdjEILEj@+MGOu4RwH(v4xY++L3yP~tOXjtYj+mj${ z2HkM4LKZL{^*9@$B5N}L<<>F>QW*}VsO6NvmARmX@z_AP{=u~Z)jM4eOz5<1ig-4@)XN2xfVv3HG!9PYHf68vedtdnse z2a9$IenQ2&X~7f&?}{sYPho4t^MF-=*6*SvL5!;TUnr!N3)~91OLYkmntCNg=iUF8+IV7Y@%fbWqIM@nObU4X z7B88+p?!<ZoLDD(sKphldJ?u@SSI!kYmf1@BM_zO}KxK1F*} zVuY>ip&2mhx6~5oO!CwoeqUsvZ3!gg>llIZGuw~IVlPbw*^&Os|El8EZId4BVg1rw+7UG@aB8oY!DX~r(qSZBV zpy?~=Gx1te9JJ1@?-AH0*2h<#!mtHbxh<0aBjHnD>_#ZOYkWiG+W99`^#Aaejpf{- zX(m!gJ9ahlBoDvLTC|m>2&0YfMIy~stS^VXoM(;&J~8k{T<#ekNvyd`Mql|9lD+uT zJYJW?gcn0@kK`g;X>uSTU7wDx(N9`M4$l&-NO!KjSt6w6^tmCDcGM#iBkWnoRZN}$ z3l}P(%$FQdxYnMe;dXk+mRR6+i{pYT3B1ujZm7?LyY&1Dap1+1*3bl3Qq~ZUx5@_k zthkQA@TQ57(CwcPcT3_kD*AuuWyjh>qNc>@xF(BgD@jP#2(pl^vujUGkgs=$4KL=j zHq=kRzXVs>gRA4~#dai~H&22T0;tC&M6xPxjOZVc7|A~ByA#Jt4!-u0MB#StBG<%* zwnpzkTZ64;pZfkf?yYhC-B*%qbq{>cz*Z(ftp4J^4_W{kpAflMS4fWBAIQCkBklBT zpFxnG>$#HdE5^sfyIq7#eB*oh&?Lwgd~QNKcyiJwRqlgF9!;Ei=y}kYW9JUc9jM)b zKac?B7F{PHa?iAzBi@hxw90*&vV8|MBH3r_BPaIyzRcda1Fd!-#@h#c3bk(0H4`HD zK9(FYW*(A!r^d)J(&VgrNtMDqW^2YnY-if`m&SnCi;1Uq$L!-%7<##Cw|e=J*H4Jr z7RCv{Cz31khUw4CKeCmE7dbTXeadAg1emAIu5K@az7@nd^f)36t1oD`?h|o zo_i)lxy3mWO~M>^EJY9+`6Qy`;$vSk2@k9-nA5WFguNA8tnI1ifCbw-VM=j$Lj8<{ znA*6%L>LP(Xs;$gXeYMX6DNG;8HcO~=Yy|5XL!p!I7cqU7T%t*V7@&?#vySJ1}=(?!Y-a@IT=Ra9T{> RcJTlJ002ovPDHLkV1lc84Tx07wm;mUmPX*B8g%%xo{TU6vwc>AklFq%OTkl_mFQv@x1^BM1TV}0C2duqR=S6Xn?LjUp6xrb&~O43j*Nv zEr418u3H3zGns$s|L;SQD-ufpfWpxLJ03rmi*g~#S@{x?OrJ!Vo{}kJ7$ajbnjp%m zGEV!%=70KpVow?KvV}a4moSaFCQKV= zXBIPnpP$8-NG!rR+)R#`$7JVZi#Wn10DSspSrkx`)s~4C+0n+?(b2-z5-tDd^^cpM zz5W?wz5V3zGUCskL5!X++LzcbT23thtSPiMTfS&1I{|204}j|3FPi>70OSh+Xzlyz zdl<5LNtZ}OE>>3g`T3RtKG#xK(9i3CI(+v0d-&=+OWAp!Ysd8Ar*foO5~i%E+?=c& zshF87;&Ay)i~kOm zCIB-Z!^JGdti+UJsxgN!t(Y#%b<8kk67vyD#cE*9urAm@Y#cTXn~yERR$}Y1E!Yd# zo7hq8Ya9;8z!~A3Z~?e@Tn26#t`xT$*Ni)h>&K1Yrto;Y8r}@=h7ZGY@Dh9xekcA2 z{tSKqKZ<`tAQQ9+wgf*y0zpVvOQ<9qCY&Y=5XJ~ILHOG0j2XwBQ%7jM`P2tv~{#P+6CGu9Y;5!2hua>CG_v;z4S?CC1rc%807-x z8s$^ULkxsr$OvR)G0GUn7`GVjR5Vq*RQM{JRGL%DRgX~5SKp(4L49HleU9rK?wsN|$L8GCfHh1tA~lw29MI^|n9|hJ z^w$(=?$kW5IibbS^3=-Es?a*EHLgw5cGnhYS7@Kne#%s4dNH$@Rm?8tq>hG8fR0pW zzfP~tjINRHeBHIW&AJctNO~;2RJ{tlPQ6KeZT(RF<@$~KcMXUJEQ54|9R}S7(}qTd zv4$HA+YFx=sTu_uEj4O1x^GN1_Ap*-Tx)#81ZToB$u!w*a?KPrbudjgtugI0gUuYx z1ZKO<`pvQC&gMe%TJu2*iiMX&o<*a@uqDGX#B!}=o8@yWeX9hktybMuAFUm%v#jf^ z@7XBX1lg>$>9G0T*3_13TVs2}j%w#;x5}>F?uEUXJ>Pzh{cQ)DL#V?BhfaqNj!uqZ z$0o;dCw-@6r(I5iEIKQkRm!^LjCJ;QUgdn!`K^nii^S!a%Wtk0u9>cfU7yS~n#-SC zH+RHM*Nx-0-)+d9>7MMq&wa>4$AjZh>+#4_&y(j_?>XjW;+5fb#Ot}YwYS*2#e16V z!d}5X>x20C`xN{1`YQR(_pSDQ=%?$K=GW*q>F?mb%>QfvHXt})YrtTjW*|4PA#gIt zDQHDdS1=_wD!4lMQHW`XIHV&K4h;(37J7f4!93x-wlEMD7`83!LAX));_x3Ma1r4V zH4%>^Z6cRPc1O{olA;bry^i*dE{nc5-*~=serJq)Okzw!%yg_zYWi`#ol25V;v^kU#wN!mA5MPH z3FFjqrcwe^cBM>m+1wr6XFN|{1#g`1#xLiOrMjh-r#?w@OWT$Wgg6&&5F%x&L(6hXP*!%2{VOVIa)adIsGCtQITk9vCHD^izmgw;`&@D zcVTY3gpU49^+=7S>!rha?s+wNZ}MaEj~6Hw2n%|am@e70WNfM5(r=exmT{MLF4tMU zX8G_6uNC`OLMu~NcCOM}Rk&(&wg2ivYe;J{*Zj2BdTsgISLt?eJQu}$~QLORDCnMIdyYynPb_W zEx0YhEw{FMY&}%2SiZD;WLxOA)(U1tamB0cN!u@1+E?z~LE0hRF;o>&)xJ}I=a!xC ztJAA*)_B)6@6y<{Y1i~_-tK`to_m`1YVIxB`);3L-|hYW`&(-bYby`n4&)tpTo+T< z{VnU;hI;k-lKKw^g$IWYMIP#EaB65ctZ}%k5pI+=jvq-pa_u{x@7kLzn)Wv{noEv? zqtc^Kzfb=D*0JDYoyS?nn|?6(VOI;SrMMMpUD7()mfkkh9^c-7BIrbChiga6kCs0k zJgIZC=9KcOveTr~g{NoFEIl)IR&;jaT-v#j&ZN$J=i|=b=!)p-y%2oi(nY_E=exbS z&s=i5bn>#xz3Ke>~2=f&N;yEFGz-^boBexUH6@}b7V+Mi8+ZXR+R zIyLMw-18{v(Y+Dw$g^K^e|bMz_?Y^*a!h-y;fd{&ljDBl*PbqTI{HlXY-Xb9SH)j< zJvV;-!*8Cy^-RW1j=m7TnEk!oGgTQtIBtl{(fr2=RonS~Sh%E;epa@BT zl>s{n?2R&lWI2wUSe7)_7uJ$SQl#Z*cV|4yo#k+5*krT2U%!5@UVY{Fzg743>rZyS z?wRT4yh~PBy?XVkZryXw|D1cyx#zlNSvunsm*O~&`R!xA9_P8I`Zu)5#uH7ha4nK_^+@h&otXgiQb?gWSDtzW?Yo&srav(A##-34LjaQ_9Z= z0{kw*34TTZ{e}VY3cQ!Eyde^<+#O>LjC-S#)JZ+dI%eARc<0*C28x^Alv z>C^k&k9N~rwb-Khhl$=3`kozr-WTAv5m5|UGv{_tcU%kP{YOpLxu#D4ypue9-7xs& zPO!!3SM{CaBx@t*s#9n7Z#=yIGpTd&cazFNNKygob;FAhrQw&PSFQGr4Q2%{Zk z6!dTH0r;mI?05ASy?xzhZgNvc(RV(^_r1~nqaR$=pAW7N zeVFMzq3_w`=RE;_NK6Clw*cuE9SiK1LB*T`_2gpYT4qeR|I({&@-TA6pkht|x)@T7IM)Z< z&n-GxZ`I8b0Dc|@)!vjjj@R8;%bjO(%!H{`gT=4gvoi0Drz$?hE!WVxu1siysuwZ)1GD`8Km%Rm0_Lc60}*@8~aT zrO`ek!Jke^;E(hhT{J&We_0!O9dqUsi z^7HAT@7@4!n07n3ckWFOHTF?p?+1^(xhdbxdP{EBa@{;}-6=pkZvglMKBs{DwBr;U zi!qyNm;IFaug_<$Y^cWOkEyzj{IIPSqii@{R3CRb=DZ&P!xXF34>sBt_+$0 z{YF`?)PQfm9+OY=Vcj`Kz<%kr0AN4He}7Fk+ZBNQMt27(0iHVtm}fou41zyzv9T9m z+diZA+UhrTe=+B}E#weRd27LK zx*Oi0yXh6N=QdM-oqfT+h}bgEZDc*yX||jq>jUsj({IoIpPu6djT+o}o8G3=51Qt) z7Zjy8XmQ+iys#|&q$mXp9)K^f7w~Vek7lNeVDn)abn|kxk(Z5ZzOb6TVSHo8skHj- zK!0nSo&LSn=7)VlMAI8$U%$!j_U)}cVoWfX9QVpY$6XEX(jTgk-NL5l_WU)^0{WDI z9R$+bPmb(`UFfeWe=tH8P%FcbTgH=-UsX51kaf1^CP5>c)Qbex6Yr zt^6`t{nGVvZ=gR}!9Jm{`qYz0-`xOTGp}r@%h#3wb2N9r{mR4VhfF&i1fCc+@dK~v z2HpUmk6q78H+x>jaU*om&JwR+U%;Gky~oYylwkrRi+Sl%XS8@hrIin>-zQ4Sug-Zv zObhlMFOAEd_FU_wY<|P+2aUY&27Cte2Su|G;73JX2=se3*f;apqSEZ|!sv^|-G^v+ z`vLtn?e1im-oD?E`Rl%+^EYZ>cbw}uOQo?p?$Vt*o;BH3BfIl%r2ruG=_eY|rC!f# zrqgCW6#jMpF#RQ0pXr-EqvzOuQgU9O=|aO|zZ;%-&khpk80qb+as+z!p??5>7rJ;G5n|L8$pR+|`*1&Od z*tufR*a?GyJ7iYNFlTh{`XNj|a0UYa9;7C|1E8<=UKW)Ceb0BwLB<|NcLnt9(=slQ zhGh`P#h}cKpllYsq*I0fK?DHw%B+a<85n=rFn&8v3)$+o59K?hLN6=r?&O(XB-w3? zb=)oIsch^Ou#al&eT4s%wDs1a+g$G$;V)2+5d1|tUir&{S+uYPcM+E!0AU~n68gi>VY4p{b4FP^A#czs0z^13;ajw^zW@0q|eKu%(|GCN7InSK(FzpQR%gm5dmU zVU=A!D4jtHz*nH}dQO?;Vd;7RdyZKq42v{NN*~sC{iMv;`(;t2Yyp5D#6=DEMUe%K z+%mp^zM1D}^lz8#e8B+y^3|(FAjt;5$ohz_(II?g$&pYv(pMNxj;Vm!rbirC=Ut% z-p_Ia05E(5u&*@wtk}p0F#kM^UJv7O%q?AezSzd}C!*Qa0=VuEddCXfx3qR^j_LZL zH|?$Biy0l&3D{?FLnMCIq7NYaHKOzKj6E&GaV7lu4n;uyL_$9h$kSg+_5yh)2%KQF zub|$m@2Eai5zvo*w&zc7Z)rMu$HUQ^-kuNkpmzw}`$gadxrBSfICr4EA4A;&pkFNp z;l(`83nSP&S!R4Pb6LN5b7-d9F?y>(o}PH9G#Eo=ZbDy1-sAG~>80wD%kC^6gi>D;J-4?{kHz4)D|W`>n`;yXS|2>!-nB5Wue)8#WsZ61k7M zR%VRHbH8vS4;9QWog9ecat{X1bIVQTFQ7(Jj>1oi=qR?xcHAkW447U-qXH#gI~H`MMDw7Z33H`6a; zvSEhoF+hGN^Y1jtXhdhV_R1IIg@{q8?w(Hmh7`FGMAX@&)= z$OZBRFKC22G|!-U1@!Af!QPIh@{Tn8b^I}d#iAO$ImQVq!M@u>cQD9L=xe!l$NmYS z?~b?HVWO$cGgf<_S-a~?Lu)1-e>02xs1^ENFA3mC&^nNkiMhRrRXGnclum&>@iT!7 z&=Ts}t z-ggRNJ@GRIeW~CCQV*RCnOI~nxxn3p;W-Wiu#lfsKo8?5AyI=_-tZgwAWYHjvZ5DH z=XMCce039(jUsKN*^LkK`!T=i5A~$Bp}Ee(7vnOb8*De%c?kUq*cr`*06GAVu@2cK z!OzRmAABd|46lqxH2p#ffB>Jpk0MakeZ-M5M?h}0c`$v%v`3#qlMgHj-t#@Zc29ak zX#~0vH)f>9@MDDDKJ=zb7`kJk0n`n>p)*v6-U4~{X@J@T-P1fTLs$uk7bDb@^|hi2 z&C_5imzhr5ydAxbnrteNry@hazBkBE=(~S6c@pTm*W2yGAul*rNH=glX}iS!1o+*JoH@ulUd{Z zM+U+9xq&-3o%!vlfjiwS++jRqGbLyXcZO~24`NR{l>@I#Q@_NuPQ1SB@mc_ zb|VW-4*X@<;PJ=d&Om{8m~MbO&K%>0MG|z;`vRlk$u>Xe4)W<>Iyai#AgPD%a=z}R z$?vVzS$Y`lPF;aMurow<=}yapKdvY1z8tA$GzYK;O-6JHX1aJ9BTb6WRHi!m2maI1 z5uI#TF2OL5^3~lmy6X{YFkDJ|uz9&Im=QPmwaY~z^{wToT_}-U1 z+28LzeBIYjJWgVNHR=Y^)Clea-0=`*c~nM0h$0$&XA(X4BR_xsr)`U&PdM+OqoBH& zzEpQB`}GsC7rTrWeYP8#DheqZ`+$AOK4#xwBNf}O^7_TW*MH-i`oH#b=Qsb!SKQ=2 zLme6NQ}lNXJ|HFS8&lu*xwo3%^{roz zzonIUL(NkBU z`GcW}?r~HC}OnE=(9=u+~53#i~px?_NZ{2uMsu)2(A>9ewd2w z6aZz#^t`;ziScENkR6QCjsg1?J!hKZdA=0*tH?gM4L^fA-M}#ZIkmXNNH0u1erw-wK5!9MXB! z+hYFO$!;R31nl@>YHeM97fj5=hUWB-Kw&K$3*hN9?9V&?+@D^a`tE<_%k4jLZo{9` zqf}PuFKcXFHuaZ;O`tAUhElU}+F+9QI^e%4c3*$|Kz23RD#6YNeHI`L>CFj+?2Eol z(VK07Ez*TmDSzjgi8DQgsLOmp|2~YOiB4zSZ&s_R{JjPD;a@Vn0W(xyV*c%6FXKdnWmv zpZVC@-~QcT^9>vs0|C6ptp2oSdLYm+KT|7H@O}@Ff3q;r7TwKa+t7X3A+}64LvR

    9Z-9dO`x9E8>fw?Zh>7@vYYL``g}V2m0Qsn8j8j$+G`u7x3qd~ z0(NQjZPA^FOHuFt!!D0tXG6fd%y$;;G4b=6J@m#qgJ4gy z{Xl+C$7}T%=O+6hxKHR?x5|FI(@fu&wgS%8XjUPBtqS)XfMr9vs3Nlw#w#y(epOn z3Hq%Cqq*Z44KcA}J(8KOAK?!UiH~LaE;GC^!(d;I{@7sZTfY1E=YHVABmbfrW?8^q z3x5H-6aWGHmbOlhDltbJyDkr)<*hP%K)l%iA4W=CaYgf#<65vwlRp^b*NC*K;^ggwsJ%j6WrCg1 zcZ-UnM`we+HBaBHm}5VXAJO0)*Em>jpZ8`i&-m*o=^^%apV$b8;JhL&qKGpV-1EX| zd@5@^`ycxlM_VseK4+wU&^ z2cP|e)}Nx=xzgC*MJLtX06XArfSul0FblW<;xSKf=@zJ;4hSE%&KL(3JRC%Cnd?Mv zjo>8MFR3+oQnyiT{F859`>|hauK$Y5sEy#WL9V^g=82TerPXJoW~)IyBl@b9WXAFO z_Pt`^qX>4#&T_UOU%kRk;cdg8C-glki9Zv1y(u)jCbj477Pp7)t>=Y&kMOPOCDG@f%BANljYvh;V?0QRo|@GI zZyo7|@oEbrH!5vs;(tQ~b91~qAgY)0>m2>~yQ}2c!*S+sDqI*Pt*Z|!7 zHLw!{j~S*0cB8GMnH%MNOIIIht}}nQC%u7k&)uT8vC%{4C4~B7E!e;GiOqlXQx_k6 zo~9$cq4GK8gUpQJNbNHM@}wzE%Ph?_H=@0sbtvW45#D6sTpn(6{&y zv>X_TyGL%~uHviYUc%ha70mwU>#H zU#{ZgZ{Dh_WAn?9H0*ZMT^cB2CN>G>x-%QhTplp7jS03sA&yF{gIR{B8_m4}cVZ?1 z`RDT1N5AV!^S9a^@7&wm`5nN0-3a?SB8D0lBxPgnKHpkxy{>}kDo=WU*-3ALV+8bN z^5$$(DKL_12&KzV5|m{;%hj|NB)ZSNsB#3W%y_*vwc-dOasJRS@zc z+oLogO?3p#gRCQ_?tY2`Jo9oY!6}GWU9X`;P~_*R|zXvj+IS0eFGWq#UjV)Hz^S*)*6E>BICJw4of4Yc%j9uOcJB&%4cEm|On)n$Sr;MuVkQqQq9}5ID&V6u6X?iL!tI z@=S6q+wG|AWq#QwPkz)6O63Z=dod~#hxL&e4jOQm@Fv@5hnd6^`tC)WlQ{R@(KpD} z%C0}XH&5%aOe@`4b^!d&ujz_d{G3J;3}Iq5MnSTRkV`~122@BS#u^&D;@-RLdvwyo z0Q`rMad)@f(az9#6ql649wwVjva^ZlH(>g8K=UF|<4a#@|9gMr({AUB&4>RGam}~d z7`&6#O9S~q?1KY+q9_@;Gl_Dh*(*EuX38sM+r96+P-@x58@I3Xl$Z1<3bS1)T2k-w z9ft1bR)}?PktZ|ox*o=&JoWMTk}Q-2IyoA=?Cto0iK>c`0`Vuy*?;NU@6Y|UUg|d9 z5;%eAUBKPMv2$N41sVbDs-`KEeo;3BJ29gb?0`I=9awtPOy|(#+w1-_-~N@wAO5vZ zJp8Yg3xT{Q0W&@(p3Pz2Gjpbp5Gg6BgOUV*hUQa21TPZcH18CbwM^C>c#>r`I~dZG z@iSm9Llomz>5&;wIXm~IuMd5vCiJBV$15jGU$Xa-&*U75{W5quE|&&(oaJt_sCZf< z^Gb`~S=R#rJ|Aq*)fz-dvH(LIImVff#(}~iPIc*CZihPv;H?Iap};`^?0nAT^W$2; zG?_Yuw7UVO*ASrhjnM1@o?Srxd<&=Cmz&@J{AXt0`BJ$0Ma(88c39>Cxy1mLp4p_# ziv%V}ED;i2sSJ-Wb-5bkWqZx~1DU^dQGN-mb)vdw*z}VOnLx4Ao8&kRpLu z%eV*3?9|MJL!yO~i@=GlIIWLexjpydX5ltuYhMN2HvxAW9fKWVJGdkGCuO7shRp%> z7~yCHCvCn5Wh5jwWTHcgw!8k*pLt{PfB5GgedoX2DDxE{o+26}HHn(^1^_*|T$Q4f znTX`?3`$m7!&HNyy@!U6nU6)qFyU0`oHv=M7Db^KD!oLR9!TG3_MoqXH`POEPv|>B z&E1D5OkZU$G3M9Jq$E-fQ_W0ms+lI^?{&k^bRl$Zjp=k$l>-5uxDKw;K)VUZv*aGV zMG2osG=kokvyeyO%0#0GiA`znP<{X-3!*0F7%gBj_`vi~OYt@u8TA0s9!x*qbiKK+ zPW|PjPxb$k2Y&x8WCqie5?>2?Z!Ms% zJTou2t7{Dwpu)YfPUH->9#&NH9ke`rqVy^d1Jeu>ZUA;^@MSdXG?p&Cws3PZciZc9 z4}ts8kqYT$Z&#IMJ>sBhh)I{&ze5;$OPg0~g?LsfVcDf!7vRul$G`IFw=VpZ-@5$n zf18t}RW=z6_>A00?og&Acpwug^PWk+S!gN9?qI{2j;2^#$HP3gjA?GkVNHf=1-3e; zlta7(1+48YsB z*gNb|Ng_J{yO>)Ve4j0y0K9FqWA0^>7syMK$8aMEhv5^Tw(WaUm)~9Z!R7OB{Wox~ zBrQ3wlxm$Id5&RoLlr`7ikvlbw@ZBChy?d5m}YRSVynfm>Q2wC;!kN-;BKZl417A1 z35`TwKRI*mFMery`NwzD7a<-q zYU?p}#x0+-)Z@bDQ$;eLxRt|oM2n|_;DDkddEhQ5>(1(HpjRz|sU^t@ZY&-~nWQhG z-~s3|(FlBcf)oSrm62iRohF};-U5(`A5|bv^BO@ake`Pn{-fYC&z(E>uf3Mv|9?pC zsbtJKDJgNYF+C<4_z7!kFx}l7)#5J?0(bc;7;+nfK3OLfVI%u&N}^Cl3wB_cDakRz zI1J4WL?EdzrUUu*w1RAeq=`PhdEw8II`LEL1FL|KK!_?St0LgeS);jgVgl|o!qJM% zG-^K?L1dmAf;^7b`58{%jsLA@J~{K--^kZ~gSaFyyDlVQKG~)75CPAm^SqCvl?0?g z1OLs2%QTj93i=MxO^kMPULI;CW#3QIjOBX>otV(~Xhb^3)rX+(_dws@*-2l|Bb_Y% zFu#owN7w^AYSzT=`J3Ccct@rhL59JNs-g#K@P0H!k~>D_DIR-5a{WQ*5Q#@hEc7_y z<1qe~8wL&lc)&ejlO`vt;-s8V3m9Tm3{Ueq1K2tRSf{4|`L|ksvH48+D`c0=Q1;1F zgT35n@%}C?KAmY733#pbQ(se%{X;_X6gG$RZFa^ zHnLJfcNuDMztX-WnZ*Yp{mq>_H#qm*Z->iYkO5WzKh)w`Q7M7t9k=I|t-zgLVOhOk zrd(~`^)5e>l^5VZdpBW`s|~dG7|j6NyOXoFE~|Fc+PldPNjd^DyNAxh*}v3b8g8F% zVI8XK2+I0DjFizBZ&Hre0!FAqa|o)|6vz)497(u`19xdDIsfP0YTWzVXza{v!$^l9 zPI0(0e8VNK9AzQ4+BRv*I&Lp)m(8@xke#$rSa198z<2SJhv)#(-nY^B!R!xA=zBcU z9qQLdqOaCg2u;p#L^TVcr7Zf^Ae5@o;>#`S35WHRw{C0%EgYos%?(};{|&lLUq9na~`7lhE6 zf}_Tpo4VvOV-@Y4F<2Q7wRZ+-D%=_Iic?fknT{oI4pfVtqUajlA_9EDNL5~ zR1m}_hVl6&x1Yc5^%fHn#B93Dm+r%WDCpdpK4GG1_5gUrQ_eWy#zMIolRt_M;-qOA z2Ly1XfV;-Sz-2!vAdT}xmote%UhW@*Cl*zLD4e&erDNai05{)b#JPzS;~yw z3AAJ^NZOfa~{N66^zh#$csfU z@VuB61X-kILf;Rpn;k^o>16KTaiaVGlc6v4BIGGZSw{y7`x<_AQQT(MJ6>bjFFN-g zdPzK6I*ZqLnQ68G{L<~~gjK!j#MhYpcz>OMt(L>A2&{ZXnWQDdzNBR2&Vs@CSCx@~ zzN7HSg1Za^^dOJyvs#5xhMGw^$rezXqS2X!ZEJvM*dF39UGK%$v7r4ktS3lahs97? z&6F!cH}Mh{4szldcPbgUX?qD!&)wzVq3diug;{OQQ*;HBD3IAK6^B_TV#z%wOX6)+ zF0_*S<-vlx@Y&Y}-ZS#cCp>eQuU%E*!bvFnz8-$p zg$vjTINH22K|I5=19#JNd*|JBCUGx6P#TKe zW||Rzzs0+~=qPuEbLWoZpIa>b^)A*|Of(cwg2~aW?vC3PLomy2Gkbf|91Kyp12Fsbb@Voet2+lwWFV5iq|DfO?cWDwH{MO~q%pPJh*No9)1JyPF;hhj6{a+{g?y4;6v=^8{b2 ze*`O8^G}M-8QMF+dB&+~25Iv84qs6|C}KfUnI5yHsVC;8L^?=^f^dle_DXteso zt_$pi$J^Jhubr%60(rmbMo&Al|N5X<{(=H-v_v9ViBP;StQ9FK-IM}>1f)TWtOf}Q z=2`Qekr}C{eHH{Hj#fTd)UnK+(AQFD7x_*HeVK~}*19G*CSyFe~s zo|NYdq1tb_QNy46>_)l#i=1L+l9Iupo+0T6A%9Y)JvL|EDHiPRY3$`JI9aml=B)*n zHUBhf_XvTK=~nCP8IWftEe8KAh=X|k)eZj-Ym_EaoZiB02HbU|qW*Id#onjhk9;&3 zuSblW+x0~I-L5ajIALFH^r!Y*y?@{6tvc{-dOcahL~|{S_>Il@UtNrgmy8DQyA0J_ z7C8stOPnXH8$!Au#Z;A~fyE+A0%EjxdJW%`lMYWBBn;wkLf;{YPbqzs4+^acnT*+$ z6$0pYexreH9>okkb<1hrP1f+4U#p^NZt_Am>vs`6S4GQveeX;3p=X`foY$RMhIK(V zFS|2S%#M6OQH`=o$|3e}-?w%S+-ZSVA|)_!4JL;%MOg0lePu zTvqr757u4fC2qPKULVx}-2iiJg0>$5?nK20EY(OL7Lpp>aPB*Bq%7KPCq`}%A2TgsG>o-drC0P(*dw~az$L^=W_sw_ zV04ElcR7|KKcR0$97`(gPaA#vtFJ*?ANRSUEHJ;yPFn>~1k6A$dPw&Kkk4MnOta?R ze)I;t<%Y>FX#id(7|y5*Ihx8&6kHjt-7Qy>yliZ)V5Vs@Et(*$qKit87Ej6(!e~(z z+)erI`LX#eGomrZv&jo+8;rvzs_4DyZOG*EIF4=-aQZC9x0`7Xn&~9kMXhZ&?k7Xa za_)VC3F$EX?h=OhiuZtebg+#>^L|fzM%0o5qv~p@sT7c2`HCxJeG1Z{O z%hW@@6aSqw{C4?QNL#Q~cMI+o%=l{TQuUmz_t9rr`1B0z_uBZ*X!N>9#onX(bh3YI zT8nOm-j=SG-j>$(yy!jrOc6S#`Clvwf}wFg^UZMfbG#)jO$2YSS}G zn`@F~X|Jt*UyhF5gu2JEis)%+ZRst=#?o7WZ`(=xlPI>WxR)6*(njwV;kTSG^Zx^- z4%<3J2EgHKVx?Ixi;1d!taQSl1HixnF&blI$HmDCz(eTh>-mJfdy?jq(03X7o^ygP z7{v|K8mUW+;ufOB`TfM}6F^ZiyCKKl>Rhet@S5}_f2ztBz?+Yxq$QH3Gyh6?H;&4s ze4$v+W4Doa+(x63yS>Ei;gbXC(b);kB5A>(fBy^L*_!_YvL7u1@{Ai~8b&wJUa^>H zgPcHNfISsU#|22r5mv1qd}?+k1jyiLRVBTV>a6oRFiDdIKM_8UHpyBS`-Qi-QTi9( zjofF5S-=}XQI-eH8VXa}1@tGu@KnWAQhV-)lbalnjm4zj>yl-X5f*I@W8;07nU2Hy}@hiA2!Ke|4!2h98Z$ z!hW}NKT7ZV<=9p9*0uVee(N^U(w}3LGuJKri>tYJ`JLE(hG$ei60NmaAupiUrb?)- zPO`n!ZYs3b+phc3b5iQ?q@a7@NYU&lEHf?>6ZKFcAH;8%=AbR?iA9cd(j+hNy*Qe_ zih@6(?`fxRLH=%Fm!8T9o#?nAMsZ`>i`#aaq;tmt>z2t1cK9}Sct-P5(iIO+*#+=A zS(#~GE#o_ilUpepPhBcVUPOwb26G(o%qKm!56ENur{iW$x4-f``au3*>5PQe(3j98 z$}q%0q@*@)&}xx${ssJkEcvtOPrHorLNcD}}9g|aD<6qhO6#)m9P zOJpZT2SAkX%mAC$vhYUF==Hpj0;Yi&dv4F$VE-U2;`V;4&U+kV_LiJeBnZ#|v#J8!^W0(L+k;pNFG zA|k_yuv(?T8^4MS?|_A(X@cg*P2Z6cV((pvtFfzSerW4Aq3_Y!(bB-SUrpa;F!gk= z-+3K6g1D^5k%FMGsUzhKGb@KI$ixV}D?5DTDjwbzvDQer#j(`;C8u%kdU<#H^>Qg# zC_2n~X_D?_6YVeR5oOy9od#L220?>+1AEG52jhVY%%(82WU%w$vd2syn-LSDK@>ds zpbj^uQs`iSXZ7$MRT!zw2YEzTC`l%ukdQ)Vf@96< zudjZs;jDh`+gi<^eJeiq{db;nuNlo$hHrtq%r>LLcRn7us5WZJ(G4}1bQ0&q%_SN< zQ=W_lug_RtOd(p%Z5Ca(hXcLIQj9FtMRr;`2m*j5b}{k+^aR?f+n9t#LHL{jU{%mQ zQ-G}Jjw_%{OV;WZZO8fjZ)-OGey6$kXTB8q-&Ia?HMxGA4K|r?tYEjbv3tHcQn56sxTvB5bT7%x zU-mcdXcjx`tYKP1jt_b`Tv=(6QOh@Y1@vjMAE!;3;*kFl#uyX+<38B zN{b(mI(j3REvAUp(I~pt>yxV{WQLqliy6Zo*-P=+ggACs?kxk&eP&S>L~aqSr63>l z!&DipVG=6EuM~aBttjgKr={raszRO*;G<%Cv4)OScP0KHVhOP(iDlTsWB@_7F-#6L z9%fC$G8k<3RxkAL{cv>d(hq#~lKWW$I?Nz7d%f~;H99f5sAn~K7(L6}IozX?LyVNz ze1>r-ahlAoQYdPU3z}Hiaymw)7#e~p9KfDL2j(|{F~1ow^VWRtiUIRcVA1o&1t@-M ziWN+1ZVi2;wO<6^OXrm3;CeiYb z+OyITfSyWyrSjKYKjAxdqTJMQvw=tmp#WdwFZxLM8G;>W|lr@2T@J1;M^TbOV112q^7CEap zTHEf&lINWJ_g<+YY04WTPF5k0f^2W9>%D}TW{IR@FF4ZTFE0W|_bOiO1ye=|^@+w} z!JjfA{}>&gP)g;(AW;>tp+J1r;>3lISrF|($Fc`w6y!1iXUdaUJOq@{(@swr0Rb3? zH7-omO4M4}K$t9SOdn`_yhX$?35OQZOFr7Wa;5WPnq9(l0NI6ccoMEQ|LE=ib@BYA zs}IjJEV7`kw-&<-fV7QMETdbHr@q_@a~RrtrEO}21gK;CpqfR#f!t9_0tr?YWkG#! zfc^u_G!g*#;XSY(O;4jN0f9@Ncz3}7fPIz)_i^iNsjd5{-qPF!?w44-$aR)K@#yVa z$!5AC@q$_+(Zs@~=qP9bg#KxI4s-1oTaX@EDKoeXg$7+6EuQAadc)-cd42}cSED6v zzkk_HZ$oRml>nnaT)&3iNIe|SJcb&XQ4O75Ac+}L3A;SeoTkIggxsV3^2@$AuK}8NZpbA zGRvU1Yt(5*)u{3l`i@E8Z=Egw+HXwH-g^J&>zr#rU52ZgmV}}l$_8(8#}?S%otU{S zOn*|h>UHj5g4Iz~#Ac?LP)Rxi@iop%*Iq28#mivdzdb1La(MZYL(YS$pH2m&GLrH_ zG=|py)Dm&ZTBuBCUf6O6L~Y1I@1hyfx3JyER8AcYun&+p9*czL+E>p$m4A&Q2tNRV zk-$?Rk4av3Z5RnAVXwR|%pe2;5MQKhA$A46HTl+Hc-e-Pp)LJn+WX00n`{0h!{pN9 zR74I7_~yhB|ETRmJ#CUh;CPm?kSb#+Y!v_#%x#3|04%C)?>t*tyffIW_HGMXeBW&_ z=uie|qH$_%-$1O}X=r6n?mY@Bd>k8`^v>D@@UpqfEVIPNrjuV=S^JS*4esD+!lr4M z7_^v{DWJ!)4x}p<5Q{kDMY}$3|MRcTw*QN5TDvvRsZGZ% zw*AzGbvtfib!A$E0RYr@qwGmg(?-=(ypUm!Cb`o35?Ck!nh!al?|Ag>udUttY!tkY z^z{~}==D%fs*mAQCMknJ4W|*NZa-5*Lf^5GG*Zk>s3d=f0AKY4{%XS0EwSNCj`K3M z_>WIBaAvImz7ohwlkc^r&AN~M)%CIs$`h4|&VWP_-QGOIb~&`{P~GmwdHjpBGVi|HSFcMB%Fdy5`(_@XngpnJq0m}?C@8i_Z`^=sOUlUb5Bz0C&k|K#wYlPtmMWc^T zoVdVB4bj7nT6WW0^ryOJ6dEuhHHqAq^N%FNvqU@rA^?woCUI-@EcHtAFHV*p{_e-8 zTEF?#i|%L5kk^8`1c?>$N3EW&*K1jNs4b!UhU+ezN3USvKIkmh1W{CcEb+t0WQBE$ ziqQu&y2ERR9`7K^4`7Xdh##;1vTu?;n;XQK`db-JXq~Y_C)GJei_vKF`)7rBtLLiE4#1sRPWaXnB z-7v(Q!J8SyE@Cn`?msRl&?IFyhQHMYf=|Vi3UhPkBU%2tub%TiXKb?4 zDx5M}wg5@MZb5~^v5Q((_mOEvN(W9=KX&q|JNJK9O+uTcXaH1(s$t-x3CV-TO0%I% zG>nyaF_x7Au?U`Fww;!Y_8k_Vqt%_PheFe!HEC2WPGHI=x<<29K+AsRAbP8brHCM8 zh%_a7fu_@%IvJCQh)aRyCNmGuR8kjmXyYniP)GYQ=0NIPSX+JRt8??8B{PH(zJ}%! zn7Z+}bmyl-dWbHJgqUeOMGGiS%F}ces$3mIJB>5skD*q4OOKQW$QAE1egDCPz9QTq zi{&>sGDm)hFmA{2X8qBi^*8 z8bJP96*`HDh9%!#b+y#n<=L6Cv;L-+wl0@>2k+i=tH`j@zTAqR>%IAwqOp}0>)MpIA-CQ-eGM~)Vz#u=HQxGRa5o2Eg^Qq)|8 z=1!Og{7wdC@h%s#8VW#AIR~9+CeL3Z4dCYtunBRj7H2g(-Tx@X0=znb1$g0_KG*j) zW+ef4&LEzyP)RE$BujfoP&tgL90J{i638V?5daew4({P%u5TKE7a-}Lb~_H$Y@G%0 z>IUZ>=OgRI?+q}`pkd;X*Lo&6XfVwniP%t27y#0i7}v4?;pHJ&Y8C{25-Kq1jtEaL zOqgliDp6ydjWoe+(k4QXdz%mzJy0*Of7zn9UXU_M?yttdpt0 zDcrq8A|b;GOQDitJOv@r-hO|j^Y-PZ=l;dr3+_ivjJy@-c3(a5=zlGpYnqTJh~QQp zj3m+?o1u(_$ZlxK*it4Y`lQKapuL6~tnt~H(D&@nSDlNIqVWeYFoV9(mhP=o&{Ckw z21fDeMfxka1ztNcu~X_^U47X0*O+QU+-MvieOzThk{t12b%UJuNAt91A(xs`cbAREOzprcXvX)gkK(Ar;gykwnh4d9^? zb1_1ZbtWMUjSL@5LcropxW9N?nmg(lO)rNQ3A^MIBp5MB26vs9@w6m`A}g#Ge_VhU zn8`dNn>LIu=9QRMt}q8@Vdcni0-2 zb5Gshg1h#|p_+h6b+wT*)Oo2l_zevbcqpO;AHAYxP!mu&0ryzV&7O=|cnZmplcChh zlb>f%gkHLa!C|WOx=Fc)A;l*-9(Eetjk15&uD;*-*Z!`ecL#b)D-z?VImpjSm9TDP zxB*PUP2APwSM^8?L4ID^o{HS7`pksBr=Gr=#K_PLg<0!3iOHcG zd{h$l>@;8|Af^Hoc6C|4_A>qP#nC95>YHs^eEA~w_zL9p?Ty>Zm`uqTuOAzrf1$J* zy*kU~2Q#j7wZ|HgbasXcgH;7UwD~2&hZ$5X=R&Prf^LDh{B)LllYHqTWl8UVLYq#l zxGsr(@K~e1qpK(6#}62qE1FMCgyn!BN`=tCR!Y=G{5BIrxmkv9V%0E|nnN0_@I*{* zHF*w?RHU4WId=fOxrYIIJ+><1<08afH^8vejLSw34M2(Hr2;fiCyUq#BYgC#j||Be z`Or!$4c|0wTL!EFhWSUjrbL%dC*iV4BrUMyxHk}$q07& z8#DpRYVYIHU2kFmzNS6ov$MtdQZ^ZhYDv=?+(lCfZ)z&xEp3pcSt|{GLf=zQ-#nJ; zh7nK{H3}SbgqAVyWjZ6-m@_LU>wUI;)SPV~j}$NxV-KL$4VEuGj}M=| zOYCJQdcou*HbVrA{K{^oYg9g#h^sGDVLax{Y5t?Sj+Coiq46d}?a5oqrYmEj~;3f{(Bw07WE`S%vNs|?m4_k=iqZ9$WC@u!o z{UkK>d0VMbS>GF&6R3mY+DRs%#bczV?r8A(2W(Yf#f@}--Bb0{ZS1qgt*?Hkd#EmI z-Z>5f#NMOL2RurFj^w~nqzpn)0141D`rsH8#gV`=7Fn4^S>7TNFlyx~S*soLBzXZV zj?K1FArfb4mU$U>cF=B9YV+gDj^9{%%bv-QH%r=X8V}sYbPF31Nhw%)z6hA$L3E02 zG@w5gr3AWb*rd39MnB9Gh5lK8dE=fR&F1+W9I9E(RkKk@^62vVpknR{A* z*Rupl;}+FS_t5=nfL~lI-;qv9r>$npDHIZr2g{96#7+#T^Bqwu$WLNv5`uE4^Zw@7 zi-o~rad#S}eipMm_C_sKGw9a$XHA3g%Lu)7lbYsc#T-^Qn+w`yXPOXUW~t(A<*itp zn90qdqI;Ndfa^6d;Zz89c!yJYv6z(a+@EuwHiA`jvh6r7z>90Gz^!SoR;<9CFObwS z$0&3SaAz=y3NfvMsV&Oh9&njfl!I~+?pWnk6M zdq&fe89{W>CV(HeHH6m|FmSy%s{Vw&@H6GC&{t+t<##1I*VMaWruxwA6t%E> zv}&&&y>j2If1~I2I=*R6A`H$_8LjOt$Gvg;jbi!I3+`Ps;n{4SNkW7k zw$a&CWJCtc4Lh;dA}P%7;tuHV%5WfN6u1fK#Gu;7EdqE^Zb+qhT=oT;5?}aed zv&JpM*5L+~(qhisc;vWm%&!3mgzvNRU99C0f(8{!y)6Ot z7GU1U3j8m$Y5m@&(GgLq{aD4)LwidjvdH21=Y&9>V=ehqKL7`{(ai+@(%nt>(1_0d z*4}SI-_uK9Q4_3Tq6bG?#f->JQ!~j#D;7H64e#9)xl*u;?h~X*4RFp%7*#i6)4<5a( zL}$yGRx{?ak@SMN13^wmBhw9$OqF_XqHQ1|rWxsf+*|@gnxR0z)>qFv@VYBqJOx11oss7(+ZF25)uND+yx+^a+s92gTAQ(!bn$t^)UKpx4a` z!Ab=KjF#=6M!z}Iv3v6-*S~0f^Ad+QZog2x>b&YsUki%m`?E#pccFW;XfOwY2u_5O zggRj~Z0O4fcP*X7^2KBeF>R^$wrweZ*EUX3zY#5Ldm0U1wsn*ZBlYPA)PS~xm>QN< z!k!clG?*Sbp|6_#tkKufQFT`_*#N}H*(MV&)hTutvE8H`vIPJmKt*68GXQqpJC3u` z(!{1!K5saEg89sfiJFi}e7PoOqB1Krjr8|&s4-b47n6!E$Js_1eYqocBsMIx*n&F`mO3|>skwrfb}1Fkkn054iIqUAQZg;vM?+HRuUaF;=;p`>Tj~O zva2Pl|R8_DK6Fhk!mkOKj_|=?@^Dd)TgSKm#}yWioD0rP4y-} zGa<@^&jRj9P*iyJ2@GEnYR=4w=H+kS2wJLw>hwX2bDEP!OZ*-a2>WB81o~s*)+{8KZ!tdmpF1_O}+4bZ?C~1ndHNgrO3LkrEYP zl(t+sjFixht4xV;ZadD)^`{SN_p(#fL(g9!!-MMAMX+nnY%z`2%zH?1@g5RhdQVGx zy}xaXr>fOk!ODlZHTV z5j072PrQ5Id3EHagj?e_-pFmnI(*fM+B4K|j!4u#%Vv=(K^bcBzi2isHIwfJS1|!V za$4v;?K|5x(9mvNGTD1ry6fHt6cbr9be@QaJbXgm16u5C(f5ECF=RgI`^k3)w1COI zY`&i#xyq_jk-;-kg&PhxL#&-lE?zs%CPvrqEu(9W)krbguzUbW6`77kzIyzj1pwXK zKF99It@BP`0{B?TUDKMjLI*jNiN#z_c}lM;k*L@A=0QVb#}@l$R89MR;&=$1*xjieB0=pMS4 zHhtE1Kw*l%Oa=#BG4hoFzygy66Fu*cKOebMb+~VRrN*N00)!dnnScbqqfJ6oB&i6v zL{t55SGW4W|EeCB*&aM3|4ozh%TW;|YHlucr_>gQABTUX>Wo2LAes#kjf zy~$If{2wY}%2B|y%zm7G4KSmOE}OPhi#-9)&^Mq;j82HKakDx zzW68v?&is9t^V>s!pLNa9I4Xqa2WGGJoEk>?DJgJ(+E#$L3nw!!W72_ou->cJyLwIBpxa|yys?VW)jA8hzehUyt#>(-D+TJ{c-KE`t438=iUZX zm@1@9l0yTWFkFl*|F1s^?tI==bYrQWb+G_o>s&#x{r2$tJ?dK!)^6p<;cs;IuGD*) zGTN4E&v7+)wWV!OD?F+v9R9Cu_|ZD)o)h{Wz76XuPbGcpx{0J`vg`uh{oZ2S4r^vH za?_hPhj-S|%g)PQ=iYU9X>i9|ITw22YUuf9c~0iMgi0KokYkE-nx3hQAO>blCK$_7 zTUwl$b6g78-wG(iq$)ZmY-X`#6_#bg;pdeJ6X)$E%0r{?io8cUFPwwS!P&Fe+FAfm zJ++P7o}=|uomF)1!%*yd>bD7&!C1fDKYHiXrz$P zOH>a@1*~k5r>&@7S;>e<^%NaOiZPLySrDAG@2U6Jb(Z#34;}f{ArI~EcRcfuzpgIZ zuTCc+z$f&rX#7Fax9$w;DyO#W($dxWETG?VoSS^SeVcT}b*YlvUpJqYuidD!SK2T4 z*jH;8?(u~^bxj8>JEYXd%Fsr+e68i)zxTG+$kzStAol%M?1!DwA2bI({y{%vr43~j zWOX8x_<&3>+&{xjGA`DrXA|V|R`8=7co@cClf{u^3)pw_ZE!9~y5`$Sr)kBV2EKmwG?45OA z%rUrjmEx>qF(fo&>_nP`Qz(K4bneL{!`x;AvNFmC&H&%0sS?YQ-|_t_KR zcWCe1DsxsoSZ9aotM{q9UcK9=tn^k-6 z6GzQI6^n`HL;c4rT044zMWIP*?=r<}RV)Ya)rky?TJr?K003Wcr9npOW ziy{&gInW9kMUaQA;l{ELWR49m)cF3z*xYS+d(}`3bzF^D+ItNkUO`$&pByNHcP+2X zni+)iu!*h77jy~S z6}e)8ecJ6`V&QLrdOL8FwY#nWedXM;n{NgzJhJ3?bF4qi@>M}{nbmk$Zf$;^-1sd6 zXsf7EIqPler4?E|THJVtS%AFXd4qgd6lLf0!)ZAuNic#%y;S~JR@yF8vc#5X>a47Q zg$B(nB*rgr(t`uEJ9Z*9(=z zk}y5;qNKG3KYX5|Y4!tFzS;=@7Lx|}J>7^C8zwCDI0qm@*lw1VZ z?*Q!c0ZRn>Za;bC&5@rwNLnt-GP|p-t~=F1%WgE>e4vFbn1Gb^SzkvJk*1tHedsDR zzRH#|s+F+oQXl5&rU7;koiEyWEE{eS>0U9pYX|sI1F!JAfnPNJBF`1{9uk`vk*^sE z!dh;uS;2&ROyPXTtE^o1KW&~MT-7Ztz?+j}{1N4(y^jDqSr;D-+^h`H;&E7-ye>S3 zJt$8SuReP(sAf8OWU}W4FxyCj-lOaU06kA2u+?aL{OEvFdjUw*!n=N?k;Z7G=B^7) z>4|;oJ^G%u9n~J=mc80?+`CmU1h7oct$NOC!|Tp?Zd2=xGqK2M*h(N?V0=~@s}_K)U(zKc;ung6hq&~K!@3*bjmg}Uz~_XyTO-e@Mj$zn(zbu|Ox ziKJMpDQGeu77@Zrf@VZG@M-^InP2riNdOoXBKtr6?0+-*o}+C!?wg$i!0%V534M=F zmHpcC=y$4qaT4^cpRMk8x{A@>E1SE_GDdR;+^;;0+|{5#47&W4f!9mcyspo3Y3;xp zC|t3-iQ$CBF^OYAh4oO}iAzsdM$dKQ98+|o0j`oppbzLbgp3u+78$D&OXLsZ4*RVAYsT(Fk183devRJ@h4<_Pzb4TjbE24xn;>{FJQ!aNg6 zdk5S@BtY7A`#}><b|7B99OW!=Vc|u8Lkpt-?asl^_!6`&K$}e3YU=P7Ne#_-doY zj&hGPdab51Z)?lC9dL$^_eQoYYuc+Vk6ei3-Bu^jc0%7HH=}-VD(PFliE!ssl%aV> z;BK6%wA;NGoaEx`Tj0JDd;K6}ZH>O)^X9!EWA8?OP^5m@Al}^NyF6ntOqTIXQ0Cby zXi)2BGF!aP5fiW+vP?GU;i&ifzC67jABC1$-;$%E7!^x^? zqeRaL#2m%!DJPL>hoa=)lii}UR47Qw0H5O zDtBQy{R4(Ayd_2=R{v%(P!^hCaZa4AjA6)umXY38ZeVHg{31_GzL)(#1_@^LdjYVP zXcagcpjQfnaSCdVrhuu!s;|Klo?&I(IJZ12zp-s;sBK=HwlrhFaobYejds*VBWOEn zQ8mQ;6Z#&t<#u0x8tH3md1?UOR7kz;R#i2ly|cKr9IA4rE(A}xouC7_M}9vm{2)(! z#h;UxHDDcSG->>u1*$DEBLpexK=3KMyyr#%KGs~u6t_$PclH8UG00YtK0}AsYMLtW zp3$$l7CoKwpuHj>b72$ST#O%Rv%dD2`zvRv!X)cOG2<)I?7M$q@dqC+`OoSxE)h8; z(N?9xqe}wMl}^9_ydHMQqM8XL(cWEY?}P!Fdvki`>&b6)wRklOI#nV7-_6r3DRCN@ zmB1swy~px*PO^a;!b5Xs(9`GFdswEb+Ef~zKuG{Ea4$X8d0bEe;Gv4zyv8{8xTULy zJ<)&lp6XpE*Otg}Y-nmr{*MRn^u3d<(}cdKiN2PhP8Yyqp3y`m+1?f7ekFF?Rib4S zDHHe?G0OyA7p*-2%soHT^qs(!u{q5GdMh!DX^>-xXQC0`=YnyQ55~&?d5$s5qkW4X zD$N|etH9i}Mg?5;H|C5oL7EGc9a8I*5)H{7d?KY6aIYoK(b~mh)I&o8qzm5O=ugjG z{}+FF>d*Egw=FQ!EPEG|w@fr;E<261cc4uI?XbmEgg;b4+Pgx5a0VDHEKb4hm)Bmw z2u096Mk7`OWrbcp#8&e}KPjB7p_<_k=)>lEgB?XScnbiuBzfBf z@T&K?0IybqpxjH3)8-)!KMY0hskW3bQ(IyiJ7!x-If+dk3^DLXCq6=RZUOv+zDH=b zJuW?+^sOp-x&R*YjEb~(=i2kY&mFhB(e$Q>TG`At{HVq3p&o{Jz#WT-v3*BT9swpT_CMM9*0--V{%9}urvzPMJWrZ@Ps%+BUo0wO~ut0#1wXT${U+w-3Ye17M z0qs3w>MXA3%nfl9l-S`LOmEGLu%DNHn}lNnvR*oxA8;_~Jq!vKlqAk_ffA_?$Xa&j zT5r3MdoZ&W85J8)5-=%Y&xfJtJ=K<`gBW3npfsbQaobYuSP6HvBTakcGNJF0n{3}7 zoJRWE${x(+_Nw<|FJ%BWIH$CUM3WuCZB9|#e=%)IE1;)cX17^hyui^i| z{ExnQY34iLk1Fo<3{_j2l%PtR(Mi$Ty>%G63$U}4TRGqa&Pl8$oE#6_WxsY}CdW== zi#k_nUn1Vcw>rPI{>tVXa5+|)AS0d!ojlF`T*evf@9{?I^~iz-xNCJJ2h&XM5hmHw z(1PmtTFOtco7;;VM*<}S9z5Cu+zU1X^4l*x)F;M00Q>;zRRFJh`yUDmeD$7sUsGty zylzJW^7i}@_V|LJM~;a0w`g>P- zWiW{eiR5?!^(0{gj>ek-?94neR~m5V@F+!^yBul+UwUmAdoeJbNg?p$p2_2%Tl?|8 z6A}nX_>fl^4L&Txd=L&83B^7ehLA*bimz38lKu! z0Izp+u{BiyKc3cE0ephiK5go+d*h+z?~`Bjp2I$BMl$pb1M=hfGbAV_a0$*bZ7Bug zsAWRmqqf-o*PllE+S;B9fH!G@jsLHTeOI0t<(E-TndW-O>jnxf#|m5w{6>U{hO{ub zo1?7C)l?NhsB}`JMZ-6hJF))qq;|TgHoTYyv<2J%xSIlc+UjGl77%4Rsj@ek%mDmm zOmH`$g&~@Ii-F5xlicXl99%K=g)cAv)bf|IZ{YluPn9jq2<6O~yqYruASZcFo~1p& zov@?LmD0g(zm_oTH`GKYEyUd*qk&p(cd-BP{8kY(zMmL?uiA0X%MLs_q3?msbEGe&!Jjtz+7~z# z0B^c}ot{8zohmb=l7|JomdPm-6+QZ6&LBVZQexh*GY5fB&|_W<0lsVAh^Y4m%TC$r zfu~}p_n=siuYn#Thyil#O*&rAo2f=(XI&$N)OOT@=3aq29$oD2{ygA5MY;v1(JWd;zDa(X7)i6a;N>wZ-5j)Qw8#ZREW09O zV$EPpiI#DF0iM`I8m+hfwo`Ui2>p{C;yoxvW|%(}c@UZXkskxik0RA@Pyc=B^yqR!7%^w->A7 zN${jNVM_s%0Ny<+lvMXMCK?xj=Ux$snvLf9A&G-0Ue~;@#!H#n(yfJMy>{;QmVf*w z&#(n6bU_)ptjA&E34I^m&WF9?G}71BeVo2wfBW9-uW?&eQkv_pxa(z;G%v2(NE5GY z%#aATr|2+r4*~&8W|5)~3H0O$OaS=8#WVv5$-a4(iotgM5xC=7@JOp8hiWj#O0O&# z*n)}8d#1_8;2+%m8xQ_@@HJA0k$V6?YJv9dvUC)|F)4XxSmuLt^;rJV&=rHqVj1JdO0VFECEuu)lqe z_Rn+QyO2u2rm{-iHLqYb`5h22q%3vJ)c#Ia<^GZNLhX6cEe7 z+NW&`+O=xi1MK#?@jk_61}sZp84cb7yh8md@x0HEo{-aIE;34pdZmP=%rqXG$wxEB zJE8~F0N;Z~v$W~aivV8zYC_+mx82TPoJRWEI-W9szi!r3RRm3A4r@yTU@tbYy|*xP4d-6;dI z<)eGI-~9aG$JX!0U8dF+q@u^Yj9G?YBBs(R1rPbmJmxTw7AF|sOkFQ~^Gn5aCsSNa z8MT?$OJRD98#L$2H3VlxcqOM7-0m&{0JW_EpjU&E86Az;wu;@>0=5#wFJ z8h<_6#YR~qytg))yEGaq$2HLqkrjF}bxDJ-obE?$K?(2)eUI8|dtZMV>1%6x$^hP0 z3xfhn?h(dkGPXmzhi`ZzK=HtLiX2Rhe}Nx6izY zyvQlb@hKfJ`6Rt50w;+k0#F=h!Ex@eBL?0S^5Xb2Go*1mIMRrTu|^DJgxg5t&e0eh z14?58Jm9_s@Lle`$P>l`xcW`b%uX7f`spT%tqe_xMp%yb{=Nwqj*P%S_2HgIO*m3} z;If&}_ed?a|HY?~zP7ff8sL=}t8z_YD#!;Km;&lm`d(&fntUA6LJB}gjFU>s1cW?y zJP@*t^1kljO*3&tu4BwJF;-gt&F(+?%^#WlNhcp!k584-#40Qr#K&u;41nFpI*y2~ z{{P#16F9l9`p)-mwe_a%mRpu(*%r3J#0ske0g?!c1lc0%XV@ zNt2fe`Meiqvhp&?%fLJ!dGQ1icu5EZViWdQ;6P+B7+V%XURq15ms;;K-`_cPs%}+x zOI@YK-EtkN`rf*AmvhhmoZtDM|B5@E9GARhtYcazna8kLS)9wfaSI~i;+reXB&LXq zSIezmw1n3gO#@0*uSL5`w2%%fRuI4w)yi`4E9Tztvg=pi3i`#F(UsZ&AJdjr+%+V? zGZJuQYqzB)V3%chj>B3@Ju%jdD4+<~;~8hi)6} zUTjf38Y4|-lMzmgc1RMEmJnGGrzJ#Kbw8Y7uqOxvNHpG>n)i#zthquW8h0CPl-NBV z2QL}z5&*Bs)3&W9&t%AO!2#fouH4+K#65NIWus*6*6K>#lX0)!SMO~_iCBBlmP$u6 zOfkW&ut?H`VedQmU91d#MBl+%ZuJ$Lk-n~$n;PK5;zUL!B2!mFyC`-ELU;@T5lbM2 zM^Ym$bBV>K9XJII8a|TuZlxkI4JJawIL4F^1jkt2tLzwm-V0v8^O?s!|HwP(_d7{-XEP`#KI@t05!q z8qyCy@vsY+9H=eJ&}VHm`MKL-MBj6_&mg~UM*6z?Z)$*7B?G0HYsF-*z_kgC!(S5@ zC-JdpTrni6c#8}l4W23)S*k zzxu$*XMOZ-lfQrVp%U(2%mrkYLWmBy(*&5#!@QHq37(83<3UTwrj*WU!B5a#G~<~L z=Pr)E?Cs*tlFgsHXxXW@6~L=)o9H*P3z$3=2fk=Hc!82Q&qY=%j}L&?d+L4l-W^e* z5qAwIqLNu&5rFS<@Vd{rMQ;7n7Qm0_+utg~eZM^PJ-=bZTD9L~0Dp&9+P*LIP8{~4 z$_=Czl^B6U(THaxzkq>P5O>SLhXgSQvs6n|DFQWc3fcuTKI>Bg%6eJZ1>oXBdDV(RU~U^j&m$=zB!~ zyjt!auQ+~nrr0TAsRF2D4Oju$35ZLY1SAs(C_H&lR zviyWSZ=yL}LDr3%X_fan1L+&ICt+2JH*ZwH^rl6k zaUzl-hYyQUllTpjEN)ssJdPP9;m4eEHL0HRWBdr@PkO#$IXsy&S`NM_j#K&#z-5>s zfGkpYba4(V2>y$9u_Zjbm6BtdQ{!Zr9uC&4#lR zFL}kvFa7ya_;X*od;8ar;e;{1tYHMZ4Y)^T3`EoMAYN29?M9l66@%$Yk}Z)HT+J|7 zodaeRmJKCmYlTOyFTHg#S|?Tz2QT;fp#dHd4bl9eku~oV-9nGtgO7s(CC#K}?cj+5 zX~2{ODnkRj5=_YA%1*%Lbyi{LYFcY0wPfn$R=*?qo~y;y|7|nU*VT4Y0sP#Nec3!5 zd}(i-%|D9FvUE-6ovCNuSQ7@mlwwDN&plS)2IM0=ozldLXr#2W0517_@Chk4A z%oYKe(*K-{^I}hwP(I*>2#$9N`^Rog{0{W zTZn8z5~3*MxT3TL#C~rjbN=MYe4q)lHF4UH7YcqbUi9TMzzv-h>Z7(O2Ao2)~THh(?p8g0>|?0(`-E1Y3)wiY9Aym;uX( zz5_PcIZoe<^mWzUL;!z?Ivw!JGq+~m(W9iL+YY#&A!bSmF-;{+TT5A7DzGgipweR@V=lmvkOLQ_XLN|*UeHypY zILyWhfky%?AJ3>}Nc{J45~^144Xa@XKZD0pQxf$NgW_5(U{g43P z71225;4*WQ>TuWdZCRB39e^hg$51aX&&dKJJvpFh0v;aVyLB4TcPLVri#8*D z-P>#ufESP*Y}e_qSDD;q0%R#kMFAN^+NdM6;0vz#epVyUIUtYgQy8`qb#RrPpqva0 zOT1B&q|_I?bm3Y)M#A&a`V*>pj&^_1;b#LOiA6 zId}kGS`>-9?0tvh;O(_`0w2+L_@1>H>FeHRlK?!`IdCBJUU@rccBA(dzRnRKD>$2E z%Tpw%2**<57TDBDfV)Dl5EMQR!zLmc3-C9?&nSw^0$e<*Km)(-B`?-C5A0M1+XWdb zl0-wFLc&6s?;MS(Dv^7MqXXCr=yPB{Xk82^AGbk%OxlK)_Xy9}gRB4dJm>ST{mft8 z{MmZ75QFUzrif@=eWpFU1ps+0WYT7%nN5V{Y`N_eJAv2+J+3KfT<6AXgS=$4YTJ(7 znAJlJ*NOH0ps!%gXE}kDlSDKqTLC)Vo8WOn-D|5|r1#YO8U-u8vj7L50Pq;J4)rRQ zgSQb>t-^XZfFIF!sODdF(PpHttK_Btc*Gf!Jo6vdoL#qNb?=_cJBu1_b~-IAt^`Cw zO#P^gP^N`wny6cT1jPG6bzy9Q#L@L~03%Pi8OZR6!HD1GMFV&@1Rb~=P#1{vY4K^Y zC(#goqb!(Xwwc$!sJgH+@Z_m%!>@QHFn z-(gyQ?Q1q8eO*n@GtAv-mthOM-+pj^w$Ho6_m1rFO5?;%DTbNS%`GlfgW?#HnFfdn z&rG0Tj3v|H-p}uOUi^Mlk090*XGN+u6}^MnMYYeuyA?cTnd@BYE}h0pyP=8;yE zk|`u=g)w?oj7l+?L`;LpWE_#AoEfnr@g|x?1*P*M+5L2BRTp|)h9GG?I?>R=`$qm z8oV8?70rmgLlL0wqRmKOSH;Z(@a-DOl4fb@%;utqbaQ92a#Bu-m+03?Ll=Y% z--v1}x^2ztV`?=5KAQ9t{L}NCHbwUK2k2R%i#ZzTBB@82Y#ttxx3B6Ve zxWi&?=^CQ5l@%esVXR|*!$`%PCy!AyWB&A2OO^3$waWNZy)=DQZDPkYOIPjQ`S1;U zW=`DlE2joeG0v8{6 zH!Pp*|Bp*DHrbPaJ?qUIc zg024;;jXC`ZBPfjAC#vGprTKHRil^^cXjb`RnO5gJXcYX5IiH?$%iZ7kzVLY;W%eSV z1G%k<7~k?*A@NFhZ*snuutL&=)!>1OMYgiSFHg(h*(f$cagpjxa(p_f+gGDp2MF@*w$9C_h+`2Ky31AAnU|4xXcw z-sWK9fNN(_z`UX505H!yB3>RRS`1f^M=RfM#2SIe#Dz2S4_i-Xr9L+Sel7cC6)?8o{Vy{FpJO_plQ7Hw(!+~BoisY=^P zVQoz%)s};n5q$@3vGvd2jP!N29lU3_D=*RZAotymn8qXdcHL`c;^IkGkt%R-C5iop z7*3pYDoGRUiZa}`N!};9M;+hKauO#pBmvxr)uKEaDaHy)5CR{MD}Q@P!T2NIF_6yP zC~I@EMwCMfkvmCoEIHqTJUU^Cl(lpaZUBrxbH9P0Fa7bc+YTGUi2;9UFdaCH@=J?Hn{@C2SC%NyC>cm?F=G5fskAaH3b4;Qbw3Aw&ZGOOm5Qt#GXUw(?8 zeaDB4NQM|SS&JyuOnoIx-jPgkNey;CNJA5ylL;b5mm%(boeGG<(-iBtcLz#Lb*9Az z{Uff^0=!yN@FMNTdXvkbci-IS+xN`3Wo#7AP-du#E9|m3c=b|=eKd6rUPkmCyv^3W zVsp~h)^(VkvG!YD;89~L315jbYj*pg{c!Qz5H8+3;d#d=<7~I`dcexp#)1rSO;#^$ z%gRL2Y8LTyT|p%7)q)0ABSpCP6cBF4Ex%N#5HhzJz`(+RH&K6)R z98|-zq@pbz55>Nx0ABKUaqlscn<3&*ub=1ObX3$?YBYoXFtUbEj zuaDt&?_MuE(gFFKQ{?f1K)wNNisyd&uHy9efNw+M7- z(qo##z&dcP4||gBAfd-%hd)91jt{)}z0dohk39EU?xg7TGILBZp@j3-Y%p*U<0rGp z3~j27x)XZ4O84)Ynp`R^EsZy1Hjme@S;b)5$TyOcH+;WY<~^F&-_SPYddy4*@glcjpqFhvS$w;Iw) zNR#ED#RB|@zJs>gTIX*@`nq~KWR2l@fJ|)Vu7g`-&k)- z!VR_?L3jAMPdxddul>M{_g`23;*HaQnr!4TS_l5EH|(lvoaca=NTG#MBMWjlTg&vo zf9f;u+x@YReB+n+!RQ>4Rx2oyWkU|ANx^!p3f#@@E4a3zwOEh)gYTy_;W`l~hWm-(2){ue7NEz9Xhl?*rj)-+Lh2f8=(*3-X7(V`nN}V`nkl-Hd#3^Yul5eyZiq zR>#xUBp_S^mz%i?2mF!pmZ_DG0gSPD1b-lz(=n-Oi_zkVCn+A6%!0|x2<)JeNX8! z)Q+|-EeAi`d&pKmZD}V;(a3b|fo|sNG@|c7&9>&rt}QnceO*O29l%>IUR-t?AfDtSjRxsa5xdLWFD-Zl!mwj8X}}#VtAzBV z#g?Sqiyt_)W8V9g7FARbFPY6Lak3ub0NOlo!+*H90bGH+y+`Veh1H7e{B0-)54(Rv zGt#iiVjqR7S3zssHW;$+>~s&SAFI3ClOom+c4sRv;y4Tor0I!Pm zfV}Oy23KxBbWgk9`>g23+n>$vy~9V4l6l+rdE)Gs%=iNIkKCh4pb{MQ zgoX{h_(Y`T3Sm}TI?8oS7&8q&UO^NToe4_e&R`sE>`JYH;W?8F7Wt%~!osGGOs0^e z^1Z?l7AGw9ph@zzqdOL?yjvck0(bz^$lzTa2ipqZ#lI_VrY)H_S*nidpltl5=cUn% z{l0Qw;q--y0Oe||gxh19kIq>vS0Xb;0lYxT0{n$=@Rsuwu&d5)ySf^7AW7AIWfc3q zvYhFHyV`Tu;>2Cudu*&*=btyCZ@x7yY=1M-*S*oE1b9_WAg?X_C%u~)@z)teLd>njCN#|l|tITWY?e-u*+)3=^6 z=`(EGnZdE4YkN-`(YLof*88?q>1#*VdT+HUjys@tl>zSCy&162mD@9{ZCp8Y$eqKH zecj{k_W`yCcio2kUvcVh`Bj|@{T6Vmz|ysLxFI<;O6yJv0AJuQ(e z=vxj?ZRy(4(tg-_TY$HsnqdKcMBias-(9!4>Fa7~k-&Z10$VuL^8&Vi@L>B*4qWgX z**h_L=&4K``;V|P*tUQGw^lE>QRjaYXIPb6l(bgx(u$y-0#eOar5P?tvZa=p{M4K>u7%5-91GRy-qM8n(l%TiBBJ|CUT?zN!o>T z7Yj;~9b16c^YpPtbab$?c75ICMD2TmzqRiz(dWuw&v{JFRz1%lNwOS#w=FF|4_{MD z3pd#g2k;~M4$%HvkiPEi)K^AjkQUH*a(Y~L!?EqyLz1Hp*1ev^St%-j=L&QHsKdgy zVCB(r!p##_ARB8G{IeA;kIf+EUK;Gf6bBOadJB|Yiy8-*R!*48yDN}#)xmR~>lFH1 zPR`cDP6~Dj??B1cSI@eb^&JkHY%RCaU;Cc6E%S8iPH%&~jiZC5#c+2S(RZ*mKiAot zk-o0Jmd@_mXuF{nxP7lx=vF58*wJ<7Ozt{`4G=p=%y0f;P{TO_d06aRu^eqLaOMZ@ zM)&hG^tk|2(Ibs5e69`~-F0={Xr0b=UcSDj^XX()G#}+kqM&GNm5vg9tsgJpv2~uS zdiJz!{ykVt60-3(mZ^?3rKd*}f~g*sM9 zt00tfKz|L+{YV?J(w^fwoVJ7ArTdJv>T7;QvSh?c)EsRE_WBYrvDPq=E^*3jF8je% zT&>wsehrwuKITRBPzLj>Xph!1YV_`kNcAVYC=O;?Me#0*RFNyLEp>DVvYR`(w#?mj z;NAX7$JJ{@Uz)OqzJxiEU0l0t75cjRimszFbPL?fm5;1{bwu-;z9BpEURwt>yp9b! z(B1Xgx?+&geYwDG9so3JxVmaPOj*0W?fHlw0lUr`$HwayXtENTnqn*EfmTM&uL1oA z>=y~2j^6VPFiXH7=idbk`62ixy&JzV)RL%^DzBDr=uc}N?9 zjfPe6ecL4um>0V5?%5?~|L}Kb48Oh5I;w8&9d$?nRZaX(U&kjqtT;X#3)W{WrNx-}PL@J9;8p1*B8G6k1v_ z_Dcj4m1v2q@8%r@?B>0AKhiw*;Bklc+~-2tmCMgls1!jfiX=IKP_jMtm3ERF!5wf zA~>**5rrSOV&aU6o#INFr~_Pi-b#Z)(Sp|+6dP%62NL;nyZ7oW6!Jf>-Zyzdu=l~) zN%e7Mt*i3@;LaOlYu>l-g0TY8FO?ew=U;KVl3Rx<`E$dvU($3)+QpO%UD+7vzN?YH zo6l0i^N*aNJ#jem{R8HXfQXrP`)+^1UyWSW-5R|NYFwH>Co>+{!EqLr~LfxhK>LlC|$eT$ckzM>(6gb?M5aJ7+gQ2@SYAZ+BRTjiYm z2xNe(+T@D$@1L3UuP@!|f4%hipfbK2jtvm1lER}vrfm~ExDdd|VIT(&XGRra*V%^H zz`Ix109$KNxZlsWUF%v9Wz}Uon71I=Ni-(!D@DfP96356JqX9J)yFF3eJ}9%kG~! z3mp!mw;!~x>&txcz=1BcR)L#6Z$p2(ck#S~2RnDNbU)1Vw*}sb@nE9F1WvsODi@IK zMFFFgqMfaRGxKQ?!jpX_EwV=TW|z@T)gBiUO!aQ$pMkzChO}&?CrS%#-&w$b)QEph zBVZk!?63Ok`qQv=T8qA^1o4%=%(h;wO!H&0(b^vr^9!; zhPsgYk+Bgz&^f%;&Or{^z+G$ox(ScHvg^?_KYxes?c3+wKjZmkkT(v8lPeR!Vre;y zqGB+HAUW~NsDyJA6}^+9NiDJt!BD(=lMa2nEwmp1W@ImFG1#-VKFQikPnDP1iS}Ci%_Hrnkx*( zN~Z}{-7N+Tsrj%)v|lxFd7406W?MX;ear1>Ys55~xS0S5sDZTbve9>HMBl-wye56! zZA8`_@l9N*)(QvZd~uqO)eUJ}c#M$TseE6ftO9j)%p91rQf`CxfzH*|luTdub&hH0 z{m3u9oN}_~$4&gDeumic`*s-Rn#myLjR%v_wqRjwB`j9!WPeS=X1R!Tp+(p(Fp88) zScibO@Gt@}2Kr6AAMP=jHvmstYOoJe9N?b&S_9eOV-a4Co*|6qLK|$18l*blx_~%< zu7}Q=9Nq=g*8se5#XXMQvEuwR%O3y{aF_iFZI%G^VO9v^jJ8ZekR7ZidsY}XGLS!8 zwXxU-T>i*IF1T4e$#}R$IZFwo^Yh>_>OKT=>4I+)#9-gsVf!tnh zxaE$r+XL+Chz`^NbRsI+Uc})$_R^#F?zeH%Q9QaqA(OUzf(EDU+~5}tsQy8&`?+G}beird(xaNbP-}y`Ihy;^RL`$PXlPrbQ;)pj$h@1HnV;K!jtmPRm zn12*HOsCrx)2w)w$e6paU)pxm*6gTix2mZJs4_R$LzA72eCTc*n!0qq!O~se9yN*g zpCJc{$rYJKK?L2Iq!4#0A2vz@rZN+^+4skb_wDMbfB;_YF=Sh=4Z;F$s$;l!jE?{V zIv02#ke7Hy(+xr!<5?}PzW3=4;79ajTyIMH+8$&kYLq>8buN&1Qr{)^?cgeHznPwO z++2_wZuSQH+W~iVKmq%q1MD0C*cBIjKg-rRU)BV&Br)moPAOg9Hg7lJJ-Jt@@=!4G zfMzaD-q%eUJe-2oiN2@jZib@Ne4TncI_J&tMH;f!#A)J)fjx_eShYMKvmBfq{>*C~tpoL|ht^-7=mOj$ptO)>Iwl=t!yPorpX;d&MUkcH~+pgbr zvhv`=voXv&9I!#CD2{18Wf?*G(QKknBDqR=x$31Q%n#4Z2Sjj83X3agR6^iTn9jVD zr$FRcBf9a%B*3vnaf00bxwbtt*NJVqn<$o7ZTF%QIeS^B`HRp!f$kMnZ9xHG*CL}? zV9?3NlP!G!PzPKM?RnksSx>%y26n)WLQ@S#cL`MnEo#TLKv*F$EO)0fM0jQpl0}+{ zp07-meBl_)Dp@^pCHA|;06bqu^xd5F#Sj5n8aZ5tXj++jsTH(_#R=l|X^{7pRRF&V z+}8s36Z^dSF3%T-zWeBy-<+ z&UmsN@2$$__!bn~8IMeLR=iWZiv+0YDVWJAM)u`$>7^uJe;0Dq1GshJ?JUsnL~h*xrwghowdmm{x#44%q{=< zPhNC4K42g@(++tL(N!uICzXves+KjI_g7J;lWv|b6xwCY}1s${3K z8c{u^LC(=HR_8x|>7}{OZ_^!SBx%)=B4N`WZ=*ucorQ##6rnqc(;{G=RiZQzxKn@8 zUQ?$g_$(nqyBF<$?q|LsUjqZHntOZkHGj%^_xmUIEGx)e7Fts+vJz;Hrjc12iKAy9 z2U`RFQg;xKM*Fo^c|_kWL|+D_hK@GyUm8J>uTk=XGEAC*n5Gw1rpXaa&_hI)uB-xh z_k$D3h-(+euH#E5_WAW)q^%RM)3=uzXM@vM?Z$*H@TaHgPPN_G1~2#vn2$|hI;g<` zmxwb+4I_;bgJp$*B2jc5jzW$>o3$uDs4bHjcFx$irxPU9ZcyhUtWc$dh&l|@81X@h zVI&+J>tV8PVYdCman`=d5MMDR$ zN;N&Tg$X2pEg8I#wY`Fm#BSQB}c%y|HGs3a6%I--%( z1v~Nr&C}?FjE7gK((o;tC-MsWowK8v;MMWYnoe{Rp3+pTn3r@+}CFk6ceVMoS zP+MrzKHkf{Tak-`!aA5U)g7Z;a;iE@A#xQ$18mCung{q@SN-*Kr>B1T|6N!#oHwzSu`i@2libc2oMtVtD;Ol{ zMWRAbY$;_{At}M~Hwrbq4;(716ri>ou~jiOSNxhPuUE;GW_-P(r>2JP+Jo-U9GMIf zuDb6vVocfT&!W5E2$Dt-QF{$7fk9EDbD@I(p8z~`XC(*SnR3_%P@^YZcVgnIZ6A7O z{@HqeGqDfJ!OJvjH3whDGYo<=@HI6G2pD-xjXFk~)9%6sM20#7r=MkKRz~#QO7t}= zGSQZQ$PA?XVT{o2+M_Y7GjX3qRJjc)!;x-}Tg}01h#c}rO$X;bFIl_5y<)+AySVmJ z&_W(L8SKKEU@@q`(PYb2cv$5m1>OL5FDnJ(NO4J82iOCS1;CMb9TlaURu&;`318c- z#^%^d7}&NjA}ASN@``hn5_$kRfLBBe%aF$rrx3(VtURJ9jQu16tP9{#0SPStl>+Q> z5wW}E9!9nKy6^qd@3!}C{{=?`@Ewru`Eg@k1hf#lr=aVNwHg~mu*f~UD)B^=^MgY6^EzJqB{3IR<}BD5wQUwIO5(@gK(kp8$a^S zZ-4FY{^SS6tb>oreux~z*!{@*Loy?JGfEAXP4p%(%%r`QOoaC=B26YEVg$a{WzP@b zcX~BI&2N&MtXE8R4Baiwy;d2^NBU-&%(=9t!cNdKc-EE>W7DeAzhp^7zO2Y^@uWQ6 zsXyRblx9G^=Kpy0AHK}+wnlP!>}B@=-zp~_DiMd7y9NHxn0W@e zB5pI;rD&_9t$4+jH>=iHuh2&H-K_MjFoDyr>D(X?NhY8$6Sqh!o+NA_mSgrVlxGcB z-|6f4hV=Ip!2J+o@c`%S_q=@wQ;xJbcY*tfQc#bgU;?gvg^pesF9s`dJuC)ODClG4 z(sbtX7@QB>jt787mHU~RrPO;QEQTfxK__min?WPxg`k%hK=Pc0|OGv*^VNgGW z@zPmlvWG7eTSa$ zbB%{5tzUrQmfE~&oY!!0K!#MHhOFA6KDwJ$&2$rg|MTy<{)TJc`CUCvXO1-!yWyyN ze%bIBY3&)?J*C;0J7Xj-hFBpjLTA+gYk$-xSZ=~@F-9?wgbz!=tvMR?*W;D(w`O=Q_X|?Z+P7s%445AK%q z1ATsB$0>Z{=?X$8&GZ)Nv4oVAI15lpVoZ~H%ODJT;(7u{FI6E@B^W%@Vbpnu3j=T_ zX|sOKsq#b@;ME}Jxfd`osgLP$$&$U!)Pd-G;?qz1vp4_ipZhnz|66ap_xMg}jSzmr ztorypVRo1VtSthGphzVSM567H^hGjf6Xi#%4y{6vt8gj&g8_PWJ4H)}{?etn6rV0H zEIthE!+e00^aQ+?gpZnNpu4^%Nr(v#J%bf6>^$#;l%AP^7JTtH|MNA!u>Yst92OoV z9Z0+Ag)bJ}HD=T?b7iSEcT!7Zfjk4vh`5j*vIqhQ(Eqb=*5aNwARGkXPUY#n9`wC- zMBgn!Ulz2COATZoi7_jHm!75WRkEN+zmIBJ8etY(b*i)5=^^_a57q81h-c))WUOXR z>@$u^GJ%xLU1Aw=?gICyf>TEWOM+4vdtf?w9OCAIToNarMDB;wAu%9NHQ?(0%7eky;Dznr!Rbc_rsF2suRdE z8!i)$-On085MwZ|C4(JF2qy2qUJ_7v=M4m+{F@J?ZUleA#b&{O&is>*wG7gP&@}SSFc9 z7e9*n1J_JiSQgy|+EI+C+ndf#i}GR;rr6s{f-m{AySFj??|xaem#4c?>{?V;4MkLB zSxoFSJDq?gpo{Zxaq*f-sH4AQX0iC2|N9?*?=3g|!ArfMYSsgthv0+4NW5UWt1{}5 z{W6-Vf=#gp?)<_NA7Zs3J!_hv z5+ImY)efAH_$bJq** zdhe&6dVLydPAGt>9mM`)6+Y?x%e4IoIs^i*NHI1(VYYwTmWk(H(Gi=8>x1 zP;}=yt$NJ0eRTia1GjvY)uW=DL3h?WswCTlwHfCN;prCQt?X}9(e#ni3pb7ED;(Ma z^zC1nhZq;I#?FGKYQKcx7bA0VF7rw&-sG;^vV-&&l|F#C2j!*OC~Y$iOiAM!zrKLA z3<^~r?!D&Y=_Ae^a0lSvu%f~*zW6;)pB`JIO08G#WWto7J zF~Y#P00!1p@)G=IO>64c%OR=0l3r+Ri*L_@?PJT8?Ng_xX2<7eXSY3g&E({f8>6wW z?qG0h{Pq@MOq+9;8KNAVG)RXX; z0nnJ$fXD(MZ!RN1Q5bB?Y7(&Vg1GKr&K&{n;uI_Z-1FeA_uh1K_Zd#6WGrcNIA-oQ z$RDcAQ!Ey0OPiR~wmFGOSUPsqEw}ua{Xfs==Uz3lQa$_Bql+syeErPQuKCkTvu95) zZ(ClhvksB?QL`{I8f|-uW+Dm!D%Y*8rMWrDoa_D=a5rBp-KWac^4#RYFrZr z+c`OT|Fxxw&+qYrdal;GK=j~ofVwABe6d-1x^rmQVsKrz7yVraKzj9OpL)jqs->nn z(U1RWD&?3g=}<=KT_n$b~Byt4*JJFK5GEHiFJNthrfF~!ZY=BFcFTSJ+js^LAf9! zF(fc8!?_FG-~HOR{Lqv3zU}8gEQ>Vewxd60kSjcdEJhgQTBr!zHTF9U7+;4eMt9M7 zlf{Ln-YYK7a%pgL1*rsD8P4>nUuXWGsHA=^PUZ7tXPkhzmrKayG25Of;k%O0nV`uKi!FOfsD7lAO#7QYC%bE!_P;XcA!!zUK zvv=+tpY`@$Gx%dJ?-i&p46qB}^-iKYFl#3D3#J&=EtuBROtFl^~WK1AtsptCKlMp z5K9Sb8CJb;?kE)z%e?#4k`S$d(1V*{2B_8Bft z7ptv(@A$*t@I{P!V9abs5Qm1M8^1DSv1MHP8&OsU+?V0t!_rtbftpgD#!&^RMm|qe z0JIE}KWJGmNb=GzAK$r@RBw7X9Q*1{^BnQ2fQ!KDg1LB8YyLQZS37AUlzNr;VaDM| zzDirAIGXDn+Q2TFo0hx?bT>jn(Y%lD3upFz86^sUF8AM9CO$}J@cV^^ES(X-DUt>v zcdU7hsL^X8r;~y@OTY5y&czXZH`u`*L;5lxvFkvsnIQv*u1H^*p#8Mo0-DFi$Q0Sg z=9}~0+?laVQB~)TU?zzdtIXXEz_YxC@e@$klw=amC{Wlmf&EsdW^pY!iJf}^8G1R$ zTGgng^lbo+;YVq69<6yBuz^tt?=y< zA3H?p~)08)cV@23ex>dO4K z=e_yuu_$?(JK8&%yUAR%r(6uq-Q37=?Hz#E(aih81JAyTtRoo8z?Bo6rzN{0w#3lD z+6ZV@-y@AV0~c)(z%G$#j5lzI5p5EV=xZ9c@6RTquVdUad28Yp-4 z)>Cm-3HM}6jkby8fdiR$Fb@+Q2dBf{6TO?gN4hZ?k|!zi+i>6MgOl`_q%%^ZS`^Bt*r)ieqifZ{(Qb(lp-M1K-b4P-Ll&{ z`?UN2?u)lPEKZKN#aO}=65^@T5iptqxrrx{X3X?naXgKxr%=Rj5g@vZg0iUN$m?5K ziOHa5miSV#q6JT4B>A7d_gg>tl0EOV0MBKeiAC?HcfQy~@8~{yPxY|;*1f;BI#0!I z?YE2j*U{T;9o^TXxjXM-)*{~?Is2R}8EgL5hhFem(H%!$qHi`aVUxiOhMWRulsF&d4r&4dBl6|FgpSK$Yt9QnlNW!x_PgeEHL+u;enC-2rDke0p{NFP2VtT~k zNW+$V@lnUY;wj#F23MaiaE2b?5R8IJx4cq|dp9?j-PXOY``vxN7+W_WbT7xmq;v13AzCi4lbs+i?r;{a<95N}pn*;srki&mf-F3l~1!*RYBEH+|P+@*L|ZflC(Sw-9R_RR^G z;g5SyN9+7|J>Gr46jgDcClJhut(G48_PZWBJ6okcVDaF`#2S-}h7r*)S`$RIDBjGP z9@{TJBPofD1aLA(h7*=p%EifVe&{*bzSWEunXl1DoTbb1=FZp+!k7K$N=r-%v9c$z0$?o&)xD$^mz$3qebJpeZ+ZJm%nL~o>ZtCxyi3{nFK#Pty}RWn zHjP%-_UzvIg%|&=0%+iTf`(kcjq^Gf0Lh7!M!agoZULbv6#oeho~av>QqZO&&xL~* zOhDgE-3|FXITnbxl(vY;0?4x}uy*cd-PgXEW_u^Q zl00=(E^Z5GVCy4_jdyQ-=E3j#_+4MS`6L`95*3te40Jckv-(O`8cW#}4ot0TYQVif z79?{uP7%@RQdXOrObg5WNdJKkNQf>`y6S_b^+Z^|vE6%!O4XvIK&%ShP?;i?R(zJ=D5jVG_h2{00&iVg-CjY$xVC`N~3J0Ct+F> zBM->&6a7lY$Fk9`v}gBs0yog{N_^Xge)QewR#}X*Ovkec!F$ZKOjOz;vALQowvu#t zEF=)owlrQOfd)%CHBT{_1@H@Ag$!0fk+|@+3T)jHH$qechlkSBf_xUmfBA){9((<< z7yjeq^?&awfIl=V>@pjdtII@CLN$kymH2szW`Kw$#3PYeVMJfoq?doYLHc?T zaj*$xO7x5*EVR;w9R0KigOt{1;#Op}#ot zjAQ(Q!8?YISg@1$8`iCa6~$5-PE9H{mS*|%^cZ?pn7h&$wC$67GUdi?VhmM@wxtW< zN*a#iAe8_P7+n=5qQTYzn;Zo$iP5aL{>R_{*t`DSzx?2{3>QDm6HoDxBagscD>iLM z<>I!0CU*DHq%QhX(#7jf{>=Md{Q)?3j46mK!F0^Dtk_~&Q!|6J3d2e!!pjjS78v#- zaU`*vZZ6^ViW<+jG#DxD#!$D}JNRB5_eLq#)Zy(r6%_ zNsV~MJAM!5?T>cl?`0AEd`R2ux{K#2MiHq0Aaq(_zP8|Iis0BoG6*scxz6sPfW0-h(OsGQUi9ND6AsH%|NHNsXX=@dcKu#ShD zhgVBTB0Hk5YuC%N9g4mz&KO|@zmoW^#T4agG9Kg4FfHqumKDzwYuLZ<>&V}+lrp2R z58!)WF=;O2^Fq2ZoEuB65@{@mtc4JWu=sQfOneoq!xaI%DoXFmo-lghNP9wh(HPu&3xtmi_dYwZJ)QUVq~#NB z7~a+Nrcd3RHdhY&Q9O~YOkqyRvXA8%AuiIS$dVsHA5#kk)Wj8HA_;%~y|4enxBc{; z-{h6gjC(t%h^(fhR99DWT>J>!X^aajqP<3@o7gp(+H8Ve{n+!4y#9T!L8*?{A)GtS zP0O=#!$>kCbUn7PNhK;Kjm5KRSthXhAzk+4;x;1O*RhVkR04h;hxeGsvm^RmAgPAB zd|>*bWL4zy0>p#?W0Tex*Y)K`v#IeZV?8tJIrB(3`L_IBbvq}(TcMvt>ome!MtOwg zO2cXoB@WeeYzB97uGvl&ue*=Ra4q1#UDVOOI5;IMZ%1A{bL8cJ|Hi+5)kknTZxP6? z1xp^IJVQqs&#adbk%gHiQHgjd6?6IH^ znyko!nlh;=A}Y2LBl@-*_A+jUq;H|nN*Ybp$u|)vyxcCtV?^?ONb2vCMV$R;?3Hec(smdFXc#k~L$% z9V=E^8x9`%yBXwlvVwb!1h=*}_x=zvI%fGj*vOs`xGyp2?_j%+@2JyD0{-Ku+AJ3z zvb4uxMj(%`6q0yUa(J{dmv}_)^}>JuH~;cOAKZP{ZPVX~sAi5jIWdiBFE|;2yd~5y zf8F5Ms!aedWwzQ_t2fHWKky5``@#Fa@i-Ge6CB4iwSePl9A{FEHL$Gknn_wICl!>b z;_ynyRHMpt7S|U1<`^7&bH%Fz5J3pY!|szKi%{Mp`r6jL41do{-=F`z_34`^Cb?on z^>FXUwgK)Md0I`YfFE*jvR}^2!OO5+y{m@oj>OEQBEn131YuS7c4Rd6MjF%(;73YU z*S3Ri&jaq+>FZAZ>^t6i=<7>U%fyao5)X*h2H-QSDd6Ce2KrT$Y`$McQ#zK^8>MtA zDW}IL>)EywwM^h{{`ayLkQQZu0hUvnn6Af>V!>JYJtpH)DTP&fNkU`~r1t#dZnY zl}%tLAIkBM|JZxq@R3)21a7^_I;}ma6T20Q$R_GB?Alnlm7#WQ0H933xCH`tqjzQ8 zHvMT-D)r*HS7<(hU)zktcccc7WH-Mt`a1FJh`yE}gZ@1aeNTK+^!=d^uSZ{!dLV{t zF`WnLl##u6xp!}uoSXJ#NM?GtcUAG69DG=u$YL_fD>-w6gvOXi8fh$DNz(T&z)KFR z0q#J4#KEg=H(mtnd2m-}kXS~X`))qSU$cd47JlvRzxPX@c<4zF;jF0iqh^o;xNz@i zQqw|AlJ#yUkck}lT&lw0tpMVCZv))5jI*$-r0QZ1%O@sL(iF2faSko?V*kW0MXrj@*qC~ zdKzY(;^;1L&x>Wmx!+{3S-9)_@7(`Cf8`H}4$?ph2kn{IiT~+mr4}}>t#SyVk*Q_z z3ObcyB5+@tZKP%5gBOWT(~M`ZsY?7w`qfH-$$#-Iq!h=`#9gA_Y?Qu^c@vFC^tHq} z-{1AfPJq|B zGP6qSDD2_j=^rL$xU&BEgt{>tytsGA!MRyp9br_?-2#pn+Uldh?Y3Gp$!g+yKFHp) zhp#>TYwvv9FTDT3ryNz_8002|gZ7%}%aF;mhxobA`@4ZR?*Fk`AHLxNbD{U;QF6E zdHwu<_=7+CkH7sRul6KVz)J|~{htMvi=9bReszN~omIQFyC znZH!c{Be>rr^szl<-jtaaU+WeF=b)m8Mb_+WD^H35&ejRrx6AyZeR=C^$|6s?vS<2 z6Zo)9Pwsv4@t=9mZ~yWq=bwC{4ekwzX2iv@SZAh15}!#9i%oNka}%PO$p#^5;K=8( ze^)0ka`$!sBD0L+)*iv-Zr^(#6UZ;k)Ux9xxOmJam+*2LizbLwRrLu{=GTaO;Q3>K zJo&r6l#8z1?J|Nh5sdfQ9ibg*>Or=M&8*sb9-yHy_5;@sIu3dtdRPU;HBte!@lKh+;YSRx4X!eh?MJb^^?qOFxNPF$~#-Cuqeb(vQ^FqWC5___C%;kOf1B` zV=j%|v;bZlyZ~Mtyj!ZXJ1T3mfa?I-3e?59Y3sPR9qix4hf=-!+ducCA9%}K9<1(M zFta*L(doG=*@28n zpKrILa3$5(iQ6!vywB+LN`wJ<8HVN>TjG9r%_)Bu9&HlY;Jgg=L3cbLn0T1t1@iyA zaNWrl{ps7^{O)i2wbx$#%y)htE)@1%+`DP=7tA=o*H#|~^ag_Xex(-I-rDNp=&gN? zKCRYd0x656>-lJcKRHqV_(y;7ci!+%ulk6V?S}S%J3(adGK7OC43q))4DpIzh|9$N z)2gjjY9$?T-`>ouN$g=BaU1>*opq^z-Avs;DNN6f??j|lVMw8%umxCY4 z;QJ`w{@ULG^j+ZR08kbaQm9GZehnWD^rav>_D?_Y=Rf?ZH@~AEN2H~L?P8#J!5x6d zEE0oNmg<#OT8ZOkHE45ijdXTbqiyq!XvUaNbvprfw?*|}Id}V^{a!~@!|JSbv@NPZ z#Jj?hzt|%1LBu*A8Ll5h2@Di{{d?$OKZRubR}g zYD3skzvp>Je&ep6|F3`Z@Y9abxHLSpj!Es{!@&dY%_iose#_9l5)&_y2qMjuW>P02 z=#=5ycc;f5z9%a;q*NE()%X-FsOI-n(z&Dd2y*&?==;(UeN8iV7$nizeH?AIJr8{w zUwqz?xBTOU(6^p88#kwuyY8@I3B3KDcMxW6ox<>hsbdlD9rSz#x_|$S7fd#fhFDdE zg~gR9ERfALW0aSRM6AO9#mh>7e2HL1r61V&*&BcMyMORqMnrQjPdLd(92>GFb*(G5 zxIuib7j(qeIU>Bi=R1Hn_B{6{aHph;*PZ%{yMN`azkdIBeNLv3c%>kYA!vvJc(^z6 z=^1cuL@gqNNvRr-$Hll6HR0eU4F_jLGwn_=cVxb6A8YE3cAR2pt_L1G#L)u+d1!w) zsPDRsKyycf)60wDWI(d@=5iQTn-S)A2<8?GemDWL6Yho>g#dk#04D_$GrSGj8b2%E zTz+WVTc7rSzh>9X@BUtve}XcPgNWt6c_Uf&P){M_k3=juo2`c3z^SjDv4 zId0~D*Eaug^wyvkE9HP)T)JfKn#?t^Q_`hfXYTsazkB=ZzWBp`hXhPBYRgUPn69;m zK*7|Q1NSnDm)e;mnJC7SOOweGde+G%6Z@VjI+xWJQ%c3TbYjvU(bu+j&tFHso`bnB z?)N-xwSS4|I|k!M`n7fGJLlb+&N+jL_Nu&J_U>w*IMa`#h0IQ@A+PZ#MI7}Vu6Q?3CQbf#1IJV9nh>Vz#?XR9;GT0y4B#WiQP_<2f5LaBl}RO z-(gl&TH=rBOQX2*n9x_NZ|J-ReJ!}JPhYLd_d;8vSVc5*uF~e<8S0Gq{jc-_adBr$ z+k$P4@vuBy2vM{~E0Gv?xcDqCz{nQ}gHR$WP4P8Xef;Lv-}b|QW*(rG;KO{hRoBcn z8egyIverO%gMB^jP29T*)<_GFzUbq>bNt8O`To;SImT>YJQNi+OoQEU>~L<{M<#-| zBH*4dyN74-XcOn&2KUlbG9BCH`P|;z3@`aydkH5{w-2>{S@u&)4(TW$FD`z5uLq~+ z`DaV_1#?k3SP2UO_VHmu>DCP=PlDc{5YIM2t?K4J?3$wQk5$RjT6J)A z)>gE3z%FiGt2BvXG?|NIZ5l_D`QZ^F1Fv@Zk45_YiuRt*!T8rjb zMj2}vBF3>=E5s7Z#9_;V`(gvp%v3U&6UkX$hxV$?Jns;H5173XecJ>$*Jdlx_dMLY zUef@)*(1S1wK~rMOaM&3*yk?y6$Aczwx)R-@M@JvBOU- zj^BMF8e6qgw+P(TceK@~pQ~T5PhCpD@29u>uB~eCK5im+pzhA&6jNH$)oS%wU;OII zvk$-Zk?*_npKI4GFsPJs$wYJ%{SEEClIa7Yxty4!NarU4LdsgEghCb0j+oEo`A1U+ z?lug?S>&?TkJ&+&K*OIAcRpw}`?M0?9vJhec4MX3xMXv4I zuiEqgytuf1-W|cbwOV~ z>|0med32!L0{9cA&FfCBG;cY2tntjd-@p8v&;I$@ryWz8J(Gz^gIoigcp9>~uh zxhH7sEc%XjsYNwAtGj`X=!aeOI;6#tv*E`u4BN z`(L07@O_Hb9W2owm6#@842d*oI5!`l*YX_31I{dhn@@}5;Y#^W=MKHT5L9O~{G~P3 zNo}iKAc`t17RRXcG&SlgMUm?JE3#F;%Yv|#7BEOINioL12!Fz+(s z|7PiyuPuc6K!_VzVG{F( z?Y5v=d`9JKGq)BW*?wJga&|5}NlKDt5%oifh`4YTwG!!O$U&Hkfpo5yx&*(8-nC>L zS$Rfs!|@vR$4g|};(W6G%)Rl|C%;&qTe!P+!vfZ4P)Nq5Aup#m%ov&S;$%dJ+BAT? z5(VN~lL747W_XvSty)qnnoz!EWhy4x?M~_v$0eyENLZP>bz)n^xp%B8EbV(mafj#? zR;k{Yi_g@F#tu@8e+2f&;!B*p9SR6zCT5BjpAVU?e7+6a`r~vo~ z2gxs>L9GEzRzJh7ku8LQOrR>(>4d@{i90B-7GzXd(7yyD@Uq+zDGC}!_b9n`8j*rV zGGhvqXKpj`0t3;o2kDPFGS4oh^R2K+s-TEZP*BD%eiXRVW3Zby`-+w%us2)w)jv2HhJYp^qKY~TF4LxesGqxW9G3$&z1OSoZ5eG(#nL zCLttI2uTJ6Hy1OAi@3tb)u@ag{&SwD7M*m{kz@#FRdI_I;<79;{e;?cD} z;k!AeNtPD{miPrkFkmmI0)n=IdGP75ugal{b|WEd2(p<9gMI~QE10N|3LK^YLvm!9 zBzTxCCT1tJG$(+VOd%1+%waNR#WL8$c9!QnmC~u>hIaH4TtcPup03vVViMbWOemX2 zJ*E9y2`96uN|@FC#UPYRd_e4RW_%DDyb41W9t1j2&rm9%R>%s4e1aY6~tKV5BQ4i;z71%GS|a-8oB`k z2&$xN-bZiV41*$sFoiHBB_4B@3F(2y2vMCm8>Ht~HB^ZeTrE{{bR}|V6{_$8#UwL{ zRvJuamhR0)Go3~xb2>UfMy}bmhpL>IoGGNUmN~SpGTqdhzMz&I(btf%_s2mbM~j+D&K0F41~^`xov%yb$xHlu-*ty2eZ{{{2F4LJkXa` zssYKK(I~>30`PIrKo|qgHw5@Cv^gYmXRAJ8_6f3r_a^;=e!{k3z?<)MB$*uQyzYAm z*g1ZgN}k?`C!sZfj*pkTwtDCd{fWQHiWa*wX;lo}8;1Ux(4(xFwo0tr*jG~QD65b_ z3t1yGCu zfxhnTIwBg){Fdm4h)#AOJ8}e*_L;+eX?rD`K7~XPU+SgV)86E1d{l5$snE&E`RG-G zZCN9#XA%6l7UxX@qG=F=?Lm<{rf_Bn@kq5m^x~3NP-Z9zGq!wzi<3T?LM~%gz9qyI zikHu}*-$if$)GZfn3>N2cHl_jWwIm_@wJ+Tr4rmcviWR0tuw%b#u+48V;sY124Ghn z)zf1SWs2vnkiUuwX#$6}MKazQDN}jA8Q=#@Yg+EjROy0Sx6>a)Df)&&8mJ(*Z9B|N zdh;ulv@uyEkkwSym?1D(Sj_tN3b{i8cGAxInG(IzSG=GkuGI9MEN%KlnITe)p;23DZ;xEszbr3o!$Rw3 z(iYD^O+UmXL_m&xC$mu2YA`5DXQgVx(A{M0;g#j!;^=OiT$~xC+5;L5(gGv;_O{lQMql@K z9S&Xx48W_Kv^loo5%=8Av)saySp=y;%I`6NoVRot3Dy;?KeTA6VjH;8=6mesRg zYH#J6pB0oc3}H0KwcUzE-o2fA%OjH_j!j$lTLWuml9nqKhosQPBe>4Kmg6uB4D(9L z3{$ZtRV>}l2zKkZo<8yOw+PliqzFyI}bD9uBQ$@!0lD%|7-e$JFT;%cieoh z*k<=q2UDQNsM}3`hL`aro|q53)HPEZkyipfrg#B~xj%GM_EYPNx#ZXrkMCIQ!NY#-7um$M(qg zn^+8CBh6}ndcZa;j2oKQvWb+rA(IFuswCOMG8vb~Vxv4QZ6j_Nd2^3#zu82Aw1L9z z@C57~am*#8`-r|*CVhK2cy(YyXfudm4Jg)>>U^9U>ap6KACEC$8@@l$SnLkq7VBu- zv^9uzKMiCPAGMt&UdOgnk$b)IvA4C>R}EnI4FmDaTUu%#V@p;nfx5W0kQguCnKQ)r zAy;NUsG9-4hXD>)AGycNbL>69e&B!v7N%SdV7E`Qe|r>kYXbcp)NG$8+xhygdn`kc zAJLb{8CMv6*9Lg?+%-V{+=G_^T0Lb3uXojUPl4Nw)$4@IjSEktd1;Q_Da4U#gL8;WV6mNt2SBS&j6~lA~ z_wEdra)U;SJtFAF_H_*v)Q~i?}cOA4WKK^S8@DmBYvp>=wIOm z=cYL)^oU|E&q_`2HKMOt!IrH~-&Fvw*E;ud&2eenCHWJ%rq<@vbiHJ-+G_A>s{yR- zoJl94J$|d7&J9|7Y%BZ~Q1{nU16f6b+5ILi&FJA)gm2oHs&<#bzO0Y zLEh!qA8TM&jq;@j2=s&A82ycQ5As1#Z}uhTv&@0*GfEREai7kUILF&#L0{jyeP^EE zuht<`*6Ks&ElerSF;mF9LT!USDzp*U4RVxXYy>e@6bQfh$KTr3llW8NAw2*<=11?#=FP{lDVC zJ8yBcv*$Sw*O5|tU|TR>YjEqVc6b5pt^#!(yD&wX?r^Eg+F)>qesI7%y0_nS0pHdO zF8|CutNPCqy_;bij+(yIssA3kr&U1b*k#yRkJfth#^|jxt>{I2UAelx9qOCNb!>Cd zd_-T-_F~(+qygD8G&`a9L|4fb7(qr zhdC}~nb`r%_wLBk0ZD?(RKH$|?|hjhw8KAW>2b!n}7_6}|xQF(mPdqiJL_3q!zu>#u= zz;`R%-CGm%yFc4KH-P((U#Z_$f%fVbz1)^Bt81|Nj%>-kv;kUg?I@mUd+}U v>|R3y{9un6ojq!SQ45S(VAKMay9NFiOI-T-iei+?00000NkvXXu0mjfJRjHe 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 1683/6909] 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 1684/6909] 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 1685/6909] 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 1686/6909] 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 1687/6909] 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 1688/6909] 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 2feaf2c74a9e6ccdff263a597157abf483180229 Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Fri, 12 Jun 2020 19:17:52 +0200 Subject: [PATCH 1689/6909] added music during pause --- osu.Game/Screens/Play/Player.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 83991ad027..e37cf9a348 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -78,6 +78,8 @@ namespace osu.Game.Screens.Play private IAPIProvider api { get; set; } private SampleChannel sampleRestart; + + private SampleChannel samplePause; public BreakOverlay BreakOverlay; @@ -161,6 +163,9 @@ namespace osu.Game.Screens.Play return; sampleRestart = audio.Samples.Get(@"Gameplay/restart"); + + samplePause = audio.Samples.Get(@"Gameplay/pause-loop"); + samplePause.Looping = true; mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); @@ -407,7 +412,11 @@ namespace osu.Game.Screens.Play if (canPause) Pause(); else + { + samplePause?.Stop(); + Logger.LogPrint(@"_______sample stopped in performUserRequestedExit"); this.Exit(); + } } /// @@ -416,6 +425,8 @@ namespace osu.Game.Screens.Play /// public void Restart() { + Logger.LogPrint(@"_______sample stopped in Restart"); + samplePause?.Stop(); sampleRestart?.Play(); RestartRequested?.Invoke(); @@ -564,6 +575,8 @@ namespace osu.Game.Screens.Play GameplayClockContainer.Stop(); PauseOverlay.Show(); lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; + + samplePause?.Play(); } public void Resume() @@ -583,6 +596,8 @@ namespace osu.Game.Screens.Play { GameplayClockContainer.Start(); IsResuming = false; + Logger.LogPrint(@"_______sample stopped in Resume"); + samplePause?.Stop(); } } From aa476835e7b657b414093fb3dcf6b19c3d935d9d Mon Sep 17 00:00:00 2001 From: mcendu Date: Sat, 13 Jun 2020 11:31:34 +0800 Subject: [PATCH 1690/6909] 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 1691/6909] 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 1692/6909] 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 1693/6909] 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 6fd8548f79a772d90b08cefd4e508d32d92d3c5f Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Sat, 13 Jun 2020 10:13:41 +0200 Subject: [PATCH 1694/6909] no longer crash if the restart sample isn't found --- 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 e37cf9a348..4025bbd442 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Play sampleRestart = audio.Samples.Get(@"Gameplay/restart"); samplePause = audio.Samples.Get(@"Gameplay/pause-loop"); - samplePause.Looping = true; + if(samplePause != null) { samplePause.Looping = true; } mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); From 8b8f2dfda2ebfad979ce7fa148d824a80db7b418 Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Sat, 13 Jun 2020 10:31:54 +0200 Subject: [PATCH 1695/6909] Removed duplicate samplepause.stop() calls, removed test lines Since restart() always call perform immediate exit when the function lead to a restart, there is no need to stop the pause sample in restart --- osu.Game/Screens/Play/Player.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4025bbd442..f9e18db581 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -414,7 +414,6 @@ namespace osu.Game.Screens.Play else { samplePause?.Stop(); - Logger.LogPrint(@"_______sample stopped in performUserRequestedExit"); this.Exit(); } } @@ -425,8 +424,6 @@ namespace osu.Game.Screens.Play ///
    public void Restart() { - Logger.LogPrint(@"_______sample stopped in Restart"); - samplePause?.Stop(); sampleRestart?.Play(); RestartRequested?.Invoke(); @@ -596,7 +593,7 @@ namespace osu.Game.Screens.Play { GameplayClockContainer.Start(); IsResuming = false; - Logger.LogPrint(@"_______sample stopped in Resume"); + samplePause?.Stop(); } } From 794b8673e21b7ccc60e7bac938426c48e3a6abc9 Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Sat, 13 Jun 2020 10:56:02 +0200 Subject: [PATCH 1696/6909] formated using dotnet format --- 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 f9e18db581..ce7bb60048 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Play private IAPIProvider api { get; set; } private SampleChannel sampleRestart; - + private SampleChannel samplePause; public BreakOverlay BreakOverlay; @@ -163,9 +163,9 @@ namespace osu.Game.Screens.Play return; sampleRestart = audio.Samples.Get(@"Gameplay/restart"); - + samplePause = audio.Samples.Get(@"Gameplay/pause-loop"); - if(samplePause != null) { samplePause.Looping = true; } + if (samplePause != null) { samplePause.Looping = true; } mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); From c490dba7b3e0e22202ab861e0132a5779d818f0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Jun 2020 18:18:46 +0900 Subject: [PATCH 1697/6909] 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 1698/6909] 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 1699/6909] 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 1700/6909] 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 1701/6909] 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 1702/6909] 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 1703/6909] 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 1704/6909] 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 1705/6909] 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 04c1efe298681c82da4b2342b9d0bb78e432d2ff Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Sat, 13 Jun 2020 14:33:55 +0200 Subject: [PATCH 1706/6909] resolved issues with inspect code script --- osu.Game/Screens/Play/Player.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ce7bb60048..d5e9c54e04 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -165,7 +165,8 @@ namespace osu.Game.Screens.Play sampleRestart = audio.Samples.Get(@"Gameplay/restart"); samplePause = audio.Samples.Get(@"Gameplay/pause-loop"); - if (samplePause != null) { samplePause.Looping = true; } + if (samplePause != null) + samplePause.Looping = true; mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); 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 1707/6909] 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 1708/6909] 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 1709/6909] 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 1710/6909] 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 1711/6909] 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 1712/6909] 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 1713/6909] 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 1714/6909] 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 1715/6909] 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 1716/6909] 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 1717/6909] 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 1718/6909] 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 1719/6909] 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 1720/6909] 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 1721/6909] 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 1722/6909] 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 1723/6909] 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 1724/6909] 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 1725/6909] 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 1726/6909] 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 1727/6909] 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 1728/6909] 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 1729/6909] 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 1730/6909] 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 1731/6909] 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 1732/6909] 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 1733/6909] 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 1734/6909] 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 1735/6909] 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 1736/6909] 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 1737/6909] 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 1738/6909] 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 1739/6909] 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 1740/6909] 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 1741/6909] 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 1742/6909] 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 1743/6909] 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 1744/6909] 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 1745/6909] 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 1746/6909] 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 1747/6909] 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 1748/6909] 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 1749/6909] 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 1750/6909] 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 1751/6909] 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 1752/6909] 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 1753/6909] 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 1754/6909] 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 1755/6909] 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 1756/6909] 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 1757/6909] 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 1758/6909] 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 1759/6909] 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 1760/6909] 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 1761/6909] 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 1762/6909] 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 1763/6909] 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 1764/6909] 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 1765/6909] 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 1766/6909] 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 1767/6909] 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 1768/6909] 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 1769/6909] 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 29ae1c460aa9564c6fc04ffa0d001d1139c42c43 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 16 Jun 2020 17:00:20 +0200 Subject: [PATCH 1770/6909] TournamentStorage now takes in a parent storage --- osu.Game.Tournament/IO/TournamentStorage.cs | 28 ++++++++++----------- osu.Game.Tournament/TournamentGameBase.cs | 8 +++--- 2 files changed, 17 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 05ee7a3618..d6e95bc4b6 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -13,18 +13,18 @@ namespace osu.Game.Tournament.IO { public class TournamentStorage : WrappedStorage { - private readonly GameHost host; + private readonly Storage storage; internal readonly TournamentVideoResourceStore VideoStore; internal readonly Storage ConfigurationStorage; private const string default_tournament = "default"; private const string config_directory = "config"; - public TournamentStorage(GameHost host) - : base(host.Storage.GetStorageForDirectory("tournaments"), string.Empty) + public TournamentStorage(Storage storage) + : base(storage.GetStorageForDirectory("tournaments"), string.Empty) { - this.host = host; + this.storage = storage; - TournamentStorageManager storageConfig = new TournamentStorageManager(host.Storage); + TournamentStorageManager storageConfig = new TournamentStorageManager(storage); var currentTournament = storageConfig.Get(StorageConfig.CurrentTournament); @@ -48,7 +48,7 @@ namespace osu.Game.Tournament.IO internal void Migrate() { - var source = new DirectoryInfo(host.Storage.GetFullPath("tournament")); + var source = new DirectoryInfo(storage.GetFullPath("tournament")); var destination = new DirectoryInfo(GetFullPath(default_tournament)); var cfgDestination = new DirectoryInfo(GetFullPath(default_tournament + Path.DirectorySeparatorChar + config_directory)); @@ -58,31 +58,31 @@ namespace osu.Game.Tournament.IO if (!cfgDestination.Exists) destination.CreateSubdirectory(config_directory); - if (host.Storage.Exists("bracket.json")) + if (storage.Exists("bracket.json")) { Logger.Log("Migrating bracket to default tournament storage."); - var bracketFile = new System.IO.FileInfo(host.Storage.GetFullPath("bracket.json")); + var bracketFile = new System.IO.FileInfo(storage.GetFullPath("bracket.json")); moveFile(bracketFile, destination); } - if (host.Storage.Exists("drawings.txt")) + if (storage.Exists("drawings.txt")) { Logger.Log("Migrating drawings to default tournament storage."); - var drawingsFile = new System.IO.FileInfo(host.Storage.GetFullPath("drawings.txt")); + var drawingsFile = new System.IO.FileInfo(storage.GetFullPath("drawings.txt")); moveFile(drawingsFile, destination); } - if (host.Storage.Exists("drawings.ini")) + if (storage.Exists("drawings.ini")) { Logger.Log("Migrating drawing configuration to default tournament storage."); - var drawingsConfigFile = new System.IO.FileInfo(host.Storage.GetFullPath("drawings.ini")); + var drawingsConfigFile = new System.IO.FileInfo(storage.GetFullPath("drawings.ini")); moveFile(drawingsConfigFile, cfgDestination); } - if (host.Storage.Exists("drawings_results.txt")) + if (storage.Exists("drawings_results.txt")) { Logger.Log("Migrating drawings results to default tournament storage."); - var drawingsResultsFile = new System.IO.FileInfo(host.Storage.GetFullPath("drawings_results.txt")); + var drawingsResultsFile = new System.IO.FileInfo(storage.GetFullPath("drawings_results.txt")); moveFile(drawingsResultsFile, destination); } diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index a779135345..7ec8d0f18a 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -8,6 +8,7 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; using osu.Framework.Input; +using osu.Framework.Platform; using osu.Framework.IO.Stores; using osu.Game.Beatmaps; using osu.Game.Online.API.Requests; @@ -23,11 +24,8 @@ namespace osu.Game.Tournament public class TournamentGameBase : OsuGameBase { private const string bracket_filename = "bracket.json"; - private LadderInfo ladder; - private TournamentStorage storage; - private DependencyContainer dependencies; private FileBasedIPC ipc; @@ -37,11 +35,11 @@ namespace osu.Game.Tournament } [BackgroundDependencyLoader] - private void load() + private void load(Storage baseStorage) { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); - dependencies.CacheAs(storage = new TournamentStorage(Host)); + dependencies.CacheAs(storage = new TournamentStorage(baseStorage)); Textures.AddStore(new TextureLoaderStore(storage.VideoStore)); From b75fd7bfa8756addf028665492b456334d55b620 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 16 Jun 2020 17:14:54 +0200 Subject: [PATCH 1771/6909] Refactor moving logic (1/2) --- osu.Game.Tournament/IO/TournamentStorage.cs | 43 +++++++-------------- 1 file changed, 15 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index d6e95bc4b6..1962cc46d8 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -57,34 +57,11 @@ namespace osu.Game.Tournament.IO if (!cfgDestination.Exists) destination.CreateSubdirectory(config_directory); - - if (storage.Exists("bracket.json")) - { - Logger.Log("Migrating bracket to default tournament storage."); - var bracketFile = new System.IO.FileInfo(storage.GetFullPath("bracket.json")); - moveFile(bracketFile, destination); - } - - if (storage.Exists("drawings.txt")) - { - Logger.Log("Migrating drawings to default tournament storage."); - var drawingsFile = new System.IO.FileInfo(storage.GetFullPath("drawings.txt")); - moveFile(drawingsFile, destination); - } - - if (storage.Exists("drawings.ini")) - { - Logger.Log("Migrating drawing configuration to default tournament storage."); - var drawingsConfigFile = new System.IO.FileInfo(storage.GetFullPath("drawings.ini")); - moveFile(drawingsConfigFile, cfgDestination); - } - - if (storage.Exists("drawings_results.txt")) - { - Logger.Log("Migrating drawings results to default tournament storage."); - var drawingsResultsFile = new System.IO.FileInfo(storage.GetFullPath("drawings_results.txt")); - moveFile(drawingsResultsFile, destination); - } + + moveFileIfExists("bracket.json", destination); + moveFileIfExists("drawings.txt", destination); + moveFileIfExists("drawings_results.txt", destination); + moveFileIfExists("drawings.ini", cfgDestination); if (source.Exists) { @@ -94,6 +71,16 @@ namespace osu.Game.Tournament.IO } } + private void moveFileIfExists(string file, DirectoryInfo destination) + { + if (storage.Exists(file)) + { + Logger.Log($"Migrating {file} to default tournament storage."); + var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file)); + moveFile(fileInfo, destination); + } + } + private void copyRecursive(DirectoryInfo source, DirectoryInfo destination) { // based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo From 02d66c4856626e60226e1a4ebcac6dffdcef13a7 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 16 Jun 2020 17:15:43 +0200 Subject: [PATCH 1772/6909] Refactor moving (2/2) --- osu.Game.Tournament/IO/TournamentStorage.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 1962cc46d8..611592e0e3 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tournament.IO if (!cfgDestination.Exists) destination.CreateSubdirectory(config_directory); - + moveFileIfExists("bracket.json", destination); moveFileIfExists("drawings.txt", destination); moveFileIfExists("drawings_results.txt", destination); @@ -77,7 +77,8 @@ namespace osu.Game.Tournament.IO { Logger.Log($"Migrating {file} to default tournament storage."); var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file)); - moveFile(fileInfo, destination); + attemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true)); + fileInfo.Delete(); } } @@ -112,12 +113,6 @@ namespace osu.Game.Tournament.IO attemptOperation(target.Delete); } - private void moveFile(System.IO.FileInfo file, DirectoryInfo destination) - { - attemptOperation(() => file.CopyTo(Path.Combine(destination.FullName, file.Name), true)); - file.Delete(); - } - private void attemptOperation(Action action, int attempts = 10) { while (true) From dd9697032c0071931eecf6b563c1774647ae33dd Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 16 Jun 2020 17:39:20 +0200 Subject: [PATCH 1773/6909] Introduce new class MigratableStorage --- osu.Game.Tournament/IO/TournamentStorage.cs | 70 +++------------ osu.Game/IO/MigratableStorage.cs | 95 +++++++++++++++++++++ 2 files changed, 105 insertions(+), 60 deletions(-) create mode 100644 osu.Game/IO/MigratableStorage.cs diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 611592e0e3..1731b96095 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -11,7 +11,7 @@ using osu.Game.Tournament.Configuration; namespace osu.Game.Tournament.IO { - public class TournamentStorage : WrappedStorage + public class TournamentStorage : MigratableStorage { private readonly Storage storage; internal readonly TournamentVideoResourceStore VideoStore; @@ -52,8 +52,15 @@ namespace osu.Game.Tournament.IO var destination = new DirectoryInfo(GetFullPath(default_tournament)); var cfgDestination = new DirectoryInfo(GetFullPath(default_tournament + Path.DirectorySeparatorChar + config_directory)); - if (!destination.Exists) - destination.Create(); + // if (!destination.Exists) + // destination.Create(); + + if (source.Exists) + { + Logger.Log("Migrating tournament assets to default tournament storage."); + copyRecursive(source, destination); + deleteRecursive(source); + } if (!cfgDestination.Exists) destination.CreateSubdirectory(config_directory); @@ -62,13 +69,6 @@ namespace osu.Game.Tournament.IO moveFileIfExists("drawings.txt", destination); moveFileIfExists("drawings_results.txt", destination); moveFileIfExists("drawings.ini", cfgDestination); - - if (source.Exists) - { - Logger.Log("Migrating tournament assets to default tournament storage."); - copyRecursive(source, destination); - deleteRecursive(source); - } } private void moveFileIfExists(string file, DirectoryInfo destination) @@ -81,55 +81,5 @@ namespace osu.Game.Tournament.IO fileInfo.Delete(); } } - - private void copyRecursive(DirectoryInfo source, DirectoryInfo destination) - { - // based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo - - foreach (System.IO.FileInfo fi in source.GetFiles()) - { - attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true)); - } - - foreach (DirectoryInfo dir in source.GetDirectories()) - { - copyRecursive(dir, destination.CreateSubdirectory(dir.Name)); - } - } - - private void deleteRecursive(DirectoryInfo target) - { - foreach (System.IO.FileInfo fi in target.GetFiles()) - { - attemptOperation(() => fi.Delete()); - } - - foreach (DirectoryInfo dir in target.GetDirectories()) - { - attemptOperation(() => dir.Delete(true)); - } - - if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) - attemptOperation(target.Delete); - } - - private void attemptOperation(Action action, int attempts = 10) - { - while (true) - { - try - { - action(); - return; - } - catch (Exception) - { - if (attempts-- == 0) - throw; - } - - Thread.Sleep(250); - } - } } } diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs new file mode 100644 index 0000000000..0ab0ea9934 --- /dev/null +++ b/osu.Game/IO/MigratableStorage.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; +using System.IO; +using System.Linq; +using System.Threading; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Configuration; + +namespace osu.Game.IO +{ + public abstract class MigratableStorage : WrappedStorage + { + + virtual protected string[] IGNORE_DIRECTORIES { get; set; } = Array.Empty(); + + virtual protected string[] IGNORE_FILES { get; set; } = Array.Empty(); + + public MigratableStorage(Storage storage, string subPath = null) + : base(storage, subPath) + { + } + + protected void deleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) + { + foreach (System.IO.FileInfo fi in target.GetFiles()) + { + if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) + continue; + + attemptOperation(() => fi.Delete()); + } + + foreach (DirectoryInfo dir in target.GetDirectories()) + { + if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) + continue; + + attemptOperation(() => dir.Delete(true)); + } + + if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) + attemptOperation(target.Delete); + } + + protected void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) + { + // based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo + if (!destination.Exists) + Directory.CreateDirectory(destination.FullName); + + foreach (System.IO.FileInfo fi in source.GetFiles()) + { + if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) + continue; + + attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true)); + } + + foreach (DirectoryInfo dir in source.GetDirectories()) + { + if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) + continue; + + copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); + } + } + + /// + /// 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). + protected static void attemptOperation(Action action, int attempts = 10) + { + while (true) + { + try + { + action(); + return; + } + catch (Exception) + { + if (attempts-- == 0) + throw; + } + + Thread.Sleep(250); + } + } + } +} From 5e74985edafa034306c4559d32e99aa495697d80 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Jun 2020 19:28:40 +0900 Subject: [PATCH 1774/6909] 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 1775/6909] 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 1776/6909] 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 1777/6909] 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 1778/6909] 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 1779/6909] 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 1780/6909] 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 1781/6909] 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 1782/6909] 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 1783/6909] 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 1784/6909] 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 1785/6909] 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 1786/6909] 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 1787/6909] 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 1788/6909] 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 1789/6909] 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 1790/6909] 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 1791/6909] 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 1792/6909] 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 1793/6909] 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 1794/6909] 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 1795/6909] 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 1796/6909] 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 1797/6909] 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 1798/6909] 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 1799/6909] 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 1800/6909] 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 1801/6909] 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 1802/6909] 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 1803/6909] 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 1804/6909] 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 1805/6909] 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 1806/6909] 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 1807/6909] 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 1808/6909] 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 1809/6909] 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 1810/6909] 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 1811/6909] 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 1812/6909] 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 1813/6909] 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 1814/6909] 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 1815/6909] 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 1816/6909] 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 1817/6909] 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 1818/6909] 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 1819/6909] 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 1820/6909] 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 1821/6909] 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 1822/6909] 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 1823/6909] 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 1824/6909] 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 1825/6909] 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 1826/6909] 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 1827/6909] 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 1828/6909] 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 1829/6909] 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 1830/6909] 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 1831/6909] 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 1832/6909] 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 1833/6909] 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 1834/6909] 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 1835/6909] 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 1836/6909] 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 1837/6909] 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 1838/6909] 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 1839/6909] 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 1840/6909] 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 1841/6909] 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 1842/6909] 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 1843/6909] 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 21774b8967ffd8dfd99c1f29b9179394197599f5 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 22 Jun 2020 11:38:50 +0200 Subject: [PATCH 1844/6909] Move static properties to parent class and inherit OsuStorage from it --- .../NonVisual/CustomDataDirectoryTest.cs | 4 +- osu.Game/IO/MigratableStorage.cs | 8 +- osu.Game/IO/OsuStorage.cs | 78 +------------------ 3 files changed, 9 insertions(+), 81 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index f3d54d876a..5abefe3198 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -151,13 +151,13 @@ namespace osu.Game.Tests.NonVisual 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) + foreach (var file in MigratableStorage.IGNORE_FILES) { Assert.That(host.Storage.Exists(file), Is.True); Assert.That(storage.Exists(file), Is.False); } - foreach (var dir in OsuStorage.IGNORE_DIRECTORIES) + foreach (var dir in MigratableStorage.IGNORE_DIRECTORIES) { Assert.That(host.Storage.ExistsDirectory(dir), Is.True); Assert.That(storage.ExistsDirectory(dir), Is.False); diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 0ab0ea9934..004aa4e09a 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -14,9 +14,13 @@ namespace osu.Game.IO public abstract class MigratableStorage : WrappedStorage { - virtual protected string[] IGNORE_DIRECTORIES { get; set; } = Array.Empty(); + internal static readonly string[] IGNORE_DIRECTORIES = { "cache" }; - virtual protected string[] IGNORE_FILES { get; set; } = Array.Empty(); + internal static readonly string[] IGNORE_FILES = + { + "framework.ini", + "storage.ini" + }; public MigratableStorage(Storage storage, string subPath = null) : base(storage, subPath) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 499bcb4063..416d2082c3 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -11,19 +11,11 @@ using osu.Game.Configuration; namespace osu.Game.IO { - public class OsuStorage : WrappedStorage + public class OsuStorage : MigratableStorage { 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) { @@ -76,73 +68,5 @@ namespace osu.Game.IO deleteRecursive(source); } - - private static void deleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) - { - foreach (System.IO.FileInfo fi in target.GetFiles()) - { - if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) - continue; - - attemptOperation(() => fi.Delete()); - } - - foreach (DirectoryInfo dir in target.GetDirectories()) - { - if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) - continue; - - attemptOperation(() => dir.Delete(true)); - } - - if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) - attemptOperation(target.Delete); - } - - 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 (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) - continue; - - attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true)); - } - - foreach (DirectoryInfo dir in source.GetDirectories()) - { - if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) - continue; - - copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); - } - } - - /// - /// 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) - { - while (true) - { - try - { - action(); - return; - } - catch (Exception) - { - if (attempts-- == 0) - throw; - } - - Thread.Sleep(250); - } - } } } From f878388d578a798698fb7de98935f3fe27239274 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 22 Jun 2020 11:56:14 +0200 Subject: [PATCH 1845/6909] Fix TestMigrationToSeeminglyNestedTarget failing --- osu.Game.Tournament/IO/TournamentStorage.cs | 3 --- osu.Game/IO/MigratableStorage.cs | 2 +- osu.Game/IO/OsuStorage.cs | 2 -- 3 files changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 1731b96095..c1629f270f 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -52,9 +52,6 @@ namespace osu.Game.Tournament.IO var destination = new DirectoryInfo(GetFullPath(default_tournament)); var cfgDestination = new DirectoryInfo(GetFullPath(default_tournament + Path.DirectorySeparatorChar + config_directory)); - // if (!destination.Exists) - // destination.Create(); - if (source.Exists) { Logger.Log("Migrating tournament assets to default tournament storage."); diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 004aa4e09a..95721a736e 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -53,7 +53,7 @@ namespace osu.Game.IO { // based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo if (!destination.Exists) - Directory.CreateDirectory(destination.FullName); + Directory.CreateDirectory(destination.FullName); foreach (System.IO.FileInfo fi in source.GetFiles()) { diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 416d2082c3..d37336234a 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -55,8 +55,6 @@ namespace osu.Game.IO { if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0) throw new ArgumentException("Destination provided already has files or directories present", nameof(newLocation)); - - deleteRecursive(destination); } copyRecursive(source, destination); From 2b7fb2b71d352a97031b7477315c50f0a9f8ba70 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 19:04:51 +0900 Subject: [PATCH 1846/6909] 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 1847/6909] 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 1848/6909] 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 eec1e9ef4d660acf88de42ec6a814c96e2cf97cf Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 22 Jun 2020 12:22:59 +0200 Subject: [PATCH 1849/6909] Remove unnecessary comments and added file check for tournament.ini on test start --- .../NonVisual/CustomTourneyDirectoryTest.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 37f456ae96..92ff39c67c 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -78,10 +78,13 @@ namespace osu.Game.Tournament.Tests.NonVisual { using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) { - // Recreate the old setup that uses "tournament" as the base path. string osuRoot = basePath(nameof(TestMigration)); + string configFile = Path.Combine(osuRoot, "tournament.ini"); - // Define all the paths for the old scenario + if (File.Exists(configFile)) + File.Delete(configFile); + + // Recreate the old setup that uses "tournament" as the base path. string oldPath = Path.Combine(osuRoot, "tournament"); string videosPath = Path.Combine(oldPath, "videos"); string modsPath = Path.Combine(oldPath, "mods"); @@ -141,7 +144,6 @@ namespace osu.Game.Tournament.Tests.NonVisual } finally { - // Cleaning up after ourselves. host.Storage.Delete("tournament.ini"); host.Storage.DeleteDirectory("tournaments"); host.Exit(); From 08759da3a7642647d08317231da389b7f78daa44 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 22 Jun 2020 12:41:43 +0200 Subject: [PATCH 1850/6909] Move drawings.ini out of config subfolder --- .../NonVisual/CustomTourneyDirectoryTest.cs | 2 +- osu.Game.Tournament/IO/TournamentStorage.cs | 9 +-------- osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs | 2 +- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 92ff39c67c..9e6675e09f 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -136,7 +136,7 @@ namespace osu.Game.Tournament.Tests.NonVisual Assert.True(storage.Exists("drawings.txt")); Assert.True(storage.Exists("drawings_results.txt")); - Assert.True(storage.ConfigurationStorage.Exists("drawings.ini")); + Assert.True(storage.Exists("drawings.ini")); Assert.True(storage.Exists(videoFile)); Assert.True(storage.Exists(modFile)); diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index c1629f270f..c9d7ef3126 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -15,9 +15,7 @@ namespace osu.Game.Tournament.IO { private readonly Storage storage; internal readonly TournamentVideoResourceStore VideoStore; - internal readonly Storage ConfigurationStorage; private const string default_tournament = "default"; - private const string config_directory = "config"; public TournamentStorage(Storage storage) : base(storage.GetStorageForDirectory("tournaments"), string.Empty) @@ -40,8 +38,6 @@ namespace osu.Game.Tournament.IO ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_tournament)); } - ConfigurationStorage = UnderlyingStorage.GetStorageForDirectory(config_directory); - VideoStore = new TournamentVideoResourceStore(this); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } @@ -50,7 +46,6 @@ namespace osu.Game.Tournament.IO { var source = new DirectoryInfo(storage.GetFullPath("tournament")); var destination = new DirectoryInfo(GetFullPath(default_tournament)); - var cfgDestination = new DirectoryInfo(GetFullPath(default_tournament + Path.DirectorySeparatorChar + config_directory)); if (source.Exists) { @@ -59,13 +54,11 @@ namespace osu.Game.Tournament.IO deleteRecursive(source); } - if (!cfgDestination.Exists) - destination.CreateSubdirectory(config_directory); moveFileIfExists("bracket.json", destination); moveFileIfExists("drawings.txt", destination); moveFileIfExists("drawings_results.txt", destination); - moveFileIfExists("drawings.ini", cfgDestination); + moveFileIfExists("drawings.ini", destination); } private void moveFileIfExists(string file, DirectoryInfo destination) diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs index de909af152..8b6bd21ee6 100644 --- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs +++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs @@ -54,7 +54,7 @@ namespace osu.Game.Tournament.Screens.Drawings return; } - drawingsConfig = new DrawingsConfigManager(storage.ConfigurationStorage); + drawingsConfig = new DrawingsConfigManager(storage); InternalChildren = new Drawable[] { From 6b14079c0a80bc256f49376d939a861c7cd59ba7 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 22 Jun 2020 12:43:01 +0200 Subject: [PATCH 1851/6909] InspectCode changes --- osu.Game.Tournament/IO/TournamentStorage.cs | 9 +++------ osu.Game/IO/MigratableStorage.cs | 21 +++++++++------------ osu.Game/IO/OsuStorage.cs | 6 ++---- 3 files changed, 14 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index c9d7ef3126..12dcc2195c 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.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.Threading; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.IO; @@ -50,11 +48,10 @@ namespace osu.Game.Tournament.IO if (source.Exists) { Logger.Log("Migrating tournament assets to default tournament storage."); - copyRecursive(source, destination); - deleteRecursive(source); + CopyRecursive(source, destination); + DeleteRecursive(source); } - moveFileIfExists("bracket.json", destination); moveFileIfExists("drawings.txt", destination); moveFileIfExists("drawings_results.txt", destination); @@ -67,7 +64,7 @@ namespace osu.Game.Tournament.IO { Logger.Log($"Migrating {file} to default tournament storage."); var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file)); - attemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true)); + AttemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true)); fileInfo.Delete(); } } diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 95721a736e..7efc37990f 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -5,15 +5,12 @@ using System; using System.IO; using System.Linq; using System.Threading; -using osu.Framework.Logging; using osu.Framework.Platform; -using osu.Game.Configuration; namespace osu.Game.IO { public abstract class MigratableStorage : WrappedStorage { - internal static readonly string[] IGNORE_DIRECTORIES = { "cache" }; internal static readonly string[] IGNORE_FILES = @@ -22,19 +19,19 @@ namespace osu.Game.IO "storage.ini" }; - public MigratableStorage(Storage storage, string subPath = null) + protected MigratableStorage(Storage storage, string subPath = null) : base(storage, subPath) { } - protected void deleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) + protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) { foreach (System.IO.FileInfo fi in target.GetFiles()) { if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) continue; - attemptOperation(() => fi.Delete()); + AttemptOperation(() => fi.Delete()); } foreach (DirectoryInfo dir in target.GetDirectories()) @@ -42,14 +39,14 @@ namespace osu.Game.IO if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) continue; - attemptOperation(() => dir.Delete(true)); + AttemptOperation(() => dir.Delete(true)); } if (target.GetFiles().Length == 0 && target.GetDirectories().Length == 0) - attemptOperation(target.Delete); + AttemptOperation(target.Delete); } - protected void copyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) + protected void CopyRecursive(DirectoryInfo source, DirectoryInfo destination, bool topLevelExcludes = true) { // based off example code https://docs.microsoft.com/en-us/dotnet/api/system.io.directoryinfo if (!destination.Exists) @@ -60,7 +57,7 @@ namespace osu.Game.IO if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) continue; - attemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true)); + AttemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true)); } foreach (DirectoryInfo dir in source.GetDirectories()) @@ -68,7 +65,7 @@ namespace osu.Game.IO if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) continue; - copyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); + CopyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); } } @@ -77,7 +74,7 @@ namespace osu.Game.IO /// /// The action to perform. /// The number of attempts (250ms wait between each). - protected static void attemptOperation(Action action, int attempts = 10) + protected static void AttemptOperation(Action action, int attempts = 10) { while (true) { diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index d37336234a..3d224841f3 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -3,8 +3,6 @@ using System; using System.IO; -using System.Linq; -using System.Threading; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Configuration; @@ -57,14 +55,14 @@ namespace osu.Game.IO throw new ArgumentException("Destination provided already has files or directories present", nameof(newLocation)); } - copyRecursive(source, destination); + CopyRecursive(source, destination); ChangeTargetStorage(host.GetStorage(newLocation)); storageConfig.Set(StorageConfig.FullPath, newLocation); storageConfig.Save(); - deleteRecursive(source); + DeleteRecursive(source); } } } From 4d30761ce3131eccaab114285018f5ab0cc54a79 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 19:49:38 +0900 Subject: [PATCH 1852/6909] 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 a94dcc4923023d1322af32ed35ccb05f4ccfec57 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 22 Jun 2020 12:59:38 +0200 Subject: [PATCH 1853/6909] Add xmldoc to MigratableStorage --- osu.Game/IO/MigratableStorage.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 7efc37990f..45aba41315 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -9,6 +9,9 @@ using osu.Framework.Platform; namespace osu.Game.IO { + /// + /// A that is migratable to different locations. + /// public abstract class MigratableStorage : WrappedStorage { internal static readonly string[] IGNORE_DIRECTORIES = { "cache" }; From e0d5a9182e76eb44d9cf18ac6b2ba259316fbb9b Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 22 Jun 2020 12:59:56 +0200 Subject: [PATCH 1854/6909] make tournament migration private --- osu.Game.Tournament/IO/TournamentStorage.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 12dcc2195c..ebd8d2b63f 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tournament.IO } else { - Migrate(); + migrate(); storageConfig.Set(StorageConfig.CurrentTournament, default_tournament); storageConfig.Save(); ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_tournament)); @@ -40,7 +40,7 @@ namespace osu.Game.Tournament.IO Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } - internal void Migrate() + private void migrate() { var source = new DirectoryInfo(storage.GetFullPath("tournament")); var destination = new DirectoryInfo(GetFullPath(default_tournament)); From a899c754f11942c8967ff78139ef7bb5433e1b8c Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 22 Jun 2020 13:03:24 +0200 Subject: [PATCH 1855/6909] Remove whitespace at the end of xmldoc line --- osu.Game/IO/MigratableStorage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 45aba41315..c4dc4bcfb2 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -10,7 +10,7 @@ using osu.Framework.Platform; namespace osu.Game.IO { /// - /// A that is migratable to different locations. + /// A that is migratable to different locations. /// public abstract class MigratableStorage : WrappedStorage { From 5c4df2e32c0a1cd8f79f8d5e1e99e9fef6bfd228 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:20:42 +0900 Subject: [PATCH 1856/6909] 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 1857/6909] 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 1858/6909] 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 a4eb6c81c5a90b15d298fe705cc0699c98da1772 Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Mon, 22 Jun 2020 13:40:31 +0200 Subject: [PATCH 1859/6909] undid changes to the file --- osu.Game/Screens/Play/Player.cs | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index d5e9c54e04..b6d87e658b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -5,11 +5,13 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using Humanizer; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -78,9 +80,7 @@ namespace osu.Game.Screens.Play private IAPIProvider api { get; set; } private SampleChannel sampleRestart; - - private SampleChannel samplePause; - + public BreakOverlay BreakOverlay; private BreakTracker breakTracker; @@ -164,10 +164,6 @@ namespace osu.Game.Screens.Play sampleRestart = audio.Samples.Get(@"Gameplay/restart"); - samplePause = audio.Samples.Get(@"Gameplay/pause-loop"); - if (samplePause != null) - samplePause.Looping = true; - mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); @@ -414,7 +410,6 @@ namespace osu.Game.Screens.Play Pause(); else { - samplePause?.Stop(); this.Exit(); } } @@ -569,21 +564,20 @@ namespace osu.Game.Screens.Play DrawableRuleset.CancelResume(); IsResuming = false; } - GameplayClockContainer.Stop(); PauseOverlay.Show(); lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; - - samplePause?.Play(); + } public void Resume() { if (!canResume) return; + IsResuming = true; PauseOverlay.Hide(); - + // breaks and time-based conditions may allow instant resume. if (breakTracker.IsBreakTime.Value) completeResume(); @@ -594,8 +588,6 @@ namespace osu.Game.Screens.Play { GameplayClockContainer.Start(); IsResuming = false; - - samplePause?.Stop(); } } @@ -672,7 +664,9 @@ namespace osu.Game.Screens.Play // as we are no longer the current screen, we cannot guarantee the track is still usable. GameplayClockContainer?.StopUsingBeatmapClock(); + fadeOut(); + return base.OnExiting(next); } @@ -717,7 +711,12 @@ namespace osu.Game.Screens.Play Background.EnableUserDim.Value = false; storyboardReplacesBackground.Value = false; } + #endregion + } + + + } From 983f0ada2da68527f5892ffd6ff0202c05d1d439 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 20:44:39 +0900 Subject: [PATCH 1860/6909] 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 1861/6909] 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 1862/6909] 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 1863/6909] 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 9dea96e5fdff2a741af29b10948be6a65d63d634 Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Mon, 22 Jun 2020 14:02:21 +0200 Subject: [PATCH 1864/6909] added pause sound with fading --- osu.Game/Screens/Play/PauseOverlay.cs | 39 ++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 6cc6027a03..6cca0c47fd 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -3,9 +3,18 @@ using System; using System.Linq; +using System.Runtime.CompilerServices; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NUnit.Framework.Internal; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; using osu.Game.Graphics; using osuTK.Graphics; +using osu.Framework.Logging; + namespace osu.Game.Screens.Play { @@ -16,14 +25,42 @@ namespace osu.Game.Screens.Play public override string Header => "paused"; public override string Description => "you're not going to do what i think you're going to do, are ya?"; + private DrawableSample pauseLoop; + protected override Action BackAction => () => InternalButtons.Children.First().Click(); [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, AudioManager audio) { AddButton("Continue", colours.Green, () => OnResume?.Invoke()); AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); + + var sampleChannel = audio.Samples.Get(@"Gameplay/pause-loop"); + if (sampleChannel != null) + { + AddInternal(pauseLoop = new DrawableSample(sampleChannel) + { + Looping = true, + }); + pauseLoop?.VolumeTo(0.0f); + pauseLoop?.Play(); + } } + + + protected override void PopIn() + { + base.PopIn(); + pauseLoop?.VolumeTo(1.0f, 400, Easing.InQuint); + } + + protected override void PopOut() + { + base.PopOut(); + pauseLoop?.VolumeTo(0.0f); + } + + } } From cb03e6faa9cf975719a8d3753ca063bb986ed8bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 21:09:47 +0900 Subject: [PATCH 1865/6909] 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 1866/6909] 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 836386d03ba0da92f7b625d3cc361110512d15a8 Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Mon, 22 Jun 2020 15:22:13 +0200 Subject: [PATCH 1867/6909] removed duplicate lines --- osu.Game/Screens/Play/PauseOverlay.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 6cca0c47fd..fc4e509c2c 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -14,7 +14,7 @@ using osu.Framework.Graphics.Audio; using osu.Game.Graphics; using osuTK.Graphics; using osu.Framework.Logging; - +using SharpCompress.Common; namespace osu.Game.Screens.Play { @@ -43,7 +43,6 @@ namespace osu.Game.Screens.Play { Looping = true, }); - pauseLoop?.VolumeTo(0.0f); pauseLoop?.Play(); } } @@ -58,7 +57,7 @@ namespace osu.Game.Screens.Play protected override void PopOut() { base.PopOut(); - pauseLoop?.VolumeTo(0.0f); + pauseLoop.VolumeTo(0.0f); } From 1bf00e0c820c8c391a0b7574b8cfc28ea0374c43 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Jun 2020 23:22:49 +0900 Subject: [PATCH 1868/6909] 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 1869/6909] 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 1870/6909] 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 1871/6909] 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 1872/6909] 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 1873/6909] 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 1874/6909] 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 1875/6909] 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 1876/6909] 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 1877/6909] 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 1878/6909] 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 1879/6909] 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 1880/6909] 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 1881/6909] 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 1882/6909] 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 624ad65806da84f82078cc188bc80c2feb4d0d54 Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Tue, 23 Jun 2020 13:09:24 +0200 Subject: [PATCH 1883/6909] formating --- osu.Game/Screens/Play/PauseOverlay.cs | 17 +++++------------ osu.Game/Screens/Play/Player.cs | 18 +++--------------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index fc4e509c2c..191bf0d901 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -3,18 +3,12 @@ using System; using System.Linq; -using System.Runtime.CompilerServices; -using Microsoft.CodeAnalysis.CSharp.Syntax; -using NUnit.Framework.Internal; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Game.Graphics; using osuTK.Graphics; -using osu.Framework.Logging; -using SharpCompress.Common; namespace osu.Game.Screens.Play { @@ -37,17 +31,18 @@ namespace osu.Game.Screens.Play AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); var sampleChannel = audio.Samples.Get(@"Gameplay/pause-loop"); + if (sampleChannel != null) { - AddInternal(pauseLoop = new DrawableSample(sampleChannel) + pauseLoop = new DrawableSample(sampleChannel) { Looping = true, - }); + }; + AddInternal(pauseLoop); pauseLoop?.Play(); } } - protected override void PopIn() { base.PopIn(); @@ -57,9 +52,7 @@ namespace osu.Game.Screens.Play protected override void PopOut() { base.PopOut(); - pauseLoop.VolumeTo(0.0f); + pauseLoop?.VolumeTo(0.0f); } - - } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ce790e1315..d3b88e56ae 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -5,13 +5,11 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using Humanizer; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Logging; @@ -80,7 +78,7 @@ namespace osu.Game.Screens.Play private IAPIProvider api { get; set; } private SampleChannel sampleRestart; - + public BreakOverlay BreakOverlay; private BreakTracker breakTracker; @@ -412,9 +410,7 @@ namespace osu.Game.Screens.Play if (canPause) Pause(); else - { this.Exit(); - } } /// @@ -567,20 +563,19 @@ namespace osu.Game.Screens.Play DrawableRuleset.CancelResume(); IsResuming = false; } + GameplayClockContainer.Stop(); PauseOverlay.Show(); lastPauseActionTime = GameplayClockContainer.GameplayClock.CurrentTime; - } public void Resume() { if (!canResume) return; - IsResuming = true; PauseOverlay.Hide(); - + // breaks and time-based conditions may allow instant resume. if (breakTracker.IsBreakTime.Value) completeResume(); @@ -671,9 +666,7 @@ namespace osu.Game.Screens.Play // as we are no longer the current screen, we cannot guarantee the track is still usable. GameplayClockContainer?.StopUsingBeatmapClock(); - fadeOut(); - return base.OnExiting(next); } @@ -718,12 +711,7 @@ namespace osu.Game.Screens.Play Background.EnableUserDim.Value = false; storyboardReplacesBackground.Value = false; } - #endregion - } - - - } From e7238e25f96de3c9de8c622d3026a04d34039cc8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 23 Jun 2020 20:36:09 +0900 Subject: [PATCH 1884/6909] 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 1885/6909] 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 1886/6909] 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 1887/6909] 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 1888/6909] 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 1889/6909] 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 a47d34f1db3ef3f69be44b0194630b06ebefda84 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 23 Jun 2020 23:34:26 +0200 Subject: [PATCH 1890/6909] make ignore properties protected virtual get-only in base --- .../NonVisual/CustomDataDirectoryTest.cs | 5 +++-- osu.Game/IO/MigratableStorage.cs | 11 +++------- osu.Game/IO/OsuStorage.cs | 20 +++++++++++++++++++ 3 files changed, 26 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 5abefe3198..5278837073 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -126,6 +126,7 @@ namespace osu.Game.Tests.NonVisual { var osu = loadOsu(host); var storage = osu.Dependencies.Get(); + var osuStorage = storage as OsuStorage; // ensure we perform a save host.Dependencies.Get().Save(); @@ -151,13 +152,13 @@ namespace osu.Game.Tests.NonVisual Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); - foreach (var file in MigratableStorage.IGNORE_FILES) + 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 MigratableStorage.IGNORE_DIRECTORIES) + foreach (var dir in osuStorage.IGNORE_DIRECTORIES) { Assert.That(host.Storage.ExistsDirectory(dir), Is.True); Assert.That(storage.ExistsDirectory(dir), Is.False); diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index c4dc4bcfb2..0656e61f10 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -14,14 +14,9 @@ namespace osu.Game.IO /// public abstract class MigratableStorage : WrappedStorage { - internal static readonly string[] IGNORE_DIRECTORIES = { "cache" }; - - internal static readonly string[] IGNORE_FILES = - { - "framework.ini", - "storage.ini" - }; - + internal virtual string[] IGNORE_DIRECTORIES { get; } + internal virtual string[] IGNORE_FILES { get; } + protected MigratableStorage(Storage storage, string subPath = null) : base(storage, subPath) { diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 3d224841f3..bbec6eb575 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -14,6 +14,26 @@ namespace osu.Game.IO private readonly GameHost host; private readonly StorageConfigManager storageConfig; + internal override string[] IGNORE_DIRECTORIES + { + get + { + return new string[] { "cache" }; + } + } + + internal override string[] IGNORE_FILES + { + get + { + return new string[] + { + "framework.ini", + "storage.ini" + }; + } + } + public OsuStorage(GameHost host) : base(host.Storage, string.Empty) { From 8b9cf6fc52e84ba1cd0f3ceac2c9561f3e6d0c3c Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 23 Jun 2020 23:57:58 +0200 Subject: [PATCH 1891/6909] Remove default value in Storagemgr --- .../Configuration/TournamentStorageManager.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game.Tournament/Configuration/TournamentStorageManager.cs b/osu.Game.Tournament/Configuration/TournamentStorageManager.cs index 653ea14352..e3d0a9e75c 100644 --- a/osu.Game.Tournament/Configuration/TournamentStorageManager.cs +++ b/osu.Game.Tournament/Configuration/TournamentStorageManager.cs @@ -14,12 +14,6 @@ namespace osu.Game.Tournament.Configuration : base(storage) { } - - protected override void InitialiseDefaults() - { - base.InitialiseDefaults(); - Set(StorageConfig.CurrentTournament, string.Empty); - } } public enum StorageConfig From 8e8458ab8fa082a7956265098a6a539f4be09244 Mon Sep 17 00:00:00 2001 From: Shivam Date: Tue, 23 Jun 2020 23:58:28 +0200 Subject: [PATCH 1892/6909] make migrate public abstract in base and override --- osu.Game.Tournament/IO/TournamentStorage.cs | 8 ++++---- osu.Game/IO/MigratableStorage.cs | 4 +++- osu.Game/IO/OsuStorage.cs | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index ebd8d2b63f..5c1d9a39c5 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tournament.IO private readonly Storage storage; internal readonly TournamentVideoResourceStore VideoStore; private const string default_tournament = "default"; - + public TournamentStorage(Storage storage) : base(storage.GetStorageForDirectory("tournaments"), string.Empty) { @@ -30,7 +30,7 @@ namespace osu.Game.Tournament.IO } else { - migrate(); + Migrate(GetFullPath(default_tournament)); storageConfig.Set(StorageConfig.CurrentTournament, default_tournament); storageConfig.Save(); ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_tournament)); @@ -40,10 +40,10 @@ namespace osu.Game.Tournament.IO Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } - private void migrate() + override public void Migrate(string newLocation) { var source = new DirectoryInfo(storage.GetFullPath("tournament")); - var destination = new DirectoryInfo(GetFullPath(default_tournament)); + var destination = new DirectoryInfo(newLocation); if (source.Exists) { diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 0656e61f10..0f064dfe2d 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -16,12 +16,14 @@ namespace osu.Game.IO { internal virtual string[] IGNORE_DIRECTORIES { get; } internal virtual string[] IGNORE_FILES { get; } - + protected MigratableStorage(Storage storage, string subPath = null) : base(storage, subPath) { } + abstract public void Migrate(string newLocation); + protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) { foreach (System.IO.FileInfo fi in target.GetFiles()) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index bbec6eb575..8890ecf843 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -53,7 +53,7 @@ namespace osu.Game.IO Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs"); } - public void Migrate(string newLocation) + override public void Migrate(string newLocation) { var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); From 7a3315dcf82b5aea1ce12343ca81d9da95b6176f Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 24 Jun 2020 00:00:21 +0200 Subject: [PATCH 1893/6909] invert and early return --- osu.Game.Tournament/IO/TournamentStorage.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 5c1d9a39c5..5f90598890 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tournament.IO private readonly Storage storage; internal readonly TournamentVideoResourceStore VideoStore; private const string default_tournament = "default"; - + public TournamentStorage(Storage storage) : base(storage.GetStorageForDirectory("tournaments"), string.Empty) { @@ -60,13 +60,13 @@ namespace osu.Game.Tournament.IO private void moveFileIfExists(string file, DirectoryInfo destination) { - if (storage.Exists(file)) - { - Logger.Log($"Migrating {file} to default tournament storage."); - var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file)); - AttemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true)); - fileInfo.Delete(); - } + if (!storage.Exists(file)) + return; + + Logger.Log($"Migrating {file} to default tournament storage."); + var fileInfo = new System.IO.FileInfo(storage.GetFullPath(file)); + AttemptOperation(() => fileInfo.CopyTo(Path.Combine(destination.FullName, fileInfo.Name), true)); + fileInfo.Delete(); } } } From 0ca8c961c8148813726a1172cc680b6d10a0fffb Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 24 Jun 2020 00:04:57 +0200 Subject: [PATCH 1894/6909] Remove string interpolation & unnecessary test setup --- .../NonVisual/CustomTourneyDirectoryTest.cs | 5 ----- osu.Game.Tournament/Components/TourneyVideo.cs | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 9e6675e09f..29e1725c6d 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -18,11 +18,6 @@ namespace osu.Game.Tournament.Tests.NonVisual [TestFixture] public class CustomTourneyDirectoryTest { - [SetUp] - public void SetUp() - { - } - [Test] public void TestDefaultDirectory() { diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 5a595f4f44..0052b9a431 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tournament.Components [BackgroundDependencyLoader] private void load(TournamentStorage storage) { - var stream = storage.VideoStore.GetStream($@"{filename}"); + var stream = storage.VideoStore.GetStream(filename); if (stream != null) { From e5851be9ad222094f0d710c79ac8d5d8567d439d Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 24 Jun 2020 00:06:27 +0200 Subject: [PATCH 1895/6909] change accessor from internal readonly to public get-only Also changes the class accessor from internal to public --- osu.Game.Tournament/IO/TournamentStorage.cs | 2 +- osu.Game.Tournament/IO/TournamentVideoResourceStore.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 5f90598890..14ff8d59e5 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tournament.IO public class TournamentStorage : MigratableStorage { private readonly Storage storage; - internal readonly TournamentVideoResourceStore VideoStore; + public TournamentVideoResourceStore VideoStore { get; } private const string default_tournament = "default"; public TournamentStorage(Storage storage) diff --git a/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs index 1ccd20fe21..4b26840b79 100644 --- a/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs +++ b/osu.Game.Tournament/IO/TournamentVideoResourceStore.cs @@ -6,7 +6,7 @@ using osu.Framework.Platform; namespace osu.Game.Tournament.IO { - internal class TournamentVideoResourceStore : NamespacedResourceStore + public class TournamentVideoResourceStore : NamespacedResourceStore { public TournamentVideoResourceStore(Storage storage) : base(new StorageBackedResourceStore(storage), "videos") From 9d2392b6b1c8fccd9e15625a72cabf21419c6028 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 24 Jun 2020 00:14:44 +0200 Subject: [PATCH 1896/6909] Cache TournamentStorage as Storage and only cast when necessary --- osu.Game.Tournament/Components/TourneyVideo.cs | 7 ++++--- .../Screens/Drawings/Components/StorageBackedTeamList.cs | 6 +++--- osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs | 6 +++--- osu.Game.Tournament/TournamentGameBase.cs | 2 +- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 0052b9a431..17d4eb7a28 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -7,6 +7,7 @@ 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; using osu.Game.Tournament.IO; @@ -18,7 +19,6 @@ namespace osu.Game.Tournament.Components private readonly string filename; private readonly bool drawFallbackGradient; private Video video; - private ManualClock manualClock; public TourneyVideo(string filename, bool drawFallbackGradient = false) @@ -28,9 +28,10 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader] - private void load(TournamentStorage storage) + private void load(Storage storage) { - var stream = storage.VideoStore.GetStream(filename); + var tournamentStorage = storage as TournamentStorage; + var stream = tournamentStorage.VideoStore.GetStream(filename); if (stream != null) { diff --git a/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs index ecc23181be..f96ec01cbb 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/StorageBackedTeamList.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using osu.Framework.Logging; -using osu.Game.Tournament.IO; +using osu.Framework.Platform; using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Screens.Drawings.Components @@ -14,9 +14,9 @@ namespace osu.Game.Tournament.Screens.Drawings.Components { private const string teams_filename = "drawings.txt"; - private readonly TournamentStorage storage; + private readonly Storage storage; - public StorageBackedTeamList(TournamentStorage storage) + public StorageBackedTeamList(Storage storage) { this.storage = storage; } diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs index 8b6bd21ee6..e10154b722 100644 --- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs +++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs @@ -12,9 +12,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; +using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Tournament.Components; -using osu.Game.Tournament.IO; using osu.Game.Tournament.Models; using osu.Game.Tournament.Screens.Drawings.Components; using osuTK; @@ -36,12 +36,12 @@ namespace osu.Game.Tournament.Screens.Drawings private Task writeOp; - private TournamentStorage storage; + private Storage storage; public ITeamList TeamList; [BackgroundDependencyLoader] - private void load(TextureStore textures, TournamentStorage storage) + private void load(TextureStore textures, Storage storage) { RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 0702b435a5..6a533f96d8 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tournament { Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); - dependencies.CacheAs(storage = new TournamentStorage(baseStorage)); + dependencies.CacheAs(storage = new TournamentStorage(baseStorage)); Textures.AddStore(new TextureLoaderStore(storage.VideoStore)); From c32ef5e718c4a7df5908ea8dcbce9d998f3a1926 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 24 Jun 2020 00:37:29 +0200 Subject: [PATCH 1897/6909] Address formatting issues --- .../NonVisual/CustomDataDirectoryTest.cs | 4 ++-- osu.Game.Tournament/IO/TournamentStorage.cs | 2 +- osu.Game/IO/MigratableStorage.cs | 14 +++++------ osu.Game/IO/OsuStorage.cs | 24 +++++-------------- 4 files changed, 16 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 5278837073..125d2b3ef7 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -152,13 +152,13 @@ namespace osu.Game.Tests.NonVisual 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) + foreach (var file in osuStorage.IgnoreFiles) { Assert.That(host.Storage.Exists(file), Is.True); Assert.That(storage.Exists(file), Is.False); } - foreach (var dir in osuStorage.IGNORE_DIRECTORIES) + foreach (var dir in osuStorage.IgnoreDirectories) { Assert.That(host.Storage.ExistsDirectory(dir), Is.True); Assert.That(storage.ExistsDirectory(dir), Is.False); diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 14ff8d59e5..b906ea6c50 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -40,7 +40,7 @@ namespace osu.Game.Tournament.IO Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } - override public void Migrate(string newLocation) + public override void Migrate(string newLocation) { var source = new DirectoryInfo(storage.GetFullPath("tournament")); var destination = new DirectoryInfo(newLocation); diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 0f064dfe2d..41a057d016 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -14,21 +14,21 @@ namespace osu.Game.IO /// public abstract class MigratableStorage : WrappedStorage { - internal virtual string[] IGNORE_DIRECTORIES { get; } - internal virtual string[] IGNORE_FILES { get; } + internal virtual string[] IgnoreDirectories => new string[] { }; + internal virtual string[] IgnoreFiles => new string[] { }; protected MigratableStorage(Storage storage, string subPath = null) : base(storage, subPath) { } - abstract public void Migrate(string newLocation); + public abstract void Migrate(string newLocation); protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) { foreach (System.IO.FileInfo fi in target.GetFiles()) { - if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) + if (topLevelExcludes && IgnoreFiles.Contains(fi.Name)) continue; AttemptOperation(() => fi.Delete()); @@ -36,7 +36,7 @@ namespace osu.Game.IO foreach (DirectoryInfo dir in target.GetDirectories()) { - if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) + if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name)) continue; AttemptOperation(() => dir.Delete(true)); @@ -54,7 +54,7 @@ namespace osu.Game.IO foreach (System.IO.FileInfo fi in source.GetFiles()) { - if (topLevelExcludes && IGNORE_FILES.Contains(fi.Name)) + if (topLevelExcludes && IgnoreFiles.Contains(fi.Name)) continue; AttemptOperation(() => fi.CopyTo(Path.Combine(destination.FullName, fi.Name), true)); @@ -62,7 +62,7 @@ namespace osu.Game.IO foreach (DirectoryInfo dir in source.GetDirectories()) { - if (topLevelExcludes && IGNORE_DIRECTORIES.Contains(dir.Name)) + if (topLevelExcludes && IgnoreDirectories.Contains(dir.Name)) continue; CopyRecursive(dir, destination.CreateSubdirectory(dir.Name), false); diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 8890ecf843..514f172f74 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -14,25 +14,13 @@ namespace osu.Game.IO private readonly GameHost host; private readonly StorageConfigManager storageConfig; - internal override string[] IGNORE_DIRECTORIES - { - get - { - return new string[] { "cache" }; - } - } + internal override string[] IgnoreDirectories => new[] { "cache" }; - internal override string[] IGNORE_FILES + internal override string[] IgnoreFiles => new[] { - get - { - return new string[] - { - "framework.ini", - "storage.ini" - }; - } - } + "framework.ini", + "storage.ini" + }; public OsuStorage(GameHost host) : base(host.Storage, string.Empty) @@ -53,7 +41,7 @@ namespace osu.Game.IO Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs"); } - override public void Migrate(string newLocation) + public override void Migrate(string newLocation) { var source = new DirectoryInfo(GetFullPath(".")); var destination = new DirectoryInfo(newLocation); From af1134084948db534072997b41ee7a7e5758c2b0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 24 Jun 2020 02:13:28 +0200 Subject: [PATCH 1898/6909] Fix nullref exceptions and redundant explicit type --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 4 ++-- osu.Game.Tournament/Components/TourneyVideo.cs | 3 +-- osu.Game/IO/MigratableStorage.cs | 4 ++-- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 125d2b3ef7..c8a5988104 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -152,13 +152,13 @@ namespace osu.Game.Tests.NonVisual Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); - foreach (var file in osuStorage.IgnoreFiles) + foreach (var file in osuStorage?.IgnoreFiles ?? Array.Empty()) { Assert.That(host.Storage.Exists(file), Is.True); Assert.That(storage.Exists(file), Is.False); } - foreach (var dir in osuStorage.IgnoreDirectories) + foreach (var dir in osuStorage?.IgnoreDirectories ?? Array.Empty()) { Assert.That(host.Storage.ExistsDirectory(dir), Is.True); Assert.That(storage.ExistsDirectory(dir), Is.False); diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 17d4eb7a28..794b72b3a9 100644 --- a/osu.Game.Tournament/Components/TourneyVideo.cs +++ b/osu.Game.Tournament/Components/TourneyVideo.cs @@ -30,8 +30,7 @@ namespace osu.Game.Tournament.Components [BackgroundDependencyLoader] private void load(Storage storage) { - var tournamentStorage = storage as TournamentStorage; - var stream = tournamentStorage.VideoStore.GetStream(filename); + var stream = (storage as TournamentStorage)?.VideoStore.GetStream(filename); if (stream != null) { diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 41a057d016..ec85e0bac9 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -14,8 +14,8 @@ namespace osu.Game.IO /// public abstract class MigratableStorage : WrappedStorage { - internal virtual string[] IgnoreDirectories => new string[] { }; - internal virtual string[] IgnoreFiles => new string[] { }; + internal virtual string[] IgnoreDirectories => Array.Empty(); + internal virtual string[] IgnoreFiles => Array.Empty(); protected MigratableStorage(Storage storage, string subPath = null) : base(storage, subPath) From 839f197111c55d00474da01aced1a07e46a7fe69 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 24 Jun 2020 02:37:59 +0200 Subject: [PATCH 1899/6909] Change type from TournamentStorage to Storage in tests --- .../NonVisual/CustomTourneyDirectoryTest.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 29e1725c6d..4cede15d7a 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -10,7 +10,6 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; using osu.Game.Tournament.Configuration; -using osu.Game.Tournament.IO; using osu.Game.Tests; namespace osu.Game.Tournament.Tests.NonVisual @@ -26,7 +25,7 @@ namespace osu.Game.Tournament.Tests.NonVisual try { var osu = loadOsu(host); - var storage = osu.Dependencies.Get(); + var storage = osu.Dependencies.Get(); var defaultStorage = Path.Combine(tournamentBasePath(nameof(TestDefaultDirectory)), "default"); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorage)); } @@ -57,7 +56,7 @@ namespace osu.Game.Tournament.Tests.NonVisual { var osu = loadOsu(host); - storage = osu.Dependencies.Get(); + storage = osu.Dependencies.Get(); Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(tournamentBasePath(nameof(TestCustomDirectory)), "custom"))); } @@ -113,7 +112,7 @@ namespace osu.Game.Tournament.Tests.NonVisual { var osu = loadOsu(host); - var storage = osu.Dependencies.Get(); + var storage = osu.Dependencies.Get(); var migratedPath = Path.Combine(tournamentBasePath(nameof(TestMigration)), "default"); From c94f95cc0d6bf3ba92098d6fe5f3190d8ddf4153 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 24 Jun 2020 02:40:22 +0200 Subject: [PATCH 1900/6909] Check if the file exists before reading This is (also) to address the review from bdach about StorageManager initialising a default value that gets overwritten upon migration anyway. --- osu.Game.Tournament/IO/TournamentStorage.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index b906ea6c50..ed1bfb7449 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -22,11 +22,9 @@ namespace osu.Game.Tournament.IO TournamentStorageManager storageConfig = new TournamentStorageManager(storage); - var currentTournament = storageConfig.Get(StorageConfig.CurrentTournament); - - if (!string.IsNullOrEmpty(currentTournament)) + if (storage.Exists("tournament.ini")) { - ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(currentTournament)); + ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(storageConfig.Get(StorageConfig.CurrentTournament))); } else { From ccb27082d52c2bda1bfd01e4a5ca3583e49c3035 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Jun 2020 11:08:32 +0900 Subject: [PATCH 1901/6909] 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 1902/6909] 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 1903/6909] 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 1904/6909] 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 68f078c9e67d4239eaf02575e58c81b120e44197 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Sun, 5 Jan 2020 22:11:37 +0100 Subject: [PATCH 1905/6909] Replace logo-triangles.mp4 with shadered logo-triangles.png --- osu.Game/Screens/Menu/IntroTriangles.cs | 9 +- osu.Game/Screens/Menu/LazerLogo.cs | 109 ++++++++++++++++++++++++ 2 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Screens/Menu/LazerLogo.cs diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 9be74a0fd9..36f00a13ef 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.IO; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -12,7 +11,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; -using osu.Framework.Graphics.Video; using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Graphics; @@ -88,7 +86,7 @@ namespace osu.Game.Screens.Menu private RulesetFlow rulesets; private Container rulesetsScale; private Container logoContainerSecondary; - private Drawable lazerLogo; + private LazerLogo lazerLogo; private GlitchingTriangles triangles; @@ -139,10 +137,10 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = lazerLogo = new LazerLogo(textures.GetStream("Menu/logo-triangles.mp4")) + Child = lazerLogo = new LazerLogo() { Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Origin = Anchor.Centre } }, }; @@ -218,6 +216,7 @@ namespace osu.Game.Screens.Menu // matching flyte curve y = 0.25x^2 + (max(0, x - 0.7) / 0.3) ^ 5 lazerLogo.FadeIn().ScaleTo(scale_start).Then().Delay(logo_scale_duration * 0.7f).ScaleTo(scale_start - scale_adjust, logo_scale_duration * 0.3f, Easing.InQuint); + lazerLogo.Start(logo_1, logo_scale_duration); logoContainerSecondary.ScaleTo(scale_start).Then().ScaleTo(scale_start - scale_adjust * 0.25f, logo_scale_duration, Easing.InQuad); } diff --git a/osu.Game/Screens/Menu/LazerLogo.cs b/osu.Game/Screens/Menu/LazerLogo.cs new file mode 100644 index 0000000000..ab9c82bc58 --- /dev/null +++ b/osu.Game/Screens/Menu/LazerLogo.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; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Primitives; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Textures; +using osu.Framework.MathUtils; +using osuTK; + +namespace osu.Game.Screens.Menu +{ + public class LazerLogo : Drawable + { + private IShader shader; + private Texture texture; + + private double startTime = -1000; + private double animationTime = -1000; + + private float animation; + private float highlight; + + public LazerLogo() + { + Size = new Vector2(960); + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders, TextureStore textures) + { + shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LazerLogo"); + texture = textures.Get("Menu/logo-triangles.png"); + } + + public void Start(double delay, double duration) + { + startTime = Clock.CurrentTime + delay; + animationTime = duration; + } + + public override bool IsPresent => true; + + protected override void Update() + { + base.Update(); + + if (animationTime < 0) return; + + highlight = Clock.CurrentTime < startTime + 0.4 * animationTime + ? Interpolation.ValueAt(Clock.CurrentTime, 0f, 1f, startTime, startTime + animationTime * 1.07, Easing.OutCirc) + : Interpolation.ValueAt(Clock.CurrentTime, 0.6f, 1f, startTime, startTime + animationTime * 0.9); + + animation = Clock.CurrentTime < startTime + 0.5 * animationTime + ? Interpolation.ValueAt(Clock.CurrentTime, 0f, 0.8f, startTime, startTime + animationTime * 1.23, Easing.OutQuart) + : Interpolation.ValueAt(Clock.CurrentTime, 0.4f, 1f, startTime, startTime + animationTime); + } + + protected override DrawNode CreateDrawNode() => new LazerLogoDrawNode(this); + + private class LazerLogoDrawNode : DrawNode + { + protected new LazerLogo Source => (LazerLogo)base.Source; + + private IShader shader; + private Texture texture; + private Quad screenSpaceDrawQuad; + private float animation; + private float highlight; + + public LazerLogoDrawNode(LazerLogo source) + : base(source) + { + } + + public override void ApplyState() + { + base.ApplyState(); + + shader = Source.shader; + texture = Source.texture; + screenSpaceDrawQuad = Source.ScreenSpaceDrawQuad; + animation = Source.animation; + highlight = Source.highlight; + } + + protected virtual void Blit(Action vertexAction) + { + DrawQuad(texture, screenSpaceDrawQuad, DrawColourInfo.Colour, null, vertexAction); + } + + public override void Draw(Action vertexAction) + { + base.Draw(vertexAction); + + shader.Bind(); + shader.GetUniform("highlight").Value = highlight; + shader.GetUniform("animation").Value = animation; + + Blit(vertexAction); + + shader.Unbind(); + } + } + } +} From 5fd6246d1b5e3b0d650cf4117d10df84b6d9f5de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Jun 2020 17:57:07 +0900 Subject: [PATCH 1906/6909] 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 1907/6909] 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 1908/6909] 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 53107973a33a82bf5b1b70bb158d293b183536eb Mon Sep 17 00:00:00 2001 From: BananeVolante <42553638+BananeVolante@users.noreply.github.com> Date: Wed, 24 Jun 2020 14:01:13 +0200 Subject: [PATCH 1909/6909] merged 2 lines Co-authored-by: Salman Ahmed --- osu.Game/Screens/Play/PauseOverlay.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 191bf0d901..2656ef1ebd 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -34,11 +34,10 @@ namespace osu.Game.Screens.Play if (sampleChannel != null) { - pauseLoop = new DrawableSample(sampleChannel) + AddInternal(pauseLoop = new DrawableSample(sampleChannel) { Looping = true, - }; - AddInternal(pauseLoop); + }); pauseLoop?.Play(); } } From 768e28faba318a3c2ef46fbce030ffb52d98b060 Mon Sep 17 00:00:00 2001 From: jorolf Date: Wed, 24 Jun 2020 14:11:38 +0200 Subject: [PATCH 1910/6909] generalize and simplify animation --- .../UserInterface/TestSceneHueAnimation.cs | 29 +++++ osu.Game/Graphics/Sprites/HueAnimation.cs | 75 ++++++++++++ osu.Game/Screens/Menu/IntroTriangles.cs | 45 ++++++-- osu.Game/Screens/Menu/LazerLogo.cs | 109 ------------------ 4 files changed, 141 insertions(+), 117 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs create mode 100644 osu.Game/Graphics/Sprites/HueAnimation.cs delete mode 100644 osu.Game/Screens/Menu/LazerLogo.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs new file mode 100644 index 0000000000..582849a053 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Tests.Visual.UserInterface +{ + [TestFixture] + public class TestSceneHueAnimation : OsuTestScene + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + HueAnimation anim; + + Add(anim = new HueAnimation + { + Texture = textures.Get("Intro/Triangles/logo-triangles.png"), + Colour = Colour4.White, + }); + + AddSliderStep("Progress", 0f, 1f, 0f, newValue => anim.AnimationProgress = newValue); + } + } +} diff --git a/osu.Game/Graphics/Sprites/HueAnimation.cs b/osu.Game/Graphics/Sprites/HueAnimation.cs new file mode 100644 index 0000000000..55a167cf59 --- /dev/null +++ b/osu.Game/Graphics/Sprites/HueAnimation.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Shaders; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osuTK; + +namespace osu.Game.Graphics.Sprites +{ + public class HueAnimation : Sprite + { + public HueAnimation() + { + Size = new Vector2(960); + } + + [BackgroundDependencyLoader] + private void load(ShaderManager shaders, TextureStore textures) + { + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"HueAnimation"); + RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"HueAnimation"); // Masking isn't supported for now + } + + private float animationProgress; + + public float AnimationProgress + { + get => animationProgress; + set + { + if (animationProgress == value) return; + + animationProgress = value; + Invalidate(Invalidation.DrawInfo); + } + } + + public override bool IsPresent => true; + + protected override DrawNode CreateDrawNode() => new HueAnimationDrawNode(this); + + private class HueAnimationDrawNode : SpriteDrawNode + { + protected new HueAnimation Source => (HueAnimation)base.Source; + + private float progress; + + public HueAnimationDrawNode(HueAnimation source) + : base(source) + { + } + + public override void ApplyState() + { + base.ApplyState(); + + progress = Source.animationProgress; + } + + protected override void Blit(Action vertexAction) + { + Shader.GetUniform("progress").UpdateValue(ref progress); + + base.Blit(vertexAction); + } + + protected override bool CanDrawOpaqueInterior => false; + } + } +} diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 36f00a13ef..53eb0eb270 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -137,7 +138,7 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Child = lazerLogo = new LazerLogo() + Child = lazerLogo = new LazerLogo { Anchor = Anchor.Centre, Origin = Anchor.Centre @@ -216,7 +217,13 @@ namespace osu.Game.Screens.Menu // matching flyte curve y = 0.25x^2 + (max(0, x - 0.7) / 0.3) ^ 5 lazerLogo.FadeIn().ScaleTo(scale_start).Then().Delay(logo_scale_duration * 0.7f).ScaleTo(scale_start - scale_adjust, logo_scale_duration * 0.3f, Easing.InQuint); - lazerLogo.Start(logo_1, logo_scale_duration); + + lazerLogo.TransformTo(nameof(LazerLogo.Highlight), 0.6f, logo_scale_duration * 0.4f, Easing.OutCirc).Then() + .TransformTo(nameof(LazerLogo.Highlight), 1f, logo_scale_duration * 0.4f); + + lazerLogo.TransformTo(nameof(LazerLogo.Animation), 0.4f, logo_scale_duration * 0.5f, Easing.OutQuart).Then() + .TransformTo(nameof(LazerLogo.Animation), 1f, logo_scale_duration * 0.4f); + logoContainerSecondary.ScaleTo(scale_start).Then().ScaleTo(scale_start - scale_adjust * 0.25f, logo_scale_duration, Easing.InQuad); } @@ -258,20 +265,42 @@ namespace osu.Game.Screens.Menu private class LazerLogo : CompositeDrawable { - private readonly Stream videoStream; + private HueAnimation highlight, animation; - public LazerLogo(Stream videoStream) + public float Highlight + { + get => highlight.AnimationProgress; + set => highlight.AnimationProgress = value; + } + + public float Animation + { + get => animation.AnimationProgress; + set => animation.AnimationProgress = value; + } + + public LazerLogo() { - this.videoStream = videoStream; Size = new Vector2(960); } [BackgroundDependencyLoader] - private void load() + private void load(TextureStore textures) { - InternalChild = new Video(videoStream) + const string lazer_logo_texture = @"Intro/Triangles/logo-triangles"; + + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, + highlight = new HueAnimation + { + Texture = textures.Get(lazer_logo_texture), + Colour = OsuColour.Gray(0.6f).Opacity(0.8f), + }, + animation = new HueAnimation + { + Texture = textures.Get(lazer_logo_texture), + Colour = Color4.White.Opacity(0.8f), + }, }; } } diff --git a/osu.Game/Screens/Menu/LazerLogo.cs b/osu.Game/Screens/Menu/LazerLogo.cs deleted file mode 100644 index ab9c82bc58..0000000000 --- a/osu.Game/Screens/Menu/LazerLogo.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.OpenGL.Vertices; -using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shaders; -using osu.Framework.Graphics.Textures; -using osu.Framework.MathUtils; -using osuTK; - -namespace osu.Game.Screens.Menu -{ - public class LazerLogo : Drawable - { - private IShader shader; - private Texture texture; - - private double startTime = -1000; - private double animationTime = -1000; - - private float animation; - private float highlight; - - public LazerLogo() - { - Size = new Vector2(960); - } - - [BackgroundDependencyLoader] - private void load(ShaderManager shaders, TextureStore textures) - { - shader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LazerLogo"); - texture = textures.Get("Menu/logo-triangles.png"); - } - - public void Start(double delay, double duration) - { - startTime = Clock.CurrentTime + delay; - animationTime = duration; - } - - public override bool IsPresent => true; - - protected override void Update() - { - base.Update(); - - if (animationTime < 0) return; - - highlight = Clock.CurrentTime < startTime + 0.4 * animationTime - ? Interpolation.ValueAt(Clock.CurrentTime, 0f, 1f, startTime, startTime + animationTime * 1.07, Easing.OutCirc) - : Interpolation.ValueAt(Clock.CurrentTime, 0.6f, 1f, startTime, startTime + animationTime * 0.9); - - animation = Clock.CurrentTime < startTime + 0.5 * animationTime - ? Interpolation.ValueAt(Clock.CurrentTime, 0f, 0.8f, startTime, startTime + animationTime * 1.23, Easing.OutQuart) - : Interpolation.ValueAt(Clock.CurrentTime, 0.4f, 1f, startTime, startTime + animationTime); - } - - protected override DrawNode CreateDrawNode() => new LazerLogoDrawNode(this); - - private class LazerLogoDrawNode : DrawNode - { - protected new LazerLogo Source => (LazerLogo)base.Source; - - private IShader shader; - private Texture texture; - private Quad screenSpaceDrawQuad; - private float animation; - private float highlight; - - public LazerLogoDrawNode(LazerLogo source) - : base(source) - { - } - - public override void ApplyState() - { - base.ApplyState(); - - shader = Source.shader; - texture = Source.texture; - screenSpaceDrawQuad = Source.ScreenSpaceDrawQuad; - animation = Source.animation; - highlight = Source.highlight; - } - - protected virtual void Blit(Action vertexAction) - { - DrawQuad(texture, screenSpaceDrawQuad, DrawColourInfo.Colour, null, vertexAction); - } - - public override void Draw(Action vertexAction) - { - base.Draw(vertexAction); - - shader.Bind(); - shader.GetUniform("highlight").Value = highlight; - shader.GetUniform("animation").Value = animation; - - Blit(vertexAction); - - shader.Unbind(); - } - } - } -} From 2e8f30461f63db8c651b20a463f8e4f34ae23c8a Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Wed, 24 Jun 2020 14:22:12 +0200 Subject: [PATCH 1911/6909] play/stops music when entering the pause overlay, instead of letting it play silently in the background --- osu.Game/Screens/Play/PauseOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 2656ef1ebd..8b35c69aa7 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -38,13 +38,13 @@ namespace osu.Game.Screens.Play { Looping = true, }); - pauseLoop?.Play(); } } protected override void PopIn() { base.PopIn(); + pauseLoop?.Play(); pauseLoop?.VolumeTo(1.0f, 400, Easing.InQuint); } @@ -52,6 +52,7 @@ namespace osu.Game.Screens.Play { base.PopOut(); pauseLoop?.VolumeTo(0.0f); + pauseLoop?.Stop(); } } } From 0d3bc1ac29685628e57cceddd94d525b6ef48ea2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Jun 2020 22:29:30 +0900 Subject: [PATCH 1912/6909] 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 6f0ec36407510e54c49b4cef6710bf620c35a564 Mon Sep 17 00:00:00 2001 From: jorolf Date: Wed, 24 Jun 2020 16:27:00 +0200 Subject: [PATCH 1913/6909] remove size from hue animation --- .../Visual/UserInterface/TestSceneHueAnimation.cs | 6 ++++-- osu.Game/Graphics/Sprites/HueAnimation.cs | 6 ------ osu.Game/Screens/Menu/IntroTriangles.cs | 2 ++ 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs index 582849a053..85ddfb08f9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs @@ -13,13 +13,15 @@ namespace osu.Game.Tests.Visual.UserInterface public class TestSceneHueAnimation : OsuTestScene { [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { HueAnimation anim; Add(anim = new HueAnimation { - Texture = textures.Get("Intro/Triangles/logo-triangles.png"), + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Texture = textures.Get("Intro/Triangles/logo-triangles"), Colour = Colour4.White, }); diff --git a/osu.Game/Graphics/Sprites/HueAnimation.cs b/osu.Game/Graphics/Sprites/HueAnimation.cs index 55a167cf59..4f2bafe27f 100644 --- a/osu.Game/Graphics/Sprites/HueAnimation.cs +++ b/osu.Game/Graphics/Sprites/HueAnimation.cs @@ -8,17 +8,11 @@ using osu.Framework.Graphics.OpenGL.Vertices; using osu.Framework.Graphics.Shaders; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osuTK; namespace osu.Game.Graphics.Sprites { public class HueAnimation : Sprite { - public HueAnimation() - { - Size = new Vector2(960); - } - [BackgroundDependencyLoader] private void load(ShaderManager shaders, TextureStore textures) { diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 53eb0eb270..95fc101094 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -293,11 +293,13 @@ namespace osu.Game.Screens.Menu { highlight = new HueAnimation { + RelativeSizeAxes = Axes.Both, Texture = textures.Get(lazer_logo_texture), Colour = OsuColour.Gray(0.6f).Opacity(0.8f), }, animation = new HueAnimation { + RelativeSizeAxes = Axes.Both, Texture = textures.Get(lazer_logo_texture), Colour = Color4.White.Opacity(0.8f), }, 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 1914/6909] 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 1915/6909] 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 1916/6909] 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 063503f4db9ec775ffaa1204fc2e69a55d25329c Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 24 Jun 2020 20:43:56 +0200 Subject: [PATCH 1917/6909] Move null check outside of the loop --- .../NonVisual/CustomDataDirectoryTest.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index c8a5988104..4149e3d3ef 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -152,16 +152,19 @@ namespace osu.Game.Tests.NonVisual Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); - foreach (var file in osuStorage?.IgnoreFiles ?? Array.Empty()) + if (osuStorage != null) { - Assert.That(host.Storage.Exists(file), Is.True); - Assert.That(storage.Exists(file), Is.False); - } + foreach (var file in osuStorage.IgnoreFiles) + { + Assert.That(host.Storage.Exists(file), Is.True); + Assert.That(storage.Exists(file), Is.False); + } - foreach (var dir in osuStorage?.IgnoreDirectories ?? Array.Empty()) - { - Assert.That(host.Storage.ExistsDirectory(dir), Is.True); - Assert.That(storage.ExistsDirectory(dir), Is.False); + foreach (var dir in osuStorage.IgnoreDirectories) + { + 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}")); From 47a732ef604ac2f675ef71683d69eb10930d4785 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 24 Jun 2020 23:01:56 +0200 Subject: [PATCH 1918/6909] Address review comments Now asserting instead of an if-statement, change cast from OsuStorage to MigratableStorage and make internal virtual properties protected. --- .../NonVisual/CustomDataDirectoryTest.cs | 25 +++++++++---------- osu.Game/IO/MigratableStorage.cs | 4 +-- osu.Game/IO/OsuStorage.cs | 4 +-- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 4149e3d3ef..43c1c77786 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -126,7 +126,7 @@ namespace osu.Game.Tests.NonVisual { var osu = loadOsu(host); var storage = osu.Dependencies.Get(); - var osuStorage = storage as OsuStorage; + var osuStorage = storage as MigratableStorage; // ensure we perform a save host.Dependencies.Get().Save(); @@ -152,19 +152,18 @@ namespace osu.Game.Tests.NonVisual Assert.That(!host.Storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); Assert.That(storage.ExistsDirectory(Path.Combine("test-nested", "cache"))); - if (osuStorage != null) - { - foreach (var file in osuStorage.IgnoreFiles) - { - Assert.That(host.Storage.Exists(file), Is.True); - Assert.That(storage.Exists(file), Is.False); - } + Assert.That(osuStorage, Is.Not.Null); - foreach (var dir in osuStorage.IgnoreDirectories) - { - Assert.That(host.Storage.ExistsDirectory(dir), Is.True); - Assert.That(storage.ExistsDirectory(dir), Is.False); - } + foreach (var file in osuStorage.IgnoreFiles) + { + Assert.That(host.Storage.Exists(file), Is.True); + Assert.That(storage.Exists(file), Is.False); + } + + foreach (var dir in osuStorage.IgnoreDirectories) + { + 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}")); diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index ec85e0bac9..faa39d2ef8 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -14,8 +14,8 @@ namespace osu.Game.IO /// public abstract class MigratableStorage : WrappedStorage { - internal virtual string[] IgnoreDirectories => Array.Empty(); - internal virtual string[] IgnoreFiles => Array.Empty(); + public virtual string[] IgnoreDirectories => Array.Empty(); + public virtual string[] IgnoreFiles => Array.Empty(); protected MigratableStorage(Storage storage, string subPath = null) : base(storage, subPath) diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 514f172f74..31ee802141 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.cs @@ -14,9 +14,9 @@ namespace osu.Game.IO private readonly GameHost host; private readonly StorageConfigManager storageConfig; - internal override string[] IgnoreDirectories => new[] { "cache" }; + public override string[] IgnoreDirectories => new[] { "cache" }; - internal override string[] IgnoreFiles => new[] + public override string[] IgnoreFiles => new[] { "framework.ini", "storage.ini" From 1409ace282035eb5c5b01e5d2e099049c0dad829 Mon Sep 17 00:00:00 2001 From: jorolf Date: Thu, 25 Jun 2020 00:59:12 +0200 Subject: [PATCH 1919/6909] apply suggestions --- osu.Game/Graphics/Sprites/HueAnimation.cs | 4 ++-- osu.Game/Screens/Menu/IntroTriangles.cs | 26 +++++++++++------------ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Graphics/Sprites/HueAnimation.cs b/osu.Game/Graphics/Sprites/HueAnimation.cs index 4f2bafe27f..82ac1aad36 100644 --- a/osu.Game/Graphics/Sprites/HueAnimation.cs +++ b/osu.Game/Graphics/Sprites/HueAnimation.cs @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.Sprites private class HueAnimationDrawNode : SpriteDrawNode { - protected new HueAnimation Source => (HueAnimation)base.Source; + private HueAnimation source => (HueAnimation)base.Source; private float progress; @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.Sprites { base.ApplyState(); - progress = Source.animationProgress; + progress = source.animationProgress; } protected override void Blit(Action vertexAction) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 95fc101094..2074fc7081 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -218,11 +218,11 @@ namespace osu.Game.Screens.Menu // matching flyte curve y = 0.25x^2 + (max(0, x - 0.7) / 0.3) ^ 5 lazerLogo.FadeIn().ScaleTo(scale_start).Then().Delay(logo_scale_duration * 0.7f).ScaleTo(scale_start - scale_adjust, logo_scale_duration * 0.3f, Easing.InQuint); - lazerLogo.TransformTo(nameof(LazerLogo.Highlight), 0.6f, logo_scale_duration * 0.4f, Easing.OutCirc).Then() - .TransformTo(nameof(LazerLogo.Highlight), 1f, logo_scale_duration * 0.4f); + lazerLogo.TransformTo(nameof(LazerLogo.OutlineHighlight), 0.6f, logo_scale_duration * 0.4f, Easing.OutCirc).Then() + .TransformTo(nameof(LazerLogo.OutlineHighlight), 1f, logo_scale_duration * 0.4f); - lazerLogo.TransformTo(nameof(LazerLogo.Animation), 0.4f, logo_scale_duration * 0.5f, Easing.OutQuart).Then() - .TransformTo(nameof(LazerLogo.Animation), 1f, logo_scale_duration * 0.4f); + lazerLogo.TransformTo(nameof(LazerLogo.Outline), 0.4f, logo_scale_duration * 0.5f, Easing.OutQuart).Then() + .TransformTo(nameof(LazerLogo.Outline), 1f, logo_scale_duration * 0.4f); logoContainerSecondary.ScaleTo(scale_start).Then().ScaleTo(scale_start - scale_adjust * 0.25f, logo_scale_duration, Easing.InQuad); } @@ -265,18 +265,18 @@ namespace osu.Game.Screens.Menu private class LazerLogo : CompositeDrawable { - private HueAnimation highlight, animation; + private HueAnimation outlineHighlight, outline; - public float Highlight + public float OutlineHighlight { - get => highlight.AnimationProgress; - set => highlight.AnimationProgress = value; + get => outlineHighlight.AnimationProgress; + set => outlineHighlight.AnimationProgress = value; } - public float Animation + public float Outline { - get => animation.AnimationProgress; - set => animation.AnimationProgress = value; + get => outline.AnimationProgress; + set => outline.AnimationProgress = value; } public LazerLogo() @@ -291,13 +291,13 @@ namespace osu.Game.Screens.Menu InternalChildren = new Drawable[] { - highlight = new HueAnimation + outlineHighlight = new HueAnimation { RelativeSizeAxes = Axes.Both, Texture = textures.Get(lazer_logo_texture), Colour = OsuColour.Gray(0.6f).Opacity(0.8f), }, - animation = new HueAnimation + outline = new HueAnimation { RelativeSizeAxes = Axes.Both, Texture = textures.Get(lazer_logo_texture), From ac5cd8f25a3a1f08c3adc3fe562e0fbdc5b0585c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 13:40:26 +0900 Subject: [PATCH 1920/6909] 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 1921/6909] 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 1922/6909] 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 1923/6909] 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 1924/6909] 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 1925/6909] 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 1926/6909] 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 1927/6909] 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 1928/6909] 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 9e5cc1b7a2d19bc7974065d9ed515425acf559dc Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Thu, 25 Jun 2020 13:26:42 +0200 Subject: [PATCH 1929/6909] added skin support for the pause loop --- osu.Game/Screens/Play/PauseOverlay.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 8b35c69aa7..fc13743fe5 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -7,7 +7,9 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; +using osu.Game.Audio; using osu.Game.Graphics; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Screens.Play @@ -24,13 +26,13 @@ namespace osu.Game.Screens.Play protected override Action BackAction => () => InternalButtons.Children.First().Click(); [BackgroundDependencyLoader] - private void load(OsuColour colours, AudioManager audio) + private void load(OsuColour colours, AudioManager audio, SkinManager skins) { AddButton("Continue", colours.Green, () => OnResume?.Invoke()); AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); - var sampleChannel = audio.Samples.Get(@"Gameplay/pause-loop"); + var sampleChannel = skins.GetSample(new SampleInfo("pause-loop")) ?? audio.Samples.Get(@"Gameplay/pause-loop"); if (sampleChannel != null) { From 54f087b933ef1c6df225508271c3d5c634454d69 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Jun 2020 18:59:14 +0900 Subject: [PATCH 1930/6909] 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 1931/6909] 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 1932/6909] 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 1933/6909] 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 1934/6909] 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 1935/6909] 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 1936/6909] 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 1937/6909] 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 1938/6909] 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 1939/6909] 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 1940/6909] 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 7d2d6a52c92a6871d9443f1700721eb837716d0e Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Thu, 25 Jun 2020 18:58:04 +0200 Subject: [PATCH 1941/6909] now uses SkinnableSample instead of Drawable sample Still does not support switching skins after Pause overlay loading, there will be no sound for the first pause (works fine the the nexts) Also, the pause loop seems to play for approximately 1 second when exiting the screens via restart or quit finally, since SkinnableSound does not play a sound if its aggregate volume is at 0, i had turn up the volume a bit before playing the loop --- osu.Game/Screens/Play/PauseOverlay.cs | 29 +++++++++++++++------------ 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index fc13743fe5..990d85b1cf 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -3,8 +3,11 @@ using System; using System.Linq; +using Humanizer; +using NUnit.Framework.Internal; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Game.Audio; @@ -21,40 +24,40 @@ namespace osu.Game.Screens.Play public override string Header => "paused"; public override string Description => "you're not going to do what i think you're going to do, are ya?"; - private DrawableSample pauseLoop; + private SkinnableSound pauseLoop; protected override Action BackAction => () => InternalButtons.Children.First().Click(); [BackgroundDependencyLoader] - private void load(OsuColour colours, AudioManager audio, SkinManager skins) + private void load(OsuColour colours) { AddButton("Continue", colours.Green, () => OnResume?.Invoke()); AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); - var sampleChannel = skins.GetSample(new SampleInfo("pause-loop")) ?? audio.Samples.Get(@"Gameplay/pause-loop"); - - if (sampleChannel != null) + AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("pause-loop")) { - AddInternal(pauseLoop = new DrawableSample(sampleChannel) - { - Looping = true, - }); - } + Looping = true, + }); + } protected override void PopIn() { base.PopIn(); + + //SkinnableSound only plays a sound if its aggregate volume is > 0, so the volume must be turned up before playing it + pauseLoop?.TransformBindableTo(pauseLoop.Volume, 0.00001); + pauseLoop?.TransformBindableTo(pauseLoop.Volume, 1.0f, 400, Easing.InQuint); pauseLoop?.Play(); - pauseLoop?.VolumeTo(1.0f, 400, Easing.InQuint); } protected override void PopOut() { base.PopOut(); - pauseLoop?.VolumeTo(0.0f); - pauseLoop?.Stop(); + pauseLoop?.Stop(); + pauseLoop?.TransformBindableTo(pauseLoop.Volume, 0.0f); } + } } From d82d901542ddb0d4018caf6b89ddc64d307abd8d Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 25 Jun 2020 20:36:55 +0200 Subject: [PATCH 1942/6909] Reuse custom_tournament where it was still used as a literal --- .../NonVisual/CustomTourneyDirectoryTest.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index 4cede15d7a..ce0ceae2e1 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Tests.NonVisual storage = osu.Dependencies.Get(); - Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(tournamentBasePath(nameof(TestCustomDirectory)), "custom"))); + Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(tournamentBasePath(nameof(TestCustomDirectory)), custom_tournament))); } finally { @@ -80,6 +80,7 @@ namespace osu.Game.Tournament.Tests.NonVisual // Recreate the old setup that uses "tournament" as the base path. string oldPath = Path.Combine(osuRoot, "tournament"); + string videosPath = Path.Combine(oldPath, "videos"); string modsPath = Path.Combine(oldPath, "mods"); string flagsPath = Path.Combine(oldPath, "flags"); From e3d654d33f2f5d1daeed1f72e263e1e943104ed3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 26 Jun 2020 20:14:02 +0900 Subject: [PATCH 1943/6909] 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 1944/6909] 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 1945/6909] 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 1946/6909] 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 a4bb238c4534c4bce43404c79b2e2402fb78b3d1 Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Fri, 26 Jun 2020 14:07:27 +0200 Subject: [PATCH 1947/6909] fixed bug preventing the pause loop from playing during the first pause after changing a skin --- osu.Game/Screens/Play/PauseOverlay.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 990d85b1cf..81c288f928 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -3,13 +3,8 @@ using System; using System.Linq; -using Humanizer; -using NUnit.Framework.Internal; using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Graphics; -using osu.Framework.Graphics.Audio; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Skinning; @@ -39,7 +34,9 @@ namespace osu.Game.Screens.Play { Looping = true, }); - + // PopIn is called before updating the skin, and when a sample is updated, its "playing" value is reset + // the sample must be played again(and if it plays when it shouldn't, the volume will be at 0) + pauseLoop.OnSkinChanged += () => pauseLoop.Play(); } protected override void PopIn() @@ -55,9 +52,9 @@ namespace osu.Game.Screens.Play protected override void PopOut() { base.PopOut(); - pauseLoop?.Stop(); + + pauseLoop?.Stop(); pauseLoop?.TransformBindableTo(pauseLoop.Volume, 0.0f); } - } } From 97a212a7f6a35a17a098e9ae11ff2a9b27833666 Mon Sep 17 00:00:00 2001 From: Power Maker Date: Fri, 26 Jun 2020 14:32:01 +0200 Subject: [PATCH 1948/6909] 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 1949/6909] 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 1950/6909] 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 1951/6909] 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 1952/6909] 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 1953/6909] 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 1954/6909] 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 1955/6909] 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 1956/6909] 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 1957/6909] 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 1958/6909] 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 1959/6909] 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 1960/6909] 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 1961/6909] 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 1962/6909] 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 0cddb85f1b83eb1c2b7a4dab43ec17b2f4e35cee Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 28 Jun 2020 15:27:50 +0200 Subject: [PATCH 1963/6909] Move storageconfig set and saving to migrate method --- .../NonVisual/CustomTourneyDirectoryTest.cs | 2 +- osu.Game.Tournament/IO/TournamentStorage.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index ce0ceae2e1..b75a9a6929 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tournament.Tests.NonVisual // Recreate the old setup that uses "tournament" as the base path. string oldPath = Path.Combine(osuRoot, "tournament"); - + string videosPath = Path.Combine(oldPath, "videos"); string modsPath = Path.Combine(oldPath, "mods"); string flagsPath = Path.Combine(oldPath, "flags"); diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index ed1bfb7449..ddc298a7ea 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -11,16 +11,17 @@ namespace osu.Game.Tournament.IO { public class TournamentStorage : MigratableStorage { - private readonly Storage storage; - public TournamentVideoResourceStore VideoStore { get; } private const string default_tournament = "default"; + private readonly Storage storage; + private readonly TournamentStorageManager storageConfig; + public TournamentVideoResourceStore VideoStore { get; } public TournamentStorage(Storage storage) : base(storage.GetStorageForDirectory("tournaments"), string.Empty) { this.storage = storage; - TournamentStorageManager storageConfig = new TournamentStorageManager(storage); + storageConfig = new TournamentStorageManager(storage); if (storage.Exists("tournament.ini")) { @@ -29,8 +30,6 @@ namespace osu.Game.Tournament.IO else { Migrate(GetFullPath(default_tournament)); - storageConfig.Set(StorageConfig.CurrentTournament, default_tournament); - storageConfig.Save(); ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_tournament)); } @@ -54,6 +53,8 @@ namespace osu.Game.Tournament.IO moveFileIfExists("drawings.txt", destination); moveFileIfExists("drawings_results.txt", destination); moveFileIfExists("drawings.ini", destination); + storageConfig.Set(StorageConfig.CurrentTournament, default_tournament); + storageConfig.Save(); } private void moveFileIfExists(string file, DirectoryInfo destination) From 006adf0fb50a903c67c7317b3c721ff653615506 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 28 Jun 2020 22:45:13 +0900 Subject: [PATCH 1964/6909] 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 820056cc4e2e1497a53a3d9af7db3594940e5fcd Mon Sep 17 00:00:00 2001 From: jorolf Date: Sun, 28 Jun 2020 17:53:53 +0200 Subject: [PATCH 1965/6909] update colours/transformations --- osu.Game/Screens/Menu/IntroTriangles.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 2074fc7081..8118491c36 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Screens; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -218,11 +217,14 @@ namespace osu.Game.Screens.Menu // matching flyte curve y = 0.25x^2 + (max(0, x - 0.7) / 0.3) ^ 5 lazerLogo.FadeIn().ScaleTo(scale_start).Then().Delay(logo_scale_duration * 0.7f).ScaleTo(scale_start - scale_adjust, logo_scale_duration * 0.3f, Easing.InQuint); - lazerLogo.TransformTo(nameof(LazerLogo.OutlineHighlight), 0.6f, logo_scale_duration * 0.4f, Easing.OutCirc).Then() - .TransformTo(nameof(LazerLogo.OutlineHighlight), 1f, logo_scale_duration * 0.4f); + const double highlight_duration = logo_scale_duration / 1.4; - lazerLogo.TransformTo(nameof(LazerLogo.Outline), 0.4f, logo_scale_duration * 0.5f, Easing.OutQuart).Then() - .TransformTo(nameof(LazerLogo.Outline), 1f, logo_scale_duration * 0.4f); + //Since we only have one texture, roughly align it by changing the timing + lazerLogo.Outline = -0.4f; + lazerLogo.TransformTo(nameof(LazerLogo.Outline), 1f, highlight_duration * 1.4); + + lazerLogo.OutlineHighlight = 0f; + lazerLogo.TransformTo(nameof(LazerLogo.OutlineHighlight), 1f, highlight_duration); logoContainerSecondary.ScaleTo(scale_start).Then().ScaleTo(scale_start - scale_adjust * 0.25f, logo_scale_duration, Easing.InQuad); } @@ -295,13 +297,13 @@ namespace osu.Game.Screens.Menu { RelativeSizeAxes = Axes.Both, Texture = textures.Get(lazer_logo_texture), - Colour = OsuColour.Gray(0.6f).Opacity(0.8f), + Colour = OsuColour.Gray(0.8f), }, outline = new HueAnimation { RelativeSizeAxes = Axes.Both, Texture = textures.Get(lazer_logo_texture), - Colour = Color4.White.Opacity(0.8f), + Colour = OsuColour.Gray(0.6f * 0.8f), }, }; } From 79eca8e1bffed2138d9b89cb1216d013205e5a74 Mon Sep 17 00:00:00 2001 From: jorolf Date: Sun, 28 Jun 2020 17:55:01 +0200 Subject: [PATCH 1966/6909] remove unneeded "base." --- osu.Game/Graphics/Sprites/HueAnimation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Sprites/HueAnimation.cs b/osu.Game/Graphics/Sprites/HueAnimation.cs index 82ac1aad36..8ad68ace05 100644 --- a/osu.Game/Graphics/Sprites/HueAnimation.cs +++ b/osu.Game/Graphics/Sprites/HueAnimation.cs @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.Sprites private class HueAnimationDrawNode : SpriteDrawNode { - private HueAnimation source => (HueAnimation)base.Source; + private HueAnimation source => (HueAnimation)Source; private float progress; 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 1967/6909] 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 1968/6909] 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 1969/6909] 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 1970/6909] 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 1971/6909] 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 1972/6909] 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 1973/6909] 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 1974/6909] 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 1975/6909] 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 1976/6909] 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 1977/6909] 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 1978/6909] 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 641ea5b950f6087d79b24b8339e2f5fa9b4bc10a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 30 Jun 2020 13:12:33 +0200 Subject: [PATCH 1979/6909] Make the disabling of the win key during gameplay a toggleable setting. --- osu.Game/Configuration/OsuConfigManager.cs | 4 +++- .../Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 9d31bc9bba..e7a86e080d 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -98,6 +98,7 @@ namespace osu.Game.Configuration Set(OsuSetting.ScoreDisplayMode, ScoringMode.Standardised); Set(OsuSetting.IncreaseFirstObjectVisibility, true); + Set(OsuSetting.GameplayDisableWinKey, true); // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -227,6 +228,7 @@ namespace osu.Game.Configuration IntroSequence, UIHoldActivationDelay, HitLighting, - MenuBackgroundSource + MenuBackgroundSource, + GameplayDisableWinKey } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 93a02ea0e4..60197c62b5 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -76,6 +76,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { LabelText = "Score display mode", Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) + }, + new SettingsCheckbox + { + LabelText = "Disable Win key during gameplay", + Bindable = config.GetBindable(OsuSetting.GameplayDisableWinKey) } }; } From 85c42456f25c9a27cd4871fb2cc71e1e3caf1b17 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 30 Jun 2020 21:38:51 +0900 Subject: [PATCH 1980/6909] 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 1981/6909] 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 1982/6909] 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 1983/6909] 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 1984/6909] 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 1985/6909] 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 1986/6909] 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 1987/6909] 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 1988/6909] 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 ab134c0ed7f92a2c83a503c679a18d1af1e8a1bc Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Wed, 1 Jul 2020 13:27:33 +0200 Subject: [PATCH 1989/6909] removed unneeded information in a comment --- osu.Game/Screens/Play/PauseOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 81c288f928..56d0e2d958 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Play Looping = true, }); // PopIn is called before updating the skin, and when a sample is updated, its "playing" value is reset - // the sample must be played again(and if it plays when it shouldn't, the volume will be at 0) + // the sample must be played again pauseLoop.OnSkinChanged += () => pauseLoop.Play(); } From ab1eb469af357ecde23288cd14294e91c54dbe7e Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Wed, 1 Jul 2020 13:30:23 +0200 Subject: [PATCH 1990/6909] removed unneeded null checks --- osu.Game/Screens/Play/PauseOverlay.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 56d0e2d958..022183d82b 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -44,17 +44,17 @@ namespace osu.Game.Screens.Play base.PopIn(); //SkinnableSound only plays a sound if its aggregate volume is > 0, so the volume must be turned up before playing it - pauseLoop?.TransformBindableTo(pauseLoop.Volume, 0.00001); - pauseLoop?.TransformBindableTo(pauseLoop.Volume, 1.0f, 400, Easing.InQuint); - pauseLoop?.Play(); + pauseLoop.TransformBindableTo(pauseLoop.Volume, 0.00001); + pauseLoop.TransformBindableTo(pauseLoop.Volume, 1.0f, 400, Easing.InQuint); + pauseLoop.Play(); } protected override void PopOut() { base.PopOut(); - pauseLoop?.Stop(); - pauseLoop?.TransformBindableTo(pauseLoop.Volume, 0.0f); + pauseLoop.Stop(); + pauseLoop.TransformBindableTo(pauseLoop.Volume, 0.0f); } } } From 1edfac4923623a1d78b1379c4f2c7e8e4177a01b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 1 Jul 2020 23:21:08 +0900 Subject: [PATCH 1991/6909] 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 fc1eb42a650fef5497bec37e20b5e2a29f773c07 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 1 Jul 2020 17:15:41 +0200 Subject: [PATCH 1992/6909] Disable windows key while in gameplay. --- osu.Desktop/OsuGameDesktop.cs | 4 + osu.Desktop/Windows/GameplayWinKeyHandler.cs | 39 ++++++++++ osu.Desktop/Windows/WindowsKey.cs | 82 ++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 osu.Desktop/Windows/GameplayWinKeyHandler.cs create mode 100644 osu.Desktop/Windows/WindowsKey.cs diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index cd31df316a..d05a4af126 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -16,6 +16,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Screens.Menu; using osu.Game.Updater; +using osu.Desktop.Windows; namespace osu.Desktop { @@ -98,6 +99,9 @@ namespace osu.Desktop LoadComponentAsync(versionManager = new VersionManager { Depth = int.MinValue }, Add); LoadComponentAsync(new DiscordRichPresence(), Add); + + if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + LoadComponentAsync(new GameplayWinKeyHandler(), Add); } protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs new file mode 100644 index 0000000000..cc0150497b --- /dev/null +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Game.Configuration; + +namespace osu.Desktop.Windows +{ + public class GameplayWinKeyHandler : Component + { + private Bindable winKeyEnabled; + private Bindable disableWinKey; + + private GameHost host; + + [BackgroundDependencyLoader] + private void load(GameHost host, OsuConfigManager config) + { + this.host = host; + + winKeyEnabled = host.AllowScreenSuspension.GetBoundCopy(); + winKeyEnabled.ValueChanged += toggleWinKey; + + disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); + disableWinKey.BindValueChanged(t => winKeyEnabled.TriggerChange()); + } + + private void toggleWinKey(ValueChangedEvent e) + { + if (!e.NewValue && disableWinKey.Value) + host.InputThread.Scheduler.Add(WindowsKey.Disable); + else + host.InputThread.Scheduler.Add(WindowsKey.Enable); + } + } +} diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs new file mode 100644 index 0000000000..748d9c55d6 --- /dev/null +++ b/osu.Desktop/Windows/WindowsKey.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 System.Runtime.InteropServices; + +namespace osu.Desktop.Windows +{ + internal class WindowsKey + { + private delegate int LowLevelKeyboardProcDelegate(int nCode, int wParam, ref KdDllHookStruct lParam); + + private static bool isBlocked; + + private const int wh_keyboard_ll = 13; + private const int wm_keydown = 256; + private const int wm_syskeyup = 261; + + //Resharper disable once NotAccessedField.Local + private static LowLevelKeyboardProcDelegate keyboardHookDelegate; // keeping a reference alive for the GC + private static IntPtr keyHook; + + [StructLayout(LayoutKind.Explicit)] + private struct KdDllHookStruct + { + [FieldOffset(0)] + public readonly int VkCode; + + [FieldOffset(8)] + public readonly int Flags; + } + + private static int lowLevelKeyboardProc(int nCode, int wParam, ref KdDllHookStruct lParam) + { + if (wParam >= wm_keydown && wParam <= wm_syskeyup) + { + switch (lParam.VkCode) + { + case 0x09 when lParam.Flags == 32: // alt + tab + case 0x1b when lParam.Flags == 32: // alt + esc + case 0x5B: // left windows key + case 0x5C: // right windows key + return 1; + } + } + + return callNextHookEx(0, nCode, wParam, ref lParam); + } + + internal static void Disable() + { + if (keyHook != IntPtr.Zero || isBlocked) + return; + + keyHook = setWindowsHookEx(wh_keyboard_ll, (keyboardHookDelegate = lowLevelKeyboardProc), Marshal.GetHINSTANCE(System.Reflection.Assembly.GetExecutingAssembly().GetModules()[0]), 0); + + isBlocked = true; + } + + internal static void Enable() + { + if (keyHook == IntPtr.Zero || !isBlocked) + return; + + keyHook = unhookWindowsHookEx(keyHook); + keyboardHookDelegate = null; + + keyHook = IntPtr.Zero; + + isBlocked = false; + } + + [DllImport(@"user32.dll", EntryPoint = @"SetWindowsHookExA")] + private static extern IntPtr setWindowsHookEx(int idHook, LowLevelKeyboardProcDelegate lpfn, IntPtr hMod, int dwThreadId); + + [DllImport(@"user32.dll", EntryPoint = @"UnhookWindowsHookEx")] + private static extern IntPtr unhookWindowsHookEx(IntPtr hHook); + + [DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")] + private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam); + } +} From 3278a1d7d821e4fd5cfe9d4bde6125ef9a77a09c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 2 Jul 2020 00:21:45 +0900 Subject: [PATCH 1993/6909] 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 c3cd2a74f5f0ee89e531d564db3d7a5cb9e3ed04 Mon Sep 17 00:00:00 2001 From: Shivam Date: Wed, 1 Jul 2020 22:57:16 +0200 Subject: [PATCH 1994/6909] Move general purpose migration to MigratableStorage --- osu.Game.Tournament/IO/TournamentStorage.cs | 10 +++--- osu.Game/IO/MigratableStorage.cs | 31 +++++++++++++++++- osu.Game/IO/OsuStorage.cs | 36 ++------------------- osu.Game/OsuGameBase.cs | 2 +- 4 files changed, 38 insertions(+), 41 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index ddc298a7ea..6505135b42 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -28,19 +28,16 @@ namespace osu.Game.Tournament.IO ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(storageConfig.Get(StorageConfig.CurrentTournament))); } else - { - Migrate(GetFullPath(default_tournament)); - ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(default_tournament)); - } + Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament)); VideoStore = new TournamentVideoResourceStore(this); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } - public override void Migrate(string newLocation) + public override void Migrate(Storage newStorage) { var source = new DirectoryInfo(storage.GetFullPath("tournament")); - var destination = new DirectoryInfo(newLocation); + var destination = new DirectoryInfo(newStorage.GetFullPath(".")); if (source.Exists) { @@ -53,6 +50,7 @@ namespace osu.Game.Tournament.IO moveFileIfExists("drawings.txt", destination); moveFileIfExists("drawings_results.txt", destination); moveFileIfExists("drawings.ini", destination); + ChangeTargetStorage(newStorage); storageConfig.Set(StorageConfig.CurrentTournament, default_tournament); storageConfig.Save(); } diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index faa39d2ef8..13aae92dfd 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -22,7 +22,36 @@ namespace osu.Game.IO { } - public abstract void Migrate(string newLocation); + /// + /// A general purpose migration method to move the storage to a different location. + /// The target storage of the migration. + /// + public virtual void Migrate(Storage newStorage) + { + var source = new DirectoryInfo(GetFullPath(".")); + var destination = new DirectoryInfo(newStorage.GetFullPath(".")); + + // 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(newStorage)); + + if (sourceUri.IsBaseOf(destinationUri)) + throw new ArgumentException("Destination provided is inside the source", nameof(newStorage)); + + // 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 ArgumentException("Destination provided already has files or directories present", nameof(newStorage)); + } + + CopyRecursive(source, destination); + ChangeTargetStorage(newStorage); + DeleteRecursive(source); + } protected void DeleteRecursive(DirectoryInfo target, bool topLevelExcludes = true) { diff --git a/osu.Game/IO/OsuStorage.cs b/osu.Game/IO/OsuStorage.cs index 31ee802141..7104031b56 100644 --- a/osu.Game/IO/OsuStorage.cs +++ b/osu.Game/IO/OsuStorage.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.IO; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Configuration; @@ -11,7 +9,6 @@ namespace osu.Game.IO { public class OsuStorage : MigratableStorage { - private readonly GameHost host; private readonly StorageConfigManager storageConfig; public override string[] IgnoreDirectories => new[] { "cache" }; @@ -25,8 +22,6 @@ namespace osu.Game.IO public OsuStorage(GameHost host) : base(host.Storage, string.Empty) { - this.host = host; - storageConfig = new StorageConfigManager(host.Storage); var customStoragePath = storageConfig.Get(StorageConfig.FullPath); @@ -41,36 +36,11 @@ namespace osu.Game.IO Logger.Storage = UnderlyingStorage.GetStorageForDirectory("logs"); } - public override void Migrate(string newLocation) + public override void Migrate(Storage newStorage) { - var source = new DirectoryInfo(GetFullPath(".")); - var destination = new DirectoryInfo(newLocation); - - // 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) - { - if (destination.GetFiles().Length > 0 || destination.GetDirectories().Length > 0) - throw new ArgumentException("Destination provided already has files or directories present", nameof(newLocation)); - } - - CopyRecursive(source, destination); - - ChangeTargetStorage(host.GetStorage(newLocation)); - - storageConfig.Set(StorageConfig.FullPath, newLocation); + base.Migrate(newStorage); + storageConfig.Set(StorageConfig.FullPath, newStorage.GetFullPath(".")); storageConfig.Save(); - - DeleteRecursive(source); } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3e7311092e..97a4e212e8 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -370,7 +370,7 @@ namespace osu.Game public void Migrate(string path) { contextFactory.FlushConnections(); - (Storage as OsuStorage)?.Migrate(path); + (Storage as OsuStorage)?.Migrate(Host.GetStorage(path)); } } } From 5c1f1ab622c8a4e862a7652564b557c76b9514ab Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 1 Jul 2020 14:31:06 -0700 Subject: [PATCH 1995/6909] 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 66e61aacff983d7354e6bb267cd472ee090d49fe Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 2 Jul 2020 00:32:09 +0200 Subject: [PATCH 1996/6909] Logger now shows the actual path of the destination Forgot to change this while changing the param from string to Storage --- osu.Game/IO/MigratableStorage.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 13aae92dfd..21087d7dc6 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -36,16 +36,16 @@ namespace osu.Game.IO var destinationUri = new Uri(destination.FullName + Path.DirectorySeparatorChar); if (sourceUri == destinationUri) - throw new ArgumentException("Destination provided is already the current location", nameof(newStorage)); + throw new ArgumentException("Destination provided is already the current location", destination.FullName); if (sourceUri.IsBaseOf(destinationUri)) - throw new ArgumentException("Destination provided is inside the source", nameof(newStorage)); + throw new ArgumentException("Destination provided is inside the source", destination.FullName); // 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 ArgumentException("Destination provided already has files or directories present", nameof(newStorage)); + throw new ArgumentException("Destination provided already has files or directories present", destination.FullName); } CopyRecursive(source, destination); From fa252d5e950d685699632b9cfb78ea7d25a95c58 Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 1 Jul 2020 17:37:38 -0700 Subject: [PATCH 1997/6909] 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 18e30a7fc4123a297f271bfb8ddc4fbe06fa9f23 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 2 Jul 2020 19:12:45 +0200 Subject: [PATCH 1998/6909] Implement background switching based on the intro Only the Welcome intro has its own unique background right now --- .../Backgrounds/BackgroundScreenDefault.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 980a127cf4..ae3ad63ac8 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -22,11 +22,12 @@ namespace osu.Game.Screens.Backgrounds private int currentDisplay; private const int background_count = 7; - private string backgroundName => $@"Menu/menu-background-{currentDisplay % background_count + 1}"; + private string backgroundName; private Bindable user; private Bindable skin; private Bindable mode; + private Bindable introSequence; [Resolved] private IBindable beatmap { get; set; } @@ -42,11 +43,13 @@ namespace osu.Game.Screens.Backgrounds user = api.LocalUser.GetBoundCopy(); skin = skinManager.CurrentSkin.GetBoundCopy(); mode = config.GetBindable(OsuSetting.MenuBackgroundSource); + introSequence = config.GetBindable(OsuSetting.IntroSequence); user.ValueChanged += _ => Next(); skin.ValueChanged += _ => Next(); mode.ValueChanged += _ => Next(); beatmap.ValueChanged += _ => Next(); + introSequence.ValueChanged += _ => Next(); currentDisplay = RNG.Next(0, background_count); @@ -74,6 +77,17 @@ namespace osu.Game.Screens.Backgrounds { Background newBackground; + switch (introSequence.Value) + { + case IntroSequence.Welcome: + backgroundName = "Menu/menu-background-welcome"; + break; + + default: + backgroundName = $@"Menu/menu-background-{currentDisplay % background_count + 1}"; + break; + } + if (user.Value?.IsSupporter ?? false) { switch (mode.Value) From e80a5a085afe07c55c9c112450df8f173af153e2 Mon Sep 17 00:00:00 2001 From: Shivam Date: Thu, 2 Jul 2020 19:45:18 +0200 Subject: [PATCH 1999/6909] Make backgroundName local --- osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index ae3ad63ac8..2c22e60195 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -21,9 +21,6 @@ namespace osu.Game.Screens.Backgrounds private int currentDisplay; private const int background_count = 7; - - private string backgroundName; - private Bindable user; private Bindable skin; private Bindable mode; @@ -76,6 +73,7 @@ namespace osu.Game.Screens.Backgrounds private Background createBackground() { Background newBackground; + string backgroundName; switch (introSequence.Value) { From 718f06c69075b9199ac07de2db66e6b8124182e5 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 2 Jul 2020 12:35:32 -0700 Subject: [PATCH 2000/6909] 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 2001/6909] 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 2002/6909] 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 4ded6d1913c6db7811cb3f4ac4a5037e3f843cb0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 3 Jul 2020 11:36:03 +0200 Subject: [PATCH 2003/6909] Change background path with resource change --- osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 2c22e60195..ef41c5be3d 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Backgrounds switch (introSequence.Value) { case IntroSequence.Welcome: - backgroundName = "Menu/menu-background-welcome"; + backgroundName = "Intro/Welcome/menu-background"; break; default: From e8f23e35a572d658536a19e083e451d29dc610fa Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Fri, 3 Jul 2020 14:33:42 +0200 Subject: [PATCH 2004/6909] WIP : replaced TransformBindableTo by VolumeTo Currently, the VolumeTO calls taht use a fading does not do anything. calling VolumeTo calls pauseLoop.samplesContainer.TransformBindableTo(....), while i used to call pauseLoop.TransformBindableTo(....) --- osu.Game/Screens/Play/PauseOverlay.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 022183d82b..a8d291d6c3 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -3,8 +3,10 @@ using System; using System.Linq; +using NUnit.Framework.Internal; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Skinning; @@ -44,8 +46,8 @@ namespace osu.Game.Screens.Play base.PopIn(); //SkinnableSound only plays a sound if its aggregate volume is > 0, so the volume must be turned up before playing it - pauseLoop.TransformBindableTo(pauseLoop.Volume, 0.00001); - pauseLoop.TransformBindableTo(pauseLoop.Volume, 1.0f, 400, Easing.InQuint); + pauseLoop.VolumeTo(0.00001f); + pauseLoop.VolumeTo(1.0f, 400, Easing.InQuint); pauseLoop.Play(); } @@ -53,8 +55,9 @@ namespace osu.Game.Screens.Play { base.PopOut(); - pauseLoop.Stop(); - pauseLoop.TransformBindableTo(pauseLoop.Volume, 0.0f); + var transformSeq = pauseLoop.VolumeTo(0.0f, 190, Easing.OutQuad ); + transformSeq.Finally(_ => pauseLoop.Stop()); + } } } From ffec4298a7b9c535db5b5dca6c8c2c4074cbf1e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jul 2020 16:45:46 +0900 Subject: [PATCH 2005/6909] 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 8869979599b2b79371b0ef2278a5f32f7200e883 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 4 Jul 2020 12:30:09 +0200 Subject: [PATCH 2006/6909] Trigger hook activation on bind. --- osu.Desktop/Windows/GameplayWinKeyHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs index cc0150497b..394df9dd0c 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.cs @@ -25,7 +25,7 @@ namespace osu.Desktop.Windows winKeyEnabled.ValueChanged += toggleWinKey; disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); - disableWinKey.BindValueChanged(t => winKeyEnabled.TriggerChange()); + disableWinKey.BindValueChanged(t => winKeyEnabled.TriggerChange(), true); } private void toggleWinKey(ValueChangedEvent e) From 02871c960ba37153075a2781f94de943e49b5ac0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 4 Jul 2020 23:25:06 +0900 Subject: [PATCH 2007/6909] 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 52b313f2909eca32da32c66ab9bf0aad9ff09abd Mon Sep 17 00:00:00 2001 From: jorolf Date: Sat, 4 Jul 2020 19:06:26 +0200 Subject: [PATCH 2008/6909] change textures --- .../UserInterface/TestSceneHueAnimation.cs | 2 +- osu.Game/Screens/Menu/IntroTriangles.cs | 39 ++++++++----------- 2 files changed, 18 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs index 85ddfb08f9..b341291c58 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.UserInterface { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, - Texture = textures.Get("Intro/Triangles/logo-triangles"), + Texture = textures.Get("Intro/Triangles/logo-background"), Colour = Colour4.White, }); diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 8118491c36..38da98220d 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -217,14 +217,8 @@ namespace osu.Game.Screens.Menu // matching flyte curve y = 0.25x^2 + (max(0, x - 0.7) / 0.3) ^ 5 lazerLogo.FadeIn().ScaleTo(scale_start).Then().Delay(logo_scale_duration * 0.7f).ScaleTo(scale_start - scale_adjust, logo_scale_duration * 0.3f, Easing.InQuint); - const double highlight_duration = logo_scale_duration / 1.4; - - //Since we only have one texture, roughly align it by changing the timing - lazerLogo.Outline = -0.4f; - lazerLogo.TransformTo(nameof(LazerLogo.Outline), 1f, highlight_duration * 1.4); - - lazerLogo.OutlineHighlight = 0f; - lazerLogo.TransformTo(nameof(LazerLogo.OutlineHighlight), 1f, highlight_duration); + lazerLogo.TransformTo(nameof(LazerLogo.Background), 1f, logo_scale_duration); + lazerLogo.TransformTo(nameof(LazerLogo.Highlight), 1f, logo_scale_duration); logoContainerSecondary.ScaleTo(scale_start).Then().ScaleTo(scale_start - scale_adjust * 0.25f, logo_scale_duration, Easing.InQuad); } @@ -267,18 +261,18 @@ namespace osu.Game.Screens.Menu private class LazerLogo : CompositeDrawable { - private HueAnimation outlineHighlight, outline; + private HueAnimation highlight, background; - public float OutlineHighlight + public float Highlight { - get => outlineHighlight.AnimationProgress; - set => outlineHighlight.AnimationProgress = value; + get => highlight.AnimationProgress; + set => highlight.AnimationProgress = value; } - public float Outline + public float Background { - get => outline.AnimationProgress; - set => outline.AnimationProgress = value; + get => background.AnimationProgress; + set => background.AnimationProgress = value; } public LazerLogo() @@ -289,21 +283,22 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(TextureStore textures) { - const string lazer_logo_texture = @"Intro/Triangles/logo-triangles"; + const string lazer_logo_background = @"Intro/Triangles/logo-background"; + const string lazer_logo_highlight = @"Intro/Triangles/logo-highlight"; InternalChildren = new Drawable[] { - outlineHighlight = new HueAnimation + highlight = new HueAnimation { RelativeSizeAxes = Axes.Both, - Texture = textures.Get(lazer_logo_texture), - Colour = OsuColour.Gray(0.8f), + Texture = textures.Get(lazer_logo_highlight), + Colour = OsuColour.Gray(1f), }, - outline = new HueAnimation + background = new HueAnimation { RelativeSizeAxes = Axes.Both, - Texture = textures.Get(lazer_logo_texture), - Colour = OsuColour.Gray(0.6f * 0.8f), + Texture = textures.Get(lazer_logo_background), + Colour = OsuColour.Gray(0.6f), }, }; } 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 2009/6909] 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 2010/6909] 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 2011/6909] 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 2012/6909] 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 ce5da5c51b98136503052eb11df547497019e6fb Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 5 Jul 2020 18:52:27 +0200 Subject: [PATCH 2013/6909] Block CTRL + ESC --- osu.Desktop/Windows/GameplayWinKeyHandler.cs | 8 ++++---- osu.Desktop/Windows/WindowsKey.cs | 4 ++++ 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs index 394df9dd0c..4f74a4f492 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.cs @@ -11,7 +11,7 @@ namespace osu.Desktop.Windows { public class GameplayWinKeyHandler : Component { - private Bindable winKeyEnabled; + private Bindable allowScreenSuspension; private Bindable disableWinKey; private GameHost host; @@ -21,11 +21,11 @@ namespace osu.Desktop.Windows { this.host = host; - winKeyEnabled = host.AllowScreenSuspension.GetBoundCopy(); - winKeyEnabled.ValueChanged += toggleWinKey; + allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy(); + allowScreenSuspension.ValueChanged += toggleWinKey; disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); - disableWinKey.BindValueChanged(t => winKeyEnabled.TriggerChange(), true); + disableWinKey.BindValueChanged(t => allowScreenSuspension.TriggerChange(), true); } private void toggleWinKey(ValueChangedEvent e) diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs index 748d9c55d6..175401aaed 100644 --- a/osu.Desktop/Windows/WindowsKey.cs +++ b/osu.Desktop/Windows/WindowsKey.cs @@ -38,6 +38,7 @@ namespace osu.Desktop.Windows { case 0x09 when lParam.Flags == 32: // alt + tab case 0x1b when lParam.Flags == 32: // alt + esc + case 0x1b when (getKeyState(0x11) & 0x8000) != 0: //ctrl + esc case 0x5B: // left windows key case 0x5C: // right windows key return 1; @@ -78,5 +79,8 @@ namespace osu.Desktop.Windows [DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")] private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam); + + [DllImport(@"user32.dll", EntryPoint = @"GetKeyState")] + private static extern int getKeyState(int vkey); } } From c18ca19c9d60c1e5715d7b2ea05e00566f827fbd Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 6 Jul 2020 05:31:34 +0300 Subject: [PATCH 2014/6909] 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 2015/6909] 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 2016/6909] 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 2017/6909] 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 022e4b6335c0ebdfbc48ec4f1764ba04257a01b4 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 6 Jul 2020 11:15:56 +0200 Subject: [PATCH 2018/6909] Apply review suggestions. --- osu.Desktop/Windows/WindowsKey.cs | 6 ------ .../Settings/Sections/Gameplay/GeneralSettings.cs | 15 ++++++++++----- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs index 175401aaed..4a815b135e 100644 --- a/osu.Desktop/Windows/WindowsKey.cs +++ b/osu.Desktop/Windows/WindowsKey.cs @@ -36,9 +36,6 @@ namespace osu.Desktop.Windows { switch (lParam.VkCode) { - case 0x09 when lParam.Flags == 32: // alt + tab - case 0x1b when lParam.Flags == 32: // alt + esc - case 0x1b when (getKeyState(0x11) & 0x8000) != 0: //ctrl + esc case 0x5B: // left windows key case 0x5C: // right windows key return 1; @@ -79,8 +76,5 @@ namespace osu.Desktop.Windows [DllImport(@"user32.dll", EntryPoint = @"CallNextHookEx")] private static extern int callNextHookEx(int hHook, int nCode, int wParam, ref KdDllHookStruct lParam); - - [DllImport(@"user32.dll", EntryPoint = @"GetKeyState")] - private static extern int getKeyState(int vkey); } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 60197c62b5..0149e6c3a6 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.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.Graphics; using osu.Game.Configuration; @@ -76,13 +77,17 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { LabelText = "Score display mode", Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) - }, - new SettingsCheckbox - { - LabelText = "Disable Win key during gameplay", - Bindable = config.GetBindable(OsuSetting.GameplayDisableWinKey) } }; + + if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) + { + Add(new SettingsCheckbox + { + LabelText = "Disable Windows key during gameplay", + Bindable = config.GetBindable(OsuSetting.GameplayDisableWinKey) + }); + } } } } From dbbee481f60b21911b5c67fa1d575272ac7a3a25 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 6 Jul 2020 22:01:45 +0900 Subject: [PATCH 2019/6909] 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 2020/6909] 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 2021/6909] 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 2022/6909] 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 2023/6909] 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 2024/6909] 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 2025/6909] 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 2026/6909] 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 2027/6909] 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 2028/6909] 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 2029/6909] 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 2030/6909] 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 2031/6909] 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 2032/6909] 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 2033/6909] 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 2034/6909] 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 2035/6909] 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 2036/6909] 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 2037/6909] 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 2038/6909] 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 2039/6909] 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 2040/6909] 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 2041/6909] 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 2042/6909] 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 2043/6909] 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 2044/6909] 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 f03303573ef1bd2fe972b670d0f7ea4a9eedfb83 Mon Sep 17 00:00:00 2001 From: BananeVolante Date: Wed, 8 Jul 2020 13:54:22 +0200 Subject: [PATCH 2045/6909] formating --- osu.Game/Screens/Play/PauseOverlay.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index a8d291d6c3..7b3fba7ddf 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -3,10 +3,8 @@ using System; using System.Linq; -using NUnit.Framework.Internal; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Audio; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Skinning; @@ -55,9 +53,8 @@ namespace osu.Game.Screens.Play { base.PopOut(); - var transformSeq = pauseLoop.VolumeTo(0.0f, 190, Easing.OutQuad ); + var transformSeq = pauseLoop.VolumeTo(0.0f, 190, Easing.OutQuad); transformSeq.Finally(_ => pauseLoop.Stop()); - } } } From de4c22c70923b2a9434eadc993eda361bd1c6bd6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 8 Jul 2020 17:58:09 +0300 Subject: [PATCH 2046/6909] Implement news api request --- .../Online/API/Requests/GetNewsRequest.cs | 27 +++++++++++++++++++ .../Online/API/Requests/GetNewsResponse.cs | 15 +++++++++++ 2 files changed, 42 insertions(+) create mode 100644 osu.Game/Online/API/Requests/GetNewsRequest.cs create mode 100644 osu.Game/Online/API/Requests/GetNewsResponse.cs diff --git a/osu.Game/Online/API/Requests/GetNewsRequest.cs b/osu.Game/Online/API/Requests/GetNewsRequest.cs new file mode 100644 index 0000000000..36d9dc0652 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetNewsRequest.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.IO.Network; +using osu.Game.Extensions; + +namespace osu.Game.Online.API.Requests +{ + public class GetNewsRequest : APIRequest + { + private readonly Cursor cursor; + + public GetNewsRequest(Cursor cursor = null) + { + this.cursor = cursor; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.AddCursor(cursor); + return req; + } + + protected override string Target => "news"; + } +} diff --git a/osu.Game/Online/API/Requests/GetNewsResponse.cs b/osu.Game/Online/API/Requests/GetNewsResponse.cs new file mode 100644 index 0000000000..835289a51d --- /dev/null +++ b/osu.Game/Online/API/Requests/GetNewsResponse.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.Collections.Generic; +using Newtonsoft.Json; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Online.API.Requests +{ + public class GetNewsResponse : ResponseWithCursor + { + [JsonProperty("news_posts")] + public IEnumerable NewsPosts; + } +} From 49d998c8db2f7a6be17d3fd25898015e5a3920b8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 8 Jul 2020 18:24:13 +0300 Subject: [PATCH 2047/6909] Refactor NewsOverlay to use displays logic --- .../Visual/Online/TestSceneNewsOverlay.cs | 4 +- osu.Game/Overlays/DashboardOverlay.cs | 8 +-- osu.Game/Overlays/NewsOverlay.cs | 70 +++++++++++++------ 3 files changed, 53 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index d47c972564..0b3ede5d13 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -22,12 +22,12 @@ namespace osu.Game.Tests.Visual.Online AddStep(@"Show front page", () => news.ShowFrontPage()); AddStep(@"Custom article", () => news.Current.Value = "Test Article 101"); - AddStep(@"Article covers", () => news.LoadAndShowContent(new NewsCoverTest())); + AddStep(@"Article covers", () => news.LoadDisplay(new NewsCoverTest())); } private class TestNewsOverlay : NewsOverlay { - public new void LoadAndShowContent(NewsContent content) => base.LoadAndShowContent(content); + public void LoadDisplay(NewsContent content) => base.LoadDisplay(content); } private class NewsCoverTest : NewsContent diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index a72c3f4fa5..e3a4b0e152 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -19,7 +19,6 @@ namespace osu.Game.Overlays { private CancellationTokenSource cancellationToken; - private Box background; private Container content; private DashboardOverlayHeader header; private LoadingLayer loading; @@ -35,9 +34,10 @@ namespace osu.Game.Overlays { Children = new Drawable[] { - background = new Box + new Box { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background5 }, scrollFlow = new OverlayScrollContainer { @@ -66,8 +66,6 @@ namespace osu.Game.Overlays }, loading = new LoadingLayer(content), }; - - background.Colour = ColourProvider.Background5; } protected override void LoadComplete() diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 46d692d44d..ec25827b5a 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -7,37 +7,40 @@ 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.UserInterface; using osu.Game.Overlays.News; namespace osu.Game.Overlays { public class NewsOverlay : FullscreenOverlay { - private NewsHeader header; - - private Container content; - public readonly Bindable Current = new Bindable(null); + private Container content; + private LoadingLayer loading; + private OverlayScrollContainer scrollFlow; + public NewsOverlay() : base(OverlayColourScheme.Purple) { } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { + NewsHeader header; + Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.PurpleDarkAlternative + Colour = ColourProvider.Background5, }, - new OverlayScrollContainer + scrollFlow = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -49,7 +52,7 @@ namespace osu.Game.Overlays { ShowFrontPage = ShowFrontPage }, - content = new Container + content = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -57,25 +60,16 @@ namespace osu.Game.Overlays }, }, }, + loading = new LoadingLayer(content), }; header.Post.BindTo(Current); - Current.TriggerChange(); } - private CancellationTokenSource loadContentCancellation; - - protected void LoadAndShowContent(NewsContent newContent) + protected override void LoadComplete() { - content.FadeTo(0.2f, 300, Easing.OutQuint); - - loadContentCancellation?.Cancel(); - - LoadComponentAsync(newContent, c => - { - content.Child = c; - content.FadeIn(300, Easing.OutQuint); - }, (loadContentCancellation = new CancellationTokenSource()).Token); + base.LoadComplete(); + Current.BindValueChanged(onCurrentChanged, true); } public void ShowFrontPage() @@ -83,5 +77,37 @@ namespace osu.Game.Overlays Current.Value = null; Show(); } + + private CancellationTokenSource cancellationToken; + + private void onCurrentChanged(ValueChangedEvent current) + { + cancellationToken?.Cancel(); + loading.Show(); + + if (current.NewValue == null) + { + LoadDisplay(Empty()); + return; + } + + LoadDisplay(Empty()); + } + + protected void LoadDisplay(Drawable display) + { + scrollFlow.ScrollToStart(); + LoadComponentAsync(display, loaded => + { + content.Child = loaded; + loading.Hide(); + }, (cancellationToken = new CancellationTokenSource()).Token); + } + + protected override void Dispose(bool isDisposing) + { + cancellationToken?.Cancel(); + base.Dispose(isDisposing); + } } } From 0b4213f3307c5c0145c9d2185e575d56833ea8a5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 8 Jul 2020 20:07:29 +0300 Subject: [PATCH 2048/6909] Implement FrontPageDisplay --- .../Visual/Online/TestSceneNewsOverlay.cs | 2 + .../News/Displays/FrontpageDisplay.cs | 133 ++++++++++++++++++ osu.Game/Overlays/NewsOverlay.cs | 3 +- 3 files changed, 137 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/News/Displays/FrontpageDisplay.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index 0b3ede5d13..151b3df68f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -12,6 +12,8 @@ namespace osu.Game.Tests.Visual.Online { private TestNewsOverlay news; + protected override bool UseOnlineAPI => true; + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs new file mode 100644 index 0000000000..611a072047 --- /dev/null +++ b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; +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 osuTK; + +namespace osu.Game.Overlays.News.Displays +{ + public class FrontpageDisplay : CompositeDrawable + { + [Resolved] + private IAPIProvider api { get; set; } + + private readonly FillFlowContainer content; + private readonly FrontpageShowMoreButton showMore; + + private GetNewsRequest request; + private Cursor lastCursor; + + public FrontpageDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding + { + Top = 20, + Bottom = 10, + Left = 35, + Right = 55 + }; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + content = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10) + }, + showMore = new FrontpageShowMoreButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding + { + Vertical = 15 + }, + Action = fetchPage, + Alpha = 0 + } + } + }; + } + + [BackgroundDependencyLoader] + private void load() + { + fetchPage(); + } + + private void fetchPage() + { + request = new GetNewsRequest(lastCursor); + request.Success += response => Schedule(() => createContent(response)); + api.PerformAsync(request); + } + + private CancellationTokenSource cancellationToken; + + private void createContent(GetNewsResponse response) + { + lastCursor = response.Cursor; + + FillFlowContainer flow; + + flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10) + }; + + response.NewsPosts.ForEach(p => + { + flow.Add(new NewsCard(p)); + }); + + LoadComponentAsync(flow, loaded => + { + content.Add(loaded); + showMore.IsLoading = false; + showMore.Show(); + }, (cancellationToken = new CancellationTokenSource()).Token); + } + + protected override void Dispose(bool isDisposing) + { + request?.Cancel(); + cancellationToken?.Cancel(); + base.Dispose(isDisposing); + } + + private class FrontpageShowMoreButton : ShowMoreButton + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Height = 20; + + IdleColour = colourProvider.Background3; + HoverColour = colourProvider.Background2; + ChevronIconColour = colourProvider.Foreground1; + } + } + } +} diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index ec25827b5a..c6c5d132c1 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.News; +using osu.Game.Overlays.News.Displays; namespace osu.Game.Overlays { @@ -87,7 +88,7 @@ namespace osu.Game.Overlays if (current.NewValue == null) { - LoadDisplay(Empty()); + LoadDisplay(new FrontpageDisplay()); return; } From 57b935ec50b42a8c1eb2ebff1b04fd42f65e6a63 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 8 Jul 2020 20:17:15 +0300 Subject: [PATCH 2049/6909] Remove outdated elements --- .../Visual/Online/TestSceneNewsOverlay.cs | 47 +---- .../News/Displays/FrontpageDisplay.cs | 4 +- osu.Game/Overlays/News/NewsArticleCover.cs | 174 ------------------ osu.Game/Overlays/News/NewsContent.cs | 19 -- 4 files changed, 3 insertions(+), 241 deletions(-) delete mode 100644 osu.Game/Overlays/News/NewsArticleCover.cs delete mode 100644 osu.Game/Overlays/News/NewsContent.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index 151b3df68f..f0dc309d01 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -1,68 +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 osu.Framework.Graphics; using osu.Game.Overlays; -using osu.Game.Overlays.News; namespace osu.Game.Tests.Visual.Online { public class TestSceneNewsOverlay : OsuTestScene { - private TestNewsOverlay news; + private NewsOverlay news; protected override bool UseOnlineAPI => true; protected override void LoadComplete() { base.LoadComplete(); - Add(news = new TestNewsOverlay()); + Add(news = new NewsOverlay()); AddStep(@"Show", news.Show); AddStep(@"Hide", news.Hide); AddStep(@"Show front page", () => news.ShowFrontPage()); AddStep(@"Custom article", () => news.Current.Value = "Test Article 101"); - - AddStep(@"Article covers", () => news.LoadDisplay(new NewsCoverTest())); - } - - private class TestNewsOverlay : NewsOverlay - { - public void LoadDisplay(NewsContent content) => base.LoadDisplay(content); - } - - private class NewsCoverTest : NewsContent - { - public NewsCoverTest() - { - Spacing = new osuTK.Vector2(0, 10); - - var article = new NewsArticleCover.ArticleInfo - { - Author = "Ephemeral", - CoverUrl = "https://assets.ppy.sh/artists/58/header.jpg", - Time = new DateTime(2019, 12, 4), - Title = "New Featured Artist: Kurokotei" - }; - - Children = new Drawable[] - { - new NewsArticleCover(article) - { - Height = 200 - }, - new NewsArticleCover(article) - { - Height = 120 - }, - new NewsArticleCover(article) - { - RelativeSizeAxes = Axes.None, - Size = new osuTK.Vector2(400, 200), - } - }; - } } } } diff --git a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs index 611a072047..270c62d42f 100644 --- a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs +++ b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs @@ -87,9 +87,7 @@ namespace osu.Game.Overlays.News.Displays { lastCursor = response.Cursor; - FillFlowContainer flow; - - flow = new FillFlowContainer + var flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, diff --git a/osu.Game/Overlays/News/NewsArticleCover.cs b/osu.Game/Overlays/News/NewsArticleCover.cs deleted file mode 100644 index e3f5a8cea3..0000000000 --- a/osu.Game/Overlays/News/NewsArticleCover.cs +++ /dev/null @@ -1,174 +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.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; -using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osuTK.Graphics; - -namespace osu.Game.Overlays.News -{ - public class NewsArticleCover : Container - { - private const int hover_duration = 300; - - private readonly Box gradient; - - public NewsArticleCover(ArticleInfo info) - { - RelativeSizeAxes = Axes.X; - Masking = true; - CornerRadius = 4; - - NewsBackground bg; - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(OsuColour.Gray(0.2f), OsuColour.Gray(0.1f)) - }, - new DelayedLoadWrapper(bg = new NewsBackground(info.CoverUrl) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fill, - Alpha = 0 - }) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - }, - gradient = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4.Black.Opacity(0.1f), Color4.Black.Opacity(0.7f)), - Alpha = 0 - }, - new DateContainer(info.Time) - { - Margin = new MarginPadding - { - Right = 20, - Top = 20, - } - }, - new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding - { - Left = 25, - Bottom = 50, - }, - Font = OsuFont.GetFont(Typeface.Torus, 24, FontWeight.Bold), - Text = info.Title, - }, - new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding - { - Left = 25, - Bottom = 30, - }, - Font = OsuFont.GetFont(Typeface.Torus, 16, FontWeight.Bold), - Text = "by " + info.Author - } - }; - - bg.OnLoadComplete += d => d.FadeIn(250, Easing.In); - } - - protected override bool OnHover(HoverEvent e) - { - gradient.FadeIn(hover_duration, Easing.OutQuint); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - base.OnHoverLost(e); - gradient.FadeOut(hover_duration, Easing.OutQuint); - } - - [LongRunningLoad] - private class NewsBackground : Sprite - { - private readonly string url; - - public NewsBackground(string coverUrl) - { - url = coverUrl ?? "Headers/news"; - } - - [BackgroundDependencyLoader] - private void load(LargeTextureStore store) - { - Texture = store.Get(url); - } - } - - private class DateContainer : Container, IHasTooltip - { - private readonly DateTime date; - - public DateContainer(DateTime date) - { - this.date = date; - - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - Masking = true; - CornerRadius = 4; - AutoSizeAxes = Axes.Both; - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.5f), - }, - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(Typeface.Torus, 12, FontWeight.Bold, false, false), - Text = date.ToString("d MMM yyy").ToUpper(), - Margin = new MarginPadding - { - Vertical = 4, - Horizontal = 8, - } - } - }; - } - - 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 - public class ArticleInfo - { - public string Title { get; set; } - public string CoverUrl { get; set; } - public DateTime Time { get; set; } - public string Author { get; set; } - } - } -} diff --git a/osu.Game/Overlays/News/NewsContent.cs b/osu.Game/Overlays/News/NewsContent.cs deleted file mode 100644 index 5ff210f9f5..0000000000 --- a/osu.Game/Overlays/News/NewsContent.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; - -namespace osu.Game.Overlays.News -{ - public abstract class NewsContent : FillFlowContainer - { - protected NewsContent() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Direction = FillDirection.Vertical; - Padding = new MarginPadding { Bottom = 100, Top = 20, Horizontal = 50 }; - } - } -} 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 2050/6909] 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 2051/6909] 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 2052/6909] 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 2053/6909] 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 900f2d309b84f43d151685488b6824bb8c5c40c5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 9 Jul 2020 01:26:56 +0300 Subject: [PATCH 2054/6909] Classes naming adjustments --- osu.Game/Overlays/News/Displays/FrontpageDisplay.cs | 10 +++++----- osu.Game/Overlays/NewsOverlay.cs | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs index 270c62d42f..f0b25c8143 100644 --- a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs +++ b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs @@ -13,18 +13,18 @@ using osuTK; namespace osu.Game.Overlays.News.Displays { - public class FrontpageDisplay : CompositeDrawable + public class FrontPageDisplay : CompositeDrawable { [Resolved] private IAPIProvider api { get; set; } private readonly FillFlowContainer content; - private readonly FrontpageShowMoreButton showMore; + private readonly FrontPageShowMoreButton showMore; private GetNewsRequest request; private Cursor lastCursor; - public FrontpageDisplay() + public FrontPageDisplay() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.News.Displays Direction = FillDirection.Vertical, Spacing = new Vector2(0, 10) }, - showMore = new FrontpageShowMoreButton + showMore = new FrontPageShowMoreButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.News.Displays base.Dispose(isDisposing); } - private class FrontpageShowMoreButton : ShowMoreButton + private class FrontPageShowMoreButton : ShowMoreButton { [BackgroundDependencyLoader] private void load(OverlayColourProvider colourProvider) diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index c6c5d132c1..4cd83f83af 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -88,7 +88,7 @@ namespace osu.Game.Overlays if (current.NewValue == null) { - LoadDisplay(new FrontpageDisplay()); + LoadDisplay(new FrontPageDisplay()); return; } From 62e2bc11983cc46feffb35badfa82deee9740e6d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 9 Jul 2020 01:29:27 +0300 Subject: [PATCH 2055/6909] Fix potential double-request situation --- osu.Game/Overlays/News/Displays/FrontpageDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs index f0b25c8143..73af51c342 100644 --- a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs +++ b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs @@ -76,6 +76,8 @@ namespace osu.Game.Overlays.News.Displays private void fetchPage() { + request?.Cancel(); + request = new GetNewsRequest(lastCursor); request.Success += response => Schedule(() => createContent(response)); api.PerformAsync(request); From dfa22b1e4c48fa297508ed57cc84565899c34cd2 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 9 Jul 2020 02:37:42 +0300 Subject: [PATCH 2056/6909] Styles improvements --- .../Visual/Online/TestSceneShowMoreButton.cs | 20 +++----- .../Graphics/UserInterface/ShowMoreButton.cs | 47 +++++++++++-------- .../Comments/CommentsShowMoreButton.cs | 11 ----- .../News/Displays/FrontpageDisplay.cs | 26 +++------- .../Profile/Sections/PaginatedContainer.cs | 5 +- .../Profile/Sections/ProfileShowMoreButton.cs | 19 -------- 6 files changed, 42 insertions(+), 86 deletions(-) delete mode 100644 osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs index 273f593c32..f1c69f0ac3 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs @@ -4,19 +4,22 @@ using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; using osu.Framework.Allocation; -using osu.Game.Graphics; +using osu.Game.Overlays; namespace osu.Game.Tests.Visual.Online { public class TestSceneShowMoreButton : OsuTestScene { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red); + public TestSceneShowMoreButton() { - TestButton button = null; + ShowMoreButton button = null; int fireCount = 0; - Add(button = new TestButton + Add(button = new ShowMoreButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -46,16 +49,5 @@ namespace osu.Game.Tests.Visual.Online AddAssert("action fired twice", () => fireCount == 2); AddAssert("is in loading state", () => button.IsLoading); } - - private class TestButton : ShowMoreButton - { - [BackgroundDependencyLoader] - private void load(OsuColour colors) - { - IdleColour = colors.YellowDark; - HoverColour = colors.Yellow; - ChevronIconColour = colors.Red; - } - } } } diff --git a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs index c9cd9f1158..db563b346c 100644 --- a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs +++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs @@ -1,13 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; 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 osu.Game.Overlays; using osuTK; -using osuTK.Graphics; using System.Collections.Generic; namespace osu.Game.Graphics.UserInterface @@ -16,14 +17,6 @@ namespace osu.Game.Graphics.UserInterface { private const int duration = 200; - private Color4 chevronIconColour; - - protected Color4 ChevronIconColour - { - get => chevronIconColour; - set => chevronIconColour = leftChevron.Colour = rightChevron.Colour = value; - } - public string Text { get => text.Text; @@ -32,22 +25,26 @@ namespace osu.Game.Graphics.UserInterface protected override IEnumerable EffectTargets => new[] { background }; - private ChevronIcon leftChevron; - private ChevronIcon rightChevron; private SpriteText text; private Box background; private FillFlowContainer textContainer; public ShowMoreButton() { - Height = 30; - Width = 140; + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IdleColour = colourProvider.Background2; + HoverColour = colourProvider.Background1; } protected override Drawable CreateContent() => new CircularContainer { Masking = true, - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Children = new Drawable[] { background = new Box @@ -56,22 +53,28 @@ namespace osu.Game.Graphics.UserInterface }, textContainer = new FillFlowContainer { + AlwaysPresent = true, Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(7), + Margin = new MarginPadding + { + Horizontal = 20, + Vertical = 5, + }, Children = new Drawable[] { - leftChevron = new ChevronIcon(), + new ChevronIcon(), text = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Text = "show more".ToUpper(), }, - rightChevron = new ChevronIcon(), + new ChevronIcon() } } } @@ -83,15 +86,19 @@ namespace osu.Game.Graphics.UserInterface private class ChevronIcon : SpriteIcon { - private const int icon_size = 8; - public ChevronIcon() { Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(icon_size); + Size = new Vector2(8); Icon = FontAwesome.Solid.ChevronDown; } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Foreground1; + } } } } diff --git a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs b/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs index d2ff7ecb1f..adf64eabb1 100644 --- a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs +++ b/osu.Game/Overlays/Comments/CommentsShowMoreButton.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.Allocation; using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; @@ -11,16 +10,6 @@ namespace osu.Game.Overlays.Comments { public readonly BindableInt Current = new BindableInt(); - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Height = 20; - - IdleColour = colourProvider.Background2; - HoverColour = colourProvider.Background1; - ChevronIconColour = colourProvider.Foreground1; - } - protected override void LoadComplete() { Current.BindValueChanged(onCurrentChanged, true); diff --git a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs index 73af51c342..386e0b0dca 100644 --- a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs +++ b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.News.Displays private IAPIProvider api { get; set; } private readonly FillFlowContainer content; - private readonly FrontPageShowMoreButton showMore; + private readonly ShowMoreButton showMore; private GetNewsRequest request; private Cursor lastCursor; @@ -30,10 +30,9 @@ namespace osu.Game.Overlays.News.Displays AutoSizeAxes = Axes.Y; Padding = new MarginPadding { - Top = 20, - Bottom = 10, - Left = 35, - Right = 55 + Vertical = 20, + Left = 30, + Right = 50 }; InternalChild = new FillFlowContainer @@ -53,13 +52,13 @@ namespace osu.Game.Overlays.News.Displays Direction = FillDirection.Vertical, Spacing = new Vector2(0, 10) }, - showMore = new FrontPageShowMoreButton + showMore = new ShowMoreButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Margin = new MarginPadding { - Vertical = 15 + Top = 15 }, Action = fetchPage, Alpha = 0 @@ -116,18 +115,5 @@ namespace osu.Game.Overlays.News.Displays cancellationToken?.Cancel(); base.Dispose(isDisposing); } - - private class FrontPageShowMoreButton : ShowMoreButton - { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Height = 20; - - IdleColour = colourProvider.Background3; - HoverColour = colourProvider.Background2; - ChevronIconColour = colourProvider.Foreground1; - } - } } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index a30ff786fb..9720469548 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -14,12 +14,13 @@ using osu.Game.Users; using System.Collections.Generic; using System.Linq; using System.Threading; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Profile.Sections { public abstract class PaginatedContainer : FillFlowContainer { - private readonly ProfileShowMoreButton moreButton; + private readonly ShowMoreButton moreButton; private readonly OsuSpriteText missingText; private APIRequest> retrievalRequest; private CancellationTokenSource loadCancellation; @@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Profile.Sections RelativeSizeAxes = Axes.X, Spacing = new Vector2(0, 2), }, - moreButton = new ProfileShowMoreButton + moreButton = new ShowMoreButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs b/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs deleted file mode 100644 index 426ebeebe6..0000000000 --- a/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Overlays.Profile.Sections -{ - public class ProfileShowMoreButton : ShowMoreButton - { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - IdleColour = colourProvider.Background2; - HoverColour = colourProvider.Background1; - ChevronIconColour = colourProvider.Foreground1; - } - } -} From 3ba8ec0fd75f73dda2db4d8bb855e7544a7f7aba Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 9 Jul 2020 03:40:14 +0300 Subject: [PATCH 2057/6909] Don't set null value to show front page --- .../Visual/Online/TestSceneNewsOverlay.cs | 16 ++++--- osu.Game/Overlays/News/NewsHeader.cs | 46 ++++++++++--------- osu.Game/Overlays/NewsOverlay.cs | 26 +++++------ 3 files changed, 45 insertions(+), 43 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index f0dc309d01..e35ef4916b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -7,19 +7,21 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneNewsOverlay : OsuTestScene { - private NewsOverlay news; - protected override bool UseOnlineAPI => true; protected override void LoadComplete() { base.LoadComplete(); - Add(news = new NewsOverlay()); - AddStep(@"Show", news.Show); - AddStep(@"Hide", news.Hide); - AddStep(@"Show front page", () => news.ShowFrontPage()); - AddStep(@"Custom article", () => news.Current.Value = "Test Article 101"); + NewsOverlay news; + Add(news = new NewsOverlay()); + + AddStep("Show", news.Show); + AddStep("Hide", news.Hide); + + AddStep("Show front page", () => news.ShowFrontPage()); + AddStep("Custom article", () => news.ShowArticle("Test Article 101")); + AddStep("Custom article", () => news.ShowArticle("Test Article 102")); } } } diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index 8214c71b3a..ee7991c0c6 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -3,44 +3,46 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using System; namespace osu.Game.Overlays.News { public class NewsHeader : BreadcrumbControlOverlayHeader { - private const string front_page_string = "frontpage"; + public const string FRONT_PAGE_STRING = "frontpage"; - public readonly Bindable Post = new Bindable(null); - - public Action ShowFrontPage; + public readonly Bindable Post = new Bindable(FRONT_PAGE_STRING); public NewsHeader() { - TabControl.AddItem(front_page_string); - - Current.ValueChanged += e => - { - if (e.NewValue == front_page_string) - ShowFrontPage?.Invoke(); - }; - - Post.ValueChanged += showPost; + TabControl.AddItem(FRONT_PAGE_STRING); + Current.Value = FRONT_PAGE_STRING; + Current.BindValueChanged(onCurrentChanged); + Post.BindValueChanged(onPostChanged, true); } - private void showPost(ValueChangedEvent e) - { - if (e.OldValue != null) - TabControl.RemoveItem(e.OldValue); + public void SetFrontPage() => Post.Value = FRONT_PAGE_STRING; - if (e.NewValue != null) + public void SetArticle(string slug) => Post.Value = slug; + + private void onCurrentChanged(ValueChangedEvent current) + { + if (current.NewValue == FRONT_PAGE_STRING) + Post.Value = FRONT_PAGE_STRING; + } + + private void onPostChanged(ValueChangedEvent post) + { + if (post.OldValue != FRONT_PAGE_STRING) + TabControl.RemoveItem(post.OldValue); + + if (post.NewValue != FRONT_PAGE_STRING) { - TabControl.AddItem(e.NewValue); - Current.Value = e.NewValue; + TabControl.AddItem(post.NewValue); + Current.Value = post.NewValue; } else { - Current.Value = front_page_string; + Current.Value = FRONT_PAGE_STRING; } } diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 4cd83f83af..db989e71bf 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -15,10 +15,9 @@ namespace osu.Game.Overlays { public class NewsOverlay : FullscreenOverlay { - public readonly Bindable Current = new Bindable(null); - private Container content; private LoadingLayer loading; + private NewsHeader header; private OverlayScrollContainer scrollFlow; public NewsOverlay() @@ -29,8 +28,6 @@ namespace osu.Game.Overlays [BackgroundDependencyLoader] private void load() { - NewsHeader header; - Children = new Drawable[] { new Box @@ -49,10 +46,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new NewsHeader - { - ShowFrontPage = ShowFrontPage - }, + header = new NewsHeader(), content = new Container { RelativeSizeAxes = Axes.X, @@ -63,30 +57,34 @@ namespace osu.Game.Overlays }, loading = new LoadingLayer(content), }; - - header.Post.BindTo(Current); } protected override void LoadComplete() { base.LoadComplete(); - Current.BindValueChanged(onCurrentChanged, true); + header.Post.BindValueChanged(onPostChanged, true); } public void ShowFrontPage() { - Current.Value = null; + header.SetFrontPage(); + Show(); + } + + public void ShowArticle(string slug) + { + header.SetArticle(slug); Show(); } private CancellationTokenSource cancellationToken; - private void onCurrentChanged(ValueChangedEvent current) + private void onPostChanged(ValueChangedEvent post) { cancellationToken?.Cancel(); loading.Show(); - if (current.NewValue == null) + if (post.NewValue == NewsHeader.FRONT_PAGE_STRING) { LoadDisplay(new FrontPageDisplay()); return; From aeb664aca759e7061a6140b7c51dabc31fa26132 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 9 Jul 2020 04:00:53 +0300 Subject: [PATCH 2058/6909] Delete broken file --- .../News/Displays/FrontpageDisplay.cs | 119 ------------------ 1 file changed, 119 deletions(-) delete mode 100644 osu.Game/Overlays/News/Displays/FrontpageDisplay.cs diff --git a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs deleted file mode 100644 index 386e0b0dca..0000000000 --- a/osu.Game/Overlays/News/Displays/FrontpageDisplay.cs +++ /dev/null @@ -1,119 +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.Threading; -using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; -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 osuTK; - -namespace osu.Game.Overlays.News.Displays -{ - public class FrontPageDisplay : CompositeDrawable - { - [Resolved] - private IAPIProvider api { get; set; } - - private readonly FillFlowContainer content; - private readonly ShowMoreButton showMore; - - private GetNewsRequest request; - private Cursor lastCursor; - - public FrontPageDisplay() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Padding = new MarginPadding - { - Vertical = 20, - Left = 30, - Right = 50 - }; - - InternalChild = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), - Children = new Drawable[] - { - content = new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10) - }, - showMore = new ShowMoreButton - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding - { - Top = 15 - }, - Action = fetchPage, - Alpha = 0 - } - } - }; - } - - [BackgroundDependencyLoader] - private void load() - { - fetchPage(); - } - - private void fetchPage() - { - request?.Cancel(); - - request = new GetNewsRequest(lastCursor); - request.Success += response => Schedule(() => createContent(response)); - api.PerformAsync(request); - } - - private CancellationTokenSource cancellationToken; - - private void createContent(GetNewsResponse response) - { - lastCursor = response.Cursor; - - var flow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10) - }; - - response.NewsPosts.ForEach(p => - { - flow.Add(new NewsCard(p)); - }); - - LoadComponentAsync(flow, loaded => - { - content.Add(loaded); - showMore.IsLoading = false; - showMore.Show(); - }, (cancellationToken = new CancellationTokenSource()).Token); - } - - protected override void Dispose(bool isDisposing) - { - request?.Cancel(); - cancellationToken?.Cancel(); - base.Dispose(isDisposing); - } - } -} From f663dd18033e9cfc83dbbaa652f862a4e7fb8099 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 9 Jul 2020 04:02:14 +0300 Subject: [PATCH 2059/6909] Fix incorrect file name --- .../News/Displays/FrontPageDisplay.cs | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 osu.Game/Overlays/News/Displays/FrontPageDisplay.cs diff --git a/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs new file mode 100644 index 0000000000..67b5edfafd --- /dev/null +++ b/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs @@ -0,0 +1,115 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using System.Threading; +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 osuTK; + +namespace osu.Game.Overlays.News.Displays +{ + public class FrontPageDisplay : CompositeDrawable + { + [Resolved] + private IAPIProvider api { get; set; } + + private readonly FillFlowContainer content; + private readonly ShowMoreButton showMore; + + private GetNewsRequest request; + private Cursor lastCursor; + + public FrontPageDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Padding = new MarginPadding + { + Vertical = 20, + Left = 30, + Right = 50 + }; + + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + content = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10) + }, + showMore = new ShowMoreButton + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding + { + Top = 15 + }, + Action = fetchPage, + Alpha = 0 + } + } + }; + } + + [BackgroundDependencyLoader] + private void load() + { + fetchPage(); + } + + private void fetchPage() + { + request?.Cancel(); + + request = new GetNewsRequest(lastCursor); + request.Success += response => Schedule(() => createContent(response)); + api.PerformAsync(request); + } + + private CancellationTokenSource cancellationToken; + + private void createContent(GetNewsResponse response) + { + lastCursor = response.Cursor; + + var flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = response.NewsPosts.Select(p => new NewsCard(p)).ToList() + }; + + LoadComponentAsync(flow, loaded => + { + content.Add(loaded); + showMore.IsLoading = false; + showMore.Show(); + }, (cancellationToken = new CancellationTokenSource()).Token); + } + + protected override void Dispose(bool isDisposing) + { + request?.Cancel(); + cancellationToken?.Cancel(); + base.Dispose(isDisposing); + } + } +} From c10cf2ef496544f9dfcd9c3a0533ae29c81176fa Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 8 Jul 2020 19:01:12 -0700 Subject: [PATCH 2060/6909] 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 2061/6909] 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 2062/6909] 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 2063/6909] 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 2064/6909] 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 2065/6909] 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 2066/6909] 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 2067/6909] 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 2068/6909] 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 2069/6909] 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 2070/6909] 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 2071/6909] 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 2072/6909] 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 2073/6909] 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 2074/6909] 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 2075/6909] 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 2076/6909] 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 321815f535b53fce4f4db3749800422d55ef8ba7 Mon Sep 17 00:00:00 2001 From: Yao Chung Hu <30311066+FlashyReese@users.noreply.github.com> Date: Thu, 9 Jul 2020 14:01:28 -0500 Subject: [PATCH 2077/6909] Add playfield bounds box with toggle and dim slider --- osu.Game/Configuration/OsuConfigManager.cs | 7 +++- .../Sections/Gameplay/GeneralSettings.cs | 14 ++++++- osu.Game/Rulesets/UI/Playfield.cs | 41 ++++++++++++++++++- .../Play/PlayerSettings/VisualSettings.cs | 15 ++++++- 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 9d31bc9bba..40a132a8e8 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -99,6 +99,9 @@ namespace osu.Game.Configuration Set(OsuSetting.IncreaseFirstObjectVisibility, true); + Set(OsuSetting.ShowPlayfieldArea, false); + Set(OsuSetting.PlayfieldAreaDimLevel, 0.1, 0, 1, 0.01); + // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -227,6 +230,8 @@ namespace osu.Game.Configuration IntroSequence, UIHoldActivationDelay, HitLighting, - MenuBackgroundSource + MenuBackgroundSource, + ShowPlayfieldArea, + PlayfieldAreaDimLevel } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 93a02ea0e4..ad02b54dd8 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -76,7 +76,19 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { LabelText = "Score display mode", Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) - } + }, + new SettingsCheckbox + { + LabelText = "Show playfield area", + Bindable = config.GetBindable(OsuSetting.ShowPlayfieldArea) + }, + new SettingsSlider + { + LabelText = "Playfield area dim", + Bindable = config.GetBindable(OsuSetting.PlayfieldAreaDimLevel), + KeyboardStep = 0.01f, + DisplayAsPercentage = true + }, }; } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index c52183f3f2..2ec84cca8c 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -12,6 +12,9 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mods; using osuTK; +using osu.Framework.Graphics.Shapes; +using osuTK.Graphics; +using osu.Game.Configuration; namespace osu.Game.Rulesets.UI { @@ -51,6 +54,10 @@ namespace osu.Game.Rulesets.UI /// public readonly BindableBool DisplayJudgements = new BindableBool(true); + private Bindable showPlayfieldArea; + private Bindable playfieldAreaDimLevel; + private Box playfieldArea; + /// /// Creates a new . /// @@ -65,7 +72,7 @@ namespace osu.Game.Rulesets.UI private IReadOnlyList mods { get; set; } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { Cursor = CreateCursor(); @@ -76,6 +83,38 @@ namespace osu.Game.Rulesets.UI AddInternal(Cursor); } + + showPlayfieldArea = config.GetBindable(OsuSetting.ShowPlayfieldArea); + playfieldAreaDimLevel = config.GetBindable(OsuSetting.PlayfieldAreaDimLevel); + showPlayfieldArea.ValueChanged += _ => UpdateVisuals(); + playfieldAreaDimLevel.ValueChanged += _ => UpdateVisuals(); + UpdateVisuals(); + } + protected virtual void UpdateVisuals() + { + if(playfieldArea == null) + { + if (showPlayfieldArea.Value) + { + AddInternal(playfieldArea = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.White, + Alpha = (float)playfieldAreaDimLevel.Value, + }); + } + } + else + { + if (showPlayfieldArea.Value) + { + playfieldArea.Alpha = (float)playfieldAreaDimLevel.Value; + } + else + { + playfieldArea.Alpha = 0; + } + } } /// diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index d6c66d0751..36e7c53132 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -14,9 +14,11 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerSliderBar dimSliderBar; private readonly PlayerSliderBar blurSliderBar; + private readonly PlayerSliderBar playfieldAreaDimSliderBar; private readonly PlayerCheckbox showStoryboardToggle; private readonly PlayerCheckbox beatmapSkinsToggle; private readonly PlayerCheckbox beatmapHitsoundsToggle; + private readonly PlayerCheckbox showPlayfieldAreaToggle; public VisualSettings() { @@ -39,12 +41,21 @@ namespace osu.Game.Screens.Play.PlayerSettings DisplayAsPercentage = true }, new OsuSpriteText + { + Text = "Playfieldd area dim:" + }, + playfieldAreaDimSliderBar = new PlayerSliderBar + { + DisplayAsPercentage = true + }, + new OsuSpriteText { Text = "Toggles:" }, showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboard / Video" }, beatmapSkinsToggle = new PlayerCheckbox { LabelText = "Beatmap skins" }, - beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } + beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" }, + showPlayfieldAreaToggle = new PlayerCheckbox { LabelText = "Show playfield area" } }; } @@ -53,9 +64,11 @@ namespace osu.Game.Screens.Play.PlayerSettings { dimSliderBar.Bindable = config.GetBindable(OsuSetting.DimLevel); blurSliderBar.Bindable = config.GetBindable(OsuSetting.BlurLevel); + playfieldAreaDimSliderBar.Bindable = config.GetBindable(OsuSetting.PlayfieldAreaDimLevel); showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); + showPlayfieldAreaToggle.Current = config.GetBindable(OsuSetting.ShowPlayfieldArea); } } } From 8121ccaad077e5a93088185ea0f92036af7ca6a1 Mon Sep 17 00:00:00 2001 From: Yao Chung Hu <30311066+FlashyReese@users.noreply.github.com> Date: Thu, 9 Jul 2020 15:00:26 -0500 Subject: [PATCH 2078/6909] Change Box to EditorPlayfieldBorder --- osu.Game/Rulesets/UI/Playfield.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 2ec84cca8c..f6eb74a030 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -12,9 +12,8 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mods; using osuTK; -using osu.Framework.Graphics.Shapes; -using osuTK.Graphics; using osu.Game.Configuration; +using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.UI { @@ -56,7 +55,7 @@ namespace osu.Game.Rulesets.UI private Bindable showPlayfieldArea; private Bindable playfieldAreaDimLevel; - private Box playfieldArea; + private EditorPlayfieldBorder playfieldArea; /// /// Creates a new . @@ -90,16 +89,16 @@ namespace osu.Game.Rulesets.UI playfieldAreaDimLevel.ValueChanged += _ => UpdateVisuals(); UpdateVisuals(); } + protected virtual void UpdateVisuals() { - if(playfieldArea == null) + if (playfieldArea == null) { if (showPlayfieldArea.Value) { - AddInternal(playfieldArea = new Box + AddInternal(playfieldArea = new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both, - Colour = Color4.White, Alpha = (float)playfieldAreaDimLevel.Value, }); } From 4c24388fc0a6ad7d14ffb50142ea5124c76fad4f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 10 Jul 2020 10:16:44 +0900 Subject: [PATCH 2079/6909] 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 2080/6909] 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 2081/6909] 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 2082/6909] 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 2083/6909] 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 2084/6909] 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 2085/6909] 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 2086/6909] 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 2087/6909] 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 2088/6909] 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 2089/6909] 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 2090/6909] 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 2091/6909] 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 2092/6909] 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 2093/6909] 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 2094/6909] 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 2095/6909] 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 2096/6909] 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 2097/6909] 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 2098/6909] 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 2099/6909] 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 2100/6909] 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 2101/6909] 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 2102/6909] 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 2103/6909] 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 2104/6909] 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 2105/6909] 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 2106/6909] 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 2107/6909] 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 2108/6909] 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 2109/6909] 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 0d95b768aa2e04f8543731afa71a31282c4c42c3 Mon Sep 17 00:00:00 2001 From: Yao Chung Hu <30311066+FlashyReese@users.noreply.github.com> Date: Fri, 10 Jul 2020 07:34:48 -0500 Subject: [PATCH 2110/6909] Rename and Move EditorPlayfieldBorder to PlayfieldBorder for general purpose --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 3 +- osu.Game/Rulesets/UI/Playfield.cs | 40 +------------------ ...rPlayfieldBorder.cs => PlayfieldBorder.cs} | 6 +-- 3 files changed, 6 insertions(+), 43 deletions(-) rename osu.Game/Screens/{Edit/Compose/Components/EditorPlayfieldBorder.cs => PlayfieldBorder.cs} (82%) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c25fb03fd0..6028ab77e1 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -20,6 +20,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; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Compose; @@ -108,7 +109,7 @@ namespace osu.Game.Rulesets.Edit drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[] { LayerBelowRuleset, - new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both } + new PlayfieldBorder { RelativeSizeAxes = Axes.Both } }), drawableRulesetWrapper, // layers above playfield diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index f6eb74a030..c52183f3f2 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -12,8 +12,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mods; using osuTK; -using osu.Game.Configuration; -using osu.Game.Screens.Edit.Compose.Components; namespace osu.Game.Rulesets.UI { @@ -53,10 +51,6 @@ namespace osu.Game.Rulesets.UI /// public readonly BindableBool DisplayJudgements = new BindableBool(true); - private Bindable showPlayfieldArea; - private Bindable playfieldAreaDimLevel; - private EditorPlayfieldBorder playfieldArea; - /// /// Creates a new . /// @@ -71,7 +65,7 @@ namespace osu.Game.Rulesets.UI private IReadOnlyList mods { get; set; } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load() { Cursor = CreateCursor(); @@ -82,38 +76,6 @@ namespace osu.Game.Rulesets.UI AddInternal(Cursor); } - - showPlayfieldArea = config.GetBindable(OsuSetting.ShowPlayfieldArea); - playfieldAreaDimLevel = config.GetBindable(OsuSetting.PlayfieldAreaDimLevel); - showPlayfieldArea.ValueChanged += _ => UpdateVisuals(); - playfieldAreaDimLevel.ValueChanged += _ => UpdateVisuals(); - UpdateVisuals(); - } - - protected virtual void UpdateVisuals() - { - if (playfieldArea == null) - { - if (showPlayfieldArea.Value) - { - AddInternal(playfieldArea = new EditorPlayfieldBorder - { - RelativeSizeAxes = Axes.Both, - Alpha = (float)playfieldAreaDimLevel.Value, - }); - } - } - else - { - if (showPlayfieldArea.Value) - { - playfieldArea.Alpha = (float)playfieldAreaDimLevel.Value; - } - else - { - playfieldArea.Alpha = 0; - } - } } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/EditorPlayfieldBorder.cs b/osu.Game/Screens/PlayfieldBorder.cs similarity index 82% rename from osu.Game/Screens/Edit/Compose/Components/EditorPlayfieldBorder.cs rename to osu.Game/Screens/PlayfieldBorder.cs index 4d956336b7..a3be38f0a2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/EditorPlayfieldBorder.cs +++ b/osu.Game/Screens/PlayfieldBorder.cs @@ -6,14 +6,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osuTK.Graphics; -namespace osu.Game.Screens.Edit.Compose.Components +namespace osu.Game.Screens { /// /// Provides a border around the playfield. /// - public class EditorPlayfieldBorder : CompositeDrawable + public class PlayfieldBorder : CompositeDrawable { - public EditorPlayfieldBorder() + public PlayfieldBorder() { RelativeSizeAxes = Axes.Both; From d40f209f4bbb15406b5512ad77585311ead25af4 Mon Sep 17 00:00:00 2001 From: Yao Chung Hu <30311066+FlashyReese@users.noreply.github.com> Date: Fri, 10 Jul 2020 07:36:21 -0500 Subject: [PATCH 2111/6909] Move Playfield Border to OsuPlayfield Ruleset --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 19 +++++++++++++++++++ osu.Game/Configuration/OsuConfigManager.cs | 6 ++---- .../Sections/Gameplay/GeneralSettings.cs | 13 +++---------- .../Play/PlayerSettings/VisualSettings.cs | 15 +-------------- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 4b1a2ce43c..3189db69a5 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -12,6 +12,10 @@ using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; +using osu.Framework.Allocation; +using osu.Game.Configuration; +using osu.Framework.Bindables; +using osu.Game.Screens; namespace osu.Game.Rulesets.Osu.UI { @@ -26,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); + private Bindable showPlayfieldBorder; + public OsuPlayfield() { InternalChildren = new Drawable[] @@ -56,6 +62,19 @@ namespace osu.Game.Rulesets.Osu.UI hitPolicy = new OrderedHitPolicy(HitObjectContainer); } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + showPlayfieldBorder = config.GetBindable(OsuSetting.ShowPlayfieldBorder); + if (showPlayfieldBorder.Value) + { + AddInternal(new PlayfieldBorder + { + RelativeSizeAxes = Axes.Both + }); + } + } + public override void Add(DrawableHitObject h) { h.OnNewResult += onNewResult; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 40a132a8e8..9ed73b7bd6 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -99,8 +99,7 @@ namespace osu.Game.Configuration Set(OsuSetting.IncreaseFirstObjectVisibility, true); - Set(OsuSetting.ShowPlayfieldArea, false); - Set(OsuSetting.PlayfieldAreaDimLevel, 0.1, 0, 1, 0.01); + Set(OsuSetting.ShowPlayfieldBorder, false); // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -231,7 +230,6 @@ namespace osu.Game.Configuration UIHoldActivationDelay, HitLighting, MenuBackgroundSource, - ShowPlayfieldArea, - PlayfieldAreaDimLevel + ShowPlayfieldBorder } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index ad02b54dd8..85eb61edff 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -79,16 +79,9 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }, new SettingsCheckbox { - LabelText = "Show playfield area", - Bindable = config.GetBindable(OsuSetting.ShowPlayfieldArea) - }, - new SettingsSlider - { - LabelText = "Playfield area dim", - Bindable = config.GetBindable(OsuSetting.PlayfieldAreaDimLevel), - KeyboardStep = 0.01f, - DisplayAsPercentage = true - }, + LabelText = "Show playfield border", + Bindable = config.GetBindable(OsuSetting.ShowPlayfieldBorder) + } }; } } diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index 36e7c53132..d6c66d0751 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -14,11 +14,9 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerSliderBar dimSliderBar; private readonly PlayerSliderBar blurSliderBar; - private readonly PlayerSliderBar playfieldAreaDimSliderBar; private readonly PlayerCheckbox showStoryboardToggle; private readonly PlayerCheckbox beatmapSkinsToggle; private readonly PlayerCheckbox beatmapHitsoundsToggle; - private readonly PlayerCheckbox showPlayfieldAreaToggle; public VisualSettings() { @@ -41,21 +39,12 @@ namespace osu.Game.Screens.Play.PlayerSettings DisplayAsPercentage = true }, new OsuSpriteText - { - Text = "Playfieldd area dim:" - }, - playfieldAreaDimSliderBar = new PlayerSliderBar - { - DisplayAsPercentage = true - }, - new OsuSpriteText { Text = "Toggles:" }, showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboard / Video" }, beatmapSkinsToggle = new PlayerCheckbox { LabelText = "Beatmap skins" }, - beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" }, - showPlayfieldAreaToggle = new PlayerCheckbox { LabelText = "Show playfield area" } + beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } }; } @@ -64,11 +53,9 @@ namespace osu.Game.Screens.Play.PlayerSettings { dimSliderBar.Bindable = config.GetBindable(OsuSetting.DimLevel); blurSliderBar.Bindable = config.GetBindable(OsuSetting.BlurLevel); - playfieldAreaDimSliderBar.Bindable = config.GetBindable(OsuSetting.PlayfieldAreaDimLevel); showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); - showPlayfieldAreaToggle.Current = config.GetBindable(OsuSetting.ShowPlayfieldArea); } } } From 13205319f3c910a12e8854da1607244d27da65f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 10 Jul 2020 22:37:29 +0900 Subject: [PATCH 2112/6909] 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 2113/6909] 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 2114/6909] 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 fa0c2d7f84985060a047be059fc0721e8c2fce0a Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 10 Jul 2020 10:42:41 -0700 Subject: [PATCH 2115/6909] Fix room name overflowing on multiplayer lounge --- .../Multi/Lounge/Components/RoomInfo.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomInfo.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomInfo.cs index 02f2667802..e6f6ce5ed2 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomInfo.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomInfo.cs @@ -4,9 +4,8 @@ using System.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.Containers; using osu.Game.Screens.Multi.Components; using osuTK; @@ -15,7 +14,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components public class RoomInfo : MultiplayerComposite { private readonly List statusElements = new List(); - private readonly SpriteText roomName; + private readonly OsuTextFlowContainer roomName; public RoomInfo() { @@ -43,18 +42,23 @@ namespace osu.Game.Screens.Multi.Lounge.Components { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Children = new Drawable[] { - roomName = new OsuSpriteText { Font = OsuFont.GetFont(size: 30) }, + roomName = new OsuTextFlowContainer(t => t.Font = OsuFont.GetFont(size: 30)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, statusInfo = new RoomStatusInfo(), } }, typeInfo = new ModeTypeInfo { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight } } }, From 789c921af1869435142f77c82fbdf5227c3a7af5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 11 Jul 2020 06:47:52 +0300 Subject: [PATCH 2116/6909] Move replies button to a new line --- osu.Game/Overlays/Comments/DrawableComment.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 46f600615a..2a63060385 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -163,20 +163,28 @@ namespace osu.Game.Overlays.Comments AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Right = 40 } }, - info = new FillFlowContainer + new FillFlowContainer { AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), + Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + info = new FillFlowContainer { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 12), - Colour = OsuColour.Gray(0.7f), - Text = HumanizerUtils.Humanize(Comment.CreatedAt) + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 12), + Colour = OsuColour.Gray(0.7f), + Text = HumanizerUtils.Humanize(Comment.CreatedAt) + }, + } }, repliesButton = new RepliesButton(Comment.RepliesCount) { From da249abd19382e9c3c6c0deeab0529f510314bd9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 11 Jul 2020 07:47:17 +0300 Subject: [PATCH 2117/6909] Implement CommentRepliesButton --- .../TestSceneCommentRepliesButton.cs | 54 ++++++++++ .../Comments/Buttons/CommentRepliesButton.cs | 100 ++++++++++++++++++ .../Comments/Buttons/LoadRepliesButton.cs | 10 ++ .../Comments/Buttons/ShowRepliesButton.cs | 19 ++++ 4 files changed, 183 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs create mode 100644 osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs create mode 100644 osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs create mode 100644 osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs new file mode 100644 index 0000000000..b4f518a5d0 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.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.Game.Overlays.Comments.Buttons; +using osu.Framework.Graphics; +using osu.Framework.Allocation; +using osu.Game.Overlays; +using osu.Framework.Graphics.Containers; +using osuTK; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneCommentRepliesButton : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + public TestSceneCommentRepliesButton() + { + Child = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new TestButton + { + Action = () => { } + }, + new LoadRepliesButton + { + Action = () => { } + }, + new ShowRepliesButton(1) + { + Action = () => { } + }, + new ShowRepliesButton(2) + { + Action = () => { } + } + } + }; + } + + private class TestButton : CommentRepliesButton + { + protected override string GetText() => "sample text"; + } + } +} diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs new file mode 100644 index 0000000000..13924200c2 --- /dev/null +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -0,0 +1,100 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +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.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Comments.Buttons +{ + public abstract class CommentRepliesButton : OsuHoverContainer + { + protected override IEnumerable EffectTargets => new[] { background }; + + protected ChevronIcon Icon; + private Box background; + + public CommentRepliesButton() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Add(new CircularContainer + { + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + new Container + { + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding + { + Vertical = 5, + Horizontal = 10, + }, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(15, 0), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Text = GetText() + }, + Icon = new ChevronIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + } + } + } + } + } + }); + + IdleColour = colourProvider.Background2; + HoverColour = colourProvider.Background1; + } + + protected abstract string GetText(); + + protected class ChevronIcon : SpriteIcon + { + public ChevronIcon() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Size = new Vector2(7.5f); + Icon = FontAwesome.Solid.ChevronDown; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Foreground1; + } + } + } +} diff --git a/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs new file mode 100644 index 0000000000..41cce72272 --- /dev/null +++ b/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.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.Comments.Buttons +{ + public class LoadRepliesButton : CommentRepliesButton + { + protected override string GetText() => "load replies"; + } +} diff --git a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs new file mode 100644 index 0000000000..1e8c732453 --- /dev/null +++ b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.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 Humanizer; + +namespace osu.Game.Overlays.Comments.Buttons +{ + public class ShowRepliesButton : CommentRepliesButton + { + private readonly int count; + + public ShowRepliesButton(int count) + { + this.count = count; + } + + protected override string GetText() => "reply".ToQuantity(count); + } +} From 0861ee0c8e985bb413681037ab54ea00f1910d82 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 11 Jul 2020 07:54:37 +0300 Subject: [PATCH 2118/6909] Make Icon rotate when clicking ShowRepliesButton --- .../Comments/Buttons/ShowRepliesButton.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs index 1e8c732453..2381727431 100644 --- a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs @@ -2,11 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using Humanizer; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osuTK; namespace osu.Game.Overlays.Comments.Buttons { public class ShowRepliesButton : CommentRepliesButton { + public readonly BindableBool Expanded = new BindableBool(true); + private readonly int count; public ShowRepliesButton(int count) @@ -14,6 +20,23 @@ namespace osu.Game.Overlays.Comments.Buttons this.count = count; } + protected override void LoadComplete() + { + base.LoadComplete(); + Expanded.BindValueChanged(onExpandedChanged, true); + } + + private void onExpandedChanged(ValueChangedEvent expanded) + { + Icon.ScaleTo(new Vector2(1, expanded.NewValue ? -1 : 1)); + } + + protected override bool OnClick(ClickEvent e) + { + Expanded.Toggle(); + return base.OnClick(e); + } + protected override string GetText() => "reply".ToQuantity(count); } } From 42d3288f176d312833cad45f23cb69e73718e48c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 11 Jul 2020 08:01:11 +0300 Subject: [PATCH 2119/6909] Update old buttons usage --- osu.Game/Overlays/Comments/DrawableComment.cs | 50 ++++--------------- 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 2a63060385..7bd5e22038 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -16,12 +16,12 @@ using System.Linq; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; using osu.Framework.Allocation; -using osuTK.Graphics; using System.Collections.Generic; using System; using osu.Framework.Graphics.Shapes; using osu.Framework.Extensions.IEnumerableExtensions; using System.Collections.Specialized; +using osu.Game.Overlays.Comments.Buttons; namespace osu.Game.Overlays.Comments { @@ -46,9 +46,9 @@ namespace osu.Game.Overlays.Comments private FillFlowContainer childCommentsVisibilityContainer; private FillFlowContainer childCommentsContainer; - private LoadMoreCommentsButton loadMoreCommentsButton; + private LoadRepliesButton loadRepliesButton; private ShowMoreButton showMoreButton; - private RepliesButton repliesButton; + private ShowRepliesButton showRepliesButton; private ChevronButton chevronButton; private DeletedCommentsCounter deletedCommentsCounter; @@ -186,11 +186,11 @@ namespace osu.Game.Overlays.Comments }, } }, - repliesButton = new RepliesButton(Comment.RepliesCount) + showRepliesButton = new ShowRepliesButton(Comment.RepliesCount) { Expanded = { BindTarget = childrenExpanded } }, - loadMoreCommentsButton = new LoadMoreCommentsButton + loadRepliesButton = new LoadRepliesButton { Action = () => RepliesRequested(this, ++currentPage) } @@ -347,14 +347,16 @@ namespace osu.Game.Overlays.Comments var loadedReplesCount = loadedReplies.Count; var hasUnloadedReplies = loadedReplesCount != Comment.RepliesCount; - loadMoreCommentsButton.FadeTo(hasUnloadedReplies && loadedReplesCount == 0 ? 1 : 0); + loadRepliesButton.FadeTo(hasUnloadedReplies && loadedReplesCount == 0 ? 1 : 0); showMoreButton.FadeTo(hasUnloadedReplies && loadedReplesCount > 0 ? 1 : 0); - repliesButton.FadeTo(loadedReplesCount != 0 ? 1 : 0); + showRepliesButton.FadeTo(loadedReplesCount != 0 ? 1 : 0); if (Comment.IsTopLevel) chevronButton.FadeTo(loadedReplesCount != 0 ? 1 : 0); - showMoreButton.IsLoading = loadMoreCommentsButton.IsLoading = false; + showMoreButton.IsLoading = false; + + //loadRepliesButton.IsLoading = false; } private class ChevronButton : ShowChildrenButton @@ -375,38 +377,6 @@ namespace osu.Game.Overlays.Comments } } - private class RepliesButton : ShowChildrenButton - { - private readonly SpriteText text; - private readonly int count; - - public RepliesButton(int count) - { - this.count = count; - - Child = text = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - }; - } - - protected override void OnExpandedChanged(ValueChangedEvent expanded) - { - text.Text = $@"{(expanded.NewValue ? "[-]" : "[+]")} replies ({count})"; - } - } - - private class LoadMoreCommentsButton : GetCommentRepliesButton - { - public LoadMoreCommentsButton() - { - IdleColour = OsuColour.Gray(0.7f); - HoverColour = Color4.White; - } - - protected override string GetText() => @"[+] load replies"; - } - private class ShowMoreButton : GetCommentRepliesButton { [BackgroundDependencyLoader] From b1b2e961bc0027b6305751ad469bf60e69186ee2 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 11 Jul 2020 08:13:11 +0300 Subject: [PATCH 2120/6909] Update arrow colour on hover --- .../TestSceneCommentRepliesButton.cs | 18 +-- .../Comments/Buttons/CommentRepliesButton.cs | 125 ++++++++++-------- 2 files changed, 70 insertions(+), 73 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs index b4f518a5d0..e62092a180 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs @@ -26,22 +26,10 @@ namespace osu.Game.Tests.Visual.UserInterface Spacing = new Vector2(0, 10), Children = new Drawable[] { - new TestButton - { - Action = () => { } - }, - new LoadRepliesButton - { - Action = () => { } - }, - new ShowRepliesButton(1) - { - Action = () => { } - }, + new TestButton(), + new LoadRepliesButton(), + new ShowRepliesButton(1), new ShowRepliesButton(2) - { - Action = () => { } - } } }; } diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index 13924200c2..7ea256d113 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -1,100 +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; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osuTK; namespace osu.Game.Overlays.Comments.Buttons { - public abstract class CommentRepliesButton : OsuHoverContainer + public abstract class CommentRepliesButton : CompositeDrawable { - protected override IEnumerable EffectTargets => new[] { background }; + public Action Action { get; set; } - protected ChevronIcon Icon; + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + protected SpriteIcon Icon; private Box background; - public CommentRepliesButton() + [BackgroundDependencyLoader] + private void load() { AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Add(new CircularContainer + InternalChildren = new Drawable[] { - AutoSizeAxes = Axes.Both, - Masking = true, - Children = new Drawable[] + new CircularContainer { - background = new Box + AutoSizeAxes = Axes.Both, + Masking = true, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - new Container - { - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding + background = new Box { - Vertical = 5, - Horizontal = 10, + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background2 }, - Child = new FillFlowContainer + new Container { AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(15, 0), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] + Margin = new MarginPadding { - new OsuSpriteText + Vertical = 5, + Horizontal = 10, + }, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(15, 0), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = GetText() - }, - Icon = new ChevronIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Text = GetText() + }, + Icon = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(7.5f), + Icon = FontAwesome.Solid.ChevronDown, + Colour = colourProvider.Foreground1 + } } } } } - } - }); - - IdleColour = colourProvider.Background2; - HoverColour = colourProvider.Background1; + }, + new HoverClickSounds(), + }; } protected abstract string GetText(); - protected class ChevronIcon : SpriteIcon + protected override bool OnHover(HoverEvent e) { - public ChevronIcon() - { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - Size = new Vector2(7.5f); - Icon = FontAwesome.Solid.ChevronDown; - } + base.OnHover(e); + background.FadeColour(colourProvider.Background1, 200, Easing.OutQuint); + Icon.FadeColour(colourProvider.Light1, 200, Easing.OutQuint); + return true; + } - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Colour = colourProvider.Foreground1; - } + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint); + Icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); + } + + protected override bool OnClick(ClickEvent e) + { + Action?.Invoke(); + return base.OnClick(e); } } } From 84392d0d130242616ded329ac711355bbc7c8c0c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 11 Jul 2020 08:50:35 +0300 Subject: [PATCH 2121/6909] Add loading spinner --- osu.Game/Overlays/Comments/DrawableComment.cs | 43 +++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 7bd5e22038..fef8194712 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -22,6 +22,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Extensions.IEnumerableExtensions; using System.Collections.Specialized; using osu.Game.Overlays.Comments.Buttons; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Comments { @@ -51,6 +52,7 @@ namespace osu.Game.Overlays.Comments private ShowRepliesButton showRepliesButton; private ChevronButton chevronButton; private DeletedCommentsCounter deletedCommentsCounter; + private Loading loading; public DrawableComment(Comment comment) { @@ -192,7 +194,12 @@ namespace osu.Game.Overlays.Comments }, loadRepliesButton = new LoadRepliesButton { - Action = () => RepliesRequested(this, ++currentPage) + Action = () => + { + RepliesRequested(this, ++currentPage); + loadRepliesButton.Hide(); + loading.Show(); + } } } } @@ -202,6 +209,11 @@ namespace osu.Game.Overlays.Comments } } }, + loading = new Loading + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, childCommentsVisibilityContainer = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -355,8 +367,7 @@ namespace osu.Game.Overlays.Comments chevronButton.FadeTo(loadedReplesCount != 0 ? 1 : 0); showMoreButton.IsLoading = false; - - //loadRepliesButton.IsLoading = false; + loading.Hide(); } private class ChevronButton : ShowChildrenButton @@ -427,5 +438,31 @@ namespace osu.Game.Overlays.Comments return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? @"deleted" : string.Empty; } } + + private class Loading : Container + { + private readonly LoadingSpinner loading; + + public Loading() + { + Child = loading = new LoadingSpinner + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Size = new Vector2(15), + Margin = new MarginPadding + { + Vertical = 5, + Left = 80 + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + loading.Show(); + } + } } } From 024ccc75eefcec4e62b5a6a79d658df8a421657b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 11 Jul 2020 09:03:03 +0300 Subject: [PATCH 2122/6909] Adjust margins/paddings --- osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs | 4 ++++ osu.Game/Overlays/Comments/DrawableComment.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index 7ea256d113..f4bab3d9d7 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -29,6 +29,10 @@ namespace osu.Game.Overlays.Comments.Buttons private void load() { AutoSizeAxes = Axes.Both; + Margin = new MarginPadding + { + Vertical = 2 + }; InternalChildren = new Drawable[] { new CircularContainer diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index fef8194712..813540b28d 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -83,7 +83,7 @@ namespace osu.Game.Overlays.Comments { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(margin) { Left = margin + 5 }, + Padding = new MarginPadding(margin) { Left = margin + 5, Top = Comment.IsTopLevel ? 10 : 0 }, Child = content = new GridContainer { RelativeSizeAxes = Axes.X, From acfb6eecc65b5a3b66af8f44d155178f7b0cbcf7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 11 Jul 2020 20:17:40 +0900 Subject: [PATCH 2123/6909] 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 2124/6909] 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 2125/6909] 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; } /// From 9b4bed2ab223e8e81aafc49d9ee0d9012aa0e740 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 11 Jul 2020 16:02:47 -0700 Subject: [PATCH 2126/6909] Add ability to select mods from a specific score --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 13 +++++++++++++ osu.Game/Overlays/Mods/ModSection.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 +++--- osu.Game/Screens/Select/SongSelect.cs | 2 +- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 1469f29874..c1771fbf3d 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -58,6 +58,9 @@ namespace osu.Game.Online.Leaderboards [Resolved(CanBeNull = true)] private DialogOverlay dialogOverlay { get; set; } + [Resolved(CanBeNull = true)] + private SongSelect songSelect { get; set; } + public LeaderboardScore(ScoreInfo score, int? rank, bool allowHighlight = true) { this.score = score; @@ -373,11 +376,21 @@ namespace osu.Game.Online.Leaderboards { List items = new List(); + if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered)) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => getMods())); + if (score.ID != 0) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); return items.ToArray(); } } + + private void getMods() + { + songSelect.ModSelect.DeselectAll(true); + + songSelect.Mods.Value = score.Mods; + } } } diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 7235a18a23..45fae90a1e 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Mods return base.OnKeyDown(e); } - public void DeselectAll() => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); + public void DeselectAll(bool immediate = false) => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null), immediate); /// /// Deselect one or more mods in this section. diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 3d0ad1a594..d83dd77401 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -231,7 +231,7 @@ namespace osu.Game.Overlays.Mods { Width = 180, Text = "Deselect All", - Action = DeselectAll, + Action = () => DeselectAll(), Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, @@ -328,10 +328,10 @@ namespace osu.Game.Overlays.Mods sampleOff = audio.Samples.Get(@"UI/check-off"); } - public void DeselectAll() + public void DeselectAll(bool immediate = false) { foreach (var section in ModSectionsContainer.Children) - section.DeselectAll(); + section.DeselectAll(immediate); refreshSelectedMods(); } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index e3705b15fa..23fa3cd724 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select [Resolved] private BeatmapManager beatmaps { get; set; } - protected ModSelectOverlay ModSelect { get; private set; } + public ModSelectOverlay ModSelect { get; private set; } protected SampleChannel SampleConfirm { get; private set; } From 0d26ad9ddb11c46e64377eadec5fded63de37a9a Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 11 Jul 2020 16:22:01 -0700 Subject: [PATCH 2127/6909] Fix user top score not having a context menu --- osu.Game/Online/Leaderboards/Leaderboard.cs | 38 ++++++++++----------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index e2a817aaff..d51bf963fc 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -170,36 +170,36 @@ namespace osu.Game.Online.Leaderboards { InternalChildren = new Drawable[] { - new GridContainer + new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Child = new GridContainer { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new OsuContextMenuContainer + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Child = scrollContainer = new OsuScrollContainer + scrollContainer = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, } + }, + new Drawable[] + { + content = new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }, } }, - new Drawable[] - { - content = new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - }, - } }, }, loading = new LoadingSpinner(), From 25d2d9ba5c19252574371d41f3699edd7ec27a33 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 11 Jul 2020 16:24:57 -0700 Subject: [PATCH 2128/6909] Convert getMods reference to method group --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index c1771fbf3d..7203683711 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -377,7 +377,7 @@ namespace osu.Game.Online.Leaderboards List items = new List(); if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered)) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => getMods())); + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, getMods)); if (score.ID != 0) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); From 4d7dc9f5eb9e0cd2e34b4ecc7296b59c081d1beb Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 11 Jul 2020 18:27:47 -0700 Subject: [PATCH 2129/6909] Fix color and underline of tab control checkboxes when initially checked --- .../Graphics/UserInterface/OsuTabControlCheckbox.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs index 544acc7eb2..b9afc2fa1f 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs @@ -32,12 +32,6 @@ namespace osu.Game.Graphics.UserInterface { accentColour = value; - if (Current.Value) - { - text.Colour = AccentColour; - icon.Colour = AccentColour; - } - updateFade(); } } @@ -89,6 +83,8 @@ namespace osu.Game.Graphics.UserInterface { icon.Icon = selected.NewValue ? FontAwesome.Regular.CheckCircle : FontAwesome.Regular.Circle; text.Font = text.Font.With(weight: selected.NewValue ? FontWeight.Bold : FontWeight.Medium); + + updateFade(); }; } @@ -115,8 +111,8 @@ namespace osu.Game.Graphics.UserInterface private void updateFade() { - box.FadeTo(IsHovered ? 1 : 0, transition_length, Easing.OutQuint); - text.FadeColour(IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint); + box.FadeTo(Current.Value || IsHovered ? 1 : 0, transition_length, Easing.OutQuint); + text.FadeColour(Current.Value || IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint); } } } From 681f0015255c0b269a305d6aff3308e3da9c6ab0 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 11 Jul 2020 19:19:34 -0700 Subject: [PATCH 2130/6909] Convert icon to local variable --- osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs index b9afc2fa1f..bdc95ee048 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs @@ -21,7 +21,6 @@ namespace osu.Game.Graphics.UserInterface { private readonly Box box; private readonly SpriteText text; - private readonly SpriteIcon icon; private Color4? accentColour; @@ -46,6 +45,8 @@ namespace osu.Game.Graphics.UserInterface public OsuTabControlCheckbox() { + SpriteIcon icon; + AutoSizeAxes = Axes.Both; Children = new Drawable[] From da40f29b4405990a3db8150fbd0fafa4f34f9f83 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 12 Jul 2020 12:27:26 +0300 Subject: [PATCH 2131/6909] Make button text a property --- .../TestSceneCommentRepliesButton.cs | 5 ++- .../Comments/Buttons/CommentRepliesButton.cs | 32 ++++++++++++------- .../Comments/Buttons/LoadRepliesButton.cs | 5 ++- .../Comments/Buttons/ShowRepliesButton.cs | 6 +--- 4 files changed, 29 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs index e62092a180..baeb1ae822 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs @@ -36,7 +36,10 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestButton : CommentRepliesButton { - protected override string GetText() => "sample text"; + public TestButton() + { + Text = "sample text"; + } } } } diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index f4bab3d9d7..1121ac5327 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -19,14 +19,20 @@ namespace osu.Game.Overlays.Comments.Buttons { public Action Action { get; set; } + protected string Text + { + get => text.Text; + set => text.Text = value; + } + [Resolved] private OverlayColourProvider colourProvider { get; set; } - protected SpriteIcon Icon; - private Box background; + protected readonly SpriteIcon Icon; + private readonly Box background; + private readonly OsuSpriteText text; - [BackgroundDependencyLoader] - private void load() + public CommentRepliesButton() { AutoSizeAxes = Axes.Both; Margin = new MarginPadding @@ -43,8 +49,7 @@ namespace osu.Game.Overlays.Comments.Buttons { background = new Box { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background2 + RelativeSizeAxes = Axes.Both }, new Container { @@ -63,20 +68,18 @@ namespace osu.Game.Overlays.Comments.Buttons Origin = Anchor.Centre, Children = new Drawable[] { - new OsuSpriteText + text = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = GetText() + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) }, Icon = new SpriteIcon { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Size = new Vector2(7.5f), - Icon = FontAwesome.Solid.ChevronDown, - Colour = colourProvider.Foreground1 + Icon = FontAwesome.Solid.ChevronDown } } } @@ -87,7 +90,12 @@ namespace osu.Game.Overlays.Comments.Buttons }; } - protected abstract string GetText(); + [BackgroundDependencyLoader] + private void load() + { + background.Colour = colourProvider.Background2; + Icon.Colour = colourProvider.Foreground1; + } protected override bool OnHover(HoverEvent e) { diff --git a/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs index 41cce72272..9387c95758 100644 --- a/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs @@ -5,6 +5,9 @@ namespace osu.Game.Overlays.Comments.Buttons { public class LoadRepliesButton : CommentRepliesButton { - protected override string GetText() => "load replies"; + public LoadRepliesButton() + { + Text = "load replies"; + } } } diff --git a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs index 2381727431..01c2e8a7a7 100644 --- a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs @@ -13,11 +13,9 @@ namespace osu.Game.Overlays.Comments.Buttons { public readonly BindableBool Expanded = new BindableBool(true); - private readonly int count; - public ShowRepliesButton(int count) { - this.count = count; + Text = "reply".ToQuantity(count); } protected override void LoadComplete() @@ -36,7 +34,5 @@ namespace osu.Game.Overlays.Comments.Buttons Expanded.Toggle(); return base.OnClick(e); } - - protected override string GetText() => "reply".ToQuantity(count); } } From c3524cbe57f60e826df8b12fdcdf5bb178497b80 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 12 Jul 2020 12:32:26 +0300 Subject: [PATCH 2132/6909] Make icon private but expose a protected method --- .../Comments/Buttons/CommentRepliesButton.cs | 12 +++++++----- .../Overlays/Comments/Buttons/ShowRepliesButton.cs | 9 +-------- 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index 1121ac5327..dd02a35a06 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Comments.Buttons [Resolved] private OverlayColourProvider colourProvider { get; set; } - protected readonly SpriteIcon Icon; + private readonly SpriteIcon icon; private readonly Box background; private readonly OsuSpriteText text; @@ -74,7 +74,7 @@ namespace osu.Game.Overlays.Comments.Buttons Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) }, - Icon = new SpriteIcon + icon = new SpriteIcon { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, @@ -94,14 +94,16 @@ namespace osu.Game.Overlays.Comments.Buttons private void load() { background.Colour = colourProvider.Background2; - Icon.Colour = colourProvider.Foreground1; + icon.Colour = colourProvider.Foreground1; } + protected void ToggleIcon(bool upwards) => icon.ScaleTo(new Vector2(1, upwards ? -1 : 1)); + protected override bool OnHover(HoverEvent e) { base.OnHover(e); background.FadeColour(colourProvider.Background1, 200, Easing.OutQuint); - Icon.FadeColour(colourProvider.Light1, 200, Easing.OutQuint); + icon.FadeColour(colourProvider.Light1, 200, Easing.OutQuint); return true; } @@ -109,7 +111,7 @@ namespace osu.Game.Overlays.Comments.Buttons { base.OnHoverLost(e); background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint); - Icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); + icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs index 01c2e8a7a7..118cac5b4c 100644 --- a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs @@ -3,9 +3,7 @@ using Humanizer; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Input.Events; -using osuTK; namespace osu.Game.Overlays.Comments.Buttons { @@ -21,12 +19,7 @@ namespace osu.Game.Overlays.Comments.Buttons protected override void LoadComplete() { base.LoadComplete(); - Expanded.BindValueChanged(onExpandedChanged, true); - } - - private void onExpandedChanged(ValueChangedEvent expanded) - { - Icon.ScaleTo(new Vector2(1, expanded.NewValue ? -1 : 1)); + Expanded.BindValueChanged(expanded => ToggleIcon(expanded.NewValue), true); } protected override bool OnClick(ClickEvent e) From be36a4b7686a120641a7ec29f727a2898dd54053 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 12 Jul 2020 12:39:48 +0300 Subject: [PATCH 2133/6909] Make ctor protected --- osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index dd02a35a06..65648f6751 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.Comments.Buttons private readonly Box background; private readonly OsuSpriteText text; - public CommentRepliesButton() + protected CommentRepliesButton() { AutoSizeAxes = Axes.Both; Margin = new MarginPadding From c9d21894e5aeba8e2ddbf7c97779584bbc95ac93 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 12 Jul 2020 13:19:28 +0300 Subject: [PATCH 2134/6909] Add test for icon toggle --- .../TestSceneCommentRepliesButton.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs index baeb1ae822..73fe66d6eb 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs @@ -7,6 +7,9 @@ using osu.Framework.Allocation; using osu.Game.Overlays; using osu.Framework.Graphics.Containers; using osuTK; +using NUnit.Framework; +using System.Linq; +using osu.Framework.Graphics.Sprites; namespace osu.Game.Tests.Visual.UserInterface { @@ -15,6 +18,8 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + private readonly TestButton button; + public TestSceneCommentRepliesButton() { Child = new FillFlowContainer @@ -26,7 +31,7 @@ namespace osu.Game.Tests.Visual.UserInterface Spacing = new Vector2(0, 10), Children = new Drawable[] { - new TestButton(), + button = new TestButton(), new LoadRepliesButton(), new ShowRepliesButton(1), new ShowRepliesButton(2) @@ -34,12 +39,25 @@ namespace osu.Game.Tests.Visual.UserInterface }; } + [Test] + public void TestArrowRotation() + { + AddStep("Toggle icon up", () => button.ToggleIcon(true)); + AddAssert("Icon facing upwards", () => button.Icon.Scale.Y == -1); + AddStep("Toggle icon down", () => button.ToggleIcon(false)); + AddAssert("Icon facing downwards", () => button.Icon.Scale.Y == 1); + } + private class TestButton : CommentRepliesButton { + public SpriteIcon Icon => InternalChildren.OfType().First().Children.OfType().First().Children.OfType().First().Children.OfType().First(); + public TestButton() { Text = "sample text"; } + + public new void ToggleIcon(bool upwards) => base.ToggleIcon(upwards); } } } From 0d6dbb652315f7f8e341ddf2478617308e3ac59f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 12 Jul 2020 13:36:42 +0300 Subject: [PATCH 2135/6909] Clean up exposed icon for tests --- .../Visual/UserInterface/TestSceneCommentRepliesButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs index 73fe66d6eb..7f5806705e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs @@ -10,6 +10,7 @@ using osuTK; using NUnit.Framework; using System.Linq; using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; namespace osu.Game.Tests.Visual.UserInterface { @@ -50,7 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestButton : CommentRepliesButton { - public SpriteIcon Icon => InternalChildren.OfType().First().Children.OfType().First().Children.OfType().First().Children.OfType().First(); + public SpriteIcon Icon => this.ChildrenOfType().First(); public TestButton() { From d18609e9003548a2f21af720d3c3357708bdc6f7 Mon Sep 17 00:00:00 2001 From: vntxx <38376434+vntxx@users.noreply.github.com> Date: Fri, 10 Jul 2020 21:05:23 +0200 Subject: [PATCH 2136/6909] Added notifications keybinding Implementation of #9502 --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ osu.Game/OsuGame.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 618798a6d8..8a5acbadbc 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -35,6 +35,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleDirect), + new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), new KeyBinding(InputKey.Escape, GlobalAction.Back), new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), @@ -157,5 +158,8 @@ namespace osu.Game.Input.Bindings [Description("Home")] Home, + + [Description("Toggle notifications")] + ToggleNotifications } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 92233f143d..47a7c2ae11 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -890,6 +890,10 @@ namespace osu.Game beatmapListing.ToggleVisibility(); return true; + case GlobalAction.ToggleNotifications: + notifications.ToggleVisibility(); + return true; + case GlobalAction.ToggleGameplayMouseButtons: LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get(OsuSetting.MouseDisableButtons)); return true; From 0e49bf127ba527d1cccf1a6d536d56e966661a1d Mon Sep 17 00:00:00 2001 From: LastExceed Date: Sun, 12 Jul 2020 13:57:06 +0200 Subject: [PATCH 2137/6909] wrap HitObjectContainer in BufferedContainer --- osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs index ba5281a1a2..5eccb891cc 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.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Skinning; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; @@ -17,9 +18,10 @@ namespace osu.Game.Rulesets.Mania.UI.Components public HitObjectArea(HitObjectContainer hitObjectContainer) { - InternalChildren = new[] + InternalChild = new BufferedContainer { - hitObjectContainer, + RelativeSizeAxes = Axes.Both, + Child = hitObjectContainer }; } From 06ed5316c4a723cf98815d4fcc49bae0b40a2c9e Mon Sep 17 00:00:00 2001 From: LastExceed Date: Sun, 12 Jul 2020 13:57:36 +0200 Subject: [PATCH 2138/6909] expose hitObectArea in Column --- osu.Game.Rulesets.Mania/UI/Column.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 511d6c8623..d69858c41c 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable Action = new Bindable(); - private readonly ColumnHitObjectArea hitObjectArea; + public readonly ColumnHitObjectArea hitObjectArea; internal readonly Container TopLevelContainer; From 1cf8b599a1f6e52d6453a43e727f13c9d40e63ca Mon Sep 17 00:00:00 2001 From: LastExceed Date: Sun, 12 Jul 2020 13:57:44 +0200 Subject: [PATCH 2139/6909] implement fadein --- .../Mods/ManiaModFadeIn.cs | 93 ++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index 4c125ad6ef..d2b1307585 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -2,14 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; +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.Testing; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModFadeIn : Mod + public class ManiaModFadeIn : Mod, IApplicableToDrawableRuleset { public override string Name => "Fade In"; public override string Acronym => "FI"; @@ -19,5 +29,86 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; + + private const float lanecover_size_filled = 0.5f; + private const float lanecover_size_gradient = 0.25f; + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; + + foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) + { + column.hitObjectArea.ChildrenOfType().First().Add(new LaneCover(false) + { + RelativeSizeAxes = Axes.Both, + SizeFilled = lanecover_size_filled, + SizeGradient = lanecover_size_gradient + }); + } + } + + private class LaneCover : CompositeDrawable + { + private readonly Box gradient; + private readonly Box filled; + private readonly bool reversed; + + public LaneCover(bool reversed) + { + Blending = new BlendingParameters + { + RGBEquation = BlendingEquation.Add, + Source = BlendingType.Zero, + Destination = BlendingType.One, + AlphaEquation = BlendingEquation.Add, + SourceAlpha = BlendingType.Zero, + DestinationAlpha = BlendingType.OneMinusSrcAlpha + }; + + InternalChildren = new Drawable[] + { + gradient = new Box + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Colour = ColourInfo.GradientVertical(Color4.White.Opacity(1f), Color4.White.Opacity(0f)) + }, + filled = new Box + { + RelativeSizeAxes = Axes.Both + } + }; + + if (reversed) + { + gradient.Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0f), Color4.White.Opacity(1f)); + filled.Anchor = Anchor.BottomLeft; + filled.Origin = Anchor.BottomLeft; + } + + this.reversed = reversed; + } + + public float SizeFilled + { + set + { + filled.Height = value; + if (!reversed) + gradient.Y = value; + } + } + + public float SizeGradient + { + set + { + gradient.Height = value; + if (reversed) + gradient.Y = 1 - value - filled.Height; + } + } + } } } From 3606febe3184eb5b1ae8ee04041e89f9024da973 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Sun, 12 Jul 2020 14:23:55 +0200 Subject: [PATCH 2140/6909] fix case convention violation --- osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs | 2 +- osu.Game.Rulesets.Mania/UI/Column.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index d2b1307585..f083d731c8 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { - column.hitObjectArea.ChildrenOfType().First().Add(new LaneCover(false) + column.HitObjectArea.ChildrenOfType().First().Add(new LaneCover(false) { RelativeSizeAxes = Axes.Both, SizeFilled = lanecover_size_filled, diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index d69858c41c..642353bd0b 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable Action = new Bindable(); - public readonly ColumnHitObjectArea hitObjectArea; + public readonly ColumnHitObjectArea HitObjectArea; internal readonly Container TopLevelContainer; - public Container UnderlayElements => hitObjectArea.UnderlayElements; + public Container UnderlayElements => HitObjectArea.UnderlayElements; public Column(int index) { @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.UI { // For input purposes, the background is added at the highest depth, but is then proxied back below all other elements background.CreateProxy(), - hitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, + HitObjectArea = new ColumnHitObjectArea(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.KeyArea, Index), _ => new DefaultKeyArea()) { RelativeSizeAxes = Axes.Both @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } }; - TopLevelContainer.Add(hitObjectArea.Explosions.CreateProxy()); + TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); } public override Axes RelativeSizeAxes => Axes.Y; @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Both }; - hitObjectArea.Explosions.Add(explosion); + HitObjectArea.Explosions.Add(explosion); explosion.Delay(200).Expire(true); } From 598e48678e2e143b6671a9cd2c9f55dad36f3bff Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 12 Jul 2020 15:45:48 +0300 Subject: [PATCH 2141/6909] Refactor NewsHeader --- osu.Game/Overlays/News/NewsHeader.cs | 54 ++++++++++++++-------------- osu.Game/Overlays/NewsOverlay.cs | 19 ++++++---- 2 files changed, 41 insertions(+), 32 deletions(-) diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index ee7991c0c6..ddada2bdaf 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -8,41 +9,42 @@ namespace osu.Game.Overlays.News { public class NewsHeader : BreadcrumbControlOverlayHeader { - public const string FRONT_PAGE_STRING = "frontpage"; + private const string front_page_string = "frontpage"; - public readonly Bindable Post = new Bindable(FRONT_PAGE_STRING); + public Action ShowFrontPage; + + private readonly Bindable article = new Bindable(null); public NewsHeader() { - TabControl.AddItem(FRONT_PAGE_STRING); - Current.Value = FRONT_PAGE_STRING; - Current.BindValueChanged(onCurrentChanged); - Post.BindValueChanged(onPostChanged, true); - } + TabControl.AddItem(front_page_string); - public void SetFrontPage() => Post.Value = FRONT_PAGE_STRING; - - public void SetArticle(string slug) => Post.Value = slug; - - private void onCurrentChanged(ValueChangedEvent current) - { - if (current.NewValue == FRONT_PAGE_STRING) - Post.Value = FRONT_PAGE_STRING; - } - - private void onPostChanged(ValueChangedEvent post) - { - if (post.OldValue != FRONT_PAGE_STRING) - TabControl.RemoveItem(post.OldValue); - - if (post.NewValue != FRONT_PAGE_STRING) + Current.BindValueChanged(e => { - TabControl.AddItem(post.NewValue); - Current.Value = post.NewValue; + if (e.NewValue == front_page_string) + ShowFrontPage?.Invoke(); + }); + + article.BindValueChanged(onArticleChanged, true); + } + + public void SetFrontPage() => article.Value = null; + + public void SetArticle(string slug) => article.Value = slug; + + private void onArticleChanged(ValueChangedEvent e) + { + if (e.OldValue != null) + TabControl.RemoveItem(e.OldValue); + + if (e.NewValue != null) + { + TabControl.AddItem(e.NewValue); + Current.Value = e.NewValue; } else { - Current.Value = FRONT_PAGE_STRING; + Current.Value = front_page_string; } } diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index db989e71bf..a5687b77e2 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -15,6 +15,8 @@ namespace osu.Game.Overlays { public class NewsOverlay : FullscreenOverlay { + private readonly Bindable article = new Bindable(null); + private Container content; private LoadingLayer loading; private NewsHeader header; @@ -46,7 +48,10 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new NewsHeader(), + header = new NewsHeader + { + ShowFrontPage = ShowFrontPage + }, content = new Container { RelativeSizeAxes = Axes.X, @@ -62,34 +67,36 @@ namespace osu.Game.Overlays protected override void LoadComplete() { base.LoadComplete(); - header.Post.BindValueChanged(onPostChanged, true); + article.BindValueChanged(onArticleChanged, true); } public void ShowFrontPage() { - header.SetFrontPage(); + article.Value = null; Show(); } public void ShowArticle(string slug) { - header.SetArticle(slug); + article.Value = slug; Show(); } private CancellationTokenSource cancellationToken; - private void onPostChanged(ValueChangedEvent post) + private void onArticleChanged(ValueChangedEvent e) { cancellationToken?.Cancel(); loading.Show(); - if (post.NewValue == NewsHeader.FRONT_PAGE_STRING) + if (e.NewValue == null) { + header.SetFrontPage(); LoadDisplay(new FrontPageDisplay()); return; } + header.SetArticle(e.NewValue); LoadDisplay(Empty()); } From bdf680aecbc2c81297fbd2fc5f9fcda993829799 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Sun, 12 Jul 2020 14:53:40 +0200 Subject: [PATCH 2142/6909] inline single-use constants --- osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index f083d731c8..3777193f49 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -30,9 +30,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override bool Ranked => true; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; - private const float lanecover_size_filled = 0.5f; - private const float lanecover_size_gradient = 0.25f; - public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; @@ -42,8 +39,8 @@ namespace osu.Game.Rulesets.Mania.Mods column.HitObjectArea.ChildrenOfType().First().Add(new LaneCover(false) { RelativeSizeAxes = Axes.Both, - SizeFilled = lanecover_size_filled, - SizeGradient = lanecover_size_gradient + SizeFilled = 0.5f, + SizeGradient = 0.25f }); } } From a72bb932661faa2442ceb9160d61b0d21e803790 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 12 Jul 2020 15:57:18 +0300 Subject: [PATCH 2143/6909] Add test scene for NewsHeader --- .../Visual/Online/TestSceneNewsHeader.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs new file mode 100644 index 0000000000..65e86e925d --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Overlays.News; +using osu.Framework.Graphics; +using osu.Game.Overlays; +using osu.Framework.Allocation; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneNewsHeader : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + private TestHeader header; + + [SetUp] + public void Setup() + { + Child = header = new TestHeader + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }; + } + + [Test] + public void TestControl() + { + AddAssert("Front page selected", () => header.Current.Value == "frontpage"); + AddAssert("1 tab total", () => header.TabCount == 1); + + AddStep("Set article 1", () => header.SetArticle("1")); + AddAssert("Article 1 selected", () => header.Current.Value == "1"); + AddAssert("2 tabs total", () => header.TabCount == 2); + + AddStep("Set article 2", () => header.SetArticle("2")); + AddAssert("Article 2 selected", () => header.Current.Value == "2"); + AddAssert("2 tabs total", () => header.TabCount == 2); + + AddStep("Set front page", () => header.SetFrontPage()); + AddAssert("Front page selected", () => header.Current.Value == "frontpage"); + AddAssert("1 tab total", () => header.TabCount == 1); + } + + private class TestHeader : NewsHeader + { + public int TabCount => TabControl.Items.Count; + } + } +} From 444701fdd0c0903346177900acef014755da297f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 12 Jul 2020 16:13:48 +0300 Subject: [PATCH 2144/6909] Use dummy api for tests --- .../Visual/Online/TestSceneNewsHeader.cs | 4 +- .../Visual/Online/TestSceneNewsOverlay.cs | 64 +++++++++++++++---- 2 files changed, 53 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs index 65e86e925d..78288bf6e4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsHeader.cs @@ -17,14 +17,14 @@ namespace osu.Game.Tests.Visual.Online private TestHeader header; [SetUp] - public void Setup() + public void Setup() => Schedule(() => { Child = header = new TestHeader { Anchor = Anchor.Centre, Origin = Anchor.Centre }; - } + }); [Test] public void TestControl() diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs index e35ef4916b..37d51c16d2 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsOverlay.cs @@ -1,27 +1,65 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using NUnit.Framework; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; namespace osu.Game.Tests.Visual.Online { public class TestSceneNewsOverlay : OsuTestScene { - protected override bool UseOnlineAPI => true; + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; - protected override void LoadComplete() + private NewsOverlay news; + + [SetUp] + public void SetUp() => Schedule(() => Child = news = new NewsOverlay()); + + [Test] + public void TestRequest() { - base.LoadComplete(); - - NewsOverlay news; - Add(news = new NewsOverlay()); - - AddStep("Show", news.Show); - AddStep("Hide", news.Hide); - - AddStep("Show front page", () => news.ShowFrontPage()); - AddStep("Custom article", () => news.ShowArticle("Test Article 101")); - AddStep("Custom article", () => news.ShowArticle("Test Article 102")); + setUpNewsResponse(responseExample); + AddStep("Show", () => news.Show()); + AddStep("Show article", () => news.ShowArticle("article")); } + + private void setUpNewsResponse(GetNewsResponse r) + => AddStep("set up response", () => + { + dummyAPI.HandleRequest = request => + { + if (!(request is GetNewsRequest getNewsRequest)) + return; + + getNewsRequest.TriggerSuccess(r); + }; + }); + + private GetNewsResponse responseExample => new GetNewsResponse + { + NewsPosts = new[] + { + 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.", + Author = "someone, someone1, someone2, someone3, someone4", + FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png", + PublishedAt = DateTimeOffset.Now + }, + new APINewsPost + { + 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 + } + } + }; } } From ab11a112b7078c538e5b23d57603294a02c04749 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Jul 2020 22:21:16 +0900 Subject: [PATCH 2145/6909] Fix correct filter criteria not being applied to beatmap carousel if beatmaps take too long to load --- osu.Game/Screens/Select/BeatmapCarousel.cs | 29 ++++++++++++++-------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 6f913a3177..5a4160960a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -95,7 +95,6 @@ namespace osu.Game.Screens.Select CarouselRoot newRoot = new CarouselRoot(this); beatmapSets.Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild); - newRoot.Filter(activeCriteria); // preload drawables as the ctor overhead is quite high currently. _ = newRoot.Drawables; @@ -108,6 +107,9 @@ namespace osu.Game.Screens.Select itemsCache.Invalidate(); scrollPositionCache.Invalidate(); + // the filter criteria may have changed since the above filter operation. + FlushPendingFilterOperations(); + // Run on late scheduler want to ensure this runs after all pending UpdateBeatmapSet / RemoveBeatmapSet operations are run. SchedulerAfterChildren.Add(() => { @@ -321,6 +323,9 @@ namespace osu.Game.Screens.Select /// True if a selection could be made, else False. public bool SelectNextRandom() { + if (!AllowSelection) + return false; + var visibleSets = beatmapSets.Where(s => !s.Filtered.Value).ToList(); if (!visibleSets.Any()) return false; @@ -427,7 +432,19 @@ namespace osu.Game.Screens.Select private void applyActiveCriteria(bool debounce, bool alwaysResetScrollPosition = true) { - if (root.Children.Any() != true) return; + PendingFilter?.Cancel(); + PendingFilter = null; + + if (debounce) + PendingFilter = Scheduler.AddDelayed(perform, 250); + else + { + // if initial load is not yet finished, this will be run inline in loadBeatmapSets to ensure correct order of operation. + if (!BeatmapSetsLoaded) + PendingFilter = Schedule(perform); + else + perform(); + } void perform() { @@ -439,14 +456,6 @@ namespace osu.Game.Screens.Select if (alwaysResetScrollPosition || !scroll.UserScrolling) ScrollToSelected(); } - - PendingFilter?.Cancel(); - PendingFilter = null; - - if (debounce) - PendingFilter = Scheduler.AddDelayed(perform, 250); - else - perform(); } private float? scrollTarget; From 08696b9bca23ce188b3e6bb90fc1d11bc36cbdef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Jul 2020 23:03:03 +0900 Subject: [PATCH 2146/6909] Allow pausing gameplay via middle mouse button --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 1 + 2 files changed, 5 insertions(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 618798a6d8..567c81c018 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -56,6 +56,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit), new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), + new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), }; public IEnumerable AudioControlKeyBindings => new[] @@ -157,5 +158,8 @@ namespace osu.Game.Input.Bindings [Description("Home")] Home, + + [Description("Pause")] + PauseGameplay, } } diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 684834123b..81ad29107f 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -251,6 +251,7 @@ namespace osu.Game.Screens.Play.HUD switch (action) { case GlobalAction.Back: + case GlobalAction.PauseGameplay: if (!pendingAnimation) BeginConfirm(); return true; From c1aafe83fa042ac01e28631bf9bda2695a71a100 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 12 Jul 2020 23:05:47 +0900 Subject: [PATCH 2147/6909] Add note about future behaviour --- osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 81ad29107f..74064c507f 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -251,7 +251,7 @@ namespace osu.Game.Screens.Play.HUD switch (action) { case GlobalAction.Back: - case GlobalAction.PauseGameplay: + case GlobalAction.PauseGameplay: // in the future this behaviour will differ for replays etc. if (!pendingAnimation) BeginConfirm(); return true; From 9c039848bc0fe48bae87214d6871c43c4b9a0a9c Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 12 Jul 2020 12:04:53 -0700 Subject: [PATCH 2148/6909] Simplify and add null check --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 5 ++--- osu.Game/Overlays/Mods/ModSection.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 +++--- osu.Game/Screens/Select/SongSelect.cs | 2 +- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 7203683711..40323c325e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -388,9 +388,8 @@ namespace osu.Game.Online.Leaderboards private void getMods() { - songSelect.ModSelect.DeselectAll(true); - - songSelect.Mods.Value = score.Mods; + if (songSelect != null) + songSelect.Mods.Value = score.Mods; } } } diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 45fae90a1e..7235a18a23 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.Mods return base.OnKeyDown(e); } - public void DeselectAll(bool immediate = false) => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null), immediate); + public void DeselectAll() => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); /// /// Deselect one or more mods in this section. diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index d83dd77401..3d0ad1a594 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -231,7 +231,7 @@ namespace osu.Game.Overlays.Mods { Width = 180, Text = "Deselect All", - Action = () => DeselectAll(), + Action = DeselectAll, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, @@ -328,10 +328,10 @@ namespace osu.Game.Overlays.Mods sampleOff = audio.Samples.Get(@"UI/check-off"); } - public void DeselectAll(bool immediate = false) + public void DeselectAll() { foreach (var section in ModSectionsContainer.Children) - section.DeselectAll(immediate); + section.DeselectAll(); refreshSelectedMods(); } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 23fa3cd724..e3705b15fa 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select [Resolved] private BeatmapManager beatmaps { get; set; } - public ModSelectOverlay ModSelect { get; private set; } + protected ModSelectOverlay ModSelect { get; private set; } protected SampleChannel SampleConfirm { get; private set; } From 6eec2f9429bd3548d20a1c3bb1b0d21119b77db0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 13 Jul 2020 01:22:05 +0300 Subject: [PATCH 2149/6909] Implement RankingsSortTabControl component --- .../TestSceneRankingsSortTabControl.cs | 25 +++++++++++++++++++ osu.Game/Overlays/Comments/CommentsHeader.cs | 2 +- osu.Game/Overlays/OverlaySortTabControl.cs | 16 +++++++++--- .../Rankings/RankingsSortTabControl.cs | 19 ++++++++++++++ 4 files changed, 57 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs create mode 100644 osu.Game/Overlays/Rankings/RankingsSortTabControl.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.cs new file mode 100644 index 0000000000..24bc0dbc97 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneRankingsSortTabControl.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.Graphics; +using osu.Game.Overlays; +using osu.Game.Overlays.Rankings; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneRankingsSortTabControl : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + + public TestSceneRankingsSortTabControl() + { + Child = new RankingsSortTabControl + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }; + } + } +} diff --git a/osu.Game/Overlays/Comments/CommentsHeader.cs b/osu.Game/Overlays/Comments/CommentsHeader.cs index 83f44ccd80..0dd68bbd41 100644 --- a/osu.Game/Overlays/Comments/CommentsHeader.cs +++ b/osu.Game/Overlays/Comments/CommentsHeader.cs @@ -86,7 +86,7 @@ namespace osu.Game.Overlays.Comments { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 12), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Text = @"Show deleted" } }, diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs index 395f3aec4c..b2212336ef 100644 --- a/osu.Game/Overlays/OverlaySortTabControl.cs +++ b/osu.Game/Overlays/OverlaySortTabControl.cs @@ -30,6 +30,14 @@ namespace osu.Game.Overlays set => current.Current = value; } + public string Title + { + get => text.Text; + set => text.Text = value; + } + + private readonly OsuSpriteText text; + public OverlaySortTabControl() { AutoSizeAxes = Axes.Both; @@ -40,11 +48,11 @@ namespace osu.Game.Overlays Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OsuSpriteText + text = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 12), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Text = @"Sort by" }, CreateControl().With(c => @@ -133,7 +141,7 @@ namespace osu.Game.Overlays { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 12), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Text = (value as Enum)?.GetDescription() ?? value.ToString() } } @@ -163,7 +171,7 @@ namespace osu.Game.Overlays ContentColour = Active.Value && !IsHovered ? colourProvider.Light1 : Color4.White; - text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.Medium); + text.Font = text.Font.With(weight: Active.Value ? FontWeight.Bold : FontWeight.SemiBold); } } } diff --git a/osu.Game/Overlays/Rankings/RankingsSortTabControl.cs b/osu.Game/Overlays/Rankings/RankingsSortTabControl.cs new file mode 100644 index 0000000000..c0bbf46e30 --- /dev/null +++ b/osu.Game/Overlays/Rankings/RankingsSortTabControl.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. + +namespace osu.Game.Overlays.Rankings +{ + public class RankingsSortTabControl : OverlaySortTabControl + { + public RankingsSortTabControl() + { + Title = "Show"; + } + } + + public enum RankingsSortCriteria + { + All, + Friends + } +} From f442df75a9bf16d463a898bc67f15acb539960dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Jul 2020 09:00:10 +0900 Subject: [PATCH 2150/6909] Add missing released conditional --- osu.Game/Screens/Play/HUD/HoldForMenuButton.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 74064c507f..387c0e587b 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -265,6 +265,7 @@ namespace osu.Game.Screens.Play.HUD switch (action) { case GlobalAction.Back: + case GlobalAction.PauseGameplay: AbortConfirm(); break; } From 0718e9e4b64fceae400129de0a3a6a6ae5627f37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Jul 2020 13:08:41 +0900 Subject: [PATCH 2151/6909] Update outdated comment --- 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 5a4160960a..5f6f859d66 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Select itemsCache.Invalidate(); scrollPositionCache.Invalidate(); - // the filter criteria may have changed since the above filter operation. + // apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false). FlushPendingFilterOperations(); // Run on late scheduler want to ensure this runs after all pending UpdateBeatmapSet / RemoveBeatmapSet operations are run. From ac7252e152ce53b7d0f77654c4159193e4872d87 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 12 Jul 2020 22:04:00 -0700 Subject: [PATCH 2152/6909] Fix context menu not masking outside of leaderboard area --- osu.Game/Online/Leaderboards/Leaderboard.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index d51bf963fc..800029ceb9 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -173,6 +173,7 @@ namespace osu.Game.Online.Leaderboards new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, + Masking = true, Child = new GridContainer { RelativeSizeAxes = Axes.Both, From db6a9c97172ec8418633f62fe2e8ebcc901b6605 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 12 Jul 2020 22:06:17 -0700 Subject: [PATCH 2153/6909] Move null check to menu item addition --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 40323c325e..b60d71cfe7 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -376,8 +376,8 @@ namespace osu.Game.Online.Leaderboards { List items = new List(); - if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered)) - items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, getMods)); + if (score.Mods.Length > 0 && modsContainer.Any(s => s.IsHovered) && songSelect != null) + items.Add(new OsuMenuItem("Use these mods", MenuItemType.Highlighted, () => songSelect.Mods.Value = score.Mods)); if (score.ID != 0) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay?.Push(new LocalScoreDeleteDialog(score)))); @@ -385,11 +385,5 @@ namespace osu.Game.Online.Leaderboards return items.ToArray(); } } - - private void getMods() - { - if (songSelect != null) - songSelect.Mods.Value = score.Mods; - } } } From 79ea6dbd5cad12e26310d4c9f1fce87b767daa71 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 12 Jul 2020 22:24:11 -0700 Subject: [PATCH 2154/6909] Only allow link clicking and tooltips of multi room drawables when selected --- osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index 3f5a2eb1d3..8dd1b239e8 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -219,6 +219,8 @@ namespace osu.Game.Screens.Multi.Lounge.Components Alpha = 0; } + protected override bool ShouldBeConsideredForInput(Drawable child) => state == SelectionState.Selected; + private class RoomName : OsuSpriteText { [Resolved(typeof(Room), nameof(Online.Multiplayer.Room.Name))] From 352f59942e5aff3523582893ebad5bd78349f5e9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Jul 2020 16:50:54 +0900 Subject: [PATCH 2155/6909] Fix incorrect time delta in taiko strain --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs index c6fe273b50..99975d9174 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double addition = 1; // We get an extra addition if we are not a slider or spinner - if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) + if (current.LastObject is Hit && current.BaseObject is Hit && (current.BaseObject.StartTime - current.LastObject.StartTime) < 1000) { if (hasColourChange(current)) addition += 0.75; From 1116703e92c1a6ac82d4b63298780d5ea584ace8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Jul 2020 16:52:05 +0900 Subject: [PATCH 2156/6909] Fix potential out-of-order objects after conversion --- osu.Game/Beatmaps/BeatmapConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 99e0bf4e33..11fee030f8 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; - beatmap.HitObjects = convertHitObjects(original.HitObjects, original); + beatmap.HitObjects = convertHitObjects(original.HitObjects, original).OrderBy(s => s.StartTime).ToList(); beatmap.Breaks = original.Breaks; return beatmap; From cd3500510e71ef1d7de641e648d1cb7b7e64eff5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Jul 2020 17:05:29 +0900 Subject: [PATCH 2157/6909] Fix carousel tests relying on initial selection being null --- .../SongSelect/TestSceneBeatmapCarousel.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 073d75692e..70eafcb2a7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -80,9 +80,9 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestRecommendedSelection() { - loadBeatmaps(); + loadBeatmaps(carouselAdjust: carousel => carousel.GetRecommendedBeatmap = beatmaps => beatmaps.LastOrDefault()); - AddStep("set recommendation function", () => carousel.GetRecommendedBeatmap = beatmaps => beatmaps.LastOrDefault()); + AddStep("select last", () => carousel.SelectBeatmap(carousel.BeatmapSets.Last().Beatmaps.Last())); // check recommended was selected advanceSelection(direction: 1, diff: false); @@ -114,7 +114,7 @@ namespace osu.Game.Tests.Visual.SongSelect { loadBeatmaps(); - advanceSelection(direction: 1, diff: false); + AddStep("select last", () => carousel.SelectBeatmap(carousel.BeatmapSets.First().Beatmaps.First())); waitForSelection(1, 1); advanceSelection(direction: 1, diff: true); @@ -707,9 +707,9 @@ namespace osu.Game.Tests.Visual.SongSelect checkVisibleItemCount(true, 15); } - private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null) + private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null) { - createCarousel(); + createCarousel(carouselAdjust); if (beatmapSets == null) { @@ -730,17 +730,21 @@ namespace osu.Game.Tests.Visual.SongSelect AddUntilStep("Wait for load", () => changed); } - private void createCarousel(Container target = null) + private void createCarousel(Action carouselAdjust = null, Container target = null) { AddStep("Create carousel", () => { selectedSets.Clear(); eagerSelectedIDs.Clear(); - (target ?? this).Child = carousel = new TestBeatmapCarousel + carousel = new TestBeatmapCarousel { RelativeSizeAxes = Axes.Both, }; + + carouselAdjust?.Invoke(carousel); + + (target ?? this).Child = carousel; }); } From 0ea13dea55caca5ba2d52d330c889880c924c150 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Jul 2020 17:06:00 +0900 Subject: [PATCH 2158/6909] Introduce legacy timing point fp errors --- .../Beatmaps/TaikoBeatmapConverter.cs | 79 +++++++++++-------- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 4 +- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 13 ++- 3 files changed, 62 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 78550ed270..2a1aa5d1df 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -10,6 +10,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; namespace osu.Game.Rulesets.Taiko.Beatmaps { @@ -82,37 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { case IHasDistance distanceData: { - // Number of spans of the object - one for the initial length and for each repeat - int spans = (obj as IHasRepeats)?.SpanCount() ?? 1; - - TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); - DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime); - - double speedAdjustment = difficultyPoint.SpeedMultiplier; - 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; - - // 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; - // The duration of the taiko hit object - double taikoDuration = distance / taikoVelocity; - - // The velocity of the osu! hit object - calculated as the velocity of a slider - double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / speedAdjustedBeatLength; - // The duration of the osu! hit object - double osuDuration = distance / osuVelocity; - - // osu-stable always uses the speed-adjusted beatlength to determine the velocities, but - // only uses it for tick rate if beatmap version < 8 - if (beatmap.BeatmapInfo.BeatmapVersion >= 8) - speedAdjustedBeatLength *= speedAdjustment; - - // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat - double tickSpacing = Math.Min(speedAdjustedBeatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans); - - if (!isForCurrentRuleset && tickSpacing > 0 && osuDuration < 2 * speedAdjustedBeatLength) + if (shouldConvertSliderToHits(obj, beatmap, distanceData, out var taikoDuration, out var tickSpacing)) { List> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples }); @@ -184,6 +155,52 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps } } + private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out double taikoDuration, out double tickSpacing) + { + // DO NOT CHANGE OR REFACTOR ANYTHING IN HERE WITHOUT TESTING AGAINST _ALL_ BEATMAPS. + // Some of these calculations look redundant, but they are not - extremely small floating point errors are introduced to maintain 1:1 compatibility with stable. + // Rounding cannot be used as an alternative since the error deltas have been observed to be between 1e-2 and 1e-6. + + // The true distance, accounting for any repeats. This ends up being the drum roll distance later + int spans = (obj as IHasRepeats)?.SpanCount() ?? 1; + double distance = distanceData.Distance * spans * LEGACY_VELOCITY_MULTIPLIER; + + TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(obj.StartTime); + DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(obj.StartTime); + + double beatLength; +#pragma warning disable 618 + if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint) +#pragma warning restore 618 + beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; + else + beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier; + + double sliderScoringPointDistance = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate; + + // The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll. + double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate; + taikoDuration = distance / taikoVelocity * beatLength; + + if (isForCurrentRuleset) + { + tickSpacing = 0; + return false; + } + + double osuVelocity = taikoVelocity * (1000f / beatLength); + + // osu-stable always uses the speed-adjusted beatlength to determine the osu! velocity, but only uses it for conversion if beatmap version < 8 + if (beatmap.BeatmapInfo.BeatmapVersion >= 8) + beatLength = timingPoint.BeatLength; + + // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat + tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans); + + return tickSpacing > 0 + && distance / osuVelocity * 1000 < 2 * beatLength; + } + protected override Beatmap CreateBeatmap() => new TaikoBeatmap(); } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index be5cd78dc8..b30ec0ca2c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -369,7 +369,9 @@ namespace osu.Game.Beatmaps.Formats addControlPoint(time, controlPoint, true); } - addControlPoint(time, new LegacyDifficultyControlPoint +#pragma warning disable 618 + addControlPoint(time, new LegacyDifficultyControlPoint(beatLength) +#pragma warning restore 618 { SpeedMultiplier = speedMultiplier, }, timingChange); diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index a0e83554a3..44ef9bcacc 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -159,11 +159,20 @@ namespace osu.Game.Beatmaps.Formats Mania, } - internal class LegacyDifficultyControlPoint : DifficultyControlPoint + [Obsolete("Do not use unless you're a legacy ruleset and 100% sure.")] + public class LegacyDifficultyControlPoint : DifficultyControlPoint { - public LegacyDifficultyControlPoint() + /// + /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. + /// DO NOT USE THIS UNLESS 100% SURE. + /// + public readonly float BpmMultiplier; + + public LegacyDifficultyControlPoint(double beatLength) { SpeedMultiplierBindable.Precision = double.Epsilon; + + BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100f : 1; } } From 7b7b92aa10ab0f65f21e338d34d015ed4fe45f6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 13 Jul 2020 17:28:16 +0900 Subject: [PATCH 2159/6909] Fix potential crash when trying to ensure music is playing --- osu.Game/Overlays/MusicController.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 09f2a66b47..546f7a1ec4 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -149,7 +149,7 @@ namespace osu.Game.Overlays if (beatmap.Disabled) return; - next(); + NextTrack(); } else if (!IsPlaying) { @@ -217,6 +217,9 @@ namespace osu.Game.Overlays /// The that indicate the decided action. private PreviousTrackResult prev() { + if (beatmap.Disabled) + return PreviousTrackResult.None; + var currentTrackPosition = current?.Track.CurrentTime; if (currentTrackPosition >= restart_cutoff_point) @@ -248,6 +251,9 @@ namespace osu.Game.Overlays private bool next() { + if (beatmap.Disabled) + return false; + queuedDirection = TrackChangeDirection.Next; var playable = BeatmapSets.SkipWhile(i => i.ID != current.BeatmapSetInfo.ID).ElementAtOrDefault(1) ?? BeatmapSets.FirstOrDefault(); From 4b3cffb246c3df3f22d14f14e2d336cd6575f890 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Mon, 13 Jul 2020 11:55:13 +0200 Subject: [PATCH 2160/6909] expose hitObjectContainer in HitObjectArea --- osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs | 2 +- osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index 3777193f49..ce00f0ccda 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { - column.HitObjectArea.ChildrenOfType().First().Add(new LaneCover(false) + ((BufferedContainer)column.HitObjectArea.HitObjectContainer.Parent).Add(new LaneCover(false) { RelativeSizeAxes = Axes.Both, SizeFilled = 0.5f, diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs index 5eccb891cc..d21a156437 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs @@ -15,13 +15,14 @@ namespace osu.Game.Rulesets.Mania.UI.Components public class HitObjectArea : SkinReloadableDrawable { protected readonly IBindable Direction = new Bindable(); + public readonly HitObjectContainer HitObjectContainer; public HitObjectArea(HitObjectContainer hitObjectContainer) { InternalChild = new BufferedContainer { RelativeSizeAxes = Axes.Both, - Child = hitObjectContainer + Child = HitObjectContainer = hitObjectContainer }; } From 69548447a7e3bf9ac63c2e1e05ceff17123ed6ed Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 13 Jul 2020 20:03:07 +0900 Subject: [PATCH 2161/6909] Adjust step name --- 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 70eafcb2a7..a3ea4619cc 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -114,7 +114,7 @@ namespace osu.Game.Tests.Visual.SongSelect { loadBeatmaps(); - AddStep("select last", () => carousel.SelectBeatmap(carousel.BeatmapSets.First().Beatmaps.First())); + AddStep("select first", () => carousel.SelectBeatmap(carousel.BeatmapSets.First().Beatmaps.First())); waitForSelection(1, 1); advanceSelection(direction: 1, diff: true); From 31782172165372dec692b8d875efd0ac58e06b24 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Mon, 13 Jul 2020 13:14:47 +0200 Subject: [PATCH 2162/6909] remove unnecessary import --- osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index ce00f0ccda..06a6655da6 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Testing; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; From ca39f2aa24c7340dea8667e556d024c61a703628 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Mon, 13 Jul 2020 13:43:32 +0200 Subject: [PATCH 2163/6909] only insert BufferedContainer when using FI --- osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs | 18 +++++++++++++++--- .../UI/Components/HitObjectArea.cs | 2 +- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index 06a6655da6..ed990f8d5f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -35,11 +35,23 @@ namespace osu.Game.Rulesets.Mania.Mods foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) { - ((BufferedContainer)column.HitObjectArea.HitObjectContainer.Parent).Add(new LaneCover(false) + HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; + Container hocParent = (Container)hoc.Parent; + + hocParent.Remove(hoc); + hocParent.Add(new BufferedContainer { RelativeSizeAxes = Axes.Both, - SizeFilled = 0.5f, - SizeGradient = 0.25f + Children = new Drawable[] + { + hoc, + new LaneCover(false) + { + RelativeSizeAxes = Axes.Both, + SizeFilled = 0.5f, + SizeGradient = 0.25f + } + } }); } } diff --git a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs index d21a156437..8f7880dafa 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/HitObjectArea.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.UI.Components public HitObjectArea(HitObjectContainer hitObjectContainer) { - InternalChild = new BufferedContainer + InternalChild = new Container { RelativeSizeAxes = Axes.Both, Child = HitObjectContainer = hitObjectContainer From 8a3cadc111cfadb6744a5e22b4f5d2bd4abe3550 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Jul 2020 20:12:50 +0900 Subject: [PATCH 2164/6909] Fix judgement animations not resetting on use --- .../Objects/Drawables/DrawableOsuJudgement.cs | 2 ++ osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 10 +++++++--- osu.Game/Skinning/SkinnableDrawable.cs | 6 ++++++ 3 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index cfe969d1cc..1493ddfcf3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (lighting != null) { + lighting.ResetAnimation(); + if (JudgedObject != null) { lightingColour = JudgedObject.AccentColour.GetBoundCopy(); diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 4e7f0018ef..052aaa3c65 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -31,8 +31,10 @@ namespace osu.Game.Rulesets.Judgements public JudgementResult Result { get; private set; } public DrawableHitObject JudgedObject { get; private set; } - protected Container JudgementBody; - protected SpriteText JudgementText; + protected Container JudgementBody { get; private set; } + protected SpriteText JudgementText { get; private set; } + + private SkinnableDrawable bodyDrawable; /// /// Duration of initial fade in. @@ -89,6 +91,8 @@ namespace osu.Game.Rulesets.Judgements prepareDrawables(); + bodyDrawable.ResetAnimation(); + this.FadeInFromZero(FadeInDuration, Easing.OutQuint); JudgementBody.ScaleTo(1); JudgementBody.RotateTo(0); @@ -131,7 +135,7 @@ namespace osu.Game.Rulesets.Judgements Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Child = new SkinnableDrawable(new GameplaySkinComponent(type), _ => JudgementText = new OsuSpriteText + Child = bodyDrawable = new SkinnableDrawable(new GameplaySkinComponent(type), _ => JudgementText = new OsuSpriteText { Text = type.GetDescription().ToUpperInvariant(), Font = OsuFont.Numeric.With(size: 20), diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index 0f0d3da5aa..d9a5036649 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Caching; using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; using osuTK; namespace osu.Game.Skinning @@ -50,6 +51,11 @@ namespace osu.Game.Skinning RelativeSizeAxes = Axes.Both; } + /// + /// Seeks to the 0-th frame if the content of this is an . + /// + public void ResetAnimation() => (Drawable as IFramedAnimation)?.GotoFrame(0); + private readonly Func createDefault; private readonly Cached scaling = new Cached(); From 53520ec7c45b65961b9653c3c7f1b0927ca23889 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 13 Jul 2020 20:55:55 +0900 Subject: [PATCH 2165/6909] Add test --- .../TestSceneDrawableJudgement.cs | 45 ++++++++++++++++--- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index c81edf4e07..f08f994b07 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -2,9 +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.Extensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -16,14 +19,46 @@ namespace osu.Game.Rulesets.Osu.Tests { public TestSceneDrawableJudgement() { + var pools = new List>(); + foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) { - AddStep("Show " + result.GetDescription(), () => SetContents(() => - new DrawableOsuJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null) + AddStep("Show " + result.GetDescription(), () => + { + int poolIndex = 0; + + SetContents(() => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - })); + DrawablePool pool; + + if (poolIndex >= pools.Count) + pools.Add(pool = new DrawablePool(1)); + else + { + pool = pools[poolIndex]; + + // We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent. + ((Container)pool.Parent).Clear(false); + } + + var container = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + pool, + pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j => + { + j.Anchor = Anchor.Centre; + j.Origin = Anchor.Centre; + }) + } + }; + + poolIndex++; + return container; + }); + }); } } } From 8087a75c3506e1906af24c3fc9f0639048e8b9d8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 13 Jul 2020 17:22:39 +0000 Subject: [PATCH 2166/6909] Bump NUnit3TestAdapter from 3.15.1 to 3.17.0 Bumps [NUnit3TestAdapter](https://github.com/nunit/nunit3-vs-adapter) from 3.15.1 to 3.17.0. - [Release notes](https://github.com/nunit/nunit3-vs-adapter/releases) - [Commits](https://github.com/nunit/nunit3-vs-adapter/compare/V3.15.1...V3.17) Signed-off-by: dependabot-preview[bot] --- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- .../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 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 41e726e05c..ff26f4afaa 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -9,7 +9,7 @@ - + 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 cbd3dc5518..7c0b73e8c3 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 @@ -4,7 +4,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 77c871718b..972cbec4a2 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 @@ -4,7 +4,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 2fcfa1deb7..d6a68abaf2 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 @@ -4,7 +4,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 28b8476a22..ada7ac5d74 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 @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 5ee887cb64..4b0506d818 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -5,7 +5,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index aa37326a49..f256b8e4e9 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe From 38e9b882b813f68fc350ae4538ab1e6c5abf806f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jul 2020 07:04:30 +0900 Subject: [PATCH 2167/6909] 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 0881861bdc..1d1583c55a 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 cba2d62bf5..4295e02d24 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 45e0da36c1..3627cc032e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From b59e4f8a7ef40ece6455b20a612ff942e18917c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jul 2020 08:15:14 +0900 Subject: [PATCH 2168/6909] Change difficulty adjust mod to match stable range of 0-10 --- osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs | 4 ++-- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index 8228161008..ff995e38ce 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Mods public BindableNumber CircleSize { get; } = new BindableFloat { Precision = 0.1f, - MinValue = 1, + MinValue = 0, MaxValue = 10, Default = 5, Value = 5, @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Mods public BindableNumber ApproachRate { get; } = new BindableFloat { Precision = 0.1f, - MinValue = 1, + MinValue = 0, MaxValue = 10, Default = 5, Value = 5, diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index c3a8efdd66..165644edbe 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mods public BindableNumber DrainRate { get; } = new BindableFloat { Precision = 0.1f, - MinValue = 1, + MinValue = 0, MaxValue = 10, Default = 5, Value = 5, @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mods public BindableNumber OverallDifficulty { get; } = new BindableFloat { Precision = 0.1f, - MinValue = 1, + MinValue = 0, MaxValue = 10, Default = 5, Value = 5, From 56349e65f3929032561d3059622a201f87afc604 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jul 2020 03:01:14 +0300 Subject: [PATCH 2169/6909] Rename arrow direction method --- .../Visual/UserInterface/TestSceneCommentRepliesButton.cs | 8 ++++---- .../Overlays/Comments/Buttons/CommentRepliesButton.cs | 2 +- osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs index 7f5806705e..c2dc804385 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs @@ -41,11 +41,11 @@ namespace osu.Game.Tests.Visual.UserInterface } [Test] - public void TestArrowRotation() + public void TestArrowDirection() { - AddStep("Toggle icon up", () => button.ToggleIcon(true)); + AddStep("Set upwards", () => button.SetIconDirection(true)); AddAssert("Icon facing upwards", () => button.Icon.Scale.Y == -1); - AddStep("Toggle icon down", () => button.ToggleIcon(false)); + AddStep("Set downwards", () => button.SetIconDirection(false)); AddAssert("Icon facing downwards", () => button.Icon.Scale.Y == 1); } @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.UserInterface Text = "sample text"; } - public new void ToggleIcon(bool upwards) => base.ToggleIcon(upwards); + public new void SetIconDirection(bool upwards) => base.SetIconDirection(upwards); } } } diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index 65648f6751..abe80722e2 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Comments.Buttons icon.Colour = colourProvider.Foreground1; } - protected void ToggleIcon(bool upwards) => icon.ScaleTo(new Vector2(1, upwards ? -1 : 1)); + protected void SetIconDirection(bool upwards) => icon.ScaleTo(new Vector2(1, upwards ? -1 : 1)); protected override bool OnHover(HoverEvent e) { diff --git a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs index 118cac5b4c..e2023c2f58 100644 --- a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Comments.Buttons protected override void LoadComplete() { base.LoadComplete(); - Expanded.BindValueChanged(expanded => ToggleIcon(expanded.NewValue), true); + Expanded.BindValueChanged(expanded => SetIconDirection(expanded.NewValue), true); } protected override bool OnClick(ClickEvent e) From 7c71cc6b6174a6a62a0f78cd9029bb12943ff82b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jul 2020 03:06:51 +0300 Subject: [PATCH 2170/6909] Remove unneeded class from DrawableComment --- osu.Game/Overlays/Comments/DrawableComment.cs | 41 +++++-------------- 1 file changed, 11 insertions(+), 30 deletions(-) diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 813540b28d..731ebe7104 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Comments private ShowRepliesButton showRepliesButton; private ChevronButton chevronButton; private DeletedCommentsCounter deletedCommentsCounter; - private Loading loading; + private LoadingSpinner loading; public DrawableComment(Comment comment) { @@ -209,10 +209,16 @@ namespace osu.Game.Overlays.Comments } } }, - loading = new Loading + loading = new LoadingSpinner { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Size = new Vector2(15), + Margin = new MarginPadding + { + Vertical = 5, + Left = 80 + } }, childCommentsVisibilityContainer = new FillFlowContainer { @@ -368,6 +374,7 @@ namespace osu.Game.Overlays.Comments showMoreButton.IsLoading = false; loading.Hide(); + loading.FinishTransforms(); } private class ChevronButton : ShowChildrenButton @@ -438,31 +445,5 @@ namespace osu.Game.Overlays.Comments return parentComment.HasMessage ? parentComment.Message : parentComment.IsDeleted ? @"deleted" : string.Empty; } } - - private class Loading : Container - { - private readonly LoadingSpinner loading; - - public Loading() - { - Child = loading = new LoadingSpinner - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Size = new Vector2(15), - Margin = new MarginPadding - { - Vertical = 5, - Left = 80 - } - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - loading.Show(); - } - } } } From 8a77a3621e71961bbcccb0a3b38eb2e1d9e96561 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jul 2020 11:03:55 +0900 Subject: [PATCH 2171/6909] 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 1d1583c55a..8510632d45 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 4295e02d24..05d6f27d40 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 3627cc032e..af779b32fd 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From e35e9df4e1a6acdeaae0a2022363d8240f9cb390 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jul 2020 12:02:03 +0900 Subject: [PATCH 2172/6909] Fix local online cache database not being used when offline / logged out --- osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index 3106d1143e..4de4e21b15 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -48,9 +48,6 @@ namespace osu.Game.Beatmaps 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()); } From a25f4880d6074b80cda388d5ba7baa7711c430e1 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Tue, 14 Jul 2020 10:35:01 +0700 Subject: [PATCH 2173/6909] disable hit explotion when hit lighting off --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 82cbbefcca..793db361da 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Skinning; @@ -99,6 +100,7 @@ namespace osu.Game.Rulesets.Catch.UI private double hyperDashModifier = 1; private int hyperDashDirection; private float hyperDashTargetPosition; + private bool hitLighting; public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) { @@ -114,8 +116,10 @@ namespace osu.Game.Rulesets.Catch.UI } [BackgroundDependencyLoader] - private void load() + private void load(OsuConfigManager config) { + hitLighting = config.Get(OsuSetting.HitLighting); + InternalChildren = new Drawable[] { caughtFruit = new Container @@ -189,11 +193,14 @@ namespace osu.Game.Rulesets.Catch.UI caughtFruit.Add(fruit); - AddInternal(new HitExplosion(fruit) + if (hitLighting) { - X = fruit.X, - Scale = new Vector2(fruit.HitObject.Scale) - }); + AddInternal(new HitExplosion(fruit) + { + X = fruit.X, + Scale = new Vector2(fruit.HitObject.Scale) + }); + } } /// From 3e2d184a911ff88e445618064451e186e5e7c592 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Tue, 14 Jul 2020 10:52:34 +0700 Subject: [PATCH 2174/6909] change hitlighting bool to bindable --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 793db361da..a0a5f7279c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; @@ -100,7 +101,7 @@ namespace osu.Game.Rulesets.Catch.UI private double hyperDashModifier = 1; private int hyperDashDirection; private float hyperDashTargetPosition; - private bool hitLighting; + private Bindable hitLighting; public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) { @@ -118,7 +119,7 @@ namespace osu.Game.Rulesets.Catch.UI [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - hitLighting = config.Get(OsuSetting.HitLighting); + hitLighting = config.GetBindable(OsuSetting.HitLighting); InternalChildren = new Drawable[] { @@ -193,7 +194,7 @@ namespace osu.Game.Rulesets.Catch.UI caughtFruit.Add(fruit); - if (hitLighting) + if (hitLighting.Value) { AddInternal(new HitExplosion(fruit) { From 7fe69bb1996e83fd70417670a709a67d3cb2a1c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jul 2020 13:07:17 +0900 Subject: [PATCH 2175/6909] Fix some web requests retrieving the user too early --- osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs | 7 +++---- osu.Game/Online/API/APIRequest.cs | 7 +++++++ osu.Game/Online/API/Requests/JoinChannelRequest.cs | 7 ++----- osu.Game/Online/API/Requests/JoinRoomRequest.cs | 7 ++----- osu.Game/Online/API/Requests/LeaveChannelRequest.cs | 7 ++----- osu.Game/Online/API/Requests/PartRoomRequest.cs | 7 ++----- osu.Game/Online/Chat/ChannelManager.cs | 4 ++-- osu.Game/Screens/Multi/RoomManager.cs | 4 ++-- 8 files changed, 22 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs index 1e77d50115..42948c3731 100644 --- a/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs +++ b/osu.Game.Tests/Online/TestDummyAPIRequestHandling.cs @@ -8,7 +8,6 @@ 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 { @@ -55,7 +54,7 @@ namespace osu.Game.Tests.Online AddStep("fire request", () => { gotResponse = false; - request = new LeaveChannelRequest(new Channel(), new User()); + request = new LeaveChannelRequest(new Channel()); request.Success += () => gotResponse = true; API.Queue(request); }); @@ -74,7 +73,7 @@ namespace osu.Game.Tests.Online AddStep("fire request", () => { gotResponse = false; - request = new LeaveChannelRequest(new Channel(), new User()); + request = new LeaveChannelRequest(new Channel()); request.Success += () => gotResponse = true; API.Perform(request); }); @@ -93,7 +92,7 @@ namespace osu.Game.Tests.Online AddStep("fire request", () => { gotResponse = false; - request = new LeaveChannelRequest(new Channel(), new User()); + request = new LeaveChannelRequest(new Channel()); request.Success += () => gotResponse = true; API.PerformAsync(request); }); diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 0f8acbb7af..2115326cc2 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -5,6 +5,7 @@ using System; using Newtonsoft.Json; using osu.Framework.IO.Network; using osu.Framework.Logging; +using osu.Game.Users; namespace osu.Game.Online.API { @@ -61,6 +62,11 @@ namespace osu.Game.Online.API protected APIAccess API; protected WebRequest WebRequest; + /// + /// The currently logged in user. Note that this will only be populated during . + /// + protected User User { get; private set; } + /// /// Invoked on successful completion of an API request. /// This will be scheduled to the API's internal scheduler (run on update thread automatically). @@ -86,6 +92,7 @@ namespace osu.Game.Online.API } API = apiAccess; + User = apiAccess.LocalUser.Value; if (checkAndScheduleFailure()) return; diff --git a/osu.Game/Online/API/Requests/JoinChannelRequest.cs b/osu.Game/Online/API/Requests/JoinChannelRequest.cs index f6ed5f22c9..33eab7e355 100644 --- a/osu.Game/Online/API/Requests/JoinChannelRequest.cs +++ b/osu.Game/Online/API/Requests/JoinChannelRequest.cs @@ -4,19 +4,16 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Chat; -using osu.Game.Users; namespace osu.Game.Online.API.Requests { public class JoinChannelRequest : APIRequest { private readonly Channel channel; - private readonly User user; - public JoinChannelRequest(Channel channel, User user) + public JoinChannelRequest(Channel channel) { this.channel = channel; - this.user = user; } protected override WebRequest CreateWebRequest() @@ -26,6 +23,6 @@ namespace osu.Game.Online.API.Requests return req; } - protected override string Target => $@"chat/channels/{channel.Id}/users/{user.Id}"; + protected override string Target => $@"chat/channels/{channel.Id}/users/{User.Id}"; } } diff --git a/osu.Game/Online/API/Requests/JoinRoomRequest.cs b/osu.Game/Online/API/Requests/JoinRoomRequest.cs index 36b275236c..b0808afa45 100644 --- a/osu.Game/Online/API/Requests/JoinRoomRequest.cs +++ b/osu.Game/Online/API/Requests/JoinRoomRequest.cs @@ -4,19 +4,16 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Multiplayer; -using osu.Game.Users; namespace osu.Game.Online.API.Requests { public class JoinRoomRequest : APIRequest { private readonly Room room; - private readonly User user; - public JoinRoomRequest(Room room, User user) + public JoinRoomRequest(Room room) { this.room = room; - this.user = user; } protected override WebRequest CreateWebRequest() @@ -26,6 +23,6 @@ namespace osu.Game.Online.API.Requests return req; } - protected override string Target => $"rooms/{room.RoomID.Value}/users/{user.Id}"; + protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}"; } } diff --git a/osu.Game/Online/API/Requests/LeaveChannelRequest.cs b/osu.Game/Online/API/Requests/LeaveChannelRequest.cs index f2ae3926bd..7dfc9a0aed 100644 --- a/osu.Game/Online/API/Requests/LeaveChannelRequest.cs +++ b/osu.Game/Online/API/Requests/LeaveChannelRequest.cs @@ -4,19 +4,16 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Chat; -using osu.Game.Users; namespace osu.Game.Online.API.Requests { public class LeaveChannelRequest : APIRequest { private readonly Channel channel; - private readonly User user; - public LeaveChannelRequest(Channel channel, User user) + public LeaveChannelRequest(Channel channel) { this.channel = channel; - this.user = user; } protected override WebRequest CreateWebRequest() @@ -26,6 +23,6 @@ namespace osu.Game.Online.API.Requests return req; } - protected override string Target => $@"chat/channels/{channel.Id}/users/{user.Id}"; + protected override string Target => $@"chat/channels/{channel.Id}/users/{User.Id}"; } } diff --git a/osu.Game/Online/API/Requests/PartRoomRequest.cs b/osu.Game/Online/API/Requests/PartRoomRequest.cs index e1550cb2e0..c988cd5c9e 100644 --- a/osu.Game/Online/API/Requests/PartRoomRequest.cs +++ b/osu.Game/Online/API/Requests/PartRoomRequest.cs @@ -4,19 +4,16 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.Multiplayer; -using osu.Game.Users; namespace osu.Game.Online.API.Requests { public class PartRoomRequest : APIRequest { private readonly Room room; - private readonly User user; - public PartRoomRequest(Room room, User user) + public PartRoomRequest(Room room) { this.room = room; - this.user = user; } protected override WebRequest CreateWebRequest() @@ -26,6 +23,6 @@ namespace osu.Game.Online.API.Requests return req; } - protected override string Target => $"rooms/{room.RoomID.Value}/users/{user.Id}"; + protected override string Target => $"rooms/{room.RoomID.Value}/users/{User.Id}"; } } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 3b336fef4f..f7ed57f207 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -381,7 +381,7 @@ namespace osu.Game.Online.Chat break; default: - var req = new JoinChannelRequest(channel, api.LocalUser.Value); + var req = new JoinChannelRequest(channel); req.Success += () => joinChannel(channel, fetchInitialMessages); req.Failure += ex => LeaveChannel(channel); api.Queue(req); @@ -410,7 +410,7 @@ namespace osu.Game.Online.Chat if (channel.Joined.Value) { - api.Queue(new LeaveChannelRequest(channel, api.LocalUser.Value)); + api.Queue(new LeaveChannelRequest(channel)); channel.Joined.Value = false; } } diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index ac1f74b6a6..491be2e946 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -114,7 +114,7 @@ namespace osu.Game.Screens.Multi public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) { currentJoinRoomRequest?.Cancel(); - currentJoinRoomRequest = new JoinRoomRequest(room, api.LocalUser.Value); + currentJoinRoomRequest = new JoinRoomRequest(room); currentJoinRoomRequest.Success += () => { @@ -139,7 +139,7 @@ namespace osu.Game.Screens.Multi if (joinedRoom == null) return; - api.Queue(new PartRoomRequest(joinedRoom, api.LocalUser.Value)); + api.Queue(new PartRoomRequest(joinedRoom)); joinedRoom = null; } From 1a2f5cb477a2c13d095fd63b998a4ea3f53b8829 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 13 Jul 2020 23:59:20 -0700 Subject: [PATCH 2176/6909] Add OnBackButton bool to OsuScreen --- osu.Game/OsuGame.cs | 4 +++- osu.Game/Screens/IOsuScreen.cs | 2 ++ osu.Game/Screens/OsuScreen.cs | 2 ++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 47a7c2ae11..618049e72c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -573,7 +573,9 @@ namespace osu.Game Origin = Anchor.BottomLeft, Action = () => { - if ((ScreenStack.CurrentScreen as IOsuScreen)?.AllowBackButton == true) + var currentScreen = ScreenStack.CurrentScreen as IOsuScreen; + + if (currentScreen?.AllowBackButton == true && !currentScreen.OnBackButton()) ScreenStack.Exit(); } }, diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 22fe0ad816..6ed04291ce 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -56,5 +56,7 @@ namespace osu.Game.Screens /// Whether mod rate adjustments are allowed to be applied. /// bool AllowRateAdjustments { get; } + + bool OnBackButton(); } } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 35bb4fa34f..872a1cd39a 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -258,5 +258,7 @@ namespace osu.Game.Screens /// Note that the instance created may not be the used instance if it matches the BackgroundMode equality clause. /// protected virtual BackgroundScreen CreateBackground() => null; + + public virtual bool OnBackButton() => false; } } From 4caf4d31d4a9fb0b56ce528c3b5fac6194718ca3 Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 14 Jul 2020 00:00:10 -0700 Subject: [PATCH 2177/6909] Fix mod select blocking home and alt f4 in song select --- osu.Game/Screens/Select/SongSelect.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index e3705b15fa..74a5ee8309 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -599,12 +599,6 @@ namespace osu.Game.Screens.Select public override bool OnExiting(IScreen next) { - if (ModSelect.State.Value == Visibility.Visible) - { - ModSelect.Hide(); - return true; - } - if (base.OnExiting(next)) return true; @@ -620,6 +614,17 @@ namespace osu.Game.Screens.Select return false; } + public override bool OnBackButton() + { + if (ModSelect.State.Value == Visibility.Visible) + { + ModSelect.Hide(); + return true; + } + + return false; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From daa7430fd8d07befdcf2544143ccfe7ef1cd6316 Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 14 Jul 2020 00:00:43 -0700 Subject: [PATCH 2178/6909] Fix statistics screen blocking retry, home, and alt f4 --- osu.Game/Screens/Ranking/ResultsScreen.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 49ce07b708..44458d8c8e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -194,6 +194,13 @@ namespace osu.Game.Screens.Ranking } public override bool OnExiting(IScreen next) + { + Background.FadeTo(1, 250); + + return base.OnExiting(next); + } + + public override bool OnBackButton() { if (statisticsPanel.State.Value == Visibility.Visible) { @@ -201,9 +208,7 @@ namespace osu.Game.Screens.Ranking return true; } - Background.FadeTo(1, 250); - - return base.OnExiting(next); + return false; } private void addScore(ScoreInfo score) From 3573460d9c05aebe54995c72ea16e231f0e91dc8 Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 14 Jul 2020 00:02:01 -0700 Subject: [PATCH 2179/6909] Fix multiplayer screens blocking home and alt f4 --- osu.Game/Screens/Multi/Multiplayer.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 3178e35581..067a42d57d 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -250,12 +250,6 @@ namespace osu.Game.Screens.Multi { roomManager.PartRoom(); - if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) - { - screenStack.Exit(); - return true; - } - waves.Hide(); this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); @@ -269,6 +263,17 @@ namespace osu.Game.Screens.Multi return false; } + public override bool OnBackButton() + { + if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) + { + screenStack.Exit(); + return true; + } + + return false; + } + protected override void LogoExiting(OsuLogo logo) { base.LogoExiting(logo); From 8ace06fcc5c47b1c5f67c659cd8e205a1954f2a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jul 2020 16:03:40 +0900 Subject: [PATCH 2180/6909] Fix continuations attaching to the BeatmapOnlineLookupQueue scheduler --- osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index 4de4e21b15..16207c7d2a 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -54,7 +54,7 @@ namespace osu.Game.Beatmaps // todo: expose this when we need to do individual difficulty lookups. protected Task UpdateAsync(BeatmapSetInfo beatmapSet, BeatmapInfo beatmap, CancellationToken cancellationToken) - => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler, updateScheduler); + => Task.Factory.StartNew(() => lookup(beatmapSet, beatmap), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); private void lookup(BeatmapSetInfo set, BeatmapInfo beatmap) { From 36041fc2453832976cad0b6c89a0ec0979dae68b Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 14 Jul 2020 00:29:57 -0700 Subject: [PATCH 2181/6909] Fix back button not working correctly with multi song select's mod select --- osu.Game/Screens/Multi/Multiplayer.cs | 12 ++++++++++-- osu.Game/Screens/Select/SongSelect.cs | 2 +- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 067a42d57d..2a73b53199 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -20,9 +20,9 @@ using osu.Game.Online.Multiplayer; using osu.Game.Screens.Menu; using osu.Game.Screens.Multi.Components; 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.Select; using osuTK; namespace osu.Game.Screens.Multi @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Multi private readonly Bindable selectedRoom = new Bindable(); [Cached] - private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); + private readonly Bindable currentFilter = new Bindable(new Lounge.Components.FilterCriteria()); [Cached(Type = typeof(IRoomManager))] private RoomManager roomManager; @@ -265,6 +265,14 @@ namespace osu.Game.Screens.Multi public override bool OnBackButton() { + var songSelect = screenStack.CurrentScreen as MatchSongSelect; + + if (songSelect?.ModSelect.State.Value == Visibility.Visible) + { + songSelect.ModSelect.Hide(); + return true; + } + if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) { screenStack.Exit(); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 74a5ee8309..87fad66b66 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select [Resolved] private BeatmapManager beatmaps { get; set; } - protected ModSelectOverlay ModSelect { get; private set; } + public ModSelectOverlay ModSelect { get; private set; } protected SampleChannel SampleConfirm { get; private set; } From 304e518f7ba48d32dc426a0bf959d5481cb30f12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jul 2020 17:30:20 +0900 Subject: [PATCH 2182/6909] 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 8510632d45..85d154f2e2 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 05d6f27d40..b8e73262c4 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 af779b32fd..1faf60b1d9 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From fec2594ac67f42ef52f2aa0fd63afbc4b65ca88b Mon Sep 17 00:00:00 2001 From: LastExceed Date: Tue, 14 Jul 2020 11:56:31 +0200 Subject: [PATCH 2183/6909] reverse LaneCover when playing up-scroll --- .../Mods/ManiaModFadeIn.cs | 58 +++++++++++-------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index ed990f8d5f..21c855dedd 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -10,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; @@ -45,11 +47,9 @@ namespace osu.Game.Rulesets.Mania.Mods Children = new Drawable[] { hoc, - new LaneCover(false) + new LaneCover(0.5f, false) { - RelativeSizeAxes = Axes.Both, - SizeFilled = 0.5f, - SizeGradient = 0.25f + RelativeSizeAxes = Axes.Both } } }); @@ -60,9 +60,8 @@ namespace osu.Game.Rulesets.Mania.Mods { private readonly Box gradient; private readonly Box filled; - private readonly bool reversed; - public LaneCover(bool reversed) + public LaneCover(float initialCoverage, bool reversed) { Blending = new BlendingParameters { @@ -73,50 +72,61 @@ namespace osu.Game.Rulesets.Mania.Mods SourceAlpha = BlendingType.Zero, DestinationAlpha = BlendingType.OneMinusSrcAlpha }; - InternalChildren = new Drawable[] { gradient = new Box { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4.White.Opacity(1f), Color4.White.Opacity(0f)) + Height = 0.25f }, filled = new Box { RelativeSizeAxes = Axes.Both } }; - - if (reversed) - { - gradient.Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0f), Color4.White.Opacity(1f)); - filled.Anchor = Anchor.BottomLeft; - filled.Origin = Anchor.BottomLeft; - } - - this.reversed = reversed; + Coverage = initialCoverage; + Reversed = reversed; } - public float SizeFilled + private float coverage; + + public float Coverage { set { filled.Height = value; - if (!reversed) - gradient.Y = value; + gradient.Y = reversed ? 1 - value - gradient.Height : value; + coverage = value; } } - public float SizeGradient + private bool reversed; + + public bool Reversed { set { - gradient.Height = value; - if (reversed) - gradient.Y = 1 - value - filled.Height; + filled.Anchor = value ? Anchor.BottomLeft : Anchor.TopLeft; + filled.Origin = value ? Anchor.BottomLeft : Anchor.TopLeft; + gradient.Colour = ColourInfo.GradientVertical( + Color4.White.Opacity(value ? 0f : 1f), + Color4.White.Opacity(value ? 1f : 0f) + ); + + reversed = value; + Coverage = coverage; //re-apply coverage to update visuals } } + + [BackgroundDependencyLoader] + private void load(ManiaRulesetConfigManager configManager) + { + var scrollDirection = configManager.GetBindable(ManiaRulesetSetting.ScrollDirection); + + if (scrollDirection.Value == ManiaScrollingDirection.Up) + Reversed = !reversed; + } } } } From c7d3b025ada6692d159182bc42fbd9d1e844931f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jul 2020 20:11:54 +0900 Subject: [PATCH 2184/6909] Rename variable and change default to true --- osu.Game/Configuration/OsuConfigManager.cs | 4 ++-- .../Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 2 +- osu.Game/Screens/Play/ComboEffects.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 10d11f967e..268328272c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -91,7 +91,7 @@ namespace osu.Game.Configuration Set(OsuSetting.FadePlayfieldWhenHealthLow, true); Set(OsuSetting.KeyOverlay, false); Set(OsuSetting.PositionalHitSounds, true); - Set(OsuSetting.AlwaysPlayComboBreak, false); + Set(OsuSetting.AlwaysPlayFirstComboBreak, true); Set(OsuSetting.ScoreMeter, ScoreMeterType.HitErrorBoth); Set(OsuSetting.FloatingComments, false); @@ -181,7 +181,7 @@ namespace osu.Game.Configuration ShowStoryboard, KeyOverlay, PositionalHitSounds, - AlwaysPlayComboBreak, + AlwaysPlayFirstComboBreak, ScoreMeter, FloatingComments, ShowInterface, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index f3534e4625..d79f1ba583 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -71,7 +71,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { LabelText = "Always play first combo break sound", Keywords = new[] { "regardless", "combobreak.wav" }, - Bindable = config.GetBindable(OsuSetting.AlwaysPlayComboBreak) + Bindable = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak) }, new SettingsEnumDropdown { diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index c56ee35cec..a836b6137e 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play private void load(OsuConfigManager config) { InternalChild = comboBreakSample = new SkinnableSound(new SampleInfo("combobreak")); - alwaysPlay = config.GetBindable(OsuSetting.AlwaysPlayComboBreak); + alwaysPlay = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak); } protected override void LoadComplete() From 956980ee9055738a73dd9791ca52ff1a3de2ab18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jul 2020 20:12:58 +0900 Subject: [PATCH 2185/6909] Remove setting from gameplay settings screen --- .../Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index d79f1ba583..93a02ea0e4 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -67,12 +67,6 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Positional hitsounds", Bindable = config.GetBindable(OsuSetting.PositionalHitSounds) }, - new SettingsCheckbox - { - LabelText = "Always play first combo break sound", - Keywords = new[] { "regardless", "combobreak.wav" }, - Bindable = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak) - }, new SettingsEnumDropdown { LabelText = "Score meter type", From 2626ab41c3e38b775896c652c59f7b4b0ee335d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 14 Jul 2020 20:15:29 +0900 Subject: [PATCH 2186/6909] Add implicit braces for clarity --- osu.Game/Screens/Play/ComboEffects.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index a836b6137e..5bcda50399 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Play private void onComboChange(ValueChangedEvent combo) { - if (combo.NewValue == 0 && (combo.OldValue > 20 || alwaysPlay.Value && firstTime)) + if (combo.NewValue == 0 && (combo.OldValue > 20 || (alwaysPlay.Value && firstTime))) { comboBreakSample?.Play(); firstTime = false; From b64ddf061ea5ba2aa1aa479ccbbba148df5018d9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 14 Jul 2020 20:37:21 +0900 Subject: [PATCH 2187/6909] Remove whitespace --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 08b2acc2ef..6ae420b162 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -162,7 +162,7 @@ namespace osu.Game.Input.Bindings [Description("Toggle notifications")] ToggleNotifications, - + [Description("Pause")] PauseGameplay, } From 4c2294f0cd65ebce410d979af5f3b7b395da64ef Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jul 2020 15:02:29 +0300 Subject: [PATCH 2188/6909] Refactor LoadRepliesButton to inherit LoadingButton --- .../TestSceneCommentRepliesButton.cs | 5 +++- .../Comments/Buttons/CommentRepliesButton.cs | 12 +++------- .../Comments/Buttons/LoadRepliesButton.cs | 23 ++++++++++++++++-- .../Comments/Buttons/ShowRepliesButton.cs | 4 ++++ osu.Game/Overlays/Comments/DrawableComment.cs | 24 ++----------------- 5 files changed, 34 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs index c2dc804385..c2ac5179c9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentRepliesButton.cs @@ -33,7 +33,10 @@ namespace osu.Game.Tests.Visual.UserInterface Children = new Drawable[] { button = new TestButton(), - new LoadRepliesButton(), + new LoadRepliesButton + { + Action = () => { } + }, new ShowRepliesButton(1), new ShowRepliesButton(2) } diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index abe80722e2..f7e0cb0a6c 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.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; @@ -17,8 +16,6 @@ namespace osu.Game.Overlays.Comments.Buttons { public abstract class CommentRepliesButton : CompositeDrawable { - public Action Action { get; set; } - protected string Text { get => text.Text; @@ -72,6 +69,7 @@ namespace osu.Game.Overlays.Comments.Buttons { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + AlwaysPresent = true, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) }, icon = new SpriteIcon @@ -99,6 +97,8 @@ namespace osu.Game.Overlays.Comments.Buttons protected void SetIconDirection(bool upwards) => icon.ScaleTo(new Vector2(1, upwards ? -1 : 1)); + public void ToggleTextVisibility(bool visible) => text.FadeTo(visible ? 1 : 0, 200, Easing.OutQuint); + protected override bool OnHover(HoverEvent e) { base.OnHover(e); @@ -113,11 +113,5 @@ namespace osu.Game.Overlays.Comments.Buttons background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint); icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); } - - protected override bool OnClick(ClickEvent e) - { - Action?.Invoke(); - return base.OnClick(e); - } } } diff --git a/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs index 9387c95758..4998e5391e 100644 --- a/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/LoadRepliesButton.cs @@ -1,13 +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.Game.Graphics.UserInterface; + namespace osu.Game.Overlays.Comments.Buttons { - public class LoadRepliesButton : CommentRepliesButton + public class LoadRepliesButton : LoadingButton { + private ButtonContent content; + public LoadRepliesButton() { - Text = "load replies"; + AutoSizeAxes = Axes.Both; + } + + protected override Drawable CreateContent() => content = new ButtonContent(); + + protected override void OnLoadStarted() => content.ToggleTextVisibility(false); + + protected override void OnLoadFinished() => content.ToggleTextVisibility(true); + + private class ButtonContent : CommentRepliesButton + { + public ButtonContent() + { + Text = "load replies"; + } } } } diff --git a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs index e2023c2f58..aeb33e6756 100644 --- a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.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 Humanizer; using osu.Framework.Bindables; using osu.Framework.Input.Events; @@ -9,6 +10,8 @@ namespace osu.Game.Overlays.Comments.Buttons { public class ShowRepliesButton : CommentRepliesButton { + public Action Action; + public readonly BindableBool Expanded = new BindableBool(true); public ShowRepliesButton(int count) @@ -25,6 +28,7 @@ namespace osu.Game.Overlays.Comments.Buttons protected override bool OnClick(ClickEvent e) { Expanded.Toggle(); + Action?.Invoke(); return base.OnClick(e); } } diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 731ebe7104..3cdc0a0cbd 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -22,7 +22,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Extensions.IEnumerableExtensions; using System.Collections.Specialized; using osu.Game.Overlays.Comments.Buttons; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Comments { @@ -52,7 +51,6 @@ namespace osu.Game.Overlays.Comments private ShowRepliesButton showRepliesButton; private ChevronButton chevronButton; private DeletedCommentsCounter deletedCommentsCounter; - private LoadingSpinner loading; public DrawableComment(Comment comment) { @@ -194,12 +192,7 @@ namespace osu.Game.Overlays.Comments }, loadRepliesButton = new LoadRepliesButton { - Action = () => - { - RepliesRequested(this, ++currentPage); - loadRepliesButton.Hide(); - loading.Show(); - } + Action = () => RepliesRequested(this, ++currentPage) } } } @@ -209,17 +202,6 @@ namespace osu.Game.Overlays.Comments } } }, - loading = new LoadingSpinner - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Size = new Vector2(15), - Margin = new MarginPadding - { - Vertical = 5, - Left = 80 - } - }, childCommentsVisibilityContainer = new FillFlowContainer { RelativeSizeAxes = Axes.X, @@ -372,9 +354,7 @@ namespace osu.Game.Overlays.Comments if (Comment.IsTopLevel) chevronButton.FadeTo(loadedReplesCount != 0 ? 1 : 0); - showMoreButton.IsLoading = false; - loading.Hide(); - loading.FinishTransforms(); + showMoreButton.IsLoading = loadRepliesButton.IsLoading = false; } private class ChevronButton : ShowChildrenButton From 28006ac33f7f71fefd1dafdf490a760d260f661a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jul 2020 15:12:18 +0300 Subject: [PATCH 2189/6909] Remove unnecessary action from ShowRepliesButton --- osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs index aeb33e6756..e2023c2f58 100644 --- a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.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 Humanizer; using osu.Framework.Bindables; using osu.Framework.Input.Events; @@ -10,8 +9,6 @@ namespace osu.Game.Overlays.Comments.Buttons { public class ShowRepliesButton : CommentRepliesButton { - public Action Action; - public readonly BindableBool Expanded = new BindableBool(true); public ShowRepliesButton(int count) @@ -28,7 +25,6 @@ namespace osu.Game.Overlays.Comments.Buttons protected override bool OnClick(ClickEvent e) { Expanded.Toggle(); - Action?.Invoke(); return base.OnClick(e); } } From fcda4d9f15240906e1eb4f3ccbe3648a7a45e226 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Tue, 14 Jul 2020 15:06:15 +0200 Subject: [PATCH 2190/6909] move lanecover implementation to ManiaModHidden --- .../Mods/ManiaModFadeIn.cs | 118 +----------------- .../Mods/ManiaModHidden.cs | 111 +++++++++++++++- 2 files changed, 111 insertions(+), 118 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index 21c855dedd..bdc8cb31e5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -1,132 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.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.Game.Graphics; -using osu.Game.Rulesets.Mania.Configuration; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.UI; -using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModFadeIn : Mod, IApplicableToDrawableRuleset + public class ManiaModFadeIn : ManiaModHidden { public override string Name => "Fade In"; public override string Acronym => "FI"; public override IconUsage? Icon => OsuIcon.ModHidden; - public override ModType Type => ModType.DifficultyIncrease; public override string Description => @"Keys appear out of nowhere!"; - public override double ScoreMultiplier => 1; - public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; - - public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) - { - ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; - - foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) - { - HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; - Container hocParent = (Container)hoc.Parent; - - hocParent.Remove(hoc); - hocParent.Add(new BufferedContainer - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - hoc, - new LaneCover(0.5f, false) - { - RelativeSizeAxes = Axes.Both - } - } - }); - } - } - - private class LaneCover : CompositeDrawable - { - private readonly Box gradient; - private readonly Box filled; - - public LaneCover(float initialCoverage, bool reversed) - { - Blending = new BlendingParameters - { - RGBEquation = BlendingEquation.Add, - Source = BlendingType.Zero, - Destination = BlendingType.One, - AlphaEquation = BlendingEquation.Add, - SourceAlpha = BlendingType.Zero, - DestinationAlpha = BlendingType.OneMinusSrcAlpha - }; - InternalChildren = new Drawable[] - { - gradient = new Box - { - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, - Height = 0.25f - }, - filled = new Box - { - RelativeSizeAxes = Axes.Both - } - }; - Coverage = initialCoverage; - Reversed = reversed; - } - - private float coverage; - - public float Coverage - { - set - { - filled.Height = value; - gradient.Y = reversed ? 1 - value - gradient.Height : value; - coverage = value; - } - } - - private bool reversed; - - public bool Reversed - { - set - { - filled.Anchor = value ? Anchor.BottomLeft : Anchor.TopLeft; - filled.Origin = value ? Anchor.BottomLeft : Anchor.TopLeft; - gradient.Colour = ColourInfo.GradientVertical( - Color4.White.Opacity(value ? 0f : 1f), - Color4.White.Opacity(value ? 1f : 0f) - ); - - reversed = value; - Coverage = coverage; //re-apply coverage to update visuals - } - } - - [BackgroundDependencyLoader] - private void load(ManiaRulesetConfigManager configManager) - { - var scrollDirection = configManager.GetBindable(ManiaRulesetSetting.ScrollDirection); - - if (scrollDirection.Value == ManiaScrollingDirection.Up) - Reversed = !reversed; - } - } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 66b90984b4..3eafbdb671 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -2,15 +2,124 @@ // 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.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModHidden : ModHidden + public class ManiaModHidden : ModHidden, IApplicableToDrawableRuleset { public override string Description => @"Keys fade out before you hit them!"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; + + foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) + { + HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; + Container hocParent = (Container)hoc.Parent; + + hocParent.Remove(hoc); + hocParent.Add(new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + hoc, + new LaneCover(0.5f, false) + { + RelativeSizeAxes = Axes.Both + } + } + }); + } + } + + private class LaneCover : CompositeDrawable + { + private readonly Box gradient; + private readonly Box filled; + + public LaneCover(float initialCoverage, bool reversed) + { + Blending = new BlendingParameters + { + RGBEquation = BlendingEquation.Add, + Source = BlendingType.Zero, + Destination = BlendingType.One, + AlphaEquation = BlendingEquation.Add, + SourceAlpha = BlendingType.Zero, + DestinationAlpha = BlendingType.OneMinusSrcAlpha + }; + InternalChildren = new Drawable[] + { + gradient = new Box + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Height = 0.25f + }, + filled = new Box + { + RelativeSizeAxes = Axes.Both + } + }; + Coverage = initialCoverage; + Reversed = reversed; + } + + private float coverage; + + public float Coverage + { + set + { + filled.Height = value; + gradient.Y = reversed ? 1 - value - gradient.Height : value; + coverage = value; + } + } + + private bool reversed; + + public bool Reversed + { + set + { + filled.Anchor = value ? Anchor.BottomLeft : Anchor.TopLeft; + filled.Origin = value ? Anchor.BottomLeft : Anchor.TopLeft; + gradient.Colour = ColourInfo.GradientVertical( + Color4.White.Opacity(value ? 0f : 1f), + Color4.White.Opacity(value ? 1f : 0f) + ); + + reversed = value; + Coverage = coverage; //re-apply coverage to update visuals + } + } + + [BackgroundDependencyLoader] + private void load(ManiaRulesetConfigManager configManager) + { + var scrollDirection = configManager.GetBindable(ManiaRulesetSetting.ScrollDirection); + + if (scrollDirection.Value == ManiaScrollingDirection.Up) + Reversed = !reversed; + } + } } } From 921939f97a26a5edf965ac2c6c56f81661a4c5c2 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Tue, 14 Jul 2020 15:12:00 +0200 Subject: [PATCH 2191/6909] extract coverage updating logic to separate method --- .../Mods/ManiaModHidden.cs | 39 +++++++++++++------ 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 3eafbdb671..41f9948c2a 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -40,8 +40,9 @@ namespace osu.Game.Rulesets.Mania.Mods Children = new Drawable[] { hoc, - new LaneCover(0.5f, false) + new LaneCover(false) { + Coverage = 0.5f, RelativeSizeAxes = Axes.Both } } @@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Mods private readonly Box gradient; private readonly Box filled; - public LaneCover(float initialCoverage, bool reversed) + public LaneCover(bool reversed) { Blending = new BlendingParameters { @@ -65,6 +66,7 @@ namespace osu.Game.Rulesets.Mania.Mods SourceAlpha = BlendingType.Zero, DestinationAlpha = BlendingType.OneMinusSrcAlpha }; + InternalChildren = new Drawable[] { gradient = new Box @@ -78,19 +80,35 @@ namespace osu.Game.Rulesets.Mania.Mods RelativeSizeAxes = Axes.Both } }; - Coverage = initialCoverage; + Reversed = reversed; } + private void updateCoverage() + { + filled.Anchor = reversed ? Anchor.BottomLeft : Anchor.TopLeft; + filled.Origin = reversed ? Anchor.BottomLeft : Anchor.TopLeft; + filled.Height = coverage; + + gradient.Y = reversed ? 1 - filled.Height - gradient.Height : coverage; + gradient.Colour = ColourInfo.GradientVertical( + Color4.White.Opacity(reversed ? 0f : 1f), + Color4.White.Opacity(reversed ? 1f : 0f) + ); + } + private float coverage; public float Coverage { set { - filled.Height = value; - gradient.Y = reversed ? 1 - value - gradient.Height : value; + if (coverage == value) + return; + coverage = value; + + updateCoverage(); } } @@ -100,15 +118,12 @@ namespace osu.Game.Rulesets.Mania.Mods { set { - filled.Anchor = value ? Anchor.BottomLeft : Anchor.TopLeft; - filled.Origin = value ? Anchor.BottomLeft : Anchor.TopLeft; - gradient.Colour = ColourInfo.GradientVertical( - Color4.White.Opacity(value ? 0f : 1f), - Color4.White.Opacity(value ? 1f : 0f) - ); + if (reversed == value) + return; reversed = value; - Coverage = coverage; //re-apply coverage to update visuals + + updateCoverage(); } } From c2c80d2a9891acd9959f3598249ca5cfabaf1b9c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jul 2020 17:34:01 +0300 Subject: [PATCH 2192/6909] Refactor SpotlightSelector layout --- .../Overlays/Rankings/SpotlightSelector.cs | 75 ++++++++++++------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs index f019b50ae8..4d07d6c118 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -50,10 +50,11 @@ namespace osu.Game.Overlays.Rankings public SpotlightSelector() { RelativeSizeAxes = Axes.X; - Height = 100; + AutoSizeAxes = Axes.Y; Add(content = new Container { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, Children = new Drawable[] { background = new Box @@ -62,31 +63,52 @@ namespace osu.Game.Overlays.Rankings }, new Container { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Vertical = 10 }, - Children = new Drawable[] + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN }, + Child = new FillFlowContainer { - dropdown = new SpotlightsDropdown + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Current = Current, - Depth = -float.MaxValue - }, - new FillFlowContainer - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(15, 0), - Children = new Drawable[] + new Container { - startDateColumn = new InfoColumn(@"Start Date"), - endDateColumn = new InfoColumn(@"End Date"), - mapCountColumn = new InfoColumn(@"Map Count"), - participantsColumn = new InfoColumn(@"Participants") + Margin = new MarginPadding { Vertical = 20 }, + RelativeSizeAxes = Axes.X, + Height = 40, + Depth = -float.MaxValue, + Child = dropdown = new SpotlightsDropdown + { + RelativeSizeAxes = Axes.X, + Current = Current + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Margin = new MarginPadding { Vertical = 5 }, + Children = new Drawable[] + { + startDateColumn = new InfoColumn(@"Start Date"), + endDateColumn = new InfoColumn(@"End Date"), + mapCountColumn = new InfoColumn(@"Map Count"), + participantsColumn = new InfoColumn(@"Participants") + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Vertical = 20 }, + Child = new RankingsSortTabControl + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight + } } } } @@ -128,12 +150,13 @@ namespace osu.Game.Overlays.Rankings { AutoSizeAxes = Axes.Both; Direction = FillDirection.Vertical; + Margin = new MarginPadding { Vertical = 10 }; Children = new Drawable[] { new OsuSpriteText { Text = name, - Font = OsuFont.GetFont(size: 10), + Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular), }, new Container { @@ -143,7 +166,7 @@ namespace osu.Game.Overlays.Rankings { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Font = OsuFont.GetFont(size: 18, weight: FontWeight.Light), + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Light), } } }; From 25fb49d59fe5e9fb122eec309e4c034a0a1100cb Mon Sep 17 00:00:00 2001 From: LastExceed Date: Tue, 14 Jul 2020 16:43:15 +0200 Subject: [PATCH 2193/6909] bind laneCover direction to scroll direction --- .../Mods/ManiaModHidden.cs | 36 +++++++------------ 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 41f9948c2a..fd65edd482 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Mania.Mods Children = new Drawable[] { hoc, - new LaneCover(false) + new LaneCover { Coverage = 0.5f, RelativeSizeAxes = Axes.Both @@ -54,8 +55,10 @@ namespace osu.Game.Rulesets.Mania.Mods { private readonly Box gradient; private readonly Box filled; + private bool reversed; + private readonly Bindable scrollDirection = new Bindable(); - public LaneCover(bool reversed) + public LaneCover() { Blending = new BlendingParameters { @@ -80,8 +83,6 @@ namespace osu.Game.Rulesets.Mania.Mods RelativeSizeAxes = Axes.Both } }; - - Reversed = reversed; } private void updateCoverage() @@ -97,6 +98,12 @@ namespace osu.Game.Rulesets.Mania.Mods ); } + private void onScrollDirectionChanged(ValueChangedEvent valueChangedEvent) + { + reversed = valueChangedEvent.NewValue == ManiaScrollingDirection.Up; + updateCoverage(); + } + private float coverage; public float Coverage @@ -112,28 +119,11 @@ namespace osu.Game.Rulesets.Mania.Mods } } - private bool reversed; - - public bool Reversed - { - set - { - if (reversed == value) - return; - - reversed = value; - - updateCoverage(); - } - } - [BackgroundDependencyLoader] private void load(ManiaRulesetConfigManager configManager) { - var scrollDirection = configManager.GetBindable(ManiaRulesetSetting.ScrollDirection); - - if (scrollDirection.Value == ManiaScrollingDirection.Up) - Reversed = !reversed; + scrollDirection.BindTo(configManager.GetBindable(ManiaRulesetSetting.ScrollDirection)); + scrollDirection.BindValueChanged(onScrollDirectionChanged, true); } } } From 3b7d52da099035167da942935730429160a33447 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Tue, 14 Jul 2020 16:48:14 +0200 Subject: [PATCH 2194/6909] rearrange LaneCover members --- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index fd65edd482..ae927e5cd4 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -85,6 +85,13 @@ namespace osu.Game.Rulesets.Mania.Mods }; } + [BackgroundDependencyLoader] + private void load(ManiaRulesetConfigManager configManager) + { + scrollDirection.BindTo(configManager.GetBindable(ManiaRulesetSetting.ScrollDirection)); + scrollDirection.BindValueChanged(onScrollDirectionChanged, true); + } + private void updateCoverage() { filled.Anchor = reversed ? Anchor.BottomLeft : Anchor.TopLeft; @@ -118,13 +125,6 @@ namespace osu.Game.Rulesets.Mania.Mods updateCoverage(); } } - - [BackgroundDependencyLoader] - private void load(ManiaRulesetConfigManager configManager) - { - scrollDirection.BindTo(configManager.GetBindable(ManiaRulesetSetting.ScrollDirection)); - scrollDirection.BindValueChanged(onScrollDirectionChanged, true); - } } } } From f73fd7ffe9ce7dbeab4e0bb120083f1ffd8bd5ce Mon Sep 17 00:00:00 2001 From: LastExceed Date: Tue, 14 Jul 2020 17:04:09 +0200 Subject: [PATCH 2195/6909] read scroll direction from IScrollingInfo instead of config --- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index ae927e5cd4..af6fc24983 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -10,11 +10,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Rulesets.UI.Scrolling; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Mods @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Mods private readonly Box gradient; private readonly Box filled; private bool reversed; - private readonly Bindable scrollDirection = new Bindable(); + private readonly IBindable scrollDirection = new Bindable(); public LaneCover() { @@ -86,9 +86,9 @@ namespace osu.Game.Rulesets.Mania.Mods } [BackgroundDependencyLoader] - private void load(ManiaRulesetConfigManager configManager) + private void load(IScrollingInfo configManager) { - scrollDirection.BindTo(configManager.GetBindable(ManiaRulesetSetting.ScrollDirection)); + scrollDirection.BindTo(configManager.Direction); scrollDirection.BindValueChanged(onScrollDirectionChanged, true); } @@ -105,9 +105,9 @@ namespace osu.Game.Rulesets.Mania.Mods ); } - private void onScrollDirectionChanged(ValueChangedEvent valueChangedEvent) + private void onScrollDirectionChanged(ValueChangedEvent valueChangedEvent) { - reversed = valueChangedEvent.NewValue == ManiaScrollingDirection.Up; + reversed = valueChangedEvent.NewValue == ScrollingDirection.Up; updateCoverage(); } From 3d9e174ae8369f6a655e7781ea4af73470a585bb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jul 2020 18:09:09 +0300 Subject: [PATCH 2196/6909] Make sort changes affect request result --- .../Visual/Online/TestSceneRankingsTables.cs | 3 ++- .../API/Requests/GetSpotlightRankingsRequest.cs | 6 +++++- osu.Game/Overlays/Rankings/SpotlightSelector.cs | 11 ++++++----- osu.Game/Overlays/Rankings/SpotlightsLayout.cs | 10 +++++++--- 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs index a3b102dc76..ee109189c7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsTables.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Catch; using osu.Framework.Allocation; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; +using osu.Game.Overlays.Rankings; namespace osu.Game.Tests.Visual.Online { @@ -105,7 +106,7 @@ namespace osu.Game.Tests.Visual.Online { onLoadStarted(); - request = new GetSpotlightRankingsRequest(ruleset, spotlight); + request = new GetSpotlightRankingsRequest(ruleset, spotlight, RankingsSortCriteria.All); ((GetSpotlightRankingsRequest)request).Success += rankings => Schedule(() => { var table = new ScoresTable(1, rankings.Users); diff --git a/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs b/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs index a279db134f..25e6b3f1af 100644 --- a/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetSpotlightRankingsRequest.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.IO.Network; +using osu.Game.Overlays.Rankings; using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests @@ -9,11 +10,13 @@ namespace osu.Game.Online.API.Requests public class GetSpotlightRankingsRequest : GetRankingsRequest { private readonly int spotlight; + private readonly RankingsSortCriteria sort; - public GetSpotlightRankingsRequest(RulesetInfo ruleset, int spotlight) + public GetSpotlightRankingsRequest(RulesetInfo ruleset, int spotlight, RankingsSortCriteria sort) : base(ruleset, 1) { this.spotlight = spotlight; + this.sort = sort; } protected override WebRequest CreateWebRequest() @@ -21,6 +24,7 @@ namespace osu.Game.Online.API.Requests var req = base.CreateWebRequest(); req.AddParameter("spotlight", spotlight.ToString()); + req.AddParameter("filter", sort.ToString().ToLower()); return req; } diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs index 4d07d6c118..fbea53782b 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -22,10 +22,8 @@ namespace osu.Game.Overlays.Rankings { private const int duration = 300; - private readonly Box background; - private readonly SpotlightsDropdown dropdown; - private readonly BindableWithCurrent current = new BindableWithCurrent(); + public readonly Bindable Sort = new Bindable(); public Bindable Current { @@ -41,11 +39,13 @@ namespace osu.Game.Overlays.Rankings protected override bool StartHidden => true; + private readonly Box background; + private readonly Container content; + private readonly SpotlightsDropdown dropdown; private readonly InfoColumn startDateColumn; private readonly InfoColumn endDateColumn; private readonly InfoColumn mapCountColumn; private readonly InfoColumn participantsColumn; - private readonly Container content; public SpotlightSelector() { @@ -107,7 +107,8 @@ namespace osu.Game.Overlays.Rankings Child = new RankingsSortTabControl { Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight + Origin = Anchor.CentreRight, + Current = Sort } } } diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index 917509e842..0f9b07bf89 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -24,6 +24,7 @@ namespace osu.Game.Overlays.Rankings public readonly Bindable Ruleset = new Bindable(); private readonly Bindable selectedSpotlight = new Bindable(); + private readonly Bindable sort = new Bindable(); [Resolved] private IAPIProvider api { get; set; } @@ -72,6 +73,8 @@ namespace osu.Game.Overlays.Rankings } } }; + + sort.BindTo(selector.Sort); } protected override void LoadComplete() @@ -80,7 +83,8 @@ namespace osu.Game.Overlays.Rankings selector.Show(); - selectedSpotlight.BindValueChanged(onSpotlightChanged); + selectedSpotlight.BindValueChanged(_ => onSpotlightChanged()); + sort.BindValueChanged(_ => onSpotlightChanged()); Ruleset.BindValueChanged(onRulesetChanged); getSpotlights(); @@ -101,14 +105,14 @@ namespace osu.Game.Overlays.Rankings selectedSpotlight.TriggerChange(); } - private void onSpotlightChanged(ValueChangedEvent spotlight) + private void onSpotlightChanged() { loading.Show(); cancellationToken?.Cancel(); getRankingsRequest?.Cancel(); - getRankingsRequest = new GetSpotlightRankingsRequest(Ruleset.Value, spotlight.NewValue.Id); + getRankingsRequest = new GetSpotlightRankingsRequest(Ruleset.Value, selectedSpotlight.Value.Id, sort.Value); getRankingsRequest.Success += onSuccess; api.Queue(getRankingsRequest); } From 85c875757219da92b49e4d3582def1653ae2c8f6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 14 Jul 2020 21:18:46 +0300 Subject: [PATCH 2197/6909] Return true on click --- osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs index e2023c2f58..04e7e25cc5 100644 --- a/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowRepliesButton.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Comments.Buttons protected override bool OnClick(ClickEvent e) { Expanded.Toggle(); - return base.OnClick(e); + return true; } } } From 21ed9df1ea49fe4c97b9156f737ab62adea4711d Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 14 Jul 2020 13:14:59 -0700 Subject: [PATCH 2198/6909] Add xmldoc for OnBackButton --- osu.Game/Screens/IOsuScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 6ed04291ce..5f9f611a24 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -57,6 +57,9 @@ namespace osu.Game.Screens /// bool AllowRateAdjustments { get; } + /// + /// Whether there are sub overlays/screens that need closing with the back button before this can be exited. + /// bool OnBackButton(); } } From 73e1bf0d89ec605011cb6e7270cc4c2b3ca31307 Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 14 Jul 2020 13:19:48 -0700 Subject: [PATCH 2199/6909] Check sub screen's OnBackButton instead --- osu.Game/Screens/Multi/Multiplayer.cs | 11 +++-------- osu.Game/Screens/Select/SongSelect.cs | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 2a73b53199..951f21dc2d 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -20,9 +20,9 @@ using osu.Game.Online.Multiplayer; using osu.Game.Screens.Menu; using osu.Game.Screens.Multi.Components; 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.Select; using osuTK; namespace osu.Game.Screens.Multi @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Multi private readonly Bindable selectedRoom = new Bindable(); [Cached] - private readonly Bindable currentFilter = new Bindable(new Lounge.Components.FilterCriteria()); + private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); [Cached(Type = typeof(IRoomManager))] private RoomManager roomManager; @@ -265,13 +265,8 @@ namespace osu.Game.Screens.Multi public override bool OnBackButton() { - var songSelect = screenStack.CurrentScreen as MatchSongSelect; - - if (songSelect?.ModSelect.State.Value == Visibility.Visible) - { - songSelect.ModSelect.Hide(); + if ((screenStack.CurrentScreen as IMultiplayerSubScreen).OnBackButton()) return true; - } if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 87fad66b66..74a5ee8309 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Select [Resolved] private BeatmapManager beatmaps { get; set; } - public ModSelectOverlay ModSelect { get; private set; } + protected ModSelectOverlay ModSelect { get; private set; } protected SampleChannel SampleConfirm { get; private set; } From 79f6092344f01b3bb263c769af4d281edb6dd213 Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 14 Jul 2020 13:31:15 -0700 Subject: [PATCH 2200/6909] Fix back button not glowing when closing mod select with escape --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 3d0ad1a594..c4a59b57cb 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -19,6 +19,7 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Overlays.Mods.Sections; using osu.Game.Rulesets.Mods; using osu.Game.Screens; @@ -489,5 +490,7 @@ namespace osu.Game.Overlays.Mods } #endregion + + public override bool OnPressed(GlobalAction action) => false; // handled by back button } } From ecbd8067e9f513682a5ae9cb1fcf9cf1d3dc7a57 Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 14 Jul 2020 20:18:47 -0700 Subject: [PATCH 2201/6909] Add ability to seek replays/auto with arrow keys --- osu.Game/Screens/Play/SongProgressBar.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/SongProgressBar.cs b/osu.Game/Screens/Play/SongProgressBar.cs index 5052b32335..939b5fad1f 100644 --- a/osu.Game/Screens/Play/SongProgressBar.cs +++ b/osu.Game/Screens/Play/SongProgressBar.cs @@ -57,6 +57,8 @@ namespace osu.Game.Screens.Play set => CurrentNumber.Value = value; } + protected override bool AllowKeyboardInputWhenNotHovered => true; + public SongProgressBar(float barHeight, float handleBarHeight, Vector2 handleSize) { CurrentNumber.MinValue = 0; From 350a4a153bc88948bf47763ee42d635b19d642fd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Jul 2020 12:59:31 +0900 Subject: [PATCH 2202/6909] Fix possible nullref --- 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 951f21dc2d..269eab5772 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -265,7 +265,7 @@ namespace osu.Game.Screens.Multi public override bool OnBackButton() { - if ((screenStack.CurrentScreen as IMultiplayerSubScreen).OnBackButton()) + if ((screenStack.CurrentScreen as IMultiplayerSubScreen)?.OnBackButton() == true) return true; if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) From e2c043737dbe4c95f8273488cfdac437474072fd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Jul 2020 13:08:57 +0900 Subject: [PATCH 2203/6909] Reword xmldoc to specify intended usage --- osu.Game/Screens/IOsuScreen.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 5f9f611a24..761f842c22 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -58,8 +58,12 @@ namespace osu.Game.Screens bool AllowRateAdjustments { get; } /// - /// Whether there are sub overlays/screens that need closing with the back button before this can be exited. + /// Invoked when the back button has been pressed to close any overlays before exiting this . /// + /// + /// Return true to block this from being exited after closing an overlay. + /// Return false if this should continue exiting. + /// bool OnBackButton(); } } From d8ebb8e3eb0385fb6046e78b41268ebf56c261c1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Jul 2020 13:17:22 +0900 Subject: [PATCH 2204/6909] Move override to a bit better location --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index c4a59b57cb..8a5e4d2683 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -404,6 +404,8 @@ namespace osu.Game.Overlays.Mods return base.OnKeyDown(e); } + public override bool OnPressed(GlobalAction action) => false; // handled by back button + private void availableModsChanged(ValueChangedEvent>> mods) { if (mods.NewValue == null) return; @@ -490,7 +492,5 @@ namespace osu.Game.Overlays.Mods } #endregion - - public override bool OnPressed(GlobalAction action) => false; // handled by back button } } From d1aedd15c4817bce96567768e33ae9e3606357bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Jul 2020 13:35:40 +0900 Subject: [PATCH 2205/6909] Add noto-thai font --- osu.Game/OsuGameBase.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index c79f710151..dd120937af 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -152,6 +152,7 @@ namespace osu.Game AddFont(Resources, @"Fonts/Noto-Hangul"); AddFont(Resources, @"Fonts/Noto-CJK-Basic"); AddFont(Resources, @"Fonts/Noto-CJK-Compatibility"); + AddFont(Resources, @"Fonts/Noto-Thai"); AddFont(Resources, @"Fonts/Venera-Light"); AddFont(Resources, @"Fonts/Venera-Bold"); From ec3fe8d34660ea5bc4d8cb3187f925c7423dfcb0 Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 14 Jul 2020 21:59:26 -0700 Subject: [PATCH 2206/6909] Add test for arrow key seeking --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 0be949650e..067fa5ae96 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Play.Break; using osu.Game.Screens.Ranking; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { @@ -34,6 +35,8 @@ 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("seek with right arrow key", () => press(Key.Right)); + AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); seekToBreak(0); seekToBreak(1); @@ -54,5 +57,11 @@ namespace osu.Game.Tests.Visual.Gameplay BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex); } + + private void press(Key key) + { + InputManager.PressKey(key); + InputManager.ReleaseKey(key); + } } } From 0043bd74bac5e8b0e68b7cb68b7d6ecd8be3e492 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 15 Jul 2020 08:27:46 +0300 Subject: [PATCH 2207/6909] Rework SpotlightSelector header layout --- .../Overlays/Rankings/SpotlightSelector.cs | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs index fbea53782b..f112c1ec43 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -85,30 +85,32 @@ namespace osu.Game.Overlays.Rankings Current = Current } }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Margin = new MarginPadding { Vertical = 5 }, - Children = new Drawable[] - { - startDateColumn = new InfoColumn(@"Start Date"), - endDateColumn = new InfoColumn(@"End Date"), - mapCountColumn = new InfoColumn(@"Map Count"), - participantsColumn = new InfoColumn(@"Participants") - } - }, new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Vertical = 20 }, - Child = new RankingsSortTabControl + Children = new Drawable[] { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Current = Sort + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Margin = new MarginPadding { Bottom = 5 }, + Children = new Drawable[] + { + startDateColumn = new InfoColumn(@"Start Date"), + endDateColumn = new InfoColumn(@"End Date"), + mapCountColumn = new InfoColumn(@"Map Count"), + participantsColumn = new InfoColumn(@"Participants") + } + }, + new RankingsSortTabControl + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Current = Sort + } } } } From 0125a7ef3b5dc15c3b49de6990b1b04bc5809bbe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jul 2020 15:36:17 +0900 Subject: [PATCH 2208/6909] Fix single-frame glitch in SS grade display animation --- .../Screens/Ranking/Expanded/Accuracy/RankText.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs index 8343716e7e..cc732382f4 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/RankText.cs @@ -77,11 +77,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy Origin = Anchor.Centre, BlurSigma = new Vector2(35), BypassAutoSizeAxes = Axes.Both, - RelativeSizeAxes = Axes.Both, + Size = new Vector2(200), CacheDrawnFrameBuffer = true, Blending = BlendingParameters.Additive, Alpha = 0, - Size = new Vector2(2f), // increase buffer size to allow for scale Scale = new Vector2(1.8f), Children = new[] { @@ -122,15 +121,18 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy } 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); + flash.FadeOutFromOne(3000); + superFlash.FadeOutFromOne(800, Easing.OutQuint); + } + else + { + flash.FadeOutFromOne(1200, Easing.OutQuint); } } } From fa407d2c7b5312d274334be3567a4f52feb95708 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jul 2020 16:42:37 +0900 Subject: [PATCH 2209/6909] Make tests better --- osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 067fa5ae96..4743317fdd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -35,8 +35,18 @@ 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)); + + double? time = null; + + AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime); + + // test seek via keyboard AddStep("seek with right arrow key", () => press(Key.Right)); - AddUntilStep("key counter counted keys", () => Player.HUDOverlay.KeyCounter.Children.Any(kc => kc.CountPresses > 2)); + AddAssert("time seeked forward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime > time + 2000); + + AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime); + AddStep("seek with left arrow key", () => press(Key.Left)); + AddAssert("time seeked backward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < time); seekToBreak(0); seekToBreak(1); From e95a1beaef5cda4fab4977612546f4211d04accd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Jul 2020 16:53:27 +0900 Subject: [PATCH 2210/6909] Update state after applying hitobject --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 44afb7a227..b633cb0860 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -147,8 +147,9 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable = HitObject.SamplesBindable.GetBoundCopy(); samplesBindable.CollectionChanged += (_, __) => loadSamples(); - updateState(ArmedState.Idle, true); apply(HitObject); + + updateState(ArmedState.Idle, true); } private void loadSamples() From f13bde68e665088021ee1db2c43565557b30b6e8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Jul 2020 16:53:36 +0900 Subject: [PATCH 2211/6909] Add test for catch hidden mod --- .../TestSceneCatchModHidden.cs | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs new file mode 100644 index 0000000000..f15da29993 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.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 System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Catch.Mods; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Catch.UI; +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 TestSceneCatchModHidden : ModTestScene + { + [BackgroundDependencyLoader] + private void load() + { + LocalConfig.Set(OsuSetting.IncreaseFirstObjectVisibility, false); + } + + [Test] + public void TestJuiceStream() + { + CreateModTest(new ModTestData + { + Beatmap = new Beatmap + { + HitObjects = new List + { + new JuiceStream + { + StartTime = 1000, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }), + X = CatchPlayfield.WIDTH / 2 + } + } + }, + Mod = new CatchModHidden(), + PassCondition = () => Player.Results.Count > 0 + && Player.ChildrenOfType().Single().Alpha > 0 + && Player.ChildrenOfType().Last().Alpha > 0 + }); + } + + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + } +} From b43b1673ccd2311aa2d75f89efb3dcaa5df872cc Mon Sep 17 00:00:00 2001 From: LastExceed Date: Wed, 15 Jul 2020 10:41:34 +0200 Subject: [PATCH 2212/6909] fix leftover parameter name --- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index af6fc24983..1c97638697 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -86,9 +86,9 @@ namespace osu.Game.Rulesets.Mania.Mods } [BackgroundDependencyLoader] - private void load(IScrollingInfo configManager) + private void load(IScrollingInfo scrollingInfo) { - scrollDirection.BindTo(configManager.Direction); + scrollDirection.BindTo(scrollingInfo.Direction); scrollDirection.BindValueChanged(onScrollDirectionChanged, true); } From e12f02a634a386bfe657659605ba47335d33ac51 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Wed, 15 Jul 2020 11:07:30 +0200 Subject: [PATCH 2213/6909] simplify reversing using rotation --- .../Mods/ManiaModHidden.cs | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 1c97638697..180341014d 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -44,7 +44,9 @@ namespace osu.Game.Rulesets.Mania.Mods new LaneCover { Coverage = 0.5f, - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre } } }); @@ -55,7 +57,6 @@ namespace osu.Game.Rulesets.Mania.Mods { private readonly Box gradient; private readonly Box filled; - private bool reversed; private readonly IBindable scrollDirection = new Bindable(); public LaneCover() @@ -76,11 +77,17 @@ namespace osu.Game.Rulesets.Mania.Mods { RelativeSizeAxes = Axes.Both, RelativePositionAxes = Axes.Both, - Height = 0.25f + Height = 0.25f, + Colour = ColourInfo.GradientVertical( + Color4.White.Opacity(0f), + Color4.White.Opacity(1f) + ) }, filled = new Box { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft } }; } @@ -92,37 +99,18 @@ namespace osu.Game.Rulesets.Mania.Mods scrollDirection.BindValueChanged(onScrollDirectionChanged, true); } - private void updateCoverage() - { - filled.Anchor = reversed ? Anchor.BottomLeft : Anchor.TopLeft; - filled.Origin = reversed ? Anchor.BottomLeft : Anchor.TopLeft; - filled.Height = coverage; - - gradient.Y = reversed ? 1 - filled.Height - gradient.Height : coverage; - gradient.Colour = ColourInfo.GradientVertical( - Color4.White.Opacity(reversed ? 0f : 1f), - Color4.White.Opacity(reversed ? 1f : 0f) - ); - } - private void onScrollDirectionChanged(ValueChangedEvent valueChangedEvent) { - reversed = valueChangedEvent.NewValue == ScrollingDirection.Up; - updateCoverage(); + bool isUpscroll = valueChangedEvent.NewValue == ScrollingDirection.Up; + Rotation = isUpscroll ? 180f : 0f; } - private float coverage; - public float Coverage { set { - if (coverage == value) - return; - - coverage = value; - - updateCoverage(); + filled.Height = value; + gradient.Y = 1 - filled.Height - gradient.Height; } } } From 4a2890c0540166c61e87fd456fa6951c8a45ac0b Mon Sep 17 00:00:00 2001 From: LastExceed Date: Wed, 15 Jul 2020 11:15:47 +0200 Subject: [PATCH 2214/6909] implement FI by flipping HD upside down --- osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs | 9 +++++++++ osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 12 +++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index bdc8cb31e5..f6a218b268 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -3,6 +3,9 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.UI; +using osuTK; namespace osu.Game.Rulesets.Mania.Mods { @@ -12,5 +15,11 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Acronym => "FI"; public override IconUsage? Icon => OsuIcon.ModHidden; public override string Description => @"Keys appear out of nowhere!"; + + public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + base.ApplyToDrawableRuleset(drawableRuleset); + laneCovers.ForEach(laneCover => laneCover.Scale = new Vector2(1f, -1f)); + } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 180341014d..8748e49ab1 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.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; @@ -24,8 +25,9 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Description => @"Keys fade out before you hit them!"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; + protected List laneCovers = new List(); - public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; @@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Mania.Mods HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; Container hocParent = (Container)hoc.Parent; + LaneCover laneCover; + hocParent.Remove(hoc); hocParent.Add(new BufferedContainer { @@ -41,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Mods Children = new Drawable[] { hoc, - new LaneCover + laneCover = new LaneCover { Coverage = 0.5f, RelativeSizeAxes = Axes.Both, @@ -50,10 +54,12 @@ namespace osu.Game.Rulesets.Mania.Mods } } }); + + laneCovers.Add(laneCover); } } - private class LaneCover : CompositeDrawable + protected class LaneCover : CompositeDrawable { private readonly Box gradient; private readonly Box filled; From d2e78d080c87025403db5c6fbcdcf8c1d7752f50 Mon Sep 17 00:00:00 2001 From: LastExceed Date: Wed, 15 Jul 2020 11:29:13 +0200 Subject: [PATCH 2215/6909] fix naming convention violation --- osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs | 2 +- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index f6a218b268..e29f2e2a00 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { base.ApplyToDrawableRuleset(drawableRuleset); - laneCovers.ForEach(laneCover => laneCover.Scale = new Vector2(1f, -1f)); + LaneCovers.ForEach(laneCover => laneCover.Scale = new Vector2(1f, -1f)); } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 8748e49ab1..3af6ff009f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Description => @"Keys fade out before you hit them!"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; - protected List laneCovers = new List(); + protected List LaneCovers = new List(); public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Mods } }); - laneCovers.Add(laneCover); + LaneCovers.Add(laneCover); } } From 8e4f85414573d6a788cdbb050bb1bc6f80fe18eb Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Wed, 15 Jul 2020 16:34:13 +0700 Subject: [PATCH 2216/6909] initial test hit lighting catch --- .../TestSceneHitLighting.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneHitLighting.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHitLighting.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHitLighting.cs new file mode 100644 index 0000000000..c5fa957130 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHitLighting.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +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.Configuration; +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; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public class TestSceneHitLighting : CatchSkinnableTestScene + { + private RulesetInfo catchRuleset; + private OsuConfigManager config; + + public TestSceneHitLighting() + { + AddToggleStep("toggle hit lighting", enabled => createCatcher(enabled)); + AddStep("catch fruit", () => catchFruit(new TestFruit() + { + X = this.ChildrenOfType().First().MovableCatcher.X + })); + } + + private void catchFruit(Fruit fruit) + { + 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 = HitResult.Great }); + + drawable.Expire(); + }); + }); + } + + private void createCatcher(bool hitLighting) + { + config.Set(OsuSetting.HitLighting, hitLighting); + SetContents(() => new CatchInputManager(catchRuleset) + { + RelativeSizeAxes = Axes.Both, + Child = new TestCatcherArea() + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation + }, + }); + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets, OsuConfigManager configManager) + { + catchRuleset = rulesets.GetRuleset(2); + config = configManager; + } + + public class TestFruit : Fruit + { + public TestFruit() + { + ApplyDefaultsToSelf(new ControlPointInfo(), new BeatmapDifficulty()); + } + } + + private class TestCatcherArea : CatcherArea + { + public TestCatcherArea() + : base(new BeatmapDifficulty()) + { + } + + public new Catcher MovableCatcher => base.MovableCatcher; + } + } +} From 19ab973bb99bdcfc0cb0ff25bc64fcf0875d417d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jul 2020 18:48:20 +0900 Subject: [PATCH 2217/6909] Add second layer to test scene --- .../UserInterface/TestSceneHueAnimation.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs index b341291c58..9c5888d072 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; namespace osu.Game.Tests.Visual.UserInterface @@ -15,6 +16,16 @@ namespace osu.Game.Tests.Visual.UserInterface [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { + HueAnimation anim2; + + Add(anim2 = new HueAnimation + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Texture = textures.Get("Intro/Triangles/logo-highlight"), + Colour = Colour4.White, + }); + HueAnimation anim; Add(anim = new HueAnimation @@ -22,10 +33,14 @@ namespace osu.Game.Tests.Visual.UserInterface RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, Texture = textures.Get("Intro/Triangles/logo-background"), - Colour = Colour4.White, + Colour = OsuColour.Gray(0.6f), }); - AddSliderStep("Progress", 0f, 1f, 0f, newValue => anim.AnimationProgress = newValue); + AddSliderStep("Progress", 0f, 1f, 0f, newValue => + { + anim2.AnimationProgress = newValue; + anim.AnimationProgress = newValue; + }); } } } From 675544ec2f63d06994659eb259fc39dac4e108d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jul 2020 19:03:59 +0900 Subject: [PATCH 2218/6909] Tidy up colour and variable usage --- osu.Game/Screens/Menu/IntroTriangles.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 38da98220d..5c8e7049f0 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -283,21 +283,18 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load(TextureStore textures) { - const string lazer_logo_background = @"Intro/Triangles/logo-background"; - const string lazer_logo_highlight = @"Intro/Triangles/logo-highlight"; - InternalChildren = new Drawable[] { highlight = new HueAnimation { RelativeSizeAxes = Axes.Both, - Texture = textures.Get(lazer_logo_highlight), - Colour = OsuColour.Gray(1f), + Texture = textures.Get(@"Intro/Triangles/logo-highlight"), + Colour = Color4.White, }, background = new HueAnimation { RelativeSizeAxes = Axes.Both, - Texture = textures.Get(lazer_logo_background), + Texture = textures.Get(@"Intro/Triangles/logo-background"), Colour = OsuColour.Gray(0.6f), }, }; From 53a46400f50a47b3d5399ffb0444ffc84ffbbe52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jul 2020 19:06:40 +0900 Subject: [PATCH 2219/6909] 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 85d154f2e2..71d4e5aacf 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 b8e73262c4..2f3d08c528 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 1faf60b1d9..2bb3914c25 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 94834e4920200417848944b318f58e5e46c1f043 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Jul 2020 19:35:52 +0900 Subject: [PATCH 2220/6909] Select mods via exact types --- osu.Game/Overlays/Mods/ModSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 7235a18a23..3701f9ecab 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -132,7 +132,7 @@ namespace osu.Game.Overlays.Mods { foreach (var button in buttons) { - int i = Array.FindIndex(button.Mods, m => modTypes.Any(t => t.IsInstanceOfType(m))); + int i = Array.FindIndex(button.Mods, m => modTypes.Any(t => t == m.GetType())); if (i >= 0) button.SelectAt(i); From 87f7d8744de0bd3219e728f013942616c2138aa1 Mon Sep 17 00:00:00 2001 From: jorolf Date: Wed, 15 Jul 2020 12:40:46 +0200 Subject: [PATCH 2221/6909] simplify transform --- osu.Game/Screens/Menu/IntroTriangles.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 5c8e7049f0..b56ba6c8a4 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -217,8 +217,7 @@ namespace osu.Game.Screens.Menu // matching flyte curve y = 0.25x^2 + (max(0, x - 0.7) / 0.3) ^ 5 lazerLogo.FadeIn().ScaleTo(scale_start).Then().Delay(logo_scale_duration * 0.7f).ScaleTo(scale_start - scale_adjust, logo_scale_duration * 0.3f, Easing.InQuint); - lazerLogo.TransformTo(nameof(LazerLogo.Background), 1f, logo_scale_duration); - lazerLogo.TransformTo(nameof(LazerLogo.Highlight), 1f, logo_scale_duration); + lazerLogo.TransformTo(nameof(LazerLogo.Progress), 1f, logo_scale_duration); logoContainerSecondary.ScaleTo(scale_start).Then().ScaleTo(scale_start - scale_adjust * 0.25f, logo_scale_duration, Easing.InQuad); } @@ -263,16 +262,14 @@ namespace osu.Game.Screens.Menu { private HueAnimation highlight, background; - public float Highlight - { - get => highlight.AnimationProgress; - set => highlight.AnimationProgress = value; - } - - public float Background + public float Progress { get => background.AnimationProgress; - set => background.AnimationProgress = value; + set + { + background.AnimationProgress = value; + highlight.AnimationProgress = value; + } } public LazerLogo() From 1a6ae3194e795bf011b903ff41f4e994744ad691 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 15 Jul 2020 19:45:48 +0900 Subject: [PATCH 2222/6909] Add test --- .../Mods/ManiaModFadeIn.cs | 9 +-------- .../TestSceneModSelectOverlay.cs | 20 +++++++++++++++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index 4c125ad6ef..bdc8cb31e5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.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; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModFadeIn : Mod + public class ManiaModFadeIn : ManiaModHidden { public override string Name => "Fade In"; public override string Acronym => "FI"; public override IconUsage? Icon => OsuIcon.ModHidden; - public override ModType Type => ModType.DifficultyIncrease; public override string Description => @"Keys appear out of nowhere!"; - public override double ScoreMultiplier => 1; - public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; } } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index ce691bff70..6f083f4ab6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -13,6 +13,8 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; 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; @@ -99,6 +101,12 @@ namespace osu.Game.Tests.Visual.UserInterface public void TestManiaMods() { changeRuleset(3); + + var mania = new ManiaRuleset(); + + testModsWithSameBaseType( + mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModFadeIn)), + mania.GetAllMods().Single(m => m.GetType() == typeof(ManiaModHidden))); } [Test] @@ -197,6 +205,18 @@ namespace osu.Game.Tests.Visual.UserInterface checkLabelColor(() => Color4.White); } + private void testModsWithSameBaseType(Mod modA, Mod modB) + { + selectNext(modA); + checkSelected(modA); + selectNext(modB); + checkSelected(modB); + + // Backwards + selectPrevious(modA); + checkSelected(modA); + } + 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)); From 2624862e32c6310e16a400bd9ad97a96a72bf0ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jul 2020 20:58:09 +0900 Subject: [PATCH 2223/6909] Fix osu!catch dropping fruit appearing above the plate instead of behind --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 29 +++++++++++--------- osu.Game.Rulesets.Catch/UI/Catcher.cs | 26 +++++++++--------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 4 +-- 3 files changed, 31 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index d034f3c7d4..63751ecb0d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -35,22 +35,25 @@ namespace osu.Game.Rulesets.Catch.UI public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation) { - Container explodingFruitContainer; + var explodingFruitContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }; + + CatcherArea = new CatcherArea(difficulty) + { + CreateDrawableRepresentation = createDrawableRepresentation, + ExplodingFruitTarget = explodingFruitContainer, + Anchor = Anchor.BottomLeft, + Origin = Anchor.TopLeft, + }; InternalChildren = new Drawable[] { - explodingFruitContainer = new Container - { - RelativeSizeAxes = Axes.Both, - }, - CatcherArea = new CatcherArea(difficulty) - { - CreateDrawableRepresentation = createDrawableRepresentation, - ExplodingFruitTarget = explodingFruitContainer, - Anchor = Anchor.BottomLeft, - Origin = Anchor.TopLeft, - }, - HitObjectContainer + explodingFruitContainer, + CatcherArea.MovableCatcher.CaughtFruitContainer.CreateProxy(), + HitObjectContainer, + CatcherArea }; } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 82cbbefcca..fd7a1fd3c3 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -46,6 +46,12 @@ namespace osu.Game.Rulesets.Catch.UI public Container ExplodingFruitTarget; + public Container CaughtFruitContainer { get; } = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }; + [NotNull] private readonly Container trailsTarget; @@ -83,8 +89,6 @@ namespace osu.Game.Rulesets.Catch.UI /// private readonly float catchWidth; - private Container caughtFruit; - private CatcherSprite catcherIdle; private CatcherSprite catcherKiai; private CatcherSprite catcherFail; @@ -118,11 +122,7 @@ namespace osu.Game.Rulesets.Catch.UI { InternalChildren = new Drawable[] { - caughtFruit = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - }, + CaughtFruitContainer, catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) { Anchor = Anchor.TopCentre, @@ -176,7 +176,7 @@ namespace osu.Game.Rulesets.Catch.UI const float allowance = 10; - while (caughtFruit.Any(f => + while (CaughtFruitContainer.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) { @@ -187,7 +187,7 @@ namespace osu.Game.Rulesets.Catch.UI fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); - caughtFruit.Add(fruit); + CaughtFruitContainer.Add(fruit); AddInternal(new HitExplosion(fruit) { @@ -342,7 +342,7 @@ namespace osu.Game.Rulesets.Catch.UI /// public void Drop() { - foreach (var f in caughtFruit.ToArray()) + foreach (var f in CaughtFruitContainer.ToArray()) Drop(f); } @@ -351,7 +351,7 @@ namespace osu.Game.Rulesets.Catch.UI /// public void Explode() { - foreach (var f in caughtFruit.ToArray()) + foreach (var f in CaughtFruitContainer.ToArray()) Explode(f); } @@ -450,9 +450,9 @@ namespace osu.Game.Rulesets.Catch.UI if (ExplodingFruitTarget != null) { fruit.Anchor = Anchor.TopLeft; - fruit.Position = caughtFruit.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); + fruit.Position = CaughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); - if (!caughtFruit.Remove(fruit)) + if (!CaughtFruitContainer.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; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index bf1ac5bc0e..4255c3b1af 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.UI public Func> CreateDrawableRepresentation; + public readonly Catcher MovableCatcher; + public Container ExplodingFruitTarget { set => MovableCatcher.ExplodingFruitTarget = value; @@ -104,7 +106,5 @@ namespace osu.Game.Rulesets.Catch.UI if (state?.CatcherX != null) MovableCatcher.X = state.CatcherX.Value; } - - protected internal readonly Catcher MovableCatcher; } } From 72789dc0aa56b5e47dbb081698349b07f77dca4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jul 2020 21:52:37 +0900 Subject: [PATCH 2224/6909] Remove redundant array spec --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 63751ecb0d..a29d167c5b 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.UI Origin = Anchor.TopLeft, }; - InternalChildren = new Drawable[] + InternalChildren = new [] { explodingFruitContainer, CatcherArea.MovableCatcher.CaughtFruitContainer.CreateProxy(), From 3666599053aa3a2f7dbf7544326adcf89b6f2ec8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 15 Jul 2020 22:00:48 +0900 Subject: [PATCH 2225/6909] Remove space --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index a29d167c5b..18dc3adf76 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.UI Origin = Anchor.TopLeft, }; - InternalChildren = new [] + InternalChildren = new[] { explodingFruitContainer, CatcherArea.MovableCatcher.CaughtFruitContainer.CreateProxy(), From dcd345eed95228cfa729483e657290b4a059f7bc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 01:20:32 +0900 Subject: [PATCH 2226/6909] Add a few tests --- .../TaikoBeatmapConversionTest.cs | 2 + ...er-conversion-v14-expected-conversion.json | 379 ++++++++++++++++++ .../Beatmaps/slider-conversion-v14.osu | 32 ++ ...der-conversion-v6-expected-conversion.json | 137 +++++++ .../Testing/Beatmaps/slider-conversion-v6.osu | 20 + 5 files changed, 570 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json create mode 100644 osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14.osu create mode 100644 osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6-expected-conversion.json create mode 100644 osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6.osu diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index f7729138ff..d0c57b20c0 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Taiko.Tests [TestCase("basic")] [TestCase("slider-generating-drumroll")] [TestCase("sample-to-type-conversions")] + [TestCase("slider-conversion-v6")] + [TestCase("slider-conversion-v14")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json new file mode 100644 index 0000000000..6a6063cb74 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json @@ -0,0 +1,379 @@ +{ + "Mappings": [{ + "StartTime": 2000, + "Objects": [{ + "StartTime": 2000, + "EndTime": 2000, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + }, + { + "StartTime": 2173, + "EndTime": 2173, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 4000, + "Objects": [{ + "StartTime": 4000, + "EndTime": 4000, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 4173, + "EndTime": 4173, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 6000, + "Objects": [{ + "StartTime": 6000, + "EndTime": 6000, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 6271, + "EndTime": 6271, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 6542, + "EndTime": 6542, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + }, + { + "StartTime": 8000, + "Objects": [{ + "StartTime": 8000, + "EndTime": 8000, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8026, + "EndTime": 8026, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8053, + "EndTime": 8053, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8080, + "EndTime": 8080, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8107, + "EndTime": 8107, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8133, + "EndTime": 8133, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8160, + "EndTime": 8160, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8187, + "EndTime": 8187, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8214, + "EndTime": 8214, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8241, + "EndTime": 8241, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8267, + "EndTime": 8267, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8294, + "EndTime": 8294, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8321, + "EndTime": 8321, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8348, + "EndTime": 8348, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8374, + "EndTime": 8374, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8401, + "EndTime": 8401, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8428, + "EndTime": 8428, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8455, + "EndTime": 8455, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8482, + "EndTime": 8482, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8508, + "EndTime": 8508, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8535, + "EndTime": 8535, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8562, + "EndTime": 8562, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8589, + "EndTime": 8589, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8615, + "EndTime": 8615, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8642, + "EndTime": 8642, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8669, + "EndTime": 8669, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8696, + "EndTime": 8696, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8723, + "EndTime": 8723, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8749, + "EndTime": 8749, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8776, + "EndTime": 8776, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8803, + "EndTime": 8803, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8830, + "EndTime": 8830, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 8857, + "EndTime": 8857, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14.osu new file mode 100644 index 0000000000..4c8fb1fde6 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14.osu @@ -0,0 +1,32 @@ +osu file format v14 + +[General] +Mode: 0 + +[Difficulty] +HPDrainRate:7 +CircleSize:4 +OverallDifficulty:8 +ApproachRate:9.2 +SliderMultiplier:2.3 +SliderTickRate:1 + +[TimingPoints] +0,333.333333333333,4,1,0,50,1,0 +2000,-100,4,2,0,80,0,0 + +6000,389.61038961039,4,2,1,60,1,0 + +8000,428.571428571429,4,3,1,65,1,0 +8000,-133.333333333333,4,1,1,45,0,0 + +[HitObjects] +// Should convert. +48,32,2000,6,0,B|168:32,1,120,4|2 +312,68,4000,2,0,B|288:52|256:44|216:52|200:68,1,120,0|8 + +// Should convert. +184,224,6000,2,0,L|336:308,2,160,2|2|0,0:0|0:0|0:0,0:0:0:0: + +// Should convert. +328,36,8000,6,0,L|332:16,32,10.7812504112721,0|0,0:0,0:0:0:0: diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6-expected-conversion.json new file mode 100644 index 0000000000..c3d3c52ebd --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6-expected-conversion.json @@ -0,0 +1,137 @@ +{ + "Mappings": [{ + "StartTime": 0, + "Objects": [{ + "StartTime": 0, + "EndTime": 0, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + }, + { + "StartTime": 162, + "EndTime": 162, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 325, + "EndTime": 325, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + }, + { + "StartTime": 487, + "EndTime": 487, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 650, + "EndTime": 650, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + }, + { + "StartTime": 813, + "EndTime": 813, + "IsRim": false, + "IsCentre": true, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 975, + "EndTime": 975, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + } + ] + }, + { + "StartTime": 2000, + "Objects": [{ + "StartTime": 2000, + "EndTime": 2000, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 2162, + "EndTime": 2162, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 2325, + "EndTime": 2325, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + }, + { + "StartTime": 2487, + "EndTime": 2487, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 2650, + "EndTime": 2650, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + }, + { + "StartTime": 2813, + "EndTime": 2813, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": true + }, + { + "StartTime": 2975, + "EndTime": 2975, + "IsRim": true, + "IsCentre": false, + "IsDrumRoll": false, + "IsSwell": false, + "IsStrong": false + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6.osu new file mode 100644 index 0000000000..c1e4c3bbd7 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v6.osu @@ -0,0 +1,20 @@ +osu file format v6 + +[General] +Mode: 0 + +[Difficulty] +HPDrainRate:3 +CircleSize:4 +OverallDifficulty:1 +SliderMultiplier:1.2 +SliderTickRate:3 + +[TimingPoints] +0,487.884208814441,4,1,0,60,1,0 +2000,-100,4,1,0,65,0,1 + +[HitObjects] +// Should convert. +376,64,0,6,0,B|256:32|136:64,1,240,6|0 +256,120,2000,6,8,C|264:192|336:192,2,120,8|8|6 \ No newline at end of file From 6b2b3f4d4d12276ac0b7ebcc62e92a586502e301 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 14:10:44 +0900 Subject: [PATCH 2227/6909] Expose accuracy/combo portion adjustments --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 28 +++++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index eb49638d59..f1cdfd93c8 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -16,8 +17,6 @@ namespace osu.Game.Rulesets.Scoring { public class ScoreProcessor : JudgementProcessor { - private const double base_portion = 0.3; - private const double combo_portion = 0.7; private const double max_score = 1000000; /// @@ -55,8 +54,20 @@ namespace osu.Game.Rulesets.Scoring /// public readonly Bindable Mode = new Bindable(); - private double maxHighestCombo; + /// + /// The default portion of awarded for hitting s accurately. Defaults to 30%. + /// + protected virtual double DefaultAccuracyPortion => 0.3; + /// + /// The default portion of awarded for achieving a high combo. Default to 70%. + /// + protected virtual double DefaultComboPortion => 0.7; + + private readonly double accuracyPortion; + private readonly double comboPortion; + + private double maxHighestCombo; private double maxBaseScore; private double rollingMaxBaseScore; private double baseScore; @@ -69,7 +80,11 @@ namespace osu.Game.Rulesets.Scoring public ScoreProcessor() { - Debug.Assert(base_portion + combo_portion == 1.0); + accuracyPortion = DefaultAccuracyPortion; + comboPortion = DefaultComboPortion; + + if (!Precision.AlmostEquals(1.0, accuracyPortion + comboPortion)) + throw new InvalidOperationException($"{nameof(DefaultAccuracyPortion)} + {nameof(DefaultComboPortion)} must equal 1."); Combo.ValueChanged += combo => HighestCombo.Value = Math.Max(HighestCombo.Value, combo.NewValue); Accuracy.ValueChanged += accuracy => @@ -189,7 +204,10 @@ namespace osu.Game.Rulesets.Scoring { default: case ScoringMode.Standardised: - return (max_score * (base_portion * baseScore / maxBaseScore + combo_portion * HighestCombo.Value / maxHighestCombo) + bonusScore) * scoreMultiplier; + double accuracyScore = accuracyPortion * baseScore / maxBaseScore; + double comboScore = comboPortion * HighestCombo.Value / maxHighestCombo; + + return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; case ScoringMode.Classic: // should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1) From 2b39857b8cec4b4a1d7777a7987bac9813f50d26 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 14:10:52 +0900 Subject: [PATCH 2228/6909] Make mania 80% acc 20% combo --- osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 9b54b48de3..ba84c21845 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Mania.Scoring { internal class ManiaScoreProcessor : ScoreProcessor { + protected override double DefaultAccuracyPortion => 0.8; + + protected override double DefaultComboPortion => 0.2; + public override HitWindows CreateHitWindows() => new ManiaHitWindows(); } } From 35d6501478411b35002af85a22a0ed85ee4720af Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 14:13:46 +0900 Subject: [PATCH 2229/6909] Also adjust taiko --- osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index 003d40af56..e29ea87d25 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -7,6 +7,10 @@ namespace osu.Game.Rulesets.Taiko.Scoring { internal class TaikoScoreProcessor : ScoreProcessor { + protected override double DefaultAccuracyPortion => 0.75; + + protected override double DefaultComboPortion => 0.25; + public override HitWindows CreateHitWindows() => new TaikoHitWindows(); } } From 0a1e6a82739101f3c2db1b6b8a859cfe3dcddec7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jul 2020 14:25:45 +0900 Subject: [PATCH 2230/6909] Fix storyboard video playback when not starting at beginning of beatmap --- osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs index a85936edf7..4ea582ca4a 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardVideo.cs @@ -55,10 +55,11 @@ namespace osu.Game.Storyboards.Drawables if (video == null) return; - video.PlaybackPosition = Clock.CurrentTime - Video.StartTime; - - using (video.BeginAbsoluteSequence(0)) + using (video.BeginAbsoluteSequence(Video.StartTime)) + { + Schedule(() => video.PlaybackPosition = Time.Current - Video.StartTime); video.FadeIn(500); + } } } } From 87713215dcae650a2209bcb5f9e5dd85eadc7c6f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 14:30:17 +0900 Subject: [PATCH 2231/6909] Remove redundant parens --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs index 99975d9174..2c1885ae1a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double addition = 1; // We get an extra addition if we are not a slider or spinner - if (current.LastObject is Hit && current.BaseObject is Hit && (current.BaseObject.StartTime - current.LastObject.StartTime) < 1000) + if (current.LastObject is Hit && current.BaseObject is Hit && current.BaseObject.StartTime - current.LastObject.StartTime < 1000) { if (hasColourChange(current)) addition += 0.75; From 96724bde32d07c286fdbd81717afccd7bc531194 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 15:05:01 +0900 Subject: [PATCH 2232/6909] Fix chat name backgrounds not dimming --- osu.Game/Overlays/Chat/ChatLine.cs | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 496986dc56..4eb348ae33 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -114,21 +115,26 @@ namespace osu.Game.Overlays.Chat Colour = Color4.Black.Opacity(0.3f), Type = EdgeEffectType.Shadow, }, - // Drop shadow effect Child = new Container { AutoSizeAxes = Axes.Both, + Y = 3, Masking = true, CornerRadius = 4, - EdgeEffect = new EdgeEffectParameters + Children = new Drawable[] { - Radius = 1, - Colour = Color4Extensions.FromHex(message.Sender.Colour), - Type = EdgeEffectType.Shadow, - }, - Padding = new MarginPadding { Left = 3, Right = 3, Bottom = 1, Top = -3 }, - Y = 3, - Child = username, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(message.Sender.Colour), + }, + new Container + { + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 4, Right = 4, Bottom = 1, Top = -2 }, + Child = username + } + } } }; } From c42b315abb65e1162dd97eb6f53d69b2182fbb09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 16 Jul 2020 15:35:19 +0900 Subject: [PATCH 2233/6909] Expose via CreateProxiedContent method --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 22 +++++++++++++------- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 18dc3adf76..154e1576db 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.UI InternalChildren = new[] { explodingFruitContainer, - CatcherArea.MovableCatcher.CaughtFruitContainer.CreateProxy(), + CatcherArea.MovableCatcher.CreateProxiedContent(), HitObjectContainer, CatcherArea }; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index fd7a1fd3c3..8629a19470 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Catch.UI public Container ExplodingFruitTarget; - public Container CaughtFruitContainer { get; } = new Container + private Container caughtFruitContainer { get; } = new Container { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Catch.UI { InternalChildren = new Drawable[] { - CaughtFruitContainer, + caughtFruitContainer, catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) { Anchor = Anchor.TopCentre, @@ -145,6 +145,12 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } + /// + /// Creates proxied content to be displayed beneath hitobjects. + /// + /// + public Drawable CreateProxiedContent() => caughtFruitContainer.CreateProxy(); + /// /// Calculates the scale of the catcher based off the provided beatmap difficulty. /// @@ -176,7 +182,7 @@ namespace osu.Game.Rulesets.Catch.UI const float allowance = 10; - while (CaughtFruitContainer.Any(f => + while (caughtFruitContainer.Any(f => f.LifetimeEnd == double.MaxValue && Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) { @@ -187,7 +193,7 @@ namespace osu.Game.Rulesets.Catch.UI fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); - CaughtFruitContainer.Add(fruit); + caughtFruitContainer.Add(fruit); AddInternal(new HitExplosion(fruit) { @@ -342,7 +348,7 @@ namespace osu.Game.Rulesets.Catch.UI /// public void Drop() { - foreach (var f in CaughtFruitContainer.ToArray()) + foreach (var f in caughtFruitContainer.ToArray()) Drop(f); } @@ -351,7 +357,7 @@ namespace osu.Game.Rulesets.Catch.UI /// public void Explode() { - foreach (var f in CaughtFruitContainer.ToArray()) + foreach (var f in caughtFruitContainer.ToArray()) Explode(f); } @@ -450,9 +456,9 @@ namespace osu.Game.Rulesets.Catch.UI if (ExplodingFruitTarget != null) { fruit.Anchor = Anchor.TopLeft; - fruit.Position = CaughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); + fruit.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); - if (!CaughtFruitContainer.Remove(fruit)) + if (!caughtFruitContainer.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; From ab477c3be4289ae460824caaad1912651411562f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 16 Jul 2020 15:55:35 +0900 Subject: [PATCH 2234/6909] Remove returns xmldoc --- 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 8629a19470..b41fd24a9c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -148,7 +148,6 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Creates proxied content to be displayed beneath hitobjects. /// - /// public Drawable CreateProxiedContent() => caughtFruitContainer.CreateProxy(); /// From 16d429d2b6b5d62ddba792786376cd03ca5fef55 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Thu, 16 Jul 2020 15:24:03 +0700 Subject: [PATCH 2235/6909] Delete test hitlighting --- .../TestSceneHitLighting.cs | 96 ------------------- 1 file changed, 96 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneHitLighting.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHitLighting.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHitLighting.cs deleted file mode 100644 index c5fa957130..0000000000 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHitLighting.cs +++ /dev/null @@ -1,96 +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.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics; -using osu.Framework.Testing; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Configuration; -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; - -namespace osu.Game.Rulesets.Catch.Tests -{ - [TestFixture] - public class TestSceneHitLighting : CatchSkinnableTestScene - { - private RulesetInfo catchRuleset; - private OsuConfigManager config; - - public TestSceneHitLighting() - { - AddToggleStep("toggle hit lighting", enabled => createCatcher(enabled)); - AddStep("catch fruit", () => catchFruit(new TestFruit() - { - X = this.ChildrenOfType().First().MovableCatcher.X - })); - } - - private void catchFruit(Fruit fruit) - { - 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 = HitResult.Great }); - - drawable.Expire(); - }); - }); - } - - private void createCatcher(bool hitLighting) - { - config.Set(OsuSetting.HitLighting, hitLighting); - SetContents(() => new CatchInputManager(catchRuleset) - { - RelativeSizeAxes = Axes.Both, - Child = new TestCatcherArea() - { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation - }, - }); - } - - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets, OsuConfigManager configManager) - { - catchRuleset = rulesets.GetRuleset(2); - config = configManager; - } - - public class TestFruit : Fruit - { - public TestFruit() - { - ApplyDefaultsToSelf(new ControlPointInfo(), new BeatmapDifficulty()); - } - } - - private class TestCatcherArea : CatcherArea - { - public TestCatcherArea() - : base(new BeatmapDifficulty()) - { - } - - public new Catcher MovableCatcher => base.MovableCatcher; - } - } -} From 7021459c752fe32a07fe3053474d56464877072a Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Thu, 16 Jul 2020 15:25:07 +0700 Subject: [PATCH 2236/6909] add hit lighting test in test scene catcher area --- .../TestSceneCatcherArea.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index fbb22a8498..6d6f0357c2 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Rulesets.Catch.Beatmaps; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; @@ -24,6 +25,7 @@ namespace osu.Game.Rulesets.Catch.Tests public class TestSceneCatcherArea : CatchSkinnableTestScene { private RulesetInfo catchRuleset; + private OsuConfigManager config; public TestSceneCatcherArea() { @@ -52,6 +54,24 @@ namespace osu.Game.Rulesets.Catch.Tests }, true), 20); } + [TestCase(true)] + [TestCase(false)] + public void TestHitLighting(bool enable) { + Catcher catcher = this.ChildrenOfType().First().MovableCatcher; + + AddStep("Toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable)); + AddStep("Catch fruit", () => catchFruit(new TestFruit(false) + { + X = catcher.X + })); + AddStep("Catch fruit last combo", () => catchFruit(new TestFruit(false) + { + X = catcher.X, + LastInCombo = true + })); + AddAssert("Check hit explotion", () => catcher.ChildrenOfType().Any() == enable); + } + private void catchFruit(Fruit fruit, bool miss = false) { this.ChildrenOfType().ForEach(area => @@ -84,9 +104,10 @@ namespace osu.Game.Rulesets.Catch.Tests } [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) + private void load(RulesetStore rulesets, OsuConfigManager configManager) { catchRuleset = rulesets.GetRuleset(2); + config = configManager; } public class TestFruit : Fruit From 1384e61747065dd777dc6fd8a721c1082b6e92db Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 16:34:16 +0900 Subject: [PATCH 2237/6909] Move cover to a separate file, rename --- .../Mods/ManiaModHidden.cs | 77 +------------------ osu.Game.Rulesets.Mania/UI/PlayfieldCover.cs | 77 +++++++++++++++++++ 2 files changed, 81 insertions(+), 73 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/PlayfieldCover.cs diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 3af6ff009f..83d252b2f6 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -4,19 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; -using osu.Game.Rulesets.UI.Scrolling; -using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Mods { @@ -25,7 +18,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Description => @"Keys fade out before you hit them!"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; - protected List LaneCovers = new List(); + protected List PlayfieldCovers = new List(); public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { @@ -36,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Mods HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; Container hocParent = (Container)hoc.Parent; - LaneCover laneCover; + PlayfieldCover laneCover; hocParent.Remove(hoc); hocParent.Add(new BufferedContainer @@ -45,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Mods Children = new Drawable[] { hoc, - laneCover = new LaneCover + laneCover = new PlayfieldCover { Coverage = 0.5f, RelativeSizeAxes = Axes.Both, @@ -55,69 +48,7 @@ namespace osu.Game.Rulesets.Mania.Mods } }); - LaneCovers.Add(laneCover); - } - } - - protected class LaneCover : CompositeDrawable - { - private readonly Box gradient; - private readonly Box filled; - private readonly IBindable scrollDirection = new Bindable(); - - public LaneCover() - { - Blending = new BlendingParameters - { - RGBEquation = BlendingEquation.Add, - Source = BlendingType.Zero, - Destination = BlendingType.One, - AlphaEquation = BlendingEquation.Add, - SourceAlpha = BlendingType.Zero, - DestinationAlpha = BlendingType.OneMinusSrcAlpha - }; - - InternalChildren = new Drawable[] - { - gradient = new Box - { - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, - Height = 0.25f, - Colour = ColourInfo.GradientVertical( - Color4.White.Opacity(0f), - Color4.White.Opacity(1f) - ) - }, - filled = new Box - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft - } - }; - } - - [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo) - { - scrollDirection.BindTo(scrollingInfo.Direction); - scrollDirection.BindValueChanged(onScrollDirectionChanged, true); - } - - private void onScrollDirectionChanged(ValueChangedEvent valueChangedEvent) - { - bool isUpscroll = valueChangedEvent.NewValue == ScrollingDirection.Up; - Rotation = isUpscroll ? 180f : 0f; - } - - public float Coverage - { - set - { - filled.Height = value; - gradient.Y = 1 - filled.Height - gradient.Height; - } + PlayfieldCovers.Add(laneCover); } } } diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCover.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCover.cs new file mode 100644 index 0000000000..27fb23e3f2 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCover.cs @@ -0,0 +1,77 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using 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.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class PlayfieldCover : CompositeDrawable + { + private readonly Box gradient; + private readonly Box filled; + private readonly IBindable scrollDirection = new Bindable(); + + public PlayfieldCover() + { + Blending = new BlendingParameters + { + RGBEquation = BlendingEquation.Add, + Source = BlendingType.Zero, + Destination = BlendingType.One, + AlphaEquation = BlendingEquation.Add, + SourceAlpha = BlendingType.Zero, + DestinationAlpha = BlendingType.OneMinusSrcAlpha + }; + + InternalChildren = new Drawable[] + { + gradient = new Box + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Height = 0.25f, + Colour = ColourInfo.GradientVertical( + Color4.White.Opacity(0f), + Color4.White.Opacity(1f) + ) + }, + filled = new Box + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft + } + }; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + scrollDirection.BindTo(scrollingInfo.Direction); + scrollDirection.BindValueChanged(onScrollDirectionChanged, true); + } + + private void onScrollDirectionChanged(ValueChangedEvent valueChangedEvent) + { + bool isUpscroll = valueChangedEvent.NewValue == ScrollingDirection.Up; + Rotation = isUpscroll ? 180f : 0f; + } + + public float Coverage + { + set + { + filled.Height = value; + gradient.Y = 1 - filled.Height - gradient.Height; + } + } + } +} From b7f6ae5db9b6f1081df886110605469b9f06749c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 17:26:18 +0900 Subject: [PATCH 2238/6909] Make the cover into a container --- .../Mods/ManiaModFadeIn.cs | 10 +- .../Mods/ManiaModHidden.cs | 34 +++-- osu.Game.Rulesets.Mania/UI/PlayfieldCover.cs | 77 ------------ .../UI/PlayfieldCoveringContainer.cs | 116 ++++++++++++++++++ 4 files changed, 133 insertions(+), 104 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/UI/PlayfieldCover.cs create mode 100644 osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index e29f2e2a00..5c643a7d37 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -3,9 +3,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.UI; -using osuTK; +using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Mods { @@ -16,10 +14,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override IconUsage? Icon => OsuIcon.ModHidden; public override string Description => @"Keys appear out of nowhere!"; - public override void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) - { - base.ApplyToDrawableRuleset(drawableRuleset); - LaneCovers.ForEach(laneCover => laneCover.Scale = new Vector2(1f, -1f)); - } + protected override PlayfieldCoveringContainer CreateCover() => new PlayfieldCoveringContainer(); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 83d252b2f6..023de8fe25 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.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.Graphics; using osu.Framework.Graphics.Containers; @@ -10,6 +9,7 @@ using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; +using osuTK; namespace osu.Game.Rulesets.Mania.Mods { @@ -18,7 +18,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Description => @"Keys fade out before you hit them!"; public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; - protected List PlayfieldCovers = new List(); public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { @@ -29,26 +28,23 @@ namespace osu.Game.Rulesets.Mania.Mods HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; Container hocParent = (Container)hoc.Parent; - PlayfieldCover laneCover; - hocParent.Remove(hoc); - hocParent.Add(new BufferedContainer + hocParent.Add(CreateCover().With(c => { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - hoc, - laneCover = new PlayfieldCover - { - Coverage = 0.5f, - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre - } - } - }); + c.RelativeSizeAxes = Axes.Both; + c.Coverage = 0.5f; + c.Child = hoc; + })); + } + } - PlayfieldCovers.Add(laneCover); + protected virtual PlayfieldCoveringContainer CreateCover() => new ModHiddenCoveringContainer(); + + private class ModHiddenCoveringContainer : PlayfieldCoveringContainer + { + public ModHiddenCoveringContainer() + { + Cover.Scale = new Vector2(1, -1); } } } diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCover.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCover.cs deleted file mode 100644 index 27fb23e3f2..0000000000 --- a/osu.Game.Rulesets.Mania/UI/PlayfieldCover.cs +++ /dev/null @@ -1,77 +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.Shapes; -using osu.Game.Rulesets.UI.Scrolling; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Mania.UI -{ - public class PlayfieldCover : CompositeDrawable - { - private readonly Box gradient; - private readonly Box filled; - private readonly IBindable scrollDirection = new Bindable(); - - public PlayfieldCover() - { - Blending = new BlendingParameters - { - RGBEquation = BlendingEquation.Add, - Source = BlendingType.Zero, - Destination = BlendingType.One, - AlphaEquation = BlendingEquation.Add, - SourceAlpha = BlendingType.Zero, - DestinationAlpha = BlendingType.OneMinusSrcAlpha - }; - - InternalChildren = new Drawable[] - { - gradient = new Box - { - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Both, - Height = 0.25f, - Colour = ColourInfo.GradientVertical( - Color4.White.Opacity(0f), - Color4.White.Opacity(1f) - ) - }, - filled = new Box - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft - } - }; - } - - [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo) - { - scrollDirection.BindTo(scrollingInfo.Direction); - scrollDirection.BindValueChanged(onScrollDirectionChanged, true); - } - - private void onScrollDirectionChanged(ValueChangedEvent valueChangedEvent) - { - bool isUpscroll = valueChangedEvent.NewValue == ScrollingDirection.Up; - Rotation = isUpscroll ? 180f : 0f; - } - - public float Coverage - { - set - { - filled.Height = value; - gradient.Y = 1 - filled.Height - gradient.Height; - } - } - } -} diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs new file mode 100644 index 0000000000..5eb628947d --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.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 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.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.UI +{ + /// + /// A that has its contents partially hidden by an adjustable "cover". + /// + /// + /// The covered area extends in the scrolling direction, with its size depending on . + /// + public class PlayfieldCoveringContainer : Container + { + protected override Container Content => content; + private readonly Container content; + + /// + /// The complete cover, including gradient and fill. + /// + protected readonly Drawable Cover; + + /// + /// The gradient portion of the cover. + /// + private readonly Box gradient; + + /// + /// The fully-opaque portion of the cover. + /// + private readonly Box filled; + + private readonly IBindable scrollDirection = new Bindable(); + + public PlayfieldCoveringContainer() + { + InternalChild = new BufferedContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + content = new Container { RelativeSizeAxes = Axes.Both }, + Cover = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Blending = new BlendingParameters + { + // Don't change the destination colour. + RGBEquation = BlendingEquation.Add, + Source = BlendingType.Zero, + Destination = BlendingType.One, + // Subtract the cover's alpha from the destination (points with alpha 1 should make the destination completely transparent). + AlphaEquation = BlendingEquation.Add, + SourceAlpha = BlendingType.Zero, + DestinationAlpha = BlendingType.OneMinusSrcAlpha + }, + Children = new Drawable[] + { + gradient = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + Height = 0.25f, + Colour = ColourInfo.GradientVertical( + Color4.White.Opacity(0f), + Color4.White.Opacity(1f) + ) + }, + filled = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both + } + } + } + } + }; + } + + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { + scrollDirection.BindTo(scrollingInfo.Direction); + scrollDirection.BindValueChanged(onScrollDirectionChanged, true); + } + + private void onScrollDirectionChanged(ValueChangedEvent direction) + => Cover.Rotation = direction.NewValue == ScrollingDirection.Up ? 0 : 180f; + + /// + /// The relative area that should be completely covered. This does not include the fade. + /// + public float Coverage + { + set + { + filled.Height = value; + gradient.Y = -value; + } + } + } +} From 545e4a1f66ad25a1969d7669dc79fee47ce6a7c3 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Thu, 16 Jul 2020 15:32:07 +0700 Subject: [PATCH 2239/6909] fix formatting --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 6d6f0357c2..5f9dedcbed 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -56,7 +56,8 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase(true)] [TestCase(false)] - public void TestHitLighting(bool enable) { + public void TestHitLighting(bool enable) + { Catcher catcher = this.ChildrenOfType().First().MovableCatcher; AddStep("Toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable)); From d546db0ec93296aea0c60e751c88b5d8a15a7237 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 17:35:00 +0900 Subject: [PATCH 2240/6909] Fix default coverage --- osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs index 5eb628947d..752ac653a3 100644 --- a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs @@ -83,7 +83,8 @@ namespace osu.Game.Rulesets.Mania.UI { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Height = 0 } } } From b09c584d910ffe2271b097524dd86261ef5b1fd5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 17:35:02 +0900 Subject: [PATCH 2241/6909] Add test --- .../TestScenePlayfieldCoveringContainer.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs new file mode 100644 index 0000000000..01fee47420 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.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.Shapes; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestScenePlayfieldCoveringContainer : OsuTestScene + { + private readonly ScrollingTestContainer scrollingContainer; + private readonly PlayfieldCoveringContainer cover; + + public TestScenePlayfieldCoveringContainer() + { + Child = scrollingContainer = new ScrollingTestContainer(ScrollingDirection.Down) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(300, 500), + Child = cover = new PlayfieldCoveringContainer + { + RelativeSizeAxes = Axes.Both, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Orange + } + } + }; + } + + [Test] + public void TestScrollingDownwards() + { + AddStep("set down scroll", () => scrollingContainer.Direction = ScrollingDirection.Down); + AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f); + AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f); + AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f); + } + + [Test] + public void TestScrollingUpwards() + { + AddStep("set up scroll", () => scrollingContainer.Direction = ScrollingDirection.Up); + AddStep("set coverage = 0.5", () => cover.Coverage = 0.5f); + AddStep("set coverage = 0.8f", () => cover.Coverage = 0.8f); + AddStep("set coverage = 0.2f", () => cover.Coverage = 0.2f); + } + } +} From 84e2e5677a3ccbfc6fb38c56e1d94a606852a6b0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 17:35:51 +0900 Subject: [PATCH 2242/6909] Add more info to xmldoc --- osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs index 752ac653a3..8579799af3 100644 --- a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs @@ -14,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.UI { /// - /// A that has its contents partially hidden by an adjustable "cover". + /// A that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield. /// /// /// The covered area extends in the scrolling direction, with its size depending on . From 02031cea01b918da2f3e7cb3048113f32e461b1e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 17:37:30 +0900 Subject: [PATCH 2243/6909] Add newline --- osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs index 8579799af3..fe4ca38d0c 100644 --- a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs @@ -22,6 +22,7 @@ namespace osu.Game.Rulesets.Mania.UI public class PlayfieldCoveringContainer : Container { protected override Container Content => content; + private readonly Container content; /// From 74c7d9e67d6f09eb182d0765e808f8c01821e2b8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 17:38:52 +0900 Subject: [PATCH 2244/6909] Use WithChild --- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 023de8fe25..64bd7246ae 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -29,11 +29,10 @@ namespace osu.Game.Rulesets.Mania.Mods Container hocParent = (Container)hoc.Parent; hocParent.Remove(hoc); - hocParent.Add(CreateCover().With(c => + hocParent.Add(CreateCover().WithChild(hoc).With(c => { c.RelativeSizeAxes = Axes.Both; c.Coverage = 0.5f; - c.Child = hoc; })); } } From 967238e2694127b2bdd50e1f4f3a1efd59d2fb0b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 17:47:00 +0900 Subject: [PATCH 2245/6909] Add comment explaining scale --- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 64bd7246ae..17e0637e04 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -43,6 +43,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public ModHiddenCoveringContainer() { + // This cover extends outwards from the hit position. Cover.Scale = new Vector2(1, -1); } } From 6df1b1d9ea19bfd65be8bba837c5c8b2feff38ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 20:38:33 +0900 Subject: [PATCH 2246/6909] Add a background beatmap difficulty manager --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 99 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 1 + 2 files changed, 100 insertions(+) create mode 100644 osu.Game/Beatmaps/BeatmapDifficultyManager.cs diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs new file mode 100644 index 0000000000..f09118a24a --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -0,0 +1,99 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Threading; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Beatmaps +{ + public class BeatmapDifficultyManager : CompositeDrawable + { + // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes. + private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager)); + + private readonly TimedExpiryCache difficultyCache = new TimedExpiryCache { ExpiryTime = 60000 }; + private readonly BeatmapManager beatmapManager; + + public BeatmapDifficultyManager(BeatmapManager beatmapManager) + { + this.beatmapManager = beatmapManager; + } + + public Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + CancellationToken cancellationToken = default) + => Task.Factory.StartNew(() => GetDifficulty(beatmapInfo, rulesetInfo, mods), cancellationToken, + TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, + updateScheduler); + + public double GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) + { + // Difficulty can only be computed if the beatmap is locally available. + if (beatmapInfo.ID == 0) + return 0; + + // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. + rulesetInfo ??= beatmapInfo.Ruleset; + + var key = new DifficultyCacheLookup(beatmapInfo, rulesetInfo, mods); + if (difficultyCache.TryGetValue(key, out var existing)) + return existing; + + try + { + var ruleset = rulesetInfo.CreateInstance(); + Debug.Assert(ruleset != null); + + var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); + var attributes = calculator.Calculate(mods?.ToArray() ?? Array.Empty()); + + difficultyCache.Add(key, attributes.StarRating); + return attributes.StarRating; + } + catch + { + difficultyCache.Add(key, 0); + return 0; + } + } + + private readonly struct DifficultyCacheLookup : IEquatable + { + private readonly BeatmapInfo beatmapInfo; + private readonly RulesetInfo rulesetInfo; + private readonly IReadOnlyList mods; + + public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods) + { + this.beatmapInfo = beatmapInfo; + this.rulesetInfo = rulesetInfo; + this.mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); + } + + public bool Equals(DifficultyCacheLookup other) + => beatmapInfo.Equals(other.beatmapInfo) + && mods.SequenceEqual(other.mods); + + public override int GetHashCode() + { + var hashCode = new HashCode(); + + hashCode.Add(beatmapInfo.Hash); + hashCode.Add(rulesetInfo.GetHashCode()); + foreach (var mod in mods) + hashCode.Add(mod.Acronym); + + return hashCode.ToHashCode(); + } + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dd120937af..1e6631ffa0 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -199,6 +199,7 @@ namespace osu.Game ScoreManager.Undelete(getBeatmapScores(item), true); }); + dependencies.Cache(new BeatmapDifficultyManager(BeatmapManager)); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); From 68d2888a8c16cd5e16c1f50f12e5d22279dfd2c4 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Jul 2020 14:48:40 +0300 Subject: [PATCH 2247/6909] Add NewsOverlay to the game --- osu.Game.Tests/Visual/TestSceneOsuGame.cs | 1 + osu.Game/OsuGame.cs | 5 ++++- osu.Game/Overlays/Toolbar/Toolbar.cs | 1 + .../Overlays/Toolbar/ToolbarNewsButton.cs | 22 +++++++++++++++++++ 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs diff --git a/osu.Game.Tests/Visual/TestSceneOsuGame.cs b/osu.Game.Tests/Visual/TestSceneOsuGame.cs index 22ae5257e7..b347c39c1e 100644 --- a/osu.Game.Tests/Visual/TestSceneOsuGame.cs +++ b/osu.Game.Tests/Visual/TestSceneOsuGame.cs @@ -44,6 +44,7 @@ namespace osu.Game.Tests.Visual typeof(NotificationOverlay), typeof(BeatmapListingOverlay), typeof(DashboardOverlay), + typeof(NewsOverlay), typeof(ChannelManager), typeof(ChatOverlay), typeof(SettingsOverlay), diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 618049e72c..84b32673d5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -71,6 +71,8 @@ namespace osu.Game private DashboardOverlay dashboard; + private NewsOverlay news; + private UserProfileOverlay userProfile; private BeatmapSetOverlay beatmapSetOverlay; @@ -630,6 +632,7 @@ namespace osu.Game // overlay elements loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); + loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); var rankingsOverlay = loadComponentSingleFile(new RankingsOverlay(), overlayContent.Add, true); loadComponentSingleFile(channelManager = new ChannelManager(), AddInternal, true); loadComponentSingleFile(chatOverlay = new ChatOverlay(), overlayContent.Add, true); @@ -687,7 +690,7 @@ namespace osu.Game } // ensure only one of these overlays are open at once. - var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, dashboard, beatmapListing, changelogOverlay, rankingsOverlay }; + var singleDisplayOverlays = new OverlayContainer[] { chatOverlay, news, dashboard, beatmapListing, changelogOverlay, rankingsOverlay }; foreach (var overlay in singleDisplayOverlays) { diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index de08b79f57..5bdd86c671 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -69,6 +69,7 @@ namespace osu.Game.Overlays.Toolbar AutoSizeAxes = Axes.X, Children = new Drawable[] { + new ToolbarNewsButton(), new ToolbarChangelogButton(), new ToolbarRankingsButton(), new ToolbarBeatmapListingButton(), diff --git a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs new file mode 100644 index 0000000000..e813a3f4cb --- /dev/null +++ b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.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.Allocation; +using osu.Framework.Graphics.Sprites; + +namespace osu.Game.Overlays.Toolbar +{ + public class ToolbarNewsButton : ToolbarOverlayToggleButton + { + public ToolbarNewsButton() + { + Icon = FontAwesome.Solid.Newspaper; + } + + [BackgroundDependencyLoader(true)] + private void load(NewsOverlay news) + { + StateContainer = news; + } + } +} From dac98c8914c7c7f873fa26945bf2195fd72f271b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Jul 2020 14:55:02 +0300 Subject: [PATCH 2248/6909] Add shortcut for news overlay --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++++ osu.Game/OsuGame.cs | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 6ae420b162..9f59551b94 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -36,6 +36,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleDirect), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), + new KeyBinding(new[] { InputKey.Control, InputKey.A }, GlobalAction.ToggleNews), new KeyBinding(InputKey.Escape, GlobalAction.Back), new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), @@ -165,5 +166,8 @@ namespace osu.Game.Input.Bindings [Description("Pause")] PauseGameplay, + + [Description("Toggle news overlay")] + ToggleNews } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 84b32673d5..fc904cb09c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -872,6 +872,10 @@ namespace osu.Game dashboard.ToggleVisibility(); return true; + case GlobalAction.ToggleNews: + news.ToggleVisibility(); + return true; + case GlobalAction.ResetInputSettings: var sensitivity = frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity); From 3191bb506fe41950ec8f3b25be5632782499479a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 21:07:14 +0900 Subject: [PATCH 2249/6909] Improve asynchronous process --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 83 ++++++++++++------- 1 file changed, 55 insertions(+), 28 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index f09118a24a..02342e9595 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -29,32 +29,32 @@ namespace osu.Game.Beatmaps this.beatmapManager = beatmapManager; } - public Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, - CancellationToken cancellationToken = default) - => Task.Factory.StartNew(() => GetDifficulty(beatmapInfo, rulesetInfo, mods), cancellationToken, - TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, - updateScheduler); + public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + CancellationToken cancellationToken = default) + { + if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) + return existing; + + return await Task.Factory.StartNew(() => getDifficulty(key), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + } public double GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) { - // Difficulty can only be computed if the beatmap is locally available. - if (beatmapInfo.ID == 0) - return 0; - - // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. - rulesetInfo ??= beatmapInfo.Ruleset; - - var key = new DifficultyCacheLookup(beatmapInfo, rulesetInfo, mods); - if (difficultyCache.TryGetValue(key, out var existing)) + if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; + return getDifficulty(key); + } + + private double getDifficulty(in DifficultyCacheLookup key) + { try { - var ruleset = rulesetInfo.CreateInstance(); + var ruleset = key.RulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); - var attributes = calculator.Calculate(mods?.ToArray() ?? Array.Empty()); + var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); + var attributes = calculator.Calculate(key.Mods); difficultyCache.Add(key, attributes.StarRating); return attributes.StarRating; @@ -66,30 +66,57 @@ namespace osu.Game.Beatmaps } } + /// + /// Attempts to retrieve an existing difficulty for the combination. + /// + /// The . + /// The . + /// The s. + /// The existing difficulty value, if present. + /// The key that was used to perform this lookup. This can be further used to query . + /// Whether an existing difficulty was found. + private bool tryGetGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out double existingDifficulty, out DifficultyCacheLookup key) + { + // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. + rulesetInfo ??= beatmapInfo.Ruleset; + + // Difficulty can only be computed if the beatmap is locally available. + if (beatmapInfo.ID == 0) + { + existingDifficulty = 0; + key = default; + + return true; + } + + key = new DifficultyCacheLookup(beatmapInfo, rulesetInfo, mods); + return difficultyCache.TryGetValue(key, out existingDifficulty); + } + private readonly struct DifficultyCacheLookup : IEquatable { - private readonly BeatmapInfo beatmapInfo; - private readonly RulesetInfo rulesetInfo; - private readonly IReadOnlyList mods; + public readonly BeatmapInfo BeatmapInfo; + public readonly RulesetInfo RulesetInfo; + public readonly Mod[] Mods; public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods) { - this.beatmapInfo = beatmapInfo; - this.rulesetInfo = rulesetInfo; - this.mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); + BeatmapInfo = beatmapInfo; + RulesetInfo = rulesetInfo; + Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); } public bool Equals(DifficultyCacheLookup other) - => beatmapInfo.Equals(other.beatmapInfo) - && mods.SequenceEqual(other.mods); + => BeatmapInfo.Equals(other.BeatmapInfo) + && Mods.SequenceEqual(other.Mods); public override int GetHashCode() { var hashCode = new HashCode(); - hashCode.Add(beatmapInfo.Hash); - hashCode.Add(rulesetInfo.GetHashCode()); - foreach (var mod in mods) + hashCode.Add(BeatmapInfo.Hash); + hashCode.Add(RulesetInfo.GetHashCode()); + foreach (var mod in Mods) hashCode.Add(mod.Acronym); return hashCode.ToHashCode(); From 24f14751ce77a98c3a520e3b66c25df05666c0c6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 21:08:08 +0900 Subject: [PATCH 2250/6909] Update beatmap details SR on ruleset/mod changes --- .../Screens/Select/Details/AdvancedStats.cs | 36 +++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 02822ea608..c5fc3701f8 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -14,10 +14,13 @@ using osu.Framework.Bindables; using System.Collections.Generic; using osu.Game.Rulesets.Mods; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Overlays.Settings; +using osu.Game.Rulesets; namespace osu.Game.Screens.Select.Details { @@ -26,6 +29,12 @@ namespace osu.Game.Screens.Select.Details [Resolved] private IBindable> mods { get; set; } + [Resolved] + private IBindable ruleset { get; set; } + + [Resolved] + private BeatmapDifficultyManager difficultyManager { get; set; } + protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate; private readonly StatisticRow starDifficulty; @@ -71,6 +80,7 @@ namespace osu.Game.Screens.Select.Details { base.LoadComplete(); + ruleset.BindValueChanged(_ => updateStatistics()); mods.BindValueChanged(modsChanged, true); } @@ -132,11 +142,33 @@ namespace osu.Game.Screens.Select.Details break; } - starDifficulty.Value = ((float)(Beatmap?.StarDifficulty ?? 0), null); - HpDrain.Value = (baseDifficulty?.DrainRate ?? 0, adjustedDifficulty?.DrainRate); Accuracy.Value = (baseDifficulty?.OverallDifficulty ?? 0, adjustedDifficulty?.OverallDifficulty); ApproachRate.Value = (baseDifficulty?.ApproachRate ?? 0, adjustedDifficulty?.ApproachRate); + + updateStarDifficulty(); + } + + private CancellationTokenSource starDifficultyCancellationSource; + + private void updateStarDifficulty() + { + starDifficultyCancellationSource?.Cancel(); + + if (Beatmap == null) + return; + + var ourSource = starDifficultyCancellationSource = new CancellationTokenSource(); + + Task.WhenAll(difficultyManager.GetDifficultyAsync(Beatmap, ruleset.Value, cancellationToken: ourSource.Token), + difficultyManager.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, ourSource.Token)).ContinueWith(t => + { + Schedule(() => + { + if (!ourSource.IsCancellationRequested) + starDifficulty.Value = ((float)t.Result[0], (float)t.Result[1]); + }); + }, ourSource.Token); } public class StatisticRow : Container, IHasAccentColour From 9a52058a7aa5a8aa153a8793c83d77b0b1d37b3f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 21:08:24 +0900 Subject: [PATCH 2251/6909] Update carousel beatmap SR on mod/ruleset changes --- .../Carousel/DrawableCarouselBeatmap.cs | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 3e4798a812..d4205a4b93 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -3,7 +3,9 @@ using System; using System.Collections.Generic; +using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -13,6 +15,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -20,6 +23,8 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -41,6 +46,15 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private BeatmapSetOverlay beatmapOverlay { get; set; } + [Resolved] + private IBindable ruleset { get; set; } + + [Resolved] + private IBindable> mods { get; set; } + + [Resolved] + private BeatmapDifficultyManager difficultyManager { get; set; } + public DrawableCarouselBeatmap(CarouselBeatmap panel) : base(panel) { @@ -137,7 +151,6 @@ namespace osu.Game.Screens.Select.Carousel }, starCounter = new StarCounter { - Current = (float)beatmap.StarDifficulty, Scale = new Vector2(0.8f), } } @@ -147,6 +160,36 @@ namespace osu.Game.Screens.Select.Carousel } } }; + + ruleset.BindValueChanged(_ => refreshStarCounter()); + mods.BindValueChanged(_ => refreshStarCounter(), true); + } + + private ScheduledDelegate scheduledRefresh; + private CancellationTokenSource cancellationSource; + + private void refreshStarCounter() + { + scheduledRefresh?.Cancel(); + scheduledRefresh = null; + + cancellationSource?.Cancel(); + cancellationSource = null; + + // Only want to run the calculation when we become visible. + scheduledRefresh = Schedule(() => + { + var ourSource = cancellationSource = new CancellationTokenSource(); + difficultyManager.GetDifficultyAsync(beatmap, ruleset.Value, mods.Value, ourSource.Token).ContinueWith(t => + { + // We're currently on a random threadpool thread which we must exit. + Schedule(() => + { + if (!ourSource.IsCancellationRequested) + starCounter.Current = (float)t.Result; + }); + }, ourSource.Token); + }); } protected override void Selected() From 18d36850233e2bd7d464c4409fb89dc84b50b43f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 21:17:51 +0900 Subject: [PATCH 2252/6909] Pass in content --- .../TestScenePlayfieldCoveringContainer.cs | 11 +++++------ osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 7 ++++--- .../UI/PlayfieldCoveringContainer.cs | 10 +++------- 4 files changed, 14 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs index 01fee47420..cbd33ca9a8 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs @@ -24,14 +24,13 @@ namespace osu.Game.Rulesets.Mania.Tests Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(300, 500), - Child = cover = new PlayfieldCoveringContainer + Child = cover = new PlayfieldCoveringContainer(new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Orange + }) { RelativeSizeAxes = Axes.Both, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Orange - } } }; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index 5c643a7d37..9761599e8e 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.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.Rulesets.Mania.UI; @@ -14,6 +15,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override IconUsage? Icon => OsuIcon.ModHidden; public override string Description => @"Keys appear out of nowhere!"; - protected override PlayfieldCoveringContainer CreateCover() => new PlayfieldCoveringContainer(); + protected override PlayfieldCoveringContainer CreateCover(Drawable content) => new PlayfieldCoveringContainer(content); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 17e0637e04..3f7c09674e 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Mania.Mods Container hocParent = (Container)hoc.Parent; hocParent.Remove(hoc); - hocParent.Add(CreateCover().WithChild(hoc).With(c => + hocParent.Add(CreateCover(hoc).With(c => { c.RelativeSizeAxes = Axes.Both; c.Coverage = 0.5f; @@ -37,11 +37,12 @@ namespace osu.Game.Rulesets.Mania.Mods } } - protected virtual PlayfieldCoveringContainer CreateCover() => new ModHiddenCoveringContainer(); + protected virtual PlayfieldCoveringContainer CreateCover(Drawable content) => new ModHiddenCoveringContainer(content); private class ModHiddenCoveringContainer : PlayfieldCoveringContainer { - public ModHiddenCoveringContainer() + public ModHiddenCoveringContainer(Drawable content) + : base(content) { // This cover extends outwards from the hit position. Cover.Scale = new Vector2(1, -1); diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs index fe4ca38d0c..faac663169 100644 --- a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs @@ -19,12 +19,8 @@ namespace osu.Game.Rulesets.Mania.UI /// /// The covered area extends in the scrolling direction, with its size depending on . /// - public class PlayfieldCoveringContainer : Container + public class PlayfieldCoveringContainer : CompositeDrawable { - protected override Container Content => content; - - private readonly Container content; - /// /// The complete cover, including gradient and fill. /// @@ -42,14 +38,14 @@ namespace osu.Game.Rulesets.Mania.UI private readonly IBindable scrollDirection = new Bindable(); - public PlayfieldCoveringContainer() + public PlayfieldCoveringContainer(Drawable content) { InternalChild = new BufferedContainer { RelativeSizeAxes = Axes.Both, Children = new[] { - content = new Container { RelativeSizeAxes = Axes.Both }, + content, Cover = new Container { Anchor = Anchor.Centre, From 2d9909cdd89b658411b450ad6e6ee86a8ba67193 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Jul 2020 15:18:01 +0300 Subject: [PATCH 2253/6909] Make news cards clickable --- .../Visual/Online/TestSceneNewsCard.cs | 7 +-- osu.Game/Overlays/News/NewsCard.cs | 50 ++++++++----------- 2 files changed, 26 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs index 0446cadac9..17675bfbc0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNewsCard.cs @@ -31,15 +31,16 @@ namespace osu.Game.Tests.Visual.Online { new NewsCard(new APINewsPost { - Title = "This post has an image which starts with \"/\" and has many authors!", + Title = "This post has an image which starts with \"/\" and has many authors! (clickable)", 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 = DateTimeOffset.Now + PublishedAt = DateTimeOffset.Now, + Slug = "2020-07-16-summer-theme-park-2020-voting-open" }), new NewsCard(new APINewsPost { - Title = "This post has a full-url image! (HTML entity: &)", + Title = "This post has a full-url image! (HTML entity: &) (non-clickable)", Preview = "boom (HTML entity: &)", Author = "user (HTML entity: &)", FirstImage = "https://assets.ppy.sh/artists/88/header.jpg", diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index 9c478a7c1d..38362038ae 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -10,18 +11,17 @@ 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.Framework.Platform; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.News { - public class NewsCard : CompositeDrawable + public class NewsCard : OsuHoverContainer { - [Resolved] - private OverlayColourProvider colourProvider { get; set; } + protected override IEnumerable EffectTargets => new[] { background }; private readonly APINewsPost post; @@ -31,24 +31,28 @@ namespace osu.Game.Overlays.News public NewsCard(APINewsPost post) { this.post = post; - } - [BackgroundDependencyLoader] - private void load() - { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Masking = true; CornerRadius = 6; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider, GameHost host) + { + if (post.Slug != null) + { + TooltipText = "view in browser"; + Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug); + } NewsBackground bg; - - InternalChildren = new Drawable[] + AddRange(new Drawable[] { background = new Box { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4 + RelativeSizeAxes = Axes.Both }, new FillFlowContainer { @@ -104,9 +108,11 @@ namespace osu.Game.Overlays.News } } } - }, - new HoverClickSounds() - }; + } + }); + + IdleColour = colourProvider.Background4; + HoverColour = colourProvider.Background3; bg.OnLoadComplete += d => d.FadeIn(250, Easing.In); @@ -116,18 +122,6 @@ namespace osu.Game.Overlays.News 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 { From 03a7b8a6ef2268b1a9772a766a5617f5b8982885 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 21:18:24 +0900 Subject: [PATCH 2254/6909] Rename --- .../TestScenePlayfieldCoveringContainer.cs | 4 ++-- osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs | 2 +- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 6 +++--- ...ieldCoveringContainer.cs => PlayfieldCoveringWrapper.cs} | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game.Rulesets.Mania/UI/{PlayfieldCoveringContainer.cs => PlayfieldCoveringWrapper.cs} (97%) diff --git a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs index cbd33ca9a8..8698ba3abd 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestScenePlayfieldCoveringContainer.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Mania.Tests public class TestScenePlayfieldCoveringContainer : OsuTestScene { private readonly ScrollingTestContainer scrollingContainer; - private readonly PlayfieldCoveringContainer cover; + private readonly PlayfieldCoveringWrapper cover; public TestScenePlayfieldCoveringContainer() { @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(300, 500), - Child = cover = new PlayfieldCoveringContainer(new Box + Child = cover = new PlayfieldCoveringWrapper(new Box { RelativeSizeAxes = Axes.Both, Colour = Color4.Orange diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index 9761599e8e..3893b83db9 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override IconUsage? Icon => OsuIcon.ModHidden; public override string Description => @"Keys appear out of nowhere!"; - protected override PlayfieldCoveringContainer CreateCover(Drawable content) => new PlayfieldCoveringContainer(content); + protected override PlayfieldCoveringWrapper CreateCover(Drawable content) => new PlayfieldCoveringWrapper(content); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 3f7c09674e..e460ccee9c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -37,11 +37,11 @@ namespace osu.Game.Rulesets.Mania.Mods } } - protected virtual PlayfieldCoveringContainer CreateCover(Drawable content) => new ModHiddenCoveringContainer(content); + protected virtual PlayfieldCoveringWrapper CreateCover(Drawable content) => new ModHiddenCoveringWrapper(content); - private class ModHiddenCoveringContainer : PlayfieldCoveringContainer + private class ModHiddenCoveringWrapper : PlayfieldCoveringWrapper { - public ModHiddenCoveringContainer(Drawable content) + public ModHiddenCoveringWrapper(Drawable content) : base(content) { // This cover extends outwards from the hit position. diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs similarity index 97% rename from osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs rename to osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs index faac663169..207239dd38 100644 --- a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringContainer.cs +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// The covered area extends in the scrolling direction, with its size depending on . /// - public class PlayfieldCoveringContainer : CompositeDrawable + public class PlayfieldCoveringWrapper : CompositeDrawable { /// /// The complete cover, including gradient and fill. @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.UI private readonly IBindable scrollDirection = new Bindable(); - public PlayfieldCoveringContainer(Drawable content) + public PlayfieldCoveringWrapper(Drawable content) { InternalChild = new BufferedContainer { From 8d9e5db641a5f2b54ff88e988dd4a353cff2906b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 16 Jul 2020 21:29:39 +0900 Subject: [PATCH 2255/6909] Use enum values instead of class override --- .../Mods/ManiaModFadeIn.cs | 3 +- .../Mods/ManiaModHidden.cs | 21 +++++-------- .../UI/PlayfieldCoveringWrapper.cs | 31 +++++++++++++++---- 3 files changed, 33 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index 3893b83db9..cbdcd49c5b 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.UI; @@ -15,6 +14,6 @@ namespace osu.Game.Rulesets.Mania.Mods public override IconUsage? Icon => OsuIcon.ModHidden; public override string Description => @"Keys appear out of nowhere!"; - protected override PlayfieldCoveringWrapper CreateCover(Drawable content) => new PlayfieldCoveringWrapper(content); + protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index e460ccee9c..4bdb15526f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; -using osuTK; namespace osu.Game.Rulesets.Mania.Mods { @@ -19,6 +18,11 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; + /// + /// The direction in which the cover should expand. + /// + protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; + public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; @@ -29,24 +33,13 @@ namespace osu.Game.Rulesets.Mania.Mods Container hocParent = (Container)hoc.Parent; hocParent.Remove(hoc); - hocParent.Add(CreateCover(hoc).With(c => + hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c => { c.RelativeSizeAxes = Axes.Both; + c.Direction = ExpandDirection; c.Coverage = 0.5f; })); } } - - protected virtual PlayfieldCoveringWrapper CreateCover(Drawable content) => new ModHiddenCoveringWrapper(content); - - private class ModHiddenCoveringWrapper : PlayfieldCoveringWrapper - { - public ModHiddenCoveringWrapper(Drawable content) - : base(content) - { - // This cover extends outwards from the hit position. - Cover.Scale = new Vector2(1, -1); - } - } } } diff --git a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs index 207239dd38..15d216e8c5 100644 --- a/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs +++ b/osu.Game.Rulesets.Mania/UI/PlayfieldCoveringWrapper.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.UI.Scrolling; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.UI @@ -16,15 +17,12 @@ namespace osu.Game.Rulesets.Mania.UI /// /// A that has its contents partially hidden by an adjustable "cover". This is intended to be used in a playfield. /// - /// - /// The covered area extends in the scrolling direction, with its size depending on . - /// public class PlayfieldCoveringWrapper : CompositeDrawable { /// /// The complete cover, including gradient and fill. /// - protected readonly Drawable Cover; + private readonly Drawable cover; /// /// The gradient portion of the cover. @@ -46,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI Children = new[] { content, - Cover = new Container + cover = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -97,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.UI } private void onScrollDirectionChanged(ValueChangedEvent direction) - => Cover.Rotation = direction.NewValue == ScrollingDirection.Up ? 0 : 180f; + => cover.Rotation = direction.NewValue == ScrollingDirection.Up ? 0 : 180f; /// /// The relative area that should be completely covered. This does not include the fade. @@ -110,5 +108,26 @@ namespace osu.Game.Rulesets.Mania.UI gradient.Y = -value; } } + + /// + /// The direction in which the cover expands. + /// + public CoverExpandDirection Direction + { + set => cover.Scale = value == CoverExpandDirection.AlongScroll ? Vector2.One : new Vector2(1, -1); + } + } + + public enum CoverExpandDirection + { + /// + /// The cover expands along the scrolling direction. + /// + AlongScroll, + + /// + /// The cover expands against the scrolling direction. + /// + AgainstScroll } } From 939441ae408d5f4eb7ee61dba8da54e5e056d481 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 16 Jul 2020 14:50:11 +0200 Subject: [PATCH 2256/6909] Disable the windows key only when in gameplay. --- osu.Desktop/Windows/GameplayWinKeyHandler.cs | 14 +++++++------- osu.Game/Configuration/SessionStatics.cs | 4 +++- osu.Game/Screens/Play/ScreenSuspensionHandler.cs | 13 ++++++++++++- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs index 4f74a4f492..d5ef89c680 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.cs @@ -11,26 +11,26 @@ namespace osu.Desktop.Windows { public class GameplayWinKeyHandler : Component { - private Bindable allowScreenSuspension; private Bindable disableWinKey; + private Bindable disableWinKeySetting; private GameHost host; [BackgroundDependencyLoader] - private void load(GameHost host, OsuConfigManager config) + private void load(GameHost host, OsuConfigManager config, SessionStatics statics) { this.host = host; - allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy(); - allowScreenSuspension.ValueChanged += toggleWinKey; + disableWinKey = statics.GetBindable(Static.DisableWindowsKey); + disableWinKey.ValueChanged += toggleWinKey; - disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); - disableWinKey.BindValueChanged(t => allowScreenSuspension.TriggerChange(), true); + disableWinKeySetting = config.GetBindable(OsuSetting.GameplayDisableWinKey); + disableWinKeySetting.BindValueChanged(t => disableWinKey.TriggerChange(), true); } private void toggleWinKey(ValueChangedEvent e) { - if (!e.NewValue && disableWinKey.Value) + if (e.NewValue && disableWinKeySetting.Value) host.InputThread.Scheduler.Add(WindowsKey.Disable); else host.InputThread.Scheduler.Add(WindowsKey.Enable); diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 40b2adb867..7aad79b5ad 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -12,12 +12,14 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); + Set(Static.DisableWindowsKey, false); } } public enum Static { LoginOverlayDisplayed, - MutedAudioNotificationShownOnce + MutedAudioNotificationShownOnce, + DisableWindowsKey } } diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index 8585a5c309..c2c7f1ac41 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; +using osu.Game.Configuration; namespace osu.Game.Screens.Play { @@ -22,6 +23,9 @@ namespace osu.Game.Screens.Play [Resolved] private GameHost host { get; set; } + [Resolved] + private SessionStatics statics { get; set; } + public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer) { this.gameplayClockContainer = gameplayClockContainer ?? throw new ArgumentNullException(nameof(gameplayClockContainer)); @@ -36,7 +40,11 @@ namespace osu.Game.Screens.Play Debug.Assert(host.AllowScreenSuspension.Value); isPaused = gameplayClockContainer.IsPaused.GetBoundCopy(); - isPaused.BindValueChanged(paused => host.AllowScreenSuspension.Value = paused.NewValue, true); + isPaused.BindValueChanged(paused => + { + host.AllowScreenSuspension.Value = paused.NewValue; + statics.Set(Static.DisableWindowsKey, !paused.NewValue); + }, true); } protected override void Dispose(bool isDisposing) @@ -46,7 +54,10 @@ namespace osu.Game.Screens.Play isPaused?.UnbindAll(); if (host != null) + { host.AllowScreenSuspension.Value = true; + statics.Set(Static.DisableWindowsKey, false); + } } } } From 396ada7f39fb52a3301398c1cf8d17767da86bf6 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 16 Jul 2020 15:03:25 +0200 Subject: [PATCH 2257/6909] Enable windows key when a replay is loaded. --- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/ScreenSuspensionHandler.cs | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..e0721d55f7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -181,7 +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)); + AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer, DrawableRuleset.HasReplayLoaded)); dependencies.CacheAs(gameplayBeatmap); diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index c2c7f1ac41..6865db5a5e 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -19,6 +19,7 @@ namespace osu.Game.Screens.Play { private readonly GameplayClockContainer gameplayClockContainer; private Bindable isPaused; + private readonly Bindable hasReplayLoaded; [Resolved] private GameHost host { get; set; } @@ -26,9 +27,10 @@ namespace osu.Game.Screens.Play [Resolved] private SessionStatics statics { get; set; } - public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer) + public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer, Bindable hasReplayLoaded) { this.gameplayClockContainer = gameplayClockContainer ?? throw new ArgumentNullException(nameof(gameplayClockContainer)); + this.hasReplayLoaded = hasReplayLoaded.GetBoundCopy(); } protected override void LoadComplete() @@ -43,8 +45,9 @@ namespace osu.Game.Screens.Play isPaused.BindValueChanged(paused => { host.AllowScreenSuspension.Value = paused.NewValue; - statics.Set(Static.DisableWindowsKey, !paused.NewValue); + statics.Set(Static.DisableWindowsKey, !paused.NewValue && !hasReplayLoaded.Value); }, true); + hasReplayLoaded.BindValueChanged(_ => isPaused.TriggerChange(), true); } protected override void Dispose(bool isDisposing) @@ -52,6 +55,7 @@ namespace osu.Game.Screens.Play base.Dispose(isDisposing); isPaused?.UnbindAll(); + hasReplayLoaded.UnbindAll(); if (host != null) { From f72a6b8c9d55642706b80e419fb567e620de1a62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jul 2020 20:08:48 +0200 Subject: [PATCH 2258/6909] Use [Resolved] instead --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 5f9dedcbed..6bacb0383f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -25,7 +25,9 @@ namespace osu.Game.Rulesets.Catch.Tests public class TestSceneCatcherArea : CatchSkinnableTestScene { private RulesetInfo catchRuleset; - private OsuConfigManager config; + + [Resolved] + private OsuConfigManager config { get; set; } public TestSceneCatcherArea() { @@ -105,10 +107,9 @@ namespace osu.Game.Rulesets.Catch.Tests } [BackgroundDependencyLoader] - private void load(RulesetStore rulesets, OsuConfigManager configManager) + private void load(RulesetStore rulesets) { catchRuleset = rulesets.GetRuleset(2); - config = configManager; } public class TestFruit : Fruit From 54b0321581cfd75da817323b93eeb4be88395f5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jul 2020 20:12:32 +0200 Subject: [PATCH 2259/6909] Factor out catcher as property --- .../TestSceneCatcherArea.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 6bacb0383f..0588c4ba57 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Catch.Tests [Resolved] private OsuConfigManager config { get; set; } + private Catcher catcher => this.ChildrenOfType().First().MovableCatcher; + public TestSceneCatcherArea() { AddSliderStep("CircleSize", 0, 8, 5, createCatcher); @@ -38,20 +40,20 @@ namespace osu.Game.Rulesets.Catch.Tests AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false) { - X = this.ChildrenOfType().First().MovableCatcher.X + X = catcher.X }), 20); AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false) { - X = this.ChildrenOfType().First().MovableCatcher.X, + X = catcher.X, LastInCombo = true, }), 20); AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true) { - X = this.ChildrenOfType().First().MovableCatcher.X, + X = catcher.X }), 20); AddRepeatStep("miss fruit", () => catchFruit(new Fruit { - X = this.ChildrenOfType().First().MovableCatcher.X + 100, + X = catcher.X + 100, LastInCombo = true, }, true), 20); } @@ -60,8 +62,6 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase(false)] public void TestHitLighting(bool enable) { - Catcher catcher = this.ChildrenOfType().First().MovableCatcher; - AddStep("Toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable)); AddStep("Catch fruit", () => catchFruit(new TestFruit(false) { From fbf3a098351d7a7dec872bd2953d2e3f563862ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jul 2020 20:14:57 +0200 Subject: [PATCH 2260/6909] Create catcher explicitly to make tests independent of ctor --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 0588c4ba57..d23c691023 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase(false)] public void TestHitLighting(bool enable) { + AddStep("create catcher", () => createCatcher(5)); + AddStep("Toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable)); AddStep("Catch fruit", () => catchFruit(new TestFruit(false) { From a8e96b399457bfafa4caa0a3b4dc84939a964086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 16 Jul 2020 20:17:37 +0200 Subject: [PATCH 2261/6909] Reword test steps for consistency & fix typo --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index d23c691023..b4f123598b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -64,17 +64,17 @@ namespace osu.Game.Rulesets.Catch.Tests { AddStep("create catcher", () => createCatcher(5)); - AddStep("Toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable)); - AddStep("Catch fruit", () => catchFruit(new TestFruit(false) + AddStep("toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable)); + AddStep("catch fruit", () => catchFruit(new TestFruit(false) { X = catcher.X })); - AddStep("Catch fruit last combo", () => catchFruit(new TestFruit(false) + AddStep("catch fruit last in combo", () => catchFruit(new TestFruit(false) { X = catcher.X, LastInCombo = true })); - AddAssert("Check hit explotion", () => catcher.ChildrenOfType().Any() == enable); + AddAssert("check hit explosion", () => catcher.ChildrenOfType().Any() == enable); } private void catchFruit(Fruit fruit, bool miss = false) From f67b93936ffeb85bb86b0d27e4cd5af94148ea3b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Jul 2020 23:11:55 +0300 Subject: [PATCH 2262/6909] Remove shortcut --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ---- osu.Game/OsuGame.cs | 4 ---- 2 files changed, 8 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 9f59551b94..6ae420b162 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -36,7 +36,6 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleDirect), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), - new KeyBinding(new[] { InputKey.Control, InputKey.A }, GlobalAction.ToggleNews), new KeyBinding(InputKey.Escape, GlobalAction.Back), new KeyBinding(InputKey.ExtraMouseButton1, GlobalAction.Back), @@ -166,8 +165,5 @@ namespace osu.Game.Input.Bindings [Description("Pause")] PauseGameplay, - - [Description("Toggle news overlay")] - ToggleNews } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fc904cb09c..84b32673d5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -872,10 +872,6 @@ namespace osu.Game dashboard.ToggleVisibility(); return true; - case GlobalAction.ToggleNews: - news.ToggleVisibility(); - return true; - case GlobalAction.ResetInputSettings: var sensitivity = frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity); From ab23e7dfd4faad3040a8e24eb114a16b6956c183 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 16 Jul 2020 23:14:51 +0300 Subject: [PATCH 2263/6909] Protect the NewsCard from clicks while hovering DateContainer --- osu.Game/Overlays/News/NewsCard.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index 38362038ae..201c3ce826 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -11,6 +11,7 @@ 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.Framework.Platform; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -187,6 +188,8 @@ namespace osu.Game.Overlays.News } }; } + + protected override bool OnClick(ClickEvent e) => true; // Protects the NewsCard from clicks while hovering DateContainer } } } From 6a3b2ca7cfb5b42c9a15d15d84c24c39a3e1b53f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jul 2020 11:39:51 +0900 Subject: [PATCH 2264/6909] Ensure nUnit runs with correct CurrentDirectory --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 85d5fce29a..29ca385275 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -783,6 +783,7 @@ See the LICENCE file in the repository root for full licence text. True True True + TestFolder True True o!f – Object Initializer: Anchor&Origin From c44ac9104f77d72d3917c47005a3c7f7b849c72e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Jul 2020 14:19:43 +0900 Subject: [PATCH 2265/6909] Fix post-merge error --- .../Difficulty/Skills/Strain.cs | 95 ------------------- 1 file changed, 95 deletions(-) delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs deleted file mode 100644 index 2c1885ae1a..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.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 osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Skills -{ - public class Strain : Skill - { - private const double rhythm_change_base_threshold = 0.2; - private const double rhythm_change_base = 2.0; - - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.3; - - private ColourSwitch lastColourSwitch = ColourSwitch.None; - - private int sameColourCount = 1; - - protected override double StrainValueOf(DifficultyHitObject current) - { - double addition = 1; - - // We get an extra addition if we are not a slider or spinner - if (current.LastObject is Hit && current.BaseObject is Hit && current.BaseObject.StartTime - current.LastObject.StartTime < 1000) - { - if (hasColourChange(current)) - addition += 0.75; - - if (hasRhythmChange(current)) - addition += 1; - } - else - { - lastColourSwitch = ColourSwitch.None; - sameColourCount = 1; - } - - double additionFactor = 1; - - // Scale the addition factor linearly from 0.4 to 1 for DeltaTime from 0 to 50 - if (current.DeltaTime < 50) - additionFactor = 0.4 + 0.6 * current.DeltaTime / 50; - - return additionFactor * addition; - } - - private bool hasRhythmChange(DifficultyHitObject current) - { - // We don't want a division by zero if some random mapper decides to put two HitObjects at the same time. - if (current.DeltaTime == 0 || Previous.Count == 0 || Previous[0].DeltaTime == 0) - return false; - - double timeElapsedRatio = Math.Max(Previous[0].DeltaTime / current.DeltaTime, current.DeltaTime / Previous[0].DeltaTime); - - if (timeElapsedRatio >= 8) - return false; - - double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0; - - return difference > rhythm_change_base_threshold && difference < 1 - rhythm_change_base_threshold; - } - - private bool hasColourChange(DifficultyHitObject current) - { - var taikoCurrent = (TaikoDifficultyHitObject)current; - - if (!taikoCurrent.HasTypeChange) - { - sameColourCount++; - return false; - } - - var oldColourSwitch = lastColourSwitch; - var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd; - - lastColourSwitch = newColourSwitch; - sameColourCount = 1; - - // We only want a bonus if the parity of the color switch changes - return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch; - } - - private enum ColourSwitch - { - None, - Even, - Odd - } - } -} From f4d0bef897f7305799bd8b8573e13962970ee2c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jul 2020 14:38:28 +0900 Subject: [PATCH 2266/6909] Fix timing screen test crashing due to missing dependency --- osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 2a7f9389d1..09f5ac2224 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Timing; @@ -13,6 +14,7 @@ namespace osu.Game.Tests.Visual.Editing public class TestSceneTimingScreen : EditorClockTestScene { [Cached(typeof(EditorBeatmap))] + [Cached(typeof(IBeatSnapProvider))] private readonly EditorBeatmap editorBeatmap; public TestSceneTimingScreen() From e96e5587288ed80fbe21977344ada99d43f24854 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jul 2020 16:03:13 +0900 Subject: [PATCH 2267/6909] Fix reversing scroll direction not always behaving as expected --- osu.Game/Screens/Edit/Editor.cs | 17 ++++++++++++++--- osu.Game/Screens/Edit/EditorClock.cs | 2 +- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9f61589c36..d92f3922c3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -275,11 +275,22 @@ namespace osu.Game.Screens.Edit protected override bool OnScroll(ScrollEvent e) { - scrollAccumulation += (e.ScrollDelta.X + e.ScrollDelta.Y) * (e.IsPrecise ? 0.1 : 1); + const double precision = 1; - const int precision = 1; + double scrollComponent = e.ScrollDelta.X + e.ScrollDelta.Y; - while (Math.Abs(scrollAccumulation) > precision) + double scrollDirection = Math.Sign(scrollComponent); + + // this is a special case to handle the "pivot" scenario. + // if we are precise scrolling in one direction then change our mind and scroll backwards, + // the existing accumulation should be applied in the inverse direction to maintain responsiveness. + if (Math.Sign(scrollAccumulation) != scrollDirection) + scrollAccumulation = scrollDirection * (precision - Math.Abs(scrollAccumulation)); + + scrollAccumulation += scrollComponent * (e.IsPrecise ? 0.1 : 1); + + // because we are doing snapped seeking, we need to add up precise scrolls until they accumulate to an arbitrary cut-off. + while (Math.Abs(scrollAccumulation) >= precision) { if (scrollAccumulation > 0) seek(e, -1); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index dd934c10cd..36f3efec58 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -120,7 +120,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(current, seekTime)) + if (Precision.AlmostEquals(current, seekTime, 1)) { closestBeat += direction > 0 ? 1 : -1; seekTime = timingPoint.Time + closestBeat * seekAmount; From 039790ce4f02700d69b56837f86a3b210ee214f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jul 2020 16:40:02 +0900 Subject: [PATCH 2268/6909] Perform next timing point check before ensuring movement --- osu.Game/Screens/Edit/EditorClock.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 36f3efec58..d4d0feb813 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -118,9 +118,14 @@ namespace osu.Game.Screens.Edit seekTime = timingPoint.Time + closestBeat * seekAmount; + // limit forward seeking to only up to the next timing point's start time. + var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); + if (seekTime > nextTimingPoint?.Time) + seekTime = nextTimingPoint.Time; + // 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(current, seekTime, 1)) + if (Precision.AlmostEquals(current, seekTime, 0.5f)) { closestBeat += direction > 0 ? 1 : -1; seekTime = timingPoint.Time + closestBeat * seekAmount; @@ -129,10 +134,6 @@ namespace osu.Game.Screens.Edit if (seekTime < timingPoint.Time && timingPoint != ControlPointInfo.TimingPoints.First()) seekTime = timingPoint.Time; - var nextTimingPoint = ControlPointInfo.TimingPoints.FirstOrDefault(t => t.Time > timingPoint.Time); - if (seekTime > nextTimingPoint?.Time) - seekTime = nextTimingPoint.Time; - // Ensure the sought point is within the boundaries seekTime = Math.Clamp(seekTime, 0, TrackLength); SeekTo(seekTime); From 9f7750e615194fea2c8eea04194836b71d9d39c9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Jul 2020 16:54:30 +0900 Subject: [PATCH 2269/6909] Add texture wrapping support to skins --- osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs | 3 ++- .../TestSceneSkinFallbacks.cs | 3 ++- .../Gameplay/TestSceneHitObjectAccentColour.cs | 3 ++- .../Skins/TestSceneSkinConfigurationLookup.cs | 3 ++- .../Visual/Gameplay/TestSceneSkinnableDrawable.cs | 7 ++++--- osu.Game/Skinning/DefaultSkin.cs | 3 ++- osu.Game/Skinning/ISkin.cs | 13 ++++++++++++- osu.Game/Skinning/LegacySkin.cs | 7 ++++--- osu.Game/Skinning/LegacySkinTransformer.cs | 6 +++++- osu.Game/Skinning/Skin.cs | 5 ++++- osu.Game/Skinning/SkinManager.cs | 3 ++- osu.Game/Skinning/SkinProvidingContainer.cs | 7 ++++--- osu.Game/Tests/Visual/SkinnableTestScene.cs | 7 ++++--- 13 files changed, 49 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index 46769f65fe..dde02e873b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing.Input; using osu.Game.Audio; @@ -79,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Tests public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException(); - public Texture GetTexture(string componentName) + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { switch (componentName) { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index b357e20ee8..075bf314bc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -9,6 +9,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Timing; @@ -131,7 +132,7 @@ namespace osu.Game.Rulesets.Osu.Tests }; } - public Texture GetTexture(string componentName) => null; + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public SampleChannel GetSample(ISampleInfo sampleInfo) => null; diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index 7a89642e11..2d5e4b911e 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -8,6 +8,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; @@ -118,7 +119,7 @@ namespace osu.Game.Tests.Gameplay public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotImplementedException(); - public Texture GetTexture(string componentName) => throw new NotImplementedException(); + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException(); public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); diff --git a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs index 8deed75a56..ad5b3ec0f6 100644 --- a/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs +++ b/osu.Game.Tests/Skins/TestSceneSkinConfigurationLookup.cs @@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Game.Audio; @@ -216,7 +217,7 @@ namespace osu.Game.Tests.Skins public Drawable GetDrawableComponent(ISkinComponent component) => skin.GetDrawableComponent(component); - public Texture GetTexture(string componentName) => skin.GetTexture(componentName); + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => skin.GetTexture(componentName, wrapModeS, wrapModeT); public SampleChannel GetSample(ISampleInfo sampleInfo) => skin.GetSample(sampleInfo); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs index 3b91243fee..bed48f3d86 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableDrawable.cs @@ -10,6 +10,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Game.Audio; @@ -295,7 +296,7 @@ namespace osu.Game.Tests.Visual.Gameplay } : null; - public Texture GetTexture(string componentName) => throw new NotImplementedException(); + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException(); public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); @@ -306,7 +307,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public Drawable GetDrawableComponent(ISkinComponent componentName) => new SecondarySourceBox(); - public Texture GetTexture(string componentName) => throw new NotImplementedException(); + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException(); public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); @@ -318,7 +319,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public Drawable GetDrawableComponent(ISkinComponent componentName) => new BaseSourceBox(); - public Texture GetTexture(string componentName) => throw new NotImplementedException(); + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => throw new NotImplementedException(); public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotImplementedException(); diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 6b4af21b37..61d0112c89 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osuTK.Graphics; @@ -21,7 +22,7 @@ namespace osu.Game.Skinning public override Drawable GetDrawableComponent(ISkinComponent component) => null; - public override Texture GetTexture(string componentName) => null; + public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; public override SampleChannel GetSample(ISampleInfo sampleInfo) => null; diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index cb2a379b8e..5abd963773 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -5,6 +5,7 @@ using JetBrains.Annotations; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; @@ -29,7 +30,17 @@ namespace osu.Game.Skinning /// The requested texture. /// A matching texture, or null if unavailable. [CanBeNull] - Texture GetTexture(string componentName); + Texture GetTexture(string componentName) => GetTexture(componentName, default, default); + + /// + /// Retrieve a . + /// + /// The requested texture. + /// The texture wrap mode in horizontal direction. + /// The texture wrap mode in vertical direction. + /// A matching texture, or null if unavailable. + [CanBeNull] + Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT); /// /// Retrieve a . diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 4b70ccc6ad..3bbeff9918 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -11,6 +11,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Audio; @@ -311,17 +312,17 @@ namespace osu.Game.Skinning return this.GetAnimation(component.LookupName, false, false); } - public override Texture GetTexture(string componentName) + public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { foreach (var name in getFallbackNames(componentName)) { float ratio = 2; - var texture = Textures?.Get($"{name}@2x"); + var texture = Textures?.Get($"{name}@2x", wrapModeS, wrapModeT); if (texture == null) { ratio = 1; - texture = Textures?.Get(name); + texture = Textures?.Get(name, wrapModeS, wrapModeT); } if (texture == null) diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index 94a7a32f05..786056b932 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -4,6 +4,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; @@ -27,7 +28,10 @@ namespace osu.Game.Skinning public abstract Drawable GetDrawableComponent(ISkinComponent component); - public Texture GetTexture(string componentName) => Source.GetTexture(componentName); + public Texture GetTexture(string componentName) => GetTexture(componentName, default, default); + + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + => Source.GetTexture(componentName, wrapModeS, wrapModeT); public virtual SampleChannel GetSample(ISampleInfo sampleInfo) { diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index fa4aebd8a5..4b0cf02c0a 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; @@ -20,7 +21,9 @@ namespace osu.Game.Skinning public abstract SampleChannel GetSample(ISampleInfo sampleInfo); - public abstract Texture GetTexture(string componentName); + public Texture GetTexture(string componentName) => GetTexture(componentName, default, default); + + public abstract Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT); public abstract IBindable GetConfig(TLookup lookup); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index d65c74ef62..e1f713882a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Platform; @@ -130,7 +131,7 @@ namespace osu.Game.Skinning public Drawable GetDrawableComponent(ISkinComponent component) => CurrentSkin.Value.GetDrawableComponent(component); - public Texture GetTexture(string componentName) => CurrentSkin.Value.GetTexture(componentName); + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => CurrentSkin.Value.GetTexture(componentName, wrapModeS, wrapModeT); public SampleChannel GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo); diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 1c01bbf1ab..adf62ed452 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -7,6 +7,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; @@ -47,13 +48,13 @@ namespace osu.Game.Skinning return fallbackSource?.GetDrawableComponent(component); } - public Texture GetTexture(string componentName) + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { Texture sourceTexture; - if (AllowTextureLookup(componentName) && (sourceTexture = skin?.GetTexture(componentName)) != null) + if (AllowTextureLookup(componentName) && (sourceTexture = skin?.GetTexture(componentName, wrapModeS, wrapModeT)) != null) return sourceTexture; - return fallbackSource?.GetTexture(componentName); + return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT); } public SampleChannel GetSample(ISampleInfo sampleInfo) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index ea7cdaaac6..81c13112d0 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; @@ -157,7 +158,7 @@ namespace osu.Game.Tests.Visual this.extrapolateAnimations = extrapolateAnimations; } - public override Texture GetTexture(string componentName) + public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { // extrapolate frames to test longer animations if (extrapolateAnimations) @@ -165,10 +166,10 @@ namespace osu.Game.Tests.Visual var match = Regex.Match(componentName, "-([0-9]*)"); if (match.Length > 0 && int.TryParse(match.Groups[1].Value, out var number) && number < 60) - return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}")); + return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT); } - return base.GetTexture(componentName); + return base.GetTexture(componentName, wrapModeS, wrapModeT); } } } From fc0f3f917184f86ae99239926edad41dd4fa1288 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Jul 2020 16:55:01 +0900 Subject: [PATCH 2270/6909] Fix taiko drumroll bodies behaving badly with edge alphas --- osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs index 8531f3cefd..8223e3bc01 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Skinning; @@ -34,13 +35,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning Anchor = Anchor.CentreRight, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, - Texture = skin.GetTexture("taiko-roll-end"), + Texture = skin.GetTexture("taiko-roll-end", WrapMode.ClampToEdge, WrapMode.ClampToEdge), FillMode = FillMode.Fit, }, body = new Sprite { RelativeSizeAxes = Axes.Both, - Texture = skin.GetTexture("taiko-roll-middle"), + Texture = skin.GetTexture("taiko-roll-middle", WrapMode.ClampToEdge, WrapMode.ClampToEdge), }, headCircle = new LegacyCirclePiece { From 222a22182e849c36cb092f10d73da75a10219f97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jul 2020 17:03:57 +0900 Subject: [PATCH 2271/6909] Fix double-click incorrectly firing across disparate targets --- .../Edit/Compose/Components/BlueprintContainer.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index c6e228262f..1b189fcf48 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -121,14 +121,19 @@ namespace osu.Game.Screens.Edit.Compose.Components return e.Button == MouseButton.Left; } + private SelectionBlueprint clickedBlueprint; + protected override bool OnClick(ClickEvent e) { if (e.Button == MouseButton.Right) return false; + // store for double-click handling + clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered); + // Deselection should only occur if no selected blueprints are hovered // A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection - if (endClickSelection() || selectionHandler.SelectedBlueprints.Any(b => b.IsHovered)) + if (endClickSelection() || clickedBlueprint != null) return true; deselectAll(); @@ -140,9 +145,8 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Button == MouseButton.Right) return false; - SelectionBlueprint clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered); - - if (clickedBlueprint == null) + // ensure the blueprint which was hovered for the first click is still the hovered blueprint. + if (clickedBlueprint == null || selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint) return false; editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); From afca535abe5951a4eed0543dfc1da679d525242d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Jul 2020 16:57:33 +0900 Subject: [PATCH 2272/6909] Add texture wrapping support to GetAnimation() --- osu.Game/Skinning/IAnimationTimeReference.cs | 3 ++- osu.Game/Skinning/LegacySkinExtensions.cs | 10 ++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs index 4ed5ef64c3..7e52bb8176 100644 --- a/osu.Game/Skinning/IAnimationTimeReference.cs +++ b/osu.Game/Skinning/IAnimationTimeReference.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.OpenGL.Textures; using osu.Framework.Timing; namespace osu.Game.Skinning @@ -11,7 +12,7 @@ namespace osu.Game.Skinning /// /// /// This should not be used to start an animation immediately at the current time. - /// To do so, use with startAtCurrentTime = true instead. + /// 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 549571dec4..7cf41ef3c1 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -15,6 +16,11 @@ namespace osu.Game.Skinning { public static Drawable GetAnimation(this ISkin source, string componentName, bool animatable, bool looping, bool applyConfigFrameRate = false, string animationSeparator = "-", bool startAtCurrentTime = true, double? frameLength = null) + => source.GetAnimation(componentName, default, default, animatable, looping, applyConfigFrameRate, animationSeparator, startAtCurrentTime, frameLength); + + public static Drawable GetAnimation(this ISkin source, string componentName, WrapMode wrapModeS, WrapMode wrapModeT, bool animatable, bool looping, bool applyConfigFrameRate = false, + string animationSeparator = "-", + bool startAtCurrentTime = true, double? frameLength = null) { Texture texture; @@ -38,7 +44,7 @@ namespace osu.Game.Skinning } // if an animation was not allowed or not found, fall back to a sprite retrieval. - if ((texture = source.GetTexture(componentName)) != null) + if ((texture = source.GetTexture(componentName, wrapModeS, wrapModeT)) != null) return new Sprite { Texture = texture }; return null; @@ -47,7 +53,7 @@ namespace osu.Game.Skinning { for (int i = 0; true; i++) { - if ((texture = source.GetTexture($"{componentName}{animationSeparator}{i}")) == null) + if ((texture = source.GetTexture($"{componentName}{animationSeparator}{i}", wrapModeS, wrapModeT)) == null) break; yield return texture; From b3769112fb5b774dbfc64efdbb2b932bd754760f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 17 Jul 2020 17:08:26 +0900 Subject: [PATCH 2273/6909] Fix mania hold note bodies behaving badly with edge alphas --- osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs | 3 ++- osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index a749f80855..9f716428c0 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; @@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Skinning string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value ?? $"mania-note{FallbackColumnIndex}L"; - sprite = skin.GetAnimation(imageName, true, true).With(d => + sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => { if (d == null) return; diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs index 515c941d65..283b04373b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.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.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.UI.Scrolling; @@ -92,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Skinning string noteImage = GetColumnSkinConfig(skin, lookup)?.Value ?? $"mania-note{FallbackColumnIndex}{suffix}"; - return skin.GetTexture(noteImage); + return skin.GetTexture(noteImage, WrapMode.ClampToEdge, WrapMode.ClampToEdge); } } } From 94b3a6462bf2956b719161f6397c829f42159846 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jul 2020 17:16:37 +0900 Subject: [PATCH 2274/6909] Update actually incorrect test steps --- osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs index 41d1459103..3a19eabe81 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSeekSnapping.cs @@ -175,13 +175,13 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("Time = 50", () => Clock.CurrentTime == 50); AddStep("Seek(49.999)", () => Clock.Seek(49.999)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 50", () => Clock.CurrentTime == 50); + AddAssert("Time = 100", () => Clock.CurrentTime == 100); AddStep("Seek(99)", () => Clock.Seek(99)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddAssert("Time = 100", () => Clock.CurrentTime == 100); AddStep("Seek(99.999)", () => Clock.Seek(99.999)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); - AddAssert("Time = 100", () => Clock.CurrentTime == 100); + AddAssert("Time = 100", () => Clock.CurrentTime == 150); AddStep("Seek(174)", () => Clock.Seek(174)); AddStep("SeekForward, Snap", () => Clock.SeekForward(true)); AddAssert("Time = 175", () => Clock.CurrentTime == 175); From ea6f257dc213d4ae1430d3a70e6c8edde3a0f4ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jul 2020 17:48:27 +0900 Subject: [PATCH 2275/6909] Add a display of count of selected objects in editor --- .../Compose/Components/BlueprintContainer.cs | 1 + .../Compose/Components/SelectionHandler.cs | 60 ++++++++++++++----- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index c6e228262f..33c33267a3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -64,6 +64,7 @@ namespace osu.Game.Screens.Edit.Compose.Components DragBox = CreateDragBox(select), selectionHandler, SelectionBlueprints = CreateSelectionBlueprintContainer(), + selectionHandler.CreateProxy(), DragBox.CreateProxy().With(p => p.Depth = float.MinValue) }); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 38893f90a8..8e7afdaf02 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -15,6 +15,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Input.States; using osu.Game.Audio; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -35,7 +36,9 @@ namespace osu.Game.Screens.Edit.Compose.Components public IEnumerable SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject); - private Drawable outline; + private Drawable content; + + private OsuSpriteText selectionDetailsText; [Resolved(CanBeNull = true)] protected EditorBeatmap EditorBeatmap { get; private set; } @@ -55,16 +58,41 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load(OsuColour colours) { - InternalChild = outline = new Container + InternalChild = content = new Container { - Masking = true, - BorderThickness = BORDER_RADIUS, - BorderColour = colours.Yellow, - Child = new Box + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0 + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + BorderThickness = BORDER_RADIUS, + BorderColour = colours.Yellow, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + AlwaysPresent = true, + Alpha = 0 + } + }, + new Container + { + Name = "info text", + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = colours.Yellow, + RelativeSizeAxes = Axes.Both, + }, + selectionDetailsText = new OsuSpriteText + { + Padding = new MarginPadding(2), + Colour = colours.Gray0, + } + } + } } }; } @@ -131,9 +159,7 @@ namespace osu.Game.Screens.Edit.Compose.Components selectedBlueprints.Remove(blueprint); 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) - UpdateVisibility(); + UpdateVisibility(); } /// @@ -179,7 +205,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// internal void UpdateVisibility() { - if (selectedBlueprints.Count > 0) + int count = selectedBlueprints.Count; + + selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; + + if (count > 0) Show(); else Hide(); @@ -205,8 +235,8 @@ namespace osu.Game.Screens.Edit.Compose.Components topLeft -= new Vector2(5); bottomRight += new Vector2(5); - outline.Size = bottomRight - topLeft; - outline.Position = topLeft; + content.Size = bottomRight - topLeft; + content.Position = topLeft; } #endregion From b4b230288bc8592bf4bf3cfd38811b45859fb850 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jul 2020 17:51:39 +0900 Subject: [PATCH 2276/6909] Shift hue of selection handler box to not collide with blueprints --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 8e7afdaf02..9700cb8c8e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Edit.Compose.Components RelativeSizeAxes = Axes.Both, Masking = true, BorderThickness = BORDER_RADIUS, - BorderColour = colours.Yellow, + BorderColour = colours.YellowDark, Child = new Box { RelativeSizeAxes = Axes.Both, @@ -83,13 +83,14 @@ namespace osu.Game.Screens.Edit.Compose.Components { new Box { - Colour = colours.Yellow, + Colour = colours.YellowDark, RelativeSizeAxes = Axes.Both, }, selectionDetailsText = new OsuSpriteText { Padding = new MarginPadding(2), Colour = colours.Gray0, + Font = OsuFont.Default.With(size: 11) } } } From a39c4236c7d8bdaaa7e86f50de4eb8282c4e0999 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jul 2020 19:08:50 +0900 Subject: [PATCH 2277/6909] Fix multiple issues and standardise transforms --- osu.Game/Screens/Play/GameplayMenuOverlay.cs | 7 ++++--- osu.Game/Screens/Play/PauseOverlay.cs | 15 ++++++++++----- osu.Game/Skinning/SkinnableSound.cs | 6 ++++-- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 6b37135c86..57403a0987 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -24,7 +24,8 @@ namespace osu.Game.Screens.Play { public abstract class GameplayMenuOverlay : OverlayContainer, IKeyBindingHandler { - private const int transition_duration = 200; + protected const int TRANSITION_DURATION = 200; + private const int button_height = 70; private const float background_alpha = 0.75f; @@ -156,8 +157,8 @@ namespace osu.Game.Screens.Play } } - protected override void PopIn() => this.FadeIn(transition_duration, Easing.In); - protected override void PopOut() => this.FadeOut(transition_duration, Easing.In); + protected override void PopIn() => this.FadeIn(TRANSITION_DURATION, Easing.In); + protected override void PopOut() => this.FadeOut(TRANSITION_DURATION, Easing.In); // Don't let mouse down events through the overlay or people can click circles while paused. protected override bool OnMouseDown(MouseDownEvent e) => true; diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 7b3fba7ddf..e74585990a 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -16,6 +16,8 @@ namespace osu.Game.Screens.Play { public Action OnResume; + public override bool IsPresent => base.IsPresent || pauseLoop.IsPlaying; + public override string Header => "paused"; public override string Description => "you're not going to do what i think you're going to do, are ya?"; @@ -23,6 +25,8 @@ namespace osu.Game.Screens.Play protected override Action BackAction => () => InternalButtons.Children.First().Click(); + private const float minimum_volume = 0.0001f; + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -34,18 +38,20 @@ namespace osu.Game.Screens.Play { Looping = true, }); + // PopIn is called before updating the skin, and when a sample is updated, its "playing" value is reset // the sample must be played again pauseLoop.OnSkinChanged += () => pauseLoop.Play(); + + // SkinnableSound only plays a sound if its aggregate volume is > 0, so the volume must be turned up before playing it + pauseLoop.VolumeTo(minimum_volume); } protected override void PopIn() { base.PopIn(); - //SkinnableSound only plays a sound if its aggregate volume is > 0, so the volume must be turned up before playing it - pauseLoop.VolumeTo(0.00001f); - pauseLoop.VolumeTo(1.0f, 400, Easing.InQuint); + pauseLoop.VolumeTo(1.0f, TRANSITION_DURATION, Easing.InQuint); pauseLoop.Play(); } @@ -53,8 +59,7 @@ namespace osu.Game.Screens.Play { base.PopOut(); - var transformSeq = pauseLoop.VolumeTo(0.0f, 190, Easing.OutQuad); - transformSeq.Finally(_ => pauseLoop.Stop()); + pauseLoop.VolumeTo(minimum_volume, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop()); } } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 24d6648273..fb27ba0550 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -45,6 +45,10 @@ namespace osu.Game.Skinning public BindableNumber Tempo => samplesContainer.Tempo; + public override bool IsPresent => Scheduler.HasPendingTasks || IsPlaying; + + public bool IsPlaying => samplesContainer.Any(s => s.Playing); + /// /// Smoothly adjusts over time. /// @@ -94,8 +98,6 @@ namespace osu.Game.Skinning public void Stop() => samplesContainer.ForEach(c => c.Stop()); - public override bool IsPresent => Scheduler.HasPendingTasks; - protected override void SkinChanged(ISkinSource skin, bool allowFallback) { var channels = hitSamples.Select(s => From 77143952a91da519665c0a13a19abe01fb97275a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 17 Jul 2020 19:17:48 +0900 Subject: [PATCH 2278/6909] Add test coverage --- .../Visual/Gameplay/TestScenePause.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 1961a224c1..420bf29429 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Rulesets; using osu.Game.Screens.Play; +using osu.Game.Skinning; using osuTK; using osuTK.Input; @@ -221,6 +222,31 @@ namespace osu.Game.Tests.Visual.Gameplay confirmExited(); } + [Test] + public void TestPauseSoundLoop() + { + AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000)); + + SkinnableSound getLoop() => Player.ChildrenOfType().FirstOrDefault()?.ChildrenOfType().FirstOrDefault(); + + pauseAndConfirm(); + AddAssert("loop is playing", () => getLoop().IsPlaying); + + resumeAndConfirm(); + AddUntilStep("loop is stopped", () => !getLoop().IsPlaying); + + AddUntilStep("pause again", () => + { + Player.Pause(); + return !Player.GameplayClockContainer.GameplayClock.IsRunning; + }); + + AddAssert("loop is playing", () => getLoop().IsPlaying); + + resumeAndConfirm(); + AddUntilStep("loop is stopped", () => !getLoop().IsPlaying); + } + private void pauseAndConfirm() { pause(); From 9857779d424a8caa066a25a6ee5c603150083dfb Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 17 Jul 2020 16:12:01 +0200 Subject: [PATCH 2279/6909] Added slider for the grow/deflate mod --- osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs | 12 +++++++++++- osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs | 12 +++++++++++- osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs | 4 ++-- 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs index 73cb483ef0..60dbe16453 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.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.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; namespace osu.Game.Rulesets.Osu.Mods { @@ -15,6 +17,14 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Hit them at the right size!"; - protected override float StartScale => 2f; + [SettingSource("Starting size", "The object starting size")] + public override BindableNumber StartScale { get; } = new BindableFloat + { + MinValue = 1f, + MaxValue = 15f, + Default = 2f, + Value = 2f, + Precision = 0.1f, + }; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs index f08d4e8f5e..8e8268f8cf 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.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.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; namespace osu.Game.Rulesets.Osu.Mods { @@ -15,6 +17,14 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Hit them at the right size!"; - protected override float StartScale => 0.5f; + [SettingSource("Starting size", "The object starting size")] + public override BindableNumber StartScale { get; } = new BindableFloat + { + MinValue = 0f, + MaxValue = 0.99f, + Default = 0.5f, + Value = 0.5f, + Precision = 0.01f, + }; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index 42ddddc4dd..06ba4cde4a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; - protected virtual float StartScale => 1; + public abstract BindableNumber StartScale { get; } protected virtual float EndScale => 1; @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.Mods case DrawableHitCircle _: { using (drawable.BeginAbsoluteSequence(h.StartTime - h.TimePreempt)) - drawable.ScaleTo(StartScale).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine); + drawable.ScaleTo(StartScale.Value).Then().ScaleTo(EndScale, h.TimePreempt, Easing.OutSine); break; } } From a6cf77beae9236f2893068ad2c42b379c166749a Mon Sep 17 00:00:00 2001 From: Fabian Date: Fri, 17 Jul 2020 17:53:20 +0200 Subject: [PATCH 2280/6909] Clarified what the slider value is --- osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs index 60dbe16453..076fde08f8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Hit them at the right size!"; - [SettingSource("Starting size", "The object starting size")] + [SettingSource("Starting size modifier", "The object starting size modifier")] public override BindableNumber StartScale { get; } = new BindableFloat { MinValue = 1f, diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs index 8e8268f8cf..5288bdd62c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Hit them at the right size!"; - [SettingSource("Starting size", "The object starting size")] + [SettingSource("Starting size modifier", "The object starting size modifier")] public override BindableNumber StartScale { get; } = new BindableFloat { MinValue = 0f, From 0975610bf77ac5fd574e71fbe4f4301f6f95c952 Mon Sep 17 00:00:00 2001 From: Fabian Date: Sat, 18 Jul 2020 02:21:55 +0200 Subject: [PATCH 2281/6909] Increased maximum start modifier higher (15x -> 25x) Tried playing around with higher values and personally had quite fun when the circles covered the whole screen so I raised the max modifier to 25. Works best with an AR of <6. --- osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs index 076fde08f8..6302d47843 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override BindableNumber StartScale { get; } = new BindableFloat { MinValue = 1f, - MaxValue = 15f, + MaxValue = 25f, Default = 2f, Value = 2f, Precision = 0.1f, From 20096f9aea4fedd471f80c3c12d1d53647c2d70b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Jul 2020 11:44:18 +0900 Subject: [PATCH 2282/6909] Remove remaining per-Update transform in OsuLogo to reduce allocations --- osu.Game/Screens/Menu/OsuLogo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 089906c342..34d49685d2 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -330,7 +330,7 @@ namespace osu.Game.Screens.Menu if (Beatmap.Value.Track.IsRunning) { var maxAmplitude = lastBeatIndex >= 0 ? Beatmap.Value.Track.CurrentAmplitudes.Maximum : 0; - logoAmplitudeContainer.ScaleTo(1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 75, Easing.OutQuint); + logoAmplitudeContainer.Scale = new Vector2((float)Interpolation.Damp(logoAmplitudeContainer.Scale.X, 1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 0.5f, Time.Elapsed)); if (maxAmplitude > velocity_adjust_cutoff) triangles.Velocity = 1 + Math.Max(0, maxAmplitude - velocity_adjust_cutoff) * 50; From 8147e67f5337b9bfe6d29cbfc7a6c8bcb0015f7c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Jul 2020 11:53:04 +0900 Subject: [PATCH 2283/6909] Use static instances in all fallback ControlPoint lookups to reduce allocations --- .../ControlPoints/ControlPointInfo.cs | 16 ++++++------- .../ControlPoints/DifficultyControlPoint.cs | 5 ++++ .../ControlPoints/EffectControlPoint.cs | 6 +++++ .../ControlPoints/TimingControlPoint.cs | 12 ++++++++++ .../Containers/BeatSyncedContainer.cs | 23 ++----------------- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index af6ca24165..49f1052248 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -64,14 +64,14 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time to find the difficulty control point at. /// The difficulty control point. - public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time); + public DifficultyControlPoint DifficultyPointAt(double time) => binarySearchWithFallback(DifficultyPoints, time, DifficultyControlPoint.DEFAULT); /// /// Finds the effect control point that is active at . /// /// The time to find the effect control point at. /// The effect control point. - public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time); + public EffectControlPoint EffectPointAt(double time) => binarySearchWithFallback(EffectPoints, time, EffectControlPoint.DEFAULT); /// /// Finds the sound control point that is active at . @@ -92,21 +92,21 @@ namespace osu.Game.Beatmaps.ControlPoints /// [JsonIgnore] public double BPMMaximum => - 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength; + 60000 / (TimingPoints.OrderBy(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength; /// /// Finds the minimum BPM represented by any timing control point. /// [JsonIgnore] public double BPMMinimum => - 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? new TimingControlPoint()).BeatLength; + 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength; /// /// Finds the mode BPM (most common BPM) represented by the control points. /// [JsonIgnore] public double BPMMode => - 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? new TimingControlPoint()).BeatLength; + 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength; /// /// Remove all s and return to a pristine state. @@ -170,12 +170,12 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The list to search. /// The time to find the control point at. - /// The control point to use when is before any control points. If null, a new control point will be constructed. + /// The control point to use when is before any control points. /// The active control point at , or a fallback if none found. - private T binarySearchWithFallback(IReadOnlyList list, double time, T prePoint = null) + private T binarySearchWithFallback(IReadOnlyList list, double time, T fallback) where T : ControlPoint, new() { - return binarySearch(list, time) ?? prePoint ?? new T(); + return binarySearch(list, time) ?? fallback; } /// diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 2448b2b25c..1d38790f87 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -7,6 +7,11 @@ namespace osu.Game.Beatmaps.ControlPoints { public class DifficultyControlPoint : ControlPoint { + public static readonly DifficultyControlPoint DEFAULT = new DifficultyControlPoint + { + SpeedMultiplierBindable = { Disabled = true }, + }; + /// /// The speed multiplier at this control point. /// diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 9b69147468..9e8e3978be 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -7,6 +7,12 @@ namespace osu.Game.Beatmaps.ControlPoints { public class EffectControlPoint : ControlPoint { + public static readonly EffectControlPoint DEFAULT = new EffectControlPoint + { + KiaiModeBindable = { Disabled = true }, + OmitFirstBarLineBindable = { Disabled = true } + }; + /// /// Whether the first bar line of this control point is ignored. /// diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 1927dd6575..c3a32c4410 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -13,6 +13,18 @@ namespace osu.Game.Beatmaps.ControlPoints /// public readonly Bindable TimeSignatureBindable = new Bindable(TimeSignatures.SimpleQuadruple) { Default = TimeSignatures.SimpleQuadruple }; + /// + /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing. + /// + private const double default_beat_length = 60000.0 / 60.0; + + public static readonly TimingControlPoint DEFAULT = new TimingControlPoint + { + BeatLength = default_beat_length, + BeatLengthBindable = { Disabled = true }, + TimeSignatureBindable = { Disabled = true } + }; + /// /// The time signature at this control point. /// diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index dd5c41285a..df063f57d5 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -43,14 +43,6 @@ namespace osu.Game.Graphics.Containers /// public double MinimumBeatLength { get; set; } - /// - /// Default length of a beat in milliseconds. Used whenever there is no beatmap or track playing. - /// - private const double default_beat_length = 60000.0 / 60.0; - - private TimingControlPoint defaultTiming; - private EffectControlPoint defaultEffect; - protected bool IsBeatSyncedWithTrack { get; private set; } protected override void Update() @@ -81,8 +73,8 @@ namespace osu.Game.Graphics.Containers if (timingPoint == null || !IsBeatSyncedWithTrack) { currentTrackTime = Clock.CurrentTime; - timingPoint = defaultTiming; - effectPoint = defaultEffect; + timingPoint = TimingControlPoint.DEFAULT; + effectPoint = EffectControlPoint.DEFAULT; } double beatLength = timingPoint.BeatLength / Divisor; @@ -116,17 +108,6 @@ namespace osu.Game.Graphics.Containers private void load(IBindable beatmap) { Beatmap.BindTo(beatmap); - - defaultTiming = new TimingControlPoint - { - BeatLength = default_beat_length, - }; - - defaultEffect = new EffectControlPoint - { - KiaiMode = false, - OmitFirstBarLine = false - }; } protected virtual void OnNewBeat(int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes) From 2f16b448ea8619ea6ea8d34231ff94f040702e53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Jul 2020 12:03:49 +0900 Subject: [PATCH 2284/6909] Set beatLength inline --- osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index c3a32c4410..9345299c3a 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -20,8 +20,11 @@ namespace osu.Game.Beatmaps.ControlPoints public static readonly TimingControlPoint DEFAULT = new TimingControlPoint { - BeatLength = default_beat_length, - BeatLengthBindable = { Disabled = true }, + BeatLengthBindable = + { + Value = default_beat_length, + Disabled = true + }, TimeSignatureBindable = { Disabled = true } }; From 7250bc351d3068ed767d73cf67a663135119da65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 18 Jul 2020 12:06:41 +0900 Subject: [PATCH 2285/6909] Remove unnecessary new() specification --- 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 49f1052248..55a04e5ee8 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -173,7 +173,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// The control point to use when is before any control points. /// The active control point at , or a fallback if none found. private T binarySearchWithFallback(IReadOnlyList list, double time, T fallback) - where T : ControlPoint, new() + where T : ControlPoint { return binarySearch(list, time) ?? fallback; } From 81d95f8584b21f8b656ab522107a130acbe29941 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 18 Jul 2020 20:24:38 +0300 Subject: [PATCH 2286/6909] Implement UserBrickPanel component --- .../Visual/Online/TestSceneSocialOverlay.cs | 84 ------ .../Visual/Online/TestSceneUserPanel.cs | 13 + .../Dashboard/Friends/FriendDisplay.cs | 3 + .../OverlayPanelDisplayStyleControl.cs | 7 +- .../Sections/General/LoginSettings.cs | 2 +- osu.Game/Overlays/SocialOverlay.cs | 242 ------------------ osu.Game/Users/ExtendedUserPanel.cs | 148 +++++++++++ osu.Game/Users/UserBrickPanel.cs | 65 +++++ osu.Game/Users/UserGridPanel.cs | 2 +- osu.Game/Users/UserListPanel.cs | 2 +- osu.Game/Users/UserPanel.cs | 130 +--------- 11 files changed, 241 insertions(+), 457 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs delete mode 100644 osu.Game/Overlays/SocialOverlay.cs create mode 100644 osu.Game/Users/ExtendedUserPanel.cs create mode 100644 osu.Game/Users/UserBrickPanel.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs deleted file mode 100644 index 77e77d90c1..0000000000 --- a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs +++ /dev/null @@ -1,84 +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.Game.Overlays; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual.Online -{ - [TestFixture] - public class TestSceneSocialOverlay : OsuTestScene - { - protected override bool UseOnlineAPI => true; - - public TestSceneSocialOverlay() - { - SocialOverlay s = new SocialOverlay - { - Users = new[] - { - new User - { - Username = @"flyte", - Id = 3103765, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", - }, - new User - { - Username = @"Cookiezi", - Id = 124493, - Country = new Country { FlagName = @"KR" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", - }, - new User - { - Username = @"Angelsim", - Id = 1777162, - Country = new Country { FlagName = @"KR" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - }, - new User - { - Username = @"Rafis", - Id = 2558286, - Country = new Country { FlagName = @"PL" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg", - }, - new User - { - Username = @"hvick225", - Id = 50265, - Country = new Country { FlagName = @"TW" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", - }, - new User - { - Username = @"peppy", - Id = 2, - Country = new Country { FlagName = @"AU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" - }, - new User - { - Username = @"filsdelama", - Id = 2831793, - Country = new Country { FlagName = @"FR" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c7.jpg" - }, - new User - { - Username = @"_index", - Id = 652457, - Country = new Country { FlagName = @"RU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c8.jpg" - }, - }, - }; - Add(s); - - AddStep(@"toggle", s.ToggleVisibility); - } - } -} diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs index f763e50067..c2e9945c99 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserPanel.cs @@ -42,6 +42,19 @@ namespace osu.Game.Tests.Visual.Online Spacing = new Vector2(10f), Children = new Drawable[] { + new UserBrickPanel(new User + { + Username = @"flyte", + Id = 3103765, + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" + }), + new UserBrickPanel(new User + { + Username = @"peppy", + Id = 2, + Colour = "99EB47", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }), flyte = new UserGridPanel(new User { Username = @"flyte", diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 79fda99c73..41b25ee1a5 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -225,6 +225,9 @@ namespace osu.Game.Overlays.Dashboard.Friends case OverlayPanelDisplayStyle.List: return new UserListPanel(user); + + case OverlayPanelDisplayStyle.Brick: + return new UserBrickPanel(user); } } diff --git a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs index 7269007b41..87b9d89d4d 100644 --- a/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs +++ b/osu.Game/Overlays/OverlayPanelDisplayStyleControl.cs @@ -34,6 +34,10 @@ namespace osu.Game.Overlays { Icon = FontAwesome.Solid.Bars }); + AddTabItem(new PanelDisplayTabItem(OverlayPanelDisplayStyle.Brick) + { + Icon = FontAwesome.Solid.Th + }); } protected override TabFillFlowContainer CreateTabFlow() => new TabFillFlowContainer @@ -96,6 +100,7 @@ namespace osu.Game.Overlays public enum OverlayPanelDisplayStyle { Card, - List + List, + Brick } } diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 52b712a40e..34e5da4ef4 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Settings.Sections.General [Resolved] private OsuColour colours { get; set; } - private UserPanel panel; + private UserGridPanel panel; private UserDropdown dropdown; /// diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs deleted file mode 100644 index 1b05142192..0000000000 --- a/osu.Game/Overlays/SocialOverlay.cs +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Bindables; -using osuTK; -using osuTK.Graphics; -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.Overlays.SearchableList; -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 -{ - public class SocialOverlay : SearchableListOverlay - { - private readonly LoadingSpinner loading; - private FillFlowContainer panels; - - 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(); - - private User[] users = Array.Empty(); - - public User[] Users - { - get => users; - set - { - if (users == value) - return; - - users = value ?? Array.Empty(); - - if (LoadState >= LoadState.Ready) - recreatePanels(); - } - } - - public SocialOverlay() - : base(OverlayColourScheme.Pink) - { - Add(loading = new LoadingSpinner()); - - Filter.Search.Current.ValueChanged += text => - { - if (!string.IsNullOrEmpty(text.NewValue)) - { - // force searching in players until searching for friends is supported - Header.Tabs.Current.Value = SocialTab.AllPlayers; - - if (Filter.Tabs.Current.Value != SocialSortCriteria.Rank) - Filter.Tabs.Current.Value = SocialSortCriteria.Rank; - } - }; - - Header.Tabs.Current.ValueChanged += _ => queueUpdate(); - Filter.Tabs.Current.ValueChanged += _ => onFilterUpdate(); - - Filter.DisplayStyleControl.DisplayStyle.ValueChanged += _ => recreatePanels(); - Filter.Dropdown.Current.ValueChanged += _ => recreatePanels(); - - currentQuery.BindTo(Filter.Search.Current); - currentQuery.ValueChanged += query => - { - queryChangedDebounce?.Cancel(); - - if (string.IsNullOrEmpty(query.NewValue)) - queueUpdate(); - else - queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500); - }; - } - - [BackgroundDependencyLoader] - private void load() - { - recreatePanels(); - } - - private APIRequest getUsersRequest; - - private readonly Bindable currentQuery = new Bindable(); - - private ScheduledDelegate queryChangedDebounce; - - private void queueUpdate() => Scheduler.AddOnce(updateSearch); - - private CancellationTokenSource loadCancellation; - - private void updateSearch() - { - queryChangedDebounce?.Cancel(); - - if (!IsLoaded) - return; - - Users = null; - clearPanels(); - getUsersRequest?.Cancel(); - - if (API?.IsLoggedIn != true) - return; - - switch (Header.Tabs.Current.Value) - { - case SocialTab.Friends: - var friendRequest = new GetFriendsRequest(); // TODO filter arguments? - friendRequest.Success += users => Users = users.ToArray(); - API.Queue(getUsersRequest = friendRequest); - break; - - default: - var userRequest = new GetUsersRequest(); // TODO filter arguments! - userRequest.Success += res => Users = res.Users.Select(r => r.User).ToArray(); - API.Queue(getUsersRequest = userRequest); - break; - } - } - - private void recreatePanels() - { - clearPanels(); - - if (Users == null) - { - loading.Hide(); - return; - } - - IEnumerable sortedUsers = Users; - - switch (Filter.Tabs.Current.Value) - { - case SocialSortCriteria.Location: - sortedUsers = sortedUsers.OrderBy(u => u.Country.FullName); - break; - - case SocialSortCriteria.Name: - sortedUsers = sortedUsers.OrderBy(u => u.Username); - break; - } - - if (Filter.Dropdown.Current.Value == SortDirection.Descending) - sortedUsers = sortedUsers.Reverse(); - - var newPanels = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(10f), - Margin = new MarginPadding { Top = 10 }, - ChildrenEnumerable = sortedUsers.Select(u => - { - UserPanel panel; - - switch (Filter.DisplayStyleControl.DisplayStyle.Value) - { - case PanelDisplayStyle.Grid: - panel = new UserGridPanel(u) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Width = 290, - }; - break; - - default: - panel = new UserListPanel(u); - break; - } - - panel.Status.BindTo(u.Status); - panel.Activity.BindTo(u.Activity); - return panel; - }) - }; - - LoadComponentAsync(newPanels, f => - { - if (panels != null) - ScrollFlow.Remove(panels); - - loading.Hide(); - ScrollFlow.Add(panels = newPanels); - }, (loadCancellation = new CancellationTokenSource()).Token); - } - - private void onFilterUpdate() - { - if (Filter.Tabs.Current.Value == SocialSortCriteria.Rank) - { - queueUpdate(); - return; - } - - recreatePanels(); - } - - private void clearPanels() - { - loading.Show(); - - loadCancellation?.Cancel(); - - if (panels != null) - { - panels.Expire(); - panels = null; - } - } - - public override void APIStateChanged(IAPIProvider api, APIState state) - { - switch (state) - { - case APIState.Online: - queueUpdate(); - break; - - default: - Users = null; - clearPanels(); - break; - } - } - } -} diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs new file mode 100644 index 0000000000..5bd98b3fb7 --- /dev/null +++ b/osu.Game/Users/ExtendedUserPanel.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 osuTK; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Framework.Graphics.Sprites; +using osu.Game.Users.Drawables; +using osu.Framework.Input.Events; + +namespace osu.Game.Users +{ + public abstract class ExtendedUserPanel : UserPanel + { + public readonly Bindable Status = new Bindable(); + + public readonly IBindable Activity = new Bindable(); + + protected TextFlowContainer LastVisitMessage { get; private set; } + + private SpriteIcon statusIcon; + private OsuSpriteText statusMessage; + + public ExtendedUserPanel(User user) + : base(user) + { + } + + [BackgroundDependencyLoader] + private void load() + { + BorderColour = ColourProvider?.Light1 ?? Colours.GreyVioletLighter; + + Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value); + Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Status.TriggerChange(); + + // Colour should be applied immediately on first load. + statusIcon.FinishTransforms(); + } + + protected UpdateableAvatar CreateAvatar() => new UpdateableAvatar + { + User = User, + OpenOnClick = { Value = false } + }; + + protected UpdateableFlag CreateFlag() => new UpdateableFlag(User.Country) + { + Size = new Vector2(39, 26) + }; + + 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.CentreRight : Anchor.CentreLeft; + + statusContainer.Add(LastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)).With(text => + { + text.Anchor = alignment; + text.Origin = 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) + { + Shadow = false + }); + } + })); + + statusContainer.Add(statusMessage = new OsuSpriteText + { + Anchor = alignment, + Origin = alignment, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) + }); + + return statusContainer; + } + + private void displayStatus(UserStatus status, UserActivity activity = null) + { + 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)) + { + statusMessage.Text = activity.Status; + statusIcon.FadeColour(activity.GetAppropriateColour(Colours), 500, Easing.OutQuint); + return; + } + + // Otherwise use only status + statusMessage.Text = status.Message; + statusIcon.FadeColour(status.GetAppropriateColour(Colours), 500, Easing.OutQuint); + + return; + } + + // Fallback to web status if local one is null + if (User.IsOnline) + { + Status.Value = new UserStatusOnline(); + return; + } + + Status.Value = new UserStatusOffline(); + } + + protected override bool OnHover(HoverEvent e) + { + BorderThickness = 2; + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + BorderThickness = 0; + base.OnHoverLost(e); + } + } +} diff --git a/osu.Game/Users/UserBrickPanel.cs b/osu.Game/Users/UserBrickPanel.cs new file mode 100644 index 0000000000..f6eabc3b75 --- /dev/null +++ b/osu.Game/Users/UserBrickPanel.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using 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; + +namespace osu.Game.Users +{ + public class UserBrickPanel : UserPanel + { + public UserBrickPanel(User user) + : base(user) + { + AutoSizeAxes = Axes.X; + Height = 23; + CornerRadius = 6; + } + + [BackgroundDependencyLoader] + private void load() + { + Background.FadeTo(0.3f); + } + + protected override Drawable CreateLayout() => new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Margin = new MarginPadding + { + Horizontal = 5 + }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new CircularContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Masking = true, + Width = 4, + Height = 13, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = string.IsNullOrEmpty(User.Colour) ? Color4Extensions.FromHex("0087ca") : Color4Extensions.FromHex(User.Colour) + } + }, + CreateUsername().With(u => + { + u.Anchor = Anchor.CentreLeft; + u.Origin = Anchor.CentreLeft; + u.Font = OsuFont.GetFont(size: 13, weight: FontWeight.Bold); + }) + } + }; + } +} diff --git a/osu.Game/Users/UserGridPanel.cs b/osu.Game/Users/UserGridPanel.cs index e62a834d6d..44dcbc305d 100644 --- a/osu.Game/Users/UserGridPanel.cs +++ b/osu.Game/Users/UserGridPanel.cs @@ -9,7 +9,7 @@ using osuTK; namespace osu.Game.Users { - public class UserGridPanel : UserPanel + public class UserGridPanel : ExtendedUserPanel { private const int margin = 10; diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs index 1c3ae20577..9c95eff739 100644 --- a/osu.Game/Users/UserListPanel.cs +++ b/osu.Game/Users/UserListPanel.cs @@ -12,7 +12,7 @@ using osu.Game.Overlays.Profile.Header.Components; namespace osu.Game.Users { - public class UserListPanel : UserPanel + public class UserListPanel : ExtendedUserPanel { public UserListPanel(User user) : base(user) diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 6f59f9e443..94c0c31cfc 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -14,11 +12,8 @@ using osu.Game.Overlays; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Containers; -using osu.Game.Users.Drawables; using JetBrains.Annotations; -using osu.Framework.Input.Events; namespace osu.Game.Users { @@ -26,21 +21,12 @@ namespace osu.Game.Users { public readonly User User; - public readonly Bindable Status = new Bindable(); - - public readonly IBindable Activity = new Bindable(); - public new Action Action; protected Action ViewProfile { get; private set; } protected DelayedLoadUnloadWrapper Background { get; private set; } - protected TextFlowContainer LastVisitMessage { get; private set; } - - private SpriteIcon statusIcon; - private OsuSpriteText statusMessage; - protected UserPanel(User user) { if (user == null) @@ -53,23 +39,22 @@ namespace osu.Game.Users private UserProfileOverlay profileOverlay { get; set; } [Resolved(canBeNull: true)] - private OverlayColourProvider colourProvider { get; set; } + protected OverlayColourProvider ColourProvider { get; private set; } [Resolved] - private OsuColour colours { get; set; } + protected OsuColour Colours { get; private set; } [BackgroundDependencyLoader] private void load() { Masking = true; - BorderColour = colourProvider?.Light1 ?? colours.GreyVioletLighter; AddRange(new[] { new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider?.Background5 ?? colours.Gray1 + Colour = ColourProvider?.Background5 ?? Colours.Gray1 }, Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground { @@ -86,9 +71,6 @@ namespace osu.Game.Users CreateLayout() }); - Status.ValueChanged += status => displayStatus(status.NewValue, Activity.Value); - Activity.ValueChanged += activity => displayStatus(Status.Value, activity.NewValue); - base.Action = ViewProfile = () => { Action?.Invoke(); @@ -96,41 +78,9 @@ namespace osu.Game.Users }; } - protected override void LoadComplete() - { - base.LoadComplete(); - Status.TriggerChange(); - - // Colour should be applied immediately on first load. - statusIcon.FinishTransforms(); - } - - 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: 16, weight: FontWeight.Bold), @@ -138,80 +88,6 @@ namespace osu.Game.Users 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.CentreRight : Anchor.CentreLeft; - - statusContainer.Add(LastVisitMessage = new TextFlowContainer(t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)).With(text => - { - text.Anchor = alignment; - text.Origin = 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) - { - Shadow = false - }); - } - })); - - statusContainer.Add(statusMessage = new OsuSpriteText - { - Anchor = alignment, - Origin = alignment, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold) - }); - - return statusContainer; - } - - private void displayStatus(UserStatus status, UserActivity activity = null) - { - 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)) - { - statusMessage.Text = activity.Status; - statusIcon.FadeColour(activity.GetAppropriateColour(colours), 500, Easing.OutQuint); - return; - } - - // Otherwise use only status - statusMessage.Text = status.Message; - statusIcon.FadeColour(status.GetAppropriateColour(colours), 500, Easing.OutQuint); - - return; - } - - // Fallback to web status if local one is null - if (User.IsOnline) - { - Status.Value = new UserStatusOnline(); - return; - } - - Status.Value = new UserStatusOffline(); - } - public MenuItem[] ContextMenuItems => new MenuItem[] { new OsuMenuItem("View Profile", MenuItemType.Highlighted, ViewProfile), From 753b1f3401757cb6cd71b1cece3b2499a59f1328 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 18 Jul 2020 20:26:15 +0300 Subject: [PATCH 2287/6909] Make ctor protected --- osu.Game/Users/ExtendedUserPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/ExtendedUserPanel.cs b/osu.Game/Users/ExtendedUserPanel.cs index 5bd98b3fb7..2604815751 100644 --- a/osu.Game/Users/ExtendedUserPanel.cs +++ b/osu.Game/Users/ExtendedUserPanel.cs @@ -25,7 +25,7 @@ namespace osu.Game.Users private SpriteIcon statusIcon; private OsuSpriteText statusMessage; - public ExtendedUserPanel(User user) + protected ExtendedUserPanel(User user) : base(user) { } From 3e773fde27d165bd43558ba48d5685c3568d5a26 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 18 Jul 2020 23:15:22 +0300 Subject: [PATCH 2288/6909] Remove SocialOverlay component as never being used --- .../Visual/Online/TestSceneSocialOverlay.cs | 84 ------ osu.Game/Overlays/SocialOverlay.cs | 242 ------------------ 2 files changed, 326 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs delete mode 100644 osu.Game/Overlays/SocialOverlay.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs deleted file mode 100644 index 77e77d90c1..0000000000 --- a/osu.Game.Tests/Visual/Online/TestSceneSocialOverlay.cs +++ /dev/null @@ -1,84 +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.Game.Overlays; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual.Online -{ - [TestFixture] - public class TestSceneSocialOverlay : OsuTestScene - { - protected override bool UseOnlineAPI => true; - - public TestSceneSocialOverlay() - { - SocialOverlay s = new SocialOverlay - { - Users = new[] - { - new User - { - Username = @"flyte", - Id = 3103765, - Country = new Country { FlagName = @"JP" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c1.jpg", - }, - new User - { - Username = @"Cookiezi", - Id = 124493, - Country = new Country { FlagName = @"KR" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c2.jpg", - }, - new User - { - Username = @"Angelsim", - Id = 1777162, - Country = new Country { FlagName = @"KR" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", - }, - new User - { - Username = @"Rafis", - Id = 2558286, - Country = new Country { FlagName = @"PL" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c4.jpg", - }, - new User - { - Username = @"hvick225", - Id = 50265, - Country = new Country { FlagName = @"TW" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c5.jpg", - }, - new User - { - Username = @"peppy", - Id = 2, - Country = new Country { FlagName = @"AU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" - }, - new User - { - Username = @"filsdelama", - Id = 2831793, - Country = new Country { FlagName = @"FR" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c7.jpg" - }, - new User - { - Username = @"_index", - Id = 652457, - Country = new Country { FlagName = @"RU" }, - CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c8.jpg" - }, - }, - }; - Add(s); - - AddStep(@"toggle", s.ToggleVisibility); - } - } -} diff --git a/osu.Game/Overlays/SocialOverlay.cs b/osu.Game/Overlays/SocialOverlay.cs deleted file mode 100644 index 1b05142192..0000000000 --- a/osu.Game/Overlays/SocialOverlay.cs +++ /dev/null @@ -1,242 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Bindables; -using osuTK; -using osuTK.Graphics; -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.Overlays.SearchableList; -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 -{ - public class SocialOverlay : SearchableListOverlay - { - private readonly LoadingSpinner loading; - private FillFlowContainer panels; - - 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(); - - private User[] users = Array.Empty(); - - public User[] Users - { - get => users; - set - { - if (users == value) - return; - - users = value ?? Array.Empty(); - - if (LoadState >= LoadState.Ready) - recreatePanels(); - } - } - - public SocialOverlay() - : base(OverlayColourScheme.Pink) - { - Add(loading = new LoadingSpinner()); - - Filter.Search.Current.ValueChanged += text => - { - if (!string.IsNullOrEmpty(text.NewValue)) - { - // force searching in players until searching for friends is supported - Header.Tabs.Current.Value = SocialTab.AllPlayers; - - if (Filter.Tabs.Current.Value != SocialSortCriteria.Rank) - Filter.Tabs.Current.Value = SocialSortCriteria.Rank; - } - }; - - Header.Tabs.Current.ValueChanged += _ => queueUpdate(); - Filter.Tabs.Current.ValueChanged += _ => onFilterUpdate(); - - Filter.DisplayStyleControl.DisplayStyle.ValueChanged += _ => recreatePanels(); - Filter.Dropdown.Current.ValueChanged += _ => recreatePanels(); - - currentQuery.BindTo(Filter.Search.Current); - currentQuery.ValueChanged += query => - { - queryChangedDebounce?.Cancel(); - - if (string.IsNullOrEmpty(query.NewValue)) - queueUpdate(); - else - queryChangedDebounce = Scheduler.AddDelayed(updateSearch, 500); - }; - } - - [BackgroundDependencyLoader] - private void load() - { - recreatePanels(); - } - - private APIRequest getUsersRequest; - - private readonly Bindable currentQuery = new Bindable(); - - private ScheduledDelegate queryChangedDebounce; - - private void queueUpdate() => Scheduler.AddOnce(updateSearch); - - private CancellationTokenSource loadCancellation; - - private void updateSearch() - { - queryChangedDebounce?.Cancel(); - - if (!IsLoaded) - return; - - Users = null; - clearPanels(); - getUsersRequest?.Cancel(); - - if (API?.IsLoggedIn != true) - return; - - switch (Header.Tabs.Current.Value) - { - case SocialTab.Friends: - var friendRequest = new GetFriendsRequest(); // TODO filter arguments? - friendRequest.Success += users => Users = users.ToArray(); - API.Queue(getUsersRequest = friendRequest); - break; - - default: - var userRequest = new GetUsersRequest(); // TODO filter arguments! - userRequest.Success += res => Users = res.Users.Select(r => r.User).ToArray(); - API.Queue(getUsersRequest = userRequest); - break; - } - } - - private void recreatePanels() - { - clearPanels(); - - if (Users == null) - { - loading.Hide(); - return; - } - - IEnumerable sortedUsers = Users; - - switch (Filter.Tabs.Current.Value) - { - case SocialSortCriteria.Location: - sortedUsers = sortedUsers.OrderBy(u => u.Country.FullName); - break; - - case SocialSortCriteria.Name: - sortedUsers = sortedUsers.OrderBy(u => u.Username); - break; - } - - if (Filter.Dropdown.Current.Value == SortDirection.Descending) - sortedUsers = sortedUsers.Reverse(); - - var newPanels = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(10f), - Margin = new MarginPadding { Top = 10 }, - ChildrenEnumerable = sortedUsers.Select(u => - { - UserPanel panel; - - switch (Filter.DisplayStyleControl.DisplayStyle.Value) - { - case PanelDisplayStyle.Grid: - panel = new UserGridPanel(u) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Width = 290, - }; - break; - - default: - panel = new UserListPanel(u); - break; - } - - panel.Status.BindTo(u.Status); - panel.Activity.BindTo(u.Activity); - return panel; - }) - }; - - LoadComponentAsync(newPanels, f => - { - if (panels != null) - ScrollFlow.Remove(panels); - - loading.Hide(); - ScrollFlow.Add(panels = newPanels); - }, (loadCancellation = new CancellationTokenSource()).Token); - } - - private void onFilterUpdate() - { - if (Filter.Tabs.Current.Value == SocialSortCriteria.Rank) - { - queueUpdate(); - return; - } - - recreatePanels(); - } - - private void clearPanels() - { - loading.Show(); - - loadCancellation?.Cancel(); - - if (panels != null) - { - panels.Expire(); - panels = null; - } - } - - public override void APIStateChanged(IAPIProvider api, APIState state) - { - switch (state) - { - case APIState.Online: - queueUpdate(); - break; - - default: - Users = null; - clearPanels(); - break; - } - } - } -} From a6562be3eb33d35ac6b47a2d19cdaee3f6cc52dd Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 18 Jul 2020 23:27:33 +0300 Subject: [PATCH 2289/6909] Remove unused classes from social namespace --- osu.Game/Overlays/Social/FilterControl.cs | 33 ----------- osu.Game/Overlays/Social/Header.cs | 67 ----------------------- 2 files changed, 100 deletions(-) delete mode 100644 osu.Game/Overlays/Social/FilterControl.cs delete mode 100644 osu.Game/Overlays/Social/Header.cs diff --git a/osu.Game/Overlays/Social/FilterControl.cs b/osu.Game/Overlays/Social/FilterControl.cs deleted file mode 100644 index 93fcc3c401..0000000000 --- a/osu.Game/Overlays/Social/FilterControl.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.Extensions.Color4Extensions; -using osuTK.Graphics; -using osu.Framework.Graphics; -using osu.Game.Overlays.SearchableList; - -namespace osu.Game.Overlays.Social -{ - public class FilterControl : SearchableListFilterControl - { - protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"47253a"); - protected override SocialSortCriteria DefaultTab => SocialSortCriteria.Rank; - protected override SortDirection DefaultCategory => SortDirection.Ascending; - - public FilterControl() - { - Tabs.Margin = new MarginPadding { Top = 10 }; - } - } - - public enum SocialSortCriteria - { - Rank, - Name, - Location, - //[Description("Time Zone")] - //TimeZone, - //[Description("World Map")] - //WorldMap, - } -} diff --git a/osu.Game/Overlays/Social/Header.cs b/osu.Game/Overlays/Social/Header.cs deleted file mode 100644 index 22e0fdcd56..0000000000 --- a/osu.Game/Overlays/Social/Header.cs +++ /dev/null @@ -1,67 +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.Overlays.SearchableList; -using osuTK.Graphics; -using osu.Framework.Graphics; -using osu.Game.Graphics.Sprites; -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 -{ - public class Header : SearchableListHeader - { - private OsuSpriteText browser; - - protected override Color4 BackgroundColour => Color4Extensions.FromHex(@"38202e"); - - protected override SocialTab DefaultTab => SocialTab.AllPlayers; - protected override IconUsage Icon => FontAwesome.Solid.Users; - - protected override Drawable CreateHeaderText() - { - return new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Children = new[] - { - new OsuSpriteText - { - Text = "social ", - Font = OsuFont.GetFont(size: 25), - }, - browser = new OsuSpriteText - { - Text = "browser", - Font = OsuFont.GetFont(size: 25, weight: FontWeight.Light), - }, - }, - }; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - browser.Colour = colours.Pink; - } - } - - public enum SocialTab - { - [Description("All Players")] - AllPlayers, - - [Description("Friends")] - Friends, - //[Description("Team Members")] - //TeamMembers, - //[Description("Chat Channels")] - //ChatChannels, - } -} From 56b0094d4373a48c6e135fdbf23b6e95aec07d5b Mon Sep 17 00:00:00 2001 From: Fabian Date: Sat, 18 Jul 2020 23:10:05 +0200 Subject: [PATCH 2290/6909] Update slider labels & descriptions --- osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs index 6302d47843..ee6a7815e2 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDeflate.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Hit them at the right size!"; - [SettingSource("Starting size modifier", "The object starting size modifier")] + [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")] public override BindableNumber StartScale { get; } = new BindableFloat { MinValue = 1f, diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs index 5288bdd62c..182d6eeb4b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModGrow.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override string Description => "Hit them at the right size!"; - [SettingSource("Starting size modifier", "The object starting size modifier")] + [SettingSource("Starting Size", "The initial size multiplier applied to all objects.")] public override BindableNumber StartScale { get; } = new BindableFloat { MinValue = 0f, From 2025e5418c841a386ba5599164c3c9d017c7814c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 19 Jul 2020 04:10:35 +0300 Subject: [PATCH 2291/6909] Minor visual adjustments --- osu.Game/Users/UserBrickPanel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Users/UserBrickPanel.cs b/osu.Game/Users/UserBrickPanel.cs index f6eabc3b75..9ca7768187 100644 --- a/osu.Game/Users/UserBrickPanel.cs +++ b/osu.Game/Users/UserBrickPanel.cs @@ -16,15 +16,14 @@ namespace osu.Game.Users public UserBrickPanel(User user) : base(user) { - AutoSizeAxes = Axes.X; - Height = 23; + AutoSizeAxes = Axes.Both; CornerRadius = 6; } [BackgroundDependencyLoader] private void load() { - Background.FadeTo(0.3f); + Background.FadeTo(0.2f); } protected override Drawable CreateLayout() => new FillFlowContainer @@ -34,7 +33,8 @@ namespace osu.Game.Users Spacing = new Vector2(5, 0), Margin = new MarginPadding { - Horizontal = 5 + Horizontal = 10, + Vertical = 3, }, Anchor = Anchor.Centre, Origin = Anchor.Centre, From 648e414c14ac0ade2821b91b7101f225a3b8a65f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Jul 2020 11:04:33 +0900 Subject: [PATCH 2292/6909] Update InputHandlers in line with framework changes --- .../Replays/CatchFramedReplayInputHandler.cs | 15 ++++++--------- .../Replays/ManiaFramedReplayInputHandler.cs | 5 ++++- .../Replays/OsuFramedReplayInputHandler.cs | 12 ++++++------ .../Replays/TaikoFramedReplayInputHandler.cs | 5 ++++- .../Visual/Gameplay/TestSceneReplayRecorder.cs | 15 +++------------ .../Visual/Gameplay/TestSceneReplayRecording.cs | 15 +++------------ .../Rulesets/Replays/FramedReplayInputHandler.cs | 3 --- 7 files changed, 26 insertions(+), 44 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs index f122588a2b..24c21fbc84 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs @@ -35,18 +35,15 @@ namespace osu.Game.Rulesets.Catch.Replays } } - public override List GetPendingInputs() + public override void GetPendingInputs(List input) { - if (!Position.HasValue) return new List(); + if (!Position.HasValue) return; - return new List + input.Add(new CatchReplayState { - new CatchReplayState - { - PressedActions = CurrentFrame?.Actions ?? new List(), - CatcherX = Position.Value - }, - }; + PressedActions = CurrentFrame?.Actions ?? new List(), + CatcherX = Position.Value + }); } public class CatchReplayState : ReplayState diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs index 899718b77e..26c4ccf289 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs @@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Mania.Replays protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any(); - public override List GetPendingInputs() => new List { new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() } }; + public override void GetPendingInputs(List input) + { + input.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); + } } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs index b42e9ac187..5c803539c2 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs @@ -36,19 +36,19 @@ namespace osu.Game.Rulesets.Osu.Replays } } - public override List GetPendingInputs() + public override void GetPendingInputs(List input) { - return new List - { + input.Add( new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) - }, + }); + input.Add( new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() - } - }; + }); + ; } } } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs index 97337acc45..7361d4efa8 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs @@ -18,6 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Replays protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any(); - public override List GetPendingInputs() => new List { new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() } }; + public override void GetPendingInputs(List input) + { + input.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index c7455583e4..e473f49826 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -173,19 +173,10 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public override List GetPendingInputs() + public override void GetPendingInputs(List inputs) { - return new List - { - new MousePositionAbsoluteInput - { - Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) - }, - new ReplayState - { - PressedActions = CurrentFrame?.Actions ?? new List() - } - }; + inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) }); + inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index 7822f07957..e891ed617a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -113,19 +113,10 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public override List GetPendingInputs() + public override void GetPendingInputs(List inputs) { - return new List - { - new MousePositionAbsoluteInput - { - Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) - }, - new ReplayState - { - PressedActions = CurrentFrame?.Actions ?? new List() - } - }; + inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) }); + inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); } } diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 55d82c4083..cf5c88b8fd 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using JetBrains.Annotations; -using osu.Framework.Input.StateChanges; using osu.Game.Input.Handlers; using osu.Game.Replays; @@ -69,8 +68,6 @@ namespace osu.Game.Rulesets.Replays return true; } - public override List GetPendingInputs() => new List(); - private const double sixty_frame_time = 1000.0 / 60; protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2; From 72ace508b605dace2f6f3e684aa83fac3bae3c4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Jul 2020 11:37:10 +0900 Subject: [PATCH 2293/6909] Reduce memory allocations in MenuCursorContainer --- osu.Game/Graphics/Cursor/MenuCursorContainer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs index b7ea1ba56a..02bfb3fad6 100644 --- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs +++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -55,7 +54,15 @@ namespace osu.Game.Graphics.Cursor return; } - var newTarget = inputManager.HoveredDrawables.OfType().FirstOrDefault(t => t.ProvidingUserCursor) ?? this; + IProvideCursor newTarget = this; + + foreach (var d in inputManager.HoveredDrawables) + { + if (!(d is IProvideCursor p) || !p.ProvidingUserCursor) continue; + + newTarget = p; + break; + } if (currentTarget == newTarget) return; From a7fcce0bf719a68fda8d5cb4abad429d72828699 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Jul 2020 11:37:38 +0900 Subject: [PATCH 2294/6909] Fix hard crash on notifications firing before NotificationOverlay is ready --- osu.Game/OsuGame.cs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 618049e72c..f4bb10340e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -63,7 +63,8 @@ namespace osu.Game private ChannelManager channelManager; - private NotificationOverlay notifications; + [NotNull] + private readonly NotificationOverlay notifications = new NotificationOverlay(); private NowPlayingOverlay nowPlaying; @@ -82,7 +83,7 @@ namespace osu.Game public virtual Storage GetStorageForStableInstall() => null; - public float ToolbarOffset => Toolbar.Position.Y + Toolbar.DrawHeight; + public float ToolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0); private IdleTracker idleTracker; @@ -250,7 +251,7 @@ namespace osu.Game case LinkAction.OpenEditorTimestamp: case LinkAction.JoinMultiplayerMatch: case LinkAction.Spectate: - waitForReady(() => notifications, _ => notifications?.Post(new SimpleNotification + waitForReady(() => notifications, _ => notifications.Post(new SimpleNotification { Text = @"This link type is not yet supported!", Icon = FontAwesome.Solid.LifeRing, @@ -536,14 +537,14 @@ namespace osu.Game MenuCursorContainer.CanShowCursor = menuScreen?.CursorVisible ?? false; // todo: all archive managers should be able to be looped here. - SkinManager.PostNotification = n => notifications?.Post(n); + SkinManager.PostNotification = n => notifications.Post(n); SkinManager.GetStableStorage = GetStorageForStableInstall; - BeatmapManager.PostNotification = n => notifications?.Post(n); + BeatmapManager.PostNotification = n => notifications.Post(n); BeatmapManager.GetStableStorage = GetStorageForStableInstall; BeatmapManager.PresentImport = items => PresentBeatmap(items.First()); - ScoreManager.PostNotification = n => notifications?.Post(n); + ScoreManager.PostNotification = n => notifications.Post(n); ScoreManager.GetStableStorage = GetStorageForStableInstall; ScoreManager.PresentImport = items => PresentScore(items.First()); @@ -615,12 +616,12 @@ namespace osu.Game loadComponentSingleFile(MusicController = new MusicController(), Add, true); - loadComponentSingleFile(notifications = new NotificationOverlay + loadComponentSingleFile(notifications.With(d => { - GetToolbarHeight = () => ToolbarOffset, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - }, rightFloatingOverlayContent.Add, true); + d.GetToolbarHeight = () => ToolbarOffset; + d.Anchor = Anchor.TopRight; + d.Origin = Anchor.TopRight; + }), rightFloatingOverlayContent.Add, true); loadComponentSingleFile(screenshotManager, Add); From 3823bd8343d7ae3a8e8281ce154cdd6a05031e09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 19 Jul 2020 14:11:21 +0900 Subject: [PATCH 2295/6909] Add back missing default implementations for lookup functions --- osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs | 4 ++-- osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs | 6 ++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 55a04e5ee8..e7788b75f3 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -78,14 +78,14 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The time to find the sound control point at. /// The sound control point. - public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : null); + public SampleControlPoint SamplePointAt(double time) => binarySearchWithFallback(SamplePoints, time, SamplePoints.Count > 0 ? SamplePoints[0] : SampleControlPoint.DEFAULT); /// /// Finds the timing control point that is active at . /// /// The time to find the timing control point at. /// The timing control point. - public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : null); + public TimingControlPoint TimingPointAt(double time) => binarySearchWithFallback(TimingPoints, time, TimingPoints.Count > 0 ? TimingPoints[0] : TimingControlPoint.DEFAULT); /// /// Finds the maximum BPM represented by any timing control point. diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 61851a00d7..c052c04ea0 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -10,6 +10,12 @@ namespace osu.Game.Beatmaps.ControlPoints { public const string DEFAULT_BANK = "normal"; + public static readonly SampleControlPoint DEFAULT = new SampleControlPoint + { + SampleBankBindable = { Disabled = true }, + SampleVolumeBindable = { Disabled = true } + }; + /// /// The default sample bank at this control point. /// From 55d921ef85629cd0b04687497793336abfda88aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jul 2020 15:19:17 +0900 Subject: [PATCH 2296/6909] Improve feel of animation --- .../TestSceneSpinner.cs | 2 +- .../Objects/Drawables/DrawableSpinner.cs | 32 ++++++++++++------- .../Drawables/Pieces/SpinnerBackground.cs | 10 +++--- .../Objects/Drawables/Pieces/SpinnerTicks.cs | 8 ++--- 4 files changed, 31 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 8cb7f3f4b6..67afc45e32 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void testSingle(float circleSize, bool auto = false) { - var spinner = new Spinner { StartTime = Time.Current + 1000, EndTime = Time.Current + 4000 }; + var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 }; spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index be6766509c..3dd98be6fb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Background = new SpinnerBackground { - Alpha = 0.6f, + Alpha = 1f, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Background.AccentColour = normalColour; - completeColour = colours.YellowLight.Opacity(0.75f); + completeColour = colours.YellowLight; Disc.AccentColour = fillColour; circle.Colour = colours.BlueDark; @@ -152,8 +152,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Disc.FadeAccent(completeColour, duration); - Background.FadeAccent(completeColour, duration); - Background.FadeOut(duration); + Background.FadeAccent(completeColour.Darken(1), duration); circle.FadeColour(completeColour, duration); glow.FadeColour(completeColour, duration); @@ -204,14 +203,25 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateInitialTransforms(); - circleContainer.ScaleTo(Spinner.Scale * 0.3f); - circleContainer.ScaleTo(Spinner.Scale, HitObject.TimePreempt / 1.4f, Easing.OutQuint); + circleContainer.ScaleTo(0); + mainContainer.ScaleTo(0); - mainContainer - .ScaleTo(0) - .ScaleTo(Spinner.Scale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt - 150, Easing.OutQuint) - .Then() - .ScaleTo(1, 500, Easing.OutQuint); + using (BeginDelayedSequence(HitObject.TimePreempt / 2, true)) + { + float phaseOneScale = Spinner.Scale * 0.8f; + + circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 2f, Easing.OutQuint); + + mainContainer + .ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt / 2, Easing.OutElasticHalf) + .RotateTo(25, HitObject.TimePreempt + Spinner.Duration); + + using (BeginDelayedSequence(HitObject.TimePreempt / 2, true)) + { + circleContainer.ScaleTo(Spinner.Scale * 1.4f, 400, Easing.OutQuint); + mainContainer.ScaleTo(Spinner.Scale, 400, Easing.OutQuint); + } + } } protected override void UpdateStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs index 77228e28af..bfb9a9e763 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.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.Graphics; 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.Osu.Objects.Drawables.Pieces { public class SpinnerBackground : CircularContainer, IHasAccentColour { - protected Box Disc; + private readonly Box disc; public Color4 AccentColour { - get => Disc.Colour; + get => disc.Colour; set { - Disc.Colour = value; + disc.Colour = value; EdgeEffect = new EdgeEffectParameters { @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Children = new Drawable[] { - Disc = new Box + disc = new Box { Origin = Anchor.Centre, Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs index 676cefb236..0e7dafdbea 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs @@ -20,24 +20,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Anchor = Anchor.Centre; RelativeSizeAxes = Axes.Both; - const float count = 18; + const float count = 8; for (float i = 0; i < count; i++) { Add(new Container { Colour = Color4.Black, - Alpha = 0.4f, + Alpha = 0.2f, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Radius = 10, + Radius = 20, Colour = Color4.Gray.Opacity(0.2f), }, RelativePositionAxes = Axes.Both, Masking = true, CornerRadius = 5, - Size = new Vector2(60, 10), + Size = new Vector2(65, 10), Origin = Anchor.Centre, Position = new Vector2( 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.86f, From 33e58bb7db992e24562ecf38774b09297b6b55cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jul 2020 17:22:17 +0900 Subject: [PATCH 2297/6909] Fix sizing and colour not correct on hit --- .../Objects/Drawables/DrawableSpinner.cs | 55 +++++++++++-------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 3dd98be6fb..b8a290a978 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -147,15 +147,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (Progress >= 1 && !Disc.Complete) { Disc.Complete = true; - - const float duration = 200; - - Disc.FadeAccent(completeColour, duration); - - Background.FadeAccent(completeColour.Darken(1), duration); - - circle.FadeColour(completeColour, duration); - glow.FadeColour(completeColour, duration); + transformFillColour(completeColour, 200); } if (userTriggered || Time.Current < Spinner.EndTime) @@ -208,18 +200,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables using (BeginDelayedSequence(HitObject.TimePreempt / 2, true)) { - float phaseOneScale = Spinner.Scale * 0.8f; + float phaseOneScale = Spinner.Scale * 0.7f; - circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 2f, Easing.OutQuint); + circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 4, Easing.OutQuint); mainContainer - .ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.4f, HitObject.TimePreempt / 2, Easing.OutElasticHalf) - .RotateTo(25, HitObject.TimePreempt + Spinner.Duration); + .ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.5f, HitObject.TimePreempt / 4, Easing.OutQuint) + .RotateTo((float)(25 * Spinner.Duration / 2000), HitObject.TimePreempt + Spinner.Duration); using (BeginDelayedSequence(HitObject.TimePreempt / 2, true)) { - circleContainer.ScaleTo(Spinner.Scale * 1.4f, 400, Easing.OutQuint); - mainContainer.ScaleTo(Spinner.Scale, 400, Easing.OutQuint); + circleContainer.ScaleTo(Spinner.Scale, 400, Easing.OutQuint); + mainContainer.ScaleTo(1, 400, Easing.OutQuint); } } } @@ -228,18 +220,33 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateStateTransforms(state); - var sequence = this.Delay(Spinner.Duration).FadeOut(160); - - switch (state) + using (BeginDelayedSequence(Spinner.Duration, true)) { - case ArmedState.Hit: - sequence.ScaleTo(Scale * 1.2f, 320, Easing.Out); - break; + this.FadeOut(160); - case ArmedState.Miss: - sequence.ScaleTo(Scale * 0.8f, 320, Easing.In); - break; + switch (state) + { + case ArmedState.Hit: + transformFillColour(completeColour, 0); + this.ScaleTo(Scale * 1.2f, 320, Easing.Out); + mainContainer.RotateTo(mainContainer.Rotation + 180, 320); + break; + + case ArmedState.Miss: + this.ScaleTo(Scale * 0.8f, 320, Easing.In); + break; + } } } + + private void transformFillColour(Colour4 colour, double duration) + { + Disc.FadeAccent(colour, duration); + + Background.FadeAccent(colour.Darken(1), duration); + + circle.FadeColour(colour, duration); + glow.FadeColour(colour, duration); + } } } From 4cbc176cb66c3c9616ccc613252c7eee20d9a97f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jul 2020 17:48:35 +0900 Subject: [PATCH 2298/6909] Add less fill and more transparency --- .../Objects/Drawables/DrawableSpinner.cs | 10 ++++-- .../Drawables/Pieces/SpinnerBackground.cs | 8 ++--- .../Objects/Drawables/Pieces/SpinnerTicks.cs | 34 ++++++++++++++----- 3 files changed, 36 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index b8a290a978..fafb3ab69d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -93,7 +93,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Background = new SpinnerBackground { - Alpha = 1f, + Disc = + { + Alpha = 0f, + }, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -125,10 +128,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void load(OsuColour colours) { normalColour = baseColour; + completeColour = colours.YellowLight; Background.AccentColour = normalColour; - - completeColour = colours.YellowLight; + Ticks.AccentColour = normalColour; Disc.AccentColour = fillColour; circle.Colour = colours.BlueDark; @@ -244,6 +247,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Disc.FadeAccent(colour, duration); Background.FadeAccent(colour.Darken(1), duration); + Ticks.FadeAccent(colour, duration); circle.FadeColour(colour, duration); glow.FadeColour(colour, duration); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs index bfb9a9e763..944354abca 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs @@ -12,14 +12,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class SpinnerBackground : CircularContainer, IHasAccentColour { - private readonly Box disc; + public readonly Box Disc; public Color4 AccentColour { - get => disc.Colour; + get => Disc.Colour; set { - disc.Colour = value; + Disc.Colour = value; EdgeEffect = new EdgeEffectParameters { @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Children = new Drawable[] { - disc = new Box + Disc = new Box { Origin = Anchor.Centre, Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs index 0e7dafdbea..95bccbf2fc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.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.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -9,10 +10,11 @@ using osu.Framework.Graphics.Effects; using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class SpinnerTicks : Container + public class SpinnerTicks : Container, IHasAccentColour { public SpinnerTicks() { @@ -26,14 +28,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { Add(new Container { - Colour = Color4.Black, - Alpha = 0.2f, - EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Radius = 20, - Colour = Color4.Gray.Opacity(0.2f), - }, + Alpha = 0.4f, + Blending = BlendingParameters.Additive, RelativePositionAxes = Axes.Both, Masking = true, CornerRadius = 5, @@ -54,5 +50,25 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces }); } } + + public Color4 AccentColour + { + get => Colour; + set + { + Colour = value; + + foreach (var c in Children.OfType()) + { + c.EdgeEffect = + new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Radius = 20, + Colour = value.Opacity(0.8f), + }; + } + } + } } } From e06d3c5812e87037b94a756b1bba98cdcaf09d87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 20 Jul 2020 17:52:59 +0900 Subject: [PATCH 2299/6909] Minor adjustments to tick clearance --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerTicks.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index fafb3ab69d..9c4608cbb1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -208,7 +208,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 4, Easing.OutQuint); mainContainer - .ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.5f, HitObject.TimePreempt / 4, Easing.OutQuint) + .ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.6f, HitObject.TimePreempt / 4, Easing.OutQuint) .RotateTo((float)(25 * Spinner.Duration / 2000), HitObject.TimePreempt + Spinner.Duration); using (BeginDelayedSequence(HitObject.TimePreempt / 2, true)) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs index 95bccbf2fc..ba7e8eae6f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs @@ -33,11 +33,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces RelativePositionAxes = Axes.Both, Masking = true, CornerRadius = 5, - Size = new Vector2(65, 10), + Size = new Vector2(60, 10), Origin = Anchor.Centre, Position = new Vector2( - 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.86f, - 0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.86f + 0.5f + MathF.Sin(i / count * 2 * MathF.PI) / 2 * 0.83f, + 0.5f + MathF.Cos(i / count * 2 * MathF.PI) / 2 * 0.83f ), Rotation = -i / count * 360 + 90, Children = new[] From 6a144fba80a4ec8b8c10367010d5c02f8129d7f6 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Mon, 20 Jul 2020 17:24:17 +0700 Subject: [PATCH 2300/6909] add epilepsy warning in metadata display --- .../Screens/Play/BeatmapMetadataDisplay.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index a84a85ea47..069ac69622 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -49,6 +49,37 @@ namespace osu.Game.Screens.Play } } + private class EpilepsyWarning : FillFlowContainer + { + public EpilepsyWarning() + { + AutoSizeAxes = Axes.Both; + Direction = FillDirection.Vertical; + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Icon = FontAwesome.Solid.ExclamationTriangle, + Size = new Vector2(40) + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "This beatmap contains scenes with rapidly flashing colours." + }, + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = "Please take caution if you are affected by epilepsy." + } + }; + } + } + private readonly WorkingBeatmap beatmap; private readonly Bindable> mods; private readonly Drawable facade; @@ -162,6 +193,12 @@ namespace osu.Game.Screens.Play AutoSizeAxes = Axes.Both, Margin = new MarginPadding { Top = 20 }, Current = mods + }, + new EpilepsyWarning + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Top = 20 }, } }, } From acbf13ddc4050e3462f34ab4c3bf5075aff1f8a5 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Mon, 20 Jul 2020 17:36:42 +0700 Subject: [PATCH 2301/6909] add epilepsy warning field --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 ++ osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 3 +++ 2 files changed, 5 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 3860f12baa..da4c4ca36b 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -91,6 +91,8 @@ namespace osu.Game.Beatmaps public bool LetterboxInBreaks { get; set; } public bool WidescreenStoryboard { get; set; } + public bool EpilepsyWarning { get; set; } + // Editor // This bookmarks stuff is necessary because DB doesn't know how to store int[] [JsonIgnore] diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index b30ec0ca2c..fd17e38a4f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -175,6 +175,9 @@ namespace osu.Game.Beatmaps.Formats case @"WidescreenStoryboard": beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1; break; + case @"EpilepsyWarning": + beatmap.BeatmapInfo.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1; + break; } } From 055e31ddd54b7867589261ae632bc14c5103bf08 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Mon, 20 Jul 2020 18:37:02 +0700 Subject: [PATCH 2302/6909] update minor --- osu.Game/Beatmaps/BeatmapInfo.cs | 1 + osu.Game/Screens/Play/BeatmapMetadataDisplay.cs | 11 ++++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index da4c4ca36b..b7946d53ca 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -91,6 +91,7 @@ namespace osu.Game.Beatmaps public bool LetterboxInBreaks { get; set; } public bool WidescreenStoryboard { get; set; } + [NotMapped] public bool EpilepsyWarning { get; set; } // Editor diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index 069ac69622..f672a4db6c 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -51,8 +51,13 @@ namespace osu.Game.Screens.Play private class EpilepsyWarning : FillFlowContainer { - public EpilepsyWarning() + public EpilepsyWarning(bool warning) { + if (warning) + this.Show(); + else + this.Hide(); + AutoSizeAxes = Axes.Both; Direction = FillDirection.Vertical; Children = new Drawable[] @@ -62,7 +67,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, Icon = FontAwesome.Solid.ExclamationTriangle, - Size = new Vector2(40) + Size = new Vector2(40), }, new OsuSpriteText { @@ -194,7 +199,7 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding { Top = 20 }, Current = mods }, - new EpilepsyWarning + new EpilepsyWarning(beatmap.BeatmapInfo.EpilepsyWarning) { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, From f044c06d089319841abc78a59c68096fd0a5a330 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 20 Jul 2020 22:26:58 +0900 Subject: [PATCH 2303/6909] Fix hold notes accepting presses during release lenience --- .../TestSceneHoldNoteInput.cs | 65 +++++++++++++++++-- .../Objects/Drawables/DrawableHoldNote.cs | 6 ++ 2 files changed, 66 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 0d13b85901..95072cf4f8 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.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 NUnit.Framework; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -10,6 +11,8 @@ using osu.Game.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Mania.Scoring; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -236,6 +239,53 @@ namespace osu.Game.Rulesets.Mania.Tests assertTailJudgement(HitResult.Meh); } + [Test] + public void TestMissReleaseAndHitSecondRelease() + { + var windows = new ManiaHitWindows(); + windows.SetDifficulty(10); + + var beatmap = new Beatmap + { + HitObjects = + { + new HoldNote + { + StartTime = 1000, + Duration = 500, + Column = 0, + }, + new HoldNote + { + StartTime = 1000 + 500 + windows.WindowFor(HitResult.Miss) + 10, + Duration = 500, + Column = 0, + }, + }, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty + { + SliderTickRate = 4, + OverallDifficulty = 10, + }, + Ruleset = new ManiaRuleset().RulesetInfo + }, + }; + + performTest(new List + { + new ManiaReplayFrame(beatmap.HitObjects[1].StartTime, ManiaAction.Key1), + new ManiaReplayFrame(beatmap.HitObjects[1].GetEndTime()), + }, beatmap); + + AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) + .All(j => j.Type == HitResult.Miss)); + + AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) + .All(j => j.Type == HitResult.Perfect)); + } + private void assertHeadJudgement(HitResult result) => AddAssert($"head judged as {result}", () => judgementResults[0].Type == result); @@ -250,11 +300,11 @@ namespace osu.Game.Rulesets.Mania.Tests private ScoreAccessibleReplayPlayer currentPlayer; - private void performTest(List frames) + private void performTest(List frames, Beatmap beatmap = null) { - AddStep("load player", () => + if (beatmap == null) { - Beatmap.Value = CreateWorkingBeatmap(new Beatmap + beatmap = new Beatmap { HitObjects = { @@ -270,9 +320,14 @@ namespace osu.Game.Rulesets.Mania.Tests BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 4 }, Ruleset = new ManiaRuleset().RulesetInfo }, - }); + }; - Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + } + + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(beatmap); var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 2262bd2b7d..0c5289efe1 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -167,6 +167,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; + // The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed). + // But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time. + // Note: Unlike below, we use the tail's start time to determine the time offset. + if (Time.Current > Tail.HitObject.StartTime && !Tail.HitObject.HitWindows.CanBeHit(Time.Current - Tail.HitObject.StartTime)) + return false; + beginHoldAt(Time.Current - Head.HitObject.StartTime); Head.UpdateResult(); From 72722d4c721c304a8119e56a496a0d05173cfa20 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 20 Jul 2020 17:18:26 +0000 Subject: [PATCH 2304/6909] Bump Microsoft.Build.Traversal from 2.0.50 to 2.0.52 Bumps [Microsoft.Build.Traversal](https://github.com/Microsoft/MSBuildSdks) from 2.0.50 to 2.0.52. - [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.50...Microsoft.Build.Traversal.2.0.52) 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 9aa5b6192b..233a040d18 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.0.50" + "Microsoft.Build.Traversal": "2.0.52" } } \ No newline at end of file From f71ed47e6693f59c0bba83433d83d0f43c876833 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 20 Jul 2020 11:52:02 -0700 Subject: [PATCH 2305/6909] Fix focused textbox absorbing input when unfocused --- osu.Game/Graphics/UserInterface/FocusedTextBox.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs index 8977f014b6..f77a3109c9 100644 --- a/osu.Game/Graphics/UserInterface/FocusedTextBox.cs +++ b/osu.Game/Graphics/UserInterface/FocusedTextBox.cs @@ -67,6 +67,8 @@ namespace osu.Game.Graphics.UserInterface public bool OnPressed(GlobalAction action) { + if (!HasFocus) return false; + if (action == GlobalAction.Back) { if (Text.Length > 0) From f48984920d5b489adba4afd4a8c3c9fedaceebe1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 11:21:32 +0900 Subject: [PATCH 2306/6909] Change bonus volume logic to work --- .../Objects/Drawables/DrawableSpinnerTick.cs | 14 +------------- 1 file changed, 1 insertion(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 6512a9526e..436994e480 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.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.Audio; -using osu.Framework.Bindables; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; @@ -10,8 +8,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject { - private readonly BindableDouble bonusSampleVolume = new BindableDouble(); - private bool hasBonusPoints; /// @@ -25,8 +21,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { hasBonusPoints = value; - bonusSampleVolume.Value = value ? 1 : 0; - ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints = value; + Samples.Volume.Value = ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints ? 1 : 0; } } @@ -37,13 +32,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } - protected override void LoadComplete() - { - base.LoadComplete(); - - Samples.AddAdjustment(AdjustableProperty.Volume, bonusSampleVolume); - } - /// /// Apply a judgement result. /// From 4dd40542d519ad2f5db5a529ec864b990a0ec697 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 11:21:58 +0900 Subject: [PATCH 2307/6909] Rename rotation set method to match others --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerBonusComponent.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index b82b44f35b..2707453ab9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -227,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Ticks.Rotation = Disc.Rotation; SpmCounter.SetRotation(Disc.CumulativeRotation); - bonusComponent.UpdateRotation(Disc.CumulativeRotation); + bonusComponent.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/SpinnerBonusComponent.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs index 5c96751b3a..c49c10b45c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private int currentSpins; - public void UpdateRotation(double rotation) + public void SetRotation(double rotation) { if (ticks.Count == 0) return; From a8991bb8bfa69a62d9e9a753a7d8d8ba435d48bd Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 20 Jul 2020 20:13:09 -0700 Subject: [PATCH 2308/6909] Fix keybind clear button always clearing first keybind regardless of target --- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index eafb4572ca..d58acc1ac4 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -187,7 +187,7 @@ namespace osu.Game.Overlays.KeyBinding if (bindTarget.IsHovered) finalise(); - else + else if (buttons.Any(b => b.IsHovered)) updateBindTarget(); } From 35ad409da6fd60f48f66b58003058ac9d2c5b360 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 21 Jul 2020 06:59:24 +0300 Subject: [PATCH 2309/6909] Fix spinner bonus ticks samples not actually playing --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 436994e480..d49766adda 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -21,7 +21,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { hasBonusPoints = value; - Samples.Volume.Value = ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints ? 1 : 0; + ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints = value; + Samples.Volume.Value = value ? 1 : 0; } } From c1442568b95548316e748e470c00c70a45bd5f9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 17:04:29 +0900 Subject: [PATCH 2310/6909] Make perfect mod ignore all non-combo-affecting hitobjects --- .../Mods/TestSceneCatchModPerfect.cs | 2 +- osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs | 6 ------ .../Skinning/TestSceneDrawableTaikoMascot.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs | 4 ++-- .../Judgements/TaikoDrumRollJudgement.cs | 2 -- osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs | 2 -- osu.Game/Rulesets/Mods/ModPerfect.cs | 1 + 7 files changed, 5 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index 3e06e78dba..c1b7214d72 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods 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)] + [TestCase(false)] public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss); } } diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs index e3391c47f1..fb92399102 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModPerfect.cs @@ -1,17 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.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.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index d200c44a02..cb6a0decde 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning 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 DrumRoll(), new TaikoDrumRollJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle); } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs index aaa634648a..0be005e1c4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoSuddenDeath.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Tests }; [Test] - public void TestSpinnerDoesNotFail() + public void TestSpinnerDoesFail() { bool judged = false; AddStep("Setup judgements", () => @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Player.ScoreProcessor.NewJudgement += b => judged = true; }); AddUntilStep("swell judged", () => judged); - AddAssert("not failed", () => !Player.HasFailed); + AddAssert("failed", () => Player.HasFailed); } } } diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs index 604daa929f..0d91002f4b 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollJudgement.cs @@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoDrumRollJudgement : TaikoJudgement { - public override bool AffectsCombo => false; - protected override double HealthIncreaseFor(HitResult result) { // Drum rolls can be ignored with no health penalty diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs index 29be5e0eac..4d61efd3ee 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoSwellJudgement.cs @@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoSwellJudgement : TaikoJudgement { - public override bool AffectsCombo => false; - protected override double HealthIncreaseFor(HitResult result) { switch (result) diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 7fe606d584..65f1a972ed 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Mods protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => !(result.Judgement is IgnoreJudgement) + && result.Judgement.AffectsCombo && result.Type != result.Judgement.MaxResult; } } From d9fedb293a1e712fa4a4ac5902c9edf1592ba84b Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Tue, 21 Jul 2020 15:48:11 +0700 Subject: [PATCH 2311/6909] add initial test --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 4c73065087..341924ae6d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -35,6 +35,8 @@ namespace osu.Game.Tests.Visual.Gameplay private TestPlayerLoaderContainer container; private TestPlayer player; + private bool EpilepsyWarning = false; + [Resolved] private AudioManager audioManager { get; set; } @@ -55,6 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay beforeLoadAction?.Invoke(); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + Beatmap.Value.BeatmapInfo.EpilepsyWarning = EpilepsyWarning; foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToTrack(Beatmap.Value.Track); @@ -240,6 +243,15 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for player load", () => player.IsLoaded); } + [TestCase(true)] + [TestCase(false)] + public void TestEpilepsyWarning(bool warning) + { + AddStep("change epilepsy warning", () => EpilepsyWarning = warning); + AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + } + private class TestPlayerLoaderContainer : Container { [Cached] From 95f52573f7ca180bfb366aaf5dc9f7fdddddc419 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Tue, 21 Jul 2020 15:58:25 +0700 Subject: [PATCH 2312/6909] change font size --- osu.Game/Screens/Play/BeatmapMetadataDisplay.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index f672a4db6c..bab141a75e 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -73,13 +73,15 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "This beatmap contains scenes with rapidly flashing colours." + Text = "This beatmap contains scenes with rapidly flashing colours.", + Font = OsuFont.GetFont(size: 20), }, new OsuSpriteText { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = "Please take caution if you are affected by epilepsy." + Text = "Please take caution if you are affected by epilepsy.", + Font = OsuFont.GetFont(size: 20), } }; } From 05102bc1baf00b4508bf57dfe0e749569944b8ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 18:22:37 +0900 Subject: [PATCH 2313/6909] Split ticks up into bonus and non-bonus --- .../Judgements/OsuSpinnerTickJudgement.cs | 18 -------------- .../Objects/Drawables/DrawableSpinner.cs | 3 +++ .../Drawables/DrawableSpinnerBonusTick.cs | 13 ++++++++++ .../Objects/Drawables/DrawableSpinnerTick.cs | 19 --------------- .../Drawables/Pieces/SpinnerBonusComponent.cs | 5 +--- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 19 ++++++++++----- .../Objects/SpinnerBonusTick.cs | 24 +++++++++++++++++++ osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 15 +++++++----- 8 files changed, 63 insertions(+), 53 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs create mode 100644 osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.cs deleted file mode 100644 index f9cac7a2c1..0000000000 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerTickJudgement.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.Game.Rulesets.Scoring; - -namespace osu.Game.Rulesets.Osu.Judgements -{ - public class OsuSpinnerTickJudgement : OsuJudgement - { - internal bool HasBonusPoints; - - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) => 100 + (HasBonusPoints ? 1000 : 0); - - protected override double HealthIncreaseFor(HitResult result) => 0; - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 2707453ab9..531d16d1d1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -157,6 +157,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { switch (hitObject) { + case SpinnerBonusTick bonusTick: + return new DrawableSpinnerBonusTick(bonusTick); + case SpinnerTick tick: return new DrawableSpinnerTick(tick); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs new file mode 100644 index 0000000000..2e1c07c4c6 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.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.Osu.Objects.Drawables +{ + public class DrawableSpinnerBonusTick : DrawableSpinnerTick + { + public DrawableSpinnerBonusTick(SpinnerBonusTick spinnerTick) + : base(spinnerTick) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index d49766adda..5fb7653f5a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -1,31 +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.Osu.Judgements; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject { - private bool hasBonusPoints; - - /// - /// Whether this judgement has a bonus of 1,000 points additional to the numeric result. - /// Set when a spin occured after the spinner has completed. - /// - public bool HasBonusPoints - { - get => hasBonusPoints; - internal set - { - hasBonusPoints = value; - - ((OsuSpinnerTickJudgement)Result.Judgement).HasBonusPoints = value; - Samples.Volume.Value = value ? 1 : 0; - } - } - public override bool DisplayResult => false; public DrawableSpinnerTick(SpinnerTick spinnerTick) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs index c49c10b45c..9a65247453 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs @@ -55,12 +55,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var tick = ticks[currentSpins]; if (direction >= 0) - { - tick.HasBonusPoints = currentSpins > spinsRequired; tick.TriggerResult(true); - } - if (tick.HasBonusPoints) + if (tick is DrawableSpinnerBonusTick) { bonusCounter.Text = $"{1000 * (currentSpins - spinsRequired)}"; bonusCounter.FadeOutFromOne(1500); diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 4c21d9cfde..1c30058d5d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -3,13 +3,11 @@ using System; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects.Types; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.Scoring; -using osuTK; namespace osu.Game.Rulesets.Osu.Objects { @@ -28,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.Objects /// public int SpinsRequired { get; protected set; } = 1; + public int MaximumBonusSpins => SpinsRequired; + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -42,9 +42,16 @@ namespace osu.Game.Rulesets.Osu.Objects { base.CreateNestedHitObjects(); - var maximumSpins = OsuAutoGeneratorBase.SPIN_RADIUS * (Duration / 1000) / MathHelper.TwoPi; - for (int i = 0; i < maximumSpins; i++) - AddNested(new SpinnerTick()); + int totalSpins = MaximumBonusSpins + SpinsRequired; + + for (int i = 0; i < totalSpins; i++) + { + double startTime = StartTime + (float)(i + 1) / totalSpins * Duration; + + AddNested(i < SpinsRequired + ? new SpinnerTick { StartTime = startTime } + : new SpinnerBonusTick { StartTime = startTime }); + } } public override Judgement CreateJudgement() => new OsuJudgement(); diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs new file mode 100644 index 0000000000..84eb58c70b --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.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.Audio; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Osu.Objects +{ + public class SpinnerBonusTick : SpinnerTick + { + public SpinnerBonusTick() + { + Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); + } + + public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement(); + + public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement + { + protected override int NumericResultFor(HitResult result) => 1100; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index 318e8e71a2..89ad45b267 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.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.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Scoring; @@ -10,13 +9,17 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SpinnerTick : OsuHitObject { - public SpinnerTick() - { - Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); - } - public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + public class OsuSpinnerTickJudgement : OsuJudgement + { + public override bool AffectsCombo => false; + + protected override int NumericResultFor(HitResult result) => 100; + + protected override double HealthIncreaseFor(HitResult result) => 0; + } } } From 947f4e0d4c5aac6609ef6cfdf5488256402c6376 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 19:03:17 +0900 Subject: [PATCH 2314/6909] Move tick handling to DrawableSpinner itself --- .../Objects/Drawables/DrawableSpinner.cs | 42 ++++++++- .../Objects/Drawables/DrawableSpinnerTick.cs | 7 +- .../Drawables/Pieces/SpinnerBonusComponent.cs | 87 ------------------- .../Drawables/Pieces/SpinnerBonusDisplay.cs | 44 ++++++++++ 4 files changed, 86 insertions(+), 94 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 531d16d1d1..df6eb206da 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public readonly SpinnerDisc Disc; public readonly SpinnerTicks Ticks; public readonly SpinnerSpmCounter SpmCounter; - private readonly SpinnerBonusComponent bonusComponent; + private readonly SpinnerBonusDisplay bonusDisplay; private readonly Container mainContainer; @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Y = 120, Alpha = 0 }, - bonusComponent = new SpinnerBonusComponent(this, ticks) + bonusDisplay = new SpinnerBonusDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -199,6 +199,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < Spinner.EndTime) return; + // Trigger a miss result for remaining ticks to avoid infinite gameplay. + foreach (var tick in ticks.Where(t => !t.IsHit)) + tick.TriggerResult(HitResult.Miss); + ApplyResult(r => { if (Progress >= 1) @@ -230,7 +234,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Ticks.Rotation = Disc.Rotation; SpmCounter.SetRotation(Disc.CumulativeRotation); - bonusComponent.SetRotation(Disc.CumulativeRotation); + + updateBonusScore(); float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; float targetScale = relativeCircleScale + (1 - relativeCircleScale) * Progress; @@ -239,6 +244,37 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } + private int wholeSpins; + + private void updateBonusScore() + { + if (ticks.Count == 0) + return; + + int spins = (int)(Disc.CumulativeRotation / 360); + + while (wholeSpins != spins) + { + if (wholeSpins < spins) + { + var tick = ticks.FirstOrDefault(t => !t.IsHit); + + if (tick != null) + { + tick.TriggerResult(HitResult.Great); + if (tick is DrawableSpinnerBonusTick) + bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired); + } + + wholeSpins++; + } + else + { + wholeSpins--; + } + } + } + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 5fb7653f5a..6c9570c381 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -17,11 +17,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Apply a judgement result. /// - /// Whether to apply a result, otherwise. - internal void TriggerResult(bool hit) + /// Whether to apply a result, otherwise. + internal void TriggerResult(HitResult result) { - HitObject.StartTime = Time.Current; - ApplyResult(r => r.Type = hit ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = result); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs deleted file mode 100644 index 9a65247453..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusComponent.cs +++ /dev/null @@ -1,87 +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.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Drawables; - -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces -{ - /// - /// A component that tracks spinner spins and add bonus score for it. - /// - public class SpinnerBonusComponent : CompositeDrawable - { - private readonly DrawableSpinner drawableSpinner; - private readonly Container ticks; - private readonly OsuSpriteText bonusCounter; - - public SpinnerBonusComponent(DrawableSpinner drawableSpinner, Container ticks) - { - this.drawableSpinner = drawableSpinner; - this.ticks = ticks; - - drawableSpinner.OnNewResult += onNewResult; - - AutoSizeAxes = Axes.Both; - InternalChild = bonusCounter = new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.Numeric.With(size: 24), - Alpha = 0, - }; - } - - private int currentSpins; - - public void SetRotation(double rotation) - { - if (ticks.Count == 0) - return; - - int spinsRequired = ((Spinner)drawableSpinner.HitObject).SpinsRequired; - - int newSpins = Math.Clamp((int)(rotation / 360), 0, ticks.Count - 1); - int direction = Math.Sign(newSpins - currentSpins); - - while (currentSpins != newSpins) - { - var tick = ticks[currentSpins]; - - if (direction >= 0) - tick.TriggerResult(true); - - if (tick is DrawableSpinnerBonusTick) - { - bonusCounter.Text = $"{1000 * (currentSpins - spinsRequired)}"; - bonusCounter.FadeOutFromOne(1500); - bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); - } - - currentSpins += direction; - } - } - - private void onNewResult(DrawableHitObject hitObject, JudgementResult result) - { - if (!result.HasResult || hitObject != drawableSpinner) - return; - - // Trigger a miss result for remaining ticks to avoid infinite gameplay. - foreach (var tick in ticks.Where(t => !t.IsHit)) - tick.TriggerResult(false); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - drawableSpinner.OnNewResult -= onNewResult; - } - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs new file mode 100644 index 0000000000..76d7f1843e --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + /// + /// A component that tracks spinner spins and add bonus score for it. + /// + public class SpinnerBonusDisplay : CompositeDrawable + { + private readonly OsuSpriteText bonusCounter; + + public SpinnerBonusDisplay() + { + AutoSizeAxes = Axes.Both; + + InternalChild = bonusCounter = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.Numeric.With(size: 24), + Alpha = 0, + }; + } + + private int displayedCount; + + public void SetBonusCount(int count) + { + if (displayedCount == count) + return; + + displayedCount = count; + bonusCounter.Text = $"{1000 * count}"; + bonusCounter.FadeOutFromOne(1500); + bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); + } + } +} From 7f2ae694cc96e41175932d16353fa3e1c0a3e9ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 19:21:30 +0900 Subject: [PATCH 2315/6909] Simplify rewind handling --- .../Objects/Drawables/DrawableSpinner.cs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index df6eb206da..a8ecb60038 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -253,25 +253,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables int spins = (int)(Disc.CumulativeRotation / 360); + if (spins < wholeSpins) + { + // rewinding, silently handle + wholeSpins = spins; + return; + } + while (wholeSpins != spins) { - if (wholeSpins < spins) - { - var tick = ticks.FirstOrDefault(t => !t.IsHit); + var tick = ticks.FirstOrDefault(t => !t.IsHit); - if (tick != null) - { - tick.TriggerResult(HitResult.Great); - if (tick is DrawableSpinnerBonusTick) - bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired); - } - - wholeSpins++; - } - else + // tick may be null if we've hit the spin limit. + if (tick != null) { - wholeSpins--; + tick.TriggerResult(HitResult.Great); + if (tick is DrawableSpinnerBonusTick) + bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired); } + + wholeSpins++; } } From a4680d7a8945ded3804b0a0b84500a0a47241e44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 19:22:42 +0900 Subject: [PATCH 2316/6909] Reduce test range as to not hit spin cat --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 0f1cbcd44c..6e277ff37e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -164,13 +164,13 @@ namespace osu.Game.Rulesets.Osu.Tests { double estimatedSpm = 0; - addSeekStep(2500); + addSeekStep(1000); AddStep("retrieve spm", () => estimatedSpm = drawableSpinner.SpmCounter.SpinsPerMinute); - addSeekStep(5000); + addSeekStep(2000); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); - addSeekStep(2500); + addSeekStep(1000); AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); } From 1560e1786a09475d4537bfc02b881d8bb2f422f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 19:48:44 +0900 Subject: [PATCH 2317/6909] Revert back to bool for application --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 ++-- .../Objects/Drawables/DrawableSpinnerTick.cs | 7 ++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a8ecb60038..ecf78efdd9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -201,7 +201,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Trigger a miss result for remaining ticks to avoid infinite gameplay. foreach (var tick in ticks.Where(t => !t.IsHit)) - tick.TriggerResult(HitResult.Miss); + tick.TriggerResult(false); ApplyResult(r => { @@ -267,7 +267,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // tick may be null if we've hit the spin limit. if (tick != null) { - tick.TriggerResult(HitResult.Great); + tick.TriggerResult(true); if (tick is DrawableSpinnerBonusTick) bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index 6c9570c381..c390b673be 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -17,10 +17,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// Apply a judgement result. /// - /// Whether to apply a result, otherwise. - internal void TriggerResult(HitResult result) - { - ApplyResult(r => r.Type = result); - } + /// Whether this tick was reached. + internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss); } } From bc079fccf52d5d338609ca87249208a899343958 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 21 Jul 2020 19:52:16 +0900 Subject: [PATCH 2318/6909] Add health drain for spinner ticks --- osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs | 2 ++ osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 84eb58c70b..6ca2d4d72d 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -19,6 +19,8 @@ namespace osu.Game.Rulesets.Osu.Objects public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement { protected override int NumericResultFor(HitResult result) => 1100; + + protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index 89ad45b267..c81348fbbf 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects protected override int NumericResultFor(HitResult result) => 100; - protected override double HealthIncreaseFor(HitResult result) => 0; + protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0; } } } From 107b5ca4f2ab5ca29356aad95a852cb28aa4e856 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Jul 2020 23:13:04 +0900 Subject: [PATCH 2319/6909] Add support for bindable retrieval --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 186 ++++++++++++++++-- osu.Game/OsuGameBase.cs | 5 +- .../Carousel/DrawableCarouselBeatmap.cs | 59 ++---- .../Screens/Select/Details/AdvancedStats.cs | 25 ++- 4 files changed, 208 insertions(+), 67 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 02342e9595..379cb6aa63 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -9,7 +9,9 @@ using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Framework.Lists; using osu.Framework.Threading; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -21,32 +23,154 @@ namespace osu.Game.Beatmaps // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes. private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager)); - private readonly TimedExpiryCache difficultyCache = new TimedExpiryCache { ExpiryTime = 60000 }; - private readonly BeatmapManager beatmapManager; + // A cache that keeps references to BeatmapInfos for 60sec. + private readonly TimedExpiryCache difficultyCache = new TimedExpiryCache { ExpiryTime = 60000 }; - public BeatmapDifficultyManager(BeatmapManager beatmapManager) + // All bindables that should be updated along with the current ruleset + mods. + private readonly LockedWeakList trackedBindables = new LockedWeakList(); + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + [Resolved] + private Bindable currentRuleset { get; set; } + + [Resolved] + private Bindable> currentMods { get; set; } + + protected override void LoadComplete() { - this.beatmapManager = beatmapManager; + base.LoadComplete(); + + currentRuleset.BindValueChanged(_ => updateTrackedBindables()); + currentMods.BindValueChanged(_ => updateTrackedBindables(), true); } - public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, - CancellationToken cancellationToken = default) + /// + /// Retrieves an containing the star difficulty of a with a given and combination. + /// + /// + /// This will not update to follow the currently-selected ruleset and mods. + /// + /// The to get the difficulty of. + /// The to get the difficulty with. + /// The s to get the difficulty with. + /// An optional which stops updating the star difficulty for the given . + /// An that is updated to contain the star difficulty when it becomes available. + public IBindable GetUntrackedBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + CancellationToken cancellationToken = default) + => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); + + /// + /// Retrieves a containing the star difficulty of a that follows the user's currently-selected ruleset and mods. + /// + /// + /// Ensure to hold a local reference of the returned in order to receive value-changed events. + /// + /// The to get the difficulty of. + /// An optional which stops updating the star difficulty for the given . + /// An that is updated to contain the star difficulty when it becomes available, or when the currently-selected ruleset and mods change. + public IBindable GetTrackedBindable([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) + { + var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); + trackedBindables.Add(bindable); + return bindable; + } + + /// + /// Retrieves the difficulty of a . + /// + /// The to get the difficulty of. + /// The to get the difficulty with. + /// The s to get the difficulty with. + /// An optional which stops computing the star difficulty. + /// The . + public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + CancellationToken cancellationToken = default) { if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return await Task.Factory.StartNew(() => getDifficulty(key), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + return await Task.Factory.StartNew(() => computeDifficulty(key), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } - public double GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) + /// + /// Retrieves the difficulty of a . + /// + /// The to get the difficulty of. + /// The to get the difficulty with. + /// The s to get the difficulty with. + /// The . + public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) { if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return getDifficulty(key); + return computeDifficulty(key); } - private double getDifficulty(in DifficultyCacheLookup key) + private CancellationTokenSource trackedUpdateCancellationSource; + + /// + /// Updates all tracked using the current ruleset and mods. + /// + private void updateTrackedBindables() + { + trackedUpdateCancellationSource?.Cancel(); + trackedUpdateCancellationSource = new CancellationTokenSource(); + + foreach (var b in trackedBindables) + { + if (trackedUpdateCancellationSource.IsCancellationRequested) + break; + + using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken)) + updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token); + } + } + + /// + /// Updates the value of a with a given ruleset + mods. + /// + /// The to update. + /// The to update with. + /// The s to update with. + /// A token that may be used to cancel this update. + private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IReadOnlyList mods, CancellationToken cancellationToken = default) + { + GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken).ContinueWith(t => + { + // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events. + Schedule(() => + { + if (!cancellationToken.IsCancellationRequested) + bindable.Value = t.Result; + }); + }, cancellationToken); + } + + /// + /// Creates a new and triggers an initial value update. + /// + /// The that star difficulty should correspond to. + /// The initial to get the difficulty with. + /// The initial s to get the difficulty with. + /// An optional which stops updating the star difficulty for the given . + /// The . + private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IReadOnlyList initialMods, + CancellationToken cancellationToken) + { + var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken); + updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken); + return bindable; + } + + /// + /// Computes the difficulty defined by a key, and stores it to the timed cache. + /// + /// The that defines the computation parameters. + /// The . + private StarDifficulty computeDifficulty(in DifficultyCacheLookup key) { try { @@ -56,13 +180,17 @@ namespace osu.Game.Beatmaps var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); var attributes = calculator.Calculate(key.Mods); - difficultyCache.Add(key, attributes.StarRating); - return attributes.StarRating; + var difficulty = new StarDifficulty(attributes.StarRating); + difficultyCache.Add(key, difficulty); + + return difficulty; } catch { - difficultyCache.Add(key, 0); - return 0; + var difficulty = new StarDifficulty(0); + difficultyCache.Add(key, difficulty); + + return difficulty; } } @@ -73,9 +201,9 @@ namespace osu.Game.Beatmaps /// The . /// The s. /// The existing difficulty value, if present. - /// The key that was used to perform this lookup. This can be further used to query . + /// The key that was used to perform this lookup. This can be further used to query . /// Whether an existing difficulty was found. - private bool tryGetGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out double existingDifficulty, out DifficultyCacheLookup key) + private bool tryGetGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; @@ -83,7 +211,7 @@ namespace osu.Game.Beatmaps // Difficulty can only be computed if the beatmap is locally available. if (beatmapInfo.ID == 0) { - existingDifficulty = 0; + existingDifficulty = new StarDifficulty(0); key = default; return true; @@ -122,5 +250,29 @@ namespace osu.Game.Beatmaps return hashCode.ToHashCode(); } } + + private class BindableStarDifficulty : Bindable + { + public readonly BeatmapInfo Beatmap; + public readonly CancellationToken CancellationToken; + + public BindableStarDifficulty(BeatmapInfo beatmap, CancellationToken cancellationToken) + { + Beatmap = beatmap; + CancellationToken = cancellationToken; + } + } + } + + public readonly struct StarDifficulty + { + public readonly double Stars; + + public StarDifficulty(double stars) + { + Stars = stars; + + // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) + } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1e6631ffa0..fe5c0704b7 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -199,7 +199,10 @@ namespace osu.Game ScoreManager.Undelete(getBeatmapScores(item), true); }); - dependencies.Cache(new BeatmapDifficultyManager(BeatmapManager)); + var difficultyManager = new BeatmapDifficultyManager(); + dependencies.Cache(difficultyManager); + AddInternal(difficultyManager); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index d4205a4b93..d5aeecae04 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -23,8 +22,6 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Rulesets; -using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -46,15 +43,12 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private BeatmapSetOverlay beatmapOverlay { get; set; } - [Resolved] - private IBindable ruleset { get; set; } - - [Resolved] - private IBindable> mods { get; set; } - [Resolved] private BeatmapDifficultyManager difficultyManager { get; set; } + private IBindable starDifficultyBindable; + private CancellationTokenSource starDifficultyCancellationSource; + public DrawableCarouselBeatmap(CarouselBeatmap panel) : base(panel) { @@ -160,36 +154,6 @@ namespace osu.Game.Screens.Select.Carousel } } }; - - ruleset.BindValueChanged(_ => refreshStarCounter()); - mods.BindValueChanged(_ => refreshStarCounter(), true); - } - - private ScheduledDelegate scheduledRefresh; - private CancellationTokenSource cancellationSource; - - private void refreshStarCounter() - { - scheduledRefresh?.Cancel(); - scheduledRefresh = null; - - cancellationSource?.Cancel(); - cancellationSource = null; - - // Only want to run the calculation when we become visible. - scheduledRefresh = Schedule(() => - { - var ourSource = cancellationSource = new CancellationTokenSource(); - difficultyManager.GetDifficultyAsync(beatmap, ruleset.Value, mods.Value, ourSource.Token).ContinueWith(t => - { - // We're currently on a random threadpool thread which we must exit. - Schedule(() => - { - if (!ourSource.IsCancellationRequested) - starCounter.Current = (float)t.Result; - }); - }, ourSource.Token); - }); } protected override void Selected() @@ -224,6 +188,17 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0) starCounter.ReplayAnimation(); + if (Item.State.Value == CarouselItemState.Collapsed) + starDifficultyCancellationSource?.Cancel(); + else + { + starDifficultyCancellationSource?.Cancel(); + + // We've potentially cancelled the computation above so a new bindable is required. + starDifficultyBindable = difficultyManager.GetTrackedBindable(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); + starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true); + } + base.ApplyState(); } @@ -248,5 +223,11 @@ namespace osu.Game.Screens.Select.Carousel return items.ToArray(); } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + starDifficultyCancellationSource?.Cancel(); + } } } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index c5fc3701f8..aefba397b9 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -15,7 +15,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Mods; using System.Linq; using System.Threading; -using System.Threading.Tasks; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; @@ -149,6 +148,8 @@ namespace osu.Game.Screens.Select.Details updateStarDifficulty(); } + private IBindable normalStarDifficulty; + private IBindable moddedStarDifficulty; private CancellationTokenSource starDifficultyCancellationSource; private void updateStarDifficulty() @@ -160,15 +161,19 @@ namespace osu.Game.Screens.Select.Details var ourSource = starDifficultyCancellationSource = new CancellationTokenSource(); - Task.WhenAll(difficultyManager.GetDifficultyAsync(Beatmap, ruleset.Value, cancellationToken: ourSource.Token), - difficultyManager.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, ourSource.Token)).ContinueWith(t => - { - Schedule(() => - { - if (!ourSource.IsCancellationRequested) - starDifficulty.Value = ((float)t.Result[0], (float)t.Result[1]); - }); - }, ourSource.Token); + normalStarDifficulty = difficultyManager.GetUntrackedBindable(Beatmap, ruleset.Value, cancellationToken: ourSource.Token); + moddedStarDifficulty = difficultyManager.GetUntrackedBindable(Beatmap, ruleset.Value, mods.Value, ourSource.Token); + + normalStarDifficulty.BindValueChanged(_ => updateDisplay()); + moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true); + + void updateDisplay() => starDifficulty.Value = ((float)normalStarDifficulty.Value.Stars, (float)moddedStarDifficulty.Value.Stars); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + starDifficultyCancellationSource?.Cancel(); } public class StatisticRow : Container, IHasAccentColour From 00e6217f60c9d1981e1eb16e1b21b39a70b844a9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 21 Jul 2020 23:50:54 +0900 Subject: [PATCH 2320/6909] Don't store BeatmapInfo/RulesetInfo references, remove TimedExpiryCache --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 379cb6aa63..b469ca78fb 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; @@ -24,7 +25,7 @@ namespace osu.Game.Beatmaps private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager)); // A cache that keeps references to BeatmapInfos for 60sec. - private readonly TimedExpiryCache difficultyCache = new TimedExpiryCache { ExpiryTime = 60000 }; + private readonly ConcurrentDictionary difficultyCache = new ConcurrentDictionary(); // All bindables that should be updated along with the current ruleset + mods. private readonly LockedWeakList trackedBindables = new LockedWeakList(); @@ -91,7 +92,8 @@ namespace osu.Game.Beatmaps if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return await Task.Factory.StartNew(() => computeDifficulty(key), cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken, + TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } /// @@ -106,7 +108,7 @@ namespace osu.Game.Beatmaps if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return computeDifficulty(key); + return computeDifficulty(key, beatmapInfo, rulesetInfo); } private CancellationTokenSource trackedUpdateCancellationSource; @@ -169,28 +171,24 @@ namespace osu.Game.Beatmaps /// Computes the difficulty defined by a key, and stores it to the timed cache. /// /// The that defines the computation parameters. + /// The to compute the difficulty of. + /// The to compute the difficulty with. /// The . - private StarDifficulty computeDifficulty(in DifficultyCacheLookup key) + private StarDifficulty computeDifficulty(in DifficultyCacheLookup key, BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo) { try { - var ruleset = key.RulesetInfo.CreateInstance(); + var ruleset = rulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.BeatmapInfo)); + var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); var attributes = calculator.Calculate(key.Mods); - var difficulty = new StarDifficulty(attributes.StarRating); - difficultyCache.Add(key, difficulty); - - return difficulty; + return difficultyCache[key] = new StarDifficulty(attributes.StarRating); } catch { - var difficulty = new StarDifficulty(0); - difficultyCache.Add(key, difficulty); - - return difficulty; + return difficultyCache[key] = new StarDifficulty(0); } } @@ -208,8 +206,8 @@ namespace osu.Game.Beatmaps // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; - // Difficulty can only be computed if the beatmap is locally available. - if (beatmapInfo.ID == 0) + // Difficulty can only be computed if the beatmap and ruleset are locally available. + if (beatmapInfo.ID == 0 || rulesetInfo.ID == null) { existingDifficulty = new StarDifficulty(0); key = default; @@ -217,33 +215,34 @@ namespace osu.Game.Beatmaps return true; } - key = new DifficultyCacheLookup(beatmapInfo, rulesetInfo, mods); + key = new DifficultyCacheLookup(beatmapInfo.ID, rulesetInfo.ID.Value, mods); return difficultyCache.TryGetValue(key, out existingDifficulty); } private readonly struct DifficultyCacheLookup : IEquatable { - public readonly BeatmapInfo BeatmapInfo; - public readonly RulesetInfo RulesetInfo; + public readonly int BeatmapId; + public readonly int RulesetId; public readonly Mod[] Mods; - public DifficultyCacheLookup(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods) + public DifficultyCacheLookup(int beatmapId, int rulesetId, IEnumerable mods) { - BeatmapInfo = beatmapInfo; - RulesetInfo = rulesetInfo; + BeatmapId = beatmapId; + RulesetId = rulesetId; Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); } public bool Equals(DifficultyCacheLookup other) - => BeatmapInfo.Equals(other.BeatmapInfo) + => BeatmapId == other.BeatmapId + && RulesetId == other.RulesetId && Mods.SequenceEqual(other.Mods); public override int GetHashCode() { var hashCode = new HashCode(); - hashCode.Add(BeatmapInfo.Hash); - hashCode.Add(RulesetInfo.GetHashCode()); + hashCode.Add(BeatmapId); + hashCode.Add(RulesetId); foreach (var mod in Mods) hashCode.Add(mod.Acronym); From e96f8f1cb652e4d312758b20414f74c01d144ca0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 21 Jul 2020 20:02:22 +0300 Subject: [PATCH 2321/6909] Make content side padding adjustable for OverlayHeader --- .../UserInterface/TestSceneOverlayHeader.cs | 20 +++++++++++--- osu.Game/Overlays/OverlayHeader.cs | 27 ++++++++++++++----- osu.Game/Overlays/TabControlOverlayHeader.cs | 24 ++++++++++++++--- 3 files changed, 57 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index 60af5b37ef..01c13dbc97 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -36,11 +36,11 @@ namespace osu.Game.Tests.Visual.UserInterface } }); - addHeader("Orange OverlayHeader (no background)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange); - addHeader("Blue OverlayHeader", new TestNoControlHeader(), OverlayColourScheme.Blue); + addHeader("Orange OverlayHeader (no background, 100 padding)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange); + addHeader("Blue OverlayHeader (default 70 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue); addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green); - addHeader("Pink TabControlOverlayHeader (enum)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink); - addHeader("Red BreadcrumbControlOverlayHeader (no background)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red); + addHeader("Pink TabControlOverlayHeader (enum, 30 padding)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink); + addHeader("Red BreadcrumbControlOverlayHeader (no background, 10 padding)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red); } private void addHeader(string name, OverlayHeader header, OverlayColourScheme colourScheme) @@ -86,6 +86,11 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestNoBackgroundHeader : OverlayHeader { protected override OverlayTitle CreateTitle() => new TestTitle(); + + public TestNoBackgroundHeader() + { + ContentSidePadding = 100; + } } private class TestNoControlHeader : OverlayHeader @@ -112,6 +117,11 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestEnumTabControlHeader : TabControlOverlayHeader { + public TestEnumTabControlHeader() + { + ContentSidePadding = 30; + } + protected override Drawable CreateBackground() => new OverlayHeaderBackground(@"Headers/rankings"); protected override OverlayTitle CreateTitle() => new TestTitle(); @@ -130,6 +140,8 @@ namespace osu.Game.Tests.Visual.UserInterface public TestBreadcrumbControlHeader() { + ContentSidePadding = 10; + TabControl.AddItem("tab1"); TabControl.AddItem("tab2"); TabControl.Current.Value = "tab2"; diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index dbc934bde9..c9b9e3b836 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -12,9 +12,26 @@ namespace osu.Game.Overlays { public abstract class OverlayHeader : Container { - public const int CONTENT_X_MARGIN = 50; + private float contentSidePadding; + + /// + /// Horizontal padding of the header content. + /// + protected float ContentSidePadding + { + get => contentSidePadding; + set + { + contentSidePadding = value; + content.Padding = new MarginPadding + { + Horizontal = value + }; + } + } private readonly Box titleBackground; + private readonly Container content; protected readonly FillFlowContainer HeaderInfo; @@ -50,14 +67,10 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = Color4.Gray, }, - new Container + content = new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Horizontal = CONTENT_X_MARGIN, - }, Children = new[] { CreateTitle().With(title => @@ -79,6 +92,8 @@ namespace osu.Game.Overlays CreateContent() } }); + + ContentSidePadding = 70; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index e8e000f441..61605d9e9e 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 Container tabControlContainer; private readonly BindableWithCurrent current = new BindableWithCurrent(); public Bindable Current @@ -30,6 +31,16 @@ namespace osu.Game.Overlays set => current.Current = value; } + protected new float ContentSidePadding + { + get => base.ContentSidePadding; + set + { + base.ContentSidePadding = value; + tabControlContainer.Padding = new MarginPadding { Horizontal = value }; + } + } + protected TabControlOverlayHeader() { HeaderInfo.Add(new Container @@ -42,11 +53,16 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both, }, - TabControl = CreateTabControl().With(control => + tabControlContainer = new Container { - control.Margin = new MarginPadding { Left = CONTENT_X_MARGIN }; - control.Current = Current; - }) + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = ContentSidePadding }, + Child = TabControl = CreateTabControl().With(control => + { + control.Current = Current; + }) + } } }); } From 0145ca09e5d8ebd98a85857c6fab121d7c112143 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 21 Jul 2020 20:11:10 +0300 Subject: [PATCH 2322/6909] Apply changes to overlays --- osu.Game/Overlays/OverlayHeader.cs | 2 +- osu.Game/Overlays/Profile/ProfileHeader.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index c9b9e3b836..cc7f798c4a 100644 --- a/osu.Game/Overlays/OverlayHeader.cs +++ b/osu.Game/Overlays/OverlayHeader.cs @@ -93,7 +93,7 @@ namespace osu.Game.Overlays } }); - ContentSidePadding = 70; + ContentSidePadding = 50; } [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 0161d91daa..2895fa0726 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -23,6 +23,8 @@ namespace osu.Game.Overlays.Profile public ProfileHeader() { + ContentSidePadding = 70; + User.ValueChanged += e => updateDisplay(e.NewValue); TabControl.AddItem("info"); From 0a71194ea69e07e516be8db5c9180d612459aec8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 21 Jul 2020 22:46:08 +0300 Subject: [PATCH 2323/6909] Fix SpotlightSelector is a VisibilityContainer without a reason --- .../TestSceneRankingsSpotlightSelector.cs | 6 - .../Overlays/Rankings/SpotlightSelector.cs | 104 ++++++++---------- .../Overlays/Rankings/SpotlightsLayout.cs | 2 - 3 files changed, 45 insertions(+), 67 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs index 997db827f3..d60222fa0b 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsSpotlightSelector.cs @@ -30,12 +30,6 @@ namespace osu.Game.Tests.Visual.Online Add(selector = new SpotlightSelector()); } - [Test] - public void TestVisibility() - { - AddStep("Toggle Visibility", selector.ToggleVisibility); - } - [Test] public void TestLocalSpotlights() { diff --git a/osu.Game/Overlays/Rankings/SpotlightSelector.cs b/osu.Game/Overlays/Rankings/SpotlightSelector.cs index f112c1ec43..422373d099 100644 --- a/osu.Game/Overlays/Rankings/SpotlightSelector.cs +++ b/osu.Game/Overlays/Rankings/SpotlightSelector.cs @@ -18,10 +18,8 @@ using osu.Game.Online.API.Requests; namespace osu.Game.Overlays.Rankings { - public class SpotlightSelector : VisibilityContainer, IHasCurrentValue + public class SpotlightSelector : CompositeDrawable, IHasCurrentValue { - private const int duration = 300; - private readonly BindableWithCurrent current = new BindableWithCurrent(); public readonly Bindable Sort = new Bindable(); @@ -37,10 +35,7 @@ namespace osu.Game.Overlays.Rankings set => dropdown.Items = value; } - protected override bool StartHidden => true; - private readonly Box background; - private readonly Container content; private readonly SpotlightsDropdown dropdown; private readonly InfoColumn startDateColumn; private readonly InfoColumn endDateColumn; @@ -51,73 +46,68 @@ namespace osu.Game.Overlays.Rankings { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - Add(content = new Container + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + background = new Box { - background = new Box - { - RelativeSizeAxes = Axes.Both, - }, - new Container + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN }, + Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN }, - Child = new FillFlowContainer + Direction = FillDirection.Vertical, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] + new Container { - new Container - { - Margin = new MarginPadding { Vertical = 20 }, - RelativeSizeAxes = Axes.X, - Height = 40, - Depth = -float.MaxValue, - Child = dropdown = new SpotlightsDropdown - { - RelativeSizeAxes = Axes.X, - Current = Current - } - }, - new Container + Margin = new MarginPadding { Vertical = 20 }, + RelativeSizeAxes = Axes.X, + Height = 40, + Depth = -float.MaxValue, + Child = dropdown = new SpotlightsDropdown { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + Current = Current + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new FillFlowContainer { - new FillFlowContainer + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Margin = new MarginPadding { Bottom = 5 }, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Margin = new MarginPadding { Bottom = 5 }, - Children = new Drawable[] - { - startDateColumn = new InfoColumn(@"Start Date"), - endDateColumn = new InfoColumn(@"End Date"), - mapCountColumn = new InfoColumn(@"Map Count"), - participantsColumn = new InfoColumn(@"Participants") - } - }, - new RankingsSortTabControl - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Current = Sort + startDateColumn = new InfoColumn(@"Start Date"), + endDateColumn = new InfoColumn(@"End Date"), + mapCountColumn = new InfoColumn(@"Map Count"), + participantsColumn = new InfoColumn(@"Participants") } + }, + new RankingsSortTabControl + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Current = Sort } } } } } } - }); + }; } [BackgroundDependencyLoader] @@ -134,10 +124,6 @@ namespace osu.Game.Overlays.Rankings participantsColumn.Value = response.Spotlight.Participants?.ToString("N0"); } - protected override void PopIn() => content.FadeIn(duration, Easing.OutQuint); - - protected override void PopOut() => content.FadeOut(duration, Easing.OutQuint); - private string dateToString(DateTimeOffset date) => date.ToString("yyyy-MM-dd"); private class InfoColumn : FillFlowContainer diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index 0f9b07bf89..61339df76f 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -81,8 +81,6 @@ namespace osu.Game.Overlays.Rankings { base.LoadComplete(); - selector.Show(); - selectedSpotlight.BindValueChanged(_ => onSpotlightChanged()); sort.BindValueChanged(_ => onSpotlightChanged()); Ruleset.BindValueChanged(onRulesetChanged); From ad9492804a645ea851f815b23878e4ab98211f6c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 21 Jul 2020 22:56:44 +0300 Subject: [PATCH 2324/6909] Apply suggestions --- osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs | 2 +- osu.Game/Overlays/Profile/ProfileHeader.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs index 01c13dbc97..2a76b8e265 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneOverlayHeader.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); addHeader("Orange OverlayHeader (no background, 100 padding)", new TestNoBackgroundHeader(), OverlayColourScheme.Orange); - addHeader("Blue OverlayHeader (default 70 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue); + addHeader("Blue OverlayHeader (default 50 padding)", new TestNoControlHeader(), OverlayColourScheme.Blue); addHeader("Green TabControlOverlayHeader (string) with ruleset selector", new TestStringTabControlHeader(), OverlayColourScheme.Green); addHeader("Pink TabControlOverlayHeader (enum, 30 padding)", new TestEnumTabControlHeader(), OverlayColourScheme.Pink); addHeader("Red BreadcrumbControlOverlayHeader (no background, 10 padding)", new TestBreadcrumbControlHeader(), OverlayColourScheme.Red); diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 2895fa0726..2e5f1071f2 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Profile public ProfileHeader() { - ContentSidePadding = 70; + ContentSidePadding = UserProfileOverlay.CONTENT_X_MARGIN; User.ValueChanged += e => updateDisplay(e.NewValue); From cccb47e6e04e956dc4cfe73dfdff8c2bdc993526 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 11:29:23 +0900 Subject: [PATCH 2325/6909] Add user cover background to expanded version of score panel --- osu.Game/Screens/Ranking/ScorePanel.cs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 9633f5c533..5da432d5b2 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -13,6 +13,7 @@ using osu.Framework.Input.Events; using osu.Game.Scoring; using osu.Game.Screens.Ranking.Contracted; using osu.Game.Screens.Ranking.Expanded; +using osu.Game.Users; using osuTK; using osuTK.Graphics; @@ -142,7 +143,16 @@ namespace osu.Game.Screens.Ranking CornerRadius = 20, CornerExponent = 2.5f, Masking = true, - Child = middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both } + Children = new[] + { + middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both }, + new UserCoverBackground + { + RelativeSizeAxes = Axes.Both, + User = Score.User, + Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0)) + }, + } }, middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } } @@ -155,18 +165,10 @@ namespace osu.Game.Screens.Ranking { 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(); + + topLayerBackground.FinishTransforms(false, nameof(Colour)); + middleLayerBackground.FinishTransforms(false, nameof(Colour)); } private PanelState state = PanelState.Contracted; From fea6389f693947dd22d8f4bda9ecc30c33278cdc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 12:41:06 +0900 Subject: [PATCH 2326/6909] Hide HUD elements during break time by default --- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 8 ++-- osu.Game/Configuration/HUDVisibilityMode.cs | 17 ++++++++ osu.Game/Configuration/OsuConfigManager.cs | 4 +- .../Sections/Gameplay/GeneralSettings.cs | 6 +-- osu.Game/Screens/Play/HUDOverlay.cs | 42 +++++++++++++++---- osu.Game/Screens/Play/Player.cs | 1 + 6 files changed, 60 insertions(+), 18 deletions(-) create mode 100644 osu.Game/Configuration/HUDVisibilityMode.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index c192a7b0e0..e84e3cc930 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -65,17 +65,17 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExternalHideDoesntAffectConfig() { - bool originalConfigValue = false; + HUDVisibilityMode originalConfigValue = HUDVisibilityMode.DuringGameplay; createNew(); - AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.ShowInterface)); + AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode)); AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); - AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.ShowInterface)); + AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.HUDVisibilityMode)); AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); - AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.ShowInterface)); + AddAssert("config unchanged", () => originalConfigValue == config.Get(OsuSetting.HUDVisibilityMode)); } [Test] diff --git a/osu.Game/Configuration/HUDVisibilityMode.cs b/osu.Game/Configuration/HUDVisibilityMode.cs new file mode 100644 index 0000000000..2b133b1bcf --- /dev/null +++ b/osu.Game/Configuration/HUDVisibilityMode.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.ComponentModel; + +namespace osu.Game.Configuration +{ + public enum HUDVisibilityMode + { + Never, + + [Description("Hide during breaks")] + DuringGameplay, + + Always + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 268328272c..3ce71e8549 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -85,7 +85,7 @@ namespace osu.Game.Configuration Set(OsuSetting.HitLighting, true); - Set(OsuSetting.ShowInterface, true); + Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.DuringGameplay); Set(OsuSetting.ShowProgressGraph, true); Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); Set(OsuSetting.FadePlayfieldWhenHealthLow, true); @@ -184,7 +184,7 @@ namespace osu.Game.Configuration AlwaysPlayFirstComboBreak, ScoreMeter, FloatingComments, - ShowInterface, + HUDVisibilityMode, ShowProgressGraph, ShowHealthDisplayWhenCantFail, FadePlayfieldWhenHealthLow, diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 93a02ea0e4..af71c4f4e8 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -36,10 +36,10 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Lighten playfield during breaks", Bindable = config.GetBindable(OsuSetting.LightenDuringBreaks) }, - new SettingsCheckbox + new SettingsEnumDropdown { - LabelText = "Show score overlay", - Bindable = config.GetBindable(OsuSetting.ShowInterface) + LabelText = "Score overlay (HUD) mode", + Bindable = config.GetBindable(OsuSetting.HUDVisibilityMode) }, new SettingsCheckbox { diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index f09745cf71..ef1f80e0d5 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Play /// public Bindable ShowHud { get; } = new BindableBool(); - private Bindable configShowHud; + private Bindable configVisibilityMode; private readonly Container visibilityContainer; @@ -63,6 +63,8 @@ namespace osu.Game.Screens.Play private readonly Container topScoreContainer; + internal readonly IBindable IsBreakTime = new Bindable(); + private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods) @@ -139,9 +141,9 @@ namespace osu.Game.Screens.Play ModDisplay.Current.Value = mods; - configShowHud = config.GetBindable(OsuSetting.ShowInterface); + configVisibilityMode = config.GetBindable(OsuSetting.HUDVisibilityMode); - if (!configShowHud.Value && !hasShownNotificationOnce) + if (configVisibilityMode.Value == HUDVisibilityMode.Never && !hasShownNotificationOnce) { hasShownNotificationOnce = true; @@ -177,15 +179,33 @@ namespace osu.Game.Screens.Play } }, true); - configShowHud.BindValueChanged(visible => - { - if (!ShowHud.Disabled) - ShowHud.Value = visible.NewValue; - }, true); + IsBreakTime.BindValueChanged(_ => updateVisibility()); + configVisibilityMode.BindValueChanged(_ => updateVisibility(), true); replayLoaded.BindValueChanged(replayLoadedValueChanged, true); } + private void updateVisibility() + { + if (ShowHud.Disabled) + return; + + switch (configVisibilityMode.Value) + { + case HUDVisibilityMode.Never: + ShowHud.Value = false; + break; + + case HUDVisibilityMode.DuringGameplay: + ShowHud.Value = replayLoaded.Value || !IsBreakTime.Value; + break; + + case HUDVisibilityMode.Always: + ShowHud.Value = true; + break; + } + } + private void replayLoadedValueChanged(ValueChangedEvent e) { PlayerSettingsOverlay.ReplayLoaded = e.NewValue; @@ -202,6 +222,8 @@ namespace osu.Game.Screens.Play ModDisplay.Delay(2000).FadeOut(200); KeyCounter.Margin = new MarginPadding(10); } + + updateVisibility(); } protected virtual void BindDrawableRuleset(DrawableRuleset drawableRuleset) @@ -222,7 +244,9 @@ namespace osu.Game.Screens.Play switch (e.Key) { case Key.Tab: - configShowHud.Value = !configShowHud.Value; + configVisibilityMode.Value = configVisibilityMode.Value != HUDVisibilityMode.Never + ? HUDVisibilityMode.Never + : HUDVisibilityMode.DuringGameplay; return true; } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..50b2d5a021 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -612,6 +612,7 @@ namespace osu.Game.Screens.Play // bind component bindables. Background.IsBreakTime.BindTo(breakTracker.IsBreakTime); + HUDOverlay.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); From aca4110e36d03568e1ca2ceadeaf3df42a41093e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 12:47:53 +0900 Subject: [PATCH 2327/6909] Use existing star difficulty if non-local beatmap/ruleset --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index b469ca78fb..d94e04a79b 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -209,7 +209,8 @@ namespace osu.Game.Beatmaps // Difficulty can only be computed if the beatmap and ruleset are locally available. if (beatmapInfo.ID == 0 || rulesetInfo.ID == null) { - existingDifficulty = new StarDifficulty(0); + // If not, fall back to the existing star difficulty (e.g. from an online source). + existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty); key = default; return true; From 6b7f05740e51c77790295a0ba882c8b45d379bb2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 12:48:12 +0900 Subject: [PATCH 2328/6909] Fix potential missing ruleset --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index d94e04a79b..a9f34acd14 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -176,6 +176,9 @@ namespace osu.Game.Beatmaps /// The . private StarDifficulty computeDifficulty(in DifficultyCacheLookup key, BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo) { + // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. + rulesetInfo ??= beatmapInfo.Ruleset; + try { var ruleset = rulesetInfo.CreateInstance(); From ac602846df9d6a6be5b70672b537fe126cd0274e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 16:37:24 +0900 Subject: [PATCH 2329/6909] Expose balance and sample loading methods in DrawableHitObject --- .../Objects/Drawables/DrawableHitObject.cs | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index b633cb0860..f275153ce3 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (Result == null) throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); - loadSamples(); + LoadSamples(); } protected override void LoadComplete() @@ -145,14 +145,14 @@ namespace osu.Game.Rulesets.Objects.Drawables } samplesBindable = HitObject.SamplesBindable.GetBoundCopy(); - samplesBindable.CollectionChanged += (_, __) => loadSamples(); + samplesBindable.CollectionChanged += (_, __) => LoadSamples(); apply(HitObject); updateState(ArmedState.Idle, true); } - private void loadSamples() + protected virtual void LoadSamples() { if (Samples != null) { @@ -353,17 +353,32 @@ namespace osu.Game.Rulesets.Objects.Drawables [Resolved(canBeNull: true)] private GameplayClock gameplayClock { get; set; } + /// + /// Calculate the position to be used for sample playback at a specified X position (0..1). + /// + /// The lookup X position. Generally should be . + /// + protected double CalculateSamplePlaybackBalance(double position) + { + const float balance_adjust_amount = 0.4f; + + return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0); + } + + /// + /// Whether samples should currently be playing. Will be false during seek operations. + /// + protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true; + /// /// Plays all the hit sounds for this . /// This is invoked automatically when this is hit. /// public virtual void PlaySamples() { - const float balance_adjust_amount = 0.4f; - - if (Samples != null && gameplayClock?.IsSeeking != true) + if (Samples != null && ShouldPlaySamples) { - Samples.Balance.Value = balance_adjust_amount * (userPositionalHitSounds.Value ? SamplePlaybackPosition - 0.5f : 0); + Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition); Samples.Play(); } } From 3ed40d3a6b1fd14413920fc70208a71e4beebf99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 16:37:53 +0900 Subject: [PATCH 2330/6909] Fix SkinnableSounds not continuing playback on skin change --- osu.Game/Skinning/SkinnableSound.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 24d6648273..49f9f01cff 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -98,6 +98,8 @@ namespace osu.Game.Skinning protected override void SkinChanged(ISkinSource skin, bool allowFallback) { + bool wasPlaying = samplesContainer.Any(s => s.Playing); + var channels = hitSamples.Select(s => { var ch = skin.GetSample(s); @@ -121,6 +123,9 @@ namespace osu.Game.Skinning }).Where(c => c != null); samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); + + if (wasPlaying) + Play(); } } } From 2126f6bffc9d613706e12c4ef153c1fb7cb567ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 16:37:38 +0900 Subject: [PATCH 2331/6909] Add slider "sliding" sample support --- .../Objects/Drawables/DrawableSlider.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 72502c02cd..5059ec1231 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osuTK; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -11,6 +12,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.Osu.UI; using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -81,6 +83,41 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables foreach (var drawableHitObject in NestedHitObjects) drawableHitObject.AccentColour.Value = colour.NewValue; }, true); + + Tracking.BindValueChanged(updateSlidingSample); + } + + private SkinnableSound slidingSample; + + protected override void LoadSamples() + { + base.LoadSamples(); + + slidingSample?.Expire(); + + var firstSample = HitObject.Samples.FirstOrDefault(); + + if (firstSample != null) + { + var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); + clone.Name = "sliderslide"; + + AddInternal(slidingSample = new SkinnableSound(clone) + { + Looping = true + }); + } + } + + private void updateSlidingSample(ValueChangedEvent tracking) + { + // note that samples will not start playing if exiting a seek operation in the middle of a slider. + // may be something we want to address at a later point, but not so easy to make happen right now + // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update). + if (tracking.NewValue && ShouldPlaySamples) + slidingSample?.Play(); + else + slidingSample?.Stop(); } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -156,6 +193,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking.Value = Ball.Tracking; + if (Tracking.Value && slidingSample != null) + // keep the sliding sample playing at the current tracking position + slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X); + double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); Ball.UpdateProgress(completionProgress); From 0957c5f74ce0bdf1e3fc6524d18d0021780abb86 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 18:29:50 +0900 Subject: [PATCH 2332/6909] Re-namespace multiplayer requests/responses --- .../Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs | 1 - .../Requests/Responses => Multiplayer}/APICreatedRoom.cs | 3 +-- osu.Game/Online/{API => Multiplayer}/APIPlaylistBeatmap.cs | 2 +- .../{API/Requests/Responses => Multiplayer}/APIScoreToken.cs | 2 +- .../{API/Requests => Multiplayer}/CreateRoomRequest.cs | 5 ++--- .../{API/Requests => Multiplayer}/CreateRoomScoreRequest.cs | 4 ++-- .../Requests => Multiplayer}/GetRoomPlaylistScoresRequest.cs | 3 ++- .../Online/{API/Requests => Multiplayer}/GetRoomRequest.cs | 4 ++-- .../{API/Requests => Multiplayer}/GetRoomScoresRequest.cs | 3 ++- .../Online/{API/Requests => Multiplayer}/GetRoomsRequest.cs | 4 ++-- .../Online/{API/Requests => Multiplayer}/JoinRoomRequest.cs | 4 ++-- .../Online/{API/Requests => Multiplayer}/PartRoomRequest.cs | 4 ++-- osu.Game/Online/{API => Multiplayer}/RoomScore.cs | 4 ++-- .../{API/Requests => Multiplayer}/SubmitRoomScoreRequest.cs | 3 ++- osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs | 1 - osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs | 1 - osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs | 1 - osu.Game/Screens/Multi/RoomManager.cs | 1 - 18 files changed, 23 insertions(+), 27 deletions(-) rename osu.Game/Online/{API/Requests/Responses => Multiplayer}/APICreatedRoom.cs (78%) rename osu.Game/Online/{API => Multiplayer}/APIPlaylistBeatmap.cs (94%) rename osu.Game/Online/{API/Requests/Responses => Multiplayer}/APIScoreToken.cs (85%) rename osu.Game/Online/{API/Requests => Multiplayer}/CreateRoomRequest.cs (86%) rename osu.Game/Online/{API/Requests => Multiplayer}/CreateRoomScoreRequest.cs (90%) rename osu.Game/Online/{API/Requests => Multiplayer}/GetRoomPlaylistScoresRequest.cs (92%) rename osu.Game/Online/{API/Requests => Multiplayer}/GetRoomRequest.cs (84%) rename osu.Game/Online/{API/Requests => Multiplayer}/GetRoomScoresRequest.cs (89%) rename osu.Game/Online/{API/Requests => Multiplayer}/GetRoomsRequest.cs (94%) rename osu.Game/Online/{API/Requests => Multiplayer}/JoinRoomRequest.cs (90%) rename osu.Game/Online/{API/Requests => Multiplayer}/PartRoomRequest.cs (90%) rename osu.Game/Online/{API => Multiplayer}/RoomScore.cs (97%) rename osu.Game/Online/{API/Requests => Multiplayer}/SubmitRoomScoreRequest.cs (95%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 9fc7c336cb..0da1e11fee 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Threading.Tasks; 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; diff --git a/osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs b/osu.Game/Online/Multiplayer/APICreatedRoom.cs similarity index 78% rename from osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs rename to osu.Game/Online/Multiplayer/APICreatedRoom.cs index a554101bc7..2a3bb39647 100644 --- a/osu.Game/Online/API/Requests/Responses/APICreatedRoom.cs +++ b/osu.Game/Online/Multiplayer/APICreatedRoom.cs @@ -2,9 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using Newtonsoft.Json; -using osu.Game.Online.Multiplayer; -namespace osu.Game.Online.API.Requests.Responses +namespace osu.Game.Online.Multiplayer { public class APICreatedRoom : Room { diff --git a/osu.Game/Online/API/APIPlaylistBeatmap.cs b/osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs similarity index 94% rename from osu.Game/Online/API/APIPlaylistBeatmap.cs rename to osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs index 4f7786e880..98972ef36d 100644 --- a/osu.Game/Online/API/APIPlaylistBeatmap.cs +++ b/osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs @@ -6,7 +6,7 @@ using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; -namespace osu.Game.Online.API +namespace osu.Game.Online.Multiplayer { public class APIPlaylistBeatmap : APIBeatmap { diff --git a/osu.Game/Online/API/Requests/Responses/APIScoreToken.cs b/osu.Game/Online/Multiplayer/APIScoreToken.cs similarity index 85% rename from osu.Game/Online/API/Requests/Responses/APIScoreToken.cs rename to osu.Game/Online/Multiplayer/APIScoreToken.cs index 1d2465bedf..1f0063d94e 100644 --- a/osu.Game/Online/API/Requests/Responses/APIScoreToken.cs +++ b/osu.Game/Online/Multiplayer/APIScoreToken.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace osu.Game.Online.API.Requests.Responses +namespace osu.Game.Online.Multiplayer { public class APIScoreToken { diff --git a/osu.Game/Online/API/Requests/CreateRoomRequest.cs b/osu.Game/Online/Multiplayer/CreateRoomRequest.cs similarity index 86% rename from osu.Game/Online/API/Requests/CreateRoomRequest.cs rename to osu.Game/Online/Multiplayer/CreateRoomRequest.cs index c848c55cc6..dcb4ed51ea 100644 --- a/osu.Game/Online/API/Requests/CreateRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/CreateRoomRequest.cs @@ -4,10 +4,9 @@ using System.Net.Http; using Newtonsoft.Json; using osu.Framework.IO.Network; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class CreateRoomRequest : APIRequest { diff --git a/osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs similarity index 90% rename from osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs rename to osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs index e6246b4f1f..f973f96b37 100644 --- a/osu.Game/Online/API/Requests/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs @@ -3,9 +3,9 @@ using System.Net.Http; using osu.Framework.IO.Network; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class CreateRoomScoreRequest : APIRequest { diff --git a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs similarity index 92% rename from osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs rename to osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs index 38f852870b..833a761f42 100644 --- a/osu.Game/Online/API/Requests/GetRoomPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs @@ -3,8 +3,9 @@ using System.Collections.Generic; using Newtonsoft.Json; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class GetRoomPlaylistScoresRequest : APIRequest { diff --git a/osu.Game/Online/API/Requests/GetRoomRequest.cs b/osu.Game/Online/Multiplayer/GetRoomRequest.cs similarity index 84% rename from osu.Game/Online/API/Requests/GetRoomRequest.cs rename to osu.Game/Online/Multiplayer/GetRoomRequest.cs index 531e1857de..2907b49f1d 100644 --- a/osu.Game/Online/API/Requests/GetRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomRequest.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.Game.Online.Multiplayer; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class GetRoomRequest : APIRequest { diff --git a/osu.Game/Online/API/Requests/GetRoomScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs similarity index 89% rename from osu.Game/Online/API/Requests/GetRoomScoresRequest.cs rename to osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs index eb53369d18..bc913030dd 100644 --- a/osu.Game/Online/API/Requests/GetRoomScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs @@ -2,9 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class GetRoomScoresRequest : APIRequest> { diff --git a/osu.Game/Online/API/Requests/GetRoomsRequest.cs b/osu.Game/Online/Multiplayer/GetRoomsRequest.cs similarity index 94% rename from osu.Game/Online/API/Requests/GetRoomsRequest.cs rename to osu.Game/Online/Multiplayer/GetRoomsRequest.cs index c47ed20909..64e0386f77 100644 --- a/osu.Game/Online/API/Requests/GetRoomsRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomsRequest.cs @@ -4,10 +4,10 @@ using System.Collections.Generic; using Humanizer; using osu.Framework.IO.Network; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API; using osu.Game.Screens.Multi.Lounge.Components; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class GetRoomsRequest : APIRequest> { diff --git a/osu.Game/Online/API/Requests/JoinRoomRequest.cs b/osu.Game/Online/Multiplayer/JoinRoomRequest.cs similarity index 90% rename from osu.Game/Online/API/Requests/JoinRoomRequest.cs rename to osu.Game/Online/Multiplayer/JoinRoomRequest.cs index b0808afa45..74375af856 100644 --- a/osu.Game/Online/API/Requests/JoinRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/JoinRoomRequest.cs @@ -3,9 +3,9 @@ using System.Net.Http; using osu.Framework.IO.Network; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class JoinRoomRequest : APIRequest { diff --git a/osu.Game/Online/API/Requests/PartRoomRequest.cs b/osu.Game/Online/Multiplayer/PartRoomRequest.cs similarity index 90% rename from osu.Game/Online/API/Requests/PartRoomRequest.cs rename to osu.Game/Online/Multiplayer/PartRoomRequest.cs index c988cd5c9e..54bb005d96 100644 --- a/osu.Game/Online/API/Requests/PartRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/PartRoomRequest.cs @@ -3,9 +3,9 @@ using System.Net.Http; using osu.Framework.IO.Network; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class PartRoomRequest : APIRequest { diff --git a/osu.Game/Online/API/RoomScore.cs b/osu.Game/Online/Multiplayer/RoomScore.cs similarity index 97% rename from osu.Game/Online/API/RoomScore.cs rename to osu.Game/Online/Multiplayer/RoomScore.cs index 3c7f8c9833..97f378856f 100644 --- a/osu.Game/Online/API/RoomScore.cs +++ b/osu.Game/Online/Multiplayer/RoomScore.cs @@ -6,13 +6,13 @@ using System.Collections.Generic; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.API; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Users; -namespace osu.Game.Online.API +namespace osu.Game.Online.Multiplayer { public class RoomScore { diff --git a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs similarity index 95% rename from osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs rename to osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs index 8eb2952159..f725ea5dc9 100644 --- a/osu.Game/Online/API/Requests/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs @@ -4,9 +4,10 @@ using System.Net.Http; using Newtonsoft.Json; using osu.Framework.IO.Network; +using osu.Game.Online.API; using osu.Game.Scoring; -namespace osu.Game.Online.API.Requests +namespace osu.Game.Online.Multiplayer { public class SubmitRoomScoreRequest : APIRequest { diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index 571bbde716..1afbf5c32a 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Online.Multiplayer; diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index cf0197d26b..c2381fe219 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -10,7 +10,6 @@ using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Scoring; diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 5cafc974f1..f367d44347 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -9,7 +9,6 @@ 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; using osu.Game.Scoring; using osu.Game.Screens.Ranking; diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index 491be2e946..2a96fa536d 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -14,7 +14,6 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Screens.Multi.Lounge.Components; From e423630b7cbec15c0457089513ff0af85822591b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 18:37:00 +0900 Subject: [PATCH 2333/6909] Rename RoomScore -> MultiplayerScore --- .../Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs | 4 ++-- osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs | 2 +- .../Online/Multiplayer/{RoomScore.cs => MultiplayerScore.cs} | 2 +- osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename osu.Game/Online/Multiplayer/{RoomScore.cs => MultiplayerScore.cs} (98%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 0da1e11fee..0023866124 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -64,11 +64,11 @@ namespace osu.Game.Tests.Visual.Multiplayer private void bindHandler(double delay = 0) { - var roomScores = new List(); + var roomScores = new List(); for (int i = 0; i < 10; i++) { - roomScores.Add(new RoomScore + roomScores.Add(new MultiplayerScore { ID = i, Accuracy = 0.9 - 0.01 * i, diff --git a/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs index 833a761f42..3d3bd20ff3 100644 --- a/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs @@ -24,6 +24,6 @@ namespace osu.Game.Online.Multiplayer public class RoomPlaylistScores { [JsonProperty("scores")] - public List Scores { get; set; } + public List Scores { get; set; } } } diff --git a/osu.Game/Online/Multiplayer/RoomScore.cs b/osu.Game/Online/Multiplayer/MultiplayerScore.cs similarity index 98% rename from osu.Game/Online/Multiplayer/RoomScore.cs rename to osu.Game/Online/Multiplayer/MultiplayerScore.cs index 97f378856f..3bbf19b11f 100644 --- a/osu.Game/Online/Multiplayer/RoomScore.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerScore.cs @@ -14,7 +14,7 @@ using osu.Game.Users; namespace osu.Game.Online.Multiplayer { - public class RoomScore + public class MultiplayerScore { [JsonProperty("id")] public int ID { get; set; } diff --git a/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs index f725ea5dc9..d31aef2ea5 100644 --- a/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs @@ -9,7 +9,7 @@ using osu.Game.Scoring; namespace osu.Game.Online.Multiplayer { - public class SubmitRoomScoreRequest : APIRequest + public class SubmitRoomScoreRequest : APIRequest { private readonly int scoreId; private readonly int roomId; From d9633fee64a270364f618fc415d5d48d93a808e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 18:47:09 +0900 Subject: [PATCH 2334/6909] Rename request --- .../Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs | 2 +- ...PlaylistScoresRequest.cs => IndexPlaylistScoresRequest.cs} | 4 ++-- osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game/Online/Multiplayer/{GetRoomPlaylistScoresRequest.cs => IndexPlaylistScoresRequest.cs} (82%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 0023866124..37d31264b6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -97,7 +97,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { switch (request) { - case GetRoomPlaylistScoresRequest r: + case IndexPlaylistScoresRequest r: if (delay == 0) success(); else diff --git a/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs similarity index 82% rename from osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs rename to osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs index 3d3bd20ff3..c435dc6030 100644 --- a/osu.Game/Online/Multiplayer/GetRoomPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs @@ -7,12 +7,12 @@ using osu.Game.Online.API; namespace osu.Game.Online.Multiplayer { - public class GetRoomPlaylistScoresRequest : APIRequest + public class IndexPlaylistScoresRequest : APIRequest { private readonly int roomId; private readonly int playlistItemId; - public GetRoomPlaylistScoresRequest(int roomId, int playlistItemId) + public IndexPlaylistScoresRequest(int roomId, int playlistItemId) { this.roomId = roomId; this.playlistItemId = playlistItemId; diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index f367d44347..b90c7252c4 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Multi.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { - var req = new GetRoomPlaylistScoresRequest(roomId, playlistItem.ID); + var req = new IndexPlaylistScoresRequest(roomId, playlistItem.ID); req.Success += r => { From ec33a6ea8791b6878912b26dd9209063a45f5719 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 18:47:40 +0900 Subject: [PATCH 2335/6909] Add additional responses --- .../Online/Multiplayer/MultiplayerScores.cs | 39 +++++++++++++++++++ .../Multiplayer/MultiplayerScoresAround.cs | 25 ++++++++++++ .../Multiplayer/MultiplayerScoresSort.cs | 14 +++++++ 3 files changed, 78 insertions(+) create mode 100644 osu.Game/Online/Multiplayer/MultiplayerScores.cs create mode 100644 osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs create mode 100644 osu.Game/Online/Multiplayer/MultiplayerScoresSort.cs diff --git a/osu.Game/Online/Multiplayer/MultiplayerScores.cs b/osu.Game/Online/Multiplayer/MultiplayerScores.cs new file mode 100644 index 0000000000..f944a8999c --- /dev/null +++ b/osu.Game/Online/Multiplayer/MultiplayerScores.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using Newtonsoft.Json; +using osu.Game.Online.API.Requests; + +namespace osu.Game.Online.Multiplayer +{ + /// + /// An object which contains scores and related data for fetching next pages. + /// + public class MultiplayerScores + { + /// + /// To be used for fetching the next page. + /// + [JsonProperty("cursor")] + public Cursor Cursor { get; set; } + + /// + /// The scores. + /// + [JsonProperty("scores")] + public List Scores { get; set; } + + /// + /// The total scores in the playlist item. Only provided via . + /// + [JsonProperty("total")] + public int? TotalScores { get; set; } + + /// + /// The user's score, if any. Only provided via . + /// + [JsonProperty("user_score")] + public MultiplayerScore UserScore { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs b/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs new file mode 100644 index 0000000000..e83cc1b753 --- /dev/null +++ b/osu.Game/Online/Multiplayer/MultiplayerScoresAround.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 Newtonsoft.Json; + +namespace osu.Game.Online.Multiplayer +{ + /// + /// An object which stores scores higher and lower than the user's score. + /// + public class MultiplayerScoresAround + { + /// + /// Scores sorted "higher" than the user's score, depending on the sorting order. + /// + [JsonProperty("higher")] + public MultiplayerScores Higher { get; set; } + + /// + /// Scores sorted "lower" than the user's score, depending on the sorting order. + /// + [JsonProperty("lower")] + public MultiplayerScores Lower { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerScoresSort.cs b/osu.Game/Online/Multiplayer/MultiplayerScoresSort.cs new file mode 100644 index 0000000000..decb1c4dfe --- /dev/null +++ b/osu.Game/Online/Multiplayer/MultiplayerScoresSort.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.Multiplayer +{ + /// + /// Sorting option for indexing multiplayer scores. + /// + public enum MultiplayerScoresSort + { + Ascending, + Descending + } +} From 634efe31f843f53a8a86ae01319705b748398449 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 18:51:54 +0900 Subject: [PATCH 2336/6909] Inherit ResponseWithCursor --- osu.Game/Online/Multiplayer/MultiplayerScores.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerScores.cs b/osu.Game/Online/Multiplayer/MultiplayerScores.cs index f944a8999c..6f74fc8984 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScores.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerScores.cs @@ -10,14 +10,8 @@ namespace osu.Game.Online.Multiplayer /// /// An object which contains scores and related data for fetching next pages. /// - public class MultiplayerScores + public class MultiplayerScores : ResponseWithCursor { - /// - /// To be used for fetching the next page. - /// - [JsonProperty("cursor")] - public Cursor Cursor { get; set; } - /// /// The scores. /// From c75955e3819f5b7c8ec3a77af1bce267f2008633 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 18:52:25 +0900 Subject: [PATCH 2337/6909] Add responses to MultiplayerScore --- osu.Game/Online/Multiplayer/MultiplayerScore.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerScore.cs b/osu.Game/Online/Multiplayer/MultiplayerScore.cs index 3bbf19b11f..1793ba72ef 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScore.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerScore.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Converters; using osu.Game.Online.API; @@ -47,6 +48,19 @@ namespace osu.Game.Online.Multiplayer [JsonProperty("ended_at")] public DateTimeOffset EndedAt { get; set; } + /// + /// The position of this score, starting at 1. + /// + [JsonProperty("position")] + public int? Position { get; set; } + + /// + /// Any scores in the room around this score. + /// + [JsonProperty("scores_around")] + [CanBeNull] + public MultiplayerScoresAround ScoresAround { get; set; } + public ScoreInfo CreateScoreInfo(PlaylistItem playlistItem) { var rulesetInstance = playlistItem.Ruleset.Value.CreateInstance(); From 334fb7d4753386c6d534efe27e80e421a3b8a94f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 18:54:41 +0900 Subject: [PATCH 2338/6909] Add additional params to index request --- .../Multiplayer/IndexPlaylistScoresRequest.cs | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs index c435dc6030..b43614bf6c 100644 --- a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs @@ -3,19 +3,49 @@ using System.Collections.Generic; using Newtonsoft.Json; +using osu.Framework.IO.Network; +using osu.Game.Extensions; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; namespace osu.Game.Online.Multiplayer { + /// + /// Returns a list of scores for the specified playlist item. + /// public class IndexPlaylistScoresRequest : APIRequest { private readonly int roomId; private readonly int playlistItemId; + private readonly Cursor cursor; + private readonly MultiplayerScoresSort? sort; - public IndexPlaylistScoresRequest(int roomId, int playlistItemId) + public IndexPlaylistScoresRequest(int roomId, int playlistItemId, Cursor cursor = null, MultiplayerScoresSort? sort = null) { this.roomId = roomId; this.playlistItemId = playlistItemId; + this.cursor = cursor; + this.sort = sort; + } + + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + + req.AddCursor(cursor); + + switch (sort) + { + case MultiplayerScoresSort.Ascending: + req.AddParameter("sort", "scores_asc"); + break; + + case MultiplayerScoresSort.Descending: + req.AddParameter("sort", "scores_desc"); + break; + } + + return req; } protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores"; From 53a9ac3c1aa160bd1ccd8a23aa3833bd5f014e9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 19:06:39 +0900 Subject: [PATCH 2339/6909] Fix slider ball rotation being applied to follow circle and specular layer --- .../Objects/Drawables/Pieces/SliderBall.cs | 18 ++++-------- .../Skinning/LegacySliderBall.cs | 28 +++++++++++++++++-- 2 files changed, 30 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 395c76a233..b87e112d10 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private readonly Slider slider; private readonly Drawable followCircle; private readonly DrawableSlider drawableSlider; - private readonly CircularContainer ball; + private readonly Drawable ball; public SliderBall(Slider slider, DrawableSlider drawableSlider = null) { @@ -54,19 +54,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Alpha = 0, Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderFollowCircle), _ => new DefaultFollowCircle()), }, - ball = new CircularContainer + ball = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()) { - Masking = true, - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, Anchor = Anchor.Centre, - Alpha = 1, - Child = new Container - { - RelativeSizeAxes = Axes.Both, - Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBall), _ => new DefaultSliderBall()), - } - } + Origin = Anchor.Centre, + }, }; } @@ -187,7 +179,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; Position = newPos; - Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); + ball.Rotation = -90 + (float)(-Math.Atan2(diff.X, diff.Y) * 180 / Math.PI); lastPosition = newPos; } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index b4ed75d97c..0f586034d5 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -15,6 +15,9 @@ namespace osu.Game.Rulesets.Osu.Skinning { private readonly Drawable animationContent; + private Sprite layerNd; + private Sprite layerSpec; + public LegacySliderBall(Drawable animationContent) { this.animationContent = animationContent; @@ -29,18 +32,37 @@ namespace osu.Game.Rulesets.Osu.Skinning InternalChildren = new[] { - new Sprite + layerNd = new Sprite { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Texture = skin.GetTexture("sliderb-nd"), Colour = new Color4(5, 5, 5, 255), }, - animationContent, - new Sprite + animationContent.With(d => { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + }), + layerSpec = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Texture = skin.GetTexture("sliderb-spec"), Blending = BlendingParameters.Additive, }, }; } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + //undo rotation on layers which should not be rotated. + float appliedRotation = Parent.Rotation; + + layerNd.Rotation = -appliedRotation; + layerSpec.Rotation = -appliedRotation; + } } } From bd6a51f545a5121d98e57f3e8894094d3cb1e738 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 19:30:10 +0900 Subject: [PATCH 2340/6909] Hide slider repeat judgements temporarily --- 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 720ffcd51c..d79ecb7b4e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -23,6 +23,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Drawable scaleContainer; + public override bool DisplayResult => false; + public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) : base(sliderRepeat) { From f8401a76a25a59706226eee625dc479d13116c10 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 19:40:00 +0900 Subject: [PATCH 2341/6909] Use show/index requests in results screen --- .../ShowPlaylistUserScoreRequest.cs | 23 +++++++ .../Multi/Ranking/TimeshiftResultsScreen.cs | 63 +++++++++++++++++-- 2 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs diff --git a/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs new file mode 100644 index 0000000000..936b8bbe89 --- /dev/null +++ b/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs @@ -0,0 +1,23 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.API; + +namespace osu.Game.Online.Multiplayer +{ + public class ShowPlaylistUserScoreRequest : APIRequest + { + private readonly int roomId; + private readonly int playlistItemId; + private readonly long userId; + + public ShowPlaylistUserScoreRequest(int roomId, int playlistItemId, long userId) + { + this.roomId = roomId; + this.playlistItemId = playlistItemId; + this.userId = userId; + } + + protected override string Target => $"rooms/{roomId}/playlist/{playlistItemId}/scores/users/{userId}"; + } +} diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index b90c7252c4..47aab02b1a 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -9,6 +9,7 @@ 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; using osu.Game.Scoring; using osu.Game.Screens.Ranking; @@ -21,6 +22,11 @@ namespace osu.Game.Screens.Multi.Ranking private readonly PlaylistItem playlistItem; private LoadingSpinner loadingLayer; + private Cursor higherScoresCursor; + private Cursor lowerScoresCursor; + + [Resolved] + private IAPIProvider api { get; set; } public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true) : base(score, allowRetry) @@ -44,17 +50,62 @@ namespace osu.Game.Screens.Multi.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { - var req = new IndexPlaylistScoresRequest(roomId, playlistItem.ID); + // This performs two requests: + // 1. A request to show the user's score. + // 2. If (1) fails, a request to index the room. - req.Success += r => + var userScoreReq = new ShowPlaylistUserScoreRequest(roomId, playlistItem.ID, api.LocalUser.Value.Id); + + userScoreReq.Success += userScore => { - scoresCallback?.Invoke(r.Scores.Where(s => s.ID != Score?.OnlineScoreID).Select(s => s.CreateScoreInfo(playlistItem))); - loadingLayer.Hide(); + var allScores = new List { userScore }; + + if (userScore.ScoresAround?.Higher != null) + { + allScores.AddRange(userScore.ScoresAround.Higher.Scores); + higherScoresCursor = userScore.ScoresAround.Higher.Cursor; + } + + if (userScore.ScoresAround?.Lower != null) + { + allScores.AddRange(userScore.ScoresAround.Lower.Scores); + lowerScoresCursor = userScore.ScoresAround.Lower.Cursor; + } + + success(allScores); }; - req.Failure += _ => loadingLayer.Hide(); + userScoreReq.Failure += _ => + { + // Fallback to a normal index. + var indexReq = new IndexPlaylistScoresRequest(roomId, playlistItem.ID); + indexReq.Success += r => success(r.Scores); + indexReq.Failure += __ => loadingLayer.Hide(); + api.Queue(indexReq); + }; - return req; + return userScoreReq; + + void success(List scores) + { + var scoreInfos = new List(scores.Select(s => s.CreateScoreInfo(playlistItem))); + + // Select a score if we don't already have one selected. + // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). + if (SelectedScore.Value == null) + { + Schedule(() => + { + // Prefer selecting the local user's score, or otherwise default to the first visible score. + SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); + }); + } + + // Invoke callback to add the scores. Exclude the user's current score which was added previously. + scoresCallback?.Invoke(scoreInfos.Where(s => s.ID != Score?.OnlineScoreID)); + + loadingLayer.Hide(); + } } } } From 798bf0503818856b48f150b4598c2e01f340fdaf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 19:43:48 +0900 Subject: [PATCH 2342/6909] 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 71d4e5aacf..c0c75b8d71 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 2f3d08c528..e8c333b6b1 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 2bb3914c25..8d1b837995 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 2c62b23d859d46b1e4f3c21ea18e8c52a910b9c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 19:53:45 +0900 Subject: [PATCH 2343/6909] Update naming --- .../Replays/CatchFramedReplayInputHandler.cs | 4 ++-- .../Replays/ManiaFramedReplayInputHandler.cs | 4 ++-- .../Replays/OsuFramedReplayInputHandler.cs | 15 +++------------ .../Replays/TaikoFramedReplayInputHandler.cs | 4 ++-- .../Visual/Gameplay/TestSceneReplayRecorder.cs | 2 +- .../Visual/Gameplay/TestSceneReplayRecording.cs | 2 +- 6 files changed, 11 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs index 24c21fbc84..99d899db80 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchFramedReplayInputHandler.cs @@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Catch.Replays } } - public override void GetPendingInputs(List input) + public override void CollectPendingInputs(List inputs) { if (!Position.HasValue) return; - input.Add(new CatchReplayState + inputs.Add(new CatchReplayState { PressedActions = CurrentFrame?.Actions ?? new List(), CatcherX = Position.Value diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs index 26c4ccf289..aa0c148caf 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaFramedReplayInputHandler.cs @@ -18,9 +18,9 @@ namespace osu.Game.Rulesets.Mania.Replays protected override bool IsImportant(ManiaReplayFrame frame) => frame.Actions.Any(); - public override void GetPendingInputs(List input) + public override void CollectPendingInputs(List inputs) { - input.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); + inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); } } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs index 5c803539c2..cf48dc053f 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuFramedReplayInputHandler.cs @@ -36,19 +36,10 @@ namespace osu.Game.Rulesets.Osu.Replays } } - public override void GetPendingInputs(List input) + public override void CollectPendingInputs(List inputs) { - input.Add( - new MousePositionAbsoluteInput - { - Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) - }); - input.Add( - new ReplayState - { - PressedActions = CurrentFrame?.Actions ?? new List() - }); - ; + inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(Position ?? Vector2.Zero) }); + inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); } } } diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs index 7361d4efa8..138e8f9785 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoFramedReplayInputHandler.cs @@ -18,9 +18,9 @@ namespace osu.Game.Rulesets.Taiko.Replays protected override bool IsImportant(TaikoReplayFrame frame) => frame.Actions.Any(); - public override void GetPendingInputs(List input) + public override void CollectPendingInputs(List inputs) { - input.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); + inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index e473f49826..bc1c10e59d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -173,7 +173,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public override void GetPendingInputs(List inputs) + public override void CollectPendingInputs(List inputs) { inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) }); inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index e891ed617a..c0f99db85d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -113,7 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - public override void GetPendingInputs(List inputs) + public override void CollectPendingInputs(List inputs) { inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) }); inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); From 568fb51ce239c8f2fbe26ab63d83ea1a508bb65e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 20:24:42 +0900 Subject: [PATCH 2344/6909] Remove RoomPlaylistScores intermediate class --- .../Multiplayer/TestSceneTimeshiftResultsScreen.cs | 2 +- .../Online/Multiplayer/IndexPlaylistScoresRequest.cs | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 37d31264b6..44ca676c4f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Multiplayer void success() { - r.TriggerSuccess(new RoomPlaylistScores { Scores = roomScores }); + r.TriggerSuccess(new MultiplayerScores { Scores = roomScores }); roomsReceived = true; } diff --git a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs index b43614bf6c..d23208d338 100644 --- a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.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 Newtonsoft.Json; using osu.Framework.IO.Network; using osu.Game.Extensions; using osu.Game.Online.API; @@ -13,7 +11,7 @@ namespace osu.Game.Online.Multiplayer /// /// Returns a list of scores for the specified playlist item. /// - public class IndexPlaylistScoresRequest : APIRequest + public class IndexPlaylistScoresRequest : APIRequest { private readonly int roomId; private readonly int playlistItemId; @@ -50,10 +48,4 @@ namespace osu.Game.Online.Multiplayer protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores"; } - - public class RoomPlaylistScores - { - [JsonProperty("scores")] - public List Scores { get; set; } - } } From b7790de66fbc2d11126fb0c0dadf17090c647aa0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 20:24:48 +0900 Subject: [PATCH 2345/6909] Fix incorrect sort param --- osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs index d23208d338..7273c0eea6 100644 --- a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs @@ -35,11 +35,11 @@ namespace osu.Game.Online.Multiplayer switch (sort) { case MultiplayerScoresSort.Ascending: - req.AddParameter("sort", "scores_asc"); + req.AddParameter("sort", "score_asc"); break; case MultiplayerScoresSort.Descending: - req.AddParameter("sort", "scores_desc"); + req.AddParameter("sort", "score_desc"); break; } From 46ea775cfb36d15a3dc7a13098bd62c18cbb7987 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 22 Jul 2020 20:24:55 +0900 Subject: [PATCH 2346/6909] Implement paging --- .../Multi/Ranking/TimeshiftResultsScreen.cs | 84 +++++++++++++++---- osu.Game/Screens/Ranking/ResultsScreen.cs | 41 +++++++-- osu.Game/Screens/Ranking/ScorePanelList.cs | 4 + 3 files changed, 109 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 47aab02b1a..75a61b92ee 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -72,40 +73,93 @@ namespace osu.Game.Screens.Multi.Ranking lowerScoresCursor = userScore.ScoresAround.Lower.Cursor; } - success(allScores); + performSuccessCallback(scoresCallback, allScores); }; userScoreReq.Failure += _ => { // Fallback to a normal index. var indexReq = new IndexPlaylistScoresRequest(roomId, playlistItem.ID); - indexReq.Success += r => success(r.Scores); + + indexReq.Success += r => + { + performSuccessCallback(scoresCallback, r.Scores); + lowerScoresCursor = r.Cursor; + }; + indexReq.Failure += __ => loadingLayer.Hide(); + api.Queue(indexReq); }; return userScoreReq; + } - void success(List scores) + protected override APIRequest FetchNextPage(int direction, Action> scoresCallback) + { + Debug.Assert(direction == 1 || direction == -1); + + Cursor cursor; + MultiplayerScoresSort sort; + + switch (direction) { - var scoreInfos = new List(scores.Select(s => s.CreateScoreInfo(playlistItem))); + case -1: + cursor = higherScoresCursor; + sort = MultiplayerScoresSort.Ascending; + break; - // Select a score if we don't already have one selected. - // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). - if (SelectedScore.Value == null) + default: + cursor = lowerScoresCursor; + sort = MultiplayerScoresSort.Descending; + break; + } + + if (cursor == null) + return null; + + var indexReq = new IndexPlaylistScoresRequest(roomId, playlistItem.ID, cursor, sort); + + indexReq.Success += r => + { + switch (direction) { - Schedule(() => - { - // Prefer selecting the local user's score, or otherwise default to the first visible score. - SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); - }); + case -1: + higherScoresCursor = r.Cursor; + break; + + default: + lowerScoresCursor = r.Cursor; + break; } - // Invoke callback to add the scores. Exclude the user's current score which was added previously. - scoresCallback?.Invoke(scoreInfos.Where(s => s.ID != Score?.OnlineScoreID)); + performSuccessCallback(scoresCallback, r.Scores); + }; - loadingLayer.Hide(); + indexReq.Failure += _ => loadingLayer.Hide(); + + return indexReq; + } + + private void performSuccessCallback(Action> callback, List scores) + { + var scoreInfos = new List(scores.Select(s => s.CreateScoreInfo(playlistItem))); + + // Select a score if we don't already have one selected. + // Note: This is done before the callback so that the panel list centres on the selected score before panels are added (eliminating initial scroll). + if (SelectedScore.Value == null) + { + Schedule(() => + { + // Prefer selecting the local user's score, or otherwise default to the first visible score. + SelectedScore.Value = scoreInfos.FirstOrDefault(s => s.User.Id == api.LocalUser.Value.Id) ?? scoreInfos.FirstOrDefault(); + }); } + + // Invoke callback to add the scores. Exclude the user's current score which was added previously. + callback?.Invoke(scoreInfos.Where(s => s.ID != Score?.OnlineScoreID)); + + loadingLayer.Hide(); } } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 44458d8c8e..c5512822b2 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -164,11 +164,7 @@ namespace osu.Game.Screens.Ranking { base.LoadComplete(); - var req = FetchScores(scores => Schedule(() => - { - foreach (var s in scores) - addScore(s); - })); + var req = FetchScores(fetchScoresCallback); if (req != null) api.Queue(req); @@ -176,6 +172,29 @@ namespace osu.Game.Screens.Ranking statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } + private APIRequest nextPageRequest; + + protected override void Update() + { + base.Update(); + + if (hasAnyScores && nextPageRequest == null) + { + if (scorePanelList.IsScrolledToStart) + nextPageRequest = FetchNextPage(-1, fetchScoresCallback); + else if (scorePanelList.IsScrolledToEnd) + nextPageRequest = FetchNextPage(1, fetchScoresCallback); + + if (nextPageRequest != null) + { + nextPageRequest.Success += () => nextPageRequest = null; + nextPageRequest.Failure += _ => nextPageRequest = null; + + api.Queue(nextPageRequest); + } + } + } + /// /// Performs a fetch/refresh of scores to be displayed. /// @@ -183,6 +202,18 @@ 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 virtual APIRequest FetchNextPage(int direction, Action> scoresCallback) => null; + + private bool hasAnyScores; + + private void fetchScoresCallback(IEnumerable scores) => Schedule(() => + { + foreach (var s in scores) + addScore(s); + + hasAnyScores = true; + }); + public override void OnEntering(IScreen last) { base.OnEntering(last); diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 0f8bc82ac0..aba8314732 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -26,6 +26,10 @@ namespace osu.Game.Screens.Ranking /// private const float expanded_panel_spacing = 15; + public bool IsScrolledToStart => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.Current <= 100; + + public bool IsScrolledToEnd => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(100); + /// /// An action to be invoked if a is clicked while in an expanded state. /// From 113fac84ddf195b10d1ae3a9b1cd1e437c94d0ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 22 Jul 2020 21:14:04 +0900 Subject: [PATCH 2347/6909] Fix circle container type --- 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 b87e112d10..07dc6021c9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -184,7 +184,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces lastPosition = newPos; } - private class FollowCircleContainer : Container + private class FollowCircleContainer : CircularContainer { public override bool HandlePositionalInput => true; } From 50f72ac9cb90074f647d76085b515d4ce8d9b45d Mon Sep 17 00:00:00 2001 From: jorolf Date: Wed, 22 Jul 2020 22:10:59 +0200 Subject: [PATCH 2348/6909] rename classes --- ...neHueAnimation.cs => TestSceneLogoAnimation.cs} | 10 +++++----- .../Sprites/{HueAnimation.cs => LogoAnimation.cs} | 14 +++++++------- osu.Game/Screens/Menu/IntroTriangles.cs | 6 +++--- 3 files changed, 15 insertions(+), 15 deletions(-) rename osu.Game.Tests/Visual/UserInterface/{TestSceneHueAnimation.cs => TestSceneLogoAnimation.cs} (85%) rename osu.Game/Graphics/Sprites/{HueAnimation.cs => LogoAnimation.cs} (79%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs similarity index 85% rename from osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs index 9c5888d072..155d043bf9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneHueAnimation.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoAnimation.cs @@ -11,14 +11,14 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Tests.Visual.UserInterface { [TestFixture] - public class TestSceneHueAnimation : OsuTestScene + public class TestSceneLogoAnimation : OsuTestScene { [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - HueAnimation anim2; + LogoAnimation anim2; - Add(anim2 = new HueAnimation + Add(anim2 = new LogoAnimation { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, @@ -26,9 +26,9 @@ namespace osu.Game.Tests.Visual.UserInterface Colour = Colour4.White, }); - HueAnimation anim; + LogoAnimation anim; - Add(anim = new HueAnimation + Add(anim = new LogoAnimation { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, diff --git a/osu.Game/Graphics/Sprites/HueAnimation.cs b/osu.Game/Graphics/Sprites/LogoAnimation.cs similarity index 79% rename from osu.Game/Graphics/Sprites/HueAnimation.cs rename to osu.Game/Graphics/Sprites/LogoAnimation.cs index 8ad68ace05..b1383065fe 100644 --- a/osu.Game/Graphics/Sprites/HueAnimation.cs +++ b/osu.Game/Graphics/Sprites/LogoAnimation.cs @@ -11,13 +11,13 @@ using osu.Framework.Graphics.Textures; namespace osu.Game.Graphics.Sprites { - public class HueAnimation : Sprite + public class LogoAnimation : Sprite { [BackgroundDependencyLoader] private void load(ShaderManager shaders, TextureStore textures) { - TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"HueAnimation"); - RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"HueAnimation"); // Masking isn't supported for now + TextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation"); + RoundedTextureShader = shaders.Load(VertexShaderDescriptor.TEXTURE_2, @"LogoAnimation"); // Masking isn't supported for now } private float animationProgress; @@ -36,15 +36,15 @@ namespace osu.Game.Graphics.Sprites public override bool IsPresent => true; - protected override DrawNode CreateDrawNode() => new HueAnimationDrawNode(this); + protected override DrawNode CreateDrawNode() => new LogoAnimationDrawNode(this); - private class HueAnimationDrawNode : SpriteDrawNode + private class LogoAnimationDrawNode : SpriteDrawNode { - private HueAnimation source => (HueAnimation)Source; + private LogoAnimation source => (LogoAnimation)Source; private float progress; - public HueAnimationDrawNode(HueAnimation source) + public LogoAnimationDrawNode(LogoAnimation source) : base(source) { } diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index b56ba6c8a4..a9ef20436f 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -260,7 +260,7 @@ namespace osu.Game.Screens.Menu private class LazerLogo : CompositeDrawable { - private HueAnimation highlight, background; + private LogoAnimation highlight, background; public float Progress { @@ -282,13 +282,13 @@ namespace osu.Game.Screens.Menu { InternalChildren = new Drawable[] { - highlight = new HueAnimation + highlight = new LogoAnimation { RelativeSizeAxes = Axes.Both, Texture = textures.Get(@"Intro/Triangles/logo-highlight"), Colour = Color4.White, }, - background = new HueAnimation + background = new LogoAnimation { RelativeSizeAxes = Axes.Both, Texture = textures.Get(@"Intro/Triangles/logo-background"), From ee05d5cb14b7d946a0335f9f7208b6213da6ed57 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jul 2020 09:06:15 +0900 Subject: [PATCH 2349/6909] Remove no-longer-necessary play trigger on skin change --- osu.Game/Screens/Play/PauseOverlay.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index e74585990a..fa917cda32 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -39,10 +39,6 @@ namespace osu.Game.Screens.Play Looping = true, }); - // PopIn is called before updating the skin, and when a sample is updated, its "playing" value is reset - // the sample must be played again - pauseLoop.OnSkinChanged += () => pauseLoop.Play(); - // SkinnableSound only plays a sound if its aggregate volume is > 0, so the volume must be turned up before playing it pauseLoop.VolumeTo(minimum_volume); } From 4102dae999cb7f63294b033898885d50afbc799b Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 22 Jul 2020 21:45:27 +0200 Subject: [PATCH 2350/6909] Revert commit 939441ae --- osu.Desktop/Windows/GameplayWinKeyHandler.cs | 14 +++++++------- osu.Game/Configuration/SessionStatics.cs | 4 +--- osu.Game/Screens/Play/Player.cs | 2 +- .../Screens/Play/ScreenSuspensionHandler.cs | 19 ++----------------- 4 files changed, 11 insertions(+), 28 deletions(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs index d5ef89c680..4f74a4f492 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.cs @@ -11,26 +11,26 @@ namespace osu.Desktop.Windows { public class GameplayWinKeyHandler : Component { + private Bindable allowScreenSuspension; private Bindable disableWinKey; - private Bindable disableWinKeySetting; private GameHost host; [BackgroundDependencyLoader] - private void load(GameHost host, OsuConfigManager config, SessionStatics statics) + private void load(GameHost host, OsuConfigManager config) { this.host = host; - disableWinKey = statics.GetBindable(Static.DisableWindowsKey); - disableWinKey.ValueChanged += toggleWinKey; + allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy(); + allowScreenSuspension.ValueChanged += toggleWinKey; - disableWinKeySetting = config.GetBindable(OsuSetting.GameplayDisableWinKey); - disableWinKeySetting.BindValueChanged(t => disableWinKey.TriggerChange(), true); + disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); + disableWinKey.BindValueChanged(t => allowScreenSuspension.TriggerChange(), true); } private void toggleWinKey(ValueChangedEvent e) { - if (e.NewValue && disableWinKeySetting.Value) + if (!e.NewValue && disableWinKey.Value) host.InputThread.Scheduler.Add(WindowsKey.Disable); else host.InputThread.Scheduler.Add(WindowsKey.Enable); diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 7aad79b5ad..40b2adb867 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -12,14 +12,12 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); - Set(Static.DisableWindowsKey, false); } } public enum Static { LoginOverlayDisplayed, - MutedAudioNotificationShownOnce, - DisableWindowsKey + MutedAudioNotificationShownOnce } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e0721d55f7..541275cf55 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -181,7 +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, DrawableRuleset.HasReplayLoaded)); + AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); dependencies.CacheAs(gameplayBeatmap); diff --git a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs index 6865db5a5e..8585a5c309 100644 --- a/osu.Game/Screens/Play/ScreenSuspensionHandler.cs +++ b/osu.Game/Screens/Play/ScreenSuspensionHandler.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; -using osu.Game.Configuration; namespace osu.Game.Screens.Play { @@ -19,18 +18,13 @@ namespace osu.Game.Screens.Play { private readonly GameplayClockContainer gameplayClockContainer; private Bindable isPaused; - private readonly Bindable hasReplayLoaded; [Resolved] private GameHost host { get; set; } - [Resolved] - private SessionStatics statics { get; set; } - - public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer, Bindable hasReplayLoaded) + public ScreenSuspensionHandler([NotNull] GameplayClockContainer gameplayClockContainer) { this.gameplayClockContainer = gameplayClockContainer ?? throw new ArgumentNullException(nameof(gameplayClockContainer)); - this.hasReplayLoaded = hasReplayLoaded.GetBoundCopy(); } protected override void LoadComplete() @@ -42,12 +36,7 @@ namespace osu.Game.Screens.Play Debug.Assert(host.AllowScreenSuspension.Value); isPaused = gameplayClockContainer.IsPaused.GetBoundCopy(); - isPaused.BindValueChanged(paused => - { - host.AllowScreenSuspension.Value = paused.NewValue; - statics.Set(Static.DisableWindowsKey, !paused.NewValue && !hasReplayLoaded.Value); - }, true); - hasReplayLoaded.BindValueChanged(_ => isPaused.TriggerChange(), true); + isPaused.BindValueChanged(paused => host.AllowScreenSuspension.Value = paused.NewValue, true); } protected override void Dispose(bool isDisposing) @@ -55,13 +44,9 @@ namespace osu.Game.Screens.Play base.Dispose(isDisposing); isPaused?.UnbindAll(); - hasReplayLoaded.UnbindAll(); if (host != null) - { host.AllowScreenSuspension.Value = true; - statics.Set(Static.DisableWindowsKey, false); - } } } } From acff270e969cde58d12893fc891351d3d06afdbd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jul 2020 19:14:18 +0900 Subject: [PATCH 2351/6909] Fix failing test by moving slider closer --- osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index c3b4d2625e..854626d362 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Osu.Tests const double time_slider = 1500; const double time_circle = 1510; Vector2 positionCircle = Vector2.Zero; - Vector2 positionSlider = new Vector2(80); + Vector2 positionSlider = new Vector2(30); var hitObjects = new List { From 5e6adfff99b1b348897ab4606aef7f910016560c Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 23 Jul 2020 12:45:14 +0200 Subject: [PATCH 2352/6909] Disable windows key only while in gameplay. --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Desktop/Windows/GameplayWinKeyHandler.cs | 15 ++++++++++++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index d05a4af126..6eefee3b50 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -101,7 +101,7 @@ namespace osu.Desktop LoadComponentAsync(new DiscordRichPresence(), Add); if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) - LoadComponentAsync(new GameplayWinKeyHandler(), Add); + LoadComponentAsync(new GameplayWinKeyHandler(ScreenStack), Add); } protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyHandler.cs index 4f74a4f492..96154356d0 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyHandler.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; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Game.Configuration; +using osu.Game.Screens; +using osu.Game.Screens.Play; namespace osu.Desktop.Windows { @@ -14,8 +17,16 @@ namespace osu.Desktop.Windows private Bindable allowScreenSuspension; private Bindable disableWinKey; + private readonly OsuScreenStack screenStack; private GameHost host; + private Type currentScreenType => screenStack.CurrentScreen?.GetType(); + + public GameplayWinKeyHandler(OsuScreenStack stack) + { + screenStack = stack; + } + [BackgroundDependencyLoader] private void load(GameHost host, OsuConfigManager config) { @@ -30,7 +41,9 @@ namespace osu.Desktop.Windows private void toggleWinKey(ValueChangedEvent e) { - if (!e.NewValue && disableWinKey.Value) + var isPlayer = typeof(Player).IsAssignableFrom(currentScreenType) && currentScreenType != typeof(ReplayPlayer); + + if (!e.NewValue && disableWinKey.Value && isPlayer) host.InputThread.Scheduler.Add(WindowsKey.Disable); else host.InputThread.Scheduler.Add(WindowsKey.Enable); From f883cb85d72ff2f98ec87a1f207e239b805e2c8b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Jul 2020 21:24:31 +0900 Subject: [PATCH 2353/6909] Null out the sample too --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5059ec1231..07f40f763b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -94,6 +94,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.LoadSamples(); slidingSample?.Expire(); + slidingSample = null; var firstSample = HitObject.Samples.FirstOrDefault(); From d0b35d7b32895ff3f988c0f6e85fa86eaacaad0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 23 Jul 2020 22:13:37 +0900 Subject: [PATCH 2354/6909] 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 c0c75b8d71..e5b0245dd0 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 e8c333b6b1..5af28ae11a 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 8d1b837995..4a94ec33d8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 76284a0f018f2c8c3a502db24360081e0c1f5996 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 23 Jul 2020 23:18:43 +0900 Subject: [PATCH 2355/6909] Move cancellation out of condition --- .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index d5aeecae04..1b5b448e1f 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -188,12 +188,11 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value != CarouselItemState.Collapsed && Alpha == 0) starCounter.ReplayAnimation(); - if (Item.State.Value == CarouselItemState.Collapsed) - starDifficultyCancellationSource?.Cancel(); - else - { - starDifficultyCancellationSource?.Cancel(); + starDifficultyCancellationSource?.Cancel(); + // Only compute difficulty when the item is visible. + if (Item.State.Value != CarouselItemState.Collapsed) + { // We've potentially cancelled the computation above so a new bindable is required. starDifficultyBindable = difficultyManager.GetTrackedBindable(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true); From f75f1231b7f2300b260e25e7dc8f2d4d273b2bc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 10:41:09 +0900 Subject: [PATCH 2356/6909] Invert conditional for readability --- osu.Game/Graphics/Cursor/MenuCursorContainer.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs index 02bfb3fad6..3015c44613 100644 --- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs +++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs @@ -58,10 +58,11 @@ namespace osu.Game.Graphics.Cursor foreach (var d in inputManager.HoveredDrawables) { - if (!(d is IProvideCursor p) || !p.ProvidingUserCursor) continue; - - newTarget = p; - break; + if (d is IProvideCursor p && p.ProvidingUserCursor) + { + newTarget = p; + break; + } } if (currentTarget == newTarget) From 264bd7ced1c8a8caab663eebba2114bfefa766a9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 13:38:53 +0900 Subject: [PATCH 2357/6909] Apply general refactoring from review --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index a9f34acd14..12d472e8c6 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes. private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager)); - // A cache that keeps references to BeatmapInfos for 60sec. + // A permanent cache to prevent re-computations. private readonly ConcurrentDictionary difficultyCache = new ConcurrentDictionary(); // All bindables that should be updated along with the current ruleset + mods. @@ -48,29 +48,29 @@ namespace osu.Game.Beatmaps } /// - /// Retrieves an containing the star difficulty of a with a given and combination. + /// Retrieves a bindable containing the star difficulty of a with a given and combination. /// /// - /// This will not update to follow the currently-selected ruleset and mods. + /// The bindable will not update to follow the currently-selected ruleset and mods. /// /// The to get the difficulty of. /// The to get the difficulty with. /// The s to get the difficulty with. /// An optional which stops updating the star difficulty for the given . - /// An that is updated to contain the star difficulty when it becomes available. + /// A bindable that is updated to contain the star difficulty when it becomes available. public IBindable GetUntrackedBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); /// - /// Retrieves a containing the star difficulty of a that follows the user's currently-selected ruleset and mods. + /// Retrieves a bindable containing the star difficulty of a that follows the user's currently-selected ruleset and mods. /// /// - /// Ensure to hold a local reference of the returned in order to receive value-changed events. + /// Ensure to hold a local reference of the returned bindable in order to receive value-changed events. /// /// The to get the difficulty of. /// An optional which stops updating the star difficulty for the given . - /// An that is updated to contain the star difficulty when it becomes available, or when the currently-selected ruleset and mods change. + /// A bindable that is updated to contain the star difficulty when it becomes available, or when the currently-selected ruleset and mods change. public IBindable GetTrackedBindable([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) { var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); @@ -89,7 +89,7 @@ namespace osu.Game.Beatmaps public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, CancellationToken cancellationToken = default) { - if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) + if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken, @@ -105,7 +105,7 @@ namespace osu.Game.Beatmaps /// The . public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) { - if (tryGetGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) + if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; return computeDifficulty(key, beatmapInfo, rulesetInfo); @@ -204,7 +204,7 @@ namespace osu.Game.Beatmaps /// The existing difficulty value, if present. /// The key that was used to perform this lookup. This can be further used to query . /// Whether an existing difficulty was found. - private bool tryGetGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) + private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; From de007cc1c63da3bd88ee52881a31f8cce91c2ec0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 13:40:01 +0900 Subject: [PATCH 2358/6909] Use IEnumerable mods instead of IReadOnlyList --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 12d472e8c6..914874e210 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -58,7 +58,7 @@ namespace osu.Game.Beatmaps /// The s to get the difficulty with. /// An optional which stops updating the star difficulty for the given . /// A bindable that is updated to contain the star difficulty when it becomes available. - public IBindable GetUntrackedBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + public IBindable GetUntrackedBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null, CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); @@ -86,7 +86,7 @@ namespace osu.Game.Beatmaps /// The s to get the difficulty with. /// An optional which stops computing the star difficulty. /// The . - public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null, + public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null, CancellationToken cancellationToken = default) { if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) @@ -103,7 +103,7 @@ namespace osu.Game.Beatmaps /// The to get the difficulty with. /// The s to get the difficulty with. /// The . - public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IReadOnlyList mods = null) + public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null) { if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; @@ -138,7 +138,7 @@ namespace osu.Game.Beatmaps /// The to update with. /// The s to update with. /// A token that may be used to cancel this update. - private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IReadOnlyList mods, CancellationToken cancellationToken = default) + private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, CancellationToken cancellationToken = default) { GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken).ContinueWith(t => { @@ -159,7 +159,7 @@ namespace osu.Game.Beatmaps /// The initial s to get the difficulty with. /// An optional which stops updating the star difficulty for the given . /// The . - private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IReadOnlyList initialMods, + private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable initialMods, CancellationToken cancellationToken) { var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken); @@ -204,7 +204,7 @@ namespace osu.Game.Beatmaps /// The existing difficulty value, if present. /// The key that was used to perform this lookup. This can be further used to query . /// Whether an existing difficulty was found. - private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IReadOnlyList mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) + private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; From b10b99a6703c9a68d2f1da1b013a46460548a988 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 13:52:43 +0900 Subject: [PATCH 2359/6909] Change method signatures to remove tracked/untracked --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 37 +++++++++---------- .../Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Screens/Select/Details/AdvancedStats.cs | 4 +- 3 files changed, 20 insertions(+), 23 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 914874e210..d86c0dd945 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -47,6 +47,19 @@ namespace osu.Game.Beatmaps currentMods.BindValueChanged(_ => updateTrackedBindables(), true); } + /// + /// Retrieves a bindable containing the star difficulty of a that follows the currently-selected ruleset and mods. + /// + /// The to get the difficulty of. + /// An optional which stops updating the star difficulty for the given . + /// A bindable that is updated to contain the star difficulty when it becomes available. + public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) + { + var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); + trackedBindables.Add(bindable); + return bindable; + } + /// /// Retrieves a bindable containing the star difficulty of a with a given and combination. /// @@ -54,30 +67,14 @@ namespace osu.Game.Beatmaps /// The bindable will not update to follow the currently-selected ruleset and mods. /// /// The to get the difficulty of. - /// The to get the difficulty with. - /// The s to get the difficulty with. + /// The to get the difficulty with. If null, the difficulty will change along with the game-wide ruleset and mods. + /// The s to get the difficulty with. If null, the difficulty will change along with the game-wide mods. /// An optional which stops updating the star difficulty for the given . /// A bindable that is updated to contain the star difficulty when it becomes available. - public IBindable GetUntrackedBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null, - CancellationToken cancellationToken = default) + public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [NotNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, + CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); - /// - /// Retrieves a bindable containing the star difficulty of a that follows the user's currently-selected ruleset and mods. - /// - /// - /// Ensure to hold a local reference of the returned bindable in order to receive value-changed events. - /// - /// The to get the difficulty of. - /// An optional which stops updating the star difficulty for the given . - /// A bindable that is updated to contain the star difficulty when it becomes available, or when the currently-selected ruleset and mods change. - public IBindable GetTrackedBindable([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) - { - var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); - trackedBindables.Add(bindable); - return bindable; - } - /// /// Retrieves the difficulty of a . /// diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 1b5b448e1f..c559b4f8f5 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -194,7 +194,7 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value != CarouselItemState.Collapsed) { // We've potentially cancelled the computation above so a new bindable is required. - starDifficultyBindable = difficultyManager.GetTrackedBindable(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); + starDifficultyBindable = difficultyManager.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true); } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index aefba397b9..1557a025ef 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -161,8 +161,8 @@ namespace osu.Game.Screens.Select.Details var ourSource = starDifficultyCancellationSource = new CancellationTokenSource(); - normalStarDifficulty = difficultyManager.GetUntrackedBindable(Beatmap, ruleset.Value, cancellationToken: ourSource.Token); - moddedStarDifficulty = difficultyManager.GetUntrackedBindable(Beatmap, ruleset.Value, mods.Value, ourSource.Token); + normalStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, null, cancellationToken: ourSource.Token); + moddedStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, ourSource.Token); normalStarDifficulty.BindValueChanged(_ => updateDisplay()); moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true); From 44b0aae20d753f707065d21cf9d25da7b52936c3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 13:54:47 +0900 Subject: [PATCH 2360/6909] Allow nullable ruleset, reword xmldoc --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index d86c0dd945..5e644fbf1c 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -67,11 +67,11 @@ namespace osu.Game.Beatmaps /// The bindable will not update to follow the currently-selected ruleset and mods. /// /// The to get the difficulty of. - /// The to get the difficulty with. If null, the difficulty will change along with the game-wide ruleset and mods. - /// The s to get the difficulty with. If null, the difficulty will change along with the game-wide mods. + /// The to get the difficulty with. If null, the 's ruleset is used. + /// The s to get the difficulty with. If null, no mods will be assumed. /// An optional which stops updating the star difficulty for the given . /// A bindable that is updated to contain the star difficulty when it becomes available. - public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [NotNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, + public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); From d093dc09f94a194eb8e1d936c7ccf36b3a1ff712 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 14:10:05 +0900 Subject: [PATCH 2361/6909] Limit notification text length to avoid large error messages degrading performance --- osu.Game/OsuGame.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index f4bb10340e..d6a07651e2 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 Humanizer; using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -759,7 +760,7 @@ namespace osu.Game Schedule(() => notifications.Post(new SimpleNotification { Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, - Text = entry.Message + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), + Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), })); } else if (recentLogCount == short_term_display_limit) From 4e0f16a45059996dd0ac01ef70cf6bace62b70a7 Mon Sep 17 00:00:00 2001 From: Poliwrath Date: Fri, 24 Jul 2020 02:00:18 -0400 Subject: [PATCH 2362/6909] Add JPEG screenshot quality setting --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Graphics/ScreenshotManager.cs | 5 ++++- .../Overlays/Settings/Sections/Graphics/DetailSettings.cs | 5 +++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 268328272c..a45f5994b7 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -106,6 +106,7 @@ namespace osu.Game.Configuration Set(OsuSetting.Version, string.Empty); Set(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg); + Set(OsuSetting.ScreenshotJpegQuality, 75, 0, 100); Set(OsuSetting.ScreenshotCaptureMenuCursor, false); Set(OsuSetting.SongSelectRightMouseScroll, false); @@ -212,6 +213,7 @@ namespace osu.Game.Configuration ShowConvertedBeatmaps, Skin, ScreenshotFormat, + ScreenshotJpegQuality, ScreenshotCaptureMenuCursor, SongSelectRightMouseScroll, BeatmapSkins, diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 9804aefce8..091e206a80 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -19,6 +19,7 @@ using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Formats.Jpeg; namespace osu.Game.Graphics { @@ -33,6 +34,7 @@ namespace osu.Game.Graphics public IBindable CursorVisibility => cursorVisibility; private Bindable screenshotFormat; + private Bindable screenshotJpegQuality; private Bindable captureMenuCursor; [Resolved] @@ -51,6 +53,7 @@ namespace osu.Game.Graphics this.storage = storage.GetStorageForDirectory(@"screenshots"); screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat); + screenshotJpegQuality = config.GetBindable(OsuSetting.ScreenshotJpegQuality); captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor); shutter = audio.Samples.Get("UI/shutter"); @@ -119,7 +122,7 @@ namespace osu.Game.Graphics break; case ScreenshotFormat.Jpg: - image.SaveAsJpeg(stream); + image.SaveAsJpeg(stream, new JpegEncoder { Quality = screenshotJpegQuality.Value }); break; default: diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index 3089040f96..8b783fb104 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -31,6 +31,11 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = "Screenshot format", Bindable = config.GetBindable(OsuSetting.ScreenshotFormat) }, + new SettingsSlider + { + LabelText = "JPEG Screenshot quality", + Bindable = config.GetBindable(OsuSetting.ScreenshotJpegQuality) + }, new SettingsCheckbox { LabelText = "Show menu cursor in screenshots", From 05235c70c53186c5b3bceca9d8ad3963463ee9ec Mon Sep 17 00:00:00 2001 From: Poliwrath Date: Fri, 24 Jul 2020 02:26:45 -0400 Subject: [PATCH 2363/6909] remove jpeg quality setting, use 92 for quality --- osu.Game/Configuration/OsuConfigManager.cs | 2 -- osu.Game/Graphics/ScreenshotManager.cs | 6 +++--- .../Overlays/Settings/Sections/Graphics/DetailSettings.cs | 5 ----- 3 files changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a45f5994b7..268328272c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -106,7 +106,6 @@ namespace osu.Game.Configuration Set(OsuSetting.Version, string.Empty); Set(OsuSetting.ScreenshotFormat, ScreenshotFormat.Jpg); - Set(OsuSetting.ScreenshotJpegQuality, 75, 0, 100); Set(OsuSetting.ScreenshotCaptureMenuCursor, false); Set(OsuSetting.SongSelectRightMouseScroll, false); @@ -213,7 +212,6 @@ namespace osu.Game.Configuration ShowConvertedBeatmaps, Skin, ScreenshotFormat, - ScreenshotJpegQuality, ScreenshotCaptureMenuCursor, SongSelectRightMouseScroll, BeatmapSkins, diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index 091e206a80..d1f6fd445e 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -34,7 +34,6 @@ namespace osu.Game.Graphics public IBindable CursorVisibility => cursorVisibility; private Bindable screenshotFormat; - private Bindable screenshotJpegQuality; private Bindable captureMenuCursor; [Resolved] @@ -53,7 +52,6 @@ namespace osu.Game.Graphics this.storage = storage.GetStorageForDirectory(@"screenshots"); screenshotFormat = config.GetBindable(OsuSetting.ScreenshotFormat); - screenshotJpegQuality = config.GetBindable(OsuSetting.ScreenshotJpegQuality); captureMenuCursor = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor); shutter = audio.Samples.Get("UI/shutter"); @@ -122,7 +120,9 @@ namespace osu.Game.Graphics break; case ScreenshotFormat.Jpg: - image.SaveAsJpeg(stream, new JpegEncoder { Quality = screenshotJpegQuality.Value }); + const int jpeg_quality = 92; + + image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality }); break; default: diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index 8b783fb104..3089040f96 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -31,11 +31,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = "Screenshot format", Bindable = config.GetBindable(OsuSetting.ScreenshotFormat) }, - new SettingsSlider - { - LabelText = "JPEG Screenshot quality", - Bindable = config.GetBindable(OsuSetting.ScreenshotJpegQuality) - }, new SettingsCheckbox { LabelText = "Show menu cursor in screenshots", From 877b985e900a1c4669e04dcb16f47f760232aafd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 16:11:28 +0900 Subject: [PATCH 2364/6909] Remove local cancellation token --- osu.Game/Screens/Select/Details/AdvancedStats.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 1557a025ef..44c328187f 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -159,10 +159,10 @@ namespace osu.Game.Screens.Select.Details if (Beatmap == null) return; - var ourSource = starDifficultyCancellationSource = new CancellationTokenSource(); + starDifficultyCancellationSource = new CancellationTokenSource(); - normalStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, null, cancellationToken: ourSource.Token); - moddedStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, ourSource.Token); + normalStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); + moddedStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); normalStarDifficulty.BindValueChanged(_ => updateDisplay()); moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true); From dbe9180c55c4e4d6a8991b76aa48a9a4b5f46674 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 16:38:48 +0900 Subject: [PATCH 2365/6909] Rename class and remove screen conditionals --- osu.Desktop/OsuGameDesktop.cs | 2 +- ...KeyHandler.cs => GameplayWinKeyBlocker.cs} | 23 +++++-------------- osu.Desktop/Windows/WindowsKey.cs | 2 +- 3 files changed, 8 insertions(+), 19 deletions(-) rename osu.Desktop/Windows/{GameplayWinKeyHandler.cs => GameplayWinKeyBlocker.cs} (55%) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 6eefee3b50..2079f136d2 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -101,7 +101,7 @@ namespace osu.Desktop LoadComponentAsync(new DiscordRichPresence(), Add); if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) - LoadComponentAsync(new GameplayWinKeyHandler(ScreenStack), Add); + LoadComponentAsync(new GameplayWinKeyBlocker(), Add); } protected override void ScreenChanged(IScreen lastScreen, IScreen newScreen) diff --git a/osu.Desktop/Windows/GameplayWinKeyHandler.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs similarity index 55% rename from osu.Desktop/Windows/GameplayWinKeyHandler.cs rename to osu.Desktop/Windows/GameplayWinKeyBlocker.cs index 96154356d0..86174ceb90 100644 --- a/osu.Desktop/Windows/GameplayWinKeyHandler.cs +++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs @@ -1,49 +1,38 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Game.Configuration; -using osu.Game.Screens; -using osu.Game.Screens.Play; namespace osu.Desktop.Windows { - public class GameplayWinKeyHandler : Component + public class GameplayWinKeyBlocker : Component { private Bindable allowScreenSuspension; private Bindable disableWinKey; - private readonly OsuScreenStack screenStack; private GameHost host; - private Type currentScreenType => screenStack.CurrentScreen?.GetType(); - - public GameplayWinKeyHandler(OsuScreenStack stack) - { - screenStack = stack; - } - [BackgroundDependencyLoader] private void load(GameHost host, OsuConfigManager config) { this.host = host; allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy(); - allowScreenSuspension.ValueChanged += toggleWinKey; + allowScreenSuspension.BindValueChanged(_ => updateBlocking()); disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); - disableWinKey.BindValueChanged(t => allowScreenSuspension.TriggerChange(), true); + disableWinKey.BindValueChanged(_ => updateBlocking(), true); } - private void toggleWinKey(ValueChangedEvent e) + private void updateBlocking() { - var isPlayer = typeof(Player).IsAssignableFrom(currentScreenType) && currentScreenType != typeof(ReplayPlayer); + bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value; - if (!e.NewValue && disableWinKey.Value && isPlayer) + if (shouldDisable) host.InputThread.Scheduler.Add(WindowsKey.Disable); else host.InputThread.Scheduler.Add(WindowsKey.Enable); diff --git a/osu.Desktop/Windows/WindowsKey.cs b/osu.Desktop/Windows/WindowsKey.cs index 4a815b135e..f19d741107 100644 --- a/osu.Desktop/Windows/WindowsKey.cs +++ b/osu.Desktop/Windows/WindowsKey.cs @@ -21,7 +21,7 @@ namespace osu.Desktop.Windows private static IntPtr keyHook; [StructLayout(LayoutKind.Explicit)] - private struct KdDllHookStruct + private readonly struct KdDllHookStruct { [FieldOffset(0)] public readonly int VkCode; From 5f98195144d1068063bfd015d2501255afcd0a34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 18:16:36 +0900 Subject: [PATCH 2366/6909] Load nested hitobjects during map load --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index f275153ce3..581617b567 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -129,9 +129,9 @@ namespace osu.Game.Rulesets.Objects.Drawables LoadSamples(); } - protected override void LoadComplete() + protected override void LoadAsyncComplete() { - base.LoadComplete(); + base.LoadAsyncComplete(); HitObject.DefaultsApplied += onDefaultsApplied; @@ -148,6 +148,11 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.CollectionChanged += (_, __) => LoadSamples(); apply(HitObject); + } + + protected override void LoadComplete() + { + base.LoadComplete(); updateState(ArmedState.Idle, true); } From 2b068298cc339f3134662331037569cf08b18cd3 Mon Sep 17 00:00:00 2001 From: bastoo0 <37190278+bastoo0@users.noreply.github.com> Date: Fri, 24 Jul 2020 12:01:23 +0200 Subject: [PATCH 2367/6909] Fix inconsistency between this and osu-performance The bonus value for HD is given twice here (probably a merge issue). The correct bonus is currently used on stable: https://github.com/ppy/osu-performance/blob/736515a0347ba909d5ac303df7051b600f6655be/src/performance/catch/CatchScore.cpp#L68 --- osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index 2ee7cea645..d700f79e5b 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -78,7 +78,6 @@ namespace osu.Game.Rulesets.Catch.Difficulty if (mods.Any(m => m is ModHidden)) { - 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.0) value *= 1.05 + 0.075 * (10.0 - approachRate); // 7.5% for each AR below 10 From 8f841b47e68ea251a6bd7ec832007d8e50220d11 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 24 Jul 2020 19:24:20 +0900 Subject: [PATCH 2368/6909] Cancel previous initial state computations --- .../Scrolling/ScrollingHitObjectContainer.cs | 43 +++++++++++++------ 1 file changed, 30 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 0dc3324559..bf64175468 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -2,11 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Layout; +using osu.Framework.Threading; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -17,7 +19,7 @@ namespace osu.Game.Rulesets.UI.Scrolling { private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); - private readonly Dictionary hitObjectInitialStateCache = new Dictionary(); + private readonly Dictionary hitObjectInitialStateCache = new Dictionary(); [Resolved] private IScrollingInfo scrollingInfo { get; set; } @@ -175,10 +177,10 @@ namespace osu.Game.Rulesets.UI.Scrolling { // 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)) + if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state)) { combinedObjCache.Invalidate(); - objCache.Invalidate(); + state.Cache.Invalidate(); } } @@ -190,8 +192,8 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!layoutCache.IsValid) { - foreach (var cached in hitObjectInitialStateCache.Values) - cached.Invalidate(); + foreach (var state in hitObjectInitialStateCache.Values) + state.Cache.Invalidate(); combinedObjCache.Invalidate(); scrollingInfo.Algorithm.Reset(); @@ -215,16 +217,18 @@ namespace osu.Game.Rulesets.UI.Scrolling foreach (var obj in Objects) { - if (!hitObjectInitialStateCache.TryGetValue(obj, out var objCache)) - objCache = hitObjectInitialStateCache[obj] = new Cached(); + if (!hitObjectInitialStateCache.TryGetValue(obj, out var state)) + state = hitObjectInitialStateCache[obj] = new InitialState(new Cached()); - if (objCache.IsValid) + if (state.Cache.IsValid) continue; - computeLifetimeStartRecursive(obj); - computeInitialStateRecursive(obj); + state.ScheduledComputation?.Cancel(); + state.ScheduledComputation = computeInitialStateRecursive(obj); - objCache.Validate(); + computeLifetimeStartRecursive(obj); + + state.Cache.Validate(); } combinedObjCache.Validate(); @@ -267,8 +271,7 @@ namespace osu.Game.Rulesets.UI.Scrolling return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); } - // Cant use AddOnce() since the delegate is re-constructed every invocation - private void computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => + private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => { if (hitObject.HitObject is IHasDuration e) { @@ -325,5 +328,19 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } } + + private class InitialState + { + [NotNull] + public readonly Cached Cache; + + [CanBeNull] + public ScheduledDelegate ScheduledComputation; + + public InitialState(Cached cache) + { + Cache = cache; + } + } } } From eb84f2503657fe5c8332e18c20ac9b0281d45d87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 19:34:13 +0900 Subject: [PATCH 2369/6909] Adjust maximum spins to roughly match stable --- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 1c30058d5d..9699ab9502 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -26,7 +26,10 @@ namespace osu.Game.Rulesets.Osu.Objects /// public int SpinsRequired { get; protected set; } = 1; - public int MaximumBonusSpins => SpinsRequired; + /// + /// Number of spins available to give bonus, beyond . + /// + public int MaximumBonusSpins => (int)(SpinsRequired * 1.8f); // roughly matches stable protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { From 82e4050fddb27908c282a5f307f62b22ef62940c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 19:41:34 +0900 Subject: [PATCH 2370/6909] Fix xmldoc --- .../Objects/Drawables/Pieces/SpinnerBonusDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs index 76d7f1843e..a8f5580735 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { /// - /// A component that tracks spinner spins and add bonus score for it. + /// Shows incremental bonus score achieved for a spinner. /// public class SpinnerBonusDisplay : CompositeDrawable { From dd45f0bd40d7aad6def77ce95ed2d2013cd03082 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 21:03:55 +0900 Subject: [PATCH 2371/6909] Adjust max spins to "match" stable --- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 9699ab9502..2c03e6eeac 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -29,16 +29,21 @@ namespace osu.Game.Rulesets.Osu.Objects /// /// Number of spins available to give bonus, beyond . /// - public int MaximumBonusSpins => (int)(SpinsRequired * 1.8f); // roughly matches stable + public int MaximumBonusSpins { get; protected set; } = 1; protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - SpinsRequired = (int)(Duration / 1000 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5)); + double secondsDuration = Duration / 1000; // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. - SpinsRequired = (int)Math.Max(1, SpinsRequired * 0.6); + double minimumRotationsPerSecond = 0.6 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); + + const double maximum_rotations_per_second = 8; // close to 477rpm. + + SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond)); + MaximumBonusSpins = (int)(maximum_rotations_per_second / minimumRotationsPerSecond * secondsDuration); } protected override void CreateNestedHitObjects() From a6a7961af9c2788202055efff6a9e42cfd3a7344 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 24 Jul 2020 22:09:25 +0900 Subject: [PATCH 2372/6909] Change div to subtraction to fix calculation --- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 2c03e6eeac..619b49926e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.cs @@ -35,15 +35,18 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. + const double stable_matching_fudge = 0.6; + + // close to 477rpm + const double maximum_rotations_per_second = 8; + double secondsDuration = Duration / 1000; - // spinning doesn't match 1:1 with stable, so let's fudge them easier for the time being. - double minimumRotationsPerSecond = 0.6 * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); - - const double maximum_rotations_per_second = 8; // close to 477rpm. + double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond)); - MaximumBonusSpins = (int)(maximum_rotations_per_second / minimumRotationsPerSecond * secondsDuration); + MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); } protected override void CreateNestedHitObjects() From 7e5147761fad18ea1bd904c0d718af15d8c2cc42 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 25 Jul 2020 09:26:29 +0300 Subject: [PATCH 2373/6909] Use OverlayView for FrontPageDisplay --- .../News/Displays/FrontPageDisplay.cs | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs index 67b5edfafd..2c228e2f78 100644 --- a/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs +++ b/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs @@ -13,18 +13,18 @@ using osuTK; namespace osu.Game.Overlays.News.Displays { - public class FrontPageDisplay : CompositeDrawable + public class FrontPageDisplay : OverlayView { - [Resolved] - private IAPIProvider api { get; set; } + protected override APIRequest CreateRequest() => new GetNewsRequest(); - private readonly FillFlowContainer content; - private readonly ShowMoreButton showMore; + private FillFlowContainer content; + private ShowMoreButton showMore; - private GetNewsRequest request; + private GetNewsRequest olderPostsRequest; private Cursor lastCursor; - public FrontPageDisplay() + [BackgroundDependencyLoader] + private void load() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -60,32 +60,19 @@ namespace osu.Game.Overlays.News.Displays { Top = 15 }, - Action = fetchPage, + Action = fetchOlderPosts, Alpha = 0 } } }; } - [BackgroundDependencyLoader] - private void load() - { - fetchPage(); - } - - private void fetchPage() - { - request?.Cancel(); - - request = new GetNewsRequest(lastCursor); - request.Success += response => Schedule(() => createContent(response)); - api.PerformAsync(request); - } - private CancellationTokenSource cancellationToken; - private void createContent(GetNewsResponse response) + protected override void OnSuccess(GetNewsResponse response) { + cancellationToken?.Cancel(); + lastCursor = response.Cursor; var flow = new FillFlowContainer @@ -105,9 +92,18 @@ namespace osu.Game.Overlays.News.Displays }, (cancellationToken = new CancellationTokenSource()).Token); } + private void fetchOlderPosts() + { + olderPostsRequest?.Cancel(); + + olderPostsRequest = new GetNewsRequest(lastCursor); + olderPostsRequest.Success += response => Schedule(() => OnSuccess(response)); + API.PerformAsync(olderPostsRequest); + } + protected override void Dispose(bool isDisposing) { - request?.Cancel(); + olderPostsRequest?.Cancel(); cancellationToken?.Cancel(); base.Dispose(isDisposing); } From c6ae2f520e8a7c1ea2446acf401e8a352f57028b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 25 Jul 2020 10:39:10 +0300 Subject: [PATCH 2374/6909] Fix FrontPageDisplay is adding more news posts on api state change --- osu.Game/Overlays/News/Displays/FrontPageDisplay.cs | 2 ++ osu.Game/Overlays/OverlayView.cs | 12 +++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs index 2c228e2f78..fbacf53bf6 100644 --- a/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs +++ b/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs @@ -15,6 +15,8 @@ namespace osu.Game.Overlays.News.Displays { public class FrontPageDisplay : OverlayView { + protected override bool PerformFetchOnApiStateChange => false; + protected override APIRequest CreateRequest() => new GetNewsRequest(); private FillFlowContainer content; diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index 3e2c54c726..f73ca3aa6e 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -18,6 +18,11 @@ namespace osu.Game.Overlays public abstract class OverlayView : CompositeDrawable, IOnlineComponent where T : class { + /// + /// Whether we should perform fetch on api state change to online (true by default). + /// + protected virtual bool PerformFetchOnApiStateChange => true; + [Resolved] protected IAPIProvider API { get; private set; } @@ -33,6 +38,10 @@ namespace osu.Game.Overlays { base.LoadComplete(); API.Register(this); + + // If property is true - fetch will be triggered automatically by APIStateChanged and if not - we need to manually call it. + if (!PerformFetchOnApiStateChange) + PerformFetch(); } /// @@ -64,7 +73,8 @@ namespace osu.Game.Overlays switch (state) { case APIState.Online: - PerformFetch(); + if (PerformFetchOnApiStateChange) + PerformFetch(); break; } } From 897ab4a9bb2de4b5ffd2cbb9510cd74c758cfc20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 25 Jul 2020 11:53:38 +0200 Subject: [PATCH 2375/6909] Add example test resources to demonstrate fail case --- .../Resources/special-skin/pippidonclear.png | Bin 0 -> 8462 bytes .../Resources/special-skin/pippidonfail.png | Bin 0 -> 10643 bytes .../Resources/special-skin/pippidonidle.png | Bin 0 -> 10090 bytes .../Resources/special-skin/pippidonkiai.png | Bin 0 -> 12368 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonidle.png create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonclear.png new file mode 100644 index 0000000000000000000000000000000000000000..c5bcdbd3fc13b3ca847cfba5402c2db38cd962c7 GIT binary patch literal 8462 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonfail.png new file mode 100644 index 0000000000000000000000000000000000000000..39cf7372851ff962b02d9184fd9169faf06c702b GIT binary patch literal 10643 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 b?-&p%=c-}*c9uXxB}hdQfLfd!P#rCUH62?6Pr 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png b/osu.Game.Rulesets.Taiko.Tests/Resources/special-skin/pippidonkiai.png new file mode 100644 index 0000000000000000000000000000000000000000..7de00b5390494b7406e7595de865a8d6a659adcb GIT binary patch literal 12368 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 k73Wz0B#q`)9?)YNC#MoxTnmB!20&4eQ Date: Sat, 25 Jul 2020 12:03:54 +0200 Subject: [PATCH 2376/6909] Add support for indexless mascot texture lookups --- .../Skinning/TaikoLegacySkinTransformer.cs | 5 +---- osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs | 9 ++++++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 23d675cfb0..f032c5f485 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -91,10 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; case TaikoSkinComponents.Mascot: - if (GetTexture("pippidonclear0") != null) - return new DrawableTaikoMascot(); - - return null; + return new DrawableTaikoMascot(); } return Source.GetDrawableComponent(component); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs index 6f25a5f662..9c76aea54c 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoMascotAnimation.cs @@ -128,6 +128,13 @@ namespace osu.Game.Rulesets.Taiko.UI } private static Texture getAnimationFrame(ISkin skin, TaikoMascotAnimationState state, int frameIndex) - => skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); + { + var texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}{frameIndex}"); + + if (frameIndex == 0 && texture == null) + texture = skin.GetTexture($"pippidon{state.ToString().ToLower()}"); + + return texture; + } } } From f7a330becd68780ddbb91543350eee87b15d15a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 25 Jul 2020 12:13:19 +0200 Subject: [PATCH 2377/6909] Fix tests failing due to not checking state early enough --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index cb6a0decde..47d8a5c012 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -36,6 +37,10 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private TaikoScoreProcessor scoreProcessor; private IEnumerable mascots => this.ChildrenOfType(); + + private IEnumerable animatedMascots => + mascots.Where(mascot => mascot.ChildrenOfType().All(animation => animation.FrameCount > 0)); + private IEnumerable playfields => this.ChildrenOfType(); [SetUp] @@ -72,11 +77,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning 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)); + AddAssert("skins with animations remain in clear state", () => animatedMascotsIn(TaikoMascotAnimationState.Clear)); AddUntilStep("state reverts to fail", () => allMascotsIn(TaikoMascotAnimationState.Fail)); AddStep("set clear state again", () => mascots.ForEach(mascot => mascot.State.Value = TaikoMascotAnimationState.Clear)); - AddAssert("skins with animations change to clear", () => someMascotsIn(TaikoMascotAnimationState.Clear)); + AddAssert("skins with animations change to clear", () => animatedMascotsIn(TaikoMascotAnimationState.Clear)); } [Test] @@ -186,10 +191,18 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private void assertStateAfterResult(JudgementResult judgementResult, TaikoMascotAnimationState expectedState) { - AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", - () => applyNewResult(judgementResult)); + TaikoMascotAnimationState[] mascotStates = null; - AddAssert($"state is {expectedState.ToString().ToLower()}", () => allMascotsIn(expectedState)); + AddStep($"{judgementResult.Type.ToString().ToLower()} result for {judgementResult.Judgement.GetType().Name.Humanize(LetterCasing.LowerCase)}", + () => + { + applyNewResult(judgementResult); + // store the states as soon as possible, so that the delay between steps doesn't incorrectly fail the test + // due to not checking if the state changed quickly enough. + Schedule(() => mascotStates = animatedMascots.Select(mascot => mascot.State.Value).ToArray()); + }); + + AddAssert($"state is {expectedState.ToString().ToLower()}", () => mascotStates.All(state => state == expectedState)); } private void applyNewResult(JudgementResult judgementResult) @@ -211,6 +224,6 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning } private bool allMascotsIn(TaikoMascotAnimationState state) => mascots.All(d => d.State.Value == state); - private bool someMascotsIn(TaikoMascotAnimationState state) => mascots.Any(d => d.State.Value == state); + private bool animatedMascotsIn(TaikoMascotAnimationState state) => animatedMascots.Any(d => d.State.Value == state); } } From 788395f8bf24f1702697298c31a7435897c7e7f3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 25 Jul 2020 15:08:06 +0300 Subject: [PATCH 2378/6909] Revert changes regarding FrontPageDisplay and OverlayView --- .../News/Displays/FrontPageDisplay.cs | 35 ++++++++++--------- osu.Game/Overlays/OverlayView.cs | 12 +------ 2 files changed, 19 insertions(+), 28 deletions(-) diff --git a/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs b/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs index fbacf53bf6..0f177f151a 100644 --- a/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs +++ b/osu.Game/Overlays/News/Displays/FrontPageDisplay.cs @@ -13,16 +13,15 @@ using osuTK; namespace osu.Game.Overlays.News.Displays { - public class FrontPageDisplay : OverlayView + public class FrontPageDisplay : CompositeDrawable { - protected override bool PerformFetchOnApiStateChange => false; - - protected override APIRequest CreateRequest() => new GetNewsRequest(); + [Resolved] + private IAPIProvider api { get; set; } private FillFlowContainer content; private ShowMoreButton showMore; - private GetNewsRequest olderPostsRequest; + private GetNewsRequest request; private Cursor lastCursor; [BackgroundDependencyLoader] @@ -62,16 +61,27 @@ namespace osu.Game.Overlays.News.Displays { Top = 15 }, - Action = fetchOlderPosts, + Action = performFetch, Alpha = 0 } } }; + + performFetch(); + } + + private void performFetch() + { + request?.Cancel(); + + request = new GetNewsRequest(lastCursor); + request.Success += response => Schedule(() => onSuccess(response)); + api.PerformAsync(request); } private CancellationTokenSource cancellationToken; - protected override void OnSuccess(GetNewsResponse response) + private void onSuccess(GetNewsResponse response) { cancellationToken?.Cancel(); @@ -94,18 +104,9 @@ namespace osu.Game.Overlays.News.Displays }, (cancellationToken = new CancellationTokenSource()).Token); } - private void fetchOlderPosts() - { - olderPostsRequest?.Cancel(); - - olderPostsRequest = new GetNewsRequest(lastCursor); - olderPostsRequest.Success += response => Schedule(() => OnSuccess(response)); - API.PerformAsync(olderPostsRequest); - } - protected override void Dispose(bool isDisposing) { - olderPostsRequest?.Cancel(); + request?.Cancel(); cancellationToken?.Cancel(); base.Dispose(isDisposing); } diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index f73ca3aa6e..3e2c54c726 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -18,11 +18,6 @@ namespace osu.Game.Overlays public abstract class OverlayView : CompositeDrawable, IOnlineComponent where T : class { - /// - /// Whether we should perform fetch on api state change to online (true by default). - /// - protected virtual bool PerformFetchOnApiStateChange => true; - [Resolved] protected IAPIProvider API { get; private set; } @@ -38,10 +33,6 @@ namespace osu.Game.Overlays { base.LoadComplete(); API.Register(this); - - // If property is true - fetch will be triggered automatically by APIStateChanged and if not - we need to manually call it. - if (!PerformFetchOnApiStateChange) - PerformFetch(); } /// @@ -73,8 +64,7 @@ namespace osu.Game.Overlays switch (state) { case APIState.Online: - if (PerformFetchOnApiStateChange) - PerformFetch(); + PerformFetch(); break; } } From 648f9204f5c59a992080f7bd9d9777a4852ce7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Jul 2020 15:09:12 +0200 Subject: [PATCH 2379/6909] Add sample lifetime constraints for taiko --- .../Audio/DrumSampleContainer.cs | 64 +++++++++++++++++++ .../Audio/DrumSampleMapping.cs | 52 --------------- .../Skinning/LegacyInputDrum.cs | 4 +- osu.Game.Rulesets.Taiko/UI/InputDrum.cs | 10 +-- osu.Game/Skinning/SkinnableSound.cs | 3 + 5 files changed, 74 insertions(+), 59 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs delete mode 100644 osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs new file mode 100644 index 0000000000..7c39c040b1 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.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 osu.Framework.Graphics.Containers; +using osu.Game.Audio; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Taiko.Audio +{ + /// + /// Stores samples for the input drum. + /// The lifetime of the samples is adjusted so that they are only alive during the appropriate sample control point. + /// + public class DrumSampleContainer : LifetimeManagementContainer + { + private readonly ControlPointInfo controlPoints; + private readonly Dictionary mappings = new Dictionary(); + + public DrumSampleContainer(ControlPointInfo controlPoints) + { + this.controlPoints = controlPoints; + + IReadOnlyList samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints; + + for (int i = 0; i < samplePoints.Count; i++) + { + var samplePoint = samplePoints[i]; + + var centre = samplePoint.GetSampleInfo(); + var rim = samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP); + + var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue; + var lifetimeEnd = i + 1 < samplePoints.Count ? samplePoints[i + 1].Time : double.MaxValue; + + mappings[samplePoint.Time] = new DrumSample + { + Centre = addSound(centre, lifetimeStart, lifetimeEnd), + Rim = addSound(rim, lifetimeStart, lifetimeEnd) + }; + } + } + + private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd) + { + var drawable = new SkinnableSound(hitSampleInfo) + { + LifetimeStart = lifetimeStart, + LifetimeEnd = lifetimeEnd + }; + AddInternal(drawable); + return drawable; + } + + public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time]; + + public class DrumSample + { + public SkinnableSound Centre; + public SkinnableSound Rim; + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs deleted file mode 100644 index c31b07344d..0000000000 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleMapping.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Audio; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Skinning; - -namespace osu.Game.Rulesets.Taiko.Audio -{ - public class DrumSampleMapping - { - private readonly ControlPointInfo controlPoints; - private readonly Dictionary mappings = new Dictionary(); - - public readonly List Sounds = new List(); - - public DrumSampleMapping(ControlPointInfo controlPoints) - { - this.controlPoints = controlPoints; - - IEnumerable samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints; - - foreach (var s in samplePoints) - { - var centre = s.GetSampleInfo(); - var rim = s.GetSampleInfo(HitSampleInfo.HIT_CLAP); - - mappings[s.Time] = new DrumSample - { - Centre = addSound(centre), - Rim = addSound(rim) - }; - } - } - - private SkinnableSound addSound(HitSampleInfo hitSampleInfo) - { - var drawable = new SkinnableSound(hitSampleInfo); - Sounds.Add(drawable); - return drawable; - } - - public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time]; - - public class DrumSample - { - public SkinnableSound Centre; - public SkinnableSound Rim; - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs index 81d645e294..b7b55b9ae0 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning public readonly Sprite Centre; [Resolved] - private DrumSampleMapping sampleMappings { get; set; } + private DrumSampleContainer sampleContainer { get; set; } public LegacyHalfDrum(bool flipped) { @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning public bool OnPressed(TaikoAction action) { Drawable target = null; - var drumSample = sampleMappings.SampleAt(Time.Current); + var drumSample = sampleContainer.SampleAt(Time.Current); if (action == CentreAction) { diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 06ccd45cb8..f76f3d851a 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -25,11 +25,11 @@ namespace osu.Game.Rulesets.Taiko.UI private const float middle_split = 0.025f; [Cached] - private DrumSampleMapping sampleMapping; + private DrumSampleContainer sampleContainer; public InputDrum(ControlPointInfo controlPoints) { - sampleMapping = new DrumSampleMapping(controlPoints); + sampleContainer = new DrumSampleContainer(controlPoints); RelativeSizeAxes = Axes.Both; } @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.UI } }); - AddRangeInternal(sampleMapping.Sounds); + AddRangeInternal(sampleContainer.Sounds); } /// @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Taiko.UI private readonly Sprite centreHit; [Resolved] - private DrumSampleMapping sampleMappings { get; set; } + private DrumSampleContainer sampleContainer { get; set; } public TaikoHalfDrum(bool flipped) { @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.UI Drawable target = null; Drawable back = null; - var drumSample = sampleMappings.SampleAt(Time.Current); + var drumSample = sampleContainer.SampleAt(Time.Current); if (action == CentreAction) { diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 49f9f01cff..fb9cab74c8 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -22,6 +22,9 @@ namespace osu.Game.Skinning [Resolved] private ISampleStore samples { get; set; } + public override bool RemoveWhenNotAlive => false; + public override bool RemoveCompletedTransforms => false; + public SkinnableSound(ISampleInfo hitSamples) : this(new[] { hitSamples }) { From 8e6a0493b4f972f2d577c91b0f8cecfd6a74ba8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Jul 2020 15:15:01 +0200 Subject: [PATCH 2380/6909] Adjust InputDrum usage --- osu.Game.Rulesets.Taiko/UI/InputDrum.cs | 60 +++++++++++++------------ 1 file changed, 31 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index f76f3d851a..5966b24b34 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -37,39 +37,41 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - Child = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, - Scale = new Vector2(0.9f), - Children = new Drawable[] + new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.InputDrum), _ => new Container { - new TaikoHalfDrum(false) + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + Scale = new Vector2(0.9f), + 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) - { - 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(sampleContainer.Sounds); + }), + sampleContainer + }; } /// From c78c346b627e7fa89ea99c44a521216812ed5012 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 14:11:01 +0900 Subject: [PATCH 2381/6909] 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 e5b0245dd0..7e6f1469f5 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 5af28ae11a..ab434def38 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 4a94ec33d8..618de5d19f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 5b724d96597a8fabf89b8813fafc1ce0fd0a869e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 15:10:32 +0900 Subject: [PATCH 2382/6909] Adjust damp base component to provide ample tweening --- osu.Game/Screens/Menu/OsuLogo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 34d49685d2..f5e4b078da 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -330,7 +330,7 @@ namespace osu.Game.Screens.Menu if (Beatmap.Value.Track.IsRunning) { var maxAmplitude = lastBeatIndex >= 0 ? Beatmap.Value.Track.CurrentAmplitudes.Maximum : 0; - logoAmplitudeContainer.Scale = new Vector2((float)Interpolation.Damp(logoAmplitudeContainer.Scale.X, 1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 0.5f, Time.Elapsed)); + logoAmplitudeContainer.Scale = new Vector2((float)Interpolation.Damp(logoAmplitudeContainer.Scale.X, 1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 0.9f, Time.Elapsed)); if (maxAmplitude > velocity_adjust_cutoff) triangles.Velocity = 1 + Math.Max(0, maxAmplitude - velocity_adjust_cutoff) * 50; From 3257c1e9f21bed20047f47c98a6aed7dc4c2107c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 16:02:52 +0900 Subject: [PATCH 2383/6909] Move interface exposing into region --- osu.Game/Skinning/SkinnableSound.cs | 76 +++++++++++++++-------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index fb9cab74c8..11d1011ed8 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -40,42 +40,6 @@ namespace osu.Game.Skinning private readonly AudioContainer samplesContainer; - public BindableNumber Volume => samplesContainer.Volume; - - public BindableNumber Balance => samplesContainer.Balance; - - public BindableNumber Frequency => samplesContainer.Frequency; - - 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; @@ -130,5 +94,45 @@ namespace osu.Game.Skinning if (wasPlaying) Play(); } + + #region Re-expose AudioContainer + + public BindableNumber Volume => samplesContainer.Volume; + + public BindableNumber Balance => samplesContainer.Balance; + + public BindableNumber Frequency => samplesContainer.Frequency; + + 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); + + #endregion } } From 9889bfa0f3b8a50765ec611b2a54bae898e54dcb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 16:15:49 +0900 Subject: [PATCH 2384/6909] Stop playing samples on pause, resume looping on unpause --- osu.Game/Skinning/SkinnableSound.cs | 60 ++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 11d1011ed8..f54eff51c2 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Transforms; using osu.Game.Audio; +using osu.Game.Screens.Play; namespace osu.Game.Skinning { @@ -22,9 +23,13 @@ namespace osu.Game.Skinning [Resolved] private ISampleStore samples { get; set; } + private bool requestedPlaying; + public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; + private readonly AudioContainer samplesContainer; + public SkinnableSound(ISampleInfo hitSamples) : this(new[] { hitSamples }) { @@ -36,9 +41,28 @@ namespace osu.Game.Skinning InternalChild = samplesContainer = new AudioContainer(); } - private bool looping; + private Bindable gameplayClockPaused; - private readonly AudioContainer samplesContainer; + [BackgroundDependencyLoader(true)] + private void load(GameplayClock gameplayClock) + { + // if in a gameplay context, pause sample playback when gameplay is paused. + gameplayClockPaused = gameplayClock?.IsPaused.GetBoundCopy(); + gameplayClockPaused?.BindValueChanged(paused => + { + if (requestedPlaying) + { + if (paused.NewValue) + stop(); + // it's not easy to know if a sample has finished playing (to end). + // to keep things simple only resume playing looping samples. + else if (Looping) + play(); + } + }); + } + + private bool looping; public bool Looping { @@ -53,20 +77,36 @@ namespace osu.Game.Skinning } } - public void Play() => samplesContainer.ForEach(c => + public void Play() { - if (c.AggregateVolume.Value > 0) - c.Play(); - }); + requestedPlaying = true; + play(); + } - public void Stop() => samplesContainer.ForEach(c => c.Stop()); + private void play() + { + samplesContainer.ForEach(c => + { + if (c.AggregateVolume.Value > 0) + c.Play(); + }); + } + + public void Stop() + { + requestedPlaying = false; + stop(); + } + + private void stop() + { + samplesContainer.ForEach(c => c.Stop()); + } public override bool IsPresent => Scheduler.HasPendingTasks; protected override void SkinChanged(ISkinSource skin, bool allowFallback) { - bool wasPlaying = samplesContainer.Any(s => s.Playing); - var channels = hitSamples.Select(s => { var ch = skin.GetSample(s); @@ -91,7 +131,7 @@ namespace osu.Game.Skinning samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); - if (wasPlaying) + if (requestedPlaying) Play(); } From 5e7237bf567cb5cf1539acb1d6ef05427a490b5a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 27 Jul 2020 10:29:16 +0300 Subject: [PATCH 2385/6909] Fix incorrect default hitcircle font overlapping applied to legacy skins --- 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 3e5758ca01..95ef2d58b1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Skinning case OsuSkinComponents.HitCircleText: var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default"; - var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? 0; + var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2; return !hasFont(font) ? null From d8f4e044de943c0e26603ee1536f631b7db95036 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 17:46:10 +0900 Subject: [PATCH 2386/6909] Add test coverage --- .../Gameplay/TestSceneSkinnableSound.cs | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs new file mode 100644 index 0000000000..1128b17303 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.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 System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Audio; +using osu.Game.Screens.Play; +using osu.Game.Skinning; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSkinnableSound : OsuTestScene + { + [Cached] + private GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + + private SkinnableSound skinnableSounds; + + [SetUp] + public void SetUp() + { + Children = new Drawable[] + { + new Container + { + Clock = gameplayClock, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + skinnableSounds = new SkinnableSound(new SampleInfo("normal-sliderslide")) + { + Looping = true + } + } + }, + }; + } + + [Test] + public void TestStoppedSoundDoesntResumeAfterPause() + { + DrawableSample sample = null; + AddStep("start sample", () => + { + skinnableSounds.Play(); + sample = skinnableSounds.ChildrenOfType().First(); + }); + + AddUntilStep("wait for sample to start playing", () => sample.Playing); + + AddStep("stop sample", () => skinnableSounds.Stop()); + + AddUntilStep("wait for sample to stop playing", () => !sample.Playing); + + AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); + AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); + + AddWaitStep("wait a bit", 5); + AddAssert("sample not playing", () => !sample.Playing); + } + + [Test] + public void TestLoopingSoundResumesAfterPause() + { + DrawableSample sample = null; + AddStep("start sample", () => + { + skinnableSounds.Play(); + sample = skinnableSounds.ChildrenOfType().First(); + }); + + AddUntilStep("wait for sample to start playing", () => sample.Playing); + + AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); + AddUntilStep("wait for sample to stop playing", () => !sample.Playing); + + AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); + AddUntilStep("wait for sample to start playing", () => sample.Playing); + } + } +} From 12368ace3b421382420ed1ef41684f6f02a4dfcd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 17:46:44 +0900 Subject: [PATCH 2387/6909] Rename variable --- .../Visual/Gameplay/TestSceneSkinnableSound.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 1128b17303..73704faa1f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayClock gameplayClock = new GameplayClock(new FramedClock()); - private SkinnableSound skinnableSounds; + private SkinnableSound skinnableSound; [SetUp] public void SetUp() @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - skinnableSounds = new SkinnableSound(new SampleInfo("normal-sliderslide")) + skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide")) { Looping = true } @@ -48,13 +48,13 @@ namespace osu.Game.Tests.Visual.Gameplay DrawableSample sample = null; AddStep("start sample", () => { - skinnableSounds.Play(); - sample = skinnableSounds.ChildrenOfType().First(); + skinnableSound.Play(); + sample = skinnableSound.ChildrenOfType().First(); }); AddUntilStep("wait for sample to start playing", () => sample.Playing); - AddStep("stop sample", () => skinnableSounds.Stop()); + AddStep("stop sample", () => skinnableSound.Stop()); AddUntilStep("wait for sample to stop playing", () => !sample.Playing); @@ -71,8 +71,8 @@ namespace osu.Game.Tests.Visual.Gameplay DrawableSample sample = null; AddStep("start sample", () => { - skinnableSounds.Play(); - sample = skinnableSounds.ChildrenOfType().First(); + skinnableSound.Play(); + sample = skinnableSound.ChildrenOfType().First(); }); AddUntilStep("wait for sample to start playing", () => sample.Playing); From 5fd73795f629bcf4c48379410f8c7d538c4494ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 18:02:14 +0900 Subject: [PATCH 2388/6909] Remove wait steps and add coverage of non-looping sounds --- .../Gameplay/TestSceneSkinnableSound.cs | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 73704faa1f..5b7704122b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -31,13 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay { Clock = gameplayClock, RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide")) - { - Looping = true - } - } + Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide")) }, }; } @@ -46,27 +40,47 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestStoppedSoundDoesntResumeAfterPause() { DrawableSample sample = null; - AddStep("start sample", () => + AddStep("start sample with looping", () => { + skinnableSound.Looping = true; skinnableSound.Play(); sample = skinnableSound.ChildrenOfType().First(); }); - AddUntilStep("wait for sample to start playing", () => sample.Playing); + AddAssert("sample playing", () => sample.Playing); AddStep("stop sample", () => skinnableSound.Stop()); - AddUntilStep("wait for sample to stop playing", () => !sample.Playing); + AddAssert("sample not playing", () => !sample.Playing); AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); - AddWaitStep("wait a bit", 5); AddAssert("sample not playing", () => !sample.Playing); } [Test] public void TestLoopingSoundResumesAfterPause() + { + DrawableSample sample = null; + AddStep("start sample with looping", () => + { + skinnableSound.Looping = true; + skinnableSound.Play(); + sample = skinnableSound.ChildrenOfType().First(); + }); + + AddAssert("sample playing", () => sample.Playing); + + AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); + AddAssert("sample not playing", () => !sample.Playing); + + AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); + AddUntilStep("wait for sample to start playing", () => sample.Playing); + } + + [Test] + public void TestNonLoopingStopsWithPause() { DrawableSample sample = null; AddStep("start sample", () => @@ -75,13 +89,13 @@ namespace osu.Game.Tests.Visual.Gameplay sample = skinnableSound.ChildrenOfType().First(); }); - AddUntilStep("wait for sample to start playing", () => sample.Playing); + AddAssert("sample playing", () => sample.Playing); AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); - AddUntilStep("wait for sample to stop playing", () => !sample.Playing); + AddAssert("sample not playing", () => !sample.Playing); AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); - AddUntilStep("wait for sample to start playing", () => sample.Playing); + AddAssert("sample not playing", () => !sample.Playing); } } } From 10101d5b31da4499d99f4564fc83dbc709ec7f4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 18:06:14 +0900 Subject: [PATCH 2389/6909] Reduce spinner tick and bonus score --- osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs | 2 +- osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 6ca2d4d72d..b59428e701 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement { - protected override int NumericResultFor(HitResult result) => 1100; + protected override int NumericResultFor(HitResult result) => 50; protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2; } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index c81348fbbf..346f949a4f 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Objects { public override bool AffectsCombo => false; - protected override int NumericResultFor(HitResult result) => 100; + protected override int NumericResultFor(HitResult result) => 10; protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0; } From 06c4fb717146b999ed2496c2dadfb48fe6e0b404 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 18:40:53 +0900 Subject: [PATCH 2390/6909] Update bonus score spec in test --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 6e277ff37e..23b440ced2 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.Disc.CumulativeRotation / 360) * 100; + return totalScore == (int)(drawableSpinner.Disc.CumulativeRotation / 360) * 10; }); addSeekStep(0); From 1f8abf2cf6a085fe99838dda46835e2c1eaaf107 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 19:03:21 +0900 Subject: [PATCH 2391/6909] Fix headless tests --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 5b7704122b..9cfea8ec85 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Gameplay private SkinnableSound skinnableSound; [SetUp] - public void SetUp() + public void SetUp() => Schedule(() => { Children = new Drawable[] { @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide")) }, }; - } + }); [Test] public void TestStoppedSoundDoesntResumeAfterPause() From 33e8e0aa187ccbc8cdbdf1f5da8e6da5e4271c13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 27 Jul 2020 19:05:31 +0900 Subject: [PATCH 2392/6909] Add back until steps so headless tests can better handle thread delays --- .../Visual/Gameplay/TestSceneSkinnableSound.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 9cfea8ec85..81e5f32ee8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -47,15 +47,16 @@ namespace osu.Game.Tests.Visual.Gameplay sample = skinnableSound.ChildrenOfType().First(); }); - AddAssert("sample playing", () => sample.Playing); + AddUntilStep("wait for sample to start playing", () => sample.Playing); AddStep("stop sample", () => skinnableSound.Stop()); - AddAssert("sample not playing", () => !sample.Playing); + AddUntilStep("wait for sample to stop playing", () => !sample.Playing); AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); + AddWaitStep("wait a bit", 5); AddAssert("sample not playing", () => !sample.Playing); } @@ -70,13 +71,10 @@ namespace osu.Game.Tests.Visual.Gameplay sample = skinnableSound.ChildrenOfType().First(); }); - AddAssert("sample playing", () => sample.Playing); + AddUntilStep("wait for sample to start playing", () => sample.Playing); AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); - AddAssert("sample not playing", () => !sample.Playing); - - AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); - AddUntilStep("wait for sample to start playing", () => sample.Playing); + AddUntilStep("wait for sample to stop playing", () => !sample.Playing); } [Test] @@ -92,9 +90,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sample playing", () => sample.Playing); AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); - AddAssert("sample not playing", () => !sample.Playing); + AddUntilStep("wait for sample to stop playing", () => !sample.Playing); AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); + + AddAssert("sample not playing", () => !sample.Playing); + AddAssert("sample not playing", () => !sample.Playing); AddAssert("sample not playing", () => !sample.Playing); } } From bbc7d69524931143073058d6a20bcdb799482435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 14 Jul 2020 22:51:30 +0200 Subject: [PATCH 2393/6909] Add failing test cases --- .../TestSceneDrawableJudgement.cs | 102 ++++++++++++------ .../Objects/Drawables/DrawableOsuJudgement.cs | 21 ++-- 2 files changed, 83 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index f08f994b07..4bb4619a1b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -4,62 +4,104 @@ using System; using System.Collections.Generic; using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; +using osu.Framework.Testing; +using osu.Game.Configuration; 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.Skinning; namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneDrawableJudgement : OsuSkinnableTestScene { + [Resolved] + private OsuConfigManager config { get; set; } + + private readonly List> pools; + public TestSceneDrawableJudgement() { - var pools = new List>(); + pools = new List>(); foreach (HitResult result in Enum.GetValues(typeof(HitResult)).OfType().Skip(1)) + showResult(result); + } + + [Test] + public void TestHitLightingDisabled() + { + AddStep("hit lighting disabled", () => config.Set(OsuSetting.HitLighting, false)); + + showResult(HitResult.Great); + + AddUntilStep("judgements shown", () => this.ChildrenOfType().Any()); + AddAssert("hit lighting hidden", + () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0)); + } + + [Test] + public void TestHitLightingEnabled() + { + AddStep("hit lighting enabled", () => config.Set(OsuSetting.HitLighting, true)); + + showResult(HitResult.Great); + + AddUntilStep("judgements shown", () => this.ChildrenOfType().Any()); + AddAssert("hit lighting shown", + () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha > 0)); + } + + private void showResult(HitResult result) + { + AddStep("Show " + result.GetDescription(), () => { - AddStep("Show " + result.GetDescription(), () => + int poolIndex = 0; + + SetContents(() => { - int poolIndex = 0; + DrawablePool pool; - SetContents(() => + if (poolIndex >= pools.Count) + pools.Add(pool = new DrawablePool(1)); + else { - DrawablePool pool; + pool = pools[poolIndex]; - if (poolIndex >= pools.Count) - pools.Add(pool = new DrawablePool(1)); - else + // We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent. + ((Container)pool.Parent).Clear(false); + } + + var container = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - pool = pools[poolIndex]; - - // We need to make sure neither the pool nor the judgement get disposed when new content is set, and they both share the same parent. - ((Container)pool.Parent).Clear(false); - } - - var container = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + pool, + pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j => { - pool, - pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j => - { - j.Anchor = Anchor.Centre; - j.Origin = Anchor.Centre; - }) - } - }; + j.Anchor = Anchor.Centre; + j.Origin = Anchor.Centre; + }) + } + }; - poolIndex++; - return container; - }); + poolIndex++; + return container; }); - } + }); + } + + private class TestDrawableOsuJudgement : DrawableOsuJudgement + { + public new SkinnableSprite Lighting => base.Lighting; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 1493ddfcf3..3e45d3509d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -16,7 +16,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableOsuJudgement : DrawableJudgement { - private SkinnableSprite lighting; + protected SkinnableSprite Lighting; + private Bindable lightingColour; public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject) @@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { if (config.Get(OsuSetting.HitLighting)) { - AddInternal(lighting = new SkinnableSprite("lighting") + AddInternal(Lighting = new SkinnableSprite("lighting") { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -60,32 +61,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables lightingColour?.UnbindAll(); - if (lighting != null) + if (Lighting != null) { - lighting.ResetAnimation(); + Lighting.ResetAnimation(); if (JudgedObject != 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 { - lighting.Colour = Color4.White; + Lighting.Colour = Color4.White; } } } - protected override double FadeOutDelay => lighting == null ? base.FadeOutDelay : 1400; + protected override double FadeOutDelay => Lighting == null ? base.FadeOutDelay : 1400; protected override void ApplyHitAnimations() { - if (lighting != null) + if (Lighting != null) { JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400); - lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); - lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); + Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); + Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); } JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); From 21ae33e28487a4939ed01ac03159c9e321a8f6b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Jul 2020 23:20:38 +0200 Subject: [PATCH 2394/6909] Determine whether to show lighting at prepare time --- .../Objects/Drawables/DrawableOsuJudgement.cs | 44 +++++++++---------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 3e45d3509d..8079ce4a3f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -20,6 +20,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Bindable lightingColour; + [Resolved] + private OsuConfigManager config { get; set; } + public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject) : base(result, judgedObject) { @@ -30,18 +33,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load() { - if (config.Get(OsuSetting.HitLighting)) + AddInternal(Lighting = new SkinnableSprite("lighting") { - AddInternal(Lighting = new SkinnableSprite("lighting") - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Blending = BlendingParameters.Additive, - Depth = float.MaxValue - }); - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, + Depth = float.MaxValue, + Alpha = 0 + }); } public override void Apply(JudgementResult result, DrawableHitObject judgedObject) @@ -61,19 +62,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables lightingColour?.UnbindAll(); - if (Lighting != null) - { - Lighting.ResetAnimation(); + Lighting.ResetAnimation(); - 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; - } + 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; } } @@ -81,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void ApplyHitAnimations() { - if (Lighting != null) + if (config.Get(OsuSetting.HitLighting)) { JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400); From 5fc7039bf2c6b4a9878611beb72427aa18c40b10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Jul 2020 23:22:31 +0200 Subject: [PATCH 2395/6909] Prevent DrawableJudgement from removing other children --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 052aaa3c65..e085334649 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -130,11 +130,16 @@ namespace osu.Game.Rulesets.Judgements if (type == currentDrawableType) return; - InternalChild = JudgementBody = new Container + // sub-classes might have added their own children that would be removed here if .InternalChild was used. + if (JudgementBody != null) + RemoveInternal(JudgementBody); + + AddInternal(JudgementBody = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, + Depth = -float.MaxValue, Child = bodyDrawable = new SkinnableDrawable(new GameplaySkinComponent(type), _ => JudgementText = new OsuSpriteText { Text = type.GetDescription().ToUpperInvariant(), @@ -142,7 +147,7 @@ namespace osu.Game.Rulesets.Judgements Colour = colours.ForHitResult(type), Scale = new Vector2(0.85f, 1), }, confineMode: ConfineMode.NoScaling) - }; + }); currentDrawableType = type; } From 7ad3101d082975980eb3990c9db1a2981211bc0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 26 Jul 2020 23:26:21 +0200 Subject: [PATCH 2396/6909] Bring back custom fade-out delay if hit lighting is on --- .../TestSceneDrawableJudgement.cs | 5 +++++ .../Objects/Drawables/DrawableOsuJudgement.cs | 13 +++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index 4bb4619a1b..646f12f710 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests showResult(HitResult.Great); AddUntilStep("judgements shown", () => this.ChildrenOfType().Any()); + AddAssert("judgement body immediately visible", + () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha == 1)); AddAssert("hit lighting hidden", () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0)); } @@ -55,6 +57,8 @@ namespace osu.Game.Rulesets.Osu.Tests showResult(HitResult.Great); AddUntilStep("judgements shown", () => this.ChildrenOfType().Any()); + AddAssert("judgement body not immediately visible", + () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1)); AddAssert("hit lighting shown", () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha > 0)); } @@ -102,6 +106,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestDrawableOsuJudgement : DrawableOsuJudgement { public new SkinnableSprite Lighting => base.Lighting; + public new Container JudgementBody => base.JudgementBody; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 8079ce4a3f..012d9f8878 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -75,17 +75,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected override double FadeOutDelay => Lighting == null ? base.FadeOutDelay : 1400; + private double fadeOutDelay; + protected override double FadeOutDelay => fadeOutDelay; protected override void ApplyHitAnimations() { - if (config.Get(OsuSetting.HitLighting)) + bool hitLightingEnabled = config.Get(OsuSetting.HitLighting); + + if (hitLightingEnabled) { JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400); Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); } + else + { + JudgementBody.Alpha = 1; + } + + fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay; JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); base.ApplyHitAnimations(); From abdbeafc8ad4f52274ae1b65f29034f2fe676f2d Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 27 Jul 2020 17:21:15 +0000 Subject: [PATCH 2397/6909] Bump Sentry from 2.1.4 to 2.1.5 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 2.1.4 to 2.1.5. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/2.1.4...2.1.5) 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 ab434def38..7ebffc6d10 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + From 1b5a23311ef36d80566fb8e555d250f9eec072d4 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 28 Jul 2020 00:29:17 +0300 Subject: [PATCH 2398/6909] Update ChevronButton position/colour --- .../Comments/Buttons/ChevronButton.cs | 48 +++++++++++++++++++ osu.Game/Overlays/Comments/DrawableComment.cs | 32 ++++--------- .../Overlays/Comments/ShowChildrenButton.cs | 33 ------------- 3 files changed, 58 insertions(+), 55 deletions(-) create mode 100644 osu.Game/Overlays/Comments/Buttons/ChevronButton.cs delete mode 100644 osu.Game/Overlays/Comments/ShowChildrenButton.cs diff --git a/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs b/osu.Game/Overlays/Comments/Buttons/ChevronButton.cs new file mode 100644 index 0000000000..48f34e8f59 --- /dev/null +++ b/osu.Game/Overlays/Comments/Buttons/ChevronButton.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.Containers; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; +using osuTK; +using osu.Framework.Allocation; + +namespace osu.Game.Overlays.Comments.Buttons +{ + public class ChevronButton : OsuHoverContainer + { + public readonly BindableBool Expanded = new BindableBool(true); + + private readonly SpriteIcon icon; + + public ChevronButton() + { + Size = new Vector2(40, 22); + Child = icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(12), + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IdleColour = HoverColour = colourProvider.Foreground1; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Action = Expanded.Toggle; + Expanded.BindValueChanged(onExpandedChanged, true); + } + + private void onExpandedChanged(ValueChangedEvent expanded) + { + icon.Icon = expanded.NewValue ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown; + } + } +} diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 3cdc0a0cbd..39ad60b61c 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -228,13 +228,19 @@ namespace osu.Game.Overlays.Comments }, } }, - chevronButton = new ChevronButton + new Container { + Size = new Vector2(70, 40), Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Margin = new MarginPadding { Right = 30, Top = margin }, - Expanded = { BindTarget = childrenExpanded }, - Alpha = 0 + Margin = new MarginPadding { Horizontal = 5 }, + Child = chevronButton = new ChevronButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Expanded = { BindTarget = childrenExpanded }, + Alpha = 0 + } } }; @@ -357,24 +363,6 @@ namespace osu.Game.Overlays.Comments showMoreButton.IsLoading = loadRepliesButton.IsLoading = false; } - private class ChevronButton : ShowChildrenButton - { - private readonly SpriteIcon icon; - - public ChevronButton() - { - Child = icon = new SpriteIcon - { - Size = new Vector2(12), - }; - } - - protected override void OnExpandedChanged(ValueChangedEvent expanded) - { - icon.Icon = expanded.NewValue ? FontAwesome.Solid.ChevronUp : FontAwesome.Solid.ChevronDown; - } - } - private class ShowMoreButton : GetCommentRepliesButton { [BackgroundDependencyLoader] diff --git a/osu.Game/Overlays/Comments/ShowChildrenButton.cs b/osu.Game/Overlays/Comments/ShowChildrenButton.cs deleted file mode 100644 index 5ec7c1d471..0000000000 --- a/osu.Game/Overlays/Comments/ShowChildrenButton.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.Graphics; -using osu.Game.Graphics.Containers; -using osu.Framework.Bindables; -using osuTK.Graphics; -using osu.Game.Graphics; - -namespace osu.Game.Overlays.Comments -{ - public abstract class ShowChildrenButton : OsuHoverContainer - { - public readonly BindableBool Expanded = new BindableBool(true); - - protected ShowChildrenButton() - { - AutoSizeAxes = Axes.Both; - IdleColour = OsuColour.Gray(0.7f); - HoverColour = Color4.White; - } - - protected override void LoadComplete() - { - Action = Expanded.Toggle; - - Expanded.BindValueChanged(OnExpandedChanged, true); - base.LoadComplete(); - } - - protected abstract void OnExpandedChanged(ValueChangedEvent expanded); - } -} From 46d1de7fa7efdd2597c02b5431313c172d4d6169 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 28 Jul 2020 00:43:06 +0300 Subject: [PATCH 2399/6909] ShowMoreButton rework --- .../ShowMoreButton.cs} | 27 +++++++++++-------- osu.Game/Overlays/Comments/DrawableComment.cs | 13 --------- 2 files changed, 16 insertions(+), 24 deletions(-) rename osu.Game/Overlays/Comments/{GetCommentRepliesButton.cs => Buttons/ShowMoreButton.cs} (66%) diff --git a/osu.Game/Overlays/Comments/GetCommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs similarity index 66% rename from osu.Game/Overlays/Comments/GetCommentRepliesButton.cs rename to osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs index a3817ba416..0f07a7141c 100644 --- a/osu.Game/Overlays/Comments/GetCommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs @@ -8,38 +8,43 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using System.Collections.Generic; using osuTK; +using osu.Framework.Allocation; -namespace osu.Game.Overlays.Comments +namespace osu.Game.Overlays.Comments.Buttons { - public abstract class GetCommentRepliesButton : LoadingButton + public class ShowMoreButton : LoadingButton { - private const int duration = 200; - protected override IEnumerable EffectTargets => new[] { text }; private OsuSpriteText text; - protected GetCommentRepliesButton() + public ShowMoreButton() { AutoSizeAxes = Axes.Both; + Margin = new MarginPadding { Vertical = 10, Left = 80 }; LoadingAnimationSize = new Vector2(8); } + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IdleColour = colourProvider.Light2; + HoverColour = colourProvider.Light1; + } + protected override Drawable CreateContent() => new Container { AutoSizeAxes = Axes.Both, Child = text = new OsuSpriteText { AlwaysPresent = true, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = GetText() + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Text = "show more" } }; - protected abstract string GetText(); + protected override void OnLoadStarted() => text.FadeOut(200, Easing.OutQuint); - protected override void OnLoadStarted() => text.FadeOut(duration, Easing.OutQuint); - - protected override void OnLoadFinished() => text.FadeIn(duration, Easing.OutQuint); + protected override void OnLoadFinished() => text.FadeIn(200, Easing.OutQuint); } } diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 39ad60b61c..05959dbfd9 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -363,19 +363,6 @@ namespace osu.Game.Overlays.Comments showMoreButton.IsLoading = loadRepliesButton.IsLoading = false; } - private class ShowMoreButton : GetCommentRepliesButton - { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Margin = new MarginPadding { Vertical = 10, Left = 80 }; - IdleColour = colourProvider.Light2; - HoverColour = colourProvider.Light1; - } - - protected override string GetText() => @"Show More"; - } - private class ParentUsername : FillFlowContainer, IHasTooltip { public string TooltipText => getParentMessage(); From 69691b373971f03eb00a589768cc1c67b39fcb7f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 28 Jul 2020 00:53:51 +0300 Subject: [PATCH 2400/6909] Use DrawableDate to represent creation date --- osu.Game/Overlays/Comments/DrawableComment.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 05959dbfd9..07c3ba970f 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -58,7 +58,7 @@ namespace osu.Game.Overlays.Comments } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { LinkFlowContainer username; FillFlowContainer info; @@ -176,14 +176,12 @@ namespace osu.Game.Overlays.Comments Spacing = new Vector2(10, 0), Children = new Drawable[] { - new OsuSpriteText + new DrawableDate(Comment.CreatedAt, 12, false) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 12), - Colour = OsuColour.Gray(0.7f), - Text = HumanizerUtils.Humanize(Comment.CreatedAt) - }, + Colour = colourProvider.Foreground1 + } } }, showRepliesButton = new ShowRepliesButton(Comment.RepliesCount) From 6737c57e334377dbbc170f1a8ae3748118503dbd Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 28 Jul 2020 01:10:13 +0300 Subject: [PATCH 2401/6909] Adjust colour of edit info --- osu.Game/Overlays/Comments/DrawableComment.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 07c3ba970f..e2ab72d62f 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -253,8 +253,9 @@ namespace osu.Game.Overlays.Comments { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(size: 12), - Text = $@"edited {HumanizerUtils.Humanize(Comment.EditedAt.Value)} by {Comment.EditedUser.Username}" + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), + Text = $@"edited {HumanizerUtils.Humanize(Comment.EditedAt.Value)} by {Comment.EditedUser.Username}", + Colour = colourProvider.Foreground1 }); } From 2d502cebdab2fb489568ad3296565e8204a21c45 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 28 Jul 2020 02:36:25 +0300 Subject: [PATCH 2402/6909] Update DrawableComment layout --- .../Comments/Buttons/CommentRepliesButton.cs | 4 - .../Comments/Buttons/ShowMoreButton.cs | 1 - .../Comments/DeletedCommentsCounter.cs | 2 - osu.Game/Overlays/Comments/DrawableComment.cs | 167 ++++++++++-------- 4 files changed, 89 insertions(+), 85 deletions(-) diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index f7e0cb0a6c..53438ca421 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -32,10 +32,6 @@ namespace osu.Game.Overlays.Comments.Buttons protected CommentRepliesButton() { AutoSizeAxes = Axes.Both; - Margin = new MarginPadding - { - Vertical = 2 - }; InternalChildren = new Drawable[] { new CircularContainer diff --git a/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs index 0f07a7141c..2c363564d2 100644 --- a/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs @@ -21,7 +21,6 @@ namespace osu.Game.Overlays.Comments.Buttons public ShowMoreButton() { AutoSizeAxes = Axes.Both; - Margin = new MarginPadding { Vertical = 10, Left = 80 }; LoadingAnimationSize = new Vector2(8); } diff --git a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs index f22086bf23..56588ef0a8 100644 --- a/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs +++ b/osu.Game/Overlays/Comments/DeletedCommentsCounter.cs @@ -23,8 +23,6 @@ namespace osu.Game.Overlays.Comments public DeletedCommentsCounter() { AutoSizeAxes = Axes.Both; - Margin = new MarginPadding { Vertical = 10, Left = 80 }; - InternalChild = new FillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index e2ab72d62f..9c0a48ec29 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -28,7 +28,6 @@ namespace osu.Game.Overlays.Comments public class DrawableComment : CompositeDrawable { private const int avatar_size = 40; - private const int margin = 10; public Action RepliesRequested; @@ -70,25 +69,25 @@ namespace osu.Game.Overlays.Comments AutoSizeAxes = Axes.Y; InternalChildren = new Drawable[] { - new FillFlowContainer + new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] + Padding = getPadding(Comment.IsTopLevel), + Child = new FillFlowContainer { - new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(margin) { Left = margin + 5, Top = Comment.IsTopLevel ? 10 : 0 }, - Child = content = new GridContainer + content = new GridContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, ColumnDimensions = new[] { - new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.Absolute, size: avatar_size + 10), new Dimension(), }, RowDimensions = new[] @@ -99,91 +98,84 @@ namespace osu.Game.Overlays.Comments { new Drawable[] { - new FillFlowContainer + new Container { - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Horizontal = margin }, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), + Size = new Vector2(avatar_size), Children = new Drawable[] { - new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 40, - AutoSizeAxes = Axes.Y, - Child = votePill = new VotePill(Comment) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - } - }, new UpdateableAvatar(Comment.User) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, Size = new Vector2(avatar_size), Masking = true, CornerRadius = avatar_size / 2f, CornerExponent = 2, }, + votePill = new VotePill(Comment) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Margin = new MarginPadding + { + Right = 5 + } + } } }, new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0, 3), + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 4), + Margin = new MarginPadding + { + Vertical = 2 + }, Children = new Drawable[] { new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(7, 0), + Spacing = new Vector2(10, 0), Children = new Drawable[] { - username = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true)) + username = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold)) { - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both }, new ParentUsername(Comment), new OsuSpriteText { Alpha = Comment.IsDeleted ? 1 : 0, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true), - Text = @"deleted", + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), + Text = "deleted" } } }, message = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 14)) { RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 40 } + AutoSizeAxes = Axes.Y }, - new FillFlowContainer + info = new FillFlowContainer { AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), Children = new Drawable[] { - info = new FillFlowContainer + new DrawableDate(Comment.CreatedAt, 12, false) { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - new DrawableDate(Comment.CreatedAt, 12, false) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = colourProvider.Foreground1 - } - } - }, + Colour = colourProvider.Foreground1 + } + } + }, + new Container + { + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { showRepliesButton = new ShowRepliesButton(Comment.RepliesCount) { Expanded = { BindTarget = childrenExpanded } @@ -198,32 +190,36 @@ namespace osu.Game.Overlays.Comments } } } - } - }, - childCommentsVisibilityContainer = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] + }, + childCommentsVisibilityContainer = new FillFlowContainer { - childCommentsContainer = new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Left = 20 }, + Children = new Drawable[] { - Padding = new MarginPadding { Left = 20 }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical - }, - deletedCommentsCounter = new DeletedCommentsCounter - { - ShowDeleted = { BindTarget = ShowDeleted } - }, - showMoreButton = new ShowMoreButton - { - Action = () => RepliesRequested(this, ++currentPage) + childCommentsContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical + }, + deletedCommentsCounter = new DeletedCommentsCounter + { + ShowDeleted = { BindTarget = ShowDeleted }, + Margin = new MarginPadding + { + Top = 10 + } + }, + showMoreButton = new ShowMoreButton + { + Action = () => RepliesRequested(this, ++currentPage) + } } - } - }, + }, + } } }, new Container @@ -251,8 +247,6 @@ namespace osu.Game.Overlays.Comments { info.Add(new OsuSpriteText { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), Text = $@"edited {HumanizerUtils.Humanize(Comment.EditedAt.Value)} by {Comment.EditedUser.Username}", Colour = colourProvider.Foreground1 @@ -362,6 +356,23 @@ namespace osu.Game.Overlays.Comments showMoreButton.IsLoading = loadRepliesButton.IsLoading = false; } + private MarginPadding getPadding(bool isTopLevel) + { + if (isTopLevel) + { + return new MarginPadding + { + Horizontal = 70, + Vertical = 15 + }; + } + + return new MarginPadding + { + Top = 10 + }; + } + private class ParentUsername : FillFlowContainer, IHasTooltip { public string TooltipText => getParentMessage(); From dc577aa6fa2bc0df944d84bfbb27085217929737 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jul 2020 11:22:58 +0900 Subject: [PATCH 2403/6909] Fix display of bonus score --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerBonusDisplay.cs | 2 +- osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs | 4 +++- osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 4 +++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 23b440ced2..c36bec391f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.Disc.CumulativeRotation / 360) * 10; + return totalScore == (int)(drawableSpinner.Disc.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK; }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs index a8f5580735..b499d7a92b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; displayedCount = count; - bonusCounter.Text = $"{1000 * count}"; + bonusCounter.Text = $"{SpinnerBonusTick.SCORE_PER_TICK * count}"; bonusCounter.FadeOutFromOne(1500); bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index b59428e701..9c4b6f774f 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -9,6 +9,8 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SpinnerBonusTick : SpinnerTick { + public new const int SCORE_PER_TICK = 50; + public SpinnerBonusTick() { Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); @@ -18,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement { - protected override int NumericResultFor(HitResult result) => 50; + protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK; protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2; } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index 346f949a4f..de3ae27e55 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -9,6 +9,8 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SpinnerTick : OsuHitObject { + public const int SCORE_PER_TICK = 10; + public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; @@ -17,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects { public override bool AffectsCombo => false; - protected override int NumericResultFor(HitResult result) => 10; + protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK; protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0; } From df3e2cc640ca60f5118927f43f67e71b1e0f69b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jul 2020 12:08:15 +0900 Subject: [PATCH 2404/6909] Fix potential crash due to cross-thread TrackVirtualManual.Stop --- osu.Game/Tests/Visual/OsuTestScene.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index cb9ed40b00..866fc215d6 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -305,8 +305,10 @@ namespace osu.Game.Tests.Visual { double refTime = referenceClock.CurrentTime; - if (lastReferenceTime.HasValue) - accumulated += (refTime - lastReferenceTime.Value) * Rate; + double? lastRefTime = lastReferenceTime; + + if (lastRefTime != null) + accumulated += (refTime - lastRefTime.Value) * Rate; lastReferenceTime = refTime; } From a210deee9a3caa6cd20c841d808858f3e10331cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jul 2020 12:16:01 +0900 Subject: [PATCH 2405/6909] Remove unnecessary depth setter --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index e085334649..d24c81536e 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -139,7 +139,6 @@ namespace osu.Game.Rulesets.Judgements Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Depth = -float.MaxValue, Child = bodyDrawable = new SkinnableDrawable(new GameplaySkinComponent(type), _ => JudgementText = new OsuSpriteText { Text = type.GetDescription().ToUpperInvariant(), From a99c6698b7350b2f4362761cbaf64e932e2232fc Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 28 Jul 2020 04:25:01 +0000 Subject: [PATCH 2406/6909] Bump SharpCompress from 0.25.1 to 0.26.0 Bumps [SharpCompress](https://github.com/adamhathcock/sharpcompress) from 0.25.1 to 0.26.0. - [Release notes](https://github.com/adamhathcock/sharpcompress/releases) - [Commits](https://github.com/adamhathcock/sharpcompress/compare/0.25.1...0.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 7ebffc6d10..5ac54a853f 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 618de5d19f..8b2d1346be 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -81,7 +81,7 @@ - + From 72c8f0737ef5d125879c044677183f04f259cf02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jul 2020 14:18:14 +0900 Subject: [PATCH 2407/6909] Fix Autopilot mod incompatibility with WindUp/WindDown --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index d75f4c70d7..2263e2b2f4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Osu.Mods { @@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Mods private OsuInputManager inputManager; + private GameplayClock gameplayClock; + private List replayFrames; private int currentFrame; @@ -38,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Mods { if (currentFrame == replayFrames.Count - 1) return; - double time = playfield.Time.Current; + double time = gameplayClock.CurrentTime; // Very naive implementation of autopilot based on proximity to replay frames. // TODO: this needs to be based on user interactions to better match stable (pausing until judgement is registered). @@ -53,6 +56,8 @@ namespace osu.Game.Rulesets.Osu.Mods public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { + gameplayClock = drawableRuleset.FrameStableClock; + // Grab the input manager to disable the user's cursor, and for future use inputManager = (OsuInputManager)drawableRuleset.KeyBindingInputManager; inputManager.AllowUserCursorMovement = false; From e795b1ea318a97ff6693526b78003c85fd231cd2 Mon Sep 17 00:00:00 2001 From: Joe Yuan Date: Tue, 28 Jul 2020 00:38:31 -0700 Subject: [PATCH 2408/6909] Failing effect displays vertically --- osu.Game/Screens/Play/HUD/FailingLayer.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/FailingLayer.cs b/osu.Game/Screens/Play/HUD/FailingLayer.cs index 84dbb35f68..847b8a53cf 100644 --- a/osu.Game/Screens/Play/HUD/FailingLayer.cs +++ b/osu.Game/Screens/Play/HUD/FailingLayer.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Play.HUD private const float max_alpha = 0.4f; private const int fade_time = 400; - private const float gradient_size = 0.3f; + private const float gradient_size = 0.2f; /// /// The threshold under which the current player life should be considered low and the layer should start fading in. @@ -56,16 +56,16 @@ namespace osu.Game.Screens.Play.HUD new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientVertical(Color4.White, Color4.White.Opacity(0)), - Height = gradient_size, + Colour = ColourInfo.GradientHorizontal(Color4.White, Color4.White.Opacity(0)), + Width = gradient_size, }, new Box { RelativeSizeAxes = Axes.Both, - Height = gradient_size, - Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0), Color4.White), - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, + Width = gradient_size, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White), + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, }, } }, From ff3cb6487d2d474ff8d9ec9c6f164ee47d6efe62 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 16:52:07 +0900 Subject: [PATCH 2409/6909] Store all linked cancellation tokens --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 33 ++++++++++++++++--- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 5e644fbf1c..e625f6f96e 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -109,25 +109,42 @@ namespace osu.Game.Beatmaps } private CancellationTokenSource trackedUpdateCancellationSource; + private readonly List linkedCancellationSources = new List(); /// /// Updates all tracked using the current ruleset and mods. /// private void updateTrackedBindables() { - trackedUpdateCancellationSource?.Cancel(); + cancelTrackedBindableUpdate(); trackedUpdateCancellationSource = new CancellationTokenSource(); foreach (var b in trackedBindables) { - if (trackedUpdateCancellationSource.IsCancellationRequested) - break; + var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken); + linkedCancellationSources.Add(linkedSource); - using (var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken)) - updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token); + updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token); } } + /// + /// Cancels the existing update of all tracked via . + /// + private void cancelTrackedBindableUpdate() + { + trackedUpdateCancellationSource?.Cancel(); + trackedUpdateCancellationSource = null; + + foreach (var c in linkedCancellationSources) + { + c.Cancel(); + c.Dispose(); + } + + linkedCancellationSources.Clear(); + } + /// /// Updates the value of a with a given ruleset + mods. /// @@ -220,6 +237,12 @@ namespace osu.Game.Beatmaps return difficultyCache.TryGetValue(key, out existingDifficulty); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + cancelTrackedBindableUpdate(); + } + private readonly struct DifficultyCacheLookup : IEquatable { public readonly int BeatmapId; From 96f68a32518422f90abbdce672089bd5b5ee50f3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 16:52:19 +0900 Subject: [PATCH 2410/6909] Reorder method --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index e625f6f96e..98a1462d99 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -145,6 +145,22 @@ namespace osu.Game.Beatmaps linkedCancellationSources.Clear(); } + /// + /// Creates a new and triggers an initial value update. + /// + /// The that star difficulty should correspond to. + /// The initial to get the difficulty with. + /// The initial s to get the difficulty with. + /// An optional which stops updating the star difficulty for the given . + /// The . + private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable initialMods, + CancellationToken cancellationToken) + { + var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken); + updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken); + return bindable; + } + /// /// Updates the value of a with a given ruleset + mods. /// @@ -165,22 +181,6 @@ namespace osu.Game.Beatmaps }, cancellationToken); } - /// - /// Creates a new and triggers an initial value update. - /// - /// The that star difficulty should correspond to. - /// The initial to get the difficulty with. - /// The initial s to get the difficulty with. - /// An optional which stops updating the star difficulty for the given . - /// The . - private BindableStarDifficulty createBindable([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo initialRulesetInfo, [CanBeNull] IEnumerable initialMods, - CancellationToken cancellationToken) - { - var bindable = new BindableStarDifficulty(beatmapInfo, cancellationToken); - updateBindable(bindable, initialRulesetInfo, initialMods, cancellationToken); - return bindable; - } - /// /// Computes the difficulty defined by a key, and stores it to the timed cache. /// From ca434e82d961dac772296c6d335ff699efdcc1ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 28 Jul 2020 17:09:38 +0900 Subject: [PATCH 2411/6909] Fix test failures due to gameplay clock not being unpaused --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 81e5f32ee8..e0a1f947ec 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -25,6 +25,8 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUp] public void SetUp() => Schedule(() => { + gameplayClock.IsPaused.Value = false; + Children = new Drawable[] { new Container @@ -42,9 +44,10 @@ namespace osu.Game.Tests.Visual.Gameplay DrawableSample sample = null; AddStep("start sample with looping", () => { + sample = skinnableSound.ChildrenOfType().First(); + skinnableSound.Looping = true; skinnableSound.Play(); - sample = skinnableSound.ChildrenOfType().First(); }); AddUntilStep("wait for sample to start playing", () => sample.Playing); From fa25f8aef9993c53f2ac72a9ed1ecd4446848b3b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 17:23:35 +0900 Subject: [PATCH 2412/6909] Dispose update scheduler --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 98a1462d99..b3afd1d4fd 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -240,7 +240,9 @@ namespace osu.Game.Beatmaps protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + cancelTrackedBindableUpdate(); + updateScheduler.Dispose(); } private readonly struct DifficultyCacheLookup : IEquatable From f7cd6e83aa81bc24ccce152e37028851e1af8ee0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 17:58:58 +0900 Subject: [PATCH 2413/6909] Adjust mania scoring to be 95% based on accuracy --- osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index ba84c21845..4b2f643333 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -7,9 +7,9 @@ namespace osu.Game.Rulesets.Mania.Scoring { internal class ManiaScoreProcessor : ScoreProcessor { - protected override double DefaultAccuracyPortion => 0.8; + protected override double DefaultAccuracyPortion => 0.95; - protected override double DefaultComboPortion => 0.2; + protected override double DefaultComboPortion => 0.05; public override HitWindows CreateHitWindows() => new ManiaHitWindows(); } From 375dad087837ba6156be5ec629b0a370c19c7891 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 17:59:52 +0900 Subject: [PATCH 2414/6909] Increase PERFECT from 320 to 350 score --- osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index 53db676a54..53967ffa05 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Judgements return 300; case HitResult.Perfect: - return 320; + return 350; } } } From 54d2f2c8cd2e837d7b0e046f4bf8df91317eb802 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 20:34:09 +0900 Subject: [PATCH 2415/6909] Delay loading of cover backgrounds in score panels --- .../Ranking/Contracted/ContractedPanelMiddleContent.cs | 5 ++++- osu.Game/Screens/Ranking/ScorePanel.cs | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 8cd0e7025e..3ffb205d09 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -70,11 +70,14 @@ namespace osu.Game.Screens.Ranking.Contracted RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("444") }, - new UserCoverBackground + new DelayedLoadUnloadWrapper(() => new UserCoverBackground { RelativeSizeAxes = Axes.Both, User = score.User, Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0)) + }, 300, 5000) + { + RelativeSizeAxes = Axes.Both }, new FillFlowContainer { diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 5da432d5b2..7ac98604f4 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -146,11 +146,14 @@ namespace osu.Game.Screens.Ranking Children = new[] { middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both }, - new UserCoverBackground + new DelayedLoadUnloadWrapper(() => new UserCoverBackground { RelativeSizeAxes = Axes.Both, User = Score.User, Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0)) + }, 300, 5000) + { + RelativeSizeAxes = Axes.Both }, } }, From 42e88c53d75a03f37fe8699ca2512c0477fb8c75 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 20:50:55 +0900 Subject: [PATCH 2416/6909] Embed behaviour into UserCoverBackground --- osu.Game/Overlays/Profile/ProfileHeader.cs | 7 ++++++- .../Contracted/ContractedPanelMiddleContent.cs | 5 +---- osu.Game/Screens/Ranking/ScorePanel.cs | 7 ++----- osu.Game/Users/UserCoverBackground.cs | 11 +++++++++++ osu.Game/Users/UserPanel.cs | 10 ++-------- 5 files changed, 22 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 2e5f1071f2..55474c9d3e 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Profile Masking = true, Children = new Drawable[] { - coverContainer = new UserCoverBackground + coverContainer = new ProfileCoverBackground { RelativeSizeAxes = Axes.Both, }, @@ -100,5 +100,10 @@ namespace osu.Game.Overlays.Profile IconTexture = "Icons/profile"; } } + + private class ProfileCoverBackground : UserCoverBackground + { + protected override double LoadDelay => 0; + } } } diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 3ffb205d09..8cd0e7025e 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -70,14 +70,11 @@ namespace osu.Game.Screens.Ranking.Contracted RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex("444") }, - new DelayedLoadUnloadWrapper(() => new UserCoverBackground + new UserCoverBackground { RelativeSizeAxes = Axes.Both, User = score.User, Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0)) - }, 300, 5000) - { - RelativeSizeAxes = Axes.Both }, new FillFlowContainer { diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 7ac98604f4..24d193e9a7 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -146,15 +146,12 @@ namespace osu.Game.Screens.Ranking Children = new[] { middleLayerBackground = new Box { RelativeSizeAxes = Axes.Both }, - new DelayedLoadUnloadWrapper(() => new UserCoverBackground + new UserCoverBackground { RelativeSizeAxes = Axes.Both, User = Score.User, Colour = ColourInfo.GradientVertical(Color4.White.Opacity(0.5f), Color4Extensions.FromHex("#444").Opacity(0)) - }, 300, 5000) - { - RelativeSizeAxes = Axes.Both - }, + } } }, middleLayerContentContainer = new Container { RelativeSizeAxes = Axes.Both } diff --git a/osu.Game/Users/UserCoverBackground.cs b/osu.Game/Users/UserCoverBackground.cs index 748d9bd939..34bbf6892e 100644 --- a/osu.Game/Users/UserCoverBackground.cs +++ b/osu.Game/Users/UserCoverBackground.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; @@ -23,6 +24,16 @@ namespace osu.Game.Users protected override Drawable CreateDrawable(User user) => new Cover(user); + protected override double LoadDelay => 300; + + /// + /// Delay before the background is unloaded while off-screen. + /// + protected virtual double UnloadDelay => 5000; + + protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) + => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay); + [LongRunningLoad] private class Cover : CompositeDrawable { diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 94c0c31cfc..57a87a713d 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -25,7 +24,7 @@ namespace osu.Game.Users protected Action ViewProfile { get; private set; } - protected DelayedLoadUnloadWrapper Background { get; private set; } + protected Drawable Background { get; private set; } protected UserPanel(User user) { @@ -56,17 +55,12 @@ namespace osu.Game.Users RelativeSizeAxes = Axes.Both, Colour = ColourProvider?.Background5 ?? Colours.Gray1 }, - Background = new DelayedLoadUnloadWrapper(() => new UserCoverBackground + Background = 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() }); From 9f6446d83685fb4d6052930129ae7b68f7781f50 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 20:58:13 +0900 Subject: [PATCH 2417/6909] Add xmldocs + refactoring --- osu.Game/Screens/Ranking/ResultsScreen.cs | 17 +++++++++++------ osu.Game/Screens/Ranking/ScorePanelList.cs | 15 +++++++++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index c5512822b2..254ab76f5b 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -50,6 +50,9 @@ namespace osu.Game.Screens.Ranking private ScorePanelList scorePanelList; private Container detachedPanelContainer; + private bool fetchedInitialScores; + private APIRequest nextPageRequest; + protected ResultsScreen(ScoreInfo score, bool allowRetry = true) { Score = score; @@ -172,13 +175,11 @@ namespace osu.Game.Screens.Ranking statisticsPanel.State.BindValueChanged(onStatisticsStateChanged, true); } - private APIRequest nextPageRequest; - protected override void Update() { base.Update(); - if (hasAnyScores && nextPageRequest == null) + if (fetchedInitialScores && nextPageRequest == null) { if (scorePanelList.IsScrolledToStart) nextPageRequest = FetchNextPage(-1, fetchScoresCallback); @@ -202,16 +203,20 @@ 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; + /// + /// Performs a fetch of the next page of scores. This is invoked every frame until a non-null is returned. + /// + /// The fetch direction. -1 to fetch scores greater than the current start of the list, and 1 to fetch scores lower than the current end of the list. + /// A callback which should be called when fetching is completed. Scheduling is not required. + /// An responsible for the fetch operation. This will be queued and performed automatically. protected virtual APIRequest FetchNextPage(int direction, Action> scoresCallback) => null; - private bool hasAnyScores; - private void fetchScoresCallback(IEnumerable scores) => Schedule(() => { foreach (var s in scores) addScore(s); - hasAnyScores = true; + fetchedInitialScores = true; }); public override void OnEntering(IScreen last) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index aba8314732..b2e1e91831 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -26,9 +26,20 @@ namespace osu.Game.Screens.Ranking /// private const float expanded_panel_spacing = 15; - public bool IsScrolledToStart => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.Current <= 100; + /// + /// Minimum distance from either end point of the list that the list can be considered scrolled to the end point. + /// + private const float scroll_endpoint_distance = 100; - public bool IsScrolledToEnd => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(100); + /// + /// Whether this can be scrolled and is currently scrolled to the start. + /// + public bool IsScrolledToStart => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.Current <= scroll_endpoint_distance; + + /// + /// Whether this can be scrolled and is currently scrolled to the end. + /// + public bool IsScrolledToEnd => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance); /// /// An action to be invoked if a is clicked while in an expanded state. From db91d1de50e9ab92f7df1ec45abbea93696766af Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 21:40:11 +0900 Subject: [PATCH 2418/6909] Use response params in next page request --- .../Multiplayer/IndexPlaylistScoresRequest.cs | 25 +++++----- .../Online/Multiplayer/IndexScoresParams.cs | 20 ++++++++ .../Online/Multiplayer/MultiplayerScores.cs | 6 +++ .../Multi/Ranking/TimeshiftResultsScreen.cs | 49 ++++++------------- 4 files changed, 53 insertions(+), 47 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/IndexScoresParams.cs diff --git a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs index 7273c0eea6..67793df344 100644 --- a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.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.IO.Network; using osu.Game.Extensions; using osu.Game.Online.API; @@ -16,31 +17,31 @@ namespace osu.Game.Online.Multiplayer private readonly int roomId; private readonly int playlistItemId; private readonly Cursor cursor; - private readonly MultiplayerScoresSort? sort; + private readonly IndexScoresParams indexParams; - public IndexPlaylistScoresRequest(int roomId, int playlistItemId, Cursor cursor = null, MultiplayerScoresSort? sort = null) + public IndexPlaylistScoresRequest(int roomId, int playlistItemId) { this.roomId = roomId; this.playlistItemId = playlistItemId; + } + + public IndexPlaylistScoresRequest(int roomId, int playlistItemId, [NotNull] Cursor cursor, [NotNull] IndexScoresParams indexParams) + : this(roomId, playlistItemId) + { this.cursor = cursor; - this.sort = sort; + this.indexParams = indexParams; } protected override WebRequest CreateWebRequest() { var req = base.CreateWebRequest(); - req.AddCursor(cursor); - - switch (sort) + if (cursor != null) { - case MultiplayerScoresSort.Ascending: - req.AddParameter("sort", "score_asc"); - break; + req.AddCursor(cursor); - case MultiplayerScoresSort.Descending: - req.AddParameter("sort", "score_desc"); - break; + foreach (var (key, value) in indexParams.Properties) + req.AddParameter(key, value.ToString()); } return req; diff --git a/osu.Game/Online/Multiplayer/IndexScoresParams.cs b/osu.Game/Online/Multiplayer/IndexScoresParams.cs new file mode 100644 index 0000000000..8160dfefaf --- /dev/null +++ b/osu.Game/Online/Multiplayer/IndexScoresParams.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.Multiplayer +{ + /// + /// A collection of parameters which should be passed to the index endpoint to fetch the next page. + /// + public class IndexScoresParams + { + [UsedImplicitly] + [JsonExtensionData] + public IDictionary Properties; + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerScores.cs b/osu.Game/Online/Multiplayer/MultiplayerScores.cs index 6f74fc8984..2d0f98e032 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScores.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerScores.cs @@ -29,5 +29,11 @@ namespace osu.Game.Online.Multiplayer /// [JsonProperty("user_score")] public MultiplayerScore UserScore { get; set; } + + /// + /// The parameters to be used to fetch the next page. + /// + [JsonProperty("params")] + public IndexScoresParams Params { get; set; } } } diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 75a61b92ee..648bee385c 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -10,7 +10,6 @@ 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; using osu.Game.Scoring; using osu.Game.Screens.Ranking; @@ -23,8 +22,8 @@ namespace osu.Game.Screens.Multi.Ranking private readonly PlaylistItem playlistItem; private LoadingSpinner loadingLayer; - private Cursor higherScoresCursor; - private Cursor lowerScoresCursor; + private MultiplayerScores higherScores; + private MultiplayerScores lowerScores; [Resolved] private IAPIProvider api { get; set; } @@ -52,8 +51,8 @@ namespace osu.Game.Screens.Multi.Ranking protected override APIRequest FetchScores(Action> scoresCallback) { // This performs two requests: - // 1. A request to show the user's score. - // 2. If (1) fails, a request to index the room. + // 1. A request to show the user's score (and scores around). + // 2. If that fails, a request to index the room starting from the highest score. var userScoreReq = new ShowPlaylistUserScoreRequest(roomId, playlistItem.ID, api.LocalUser.Value.Id); @@ -64,13 +63,13 @@ namespace osu.Game.Screens.Multi.Ranking if (userScore.ScoresAround?.Higher != null) { allScores.AddRange(userScore.ScoresAround.Higher.Scores); - higherScoresCursor = userScore.ScoresAround.Higher.Cursor; + higherScores = userScore.ScoresAround.Higher; } if (userScore.ScoresAround?.Lower != null) { allScores.AddRange(userScore.ScoresAround.Lower.Scores); - lowerScoresCursor = userScore.ScoresAround.Lower.Cursor; + lowerScores = userScore.ScoresAround.Lower; } performSuccessCallback(scoresCallback, allScores); @@ -84,7 +83,7 @@ namespace osu.Game.Screens.Multi.Ranking indexReq.Success += r => { performSuccessCallback(scoresCallback, r.Scores); - lowerScoresCursor = r.Cursor; + lowerScores = r; }; indexReq.Failure += __ => loadingLayer.Hide(); @@ -99,39 +98,19 @@ namespace osu.Game.Screens.Multi.Ranking { Debug.Assert(direction == 1 || direction == -1); - Cursor cursor; - MultiplayerScoresSort sort; + MultiplayerScores pivot = direction == -1 ? higherScores : lowerScores; - switch (direction) - { - case -1: - cursor = higherScoresCursor; - sort = MultiplayerScoresSort.Ascending; - break; - - default: - cursor = lowerScoresCursor; - sort = MultiplayerScoresSort.Descending; - break; - } - - if (cursor == null) + if (pivot?.Cursor == null) return null; - var indexReq = new IndexPlaylistScoresRequest(roomId, playlistItem.ID, cursor, sort); + var indexReq = new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params); indexReq.Success += r => { - switch (direction) - { - case -1: - higherScoresCursor = r.Cursor; - break; - - default: - lowerScoresCursor = r.Cursor; - break; - } + if (direction == -1) + higherScores = r; + else + lowerScores = r; performSuccessCallback(scoresCallback, r.Scores); }; From ccc377ae6af607215edc8756a1cd24881be427d1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 21:42:04 +0900 Subject: [PATCH 2419/6909] Remove unused enum --- .../Online/Multiplayer/MultiplayerScoresSort.cs | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 osu.Game/Online/Multiplayer/MultiplayerScoresSort.cs diff --git a/osu.Game/Online/Multiplayer/MultiplayerScoresSort.cs b/osu.Game/Online/Multiplayer/MultiplayerScoresSort.cs deleted file mode 100644 index decb1c4dfe..0000000000 --- a/osu.Game/Online/Multiplayer/MultiplayerScoresSort.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. - -namespace osu.Game.Online.Multiplayer -{ - /// - /// Sorting option for indexing multiplayer scores. - /// - public enum MultiplayerScoresSort - { - Ascending, - Descending - } -} From a57b6bdc1817bec9274c92a3d879878707c355c1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 11:29:38 +0900 Subject: [PATCH 2420/6909] Remove cancellation of linked tokens --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index b3afd1d4fd..58b96b08b0 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -137,10 +137,7 @@ namespace osu.Game.Beatmaps trackedUpdateCancellationSource = null; foreach (var c in linkedCancellationSources) - { - c.Cancel(); c.Dispose(); - } linkedCancellationSources.Clear(); } From 46483622149904c9240f6f6e53ba4ef283edb07d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 11:30:25 +0900 Subject: [PATCH 2421/6909] Safeguard against potential finalise-before-initialised --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 58b96b08b0..b80b4e45ed 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -136,10 +136,13 @@ namespace osu.Game.Beatmaps trackedUpdateCancellationSource?.Cancel(); trackedUpdateCancellationSource = null; - foreach (var c in linkedCancellationSources) - c.Dispose(); + if (linkedCancellationSources != null) + { + foreach (var c in linkedCancellationSources) + c.Dispose(); - linkedCancellationSources.Clear(); + linkedCancellationSources.Clear(); + } } /// @@ -239,7 +242,7 @@ namespace osu.Game.Beatmaps base.Dispose(isDisposing); cancelTrackedBindableUpdate(); - updateScheduler.Dispose(); + updateScheduler?.Dispose(); } private readonly struct DifficultyCacheLookup : IEquatable From 6c7e806eacecd71ad073d160ac95dbcadaad8199 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jul 2020 12:39:18 +0900 Subject: [PATCH 2422/6909] Include executable hash when submitting multiplayer scores --- osu.Game/OsuGameBase.cs | 14 ++++++++++++++ osu.Game/Scoring/ScoreInfo.cs | 3 +++ osu.Game/Screens/Play/Player.cs | 1 + 3 files changed, 18 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index fe5c0704b7..964a7fdd35 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Development; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.IO.Stores; @@ -97,6 +98,11 @@ namespace osu.Game public virtual Version AssemblyVersion => Assembly.GetEntryAssembly()?.GetName().Version ?? new Version(); + /// + /// MD5 representation of the game executable. + /// + public string VersionHash { get; private set; } + public bool IsDeployedBuild => AssemblyVersion.Major > 0; public virtual string Version @@ -128,6 +134,14 @@ namespace osu.Game [BackgroundDependencyLoader] private void load() { + var assembly = Assembly.GetEntryAssembly(); + + if (assembly != null) + { + using (var str = File.OpenRead(assembly.Location)) + VersionHash = str.ComputeMD5Hash(); + } + Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); dependencies.Cache(contextFactory = new DatabaseContextFactory(Storage)); diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 84c0d5b54e..2cc065b5ad 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -51,6 +51,9 @@ namespace osu.Game.Scoring [NotMapped] public bool Passed { get; set; } = true; + [JsonProperty("version_hash")] + public string VersionHash { get; set; } + [JsonIgnore] public virtual RulesetInfo Ruleset { get; set; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..5df6cf42cb 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -465,6 +465,7 @@ namespace osu.Game.Screens.Play { Beatmap = Beatmap.Value.BeatmapInfo, Ruleset = rulesetInfo, + VersionHash = Game.VersionHash, Mods = Mods.Value.ToArray(), }; From d7fab98af0352c39d07a9bd8d3325aa373376e78 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 29 Jul 2020 06:39:23 +0300 Subject: [PATCH 2423/6909] Update comments container footer in line with web --- .../Overlays/Comments/CommentsContainer.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index f71808ba89..2a78748be6 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -78,21 +78,22 @@ namespace osu.Game.Overlays.Comments AutoSizeAxes = Axes.Y, Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4 - }, new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, + Margin = new MarginPadding { Bottom = 20 }, Children = new Drawable[] { deletedCommentsCounter = new DeletedCommentsCounter { - ShowDeleted = { BindTarget = ShowDeleted } + ShowDeleted = { BindTarget = ShowDeleted }, + Margin = new MarginPadding + { + Horizontal = 70, + Vertical = 10 + } }, new Container { @@ -102,7 +103,10 @@ namespace osu.Game.Overlays.Comments { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Margin = new MarginPadding(5), + Margin = new MarginPadding + { + Vertical = 10 + }, Action = getComments, IsLoading = true, } From 9e6d562872effe4ab8c763dd0a6db0b7f0be1ffe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jul 2020 13:18:40 +0900 Subject: [PATCH 2424/6909] Send in initial score request instead --- osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs | 5 ++++- osu.Game/Scoring/ScoreInfo.cs | 3 --- osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs | 2 +- osu.Game/Screens/Play/Player.cs | 1 - 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs index f973f96b37..2d99b12519 100644 --- a/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs @@ -11,17 +11,20 @@ namespace osu.Game.Online.Multiplayer { private readonly int roomId; private readonly int playlistItemId; + private readonly string versionHash; - public CreateRoomScoreRequest(int roomId, int playlistItemId) + public CreateRoomScoreRequest(int roomId, int playlistItemId, string versionHash) { this.roomId = roomId; this.playlistItemId = playlistItemId; + this.versionHash = versionHash; } protected override WebRequest CreateWebRequest() { var req = base.CreateWebRequest(); req.Method = HttpMethod.Post; + req.AddParameter("version_hash", versionHash); return req; } diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 2cc065b5ad..84c0d5b54e 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -51,9 +51,6 @@ namespace osu.Game.Scoring [NotMapped] public bool Passed { get; set; } = true; - [JsonProperty("version_hash")] - public string VersionHash { get; set; } - [JsonIgnore] public virtual RulesetInfo Ruleset { get; set; } diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index c2381fe219..da082692d7 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Multi.Play if (!playlistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals))) throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods"); - var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID); + var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID, Game.VersionHash); req.Success += r => token = r.ID; req.Failure += e => { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 5df6cf42cb..541275cf55 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -465,7 +465,6 @@ namespace osu.Game.Screens.Play { Beatmap = Beatmap.Value.BeatmapInfo, Ruleset = rulesetInfo, - VersionHash = Game.VersionHash, Mods = Mods.Value.ToArray(), }; From c3c60334ec7b1806cd9cb7e99b39a0d9951c50a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jul 2020 15:24:14 +0900 Subject: [PATCH 2425/6909] Add skinning support to spinner test scene --- .../TestSceneSpinner.cs | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 67afc45e32..8e3a22bfdc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -4,37 +4,32 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; 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.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] - public class TestSceneSpinner : OsuTestScene + public class TestSceneSpinner : OsuSkinnableTestScene { - private readonly Container content; - protected override Container Content => content; - private int depthIndex; public TestSceneSpinner() { - base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); + // base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); - AddStep("Miss Big", () => testSingle(2)); - AddStep("Miss Medium", () => testSingle(5)); - AddStep("Miss Small", () => testSingle(7)); - AddStep("Hit Big", () => testSingle(2, true)); - AddStep("Hit Medium", () => testSingle(5, true)); - AddStep("Hit Small", () => testSingle(7, true)); + AddStep("Miss Big", () => SetContents(() => testSingle(2))); + AddStep("Miss Medium", () => SetContents(() => testSingle(5))); + AddStep("Miss Small", () => SetContents(() => testSingle(7))); + AddStep("Hit Big", () => SetContents(() => testSingle(2, true))); + AddStep("Hit Medium", () => SetContents(() => testSingle(5, true))); + AddStep("Hit Small", () => SetContents(() => testSingle(7, true))); } - private void testSingle(float circleSize, bool auto = false) + private Drawable testSingle(float circleSize, bool auto = false) { var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 }; @@ -49,12 +44,12 @@ namespace osu.Game.Rulesets.Osu.Tests foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToDrawableHitObjects(new[] { drawable }); - Add(drawable); + return drawable; } private class TestDrawableSpinner : DrawableSpinner { - private bool auto; + private readonly bool auto; public TestDrawableSpinner(Spinner s, bool auto) : base(s) @@ -62,16 +57,11 @@ namespace osu.Game.Rulesets.Osu.Tests this.auto = auto; } - protected override void CheckForResult(bool userTriggered, double timeOffset) + protected override void Update() { - if (auto && !userTriggered && Time.Current > Spinner.StartTime + Spinner.Duration / 2 && Progress < 1) - { - // force completion only once to not break human interaction - Disc.CumulativeRotation = Spinner.SpinsRequired * 360; - auto = false; - } - - base.CheckForResult(userTriggered, timeOffset); + base.Update(); + if (auto) + Disc.Rotate((float)(Clock.ElapsedFrameTime * 3)); } } } From 0f1f4b2b5c97656e80650b4560bfe4d1adb3b740 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 15:36:42 +0900 Subject: [PATCH 2426/6909] Add pooling for mania hit explosions --- .../Skinning/LegacyHitExplosion.cs | 7 ++- osu.Game.Rulesets.Mania/UI/Column.cs | 15 ++----- .../UI/DefaultHitExplosion.cs | 28 +++++------- osu.Game.Rulesets.Mania/UI/IHitExplosion.cs | 16 +++++++ .../UI/PoolableHitExplosion.cs | 44 +++++++++++++++++++ 5 files changed, 79 insertions(+), 31 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/IHitExplosion.cs create mode 100644 osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index bc93bb2615..c2b39945c2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -6,13 +6,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; +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 LegacyHitExplosion : LegacyManiaColumnElement + public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion { private readonly IBindable direction = new Bindable(); @@ -62,10 +63,8 @@ namespace osu.Game.Rulesets.Mania.Skinning explosion.Anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; } - protected override void LoadComplete() + public void Animate() { - base.LoadComplete(); - explosion?.FadeInFromZero(80) .Then().FadeOut(120); } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 642353bd0b..7ddac759db 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -9,9 +9,9 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -34,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.UI public readonly Bindable Action = new Bindable(); public readonly ColumnHitObjectArea HitObjectArea; - internal readonly Container TopLevelContainer; + private readonly DrawablePool hitExplosionPool; public Container UnderlayElements => HitObjectArea.UnderlayElements; @@ -53,6 +53,7 @@ namespace osu.Game.Rulesets.Mania.UI InternalChildren = new[] { + hitExplosionPool = new DrawablePool(5), // 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(Index, HitObjectContainer) { RelativeSizeAxes = Axes.Both }, @@ -108,15 +109,7 @@ namespace osu.Game.Rulesets.Mania.UI if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; - var explosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, Index), _ => - new DefaultHitExplosion(judgedObject.AccentColour.Value, judgedObject is DrawableHoldNoteTick)) - { - RelativeSizeAxes = Axes.Both - }; - - HitObjectArea.Explosions.Add(explosion); - - explosion.Delay(200).Expire(true); + HitObjectArea.Explosions.Add(hitExplosionPool.Get()); } public bool OnPressed(ManiaAction action) diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs index 7a047ed121..bac77b134c 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs @@ -15,7 +15,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.UI { - public class DefaultHitExplosion : CompositeDrawable + public class DefaultHitExplosion : CompositeDrawable, IHitExplosion { public override bool RemoveWhenNotAlive => true; @@ -123,21 +123,6 @@ namespace osu.Game.Rulesets.Mania.UI direction.BindValueChanged(onDirectionChanged, true); } - protected override void LoadComplete() - { - const double duration = 200; - - base.LoadComplete(); - - largeFaint - .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) - .FadeOut(duration * 2); - - mainGlow1.ScaleTo(1.4f, duration, Easing.OutQuint); - - this.FadeOut(duration, Easing.Out); - } - private void onDirectionChanged(ValueChangedEvent direction) { if (direction.NewValue == ScrollingDirection.Up) @@ -151,5 +136,16 @@ namespace osu.Game.Rulesets.Mania.UI Y = -DefaultNotePiece.NOTE_HEIGHT / 2; } } + + public void Animate() + { + largeFaint + .ResizeTo(largeFaint.Size * new Vector2(5, 1), PoolableHitExplosion.DURATION, Easing.OutQuint) + .FadeOut(PoolableHitExplosion.DURATION * 2); + + mainGlow1.ScaleTo(1.4f, PoolableHitExplosion.DURATION, Easing.OutQuint); + + this.FadeOut(PoolableHitExplosion.DURATION, Easing.Out); + } } } diff --git a/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs new file mode 100644 index 0000000000..da1742e48b --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/IHitExplosion.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. + +namespace osu.Game.Rulesets.Mania.UI +{ + /// + /// Common interface for all hit explosion bodies. + /// + public interface IHitExplosion + { + /// + /// Begins animating this . + /// + void Animate(); + } +} diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs new file mode 100644 index 0000000000..43808f99a8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class PoolableHitExplosion : PoolableDrawable + { + public const double DURATION = 200; + + [Resolved] + private Column column { get; set; } + + private SkinnableDrawable skinnableExplosion; + + public PoolableHitExplosion() + { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), + _ => new DefaultHitExplosion(column.AccentColour, false /*todo */)) + { + RelativeSizeAxes = Axes.Both + }; + } + + protected override void PrepareForUse() + { + base.PrepareForUse(); + + (skinnableExplosion?.Drawable as IHitExplosion)?.Animate(); + + this.Delay(DURATION).Then().Expire(); + } + } +} From 7f2e554ad47a6e2ac9f3d691d1d128838a49546f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 15:52:25 +0900 Subject: [PATCH 2427/6909] Fix animations not being reset --- .../Skinning/LegacyHitExplosion.cs | 2 ++ osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs | 15 +++++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index c2b39945c2..c4fcffbc22 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -65,6 +65,8 @@ namespace osu.Game.Rulesets.Mania.Skinning public void Animate() { + (explosion as IFramedAnimation)?.GotoFrame(0); + explosion?.FadeInFromZero(80) .Then().FadeOut(120); } diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs index bac77b134c..3007668117 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Mania.UI { public class DefaultHitExplosion : CompositeDrawable, IHitExplosion { + private const float default_large_faint_size = 0.8f; + public override bool RemoveWhenNotAlive => true; private readonly IBindable direction = new Bindable(); @@ -54,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Both, Masking = true, // we want our size to be very small so the glow dominates it. - Size = new Vector2(0.8f), + Size = new Vector2(default_large_faint_size), Blending = BlendingParameters.Additive, EdgeEffect = new EdgeEffectParameters { @@ -140,12 +142,17 @@ namespace osu.Game.Rulesets.Mania.UI public void Animate() { largeFaint - .ResizeTo(largeFaint.Size * new Vector2(5, 1), PoolableHitExplosion.DURATION, Easing.OutQuint) + .ResizeTo(default_large_faint_size) + .Then() + .ResizeTo(default_large_faint_size * new Vector2(5, 1), PoolableHitExplosion.DURATION, Easing.OutQuint) .FadeOut(PoolableHitExplosion.DURATION * 2); - mainGlow1.ScaleTo(1.4f, PoolableHitExplosion.DURATION, Easing.OutQuint); + mainGlow1 + .ScaleTo(1) + .Then() + .ScaleTo(1.4f, PoolableHitExplosion.DURATION, Easing.OutQuint); - this.FadeOut(PoolableHitExplosion.DURATION, Easing.Out); + this.FadeOutFromOne(PoolableHitExplosion.DURATION, Easing.Out); } } } From 00821e7b653b58f55de16c9e9cfdb87b2846f85c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 16:14:19 +0900 Subject: [PATCH 2428/6909] Re-implement support for small ticks --- .../Skinning/LegacyHitExplosion.cs | 3 +- osu.Game.Rulesets.Mania/UI/Column.cs | 2 +- .../UI/DefaultHitExplosion.cs | 43 +++++++++++-------- osu.Game.Rulesets.Mania/UI/IHitExplosion.cs | 5 ++- .../UI/PoolableHitExplosion.cs | 13 ++++-- 5 files changed, 41 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index c4fcffbc22..12747924de 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -63,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.Skinning explosion.Anchor = direction.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; } - public void Animate() + public void Animate(JudgementResult result) { (explosion as IFramedAnimation)?.GotoFrame(0); diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 7ddac759db..255ce4c064 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -109,7 +109,7 @@ namespace osu.Game.Rulesets.Mania.UI if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; - HitObjectArea.Explosions.Add(hitExplosionPool.Get()); + HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result))); } public bool OnPressed(ManiaAction action) diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs index 3007668117..225269cf48 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs @@ -8,6 +8,8 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Utils; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.UI.Scrolling; using osuTK; @@ -21,31 +23,30 @@ namespace osu.Game.Rulesets.Mania.UI public override bool RemoveWhenNotAlive => true; + [Resolved] + private Column column { get; set; } + private readonly IBindable direction = new Bindable(); - private readonly CircularContainer largeFaint; - private readonly CircularContainer mainGlow1; + private CircularContainer largeFaint; + private CircularContainer mainGlow1; - public DefaultHitExplosion(Color4 objectColour, bool isSmall = false) + public DefaultHitExplosion() { Origin = Anchor.Centre; RelativeSizeAxes = Axes.X; Height = DefaultNotePiece.NOTE_HEIGHT; + } - // scale roughly in-line with visual appearance of notes - Scale = new Vector2(1f, 0.6f); - - if (isSmall) - Scale *= 0.5f; - + [BackgroundDependencyLoader] + private void load(IScrollingInfo scrollingInfo) + { const float angle_variangle = 15; // should be less than 45 - const float roundness = 80; - const float initial_height = 10; - var colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1); + var colour = Interpolation.ValueAt(0.4f, column.AccentColour, Color4.White, 0, 1); InternalChildren = new Drawable[] { @@ -61,7 +62,7 @@ namespace osu.Game.Rulesets.Mania.UI EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), + Colour = Interpolation.ValueAt(0.1f, column.AccentColour, Color4.White, 0, 1).Opacity(0.3f), Roundness = 160, Radius = 200, }, @@ -76,7 +77,7 @@ namespace osu.Game.Rulesets.Mania.UI EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), + Colour = Interpolation.ValueAt(0.6f, column.AccentColour, Color4.White, 0, 1), Roundness = 20, Radius = 50, }, @@ -116,11 +117,7 @@ namespace osu.Game.Rulesets.Mania.UI }, } }; - } - [BackgroundDependencyLoader] - private void load(IScrollingInfo scrollingInfo) - { direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); } @@ -139,8 +136,16 @@ namespace osu.Game.Rulesets.Mania.UI } } - public void Animate() + public void Animate(JudgementResult result) { + // scale roughly in-line with visual appearance of notes + Vector2 scale = new Vector2(1, 0.6f); + + if (result.Judgement is HoldNoteTickJudgement) + scale *= 0.5f; + + this.ScaleTo(scale); + largeFaint .ResizeTo(default_large_faint_size) .Then() diff --git a/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs index da1742e48b..3252dcc276 100644 --- a/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/IHitExplosion.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Judgements; + namespace osu.Game.Rulesets.Mania.UI { /// @@ -11,6 +13,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// Begins animating this . /// - void Animate(); + /// The type of that caused this explosion. + void Animate(JudgementResult result); } } diff --git a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs index 43808f99a8..64b7d7d550 100644 --- a/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/PoolableHitExplosion.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Judgements; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.UI @@ -12,6 +13,8 @@ namespace osu.Game.Rulesets.Mania.UI { public const double DURATION = 200; + public JudgementResult Result { get; private set; } + [Resolved] private Column column { get; set; } @@ -25,18 +28,22 @@ namespace osu.Game.Rulesets.Mania.UI [BackgroundDependencyLoader] private void load() { - InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), - _ => new DefaultHitExplosion(column.AccentColour, false /*todo */)) + InternalChild = skinnableExplosion = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HitExplosion, column.Index), _ => new DefaultHitExplosion()) { RelativeSizeAxes = Axes.Both }; } + public void Apply(JudgementResult result) + { + Result = result; + } + protected override void PrepareForUse() { base.PrepareForUse(); - (skinnableExplosion?.Drawable as IHitExplosion)?.Animate(); + (skinnableExplosion?.Drawable as IHitExplosion)?.Animate(Result); this.Delay(DURATION).Then().Expire(); } From d01d1ce3f1f6bd1b239eab98cf0ffb1a6abdb9bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jul 2020 16:25:10 +0900 Subject: [PATCH 2429/6909] Add initial support for spinner disc skinning --- .../old-skin/spinner-approachcircle.png | Bin 0 -> 26350 bytes .../Resources/old-skin/spinner-background.png | Bin 0 -> 46103 bytes .../Resources/old-skin/spinner-circle.png | Bin 0 -> 166439 bytes .../Resources/old-skin/spinner-clear.png | Bin 0 -> 39074 bytes .../Resources/old-skin/spinner-metre.png | Bin 0 -> 14518 bytes .../Resources/old-skin/spinner-osu.png | Bin 0 -> 18585 bytes .../Resources/old-skin/spinner-spin.png | Bin 0 -> 21353 bytes .../Resources/old-skin/spinnerbonus.wav | Bin 0 -> 309536 bytes .../Resources/old-skin/spinnerspin.wav | Bin 0 -> 36868 bytes .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 30 ++++++++++++------ osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 1 + .../Skinning/OsuLegacySkinTransformer.cs | 9 ++++++ 13 files changed, 31 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-circle.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-metre.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-osu.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerspin.wav diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-approachcircle.png new file mode 100644 index 0000000000000000000000000000000000000000..3811e5050f4cc0404a5713b1b875ab33ac32243a GIT binary patch literal 26350 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 S&tJL__^tB%W98pWj^KYrw90@0 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-background.png new file mode 100644 index 0000000000000000000000000000000000000000..d84eab2f15eb844ef0c37f3a0fbda07f7f4b4d16 GIT binary patch literal 46103 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

    Gk~*^-q;;x5EnoHUef!Db4# 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�r4vqggU&a{!Z;_c?}k>Xn?pcZ1606Q~R$ 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%0Adh;ycK}XpES)0?nv?hw6qc!^ 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 LNVHN!-~ayrzhB6! literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-clear.png new file mode 100644 index 0000000000000000000000000000000000000000..c66f1c9309cce5f54b6345d84a5bb4efbec30a82 GIT binary patch literal 39074 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%0Dwjh0N}037W8g|D4}N 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 kWRgiHc{?~s(fh($ literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinner-spin.png new file mode 100644 index 0000000000000000000000000000000000000000..98a9991c2f103308706ea3c52baf0b3e90ef5a11 GIT binary patch literal 21353 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

    SjXN<|hNt%y;m$|%aMvI6 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav b/osu.Game.Rulesets.Osu.Tests/Resources/old-skin/spinnerbonus.wav new file mode 100644 index 0000000000000000000000000000000000000000..5e583e77aa7441b968e4ebc8de47da1033206c29 GIT binary patch literal 309536 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

    }JOkaT^af>To^nKaittEF$R(n9ODq-t4bDj? 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�MEB%`;@=66TlPg~+G*5Vp;#AkidRNgXuZ=u+WJYiqXiez2M%n6~) z0T)_l8nXqLg4yO}#>+oR@9tK3YiV;5DQHt%w8Xm`_9miU&#CJDIeoTo~RVT3qcHZ55#y5KYFB5ZQ>Dz-L2-;3NEmm`S zc%PF^IoQee@cp>a7ghd)=}>xlL-~@O5~I}UQ99zqSubWC%lsqrg!*xER&gE0WOuhZ zW_fRNHcq`c1mR~)*OSyqqiy7y$9*J-=^Vi`<7Iu+NNyR`g5SWU1}0w6ZECi=0W?X zn}GQQi*tjzznz(bRyeAYl$eG2-8IYl#O|pfe8nEQHsM+rrZh6EOSC@cv}$U!0x;?! z{Ji?(rk=G6t0WA00~C1y|D;Tt*s_Pa=u>_TT)`i8*Nc2Y>3MXBC z3o#@39pvO3mf-<)LW-kqtfqU0_G1ti4JKpli*cA>duVvk2=S+*tD>kj>(pjIj5u-!2zo*=uhH7`RfgPe{smW5i z<+qxI!4B!@!g%W~qV1&iO!E9)HKS5J5k^a4od$z=k z>|6*#Rn`BGbTX$PdwXN`MU)#7Puu@iR)rDPu}u^?cB%J|(_j^c-E0qLcORFA@|kID zMNM7H^Zn9E-VrPq?CY6!(6Md9=$6BIe4s)v8Q(+MSKqGTRo5f_Ju3CLO=heV`zre_ zHwALL`)|{ZG^7x1={}W!_g}^&EHQ^up9_2_w82}B2Nng6=|PrI5iNwt?Zt9c;6PtN zw{j`gBwkE5cvwAjmb+O`%7cJtmvT6nY<6fBlDqmSLg{jId8ZTSLH|KrDIsRSHzE=rcDdOITB_t zzT#i_0%xbMqs5}H$$AH)o1=TwSNU|+laeh1t%JiTJgb{xs|P_n>W`lyZ}Lc7lUAQ5 zX&H~~^5(B6h5qq9-o>4tmw$>;J@mFA_fa^)hRnm6SM42GLP`D%=j7j@bvv+V*JS-F zu#rKS;?Lahv+ls|a9Y}ZX2*Wv4)`O5bGP6+zBqm8FH$)!$8}Y6Uy-yDhoMPS8$CU} z8)hsiXEB0Y~eqzc{A5 zxL|hbU$x2q^s8yQ;F?rge;g-ZLBUDqeB}uZ|KXY?{#i7on``43BCGr)r?W zU9Qg@Nr}Fjp02Mdx2HEV#Eoi0WpR=AAvG<%$^2Z&>-v-W6kpAGm2Bb^(9{I(&}7-f zgm|-9AXbSRb|$8zEiV3Ho!*0{H6k=1V<@)Qt4sdHfz71Z8Ju#2prg5ykNJ;!^a_x! zV|uwvdFDUrks?mgcC)fEy6c8~S}Wk%-ZWvl2uHI=<rW zQ+1y?;h$-YL#Av-srC<2)O2*8T5v^K%^9^J7Czve^uz(r@y0&&>F46YXFAD;MTG*8 z^SX3pFFMl*=&4@#HVXWw^bUR1VnJu%@^&+CBRXBRzzAekzWm5Ps%U7*ahN?_(;y z#k9m5WxTs7J&Mqnx8uwD=Jgq^X_&!nSHqs?QJ0mI0qVm*Z%iwq#@xkC zbS(_DRdAcCx~mM>LQS*<3%d}z|84vs9tgAqratbX)Xcc9rmDmlsGvS-J`8zCxH2c% z|1h+t-On{T$Mf_Tm$TmFjDNgY1tbz%9OFf)NFTPdfF$^=Wcfd&; zHjnI{nn}9{s+Mx*EZ}DR;7#Q-oi|t{8sYE!1|{2`JZ09Y14ZI(v7>P6aq?dVMMe>< zX%AfYd-SA})YGkKD*p=BgVTKtwWtW_Oz6VECmeAxSDCm>yo2JkjUM11KjTZfv?}_7TjbJGGy}C|vz?sV$M8zX^c~00 zT-}2!yWrHGRU>?3!lYB+PLnu2)zQ^WcpNo{zfoolL(d9{1DB)2oUx7S;PW(C-@+ar z=FT%Qup)4m3CRcK(pKTU;exo%Drkw#*olMciZ#IpWzcMJu#(r^tOvM1Ptc3quMfzk zzS>QVGM|#2k8}&)(HrFSX1)`JX6cb0 zQ>E?KKfG@Cp|4(IC{^lQ9?dsnX137{Pv^gJuV+>b#$STsFt^-UQ@8#{-~n~ZgPv?T z6CkVnow&~RD7lIkALy&YCh9=&;>p@r6}fPNT_p$gx?j_AJ>idTeENGit1DHGDecuzXwNC}`ZU$Y5D4G&;GMyj zaL84?r5e!W#oVU5;bI!a8$e`tiBfsEaV*F9?2A^k&GA*NZ)N^*_s~d|e7>4^q;@L4 zt}wAidgq6|>xU>)`-UD19|=$42j4C-PsLxyZk$SKgVa13p=PSd3x3l@F~6%Un$iw@ zD@wnM@omH}@P4&LSe{OY15PrTvPvykLLMHhBk%4>ZdOANfzDmQ8+X4x(b0^4@+q85 zB)FY2{YHN7RZO^dr4;{2J^HY!?N&c^fEv3nJnT;=>T@$Hk?WfFg}(PB)y@XWil;bz zR#&y(Vp3(CXwVb_I84Xe#a)oC#eij|WQNPj@5@bxuWN#q=zRX*fOb=86peFHRn>Hg ziT3=QPE)EE(NV1s_bQ2Z8@cEe#Mw1=CQ{p1KJpZvqrG}i2Ut|RTH)y~SG_e=0rU(W zqx7Chu{qAo`OzEdi;eH?){k}59>t~}hxJ`iNzHasm+{Rw%PX-dFRb_U`j6Sga|bM@ zjmkWwLaG+{NfhttG`?!GVvUnnMjmT*UGrQXwi2UT-s(wClj(No9r|%wFMt;74f2!uTStPSrKSxa=x*;=R;3%sQM|pXgbTh!w6ID zBWU#=gmn&-yZcZG4G^6+ipNj6_l?~DRj{e%R57`6-fbbx-D$k;;e(LUm(--JddizP zO*8YiofpNSQ>C5JMO+@odtZh5alEgtygPVOHMBvd7-T{@U3dMKw_Q_=ZR`ncp+$MY zEL?t*9X|yha*96Y67r5Xw~48FNvR>j$0^hlK3?pYh8-O{*)uP z1|JH%h~+!tUhXk1+}8P@j<2~9k2GI2xLLndMBQG_cV4Q(dW~wo10C{{saN~d-7PpUPS>XJU!4Gf2SHPBl$axy21^G|Xuy2taI zcmbHvvxzseV#laEoI z-$Sq8o|5fz(YB`V__n;zmTzU2|Nj^C*L(O5pY)9X)rFP!Og!I{Vv}zDoQjkiZhKiOCx)%1?{$N1&=aX}#$%H5i?c7PT#oAn&f;d;@lv?KykOPxZ64HC0Q{NCOG}ArZfc@x-Sz4x2>YCgs2M+a(wo#)kfp#>6&^Ffd zmJ=~g!I7)zW%Aj4w$<-lX7=qjr?a#-(@!7t3-6Og^srw9Q$1TT^~!^|w5LODWrH5Xx!P~jGcEaGTpa6YQ9$ybt{GIY?t!il#7qt(a*N5DI#yFPN(9}v0!XqX> zm+HO0OQh+ii;IHKd8Q+s=1HRXd_FWGwZf-?8o^ai+EwCyWjDQ4II8dMtsmd4pFXWG zcuxH9;Y3}yuH3t3YGb?#_?WYr1J`@2&-7CKF4f;ZFqA8?^7Laf^s9fUEi+Z?7a_Mc zbeT{pXZoz4YW-c_V^c)$2l*7-7XA!lea@q;+ogr_ha-BEc+u&VBCv<8s*w*UDsvvN^d|J?~(mx zx=Ej!mwZKLxe?NJf}Sayyv@1YOab17qGy$uliJXj%M|=td{=Mv#TY7vioVP9CTLo_ zO&3EsIFUUM@9Bt7{MES)QaaZQ{(-}u=9W~@Lr-&)K9IL7#>esduWgEU3TON`Y`c1d zGN)ebZu9BQoUs8a*Y+yCVz8kLy!hX@J2bCI-7fs5h&?h~hJN-HRrVdcqoz6o?Nn8J z@yjXig*eUND6#Ii=djK#sfBrua{4SH_RM!?TI!Z6>V=z7bv*+U{#doL2e))aeY73g zRv)8w+CQ782ER$K9h2YZ(uWu38?}tu|60}sZi!2yubQQe#WyC#nxc9S{!^F#;KzjMu8jC7{T`tBe3In{Vb?7{|AkYkQ`3&rHnU&P}N+?{7Vr6W#aZhcuA z^})43cGG^d;VLUks!!l%zei6wl*;G}S!_sfPT)W?Q};8^%;z>+ON!AI{0589AA6EY zeVh%wpT@or%}c{pa_W`3xt;szvZE$b&O(1`P-dO8U&MZMcw>;_YoCnyvF>0zv?5n< zl`}ftJZJty`S_^Vj+D1QJ^l{+UCZ)j8x(s%4_u#u=1a=wbMW7*SehN-3TdtRWqpx$ zh~9sq9Z=u83!PyLQ@p2ru)4vJ%FU49xs+aa&|#m9my;FV_s0ermPOFLo~ru$z0Zeq ziCdh*Fd?E=PEF$ZJ5|nTU^=8YpWjmD)2InW(E(2 zitvvHrbSEg;QZNX?Z#0ju5(@Q_YMeF6Ti2qHLj}NHkw^(5?{@Y=(LH7?a`CbHu}!F zR8;{nI&S8FDhBqb3icAEdr0iR-Nah4U}1XQO{&@3)n!$apQl(ym1N4-=XIZKw&*v= zt@>8SQ^m7i6|AbdxB%}@xz079?fVKV{gmAs(@kUyO{>g@?BVbLQE9H}k)3W%src&@ zZ)0GocSN)EI0~7eD-WUW2cw@CU8|PM3ulVPzNJoVX6y1T=Ei;s^{1+71e+at-N&*( zT9SjpQoD^>coym5pLAQdnw(m!g8x1dNrd7xV@;#q<9_eVDqwfsk*rzx-bY01ZxY>A z2H%;4JWQX|Q-^XEp7Jt>zkQr;4!c>Euw5}a*S_kGaB_|=<&a70zNVXI(Gk@ZOa7+v zu4-c9Ik;t2*~lO!4B%E1g>yJ(*Ov3XS5;KPOVxz+9g#;Ku>G=!)AWn?IYT^}BKr0> zk5Ej+Uk#;bt-5+aO__{u^;}E4_v?di;EHcBBYZ23%ZoNR2I(=&(2hJBUI9n>Q2)MC z&N?jDmco41QAtH;iD#Nd>~7D__k8@m&&);n^HWx{=#6~II;bq?o0mLbfBT)VgI<)1 z>%+rnG8RXEiQJR^Z{%mYvG=C^&Fvr%%5;XZB`YSnm>;iW`_UCyxQA0&H2xkexgxdm zG!yjeRf17n#R^ex3_P{AI(s`@`5^7X3{^xEQScJire*Th2|gPg@F9y$F24x9SwLqw zBDqx*D2wk**zfnIZao$q1*iQhelRg#O_U6@q`N-g1e}2f913n!m8{3AEcX*iVkPdO zMGax5zva8NDb^G#l|g+lkp5sS_V@-l=~I3hflwo7^h>zRe!Tk{d4H>o4*4kC2f%4Q zG9UGFxNx{tXp_FD5WoD@YN-h{;!|V2c!Xx*IKRSrPmPwv&E2Bos3+c^kB)>drg-${ zbu0IANI4b)jfFSS2yRR(4&j{Pr)OY0MyYci)@7tNH_Zy>2+qX!l}%nAzhl3YUeQScma zi+Z`GQ+23Y;$O-9vrRiZhfOcQ0q7BvPCuKKJ(d_sN09PKe^y2Ms(Pf6>aQLhRx>^- zy}X|_*t(k`ZFgW6-qPEDz+I@n{p6)tX=8q|0-Sd-mGt|(H9t-kQ3GBKOcB4!z~TCZ zDytN0>p)kjq3Ur4SZfCVc=!iSu3f{Ca1BV`L^|8UDw&Dc*x6Rs0o(WLnmO;sSQJw!o}r~i=jWbukgq zF2AZNCX3jIu)pQ~o?v{XC~!aZbOFyzr0waGtd38quLv{fg)fTt*{ zhkho!MW=n-Q(dK2Yym%g7`nT`H@_6GAio#&o-dnOY@*Alt&a)>F2SomFfaSEzOtW= z^aJXFMXJ#yxbL<8*H{s@5VYgB#O*r4x74E#>UQ5#Q;pU41_LYo-QBR{+bKS?nQU4G z@y>t|z7Z^7POvyWdUbdM47sBY;A=fpIo#ueZr@<}v;`2R zF`>U8spHJwJts={QpJS%)cyn8J{rFpv+_BXrmLE*JG}Z7_IHD=Gh5~6nfj0j-Dx)U z-&e4bzCK}d9pO8oX(5Qh8d0_sx8#R)6;st`ui@1iITe-Mgql<@e{(Wz1hG9vpV~#P zIpdGQ*E{GAVnE9Gc7t!-Lywohf8We;x1%nol@6|YaxKL7CNs>1@Ln?$Uzy~YWHW6) zm{di5$3pS=XYNhsO?RB(PGl<+HU53r^1I~C>2ls2ZVCgi4s{a8uAlaFh;d$i+mE66^K6ee{w@wiU)R(;D2 zvQUbV-|l`D5vvk1LKEloNhrv8zWD^|k?HWVQS^~%p_86u33qU=v)#cI_mdE$ia50(rQuL|lr1KYe}mEF ziVrZ+_Mh8+C0gFj%r!cZqVX|m-qU!H!uW&{G*sJona&C}wFxVelK$?<%*f}IF7ML! z&E`ClJ6yut$S|k0qn~`lT=gXR<0WtKS#joG4u2=rP9OR6E1lv)b|ZbCXb-PnuU}j$ zJJe5})VVB_JHC@2M&Wngk-64;&a*ij-%df5BRokjHJFb?-oQGX!VLBAWsYq7MeCft zNn3i4g;1r#deFi!*0DS!3V3SY`YEG=b+}Me@o&G%dzmx04tB9#?n&RRg!%mER8u$N zxiWS7vqj-^y7^Q3fu*KD#_9!T%5^8o3h^=wMiE{C%%-W*2&PXQ;2r=pPz^X zC7=oe;2BFX0F!l7|In=eA_i`PP5i&}P&2ydDp1-YqCqh6gT7}j{OctM+h|T6PeH)? z@J?P4_<-NjI2xY-7UdI&&p}$k9o%SAC`$(I>>*0qe_=|+O%T-b@1#S*Uy;9W;i*_# z{M(6>>8g^r3$n3DmY)v^c|n}LA3hM{F|y04{u_cnMKq`_7tW?H7$#o*1*t2bX02}` zVSnOxO0JqBasa2hSUlar5vQ$Bkx)x)ajRBvqy7z()jdSgsQ$j$7V>YL*ea>1ZJgZGs_BW`7xH@opCzuSq*nO3m&{G> zhTkt%|L)VdG!${|I`_LlFAC zBGoF|*M*++DqKs%Tip$1o~+6}O4Yi1Wu9&P3K#OOYAX)? zW_s~CDDAKK<1J>!p1_+vFCvM)?)w*Ns2=Ky`Rbd7Fo+n1O;E?&!cPvUCy#~7hevag zeci^=Parg-xeQI48uHq7$8POfTlrnB-at19Y%7O72qtBlOJznFtR`J~Hs`MwTX!b+UfATXILq5BxaZ1vR ze4}z+0cXu)%g|;uPeTm)5*^lkFx&IV1_LMkqF3`<0evGEzby@2z zb;WN!%XA&%+i=NGZZ(C*LI7O*eBf!(F*kUG)Qmy_I)0h1*GIH}n8RJ4=UOQv^II@2BqkP#%Qs)UkC< zu-*uNIS$<$oF~s@*_ZGq?x&q+O;8Odz0vTuMTUc{CPNWhkLM8ZG3}&t0=Va5BYQo*QEOT#=_9(tM0^kvn5yj z^{W5e<=eM~ll@2C{S5}`DxB+Gc`UVwsG3RcF7|1KAx6b)?OE_x-=E})d5%aE3?#XH^fZ|!>j(K{W(YrW`F5*)Z%-* z>YQK;?!-%M3#~^>UOVkEZo!|K84p5d>zT?}geg6)CkpV>s^+P_X`WZIi#tEjBuwSxLg;la-PZx?((2gFVpyI$c$r^B$?l@bVn1~PrA}?AcW!E+ ze>~TJoy$B>_WR9dl<=<8gX7_^wbgMcKd@ywvQr$1>WK66#p(@k!%~3`d_nFM>+j=U zxRbhVXRNWRe6RYXgJ&>}{=S!rsHL1zT}M<|K3GYcG*|WiSKxTCQ|L*Wk#-y%u2PNX zj11xHb5mMcT7P}kRn9*nux}snp=gdtnQ!*-&)A=49@q09c*!*OSG4GpqFrHYV|13U zL5H%L1AI=mGtfyGC5rBMr^Z4&TR>J4T#nkpGhWfb6t>&_ZCR`~Pqn(<=VM|-E-Hgn z)H_*Dc8=s&_}ylzf;Q^G3Go|!`XukvKR6WZq@x_5!<`{I7KL{8f_I%2I~KZ;lbq&3 z9O<9F?i`dWImi6UXxM2v&Hlr2d_6&t?S@YHQ~NDss1jRKAk4={7s=z#T%Z&cqbW{ z4()jkesda!oSJVrr$cTT`dTITx3`zNq04>FI;z&fkiT>u-|M`u5>yqValtvw75pw+ zwMuQaPZWV2Pf+Kq3_hX4-ek6Yrpo{B&}E&>3cfd;VM-;On}g11Ia?^oC0|zgHsw6n zm*-<9MfSar$*fp^n(BdxQsP}jPwmomruAvADf4N4?{M$Gq^cXD&i)k=IvxWuN4L>Z z7iZ>M#nx85_%ojFMCVAHi&w#!chE^Zry4GfwYZ6T=|0G9SGnv@i26cP5Ku;UlUz*6A$$y|yX0yy)biViSi})@! zC;mbrzv$fDlyFPkUS6Hw7!}khi2rlp|Ao)M{3oOx=D$e(1v7LJCuc7Kud)K#`(0VEBYSxM9(%8Cq&%|hY_xqitPS~Uj9Yjg7 z>wp|vD|Af#K0z<=s?R<6dRD9s4c$p=Sz6t(Tn6d^H<+aV`iFk%1x)HR|7@ID`|4)x zzvk7FLb|XOxT~sWMC&_uJw@OuaI}}tucVsQ#!#*VQr_9^4{>=Iyy2CW`7FvSN^8NSR6aEXm#JTBjYO<%& zI*Bbyc(K22J~MCHcV^wznRzZ7tbm!>tSj3T??V4w7P>T+7X2xFNm)Be5JCd zpMyf|ntDH*yiygj{HFM~!YRM-2geOtWU2GtN9pvcCVbhE^!;j*X|Ur zGj$DD@x7B(6BXT?WN;5w;yX&B-f0EWUbB!(#njD@1!Xp=#Z=B1#gP)yaA9=}=Cy36z3Pfwff z8BPf-5k;!nFZUX3@+KZE8Ii5D^!KIxOZjl4Da6}Di|Fi*s5I_?Ouax!{F0gOGV#~! zT^M90;dj0aKW0_q^mTu1J}>6Qlz=Zl=r5^?-Vv!tauB^Qr z=#!b0UI(4(DQ_wir90_cCLo|Pqxn{F9)6x*L#eD zxc-0DasBA2R-69asEb^J`;P{zDX<@hu<>V7fc6@D@NOCr{0O`q-q$-@jdV;j(ygi5?>^S_kys|0XBPiu*W$ybZCqU8TKVPxJ0|>~@@5vah>#pV;=b zjDDB;rn9>qQm20Wq_R%9M-Vuq=;*{Y^(vi@N_nn?*!`61i8vWMTHqu+08DWzHH zD%FrjY$m#9$cRT0-E~r%aSDBP7rS))aZjxt9Q6#`^zT?_C;tvrUtC{x0IoAx z?NAL4^CP@_ph~2HPgE&w8V7>=%m|h?(>|9wz<3B%(NGB!E=9~ZE>^F1P*L0@2ECbh zB=I?Y?`pGc6Qk+Tv9`DF&K#dP&nAxYS#M`Gj{X|`h;QtDBKUl}EG`7<&<=J9wGV&6 z&GK5>bnXj<)AOa@lD?4}!5p*Rfp8JkJ!k-(WAok*pc!2R_3hU5)z`5V_}Q&<60W+!7jyMOk37e>{ZxzVu>T z=)aGwj{LAJJ)nMTBxCKvCHLmv z`Ut!^sdwywwV6O;x>B|CpW9#CQ=Da2Oj_`qjG3BlKMHSu0Pi)1*YEw%+A5qu55eUQ z>L|a&dOSx%I>!0$s`DzMbNGQ9$s!C)7hLzF`rDHBjBSN~dNCZbH!MJ_jqmxo8h zaR_}8y0%67pSx9=hWs-(zkEi`c>2pJPLFhNC zybtw7M|}R*AZE27Uj^v?uK4!*;U6EUE;@OiHTBdlKqb#YY(H?nZ-F<|^k$0F^KS8u zTRNF7DG+~<4@Nm{H|QO!_}rItQziL#1x#e0r$%Tji*%&VzR}MNc@|L&&_97kRD35? zPE~1J=TTwj3&&~wE1Jh?21m>u-V2|p8mtJRPi;HN8Q;ty`xaVfvpIIPHuhF#$6irk zw1^FlJude3#ni`nI{wMs$gLG+@1$=WX3t1Q*bSNKJZk!`BLf}wcJcrY2!1E- zWh?RB-D!T#s}^6>Bc0>5z41DO@;db78NF4M|LY@A%)j)-uZRN~`pd7>Sx4dfO?3Ag zIRmeRQ{MrLy@Lg~6nb6Ov}m)f^>;xF5v zXJJks$8JmvN9ZO-n?8G+lSeB!N_k!66Vy|!{JpXqL?%O0UQ+LD$AtarjOUNP8#`k1 z_))$NF$#h(wa-&r`9F((Vjkt**f(b9-}K#wKrZIPds1rXC(Pz#n8zteL9><)^_V+# zN&j3;EgYlbxLG&!py%-*6S@ zYPuc2?6Gps$wbcNbgag3RaZN){}v3-Y!yjCde%*5CN^Vb9~6xXs&vcaUNYg@t!%B@ zKtqtY?hu~KUFk>HH zp6hc?c~q+l6B9(@?J9}CRnW`bsU(f-fxDbpzRXZZUaA#qnd z(E?^N_NIJRDLXIlM*rTFa1Y+y2eJn1_EyFoa!b3~2mT_o=TT~@n7D0Nfl?585%Uij8GSc!k4UviFnF*+0n zT-iLpV5-~|x_}NiuuW=&eIn;Ou;yx1M|tq4WyJShu;wjlhCB7hL$OO+vFo|7qwM#4 z;&E@_$M7HK%^Rfc7mvrJeQug>0nDhA4ILe*uh!wAn#d7%VBXHizXQ#et`PYq>+$<@ z2w9$0JgaWj$;=isSzl*u&l+iRcsL#2d{Jt5;u$&jV{U6b)zyvDPNk)%Kau`qMwyH+ zGD>IMmQg5uV5F0i|6XXPtkT>p-UxlmZOQj#?9?`gDJt~8buLY)g}7?e1vSgaP97--FUZqk#Nq2Fd8hQb3)Z1$4 zE3sL0ITh6yd+Y&g=J{`f%9QlvcA4i{14a2R^uE2f--&D^sLwu*ROVqdJFQDvcP>F` zp@Ay=Qf}5yZqq4eypfrnZz);pnZf)5f?6Jikj~F#7kaE=mK z^Vg~U5@KF&72Mfi@6d~4>{3dqO_VTW%>Mi-hUE=@>>Fq2?X?8TF-9M@SVlN#2H|7( zzm0CRunB}T|Lg&rL2A#=y|j}y-(m~@z-|r0%%3)qQ3!7Ps!lEsUgsRd@sd4XIqe9| zDPmpWICRV$+V^z$*LcIv=M|b;L|q#!#(Chl%05Ejuo5~S!z14%+xHQf3W}{U8lm6m zioVi^y+a@K1jXO!#MjsF`Nt}`!nl`rao-WR)!o0{nEdK)a5kESr&LO3)i67t z=2O+OdoUmkf~_H0T}?VIptF5M#>|J)DC(0AhH~ekPUw>S(MF-n#4%lBTRroQ!QFvF z6jEbN^88^2;X4~*?y)W4cbeP5YVOG@(7T-hQrbIPPTPAE_)+Wuq4mpCSKN& zexl-eQOz=hTg)8pymU$snS)#GIL5YFe2dGFkz#wxS?fk7(v zhbZUjz)_MglZLwW#h&N}4Ejg9_F4Yyr*Eq!8r}$TD=*IMiBE&5SM}5mnO5oUY#&p} z6cV+o;R1(?JC`ZeYf(VepoGeSV_FitA#}4?HbE^s#P_}!m}D-hs82Cb*EcFuou+58 z`Ky&C*V`oX;U|vrDGlhFM(_fA$+Y?FCNJN#Z*45y?Q&b7AJ$Rc$aD5VOw9uK@CI44 zQ0Oa>IT75Uv-`p{MAYPEclxWVd|0}1hJJ@)xTy{EL-00v=sml@?7sJm-=M(!N(31M zHR_9h87!;5qQdM00b9pA<3Zi@Kk<&b={{zF`}?kUQM@!uxn;|CAIUVcJ%JzG>r$pX z-lfqvM_Uon)Bd8)c_MhiT-jk7p}aK8>vaa7n<~zS31~@Q(AXcrO7e8{w5NO2{<1Iq=W1HvFQfTPqz{%^ zi;IS7iEeV(9$nir6s`lIh$ZOSTe??8!}F;5i>To);VQCI7nSiQYvSo{GW|D>?s+qu z{XDkixnvBdzgDNTMUKu&yZ}Q=DSyi9l+UTGhu8yC*L@!Dj-SC+HG>@Q#nz?MJFJCv zM(~=weETXCf*t*o6LRqYDE@!E5h{wWWie5gP2YU)Hs1if=tHS;mOgfj?3!1FQxsG7 z@%4=>9|St{+CC^F4|JP5>3Di@b8hGfXF#u3a5iiWo$1QY;AT8QI=t+RNvxIBGWj_! zd@WPwv zC<3mB;=RVd`~n5ev(TdT@s)b0m-vJ(pwNC8ryftdqQcFN6TeYKe^lMF)xWt^e&5Zf z=`o$&RyR7Yse#&_Oy%HKc;HBV-I_pqo!?=8$Bk57%bn0Ay!1;ulT9#VHN1nnbz9Fl zTQ}<~TI1_~7f+W!{CP{%(Dh*CH|w8{+Z{*W>w#Y*hM+Hhj`Rj zwKA6`ETtLb_0&a<0$@u+#1)NIubJ@zEGubPT~PiO?q_5W}> z|4~ZCp$msa=Go5MG{5U-b3dsGvoosC!vy9sxRnjrci6vp}kpd%4}OABe_aHwcKAXiw~3i z-deujcIv+KqU&~fxVMa*gExF?qtSlqiznze_XnQ6&Xz`lZN=MS=C-@Zsgr2Vx5?jS zocF&I?d9x&P`QcNnEqaA6gnNGntT8cNVoZxgS@sYH%F&TP|_xXVwJPw=P1u9k;b}}8)QH6@K86-Y4 zKFhcKF>{wG#Qz=^u+gn&Z+=W^)dw96{+5aer7Sg^g34RU{NbT|*gj+bEmbb*j z87XFiR|j}sSA2{}npb>T24g5sCk2&|jihy*)I93Sz8hv{t<6CSL3|CqEifR1K!H3mW;) z^7S2h-x`J-KhslFll>GN_M5tx%>PtMzTd9pfBJ4*P!xe$- zZdz9vxCvLI%E3SJ8inyNbLlbr=|=nT-7F*5_l5Vg;~EjC$7&v)tiJjT!}_=9@Ce+e zU-Fz@_8-08C0YN**xYCbYVVuOdpsWPg_~^{J082=bKRvDx}RdLCy%NxMd4c1Tp_$@ zL~c**28xF6H8V2=nljG3V3F|ClwT9*A@2|a(x?Z1Q=2b#e_Q#!C-jMTC*M@tXE%d& z23l8`!gwq-%)i*_(sEq|Gv^_!_72_58BcqNj;^JX{;>Y=GEaa3e)>q=MI`i|Z=W7q z@4P>v!`^R)$NSV_Q`FZzVTM7P*bYAPKhDTT(QJ>om_p9XV7UHX4A&uN_Vq+k<-Ax0 zxkO!eqj>xs)aYpPH9CRGrmh;AUt31YzA?0qU*#?{{=023IPHJk1u<_6r+(VDtq-zRCl{xJ`Dl8e-KZ>^8&^0Fzc_OWzRDLY_S7deKGFyzzI_wOoKKE;V4 zD8>y`y?$Yi^SqN;3s(P|*w#Pv4b5`c?>mAWD6Ue>75v%k@L5r+x*e@4)lMu?PF+zS zcUK-qa(nWF#GU%sQqas+^4$;DcNZq*@)4@GQ*PBMC-g3>SnrJngqn z!)^4^mK%G>XN?{Xi{u>zJiP)fN@vFS!zKj)#e`W?Q3wAxu?|2YD{*6xTf#i-9<4o69 z(|L;cyJb|#7ZRN?vMU0;;4okEF4_w@eAJ9g3|cx2hf|P>J+)Q7D`%sIs;OoaV^83B zFN<;2oSUaS*?v0i3nKa;E-`;Q`E}!mVaUsE@9vpZG;4C^>zVguw#F%*$$ZLIhwa#e zVe#>hh02t;b8%H)g=*kWj&tI99=f)V)A23X%1TrUF=~Rx(>@Xj`Z%XKg9Gu@RbYxk zF@ZVYw~JIPl`+F#^UdpNA3+`Jh1X=>JGn`eb0a5WY<2{03w}tC@r3&Q1#_bb8NL@J z{YHrTQGXn!c|AjcwI0s#5ckKns@MLW?GLsRPj!<&hR)}gb*HNspBgkpj+WWRfL zY<;nH=gfPKg;W=&^mrAc-NlpnTZAr-?R{Ndd{DMbO*Vg{MzlePj^`uD<#~wh37VHE zlgUE*x5yH#bah>Hp17Q3K{h#tBD}ez!L{E{Cww?;& zj0ua0!9#HTYRS%t1)OeMK-OpQu7 zbDC1CJk3~MvjrQyr&q+6Hs010okBXczBk@&wa$KtDcFZOKn}aUN4KrY`J7H*g}H_U zv_!R_#qUwmye$HjzwXpoNnN^#f~$qx@ThJ$oLCoshiAlPDE~t+-~qhj%0RtGV*%R5 zipl%KaTovcjcH-)@(2i2MJ)Flw1VGJCY4DWg;V<@Er4Ik6}jZUS4&$gZWj#Y2|gSs zudWzOgIGQGyPww}djEC&_fyE?TCTvaUmP|&PzC;FKLwdxj;w372V%86PhC#0CGJy7go+}UHM)2hk`(_$aP)i2-) zD^Wr&q~Ur7b5jSR)dqVt!M>xJp&)bGU6ZMa@qDN`Qh)BC4SOn-+?qjq{Gchl~X z`|Zl;VD6)%8Lj(5ts(nArrcK&bD%0+49HJcQ9R9sbWrs3XaSnH9Q0a$h;>a(620f< z=Y!{Ij<-5Xs~K5HHR{UZu)+)Bo7r-X{_WV zT}ug^)t*EJUOLTf9b0Bo(l?=dO!hS8Ko_8zyI;+a&+JqwKBVuv>z#GLZB%nF=s+HK zcT3?{3#gAT>WTt5{NB31QZ!8|f2UX7z>9k6N}gu+&;h-5W0^Rvb3H0%ZBmV=bf^>L z%r-V9|AU)Y6K%@j@RzI&Ji@PJHOC~>73*L~6e(?K_?yeL4Jh0j%Ij6c?;>ek>D%)0 zje0PWa*k|H^Yypi^>FY-==TMv-$|Lb2|ZRNePIoJT2UD!pG@+C$akyX8}@y2ixAz_ zWna+@&U601p`UGzAyu2B(v zKufYXRt>)PJj|_!s;Y%5`ljn#LMgaagwp>a-}JB2vfDlPH?PBI(th&B{*R^mfY-Tx z{|A2Vvur~4%*>8&l)WQ+WoD1;y~-vh%qUGT+|d8v)C@W1lkY%=5Sa9xb( z5@mkN8+rxnvwKt0q6f!B$5x3Q8`~zfQtZ)~Nih#a{oE$V6ji557dairS~)_^9#*JL ztCP{^ZWv$C{Vl)5ZTHz<#IWV!^O6##$Sq!y|Aep$Kj{q&CZ~NKDxvqUdDQPwW1}z7 zB$kXxMW6JHyEc!>Vd}Yq=^HDnr;l1@8T@cb{b;`Pzs5c3_u}(YMf{d9T)y&lI0GJ` z3+C7?bl$(h~V)3d}_<^RYX+9jkjxs@T_J<6?`Yn3iHt ziU}zSr`SbD&^l&t^c2~}I=?+>B69z80NuE&jR+!awm*xWrfZ`8y^MUyys0p_2LR;PS= z7?lc_kdCJH0KNBU`9vO?gadl^nqd<@rNQ1NyIBQYf2I2MqiRJ?^@;~#MsasK2A%VH z@JHKKO`2i~cgc9GP+#tic!qxp?pyo9UA&ofi+Dd&JMv8A2s6W8c1OSzec7?m^UZTV8ac+i z%`48?BvENR_UBC;cM1Ju9n>y*t6T(}KtXE%d{FgJo;fx-AO1&WxB%`w6BKY$9=#Qo z9;c#EhyTsXFI~k+53tW`aq{1~(PxdhW~ubamr#*PP2b&3{pUt_V^UCedJQtGit!Ca z4b|I~&%F6N(UW5K#*~e%MnhO7_J!C3G1JV-crm(FRL{sh*tr$5r0n6ziKoJfM>DouhglJI_0-6-*p z=j$K&gYMQcv~#WLWsmUybFfg4tZ1F^xWo;5S?AJ-K89=7LpI;gN?wSI(b-+nT=y>V zAH*M`Mqlo(ty_tk!gHwF$I~UYjH(%ZC3;~@d`#2WKJG@FZc_XVzYmSA9Q)X8=cn8Y zc|kRykxH_;c1h#B)7gZA?(iC9o@dSYUNG7kH=?Gdof(wy4Yk@KnBocy+=6Bzw=6s( z&NDSm?*?V+WOcLodahyCy?*pFT5BkFCJn>7opci~~!6cv8C%#b~ zV7)rPS}LM`_GgKE0m?-kko}y(5L}|t`%SzHW_oTG@n+K|mI*(QDJ<0A-;|dw0zb4$ zRx}OACy&9QxA*>+#kvf#+&uM_>}5Gk&PGa?op{7iGN>(X2N=QwdO71ZG8<@ z@H9cRzLZpv+WiF+&ALX$%1$zyJw1&wv4xqtKbjhG1ONMj+TU_n$yKpE=$v>OD;Ov5 zdg#0#6#r+-nqTKrC*bb)%gI`^_N`9wautBm)@}k#!wT8`9SY&E{W=J3r_poX4I=(Q zd=G`|n4j^M)yN=%oy6L2!Uo+5eG%EqEbJ}jwr`3lAN!2kMIOYQg+*({T#8;6{erv8 zDn{*w)|O$3s++m+Hm`StMslnP`yZHN^6<$Wy4mbzx-5*lg!7tCPxTCh{y7a%Ro(OX zotAcToqZ8wcz~=r5r(_LsE*m@4cq~|FSclFhND!vm%zv)oqEEc~Rrpl&| z@_T3DN2j5s^?gZ=q@Fz}irM|z*XNY8<bc8q)&1;;)w}m50Vbcrn z1NZIlMOklx{)PG&)aCZ$4+_FPbezq7$B(k7yjapD)^r#KY=-W+wL0epB^9BRZx_BG zZme=c=h*mbFn*`F;W{L)!FV^~hlqO15^M5F&*D8Z>I%sO=QfVa$lDF0#$FJUI<{-< z=dn9te{@4cq1bO^`3s`cQe*#(TR2X|x0QWu5uHDV%4;SL2~;}qJLB5LWr(|}2O(=* zbJ({}d_}QwEFJVhr=qw1qdgexu&&{L5X@PhG^n7|6un!BB>CCbPAphY6EJdm^*g!) z3aXnf$10_At{YNn^_CI1zf6AK0;e0?3s_k<@+K&t2Q+b6*G#x=wu+U(f0x$l|}-;_6c2j$+lMY6bH*b#@n*>35|-E*$xz2=}zg z&H%A13b%hL>8$9r+e*KIhkGLHcDJ09`f@l>C$x9&FN9yjHl)Je|LJz5+1Sfl$r`9F z@Wcdv9|-AZGN183?Du77E<#4#k>=qd-fj#IDt+Sb zCQvpu-6oSeeOe?eFpFXio$p`aS#*3?RrRw#45x5scIcWqKJ0*iK}8offDSbiB{(mWzJBL}QXjCtOXvcb=;6 zCHnD*=v!31-P?BUZ3$qgL;5GPWfSKMC;|j#>dotxoizf}A z^n0@HNu9U{afjkRN!Xk?C%hVK*ubQ+7SQ4`wa7AN&P_DGd9n9dOEbSi@63BqjrC)! zrsnxo9Xw8cSXZ5QtH|6!1uLyJ_|8cv6h11xq=#TS%K3wgzl6Kup3v#Eq!au*=__aT zQ(mkP2Bm>$ojvk2zH~ecTg6GpCI|ReP5pauemKQOe)^weDyIAP`c-E(JsySBWqRQ-jD(P2}M(E;f z0!glNb9rCt%P!(yCkV0;MSA{(r{(>bOpR_toxYte{|&x;3T|p^QV%xYM2yh-vQ{_QNM6HHmeqr_EQ7o#E{2+$X;*{Qr%KUp3dHvJa z+_~xv=3Y>9h4D&jpo2-`QFH1u_v*0x)3W3sd(NF$l&2bvi;Y#UdfQpsBTgNpOV}n; z-6R5D*D*5$zgvqJDvgc2Yr1Xy$Vr&grc~>HQ|H!%E8da0mZ00o#P7#q(65__{5IzB zf;wJn7F-^ReU~b+JYO82)JJb_IzKlCvYVj0thmbbkGx4w-YKs>syMZ^)1uoHPaWfH zGP_6#IsZa~kd8-L6uuiCLN_=F16s(r3w&TDyy(BW^;W|C{pigaVR$NfqI7gwfi|}j z&BbMCvqN&FWeHu&YW0pJH|g}$js6`pdYRqbR#l2pD`-riXOaj%TRjwFYons>MGa6- zKI3MzL)6mG%Td~!!!zA1pS3>8QQ3N0bN6Z{eWQ~3EyeqO`A>Rx!_AKG7ypiqtOTmL zc?o|dRKj=Q#DH{CX=PA>sAJj6C(TPVV2Te=Y6L0&!rO z)7e!Gy9{M?L!Fj$J@p@SUr)3566)p;upmps;GwL3us`2~7gx*X=KJqr-fNslvz8(- zPOUSj(dQG}7do{WS^iY<`=HAFF6Uuv2Z6N?Fa=mv(`SsHn<%7fPa@Z2Ns|Kn0qR$?7lBEU2+H>Msju zZBF4;me$XAlu-GKfiAC`b`a!TAJC>|F&$|jruR>im*?B3O_YwEW&A_K`@-|A#21uV zBdJ}!6px2s{VVHrtIk?qHz)j5C_6vX*jHxV2|Z*e@8V^CQ>pLcSsKDwt#!%1;hEO* z(HnTxSN;1Dcy|aCIX3*K`8GjKA(%zo%C7Ih8y;5yo-S9;P0g?kzqD316DyXy=?wgW z?>XqSv{#Q_YuZAJq{I3!=7d}D1B0E@b87mX#eog*#c7)S>$0fdc)iuswV&J9U-^>E znEkSDWQ|kxSilohN!mtvF$C6|Dl5qVxmbSUi*^s)=hRijQ2{*Zd22c6;>bYVU;(+TZJe9mq$*dm>DeM|KVSX!ocgP%=$Yu zRjp0~YckfZ4;K?Nxaoen47U=_sXE2%5V`jjpX(;h=m);PHedXqnTS8~`x#6z-D)!E zPt@drKg!}%W~C9>O{x2k56Y<)+Sv)5YeoK!C}C#9AzWHsZ1y7@?R+@rzvac9yT5prxuS5-ea#ORpaT>>pFl1_IBFGd9FHo#XjQM zFX&5aK_ztERL?J9t&`LPz0Ej@<^v{)v5&%q?azKsIGTnqr+q){ye((pmGq-jQSJJf zexiyB%2KiM5!^b{q`wVTrU)d^#w3DLe&(X*8_IY7tkPV_YVT6pTkF>*y^i1VF*9k= zI)r=KgVHLdPpHrn^_P!V1I)`8_t(We8iINm#@@y*gB$XGGiCRAC?FyHDyE^T+Yk!D z;+OSKR21{8&{0+p_3P3(baWzH%X$J`;ZC)z8lm6p#vh{QX}G2)Rw=)}kuuKPK)Q+x z;cWa*c~y`&HJv?t%&*DkV6JHqMSX@3V2h*b{0DrZcJja@BH;|xqES}1r_61dllg~Q zZytJ;Oq3HlDWO^?ccE^kLs`V19}hQDA;07{gPvC8by+|inMnyqu!@s3K<~kE>}nJ7 z^mBIo5oJvd7T36!dTz@P}KdCA(m4yYRl>>WNB_$2_I4AVuW4AQx3fp6(MSxyRru>(h_# z3AnX6-wt*bUnJ&or#lT!*J)N1`eeoAG1yLjO7K=@&ea*h;N*`2~fiV z_EC-}Ov^8Cl#!&vZlsgxzYC#!BG!J-iXT%Y^k8GTA)hpqJpaimk0)+bd!I)u+*eI{ z5PjS4^fp6%XI^JIF{v@t!sjyd3H*2`QSSqmRzSy4679s_`rcQWNs!KsZkMPta+&Y& z0JrfQ2H&`*efdgz``5;qZOC#<)7TVr{uc2Qo)Pk<_gjEc*r^B3}mCn_fua&@7MbxzHzK8hG8!$_z8Q8e-z^xGMq^EOsvFN^O7Pc-FUszNh$ob~q3 zSUqvByiYb5;yY}|^FovF!HM~t+qta#V;p?`(C5(5Z#+hzu`Q5P3P;%r>+&_uC4)GX zLd09>=SyG%|A8LwQ`XnSdThgeHgjT*!=zz$e^pfa!D;%8`fxFA+ycLUBkMec(fEkh z+RYZ1*o&!hz)dRgJu#CDMcA^Cz$IGIeiXvT!#O=|Ke@;!PFZ?!??OZiiiUP76M4*Y zA1{Xwrp>?UX2%TF+dc3Whmu#ihR=~CD)!cUFhbpWEM?&*YJRaYxZ4SRtZg>$y@~oT zC%^a-yj6yB;}>c}~MaJx?vLs~ zq_qul`#rGmLXkQTn|s|F{?4m)GLgMZa)jHV`t`DCb{+pSz#eU7PY1-y_5Ak{nve1_ zr%zz|Jrswp$)vu33r@-nQbO$mD+b_3xLPfVNSv&k2|No|S80YlP5)}@r1zyE&z5x|3q!jsx$2v%<*9>!4 z4BC7K(#S#g5{YB|m1<(X?)>qIOH4XU=X>TuOU-%5(yE?^;p!cy9q>TYjsMqB(Sev=Np!!ora;X=-T8i--7 ze#c;%cYE2?_de-MPS#ItS<^=6eTMaD zu5Trs**aI~8;eq>-U*L@=X+A{6~rQa4#8}*mL1{N9nMp%dnfu?gB-q6W9W;ql`d@l z9l6T_xyonO;y1ZjNs;#Jh)BxPrLcFP8v8;VNEEdznlazk&u(MW9eJ__Vo@_0NiKM2 zC2wEJd5^-fMJ7hev3r_zkx%aR62$)&G~HJ|7kK)&+0Q+*vddr-r$#NJc_^hXWvH)g z(Q9yu<09{1(nraM^3&iJu+M{O@^2*EqZ1pYEA2it)jL+VS@J~UkKBBe=R;;YWziSqGXJoZ#Z-^KLwgzR#9KNitML2J zi}{@sx2tg7vGbYbNH0^CZVcy#x>7>^K_^gAub=>)u`i^S8_2@%V7znS!k%)AUn+N;-4eTJ z$knrlw!;GbRU9(O1V`|XtJ&&LDnHOU{bUi`?}~&dD!`nST`%#^_0<{TBih4zfxftn zQ`(*{><{nu!OvIa_fCrmE#QoGqSFnhu^!Czv~!S)^*_fOjEDcO>eGo5wFAz5860?m z2Yyq2vR00rR+Z{ypSl8V@(Q=5cCz;?+&QwtwA#~$wMqT#DY;%X(X8sQc9O$6DRad(}+%q8ZYwFYn|g_xSakhp*UT|U>wA%IHLT%mYN>O!36}W3TXJfPbe*X$D$z{rqHKKHoT=2&%bfgjG|ih; z(VsPaXrS)IaeQjmq){SV8}+}8?t}g%Zbn?TxGcUps>#>GCoVF1Eftow39hxIpZFLO zxr4FqDR%!GdetPiFU?CkPO1F^AMldCuSqQUw2b)`x$sFj)g<=b7n9xzKO8VJaWb_@ zY^xMDqmvqAN|7xS&aFo%w;F$vxgq7viTYFS_dij;CEmA@nTGXb92031udC72Au(M{ zvtJ>hoSyVjIrR;? zXU!k^iT$4Fr01Z-Ngv)S%NfXvzN*I6*VL5hx+WLGreDa;Psrbj(I@;)L4HDa`mYfU zv8@f@oSZ5?eX;!|bc%n^GOE)P%nMz`&^5(HZi5;AQIC2Eg=I{fs;4C_opfJM4PK#L;RS2lPmH)GyrE+?4`4v|94nYr?(k&_vvB1w7)X zx`Y!#J=BSB>P;H0|NbAfs@UkC;EDoKt5g);#%y*@Uen*%W(Tn~mwt=KBET7bZXJv> z0)tgb-J*hO+C>#4Gw<99l@xK4`sIp!jnblPD!g_=Fd-q#ozoJ7& zpo3Nr=@F4|6pca`zs9kc%`}&3a1U)nnh|)*F{)zkv(B$@%YR$nD)O4Sc(ngQ&x%sH z<&>Ywr1sl^Z(!)d?C%A++^=^1GnMyuozqu*)~7scP&M2}$dE1 z82)IjjP@9|W4qny$VYuE1G;Nc_A5?n9$8uki1|@sb9LVKyhsnZSao^qkNB)ha?eEA zsT!|2j0KFrc-2o{&3;td(^RN}mHdd;YvU|+hkM8B{#@*=e@j=hMg-|5Gx^Wd)su4V zXH~0@gsWq)CPkFPX7qRV8_V(bMQx%Z`vBs}D97(F%RUZcrSbZ=uv@EPrS6GaXz2b> zO?cM~)8$lj&zf(OLmpiohgKLzv7UAyT0Ae087qVX&OswEiPEJ2-#Ujc+=xe+4_kK- zw{q|TSNYn1Bl5^!`oV*<=qEbZ&x@*NL*;+@&8I#rF3uMZ_F*T><3&FZ>(=`Bv-MMAtoq6uDs;J^ z&gGcxA4S!T>Zd=-`!CAOvarrRKKUv9TwV-mI#_qFDpL!r&Svaz8LZbI;`<(%!$q0V zZ76hw%%QS=k^~w06DYl^+~FooMyxEq7|!Lq=>e5!9$N6D8KChQ{9ux^{|>wE&Y$#T zRaJO^8#El#*uv9vnV(R*^~7^8g&nH;*JC1RS>GAlF)`69c6C~VUyGda2W+9D>R}bx zeT>X^GPJi?&+BYx=X<)0>ejIm{5;bniN=ONUdOY7+fh`p1f!Yq>z*;NSjl zZJM&fa>H@nF(WV2QT^&;jAfeeaoN=%oKg?|I>;mk!v&e~9SbqW|2VPto!?L8c4a6V z4`9O9;6!d=pW7ufI=AtBDRldthkIu86r(Vw-@(5{`Rax0Oc6X>XPHm%=~vUy&5{9+ zv7)bFj*Iw;WI_9VX9GHtsdD~lPR_e(ei!)V-IPt`)Ka2g{y5VjcR58x;DZjTD9a&} zE6!yJwWP&z`t(jzEtss7-1fTn8D+nYi_iHmU8_A;U%kKOps^eHns%n6_eebG2K&Du z>PfWdJK}Rx0-cbP8uV-uOh~8_DXlfm1sP2&d7Us+aSVZz}pvu0`9P`Kv8^I zouxZ(@hoc(sxP_|)Z&ZryV)pY7K>W)~K-rTlKM z={PSX#_7C1OnrBoO1dXcm(AJ#K_~k2c5EEg!G6A}hSyArn9BNRM#e>sgnJ*EH+Ktz zJRS-@%kLM~zc|mjyeLxLjED-&6L+)VliE9(BlWOOb<^$vb;^x?d@0KaCf3t8<{CWI~&S3{WNE||UBc>diovOmLn!*x}}PVsV0 zlJBwTE!K4KDit8(9FTo(zt?Bob48&?IEv@p6_Y~kZ2@~PhF?A)-HH6c8Ll-&eZPi9^Mr6g!4aISs(tqB7YJQPQ z^=v4zY~*I1KV|3?{1Ro-QY*Y*aajE$pZaBt&~c1r_RtepX0w&6E_&`u=22f#8*0HW zy83*tsxj2`Uj==t6sk9WQiJSL!8zsg#ml=QllJo+Q^mo1CK{Hd99v4;HYeflguG_O z6ok}2qML6(;k^ul)R`W2omkRs^HJA-O_Pr;VDGQ+0vF(x$%*aRMQ{IgGO?n}`e{#l)CBU1{9!FhnMm`?wu=!9 zVVfCh&?}&WU}9Sl{LK*F_uFJ<_Z*M7!`CT(u@N2GX{%U-y@YXvBh@y3=IbNivJyPd zR&}N$;SwT$e>~hXC%k=n}BBzN|LS; zT4RBln|(1x#Ja%8yrU96)(X!Q5vRd(y`8?YV$eAV^esJ+?Nx9`VRU}xbBn?J13bl> zEU>BX37Nwa0};G#b*i(48fwDL^?*O`X6lpfXRS+tJPFcUq?WNor^uP8i_XAYdbI~? z37I0tK_6AkxVbFr+ygyk#3UT2jDDc6rM5Hmh6>6Adc<}jVOd(Gme|z4Oqlyjyc(c` zAOnv0Er{_H@0}8sDK4Hi(V6oO&hHGKEeUIJmTF-r{%4)tU*|L*gW9UoDQAIP2M6^j z@nMxo7*Es$67dL!*z)I)#a=NbP+j&GiRQxu>!J9C&QC2`@{7s(g4t>&U$Fn{Y_}4B zIz|7^VwuAL`9eCC*)Oru6IF&c;U|J96I3U@6ALHMbDvYq$VM+T(psm}&3{&f>@plu zkiw}KHgO&c+oBqG(U~g*>%FV{JCl0xj)=Ux{i@JbeCbYK(=bv+b(fz=uE#%sT>4-t z&xcYu2eGohnX>HTtRf;}sM^Oy)yMMC>7&HFv_38E%;frrnI|En)1_GbA3+r5ux z&8%TsvOROaMr@+mz{)Z|#1<$i(xBW5Q?E=I7$=GI+Y> ze9IKIYlKJ-6F(jr{-C4@u(U`SrMl)w)Pp5 zRh~&)&(CCWBlJ-!=o|6XXkkCYC0{dPC0;L3VH(QO=Ga|T!INdEvT~KaDS&O;7(TJ$eqa z9On4S&r&)=08Rb*jJTLhg(J9q>3ni!?3hSf1*=@d?RHaml$FrHRi9#z*ZW`60bc2b zUysGdAMuBSRA{q?-MC2s_6zTsPDOqR#nBY3drK9r{V>1{wcxGLM>p(cFpDgmcwI~F zYrd>&3vV%(HMN4-^1}16K0!M6Slj!48<7%wmWTFk7e2qDtaLkE(3+LT(L`OKGHC?$ z#_&3ysp_Rwt0{@MxbO8wd7ay`ym#zDFgYP3#p4dXX0R$)Ng9ASz9EDSdBy3zrpI}n z6)YpKd{gBrm|0vHe?AV*Ip!w@W4uSmfe*rCeR$DEGR9!~*DBT5v%b>M%`}eehLKz4 zPKd9a$q!_ET_gXcZ|qFn`=#vVSre2m!Bk&6*&|s~RTYnaM7d_vN?jATVY`m#cPg!C zC!%32<{Gw=Ge4Dv6Pwe8r-F=EbDcROAacjoBN$f7@C zifsO03d-qn%UVLIZd-*V2hi_R3 zKYiy@F88E?w(P2vDa7x$^C_l5hwoY4(U_D`yx&Y-=n!9BURL%6&EVZ+Pw<&c_>Psl zhQT~a`xzPjQ$*V#gZ@_r+Ei71uDZq=XLFWF+ztQNjbHl+@=C%}OcP;KQuPjWI?mvr zvUv64eAyTdzwU^Y^#8 zb*%*kF;&7}Si3jlhwD(96u%`tDZZ0jriT2!hdrIa?+v<{7e)=>9G?);!9DnkPU5zj|;kInO z8L#q-dq+yx$3B?8=_39_zPcUXQ9-^`)kz7Iaf6c;YZchRMVgvco^_EpdKmv*Nn|V~ zHyxp)`-)q;r^-?C$kYeexA_zqC-GlR=rbRvBrI1~sgCve4%d3h^S%>#K~#E$SG}Yz zHco~fB^IqvV{0eA{?2baYpTb19UAG)+sN>@nH|z0i!ZY=iS2h#- z^Wgt>sC&NRX24jr>ebFo0a}RH+5B0I{6IWKLE4=|c;`Z?cORYG6K*uN`gH-W6MuO_u5IVUaLOZBEmJB1>9D%NSdcWwel zKP}HlOQHP=HP1$P>`zSa&sJcVleJjBS3`~K7d;-oP$suAXR`nuZLHs;b!I-+kNH#l z@%VKn)WzTvFDJasL+w>jD8~lA4GoJ-h+Ej@ zZe$0Jw7ZD&mC4jMR9Pa_=TC_3E99>qK^yPjaxzeyR24V+QYU@ul80`-;Ib3&4mI^vW)~hWJl;KC;yunLVH^Mu#%sBOMQDi5Ad59Ra?AFFQa%6 zG1OTvqs}os*`E$k->P7`;%#$}4&njMC#%Qz!Zz*b1^#`G% zHy-L}qz;(M89NMVEy5r)(0%nKhG+|WIwof;hAFtE)2WQ?Vq5rmR&bJqSM$}1#lE0V zCNsOd;!JH9V>VDJt>OQB(dQoG^-`O7JlOu^)8U^TS3Sh6t6(xpLEo9c+rI@j?ZxX~ zdh1P-oyu##-<&#TkAaYA>8X}{$j6x`OsHzTlsD_ z)SZ9p$UE%~SHwqWU~|_a>X-(3DU{cWwZBt z*2d0n=n>rs{r;O!L7nOVm3vQCcvU2LE;*O4OU3rNjHNvf{jBWuDYFQk66toj+u=XG z+57NowcTzxB6271y;IFHsNJVXS|wjK0m^ML1x?Z|AOEyjy5YDY@w0TN&X#o@(fyNE zbc+@1$6DhbFou7z`74xp3Hspcs1a`nmDHy*0PdR1-wubq3i(9evx4rv>WHl^RQQ_c zT?jacPH^!Y`DUz)c>@M^oow;G^(ilFf06p{wz^(7X!HX;DW9sIoj2jLM|=}i_y{I#i&3^nBF-JW8%1n#D^kUzOE~E!{TFj?0ePe3GoQfV7og#XzX-aXr zvO38#KEd8?hwYETN8j>zope7Ynw_#Ou1nngC-ci@`|X?6!9Pn7XG@^60a6p9I|UCa!i5+ zYWes5*uq*+*#WuI$1*#4K5J+x{scPrUqqsBX%2@wZ{=twQ&^+4Sm0tJM+JJyEJ=wx z?{}12pTTH<$ouR2?hH7L3MLIsqn&s|R{hw`3E87FQ4chW+6?z^gRX8lwZW|%&Ah^7 zim?$+)Buy5mzZx98UI7v5jQYbbN6}?TJVyXiwd4CAN&=d=03c946FH_nqrYz#xx1; zwrXOg(Gc?^GDW|x7CfF#?@bK$Nmk!qk8*yj%db9FPjUXX*Xm7id{FK%ob7&`c$!ve zB>ug$Px^z*I zrxERN$n)eGgLG%!GvPdMbb8aQj_KBVTUTDWs3X)h4e%gqVSs;)80C21lS z@^<%}jaPk&BslPAZ09aK({v}Ss!SrnoYtU^>i_O^AH!Reu%ZuW61uUAW-wC7%}8sU zkv2SGNts4|zUeGRvmIT+G1%_~H;@L={tudurRLe+H#gyAVn6D+%@h>TVom`S&R-&P znds7%rT2*HW~Nbpecvtk=&L5)B+&xwmBqA3nu4KgLWTcPa;Ib4gz~Djm3;PgIuj=9 zNt>l=|GAFi-|#L?sWf}w(;g&kjF=uO8u_~kAHnRPdJyFc);Ylp-CZKVYif-N(92lb zMf%a*>t`0ZdU{$8d_;TZ&R;in{f=J8Sl?ph% z>MA@>d(QQ|a9OqPi>8jms)0;SyqVY{xtefZ$JhOc;r62eyN)x-_%pZVv^Qz%t_l8i z%((6vQ$MDlyQ11gez<$=<KcFRol>qh-{$$dGw-9c3&x^i^!=-`gGF?v83 zV_ABI_Sm~-Nz0t~X`VeMF;Q1%KYUJZk#7fvpo#97b=aya*ri{f(9O>LkI8Xl0p?^X z&)AjaXH=nDC*pUN1@!di-f$ZK{-X-YIK0RU>I7e6wf5nBzHt8L@M;HS|G{jj?K(l9 zhQc#M9TWqiBF8{`rQN-JQ-y1@{Cy)n@u3;!qvKof_IKiTnZ)yr+Yt}P#hdKEH~zJR zXQ)?Bgm>!vnGxzk>9HxQaCBEwlGmAPbHluon3xRi;mU1(%zyge`bFQM0AEX)Tq@L> zXWxoT8{meJ_w_wK?_|}{JN~*8wFQHa8F$y4LgD+wXdK@(9O{esl6zR1!Rm99sXU$+ zRp;qkc>*m3w^V&d=Me$(&c?Ep!HmqOmHrLBeM+6IB9GgXl4Wh^W~i)Ky3Ow7)!qKG znt#futuo7tnA=AzvodY(IvR?+vhwjJJ^iF=e$!LM(alwqNj^5wt*)w7R@2wYxqIw2 z5r3_lT+c^zcEX;~ZM9Z?G8Z*gdsV8TQL|O7hr&NMd4Rv6+zGU(nUaP%u~Xe>Rz>7$ zWzXkB68|Nph1`bnW%t9CdBb(db1mNT%4w-)#^GZ2sDU44!996_wREj1bg*<&-+rwA z7%iV@j9Y#a=QluqMln+$FTsFIa3mjyfd8_lwbZk->5+Pg#3|IN`&fxt6fa|GN=vHw z?4vNK%yv4FZSeyy;$Om-(oish^Q}+dxFz;g+Rah{O<)IFVU}E5Zs&1?0Y|Z(-qt0z9 z8qiU^=?ztjdkML%`wsQXx8=~U$+#Y3IG)zqy$8?OjDkD3+q@T^qzI-YGfpoL#=iyx zHJ#q)3fvrU?FV(bF1G*cLe+TTj_wAmW|G-T69y-+#thN>OdV?r*Q;C$4P z^}cBK(jU-Hlf;Rp`i0|OiMyoRx&4#cPdYw%@5#9*jaWb@vv1#mupT6?4-bOz_eBg2 zJ!gLXJQEduiq00(%?#m|rlqYmFWg;8Q3IW@eo)O8S$-|qf1n`Wjgbma%nbhQm`GI< zlD??_=emi8ZRPh@V3%1CMM3twfJI+{t)BLDy)Y`r)LU!Q-OZCty@3(AhexObslBD@ z5UQs(_z7<_0W@b$K*q35Q zx_ho!?4y`PG0`!T+>g{UD$qa8m+`%)YBhrfc9AT8gqz$;xPd8C{K~jtaV^bMc*jhI z8m65z$0D{@U9ac-CWX7=PI5pN8)=NI;R6qZq9`?A^i>8=dB7Uv3a!STo`jq8>rvSW zDgNryw}Zp-$b0sQpC#1x2hxu%$KdSp3b*mxMb(XK(axoz8-0^UI;6sWfyVbVcJGpG zyga_+0#@S$9C;U`885#}Yi3q6d-H*O=P3+UtbM7a`uLFy`g@w(E>xLs^YF!@Zkc>q zhsA&Fou9}#`r@Yp99@UMTFz5Hk|8!RdF?nY(E+@0J<5mY;GCB*=;dYcry=L6^cW-T z?|RI05j@Evny(9H)SSYX_H|lEgub($ZWS`osSlOcmtxf*$YehKTs}3B{c2qA$;ul; zPU%(C|1vFYO(?T0?6lfWC7SH=<{B-tn(wOOzKUb53`2~8pkirWPI;n(P}F!Hs+f6f zNAbz;iTmHVCGI7YzRSZYeWB_-rlqClMN+HB?{+Ux)#z4cJNFU?y1JvIQgpm2f-ktu zJauIE&@4zVLsF~on#8=OC%ptoJ)h8w-tV*%-`yP9e{}t}$A2zP?tA!CBr8d0IEgPj zCS%FU=RYSxCAhVDF5H^~-?yH38_vhAQ1{M>)p_4{rp6r&P&4^M=FyX{%gQUK;Du90 zw~5|ux>-8s@}Zj{%SA7yCaF$6wpo8z%j9@8+AOP)bcpBUzoS5ZKK^vvqPQAyW#ZDr zm5i$sw>mDf`S#sd!P&&m!ef$NltsQqE82@Lp{lvokD?lxly*D%*_aM7-D5hN_3@Wg z_=At>5;IJ%NC#C-)5QXR@$uCKUw`&Rk|~BsMNetIo2BVJFIgP z#A4nnL*yDfP()}4AKJj@4d(mbRn@o&BW|PS?k^gaqv9y!led(sA5e$uYlmKx<^B`O zg;9P@R@+e(sidCNy|U=`Z0s4mr@ye!wqEUhdDC=!$DqXhPUmr6ue+Jfv#>}t&7sMq zt{EXhUkW#%_kV=7{s@CujppZHdEN@W^?4wQdhSMiK%r1AayPd42bJ_M^&tJ`M0bPH z>e0w{c49}WAk37TeMXIbHJn>jcBfwBl{8oralVDseEU&2RpH@2f%}hRil4FL?wSZwvqBSJ`<{H(GlqvztEHruujD z;^5AQYVdmLRB8`WV-_^Y-MuklYE?JI4Aeca6o>dQc>+gEaqA3hQbXl?4Xz_gcnj}$ z9dFP9y8fU0vIb(3%F=@ski&$m*L-==Wwo(5w!WWY>odPLV`Cr4VKef2*Ys`fw8M*G zi1%r_wqbXw@S6`KnoynRwHtA!ZH|xngl2A-9l5QN@G4H{l+!=M&QB3F+dzhUDcg5A zH6xw;^ofHKw#2^_f5JSYK5=d1`o+B=`o+c7Vg&)-%i?r4qiOlVa=WYNl`fGc29SY8*YEi>^tR><{QuMti+Fhc0Dxiwil2-O3*sGeZ z&g!!6_jDemFxz+%CjGEIkLGFHt4N>M-?|c)IZZqoD8s8ohqxARv^;92dAmhSG@8r0 z!uZ7Q>I;9Uel?}T`~-_}1K*q`JVCr$ulu09do2s;o#^A{u{1LM19ZqOljn%+Q@xI* zIeC>@EtqpK1PfUlZrVVh6x8Dv;ic1y)EywyujK}v|tuuVTdA4$ zR?!{=)xQVl{AcAlV#~*}sCsbjVt8q$c-)P0wl}rVc5FineJ8~z2{OvVuDNM<6N{fE z=FM_4_p|+1^k_CwJ$O6mfA+P!JSxe=uwGubH6FDB-nqoC7I!B?i3uy(`CZ! z))4nbsIfhQ%=XCb_VID|^q}O0-b+F58U3Hc@LfFmZKpp_rOc(#Yfhu_D54%tyb&K2 z+`f`Q1gVQ9Sf}1Im>20yMKi{Xip}9v>Y;712rD7vEUG6f@x^8Mw|TOLn`RN^HG|@+ zNKzYr+}~+@o1$So{Y6E!yE42|M}A}uo@61tx{5R^P@SJ)?H_vtEhU zDVcMr34gA)sT2lp3H9{j$O%lQ2ZrKVc(;VAQYF}@9*of!0{#=?pTxHn6O(>}s=GSDK|OOTo!wFR<-B^qzjnB~ zo&8WnX}fyBN~fc-uls!Q_Yuc+DkZ{u{rLApQ^&ffkaWQ_#?$Clw1(BNC5^4%`zpux zaTe2@_a=#z;gLu+ylwV;I0V&B#OJWyv(re7RcgOm^H>%#Wp8 zI*{xoSM!v6V4w&ZgbK3RhA>khi0MyXd$0)G;LB|om7C^#&Lr zU0#;ZR2B*w%cDNzq0-1CC%`>7MUsAMGm!~<%}d;%AEl%@7muhEex{1}oIQ-zkCc;r z_s2p`=g-&bzKYS! z>-^`H9W8YxYvAMZi67Z9*#AYoD93+O#Cr|)O%?T@XX~l9aa#SV3Dm!bCb6Y;*yRcC zvZ7~H`$jtN(QF|F&M@d6Xv62Hlc^1*2>L`uyo)mVAin2!2q+gmcnT){37)^Ih@ z>r9uQGPFv)yf}9JCp)=9^jpmC-+%v3zWZk4kBd{HMq zvysPdW@ppG9Q$aoYQUvkdDAL##t_BuMU2!Hk@=jxS_x%Vq@A6inlU5k3J&*Y3dEa~ z-7R6f2psCa>af}I=hbKkYU}i^rGhvH+n1LDx+;b0diTMU=hdgmEe25gop87G$*7;q zj#>rD4b$=Qyh*&b@Rl>x#G>4`)7Tw!@AJpOJe`Q7uORC2{OoKj@d3Qi<8UUOpxZ^T z@@n8eyDwt7ciPUo<%Pvp(hj1uOv2v+Cn1I4cWvQwVCBA#F?#gk;u~2*k_#gk*qEU1^IUIzo*Jf zynTH&DY)zCRm^vwwp=K3Zp20A;%OJM{gjyUW}bf#)YcicEk}i09QGcnI+s>;>CcE( z7>j>HPs9E-Ff%Ds;3iU1r!X&KofF#=7f_6z;U28N0!Q^SmMRl&C19l6h)1WBTKle0 zs0c=NvbaP_X?5QARr|112UcORLs%v$U#AJ$k&4d0)ezPS{rA#7@GF|{x~>TJ)u9wG{E;5osUS5BTD zl9`(46)KzAV&EdSK1`leKu)_Ae>MS6Gc#Eic?PGR4hl(aHCftj(+YR``WJ@p z;d7_+iT-h8$0_=OzG7q$o9f%^uxM})t9ID`iN_6;7uQ>};Trz$1D|pZUtWzGE+vj2 zK^%An&Uj5ddl_uGR_6PG3U_^{@h|6b8tmDIa-n!q*t!NcU*`!Ql#6Cet|8N0~&MEqmw+n~ZF z?OD-aHqc9E)eOSw4JFJ}tsm_?1-;G7b z(5{Hzw2kt?J-ElCVi#+kp*c{I{p7v<7+~Ik+{yV$;$kegB zqWnSKW>cYqcJi17xSVWq#dlf4170TJCC;fxbQ6J};CjBngNHAJ=)GO8b}b1BPSh!GiCNM<;qrgwWw?A*o^Zm|lZpo{!=_zds5-S3f}`zukXugG{5BmFIu zSj|lGWuEaK{Yn>kZ7|;^NoAv;J!&M+XdQkHo?gSE(x_cc_1Z<{sed}@A7f=(L+d9* z)sw2Uy)lN@<-LoNXUTPwN%a-uN16Os$r<<({(0zR7fAXLhWbzZ{zwmgaDV$kn#sQs zU*KDodCq^r#rdygB4k=t(*h!X116j1Bn;-`@i z4vfc9p0}qR35l)KOoj=oJ#4_x?x6t8s`kA?23bKT=?C(SrF_*~zGXA?p4a`{TRit0 zP||ad+jly|w2Z@Qqt;XYJ`BU#EPfvM>fPMCjT`YwkwQT{~hi);5!4W z+QFyHwvNrzwoZt8`RLDT$WntD3@M@Ll(Zo^VW0D`#Y@)q4LI~O821jJ*F>%7FrMo> zIe83qY$v@4%cvxN@O=Bl+T(hVE^%IF|da>?nb^T|s#; zQ_Of9UJCBO?8rvT%Es^VQM>Tat9a0_=}cn%pOsjZ&U`>#RftvinEjB?XDsOh=<`jU zdN9SuP=8M6&w4r==X`fm#4yojo(})acK^C_ddlfc!px5povw*0Wv%pZtG=0+xsTn- z;bd3gr-q2JBW1t^sKSCArU<5HwVxUVF?{B;6@qd1;=_U-&f>gbm|F0d{oZExPsj#y z`-Cf<)Y58hZ_3vX!=+Df7JFdkSLiKMi2%3tub$-B-brR^zJV0(ik8LX>yyOjs=Br2 zSmhp6fA#F-Ej)8OesBhVF`NSZ#boVKPU_>kIJ#3-^Q5!$vmIY9XRS$(vddl!cYfaF z4Ss>QO7I}BtJ-e$y=Q&zIT=+9I~*f(FCfD?MvXKWrv6Kv<20>ID}2{NSpO7FQ4aS3 zjuw$qCk3~R#X(Kk_@e);#VlU1l3A~Xd7CPxAB=DouCn_c@`-O`Qir_Db{0LK7g$OM za#@E>6<+^iGr_Ovfjs8Fx>1#!;)RPPeG40=r$M`rm_e_~8u4=y1RYEZPH)xvsv^z7 zqpV}Azq+I84i))1m~|#jBs;a(NRep+%Za7rSk6PQREv5jqiW){XYlb~dV=Y8ybLdU zP{upb>I`%e&S2Ij;rwzyLSwA%d+fd?pPWi((_i-Wq}@HLjUQbx%27 z10Ha^YD_wa<2r9_ndrn<@*^(9g`Pjh$&zsj&iykyX zJFH|y`A}S9HjG#y9=jeNIuO6~zADl}cyl{MT?P-h*(ojU%xt11FAg(QhfAXE(G(|c zzh@|gi@Jx&ij#*FhOe7L*gZs#&Q5Uy=c2ZM8wIN!<3IifZ7x@*Pc6G?27&aJ#kKRQ z<>b$)`P(Nlp=eK>lG^P)+xuQm>~m`99eI-v?05*WKS|@3iwB&g4j!`hy*%LxR=>uY zee2JIcKkO8`=s}~!-i9`maD#JiFK^2!|x;(Z8QrVtfy&DI6Wk@+)o7jNx<_o@s6FK zotg0YqliXq=~Sp7PU!^%ebFM~?E!b7b*<4ba`tT5+pJKaj= zcg-wCfBXOcy2leM| z>I2>R=2!XYp-$CjEcl95x+rs5ZLhk}72Jg_r-|DmMeSe`ZV_vGIHG$pW6}|xzMqI)hJEIdYvjfr7xiZwSZl@q zx3|vl*BS5B8jjuxwO!OTu^3)W@~PV=CD6^zktYt~-M@oGn#%NUQH?Z&2Rra_Rm99} zK3OcUnwN#t<{$gPzN@{~uVU807N~?x%XXQZ_nlp6MvwR4_7IudAh$EYLxNQEu)@jWKD-{iu#Fr$Bd0VY} zKT-ZUh(4dQnwPJNb)pXW^yB%bf^gq@@xCQns=*;MapWs7qK^Yu{N& zj#2NR86acG%nFwJe}OjX9-I7)zrN4+w-G^)v8w`@r}ETm)t#Sd zklYQqWCb{6g?O5dKJ=-`i#h_jI`iA~ycHFv4=3Z_JyzrooK!HU_j|rP=ub@$r~C2# zk#uicDMo_4Czm?gyLxtS@CLa>p{!1Fddx_949P-28=Svyy=zKm?iqE^OZKss9gcVQ z_lQhE2VaC$Y7I3kW`$d1rdx3UORZ{0v9_ZB*B2`goYEk=_Tjf)=2y?~WYwvXW{7f6 zM4%n+h&j-zrPQ3T0>aYW?nxZbUKd*c<3E-L5&$ywodUOlYO?vGOAas z+DD!#hqL*<(|6Fm@3LmY=u$&s!&`#%@uPc4$Go=vcz2W4v0?qCxH1zX)JAr>hlLeptIK%SU?yc#h+?w89mmcGnR#m`xS#yIHT+P{%01`ZyYhXz zobnnF=}H{k8QkPanf4?*9}`-wOTCd5&O}S_Gi34#tiKXxd`U&F4h_&n{o_YfzOwn- z8ah;RdpVuki0aQX`DtpEz4*2bJn)Olu=?%RyorxS>Z}JVWiJ9)~?Q_ zi`}Fec^cpFv(qsH#(mj&E#T)1VL;m2^R>=P8i=ox^Dx56$V)>$h^ApRd$`0~mxE3A zQQxel4g6LHwbS3e_4iLOU_ZeDrQyxplpxd1YVPM^YXR z-yg)tUH*HCbI^ppKkHn#cedhm_MOK_7JyVfhagK@r^(i9t)JNB=fCi?YsG;-^yim{ z`KRleUm}m`>u-^vZ&+TOy{Tv4*FhD{SWeO8-spm|wmh_dPpQ|RHVb8yeD$b*>B2*n z;{Wf%nG1NP_~b78o4nK#C#j(hgdmH1)}Gd}${pC(X#IJ!#H089KGr%;!0vy@&b}2H z9)#*fxF7xcIq$ftOe zzY=aG^wi<CUu>@sn>b%Lj2HlogT|&wIe)56}@j| z`KP8n^;~E_R=nuJq*QQ|297P_s*1MgAx~Gs>|_Hxmo`Ud{a{lMPZ!Mg2-L&j@zW_ zH!E@&OOt`HlUB(xkw{ne=vL{`~5$y2mitII*K)u52?Q#OATeD~J z8W+9CAu%&XzwTmt5qQFdDtRmYK7t)YLl)zCf)dU_T3JS{-_wcjX&{%oDp$wY>`uQ< zg;V&R!LU#;+x3w8Sua`oT&K04EGRA2$5W?3Rp#ryxoE!jKl1DSty?U&)Ebc*fc$g9Q0{HQ9O>N}`#% z#Nyc0PD;Xa)DD%c>w3LH*^;xXaiZ>W2<9qJ(+;{yFi~oEL^(M|glQb#=p4xzRVeDI zsB3CqgRvoh`V?(2cK<=4pXn8;4bQF@v$7;zQ)AdFBl-ykl39FQhGPsk^$z^g zAD0jPZs)qPgRN>lW&O+^dGZj^v#mH(o8I-2h`AV3{i+IBOLH>j;_kEaU;Dx}tw{u1 zoNPDuJ7w_^71dsriX}m(?wiiR9bYj~zenz-F05;M=4ct=0IIqfb~Y_PcnoWE5)R77 z+f=ppnRE{a|MnfO=J#-F=l^vUo`S{Cww4!p?h9DOZ|r}2=lcO`Tni(=DUWQ;Uh;}n zM`dm8M1$Z~nz7DoZ{GAEJ@)qS`|`Xk{KzWmfaZF4=cyhJkp~x1v-(@?p9`-AsBbYJ z7{L;%vDVp;LkYF{MAgVls`Qg&*Ts@K*;=0IJJI)nuIt-)@1Om;inp!~3#`Yd6vnT1 zQWYzSvzo6av>q1v#K~DB7%gpo@AG+5 zf69jUovL0Y@SUXbJjb$@W9T}mwpG=WQWbk!k*|4LHot-Aa$A+?orY?6k`<|fxq05Y zHpXGSlU(2bFB!SqmCs#8(7;nKG9t4LqisG5N?hMFQ)d}g_$p+LKm&GYZ&cl zTddk(9Ya%T4Ti$z#m%4DhM#-R-hN1Z8sQ$;ZWKQY^^Gj0ck5~r+0QCqw|Ud6klS4x zL1jGU0eN2^>v}zL3uZcV;_8HVbx1d$_nK%zO>J?zxBOy#Qa$|{y{R>7nEtUP@`hO_ zm-XSSHr=eM9a|5tJu!C1G;W9odanx+6NxH50D6$eo*AowVjPOuOpg3{0di+L5?ajP6ZgH70Qpdwq^| z-4gdE@?B4$iGDoUMo)S#A~B+Z>V6lPr4WE zJ@$oriV~$^(NB1dU@F29z1LUdI~hDjm1MQVbf0-KTs9TvEyp$&iI|sp%z!_O!xfz1 zeYe4t^PSY7I$z4oYejL0BgLVj;ztp--k$n;T4Zteg0810+(Gft({)eZu!n8*RGZjg z(9gZwK3w24v$?5lkU8d;%-$Op|8ji)_;&7xy%hh2-s_;|I}NWK#HMe}WvK`q9mm$q zrYe{mSz6yjhUk}cD0~vV&CQl|qYtyNn^aa0oVDw)eNwUqd>RM(mwa-a=YAdgS5yCQ zI<}mku3be8*dVKXmOmXO_ZVyS%0Mt1*;wI` zl~jIeQoYuO{Ccyjo$ylS@JV{94)hr}%nqC3j-;ND>S$Q?C*49r)x8FaqqAB47b-TB zo%oD)sC%+^E=`qm3vc&_uLpAA2xoQ=#-;+x`iP~M!Sfu4Lnf;NOu)n(;5}07G|rC& z%45}UiazgRY$~d5r?;B7V3dQ9##g#friw!~)JLx9fSoLA*2bC-6GwBg)C9cwdAC9? zgM72;`K;|!H-SD!^FYDO>0tU+D+p?_ex!|Vap=eA?Sd@t$bz4jkF0b~o~NKWYo}++ z#ha_8CE(;|Kou?7>_UF8F2<(l|6}Ps;C(LN|AC)@Y}uRaB2poHW`<;x8Hx0bjLfW* zy-8$cG>nWyW*JddG7^f&UfD#Pb3W()ywC6d_&knz&gb)f-}iOBuGjUtuKRjZR*GZH zPpIy0v8E@n@h^1HRFY4xr_0t=+k6BWmb$}NbtME$jD&9-z_LqHjQMtr0Ic(0v0%Cg( z3@w8w{|~KeGd?y6=DjI5YwL3-U;{U(F}*3Ymmokk*Oo?oE6i5dO`qEjy;6C<*6?c_ zHqeh6UJj2xEB6?Pk>$lG9#J7O(-XU}-1{oYbL3(<_YwF|eO=Y}g`oR3i(=7H|Q9q6T>v~k0au~yD zs~B?ldx_mK)-*DM&8C1g#4U#CS+DIm8pp-R{yz6tOyvQ%(C=nkf)l6%$ zgE^`^3w1mVrI+@h%TJR@U7;owWi2;kM7^!(L0ZL4N?Jy~+0E*tv-hz&uQKA*sr;JK zuesIO8>uC%qs`WE)dy868}NtEXhM52{*7|I*JZt@SndDh!4=qA=#0FM&2)8d8+_+R ztZk_5t%SPcO?ACreP@iDs5&7IbIb>>{T2AcjOu3my8`(-@m<<_9 z(@BR}hduI=&_g{-_W2+Z&W+qmYytNUQFtEV6z5dq7W?itI(l-bZ{3%f|AJ!;6!X*K zV}tZq-cT)^O(pB+YXW8Ozk~t$UNfkIhuO|6SY|k@|GiicC2wCNqhA(XLl;|0eNA)p z4*zMU@prM!ok%s`9>f2}e8%YL&GN~L*vcHJwV#JQ=F~(dJ@jGPNk6ZFPJA7TFNCvg z;_uHwp>9^UA3GUHBl*Go)RpO_r4`)Jv;T!in-+GL6=l-ttysX@itAR}k2!_s(hT+L z4RgCw51V@OT`|lny^4DrmKF^^%d?_h`b37&kLt_BSF8I!;_0CRnF12TCsf8NK9_}- zl8<~Xr@0_=zK&CzrSF`jU}s|6ooUxAFuki$Pf;csyMmW+h=_lkWREKNo=)ON)Gu$#)L3)yYvA+2%x%^&FL?x^BGh^f_dR z%@X^J{-NTrmGuptq}HU039}dK(%O64nH&0gzoYJq6BBBwcpeqEdy3%YJR>K+lVCM; z)vTv5zGn6BL9zE(FKD*H)da6vgFd>-w^&@;LoE z4}GaM>)YT?ALmD9_Z{tb9D?Ty#&l*gMWnWQpmUR6A%C zeVz{R6_4%iJ_{w)k$U0$e?gGy@bY^;(MFEfP-Of#A(x)l&hF%6@qZ=%-zQGo!BcC( zn#+mr>+%j+${pw!xLU~i`dFdul>gjO>%@X8Ua90iX<$ML{QVXGHkc=_U|AP2i04#h zCc=hmQR%UsGVm&=pWg^Co|NN%ru(&;oHRV~t^<4iRo0Nho)u=#_t@Jfkg+7>x?@!@ z(2wI?U0DjrNc(*RZl>1BoJ+TLR`V}2Lb;r9=6R^{g8cAbHr|&u`m64YTs&bmedm3M zz1dSHx>7dUP<882yOTMM@sS$L5t;6jx_XCFB@XCg&#SAkZfsO+Y11c;s3#YUnXay2 zT9T`Nnu;`S`ge8piO{Au+Zn;j2T@SV!qC%d z*`uj!rBs|V>1e1dCJ)D!j?iP%$VhKn{ZCa+ir{>oL%cLFYObic&Pm34xWKJQd&l(=Zx`d)~>08gF-g_g)Zj>to=%5kpKiSJO${)NByM4nh(vL$87L5rLs ztDkIYS4}zdN;`H})=`m)xfZJ!ET#;^r^d*W-^cmOsT`ekF5|1D$^Mh{(VnsbUDOj} z+4Evit_o#t5!LK>9QblnMXaWsEMpjpo2e^ls@mVH^p3x=>GJM;4F8W+ncczG_tH#a z`BO8M+_~0ri~7?RsP>y_XVJ9!`l9C6s23siZagtf%pG6no-;W znS_h|Ok2yRGF#7VsKU{4bl1tO=U+N)CtjbQ(p5@Lw4q93GYHVYwHKqBKSrr9E(`rk zys3+;rlNW*k_DZ|Z-w1!2qtpo34EFIu`b&%q+ikHKbG$epz0NgZGh>&W#Uey*xa65b42#rP1jrS#um|+T;+W~e*|+{&Mvoj zW=C!l7}lF@G}BXZw#D7&^Y1u~_@|XE>8T_i>M6M=BFvS$%}@(G9Mv`YG2G%oOj|S3 z_nGW>UH|-!*j{q$?6T{%RHa(<`7c!j&SR{FqLy3BB09=9ih{$v#$mxfMxyw7NB29$ zJ%riCsS|&e#Z@*>?jijk1B`Der~6T5y)^XeZw1ELj~Tvy9j0@U{ZzrC*3n}!!Lxc^ zJ>^q9{a)3cKgLe4;Z`?Az=$57cz@dpg9_8qDnOcYw5iuk8NKYZ`ETYy{1^WMe_U_g zNLk!+g$%q7E#!etl468{%#{=liL1A8ny`N_`Mv>e4?n>*v|{Y;h_F z?zuCntXkhglM_Eu`Pk-Z&)+$p(hi=U5c%7vC#9r;1%Hpou5ROyYhgw^zLZh+bqP7sz9o6Ro}C&YEI&Yr`Rom zrU~NISGdLJNs3x~%25uhdQKiO8XGwZ$xcDLV{q)MUCgSI9G<#XhBk9auF`@wvI;*> zOUdsi*UL{&$RY-mpn^7*srIGOO@dNWWTKzqg(oQ8{qd%H_N+KQ8WYosg7}5nMlV&P z3$mdn)hN1RfoE`^A^iI>6DdX~)$~(%-$+7EwX+`HEd$PTN>z3hY;8^3+fAQ7N0FKj zlS`<{|4gkNX;pj5$0k_)Z(+_{_g+TDda4L>nHtqiW#cKhp3a)4##b`BgKSi$5)k)I z*t-fB2u}(4f(O4MqQ+rvAJ7-(%TTLCPjN!;4u;fA?0

    942hnQeXd_w3}~ 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 background.AccentColour; - set => background.AccentColour = value; + get => accentColour; + set + { + accentColour = value; + if (background.Drawable is IHasAccentColour accent) + accent.AccentColour = value; + } } - private readonly SpinnerBackground background; + private readonly SkinnableDrawable background; private const float idle_alpha = 0.2f; private const float tracking_alpha = 0.4f; @@ -37,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Children = new Drawable[] { - background = new SpinnerBackground { Alpha = idle_alpha }, + background = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerDisc), _ => new SpinnerBackground { Alpha = idle_alpha }), }; } @@ -54,7 +62,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces tracking = value; - background.FadeTo(tracking ? tracking_alpha : idle_alpha, 100); + // todo: new default only + background.Drawable.FadeTo(tracking ? tracking_alpha : idle_alpha, 100); } } @@ -121,11 +130,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (Complete && updateCompleteTick()) { - background.FinishTransforms(false, nameof(Alpha)); - background - .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo) - .Then() - .FadeTo(tracking_alpha, 250, Easing.OutQuint); + // todo: new default only + background.Drawable.FinishTransforms(false, nameof(Alpha)); + background.Drawable + .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo) + .Then() + .FadeTo(tracking_alpha, 250, Easing.OutQuint); } Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index b2cdc8ccbf..e25b4a5efc 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -17,5 +17,6 @@ namespace osu.Game.Rulesets.Osu SliderFollowCircle, SliderBall, SliderBody, + SpinnerDisc } } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 95ef2d58b1..c5b5598be7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Game.Skinning; using osuTK; @@ -102,6 +103,14 @@ namespace osu.Game.Rulesets.Osu.Skinning Scale = new Vector2(0.8f), Spacing = new Vector2(-overlap, 0) }; + + case OsuSkinComponents.SpinnerDisc: + if (Source.GetTexture("spinner-background") != null) + return new Sprite { Texture = Source.GetTexture("spinner-circle") }; + else if (Source.GetTexture("spinner-top") != null) + return new Sprite { Texture = Source.GetTexture("spinner-top") }; + + return null; } return null; From e5ebd211569f8f0fe050142272c6e52ca64fb4b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 16:25:17 +0900 Subject: [PATCH 2430/6909] Fix test scene and add pooling support --- .../Skinning/TestSceneHitExplosion.cs | 50 ++++++++++++------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index a692c0b697..0c56f7bcf4 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -1,23 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.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.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Skinning; +using osu.Game.Rulesets.Objects; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Tests.Skinning { [TestFixture] public class TestSceneHitExplosion : ManiaSkinnableTestScene { + private readonly List> hitExplosionPools = new List>(); + public TestSceneHitExplosion() { int runcount = 0; @@ -29,28 +33,40 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning if (runcount % 15 > 12) return; - CreatedDrawables.OfType().ForEach(c => + int poolIndex = 0; + + foreach (var c in CreatedDrawables.OfType()) { - 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, - Origin = Anchor.Centre, - })); - }); + c.Add(hitExplosionPools[poolIndex].Get(e => + { + e.Apply(new JudgementResult(new HitObject(), runcount % 6 == 0 ? new HoldNoteTickJudgement() : new ManiaJudgement())); + + e.Anchor = Anchor.Centre; + e.Origin = Anchor.Centre; + })); + + poolIndex++; + } }, 100); } [BackgroundDependencyLoader] private void load() { - SetContents(() => new ColumnTestContainer(0, ManiaAction.Key1) + SetContents(() => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativePositionAxes = Axes.Y, - Y = -0.25f, - Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT), + var pool = new DrawablePool(5); + hitExplosionPools.Add(pool); + + return new ColumnTestContainer(0, ManiaAction.Key1) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.Y, + Y = -0.25f, + Size = new Vector2(Column.COLUMN_WIDTH, DefaultNotePiece.NOTE_HEIGHT), + Child = pool + }; }); } } From 5439099b7cb23253179c270b9f4ddc2002163ded Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 29 Jul 2020 10:34:09 +0300 Subject: [PATCH 2431/6909] Merge GlobalSkinConfiguration settings into the LegacySetting enum --- .../Gameplay/TestSceneHitObjectSamples.cs | 8 ++++---- .../Objects/Legacy/ConvertHitObjectParser.cs | 2 +- osu.Game/Skinning/GlobalSkinConfiguration.cs | 11 ----------- osu.Game/Skinning/LegacySkin.cs | 14 ++++---------- osu.Game/Skinning/LegacySkinConfiguration.cs | 2 ++ osu.Game/Skinning/LegacySkinExtensions.cs | 3 ++- osu.Game/Skinning/LegacySkinTransformer.cs | 3 ++- 7 files changed, 15 insertions(+), 28 deletions(-) delete mode 100644 osu.Game/Skinning/GlobalSkinConfiguration.cs diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index 737946e1e0..583400f579 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -6,9 +6,9 @@ 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; +using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Tests.Gameplay { @@ -190,7 +190,7 @@ namespace osu.Game.Tests.Gameplay } ///

    - /// Tests that when a custom sample bank is used, but is disabled, + /// Tests that when a custom sample bank is used, but is disabled, /// only the additional sound will be looked up. /// [Test] @@ -209,7 +209,7 @@ namespace osu.Game.Tests.Gameplay } /// - /// Tests that when a normal sample bank is used and is disabled, + /// Tests that when a normal sample bank is used and is disabled, /// the normal sound will be looked up anyway. /// [Test] @@ -226,6 +226,6 @@ namespace osu.Game.Tests.Gameplay } private void disableLayeredHitSounds() - => AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[GlobalSkinConfiguration.LayeredHitSounds.ToString()] = "0"); + => AddStep("set LayeredHitSounds to false", () => Skin.Configuration.ConfigDictionary[LegacySetting.LayeredHitSounds.ToString()] = "0"); } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 77075b2abe..9afc0ecaf4 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.Objects.Legacy ///
    /// /// Layered hit samples are automatically added in all modes (except osu!mania), but can be disabled - /// using the skin config option. + /// using the skin config option. /// public bool IsLayered { get; set; } } diff --git a/osu.Game/Skinning/GlobalSkinConfiguration.cs b/osu.Game/Skinning/GlobalSkinConfiguration.cs deleted file mode 100644 index d405702ea5..0000000000 --- a/osu.Game/Skinning/GlobalSkinConfiguration.cs +++ /dev/null @@ -1,11 +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.Skinning -{ - public enum GlobalSkinConfiguration - { - AnimationFramerate, - LayeredHitSounds, - } -} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 3bbeff9918..5843cde94d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -120,15 +120,6 @@ namespace osu.Game.Skinning break; - case LegacySkinConfiguration.LegacySetting legacy: - switch (legacy) - { - case LegacySkinConfiguration.LegacySetting.Version: - return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION)); - } - - break; - case SkinCustomColourLookup customColour: return SkinUtils.As(getCustomColour(Configuration, customColour.Lookup.ToString())); @@ -142,8 +133,11 @@ namespace osu.Game.Skinning break; + case LegacySkinConfiguration.LegacySetting s when s == LegacySkinConfiguration.LegacySetting.Version: + return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION)); + default: - // handles lookups like GlobalSkinConfiguration + // handles lookups like some in LegacySkinConfiguration.LegacySetting try { diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index 027f5b8883..41b7aea34b 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.cs @@ -15,6 +15,8 @@ namespace osu.Game.Skinning public enum LegacySetting { Version, + AnimationFramerate, + LayeredHitSounds, } } } diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 7cf41ef3c1..bb46dc8b9f 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Skinning { @@ -89,7 +90,7 @@ namespace osu.Game.Skinning { if (applyConfigFrameRate) { - var iniRate = source.GetConfig(GlobalSkinConfiguration.AnimationFramerate); + var iniRate = source.GetConfig(LegacySetting.AnimationFramerate); if (iniRate?.Value > 0) return 1000f / iniRate.Value; diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index 786056b932..ebc4757e75 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; +using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Skinning { @@ -38,7 +39,7 @@ namespace osu.Game.Skinning if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample)) return Source.GetSample(sampleInfo); - var playLayeredHitSounds = GetConfig(GlobalSkinConfiguration.LayeredHitSounds); + var playLayeredHitSounds = GetConfig(LegacySetting.LayeredHitSounds); if (legacySample.IsLayered && playLayeredHitSounds?.Value == false) return new SampleChannelVirtual(); From e98154b43277b726e286216b139ab9923e7806da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jul 2020 16:37:23 +0900 Subject: [PATCH 2432/6909] Add initial support for spinner background layer --- .../Objects/Drawables/DrawableSpinner.cs | 11 ++++++----- .../Objects/Drawables/Pieces/SpinnerDisc.cs | 2 +- .../Pieces/{SpinnerBackground.cs => SpinnerFill.cs} | 4 ++-- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 3 ++- .../Skinning/OsuLegacySkinTransformer.cs | 6 ++++++ 5 files changed, 17 insertions(+), 9 deletions(-) rename osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/{SpinnerBackground.cs => SpinnerFill.cs} (92%) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index ac0df0aef6..37601d9680 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Objects; using osu.Framework.Utils; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Container mainContainer; - public readonly SpinnerBackground Background; + public readonly SkinnableDrawable Background; private readonly Container circleContainer; private readonly CirclePiece circle; private readonly GlowPiece glow; @@ -96,7 +97,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RelativeSizeAxes = Axes.Y, Children = new[] { - Background = new SpinnerBackground + Background = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBackground), _ => new SpinnerFill { Disc = { @@ -104,7 +105,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }, Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, + }), Disc = new SpinnerDisc(Spinner) { Scale = Vector2.Zero, @@ -173,7 +174,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables normalColour = baseColour; completeColour = colours.YellowLight; - Background.AccentColour = normalColour; + if (Background.Drawable is IHasAccentColour accent) accent.AccentColour = normalColour; Ticks.AccentColour = normalColour; Disc.AccentColour = fillColour; @@ -328,7 +329,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Disc.FadeAccent(colour, duration); - Background.FadeAccent(colour.Darken(1), duration); + (Background.Drawable as IHasAccentColour)?.FadeAccent(colour.Darken(1), duration); Ticks.FadeAccent(colour, duration); circle.FadeColour(colour, duration); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs index 86588a8a9f..22a6fc391a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Children = new Drawable[] { - background = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerDisc), _ => new SpinnerBackground { Alpha = idle_alpha }), + background = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerDisc), _ => new SpinnerFill { Alpha = idle_alpha }), }; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs similarity index 92% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs rename to osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs index 944354abca..dbba1044ca 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBackground.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs @@ -10,7 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class SpinnerBackground : CircularContainer, IHasAccentColour + public class SpinnerFill : CircularContainer, IHasAccentColour { public readonly Box Disc; @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - public SpinnerBackground() + public SpinnerFill() { RelativeSizeAxes = Axes.Both; Masking = true; diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index e25b4a5efc..d72673f9ee 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Osu SliderFollowCircle, SliderBall, SliderBody, - SpinnerDisc + SpinnerDisc, + SpinnerBackground } } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index c5b5598be7..28eef603f4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -111,6 +111,12 @@ namespace osu.Game.Rulesets.Osu.Skinning return new Sprite { Texture = Source.GetTexture("spinner-top") }; return null; + + case OsuSkinComponents.SpinnerBackground: + if (Source.GetTexture("spinner-background") != null) + return new Sprite { Texture = Source.GetTexture("spinner-background") }; + + return null; } return null; From 5df406a0352285c871ed8606e54fae1c03e07a11 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 16:41:10 +0900 Subject: [PATCH 2433/6909] Add pooling for mania judgements --- .../UI/DrawableManiaJudgement.cs | 4 ++++ osu.Game.Rulesets.Mania/UI/Stage.cs | 16 ++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index 8797f014df..d99f6cb8d3 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -15,6 +15,10 @@ namespace osu.Game.Rulesets.Mania.UI { } + public DrawableManiaJudgement() + { + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index faa04dea97..36780b0f80 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; @@ -33,8 +34,8 @@ namespace osu.Game.Rulesets.Mania.UI public IReadOnlyList Columns => columnFlow.Children; private readonly FillFlowContainer columnFlow; - public Container Judgements => judgements; private readonly JudgementContainer judgements; + private readonly DrawablePool judgementPool; private readonly Drawable barLineContainer; private readonly Container topLevelContainer; @@ -63,6 +64,7 @@ namespace osu.Game.Rulesets.Mania.UI InternalChildren = new Drawable[] { + judgementPool = new DrawablePool(2), new Container { Anchor = Anchor.TopCentre, @@ -208,12 +210,14 @@ namespace osu.Game.Rulesets.Mania.UI if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; - judgements.Clear(); - judgements.Add(new DrawableManiaJudgement(result, judgedObject) + judgements.Clear(false); + judgements.Add(judgementPool.Get(j => { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); + j.Apply(result, judgedObject); + + j.Anchor = Anchor.Centre; + j.Origin = Anchor.Centre; + })); } protected override void Update() From 1c00cf95d5f7cb838bd72f2dbf09fd4a845c1422 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jul 2020 16:55:42 +0900 Subject: [PATCH 2434/6909] Add initial support for spinner middle skinning --- .../Objects/Drawables/DrawableSpinner.cs | 35 ++++++++++++------- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 3 +- .../Skinning/OsuLegacySkinTransformer.cs | 8 +++++ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 37601d9680..d6914c7a69 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects; using osu.Framework.Utils; @@ -36,11 +37,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Container mainContainer; public readonly SkinnableDrawable Background; - private readonly Container circleContainer; - private readonly CirclePiece circle; - private readonly GlowPiece glow; + private readonly SkinnableDrawable circleContainer; + private CirclePiece circle; + private GlowPiece glow; - private readonly SpriteIcon symbol; + private SpriteIcon symbol; private readonly Color4 baseColour = Color4Extensions.FromHex(@"002c3c"); private readonly Color4 fillColour = Color4Extensions.FromHex(@"005b7c"); @@ -66,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { ticks = new Container(), - circleContainer = new Container + circleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerCentre), _ => new Container { AutoSizeAxes = Axes.Both, Anchor = Anchor.Centre, @@ -89,6 +90,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Shadow = false, }, } + }) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, }, mainContainer = new AspectContainer { @@ -175,11 +180,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables completeColour = colours.YellowLight; if (Background.Drawable is IHasAccentColour accent) accent.AccentColour = normalColour; + Ticks.AccentColour = normalColour; Disc.AccentColour = fillColour; - circle.Colour = colours.BlueDark; - glow.Colour = colours.BlueDark; + if (circle != null) circle.Colour = colours.BlueDark; + if (glow != null) glow.Colour = colours.BlueDark; positionBindable.BindValueChanged(pos => Position = pos.NewValue); positionBindable.BindTo(HitObject.PositionBindable); @@ -224,6 +230,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; } + private float relativeHeight => ToScreenSpace(new RectangleF(0, 0, OsuHitObject.OBJECT_RADIUS, OsuHitObject.OBJECT_RADIUS)).Height / mainContainer.DrawHeight; + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -231,18 +239,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!SpmCounter.IsPresent && Disc.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); - circle.Rotation = Disc.Rotation; + if (circle != null) circle.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation; SpmCounter.SetRotation(Disc.CumulativeRotation); updateBonusScore(); - float relativeCircleScale = Spinner.Scale * circle.DrawHeight / mainContainer.DrawHeight; + float relativeCircleScale = Spinner.Scale * relativeHeight; 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)); + if (symbol != null) + symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } private int wholeSpins; @@ -291,7 +300,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 4, Easing.OutQuint); mainContainer - .ScaleTo(phaseOneScale * circle.DrawHeight / DrawHeight * 1.6f, HitObject.TimePreempt / 4, Easing.OutQuint) + .ScaleTo(phaseOneScale * relativeHeight * 1.6f, HitObject.TimePreempt / 4, Easing.OutQuint) .RotateTo((float)(25 * Spinner.Duration / 2000), HitObject.TimePreempt + Spinner.Duration); using (BeginDelayedSequence(HitObject.TimePreempt / 2, true)) @@ -332,8 +341,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables (Background.Drawable as IHasAccentColour)?.FadeAccent(colour.Darken(1), duration); Ticks.FadeAccent(colour, duration); - circle.FadeColour(colour, duration); - glow.FadeColour(colour, duration); + circle?.FadeColour(colour, duration); + glow?.FadeColour(colour, duration); } } } diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index d72673f9ee..c05dbf6b16 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Osu SliderBall, SliderBody, SpinnerDisc, - SpinnerBackground + SpinnerBackground, + SpinnerCentre } } diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 28eef603f4..a5198d3448 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -117,6 +117,14 @@ namespace osu.Game.Rulesets.Osu.Skinning return new Sprite { Texture = Source.GetTexture("spinner-background") }; return null; + + case OsuSkinComponents.SpinnerCentre: + if (Source.GetTexture("spinner-background") != null) + return Drawable.Empty(); + else if (Source.GetTexture("spinner-top") != null) + return new Sprite { Texture = Source.GetTexture("spinner-middle2") }; + + return null; } return null; From 2cd6e89cb05ba3cea8854dad77c614c4fd95cb63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jul 2020 18:02:12 +0900 Subject: [PATCH 2435/6909] Move default centre implementation out of DrawableSpinner --- .../Objects/Drawables/DefaultSpinnerCentre.cs | 83 +++++++++++++++++++ .../Objects/Drawables/DrawableSpinner.cs | 40 +-------- 2 files changed, 84 insertions(+), 39 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerCentre.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerCentre.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerCentre.cs new file mode 100644 index 0000000000..d07829fbac --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerCentre.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public class DefaultSpinnerCentre : CompositeDrawable + { + private DrawableSpinner spinner; + + private CirclePiece circle; + private GlowPiece glow; + private SpriteIcon symbol; + + [BackgroundDependencyLoader] + private void load(OsuColour colours, DrawableHitObject drawableHitObject) + { + spinner = (DrawableSpinner)drawableHitObject; + + AutoSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + InternalChildren = new Drawable[] + { + glow = new GlowPiece(), + circle = new CirclePiece + { + Position = Vector2.Zero, + Anchor = Anchor.Centre, + }, + new RingPiece(), + symbol = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(48), + Icon = FontAwesome.Solid.Asterisk, + Shadow = false, + }, + }; + + drawableHitObject.State.BindValueChanged(val => + { + Color4 colour; + + switch (val.NewValue) + { + default: + colour = colours.BlueDark; + break; + + case ArmedState.Hit: + colour = colours.YellowLight; + break; + } + + circle.FadeColour(colour, 200); + glow.FadeColour(colour, 200); + }, true); + + FinishTransforms(true); + } + + protected override void Update() + { + base.Update(); + + circle.Rotation = spinner.Disc.Rotation; + symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, spinner.Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index d6914c7a69..b2844c79a2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -14,7 +14,6 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects; using osu.Framework.Utils; using osu.Game.Rulesets.Scoring; @@ -38,10 +37,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public readonly SkinnableDrawable Background; private readonly SkinnableDrawable circleContainer; - private CirclePiece circle; - private GlowPiece glow; - - private SpriteIcon symbol; private readonly Color4 baseColour = Color4Extensions.FromHex(@"002c3c"); private readonly Color4 fillColour = Color4Extensions.FromHex(@"005b7c"); @@ -67,30 +62,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { ticks = new Container(), - circleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerCentre), _ => new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] - { - glow = new GlowPiece(), - circle = new CirclePiece - { - Position = Vector2.Zero, - Anchor = Anchor.Centre, - }, - new RingPiece(), - symbol = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(48), - Icon = FontAwesome.Solid.Asterisk, - Shadow = false, - }, - } - }) + circleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerCentre), _ => new DefaultSpinnerCentre()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -184,9 +156,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Ticks.AccentColour = normalColour; Disc.AccentColour = fillColour; - if (circle != null) circle.Colour = colours.BlueDark; - if (glow != null) glow.Colour = colours.BlueDark; - positionBindable.BindValueChanged(pos => Position = pos.NewValue); positionBindable.BindTo(HitObject.PositionBindable); } @@ -239,7 +208,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!SpmCounter.IsPresent && Disc.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); - if (circle != null) circle.Rotation = Disc.Rotation; Ticks.Rotation = Disc.Rotation; SpmCounter.SetRotation(Disc.CumulativeRotation); @@ -249,9 +217,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables float relativeCircleScale = Spinner.Scale * relativeHeight; 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))); - - if (symbol != null) - symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } private int wholeSpins; @@ -340,9 +305,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables (Background.Drawable as IHasAccentColour)?.FadeAccent(colour.Darken(1), duration); Ticks.FadeAccent(colour, duration); - - circle?.FadeColour(colour, duration); - glow?.FadeColour(colour, duration); } } } From 2a5e9fed4d3f8d3a1cb3ea44f34e30a739459df2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jul 2020 18:15:19 +0900 Subject: [PATCH 2436/6909] Move default background implementation out of DrawableSpinner --- .../Drawables/DefaultSpinnerBackground.cs | 44 +++++++++++++++++++ .../Objects/Drawables/DrawableSpinner.cs | 14 +----- 2 files changed, 45 insertions(+), 13 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerBackground.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerBackground.cs new file mode 100644 index 0000000000..be864b8c16 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerBackground.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public class DefaultSpinnerBackground : SpinnerFill + { + [BackgroundDependencyLoader] + private void load(OsuColour colours, DrawableHitObject drawableHitObject) + { + Disc.Alpha = 0; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + drawableHitObject.State.BindValueChanged(val => + { + Color4 colour; + + switch (val.NewValue) + { + default: + colour = colours.BlueDark; + break; + + case ArmedState.Hit: + colour = colours.YellowLight; + break; + } + + this.FadeAccent(colour.Darken(1), 200); + }, true); + + FinishTransforms(true); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index b2844c79a2..a0c2a06ff8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -74,15 +74,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RelativeSizeAxes = Axes.Y, Children = new[] { - Background = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBackground), _ => new SpinnerFill - { - Disc = - { - Alpha = 0f, - }, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }), + Background = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBackground), _ => new DefaultSpinnerBackground()), Disc = new SpinnerDisc(Spinner) { Scale = Vector2.Zero, @@ -151,8 +143,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables normalColour = baseColour; completeColour = colours.YellowLight; - if (Background.Drawable is IHasAccentColour accent) accent.AccentColour = normalColour; - Ticks.AccentColour = normalColour; Disc.AccentColour = fillColour; @@ -302,8 +292,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void transformFillColour(Colour4 colour, double duration) { Disc.FadeAccent(colour, duration); - - (Background.Drawable as IHasAccentColour)?.FadeAccent(colour.Darken(1), duration); Ticks.FadeAccent(colour, duration); } } From 4c00c11541c18a07d32e2332bfce515bce4d5c8c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 20:53:14 +0900 Subject: [PATCH 2437/6909] Remove unnecessary change --- .../Difficulty/TaikoPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index e6dd9f5084..b9d95a6ba6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; // Longer maps are worth more - double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0); + double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); strainValue *= lengthBonus; // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available From c1a4f2e6afd823b9e36c73f76f16ea4445da0a88 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 29 Jul 2020 20:53:50 +0900 Subject: [PATCH 2438/6909] Update expected SR in test --- .../TaikoDifficultyCalculatorTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index e7b6d8615b..2d51e82bc4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.9811338051242915d, "diffcalc-test")] - [TestCase(2.9811338051242915d, "diffcalc-test-strong")] + [TestCase(2.2905937546434592d, "diffcalc-test")] + [TestCase(2.2905937546434592d, "diffcalc-test-strong")] public void Test(double expected, string name) => base.Test(expected, name); From 023feaf438ca028ca0ee44d27d38d86408ed4c29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jul 2020 20:01:01 +0900 Subject: [PATCH 2439/6909] Refactor to centralise implementation into a single component Turns out this is a better way forward. --- .../TestSceneSpinner.cs | 2 +- .../TestSceneSpinnerRotation.cs | 22 +-- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 4 +- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 4 +- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 3 +- .../Drawables/DefaultSpinnerBackground.cs | 44 ----- .../Objects/Drawables/DrawableSpinner.cs | 144 +++------------ .../Drawables/Pieces/DefaultSpinnerDisc.cs | 170 ++++++++++++++++++ .../Objects/Drawables/Pieces/SpinnerFill.cs | 3 + ...innerDisc.cs => SpinnerRotationTracker.cs} | 85 ++------- .../Drawables/SpinnerBackgroundLayer.cs | 22 +++ ...SpinnerCentre.cs => SpinnerCentreLayer.cs} | 41 ++--- 12 files changed, 265 insertions(+), 279 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerBackground.cs create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs rename osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/{SpinnerDisc.cs => SpinnerRotationTracker.cs} (59%) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs rename osu.Game.Rulesets.Osu/Objects/Drawables/{DefaultSpinnerCentre.cs => SpinnerCentreLayer.cs} (67%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 8e3a22bfdc..65b338882e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Osu.Tests { base.Update(); if (auto) - Disc.Rotate((float)(Clock.ElapsedFrameTime * 3)); + RotationTracker.AddRotation((float)(Clock.ElapsedFrameTime * 3)); } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index c36bec391f..319d326a01 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -61,12 +61,12 @@ namespace osu.Game.Rulesets.Osu.Tests public void TestSpinnerRewindingRotation() { 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.CumulativeRotation, 0, 100)); + AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); + AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.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.CumulativeRotation, 0, 100)); + AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); + AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100)); } [Test] @@ -75,24 +75,24 @@ namespace osu.Game.Rulesets.Osu.Tests double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0; addSeekStep(5000); - AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.Disc.Rotation); - AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.Disc.CumulativeRotation); + AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.RotationTracker.Rotation); + AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.RotationTracker.CumulativeRotation); 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)); + () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalRelativeDiscRotation / 2, 100)); AddUntilStep("symbol rotation rewound", () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, 100)); addSeekStep(5000); AddAssert("is disc rotation almost same", - () => Precision.AlmostEquals(drawableSpinner.Disc.Rotation, finalRelativeDiscRotation, 100)); + () => Precision.AlmostEquals(drawableSpinner.RotationTracker.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.CumulativeRotation, finalAbsoluteDiscRotation, 100)); + () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalAbsoluteDiscRotation, 100)); } [Test] @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(5000); - AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.Disc.Rotation > 0 : drawableSpinner.Disc.Rotation < 0); + AddAssert("disc spin direction correct", () => clockwise ? drawableSpinner.RotationTracker.Rotation > 0 : drawableSpinner.RotationTracker.Rotation < 0); AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0); } @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.Disc.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK; + return totalScore == (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK; }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index fdba03f260..08fd13915d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -82,9 +82,7 @@ namespace osu.Game.Rulesets.Osu.Mods case DrawableSpinner spinner: // hide elements we don't care about. - spinner.Disc.Hide(); - spinner.Ticks.Hide(); - spinner.Background.Hide(); + // todo: hide background using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true)) spinner.FadeOut(fadeOutDuration); diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 7b54baa99b..47d765fecd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -40,8 +40,8 @@ namespace osu.Game.Rulesets.Osu.Mods { var spinner = (DrawableSpinner)drawable; - spinner.Disc.Tracking = true; - spinner.Disc.Rotate(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); + spinner.RotationTracker.Tracking = true; + spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 774f9cf58b..f209b315af 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -58,8 +58,7 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSpinner spinner: - spinner.Disc.Hide(); - spinner.Background.Hide(); + //todo: hide background break; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerBackground.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerBackground.cs deleted file mode 100644 index be864b8c16..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerBackground.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Game.Graphics; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using osuTK.Graphics; - -namespace osu.Game.Rulesets.Osu.Objects.Drawables -{ - public class DefaultSpinnerBackground : SpinnerFill - { - [BackgroundDependencyLoader] - private void load(OsuColour colours, DrawableHitObject drawableHitObject) - { - Disc.Alpha = 0; - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - - drawableHitObject.State.BindValueChanged(val => - { - Color4 colour; - - switch (val.NewValue) - { - default: - colour = colours.BlueDark; - break; - - case ArmedState.Hit: - colour = colours.YellowLight; - break; - } - - this.FadeAccent(colour.Darken(1), 200); - }, true); - - FinishTransforms(true); - } - } -} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a0c2a06ff8..11f1afafba 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -3,22 +3,19 @@ using System; using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using osuTK; -using osuTK.Graphics; -using osu.Game.Graphics; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; +using osu.Game.Graphics; using osu.Game.Rulesets.Objects; -using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -28,24 +25,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Container ticks; - public readonly SpinnerDisc Disc; - public readonly SpinnerTicks Ticks; + public readonly SpinnerRotationTracker RotationTracker; public readonly SpinnerSpmCounter SpmCounter; private readonly SpinnerBonusDisplay bonusDisplay; - private readonly Container mainContainer; - - public readonly SkinnableDrawable Background; - private readonly SkinnableDrawable circleContainer; - - private readonly Color4 baseColour = Color4Extensions.FromHex(@"002c3c"); - private readonly Color4 fillColour = Color4Extensions.FromHex(@"005b7c"); - private readonly IBindable positionBindable = new Bindable(); - private Color4 normalColour; - private Color4 completeColour; - public DrawableSpinner(Spinner s) : base(s) { @@ -62,27 +47,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { ticks = new Container(), - circleContainer = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerCentre), _ => new DefaultSpinnerCentre()) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - mainContainer = new AspectContainer + RotationTracker = new SpinnerRotationTracker(Spinner), + new AspectContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, - Children = new[] + Scale = new Vector2(Spinner.Scale), + Children = new Drawable[] { - Background = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBackground), _ => new DefaultSpinnerBackground()), - Disc = new SpinnerDisc(Spinner) - { - Scale = Vector2.Zero, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - circleContainer.CreateProxy(), - Ticks = new SpinnerTicks + new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerDisc), _ => new DefaultSpinnerDisc()), + RotationTracker = new SpinnerRotationTracker(Spinner) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -117,6 +92,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + protected override void UpdateStateTransforms(ArmedState state) + { + base.UpdateStateTransforms(state); + + using (BeginDelayedSequence(Spinner.Duration, true)) + this.FadeOut(160); + } + protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); @@ -140,27 +123,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load(OsuColour colours) { - normalColour = baseColour; - completeColour = colours.YellowLight; - - Ticks.AccentColour = normalColour; - Disc.AccentColour = fillColour; - positionBindable.BindValueChanged(pos => Position = pos.NewValue); positionBindable.BindTo(HitObject.PositionBindable); } - public float Progress => Math.Clamp(Disc.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1); + public float Progress => Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1); protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Time.Current < HitObject.StartTime) return; - if (Progress >= 1 && !Disc.Complete) - { - Disc.Complete = true; - transformFillColour(completeColour, 200); - } + RotationTracker.Complete.Value = Progress >= 1; if (userTriggered || Time.Current < Spinner.EndTime) return; @@ -186,27 +159,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.Update(); if (HandleUserInput) - Disc.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; + RotationTracker.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; } - private float relativeHeight => ToScreenSpace(new RectangleF(0, 0, OsuHitObject.OBJECT_RADIUS, OsuHitObject.OBJECT_RADIUS)).Height / mainContainer.DrawHeight; + public float RelativeHeight => ToScreenSpace(new RectangleF(0, 0, 0, OsuHitObject.OBJECT_RADIUS * 2)).Height / DrawHeight; protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - if (!SpmCounter.IsPresent && Disc.Tracking) + if (!SpmCounter.IsPresent && RotationTracker.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); - - Ticks.Rotation = Disc.Rotation; - - SpmCounter.SetRotation(Disc.CumulativeRotation); + SpmCounter.SetRotation(RotationTracker.CumulativeRotation); updateBonusScore(); - - float relativeCircleScale = Spinner.Scale * relativeHeight; - 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))); } private int wholeSpins; @@ -216,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (ticks.Count == 0) return; - int spins = (int)(Disc.CumulativeRotation / 360); + int spins = (int)(RotationTracker.CumulativeRotation / 360); if (spins < wholeSpins) { @@ -240,59 +206,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables wholeSpins++; } } - - protected override void UpdateInitialTransforms() - { - base.UpdateInitialTransforms(); - - circleContainer.ScaleTo(0); - mainContainer.ScaleTo(0); - - using (BeginDelayedSequence(HitObject.TimePreempt / 2, true)) - { - float phaseOneScale = Spinner.Scale * 0.7f; - - circleContainer.ScaleTo(phaseOneScale, HitObject.TimePreempt / 4, Easing.OutQuint); - - mainContainer - .ScaleTo(phaseOneScale * relativeHeight * 1.6f, HitObject.TimePreempt / 4, Easing.OutQuint) - .RotateTo((float)(25 * Spinner.Duration / 2000), HitObject.TimePreempt + Spinner.Duration); - - using (BeginDelayedSequence(HitObject.TimePreempt / 2, true)) - { - circleContainer.ScaleTo(Spinner.Scale, 400, Easing.OutQuint); - mainContainer.ScaleTo(1, 400, Easing.OutQuint); - } - } - } - - protected override void UpdateStateTransforms(ArmedState state) - { - base.UpdateStateTransforms(state); - - using (BeginDelayedSequence(Spinner.Duration, true)) - { - this.FadeOut(160); - - switch (state) - { - case ArmedState.Hit: - transformFillColour(completeColour, 0); - this.ScaleTo(Scale * 1.2f, 320, Easing.Out); - mainContainer.RotateTo(mainContainer.Rotation + 180, 320); - break; - - case ArmedState.Miss: - this.ScaleTo(Scale * 0.8f, 320, Easing.In); - break; - } - } - } - - private void transformFillColour(Colour4 colour, double duration) - { - Disc.FadeAccent(colour, duration); - Ticks.FadeAccent(colour, duration); - } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs new file mode 100644 index 0000000000..11cd73b995 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -0,0 +1,170 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +{ + public class DefaultSpinnerDisc : CompositeDrawable + { + private DrawableSpinner drawableSpinner; + + private Spinner spinner; + + private const float idle_alpha = 0.2f; + private const float tracking_alpha = 0.4f; + + private Color4 normalColour; + private Color4 completeColour; + + private SpinnerTicks ticks; + + private int completeTick; + private SpinnerFill fill; + private Container mainContainer; + private SpinnerCentreLayer centre; + private SpinnerBackgroundLayer background; + + public DefaultSpinnerDisc() + { + RelativeSizeAxes = Axes.Both; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, DrawableHitObject drawableHitObject) + { + drawableSpinner = (DrawableSpinner)drawableHitObject; + spinner = (Spinner)drawableSpinner.HitObject; + + normalColour = colours.BlueDark; + completeColour = colours.YellowLight; + + InternalChildren = new Drawable[] + { + mainContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + background = new SpinnerBackgroundLayer(), + fill = new SpinnerFill + { + Alpha = idle_alpha, + AccentColour = normalColour + }, + ticks = new SpinnerTicks + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AccentColour = normalColour + }, + } + }, + centre = new SpinnerCentreLayer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + centre.ScaleTo(0); + mainContainer.ScaleTo(0); + this.ScaleTo(1); + + drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); + drawableSpinner.State.BindValueChanged(updateStateTransforms, true); + } + + private void updateStateTransforms(ValueChangedEvent state) + { + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) + { + float phaseOneScale = spinner.Scale * 0.7f; + + centre.ScaleTo(phaseOneScale, spinner.TimePreempt / 4, Easing.OutQuint); + + mainContainer + .ScaleTo(phaseOneScale * drawableSpinner.RelativeHeight * 1.6f, spinner.TimePreempt / 4, Easing.OutQuint); + + this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); + + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) + { + centre.ScaleTo(spinner.Scale, spinner.TimePreempt / 2, Easing.OutQuint); + mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); + } + } + + // transforms we have from completing the spinner will be rolled back, so reapply immediately. + updateComplete(state.NewValue == ArmedState.Hit, 0); + + using (BeginDelayedSequence(spinner.Duration, true)) + { + switch (state.NewValue) + { + case ArmedState.Hit: + this.ScaleTo(Scale * 1.2f, 320, Easing.Out); + this.RotateTo(mainContainer.Rotation + 180, 320); + break; + + case ArmedState.Miss: + this.ScaleTo(Scale * 0.8f, 320, Easing.In); + break; + } + } + } + + private void updateComplete(bool complete, double duration) + { + var colour = complete ? completeColour : normalColour; + + ticks.FadeAccent(colour.Darken(1), duration); + fill.FadeAccent(colour.Darken(1), duration); + + background.FadeAccent(colour, duration); + centre.FadeAccent(colour, duration); + } + + private bool updateCompleteTick() => completeTick != (completeTick = (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360)); + + protected override void Update() + { + base.Update(); + + if (drawableSpinner.RotationTracker.Complete.Value && updateCompleteTick()) + { + fill.FinishTransforms(false, nameof(Alpha)); + fill + .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo) + .Then() + .FadeTo(tracking_alpha, 250, Easing.OutQuint); + } + + float relativeCircleScale = spinner.Scale * drawableSpinner.RelativeHeight; + float targetScale = relativeCircleScale + (1 - relativeCircleScale) * drawableSpinner.Progress; + + fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); + mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs index dbba1044ca..043bc5618c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs @@ -36,6 +36,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces RelativeSizeAxes = Axes.Both; Masking = true; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Children = new Drawable[] { Disc = new Box diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs similarity index 59% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs rename to osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index 22a6fc391a..968c2a6df5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs @@ -2,85 +2,33 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osuTK; -using osuTK.Graphics; using osu.Framework.Utils; -using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class SpinnerDisc : CircularContainer, IHasAccentColour + public class SpinnerRotationTracker : CircularContainer { private readonly Spinner spinner; - private Color4 accentColour; - - public Color4 AccentColour - { - get => accentColour; - set - { - accentColour = value; - if (background.Drawable is IHasAccentColour accent) - accent.AccentColour = value; - } - } - - private readonly SkinnableDrawable background; - - private const float idle_alpha = 0.2f; - private const float tracking_alpha = 0.4f; - public override bool IsPresent => true; // handle input when hidden - public SpinnerDisc(Spinner s) + public SpinnerRotationTracker(Spinner s) { spinner = s; RelativeSizeAxes = Axes.Both; - - Children = new Drawable[] - { - background = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerDisc), _ => new SpinnerFill { Alpha = idle_alpha }), - }; } public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - private bool tracking; + public bool Tracking { get; set; } - public bool Tracking - { - get => tracking; - set - { - if (value == tracking) return; - - tracking = value; - - // todo: new default only - background.Drawable.FadeTo(tracking ? tracking_alpha : idle_alpha, 100); - } - } - - private bool complete; - - public bool Complete - { - get => complete; - set - { - if (value == complete) return; - - complete = value; - - updateCompleteTick(); - } - } + public readonly BindableBool Complete = new BindableBool(); /// /// The total rotation performed on the spinner disc, disregarding the spin direction. @@ -93,7 +41,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 CumulativeRotation; + public float CumulativeRotation { get; private set; } /// /// Whether currently in the correct time range to allow spinning. @@ -110,9 +58,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private float lastAngle; private float currentRotation; - private int completeTick; - - private bool updateCompleteTick() => completeTick != (completeTick = (int)(CumulativeRotation / 360)); private bool rotationTransferred; @@ -123,21 +68,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var delta = thisAngle - lastAngle; - if (tracking) - Rotate(delta); + if (Tracking) + AddRotation(delta); lastAngle = thisAngle; - if (Complete && updateCompleteTick()) - { - // todo: new default only - background.Drawable.FinishTransforms(false, nameof(Alpha)); - background.Drawable - .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo) - .Then() - .FadeTo(tracking_alpha, 250, Easing.OutQuint); - } - Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); } @@ -148,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces /// Will be a no-op if not a valid time to spin. /// /// The delta angle. - public void Rotate(float angle) + public void AddRotation(float angle) { if (!isSpinnableTime) return; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs new file mode 100644 index 0000000000..3cd2454706 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.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.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public class SpinnerBackgroundLayer : SpinnerFill + { + [BackgroundDependencyLoader] + private void load(OsuColour colours, DrawableHitObject drawableHitObject) + { + Disc.Alpha = 0; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerCentre.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs similarity index 67% rename from osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerCentre.cs rename to osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs index d07829fbac..8803b92815 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DefaultSpinnerCentre.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs @@ -15,7 +15,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DefaultSpinnerCentre : CompositeDrawable + public class SpinnerCentreLayer : CompositeDrawable, IHasAccentColour { private DrawableSpinner spinner; @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private SpriteIcon symbol; [BackgroundDependencyLoader] - private void load(OsuColour colours, DrawableHitObject drawableHitObject) + private void load(DrawableHitObject drawableHitObject) { spinner = (DrawableSpinner)drawableHitObject; @@ -49,35 +49,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Shadow = false, }, }; - - drawableHitObject.State.BindValueChanged(val => - { - Color4 colour; - - switch (val.NewValue) - { - default: - colour = colours.BlueDark; - break; - - case ArmedState.Hit: - colour = colours.YellowLight; - break; - } - - circle.FadeColour(colour, 200); - glow.FadeColour(colour, 200); - }, true); - - FinishTransforms(true); } protected override void Update() { base.Update(); + symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, spinner.RotationTracker.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); + } - circle.Rotation = spinner.Disc.Rotation; - symbol.Rotation = (float)Interpolation.Lerp(symbol.Rotation, spinner.Disc.Rotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); + private Color4 accentColour; + + public Color4 AccentColour + { + get => accentColour; + set + { + accentColour = value; + + circle.Colour = accentColour; + glow.Colour = accentColour; + } } } } From 2b71ffa2ed9a45ec87b38fcd63cb053f244f800e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 29 Jul 2020 22:31:18 +0900 Subject: [PATCH 2440/6909] Add back legacy implementations --- .../Objects/Drawables/DrawableSpinner.cs | 2 +- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 4 +- .../Skinning/LegacyNewStyleSpinner.cs | 80 +++++++++++++++++++ .../Skinning/LegacyOldStyleSpinner.cs | 64 +++++++++++++++ .../Skinning/OsuLegacySkinTransformer.cs | 25 ++---- 5 files changed, 151 insertions(+), 24 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs create mode 100644 osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 11f1afafba..f36ecd9dc4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Scale = new Vector2(Spinner.Scale), Children = new Drawable[] { - new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerDisc), _ => new DefaultSpinnerDisc()), + new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()), RotationTracker = new SpinnerRotationTracker(Spinner) { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index c05dbf6b16..5468764692 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -17,8 +17,6 @@ namespace osu.Game.Rulesets.Osu SliderFollowCircle, SliderBall, SliderBody, - SpinnerDisc, - SpinnerBackground, - SpinnerCentre + SpinnerBody } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs new file mode 100644 index 0000000000..65b8b979fc --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public class LegacyNewStyleSpinner : CompositeDrawable + { + private Sprite discBottom; + private Sprite discTop; + private Sprite spinningMiddle; + + private DrawableSpinner drawableSpinner; + + [BackgroundDependencyLoader] + private void load(ISkinSource source, DrawableHitObject drawableObject) + { + drawableSpinner = (DrawableSpinner)drawableObject; + + InternalChildren = new Drawable[] + { + discBottom = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-bottom") + }, + discTop = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-top") + }, + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-middle") + }, + spinningMiddle = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-middle2") + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + this.FadeOut(); + drawableSpinner.State.BindValueChanged(updateStateTransforms, true); + } + + private void updateStateTransforms(ValueChangedEvent state) + { + var spinner = drawableSpinner.HitObject; + + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) + this.FadeInFromZero(spinner.TimePreempt / 2); + } + + protected override void Update() + { + base.Update(); + spinningMiddle.Rotation = discTop.Rotation = drawableSpinner.RotationTracker.Rotation; + discBottom.Rotation = discTop.Rotation / 3; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs new file mode 100644 index 0000000000..16d8664e66 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public class LegacyOldStyleSpinner : CompositeDrawable + { + private DrawableSpinner drawableSpinner; + private Sprite disc; + + [BackgroundDependencyLoader] + private void load(ISkinSource source, DrawableHitObject drawableObject) + { + drawableSpinner = (DrawableSpinner)drawableObject; + + InternalChildren = new Drawable[] + { + new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-background") + }, + disc = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-circle") + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + this.FadeOut(); + drawableSpinner.State.BindValueChanged(updateStateTransforms, true); + } + + private void updateStateTransforms(ValueChangedEvent state) + { + var spinner = drawableSpinner.HitObject; + + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) + this.FadeInFromZero(spinner.TimePreempt / 2); + } + + protected override void Update() + { + base.Update(); + disc.Rotation = drawableSpinner.RotationTracker.Rotation; + } + } +} diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index a5198d3448..44f431d6f7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Game.Skinning; using osuTK; @@ -104,25 +103,11 @@ namespace osu.Game.Rulesets.Osu.Skinning Spacing = new Vector2(-overlap, 0) }; - case OsuSkinComponents.SpinnerDisc: - if (Source.GetTexture("spinner-background") != null) - return new Sprite { Texture = Source.GetTexture("spinner-circle") }; - else if (Source.GetTexture("spinner-top") != null) - return new Sprite { Texture = Source.GetTexture("spinner-top") }; - - return null; - - case OsuSkinComponents.SpinnerBackground: - if (Source.GetTexture("spinner-background") != null) - return new Sprite { Texture = Source.GetTexture("spinner-background") }; - - return null; - - case OsuSkinComponents.SpinnerCentre: - if (Source.GetTexture("spinner-background") != null) - return Drawable.Empty(); - else if (Source.GetTexture("spinner-top") != null) - return new Sprite { Texture = Source.GetTexture("spinner-middle2") }; + case OsuSkinComponents.SpinnerBody: + if (Source.GetTexture("spinner-top") != null) + return new LegacyNewStyleSpinner(); + else if (Source.GetTexture("spinner-background") != null) + return new LegacyOldStyleSpinner(); return null; } From ca21f038e0c8bf8f2f2a619562d88af7feac50cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 10:35:48 +0900 Subject: [PATCH 2441/6909] Add xmldoc for legacy classes --- osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs | 4 ++++ osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 65b8b979fc..2521bb1d76 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -12,6 +12,10 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Skinning { + /// + /// Legacy skinned spinner with two main spinning layers, one fixed overlay and one final spinning overlay. + /// No background layer. + /// public class LegacyNewStyleSpinner : CompositeDrawable { private Sprite discBottom; diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 16d8664e66..e6a4064129 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -12,6 +12,9 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Skinning { + /// + /// Legacy skinned spinner with one main spinning layer and a background layer. + /// public class LegacyOldStyleSpinner : CompositeDrawable { private DrawableSpinner drawableSpinner; From d4496eb9824ad8bbc14da66d00b14c037fc54c69 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 30 Jul 2020 04:51:09 +0300 Subject: [PATCH 2442/6909] Update ShowMoreButton in line with web --- .../Visual/Online/TestSceneShowMoreButton.cs | 20 ++--- .../Graphics/UserInterface/ShowMoreButton.cs | 84 ++++++++++++++----- .../Comments/Buttons/CommentRepliesButton.cs | 15 ++-- .../Comments/CommentsShowMoreButton.cs | 11 --- .../Profile/Sections/PaginatedContainer.cs | 5 +- .../Profile/Sections/ProfileShowMoreButton.cs | 19 ----- 6 files changed, 76 insertions(+), 78 deletions(-) delete mode 100644 osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs index 273f593c32..18ac415126 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneShowMoreButton.cs @@ -4,19 +4,22 @@ using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; using osu.Framework.Allocation; -using osu.Game.Graphics; +using osu.Game.Overlays; namespace osu.Game.Tests.Visual.Online { public class TestSceneShowMoreButton : OsuTestScene { + [Cached] + private OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Green); + public TestSceneShowMoreButton() { - TestButton button = null; + ShowMoreButton button = null; int fireCount = 0; - Add(button = new TestButton + Add(button = new ShowMoreButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -46,16 +49,5 @@ namespace osu.Game.Tests.Visual.Online AddAssert("action fired twice", () => fireCount == 2); AddAssert("is in loading state", () => button.IsLoading); } - - private class TestButton : ShowMoreButton - { - [BackgroundDependencyLoader] - private void load(OsuColour colors) - { - IdleColour = colors.YellowDark; - HoverColour = colors.Yellow; - ChevronIconColour = colors.Red; - } - } } } diff --git a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs index c9cd9f1158..f4ab53d305 100644 --- a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs +++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osuTK; -using osuTK.Graphics; using System.Collections.Generic; namespace osu.Game.Graphics.UserInterface @@ -16,14 +18,6 @@ namespace osu.Game.Graphics.UserInterface { private const int duration = 200; - private Color4 chevronIconColour; - - protected Color4 ChevronIconColour - { - get => chevronIconColour; - set => chevronIconColour = leftChevron.Colour = rightChevron.Colour = value; - } - public string Text { get => text.Text; @@ -32,22 +26,28 @@ namespace osu.Game.Graphics.UserInterface protected override IEnumerable EffectTargets => new[] { background }; - private ChevronIcon leftChevron; - private ChevronIcon rightChevron; + private ChevronIcon leftIcon; + private ChevronIcon rightIcon; private SpriteText text; private Box background; private FillFlowContainer textContainer; public ShowMoreButton() { - Height = 30; - Width = 140; + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + IdleColour = colourProvider.Background2; + HoverColour = colourProvider.Background1; } protected override Drawable CreateContent() => new CircularContainer { Masking = true, - RelativeSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Both, Children = new Drawable[] { background = new Box @@ -56,22 +56,36 @@ namespace osu.Game.Graphics.UserInterface }, textContainer = new FillFlowContainer { + AlwaysPresent = true, Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(7), + Spacing = new Vector2(10), + Margin = new MarginPadding + { + Horizontal = 20, + Vertical = 5 + }, Children = new Drawable[] { - leftChevron = new ChevronIcon(), + leftIcon = new ChevronIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, text = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Text = "show more".ToUpper(), }, - rightChevron = new ChevronIcon(), + rightIcon = new ChevronIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } } } } @@ -81,17 +95,41 @@ namespace osu.Game.Graphics.UserInterface protected override void OnLoadFinished() => textContainer.FadeIn(duration, Easing.OutQuint); - private class ChevronIcon : SpriteIcon + protected override bool OnHover(HoverEvent e) { - private const int icon_size = 8; + base.OnHover(e); + leftIcon.FadeHoverColour(); + rightIcon.FadeHoverColour(); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + leftIcon.FadeIdleColour(); + rightIcon.FadeIdleColour(); + } + + public class ChevronIcon : SpriteIcon + { + [Resolved] + private OverlayColourProvider colourProvider { get; set; } public ChevronIcon() { - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - Size = new Vector2(icon_size); + Size = new Vector2(7.5f); Icon = FontAwesome.Solid.ChevronDown; } + + [BackgroundDependencyLoader] + private void load() + { + Colour = colourProvider.Foreground1; + } + + public void FadeHoverColour() => this.FadeColour(colourProvider.Light1, 200, Easing.OutQuint); + + public void FadeIdleColour() => this.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index 53438ca421..202f3ddd7b 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -5,12 +5,12 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osuTK; +using static osu.Game.Graphics.UserInterface.ShowMoreButton; namespace osu.Game.Overlays.Comments.Buttons { @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Comments.Buttons [Resolved] private OverlayColourProvider colourProvider { get; set; } - private readonly SpriteIcon icon; + private readonly ChevronIcon icon; private readonly Box background; private readonly OsuSpriteText text; @@ -68,12 +68,10 @@ namespace osu.Game.Overlays.Comments.Buttons AlwaysPresent = true, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold) }, - icon = new SpriteIcon + icon = new ChevronIcon { Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(7.5f), - Icon = FontAwesome.Solid.ChevronDown + Origin = Anchor.CentreLeft } } } @@ -88,7 +86,6 @@ namespace osu.Game.Overlays.Comments.Buttons private void load() { background.Colour = colourProvider.Background2; - icon.Colour = colourProvider.Foreground1; } protected void SetIconDirection(bool upwards) => icon.ScaleTo(new Vector2(1, upwards ? -1 : 1)); @@ -99,7 +96,7 @@ namespace osu.Game.Overlays.Comments.Buttons { base.OnHover(e); background.FadeColour(colourProvider.Background1, 200, Easing.OutQuint); - icon.FadeColour(colourProvider.Light1, 200, Easing.OutQuint); + icon.FadeHoverColour(); return true; } @@ -107,7 +104,7 @@ namespace osu.Game.Overlays.Comments.Buttons { base.OnHoverLost(e); background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint); - icon.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); + icon.FadeIdleColour(); } } } diff --git a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs b/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs index d2ff7ecb1f..adf64eabb1 100644 --- a/osu.Game/Overlays/Comments/CommentsShowMoreButton.cs +++ b/osu.Game/Overlays/Comments/CommentsShowMoreButton.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.Allocation; using osu.Framework.Bindables; using osu.Game.Graphics.UserInterface; @@ -11,16 +10,6 @@ namespace osu.Game.Overlays.Comments { public readonly BindableInt Current = new BindableInt(); - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - Height = 20; - - IdleColour = colourProvider.Background2; - HoverColour = colourProvider.Background1; - ChevronIconColour = colourProvider.Foreground1; - } - protected override void LoadComplete() { Current.BindValueChanged(onCurrentChanged, true); diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index a30ff786fb..9720469548 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -14,12 +14,13 @@ using osu.Game.Users; using System.Collections.Generic; using System.Linq; using System.Threading; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Profile.Sections { public abstract class PaginatedContainer : FillFlowContainer { - private readonly ProfileShowMoreButton moreButton; + private readonly ShowMoreButton moreButton; private readonly OsuSpriteText missingText; private APIRequest> retrievalRequest; private CancellationTokenSource loadCancellation; @@ -74,7 +75,7 @@ namespace osu.Game.Overlays.Profile.Sections RelativeSizeAxes = Axes.X, Spacing = new Vector2(0, 2), }, - moreButton = new ProfileShowMoreButton + moreButton = new ShowMoreButton { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs b/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs deleted file mode 100644 index 426ebeebe6..0000000000 --- a/osu.Game/Overlays/Profile/Sections/ProfileShowMoreButton.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Overlays.Profile.Sections -{ - public class ProfileShowMoreButton : ShowMoreButton - { - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - IdleColour = colourProvider.Background2; - HoverColour = colourProvider.Background1; - ChevronIconColour = colourProvider.Foreground1; - } - } -} From 45ddc7a2e9f9d1b05404831c172ff479920031a5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 30 Jul 2020 05:02:01 +0300 Subject: [PATCH 2443/6909] Rename ShowMoreButton in comments namespace to ShowMoreRepliesButton --- .../Buttons/{ShowMoreButton.cs => ShowMoreRepliesButton.cs} | 4 ++-- osu.Game/Overlays/Comments/DrawableComment.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game/Overlays/Comments/Buttons/{ShowMoreButton.cs => ShowMoreRepliesButton.cs} (93%) diff --git a/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs b/osu.Game/Overlays/Comments/Buttons/ShowMoreRepliesButton.cs similarity index 93% rename from osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs rename to osu.Game/Overlays/Comments/Buttons/ShowMoreRepliesButton.cs index 2c363564d2..c115a8bb8f 100644 --- a/osu.Game/Overlays/Comments/Buttons/ShowMoreButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/ShowMoreRepliesButton.cs @@ -12,13 +12,13 @@ using osu.Framework.Allocation; namespace osu.Game.Overlays.Comments.Buttons { - public class ShowMoreButton : LoadingButton + public class ShowMoreRepliesButton : LoadingButton { protected override IEnumerable EffectTargets => new[] { text }; private OsuSpriteText text; - public ShowMoreButton() + public ShowMoreRepliesButton() { AutoSizeAxes = Axes.Both; LoadingAnimationSize = new Vector2(8); diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 9c0a48ec29..31aa41e967 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -46,7 +46,7 @@ namespace osu.Game.Overlays.Comments private FillFlowContainer childCommentsVisibilityContainer; private FillFlowContainer childCommentsContainer; private LoadRepliesButton loadRepliesButton; - private ShowMoreButton showMoreButton; + private ShowMoreRepliesButton showMoreButton; private ShowRepliesButton showRepliesButton; private ChevronButton chevronButton; private DeletedCommentsCounter deletedCommentsCounter; @@ -213,7 +213,7 @@ namespace osu.Game.Overlays.Comments Top = 10 } }, - showMoreButton = new ShowMoreButton + showMoreButton = new ShowMoreRepliesButton { Action = () => RepliesRequested(this, ++currentPage) } From 64c7ae768641e22a51fffb0e2a828ebcba00eab9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 11:25:49 +0900 Subject: [PATCH 2444/6909] Fix hit transforms not playing out correctly --- .../Objects/Drawables/Pieces/DefaultSpinnerDisc.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 11cd73b995..6a40069c5d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -87,16 +87,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.LoadComplete(); - centre.ScaleTo(0); - mainContainer.ScaleTo(0); - this.ScaleTo(1); - drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); drawableSpinner.State.BindValueChanged(updateStateTransforms, true); } private void updateStateTransforms(ValueChangedEvent state) { + centre.ScaleTo(0); + mainContainer.ScaleTo(0); + this.ScaleTo(1); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) { float phaseOneScale = spinner.Scale * 0.7f; From 54fee7e7160ec6931937f37c31898a5e554bc247 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 11:50:13 +0900 Subject: [PATCH 2445/6909] Simplify and standardise scale for default display --- .../Objects/Drawables/DrawableSpinner.cs | 12 ---------- .../Drawables/Pieces/DefaultSpinnerDisc.cs | 22 +++++++++---------- .../Objects/Drawables/SpinnerCentreLayer.cs | 3 --- 3 files changed, 11 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index f36ecd9dc4..52a1b4679b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Primitives; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -39,29 +38,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RelativeSizeAxes = Axes.Both; - // we are slightly bigger than our parent, to clip the top and bottom of the circle - Height = 1.3f; - Spinner = s; InternalChildren = new Drawable[] { ticks = new Container(), - RotationTracker = new SpinnerRotationTracker(Spinner), new AspectContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, - Scale = new Vector2(Spinner.Scale), Children = new Drawable[] { new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()), RotationTracker = new SpinnerRotationTracker(Spinner) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, } }, SpmCounter = new SpinnerSpmCounter @@ -162,8 +152,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RotationTracker.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; } - public float RelativeHeight => ToScreenSpace(new RectangleF(0, 0, 0, OsuHitObject.OBJECT_RADIUS * 2)).Height / DrawHeight; - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 6a40069c5d..2644f425a0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -39,6 +39,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { RelativeSizeAxes = Axes.Both; + // we are slightly bigger than our parent, to clip the top and bottom of the circle + // this should probably be revisited when scaled spinners are a thing. + Scale = new Vector2(1.3f); + Anchor = Anchor.Centre; Origin = Anchor.Centre; } @@ -95,22 +99,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { centre.ScaleTo(0); mainContainer.ScaleTo(0); - this.ScaleTo(1); using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) { - float phaseOneScale = spinner.Scale * 0.7f; - - centre.ScaleTo(phaseOneScale, spinner.TimePreempt / 4, Easing.OutQuint); - - mainContainer - .ScaleTo(phaseOneScale * drawableSpinner.RelativeHeight * 1.6f, spinner.TimePreempt / 4, Easing.OutQuint); - + // constant ambient rotation to give the spinner "spinning" character. this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); + centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); + mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint); + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) { - centre.ScaleTo(spinner.Scale, spinner.TimePreempt / 2, Easing.OutQuint); + centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint); mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); } } @@ -160,8 +160,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces .FadeTo(tracking_alpha, 250, Easing.OutQuint); } - float relativeCircleScale = spinner.Scale * drawableSpinner.RelativeHeight; - float targetScale = relativeCircleScale + (1 - relativeCircleScale) * drawableSpinner.Progress; + const float initial_scale = 0.2f; + float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress; fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs index 8803b92815..b62ce822f0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs @@ -28,9 +28,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { spinner = (DrawableSpinner)drawableHitObject; - AutoSizeAxes = Axes.Both; - Anchor = Anchor.Centre; - Origin = Anchor.Centre; InternalChildren = new Drawable[] { glow = new GlowPiece(), From 4d822742e83a8892838b7ddf105f5f4c6c472858 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 12:05:19 +0900 Subject: [PATCH 2446/6909] Add scale and tint to new legacy style spinner --- .../Skinning/LegacyNewStyleSpinner.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 2521bb1d76..618cb0e9ad 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -6,9 +6,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Skinning { @@ -21,14 +25,19 @@ namespace osu.Game.Rulesets.Osu.Skinning private Sprite discBottom; private Sprite discTop; private Sprite spinningMiddle; + private Sprite fixedMiddle; private DrawableSpinner drawableSpinner; + private const float final_scale = 0.625f; + [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) { drawableSpinner = (DrawableSpinner)drawableObject; + Scale = new Vector2(final_scale); + InternalChildren = new Drawable[] { discBottom = new Sprite @@ -43,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Origin = Anchor.Centre, Texture = source.GetTexture("spinner-top") }, - new Sprite + fixedMiddle = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -68,10 +77,14 @@ namespace osu.Game.Rulesets.Osu.Skinning private void updateStateTransforms(ValueChangedEvent state) { - var spinner = drawableSpinner.HitObject; + var spinner = (Spinner)drawableSpinner.HitObject; using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) this.FadeInFromZero(spinner.TimePreempt / 2); + + fixedMiddle.FadeColour(Color4.White); + using (BeginAbsoluteSequence(spinner.StartTime, true)) + fixedMiddle.FadeColour(Color4.Red, spinner.Duration); } protected override void Update() @@ -79,6 +92,9 @@ namespace osu.Game.Rulesets.Osu.Skinning base.Update(); spinningMiddle.Rotation = discTop.Rotation = drawableSpinner.RotationTracker.Rotation; discBottom.Rotation = discTop.Rotation / 3; + + Scale = new Vector2(final_scale * 0.8f + + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * (final_scale * 0.2f)); } } } From d2b3fe1e7b3412dd82082c3ffe818972a52f47e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 12:08:04 +0900 Subject: [PATCH 2447/6909] Add scale to old legacy spinner --- osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index e6a4064129..02b8c7eef1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu.Skinning { @@ -25,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { drawableSpinner = (DrawableSpinner)drawableObject; + Scale = new Vector2(0.625f); + InternalChildren = new Drawable[] { new Sprite From 743d165319cba9784ee2700524999c59298e1880 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 12:32:19 +0900 Subject: [PATCH 2448/6909] Add old style spin metre --- .../Skinning/LegacyOldStyleSpinner.cs | 50 +++++++++++++++++-- 1 file changed, 45 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 02b8c7eef1..9b1f2b31e3 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -1,11 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; @@ -20,37 +22,59 @@ namespace osu.Game.Rulesets.Osu.Skinning { private DrawableSpinner drawableSpinner; private Sprite disc; + private Container metre; [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) { drawableSpinner = (DrawableSpinner)drawableObject; - Scale = new Vector2(0.625f); + RelativeSizeAxes = Axes.Both; InternalChildren = new Drawable[] { new Sprite { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-background") + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Texture = source.GetTexture("spinner-background"), + Y = 20, + Scale = new Vector2(0.625f) }, disc = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-circle") + Texture = source.GetTexture("spinner-circle"), + Scale = new Vector2(0.625f) + }, + metre = new Container + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Y = 20, + Masking = true, + Child = new Sprite + { + Texture = source.GetTexture("spinner-metre"), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + }, + Scale = new Vector2(0.625f) } }; } + private Vector2 metreFinalSize; + protected override void LoadComplete() { base.LoadComplete(); this.FadeOut(); drawableSpinner.State.BindValueChanged(updateStateTransforms, true); + + metreFinalSize = metre.Size = metre.Child.Size; } private void updateStateTransforms(ValueChangedEvent state) @@ -65,6 +89,22 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.Update(); disc.Rotation = drawableSpinner.RotationTracker.Rotation; + metre.Height = getMetreHeight(drawableSpinner.Progress); + } + + private const int total_bars = 10; + + private float getMetreHeight(float progress) + { + progress = Math.Min(99, progress * 100); + + int barCount = (int)progress / 10; + + // todo: add SpinnerNoBlink support + if (RNG.NextBool(((int)progress % 10) / 10f)) + barCount++; + + return (float)barCount / total_bars * metreFinalSize.Y; } } } From 19fb350cd81d1ed3567f967a4a3d1f1d00943454 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 12:50:27 +0900 Subject: [PATCH 2449/6909] Move offset and scale to constant --- .../Skinning/LegacyOldStyleSpinner.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 9b1f2b31e3..0ae1d8f683 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -24,6 +24,10 @@ namespace osu.Game.Rulesets.Osu.Skinning private Sprite disc; private Container metre; + private const float background_y_offset = 20; + + private const float sprite_scale = 1 / 1.6f; + [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) { @@ -38,21 +42,21 @@ namespace osu.Game.Rulesets.Osu.Skinning Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, Texture = source.GetTexture("spinner-background"), - Y = 20, - Scale = new Vector2(0.625f) + Y = background_y_offset, + Scale = new Vector2(sprite_scale) }, disc = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-circle"), - Scale = new Vector2(0.625f) + Scale = new Vector2(sprite_scale) }, metre = new Container { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Y = 20, + Y = background_y_offset, Masking = true, Child = new Sprite { From c1085d49d3c41b88233cf530e41774065161f527 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 12:55:34 +0900 Subject: [PATCH 2450/6909] Add more xmldoc --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 52a1b4679b..f65570a3d5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -117,6 +117,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables positionBindable.BindTo(HitObject.PositionBindable); } + /// + /// The completion progress of this spinner from 0..1 (clamped). + /// public float Progress => Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1); protected override void CheckForResult(bool userTriggered, double timeOffset) From 41c0f7557ac2a3bcc46174d811ead37d432a17fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 12:56:30 +0900 Subject: [PATCH 2451/6909] Remove traceable spinner reference for now --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index f209b315af..977485ba66 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -57,9 +57,7 @@ namespace osu.Game.Rulesets.Osu.Mods applySliderState(slider); break; - case DrawableSpinner spinner: - //todo: hide background - break; + //todo: hide spinner background somehow } } From ec0d7760afeca015e7ed8ce488a27bedcb6cc20f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 13:06:53 +0900 Subject: [PATCH 2452/6909] Move todo? --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 977485ba66..d7582f3196 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -43,6 +43,8 @@ namespace osu.Game.Rulesets.Osu.Mods var h = drawableOsu.HitObject; + //todo: expose and hide spinner background somehow + switch (drawable) { case DrawableHitCircle circle: @@ -56,8 +58,6 @@ namespace osu.Game.Rulesets.Osu.Mods slider.Body.OnSkinChanged += () => applySliderState(slider); applySliderState(slider); break; - - //todo: hide spinner background somehow } } From 6473bf503b91bdc6d8b8acb474530e7125862152 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 30 Jul 2020 07:09:40 +0300 Subject: [PATCH 2453/6909] Remove use of `case when` --- osu.Game/Skinning/LegacySkin.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 5843cde94d..d98f8aba83 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -133,8 +133,14 @@ namespace osu.Game.Skinning break; - case LegacySkinConfiguration.LegacySetting s when s == LegacySkinConfiguration.LegacySetting.Version: - return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION)); + case LegacySkinConfiguration.LegacySetting legacy: + switch (legacy) + { + case LegacySkinConfiguration.LegacySetting.Version: + return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION)); + } + + goto default; default: // handles lookups like some in LegacySkinConfiguration.LegacySetting From e5991d6e1487cd3f9a3596494fc732a0f36a5155 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 13:49:04 +0900 Subject: [PATCH 2454/6909] Change method structure for hover/unhover state setting (shouldn't be called "Fade") --- osu.Game/Graphics/UserInterface/ShowMoreButton.cs | 13 ++++++------- .../Comments/Buttons/CommentRepliesButton.cs | 4 ++-- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs index f4ab53d305..924c7913f3 100644 --- a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs +++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs @@ -98,16 +98,16 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { base.OnHover(e); - leftIcon.FadeHoverColour(); - rightIcon.FadeHoverColour(); + leftIcon.SetHoveredState(true); + rightIcon.SetHoveredState(true); return true; } protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); - leftIcon.FadeIdleColour(); - rightIcon.FadeIdleColour(); + leftIcon.SetHoveredState(false); + rightIcon.SetHoveredState(false); } public class ChevronIcon : SpriteIcon @@ -127,9 +127,8 @@ namespace osu.Game.Graphics.UserInterface Colour = colourProvider.Foreground1; } - public void FadeHoverColour() => this.FadeColour(colourProvider.Light1, 200, Easing.OutQuint); - - public void FadeIdleColour() => this.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); + public void SetHoveredState(bool hovered) => + this.FadeColour(hovered ? colourProvider.Light1 : colourProvider.Foreground1, 200, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index 202f3ddd7b..57bf2af4d2 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -96,7 +96,7 @@ namespace osu.Game.Overlays.Comments.Buttons { base.OnHover(e); background.FadeColour(colourProvider.Background1, 200, Easing.OutQuint); - icon.FadeHoverColour(); + icon.SetHoveredState(true); return true; } @@ -104,7 +104,7 @@ namespace osu.Game.Overlays.Comments.Buttons { base.OnHoverLost(e); background.FadeColour(colourProvider.Background2, 200, Easing.OutQuint); - icon.FadeIdleColour(); + icon.SetHoveredState(false); } } } From 7071f9a3af6fd281cf76c5aacef45959df5c3e87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 14:24:21 +0900 Subject: [PATCH 2455/6909] 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 7e6f1469f5..61314bdc10 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 5ac54a853f..d6aeca1f53 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 8b2d1346be..9b8d70ab6d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From d8bb52800fe5e351dd730cd2d37f707b1a447c3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 14:31:05 +0900 Subject: [PATCH 2456/6909] Update framework again (github deploy failed) --- 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 61314bdc10..0d951f58e6 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 d6aeca1f53..92e7080fed 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 9b8d70ab6d..973c1d5b89 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 1dfd2112c60d2f468a74675f383b4125334d11cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 15:32:08 +0900 Subject: [PATCH 2457/6909] Source hash from osu.Game.dll rather than executable --- osu.Game/OsuGameBase.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 964a7fdd35..278f2d849f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -134,13 +134,8 @@ namespace osu.Game [BackgroundDependencyLoader] private void load() { - var assembly = Assembly.GetEntryAssembly(); - - if (assembly != null) - { - using (var str = File.OpenRead(assembly.Location)) - VersionHash = str.ComputeMD5Hash(); - } + using (var str = File.OpenRead(typeof(OsuGameBase).Assembly.Location)) + VersionHash = str.ComputeMD5Hash(); Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); From cf697cc276e7aa466d068566b4250009291efdd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 15:32:40 +0900 Subject: [PATCH 2458/6909] Update framework again (fix audio component disposal issue) --- 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 0d951f58e6..0ac766926c 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 92e7080fed..1ececa448c 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 973c1d5b89..ef5ba10d17 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 6b9102b2a43665b82c92bcb3a3a643e8d23acce9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 17:58:49 +0900 Subject: [PATCH 2459/6909] Add osu!catch banana catching sounds --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 21 +++++++++++++++++++ .../Objects/BananaShower.cs | 12 ++++++++--- .../Objects/Drawables/DrawableBanana.cs | 7 +++++++ 3 files changed, 37 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 0b3d1d23e0..4ecfb7b16d 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.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.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Judgements; @@ -8,8 +10,27 @@ namespace osu.Game.Rulesets.Catch.Objects { public class Banana : Fruit { + /// + /// Index of banana in current shower. + /// + public int BananaIndex; + public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; public override Judgement CreateJudgement() => new CatchBananaJudgement(); + + private static readonly List samples = new List { new BananaHitSampleInfo() }; + + public Banana() + { + Samples = samples; + } + + private class BananaHitSampleInfo : HitSampleInfo + { + private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; + + public override IEnumerable LookupNames => lookupNames; + } } } diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index 04a995c77e..89c51459a6 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs @@ -30,15 +30,21 @@ namespace osu.Game.Rulesets.Catch.Objects if (spacing <= 0) return; - for (double i = StartTime; i <= EndTime; i += spacing) + double time = StartTime; + int i = 0; + + while (time <= EndTime) { cancellationToken.ThrowIfCancellationRequested(); AddNested(new Banana { - Samples = Samples, - StartTime = i + StartTime = time, + BananaIndex = i, }); + + time += spacing; + i++; } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 01b76ceed9..a865984d45 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -40,6 +40,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1); } + public override void PlaySamples() + { + base.PlaySamples(); + if (Samples != null) + Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f; + } + private Color4 getBananaColour() { switch (RNG.Next(0, 3)) From 38a4bdf068c7b727e046a5b6d6119d6b3339f51b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 30 Jul 2020 19:34:59 +0900 Subject: [PATCH 2460/6909] Add spinner spin sample support --- .../Objects/Drawables/DrawableSpinner.cs | 62 ++++++++++++++++++- .../Pieces/SpinnerRotationTracker.cs | 9 ++- 2 files changed, 69 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index f65570a3d5..68516bedf8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -70,6 +70,58 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }; } + private Bindable isSpinning; + + protected override void LoadComplete() + { + base.LoadComplete(); + + isSpinning = RotationTracker.IsSpinning.GetBoundCopy(); + isSpinning.BindValueChanged(updateSpinningSample); + } + + private SkinnableSound spinningSample; + + private const float minimum_volume = 0.0001f; + + protected override void LoadSamples() + { + base.LoadSamples(); + + spinningSample?.Expire(); + spinningSample = null; + + var firstSample = HitObject.Samples.FirstOrDefault(); + + if (firstSample != null) + { + var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); + clone.Name = "spinnerspin"; + + AddInternal(spinningSample = new SkinnableSound(clone) + { + Volume = { Value = minimum_volume }, + Looping = true, + }); + } + } + + private void updateSpinningSample(ValueChangedEvent tracking) + { + // note that samples will not start playing if exiting a seek operation in the middle of a spinner. + // may be something we want to address at a later point, but not so easy to make happen right now + // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update). + if (tracking.NewValue && ShouldPlaySamples) + { + spinningSample?.Play(); + spinningSample?.VolumeTo(1, 200); + } + else + { + spinningSample?.VolumeTo(minimum_volume, 200).Finally(_ => spinningSample.Stop()); + } + } + protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); @@ -88,6 +140,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables using (BeginDelayedSequence(Spinner.Duration, true)) this.FadeOut(160); + + // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback. + isSpinning?.TriggerChange(); } protected override void ClearNestedHitObjects() @@ -151,8 +206,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void Update() { base.Update(); + if (HandleUserInput) - RotationTracker.Tracking = OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false; + RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); + + if (spinningSample != null) + // todo: implement SpinnerFrequencyModulate + spinningSample.Frequency.Value = 0.5f + Progress; } protected override void UpdateAfterChildren() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index 968c2a6df5..0cc6c842f4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs @@ -43,6 +43,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces /// public float CumulativeRotation { get; private set; } + /// + /// Whether the spinning is spinning at a reasonable speed to be considered visually spinning. + /// + public readonly BindableBool IsSpinning = new BindableBool(); + /// /// Whether currently in the correct time range to allow spinning. /// @@ -73,7 +78,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces lastAngle = thisAngle; - Rotation = (float)Interpolation.Lerp(Rotation, currentRotation / 2, Math.Clamp(Math.Abs(Time.Elapsed) / 40, 0, 1)); + IsSpinning.Value = isSpinnableTime && Math.Abs(currentRotation / 2 - Rotation) > 5f; + + Rotation = (float)Interpolation.Damp(Rotation, currentRotation / 2, 0.99, Math.Abs(Time.Elapsed)); } /// From 23ab6f8f946739bb788cc480f894b57611e78647 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 30 Jul 2020 21:10:13 +0900 Subject: [PATCH 2461/6909] Fix dynamic compilation loading wrong ruleset versions --- osu.Game/Rulesets/RulesetStore.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 58a2ba056e..837796287a 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -65,11 +65,15 @@ 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 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; - } + var domainAssembly = AppDomain.CurrentDomain.GetAssemblies() + // Given name is always going to be equally-or-more qualified than the assembly name. + .Where(a => args.Name.Contains(a.GetName().Name, StringComparison.Ordinal)) + // Pick the greatest assembly version. + .OrderBy(a => a.GetName().Version) + .LastOrDefault(); + + if (domainAssembly != null) + return domainAssembly; return loadedAssemblies.Keys.FirstOrDefault(a => a.FullName == asm.FullName); } From 5af45bcdcc008b8bc61d3919037b6cd150532893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jul 2020 20:10:41 +0200 Subject: [PATCH 2462/6909] Expand tests to cover non-bank sample lookups --- osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index 737946e1e0..a70b08a0d3 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -67,9 +67,11 @@ namespace osu.Game.Tests.Gameplay /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the beatmap skin: /// normal-hitnormal2 /// normal-hitnormal + /// hitnormal /// [TestCase("normal-hitnormal2")] [TestCase("normal-hitnormal")] + [TestCase("hitnormal")] public void TestDefaultCustomSampleFromBeatmap(string expectedSample) { SetupSkins(expectedSample, expectedSample); @@ -83,9 +85,11 @@ namespace osu.Game.Tests.Gameplay /// 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 + /// hitnormal /// [TestCase("normal-hitnormal2")] [TestCase("normal-hitnormal")] + [TestCase("hitnormal")] public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample) { SetupSkins(string.Empty, expectedSample); @@ -145,6 +149,7 @@ namespace osu.Game.Tests.Gameplay /// [TestCase("normal-hitnormal2")] [TestCase("normal-hitnormal")] + [TestCase("hitnormal")] public void TestControlPointCustomSampleFromBeatmap(string sampleName) { SetupSkins(sampleName, sampleName); From 566c5310bf954c1b7b5b71dceb63d4a904d81162 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jul 2020 21:34:57 +0200 Subject: [PATCH 2463/6909] Add test coverage for taiko sample lookups --- ...o-hitobject-beatmap-custom-sample-bank.osu | 10 ++++ .../TestSceneTaikoHitObjectSamples.cs | 52 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu b/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu new file mode 100644 index 0000000000..f9755782c2 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Resources/SampleLookups/taiko-hitobject-beatmap-custom-sample-bank.osu @@ -0,0 +1,10 @@ +osu file format v14 + +[General] +Mode: 1 + +[TimingPoints] +0,300,4,1,2,100,1,0 + +[HitObjects] +444,320,1000,5,0,0:0:0:0: diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.cs new file mode 100644 index 0000000000..7089ea6619 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectSamples.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.Reflection; +using NUnit.Framework; +using osu.Framework.IO.Stores; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneTaikoHitObjectSamples : HitObjectSampleTest + { + protected override Ruleset CreatePlayerRuleset() => new TaikoRuleset(); + + protected override IResourceStore Resources => new DllResourceStore(Assembly.GetAssembly(typeof(TestSceneTaikoHitObjectSamples))); + + [TestCase("taiko-normal-hitnormal")] + [TestCase("normal-hitnormal")] + [TestCase("hitnormal")] + public void TestDefaultCustomSampleFromBeatmap(string expectedSample) + { + SetupSkins(expectedSample, expectedSample); + + CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu"); + + AssertBeatmapLookup(expectedSample); + } + + [TestCase("taiko-normal-hitnormal")] + [TestCase("normal-hitnormal")] + [TestCase("hitnormal")] + public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample) + { + SetupSkins(string.Empty, expectedSample); + + CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu"); + + AssertUserLookup(expectedSample); + } + + [TestCase("taiko-normal-hitnormal2")] + [TestCase("normal-hitnormal2")] + public void TestUserSkinLookupIgnoresSampleBank(string unwantedSample) + { + SetupSkins(string.Empty, unwantedSample); + + CreateTestWithBeatmap("taiko-hitobject-beatmap-custom-sample-bank.osu"); + + AssertNoLookup(unwantedSample); + } + } +} From 2df5fafea0ed7a537eb51e2a830f851702f90dc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jul 2020 21:39:45 +0200 Subject: [PATCH 2464/6909] Add failing test case --- .../Gameplay/TestSceneHitObjectSamples.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index a70b08a0d3..c3acc2ebe7 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -82,12 +82,11 @@ namespace osu.Game.Tests.Gameplay } /// - /// 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 + /// Tests that a hitobject which provides a custom sample set of 2 retrieves the following samples from the user skin + /// (ignoring the custom sample set index) when the beatmap skin does not contain the sample: /// normal-hitnormal /// hitnormal /// - [TestCase("normal-hitnormal2")] [TestCase("normal-hitnormal")] [TestCase("hitnormal")] public void TestDefaultCustomSampleFromUserSkinFallback(string expectedSample) @@ -99,6 +98,23 @@ namespace osu.Game.Tests.Gameplay AssertUserLookup(expectedSample); } + /// + /// Tests that a hitobject which provides a custom sample set of 2 does not retrieve a normal-hitnormal2 sample from the user skin + /// if the beatmap skin does not contain the sample. + /// User skins in stable ignore the custom sample set index when performing lookups. + /// + [Test] + public void TestUserSkinLookupIgnoresSampleBank() + { + const string unwanted_sample = "normal-hitnormal2"; + + SetupSkins(string.Empty, unwanted_sample); + + CreateTestWithBeatmap("hitobject-beatmap-custom-sample.osu"); + + AssertNoLookup(unwanted_sample); + } + /// /// Tests that a hitobject which provides a sample file retrieves the sample file from the beatmap skin. /// @@ -183,7 +199,7 @@ namespace osu.Game.Tests.Gameplay string[] expectedSamples = { "normal-hitnormal2", - "normal-hitwhistle2" + "normal-hitwhistle" // user skin lookups ignore custom sample set index }; SetupSkins(expectedSamples[0], expectedSamples[1]); From 2bb436fd3c6dd6a6d172c462ed264ae8bc963faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 28 Jul 2020 23:52:09 +0200 Subject: [PATCH 2465/6909] Do not use custom sample banks outside of beatmap skin --- osu.Game/Skinning/LegacyBeatmapSkin.cs | 1 + osu.Game/Skinning/LegacySkin.cs | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index 87bca856a3..d647bc4a2d 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -14,6 +14,7 @@ namespace osu.Game.Skinning public class LegacyBeatmapSkin : LegacySkin { protected override bool AllowManiaSkin => false; + protected override bool UseCustomSampleBanks => true; 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 3bbeff9918..187d601812 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -38,6 +38,12 @@ namespace osu.Game.Skinning protected virtual bool AllowManiaSkin => hasKeyTexture.Value; + /// + /// Whether this skin can use samples with a custom bank (custom sample set in stable terminology). + /// Added in order to match sample lookup logic from stable (in stable, only the beatmap skin could use samples with a custom sample bank). + /// + protected virtual bool UseCustomSampleBanks => false; + public new LegacySkinConfiguration Configuration { get => base.Configuration as LegacySkinConfiguration; @@ -337,7 +343,12 @@ namespace osu.Game.Skinning public override SampleChannel GetSample(ISampleInfo sampleInfo) { - foreach (var lookup in sampleInfo.LookupNames) + var lookupNames = sampleInfo.LookupNames; + + if (sampleInfo is HitSampleInfo hitSample) + lookupNames = getLegacyLookupNames(hitSample); + + foreach (var lookup in lookupNames) { var sample = Samples?.Get(lookup); @@ -361,5 +372,18 @@ namespace osu.Game.Skinning string lastPiece = componentName.Split('/').Last(); yield return componentName.StartsWith("Gameplay/taiko/") ? "taiko-" + lastPiece : lastPiece; } + + private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample) + { + var lookupNames = hitSample.LookupNames; + + if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix)) + // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin. + // using .EndsWith() is intentional as it ensures parity in all edge cases + // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not). + lookupNames = hitSample.LookupNames.Where(name => !name.EndsWith(hitSample.Suffix)); + + return lookupNames; + } } } From 971eafde2b05e51231208eff01f326664f6a7a4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 30 Jul 2020 22:07:07 +0200 Subject: [PATCH 2466/6909] Move fallback to non-bank samples to centralise hackery --- osu.Game/Skinning/LegacySkin.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 187d601812..fc04383a64 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -356,10 +356,6 @@ namespace osu.Game.Skinning return sample; } - if (sampleInfo is HitSampleInfo hsi) - // Try fallback to non-bank samples. - return Samples?.Get(hsi.Name); - return null; } @@ -383,6 +379,11 @@ namespace osu.Game.Skinning // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not). lookupNames = hitSample.LookupNames.Where(name => !name.EndsWith(hitSample.Suffix)); + // also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort. + // going forward specifying banks shall always be required, even for elements that wouldn't require it on stable, + // which is why this is done locally here. + lookupNames = lookupNames.Append(hitSample.Name); + return lookupNames; } } From 8e49256a5c28bac5d93d765c2efc4833e62bc165 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jul 2020 09:03:29 +0900 Subject: [PATCH 2467/6909] Rename and split up statement to make more legible --- .../Drawables/Pieces/DefaultSpinnerDisc.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 2644f425a0..7f65a8c022 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -29,7 +29,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private SpinnerTicks ticks; - private int completeTick; + private int wholeRotationCount; + private SpinnerFill fill; private Container mainContainer; private SpinnerCentreLayer centre; @@ -145,13 +146,24 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces centre.FadeAccent(colour, duration); } - private bool updateCompleteTick() => completeTick != (completeTick = (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360)); + private bool checkNewRotationCount + { + get + { + int rotations = (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360); + + if (wholeRotationCount == rotations) return false; + + wholeRotationCount = rotations; + return true; + } + } protected override void Update() { base.Update(); - if (drawableSpinner.RotationTracker.Complete.Value && updateCompleteTick()) + if (drawableSpinner.RotationTracker.Complete.Value && checkNewRotationCount) { fill.FinishTransforms(false, nameof(Alpha)); fill From cd570433f4aa1779d7bed92aac36e5fd368b6546 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jul 2020 09:04:20 +0900 Subject: [PATCH 2468/6909] Move private methods to bottom of class --- .../Drawables/Pieces/DefaultSpinnerDisc.cs | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 7f65a8c022..81dea8d1c1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -96,6 +96,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces drawableSpinner.State.BindValueChanged(updateStateTransforms, true); } + protected override void Update() + { + base.Update(); + + if (drawableSpinner.RotationTracker.Complete.Value && checkNewRotationCount) + { + fill.FinishTransforms(false, nameof(Alpha)); + fill + .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo) + .Then() + .FadeTo(tracking_alpha, 250, Easing.OutQuint); + } + + const float initial_scale = 0.2f; + float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress; + + fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); + mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation; + } + private void updateStateTransforms(ValueChangedEvent state) { centre.ScaleTo(0); @@ -159,24 +179,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - protected override void Update() - { - base.Update(); - - if (drawableSpinner.RotationTracker.Complete.Value && checkNewRotationCount) - { - fill.FinishTransforms(false, nameof(Alpha)); - fill - .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo) - .Then() - .FadeTo(tracking_alpha, 250, Easing.OutQuint); - } - - const float initial_scale = 0.2f; - float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress; - - fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); - mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation; - } } } From 86784e30ad4cea26f205370fc5c2781ce9802e6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jul 2020 09:54:30 +0900 Subject: [PATCH 2469/6909] Fix spacing --- .../Objects/Drawables/Pieces/DefaultSpinnerDisc.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 81dea8d1c1..79bfeedd07 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -178,6 +178,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return true; } } - } } From fb74195d83be6b7361740e3584732a52c8795622 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jul 2020 10:43:54 +0900 Subject: [PATCH 2470/6909] Move InputManager implementation to base skinnable test scene class --- .../OsuSkinnableTestScene.cs | 15 +++++++++++++++ osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs | 14 -------------- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 2 -- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs index a0a38fc47b..cad98185ce 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuSkinnableTestScene.cs @@ -1,12 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests { public abstract class OsuSkinnableTestScene : SkinnableTestScene { + private Container content; + + protected override Container Content + { + get + { + if (content == null) + base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); + + return content; + } + } + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index a9404f665a..6a689a1f80 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -26,19 +25,6 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class TestSceneSlider : OsuSkinnableTestScene { - private Container content; - - protected override Container Content - { - get - { - if (content == null) - base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); - - return content; - } - } - private int depthIndex; public TestSceneSlider() diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 65b338882e..b57561f3e1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -19,8 +19,6 @@ namespace osu.Game.Rulesets.Osu.Tests public TestSceneSpinner() { - // base.Content.Add(content = new OsuInputManager(new RulesetInfo { ID = 0 })); - AddStep("Miss Big", () => SetContents(() => testSingle(2))); AddStep("Miss Medium", () => SetContents(() => testSingle(5))); AddStep("Miss Small", () => SetContents(() => testSingle(7))); From 6452d62249f1252034fd7c8d85c3929e692c1336 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jul 2020 12:52:03 +0900 Subject: [PATCH 2471/6909] 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 0ac766926c..13b4b6ebbb 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 1ececa448c..745555e0e2 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 ef5ba10d17..f1080f0c8b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 186b452331437ba41ef07d2559b1b28314d89933 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jul 2020 14:48:56 +0900 Subject: [PATCH 2472/6909] Apply common multiplication refactor --- osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 618cb0e9ad..72bc3ddc9a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -93,8 +93,7 @@ namespace osu.Game.Rulesets.Osu.Skinning spinningMiddle.Rotation = discTop.Rotation = drawableSpinner.RotationTracker.Rotation; discBottom.Rotation = discTop.Rotation / 3; - Scale = new Vector2(final_scale * 0.8f - + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * (final_scale * 0.2f)); + Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * 0.2f)); } } } From 62ba214dad3d9cd99b760a1bed13e04ea78bd97a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jul 2020 16:21:47 +0900 Subject: [PATCH 2473/6909] Use OrderByDescending --- osu.Game/Rulesets/RulesetStore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 837796287a..dd43092c0d 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -69,8 +69,8 @@ namespace osu.Game.Rulesets // Given name is always going to be equally-or-more qualified than the assembly name. .Where(a => args.Name.Contains(a.GetName().Name, StringComparison.Ordinal)) // Pick the greatest assembly version. - .OrderBy(a => a.GetName().Version) - .LastOrDefault(); + .OrderByDescending(a => a.GetName().Version) + .FirstOrDefault(); if (domainAssembly != null) return domainAssembly; From 88e179d8aa684a1ef8b9971f74cad059f20e6ce3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 17:40:58 +0900 Subject: [PATCH 2474/6909] Split out index-only response --- .../TestSceneTimeshiftResultsScreen.cs | 2 +- .../Multiplayer/IndexPlaylistScoresRequest.cs | 2 +- .../Multiplayer/IndexedMultiplayerScores.cs | 27 +++++++++++++++++++ .../Online/Multiplayer/MultiplayerScores.cs | 12 --------- 4 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 44ca676c4f..8b6d6694c3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -111,7 +111,7 @@ namespace osu.Game.Tests.Visual.Multiplayer void success() { - r.TriggerSuccess(new MultiplayerScores { Scores = roomScores }); + r.TriggerSuccess(new IndexedMultiplayerScores { Scores = roomScores }); roomsReceived = true; } diff --git a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs index 67793df344..91f24933e1 100644 --- a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs @@ -12,7 +12,7 @@ namespace osu.Game.Online.Multiplayer /// /// Returns a list of scores for the specified playlist item. /// - public class IndexPlaylistScoresRequest : APIRequest + public class IndexPlaylistScoresRequest : APIRequest { private readonly int roomId; private readonly int playlistItemId; diff --git a/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs b/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs new file mode 100644 index 0000000000..e237b7e3fb --- /dev/null +++ b/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.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 JetBrains.Annotations; +using Newtonsoft.Json; + +namespace osu.Game.Online.Multiplayer +{ + /// + /// A object returned via a . + /// + public class IndexedMultiplayerScores : MultiplayerScores + { + /// + /// The total scores in the playlist item. + /// + [JsonProperty("total")] + public int? TotalScores { get; set; } + + /// + /// The user's score, if any. + /// + [JsonProperty("user_score")] + [CanBeNull] + public MultiplayerScore UserScore { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerScores.cs b/osu.Game/Online/Multiplayer/MultiplayerScores.cs index 2d0f98e032..8b8dd9e48d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScores.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerScores.cs @@ -18,18 +18,6 @@ namespace osu.Game.Online.Multiplayer [JsonProperty("scores")] public List Scores { get; set; } - /// - /// The total scores in the playlist item. Only provided via . - /// - [JsonProperty("total")] - public int? TotalScores { get; set; } - - /// - /// The user's score, if any. Only provided via . - /// - [JsonProperty("user_score")] - public MultiplayerScore UserScore { get; set; } - /// /// The parameters to be used to fetch the next page. /// From eadef53e68cd1612f8bb0b0a6d7260f5631ad07f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 17:43:40 +0900 Subject: [PATCH 2475/6909] Add more annotations --- osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs b/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs index e83cc1b753..2ac62d0300 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerScoresAround.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 Newtonsoft.Json; namespace osu.Game.Online.Multiplayer @@ -14,12 +15,14 @@ namespace osu.Game.Online.Multiplayer /// Scores sorted "higher" than the user's score, depending on the sorting order. ///
    [JsonProperty("higher")] + [CanBeNull] public MultiplayerScores Higher { get; set; } /// /// Scores sorted "lower" than the user's score, depending on the sorting order. /// [JsonProperty("lower")] + [CanBeNull] public MultiplayerScores Lower { get; set; } } } From 6d728d27fc7d150c313ec442b810772f174995cf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 17:58:48 +0900 Subject: [PATCH 2476/6909] Cleanup/consolidate indexing request --- .../Multi/Ranking/TimeshiftResultsScreen.cs | 52 +++++++++++-------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 648bee385c..0ef4953e83 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -75,21 +76,8 @@ namespace osu.Game.Screens.Multi.Ranking performSuccessCallback(scoresCallback, allScores); }; - userScoreReq.Failure += _ => - { - // Fallback to a normal index. - var indexReq = new IndexPlaylistScoresRequest(roomId, playlistItem.ID); - - indexReq.Success += r => - { - performSuccessCallback(scoresCallback, r.Scores); - lowerScores = r; - }; - - indexReq.Failure += __ => loadingLayer.Hide(); - - api.Queue(indexReq); - }; + // On failure, fallback to a normal index. + userScoreReq.Failure += _ => api.Queue(createIndexRequest(scoresCallback)); return userScoreReq; } @@ -103,16 +91,30 @@ namespace osu.Game.Screens.Multi.Ranking if (pivot?.Cursor == null) return null; - var indexReq = new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params); + return createIndexRequest(scoresCallback, pivot); + } + + /// + /// Creates a with an optional score pivot. + /// + /// Does not queue the request. + /// The callback to perform with the resulting scores. + /// An optional score pivot to retrieve scores around. Can be null to retrieve scores from the highest score. + /// The indexing . + private APIRequest createIndexRequest(Action> scoresCallback, [CanBeNull] MultiplayerScores pivot = null) + { + var indexReq = pivot != null + ? new IndexPlaylistScoresRequest(roomId, playlistItem.ID, pivot.Cursor, pivot.Params) + : new IndexPlaylistScoresRequest(roomId, playlistItem.ID); indexReq.Success += r => { - if (direction == -1) - higherScores = r; - else + if (pivot == lowerScores) lowerScores = r; + else + higherScores = r; - performSuccessCallback(scoresCallback, r.Scores); + performSuccessCallback(scoresCallback, r.Scores, pivot); }; indexReq.Failure += _ => loadingLayer.Hide(); @@ -120,7 +122,13 @@ namespace osu.Game.Screens.Multi.Ranking return indexReq; } - private void performSuccessCallback(Action> callback, List scores) + /// + /// Transforms returned into s, ensure the is put into a sane state, and invokes a given success callback. + /// + /// The callback to invoke with the final s. + /// The s that were retrieved from s. + /// An optional pivot around which the scores were retrieved. + private void performSuccessCallback([NotNull] Action> callback, [NotNull] List scores, [CanBeNull] MultiplayerScores pivot = null) { var scoreInfos = new List(scores.Select(s => s.CreateScoreInfo(playlistItem))); @@ -136,7 +144,7 @@ namespace osu.Game.Screens.Multi.Ranking } // Invoke callback to add the scores. Exclude the user's current score which was added previously. - callback?.Invoke(scoreInfos.Where(s => s.ID != Score?.OnlineScoreID)); + callback.Invoke(scoreInfos.Where(s => s.ID != Score?.OnlineScoreID)); loadingLayer.Hide(); } From 9966d4f3b3b5da3c364157ab38dbadd0343152ae Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 19:57:05 +0900 Subject: [PATCH 2477/6909] Add more loading spinners --- .../Multi/Ranking/TimeshiftResultsScreen.cs | 81 ++++++++++++++++--- osu.Game/Screens/Ranking/ResultsScreen.cs | 30 +++---- osu.Game/Screens/Ranking/ScorePanelList.cs | 10 +++ 3 files changed, 97 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 0ef4953e83..87de9fd72a 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -22,7 +22,9 @@ namespace osu.Game.Screens.Multi.Ranking private readonly int roomId; private readonly PlaylistItem playlistItem; - private LoadingSpinner loadingLayer; + private LoadingSpinner leftLoadingLayer; + private LoadingSpinner centreLoadingLayer; + private LoadingSpinner rightLoadingLayer; private MultiplayerScores higherScores; private MultiplayerScores lowerScores; @@ -39,13 +41,29 @@ namespace osu.Game.Screens.Multi.Ranking [BackgroundDependencyLoader] private void load() { - AddInternal(loadingLayer = new LoadingLayer + AddInternal(new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - X = -10, - State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden }, - Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y } + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y }, + Children = new Drawable[] + { + leftLoadingLayer = new PanelListLoadingSpinner(ScorePanelList) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + }, + centreLoadingLayer = new PanelListLoadingSpinner(ScorePanelList) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden }, + }, + rightLoadingLayer = new PanelListLoadingSpinner(ScorePanelList) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.Centre, + }, + } }); } @@ -91,6 +109,11 @@ namespace osu.Game.Screens.Multi.Ranking if (pivot?.Cursor == null) return null; + if (pivot == higherScores) + leftLoadingLayer.Show(); + else + rightLoadingLayer.Show(); + return createIndexRequest(scoresCallback, pivot); } @@ -114,10 +137,10 @@ namespace osu.Game.Screens.Multi.Ranking else higherScores = r; - performSuccessCallback(scoresCallback, r.Scores, pivot); + performSuccessCallback(scoresCallback, r.Scores, r); }; - indexReq.Failure += _ => loadingLayer.Hide(); + indexReq.Failure += _ => hideLoadingSpinners(pivot); return indexReq; } @@ -146,7 +169,45 @@ namespace osu.Game.Screens.Multi.Ranking // Invoke callback to add the scores. Exclude the user's current score which was added previously. callback.Invoke(scoreInfos.Where(s => s.ID != Score?.OnlineScoreID)); - loadingLayer.Hide(); + hideLoadingSpinners(pivot); + } + + private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null) + { + centreLoadingLayer.Hide(); + + if (pivot == lowerScores) + rightLoadingLayer.Hide(); + else if (pivot == higherScores) + leftLoadingLayer.Hide(); + } + + private class PanelListLoadingSpinner : LoadingSpinner + { + private readonly ScorePanelList list; + + /// + /// Creates a new . + /// + /// The list to track. + /// Whether the spinner should have a surrounding black box for visibility. + public PanelListLoadingSpinner(ScorePanelList list, bool withBox = true) + : base(withBox) + { + this.list = list; + } + + protected override void Update() + { + base.Update(); + + float panelOffset = list.DrawWidth / 2 - ScorePanel.EXPANDED_WIDTH; + + if ((Anchor & Anchor.x0) > 0) + X = (float)(panelOffset - list.Current); + else if ((Anchor & Anchor.x2) > 0) + X = (float)(list.ScrollableExtent - list.Current - panelOffset); + } } } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 254ab76f5b..2506a63cc0 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -37,7 +37,8 @@ namespace osu.Game.Screens.Ranking public readonly Bindable SelectedScore = new Bindable(); public readonly ScoreInfo Score; - private readonly bool allowRetry; + + protected ScorePanelList ScorePanelList { get; private set; } [Resolved(CanBeNull = true)] private Player player { get; set; } @@ -47,12 +48,13 @@ namespace osu.Game.Screens.Ranking private StatisticsPanel statisticsPanel; private Drawable bottomPanel; - private ScorePanelList scorePanelList; private Container detachedPanelContainer; private bool fetchedInitialScores; private APIRequest nextPageRequest; + private readonly bool allowRetry; + protected ResultsScreen(ScoreInfo score, bool allowRetry = true) { Score = score; @@ -87,7 +89,7 @@ namespace osu.Game.Screens.Ranking RelativeSizeAxes = Axes.Both, Score = { BindTarget = SelectedScore } }, - scorePanelList = new ScorePanelList + ScorePanelList = new ScorePanelList { RelativeSizeAxes = Axes.Both, SelectedScore = { BindTarget = SelectedScore }, @@ -145,7 +147,7 @@ namespace osu.Game.Screens.Ranking }; if (Score != null) - scorePanelList.AddScore(Score); + ScorePanelList.AddScore(Score); if (player != null && allowRetry) { @@ -181,9 +183,9 @@ namespace osu.Game.Screens.Ranking if (fetchedInitialScores && nextPageRequest == null) { - if (scorePanelList.IsScrolledToStart) + if (ScorePanelList.IsScrolledToStart) nextPageRequest = FetchNextPage(-1, fetchScoresCallback); - else if (scorePanelList.IsScrolledToEnd) + else if (ScorePanelList.IsScrolledToEnd) nextPageRequest = FetchNextPage(1, fetchScoresCallback); if (nextPageRequest != null) @@ -249,7 +251,7 @@ namespace osu.Game.Screens.Ranking private void addScore(ScoreInfo score) { - var panel = scorePanelList.AddScore(score); + var panel = ScorePanelList.AddScore(score); if (detachedPanel != null) panel.Alpha = 0; @@ -262,11 +264,11 @@ namespace osu.Game.Screens.Ranking if (state.NewValue == Visibility.Visible) { // Detach the panel in its original location, and move into the desired location in the local container. - var expandedPanel = scorePanelList.GetPanelForScore(SelectedScore.Value); + var expandedPanel = ScorePanelList.GetPanelForScore(SelectedScore.Value); var screenSpacePos = expandedPanel.ScreenSpaceDrawQuad.TopLeft; // Detach and move into the local container. - scorePanelList.Detach(expandedPanel); + ScorePanelList.Detach(expandedPanel); detachedPanelContainer.Add(expandedPanel); // Move into its original location in the local container first, then to the final location. @@ -276,9 +278,9 @@ namespace osu.Game.Screens.Ranking .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)) + foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) contracted.FadeOut(150, Easing.OutQuint); - scorePanelList.HandleInput = false; + ScorePanelList.HandleInput = false; // Dim background. Background.FadeTo(0.1f, 150); @@ -291,7 +293,7 @@ namespace osu.Game.Screens.Ranking // Remove from the local container and re-attach. detachedPanelContainer.Remove(detachedPanel); - scorePanelList.Attach(detachedPanel); + ScorePanelList.Attach(detachedPanel); // Move into its original location in the attached container first, then to the final location. var origLocation = detachedPanel.Parent.ToLocalSpace(screenSpacePos); @@ -300,9 +302,9 @@ namespace osu.Game.Screens.Ranking .MoveTo(new Vector2(0, origLocation.Y), 150, Easing.OutQuint); // Show contracted panels. - foreach (var contracted in scorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) + foreach (var contracted in ScorePanelList.GetScorePanels().Where(p => p.State == PanelState.Contracted)) contracted.FadeIn(150, Easing.OutQuint); - scorePanelList.HandleInput = true; + ScorePanelList.HandleInput = true; // Un-dim background. Background.FadeTo(0.5f, 150); diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index b2e1e91831..10bd99c8ce 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -41,6 +41,16 @@ namespace osu.Game.Screens.Ranking ///
    public bool IsScrolledToEnd => flow.Count > 0 && scroll.ScrollableExtent > 0 && scroll.IsScrolledToEnd(scroll_endpoint_distance); + /// + /// The current scroll position. + /// + public double Current => scroll.Current; + + /// + /// The scrollable extent. + /// + public double ScrollableExtent => scroll.ScrollableExtent; + /// /// An action to be invoked if a is clicked while in an expanded state. /// From 4d2a677080beaed2a64c6ece28c663cf4d9f3aff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jul 2020 20:33:18 +0900 Subject: [PATCH 2478/6909] Fix next track starting before previous one is paused Closes #9651. --- osu.Game/Overlays/MusicController.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 546f7a1ec4..212d4d4850 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -262,7 +262,11 @@ namespace osu.Game.Overlays { if (beatmap is Bindable working) working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); - beatmap.Value.Track.Restart(); + + // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase). + // we probably want to move this to a central method for switching to a new working beatmap in the future. + Schedule(() => beatmap.Value.Track.Restart()); + return true; } From 8e8a11bb72f558c0fb8144390bdab7e5d928ea73 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 20:55:26 +0900 Subject: [PATCH 2479/6909] Add APIRequest.TriggerFailure() for testing --- osu.Game/Online/API/APIRequest.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 2115326cc2..6912d9b629 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -136,6 +136,11 @@ namespace osu.Game.Online.API Success?.Invoke(); } + internal void TriggerFailure(Exception e) + { + Failure?.Invoke(e); + } + public void Cancel() => Fail(new OperationCanceledException(@"Request cancelled")); public void Fail(Exception e) @@ -166,7 +171,7 @@ namespace osu.Game.Online.API } Logger.Log($@"Failing request {this} ({e})", LoggingTarget.Network); - pendingFailure = () => Failure?.Invoke(e); + pendingFailure = () => TriggerFailure(e); checkAndScheduleFailure(); } From 2b77f99f56af81771980d77e8e5e57f02ff60f59 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 20:55:44 +0900 Subject: [PATCH 2480/6909] Initialise some response parameters --- osu.Game/Online/API/Requests/Cursor.cs | 2 +- osu.Game/Online/Multiplayer/IndexScoresParams.cs | 2 +- osu.Game/Online/Multiplayer/MultiplayerScores.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/Requests/Cursor.cs b/osu.Game/Online/API/Requests/Cursor.cs index f21445ca32..3de8db770c 100644 --- a/osu.Game/Online/API/Requests/Cursor.cs +++ b/osu.Game/Online/API/Requests/Cursor.cs @@ -15,6 +15,6 @@ namespace osu.Game.Online.API.Requests { [UsedImplicitly] [JsonExtensionData] - public IDictionary Properties; + public IDictionary Properties { get; set; } = new Dictionary(); } } diff --git a/osu.Game/Online/Multiplayer/IndexScoresParams.cs b/osu.Game/Online/Multiplayer/IndexScoresParams.cs index 8160dfefaf..a511e9a780 100644 --- a/osu.Game/Online/Multiplayer/IndexScoresParams.cs +++ b/osu.Game/Online/Multiplayer/IndexScoresParams.cs @@ -15,6 +15,6 @@ namespace osu.Game.Online.Multiplayer { [UsedImplicitly] [JsonExtensionData] - public IDictionary Properties; + public IDictionary Properties { get; set; } = new Dictionary(); } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerScores.cs b/osu.Game/Online/Multiplayer/MultiplayerScores.cs index 8b8dd9e48d..7b9dcff828 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScores.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerScores.cs @@ -16,12 +16,12 @@ namespace osu.Game.Online.Multiplayer /// The scores. ///
    [JsonProperty("scores")] - public List Scores { get; set; } + public List Scores { get; set; } = new List(); /// /// The parameters to be used to fetch the next page. /// [JsonProperty("params")] - public IndexScoresParams Params { get; set; } + public IndexScoresParams Params { get; set; } = new IndexScoresParams(); } } From a4a4c8761241eabdf82a35fe02686aa5453c1d6e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 21:21:48 +0900 Subject: [PATCH 2481/6909] Fix incorrect score id being used --- osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 87de9fd72a..b0cf63a7a9 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -167,7 +167,7 @@ namespace osu.Game.Screens.Multi.Ranking } // Invoke callback to add the scores. Exclude the user's current score which was added previously. - callback.Invoke(scoreInfos.Where(s => s.ID != Score?.OnlineScoreID)); + callback.Invoke(scoreInfos.Where(s => s.OnlineScoreID != Score?.OnlineScoreID)); hideLoadingSpinners(pivot); } From 17018ffa8b616fe8da85c7acce17ad3744c9c018 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 21:33:04 +0900 Subject: [PATCH 2482/6909] Fix potentially triggering new requests too early --- osu.Game/Screens/Ranking/ResultsScreen.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 2506a63cc0..c95cf1066e 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -190,8 +190,9 @@ namespace osu.Game.Screens.Ranking if (nextPageRequest != null) { - nextPageRequest.Success += () => nextPageRequest = null; - nextPageRequest.Failure += _ => nextPageRequest = null; + // Scheduled after children to give the list a chance to update its scroll position and not potentially trigger a second request too early. + nextPageRequest.Success += () => ScheduleAfterChildren(() => nextPageRequest = null); + nextPageRequest.Failure += _ => ScheduleAfterChildren(() => nextPageRequest = null); api.Queue(nextPageRequest); } From f1e721e396988d5410b32fa3402b612f59dca23d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 21:39:50 +0900 Subject: [PATCH 2483/6909] Rewrite test scene and add more tests --- .../TestSceneTimeshiftResultsScreen.cs | 344 +++++++++++++++--- .../Multiplayer/IndexPlaylistScoresRequest.cs | 31 +- .../Multi/Ranking/TimeshiftResultsScreen.cs | 23 +- 3 files changed, 329 insertions(+), 69 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 8b6d6694c3..628d08a314 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -3,14 +3,24 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Net; using System.Threading.Tasks; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; using NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; 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.Screens.Ranking; using osu.Game.Tests.Beatmaps; using osu.Game.Users; @@ -18,43 +28,134 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneTimeshiftResultsScreen : ScreenTestScene { - private bool roomsReceived; + private const int scores_per_result = 10; + + private TestResultsScreen resultsScreen; + private int currentScoreId; + private bool requestComplete; [SetUp] public void Setup() => Schedule(() => { - roomsReceived = false; + currentScoreId = 0; + requestComplete = false; bindHandler(); }); [Test] - public void TestShowResultsWithScore() + public void TestShowWithUserScore() { - createResults(new TestScoreInfo(new OsuRuleset().RulesetInfo)); - AddWaitStep("wait for display", 5); + ScoreInfo userScore = null; + + AddStep("bind user score info handler", () => + { + userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ }; + bindHandler(userScore: userScore); + }); + + createResults(() => userScore); + waitForDisplay(); + + AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded); } [Test] - public void TestShowResultsNullScore() + public void TestShowNullUserScore() { - createResults(null); - AddWaitStep("wait for display", 5); + createResults(); + waitForDisplay(); + + AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } [Test] - public void TestShowResultsNullScoreWithDelay() + public void TestShowUserScoreWithDelay() + { + ScoreInfo userScore = null; + + AddStep("bind user score info handler", () => + { + userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ }; + bindHandler(3000, userScore); + }); + + createResults(() => userScore); + waitForDisplay(); + + AddAssert("more than 1 panel displayed", () => this.ChildrenOfType().Count() > 1); + AddAssert("user score selected", () => this.ChildrenOfType().Single(p => p.Score.OnlineScoreID == userScore.OnlineScoreID).State == PanelState.Expanded); + } + + [Test] + public void TestShowNullUserScoreWithDelay() { AddStep("bind delayed handler", () => bindHandler(3000)); - createResults(null); - AddUntilStep("wait for rooms to be received", () => roomsReceived); - AddWaitStep("wait for display", 5); + + createResults(); + waitForDisplay(); + + AddAssert("top score selected", () => this.ChildrenOfType().OrderByDescending(p => p.Score.TotalScore).First().State == PanelState.Expanded); } - private void createResults(ScoreInfo score) + [Test] + public void TestFetchWhenScrolledToTheRight() + { + createResults(); + waitForDisplay(); + + AddStep("bind delayed handler", () => bindHandler(3000)); + + for (int i = 0; i < 2; i++) + { + int beforePanelCount = 0; + + AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); + AddStep("scroll to right", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToEnd(false)); + + AddAssert("right loading spinner shown", () => resultsScreen.RightSpinner.State.Value == Visibility.Visible); + waitForDisplay(); + + AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); + AddAssert("right loading spinner hidden", () => resultsScreen.RightSpinner.State.Value == Visibility.Hidden); + } + } + + [Test] + public void TestFetchWhenScrolledToTheLeft() + { + ScoreInfo userScore = null; + + AddStep("bind user score info handler", () => + { + userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ }; + bindHandler(userScore: userScore); + }); + + createResults(() => userScore); + waitForDisplay(); + + AddStep("bind delayed handler", () => bindHandler(3000)); + + for (int i = 0; i < 2; i++) + { + int beforePanelCount = 0; + + AddStep("get panel count", () => beforePanelCount = this.ChildrenOfType().Count()); + AddStep("scroll to left", () => resultsScreen.ScorePanelList.ChildrenOfType().Single().ScrollToStart(false)); + + AddAssert("left loading spinner shown", () => resultsScreen.LeftSpinner.State.Value == Visibility.Visible); + waitForDisplay(); + + AddAssert($"count increased by {scores_per_result}", () => this.ChildrenOfType().Count() == beforePanelCount + scores_per_result); + AddAssert("left loading spinner hidden", () => resultsScreen.LeftSpinner.State.Value == Visibility.Hidden); + } + } + + private void createResults(Func getScore = null) { AddStep("load results", () => { - LoadScreen(new TimeshiftResultsScreen(score, 1, new PlaylistItem + LoadScreen(resultsScreen = new TestResultsScreen(getScore?.Invoke(), 1, new PlaylistItem { Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, Ruleset = { Value = new OsuRuleset().RulesetInfo } @@ -62,62 +163,213 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } - private void bindHandler(double delay = 0) + private void waitForDisplay() { - var roomScores = new List(); + AddUntilStep("wait for request to complete", () => requestComplete); + AddWaitStep("wait for display", 5); + } - for (int i = 0; i < 10; i++) + private void bindHandler(double delay = 0, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request => + { + requestComplete = false; + + if (failRequests) { - roomScores.Add(new MultiplayerScore + triggerFail(request, delay); + return; + } + + switch (request) + { + case ShowPlaylistUserScoreRequest s: + if (userScore == null) + triggerFail(s, delay); + else + triggerSuccess(s, createUserResponse(userScore), delay); + break; + + case IndexPlaylistScoresRequest i: + triggerSuccess(i, createIndexResponse(i), delay); + break; + } + }; + + private void triggerSuccess(APIRequest req, T result, double delay) + where T : class + { + if (delay == 0) + success(); + else + { + Task.Run(async () => { - ID = i, - Accuracy = 0.9 - 0.01 * i, - EndedAt = DateTimeOffset.Now.Subtract(TimeSpan.FromHours(i)), + await Task.Delay(TimeSpan.FromMilliseconds(delay)); + Schedule(success); + }); + } + + void success() + { + requestComplete = true; + req.TriggerSuccess(result); + } + } + + private void triggerFail(APIRequest req, double delay) + { + if (delay == 0) + fail(); + else + { + Task.Run(async () => + { + await Task.Delay(TimeSpan.FromMilliseconds(delay)); + Schedule(fail); + }); + } + + void fail() + { + requestComplete = true; + req.TriggerFailure(new WebException("Failed.")); + } + } + + private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore) + { + var multiplayerUserScore = new MultiplayerScore + { + ID = (int)(userScore.OnlineScoreID ?? currentScoreId++), + Accuracy = userScore.Accuracy, + EndedAt = userScore.Date, + Passed = userScore.Passed, + Rank = userScore.Rank, + MaxCombo = userScore.MaxCombo, + TotalScore = userScore.TotalScore, + User = userScore.User, + Statistics = userScore.Statistics, + ScoresAround = new MultiplayerScoresAround + { + Higher = new MultiplayerScores(), + Lower = new MultiplayerScores() + } + }; + + for (int i = 1; i <= scores_per_result; i++) + { + multiplayerUserScore.ScoresAround.Lower.Scores.Add(new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = userScore.Accuracy, + EndedAt = userScore.Date, Passed = true, - Rank = ScoreRank.B, - MaxCombo = 999, - TotalScore = 999999 - i * 1000, + Rank = userScore.Rank, + MaxCombo = userScore.MaxCombo, + TotalScore = userScore.TotalScore - i, User = new User { Id = 2, Username = $"peppy{i}", CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }, - Statistics = + Statistics = userScore.Statistics + }); + + multiplayerUserScore.ScoresAround.Higher.Scores.Add(new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = userScore.Accuracy, + EndedAt = userScore.Date, + Passed = true, + Rank = userScore.Rank, + MaxCombo = userScore.MaxCombo, + TotalScore = userScore.TotalScore + i, + User = new User + { + Id = 2, + Username = $"peppy{i}", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, + Statistics = userScore.Statistics + }); + } + + addCursor(multiplayerUserScore.ScoresAround.Lower); + addCursor(multiplayerUserScore.ScoresAround.Higher); + + return multiplayerUserScore; + } + + private IndexedMultiplayerScores createIndexResponse(IndexPlaylistScoresRequest req) + { + var result = new IndexedMultiplayerScores(); + + long startTotalScore = req.Cursor?.Properties["total_score"].ToObject() ?? 1000000; + string sort = req.IndexParams?.Properties["sort"].ToObject() ?? "score_desc"; + + for (int i = 1; i <= scores_per_result; i++) + { + result.Scores.Add(new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.X, + MaxCombo = 1000, + TotalScore = startTotalScore + (sort == "score_asc" ? i : -i), + User = new User + { + Id = 2, + Username = $"peppy{i}", + CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }, + Statistics = new Dictionary { { HitResult.Miss, 1 }, { HitResult.Meh, 50 }, { HitResult.Good, 100 }, - { HitResult.Great, 300 }, + { HitResult.Great, 300 } } }); } - ((DummyAPIAccess)API).HandleRequest = request => + addCursor(result); + + return result; + } + + private void addCursor(MultiplayerScores scores) + { + scores.Cursor = new Cursor { - switch (request) + Properties = new Dictionary { - case IndexPlaylistScoresRequest r: - if (delay == 0) - success(); - else - { - Task.Run(async () => - { - await Task.Delay(TimeSpan.FromMilliseconds(delay)); - Schedule(success); - }); - } + { "total_score", JToken.FromObject(scores.Scores[^1].TotalScore) }, + { "score_id", JToken.FromObject(scores.Scores[^1].ID) }, + } + }; - void success() - { - r.TriggerSuccess(new IndexedMultiplayerScores { Scores = roomScores }); - roomsReceived = true; - } - - break; + scores.Params = new IndexScoresParams + { + Properties = new Dictionary + { + { "sort", JToken.FromObject(scores.Scores[^1].TotalScore > scores.Scores[^2].TotalScore ? "score_asc" : "score_desc") } } }; } + + private class TestResultsScreen : TimeshiftResultsScreen + { + public new LoadingSpinner LeftSpinner => base.LeftSpinner; + public new LoadingSpinner CentreSpinner => base.CentreSpinner; + public new LoadingSpinner RightSpinner => base.RightSpinner; + public new ScorePanelList ScorePanelList => base.ScorePanelList; + + public TestResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true) + : base(score, roomId, playlistItem, allowRetry) + { + } + } } } diff --git a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs index 91f24933e1..684d0aecd8 100644 --- a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.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 JetBrains.Annotations; using osu.Framework.IO.Network; using osu.Game.Extensions; @@ -14,39 +15,45 @@ namespace osu.Game.Online.Multiplayer ///
    public class IndexPlaylistScoresRequest : APIRequest { - private readonly int roomId; - private readonly int playlistItemId; - private readonly Cursor cursor; - private readonly IndexScoresParams indexParams; + public readonly int RoomId; + public readonly int PlaylistItemId; + + [CanBeNull] + public readonly Cursor Cursor; + + [CanBeNull] + public readonly IndexScoresParams IndexParams; public IndexPlaylistScoresRequest(int roomId, int playlistItemId) { - this.roomId = roomId; - this.playlistItemId = playlistItemId; + RoomId = roomId; + PlaylistItemId = playlistItemId; } public IndexPlaylistScoresRequest(int roomId, int playlistItemId, [NotNull] Cursor cursor, [NotNull] IndexScoresParams indexParams) : this(roomId, playlistItemId) { - this.cursor = cursor; - this.indexParams = indexParams; + Cursor = cursor; + IndexParams = indexParams; } protected override WebRequest CreateWebRequest() { var req = base.CreateWebRequest(); - if (cursor != null) + if (Cursor != null) { - req.AddCursor(cursor); + Debug.Assert(IndexParams != null); - foreach (var (key, value) in indexParams.Properties) + req.AddCursor(Cursor); + + foreach (var (key, value) in IndexParams.Properties) req.AddParameter(key, value.ToString()); } return req; } - protected override string Target => $@"rooms/{roomId}/playlist/{playlistItemId}/scores"; + protected override string Target => $@"rooms/{RoomId}/playlist/{PlaylistItemId}/scores"; } } diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index b0cf63a7a9..e212bd4a82 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -22,9 +22,10 @@ namespace osu.Game.Screens.Multi.Ranking private readonly int roomId; private readonly PlaylistItem playlistItem; - private LoadingSpinner leftLoadingLayer; - private LoadingSpinner centreLoadingLayer; - private LoadingSpinner rightLoadingLayer; + protected LoadingSpinner LeftSpinner { get; private set; } + protected LoadingSpinner CentreSpinner { get; private set; } + protected LoadingSpinner RightSpinner { get; private set; } + private MultiplayerScores higherScores; private MultiplayerScores lowerScores; @@ -47,18 +48,18 @@ namespace osu.Game.Screens.Multi.Ranking Padding = new MarginPadding { Bottom = TwoLayerButton.SIZE_EXTENDED.Y }, Children = new Drawable[] { - leftLoadingLayer = new PanelListLoadingSpinner(ScorePanelList) + LeftSpinner = new PanelListLoadingSpinner(ScorePanelList) { Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, }, - centreLoadingLayer = new PanelListLoadingSpinner(ScorePanelList) + CentreSpinner = new PanelListLoadingSpinner(ScorePanelList) { Anchor = Anchor.Centre, Origin = Anchor.Centre, State = { Value = Score == null ? Visibility.Visible : Visibility.Hidden }, }, - rightLoadingLayer = new PanelListLoadingSpinner(ScorePanelList) + RightSpinner = new PanelListLoadingSpinner(ScorePanelList) { Anchor = Anchor.CentreRight, Origin = Anchor.Centre, @@ -110,9 +111,9 @@ namespace osu.Game.Screens.Multi.Ranking return null; if (pivot == higherScores) - leftLoadingLayer.Show(); + LeftSpinner.Show(); else - rightLoadingLayer.Show(); + RightSpinner.Show(); return createIndexRequest(scoresCallback, pivot); } @@ -174,12 +175,12 @@ namespace osu.Game.Screens.Multi.Ranking private void hideLoadingSpinners([CanBeNull] MultiplayerScores pivot = null) { - centreLoadingLayer.Hide(); + CentreSpinner.Hide(); if (pivot == lowerScores) - rightLoadingLayer.Hide(); + RightSpinner.Hide(); else if (pivot == higherScores) - leftLoadingLayer.Hide(); + LeftSpinner.Hide(); } private class PanelListLoadingSpinner : LoadingSpinner From e8f75a78e8ce4d820d72d05efacc6cec70f51264 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 31 Jul 2020 22:02:12 +0900 Subject: [PATCH 2484/6909] Also fix second instance of same execution --- osu.Game/Overlays/MusicController.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 212d4d4850..a990f9a6ab 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -236,8 +236,8 @@ namespace osu.Game.Overlays { if (beatmap is Bindable working) working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); - beatmap.Value.Track.Restart(); + restartTrack(); return PreviousTrackResult.Previous; } @@ -263,16 +263,20 @@ namespace osu.Game.Overlays if (beatmap is Bindable working) working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); - // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase). - // we probably want to move this to a central method for switching to a new working beatmap in the future. - Schedule(() => beatmap.Value.Track.Restart()); - + restartTrack(); return true; } return false; } + private void restartTrack() + { + // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase). + // we probably want to move this to a central method for switching to a new working beatmap in the future. + Schedule(() => beatmap.Value.Track.Restart()); + } + private WorkingBeatmap current; private TrackChangeDirection? queuedDirection; From b361761d869950fd802df2ae551e6819037233da Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 28 Jul 2020 22:08:10 +0900 Subject: [PATCH 2485/6909] Add position display in contracted score panels --- .../Online/Multiplayer/MultiplayerScore.cs | 3 +- osu.Game/Scoring/ScoreInfo.cs | 7 ++++ .../Contracted/ContractedPanelTopContent.cs | 37 +++++++++++++++++++ osu.Game/Screens/Ranking/ScorePanel.cs | 1 + osu.Game/Tests/TestScoreInfo.cs | 2 + 5 files changed, 49 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs diff --git a/osu.Game/Online/Multiplayer/MultiplayerScore.cs b/osu.Game/Online/Multiplayer/MultiplayerScore.cs index 1793ba72ef..8191003aad 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScore.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerScore.cs @@ -80,7 +80,8 @@ namespace osu.Game.Online.Multiplayer Date = EndedAt, Hash = string.Empty, // todo: temporary? Rank = Rank, - Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty() + Mods = Mods?.Select(m => m.ToMod(rulesetInstance)).ToArray() ?? Array.Empty(), + Position = Position, }; return scoreInfo; diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 84c0d5b54e..efcf1737c9 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -179,6 +179,13 @@ namespace osu.Game.Scoring [JsonIgnore] public bool DeletePending { get; set; } + /// + /// The position of this score, starting at 1. + /// + [NotMapped] + [JsonProperty("position")] + public int? Position { get; set; } + [Serializable] protected class DeserializedMod : IMod { diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.cs new file mode 100644 index 0000000000..0935ee7fb2 --- /dev/null +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelTopContent.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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Ranking.Contracted +{ + public class ContractedPanelTopContent : CompositeDrawable + { + private readonly ScoreInfo score; + + public ContractedPanelTopContent(ScoreInfo score) + { + this.score = score; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Y = 6, + Text = score.Position != null ? $"#{score.Position}" : string.Empty, + Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold) + }; + } + } +} diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 5da432d5b2..b32da805e4 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -213,6 +213,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); + topLayerContentContainer.Add(middleLayerContent = new ContractedPanelTopContent(Score).With(d => d.Alpha = 0)); middleLayerContentContainer.Add(topLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0)); break; } diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs index 1193a29d70..31cced6ce4 100644 --- a/osu.Game/Tests/TestScoreInfo.cs +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -37,6 +37,8 @@ namespace osu.Game.Tests Statistics[HitResult.Meh] = 50; Statistics[HitResult.Good] = 100; Statistics[HitResult.Great] = 300; + + Position = 1; } private class TestModHardRock : ModHardRock From 4f3795486d34f0db6e97a04b53ba23d5e1be8048 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 22:36:37 +0900 Subject: [PATCH 2486/6909] Post-process responses to populate positions --- .../TestSceneTimeshiftResultsScreen.cs | 1 + .../Multi/Ranking/TimeshiftResultsScreen.cs | 36 +++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs index 628d08a314..03fd2b968c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs @@ -244,6 +244,7 @@ namespace osu.Game.Tests.Visual.Multiplayer EndedAt = userScore.Date, Passed = userScore.Passed, Rank = userScore.Rank, + Position = 200, MaxCombo = userScore.MaxCombo, TotalScore = userScore.TotalScore, User = userScore.User, diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index e212bd4a82..232f368fb3 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -84,12 +84,18 @@ namespace osu.Game.Screens.Multi.Ranking { allScores.AddRange(userScore.ScoresAround.Higher.Scores); higherScores = userScore.ScoresAround.Higher; + + Debug.Assert(userScore.Position != null); + setPositions(higherScores, userScore.Position.Value, -1); } if (userScore.ScoresAround?.Lower != null) { allScores.AddRange(userScore.ScoresAround.Lower.Scores); lowerScores = userScore.ScoresAround.Lower; + + Debug.Assert(userScore.Position != null); + setPositions(lowerScores, userScore.Position.Value, 1); } performSuccessCallback(scoresCallback, allScores); @@ -134,9 +140,15 @@ namespace osu.Game.Screens.Multi.Ranking indexReq.Success += r => { if (pivot == lowerScores) + { lowerScores = r; + setPositions(r, pivot, 1); + } else + { higherScores = r; + setPositions(r, pivot, -1); + } performSuccessCallback(scoresCallback, r.Scores, r); }; @@ -183,6 +195,30 @@ namespace osu.Game.Screens.Multi.Ranking LeftSpinner.Hide(); } + /// + /// Applies positions to all s from a given pivot. + /// + /// The to set positions on. + /// The pivot. + /// The amount to increment the pivot position by for each in . + private void setPositions([NotNull] MultiplayerScores scores, [CanBeNull] MultiplayerScores pivot, int increment) + => setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment); + + /// + /// Applies positions to all s from a given pivot. + /// + /// The to set positions on. + /// The pivot position. + /// The amount to increment the pivot position by for each in . + private void setPositions([NotNull] MultiplayerScores scores, int pivotPosition, int increment) + { + foreach (var s in scores.Scores) + { + pivotPosition += increment; + s.Position = pivotPosition; + } + } + private class PanelListLoadingSpinner : LoadingSpinner { private readonly ScorePanelList list; From 308f8bf9bf1b5eb7a929d8f1a19c0746d908bbed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 23:11:42 +0900 Subject: [PATCH 2487/6909] Fix inverted naming --- osu.Game/Screens/Ranking/ScorePanel.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index ae55f6e0ae..1904da7094 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -203,8 +203,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(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score).With(d => d.Alpha = 0)); break; case PanelState.Contracted: @@ -213,8 +213,8 @@ 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); - topLayerContentContainer.Add(middleLayerContent = new ContractedPanelTopContent(Score).With(d => d.Alpha = 0)); - middleLayerContentContainer.Add(topLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0)); + topLayerContentContainer.Add(topLayerContent = new ContractedPanelTopContent(Score).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(middleLayerContent = new ContractedPanelMiddleContent(Score).With(d => d.Alpha = 0)); break; } From 04b71a0c7c3fb0420dccfb645944d4687dd4588d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 31 Jul 2020 23:16:55 +0900 Subject: [PATCH 2488/6909] Adjust xmldoc --- osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 232f368fb3..8da6a530a8 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -196,7 +196,7 @@ namespace osu.Game.Screens.Multi.Ranking } /// - /// Applies positions to all s from a given pivot. + /// Applies positions to all s referenced to a given pivot. /// /// The to set positions on. /// The pivot. @@ -205,7 +205,7 @@ namespace osu.Game.Screens.Multi.Ranking => setPositions(scores, pivot?.Scores[^1].Position ?? 0, increment); /// - /// Applies positions to all s from a given pivot. + /// Applies positions to all s referenced to a given pivot. /// /// The to set positions on. /// The pivot position. From 9e244be4890aacc1ff0c1534fcd860817e507eae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Aug 2020 00:05:04 +0900 Subject: [PATCH 2489/6909] Use better conditional for choosing which spinner type to use Co-authored-by: Dan Balasescu --- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 44f431d6f7..81d1d05b66 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -104,9 +104,11 @@ namespace osu.Game.Rulesets.Osu.Skinning }; case OsuSkinComponents.SpinnerBody: - if (Source.GetTexture("spinner-top") != null) + bool hasBackground = Source.GetTexture("spinner-background") != null; + + if (Source.GetTexture("spinner-top") != null && !hasBackground) return new LegacyNewStyleSpinner(); - else if (Source.GetTexture("spinner-background") != null) + else if (hasBackground) return new LegacyOldStyleSpinner(); return null; From bb01ee5be953d6a241830f8a08e3a029298f9e25 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Aug 2020 00:27:00 +0900 Subject: [PATCH 2490/6909] Fix trackign alpha not being applied --- .../Drawables/Pieces/DefaultSpinnerDisc.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 79bfeedd07..eba5d869c0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -100,13 +100,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.Update(); - if (drawableSpinner.RotationTracker.Complete.Value && checkNewRotationCount) + if (drawableSpinner.RotationTracker.Complete.Value) { - fill.FinishTransforms(false, nameof(Alpha)); - fill - .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo) - .Then() - .FadeTo(tracking_alpha, 250, Easing.OutQuint); + if (checkNewRotationCount) + { + fill.FinishTransforms(false, nameof(Alpha)); + fill + .FadeTo(tracking_alpha + 0.2f, 60, Easing.OutExpo) + .Then() + .FadeTo(tracking_alpha, 250, Easing.OutQuint); + } + } + else + { + fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Clock.ElapsedFrameTime); } const float initial_scale = 0.2f; From 180afff80515bc09a0833d1acb45a3bac243a958 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 1 Aug 2020 00:39:04 +0900 Subject: [PATCH 2491/6909] Ensure damp is always positive exponent --- .../Objects/Drawables/Pieces/DefaultSpinnerDisc.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index eba5d869c0..dfb692eba9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } else { - fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Clock.ElapsedFrameTime); + fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime)); } const float initial_scale = 0.2f; From 74f70136fdf80faa47d53772c7c4f386d5e6c3e1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 1 Aug 2020 06:00:24 +0300 Subject: [PATCH 2492/6909] Implement DashboardBeatmapPanel component --- .../TestSceneDashboardBeatmapPanel.cs | 52 ++++++ .../Dashboard/DashboardBeatmapPanel.cs | 159 ++++++++++++++++++ 2 files changed, 211 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs create mode 100644 osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs new file mode 100644 index 0000000000..a61cd37513 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.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.Dashboard.Dashboard; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Framework.Allocation; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneDashboardBeatmapPanel : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + public TestSceneDashboardBeatmapPanel() + { + Add(new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Width = 300, + Child = new DashboardBeatmapPanel(beatmap_set) + }); + } + + private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Title = "Very Long Title (TV size) [TATOE]", + Artist = "This artist has a really long name how is it possible", + Author = new User + { + Username = "author", + Id = 100 + } + }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608", + } + } + }; + } +} diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs new file mode 100644 index 0000000000..84cb5ae46a --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs @@ -0,0 +1,159 @@ +// Copyright (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.Framework.Input.Events; +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; + +namespace osu.Game.Overlays.Dashboard.Dashboard +{ + public class DashboardBeatmapPanel : OsuClickableContainer + { + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + [Resolved(canBeNull: true)] + private BeatmapSetOverlay beatmapOverlay { get; set; } + + private readonly BeatmapSetInfo setInfo; + + private Box background; + private SpriteIcon chevron; + + public DashboardBeatmapPanel(BeatmapSetInfo setInfo) + { + this.setInfo = setInfo; + } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + Height = 60; + Children = new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background3, + Alpha = 0 + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 10 }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 70), + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + RowDimensions = new[] + { + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 6, + Child = new UpdateableBeatmapSetCover + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BeatmapSet = setInfo + } + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 10 }, + Child = new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Truncate = true, + Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Text = setInfo.Metadata.Title + }, + new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Truncate = true, + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Text = setInfo.Metadata.Artist + }, + new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }.With(c => + { + c.AddText("by "); + c.AddUserLink(setInfo.Metadata.Author); + }) + } + } + }, + chevron = new SpriteIcon + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(16), + Icon = FontAwesome.Solid.ChevronRight, + Colour = colourProvider.Foreground1 + } + } + } + } + } + }; + + Action = () => + { + if (setInfo.OnlineBeatmapSetID.HasValue) + beatmapOverlay?.FetchAndShowBeatmapSet(setInfo.OnlineBeatmapSetID.Value); + }; + } + + protected override bool OnHover(HoverEvent e) + { + base.OnHover(e); + background.FadeIn(200, Easing.OutQuint); + chevron.FadeColour(colourProvider.Light1, 200, Easing.OutQuint); + return true; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + background.FadeOut(200, Easing.OutQuint); + chevron.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); + } + } +} From ce47a34991ce574265aeaf37134c3d9f6de02cee Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 1 Aug 2020 06:14:24 +0300 Subject: [PATCH 2493/6909] Implement DashboardNewBeatmapPanel component --- .../TestSceneDashboardBeatmapPanel.cs | 8 ++- .../Dashboard/DashboardBeatmapPanel.cs | 56 +++++++++++-------- .../Dashboard/DashboardNewBeatmapPanel.cs | 22 ++++++++ 3 files changed, 61 insertions(+), 25 deletions(-) create mode 100644 osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs index a61cd37513..5f1af012db 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Framework.Allocation; using osu.Game.Users; +using System; namespace osu.Game.Tests.Visual.UserInterface { @@ -24,7 +25,7 @@ namespace osu.Game.Tests.Visual.UserInterface Origin = Anchor.Centre, AutoSizeAxes = Axes.Y, Width = 300, - Child = new DashboardBeatmapPanel(beatmap_set) + Child = new DashboardNewBeatmapPanel(beatmap_set) }); } @@ -33,7 +34,7 @@ namespace osu.Game.Tests.Visual.UserInterface Metadata = new BeatmapMetadata { Title = "Very Long Title (TV size) [TATOE]", - Artist = "This artist has a really long name how is it possible", + Artist = "This artist has a really long name how is this possible", Author = new User { Username = "author", @@ -45,7 +46,8 @@ namespace osu.Game.Tests.Visual.UserInterface Covers = new BeatmapSetOnlineCovers { Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608", - } + }, + Ranked = DateTimeOffset.Now } }; } diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs index 84cb5ae46a..30b0086b8a 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs @@ -16,22 +16,22 @@ using osuTK; namespace osu.Game.Overlays.Dashboard.Dashboard { - public class DashboardBeatmapPanel : OsuClickableContainer + public abstract class DashboardBeatmapPanel : OsuClickableContainer { [Resolved] - private OverlayColourProvider colourProvider { get; set; } + protected OverlayColourProvider ColourProvider { get; private set; } [Resolved(canBeNull: true)] private BeatmapSetOverlay beatmapOverlay { get; set; } - private readonly BeatmapSetInfo setInfo; + protected readonly BeatmapSetInfo SetInfo; private Box background; private SpriteIcon chevron; - public DashboardBeatmapPanel(BeatmapSetInfo setInfo) + protected DashboardBeatmapPanel(BeatmapSetInfo setInfo) { - this.setInfo = setInfo; + SetInfo = setInfo; } [BackgroundDependencyLoader] @@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background3, + Colour = ColourProvider.Background3, Alpha = 0 }, new Container @@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - BeatmapSet = setInfo + BeatmapSet = SetInfo } }, new Container @@ -99,24 +99,34 @@ namespace osu.Game.Overlays.Dashboard.Dashboard RelativeSizeAxes = Axes.X, Truncate = true, Font = OsuFont.GetFont(weight: FontWeight.SemiBold), - Text = setInfo.Metadata.Title + Text = SetInfo.Metadata.Title }, new OsuSpriteText { RelativeSizeAxes = Axes.X, Truncate = true, Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Text = setInfo.Metadata.Artist + Text = SetInfo.Metadata.Artist }, - new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold)) + new FillFlowContainer { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }.With(c => - { - c.AddText("by "); - c.AddUserLink(setInfo.Metadata.Author); - }) + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(5, 0), + Margin = new MarginPadding { Top = 2 }, + Children = new Drawable[] + { + new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold)) + { + AutoSizeAxes = Axes.Both + }.With(c => + { + c.AddText("by "); + c.AddUserLink(SetInfo.Metadata.Author); + }), + CreateInfo() + } + } } } }, @@ -126,7 +136,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard Origin = Anchor.CentreRight, Size = new Vector2(16), Icon = FontAwesome.Solid.ChevronRight, - Colour = colourProvider.Foreground1 + Colour = ColourProvider.Foreground1 } } } @@ -136,16 +146,18 @@ namespace osu.Game.Overlays.Dashboard.Dashboard Action = () => { - if (setInfo.OnlineBeatmapSetID.HasValue) - beatmapOverlay?.FetchAndShowBeatmapSet(setInfo.OnlineBeatmapSetID.Value); + if (SetInfo.OnlineBeatmapSetID.HasValue) + beatmapOverlay?.FetchAndShowBeatmapSet(SetInfo.OnlineBeatmapSetID.Value); }; } + protected abstract Drawable CreateInfo(); + protected override bool OnHover(HoverEvent e) { base.OnHover(e); background.FadeIn(200, Easing.OutQuint); - chevron.FadeColour(colourProvider.Light1, 200, Easing.OutQuint); + chevron.FadeColour(ColourProvider.Light1, 200, Easing.OutQuint); return true; } @@ -153,7 +165,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard { base.OnHoverLost(e); background.FadeOut(200, Easing.OutQuint); - chevron.FadeColour(colourProvider.Foreground1, 200, Easing.OutQuint); + chevron.FadeColour(ColourProvider.Foreground1, 200, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.cs new file mode 100644 index 0000000000..6d2ec7ae37 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.Dashboard.Dashboard +{ + public class DashboardNewBeatmapPanel : DashboardBeatmapPanel + { + public DashboardNewBeatmapPanel(BeatmapSetInfo setInfo) + : base(setInfo) + { + } + + protected override Drawable CreateInfo() => new DrawableDate(SetInfo.OnlineInfo.Ranked.Value, 10, false) + { + Colour = ColourProvider.Foreground1 + }; + } +} From 7624804edfacb185dc456c18eb32648287613665 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 1 Aug 2020 06:23:06 +0300 Subject: [PATCH 2494/6909] Implement DashboardPopularBeatmapPanel component --- .../TestSceneDashboardBeatmapPanel.cs | 35 ++++++++++++++-- .../Dashboard/DashboardBeatmapPanel.cs | 2 +- .../Dashboard/DashboardPopularBeatmapPanel.cs | 42 +++++++++++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Overlays/Dashboard/Dashboard/DashboardPopularBeatmapPanel.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs index 5f1af012db..10682490ae 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs @@ -9,6 +9,7 @@ using osu.Game.Overlays; using osu.Framework.Allocation; using osu.Game.Users; using System; +using osuTK; namespace osu.Game.Tests.Visual.UserInterface { @@ -19,17 +20,23 @@ namespace osu.Game.Tests.Visual.UserInterface public TestSceneDashboardBeatmapPanel() { - Add(new Container + Add(new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Y, Width = 300, - Child = new DashboardNewBeatmapPanel(beatmap_set) + Spacing = new Vector2(0, 10), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new DashboardNewBeatmapPanel(new_beatmap_set), + new DashboardPopularBeatmapPanel(popular_beatmap_set) + } }); } - private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo + private static readonly BeatmapSetInfo new_beatmap_set = new BeatmapSetInfo { Metadata = new BeatmapMetadata { @@ -50,5 +57,27 @@ namespace osu.Game.Tests.Visual.UserInterface Ranked = DateTimeOffset.Now } }; + + private static readonly BeatmapSetInfo popular_beatmap_set = new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Title = "Title", + Artist = "Artist", + Author = new User + { + Username = "author", + Id = 100 + } + }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608", + }, + FavouriteCount = 100 + } + }; } } diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs index 30b0086b8a..fc70d9d40a 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs @@ -112,7 +112,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), + Spacing = new Vector2(3, 0), Margin = new MarginPadding { Top = 2 }, Children = new Drawable[] { diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardPopularBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardPopularBeatmapPanel.cs new file mode 100644 index 0000000000..04bb261dce --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardPopularBeatmapPanel.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Dashboard.Dashboard +{ + public class DashboardPopularBeatmapPanel : DashboardBeatmapPanel + { + public DashboardPopularBeatmapPanel(BeatmapSetInfo setInfo) + : base(setInfo) + { + } + + protected override Drawable CreateInfo() => new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(3, 0), + Colour = ColourProvider.Foreground1, + Children = new Drawable[] + { + new SpriteIcon + { + Size = new Vector2(10), + Icon = FontAwesome.Solid.Heart + }, + new OsuSpriteText + { + Font = OsuFont.GetFont(size: 10), + Text = SetInfo.OnlineInfo.FavouriteCount.ToString() + } + } + }; + } +} From b5f688e63aeb9168cb4d73c651889dab270871a3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 1 Aug 2020 07:04:39 +0300 Subject: [PATCH 2495/6909] Implement DashboardBeatmapListing component --- .../TestSceneDashboardBeatmapListing.cs | 144 ++++++++++++++++++ .../TestSceneDashboardBeatmapPanel.cs | 83 ---------- .../Dashboard/DashboardBeatmapListing.cs | 43 ++++++ .../Dashboard/DashboardBeatmapPanel.cs | 2 +- .../Dashboard/DashboardNewBeatmapPanel.cs | 3 +- .../Dashboard/DrawableBeatmapsList.cs | 57 +++++++ .../Dashboard/DrawableNewBeatmapsList.cs | 20 +++ .../Dashboard/DrawablePopularBeatmapsList.cs | 20 +++ 8 files changed, 287 insertions(+), 85 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs delete mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs create mode 100644 osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs create mode 100644 osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapsList.cs create mode 100644 osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapsList.cs create mode 100644 osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapsList.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs new file mode 100644 index 0000000000..be0cc5187e --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.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 osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Game.Overlays.Dashboard.Dashboard; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Framework.Allocation; +using osu.Game.Users; +using System; +using osu.Framework.Graphics.Shapes; +using System.Collections.Generic; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneDashboardBeatmapListing : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); + + private readonly Container content; + + public TestSceneDashboardBeatmapListing() + { + Add(content = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Width = 300, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4 + }, + new DashboardBeatmapListing(new_beatmaps, popular_beatmaps) + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + AddStep("Set width to 500", () => content.ResizeWidthTo(500, 500)); + AddStep("Set width to 300", () => content.ResizeWidthTo(300, 500)); + } + + private static readonly List new_beatmaps = new List + { + new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Title = "Very Long Title (TV size) [TATOE]", + Artist = "This artist has a really long name how is this possible", + Author = new User + { + Username = "author", + Id = 100 + } + }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608", + }, + Ranked = DateTimeOffset.Now + } + }, + new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Title = "Very Long Title (TV size) [TATOE]", + Artist = "This artist has a really long name how is this possible", + Author = new User + { + Username = "author", + Id = 100 + } + }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608", + }, + Ranked = DateTimeOffset.MinValue + } + } + }; + + private static readonly List popular_beatmaps = new List + { + new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Title = "Title", + Artist = "Artist", + Author = new User + { + Username = "author", + Id = 100 + } + }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586", + }, + FavouriteCount = 100 + } + }, + new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Title = "Title 2", + Artist = "Artist 2", + Author = new User + { + Username = "someone", + Id = 100 + } + }, + OnlineInfo = new BeatmapSetOnlineInfo + { + Covers = new BeatmapSetOnlineCovers + { + Cover = "https://assets.ppy.sh/beatmaps/1079428/covers/cover.jpg?1595295586", + }, + FavouriteCount = 10 + } + } + }; + } +} diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs deleted file mode 100644 index 10682490ae..0000000000 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapPanel.cs +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics; -using osu.Game.Overlays.Dashboard.Dashboard; -using osu.Game.Beatmaps; -using osu.Game.Overlays; -using osu.Framework.Allocation; -using osu.Game.Users; -using System; -using osuTK; - -namespace osu.Game.Tests.Visual.UserInterface -{ - public class TestSceneDashboardBeatmapPanel : OsuTestScene - { - [Cached] - private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - - public TestSceneDashboardBeatmapPanel() - { - Add(new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Y, - Width = 300, - Spacing = new Vector2(0, 10), - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new DashboardNewBeatmapPanel(new_beatmap_set), - new DashboardPopularBeatmapPanel(popular_beatmap_set) - } - }); - } - - private static readonly BeatmapSetInfo new_beatmap_set = new BeatmapSetInfo - { - Metadata = new BeatmapMetadata - { - Title = "Very Long Title (TV size) [TATOE]", - Artist = "This artist has a really long name how is this possible", - Author = new User - { - Username = "author", - Id = 100 - } - }, - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608", - }, - Ranked = DateTimeOffset.Now - } - }; - - private static readonly BeatmapSetInfo popular_beatmap_set = new BeatmapSetInfo - { - Metadata = new BeatmapMetadata - { - Title = "Title", - Artist = "Artist", - Author = new User - { - Username = "author", - Id = 100 - } - }, - OnlineInfo = new BeatmapSetOnlineInfo - { - Covers = new BeatmapSetOnlineCovers - { - Cover = "https://assets.ppy.sh/beatmaps/1189904/covers/cover.jpg?1595456608", - }, - FavouriteCount = 100 - } - }; - } -} diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs new file mode 100644 index 0000000000..26c892174d --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osuTK; + +namespace osu.Game.Overlays.Dashboard.Dashboard +{ + public class DashboardBeatmapListing : CompositeDrawable + { + private readonly List newBeatmaps; + private readonly List popularBeatmaps; + + public DashboardBeatmapListing(List newBeatmaps, List popularBeatmaps) + { + this.newBeatmaps = newBeatmaps; + this.popularBeatmaps = popularBeatmaps; + } + + [BackgroundDependencyLoader] + private void load() + { + 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 DrawableBeatmapsList[] + { + new DrawableNewBeatmapsList(newBeatmaps), + new DrawablePopularBeatmapsList(popularBeatmaps) + } + }; + } + } +} diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs index fc70d9d40a..bb35ddd07c 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs @@ -114,7 +114,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard Direction = FillDirection.Horizontal, Spacing = new Vector2(3, 0), Margin = new MarginPadding { Top = 2 }, - Children = new Drawable[] + Children = new[] { new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold)) { diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.cs index 6d2ec7ae37..c4782b733d 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.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.Game.Beatmaps; using osu.Game.Graphics; @@ -14,7 +15,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard { } - protected override Drawable CreateInfo() => new DrawableDate(SetInfo.OnlineInfo.Ranked.Value, 10, false) + protected override Drawable CreateInfo() => new DrawableDate(SetInfo.OnlineInfo.Ranked ?? DateTimeOffset.Now, 10, false) { Colour = ColourProvider.Foreground1 }; diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapsList.cs b/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapsList.cs new file mode 100644 index 0000000000..dfe483a962 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapsList.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Dashboard.Dashboard +{ + public abstract class DrawableBeatmapsList : CompositeDrawable + { + private readonly List beatmaps; + + protected DrawableBeatmapsList(List beatmaps) + { + this.beatmaps = beatmaps; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + FillFlowContainer flow; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold), + Colour = colourProvider.Light1, + Text = CreateTitle(), + Padding = new MarginPadding { Left = 10 } + } + } + }; + + flow.AddRange(beatmaps.Select(CreateBeatmapPanel)); + } + + protected abstract string CreateTitle(); + + protected abstract DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo); + } +} diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapsList.cs b/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapsList.cs new file mode 100644 index 0000000000..e89495dc99 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapsList.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 osu.Game.Beatmaps; + +namespace osu.Game.Overlays.Dashboard.Dashboard +{ + public class DrawableNewBeatmapsList : DrawableBeatmapsList + { + public DrawableNewBeatmapsList(List beatmaps) + : base(beatmaps) + { + } + + protected override DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo) => new DashboardNewBeatmapPanel(setInfo); + + protected override string CreateTitle() => "New Ranked Beatmaps"; + } +} diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapsList.cs b/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapsList.cs new file mode 100644 index 0000000000..8076e86ed0 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapsList.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 osu.Game.Beatmaps; + +namespace osu.Game.Overlays.Dashboard.Dashboard +{ + public class DrawablePopularBeatmapsList : DrawableBeatmapsList + { + public DrawablePopularBeatmapsList(List beatmaps) + : base(beatmaps) + { + } + + protected override DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo) => new DashboardPopularBeatmapPanel(setInfo); + + protected override string CreateTitle() => "Popular Beatmaps"; + } +} From 5b1e3e86220e7ba3b8024fa9ff6cfc035e34a82c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 1 Aug 2020 09:11:53 +0300 Subject: [PATCH 2496/6909] Remove redundant FillFlowContainer from DashboardBeatmapPanel --- .../Dashboard/DashboardBeatmapPanel.cs | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs index bb35ddd07c..61324fdc7f 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs @@ -108,25 +108,18 @@ namespace osu.Game.Overlays.Dashboard.Dashboard Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Text = SetInfo.Metadata.Artist }, - new FillFlowContainer + new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold)) { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(3, 0), - Margin = new MarginPadding { Top = 2 }, - Children = new[] - { - new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold)) - { - AutoSizeAxes = Axes.Both - }.With(c => - { - c.AddText("by "); - c.AddUserLink(SetInfo.Metadata.Author); - }), - CreateInfo() - } - } + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Spacing = new Vector2(3), + Margin = new MarginPadding { Top = 2 } + }.With(c => + { + c.AddText("by"); + c.AddUserLink(SetInfo.Metadata.Author); + c.AddArbitraryDrawable(CreateInfo()); + }) } } }, From 2190e6443a0265ba4d4db20a44b207b3a63a4760 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 1 Aug 2020 10:02:46 +0300 Subject: [PATCH 2497/6909] Apply height constraints to all settings dropdown --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 2 -- osu.Game/Overlays/Settings/SettingsDropdown.cs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 04390a1193..596d3a9801 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -116,8 +116,6 @@ namespace osu.Game.Overlays.Settings.Sections private class SkinDropdownControl : DropdownControl { protected override string GenerateItemText(SkinInfo item) => item.ToString(); - - protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200); } } diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs index 167061f485..1175ddaab8 100644 --- a/osu.Game/Overlays/Settings/SettingsDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs @@ -38,6 +38,8 @@ namespace osu.Game.Overlays.Settings Margin = new MarginPadding { Top = 5 }; RelativeSizeAxes = Axes.X; } + + protected override DropdownMenu CreateMenu() => base.CreateMenu().With(m => m.MaxHeight = 200); } } } From 5f52701273a8b41205bcd7a1c24fd02bb658560c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 1 Aug 2020 10:11:34 +0300 Subject: [PATCH 2498/6909] Remove no longer necessary custom dropdown --- .../Ladder/Components/LadderEditorSettings.cs | 2 +- .../Components/LadderSettingsDropdown.cs | 26 ------------------- .../Ladder/Components/SettingsTeamDropdown.cs | 3 ++- 3 files changed, 3 insertions(+), 28 deletions(-) delete mode 100644 osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs index 4aea7ff4c0..fa530ea2c4 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs @@ -81,7 +81,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { } - private class SettingsRoundDropdown : LadderSettingsDropdown + private class SettingsRoundDropdown : SettingsDropdown { public SettingsRoundDropdown(BindableList rounds) { diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.cs deleted file mode 100644 index 347e4d91e0..0000000000 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderSettingsDropdown.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 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 index a630e51e44..6604e3a313 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/SettingsTeamDropdown.cs @@ -6,11 +6,12 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Overlays.Settings; using osu.Game.Tournament.Models; namespace osu.Game.Tournament.Screens.Ladder.Components { - public class SettingsTeamDropdown : LadderSettingsDropdown + public class SettingsTeamDropdown : SettingsDropdown { public SettingsTeamDropdown(BindableList teams) { From 45225646684d9f28b19650b3fffaa3d9791695d6 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 1 Aug 2020 19:44:30 +0200 Subject: [PATCH 2499/6909] Add GameplayDisableOverlays setting. --- osu.Game/Configuration/OsuConfigManager.cs | 4 +++- .../Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 5 +++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a8a8794320..44c0fbde82 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -100,6 +100,7 @@ namespace osu.Game.Configuration Set(OsuSetting.IncreaseFirstObjectVisibility, true); Set(OsuSetting.GameplayDisableWinKey, true); + Set(OsuSetting.GameplayDisableOverlayActivation, true); // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -231,6 +232,7 @@ namespace osu.Game.Configuration UIHoldActivationDelay, HitLighting, MenuBackgroundSource, - GameplayDisableWinKey + GameplayDisableWinKey, + GameplayDisableOverlayActivation } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 0149e6c3a6..c2e668fe68 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -77,6 +77,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { LabelText = "Score display mode", Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) + }, + new SettingsCheckbox + { + LabelText = "Disable overlays during gameplay", + Bindable = config.GetBindable(OsuSetting.GameplayDisableOverlayActivation) } }; From f4128083312843d9594c1b96d212914a43fee60b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 2 Aug 2020 12:57:15 +0200 Subject: [PATCH 2500/6909] Check rotation with bigger tolerance to account for damp --- .../TestSceneSpinnerRotation.cs | 44 ++++++++++++++----- 1 file changed, 33 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 319d326a01..e4d89fb402 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.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; @@ -60,39 +61,60 @@ namespace osu.Game.Rulesets.Osu.Tests [Test] public void TestSpinnerRewindingRotation() { + double trackerRotationTolerance = 0; + addSeekStep(5000); + AddStep("calculate rotation tolerance", () => + { + trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); + }); AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100)); addSeekStep(0); - AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); + AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance)); AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100)); } [Test] public void TestSpinnerMiddleRewindingRotation() { - double finalAbsoluteDiscRotation = 0, finalRelativeDiscRotation = 0, finalSpinnerSymbolRotation = 0; + double finalCumulativeTrackerRotation = 0; + double finalTrackerRotation = 0, trackerRotationTolerance = 0; + double finalSpinnerSymbolRotation = 0, spinnerSymbolRotationTolerance = 0; addSeekStep(5000); - AddStep("retrieve disc relative rotation", () => finalRelativeDiscRotation = drawableSpinner.RotationTracker.Rotation); - AddStep("retrieve disc absolute rotation", () => finalAbsoluteDiscRotation = drawableSpinner.RotationTracker.CumulativeRotation); - AddStep("retrieve spinner symbol rotation", () => finalSpinnerSymbolRotation = spinnerSymbol.Rotation); + AddStep("retrieve disc rotation", () => + { + finalTrackerRotation = drawableSpinner.RotationTracker.Rotation; + trackerRotationTolerance = Math.Abs(finalTrackerRotation * 0.05f); + }); + AddStep("retrieve spinner symbol rotation", () => + { + finalSpinnerSymbolRotation = spinnerSymbol.Rotation; + spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f); + }); + AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.CumulativeRotation); 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.RotationTracker.Rotation, finalRelativeDiscRotation / 2, 100)); + // due to the exponential damping applied we're allowing a larger margin of error of about 10% + // (5% relative to the final rotation value, but we're half-way through the spin). + () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance)); AddUntilStep("symbol rotation rewound", - () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, 100)); + () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance)); + AddAssert("is cumulative rotation rewound", + // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. + () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation / 2, 100)); addSeekStep(5000); AddAssert("is disc rotation almost same", - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalRelativeDiscRotation, 100)); + () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation, trackerRotationTolerance)); AddAssert("is symbol rotation almost same", - () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, 100)); - AddAssert("is disc rotation absolute almost same", - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalAbsoluteDiscRotation, 100)); + () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance)); + AddAssert("is cumulative rotation almost same", + () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation, 100)); } [Test] From efb08aeed32b1787e79b7ebd0e7501a874cf559c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 2 Aug 2020 14:54:41 +0200 Subject: [PATCH 2501/6909] Switch unnecessary wait steps to asserts --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index e4d89fb402..b46964e8b7 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -97,12 +97,12 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.CumulativeRotation); addSeekStep(2500); - AddUntilStep("disc rotation rewound", + AddAssert("disc rotation rewound", // we want to make sure that the rotation at time 2500 is in the same direction as at time 5000, but about half-way in. // due to the exponential damping applied we're allowing a larger margin of error of about 10% // (5% relative to the final rotation value, but we're half-way through the spin). () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, finalTrackerRotation / 2, trackerRotationTolerance)); - AddUntilStep("symbol rotation rewound", + AddAssert("symbol rotation rewound", () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation rewound", // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. From 3e5c3e256d3f0747afd8f89f05e622eba1d035ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 2 Aug 2020 19:46:29 +0200 Subject: [PATCH 2502/6909] Extract method for performing generic config lookup --- osu.Game/Skinning/LegacySkin.cs | 46 ++++++++++++++++++--------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index d98f8aba83..d1acc51bed 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -143,27 +143,7 @@ namespace osu.Game.Skinning goto default; default: - // handles lookups like some in LegacySkinConfiguration.LegacySetting - - try - { - if (Configuration.ConfigDictionary.TryGetValue(lookup.ToString(), out var val)) - { - // special case for handling skins which use 1 or 0 to signify a boolean state. - if (typeof(TValue) == typeof(bool)) - val = val == "1" ? "true" : "false"; - - var bindable = new Bindable(); - if (val != null) - bindable.Parse(val); - return bindable; - } - } - catch - { - } - - break; + return genericLookup(lookup); } return null; @@ -286,6 +266,30 @@ namespace osu.Game.Skinning private IBindable getManiaImage(LegacyManiaSkinConfiguration source, string lookup) => source.ImageLookups.TryGetValue(lookup, out var image) ? new Bindable(image) : null; + [CanBeNull] + private IBindable genericLookup(TLookup lookup) + { + try + { + if (Configuration.ConfigDictionary.TryGetValue(lookup.ToString(), out var val)) + { + // special case for handling skins which use 1 or 0 to signify a boolean state. + if (typeof(TValue) == typeof(bool)) + val = val == "1" ? "true" : "false"; + + var bindable = new Bindable(); + if (val != null) + bindable.Parse(val); + return bindable; + } + } + catch + { + } + + return null; + } + public override Drawable GetDrawableComponent(ISkinComponent component) { switch (component) From ca7545917c85d5b5cf58ab54ec49aaab4c17d52f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 2 Aug 2020 19:50:17 +0200 Subject: [PATCH 2503/6909] Extract method for performing legacy lookups --- osu.Game/Skinning/LegacySkin.cs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index d1acc51bed..4e470e13b5 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -134,13 +134,7 @@ namespace osu.Game.Skinning break; case LegacySkinConfiguration.LegacySetting legacy: - switch (legacy) - { - case LegacySkinConfiguration.LegacySetting.Version: - return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION)); - } - - goto default; + return legacySettingLookup(legacy); default: return genericLookup(lookup); @@ -266,6 +260,19 @@ namespace osu.Game.Skinning private IBindable getManiaImage(LegacyManiaSkinConfiguration source, string lookup) => source.ImageLookups.TryGetValue(lookup, out var image) ? new Bindable(image) : null; + [CanBeNull] + private IBindable legacySettingLookup(LegacySkinConfiguration.LegacySetting legacySetting) + { + switch (legacySetting) + { + case LegacySkinConfiguration.LegacySetting.Version: + return SkinUtils.As(new Bindable(Configuration.LegacyVersion ?? LegacySkinConfiguration.LATEST_VERSION)); + + default: + return genericLookup(legacySetting); + } + } + [CanBeNull] private IBindable genericLookup(TLookup lookup) { From ca57c70961e51cceed04404706c59a8601fa29b9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 2 Aug 2020 21:33:14 +0300 Subject: [PATCH 2504/6909] Naming adjustments --- .../Dashboard/Dashboard/DashboardBeatmapListing.cs | 8 ++++---- .../Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs | 8 ++++---- .../{DrawableBeatmapsList.cs => DrawableBeatmapList.cs} | 4 ++-- ...awableNewBeatmapsList.cs => DrawableNewBeatmapList.cs} | 4 ++-- ...pularBeatmapsList.cs => DrawablePopularBeatmapList.cs} | 4 ++-- 5 files changed, 14 insertions(+), 14 deletions(-) rename osu.Game/Overlays/Dashboard/Dashboard/{DrawableBeatmapsList.cs => DrawableBeatmapList.cs} (92%) rename osu.Game/Overlays/Dashboard/Dashboard/{DrawableNewBeatmapsList.cs => DrawableNewBeatmapList.cs} (80%) rename osu.Game/Overlays/Dashboard/Dashboard/{DrawablePopularBeatmapsList.cs => DrawablePopularBeatmapList.cs} (79%) diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs index 26c892174d..a8c6ab7ba2 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs @@ -26,16 +26,16 @@ namespace osu.Game.Overlays.Dashboard.Dashboard { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = new FillFlowContainer + InternalChild = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 10), - Children = new DrawableBeatmapsList[] + Children = new DrawableBeatmapList[] { - new DrawableNewBeatmapsList(newBeatmaps), - new DrawablePopularBeatmapsList(popularBeatmaps) + new DrawableNewBeatmapList(newBeatmaps), + new DrawablePopularBeatmapList(popularBeatmaps) } }; } diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs index 61324fdc7f..45fa56a177 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard protected readonly BeatmapSetInfo SetInfo; - private Box background; + private Box hoverBackground; private SpriteIcon chevron; protected DashboardBeatmapPanel(BeatmapSetInfo setInfo) @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard Height = 60; Children = new Drawable[] { - background = new Box + hoverBackground = new Box { RelativeSizeAxes = Axes.Both, Colour = ColourProvider.Background3, @@ -149,7 +149,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard protected override bool OnHover(HoverEvent e) { base.OnHover(e); - background.FadeIn(200, Easing.OutQuint); + hoverBackground.FadeIn(200, Easing.OutQuint); chevron.FadeColour(ColourProvider.Light1, 200, Easing.OutQuint); return true; } @@ -157,7 +157,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard protected override void OnHoverLost(HoverLostEvent e) { base.OnHoverLost(e); - background.FadeOut(200, Easing.OutQuint); + hoverBackground.FadeOut(200, Easing.OutQuint); chevron.FadeColour(ColourProvider.Foreground1, 200, Easing.OutQuint); } } diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapsList.cs b/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs similarity index 92% rename from osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapsList.cs rename to osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs index dfe483a962..7dd969863a 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapsList.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs @@ -13,11 +13,11 @@ using osuTK; namespace osu.Game.Overlays.Dashboard.Dashboard { - public abstract class DrawableBeatmapsList : CompositeDrawable + public abstract class DrawableBeatmapList : CompositeDrawable { private readonly List beatmaps; - protected DrawableBeatmapsList(List beatmaps) + protected DrawableBeatmapList(List beatmaps) { this.beatmaps = beatmaps; } diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapsList.cs b/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs similarity index 80% rename from osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapsList.cs rename to osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs index e89495dc99..3ed42cc4bb 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapsList.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs @@ -6,9 +6,9 @@ using osu.Game.Beatmaps; namespace osu.Game.Overlays.Dashboard.Dashboard { - public class DrawableNewBeatmapsList : DrawableBeatmapsList + public class DrawableNewBeatmapList : DrawableBeatmapList { - public DrawableNewBeatmapsList(List beatmaps) + public DrawableNewBeatmapList(List beatmaps) : base(beatmaps) { } diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapsList.cs b/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapList.cs similarity index 79% rename from osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapsList.cs rename to osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapList.cs index 8076e86ed0..ee6c3e6d77 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapsList.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapList.cs @@ -6,9 +6,9 @@ using osu.Game.Beatmaps; namespace osu.Game.Overlays.Dashboard.Dashboard { - public class DrawablePopularBeatmapsList : DrawableBeatmapsList + public class DrawablePopularBeatmapList : DrawableBeatmapList { - public DrawablePopularBeatmapsList(List beatmaps) + public DrawablePopularBeatmapList(List beatmaps) : base(beatmaps) { } From 7d83cdbf1c18fdaa9d25d866ac62a3d292a5e795 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 2 Aug 2020 21:35:24 +0300 Subject: [PATCH 2505/6909] Make title in DrawableBeatmapList a property --- osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs | 4 ++-- .../Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs | 2 +- .../Dashboard/Dashboard/DrawablePopularBeatmapList.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs b/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs index 7dd969863a..4837d587fd 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard { Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold), Colour = colourProvider.Light1, - Text = CreateTitle(), + Text = Title, Padding = new MarginPadding { Left = 10 } } } @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard flow.AddRange(beatmaps.Select(CreateBeatmapPanel)); } - protected abstract string CreateTitle(); + protected abstract string Title { get; } protected abstract DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo); } diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs b/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs index 3ed42cc4bb..9856f6ae3e 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs @@ -15,6 +15,6 @@ namespace osu.Game.Overlays.Dashboard.Dashboard protected override DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo) => new DashboardNewBeatmapPanel(setInfo); - protected override string CreateTitle() => "New Ranked Beatmaps"; + protected override string Title => "New Ranked Beatmaps"; } } diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapList.cs b/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapList.cs index ee6c3e6d77..294a75c48a 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapList.cs @@ -15,6 +15,6 @@ namespace osu.Game.Overlays.Dashboard.Dashboard protected override DashboardBeatmapPanel CreateBeatmapPanel(BeatmapSetInfo setInfo) => new DashboardPopularBeatmapPanel(setInfo); - protected override string CreateTitle() => "Popular Beatmaps"; + protected override string Title => "Popular Beatmaps"; } } From bddc61756a3a40f5885b449cd72178b4fd8c2c7b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 2 Aug 2020 21:44:34 +0300 Subject: [PATCH 2506/6909] Rework padding --- .../TestSceneDashboardBeatmapListing.cs | 8 +++++++- .../Dashboard/Dashboard/DashboardBeatmapPanel.cs | 12 ++++++++---- .../Dashboard/Dashboard/DrawableBeatmapList.cs | 3 +-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs index be0cc5187e..c5714dae46 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs @@ -36,7 +36,13 @@ namespace osu.Game.Tests.Visual.UserInterface RelativeSizeAxes = Axes.Both, Colour = colourProvider.Background4 }, - new DashboardBeatmapListing(new_beatmaps, popular_beatmaps) + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 10 }, + Child = new DashboardBeatmapListing(new_beatmaps, popular_beatmaps) + } } }); } diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs index 45fa56a177..d74cdf4414 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs @@ -41,16 +41,20 @@ namespace osu.Game.Overlays.Dashboard.Dashboard Height = 60; Children = new Drawable[] { - hoverBackground = new Box + new Container { RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background3, - Alpha = 0 + Padding = new MarginPadding { Horizontal = -10 }, + Child = hoverBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background3, + Alpha = 0 + } }, new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 10 }, Child = new GridContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs b/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs index 4837d587fd..6e42881de7 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs @@ -41,8 +41,7 @@ namespace osu.Game.Overlays.Dashboard.Dashboard { Font = OsuFont.GetFont(size: 16, weight: FontWeight.Bold), Colour = colourProvider.Light1, - Text = Title, - Padding = new MarginPadding { Left = 10 } + Text = Title } } }; From dc559093cdc519e600d7304e6f14bdcc06ea91aa Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 2 Aug 2020 21:47:09 +0300 Subject: [PATCH 2507/6909] Rename namespace from Dashboard to Home --- .../Visual/UserInterface/TestSceneDashboardBeatmapListing.cs | 2 +- .../Dashboard/{Dashboard => Home}/DashboardBeatmapListing.cs | 2 +- .../Dashboard/{Dashboard => Home}/DashboardBeatmapPanel.cs | 2 +- .../Dashboard/{Dashboard => Home}/DashboardNewBeatmapPanel.cs | 2 +- .../{Dashboard => Home}/DashboardPopularBeatmapPanel.cs | 2 +- .../Dashboard/{Dashboard => Home}/DrawableBeatmapList.cs | 2 +- .../Dashboard/{Dashboard => Home}/DrawableNewBeatmapList.cs | 2 +- .../Dashboard/{Dashboard => Home}/DrawablePopularBeatmapList.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game/Overlays/Dashboard/{Dashboard => Home}/DashboardBeatmapListing.cs (96%) rename osu.Game/Overlays/Dashboard/{Dashboard => Home}/DashboardBeatmapPanel.cs (99%) rename osu.Game/Overlays/Dashboard/{Dashboard => Home}/DashboardNewBeatmapPanel.cs (93%) rename osu.Game/Overlays/Dashboard/{Dashboard => Home}/DashboardPopularBeatmapPanel.cs (96%) rename osu.Game/Overlays/Dashboard/{Dashboard => Home}/DrawableBeatmapList.cs (97%) rename osu.Game/Overlays/Dashboard/{Dashboard => Home}/DrawableNewBeatmapList.cs (92%) rename osu.Game/Overlays/Dashboard/{Dashboard => Home}/DrawablePopularBeatmapList.cs (92%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs index c5714dae46..c51204eaba 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDashboardBeatmapListing.cs @@ -3,7 +3,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; -using osu.Game.Overlays.Dashboard.Dashboard; +using osu.Game.Overlays.Dashboard.Home; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Framework.Allocation; diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs similarity index 96% rename from osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs rename to osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs index a8c6ab7ba2..4d96825353 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapListing.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapListing.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osuTK; -namespace osu.Game.Overlays.Dashboard.Dashboard +namespace osu.Game.Overlays.Dashboard.Home { public class DashboardBeatmapListing : CompositeDrawable { diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs similarity index 99% rename from osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs rename to osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs index d74cdf4414..0b660c16af 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; -namespace osu.Game.Overlays.Dashboard.Dashboard +namespace osu.Game.Overlays.Dashboard.Home { public abstract class DashboardBeatmapPanel : OsuClickableContainer { diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs similarity index 93% rename from osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.cs rename to osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs index c4782b733d..b212eaf20a 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardNewBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardNewBeatmapPanel.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Graphics; -namespace osu.Game.Overlays.Dashboard.Dashboard +namespace osu.Game.Overlays.Dashboard.Home { public class DashboardNewBeatmapPanel : DashboardBeatmapPanel { diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DashboardPopularBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs similarity index 96% rename from osu.Game/Overlays/Dashboard/Dashboard/DashboardPopularBeatmapPanel.cs rename to osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs index 04bb261dce..2fb5617796 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DashboardPopularBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; -namespace osu.Game.Overlays.Dashboard.Dashboard +namespace osu.Game.Overlays.Dashboard.Home { public class DashboardPopularBeatmapPanel : DashboardBeatmapPanel { diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs similarity index 97% rename from osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs rename to osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs index 6e42881de7..f6535b7db3 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DrawableBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Home/DrawableBeatmapList.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; -namespace osu.Game.Overlays.Dashboard.Dashboard +namespace osu.Game.Overlays.Dashboard.Home { public abstract class DrawableBeatmapList : CompositeDrawable { diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs similarity index 92% rename from osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs rename to osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs index 9856f6ae3e..75e8ca336d 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DrawableNewBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Home/DrawableNewBeatmapList.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -namespace osu.Game.Overlays.Dashboard.Dashboard +namespace osu.Game.Overlays.Dashboard.Home { public class DrawableNewBeatmapList : DrawableBeatmapList { diff --git a/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapList.cs b/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs similarity index 92% rename from osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapList.cs rename to osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs index 294a75c48a..90bd00008c 100644 --- a/osu.Game/Overlays/Dashboard/Dashboard/DrawablePopularBeatmapList.cs +++ b/osu.Game/Overlays/Dashboard/Home/DrawablePopularBeatmapList.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using osu.Game.Beatmaps; -namespace osu.Game.Overlays.Dashboard.Dashboard +namespace osu.Game.Overlays.Dashboard.Home { public class DrawablePopularBeatmapList : DrawableBeatmapList { From b96e32b0bbd741373689ad7755b54d576d87dd56 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 2 Aug 2020 12:26:09 -0700 Subject: [PATCH 2508/6909] Add xmldoc for updateBindTarget --- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index d58acc1ac4..e5b246807c 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -320,6 +320,9 @@ namespace osu.Game.Overlays.KeyBinding base.OnFocusLost(e); } + /// + /// Updates the bind target to the currently hovered key button or the first if clicked anywhere else. + /// private void updateBindTarget() { if (bindTarget != null) bindTarget.IsBinding = false; From f1ba576438eb84e9303619fb6bec9f54ad3c5a1c Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 2 Aug 2020 21:34:35 +0200 Subject: [PATCH 2509/6909] Disable overlay activation when in gameplay. --- osu.Game/Screens/Play/Player.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..24c27fde8d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -63,6 +63,8 @@ namespace osu.Game.Screens.Play private Bindable mouseWheelDisabled; + private Bindable gameplayOverlaysDisabled; + private readonly Bindable storyboardReplacesBackground = new Bindable(); public int RestartCount; @@ -77,6 +79,9 @@ namespace osu.Game.Screens.Play [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private OsuGame game { get; set; } + private SampleChannel sampleRestart; public BreakOverlay BreakOverlay; @@ -165,6 +170,7 @@ namespace osu.Game.Screens.Play sampleRestart = audio.Samples.Get(@"Gameplay/restart"); mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); + gameplayOverlaysDisabled = config.GetBindable(OsuSetting.GameplayDisableOverlayActivation); DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); @@ -197,6 +203,13 @@ namespace osu.Game.Screens.Play skipOverlay.Hide(); } + gameplayOverlaysDisabled.ValueChanged += disabled => + { + game.OverlayActivationMode.Value = disabled.NewValue && !DrawableRuleset.IsPaused.Value ? OverlayActivation.Disabled : OverlayActivation.All; + }; + DrawableRuleset.IsPaused.BindValueChanged(_ => gameplayOverlaysDisabled.TriggerChange()); + + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); // bind clock into components that require it @@ -627,6 +640,8 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHUD(HUDOverlay); + + gameplayOverlaysDisabled.TriggerChange(); } public override void OnSuspending(IScreen next) From ba77fa2945475ab3d6eb8092f384c4500f1a6f51 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 2 Aug 2020 12:41:35 -0700 Subject: [PATCH 2510/6909] Add test for clear button --- .../Settings/TestSceneKeyBindingPanel.cs | 40 +++++++++++++++++++ osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 14 +++---- 2 files changed, 47 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 3d335995ac..e7a1cab8eb 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -64,5 +64,45 @@ namespace osu.Game.Tests.Visual.Settings }, 0, true); }); } + + [Test] + public void TestClearButtonOnBindings() + { + KeyBindingRow backBindingRow = null; + + AddStep("click back binding row", () => + { + backBindingRow = panel.ChildrenOfType().ElementAt(10); + InputManager.MoveMouseTo(backBindingRow); + InputManager.Click(MouseButton.Left); + }); + + clickClearButton(); + + AddAssert("first binding cleared", () => string.IsNullOrEmpty(backBindingRow.Buttons.First().Text.Text)); + + AddStep("click second binding", () => + { + var target = backBindingRow.Buttons.ElementAt(1); + + InputManager.MoveMouseTo(target); + InputManager.Click(MouseButton.Left); + }); + + clickClearButton(); + + AddAssert("second binding cleared", () => string.IsNullOrEmpty(backBindingRow.Buttons.ElementAt(1).Text.Text)); + + void clickClearButton() + { + AddStep("click clear button", () => + { + var clearButton = backBindingRow.ChildrenOfType().Single(); + + InputManager.MoveMouseTo(clearButton); + InputManager.Click(MouseButton.Left); + }); + } + } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index e5b246807c..44b5fe09f4 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.KeyBinding private OsuSpriteText text; private Drawable pressAKey; - private FillFlowContainer buttons; + public FillFlowContainer Buttons; public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text); @@ -93,7 +93,7 @@ namespace osu.Game.Overlays.KeyBinding Text = action.GetDescription(), Margin = new MarginPadding(padding), }, - buttons = new FillFlowContainer + Buttons = new FillFlowContainer { AutoSizeAxes = Axes.Both, Anchor = Anchor.TopRight, @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.KeyBinding }; foreach (var b in bindings) - buttons.Add(new KeyButton(b)); + Buttons.Add(new KeyButton(b)); } public void RestoreDefaults() @@ -125,7 +125,7 @@ namespace osu.Game.Overlays.KeyBinding foreach (var d in Defaults) { - var button = buttons[i++]; + var button = Buttons[i++]; button.UpdateKeyCombination(d); store.Update(button.KeyBinding); } @@ -187,7 +187,7 @@ namespace osu.Game.Overlays.KeyBinding if (bindTarget.IsHovered) finalise(); - else if (buttons.Any(b => b.IsHovered)) + else if (Buttons.Any(b => b.IsHovered)) updateBindTarget(); } @@ -326,7 +326,7 @@ namespace osu.Game.Overlays.KeyBinding private void updateBindTarget() { if (bindTarget != null) bindTarget.IsBinding = false; - bindTarget = buttons.FirstOrDefault(b => b.IsHovered) ?? buttons.FirstOrDefault(); + bindTarget = Buttons.FirstOrDefault(b => b.IsHovered) ?? Buttons.FirstOrDefault(); if (bindTarget != null) bindTarget.IsBinding = true; } @@ -357,7 +357,7 @@ namespace osu.Game.Overlays.KeyBinding } } - private class KeyButton : Container + public class KeyButton : Container { public readonly Framework.Input.Bindings.KeyBinding KeyBinding; From d49e54deb60b3cfee754697c446b9b4ea21cfb2a Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 2 Aug 2020 12:47:23 -0700 Subject: [PATCH 2511/6909] Add failing test for another regressing behavior --- .../Settings/TestSceneKeyBindingPanel.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index e7a1cab8eb..96075d56d1 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -104,5 +104,37 @@ namespace osu.Game.Tests.Visual.Settings }); } } + + [Test] + public void TestClickRowSelectsFirstBinding() + { + KeyBindingRow backBindingRow = null; + + AddStep("click back binding row", () => + { + backBindingRow = panel.ChildrenOfType().ElementAt(10); + InputManager.MoveMouseTo(backBindingRow); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("first binding selected", () => backBindingRow.Buttons.First().IsBinding); + + AddStep("click second binding", () => + { + var target = backBindingRow.Buttons.ElementAt(1); + + InputManager.MoveMouseTo(target); + InputManager.Click(MouseButton.Left); + }); + + AddStep("click back binding row", () => + { + backBindingRow = panel.ChildrenOfType().ElementAt(10); + InputManager.MoveMouseTo(backBindingRow); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("first binding selected", () => backBindingRow.Buttons.First().IsBinding); + } } } From 7aafc018ad384f5e1d10437519df539a93f4a91b Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 2 Aug 2020 12:52:12 -0700 Subject: [PATCH 2512/6909] Prevent updating bind target when hovering cancel and clear buttons instead --- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index 44b5fe09f4..a7394579bd 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.KeyBinding public bool FilteringActive { get; set; } private OsuSpriteText text; - private Drawable pressAKey; + private FillFlowContainer cancelAndClearButtons; public FillFlowContainer Buttons; @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.KeyBinding Hollow = true, }; - Children = new[] + Children = new Drawable[] { new Box { @@ -99,7 +99,7 @@ namespace osu.Game.Overlays.KeyBinding Anchor = Anchor.TopRight, Origin = Anchor.TopRight }, - pressAKey = new FillFlowContainer + cancelAndClearButtons = new FillFlowContainer { AutoSizeAxes = Axes.Both, Padding = new MarginPadding(padding) { Top = height + padding * 2 }, @@ -187,7 +187,8 @@ namespace osu.Game.Overlays.KeyBinding if (bindTarget.IsHovered) finalise(); - else if (Buttons.Any(b => b.IsHovered)) + // prevent updating bind target before clear button's action + else if (!cancelAndClearButtons.Any(b => b.IsHovered)) updateBindTarget(); } @@ -298,8 +299,8 @@ namespace osu.Game.Overlays.KeyBinding if (HasFocus) GetContainingInputManager().ChangeFocus(null); - pressAKey.FadeOut(300, Easing.OutQuint); - pressAKey.BypassAutoSizeAxes |= Axes.Y; + cancelAndClearButtons.FadeOut(300, Easing.OutQuint); + cancelAndClearButtons.BypassAutoSizeAxes |= Axes.Y; } protected override void OnFocus(FocusEvent e) @@ -307,8 +308,8 @@ namespace osu.Game.Overlays.KeyBinding AutoSizeDuration = 500; AutoSizeEasing = Easing.OutQuint; - pressAKey.FadeIn(300, Easing.OutQuint); - pressAKey.BypassAutoSizeAxes &= ~Axes.Y; + cancelAndClearButtons.FadeIn(300, Easing.OutQuint); + cancelAndClearButtons.BypassAutoSizeAxes &= ~Axes.Y; updateBindTarget(); base.OnFocus(e); From fe97d472dfafa30cdd7686074191d6b04815446f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 2 Aug 2020 21:53:13 +0200 Subject: [PATCH 2513/6909] Enable back overlays when a replay is loaded. --- osu.Game/Screens/Play/Player.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 24c27fde8d..9d4a4741d5 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -205,10 +205,13 @@ namespace osu.Game.Screens.Play gameplayOverlaysDisabled.ValueChanged += disabled => { - game.OverlayActivationMode.Value = disabled.NewValue && !DrawableRuleset.IsPaused.Value ? OverlayActivation.Disabled : OverlayActivation.All; + if (DrawableRuleset.HasReplayLoaded.Value) + game.OverlayActivationMode.Value = OverlayActivation.UserTriggered; + else + game.OverlayActivationMode.Value = disabled.NewValue && !DrawableRuleset.IsPaused.Value ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; }; DrawableRuleset.IsPaused.BindValueChanged(_ => gameplayOverlaysDisabled.TriggerChange()); - + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => gameplayOverlaysDisabled.TriggerChange()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); From 435c9de8b9d500890fb649809f38d6263fca2a89 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 3 Aug 2020 15:25:23 +0900 Subject: [PATCH 2514/6909] Re-privatise buttons --- .../Visual/Settings/TestSceneKeyBindingPanel.cs | 12 ++++++------ osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 11 +++++------ 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 96075d56d1..e06b3a8a7e 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -79,11 +79,11 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("first binding cleared", () => string.IsNullOrEmpty(backBindingRow.Buttons.First().Text.Text)); + AddAssert("first binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType().First().Text.Text)); AddStep("click second binding", () => { - var target = backBindingRow.Buttons.ElementAt(1); + var target = backBindingRow.ChildrenOfType().ElementAt(1); InputManager.MoveMouseTo(target); InputManager.Click(MouseButton.Left); @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("second binding cleared", () => string.IsNullOrEmpty(backBindingRow.Buttons.ElementAt(1).Text.Text)); + AddAssert("second binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType().ElementAt(1).Text.Text)); void clickClearButton() { @@ -117,11 +117,11 @@ namespace osu.Game.Tests.Visual.Settings InputManager.Click(MouseButton.Left); }); - AddAssert("first binding selected", () => backBindingRow.Buttons.First().IsBinding); + AddAssert("first binding selected", () => backBindingRow.ChildrenOfType().First().IsBinding); AddStep("click second binding", () => { - var target = backBindingRow.Buttons.ElementAt(1); + var target = backBindingRow.ChildrenOfType().ElementAt(1); InputManager.MoveMouseTo(target); InputManager.Click(MouseButton.Left); @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Settings InputManager.Click(MouseButton.Left); }); - AddAssert("first binding selected", () => backBindingRow.Buttons.First().IsBinding); + AddAssert("first binding selected", () => backBindingRow.ChildrenOfType().First().IsBinding); } } } diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index a7394579bd..b808d49fa2 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -49,8 +49,7 @@ namespace osu.Game.Overlays.KeyBinding private OsuSpriteText text; private FillFlowContainer cancelAndClearButtons; - - public FillFlowContainer Buttons; + private FillFlowContainer buttons; public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text); @@ -93,7 +92,7 @@ namespace osu.Game.Overlays.KeyBinding Text = action.GetDescription(), Margin = new MarginPadding(padding), }, - Buttons = new FillFlowContainer + buttons = new FillFlowContainer { AutoSizeAxes = Axes.Both, Anchor = Anchor.TopRight, @@ -116,7 +115,7 @@ namespace osu.Game.Overlays.KeyBinding }; foreach (var b in bindings) - Buttons.Add(new KeyButton(b)); + buttons.Add(new KeyButton(b)); } public void RestoreDefaults() @@ -125,7 +124,7 @@ namespace osu.Game.Overlays.KeyBinding foreach (var d in Defaults) { - var button = Buttons[i++]; + var button = buttons[i++]; button.UpdateKeyCombination(d); store.Update(button.KeyBinding); } @@ -327,7 +326,7 @@ namespace osu.Game.Overlays.KeyBinding private void updateBindTarget() { if (bindTarget != null) bindTarget.IsBinding = false; - bindTarget = Buttons.FirstOrDefault(b => b.IsHovered) ?? Buttons.FirstOrDefault(); + bindTarget = buttons.FirstOrDefault(b => b.IsHovered) ?? buttons.FirstOrDefault(); if (bindTarget != null) bindTarget.IsBinding = true; } From 630322ff85a5c08a1b71e2a35404386fabd483e7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 3 Aug 2020 09:55:06 +0300 Subject: [PATCH 2515/6909] Adjust font weights in line with web --- osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs | 6 +++--- .../Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs index 0b660c16af..3badea155d 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardBeatmapPanel.cs @@ -102,17 +102,17 @@ namespace osu.Game.Overlays.Dashboard.Home { RelativeSizeAxes = Axes.X, Truncate = true, - Font = OsuFont.GetFont(weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(weight: FontWeight.Regular), Text = SetInfo.Metadata.Title }, new OsuSpriteText { RelativeSizeAxes = Axes.X, Truncate = true, - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), Text = SetInfo.Metadata.Artist }, - new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold)) + new LinkFlowContainer(f => f.Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular)) { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs b/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs index 2fb5617796..e9066c0657 100644 --- a/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/DashboardPopularBeatmapPanel.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.Dashboard.Home }, new OsuSpriteText { - Font = OsuFont.GetFont(size: 10), + Font = OsuFont.GetFont(size: 10, weight: FontWeight.Regular), Text = SetInfo.OnlineInfo.FavouriteCount.ToString() } } From af320e4a619784cd979c1eba9af93a9e29ee97d8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 3 Aug 2020 10:03:42 +0300 Subject: [PATCH 2516/6909] Fix NewsOverlay running request on startup --- osu.Game/Overlays/NewsOverlay.cs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index a5687b77e2..f8666e22c5 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -67,7 +67,26 @@ namespace osu.Game.Overlays protected override void LoadComplete() { base.LoadComplete(); - article.BindValueChanged(onArticleChanged, true); + article.BindValueChanged(onArticleChanged); + } + + private bool displayUpdateRequired = true; + + protected override void PopIn() + { + base.PopIn(); + + if (displayUpdateRequired) + { + article.TriggerChange(); + displayUpdateRequired = false; + } + } + + protected override void PopOutComplete() + { + base.PopOutComplete(); + displayUpdateRequired = true; } public void ShowFrontPage() From f812767c95428a679d6e5646fc9c6cc5b608ed17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Aug 2020 18:48:10 +0900 Subject: [PATCH 2517/6909] Add fallback hash generation to fix android startup crash --- osu.Game/OsuGameBase.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 278f2d849f..98f60d52d3 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -36,6 +36,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Skinning; using osuTK.Input; +using RuntimeInfo = osu.Framework.RuntimeInfo; namespace osu.Game { @@ -134,8 +135,17 @@ namespace osu.Game [BackgroundDependencyLoader] private void load() { - using (var str = File.OpenRead(typeof(OsuGameBase).Assembly.Location)) - VersionHash = str.ComputeMD5Hash(); + try + { + using (var str = File.OpenRead(typeof(OsuGameBase).Assembly.Location)) + VersionHash = str.ComputeMD5Hash(); + } + catch + { + // special case for android builds, which can't read DLLs from a packed apk. + // should eventually be handled in a better way. + VersionHash = $"{Version}-{RuntimeInfo.OS}".ComputeMD5Hash(); + } Resources.AddStore(new DllResourceStore(OsuResources.ResourceAssembly)); From c48648ea2ac014f40281f7b0f313949b7d454fda Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Mon, 3 Aug 2020 12:59:15 +0200 Subject: [PATCH 2518/6909] Add playfield shift like in osu-stable --- osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs index 9c8be868b0..93d88f3690 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs @@ -55,6 +55,7 @@ namespace osu.Game.Rulesets.Osu.UI // Scale = 819.2 / 512 // Scale = 1.6 Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X); + Position = new Vector2(0, 8 * Parent.ChildSize.Y / OsuPlayfield.BASE_SIZE.Y); // Size = 0.625 Size = Vector2.Divide(Vector2.One, Scale); } From 675f618b288cc1e75fed0a152c7908e61e77e9d4 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Mon, 3 Aug 2020 14:18:45 +0200 Subject: [PATCH 2519/6909] Apply playfield's offset only in play mode --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 2 +- .../UI/OsuPlayfieldAdjustmentContainer.cs | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index b4d51d11c9..e6e37fd58e 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo); - public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer(); + public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { PlayfieldShift = true }; protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay(); diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs index 93d88f3690..700601dbfd 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs @@ -15,6 +15,14 @@ namespace osu.Game.Rulesets.Osu.UI private const float playfield_size_adjust = 0.8f; + /// + /// Whether an osu-stable playfield offset should be applied (8 osu!pixels) + /// + public bool PlayfieldShift + { + set => ((ScalingContainer)content).PlayfieldShift = value; + } + public OsuPlayfieldAdjustmentContainer() { Anchor = Anchor.Centre; @@ -39,6 +47,8 @@ namespace osu.Game.Rulesets.Osu.UI ///
    private class ScalingContainer : Container { + internal bool PlayfieldShift { get; set; } + protected override void Update() { base.Update(); @@ -55,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.UI // Scale = 819.2 / 512 // Scale = 1.6 Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X); - Position = new Vector2(0, 8 * Parent.ChildSize.Y / OsuPlayfield.BASE_SIZE.Y); + Position = new Vector2(0, (PlayfieldShift ? 8f : 0f) * Parent.ChildSize.Y / OsuPlayfield.BASE_SIZE.Y); // Size = 0.625 Size = Vector2.Divide(Vector2.One, Scale); } From 4d6f60edaf8b49a95ede7b5a97fdcca16dfd13ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 3 Aug 2020 22:41:22 +0900 Subject: [PATCH 2520/6909] Fix multiplayer match select forcing playback even when user paused --- osu.Game/Screens/Multi/Multiplayer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 269eab5772..4912df17b1 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -17,6 +17,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; +using osu.Game.Overlays; using osu.Game.Screens.Menu; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Lounge; @@ -50,6 +51,9 @@ namespace osu.Game.Screens.Multi [Cached] private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); + [Resolved] + private MusicController music { get; set; } + [Cached(Type = typeof(IRoomManager))] private RoomManager roomManager; @@ -346,8 +350,7 @@ namespace osu.Game.Screens.Multi track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; track.Looping = true; - if (!track.IsRunning) - track.Restart(); + music.EnsurePlayingSomething(); } } else From 25ebb8619d2cbaa6fc232ed742d33da91b80d703 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 3 Aug 2020 16:00:10 +0200 Subject: [PATCH 2521/6909] Add tests. --- .../Gameplay/TestSceneOverlayActivation.cs | 54 +++++++++++++++++++ osu.Game/Screens/Play/Player.cs | 11 ++-- 2 files changed, 62 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs new file mode 100644 index 0000000000..107a3a2a4b --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.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.Framework.Allocation; +using osu.Game.Configuration; +using osu.Game.Overlays; +using osu.Game.Rulesets; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneOverlayActivation : OsuPlayerTestScene + { + private OverlayTestPlayer testPlayer; + + [Resolved] + private OsuConfigManager mng { get; set; } + + public override void SetUpSteps() + { + AddStep("disable overlay activation during gameplay", () => mng.Set(OsuSetting.GameplayDisableOverlayActivation, true)); + base.SetUpSteps(); + } + + [Test] + public void TestGameplayOverlayActivationSetting() + { + AddAssert("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); + } + + [Test] + public void TestGameplayOverlayActivationPaused() + { + AddUntilStep("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("pause gameplay", () => testPlayer.Pause()); + AddUntilStep("activation mode is user triggered", () => testPlayer.OverlayActivationMode == OverlayActivation.UserTriggered); + } + + [Test] + public void TestGameplayOverlayActivationReplayLoaded() + { + AddAssert("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("load a replay", () => testPlayer.DrawableRuleset.HasReplayLoaded.Value = true); + AddAssert("activation mode is user triggered", () => testPlayer.OverlayActivationMode == OverlayActivation.UserTriggered); + } + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => testPlayer = new OverlayTestPlayer(); + + private class OverlayTestPlayer : TestPlayer + { + public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value; + } + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9d4a4741d5..45a9b442be 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -79,7 +79,7 @@ namespace osu.Game.Screens.Play [Resolved] private IAPIProvider api { get; set; } - [Resolved] + [Resolved(CanBeNull = true)] private OsuGame game { get; set; } private SampleChannel sampleRestart; @@ -90,6 +90,8 @@ namespace osu.Game.Screens.Play private SkipOverlay skipOverlay; + protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.Disabled); + protected ScoreProcessor ScoreProcessor { get; private set; } protected HealthProcessor HealthProcessor { get; private set; } @@ -203,12 +205,15 @@ namespace osu.Game.Screens.Play skipOverlay.Hide(); } + if (game != null) + OverlayActivationMode.BindTo(game.OverlayActivationMode); + gameplayOverlaysDisabled.ValueChanged += disabled => { if (DrawableRuleset.HasReplayLoaded.Value) - game.OverlayActivationMode.Value = OverlayActivation.UserTriggered; + OverlayActivationMode.Value = OverlayActivation.UserTriggered; else - game.OverlayActivationMode.Value = disabled.NewValue && !DrawableRuleset.IsPaused.Value ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; + OverlayActivationMode.Value = disabled.NewValue && !DrawableRuleset.IsPaused.Value ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; }; DrawableRuleset.IsPaused.BindValueChanged(_ => gameplayOverlaysDisabled.TriggerChange()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => gameplayOverlaysDisabled.TriggerChange()); From 9d10658e3c8cd5d5f0b7d24018401c30fdbec246 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 20:14:17 +0300 Subject: [PATCH 2522/6909] Allow providing custom sprite text for RollingCounter --- .../Gameplay/Components/MatchScoreDisplay.cs | 18 ++++-- .../UserInterface/PercentageCounter.cs | 9 ++- .../Graphics/UserInterface/RollingCounter.cs | 61 +++++++++++++------ .../Graphics/UserInterface/ScoreCounter.cs | 9 ++- .../Expanded/Statistics/AccuracyStatistic.cs | 15 +++-- .../Expanded/Statistics/CounterStatistic.cs | 9 ++- .../Ranking/Expanded/TotalScoreCounter.cs | 17 ++++-- 7 files changed, 97 insertions(+), 41 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs index 2e7484542a..25417921bc 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs @@ -8,6 +8,7 @@ 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.Graphics.UserInterface; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; @@ -127,21 +128,28 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components private class MatchScoreCounter : ScoreCounter { + private OsuSpriteText displayedSpriteText; + public MatchScoreCounter() { Margin = new MarginPadding { Top = bar_height, Horizontal = 10 }; - - Winning = false; - - DisplayedCountSpriteText.Spacing = new Vector2(-6); } public bool Winning { - set => DisplayedCountSpriteText.Font = value + set => displayedSpriteText.Font = value ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true) : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true); } + + protected override OsuSpriteText CreateSpriteText() + { + displayedSpriteText = base.CreateSpriteText(); + displayedSpriteText.Spacing = new Vector2(-6); + Winning = false; + + return displayedSpriteText; + } } } } diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs index 940c9808ce..9b31935eee 100644 --- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs +++ b/osu.Game/Graphics/UserInterface/PercentageCounter.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Game.Graphics.Sprites; using osu.Game.Utils; namespace osu.Game.Graphics.UserInterface @@ -23,7 +24,6 @@ namespace osu.Game.Graphics.UserInterface public PercentageCounter() { - DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(fixedWidth: true); Current.Value = DisplayedCount = 1.0f; } @@ -37,6 +37,13 @@ namespace osu.Game.Graphics.UserInterface return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f; } + protected override OsuSpriteText CreateSpriteText() + { + var spriteText = base.CreateSpriteText(); + spriteText.Font = spriteText.Font.With(fixedWidth: true); + return spriteText; + } + public override void Increment(double amount) { Current.Value += amount; diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index cd244ed7e6..76bb4bf69d 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics.Sprites; using System; using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osuTK.Graphics; @@ -20,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface ///
    public Bindable Current = new Bindable(); - protected SpriteText DisplayedCountSpriteText; + private SpriteText displayedCountSpriteText; /// /// If true, the roll-up duration will be proportional to change in value. @@ -46,29 +47,49 @@ namespace osu.Game.Graphics.UserInterface public virtual T DisplayedCount { get => displayedCount; - set { if (EqualityComparer.Default.Equals(displayedCount, value)) return; displayedCount = value; - DisplayedCountSpriteText.Text = FormatCount(value); + if (displayedCountSpriteText != null) + displayedCountSpriteText.Text = FormatCount(value); } } public abstract void Increment(T amount); + private float textSize = 40f; + public float TextSize { - get => DisplayedCountSpriteText.Font.Size; - set => DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(size: value); + get => displayedCountSpriteText?.Font.Size ?? textSize; + set + { + if (TextSize == value) + return; + + textSize = value; + if (displayedCountSpriteText != null) + displayedCountSpriteText.Font = displayedCountSpriteText.Font.With(size: value); + } } + private Color4 accentColour; + public Color4 AccentColour { - get => DisplayedCountSpriteText.Colour; - set => DisplayedCountSpriteText.Colour = value; + get => displayedCountSpriteText?.Colour ?? accentColour; + set + { + if (AccentColour == value) + return; + + accentColour = value; + if (displayedCountSpriteText != null) + displayedCountSpriteText.Colour = value; + } } /// @@ -76,27 +97,21 @@ namespace osu.Game.Graphics.UserInterface /// protected RollingCounter() { - Children = new Drawable[] - { - DisplayedCountSpriteText = new OsuSpriteText { Font = OsuFont.Numeric } - }; - - TextSize = 40; AutoSizeAxes = Axes.Both; - DisplayedCount = Current.Value; - Current.ValueChanged += val => { - if (IsLoaded) TransformCount(displayedCount, val.NewValue); + if (IsLoaded) + TransformCount(DisplayedCount, val.NewValue); }; } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - - DisplayedCountSpriteText.Text = FormatCount(Current.Value); + displayedCountSpriteText = CreateSpriteText(); + displayedCountSpriteText.Text = FormatCount(displayedCount); + Child = displayedCountSpriteText; } /// @@ -167,5 +182,11 @@ namespace osu.Game.Graphics.UserInterface this.TransformTo(nameof(DisplayedCount), newValue, rollingTotalDuration, RollingEasing); } + + protected virtual OsuSpriteText CreateSpriteText() => new OsuSpriteText + { + Font = OsuFont.Numeric.With(size: textSize), + Colour = accentColour, + }; } } diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 01d8edaecf..438fe6c13b 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.UserInterface { @@ -24,7 +25,6 @@ namespace osu.Game.Graphics.UserInterface /// How many leading zeroes the counter will have. public ScoreCounter(uint leading = 0) { - DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(fixedWidth: true); LeadingZeroes = leading; } @@ -49,6 +49,13 @@ namespace osu.Game.Graphics.UserInterface return ((long)count).ToString(format); } + protected override OsuSpriteText CreateSpriteText() + { + var spriteText = base.CreateSpriteText(); + spriteText.Font = spriteText.Font.With(fixedWidth: true); + return spriteText; + } + public override void Increment(double amount) { Current.Value += amount; diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs index 2a0e33aab7..921ad80976 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Ranking.Expanded.Accuracy; using osu.Game.Utils; @@ -43,16 +44,18 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics 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; + + protected override OsuSpriteText CreateSpriteText() + { + var spriteText = base.CreateSpriteText(); + spriteText.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + spriteText.Spacing = new Vector2(-2, 0); + return spriteText; + } } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index 817cc9b8c2..cc0f49c968 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Ranking.Expanded.Accuracy; using osuTK; @@ -43,10 +44,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; - public Counter() + protected override OsuSpriteText CreateSpriteText() { - DisplayedCountSpriteText.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); - DisplayedCountSpriteText.Spacing = new Vector2(-2, 0); + var spriteText = base.CreateSpriteText(); + spriteText.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + spriteText.Spacing = new Vector2(-2, 0); + return spriteText; } public override void Increment(int amount) diff --git a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs index cab04edb8b..b0060d19ac 100644 --- a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs +++ b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Ranking.Expanded.Accuracy; using osuTK; @@ -23,15 +24,21 @@ namespace osu.Game.Screens.Ranking.Expanded // 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"); + protected override OsuSpriteText CreateSpriteText() + { + var spriteText = base.CreateSpriteText(); + spriteText.Anchor = Anchor.TopCentre; + spriteText.Origin = Anchor.TopCentre; + + spriteText.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true); + spriteText.Spacing = new Vector2(-5, 0); + return spriteText; + } + public override void Increment(long amount) => Current.Value += amount; } From 29053048ff8ec0e33aef5f2d9ef8966e59b29c75 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 21:40:13 +0300 Subject: [PATCH 2523/6909] Add support to use legacy combo fonts for the counter on legacy skins --- osu.Game.Rulesets.Catch/CatchSkinComponents.cs | 3 ++- .../Skinning/CatchLegacySkinTransformer.cs | 11 +++++++++++ .../Skinning/OsuLegacySkinTransformer.cs | 4 +--- osu.Game/Skinning/LegacySkinConfiguration.cs | 2 ++ osu.Game/Skinning/LegacySkinTransformer.cs | 2 ++ 5 files changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs index 80390705fe..23d8428fec 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Catch Droplet, CatcherIdle, CatcherFail, - CatcherKiai + CatcherKiai, + CatchComboCounter } } diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index d929da1a29..c2432e1dbb 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; using osuTK; +using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Catch.Skinning { @@ -51,6 +52,16 @@ namespace osu.Game.Rulesets.Catch.Skinning case CatchSkinComponents.CatcherKiai: return this.GetAnimation("fruit-catcher-kiai", true, true, true) ?? this.GetAnimation("fruit-ryuuta", true, true, true); + + case CatchSkinComponents.CatchComboCounter: + var comboFont = GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; + var fontOverlap = GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; + + // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. + if (HasFont(comboFont)) + return new LegacyComboCounter(Source, comboFont, fontOverlap); + + break; } return null; diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 81d1d05b66..b955150c90 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Skinning var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default"; var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2; - return !hasFont(font) + return !HasFont(font) ? null : new LegacySpriteText(Source, font) { @@ -145,7 +145,5 @@ namespace osu.Game.Rulesets.Osu.Skinning return Source.GetConfig(lookup); } - - private bool hasFont(string fontName) => Source.GetTexture($"{fontName}-0") != null; } } diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index 41b7aea34b..1d5412d93f 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.cs @@ -15,6 +15,8 @@ namespace osu.Game.Skinning public enum LegacySetting { Version, + ComboPrefix, + ComboOverlap, AnimationFramerate, LayeredHitSounds, } diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index ebc4757e75..acca53835c 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -47,5 +47,7 @@ namespace osu.Game.Skinning } public abstract IBindable GetConfig(TLookup lookup); + + protected bool HasFont(string fontPrefix) => GetTexture($"{fontPrefix}-0") != null; } } From f5af81d775874f928c8bf8cdeb12fffee23d9cce Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 21:41:22 +0300 Subject: [PATCH 2524/6909] Add legacy combo font glyphs to legacy skins of test purposes --- .../Resources/old-skin/score-0.png | Bin 0 -> 3092 bytes .../Resources/old-skin/score-1.png | Bin 0 -> 1237 bytes .../Resources/old-skin/score-2.png | Bin 0 -> 3134 bytes .../Resources/old-skin/score-3.png | Bin 0 -> 3712 bytes .../Resources/old-skin/score-4.png | Bin 0 -> 2395 bytes .../Resources/old-skin/score-5.png | Bin 0 -> 3067 bytes .../Resources/old-skin/score-6.png | Bin 0 -> 3337 bytes .../Resources/old-skin/score-7.png | Bin 0 -> 1910 bytes .../Resources/old-skin/score-8.png | Bin 0 -> 3652 bytes .../Resources/old-skin/score-9.png | Bin 0 -> 3561 bytes .../Resources/special-skin/score-0@2x.png | Bin 0 -> 2184 bytes .../Resources/special-skin/score-1@2x.png | Bin 0 -> 923 bytes .../Resources/special-skin/score-2@2x.png | Bin 0 -> 2196 bytes .../Resources/special-skin/score-3@2x.png | Bin 0 -> 2510 bytes .../Resources/special-skin/score-4@2x.png | Bin 0 -> 1551 bytes .../Resources/special-skin/score-5@2x.png | Bin 0 -> 2265 bytes .../Resources/special-skin/score-6@2x.png | Bin 0 -> 2438 bytes .../Resources/special-skin/score-7@2x.png | Bin 0 -> 1700 bytes .../Resources/special-skin/score-8@2x.png | Bin 0 -> 2498 bytes .../Resources/special-skin/score-9@2x.png | Bin 0 -> 2494 bytes 20 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-1.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-2.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-4.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-5.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-8.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-9.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-3@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-4@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-8@2x.png create mode 100644 osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-9@2x.png diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-0.png new file mode 100644 index 0000000000000000000000000000000000000000..8304617d8c94a8400b50a90f364941bb02983065 GIT binary patch literal 3092 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 Y0OO;-v8NaCZ2$lO07*qoM6N<$f&jDokN^Mx literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-3.png new file mode 100644 index 0000000000000000000000000000000000000000..82bec3babebd59263dc486e5900a3a01ea4aaed7 GIT binary patch literal 3712 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 JPDHLkV1kmK?iT<6 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-6.png new file mode 100644 index 0000000000000000000000000000000000000000..b4cf81f26e5cab5a068ce282ee22b15b92d0df12 GIT binary patch literal 3337 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 TZ*tsI00000NkvXXu0mjfuoP_o literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png b/osu.Game.Rulesets.Catch.Tests/Resources/old-skin/score-7.png new file mode 100644 index 0000000000000000000000000000000000000000..a23f5379b223d61079e055162fdd93f107f0ec02 GIT binary patch literal 1910 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-0@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..508cc85e4ae2ad01c53c93c314502071de8a5e1f GIT binary patch literal 2184 zcmV;32zU31P)EX>4Tx04R}tkv&MmKpe$iQ^g_`2aAX}M5s;{L`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4P#IXWr2NQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G5+ ziMW`_u8Li+2thzU!WfpBWz0!Z629Z>9s$1I#dwzgxj#pbnzI-X5Q%4*VcNtS#M7I$ z!FiuJ!ius=d`>)O(glehxvqHp#<}3Kz%wIeIyFxmAr=d5th6yJni}yGaa7fG$`>*o ztDLtuYvn3y-jlyDoYPm9xlVHk2`pj>5=1DdqJ%PR#Aww?v5=zuxQ~C(^-JVZ$W;O( z#{w$QAiI9>Klt6Pm7kpOlEQJI^TlyKMu4tepiy(2?_(i|qi@OreYZgOn%7%%AEysMnz~Bf00)P_ zXo0fVecl~v@9p0+&HjD>Ds*y+$NP(?00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00v-5L_t(|+U=cNh*eb>$Ny_|9B0f+-ZC$lm$b}CYw}WN z6j2r=`p|=E(gh8=M+BigWDrGxR0JhdP+;kUg_4=l1xjX6Obr#y#Ika7GR<4gIL^$k zhqFZ(^6a(tIeYJQ&g?&&w{_0<-@o;pz1BYa?C+B@Wy+K(Q>KVAX&V4~Ntz_-MoBkF z8YHP-@aitF6jF)gK>brrx1Kx>|iISafCT;Z6_bAf#!*|-%LQ>yST0@jBd;JQK%5U2zm2TtXQ zaSFJ*)Zq08Hiy7_6{sjuya~X8kSw29D)5E?yF=hT97EiJz>YjI+kq#2hu1sgAOK#B zG5&DiP)PiFzQXGkY$5V#Wdc4X11)(xv;epH2=Jp2c!z<033+)r#LF>YoKNr`ve@9x zPtZ@b#TI7=(A@`kV?wqO0Gm_rHQQoR-%cA|CGdlV(b*|@GZwo6fSG9me9pq)o?`9y zBKUjI!rvc2k2K5;22NTSd?MwY!BGp3ucTq_V+&)gz`&GGGs`S|o=NnY8JJ@E)6zN* z7P;w`URNh+>I+L#3q1g6fWIvLPDsK~t;3I-S9-Ef{nXO$swB*;wlwoMU_1f%tfl96 zV7%++S2!3ME$JT1;Tw%Hcyh6{(Q=%T^i*sBB`uKsEr5;kM_VN|S&p3p^ej3+piKxcT3U$qhw&Kuk?&DZAk;{UQPfD@Yk=9D-l<tK`0F5y}D_6%VPNPX_gyZ<{3kDeI%b1P&n1R?Hw++D0 zN<9OCK8}mXCSy!Pu>fw6^lyo0psOeAqzMIRjA@p%J_8s%1EpL6MO$4RJe=s0u5}o@ zC0IO*3UH0XYFsVpI?n)3a~QkK<%6vN8e>`|eGrr3xEGMFviP%jpp%kT6dhoo_k!iU zqyS2qDEnvqJ~76eigPwsS=Jvo>`?|jW9hjq_L|f|xlLP*^#q{pq_0TI&>Bla3p{{l zfJV!T9oHmfsx}B1rnMdboaRl6UpDz2SG)}&#+N7Hqc$k% znXlaEE%H%7Phfip#Kkej9~$&o%C~6Dkb6ve|SFY0J1w%?& zDrr$kclGK3u{y}cVmIXLsIP#DDp$qW*|5H>I~)EF$eQ31@Y@1ILrGG)rtiS-|NH3fs%kaO4o0000< KMNUMnLSTYH3*fx~ literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-1@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..84f74e1ec9d45818d76353aa66ed1a728a2ad118 GIT binary patch literal 923 zcmeAS@N?(olHy`uVBq!ia0vp^mOz}v!3HGRn10@2U|?*?baoE#baqxKD9TUE%t>Wn z(3n^|(bnUzgUr$R;G;^S?A;v}E-U=RTwGNj4Vmj_xx~^;pkw0NMhQ|)H9wzLi--SmSfy^?`Xxe|2ys7`)6&I5D@50pP6{;i1d-mv$q|r zoNsZcYm--vpH12&)=v4I>ni>n+xcMA0b}JfD^q_NMKSK&HCqg|XGwh&DGa$pH808$vxazra!lJ)B`s*)z^?nj` zB9uw_IFr@|h23=@?0?){J9Yk?50efRF8F-3(9W%4UF?Fu;yLe=k7v$dIGFyxEB~i< zX~MI~U!uIbmCDp`+`Ki;O^-g5Z=fq4vg+%PZ!6Kid%2*?DskxAmQ+__%h$F5U#QS8JRDvEEBsp z9z96gT`K%h@y+TToPBvs&HLK&AG~<Km8VxsGZpplN_U0GQbAsLngbaTCSj)V3+1^c`<~?C7xb^LO(J_0EC4Xxk z`7Mr6p1;InqLOQuh-MJkjMS?$KYs0XSSPxtZfod?jtGW6u6t&B)$*F@lWn`|A8CI4 zm)Ya{K<38(qx;r1az7BPI2miOboqV-{mg^uhCyB@Uus=CbLP$MdkmNE&1rvga=AM& Pju<>${an^LB{Ts5NqUv} literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-2@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..49625c6623c2d07188e263708d2649af82eefdd7 GIT binary patch literal 2196 zcmV;F2y6F=P)UW&V0004mX+uL$Nkc;* zaB^>EX>4Tx04R}tkv&MmKpe$iQ^g_`2aAX}M5s;{L`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4P#IXWr2NQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G5+ ziMW`_u8Li+2thzU!WfpBWz0!Z629Z>9s$1I#dwzgxj#pbnzI-X5Q%4*VcNtS#M7I$ z!FiuJ!ius=d`>)O(glehxvqHp#<}3Kz%wIeIyFxmAr=d5th6yJni}yGaa7fG$`>*o ztDLtuYvn3y-jlyDoYPm9xlVHk2`pj>5=1DdqJ%PR#Aww?v5=zuxQ~C(^-JVZ$W;O( z#{w$QAiI9>Klt6Pm7kpOlEQJI^TlyKMu4tepiy(2?_(i|qi@OreYZgOn%7%%AEysMnz~Bf00)P_ zXo0fVecl~v@9p0+&HjD>Ds*y+$NP(?00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00wMHL_t(|+U=ZMY!p=($NvXf5DMjvqNv3JMHCQ_fG8jk zLeX&1hy)?gfRd2-q%q;e3sGaDz9=zb5Ce!3yd(*mBmh`Qp&BmBRmoyZhr=$rEh|!YDBgpoEq|MIjJ!8zlOCt1uk-$5^ z1>)5O-~*r%Xpuw^#lURfdzwdkfdxR@L=+!jh@=&gZfN4GCnX(~bj*3xICV;QNtZkC ztwOr9N76iF%=+evjw0Z3;JnMf_ZRRsFc0YKBfplwt-!Ou_K=bD2GBXzKq@?n$`8N; zKBZ2K|C zR#2S<#>eGU8E_!O;6K2W92VOdj!hdd6QXve1o$n(&^cgIGgR=!MttYhAd%7Fn{{+U;rxwB}1b(UbJL}C1!j7;~$U8(x?tp`wT0p$SDK8)d6 zSu&lQpy9;}meKN57^7PrEU`>C*#=5wmS=kt2xW_;T8ng*fuK0$ZCI9%4ke9oKOFvl&^l?H)Q z?~&1)&Va0S9zbaq1WJ`hMy(}1CuukEA}|hU)tp3(F=s5B#KmvkIh z1Iz)6n~6=0MViw=pd7Xc!^x7CIuSGnXqjUur50(bf{Z6x0;fVQGkg@);*<}6+gkcK z(|4p8V=k&;y*7;Qko0nnqNJl`y4^v?J{u!&b}n#Z4xn_kNK-9oM^GrsB-KUW@QfTl z8DNp-V`EHRASlL|Ba)(+=d{B{lxN*Zj8`CIuaU|N6(sRM3PKFetlw*`Q*K>041|S1`?pj*e+$#&Y z0#X4S@d4x+r9nWsv+Kx9HNfn&0O<$pYXZm)pkG>mjPf!1eLe5{AdP_B+r(D$)4;5> zz%am*O*FeZfQqyLDFNPZqPQ$cQ*kK+e)ND*?Y6v`uRv}Gj(Gt24!AB&K<0bwmaccU zm=~u7MltYO6KDS>rYSnQ0bh9lSr_R+rAq+FO&&eeI%gl9NfVIi&dE==)8`}71j7JN zdlZ+|shA9E1f)Iip~rdQhmG0dtY z&8!1$GOiuz6PKYe&YcIDT9mZjDKIR<$F6Z0=?xsu0%UPQ8-WKi{49#vKq;`xrO23s zLRr+v&y#T&Se^yQ_9XW~fG->0*AR8)Q(P`>DNitv%QE^;TcgapbC&Ce0G>#Ar*dNE zyiJsuuX4G;rcJ`3yp%aQk1FHQF5{V36Aq*Z_#=qVLGE01Dk z{B(8iY2x#TfrFA3hdM(O4n!+8ocS3eX|5W!#(T-LPBA_VZjt0?m|dra)0@?DIT;?Y ziX?SV!(|mguWcP@jM-@g#krDszjAmNc8=83>%Q!!>XZ?A2qbhxJ2Vf0)EZ;Xg@H08 zZBgO}N<{(6|JA(!<-aXYsib^dxH=IgQaUL=6{`5ODnPLt0_0`!DX5@=3M#0e-0NR0 W2Bg8WSc5SD0000UW&V0004mX+uL$Nkc;* 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 BxF!Gq literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-5@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d8250b0c63d96fe5e5ecfa92b639d8fc5f61ded7 GIT binary patch literal 2265 zcmV;~2qyQ5P)EX>4Tx04R}tkv&MmKpe$iQ^g_`2aAX}M5s;{L`5963Pq?8YK2xEOfLO`CJjl7 zi=*ILaPVWX>fqw6tAnc`2!4P#IXWr2NQwVT3N2zhIPS;0dyl(!fKV?p&FUBjG~G5+ ziMW`_u8Li+2thzU!WfpBWz0!Z629Z>9s$1I#dwzgxj#pbnzI-X5Q%4*VcNtS#M7I$ z!FiuJ!ius=d`>)O(glehxvqHp#<}3Kz%wIeIyFxmAr=d5th6yJni}yGaa7fG$`>*o ztDLtuYvn3y-jlyDoYPm9xlVHk2`pj>5=1DdqJ%PR#Aww?v5=zuxQ~C(^-JVZ$W;O( z#{w$QAiI9>Klt6Pm7kpOlEQJI^TlyKMu4tepiy(2?_(i|qi@OreYZgOn%7%%AEysMnz~Bf00)P_ zXo0fVecl~v@9p0+&HjD>Ds*y+$NP(?00006VoOIv00000008+zyMF)x010qNS#tmY z3ljhU3ljkVnw%H_000McNlirueSad^gZEa<4bO1wgWnpw> zWFU8GbZ8()Nlj2!fese{00yy1L_t(|+U=cbY!y`$$N#4mfwm|DvWc-GihwJEDA8zC zM3D~=7ec^&P1FR97=p_e6B9pRB8ef8sEG=KalsFUXo6UciW^}O1rci^xI%@33Y7Ne zhq;8;rmvm3Z{Ezb&Of;cDKm4<`@Q?_a?ZVXq+Gdj<;vCd1WX1t1NQvC6kj^fVM(*iY(*SSRVo{b z1I)jYW|`UBaXFP0IHhWfq?^oaQ=AX2=mubxN*W(Gn63`zCrQ)IY_^&0FNcAy2iVJZ3scnIi|*iQ1f&DSSI1GFOJOYg9661?DgTNhTc5*w{^pte5q%o34yV31nH{z{vrKw~aB}5vayU7h98Nk) zJT>F;=_e`3UXGA7Ku2Rem6G7g|mdAP9e?oGWR%B%crZucUcq zwlEVl2e`gSN@f)>4d~%v%Ycc%v%v2jHEB`Y3#X#M`OW=|ZUdUzIQAKEQH*v^0ABI{ z^geKW8gP#E7%gp*ar1#=%4Gj)pgtm?Z-GmK95_k?DlY+ws_ogRSu`PCj4LE)O*}h+tGNZ zJHheUR@6&DfIo**L?`8NimN(@Q(RR!oQ~U_ZV$&dT>DS4|M1ER0>dSZQ;@Q4mb61s zotZVH2&XFH8rKR)1#l~{se>JMOMvQ>=2x>k=U3;HYPc3y7t+|4k`4O#c#kKXq26r{ zFjms%lFkq5=SWF!0LLcG$EU{rZDzlC!2wQ`^p2!~5sjWMX->j$j`O(Zhf?NNFG?Eb z>FnHu;naHE^Su`wmmQp1g0lmF!3o0g*=D`gx8}@?!Qn9(hf^0Bj@uu8bqo$Ca@d($ zViL8MHcDFWQ*$nn)H?>hw`RE3yk}<3K5)jx;P5^L$G9XRpVbpfJG-D5oPjYoys)!x zCPqz;c>Q0JR{6p?u|#M8ko0Uq?MF~Kf8>Q8F9nbnt4Rp)rDT9~1P1yn{{e%l0$F4gFbA&vdz!WgLp z%{;;s3*s;uX3fhkpjIgg*cbuKFR>kM)y~nmZFPUjV5UR>(+o_F)A;;O3|^Qbm}7vo z5#T%;w*#kjVsLzlaGs6;=9O|ev8)55GgAUHwatc>EcJFSei!d#CEY){W0DU^?!8QT+0fRiB zKM1%B_%uRSy}n#qh0OCq+#qR@f^#U^Q;UK`;5r4T_it0MRk~5Z_U0-VX6UOR33#@G zq~UmlU)<0j=?O`T6AY?F`j%fS!%+-jdZKwiUu9juDZn#sw{W@C41DNbmy+1)TP#LM zy(P_b!KX_l4Jq-F`y_oT>3vBn(l|Za%2=a@J4h20hH>tBVztm>$7sUDac=c nC24);+LBzka^=dED|7k}Cp+Yxt)OTj00000NkvXXu0mjfFn}8V literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-6@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..75d3cbd3bd5931289026636b1426e69e97eca4ca GIT binary patch literal 2438 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 Ef(UtvIRF3v literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png b/osu.Game.Rulesets.Catch.Tests/Resources/special-skin/score-7@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..cfe2021df40680208e385ab41ee12ab157899712 GIT binary patch literal 1700 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 IM6N<$g4va<@Bjb+ literal 0 HcmV?d00001 From d6f36457a8736963b7710cab79ceca1f5512ac35 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 21:43:03 +0300 Subject: [PATCH 2525/6909] Fix legacy font glyphs being mistaken for animation and getting "extrapolated" --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 28 +++++++++++++++------ 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 81c13112d0..49ba02fdea 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text.RegularExpressions; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -152,25 +153,38 @@ namespace osu.Game.Tests.Visual { private readonly bool extrapolateAnimations; + private readonly HashSet legacyFontPrefixes = new HashSet(); + public TestLegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, bool extrapolateAnimations) : base(skin, storage, audioManager, "skin.ini") { this.extrapolateAnimations = extrapolateAnimations; + + legacyFontPrefixes.Add(GetConfig("HitCirclePrefix")?.Value ?? "default"); + legacyFontPrefixes.Add(GetConfig("ScorePrefix")?.Value ?? "score"); + legacyFontPrefixes.Add(GetConfig("ComboPrefix")?.Value ?? "score"); } public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { // extrapolate frames to test longer animations - if (extrapolateAnimations) - { - var match = Regex.Match(componentName, "-([0-9]*)"); - - if (match.Length > 0 && int.TryParse(match.Groups[1].Value, out var number) && number < 60) - return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT); - } + if (extrapolateAnimations && isAnimationComponent(componentName, out var number) && number < 60) + return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT); return base.GetTexture(componentName, wrapModeS, wrapModeT); } + + private bool isAnimationComponent(string componentName, out int number) + { + number = 0; + + // legacy font glyph textures have the pattern "{fontPrefix}-{character}", which could be mistaken for an animation frame. + if (legacyFontPrefixes.Any(p => componentName.StartsWith($"{p}-"))) + return false; + + var match = Regex.Match(componentName, "-([0-9]*)"); + return match.Length > 0 && int.TryParse(match.Groups[1].Value, out number); + } } } } From f37ba49f7f624614b99ddfadc5e54785d0722b5b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 22:13:02 +0300 Subject: [PATCH 2526/6909] Add catch-specific combo counter with its legacy design --- .../Skinning/LegacyComboCounter.cs | 139 ++++++++++++++++++ .../UI/CatchComboDisplay.cs | 59 ++++++++ osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 32 +++- .../UI/ICatchComboCounter.cs | 34 +++++ 4 files changed, 262 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs create mode 100644 osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs create mode 100644 osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs new file mode 100644 index 0000000000..9700bd0e08 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.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; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Screens.Play; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Skinning +{ + internal class LegacyComboCounter : CompositeDrawable, ICatchComboCounter + { + private readonly ISkin skin; + + private readonly string fontName; + private readonly float fontOverlap; + + private readonly LegacyRollingCounter counter; + private LegacyRollingCounter lastExplosion; + + public LegacyComboCounter(ISkin skin, string fontName, float fontOverlap) + { + this.skin = skin; + + this.fontName = fontName; + this.fontOverlap = fontOverlap; + + AutoSizeAxes = Axes.Both; + + Alpha = 0f; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Scale = new Vector2(0.8f); + + InternalChild = counter = new LegacyRollingCounter(skin, fontName, fontOverlap) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }; + } + + public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); + public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); + + private void updateCombo(int combo, Color4? hitObjectColour, bool immediate) + { + // Combo fell to zero, roll down and fade out the counter. + if (combo == 0) + { + counter.Current.Value = 0; + if (lastExplosion != null) + lastExplosion.Current.Value = 0; + + this.FadeOut(immediate ? 0.0 : 400.0, Easing.Out); + return; + } + + // There may still be previous transforms being applied, finish them and remove explosion. + FinishTransforms(true); + if (lastExplosion != null) + RemoveInternal(lastExplosion); + + this.FadeIn().Delay(1000.0).FadeOut(300.0); + + // For simplicity, in the case of rewinding we'll just set the counter to the current combo value. + immediate |= Time.Elapsed < 0; + + if (immediate) + { + counter.SetCountWithoutRolling(combo); + return; + } + + counter.ScaleTo(1.5f).ScaleTo(0.8f, 250.0, Easing.Out) + .OnComplete(c => c.SetCountWithoutRolling(combo)); + + counter.Delay(250.0).ScaleTo(1f).ScaleTo(1.1f, 60.0).Then().ScaleTo(1f, 30.0); + + var explosion = new LegacyRollingCounter(skin, fontName, fontOverlap) + { + Alpha = 0.65f, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.5f), + Colour = hitObjectColour ?? Color4.White, + Depth = 1f, + }; + + AddInternal(explosion); + + explosion.SetCountWithoutRolling(combo); + explosion.ScaleTo(1.9f, 400.0, Easing.Out) + .FadeOut(400.0) + .Expire(true); + + lastExplosion = explosion; + } + + private class LegacyRollingCounter : RollingCounter + { + private readonly ISkin skin; + + private readonly string fontName; + private readonly float fontOverlap; + + protected override bool IsRollingProportional => true; + + public LegacyRollingCounter(ISkin skin, string fontName, float fontOverlap) + { + this.skin = skin; + this.fontName = fontName; + this.fontOverlap = fontOverlap; + } + + public override void Increment(int amount) => Current.Value += amount; + + protected override double GetProportionalDuration(int currentValue, int newValue) + { + return Math.Abs(newValue - currentValue) * 75.0; + } + + protected override OsuSpriteText CreateSpriteText() + { + return new LegacySpriteText(skin, fontName) + { + Spacing = new Vector2(-fontOverlap, 0f) + }; + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs new file mode 100644 index 0000000000..10aee2ea31 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + public class CatchComboDisplay : SkinnableDrawable + { + private int currentCombo; + + [CanBeNull] + public ICatchComboCounter ComboCounter => Drawable as ICatchComboCounter; + + public CatchComboDisplay() + : base(new CatchSkinComponent(CatchSkinComponents.CatchComboCounter), _ => Empty()) + { + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + ComboCounter?.DisplayInitialCombo(currentCombo); + } + + public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result) + { + if (!result.Judgement.AffectsCombo || !result.HasResult) + return; + + if (result.Type == HitResult.Miss) + { + updateCombo(0, null); + return; + } + + updateCombo(result.ComboAtJudgement + 1, judgedObject.AccentColour.Value); + } + + public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result) + { + if (!result.Judgement.AffectsCombo || !result.HasResult) + return; + + updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value); + } + + private void updateCombo(int newCombo, Color4? hitObjectColour) + { + currentCombo = newCombo; + ComboCounter?.UpdateCombo(newCombo, hitObjectColour); + } + } +} diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 154e1576db..b5c040f80d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -28,6 +28,7 @@ namespace osu.Game.Rulesets.Catch.UI public const float CENTER_X = WIDTH / 2; internal readonly CatcherArea CatcherArea; + private readonly CatchComboDisplay comboDisplay; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => // only check the X position; handle all vertical space. @@ -48,12 +49,22 @@ namespace osu.Game.Rulesets.Catch.UI Origin = Anchor.TopLeft, }; + comboDisplay = new CatchComboDisplay + { + AutoSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.None, + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + Y = 30f, + }; + InternalChildren = new[] { explodingFruitContainer, CatcherArea.MovableCatcher.CreateProxiedContent(), HitObjectContainer, - CatcherArea + CatcherArea, + comboDisplay, }; } @@ -62,6 +73,7 @@ namespace osu.Game.Rulesets.Catch.UI public override void Add(DrawableHitObject h) { h.OnNewResult += onNewResult; + h.OnRevertResult += onRevertResult; base.Add(h); @@ -69,7 +81,23 @@ namespace osu.Game.Rulesets.Catch.UI fruit.CheckPosition = CheckIfWeCanCatch; } + protected override void Update() + { + base.Update(); + comboDisplay.X = CatcherArea.MovableCatcher.X; + } + private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) - => CatcherArea.OnResult((DrawableCatchHitObject)judgedObject, result); + { + var catchObject = (DrawableCatchHitObject)judgedObject; + CatcherArea.OnResult(catchObject, result); + + comboDisplay.OnNewResult(catchObject, result); + } + + private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result) + { + comboDisplay.OnRevertResult((DrawableCatchHitObject)judgedObject, result); + } } } diff --git a/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs new file mode 100644 index 0000000000..1363ed1352 --- /dev/null +++ b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.UI +{ + /// + /// An interface providing a set of methods to update the combo counter. + /// + public interface ICatchComboCounter : IDrawable + { + /// + /// Updates the counter to display the provided as initial value. + /// The value should be immediately displayed without any animation. + /// + /// + /// This is required for when instantiating a combo counter in middle of accumulating combo (via skin change). + /// + /// The combo value to be displayed as initial. + void DisplayInitialCombo(int combo); + + /// + /// Updates the counter to animate a transition from the old combo value it had to the current provided one. + /// + /// + /// This is called regardless of whether the clock is rewinding. + /// + /// The new combo value. + /// The colour of the object if hit, null on miss. + void UpdateCombo(int combo, Color4? hitObjectColour); + } +} From 21eaf0e99515fb708f5e00787295f94296f997ee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 22:14:00 +0300 Subject: [PATCH 2527/6909] Expose "is break time" bindable within GameplayBeatmap --- osu.Game/Screens/Play/GameplayBeatmap.cs | 5 +++++ osu.Game/Screens/Play/Player.cs | 1 + 2 files changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index 64894544f4..d7eed73275 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -16,6 +16,11 @@ namespace osu.Game.Screens.Play { public readonly IBeatmap PlayableBeatmap; + /// + /// Whether the gameplay is currently in a break. + /// + public IBindable IsBreakTime { get; } = new Bindable(); + public GameplayBeatmap(IBeatmap playableBeatmap) { PlayableBeatmap = playableBeatmap; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..ee32ded93d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -612,6 +612,7 @@ namespace osu.Game.Screens.Play // bind component bindables. Background.IsBreakTime.BindTo(breakTracker.IsBreakTime); + gameplayBeatmap.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); From 65c269e473d15abb7bebd072dbb01f96f9849869 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 22:17:11 +0300 Subject: [PATCH 2528/6909] Hide combo counter on gameplay break Intentionally inside LegacyComboCounter and not in CatchComboDisplay, to avoid conflicting with how the legacy combo counter fades away after 1 second of no combo update, can move to parent once a DefaultComboCounter design is decided and code is shareable between. --- .../Skinning/LegacyComboCounter.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 9700bd0e08..90dd1f4e9f 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -47,6 +47,23 @@ namespace osu.Game.Rulesets.Catch.Skinning }; } + private IBindable isBreakTime; + + [Resolved(canBeNull: true)] + private GameplayBeatmap beatmap { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + isBreakTime = beatmap?.IsBreakTime.GetBoundCopy(); + isBreakTime?.BindValueChanged(b => + { + if (b.NewValue) + this.FadeOut(400.0, Easing.OutQuint); + }); + } + public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); From 5cd2841080caf8e41b135d740a246f5d6535abae Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 3 Aug 2020 22:17:42 +0300 Subject: [PATCH 2529/6909] Add test scene showing off the skinnable catch-specific combo counter --- .../TestSceneComboCounter.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs new file mode 100644 index 0000000000..2581e305dd --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -0,0 +1,79 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +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.Screens.Play; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneComboCounter : CatchSkinnableTestScene + { + private ScoreProcessor scoreProcessor; + private GameplayBeatmap gameplayBeatmap; + private readonly Bindable isBreakTime = new BindableBool(); + + [BackgroundDependencyLoader] + private void load() + { + gameplayBeatmap = new GameplayBeatmap(CreateBeatmapForSkinProvider()); + gameplayBeatmap.IsBreakTime.BindTo(isBreakTime); + Dependencies.Cache(gameplayBeatmap); + Add(gameplayBeatmap); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + scoreProcessor = new ScoreProcessor(); + + SetContents(() => new CatchComboDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(2.5f), + }); + }); + + [Test] + public void TestCatchComboCounter() + { + AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20); + AddStep("perform miss", () => performJudgement(HitResult.Miss)); + AddToggleStep("toggle gameplay break", v => isBreakTime.Value = v); + } + + private void performJudgement(HitResult type, Judgement judgement = null) + { + var judgedObject = new TestDrawableCatchHitObject(new TestCatchHitObject()); + var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type }; + scoreProcessor.ApplyResult(result); + + foreach (var counter in CreatedDrawables.Cast()) + counter.OnNewResult(judgedObject, result); + } + + private class TestDrawableCatchHitObject : DrawableCatchHitObject + { + public TestDrawableCatchHitObject(CatchHitObject hitObject) + : base(hitObject) + { + AccentColour.Value = Color4.White; + } + } + + private class TestCatchHitObject : CatchHitObject + { + } + } +} From 242a035f7e693dcbd00c0f0d381cd6f16c211f9e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 3 Aug 2020 21:25:45 +0200 Subject: [PATCH 2530/6909] Apply review suggestions. --- .../Gameplay/TestSceneOverlayActivation.cs | 4 +-- osu.Game/Screens/Play/Player.cs | 25 +++++++++++-------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 107a3a2a4b..9e93cf363d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -14,11 +14,11 @@ namespace osu.Game.Tests.Visual.Gameplay private OverlayTestPlayer testPlayer; [Resolved] - private OsuConfigManager mng { get; set; } + private OsuConfigManager config { get; set; } public override void SetUpSteps() { - AddStep("disable overlay activation during gameplay", () => mng.Set(OsuSetting.GameplayDisableOverlayActivation, true)); + AddStep("disable overlay activation during gameplay", () => config.Set(OsuSetting.GameplayDisableOverlayActivation, true)); base.SetUpSteps(); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 45a9b442be..7906f5bfe1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -90,7 +90,10 @@ namespace osu.Game.Screens.Play private SkipOverlay skipOverlay; - protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.Disabled); + /// + /// The current activation mode for overlays. + /// + protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.UserTriggered); protected ScoreProcessor ScoreProcessor { get; private set; } @@ -208,15 +211,9 @@ namespace osu.Game.Screens.Play if (game != null) OverlayActivationMode.BindTo(game.OverlayActivationMode); - gameplayOverlaysDisabled.ValueChanged += disabled => - { - if (DrawableRuleset.HasReplayLoaded.Value) - OverlayActivationMode.Value = OverlayActivation.UserTriggered; - else - OverlayActivationMode.Value = disabled.NewValue && !DrawableRuleset.IsPaused.Value ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; - }; - DrawableRuleset.IsPaused.BindValueChanged(_ => gameplayOverlaysDisabled.TriggerChange()); - DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => gameplayOverlaysDisabled.TriggerChange()); + gameplayOverlaysDisabled.ValueChanged += disabled => updateOverlayActivationMode(); + DrawableRuleset.IsPaused.BindValueChanged(_ => updateOverlayActivationMode()); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateOverlayActivationMode()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); @@ -362,6 +359,14 @@ namespace osu.Game.Screens.Play HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue; } + private void updateOverlayActivationMode() + { + if (DrawableRuleset.HasReplayLoaded.Value) + OverlayActivationMode.Value = OverlayActivation.UserTriggered; + else + OverlayActivationMode.Value = gameplayOverlaysDisabled.Value && !DrawableRuleset.IsPaused.Value ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; + } + private void updatePauseOnFocusLostState() => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost && !DrawableRuleset.HasReplayLoaded.Value From 30c7a6f6a72d9c711c32e4dc351c6d74ed39a6b2 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 3 Aug 2020 21:33:18 +0200 Subject: [PATCH 2531/6909] Fix CI issue and use method instead of triggering change on bindable. --- 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 7906f5bfe1..819942e6af 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -211,7 +211,7 @@ namespace osu.Game.Screens.Play if (game != null) OverlayActivationMode.BindTo(game.OverlayActivationMode); - gameplayOverlaysDisabled.ValueChanged += disabled => updateOverlayActivationMode(); + gameplayOverlaysDisabled.BindValueChanged(_ => updateOverlayActivationMode()); DrawableRuleset.IsPaused.BindValueChanged(_ => updateOverlayActivationMode()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateOverlayActivationMode()); @@ -654,7 +654,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHUD(HUDOverlay); - gameplayOverlaysDisabled.TriggerChange(); + updateOverlayActivationMode(); } public override void OnSuspending(IScreen next) From 4dbf695bca47f62ef35bc8724ae64871a752720c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 4 Aug 2020 00:04:00 +0300 Subject: [PATCH 2532/6909] Fix wrong ordering --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index b5c040f80d..d4a1740c12 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -51,8 +51,8 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay = new CatchComboDisplay { - AutoSizeAxes = Axes.Both, RelativeSizeAxes = Axes.None, + AutoSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, Y = 30f, From 22b52d63c7767f4cc9d75c4ff249cf5d5e6828c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Aug 2020 20:51:59 +0900 Subject: [PATCH 2533/6909] 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 13b4b6ebbb..924e9c4a16 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 745555e0e2..627c2f3d33 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 f1080f0c8b..f443937017 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From d7e82efb671016649e13a1afcaec640691567392 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 4 Aug 2020 21:16:59 +0900 Subject: [PATCH 2534/6909] Fix tests --- .../Visual/UserInterface/TestSceneLogoTrackingContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs index 010e4330d7..5582cc6826 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLogoTrackingContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -263,7 +264,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void moveLogoFacade() { - if (logoFacade?.Transforms.Count == 0 && transferContainer?.Transforms.Count == 0) + if (!(logoFacade?.Transforms).Any() && !(transferContainer?.Transforms).Any()) { Random random = new Random(); trackingContainer.Delay(500).MoveTo(new Vector2(random.Next(0, (int)logo.Parent.DrawWidth), random.Next(0, (int)logo.Parent.DrawHeight)), 300); From 9fb7b8f3d8b6e1c83c0de13c3d4eb1af8d4dc7dd Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Tue, 4 Aug 2020 15:43:33 +0200 Subject: [PATCH 2535/6909] Rename the property to "AlignWithStoryboard" --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 2 +- .../UI/OsuPlayfieldAdjustmentContainer.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index e6e37fd58e..b2299398e1 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override PassThroughInputManager CreateInputManager() => new OsuInputManager(Ruleset.RulesetInfo); - public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { PlayfieldShift = true }; + public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { AlignWithStoryboard = true }; protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay(); diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs index 700601dbfd..0d1a5a8304 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfieldAdjustmentContainer.cs @@ -11,16 +11,17 @@ namespace osu.Game.Rulesets.Osu.UI public class OsuPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer { protected override Container Content => content; - private readonly Container content; + private readonly ScalingContainer content; private const float playfield_size_adjust = 0.8f; /// - /// Whether an osu-stable playfield offset should be applied (8 osu!pixels) + /// When true, an offset is applied to allow alignment with historical storyboards displayed in the same parent space. + /// This will shift the playfield downwards slightly. /// - public bool PlayfieldShift + public bool AlignWithStoryboard { - set => ((ScalingContainer)content).PlayfieldShift = value; + set => content.PlayfieldShift = value; } public OsuPlayfieldAdjustmentContainer() @@ -65,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.UI // Scale = 819.2 / 512 // Scale = 1.6 Scale = new Vector2(Parent.ChildSize.X / OsuPlayfield.BASE_SIZE.X); - Position = new Vector2(0, (PlayfieldShift ? 8f : 0f) * Parent.ChildSize.Y / OsuPlayfield.BASE_SIZE.Y); + Position = new Vector2(0, (PlayfieldShift ? 8f : 0f) * Scale.X); // Size = 0.625 Size = Vector2.Divide(Vector2.One, Scale); } From 24bc9b33b1437cf845834743c7019a5429773b46 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 5 Aug 2020 15:40:45 +0900 Subject: [PATCH 2536/6909] Always place spinners behind hitcircles/sliders --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 600efefca3..4ef9bbe091 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -23,7 +23,8 @@ namespace osu.Game.Rulesets.Osu.UI { public class OsuPlayfield : Playfield { - private readonly ApproachCircleProxyContainer approachCircles; + private readonly ProxyContainer approachCircles; + private readonly ProxyContainer spinnerProxies; private readonly JudgementContainer judgementLayer; private readonly FollowPointRenderer followPoints; private readonly OrderedHitPolicy hitPolicy; @@ -38,6 +39,10 @@ namespace osu.Game.Rulesets.Osu.UI { InternalChildren = new Drawable[] { + spinnerProxies = new ProxyContainer + { + RelativeSizeAxes = Axes.Both + }, followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both, @@ -54,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.UI { Child = HitObjectContainer, }, - approachCircles = new ApproachCircleProxyContainer + approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both, Depth = -1, @@ -76,6 +81,9 @@ namespace osu.Game.Rulesets.Osu.UI h.OnNewResult += onNewResult; h.OnLoadComplete += d => { + if (d is DrawableSpinner) + spinnerProxies.Add(d.CreateProxy()); + if (d is IDrawableHitObjectWithProxiedApproach c) approachCircles.Add(c.ProxiedLayer.CreateProxy()); }; @@ -113,9 +121,9 @@ namespace osu.Game.Rulesets.Osu.UI public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => HitObjectContainer.ReceivePositionalInputAt(screenSpacePos); - private class ApproachCircleProxyContainer : LifetimeManagementContainer + private class ProxyContainer : LifetimeManagementContainer { - public void Add(Drawable approachCircleProxy) => AddInternal(approachCircleProxy); + public void Add(Drawable proxy) => AddInternal(proxy); } private class DrawableJudgementPool : DrawablePool From 71895964f408c1fec6c8b6ca8155ca03aca0e044 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 5 Aug 2020 11:21:09 +0200 Subject: [PATCH 2537/6909] Refactor overlay activation logic and reword tip. --- osu.Game/Screens/Menu/Disclaimer.cs | 2 +- osu.Game/Screens/Play/Player.cs | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 986de1edf0..fcb9aacd76 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -190,7 +190,7 @@ namespace osu.Game.Screens.Menu { "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!", + "All settings are dynamic and take effect in real-time. Try pausing and 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!", diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 819942e6af..8f8128abfc 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -361,10 +361,12 @@ namespace osu.Game.Screens.Play private void updateOverlayActivationMode() { - if (DrawableRuleset.HasReplayLoaded.Value) + bool canTriggerOverlays = DrawableRuleset.IsPaused.Value || !gameplayOverlaysDisabled.Value; + + if (DrawableRuleset.HasReplayLoaded.Value || canTriggerOverlays) OverlayActivationMode.Value = OverlayActivation.UserTriggered; else - OverlayActivationMode.Value = gameplayOverlaysDisabled.Value && !DrawableRuleset.IsPaused.Value ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; + OverlayActivationMode.Value = OverlayActivation.Disabled; } private void updatePauseOnFocusLostState() => From bb73489ae5947c4be39ecaca7a3634f4b49c6db1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Aug 2020 18:44:34 +0900 Subject: [PATCH 2538/6909] Fix very short spinners being impossible to complete --- .../TestSceneSpinner.cs | 40 ++++++++++++++----- .../Objects/Drawables/DrawableSpinner.cs | 13 +++++- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 3 +- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index b57561f3e1..be92a25dbe 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -17,32 +17,54 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; - public TestSceneSpinner() + private TestDrawableSpinner drawableSpinner; + + [TestCase(false)] + [TestCase(true)] + public void TestVariousSpinners(bool autoplay) { AddStep("Miss Big", () => SetContents(() => testSingle(2))); AddStep("Miss Medium", () => SetContents(() => testSingle(5))); AddStep("Miss Small", () => SetContents(() => testSingle(7))); - AddStep("Hit Big", () => SetContents(() => testSingle(2, true))); - AddStep("Hit Medium", () => SetContents(() => testSingle(5, true))); - AddStep("Hit Small", () => SetContents(() => testSingle(7, true))); + AddStep("Hit Big", () => SetContents(() => testSingle(2, autoplay))); + AddStep("Hit Medium", () => SetContents(() => testSingle(5, autoplay))); + AddStep("Hit Small", () => SetContents(() => testSingle(7, autoplay))); } - private Drawable testSingle(float circleSize, bool auto = false) + [TestCase(false)] + [TestCase(true)] + public void TestLongSpinner(bool autoplay) { - var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + 5000 }; + AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 2000))); + AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult); + AddUntilStep("Check correct progress", () => drawableSpinner.Progress == (autoplay ? 1 : 0)); + } + + [TestCase(false)] + [TestCase(true)] + public void TestSuperShortSpinner(bool autoplay) + { + AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 200))); + AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult); + AddUntilStep("Short spinner implicitly completes", () => drawableSpinner.Progress == 1); + } + + private Drawable testSingle(float circleSize, bool auto = false, double length = 3000) + { + var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + +2000 + length }; spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); - var drawable = new TestDrawableSpinner(spinner, auto) + drawableSpinner = new TestDrawableSpinner(spinner, auto) { Anchor = Anchor.Centre, Depth = depthIndex++ }; foreach (var mod in SelectedMods.Value.OfType()) - mod.ApplyToDrawableHitObjects(new[] { drawable }); + mod.ApplyToDrawableHitObjects(new[] { drawableSpinner }); - return drawable; + return drawableSpinner; } private class TestDrawableSpinner : DrawableSpinner diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 68516bedf8..7363da0de8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -175,7 +175,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// /// The completion progress of this spinner from 0..1 (clamped). /// - public float Progress => Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1); + public float Progress + { + get + { + if (Spinner.SpinsRequired == 0) + // some spinners are so short they can't require an integer spin count. + // these become implicitly hit. + return 1; + + return Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1); + } + } protected override void CheckForResult(bool userTriggered, double timeOffset) { diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 619b49926e..1658a4e7c2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.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.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; @@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Objects double minimumRotationsPerSecond = stable_matching_fudge * BeatmapDifficulty.DifficultyRange(difficulty.OverallDifficulty, 3, 5, 7.5); - SpinsRequired = (int)Math.Max(1, (secondsDuration * minimumRotationsPerSecond)); + SpinsRequired = (int)(secondsDuration * minimumRotationsPerSecond); MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); } From 3916d98e5292db1546b4b9ced3bcea8c27ae887f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Aug 2020 18:50:37 +0900 Subject: [PATCH 2539/6909] Add comment for clarity --- osu.Game/Overlays/NewsOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index f8666e22c5..09fb445b1f 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -67,6 +67,8 @@ namespace osu.Game.Overlays protected override void LoadComplete() { base.LoadComplete(); + + // should not be run until first pop-in to avoid requesting data before user views. article.BindValueChanged(onArticleChanged); } From bf1bb3267420e64400d8991c34bb2b65f026c9c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 5 Aug 2020 19:06:58 +0900 Subject: [PATCH 2540/6909] Add missing toolbar tooltips for right-hand icons --- osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs | 2 ++ osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs | 2 ++ osu.Game/Overlays/Toolbar/ToolbarChatButton.cs | 2 ++ osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs | 2 ++ osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs | 2 ++ osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs | 2 ++ osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs | 2 ++ 7 files changed, 14 insertions(+) diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs index eecb368ee9..3c38fdd207 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs @@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar public ToolbarBeatmapListingButton() { SetIcon(OsuIcon.ChevronDownCircle); + TooltipMain = "Beatmap Listing"; + TooltipSub = "Browse for new beatmaps"; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs index 84210e27a4..c88b418853 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs @@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar public ToolbarChangelogButton() { SetIcon(FontAwesome.Solid.Bullhorn); + TooltipMain = "Changelog"; + TooltipSub = "Track recent dev updates in the osu! ecosystem"; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs index ad0e5be551..ec7da54571 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs @@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar public ToolbarChatButton() { SetIcon(FontAwesome.Solid.Comments); + TooltipMain = "Chat"; + TooltipSub = "Join the real-time discussion"; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index b29aec5842..712da12208 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar public ToolbarMusicButton() { Icon = FontAwesome.Solid.Music; + TooltipMain = "Now playing"; + TooltipSub = "Manage the currently playing track"; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs index e813a3f4cb..106c67a041 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs @@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar public ToolbarNewsButton() { Icon = FontAwesome.Solid.Newspaper; + TooltipMain = "News"; + TooltipSub = "Get up-to-date on community happenings"; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs index cbd097696d..c026ce99fe 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs @@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar public ToolbarRankingsButton() { SetIcon(FontAwesome.Regular.ChartBar); + TooltipMain = "Ranking"; + TooltipSub = "Find out who's the best right now"; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index f6646eb81d..0dbb552c15 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -11,6 +11,8 @@ namespace osu.Game.Overlays.Toolbar public ToolbarSocialButton() { Icon = FontAwesome.Solid.Users; + TooltipMain = "Friends"; + TooltipSub = "Interact with those close to you"; } [BackgroundDependencyLoader(true)] From 84f6b7608cf83a76db915d148d6c7f2090290742 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 5 Aug 2020 20:05:53 +0300 Subject: [PATCH 2541/6909] Remove misleading ExpandNumberPiece lookup --- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 5 ----- osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs | 1 - 2 files changed, 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 80602dfa40..81d1d05b66 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; using osuTK; -using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Osu.Skinning { @@ -139,10 +138,6 @@ namespace osu.Game.Rulesets.Osu.Skinning // HitCircleOverlayAboveNumer (with typo) should still be supported for now. return Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber) ?? Source.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumer); - - case OsuSkinConfiguration.ExpandNumberPiece: - bool expand = !(source.GetConfig(LegacySetting.Version)?.Value >= 2.0m); - return SkinUtils.As(new BindableBool(expand)); } break; diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index c04312a2c3..154160fdb5 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -7,7 +7,6 @@ namespace osu.Game.Rulesets.Osu.Skinning { HitCirclePrefix, HitCircleOverlap, - ExpandNumberPiece, SliderBorderSize, SliderPathRadius, AllowSliderBallTint, From 1ab6110c05e7fc4e0bbc18ec3847636e847780b2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 5 Aug 2020 20:07:05 +0300 Subject: [PATCH 2542/6909] Apply fade out to the number piece with quarter the pieces duration --- .../Skinning/LegacyMainCirclePiece.cs | 67 ++++++++++++------- 1 file changed, 43 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index fe28e69ff2..41fe170ae0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; +using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Osu.Skinning { @@ -28,7 +29,9 @@ namespace osu.Game.Rulesets.Osu.Skinning Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } + private Container circlePieces; private Sprite hitCircleSprite, hitCircleOverlay; + private SkinnableSpriteText hitCircleText; private readonly IBindable state = new Bindable(); @@ -45,12 +48,27 @@ namespace osu.Game.Rulesets.Osu.Skinning InternalChildren = new Drawable[] { - hitCircleSprite = new Sprite + circlePieces = new Container { - Texture = getTextureWithFallback(string.Empty), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + hitCircleSprite = new Sprite + { + Texture = getTextureWithFallback(string.Empty), + Colour = drawableObject.AccentColour.Value, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + hitCircleOverlay = new Sprite + { + Texture = getTextureWithFallback("overlay"), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + } }, hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText { @@ -61,31 +79,16 @@ namespace osu.Game.Rulesets.Osu.Skinning Anchor = Anchor.Centre, Origin = Anchor.Centre, }, - hitCircleOverlay = new Sprite - { - Texture = getTextureWithFallback("overlay"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - } }; bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; - if (!overlayAboveNumber) - ChangeInternalChildDepth(hitCircleText, -float.MaxValue); + if (overlayAboveNumber) + AddInternal(hitCircleOverlay.CreateProxy()); state.BindTo(drawableObject.State); accentColour.BindTo(drawableObject.AccentColour); indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - state.BindValueChanged(updateState, true); - accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); - indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); Texture getTextureWithFallback(string name) { @@ -98,6 +101,15 @@ namespace osu.Game.Rulesets.Osu.Skinning } } + protected override void LoadComplete() + { + base.LoadComplete(); + + state.BindValueChanged(updateState, true); + accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); + indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); + } + private void updateState(ValueChangedEvent state) { const double legacy_fade_duration = 240; @@ -105,13 +117,20 @@ namespace osu.Game.Rulesets.Osu.Skinning switch (state.NewValue) { case ArmedState.Hit: - this.FadeOut(legacy_fade_duration, Easing.Out); + circlePieces.FadeOut(legacy_fade_duration, Easing.Out); + circlePieces.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - hitCircleSprite.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - hitCircleOverlay.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; - if (skin.GetConfig(OsuSkinConfiguration.ExpandNumberPiece)?.Value ?? true) + if (legacyVersion >= 2.0m) + // legacy skins of version 2.0 and newer apply immediate fade out to the number piece and only that. + hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); + else + { + // old skins scale and fade it normally along other pieces. + hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + } break; } From 43161697f88983bc271715e792c7a89eed9631d0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 5 Aug 2020 23:41:57 +0300 Subject: [PATCH 2543/6909] Fix wrong english --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 41fe170ae0..74a2cb7dc5 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Skinning var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; if (legacyVersion >= 2.0m) - // legacy skins of version 2.0 and newer apply immediate fade out to the number piece and only that. + // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); else { From 9465e7abe10e22482d598f9529d43a9b7468706f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 5 Aug 2020 23:45:00 +0300 Subject: [PATCH 2544/6909] Rename sprites container to "circleSprites" --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 74a2cb7dc5..9fbd9f50b4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } - private Container circlePieces; + private Container circleSprites; private Sprite hitCircleSprite, hitCircleOverlay; private SkinnableSpriteText hitCircleText; @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning InternalChildren = new Drawable[] { - circlePieces = new Container + circleSprites = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -117,8 +117,8 @@ namespace osu.Game.Rulesets.Osu.Skinning switch (state.NewValue) { case ArmedState.Hit: - circlePieces.FadeOut(legacy_fade_duration, Easing.Out); - circlePieces.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + circleSprites.FadeOut(legacy_fade_duration, Easing.Out); + circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; From 19a0eaade9b48f65bbf51ebbac98ba32b98b9dba Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Thu, 6 Aug 2020 04:41:44 +0200 Subject: [PATCH 2545/6909] Allow storyboard sprites to load textures from skins --- .../Drawables/DrawableStoryboardSprite.cs | 39 ++++++++++++++++--- 1 file changed, 34 insertions(+), 5 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index d8d3248659..d40af903a6 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Skinning; namespace osu.Game.Storyboards.Drawables { @@ -17,6 +18,12 @@ namespace osu.Game.Storyboards.Drawables { public StoryboardSprite Sprite { get; } + private ISkinSource currentSkin; + + private TextureStore storyboardTextureStore; + + private string texturePath; + private bool flipH; public bool FlipH @@ -114,14 +121,36 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(IBindable beatmap, TextureStore textureStore) + private void load(ISkinSource skin, IBindable beatmap, TextureStore textureStore) { - var path = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - if (path == null) - return; + if (skin != null) + { + currentSkin = skin; + skin.SourceChanged += onChange; + } + + storyboardTextureStore = textureStore; + + texturePath = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; + + skinChanged(); - Texture = textureStore.Get(path); Sprite.ApplyTransforms(this); } + + private void onChange() => + // schedule required to avoid calls after disposed. + // note that this has the side-effect of components only performing a possible texture change when they are alive. + Scheduler.AddOnce(skinChanged); + + private void skinChanged() + { + var newTexture = currentSkin?.GetTexture(Sprite.Path) ?? storyboardTextureStore?.Get(texturePath); + + if (Texture == newTexture) return; + + Size = Vector2.Zero; // Sprite size needs to be recalculated (e.g. aspect ratio of combo number textures may differ between skins) + Texture = newTexture; + } } } From e3f314349abc74ebc3ef6e1774045325fd763a75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Aug 2020 12:27:30 +0900 Subject: [PATCH 2546/6909] Don't use title case Co-authored-by: Joseph Madamba --- osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs index 3c38fdd207..64430c77ac 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs @@ -11,7 +11,7 @@ namespace osu.Game.Overlays.Toolbar public ToolbarBeatmapListingButton() { SetIcon(OsuIcon.ChevronDownCircle); - TooltipMain = "Beatmap Listing"; + TooltipMain = "Beatmap listing"; TooltipSub = "Browse for new beatmaps"; } From d5324be07d00ff9276831f2263ac326efa020f35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Aug 2020 12:33:40 +0900 Subject: [PATCH 2547/6909] Fix malformed testcase --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index be92a25dbe..ed89a4c991 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -23,12 +23,10 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(true)] public void TestVariousSpinners(bool autoplay) { - AddStep("Miss Big", () => SetContents(() => testSingle(2))); - AddStep("Miss Medium", () => SetContents(() => testSingle(5))); - AddStep("Miss Small", () => SetContents(() => testSingle(7))); - AddStep("Hit Big", () => SetContents(() => testSingle(2, autoplay))); - AddStep("Hit Medium", () => SetContents(() => testSingle(5, autoplay))); - AddStep("Hit Small", () => SetContents(() => testSingle(7, autoplay))); + string term = autoplay ? "Hit" : "Miss"; + AddStep($"{term} Big", () => SetContents(() => testSingle(2, autoplay))); + AddStep($"{term} Medium", () => SetContents(() => testSingle(5, autoplay))); + AddStep($"{term} Small", () => SetContents(() => testSingle(7, autoplay))); } [TestCase(false)] From 3b15a50f0d343fbf985abc0167d86f04972958df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Aug 2020 12:34:42 +0900 Subject: [PATCH 2548/6909] Fix unnecessary + character --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index ed89a4c991..47b3926ceb 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -49,7 +49,13 @@ namespace osu.Game.Rulesets.Osu.Tests private Drawable testSingle(float circleSize, bool auto = false, double length = 3000) { - var spinner = new Spinner { StartTime = Time.Current + 2000, EndTime = Time.Current + +2000 + length }; + const double delay = 2000; + + var spinner = new Spinner + { + StartTime = Time.Current + delay, + EndTime = Time.Current + delay + length + }; spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); From 9a00ad48c617a4d490ab5420698b2656c3679a45 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 14:43:39 +0900 Subject: [PATCH 2549/6909] Update components to use extension methods --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 1 + osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs | 2 ++ osu.Game/Screens/Play/PauseOverlay.cs | 1 + 3 files changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 7363da0de8..a2a49b5c42 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index 168e937256..3f7dc957fb 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -117,6 +117,8 @@ namespace osu.Game.Rulesets.UI public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); + public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotImplementedException(); + public BindableNumber Volume => throw new NotSupportedException(); public BindableNumber Balance => throw new NotSupportedException(); diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index fa917cda32..97f1d1c91d 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Game.Audio; using osu.Game.Graphics; From 641279ec3e8397f23b2f21c200cf99af313d9253 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 14:43:48 +0900 Subject: [PATCH 2550/6909] Make SkinnableSound an IAdjustableAudioComponent --- osu.Game/Skinning/SkinnableSound.cs | 42 ++++++++--------------------- 1 file changed, 11 insertions(+), 31 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 27f6c37895..11856fa581 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -4,19 +4,18 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; 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; using osu.Game.Screens.Play; namespace osu.Game.Skinning { - public class SkinnableSound : SkinReloadableDrawable + public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent { private readonly ISampleInfo[] hitSamples; @@ -143,36 +142,17 @@ namespace osu.Game.Skinning public BindableNumber Tempo => samplesContainer.Tempo; + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => samplesContainer.AddAdjustment(type, adjustBindable); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => samplesContainer.RemoveAdjustment(type, adjustBindable); + + public void RemoveAllAdjustments(AdjustableProperty type) + => samplesContainer.RemoveAllAdjustments(type); + public bool IsPlaying => samplesContainer.Any(s => s.Playing); - /// - /// 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); - #endregion } } From 6e42b8219c4510b856923aa08712c50dbf37fa87 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 4 Aug 2020 21:53:00 +0900 Subject: [PATCH 2551/6909] Move track to MusicController, compiles --- .../TestSceneHoldNoteInput.cs | 2 +- .../TestSceneOutOfOrderHits.cs | 2 +- .../TestSceneSliderInput.cs | 2 +- .../TestSceneSliderSnaking.cs | 15 +- .../TestSceneSpinnerRotation.cs | 13 +- .../Skinning/TestSceneDrawableTaikoMascot.cs | 4 +- .../Skinning/TestSceneTaikoPlayfield.cs | 2 +- .../Skins/TestSceneBeatmapSkinResources.cs | 3 +- .../Visual/Editing/TimelineTestScene.cs | 11 +- .../TestSceneCompletionCancellation.cs | 15 +- .../Gameplay/TestSceneGameplayRewinding.cs | 15 +- .../TestSceneNightcoreBeatContainer.cs | 4 +- .../Visual/Gameplay/TestScenePause.cs | 2 +- .../Visual/Gameplay/TestScenePlayerLoader.cs | 6 +- .../Visual/Gameplay/TestSceneStoryboard.cs | 10 +- .../Visual/Menus/TestSceneIntroWelcome.cs | 7 +- .../Navigation/TestSceneScreenNavigation.cs | 23 +- .../TestSceneBeatSyncedContainer.cs | 5 +- .../TestSceneNowPlayingOverlay.cs | 4 +- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 16 -- osu.Game/Beatmaps/IWorkingBeatmap.cs | 5 - osu.Game/Beatmaps/WorkingBeatmap.cs | 22 +- .../Containers/BeatSyncedContainer.cs | 16 +- osu.Game/OsuGame.cs | 23 +- osu.Game/OsuGameBase.cs | 10 - osu.Game/Overlays/Music/PlaylistOverlay.cs | 10 +- osu.Game/Overlays/MusicController.cs | 221 ++++++++++++++++-- osu.Game/Overlays/NowPlayingOverlay.cs | 10 +- osu.Game/Rulesets/Mods/IApplicableToTrack.cs | 2 +- osu.Game/Rulesets/Mods/ModDaycore.cs | 6 +- osu.Game/Rulesets/Mods/ModNightcore.cs | 6 +- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 4 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 4 +- .../Edit/Components/BottomBarContainer.cs | 2 - .../Edit/Components/PlaybackControl.cs | 8 +- .../Timelines/Summary/Parts/MarkerPart.cs | 5 +- .../Timelines/Summary/Parts/TimelinePart.cs | 8 +- .../Compose/Components/Timeline/Timeline.cs | 34 +-- .../Timeline/TimelineTickDisplay.cs | 7 +- osu.Game/Screens/Edit/Editor.cs | 8 +- osu.Game/Screens/Edit/EditorClock.cs | 22 +- osu.Game/Screens/Menu/IntroScreen.cs | 8 +- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- osu.Game/Screens/Menu/IntroWelcome.cs | 6 +- osu.Game/Screens/Menu/LogoVisualisation.cs | 12 +- osu.Game/Screens/Menu/MainMenu.cs | 16 +- osu.Game/Screens/Menu/OsuLogo.cs | 9 +- .../Multi/Match/Components/ReadyButton.cs | 6 +- osu.Game/Screens/Multi/Multiplayer.cs | 23 +- osu.Game/Screens/Play/FailAnimation.cs | 12 +- .../Screens/Play/GameplayClockContainer.cs | 46 +--- osu.Game/Screens/Select/SongSelect.cs | 34 ++- osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs | 2 - osu.Game/Tests/Visual/EditorClockTestScene.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 8 +- .../Visual/RateAdjustedBeatmapTestScene.cs | 2 +- 57 files changed, 438 insertions(+), 346 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 95072cf4f8..c3e0c277a0 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -343,7 +343,7 @@ namespace osu.Game.Rulesets.Mania.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrackTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 854626d362..dc7e59b40d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -385,7 +385,7 @@ namespace osu.Game.Rulesets.Osu.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrackTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index b543b6fa94..2dffcfeabb 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -366,7 +366,7 @@ namespace osu.Game.Rulesets.Osu.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrackTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index a69646507a..cd46e8c545 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -22,7 +22,6 @@ 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 { @@ -32,8 +31,6 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private AudioManager audioManager { get; set; } - private TrackVirtualManual track; - protected override bool Autoplay => autoplay; private bool autoplay; @@ -44,11 +41,7 @@ namespace osu.Game.Rulesets.Osu.Tests 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); - track = (TrackVirtualManual)working.Track; - return working; - } + => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); [BackgroundDependencyLoader] private void load(RulesetConfigCache configCache) @@ -72,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("enable autoplay", () => autoplay = true); base.SetUpSteps(); - AddUntilStep("wait for track to start running", () => track.IsRunning); + AddUntilStep("wait for track to start running", () => MusicController.IsPlaying); double startTime = hitObjects[sliderIndex].StartTime; retrieveDrawableSlider(sliderIndex); @@ -97,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("have autoplay", () => autoplay = true); base.SetUpSteps(); - AddUntilStep("wait for track to start running", () => track.IsRunning); + AddUntilStep("wait for track to start running", () => MusicController.IsPlaying); double startTime = hitObjects[sliderIndex].StartTime; retrieveDrawableSlider(sliderIndex); @@ -201,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void addSeekStep(double time) { - AddStep($"seek to {time}", () => track.Seek(time)); + AddStep($"seek to {time}", () => MusicController.SeekTo(time)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index b46964e8b7..105e19a73c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -24,7 +24,6 @@ using osu.Game.Scoring; using osu.Game.Storyboards; using osu.Game.Tests.Visual; using osuTK; -using static osu.Game.Tests.Visual.OsuTestScene.ClockBackedTestWorkingBeatmap; namespace osu.Game.Rulesets.Osu.Tests { @@ -33,18 +32,12 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private AudioManager audioManager { get; set; } - private TrackVirtualManual track; - protected override bool Autoplay => true; protected override TestPlayer CreatePlayer(Ruleset ruleset) => new ScoreExposedPlayer(); 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; - } + => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); private DrawableSpinner drawableSpinner; private SpriteIcon spinnerSymbol => drawableSpinner.ChildrenOfType().Single(); @@ -54,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests { base.SetUpSteps(); - AddUntilStep("wait for track to start running", () => track.IsRunning); + AddUntilStep("wait for track to start running", () => MusicController.IsPlaying); AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First()); } @@ -198,7 +191,7 @@ namespace osu.Game.Rulesets.Osu.Tests private void addSeekStep(double time) { - AddStep($"seek to {time}", () => track.Seek(time)); + AddStep($"seek to {time}", () => MusicController.SeekTo(time)); AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 47d8a5c012..ba5aef5968 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -175,11 +175,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private void createDrawableRuleset() { - AddUntilStep("wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded); + AddUntilStep("wait for beatmap to be loaded", () => MusicController.TrackLoaded); AddStep("create drawable ruleset", () => { - Beatmap.Value.Track.Start(); + MusicController.Play(true); SetContents(() => { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index 7b7e2c43d1..5c54393fb8 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }); - Beatmap.Value.Track.Start(); + MusicController.Play(true); }); AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo()) diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index 4d3b73fb32..1a19326ac4 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -32,6 +31,6 @@ namespace osu.Game.Tests.Skins public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo("sample")) != null); [Test] - public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => !(beatmap.Track is TrackVirtual)); + public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => !MusicController.IsDummyDevice); } } diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index fdb8781563..5d6136d9fb 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -3,12 +3,11 @@ using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -65,10 +64,10 @@ namespace osu.Game.Tests.Visual.Editing private readonly Drawable marker; [Resolved] - private IBindable beatmap { get; set; } + private EditorClock editorClock { get; set; } [Resolved] - private EditorClock editorClock { get; set; } + private MusicController musicController { get; set; } public AudioVisualiser() { @@ -94,8 +93,8 @@ namespace osu.Game.Tests.Visual.Editing { base.Update(); - if (beatmap.Value.Track.IsLoaded) - marker.X = (float)(editorClock.CurrentTime / beatmap.Value.Track.Length); + if (musicController.TrackLoaded) + marker.X = (float)(editorClock.CurrentTime / musicController.TrackLength); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index 79275d70a7..b39cfc3699 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -4,7 +4,6 @@ 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; @@ -18,8 +17,6 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneCompletionCancellation : OsuPlayerTestScene { - private Track track; - [Resolved] private AudioManager audio { get; set; } @@ -34,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay base.SetUpSteps(); // Ensure track has actually running before attempting to seek - AddUntilStep("wait for track to start running", () => track.IsRunning); + AddUntilStep("wait for track to start running", () => MusicController.IsPlaying); } [Test] @@ -73,13 +70,13 @@ namespace osu.Game.Tests.Visual.Gameplay private void complete() { - AddStep("seek to completion", () => track.Seek(5000)); + AddStep("seek to completion", () => MusicController.SeekTo(5000)); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); } private void cancel() { - AddStep("rewind to cancel", () => track.Seek(4000)); + AddStep("rewind to cancel", () => MusicController.SeekTo(4000)); AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value); } @@ -91,11 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay } 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; - } + => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audio); protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 2a119f5199..6bdc65078a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -5,10 +5,10 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Storyboards; @@ -21,19 +21,16 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private AudioManager audioManager { get; set; } - private Track track; + [Resolved] + private MusicController musicController { get; set; } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - { - var working = new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); - track = working.Track; - return working; - } + => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); [Test] public void TestNoJudgementsOnRewind() { - AddUntilStep("wait for track to start running", () => track.IsRunning); + AddUntilStep("wait for track to start running", () => MusicController.IsPlaying); 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)); @@ -46,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void addSeekStep(double time) { - AddStep($"seek to {time}", () => track.Seek(time)); + AddStep($"seek to {time}", () => MusicController.SeekTo(time)); // Allow a few frames of lenience AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs index 951ee1489d..ce99d85e92 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs @@ -19,8 +19,8 @@ namespace osu.Game.Tests.Visual.Gameplay Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - Beatmap.Value.Track.Start(); - Beatmap.Value.Track.Seek(Beatmap.Value.Beatmap.HitObjects.First().StartTime - 1000); + MusicController.Play(true); + MusicController.SeekTo(Beatmap.Value.Beatmap.HitObjects.First().StartTime - 1000); Add(new ModNightcore.NightcoreBeatContainer()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 420bf29429..8f14de1578 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -288,7 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void confirmNoTrackAdjustments() { - AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1); + AddAssert("track has no adjustments", () => MusicController.AggregateFrequency.Value == 1); } private void restart() => AddStep("restart", () => Player.Restart()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 4c73065087..2f86db1b25 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); foreach (var mod in SelectedMods.Value.OfType()) - mod.ApplyToTrack(Beatmap.Value.Track); + mod.ApplyToTrack(MusicController); InputManager.Child = container = new TestPlayerLoaderContainer( loader = new TestPlayerLoader(() => @@ -77,12 +77,12 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); + AddAssert("mod rate applied", () => MusicController.Rate != 1); AddStep("exit loader", () => loader.Exit()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); AddAssert("player did not load", () => !player.IsLoaded); AddUntilStep("player disposed", () => loader.DisposalTask?.IsCompleted == true); - AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1); + AddAssert("mod rate still applied", () => MusicController.Rate != 1); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 9f1492a25f..a4b558fce2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -87,11 +87,9 @@ namespace osu.Game.Tests.Visual.Gameplay private void restart() { - var track = Beatmap.Value.Track; - - track.Reset(); + MusicController.Reset(); loadStoryboard(Beatmap.Value); - track.Start(); + MusicController.Play(true); } private void loadStoryboard(WorkingBeatmap working) @@ -106,7 +104,7 @@ namespace osu.Game.Tests.Visual.Gameplay storyboard.Passing = false; storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(working.Track); + decoupledClock.ChangeSource(musicController.GetTrackClock()); } private void loadStoryboardNoVideo() @@ -129,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay storyboard = sb.CreateDrawable(Beatmap.Value); storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(Beatmap.Value.Track); + decoupledClock.ChangeSource(musicController.GetTrackClock()); } } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 8f20e38494..36f98b7a0c 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Audio.Track; using osu.Framework.Screens; using osu.Game.Screens.Menu; @@ -15,11 +14,9 @@ namespace osu.Game.Tests.Visual.Menus public TestSceneIntroWelcome() { - AddUntilStep("wait for load", () => getTrack() != null); + AddUntilStep("wait for load", () => MusicController.TrackLoaded); - AddAssert("check if menu music loops", () => getTrack().Looping); + AddAssert("check if menu music loops", () => MusicController.Looping); } - - private Track getTrack() => (IntroStack?.CurrentScreen as MainMenu)?.Track; } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8ccaca8630..455b7e56e6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -4,7 +4,6 @@ using System.Linq; 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; @@ -46,7 +45,6 @@ namespace osu.Game.Tests.Visual.Navigation Player player = null; WorkingBeatmap beatmap() => Game.Beatmap.Value; - Track track() => beatmap().Track; PushAndConfirm(() => new TestSongSelect()); @@ -62,30 +60,27 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); AddUntilStep("wait for fail", () => player.HasFailed); - AddUntilStep("wait for track stop", () => !track().IsRunning); - AddAssert("Ensure time before preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime); + AddUntilStep("wait for track stop", () => !MusicController.IsPlaying); + AddAssert("Ensure time before preview point", () => MusicController.CurrentTrackTime < beatmap().Metadata.PreviewTime); pushEscape(); - AddUntilStep("wait for track playing", () => track().IsRunning); - AddAssert("Ensure time wasn't reset to preview point", () => track().CurrentTime < beatmap().Metadata.PreviewTime); + AddUntilStep("wait for track playing", () => MusicController.IsPlaying); + AddAssert("Ensure time wasn't reset to preview point", () => MusicController.CurrentTrackTime < 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); + AddUntilStep("wait for no track", () => MusicController.IsDummyDevice); AddStep("return to menu", () => songSelect.Exit()); - AddUntilStep("wait for track", () => !(track() is TrackVirtual) && track().IsRunning); + AddUntilStep("wait for track", () => !MusicController.IsDummyDevice && MusicController.IsPlaying); } [Test] @@ -140,12 +135,12 @@ namespace osu.Game.Tests.Visual.Navigation 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; + Game.MusicController.SeekTo(MusicController.TrackLength - 1000); + // MusicController.Completed += () => trackCompleted = true; }); AddUntilStep("Track was completed", () => trackCompleted); - AddUntilStep("Track was restarted", () => Game.Beatmap.Value.Track.IsRunning); + AddUntilStep("Track was restarted", () => MusicController.IsPlaying); } private void pushEscape() => diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index dd5ceec739..f3fef8c355 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -71,6 +71,9 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly Box flashLayer; + [Resolved] + private MusicController musicController { get; set; } + public BeatContainer() { RelativeSizeAxes = Axes.X; @@ -165,7 +168,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (timingPoints.Count == 0) return 0; if (timingPoints[^1] == current) - return (int)Math.Ceiling((Beatmap.Value.Track.Length - current.Time) / current.BeatLength); + return (int)Math.Ceiling((musicController.TrackLength - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index 532744a0fc..3ecd8ab550 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -80,12 +80,12 @@ namespace osu.Game.Tests.Visual.UserInterface 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); + AddUntilStep(@"Wait for current time to update", () => musicController.CurrentTrackTime > 5000); AddStep(@"Set previous", () => musicController.PreviousTrack()); AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value); - AddUntilStep("Wait for current time to update", () => currentBeatmap.Track.CurrentTime < 5000); + AddUntilStep("Wait for current time to update", () => musicController.CurrentTrackTime < 5000); AddStep(@"Set previous", () => musicController.PreviousTrack()); AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b4b341634c..b2329f58ad 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -255,7 +255,7 @@ namespace osu.Game.Beatmaps new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)), beatmapInfo, audioManager)); } - previous?.TransferTo(working); + // previous?.TransferTo(working); return working; } } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 39c5ccab27..33945a9eb1 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -79,22 +79,6 @@ namespace osu.Game.Beatmaps } } - public override void RecycleTrack() - { - base.RecycleTrack(); - - trackStore?.Dispose(); - trackStore = null; - } - - public override void TransferTo(WorkingBeatmap other) - { - base.TransferTo(other); - - if (other is BeatmapManagerWorkingBeatmap owb && textureStore != null && BeatmapInfo.BackgroundEquals(other.BeatmapInfo)) - owb.textureStore = textureStore; - } - protected override Waveform GetWaveform() { try diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 31975157a0..086b7502a2 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -26,11 +26,6 @@ namespace osu.Game.Beatmaps /// Texture Background { get; } - /// - /// Retrieves the audio track for this . - /// - Track Track { get; } - /// /// Retrieves the for the of this . /// diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index ac399e37c4..171201ca68 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -40,7 +40,6 @@ namespace osu.Game.Beatmaps BeatmapSetInfo = beatmapInfo.BeatmapSet; Metadata = beatmapInfo.Metadata ?? BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); - track = new RecyclableLazy(() => GetTrack() ?? GetVirtualTrack(1000)); background = new RecyclableLazy(GetBackground, BackgroundStillValid); waveform = new RecyclableLazy(GetWaveform); storyboard = new RecyclableLazy(GetStoryboard); @@ -250,10 +249,9 @@ namespace osu.Game.Beatmaps protected abstract Texture GetBackground(); private readonly RecyclableLazy background; - public virtual bool TrackLoaded => track.IsResultAvailable; - public Track Track => track.Value; + public Track GetRealTrack() => GetTrack() ?? GetVirtualTrack(1000); + protected abstract Track GetTrack(); - private RecyclableLazy track; public bool WaveformLoaded => waveform.IsResultAvailable; public Waveform Waveform => waveform.Value; @@ -271,22 +269,6 @@ namespace osu.Game.Beatmaps protected virtual ISkin GetSkin() => new DefaultSkin(); private readonly RecyclableLazy skin; - /// - /// Transfer pieces of a beatmap to a new one, where possible, to save on loading. - /// - /// The new beatmap which is being switched to. - public virtual void TransferTo(WorkingBeatmap other) - { - if (track.IsResultAvailable && Track != null && BeatmapInfo.AudioEquals(other.BeatmapInfo)) - other.track = track; - } - - /// - /// Eagerly dispose of the audio track associated with this (if any). - /// Accessing track again will load a fresh instance. - /// - public virtual void RecycleTrack() => track.Recycle(); - ~WorkingBeatmap() { total_count.Value--; diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index df063f57d5..2dd28a01dc 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Overlays; namespace osu.Game.Graphics.Containers { @@ -14,6 +15,9 @@ namespace osu.Game.Graphics.Containers { protected readonly IBindable Beatmap = new Bindable(); + [Resolved] + private MusicController musicController { get; set; } + private int lastBeat; private TimingControlPoint lastTimingPoint; @@ -47,22 +51,18 @@ namespace osu.Game.Graphics.Containers protected override void Update() { - Track track = null; IBeatmap beatmap = null; double currentTrackTime = 0; TimingControlPoint timingPoint = null; EffectControlPoint effectPoint = null; - if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) - { - track = Beatmap.Value.Track; + if (musicController.TrackLoaded && Beatmap.Value.BeatmapLoaded) beatmap = Beatmap.Value.Beatmap; - } - if (track != null && beatmap != null && track.IsRunning && track.Length > 0) + if (beatmap != null && musicController.IsPlaying && musicController.TrackLength > 0) { - currentTrackTime = track.CurrentTime + EarlyActivationMilliseconds; + currentTrackTime = musicController.CurrentTrackTime + EarlyActivationMilliseconds; timingPoint = beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); @@ -98,7 +98,7 @@ namespace osu.Game.Graphics.Containers return; using (BeginDelayedSequence(-TimeSinceLastBeat, true)) - OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty); + OnNewBeat(beatIndex, timingPoint, effectPoint, musicController.CurrentAmplitudes); lastBeat = beatIndex; lastTimingPoint = timingPoint; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 26f7c3b93b..929254e8ad 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -431,19 +431,19 @@ namespace osu.Game if (newBeatmap != null) { - newBeatmap.Track.Completed += () => Scheduler.AddOnce(() => trackCompleted(newBeatmap)); + // MusicController.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 firing. - if (Beatmap.Value != b) - return; - - if (!Beatmap.Value.Track.Looping && !Beatmap.Disabled) - MusicController.NextTrack(); - } + // void trackCompleted(WorkingBeatmap b) + // { + // // the source of track completion is the audio thread, so the beatmap may have changed before firing. + // if (Beatmap.Value != b) + // return; + // + // if (!MusicController.Looping && !Beatmap.Disabled) + // MusicController.NextTrack(); + // } } private void modsChanged(ValueChangedEvent> mods) @@ -555,6 +555,7 @@ namespace osu.Game BackButton.Receptor receptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); + dependencies.CacheAs(MusicController = new MusicController()); AddRange(new Drawable[] { @@ -617,7 +618,7 @@ namespace osu.Game loadComponentSingleFile(new OnScreenDisplay(), Add, true); - loadComponentSingleFile(MusicController = new MusicController(), Add, true); + loadComponentSingleFile(MusicController, Add); loadComponentSingleFile(notifications.With(d => { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 98f60d52d3..24c1f7849c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -238,16 +238,6 @@ namespace osu.Game 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 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) - if (b.OldValue?.TrackLoaded == true && b.OldValue?.Track != b.NewValue?.Track) - b.OldValue.RecycleTrack(); - })); - dependencies.CacheAs>(Beatmap); dependencies.CacheAs(Beatmap); diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index b878aba489..c089158c01 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -30,6 +30,9 @@ namespace osu.Game.Overlays.Music [Resolved] private BeatmapManager beatmaps { get; set; } + [Resolved] + private MusicController musicController { get; set; } + private FilterControl filter; private Playlist list; @@ -80,10 +83,7 @@ namespace osu.Game.Overlays.Music BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); if (toSelect != null) - { beatmap.Value = beatmaps.GetWorkingBeatmap(toSelect); - beatmap.Value.Track.Restart(); - } }; } @@ -116,12 +116,12 @@ namespace osu.Game.Overlays.Music { if (set.ID == (beatmap.Value?.BeatmapSetInfo?.ID ?? -1)) { - beatmap.Value?.Track?.Seek(0); + musicController.SeekTo(0); return; } beatmap.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First()); - beatmap.Value.Track.Restart(); + musicController.Play(true); } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index a990f9a6ab..f5ca5a3a49 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -4,13 +4,18 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Framework.Threading; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Overlays.OSD; @@ -21,7 +26,7 @@ namespace osu.Game.Overlays /// /// Handles playback of the global music track. /// - public class MusicController : Component, IKeyBindingHandler + public class MusicController : CompositeDrawable, IKeyBindingHandler, ITrack { [Resolved] private BeatmapManager beatmaps { get; set; } @@ -61,9 +66,23 @@ namespace osu.Game.Overlays [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } + [NotNull] + private readonly TrackContainer trackContainer; + + [CanBeNull] + private DrawableTrack drawableTrack; + + [CanBeNull] + private Track track; + private IBindable> managerUpdated; private IBindable> managerRemoved; + public MusicController() + { + InternalChild = trackContainer = new TrackContainer { RelativeSizeAxes = Axes.Both }; + } + [BackgroundDependencyLoader] private void load() { @@ -95,9 +114,35 @@ namespace osu.Game.Overlays } /// - /// Returns whether the current beatmap track is playing. + /// Returns whether the beatmap track is playing. /// - public bool IsPlaying => current?.Track.IsRunning ?? false; + public bool IsPlaying => drawableTrack?.IsRunning ?? false; + + /// + /// Returns whether the beatmap track is loaded. + /// + public bool TrackLoaded => drawableTrack?.IsLoaded == true; + + /// + /// Returns the current time of the beatmap track. + /// + public double CurrentTrackTime => drawableTrack?.CurrentTime ?? 0; + + /// + /// Returns the length of the beatmap track. + /// + public double TrackLength => drawableTrack?.Length ?? 0; + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => trackContainer.AddAdjustment(type, adjustBindable); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => trackContainer.RemoveAdjustment(type, adjustBindable); + + public void Reset() => drawableTrack?.Reset(); + + [CanBeNull] + public IAdjustableClock GetTrackClock() => track; private void beatmapUpdated(ValueChangedEvent> weakSet) { @@ -130,7 +175,7 @@ namespace osu.Game.Overlays seekDelegate = Schedule(() => { if (!beatmap.Disabled) - current?.Track.Seek(position); + drawableTrack?.Seek(position); }); } @@ -142,9 +187,7 @@ namespace osu.Game.Overlays { if (IsUserPaused) return; - var track = current?.Track; - - if (track == null || track is TrackVirtual) + if (drawableTrack == null || drawableTrack.IsDummyDevice) { if (beatmap.Disabled) return; @@ -163,17 +206,15 @@ namespace osu.Game.Overlays /// Whether the operation was successful. public bool Play(bool restart = false) { - var track = current?.Track; - IsUserPaused = false; - if (track == null) + if (drawableTrack == null) return false; if (restart) - track.Restart(); + drawableTrack.Restart(); else if (!IsPlaying) - track.Start(); + drawableTrack.Start(); return true; } @@ -183,11 +224,9 @@ namespace osu.Game.Overlays /// public void Stop() { - var track = current?.Track; - IsUserPaused = true; - if (track?.IsRunning == true) - track.Stop(); + if (drawableTrack?.IsRunning == true) + drawableTrack.Stop(); } /// @@ -196,9 +235,7 @@ namespace osu.Game.Overlays /// Whether the operation was successful. public bool TogglePause() { - var track = current?.Track; - - if (track?.IsRunning == true) + if (drawableTrack?.IsRunning == true) Stop(); else Play(); @@ -220,7 +257,7 @@ namespace osu.Game.Overlays if (beatmap.Disabled) return PreviousTrackResult.None; - var currentTrackPosition = current?.Track.CurrentTime; + var currentTrackPosition = drawableTrack?.CurrentTime; if (currentTrackPosition >= restart_cutoff_point) { @@ -274,7 +311,7 @@ namespace osu.Game.Overlays { // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase). // we probably want to move this to a central method for switching to a new working beatmap in the future. - Schedule(() => beatmap.Value.Track.Restart()); + Schedule(() => drawableTrack?.Restart()); } private WorkingBeatmap current; @@ -307,6 +344,14 @@ namespace osu.Game.Overlays } current = beatmap.NewValue; + + drawableTrack?.Expire(); + drawableTrack = null; + track = null; + + if (current != null) + trackContainer.Add(drawableTrack = new DrawableTrack(track = current.GetRealTrack())); + TrackChanged?.Invoke(current, direction); ResetTrackAdjustments(); @@ -334,16 +379,15 @@ namespace osu.Game.Overlays public void ResetTrackAdjustments() { - var track = current?.Track; - if (track == null) + if (drawableTrack == null) return; - track.ResetSpeedAdjustments(); + drawableTrack.ResetSpeedAdjustments(); if (allowRateAdjustments) { foreach (var mod in mods.Value.OfType()) - mod.ApplyToTrack(track); + mod.ApplyToTrack(drawableTrack); } } @@ -394,6 +438,133 @@ namespace osu.Game.Overlays { } } + + private class TrackContainer : AudioContainer + { + } + + #region ITrack + + /// + /// The volume of this component. + /// + public BindableNumber Volume => drawableTrack?.Volume; // Todo: Bad + + /// + /// The playback balance of this sample (-1 .. 1 where 0 is centered) + /// + public BindableNumber Balance => drawableTrack?.Balance; // Todo: Bad + + /// + /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. + /// + public BindableNumber Frequency => drawableTrack?.Frequency; // Todo: Bad + + /// + /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. + /// + public BindableNumber Tempo => drawableTrack?.Tempo; // Todo: Bad + + public IBindable AggregateVolume => drawableTrack?.AggregateVolume; // Todo: Bad + + public IBindable AggregateBalance => drawableTrack?.AggregateBalance; // Todo: Bad + + public IBindable AggregateFrequency => drawableTrack?.AggregateFrequency; // Todo: Bad + + public IBindable AggregateTempo => drawableTrack?.AggregateTempo; // Todo: Bad + + /// + /// Overall playback rate (1 is 100%, -1 is reversed at 100%). + /// + public double Rate => AggregateFrequency.Value * AggregateTempo.Value; + + event Action ITrack.Completed + { + add + { + if (drawableTrack != null) + drawableTrack.Completed += value; + } + remove + { + if (drawableTrack != null) + drawableTrack.Completed -= value; + } + } + + event Action ITrack.Failed + { + add + { + if (drawableTrack != null) + drawableTrack.Failed += value; + } + remove + { + if (drawableTrack != null) + drawableTrack.Failed -= value; + } + } + + public bool Looping + { + get => drawableTrack?.Looping ?? false; + set + { + if (drawableTrack != null) + drawableTrack.Looping = value; + } + } + + public bool IsDummyDevice => drawableTrack?.IsDummyDevice ?? true; + + public double RestartPoint + { + get => drawableTrack?.RestartPoint ?? 0; + set + { + if (drawableTrack != null) + drawableTrack.RestartPoint = value; + } + } + + double ITrack.CurrentTime => CurrentTrackTime; + + double ITrack.Length + { + get => TrackLength; + set + { + if (drawableTrack != null) + drawableTrack.Length = value; + } + } + + public int? Bitrate => drawableTrack?.Bitrate; + + bool ITrack.IsRunning => IsPlaying; + + public bool IsReversed => drawableTrack?.IsReversed ?? false; + + public bool HasCompleted => drawableTrack?.HasCompleted ?? false; + + void ITrack.Reset() => drawableTrack?.Reset(); + + void ITrack.Restart() => Play(true); + + void ITrack.ResetSpeedAdjustments() => ResetTrackAdjustments(); + + bool ITrack.Seek(double seek) + { + SeekTo(seek); + return true; + } + + void ITrack.Start() => Play(); + + public ChannelAmplitudes CurrentAmplitudes => drawableTrack?.CurrentAmplitudes ?? ChannelAmplitudes.Empty; + + #endregion } public enum TrackChangeDirection diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index ebb4a96d14..fde6a52fee 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -234,14 +234,12 @@ namespace osu.Game.Overlays pendingBeatmapSwitch = null; } - var track = beatmap.Value?.TrackLoaded ?? false ? beatmap.Value.Track : null; - - if (track?.IsDummyDevice == false) + if (musicController.IsDummyDevice == false) { - progressBar.EndTime = track.Length; - progressBar.CurrentTime = track.CurrentTime; + progressBar.EndTime = musicController.TrackLength; + progressBar.CurrentTime = musicController.CurrentTrackTime; - playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; + playButton.Icon = musicController.IsPlaying ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; } else { diff --git a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs index 4d6d958e82..9b840cea08 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods /// public interface IApplicableToTrack : IApplicableMod { - void ApplyToTrack(Track track); + void ApplyToTrack(ITrack track); } } diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index bd98e735e5..9cefeb3340 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -27,11 +27,11 @@ namespace osu.Game.Rulesets.Mods }, true); } - public override void ApplyToTrack(Track track) + public override void ApplyToTrack(ITrack track) { // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) - track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); + (track as Track)?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); + (track as Track)?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } } } diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index ed8eb2fb66..b34affa77f 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -38,11 +38,11 @@ namespace osu.Game.Rulesets.Mods }, true); } - public override void ApplyToTrack(Track track) + public override void ApplyToTrack(ITrack track) { // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) - track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); + (track as Track)?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); + (track as Track)?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 874384686f..ee1280da39 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -12,9 +12,9 @@ namespace osu.Game.Rulesets.Mods { public abstract BindableNumber SpeedChange { get; } - public virtual void ApplyToTrack(Track track) + public virtual void ApplyToTrack(ITrack track) { - track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); + (track as Track)?.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } public virtual void ApplyToSample(SampleChannel sample) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 839d97f04e..0257e241b8 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -51,9 +51,9 @@ namespace osu.Game.Rulesets.Mods AdjustPitch.BindValueChanged(applyPitchAdjustment); } - public void ApplyToTrack(Track track) + public void ApplyToTrack(ITrack track) { - this.track = track; + this.track = track as Track; FinalRate.TriggerChange(); AdjustPitch.TriggerChange(); diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs index cb5078a479..8d8c59b2ee 100644 --- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs +++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs @@ -2,7 +2,6 @@ // 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; using osu.Framework.Graphics.Containers; @@ -18,7 +17,6 @@ namespace osu.Game.Screens.Edit.Components private const float contents_padding = 15; protected readonly IBindable Beatmap = new Bindable(); - protected Track Track => Beatmap.Value.Track; private readonly Drawable background; private readonly Container content; diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 59b3d1c565..0a9b4f06a7 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; using osuTK.Input; namespace osu.Game.Screens.Edit.Components @@ -27,6 +28,9 @@ namespace osu.Game.Screens.Edit.Components [Resolved] private EditorClock editorClock { get; set; } + [Resolved] + private MusicController musicController { get; set; } + private readonly BindableNumber tempo = new BindableDouble(1); [BackgroundDependencyLoader] @@ -62,12 +66,12 @@ namespace osu.Game.Screens.Edit.Components } }; - Track?.AddAdjustment(AdjustableProperty.Tempo, tempo); + musicController.AddAdjustment(AdjustableProperty.Tempo, tempo); } protected override void Dispose(bool isDisposing) { - Track?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); + musicController?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); base.Dispose(isDisposing); } 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 9e9ac93d23..a353f79ef4 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osuTK; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -58,7 +59,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts return; float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); - editorClock.SeekTo(markerPos / DrawWidth * editorClock.TrackLength); + + Debug.Assert(editorClock.TrackLength != null); + editorClock.SeekTo(markerPos / DrawWidth * editorClock.TrackLength.Value); }); } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 4a7c3f26bc..446f7fdf88 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -8,6 +8,7 @@ using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; +using osu.Game.Overlays; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { @@ -26,6 +27,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts protected override Container Content => content; + [Resolved] + private MusicController musicController { get; set; } + public TimelinePart(Container content = null) { AddInternal(this.content = content ?? new Container { RelativeSizeAxes = Axes.Both }); @@ -46,14 +50,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts private void updateRelativeChildSize() { // the track may not be loaded completely (only has a length once it is). - if (!Beatmap.Value.Track.IsLoaded) + if (!musicController.TrackLoaded) { content.RelativeChildSize = Vector2.One; Schedule(updateRelativeChildSize); return; } - content.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); + content.RelativeChildSize = new Vector2((float)Math.Max(1, musicController.TrackLength), 1); } protected virtual void LoadBeatmap(WorkingBeatmap beatmap) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 717d60b4f3..f35e5defd8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -11,6 +10,7 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osuTK; @@ -26,6 +26,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private EditorClock editorClock { get; set; } + [Resolved] + private MusicController musicController { get; set; } + public Timeline() { ZoomDuration = 200; @@ -57,18 +60,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Beatmap.BindValueChanged(b => { waveform.Waveform = b.NewValue.Waveform; - track = b.NewValue.Track; - if (track.Length > 0) + // Todo: Wrong. + Schedule(() => { - MaxZoom = getZoomLevelForVisibleMilliseconds(500); - MinZoom = getZoomLevelForVisibleMilliseconds(10000); - Zoom = getZoomLevelForVisibleMilliseconds(2000); - } + if (musicController.TrackLength > 0) + { + MaxZoom = getZoomLevelForVisibleMilliseconds(500); + MinZoom = getZoomLevelForVisibleMilliseconds(10000); + Zoom = getZoomLevelForVisibleMilliseconds(2000); + } + }); }, true); } - private float getZoomLevelForVisibleMilliseconds(double milliseconds) => (float)(track.Length / milliseconds); + private float getZoomLevelForVisibleMilliseconds(double milliseconds) => (float)(musicController.TrackLength / milliseconds); /// /// The timeline's scroll position in the last frame. @@ -90,8 +96,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// private bool trackWasPlaying; - private Track track; - protected override void Update() { base.Update(); @@ -129,18 +133,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void seekTrackToCurrent() { - if (!track.IsLoaded) + if (!musicController.TrackLoaded) return; - editorClock.Seek(Current / Content.DrawWidth * track.Length); + editorClock.Seek(Current / Content.DrawWidth * musicController.TrackLength); } private void scrollToTrackTime() { - if (!track.IsLoaded || track.Length == 0) + if (!musicController.TrackLoaded || musicController.TrackLength == 0) return; - ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false); + ScrollTo((float)(editorClock.CurrentTime / musicController.TrackLength) * Content.DrawWidth, false); } protected override bool OnMouseDown(MouseDownEvent e) @@ -184,7 +188,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); private double getTimeFromPosition(Vector2 localPosition) => - (localPosition.X / Content.DrawWidth) * track.Length; + (localPosition.X / Content.DrawWidth) * musicController.TrackLength; public float GetBeatSnapDistanceAt(double referenceTime) => throw new NotImplementedException(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 36ee976bf7..a833b354ed 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -3,10 +3,9 @@ using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Overlays; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; @@ -18,7 +17,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private EditorBeatmap beatmap { get; set; } [Resolved] - private Bindable working { get; set; } + private MusicController musicController { get; set; } [Resolved] private BindableBeatDivisor beatDivisor { get; set; } @@ -44,7 +43,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline for (var 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; + var until = i + 1 < beatmap.ControlPointInfo.TimingPoints.Count ? beatmap.ControlPointInfo.TimingPoints[i + 1].Time : musicController.TrackLength; int beat = 0; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d92f3922c3..756b03d049 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -28,6 +28,7 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; +using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Setup; @@ -53,6 +54,9 @@ namespace osu.Game.Screens.Edit [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved] + private MusicController musicController { get; set; } + private Box bottomBackground; private Container screenContainer; @@ -79,7 +83,7 @@ namespace osu.Game.Screens.Edit beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); + var sourceClock = musicController.GetTrackClock() ?? new StopwatchClock(); clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; clock.ChangeSource(sourceClock); @@ -346,7 +350,7 @@ namespace osu.Game.Screens.Edit private void resetTrack(bool seekToStart = false) { - Beatmap.Value.Track?.Stop(); + musicController.Stop(); if (seekToStart) { diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index d4d0feb813..4cb24f90a6 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -2,13 +2,16 @@ // 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.Graphics; using osu.Framework.Graphics.Transforms; using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Overlays; namespace osu.Game.Screens.Edit { @@ -17,7 +20,7 @@ namespace osu.Game.Screens.Edit /// public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { - public readonly double TrackLength; + public double? TrackLength { get; private set; } public ControlPointInfo ControlPointInfo; @@ -25,12 +28,15 @@ namespace osu.Game.Screens.Edit private readonly DecoupleableInterpolatingFramedClock underlyingClock; + [Resolved] + private MusicController musicController { get; set; } + public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) - : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor) + : this(beatmap.Beatmap.ControlPointInfo, null, beatDivisor) { } - public EditorClock(ControlPointInfo controlPointInfo, double trackLength, BindableBeatDivisor beatDivisor) + public EditorClock(ControlPointInfo controlPointInfo, double? trackLength, BindableBeatDivisor beatDivisor) { this.beatDivisor = beatDivisor; @@ -45,6 +51,13 @@ namespace osu.Game.Screens.Edit { } + [BackgroundDependencyLoader] + private void load() + { + // Todo: What. + TrackLength ??= musicController.TrackLength; + } + /// /// Seek to the closest snappable beat from a time. /// @@ -135,7 +148,8 @@ namespace osu.Game.Screens.Edit seekTime = timingPoint.Time; // Ensure the sought point is within the boundaries - seekTime = Math.Clamp(seekTime, 0, TrackLength); + Debug.Assert(TrackLength != null); + seekTime = Math.Clamp(seekTime, 0, TrackLength.Value); SeekTo(seekTime); } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 5f91aaad15..7da5df2723 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.IO.Archives; +using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Skinning; using osuTK; @@ -43,7 +44,8 @@ namespace osu.Game.Screens.Menu private WorkingBeatmap initialBeatmap; - protected Track Track => initialBeatmap?.Track; + [Resolved] + protected MusicController MusicController { get; private set; } private readonly BindableDouble exitingVolumeFade = new BindableDouble(1); @@ -111,7 +113,7 @@ namespace osu.Game.Screens.Menu if (setInfo != null) { initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - UsingThemedIntro = !(Track is TrackVirtual); + UsingThemedIntro = !MusicController.IsDummyDevice; } return UsingThemedIntro; @@ -150,7 +152,7 @@ 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(); + MusicController.Play(true); } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index a9ef20436f..86a6fa3802 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(new TrianglesIntroSequence(logo, background) { RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(UsingThemedIntro ? Track : null), + Clock = new FramedClock(UsingThemedIntro ? MusicController.GetTrackClock() : null), LoadMenu = LoadMenu }, t => { diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index bf42e36e8c..62cada577d 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.Overlays; using osu.Game.Screens.Backgrounds; using osuTK.Graphics; @@ -30,6 +31,9 @@ namespace osu.Game.Screens.Menu Alpha = 0, }; + [Resolved] + private MusicController musicController { get; set; } + private BackgroundScreenDefault background; [BackgroundDependencyLoader] @@ -40,7 +44,7 @@ namespace osu.Game.Screens.Menu pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano"); - Track.Looping = true; + musicController.Looping = true; } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index ebbb19636c..349654165f 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -20,6 +20,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Utils; +using osu.Game.Overlays; namespace osu.Game.Screens.Menu { @@ -74,6 +75,9 @@ namespace osu.Game.Screens.Menu /// public float Magnitude { get; set; } = 1; + [Resolved] + private MusicController musicController { get; set; } + private readonly float[] frequencyAmplitudes = new float[256]; private IShader shader; @@ -103,15 +107,15 @@ namespace osu.Game.Screens.Menu private void updateAmplitudes() { - var effect = beatmap.Value.BeatmapLoaded && beatmap.Value.TrackLoaded - ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(beatmap.Value.Track.CurrentTime) + var effect = beatmap.Value.BeatmapLoaded && musicController.TrackLoaded + ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(musicController.CurrentTrackTime) : null; for (int i = 0; i < temporalAmplitudes.Length; i++) temporalAmplitudes[i] = 0; - if (beatmap.Value.TrackLoaded) - addAmplitudesFromSource(beatmap.Value.Track); + if (musicController.TrackLoaded) + addAmplitudesFromSource(musicController); foreach (var source in amplitudeSources) addAmplitudesFromSource(source); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 57252d557e..c422f57332 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -5,7 +5,6 @@ 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.Platform; @@ -62,8 +61,6 @@ namespace osu.Game.Screens.Menu protected override BackgroundScreen CreateBackground() => background; - internal Track Track { get; private set; } - private Bindable holdDelay; private Bindable loginDisplayed; @@ -172,20 +169,23 @@ namespace osu.Game.Screens.Menu [Resolved] private Storage storage { get; set; } + [Resolved] + private MusicController musicController { get; set; } + public override void OnEntering(IScreen last) { base.OnEntering(last); buttons.FadeInFromZero(500); - Track = Beatmap.Value.Track; var metadata = Beatmap.Value.Metadata; - if (last is IntroScreen && Track != null) + if (last is IntroScreen && musicController.TrackLoaded) { - if (!Track.IsRunning) + // Todo: Wrong. + if (!musicController.IsPlaying) { - Track.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * Track.Length); - Track.Start(); + musicController.SeekTo(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * musicController.TrackLength); + musicController.Play(); } } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index f5e4b078da..1feb2481c3 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -17,6 +17,7 @@ using osu.Framework.Utils; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -46,7 +47,6 @@ namespace osu.Game.Screens.Menu private SampleChannel sampleBeat; private readonly Container colourAndTriangles; - private readonly Triangles triangles; /// @@ -319,6 +319,9 @@ namespace osu.Game.Screens.Menu intro.Delay(length + fade).FadeOut(); } + [Resolved] + private MusicController musicController { get; set; } + protected override void Update() { base.Update(); @@ -327,9 +330,9 @@ namespace osu.Game.Screens.Menu const float velocity_adjust_cutoff = 0.98f; const float paused_velocity = 0.5f; - if (Beatmap.Value.Track.IsRunning) + if (musicController.IsPlaying) { - var maxAmplitude = lastBeatIndex >= 0 ? Beatmap.Value.Track.CurrentAmplitudes.Maximum : 0; + var maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentAmplitudes.Maximum : 0; logoAmplitudeContainer.Scale = new Vector2((float)Interpolation.Damp(logoAmplitudeContainer.Scale.X, 1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 0.9f, Time.Elapsed)); if (maxAmplitude > velocity_adjust_cutoff) diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs index a64f24dd7e..032c8d5ca9 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; +using osu.Game.Overlays; namespace osu.Game.Screens.Multi.Match.Components { @@ -26,6 +27,9 @@ namespace osu.Game.Screens.Multi.Match.Components [Resolved] private BeatmapManager beatmaps { get; set; } + [Resolved] + private MusicController musicController { get; set; } + private bool hasBeatmap; public ReadyButton() @@ -100,7 +104,7 @@ namespace osu.Game.Screens.Multi.Match.Components return; } - bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value; + bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(musicController.TrackLength) < endDate.Value; Enabled.Value = hasBeatmap && hasEnoughTime; } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 4912df17b1..2d74434c76 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Multi private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); [Resolved] - private MusicController music { get; set; } + private MusicController musicController { get; set; } [Cached(Type = typeof(IRoomManager))] private RoomManager roomManager; @@ -343,15 +343,9 @@ namespace osu.Game.Screens.Multi { if (screenStack.CurrentScreen is MatchSubScreen) { - var track = Beatmap.Value?.Track; - - if (track != null) - { - track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - track.Looping = true; - - music.EnsurePlayingSomething(); - } + musicController.RestartPoint = Beatmap.Value.Metadata.PreviewTime; + musicController.Looping = true; + musicController.EnsurePlayingSomething(); } else { @@ -361,13 +355,8 @@ namespace osu.Game.Screens.Multi private void cancelLooping() { - var track = Beatmap?.Value?.Track; - - if (track != null) - { - track.Looping = false; - track.RestartPoint = 0; - } + musicController.Looping = false; + musicController.RestartPoint = 0; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 54c644c999..0e0ef8c675 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -8,10 +8,10 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; -using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Overlays; using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; @@ -30,12 +30,13 @@ namespace osu.Game.Screens.Play private readonly BindableDouble trackFreq = new BindableDouble(1); - private Track track; - private const float duration = 2500; private SampleChannel failSample; + [Resolved] + private MusicController musicController { get; set; } + public FailAnimation(DrawableRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; @@ -44,7 +45,6 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(AudioManager audio, IBindable beatmap) { - track = beatmap.Value.Track; failSample = audio.Samples.Get(@"Gameplay/failsound"); } @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play Expire(); }); - track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); + musicController.AddAdjustment(AdjustableProperty.Frequency, trackFreq); applyToPlayfield(drawableRuleset.Playfield); drawableRuleset.Playfield.HitObjectContainer.FlashColour(Color4.Red, 500); @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); + musicController?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); } } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 0653373c91..cf4678ab29 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -8,13 +8,13 @@ using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Overlays; using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Play @@ -27,10 +27,8 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; private readonly IReadOnlyList mods; - /// - /// The 's track. - /// - private Track track; + [Resolved] + private MusicController musicController { get; set; } public readonly BindableBool IsPaused = new BindableBool(); @@ -72,8 +70,6 @@ namespace osu.Game.Screens.Play RelativeSizeAxes = Axes.Both; - track = beatmap.Track; - adjustableClock = new DecoupleableInterpolatingFramedClock { IsCoupled = false }; // Lazer's audio timings in general doesn't match stable. This is the result of user testing, albeit limited. @@ -125,15 +121,15 @@ namespace osu.Game.Screens.Play { // 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(); + musicController.Reset(); Task.Run(() => { - track.Reset(); + musicController.Reset(); Schedule(() => { - adjustableClock.ChangeSource(track); + adjustableClock.ChangeSource(musicController.GetTrackClock()); updateRate(); if (!IsPaused.Value) @@ -194,20 +190,6 @@ namespace osu.Game.Screens.Play IsPaused.Value = true; } - /// - /// Changes the backing clock to avoid using the originally provided beatmap's track. - /// - public void StopUsingBeatmapClock() - { - if (track != beatmap.Track) - return; - - removeSourceClockAdjustments(); - - track = new TrackVirtual(beatmap.Track.Length); - adjustableClock.ChangeSource(track); - } - protected override void Update() { if (!IsPaused.Value) @@ -220,31 +202,23 @@ namespace osu.Game.Screens.Play private void updateRate() { - if (track == null) return; - speedAdjustmentsApplied = true; - track.ResetSpeedAdjustments(); - - track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - - foreach (var mod in mods.OfType()) - mod.ApplyToTrack(track); + musicController.ResetTrackAdjustments(); + musicController.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + musicController.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - removeSourceClockAdjustments(); - track = null; } private void removeSourceClockAdjustments() { if (speedAdjustmentsApplied) { - track.ResetSpeedAdjustments(); + musicController.ResetTrackAdjustments(); speedAdjustmentsApplied = false; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 74a5ee8309..9ba2732920 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -561,8 +560,11 @@ namespace osu.Game.Screens.Select BeatmapDetails.Refresh(); - Beatmap.Value.Track.Looping = true; - music?.ResetTrackAdjustments(); + if (music != null) + { + music.Looping = true; + music.ResetTrackAdjustments(); + } if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) { @@ -586,8 +588,8 @@ namespace osu.Game.Screens.Select BeatmapOptions.Hide(); - if (Beatmap.Value.Track != null) - Beatmap.Value.Track.Looping = false; + if (music != null) + music.Looping = false; this.ScaleTo(1.1f, 250, Easing.InSine); @@ -608,8 +610,8 @@ namespace osu.Game.Screens.Select FilterControl.Deactivate(); - if (Beatmap.Value.Track != null) - Beatmap.Value.Track.Looping = false; + if (music != null) + music.Looping = false; return false; } @@ -650,28 +652,18 @@ namespace osu.Game.Screens.Select BeatmapDetails.Beatmap = beatmap; - if (beatmap.Track != null) - beatmap.Track.Looping = true; + if (music != null) + music.Looping = false; } - private readonly WeakReference lastTrack = new WeakReference(null); - /// /// Ensures some music is playing for the current track. /// Will resume playback from a manual user pause if the track has changed. /// private void ensurePlayingSelected() { - Track track = Beatmap.Value.Track; - - bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track; - - track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - - if (!track.IsRunning && (music?.IsUserPaused != true || isNewTrack)) - music?.Play(true); - - lastTrack.SetTarget(track); + music.RestartPoint = Beatmap.Value.Metadata.PreviewTime; + music.EnsurePlayingSomething(); } private void carouselBeatmapsLoaded() diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index cdf9170701..ee04142035 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -27,8 +27,6 @@ namespace osu.Game.Tests.Beatmaps this.storyboard = storyboard; } - public override bool TrackLoaded => true; - public override bool BeatmapLoaded => true; protected override IBeatmap GetBeatmap() => beatmap; diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index f0ec638fc9..5226a49def 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual private void beatmapChanged(ValueChangedEvent e) { Clock.ControlPointInfo = e.NewValue.Beatmap.ControlPointInfo; - Clock.ChangeSource((IAdjustableClock)e.NewValue.Track ?? new StopwatchClock()); + Clock.ChangeSource(MusicController.GetTrackClock() ?? new StopwatchClock()); Clock.ProcessFrame(); } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 866fc215d6..e968f7e675 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -20,6 +20,7 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; +using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -135,6 +136,9 @@ namespace osu.Game.Tests.Visual [Resolved] protected AudioManager Audio { get; private set; } + [Resolved] + protected MusicController MusicController { get; private set; } + /// /// Creates the ruleset to be used for this test scene. /// @@ -164,8 +168,8 @@ namespace osu.Game.Tests.Visual rulesetDependencies?.Dispose(); - if (Beatmap?.Value.TrackLoaded == true) - Beatmap.Value.Track.Stop(); + if (MusicController?.TrackLoaded == true) + MusicController.Stop(); if (contextFactory.IsValueCreated) contextFactory.Value.ResetDatabase(); diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs index ad24ffc7b8..e7cb461d7b 100644 --- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs +++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs @@ -13,7 +13,7 @@ namespace osu.Game.Tests.Visual base.Update(); // note that this will override any mod rate application - Beatmap.Value.Track.Tempo.Value = Clock.Rate; + MusicController.Tempo.Value = Clock.Rate; } } } From 5c05fe3988ad5ba9c92e908628f2d11def1c9376 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 5 Aug 2020 21:10:38 +0900 Subject: [PATCH 2552/6909] Expose track from MusicController --- .../TestSceneHoldNoteInput.cs | 2 +- .../TestSceneOutOfOrderHits.cs | 2 +- .../TestSceneSliderInput.cs | 2 +- .../Skins/TestSceneBeatmapSkinResources.cs | 2 +- .../Visual/Editing/TimelineTestScene.cs | 6 +- .../Visual/Gameplay/TestScenePause.cs | 2 +- .../Visual/Gameplay/TestScenePlayerLoader.cs | 6 +- .../Visual/Gameplay/TestSceneStoryboard.cs | 8 +- .../Visual/Menus/TestSceneIntroWelcome.cs | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 13 +- .../TestSceneBeatSyncedContainer.cs | 3 +- .../TestSceneNowPlayingOverlay.cs | 4 +- .../Containers/BeatSyncedContainer.cs | 14 +- osu.Game/Overlays/MusicController.cs | 190 ++---------------- osu.Game/Overlays/NowPlayingOverlay.cs | 12 +- osu.Game/Rulesets/Mods/IApplicableToTrack.cs | 3 +- osu.Game/Rulesets/Mods/ModDaycore.cs | 7 +- osu.Game/Rulesets/Mods/ModNightcore.cs | 6 +- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 4 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 10 +- .../Edit/Components/PlaybackControl.cs | 4 +- .../Timelines/Summary/Parts/TimelinePart.cs | 4 +- .../Compose/Components/Timeline/Timeline.cs | 31 +-- .../Timeline/TimelineTickDisplay.cs | 3 +- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Edit/EditorClock.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 4 +- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- osu.Game/Screens/Menu/IntroWelcome.cs | 3 +- osu.Game/Screens/Menu/LogoVisualisation.cs | 5 +- osu.Game/Screens/Menu/MainMenu.cs | 10 +- osu.Game/Screens/Menu/OsuLogo.cs | 4 +- .../Multi/Match/Components/ReadyButton.cs | 2 +- osu.Game/Screens/Multi/Multiplayer.cs | 17 +- osu.Game/Screens/Play/FailAnimation.cs | 13 +- .../Screens/Play/GameplayClockContainer.cs | 44 +++- osu.Game/Screens/Select/SongSelect.cs | 33 ++- osu.Game/Tests/Visual/EditorClockTestScene.cs | 2 +- .../Visual/RateAdjustedBeatmapTestScene.cs | 4 +- 39 files changed, 204 insertions(+), 283 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index c3e0c277a0..98669efb10 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -343,7 +343,7 @@ namespace osu.Game.Rulesets.Mania.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrackTime == 0); + AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack?.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index dc7e59b40d..e5be778527 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -385,7 +385,7 @@ namespace osu.Game.Rulesets.Osu.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrackTime == 0); + AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack?.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 2dffcfeabb..c9d13d3976 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -366,7 +366,7 @@ namespace osu.Game.Rulesets.Osu.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrackTime == 0); + AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack?.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index 1a19326ac4..08a4e27ff7 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -31,6 +31,6 @@ namespace osu.Game.Tests.Skins public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo("sample")) != null); [Test] - public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => !MusicController.IsDummyDevice); + public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => MusicController.CurrentTrack?.IsDummyDevice == false); } } diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 5d6136d9fb..4113bdddf8 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.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 osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; @@ -94,7 +95,10 @@ namespace osu.Game.Tests.Visual.Editing base.Update(); if (musicController.TrackLoaded) - marker.X = (float)(editorClock.CurrentTime / musicController.TrackLength); + { + Debug.Assert(musicController.CurrentTrack != null); + marker.X = (float)(editorClock.CurrentTime / musicController.CurrentTrack.Length); + } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 8f14de1578..e500b451f0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -288,7 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void confirmNoTrackAdjustments() { - AddAssert("track has no adjustments", () => MusicController.AggregateFrequency.Value == 1); + AddAssert("track has no adjustments", () => MusicController.CurrentTrack?.AggregateFrequency.Value == 1); } private void restart() => AddStep("restart", () => Player.Restart()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 2f86db1b25..c72ab7d3d1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); foreach (var mod in SelectedMods.Value.OfType()) - mod.ApplyToTrack(MusicController); + mod.ApplyToTrack(MusicController.CurrentTrack); InputManager.Child = container = new TestPlayerLoaderContainer( loader = new TestPlayerLoader(() => @@ -77,12 +77,12 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddAssert("mod rate applied", () => MusicController.Rate != 1); + AddAssert("mod rate applied", () => MusicController.CurrentTrack?.Rate != 1); AddStep("exit loader", () => loader.Exit()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); AddAssert("player did not load", () => !player.IsLoaded); AddUntilStep("player disposed", () => loader.DisposalTask?.IsCompleted == true); - AddAssert("mod rate still applied", () => MusicController.Rate != 1); + AddAssert("mod rate still applied", () => MusicController.CurrentTrack?.Rate != 1); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index a4b558fce2..c7a012a03f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -87,9 +87,9 @@ namespace osu.Game.Tests.Visual.Gameplay private void restart() { - MusicController.Reset(); + MusicController.CurrentTrack?.Reset(); loadStoryboard(Beatmap.Value); - MusicController.Play(true); + MusicController.CurrentTrack?.Start(); } private void loadStoryboard(WorkingBeatmap working) @@ -104,7 +104,7 @@ namespace osu.Game.Tests.Visual.Gameplay storyboard.Passing = false; storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(musicController.GetTrackClock()); + decoupledClock.ChangeSource(musicController.CurrentTrack); } private void loadStoryboardNoVideo() @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay storyboard = sb.CreateDrawable(Beatmap.Value); storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(musicController.GetTrackClock()); + decoupledClock.ChangeSource(musicController.CurrentTrack); } } } diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 36f98b7a0c..a88704c831 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Menus { AddUntilStep("wait for load", () => MusicController.TrackLoaded); - AddAssert("check if menu music loops", () => MusicController.Looping); + AddAssert("check if menu music loops", () => MusicController.CurrentTrack?.Looping == true); } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 455b7e56e6..946bc2a175 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -4,6 +4,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -61,12 +62,12 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for fail", () => player.HasFailed); AddUntilStep("wait for track stop", () => !MusicController.IsPlaying); - AddAssert("Ensure time before preview point", () => MusicController.CurrentTrackTime < beatmap().Metadata.PreviewTime); + AddAssert("Ensure time before preview point", () => MusicController.CurrentTrack?.CurrentTime < beatmap().Metadata.PreviewTime); pushEscape(); AddUntilStep("wait for track playing", () => MusicController.IsPlaying); - AddAssert("Ensure time wasn't reset to preview point", () => MusicController.CurrentTrackTime < beatmap().Metadata.PreviewTime); + AddAssert("Ensure time wasn't reset to preview point", () => MusicController.CurrentTrack?.CurrentTime < beatmap().Metadata.PreviewTime); } [Test] @@ -76,11 +77,11 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => songSelect = new TestSongSelect()); - AddUntilStep("wait for no track", () => MusicController.IsDummyDevice); + AddUntilStep("wait for no track", () => MusicController.CurrentTrack?.IsDummyDevice == true); AddStep("return to menu", () => songSelect.Exit()); - AddUntilStep("wait for track", () => !MusicController.IsDummyDevice && MusicController.IsPlaying); + AddUntilStep("wait for track", () => MusicController.CurrentTrack?.IsDummyDevice == false && MusicController.IsPlaying); } [Test] @@ -135,8 +136,8 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded); AddStep("Seek close to end", () => { - Game.MusicController.SeekTo(MusicController.TrackLength - 1000); - // MusicController.Completed += () => trackCompleted = true; + Game.MusicController.SeekTo(MusicController.CurrentTrack.AsNonNull().Length - 1000); + MusicController.CurrentTrack.AsNonNull().Completed += () => trackCompleted = true; }); AddUntilStep("Track was completed", () => trackCompleted); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index f3fef8c355..ac743d76df 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -168,7 +169,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (timingPoints.Count == 0) return 0; if (timingPoints[^1] == current) - return (int)Math.Ceiling((musicController.TrackLength - current.Time) / current.BeatLength); + return (int)Math.Ceiling((musicController.CurrentTrack.AsNonNull().Length - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index 3ecd8ab550..0161ec0c56 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -80,12 +80,12 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Store track", () => currentBeatmap = Beatmap.Value); AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000)); - AddUntilStep(@"Wait for current time to update", () => musicController.CurrentTrackTime > 5000); + AddUntilStep(@"Wait for current time to update", () => musicController.CurrentTrack?.CurrentTime > 5000); AddStep(@"Set previous", () => musicController.PreviousTrack()); AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value); - AddUntilStep("Wait for current time to update", () => musicController.CurrentTrackTime < 5000); + AddUntilStep("Wait for current time to update", () => musicController.CurrentTrack?.CurrentTime < 5000); AddStep(@"Set previous", () => musicController.PreviousTrack()); AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value); diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 2dd28a01dc..92a9ed0566 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -51,6 +51,7 @@ namespace osu.Game.Graphics.Containers protected override void Update() { + ITrack track = null; IBeatmap beatmap = null; double currentTrackTime = 0; @@ -58,11 +59,14 @@ namespace osu.Game.Graphics.Containers EffectControlPoint effectPoint = null; if (musicController.TrackLoaded && Beatmap.Value.BeatmapLoaded) - beatmap = Beatmap.Value.Beatmap; - - if (beatmap != null && musicController.IsPlaying && musicController.TrackLength > 0) { - currentTrackTime = musicController.CurrentTrackTime + EarlyActivationMilliseconds; + track = musicController.CurrentTrack; + beatmap = Beatmap.Value.Beatmap; + } + + if (track != null && beatmap != null && musicController.IsPlaying && track.Length > 0) + { + currentTrackTime = track.CurrentTime + EarlyActivationMilliseconds; timingPoint = beatmap.ControlPointInfo.TimingPointAt(currentTrackTime); effectPoint = beatmap.ControlPointInfo.EffectPointAt(currentTrackTime); @@ -98,7 +102,7 @@ namespace osu.Game.Graphics.Containers return; using (BeginDelayedSequence(-TimeSinceLastBeat, true)) - OnNewBeat(beatIndex, timingPoint, effectPoint, musicController.CurrentAmplitudes); + OnNewBeat(beatIndex, timingPoint, effectPoint, track?.CurrentAmplitudes ?? ChannelAmplitudes.Empty); lastBeat = beatIndex; lastTimingPoint = timingPoint; diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index f5ca5a3a49..c22849b7d6 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -15,7 +14,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Framework.Threading; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Overlays.OSD; @@ -26,7 +24,7 @@ namespace osu.Game.Overlays /// /// Handles playback of the global music track. /// - public class MusicController : CompositeDrawable, IKeyBindingHandler, ITrack + public class MusicController : CompositeDrawable, IKeyBindingHandler { [Resolved] private BeatmapManager beatmaps { get; set; } @@ -70,10 +68,7 @@ namespace osu.Game.Overlays private readonly TrackContainer trackContainer; [CanBeNull] - private DrawableTrack drawableTrack; - - [CanBeNull] - private Track track; + public DrawableTrack CurrentTrack { get; private set; } private IBindable> managerUpdated; private IBindable> managerRemoved; @@ -116,33 +111,12 @@ namespace osu.Game.Overlays /// /// Returns whether the beatmap track is playing. /// - public bool IsPlaying => drawableTrack?.IsRunning ?? false; + public bool IsPlaying => CurrentTrack?.IsRunning ?? false; /// /// Returns whether the beatmap track is loaded. /// - public bool TrackLoaded => drawableTrack?.IsLoaded == true; - - /// - /// Returns the current time of the beatmap track. - /// - public double CurrentTrackTime => drawableTrack?.CurrentTime ?? 0; - - /// - /// Returns the length of the beatmap track. - /// - public double TrackLength => drawableTrack?.Length ?? 0; - - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => trackContainer.AddAdjustment(type, adjustBindable); - - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => trackContainer.RemoveAdjustment(type, adjustBindable); - - public void Reset() => drawableTrack?.Reset(); - - [CanBeNull] - public IAdjustableClock GetTrackClock() => track; + public bool TrackLoaded => CurrentTrack?.IsLoaded == true; private void beatmapUpdated(ValueChangedEvent> weakSet) { @@ -175,7 +149,7 @@ namespace osu.Game.Overlays seekDelegate = Schedule(() => { if (!beatmap.Disabled) - drawableTrack?.Seek(position); + CurrentTrack?.Seek(position); }); } @@ -187,7 +161,7 @@ namespace osu.Game.Overlays { if (IsUserPaused) return; - if (drawableTrack == null || drawableTrack.IsDummyDevice) + if (CurrentTrack == null || CurrentTrack.IsDummyDevice) { if (beatmap.Disabled) return; @@ -208,13 +182,13 @@ namespace osu.Game.Overlays { IsUserPaused = false; - if (drawableTrack == null) + if (CurrentTrack == null) return false; if (restart) - drawableTrack.Restart(); + CurrentTrack.Restart(); else if (!IsPlaying) - drawableTrack.Start(); + CurrentTrack.Start(); return true; } @@ -225,8 +199,8 @@ namespace osu.Game.Overlays public void Stop() { IsUserPaused = true; - if (drawableTrack?.IsRunning == true) - drawableTrack.Stop(); + if (CurrentTrack?.IsRunning == true) + CurrentTrack.Stop(); } /// @@ -235,7 +209,7 @@ namespace osu.Game.Overlays /// Whether the operation was successful. public bool TogglePause() { - if (drawableTrack?.IsRunning == true) + if (CurrentTrack?.IsRunning == true) Stop(); else Play(); @@ -257,7 +231,7 @@ namespace osu.Game.Overlays if (beatmap.Disabled) return PreviousTrackResult.None; - var currentTrackPosition = drawableTrack?.CurrentTime; + var currentTrackPosition = CurrentTrack?.CurrentTime; if (currentTrackPosition >= restart_cutoff_point) { @@ -311,7 +285,7 @@ namespace osu.Game.Overlays { // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase). // we probably want to move this to a central method for switching to a new working beatmap in the future. - Schedule(() => drawableTrack?.Restart()); + Schedule(() => CurrentTrack?.Restart()); } private WorkingBeatmap current; @@ -345,12 +319,11 @@ namespace osu.Game.Overlays current = beatmap.NewValue; - drawableTrack?.Expire(); - drawableTrack = null; - track = null; + CurrentTrack?.Expire(); + CurrentTrack = null; if (current != null) - trackContainer.Add(drawableTrack = new DrawableTrack(track = current.GetRealTrack())); + trackContainer.Add(CurrentTrack = new DrawableTrack(current.GetRealTrack())); TrackChanged?.Invoke(current, direction); @@ -379,15 +352,15 @@ namespace osu.Game.Overlays public void ResetTrackAdjustments() { - if (drawableTrack == null) + if (CurrentTrack == null) return; - drawableTrack.ResetSpeedAdjustments(); + CurrentTrack.ResetSpeedAdjustments(); if (allowRateAdjustments) { foreach (var mod in mods.Value.OfType()) - mod.ApplyToTrack(drawableTrack); + mod.ApplyToTrack(CurrentTrack); } } @@ -442,129 +415,6 @@ namespace osu.Game.Overlays private class TrackContainer : AudioContainer { } - - #region ITrack - - /// - /// The volume of this component. - /// - public BindableNumber Volume => drawableTrack?.Volume; // Todo: Bad - - /// - /// The playback balance of this sample (-1 .. 1 where 0 is centered) - /// - public BindableNumber Balance => drawableTrack?.Balance; // Todo: Bad - - /// - /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. - /// - public BindableNumber Frequency => drawableTrack?.Frequency; // Todo: Bad - - /// - /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. - /// - public BindableNumber Tempo => drawableTrack?.Tempo; // Todo: Bad - - public IBindable AggregateVolume => drawableTrack?.AggregateVolume; // Todo: Bad - - public IBindable AggregateBalance => drawableTrack?.AggregateBalance; // Todo: Bad - - public IBindable AggregateFrequency => drawableTrack?.AggregateFrequency; // Todo: Bad - - public IBindable AggregateTempo => drawableTrack?.AggregateTempo; // Todo: Bad - - /// - /// Overall playback rate (1 is 100%, -1 is reversed at 100%). - /// - public double Rate => AggregateFrequency.Value * AggregateTempo.Value; - - event Action ITrack.Completed - { - add - { - if (drawableTrack != null) - drawableTrack.Completed += value; - } - remove - { - if (drawableTrack != null) - drawableTrack.Completed -= value; - } - } - - event Action ITrack.Failed - { - add - { - if (drawableTrack != null) - drawableTrack.Failed += value; - } - remove - { - if (drawableTrack != null) - drawableTrack.Failed -= value; - } - } - - public bool Looping - { - get => drawableTrack?.Looping ?? false; - set - { - if (drawableTrack != null) - drawableTrack.Looping = value; - } - } - - public bool IsDummyDevice => drawableTrack?.IsDummyDevice ?? true; - - public double RestartPoint - { - get => drawableTrack?.RestartPoint ?? 0; - set - { - if (drawableTrack != null) - drawableTrack.RestartPoint = value; - } - } - - double ITrack.CurrentTime => CurrentTrackTime; - - double ITrack.Length - { - get => TrackLength; - set - { - if (drawableTrack != null) - drawableTrack.Length = value; - } - } - - public int? Bitrate => drawableTrack?.Bitrate; - - bool ITrack.IsRunning => IsPlaying; - - public bool IsReversed => drawableTrack?.IsReversed ?? false; - - public bool HasCompleted => drawableTrack?.HasCompleted ?? false; - - void ITrack.Reset() => drawableTrack?.Reset(); - - void ITrack.Restart() => Play(true); - - void ITrack.ResetSpeedAdjustments() => ResetTrackAdjustments(); - - bool ITrack.Seek(double seek) - { - SeekTo(seek); - return true; - } - - void ITrack.Start() => Play(); - - public ChannelAmplitudes CurrentAmplitudes => drawableTrack?.CurrentAmplitudes ?? ChannelAmplitudes.Empty; - - #endregion } public enum TrackChangeDirection diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index fde6a52fee..15b189ead6 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -234,12 +234,14 @@ namespace osu.Game.Overlays pendingBeatmapSwitch = null; } - if (musicController.IsDummyDevice == false) - { - progressBar.EndTime = musicController.TrackLength; - progressBar.CurrentTime = musicController.CurrentTrackTime; + var track = musicController.TrackLoaded ? musicController.CurrentTrack : null; - playButton.Icon = musicController.IsPlaying ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; + if (track?.IsDummyDevice == false) + { + progressBar.EndTime = track.Length; + progressBar.CurrentTime = track.CurrentTime; + + playButton.Icon = track.IsRunning ? FontAwesome.Regular.PauseCircle : FontAwesome.Regular.PlayCircle; } else { diff --git a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs index 9b840cea08..5ae41fe09c 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToTrack.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; namespace osu.Game.Rulesets.Mods @@ -10,6 +11,6 @@ namespace osu.Game.Rulesets.Mods /// public interface IApplicableToTrack : IApplicableMod { - void ApplyToTrack(ITrack track); + void ApplyToTrack(T track) where T : ITrack, IAdjustableAudioComponent; } } diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index 9cefeb3340..989978eb35 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; @@ -27,11 +26,11 @@ namespace osu.Game.Rulesets.Mods }, true); } - public override void ApplyToTrack(ITrack track) + public override void ApplyToTrack(T track) { // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) - (track as Track)?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); - (track as Track)?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); + track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); + track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } } } diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index b34affa77f..70cdaa6345 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -38,11 +38,11 @@ namespace osu.Game.Rulesets.Mods }, true); } - public override void ApplyToTrack(ITrack track) + public override void ApplyToTrack(T track) { // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) - (track as Track)?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); - (track as Track)?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); + track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); + track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index ee1280da39..e2c8ac64d9 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -12,9 +12,9 @@ namespace osu.Game.Rulesets.Mods { public abstract BindableNumber SpeedChange { get; } - public virtual void ApplyToTrack(ITrack track) + public virtual void ApplyToTrack(T track) where T : ITrack, IAdjustableAudioComponent { - (track as Track)?.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); + track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } public virtual void ApplyToSample(SampleChannel sample) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 0257e241b8..b6cbe72971 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Mods Precision = 0.01, }; - private Track track; + private ITrack track; protected ModTimeRamp() { @@ -51,9 +51,9 @@ namespace osu.Game.Rulesets.Mods AdjustPitch.BindValueChanged(applyPitchAdjustment); } - public void ApplyToTrack(ITrack track) + public void ApplyToTrack(T track) where T : ITrack, IAdjustableAudioComponent { - this.track = track as Track; + this.track = track; FinalRate.TriggerChange(); AdjustPitch.TriggerChange(); @@ -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 as IAdjustableAudioComponent)?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); - track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); + (track as IAdjustableAudioComponent)?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); } private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 0a9b4f06a7..412efe266c 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -66,12 +66,12 @@ namespace osu.Game.Screens.Edit.Components } }; - musicController.AddAdjustment(AdjustableProperty.Tempo, tempo); + musicController.CurrentTrack?.AddAdjustment(AdjustableProperty.Tempo, tempo); } protected override void Dispose(bool isDisposing) { - musicController?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); + musicController?.CurrentTrack?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); base.Dispose(isDisposing); } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 446f7fdf88..24fb855009 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osuTK; @@ -57,7 +58,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts return; } - content.RelativeChildSize = new Vector2((float)Math.Max(1, musicController.TrackLength), 1); + Debug.Assert(musicController.CurrentTrack != null); + content.RelativeChildSize = new Vector2((float)Math.Max(1, musicController.CurrentTrack.Length), 1); } protected virtual void LoadBeatmap(WorkingBeatmap beatmap) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index f35e5defd8..d556d948f6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -60,21 +62,20 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Beatmap.BindValueChanged(b => { waveform.Waveform = b.NewValue.Waveform; + track = musicController.CurrentTrack; - // Todo: Wrong. - Schedule(() => + Debug.Assert(track != null); + + if (track.Length > 0) { - if (musicController.TrackLength > 0) - { - MaxZoom = getZoomLevelForVisibleMilliseconds(500); - MinZoom = getZoomLevelForVisibleMilliseconds(10000); - Zoom = getZoomLevelForVisibleMilliseconds(2000); - } - }); + MaxZoom = getZoomLevelForVisibleMilliseconds(500); + MinZoom = getZoomLevelForVisibleMilliseconds(10000); + Zoom = getZoomLevelForVisibleMilliseconds(2000); + } }, true); } - private float getZoomLevelForVisibleMilliseconds(double milliseconds) => (float)(musicController.TrackLength / milliseconds); + private float getZoomLevelForVisibleMilliseconds(double milliseconds) => (float)(track.Length / milliseconds); /// /// The timeline's scroll position in the last frame. @@ -96,6 +97,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// private bool trackWasPlaying; + private ITrack track; + protected override void Update() { base.Update(); @@ -136,15 +139,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!musicController.TrackLoaded) return; - editorClock.Seek(Current / Content.DrawWidth * musicController.TrackLength); + editorClock.Seek(Current / Content.DrawWidth * track.Length); } private void scrollToTrackTime() { - if (!musicController.TrackLoaded || musicController.TrackLength == 0) + if (!musicController.TrackLoaded || track.Length == 0) return; - ScrollTo((float)(editorClock.CurrentTime / musicController.TrackLength) * Content.DrawWidth, false); + ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false); } protected override bool OnMouseDown(MouseDownEvent e) @@ -188,7 +191,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); private double getTimeFromPosition(Vector2 localPosition) => - (localPosition.X / Content.DrawWidth) * musicController.TrackLength; + (localPosition.X / Content.DrawWidth) * track.Length; public float GetBeatSnapDistanceAt(double referenceTime) => throw new NotImplementedException(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index a833b354ed..ceb0275a13 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -3,6 +3,7 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Overlays; @@ -43,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline for (var 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 : musicController.TrackLength; + var until = i + 1 < beatmap.ControlPointInfo.TimingPoints.Count ? beatmap.ControlPointInfo.TimingPoints[i + 1].Time : musicController.CurrentTrack.AsNonNull().Length; int beat = 0; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 756b03d049..b02aabc24d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.Edit beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - var sourceClock = musicController.GetTrackClock() ?? new StopwatchClock(); + var sourceClock = (IAdjustableClock)musicController.CurrentTrack ?? new StopwatchClock(); clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; clock.ChangeSource(sourceClock); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 4cb24f90a6..4e589aeeef 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Edit private void load() { // Todo: What. - TrackLength ??= musicController.TrackLength; + TrackLength ??= musicController.CurrentTrack?.Length ?? 0; } /// diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 7da5df2723..030923c228 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -113,7 +113,9 @@ namespace osu.Game.Screens.Menu if (setInfo != null) { initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - UsingThemedIntro = !MusicController.IsDummyDevice; + + // Todo: Wrong. + UsingThemedIntro = MusicController.CurrentTrack?.IsDummyDevice == false; } return UsingThemedIntro; diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 86a6fa3802..e29ea6e743 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(new TrianglesIntroSequence(logo, background) { RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(UsingThemedIntro ? MusicController.GetTrackClock() : null), + Clock = new FramedClock(UsingThemedIntro ? MusicController.CurrentTrack : null), LoadMenu = LoadMenu }, t => { diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 62cada577d..85f11eb244 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -44,7 +44,8 @@ namespace osu.Game.Screens.Menu pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano"); - musicController.Looping = true; + if (musicController.CurrentTrack != null) + musicController.CurrentTrack.Looping = true; } protected override void LogoArriving(OsuLogo logo, bool resuming) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 349654165f..7a1ff4fa06 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -19,6 +19,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Utils; using osu.Game.Overlays; @@ -108,14 +109,14 @@ namespace osu.Game.Screens.Menu private void updateAmplitudes() { var effect = beatmap.Value.BeatmapLoaded && musicController.TrackLoaded - ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(musicController.CurrentTrackTime) + ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(musicController.CurrentTrack.AsNonNull().CurrentTime) : null; for (int i = 0; i < temporalAmplitudes.Length; i++) temporalAmplitudes[i] = 0; if (musicController.TrackLoaded) - addAmplitudesFromSource(musicController); + addAmplitudesFromSource(musicController.CurrentTrack.AsNonNull()); foreach (var source in amplitudeSources) addAmplitudesFromSource(source); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index c422f57332..ce48777ce1 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; using System.Linq; using osuTK; using osuTK.Graphics; @@ -181,11 +182,12 @@ namespace osu.Game.Screens.Menu if (last is IntroScreen && musicController.TrackLoaded) { - // Todo: Wrong. - if (!musicController.IsPlaying) + Debug.Assert(musicController.CurrentTrack != null); + + if (!musicController.CurrentTrack.IsRunning) { - musicController.SeekTo(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * musicController.TrackLength); - musicController.Play(); + musicController.CurrentTrack.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * musicController.CurrentTrack.Length); + musicController.CurrentTrack.Start(); } } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 1feb2481c3..f028f9b229 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -330,9 +330,9 @@ namespace osu.Game.Screens.Menu const float velocity_adjust_cutoff = 0.98f; const float paused_velocity = 0.5f; - if (musicController.IsPlaying) + if (musicController.CurrentTrack?.IsRunning == true) { - var maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentAmplitudes.Maximum : 0; + var maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0; logoAmplitudeContainer.Scale = new Vector2((float)Interpolation.Damp(logoAmplitudeContainer.Scale.X, 1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 0.9f, Time.Elapsed)); if (maxAmplitude > velocity_adjust_cutoff) diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs index 032c8d5ca9..c7dc20ff23 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Multi.Match.Components return; } - bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(musicController.TrackLength) < endDate.Value; + bool hasEnoughTime = musicController.CurrentTrack != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(musicController.CurrentTrack.Length) < endDate.Value; Enabled.Value = hasBeatmap && hasEnoughTime; } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 2d74434c76..e068899c7b 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -343,9 +343,13 @@ namespace osu.Game.Screens.Multi { if (screenStack.CurrentScreen is MatchSubScreen) { - musicController.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - musicController.Looping = true; - musicController.EnsurePlayingSomething(); + if (musicController.CurrentTrack != null) + { + musicController.CurrentTrack.RestartPoint = Beatmap.Value.Metadata.PreviewTime; + musicController.CurrentTrack.Looping = true; + + musicController.EnsurePlayingSomething(); + } } else { @@ -355,8 +359,11 @@ namespace osu.Game.Screens.Multi private void cancelLooping() { - musicController.Looping = false; - musicController.RestartPoint = 0; + if (musicController.CurrentTrack != null) + { + musicController.CurrentTrack.Looping = false; + musicController.CurrentTrack.RestartPoint = 0; + } } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 0e0ef8c675..1171d8c3b0 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -9,6 +9,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Overlays; @@ -30,21 +31,21 @@ namespace osu.Game.Screens.Play private readonly BindableDouble trackFreq = new BindableDouble(1); + private DrawableTrack track; + private const float duration = 2500; private SampleChannel failSample; - [Resolved] - private MusicController musicController { get; set; } - public FailAnimation(DrawableRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; } [BackgroundDependencyLoader] - private void load(AudioManager audio, IBindable beatmap) + private void load(AudioManager audio, IBindable beatmap, MusicController musicController) { + track = musicController.CurrentTrack; failSample = audio.Samples.Get(@"Gameplay/failsound"); } @@ -68,7 +69,7 @@ namespace osu.Game.Screens.Play Expire(); }); - musicController.AddAdjustment(AdjustableProperty.Frequency, trackFreq); + track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); applyToPlayfield(drawableRuleset.Playfield); drawableRuleset.Playfield.HitObjectContainer.FlashColour(Color4.Red, 500); @@ -107,7 +108,7 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - musicController?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); + track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); } } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index cf4678ab29..61272f56ad 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -8,8 +8,10 @@ using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; @@ -27,8 +29,7 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; private readonly IReadOnlyList mods; - [Resolved] - private MusicController musicController { get; set; } + private DrawableTrack track; public readonly BindableBool IsPaused = new BindableBool(); @@ -95,8 +96,10 @@ namespace osu.Game.Screens.Play private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, MusicController musicController) { + track = musicController.CurrentTrack; + userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); @@ -121,15 +124,15 @@ namespace osu.Game.Screens.Play { // 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. - musicController.Reset(); + track.ResetSpeedAdjustments(); Task.Run(() => { - musicController.Reset(); + track.Reset(); Schedule(() => { - adjustableClock.ChangeSource(musicController.GetTrackClock()); + adjustableClock.ChangeSource(track); updateRate(); if (!IsPaused.Value) @@ -190,6 +193,20 @@ namespace osu.Game.Screens.Play IsPaused.Value = true; } + /// + /// Changes the backing clock to avoid using the originally provided track. + /// + public void StopUsingBeatmapClock() + { + if (track == null) + return; + + removeSourceClockAdjustments(); + + track = new DrawableTrack(new TrackVirtual(track.Length)); + adjustableClock.ChangeSource(track); + } + protected override void Update() { if (!IsPaused.Value) @@ -202,23 +219,30 @@ namespace osu.Game.Screens.Play private void updateRate() { + if (track == null) return; + speedAdjustmentsApplied = true; - musicController.ResetTrackAdjustments(); - musicController.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - musicController.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + track.ResetSpeedAdjustments(); + + track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + + foreach (var mod in mods.OfType()) + mod.ApplyToTrack(track); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); removeSourceClockAdjustments(); + track = null; } private void removeSourceClockAdjustments() { if (speedAdjustmentsApplied) { - musicController.ResetTrackAdjustments(); + track.ResetSpeedAdjustments(); speedAdjustmentsApplied = false; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 9ba2732920..ea04d82e67 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -31,6 +31,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; +using osu.Framework.Audio.Track; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; using osu.Game.Graphics.UserInterface; @@ -560,9 +561,9 @@ namespace osu.Game.Screens.Select BeatmapDetails.Refresh(); - if (music != null) + if (music?.CurrentTrack != null) { - music.Looping = true; + music.CurrentTrack.Looping = true; music.ResetTrackAdjustments(); } @@ -588,8 +589,8 @@ namespace osu.Game.Screens.Select BeatmapOptions.Hide(); - if (music != null) - music.Looping = false; + if (music?.CurrentTrack != null) + music.CurrentTrack.Looping = false; this.ScaleTo(1.1f, 250, Easing.InSine); @@ -610,8 +611,8 @@ namespace osu.Game.Screens.Select FilterControl.Deactivate(); - if (music != null) - music.Looping = false; + if (music?.CurrentTrack != null) + music.CurrentTrack.Looping = false; return false; } @@ -652,18 +653,30 @@ namespace osu.Game.Screens.Select BeatmapDetails.Beatmap = beatmap; - if (music != null) - music.Looping = false; + if (music?.CurrentTrack != null) + music.CurrentTrack.Looping = false; } + private readonly WeakReference lastTrack = new WeakReference(null); + /// /// Ensures some music is playing for the current track. /// Will resume playback from a manual user pause if the track has changed. /// private void ensurePlayingSelected() { - music.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - music.EnsurePlayingSomething(); + ITrack track = music?.CurrentTrack; + if (track == null) + return; + + bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track; + + track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; + + if (!track.IsRunning && (music?.IsUserPaused != true || isNewTrack)) + music?.Play(true); + + lastTrack.SetTarget(track); } private void carouselBeatmapsLoaded() diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 5226a49def..780b4f1b3a 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual private void beatmapChanged(ValueChangedEvent e) { Clock.ControlPointInfo = e.NewValue.Beatmap.ControlPointInfo; - Clock.ChangeSource(MusicController.GetTrackClock() ?? new StopwatchClock()); + Clock.ChangeSource((IAdjustableClock)MusicController.CurrentTrack ?? new StopwatchClock()); Clock.ProcessFrame(); } diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs index e7cb461d7b..ae4b0ef84a 100644 --- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs +++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.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.Extensions.ObjectExtensions; + namespace osu.Game.Tests.Visual { /// @@ -13,7 +15,7 @@ namespace osu.Game.Tests.Visual base.Update(); // note that this will override any mod rate application - MusicController.Tempo.Value = Clock.Rate; + MusicController.CurrentTrack.AsNonNull().Tempo.Value = Clock.Rate; } } } From 58660c70a3c9cad61e78ef854d238e8f569ee08e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 5 Aug 2020 21:20:41 +0900 Subject: [PATCH 2553/6909] Cache before idle tracker --- 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 929254e8ad..3e41be2028 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -554,8 +554,8 @@ namespace osu.Game Container logoContainer; BackButton.Receptor receptor; - dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); dependencies.CacheAs(MusicController = new MusicController()); + dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); AddRange(new Drawable[] { From e9fc783b1d75f31ec8291fa8a11f254f28cb1860 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 5 Aug 2020 21:21:08 +0900 Subject: [PATCH 2554/6909] Add back loop-on-completion --- osu.Game/OsuGame.cs | 18 +----------------- osu.Game/Overlays/MusicController.cs | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 3e41be2028..a41c7b28a5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -427,23 +427,7 @@ namespace osu.Game updateModDefaults(); - var newBeatmap = beatmap.NewValue; - - if (newBeatmap != null) - { - // MusicController.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 firing. - // if (Beatmap.Value != b) - // return; - // - // if (!MusicController.Looping && !Beatmap.Disabled) - // MusicController.NextTrack(); - // } + beatmap.NewValue?.BeginAsyncLoad(); } private void modsChanged(ValueChangedEvent> mods) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index c22849b7d6..50ad97be7c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -323,7 +324,10 @@ namespace osu.Game.Overlays CurrentTrack = null; if (current != null) + { trackContainer.Add(CurrentTrack = new DrawableTrack(current.GetRealTrack())); + CurrentTrack.Completed += () => onTrackCompleted(current); + } TrackChanged?.Invoke(current, direction); @@ -332,6 +336,18 @@ namespace osu.Game.Overlays queuedDirection = null; } + private void onTrackCompleted(WorkingBeatmap workingBeatmap) + { + // the source of track completion is the audio thread, so the beatmap may have changed before firing. + if (current != workingBeatmap) + return; + + Debug.Assert(CurrentTrack != null); + + if (!CurrentTrack.Looping && !beatmap.Disabled) + NextTrack(); + } + private bool allowRateAdjustments; /// From 11a6c9bdccc65e89575a0e138512ccc379b1a15f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 5 Aug 2020 21:21:14 +0900 Subject: [PATCH 2555/6909] Revert unnecessary change --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 92a9ed0566..69021e1634 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Graphics.Containers beatmap = Beatmap.Value.Beatmap; } - if (track != null && beatmap != null && musicController.IsPlaying && track.Length > 0) + if (track != null && beatmap != null && track.IsRunning && track.Length > 0) { currentTrackTime = track.CurrentTime + EarlyActivationMilliseconds; From f058f5e977bbfb532b2711e0101c76868022b808 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 5 Aug 2020 21:29:53 +0900 Subject: [PATCH 2556/6909] Fix incorrect value being set --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index ea04d82e67..80ed894233 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -654,7 +654,7 @@ namespace osu.Game.Screens.Select BeatmapDetails.Beatmap = beatmap; if (music?.CurrentTrack != null) - music.CurrentTrack.Looping = false; + music.CurrentTrack.Looping = true; } private readonly WeakReference lastTrack = new WeakReference(null); From 0edd50939783f86909c3e024d47100259417c42e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 5 Aug 2020 21:30:11 +0900 Subject: [PATCH 2557/6909] Only change track when audio doesn't equal --- osu.Game/Overlays/MusicController.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 50ad97be7c..47d1bef177 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -320,6 +320,18 @@ namespace osu.Game.Overlays current = beatmap.NewValue; + if (!beatmap.OldValue.BeatmapInfo.AudioEquals(current?.BeatmapInfo)) + changeTrack(); + + TrackChanged?.Invoke(current, direction); + + ResetTrackAdjustments(); + + queuedDirection = null; + } + + private void changeTrack() + { CurrentTrack?.Expire(); CurrentTrack = null; @@ -328,12 +340,6 @@ namespace osu.Game.Overlays trackContainer.Add(CurrentTrack = new DrawableTrack(current.GetRealTrack())); CurrentTrack.Completed += () => onTrackCompleted(current); } - - TrackChanged?.Invoke(current, direction); - - ResetTrackAdjustments(); - - queuedDirection = null; } private void onTrackCompleted(WorkingBeatmap workingBeatmap) From 86ae61c6b79aec0febbe8220781cc7ad3aefb9de Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 5 Aug 2020 22:09:47 +0900 Subject: [PATCH 2558/6909] Re-implement store transferral in BeatmapManager --- osu.Game/Beatmaps/BeatmapManager.cs | 19 +++++++++++-------- .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 17 +++++++++-------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b2329f58ad..6a7d0b053f 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -218,7 +218,7 @@ namespace osu.Game.Beatmaps removeWorkingCache(info); } - private readonly WeakList workingCache = new WeakList(); + private readonly WeakList workingCache = new WeakList(); /// /// Retrieve a instance for the provided @@ -246,16 +246,19 @@ namespace osu.Game.Beatmaps lock (workingCache) { var working = workingCache.FirstOrDefault(w => w.BeatmapInfo?.ID == beatmapInfo.ID); + if (working != null) + return working; - if (working == 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)); - } + ITrackStore trackStore = workingCache.FirstOrDefault(b => b.BeatmapInfo.AudioEquals(beatmapInfo))?.TrackStore; + TextureStore textureStore = workingCache.FirstOrDefault(b => b.BeatmapInfo.BackgroundEquals(beatmapInfo))?.TextureStore; + + textureStore ??= new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); + trackStore ??= audioManager.GetTrackStore(Files.Store); + + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager)); - // previous?.TransferTo(working); return working; } } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 33945a9eb1..ceefef5d7e 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -19,13 +19,18 @@ namespace osu.Game.Beatmaps { protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap { + public readonly TextureStore TextureStore; + public readonly ITrackStore TrackStore; + private readonly IResourceStore store; - public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, BeatmapInfo beatmapInfo, AudioManager audioManager) + public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager) : base(beatmapInfo, audioManager) { this.store = store; - this.textureStore = textureStore; + + TextureStore = textureStore; + TrackStore = trackStore; } protected override IBeatmap GetBeatmap() @@ -44,10 +49,6 @@ namespace osu.Game.Beatmaps private string getPathForFile(string filename) => BeatmapSetInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, filename, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - private TextureStore textureStore; - - private ITrackStore trackStore; - protected override bool BackgroundStillValid(Texture b) => false; // bypass lazy logic. we want to return a new background each time for refcounting purposes. protected override Texture GetBackground() @@ -57,7 +58,7 @@ namespace osu.Game.Beatmaps try { - return textureStore.Get(getPathForFile(Metadata.BackgroundFile)); + return TextureStore.Get(getPathForFile(Metadata.BackgroundFile)); } catch (Exception e) { @@ -70,7 +71,7 @@ namespace osu.Game.Beatmaps { try { - return (trackStore ??= AudioManager.GetTrackStore(store)).Get(getPathForFile(Metadata.AudioFile)); + return TrackStore.Get(getPathForFile(Metadata.AudioFile)); } catch (Exception e) { From 0f7fde5d2cd5db7eb8e621bfe221ea28cc252da8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 5 Aug 2020 22:32:44 +0900 Subject: [PATCH 2559/6909] Revert unnecessary change --- osu.Game/Overlays/Music/PlaylistOverlay.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index c089158c01..b9a58c37cb 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -83,7 +83,10 @@ namespace osu.Game.Overlays.Music BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); if (toSelect != null) + { beatmap.Value = beatmaps.GetWorkingBeatmap(toSelect); + musicController.CurrentTrack?.Restart(); + } }; } @@ -116,12 +119,12 @@ namespace osu.Game.Overlays.Music { if (set.ID == (beatmap.Value?.BeatmapSetInfo?.ID ?? -1)) { - musicController.SeekTo(0); + musicController.CurrentTrack?.Seek(0); return; } beatmap.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First()); - musicController.Play(true); + musicController.CurrentTrack?.Restart(); } } From fe8c462498ad18a842544202bc4e6a14d3e217ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 17:00:17 +0900 Subject: [PATCH 2560/6909] Remove intermediate container --- osu.Game/Overlays/MusicController.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 47d1bef177..6adfa1817e 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -65,20 +65,12 @@ namespace osu.Game.Overlays [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } - [NotNull] - private readonly TrackContainer trackContainer; - [CanBeNull] public DrawableTrack CurrentTrack { get; private set; } private IBindable> managerUpdated; private IBindable> managerRemoved; - public MusicController() - { - InternalChild = trackContainer = new TrackContainer { RelativeSizeAxes = Axes.Both }; - } - [BackgroundDependencyLoader] private void load() { @@ -337,8 +329,10 @@ namespace osu.Game.Overlays if (current != null) { - trackContainer.Add(CurrentTrack = new DrawableTrack(current.GetRealTrack())); + CurrentTrack = new DrawableTrack(current.GetRealTrack()); CurrentTrack.Completed += () => onTrackCompleted(current); + + AddInternal(CurrentTrack); } } @@ -433,10 +427,6 @@ namespace osu.Game.Overlays { } } - - private class TrackContainer : AudioContainer - { - } } public enum TrackChangeDirection From e8ab3cff3c38c5dad046b0c69f0f34b7478a08ce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 17:02:42 +0900 Subject: [PATCH 2561/6909] Add class constraint --- osu.Game/Rulesets/Mods/IApplicableToTrack.cs | 2 +- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 3 ++- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs index 5ae41fe09c..b29ba55942 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Mods /// public interface IApplicableToTrack : IApplicableMod { - void ApplyToTrack(T track) where T : ITrack, IAdjustableAudioComponent; + void ApplyToTrack(T track) where T : class, ITrack, IAdjustableAudioComponent; } } diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index e2c8ac64d9..4aee5affe9 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -12,7 +12,8 @@ namespace osu.Game.Rulesets.Mods { public abstract BindableNumber SpeedChange { get; } - public virtual void ApplyToTrack(T track) where T : ITrack, IAdjustableAudioComponent + public virtual void ApplyToTrack(T track) + where T : class, ITrack, IAdjustableAudioComponent { track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index b6cbe72971..4b5241488f 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -51,7 +51,8 @@ namespace osu.Game.Rulesets.Mods AdjustPitch.BindValueChanged(applyPitchAdjustment); } - public void ApplyToTrack(T track) where T : ITrack, IAdjustableAudioComponent + public void ApplyToTrack(T track) + where T : class, ITrack, IAdjustableAudioComponent { this.track = track; From ad959ce5238dbd8ca4465305d703efac065a3dab Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 6 Aug 2020 01:06:51 -0700 Subject: [PATCH 2562/6909] Make toolbar button abstract --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 86a3f5d8aa..a03ea64eb2 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -18,7 +18,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Toolbar { - public class ToolbarButton : OsuClickableContainer + public abstract class ToolbarButton : OsuClickableContainer { public const float WIDTH = Toolbar.HEIGHT * 1.4f; @@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Toolbar private readonly SpriteText tooltip2; protected FillFlowContainer Flow; - public ToolbarButton() + protected ToolbarButton() : base(HoverSampleSet.Loud) { Width = WIDTH; From c72ab9047e3ea00dbc6eb176a797d20dfd5c95d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 17:15:33 +0900 Subject: [PATCH 2563/6909] Cleanup test scene disposal --- osu.Game/Tests/Visual/OsuTestScene.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index e968f7e675..f2b9388fdc 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; @@ -169,7 +170,10 @@ namespace osu.Game.Tests.Visual rulesetDependencies?.Dispose(); if (MusicController?.TrackLoaded == true) - MusicController.Stop(); + { + Debug.Assert(MusicController.CurrentTrack != null); + MusicController.CurrentTrack.Stop(); + } if (contextFactory.IsValueCreated) contextFactory.Value.ResetDatabase(); From 7bcb68ffacd76d2f7b5d743781598b733f2c2731 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 6 Aug 2020 01:17:24 -0700 Subject: [PATCH 2564/6909] Handle overlay toggling with toolbar buttons instead --- osu.Game/OsuGame.cs | 28 +----------------- .../Toolbar/ToolbarBeatmapListingButton.cs | 3 ++ osu.Game/Overlays/Toolbar/ToolbarButton.cs | 29 +++++++++++++++---- .../Overlays/Toolbar/ToolbarChatButton.cs | 3 ++ .../Overlays/Toolbar/ToolbarHomeButton.cs | 18 ++---------- .../Overlays/Toolbar/ToolbarMusicButton.cs | 3 ++ .../Toolbar/ToolbarNotificationButton.cs | 3 ++ .../Overlays/Toolbar/ToolbarSettingsButton.cs | 3 ++ .../Overlays/Toolbar/ToolbarSocialButton.cs | 3 ++ 9 files changed, 45 insertions(+), 48 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 26f7c3b93b..b5752214bd 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -67,8 +67,6 @@ namespace osu.Game [NotNull] private readonly NotificationOverlay notifications = new NotificationOverlay(); - private NowPlayingOverlay nowPlaying; - private BeatmapListingOverlay beatmapListing; private DashboardOverlay dashboard; @@ -650,7 +648,7 @@ namespace osu.Game Origin = Anchor.TopRight, }, rightFloatingOverlayContent.Add, true); - loadComponentSingleFile(nowPlaying = new NowPlayingOverlay + loadComponentSingleFile(new NowPlayingOverlay { GetToolbarHeight = () => ToolbarOffset, Anchor = Anchor.TopRight, @@ -862,18 +860,6 @@ namespace osu.Game switch (action) { - case GlobalAction.ToggleNowPlaying: - nowPlaying.ToggleVisibility(); - return true; - - case GlobalAction.ToggleChat: - chatOverlay.ToggleVisibility(); - return true; - - case GlobalAction.ToggleSocial: - dashboard.ToggleVisibility(); - return true; - case GlobalAction.ResetInputSettings: var sensitivity = frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity); @@ -889,18 +875,6 @@ namespace osu.Game Toolbar.ToggleVisibility(); return true; - case GlobalAction.ToggleSettings: - Settings.ToggleVisibility(); - return true; - - case GlobalAction.ToggleDirect: - beatmapListing.ToggleVisibility(); - return true; - - case GlobalAction.ToggleNotifications: - notifications.ToggleVisibility(); - return true; - case GlobalAction.ToggleGameplayMouseButtons: LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get(OsuSetting.MouseDisableButtons)); return true; diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs index 64430c77ac..cde305fffd 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Game.Graphics; +using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { @@ -13,6 +14,8 @@ namespace osu.Game.Overlays.Toolbar SetIcon(OsuIcon.ChevronDownCircle); TooltipMain = "Beatmap listing"; TooltipSub = "Browse for new beatmaps"; + + Hotkey = GlobalAction.ToggleDirect; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index a03ea64eb2..3f1dccc45a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -5,23 +5,27 @@ 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.Bindings; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Toolbar { - public abstract class ToolbarButton : OsuClickableContainer + public abstract class ToolbarButton : OsuClickableContainer, IKeyBindingHandler { public const float WIDTH = Toolbar.HEIGHT * 1.4f; + protected GlobalAction? Hotkey { get; set; } + public void SetIcon(Drawable icon) { IconContainer.Icon = icon; @@ -164,6 +168,21 @@ namespace osu.Game.Overlays.Toolbar HoverBackground.FadeOut(200); tooltipContainer.FadeOut(100); } + + public bool OnPressed(GlobalAction action) + { + if (action == Hotkey) + { + Click(); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } } public class OpaqueBackground : Container diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs index ec7da54571..dee4be0c1f 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { @@ -13,6 +14,8 @@ namespace osu.Game.Overlays.Toolbar SetIcon(FontAwesome.Solid.Comments); TooltipMain = "Chat"; TooltipSub = "Join the real-time discussion"; + + Hotkey = GlobalAction.ToggleChat; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index e642f0c453..4845c9a99f 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -2,33 +2,19 @@ // 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, IKeyBindingHandler + public class ToolbarHomeButton : ToolbarButton { public ToolbarHomeButton() { Icon = FontAwesome.Solid.Home; 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) - { + Hotkey = GlobalAction.Home; } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index 712da12208..59276a5943 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { @@ -13,6 +14,8 @@ namespace osu.Game.Overlays.Toolbar Icon = FontAwesome.Solid.Music; TooltipMain = "Now playing"; TooltipSub = "Manage the currently playing track"; + + Hotkey = GlobalAction.ToggleNowPlaying; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs index dbd6c557d3..a699fd907f 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; @@ -28,6 +29,8 @@ namespace osu.Game.Overlays.Toolbar TooltipMain = "Notifications"; TooltipSub = "Waiting for 'ya"; + Hotkey = GlobalAction.ToggleNotifications; + Add(countDisplay = new CountCircle { Alpha = 0, diff --git a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs index 79942012f9..ed2a23ec2a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { @@ -13,6 +14,8 @@ namespace osu.Game.Overlays.Toolbar Icon = FontAwesome.Solid.Cog; TooltipMain = "Settings"; TooltipSub = "Change your settings"; + + Hotkey = GlobalAction.ToggleSettings; } [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index 0dbb552c15..6faa58c559 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Sprites; +using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { @@ -13,6 +14,8 @@ namespace osu.Game.Overlays.Toolbar Icon = FontAwesome.Solid.Users; TooltipMain = "Friends"; TooltipSub = "Interact with those close to you"; + + Hotkey = GlobalAction.ToggleSocial; } [BackgroundDependencyLoader(true)] From d574cac702612dc4c36748588cebf4991d3ab64f Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 6 Aug 2020 01:18:45 -0700 Subject: [PATCH 2565/6909] Add keybinding to toolbar button's tooltip --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 35 ++++++++++++++++++++-- 1 file changed, 32 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 3f1dccc45a..9fd47fd150 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.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.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,6 +15,7 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Input; using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; @@ -70,8 +72,12 @@ namespace osu.Game.Overlays.Toolbar private readonly FillFlowContainer tooltipContainer; private readonly SpriteText tooltip1; private readonly SpriteText tooltip2; + private readonly SpriteText keyBindingTooltip; protected FillFlowContainer Flow; + [Resolved] + private KeyBindingStore store { get; set; } + protected ToolbarButton() : base(HoverSampleSet.Loud) { @@ -127,7 +133,7 @@ namespace osu.Game.Overlays.Toolbar Origin = TooltipAnchor, Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5), Alpha = 0, - Children = new[] + Children = new Drawable[] { tooltip1 = new OsuSpriteText { @@ -136,17 +142,40 @@ namespace osu.Game.Overlays.Toolbar Shadow = true, Font = OsuFont.GetFont(size: 22, weight: FontWeight.Bold), }, - tooltip2 = new OsuSpriteText + new FillFlowContainer { + AutoSizeAxes = Axes.Both, Anchor = TooltipAnchor, Origin = TooltipAnchor, - Shadow = true, + Direction = FillDirection.Horizontal, + Children = new[] + { + tooltip2 = new OsuSpriteText { Shadow = true }, + keyBindingTooltip = new OsuSpriteText { Shadow = true } + } } } } }; } + [BackgroundDependencyLoader] + private void load() + { + updateTooltip(); + + store.KeyBindingChanged += updateTooltip; + } + + private void updateTooltip() + { + var binding = store.Query().Find(b => (GlobalAction)b.Action == Hotkey); + + var keyBindingString = binding?.KeyCombination.ReadableString(); + + keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) ? $" ({keyBindingString})" : string.Empty; + } + protected override bool OnMouseDown(MouseDownEvent e) => true; protected override bool OnClick(ClickEvent e) From f9c369b23cff9b0bdc1b8b7907ed74d8919bf123 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 6 Aug 2020 01:20:03 -0700 Subject: [PATCH 2566/6909] Fix toolbar music button tooltip overflowing off-screen --- osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index 59276a5943..f9aa2de497 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Game.Input.Bindings; @@ -9,6 +10,8 @@ namespace osu.Game.Overlays.Toolbar { public class ToolbarMusicButton : ToolbarOverlayToggleButton { + protected override Anchor TooltipAnchor => Anchor.TopRight; + public ToolbarMusicButton() { Icon = FontAwesome.Solid.Music; From f53672193eb6ccd1d14c7900603aa5269e7468c9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 17:48:07 +0900 Subject: [PATCH 2567/6909] Fix track stores being kept alive --- osu.Game/Beatmaps/BeatmapManager.cs | 16 +++++++++++++++- .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 10 +++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 6a7d0b053f..f22f41531a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -219,6 +219,7 @@ namespace osu.Game.Beatmaps } private readonly WeakList workingCache = new WeakList(); + private readonly Dictionary referencedTrackStores = new Dictionary(); /// /// Retrieve a instance for the provided @@ -257,12 +258,25 @@ namespace osu.Game.Beatmaps textureStore ??= new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); trackStore ??= audioManager.GetTrackStore(Files.Store); - workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager)); + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager, dereferenceTrackStore)); + referencedTrackStores[trackStore] = referencedTrackStores.GetOrDefault(trackStore) + 1; return working; } } + private void dereferenceTrackStore(ITrackStore trackStore) + { + lock (workingCache) + { + if (--referencedTrackStores[trackStore] == 0) + { + referencedTrackStores.Remove(trackStore); + trackStore.Dispose(); + } + } + } + /// /// Perform a lookup query on available s. /// diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index ceefef5d7e..a54d46c1b1 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -23,11 +23,14 @@ namespace osu.Game.Beatmaps public readonly ITrackStore TrackStore; private readonly IResourceStore store; + private readonly Action dereferenceAction; - public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager) + public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager, + Action dereferenceAction) : base(beatmapInfo, audioManager) { this.store = store; + this.dereferenceAction = dereferenceAction; TextureStore = textureStore; TrackStore = trackStore; @@ -137,6 +140,11 @@ namespace osu.Game.Beatmaps return null; } } + + ~BeatmapManagerWorkingBeatmap() + { + dereferenceAction?.Invoke(TrackStore); + } } } } From c8ebbc8594b79423825614268feb88f2f20a32e6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 18:19:55 +0900 Subject: [PATCH 2568/6909] Remove MusicController from EditorClock --- .../Timelines/Summary/Parts/MarkerPart.cs | 5 +-- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Edit/EditorClock.cs | 36 ++++++------------- 3 files changed, 13 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs index a353f79ef4..9e9ac93d23 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osuTK; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -59,9 +58,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts return; float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); - - Debug.Assert(editorClock.TrackLength != null); - editorClock.SeekTo(markerPos / DrawWidth * editorClock.TrackLength.Value); + editorClock.SeekTo(markerPos / DrawWidth * editorClock.TrackLength); }); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b02aabc24d..79b13a7eac 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.Edit // Todo: should probably be done at a DrawableRuleset level to share logic with Player. var sourceClock = (IAdjustableClock)musicController.CurrentTrack ?? new StopwatchClock(); - clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; + clock = new EditorClock(Beatmap.Value, musicController.CurrentTrack?.Length ?? 0, beatDivisor) { IsCoupled = false }; clock.ChangeSource(sourceClock); dependencies.CacheAs(clock); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 4e589aeeef..fbfa397795 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -2,16 +2,13 @@ // 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.Graphics; using osu.Framework.Graphics.Transforms; using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Overlays; namespace osu.Game.Screens.Edit { @@ -20,7 +17,7 @@ namespace osu.Game.Screens.Edit /// public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { - public double? TrackLength { get; private set; } + public readonly double TrackLength; public ControlPointInfo ControlPointInfo; @@ -28,34 +25,24 @@ namespace osu.Game.Screens.Edit private readonly DecoupleableInterpolatingFramedClock underlyingClock; - [Resolved] - private MusicController musicController { get; set; } - - public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) - : this(beatmap.Beatmap.ControlPointInfo, null, beatDivisor) + public EditorClock(WorkingBeatmap beatmap, double trackLength, BindableBeatDivisor beatDivisor) + : this(beatmap.Beatmap.ControlPointInfo, trackLength, beatDivisor) { } - public EditorClock(ControlPointInfo controlPointInfo, double? trackLength, BindableBeatDivisor beatDivisor) - { - this.beatDivisor = beatDivisor; - - ControlPointInfo = controlPointInfo; - TrackLength = trackLength; - - underlyingClock = new DecoupleableInterpolatingFramedClock(); - } - public EditorClock() : this(new ControlPointInfo(), 1000, new BindableBeatDivisor()) { } - [BackgroundDependencyLoader] - private void load() + public EditorClock(ControlPointInfo controlPointInfo, double trackLength, BindableBeatDivisor beatDivisor) { - // Todo: What. - TrackLength ??= musicController.CurrentTrack?.Length ?? 0; + this.beatDivisor = beatDivisor; + + ControlPointInfo = controlPointInfo; + TrackLength = trackLength; + + underlyingClock = new DecoupleableInterpolatingFramedClock(); } /// @@ -148,8 +135,7 @@ namespace osu.Game.Screens.Edit seekTime = timingPoint.Time; // Ensure the sought point is within the boundaries - Debug.Assert(TrackLength != null); - seekTime = Math.Clamp(seekTime, 0, TrackLength.Value); + seekTime = Math.Clamp(seekTime, 0, TrackLength); SeekTo(seekTime); } From 9685df0ecac3c264b6379a240c073473c4d410ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Aug 2020 18:22:17 +0900 Subject: [PATCH 2569/6909] Only update key binding on next usage to avoid large blocking calls --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 9fd47fd150..0afc6642b2 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -76,7 +77,7 @@ namespace osu.Game.Overlays.Toolbar protected FillFlowContainer Flow; [Resolved] - private KeyBindingStore store { get; set; } + private KeyBindingStore keyBindings { get; set; } protected ToolbarButton() : base(HoverSampleSet.Loud) @@ -159,21 +160,25 @@ namespace osu.Game.Overlays.Toolbar }; } + private readonly Cached tooltipKeyBinding = new Cached(); + [BackgroundDependencyLoader] private void load() { - updateTooltip(); - - store.KeyBindingChanged += updateTooltip; + keyBindings.KeyBindingChanged += () => tooltipKeyBinding.Invalidate(); + updateKeyBindingTooltip(); } - private void updateTooltip() + private void updateKeyBindingTooltip() { - var binding = store.Query().Find(b => (GlobalAction)b.Action == Hotkey); + if (tooltipKeyBinding.IsValid) + return; + var binding = keyBindings.Query().Find(b => (GlobalAction)b.Action == Hotkey); var keyBindingString = binding?.KeyCombination.ReadableString(); - keyBindingTooltip.Text = !string.IsNullOrEmpty(keyBindingString) ? $" ({keyBindingString})" : string.Empty; + + tooltipKeyBinding.Validate(); } protected override bool OnMouseDown(MouseDownEvent e) => true; @@ -187,6 +192,8 @@ namespace osu.Game.Overlays.Toolbar protected override bool OnHover(HoverEvent e) { + updateKeyBindingTooltip(); + HoverBackground.FadeIn(200); tooltipContainer.FadeIn(100); return base.OnHover(e); From 7c3ae4ed4291eeb86f704344956dedfef75e8735 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 18:25:34 +0900 Subject: [PATCH 2570/6909] Remove generics from IApplicableToTrack --- osu.Game/Rulesets/Mods/IApplicableToTrack.cs | 3 +-- osu.Game/Rulesets/Mods/ModDaycore.cs | 7 ++++--- osu.Game/Rulesets/Mods/ModNightcore.cs | 6 +++--- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 5 ++--- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 3 +-- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs b/osu.Game/Rulesets/Mods/IApplicableToTrack.cs index b29ba55942..9b840cea08 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToTrack.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToTrack.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.Audio; using osu.Framework.Audio.Track; namespace osu.Game.Rulesets.Mods @@ -11,6 +10,6 @@ namespace osu.Game.Rulesets.Mods /// public interface IApplicableToTrack : IApplicableMod { - void ApplyToTrack(T track) where T : class, ITrack, IAdjustableAudioComponent; + void ApplyToTrack(ITrack track); } } diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index 989978eb35..800312d047 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; @@ -26,11 +27,11 @@ namespace osu.Game.Rulesets.Mods }, true); } - public override void ApplyToTrack(T track) + public override void ApplyToTrack(ITrack track) { // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) - track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); + (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); + (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } } } diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 70cdaa6345..4932df08f1 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -38,11 +38,11 @@ namespace osu.Game.Rulesets.Mods }, true); } - public override void ApplyToTrack(T track) + public override void ApplyToTrack(ITrack track) { // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) - track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); + (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); + (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 4aee5affe9..ae7077c67b 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -12,10 +12,9 @@ namespace osu.Game.Rulesets.Mods { public abstract BindableNumber SpeedChange { get; } - public virtual void ApplyToTrack(T track) - where T : class, ITrack, IAdjustableAudioComponent + public virtual void ApplyToTrack(ITrack track) { - track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); + (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } public virtual void ApplyToSample(SampleChannel sample) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 4b5241488f..b904cf007b 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -51,8 +51,7 @@ namespace osu.Game.Rulesets.Mods AdjustPitch.BindValueChanged(applyPitchAdjustment); } - public void ApplyToTrack(T track) - where T : class, ITrack, IAdjustableAudioComponent + public void ApplyToTrack(ITrack track) { this.track = track; From 2e3ecf71c70c035db37621df04cc289a4b7d7489 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 18:31:08 +0900 Subject: [PATCH 2571/6909] Pass track from Player to components --- .../TestSceneGameplayClockContainer.cs | 7 +++++- .../Gameplay/TestSceneStoryboardSamples.cs | 6 +++-- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 4 +++- osu.Game/Screens/Play/FailAnimation.cs | 16 ++++++-------- .../Screens/Play/GameplayClockContainer.cs | 22 +++++++++---------- osu.Game/Screens/Play/Player.cs | 11 +++++----- 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs index cd3669f160..40f6cecd9a 100644 --- a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs @@ -19,7 +19,12 @@ namespace osu.Game.Tests.Gameplay { GameplayClockContainer gcc = null; - AddStep("create container", () => Add(gcc = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty(), 0))); + AddStep("create container", () => + { + var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + Add(gcc = new GameplayClockContainer(working.GetRealTrack(), working, Array.Empty(), 0)); + }); + AddStep("start track", () => gcc.Start()); AddUntilStep("elapsed greater than zero", () => gcc.GameplayClock.ElapsedFrameTime > 0); } diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index b30870d057..720436fae4 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -59,7 +59,9 @@ namespace osu.Game.Tests.Gameplay AddStep("create container", () => { - Add(gameplayContainer = new GameplayClockContainer(CreateWorkingBeatmap(new OsuRuleset().RulesetInfo), Array.Empty(), 0)); + var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + + Add(gameplayContainer = new GameplayClockContainer(working.GetRealTrack(), working, Array.Empty(), 0)); gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) { @@ -103,7 +105,7 @@ namespace osu.Game.Tests.Gameplay Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); SelectedMods.Value = new[] { testedMod }; - Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0)); + Add(gameplayContainer = new GameplayClockContainer(MusicController.CurrentTrack, Beatmap.Value, SelectedMods.Value, 0)); gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 7ed7a116b4..68110d759c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -32,7 +32,9 @@ namespace osu.Game.Tests.Visual.Gameplay requestCount = 0; increment = skip_time; - Child = gameplayClockContainer = new GameplayClockContainer(CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)), Array.Empty(), 0) + var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); + + Child = gameplayClockContainer = new GameplayClockContainer(working.GetRealTrack(), working, Array.Empty(), 0) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 1171d8c3b0..a7bfca612e 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -8,11 +8,10 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; -using osu.Framework.Graphics.Audio; using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Overlays; using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; @@ -28,24 +27,23 @@ namespace osu.Game.Screens.Play public Action OnComplete; private readonly DrawableRuleset drawableRuleset; + private readonly ITrack track; private readonly BindableDouble trackFreq = new BindableDouble(1); - private DrawableTrack track; - private const float duration = 2500; private SampleChannel failSample; - public FailAnimation(DrawableRuleset drawableRuleset) + public FailAnimation(DrawableRuleset drawableRuleset, ITrack track) { this.drawableRuleset = drawableRuleset; + this.track = track; } [BackgroundDependencyLoader] - private void load(AudioManager audio, IBindable beatmap, MusicController musicController) + private void load(AudioManager audio, IBindable beatmap) { - track = musicController.CurrentTrack; failSample = audio.Samples.Get(@"Gameplay/failsound"); } @@ -69,7 +67,7 @@ namespace osu.Game.Screens.Play Expire(); }); - track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); + (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, trackFreq); applyToPlayfield(drawableRuleset.Playfield); drawableRuleset.Playfield.HitObjectContainer.FlashColour(Color4.Red, 500); @@ -108,7 +106,7 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); + (track as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); } } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 61272f56ad..c4f368e1f5 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -11,12 +11,10 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Overlays; using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Play @@ -29,7 +27,7 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; private readonly IReadOnlyList mods; - private DrawableTrack track; + private ITrack track; public readonly BindableBool IsPaused = new BindableBool(); @@ -62,11 +60,13 @@ namespace osu.Game.Screens.Play private readonly FramedOffsetClock platformOffsetClock; - public GameplayClockContainer(WorkingBeatmap beatmap, IReadOnlyList mods, double gameplayStartTime) + public GameplayClockContainer(ITrack track, WorkingBeatmap beatmap, IReadOnlyList mods, double gameplayStartTime) { this.beatmap = beatmap; this.mods = mods; this.gameplayStartTime = gameplayStartTime; + this.track = track; + firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; RelativeSizeAxes = Axes.Both; @@ -96,10 +96,8 @@ namespace osu.Game.Screens.Play private readonly BindableDouble pauseFreqAdjust = new BindableDouble(1); [BackgroundDependencyLoader] - private void load(OsuConfigManager config, MusicController musicController) + private void load(OsuConfigManager config) { - track = musicController.CurrentTrack; - userAudioOffset = config.GetBindable(OsuSetting.AudioOffset); userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); @@ -132,7 +130,7 @@ namespace osu.Game.Screens.Play Schedule(() => { - adjustableClock.ChangeSource(track); + adjustableClock.ChangeSource((IAdjustableClock)track); updateRate(); if (!IsPaused.Value) @@ -203,8 +201,8 @@ namespace osu.Game.Screens.Play removeSourceClockAdjustments(); - track = new DrawableTrack(new TrackVirtual(track.Length)); - adjustableClock.ChangeSource(track); + track = new TrackVirtual(track.Length); + adjustableClock.ChangeSource((IAdjustableClock)track); } protected override void Update() @@ -224,8 +222,8 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = true; track.ResetSpeedAdjustments(); - track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); foreach (var mod in mods.OfType()) mod.ApplyToTrack(track); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..e92164de7c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -8,6 +8,7 @@ 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.Graphics; using osu.Framework.Graphics.Containers; @@ -150,7 +151,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuConfigManager config) + private void load(AudioManager audio, OsuConfigManager config, MusicController musicController) { Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray(); @@ -178,7 +179,7 @@ namespace osu.Game.Screens.Play if (!ScoreProcessor.Mode.Disabled) config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); - InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime); + InternalChild = GameplayClockContainer = new GameplayClockContainer(musicController.CurrentTrack, Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime); AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); @@ -187,7 +188,7 @@ namespace osu.Game.Screens.Play addUnderlayComponents(GameplayClockContainer); addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap); - addOverlayComponents(GameplayClockContainer, Beatmap.Value); + addOverlayComponents(GameplayClockContainer, Beatmap.Value, musicController.CurrentTrack); if (!DrawableRuleset.AllowGameplayOverlays) { @@ -264,7 +265,7 @@ namespace osu.Game.Screens.Play }); } - private void addOverlayComponents(Container target, WorkingBeatmap working) + private void addOverlayComponents(Container target, WorkingBeatmap working, ITrack track) { target.AddRange(new[] { @@ -331,7 +332,7 @@ namespace osu.Game.Screens.Play performImmediateExit(); }, }, - failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, + failAnimation = new FailAnimation(DrawableRuleset, track) { OnComplete = onFailComplete, }, }); } From ef689d943aafc413dac909da50d96532816abd6c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 18:54:08 +0900 Subject: [PATCH 2572/6909] Fix intros playing incorrectly --- osu.Game/OsuGame.cs | 4 ++-- osu.Game/Screens/Menu/IntroScreen.cs | 13 +++++++------ osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- osu.Game/Screens/Menu/IntroWelcome.cs | 9 ++------- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a41c7b28a5..0049e5a520 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -581,6 +581,8 @@ namespace osu.Game ScreenStack.ScreenPushed += screenPushed; ScreenStack.ScreenExited += screenExited; + loadComponentSingleFile(MusicController, Add); + loadComponentSingleFile(osuLogo, logo => { logoContainer.Add(logo); @@ -602,8 +604,6 @@ namespace osu.Game loadComponentSingleFile(new OnScreenDisplay(), Add, true); - loadComponentSingleFile(MusicController, Add); - loadComponentSingleFile(notifications.With(d => { d.GetToolbarHeight = () => ToolbarOffset; diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 030923c228..7e327261ab 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -44,8 +44,7 @@ namespace osu.Game.Screens.Menu private WorkingBeatmap initialBeatmap; - [Resolved] - protected MusicController MusicController { get; private set; } + protected ITrack Track { get; private set; } private readonly BindableDouble exitingVolumeFade = new BindableDouble(1); @@ -62,6 +61,9 @@ namespace osu.Game.Screens.Menu [Resolved] private AudioManager audio { get; set; } + [Resolved] + private MusicController musicController { get; set; } + /// /// Whether the is provided by osu! resources, rather than a user beatmap. /// @@ -113,9 +115,7 @@ namespace osu.Game.Screens.Menu if (setInfo != null) { initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - - // Todo: Wrong. - UsingThemedIntro = MusicController.CurrentTrack?.IsDummyDevice == false; + UsingThemedIntro = initialBeatmap.GetRealTrack().IsDummyDevice == false; } return UsingThemedIntro; @@ -154,7 +154,7 @@ 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) - MusicController.Play(true); + Track.Restart(); } protected override void LogoArriving(OsuLogo logo, bool resuming) @@ -168,6 +168,7 @@ namespace osu.Game.Screens.Menu if (!resuming) { beatmap.Value = initialBeatmap; + Track = musicController.CurrentTrack; 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 e29ea6e743..86f7dbdd6f 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(new TrianglesIntroSequence(logo, background) { RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(UsingThemedIntro ? MusicController.CurrentTrack : null), + Clock = new FramedClock(UsingThemedIntro ? (IAdjustableClock)Track : null), LoadMenu = LoadMenu }, t => { diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index 85f11eb244..e81646456f 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osuTK.Graphics; @@ -31,9 +30,6 @@ namespace osu.Game.Screens.Menu Alpha = 0, }; - [Resolved] - private MusicController musicController { get; set; } - private BackgroundScreenDefault background; [BackgroundDependencyLoader] @@ -43,9 +39,6 @@ namespace osu.Game.Screens.Menu welcome = audio.Samples.Get(@"Intro/Welcome/welcome"); pianoReverb = audio.Samples.Get(@"Intro/Welcome/welcome_piano"); - - if (musicController.CurrentTrack != null) - musicController.CurrentTrack.Looping = true; } protected override void LogoArriving(OsuLogo logo, bool resuming) @@ -54,6 +47,8 @@ namespace osu.Game.Screens.Menu if (!resuming) { + Track.Looping = true; + LoadComponentAsync(new WelcomeIntroSequence { RelativeSizeAxes = Axes.Both From f8279dab328d758f2a590924de44a8a8921ca595 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 18:54:14 +0900 Subject: [PATCH 2573/6909] Refactor MainMenu --- osu.Game/Screens/Menu/MainMenu.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index ce48777ce1..ea4347a285 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -170,9 +170,6 @@ namespace osu.Game.Screens.Menu [Resolved] private Storage storage { get; set; } - [Resolved] - private MusicController musicController { get; set; } - public override void OnEntering(IScreen last) { base.OnEntering(last); @@ -180,14 +177,14 @@ namespace osu.Game.Screens.Menu var metadata = Beatmap.Value.Metadata; - if (last is IntroScreen && musicController.TrackLoaded) + if (last is IntroScreen && music.TrackLoaded) { - Debug.Assert(musicController.CurrentTrack != null); + Debug.Assert(music.CurrentTrack != null); - if (!musicController.CurrentTrack.IsRunning) + if (!music.CurrentTrack.IsRunning) { - musicController.CurrentTrack.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * musicController.CurrentTrack.Length); - musicController.CurrentTrack.Start(); + music.CurrentTrack.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * music.CurrentTrack.Length); + music.CurrentTrack.Start(); } } From adf4f56dce1871816e29bde01e51fe78a77397ab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 19:01:23 +0900 Subject: [PATCH 2574/6909] Move MusicController to OsuGameBase --- osu.Game/OsuGame.cs | 5 ----- osu.Game/OsuGameBase.cs | 6 ++++++ 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0049e5a520..cf4610793c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -538,7 +538,6 @@ namespace osu.Game Container logoContainer; BackButton.Receptor receptor; - dependencies.CacheAs(MusicController = new MusicController()); dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); AddRange(new Drawable[] @@ -581,8 +580,6 @@ namespace osu.Game ScreenStack.ScreenPushed += screenPushed; ScreenStack.ScreenExited += screenExited; - loadComponentSingleFile(MusicController, Add); - loadComponentSingleFile(osuLogo, logo => { logoContainer.Add(logo); @@ -925,8 +922,6 @@ namespace osu.Game private ScalingContainer screenContainer; - protected MusicController MusicController { get; private set; } - protected override bool OnExiting() { if (ScreenStack.CurrentScreen is Loader) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 24c1f7849c..51b9b7278d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -30,6 +30,7 @@ using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; +using osu.Game.Overlays; using osu.Game.Resources; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -73,6 +74,8 @@ namespace osu.Game protected MenuCursorContainer MenuCursorContainer; + protected MusicController MusicController; + private Container content; protected override Container Content => content; @@ -265,6 +268,9 @@ namespace osu.Game dependencies.Cache(previewTrackManager = new PreviewTrackManager()); Add(previewTrackManager); + AddInternal(MusicController = new MusicController()); + dependencies.CacheAs(MusicController); + Ruleset.BindValueChanged(onRulesetChanged); } From 4cfca71d080822734e7f29de27b5273f3304460a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 19:05:15 +0900 Subject: [PATCH 2575/6909] Fix a few test scenes --- osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs index ae4b0ef84a..a6266d210c 100644 --- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs +++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.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 osu.Framework.Extensions.ObjectExtensions; namespace osu.Game.Tests.Visual @@ -15,7 +16,11 @@ namespace osu.Game.Tests.Visual base.Update(); // note that this will override any mod rate application - MusicController.CurrentTrack.AsNonNull().Tempo.Value = Clock.Rate; + if (MusicController.TrackLoaded) + { + Debug.Assert(MusicController.CurrentTrack != null); + MusicController.CurrentTrack.Tempo.Value = Clock.Rate; + } } } } From d1af1429b3dd8d79ba36bc5d41ac02a75ad86bac Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 6 Aug 2020 19:08:45 +0900 Subject: [PATCH 2576/6909] Fix inspection --- osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs index a6266d210c..54458716b1 100644 --- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs +++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; -using osu.Framework.Extensions.ObjectExtensions; namespace osu.Game.Tests.Visual { From e3105fd4c80caec86c086e66b5bad55d2f5d29c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Aug 2020 19:16:26 +0900 Subject: [PATCH 2577/6909] Add more resilient logic for whether to avoid playing SkinnableSound on no volume --- .../Objects/Drawables/DrawableSpinner.cs | 6 ++---- osu.Game/Screens/Play/PauseOverlay.cs | 8 ++------ osu.Game/Skinning/SkinnableSound.cs | 12 +++++++++++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 7363da0de8..b74a9c7197 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -82,8 +82,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private SkinnableSound spinningSample; - private const float minimum_volume = 0.0001f; - protected override void LoadSamples() { base.LoadSamples(); @@ -100,7 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables AddInternal(spinningSample = new SkinnableSound(clone) { - Volume = { Value = minimum_volume }, + Volume = { Value = 0 }, Looping = true, }); } @@ -118,7 +116,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } else { - spinningSample?.VolumeTo(minimum_volume, 200).Finally(_ => spinningSample.Stop()); + spinningSample?.VolumeTo(0, 200).Finally(_ => spinningSample.Stop()); } } diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index fa917cda32..3cdc558951 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -25,8 +25,6 @@ namespace osu.Game.Screens.Play protected override Action BackAction => () => InternalButtons.Children.First().Click(); - private const float minimum_volume = 0.0001f; - [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -37,10 +35,8 @@ namespace osu.Game.Screens.Play AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("pause-loop")) { Looping = true, + Volume = { Value = 0 } }); - - // SkinnableSound only plays a sound if its aggregate volume is > 0, so the volume must be turned up before playing it - pauseLoop.VolumeTo(minimum_volume); } protected override void PopIn() @@ -55,7 +51,7 @@ namespace osu.Game.Screens.Play { base.PopOut(); - pauseLoop.VolumeTo(minimum_volume, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop()); + pauseLoop.VolumeTo(0, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop()); } } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 27f6c37895..8c18e83e92 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -28,6 +28,16 @@ namespace osu.Game.Skinning public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; + /// + /// Whether to play the underlying sample when aggregate volume is zero. + /// Note that this is checked at the point of calling ; changing the volume post-play will not begin playback. + /// Defaults to false unless . + /// + /// + /// Can serve as an optimisation if it is known ahead-of-time that this behaviour will not negatively affect behaviour. + /// + protected bool SkipPlayWhenZeroVolume => !Looping; + private readonly AudioContainer samplesContainer; public SkinnableSound(ISampleInfo hitSamples) @@ -87,7 +97,7 @@ namespace osu.Game.Skinning { samplesContainer.ForEach(c => { - if (c.AggregateVolume.Value > 0) + if (!SkipPlayWhenZeroVolume || c.AggregateVolume.Value > 0) c.Play(); }); } From f994bf28884b66d7efc4654c50ebbb961640ecb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Aug 2020 21:34:48 +0900 Subject: [PATCH 2578/6909] 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 924e9c4a16..e5fed09c07 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 627c2f3d33..18c3052ca3 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 f443937017..b034253d88 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From c84452cfbfdb8e72424702ee1e25cea82aa39606 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 6 Aug 2020 21:53:20 +0900 Subject: [PATCH 2579/6909] Update usages --- osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs | 2 ++ osu.Game/Skinning/SkinnableSound.cs | 9 +++++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index 168e937256..83a1077d70 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -117,6 +117,8 @@ namespace osu.Game.Rulesets.UI public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); + public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException(); + public BindableNumber Volume => throw new NotSupportedException(); public BindableNumber Balance => throw new NotSupportedException(); diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 27f6c37895..f19aaee821 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; @@ -149,28 +150,28 @@ namespace osu.Game.Skinning /// Smoothly adjusts over time. /// /// A to which further transforms can be added. - public TransformSequence VolumeTo(double newVolume, double duration = 0, Easing easing = Easing.None) => + 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) => + 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) => + 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) => + public TransformSequence> TempoTo(double newTempo, double duration = 0, Easing easing = Easing.None) => samplesContainer.TempoTo(newTempo, duration, easing); #endregion From e0ae2b3ebf20e1454af64b001e81365e301f221b Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Thu, 6 Aug 2020 17:07:36 +0200 Subject: [PATCH 2580/6909] Switch to SkinReloadableDrawable --- .../Drawables/DrawableStoryboardSprite.cs | 30 ++++++++----------- 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index d40af903a6..d4f27bf4aa 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -14,11 +14,11 @@ using osu.Game.Skinning; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardSprite : Sprite, IFlippable, IVectorScalable + public class DrawableStoryboardSprite : SkinReloadableDrawable, IFlippable, IVectorScalable { public StoryboardSprite Sprite { get; } - private ISkinSource currentSkin; + private Sprite drawableSprite; private TextureStore storyboardTextureStore; @@ -123,34 +123,28 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader] private void load(ISkinSource skin, IBindable beatmap, TextureStore textureStore) { - if (skin != null) + InternalChild = drawableSprite = new Sprite { - currentSkin = skin; - skin.SourceChanged += onChange; - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }; storyboardTextureStore = textureStore; texturePath = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - skinChanged(); - Sprite.ApplyTransforms(this); } - private void onChange() => - // schedule required to avoid calls after disposed. - // note that this has the side-effect of components only performing a possible texture change when they are alive. - Scheduler.AddOnce(skinChanged); - - private void skinChanged() + protected override void SkinChanged(ISkinSource skin, bool allowFallback) { - var newTexture = currentSkin?.GetTexture(Sprite.Path) ?? storyboardTextureStore?.Get(texturePath); + base.SkinChanged(skin, allowFallback); + var newTexture = skin?.GetTexture(Sprite.Path) ?? storyboardTextureStore?.Get(texturePath); - if (Texture == newTexture) return; + if (drawableSprite.Texture == newTexture) return; - Size = Vector2.Zero; // Sprite size needs to be recalculated (e.g. aspect ratio of combo number textures may differ between skins) - Texture = newTexture; + drawableSprite.Size = Vector2.Zero; // Sprite size needs to be recalculated (e.g. aspect ratio of combo number textures may differ between skins) + drawableSprite.Texture = newTexture; } } } From bce3f3952fbc1154b74d190b2947624b1558c574 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 16:36:40 +0900 Subject: [PATCH 2581/6909] Split out variable declaration --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 9fbd9f50b4..0ab3e8825b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -30,7 +30,8 @@ namespace osu.Game.Rulesets.Osu.Skinning } private Container circleSprites; - private Sprite hitCircleSprite, hitCircleOverlay; + private Sprite hitCircleSprite; + private Sprite hitCircleOverlay; private SkinnableSpriteText hitCircleText; From f8ef53a62e0f4fecc26a614373749ccf08b14d43 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 7 Aug 2020 10:17:11 +0200 Subject: [PATCH 2582/6909] Fix tests. --- .../Visual/Gameplay/TestSceneOverlayActivation.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 9e93cf363d..03e1337125 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Allocation; using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -13,21 +12,25 @@ namespace osu.Game.Tests.Visual.Gameplay { private OverlayTestPlayer testPlayer; - [Resolved] - private OsuConfigManager config { get; set; } - public override void SetUpSteps() { - AddStep("disable overlay activation during gameplay", () => config.Set(OsuSetting.GameplayDisableOverlayActivation, true)); + AddStep("disable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, true)); base.SetUpSteps(); } [Test] - public void TestGameplayOverlayActivationSetting() + public void TestGameplayOverlayActivation() { AddAssert("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); } + [Test] + public void TestGameplayOverlayActivationDisabled() + { + AddStep("enable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, false)); + AddAssert("activation mode is user triggered", () => testPlayer.OverlayActivationMode == OverlayActivation.UserTriggered); + } + [Test] public void TestGameplayOverlayActivationPaused() { From 2e0f567d5def9ce469170c00f4d1c7caa4c0f43f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 7 Aug 2020 11:33:02 +0300 Subject: [PATCH 2583/6909] Implement HomeNewsPanel component --- .../Visual/Online/TestSceneHomeNewsPanel.cs | 38 +++ osu.Game/Overlays/Dashboard/Home/HomePanel.cs | 58 +++++ .../Dashboard/Home/News/HomeNewsPanel.cs | 240 ++++++++++++++++++ osu.Game/Overlays/News/NewsCard.cs | 34 +-- osu.Game/Overlays/News/NewsPostBackground.cs | 37 +++ 5 files changed, 375 insertions(+), 32 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs create mode 100644 osu.Game/Overlays/Dashboard/Home/HomePanel.cs create mode 100644 osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs create mode 100644 osu.Game/Overlays/News/NewsPostBackground.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs new file mode 100644 index 0000000000..262bc51cd8 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.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.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Game.Online.API.Requests.Responses; +using osu.Framework.Allocation; +using osu.Game.Overlays; +using System; +using osu.Game.Overlays.Dashboard.Home.News; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneHomeNewsPanel : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider overlayColour = new OverlayColourProvider(OverlayColourScheme.Purple); + + public TestSceneHomeNewsPanel() + { + Add(new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Y, + Width = 500, + Child = new HomeNewsPanel(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.", + FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png", + PublishedAt = DateTimeOffset.Now, + Slug = "2020-07-16-summer-theme-park-2020-voting-open" + }) + }); + } + } +} diff --git a/osu.Game/Overlays/Dashboard/Home/HomePanel.cs b/osu.Game/Overlays/Dashboard/Home/HomePanel.cs new file mode 100644 index 0000000000..bbe7e411fd --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Home/HomePanel.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 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 osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Dashboard.Home +{ + public class HomePanel : Container + { + protected override Container Content => content; + + [Resolved] + protected OverlayColourProvider ColourProvider { get; private set; } + + private readonly Container content; + private readonly Box background; + + public HomePanel() + { + 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(0, 1) + }; + + AddRangeInternal(new Drawable[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both + }, + content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + }); + } + + [BackgroundDependencyLoader] + private void load() + { + background.Colour = ColourProvider.Background4; + } + } +} diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs new file mode 100644 index 0000000000..85e31b3034 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs @@ -0,0 +1,240 @@ +// Copyright (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.Framework.Platform; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays.News; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Dashboard.Home.News +{ + public class HomeNewsPanel : HomePanel + { + private readonly APINewsPost post; + + public HomeNewsPanel(APINewsPost post) + { + this.post = post; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new ClickableNewsBackground(post), + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + Width = 80, + Padding = new MarginPadding(10), + Children = new Drawable[] + { + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = 1, + Colour = ColourProvider.Light1 + }, + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = 11 }, + Child = new DateContainer(post.PublishedAt) + } + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Right = 10 }, + Children = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 5, Bottom = 10 }, + Spacing = new Vector2(0, 10), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new TitleLink(post), + new TextFlowContainer(f => + { + f.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = post.Preview + } + } + } + } + } + } + } + } + } + } + }; + } + + private class ClickableNewsBackground : OsuHoverContainer + { + private readonly APINewsPost post; + + public ClickableNewsBackground(APINewsPost post) + { + this.post = post; + + RelativeSizeAxes = Axes.X; + Height = 130; + } + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + NewsPostBackground bg; + + Child = new DelayedLoadWrapper(bg = new NewsPostBackground(post.FirstImage) + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Alpha = 0 + }) + { + RelativeSizeAxes = Axes.Both + }; + + bg.OnLoadComplete += d => d.FadeIn(250, Easing.In); + + TooltipText = "view in browser"; + Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug); + + HoverColour = Color4.White; + } + } + + private class TitleLink : OsuHoverContainer + { + private readonly APINewsPost post; + + public TitleLink(APINewsPost post) + { + this.post = post; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Child = new TextFlowContainer(t => + { + t.Font = OsuFont.GetFont(weight: FontWeight.Bold); + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = post.Title + }; + + TooltipText = "view in browser"; + Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug); + } + } + + private class DateContainer : CompositeDrawable, IHasCustomTooltip + { + public ITooltip GetCustomTooltip() => new DateTooltip(); + + public object TooltipContent => date; + + private readonly DateTimeOffset date; + + public DateContainer(DateTimeOffset date) + { + this.date = date; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + AutoSizeAxes = Axes.Both; + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Font = OsuFont.GetFont(weight: FontWeight.Bold), // using Bold since there is no 800 weight alternative + Colour = colourProvider.Light1, + Text = date.Day.ToString() + }, + new TextFlowContainer(f => + { + f.Font = OsuFont.GetFont(size: 11, weight: FontWeight.Regular); + f.Colour = colourProvider.Light1; + }) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Text = $"{date:MMM yyyy}" + } + } + }; + } + } + } +} diff --git a/osu.Game/Overlays/News/NewsCard.cs b/osu.Game/Overlays/News/NewsCard.cs index 201c3ce826..599b45fa78 100644 --- a/osu.Game/Overlays/News/NewsCard.cs +++ b/osu.Game/Overlays/News/NewsCard.cs @@ -9,8 +9,6 @@ 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.Framework.Platform; using osu.Game.Graphics; @@ -48,7 +46,7 @@ namespace osu.Game.Overlays.News Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug); } - NewsBackground bg; + NewsPostBackground bg; AddRange(new Drawable[] { background = new Box @@ -70,7 +68,7 @@ namespace osu.Game.Overlays.News CornerRadius = 6, Children = new Drawable[] { - new DelayedLoadWrapper(bg = new NewsBackground(post.FirstImage) + new DelayedLoadWrapper(bg = new NewsPostBackground(post.FirstImage) { RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fill, @@ -123,34 +121,6 @@ namespace osu.Game.Overlays.News main.AddText(post.Author, t => t.Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold)); } - [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, IHasCustomTooltip { public ITooltip GetCustomTooltip() => new DateTooltip(); diff --git a/osu.Game/Overlays/News/NewsPostBackground.cs b/osu.Game/Overlays/News/NewsPostBackground.cs new file mode 100644 index 0000000000..386ef7f669 --- /dev/null +++ b/osu.Game/Overlays/News/NewsPostBackground.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.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Overlays.News +{ + [LongRunningLoad] + public class NewsPostBackground : Sprite + { + private readonly string sourceUrl; + + public NewsPostBackground(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; + } + } +} From 76d35a7667eb082d6917e1e032ab3d9f418b905d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 7 Aug 2020 12:59:45 +0300 Subject: [PATCH 2584/6909] Implement HomeNewsGroupPanel --- .../Visual/Online/TestSceneHomeNewsPanel.cs | 38 ++++- osu.Game/Overlays/Dashboard/Home/HomePanel.cs | 7 +- .../Dashboard/Home/News/HomeNewsGroupPanel.cs | 85 ++++++++++ .../Dashboard/Home/News/HomeNewsPanel.cs | 152 ++++-------------- .../Home/News/HomeNewsPanelFooter.cs | 79 +++++++++ .../Home/News/NewsPostDrawableDate.cs | 37 +++++ .../Dashboard/Home/News/NewsTitleLink.cs | 43 +++++ 7 files changed, 311 insertions(+), 130 deletions(-) create mode 100644 osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs create mode 100644 osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.cs create mode 100644 osu.Game/Overlays/Dashboard/Home/News/NewsPostDrawableDate.cs create mode 100644 osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs index 262bc51cd8..78d77c9e97 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs @@ -8,6 +8,8 @@ using osu.Framework.Allocation; using osu.Game.Overlays; using System; using osu.Game.Overlays.Dashboard.Home.News; +using osuTK; +using System.Collections.Generic; namespace osu.Game.Tests.Visual.Online { @@ -18,20 +20,40 @@ namespace osu.Game.Tests.Visual.Online public TestSceneHomeNewsPanel() { - Add(new Container + Add(new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, AutoSizeAxes = Axes.Y, Width = 500, - Child = new HomeNewsPanel(new APINewsPost + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] { - 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.", - FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png", - PublishedAt = DateTimeOffset.Now, - Slug = "2020-07-16-summer-theme-park-2020-voting-open" - }) + new HomeNewsPanel(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.", + FirstImage = "/help/wiki/shared/news/banners/monthly-beatmapping-contest.png", + PublishedAt = DateTimeOffset.Now, + Slug = "2020-07-16-summer-theme-park-2020-voting-open" + }), + new HomeNewsGroupPanel(new List + { + new APINewsPost + { + Title = "Title 1", + Slug = "2020-07-16-summer-theme-park-2020-voting-open", + PublishedAt = DateTimeOffset.Now, + }, + new APINewsPost + { + Title = "Title of this post is Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.", + Slug = "2020-07-16-summer-theme-park-2020-voting-open", + PublishedAt = DateTimeOffset.Now, + } + }) + } }); } } diff --git a/osu.Game/Overlays/Dashboard/Home/HomePanel.cs b/osu.Game/Overlays/Dashboard/Home/HomePanel.cs index bbe7e411fd..ce053cd4ec 100644 --- a/osu.Game/Overlays/Dashboard/Home/HomePanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/HomePanel.cs @@ -16,9 +16,6 @@ namespace osu.Game.Overlays.Dashboard.Home { protected override Container Content => content; - [Resolved] - protected OverlayColourProvider ColourProvider { get; private set; } - private readonly Container content; private readonly Box background; @@ -50,9 +47,9 @@ namespace osu.Game.Overlays.Dashboard.Home } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { - background.Colour = ColourProvider.Background4; + background.Colour = colourProvider.Background4; } } } diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs new file mode 100644 index 0000000000..cd1c5393c5 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.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 System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.Dashboard.Home.News +{ + public class HomeNewsGroupPanel : HomePanel + { + private readonly List posts; + + public HomeNewsGroupPanel(List posts) + { + this.posts = posts; + } + + [BackgroundDependencyLoader] + private void load() + { + Content.Padding = new MarginPadding { Vertical = 5 }; + + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = posts.Select(p => new CollapsedNewsPanel(p)).ToArray() + }; + } + + private class CollapsedNewsPanel : HomeNewsPanelFooter + { + public CollapsedNewsPanel(APINewsPost post) + : base(post) + { + } + + protected override Drawable CreateContent(APINewsPost post) => new NewsTitleLink(post); + + protected override NewsPostDrawableDate CreateDate(DateTimeOffset date) => new Date(date); + + private class Date : NewsPostDrawableDate + { + public Date(DateTimeOffset date) + : base(date) + { + } + + protected override Drawable CreateDate(DateTimeOffset date, OverlayColourProvider colourProvider) + { + var drawableDate = new TextFlowContainer(t => + { + t.Colour = colourProvider.Light1; + }) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Vertical = 5 } + }; + + drawableDate.AddText($"{date:dd} ", t => + { + t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); + }); + + drawableDate.AddText($"{date:MMM}", t => + { + t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular); + }); + + return drawableDate; + } + } + } + } +} diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs index 85e31b3034..3548b7c88d 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs @@ -5,8 +5,6 @@ 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.Framework.Platform; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -40,81 +38,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News Children = new Drawable[] { new ClickableNewsBackground(post), - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize), - new Dimension() - }, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Y, - Width = 80, - Padding = new MarginPadding(10), - Children = new Drawable[] - { - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = 1, - Colour = ColourProvider.Light1 - }, - new Container - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 11 }, - Child = new DateContainer(post.PublishedAt) - } - } - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 10 }, - Children = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 5, Bottom = 10 }, - Spacing = new Vector2(0, 10), - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new TitleLink(post), - new TextFlowContainer(f => - { - f.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); - }) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = post.Preview - } - } - } - } - } - } - } - } + new Footer(post) } } }; @@ -158,54 +82,48 @@ namespace osu.Game.Overlays.Dashboard.Home.News } } - private class TitleLink : OsuHoverContainer + private class Footer : HomeNewsPanelFooter { - private readonly APINewsPost post; + protected override float BarPading => 10; - public TitleLink(APINewsPost post) + public Footer(APINewsPost post) + : base(post) { - this.post = post; - - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; } - [BackgroundDependencyLoader] - private void load(GameHost host) + protected override NewsPostDrawableDate CreateDate(DateTimeOffset date) => new Date(date); + + protected override Drawable CreateContent(APINewsPost post) => new FillFlowContainer { - Child = new TextFlowContainer(t => + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 5, Bottom = 10 }, + Spacing = new Vector2(0, 10), + Direction = FillDirection.Vertical, + Children = new Drawable[] { - t.Font = OsuFont.GetFont(weight: FontWeight.Bold); - }) + new NewsTitleLink(post), + new TextFlowContainer(f => + { + f.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = post.Preview + } + } + }; + + private class Date : NewsPostDrawableDate + { + public Date(DateTimeOffset date) + : base(date) { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = post.Title - }; + Margin = new MarginPadding { Top = 10 }; + } - TooltipText = "view in browser"; - Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug); - } - } - - private class DateContainer : CompositeDrawable, IHasCustomTooltip - { - public ITooltip GetCustomTooltip() => new DateTooltip(); - - public object TooltipContent => date; - - private readonly DateTimeOffset date; - - public DateContainer(DateTimeOffset date) - { - this.date = date; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - AutoSizeAxes = Axes.Both; - InternalChild = new FillFlowContainer + protected override Drawable CreateDate(DateTimeOffset date, OverlayColourProvider colourProvider) => new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, @@ -219,7 +137,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News Origin = Anchor.TopRight, Font = OsuFont.GetFont(weight: FontWeight.Bold), // using Bold since there is no 800 weight alternative Colour = colourProvider.Light1, - Text = date.Day.ToString() + Text = $"{date: dd}" }, new TextFlowContainer(f => { diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.cs new file mode 100644 index 0000000000..591f53ac4a --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.Dashboard.Home.News +{ + public abstract class HomeNewsPanelFooter : CompositeDrawable + { + protected virtual float BarPading { get; } = 0; + + private readonly APINewsPost post; + + protected HomeNewsPanelFooter(APINewsPost post) + { + this.post = post; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, size: 60), + new Dimension(GridSizeMode.Absolute, size: 20), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + CreateDate(post.PublishedAt), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = BarPading }, + Child = new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight, + Width = 1, + RelativeSizeAxes = Axes.Y, + Colour = colourProvider.Light1 + } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Right = 10 }, + Child = CreateContent(post) + } + } + } + }; + } + + protected abstract NewsPostDrawableDate CreateDate(DateTimeOffset date); + + protected abstract Drawable CreateContent(APINewsPost post); + } +} diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsPostDrawableDate.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsPostDrawableDate.cs new file mode 100644 index 0000000000..8ba58e27a7 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsPostDrawableDate.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Game.Graphics; +using osu.Framework.Graphics; + +namespace osu.Game.Overlays.Dashboard.Home.News +{ + public abstract class NewsPostDrawableDate : CompositeDrawable, IHasCustomTooltip + { + public ITooltip GetCustomTooltip() => new DateTooltip(); + + public object TooltipContent => date; + + private readonly DateTimeOffset date; + + protected NewsPostDrawableDate(DateTimeOffset date) + { + this.date = date; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + InternalChild = CreateDate(date, colourProvider); + } + + protected abstract Drawable CreateDate(DateTimeOffset date, OverlayColourProvider colourProvider); + } +} diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs new file mode 100644 index 0000000000..da98c92bbe --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs @@ -0,0 +1,43 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.Dashboard.Home.News +{ + public class NewsTitleLink : OsuHoverContainer + { + private readonly APINewsPost post; + + public NewsTitleLink(APINewsPost post) + { + this.post = post; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Child = new TextFlowContainer(t => + { + t.Font = OsuFont.GetFont(weight: FontWeight.Bold); + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = post.Title + }; + + TooltipText = "view in browser"; + Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug); + } + } +} From cddd4f0a97842eefd68ba1ddda18f81ec5ca09b3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 7 Aug 2020 13:18:31 +0300 Subject: [PATCH 2585/6909] Implement HomeShowMoreNewsPanel --- .../Visual/Online/TestSceneHomeNewsPanel.cs | 3 +- .../Home/News/HomeShowMoreNewsPanel.cs | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/Dashboard/Home/News/HomeShowMoreNewsPanel.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs index 78d77c9e97..b1c0c5adcd 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs @@ -52,7 +52,8 @@ namespace osu.Game.Tests.Visual.Online Slug = "2020-07-16-summer-theme-park-2020-voting-open", PublishedAt = DateTimeOffset.Now, } - }) + }), + new HomeShowMoreNewsPanel() } }); } diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeShowMoreNewsPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeShowMoreNewsPanel.cs new file mode 100644 index 0000000000..abb4bd7969 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Home/News/HomeShowMoreNewsPanel.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Dashboard.Home.News +{ + public class HomeShowMoreNewsPanel : OsuHoverContainer + { + protected override IEnumerable EffectTargets => new[] { text }; + + [Resolved(canBeNull: true)] + private NewsOverlay overlay { get; set; } + + private OsuSpriteText text; + + public HomeShowMoreNewsPanel() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Child = new HomePanel + { + Child = text = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Margin = new MarginPadding { Vertical = 20 }, + Text = "see more" + } + }; + + IdleColour = colourProvider.Light1; + HoverColour = Color4.White; + + Action = () => + { + overlay?.ShowFrontPage(); + }; + } + } +} From 61b632516eb73c43918697a9e408b5d75d04ab4d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 19:43:16 +0900 Subject: [PATCH 2586/6909] Ensure CurrentTrack is never null --- osu.Game.Tests/Visual/Editing/TimelineTestScene.cs | 3 --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 4 ++-- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 2 +- osu.Game/Overlays/MusicController.cs | 6 ++---- .../Edit/Components/Timelines/Summary/Parts/TimelinePart.cs | 1 - .../Edit/Compose/Components/Timeline/TimelineTickDisplay.cs | 2 +- osu.Game/Screens/Menu/LogoVisualisation.cs | 4 ++-- osu.Game/Screens/Menu/MainMenu.cs | 2 -- osu.Game/Tests/Visual/OsuTestScene.cs | 3 --- osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs | 3 --- 10 files changed, 8 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 4113bdddf8..347b5757c8 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -95,10 +95,7 @@ namespace osu.Game.Tests.Visual.Editing base.Update(); if (musicController.TrackLoaded) - { - Debug.Assert(musicController.CurrentTrack != null); marker.X = (float)(editorClock.CurrentTime / musicController.CurrentTrack.Length); - } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 946bc2a175..e5d862cfc7 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -136,8 +136,8 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded); AddStep("Seek close to end", () => { - Game.MusicController.SeekTo(MusicController.CurrentTrack.AsNonNull().Length - 1000); - MusicController.CurrentTrack.AsNonNull().Completed += () => trackCompleted = true; + Game.MusicController.SeekTo(MusicController.CurrentTrack.Length - 1000); + MusicController.CurrentTrack.Completed += () => trackCompleted = true; }); AddUntilStep("Track was completed", () => trackCompleted); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index ac743d76df..3cccfa992e 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -169,7 +169,7 @@ namespace osu.Game.Tests.Visual.UserInterface if (timingPoints.Count == 0) return 0; if (timingPoints[^1] == current) - return (int)Math.Ceiling((musicController.CurrentTrack.AsNonNull().Length - current.Time) / current.BeatLength); + return (int)Math.Ceiling((musicController.CurrentTrack.Length - current.Time) / current.BeatLength); return (int)Math.Ceiling((getNextTimingPoint(current).Time - current.Time) / current.BeatLength); } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 6adfa1817e..7e3bb1ce89 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -65,8 +64,7 @@ namespace osu.Game.Overlays [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } - [CanBeNull] - public DrawableTrack CurrentTrack { get; private set; } + public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000)); private IBindable> managerUpdated; private IBindable> managerRemoved; @@ -312,7 +310,7 @@ namespace osu.Game.Overlays current = beatmap.NewValue; - if (!beatmap.OldValue.BeatmapInfo.AudioEquals(current?.BeatmapInfo)) + if (CurrentTrack == null || !beatmap.OldValue.BeatmapInfo.AudioEquals(current?.BeatmapInfo)) changeTrack(); TrackChanged?.Invoke(current, direction); diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 24fb855009..7085c8b020 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -58,7 +58,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts return; } - Debug.Assert(musicController.CurrentTrack != null); content.RelativeChildSize = new Vector2((float)Math.Max(1, musicController.CurrentTrack.Length), 1); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index ceb0275a13..1ce33f221a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline for (var 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 : musicController.CurrentTrack.AsNonNull().Length; + var until = i + 1 < beatmap.ControlPointInfo.TimingPoints.Count ? beatmap.ControlPointInfo.TimingPoints[i + 1].Time : musicController.CurrentTrack.Length; int beat = 0; diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 7a1ff4fa06..974b704200 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -109,14 +109,14 @@ namespace osu.Game.Screens.Menu private void updateAmplitudes() { var effect = beatmap.Value.BeatmapLoaded && musicController.TrackLoaded - ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(musicController.CurrentTrack.AsNonNull().CurrentTime) + ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(musicController.CurrentTrack.CurrentTime) : null; for (int i = 0; i < temporalAmplitudes.Length; i++) temporalAmplitudes[i] = 0; if (musicController.TrackLoaded) - addAmplitudesFromSource(musicController.CurrentTrack.AsNonNull()); + addAmplitudesFromSource(musicController.CurrentTrack); foreach (var source in amplitudeSources) addAmplitudesFromSource(source); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index ea4347a285..518277bce3 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -179,8 +179,6 @@ namespace osu.Game.Screens.Menu if (last is IntroScreen && music.TrackLoaded) { - Debug.Assert(music.CurrentTrack != null); - if (!music.CurrentTrack.IsRunning) { music.CurrentTrack.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * music.CurrentTrack.Length); diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index f2b9388fdc..af7579aafb 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -170,10 +170,7 @@ namespace osu.Game.Tests.Visual rulesetDependencies?.Dispose(); if (MusicController?.TrackLoaded == true) - { - Debug.Assert(MusicController.CurrentTrack != null); MusicController.CurrentTrack.Stop(); - } if (contextFactory.IsValueCreated) contextFactory.Value.ResetDatabase(); diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs index 54458716b1..027259d4f0 100644 --- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs +++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs @@ -16,10 +16,7 @@ namespace osu.Game.Tests.Visual // note that this will override any mod rate application if (MusicController.TrackLoaded) - { - Debug.Assert(MusicController.CurrentTrack != null); MusicController.CurrentTrack.Tempo.Value = Clock.Rate; - } } } } From 5002d69f6973ab9753997dd28255c90a90de270a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 20:51:56 +0900 Subject: [PATCH 2587/6909] Update inspections --- .../TestSceneHoldNoteInput.cs | 2 +- .../TestSceneOutOfOrderHits.cs | 2 +- .../TestSceneSliderInput.cs | 2 +- .../Skins/TestSceneBeatmapSkinResources.cs | 2 +- .../Visual/Editing/TimelineTestScene.cs | 1 - .../Visual/Gameplay/TestScenePause.cs | 2 +- .../Visual/Gameplay/TestScenePlayerLoader.cs | 4 +-- .../Visual/Gameplay/TestSceneStoryboard.cs | 4 +-- .../Visual/Menus/TestSceneIntroWelcome.cs | 2 +- .../Navigation/TestSceneScreenNavigation.cs | 9 +++--- .../TestSceneBeatSyncedContainer.cs | 1 - .../TestSceneNowPlayingOverlay.cs | 4 +-- osu.Game/Audio/PreviewTrackManager.cs | 2 +- osu.Game/Overlays/Music/PlaylistOverlay.cs | 6 ++-- osu.Game/Overlays/MusicController.cs | 31 +++++++------------ .../Edit/Components/PlaybackControl.cs | 4 +-- .../Timelines/Summary/Parts/TimelinePart.cs | 1 - .../Timeline/TimelineTickDisplay.cs | 1 - osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Menu/LogoVisualisation.cs | 1 - osu.Game/Screens/Menu/MainMenu.cs | 1 - osu.Game/Screens/Menu/OsuLogo.cs | 2 +- .../Multi/Match/Components/ReadyButton.cs | 2 +- osu.Game/Screens/Multi/Multiplayer.cs | 16 +++------- osu.Game/Tests/Visual/OsuTestScene.cs | 1 - .../Visual/RateAdjustedBeatmapTestScene.cs | 2 -- 26 files changed, 42 insertions(+), 65 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 98669efb10..19b69bac6d 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -343,7 +343,7 @@ namespace osu.Game.Rulesets.Mania.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack?.CurrentTime == 0); + AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index e5be778527..744ad46c28 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -385,7 +385,7 @@ namespace osu.Game.Rulesets.Osu.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack?.CurrentTime == 0); + AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index c9d13d3976..1690f648f9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -366,7 +366,7 @@ namespace osu.Game.Rulesets.Osu.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack?.CurrentTime == 0); + AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index 08a4e27ff7..2866692be4 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -31,6 +31,6 @@ namespace osu.Game.Tests.Skins public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo("sample")) != null); [Test] - public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => MusicController.CurrentTrack?.IsDummyDevice == false); + public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => MusicController.CurrentTrack.IsDummyDevice == false); } } diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 347b5757c8..4988a09650 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index e500b451f0..e7dd586f4e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -288,7 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void confirmNoTrackAdjustments() { - AddAssert("track has no adjustments", () => MusicController.CurrentTrack?.AggregateFrequency.Value == 1); + AddAssert("track has no adjustments", () => MusicController.CurrentTrack.AggregateFrequency.Value == 1); } private void restart() => AddStep("restart", () => Player.Restart()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index c72ab7d3d1..c4882046de 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -77,12 +77,12 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddAssert("mod rate applied", () => MusicController.CurrentTrack?.Rate != 1); + AddAssert("mod rate applied", () => MusicController.CurrentTrack.Rate != 1); AddStep("exit loader", () => loader.Exit()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); AddAssert("player did not load", () => !player.IsLoaded); AddUntilStep("player disposed", () => loader.DisposalTask?.IsCompleted == true); - AddAssert("mod rate still applied", () => MusicController.CurrentTrack?.Rate != 1); + AddAssert("mod rate still applied", () => MusicController.CurrentTrack.Rate != 1); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index c7a012a03f..3d2dd8a0c5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -87,9 +87,9 @@ namespace osu.Game.Tests.Visual.Gameplay private void restart() { - MusicController.CurrentTrack?.Reset(); + MusicController.CurrentTrack.Reset(); loadStoryboard(Beatmap.Value); - MusicController.CurrentTrack?.Start(); + MusicController.CurrentTrack.Start(); } private void loadStoryboard(WorkingBeatmap working) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index a88704c831..29be250b12 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Menus { AddUntilStep("wait for load", () => MusicController.TrackLoaded); - AddAssert("check if menu music loops", () => MusicController.CurrentTrack?.Looping == true); + AddAssert("check if menu music loops", () => MusicController.CurrentTrack.Looping); } } } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index e5d862cfc7..d2c71c1d17 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -4,7 +4,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; @@ -62,12 +61,12 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for fail", () => player.HasFailed); AddUntilStep("wait for track stop", () => !MusicController.IsPlaying); - AddAssert("Ensure time before preview point", () => MusicController.CurrentTrack?.CurrentTime < beatmap().Metadata.PreviewTime); + AddAssert("Ensure time before preview point", () => MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime); pushEscape(); AddUntilStep("wait for track playing", () => MusicController.IsPlaying); - AddAssert("Ensure time wasn't reset to preview point", () => MusicController.CurrentTrack?.CurrentTime < beatmap().Metadata.PreviewTime); + AddAssert("Ensure time wasn't reset to preview point", () => MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime); } [Test] @@ -77,11 +76,11 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => songSelect = new TestSongSelect()); - AddUntilStep("wait for no track", () => MusicController.CurrentTrack?.IsDummyDevice == true); + AddUntilStep("wait for no track", () => MusicController.CurrentTrack.IsDummyDevice); AddStep("return to menu", () => songSelect.Exit()); - AddUntilStep("wait for track", () => MusicController.CurrentTrack?.IsDummyDevice == false && MusicController.IsPlaying); + AddUntilStep("wait for track", () => MusicController.CurrentTrack.IsDummyDevice == false && MusicController.IsPlaying); } [Test] diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 3cccfa992e..127915c6c6 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -8,7 +8,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index 0161ec0c56..cadecbbef0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -80,12 +80,12 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Store track", () => currentBeatmap = Beatmap.Value); AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000)); - AddUntilStep(@"Wait for current time to update", () => musicController.CurrentTrack?.CurrentTime > 5000); + AddUntilStep(@"Wait for current time to update", () => musicController.CurrentTrack.CurrentTime > 5000); AddStep(@"Set previous", () => musicController.PreviousTrack()); AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value); - AddUntilStep("Wait for current time to update", () => musicController.CurrentTrack?.CurrentTime < 5000); + AddUntilStep("Wait for current time to update", () => musicController.CurrentTrack.CurrentTime < 5000); AddStep(@"Set previous", () => musicController.PreviousTrack()); AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value); diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index 862be41c1a..1c68ce71d4 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -48,7 +48,7 @@ namespace osu.Game.Audio track.Started += () => Schedule(() => { - CurrentTrack?.Stop(); + CurrentTrack.Stop(); CurrentTrack = track; audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable); }); diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index b9a58c37cb..7471e31923 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Music if (toSelect != null) { beatmap.Value = beatmaps.GetWorkingBeatmap(toSelect); - musicController.CurrentTrack?.Restart(); + musicController.CurrentTrack.Restart(); } }; } @@ -119,12 +119,12 @@ namespace osu.Game.Overlays.Music { if (set.ID == (beatmap.Value?.BeatmapSetInfo?.ID ?? -1)) { - musicController.CurrentTrack?.Seek(0); + musicController.CurrentTrack.Seek(0); return; } beatmap.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First()); - musicController.CurrentTrack?.Restart(); + musicController.CurrentTrack.Restart(); } } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 7e3bb1ce89..3e93ae2ccd 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -64,6 +64,7 @@ namespace osu.Game.Overlays [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } + [NotNull] public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000)); private IBindable> managerUpdated; @@ -102,12 +103,12 @@ namespace osu.Game.Overlays /// /// Returns whether the beatmap track is playing. /// - public bool IsPlaying => CurrentTrack?.IsRunning ?? false; + public bool IsPlaying => CurrentTrack.IsRunning; /// /// Returns whether the beatmap track is loaded. /// - public bool TrackLoaded => CurrentTrack?.IsLoaded == true; + public bool TrackLoaded => CurrentTrack.IsLoaded; private void beatmapUpdated(ValueChangedEvent> weakSet) { @@ -140,7 +141,7 @@ namespace osu.Game.Overlays seekDelegate = Schedule(() => { if (!beatmap.Disabled) - CurrentTrack?.Seek(position); + CurrentTrack.Seek(position); }); } @@ -152,7 +153,7 @@ namespace osu.Game.Overlays { if (IsUserPaused) return; - if (CurrentTrack == null || CurrentTrack.IsDummyDevice) + if (CurrentTrack.IsDummyDevice) { if (beatmap.Disabled) return; @@ -173,9 +174,6 @@ namespace osu.Game.Overlays { IsUserPaused = false; - if (CurrentTrack == null) - return false; - if (restart) CurrentTrack.Restart(); else if (!IsPlaying) @@ -190,7 +188,7 @@ namespace osu.Game.Overlays public void Stop() { IsUserPaused = true; - if (CurrentTrack?.IsRunning == true) + if (CurrentTrack.IsRunning) CurrentTrack.Stop(); } @@ -200,7 +198,7 @@ namespace osu.Game.Overlays /// Whether the operation was successful. public bool TogglePause() { - if (CurrentTrack?.IsRunning == true) + if (CurrentTrack.IsRunning) Stop(); else Play(); @@ -222,7 +220,7 @@ namespace osu.Game.Overlays if (beatmap.Disabled) return PreviousTrackResult.None; - var currentTrackPosition = CurrentTrack?.CurrentTime; + var currentTrackPosition = CurrentTrack.CurrentTime; if (currentTrackPosition >= restart_cutoff_point) { @@ -276,7 +274,7 @@ namespace osu.Game.Overlays { // if not scheduled, the previously track will be stopped one frame later (see ScheduleAfterChildren logic in GameBase). // we probably want to move this to a central method for switching to a new working beatmap in the future. - Schedule(() => CurrentTrack?.Restart()); + Schedule(() => CurrentTrack.Restart()); } private WorkingBeatmap current; @@ -310,7 +308,7 @@ namespace osu.Game.Overlays current = beatmap.NewValue; - if (CurrentTrack == null || !beatmap.OldValue.BeatmapInfo.AudioEquals(current?.BeatmapInfo)) + if (CurrentTrack.IsDummyDevice || !beatmap.OldValue.BeatmapInfo.AudioEquals(current?.BeatmapInfo)) changeTrack(); TrackChanged?.Invoke(current, direction); @@ -322,7 +320,7 @@ namespace osu.Game.Overlays private void changeTrack() { - CurrentTrack?.Expire(); + CurrentTrack.Expire(); CurrentTrack = null; if (current != null) @@ -340,8 +338,6 @@ namespace osu.Game.Overlays if (current != workingBeatmap) return; - Debug.Assert(CurrentTrack != null); - if (!CurrentTrack.Looping && !beatmap.Disabled) NextTrack(); } @@ -366,9 +362,6 @@ namespace osu.Game.Overlays public void ResetTrackAdjustments() { - if (CurrentTrack == null) - return; - CurrentTrack.ResetSpeedAdjustments(); if (allowRateAdjustments) diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 412efe266c..5bafc120af 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -66,12 +66,12 @@ namespace osu.Game.Screens.Edit.Components } }; - musicController.CurrentTrack?.AddAdjustment(AdjustableProperty.Tempo, tempo); + musicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, tempo); } protected override void Dispose(bool isDisposing) { - musicController?.CurrentTrack?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); + musicController?.CurrentTrack.RemoveAdjustment(AdjustableProperty.Tempo, tempo); base.Dispose(isDisposing); } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 7085c8b020..c8a470c58a 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osuTK; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 1ce33f221a..cb122c590e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -3,7 +3,6 @@ using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Overlays; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 79b13a7eac..9f2009b415 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.Edit // Todo: should probably be done at a DrawableRuleset level to share logic with Player. var sourceClock = (IAdjustableClock)musicController.CurrentTrack ?? new StopwatchClock(); - clock = new EditorClock(Beatmap.Value, musicController.CurrentTrack?.Length ?? 0, beatDivisor) { IsCoupled = false }; + clock = new EditorClock(Beatmap.Value, musicController.CurrentTrack.Length, beatDivisor) { IsCoupled = false }; clock.ChangeSource(sourceClock); dependencies.CacheAs(clock); diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 974b704200..4d95ee9b7b 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -19,7 +19,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Utils; using osu.Game.Overlays; diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 518277bce3..8837a49772 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; using System.Linq; using osuTK; using osuTK.Graphics; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index f028f9b229..4515ee8ed0 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -330,7 +330,7 @@ namespace osu.Game.Screens.Menu const float velocity_adjust_cutoff = 0.98f; const float paused_velocity = 0.5f; - if (musicController.CurrentTrack?.IsRunning == true) + if (musicController.CurrentTrack.IsRunning) { var maxAmplitude = lastBeatIndex >= 0 ? musicController.CurrentTrack.CurrentAmplitudes.Maximum : 0; logoAmplitudeContainer.Scale = new Vector2((float)Interpolation.Damp(logoAmplitudeContainer.Scale.X, 1 - Math.Max(0, maxAmplitude - scale_adjust_cutoff) * 0.04f, 0.9f, Time.Elapsed)); diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs index c7dc20ff23..384d3bd5a5 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Multi.Match.Components return; } - bool hasEnoughTime = musicController.CurrentTrack != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(musicController.CurrentTrack.Length) < endDate.Value; + bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(musicController.CurrentTrack.Length) < endDate.Value; Enabled.Value = hasBeatmap && hasEnoughTime; } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index e068899c7b..1a39d80f8d 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -343,13 +343,10 @@ namespace osu.Game.Screens.Multi { if (screenStack.CurrentScreen is MatchSubScreen) { - if (musicController.CurrentTrack != null) - { - musicController.CurrentTrack.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - musicController.CurrentTrack.Looping = true; + musicController.CurrentTrack.RestartPoint = Beatmap.Value.Metadata.PreviewTime; + musicController.CurrentTrack.Looping = true; - musicController.EnsurePlayingSomething(); - } + musicController.EnsurePlayingSomething(); } else { @@ -359,11 +356,8 @@ namespace osu.Game.Screens.Multi private void cancelLooping() { - if (musicController.CurrentTrack != null) - { - musicController.CurrentTrack.Looping = false; - musicController.CurrentTrack.RestartPoint = 0; - } + musicController.CurrentTrack.Looping = false; + musicController.CurrentTrack.RestartPoint = 0; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index af7579aafb..b0d15bf442 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.IO; using System.Linq; using System.Threading.Tasks; diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs index 027259d4f0..7651285970 100644 --- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs +++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.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.Diagnostics; - namespace osu.Game.Tests.Visual { /// From 028040344a9c2bfcc1cd25d3efad2e8dcf651207 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 21:07:59 +0900 Subject: [PATCH 2588/6909] Fix test scene using local beatmap --- osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index 2866692be4..03faee9ad2 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -18,17 +18,15 @@ namespace osu.Game.Tests.Skins [Resolved] private BeatmapManager beatmaps { get; set; } - private WorkingBeatmap beatmap; - [BackgroundDependencyLoader] private void load() { var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result; - beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); + Beatmap.Value = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); } [Test] - public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo("sample")) != null); + public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => Beatmap.Value.Skin.GetSample(new SampleInfo("sample")) != null); [Test] public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => MusicController.CurrentTrack.IsDummyDevice == false); From 961c6dab541c379e30cd4afe837f5bbfb265096b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 21:08:03 +0900 Subject: [PATCH 2589/6909] Fix more inspections --- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Tests/Visual/EditorClockTestScene.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 9f2009b415..1a7d76ba8f 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -83,7 +83,7 @@ namespace osu.Game.Screens.Edit beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - var sourceClock = (IAdjustableClock)musicController.CurrentTrack ?? new StopwatchClock(); + var sourceClock = (IAdjustableClock)musicController.CurrentTrack; clock = new EditorClock(Beatmap.Value, musicController.CurrentTrack.Length, beatDivisor) { IsCoupled = false }; clock.ChangeSource(sourceClock); diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 780b4f1b3a..1009151ac4 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual private void beatmapChanged(ValueChangedEvent e) { Clock.ControlPointInfo = e.NewValue.Beatmap.ControlPointInfo; - Clock.ChangeSource((IAdjustableClock)MusicController.CurrentTrack ?? new StopwatchClock()); + Clock.ChangeSource((IAdjustableClock)MusicController.CurrentTrack); Clock.ProcessFrame(); } From b08ebe6f81f9d596920ba602e04a1df9cb90798a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 21:14:45 +0900 Subject: [PATCH 2590/6909] More inspections (rider is broken) --- osu.Game/Screens/Edit/Editor.cs | 4 +--- osu.Game/Tests/Visual/EditorClockTestScene.cs | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 1a7d76ba8f..78fa6c74b0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -14,7 +14,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Platform; -using osu.Framework.Timing; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; @@ -83,9 +82,8 @@ namespace osu.Game.Screens.Edit beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - var sourceClock = (IAdjustableClock)musicController.CurrentTrack; clock = new EditorClock(Beatmap.Value, musicController.CurrentTrack.Length, beatDivisor) { IsCoupled = false }; - clock.ChangeSource(sourceClock); + clock.ChangeSource(musicController.CurrentTrack); dependencies.CacheAs(clock); AddInternal(clock); diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 1009151ac4..59c9329d37 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Input.Events; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Screens.Edit; @@ -44,7 +43,7 @@ namespace osu.Game.Tests.Visual private void beatmapChanged(ValueChangedEvent e) { Clock.ControlPointInfo = e.NewValue.Beatmap.ControlPointInfo; - Clock.ChangeSource((IAdjustableClock)MusicController.CurrentTrack); + Clock.ChangeSource(MusicController.CurrentTrack); Clock.ProcessFrame(); } From 08820c62ec6ab6121b60c59ad8e89f3dfbcef3f5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 21:36:02 +0900 Subject: [PATCH 2591/6909] Add back removed nullcheck --- osu.Game/Audio/PreviewTrackManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index 1c68ce71d4..862be41c1a 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -48,7 +48,7 @@ namespace osu.Game.Audio track.Started += () => Schedule(() => { - CurrentTrack.Stop(); + CurrentTrack?.Stop(); CurrentTrack = track; audio.Tracks.AddAdjustment(AdjustableProperty.Volume, muteBindable); }); From b6fb7a0d39e6a5667be3fa45d90a3f2a5072d5ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 22:05:58 +0900 Subject: [PATCH 2592/6909] Fix possibly setting null track --- .../Visual/Online/TestSceneNowPlayingCommand.cs | 2 +- osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 3 ++- osu.Game/Overlays/MusicController.cs | 8 +++----- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 103308d34d..9662bd65b4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby()); - AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(null, null) + AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null) { BeatmapInfo = { OnlineBeatmapID = hasOnlineId ? 1234 : (int?)null } }); diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index 8080e94075..ca801cf745 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; @@ -19,7 +20,7 @@ namespace osu.Game.Beatmaps { private readonly TextureStore textures; - public DummyWorkingBeatmap(AudioManager audio, TextureStore textures) + public DummyWorkingBeatmap([NotNull] AudioManager audio, TextureStore textures) : base(new BeatmapInfo { Metadata = new BeatmapMetadata diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 3e93ae2ccd..2aed46a1d0 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -321,15 +321,13 @@ namespace osu.Game.Overlays private void changeTrack() { CurrentTrack.Expire(); - CurrentTrack = null; + CurrentTrack = new DrawableTrack(new TrackVirtual(1000)); if (current != null) - { CurrentTrack = new DrawableTrack(current.GetRealTrack()); - CurrentTrack.Completed += () => onTrackCompleted(current); - AddInternal(CurrentTrack); - } + CurrentTrack.Completed += () => onTrackCompleted(current); + AddInternal(CurrentTrack); } private void onTrackCompleted(WorkingBeatmap workingBeatmap) From d1765c8a45e3940974c35ec2c75366c14a96a4e1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 22:06:04 +0900 Subject: [PATCH 2593/6909] Fix using the wrong music controller instance --- .../Navigation/TestSceneScreenNavigation.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index d2c71c1d17..0f06010a6a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -60,13 +60,13 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); AddUntilStep("wait for fail", () => player.HasFailed); - AddUntilStep("wait for track stop", () => !MusicController.IsPlaying); - AddAssert("Ensure time before preview point", () => MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime); + AddUntilStep("wait for track stop", () => !Game.MusicController.IsPlaying); + AddAssert("Ensure time before preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime); pushEscape(); - AddUntilStep("wait for track playing", () => MusicController.IsPlaying); - AddAssert("Ensure time wasn't reset to preview point", () => MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime); + AddUntilStep("wait for track playing", () => Game.MusicController.IsPlaying); + AddAssert("Ensure time wasn't reset to preview point", () => Game.MusicController.CurrentTrack.CurrentTime < beatmap().Metadata.PreviewTime); } [Test] @@ -76,11 +76,11 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => songSelect = new TestSongSelect()); - AddUntilStep("wait for no track", () => MusicController.CurrentTrack.IsDummyDevice); + AddUntilStep("wait for no track", () => Game.MusicController.CurrentTrack.IsDummyDevice); AddStep("return to menu", () => songSelect.Exit()); - AddUntilStep("wait for track", () => MusicController.CurrentTrack.IsDummyDevice == false && MusicController.IsPlaying); + AddUntilStep("wait for track", () => Game.MusicController.CurrentTrack.IsDummyDevice == false && Game.MusicController.IsPlaying); } [Test] @@ -135,12 +135,12 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("Wait for music controller", () => Game.MusicController.IsLoaded); AddStep("Seek close to end", () => { - Game.MusicController.SeekTo(MusicController.CurrentTrack.Length - 1000); - MusicController.CurrentTrack.Completed += () => trackCompleted = true; + Game.MusicController.SeekTo(Game.MusicController.CurrentTrack.Length - 1000); + Game.MusicController.CurrentTrack.Completed += () => trackCompleted = true; }); AddUntilStep("Track was completed", () => trackCompleted); - AddUntilStep("Track was restarted", () => MusicController.IsPlaying); + AddUntilStep("Track was restarted", () => Game.MusicController.IsPlaying); } private void pushEscape() => From e87f50f74f28ca8d5b5c6a0189b3ec2be21a1360 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 22:31:41 +0900 Subject: [PATCH 2594/6909] Rename method --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 2 +- osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs | 2 +- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs | 2 +- osu.Game.Tests/WaveformTestBeatmap.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/IWorkingBeatmap.cs | 5 +++++ osu.Game/Beatmaps/WorkingBeatmap.cs | 4 ++-- osu.Game/Overlays/MusicController.cs | 2 +- osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs | 2 +- osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 16 files changed, 21 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 30331e98d2..4a11e1785b 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -106,7 +106,7 @@ namespace osu.Game.Tests.Beatmaps.Formats protected override Texture GetBackground() => throw new NotImplementedException(); - protected override Track GetTrack() => throw new NotImplementedException(); + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); } } } diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs index 40f6cecd9a..bb60ae73db 100644 --- a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Gameplay AddStep("create container", () => { var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - Add(gcc = new GameplayClockContainer(working.GetRealTrack(), working, Array.Empty(), 0)); + Add(gcc = new GameplayClockContainer(working.GetTrack(), working, Array.Empty(), 0)); }); AddStep("start track", () => gcc.Start()); diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 720436fae4..360e7eccdc 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Gameplay { var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - Add(gameplayContainer = new GameplayClockContainer(working.GetRealTrack(), working, Array.Empty(), 0)); + Add(gameplayContainer = new GameplayClockContainer(working.GetTrack(), working, Array.Empty(), 0)); gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 68110d759c..58fd760fc3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); - Child = gameplayClockContainer = new GameplayClockContainer(working.GetRealTrack(), working, Array.Empty(), 0) + Child = gameplayClockContainer = new GameplayClockContainer(working.GetTrack(), working, Array.Empty(), 0) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index 90c91eb007..7dc5ce1d7f 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests protected override Waveform GetWaveform() => new Waveform(trackStore.GetStream(firstAudioFile)); - protected override Track GetTrack() => trackStore.Get(firstAudioFile); + protected override Track GetBeatmapTrack() => trackStore.Get(firstAudioFile); private string firstAudioFile { diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index f22f41531a..e001185da9 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -476,7 +476,7 @@ namespace osu.Game.Beatmaps protected override IBeatmap GetBeatmap() => beatmap; protected override Texture GetBackground() => null; - protected override Track GetTrack() => null; + protected override Track GetBeatmapTrack() => null; } } diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index a54d46c1b1..f1289cd3aa 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -70,7 +70,7 @@ namespace osu.Game.Beatmaps } } - protected override Track GetTrack() + protected override Track GetBeatmapTrack() { try { diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index ca801cf745..af2a2ac250 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -45,7 +45,7 @@ namespace osu.Game.Beatmaps protected override Texture GetBackground() => textures?.Get(@"Backgrounds/bg4"); - protected override Track GetTrack() => GetVirtualTrack(); + protected override Track GetBeatmapTrack() => GetVirtualTrack(); private class DummyRulesetInfo : RulesetInfo { diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 086b7502a2..e020625b99 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -54,5 +54,10 @@ namespace osu.Game.Beatmaps /// The converted . /// If could not be converted to . IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null); + + /// + /// Retrieves the which this provides. + /// + Track GetTrack(); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 171201ca68..af6a67ad3f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -249,9 +249,9 @@ namespace osu.Game.Beatmaps protected abstract Texture GetBackground(); private readonly RecyclableLazy background; - public Track GetRealTrack() => GetTrack() ?? GetVirtualTrack(1000); + public Track GetTrack() => GetBeatmapTrack() ?? GetVirtualTrack(1000); - protected abstract Track GetTrack(); + protected abstract Track GetBeatmapTrack(); public bool WaveformLoaded => waveform.IsResultAvailable; public Waveform Waveform => waveform.Value; diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 2aed46a1d0..cf420c3b91 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -324,7 +324,7 @@ namespace osu.Game.Overlays CurrentTrack = new DrawableTrack(new TrackVirtual(1000)); if (current != null) - CurrentTrack = new DrawableTrack(current.GetRealTrack()); + CurrentTrack = new DrawableTrack(current.GetTrack()); CurrentTrack.Completed += () => onTrackCompleted(current); AddInternal(CurrentTrack); diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index fc3dd4c105..57b7ce6940 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit protected override Texture GetBackground() => throw new NotImplementedException(); - protected override Track GetTrack() => throw new NotImplementedException(); + protected override Track GetBeatmapTrack() => throw new NotImplementedException(); } } } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 7e327261ab..6e85abf7dc 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Menu if (setInfo != null) { initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - UsingThemedIntro = initialBeatmap.GetRealTrack().IsDummyDevice == false; + UsingThemedIntro = initialBeatmap.GetTrack().IsDummyDevice == false; } return UsingThemedIntro; diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index 6ada632850..e492069c5e 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -208,7 +208,7 @@ namespace osu.Game.Tests.Beatmaps protected override Texture GetBackground() => throw new NotImplementedException(); - protected override Track GetTrack() => throw new NotImplementedException(); + protected override Track GetBeatmapTrack() => 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 ee04142035..d091da3206 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -35,6 +35,6 @@ namespace osu.Game.Tests.Beatmaps protected override Texture GetBackground() => null; - protected override Track GetTrack() => null; + protected override Track GetBeatmapTrack() => null; } } diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index b0d15bf442..756074c0b3 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -223,7 +223,7 @@ namespace osu.Game.Tests.Visual store?.Dispose(); } - protected override Track GetTrack() => track; + protected override Track GetBeatmapTrack() => track; public class TrackVirtualStore : AudioCollectionManager, ITrackStore { From b8373e89b7e30e0370170f10bf16c1d7bb36b835 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 23:05:59 +0900 Subject: [PATCH 2595/6909] Move beatmap bind to BDL load() --- osu.Game/Overlays/MusicController.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index cf420c3b91..26df48171e 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -79,12 +79,9 @@ namespace osu.Game.Overlays managerRemoved.BindValueChanged(beatmapRemoved); beatmapSets.AddRange(beatmaps.GetAllUsableBeatmapSets(IncludedDetails.Minimal, true).OrderBy(_ => RNG.Next())); - } - - protected override void LoadComplete() - { - base.LoadComplete(); + // Todo: These binds really shouldn't be here, but are unlikely to cause any issues for now. + // They are placed here for now since some tests rely on setting the beatmap _and_ their hierarchies inside their load(), which runs before the MusicController's load(). beatmap.BindValueChanged(beatmapChanged, true); mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } From 2351701ade19e518b2d07f308af5237df2e7eedd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 7 Aug 2020 23:08:51 +0900 Subject: [PATCH 2596/6909] Fix test not having a long enough track --- .../UserInterface/TestSceneNowPlayingOverlay.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index cadecbbef0..c14a1ddbf2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -11,6 +12,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.UserInterface { @@ -20,8 +22,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private MusicController musicController = new MusicController(); - private WorkingBeatmap currentBeatmap; - private NowPlayingOverlay nowPlayingOverlay; private RulesetStore rulesets; @@ -76,8 +76,13 @@ namespace osu.Game.Tests.Visual.UserInterface } }).Wait(), 5); - AddStep(@"Next track", () => musicController.NextTrack()); - AddStep("Store track", () => currentBeatmap = Beatmap.Value); + WorkingBeatmap currentBeatmap = null; + + AddStep("import beatmap with track", () => + { + var setWithTrack = manager.Import(TestResources.GetTestBeatmapForImport()).Result; + Beatmap.Value = currentBeatmap = manager.GetWorkingBeatmap(setWithTrack.Beatmaps.First()); + }); AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000)); AddUntilStep(@"Wait for current time to update", () => musicController.CurrentTrack.CurrentTime > 5000); From 87ce1e3558d598388c38cd078840d7148c95f0fd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 8 Aug 2020 00:58:04 +0900 Subject: [PATCH 2597/6909] Remove impossible null case (DummyWorkingBeatmap) --- osu.Game/Overlays/MusicController.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 26df48171e..c5ba82288c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -318,12 +318,9 @@ namespace osu.Game.Overlays private void changeTrack() { CurrentTrack.Expire(); - CurrentTrack = new DrawableTrack(new TrackVirtual(1000)); - - if (current != null) - CurrentTrack = new DrawableTrack(current.GetTrack()); - + CurrentTrack = new DrawableTrack(current.GetTrack()); CurrentTrack.Completed += () => onTrackCompleted(current); + AddInternal(CurrentTrack); } From 7cf225520fdfb93bffc18fc84eed087aa29d7cea Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Sat, 8 Aug 2020 02:43:39 +0200 Subject: [PATCH 2598/6909] Change from BDL to Resolved --- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index d4f27bf4aa..45c74da892 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -20,7 +20,8 @@ namespace osu.Game.Storyboards.Drawables private Sprite drawableSprite; - private TextureStore storyboardTextureStore; + [Resolved] + private TextureStore storyboardTextureStore { get; set; } private string texturePath; @@ -121,7 +122,7 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(ISkinSource skin, IBindable beatmap, TextureStore textureStore) + private void load(IBindable beatmap) { InternalChild = drawableSprite = new Sprite { @@ -129,8 +130,6 @@ namespace osu.Game.Storyboards.Drawables Origin = Anchor.Centre }; - storyboardTextureStore = textureStore; - texturePath = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; Sprite.ApplyTransforms(this); From 1090137da3d46e76f2be85d26dc14a6696393a84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Aug 2020 23:23:02 +0900 Subject: [PATCH 2599/6909] Adjust comment to read better MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Skinning/SkinnableSound.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 8c18e83e92..7ee0b474de 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -34,7 +34,7 @@ namespace osu.Game.Skinning /// Defaults to false unless . /// /// - /// Can serve as an optimisation if it is known ahead-of-time that this behaviour will not negatively affect behaviour. + /// Can serve as an optimisation if it is known ahead-of-time that this behaviour is allowed in a given use case. /// protected bool SkipPlayWhenZeroVolume => !Looping; From ffb2e56a8d31e6b62c3715380967e5b63711b672 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 8 Aug 2020 23:25:52 +0900 Subject: [PATCH 2600/6909] Reverse direction of bool to make mental parsing easier --- osu.Game/Skinning/SkinnableSound.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 7739be693d..32f49367f0 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -37,7 +37,7 @@ namespace osu.Game.Skinning /// /// Can serve as an optimisation if it is known ahead-of-time that this behaviour is allowed in a given use case. /// - protected bool SkipPlayWhenZeroVolume => !Looping; + protected bool PlayWhenZeroVolume => Looping; private readonly AudioContainer samplesContainer; @@ -98,7 +98,7 @@ namespace osu.Game.Skinning { samplesContainer.ForEach(c => { - if (!SkipPlayWhenZeroVolume || c.AggregateVolume.Value > 0) + if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) c.Play(); }); } From 9a09f97478f063384a03c0d35d23f359cc9d1353 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 8 Aug 2020 21:21:30 +0200 Subject: [PATCH 2601/6909] Extract constant to avoid double initial value spec --- osu.Game/Screens/Play/Player.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8f8128abfc..67283c843d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -50,7 +50,13 @@ namespace osu.Game.Screens.Play public override bool HideOverlaysOnEnter => true; - public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; + private const OverlayActivation initial_overlay_activation_mode = OverlayActivation.UserTriggered; + public override OverlayActivation InitialOverlayActivationMode => initial_overlay_activation_mode; + + /// + /// The current activation mode for overlays. + /// + protected readonly Bindable OverlayActivationMode = new Bindable(initial_overlay_activation_mode); /// /// Whether gameplay should pause when the game window focus is lost. @@ -90,11 +96,6 @@ namespace osu.Game.Screens.Play private SkipOverlay skipOverlay; - /// - /// The current activation mode for overlays. - /// - protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.UserTriggered); - protected ScoreProcessor ScoreProcessor { get; private set; } protected HealthProcessor HealthProcessor { get; private set; } From a72a48624d82fb1f07264403e31f8744d0ae5ef8 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 9 Aug 2020 05:16:08 +0300 Subject: [PATCH 2602/6909] Remove NewsPostDrawableDate --- .../Dashboard/Home/News/HomeNewsGroupPanel.cs | 69 +++++++++++-------- .../Dashboard/Home/News/HomeNewsPanel.cs | 34 ++++++--- .../Home/News/HomeNewsPanelFooter.cs | 6 +- .../Home/News/NewsPostDrawableDate.cs | 37 ---------- 4 files changed, 67 insertions(+), 79 deletions(-) delete mode 100644 osu.Game/Overlays/Dashboard/Home/News/NewsPostDrawableDate.cs diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs index cd1c5393c5..48ecaf57dc 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; @@ -44,41 +45,51 @@ namespace osu.Game.Overlays.Dashboard.Home.News protected override Drawable CreateContent(APINewsPost post) => new NewsTitleLink(post); - protected override NewsPostDrawableDate CreateDate(DateTimeOffset date) => new Date(date); + protected override Drawable CreateDate(DateTimeOffset date) => new Date(date); + } - private class Date : NewsPostDrawableDate + private class Date : CompositeDrawable, IHasCustomTooltip + { + public ITooltip GetCustomTooltip() => new DateTooltip(); + + public object TooltipContent => date; + + private readonly DateTimeOffset date; + + public Date(DateTimeOffset date) { - public Date(DateTimeOffset date) - : base(date) + this.date = date; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + TextFlowContainer textFlow; + + AutoSizeAxes = Axes.Both; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + InternalChild = textFlow = new TextFlowContainer(t => { - } - - protected override Drawable CreateDate(DateTimeOffset date, OverlayColourProvider colourProvider) + t.Colour = colourProvider.Light1; + }) { - var drawableDate = new TextFlowContainer(t => - { - t.Colour = colourProvider.Light1; - }) - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Vertical = 5 } - }; + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Vertical = 5 } + }; - drawableDate.AddText($"{date:dd} ", t => - { - t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); - }); + textFlow.AddText($"{date:dd}", t => + { + t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); + }); - drawableDate.AddText($"{date:MMM}", t => - { - t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular); - }); - - return drawableDate; - } + textFlow.AddText($"{date: MMM}", t => + { + t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular); + }); } } } diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs index 3548b7c88d..786c376fc9 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -84,14 +85,14 @@ namespace osu.Game.Overlays.Dashboard.Home.News private class Footer : HomeNewsPanelFooter { - protected override float BarPading => 10; + protected override float BarPadding => 10; public Footer(APINewsPost post) : base(post) { } - protected override NewsPostDrawableDate CreateDate(DateTimeOffset date) => new Date(date); + protected override Drawable CreateDate(DateTimeOffset date) => new Date(date); protected override Drawable CreateContent(APINewsPost post) => new FillFlowContainer { @@ -114,16 +115,29 @@ namespace osu.Game.Overlays.Dashboard.Home.News } } }; + } - private class Date : NewsPostDrawableDate + private class Date : CompositeDrawable, IHasCustomTooltip + { + public ITooltip GetCustomTooltip() => new DateTooltip(); + + public object TooltipContent => date; + + private readonly DateTimeOffset date; + + public Date(DateTimeOffset date) { - public Date(DateTimeOffset date) - : base(date) - { - Margin = new MarginPadding { Top = 10 }; - } + this.date = date; + } - protected override Drawable CreateDate(DateTimeOffset date, OverlayColourProvider colourProvider) => new FillFlowContainer + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + Margin = new MarginPadding { Top = 10 }; + InternalChild = new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, @@ -137,7 +151,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News Origin = Anchor.TopRight, Font = OsuFont.GetFont(weight: FontWeight.Bold), // using Bold since there is no 800 weight alternative Colour = colourProvider.Light1, - Text = $"{date: dd}" + Text = $"{date:dd}" }, new TextFlowContainer(f => { diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.cs index 591f53ac4a..3e3301b603 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News { public abstract class HomeNewsPanelFooter : CompositeDrawable { - protected virtual float BarPading { get; } = 0; + protected virtual float BarPadding { get; } = 0; private readonly APINewsPost post; @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Vertical = BarPading }, + Padding = new MarginPadding { Vertical = BarPadding }, Child = new Box { Anchor = Anchor.TopCentre, @@ -72,7 +72,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News }; } - protected abstract NewsPostDrawableDate CreateDate(DateTimeOffset date); + protected abstract Drawable CreateDate(DateTimeOffset date); protected abstract Drawable CreateContent(APINewsPost post); } diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsPostDrawableDate.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsPostDrawableDate.cs deleted file mode 100644 index 8ba58e27a7..0000000000 --- a/osu.Game/Overlays/Dashboard/Home/News/NewsPostDrawableDate.cs +++ /dev/null @@ -1,37 +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.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Game.Graphics; -using osu.Framework.Graphics; - -namespace osu.Game.Overlays.Dashboard.Home.News -{ - public abstract class NewsPostDrawableDate : CompositeDrawable, IHasCustomTooltip - { - public ITooltip GetCustomTooltip() => new DateTooltip(); - - public object TooltipContent => date; - - private readonly DateTimeOffset date; - - protected NewsPostDrawableDate(DateTimeOffset date) - { - this.date = date; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - AutoSizeAxes = Axes.Both; - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - InternalChild = CreateDate(date, colourProvider); - } - - protected abstract Drawable CreateDate(DateTimeOffset date, OverlayColourProvider colourProvider); - } -} From d8f89306917a66833db4d1587f03efb295aab3de Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 9 Aug 2020 05:28:43 +0300 Subject: [PATCH 2603/6909] Remove HomeNewsPanelFooter --- .../Dashboard/Home/News/CollapsedNewsPanel.cs | 115 ++++++++++++++++++ .../Dashboard/Home/News/HomeNewsGroupPanel.cs | 60 --------- .../Dashboard/Home/News/HomeNewsPanel.cs | 95 +++++++++------ .../Home/News/HomeNewsPanelFooter.cs | 79 ------------ 4 files changed, 174 insertions(+), 175 deletions(-) create mode 100644 osu.Game/Overlays/Dashboard/Home/News/CollapsedNewsPanel.cs delete mode 100644 osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.cs diff --git a/osu.Game/Overlays/Dashboard/Home/News/CollapsedNewsPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/CollapsedNewsPanel.cs new file mode 100644 index 0000000000..7dbc6a8f87 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/Home/News/CollapsedNewsPanel.cs @@ -0,0 +1,115 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +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; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Overlays.Dashboard.Home.News +{ + public class CollapsedNewsPanel : CompositeDrawable + { + private readonly APINewsPost post; + + public CollapsedNewsPanel(APINewsPost post) + { + this.post = post; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, size: 60), + new Dimension(GridSizeMode.Absolute, size: 20), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + new Date(post.PublishedAt), + new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight, + Width = 1, + RelativeSizeAxes = Axes.Y, + Colour = colourProvider.Light1 + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Padding = new MarginPadding { Right = 10 }, + Child = new NewsTitleLink(post) + } + } + } + }; + } + + private class Date : CompositeDrawable, IHasCustomTooltip + { + public ITooltip GetCustomTooltip() => new DateTooltip(); + + public object TooltipContent => date; + + private readonly DateTimeOffset date; + + public Date(DateTimeOffset date) + { + this.date = date; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + TextFlowContainer textFlow; + + AutoSizeAxes = Axes.Both; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + InternalChild = textFlow = new TextFlowContainer(t => + { + t.Colour = colourProvider.Light1; + }) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Margin = new MarginPadding { Vertical = 5 } + }; + + textFlow.AddText($"{date:dd}", t => + { + t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); + }); + + textFlow.AddText($"{date: MMM}", t => + { + t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular); + }); + } + } + } +} diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs index 48ecaf57dc..6007f1408b 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.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 System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Game.Graphics; using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Dashboard.Home.News @@ -35,62 +32,5 @@ namespace osu.Game.Overlays.Dashboard.Home.News Children = posts.Select(p => new CollapsedNewsPanel(p)).ToArray() }; } - - private class CollapsedNewsPanel : HomeNewsPanelFooter - { - public CollapsedNewsPanel(APINewsPost post) - : base(post) - { - } - - protected override Drawable CreateContent(APINewsPost post) => new NewsTitleLink(post); - - protected override Drawable CreateDate(DateTimeOffset date) => new Date(date); - } - - private class Date : CompositeDrawable, IHasCustomTooltip - { - public ITooltip GetCustomTooltip() => new DateTooltip(); - - public object TooltipContent => date; - - private readonly DateTimeOffset date; - - public Date(DateTimeOffset date) - { - this.date = date; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - TextFlowContainer textFlow; - - AutoSizeAxes = Axes.Both; - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; - InternalChild = textFlow = new TextFlowContainer(t => - { - t.Colour = colourProvider.Light1; - }) - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Vertical = 5 } - }; - - textFlow.AddText($"{date:dd}", t => - { - t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold); - }); - - textFlow.AddText($"{date: MMM}", t => - { - t.Font = OsuFont.GetFont(size: 14, weight: FontWeight.Regular); - }); - } - } } } diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs index 786c376fc9..ca56c33315 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; using osu.Framework.Platform; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -27,7 +28,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News } [BackgroundDependencyLoader] - private void load() + private void load(OverlayColourProvider colourProvider) { Children = new Drawable[] { @@ -39,7 +40,63 @@ namespace osu.Game.Overlays.Dashboard.Home.News Children = new Drawable[] { new ClickableNewsBackground(post), - new Footer(post) + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, size: 60), + new Dimension(GridSizeMode.Absolute, size: 20), + new Dimension() + }, + Content = new[] + { + new Drawable[] + { + new Date(post.PublishedAt), + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Vertical = 10 }, + Child = new Box + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopRight, + Width = 1, + RelativeSizeAxes = Axes.Y, + Colour = colourProvider.Light1 + } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 5, Bottom = 10 }, + Padding = new MarginPadding { Right = 10 }, + Spacing = new Vector2(0, 10), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new NewsTitleLink(post), + new TextFlowContainer(f => + { + f.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); + }) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Text = post.Preview + } + } + } + } + } + } } } }; @@ -83,40 +140,6 @@ namespace osu.Game.Overlays.Dashboard.Home.News } } - private class Footer : HomeNewsPanelFooter - { - protected override float BarPadding => 10; - - public Footer(APINewsPost post) - : base(post) - { - } - - protected override Drawable CreateDate(DateTimeOffset date) => new Date(date); - - protected override Drawable CreateContent(APINewsPost post) => new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = 5, Bottom = 10 }, - Spacing = new Vector2(0, 10), - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new NewsTitleLink(post), - new TextFlowContainer(f => - { - f.Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); - }) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Text = post.Preview - } - } - }; - } - private class Date : CompositeDrawable, IHasCustomTooltip { public ITooltip GetCustomTooltip() => new DateTooltip(); diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.cs b/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.cs deleted file mode 100644 index 3e3301b603..0000000000 --- a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanelFooter.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 System; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Online.API.Requests.Responses; - -namespace osu.Game.Overlays.Dashboard.Home.News -{ - public abstract class HomeNewsPanelFooter : CompositeDrawable - { - protected virtual float BarPadding { get; } = 0; - - private readonly APINewsPost post; - - protected HomeNewsPanelFooter(APINewsPost post) - { - this.post = post; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - InternalChild = new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - RowDimensions = new[] - { - new Dimension(GridSizeMode.AutoSize) - }, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, size: 60), - new Dimension(GridSizeMode.Absolute, size: 20), - new Dimension() - }, - Content = new[] - { - new Drawable[] - { - CreateDate(post.PublishedAt), - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Vertical = BarPadding }, - Child = new Box - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopRight, - Width = 1, - RelativeSizeAxes = Axes.Y, - Colour = colourProvider.Light1 - } - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Padding = new MarginPadding { Right = 10 }, - Child = CreateContent(post) - } - } - } - }; - } - - protected abstract Drawable CreateDate(DateTimeOffset date); - - protected abstract Drawable CreateContent(APINewsPost post); - } -} From 3a97ee4712557862fba5cecb1d625ae684d2fbee Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Sun, 9 Aug 2020 16:16:01 -0700 Subject: [PATCH 2604/6909] Context menu for duplicating multi rooms --- osu.Game/Online/Multiplayer/Room.cs | 50 +++++++++++-------- .../Multi/Lounge/Components/DrawableRoom.cs | 11 +++- .../Multi/Lounge/Components/RoomsContainer.cs | 15 ++++-- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 15 +++++- osu.Game/Screens/Multi/Multiplayer.cs | 4 +- 5 files changed, 67 insertions(+), 28 deletions(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 34cf158442..01d9446bf6 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -103,38 +103,48 @@ namespace osu.Game.Online.Multiplayer [JsonIgnore] public readonly Bindable Position = new Bindable(-1); - public void CopyFrom(Room other) + /// + /// Copies the properties from another to this room. + /// + /// The room to copy + /// Whether the copy should exclude information unique to a specific room (i.e. when duplicating a room) + public void CopyFrom(Room other, bool duplicate = false) { - RoomID.Value = other.RoomID.Value; + if (!duplicate) + { + RoomID.Value = other.RoomID.Value; + + if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) + Host.Value = other.Host.Value; + + ChannelId.Value = other.ChannelId.Value; + Status.Value = other.Status.Value; + ParticipantCount.Value = other.ParticipantCount.Value; + EndDate.Value = other.EndDate.Value; + + if (DateTimeOffset.Now >= EndDate.Value) + Status.Value = new RoomStatusEnded(); + + if (!RecentParticipants.SequenceEqual(other.RecentParticipants)) + { + RecentParticipants.Clear(); + RecentParticipants.AddRange(other.RecentParticipants); + } + + Position.Value = other.Position.Value; + } + Name.Value = other.Name.Value; - if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) - Host.Value = other.Host.Value; - - ChannelId.Value = other.ChannelId.Value; - Status.Value = other.Status.Value; Availability.Value = other.Availability.Value; Type.Value = other.Type.Value; MaxParticipants.Value = other.MaxParticipants.Value; - ParticipantCount.Value = other.ParticipantCount.Value; - EndDate.Value = other.EndDate.Value; - - if (DateTimeOffset.Now >= EndDate.Value) - Status.Value = new RoomStatusEnded(); if (!Playlist.SequenceEqual(other.Playlist)) { Playlist.Clear(); Playlist.AddRange(other.Playlist); } - - if (!RecentParticipants.SequenceEqual(other.RecentParticipants)) - { - RecentParticipants.Clear(); - RecentParticipants.AddRange(other.RecentParticipants); - } - - Position.Value = other.Position.Value; } public bool ShouldSerializeRoomID() => false; diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index 8dd1b239e8..64fbae2503 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -21,10 +21,12 @@ using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; using osuTK; using osuTK.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.UserInterface; namespace osu.Game.Screens.Multi.Lounge.Components { - public class DrawableRoom : OsuClickableContainer, IStateful, IFilterable + public class DrawableRoom : OsuClickableContainer, IStateful, IFilterable, IHasContextMenu { public const float SELECTION_BORDER_WIDTH = 4; private const float corner_radius = 5; @@ -36,6 +38,8 @@ namespace osu.Game.Screens.Multi.Lounge.Components public event Action StateChanged; + public Action DuplicateRoom; + private readonly Box selectionBox; private CachedModelDependencyContainer dependencies; @@ -232,5 +236,10 @@ namespace osu.Game.Screens.Multi.Lounge.Components Current = name; } } + + public MenuItem[] ContextMenuItems => new MenuItem[] + { + new OsuMenuItem("Duplicate", MenuItemType.Standard, () => DuplicateRoom?.Invoke(Room)) + }; } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index 447c99039a..f112dd80ee 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -16,6 +16,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.Multiplayer; using osuTK; +using osu.Game.Graphics.Cursor; namespace osu.Game.Screens.Multi.Lounge.Components { @@ -37,17 +38,24 @@ namespace osu.Game.Screens.Multi.Lounge.Components [Resolved] private IRoomManager roomManager { get; set; } + public Action DuplicateRoom; + public RoomsContainer() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = roomFlow = new FillFlowContainer + InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(2), + Child = roomFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + } }; } @@ -88,6 +96,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components { roomFlow.Add(new DrawableRoom(room) { + DuplicateRoom = DuplicateRoom, Action = () => { if (room == selectedRoom.Value) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index ff7d56a95b..5d68386398 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -62,7 +62,18 @@ namespace osu.Game.Screens.Multi.Lounge RelativeSizeAxes = Axes.Both, ScrollbarOverlapsContent = false, Padding = new MarginPadding(10), - Child = roomsContainer = new RoomsContainer { JoinRequested = joinRequested } + Child = roomsContainer = new RoomsContainer + { + JoinRequested = joinRequested, + DuplicateRoom = room => + { + Room newRoom = new Room(); + newRoom.CopyFrom(room, true); + newRoom.Name.Value = $"Copy of {room.Name.Value}"; + + Open(newRoom); + } + } }, loadingLayer = new LoadingLayer(roomsContainer), } @@ -126,7 +137,7 @@ namespace osu.Game.Screens.Multi.Lounge if (selectedRoom.Value?.RoomID.Value == null) selectedRoom.Value = new Room(); - music.EnsurePlayingSomething(); + music?.EnsurePlayingSomething(); onReturning(); } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 4912df17b1..cdaeebefb7 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Multi [Cached] private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); - [Resolved] + [Resolved(CanBeNull = true)] private MusicController music { get; set; } [Cached(Type = typeof(IRoomManager))] @@ -350,7 +350,7 @@ namespace osu.Game.Screens.Multi track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; track.Looping = true; - music.EnsurePlayingSomething(); + music?.EnsurePlayingSomething(); } } else From 78692dc684e4efa337cdbfc12b02caa2eff6a821 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 10 Aug 2020 05:21:10 +0200 Subject: [PATCH 2605/6909] Initial commit --- osu.Game/Beatmaps/BeatmapManager.cs | 6 +++- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 33 ++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b4b341634c..06acd4e9f2 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -26,6 +26,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps @@ -201,7 +202,10 @@ namespace osu.Game.Beatmaps using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmapContent).Encode(sw); + { + var skin = new LegacyBeatmapSkin(info, Files.Store, audioManager); + new LegacyBeatmapEncoder(beatmapContent, skin).Encode(sw); + } stream.Seek(0, SeekOrigin.Begin); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 57555cce90..8c96e59f30 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 osu.Game.Skinning; using osuTK; namespace osu.Game.Beatmaps.Formats @@ -22,10 +23,12 @@ namespace osu.Game.Beatmaps.Formats public const int LATEST_VERSION = 128; private readonly IBeatmap beatmap; + private readonly LegacyBeatmapSkin beatmapSkin; - public LegacyBeatmapEncoder(IBeatmap beatmap) + public LegacyBeatmapEncoder(IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin = null) { this.beatmap = beatmap; + this.beatmapSkin = beatmapSkin; if (beatmap.BeatmapInfo.RulesetID < 0 || beatmap.BeatmapInfo.RulesetID > 3) throw new ArgumentException("Only beatmaps in the osu, taiko, catch, or mania rulesets can be encoded to the legacy beatmap format.", nameof(beatmap)); @@ -53,6 +56,9 @@ namespace osu.Game.Beatmaps.Formats writer.WriteLine(); handleControlPoints(writer); + writer.WriteLine(); + handleComboColours(writer); + writer.WriteLine(); handleHitObjects(writer); } @@ -196,6 +202,31 @@ namespace osu.Game.Beatmaps.Formats } } + private void handleComboColours(TextWriter writer) + { + if (beatmapSkin == null) + return; + + var colours = beatmapSkin.Configuration.ComboColours; + + if (colours.Count == 0) + return; + + writer.WriteLine("[Colours]"); + + for (var i = 0; i < colours.Count; i++) + { + var comboColour = colours[i]; + + var r = (byte)(comboColour.R * byte.MaxValue); + var g = (byte)(comboColour.G * byte.MaxValue); + var b = (byte)(comboColour.B * byte.MaxValue); + var a = (byte)(comboColour.A * byte.MaxValue); + + writer.WriteLine($"Combo{i}: {r},{g},{b},{a}"); + } + } + private void handleHitObjects(TextWriter writer) { if (beatmap.HitObjects.Count == 0) From 1f84e541518a29d0b514f99b9ce8830b68daf68e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Aug 2020 20:16:16 +0900 Subject: [PATCH 2606/6909] Improve messaging when timeshift token retrieval fails Obviously not a final solution, but should better help self-compiling (or unofficial package) users better understand why this is happening. --- osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index da082692d7..79b4b04722 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Multi.Play { failed = true; - Logger.Error(e, "Failed to retrieve a score submission token."); + Logger.Error(e, "Failed to retrieve a score submission token.\n\nThis may happen if you are not running an official release of osu! (ie. you are self-compiling)."); Schedule(() => { From 730d13fda6f08a6976c30139ab32d908b78eadc8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 10 Aug 2020 20:48:04 +0900 Subject: [PATCH 2607/6909] Always show newly presented overlay at front This feels much better. Does not change order if the overlay to be shown is not yet completely hidden. - Closes #9815. --- osu.Game/OsuGame.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b5752214bd..623c677991 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -702,6 +702,9 @@ namespace osu.Game if (state.NewValue == Visibility.Hidden) return; singleDisplayOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); + + if (!overlay.IsPresent) + overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime); }; } From d7de8b2916af35ebd843b08c6f2a3c0d8ad788a6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 10 Aug 2020 17:17:07 +0000 Subject: [PATCH 2608/6909] Bump Microsoft.NET.Test.Sdk from 16.6.1 to 16.7.0 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.6.1 to 16.7.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.6.1...v16.7.0) 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 7c0b73e8c3..f9d56dfa78 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 972cbec4a2..ed00ed0b4c 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 d6a68abaf2..f3837ea6b1 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 ada7ac5d74..e896606ee8 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 4b0506d818..d767973528 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 f256b8e4e9..95f5deb2cc 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 61f1c4fe62d5d4c52fa98ee2895af00229c9748d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Aug 2020 19:51:00 +0200 Subject: [PATCH 2609/6909] Extract replay-transforming helper test method --- .../TestSceneSpinnerRotation.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index b46964e8b7..816c0c38d9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -121,19 +121,7 @@ namespace osu.Game.Rulesets.Osu.Tests 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); - }); - } + transformReplay(flip); addSeekStep(5000); @@ -141,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("spinner symbol direction correct", () => clockwise ? spinnerSymbol.Rotation > 0 : spinnerSymbol.Rotation < 0); } - private Replay flipReplay(Replay scoreReplay) => new Replay + private Replay flip(Replay scoreReplay) => new Replay { Frames = scoreReplay .Frames @@ -203,6 +191,18 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); } + private void transformReplay(Func replayTransformation) => AddStep("set replay", () => + { + var drawableRuleset = this.ChildrenOfType().Single(); + var score = drawableRuleset.ReplayScore; + var transformedScore = new Score + { + ScoreInfo = score.ScoreInfo, + Replay = replayTransformation.Invoke(score.Replay) + }; + drawableRuleset.SetReplayScore(transformedScore); + }); + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new Beatmap { HitObjects = new List From 052bb06c910e3618b8cc83badeb41fa0b71c7f1f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 10 Aug 2020 20:13:50 +0200 Subject: [PATCH 2610/6909] Add ability to open overlays during gameplay breaks. --- .../Visual/Gameplay/TestSceneOverlayActivation.cs | 11 +++++++++++ osu.Game/Screens/Play/Player.cs | 3 ++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 03e1337125..7fd5158515 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Game.Configuration; using osu.Game.Overlays; @@ -47,6 +48,16 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("activation mode is user triggered", () => testPlayer.OverlayActivationMode == OverlayActivation.UserTriggered); } + [Test] + public void TestGameplayOverlayActivationBreaks() + { + AddAssert("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("seek to break", () => testPlayer.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime)); + AddUntilStep("activation mode is user triggered", () => testPlayer.OverlayActivationMode == OverlayActivation.UserTriggered); + AddStep("seek to break end", () => testPlayer.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().EndTime)); + AddUntilStep("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); + } + protected override TestPlayer CreatePlayer(Ruleset ruleset) => testPlayer = new OverlayTestPlayer(); private class OverlayTestPlayer : TestPlayer diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 67283c843d..fba35af29e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -215,6 +215,7 @@ namespace osu.Game.Screens.Play gameplayOverlaysDisabled.BindValueChanged(_ => updateOverlayActivationMode()); DrawableRuleset.IsPaused.BindValueChanged(_ => updateOverlayActivationMode()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateOverlayActivationMode()); + breakTracker.IsBreakTime.BindValueChanged(_ => updateOverlayActivationMode()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); @@ -362,7 +363,7 @@ namespace osu.Game.Screens.Play private void updateOverlayActivationMode() { - bool canTriggerOverlays = DrawableRuleset.IsPaused.Value || !gameplayOverlaysDisabled.Value; + bool canTriggerOverlays = DrawableRuleset.IsPaused.Value || breakTracker.IsBreakTime.Value || !gameplayOverlaysDisabled.Value; if (DrawableRuleset.HasReplayLoaded.Value || canTriggerOverlays) OverlayActivationMode.Value = OverlayActivation.UserTriggered; From f74e162bbc90e5c32be17cc4231571a960fc482b Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 10 Aug 2020 20:27:42 +0200 Subject: [PATCH 2611/6909] Fix overlay activation mode being updated when player is not current screen. --- 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 fba35af29e..2ecddf0f23 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -363,6 +363,9 @@ namespace osu.Game.Screens.Play private void updateOverlayActivationMode() { + if (!this.IsCurrentScreen()) + return; + bool canTriggerOverlays = DrawableRuleset.IsPaused.Value || breakTracker.IsBreakTime.Value || !gameplayOverlaysDisabled.Value; if (DrawableRuleset.HasReplayLoaded.Value || canTriggerOverlays) From 5d63b5f6a5be7657a75024448b44c6ed214bf7b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Aug 2020 20:04:14 +0200 Subject: [PATCH 2612/6909] Add failing test cases --- .../TestSceneSpinnerRotation.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 816c0c38d9..b6f4efc24c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -7,6 +7,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Timing; @@ -184,6 +185,49 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("spm still valid", () => Precision.AlmostEquals(drawableSpinner.SpmCounter.SpinsPerMinute, estimatedSpm, 1.0)); } + [TestCase(0.5)] + [TestCase(2.0)] + public void TestSpinUnaffectedByClockRate(double rate) + { + double expectedProgress = 0; + double expectedSpm = 0; + + addSeekStep(1000); + AddStep("retrieve spinner state", () => + { + expectedProgress = drawableSpinner.Progress; + expectedSpm = drawableSpinner.SpmCounter.SpinsPerMinute; + }); + + addSeekStep(0); + + AddStep("adjust track rate", () => track.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate))); + // autoplay replay frames use track time; + // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time. + // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time, + // as real time is what we care about for spinners + // (so we're making the spin take 1000ms in real time *always*, regardless of the track clock's rate). + transformReplay(replay => applyRateAdjustment(replay, rate)); + + addSeekStep(1000); + AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); + AddAssert("spm almost same", () => Precision.AlmostEquals(expectedSpm, drawableSpinner.SpmCounter.SpinsPerMinute, 2.0)); + } + + private Replay applyRateAdjustment(Replay scoreReplay, double rate) => new Replay + { + Frames = scoreReplay + .Frames + .Cast() + .Select(replayFrame => + { + var adjustedTime = replayFrame.Time * rate; + return new OsuReplayFrame(adjustedTime, replayFrame.Position, replayFrame.Actions.ToArray()); + }) + .Cast() + .ToList() + }; + private void addSeekStep(double time) { AddStep($"seek to {time}", () => track.Seek(time)); From cca78235d5e9c0028852563329802f1d026da6c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 10 Aug 2020 22:17:47 +0200 Subject: [PATCH 2613/6909] Replace CumulativeRotation with RateAdjustedRotation --- .../TestSceneSpinnerRotation.cs | 12 +++++------ .../Objects/Drawables/DrawableSpinner.cs | 6 +++--- .../Drawables/Pieces/DefaultSpinnerDisc.cs | 2 +- .../Pieces/SpinnerRotationTracker.cs | 21 +++++++++++++++---- 4 files changed, 27 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index b6f4efc24c..69857f8ef9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -70,11 +70,11 @@ namespace osu.Game.Rulesets.Osu.Tests trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); }); AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); - AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100)); + AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100)); addSeekStep(0); AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance)); - AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, 0, 100)); + AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100)); } [Test] @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Osu.Tests finalSpinnerSymbolRotation = spinnerSymbol.Rotation; spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f); }); - AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.CumulativeRotation); + AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.RateAdjustedRotation); addSeekStep(2500); AddAssert("disc rotation rewound", @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation rewound", // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation / 2, 100)); + () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); addSeekStep(5000); AddAssert("is disc rotation almost same", @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("is symbol rotation almost same", () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation almost same", - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.CumulativeRotation, finalCumulativeTrackerRotation, 100)); + () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation, 100)); } [Test] @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360) * SpinnerTick.SCORE_PER_TICK; + return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * SpinnerTick.SCORE_PER_TICK; }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a2a49b5c42..f10d11827b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // these become implicitly hit. return 1; - return Math.Clamp(RotationTracker.CumulativeRotation / 360 / Spinner.SpinsRequired, 0, 1); + return Math.Clamp(RotationTracker.RateAdjustedRotation / 360 / Spinner.SpinsRequired, 0, 1); } } @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!SpmCounter.IsPresent && RotationTracker.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); - SpmCounter.SetRotation(RotationTracker.CumulativeRotation); + SpmCounter.SetRotation(RotationTracker.RateAdjustedRotation); updateBonusScore(); } @@ -245,7 +245,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (ticks.Count == 0) return; - int spins = (int)(RotationTracker.CumulativeRotation / 360); + int spins = (int)(RotationTracker.RateAdjustedRotation / 360); if (spins < wholeSpins) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index dfb692eba9..1476fe6010 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -177,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { get { - int rotations = (int)(drawableSpinner.RotationTracker.CumulativeRotation / 360); + int rotations = (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index 0cc6c842f4..f1a782cbb5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs @@ -31,17 +31,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public readonly BindableBool Complete = new BindableBool(); /// - /// The total rotation performed on the spinner disc, disregarding the spin direction. + /// The total rotation performed on the spinner disc, disregarding the spin direction, + /// adjusted for the track's playback rate. /// /// + /// /// 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). + /// + /// + /// The rotation from each frame is multiplied by the clock's current playback rate. + /// The reason this is done is to ensure that spinners give the same score and require the same number of spins + /// regardless of whether speed-modifying mods are applied. + /// /// /// - /// If the spinner is spun 360 degrees clockwise and then 360 degrees counter-clockwise, + /// Assuming no speed-modifying mods are active, + /// 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 ). + /// If Double Time is active instead (with a speed multiplier of 1.5x), + /// in the same scenario the property will return 720 * 1.5 = 1080. /// - public float CumulativeRotation { get; private set; } + public float RateAdjustedRotation { get; private set; } /// /// Whether the spinning is spinning at a reasonable speed to be considered visually spinning. @@ -113,7 +124,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } currentRotation += angle; - CumulativeRotation += Math.Abs(angle) * Math.Sign(Clock.ElapsedFrameTime); + // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback + // (see: ModTimeRamp) + RateAdjustedRotation += (float)(Math.Abs(angle) * Clock.Rate); } } } From ecb4826e1974ce4752020732d78d6631863d9e9f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Aug 2020 06:54:26 +0900 Subject: [PATCH 2614/6909] 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 e5fed09c07..a384ad4c34 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 18c3052ca3..b38ef38ec2 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 b034253d88..00ddd94d53 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From d1b106a3b557c0c8dd13746679a7e68193863eff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Aug 2020 10:59:28 +0900 Subject: [PATCH 2615/6909] Include mention of old releases in error message --- osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 79b4b04722..04da943a10 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Multi.Play { failed = true; - Logger.Error(e, "Failed to retrieve a score submission token.\n\nThis may happen if you are not running an official release of osu! (ie. you are self-compiling)."); + Logger.Error(e, "Failed to retrieve a score submission token.\n\nThis may happen if you are running an old or non-official release of osu! (ie. you are self-compiling)."); Schedule(() => { From 471ed968e3d7a81b566a137078b5e3bef9e35d10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Aug 2020 11:09:02 +0900 Subject: [PATCH 2616/6909] Fix crash when same ruleset loaded more than once If the same ruleset assembly was present more than once in the current AppDomain, the game would crash. We recently saw this in Rider EAP9. While this behaviour may change going forward, this is a good safety measure regardless. --- osu.Game/Rulesets/RulesetStore.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index dd43092c0d..5d93f5186b 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -191,6 +191,11 @@ namespace osu.Game.Rulesets if (loadedAssemblies.ContainsKey(assembly)) return; + // the same assembly may be loaded twice in the same AppDomain (currently a thing in certain Rider versions https://youtrack.jetbrains.com/issue/RIDER-48799). + // as a failsafe, also compare by FullName. + if (loadedAssemblies.Any(a => a.Key.FullName == assembly.FullName)) + return; + try { loadedAssemblies[assembly] = assembly.GetTypes().First(t => t.IsPublic && t.IsSubclassOf(typeof(Ruleset))); From 20197e276861128b96dec6fcf7f9fd148a763e8d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 Aug 2020 12:27:32 +0900 Subject: [PATCH 2617/6909] Remove locally-cached music controller --- osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 3d2dd8a0c5..c11a47d62b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -25,16 +25,12 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly Container storyboardContainer; private DrawableStoryboard storyboard; - [Cached] - private MusicController musicController = new MusicController(); - public TestSceneStoryboard() { Clock = new FramedClock(); AddRange(new Drawable[] { - musicController, new Container { RelativeSizeAxes = Axes.Both, @@ -104,7 +100,7 @@ namespace osu.Game.Tests.Visual.Gameplay storyboard.Passing = false; storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(musicController.CurrentTrack); + decoupledClock.ChangeSource(MusicController.CurrentTrack); } private void loadStoryboardNoVideo() @@ -127,7 +123,7 @@ namespace osu.Game.Tests.Visual.Gameplay storyboard = sb.CreateDrawable(Beatmap.Value); storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(musicController.CurrentTrack); + decoupledClock.ChangeSource(MusicController.CurrentTrack); } } } From b64142dff9d04deb9295eeb9c5b3e52623c63cdf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 Aug 2020 12:37:00 +0900 Subject: [PATCH 2618/6909] Fix incorrect load state being used --- osu.Game/Overlays/MusicController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index c5ba82288c..813ad26ae4 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -105,7 +105,7 @@ namespace osu.Game.Overlays /// /// Returns whether the beatmap track is loaded. /// - public bool TrackLoaded => CurrentTrack.IsLoaded; + public bool TrackLoaded => CurrentTrack.TrackLoaded; private void beatmapUpdated(ValueChangedEvent> weakSet) { From c66a14e9c5d160def0fdcdabcf39772e72cdd0dc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 Aug 2020 12:37:48 +0900 Subject: [PATCH 2619/6909] Remove beatmap from FailAnimation --- osu.Game/Screens/Play/FailAnimation.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index a7bfca612e..122ae505ca 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -11,7 +11,6 @@ using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Utils; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; @@ -42,7 +41,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(AudioManager audio, IBindable beatmap) + private void load(AudioManager audio) { failSample = audio.Samples.Get(@"Gameplay/failsound"); } From 7d35893ecd2bf8b277c2e6ac9632592dbf313f6f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 Aug 2020 12:40:58 +0900 Subject: [PATCH 2620/6909] Make MusicController non-nullable --- osu.Game/Screens/Menu/MainMenu.cs | 2 +- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 28 +++++++------------ 3 files changed, 12 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 8837a49772..470e8ca9a6 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Menu [Resolved] private GameHost host { get; set; } - [Resolved(canBeNull: true)] + [Resolved] private MusicController music { get; set; } [Resolved(canBeNull: true)] diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index ff7d56a95b..f9c5fd13a4 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Lounge [Resolved] private Bindable selectedRoom { get; set; } - [Resolved(canBeNull: true)] + [Resolved] private MusicController music { get; set; } private bool joiningRoom; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 80ed894233..ddbb021054 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -99,7 +99,7 @@ namespace osu.Game.Screens.Select private readonly Bindable decoupledRuleset = new Bindable(); - [Resolved(canBeNull: true)] + [Resolved] private MusicController music { get; set; } [BackgroundDependencyLoader(true)] @@ -561,18 +561,15 @@ namespace osu.Game.Screens.Select BeatmapDetails.Refresh(); - if (music?.CurrentTrack != null) - { - music.CurrentTrack.Looping = true; - music.ResetTrackAdjustments(); - } + music.CurrentTrack.Looping = true; + music.ResetTrackAdjustments(); if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) { updateComponentFromBeatmap(Beatmap.Value); // restart playback on returning to song select, regardless. - music?.Play(); + music.Play(); } this.FadeIn(250); @@ -589,8 +586,7 @@ namespace osu.Game.Screens.Select BeatmapOptions.Hide(); - if (music?.CurrentTrack != null) - music.CurrentTrack.Looping = false; + music.CurrentTrack.Looping = false; this.ScaleTo(1.1f, 250, Easing.InSine); @@ -611,8 +607,7 @@ namespace osu.Game.Screens.Select FilterControl.Deactivate(); - if (music?.CurrentTrack != null) - music.CurrentTrack.Looping = false; + music.CurrentTrack.Looping = false; return false; } @@ -653,8 +648,7 @@ namespace osu.Game.Screens.Select BeatmapDetails.Beatmap = beatmap; - if (music?.CurrentTrack != null) - music.CurrentTrack.Looping = true; + music.CurrentTrack.Looping = true; } private readonly WeakReference lastTrack = new WeakReference(null); @@ -665,16 +659,14 @@ namespace osu.Game.Screens.Select /// private void ensurePlayingSelected() { - ITrack track = music?.CurrentTrack; - if (track == null) - return; + ITrack track = music.CurrentTrack; bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track; track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - if (!track.IsRunning && (music?.IsUserPaused != true || isNewTrack)) - music?.Play(true); + if (!track.IsRunning && (music.IsUserPaused != true || isNewTrack)) + music.Play(true); lastTrack.SetTarget(track); } From 322d08af1bfe25f45f90fcd8bc395219ce85c108 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 Aug 2020 13:11:59 +0900 Subject: [PATCH 2621/6909] Remove more local music controller caches --- osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs | 5 ----- .../Visual/UserInterface/TestSceneBeatSyncedContainer.cs | 4 ---- 2 files changed, 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs index d7f23f5cc0..4b22af38c5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneSongTicker.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.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays; @@ -11,14 +10,10 @@ namespace osu.Game.Tests.Visual.Menus { public class TestSceneSongTicker : OsuTestScene { - [Cached] - private MusicController musicController = new MusicController(); - public TestSceneSongTicker() { AddRange(new Drawable[] { - musicController, new SongTicker { Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs index 127915c6c6..82b7e65c4f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatSyncedContainer.cs @@ -26,9 +26,6 @@ namespace osu.Game.Tests.Visual.UserInterface { private readonly NowPlayingOverlay np; - [Cached] - private MusicController musicController = new MusicController(); - public TestSceneBeatSyncedContainer() { Clock = new FramedClock(); @@ -36,7 +33,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddRange(new Drawable[] { - musicController, new BeatContainer { Anchor = Anchor.BottomCentre, From 6aafb3d2710174fe4bf302300ecf2cd1edd52988 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 Aug 2020 13:14:20 +0900 Subject: [PATCH 2622/6909] Cleanup TestSceneScreenNavigation --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 0f06010a6a..73a833c15d 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -80,7 +80,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("return to menu", () => songSelect.Exit()); - AddUntilStep("wait for track", () => Game.MusicController.CurrentTrack.IsDummyDevice == false && Game.MusicController.IsPlaying); + AddUntilStep("wait for track", () => !Game.MusicController.CurrentTrack.IsDummyDevice && Game.MusicController.IsPlaying); } [Test] From 338c01fa43657beb3f1476aab90db227605deed2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 Aug 2020 13:16:06 +0900 Subject: [PATCH 2623/6909] Remove track store reference counting, use single instance stores --- osu.Game/Beatmaps/BeatmapManager.cs | 29 ++++--------------- .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 24 +++++---------- 2 files changed, 13 insertions(+), 40 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e001185da9..6f01580998 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -63,8 +63,9 @@ namespace osu.Game.Beatmaps private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; private readonly AudioManager audioManager; - private readonly GameHost host; private readonly BeatmapOnlineLookupQueue onlineLookupQueue; + private readonly TextureStore textureStore; + private readonly ITrackStore trackStore; public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null, WorkingBeatmap defaultBeatmap = null) @@ -72,7 +73,6 @@ namespace osu.Game.Beatmaps { this.rulesets = rulesets; this.audioManager = audioManager; - this.host = host; DefaultBeatmap = defaultBeatmap; @@ -83,6 +83,9 @@ namespace osu.Game.Beatmaps beatmaps.ItemUpdated += removeWorkingCache; onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + + textureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); + trackStore = audioManager.GetTrackStore(Files.Store); } protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => @@ -219,7 +222,6 @@ namespace osu.Game.Beatmaps } private readonly WeakList workingCache = new WeakList(); - private readonly Dictionary referencedTrackStores = new Dictionary(); /// /// Retrieve a instance for the provided @@ -252,31 +254,12 @@ namespace osu.Game.Beatmaps beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; - ITrackStore trackStore = workingCache.FirstOrDefault(b => b.BeatmapInfo.AudioEquals(beatmapInfo))?.TrackStore; - TextureStore textureStore = workingCache.FirstOrDefault(b => b.BeatmapInfo.BackgroundEquals(beatmapInfo))?.TextureStore; - - textureStore ??= new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); - trackStore ??= audioManager.GetTrackStore(Files.Store); - - workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager, dereferenceTrackStore)); - referencedTrackStores[trackStore] = referencedTrackStores.GetOrDefault(trackStore) + 1; + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager)); return working; } } - private void dereferenceTrackStore(ITrackStore trackStore) - { - lock (workingCache) - { - if (--referencedTrackStores[trackStore] == 0) - { - referencedTrackStores.Remove(trackStore); - trackStore.Dispose(); - } - } - } - /// /// Perform a lookup query on available s. /// diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index f1289cd3aa..aa93833f33 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -19,21 +19,16 @@ namespace osu.Game.Beatmaps { protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap { - public readonly TextureStore TextureStore; - public readonly ITrackStore TrackStore; - private readonly IResourceStore store; - private readonly Action dereferenceAction; + private readonly TextureStore textureStore; + private readonly ITrackStore trackStore; - public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager, - Action dereferenceAction) + public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager) : base(beatmapInfo, audioManager) { this.store = store; - this.dereferenceAction = dereferenceAction; - - TextureStore = textureStore; - TrackStore = trackStore; + this.textureStore = textureStore; + this.trackStore = trackStore; } protected override IBeatmap GetBeatmap() @@ -61,7 +56,7 @@ namespace osu.Game.Beatmaps try { - return TextureStore.Get(getPathForFile(Metadata.BackgroundFile)); + return textureStore.Get(getPathForFile(Metadata.BackgroundFile)); } catch (Exception e) { @@ -74,7 +69,7 @@ namespace osu.Game.Beatmaps { try { - return TrackStore.Get(getPathForFile(Metadata.AudioFile)); + return trackStore.Get(getPathForFile(Metadata.AudioFile)); } catch (Exception e) { @@ -140,11 +135,6 @@ namespace osu.Game.Beatmaps return null; } } - - ~BeatmapManagerWorkingBeatmap() - { - dereferenceAction?.Invoke(TrackStore); - } } } } From faff0a70c49734841f6c1dd36c8df2775d1b867e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 Aug 2020 13:48:57 +0900 Subject: [PATCH 2624/6909] Privatise BMWB --- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index aa93833f33..92199789ec 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -17,7 +17,7 @@ namespace osu.Game.Beatmaps { public partial class BeatmapManager { - protected class BeatmapManagerWorkingBeatmap : WorkingBeatmap + private class BeatmapManagerWorkingBeatmap : WorkingBeatmap { private readonly IResourceStore store; private readonly TextureStore textureStore; From 031d29ac34bd6684340f51aab051b84db42c4644 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 11 Aug 2020 13:53:23 +0900 Subject: [PATCH 2625/6909] Inspect current track directly --- osu.Game/Overlays/NowPlayingOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 15b189ead6..dc52300cdb 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -234,9 +234,9 @@ namespace osu.Game.Overlays pendingBeatmapSwitch = null; } - var track = musicController.TrackLoaded ? musicController.CurrentTrack : null; + var track = musicController.CurrentTrack; - if (track?.IsDummyDevice == false) + if (track.IsDummyDevice == false) { progressBar.EndTime = track.Length; progressBar.CurrentTime = track.CurrentTime; From 8bfe6ba27c275208f665e6c125b73fade6c6fdd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 11 Aug 2020 23:04:00 +0900 Subject: [PATCH 2626/6909] Fix informational overlays not hiding each other correctly --- osu.Game/OsuGame.cs | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 623c677991..053eb01dcd 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -683,9 +683,8 @@ namespace osu.Game { overlay.State.ValueChanged += state => { - if (state.NewValue == Visibility.Hidden) return; - - informationalOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); + if (state.NewValue != Visibility.Hidden) + showOverlayAboveOthers(overlay, informationalOverlays); }; } @@ -699,12 +698,8 @@ namespace osu.Game // informational overlays should be dismissed on a show or hide of a full overlay. informationalOverlays.ForEach(o => o.Hide()); - if (state.NewValue == Visibility.Hidden) return; - - singleDisplayOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); - - if (!overlay.IsPresent) - overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime); + if (state.NewValue != Visibility.Hidden) + showOverlayAboveOthers(overlay, singleDisplayOverlays); }; } @@ -729,6 +724,15 @@ namespace osu.Game notifications.State.ValueChanged += _ => updateScreenOffset(); } + private void showOverlayAboveOthers(OverlayContainer overlay, OverlayContainer[] otherOverlays) + { + otherOverlays.Where(o => o != overlay).ForEach(o => o.Hide()); + + // show above others if not visible at all, else leave at current depth. + if (!overlay.IsPresent) + overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime); + } + public class GameIdleTracker : IdleTracker { private InputManager inputManager; From 070d71ec27fa74eec6d27fa551dd5162ab628d4e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 Aug 2020 00:48:38 +0900 Subject: [PATCH 2627/6909] More cleanups --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 4 ++-- .../Skinning/TestSceneTaikoPlayfield.cs | 2 +- osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs | 2 +- .../Visual/Gameplay/TestSceneNightcoreBeatContainer.cs | 4 ++-- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs | 3 --- osu.Game/Screens/Edit/Editor.cs | 4 ++-- osu.Game/Screens/Edit/EditorClock.cs | 4 ++-- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- 9 files changed, 12 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index ba5aef5968..6141bf062e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -175,11 +175,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private void createDrawableRuleset() { - AddUntilStep("wait for beatmap to be loaded", () => MusicController.TrackLoaded); + AddUntilStep("wait for beatmap to be loaded", () => MusicController.CurrentTrack.TrackLoaded); AddStep("create drawable ruleset", () => { - MusicController.Play(true); + MusicController.CurrentTrack.Start(); SetContents(() => { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index 5c54393fb8..a3d3bc81c4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }); - MusicController.Play(true); + MusicController.CurrentTrack.Start(); }); AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo()) diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index 03faee9ad2..49b40daf99 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -29,6 +29,6 @@ namespace osu.Game.Tests.Skins public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => Beatmap.Value.Skin.GetSample(new SampleInfo("sample")) != null); [Test] - public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => MusicController.CurrentTrack.IsDummyDevice == false); + public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => !MusicController.CurrentTrack.IsDummyDevice); } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs index ce99d85e92..53e06a632e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs @@ -19,8 +19,8 @@ namespace osu.Game.Tests.Visual.Gameplay Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - MusicController.Play(true); - MusicController.SeekTo(Beatmap.Value.Beatmap.HitObjects.First().StartTime - 1000); + MusicController.CurrentTrack.Start(); + MusicController.CurrentTrack.Seek(Beatmap.Value.Beatmap.HitObjects.First().StartTime - 1000); Add(new ModNightcore.NightcoreBeatContainer()); diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index dc52300cdb..af692486b7 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -236,7 +236,7 @@ namespace osu.Game.Overlays var track = musicController.CurrentTrack; - if (track.IsDummyDevice == false) + if (!track.IsDummyDevice) { progressBar.EndTime = track.Length; progressBar.CurrentTime = track.CurrentTime; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index d556d948f6..e1702d3eff 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -64,8 +63,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveform.Waveform = b.NewValue.Waveform; track = musicController.CurrentTrack; - Debug.Assert(track != null); - if (track.Length > 0) { MaxZoom = getZoomLevelForVisibleMilliseconds(500); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 78fa6c74b0..6722d9179c 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -82,7 +82,7 @@ namespace osu.Game.Screens.Edit beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - clock = new EditorClock(Beatmap.Value, musicController.CurrentTrack.Length, beatDivisor) { IsCoupled = false }; + clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; clock.ChangeSource(musicController.CurrentTrack); dependencies.CacheAs(clock); @@ -348,7 +348,7 @@ namespace osu.Game.Screens.Edit private void resetTrack(bool seekToStart = false) { - musicController.Stop(); + musicController.CurrentTrack.Stop(); if (seekToStart) { diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index fbfa397795..d0e265adb0 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -25,8 +25,8 @@ namespace osu.Game.Screens.Edit private readonly DecoupleableInterpolatingFramedClock underlyingClock; - public EditorClock(WorkingBeatmap beatmap, double trackLength, BindableBeatDivisor beatDivisor) - : this(beatmap.Beatmap.ControlPointInfo, trackLength, beatDivisor) + public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) + : this(beatmap.Beatmap.ControlPointInfo, beatmap.GetTrack().Length, beatDivisor) { } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 6e85abf7dc..389629445c 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Menu if (setInfo != null) { initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - UsingThemedIntro = initialBeatmap.GetTrack().IsDummyDevice == false; + UsingThemedIntro = !initialBeatmap.GetTrack().IsDummyDevice; } return UsingThemedIntro; From b66f303e71cf9083c2f764964cf3424bb4433f64 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 Aug 2020 00:48:45 +0900 Subject: [PATCH 2628/6909] Add annotation --- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index af6a67ad3f..bec2679103 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; @@ -249,6 +250,7 @@ namespace osu.Game.Beatmaps protected abstract Texture GetBackground(); private readonly RecyclableLazy background; + [NotNull] public Track GetTrack() => GetBeatmapTrack() ?? GetVirtualTrack(1000); protected abstract Track GetBeatmapTrack(); From eec94e1f535e1a5f9d3a17561bfd1ca6d2e5ccab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 Aug 2020 00:50:56 +0900 Subject: [PATCH 2629/6909] Make track not-null in GameplayClockContainer/FailAnimation --- osu.Game/Screens/Play/FailAnimation.cs | 5 ++++- osu.Game/Screens/Play/GameplayClockContainer.cs | 10 +++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 122ae505ca..6ebee209e2 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Game.Rulesets.UI; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; @@ -26,6 +27,8 @@ namespace osu.Game.Screens.Play public Action OnComplete; private readonly DrawableRuleset drawableRuleset; + + [NotNull] private readonly ITrack track; private readonly BindableDouble trackFreq = new BindableDouble(1); @@ -34,7 +37,7 @@ namespace osu.Game.Screens.Play private SampleChannel failSample; - public FailAnimation(DrawableRuleset drawableRuleset, ITrack track) + public FailAnimation(DrawableRuleset drawableRuleset, [NotNull] ITrack track) { this.drawableRuleset = drawableRuleset; this.track = track; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index c4f368e1f5..f3466a562e 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; using osu.Framework; @@ -27,6 +28,7 @@ namespace osu.Game.Screens.Play private readonly WorkingBeatmap beatmap; private readonly IReadOnlyList mods; + [NotNull] private ITrack track; public readonly BindableBool IsPaused = new BindableBool(); @@ -60,7 +62,7 @@ namespace osu.Game.Screens.Play private readonly FramedOffsetClock platformOffsetClock; - public GameplayClockContainer(ITrack track, WorkingBeatmap beatmap, IReadOnlyList mods, double gameplayStartTime) + public GameplayClockContainer([NotNull] ITrack track, WorkingBeatmap beatmap, IReadOnlyList mods, double gameplayStartTime) { this.beatmap = beatmap; this.mods = mods; @@ -196,9 +198,6 @@ namespace osu.Game.Screens.Play /// public void StopUsingBeatmapClock() { - if (track == null) - return; - removeSourceClockAdjustments(); track = new TrackVirtual(track.Length); @@ -217,8 +216,6 @@ namespace osu.Game.Screens.Play private void updateRate() { - if (track == null) return; - speedAdjustmentsApplied = true; track.ResetSpeedAdjustments(); @@ -233,7 +230,6 @@ namespace osu.Game.Screens.Play { base.Dispose(isDisposing); removeSourceClockAdjustments(); - track = null; } private void removeSourceClockAdjustments() From 688e4479506645bdacee7cdc7ae9ee9deaa5f1b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 Aug 2020 01:33:06 +0900 Subject: [PATCH 2630/6909] Fix potential hierarchy mutation from async context --- osu.Game/Overlays/MusicController.cs | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 813ad26ae4..c18b564b4f 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -317,11 +317,28 @@ namespace osu.Game.Overlays private void changeTrack() { - CurrentTrack.Expire(); - CurrentTrack = new DrawableTrack(current.GetTrack()); - CurrentTrack.Completed += () => onTrackCompleted(current); + var lastTrack = CurrentTrack; - AddInternal(CurrentTrack); + var newTrack = new DrawableTrack(current.GetTrack()); + newTrack.Completed += () => onTrackCompleted(current); + + CurrentTrack = newTrack; + + // At this point we may potentially be in an async context from tests. This is extremely dangerous but we have to make do for now. + // CurrentTrack is immediately updated above for situations where a immediate knowledge about the new track is required, + // but the mutation of the hierarchy is scheduled to avoid exceptions. + Schedule(() => + { + lastTrack.Expire(); + + if (newTrack == CurrentTrack) + AddInternal(newTrack); + else + { + // If the track has changed via changeTrack() being called multiple times in a single update, force disposal on the old track. + newTrack.Dispose(); + } + }); } private void onTrackCompleted(WorkingBeatmap workingBeatmap) From e47a1eb313f86d9c0f635e58545644139d5cbf34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 Aug 2020 01:41:21 +0900 Subject: [PATCH 2631/6909] Use adjustable ITrack --- osu.Game/Rulesets/Mods/ModDaycore.cs | 4 ++-- osu.Game/Rulesets/Mods/ModNightcore.cs | 4 ++-- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 2 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 4 ++-- osu.Game/Screens/Play/FailAnimation.cs | 4 ++-- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 ++-- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModDaycore.cs b/osu.Game/Rulesets/Mods/ModDaycore.cs index 800312d047..61ad7db706 100644 --- a/osu.Game/Rulesets/Mods/ModDaycore.cs +++ b/osu.Game/Rulesets/Mods/ModDaycore.cs @@ -30,8 +30,8 @@ namespace osu.Game.Rulesets.Mods public override void ApplyToTrack(ITrack track) { // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) - (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); - (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); + track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); + track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } } } diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 4932df08f1..4004953cd1 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -41,8 +41,8 @@ namespace osu.Game.Rulesets.Mods public override void ApplyToTrack(ITrack track) { // base.ApplyToTrack() intentionally not called (different tempo adjustment is applied) - (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); - (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); + track.AddAdjustment(AdjustableProperty.Frequency, freqAdjust); + track.AddAdjustment(AdjustableProperty.Tempo, tempoAdjust); } public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index ae7077c67b..fec21764b0 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mods public virtual void ApplyToTrack(ITrack track) { - (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); + track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } public virtual void ApplyToSample(SampleChannel sample) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index b904cf007b..20c8d0f3e7 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 as IAdjustableAudioComponent)?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); + track?.RemoveAdjustment(adjustmentForPitchSetting(adjustPitchSetting.OldValue), SpeedChange); - (track as IAdjustableAudioComponent)?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); + track?.AddAdjustment(adjustmentForPitchSetting(adjustPitchSetting.NewValue), SpeedChange); } private AdjustableProperty adjustmentForPitchSetting(bool adjustPitchSettingValue) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 6ebee209e2..dade904180 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Play Expire(); }); - (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, trackFreq); + track.AddAdjustment(AdjustableProperty.Frequency, trackFreq); applyToPlayfield(drawableRuleset.Playfield); drawableRuleset.Playfield.HitObjectContainer.FlashColour(Color4.Red, 500); @@ -108,7 +108,7 @@ namespace osu.Game.Screens.Play protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - (track as IAdjustableAudioComponent)?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); + track?.RemoveAdjustment(AdjustableProperty.Frequency, trackFreq); } } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index f3466a562e..59e26cdf55 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -219,8 +219,8 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = true; track.ResetSpeedAdjustments(); - (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); - (track as IAdjustableAudioComponent)?.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); foreach (var mod in mods.OfType()) mod.ApplyToTrack(track); From c0031955c9ed6c0b74fd655366f988b33ba34ae6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 Aug 2020 01:50:18 +0900 Subject: [PATCH 2632/6909] Update with further framework changes --- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index 86f7dbdd6f..a9ef20436f 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(new TrianglesIntroSequence(logo, background) { RelativeSizeAxes = Axes.Both, - Clock = new FramedClock(UsingThemedIntro ? (IAdjustableClock)Track : null), + Clock = new FramedClock(UsingThemedIntro ? Track : null), LoadMenu = LoadMenu }, t => { diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 59e26cdf55..f0bbcf957a 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Play Schedule(() => { - adjustableClock.ChangeSource((IAdjustableClock)track); + adjustableClock.ChangeSource(track); updateRate(); if (!IsPaused.Value) @@ -201,7 +201,7 @@ namespace osu.Game.Screens.Play removeSourceClockAdjustments(); track = new TrackVirtual(track.Length); - adjustableClock.ChangeSource((IAdjustableClock)track); + adjustableClock.ChangeSource(track); } protected override void Update() From 84655b0798d16f462ec24bb9727c91c2edcde55e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 11 Aug 2020 20:17:29 +0300 Subject: [PATCH 2633/6909] Change hover colour for news title --- osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs index da98c92bbe..d6a3a69fe0 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsTitleLink.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News } [BackgroundDependencyLoader] - private void load(GameHost host) + private void load(GameHost host, OverlayColourProvider colourProvider) { Child = new TextFlowContainer(t => { @@ -36,6 +36,8 @@ namespace osu.Game.Overlays.Dashboard.Home.News Text = post.Title }; + HoverColour = colourProvider.Light1; + TooltipText = "view in browser"; Action = () => host.OpenUrlExternally("https://osu.ppy.sh/home/news/" + post.Slug); } From b78ccf8a347ca3995712c4424f8d72f580a929de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Aug 2020 21:28:00 +0200 Subject: [PATCH 2634/6909] Rewrite Spun Out test scene --- .../Mods/TestSceneOsuModSpunOut.cs | 39 ++++++++++++ .../TestSceneSpinnerSpunOut.cs | 59 ------------------- 2 files changed, 39 insertions(+), 59 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs delete mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs new file mode 100644 index 0000000000..1b052600ca --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModSpunOut : OsuModTestScene + { + [Test] + public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData + { + Mod = new OsuModSpunOut(), + Autoplay = false, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Spinner + { + Position = new Vector2(256, 192), + StartTime = 500, + Duration = 2000 + } + } + }, + PassCondition = () => Player.ChildrenOfType().Single().Progress >= 1 + }); + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs deleted file mode 100644 index d1210db6b1..0000000000 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSpunOut.cs +++ /dev/null @@ -1,59 +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.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.Tests.Visual; - -namespace osu.Game.Rulesets.Osu.Tests -{ - [TestFixture] - public class TestSceneSpinnerSpunOut : OsuTestScene - { - [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 8fe5775ecb82be2a6d52149a4f0393150316ec58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Aug 2020 21:55:20 +0200 Subject: [PATCH 2635/6909] Allow testing mod combinations in ModTestScenes --- osu.Game/Tests/Visual/ModTestScene.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/ModTestScene.cs b/osu.Game/Tests/Visual/ModTestScene.cs index 23b5ad0bd8..a71d008eb9 100644 --- a/osu.Game/Tests/Visual/ModTestScene.cs +++ b/osu.Game/Tests/Visual/ModTestScene.cs @@ -40,8 +40,8 @@ namespace osu.Game.Tests.Visual { var mods = new List(SelectedMods.Value); - if (currentTestData.Mod != null) - mods.Add(currentTestData.Mod); + if (currentTestData.Mods != null) + mods.AddRange(currentTestData.Mods); if (currentTestData.Autoplay) mods.Add(ruleset.GetAutoplayMod()); @@ -85,9 +85,18 @@ namespace osu.Game.Tests.Visual public Func PassCondition; /// - /// The this test case tests. + /// The s this test case tests. /// - public Mod Mod; + public IReadOnlyList Mods; + + /// + /// Convenience property for setting if only + /// a single mod is to be tested. + /// + public Mod Mod + { + set => Mods = new[] { value }; + } } } } From 25f59e0489a292b825f60af57d6a8ac9df7e1692 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Aug 2020 21:55:50 +0200 Subject: [PATCH 2636/6909] Add failing test cases --- .../Mods/TestSceneOsuModSpunOut.cs | 50 ++++++++++++++----- 1 file changed, 38 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 1b052600ca..d8064d36ea 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -1,39 +1,65 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; 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 osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods { public class TestSceneOsuModSpunOut : OsuModTestScene { + protected override bool AllowFail => true; + [Test] public void TestSpinnerAutoCompleted() => CreateModTest(new ModTestData { Mod = new OsuModSpunOut(), Autoplay = false, - Beatmap = new Beatmap - { - HitObjects = new List - { - new Spinner - { - Position = new Vector2(256, 192), - StartTime = 500, - Duration = 2000 - } - } - }, + Beatmap = singleSpinnerBeatmap, PassCondition = () => Player.ChildrenOfType().Single().Progress >= 1 }); + + [TestCase(null)] + [TestCase(typeof(OsuModDoubleTime))] + [TestCase(typeof(OsuModHalfTime))] + public void TestSpinRateUnaffectedByMods(Type additionalModType) + { + var mods = new List { new OsuModSpunOut() }; + if (additionalModType != null) + mods.Add((Mod)Activator.CreateInstance(additionalModType)); + + CreateModTest(new ModTestData + { + Mods = mods, + Autoplay = false, + Beatmap = singleSpinnerBeatmap, + PassCondition = () => Precision.AlmostEquals(Player.ChildrenOfType().Single().SpinsPerMinute, 286, 1) + }); + } + + private Beatmap singleSpinnerBeatmap => new Beatmap + { + HitObjects = new List + { + new Spinner + { + Position = new Vector2(256, 192), + StartTime = 500, + Duration = 2000 + } + } + }; } } From bcaaf2527892e3ac2a02fc64e8226f5664a4a5f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Aug 2020 22:04:18 +0200 Subject: [PATCH 2637/6909] Fix Spun Out mod being affected by rate-changing mods --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 47d765fecd..2816073e8c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -41,7 +41,12 @@ namespace osu.Game.Rulesets.Osu.Mods var spinner = (DrawableSpinner)drawable; spinner.RotationTracker.Tracking = true; - spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)spinner.Clock.ElapsedFrameTime * 0.03f)); + + // because the spinner is under the gameplay clock, it is affected by rate adjustments on the track; + // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. + // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. + var rateIndependentElapsedTime = spinner.Clock.ElapsedFrameTime / spinner.Clock.Rate; + spinner.RotationTracker.AddRotation(MathUtils.RadiansToDegrees((float)rateIndependentElapsedTime * 0.03f)); } } } From 4f3f95540be8836006ce2bffee47f51efa987617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 11 Aug 2020 22:34:46 +0200 Subject: [PATCH 2638/6909] Check for zero rate to prevent crashes on unpause --- osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs index 2816073e8c..f080e11933 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpunOut.cs @@ -42,6 +42,10 @@ namespace osu.Game.Rulesets.Osu.Mods spinner.RotationTracker.Tracking = true; + // early-return if we were paused to avoid division-by-zero in the subsequent calculations. + if (Precision.AlmostEquals(spinner.Clock.Rate, 0)) + return; + // because the spinner is under the gameplay clock, it is affected by rate adjustments on the track; // for that reason using ElapsedFrameTime directly leads to fewer SPM with Half Time and more SPM with Double Time. // for spinners we want the real (wall clock) elapsed time; to achieve that, unapply the clock rate locally here. From 139c0c75f87e03ad4d86fd524e61c4dea79f0e81 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 12 Aug 2020 06:37:25 +0200 Subject: [PATCH 2639/6909] Add documentation for constructor --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 8c96e59f30..3e744056bb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -7,6 +7,7 @@ using System.Globalization; using System.IO; using System.Linq; using System.Text; +using JetBrains.Annotations; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; @@ -25,7 +26,9 @@ namespace osu.Game.Beatmaps.Formats private readonly IBeatmap beatmap; private readonly LegacyBeatmapSkin beatmapSkin; - public LegacyBeatmapEncoder(IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin = null) + /// The beatmap to encode + /// An optional beatmap skin, for encoding the beatmap's combo colours. + public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] LegacyBeatmapSkin beatmapSkin) { this.beatmap = beatmap; this.beatmapSkin = beatmapSkin; From 8ffaa49839bdf3902e57c57b0ff429b8c91a2fb1 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 12 Aug 2020 06:37:33 +0200 Subject: [PATCH 2640/6909] Handle additional null case --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 3e744056bb..eb148794de 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -207,12 +207,9 @@ namespace osu.Game.Beatmaps.Formats private void handleComboColours(TextWriter writer) { - if (beatmapSkin == null) - return; + var colours = beatmapSkin?.Configuration.ComboColours; - var colours = beatmapSkin.Configuration.ComboColours; - - if (colours.Count == 0) + if (colours == null || colours.Count == 0) return; writer.WriteLine("[Colours]"); From 69590113d62c54ec56c86c8bc529a49b0c2e337a Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Wed, 12 Aug 2020 06:38:05 +0200 Subject: [PATCH 2641/6909] Temporary changes --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 8 +++++++- osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs | 8 +++++++- osu.Game/Screens/Edit/EditorChangeHandler.cs | 8 +++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 30331e98d2..fba63f8539 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -10,6 +10,7 @@ using System.Text; using NUnit.Framework; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; @@ -19,6 +20,7 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; +using osu.Game.Skinning; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Beatmaps.Formats @@ -61,7 +63,11 @@ namespace osu.Game.Tests.Beatmaps.Formats var stream = new MemoryStream(); using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(beatmap).Encode(writer); + using (var rs = new ResourceStore()) + { + var skin = new LegacyBeatmapSkin(beatmap.BeatmapInfo, rs, null); + new LegacyBeatmapEncoder(beatmap, skin).Encode(writer); + } stream.Position = 0; diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index ff17f23d50..74b6f66d85 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -4,6 +4,7 @@ using System.IO; using System.Text; using NUnit.Framework; +using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; @@ -14,6 +15,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; +using osu.Game.Skinning; using osuTK; using Decoder = osu.Game.Beatmaps.Formats.Decoder; @@ -351,7 +353,11 @@ namespace osu.Game.Tests.Editing using (var encoded = new MemoryStream()) { using (var sw = new StreamWriter(encoded)) - new LegacyBeatmapEncoder(beatmap).Encode(sw); + using (var rs = new ResourceStore()) + { + var skin = new LegacyBeatmapSkin(beatmap.BeatmapInfo, rs, null); + new LegacyBeatmapEncoder(beatmap, skin).Encode(sw); + } return encoded.ToArray(); } diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 1553c2d2ef..6393093c74 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -6,8 +6,10 @@ using System.Collections.Generic; using System.IO; using System.Text; using osu.Framework.Bindables; +using osu.Framework.IO.Stores; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; namespace osu.Game.Screens.Edit { @@ -85,7 +87,11 @@ namespace osu.Game.Screens.Edit using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(editorBeatmap).Encode(sw); + using (var rs = new ResourceStore()) + { + var skin = new LegacyBeatmapSkin(editorBeatmap.BeatmapInfo, rs, null); + new LegacyBeatmapEncoder(editorBeatmap, skin).Encode(sw); + } savedStates.Add(stream.ToArray()); } From f3202fb123807ed3ef1b1a693e88f80f9ff42296 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 12 Aug 2020 11:24:26 +0300 Subject: [PATCH 2642/6909] Naming adjustments --- osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs | 6 +++--- .../News/{HomeNewsPanel.cs => FeaturedNewsItemPanel.cs} | 4 ++-- .../Home/News/{CollapsedNewsPanel.cs => NewsGroupItem.cs} | 4 ++-- .../News/{HomeNewsGroupPanel.cs => NewsItemGroupPanel.cs} | 8 ++++---- .../{HomeShowMoreNewsPanel.cs => ShowMoreNewsPanel.cs} | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) rename osu.Game/Overlays/Dashboard/Home/News/{HomeNewsPanel.cs => FeaturedNewsItemPanel.cs} (98%) rename osu.Game/Overlays/Dashboard/Home/News/{CollapsedNewsPanel.cs => NewsGroupItem.cs} (97%) rename osu.Game/Overlays/Dashboard/Home/News/{HomeNewsGroupPanel.cs => NewsItemGroupPanel.cs} (76%) rename osu.Game/Overlays/Dashboard/Home/News/{HomeShowMoreNewsPanel.cs => ShowMoreNewsPanel.cs} (93%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs index b1c0c5adcd..a1251ca793 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneHomeNewsPanel.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Visual.Online Spacing = new Vector2(0, 5), Children = new Drawable[] { - new HomeNewsPanel(new APINewsPost + new FeaturedNewsItemPanel(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.", @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Online PublishedAt = DateTimeOffset.Now, Slug = "2020-07-16-summer-theme-park-2020-voting-open" }), - new HomeNewsGroupPanel(new List + new NewsItemGroupPanel(new List { new APINewsPost { @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Online PublishedAt = DateTimeOffset.Now, } }), - new HomeShowMoreNewsPanel() + new ShowMoreNewsPanel() } }); } diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs similarity index 98% rename from osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs rename to osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs index ca56c33315..ee88469e2f 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/FeaturedNewsItemPanel.cs @@ -18,11 +18,11 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Dashboard.Home.News { - public class HomeNewsPanel : HomePanel + public class FeaturedNewsItemPanel : HomePanel { private readonly APINewsPost post; - public HomeNewsPanel(APINewsPost post) + public FeaturedNewsItemPanel(APINewsPost post) { this.post = post; } diff --git a/osu.Game/Overlays/Dashboard/Home/News/CollapsedNewsPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs similarity index 97% rename from osu.Game/Overlays/Dashboard/Home/News/CollapsedNewsPanel.cs rename to osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs index 7dbc6a8f87..dc4f3f8c92 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/CollapsedNewsPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsGroupItem.cs @@ -12,11 +12,11 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Dashboard.Home.News { - public class CollapsedNewsPanel : CompositeDrawable + public class NewsGroupItem : CompositeDrawable { private readonly APINewsPost post; - public CollapsedNewsPanel(APINewsPost post) + public NewsGroupItem(APINewsPost post) { this.post = post; } diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs similarity index 76% rename from osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs rename to osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs index 6007f1408b..c1d5a87ef5 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/HomeNewsGroupPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/NewsItemGroupPanel.cs @@ -10,11 +10,11 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Overlays.Dashboard.Home.News { - public class HomeNewsGroupPanel : HomePanel + public class NewsItemGroupPanel : HomePanel { private readonly List posts; - public HomeNewsGroupPanel(List posts) + public NewsItemGroupPanel(List posts) { this.posts = posts; } @@ -24,12 +24,12 @@ namespace osu.Game.Overlays.Dashboard.Home.News { Content.Padding = new MarginPadding { Vertical = 5 }; - Child = new FillFlowContainer + Child = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Children = posts.Select(p => new CollapsedNewsPanel(p)).ToArray() + Children = posts.Select(p => new NewsGroupItem(p)).ToArray() }; } } diff --git a/osu.Game/Overlays/Dashboard/Home/News/HomeShowMoreNewsPanel.cs b/osu.Game/Overlays/Dashboard/Home/News/ShowMoreNewsPanel.cs similarity index 93% rename from osu.Game/Overlays/Dashboard/Home/News/HomeShowMoreNewsPanel.cs rename to osu.Game/Overlays/Dashboard/Home/News/ShowMoreNewsPanel.cs index abb4bd7969..d25df6f189 100644 --- a/osu.Game/Overlays/Dashboard/Home/News/HomeShowMoreNewsPanel.cs +++ b/osu.Game/Overlays/Dashboard/Home/News/ShowMoreNewsPanel.cs @@ -10,7 +10,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Dashboard.Home.News { - public class HomeShowMoreNewsPanel : OsuHoverContainer + public class ShowMoreNewsPanel : OsuHoverContainer { protected override IEnumerable EffectTargets => new[] { text }; @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Dashboard.Home.News private OsuSpriteText text; - public HomeShowMoreNewsPanel() + public ShowMoreNewsPanel() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; From b10cddf625c9a51bfeac4e22a9871818486cd1f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Aug 2020 23:28:08 +0900 Subject: [PATCH 2643/6909] 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 a384ad4c34..7c0cb9271e 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 b38ef38ec2..bb89492e8b 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 00ddd94d53..8364caa42f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 00f8bb7c3e36ccb09de2b1634a9f5161b3a8cd67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 12 Aug 2020 23:28:45 +0900 Subject: [PATCH 2644/6909] 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 7c0cb9271e..241b836aac 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 bb89492e8b..63267e1494 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 8364caa42f..3500eb75dc 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 45876bc55aca3d28034c8ba7d6b3ea5322708c23 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 12 Aug 2020 23:50:33 +0900 Subject: [PATCH 2645/6909] Fix reference to non-existent variable --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index ab6cf60a79..3c559765d4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(0); - AddStep("adjust track rate", () => track.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate))); + AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate))); // autoplay replay frames use track time; // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time. // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time, From 91e28b849de0ed31402536b63069b8b2d5383f4b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 Aug 2020 00:29:23 +0900 Subject: [PATCH 2646/6909] Fix incorrect BeatmapManager construction --- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 3 ++- osu.Game/Beatmaps/BeatmapManager.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index eb4750a597..e54292f7cc 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Graphics.Cursor; using osu.Framework.Platform; using osu.Framework.Testing; @@ -79,7 +80,7 @@ namespace osu.Game.Tests.Visual.UserInterface var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); dependencies.Cache(rulesetStore = new RulesetStore(ContextFactory)); - dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, Audio, dependencies.Get(), Beatmap.Default)); + dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); beatmap = beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result.Beatmaps[0]; diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 6f01580998..0cadcf5947 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -9,6 +9,7 @@ using System.Linq.Expressions; using System.Text; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -67,7 +68,7 @@ namespace osu.Game.Beatmaps private readonly TextureStore textureStore; private readonly ITrackStore trackStore; - public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, AudioManager audioManager, GameHost host = null, + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, GameHost host = null, WorkingBeatmap defaultBeatmap = null) : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) { From d2a03f1146dc8257c1ffec299272edeff3f05d03 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 Aug 2020 00:59:22 +0900 Subject: [PATCH 2647/6909] Refactor TaikoDifficultyHitObject --- .../Preprocessing/TaikoDifficultyHitObject.cs | 13 ++++++------- osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 4 ++-- .../Difficulty/Skills/Stamina.cs | 10 +++++----- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index cd45db2119..d0f621f4ad 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -17,21 +17,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public bool StaminaCheese = false; - public readonly double NoteLength; + public readonly int ObjectIndex; - public readonly int N; - - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int n, IEnumerable commonRhythms) + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex, + IEnumerable commonRhythms) : base(hitObject, lastObject, clockRate) { var currentHit = hitObject as Hit; - NoteLength = DeltaTime; double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - Rhythm = getClosestRhythm(NoteLength / prevLength, commonRhythms); + + Rhythm = getClosestRhythm(DeltaTime / prevLength, commonRhythms); IsKat = currentHit?.Type == HitType.Rim; - N = n; + ObjectIndex = objectIndex; } private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio, IEnumerable commonRhythms) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index c3e6ee4d12..31dc93a6b2 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (samePattern) // Repetition found! { - int notesSince = hitobject.N - rhythmHistory[start].N; + int notesSince = hitobject.ObjectIndex - rhythmHistory[start].ObjectIndex; penalty *= repetitionPenalty(notesSince); break; } @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills objectStrain *= repetitionPenalties(hitobject); objectStrain *= patternLengthPenalty(notesSinceRhythmChange); - objectStrain *= speedPenalty(hitobject.NoteLength); + objectStrain *= speedPenalty(hitobject.DeltaTime); notesSinceRhythmChange = 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 29c1c3c322..c9a691a2aa 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -49,14 +49,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - if (hitObject.N % 2 == hand) + if (hitObject.ObjectIndex % 2 == hand) { double objectStrain = 1; - if (hitObject.N == 1) + if (hitObject.ObjectIndex == 1) return 1; - notePairDurationHistory.Add(hitObject.NoteLength + offhandObjectDuration); + notePairDurationHistory.Add(hitObject.DeltaTime + offhandObjectDuration); if (notePairDurationHistory.Count > max_history_length) notePairDurationHistory.RemoveAt(0); @@ -65,12 +65,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills objectStrain += speedBonus(shortestRecentNote); if (hitObject.StaminaCheese) - objectStrain *= cheesePenalty(hitObject.NoteLength + offhandObjectDuration); + objectStrain *= cheesePenalty(hitObject.DeltaTime + offhandObjectDuration); return objectStrain; } - offhandObjectDuration = hitObject.NoteLength; + offhandObjectDuration = hitObject.DeltaTime; return 0; } From 5010d2044a8b53ed8475dbfde17286485bd64872 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 Aug 2020 01:35:56 +0900 Subject: [PATCH 2648/6909] Replace IsKat with HitType --- .../Preprocessing/StaminaCheeseDetector.cs | 15 ++-- .../Preprocessing/TaikoDifficultyHitObject.cs | 4 +- .../Difficulty/Skills/Colour.cs | 87 +++++++++---------- 3 files changed, 49 insertions(+), 57 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index b52dad5198..b53bc66f39 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { @@ -17,10 +18,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing hitObjects = difficultyHitObjects; findRolls(3); findRolls(4); - findTlTap(0, true); - findTlTap(1, true); - findTlTap(0, false); - findTlTap(1, false); + findTlTap(0, HitType.Rim); + findTlTap(1, HitType.Rim); + findTlTap(0, HitType.Centre); + findTlTap(1, HitType.Centre); } private void findRolls(int patternLength) @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing for (int j = 0; j < patternLength; j++) { - if (history[j].IsKat != history[j + patternLength].IsKat) + if (history[j].HitType != history[j + patternLength].HitType) { isRepeat = false; } @@ -63,13 +64,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } - private void findTlTap(int parity, bool kat) + private void findTlTap(int parity, HitType type) { int tlLength = -2; for (int i = parity; i < hitObjects.Count; i += 2) { - if (kat == hitObjects[i].IsKat) + if (hitObjects[i].HitType == type) { tlLength += 2; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index d0f621f4ad..817e974fe8 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public class TaikoDifficultyHitObject : DifficultyHitObject { public readonly TaikoDifficultyHitObjectRhythm Rhythm; - public readonly bool IsKat; + public readonly HitType? HitType; public bool StaminaCheese = false; @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; Rhythm = getClosestRhythm(DeltaTime / prevLength, commonRhythms); - IsKat = currentHit?.Type == HitType.Rim; + HitType = currentHit?.Type; ObjectIndex = objectIndex; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 7c1623c54e..a348c25331 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -12,26 +12,54 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Colour : Skill { + private const int mono_history_max_length = 5; + protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; - private NoteColour prevNoteColour = NoteColour.None; + private HitType? previousHitType; private int currentMonoLength = 1; private readonly List monoHistory = new List(); - private const int mono_history_max_length = 5; + + protected override double StrainValueOf(DifficultyHitObject current) + { + if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) + { + previousHitType = null; + return 0.0; + } + + var taikoCurrent = (TaikoDifficultyHitObject)current; + + double objectStrain = 0.0; + + if (taikoCurrent.HitType != null && previousHitType != null && taikoCurrent.HitType != previousHitType) + { + objectStrain = 1.0; + + if (monoHistory.Count < 2) + objectStrain = 0.0; + else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) + objectStrain *= sameParityPenalty(); + + objectStrain *= repetitionPenalties(); + currentMonoLength = 1; + } + else + { + currentMonoLength += 1; + } + + previousHitType = taikoCurrent.HitType; + return objectStrain; + } private double sameParityPenalty() { return 0.0; } - private double repetitionPenalty(int notesSince) - { - double n = notesSince; - return Math.Min(1.0, 0.032 * n); - } - private double repetitionPenalties() { double penalty = 1.0; @@ -68,47 +96,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return penalty; } - protected override double StrainValueOf(DifficultyHitObject current) + private double repetitionPenalty(int notesSince) { - if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) - { - prevNoteColour = NoteColour.None; - return 0.0; - } - - TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; - - double objectStrain = 0.0; - - NoteColour noteColour = hitObject.IsKat ? NoteColour.Ka : NoteColour.Don; - - if (noteColour == NoteColour.Don && prevNoteColour == NoteColour.Ka || - noteColour == NoteColour.Ka && prevNoteColour == NoteColour.Don) - { - objectStrain = 1.0; - - if (monoHistory.Count < 2) - objectStrain = 0.0; - else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) - objectStrain *= sameParityPenalty(); - - objectStrain *= repetitionPenalties(); - currentMonoLength = 1; - } - else - { - currentMonoLength += 1; - } - - prevNoteColour = noteColour; - return objectStrain; - } - - private enum NoteColour - { - Don, - Ka, - None + double n = notesSince; + return Math.Min(1.0, 0.032 * n); } } } From 27cd9e119aa27be6ce39ee7989f30c5ead5437db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Aug 2020 12:04:32 +0900 Subject: [PATCH 2649/6909] Delay beatmap load until after transition has finished Previously the beatmap would begin loading at the same time the `PlayerLoader` class was. This can cause a horribly visible series of stutters, especially when a storyboard is involved. Obviously we should be aiming to reduce the stutters via changes to the beatmap load process (such as incremental storyboard loading, `DrawableHitObject` pooling, etc.) but this improves user experience tenfold in the mean time. --- osu.Game/Screens/Play/PlayerLoader.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 93a734589c..d32fae1b90 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -153,8 +153,6 @@ namespace osu.Game.Screens.Play { base.OnEntering(last); - prepareNewPlayer(); - content.ScaleTo(0.7f); Background?.FadeColour(Color4.White, 800, Easing.OutQuint); @@ -172,11 +170,6 @@ namespace osu.Game.Screens.Play contentIn(); - MetadataInfo.Loading = true; - - // we will only be resumed if the player has requested a re-run (see restartRequested). - prepareNewPlayer(); - this.Delay(400).Schedule(pushWhenLoaded); } @@ -257,6 +250,9 @@ namespace osu.Game.Screens.Play private void prepareNewPlayer() { + if (!this.IsCurrentScreen()) + return; + var restartCount = player?.RestartCount + 1 ?? 0; player = createPlayer(); @@ -274,8 +270,10 @@ namespace osu.Game.Screens.Play private void contentIn() { - content.ScaleTo(1, 650, Easing.OutQuint); + MetadataInfo.Loading = true; + content.FadeInFromZero(400); + content.ScaleTo(1, 650, Easing.OutQuint).Then().Schedule(prepareNewPlayer); } private void contentOut() From 99bea6b8e9e5d5a586179c385d7a14c246c522f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Aug 2020 12:52:35 +0900 Subject: [PATCH 2650/6909] Add missing null check (player construction is potentially delayed now) --- osu.Game/Screens/Play/PlayerLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index d32fae1b90..dcf84a8821 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play private bool readyForPush => // don't push unless the player is completely loaded - player.LoadState == LoadState.Ready + player?.LoadState == LoadState.Ready // don't push if the user is hovering one of the panes, unless they are idle. && (IsHovered || idleTracker.IsIdle.Value) // don't push if the user is dragging a slider or otherwise. From 5b536aebe71a49f2c4eab4edd508985d00bdcdf3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Aug 2020 12:53:37 +0900 Subject: [PATCH 2651/6909] Add missing null checks and avoid cross-test pollution --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 4c73065087..c34b523c97 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -49,6 +49,8 @@ namespace osu.Game.Tests.Visual.Gameplay /// An action to run after container load. public void ResetPlayer(bool interactive, Action beforeLoadAction = null, Action afterLoadAction = null) { + player = null; + audioManager.Volume.SetDefault(); InputManager.Clear(); @@ -80,7 +82,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); AddStep("exit loader", () => loader.Exit()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); - AddAssert("player did not load", () => !player.IsLoaded); + AddAssert("player did not load", () => player?.IsLoaded != true); AddUntilStep("player disposed", () => loader.DisposalTask?.IsCompleted == true); AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1); } @@ -94,7 +96,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for load ready", () => { moveMouse(); - return player.LoadState == LoadState.Ready; + return player?.LoadState == LoadState.Ready; }); AddRepeatStep("move mouse", moveMouse, 20); @@ -222,7 +224,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).Value = false); AddStep("load player", () => ResetPlayer(false, beforeLoad, afterLoad)); - AddUntilStep("wait for player", () => player.LoadState == LoadState.Ready); + AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1); AddStep("click notification", () => From fd7bf70b7d4302c1536a6b0e1489ac0c9ff5a1ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Aug 2020 12:59:00 +0900 Subject: [PATCH 2652/6909] Remove weird "after load" action This was pretty pointless anyway and from its usages, doesn't look to need to exist. --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index c34b523c97..d6742a27c2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -46,8 +46,7 @@ namespace osu.Game.Tests.Visual.Gameplay /// /// If the test player should behave like the production one. /// An action to run before player load but after bindable leases are returned. - /// An action to run after container load. - public void ResetPlayer(bool interactive, Action beforeLoadAction = null, Action afterLoadAction = null) + public void ResetPlayer(bool interactive, Action beforeLoadAction = null) { player = null; @@ -55,18 +54,16 @@ namespace osu.Game.Tests.Visual.Gameplay InputManager.Clear(); + container = new TestPlayerLoaderContainer(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive))); + beforeLoadAction?.Invoke(); + Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToTrack(Beatmap.Value.Track); - InputManager.Child = container = new TestPlayerLoaderContainer( - loader = new TestPlayerLoader(() => - { - afterLoadAction?.Invoke(); - return player = new TestPlayer(interactive, interactive); - })); + InputManager.Child = container; } /// @@ -197,19 +194,19 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestMutedNotificationMasterVolume() { - addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, null, () => audioManager.Volume.IsDefault); + addVolumeSteps("master volume", () => audioManager.Volume.Value = 0, () => audioManager.Volume.IsDefault); } [Test] public void TestMutedNotificationTrackVolume() { - addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, null, () => audioManager.VolumeTrack.IsDefault); + addVolumeSteps("music volume", () => audioManager.VolumeTrack.Value = 0, () => audioManager.VolumeTrack.IsDefault); } [Test] public void TestMutedNotificationMuteButton() { - addVolumeSteps("mute button", null, () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value); + addVolumeSteps("mute button", () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value); } /// @@ -217,13 +214,12 @@ namespace osu.Game.Tests.Visual.Gameplay /// /// What part of the volume system is checked /// The action to be invoked to set the volume before loading - /// The action to be invoked to set the volume after loading /// The function to be invoked and checked - private void addVolumeSteps(string volumeName, Action beforeLoad, Action afterLoad, Func assert) + private void addVolumeSteps(string volumeName, Action beforeLoad, Func assert) { AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).Value = false); - AddStep("load player", () => ResetPlayer(false, beforeLoad, afterLoad)); + AddStep("load player", () => ResetPlayer(false, beforeLoad)); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1); From cf9bda6c199bddcbf957033191285814c531b04a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Aug 2020 13:05:00 +0900 Subject: [PATCH 2653/6909] Add coverage of early exit with null and non-null player --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index d6742a27c2..e698d31176 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -66,20 +66,34 @@ namespace osu.Game.Tests.Visual.Gameplay InputManager.Child = container; } - /// - /// When exits early, it has to wait for the player load task - /// to complete before running disposal on player. This previously caused an issue where mod - /// speed adjustments were undone too late, causing cross-screen pollution. - /// [Test] - public void TestEarlyExit() + public void TestEarlyExitBeforePlayerConstruction() { AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); AddStep("exit loader", () => loader.Exit()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); - AddAssert("player did not load", () => player?.IsLoaded != true); + AddAssert("player did not load", () => player == null); + AddUntilStep("player disposed", () => loader.DisposalTask == null); + AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1); + } + + /// + /// When exits early, it has to wait for the player load task + /// to complete before running disposal on player. This previously caused an issue where mod + /// speed adjustments were undone too late, causing cross-screen pollution. + /// + [Test] + public void TestEarlyExitAfterPlayerConstruction() + { + AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); + AddUntilStep("wait for non-null player", () => player != null); + AddStep("exit loader", () => loader.Exit()); + AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); + AddAssert("player did not load", () => !player.IsLoaded); AddUntilStep("player disposed", () => loader.DisposalTask?.IsCompleted == true); AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1); } From 8ded5925ff0fbf2dcdbf4e00146898009ab556e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 Aug 2020 13:47:35 +0900 Subject: [PATCH 2654/6909] Xmldoc colour strain --- .../Difficulty/Skills/Colour.cs | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index a348c25331..db445c7d27 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -19,7 +19,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private HitType? previousHitType; + /// + /// Length of the current mono pattern. + /// private int currentMonoLength = 1; + + /// + /// List of the last most recent mono patterns, with the most recent at the end of the list. + /// private readonly List monoHistory = new List(); protected override double StrainValueOf(DifficultyHitObject current) @@ -36,12 +43,20 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (taikoCurrent.HitType != null && previousHitType != null && taikoCurrent.HitType != previousHitType) { + // The colour has changed. objectStrain = 1.0; if (monoHistory.Count < 2) + { + // There needs to be at least two streaks to determine a strain. objectStrain = 0.0; + } else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) + { + // The last streak in the history is guaranteed to be a different type to the current streak. + // If the total number of notes in the two streaks is even, apply a penalty. objectStrain *= sameParityPenalty(); + } objectStrain *= repetitionPenalties(); currentMonoLength = 1; @@ -55,11 +70,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return objectStrain; } - private double sameParityPenalty() - { - return 0.0; - } + /// + /// The penalty to apply when the total number of notes in the two most recent colour streaks is even. + /// + private double sameParityPenalty() => 0.0; + /// + /// The penalty to apply due to the length of repetition in colour streaks. + /// private double repetitionPenalties() { double penalty = 1.0; @@ -96,10 +114,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return penalty; } - private double repetitionPenalty(int notesSince) - { - double n = notesSince; - return Math.Min(1.0, 0.032 * n); - } + private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); } } From c71ee0877ff0ad2d5b161a6923a51281c513b76a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Aug 2020 14:07:07 +0900 Subject: [PATCH 2655/6909] Update fastlane and plugins --- Gemfile.lock | 73 ++++++++++++++++++++++++++-------------------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index bf971d2c22..a4b49af7e4 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -6,35 +6,36 @@ GEM 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-partitions (1.354.0) + aws-sdk-core (3.104.3) 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-kms (1.36.0) 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-s3 (1.78.0) + aws-sdk-core (~> 3, >= 3.104.3) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.1.4) - aws-eventstream (~> 1.0, >= 1.0.2) + aws-sigv4 (1.2.1) + aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.3) claide (1.0.3) colored (1.2) colored2 (3.1.2) commander-fastlane (4.4.6) highline (~> 1.7.2) - declarative (0.0.10) + declarative (0.0.20) declarative-option (0.1.0) - digest-crc (0.5.1) + digest-crc (0.6.1) + rake (~> 13.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) - dotenv (2.7.5) - emoji_regex (1.0.1) - excon (0.74.0) + dotenv (2.7.6) + emoji_regex (3.0.0) + excon (0.76.0) faraday (1.0.1) multipart-post (>= 1.2, < 3) faraday-cookie_jar (0.0.6) @@ -42,34 +43,32 @@ GEM http-cookie (~> 1.0.0) faraday_middleware (1.0.0) faraday (~> 1.0) - fastimage (2.1.7) - fastlane (2.149.1) + fastimage (2.2.0) + fastlane (2.156.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) aws-sdk-s3 (~> 1.0) - babosa (>= 1.0.2, < 2.0.0) + babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) colored commander-fastlane (>= 4.4.6, < 5.0.0) dotenv (>= 2.1.1, < 3.0.0) - emoji_regex (>= 0.1, < 2.0) + emoji_regex (>= 0.1, < 4.0) excon (>= 0.71.0, < 1.0.0) - faraday (>= 0.17, < 2.0) + faraday (~> 1.0) faraday-cookie_jar (~> 0.0.6) - faraday_middleware (>= 0.13.1, < 2.0) + faraday_middleware (~> 1.0) fastimage (>= 2.1.0, < 3.0.0) gh_inspector (>= 1.1.2, < 2.0.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) - jwt (~> 2.1.0) + jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) - multi_xml (~> 0.5) multipart-post (~> 2.0.0) plist (>= 3.1.0, < 4.0.0) - public_suffix (~> 2.0.0) - rubyzip (>= 1.3.0, < 2.0.0) + rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) simctl (~> 1.6.3) slack-notifier (>= 2.0.0, < 3.0.0) @@ -97,17 +96,17 @@ GEM google-cloud-core (1.5.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.3.2) + google-cloud-env (1.3.3) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.0.1) - google-cloud-storage (1.26.2) + google-cloud-storage (1.27.0) 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.12.0) + googleauth (0.13.1) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -119,29 +118,29 @@ GEM domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.4.0) - json (2.3.0) - jwt (2.1.0) + json (2.3.1) + jwt (2.2.1) memoist (0.16.2) mini_magick (4.10.1) mini_mime (1.0.2) mini_portile2 (2.4.0) - multi_json (1.14.1) - multi_xml (0.6.0) + multi_json (1.15.0) multipart-post (2.0.0) - nanaimo (0.2.6) + nanaimo (0.3.0) naturally (2.2.0) - nokogiri (1.10.7) + nokogiri (1.10.10) mini_portile2 (~> 2.4.0) - os (1.1.0) + os (1.1.1) plist (3.5.0) - public_suffix (2.0.5) + public_suffix (4.0.5) + rake (13.0.1) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) uber (< 0.2.0) retriable (3.1.2) rouge (2.0.7) - rubyzip (1.3.0) + rubyzip (2.3.0) security (0.1.3) signet (0.14.0) addressable (~> 2.3) @@ -160,7 +159,7 @@ GEM terminal-table (1.8.0) unicode-display_width (~> 1.1, >= 1.1.1) tty-cursor (0.7.1) - tty-screen (0.8.0) + tty-screen (0.8.1) tty-spinner (0.9.3) tty-cursor (~> 0.7) uber (0.1.0) @@ -169,12 +168,12 @@ GEM unf_ext (0.0.7.7) unicode-display_width (1.7.0) word_wrap (1.0.0) - xcodeproj (1.16.0) + xcodeproj (1.18.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) colored2 (~> 3.1) - nanaimo (~> 0.2.6) + nanaimo (~> 0.3.0) xcpretty (0.3.0) rouge (~> 2.0.7) xcpretty-travis-formatter (1.0.0) From 84cb36b6a8ee180552b41f48711b149d710cbaf6 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 13 Aug 2020 10:57:18 +0200 Subject: [PATCH 2656/6909] Defer subscriptions for updateOverlayActivationMode() to OnEntering() --- osu.Game/Screens/Play/Player.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2ecddf0f23..6bb4be4096 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -212,11 +212,6 @@ namespace osu.Game.Screens.Play if (game != null) OverlayActivationMode.BindTo(game.OverlayActivationMode); - gameplayOverlaysDisabled.BindValueChanged(_ => updateOverlayActivationMode()); - DrawableRuleset.IsPaused.BindValueChanged(_ => updateOverlayActivationMode()); - DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateOverlayActivationMode()); - breakTracker.IsBreakTime.BindValueChanged(_ => updateOverlayActivationMode()); - DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); // bind clock into components that require it @@ -363,9 +358,6 @@ namespace osu.Game.Screens.Play private void updateOverlayActivationMode() { - if (!this.IsCurrentScreen()) - return; - bool canTriggerOverlays = DrawableRuleset.IsPaused.Value || breakTracker.IsBreakTime.Value || !gameplayOverlaysDisabled.Value; if (DrawableRuleset.HasReplayLoaded.Value || canTriggerOverlays) @@ -661,7 +653,10 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHUD(HUDOverlay); - updateOverlayActivationMode(); + DrawableRuleset.IsPaused.BindValueChanged(_ => updateOverlayActivationMode()); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateOverlayActivationMode()); + breakTracker.IsBreakTime.BindValueChanged(_ => updateOverlayActivationMode()); + gameplayOverlaysDisabled.BindValueChanged(_ => updateOverlayActivationMode(), true); } public override void OnSuspending(IScreen next) From 662281d727561b70572f76d40174a1bc75d67604 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 13 Aug 2020 18:20:45 +0900 Subject: [PATCH 2657/6909] Adjust legacy spinners to fade in later Matches stable 1:1 for legacy skins. I've left lazer default as it is because changing to use the shorter apperance looks bad. This will probably change as we proceed with the redesign of the default skin. --- osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs | 4 ++-- osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 72bc3ddc9a..739c87e037 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -79,8 +79,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { var spinner = (Spinner)drawableSpinner.HitObject; - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) - this.FadeInFromZero(spinner.TimePreempt / 2); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) + this.FadeInFromZero(spinner.TimeFadeIn / 2); fixedMiddle.FadeColour(Color4.White); using (BeginAbsoluteSequence(spinner.StartTime, true)) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 0ae1d8f683..81a0df5ea5 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -85,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { var spinner = drawableSpinner.HitObject; - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) - this.FadeInFromZero(spinner.TimePreempt / 2); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) + this.FadeInFromZero(spinner.TimeFadeIn / 2); } protected override void Update() From 3cb22fad82d6d1f3b0bc07f8bb025acabb090cd5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 Aug 2020 19:48:31 +0900 Subject: [PATCH 2658/6909] Fix mods sharing bindable instances --- .../UserInterface/TestSceneModSettings.cs | 20 ++++++++++++++++++ osu.Game/Rulesets/Mods/Mod.cs | 21 ++++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index 7ff463361a..c5ce3751ef 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -15,6 +16,7 @@ using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; namespace osu.Game.Tests.Visual.UserInterface @@ -75,6 +77,24 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.Alpha == 0); } + [Test] + public void TestModSettingsUnboundWhenCopied() + { + OsuModDoubleTime original = null; + OsuModDoubleTime copy = null; + + AddStep("create mods", () => + { + original = new OsuModDoubleTime(); + copy = (OsuModDoubleTime)original.CreateCopy(); + }); + + AddStep("change property", () => original.SpeedChange.Value = 2); + + AddAssert("original has new value", () => Precision.AlmostEquals(2.0, original.SpeedChange.Value)); + AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value)); + } + private void createModSelect() { AddStep("create mod select", () => diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 0e5fe3fc9c..52ffa0ad2a 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Reflection; using Newtonsoft.Json; @@ -126,7 +127,25 @@ namespace osu.Game.Rulesets.Mods /// /// Creates a copy of this initialised to a default state. /// - public virtual Mod CreateCopy() => (Mod)MemberwiseClone(); + public virtual Mod CreateCopy() + { + var copy = (Mod)Activator.CreateInstance(GetType()); + + // Copy bindable values across + foreach (var (_, prop) in this.GetSettingsSourceProperties()) + { + var origBindable = prop.GetValue(this); + var copyBindable = prop.GetValue(copy); + + // The bindables themselves are readonly, so the value must be transferred through the Bindable.Value property. + var valueProperty = origBindable.GetType().GetProperty(nameof(Bindable.Value), BindingFlags.Public | BindingFlags.Instance); + Debug.Assert(valueProperty != null); + + valueProperty.SetValue(copyBindable, valueProperty.GetValue(origBindable)); + } + + return copy; + } public bool Equals(IMod other) => GetType() == other?.GetType(); } From 0500d82b5bed73153b1bcee54374c557a4408ab4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 13 Aug 2020 19:48:41 +0900 Subject: [PATCH 2659/6909] Fix playlist items sharing mod instances --- .../Multiplayer/TestSceneMatchSongSelect.cs | 17 +++++++++++++++++ osu.Game/Screens/Select/MatchSongSelect.cs | 2 ++ 2 files changed, 19 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index c62479faa0..3d225aa0a9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -16,7 +16,9 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Select; @@ -145,6 +147,21 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("new item has id 2", () => Room.Playlist.Last().ID == 2); } + /// + /// Tests that the same instances are not shared between two playlist items. + /// + [Test] + public void TestNewItemHasNewModInstances() + { + AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); + AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); + AddStep("change mod rate", () => ((OsuModDoubleTime)SelectedMods.Value[0]).SpeedChange.Value = 2); + AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); + + AddAssert("item 1 has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value)); + AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); + } + private class TestMatchSongSelect : MatchSongSelect { public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails; diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 2f3674642e..96a48fa3ac 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -77,6 +77,8 @@ namespace osu.Game.Screens.Select item.RequiredMods.Clear(); item.RequiredMods.AddRange(Mods.Value); + + Mods.Value = Mods.Value.Select(m => m.CreateCopy()).ToArray(); } } } From 74a8a4bca8dc7ee11a15e764e87c828bb9509668 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 13 Aug 2020 21:53:17 +0200 Subject: [PATCH 2660/6909] Make testing code clearer to understand. --- .../Gameplay/TestSceneOverlayActivation.cs | 42 +++++++++---------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 7fd5158515..04c67433fa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -11,56 +11,54 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneOverlayActivation : OsuPlayerTestScene { - private OverlayTestPlayer testPlayer; - - public override void SetUpSteps() - { - AddStep("disable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, true)); - base.SetUpSteps(); - } + protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer; [Test] public void TestGameplayOverlayActivation() { - AddAssert("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("disable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, true)); + AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); } [Test] public void TestGameplayOverlayActivationDisabled() { AddStep("enable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, false)); - AddAssert("activation mode is user triggered", () => testPlayer.OverlayActivationMode == OverlayActivation.UserTriggered); + AddAssert("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); } [Test] public void TestGameplayOverlayActivationPaused() { - AddUntilStep("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); - AddStep("pause gameplay", () => testPlayer.Pause()); - AddUntilStep("activation mode is user triggered", () => testPlayer.OverlayActivationMode == OverlayActivation.UserTriggered); + AddStep("disable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, true)); + AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("pause gameplay", () => Player.Pause()); + AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); } [Test] public void TestGameplayOverlayActivationReplayLoaded() { - AddAssert("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); - AddStep("load a replay", () => testPlayer.DrawableRuleset.HasReplayLoaded.Value = true); - AddAssert("activation mode is user triggered", () => testPlayer.OverlayActivationMode == OverlayActivation.UserTriggered); + AddStep("disable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, true)); + AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("load a replay", () => Player.DrawableRuleset.HasReplayLoaded.Value = true); + AddAssert("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); } [Test] public void TestGameplayOverlayActivationBreaks() { - AddAssert("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); - AddStep("seek to break", () => testPlayer.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime)); - AddUntilStep("activation mode is user triggered", () => testPlayer.OverlayActivationMode == OverlayActivation.UserTriggered); - AddStep("seek to break end", () => testPlayer.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().EndTime)); - AddUntilStep("activation mode is disabled", () => testPlayer.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("disable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, true)); + AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + AddStep("seek to break", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime)); + AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); + AddStep("seek to break end", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().EndTime)); + AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); } - protected override TestPlayer CreatePlayer(Ruleset ruleset) => testPlayer = new OverlayTestPlayer(); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OverlayTestPlayer(); - private class OverlayTestPlayer : TestPlayer + protected class OverlayTestPlayer : TestPlayer { public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value; } From 671141ec61e1eaf8b0daeea84c1bb03c498dc997 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Aug 2020 18:05:05 +0900 Subject: [PATCH 2661/6909] Load menu backgrounds via LargeTextureStore to reduce memory usage --- osu.Game/Graphics/Backgrounds/BeatmapBackground.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs index 387e189dc4..058d2ed0f9 100644 --- a/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs +++ b/osu.Game/Graphics/Backgrounds/BeatmapBackground.cs @@ -20,7 +20,7 @@ namespace osu.Game.Graphics.Backgrounds } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(LargeTextureStore textures) { Sprite.Texture = Beatmap?.Background ?? textures.Get(fallbackTextureName); } From c3757a4660f1b1f1633e9b16af75d2960b343006 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Aug 2020 19:22:23 +0900 Subject: [PATCH 2662/6909] Fix beatmap covers not being unloaded in most overlays Eventually we'll probably want something smarter than this, but for the time being this helps stop runaway memory usage. --- .../Drawables/UpdateableBeatmapSetCover.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs index c60bd0286e..6c229755e7 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapSetCover.cs @@ -67,19 +67,18 @@ namespace osu.Game.Beatmaps.Drawables if (beatmapSet != null) { - BeatmapSetCover cover; - - Add(displayedCover = new DelayedLoadWrapper( - cover = new BeatmapSetCover(beatmapSet, coverType) + Add(displayedCover = new DelayedLoadUnloadWrapper(() => + { + var cover = new BeatmapSetCover(beatmapSet, coverType) { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fill, - }) - ); - - cover.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out); + }; + cover.OnLoadComplete += d => d.FadeInFromZero(400, Easing.Out); + return cover; + })); } } } From e39b2e7218acf876cea7c7efcaa6ae9a4faf7818 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 14 Aug 2020 21:53:18 +0900 Subject: [PATCH 2663/6909] 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 241b836aac..f3fb949f76 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 63267e1494..a12ce138bd 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 3500eb75dc..0170e94140 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From c1a9bf507af61e2737c6c81dc06efcda3dac91c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Aug 2020 13:06:53 +0200 Subject: [PATCH 2664/6909] Add failing test case --- .../Gameplay/TestSceneGameplayMenuOverlay.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs index e8b8c7c8e9..fc9cbb073e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs @@ -272,7 +272,21 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("Overlay is closed", () => pauseOverlay.State.Value == Visibility.Hidden); } + [Test] + public void TestSelectionResetOnVisibilityChange() + { + showOverlay(); + AddStep("Select last button", () => InputManager.Key(Key.Up)); + + hideOverlay(); + showOverlay(); + + AddAssert("No button selected", + () => pauseOverlay.Buttons.All(button => !button.Selected.Value)); + } + private void showOverlay() => AddStep("Show overlay", () => pauseOverlay.Show()); + private void hideOverlay() => AddStep("Hide overlay", () => pauseOverlay.Hide()); private DialogButton getButton(int index) => pauseOverlay.Buttons.Skip(index).First(); From a426ff1d5b26e158c868cb49ec51f21a6f265971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Aug 2020 13:36:00 +0200 Subject: [PATCH 2665/6909] Refactor gameplay menu overlay to fix regression --- osu.Game/Screens/Play/GameplayMenuOverlay.cs | 78 ++++++++++++-------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayMenuOverlay.cs b/osu.Game/Screens/Play/GameplayMenuOverlay.cs index 57403a0987..f938839be3 100644 --- a/osu.Game/Screens/Play/GameplayMenuOverlay.cs +++ b/osu.Game/Screens/Play/GameplayMenuOverlay.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play public abstract string Description { get; } - protected internal FillFlowContainer InternalButtons; + protected ButtonContainer InternalButtons; public IReadOnlyList Buttons => InternalButtons; private FillFlowContainer retryCounterContainer; @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Play { RelativeSizeAxes = Axes.Both; - State.ValueChanged += s => selectionIndex = -1; + State.ValueChanged += s => InternalButtons.Deselect(); } [BackgroundDependencyLoader] @@ -114,7 +114,7 @@ namespace osu.Game.Screens.Play } } }, - InternalButtons = new FillFlowContainer + InternalButtons = new ButtonContainer { Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, @@ -186,40 +186,16 @@ namespace osu.Game.Screens.Play InternalButtons.Add(button); } - private int selectionIndex = -1; - - private void setSelected(int value) - { - if (selectionIndex == value) - return; - - // Deselect the previously-selected button - if (selectionIndex != -1) - InternalButtons[selectionIndex].Selected.Value = false; - - selectionIndex = value; - - // Select the newly-selected button - if (selectionIndex != -1) - InternalButtons[selectionIndex].Selected.Value = true; - } - public bool OnPressed(GlobalAction action) { switch (action) { case GlobalAction.SelectPrevious: - if (selectionIndex == -1 || selectionIndex == 0) - setSelected(InternalButtons.Count - 1); - else - setSelected(selectionIndex - 1); + InternalButtons.SelectPrevious(); return true; case GlobalAction.SelectNext: - if (selectionIndex == -1 || selectionIndex == InternalButtons.Count - 1) - setSelected(0); - else - setSelected(selectionIndex + 1); + InternalButtons.SelectNext(); return true; case GlobalAction.Back: @@ -241,9 +217,9 @@ namespace osu.Game.Screens.Play private void buttonSelectionChanged(DialogButton button, bool isSelected) { if (!isSelected) - setSelected(-1); + InternalButtons.Deselect(); else - setSelected(InternalButtons.IndexOf(button)); + InternalButtons.Select(button); } private void updateRetryCount() @@ -277,6 +253,46 @@ namespace osu.Game.Screens.Play }; } + protected class ButtonContainer : FillFlowContainer + { + private int selectedIndex = -1; + + private void setSelected(int value) + { + if (selectedIndex == value) + return; + + // Deselect the previously-selected button + if (selectedIndex != -1) + this[selectedIndex].Selected.Value = false; + + selectedIndex = value; + + // Select the newly-selected button + if (selectedIndex != -1) + this[selectedIndex].Selected.Value = true; + } + + public void SelectNext() + { + if (selectedIndex == -1 || selectedIndex == Count - 1) + setSelected(0); + else + setSelected(selectedIndex + 1); + } + + public void SelectPrevious() + { + if (selectedIndex == -1 || selectedIndex == 0) + setSelected(Count - 1); + else + setSelected(selectedIndex - 1); + } + + public void Deselect() => setSelected(-1); + public void Select(DialogButton button) => setSelected(IndexOf(button)); + } + private class Button : DialogButton { // required to ensure keyboard navigation always starts from an extremity (unless the cursor is moved) From 5c11270b988f7e8f85eddafe329d048d14228ad6 Mon Sep 17 00:00:00 2001 From: Ron B Date: Sat, 15 Aug 2020 20:12:06 +0300 Subject: [PATCH 2666/6909] Add SpinnerFrequencyModulate skin config option --- .../Objects/Drawables/DrawableSpinner.cs | 9 ++++++--- osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs | 3 ++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index d1a6463d72..273a9fda84 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Skinning; @@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly IBindable positionBindable = new Bindable(); + private bool spinnerFrequencyModulate; + public DrawableSpinner(Spinner s) : base(s) { @@ -165,10 +168,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, ISkinSource skin) { positionBindable.BindValueChanged(pos => Position = pos.NewValue); positionBindable.BindTo(HitObject.PositionBindable); + spinnerFrequencyModulate = skin.GetConfig(OsuSkinConfiguration.SpinnerFrequencyModulate)?.Value ?? true; } /// @@ -221,8 +225,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); if (spinningSample != null) - // todo: implement SpinnerFrequencyModulate - spinningSample.Frequency.Value = 0.5f + Progress; + spinningSample.Frequency.Value = spinnerFrequencyModulate ? 0.5f + Progress : 0.5f; } protected override void UpdateAfterChildren() diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 154160fdb5..54755bd9d5 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -13,6 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning CursorExpand, CursorRotate, HitCircleOverlayAboveNumber, - HitCircleOverlayAboveNumer // Some old skins will have this typo + HitCircleOverlayAboveNumer, // Some old skins will have this typo + SpinnerFrequencyModulate } } From 896a87e62921bfef2281a822eb20c784e3612fd2 Mon Sep 17 00:00:00 2001 From: Ron B Date: Sat, 15 Aug 2020 20:14:36 +0300 Subject: [PATCH 2667/6909] Replace accidental tab with spaces --- osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 54755bd9d5..1d34727c04 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning CursorExpand, CursorRotate, HitCircleOverlayAboveNumber, - HitCircleOverlayAboveNumer, // Some old skins will have this typo + HitCircleOverlayAboveNumer, // Some old skins will have this typo SpinnerFrequencyModulate } } From 61de3c75402f55704c07da9128778f35b374a52f Mon Sep 17 00:00:00 2001 From: Ron B Date: Sat, 15 Aug 2020 20:16:28 +0300 Subject: [PATCH 2668/6909] Replace accidental tab with spaces --- osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index 1d34727c04..e034e14eb0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Skinning CursorExpand, CursorRotate, HitCircleOverlayAboveNumber, - HitCircleOverlayAboveNumer, // Some old skins will have this typo - SpinnerFrequencyModulate + HitCircleOverlayAboveNumer, // Some old skins will have this typo + SpinnerFrequencyModulate } } From 07c25d5a78d2df33e3d54d3922da6723c3622f2f Mon Sep 17 00:00:00 2001 From: Ron B Date: Sat, 15 Aug 2020 20:51:33 +0300 Subject: [PATCH 2669/6909] Move spinnerFrequencyModulate set to ApplySkin --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 273a9fda84..5bf87ba16b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -172,6 +172,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { positionBindable.BindValueChanged(pos => Position = pos.NewValue); positionBindable.BindTo(HitObject.PositionBindable); + } + + protected override void ApplySkin(ISkinSource skin, bool allowFallback) + { spinnerFrequencyModulate = skin.GetConfig(OsuSkinConfiguration.SpinnerFrequencyModulate)?.Value ?? true; } From 40445d0005fe33943baf6350e8e2b35869e08643 Mon Sep 17 00:00:00 2001 From: Ron B Date: Sat, 15 Aug 2020 21:07:44 +0300 Subject: [PATCH 2670/6909] replicate osu-stable behaviour for spinningSample frequency --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 5bf87ba16b..dfe10eeaab 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -104,6 +104,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Volume = { Value = 0 }, Looping = true, + Frequency = { Value = 1.0f } }); } } @@ -228,8 +229,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (HandleUserInput) RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); - if (spinningSample != null) - spinningSample.Frequency.Value = spinnerFrequencyModulate ? 0.5f + Progress : 0.5f; + if (spinningSample != null && spinnerFrequencyModulate) + spinningSample.Frequency.Value = 0.5f + Progress; } protected override void UpdateAfterChildren() From a1079bac3234f53f39e894dbd888321e21561907 Mon Sep 17 00:00:00 2001 From: Ron B Date: Sat, 15 Aug 2020 21:19:47 +0300 Subject: [PATCH 2671/6909] Move frequency values into consts --- 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 dfe10eeaab..c44553a1c5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -85,6 +85,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } private SkinnableSound spinningSample; + private const float SPINNING_SAMPLE_INITAL_FREQUENCY = 1.0f; + private const float SPINNING_SAMPLE_MODULATED_BASE_FREQUENCY = 0.5f; protected override void LoadSamples() { @@ -104,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Volume = { Value = 0 }, Looping = true, - Frequency = { Value = 1.0f } + Frequency = { Value = SPINNING_SAMPLE_INITAL_FREQUENCY } }); } } @@ -230,7 +232,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); if (spinningSample != null && spinnerFrequencyModulate) - spinningSample.Frequency.Value = 0.5f + Progress; + spinningSample.Frequency.Value = SPINNING_SAMPLE_MODULATED_BASE_FREQUENCY + Progress; } protected override void UpdateAfterChildren() From 390e87273065aef35335d3d1e617dc62c9ea90eb Mon Sep 17 00:00:00 2001 From: Ron B Date: Sat, 15 Aug 2020 21:34:17 +0300 Subject: [PATCH 2672/6909] Fix acoording to review --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index c44553a1c5..a4636050bb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } private SkinnableSound spinningSample; - private const float SPINNING_SAMPLE_INITAL_FREQUENCY = 1.0f; + private const float SPINNING_SAMPLE_INITIAL_FREQUENCY = 1.0f; private const float SPINNING_SAMPLE_MODULATED_BASE_FREQUENCY = 0.5f; protected override void LoadSamples() @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Volume = { Value = 0 }, Looping = true, - Frequency = { Value = SPINNING_SAMPLE_INITAL_FREQUENCY } + Frequency = { Value = SPINNING_SAMPLE_INITIAL_FREQUENCY } }); } } @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } [BackgroundDependencyLoader] - private void load(OsuColour colours, ISkinSource skin) + private void load(OsuColour colours) { positionBindable.BindValueChanged(pos => Position = pos.NewValue); positionBindable.BindTo(HitObject.PositionBindable); @@ -179,6 +179,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void ApplySkin(ISkinSource skin, bool allowFallback) { + base.ApplySkin(skin, allowFallback); spinnerFrequencyModulate = skin.GetConfig(OsuSkinConfiguration.SpinnerFrequencyModulate)?.Value ?? true; } From 5f35b3ebb98821e2f8870964302e9c60dab84d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 15 Aug 2020 20:44:02 +0200 Subject: [PATCH 2673/6909] Fix constant casing --- .../Objects/Drawables/DrawableSpinner.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a4636050bb..a57bb466c7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -85,8 +85,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } private SkinnableSound spinningSample; - private const float SPINNING_SAMPLE_INITIAL_FREQUENCY = 1.0f; - private const float SPINNING_SAMPLE_MODULATED_BASE_FREQUENCY = 0.5f; + private const float spinning_sample_initial_frequency = 1.0f; + private const float spinning_sample_modulated_base_frequency = 0.5f; protected override void LoadSamples() { @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { Volume = { Value = 0 }, Looping = true, - Frequency = { Value = SPINNING_SAMPLE_INITIAL_FREQUENCY } + Frequency = { Value = spinning_sample_initial_frequency } }); } } @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); if (spinningSample != null && spinnerFrequencyModulate) - spinningSample.Frequency.Value = SPINNING_SAMPLE_MODULATED_BASE_FREQUENCY + Progress; + spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress; } protected override void UpdateAfterChildren() From c4a7fac760efde4622956f1fa3c581e469c8e508 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 15 Aug 2020 22:03:24 +0200 Subject: [PATCH 2674/6909] Add required parameters and other various changes --- .../Formats/LegacyBeatmapEncoderTest.cs | 30 ++++++++++++++----- .../Beatmaps/IO/ImportBeatmapTest.cs | 5 ++-- .../Editing/EditorChangeHandlerTest.cs | 6 ++-- .../Editing/LegacyEditorBeatmapPatcherTest.cs | 8 +---- osu.Game/Beatmaps/BeatmapManager.cs | 4 +-- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 13 ++++---- osu.Game/Screens/Edit/Editor.cs | 7 +++-- osu.Game/Screens/Edit/EditorChangeHandler.cs | 11 +++---- 8 files changed, 51 insertions(+), 33 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index fba63f8539..38f730995e 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -28,13 +28,25 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class LegacyBeatmapEncoderTest { - private static IEnumerable allBeatmaps => TestResources.GetStore().GetAvailableResources().Where(res => res.EndsWith(".osu")); + private static readonly DllResourceStore resource_store = TestResources.GetStore(); + + private static IEnumerable allBeatmaps = resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu")); + + private static Stream beatmapSkinStream = resource_store.GetStream("skin.ini"); + + private ISkin skin; + + [SetUp] + public void Init() + { + skin = decodeSkinFromLegacy(beatmapSkinStream); + } [TestCaseSource(nameof(allBeatmaps))] public void TestEncodeDecodeStability(string name) { - var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name)); - var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded)); + var decoded = decodeBeatmapFromLegacy(TestResources.GetStore().GetStream(name)); + var decodedAfterEncode = decodeBeatmapFromLegacy(encodeToLegacy(decoded, skin)); sort(decoded); sort(decodedAfterEncode); @@ -52,20 +64,24 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private IBeatmap decodeFromLegacy(Stream stream) + private IBeatmap decodeBeatmapFromLegacy(Stream stream) { using (var reader = new LineBufferedReader(stream)) return convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader)); } - private Stream encodeToLegacy(IBeatmap beatmap) + private ISkin decodeSkinFromLegacy(Stream stream) + { + using (var reader = new LineBufferedReader(stream)) + return new LegacySkin(SkinInfo.Default, resource_store, null); + } + + private Stream encodeToLegacy(IBeatmap beatmap, ISkin skin) { var stream = new MemoryStream(); using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - using (var rs = new ResourceStore()) { - var skin = new LegacyBeatmapSkin(beatmap.BeatmapInfo, rs, null); new LegacyBeatmapEncoder(beatmap, skin).Encode(writer); } diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0151678db3..17271184c0 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -730,7 +730,8 @@ namespace osu.Game.Tests.Beatmaps.IO 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; + var workingBeatmap = manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)); + Beatmap beatmapToUpdate = (Beatmap)workingBeatmap.Beatmap; BeatmapSetFileInfo fileToUpdate = setToUpdate.Files.First(f => beatmapToUpdate.BeatmapInfo.Path.Contains(f.Filename)); string oldMd5Hash = beatmapToUpdate.BeatmapInfo.MD5Hash; @@ -738,7 +739,7 @@ namespace osu.Game.Tests.Beatmaps.IO beatmapToUpdate.HitObjects.Clear(); beatmapToUpdate.HitObjects.Add(new HitCircle { StartTime = 5000 }); - manager.Save(beatmapInfo, beatmapToUpdate); + manager.Save(beatmapInfo, beatmapToUpdate, workingBeatmap.Skin); // 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)); diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index feda1ae0e9..6d708ce838 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -13,7 +13,7 @@ namespace osu.Game.Tests.Editing [Test] public void TestSaveRestoreState() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()), null); Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False); @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Editing [Test] public void TestMaxStatesSaved() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()), null); Assert.That(handler.CanUndo.Value, Is.False); @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Editing [Test] public void TestMaxStatesExceeded() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()), null); Assert.That(handler.CanUndo.Value, Is.False); diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index 74b6f66d85..b491157627 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -4,7 +4,6 @@ using System.IO; using System.Text; using NUnit.Framework; -using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Formats; @@ -15,7 +14,6 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; -using osu.Game.Skinning; using osuTK; using Decoder = osu.Game.Beatmaps.Formats.Decoder; @@ -353,11 +351,7 @@ namespace osu.Game.Tests.Editing using (var encoded = new MemoryStream()) { using (var sw = new StreamWriter(encoded)) - using (var rs = new ResourceStore()) - { - var skin = new LegacyBeatmapSkin(beatmap.BeatmapInfo, rs, null); - new LegacyBeatmapEncoder(beatmap, skin).Encode(sw); - } + new LegacyBeatmapEncoder(beatmap, null).Encode(sw); return encoded.ToArray(); } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 06acd4e9f2..bd757d30ca 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -195,7 +195,8 @@ namespace osu.Game.Beatmaps /// /// The to save the content against. The file referenced by will be replaced. /// The content to write. - public void Save(BeatmapInfo info, IBeatmap beatmapContent) + /// Optional beatmap skin for inline skin configuration in beatmap files. + public void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin skin) { var setInfo = QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == info.ID)); @@ -203,7 +204,6 @@ namespace osu.Game.Beatmaps { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { - var skin = new LegacyBeatmapSkin(info, Files.Store, audioManager); new LegacyBeatmapEncoder(beatmapContent, skin).Encode(sw); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index eb148794de..716f1bc814 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -24,14 +24,14 @@ namespace osu.Game.Beatmaps.Formats public const int LATEST_VERSION = 128; private readonly IBeatmap beatmap; - private readonly LegacyBeatmapSkin beatmapSkin; + private readonly ISkin skin; /// The beatmap to encode - /// An optional beatmap skin, for encoding the beatmap's combo colours. - public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] LegacyBeatmapSkin beatmapSkin) + /// An optional skin, for encoding the beatmap's combo colours. This will only work if the parameter is a type of . + public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin) { this.beatmap = beatmap; - this.beatmapSkin = beatmapSkin; + this.skin = skin; if (beatmap.BeatmapInfo.RulesetID < 0 || beatmap.BeatmapInfo.RulesetID > 3) throw new ArgumentException("Only beatmaps in the osu, taiko, catch, or mania rulesets can be encoded to the legacy beatmap format.", nameof(beatmap)); @@ -207,7 +207,10 @@ namespace osu.Game.Beatmaps.Formats private void handleComboColours(TextWriter writer) { - var colours = beatmapSkin?.Configuration.ComboColours; + if (!(skin is LegacyBeatmapSkin legacySkin)) + return; + + var colours = legacySkin?.Configuration.ComboColours; if (colours == null || colours.Count == 0) return; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d92f3922c3..d52d832bf3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -33,6 +33,7 @@ using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Play; +using osu.Game.Skinning; using osu.Game.Users; namespace osu.Game.Screens.Edit @@ -64,6 +65,7 @@ namespace osu.Game.Screens.Edit private IBeatmap playableBeatmap; private EditorBeatmap editorBeatmap; private EditorChangeHandler changeHandler; + private ISkin beatmapSkin; private DependencyContainer dependencies; @@ -92,6 +94,7 @@ namespace osu.Game.Screens.Edit try { playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + beatmapSkin = Beatmap.Value.Skin; } catch (Exception e) { @@ -104,7 +107,7 @@ namespace osu.Game.Screens.Edit AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap)); dependencies.CacheAs(editorBeatmap); - changeHandler = new EditorChangeHandler(editorBeatmap); + changeHandler = new EditorChangeHandler(editorBeatmap, beatmapSkin); dependencies.CacheAs(changeHandler); EditorMenuBar menuBar; @@ -399,7 +402,7 @@ namespace osu.Game.Screens.Edit clock.SeekForward(!clock.IsRunning, amount); } - private void saveBeatmap() => beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap); + private void saveBeatmap() => beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, Beatmap.Value.Skin); private void exportBeatmap() { diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 6393093c74..f305d2a15d 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.IO; using System.Text; using osu.Framework.Bindables; -using osu.Framework.IO.Stores; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; using osu.Game.Skinning; @@ -27,6 +26,7 @@ namespace osu.Game.Screens.Edit private int currentState = -1; private readonly EditorBeatmap editorBeatmap; + private readonly ISkin beatmapSkin; private int bulkChangesStarted; private bool isRestoring; @@ -36,7 +36,8 @@ namespace osu.Game.Screens.Edit /// Creates a new . /// /// The to track the s of. - public EditorChangeHandler(EditorBeatmap editorBeatmap) + /// The skin to track the inline skin configuration of. + public EditorChangeHandler(EditorBeatmap editorBeatmap, ISkin beatmapSkin) { this.editorBeatmap = editorBeatmap; @@ -46,6 +47,8 @@ namespace osu.Game.Screens.Edit patcher = new LegacyEditorBeatmapPatcher(editorBeatmap); + this.beatmapSkin = beatmapSkin; + // Initial state. SaveState(); } @@ -87,10 +90,8 @@ namespace osu.Game.Screens.Edit using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - using (var rs = new ResourceStore()) { - var skin = new LegacyBeatmapSkin(editorBeatmap.BeatmapInfo, rs, null); - new LegacyBeatmapEncoder(editorBeatmap, skin).Encode(sw); + new LegacyBeatmapEncoder(editorBeatmap, beatmapSkin).Encode(sw); } savedStates.Add(stream.ToArray()); From 9e4b9188e1266f1b719e6ba1212aa00ccf1ebbd8 Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Sat, 15 Aug 2020 13:06:16 -0700 Subject: [PATCH 2675/6909] Cache LoungeSubScreen, separate method, rename option --- osu.Game/Online/Multiplayer/Room.cs | 56 +++++++++++-------- .../Multi/Lounge/Components/DrawableRoom.cs | 4 +- .../Multi/Lounge/Components/RoomsContainer.cs | 11 +++- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 14 +---- 4 files changed, 45 insertions(+), 40 deletions(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 01d9446bf6..5feebe8da1 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -104,47 +104,55 @@ namespace osu.Game.Online.Multiplayer public readonly Bindable Position = new Bindable(-1); /// - /// Copies the properties from another to this room. + /// Create a copy of this room, without information specific to it, such as Room ID or host /// - /// The room to copy - /// Whether the copy should exclude information unique to a specific room (i.e. when duplicating a room) - public void CopyFrom(Room other, bool duplicate = false) + public Room CreateCopy() { - if (!duplicate) + Room newRoom = new Room { - RoomID.Value = other.RoomID.Value; + Name = { Value = Name.Value }, + Availability = { Value = Availability.Value }, + Type = { Value = Type.Value }, + MaxParticipants = { Value = MaxParticipants.Value } + }; - if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) - Host.Value = other.Host.Value; + newRoom.Playlist.AddRange(Playlist); - ChannelId.Value = other.ChannelId.Value; - Status.Value = other.Status.Value; - ParticipantCount.Value = other.ParticipantCount.Value; - EndDate.Value = other.EndDate.Value; - - if (DateTimeOffset.Now >= EndDate.Value) - Status.Value = new RoomStatusEnded(); - - if (!RecentParticipants.SequenceEqual(other.RecentParticipants)) - { - RecentParticipants.Clear(); - RecentParticipants.AddRange(other.RecentParticipants); - } - - Position.Value = other.Position.Value; - } + return newRoom; + } + public void CopyFrom(Room other) + { + RoomID.Value = other.RoomID.Value; Name.Value = other.Name.Value; + if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) + Host.Value = other.Host.Value; + + ChannelId.Value = other.ChannelId.Value; + Status.Value = other.Status.Value; Availability.Value = other.Availability.Value; Type.Value = other.Type.Value; MaxParticipants.Value = other.MaxParticipants.Value; + ParticipantCount.Value = other.ParticipantCount.Value; + EndDate.Value = other.EndDate.Value; + + if (DateTimeOffset.Now >= EndDate.Value) + Status.Value = new RoomStatusEnded(); if (!Playlist.SequenceEqual(other.Playlist)) { Playlist.Clear(); Playlist.AddRange(other.Playlist); } + + if (!RecentParticipants.SequenceEqual(other.RecentParticipants)) + { + RecentParticipants.Clear(); + RecentParticipants.AddRange(other.RecentParticipants); + } + + Position.Value = other.Position.Value; } public bool ShouldSerializeRoomID() => false; diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index 64fbae2503..db75df6054 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components public event Action StateChanged; - public Action DuplicateRoom; + public Action DuplicateRoom; private readonly Box selectionBox; private CachedModelDependencyContainer dependencies; @@ -239,7 +239,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components public MenuItem[] ContextMenuItems => new MenuItem[] { - new OsuMenuItem("Duplicate", MenuItemType.Standard, () => DuplicateRoom?.Invoke(Room)) + new OsuMenuItem("Create copy", MenuItemType.Standard, DuplicateRoom) }; } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index f112dd80ee..206ce8da0f 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -38,7 +38,8 @@ namespace osu.Game.Screens.Multi.Lounge.Components [Resolved] private IRoomManager roomManager { get; set; } - public Action DuplicateRoom; + [Resolved] + private LoungeSubScreen loungeSubScreen { get; set; } public RoomsContainer() { @@ -96,7 +97,13 @@ namespace osu.Game.Screens.Multi.Lounge.Components { roomFlow.Add(new DrawableRoom(room) { - DuplicateRoom = DuplicateRoom, + DuplicateRoom = () => + { + Room newRoom = room.CreateCopy(); + newRoom.Name.Value = $"Copy of {room.Name.Value}"; + + loungeSubScreen.Open(newRoom); + }, Action = () => { if (room == selectedRoom.Value) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index 5d68386398..a5b2499c76 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -18,6 +18,7 @@ using osu.Game.Screens.Multi.Match; namespace osu.Game.Screens.Multi.Lounge { + [Cached] public class LoungeSubScreen : MultiplayerSubScreen { public override string Title => "Lounge"; @@ -62,18 +63,7 @@ namespace osu.Game.Screens.Multi.Lounge RelativeSizeAxes = Axes.Both, ScrollbarOverlapsContent = false, Padding = new MarginPadding(10), - Child = roomsContainer = new RoomsContainer - { - JoinRequested = joinRequested, - DuplicateRoom = room => - { - Room newRoom = new Room(); - newRoom.CopyFrom(room, true); - newRoom.Name.Value = $"Copy of {room.Name.Value}"; - - Open(newRoom); - } - } + Child = roomsContainer = new RoomsContainer { JoinRequested = joinRequested } }, loadingLayer = new LoadingLayer(roomsContainer), } From 0e8411f76c156110fb7e693292d5649dd2d17265 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 15 Aug 2020 22:06:26 +0200 Subject: [PATCH 2676/6909] Rename fields and make readonly --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 38f730995e..63fdf2a8ae 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -32,14 +32,13 @@ namespace osu.Game.Tests.Beatmaps.Formats private static IEnumerable allBeatmaps = resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu")); - private static Stream beatmapSkinStream = resource_store.GetStream("skin.ini"); - + private static readonly Stream beatmap_skin_stream = resource_store.GetStream("skin.ini"); private ISkin skin; [SetUp] public void Init() { - skin = decodeSkinFromLegacy(beatmapSkinStream); + skin = decodeSkinFromLegacy(beatmap_skin_stream); } [TestCaseSource(nameof(allBeatmaps))] @@ -76,14 +75,12 @@ namespace osu.Game.Tests.Beatmaps.Formats return new LegacySkin(SkinInfo.Default, resource_store, null); } - private Stream encodeToLegacy(IBeatmap beatmap, ISkin skin) + private Stream encodeToLegacy(IBeatmap beatmap, ISkin beatmapSkin) { var stream = new MemoryStream(); using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - { - new LegacyBeatmapEncoder(beatmap, skin).Encode(writer); - } + new LegacyBeatmapEncoder(beatmap, beatmapSkin).Encode(writer); stream.Position = 0; From f5877810588dd76cd30b2ccde309e8c25cccccb7 Mon Sep 17 00:00:00 2001 From: voidedWarranties Date: Sat, 15 Aug 2020 14:27:49 -0700 Subject: [PATCH 2677/6909] Allow LoungeSubScreen to be null (fix test) --- osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index 206ce8da0f..1954d97a8f 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components [Resolved] private IRoomManager roomManager { get; set; } - [Resolved] + [Resolved(CanBeNull = true)] private LoungeSubScreen loungeSubScreen { get; set; } public RoomsContainer() @@ -100,9 +100,10 @@ namespace osu.Game.Screens.Multi.Lounge.Components DuplicateRoom = () => { Room newRoom = room.CreateCopy(); - newRoom.Name.Value = $"Copy of {room.Name.Value}"; + if (!newRoom.Name.Value.StartsWith("Copy of ")) + newRoom.Name.Value = $"Copy of {room.Name.Value}"; - loungeSubScreen.Open(newRoom); + loungeSubScreen?.Open(newRoom); }, Action = () => { From 3a6e378a08ce5fde5b23ee302f48ba4afdf4f113 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 15 Aug 2020 23:41:53 +0200 Subject: [PATCH 2678/6909] Change skin testing --- .../Formats/LegacyBeatmapEncoderTest.cs | 48 ++++++++++--------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 63fdf2a8ae..6ede30d7d8 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -32,25 +32,28 @@ namespace osu.Game.Tests.Beatmaps.Formats private static IEnumerable allBeatmaps = resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu")); - private static readonly Stream beatmap_skin_stream = resource_store.GetStream("skin.ini"); - private ISkin skin; - - [SetUp] - public void Init() - { - skin = decodeSkinFromLegacy(beatmap_skin_stream); - } - [TestCaseSource(nameof(allBeatmaps))] public void TestEncodeDecodeStability(string name) { - var decoded = decodeBeatmapFromLegacy(TestResources.GetStore().GetStream(name)); - var decodedAfterEncode = decodeBeatmapFromLegacy(encodeToLegacy(decoded, skin)); + var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name)); + var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded)); - sort(decoded); - sort(decodedAfterEncode); + sort(decoded.beatmap); + sort(decodedAfterEncode.beatmap); - Assert.That(decodedAfterEncode.Serialize(), Is.EqualTo(decoded.Serialize())); + Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); + + areSkinsEqual(decoded.beatmapSkin, decodedAfterEncode.beatmapSkin); + } + + private void areSkinsEqual(LegacySkin expected, LegacySkin actual) + { + var expectedColours = expected.Configuration.ComboColours; + var actualColours = actual.Configuration.ComboColours; + + Assert.AreEqual(expectedColours.Count, actualColours.Count); + for (int i = 0; i < expectedColours.Count; i++) + Assert.AreEqual(expectedColours[i], actualColours[i]); } private void sort(IBeatmap beatmap) @@ -63,20 +66,19 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private IBeatmap decodeBeatmapFromLegacy(Stream stream) + private (IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin) decodeFromLegacy(Stream stream) { using (var reader = new LineBufferedReader(stream)) - return convert(new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader)); + { + var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); + var beatmapSkin = new LegacyBeatmapSkin(beatmap.BeatmapInfo, resource_store, null); + return (convert(beatmap), beatmapSkin); + } } - private ISkin decodeSkinFromLegacy(Stream stream) - { - using (var reader = new LineBufferedReader(stream)) - return new LegacySkin(SkinInfo.Default, resource_store, null); - } - - private Stream encodeToLegacy(IBeatmap beatmap, ISkin beatmapSkin) + private Stream encodeToLegacy((IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin) fullBeatmap) { + var (beatmap, beatmapSkin) = fullBeatmap; var stream = new MemoryStream(); using (var writer = new StreamWriter(stream, Encoding.UTF8, 1024, true)) From 48bdbb0cfbd9f94b8d9b4182290a1dec66c7c256 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 15 Aug 2020 23:46:10 +0200 Subject: [PATCH 2679/6909] Use existing field in Editor --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d52d832bf3..a585db1ee9 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -402,7 +402,7 @@ namespace osu.Game.Screens.Edit clock.SeekForward(!clock.IsRunning, amount); } - private void saveBeatmap() => beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, Beatmap.Value.Skin); + private void saveBeatmap() => beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, beatmapSkin); private void exportBeatmap() { From 434354c44c2ab3f0bde6d0d04ade551b0a4d00ac Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 16 Aug 2020 00:21:26 +0200 Subject: [PATCH 2680/6909] Properly implement SkinConfiguration equality --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 13 +------------ osu.Game/Skinning/SkinConfiguration.cs | 12 +++++++++++- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 6ede30d7d8..66b39648e5 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -42,18 +42,7 @@ namespace osu.Game.Tests.Beatmaps.Formats sort(decodedAfterEncode.beatmap); Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); - - areSkinsEqual(decoded.beatmapSkin, decodedAfterEncode.beatmapSkin); - } - - private void areSkinsEqual(LegacySkin expected, LegacySkin actual) - { - var expectedColours = expected.Configuration.ComboColours; - var actualColours = actual.Configuration.ComboColours; - - Assert.AreEqual(expectedColours.Count, actualColours.Count); - for (int i = 0; i < expectedColours.Count; i++) - Assert.AreEqual(expectedColours[i], actualColours[i]); + Assert.IsTrue(decoded.beatmapSkin.Configuration.Equals(decodedAfterEncode.beatmapSkin.Configuration)); } private void sort(IBeatmap beatmap) diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index a55870aa6d..4b29111504 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.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 System.Collections.Generic; +using System.Linq; using osu.Game.Beatmaps.Formats; using osuTK.Graphics; @@ -10,7 +12,7 @@ namespace osu.Game.Skinning /// /// An empty skin configuration. /// - public class SkinConfiguration : IHasComboColours, IHasCustomColours + public class SkinConfiguration : IHasComboColours, IHasCustomColours, IEquatable { public readonly SkinInfo SkinInfo = new SkinInfo(); @@ -48,5 +50,13 @@ namespace osu.Game.Skinning public Dictionary CustomColours { get; set; } = new Dictionary(); public readonly Dictionary ConfigDictionary = new Dictionary(); + + public bool Equals(SkinConfiguration other) + { + return other != null && + ConfigDictionary.SequenceEqual(other.ConfigDictionary) && + ComboColours.SequenceEqual(other.ComboColours) && + CustomColours.SequenceEqual(other.CustomColours); + } } } From cfd82104dbe5f23b9a92f2129441f8a328bc80a1 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 16 Aug 2020 01:00:28 +0200 Subject: [PATCH 2681/6909] Minor changes and improvements --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 4 ++-- osu.Game/Screens/Edit/EditorChangeHandler.cs | 2 -- osu.Game/Skinning/SkinConfiguration.cs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 66b39648e5..db18f9b444 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private (IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin) decodeFromLegacy(Stream stream) + private (IBeatmap beatmap, LegacySkin beatmapSkin) decodeFromLegacy(Stream stream) { using (var reader = new LineBufferedReader(stream)) { @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private Stream encodeToLegacy((IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin) fullBeatmap) + private Stream encodeToLegacy((IBeatmap beatmap, LegacySkin beatmapSkin) fullBeatmap) { var (beatmap, beatmapSkin) = fullBeatmap; var stream = new MemoryStream(); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index f305d2a15d..0489236d45 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -90,9 +90,7 @@ namespace osu.Game.Screens.Edit using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - { new LegacyBeatmapEncoder(editorBeatmap, beatmapSkin).Encode(sw); - } savedStates.Add(stream.ToArray()); } diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index 4b29111504..a48d713771 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -56,7 +56,7 @@ namespace osu.Game.Skinning return other != null && ConfigDictionary.SequenceEqual(other.ConfigDictionary) && ComboColours.SequenceEqual(other.ComboColours) && - CustomColours.SequenceEqual(other.CustomColours); + CustomColours?.SequenceEqual(other.CustomColours) == true; } } } From f98e96e45b22b3b34e56543f2249d43e62585d5f Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Sun, 16 Aug 2020 10:52:23 +0930 Subject: [PATCH 2682/6909] Add osu!-specific enum for confine mouse mode --- osu.Game/Input/OsuConfineMouseMode.cs | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 osu.Game/Input/OsuConfineMouseMode.cs diff --git a/osu.Game/Input/OsuConfineMouseMode.cs b/osu.Game/Input/OsuConfineMouseMode.cs new file mode 100644 index 0000000000..32b456395c --- /dev/null +++ b/osu.Game/Input/OsuConfineMouseMode.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; +using osu.Framework.Input; + +namespace osu.Game.Input +{ + /// + /// Determines the situations in which the mouse cursor should be confined to the window. + /// Expands upon by providing the option to confine during gameplay. + /// + public enum OsuConfineMouseMode + { + /// + /// The mouse cursor will be free to move outside the game window. + /// + Never, + + /// + /// The mouse cursor will be locked to the window bounds while in fullscreen mode. + /// + Fullscreen, + + /// + /// The mouse cursor will be locked to the window bounds during gameplay, + /// but may otherwise move freely. + /// + [Description("During Gameplay")] + DuringGameplay, + + /// + /// The mouse cursor will always be locked to the window bounds while the game has focus. + /// + Always + } +} From 322d179076a383cf7fd2e7506e27189fba025278 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Sun, 16 Aug 2020 11:04:28 +0930 Subject: [PATCH 2683/6909] Replace settings item with osu! confine cursor mode --- osu.Game/Configuration/OsuConfigManager.cs | 3 +++ osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 6 +++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a8a8794320..9ef846c974 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -6,6 +6,7 @@ using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; using osu.Framework.Platform; +using osu.Game.Input; using osu.Game.Overlays; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; @@ -66,6 +67,7 @@ namespace osu.Game.Configuration Set(OsuSetting.MouseDisableButtons, false); Set(OsuSetting.MouseDisableWheel, false); + Set(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay); // Graphics Set(OsuSetting.ShowFpsDisplay, false); @@ -191,6 +193,7 @@ namespace osu.Game.Configuration FadePlayfieldWhenHealthLow, MouseDisableButtons, MouseDisableWheel, + ConfineMouseMode, AudioOffset, VolumeInactive, MenuMusic, diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index d27ab63fb7..0d98508e3b 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -6,9 +6,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Framework.Input; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Input; namespace osu.Game.Overlays.Settings.Sections.Input { @@ -47,10 +47,10 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = "Map absolute input to window", Bindable = config.GetBindable(FrameworkSetting.MapAbsoluteInputToWindow) }, - new SettingsEnumDropdown + new SettingsEnumDropdown { LabelText = "Confine mouse cursor to window", - Bindable = config.GetBindable(FrameworkSetting.ConfineMouseMode), + Bindable = osuConfig.GetBindable(OsuSetting.ConfineMouseMode) }, new SettingsCheckbox { From 3d6d22f70fbdc37b960d3cbc1bd90f78ba0fcb6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Aug 2020 12:39:41 +0200 Subject: [PATCH 2684/6909] Adjust README.md to read better --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index dc3ee63844..d3e9ca5121 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,9 @@ [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) -Rhythm is just a *click* away. The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew. +A free-to-win rhythm game. Rhythm is just a *click* away! + +The future of [osu!](https://osu.ppy.sh) and the beginning of an open era! Commonly known by the codename *osu!lazer*. Pew pew. ## Status From 6c44513115ec085bcdc181b3dd04a369130b6e51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 16 Aug 2020 12:53:31 +0200 Subject: [PATCH 2685/6909] Update .csproj descriptions to match --- osu.Desktop/osu.Desktop.csproj | 2 +- osu.Desktop/osu.nuspec | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 7a99c70999..62e8f7c518 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -3,7 +3,7 @@ netcoreapp3.1 WinExe true - click the circles. to the beat. + A free-to-win rhythm game. Rhythm is just a *click* away! osu! osu!lazer osu!lazer diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec index a919d54f38..2fc6009183 100644 --- a/osu.Desktop/osu.nuspec +++ b/osu.Desktop/osu.nuspec @@ -9,8 +9,7 @@ https://osu.ppy.sh/ https://puu.sh/tYyXZ/9a01a5d1b0.ico false - click the circles. to the beat. - click the circles. + A free-to-win rhythm game. Rhythm is just a *click* away! testing Copyright (c) 2020 ppy Pty Ltd en-AU From ef3c8fa21f8105ec181be6392bd65c929a597f40 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Sun, 16 Aug 2020 11:38:35 +0930 Subject: [PATCH 2686/6909] Add tracking component to handle OsuConfineMouseMode --- osu.Game/Input/ConfineMouseTracker.cs | 72 +++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 osu.Game/Input/ConfineMouseTracker.cs diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs new file mode 100644 index 0000000000..b111488a5b --- /dev/null +++ b/osu.Game/Input/ConfineMouseTracker.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Configuration; +using osu.Framework.Graphics; +using osu.Framework.Input; +using osu.Game.Configuration; +using osu.Game.Screens.Play; + +namespace osu.Game.Input +{ + /// + /// Connects with + /// while providing a property for to indicate whether gameplay is currently active. + /// + public class ConfineMouseTracker : Component + { + private Bindable frameworkConfineMode; + private Bindable osuConfineMode; + + private bool gameplayActive; + + /// + /// Indicates whether osu! is currently considered "in gameplay" for the + /// purposes of . + /// + public bool GameplayActive + { + get => gameplayActive; + set + { + if (gameplayActive == value) + return; + + gameplayActive = value; + updateConfineMode(); + } + } + + [BackgroundDependencyLoader] + private void load(FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) + { + frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); + osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); + osuConfineMode.BindValueChanged(_ => updateConfineMode(), true); + } + + private void updateConfineMode() + { + switch (osuConfineMode.Value) + { + case OsuConfineMouseMode.Never: + frameworkConfineMode.Value = ConfineMouseMode.Never; + break; + + case OsuConfineMouseMode.Fullscreen: + frameworkConfineMode.Value = ConfineMouseMode.Fullscreen; + break; + + case OsuConfineMouseMode.DuringGameplay: + frameworkConfineMode.Value = GameplayActive ? ConfineMouseMode.Always : ConfineMouseMode.Never; + break; + + case OsuConfineMouseMode.Always: + frameworkConfineMode.Value = ConfineMouseMode.Always; + break; + } + } + } +} From 00f15231bc78a6e7830694bf41cbc1db331e505f Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Sun, 16 Aug 2020 21:52:39 +0930 Subject: [PATCH 2687/6909] Cache ConfineMouseTracker --- osu.Game/OsuGame.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 053eb01dcd..7358918758 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -88,6 +88,8 @@ namespace osu.Game private IdleTracker idleTracker; + private ConfineMouseTracker confineMouseTracker; + public readonly Bindable OverlayActivationMode = new Bindable(); protected OsuScreenStack ScreenStack; @@ -553,6 +555,7 @@ namespace osu.Game BackButton.Receptor receptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); + dependencies.Cache(confineMouseTracker = new ConfineMouseTracker()); AddRange(new Drawable[] { @@ -588,7 +591,8 @@ namespace osu.Game rightFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, topMostOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, - idleTracker + idleTracker, + confineMouseTracker }); ScreenStack.ScreenPushed += screenPushed; From 85b3fff9c8a695d89453b0dbd1b55eb0d27fe5e4 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Sun, 16 Aug 2020 23:11:09 +0930 Subject: [PATCH 2688/6909] Update mouse confine when gameplay state changes --- osu.Game/Screens/Play/Player.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..3b8c4aea01 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -18,6 +18,7 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; +using osu.Game.Input; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Overlays; @@ -63,6 +64,9 @@ namespace osu.Game.Screens.Play private Bindable mouseWheelDisabled; + [Resolved(CanBeNull = true)] + private ConfineMouseTracker confineMouseTracker { get; set; } + private readonly Bindable storyboardReplacesBackground = new Bindable(); public int RestartCount; @@ -197,10 +201,15 @@ namespace osu.Game.Screens.Play skipOverlay.Hide(); } - DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => + { + updatePauseOnFocusLostState(); + updateConfineMouse(); + }, true); // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); + DrawableRuleset.IsPaused.ValueChanged += _ => updateConfineMouse(); DrawableRuleset.OnNewResult += r => { @@ -346,6 +355,12 @@ namespace osu.Game.Screens.Play && !DrawableRuleset.HasReplayLoaded.Value && !breakTracker.IsBreakTime.Value; + private void updateConfineMouse() + { + if (confineMouseTracker != null) + confineMouseTracker.GameplayActive = !GameplayClockContainer.IsPaused.Value && !DrawableRuleset.HasReplayLoaded.Value && !HasFailed; + } + private IBeatmap loadPlayableBeatmap() { IBeatmap playable; @@ -379,7 +394,7 @@ namespace osu.Game.Screens.Play } catch (Exception e) { - Logger.Error(e, "Could not load beatmap sucessfully!"); + Logger.Error(e, "Could not load beatmap successfully!"); //couldn't load, hard abort! return null; } From a6708c4286d3973c8ed68be43176228da1137237 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 16 Aug 2020 23:04:49 +0900 Subject: [PATCH 2689/6909] Rename resolved variable in MainMenu --- osu.Game/Screens/Menu/MainMenu.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 470e8ca9a6..fac9e9eb49 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Menu private GameHost host { get; set; } [Resolved] - private MusicController music { get; set; } + private MusicController musicController { get; set; } [Resolved(canBeNull: true)] private LoginOverlay login { get; set; } @@ -176,12 +176,12 @@ namespace osu.Game.Screens.Menu var metadata = Beatmap.Value.Metadata; - if (last is IntroScreen && music.TrackLoaded) + if (last is IntroScreen && musicController.TrackLoaded) { - if (!music.CurrentTrack.IsRunning) + if (!musicController.CurrentTrack.IsRunning) { - music.CurrentTrack.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * music.CurrentTrack.Length); - music.CurrentTrack.Start(); + musicController.CurrentTrack.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * musicController.CurrentTrack.Length); + musicController.CurrentTrack.Start(); } } @@ -256,7 +256,7 @@ namespace osu.Game.Screens.Menu // we may have consumed our preloaded instance, so let's make another. preloadSongSelect(); - music.EnsurePlayingSomething(); + musicController.EnsurePlayingSomething(); } public override bool OnExiting(IScreen next) From 5d433c0b055d3c284b1737df65856f5e118a817f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 16 Aug 2020 23:11:29 +0900 Subject: [PATCH 2690/6909] Fix a couple of new Resharper inspections --- osu.Game/Screens/Menu/ButtonSystem.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 30e5e9702e..5ba7a8ddc3 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -324,10 +324,9 @@ namespace osu.Game.Screens.Menu bool impact = logo.Scale.X > 0.6f; - if (lastState == ButtonSystemState.Initial) - logo.ScaleTo(0.5f, 200, Easing.In); + logo.ScaleTo(0.5f, 200, Easing.In); - logoTrackingContainer.StartTracking(logo, lastState == ButtonSystemState.EnteringMode ? 0 : 200, Easing.In); + logoTrackingContainer.StartTracking(logo, 200, Easing.In); logoDelayedAction?.Cancel(); logoDelayedAction = Scheduler.AddDelayed(() => From 589d4eeb5297a046b7001cb7c55a2e9b5c3ea824 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 16 Aug 2020 17:18:40 +0200 Subject: [PATCH 2691/6909] Remove setting. --- .../Visual/Gameplay/TestSceneOverlayActivation.cs | 12 ------------ osu.Game/Configuration/OsuConfigManager.cs | 2 -- .../Settings/Sections/Gameplay/GeneralSettings.cs | 5 ----- osu.Game/Screens/Play/Player.cs | 14 ++++++-------- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 04c67433fa..3ee0f4e720 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -3,7 +3,6 @@ using System.Linq; using NUnit.Framework; -using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -16,21 +15,12 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestGameplayOverlayActivation() { - AddStep("disable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, true)); AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); } - [Test] - public void TestGameplayOverlayActivationDisabled() - { - AddStep("enable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, false)); - AddAssert("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); - } - [Test] public void TestGameplayOverlayActivationPaused() { - AddStep("disable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, true)); AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddStep("pause gameplay", () => Player.Pause()); AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); @@ -39,7 +29,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestGameplayOverlayActivationReplayLoaded() { - AddStep("disable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, true)); AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddStep("load a replay", () => Player.DrawableRuleset.HasReplayLoaded.Value = true); AddAssert("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); @@ -48,7 +37,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestGameplayOverlayActivationBreaks() { - AddStep("disable overlay activation during gameplay", () => LocalConfig.Set(OsuSetting.GameplayDisableOverlayActivation, true)); AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddStep("seek to break", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime)); AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 44c0fbde82..d49c78183e 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -100,7 +100,6 @@ namespace osu.Game.Configuration Set(OsuSetting.IncreaseFirstObjectVisibility, true); Set(OsuSetting.GameplayDisableWinKey, true); - Set(OsuSetting.GameplayDisableOverlayActivation, true); // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -233,6 +232,5 @@ namespace osu.Game.Configuration HitLighting, MenuBackgroundSource, GameplayDisableWinKey, - GameplayDisableOverlayActivation } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index c2e668fe68..0149e6c3a6 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -77,11 +77,6 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay { LabelText = "Score display mode", Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) - }, - new SettingsCheckbox - { - LabelText = "Disable overlays during gameplay", - Bindable = config.GetBindable(OsuSetting.GameplayDisableOverlayActivation) } }; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 6bb4be4096..17838e0502 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -69,8 +69,6 @@ namespace osu.Game.Screens.Play private Bindable mouseWheelDisabled; - private Bindable gameplayOverlaysDisabled; - private readonly Bindable storyboardReplacesBackground = new Bindable(); public int RestartCount; @@ -176,7 +174,6 @@ namespace osu.Game.Screens.Play sampleRestart = audio.Samples.Get(@"Gameplay/restart"); mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); - gameplayOverlaysDisabled = config.GetBindable(OsuSetting.GameplayDisableOverlayActivation); DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); @@ -212,6 +209,10 @@ namespace osu.Game.Screens.Play if (game != null) OverlayActivationMode.BindTo(game.OverlayActivationMode); + DrawableRuleset.IsPaused.BindValueChanged(_ => updateOverlayActivationMode()); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateOverlayActivationMode()); + breakTracker.IsBreakTime.BindValueChanged(_ => updateOverlayActivationMode()); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); // bind clock into components that require it @@ -358,7 +359,7 @@ namespace osu.Game.Screens.Play private void updateOverlayActivationMode() { - bool canTriggerOverlays = DrawableRuleset.IsPaused.Value || breakTracker.IsBreakTime.Value || !gameplayOverlaysDisabled.Value; + bool canTriggerOverlays = DrawableRuleset.IsPaused.Value || breakTracker.IsBreakTime.Value; if (DrawableRuleset.HasReplayLoaded.Value || canTriggerOverlays) OverlayActivationMode.Value = OverlayActivation.UserTriggered; @@ -653,10 +654,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHUD(HUDOverlay); - DrawableRuleset.IsPaused.BindValueChanged(_ => updateOverlayActivationMode()); - DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateOverlayActivationMode()); - breakTracker.IsBreakTime.BindValueChanged(_ => updateOverlayActivationMode()); - gameplayOverlaysDisabled.BindValueChanged(_ => updateOverlayActivationMode(), true); + updateOverlayActivationMode(); } public override void OnSuspending(IScreen next) From 948c3cfbf1fe65a5974b13f555e84f2fd3566db1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Aug 2020 14:56:05 +0900 Subject: [PATCH 2692/6909] Improve visibility of toolbar tooltips against bright backgrounds --- 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 5bdd86c671..beac6adc59 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -118,9 +118,9 @@ namespace osu.Game.Overlays.Toolbar RelativeSizeAxes = Axes.X, Anchor = Anchor.BottomLeft, Alpha = 0, - Height = 90, + Height = 100, Colour = ColourInfo.GradientVertical( - OsuColour.Gray(0.1f).Opacity(0.5f), OsuColour.Gray(0.1f).Opacity(0)), + OsuColour.Gray(0).Opacity(0.9f), OsuColour.Gray(0).Opacity(0)), }, }; } From d9debef1568a393f0da721759d09206ffe5c3701 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Aug 2020 15:38:16 +0900 Subject: [PATCH 2693/6909] Add explicit LoadTrack method --- .../TestSceneGameplayClockContainer.cs | 4 +++- .../Gameplay/TestSceneStoryboardSamples.cs | 5 ++-- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 3 ++- osu.Game/Beatmaps/IWorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 23 ++++++++++++++++++- osu.Game/Overlays/MusicController.cs | 2 +- osu.Game/Screens/Edit/EditorClock.cs | 2 +- osu.Game/Screens/Menu/IntroScreen.cs | 2 +- .../Screens/Play/GameplayClockContainer.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 2 +- 10 files changed, 37 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs index bb60ae73db..dc9f540907 100644 --- a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs @@ -22,7 +22,9 @@ namespace osu.Game.Tests.Gameplay AddStep("create container", () => { var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - Add(gcc = new GameplayClockContainer(working.GetTrack(), working, Array.Empty(), 0)); + working.LoadTrack(); + + Add(gcc = new GameplayClockContainer(working, Array.Empty(), 0)); }); AddStep("start track", () => gcc.Start()); diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 360e7eccdc..6f788a070e 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -60,8 +60,9 @@ namespace osu.Game.Tests.Gameplay AddStep("create container", () => { var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); + working.LoadTrack(); - Add(gameplayContainer = new GameplayClockContainer(working.GetTrack(), working, Array.Empty(), 0)); + Add(gameplayContainer = new GameplayClockContainer(working, Array.Empty(), 0)); gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) { @@ -105,7 +106,7 @@ namespace osu.Game.Tests.Gameplay Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); SelectedMods.Value = new[] { testedMod }; - Add(gameplayContainer = new GameplayClockContainer(MusicController.CurrentTrack, Beatmap.Value, SelectedMods.Value, 0)); + Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0)); gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index 58fd760fc3..c7e5e2a7ec 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -33,8 +33,9 @@ namespace osu.Game.Tests.Visual.Gameplay increment = skip_time; var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); + working.LoadTrack(); - Child = gameplayClockContainer = new GameplayClockContainer(working.GetTrack(), working, Array.Empty(), 0) + Child = gameplayClockContainer = new GameplayClockContainer(working, Array.Empty(), 0) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index e020625b99..88d73fd7c4 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -58,6 +58,6 @@ namespace osu.Game.Beatmaps /// /// Retrieves the which this provides. /// - Track GetTrack(); + Track LoadTrack(); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index bec2679103..b4046a4e95 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -250,8 +250,29 @@ namespace osu.Game.Beatmaps protected abstract Texture GetBackground(); private readonly RecyclableLazy background; + private Track loadedTrack; + + /// + /// Load a new audio track instance for this beatmap. + /// + /// A fresh track instance, which will also be available via . [NotNull] - public Track GetTrack() => GetBeatmapTrack() ?? GetVirtualTrack(1000); + public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000); + + /// + /// Get the loaded audio track instance. must have first been called. + /// This generally happens via MusicController when changing the global beatmap. + /// + public Track Track + { + get + { + if (loadedTrack == null) + throw new InvalidOperationException($"Cannot access {nameof(Track)} without first calling {nameof(LoadTrack)}."); + + return loadedTrack; + } + } protected abstract Track GetBeatmapTrack(); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index c18b564b4f..8bbae33811 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -319,7 +319,7 @@ namespace osu.Game.Overlays { var lastTrack = CurrentTrack; - var newTrack = new DrawableTrack(current.GetTrack()); + var newTrack = new DrawableTrack(current.LoadTrack()); newTrack.Completed += () => onTrackCompleted(current); CurrentTrack = newTrack; diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index d0e265adb0..634a6f7e25 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Edit private readonly DecoupleableInterpolatingFramedClock underlyingClock; public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) - : this(beatmap.Beatmap.ControlPointInfo, beatmap.GetTrack().Length, beatDivisor) + : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor) { } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 389629445c..3e4320ae44 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Menu if (setInfo != null) { initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - UsingThemedIntro = !initialBeatmap.GetTrack().IsDummyDevice; + UsingThemedIntro = !initialBeatmap.LoadTrack().IsDummyDevice; } return UsingThemedIntro; diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index f0bbcf957a..50a7331e4f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -62,12 +62,12 @@ namespace osu.Game.Screens.Play private readonly FramedOffsetClock platformOffsetClock; - public GameplayClockContainer([NotNull] ITrack track, WorkingBeatmap beatmap, IReadOnlyList mods, double gameplayStartTime) + public GameplayClockContainer(WorkingBeatmap beatmap, IReadOnlyList mods, double gameplayStartTime) { this.beatmap = beatmap; this.mods = mods; this.gameplayStartTime = gameplayStartTime; - this.track = track; + track = beatmap.Track; firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e92164de7c..ccdd4ea8a4 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -179,7 +179,7 @@ namespace osu.Game.Screens.Play if (!ScoreProcessor.Mode.Disabled) config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); - InternalChild = GameplayClockContainer = new GameplayClockContainer(musicController.CurrentTrack, Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime); + InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime); AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); From 93a8bc3d5aa330cb6ef008ac8858ae4d005434aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 17 Aug 2020 22:36:03 +0900 Subject: [PATCH 2694/6909] Remove local reset method in GameplayClockContainer --- .../TestSceneGameplayClockContainer.cs | 4 +-- .../Gameplay/TestSceneStoryboardSamples.cs | 4 +-- .../Visual/Gameplay/TestSceneSkipOverlay.cs | 4 +-- .../Screens/Play/GameplayClockContainer.cs | 31 +++++++------------ osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Tests/Visual/PlayerTestScene.cs | 2 ++ 6 files changed, 19 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs index dc9f540907..891537c4ad 100644 --- a/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.cs +++ b/osu.Game.Tests/Gameplay/TestSceneGameplayClockContainer.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 NUnit.Framework; using osu.Framework.Testing; -using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; using osu.Game.Tests.Visual; @@ -24,7 +22,7 @@ namespace osu.Game.Tests.Gameplay var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gcc = new GameplayClockContainer(working, Array.Empty(), 0)); + Add(gcc = new GameplayClockContainer(working, 0)); }); AddStep("start track", () => gcc.Start()); diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 6f788a070e..a690eb3b59 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Gameplay var working = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); working.LoadTrack(); - Add(gameplayContainer = new GameplayClockContainer(working, Array.Empty(), 0)); + Add(gameplayContainer = new GameplayClockContainer(working, 0)); gameplayContainer.Add(sample = new DrawableStoryboardSample(new StoryboardSampleInfo(string.Empty, 0, 1)) { @@ -106,7 +106,7 @@ namespace osu.Game.Tests.Gameplay Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); SelectedMods.Value = new[] { testedMod }; - Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, SelectedMods.Value, 0)); + Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0)); gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs index c7e5e2a7ec..841722a8f1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkipOverlay.cs @@ -1,11 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using 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; using osuTK; @@ -35,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay var working = CreateWorkingBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); working.LoadTrack(); - Child = gameplayClockContainer = new GameplayClockContainer(working, Array.Empty(), 0) + Child = gameplayClockContainer = new GameplayClockContainer(working, 0) { RelativeSizeAxes = Axes.Both, Children = new Drawable[] diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 50a7331e4f..7a9cb3dddd 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.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.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -16,7 +15,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Play { @@ -26,7 +24,6 @@ namespace osu.Game.Screens.Play public class GameplayClockContainer : Container { private readonly WorkingBeatmap beatmap; - private readonly IReadOnlyList mods; [NotNull] private ITrack track; @@ -62,10 +59,9 @@ namespace osu.Game.Screens.Play private readonly FramedOffsetClock platformOffsetClock; - public GameplayClockContainer(WorkingBeatmap beatmap, IReadOnlyList mods, double gameplayStartTime) + public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime) { this.beatmap = beatmap; - this.mods = mods; this.gameplayStartTime = gameplayStartTime; track = beatmap.Track; @@ -122,13 +118,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(); + track.Seek(0); + track.Stop(); Schedule(() => { @@ -216,14 +209,13 @@ namespace osu.Game.Screens.Play private void updateRate() { - speedAdjustmentsApplied = true; - track.ResetSpeedAdjustments(); + if (speedAdjustmentsApplied) + return; track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); - foreach (var mod in mods.OfType()) - mod.ApplyToTrack(track); + speedAdjustmentsApplied = true; } protected override void Dispose(bool isDisposing) @@ -234,11 +226,12 @@ namespace osu.Game.Screens.Play private void removeSourceClockAdjustments() { - if (speedAdjustmentsApplied) - { - track.ResetSpeedAdjustments(); - speedAdjustmentsApplied = false; - } + if (!speedAdjustmentsApplied) return; + + track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); + track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + + speedAdjustmentsApplied = false; } private class HardwareCorrectionOffsetClock : FramedOffsetClock diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index ccdd4ea8a4..cc70995b26 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -179,7 +179,7 @@ namespace osu.Game.Screens.Play if (!ScoreProcessor.Mode.Disabled) config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); - InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, Mods.Value, DrawableRuleset.GameplayStartTime); + InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 2c46e7f6d3..7d06c99133 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -29,6 +29,8 @@ namespace osu.Game.Tests.Visual { Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage)); LocalConfig.GetBindable(OsuSetting.DimLevel).Value = 1.0; + + MusicController.AllowRateAdjustments = true; } [SetUpSteps] From 548ccc1a50694b931924d7cc0b8d0e49803f3037 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 18 Aug 2020 00:29:00 +0900 Subject: [PATCH 2695/6909] Initial implementation of hold note freezing --- .../Objects/Drawables/DrawableHoldNote.cs | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 0c5289efe1..39b1771643 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; @@ -11,6 +12,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -32,6 +34,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly Container tailContainer; private readonly Container tickContainer; + private readonly Container bodyPieceContainer; private readonly Drawable bodyPiece; /// @@ -44,19 +47,25 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public bool HasBroken { get; private set; } + /// + /// Whether the hold note has been released potentially without having caused a break. + /// + private bool hasReleased; + public DrawableHoldNote(HoldNote hitObject) : base(hitObject) { RelativeSizeAxes = Axes.X; - AddRangeInternal(new[] + AddRangeInternal(new Drawable[] { - bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece + bodyPieceContainer = new Container { - RelativeSizeAxes = Axes.Both - }) - { - RelativeSizeAxes = Axes.X + RelativeSizeAxes = Axes.X, + Child = bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece + { + RelativeSizeAxes = Axes.Both + }) }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, @@ -127,7 +136,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.OnDirectionChanged(e); - bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + bodyPieceContainer.Anchor = bodyPieceContainer.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + bodyPieceContainer.Anchor = bodyPieceContainer.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.BottomLeft : Anchor.TopLeft; } public override void PlaySamples() @@ -140,8 +150,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables base.Update(); // Make the body piece not lie under the head note - bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; - bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; + bodyPieceContainer.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; + bodyPieceContainer.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; + + if (Head.IsHit && !hasReleased) + { + float heightDecrease = (float)(Math.Max(0, Time.Current - HitObject.StartTime) / HitObject.Duration); + bodyPiece.Height = MathHelper.Clamp(1 - heightDecrease, 0, 1); + } } protected override void UpdateStateTransforms(ArmedState state) @@ -206,6 +222,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // If the key has been released too early, the user should not receive full score for the release if (!Tail.IsHit) HasBroken = true; + + hasReleased = true; } private void endHold() From b969bc03e0d48b91c1c17eec744b948945dec70a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Aug 2020 00:47:32 +0900 Subject: [PATCH 2696/6909] Add loading spinner while editor screen loads --- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index e9ff0b5598..67442aa55e 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK.Graphics; @@ -32,6 +33,8 @@ namespace osu.Game.Screens.Edit Container mainContent; + LoadingSpinner spinner; + Children = new Drawable[] { mainContent = new Container @@ -44,6 +47,10 @@ namespace osu.Game.Screens.Edit Top = vertical_margins + timeline_height, Bottom = vertical_margins }, + Child = spinner = new LoadingSpinner(true) + { + State = { Value = Visibility.Visible }, + }, }, new Container { @@ -87,9 +94,10 @@ namespace osu.Game.Screens.Edit } }, }; - LoadComponentAsync(CreateMainContent(), content => { + spinner.State.Value = Visibility.Hidden; + mainContent.Add(content); content.FadeInFromZero(300, Easing.OutQuint); From 583760100a633b037c5194cf8b2e0cae3820af40 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 18 Aug 2020 01:40:55 +0900 Subject: [PATCH 2697/6909] Implement mania invert mod --- .../Mods/TestSceneManiaModInvert.cs | 21 ++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 1 + .../Mods/ManiaModInvert.cs | 68 +++++++++++++++++++ osu.Game.Rulesets.Mania/Objects/HoldNote.cs | 6 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 9 +++ .../Mods/IApplicableAfterBeatmapConversion.cs | 19 ++++++ 6 files changed, 121 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs create mode 100644 osu.Game/Rulesets/Mods/IApplicableAfterBeatmapConversion.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.cs new file mode 100644 index 0000000000..f2cc254e38 --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModInvert.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 NUnit.Framework; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public class TestSceneManiaModInvert : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Test] + public void TestInversion() => CreateModTest(new ModTestData + { + Mod = new ManiaModInvert(), + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 2 + }); + } +} diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 68dce8b139..2795868c97 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -220,6 +220,7 @@ namespace osu.Game.Rulesets.Mania new ManiaModDualStages(), new ManiaModMirror(), new ManiaModDifficultyAdjust(), + new ManiaModInvert(), }; case ModType.Automation: diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs new file mode 100644 index 0000000000..2fb7a75141 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Game.Audio; +using osu.Game.Beatmaps; +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 ManiaModInvert : Mod, IApplicableAfterBeatmapConversion + { + public override string Name => "Invert"; + + public override string Acronym => "IN"; + public override double ScoreMultiplier => 1; + + public override string Description => "Hold the keys. To the beat."; + + public override ModType Type => ModType.Conversion; + + public void ApplyToBeatmap(IBeatmap beatmap) + { + var maniaBeatmap = (ManiaBeatmap)beatmap; + + var newObjects = new List(); + + foreach (var column in maniaBeatmap.HitObjects.GroupBy(h => h.Column)) + { + var newColumnObjects = new List(); + + var locations = column.OfType().Select(n => (startTime: n.StartTime, samples: n.Samples)) + .Concat(column.OfType().SelectMany(h => new[] + { + (startTime: h.StartTime, samples: h.GetNodeSamples(0)), + (startTime: h.EndTime, samples: h.GetNodeSamples(1)) + })) + .OrderBy(h => h.startTime).ToList(); + + for (int i = 0; i < locations.Count - 1; i += 2) + { + newColumnObjects.Add(new HoldNote + { + Column = column.Key, + StartTime = locations[i].startTime, + Duration = locations[i + 1].startTime - locations[i].startTime, + Samples = locations[i].samples, + NodeSamples = new List> + { + locations[i].samples, + locations[i + 1].samples + } + }); + } + + newObjects.AddRange(newColumnObjects); + } + + maniaBeatmap.HitObjects = newObjects.OrderBy(h => h.StartTime).ToList(); + + // No breaks + maniaBeatmap.Breaks.Clear(); + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs index a100c9a58e..6cc7ff92d3 100644 --- a/osu.Game.Rulesets.Mania/Objects/HoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/HoldNote.cs @@ -102,14 +102,14 @@ namespace osu.Game.Rulesets.Mania.Objects { StartTime = StartTime, Column = Column, - Samples = getNodeSamples(0), + Samples = GetNodeSamples(0), }); AddNested(Tail = new TailNote { StartTime = EndTime, Column = Column, - Samples = getNodeSamples((NodeSamples?.Count - 1) ?? 1), + Samples = GetNodeSamples((NodeSamples?.Count - 1) ?? 1), }); } @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mania.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; - private IList getNodeSamples(int nodeIndex) => + public IList GetNodeSamples(int nodeIndex) => nodeIndex < NodeSamples?.Count ? NodeSamples[nodeIndex] : Samples; } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index ac399e37c4..b4bcf285b9 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -109,6 +109,15 @@ namespace osu.Game.Beatmaps // Convert IBeatmap converted = converter.Convert(); + // Apply conversion mods to the result + foreach (var mod in mods.OfType()) + { + if (cancellationSource.IsCancellationRequested) + throw new BeatmapLoadTimeoutException(BeatmapInfo); + + mod.ApplyToBeatmap(converted); + } + // Apply difficulty mods if (mods.Any(m => m is IApplicableToDifficulty)) { diff --git a/osu.Game/Rulesets/Mods/IApplicableAfterBeatmapConversion.cs b/osu.Game/Rulesets/Mods/IApplicableAfterBeatmapConversion.cs new file mode 100644 index 0000000000..d45311675d --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableAfterBeatmapConversion.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.Beatmaps; + +namespace osu.Game.Rulesets.Mods +{ + /// + /// Interface for a that applies changes to the generated by the . + /// + public interface IApplicableAfterBeatmapConversion : IApplicableMod + { + /// + /// Applies this to the after conversion has taken place. + /// + /// The converted . + void ApplyToBeatmap(IBeatmap beatmap); + } +} From 9e8192e31d0237043743792ddb3cd75cddd86804 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2020 17:14:51 +0000 Subject: [PATCH 2698/6909] Bump Microsoft.CodeAnalysis.BannedApiAnalyzers from 3.0.0 to 3.3.0 Bumps [Microsoft.CodeAnalysis.BannedApiAnalyzers](https://github.com/dotnet/roslyn-analyzers) from 3.0.0 to 3.3.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/v3.0.0...v3.3.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 2cd40c8675..2d3478f256 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,7 +16,7 @@ - + From e4303d79436113148cbe505655e5e0f151a6cbc8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Aug 2020 12:35:23 +0900 Subject: [PATCH 2699/6909] Fix PlayerLoader test failures due to too many steps --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index e698d31176..4fac7bb45f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -71,7 +71,6 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); AddStep("exit loader", () => loader.Exit()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); AddAssert("player did not load", () => player == null); From 083bcde3cf260147304b110d4a0956285690e4ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 18 Aug 2020 13:01:35 +0900 Subject: [PATCH 2700/6909] Fix beatmap transfer not working --- osu.Game/Beatmaps/WorkingBeatmap.cs | 7 +++++++ osu.Game/Overlays/MusicController.cs | 13 ++++++++++--- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index b4046a4e95..c9a60d7948 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -259,6 +259,13 @@ namespace osu.Game.Beatmaps [NotNull] public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000); + /// + /// Transfer a valid audio track into this working beatmap. Used as an optimisation to avoid reload / track swap + /// across difficulties in the same beatmap set. + /// + /// The track to transfer. + public void TransferTrack([NotNull] Track track) => loadedTrack = track ?? throw new ArgumentNullException(nameof(track)); + /// /// Get the loaded audio track instance. must have first been called. /// This generally happens via MusicController when changing the global beatmap. diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 8bbae33811..c1116ff651 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -282,10 +282,10 @@ namespace osu.Game.Overlays { TrackChangeDirection direction = TrackChangeDirection.None; + bool audioEquals = beatmap.NewValue?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) ?? false; + if (current != null) { - bool audioEquals = beatmap.NewValue?.BeatmapInfo?.AudioEquals(current.BeatmapInfo) ?? false; - if (audioEquals) direction = TrackChangeDirection.None; else if (queuedDirection.HasValue) @@ -305,8 +305,15 @@ namespace osu.Game.Overlays current = beatmap.NewValue; - if (CurrentTrack.IsDummyDevice || !beatmap.OldValue.BeatmapInfo.AudioEquals(current?.BeatmapInfo)) + if (!audioEquals || CurrentTrack.IsDummyDevice) + { changeTrack(); + } + else + { + // transfer still valid track to new working beatmap + current.TransferTrack(beatmap.OldValue.Track); + } TrackChanged?.Invoke(current, direction); From 848f3bbf51b0b44d48f7424d4a48fc0bc3e369a7 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 17 Aug 2020 21:09:55 -0700 Subject: [PATCH 2701/6909] Show tooltip of leaderboard score rank when 1000 or higher --- .../Online/Leaderboards/LeaderboardScore.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index b60d71cfe7..662c02df0e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -82,20 +82,10 @@ namespace osu.Game.Online.Leaderboards Children = new Drawable[] { - new Container + new RankLabel(rank) { RelativeSizeAxes = Axes.Y, Width = rank_width, - Children = new[] - { - new OsuSpriteText - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Font = OsuFont.GetFont(size: 20, italics: true), - Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0), - }, - }, }, content = new Container { @@ -356,6 +346,24 @@ namespace osu.Game.Online.Leaderboards } } + private class RankLabel : Container, IHasTooltip + { + public RankLabel(int? rank) + { + TooltipText = rank == null || rank < 1000 ? null : $"{rank}"; + + Child = new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Font = OsuFont.GetFont(size: 20, italics: true), + Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0), + }; + } + + public string TooltipText { get; } + } + public class LeaderboardScoreStatistic { public IconUsage Icon; From e0383f61008db34bbcc3317e9ad12ff94f56ec7b Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 17 Aug 2020 22:07:04 -0700 Subject: [PATCH 2702/6909] Change format of rank tooltip --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 662c02df0e..a4c20d1b9e 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -350,7 +350,7 @@ namespace osu.Game.Online.Leaderboards { public RankLabel(int? rank) { - TooltipText = rank == null || rank < 1000 ? null : $"{rank}"; + TooltipText = rank == null || rank < 1000 ? null : $"#{rank:N0}"; Child = new OsuSpriteText { From 4d6b52a0d6a9a9d8e3d847c8d610d978b80d7802 Mon Sep 17 00:00:00 2001 From: Joseph Madamba Date: Mon, 17 Aug 2020 23:08:51 -0700 Subject: [PATCH 2703/6909] Simply condition Co-authored-by: Dean Herbert --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index a4c20d1b9e..87b283f6b5 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -350,7 +350,8 @@ namespace osu.Game.Online.Leaderboards { public RankLabel(int? rank) { - TooltipText = rank == null || rank < 1000 ? null : $"#{rank:N0}"; + if (rank >= 1000) + TooltipText = $"#{rank:N0}"; Child = new OsuSpriteText { From f4f642fbcf5b5109727ca17776cb91fd53b49630 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 17 Aug 2020 23:21:44 -0700 Subject: [PATCH 2704/6909] Add ability to skip cutscene with forward mouse button --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 6ae420b162..45b07581ec 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -53,6 +53,7 @@ namespace osu.Game.Input.Bindings public IEnumerable InGameKeyBindings => new[] { new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene), + new KeyBinding(InputKey.ExtraMouseButton2, GlobalAction.SkipCutscene), new KeyBinding(InputKey.Tilde, GlobalAction.QuickRetry), new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit), new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), From e1ed8554a1805dcbe055518962a0cfd3fb7674a5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 18 Aug 2020 17:23:11 +0900 Subject: [PATCH 2705/6909] Use yinyang icon --- osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs index 2fb7a75141..69f883cd3c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Graphics.Sprites; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; @@ -20,6 +21,8 @@ namespace osu.Game.Rulesets.Mania.Mods public override string Description => "Hold the keys. To the beat."; + public override IconUsage? Icon => FontAwesome.Solid.YinYang; + public override ModType Type => ModType.Conversion; public void ApplyToBeatmap(IBeatmap beatmap) From 628be66653e94ea692a717096a8b234f7f4b0a87 Mon Sep 17 00:00:00 2001 From: Jihoon Yang Date: Tue, 18 Aug 2020 01:24:56 -0700 Subject: [PATCH 2706/6909] Updated calculation of mania scroll speed --- .../Configuration/ManiaRulesetConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 7e84f17809..df453cf562 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -27,7 +27,7 @@ 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)} ({v}ms)")) + v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(13720.0 / v)} ({v}ms)")) }; } From d157c42340224d340fe632f3da416f7c2bf60a61 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 18 Aug 2020 17:40:44 +0900 Subject: [PATCH 2707/6909] Increase density by not skipping objects --- osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs index 69f883cd3c..56f6e389bf 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.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.Graphics.Sprites; @@ -43,13 +44,22 @@ namespace osu.Game.Rulesets.Mania.Mods })) .OrderBy(h => h.startTime).ToList(); - for (int i = 0; i < locations.Count - 1; i += 2) + for (int i = 0; i < locations.Count - 1; i++) { + // Full duration of the hold note. + double duration = locations[i + 1].startTime - locations[i].startTime; + + // Beat length at the end of the hold note. + double beatLength = beatmap.ControlPointInfo.TimingPointAt(locations[i + 1].startTime).BeatLength; + + // Decrease the duration by at most a 1/4 beat to ensure there's no instantaneous notes. + duration = Math.Max(duration / 2, duration - beatLength / 4); + newColumnObjects.Add(new HoldNote { Column = column.Key, StartTime = locations[i].startTime, - Duration = locations[i + 1].startTime - locations[i].startTime, + Duration = duration, Samples = locations[i].samples, NodeSamples = new List> { From 4ddc04793f5a8c61c9467c25f77fbbf860e34262 Mon Sep 17 00:00:00 2001 From: Jihoon Yang Date: Tue, 18 Aug 2020 01:44:30 -0700 Subject: [PATCH 2708/6909] Changed MAX_TIME_RANGE instead of the single instance --- .../Configuration/ManiaRulesetConfigManager.cs | 2 +- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index df453cf562..7e84f17809 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -27,7 +27,7 @@ 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(13720.0 / v)} ({v}ms)")) + v => new SettingDescription(v, "Scroll Speed", $"{(int)Math.Round(DrawableManiaRuleset.MAX_TIME_RANGE / v)} ({v}ms)")) }; } diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 94b5ee9486..5b46550333 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// The maximum time range. This occurs at a of 1. /// - public const double MAX_TIME_RANGE = 6000; + public const double MAX_TIME_RANGE = 13720; protected new ManiaPlayfield Playfield => (ManiaPlayfield)base.Playfield; From 138dc5929e9b40e40fdba96097e831eea386098b Mon Sep 17 00:00:00 2001 From: Jihoon Yang Date: Tue, 18 Aug 2020 01:46:07 -0700 Subject: [PATCH 2709/6909] Changed MIN_TIME_RANGE as well --- 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 5b46550333..7f5b9a6ee0 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// The minimum time range. This occurs at a of 40. /// - public const double MIN_TIME_RANGE = 150; + public const double MIN_TIME_RANGE = 340; /// /// The maximum time range. This occurs at a of 1. From 385f7cf85d52c0d412f03b8706157ac686fea070 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 18 Aug 2020 17:56:48 +0900 Subject: [PATCH 2710/6909] Implement mania hold note body recycling --- .../Blueprints/Components/EditBodyPiece.cs | 4 +- .../Objects/Drawables/DrawableHoldNote.cs | 10 +- .../Drawables/Pieces/DefaultBodyPiece.cs | 157 +++++++++++------- .../Objects/Drawables/Pieces/IHoldNoteBody.cs | 16 ++ 4 files changed, 121 insertions(+), 66 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs index efcfe11dad..5fa687298a 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; @@ -15,7 +16,8 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components AccentColour.Value = colours.Yellow; Background.Alpha = 0.5f; - Foreground.Alpha = 0; } + + protected override Drawable CreateForeground() => base.CreateForeground().With(d => d.Alpha = 0); } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 0c5289efe1..a44f8a8886 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly Container tailContainer; private readonly Container tickContainer; - private readonly Drawable bodyPiece; + private readonly SkinnableDrawable bodyPiece; /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { RelativeSizeAxes = Axes.X; - AddRangeInternal(new[] + AddRangeInternal(new Drawable[] { bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece { @@ -135,6 +135,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // Samples are played by the head/tail notes. } + public override void OnKilled() + { + base.OnKilled(); + (bodyPiece.Drawable as IHoldNoteBody)?.Recycle(); + } + protected override void Update() { base.Update(); diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs index bc4a095395..9999983af5 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs @@ -19,24 +19,17 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces /// /// Represents length-wise portion of a hold note. /// - public class DefaultBodyPiece : CompositeDrawable + public class DefaultBodyPiece : CompositeDrawable, IHoldNoteBody { protected readonly Bindable AccentColour = new Bindable(); - - private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize); - private readonly IBindable isHitting = new Bindable(); + protected readonly IBindable IsHitting = new Bindable(); protected Drawable Background { get; private set; } - protected BufferedContainer Foreground { get; private set; } - - private BufferedContainer subtractionContainer; - private Container subtractionLayer; + private Container foregroundContainer; public DefaultBodyPiece() { Blending = BlendingParameters.Additive; - - AddLayout(subtractionCache); } [BackgroundDependencyLoader(true)] @@ -45,7 +38,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces InternalChildren = new[] { Background = new Box { RelativeSizeAxes = Axes.Both }, - Foreground = new BufferedContainer + foregroundContainer = new Container { RelativeSizeAxes = Axes.Both } + }; + + if (drawableObject != null) + { + var holdNote = (DrawableHoldNote)drawableObject; + + AccentColour.BindTo(drawableObject.AccentColour); + IsHitting.BindTo(holdNote.IsHitting); + } + + AccentColour.BindValueChanged(onAccentChanged, true); + + Recycle(); + } + + public void Recycle() => foregroundContainer.Child = CreateForeground(); + + protected virtual Drawable CreateForeground() => new ForegroundPiece + { + AccentColour = { BindTarget = AccentColour }, + IsHitting = { BindTarget = IsHitting } + }; + + private void onAccentChanged(ValueChangedEvent accent) => Background.Colour = accent.NewValue.Opacity(0.7f); + + private class ForegroundPiece : CompositeDrawable + { + public readonly Bindable AccentColour = new Bindable(); + public readonly IBindable IsHitting = new Bindable(); + + private readonly LayoutValue subtractionCache = new LayoutValue(Invalidation.DrawSize); + + private BufferedContainer foregroundBuffer; + private BufferedContainer subtractionBuffer; + private Container subtractionLayer; + + public ForegroundPiece() + { + RelativeSizeAxes = Axes.Both; + + AddLayout(subtractionCache); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = foregroundBuffer = new BufferedContainer { Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, @@ -53,7 +93,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces Children = new Drawable[] { new Box { RelativeSizeAxes = Axes.Both }, - subtractionContainer = new BufferedContainer + subtractionBuffer = new BufferedContainer { RelativeSizeAxes = Axes.Both, // This is needed because we're blending with another object @@ -77,60 +117,51 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces } } } - } - }; - - if (drawableObject != null) - { - var holdNote = (DrawableHoldNote)drawableObject; - - AccentColour.BindTo(drawableObject.AccentColour); - isHitting.BindTo(holdNote.IsHitting); - } - - AccentColour.BindValueChanged(onAccentChanged, true); - isHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent(AccentColour.Value, AccentColour.Value)), true); - } - - private void onAccentChanged(ValueChangedEvent accent) - { - Foreground.Colour = accent.NewValue.Opacity(0.5f); - Background.Colour = accent.NewValue.Opacity(0.7f); - - 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() - { - base.Update(); - - if (!subtractionCache.IsValid) - { - subtractionLayer.Width = 5; - subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth); - subtractionLayer.EdgeEffect = new EdgeEffectParameters - { - Colour = Color4.White, - Type = EdgeEffectType.Glow, - Radius = DrawWidth }; - Foreground.ForceRedraw(); - subtractionContainer.ForceRedraw(); + AccentColour.BindValueChanged(onAccentChanged, true); + IsHitting.BindValueChanged(_ => onAccentChanged(new ValueChangedEvent(AccentColour.Value, AccentColour.Value)), true); + } - subtractionCache.Validate(); + private void onAccentChanged(ValueChangedEvent accent) + { + foregroundBuffer.Colour = accent.NewValue.Opacity(0.5f); + + const float animation_length = 50; + + foregroundBuffer.ClearTransforms(false, nameof(foregroundBuffer.Colour)); + + if (IsHitting.Value) + { + // wait for the next sync point + double synchronisedOffset = animation_length * 2 - Time.Current % (animation_length * 2); + using (foregroundBuffer.BeginDelayedSequence(synchronisedOffset)) + foregroundBuffer.FadeColour(accent.NewValue.Lighten(0.2f), animation_length).Then().FadeColour(foregroundBuffer.Colour, animation_length).Loop(); + } + + subtractionCache.Invalidate(); + } + + protected override void Update() + { + base.Update(); + + if (!subtractionCache.IsValid) + { + subtractionLayer.Width = 5; + subtractionLayer.Height = Math.Max(0, DrawHeight - DrawWidth); + subtractionLayer.EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.White, + Type = EdgeEffectType.Glow, + Radius = DrawWidth + }; + + foregroundBuffer.ForceRedraw(); + subtractionBuffer.ForceRedraw(); + + subtractionCache.Validate(); + } } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs new file mode 100644 index 0000000000..ac3792c01d --- /dev/null +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.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. + +namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces +{ + /// + /// Interface for mania hold note bodies. + /// + public interface IHoldNoteBody + { + /// + /// Recycles the contents of this to free used resources. + /// + void Recycle(); + } +} From da07354f050d15f94a6291d9296cd1286885143c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 18 Aug 2020 19:51:16 +0900 Subject: [PATCH 2711/6909] Fix some judgements potentially giving wrong score --- osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs | 2 +- osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs | 2 +- osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs index 294aab1e4e..28e5d2cc1b 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs @@ -7,7 +7,7 @@ namespace osu.Game.Rulesets.Mania.Judgements { public class HoldNoteTickJudgement : ManiaJudgement { - protected override int NumericResultFor(HitResult result) => 20; + protected override int NumericResultFor(HitResult result) => result == MaxResult ? 20 : 0; protected override double HealthIncreaseFor(HitResult result) { diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 9c4b6f774f..0b1232b8db 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement { - protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK; + protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0; protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2; } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index de3ae27e55..f54e7a9a15 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects { public override bool AffectsCombo => false; - protected override int NumericResultFor(HitResult result) => SCORE_PER_TICK; + protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0; protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0; } From a4ad0bd1744a14207e0f39b5c363e24ace00005c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 18 Aug 2020 19:51:26 +0900 Subject: [PATCH 2712/6909] Ensure 0 score from miss judgements, add test --- .../Gameplay/TestSceneScoreProcessor.cs | 41 +++++++++++++++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 12 ++++-- 2 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs new file mode 100644 index 0000000000..b0baf0385e --- /dev/null +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.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.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Gameplay +{ + [HeadlessTest] + public class TestSceneScoreProcessor : OsuTestScene + { + [Test] + public void TestNoScoreIncreaseFromMiss() + { + var beatmap = new Beatmap { HitObjects = { new TestHitObject() } }; + + var scoreProcessor = new ScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + + // Apply a miss judgement + scoreProcessor.ApplyResult(new JudgementResult(new TestHitObject(), new TestJudgement()) { Type = HitResult.Miss }); + + Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0)); + } + + private class TestHitObject : HitObject + { + public override Judgement CreateJudgement() => new TestJudgement(); + } + + private class TestJudgement : Judgement + { + protected override int NumericResultFor(HitResult result) => 100; + } + } +} diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index f1cdfd93c8..eac47aa089 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -133,17 +133,19 @@ namespace osu.Game.Rulesets.Scoring } } + double scoreIncrease = result.Type == HitResult.Miss ? 0 : result.Judgement.NumericResultFor(result); + if (result.Judgement.IsBonus) { if (result.IsHit) - bonusScore += result.Judgement.NumericResultFor(result); + bonusScore += scoreIncrease; } else { if (result.HasResult) scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; - baseScore += result.Judgement.NumericResultFor(result); + baseScore += scoreIncrease; rollingMaxBaseScore += result.Judgement.MaxNumericResult; } @@ -169,17 +171,19 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; + double scoreIncrease = result.Type == HitResult.Miss ? 0 : result.Judgement.NumericResultFor(result); + if (result.Judgement.IsBonus) { if (result.IsHit) - bonusScore -= result.Judgement.NumericResultFor(result); + bonusScore -= scoreIncrease; } else { if (result.HasResult) scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1; - baseScore -= result.Judgement.NumericResultFor(result); + baseScore -= scoreIncrease; rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } From 6aa31dffdb2b194eb0c593c8094e3d37f96f614e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 18 Aug 2020 15:25:51 +0200 Subject: [PATCH 2713/6909] Fix toolbar not respecting current overlay activation mode. --- .../Visual/Menus/TestSceneToolbar.cs | 27 +++++++++++++++++-- osu.Game/Overlays/Toolbar/Toolbar.cs | 21 +++++++++------ 2 files changed, 38 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index b4985cad9f..5170058700 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -4,8 +4,10 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; +using osu.Game.Overlays; using osu.Game.Overlays.Toolbar; using osu.Game.Rulesets; using osuTK.Input; @@ -15,7 +17,7 @@ namespace osu.Game.Tests.Visual.Menus [TestFixture] public class TestSceneToolbar : OsuManualInputManagerTestScene { - private Toolbar toolbar; + private TestToolbar toolbar; [Resolved] private RulesetStore rulesets { get; set; } @@ -23,7 +25,7 @@ namespace osu.Game.Tests.Visual.Menus [SetUp] public void SetUp() => Schedule(() => { - Child = toolbar = new Toolbar { State = { Value = Visibility.Visible } }; + Child = toolbar = new TestToolbar { State = { Value = Visibility.Visible } }; }); [Test] @@ -72,5 +74,26 @@ namespace osu.Game.Tests.Visual.Menus AddUntilStep("ruleset switched", () => rulesetSelector.Current.Value.Equals(expected)); } } + + [TestCase(OverlayActivation.All)] + [TestCase(OverlayActivation.Disabled)] + public void TestRespectsOverlayActivation(OverlayActivation mode) + { + AddStep($"set activation mode to {mode}", () => toolbar.OverlayActivationMode.Value = mode); + AddStep("hide toolbar", () => toolbar.Hide()); + AddStep("try to show toolbar", () => toolbar.Show()); + + if (mode == OverlayActivation.Disabled) + AddUntilStep("toolbar still hidden", () => toolbar.Visibility == Visibility.Hidden); + else + AddAssert("toolbar is visible", () => toolbar.Visibility == Visibility.Visible); + } + + public class TestToolbar : Toolbar + { + public new Bindable OverlayActivationMode => base.OverlayActivationMode; + + public Visibility Visibility => State.Value; + } } } diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index beac6adc59..3bf9e85428 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Toolbar private const double transition_time = 500; - private readonly Bindable overlayActivationMode = new Bindable(OverlayActivation.All); + protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.All); // Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden. public override bool PropagateNonPositionalInputSubTree => true; @@ -89,14 +89,8 @@ namespace osu.Game.Overlays.Toolbar // Bound after the selector is added to the hierarchy to give it a chance to load the available rulesets rulesetSelector.Current.BindTo(parentRuleset); - State.ValueChanged += visibility => - { - if (overlayActivationMode.Value == OverlayActivation.Disabled) - Hide(); - }; - if (osuGame != null) - overlayActivationMode.BindTo(osuGame.OverlayActivationMode); + OverlayActivationMode.BindTo(osuGame.OverlayActivationMode); } public class ToolbarBackground : Container @@ -137,6 +131,17 @@ namespace osu.Game.Overlays.Toolbar } } + protected override void UpdateState(ValueChangedEvent state) + { + if (state.NewValue == Visibility.Visible && OverlayActivationMode.Value == OverlayActivation.Disabled) + { + State.Value = Visibility.Hidden; + return; + } + + base.UpdateState(state); + } + protected override void PopIn() { this.MoveToY(0, transition_time, Easing.OutQuint); From 988ad378a7c7ccfe0e20c11b334e47a1cc368082 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 19 Aug 2020 00:05:05 +0900 Subject: [PATCH 2714/6909] Fix body size + freeze head piece --- .../Objects/Drawables/DrawableHoldNote.cs | 48 ++++++++++++------- .../Objects/Drawables/DrawableHoldNoteHead.cs | 8 ++++ 2 files changed, 40 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 39b1771643..008cc3519e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -34,8 +34,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly Container tailContainer; private readonly Container tickContainer; - private readonly Container bodyPieceContainer; - private readonly Drawable bodyPiece; + /// + /// Contains the maximum size/position of the body prior to any offset or size adjustments. + /// + private readonly Container bodyContainer; + + /// + /// Contains the offset size/position of the body such that the body extends half-way between the head and tail pieces. + /// + private readonly Container bodyOffsetContainer; /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. @@ -57,18 +64,27 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { RelativeSizeAxes = Axes.X; - AddRangeInternal(new Drawable[] + AddRangeInternal(new[] { - bodyPieceContainer = new Container + bodyContainer = new Container { - RelativeSizeAxes = Axes.X, - Child = bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }) + bodyOffsetContainer = new Container + { + RelativeSizeAxes = Axes.X, + Child = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece + { + RelativeSizeAxes = Axes.Both + }) + }, + // The head needs to move along with changes in the size of the body. + headContainer = new Container { RelativeSizeAxes = Axes.Both } + } }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, - headContainer = new Container { RelativeSizeAxes = Axes.Both }, + headContainer.CreateProxy(), tailContainer = new Container { RelativeSizeAxes = Axes.Both }, }); } @@ -136,8 +152,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.OnDirectionChanged(e); - bodyPieceContainer.Anchor = bodyPieceContainer.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; - bodyPieceContainer.Anchor = bodyPieceContainer.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.BottomLeft : Anchor.TopLeft; + bodyOffsetContainer.Anchor = bodyOffsetContainer.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; } public override void PlaySamples() @@ -149,15 +164,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.Update(); - // Make the body piece not lie under the head note - bodyPieceContainer.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; - bodyPieceContainer.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; - + // Decrease the size of the body while the hold note is held and the head has been hit. if (Head.IsHit && !hasReleased) { float heightDecrease = (float)(Math.Max(0, Time.Current - HitObject.StartTime) / HitObject.Duration); - bodyPiece.Height = MathHelper.Clamp(1 - heightDecrease, 0, 1); + bodyContainer.Height = MathHelper.Clamp(1 - heightDecrease, 0, 1); } + + // Offset the body to extend half-way under the head and tail. + bodyOffsetContainer.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; + bodyOffsetContainer.Height = bodyContainer.DrawHeight - Head.Height / 2 + Tail.Height / 2; } protected override void UpdateStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs index a73fe259e4..cd56b81e10 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Objects.Drawables; + namespace osu.Game.Rulesets.Mania.Objects.Drawables { /// @@ -17,6 +19,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); + protected override void UpdateStateTransforms(ArmedState state) + { + // This hitobject should never expire, so this is just a safe maximum. + LifetimeEnd = LifetimeStart + 30000; + } + public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note public override void OnReleased(ManiaAction action) From 99315a4aa74c434bb4938357d75b65543150ff59 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 19 Aug 2020 00:05:36 +0900 Subject: [PATCH 2715/6909] Fix incorrect anchors for up-scroll --- .../Objects/Drawables/DrawableHoldNote.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 008cc3519e..e120fab21b 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -152,7 +152,18 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.OnDirectionChanged(e); - bodyOffsetContainer.Anchor = bodyOffsetContainer.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + // The body container is anchored from the position of the tail, since its height is changed when the hold note is being hit. + // The body offset container is anchored from the position of the head (inverse of the above). + if (e.NewValue == ScrollingDirection.Up) + { + bodyContainer.Anchor = bodyContainer.Origin = Anchor.BottomLeft; + bodyOffsetContainer.Anchor = bodyOffsetContainer.Origin = Anchor.TopLeft; + } + else + { + bodyContainer.Anchor = bodyContainer.Origin = Anchor.TopLeft; + bodyOffsetContainer.Anchor = bodyOffsetContainer.Origin = Anchor.BottomLeft; + } } public override void PlaySamples() From af8f727721cc227082ffc78d20b3b39dbf73fb3e Mon Sep 17 00:00:00 2001 From: Jihoon Yang Date: Tue, 18 Aug 2020 08:28:53 -0700 Subject: [PATCH 2716/6909] Disable LegacyHitExplosion for hold notes --- osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index 12747924de..e80d968f37 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Judgements; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; @@ -66,10 +67,13 @@ namespace osu.Game.Rulesets.Mania.Skinning public void Animate(JudgementResult result) { - (explosion as IFramedAnimation)?.GotoFrame(0); + if (!(result.Judgement is HoldNoteTickJudgement)) + { + (explosion as IFramedAnimation)?.GotoFrame(0); - explosion?.FadeInFromZero(80) - .Then().FadeOut(120); + explosion?.FadeInFromZero(80) + .Then().FadeOut(120); + } } } } From cd2280b5bf8947f29d379da16cb8826eff637fe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 15:18:52 +0200 Subject: [PATCH 2717/6909] Fix cheese indexing bug --- .../Difficulty/Preprocessing/StaminaCheeseDetector.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index b53bc66f39..29e631e515 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Game.Rulesets.Taiko.Objects; @@ -58,7 +59,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { for (int j = repetitionStart; j < i; j++) { - hitObjects[i].StaminaCheese = true; + hitObjects[j].StaminaCheese = true; } } } @@ -81,9 +82,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (tlLength >= tl_min_repetitions) { - for (int j = i - tlLength; j < i; j++) + for (int j = Math.Max(0, i - tlLength); j < i; j++) { - hitObjects[i].StaminaCheese = true; + hitObjects[j].StaminaCheese = true; } } } From 9fb494d5d3067bb39081222cb3f39d5b2dc9ba74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 15:24:30 +0200 Subject: [PATCH 2718/6909] Eliminate unnecessary loop --- .../Difficulty/Skills/Colour.cs | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index db445c7d27..e93893d894 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -80,6 +80,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// private double repetitionPenalties() { + const int l = 2; double penalty = 1.0; monoHistory.Add(currentMonoLength); @@ -87,27 +88,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (monoHistory.Count > mono_history_max_length) monoHistory.RemoveAt(0); - for (int l = 2; l <= mono_history_max_length / 2; l++) + for (int start = monoHistory.Count - l - 1; start >= 0; start--) { - for (int start = monoHistory.Count - l - 1; start >= 0; start--) + bool samePattern = true; + + for (int i = 0; i < l; i++) { - bool samePattern = true; - - for (int i = 0; i < l; i++) + if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) { - if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) - { - samePattern = false; - } + samePattern = false; } + } - if (samePattern) // Repetition found! - { - int notesSince = 0; - for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; - penalty *= repetitionPenalty(notesSince); - break; - } + if (samePattern) // Repetition found! + { + int notesSince = 0; + for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; + penalty *= repetitionPenalty(notesSince); + break; } } From 474f2452226cf79b271813824c5bad9add33a177 Mon Sep 17 00:00:00 2001 From: Jihoon Yang Date: Tue, 18 Aug 2020 08:40:29 -0700 Subject: [PATCH 2719/6909] Replace nested loop with early return --- .../Skinning/LegacyHitExplosion.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs index e80d968f37..41f3090afd 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -67,13 +67,13 @@ namespace osu.Game.Rulesets.Mania.Skinning public void Animate(JudgementResult result) { - if (!(result.Judgement is HoldNoteTickJudgement)) - { - (explosion as IFramedAnimation)?.GotoFrame(0); + if (result.Judgement is HoldNoteTickJudgement) + return; - explosion?.FadeInFromZero(80) - .Then().FadeOut(120); - } + (explosion as IFramedAnimation)?.GotoFrame(0); + + explosion?.FadeInFromZero(80) + .Then().FadeOut(120); } } } From 4d4d9b7356e1963b6d397bee2951a4b67a5bea25 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 19 Aug 2020 01:37:24 +0900 Subject: [PATCH 2720/6909] Add rewinding support --- .../Objects/Drawables/DrawableHoldNote.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index e120fab21b..0e1e700702 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Whether the hold note has been released potentially without having caused a break. /// - private bool hasReleased; + private double? releaseTime; public DrawableHoldNote(HoldNote hitObject) : base(hitObject) @@ -175,8 +175,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.Update(); - // Decrease the size of the body while the hold note is held and the head has been hit. - if (Head.IsHit && !hasReleased) + if (Time.Current < releaseTime) + releaseTime = null; + + // Decrease the size of the body while the hold note is held and the head has been hit. This stops at the very first release point. + if (Head.IsHit && releaseTime == null) { float heightDecrease = (float)(Math.Max(0, Time.Current - HitObject.StartTime) / HitObject.Duration); bodyContainer.Height = MathHelper.Clamp(1 - heightDecrease, 0, 1); @@ -250,7 +253,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!Tail.IsHit) HasBroken = true; - hasReleased = true; + releaseTime = Time.Current; } private void endHold() From 1d9d885d27fbd22f1118944a99fc2889d3617ca3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 19 Aug 2020 01:40:26 +0900 Subject: [PATCH 2721/6909] Mask the tail as the body gets shorter --- .../Objects/Drawables/DrawableHoldNote.cs | 38 +++++++++++++++++-- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 0e1e700702..d2412df7c3 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -44,6 +44,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// private readonly Container bodyOffsetContainer; + /// + /// Contains the masking area for the tail, which is resized along with . + /// + private readonly Container tailMaskingContainer; + /// /// Time at which the user started holding this hold note. Null if the user is not holding this hold note. /// @@ -84,8 +89,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, - headContainer.CreateProxy(), - tailContainer = new Container { RelativeSizeAxes = Axes.Both }, + tailMaskingContainer = new Container + { + RelativeSizeAxes = Axes.X, + Masking = true, + Child = tailContainer = new Container + { + RelativeSizeAxes = Axes.X, + } + }, + headContainer.CreateProxy() }); } @@ -154,15 +167,22 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // The body container is anchored from the position of the tail, since its height is changed when the hold note is being hit. // The body offset container is anchored from the position of the head (inverse of the above). + // The tail containers are both anchored from the position of the tail. if (e.NewValue == ScrollingDirection.Up) { bodyContainer.Anchor = bodyContainer.Origin = Anchor.BottomLeft; bodyOffsetContainer.Anchor = bodyOffsetContainer.Origin = Anchor.TopLeft; + + tailMaskingContainer.Anchor = tailMaskingContainer.Origin = Anchor.BottomLeft; + tailContainer.Anchor = tailContainer.Origin = Anchor.BottomLeft; } else { bodyContainer.Anchor = bodyContainer.Origin = Anchor.TopLeft; bodyOffsetContainer.Anchor = bodyOffsetContainer.Origin = Anchor.BottomLeft; + + tailMaskingContainer.Anchor = tailMaskingContainer.Origin = Anchor.TopLeft; + tailContainer.Anchor = tailContainer.Origin = Anchor.TopLeft; } } @@ -185,9 +205,19 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables bodyContainer.Height = MathHelper.Clamp(1 - heightDecrease, 0, 1); } - // Offset the body to extend half-way under the head and tail. + // Re-position the body half-way up the head, and extend the height until it's half-way under the tail. bodyOffsetContainer.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; - bodyOffsetContainer.Height = bodyContainer.DrawHeight - Head.Height / 2 + Tail.Height / 2; + bodyOffsetContainer.Height = bodyContainer.DrawHeight + Tail.Height / 2 - Head.Height / 2; + + // The tail is positioned to be "outside" the hold note, so re-position its masking container to fully cover the tail and extend the height until it's half-way under the head. + // The masking height is determined by the size of the body so that the head and tail don't overlap as the body becomes shorter via hitting (above). + tailMaskingContainer.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Tail.Height; + tailMaskingContainer.Height = bodyContainer.DrawHeight + Tail.Height - Head.Height / 2; + + // The tail container needs the reverse of the above offset applied to bring the tail to its original position. + // It also needs the full original height of the hold note to maintain positioning even as the height of the masking container changes. + tailContainer.Y = -tailMaskingContainer.Y; + tailContainer.Height = DrawHeight; } protected override void UpdateStateTransforms(ArmedState state) From 6c759f31f175f784f447f1ef0d21e20460429ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:13:18 +0200 Subject: [PATCH 2722/6909] Add and use limited capacity queue --- .../Preprocessing/StaminaCheeseDetector.cs | 10 +- .../Difficulty/Skills/Colour.cs | 9 +- .../Difficulty/Skills/Rhythm.cs | 9 +- .../Difficulty/Skills/Stamina.cs | 9 +- .../NonVisual/LimitedCapacityQueueTest.cs | 98 +++++++++++++++ .../Difficulty/Utils/LimitedCapacityQueue.cs | 114 ++++++++++++++++++ 6 files changed, 226 insertions(+), 23 deletions(-) create mode 100644 osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs create mode 100644 osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index 29e631e515..c6317ff195 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing @@ -27,16 +28,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing private void findRolls(int patternLength) { - List history = new List(); + var history = new LimitedCapacityQueue(2 * patternLength); int repetitionStart = 0; for (int i = 0; i < hitObjects.Count; i++) { - history.Add(hitObjects[i]); - if (history.Count < 2 * patternLength) continue; - - if (history.Count > 2 * patternLength) history.RemoveAt(0); + history.Enqueue(hitObjects[i]); + if (!history.Full) + continue; bool isRepeat = true; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index e93893d894..e9e0930a9a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// List of the last most recent mono patterns, with the most recent at the end of the list. /// - private readonly List monoHistory = new List(); + private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length); protected override double StrainValueOf(DifficultyHitObject current) { @@ -83,10 +83,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills const int l = 2; double penalty = 1.0; - monoHistory.Add(currentMonoLength); - - if (monoHistory.Count > mono_history_max_length) - monoHistory.RemoveAt(0); + monoHistory.Enqueue(currentMonoLength); for (int start = monoHistory.Count - l - 1; start >= 0; start--) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 31dc93a6b2..caf1acccf4 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private const double strain_decay = 0.96; private double currentStrain; - private readonly List rhythmHistory = new List(); + private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); private const int rhythm_history_max_length = 8; private int notesSinceRhythmChange; @@ -32,10 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { double penalty = 1; - rhythmHistory.Add(hitobject); - - if (rhythmHistory.Count > rhythm_history_max_length) - rhythmHistory.RemoveAt(0); + rhythmHistory.Enqueue(hitobject); for (int l = 2; l <= rhythm_history_max_length / 2; l++) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index c9a691a2aa..430a553113 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.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.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainDecayBase => 0.4; private const int max_history_length = 2; - private readonly List notePairDurationHistory = new List(); + private readonly LimitedCapacityQueue notePairDurationHistory = new LimitedCapacityQueue(max_history_length); private double offhandObjectDuration = double.MaxValue; @@ -56,10 +56,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (hitObject.ObjectIndex == 1) return 1; - notePairDurationHistory.Add(hitObject.DeltaTime + offhandObjectDuration); - - if (notePairDurationHistory.Count > max_history_length) - notePairDurationHistory.RemoveAt(0); + notePairDurationHistory.Enqueue(hitObject.DeltaTime + offhandObjectDuration); double shortestRecentNote = notePairDurationHistory.Min(); objectStrain += speedBonus(shortestRecentNote); diff --git a/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs new file mode 100644 index 0000000000..52463dd7eb --- /dev/null +++ b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Rulesets.Difficulty.Utils; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class LimitedCapacityQueueTest + { + private const int capacity = 3; + + private LimitedCapacityQueue queue; + + [SetUp] + public void SetUp() + { + queue = new LimitedCapacityQueue(capacity); + } + + [Test] + public void TestEmptyQueue() + { + Assert.AreEqual(0, queue.Count); + + Assert.Throws(() => _ = queue[0]); + + Assert.Throws(() => _ = queue.Dequeue()); + + int count = 0; + foreach (var _ in queue) + count++; + + Assert.AreEqual(0, count); + } + + [TestCase(1)] + [TestCase(2)] + [TestCase(3)] + public void TestBelowCapacity(int count) + { + for (int i = 0; i < count; ++i) + queue.Enqueue(i); + + Assert.AreEqual(count, queue.Count); + + for (int i = 0; i < count; ++i) + Assert.AreEqual(i, queue[i]); + + int j = 0; + foreach (var item in queue) + Assert.AreEqual(j++, item); + + for (int i = queue.Count; i < queue.Count + capacity; i++) + Assert.Throws(() => _ = queue[i]); + } + + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void TestEnqueueAtFullCapacity(int count) + { + for (int i = 0; i < count; ++i) + queue.Enqueue(i); + + Assert.AreEqual(capacity, queue.Count); + + for (int i = 0; i < queue.Count; ++i) + Assert.AreEqual(count - capacity + i, queue[i]); + + int j = count - capacity; + foreach (var item in queue) + Assert.AreEqual(j++, item); + + for (int i = queue.Count; i < queue.Count + capacity; i++) + Assert.Throws(() => _ = queue[i]); + } + + [TestCase(4)] + [TestCase(5)] + [TestCase(6)] + public void TestDequeueAtFullCapacity(int count) + { + for (int i = 0; i < count; ++i) + queue.Enqueue(i); + + for (int i = 0; i < capacity; ++i) + { + Assert.AreEqual(count - capacity + i, queue.Dequeue()); + Assert.AreEqual(2 - i, queue.Count); + } + + Assert.Throws(() => queue.Dequeue()); + } + } +} diff --git a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs new file mode 100644 index 0000000000..0f014e8a8c --- /dev/null +++ b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs @@ -0,0 +1,114 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections; +using System.Collections.Generic; + +namespace osu.Game.Rulesets.Difficulty.Utils +{ + /// + /// An indexed queue with limited capacity. + /// Respects first-in-first-out insertion order. + /// + public class LimitedCapacityQueue : IEnumerable + { + /// + /// The number of elements in the queue. + /// + public int Count { get; private set; } + + /// + /// Whether the queue is full (adding any new items will cause removing existing ones). + /// + public bool Full => Count == capacity; + + private readonly T[] array; + private readonly int capacity; + + // Markers tracking the queue's first and last element. + private int start, end; + + /// + /// Constructs a new + /// + /// The number of items the queue can hold. + public LimitedCapacityQueue(int capacity) + { + if (capacity < 0) + throw new ArgumentOutOfRangeException(nameof(capacity)); + + this.capacity = capacity; + array = new T[capacity]; + start = 0; + end = -1; + } + + /// + /// Removes an item from the front of the . + /// + /// The item removed from the front of the queue. + public T Dequeue() + { + if (Count == 0) + throw new InvalidOperationException("Queue is empty."); + + var result = array[start]; + start = (start + 1) % capacity; + Count--; + return result; + } + + /// + /// Adds an item to the back of the . + /// If the queue is holding elements at the point of addition, + /// the item at the front of the queue will be removed. + /// + /// The item to be added to the back of the queue. + public void Enqueue(T item) + { + end = (end + 1) % capacity; + if (Count == capacity) + start = (start + 1) % capacity; + else + Count++; + array[end] = item; + } + + /// + /// Retrieves the item at the given index in the queue. + /// + /// + /// The index of the item to retrieve. + /// The item with index 0 is at the front of the queue + /// (it was added the earliest). + /// + public T this[int index] + { + get + { + if (index < 0 || index >= Count) + throw new ArgumentOutOfRangeException(nameof(index)); + + return array[(start + index) % capacity]; + } + } + + /// + /// Enumerates the queue from its start to its end. + /// + public IEnumerator GetEnumerator() + { + if (Count == 0) + yield break; + + for (int i = 0; i < Count; i++) + yield return array[(start + i) % capacity]; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } +} From 292d38362c504196b7593042fc8dcba2104a20af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:18:36 +0200 Subject: [PATCH 2723/6909] De-nest cheese detection logic --- .../Preprocessing/StaminaCheeseDetector.cs | 47 +++++++++---------- 1 file changed, 22 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index c6317ff195..e1dad70d90 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -38,33 +38,34 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (!history.Full) continue; - bool isRepeat = true; - - for (int j = 0; j < patternLength; j++) - { - if (history[j].HitType != history[j + patternLength].HitType) - { - isRepeat = false; - } - } - - if (!isRepeat) + if (!containsPatternRepeat(history, patternLength)) { repetitionStart = i - 2 * patternLength; + continue; } int repeatedLength = i - repetitionStart; + if (repeatedLength < roll_min_repetitions) + continue; - if (repeatedLength >= roll_min_repetitions) + for (int j = repetitionStart; j < i; j++) { - for (int j = repetitionStart; j < i; j++) - { - hitObjects[j].StaminaCheese = true; - } + hitObjects[j].StaminaCheese = true; } } } + private static bool containsPatternRepeat(LimitedCapacityQueue history, int patternLength) + { + for (int j = 0; j < patternLength; j++) + { + if (history[j].HitType != history[j + patternLength].HitType) + return false; + } + + return true; + } + private void findTlTap(int parity, HitType type) { int tlLength = -2; @@ -72,20 +73,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing for (int i = parity; i < hitObjects.Count; i += 2) { if (hitObjects[i].HitType == type) - { tlLength += 2; - } else - { tlLength = -2; - } - if (tlLength >= tl_min_repetitions) + if (tlLength < tl_min_repetitions) + continue; + + for (int j = Math.Max(0, i - tlLength); j < i; j++) { - for (int j = Math.Max(0, i - tlLength); j < i; j++) - { - hitObjects[j].StaminaCheese = true; - } + hitObjects[j].StaminaCheese = true; } } } From ff44437706bc5eb34582329db869c2031772954e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:29:51 +0200 Subject: [PATCH 2724/6909] Extract method for marking cheese --- .../Preprocessing/StaminaCheeseDetector.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index e1dad70d90..3f952d8317 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -48,10 +48,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (repeatedLength < roll_min_repetitions) continue; - for (int j = repetitionStart; j < i; j++) - { - hitObjects[j].StaminaCheese = true; - } + markObjectsAsCheese(repetitionStart, i); } } @@ -80,11 +77,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (tlLength < tl_min_repetitions) continue; - for (int j = Math.Max(0, i - tlLength); j < i; j++) - { - hitObjects[j].StaminaCheese = true; - } + markObjectsAsCheese(Math.Max(0, i - tlLength), i); } } + + private void markObjectsAsCheese(int start, int end) + { + for (int i = start; i < end; ++i) + hitObjects[i].StaminaCheese = true; + } } } From f22050c9759648521ea2bd05f51b3932e718eb00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:31:10 +0200 Subject: [PATCH 2725/6909] Remove unnecessary initialiser --- .../Difficulty/Preprocessing/TaikoDifficultyHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 817e974fe8..81b304af13 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly TaikoDifficultyHitObjectRhythm Rhythm; public readonly HitType? HitType; - public bool StaminaCheese = false; + public bool StaminaCheese; public readonly int ObjectIndex; From c6a640db55aa386b8acc05fb788c08cfee76aafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:34:44 +0200 Subject: [PATCH 2726/6909] Remove superfluous IsRepeat field --- .../TaikoDifficultyHitObjectRhythm.cs | 4 +--- .../Difficulty/Skills/Rhythm.cs | 2 +- .../Difficulty/TaikoDifficultyCalculator.cs | 18 +++++++++--------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index 0ad885d9bd..9c22eff22a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -7,13 +7,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public readonly double Difficulty; public readonly double Ratio; - public readonly bool IsRepeat; - public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty, bool isRepeat) + public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) { Ratio = numerator / (double)denominator; Difficulty = difficulty; - IsRepeat = isRepeat; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index caf1acccf4..483e94cd70 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current; notesSinceRhythmChange += 1; - if (hitobject.Rhythm.IsRepeat) + if (hitobject.Rhythm.Difficulty == 0.0) { return 0.0; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 789fd7c63b..7a9f1765ae 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -26,15 +26,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms = { - new TaikoDifficultyHitObjectRhythm(1, 1, 0.0, true), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3, false), - new TaikoDifficultyHitObjectRhythm(1, 2, 0.5, false), - new TaikoDifficultyHitObjectRhythm(3, 1, 0.3, false), - new TaikoDifficultyHitObjectRhythm(1, 3, 0.35, false), - new TaikoDifficultyHitObjectRhythm(3, 2, 0.6, false), - new TaikoDifficultyHitObjectRhythm(2, 3, 0.4, false), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5, false), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7, false) + new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), + new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), + new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), + new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), + new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), + new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), + new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), + new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), + new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) }; public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) From 00ae456f0879342fb3bc55e8be717585b7ef5e7f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:39:03 +0200 Subject: [PATCH 2727/6909] Remove unnecessary null check --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index e9e0930a9a..2a72f884d1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills double objectStrain = 0.0; - if (taikoCurrent.HitType != null && previousHitType != null && taikoCurrent.HitType != previousHitType) + if (previousHitType != null && taikoCurrent.HitType != previousHitType) { // The colour has changed. objectStrain = 1.0; From d7ff3d77eb538b598e9878fa3cd814daf15fc499 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:44:41 +0200 Subject: [PATCH 2728/6909] Slightly optimise and de-branch repetition pattern recognition --- .../Difficulty/Skills/Colour.cs | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 2a72f884d1..dd8b536afc 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -87,28 +87,29 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills for (int start = monoHistory.Count - l - 1; start >= 0; start--) { - bool samePattern = true; + if (!isSamePattern(start, l)) + continue; - for (int i = 0; i < l; i++) - { - if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) - { - samePattern = false; - } - } - - if (samePattern) // Repetition found! - { - int notesSince = 0; - for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; - penalty *= repetitionPenalty(notesSince); - break; - } + int notesSince = 0; + for (int i = start; i < monoHistory.Count; i++) notesSince += monoHistory[i]; + penalty *= repetitionPenalty(notesSince); + break; } return penalty; } + private bool isSamePattern(int start, int l) + { + for (int i = 0; i < l; i++) + { + if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) + return false; + } + + return true; + } + private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); } } From ce0e5cf9a168ce86b5ea176a4767d4929aa6f211 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:47:36 +0200 Subject: [PATCH 2729/6909] Slightly optimise and de-branch rhythm pattern recognition --- .../Difficulty/Skills/Rhythm.cs | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 483e94cd70..4c06deb5c0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -38,28 +38,29 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { for (int start = rhythmHistory.Count - l - 1; start >= 0; start--) { - bool samePattern = true; + if (!samePattern(start, l)) + continue; - for (int i = 0; i < l; i++) - { - if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - l + i].Rhythm) - { - samePattern = false; - } - } - - if (samePattern) // Repetition found! - { - int notesSince = hitobject.ObjectIndex - rhythmHistory[start].ObjectIndex; - penalty *= repetitionPenalty(notesSince); - break; - } + int notesSince = hitobject.ObjectIndex - rhythmHistory[start].ObjectIndex; + penalty *= repetitionPenalty(notesSince); + break; } } return penalty; } + private bool samePattern(int start, int l) + { + for (int i = 0; i < l; i++) + { + if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - l + i].Rhythm) + return false; + } + + return true; + } + private double patternLengthPenalty(int patternLength) { double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); From 80e4c157279d5a58b873d14fedf3ea3b26ad7bf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:50:16 +0200 Subject: [PATCH 2730/6909] Use Math.Clamp --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 4c06deb5c0..f6ef6470ed 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private double patternLengthPenalty(int patternLength) { double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); - double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); + double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0); return Math.Min(shortPatternPenalty, longPatternPenalty); } From c827e215069bde7be1747524f6fc9a9e755d61d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:51:19 +0200 Subject: [PATCH 2731/6909] Extract helper method to reset rhythm strain --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index f6ef6470ed..6bb2eaf06a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -74,8 +74,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (noteLengthMs < 80) return 1; if (noteLengthMs < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMs); - currentStrain = 0.0; - notesSinceRhythmChange = 0; + resetRhythmStrain(); return 0.0; } @@ -83,8 +82,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { if (!(current.BaseObject is Hit)) { - currentStrain = 0.0; - notesSinceRhythmChange = 0; + resetRhythmStrain(); return 0.0; } @@ -109,5 +107,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills currentStrain += objectStrain; return currentStrain; } + + private void resetRhythmStrain() + { + currentStrain = 0.0; + notesSinceRhythmChange = 0; + } } } From 51d41515ef857386cb89467b8777a8429ab9c072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:54:20 +0200 Subject: [PATCH 2732/6909] Simplify expression with ternary --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 430a553113..13510290f7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -73,12 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills public Stamina(bool rightHand) { - hand = 0; - - if (rightHand) - { - hand = 1; - } + hand = rightHand ? 1 : 0; } } } From cb5ea6aa9a54d26d454456837fc1b15dbb7f7819 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 19:59:28 +0200 Subject: [PATCH 2733/6909] Generalise p-norm function --- .../Difficulty/TaikoDifficultyCalculator.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7a9f1765ae..aa21df0228 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -49,13 +49,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } - private double norm(double p, double v1, double v2, double v3) + private double norm(double p, params double[] values) { - return Math.Pow( - Math.Pow(v1, p) + - Math.Pow(v2, p) + - Math.Pow(v3, p) - , 1 / p); + return Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); } private double rescale(double sr) From 27f97973ee188bf77c6a10248aa88ddad6057989 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 18 Aug 2020 20:14:00 +0200 Subject: [PATCH 2734/6909] Add more proper typing to skills --- .../Difficulty/TaikoDifficultyCalculator.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index aa21df0228..d3ff0b95ee 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -61,7 +61,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return 10.43 * Math.Log(sr / 8 + 1); } - private double locallyCombinedDifficulty(double staminaPenalty, Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) + private double locallyCombinedDifficulty( + double staminaPenalty, Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft) { double difficulty = 0; double weight = 1; @@ -71,7 +72,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier; double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier; - double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; + double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } @@ -89,14 +90,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; - double colourRating = skills[0].DifficultyValue() * colour_skill_multiplier; - double rhythmRating = skills[1].DifficultyValue() * rhythm_skill_multiplier; - double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * stamina_skill_multiplier; + var colour = (Colour)skills[0]; + var rhythm = (Rhythm)skills[1]; + var staminaRight = (Stamina)skills[2]; + var staminaLeft = (Stamina)skills[3]; + + double colourRating = colour.DifficultyValue() * colour_skill_multiplier; + double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier; double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); staminaRating *= staminaPenalty; - double combinedRating = locallyCombinedDifficulty(staminaPenalty, skills[0], skills[1], skills[2], skills[3]); + double combinedRating = locallyCombinedDifficulty(staminaPenalty, colour, rhythm, staminaRight, staminaLeft); double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); double starRating = 1.4 * separatedRating + 0.5 * combinedRating; starRating = rescale(starRating); From 8f1a71c6b1c563b7238c81c997b60df1dadd8440 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 07:44:45 +0300 Subject: [PATCH 2735/6909] Remove counter sprite attributes for not being of any reasonable use --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 2 - .../UserInterface/PercentageCounter.cs | 9 ++--- .../Graphics/UserInterface/RollingCounter.cs | 38 +------------------ .../Graphics/UserInterface/ScoreCounter.cs | 8 +--- .../UserInterface/SimpleComboCounter.cs | 7 +++- osu.Game/Screens/Play/HUDOverlay.cs | 3 -- 6 files changed, 13 insertions(+), 54 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index 030d420ec0..09b4f9b761 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -20,7 +20,6 @@ namespace osu.Game.Tests.Visual.Gameplay { Origin = Anchor.TopRight, Anchor = Anchor.TopRight, - TextSize = 40, Margin = new MarginPadding(20), }; Add(score); @@ -30,7 +29,6 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, Margin = new MarginPadding(10), - TextSize = 40, }; Add(comboCounter); diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs index 9b31935eee..3ea9c1053c 100644 --- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs +++ b/osu.Game/Graphics/UserInterface/PercentageCounter.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Utils; @@ -28,7 +29,7 @@ namespace osu.Game.Graphics.UserInterface } [BackgroundDependencyLoader] - private void load(OsuColour colours) => AccentColour = colours.BlueLighter; + private void load(OsuColour colours) => Colour = colours.BlueLighter; protected override string FormatCount(double count) => count.FormatAccuracy(); @@ -38,11 +39,7 @@ namespace osu.Game.Graphics.UserInterface } protected override OsuSpriteText CreateSpriteText() - { - var spriteText = base.CreateSpriteText(); - spriteText.Font = spriteText.Font.With(fixedWidth: true); - return spriteText; - } + => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f, fixedWidth: true)); public override void Increment(double amount) { diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index 76bb4bf69d..7c53d4fa0d 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -9,11 +9,10 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osuTK.Graphics; namespace osu.Game.Graphics.UserInterface { - public abstract class RollingCounter : Container, IHasAccentColour + public abstract class RollingCounter : Container where T : struct, IEquatable { /// @@ -60,38 +59,6 @@ namespace osu.Game.Graphics.UserInterface public abstract void Increment(T amount); - private float textSize = 40f; - - public float TextSize - { - get => displayedCountSpriteText?.Font.Size ?? textSize; - set - { - if (TextSize == value) - return; - - textSize = value; - if (displayedCountSpriteText != null) - displayedCountSpriteText.Font = displayedCountSpriteText.Font.With(size: value); - } - } - - private Color4 accentColour; - - public Color4 AccentColour - { - get => displayedCountSpriteText?.Colour ?? accentColour; - set - { - if (AccentColour == value) - return; - - accentColour = value; - if (displayedCountSpriteText != null) - displayedCountSpriteText.Colour = value; - } - } - /// /// Skeleton of a numeric counter which value rolls over time. /// @@ -185,8 +152,7 @@ namespace osu.Game.Graphics.UserInterface protected virtual OsuSpriteText CreateSpriteText() => new OsuSpriteText { - Font = OsuFont.Numeric.With(size: textSize), - Colour = accentColour, + Font = OsuFont.Numeric.With(size: 40f), }; } } diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 438fe6c13b..faabe69f87 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Graphics.UserInterface } [BackgroundDependencyLoader] - private void load(OsuColour colours) => AccentColour = colours.BlueLighter; + private void load(OsuColour colours) => Colour = colours.BlueLighter; protected override double GetProportionalDuration(double currentValue, double newValue) { @@ -50,11 +50,7 @@ namespace osu.Game.Graphics.UserInterface } protected override OsuSpriteText CreateSpriteText() - { - var spriteText = base.CreateSpriteText(); - spriteText.Font = spriteText.Font.With(fixedWidth: true); - return spriteText; - } + => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); public override void Increment(double amount) { diff --git a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs b/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs index af03cbb63e..aac0166774 100644 --- a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs +++ b/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics.Sprites; namespace osu.Game.Graphics.UserInterface { @@ -19,7 +21,7 @@ namespace osu.Game.Graphics.UserInterface } [BackgroundDependencyLoader] - private void load(OsuColour colours) => AccentColour = colours.BlueLighter; + private void load(OsuColour colours) => Colour = colours.BlueLighter; protected override string FormatCount(int count) { @@ -35,5 +37,8 @@ namespace osu.Game.Graphics.UserInterface { Current.Value += amount; } + + protected override OsuSpriteText CreateSpriteText() + => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f)); } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index f09745cf71..26aefa138b 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -232,7 +232,6 @@ namespace osu.Game.Screens.Play protected virtual RollingCounter CreateAccuracyCounter() => new PercentageCounter { - TextSize = 20, BypassAutoSizeAxes = Axes.X, Anchor = Anchor.TopLeft, Origin = Anchor.TopRight, @@ -241,14 +240,12 @@ namespace osu.Game.Screens.Play protected virtual ScoreCounter CreateScoreCounter() => new ScoreCounter(6) { - TextSize = 40, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }; protected virtual RollingCounter CreateComboCounter() => new SimpleComboCounter { - TextSize = 20, BypassAutoSizeAxes = Axes.X, Anchor = Anchor.TopRight, Origin = Anchor.TopLeft, From 5759ffff6fc8696dc7ca98ef507c5b39c6743334 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 07:45:05 +0300 Subject: [PATCH 2736/6909] Use the property instead of the backing field --- osu.Game/Graphics/UserInterface/RollingCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index 7c53d4fa0d..6763198213 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -77,7 +77,7 @@ namespace osu.Game.Graphics.UserInterface private void load() { displayedCountSpriteText = CreateSpriteText(); - displayedCountSpriteText.Text = FormatCount(displayedCount); + displayedCountSpriteText.Text = FormatCount(DisplayedCount); Child = displayedCountSpriteText; } From ee9fa11d142ed4fe14ca3dc06bfcc9edb56c02f5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 07:47:02 +0300 Subject: [PATCH 2737/6909] Use `With(s => ...)` extension for better readability --- .../Gameplay/Components/MatchScoreDisplay.cs | 23 ++++++++++--------- .../Expanded/Statistics/AccuracyStatistic.cs | 10 ++++---- .../Expanded/Statistics/CounterStatistic.cs | 10 ++++---- .../Ranking/Expanded/TotalScoreCounter.cs | 14 +++++------ 4 files changed, 26 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs index 25417921bc..695c6d6f3e 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/MatchScoreDisplay.cs @@ -137,19 +137,20 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components public bool Winning { - set => displayedSpriteText.Font = value + set => updateFont(value); + } + + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => + { + displayedSpriteText = s; + displayedSpriteText.Spacing = new Vector2(-6); + updateFont(false); + }); + + private void updateFont(bool winning) + => displayedSpriteText.Font = winning ? OsuFont.Torus.With(weight: FontWeight.Bold, size: 50, fixedWidth: true) : OsuFont.Torus.With(weight: FontWeight.Regular, size: 40, fixedWidth: true); - } - - protected override OsuSpriteText CreateSpriteText() - { - displayedSpriteText = base.CreateSpriteText(); - displayedSpriteText.Spacing = new Vector2(-6); - Winning = false; - - return displayedSpriteText; - } } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs index 921ad80976..6933456e7e 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs @@ -49,13 +49,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public override void Increment(double amount) => Current.Value += amount; - protected override OsuSpriteText CreateSpriteText() + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => { - var spriteText = base.CreateSpriteText(); - spriteText.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); - spriteText.Spacing = new Vector2(-2, 0); - return spriteText; - } + s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + s.Spacing = new Vector2(-2, 0); + }); } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index cc0f49c968..043a560d12 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -44,13 +44,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; - protected override OsuSpriteText CreateSpriteText() + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => { - var spriteText = base.CreateSpriteText(); - spriteText.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); - spriteText.Spacing = new Vector2(-2, 0); - return spriteText; - } + s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + s.Spacing = new Vector2(-2, 0); + }); public override void Increment(int amount) => Current.Value += amount; diff --git a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs index b0060d19ac..7f6fd1eabe 100644 --- a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs +++ b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs @@ -28,16 +28,14 @@ namespace osu.Game.Screens.Ranking.Expanded protected override string FormatCount(long count) => count.ToString("N0"); - protected override OsuSpriteText CreateSpriteText() + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => { - var spriteText = base.CreateSpriteText(); - spriteText.Anchor = Anchor.TopCentre; - spriteText.Origin = Anchor.TopCentre; + s.Anchor = Anchor.TopCentre; + s.Origin = Anchor.TopCentre; - spriteText.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true); - spriteText.Spacing = new Vector2(-5, 0); - return spriteText; - } + s.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true); + s.Spacing = new Vector2(-5, 0); + }); public override void Increment(long amount) => Current.Value += amount; From 422100192c3a6c14628b03369be6dbfdb93cf18d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 07:58:23 +0300 Subject: [PATCH 2738/6909] Move HasFont to legacy skin extensions class instead --- osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs | 2 +- osu.Game/Skinning/LegacySkinExtensions.cs | 3 +++ osu.Game/Skinning/LegacySkinTransformer.cs | 2 -- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index c2432e1dbb..0e434291c1 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.Skinning var fontOverlap = GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. - if (HasFont(comboFont)) + if (this.HasFont(comboFont)) return new LegacyComboCounter(Source, comboFont, fontOverlap); break; diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index b955150c90..851a8d56c9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Skinning var font = GetConfig(OsuSkinConfiguration.HitCirclePrefix)?.Value ?? "default"; var overlap = GetConfig(OsuSkinConfiguration.HitCircleOverlap)?.Value ?? -2; - return !HasFont(font) + return !this.HasFont(font) ? null : new LegacySpriteText(Source, font) { diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index bb46dc8b9f..0ee02a2442 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -62,6 +62,9 @@ namespace osu.Game.Skinning } } + public static bool HasFont(this ISkin source, string fontPrefix) + => source.GetTexture($"{fontPrefix}-0") != null; + public class SkinnableTextureAnimation : TextureAnimation { [Resolved(canBeNull: true)] diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index acca53835c..ebc4757e75 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -47,7 +47,5 @@ namespace osu.Game.Skinning } public abstract IBindable GetConfig(TLookup lookup); - - protected bool HasFont(string fontPrefix) => GetTexture($"{fontPrefix}-0") != null; } } From 885f8104f566cb350aef38bc8e99649f49a4a7ea Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 08:00:57 +0300 Subject: [PATCH 2739/6909] Always use public accessors even on legacy classes Because of https://github.com/ppy/osu-framework/issues/3727 --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 90dd1f4e9f..13c751ac5d 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -16,7 +16,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning { - internal class LegacyComboCounter : CompositeDrawable, ICatchComboCounter + public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter { private readonly ISkin skin; From d4bde0afe57c04a01fe5ab880f7dc70686fc0f0e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 08:18:30 +0300 Subject: [PATCH 2740/6909] Do not pass accent value on a reverted miss judgement --- osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index 10aee2ea31..351610646d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -47,6 +47,12 @@ namespace osu.Game.Rulesets.Catch.UI if (!result.Judgement.AffectsCombo || !result.HasResult) return; + if (result.Type == HitResult.Miss) + { + updateCombo(result.ComboAtJudgement, null); + return; + } + updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value); } From a59dabca7ed30c48e6c8cae98b8fe6c2558b9e5c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 08:28:11 +0300 Subject: [PATCH 2741/6909] Use existing hit objects instead of defining own --- .../TestSceneComboCounter.cs | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs index 2581e305dd..079cdfc44b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -55,25 +55,13 @@ namespace osu.Game.Rulesets.Catch.Tests private void performJudgement(HitResult type, Judgement judgement = null) { - var judgedObject = new TestDrawableCatchHitObject(new TestCatchHitObject()); + var judgedObject = new DrawableFruit(new Fruit()) { AccentColour = { Value = judgedObjectColour } }; + var result = new JudgementResult(judgedObject.HitObject, judgement ?? new Judgement()) { Type = type }; scoreProcessor.ApplyResult(result); foreach (var counter in CreatedDrawables.Cast()) counter.OnNewResult(judgedObject, result); } - - private class TestDrawableCatchHitObject : DrawableCatchHitObject - { - public TestDrawableCatchHitObject(CatchHitObject hitObject) - : base(hitObject) - { - AccentColour.Value = Color4.White; - } - } - - private class TestCatchHitObject : CatchHitObject - { - } } } From dde0bc6070f41bf001e4d108bf8a0fe77f22191b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 08:29:24 +0300 Subject: [PATCH 2742/6909] Add step to randomize judged object's combo colour --- .../TestSceneComboCounter.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs index 079cdfc44b..89521d616d 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; @@ -23,6 +24,8 @@ namespace osu.Game.Rulesets.Catch.Tests private GameplayBeatmap gameplayBeatmap; private readonly Bindable isBreakTime = new BindableBool(); + private Color4 judgedObjectColour = Color4.White; + [BackgroundDependencyLoader] private void load() { @@ -50,7 +53,17 @@ namespace osu.Game.Rulesets.Catch.Tests { AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20); AddStep("perform miss", () => performJudgement(HitResult.Miss)); + AddToggleStep("toggle gameplay break", v => isBreakTime.Value = v); + AddStep("randomize judged object colour", () => + { + judgedObjectColour = new Color4( + RNG.NextSingle(1f), + RNG.NextSingle(1f), + RNG.NextSingle(1f), + 1f + ); + }); } private void performJudgement(HitResult type, Judgement judgement = null) From af52b73b06ed9b6482ebb7cba4c0765b19ebdc24 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 19 Aug 2020 08:39:40 +0300 Subject: [PATCH 2743/6909] Fill out missing documentation --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 3 +++ osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 13c751ac5d..8ea06688cb 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -16,6 +16,9 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning { + /// + /// A combo counter implementation that visually behaves almost similar to osu!stable's combo counter. + /// public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter { private readonly ISkin skin; diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index 351610646d..b53711e4ed 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -10,6 +10,9 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.UI { + /// + /// Represents a component that displays a skinned and handles combo judgement results for updating it accordingly. + /// public class CatchComboDisplay : SkinnableDrawable { private int currentCombo; From 06503597e00f8b784c0c712f97c17ff589b086f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Aug 2020 19:09:35 +0900 Subject: [PATCH 2744/6909] Remove unnecessarily exposed visibility state --- osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 5170058700..56c030df77 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -84,16 +84,14 @@ namespace osu.Game.Tests.Visual.Menus AddStep("try to show toolbar", () => toolbar.Show()); if (mode == OverlayActivation.Disabled) - AddUntilStep("toolbar still hidden", () => toolbar.Visibility == Visibility.Hidden); + AddUntilStep("toolbar still hidden", () => toolbar.State.Value == Visibility.Hidden); else - AddAssert("toolbar is visible", () => toolbar.Visibility == Visibility.Visible); + AddAssert("toolbar is visible", () => toolbar.State.Value == Visibility.Visible); } public class TestToolbar : Toolbar { public new Bindable OverlayActivationMode => base.OverlayActivationMode; - - public Visibility Visibility => State.Value; } } } From 3e4eae7fe4bcad660abf0bb6018e3f2f0d66bf3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Aug 2020 19:10:45 +0900 Subject: [PATCH 2745/6909] Remove unnecessary until step --- osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 56c030df77..f819ae4682 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("try to show toolbar", () => toolbar.Show()); if (mode == OverlayActivation.Disabled) - AddUntilStep("toolbar still hidden", () => toolbar.State.Value == Visibility.Hidden); + AddAssert("toolbar still hidden", () => toolbar.State.Value == Visibility.Hidden); else AddAssert("toolbar is visible", () => toolbar.State.Value == Visibility.Visible); } From f6ca31688e73757dbf12a37381f52e6c91917f46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Aug 2020 21:39:55 +0900 Subject: [PATCH 2746/6909] Fix incorrect spacing --- 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 41f3090afd..7c5d41efcf 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Mania.Skinning (explosion as IFramedAnimation)?.GotoFrame(0); explosion?.FadeInFromZero(80) - .Then().FadeOut(120); + .Then().FadeOut(120); } } } From 4397be60e2a3bd249662437d9f6e1272b470bc0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Aug 2020 21:58:02 +0900 Subject: [PATCH 2747/6909] 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 f3fb949f76..1a76a24496 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 a12ce138bd..d1e2033596 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 0170e94140..9b25eaab41 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 1badc584f6cc7920558fce900d6a275f62f55188 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 19 Aug 2020 22:10:58 +0900 Subject: [PATCH 2748/6909] Update textbox event names --- osu.Game/Graphics/UserInterface/OsuTextBox.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 0d173e2d3e..1ec4dfc91a 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -74,9 +74,9 @@ namespace osu.Game.Graphics.UserInterface protected override Color4 SelectionColour => new Color4(249, 90, 255, 255); - protected override void OnTextAdded(string added) + protected override void OnUserTextAdded(string added) { - base.OnTextAdded(added); + base.OnUserTextAdded(added); if (added.Any(char.IsUpper) && AllowUniqueCharacterSamples) capsTextAddedSample?.Play(); @@ -84,9 +84,9 @@ namespace osu.Game.Graphics.UserInterface textAddedSamples[RNG.Next(0, 3)]?.Play(); } - protected override void OnTextRemoved(string removed) + protected override void OnUserTextRemoved(string removed) { - base.OnTextRemoved(removed); + base.OnUserTextRemoved(removed); textRemovedSample?.Play(); } From ff0dec3dd928aa0ab0467cadb1027414bfe1606e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Aug 2020 12:23:39 +0900 Subject: [PATCH 2749/6909] Update plist path to work with newer fastlane version It seems they have fixed the working/current directory and the parent traversal is no longer required. --- fastlane/Fastfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 4fd0e5e8c7..8c278604aa 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -113,7 +113,7 @@ platform :ios do souyuz( platform: "ios", - plist_path: "../osu.iOS/Info.plist" + plist_path: "osu.iOS/Info.plist" ) end @@ -127,7 +127,7 @@ platform :ios do end lane :update_version do |options| - options[:plist_path] = '../osu.iOS/Info.plist' + options[:plist_path] = 'osu.iOS/Info.plist' app_version(options) end From 6358f4a661f282719597d215a1129af97e63ae5b Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Thu, 20 Aug 2020 17:33:08 +0930 Subject: [PATCH 2750/6909] Disable CA2225 warning regarding operator overloads --- .editorconfig | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index 67f98f94eb..a5f7795882 100644 --- a/.editorconfig +++ b/.editorconfig @@ -191,4 +191,7 @@ dotnet_diagnostic.IDE0052.severity = silent #Rules for disposable dotnet_diagnostic.IDE0067.severity = none dotnet_diagnostic.IDE0068.severity = none -dotnet_diagnostic.IDE0069.severity = none \ No newline at end of file +dotnet_diagnostic.IDE0069.severity = none + +#Disable operator overloads requiring alternate named methods +dotnet_diagnostic.CA2225.severity = none \ No newline at end of file From 1f14d9b690d39122cd89ef799429c12c7511e34e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Aug 2020 18:15:06 +0900 Subject: [PATCH 2751/6909] Use correct width adjust for osu!catch playfield --- .../UI/CatchPlayfieldAdjustmentContainer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs index 8ee23461ba..040247a264 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs @@ -10,6 +10,8 @@ namespace osu.Game.Rulesets.Catch.UI { public class CatchPlayfieldAdjustmentContainer : PlayfieldAdjustmentContainer { + private const float playfield_size_adjust = 0.8f; + protected override Container Content => content; private readonly Container content; @@ -18,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.UI Anchor = Anchor.TopCentre; Origin = Anchor.TopCentre; - Size = new Vector2(0.86f); // matches stable's vertical offset for catcher plate + Size = new Vector2(playfield_size_adjust); InternalChild = new Container { From a94a86178bcbbde7a7b1a96cf871be29c3e96b40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Aug 2020 19:12:37 +0900 Subject: [PATCH 2752/6909] Align osu!catch playfield with stable 1:1 --- .../UI/CatchPlayfieldAdjustmentContainer.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs index 040247a264..efc1b24ed5 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfieldAdjustmentContainer.cs @@ -17,8 +17,12 @@ namespace osu.Game.Rulesets.Catch.UI public CatchPlayfieldAdjustmentContainer() { - Anchor = Anchor.TopCentre; - Origin = Anchor.TopCentre; + // because we are using centre anchor/origin, we will need to limit visibility in the future + // to ensure tall windows do not get a readability advantage. + // it may be possible to bake the catch-specific offsets (-100..340 mentioned below) into new values + // which are compatible with TopCentre alignment. + Anchor = Anchor.Centre; + Origin = Anchor.Centre; Size = new Vector2(playfield_size_adjust); @@ -29,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, FillAspectRatio = 4f / 3, - Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both } + Child = content = new ScalingContainer { RelativeSizeAxes = Axes.Both, } }; } @@ -42,8 +46,14 @@ namespace osu.Game.Rulesets.Catch.UI { base.Update(); + // in stable, fruit fall vertically from -100 to 340. + // to emulate this, we want to make our playfield 440 gameplay pixels high. + // we then offset it -100 vertically in the position set below. + const float stable_v_offset_ratio = 440 / 384f; + Scale = new Vector2(Parent.ChildSize.X / CatchPlayfield.WIDTH); - Size = Vector2.Divide(Vector2.One, Scale); + Position = new Vector2(0, -100 * stable_v_offset_ratio + Scale.X); + Size = Vector2.Divide(new Vector2(1, stable_v_offset_ratio), Scale); } } } From e6d13edafb8200de6122d685dd5cdffaf724d2c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 20 Aug 2020 19:41:27 +0900 Subject: [PATCH 2753/6909] Force tournament client to run in windowed mode We generally haven't tested in other modes, and it doesn't really make sense as you wouldn't be able to use it in a meaningful way otherwise. - [ ] Test on windows. --- osu.Game.Tournament/TournamentGame.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 7b1a174c1e..307ee1c773 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -31,6 +31,7 @@ namespace osu.Game.Tournament public static readonly Color4 TEXT_COLOUR = Color4Extensions.FromHex("#fff"); private Drawable heightWarning; private Bindable windowSize; + private Bindable windowMode; [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfig) @@ -43,6 +44,12 @@ namespace osu.Game.Tournament heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; }), true); + windowMode = frameworkConfig.GetBindable(FrameworkSetting.WindowMode); + windowMode.BindValueChanged(mode => ScheduleAfterChildren(() => + { + windowMode.Value = WindowMode.Windowed; + }), true); + AddRange(new[] { new Container From c89509aca01da1f461382a3851f72c32131fc54e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 20 Aug 2020 20:25:40 +0900 Subject: [PATCH 2754/6909] Fix right bound not being applied correctly --- .../CatchBeatmapConversionTest.cs | 1 + .../Beatmaps/CatchBeatmapProcessor.cs | 2 +- ...t-bound-hr-offset-expected-conversion.json | 17 ++++++++++++++++ .../Beatmaps/right-bound-hr-offset.osu | 20 +++++++++++++++++++ 4 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json create mode 100644 osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index df54df7b01..8c48158acd 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase("hardrock-stream", new[] { typeof(CatchModHardRock) })] [TestCase("hardrock-repeat-slider", new[] { typeof(CatchModHardRock) })] [TestCase("hardrock-spinner", new[] { typeof(CatchModHardRock) })] + [TestCase("right-bound-hr-offset", new[] { typeof(CatchModHardRock) })] public new void Test(string name, params Type[] mods) => base.Test(name, mods); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index bb14988414..15e6e98f5a 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps if (amount > 0) { // Clamp to the right bound - if (position + amount < 1) + if (position + amount < CatchPlayfield.WIDTH) position += amount; } else diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json new file mode 100644 index 0000000000..3bde97070c --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset-expected-conversion.json @@ -0,0 +1,17 @@ +{ + "Mappings": [{ + "StartTime": 3368, + "Objects": [{ + "StartTime": 3368, + "Position": 374 + }] + }, + { + "StartTime": 3501, + "Objects": [{ + "StartTime": 3501, + "Position": 446 + }] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu new file mode 100644 index 0000000000..6630f369d5 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Resources/Testing/Beatmaps/right-bound-hr-offset.osu @@ -0,0 +1,20 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 2 + +[Difficulty] +HPDrainRate:6 +CircleSize:4 +OverallDifficulty:9.6 +ApproachRate:9.6 +SliderMultiplier:1.9 +SliderTickRate:1 + +[TimingPoints] +2169,266.666666666667,4,2,1,70,1,0 + +[HitObjects] +374,60,3368,1,0,0:0:0:0: +410,146,3501,1,2,0:1:0:0: \ No newline at end of file From f1e09466036ae60f50296dd22c700cc6e14e9522 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 20 Aug 2020 22:38:47 +0900 Subject: [PATCH 2755/6909] Remove release samples in invert mod --- osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs index 56f6e389bf..593b459e8a 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs @@ -60,12 +60,7 @@ namespace osu.Game.Rulesets.Mania.Mods Column = column.Key, StartTime = locations[i].startTime, Duration = duration, - Samples = locations[i].samples, - NodeSamples = new List> - { - locations[i].samples, - locations[i + 1].samples - } + NodeSamples = new List> { locations[i].samples, new List() } }); } From 54a2322090a7c1555e7c170ce28443c46f1b20cc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 20 Aug 2020 22:51:52 +0900 Subject: [PATCH 2756/6909] Use Array.Empty<> --- osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs index 593b459e8a..1ea45c295c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModInvert.cs @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Mania.Mods Column = column.Key, StartTime = locations[i].startTime, Duration = duration, - NodeSamples = new List> { locations[i].samples, new List() } + NodeSamples = new List> { locations[i].samples, Array.Empty() } }); } From a193fb79071a6cbfb8ddf09b1b27e2b38987bcb1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 20 Aug 2020 23:15:30 +0900 Subject: [PATCH 2757/6909] Fix test not working for droplets/tinydroplets --- .../TestSceneFruitObjects.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index c07e4fdad3..6182faedd1 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -30,9 +30,8 @@ namespace osu.Game.Rulesets.Catch.Tests private Drawable createDrawableTinyDroplet() { - var droplet = new TinyDroplet + var droplet = new TestCatchTinyDroplet { - StartTime = Clock.CurrentTime, Scale = 1.5f, }; @@ -49,9 +48,8 @@ namespace osu.Game.Rulesets.Catch.Tests private Drawable createDrawableDroplet() { - var droplet = new Droplet + var droplet = new TestCatchDroplet { - StartTime = Clock.CurrentTime, Scale = 1.5f, }; @@ -95,5 +93,21 @@ namespace osu.Game.Rulesets.Catch.Tests public override FruitVisualRepresentation VisualRepresentation { get; } } + + public class TestCatchDroplet : Droplet + { + public TestCatchDroplet() + { + StartTime = 1000000000000; + } + } + + public class TestCatchTinyDroplet : TinyDroplet + { + public TestCatchTinyDroplet() + { + StartTime = 1000000000000; + } + } } } From 725caa9382128e5dedfec1a664e26dae89b7489e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 20 Aug 2020 23:16:37 +0900 Subject: [PATCH 2758/6909] Add visual test for hyperdash droplets --- osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 6182faedd1..385d8ed7fa 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -20,12 +20,13 @@ namespace osu.Game.Rulesets.Catch.Tests foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) AddStep($"show {rep}", () => SetContents(() => createDrawable(rep))); - AddStep("show droplet", () => SetContents(createDrawableDroplet)); - + AddStep("show droplet", () => SetContents(() => createDrawableDroplet())); AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true))); + + AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true))); } private Drawable createDrawableTinyDroplet() @@ -46,11 +47,12 @@ namespace osu.Game.Rulesets.Catch.Tests }; } - private Drawable createDrawableDroplet() + private Drawable createDrawableDroplet(bool hyperdash = false) { var droplet = new TestCatchDroplet { Scale = 1.5f, + HyperDashTarget = hyperdash ? new Banana() : null }; return new DrawableDroplet(droplet) From 40a456170b73754fd172c2cb3bf4bc697f5c3bcd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 20 Aug 2020 23:34:40 +0900 Subject: [PATCH 2759/6909] Add default skin display for hyperdash droplets --- .../Objects/Drawables/DrawableDroplet.cs | 7 +- .../Objects/Drawables/DropletPiece.cs | 70 +++++++++++++++++++ .../Objects/Drawables/FruitPiece.cs | 6 -- 3 files changed, 71 insertions(+), 12 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index cad8892283..592b69d963 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Utils; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables @@ -21,11 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new Pulp - { - Size = Size / 4, - AccentColour = { BindTarget = AccentColour } - }); + ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece()); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs new file mode 100644 index 0000000000..d6c9f4398f --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + public class DropletPiece : CompositeDrawable + { + public DropletPiece() + { + Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); + } + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableObject) + { + DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; + var hitObject = drawableCatchObject.HitObject; + + InternalChild = new Pulp + { + // RelativeSizeAxes is not used since the edge effect uses Size. + Size = Size, + AccentColour = { BindTarget = drawableObject.AccentColour } + }; + + if (hitObject.HyperDash) + { + AddInternal(new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = new Vector2(2f), + Depth = 1, + Children = new Drawable[] + { + new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR, + BorderThickness = 6, + Children = new Drawable[] + { + new Box + { + AlwaysPresent = true, + Alpha = 0.3f, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR, + } + } + } + } + }); + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs index 7ac9f11ad6..4bffdab3d8 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -21,11 +20,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public const float RADIUS_ADJUST = 1.1f; private Circle border; - private CatchHitObject hitObject; - private readonly IBindable accentColour = new Bindable(); - public FruitPiece() { RelativeSizeAxes = Axes.Both; @@ -37,8 +33,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; hitObject = drawableCatchObject.HitObject; - accentColour.BindTo(drawableCatchObject.AccentColour); - AddRangeInternal(new[] { getFruitFor(drawableCatchObject.HitObject.VisualRepresentation), From 35ff25940b11437e8823b0b904c78c99ee101428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Aug 2020 18:16:32 +0200 Subject: [PATCH 2760/6909] Add sample playback to juice stream test scene --- osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index d6bba3d55e..3c636a5b97 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; @@ -38,7 +40,11 @@ namespace osu.Game.Rulesets.Catch.Tests new Vector2(width, 0) }), StartTime = i * 2000, - NewCombo = i % 8 == 0 + NewCombo = i % 8 == 0, + Samples = new List(new[] + { + new HitSampleInfo { Bank = "normal", Name = "hitnormal", Volume = 100 } + }) }); } From 45e2ea71b4e28380fdcad4719271fa7438beab98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Aug 2020 18:41:08 +0200 Subject: [PATCH 2761/6909] Rename Palpable{-> Drawable}CatchHitObject --- .../Objects/Drawables/DrawableCatchHitObject.cs | 4 ++-- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs | 2 +- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index c6345a9df7..883d2048ed 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -15,14 +15,14 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public abstract class PalpableCatchHitObject : DrawableCatchHitObject + public abstract class PalpableDrawableCatchHitObject : DrawableCatchHitObject where TObject : CatchHitObject { public override bool CanBePlated => true; protected Container ScaleContainer { get; private set; } - protected PalpableCatchHitObject(TObject hitObject) + protected PalpableDrawableCatchHitObject(TObject hitObject) : base(hitObject) { Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index cad8892283..77ae7e9a54 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableDroplet : PalpableCatchHitObject + public class DrawableDroplet : PalpableDrawableCatchHitObject { public override bool StaysOnPlate => false; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index fae5a10d04..c1c34e4157 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -8,7 +8,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableFruit : PalpableCatchHitObject + public class DrawableFruit : PalpableDrawableCatchHitObject { public DrawableFruit(Fruit h) : base(h) From f956c9fe37925edb9780f778b181b24a6a44dade Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 21 Aug 2020 02:01:29 +0900 Subject: [PATCH 2762/6909] Clobber in a gameplay test --- .../TestSceneHyperDash.cs | 19 +++++++++++++++++++ osu.Game.Rulesets.Catch/UI/Catcher.cs | 5 ++--- 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index ad24adf352..1aa333c401 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; @@ -31,6 +32,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); } + + AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); } private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; @@ -46,6 +49,8 @@ namespace osu.Game.Rulesets.Catch.Tests } }; + beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); + // Should produce a hyper-dash (edge case test) beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true }); beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true }); @@ -63,6 +68,20 @@ namespace osu.Game.Rulesets.Catch.Tests createObjects(() => new Fruit { X = right_x }); createObjects(() => new TestJuiceStream(left_x), 1); + beatmap.ControlPointInfo.Add(7900, new TimingControlPoint + { + BeatLength = 50 + }); + + createObjects(() => new TestJuiceStream(left_x) + { + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero), + new PathControlPoint(new Vector2(512, 0)) + }) + }, 1); + return beatmap; void createObjects(Func createObject, int count = 3) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 8820dff730..0897ccf2d5 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -226,9 +226,8 @@ 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; + // only update hyperdash state if we are catching a fruit or a droplet (and not a tiny droplet). + if (!(fruit is Fruit || fruit is Droplet) || fruit is TinyDroplet) return validCatch; if (validCatch && fruit.HyperDash) { From 28534c1599ed85f8b1a6a1a99fc3c78d7f21b131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Aug 2020 18:48:01 +0200 Subject: [PATCH 2763/6909] Reintroduce PalpableCatchHitObject at data level --- osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs | 13 +++++++++++++ .../Objects/Drawables/DrawableCatchHitObject.cs | 8 ++------ osu.Game.Rulesets.Catch/Objects/Droplet.cs | 2 +- osu.Game.Rulesets.Catch/Objects/Fruit.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 2 +- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 04932ecdbb..5985ec9b68 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -27,6 +27,11 @@ namespace osu.Game.Rulesets.Catch.Objects set => x = value; } + /// + /// Whether this object can be placed on the catcher's plate. + /// + public virtual bool CanBePlated => false; + /// /// A random offset applied to , set by the . /// @@ -100,6 +105,14 @@ namespace osu.Game.Rulesets.Catch.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; } + /// + /// Represents a single object that can be caught by the catcher. + /// + public abstract class PalpableCatchHitObject : CatchHitObject + { + public override bool CanBePlated => true; + } + public enum FruitVisualRepresentation { Pear, diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 883d2048ed..2fe017dc62 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -16,10 +16,8 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { public abstract class PalpableDrawableCatchHitObject : DrawableCatchHitObject - where TObject : CatchHitObject + where TObject : PalpableCatchHitObject { - public override bool CanBePlated => true; - protected Container ScaleContainer { get; private set; } protected PalpableDrawableCatchHitObject(TObject hitObject) @@ -65,9 +63,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public abstract class DrawableCatchHitObject : DrawableHitObject { - public virtual bool CanBePlated => false; - - public virtual bool StaysOnPlate => CanBePlated; + public virtual bool StaysOnPlate => HitObject.CanBePlated; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; diff --git a/osu.Game.Rulesets.Catch/Objects/Droplet.cs b/osu.Game.Rulesets.Catch/Objects/Droplet.cs index 7b0bb3f0ae..9c1004a04b 100644 --- a/osu.Game.Rulesets.Catch/Objects/Droplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Droplet.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets.Judgements; namespace osu.Game.Rulesets.Catch.Objects { - public class Droplet : CatchHitObject + public class Droplet : PalpableCatchHitObject { public override Judgement CreateJudgement() => new CatchDropletJudgement(); } diff --git a/osu.Game.Rulesets.Catch/Objects/Fruit.cs b/osu.Game.Rulesets.Catch/Objects/Fruit.cs index 6f0423b420..43486796ad 100644 --- a/osu.Game.Rulesets.Catch/Objects/Fruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Fruit.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets.Judgements; namespace osu.Game.Rulesets.Catch.Objects { - public class Fruit : CatchHitObject + public class Fruit : PalpableCatchHitObject { public override Judgement CreateJudgement() => new CatchJudgement(); } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 4255c3b1af..03ebf01b9b 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.UI lastPlateableFruit.OnLoadComplete += _ => action(); } - if (result.IsHit && fruit.CanBePlated) + if (result.IsHit && fruit.HitObject.CanBePlated) { // create a new (cloned) fruit to stay on the plate. the original is faded out immediately. var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject); From 9546fbb64bf823392b92333238ed1cf560c6fcae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Aug 2020 18:58:07 +0200 Subject: [PATCH 2764/6909] Prevent catcher from performing invalid catches --- 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 8820dff730..e4a3c01dbc 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -216,6 +216,9 @@ namespace osu.Game.Rulesets.Catch.UI /// Whether the catch is possible. public bool AttemptCatch(CatchHitObject fruit) { + if (!fruit.CanBePlated) + return false; + var halfCatchWidth = catchWidth * 0.5f; // this stuff wil disappear once we move fruit to non-relative coordinate space in the future. From 738ff7ba217e1db02c5f9232076a61b8438a2229 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 21 Aug 2020 02:21:16 +0900 Subject: [PATCH 2765/6909] Use full catcher width for hyperdash calculation --- osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs | 6 ++++++ osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 15e6e98f5a..a08c5b6fb1 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -212,6 +212,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; + + // Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins. + // This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible. + // For now, to bring gameplay (and diffcalc!) completely in-line with stable, this code also uses the full catcher size. + halfCatcherWidth /= Catcher.ALLOWED_CATCH_RANGE; + int lastDirection = 0; double lastExcess = halfCatcherWidth; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 8820dff730..11e69678ca 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable. /// - private const float allowed_catch_range = 0.8f; + public const float ALLOWED_CATCH_RANGE = 0.8f; /// /// The drawable catcher for . @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The scale of the catcher. internal static float CalculateCatchWidth(Vector2 scale) - => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range; + => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE; /// /// Calculates the width of the area used for attempting catches in gameplay. From bd4acdce789776c1252c7f60604296b6ae63bd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Aug 2020 21:01:58 +0200 Subject: [PATCH 2766/6909] Add until step to ensure failure --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs index 61859c9da3..dbf5b98e52 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs @@ -14,7 +14,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { Screens.Multi.Multiplayer multi = new Screens.Multi.Multiplayer(); - AddStep(@"show", () => LoadScreen(multi)); + AddStep("show", () => LoadScreen(multi)); + AddUntilStep("wait for loaded", () => multi.IsLoaded); } } } From dcce7a213052cc16b49196c3c3441c38ee4f9809 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 20 Aug 2020 21:03:27 +0200 Subject: [PATCH 2767/6909] Cache local music controller to resolve failure --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs index dbf5b98e52..3924b0333f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Overlays; namespace osu.Game.Tests.Visual.Multiplayer { @@ -10,6 +12,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { protected override bool UseOnlineAPI => true; + [Cached] + private MusicController musicController { get; set; } = new MusicController(); + public TestSceneMultiScreen() { Screens.Multi.Multiplayer multi = new Screens.Multi.Multiplayer(); From f00bc67aaa0852e9ed77f83eced4bcaeb9ebd35a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 21 Aug 2020 12:29:28 +0900 Subject: [PATCH 2768/6909] Fix pulp and use relative sizse --- osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs | 3 +-- osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs index d6c9f4398f..c2499446fa 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs @@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables InternalChild = new Pulp { - // RelativeSizeAxes is not used since the edge effect uses Size. - Size = Size, + RelativeSizeAxes = Axes.Both, AccentColour = { BindTarget = drawableObject.AccentColour } }; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs index 1e7506a257..d3e4945611 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, - Radius = Size.X / 2, + Radius = DrawWidth / 2, Colour = colour.NewValue.Darken(0.2f).Opacity(0.75f) }; } From dd1f2db1752be453e14964d6b47f87aa04e8a611 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 21 Aug 2020 12:30:33 +0900 Subject: [PATCH 2769/6909] Use startTime in test --- osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 1aa333c401..6dab2a0b56 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Catch.Tests createObjects(() => new Fruit { X = right_x }); createObjects(() => new TestJuiceStream(left_x), 1); - beatmap.ControlPointInfo.Add(7900, new TimingControlPoint + beatmap.ControlPointInfo.Add(startTime, new TimingControlPoint { BeatLength = 50 }); From 6ad7a3686b011c35cb17e6bfa8d0303e1cf9fc78 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 21 Aug 2020 13:13:08 +0900 Subject: [PATCH 2770/6909] Simplify condition --- 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 7fffa1fdc3..8e74437834 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -229,8 +229,8 @@ namespace osu.Game.Rulesets.Catch.UI catchObjectPosition >= catcherPosition - halfCatchWidth && catchObjectPosition <= catcherPosition + halfCatchWidth; - // only update hyperdash state if we are catching a fruit or a droplet (and not a tiny droplet). - if (!(fruit is Fruit || fruit is Droplet) || fruit is TinyDroplet) return validCatch; + // only update hyperdash state if we are catching not catching a tiny droplet. + if (fruit is TinyDroplet) return validCatch; if (validCatch && fruit.HyperDash) { From 62d833d63d93690d5a40162d09d2ee763858c4cd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 21 Aug 2020 13:14:50 +0900 Subject: [PATCH 2771/6909] Fix comment --- 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 8e74437834..952ff6b0ce 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -229,7 +229,7 @@ namespace osu.Game.Rulesets.Catch.UI catchObjectPosition >= catcherPosition - halfCatchWidth && catchObjectPosition <= catcherPosition + halfCatchWidth; - // only update hyperdash state if we are catching not catching a tiny droplet. + // only update hyperdash state if we are not catching a tiny droplet. if (fruit is TinyDroplet) return validCatch; if (validCatch && fruit.HyperDash) From 526f06be4c714410061a732dfb041c84c3c45f0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 13:53:12 +0900 Subject: [PATCH 2772/6909] Add back track loaded bool in WorkingBeatmap --- osu.Game/Beatmaps/WorkingBeatmap.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 051d66af7b..8d5543cadb 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -275,6 +275,11 @@ namespace osu.Game.Beatmaps /// The track to transfer. public void TransferTrack([NotNull] Track track) => loadedTrack = track ?? throw new ArgumentNullException(nameof(track)); + /// + /// Whether this beatmap's track has been loaded via . + /// + public bool TrackLoaded => loadedTrack != null; + /// /// Get the loaded audio track instance. must have first been called. /// This generally happens via MusicController when changing the global beatmap. @@ -283,7 +288,7 @@ namespace osu.Game.Beatmaps { get { - if (loadedTrack == null) + if (!TrackLoaded) throw new InvalidOperationException($"Cannot access {nameof(Track)} without first calling {nameof(LoadTrack)}."); return loadedTrack; From 0b0ff626477a611a161ece951ffb9f0682dd8a44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 14:46:23 +0900 Subject: [PATCH 2773/6909] Switch timeline to use track directly from beatmap again --- .../Compose/Components/Timeline/Timeline.cs | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index e1702d3eff..96c48c0ddc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -30,6 +30,28 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private MusicController musicController { get; set; } + /// + /// The timeline's scroll position in the last frame. + /// + private float lastScrollPosition; + + /// + /// The track time in the last frame. + /// + private double lastTrackTime; + + /// + /// Whether the user is currently dragging the timeline. + /// + private bool handlingDragInput; + + /// + /// Whether the track was playing before a user drag event. + /// + private bool trackWasPlaying; + + private ITrack track; + public Timeline() { ZoomDuration = 200; @@ -61,9 +83,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Beatmap.BindValueChanged(b => { waveform.Waveform = b.NewValue.Waveform; - track = musicController.CurrentTrack; + track = b.NewValue.Track; - if (track.Length > 0) + // todo: i don't think this is safe, the track may not be loaded yet. + if (b.NewValue.Track.Length > 0) { MaxZoom = getZoomLevelForVisibleMilliseconds(500); MinZoom = getZoomLevelForVisibleMilliseconds(10000); @@ -74,28 +97,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private float getZoomLevelForVisibleMilliseconds(double milliseconds) => (float)(track.Length / milliseconds); - /// - /// The timeline's scroll position in the last frame. - /// - private float lastScrollPosition; - - /// - /// The track time in the last frame. - /// - private double lastTrackTime; - - /// - /// Whether the user is currently dragging the timeline. - /// - private bool handlingDragInput; - - /// - /// Whether the track was playing before a user drag event. - /// - private bool trackWasPlaying; - - private ITrack track; - protected override void Update() { base.Update(); From d2c2e8bbe89536f5fed2a35ea35aa514029ac28d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 15:05:56 +0900 Subject: [PATCH 2774/6909] Revert some more usage of MusicController back to WorkingBeatmap --- .../TestSceneHoldNoteInput.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 2 +- .../Skinning/TestSceneDrawableTaikoMascot.cs | 4 ++-- .../Skinning/TestSceneTaikoPlayfield.cs | 2 +- osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs | 10 +++++++--- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs | 2 ++ osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs | 3 +-- 9 files changed, 17 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 19b69bac6d..95072cf4f8 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -343,7 +343,7 @@ namespace osu.Game.Rulesets.Mania.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack.CurrentTime == 0); + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 744ad46c28..854626d362 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -385,7 +385,7 @@ namespace osu.Game.Rulesets.Osu.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack.CurrentTime == 0); + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 1690f648f9..b543b6fa94 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -366,7 +366,7 @@ namespace osu.Game.Rulesets.Osu.Tests judgementResults = new List(); }); - AddUntilStep("Beatmap at 0", () => MusicController.CurrentTrack.CurrentTime == 0); + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 6141bf062e..47d8a5c012 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -175,11 +175,11 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning private void createDrawableRuleset() { - AddUntilStep("wait for beatmap to be loaded", () => MusicController.CurrentTrack.TrackLoaded); + AddUntilStep("wait for beatmap to be loaded", () => Beatmap.Value.Track.IsLoaded); AddStep("create drawable ruleset", () => { - MusicController.CurrentTrack.Start(); + Beatmap.Value.Track.Start(); SetContents(() => { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs index a3d3bc81c4..7b7e2c43d1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoPlayfield.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = 1000 }); - MusicController.CurrentTrack.Start(); + Beatmap.Value.Track.Start(); }); AddStep("Load playfield", () => SetContents(() => new TaikoPlayfield(new ControlPointInfo()) diff --git a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs index 49b40daf99..eff430ac25 100644 --- a/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs +++ b/osu.Game.Tests/Skins/TestSceneBeatmapSkinResources.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -18,17 +19,20 @@ namespace osu.Game.Tests.Skins [Resolved] private BeatmapManager beatmaps { get; set; } + private WorkingBeatmap beatmap; + [BackgroundDependencyLoader] private void load() { var imported = beatmaps.Import(new ZipArchiveReader(TestResources.OpenResource("Archives/ogg-beatmap.osz"))).Result; - Beatmap.Value = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); + beatmap = beatmaps.GetWorkingBeatmap(imported.Beatmaps[0]); + beatmap.LoadTrack(); } [Test] - public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => Beatmap.Value.Skin.GetSample(new SampleInfo("sample")) != null); + public void TestRetrieveOggSample() => AddAssert("sample is non-null", () => beatmap.Skin.GetSample(new SampleInfo("sample")) != null); [Test] - public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => !MusicController.CurrentTrack.IsDummyDevice); + public void TestRetrieveOggTrack() => AddAssert("track is non-null", () => !(beatmap.Track is TrackVirtual)); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 8d5543cadb..6a89739e6f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -278,7 +278,7 @@ namespace osu.Game.Beatmaps /// /// Whether this beatmap's track has been loaded via . /// - public bool TrackLoaded => loadedTrack != null; + public virtual bool TrackLoaded => loadedTrack != null; /// /// Get the loaded audio track instance. must have first been called. diff --git a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs index d091da3206..bfcb2403c1 100644 --- a/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestWorkingBeatmap.cs @@ -27,6 +27,8 @@ namespace osu.Game.Tests.Beatmaps this.storyboard = storyboard; } + public override bool TrackLoaded => true; + public override bool BeatmapLoaded => true; protected override IBeatmap GetBeatmap() => beatmap; diff --git a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs index 7651285970..ad24ffc7b8 100644 --- a/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs +++ b/osu.Game/Tests/Visual/RateAdjustedBeatmapTestScene.cs @@ -13,8 +13,7 @@ namespace osu.Game.Tests.Visual base.Update(); // note that this will override any mod rate application - if (MusicController.TrackLoaded) - MusicController.CurrentTrack.Tempo.Value = Clock.Rate; + Beatmap.Value.Track.Tempo.Value = Clock.Rate; } } } From f7e4feee3449630475a11a2291803997fb23e029 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 15:25:57 +0900 Subject: [PATCH 2775/6909] Update remaining Player components to use WorkingBeatmap again --- osu.Game/Screens/Play/FailAnimation.cs | 13 ++++++------- osu.Game/Screens/Play/Player.cs | 8 ++++---- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index dade904180..54c644c999 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -6,12 +6,12 @@ using osu.Framework.Bindables; using osu.Game.Rulesets.UI; using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using osu.Framework.Allocation; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; @@ -28,24 +28,23 @@ namespace osu.Game.Screens.Play private readonly DrawableRuleset drawableRuleset; - [NotNull] - private readonly ITrack track; - private readonly BindableDouble trackFreq = new BindableDouble(1); + private Track track; + private const float duration = 2500; private SampleChannel failSample; - public FailAnimation(DrawableRuleset drawableRuleset, [NotNull] ITrack track) + public FailAnimation(DrawableRuleset drawableRuleset) { this.drawableRuleset = drawableRuleset; - this.track = track; } [BackgroundDependencyLoader] - private void load(AudioManager audio) + private void load(AudioManager audio, IBindable beatmap) { + track = beatmap.Value.Track; failSample = audio.Samples.Get(@"Gameplay/failsound"); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index cc70995b26..a8fda10604 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -151,7 +151,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuConfigManager config, MusicController musicController) + private void load(AudioManager audio, OsuConfigManager config) { Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray(); @@ -188,7 +188,7 @@ namespace osu.Game.Screens.Play addUnderlayComponents(GameplayClockContainer); addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap); - addOverlayComponents(GameplayClockContainer, Beatmap.Value, musicController.CurrentTrack); + addOverlayComponents(GameplayClockContainer, Beatmap.Value); if (!DrawableRuleset.AllowGameplayOverlays) { @@ -265,7 +265,7 @@ namespace osu.Game.Screens.Play }); } - private void addOverlayComponents(Container target, WorkingBeatmap working, ITrack track) + private void addOverlayComponents(Container target, WorkingBeatmap working) { target.AddRange(new[] { @@ -332,7 +332,7 @@ namespace osu.Game.Screens.Play performImmediateExit(); }, }, - failAnimation = new FailAnimation(DrawableRuleset, track) { OnComplete = onFailComplete, }, + failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, }); } From 0ae460fb8f4e7b9c235ce0cb27ae933157d162c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 15:50:14 +0900 Subject: [PATCH 2776/6909] Avoid beatmap load call in IntroScreen --- osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs | 3 ++- osu.Game/Screens/Menu/IntroScreen.cs | 3 ++- osu.Game/Screens/Menu/IntroTriangles.cs | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs index 29be250b12..5f135febf4 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneIntroWelcome.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Screens; +using osu.Framework.Utils; using osu.Game.Screens.Menu; namespace osu.Game.Tests.Visual.Menus @@ -15,7 +16,7 @@ namespace osu.Game.Tests.Visual.Menus public TestSceneIntroWelcome() { AddUntilStep("wait for load", () => MusicController.TrackLoaded); - + AddAssert("correct track", () => Precision.AlmostEquals(MusicController.CurrentTrack.Length, 48000, 1)); AddAssert("check if menu music loops", () => MusicController.CurrentTrack.Looping); } } diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 3e4320ae44..363933694d 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -66,6 +66,7 @@ namespace osu.Game.Screens.Menu /// /// Whether the is provided by osu! resources, rather than a user beatmap. + /// Only valid during or after . /// protected bool UsingThemedIntro { get; private set; } @@ -115,7 +116,6 @@ namespace osu.Game.Screens.Menu if (setInfo != null) { initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - UsingThemedIntro = !initialBeatmap.LoadTrack().IsDummyDevice; } return UsingThemedIntro; @@ -169,6 +169,7 @@ namespace osu.Game.Screens.Menu { beatmap.Value = initialBeatmap; Track = musicController.CurrentTrack; + UsingThemedIntro = !initialBeatmap.LoadTrack().IsDummyDevice; 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 a9ef20436f..52281967ee 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Menu [BackgroundDependencyLoader] private void load() { - if (MenuVoice.Value && !UsingThemedIntro) + if (MenuVoice.Value) welcome = audio.Samples.Get(@"Intro/welcome"); } From 3b03116179c3e0ec88b815dd2bcfc32961058510 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 16:45:59 +0900 Subject: [PATCH 2777/6909] Remove unnecessary using statement --- osu.Game/Screens/Play/Player.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a8fda10604..9be4fd6a65 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -8,7 +8,6 @@ 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.Graphics; using osu.Framework.Graphics.Containers; From 70697cf1a0e36e57b3533613a5795bdb8722a746 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 16:58:45 +0900 Subject: [PATCH 2778/6909] Restore remaining editor components to use Beatmap.Track --- osu.Game.Tests/Visual/Editing/TimelineTestScene.cs | 11 ++++++----- .../Screens/Edit/Components/BottomBarContainer.cs | 2 ++ osu.Game/Screens/Edit/Components/PlaybackControl.cs | 8 ++------ .../Timelines/Summary/Parts/TimelinePart.cs | 8 ++------ .../Edit/Compose/Components/Timeline/Timeline.cs | 6 +++--- .../Components/Timeline/TimelineTickDisplay.cs | 7 ++++--- osu.Game/Screens/Edit/Editor.cs | 10 ++++------ osu.Game/Screens/Edit/EditorClock.cs | 10 +++++----- osu.Game/Tests/Visual/EditorClockTestScene.cs | 3 ++- 9 files changed, 30 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 4988a09650..fdb8781563 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -3,11 +3,12 @@ using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; @@ -64,10 +65,10 @@ namespace osu.Game.Tests.Visual.Editing private readonly Drawable marker; [Resolved] - private EditorClock editorClock { get; set; } + private IBindable beatmap { get; set; } [Resolved] - private MusicController musicController { get; set; } + private EditorClock editorClock { get; set; } public AudioVisualiser() { @@ -93,8 +94,8 @@ namespace osu.Game.Tests.Visual.Editing { base.Update(); - if (musicController.TrackLoaded) - marker.X = (float)(editorClock.CurrentTime / musicController.CurrentTrack.Length); + if (beatmap.Value.Track.IsLoaded) + marker.X = (float)(editorClock.CurrentTime / beatmap.Value.Track.Length); } } diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs index 8d8c59b2ee..cb5078a479 100644 --- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs +++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs @@ -2,6 +2,7 @@ // 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; using osu.Framework.Graphics.Containers; @@ -17,6 +18,7 @@ namespace osu.Game.Screens.Edit.Components private const float contents_padding = 15; protected readonly IBindable Beatmap = new Bindable(); + protected Track Track => Beatmap.Value.Track; private readonly Drawable background; private readonly Container content; diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 5bafc120af..59b3d1c565 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -16,7 +16,6 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays; using osuTK.Input; namespace osu.Game.Screens.Edit.Components @@ -28,9 +27,6 @@ namespace osu.Game.Screens.Edit.Components [Resolved] private EditorClock editorClock { get; set; } - [Resolved] - private MusicController musicController { get; set; } - private readonly BindableNumber tempo = new BindableDouble(1); [BackgroundDependencyLoader] @@ -66,12 +62,12 @@ namespace osu.Game.Screens.Edit.Components } }; - musicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, tempo); + Track?.AddAdjustment(AdjustableProperty.Tempo, tempo); } protected override void Dispose(bool isDisposing) { - musicController?.CurrentTrack.RemoveAdjustment(AdjustableProperty.Tempo, tempo); + Track?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); base.Dispose(isDisposing); } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index c8a470c58a..4a7c3f26bc 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -8,7 +8,6 @@ using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; -using osu.Game.Overlays; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { @@ -27,9 +26,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts protected override Container Content => content; - [Resolved] - private MusicController musicController { get; set; } - public TimelinePart(Container content = null) { AddInternal(this.content = content ?? new Container { RelativeSizeAxes = Axes.Both }); @@ -50,14 +46,14 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts private void updateRelativeChildSize() { // the track may not be loaded completely (only has a length once it is). - if (!musicController.TrackLoaded) + if (!Beatmap.Value.Track.IsLoaded) { content.RelativeChildSize = Vector2.One; Schedule(updateRelativeChildSize); return; } - content.RelativeChildSize = new Vector2((float)Math.Max(1, musicController.CurrentTrack.Length), 1); + content.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); } protected virtual void LoadBeatmap(WorkingBeatmap beatmap) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 96c48c0ddc..c6bfdda698 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 /// private bool trackWasPlaying; - private ITrack track; + private Track track; public Timeline() { @@ -134,7 +134,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void seekTrackToCurrent() { - if (!musicController.TrackLoaded) + if (!track.IsLoaded) return; editorClock.Seek(Current / Content.DrawWidth * track.Length); @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void scrollToTrackTime() { - if (!musicController.TrackLoaded || track.Length == 0) + if (!track.IsLoaded || track.Length == 0) return; ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index cb122c590e..36ee976bf7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -3,9 +3,10 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Overlays; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; @@ -17,7 +18,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private EditorBeatmap beatmap { get; set; } [Resolved] - private MusicController musicController { get; set; } + private Bindable working { get; set; } [Resolved] private BindableBeatDivisor beatDivisor { get; set; } @@ -43,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline for (var 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 : musicController.CurrentTrack.Length; + var until = i + 1 < beatmap.ControlPointInfo.TimingPoints.Count ? beatmap.ControlPointInfo.TimingPoints[i + 1].Time : working.Value.Track.Length; int beat = 0; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 6722d9179c..d92f3922c3 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -14,6 +14,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Framework.Platform; +using osu.Framework.Timing; using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Edit.Components; using osu.Game.Screens.Edit.Components.Menus; @@ -27,7 +28,6 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; -using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Setup; @@ -53,9 +53,6 @@ namespace osu.Game.Screens.Edit [Resolved] private BeatmapManager beatmapManager { get; set; } - [Resolved] - private MusicController musicController { get; set; } - private Box bottomBackground; private Container screenContainer; @@ -82,8 +79,9 @@ namespace osu.Game.Screens.Edit beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); // Todo: should probably be done at a DrawableRuleset level to share logic with Player. + var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; - clock.ChangeSource(musicController.CurrentTrack); + clock.ChangeSource(sourceClock); dependencies.CacheAs(clock); AddInternal(clock); @@ -348,7 +346,7 @@ namespace osu.Game.Screens.Edit private void resetTrack(bool seekToStart = false) { - musicController.CurrentTrack.Stop(); + Beatmap.Value.Track?.Stop(); if (seekToStart) { diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 634a6f7e25..d4d0feb813 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -30,11 +30,6 @@ namespace osu.Game.Screens.Edit { } - public EditorClock() - : this(new ControlPointInfo(), 1000, new BindableBeatDivisor()) - { - } - public EditorClock(ControlPointInfo controlPointInfo, double trackLength, BindableBeatDivisor beatDivisor) { this.beatDivisor = beatDivisor; @@ -45,6 +40,11 @@ namespace osu.Game.Screens.Edit underlyingClock = new DecoupleableInterpolatingFramedClock(); } + public EditorClock() + : this(new ControlPointInfo(), 1000, new BindableBeatDivisor()) + { + } + /// /// Seek to the closest snappable beat from a time. /// diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index 59c9329d37..f0ec638fc9 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Input.Events; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Screens.Edit; @@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual private void beatmapChanged(ValueChangedEvent e) { Clock.ControlPointInfo = e.NewValue.Beatmap.ControlPointInfo; - Clock.ChangeSource(MusicController.CurrentTrack); + Clock.ChangeSource((IAdjustableClock)e.NewValue.Track ?? new StopwatchClock()); Clock.ProcessFrame(); } From a47b8222b53a00daab14556988952824e59ec1ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 16:59:11 +0900 Subject: [PATCH 2779/6909] Restore multiplayer to use beatmap track --- osu.Game/Screens/Multi/Multiplayer.cs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 1a39d80f8d..4912df17b1 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Multi private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); [Resolved] - private MusicController musicController { get; set; } + private MusicController music { get; set; } [Cached(Type = typeof(IRoomManager))] private RoomManager roomManager; @@ -343,10 +343,15 @@ namespace osu.Game.Screens.Multi { if (screenStack.CurrentScreen is MatchSubScreen) { - musicController.CurrentTrack.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - musicController.CurrentTrack.Looping = true; + var track = Beatmap.Value?.Track; - musicController.EnsurePlayingSomething(); + if (track != null) + { + track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; + track.Looping = true; + + music.EnsurePlayingSomething(); + } } else { @@ -356,8 +361,13 @@ namespace osu.Game.Screens.Multi private void cancelLooping() { - musicController.CurrentTrack.Looping = false; - musicController.CurrentTrack.RestartPoint = 0; + var track = Beatmap?.Value?.Track; + + if (track != null) + { + track.Looping = false; + track.RestartPoint = 0; + } } protected override void Dispose(bool isDisposing) From d5cbb589c2ff6fa2a8cfac96ef881d3406149ca9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 17:21:08 +0900 Subject: [PATCH 2780/6909] Revert some test scene changes to use Beatmap.Track where relevant --- .../Gameplay/TestSceneCompletionCancellation.cs | 6 +++--- .../Visual/Gameplay/TestSceneGameplayRewinding.cs | 12 ++++-------- .../Gameplay/TestSceneNightcoreBeatContainer.cs | 4 ++-- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 2 +- .../Visual/Gameplay/TestScenePlayerLoader.cs | 8 ++++---- .../Visual/Gameplay/TestSceneStoryboard.cs | 14 ++++++++++---- 6 files changed, 24 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index b39cfc3699..6fd5511e5a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay base.SetUpSteps(); // Ensure track has actually running before attempting to seek - AddUntilStep("wait for track to start running", () => MusicController.IsPlaying); + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); } [Test] @@ -70,13 +70,13 @@ namespace osu.Game.Tests.Visual.Gameplay private void complete() { - AddStep("seek to completion", () => MusicController.SeekTo(5000)); + AddStep("seek to completion", () => Beatmap.Value.Track.Seek(5000)); AddUntilStep("completion set by processor", () => Player.ScoreProcessor.HasCompleted.Value); } private void cancel() { - AddStep("rewind to cancel", () => MusicController.SeekTo(4000)); + AddStep("rewind to cancel", () => Beatmap.Value.Track.Seek(4000)); AddUntilStep("completion cleared by processor", () => !Player.ScoreProcessor.HasCompleted.Value); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs index 6bdc65078a..73c6970482 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayRewinding.cs @@ -8,7 +8,6 @@ using osu.Framework.Audio; using osu.Framework.Utils; using osu.Framework.Timing; using osu.Game.Beatmaps; -using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Storyboards; @@ -21,16 +20,13 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private AudioManager audioManager { get; set; } - [Resolved] - private MusicController musicController { get; set; } - - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - => new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => + new ClockBackedTestWorkingBeatmap(beatmap, storyboard, new FramedClock(new ManualClock { Rate = 1 }), audioManager); [Test] public void TestNoJudgementsOnRewind() { - AddUntilStep("wait for track to start running", () => MusicController.IsPlaying); + AddUntilStep("wait for track to start running", () => Beatmap.Value.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)); @@ -43,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void addSeekStep(double time) { - AddStep($"seek to {time}", () => MusicController.SeekTo(time)); + AddStep($"seek to {time}", () => Beatmap.Value.Track.Seek(time)); // Allow a few frames of lenience AddUntilStep("wait for seek to finish", () => Precision.AlmostEquals(time, Player.DrawableRuleset.FrameStableClock.CurrentTime, 100)); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs index 53e06a632e..951ee1489d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneNightcoreBeatContainer.cs @@ -19,8 +19,8 @@ namespace osu.Game.Tests.Visual.Gameplay Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - MusicController.CurrentTrack.Start(); - MusicController.CurrentTrack.Seek(Beatmap.Value.Beatmap.HitObjects.First().StartTime - 1000); + Beatmap.Value.Track.Start(); + Beatmap.Value.Track.Seek(Beatmap.Value.Beatmap.HitObjects.First().StartTime - 1000); Add(new ModNightcore.NightcoreBeatContainer()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index e7dd586f4e..420bf29429 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -288,7 +288,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void confirmNoTrackAdjustments() { - AddAssert("track has no adjustments", () => MusicController.CurrentTrack.AggregateFrequency.Value == 1); + AddAssert("track has no adjustments", () => Beatmap.Value.Track.AggregateFrequency.Value == 1); } private void restart() => AddStep("restart", () => Player.Restart()); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index c3be6aee2b..4fac7bb45f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Gameplay Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); foreach (var mod in SelectedMods.Value.OfType()) - mod.ApplyToTrack(MusicController.CurrentTrack); + mod.ApplyToTrack(Beatmap.Value.Track); InputManager.Child = container; } @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); AddAssert("player did not load", () => player == null); AddUntilStep("player disposed", () => loader.DisposalTask == null); - AddAssert("mod rate still applied", () => MusicController.CurrentTrack.Rate != 1); + AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1); } /// @@ -88,13 +88,13 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddAssert("mod rate applied", () => MusicController.CurrentTrack.Rate != 1); + AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); AddUntilStep("wait for non-null player", () => player != null); AddStep("exit loader", () => loader.Exit()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); AddAssert("player did not load", () => !player.IsLoaded); AddUntilStep("player disposed", () => loader.DisposalTask?.IsCompleted == true); - AddAssert("mod rate still applied", () => MusicController.CurrentTrack.Rate != 1); + AddAssert("mod rate still applied", () => Beatmap.Value.Track.Rate != 1); } [Test] diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index c11a47d62b..9f1492a25f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -25,12 +25,16 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly Container storyboardContainer; private DrawableStoryboard storyboard; + [Cached] + private MusicController musicController = new MusicController(); + public TestSceneStoryboard() { Clock = new FramedClock(); AddRange(new Drawable[] { + musicController, new Container { RelativeSizeAxes = Axes.Both, @@ -83,9 +87,11 @@ namespace osu.Game.Tests.Visual.Gameplay private void restart() { - MusicController.CurrentTrack.Reset(); + var track = Beatmap.Value.Track; + + track.Reset(); loadStoryboard(Beatmap.Value); - MusicController.CurrentTrack.Start(); + track.Start(); } private void loadStoryboard(WorkingBeatmap working) @@ -100,7 +106,7 @@ namespace osu.Game.Tests.Visual.Gameplay storyboard.Passing = false; storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(MusicController.CurrentTrack); + decoupledClock.ChangeSource(working.Track); } private void loadStoryboardNoVideo() @@ -123,7 +129,7 @@ namespace osu.Game.Tests.Visual.Gameplay storyboard = sb.CreateDrawable(Beatmap.Value); storyboardContainer.Add(storyboard); - decoupledClock.ChangeSource(MusicController.CurrentTrack); + decoupledClock.ChangeSource(Beatmap.Value.Track); } } } From aead13628bbaa284b4dc1719c7c83e173f9e8565 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 21 Aug 2020 17:52:42 +0900 Subject: [PATCH 2781/6909] Rework freezing to use masking --- .../Objects/Drawables/DrawableHoldNote.cs | 112 +++++++++--------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 229ce355d7..e959509b96 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -35,19 +34,14 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly Container tickContainer; /// - /// Contains the maximum size/position of the body prior to any offset or size adjustments. + /// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed. /// - private readonly Container bodyContainer; + private readonly Container sizingContainer; /// - /// Contains the offset size/position of the body such that the body extends half-way between the head and tail pieces. + /// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of . /// - private readonly Container bodyOffsetContainer; - - /// - /// Contains the masking area for the tail, which is resized along with . - /// - private readonly Container tailMaskingContainer; + private readonly Container maskingContainer; private readonly SkinnableDrawable bodyPiece; @@ -71,36 +65,43 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { RelativeSizeAxes = Axes.X; - AddRangeInternal(new[] + Container maskedContents; + + AddRangeInternal(new Drawable[] { - bodyContainer = new Container + sizingContainer = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - bodyOffsetContainer = new Container + maskingContainer = new Container { - RelativeSizeAxes = Axes.X, - Child = bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece + RelativeSizeAxes = Axes.Both, + Child = maskedContents = new Container { - RelativeSizeAxes = Axes.Both - }) + RelativeSizeAxes = Axes.Both, + Masking = true, + } }, - // The head needs to move along with changes in the size of the body. headContainer = new Container { RelativeSizeAxes = Axes.Both } } }, - tickContainer = new Container { RelativeSizeAxes = Axes.Both }, - tailMaskingContainer = new Container + bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece { - RelativeSizeAxes = Axes.X, - Masking = true, - Child = tailContainer = new Container - { - RelativeSizeAxes = Axes.X, - } + RelativeSizeAxes = Axes.Both, + }) + { + RelativeSizeAxes = Axes.X }, - headContainer.CreateProxy() + tickContainer = new Container { RelativeSizeAxes = Axes.Both }, + tailContainer = new Container { RelativeSizeAxes = Axes.Both }, + }); + + maskedContents.AddRange(new[] + { + bodyPiece.CreateProxy(), + tickContainer.CreateProxy(), + tailContainer.CreateProxy(), }); } @@ -167,24 +168,15 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.OnDirectionChanged(e); - // The body container is anchored from the position of the tail, since its height is changed when the hold note is being hit. - // The body offset container is anchored from the position of the head (inverse of the above). - // The tail containers are both anchored from the position of the tail. if (e.NewValue == ScrollingDirection.Up) { - bodyContainer.Anchor = bodyContainer.Origin = Anchor.BottomLeft; - bodyOffsetContainer.Anchor = bodyOffsetContainer.Origin = Anchor.TopLeft; - - tailMaskingContainer.Anchor = tailMaskingContainer.Origin = Anchor.BottomLeft; - tailContainer.Anchor = tailContainer.Origin = Anchor.BottomLeft; + bodyPiece.Anchor = bodyPiece.Origin = Anchor.TopLeft; + sizingContainer.Anchor = sizingContainer.Origin = Anchor.BottomLeft; } else { - bodyContainer.Anchor = bodyContainer.Origin = Anchor.TopLeft; - bodyOffsetContainer.Anchor = bodyOffsetContainer.Origin = Anchor.BottomLeft; - - tailMaskingContainer.Anchor = tailMaskingContainer.Origin = Anchor.TopLeft; - tailContainer.Anchor = tailContainer.Origin = Anchor.TopLeft; + bodyPiece.Anchor = bodyPiece.Origin = Anchor.BottomLeft; + sizingContainer.Anchor = sizingContainer.Origin = Anchor.TopLeft; } } @@ -206,26 +198,34 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (Time.Current < releaseTime) releaseTime = null; - // Decrease the size of the body while the hold note is held and the head has been hit. This stops at the very first release point. + // Pad the full size container so its contents (i.e. the masking container) reach under the tail. + // This is required for the tail to not be masked away, since it lies outside the bounds of the hold note. + sizingContainer.Padding = new MarginPadding + { + Top = Direction.Value == ScrollingDirection.Down ? -Tail.Height : 0, + Bottom = Direction.Value == ScrollingDirection.Up ? -Tail.Height : 0, + }; + + // Pad the masking container to the starting position of the body piece (half-way under the head). + // This is required ot make the body start getting masked immediately as soon as the note is held. + maskingContainer.Padding = new MarginPadding + { + Top = Direction.Value == ScrollingDirection.Up ? Head.Height / 2 : 0, + Bottom = Direction.Value == ScrollingDirection.Down ? Head.Height / 2 : 0, + }; + + // Position and resize the body to lie half-way under the head and the tail notes. + bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; + bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; + + // As the note is being held, adjust the size of the fullSizeContainer. This has two effects: + // 1. The contained masking container will mask the body and ticks. + // 2. The head note will move along with the new "head position" in the container. if (Head.IsHit && releaseTime == null) { float heightDecrease = (float)(Math.Max(0, Time.Current - HitObject.StartTime) / HitObject.Duration); - bodyContainer.Height = MathHelper.Clamp(1 - heightDecrease, 0, 1); + sizingContainer.Height = Math.Clamp(1 - heightDecrease, 0, 1); } - - // Re-position the body half-way up the head, and extend the height until it's half-way under the tail. - bodyOffsetContainer.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; - bodyOffsetContainer.Height = bodyContainer.DrawHeight + Tail.Height / 2 - Head.Height / 2; - - // The tail is positioned to be "outside" the hold note, so re-position its masking container to fully cover the tail and extend the height until it's half-way under the head. - // The masking height is determined by the size of the body so that the head and tail don't overlap as the body becomes shorter via hitting (above). - tailMaskingContainer.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Tail.Height; - tailMaskingContainer.Height = bodyContainer.DrawHeight + Tail.Height - Head.Height / 2; - - // The tail container needs the reverse of the above offset applied to bring the tail to its original position. - // It also needs the full original height of the hold note to maintain positioning even as the height of the masking container changes. - tailContainer.Y = -tailMaskingContainer.Y; - tailContainer.Height = DrawHeight; } protected override void UpdateStateTransforms(ArmedState state) From 69cb9f309123ec3719064ad3bfc9b155ddb4b796 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 18:19:47 +0900 Subject: [PATCH 2782/6909] Fix potential crash if disposing a DrawableStoryboardSample twice --- osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 8eaf9ac652..119c48836b 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -76,6 +76,8 @@ namespace osu.Game.Storyboards.Drawables protected override void Dispose(bool isDisposing) { Channel?.Stop(); + Channel = null; + base.Dispose(isDisposing); } } From 1edafc39bac27e54ebb32dfb44124382ec3b55b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 18:33:24 +0900 Subject: [PATCH 2783/6909] Fix intro welcome playing double due to missing conditional --- 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 52281967ee..a96fddb5ad 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -64,7 +64,8 @@ namespace osu.Game.Screens.Menu }, t => { AddInternal(t); - welcome?.Play(); + if (!UsingThemedIntro) + welcome?.Play(); StartTrack(); }); From 308d9f59679033e0585cd96b4d5e550c9d2b31d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 18:43:58 +0900 Subject: [PATCH 2784/6909] Ensure locally executed methods are always loaded before propagation --- osu.Game/Overlays/MusicController.cs | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index c1116ff651..112026d9e2 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -231,9 +231,7 @@ namespace osu.Game.Overlays if (playable != null) { - if (beatmap is Bindable working) - working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); - + changeBeatmap(beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value)); restartTrack(); return PreviousTrackResult.Previous; } @@ -257,9 +255,7 @@ namespace osu.Game.Overlays if (playable != null) { - if (beatmap is Bindable working) - working.Value = beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value); - + changeBeatmap(beatmaps.GetWorkingBeatmap(playable.Beatmaps.First(), beatmap.Value)); restartTrack(); return true; } @@ -278,11 +274,15 @@ namespace osu.Game.Overlays private TrackChangeDirection? queuedDirection; - private void beatmapChanged(ValueChangedEvent beatmap) + private void beatmapChanged(ValueChangedEvent beatmap) => changeBeatmap(beatmap.NewValue); + + private void changeBeatmap(WorkingBeatmap newWorking) { + var lastWorking = current; + TrackChangeDirection direction = TrackChangeDirection.None; - bool audioEquals = beatmap.NewValue?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) ?? false; + bool audioEquals = newWorking?.BeatmapInfo?.AudioEquals(current?.BeatmapInfo) ?? false; if (current != null) { @@ -297,13 +297,13 @@ namespace osu.Game.Overlays { // 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(); + var next = newWorking == null ? -1 : BeatmapSets.TakeWhile(b => b.ID != newWorking.BeatmapSetInfo?.ID).Count(); direction = last > next ? TrackChangeDirection.Prev : TrackChangeDirection.Next; } } - current = beatmap.NewValue; + current = newWorking; if (!audioEquals || CurrentTrack.IsDummyDevice) { @@ -312,7 +312,7 @@ namespace osu.Game.Overlays else { // transfer still valid track to new working beatmap - current.TransferTrack(beatmap.OldValue.Track); + current.TransferTrack(lastWorking.Track); } TrackChanged?.Invoke(current, direction); @@ -320,6 +320,11 @@ namespace osu.Game.Overlays ResetTrackAdjustments(); queuedDirection = null; + + // this will be a noop if coming from the beatmapChanged event. + // the exception is local operations like next/prev, where we want to complete loading the track before sending out a change. + if (beatmap.Value != current && beatmap is Bindable working) + working.Value = current; } private void changeTrack() From 4239e9f684e7e1594fc652be1640a940d9092cd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 18:44:14 +0900 Subject: [PATCH 2785/6909] Fix storyboard test not actually working due to incorrect track referencing --- .../Visual/Gameplay/TestSceneStoryboard.cs | 50 ++++++++----------- 1 file changed, 21 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs index 9f1492a25f..5a2b8d22fd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboard.cs @@ -22,19 +22,32 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneStoryboard : OsuTestScene { - private readonly Container storyboardContainer; + private Container storyboardContainer; private DrawableStoryboard storyboard; - [Cached] - private MusicController musicController = new MusicController(); + [Test] + public void TestStoryboard() + { + AddStep("Restart", restart); + AddToggleStep("Passing", passing => + { + if (storyboard != null) storyboard.Passing = passing; + }); + } - public TestSceneStoryboard() + [Test] + public void TestStoryboardMissingVideo() + { + AddStep("Load storyboard with missing video", loadStoryboardNoVideo); + } + + [BackgroundDependencyLoader] + private void load() { Clock = new FramedClock(); AddRange(new Drawable[] { - musicController, new Container { RelativeSizeAxes = Axes.Both, @@ -58,32 +71,11 @@ namespace osu.Game.Tests.Visual.Gameplay State = { Value = Visibility.Visible }, } }); + + Beatmap.BindValueChanged(beatmapChanged, true); } - [Test] - public void TestStoryboard() - { - AddStep("Restart", restart); - AddToggleStep("Passing", passing => - { - if (storyboard != null) storyboard.Passing = passing; - }); - } - - [Test] - public void TestStoryboardMissingVideo() - { - AddStep("Load storyboard with missing video", loadStoryboardNoVideo); - } - - [BackgroundDependencyLoader] - private void load() - { - Beatmap.ValueChanged += beatmapChanged; - } - - private void beatmapChanged(ValueChangedEvent e) - => loadStoryboard(e.NewValue); + private void beatmapChanged(ValueChangedEvent e) => loadStoryboard(e.NewValue); private void restart() { From f63d1ba612df01640d3abe79a1db5217a947f54a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 21 Aug 2020 18:52:53 +0900 Subject: [PATCH 2786/6909] Remove stray call to LoadTrack that was forgotten --- osu.Game/Screens/Menu/IntroScreen.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 363933694d..884cbfe107 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -12,7 +12,6 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.IO.Archives; -using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Skinning; using osuTK; @@ -61,9 +60,6 @@ namespace osu.Game.Screens.Menu [Resolved] private AudioManager audio { get; set; } - [Resolved] - private MusicController musicController { get; set; } - /// /// Whether the is provided by osu! resources, rather than a user beatmap. /// Only valid during or after . @@ -168,8 +164,8 @@ namespace osu.Game.Screens.Menu if (!resuming) { beatmap.Value = initialBeatmap; - Track = musicController.CurrentTrack; - UsingThemedIntro = !initialBeatmap.LoadTrack().IsDummyDevice; + Track = initialBeatmap.Track; + UsingThemedIntro = !initialBeatmap.Track.IsDummyDevice; logo.MoveTo(new Vector2(0.5f)); logo.ScaleTo(Vector2.One); From 42ee9b75df7a411fb2354ab2a47feafd288b815f Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 21 Aug 2020 19:38:59 +0900 Subject: [PATCH 2787/6909] 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 --- .../Objects/Drawables/DrawableHoldNote.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index e959509b96..40c5764a97 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -207,7 +207,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables }; // Pad the masking container to the starting position of the body piece (half-way under the head). - // This is required ot make the body start getting masked immediately as soon as the note is held. + // This is required to make the body start getting masked immediately as soon as the note is held. maskingContainer.Padding = new MarginPadding { Top = Direction.Value == ScrollingDirection.Up ? Head.Height / 2 : 0, @@ -218,13 +218,13 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; - // As the note is being held, adjust the size of the fullSizeContainer. This has two effects: + // As the note is being held, adjust the size of the sizing container. This has two effects: // 1. The contained masking container will mask the body and ticks. // 2. The head note will move along with the new "head position" in the container. if (Head.IsHit && releaseTime == null) { - float heightDecrease = (float)(Math.Max(0, Time.Current - HitObject.StartTime) / HitObject.Duration); - sizingContainer.Height = Math.Clamp(1 - heightDecrease, 0, 1); + float remainingHeight = (float)(Math.Max(0, HitObject.GetEndTime() - Time.Current) / HitObject.Duration); + sizingContainer.Height = Math.Clamp(remainingHeight, 0, 1); } } From 8632c3adf0c1bc945aa26d14b7206db5ed37a69c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 21 Aug 2020 23:11:15 +0900 Subject: [PATCH 2788/6909] Fix hold notes bouncing with SV changes --- .../Objects/Drawables/DrawableHoldNote.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 40c5764a97..0712026ca6 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -223,8 +223,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // 2. The head note will move along with the new "head position" in the container. if (Head.IsHit && releaseTime == null) { - float remainingHeight = (float)(Math.Max(0, HitObject.GetEndTime() - Time.Current) / HitObject.Duration); - sizingContainer.Height = Math.Clamp(remainingHeight, 0, 1); + // How far past the hit target this hold note is. Always a positive value. + float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y); + sizingContainer.Height = Math.Clamp(1 - yOffset / DrawHeight, 0, 1); } } From b3338347b7fd99375bc0926c4c29beda38f1171c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 21 Aug 2020 23:56:27 +0900 Subject: [PATCH 2789/6909] Remove fade on successful hits --- .../Objects/Drawables/DrawableManiaHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index a44d8b09aa..ab76a5b8f8 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables break; case ArmedState.Hit: - this.FadeOut(150, Easing.OutQuint); + this.FadeOut(); break; } } From 88d50b6c4751e7fc87580b8e5bed753052017fa3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 22 Aug 2020 00:15:37 +0900 Subject: [PATCH 2790/6909] Remove alpha mangling from LegacyDecoder --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 44ef9bcacc..c15240a4f6 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -104,10 +104,6 @@ namespace osu.Game.Beatmaps.Formats try { 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 From 2424fa08027ebe105e1102997e8379ec31528c0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 22 Aug 2020 00:15:58 +0900 Subject: [PATCH 2791/6909] Add helper methods --- osu.Game/Skinning/LegacySkinExtensions.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index bb46dc8b9f..088eae4bce 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osuTK.Graphics; using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Skinning @@ -62,6 +63,21 @@ namespace osu.Game.Skinning } } + public static Color4 ToLegacyColour(this Color4 colour) + { + if (colour.A == 0) + colour.A = 1; + return colour; + } + + public static T WithInitialColour(this T drawable, Color4 colour) + where T : Drawable + { + drawable.Alpha = colour.A; + drawable.Colour = ToLegacyColour(colour); + return drawable; + } + public class SkinnableTextureAnimation : TextureAnimation { [Resolved(canBeNull: true)] From eaba32335327dc0a4f987f8b8f35bb89a01d51cb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 22 Aug 2020 00:17:35 +0900 Subject: [PATCH 2792/6909] Update catch with legacy colour setters --- .../Skinning/CatchLegacySkinTransformer.cs | 8 +++++++- osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs | 3 +-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index d929da1a29..5abd87d6f4 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning { @@ -61,7 +62,12 @@ namespace osu.Game.Rulesets.Catch.Skinning switch (lookup) { case CatchSkinColour colour: - return Source.GetConfig(new SkinCustomColourLookup(colour)); + var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour)); + if (result == null) + return null; + + result.Value = result.Value.ToLegacyColour(); + return (IBindable)result; } return Source.GetConfig(lookup); diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 5be54d3882..c9dd1d1f3e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Skinning colouredSprite = new Sprite { Texture = skin.GetTexture(lookupName), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Skinning { base.LoadComplete(); - accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue, true); + accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue.ToLegacyColour(), true); } } } From 454564b18928a17525e12596ca38446844cb0600 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 22 Aug 2020 00:19:15 +0900 Subject: [PATCH 2793/6909] Update mania with legacy colour setters --- .../Skinning/LegacyColumnBackground.cs | 20 ++++++++----------- .../Skinning/LegacyHitTarget.cs | 2 +- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 64a7641421..b97547bbc6 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -58,28 +58,24 @@ namespace osu.Game.Rulesets.Mania.Skinning InternalChildren = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - new Box + new Box { RelativeSizeAxes = Axes.Both }.WithInitialColour(backgroundColour), + new Container { RelativeSizeAxes = Axes.Y, Width = leftLineWidth, Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasLeftLine ? 1 : 0 + Alpha = hasLeftLine ? 1 : 0, + Child = new Box { RelativeSizeAxes = Axes.Both }.WithInitialColour(lineColour) }, - new Box + new Container { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = rightLineWidth, Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasRightLine ? 1 : 0 + Alpha = hasRightLine ? 1 : 0, + Child = new Box { RelativeSizeAxes = Axes.Both }.WithInitialColour(lineColour) }, lightContainer = new Container { @@ -90,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Colour = lightColour, + Colour = lightColour.ToLegacyColour(), Texture = skin.GetTexture(lightImage), RelativeSizeAxes = Axes.X, Width = 1, diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index d055ef3480..2177eaa5e6 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Anchor = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, Height = 1, - Colour = lineColour, + Colour = lineColour.ToLegacyColour(), Alpha = showJudgementLine ? 0.9f : 0 } } From 16a2ab9dea4fd58e56eb00c017e61fce002b7ed2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 22 Aug 2020 00:20:33 +0900 Subject: [PATCH 2794/6909] Update osu with legacy colour setters --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 3 +-- osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 0ab3e8825b..8a6beddb51 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -59,7 +59,6 @@ namespace osu.Game.Rulesets.Osu.Skinning hitCircleSprite = new Sprite { Texture = getTextureWithFallback(string.Empty), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -107,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Skinning base.LoadComplete(); state.BindValueChanged(updateState, true); - accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); + accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue.ToLegacyColour(), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index 0f586034d5..3b75fcc8a0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableObject) { - animationContent.Colour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; + var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; InternalChildren = new[] { @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Texture = skin.GetTexture("sliderb-nd"), Colour = new Color4(5, 5, 5, 255), }, - animationContent.With(d => + animationContent.WithInitialColour(ballColour).With(d => { d.Anchor = Anchor.Centre; d.Origin = Anchor.Centre; From 9fbc5f3aeb546fc27572a4c511793d6dd91088ea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 22 Aug 2020 00:23:08 +0900 Subject: [PATCH 2795/6909] Update taiko with legacy colour setters --- osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs | 2 +- osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs | 6 +++--- osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs index bfcf268c3d..ed69b529ed 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - backgroundLayer.Colour = accentColour; + backgroundLayer.Colour = accentColour.ToLegacyColour(); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs index 8223e3bc01..6bb8f9433e 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs @@ -76,9 +76,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - headCircle.AccentColour = accentColour; - body.Colour = accentColour; - end.Colour = accentColour; + headCircle.AccentColour = accentColour.ToLegacyColour(); + body.Colour = accentColour.ToLegacyColour(); + end.Colour = accentColour.ToLegacyColour(); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index 656728f6e4..f36aae205a 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning @@ -18,9 +19,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning [BackgroundDependencyLoader] private void load() { - AccentColour = component == TaikoSkinComponents.CentreHit + AccentColour = (component == TaikoSkinComponents.CentreHit ? new Color4(235, 69, 44, 255) - : new Color4(67, 142, 172, 255); + : new Color4(67, 142, 172, 255)).ToLegacyColour(); } } } From f89b6f44653112a86cc5df93be0d6e11ad0ad8d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 22 Aug 2020 00:52:53 +0900 Subject: [PATCH 2796/6909] Add xmldocs --- osu.Game/Skinning/LegacySkinExtensions.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 088eae4bce..ee7d74d7ec 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -63,6 +63,11 @@ namespace osu.Game.Skinning } } + /// + /// The resultant colour after setting a post-constructor colour in osu!stable. + /// + /// The to convert. + /// The converted . public static Color4 ToLegacyColour(this Color4 colour) { if (colour.A == 0) @@ -70,6 +75,16 @@ namespace osu.Game.Skinning return colour; } + /// + /// Equivalent of setting a colour in the constructor in osu!stable. + /// Doubles the alpha channel into and uses to set . + /// + /// + /// Beware: Any existing value in is overwritten. + /// + /// The to set the "InitialColour" of. + /// The to set. + /// The given . public static T WithInitialColour(this T drawable, Color4 colour) where T : Drawable { From 356c67f00d778e1f6d2535eb534a864d6b04caa4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 22 Aug 2020 00:55:03 +0900 Subject: [PATCH 2797/6909] Remove outdated/wrong test --- osu.Game.Tests/Resources/skin-zero-alpha-colour.ini | 5 ----- osu.Game.Tests/Skins/LegacySkinDecoderTest.cs | 10 ---------- 2 files changed, 15 deletions(-) delete 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 deleted file mode 100644 index 3c0dae6b13..0000000000 --- a/osu.Game.Tests/Resources/skin-zero-alpha-colour.ini +++ /dev/null @@ -1,5 +0,0 @@ -[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 c408d2f182..aedf26ee75 100644 --- a/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs +++ b/osu.Game.Tests/Skins/LegacySkinDecoderTest.cs @@ -108,15 +108,5 @@ 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 08078b9513895806f9df2a08922fc7c4bf826fd9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 22 Aug 2020 00:56:29 +0900 Subject: [PATCH 2798/6909] Rename method to remove "InitialColour" namings --- osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs | 6 +++--- osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs | 2 +- osu.Game/Skinning/LegacySkinExtensions.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index b97547bbc6..54a16b840f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -58,14 +58,14 @@ namespace osu.Game.Rulesets.Mania.Skinning InternalChildren = new Drawable[] { - new Box { RelativeSizeAxes = Axes.Both }.WithInitialColour(backgroundColour), + new Box { RelativeSizeAxes = Axes.Both }.WithLegacyColour(backgroundColour), new Container { RelativeSizeAxes = Axes.Y, Width = leftLineWidth, Scale = new Vector2(0.740f, 1), Alpha = hasLeftLine ? 1 : 0, - Child = new Box { RelativeSizeAxes = Axes.Both }.WithInitialColour(lineColour) + Child = new Box { RelativeSizeAxes = Axes.Both }.WithLegacyColour(lineColour) }, new Container { @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Width = rightLineWidth, Scale = new Vector2(0.740f, 1), Alpha = hasRightLine ? 1 : 0, - Child = new Box { RelativeSizeAxes = Axes.Both }.WithInitialColour(lineColour) + Child = new Box { RelativeSizeAxes = Axes.Both }.WithLegacyColour(lineColour) }, lightContainer = new Container { diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index 3b75fcc8a0..27dec1b691 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Texture = skin.GetTexture("sliderb-nd"), Colour = new Color4(5, 5, 5, 255), }, - animationContent.WithInitialColour(ballColour).With(d => + animationContent.WithLegacyColour(ballColour).With(d => { d.Anchor = Anchor.Centre; d.Origin = Anchor.Centre; diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index ee7d74d7ec..7420f82f04 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -82,10 +82,10 @@ namespace osu.Game.Skinning /// /// Beware: Any existing value in is overwritten. /// - /// The to set the "InitialColour" of. + /// The to set the colour of. /// The to set. /// The given . - public static T WithInitialColour(this T drawable, Color4 colour) + public static T WithLegacyColour(this T drawable, Color4 colour) where T : Drawable { drawable.Alpha = colour.A; From 891f5cb130b50748cc517369cd33f1f6cf91aca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Aug 2020 20:00:20 +0200 Subject: [PATCH 2799/6909] Add padding to mania column borders to match stable --- .../Skinning/LegacyColumnBackground.cs | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 64a7641421..f9286b5095 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -8,6 +8,7 @@ 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; @@ -20,9 +21,12 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly IBindable direction = new Bindable(); private readonly bool isLastColumn; + private Container borderLineContainer; private Container lightContainer; private Sprite light; + private float hitPosition; + public LegacyColumnBackground(bool isLastColumn) { this.isLastColumn = isLastColumn; @@ -44,6 +48,9 @@ namespace osu.Game.Rulesets.Mania.Skinning bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m || isLastColumn; + hitPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HitPosition)?.Value + ?? Stage.HIT_TARGET_POSITION; + float lightPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value ?? 0; @@ -63,23 +70,30 @@ namespace osu.Game.Rulesets.Mania.Skinning RelativeSizeAxes = Axes.Both, Colour = backgroundColour }, - new Box + borderLineContainer = new Container { - RelativeSizeAxes = Axes.Y, - Width = leftLineWidth, - Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasLeftLine ? 1 : 0 - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = rightLineWidth, - Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasRightLine ? 1 : 0 + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Y, + Width = leftLineWidth, + Scale = new Vector2(0.740f, 1), + Colour = lineColour, + Alpha = hasLeftLine ? 1 : 0 + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = rightLineWidth, + Scale = new Vector2(0.740f, 1), + Colour = lineColour, + Alpha = hasRightLine ? 1 : 0 + } + } }, lightContainer = new Container { @@ -109,11 +123,15 @@ namespace osu.Game.Rulesets.Mania.Skinning { lightContainer.Anchor = Anchor.TopCentre; lightContainer.Scale = new Vector2(1, -1); + + borderLineContainer.Padding = new MarginPadding { Top = hitPosition }; } else { lightContainer.Anchor = Anchor.BottomCentre; lightContainer.Scale = Vector2.One; + + borderLineContainer.Padding = new MarginPadding { Bottom = hitPosition }; } } From 809a61afcbb07daa8683191fbafdd16cc6204d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 21 Aug 2020 23:05:19 +0200 Subject: [PATCH 2800/6909] Adjust key binding panel tests to not rely on row indices --- .../Settings/TestSceneKeyBindingPanel.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index e06b3a8a7e..987a4a67fe 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -68,22 +68,22 @@ namespace osu.Game.Tests.Visual.Settings [Test] public void TestClearButtonOnBindings() { - KeyBindingRow backBindingRow = null; + KeyBindingRow multiBindingRow = null; - AddStep("click back binding row", () => + AddStep("click first row with two bindings", () => { - backBindingRow = panel.ChildrenOfType().ElementAt(10); - InputManager.MoveMouseTo(backBindingRow); + multiBindingRow = panel.ChildrenOfType().First(row => row.Defaults.Count() > 1); + InputManager.MoveMouseTo(multiBindingRow); InputManager.Click(MouseButton.Left); }); clickClearButton(); - AddAssert("first binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType().First().Text.Text)); + AddAssert("first binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().First().Text.Text)); AddStep("click second binding", () => { - var target = backBindingRow.ChildrenOfType().ElementAt(1); + var target = multiBindingRow.ChildrenOfType().ElementAt(1); InputManager.MoveMouseTo(target); InputManager.Click(MouseButton.Left); @@ -91,13 +91,13 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("second binding cleared", () => string.IsNullOrEmpty(backBindingRow.ChildrenOfType().ElementAt(1).Text.Text)); + AddAssert("second binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().ElementAt(1).Text.Text)); void clickClearButton() { AddStep("click clear button", () => { - var clearButton = backBindingRow.ChildrenOfType().Single(); + var clearButton = multiBindingRow.ChildrenOfType().Single(); InputManager.MoveMouseTo(clearButton); InputManager.Click(MouseButton.Left); @@ -108,20 +108,20 @@ namespace osu.Game.Tests.Visual.Settings [Test] public void TestClickRowSelectsFirstBinding() { - KeyBindingRow backBindingRow = null; + KeyBindingRow multiBindingRow = null; - AddStep("click back binding row", () => + AddStep("click first row with two bindings", () => { - backBindingRow = panel.ChildrenOfType().ElementAt(10); - InputManager.MoveMouseTo(backBindingRow); + multiBindingRow = panel.ChildrenOfType().First(row => row.Defaults.Count() > 1); + InputManager.MoveMouseTo(multiBindingRow); InputManager.Click(MouseButton.Left); }); - AddAssert("first binding selected", () => backBindingRow.ChildrenOfType().First().IsBinding); + AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType().First().IsBinding); AddStep("click second binding", () => { - var target = backBindingRow.ChildrenOfType().ElementAt(1); + var target = multiBindingRow.ChildrenOfType().ElementAt(1); InputManager.MoveMouseTo(target); InputManager.Click(MouseButton.Left); @@ -129,12 +129,12 @@ namespace osu.Game.Tests.Visual.Settings AddStep("click back binding row", () => { - backBindingRow = panel.ChildrenOfType().ElementAt(10); - InputManager.MoveMouseTo(backBindingRow); + multiBindingRow = panel.ChildrenOfType().ElementAt(10); + InputManager.MoveMouseTo(multiBindingRow); InputManager.Click(MouseButton.Left); }); - AddAssert("first binding selected", () => backBindingRow.ChildrenOfType().First().IsBinding); + AddAssert("first binding selected", () => multiBindingRow.ChildrenOfType().First().IsBinding); } } } From 0b6185cd14c18b32a031ef0b8d67b00ea6eef134 Mon Sep 17 00:00:00 2001 From: Keijia Date: Sat, 22 Aug 2020 01:09:35 +0300 Subject: [PATCH 2801/6909] add "hp" filter keyword --- osu.Game/Screens/Select/FilterQueryParser.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 89afc729fe..b7bcf99ce0 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.Select internal static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( - @"\b(?stars|ar|dr|cs|divisor|length|objects|bpm|status|creator|artist)(?[=:><]+)(?("".*"")|(\S*))", + @"\b(?stars|ar|dr|hp|cs|divisor|length|objects|bpm|status|creator|artist)(?[=:><]+)(?("".*"")|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static void ApplyQueries(FilterCriteria criteria, string query) @@ -46,6 +46,10 @@ namespace osu.Game.Screens.Select updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); break; + case "hp" when parseFloatWithPoint(value, out var dr): + updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); + break; + case "cs" when parseFloatWithPoint(value, out var cs): updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2); break; From f9fe37a8a51aba8955bcf3a6e5d7169cd480adbb Mon Sep 17 00:00:00 2001 From: Keijia Date: Sat, 22 Aug 2020 01:54:01 +0300 Subject: [PATCH 2802/6909] Added test for "hp" filter keyword --- .../NonVisual/Filtering/FilterQueryParserTest.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 7b2913b817..d15682b1eb 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual.Filtering } [Test] - public void TestApplyDrainRateQueries() + public void TestApplyDrainRateQueriesByDrKeyword() { const string query = "dr>2 quite specific dr<:6"; var filterCriteria = new FilterCriteria(); @@ -73,6 +73,20 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.Less(filterCriteria.DrainRate.Min, 6.1f); } + [Test] + public void TestApplyDrainRateQueriesByHpKeyword() + { + const string query = "hp>2 quite specific hp<=6"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim()); + Assert.AreEqual(2, filterCriteria.SearchTerms.Length); + Assert.Greater(filterCriteria.DrainRate.Min, 2.0f); + Assert.Less(filterCriteria.DrainRate.Min, 2.1f); + Assert.Greater(filterCriteria.DrainRate.Max, 6.0f); + Assert.Less(filterCriteria.DrainRate.Min, 6.1f); + } + [Test] public void TestApplyBPMQueries() { From b5b2e523ad3493f059fc33e15c810a98995d1a0d Mon Sep 17 00:00:00 2001 From: Keijia Date: Sat, 22 Aug 2020 12:10:31 +0300 Subject: [PATCH 2803/6909] change switch cases --- osu.Game/Screens/Select/FilterQueryParser.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index b7bcf99ce0..39fa4f777d 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -43,10 +43,7 @@ namespace osu.Game.Screens.Select break; case "dr" when parseFloatWithPoint(value, out var dr): - updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); - break; - - case "hp" when parseFloatWithPoint(value, out var dr): + case "hp" when parseFloatWithPoint(value, out dr): updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); break; From 7ae45b29dbc82c98012c4525bbf7a13d10bef161 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 22 Aug 2020 12:20:50 +0300 Subject: [PATCH 2804/6909] Finish internal counter transformation regardless of the combo --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 8ea06688cb..0d9df4f9a0 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -27,7 +27,6 @@ namespace osu.Game.Rulesets.Catch.Skinning private readonly float fontOverlap; private readonly LegacyRollingCounter counter; - private LegacyRollingCounter lastExplosion; public LegacyComboCounter(ISkin skin, string fontName, float fontOverlap) { @@ -70,8 +69,14 @@ namespace osu.Game.Rulesets.Catch.Skinning public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); + private LegacyRollingCounter lastExplosion; + private void updateCombo(int combo, Color4? hitObjectColour, bool immediate) { + // There may still be existing transforms to the counter (including value change after 250ms), + // finish them immediately before new transforms. + counter.FinishTransforms(); + // Combo fell to zero, roll down and fade out the counter. if (combo == 0) { @@ -83,8 +88,7 @@ namespace osu.Game.Rulesets.Catch.Skinning return; } - // There may still be previous transforms being applied, finish them and remove explosion. - FinishTransforms(true); + // Remove last explosion to not conflict with the upcoming one. if (lastExplosion != null) RemoveInternal(lastExplosion); From 7e838c80420d35a9c633cfc038fd3afd7ecf728a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 22 Aug 2020 13:07:15 +0300 Subject: [PATCH 2805/6909] Add comment explaining why direct string lookups are used --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 49ba02fdea..a9e3d36ca0 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -160,6 +160,7 @@ namespace osu.Game.Tests.Visual { this.extrapolateAnimations = extrapolateAnimations; + // Use a direct string lookup for simplicity, as they're legacy settings and not worth creating enums for them. legacyFontPrefixes.Add(GetConfig("HitCirclePrefix")?.Value ?? "default"); legacyFontPrefixes.Add(GetConfig("ScorePrefix")?.Value ?? "score"); legacyFontPrefixes.Add(GetConfig("ComboPrefix")?.Value ?? "score"); From b72f06fef68a219b599671cefe3ce2e2b252d3e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Aug 2020 19:42:34 +0900 Subject: [PATCH 2806/6909] Centralise and clarify LoadTrack documentation --- osu.Game/Beatmaps/IWorkingBeatmap.cs | 10 +++++++++- osu.Game/Beatmaps/WorkingBeatmap.cs | 4 ---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 88d73fd7c4..bcd94d76fd 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -56,8 +56,16 @@ namespace osu.Game.Beatmaps IBeatmap GetPlayableBeatmap(RulesetInfo ruleset, IReadOnlyList mods = null, TimeSpan? timeout = null); /// - /// Retrieves the which this provides. + /// Load a new audio track instance for this beatmap. This should be called once before accessing . + /// The caller of this method is responsible for the lifetime of the track. /// + /// + /// In a standard game context, the loading of the track is managed solely by MusicController, which will + /// automatically load the track of the current global IBindable WorkingBeatmap. + /// As such, this method should only be called in very special scenarios, such as external tests or apps which are + /// outside of the game context. + /// + /// A fresh track instance, which will also be available via . Track LoadTrack(); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 6a89739e6f..6a161e6e04 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -261,10 +261,6 @@ namespace osu.Game.Beatmaps private Track loadedTrack; - /// - /// Load a new audio track instance for this beatmap. - /// - /// A fresh track instance, which will also be available via . [NotNull] public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000); From db5226042747b7e5078903cdb14d3a36f9e4af73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Aug 2020 19:44:54 +0900 Subject: [PATCH 2807/6909] Rename and clarify comment regarding "previous" track disposal --- osu.Game/Overlays/MusicController.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 112026d9e2..6d5b5d43cd 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -331,10 +331,10 @@ namespace osu.Game.Overlays { var lastTrack = CurrentTrack; - var newTrack = new DrawableTrack(current.LoadTrack()); - newTrack.Completed += () => onTrackCompleted(current); + var queuedTrack = new DrawableTrack(current.LoadTrack()); + queuedTrack.Completed += () => onTrackCompleted(current); - CurrentTrack = newTrack; + CurrentTrack = queuedTrack; // At this point we may potentially be in an async context from tests. This is extremely dangerous but we have to make do for now. // CurrentTrack is immediately updated above for situations where a immediate knowledge about the new track is required, @@ -343,12 +343,13 @@ namespace osu.Game.Overlays { lastTrack.Expire(); - if (newTrack == CurrentTrack) - AddInternal(newTrack); + if (queuedTrack == CurrentTrack) + AddInternal(queuedTrack); else { - // If the track has changed via changeTrack() being called multiple times in a single update, force disposal on the old track. - newTrack.Dispose(); + // If the track has changed since the call to changeTrack, it is safe to dispose the + // queued track rather than consume it. + queuedTrack.Dispose(); } }); } From 122265ff0e233a72a48a329370e8f6f8c1fde6e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 22 Aug 2020 19:47:05 +0900 Subject: [PATCH 2808/6909] Revert non-track usage --- 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 c6bfdda698..c617950c64 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline track = b.NewValue.Track; // todo: i don't think this is safe, the track may not be loaded yet. - if (b.NewValue.Track.Length > 0) + if (track.Length > 0) { MaxZoom = getZoomLevelForVisibleMilliseconds(500); MinZoom = getZoomLevelForVisibleMilliseconds(10000); From fafdbb0a81ae98b0070f24f48622494aa2da6f0e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 22 Aug 2020 17:26:54 +0300 Subject: [PATCH 2809/6909] Adjust recently added inline comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index a9e3d36ca0..58e0b23fab 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -160,7 +160,7 @@ namespace osu.Game.Tests.Visual { this.extrapolateAnimations = extrapolateAnimations; - // Use a direct string lookup for simplicity, as they're legacy settings and not worth creating enums for them. + // use a direct string lookup instead of enum to avoid having to reference ruleset assemblies. legacyFontPrefixes.Add(GetConfig("HitCirclePrefix")?.Value ?? "default"); legacyFontPrefixes.Add(GetConfig("ScorePrefix")?.Value ?? "score"); legacyFontPrefixes.Add(GetConfig("ComboPrefix")?.Value ?? "score"); From ec99fcd7ab2d7ee681677cb4cb082727448f3afa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 17:10:31 +0200 Subject: [PATCH 2810/6909] Avoid passing down rhythm list every time --- .../Preprocessing/TaikoDifficultyHitObject.cs | 23 ++++++++++++++----- .../Difficulty/TaikoDifficultyCalculator.cs | 15 +----------- 2 files changed, 18 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 81b304af13..e52f616371 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.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.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; @@ -19,23 +18,35 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly int ObjectIndex; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex, - IEnumerable commonRhythms) + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex) : base(hitObject, lastObject, clockRate) { var currentHit = hitObject as Hit; double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - Rhythm = getClosestRhythm(DeltaTime / prevLength, commonRhythms); + Rhythm = getClosestRhythm(DeltaTime / prevLength); HitType = currentHit?.Type; ObjectIndex = objectIndex; } - private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio, IEnumerable commonRhythms) + private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = { - return commonRhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); + new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), + new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), + new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), + new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), + new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), + new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), + new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), + new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), + new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) + }; + + private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio) + { + return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index d3ff0b95ee..961a2dfcda 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -24,19 +24,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private const double colour_skill_multiplier = 0.01; private const double stamina_skill_multiplier = 0.02; - private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms = - { - new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), - new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), - new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), - new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), - new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), - new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), - new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), - new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) - }; - public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { @@ -135,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { taikoDifficultyHitObjects.Add( new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i, commonRhythms + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i ) ); } From cb3fef76161d8cddc1bf73f99953cb3175e33fc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 17:15:08 +0200 Subject: [PATCH 2811/6909] Inline same parity penalty --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index dd8b536afc..0453882f45 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -54,8 +54,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills else if ((monoHistory[^1] + currentMonoLength) % 2 == 0) { // The last streak in the history is guaranteed to be a different type to the current streak. - // If the total number of notes in the two streaks is even, apply a penalty. - objectStrain *= sameParityPenalty(); + // If the total number of notes in the two streaks is even, nullify this object's strain. + objectStrain = 0.0; } objectStrain *= repetitionPenalties(); @@ -70,11 +70,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return objectStrain; } - /// - /// The penalty to apply when the total number of notes in the two most recent colour streaks is even. - /// - private double sameParityPenalty() => 0.0; - /// /// The penalty to apply due to the length of repetition in colour streaks. /// From bcf3cd56574338f9ac2005854e1803a704abc439 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 17:24:51 +0200 Subject: [PATCH 2812/6909] Remove unnecessary yield iteration --- .../Difficulty/TaikoDifficultyCalculator.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 961a2dfcda..7a99abdac6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -129,8 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); - foreach (var hitobject in taikoDifficultyHitObjects) - yield return hitobject; + return taikoDifficultyHitObjects; } protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] From 7e2bef3b9fce0fffb8feb2fa0a4293a1714343bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 17:34:08 +0200 Subject: [PATCH 2813/6909] Split conditional for readability --- .../Difficulty/TaikoDifficultyCalculator.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 7a99abdac6..cbbef6e957 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -118,7 +118,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 2; i < beatmap.HitObjects.Count; i++) { // Check for negative durations - if (beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime && beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime) + var currentAfterLast = beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime; + var lastAfterSecondLast = beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime; + + if (currentAfterLast && lastAfterSecondLast) { taikoDifficultyHitObjects.Add( new TaikoDifficultyHitObject( From 8ace7df0fde9c2480a0f68ba06165e04fd18529d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 17:51:35 +0200 Subject: [PATCH 2814/6909] Reorder members for better readability --- .../Preprocessing/StaminaCheeseDetector.cs | 10 +- .../Preprocessing/TaikoDifficultyHitObject.cs | 7 +- .../Difficulty/Skills/Colour.cs | 14 +- .../Difficulty/Skills/Rhythm.cs | 68 ++++--- .../Difficulty/Skills/Stamina.cs | 38 ++-- .../Difficulty/TaikoDifficultyCalculator.cs | 181 +++++++++--------- 6 files changed, 158 insertions(+), 160 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index 3f952d8317..ef5f4433bf 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -13,11 +13,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing private const int roll_min_repetitions = 12; private const int tl_min_repetitions = 16; - private List hitObjects; + private readonly List hitObjects; - public void FindCheese(List difficultyHitObjects) + public StaminaCheeseDetector(List hitObjects) + { + this.hitObjects = hitObjects; + } + + public void FindCheese() { - hitObjects = difficultyHitObjects; findRolls(3); findRolls(4); findTlTap(0, HitType.Rim); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index e52f616371..5f7f8040c7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -13,11 +13,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public readonly TaikoDifficultyHitObjectRhythm Rhythm; public readonly HitType? HitType; + public readonly int ObjectIndex; public bool StaminaCheese; - public readonly int ObjectIndex; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex) : base(hitObject, lastObject, clockRate) { @@ -45,8 +44,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing }; private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio) - { - return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); - } + => common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 0453882f45..1adbf272b3 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -12,11 +12,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Colour : Skill { - private const int mono_history_max_length = 5; - protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; + private const int mono_history_max_length = 5; + + /// + /// List of the last most recent mono patterns, with the most recent at the end of the list. + /// + private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length); + private HitType? previousHitType; /// @@ -24,11 +29,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// private int currentMonoLength = 1; - /// - /// List of the last most recent mono patterns, with the most recent at the end of the list. - /// - private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length); - protected override double StrainValueOf(DifficultyHitObject current) { if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 6bb2eaf06a..f37a4c3f65 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -14,17 +14,43 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { protected override double SkillMultiplier => 10; protected override double StrainDecayBase => 0; - private const double strain_decay = 0.96; - private double currentStrain; - private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); + private const double strain_decay = 0.96; private const int rhythm_history_max_length = 8; + private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); + + private double currentStrain; private int notesSinceRhythmChange; - private double repetitionPenalty(int notesSince) + protected override double StrainValueOf(DifficultyHitObject current) { - return Math.Min(1.0, 0.032 * notesSince); + if (!(current.BaseObject is Hit)) + { + resetRhythmStrain(); + return 0.0; + } + + currentStrain *= strain_decay; + + TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current; + notesSinceRhythmChange += 1; + + if (hitobject.Rhythm.Difficulty == 0.0) + { + return 0.0; + } + + double objectStrain = hitobject.Rhythm.Difficulty; + + objectStrain *= repetitionPenalties(hitobject); + objectStrain *= patternLengthPenalty(notesSinceRhythmChange); + objectStrain *= speedPenalty(hitobject.DeltaTime); + + notesSinceRhythmChange = 0; + + currentStrain += objectStrain; + return currentStrain; } // Finds repetitions and applies penalties @@ -61,6 +87,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return true; } + private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); + private double patternLengthPenalty(int patternLength) { double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); @@ -78,36 +106,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return 0.0; } - protected override double StrainValueOf(DifficultyHitObject current) - { - if (!(current.BaseObject is Hit)) - { - resetRhythmStrain(); - return 0.0; - } - - currentStrain *= strain_decay; - - TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current; - notesSinceRhythmChange += 1; - - if (hitobject.Rhythm.Difficulty == 0.0) - { - return 0.0; - } - - double objectStrain = hitobject.Rhythm.Difficulty; - - objectStrain *= repetitionPenalties(hitobject); - objectStrain *= patternLengthPenalty(notesSinceRhythmChange); - objectStrain *= speedPenalty(hitobject.DeltaTime); - - notesSinceRhythmChange = 0; - - currentStrain += objectStrain; - return currentStrain; - } - private void resetRhythmStrain() { currentStrain = 0.0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 13510290f7..3fd21b5e6d 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -12,32 +12,19 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Stamina : Skill { - private readonly int hand; - protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; private const int max_history_length = 2; + + private readonly int hand; private readonly LimitedCapacityQueue notePairDurationHistory = new LimitedCapacityQueue(max_history_length); private double offhandObjectDuration = double.MaxValue; - // Penalty for tl tap or roll - private double cheesePenalty(double notePairDuration) + public Stamina(bool rightHand) { - if (notePairDuration > 125) return 1; - if (notePairDuration < 100) return 0.6; - - return 0.6 + (notePairDuration - 100) * 0.016; - } - - private double speedBonus(double notePairDuration) - { - if (notePairDuration >= 200) return 0; - - double bonus = 200 - notePairDuration; - bonus *= bonus; - return bonus / 100000; + hand = rightHand ? 1 : 0; } protected override double StrainValueOf(DifficultyHitObject current) @@ -71,9 +58,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return 0; } - public Stamina(bool rightHand) + // Penalty for tl tap or roll + private double cheesePenalty(double notePairDuration) { - hand = rightHand ? 1 : 0; + if (notePairDuration > 125) return 1; + if (notePairDuration < 100) return 0.6; + + return 0.6 + (notePairDuration - 100) * 0.016; + } + + private double speedBonus(double notePairDuration) + { + if (notePairDuration >= 200) return 0; + + double bonus = 200 - notePairDuration; + bonus *= bonus; + return bonus / 100000; } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index cbbef6e957..8e0cb2a094 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -29,87 +29,21 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } - private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) + protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { - if (colorDifficulty <= 0) return 0.79 - 0.25; + new Colour(), + new Rhythm(), + new Stamina(true), + new Stamina(false), + }; - return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; - } - - private double norm(double p, params double[] values) + protected override Mod[] DifficultyAdjustmentMods => new Mod[] { - return Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); - } - - private double rescale(double sr) - { - if (sr < 0) return sr; - - return 10.43 * Math.Log(sr / 8 + 1); - } - - private double locallyCombinedDifficulty( - double staminaPenalty, Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft) - { - double difficulty = 0; - double weight = 1; - List peaks = new List(); - - for (int i = 0; i < colour.StrainPeaks.Count; i++) - { - double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier; - double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier; - double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; - peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); - } - - foreach (double strain in peaks.OrderByDescending(d => d)) - { - difficulty += strain * weight; - weight *= 0.9; - } - - return difficulty; - } - - protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) - { - if (beatmap.HitObjects.Count == 0) - return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; - - var colour = (Colour)skills[0]; - var rhythm = (Rhythm)skills[1]; - var staminaRight = (Stamina)skills[2]; - var staminaLeft = (Stamina)skills[3]; - - double colourRating = colour.DifficultyValue() * colour_skill_multiplier; - double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; - double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier; - - double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); - staminaRating *= staminaPenalty; - - double combinedRating = locallyCombinedDifficulty(staminaPenalty, colour, rhythm, staminaRight, staminaLeft); - double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); - double starRating = 1.4 * separatedRating + 0.5 * combinedRating; - starRating = rescale(starRating); - - HitWindows hitWindows = new TaikoHitWindows(); - hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); - - return new TaikoDifficultyAttributes - { - StarRating = starRating, - Mods = mods, - StaminaStrain = staminaRating, - RhythmStrain = rhythmRating, - ColourStrain = colourRating, - // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate, - MaxCombo = beatmap.HitObjects.Count(h => h is Hit), - Skills = skills - }; - } + new TaikoModDoubleTime(), + new TaikoModHalfTime(), + new TaikoModEasy(), + new TaikoModHardRock(), + }; protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { @@ -131,24 +65,89 @@ namespace osu.Game.Rulesets.Taiko.Difficulty } } - new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); + new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese(); return taikoDifficultyHitObjects; } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { - new Colour(), - new Rhythm(), - new Stamina(true), - new Stamina(false), - }; + if (beatmap.HitObjects.Count == 0) + return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; - protected override Mod[] DifficultyAdjustmentMods => new Mod[] + var colour = (Colour)skills[0]; + var rhythm = (Rhythm)skills[1]; + var staminaRight = (Stamina)skills[2]; + var staminaLeft = (Stamina)skills[3]; + + double colourRating = colour.DifficultyValue() * colour_skill_multiplier; + double rhythmRating = rhythm.DifficultyValue() * rhythm_skill_multiplier; + double staminaRating = (staminaRight.DifficultyValue() + staminaLeft.DifficultyValue()) * stamina_skill_multiplier; + + double staminaPenalty = simpleColourPenalty(staminaRating, colourRating); + staminaRating *= staminaPenalty; + + double combinedRating = locallyCombinedDifficulty(colour, rhythm, staminaRight, staminaLeft, staminaPenalty); + double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); + double starRating = 1.4 * separatedRating + 0.5 * combinedRating; + starRating = rescale(starRating); + + HitWindows hitWindows = new TaikoHitWindows(); + hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); + + return new TaikoDifficultyAttributes + { + StarRating = starRating, + Mods = mods, + StaminaStrain = staminaRating, + RhythmStrain = rhythmRating, + ColourStrain = colourRating, + // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future + GreatHitWindow = (int)hitWindows.WindowFor(HitResult.Great) / clockRate, + MaxCombo = beatmap.HitObjects.Count(h => h is Hit), + Skills = skills + }; + } + + private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) { - new TaikoModDoubleTime(), - new TaikoModHalfTime(), - new TaikoModEasy(), - new TaikoModHardRock(), - }; + if (colorDifficulty <= 0) return 0.79 - 0.25; + + return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; + } + + private double norm(double p, params double[] values) + { + return Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + } + + private double rescale(double sr) + { + if (sr < 0) return sr; + + return 10.43 * Math.Log(sr / 8 + 1); + } + + private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty) + { + double difficulty = 0; + double weight = 1; + List peaks = new List(); + + for (int i = 0; i < colour.StrainPeaks.Count; i++) + { + double colourPeak = colour.StrainPeaks[i] * colour_skill_multiplier; + double rhythmPeak = rhythm.StrainPeaks[i] * rhythm_skill_multiplier; + double staminaPeak = (staminaRight.StrainPeaks[i] + staminaLeft.StrainPeaks[i]) * stamina_skill_multiplier * staminaPenalty; + peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); + } + + foreach (double strain in peaks.OrderByDescending(d => d)) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } } } From a0807747995823ed520f7beba6a39ddbb4580f41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 19:34:16 +0200 Subject: [PATCH 2815/6909] Add xmldoc to taiko difficulty calculation code --- .../Preprocessing/StaminaCheeseDetector.cs | 41 +++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 57 ++++++++-- .../TaikoDifficultyHitObjectRhythm.cs | 17 +++ .../Difficulty/Skills/Colour.cs | 34 ++++-- .../Difficulty/Skills/Rhythm.cs | 100 +++++++++++++----- .../Difficulty/Skills/Stamina.cs | 36 ++++++- .../Difficulty/TaikoDifficultyCalculator.cs | 47 +++++--- 7 files changed, 281 insertions(+), 51 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index ef5f4433bf..5187d101ac 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -8,11 +8,32 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { + /// + /// Detects special hit object patterns which are easier to hit using special techniques + /// than normally assumed in the fully-alternating play style. + /// + /// + /// This component detects two basic types of patterns, leveraged by the following techniques: + /// + /// Rolling allows hitting patterns with quickly and regularly alternating notes with a single hand. + /// TL tapping makes hitting longer sequences of consecutive same-colour notes with little to no colour changes in-between. + /// + /// public class StaminaCheeseDetector { + /// + /// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a roll. + /// private const int roll_min_repetitions = 12; + + /// + /// The minimum number of consecutive objects with repeating patterns that can be classified as hittable using a TL tap. + /// private const int tl_min_repetitions = 16; + /// + /// The list of all s in the map. + /// private readonly List hitObjects; public StaminaCheeseDetector(List hitObjects) @@ -20,16 +41,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing this.hitObjects = hitObjects; } + /// + /// Finds and marks all objects in that special difficulty-reducing techiques apply to + /// with the flag. + /// public void FindCheese() { findRolls(3); findRolls(4); + findTlTap(0, HitType.Rim); findTlTap(1, HitType.Rim); findTlTap(0, HitType.Centre); findTlTap(1, HitType.Centre); } + /// + /// Finds and marks all sequences hittable using a roll. + /// + /// The length of a single repeating pattern to consider (triplets/quadruplets). private void findRolls(int patternLength) { var history = new LimitedCapacityQueue(2 * patternLength); @@ -56,6 +86,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } + /// + /// Determines whether the objects stored in contain a repetition of a pattern of length . + /// private static bool containsPatternRepeat(LimitedCapacityQueue history, int patternLength) { for (int j = 0; j < patternLength; j++) @@ -67,6 +100,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing return true; } + /// + /// Finds and marks all sequences hittable using a TL tap. + /// + /// Whether sequences starting with an odd- (1) or even-indexed (0) hit object should be checked. + /// The type of hit to check for TL taps. private void findTlTap(int parity, HitType type) { int tlLength = -2; @@ -85,6 +123,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } + /// + /// Marks all objects from index up until (exclusive) as . + /// private void markObjectsAsCheese(int start, int end) { for (int i = start; i < end; ++i) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 5f7f8040c7..ae33c184d0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -9,27 +9,61 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { + /// + /// Represents a single hit object in taiko difficulty calculation. + /// public class TaikoDifficultyHitObject : DifficultyHitObject { + /// + /// The rhythm required to hit this hit object. + /// public readonly TaikoDifficultyHitObjectRhythm Rhythm; + + /// + /// The hit type of this hit object. + /// public readonly HitType? HitType; + + /// + /// The index of the object in the beatmap. + /// public readonly int ObjectIndex; + /// + /// Whether the object should carry a penalty due to being hittable using special techniques + /// making it easier to do so. + /// public bool StaminaCheese; + /// + /// Creates a new difficulty hit object. + /// + /// The gameplay associated with this difficulty object. + /// The gameplay preceding . + /// The gameplay preceding . + /// The rate of the gameplay clock. Modified by speed-changing mods. + /// The index of the object in the beatmap. public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, int objectIndex) : base(hitObject, lastObject, clockRate) { var currentHit = hitObject as Hit; - double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - - Rhythm = getClosestRhythm(DeltaTime / prevLength); + Rhythm = getClosestRhythm(lastObject, lastLastObject, clockRate); HitType = currentHit?.Type; ObjectIndex = objectIndex; } + /// + /// List of most common rhythm changes in taiko maps. + /// + /// + /// The general guidelines for the values are: + /// + /// rhythm changes with ratio closer to 1 (that are not 1) are harder to play, + /// speeding up is generally harder than slowing down (with exceptions of rhythm changes requiring a hand switch). + /// + /// private static readonly TaikoDifficultyHitObjectRhythm[] common_rhythms = { new TaikoDifficultyHitObjectRhythm(1, 1, 0.0), @@ -37,13 +71,24 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), - new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), + new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), // purposefully higher (requires hand switch in full alternating gameplay style) new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) }; - private TaikoDifficultyHitObjectRhythm getClosestRhythm(double ratio) - => common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); + /// + /// Returns the closest rhythm change from required to hit this object. + /// + /// The gameplay preceding this one. + /// The gameplay preceding . + /// The rate of the gameplay clock. + private TaikoDifficultyHitObjectRhythm getClosestRhythm(HitObject lastObject, HitObject lastLastObject, double clockRate) + { + double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; + double ratio = DeltaTime / prevLength; + + return common_rhythms.OrderBy(x => Math.Abs(x.Ratio - ratio)).First(); + } } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index 9c22eff22a..b6dc69a380 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -3,11 +3,28 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { + /// + /// Represents a rhythm change in a taiko map. + /// public class TaikoDifficultyHitObjectRhythm { + /// + /// The difficulty multiplier associated with this rhythm change. + /// public readonly double Difficulty; + + /// + /// The ratio of current to previous for the rhythm change. + /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. + /// public readonly double Ratio; + /// + /// Creates an object representing a rhythm change. + /// + /// The numerator for . + /// The denominator for + /// The difficulty multiplier associated with this rhythm change. public TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) { Ratio = numerator / (double)denominator; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 1adbf272b3..9fad83c6a1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -10,18 +10,28 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { + /// + /// Calculates the colour coefficient of taiko difficulty. + /// public class Colour : Skill { protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; + /// + /// Maximum number of entries to keep in . + /// private const int mono_history_max_length = 5; /// - /// List of the last most recent mono patterns, with the most recent at the end of the list. + /// Queue with the lengths of the last most recent mono (single-colour) patterns, + /// with the most recent value at the end of the queue. /// private readonly LimitedCapacityQueue monoHistory = new LimitedCapacityQueue(mono_history_max_length); + /// + /// The of the last object hit before the one being considered. + /// private HitType? previousHitType; /// @@ -31,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double StrainValueOf(DifficultyHitObject current) { + // changing from/to a drum roll or a swell does not constitute a colour change. + // hits spaced more than a second apart are also exempt from colour strain. if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) { previousHitType = null; @@ -75,14 +87,14 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// private double repetitionPenalties() { - const int l = 2; + const int most_recent_patterns_to_compare = 2; double penalty = 1.0; monoHistory.Enqueue(currentMonoLength); - for (int start = monoHistory.Count - l - 1; start >= 0; start--) + for (int start = monoHistory.Count - most_recent_patterns_to_compare - 1; start >= 0; start--) { - if (!isSamePattern(start, l)) + if (!isSamePattern(start, most_recent_patterns_to_compare)) continue; int notesSince = 0; @@ -94,17 +106,25 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return penalty; } - private bool isSamePattern(int start, int l) + /// + /// Determines whether the last patterns have repeated in the history + /// of single-colour note sequences, starting from . + /// + private bool isSamePattern(int start, int mostRecentPatternsToCompare) { - for (int i = 0; i < l; i++) + for (int i = 0; i < mostRecentPatternsToCompare; i++) { - if (monoHistory[start + i] != monoHistory[monoHistory.Count - l + i]) + if (monoHistory[start + i] != monoHistory[monoHistory.Count - mostRecentPatternsToCompare + i]) return false; } return true; } + /// + /// Calculates the strain penalty for a colour pattern repetition. + /// + /// The number of notes since the last repetition of the pattern. private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index f37a4c3f65..5569b27ad5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -10,64 +10,97 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { + /// + /// Calculates the rhythm coefficient of taiko difficulty. + /// public class Rhythm : Skill { protected override double SkillMultiplier => 10; protected override double StrainDecayBase => 0; + /// + /// The note-based decay for rhythm strain. + /// + /// + /// is not used here, as it's time- and not note-based. + /// private const double strain_decay = 0.96; + + /// + /// Maximum number of entries in . + /// private const int rhythm_history_max_length = 8; + /// + /// Contains the last changes in note sequence rhythms. + /// private readonly LimitedCapacityQueue rhythmHistory = new LimitedCapacityQueue(rhythm_history_max_length); + /// + /// Contains the rolling rhythm strain. + /// Used to apply per-note decay. + /// private double currentStrain; + + /// + /// Number of notes since the last rhythm change has taken place. + /// private int notesSinceRhythmChange; protected override double StrainValueOf(DifficultyHitObject current) { + // drum rolls and swells are exempt. if (!(current.BaseObject is Hit)) { - resetRhythmStrain(); + resetRhythmAndStrain(); return 0.0; } currentStrain *= strain_decay; - TaikoDifficultyHitObject hitobject = (TaikoDifficultyHitObject)current; + TaikoDifficultyHitObject hitObject = (TaikoDifficultyHitObject)current; notesSinceRhythmChange += 1; - if (hitobject.Rhythm.Difficulty == 0.0) + // rhythm difficulty zero (due to rhythm not changing) => no rhythm strain. + if (hitObject.Rhythm.Difficulty == 0.0) { return 0.0; } - double objectStrain = hitobject.Rhythm.Difficulty; + double objectStrain = hitObject.Rhythm.Difficulty; - objectStrain *= repetitionPenalties(hitobject); + objectStrain *= repetitionPenalties(hitObject); objectStrain *= patternLengthPenalty(notesSinceRhythmChange); - objectStrain *= speedPenalty(hitobject.DeltaTime); + objectStrain *= speedPenalty(hitObject.DeltaTime); + // careful - needs to be done here since calls above read this value notesSinceRhythmChange = 0; currentStrain += objectStrain; return currentStrain; } - // Finds repetitions and applies penalties - private double repetitionPenalties(TaikoDifficultyHitObject hitobject) + /// + /// Returns a penalty to apply to the current hit object caused by repeating rhythm changes. + /// + /// + /// Repetitions of more recent patterns are associated with a higher penalty. + /// + /// The current hit object being considered. + private double repetitionPenalties(TaikoDifficultyHitObject hitObject) { double penalty = 1; - rhythmHistory.Enqueue(hitobject); + rhythmHistory.Enqueue(hitObject); - for (int l = 2; l <= rhythm_history_max_length / 2; l++) + for (int mostRecentPatternsToCompare = 2; mostRecentPatternsToCompare <= rhythm_history_max_length / 2; mostRecentPatternsToCompare++) { - for (int start = rhythmHistory.Count - l - 1; start >= 0; start--) + for (int start = rhythmHistory.Count - mostRecentPatternsToCompare - 1; start >= 0; start--) { - if (!samePattern(start, l)) + if (!samePattern(start, mostRecentPatternsToCompare)) continue; - int notesSince = hitobject.ObjectIndex - rhythmHistory[start].ObjectIndex; + int notesSince = hitObject.ObjectIndex - rhythmHistory[start].ObjectIndex; penalty *= repetitionPenalty(notesSince); break; } @@ -76,37 +109,56 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return penalty; } - private bool samePattern(int start, int l) + /// + /// Determines whether the rhythm change pattern starting at is a repeat of any of the + /// . + /// + private bool samePattern(int start, int mostRecentPatternsToCompare) { - for (int i = 0; i < l; i++) + for (int i = 0; i < mostRecentPatternsToCompare; i++) { - if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - l + i].Rhythm) + if (rhythmHistory[start + i].Rhythm != rhythmHistory[rhythmHistory.Count - mostRecentPatternsToCompare + i].Rhythm) return false; } return true; } - private double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); + /// + /// Calculates a single rhythm repetition penalty. + /// + /// Number of notes since the last repetition of a rhythm change. + private static double repetitionPenalty(int notesSince) => Math.Min(1.0, 0.032 * notesSince); - private double patternLengthPenalty(int patternLength) + /// + /// Calculates a penalty based on the number of notes since the last rhythm change. + /// Both rare and frequent rhythm changes are penalised. + /// + /// Number of notes since the last rhythm change. + private static double patternLengthPenalty(int patternLength) { double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); double longPatternPenalty = Math.Clamp(2.5 - 0.15 * patternLength, 0.0, 1.0); return Math.Min(shortPatternPenalty, longPatternPenalty); } - // Penalty for notes so slow that alternating is not necessary. - private double speedPenalty(double noteLengthMs) + /// + /// Calculates a penalty for objects that do not require alternating hands. + /// + /// Time (in milliseconds) since the last hit object. + private double speedPenalty(double deltaTime) { - if (noteLengthMs < 80) return 1; - if (noteLengthMs < 210) return Math.Max(0, 1.4 - 0.005 * noteLengthMs); + if (deltaTime < 80) return 1; + if (deltaTime < 210) return Math.Max(0, 1.4 - 0.005 * deltaTime); - resetRhythmStrain(); + resetRhythmAndStrain(); return 0.0; } - private void resetRhythmStrain() + /// + /// Resets the rolling strain value and counter. + /// + private void resetRhythmAndStrain() { currentStrain = 0.0; notesSinceRhythmChange = 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 3fd21b5e6d..0b61eb9930 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -10,18 +10,45 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { + /// + /// Calculates the stamina coefficient of taiko difficulty. + /// + /// + /// The reference play style chosen uses two hands, with full alternating (the hand changes after every hit). + /// public class Stamina : Skill { protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0.4; + /// + /// Maximum number of entries to keep in . + /// private const int max_history_length = 2; + /// + /// The index of the hand this instance is associated with. + /// + /// + /// The value of 0 indicates the left hand (full alternating gameplay starting with left hand is assumed). + /// This naturally translates onto index offsets of the objects in the map. + /// private readonly int hand; + + /// + /// Stores the last durations between notes hit with the hand indicated by . + /// private readonly LimitedCapacityQueue notePairDurationHistory = new LimitedCapacityQueue(max_history_length); + /// + /// Stores the of the last object that was hit by the other hand. + /// private double offhandObjectDuration = double.MaxValue; + /// + /// Creates a skill. + /// + /// Whether this instance is performing calculations for the right hand. public Stamina(bool rightHand) { hand = rightHand ? 1 : 0; @@ -58,7 +85,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return 0; } - // Penalty for tl tap or roll + /// + /// Applies a penalty for hit objects marked with . + /// + /// The duration between the current and previous note hit using the hand indicated by . private double cheesePenalty(double notePairDuration) { if (notePairDuration > 125) return 1; @@ -67,6 +97,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return 0.6 + (notePairDuration - 100) * 0.016; } + /// + /// Applies a speed bonus dependent on the time since the last hit performed using this hand. + /// + /// The duration between the current and previous note hit using the hand indicated by . private double speedBonus(double notePairDuration) { if (notePairDuration >= 200) return 0; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 8e0cb2a094..ef43fc6d1e 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -108,6 +108,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty }; } + /// + /// Calculates the penalty for the stamina skill for maps with low colour difficulty. + /// + /// + /// Some maps (especially converts) can be easy to read despite a high note density. + /// This penalty aims to reduce the star rating of such maps by factoring in colour difficulty to the stamina skill. + /// private double simpleColourPenalty(double staminaDifficulty, double colorDifficulty) { if (colorDifficulty <= 0) return 0.79 - 0.25; @@ -115,22 +122,22 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return 0.79 - Math.Atan(staminaDifficulty / colorDifficulty - 12) / Math.PI / 2; } - private double norm(double p, params double[] values) - { - return Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); - } - - private double rescale(double sr) - { - if (sr < 0) return sr; - - return 10.43 * Math.Log(sr / 8 + 1); - } + /// + /// Returns the p-norm of an n-dimensional vector. + /// + /// The value of p to calculate the norm for. + /// The coefficients of the vector. + private double norm(double p, params double[] values) => Math.Pow(values.Sum(x => Math.Pow(x, p)), 1 / p); + /// + /// Returns the partial star rating of the beatmap, calculated using peak strains from all sections of the map. + /// + /// + /// For each section, the peak strains of all separate skills are combined into a single peak strain for the section. + /// The resulting partial rating of the beatmap is a weighted sum of the combined peaks (higher peaks are weighted more). + /// private double locallyCombinedDifficulty(Colour colour, Rhythm rhythm, Stamina staminaRight, Stamina staminaLeft, double staminaPenalty) { - double difficulty = 0; - double weight = 1; List peaks = new List(); for (int i = 0; i < colour.StrainPeaks.Count; i++) @@ -141,6 +148,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } + double difficulty = 0; + double weight = 1; + foreach (double strain in peaks.OrderByDescending(d => d)) { difficulty += strain * weight; @@ -149,5 +159,16 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return difficulty; } + + /// + /// Applies a final re-scaling of the star rating to bring maps with recorded full combos below 9.5 stars. + /// + /// The raw star rating value before re-scaling. + private double rescale(double sr) + { + if (sr < 0) return sr; + + return 10.43 * Math.Log(sr / 8 + 1); + } } } From 5afe9b73d2736c6563826916c1056fcdf285bf17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 21:27:08 +0200 Subject: [PATCH 2816/6909] Fix invalid cref --- .../Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index b6dc69a380..ea6a224094 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -14,7 +14,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly double Difficulty; /// - /// The ratio of current to previous for the rhythm change. + /// The ratio of current + /// to previous for the rhythm change. /// A above 1 indicates a slow-down; a below 1 indicates a speed-up. /// public readonly double Ratio; From 7c9fae55ad8b5aa2706f29800f91aacca11eedca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 22 Aug 2020 22:50:58 +0200 Subject: [PATCH 2817/6909] Hopefully fix off-by-one errors --- .../Preprocessing/StaminaCheeseDetector.cs | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index 5187d101ac..d07bff4369 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.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.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Objects; @@ -64,7 +63,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { var history = new LimitedCapacityQueue(2 * patternLength); - int repetitionStart = 0; + // for convenience, we're tracking the index of the item *before* our suspected repeat's start, + // as that index can be simply subtracted from the current index to get the number of elements in between + // without off-by-one errors + int indexBeforeLastRepeat = -1; for (int i = 0; i < hitObjects.Count; i++) { @@ -74,15 +76,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (!containsPatternRepeat(history, patternLength)) { - repetitionStart = i - 2 * patternLength; + // we're setting this up for the next iteration, hence the +1. + // right here this index will point at the queue's front (oldest item), + // but that item is about to be popped next loop with an enqueue. + indexBeforeLastRepeat = i - history.Count + 1; continue; } - int repeatedLength = i - repetitionStart; + int repeatedLength = i - indexBeforeLastRepeat; if (repeatedLength < roll_min_repetitions) continue; - markObjectsAsCheese(repetitionStart, i); + markObjectsAsCheese(i, repeatedLength); } } @@ -119,17 +124,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (tlLength < tl_min_repetitions) continue; - markObjectsAsCheese(Math.Max(0, i - tlLength), i); + markObjectsAsCheese(i, tlLength); } } /// - /// Marks all objects from index up until (exclusive) as . + /// Marks elements counting backwards from as . /// - private void markObjectsAsCheese(int start, int end) + private void markObjectsAsCheese(int end, int count) { - for (int i = start; i < end; ++i) - hitObjects[i].StaminaCheese = true; + for (int i = 0; i < count; ++i) + hitObjects[end - i].StaminaCheese = true; } } } From 0e9242ee9a327e220d7716c812ffb4bb3468790d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 23 Aug 2020 10:28:05 +0300 Subject: [PATCH 2818/6909] Move combo font retrieval inside the legacy component --- .../Skinning/CatchLegacySkinTransformer.cs | 3 +-- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 7 ++++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 0e434291c1..28cd0fb65b 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -55,11 +55,10 @@ namespace osu.Game.Rulesets.Catch.Skinning case CatchSkinComponents.CatchComboCounter: var comboFont = GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; - var fontOverlap = GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. if (this.HasFont(comboFont)) - return new LegacyComboCounter(Source, comboFont, fontOverlap); + return new LegacyComboCounter(Source); break; } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 0d9df4f9a0..c1c70c3a0e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -13,6 +13,7 @@ using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; +using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Catch.Skinning { @@ -28,12 +29,12 @@ namespace osu.Game.Rulesets.Catch.Skinning private readonly LegacyRollingCounter counter; - public LegacyComboCounter(ISkin skin, string fontName, float fontOverlap) + public LegacyComboCounter(ISkin skin) { this.skin = skin; - this.fontName = fontName; - this.fontOverlap = fontOverlap; + fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; + fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; AutoSizeAxes = Axes.Both; From e6646b9877883560187415b1781e3a726da1ca43 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 23 Aug 2020 15:08:02 +0200 Subject: [PATCH 2819/6909] Resolve review comments --- .../Formats/LegacyBeatmapEncoderTest.cs | 31 +++++++++++++------ .../Beatmaps/IO/ImportBeatmapTest.cs | 5 ++- osu.Game/Beatmaps/BeatmapManager.cs | 12 +++++-- .../Beatmaps/Formats/IHasCustomColours.cs | 2 +- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 25 ++++++++------- osu.Game/Screens/Edit/Editor.cs | 4 +-- osu.Game/Screens/Edit/EditorChangeHandler.cs | 4 +-- .../Skinning/LegacyManiaSkinConfiguration.cs | 2 +- osu.Game/Skinning/SkinConfiguration.cs | 16 +++++----- 9 files changed, 59 insertions(+), 42 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index db18f9b444..4e9e6743c9 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -28,25 +28,27 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestFixture] public class LegacyBeatmapEncoderTest { - private static readonly DllResourceStore resource_store = TestResources.GetStore(); + private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore(); - private static IEnumerable allBeatmaps = resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu")); + private static IEnumerable allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu")); [TestCaseSource(nameof(allBeatmaps))] public void TestEncodeDecodeStability(string name) { - var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name)); - var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded)); + var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name), name); + var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name); - sort(decoded.beatmap); - sort(decodedAfterEncode.beatmap); + sort(decoded); + sort(decodedAfterEncode); Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); Assert.IsTrue(decoded.beatmapSkin.Configuration.Equals(decodedAfterEncode.beatmapSkin.Configuration)); } - private void sort(IBeatmap beatmap) + private void sort((IBeatmap, LegacyBeatmapSkin) bs) { + var (beatmap, beatmapSkin) = bs; + // 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) { @@ -55,17 +57,26 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private (IBeatmap beatmap, LegacySkin beatmapSkin) decodeFromLegacy(Stream stream) + private (IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin) decodeFromLegacy(Stream stream, string name) { using (var reader = new LineBufferedReader(stream)) { var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); - var beatmapSkin = new LegacyBeatmapSkin(beatmap.BeatmapInfo, resource_store, null); + beatmap.BeatmapInfo.BeatmapSet.Files = new List + { + new BeatmapSetFileInfo + { + Filename = name, + FileInfo = new osu.Game.IO.FileInfo() { Hash = name } + } + }; + + var beatmapSkin = new LegacyBeatmapSkin(beatmap.BeatmapInfo, beatmaps_resource_store, null); return (convert(beatmap), beatmapSkin); } } - private Stream encodeToLegacy((IBeatmap beatmap, LegacySkin beatmapSkin) fullBeatmap) + private Stream encodeToLegacy((IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin) fullBeatmap) { var (beatmap, beatmapSkin) = fullBeatmap; var stream = new MemoryStream(); diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 17271184c0..0151678db3 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -730,8 +730,7 @@ namespace osu.Game.Tests.Beatmaps.IO BeatmapSetInfo setToUpdate = manager.GetAllUsableBeatmapSets()[0]; var beatmapInfo = setToUpdate.Beatmaps.First(b => b.RulesetID == 0); - var workingBeatmap = manager.GetWorkingBeatmap(setToUpdate.Beatmaps.First(b => b.RulesetID == 0)); - Beatmap beatmapToUpdate = (Beatmap)workingBeatmap.Beatmap; + 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; @@ -739,7 +738,7 @@ namespace osu.Game.Tests.Beatmaps.IO beatmapToUpdate.HitObjects.Clear(); beatmapToUpdate.HitObjects.Add(new HitCircle { StartTime = 5000 }); - manager.Save(beatmapInfo, beatmapToUpdate, workingBeatmap.Skin); + 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)); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index bd757d30ca..86d35749ac 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -195,8 +195,8 @@ namespace osu.Game.Beatmaps /// /// The to save the content against. The file referenced by will be replaced. /// The content to write. - /// Optional beatmap skin for inline skin configuration in beatmap files. - public void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin skin) + /// The beatmap content to write, or null if not to be changed. + public void Save(BeatmapInfo info, IBeatmap beatmapContent, LegacyBeatmapSkin beatmapSkin = null) { var setInfo = QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == info.ID)); @@ -204,7 +204,13 @@ namespace osu.Game.Beatmaps { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) { - new LegacyBeatmapEncoder(beatmapContent, skin).Encode(sw); + if (beatmapSkin == null) + { + var workingBeatmap = GetWorkingBeatmap(info); + beatmapSkin = (workingBeatmap.Skin is LegacyBeatmapSkin legacy) ? legacy : null; + } + + new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); } stream.Seek(0, SeekOrigin.Begin); diff --git a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs index 8f6c7dc328..1ac5ca83cb 100644 --- a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs +++ b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs @@ -8,6 +8,6 @@ namespace osu.Game.Beatmaps.Formats { public interface IHasCustomColours { - Dictionary CustomColours { get; set; } + IDictionary CustomColours { get; } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 716f1bc814..497c3c88d0 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Beatmaps.Formats { @@ -24,11 +25,14 @@ namespace osu.Game.Beatmaps.Formats public const int LATEST_VERSION = 128; private readonly IBeatmap beatmap; - private readonly ISkin skin; + private readonly LegacyBeatmapSkin skin; - /// The beatmap to encode - /// An optional skin, for encoding the beatmap's combo colours. This will only work if the parameter is a type of . - public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin) + /// + /// Creates a new . + /// + /// The beatmap to encode. + /// An optional skin, for encoding the beatmap's combo colours. + public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] LegacyBeatmapSkin skin) { this.beatmap = beatmap; this.skin = skin; @@ -210,7 +214,7 @@ namespace osu.Game.Beatmaps.Formats if (!(skin is LegacyBeatmapSkin legacySkin)) return; - var colours = legacySkin?.Configuration.ComboColours; + var colours = legacySkin.GetConfig>(GlobalSkinColours.ComboColours)?.Value; if (colours == null || colours.Count == 0) return; @@ -221,12 +225,11 @@ namespace osu.Game.Beatmaps.Formats { var comboColour = colours[i]; - var r = (byte)(comboColour.R * byte.MaxValue); - var g = (byte)(comboColour.G * byte.MaxValue); - var b = (byte)(comboColour.B * byte.MaxValue); - var a = (byte)(comboColour.A * byte.MaxValue); - - writer.WriteLine($"Combo{i}: {r},{g},{b},{a}"); + writer.Write(FormattableString.Invariant($"Combo{i}: ")); + writer.Write(FormattableString.Invariant($"{(byte)(comboColour.R * byte.MaxValue)},")); + writer.Write(FormattableString.Invariant($"{(byte)(comboColour.G * byte.MaxValue)},")); + writer.Write(FormattableString.Invariant($"{(byte)(comboColour.B * byte.MaxValue)},")); + writer.WriteLine(FormattableString.Invariant($"{(byte)(comboColour.A * byte.MaxValue)}")); } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a585db1ee9..7bd6529897 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Edit private IBeatmap playableBeatmap; private EditorBeatmap editorBeatmap; private EditorChangeHandler changeHandler; - private ISkin beatmapSkin; + private LegacyBeatmapSkin beatmapSkin; private DependencyContainer dependencies; @@ -94,7 +94,6 @@ namespace osu.Game.Screens.Edit try { playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); - beatmapSkin = Beatmap.Value.Skin; } catch (Exception e) { @@ -107,6 +106,7 @@ namespace osu.Game.Screens.Edit AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap)); dependencies.CacheAs(editorBeatmap); + beatmapSkin = (Beatmap.Value.Skin is LegacyBeatmapSkin legacy) ? legacy : null; changeHandler = new EditorChangeHandler(editorBeatmap, beatmapSkin); dependencies.CacheAs(changeHandler); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 0489236d45..1d10eaf5cb 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Edit private int currentState = -1; private readonly EditorBeatmap editorBeatmap; - private readonly ISkin beatmapSkin; + private readonly LegacyBeatmapSkin beatmapSkin; private int bulkChangesStarted; private bool isRestoring; @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit /// /// The to track the s of. /// The skin to track the inline skin configuration of. - public EditorChangeHandler(EditorBeatmap editorBeatmap, ISkin beatmapSkin) + public EditorChangeHandler(EditorBeatmap editorBeatmap, LegacyBeatmapSkin beatmapSkin) { this.editorBeatmap = editorBeatmap; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index af7d6007f3..f92da0b446 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -23,7 +23,7 @@ namespace osu.Game.Skinning public readonly int Keys; - public Dictionary CustomColours { get; set; } = new Dictionary(); + public IDictionary CustomColours { get; set; } = new SortedDictionary(); public Dictionary ImageLookups = new Dictionary(); diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index a48d713771..9565eee827 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -12,7 +12,7 @@ namespace osu.Game.Skinning /// /// An empty skin configuration. /// - public class SkinConfiguration : IHasComboColours, IHasCustomColours, IEquatable + public class SkinConfiguration : IEquatable, IHasComboColours, IHasCustomColours { public readonly SkinInfo SkinInfo = new SkinInfo(); @@ -47,16 +47,14 @@ namespace osu.Game.Skinning public void AddComboColours(params Color4[] colours) => comboColours.AddRange(colours); - public Dictionary CustomColours { get; set; } = new Dictionary(); + public IDictionary CustomColours { get; set; } = new SortedDictionary(); - public readonly Dictionary ConfigDictionary = new Dictionary(); + public readonly SortedDictionary ConfigDictionary = new SortedDictionary(); public bool Equals(SkinConfiguration other) - { - return other != null && - ConfigDictionary.SequenceEqual(other.ConfigDictionary) && - ComboColours.SequenceEqual(other.ComboColours) && - CustomColours?.SequenceEqual(other.CustomColours) == true; - } + => other != null + && ConfigDictionary.SequenceEqual(other.ConfigDictionary) + && ComboColours.SequenceEqual(other.ComboColours) + && CustomColours?.SequenceEqual(other.CustomColours) == true; } } From 492be0e0169248794305cda313b187b1ca0c3894 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 23 Aug 2020 15:23:10 +0200 Subject: [PATCH 2820/6909] Fix formatting --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 8 +++----- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 2 +- osu.Game/Skinning/SkinConfiguration.cs | 11 +++++------ 3 files changed, 9 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 4e9e6743c9..f093180085 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -45,12 +45,10 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(decoded.beatmapSkin.Configuration.Equals(decodedAfterEncode.beatmapSkin.Configuration)); } - private void sort((IBeatmap, LegacyBeatmapSkin) bs) + private void sort((IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin) tuple) { - var (beatmap, beatmapSkin) = bs; - // 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) + foreach (var g in tuple.beatmap.ControlPointInfo.Groups) { ArrayList.Adapter((IList)g.ControlPoints).Sort( Comparer.Create((c1, c2) => string.Compare(c1.GetType().ToString(), c2.GetType().ToString(), StringComparison.Ordinal))); @@ -67,7 +65,7 @@ namespace osu.Game.Tests.Beatmaps.Formats new BeatmapSetFileInfo { Filename = name, - FileInfo = new osu.Game.IO.FileInfo() { Hash = name } + FileInfo = new osu.Game.IO.FileInfo { Hash = name } } }; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index f92da0b446..baa7fa817c 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -23,7 +23,7 @@ namespace osu.Game.Skinning public readonly int Keys; - public IDictionary CustomColours { get; set; } = new SortedDictionary(); + public IDictionary CustomColours { get; } = new SortedDictionary(); public Dictionary ImageLookups = new Dictionary(); diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index 9565eee827..27589577e6 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -47,14 +47,13 @@ namespace osu.Game.Skinning public void AddComboColours(params Color4[] colours) => comboColours.AddRange(colours); - public IDictionary CustomColours { get; set; } = new SortedDictionary(); + public IDictionary CustomColours { get; } = new SortedDictionary(); public readonly SortedDictionary ConfigDictionary = new SortedDictionary(); - public bool Equals(SkinConfiguration other) - => other != null - && ConfigDictionary.SequenceEqual(other.ConfigDictionary) - && ComboColours.SequenceEqual(other.ComboColours) - && CustomColours?.SequenceEqual(other.CustomColours) == true; + public bool Equals(SkinConfiguration other) => other != null + && ConfigDictionary.SequenceEqual(other.ConfigDictionary) + && ComboColours.SequenceEqual(other.ComboColours) + && CustomColours?.SequenceEqual(other.CustomColours) == true; } } From 8f9e090f4c02549c8ed613e8c70785eed38d17be Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 23 Aug 2020 15:39:48 +0200 Subject: [PATCH 2821/6909] Remove Indent --- osu.Game/Skinning/SkinConfiguration.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index 27589577e6..2ac4dfa0c8 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -51,9 +51,6 @@ namespace osu.Game.Skinning public readonly SortedDictionary ConfigDictionary = new SortedDictionary(); - public bool Equals(SkinConfiguration other) => other != null - && ConfigDictionary.SequenceEqual(other.ConfigDictionary) - && ComboColours.SequenceEqual(other.ComboColours) - && CustomColours?.SequenceEqual(other.CustomColours) == true; + public bool Equals(SkinConfiguration other) => other != null && ConfigDictionary.SequenceEqual(other.ConfigDictionary) && ComboColours.SequenceEqual(other.ComboColours) && CustomColours?.SequenceEqual(other.CustomColours) == true; } } From 2dce850f5b61b248285d8df4b10ead3e7167948d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 23 Aug 2020 23:11:56 +0900 Subject: [PATCH 2822/6909] Rewrite hyperdash test to not rely on timing --- .../TestSceneHyperDash.cs | 39 +++++++++++++------ 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 6dab2a0b56..514d2aae22 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -22,21 +22,38 @@ namespace osu.Game.Rulesets.Catch.Tests [Test] public void TestHyperDash() { - AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); - AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast. - - AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0); - - for (int i = 0; i < 3; i++) + AddStep("reset count", () => { - AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); - AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); - } + inHyperDash = false; + hyperDashCount = 0; + }); - AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); + AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); + + for (int i = 0; i < 9; i++) + { + int count = i + 1; + AddUntilStep("wait for next hyperdash", () => hyperDashCount == count); + } } - private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; + private int hyperDashCount; + private bool inHyperDash; + + protected override void Update() + { + var catcher = Player.ChildrenOfType().FirstOrDefault()?.MovableCatcher; + + if (catcher == null) + return; + + if (catcher.HyperDashing != inHyperDash) + { + inHyperDash = catcher.HyperDashing; + if (catcher.HyperDashing) + hyperDashCount++; + } + } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { From d274652b3a30ace18ab23d92c7d4064c2421fd5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Aug 2020 00:13:26 +0900 Subject: [PATCH 2823/6909] Fix failures if test ran too fast --- osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 514d2aae22..a12e4b69e3 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests for (int i = 0; i < 9; i++) { int count = i + 1; - AddUntilStep("wait for next hyperdash", () => hyperDashCount == count); + AddUntilStep("wait for next hyperdash", () => hyperDashCount >= count); } } From 12ca870b74e3883ccc3396e32e160d239f419193 Mon Sep 17 00:00:00 2001 From: "Orosfai I. Zsolt" Date: Sun, 23 Aug 2020 17:34:57 +0200 Subject: [PATCH 2824/6909] Fix osu!catch relax mod --- osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs index c1d24395e4..1e42c6a240 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods protected override bool OnMouseMove(MouseMoveEvent e) { - catcher.UpdatePosition(e.MousePosition.X / DrawSize.X); + catcher.UpdatePosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH); return base.OnMouseMove(e); } } From 68a043a0703202fc918e5879cc6eef296b14f7eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Aug 2020 18:00:06 +0200 Subject: [PATCH 2825/6909] Add test case covering regression --- .../Mods/TestSceneCatchModRelax.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs new file mode 100644 index 0000000000..80939d756e --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +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.Visual; + +namespace osu.Game.Rulesets.Catch.Tests.Mods +{ + public class TestSceneCatchModRelax : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + + [Test] + public void TestModRelax() => CreateModTest(new ModTestData + { + Mod = new CatchModRelax(), + Autoplay = false, + PassCondition = () => + { + var playfield = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre); + + return Player.ScoreProcessor.Combo.Value > 0; + }, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Fruit + { + X = CatchPlayfield.CENTER_X + } + } + } + }); + } +} From a8a7d9af297efdca7a6aecd414e1c8f16421cf16 Mon Sep 17 00:00:00 2001 From: "Orosfai I. Zsolt" Date: Sun, 23 Aug 2020 21:35:15 +0200 Subject: [PATCH 2826/6909] Add testcase to osu!catch relax mod --- .../Mods/TestSceneCatchModRelax.cs | 52 ++++++++++++++++--- 1 file changed, 44 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs index 80939d756e..385de0cea7 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -10,7 +10,9 @@ 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.Rulesets.Objects.Types; using osu.Game.Tests.Visual; +using osuTK; namespace osu.Game.Rulesets.Catch.Tests.Mods { @@ -23,23 +25,57 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods { Mod = new CatchModRelax(), Autoplay = false, - PassCondition = () => - { - var playfield = this.ChildrenOfType().Single(); - InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre); - - return Player.ScoreProcessor.Combo.Value > 0; - }, + PassCondition = passCondition, Beatmap = new Beatmap { HitObjects = new List { new Fruit { - X = CatchPlayfield.CENTER_X + X = CatchPlayfield.CENTER_X, + StartTime = 0 + }, + new Fruit + { + X = 0, + StartTime = 250 + }, + new Fruit + { + X = CatchPlayfield.WIDTH, + StartTime = 500 + }, + new JuiceStream + { + X = CatchPlayfield.CENTER_X, + StartTime = 750, + Path = new SliderPath(PathType.Linear, new Vector2[] { Vector2.Zero, Vector2.UnitY * 200 }) } } } }); + + private bool passCondition() + { + var playfield = this.ChildrenOfType().Single(); + + switch (Player.ScoreProcessor.Combo.Value) + { + case 0: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre); + break; + case 1: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomLeft); + break; + case 2: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomRight); + break; + case 3: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre); + break; + } + + return Player.ScoreProcessor.Combo.Value >= 6; + } } } From 3d68f30467c02390e50f5176f294d2a1053056d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 23 Aug 2020 21:52:50 +0200 Subject: [PATCH 2827/6909] Fix code style issues --- osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs index 385de0cea7..1eb0975010 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods { X = CatchPlayfield.CENTER_X, StartTime = 750, - Path = new SliderPath(PathType.Linear, new Vector2[] { Vector2.Zero, Vector2.UnitY * 200 }) + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) } } } @@ -64,12 +64,15 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods case 0: InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre); break; + case 1: InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomLeft); break; + case 2: InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomRight); break; + case 3: InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre); break; From c03cc754e3764bda4ae8b9394eedb846bfd48eef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Aug 2020 11:38:03 +0900 Subject: [PATCH 2828/6909] Move event attaching to ensure reporting is done at a high enough rate --- .../TestSceneHyperDash.cs | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index a12e4b69e3..db09b2bc6b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -19,6 +19,9 @@ namespace osu.Game.Rulesets.Catch.Tests { protected override bool Autoplay => true; + private int hyperDashCount; + private bool inHyperDash; + [Test] public void TestHyperDash() { @@ -26,6 +29,22 @@ namespace osu.Game.Rulesets.Catch.Tests { inHyperDash = false; hyperDashCount = 0; + + // this needs to be done within the frame stable context due to how quickly hyperdash state changes occur. + Player.DrawableRuleset.FrameStableComponents.OnUpdate += d => + { + var catcher = Player.ChildrenOfType().FirstOrDefault()?.MovableCatcher; + + if (catcher == null) + return; + + if (catcher.HyperDashing != inHyperDash) + { + inHyperDash = catcher.HyperDashing; + if (catcher.HyperDashing) + hyperDashCount++; + } + }; }); AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); @@ -33,25 +52,7 @@ namespace osu.Game.Rulesets.Catch.Tests for (int i = 0; i < 9; i++) { int count = i + 1; - AddUntilStep("wait for next hyperdash", () => hyperDashCount >= count); - } - } - - private int hyperDashCount; - private bool inHyperDash; - - protected override void Update() - { - var catcher = Player.ChildrenOfType().FirstOrDefault()?.MovableCatcher; - - if (catcher == null) - return; - - if (catcher.HyperDashing != inHyperDash) - { - inHyperDash = catcher.HyperDashing; - if (catcher.HyperDashing) - hyperDashCount++; + AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count); } } From dca307e93379e258c15b5423130014afb9f8f03a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Aug 2020 13:02:39 +0900 Subject: [PATCH 2829/6909] Use beatmap directly in ReadyButton --- osu.Game/Screens/Multi/Match/Components/ReadyButton.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs index 384d3bd5a5..a64f24dd7e 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs @@ -10,7 +10,6 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -using osu.Game.Overlays; namespace osu.Game.Screens.Multi.Match.Components { @@ -27,9 +26,6 @@ namespace osu.Game.Screens.Multi.Match.Components [Resolved] private BeatmapManager beatmaps { get; set; } - [Resolved] - private MusicController musicController { get; set; } - private bool hasBeatmap; public ReadyButton() @@ -104,7 +100,7 @@ namespace osu.Game.Screens.Multi.Match.Components return; } - bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(musicController.CurrentTrack.Length) < endDate.Value; + bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value; Enabled.Value = hasBeatmap && hasEnoughTime; } From d9ba677773fc3945c3e62d6f16a9bdf5ec2f9fa5 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 24 Aug 2020 15:08:50 +0200 Subject: [PATCH 2830/6909] Change TeamFlag from sprite to a container with a sprite --- .../Components/DrawableTeamFlag.cs | 20 +++++++++++++++++-- .../Components/DrawableTournamentTeam.cs | 11 ++-------- .../Ladder/Components/DrawableMatchTeam.cs | 2 +- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamFlag.cs b/osu.Game.Tournament/Components/DrawableTeamFlag.cs index 8c85c9a46f..a2e0bf83be 100644 --- a/osu.Game.Tournament/Components/DrawableTeamFlag.cs +++ b/osu.Game.Tournament/Components/DrawableTeamFlag.cs @@ -4,19 +4,24 @@ using JetBrains.Annotations; 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.Tournament.Models; +using osuTK; namespace osu.Game.Tournament.Components { - public class DrawableTeamFlag : Sprite + public class DrawableTeamFlag : Container { private readonly TournamentTeam team; [UsedImplicitly] private Bindable flag; + private Sprite flagSprite; + public DrawableTeamFlag(TournamentTeam team) { this.team = team; @@ -27,7 +32,18 @@ namespace osu.Game.Tournament.Components { if (team == null) return; - (flag = team.FlagName.GetBoundCopy()).BindValueChanged(acronym => Texture = textures.Get($@"Flags/{team.FlagName}"), true); + Size = new Vector2(70, 47); + Masking = true; + CornerRadius = 5; + Child = flagSprite = new Sprite + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill + }; + + (flag = team.FlagName.GetBoundCopy()).BindValueChanged(acronym => flagSprite.Texture = textures.Get($@"Flags/{team.FlagName}"), true); } } } diff --git a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs index f8aed26ce1..b9442a67f5 100644 --- a/osu.Game.Tournament/Components/DrawableTournamentTeam.cs +++ b/osu.Game.Tournament/Components/DrawableTournamentTeam.cs @@ -4,9 +4,7 @@ using JetBrains.Annotations; 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.Graphics; using osu.Game.Tournament.Models; @@ -17,7 +15,7 @@ namespace osu.Game.Tournament.Components { public readonly TournamentTeam Team; - protected readonly Sprite Flag; + protected readonly Container Flag; protected readonly TournamentSpriteText AcronymText; [UsedImplicitly] @@ -27,12 +25,7 @@ namespace osu.Game.Tournament.Components { Team = team; - Flag = new DrawableTeamFlag(team) - { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit - }; - + Flag = new DrawableTeamFlag(team); AcronymText = new TournamentSpriteText { Font = OsuFont.Torus.With(weight: FontWeight.Regular), diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index 15cb7e44cb..030ccb5cb3 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components this.losers = losers; Size = new Vector2(150, 40); - Flag.Scale = new Vector2(0.9f); + Flag.Scale = new Vector2(0.6f); Flag.Anchor = Flag.Origin = Anchor.CentreLeft; AcronymText.Anchor = AcronymText.Origin = Anchor.CentreLeft; From db45d9aa8a1f395c57f6e276feb62364845dab54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Aug 2020 22:11:04 +0900 Subject: [PATCH 2831/6909] Fix catch hyper dash colour defaults not being set correctly As the defaults were not set, if a skin happened to specify 0,0,0,0 it would be ignored due to the early returns in property setters. --- osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index bab3cb748b..4dcc533dac 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.UI private readonly Container hyperDashTrails; private readonly Container endGlowSprites; - private Color4 hyperDashTrailsColour; + private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; public Color4 HyperDashTrailsColour { @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.UI } } - private Color4 endGlowSpritesColour; + private Color4 endGlowSpritesColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; public Color4 EndGlowSpritesColour { From 500cb0ccf5c5f6f09a3874fe98731c41382b6a3c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 24 Aug 2020 22:36:37 +0900 Subject: [PATCH 2832/6909] Fix legacy hit target being layered incorrectly --- .../Skinning/LegacyColumnBackground.cs | 41 +++++++++++++++---- .../Skinning/LegacyHitTarget.cs | 5 --- .../Skinning/LegacyStageBackground.cs | 6 ++- .../Skinning/ManiaLegacySkinTransformer.cs | 9 ++-- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index f9286b5095..543ea23db8 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.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 osu.Framework.Graphics; @@ -21,15 +22,20 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly IBindable direction = new Bindable(); private readonly bool isLastColumn; - private Container borderLineContainer; + [CanBeNull] + private readonly LegacyStageBackground stageBackground; + + private Container hitTargetContainer; private Container lightContainer; private Sprite light; + private Drawable hitTarget; private float hitPosition; - public LegacyColumnBackground(bool isLastColumn) + public LegacyColumnBackground(bool isLastColumn, [CanBeNull] LegacyStageBackground stageBackground) { this.isLastColumn = isLastColumn; + this.stageBackground = stageBackground; RelativeSizeAxes = Axes.Both; } @@ -47,6 +53,7 @@ namespace osu.Game.Rulesets.Mania.Skinning bool hasLeftLine = leftLineWidth > 0; bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m || isLastColumn; + bool hasHitTarget = Column.Index == 0 || stageBackground == null; hitPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; @@ -63,18 +70,29 @@ namespace osu.Game.Rulesets.Mania.Skinning Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value ?? Color4.White; - InternalChildren = new Drawable[] + Drawable background; + + InternalChildren = new[] { - new Box + background = new Box { RelativeSizeAxes = Axes.Both, Colour = backgroundColour }, - borderLineContainer = new Container + hitTargetContainer = new Container { RelativeSizeAxes = Axes.Both, Children = new[] { + // In legacy skins, the hit target takes on the full stage size and is sandwiched between the column background and the column light. + // To simulate this effect in lazer's hierarchy, the hit target is added to the first column's background and manually extended to the full size of the stage. + // Adding to the first columns allows depth issues to be resolved - if it were added to the last column, the previous column lights would appear below it. + // This still means that the hit target will appear below the next column backgrounds, but that's a much easier problem to solve by proxying the backgrounds below. + hitTarget = new LegacyHitTarget + { + RelativeSizeAxes = Axes.Y, + Alpha = hasHitTarget ? 1 : 0 + }, new Box { RelativeSizeAxes = Axes.Y, @@ -113,6 +131,9 @@ namespace osu.Game.Rulesets.Mania.Skinning } }; + // Resolve depth issues with the hit target appearing under the next column backgrounds by proxying to the stage background (always below the columns). + stageBackground?.AddColumnBackground(background.CreateProxy()); + direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); } @@ -124,17 +145,23 @@ namespace osu.Game.Rulesets.Mania.Skinning lightContainer.Anchor = Anchor.TopCentre; lightContainer.Scale = new Vector2(1, -1); - borderLineContainer.Padding = new MarginPadding { Top = hitPosition }; + hitTargetContainer.Padding = new MarginPadding { Top = hitPosition }; } else { lightContainer.Anchor = Anchor.BottomCentre; lightContainer.Scale = Vector2.One; - borderLineContainer.Padding = new MarginPadding { Bottom = hitPosition }; + hitTargetContainer.Padding = new MarginPadding { Bottom = hitPosition }; } } + protected override void Update() + { + base.Update(); + hitTarget.Width = stageBackground?.DrawWidth ?? DrawWidth; + } + public bool OnPressed(ManiaAction action) { if (action == Column.Action.Value) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index d055ef3480..1e1a9c2237 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Mania.Skinning private Container directionContainer; - public LegacyHitTarget() - { - RelativeSizeAxes = Axes.Both; - } - [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 7f5de601ca..3998f21c96 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { private Drawable leftSprite; private Drawable rightSprite; + private Container columnBackgroundContainer; public LegacyStageBackground() { @@ -44,7 +45,8 @@ namespace osu.Game.Rulesets.Mania.Skinning Origin = Anchor.TopLeft, X = -0.05f, Texture = skin.GetTexture(rightImage) - } + }, + columnBackgroundContainer = new Container { RelativeSizeAxes = Axes.Both } }; } @@ -58,5 +60,7 @@ namespace osu.Game.Rulesets.Mania.Skinning if (rightSprite?.Height > 0) rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height); } + + public void AddColumnBackground(Drawable background) => columnBackgroundContainer.Add(background); } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index e167135556..d928f1080f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -57,6 +57,8 @@ namespace osu.Game.Rulesets.Mania.Skinning /// private Lazy hasKeyTexture; + private LegacyStageBackground stageBackground; + public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap) : base(source) { @@ -88,10 +90,11 @@ namespace osu.Game.Rulesets.Mania.Skinning switch (maniaComponent.Component) { case ManiaSkinComponents.ColumnBackground: - return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1); + return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1, stageBackground); case ManiaSkinComponents.HitTarget: - return new LegacyHitTarget(); + // Created within the column background, but should not fall back. See comments in LegacyColumnBackground. + return Drawable.Empty(); case ManiaSkinComponents.KeyArea: return new LegacyKeyArea(); @@ -112,7 +115,7 @@ namespace osu.Game.Rulesets.Mania.Skinning return new LegacyHitExplosion(); case ManiaSkinComponents.StageBackground: - return new LegacyStageBackground(); + return stageBackground = new LegacyStageBackground(); case ManiaSkinComponents.StageForeground: return new LegacyStageForeground(); From 60695bee8c25ce8a40894382d338ee7a883e96f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Aug 2020 15:57:41 +0200 Subject: [PATCH 2833/6909] Remove fades when changing trail colour across skins --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 5 +++-- osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index a30e1b7b47..9289a6162c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -285,8 +285,6 @@ namespace osu.Game.Rulesets.Catch.UI private void runHyperDashStateTransition(bool hyperDashing) { - trails.HyperDashTrailsColour = hyperDashColour; - trails.EndGlowSpritesColour = hyperDashEndGlowColour; updateTrailVisibility(); if (hyperDashing) @@ -403,6 +401,9 @@ namespace osu.Game.Rulesets.Catch.UI skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ?? hyperDashColour; + trails.HyperDashTrailsColour = hyperDashColour; + trails.EndGlowSpritesColour = hyperDashEndGlowColour; + runHyperDashStateTransition(HyperDashing); } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index 4dcc533dac..f7e9fd19a7 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Catch.UI return; hyperDashTrailsColour = value; - hyperDashTrails.FadeColour(hyperDashTrailsColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + hyperDashTrails.Colour = hyperDashTrailsColour; } } @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.UI return; endGlowSpritesColour = value; - endGlowSprites.FadeColour(endGlowSpritesColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + endGlowSprites.Colour = endGlowSpritesColour; } } From 77bf646ea09823b123ea44993adf6da3f3e6b320 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 24 Aug 2020 23:01:06 +0900 Subject: [PATCH 2834/6909] Move column lines to background layer --- .../Skinning/LegacyColumnBackground.cs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 543ea23db8..f22903accf 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -70,30 +70,27 @@ namespace osu.Game.Rulesets.Mania.Skinning Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value ?? Color4.White; - Drawable background; + Container backgroundLayer; + Drawable leftLine; + Drawable rightLine; InternalChildren = new[] { - background = new Box + backgroundLayer = new Container { RelativeSizeAxes = Axes.Both, - Colour = backgroundColour + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, }, hitTargetContainer = new Container { RelativeSizeAxes = Axes.Both, Children = new[] { - // In legacy skins, the hit target takes on the full stage size and is sandwiched between the column background and the column light. - // To simulate this effect in lazer's hierarchy, the hit target is added to the first column's background and manually extended to the full size of the stage. - // Adding to the first columns allows depth issues to be resolved - if it were added to the last column, the previous column lights would appear below it. - // This still means that the hit target will appear below the next column backgrounds, but that's a much easier problem to solve by proxying the backgrounds below. - hitTarget = new LegacyHitTarget - { - RelativeSizeAxes = Axes.Y, - Alpha = hasHitTarget ? 1 : 0 - }, - new Box + leftLine = new Box { RelativeSizeAxes = Axes.Y, Width = leftLineWidth, @@ -101,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Colour = lineColour, Alpha = hasLeftLine ? 1 : 0 }, - new Box + rightLine = new Box { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, @@ -110,7 +107,16 @@ namespace osu.Game.Rulesets.Mania.Skinning Scale = new Vector2(0.740f, 1), Colour = lineColour, Alpha = hasRightLine ? 1 : 0 - } + }, + // In legacy skins, the hit target takes on the full stage size and is sandwiched between the column background and the column light. + // To simulate this effect in lazer's hierarchy, the hit target is added to the first column's background and manually extended to the full size of the stage. + // Adding to the first columns allows depth issues to be resolved - if it were added to the last column, the previous column lights would appear below it. + // This still means that the hit target will appear below the next column backgrounds, but that's a much easier problem to solve by proxying the background layer below. + hitTarget = new LegacyHitTarget + { + RelativeSizeAxes = Axes.Y, + Alpha = hasHitTarget ? 1 : 0 + }, } }, lightContainer = new Container @@ -132,7 +138,9 @@ namespace osu.Game.Rulesets.Mania.Skinning }; // Resolve depth issues with the hit target appearing under the next column backgrounds by proxying to the stage background (always below the columns). - stageBackground?.AddColumnBackground(background.CreateProxy()); + backgroundLayer.Add(leftLine.CreateProxy()); + backgroundLayer.Add(rightLine.CreateProxy()); + stageBackground?.AddColumnBackground(backgroundLayer.CreateProxy()); direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); From 018523a43a8c11243b6806fc4c4adf3384e24e6f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 25 Aug 2020 01:21:27 +0900 Subject: [PATCH 2835/6909] Rework to remove cross-class pollutions --- .../Beatmaps/StageDefinition.cs | 4 +- osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 11 +- .../Skinning/LegacyColumnBackground.cs | 96 +------------- .../Skinning/LegacyStageBackground.cs | 124 +++++++++++++++++- .../Skinning/ManiaLegacySkinTransformer.cs | 11 +- osu.Game.Rulesets.Mania/UI/Column.cs | 2 - osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 105 +++++++++++++++ osu.Game.Rulesets.Mania/UI/Stage.cs | 55 ++------ 8 files changed, 252 insertions(+), 156 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/ColumnFlow.cs diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs index 2557f2acdf..3052fc7d34 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs @@ -21,14 +21,14 @@ 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; + public readonly 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) + public readonly ColumnType GetTypeOfColumn(int column) { if (IsSpecialColumn(column)) return ColumnType.Special; diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index c0c8505f44..f078345fc1 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.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.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; @@ -14,15 +15,23 @@ namespace osu.Game.Rulesets.Mania /// public readonly int? TargetColumn; + /// + /// The intended for this component. + /// May be null if the component is not a direct member of a . + /// + public readonly StageDefinition? StageDefinition; + /// /// 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) + /// The intended for this component. May be null if the component is not a direct member of a . + public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null, StageDefinition? stageDefinition = null) : base(component) { TargetColumn = targetColumn; + StageDefinition = stageDefinition; } protected override string RulesetPrefix => ManiaRuleset.SHORT_NAME; diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index f22903accf..acae4cd6fb 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.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 JetBrains.Annotations; 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; @@ -20,22 +17,12 @@ namespace osu.Game.Rulesets.Mania.Skinning public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler { private readonly IBindable direction = new Bindable(); - private readonly bool isLastColumn; - [CanBeNull] - private readonly LegacyStageBackground stageBackground; - - private Container hitTargetContainer; private Container lightContainer; private Sprite light; - private Drawable hitTarget; - private float hitPosition; - - public LegacyColumnBackground(bool isLastColumn, [CanBeNull] LegacyStageBackground stageBackground) + public LegacyColumnBackground() { - this.isLastColumn = isLastColumn; - this.stageBackground = stageBackground; RelativeSizeAxes = Axes.Both; } @@ -45,80 +32,14 @@ namespace osu.Game.Rulesets.Mania.Skinning string lightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightImage)?.Value ?? "mania-stage-light"; - float leftLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth) - ?.Value ?? 1; - 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; - bool hasHitTarget = Column.Index == 0 || stageBackground == null; - - hitPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HitPosition)?.Value - ?? Stage.HIT_TARGET_POSITION; - float lightPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value ?? 0; - Color4 lineColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value - ?? Color4.White; - - Color4 backgroundColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value - ?? Color4.Black; - Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value ?? Color4.White; - Container backgroundLayer; - Drawable leftLine; - Drawable rightLine; - InternalChildren = new[] { - backgroundLayer = new Container - { - RelativeSizeAxes = Axes.Both, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - }, - hitTargetContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - leftLine = new Box - { - RelativeSizeAxes = Axes.Y, - Width = leftLineWidth, - Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasLeftLine ? 1 : 0 - }, - rightLine = new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = rightLineWidth, - Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasRightLine ? 1 : 0 - }, - // In legacy skins, the hit target takes on the full stage size and is sandwiched between the column background and the column light. - // To simulate this effect in lazer's hierarchy, the hit target is added to the first column's background and manually extended to the full size of the stage. - // Adding to the first columns allows depth issues to be resolved - if it were added to the last column, the previous column lights would appear below it. - // This still means that the hit target will appear below the next column backgrounds, but that's a much easier problem to solve by proxying the background layer below. - hitTarget = new LegacyHitTarget - { - RelativeSizeAxes = Axes.Y, - Alpha = hasHitTarget ? 1 : 0 - }, - } - }, lightContainer = new Container { Origin = Anchor.BottomCentre, @@ -137,11 +58,6 @@ namespace osu.Game.Rulesets.Mania.Skinning } }; - // Resolve depth issues with the hit target appearing under the next column backgrounds by proxying to the stage background (always below the columns). - backgroundLayer.Add(leftLine.CreateProxy()); - backgroundLayer.Add(rightLine.CreateProxy()); - stageBackground?.AddColumnBackground(backgroundLayer.CreateProxy()); - direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); } @@ -152,24 +68,14 @@ namespace osu.Game.Rulesets.Mania.Skinning { lightContainer.Anchor = Anchor.TopCentre; lightContainer.Scale = new Vector2(1, -1); - - hitTargetContainer.Padding = new MarginPadding { Top = hitPosition }; } else { lightContainer.Anchor = Anchor.BottomCentre; lightContainer.Scale = Vector2.One; - - hitTargetContainer.Padding = new MarginPadding { Bottom = hitPosition }; } } - protected override void Update() - { - base.Update(); - hitTarget.Width = stageBackground?.DrawWidth ?? DrawWidth; - } - public bool OnPressed(ManiaAction action) { if (action == Column.Action.Value) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 3998f21c96..675c154b82 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -2,22 +2,31 @@ // 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.Game.Rulesets.Mania.Beatmaps; +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 LegacyStageBackground : CompositeDrawable { + private readonly StageDefinition stageDefinition; + private Drawable leftSprite; private Drawable rightSprite; - private Container columnBackgroundContainer; + private ColumnFlow columnBackgrounds; - public LegacyStageBackground() + public LegacyStageBackground(StageDefinition stageDefinition) { + this.stageDefinition = stageDefinition; RelativeSizeAxes = Axes.Both; } @@ -46,8 +55,18 @@ namespace osu.Game.Rulesets.Mania.Skinning X = -0.05f, Texture = skin.GetTexture(rightImage) }, - columnBackgroundContainer = new Container { RelativeSizeAxes = Axes.Both } + columnBackgrounds = new ColumnFlow(stageDefinition) + { + RelativeSizeAxes = Axes.Y + }, + new HitTargetInsetContainer + { + Child = new LegacyHitTarget { RelativeSizeAxes = Axes.Both } + } }; + + for (int i = 0; i < stageDefinition.Columns; i++) + columnBackgrounds.SetColumn(i, new ColumnBackground(i, i == stageDefinition.Columns - 1)); } protected override void Update() @@ -61,6 +80,103 @@ namespace osu.Game.Rulesets.Mania.Skinning rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height); } - public void AddColumnBackground(Drawable background) => columnBackgroundContainer.Add(background); + private class ColumnBackground : CompositeDrawable + { + private readonly int columnIndex; + private readonly bool isLastColumn; + + public ColumnBackground(int columnIndex, bool isLastColumn) + { + this.columnIndex = columnIndex; + this.isLastColumn = isLastColumn; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + float leftLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftLineWidth, columnIndex)?.Value ?? 1; + float rightLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1; + + bool hasLeftLine = leftLineWidth > 0; + bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m + || isLastColumn; + + Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White; + Color4 backgroundColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, columnIndex)?.Value ?? Color4.Black; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, + }, + new HitTargetInsetContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Box + { + RelativeSizeAxes = Axes.Y, + Width = leftLineWidth, + Scale = new Vector2(0.740f, 1), + Colour = lineColour, + Alpha = hasLeftLine ? 1 : 0 + }, + new Box + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = rightLineWidth, + Scale = new Vector2(0.740f, 1), + Colour = lineColour, + Alpha = hasRightLine ? 1 : 0 + }, + } + } + }; + } + } + + private class HitTargetInsetContainer : Container + { + private readonly IBindable direction = new Bindable(); + + protected override Container Content => content; + private readonly Container content; + + private float hitPosition; + + public HitTargetInsetContainer() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + content.Padding = direction.NewValue == ScrollingDirection.Up + ? new MarginPadding { Top = hitPosition } + : new MarginPadding { Bottom = hitPosition }; + } + } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index d928f1080f..439e6f7df2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; using System.Collections.Generic; +using System.Diagnostics; using osu.Framework.Audio.Sample; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; @@ -57,8 +58,6 @@ namespace osu.Game.Rulesets.Mania.Skinning /// private Lazy hasKeyTexture; - private LegacyStageBackground stageBackground; - public ManiaLegacySkinTransformer(ISkinSource source, IBeatmap beatmap) : base(source) { @@ -90,10 +89,11 @@ namespace osu.Game.Rulesets.Mania.Skinning switch (maniaComponent.Component) { case ManiaSkinComponents.ColumnBackground: - return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1, stageBackground); + return new LegacyColumnBackground(); case ManiaSkinComponents.HitTarget: - // Created within the column background, but should not fall back. See comments in LegacyColumnBackground. + // Legacy skins sandwich the hit target between the column background and the column light. + // To preserve this ordering, it's created manually inside LegacyStageBackground. return Drawable.Empty(); case ManiaSkinComponents.KeyArea: @@ -115,7 +115,8 @@ namespace osu.Game.Rulesets.Mania.Skinning return new LegacyHitExplosion(); case ManiaSkinComponents.StageBackground: - return stageBackground = new LegacyStageBackground(); + Debug.Assert(maniaComponent.StageDefinition != null); + return new LegacyStageBackground(maniaComponent.StageDefinition.Value); case ManiaSkinComponents.StageForeground: return new LegacyStageForeground(); diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 255ce4c064..de4648e4fa 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -68,8 +68,6 @@ namespace osu.Game.Rulesets.Mania.UI TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); } - public override Axes RelativeSizeAxes => Axes.Y; - public ColumnType ColumnType { get; set; } public bool IsSpecial => ColumnType == ColumnType.Special; diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs new file mode 100644 index 0000000000..37ad5c609b --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Skinning; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.UI +{ + /// + /// A which flows its contents according to the s in a . + /// Content can be added to individual columns via . + /// + /// The type of content in each column. + public class ColumnFlow : CompositeDrawable + where TContent : Drawable + { + /// + /// All contents added to this . + /// + public IReadOnlyList Content => columns.Children.Select(c => c.Count == 0 ? null : (TContent)c.Child).ToList(); + + private readonly FillFlowContainer columns; + private readonly StageDefinition stageDefinition; + + public ColumnFlow(StageDefinition stageDefinition) + { + this.stageDefinition = stageDefinition; + + AutoSizeAxes = Axes.X; + + InternalChild = columns = new FillFlowContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + }; + + for (int i = 0; i < stageDefinition.Columns; i++) + columns.Add(new Container { RelativeSizeAxes = Axes.Y }); + } + + private ISkinSource currentSkin; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + currentSkin = skin; + + skin.SourceChanged += onSkinChanged; + onSkinChanged(); + } + + private void onSkinChanged() + { + for (int i = 0; i < stageDefinition.Columns; i++) + { + if (i > 0) + { + float spacing = currentSkin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1)) + ?.Value ?? Stage.COLUMN_SPACING; + + columns[i].Margin = new MarginPadding { Left = spacing }; + } + + float? width = currentSkin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i)) + ?.Value; + + if (width == null) + // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration) + columns[i].Width = stageDefinition.IsSpecialColumn(i) ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH; + else + columns[i].Width = width.Value; + } + } + + /// + /// Sets the content of one of the columns of this . + /// + /// The index of the column to set the content of. + /// The content. + public void SetColumn(int column, TContent content) => columns[column].Child = content; + + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (currentSkin != null) + currentSkin.SourceChanged -= onSkinChanged; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 36780b0f80..5944c8c218 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -11,7 +10,6 @@ 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; @@ -31,8 +29,8 @@ namespace osu.Game.Rulesets.Mania.UI public const float HIT_TARGET_POSITION = 110; - public IReadOnlyList Columns => columnFlow.Children; - private readonly FillFlowContainer columnFlow; + public IReadOnlyList Columns => columnFlow.Content; + private readonly ColumnFlow columnFlow; private readonly JudgementContainer judgements; private readonly DrawablePool judgementPool; @@ -73,16 +71,13 @@ namespace osu.Game.Rulesets.Mania.UI AutoSizeAxes = Axes.X, Children = new Drawable[] { - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: definition), _ => new DefaultStageBackground()) { RelativeSizeAxes = Axes.Both }, - columnFlow = new FillFlowContainer + columnFlow = new ColumnFlow(definition) { - Name = "Columns", RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING }, }, new Container @@ -102,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y, } }, - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: definition), _ => null) { RelativeSizeAxes = Axes.Both }, @@ -123,6 +118,8 @@ namespace osu.Game.Rulesets.Mania.UI var columnType = definition.GetTypeOfColumn(i); var column = new Column(firstColumnIndex + i) { + RelativeSizeAxes = Axes.Both, + Width = 1, ColumnType = columnType, AccentColour = columnColours[columnType], Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ } @@ -132,46 +129,10 @@ namespace osu.Game.Rulesets.Mania.UI } } - 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 ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1)) - ?.Value ?? COLUMN_SPACING; - - col.Margin = new MarginPadding { Left = spacing }; - } - - float? width = currentSkin.GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index)) - ?.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; - } - } - public void AddColumn(Column c) { topLevelContainer.Add(c.TopLevelContainer.CreateProxy()); - columnFlow.Add(c); + columnFlow.SetColumn(c.Index, c); AddNested(c); } From 50d5b020b7c579a7bea931d58267e793a90ce412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Aug 2020 20:19:33 +0200 Subject: [PATCH 2836/6909] Add failing test case --- .../TestSceneBeatmapSetOverlaySuccessRate.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs index 4cb22bf1fe..954eb74ad9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs @@ -7,8 +7,10 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapSet; using osu.Game.Screens.Select.Details; @@ -72,6 +74,20 @@ namespace osu.Game.Tests.Visual.Online }; } + [Test] + public void TestOnlyFailMetrics() + { + AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo + { + Metrics = new BeatmapMetrics + { + Fails = Enumerable.Range(1, 100).ToArray(), + } + }); + AddAssert("graph max values correct", + () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 100)); + } + private class GraphExposingSuccessRate : SuccessRate { public new FailRetryGraph Graph => base.Graph; From cc6ae8e3bd2a4ff21dee50dc7cd72f1663f32428 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Aug 2020 20:41:31 +0200 Subject: [PATCH 2837/6909] Fix crash if only one count list is received from API --- .../Screens/Select/Details/FailRetryGraph.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Details/FailRetryGraph.cs b/osu.Game/Screens/Select/Details/FailRetryGraph.cs index 134fd0598a..7cc80acfd3 100644 --- a/osu.Game/Screens/Select/Details/FailRetryGraph.cs +++ b/osu.Game/Screens/Select/Details/FailRetryGraph.cs @@ -29,16 +29,30 @@ namespace osu.Game.Screens.Select.Details var retries = Metrics?.Retries ?? Array.Empty(); var fails = Metrics?.Fails ?? Array.Empty(); + var retriesAndFails = sumRetriesAndFails(retries, fails); - float maxValue = fails.Any() ? fails.Zip(retries, (fail, retry) => fail + retry).Max() : 0; + float maxValue = retriesAndFails.Any() ? retriesAndFails.Max() : 0; failGraph.MaxValue = maxValue; retryGraph.MaxValue = maxValue; - failGraph.Values = fails.Select(f => (float)f); - retryGraph.Values = retries.Zip(fails, (retry, fail) => retry + Math.Clamp(fail, 0, maxValue)); + failGraph.Values = fails.Select(v => (float)v); + retryGraph.Values = retriesAndFails.Select(v => (float)v); } } + private int[] sumRetriesAndFails(int[] retries, int[] fails) + { + var result = new int[Math.Max(retries.Length, fails.Length)]; + + for (int i = 0; i < retries.Length; ++i) + result[i] = retries[i]; + + for (int i = 0; i < fails.Length; ++i) + result[i] += fails[i]; + + return result; + } + public FailRetryGraph() { Children = new[] From 29b4d98aaccdbd2b4440289a04cb8247492a2092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Aug 2020 20:41:50 +0200 Subject: [PATCH 2838/6909] Show retry/fail graph when either list is present --- osu.Game/Screens/Select/BeatmapDetails.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 9669a1391c..0ee52f3e48 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -236,7 +236,7 @@ namespace osu.Game.Screens.Select private void updateMetrics() { var hasRatings = beatmap?.BeatmapSet?.Metrics?.Ratings?.Any() ?? false; - var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) && (beatmap?.Metrics.Fails?.Any() ?? false); + var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) || (beatmap?.Metrics?.Fails?.Any() ?? false); if (hasRatings) { From dbf90551d65dda6b3bb8e03ad67bd75a124cd07b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 24 Aug 2020 20:47:29 +0200 Subject: [PATCH 2839/6909] Add coverage for empty metrics case --- .../Online/TestSceneBeatmapSetOverlaySuccessRate.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs index 954eb74ad9..fd5c188b94 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlaySuccessRate.cs @@ -88,6 +88,18 @@ namespace osu.Game.Tests.Visual.Online () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 100)); } + [Test] + public void TestEmptyMetrics() + { + AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo + { + Metrics = new BeatmapMetrics() + }); + + AddAssert("graph max values correct", + () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 0)); + } + private class GraphExposingSuccessRate : SuccessRate { public new FailRetryGraph Graph => base.Graph; From 723e5cafb6d1610c1857e7036a10d514db1c47eb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 25 Aug 2020 14:49:04 +0900 Subject: [PATCH 2840/6909] Fix column potentially added at wrong indices --- osu.Game.Rulesets.Mania/UI/Stage.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 5944c8c218..dfb1ee210d 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -36,7 +36,6 @@ namespace osu.Game.Rulesets.Mania.UI private readonly DrawablePool judgementPool; private readonly Drawable barLineContainer; - private readonly Container topLevelContainer; private readonly Dictionary columnColours = new Dictionary { @@ -60,6 +59,8 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; + Container topLevelContainer; + InternalChildren = new Drawable[] { judgementPool = new DrawablePool(2), @@ -116,6 +117,7 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < definition.Columns; i++) { var columnType = definition.GetTypeOfColumn(i); + var column = new Column(firstColumnIndex + i) { RelativeSizeAxes = Axes.Both, @@ -125,17 +127,12 @@ namespace osu.Game.Rulesets.Mania.UI Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ } }; - AddColumn(column); + topLevelContainer.Add(column.TopLevelContainer.CreateProxy()); + columnFlow.SetColumn(i, column); + AddNested(column); } } - public void AddColumn(Column c) - { - topLevelContainer.Add(c.TopLevelContainer.CreateProxy()); - columnFlow.SetColumn(c.Index, c); - AddNested(c); - } - public override void Add(DrawableHitObject h) { var maniaObject = (ManiaHitObject)h.HitObject; From 7e9567dae978a1030807a33f97d8355c64719ba1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 25 Aug 2020 14:49:29 +0900 Subject: [PATCH 2841/6909] Fix tests --- .../Skinning/TestSceneStageBackground.cs | 4 +++- .../Skinning/TestSceneStageForeground.cs | 3 ++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs index 87c84cf89c..a15fb392d6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Skinning; @@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [BackgroundDependencyLoader] private void load() { - SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) + SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }), + _ => new DefaultStageBackground()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs index 4e99068ed5..bceee1c599 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Tests.Skinning @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [BackgroundDependencyLoader] private void load() { - SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) + SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From ab8d9be095e4925d67cb1c06be49a2e92f24195f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 25 Aug 2020 15:16:41 +0900 Subject: [PATCH 2842/6909] Move out into a separate method --- .../Skinning/CatchLegacySkinTransformer.cs | 2 +- .../Skinning/LegacyFruitPiece.cs | 2 +- .../Skinning/LegacyColumnBackground.cs | 17 +++++-- .../Skinning/LegacyHitTarget.cs | 2 +- .../Skinning/LegacyMainCirclePiece.cs | 2 +- .../Skinning/LegacySliderBall.cs | 4 +- .../Skinning/LegacyCirclePiece.cs | 2 +- .../Skinning/LegacyDrumRoll.cs | 6 +-- osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 7 +-- .../Skinning/LegacyColourCompatibility.cs | 46 +++++++++++++++++++ osu.Game/Skinning/LegacySkinExtensions.cs | 31 ------------- 11 files changed, 73 insertions(+), 48 deletions(-) create mode 100644 osu.Game/Skinning/LegacyColourCompatibility.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 5abd87d6f4..ea2f031d65 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Skinning if (result == null) return null; - result.Value = result.Value.ToLegacyColour(); + result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value); return (IBindable)result; } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index c9dd1d1f3e..381d066750 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Skinning { base.LoadComplete(); - accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue.ToLegacyColour(), true); + accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index 54a16b840f..da6075248a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -58,14 +58,20 @@ namespace osu.Game.Rulesets.Mania.Skinning InternalChildren = new Drawable[] { - new Box { RelativeSizeAxes = Axes.Both }.WithLegacyColour(backgroundColour), + LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, backgroundColour), new Container { RelativeSizeAxes = Axes.Y, Width = leftLineWidth, Scale = new Vector2(0.740f, 1), Alpha = hasLeftLine ? 1 : 0, - Child = new Box { RelativeSizeAxes = Axes.Both }.WithLegacyColour(lineColour) + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) }, new Container { @@ -75,7 +81,10 @@ namespace osu.Game.Rulesets.Mania.Skinning Width = rightLineWidth, Scale = new Vector2(0.740f, 1), Alpha = hasRightLine ? 1 : 0, - Child = new Box { RelativeSizeAxes = Axes.Both }.WithLegacyColour(lineColour) + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) }, lightContainer = new Container { @@ -86,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Colour = lightColour.ToLegacyColour(), + Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour), Texture = skin.GetTexture(lightImage), RelativeSizeAxes = Axes.X, Width = 1, diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index 2177eaa5e6..48504e6548 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Anchor = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, Height = 1, - Colour = lineColour.ToLegacyColour(), + Colour = LegacyColourCompatibility.DisallowZeroAlpha(lineColour), Alpha = showJudgementLine ? 0.9f : 0 } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 8a6beddb51..d15a0a3203 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -106,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Skinning base.LoadComplete(); state.BindValueChanged(updateState, true); - accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue.ToLegacyColour(), true); + accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index 27dec1b691..25ab96445a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -39,11 +39,11 @@ namespace osu.Game.Rulesets.Osu.Skinning Texture = skin.GetTexture("sliderb-nd"), Colour = new Color4(5, 5, 5, 255), }, - animationContent.WithLegacyColour(ballColour).With(d => + LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d => { d.Anchor = Anchor.Centre; d.Origin = Anchor.Centre; - }), + }), ballColour), layerSpec = new Sprite { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs index ed69b529ed..9b73ccd248 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - backgroundLayer.Colour = accentColour.ToLegacyColour(); + backgroundLayer.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs index 6bb8f9433e..025eff53d5 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs @@ -76,9 +76,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - headCircle.AccentColour = accentColour.ToLegacyColour(); - body.Colour = accentColour.ToLegacyColour(); - end.Colour = accentColour.ToLegacyColour(); + headCircle.AccentColour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); + body.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); + end.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index f36aae205a..b11b64c22c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -19,9 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning [BackgroundDependencyLoader] private void load() { - AccentColour = (component == TaikoSkinComponents.CentreHit - ? new Color4(235, 69, 44, 255) - : new Color4(67, 142, 172, 255)).ToLegacyColour(); + AccentColour = LegacyColourCompatibility.DisallowZeroAlpha( + component == TaikoSkinComponents.CentreHit + ? new Color4(235, 69, 44, 255) + : new Color4(67, 142, 172, 255)); } } } diff --git a/osu.Game/Skinning/LegacyColourCompatibility.cs b/osu.Game/Skinning/LegacyColourCompatibility.cs new file mode 100644 index 0000000000..b842b50426 --- /dev/null +++ b/osu.Game/Skinning/LegacyColourCompatibility.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Skinning +{ + /// + /// Compatibility methods to convert osu!stable colours to osu!lazer-compatible ones. Should be used for legacy skins only. + /// + public static class LegacyColourCompatibility + { + /// + /// Forces an alpha of 1 if a given is fully transparent. + /// + /// + /// This is equivalent to setting colour post-constructor in osu!stable. + /// + /// The to disallow zero alpha on. + /// The resultant . + public static Color4 DisallowZeroAlpha(Color4 colour) + { + if (colour.A == 0) + colour.A = 1; + return colour; + } + + /// + /// Applies a to a , doubling the alpha value into the property. + /// + /// + /// This is equivalent to setting colour in the constructor in osu!stable. + /// + /// The to apply the colour to. + /// The to apply. + /// The given . + public static T ApplyWithDoubledAlpha(T drawable, Color4 colour) + where T : Drawable + { + drawable.Alpha = colour.A; + drawable.Colour = DisallowZeroAlpha(colour); + return drawable; + } + } +} diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 7420f82f04..bb46dc8b9f 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -using osuTK.Graphics; using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Skinning @@ -63,36 +62,6 @@ namespace osu.Game.Skinning } } - /// - /// The resultant colour after setting a post-constructor colour in osu!stable. - /// - /// The to convert. - /// The converted . - public static Color4 ToLegacyColour(this Color4 colour) - { - if (colour.A == 0) - colour.A = 1; - return colour; - } - - /// - /// Equivalent of setting a colour in the constructor in osu!stable. - /// Doubles the alpha channel into and uses to set . - /// - /// - /// Beware: Any existing value in is overwritten. - /// - /// The to set the colour of. - /// The to set. - /// The given . - public static T WithLegacyColour(this T drawable, Color4 colour) - where T : Drawable - { - drawable.Alpha = colour.A; - drawable.Colour = ToLegacyColour(colour); - return drawable; - } - public class SkinnableTextureAnimation : TextureAnimation { [Resolved(canBeNull: true)] From 7a70d063428890ee92e843001b31c08433ebe3fc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 25 Aug 2020 15:35:37 +0900 Subject: [PATCH 2843/6909] Add support for custom LightingN paths --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 1 + osu.Game/Skinning/LegacySkin.cs | 3 +++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index aebc229f7c..1e6102eaa4 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -116,6 +116,7 @@ namespace osu.Game.Skinning case string _ when pair.Key.StartsWith("KeyImage"): case string _ when pair.Key.StartsWith("Hit"): case string _ when pair.Key.StartsWith("Stage"): + case string _ when pair.Key.StartsWith("Lighting"): currentConfig.ImageLookups[pair.Key] = pair.Value; break; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 02d07eee45..10fb476728 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -173,6 +173,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ShowJudgementLine: return SkinUtils.As(new Bindable(existing.ShowJudgementLine)); + case LegacyManiaSkinConfigurationLookups.ExplosionImage: + return SkinUtils.As(getManiaImage(existing, "LightingN")); + case LegacyManiaSkinConfigurationLookups.ExplosionScale: Debug.Assert(maniaLookup.TargetColumn != null); From ff72ccabd8c15a98e597eb7464d6f5833cc122fd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 25 Aug 2020 18:44:32 +0900 Subject: [PATCH 2844/6909] Rename method --- osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs | 2 +- osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 4 ++-- osu.Game.Rulesets.Mania/UI/Stage.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 675c154b82..19ec86b1ed 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Mania.Skinning }; for (int i = 0; i < stageDefinition.Columns; i++) - columnBackgrounds.SetColumn(i, new ColumnBackground(i, i == stageDefinition.Columns - 1)); + columnBackgrounds.SetContentForColumn(i, new ColumnBackground(i, i == stageDefinition.Columns - 1)); } protected override void Update() diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs index 37ad5c609b..aef82d4c08 100644 --- a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.UI { /// /// A which flows its contents according to the s in a . - /// Content can be added to individual columns via . + /// Content can be added to individual columns via . /// /// The type of content in each column. public class ColumnFlow : CompositeDrawable @@ -86,7 +86,7 @@ namespace osu.Game.Rulesets.Mania.UI /// /// The index of the column to set the content of. /// The content. - public void SetColumn(int column, TContent content) => columns[column].Child = content; + public void SetContentForColumn(int column, TContent content) => columns[column].Child = content; public new MarginPadding Padding { diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index dfb1ee210d..f4b00ec476 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -128,7 +128,7 @@ namespace osu.Game.Rulesets.Mania.UI }; topLevelContainer.Add(column.TopLevelContainer.CreateProxy()); - columnFlow.SetColumn(i, column); + columnFlow.SetContentForColumn(i, column); AddNested(column); } } From 6c7475f085f1a06b238226d71f27e528b222fbf3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Aug 2020 18:56:15 +0900 Subject: [PATCH 2845/6909] Fix snapped distances potentially exceeding the source distance This results in slider placement including "excess" length, where the curve is not applied to the placed path. This is generally not what we want. I considered adding a bool parameter (or enum) to change the floor/rounding mode, but on further examination I think this is what we always expect from this function. --- .../TestSceneHitObjectComposerDistanceSnapping.cs | 14 +++++++------- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 8 +++++++- osu.Game/Rulesets/Edit/IPositionSnapProvider.cs | 1 + 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 168ec0f09d..bd34eaff63 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -169,17 +169,17 @@ namespace osu.Game.Tests.Editing [Test] public void GetSnappedDistanceFromDistance() { - assertSnappedDistance(50, 100); + assertSnappedDistance(50, 0); assertSnappedDistance(100, 100); - assertSnappedDistance(150, 200); + assertSnappedDistance(150, 100); assertSnappedDistance(200, 200); - assertSnappedDistance(250, 300); + assertSnappedDistance(250, 200); AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); assertSnappedDistance(50, 0); - assertSnappedDistance(100, 200); - assertSnappedDistance(150, 200); + assertSnappedDistance(100, 0); + assertSnappedDistance(150, 0); assertSnappedDistance(200, 200); assertSnappedDistance(250, 200); @@ -190,8 +190,8 @@ namespace osu.Game.Tests.Editing }); assertSnappedDistance(50, 0); - assertSnappedDistance(100, 200); - assertSnappedDistance(150, 200); + assertSnappedDistance(100, 0); + assertSnappedDistance(150, 0); assertSnappedDistance(200, 200); assertSnappedDistance(250, 200); assertSnappedDistance(400, 400); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c25fb03fd0..d0b06ce0ad 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -293,7 +293,13 @@ namespace osu.Game.Rulesets.Edit public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) { - var snappedEndTime = BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime); + double actualDuration = referenceTime + DistanceToDuration(referenceTime, distance); + + double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, referenceTime); + + // we don't want to exceed the actual duration and snap to a point in the future. + if (snappedEndTime > actualDuration) + snappedEndTime -= BeatSnapProvider.GetBeatLengthAtTime(referenceTime); return DurationToDistance(referenceTime, snappedEndTime - referenceTime); } diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index c854c06031..cce631464f 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -47,6 +47,7 @@ namespace osu.Game.Rulesets.Edit /// /// Converts an unsnapped distance to a snapped distance. + /// The returned distance will always be floored (as to never exceed the provided . /// /// The time of the timing point which resides in. /// The distance to convert. From c09cef4fca2d5264d96958cc388534012aa7ede0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 25 Aug 2020 19:39:03 +0900 Subject: [PATCH 2846/6909] Apply post-merge fixes to LegacyStageBackground --- .../Skinning/LegacyStageBackground.cs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 19ec86b1ed..16a6123724 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -108,37 +108,38 @@ namespace osu.Game.Rulesets.Mania.Skinning InternalChildren = new Drawable[] { - new Container + LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box { - RelativeSizeAxes = Axes.Both, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - }, + RelativeSizeAxes = Axes.Both + }, backgroundColour), new HitTargetInsetContainer { RelativeSizeAxes = Axes.Both, Children = new[] { - new Box + new Container { RelativeSizeAxes = Axes.Y, Width = leftLineWidth, Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasLeftLine ? 1 : 0 + Alpha = hasLeftLine ? 1 : 0, + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) }, - new Box + new Container { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.Y, Width = rightLineWidth, Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasRightLine ? 1 : 0 + Alpha = hasRightLine ? 1 : 0, + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) }, } } From 0800e4379689e8ac301ea8906f4a0e9ce7e518ce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 25 Aug 2020 19:57:49 +0900 Subject: [PATCH 2847/6909] Remove padding from columns --- osu.Game.Rulesets.Mania/UI/Stage.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index f4b00ec476..e7a2de266d 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -79,7 +79,6 @@ namespace osu.Game.Rulesets.Mania.UI columnFlow = new ColumnFlow(definition) { RelativeSizeAxes = Axes.Y, - Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING }, }, new Container { From 127330b8f9bb7ff7a9d03ad3db5044c002404f37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 25 Aug 2020 20:57:31 +0900 Subject: [PATCH 2848/6909] Add 1ms lenience to avoid potential precision issues --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index d0b06ce0ad..f134db1ffe 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -25,7 +25,7 @@ using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components; using osuTK; -using Key = osuTK.Input.Key; +using osuTK.Input; namespace osu.Game.Rulesets.Edit { @@ -297,9 +297,12 @@ namespace osu.Game.Rulesets.Edit double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, referenceTime); + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); + // we don't want to exceed the actual duration and snap to a point in the future. - if (snappedEndTime > actualDuration) - snappedEndTime -= BeatSnapProvider.GetBeatLengthAtTime(referenceTime); + // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. + if (snappedEndTime > actualDuration + 1) + snappedEndTime -= beatLength; return DurationToDistance(referenceTime, snappedEndTime - referenceTime); } From f09f882cc77f9264ad1292bc7df4c55ca3b548b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 25 Aug 2020 22:43:23 +0200 Subject: [PATCH 2849/6909] Add component for displaying simple statistics on result screen --- .../Ranking/TestSceneSimpleStatisticRow.cs | 68 ++++++++++ .../Ranking/Statistics/SimpleStatisticItem.cs | 80 ++++++++++++ .../Ranking/Statistics/SimpleStatisticRow.cs | 122 ++++++++++++++++++ 3 files changed, 270 insertions(+) create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs new file mode 100644 index 0000000000..aa569e47b1 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using Humanizer; +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.Utils; +using osu.Game.Screens.Ranking.Statistics; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneSimpleStatisticRow : OsuTestScene + { + private Container container; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new Container + { + AutoSizeAxes = Axes.Y, + Width = 700, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333"), + }, + container = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(20) + } + } + }; + }); + + [Test] + public void TestEmpty() + { + AddStep("create with no items", + () => container.Add(new SimpleStatisticRow(2, Enumerable.Empty()))); + } + + [Test] + public void TestManyItems( + [Values(1, 2, 3, 4, 12)] int itemCount, + [Values(1, 3, 5)] int columnCount) + { + AddStep($"create with {"item".ToQuantity(itemCount)}", () => + { + var items = Enumerable.Range(1, itemCount) + .Select(i => new SimpleStatisticItem($"Statistic #{i}") + { + Value = RNG.Next(100) + }); + + container.Add(new SimpleStatisticRow(columnCount, items)); + }); + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs new file mode 100644 index 0000000000..e6c4ab1c4e --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Represents a simple statistic item (one that only needs textual display). + /// Richer visualisations should be done with s. + /// + public abstract class SimpleStatisticItem : Container + { + /// + /// The text to display as the statistic's value. + /// + protected string Value + { + set => this.value.Text = value; + } + + private readonly OsuSpriteText value; + + /// + /// Creates a new simple statistic item. + /// + /// The name of the statistic. + protected SimpleStatisticItem(string name) + { + Name = name; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + AddRange(new[] + { + new OsuSpriteText + { + Text = Name, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + value = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.Torus.With(weight: FontWeight.Bold) + } + }); + } + } + + /// + /// Strongly-typed generic specialisation for . + /// + public class SimpleStatisticItem : SimpleStatisticItem + { + /// + /// The statistic's value to be displayed. + /// + public new TValue Value + { + set => base.Value = DisplayValue(value); + } + + /// + /// Used to convert to a text representation. + /// Defaults to using . + /// + protected virtual string DisplayValue(TValue value) => value.ToString(); + + public SimpleStatisticItem(string name) + : base(name) + { + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs new file mode 100644 index 0000000000..16501aae54 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs @@ -0,0 +1,122 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using 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; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Represents a statistic row with simple statistics (ones that only need textual display). + /// Richer visualisations should be done with s and s. + /// + public class SimpleStatisticRow : CompositeDrawable + { + private readonly SimpleStatisticItem[] items; + private readonly int columnCount; + + private FillFlowContainer[] columns; + + /// + /// Creates a statistic row for the supplied s. + /// + /// The number of columns to layout the into. + /// The s to display in this row. + public SimpleStatisticRow(int columnCount, IEnumerable items) + { + if (columnCount < 1) + throw new ArgumentOutOfRangeException(nameof(columnCount)); + + this.columnCount = columnCount; + this.items = items.ToArray(); + } + + [BackgroundDependencyLoader] + private void load() + { + columns = new FillFlowContainer[columnCount]; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + ColumnDimensions = createColumnDimensions().ToArray(), + Content = new[] { createColumns().ToArray() } + }; + + for (int i = 0; i < items.Length; ++i) + columns[i % columnCount].Add(items[i]); + } + + private IEnumerable createColumnDimensions() + { + for (int column = 0; column < columnCount; ++column) + { + if (column > 0) + yield return new Dimension(GridSizeMode.Absolute, 30); + + yield return new Dimension(); + } + } + + private IEnumerable createColumns() + { + for (int column = 0; column < columnCount; ++column) + { + if (column > 0) + { + yield return new Spacer + { + Alpha = items.Length > column ? 1 : 0 + }; + } + + yield return columns[column] = createColumn(); + } + } + + private FillFlowContainer createColumn() => new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical + }; + + private class Spacer : CompositeDrawable + { + public Spacer() + { + RelativeSizeAxes = Axes.Both; + Padding = new MarginPadding { Vertical = 4 }; + + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.Y, + Width = 3, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + CornerRadius = 2, + Masking = true, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#222") + } + }; + } + } + } +} From 2cf2ba8fc5cb0e1648756774d99e81b1f045c70b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 26 Aug 2020 14:24:04 +0900 Subject: [PATCH 2850/6909] Store computed accent colour to local --- osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs index 025eff53d5..5ab8e3a8c8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs @@ -76,9 +76,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - headCircle.AccentColour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); - body.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); - end.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); + var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); + + headCircle.AccentColour = colour; + body.Colour = colour; + end.Colour = colour; } } } From d057f5f4bce9297c9dc65d8eafc85cc585f604d6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 26 Aug 2020 15:37:16 +0900 Subject: [PATCH 2851/6909] Implement mania "KeysUnderNotes" skin config --- osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs | 3 +++ 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, 12 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs index 44f3e7d7b3..b269ea25d4 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs @@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Mania.Skinning direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); + + if (GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeysUnderNotes)?.Value ?? false) + Column.UnderlayElements.Add(CreateProxy()); } private void onDirectionChanged(ValueChangedEvent direction) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index af7d6007f3..a5cc899b53 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -35,6 +35,7 @@ namespace osu.Game.Skinning public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; + public bool KeysUnderNotes; public LegacyManiaSkinConfiguration(int keys) { diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index 4990ca8e60..890a0cc4ff 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -50,5 +50,6 @@ namespace osu.Game.Skinning Hit100, Hit50, Hit0, + KeysUnderNotes, } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index aebc229f7c..1ea120e8a4 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -97,6 +97,10 @@ namespace osu.Game.Skinning currentConfig.ShowJudgementLine = pair.Value == "1"; break; + case "KeysUnderNotes": + currentConfig.KeysUnderNotes = 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 02d07eee45..13a43c8aae 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -255,6 +255,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.Hit300: case LegacyManiaSkinConfigurationLookups.Hit300g: return SkinUtils.As(getManiaImage(existing, maniaLookup.Lookup.ToString())); + + case LegacyManiaSkinConfigurationLookups.KeysUnderNotes: + return SkinUtils.As(new Bindable(existing.KeysUnderNotes)); } return null; From c50e495e035bad956cf1883f510a41c2eb3e867a Mon Sep 17 00:00:00 2001 From: Poliwrath Date: Wed, 26 Aug 2020 02:49:55 -0400 Subject: [PATCH 2852/6909] fix lingering small ring in circles! intro --- osu.Game/Screens/Menu/IntroSequence.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Menu/IntroSequence.cs b/osu.Game/Screens/Menu/IntroSequence.cs index 6731fef6f7..98da31b93e 100644 --- a/osu.Game/Screens/Menu/IntroSequence.cs +++ b/osu.Game/Screens/Menu/IntroSequence.cs @@ -205,6 +205,7 @@ namespace osu.Game.Screens.Menu const int line_end_offset = 120; smallRing.Foreground.ResizeTo(1, line_duration, Easing.OutQuint); + smallRing.Delay(400).FadeOut(); lineTopLeft.MoveTo(new Vector2(-line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); lineTopRight.MoveTo(new Vector2(line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); From 97637bc747aaba00507f28f1c582e181fa7a7694 Mon Sep 17 00:00:00 2001 From: Poliwrath Date: Wed, 26 Aug 2020 01:59:53 -0400 Subject: [PATCH 2853/6909] remove new.ppy.sh from MessageFormatter --- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 6af2561c89..648e4a762b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -119,7 +119,7 @@ namespace osu.Game.Online.Chat case "http": case "https": // length > 3 since all these links need another argument to work - if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh")) + if (args.Length > 3 && args[1] == "osu.ppy.sh") { switch (args[2]) { From e6116890afbdcf93c7d032844b1670f9172a24ca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 26 Aug 2020 20:00:38 +0900 Subject: [PATCH 2854/6909] Make hitobject tests display the column --- .../Skinning/ColumnTestContainer.cs | 24 +++++++++++-------- .../Skinning/ManiaHitObjectTestScene.cs | 4 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs index ff4865c71d..8ba58e3af3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -22,18 +22,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Cached] private readonly Column column; - public ColumnTestContainer(int column, ManiaAction action) + public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false) { - this.column = new Column(column) + InternalChildren = new[] { - Action = { Value = action }, - AccentColour = Color4.Orange, - ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd - }; - - InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) - { - RelativeSizeAxes = Axes.Both + this.column = new Column(column) + { + Action = { Value = action }, + AccentColour = Color4.Orange, + ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd, + Alpha = showColumn ? 1 : 0 + }, + content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) + { + RelativeSizeAxes = Axes.Both + }, + this.column.TopLevelContainer.CreateProxy() }; } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index 18eebada00..d24c81dac6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ColumnTestContainer(0, ManiaAction.Key1) + new ColumnTestContainer(0, ManiaAction.Key1, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning })); }) }, - new ColumnTestContainer(1, ManiaAction.Key2) + new ColumnTestContainer(1, ManiaAction.Key2, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From c0c67c11b12d0df3b318c763f196f383f91f2f8c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 26 Aug 2020 20:21:41 +0900 Subject: [PATCH 2855/6909] Add parsing for hold note light/scale --- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 2 ++ .../Skinning/LegacyManiaSkinConfigurationLookup.cs | 2 ++ osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 ++++ osu.Game/Skinning/LegacySkin.cs | 14 ++++++++++++++ 4 files changed, 22 insertions(+) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index af7d6007f3..18ae6acb38 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -31,6 +31,7 @@ namespace osu.Game.Skinning public readonly float[] ColumnSpacing; public readonly float[] ColumnWidth; public readonly float[] ExplosionWidth; + public readonly float[] HoldNoteLightWidth; public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; @@ -44,6 +45,7 @@ namespace osu.Game.Skinning ColumnSpacing = new float[keys - 1]; ColumnWidth = new float[keys]; ExplosionWidth = new float[keys]; + HoldNoteLightWidth = 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 4990ca8e60..131c3fde34 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -34,6 +34,8 @@ namespace osu.Game.Skinning HoldNoteHeadImage, HoldNoteTailImage, HoldNoteBodyImage, + HoldNoteLightImage, + HoldNoteLightScale, ExplosionImage, ExplosionScale, ColumnLineColour, diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 1e6102eaa4..ca492b5a21 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -101,6 +101,10 @@ namespace osu.Game.Skinning parseArrayValue(pair.Value, currentConfig.ExplosionWidth); break; + case "LightingLWidth": + parseArrayValue(pair.Value, currentConfig.HoldNoteLightWidth); + break; + case "WidthForNoteHeightScale": float minWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; if (minWidth > 0) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 10fb476728..628169584a 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -220,6 +220,20 @@ namespace osu.Game.Skinning Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}L")); + case LegacyManiaSkinConfigurationLookups.HoldNoteLightImage: + return SkinUtils.As(getManiaImage(existing, "LightingL")); + + case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale: + Debug.Assert(maniaLookup.TargetColumn != null); + + if (GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value < 2.5m) + return SkinUtils.As(new Bindable(1)); + + if (existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] != 0) + return SkinUtils.As(new Bindable(existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + case LegacyManiaSkinConfigurationLookups.KeyImage: Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}")); From 9372c6eef6e90ff35d5bdd81d332dbded910416b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 26 Aug 2020 20:21:56 +0900 Subject: [PATCH 2856/6909] Implement hold note lighting --- .../Objects/Drawables/DrawableHoldNote.cs | 3 + .../Skinning/LegacyBodyPiece.cs | 124 +++++++++++++++--- 2 files changed, 112 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index a44f8a8886..4f29e0417e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -159,7 +159,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Tail.AllJudged) + { ApplyResult(r => r.Type = HitResult.Perfect); + endHold(); + } if (Tail.Result.Type == HitResult.Miss) HasBroken = true; diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 9f716428c0..d922934532 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; 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; @@ -19,7 +22,9 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly IBindable direction = new Bindable(); private readonly IBindable isHitting = new Bindable(); - private Drawable sprite; + private Drawable bodySprite; + private Drawable lightContainer; + private Drawable light; public LegacyBodyPiece() { @@ -32,7 +37,33 @@ namespace osu.Game.Rulesets.Mania.Skinning string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value ?? $"mania-note{FallbackColumnIndex}L"; - sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => + string lightImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightImage)?.Value + ?? "lightingL"; + + float lightScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value + ?? 1; + + // 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(lightImage, true, false); + double frameLength = 0; + if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) + frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); + + light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d => + { + if (d == null) + return; + + d.Origin = Anchor.Centre; + d.Blending = BlendingParameters.Additive; + d.Scale = new Vector2(lightScale); + }); + + if (light != null) + lightContainer = new HitTargetInsetContainer { Child = light }; + + bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => { if (d == null) return; @@ -47,8 +78,8 @@ namespace osu.Game.Rulesets.Mania.Skinning // Todo: Wrap }); - if (sprite != null) - InternalChild = sprite; + if (bodySprite != null) + InternalChild = bodySprite; direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); @@ -60,27 +91,90 @@ namespace osu.Game.Rulesets.Mania.Skinning private void onIsHittingChanged(ValueChangedEvent isHitting) { - if (!(sprite is TextureAnimation animation)) - return; + if (bodySprite is TextureAnimation bodyAnimation) + { + bodyAnimation.GotoFrame(0); + bodyAnimation.IsPlaying = isHitting.NewValue; + } - animation.GotoFrame(0); - animation.IsPlaying = isHitting.NewValue; + if (lightContainer != null) + { + if (isHitting.NewValue) + { + Column.TopLevelContainer.Add(lightContainer); + + // The light must be seeked only after being loaded, otherwise a nullref happens (https://github.com/ppy/osu-framework/issues/3847). + if (light is TextureAnimation lightAnimation) + lightAnimation.GotoFrame(0); + } + else + Column.TopLevelContainer.Remove(lightContainer); + } } private void onDirectionChanged(ValueChangedEvent direction) { - if (sprite == null) - return; - if (direction.NewValue == ScrollingDirection.Up) { - sprite.Origin = Anchor.BottomCentre; - sprite.Scale = new Vector2(1, -1); + if (bodySprite != null) + { + bodySprite.Origin = Anchor.BottomCentre; + bodySprite.Scale = new Vector2(1, -1); + } + + if (light != null) + light.Anchor = Anchor.TopCentre; } else { - sprite.Origin = Anchor.TopCentre; - sprite.Scale = Vector2.One; + if (bodySprite != null) + { + bodySprite.Origin = Anchor.TopCentre; + bodySprite.Scale = Vector2.One; + } + + if (light != null) + light.Anchor = Anchor.BottomCentre; + } + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + lightContainer?.Expire(); + } + + private class HitTargetInsetContainer : Container + { + private readonly IBindable direction = new Bindable(); + + protected override Container Content => content; + private readonly Container content; + + private float hitPosition; + + public HitTargetInsetContainer() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + content.Padding = direction.NewValue == ScrollingDirection.Up + ? new MarginPadding { Top = hitPosition } + : new MarginPadding { Bottom = hitPosition }; } } } From 6fe1279e9deb3b3bea1ec7c53b2695fd47d6eb30 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 26 Aug 2020 20:23:01 +0900 Subject: [PATCH 2857/6909] Re-use existing inset container --- .../Skinning/HitTargetInsetContainer.cs | 46 +++++++++++++++++++ .../Skinning/LegacyBodyPiece.cs | 35 -------------- .../Skinning/LegacyStageBackground.cs | 35 -------------- 3 files changed, 46 insertions(+), 70 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs new file mode 100644 index 0000000000..c8b05ed2f8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class HitTargetInsetContainer : Container + { + private readonly IBindable direction = new Bindable(); + + protected override Container Content => content; + private readonly Container content; + + private float hitPosition; + + public HitTargetInsetContainer() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + content.Padding = direction.NewValue == ScrollingDirection.Up + ? new MarginPadding { Top = hitPosition } + : new MarginPadding { Bottom = hitPosition }; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index d922934532..338dd5bb1d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -6,10 +6,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; 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; @@ -144,38 +142,5 @@ namespace osu.Game.Rulesets.Mania.Skinning lightContainer?.Expire(); } - - private class HitTargetInsetContainer : Container - { - private readonly IBindable direction = new Bindable(); - - protected override Container Content => content; - private readonly Container content; - - private float hitPosition; - - public HitTargetInsetContainer() - { - RelativeSizeAxes = Axes.Both; - - InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin, IScrollingInfo scrollingInfo) - { - hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; - - direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(onDirectionChanged, true); - } - - private void onDirectionChanged(ValueChangedEvent direction) - { - content.Padding = direction.NewValue == ScrollingDirection.Up - ? new MarginPadding { Top = hitPosition } - : new MarginPadding { Bottom = hitPosition }; - } - } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 19ec86b1ed..ead51d91d7 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -2,14 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -145,38 +143,5 @@ namespace osu.Game.Rulesets.Mania.Skinning }; } } - - private class HitTargetInsetContainer : Container - { - private readonly IBindable direction = new Bindable(); - - protected override Container Content => content; - private readonly Container content; - - private float hitPosition; - - public HitTargetInsetContainer() - { - RelativeSizeAxes = Axes.Both; - - InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; - } - - [BackgroundDependencyLoader] - private void load(ISkinSource skin, IScrollingInfo scrollingInfo) - { - hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; - - direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(onDirectionChanged, true); - } - - private void onDirectionChanged(ValueChangedEvent direction) - { - content.Padding = direction.NewValue == ScrollingDirection.Up - ? new MarginPadding { Top = hitPosition } - : new MarginPadding { Bottom = hitPosition }; - } - } } } From 157e1d89651a4866b1e413ee1f953eea5058e948 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 26 Aug 2020 20:46:12 +0900 Subject: [PATCH 2858/6909] Add fades --- .../Skinning/LegacyBodyPiece.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 338dd5bb1d..a1c2559386 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -59,7 +59,13 @@ namespace osu.Game.Rulesets.Mania.Skinning }); if (light != null) - lightContainer = new HitTargetInsetContainer { Child = light }; + { + lightContainer = new HitTargetInsetContainer + { + Alpha = 0, + Child = light + }; + } bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => { @@ -99,14 +105,24 @@ namespace osu.Game.Rulesets.Mania.Skinning { if (isHitting.NewValue) { - Column.TopLevelContainer.Add(lightContainer); + // Clear the fade out and, more importantly, the removal. + lightContainer.ClearTransforms(); - // The light must be seeked only after being loaded, otherwise a nullref happens (https://github.com/ppy/osu-framework/issues/3847). + // Only add the container if the removal has taken place. + if (lightContainer.Parent == null) + Column.TopLevelContainer.Add(lightContainer); + + // The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847). if (light is TextureAnimation lightAnimation) lightAnimation.GotoFrame(0); + + lightContainer.FadeIn(80); } else - Column.TopLevelContainer.Remove(lightContainer); + { + lightContainer.FadeOut(120) + .OnComplete(d => Column.TopLevelContainer.Remove(d)); + } } } From f65991f31fbe30fcfd55a30e921b890b0709715d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 26 Aug 2020 23:28:58 +0900 Subject: [PATCH 2859/6909] Revert some usages based on review feedback --- osu.Game/Graphics/Containers/BeatSyncedContainer.cs | 8 ++------ osu.Game/Overlays/Music/PlaylistOverlay.cs | 9 +++------ .../Edit/Compose/Components/Timeline/Timeline.cs | 4 ---- osu.Game/Screens/Menu/LogoVisualisation.cs | 12 ++++-------- 4 files changed, 9 insertions(+), 24 deletions(-) diff --git a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs index 69021e1634..1c9cdc174a 100644 --- a/osu.Game/Graphics/Containers/BeatSyncedContainer.cs +++ b/osu.Game/Graphics/Containers/BeatSyncedContainer.cs @@ -7,7 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Overlays; namespace osu.Game.Graphics.Containers { @@ -15,9 +14,6 @@ namespace osu.Game.Graphics.Containers { protected readonly IBindable Beatmap = new Bindable(); - [Resolved] - private MusicController musicController { get; set; } - private int lastBeat; private TimingControlPoint lastTimingPoint; @@ -58,9 +54,9 @@ namespace osu.Game.Graphics.Containers TimingControlPoint timingPoint = null; EffectControlPoint effectPoint = null; - if (musicController.TrackLoaded && Beatmap.Value.BeatmapLoaded) + if (Beatmap.Value.TrackLoaded && Beatmap.Value.BeatmapLoaded) { - track = musicController.CurrentTrack; + track = Beatmap.Value.Track; beatmap = Beatmap.Value.Beatmap; } diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 7471e31923..b45d84049f 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -30,9 +30,6 @@ namespace osu.Game.Overlays.Music [Resolved] private BeatmapManager beatmaps { get; set; } - [Resolved] - private MusicController musicController { get; set; } - private FilterControl filter; private Playlist list; @@ -85,7 +82,7 @@ namespace osu.Game.Overlays.Music if (toSelect != null) { beatmap.Value = beatmaps.GetWorkingBeatmap(toSelect); - musicController.CurrentTrack.Restart(); + beatmap.Value.Track.Restart(); } }; } @@ -119,12 +116,12 @@ namespace osu.Game.Overlays.Music { if (set.ID == (beatmap.Value?.BeatmapSetInfo?.ID ?? -1)) { - musicController.CurrentTrack.Seek(0); + beatmap.Value?.Track.Seek(0); return; } beatmap.Value = beatmaps.GetWorkingBeatmap(set.Beatmaps.First()); - musicController.CurrentTrack.Restart(); + beatmap.Value.Track.Restart(); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index c617950c64..8c0e35b80e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -11,7 +11,6 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Overlays; using osu.Game.Rulesets.Edit; using osuTK; @@ -27,9 +26,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private EditorClock editorClock { get; set; } - [Resolved] - private MusicController musicController { get; set; } - /// /// The timeline's scroll position in the last frame. /// diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 4d95ee9b7b..ebbb19636c 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -20,7 +20,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Utils; -using osu.Game.Overlays; namespace osu.Game.Screens.Menu { @@ -75,9 +74,6 @@ namespace osu.Game.Screens.Menu /// public float Magnitude { get; set; } = 1; - [Resolved] - private MusicController musicController { get; set; } - private readonly float[] frequencyAmplitudes = new float[256]; private IShader shader; @@ -107,15 +103,15 @@ namespace osu.Game.Screens.Menu private void updateAmplitudes() { - var effect = beatmap.Value.BeatmapLoaded && musicController.TrackLoaded - ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(musicController.CurrentTrack.CurrentTime) + var effect = beatmap.Value.BeatmapLoaded && beatmap.Value.TrackLoaded + ? beatmap.Value.Beatmap?.ControlPointInfo.EffectPointAt(beatmap.Value.Track.CurrentTime) : null; for (int i = 0; i < temporalAmplitudes.Length; i++) temporalAmplitudes[i] = 0; - if (musicController.TrackLoaded) - addAmplitudesFromSource(musicController.CurrentTrack); + if (beatmap.Value.TrackLoaded) + addAmplitudesFromSource(beatmap.Value.Track); foreach (var source in amplitudeSources) addAmplitudesFromSource(source); From fcf703864288ea6b974f21f8bf049cd254ab4036 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 27 Aug 2020 00:21:50 +0900 Subject: [PATCH 2860/6909] Fix a couple of missed cases --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs | 4 ++-- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index cd46e8c545..3d100e4b1c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("enable autoplay", () => autoplay = true); base.SetUpSteps(); - AddUntilStep("wait for track to start running", () => MusicController.IsPlaying); + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); double startTime = hitObjects[sliderIndex].StartTime; retrieveDrawableSlider(sliderIndex); @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("have autoplay", () => autoplay = true); base.SetUpSteps(); - AddUntilStep("wait for track to start running", () => MusicController.IsPlaying); + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); double startTime = hitObjects[sliderIndex].StartTime; retrieveDrawableSlider(sliderIndex); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 3c559765d4..f7909071ea 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Tests { base.SetUpSteps(); - AddUntilStep("wait for track to start running", () => MusicController.IsPlaying); + AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); AddStep("retrieve spinner", () => drawableSpinner = (DrawableSpinner)Player.DrawableRuleset.Playfield.AllHitObjects.First()); } From edc15c965cf6a29321cf84c1dcd1d7bb0fcdd17a Mon Sep 17 00:00:00 2001 From: Poliwrath Date: Wed, 26 Aug 2020 12:52:39 -0400 Subject: [PATCH 2861/6909] Update osu.Game/Screens/Menu/IntroSequence.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/Menu/IntroSequence.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/IntroSequence.cs b/osu.Game/Screens/Menu/IntroSequence.cs index 98da31b93e..d92d38da45 100644 --- a/osu.Game/Screens/Menu/IntroSequence.cs +++ b/osu.Game/Screens/Menu/IntroSequence.cs @@ -205,7 +205,7 @@ namespace osu.Game.Screens.Menu const int line_end_offset = 120; smallRing.Foreground.ResizeTo(1, line_duration, Easing.OutQuint); - smallRing.Delay(400).FadeOut(); + smallRing.Delay(400).FadeColour(Color4.Black, 300); lineTopLeft.MoveTo(new Vector2(-line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); lineTopRight.MoveTo(new Vector2(line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); From 927a2a3d2df98bb3c93e9c4b8d7af6f35e71636e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Aug 2020 19:19:42 +0200 Subject: [PATCH 2862/6909] Introduce IStatisticRow interface --- .../Ranking/Statistics/IStatisticRow.cs | 18 +++++++++++++ .../Ranking/Statistics/StatisticRow.cs | 26 +++++++++++++++++-- .../Ranking/Statistics/StatisticsPanel.cs | 24 +++-------------- 3 files changed, 45 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Screens/Ranking/Statistics/IStatisticRow.cs diff --git a/osu.Game/Screens/Ranking/Statistics/IStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/IStatisticRow.cs new file mode 100644 index 0000000000..67224041d5 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/IStatisticRow.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// A row of statistics to be displayed on the results screen. + /// + public interface IStatisticRow + { + /// + /// Creates the visual representation of this row. + /// + Drawable CreateDrawableStatisticRow(); + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs index e1ca9799a3..fff60cdcad 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs @@ -1,19 +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.Linq; using JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Ranking.Statistics { /// - /// A row of statistics to be displayed in the results screen. + /// A row of graphically detailed s to be displayed in the results screen. /// - public class StatisticRow + public class StatisticRow : IStatisticRow { /// /// The columns of this . /// [ItemNotNull] public StatisticItem[] Columns; + + public Drawable CreateDrawableStatisticRow() => new GridContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] + { + Columns?.Select(c => new StatisticContainer(c) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }).Cast().ToArray() + }, + ColumnDimensions = Enumerable.Range(0, Columns?.Length ?? 0) + .Select(i => Columns[i].Dimension ?? new Dimension()).ToArray(), + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }; } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 7f406331cd..fd62c9e7d9 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -96,27 +96,9 @@ namespace osu.Game.Screens.Ranking.Statistics Spacing = new Vector2(30, 15), }; - foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) - { - 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) - { - 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(), - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }); - } + rows.AddRange(newScore.Ruleset.CreateInstance() + .CreateStatisticsForScore(newScore, playableBeatmap) + .Select(row => row.CreateDrawableStatisticRow())); LoadComponentAsync(rows, d => { From bbb3d7522e3e606c94d3564b28b9ec21c3865a51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Aug 2020 19:24:12 +0200 Subject: [PATCH 2863/6909] Scope up return type to IStatisticRow --- 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 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2795868c97..8cc635c316 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -314,7 +314,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, IBeatmap playableBeatmap) => new[] + public override IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new IStatisticRow[] { new StatisticRow { diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index eaa5d8937a..298f1aec91 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -193,7 +193,7 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] + public override IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new IStatisticRow[] { new StatisticRow { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 2011842591..0125e0a3ad 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] + public override IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new IStatisticRow[] { new StatisticRow { diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 3a7f433a37..f82ecd842a 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -217,6 +217,6 @@ namespace osu.Game.Rulesets /// 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, IBeatmap playableBeatmap) => Array.Empty(); + public virtual IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty(); } } From f5e52c80b4db250bcb18df027a06095f35347508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Aug 2020 19:25:59 +0200 Subject: [PATCH 2864/6909] Rename {-> Drawable}SimpleStatisticRow --- ...atisticRow.cs => TestSceneDrawableSimpleStatisticRow.cs} | 6 +++--- ...{SimpleStatisticRow.cs => DrawableSimpleStatisticRow.cs} | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) rename osu.Game.Tests/Visual/Ranking/{TestSceneSimpleStatisticRow.cs => TestSceneDrawableSimpleStatisticRow.cs} (88%) rename osu.Game/Screens/Ranking/Statistics/{SimpleStatisticRow.cs => DrawableSimpleStatisticRow.cs} (96%) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs b/osu.Game.Tests/Visual/Ranking/TestSceneDrawableSimpleStatisticRow.cs similarity index 88% rename from osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs rename to osu.Game.Tests/Visual/Ranking/TestSceneDrawableSimpleStatisticRow.cs index aa569e47b1..2b0ba30357 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticRow.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneDrawableSimpleStatisticRow.cs @@ -13,7 +13,7 @@ using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Tests.Visual.Ranking { - public class TestSceneSimpleStatisticRow : OsuTestScene + public class TestSceneDrawableSimpleStatisticRow : OsuTestScene { private Container container; @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestEmpty() { AddStep("create with no items", - () => container.Add(new SimpleStatisticRow(2, Enumerable.Empty()))); + () => container.Add(new DrawableSimpleStatisticRow(2, Enumerable.Empty()))); } [Test] @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Ranking Value = RNG.Next(100) }); - container.Add(new SimpleStatisticRow(columnCount, items)); + container.Add(new DrawableSimpleStatisticRow(columnCount, items)); }); } } diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/DrawableSimpleStatisticRow.cs similarity index 96% rename from osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs rename to osu.Game/Screens/Ranking/Statistics/DrawableSimpleStatisticRow.cs index 16501aae54..a592724bc3 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/DrawableSimpleStatisticRow.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// Represents a statistic row with simple statistics (ones that only need textual display). /// Richer visualisations should be done with s and s. /// - public class SimpleStatisticRow : CompositeDrawable + public class DrawableSimpleStatisticRow : CompositeDrawable { private readonly SimpleStatisticItem[] items; private readonly int columnCount; @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The number of columns to layout the into. /// The s to display in this row. - public SimpleStatisticRow(int columnCount, IEnumerable items) + public DrawableSimpleStatisticRow(int columnCount, IEnumerable items) { if (columnCount < 1) throw new ArgumentOutOfRangeException(nameof(columnCount)); From 7c3368ecbe1ed8c6af1bd684f5af13027049b548 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Aug 2020 19:30:49 +0200 Subject: [PATCH 2865/6909] Reintroduce SimpleStatisticRow as a data class --- .../Statistics/DrawableSimpleStatisticRow.cs | 3 +- .../Ranking/Statistics/SimpleStatisticRow.cs | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs diff --git a/osu.Game/Screens/Ranking/Statistics/DrawableSimpleStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/DrawableSimpleStatisticRow.cs index a592724bc3..7f69b323d8 100644 --- a/osu.Game/Screens/Ranking/Statistics/DrawableSimpleStatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/DrawableSimpleStatisticRow.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -28,7 +29,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The number of columns to layout the into. /// The s to display in this row. - public DrawableSimpleStatisticRow(int columnCount, IEnumerable items) + public DrawableSimpleStatisticRow(int columnCount, [ItemNotNull] IEnumerable items) { if (columnCount < 1) throw new ArgumentOutOfRangeException(nameof(columnCount)); diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs new file mode 100644 index 0000000000..cd6afeb3a5 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.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 JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Contains textual statistic data to display in a . + /// + public class SimpleStatisticRow : IStatisticRow + { + /// + /// The number of columns to layout the in. + /// + public int Columns { get; set; } + + /// + /// The s that this row should contain. + /// + [ItemNotNull] + public SimpleStatisticItem[] Items { get; set; } + + public Drawable CreateDrawableStatisticRow() => new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(20), + Child = new DrawableSimpleStatisticRow(Columns, Items) + }; + } +} From 5973e2ce4e6d595a6f910b55250ed174a97db92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Aug 2020 21:20:43 +0200 Subject: [PATCH 2866/6909] Add component for unstable rate statistic --- .../Ranking/Statistics/UnstableRate.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 osu.Game/Screens/Ranking/Statistics/UnstableRate.cs diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs new file mode 100644 index 0000000000..5b368c3e8d --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Displays the unstable rate statistic for a given play. + /// + public class UnstableRate : SimpleStatisticItem + { + /// + /// Creates and computes an statistic. + /// + /// Sequence of s to calculate the unstable rate based on. + public UnstableRate(IEnumerable hitEvents) + : base("Unstable Rate") + { + var timeOffsets = hitEvents.Select(ev => ev.TimeOffset).ToArray(); + Value = 10 * standardDeviation(timeOffsets); + } + + private static double standardDeviation(double[] timeOffsets) + { + if (timeOffsets.Length == 0) + return double.NaN; + + var mean = timeOffsets.Average(); + var squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum(); + return Math.Sqrt(squares / timeOffsets.Length); + } + + protected override string DisplayValue(double value) => double.IsNaN(value) ? "(not available)" : value.ToString("N2"); + } +} From 05e725d59fd5b7710e18f2fb414eb51e02d99e6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Aug 2020 21:28:41 +0200 Subject: [PATCH 2867/6909] Add unstable rate statistic to rulesets in which it makes sense --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 8 ++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 50 ++++++++++++++++--------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 31 ++++++++++----- 3 files changed, 62 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 8cc635c316..490223b7a5 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -326,6 +326,14 @@ namespace osu.Game.Rulesets.Mania Height = 250 }), } + }, + new SimpleStatisticRow + { + Columns = 3, + Items = new SimpleStatisticItem[] + { + new UnstableRate(score.HitEvents) + } } }; } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 298f1aec91..dd950c60ec 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -193,30 +193,44 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new IStatisticRow[] + public override IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { - new StatisticRow + var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList(); + + return new IStatisticRow[] { - Columns = new[] + new StatisticRow { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), - } - }, - new StatisticRow - { - Columns = new[] + new StatisticItem("Timing Distribution", + new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow { - new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new SimpleStatisticRow + { + Columns = 3, + Items = new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + } } - } - }; + }; + } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 0125e0a3ad..938c038413 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -161,19 +161,32 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new IStatisticRow[] + public override IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { - new StatisticRow + var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList(); + + return new IStatisticRow[] { - Columns = new[] + new StatisticRow { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new SimpleStatisticRow + { + Columns = 3, + Items = new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + } } - } - }; + }; + } } } From d81d538b7e202362c8208f403c449bdc018cf443 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Aug 2020 21:29:01 +0200 Subject: [PATCH 2868/6909] Move out row anchor/origin set to one central place --- osu.Game/Screens/Ranking/Statistics/StatisticRow.cs | 2 -- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 6 +++++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs index fff60cdcad..d5324e14f0 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs @@ -21,8 +21,6 @@ namespace osu.Game.Screens.Ranking.Statistics public Drawable CreateDrawableStatisticRow() => new GridContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Content = new[] diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index fd62c9e7d9..2f3304e810 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -98,7 +98,11 @@ namespace osu.Game.Screens.Ranking.Statistics rows.AddRange(newScore.Ruleset.CreateInstance() .CreateStatisticsForScore(newScore, playableBeatmap) - .Select(row => row.CreateDrawableStatisticRow())); + .Select(row => row.CreateDrawableStatisticRow().With(r => + { + r.Anchor = Anchor.TopCentre; + r.Origin = Anchor.TopCentre; + }))); LoadComponentAsync(rows, d => { From c3197da3dac494617364c88899eef62b5d7f9bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Aug 2020 21:43:33 +0200 Subject: [PATCH 2869/6909] Adjust simple statistic item font sizes --- osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs index e6c4ab1c4e..3d9ba2f225 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs @@ -41,13 +41,14 @@ namespace osu.Game.Screens.Ranking.Statistics { Text = Name, Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14) }, value = new OsuSpriteText { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Font = OsuFont.Torus.With(weight: FontWeight.Bold) + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) } }); } From f8042e6fd311b4f6713e75c310de746d4ea5daf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 26 Aug 2020 22:34:02 +0200 Subject: [PATCH 2870/6909] Add fade to prevent jarring transitions --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 2f3304e810..3b8f980070 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -94,6 +94,7 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), + Alpha = 0 }; rows.AddRange(newScore.Ruleset.CreateInstance() @@ -111,6 +112,7 @@ namespace osu.Game.Screens.Ranking.Statistics spinner.Hide(); content.Add(d); + d.FadeIn(250, Easing.OutQuint); }, localCancellationSource.Token); }), localCancellationSource.Token); } From deb172bb6ccab9cc46f015957eb45bb06ce1c04f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Aug 2020 20:24:08 +0900 Subject: [PATCH 2871/6909] Implement basic mania hit order policy --- .../TestSceneOutOfOrderHits.cs | 124 ++++++++++++++++++ .../Objects/Drawables/DrawableHoldNote.cs | 3 + .../Drawables/DrawableManiaHitObject.cs | 9 ++ .../Objects/Drawables/DrawableNote.cs | 3 + osu.Game.Rulesets.Mania/UI/Column.cs | 10 ++ .../UI/OrderedHitPolicy.cs | 66 ++++++++++ 6 files changed, 215 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs create mode 100644 osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs new file mode 100644 index 0000000000..ed187e65bf --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.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 System.Linq; +using NUnit.Framework; +using osu.Framework.Extensions.TypeExtensions; +using osu.Framework.Screens; +using osu.Framework.Utils; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Replays; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.Replays; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Play; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests +{ + public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene + { + [Test] + public void TestPreviousHitWindowDoesNotExtendPastNextObject() + { + var objects = new List(); + var frames = new List(); + + for (int i = 0; i < 7; i++) + { + double time = 1000 + i * 100; + + objects.Add(new Note { StartTime = time }); + + if (i > 0) + { + frames.Add(new ManiaReplayFrame(time + 10, ManiaAction.Key1)); + frames.Add(new ManiaReplayFrame(time + 11)); + } + } + + performTest(objects, frames); + + addJudgementAssert(objects[0], HitResult.Miss); + + for (int i = 1; i < 7; i++) + { + addJudgementAssert(objects[i], HitResult.Perfect); + addJudgementOffsetAssert(objects[i], 10); + } + } + + private void addJudgementAssert(ManiaHitObject 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(ManiaHitObject hitObject, double offset) + { + 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 void performTest(List hitObjects, List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new ManiaBeatmap(new StageDefinition { Columns = 4 }) + { + HitObjects = hitObjects, + BeatmapInfo = + { + Ruleset = new ManiaRuleset().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); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private class ScoreAccessibleReplayPlayer : ReplayPlayer + { + public new ScoreProcessor ScoreProcessor => base.ScoreProcessor; + + protected override bool PauseOnFocusLost => false; + + public ScoreAccessibleReplayPlayer(Score score) + : base(score, false, false) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 0712026ca6..a04e5bc2f9 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -252,6 +252,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; + if (CheckHittable?.Invoke(this, Time.Current) == false) + return false; + // The tail has a lenience applied to it which is factored into the miss window (i.e. the miss judgement will be delayed). // But the hold cannot ever be started within the late-lenience window, so we should skip trying to begin the hold during that time. // Note: Unlike below, we use the tail's start time to determine the time offset. diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index ab76a5b8f8..0594d1e143 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -8,6 +9,7 @@ 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.Scoring; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -34,6 +36,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } + public Func CheckHittable; + protected DrawableManiaHitObject(ManiaHitObject hitObject) : base(hitObject) { @@ -124,6 +128,11 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables break; } } + + /// + /// Causes this to get missed, disregarding all conditions in implementations of . + /// + public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); } public abstract class DrawableManiaHitObject : DrawableManiaHitObject diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 9451bc4430..973dc06e05 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -64,6 +64,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; + if (CheckHittable?.Invoke(this, Time.Current) == false) + return false; + return UpdateResult(true); } diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index de4648e4fa..9aabcc6699 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; namespace osu.Game.Rulesets.Mania.UI { @@ -36,6 +37,7 @@ namespace osu.Game.Rulesets.Mania.UI public readonly ColumnHitObjectArea HitObjectArea; internal readonly Container TopLevelContainer; private readonly DrawablePool hitExplosionPool; + private readonly OrderedHitPolicy hitPolicy; public Container UnderlayElements => HitObjectArea.UnderlayElements; @@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.UI TopLevelContainer = new Container { RelativeSizeAxes = Axes.Both } }; + hitPolicy = new OrderedHitPolicy(HitObjectContainer); + TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); } @@ -90,6 +94,9 @@ namespace osu.Game.Rulesets.Mania.UI hitObject.AccentColour.Value = AccentColour; hitObject.OnNewResult += OnNewResult; + DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject; + maniaObject.CheckHittable = hitPolicy.IsHittable; + HitObjectContainer.Add(hitObject); } @@ -104,6 +111,9 @@ namespace osu.Game.Rulesets.Mania.UI internal void OnNewResult(DrawableHitObject judgedObject, JudgementResult result) { + if (result.IsHit) + hitPolicy.HandleHit(judgedObject); + if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs new file mode 100644 index 0000000000..68183be89f --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.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 osu.Framework.Extensions.IEnumerableExtensions; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class OrderedHitPolicy + { + private readonly HitObjectContainer hitObjectContainer; + + public OrderedHitPolicy(HitObjectContainer hitObjectContainer) + { + this.hitObjectContainer = hitObjectContainer; + } + + public bool IsHittable(DrawableHitObject hitObject, double time) + { + var nextObject = hitObjectContainer.AliveObjects.GetNext(hitObject); + return nextObject == null || time < nextObject.HitObject.StartTime; + } + + /// + /// Handles a being hit to potentially miss all earlier s. + /// + /// The that was hit. + public void HandleHit(DrawableHitObject hitObject) + { + if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) + throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); + + foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) + { + if (obj.Judged) + continue; + + ((DrawableManiaHitObject)obj).MissForcefully(); + } + } + + private IEnumerable enumerateHitObjectsUpTo(double targetTime) + { + foreach (var obj in hitObjectContainer.AliveObjects) + { + if (obj.HitObject.StartTime >= targetTime) + yield break; + + yield return obj; + + foreach (var nestedObj in obj.NestedHitObjects) + { + if (nestedObj.HitObject.StartTime >= targetTime) + break; + + yield return nestedObj; + } + } + } + } +} From 6f93df0b9dbd1317b072c61b124fb50b30017b41 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Aug 2020 21:05:12 +0900 Subject: [PATCH 2872/6909] Fix ticks causing hold note misses --- osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs index 68183be89f..dfd5136e3e 100644 --- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs @@ -48,14 +48,14 @@ namespace osu.Game.Rulesets.Mania.UI { foreach (var obj in hitObjectContainer.AliveObjects) { - if (obj.HitObject.StartTime >= targetTime) + if (obj.HitObject.GetEndTime() >= targetTime) yield break; yield return obj; foreach (var nestedObj in obj.NestedHitObjects) { - if (nestedObj.HitObject.StartTime >= targetTime) + if (nestedObj.HitObject.GetEndTime() >= targetTime) break; yield return nestedObj; From 7a5292936e57096e3521a1781d33d777fdc386d4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Aug 2020 21:15:05 +0900 Subject: [PATCH 2873/6909] Add some xmldocs --- osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs index dfd5136e3e..0f9cd48dd8 100644 --- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs @@ -11,6 +11,9 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.UI { + /// + /// Ensures that only the most recent is hittable, affectionately known as "note lock". + /// public class OrderedHitPolicy { private readonly HitObjectContainer hitObjectContainer; @@ -20,6 +23,15 @@ namespace osu.Game.Rulesets.Mania.UI this.hitObjectContainer = hitObjectContainer; } + /// + /// Determines whether a can be hit at a point in time. + /// + /// + /// Only the most recent can be hit, a previous hitobject's window cannot extend past the next one. + /// + /// The to check. + /// The time to check. + /// Whether can be hit at the given . public bool IsHittable(DrawableHitObject hitObject, double time) { var nextObject = hitObjectContainer.AliveObjects.GetNext(hitObject); From 29b29cde8e69330a1fdb62aec6ea802288242ec3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Aug 2020 23:09:54 +0900 Subject: [PATCH 2874/6909] Flip condition to reduce nesting --- .../Skinning/LegacyBodyPiece.cs | 36 +++++++++---------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index a1c2559386..f2e92a7258 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -101,28 +101,28 @@ namespace osu.Game.Rulesets.Mania.Skinning bodyAnimation.IsPlaying = isHitting.NewValue; } - if (lightContainer != null) + if (lightContainer == null) + return; + + if (isHitting.NewValue) { - if (isHitting.NewValue) - { - // Clear the fade out and, more importantly, the removal. - lightContainer.ClearTransforms(); + // Clear the fade out and, more importantly, the removal. + lightContainer.ClearTransforms(); - // Only add the container if the removal has taken place. - if (lightContainer.Parent == null) - Column.TopLevelContainer.Add(lightContainer); + // Only add the container if the removal has taken place. + if (lightContainer.Parent == null) + Column.TopLevelContainer.Add(lightContainer); - // The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847). - if (light is TextureAnimation lightAnimation) - lightAnimation.GotoFrame(0); + // The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847). + if (light is TextureAnimation lightAnimation) + lightAnimation.GotoFrame(0); - lightContainer.FadeIn(80); - } - else - { - lightContainer.FadeOut(120) - .OnComplete(d => Column.TopLevelContainer.Remove(d)); - } + lightContainer.FadeIn(80); + } + else + { + lightContainer.FadeOut(120) + .OnComplete(d => Column.TopLevelContainer.Remove(d)); } } From 700219316519201aa90ec2327091244553b6e250 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 27 Aug 2020 23:16:54 +0900 Subject: [PATCH 2875/6909] Mark nullable members --- osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index f2e92a7258..c0f0fcb4af 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.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.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -20,8 +21,13 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly IBindable direction = new Bindable(); private readonly IBindable isHitting = new Bindable(); + [CanBeNull] private Drawable bodySprite; + + [CanBeNull] private Drawable lightContainer; + + [CanBeNull] private Drawable light; public LegacyBodyPiece() From 9d70b4af0922a20a1877df82bd745896d8b72132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Aug 2020 17:35:42 +0200 Subject: [PATCH 2876/6909] Add failing test case --- .../Formats/LegacyScoreDecoderTest.cs | 66 ++++++++++++++++++ .../Resources/Replays/mania-replay.osr | Bin 0 -> 1012 bytes 2 files changed, 66 insertions(+) create mode 100644 osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs create mode 100644 osu.Game.Tests/Resources/Replays/mania-replay.osr diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs new file mode 100644 index 0000000000..31c367aad1 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko; +using osu.Game.Scoring.Legacy; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + [TestFixture] + public class LegacyScoreDecoderTest + { + [Test] + public void TestDecodeManiaReplay() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.AreEqual(3, score.ScoreInfo.Ruleset.ID); + + Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]); + Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]); + + Assert.AreEqual(829_931, score.ScoreInfo.TotalScore); + Assert.AreEqual(3, score.ScoreInfo.MaxCombo); + + Assert.That(score.Replay.Frames, Is.Not.Empty); + } + } + + private class TestLegacyScoreDecoder : LegacyScoreDecoder + { + private static readonly Dictionary rulesets = new Ruleset[] + { + new OsuRuleset(), + new TaikoRuleset(), + new CatchRuleset(), + new ManiaRuleset() + }.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID); + + protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId]; + + protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + MD5Hash = md5Hash, + Ruleset = new OsuRuleset().RulesetInfo, + BaseDifficulty = new BeatmapDifficulty() + } + }); + } + } +} diff --git a/osu.Game.Tests/Resources/Replays/mania-replay.osr b/osu.Game.Tests/Resources/Replays/mania-replay.osr new file mode 100644 index 0000000000000000000000000000000000000000..da1a7bdd28e5b89eddd9742bce2b27bc8e75e6f5 GIT binary patch literal 1012 zcmV2Fk>_^VL323Gh;F`G&3?}Wi$c+0000000031008T$3;+WF00000 z01FT?FgZ6ed@(FBI50Uld@(FxP`z80O4tZ&0{{SB001BWz5CvGFroXr>*+lrR^_@@;>&b?uOx<^mMIMT(+z#h{kY?X9fc(g(Lwebtx# z7Q6Lh=|qk7uqqmlbl#mwF~34O;`FY?03m(j9XrY3L)0)1?Fw}$4J_CNJKO;4?X$~q z;+wGa#wD!!0q2PlCzFrV3*3>Lz{D|fez@9JR^@L#NXJJJ;9{+KP~}bh@_eim%+NSk zzegnGWya)SS7pW`<9yI$g`b0t!?MZslqBX+4bVy#tZV0pDNGmeSXKuOXGQ|f3zg8y zGrMu+gxo2?L-AA5RBaNH&*&Dv9Dc!%ufdTEO? zp@}%SBej)D4N{g#xwHF)8|Ks=@OJ2^BGnjH?@$dDOeELCl()%CSSocJ#J0 zrMz`0akC4=AW51K>p71-r!saF*3AP)S9{~98w6pYSsUr1=*o_Q#S)6A-dHA@d9~(^0+ax|D@!;n;Z|| zFvVlqvtKZJf2x8EVoS#`3YPzS-*vaun`i1N*BG`MvK_+XgTPh^GgHpdy!e==KZIaT z3!YC}(8nGj*i;imyixwy9zyF`AooHY90sBzR$Obzb-Vi~&7$v#i3~s7*O{}|SLmeL zoMGZCnRwCc_XbwVIh5g#M@wt{FGra$aqhxs9vn18+0v`>fSkg3XKs=0k_AUDZ~8>a zDG78!sD4dHOSBcg!q83SKA#^J)eG3=oY`jU3QIF2QVgemNl8@7S4(W%aYN@e0jDu% zmqiUorSh0*IbBRsW~4cH;@gJJOutPU2TVP<=OEnHm=mZ{2D~6Z=U*j5r!6K@YHyj* zwhC8cbzc~TkcHU?&2W;Km_To^u4o{p7%^`#txPglTyDDFl%vh Date: Thu, 27 Aug 2020 17:40:22 +0200 Subject: [PATCH 2877/6909] Fix some legacy mania replays crashing on import --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a4a560c8e4..a3469f0965 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -241,12 +241,15 @@ namespace osu.Game.Scoring.Legacy } var diff = Parsing.ParseFloat(split[0]); + var mouseX = Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE); + var mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE); lastTime += diff; - if (i == 0 && diff == 0) - // osu-stable adds a zero-time frame before potentially valid negative user frames. - // we need to ignore this. + if (i < 2 && mouseX == 256 && mouseY == -500) + // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. + // both frames use a position of (256, -500). + // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) continue; // Todo: At some point we probably want to rewind and play back the negative-time frames @@ -255,8 +258,8 @@ namespace osu.Game.Scoring.Legacy continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, - Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE), - Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE), + mouseX, + mouseY, (ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame); replay.Frames.Add(currentFrame); From 37387d774165340e14c1e1ea493e23bb3aea693a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Aug 2020 17:57:55 +0200 Subject: [PATCH 2878/6909] Add assertions to existing test to cover bug --- osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs index 31c367aad1..9c71466489 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -4,6 +4,7 @@ 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.Catch; @@ -11,6 +12,7 @@ using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko; +using osu.Game.Scoring; using osu.Game.Scoring.Legacy; using osu.Game.Tests.Resources; @@ -35,6 +37,8 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(829_931, score.ScoreInfo.TotalScore); Assert.AreEqual(3, score.ScoreInfo.MaxCombo); + Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001)); + Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); Assert.That(score.Replay.Frames, Is.Not.Empty); } From af59e2c17954ce8fbf3df20ed2a0b6d3eeb74fa1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Aug 2020 18:05:06 +0200 Subject: [PATCH 2879/6909] Use extension methods instead of reading directly --- osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a3469f0965..97cb5ca7ab 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -13,7 +13,6 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; using osu.Game.Users; using SharpCompress.Compressors.LZMA; @@ -123,12 +122,12 @@ namespace osu.Game.Scoring.Legacy protected void CalculateAccuracy(ScoreInfo score) { - score.Statistics.TryGetValue(HitResult.Miss, out int countMiss); - score.Statistics.TryGetValue(HitResult.Meh, out int count50); - score.Statistics.TryGetValue(HitResult.Good, out int count100); - score.Statistics.TryGetValue(HitResult.Great, out int count300); - score.Statistics.TryGetValue(HitResult.Perfect, out int countGeki); - score.Statistics.TryGetValue(HitResult.Ok, out int countKatu); + int countMiss = score.GetCountMiss() ?? 0; + int count50 = score.GetCount50() ?? 0; + int count100 = score.GetCount100() ?? 0; + int count300 = score.GetCount300() ?? 0; + int countGeki = score.GetCountGeki() ?? 0; + int countKatu = score.GetCountKatu() ?? 0; switch (score.Ruleset.ID) { From f152e1b924f23da565ce0a89fb19ca3f2f16ac50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Aug 2020 20:07:30 +0200 Subject: [PATCH 2880/6909] Revert IStatisticRow changes --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 10 +------ osu.Game.Rulesets.Osu/OsuRuleset.cs | 12 ++------ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 12 ++------ osu.Game/Rulesets/Ruleset.cs | 2 +- .../Ranking/Statistics/IStatisticRow.cs | 18 ------------ .../Ranking/Statistics/SimpleStatisticRow.cs | 2 +- .../Ranking/Statistics/StatisticRow.cs | 24 ++-------------- .../Ranking/Statistics/StatisticsPanel.cs | 28 ++++++++++++++----- 8 files changed, 30 insertions(+), 78 deletions(-) delete mode 100644 osu.Game/Screens/Ranking/Statistics/IStatisticRow.cs diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 490223b7a5..bbfc5739ec 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -314,7 +314,7 @@ namespace osu.Game.Rulesets.Mania return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v); } - public override IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new IStatisticRow[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] { new StatisticRow { @@ -327,14 +327,6 @@ namespace osu.Game.Rulesets.Mania }), } }, - new SimpleStatisticRow - { - Columns = 3, - Items = new SimpleStatisticItem[] - { - new UnstableRate(score.HitEvents) - } - } }; } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index dd950c60ec..14e7b9e9a4 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -193,11 +193,11 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList(); - return new IStatisticRow[] + return new[] { new StatisticRow { @@ -222,14 +222,6 @@ namespace osu.Game.Rulesets.Osu }), } }, - new SimpleStatisticRow - { - Columns = 3, - Items = new SimpleStatisticItem[] - { - new UnstableRate(timedHitEvents) - } - } }; } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 938c038413..367d991677 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -161,11 +161,11 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList(); - return new IStatisticRow[] + return new[] { new StatisticRow { @@ -178,14 +178,6 @@ namespace osu.Game.Rulesets.Taiko }), } }, - new SimpleStatisticRow - { - Columns = 3, - Items = new SimpleStatisticItem[] - { - new UnstableRate(timedHitEvents) - } - } }; } } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index f82ecd842a..3a7f433a37 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -217,6 +217,6 @@ namespace osu.Game.Rulesets /// The , converted for this with all relevant s applied. /// The s to display. Each may contain 0 or more . [NotNull] - public virtual IStatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty(); + public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty(); } } diff --git a/osu.Game/Screens/Ranking/Statistics/IStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/IStatisticRow.cs deleted file mode 100644 index 67224041d5..0000000000 --- a/osu.Game/Screens/Ranking/Statistics/IStatisticRow.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; - -namespace osu.Game.Screens.Ranking.Statistics -{ - /// - /// A row of statistics to be displayed on the results screen. - /// - public interface IStatisticRow - { - /// - /// Creates the visual representation of this row. - /// - Drawable CreateDrawableStatisticRow(); - } -} diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs index cd6afeb3a5..5c0cb5b116 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs @@ -10,7 +10,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Contains textual statistic data to display in a . /// - public class SimpleStatisticRow : IStatisticRow + public class SimpleStatisticRow { /// /// The number of columns to layout the in. diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs index d5324e14f0..e1ca9799a3 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticRow.cs @@ -1,39 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using JetBrains.Annotations; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; namespace osu.Game.Screens.Ranking.Statistics { /// - /// A row of graphically detailed s to be displayed in the results screen. + /// A row of statistics to be displayed in the results screen. /// - public class StatisticRow : IStatisticRow + public class StatisticRow { /// /// The columns of this . /// [ItemNotNull] public StatisticItem[] Columns; - - public Drawable CreateDrawableStatisticRow() => new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] - { - Columns?.Select(c => new StatisticContainer(c) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }).Cast().ToArray() - }, - ColumnDimensions = Enumerable.Range(0, Columns?.Length ?? 0) - .Select(i => Columns[i].Dimension ?? new Dimension()).ToArray(), - RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } - }; } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 3b8f980070..128c6674e8 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -97,13 +97,27 @@ namespace osu.Game.Screens.Ranking.Statistics Alpha = 0 }; - rows.AddRange(newScore.Ruleset.CreateInstance() - .CreateStatisticsForScore(newScore, playableBeatmap) - .Select(row => row.CreateDrawableStatisticRow().With(r => - { - r.Anchor = Anchor.TopCentre; - r.Origin = Anchor.TopCentre; - }))); + foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) + { + 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) + { + 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(), + RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } + }); + } LoadComponentAsync(rows, d => { From ce013ac9b4fe80910f187b2856985b695b93cf88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Aug 2020 20:18:53 +0200 Subject: [PATCH 2881/6909] Make statistic header optional --- .../Ranking/Statistics/StatisticContainer.cs | 60 +++++++++++-------- .../Ranking/Statistics/StatisticItem.cs | 2 +- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index ed98698411..485d24d024 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -32,33 +32,9 @@ namespace osu.Game.Screens.Ranking.Statistics AutoSizeAxes = Axes.Y, Content = new[] { - new Drawable[] + new[] { - 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 = item.Name, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), - } - } - } + createHeader(item) }, new Drawable[] { @@ -78,5 +54,37 @@ namespace osu.Game.Screens.Ranking.Statistics } }; } + + private static Drawable createHeader(StatisticItem item) + { + if (string.IsNullOrEmpty(item.Name)) + return Empty(); + + return 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 = item.Name, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + } + } + }; + } } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index e959ed24fc..4903983759 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Creates a new , to be displayed inside a in the results screen. /// - /// The name of the item. + /// The name of the item. Can be to hide the item header. /// The content to be displayed. /// 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) From ea1f07e311add124437f0346fbb85300c88ab699 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Aug 2020 20:30:57 +0200 Subject: [PATCH 2882/6909] Simplify/rename SimpleStatisticRow mess --- ...ow.cs => TestSceneSimpleStatisticTable.cs} | 6 ++-- .../Ranking/Statistics/SimpleStatisticRow.cs | 34 ------------------- ...tatisticRow.cs => SimpleStatisticTable.cs} | 6 ++-- 3 files changed, 6 insertions(+), 40 deletions(-) rename osu.Game.Tests/Visual/Ranking/{TestSceneDrawableSimpleStatisticRow.cs => TestSceneSimpleStatisticTable.cs} (88%) delete mode 100644 osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs rename osu.Game/Screens/Ranking/Statistics/{DrawableSimpleStatisticRow.cs => SimpleStatisticTable.cs} (93%) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneDrawableSimpleStatisticRow.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs similarity index 88% rename from osu.Game.Tests/Visual/Ranking/TestSceneDrawableSimpleStatisticRow.cs rename to osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs index 2b0ba30357..07a0bcc8d8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneDrawableSimpleStatisticRow.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs @@ -13,7 +13,7 @@ using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Tests.Visual.Ranking { - public class TestSceneDrawableSimpleStatisticRow : OsuTestScene + public class TestSceneSimpleStatisticTable : OsuTestScene { private Container container; @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Visual.Ranking public void TestEmpty() { AddStep("create with no items", - () => container.Add(new DrawableSimpleStatisticRow(2, Enumerable.Empty()))); + () => container.Add(new SimpleStatisticTable(2, Enumerable.Empty()))); } [Test] @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Ranking Value = RNG.Next(100) }); - container.Add(new DrawableSimpleStatisticRow(columnCount, items)); + container.Add(new SimpleStatisticTable(columnCount, items)); }); } } diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs deleted file mode 100644 index 5c0cb5b116..0000000000 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticRow.cs +++ /dev/null @@ -1,34 +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.Graphics; -using osu.Framework.Graphics.Containers; - -namespace osu.Game.Screens.Ranking.Statistics -{ - /// - /// Contains textual statistic data to display in a . - /// - public class SimpleStatisticRow - { - /// - /// The number of columns to layout the in. - /// - public int Columns { get; set; } - - /// - /// The s that this row should contain. - /// - [ItemNotNull] - public SimpleStatisticItem[] Items { get; set; } - - public Drawable CreateDrawableStatisticRow() => new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding(20), - Child = new DrawableSimpleStatisticRow(Columns, Items) - }; - } -} diff --git a/osu.Game/Screens/Ranking/Statistics/DrawableSimpleStatisticRow.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs similarity index 93% rename from osu.Game/Screens/Ranking/Statistics/DrawableSimpleStatisticRow.cs rename to osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs index 7f69b323d8..8b503cc04e 100644 --- a/osu.Game/Screens/Ranking/Statistics/DrawableSimpleStatisticRow.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -14,10 +14,10 @@ using osu.Framework.Graphics.Shapes; namespace osu.Game.Screens.Ranking.Statistics { /// - /// Represents a statistic row with simple statistics (ones that only need textual display). + /// Represents a table with simple statistics (ones that only need textual display). /// Richer visualisations should be done with s and s. /// - public class DrawableSimpleStatisticRow : CompositeDrawable + public class SimpleStatisticTable : CompositeDrawable { private readonly SimpleStatisticItem[] items; private readonly int columnCount; @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// The number of columns to layout the into. /// The s to display in this row. - public DrawableSimpleStatisticRow(int columnCount, [ItemNotNull] IEnumerable items) + public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable items) { if (columnCount < 1) throw new ArgumentOutOfRangeException(nameof(columnCount)); From 43d6d2b2e8845d073f78fa491d4f9946217828c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Aug 2020 20:46:49 +0200 Subject: [PATCH 2883/6909] Add back unstable rate display --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 10 ++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 10 ++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 10 ++++++++++ 3 files changed, 30 insertions(+) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index bbfc5739ec..f7098faa5d 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -327,6 +327,16 @@ namespace osu.Game.Rulesets.Mania }), } }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(score.HitEvents) + })) + } + } }; } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 14e7b9e9a4..f527eb2312 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -222,6 +222,16 @@ namespace osu.Game.Rulesets.Osu }), } }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) + } + } }; } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 367d991677..dbc32f2c3e 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -178,6 +178,16 @@ namespace osu.Game.Rulesets.Taiko }), } }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) + } + } }; } } From 6846a245f42e60955ea20be79c7a2b43e334cbde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 27 Aug 2020 20:51:28 +0200 Subject: [PATCH 2884/6909] Reapply lost anchoring fix --- 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 128c6674e8..c2ace6a04e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -101,8 +101,8 @@ namespace osu.Game.Screens.Ranking.Statistics { rows.Add(new GridContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Content = new[] From 1c1afa1c962b99fe082d8f7e1dfc06336922ec7f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 19:16:20 +0900 Subject: [PATCH 2885/6909] Move MaxCombo to base DifficultyAttributes --- osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs | 1 - osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs | 1 - osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs | 1 - osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs | 1 + 4 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs index 75f5b18607..fa9011d826 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyAttributes.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Catch.Difficulty public class CatchDifficultyAttributes : DifficultyAttributes { public double ApproachRate; - public int MaxCombo; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 6e991a1d08..a9879013f8 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -11,6 +11,5 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double SpeedStrain; public double ApproachRate; public double OverallDifficulty; - public int MaxCombo; } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs index 75d3807bba..00ad956c8f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyAttributes.cs @@ -8,6 +8,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty public class TaikoDifficultyAttributes : DifficultyAttributes { public double GreatHitWindow; - public int MaxCombo; } } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs index b4b4bb9cd1..732dc772b7 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyAttributes.cs @@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Difficulty public Skill[] Skills; public double StarRating; + public int MaxCombo; public DifficultyAttributes() { From 85bda29b71a790cb7675d46b86dce2462deae3c7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 19:16:24 +0900 Subject: [PATCH 2886/6909] Add mania max combo attribute --- osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 37cba1fd3c..b08c520c54 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mania.Difficulty.Skills; using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; @@ -43,6 +44,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, + MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), Skills = skills }; } From 4d15f0fe520f188c9219aa7e716679967d4c5f49 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 19:16:46 +0900 Subject: [PATCH 2887/6909] Implement basic score recalculation --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 10 ++++--- .../Online/Leaderboards/LeaderboardScore.cs | 4 +-- osu.Game/OsuGameBase.cs | 9 +++--- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 5 +++- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 13 +++++--- osu.Game/Scoring/ScoreManager.cs | 30 ++++++++++++++++++- 6 files changed, 55 insertions(+), 16 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index b80b4e45ed..c02b6002d9 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -201,11 +201,11 @@ namespace osu.Game.Beatmaps var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); var attributes = calculator.Calculate(key.Mods); - return difficultyCache[key] = new StarDifficulty(attributes.StarRating); + return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo); } catch { - return difficultyCache[key] = new StarDifficulty(0); + return difficultyCache[key] = new StarDifficulty(); } } @@ -227,7 +227,7 @@ namespace osu.Game.Beatmaps if (beatmapInfo.ID == 0 || rulesetInfo.ID == null) { // If not, fall back to the existing star difficulty (e.g. from an online source). - existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty); + existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty, beatmapInfo.MaxCombo ?? 0); key = default; return true; @@ -292,10 +292,12 @@ namespace osu.Game.Beatmaps public readonly struct StarDifficulty { public readonly double Stars; + public readonly int MaxCombo; - public StarDifficulty(double stars) + public StarDifficulty(double stars, int maxCombo) { Stars = stars; + MaxCombo = maxCombo; // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) } diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 87b283f6b5..3c7ef73594 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -72,7 +72,7 @@ namespace osu.Game.Online.Leaderboards } [BackgroundDependencyLoader] - private void load(IAPIProvider api, OsuColour colour) + private void load(IAPIProvider api, OsuColour colour, ScoreManager scoreManager) { var user = score.User; @@ -194,7 +194,7 @@ namespace osu.Game.Online.Leaderboards { TextColour = Color4.White, GlowColour = Color4Extensions.FromHex(@"83ccfa"), - Text = score.TotalScore.ToString(@"N0"), + Text = scoreManager.GetTotalScore(score).ToString(@"N0"), Font = OsuFont.Numeric.With(size: 23), }, RankContainer = new Container diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 98f60d52d3..1d92315b1f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -57,6 +57,8 @@ namespace osu.Game protected ScoreManager ScoreManager; + protected BeatmapDifficultyManager DifficultyManager; + protected SkinManager SkinManager; protected RulesetStore RulesetStore; @@ -194,7 +196,7 @@ namespace osu.Game 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(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => DifficultyManager)); 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 @@ -218,9 +220,8 @@ namespace osu.Game ScoreManager.Undelete(getBeatmapScores(item), true); }); - var difficultyManager = new BeatmapDifficultyManager(); - dependencies.Cache(difficultyManager); - AddInternal(difficultyManager); + dependencies.Cache(DifficultyManager = new BeatmapDifficultyManager()); + AddInternal(DifficultyManager); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 097ca27bf7..a3500826a0 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -25,6 +25,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private const float row_height = 22; private const int text_size = 12; + [Resolved] + private ScoreManager scoreManager { get; set; } + private readonly FillFlowContainer backgroundFlow; private Color4 highAccuracyColour; @@ -121,7 +124,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores new OsuSpriteText { Margin = new MarginPadding { Right = horizontal_inset }, - Text = $@"{score.TotalScore:N0}", + Text = $@"{scoreManager.GetTotalScore(score):N0}", Font = OsuFont.GetFont(size: text_size, weight: index == 0 ? FontWeight.Bold : FontWeight.Medium) }, new OsuSpriteText diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index eac47aa089..057b1820f7 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -203,19 +203,24 @@ namespace osu.Game.Rulesets.Scoring } private double getScore(ScoringMode mode) + { + return GetScore(baseScore / maxBaseScore, HighestCombo.Value / maxHighestCombo, bonusScore, mode); + } + + public double GetScore(double accuracyRatio, double comboRatio, double bonus, ScoringMode mode) { switch (mode) { default: case ScoringMode.Standardised: - double accuracyScore = accuracyPortion * baseScore / maxBaseScore; - double comboScore = comboPortion * HighestCombo.Value / maxHighestCombo; + double accuracyScore = accuracyPortion * accuracyRatio; + double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; + return (max_score * (accuracyScore + comboScore) + bonus) * scoreMultiplier; 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 bonus + (accuracyRatio * maxBaseScore) * (1 + Math.Max(0, (comboRatio * maxHighestCombo) - 1) * scoreMultiplier / 25); } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index d5bd486e43..a82970d3df 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework.Logging; using osu.Framework.Platform; @@ -15,6 +16,7 @@ using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring.Legacy; namespace osu.Game.Scoring @@ -30,11 +32,16 @@ namespace osu.Game.Scoring private readonly RulesetStore rulesets; private readonly Func beatmaps; - public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null) + [CanBeNull] + private readonly Func difficulties; + + public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null, + Func difficulties = null) : base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost) { this.rulesets = rulesets; this.beatmaps = beatmaps; + this.difficulties = difficulties; } protected override ScoreInfo CreateModel(ArchiveReader archive) @@ -72,5 +79,26 @@ namespace osu.Game.Scoring protected override bool CheckLocalAvailability(ScoreInfo model, IQueryable items) => base.CheckLocalAvailability(model, items) || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); + + public long GetTotalScore(ScoreInfo score) + { + int? beatmapMaxCombo = score.Beatmap.MaxCombo; + + if (beatmapMaxCombo == null) + { + if (score.Beatmap.ID == 0 || difficulties == null) + return score.TotalScore; // Can't do anything. + + // We can compute the max combo locally. + beatmapMaxCombo = difficulties().GetDifficulty(score.Beatmap, score.Ruleset, score.Mods).MaxCombo; + } + + var ruleset = score.Ruleset.CreateInstance(); + var scoreProcessor = ruleset.CreateScoreProcessor(); + + scoreProcessor.Mods.Value = score.Mods; + + return (long)Math.Round(scoreProcessor.GetScore(score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo.Value, 0, ScoringMode.Standardised)); + } } } From 1e5e5cae0cbeee115aa09337051e22c943164893 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 21:34:34 +0900 Subject: [PATCH 2888/6909] Add support for standardised -> classic changes --- .../Graphics/Sprites/GlowingSpriteText.cs | 7 +++ .../Online/Leaderboards/LeaderboardScore.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 8 +-- osu.Game/Scoring/ScoreManager.cs | 60 +++++++++++++++---- 6 files changed, 62 insertions(+), 19 deletions(-) diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index 4aea5aa518..85df2d167f 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; @@ -55,6 +56,12 @@ namespace osu.Game.Graphics.Sprites set => spriteText.UseFullGlyphHeight = blurredText.UseFullGlyphHeight = value; } + public Bindable Current + { + get => spriteText.Current; + set => spriteText.Current = value; + } + public GlowingSpriteText() { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 3c7ef73594..24bb43f1b7 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -194,7 +194,7 @@ namespace osu.Game.Online.Leaderboards { TextColour = Color4.White, GlowColour = Color4Extensions.FromHex(@"83ccfa"), - Text = scoreManager.GetTotalScore(score).ToString(@"N0"), + Current = scoreManager.GetTotalScore(score), Font = OsuFont.Numeric.With(size: 23), }, RankContainer = new Container diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1d92315b1f..3839d4e734 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -196,7 +196,7 @@ namespace osu.Game 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, () => DifficultyManager)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => DifficultyManager, LocalConfig)); 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 diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index a3500826a0..832ac75882 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -124,7 +124,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores new OsuSpriteText { Margin = new MarginPadding { Right = horizontal_inset }, - Text = $@"{scoreManager.GetTotalScore(score):N0}", + Current = scoreManager.GetTotalScore(score), Font = OsuFont.GetFont(size: text_size, weight: index == 0 ? FontWeight.Bold : FontWeight.Medium) }, new OsuSpriteText diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 057b1820f7..7d138bd878 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -204,10 +204,10 @@ namespace osu.Game.Rulesets.Scoring private double getScore(ScoringMode mode) { - return GetScore(baseScore / maxBaseScore, HighestCombo.Value / maxHighestCombo, bonusScore, mode); + return GetScore(mode, maxBaseScore, maxHighestCombo, baseScore / maxBaseScore, HighestCombo.Value / maxHighestCombo, bonusScore); } - public double GetScore(double accuracyRatio, double comboRatio, double bonus, ScoringMode mode) + public double GetScore(ScoringMode mode, double maxBaseScore, double maxHighestCombo, double accuracyRatio, double comboRatio, double bonusScore) { switch (mode) { @@ -216,11 +216,11 @@ namespace osu.Game.Rulesets.Scoring double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + bonus) * scoreMultiplier; + return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; case ScoringMode.Classic: // should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1) - return bonus + (accuracyRatio * maxBaseScore) * (1 + Math.Max(0, (comboRatio * maxHighestCombo) - 1) * scoreMultiplier / 25); + return bonusScore + (accuracyRatio * maxBaseScore) * (1 + Math.Max(0, (comboRatio * maxHighestCombo) - 1) * scoreMultiplier / 25); } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index a82970d3df..134a41a7d4 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -8,9 +8,11 @@ using System.Linq; using System.Linq.Expressions; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; +using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Database; using osu.Game.IO.Archives; using osu.Game.Online.API; @@ -35,13 +37,17 @@ namespace osu.Game.Scoring [CanBeNull] private readonly Func difficulties; + [CanBeNull] + private readonly OsuConfigManager configManager; + public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null, - Func difficulties = null) + Func difficulties = null, OsuConfigManager configManager = null) : base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost) { this.rulesets = rulesets; this.beatmaps = beatmaps; this.difficulties = difficulties; + this.configManager = configManager; } protected override ScoreInfo CreateModel(ArchiveReader archive) @@ -80,25 +86,55 @@ namespace osu.Game.Scoring => base.CheckLocalAvailability(model, items) || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); - public long GetTotalScore(ScoreInfo score) + public Bindable GetTotalScore(ScoreInfo score) { - int? beatmapMaxCombo = score.Beatmap.MaxCombo; + var bindable = new TotalScoreBindable(score, difficulties); + configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode); + return bindable; + } - if (beatmapMaxCombo == null) + private class TotalScoreBindable : Bindable + { + public readonly Bindable ScoringMode = new Bindable(); + + private readonly ScoreInfo score; + private readonly Func difficulties; + + public TotalScoreBindable(ScoreInfo score, Func difficulties) { - if (score.Beatmap.ID == 0 || difficulties == null) - return score.TotalScore; // Can't do anything. + this.score = score; + this.difficulties = difficulties; - // We can compute the max combo locally. - beatmapMaxCombo = difficulties().GetDifficulty(score.Beatmap, score.Ruleset, score.Mods).MaxCombo; + ScoringMode.BindValueChanged(onScoringModeChanged, true); } - var ruleset = score.Ruleset.CreateInstance(); - var scoreProcessor = ruleset.CreateScoreProcessor(); + private void onScoringModeChanged(ValueChangedEvent mode) + { + int? beatmapMaxCombo = score.Beatmap.MaxCombo; - scoreProcessor.Mods.Value = score.Mods; + if (beatmapMaxCombo == null) + { + if (score.Beatmap.ID == 0 || difficulties == null) + { + // We don't have enough information (max combo) to compute the score, so let's use the provided score. + Value = score.TotalScore.ToString("N0"); + return; + } - return (long)Math.Round(scoreProcessor.GetScore(score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo.Value, 0, ScoringMode.Standardised)); + // We can compute the max combo locally. + beatmapMaxCombo = difficulties().GetDifficulty(score.Beatmap, score.Ruleset, score.Mods).MaxCombo; + } + + var ruleset = score.Ruleset.CreateInstance(); + var scoreProcessor = ruleset.CreateScoreProcessor(); + + scoreProcessor.Mods.Value = score.Mods; + + double maxBaseScore = 300 * beatmapMaxCombo.Value; + double maxHighestCombo = beatmapMaxCombo.Value; + + Value = Math.Round(scoreProcessor.GetScore(mode.NewValue, maxBaseScore, maxHighestCombo, score.Accuracy, score.MaxCombo / maxHighestCombo, 0)).ToString("N0"); + } } } } From 39f8b5eb854df85ff989c71e57aad50dfb558bbf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 21:45:27 +0900 Subject: [PATCH 2889/6909] Use async difficulty calculation --- osu.Game/Scoring/ScoreManager.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 134a41a7d4..8d3872cda0 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -108,6 +108,8 @@ namespace osu.Game.Scoring ScoringMode.BindValueChanged(onScoringModeChanged, true); } + private IBindable difficultyBindable; + private void onScoringModeChanged(ValueChangedEvent mode) { int? beatmapMaxCombo = score.Beatmap.MaxCombo; @@ -121,19 +123,25 @@ namespace osu.Game.Scoring return; } - // We can compute the max combo locally. - beatmapMaxCombo = difficulties().GetDifficulty(score.Beatmap, score.Ruleset, score.Mods).MaxCombo; + // We can compute the max combo locally after the async beatmap difficulty computation. + difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods); + difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true); } + else + updateScore(beatmapMaxCombo.Value); + } + private void updateScore(int beatmapMaxCombo) + { var ruleset = score.Ruleset.CreateInstance(); var scoreProcessor = ruleset.CreateScoreProcessor(); scoreProcessor.Mods.Value = score.Mods; - double maxBaseScore = 300 * beatmapMaxCombo.Value; - double maxHighestCombo = beatmapMaxCombo.Value; + double maxBaseScore = 300 * beatmapMaxCombo; + double maxHighestCombo = beatmapMaxCombo; - Value = Math.Round(scoreProcessor.GetScore(mode.NewValue, maxBaseScore, maxHighestCombo, score.Accuracy, score.MaxCombo / maxHighestCombo, 0)).ToString("N0"); + Value = Math.Round(scoreProcessor.GetScore(ScoringMode.Value, maxBaseScore, maxHighestCombo, score.Accuracy, score.MaxCombo / maxHighestCombo, 0)).ToString("N0"); } } } From 43c61e58308e2f411971a72da430292088d03487 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 22:08:28 +0900 Subject: [PATCH 2890/6909] Re-query beatmap difficulty before computing --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index b80b4e45ed..490f1ba67c 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -89,8 +89,14 @@ namespace osu.Game.Beatmaps if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken, - TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + return await Task.Factory.StartNew(() => + { + // Computation may have finished in a previous task. + if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out existing, out _)) + return existing; + + return computeDifficulty(key, beatmapInfo, rulesetInfo); + }, cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } /// From 436dbafe57614261e9380825aea13b802c9a6dbb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 22:12:17 +0900 Subject: [PATCH 2891/6909] Fix incorrect comparison for mods of different instances --- .../Beatmaps/BeatmapDifficultyManagerTest.cs | 32 +++++++++++++++++++ osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 4 +-- 2 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs new file mode 100644 index 0000000000..0f6d956b3c --- /dev/null +++ b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Tests.Beatmaps +{ + [TestFixture] + public class BeatmapDifficultyManagerTest + { + [Test] + public void TestKeyEqualsWithDifferentModInstances() + { + var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + + Assert.That(key1, Is.EqualTo(key2)); + } + + [Test] + public void TestKeyEqualsWithDifferentModOrder() + { + var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); + + Assert.That(key1, Is.EqualTo(key2)); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 490f1ba67c..0100c9b210 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -251,7 +251,7 @@ namespace osu.Game.Beatmaps updateScheduler?.Dispose(); } - private readonly struct DifficultyCacheLookup : IEquatable + public readonly struct DifficultyCacheLookup : IEquatable { public readonly int BeatmapId; public readonly int RulesetId; @@ -267,7 +267,7 @@ namespace osu.Game.Beatmaps public bool Equals(DifficultyCacheLookup other) => BeatmapId == other.BeatmapId && RulesetId == other.RulesetId - && Mods.SequenceEqual(other.Mods); + && Mods.Select(m => m.Acronym).SequenceEqual(other.Mods.Select(m => m.Acronym)); public override int GetHashCode() { From 8ffc4309fb14937c57dfad9270968d3488cbc91c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 22:23:44 +0900 Subject: [PATCH 2892/6909] Fix possible NaN values --- osu.Game/Scoring/ScoreManager.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 8d3872cda0..0165c5dc82 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -105,6 +105,8 @@ namespace osu.Game.Scoring this.score = score; this.difficulties = difficulties; + Value = "0"; + ScoringMode.BindValueChanged(onScoringModeChanged, true); } @@ -133,6 +135,12 @@ namespace osu.Game.Scoring private void updateScore(int beatmapMaxCombo) { + if (beatmapMaxCombo == 0) + { + Value = "0"; + return; + } + var ruleset = score.Ruleset.CreateInstance(); var scoreProcessor = ruleset.CreateScoreProcessor(); From d7bbb362bf5f0051bbecd4468e63be079a3252f3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 22:51:19 +0900 Subject: [PATCH 2893/6909] Separate bindables --- .../Online/Leaderboards/LeaderboardScore.cs | 2 +- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 26 ++++++++++++++----- 3 files changed, 22 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 24bb43f1b7..846bebe347 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -194,7 +194,7 @@ namespace osu.Game.Online.Leaderboards { TextColour = Color4.White, GlowColour = Color4Extensions.FromHex(@"83ccfa"), - Current = scoreManager.GetTotalScore(score), + Current = scoreManager.GetTotalScoreString(score), Font = OsuFont.Numeric.With(size: 23), }, RankContainer = new Container diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 832ac75882..6bebd98eef 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -124,7 +124,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores new OsuSpriteText { Margin = new MarginPadding { Right = horizontal_inset }, - Current = scoreManager.GetTotalScore(score), + Current = scoreManager.GetTotalScoreString(score), Font = OsuFont.GetFont(size: text_size, weight: index == 0 ? FontWeight.Bold : FontWeight.Medium) }, new OsuSpriteText diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 0165c5dc82..1943cab992 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -86,14 +86,16 @@ namespace osu.Game.Scoring => base.CheckLocalAvailability(model, items) || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); - public Bindable GetTotalScore(ScoreInfo score) + public Bindable GetTotalScore(ScoreInfo score) { var bindable = new TotalScoreBindable(score, difficulties); configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode); return bindable; } - private class TotalScoreBindable : Bindable + public Bindable GetTotalScoreString(ScoreInfo score) => new TotalScoreStringBindable(GetTotalScore(score)); + + private class TotalScoreBindable : Bindable { public readonly Bindable ScoringMode = new Bindable(); @@ -105,7 +107,7 @@ namespace osu.Game.Scoring this.score = score; this.difficulties = difficulties; - Value = "0"; + Value = 0; ScoringMode.BindValueChanged(onScoringModeChanged, true); } @@ -121,7 +123,7 @@ namespace osu.Game.Scoring if (score.Beatmap.ID == 0 || difficulties == null) { // We don't have enough information (max combo) to compute the score, so let's use the provided score. - Value = score.TotalScore.ToString("N0"); + Value = score.TotalScore; return; } @@ -137,7 +139,7 @@ namespace osu.Game.Scoring { if (beatmapMaxCombo == 0) { - Value = "0"; + Value = 0; return; } @@ -149,7 +151,19 @@ namespace osu.Game.Scoring double maxBaseScore = 300 * beatmapMaxCombo; double maxHighestCombo = beatmapMaxCombo; - Value = Math.Round(scoreProcessor.GetScore(ScoringMode.Value, maxBaseScore, maxHighestCombo, score.Accuracy, score.MaxCombo / maxHighestCombo, 0)).ToString("N0"); + Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, maxBaseScore, maxHighestCombo, score.Accuracy, score.MaxCombo / maxHighestCombo, 0)); + } + } + + private class TotalScoreStringBindable : Bindable + { + // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable (need to hold a reference) + private readonly IBindable totalScore; + + public TotalScoreStringBindable(IBindable totalScore) + { + this.totalScore = totalScore; + this.totalScore.BindValueChanged(v => Value = v.NewValue.ToString("N0"), true); } } } From ec2674e1ea60be3b7a649b38da3ff5ce39eadbe0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 22:51:39 +0900 Subject: [PATCH 2894/6909] Fix nullref with null beatmap --- osu.Game/Scoring/ScoreManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 1943cab992..634cca159a 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -116,6 +116,12 @@ namespace osu.Game.Scoring private void onScoringModeChanged(ValueChangedEvent mode) { + if (score.Beatmap == null) + { + Value = score.TotalScore; + return; + } + int? beatmapMaxCombo = score.Beatmap.MaxCombo; if (beatmapMaxCombo == null) From c1838902a669f3e8fb7c4b0c1e25bf50dfa36c84 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 28 Aug 2020 22:51:48 +0900 Subject: [PATCH 2895/6909] Add to more places --- .../Graphics/UserInterface/RollingCounter.cs | 14 ++++++---- .../Scores/TopScoreStatisticsSection.cs | 28 ++++++++++++++++++- .../ContractedPanelMiddleContent.cs | 5 +++- .../Expanded/ExpandedPanelMiddleContent.cs | 9 +++--- 4 files changed, 45 insertions(+), 11 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index 6763198213..a469927595 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -9,16 +9,20 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterface { - public abstract class RollingCounter : Container + public abstract class RollingCounter : Container, IHasCurrentValue where T : struct, IEquatable { - /// - /// The current value. - /// - public Bindable Current = new Bindable(); + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } private SpriteText displayedCountSpriteText; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index a92346e0fe..507c692eb1 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.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; @@ -38,6 +39,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private readonly FillFlowContainer statisticsColumns; private readonly ModsInfoColumn modsColumn; + [Resolved] + private ScoreManager scoreManager { get; set; } + public TopScoreStatisticsSection() { RelativeSizeAxes = Axes.X; @@ -87,6 +91,15 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }; } + [BackgroundDependencyLoader] + private void load() + { + if (score != null) + totalScoreColumn.Current = scoreManager.GetTotalScoreString(score); + } + + private ScoreInfo score; + /// /// Sets the score to be displayed. /// @@ -94,7 +107,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { set { - totalScoreColumn.Text = $@"{value.TotalScore:N0}"; + if (score == value) + return; + + score = value; + accuracyColumn.Text = value.DisplayAccuracy; maxComboColumn.Text = $@"{value.MaxCombo:N0}x"; ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0; @@ -102,6 +119,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); modsColumn.Mods = value.Mods; + + if (IsLoaded) + totalScoreColumn.Current = scoreManager.GetTotalScoreString(value); } } @@ -190,6 +210,12 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { set => text.Text = value; } + + public Bindable Current + { + get => text.Current; + set => text.Current = value; + } } private class ModsInfoColumn : InfoColumn diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 8cd0e7025e..b37b89e6c0 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -30,6 +30,9 @@ namespace osu.Game.Screens.Ranking.Contracted { private readonly ScoreInfo score; + [Resolved] + private ScoreManager scoreManager { get; set; } + /// /// Creates a new . /// @@ -160,7 +163,7 @@ namespace osu.Game.Screens.Ranking.Contracted { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Text = score.TotalScore.ToString("N0"), + Current = scoreManager.GetTotalScoreString(score), Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, fixedWidth: true), Spacing = new Vector2(-1, 0) }, diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 01502c0913..3433410d3c 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -25,15 +25,16 @@ namespace osu.Game.Screens.Ranking.Expanded /// public class ExpandedPanelMiddleContent : CompositeDrawable { - private readonly ScoreInfo score; + private const float padding = 10; + private readonly ScoreInfo score; private readonly List statisticDisplays = new List(); private FillFlowContainer starAndModDisplay; - private RollingCounter scoreCounter; - private const float padding = 10; + [Resolved] + private ScoreManager scoreManager { get; set; } /// /// Creates a new . @@ -238,7 +239,7 @@ namespace osu.Game.Screens.Ranking.Expanded using (BeginDelayedSequence(AccuracyCircle.ACCURACY_TRANSFORM_DELAY, true)) { scoreCounter.FadeIn(); - scoreCounter.Current.Value = score.TotalScore; + scoreCounter.Current = scoreManager.GetTotalScore(score); double delay = 0; From da5853e7eb2c7dd95c8d5ca22ca4a0d4488ae731 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 29 Aug 2020 10:25:43 +0200 Subject: [PATCH 2896/6909] Create a new BeatmapSetInfo when setting files --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index f093180085..dee4626cd0 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -60,12 +60,15 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var reader = new LineBufferedReader(stream)) { var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); - beatmap.BeatmapInfo.BeatmapSet.Files = new List + beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { - new BeatmapSetFileInfo + Files = new List { - Filename = name, - FileInfo = new osu.Game.IO.FileInfo { Hash = name } + new BeatmapSetFileInfo + { + Filename = name, + FileInfo = new osu.Game.IO.FileInfo { Hash = name } + } } }; From 4cb9e1d4438895b7176c72464fcd6c65e738ac71 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 29 Aug 2020 10:33:43 +0200 Subject: [PATCH 2897/6909] Initial commit --- .../TestSceneLegacyBeatmapSkin.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs | 9 ++++++--- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 5 +++-- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/IWorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 8 ++++---- osu.Game/Skinning/BeatmapSkinProvidingContainer.cs | 4 ++-- osu.Game/Skinning/DefaultBeatmapSkin.cs | 9 +++++++++ osu.Game/Skinning/IBeatmapSkin.cs | 9 +++++++++ osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 2 +- 11 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Skinning/DefaultBeatmapSkin.cs create mode 100644 osu.Game/Skinning/IBeatmapSkin.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index 3ff37c4147..03d18cefef 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Tests this.hasColours = hasColours; } - protected override ISkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours); + protected override IBeatmapSkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours); } private class TestBeatmapSkin : LegacyBeatmapSkin diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 075bf314bc..c3c19de17c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -80,15 +80,18 @@ namespace osu.Game.Rulesets.Osu.Tests public class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap { - private readonly ISkinSource skin; + private readonly ISkinSource skinSource; public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin) : base(beatmap, storyboard, frameBasedClock, audio) { - this.skin = skin; + if (!(skinSource is IBeatmapSkin)) + throw new ArgumentException("The provided skin source must be of type IBeatmapSkin."); + + skinSource = skin; } - protected override ISkin GetSkin() => skin; + protected override IBeatmapSkin GetSkin() => (IBeatmapSkin)skinSource; } public class SkinProvidingPlayer : TestPlayer diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index b30870d057..996d495e17 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -116,7 +116,8 @@ namespace osu.Game.Tests.Gameplay AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate); } - private class TestSkin : LegacySkin + // TODO: adding IBeatmapSkin changes are as minimal as possible, but this shouldn't exist or should be reworked to work with LegacyBeatmapSkin + private class TestSkin : LegacySkin, IBeatmapSkin { public TestSkin(string resourceName, AudioManager audioManager) : base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), audioManager, "skin.ini") @@ -156,7 +157,7 @@ namespace osu.Game.Tests.Gameplay this.audio = audio; } - protected override ISkin GetSkin() => new TestSkin("test-sample", audio); + protected override IBeatmapSkin GetSkin() => new TestSkin("test-sample", audio); } private class TestDrawableStoryboardSample : DrawableStoryboardSample diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 39c5ccab27..44728cc251 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -140,7 +140,7 @@ namespace osu.Game.Beatmaps return storyboard; } - protected override ISkin GetSkin() + protected override IBeatmapSkin GetSkin() { try { diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index 31975157a0..dac9389822 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps /// /// Retrieves the which this provides. /// - ISkin Skin { get; } + IBeatmapSkin Skin { get; } /// /// Constructs a playable from using the applicable converters for a specific . diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index b4bcf285b9..163b62a55c 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps background = new RecyclableLazy(GetBackground, BackgroundStillValid); waveform = new RecyclableLazy(GetWaveform); storyboard = new RecyclableLazy(GetStoryboard); - skin = new RecyclableLazy(GetSkin); + skin = new RecyclableLazy(GetSkin); total_count.Value++; } @@ -275,10 +275,10 @@ namespace osu.Game.Beatmaps private readonly RecyclableLazy storyboard; public bool SkinLoaded => skin.IsResultAvailable; - public ISkin Skin => skin.Value; + public IBeatmapSkin Skin => skin.Value; - protected virtual ISkin GetSkin() => new DefaultSkin(); - private readonly RecyclableLazy skin; + protected virtual IBeatmapSkin GetSkin() => new DefaultBeatmapSkin(); + private readonly RecyclableLazy skin; /// /// Transfer pieces of a beatmap to a new one, where possible, to save on loading. diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs index 40335db697..346bfe53b8 100644 --- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs +++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs @@ -11,7 +11,7 @@ namespace osu.Game.Skinning /// /// A container which overrides existing skin options with beatmap-local values. /// - public class BeatmapSkinProvidingContainer : SkinProvidingContainer + public class BeatmapSkinProvidingContainer : SkinProvidingContainer, IBeatmapSkin { private readonly Bindable beatmapSkins = new Bindable(); private readonly Bindable beatmapHitsounds = new Bindable(); @@ -21,7 +21,7 @@ namespace osu.Game.Skinning protected override bool AllowTextureLookup(string componentName) => beatmapSkins.Value; protected override bool AllowSampleLookup(ISampleInfo componentName) => beatmapHitsounds.Value; - public BeatmapSkinProvidingContainer(ISkin skin) + public BeatmapSkinProvidingContainer(IBeatmapSkin skin) : base(skin) { } diff --git a/osu.Game/Skinning/DefaultBeatmapSkin.cs b/osu.Game/Skinning/DefaultBeatmapSkin.cs new file mode 100644 index 0000000000..7b5ccd45c3 --- /dev/null +++ b/osu.Game/Skinning/DefaultBeatmapSkin.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.Skinning +{ + public class DefaultBeatmapSkin : DefaultSkin, IBeatmapSkin + { + } +} diff --git a/osu.Game/Skinning/IBeatmapSkin.cs b/osu.Game/Skinning/IBeatmapSkin.cs new file mode 100644 index 0000000000..77c34b8ad7 --- /dev/null +++ b/osu.Game/Skinning/IBeatmapSkin.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.Skinning +{ + public interface IBeatmapSkin : ISkin + { + } +} diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index d647bc4a2d..d53349dd11 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Skinning { - public class LegacyBeatmapSkin : LegacySkin + public class LegacyBeatmapSkin : LegacySkin, IBeatmapSkin { protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index ab4fb38657..db080d889f 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Beatmaps this.resourceStore = resourceStore; } - protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); + protected override IBeatmapSkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); } } } From 1b81415a16e82131bb170f5473202112a82182ab Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 29 Aug 2020 10:50:25 +0200 Subject: [PATCH 2898/6909] Correct comment --- 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 996d495e17..1e3755c186 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -116,7 +116,7 @@ namespace osu.Game.Tests.Gameplay AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate); } - // TODO: adding IBeatmapSkin changes are as minimal as possible, but this shouldn't exist or should be reworked to work with LegacyBeatmapSkin + // TODO: adding IBeatmapSkin to keep changes as minimal as possible, but this shouldn't exist or should be reworked to inherit LegacyBeatmapSkin private class TestSkin : LegacySkin, IBeatmapSkin { public TestSkin(string resourceName, AudioManager audioManager) From 08329aa382d7afd64e9ce5afe30a94d55d0557ec Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 29 Aug 2020 11:05:10 +0200 Subject: [PATCH 2899/6909] Remove comment again --- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 1e3755c186..bc9528beb6 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -116,7 +116,6 @@ namespace osu.Game.Tests.Gameplay AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate); } - // TODO: adding IBeatmapSkin to keep changes as minimal as possible, but this shouldn't exist or should be reworked to inherit LegacyBeatmapSkin private class TestSkin : LegacySkin, IBeatmapSkin { public TestSkin(string resourceName, AudioManager audioManager) From 82acb3506cbc36a0546ff77fc76c20d8854bb2ed Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 29 Aug 2020 11:07:28 +0200 Subject: [PATCH 2900/6909] Add and change xmldocs --- osu.Game/Beatmaps/IWorkingBeatmap.cs | 2 +- osu.Game/Skinning/IBeatmapSkin.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index dac9389822..aac41725a9 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -42,7 +42,7 @@ namespace osu.Game.Beatmaps Storyboard Storyboard { get; } /// - /// Retrieves the which this provides. + /// Retrieves the which this provides. /// IBeatmapSkin Skin { get; } diff --git a/osu.Game/Skinning/IBeatmapSkin.cs b/osu.Game/Skinning/IBeatmapSkin.cs index 77c34b8ad7..91caaed557 100644 --- a/osu.Game/Skinning/IBeatmapSkin.cs +++ b/osu.Game/Skinning/IBeatmapSkin.cs @@ -3,6 +3,9 @@ namespace osu.Game.Skinning { + /// + /// Marker interface for skins that originate from beatmaps. + /// public interface IBeatmapSkin : ISkin { } From 658a1d159f03df2a55339c5eab7e11085c75ac43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 29 Aug 2020 11:45:59 +0200 Subject: [PATCH 2901/6909] Add legacy flag value for mirror mod --- osu.Game/Beatmaps/Legacy/LegacyMods.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Beatmaps/Legacy/LegacyMods.cs b/osu.Game/Beatmaps/Legacy/LegacyMods.cs index 583e950e49..0e517ea3df 100644 --- a/osu.Game/Beatmaps/Legacy/LegacyMods.cs +++ b/osu.Game/Beatmaps/Legacy/LegacyMods.cs @@ -38,5 +38,6 @@ namespace osu.Game.Beatmaps.Legacy Key1 = 1 << 26, Key3 = 1 << 27, Key2 = 1 << 28, + Mirror = 1 << 30, } } From 58742afd99f131baeb23f0bc85b8161da1559847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 29 Aug 2020 11:47:31 +0200 Subject: [PATCH 2902/6909] Add failing test case --- osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index 957743c5f1..b22687a0a7 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })] + [TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })] public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); protected override Ruleset CreateRuleset() => new ManiaRuleset(); From da82556f6b647dff1d17b384c4061d80665b0393 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 29 Aug 2020 11:49:17 +0200 Subject: [PATCH 2903/6909] Add two-way legacy conversions for mirror mod --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index f7098faa5d..37b34d1721 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -126,6 +126,9 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlag(LegacyMods.Random)) yield return new ManiaModRandom(); + + if (mods.HasFlag(LegacyMods.Mirror)) + yield return new ManiaModMirror(); } public override LegacyMods ConvertToLegacyMods(Mod[] mods) @@ -175,6 +178,10 @@ namespace osu.Game.Rulesets.Mania case ManiaModFadeIn _: value |= LegacyMods.FadeIn; break; + + case ManiaModMirror _: + value |= LegacyMods.Mirror; + break; } } From 9ce9ba3a0d1906448611749a0dc391dcf3944e3d Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 29 Aug 2020 13:50:29 +0200 Subject: [PATCH 2904/6909] Update TestSceneSkinFallbacks.cs --- .../TestSceneSkinFallbacks.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index c3c19de17c..0fe8949360 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests public TestSceneSkinFallbacks() { testUserSkin = new TestSource("user"); - testBeatmapSkin = new TestSource("beatmap"); + testBeatmapSkin = new BeatmapTestSource(); } [Test] @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Osu.Tests public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin) : base(beatmap, storyboard, frameBasedClock, audio) { - if (!(skinSource is IBeatmapSkin)) + if (!(skin is IBeatmapSkin)) throw new ArgumentException("The provided skin source must be of type IBeatmapSkin."); skinSource = skin; @@ -115,6 +115,14 @@ namespace osu.Game.Rulesets.Osu.Tests } } + public class BeatmapTestSource : TestSource, IBeatmapSkin + { + public BeatmapTestSource() + : base("beatmap") + { + } + } + public class TestSource : ISkinSource { private readonly string identifier; From 43e91877a71c3451f4b9617212dbd0c38f7bdb2f Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sat, 29 Aug 2020 14:47:26 +0200 Subject: [PATCH 2905/6909] Scope and limit parameter to IBeatmapSkin --- .../TestSceneSkinFallbacks.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 0fe8949360..64da80a88e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Tests public class TestSceneSkinFallbacks : TestSceneOsuPlayer { private readonly TestSource testUserSkin; - private readonly TestSource testBeatmapSkin; + private readonly BeatmapTestSource testBeatmapSkin; public TestSceneSkinFallbacks() { @@ -80,18 +80,15 @@ namespace osu.Game.Rulesets.Osu.Tests public class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap { - private readonly ISkinSource skinSource; + private readonly IBeatmapSkin skin; - public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin) + public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, IBeatmapSkin skin) : base(beatmap, storyboard, frameBasedClock, audio) { - if (!(skin is IBeatmapSkin)) - throw new ArgumentException("The provided skin source must be of type IBeatmapSkin."); - - skinSource = skin; + this.skin = skin; } - protected override IBeatmapSkin GetSkin() => (IBeatmapSkin)skinSource; + protected override IBeatmapSkin GetSkin() => skin; } public class SkinProvidingPlayer : TestPlayer @@ -115,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Tests } } - public class BeatmapTestSource : TestSource, IBeatmapSkin + private class BeatmapTestSource : TestSource, IBeatmapSkin { public BeatmapTestSource() : base("beatmap") From 69fae0f4122ad64b9c25bdc83ccd4e9cb00c34ba Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 29 Aug 2020 09:30:56 -0700 Subject: [PATCH 2906/6909] Add failing replay download button test --- .../Visual/Ranking/TestSceneResultsScreen.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 74808bc2f5..a86fa05129 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -13,6 +13,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; @@ -212,6 +213,25 @@ namespace osu.Game.Tests.Visual.Ranking AddAssert("expanded panel still on screen", () => this.ChildrenOfType().Single(p => p.State == PanelState.Expanded).ScreenSpaceDrawQuad.TopLeft.X > 0); } + [Test] + public void TestDownloadButtonInitallyDisabled() + { + TestResultsScreen screen = null; + + AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); + + AddAssert("download button is disabled", () => !screen.ChildrenOfType().First().Enabled.Value); + + AddStep("click contracted panel", () => + { + var 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("download button is enabled", () => screen.ChildrenOfType().First().Enabled.Value); + } + private class TestResultsContainer : Container { [Cached(typeof(Player))] @@ -255,6 +275,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo); score.TotalScore += 10 - i; + score.Hash = $"test{i}"; scores.Add(score); } From 0a643fd5e5a4f8b6e5b92f0a27ffe6995fc26b2c Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 29 Aug 2020 09:33:01 -0700 Subject: [PATCH 2907/6909] Fix replay download button always being disabled when initial score's replay is unavailable --- .../Screens/Ranking/ReplayDownloadButton.cs | 40 ++++++++++++------- 1 file changed, 25 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index d0142e57fe..b76842f405 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -74,23 +74,33 @@ namespace osu.Game.Screens.Ranking { button.State.Value = state.NewValue; - switch (replayAvailability) - { - case ReplayAvailability.Local: - button.TooltipText = @"watch replay"; - break; - - case ReplayAvailability.Online: - button.TooltipText = @"download replay"; - break; - - default: - button.TooltipText = @"replay unavailable"; - break; - } + updateTooltip(); }, true); - button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable; + Model.BindValueChanged(_ => + { + button.Enabled.Value = replayAvailability != ReplayAvailability.NotAvailable; + + updateTooltip(); + }, true); + } + + private void updateTooltip() + { + switch (replayAvailability) + { + case ReplayAvailability.Local: + button.TooltipText = @"watch replay"; + break; + + case ReplayAvailability.Online: + button.TooltipText = @"download replay"; + break; + + default: + button.TooltipText = @"replay unavailable"; + break; + } } private enum ReplayAvailability From 5949a281fc54e94b34843ec5d91d8b819b1851b4 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 27 Aug 2020 19:29:18 +0200 Subject: [PATCH 2908/6909] Make Introduce bindable property OverlayActivationMode in OsuScreen --- osu.Game/OsuGame.cs | 5 ++++- osu.Game/Screens/IOsuScreen.cs | 4 ++-- osu.Game/Screens/OsuScreen.cs | 6 +++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 053eb01dcd..6a390942b7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -972,9 +972,12 @@ namespace osu.Game break; } + if (current is IOsuScreen currentOsuScreen) + OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); + if (newScreen is IOsuScreen newOsuScreen) { - OverlayActivationMode.Value = newOsuScreen.InitialOverlayActivationMode; + OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); MusicController.AllowRateAdjustments = newOsuScreen.AllowRateAdjustments; diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index 761f842c22..c9dce310af 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -39,9 +39,9 @@ namespace osu.Game.Screens bool HideOverlaysOnEnter { get; } /// - /// Whether overlays should be able to be opened once this screen is entered or resumed. + /// Whether overlays should be able to be opened when this screen is current. /// - OverlayActivation InitialOverlayActivationMode { get; } + public Bindable OverlayActivationMode { get; } /// /// The amount of parallax to be applied while this screen is displayed. diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 872a1cd39a..c687c34ce9 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -44,10 +44,12 @@ namespace osu.Game.Screens public virtual bool HideOverlaysOnEnter => false; /// - /// Whether overlays should be able to be opened once this screen is entered or resumed. + /// The initial initial overlay activation mode to use when this screen is entered for the first time. /// public virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All; + public Bindable OverlayActivationMode { get; } + public virtual bool CursorVisible => true; protected new OsuGameBase Game => base.Game as OsuGameBase; @@ -138,6 +140,8 @@ namespace osu.Game.Screens { Anchor = Anchor.Centre; Origin = Anchor.Centre; + + OverlayActivationMode = new Bindable(InitialOverlayActivationMode); } [BackgroundDependencyLoader(true)] From ad223bc460ea0d1a6a4d07a86d17cfa3ad0b5b38 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 27 Aug 2020 20:07:24 +0200 Subject: [PATCH 2909/6909] Make game bindable immutable. --- osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs | 7 ++++++- osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs | 2 +- osu.Game/OsuGame.cs | 2 +- osu.Game/Overlays/Toolbar/Toolbar.cs | 2 +- osu.Game/Screens/Menu/ButtonSystem.cs | 3 --- 5 files changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index f819ae4682..841860accb 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -91,7 +91,12 @@ namespace osu.Game.Tests.Visual.Menus public class TestToolbar : Toolbar { - public new Bindable OverlayActivationMode => base.OverlayActivationMode; + public TestToolbar() + { + base.OverlayActivationMode.BindTo(OverlayActivationMode); + } + + public new Bindable OverlayActivationMode { get; } = new Bindable(OverlayActivation.All); } } } diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 93ac69bdbf..751ccc8f15 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Graphics.Containers [Resolved] private PreviewTrackManager previewTrackManager { get; set; } - protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.All); + protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All); [BackgroundDependencyLoader(true)] private void load(AudioManager audio) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 6a390942b7..e6d96df927 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -88,7 +88,7 @@ namespace osu.Game private IdleTracker idleTracker; - public readonly Bindable OverlayActivationMode = new Bindable(); + public readonly IBindable OverlayActivationMode = new Bindable(); protected OsuScreenStack ScreenStack; diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 3bf9e85428..393e349bd0 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Toolbar private const double transition_time = 500; - protected readonly Bindable OverlayActivationMode = new Bindable(OverlayActivation.All); + protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All); // Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden. public override bool PropagateNonPositionalInputSubTree => true; diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 5ba7a8ddc3..4becdd58cd 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -270,9 +270,6 @@ namespace osu.Game.Screens.Menu ButtonSystemState lastState = state; state = value; - if (game != null) - game.OverlayActivationMode.Value = state == ButtonSystemState.Exit ? OverlayActivation.Disabled : OverlayActivation.All; - updateLogoState(lastState); Logger.Log($"{nameof(ButtonSystem)}'s state changed from {lastState} to {state}"); From 8de7744b52e98d0e42a9b88ec08b10ac1a5c2f6f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 28 Aug 2020 09:55:14 +0200 Subject: [PATCH 2910/6909] Add back disabling of overlays on exiting game. --- osu.Game/Screens/Menu/MainMenu.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 57252d557e..859184834b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -280,6 +280,7 @@ namespace osu.Game.Screens.Menu } buttons.State = ButtonSystemState.Exit; + OverlayActivationMode.Value = OverlayActivation.Disabled; songTicker.Hide(); From 03b7c8b88969ae337ac52bcddd56c0c5d034f111 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 29 Aug 2020 19:39:50 +0200 Subject: [PATCH 2911/6909] Remove unneeded access modifier. --- osu.Game/Screens/IOsuScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index c9dce310af..ead8e4bc22 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens /// /// Whether overlays should be able to be opened when this screen is current. /// - public Bindable OverlayActivationMode { get; } + Bindable OverlayActivationMode { get; } /// /// The amount of parallax to be applied while this screen is displayed. From e0eece11b1cde8e12856b33c5050335605b8c3cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 29 Aug 2020 20:13:03 +0200 Subject: [PATCH 2912/6909] Fix typo in test name --- 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 a86fa05129..49fa581108 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -214,7 +214,7 @@ namespace osu.Game.Tests.Visual.Ranking } [Test] - public void TestDownloadButtonInitallyDisabled() + public void TestDownloadButtonInitiallyDisabled() { TestResultsScreen screen = null; From 13df0783fe091b3171cf37972df3aa5a62c0d267 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 29 Aug 2020 20:23:22 +0200 Subject: [PATCH 2913/6909] Use Single() instead of First() where applicable --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 49fa581108..03cb5fa3db 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); - AddAssert("download button is disabled", () => !screen.ChildrenOfType().First().Enabled.Value); + AddAssert("download button is disabled", () => !screen.ChildrenOfType().Single().Enabled.Value); AddStep("click contracted panel", () => { @@ -229,7 +229,7 @@ namespace osu.Game.Tests.Visual.Ranking InputManager.Click(MouseButton.Left); }); - AddAssert("download button is enabled", () => screen.ChildrenOfType().First().Enabled.Value); + AddAssert("download button is enabled", () => screen.ChildrenOfType().Single().Enabled.Value); } private class TestResultsContainer : Container From d22768a98cba3c9d312ba2408536905371840668 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 29 Aug 2020 23:20:59 +0200 Subject: [PATCH 2914/6909] Add scale specification to spinner scene for visibility --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 47b3926ceb..94d1cb8864 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests { @@ -62,7 +63,8 @@ namespace osu.Game.Rulesets.Osu.Tests drawableSpinner = new TestDrawableSpinner(spinner, auto) { Anchor = Anchor.Centre, - Depth = depthIndex++ + Depth = depthIndex++, + Scale = new Vector2(0.75f) }; foreach (var mod in SelectedMods.Value.OfType()) From c9723e541a464c84b8e3e5c27e94af4f0c3fd32d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 29 Aug 2020 23:21:19 +0200 Subject: [PATCH 2915/6909] Add metrics skin resources for old style spinner --- .../metrics-skin/spinner-background@2x.png | Bin 0 -> 14515 bytes .../metrics-skin/spinner-circle@2x.png | Bin 0 -> 6561 bytes .../Resources/metrics-skin/spinner-metre@2x.png | Bin 0 -> 14516 bytes 3 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-metre@2x.png diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-background@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..4f50f638c5de06d711fc7d09a3a4ee13618cd59e GIT binary patch literal 14515 zcmeHNO)G>^6n=(b#_-liY7o6PHa3!?BrnZ0Grp3Pln{j-@{v*u+4u)Seu8Xl*pOm; z6rvQ$EbPd_Mxi)&ym$V7L$?+uU=Wd(Lz2dzRbVf`+Hi1HcG}{2hQ>)Qk~# zrVQ0L^+z&f4m1b-G6G-M4V|wtW{riqCxGlc^>bi#-6t1ali}8Y>(O1DpIcjAr!JWY z`x`oEZlCY>4U~EhteVUD)7h#1p{D5F$;S4OL(M7D_0jU90B@C|nG~>iHlwyCK|~OdfT^9pPhdQo`P&4{QZSASqk2k3_0NLiL%>(1 zdI7}$^nOZtpB`5+FPR$i_ml!#WwMmsqIiqWMJ&;OLvBKDLT;kAIlmi$EhYbp;`HoJ zo9P1Wis-|8N#D2}cJ4(K)Z>z#XtK;n^k7z)_Dw!W^Jpj#6xtFXD6}aE3T+93g6YG? s0<$YQg$)J9N5)6n0;Ja(3XG2_6dx^P)VSK|ID1mw4+q-(n~nX8-{y?j8UO$Q literal 0 HcmV?d00001 diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/spinner-circle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..daf28e09cbe6bbb7fc531b4773e4d381054e65cf GIT binary patch literal 6561 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 Date: Sat, 29 Aug 2020 23:29:29 +0200 Subject: [PATCH 2916/6909] Change structure of old style spinner to be closer to stable --- .../Skinning/LegacyOldStyleSpinner.cs | 85 +++++++++++-------- 1 file changed, 51 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 81a0df5ea5..e157842fd1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -22,11 +22,11 @@ namespace osu.Game.Rulesets.Osu.Skinning { private DrawableSpinner drawableSpinner; private Sprite disc; + private Sprite metreSprite; private Container metre; - private const float background_y_offset = 20; - private const float sprite_scale = 1 / 1.6f; + private const float final_metre_height = 692 * sprite_scale; [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) @@ -35,50 +35,58 @@ namespace osu.Game.Rulesets.Osu.Skinning RelativeSizeAxes = Axes.Both; - InternalChildren = new Drawable[] + InternalChild = new Container { - new Sprite + // the old-style spinner relied heavily on absolute screen-space coordinate values. + // wrap everything in a container simulating absolute coords to preserve alignment + // as there are skins that depend on it. + Width = 640, + Height = 480, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Texture = source.GetTexture("spinner-background"), - Y = background_y_offset, - Scale = new Vector2(sprite_scale) - }, - disc = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-circle"), - Scale = new Vector2(sprite_scale) - }, - metre = new Container - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Y = background_y_offset, - Masking = true, - Child = new Sprite + new Sprite { - Texture = source.GetTexture("spinner-metre"), - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-background"), + Scale = new Vector2(sprite_scale) }, - Scale = new Vector2(0.625f) + disc = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-circle"), + Scale = new Vector2(sprite_scale) + }, + metre = new Container + { + AutoSizeAxes = Axes.Both, + // this anchor makes no sense, but that's what stable uses. + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + // adjustment for stable (metre has additional offset) + Margin = new MarginPadding { Top = 20 }, + Masking = true, + Child = metreSprite = new Sprite + { + Texture = source.GetTexture("spinner-metre"), + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Scale = new Vector2(0.625f) + } + } } }; } - private Vector2 metreFinalSize; - protected override void LoadComplete() { base.LoadComplete(); this.FadeOut(); drawableSpinner.State.BindValueChanged(updateStateTransforms, true); - - metreFinalSize = metre.Size = metre.Child.Size; } private void updateStateTransforms(ValueChangedEvent state) @@ -93,7 +101,16 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.Update(); disc.Rotation = drawableSpinner.RotationTracker.Rotation; - metre.Height = getMetreHeight(drawableSpinner.Progress); + + // careful: need to call this exactly once for all calculations in a frame + // as the function has a random factor in it + var metreHeight = getMetreHeight(drawableSpinner.Progress); + + // hack to make the metre blink up from below than down from above. + // move down the container to be able to apply masking for the metre, + // and then move the sprite back up the same amount to keep its position absolute. + metre.Y = final_metre_height - metreHeight; + metreSprite.Y = -metre.Y; } private const int total_bars = 10; @@ -108,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Skinning if (RNG.NextBool(((int)progress % 10) / 10f)) barCount++; - return (float)barCount / total_bars * metreFinalSize.Y; + return (float)barCount / total_bars * final_metre_height; } } } From e428144f736cdbbb818f9c001d31866fb975b1f2 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 11:34:50 +0200 Subject: [PATCH 2917/6909] Use IBeatmapSkin --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 4 ++-- osu.Game/Beatmaps/BeatmapManager.cs | 10 +--------- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 9 +++------ 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index dee4626cd0..8d5060e2fe 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -45,7 +45,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsTrue(decoded.beatmapSkin.Configuration.Equals(decodedAfterEncode.beatmapSkin.Configuration)); } - private void sort((IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin) tuple) + private void sort((IBeatmap beatmap, IBeatmapSkin beatmapSkin) tuple) { // 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 tuple.beatmap.ControlPointInfo.Groups) @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private Stream encodeToLegacy((IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin) fullBeatmap) + private Stream encodeToLegacy((IBeatmap beatmap, IBeatmapSkin beatmapSkin) fullBeatmap) { var (beatmap, beatmapSkin) = fullBeatmap; var stream = new MemoryStream(); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 86d35749ac..89a776dd31 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -196,22 +196,14 @@ namespace osu.Game.Beatmaps /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, or null if not to be changed. - public void Save(BeatmapInfo info, IBeatmap beatmapContent, LegacyBeatmapSkin beatmapSkin = null) + public void Save(BeatmapInfo info, IBeatmap beatmapContent, IBeatmapSkin beatmapSkin = null) { var setInfo = QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == info.ID)); using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - { - if (beatmapSkin == null) - { - var workingBeatmap = GetWorkingBeatmap(info); - beatmapSkin = (workingBeatmap.Skin is LegacyBeatmapSkin legacy) ? legacy : null; - } - new LegacyBeatmapEncoder(beatmapContent, beatmapSkin).Encode(sw); - } stream.Seek(0, SeekOrigin.Begin); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 497c3c88d0..543d960300 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -25,14 +25,14 @@ namespace osu.Game.Beatmaps.Formats public const int LATEST_VERSION = 128; private readonly IBeatmap beatmap; - private readonly LegacyBeatmapSkin skin; + private readonly IBeatmapSkin skin; /// /// Creates a new . /// /// The beatmap to encode. /// An optional skin, for encoding the beatmap's combo colours. - public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] LegacyBeatmapSkin skin) + public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] IBeatmapSkin skin) { this.beatmap = beatmap; this.skin = skin; @@ -211,10 +211,7 @@ namespace osu.Game.Beatmaps.Formats private void handleComboColours(TextWriter writer) { - if (!(skin is LegacyBeatmapSkin legacySkin)) - return; - - var colours = legacySkin.GetConfig>(GlobalSkinColours.ComboColours)?.Value; + var colours = skin.GetConfig>(GlobalSkinColours.ComboColours)?.Value; if (colours == null || colours.Count == 0) return; From 08321d8dec457381c1e52cab064dd304e309a1ac Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 11:37:43 +0200 Subject: [PATCH 2918/6909] Safe checking against ComboColours instead of CustomColours --- osu.Game/Skinning/SkinConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index 2ac4dfa0c8..0f6162d6c4 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -51,6 +51,6 @@ namespace osu.Game.Skinning public readonly SortedDictionary ConfigDictionary = new SortedDictionary(); - public bool Equals(SkinConfiguration other) => other != null && ConfigDictionary.SequenceEqual(other.ConfigDictionary) && ComboColours.SequenceEqual(other.ComboColours) && CustomColours?.SequenceEqual(other.CustomColours) == true; + public bool Equals(SkinConfiguration other) => other != null && ConfigDictionary.SequenceEqual(other.ConfigDictionary) && ComboColours?.SequenceEqual(other.ComboColours) == true && CustomColours.SequenceEqual(other.CustomColours); } } From f5c82d41eb010912aebd758db6c4b3e9677ac01a Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 16:06:48 +0200 Subject: [PATCH 2919/6909] Remove if-cast --- 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 7bd6529897..273ae67ffd 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Edit private IBeatmap playableBeatmap; private EditorBeatmap editorBeatmap; private EditorChangeHandler changeHandler; - private LegacyBeatmapSkin beatmapSkin; + private IBeatmapSkin beatmapSkin; private DependencyContainer dependencies; @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Edit AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap)); dependencies.CacheAs(editorBeatmap); - beatmapSkin = (Beatmap.Value.Skin is LegacyBeatmapSkin legacy) ? legacy : null; + beatmapSkin = Beatmap.Value.Skin; changeHandler = new EditorChangeHandler(editorBeatmap, beatmapSkin); dependencies.CacheAs(changeHandler); From b39ec74bb812f5c582936cb2ca9877700d786968 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 16:07:06 +0200 Subject: [PATCH 2920/6909] Scope down to IBeatmapSkin in EditorChangeHandler --- osu.Game/Screens/Edit/EditorChangeHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 1d10eaf5cb..60d869ec82 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Edit private int currentState = -1; private readonly EditorBeatmap editorBeatmap; - private readonly LegacyBeatmapSkin beatmapSkin; + private readonly IBeatmapSkin beatmapSkin; private int bulkChangesStarted; private bool isRestoring; @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit /// /// The to track the s of. /// The skin to track the inline skin configuration of. - public EditorChangeHandler(EditorBeatmap editorBeatmap, LegacyBeatmapSkin beatmapSkin) + public EditorChangeHandler(EditorBeatmap editorBeatmap, IBeatmapSkin beatmapSkin) { this.editorBeatmap = editorBeatmap; From 7e57af3ca437b5ae7a053051eb3b64cc1c0bc0e4 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 16:07:46 +0200 Subject: [PATCH 2921/6909] Return true if both ComboColours are null --- osu.Game/Skinning/SkinConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index 0f6162d6c4..18d970dd64 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -51,6 +51,6 @@ namespace osu.Game.Skinning public readonly SortedDictionary ConfigDictionary = new SortedDictionary(); - public bool Equals(SkinConfiguration other) => other != null && ConfigDictionary.SequenceEqual(other.ConfigDictionary) && ComboColours?.SequenceEqual(other.ComboColours) == true && CustomColours.SequenceEqual(other.CustomColours); + public bool Equals(SkinConfiguration other) => other != null && ConfigDictionary.SequenceEqual(other.ConfigDictionary) && ((ComboColours == null && other.ComboColours == null) || ComboColours.SequenceEqual(other.ComboColours)) && CustomColours.SequenceEqual(other.CustomColours); } } From 1fdf8e62004f59ec098a793c6d1a8591cf053b37 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 16:07:58 +0200 Subject: [PATCH 2922/6909] Fix xmldoc in LegacyBeatmapEncoder --- 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 543d960300..8d7e509070 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps.Formats /// Creates a new . /// /// The beatmap to encode. - /// An optional skin, for encoding the beatmap's combo colours. + /// The beatmap's skin, used for encoding combo colours. public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] IBeatmapSkin skin) { this.beatmap = beatmap; From 919d7b77855435bc90df339ac3c939550145ca96 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 16:08:13 +0200 Subject: [PATCH 2923/6909] Remove redundant call to TestResources --- 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 8d5060e2fe..b25f2f1fd3 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Beatmaps.Formats [TestCaseSource(nameof(allBeatmaps))] public void TestEncodeDecodeStability(string name) { - var decoded = decodeFromLegacy(TestResources.GetStore().GetStream(name), name); + var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name); var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name); sort(decoded); From 337037ab3b0f33474c1d9cc829b25b169a49337d Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 16:08:52 +0200 Subject: [PATCH 2924/6909] Make test load actual beatmap's skin configuration --- .../Formats/LegacyBeatmapEncoderTest.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index b25f2f1fd3..bea21087c5 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Beatmaps.Formats sort(decodedAfterEncode); Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); - Assert.IsTrue(decoded.beatmapSkin.Configuration.Equals(decodedAfterEncode.beatmapSkin.Configuration)); + Assert.IsTrue(decodedAfterEncode.beatmapSkin.Configuration.Equals(decoded.beatmapSkin.Configuration)); } private void sort((IBeatmap beatmap, IBeatmapSkin beatmapSkin) tuple) @@ -55,11 +55,13 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private (IBeatmap beatmap, LegacyBeatmapSkin beatmapSkin) decodeFromLegacy(Stream stream, string name) + private (IBeatmap beatmap, TestLegacySkin beatmapSkin) decodeFromLegacy(Stream stream, string name) { using (var reader = new LineBufferedReader(stream)) { var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); + + beatmap.BeatmapInfo.Path = name; beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo { Files = new List @@ -69,14 +71,22 @@ namespace osu.Game.Tests.Beatmaps.Formats Filename = name, FileInfo = new osu.Game.IO.FileInfo { Hash = name } } - } + }, }; - var beatmapSkin = new LegacyBeatmapSkin(beatmap.BeatmapInfo, beatmaps_resource_store, null); + var beatmapSkin = new TestLegacySkin(beatmap, beatmaps_resource_store, name); return (convert(beatmap), beatmapSkin); } } + private class TestLegacySkin : LegacySkin, IBeatmapSkin + { + public TestLegacySkin(Beatmap beatmap, IResourceStore storage, string fileName) + : base(new SkinInfo() { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName) + { + } + } + private Stream encodeToLegacy((IBeatmap beatmap, IBeatmapSkin beatmapSkin) fullBeatmap) { var (beatmap, beatmapSkin) = fullBeatmap; From 7e668fc31a619ca1d89dc6532898433bf40efe07 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 16:11:49 +0200 Subject: [PATCH 2925/6909] Update osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs Co-authored-by: Salman Ahmed --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 8d7e509070..cae6a43cd4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -226,7 +226,8 @@ namespace osu.Game.Beatmaps.Formats writer.Write(FormattableString.Invariant($"{(byte)(comboColour.R * byte.MaxValue)},")); writer.Write(FormattableString.Invariant($"{(byte)(comboColour.G * byte.MaxValue)},")); writer.Write(FormattableString.Invariant($"{(byte)(comboColour.B * byte.MaxValue)},")); - writer.WriteLine(FormattableString.Invariant($"{(byte)(comboColour.A * byte.MaxValue)}")); + writer.Write(FormattableString.Invariant($"{(byte)(comboColour.A * byte.MaxValue)}")); + writer.WriteLine(); } } From 43d144b7c00756a0cb0d94a7c88750614fcd97f6 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 16:23:00 +0200 Subject: [PATCH 2926/6909] Remove empty argument list --- 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 bea21087c5..dc91af72e8 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Beatmaps.Formats private class TestLegacySkin : LegacySkin, IBeatmapSkin { public TestLegacySkin(Beatmap beatmap, IResourceStore storage, string fileName) - : base(new SkinInfo() { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName) + : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName) { } } From db413686bbe7db6dadf72d71d54fe1def027427b Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 21:12:45 +0200 Subject: [PATCH 2927/6909] Add BeatmapSkin to EditorBeatmap --- osu.Game.Tests/Editing/EditorChangeHandlerTest.cs | 6 +++--- osu.Game/Screens/Edit/Editor.cs | 10 +++------- osu.Game/Screens/Edit/EditorBeatmap.cs | 6 +++++- osu.Game/Screens/Edit/EditorChangeHandler.cs | 9 ++------- 4 files changed, 13 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index 6d708ce838..feda1ae0e9 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -13,7 +13,7 @@ namespace osu.Game.Tests.Editing [Test] public void TestSaveRestoreState() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()), null); + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False); @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Editing [Test] public void TestMaxStatesSaved() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()), null); + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); Assert.That(handler.CanUndo.Value, Is.False); @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Editing [Test] public void TestMaxStatesExceeded() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap()), null); + var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); Assert.That(handler.CanUndo.Value, Is.False); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 273ae67ffd..b1f11d79f9 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -33,7 +33,6 @@ using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Play; -using osu.Game.Skinning; using osu.Game.Users; namespace osu.Game.Screens.Edit @@ -65,7 +64,6 @@ namespace osu.Game.Screens.Edit private IBeatmap playableBeatmap; private EditorBeatmap editorBeatmap; private EditorChangeHandler changeHandler; - private IBeatmapSkin beatmapSkin; private DependencyContainer dependencies; @@ -103,11 +101,9 @@ namespace osu.Game.Screens.Edit return; } - AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap)); + AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, Beatmap.Value.Skin)); dependencies.CacheAs(editorBeatmap); - - beatmapSkin = Beatmap.Value.Skin; - changeHandler = new EditorChangeHandler(editorBeatmap, beatmapSkin); + changeHandler = new EditorChangeHandler(editorBeatmap); dependencies.CacheAs(changeHandler); EditorMenuBar menuBar; @@ -402,7 +398,7 @@ namespace osu.Game.Screens.Edit clock.SeekForward(!clock.IsRunning, amount); } - private void saveBeatmap() => beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, beatmapSkin); + private void saveBeatmap() => beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin); private void exportBeatmap() { diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 23c8c9f605..a314d50e60 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; namespace osu.Game.Screens.Edit { @@ -47,6 +48,8 @@ namespace osu.Game.Screens.Edit public readonly IBeatmap PlayableBeatmap; + public readonly IBeatmapSkin BeatmapSkin; + [Resolved] private BindableBeatDivisor beatDivisor { get; set; } @@ -54,9 +57,10 @@ namespace osu.Game.Screens.Edit private readonly Dictionary> startTimeBindables = new Dictionary>(); - public EditorBeatmap(IBeatmap playableBeatmap) + public EditorBeatmap(IBeatmap playableBeatmap, IBeatmapSkin beatmapSkin = null) { PlayableBeatmap = playableBeatmap; + BeatmapSkin = beatmapSkin; beatmapProcessor = playableBeatmap.BeatmapInfo.Ruleset?.CreateInstance().CreateBeatmapProcessor(PlayableBeatmap); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 60d869ec82..927c823c64 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -8,7 +8,6 @@ using System.Text; using osu.Framework.Bindables; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; -using osu.Game.Skinning; namespace osu.Game.Screens.Edit { @@ -26,7 +25,6 @@ namespace osu.Game.Screens.Edit private int currentState = -1; private readonly EditorBeatmap editorBeatmap; - private readonly IBeatmapSkin beatmapSkin; private int bulkChangesStarted; private bool isRestoring; @@ -36,8 +34,7 @@ namespace osu.Game.Screens.Edit /// Creates a new . /// /// The to track the s of. - /// The skin to track the inline skin configuration of. - public EditorChangeHandler(EditorBeatmap editorBeatmap, IBeatmapSkin beatmapSkin) + public EditorChangeHandler(EditorBeatmap editorBeatmap) { this.editorBeatmap = editorBeatmap; @@ -47,8 +44,6 @@ namespace osu.Game.Screens.Edit patcher = new LegacyEditorBeatmapPatcher(editorBeatmap); - this.beatmapSkin = beatmapSkin; - // Initial state. SaveState(); } @@ -90,7 +85,7 @@ namespace osu.Game.Screens.Edit using (var stream = new MemoryStream()) { using (var sw = new StreamWriter(stream, Encoding.UTF8, 1024, true)) - new LegacyBeatmapEncoder(editorBeatmap, beatmapSkin).Encode(sw); + new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw); savedStates.Add(stream.ToArray()); } From 07f6a6817961c0426baacb14507df4d1e4937451 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Sun, 30 Aug 2020 21:13:06 +0200 Subject: [PATCH 2928/6909] Update LegacyBeatmapEncoderTest.cs --- .../Formats/LegacyBeatmapEncoderTest.cs | 65 +++++++++++++++---- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index dc91af72e8..a8a3f266fc 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -7,8 +7,10 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Audio.Track; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Beatmaps; @@ -61,24 +63,59 @@ namespace osu.Game.Tests.Beatmaps.Formats { var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); - beatmap.BeatmapInfo.Path = name; - beatmap.BeatmapInfo.BeatmapSet = new BeatmapSetInfo + using (var rs = new MemoryBeatmapResourceStore(stream, name)) { - Files = new List - { - new BeatmapSetFileInfo - { - Filename = name, - FileInfo = new osu.Game.IO.FileInfo { Hash = name } - } - }, - }; - - var beatmapSkin = new TestLegacySkin(beatmap, beatmaps_resource_store, name); - return (convert(beatmap), beatmapSkin); + var beatmapSkin = new TestLegacySkin(beatmap, rs, name); + return (convert(beatmap), beatmapSkin); + } } } + private class MemoryBeatmapResourceStore : IResourceStore + { + private readonly Stream beatmapData; + private readonly string beatmapName; + + public MemoryBeatmapResourceStore(Stream beatmapData, string beatmapName) + { + this.beatmapData = beatmapData; + this.beatmapName = beatmapName; + } + + public void Dispose() => beatmapData.Dispose(); + + public byte[] Get(string name) + { + if (name != beatmapName) + return null; + + byte[] buffer = new byte[beatmapData.Length]; + beatmapData.Read(buffer, 0, buffer.Length); + return buffer; + } + + public async Task GetAsync(string name) + { + if (name != beatmapName) + return null; + + byte[] buffer = new byte[beatmapData.Length]; + await beatmapData.ReadAsync(buffer.AsMemory()); + return buffer; + } + + public Stream GetStream(string name) + { + if (name != beatmapName) + return null; + + beatmapData.Seek(0, SeekOrigin.Begin); + return beatmapData; + } + + public IEnumerable GetAvailableResources() => beatmapName.Yield(); + } + private class TestLegacySkin : LegacySkin, IBeatmapSkin { public TestLegacySkin(Beatmap beatmap, IResourceStore storage, string fileName) From 8151aa6ed8b761f4b51ceb7345c0c9bb855d7ad1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 13:31:55 +0900 Subject: [PATCH 2929/6909] Remove unused method --- osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index ed187e65bf..ab840e1c46 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.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; @@ -60,12 +59,6 @@ namespace osu.Game.Rulesets.Mania.Tests () => 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(ManiaHitObject hitObject, double offset) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", From acbeb5406f320d5749e2301f3b471b14eb85b62c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 13:33:41 +0900 Subject: [PATCH 2930/6909] Add/improve xmldoc --- .../Objects/Drawables/DrawableManiaHitObject.cs | 4 ++++ .../Objects/Drawables/DrawableOsuHitObject.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 0594d1e143..e16413bce7 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -36,6 +36,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } + /// + /// Whether this can be hit, given a time value. + /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. + /// public Func CheckHittable; protected DrawableManiaHitObject(ManiaHitObject hitObject) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 8308c0c576..2946331bc6 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 protected override float SamplePlaybackPosition => HitObject.X / OsuPlayfield.BASE_SIZE.X; /// - /// Whether this can be hit. + /// Whether this can be hit, given a time value. /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. /// public Func CheckHittable; From abdb99192397e21964706822c9d9fa8948a54c8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Aug 2020 14:15:47 +0900 Subject: [PATCH 2931/6909] Hide misses from timing distribution graph --- .../TestSceneHitEventTimingDistributionGraph.cs | 12 ++++++++++++ .../Statistics/HitEventTimingDistributionGraph.cs | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 7ca1fc842f..144f8da2fa 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -35,6 +35,18 @@ namespace osu.Game.Tests.Visual.Ranking createTest(new List()); } + [Test] + public void TestMissesDontShow() + { + createTest(Enumerable.Range(0, 100).Select(i => + { + if (i % 2 == 0) + return new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), null); + + return new HitEvent(30, HitResult.Miss, new HitCircle(), new HitCircle(), null); + }).ToList()); + } + private void createTest(List events) => AddStep("create test", () => { Children = new Drawable[] diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 527da429ed..45fdc3ff33 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.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows)).ToList(); + this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss).ToList(); } [BackgroundDependencyLoader] From c3bfce6ccff2bd908a80e47614b1329d1f585e00 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 15:03:41 +0900 Subject: [PATCH 2932/6909] Add star rating to beatmap wedge --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 27ce9e82dd..cb3a347af4 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -27,6 +27,7 @@ using osu.Framework.Logging; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Ranking.Expanded; namespace osu.Game.Screens.Select { @@ -224,8 +225,15 @@ namespace osu.Game.Screens.Select AutoSizeAxes = Axes.Both, Children = new Drawable[] { + new StarRatingDisplay(beatmapInfo) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + }, StatusPill = new BeatmapSetOnlineStatusPill { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, TextSize = 11, TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, Status = beatmapInfo.Status, From 4736845318acac3c4e4da894500389a933bc8c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Aug 2020 10:56:06 +0200 Subject: [PATCH 2933/6909] Add spacing between star rating and beatmap status --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index cb3a347af4..518ad33529 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -229,6 +229,7 @@ namespace osu.Game.Screens.Select { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + Margin = new MarginPadding { Bottom = 5 } }, StatusPill = new BeatmapSetOnlineStatusPill { From bee01bdd38cf13bfeddac343c68ad315daef570f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Aug 2020 18:01:16 +0900 Subject: [PATCH 2934/6909] Fix first scroll wheel in editor incorrectly advancing twice --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d92f3922c3..e178459d5c 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -284,7 +284,7 @@ namespace osu.Game.Screens.Edit // this is a special case to handle the "pivot" scenario. // if we are precise scrolling in one direction then change our mind and scroll backwards, // the existing accumulation should be applied in the inverse direction to maintain responsiveness. - if (Math.Sign(scrollAccumulation) != scrollDirection) + if (scrollAccumulation != 0 && Math.Sign(scrollAccumulation) != scrollDirection) scrollAccumulation = scrollDirection * (precision - Math.Abs(scrollAccumulation)); scrollAccumulation += scrollComponent * (e.IsPrecise ? 0.1 : 1); From 7d273d631be6491d3ad6ae769770469ba0cb9214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Aug 2020 11:05:42 +0200 Subject: [PATCH 2935/6909] Do not show star difficulty on wedge if zero --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 518ad33529..2b2c40411d 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -223,14 +223,13 @@ namespace osu.Game.Screens.Select Direction = FillDirection.Vertical, Padding = new MarginPadding { Top = 14, Right = shear_width / 2 }, AutoSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { - new StarRatingDisplay(beatmapInfo) + createStarRatingDisplay(beatmapInfo).With(display => { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Margin = new MarginPadding { Bottom = 5 } - }, + display.Anchor = Anchor.CentreRight; + display.Origin = Anchor.CentreRight; + }), StatusPill = new BeatmapSetOnlineStatusPill { Anchor = Anchor.CentreRight, @@ -291,6 +290,13 @@ namespace osu.Game.Screens.Select StatusPill.Hide(); } + private static Drawable createStarRatingDisplay(BeatmapInfo beatmapInfo) => beatmapInfo.StarDifficulty > 0 + ? new StarRatingDisplay(beatmapInfo) + { + Margin = new MarginPadding { Bottom = 5 } + } + : Empty(); + private void setMetadata(string source) { ArtistLabel.Text = artistBinding.Value; From 8b7446c43f1a53bbf83804486289ee6e42c5ec8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Aug 2020 18:13:51 +0900 Subject: [PATCH 2936/6909] Fix RollingCounter not updating initial value if changed before loaded --- osu.Game/Graphics/UserInterface/RollingCounter.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index 6763198213..ceb388600e 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -65,12 +65,6 @@ namespace osu.Game.Graphics.UserInterface protected RollingCounter() { AutoSizeAxes = Axes.Both; - - Current.ValueChanged += val => - { - if (IsLoaded) - TransformCount(DisplayedCount, val.NewValue); - }; } [BackgroundDependencyLoader] @@ -81,6 +75,13 @@ namespace osu.Game.Graphics.UserInterface Child = displayedCountSpriteText; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(val => TransformCount(DisplayedCount, val.NewValue), true); + } + /// /// Sets count value, bypassing rollover animation. /// From a171d0e292be37a8c85d1f5e40b933f9d07b7619 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 31 Aug 2020 18:14:22 +0900 Subject: [PATCH 2937/6909] Remove unused methods and classes --- .../UserInterface/PercentageCounter.cs | 5 --- .../Graphics/UserInterface/RollingCounter.cs | 2 -- .../Graphics/UserInterface/ScoreCounter.cs | 5 --- .../UserInterface/SimpleComboCounter.cs | 5 --- .../Screens/Play/HUD/ComboResultCounter.cs | 32 ------------------- .../Expanded/Statistics/AccuracyStatistic.cs | 3 -- .../Expanded/Statistics/CounterStatistic.cs | 3 -- .../Ranking/Expanded/TotalScoreCounter.cs | 3 -- 8 files changed, 58 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/ComboResultCounter.cs diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs index 3ea9c1053c..1ccf7798e5 100644 --- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs +++ b/osu.Game/Graphics/UserInterface/PercentageCounter.cs @@ -40,10 +40,5 @@ namespace osu.Game.Graphics.UserInterface protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f, fixedWidth: true)); - - public override void Increment(double amount) - { - Current.Value += amount; - } } } diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index ceb388600e..ece1b8e22c 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -57,8 +57,6 @@ namespace osu.Game.Graphics.UserInterface } } - public abstract void Increment(T amount); - /// /// Skeleton of a numeric counter which value rolls over time. /// diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index faabe69f87..73bbe5f03e 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -51,10 +51,5 @@ namespace osu.Game.Graphics.UserInterface protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(fixedWidth: true)); - - public override void Increment(double amount) - { - Current.Value += amount; - } } } diff --git a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs b/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs index aac0166774..c9790aed46 100644 --- a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs +++ b/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs @@ -33,11 +33,6 @@ namespace osu.Game.Graphics.UserInterface return Math.Abs(currentValue - newValue) * RollingDuration * 100.0f; } - public override void Increment(int amount) - { - Current.Value += amount; - } - protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => s.Font = s.Font.With(size: 20f)); } diff --git a/osu.Game/Screens/Play/HUD/ComboResultCounter.cs b/osu.Game/Screens/Play/HUD/ComboResultCounter.cs deleted file mode 100644 index 7ae8bc0ddf..0000000000 --- a/osu.Game/Screens/Play/HUD/ComboResultCounter.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.Game.Graphics.UserInterface; - -namespace osu.Game.Screens.Play.HUD -{ - /// - /// Used to display combo with a roll-up animation in results screen. - /// - public class ComboResultCounter : RollingCounter - { - protected override double RollingDuration => 500; - protected override Easing RollingEasing => Easing.Out; - - protected override double GetProportionalDuration(long currentValue, long newValue) - { - return currentValue > newValue ? currentValue - newValue : newValue - currentValue; - } - - protected override string FormatCount(long count) - { - return $@"{count}x"; - } - - public override void Increment(long amount) - { - Current.Value += amount; - } - } -} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs index 6933456e7e..288a107874 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/AccuracyStatistic.cs @@ -46,9 +46,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics protected override string FormatCount(double count) => count.FormatAccuracy(); - public override void Increment(double amount) - => Current.Value += amount; - protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => { s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index 043a560d12..e820831809 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -49,9 +49,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); s.Spacing = new Vector2(-2, 0); }); - - public override void Increment(int amount) - => Current.Value += amount; } } } diff --git a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs index 7f6fd1eabe..65082d3fae 100644 --- a/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs +++ b/osu.Game/Screens/Ranking/Expanded/TotalScoreCounter.cs @@ -36,8 +36,5 @@ namespace osu.Game.Screens.Ranking.Expanded s.Font = OsuFont.Torus.With(size: 60, weight: FontWeight.Light, fixedWidth: true); s.Spacing = new Vector2(-5, 0); }); - - public override void Increment(long amount) - => Current.Value += amount; } } From dd093f44d8826af623ed5232e6a38d8f39b0ce20 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 31 Aug 2020 11:16:13 +0200 Subject: [PATCH 2938/6909] Cast base immutable bindable to mutable for testing purposes and make InitialOverlayActivationMode property protected --- osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs | 7 +------ osu.Game/Screens/OsuScreen.cs | 2 +- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/StartupScreen.cs | 2 +- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 841860accb..2a4486812c 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -91,12 +91,7 @@ namespace osu.Game.Tests.Visual.Menus public class TestToolbar : Toolbar { - public TestToolbar() - { - base.OverlayActivationMode.BindTo(OverlayActivationMode); - } - - public new Bindable OverlayActivationMode { get; } = new Bindable(OverlayActivation.All); + public new Bindable OverlayActivationMode => base.OverlayActivationMode as Bindable; } } } diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index c687c34ce9..c10deaf1e5 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens /// /// The initial initial overlay activation mode to use when this screen is entered for the first time. /// - public virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All; + protected virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All; public Bindable OverlayActivationMode { get; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 541275cf55..0a5158c6dc 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Play public override bool HideOverlaysOnEnter => true; - public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; /// /// Whether gameplay should pause when the game window focus is lost. diff --git a/osu.Game/Screens/StartupScreen.cs b/osu.Game/Screens/StartupScreen.cs index c3e36c8e9d..e5e134fd39 100644 --- a/osu.Game/Screens/StartupScreen.cs +++ b/osu.Game/Screens/StartupScreen.cs @@ -18,6 +18,6 @@ namespace osu.Game.Screens public override bool AllowRateAdjustments => false; - public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; + protected override OverlayActivation InitialOverlayActivationMode => OverlayActivation.Disabled; } } From d419fe4dbf5f54edddd67cfb4507150ae43c2f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Aug 2020 12:02:02 +0200 Subject: [PATCH 2939/6909] Remove note shaking mention that doesn't apply in mania --- .../Objects/Drawables/DrawableManiaHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index e16413bce7..08c41b0d75 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -38,7 +38,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Whether this can be hit, given a time value. - /// If non-null, judgements will be ignored (resulting in a shake) whilst the function returns false. + /// If non-null, judgements will be ignored whilst the function returns false. /// public Func CheckHittable; From ed74c39b5587e2084dd9fe957aef6d4e9f422644 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 19:54:22 +0900 Subject: [PATCH 2940/6909] Move UserTopScoreContainer into base leaderboard --- .../SongSelect/TestSceneBeatmapLeaderboard.cs | 61 ++++++------- .../TestSceneUserTopScoreContainer.cs | 91 +++++++++---------- .../API/Requests/Responses/APILegacyScores.cs | 9 ++ osu.Game/Online/Leaderboards/Leaderboard.cs | 21 ++++- .../Leaderboards/UserTopScoreContainer.cs | 26 ++---- .../Match/Components/MatchLeaderboard.cs | 2 + .../Select/Leaderboards/BeatmapLeaderboard.cs | 30 ++---- 7 files changed, 114 insertions(+), 126 deletions(-) rename osu.Game/{Screens/Select => Online}/Leaderboards/UserTopScoreContainer.cs (77%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs index 48b718c04d..67cd720260 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapLeaderboard.cs @@ -5,9 +5,9 @@ using System; 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.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Scoring; using osu.Game.Screens.Select.Leaderboards; @@ -53,53 +53,46 @@ namespace osu.Game.Tests.Visual.SongSelect private void showPersonalBestWithNullPosition() { - leaderboard.TopScore = new APILegacyUserTopScoreInfo + leaderboard.TopScore = new ScoreInfo { - Position = null, - Score = new APILegacyScoreInfo + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock() }, + User = new User { - Rank = ScoreRank.XH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, }, - User = new User + Id = 6602580, + Username = @"waaiiru", + Country = new Country { - Id = 6602580, - Username = @"waaiiru", - Country = new Country - { - FullName = @"Spain", - FlagName = @"ES", - }, + FullName = @"Spain", + FlagName = @"ES", }, - } + }, }; } private void showPersonalBest() { - leaderboard.TopScore = new APILegacyUserTopScoreInfo + leaderboard.TopScore = new ScoreInfo { Position = 999, - Score = new APILegacyScoreInfo + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + User = new User { - Rank = ScoreRank.XH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, }, - User = new User + Id = 6602580, + Username = @"waaiiru", + Country = new Country { - Id = 6602580, - Username = @"waaiiru", - Country = new Country - { - FullName = @"Spain", - FlagName = @"ES", - }, + FullName = @"Spain", + FlagName = @"ES", }, - } + }, }; } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs index 0598324110..b8b8792b9b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneUserTopScoreContainer.cs @@ -6,11 +6,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osuTK.Graphics; -using osu.Game.Online.API.Requests.Responses; +using osu.Game.Online.Leaderboards; using osu.Game.Overlays; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Select.Leaderboards; using osu.Game.Users; namespace osu.Game.Tests.Visual.SongSelect @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.SongSelect public TestSceneUserTopScoreContainer() { - UserTopScoreContainer topScoreContainer; + UserTopScoreContainer topScoreContainer; Add(dialogOverlay = new DialogOverlay { @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.SongSelect RelativeSizeAxes = Axes.Both, Colour = Color4.DarkGreen, }, - topScoreContainer = new UserTopScoreContainer + topScoreContainer = new UserTopScoreContainer(s => new LeaderboardScore(s, s.Position, false)) { Origin = Anchor.BottomCentre, Anchor = Anchor.BottomCentre, @@ -52,69 +52,60 @@ namespace osu.Game.Tests.Visual.SongSelect var scores = new[] { - new APILegacyUserTopScoreInfo + new ScoreInfo { Position = 999, - Score = new APILegacyScoreInfo + Rank = ScoreRank.XH, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + Mods = new Mod[] { new OsuModHidden(), new OsuModHardRock(), }, + User = new User { - Rank = ScoreRank.XH, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - Mods = new[] { new OsuModHidden().Acronym, new OsuModHardRock().Acronym, }, - User = new User + Id = 6602580, + Username = @"waaiiru", + Country = new Country { - Id = 6602580, - Username = @"waaiiru", - Country = new Country - { - FullName = @"Spain", - FlagName = @"ES", - }, + FullName = @"Spain", + FlagName = @"ES", }, - } + }, }, - new APILegacyUserTopScoreInfo + new ScoreInfo { Position = 110000, - Score = new APILegacyScoreInfo + Rank = ScoreRank.X, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + User = new User { - Rank = ScoreRank.X, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - User = new User + Id = 4608074, + Username = @"Skycries", + Country = new Country { - Id = 4608074, - Username = @"Skycries", - Country = new Country - { - FullName = @"Brazil", - FlagName = @"BR", - }, + FullName = @"Brazil", + FlagName = @"BR", }, - } + }, }, - new APILegacyUserTopScoreInfo + new ScoreInfo { Position = 22333, - Score = new APILegacyScoreInfo + Rank = ScoreRank.S, + Accuracy = 1, + MaxCombo = 244, + TotalScore = 1707827, + User = new User { - Rank = ScoreRank.S, - Accuracy = 1, - MaxCombo = 244, - TotalScore = 1707827, - User = new User + Id = 1541390, + Username = @"Toukai", + Country = new Country { - Id = 1541390, - Username = @"Toukai", - Country = new Country - { - FullName = @"Canada", - FlagName = @"CA", - }, + FullName = @"Canada", + FlagName = @"CA", }, - } + }, } }; diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScores.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScores.cs index 75be9171b0..009639c1dc 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScores.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScores.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using Newtonsoft.Json; +using osu.Game.Rulesets; +using osu.Game.Scoring; namespace osu.Game.Online.API.Requests.Responses { @@ -22,5 +24,12 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"score")] public APILegacyScoreInfo Score; + + public ScoreInfo CreateScoreInfo(RulesetStore rulesets) + { + var score = Score.CreateScoreInfo(rulesets); + score.Position = Position; + return score; + } } } diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 800029ceb9..003d90d400 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -27,6 +27,7 @@ namespace osu.Game.Online.Leaderboards private readonly OsuScrollContainer scrollContainer; private readonly Container placeholderContainer; + private readonly UserTopScoreContainer topScoreContainer; private FillFlowContainer scrollFlow; @@ -87,6 +88,21 @@ namespace osu.Game.Online.Leaderboards } } + public TScoreInfo TopScore + { + get => topScoreContainer.Score.Value; + set + { + if (value == null) + topScoreContainer.Hide(); + else + { + topScoreContainer.Show(); + topScoreContainer.Score.Value = value; + } + } + } + protected virtual FillFlowContainer CreateScoreFlow() => new FillFlowContainer { @@ -198,8 +214,9 @@ namespace osu.Game.Online.Leaderboards { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, + Child = topScoreContainer = new UserTopScoreContainer(CreateDrawableTopScore) }, - } + }, }, }, }, @@ -367,5 +384,7 @@ namespace osu.Game.Online.Leaderboards } protected abstract LeaderboardScore CreateDrawableScore(TScoreInfo model, int index); + + protected abstract LeaderboardScore CreateDrawableTopScore(TScoreInfo model); } } diff --git a/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs similarity index 77% rename from osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs rename to osu.Game/Online/Leaderboards/UserTopScoreContainer.cs index 8e10734454..ffa7fa2c0b 100644 --- a/osu.Game/Screens/Select/Leaderboards/UserTopScoreContainer.cs +++ b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs @@ -9,31 +9,28 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; -using osu.Game.Scoring; using osuTK; -namespace osu.Game.Screens.Select.Leaderboards +namespace osu.Game.Online.Leaderboards { - public class UserTopScoreContainer : VisibilityContainer + public class UserTopScoreContainer : VisibilityContainer { private const int duration = 500; + public Bindable Score = new Bindable(); + private readonly Container scoreContainer; - - public Bindable Score = new Bindable(); - - public Action ScoreSelected; + private readonly Func createScoreDelegate; protected override bool StartHidden => true; [Resolved] private RulesetStore rulesets { get; set; } - public UserTopScoreContainer() + public UserTopScoreContainer(Func createScoreDelegate) { + this.createScoreDelegate = createScoreDelegate; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -72,7 +69,7 @@ namespace osu.Game.Screens.Select.Leaderboards private CancellationTokenSource loadScoreCancellation; - private void onScoreChanged(ValueChangedEvent score) + private void onScoreChanged(ValueChangedEvent score) { var newScore = score.NewValue; @@ -82,12 +79,7 @@ namespace osu.Game.Screens.Select.Leaderboards if (newScore == null) return; - var scoreInfo = newScore.Score.CreateScoreInfo(rulesets); - - LoadComponentAsync(new LeaderboardScore(scoreInfo, newScore.Position, false) - { - Action = () => ScoreSelected?.Invoke(scoreInfo) - }, drawableScore => + LoadComponentAsync(createScoreDelegate(newScore), drawableScore => { scoreContainer.Child = drawableScore; drawableScore.FadeInFromZero(duration, Easing.OutQuint); diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index 1afbf5c32a..01137dad43 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -51,6 +51,8 @@ namespace osu.Game.Screens.Multi.Match.Components } protected override LeaderboardScore CreateDrawableScore(APIUserScoreAggregate model, int index) => new MatchLeaderboardScore(model, index); + + protected override LeaderboardScore CreateDrawableTopScore(APIUserScoreAggregate model) => new MatchLeaderboardScore(model, 0); } public enum MatchLeaderboardScope diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index 8e85eb4eb2..a78d8e3be0 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -41,25 +40,8 @@ namespace osu.Game.Screens.Select.Leaderboards } } - public APILegacyUserTopScoreInfo TopScore - { - get => topScoreContainer.Score.Value; - set - { - if (value == null) - topScoreContainer.Hide(); - else - { - topScoreContainer.Show(); - topScoreContainer.Score.Value = value; - } - } - } - private bool filterMods; - private UserTopScoreContainer topScoreContainer; - private IBindable> itemRemoved; /// @@ -101,11 +83,6 @@ namespace osu.Game.Screens.Select.Leaderboards UpdateScores(); }; - Content.Add(topScoreContainer = new UserTopScoreContainer - { - ScoreSelected = s => ScoreSelected?.Invoke(s) - }); - itemRemoved = scoreManager.ItemRemoved.GetBoundCopy(); itemRemoved.BindValueChanged(onScoreRemoved); } @@ -183,7 +160,7 @@ namespace osu.Game.Screens.Select.Leaderboards req.Success += r => { scoresCallback?.Invoke(r.Scores.Select(s => s.CreateScoreInfo(rulesets))); - TopScore = r.UserScore; + TopScore = r.UserScore.CreateScoreInfo(rulesets); }; return req; @@ -193,5 +170,10 @@ namespace osu.Game.Screens.Select.Leaderboards { Action = () => ScoreSelected?.Invoke(model) }; + + protected override LeaderboardScore CreateDrawableTopScore(ScoreInfo model) => new LeaderboardScore(model, model.Position, false) + { + Action = () => ScoreSelected?.Invoke(model) + }; } } From d1ceb81797a8bd19b931f3816c26502673b0d8be Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 19:54:41 +0900 Subject: [PATCH 2941/6909] Rename request --- ...GetRoomScoresRequest.cs => GetRoomLeaderboardRequest.cs} | 6 ++---- osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs | 2 +- 2 files changed, 3 insertions(+), 5 deletions(-) rename osu.Game/Online/Multiplayer/{GetRoomScoresRequest.cs => GetRoomLeaderboardRequest.cs} (65%) diff --git a/osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs b/osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs similarity index 65% rename from osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs rename to osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs index bc913030dd..37c21457bc 100644 --- a/osu.Game/Online/Multiplayer/GetRoomScoresRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs @@ -1,17 +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 osu.Game.Online.API; -using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Online.Multiplayer { - public class GetRoomScoresRequest : APIRequest> + public class GetRoomLeaderboardRequest : APIRequest { private readonly int roomId; - public GetRoomScoresRequest(int roomId) + public GetRoomLeaderboardRequest(int roomId) { this.roomId = roomId; } diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index 01137dad43..56381dccb6 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Multi.Match.Components if (roomId.Value == null) return null; - var req = new GetRoomScoresRequest(roomId.Value ?? 0); + var req = new GetRoomLeaderboardRequest(roomId.Value ?? 0); req.Success += r => { From 77698ec31e8cae2550c2dac2d68bae51ab2805a6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 19:54:57 +0900 Subject: [PATCH 2942/6909] Add support for showing own top score in timeshift --- osu.Game/Online/Multiplayer/APILeaderboard.cs | 18 ++++++++++++++++++ .../Multi/Match/Components/MatchLeaderboard.cs | 6 ++---- 2 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/APILeaderboard.cs diff --git a/osu.Game/Online/Multiplayer/APILeaderboard.cs b/osu.Game/Online/Multiplayer/APILeaderboard.cs new file mode 100644 index 0000000000..96fe7cefb0 --- /dev/null +++ b/osu.Game/Online/Multiplayer/APILeaderboard.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.API.Requests.Responses; + +namespace osu.Game.Online.Multiplayer +{ + public class APILeaderboard + { + [JsonProperty("leaderboard")] + public List Leaderboard; + + [JsonProperty("own_score")] + public APIUserScoreAggregate OwnScore; + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index 56381dccb6..847f3a7b55 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -14,8 +14,6 @@ namespace osu.Game.Screens.Multi.Match.Components { public class MatchLeaderboard : Leaderboard { - public Action> ScoresLoaded; - [Resolved(typeof(Room), nameof(Room.RoomID))] private Bindable roomId { get; set; } @@ -43,8 +41,8 @@ namespace osu.Game.Screens.Multi.Match.Components req.Success += r => { - scoresCallback?.Invoke(r); - ScoresLoaded?.Invoke(r); + scoresCallback?.Invoke(r.Leaderboard); + TopScore = r.OwnScore; }; return req; From 6ed191786f978e3b6bb943bc8e33633ac17ff80e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 20:01:59 +0900 Subject: [PATCH 2943/6909] Add support for position --- .../Online/API/Requests/Responses/APIUserScoreAggregate.cs | 4 ++++ osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs index 0bba6a93bd..bcc8721400 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs @@ -33,6 +33,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty("user")] public User User { get; set; } + [JsonProperty("position")] + public int? Position { get; set; } + public ScoreInfo CreateScoreInfo() => new ScoreInfo { @@ -40,6 +43,7 @@ namespace osu.Game.Online.API.Requests.Responses PP = PP, TotalScore = TotalScore, User = User, + Position = Position }; } } diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index 847f3a7b55..7d5968202c 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Multi.Match.Components protected override LeaderboardScore CreateDrawableScore(APIUserScoreAggregate model, int index) => new MatchLeaderboardScore(model, index); - protected override LeaderboardScore CreateDrawableTopScore(APIUserScoreAggregate model) => new MatchLeaderboardScore(model, 0); + protected override LeaderboardScore CreateDrawableTopScore(APIUserScoreAggregate model) => new MatchLeaderboardScore(model, model.Position ?? 0); } public enum MatchLeaderboardScope From d22de26afb354e82769fd2ffdd3a587ae1a32f04 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 20:08:36 +0900 Subject: [PATCH 2944/6909] Add whitespace --- osu.Game/Online/Leaderboards/UserTopScoreContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs index ffa7fa2c0b..ab4210251e 100644 --- a/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs +++ b/osu.Game/Online/Leaderboards/UserTopScoreContainer.cs @@ -31,6 +31,7 @@ namespace osu.Game.Online.Leaderboards public UserTopScoreContainer(Func createScoreDelegate) { this.createScoreDelegate = createScoreDelegate; + RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; From 8cf26979fb11ec81199cf87378b20134a809d816 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 20:16:28 +0900 Subject: [PATCH 2945/6909] Allow null user score --- osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs index a78d8e3be0..8ddae67dba 100644 --- a/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs +++ b/osu.Game/Screens/Select/Leaderboards/BeatmapLeaderboard.cs @@ -160,7 +160,7 @@ namespace osu.Game.Screens.Select.Leaderboards req.Success += r => { scoresCallback?.Invoke(r.Scores.Select(s => s.CreateScoreInfo(rulesets))); - TopScore = r.UserScore.CreateScoreInfo(rulesets); + TopScore = r.UserScore?.CreateScoreInfo(rulesets); }; return req; From 61d580b6ba9841793e81600d0c228ea7848bff3e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 20:17:23 +0900 Subject: [PATCH 2946/6909] Don't highlight top score --- osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs | 2 +- .../Screens/Multi/Match/Components/MatchLeaderboardScore.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index 7d5968202c..50afbb39fe 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Multi.Match.Components protected override LeaderboardScore CreateDrawableScore(APIUserScoreAggregate model, int index) => new MatchLeaderboardScore(model, index); - protected override LeaderboardScore CreateDrawableTopScore(APIUserScoreAggregate model) => new MatchLeaderboardScore(model, model.Position ?? 0); + protected override LeaderboardScore CreateDrawableTopScore(APIUserScoreAggregate model) => new MatchLeaderboardScore(model, model.Position ?? 0, false); } public enum MatchLeaderboardScope diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs index 73a40d9579..c4e2b332b3 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs @@ -14,8 +14,8 @@ namespace osu.Game.Screens.Multi.Match.Components { private readonly APIUserScoreAggregate score; - public MatchLeaderboardScore(APIUserScoreAggregate score, int rank) - : base(score.CreateScoreInfo(), rank) + public MatchLeaderboardScore(APIUserScoreAggregate score, int rank, bool allowHighlight = true) + : base(score.CreateScoreInfo(), rank, allowHighlight) { this.score = score; } From 5e77e8cfcf74e642c2076773799ab355097fa22b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 31 Aug 2020 20:21:57 +0900 Subject: [PATCH 2947/6909] Reduce min size of chat --- osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 7c2d5cf85d..0d2adeb27c 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -172,7 +172,7 @@ namespace osu.Game.Screens.Multi.Match new Dimension(GridSizeMode.AutoSize), new Dimension(), new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 240), + new Dimension(GridSizeMode.Relative, size: 0.4f, minSize: 120), } }, null From 3b22b891d13e9d53d0266d234363dfd2362436c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Aug 2020 14:28:45 +0200 Subject: [PATCH 2948/6909] Add failing test cases --- .../NonVisual/Ranking/UnstableRateTest.cs | 43 +++++++++++++++++++ .../Ranking/Statistics/SimpleStatisticItem.cs | 9 +++- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs new file mode 100644 index 0000000000..bf4145754a --- /dev/null +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.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.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Ranking.Statistics; + +namespace osu.Game.Tests.NonVisual.Ranking +{ + [TestFixture] + public class UnstableRateTest + { + [Test] + public void TestDistributedHits() + { + var events = Enumerable.Range(-5, 11) + .Select(t => new HitEvent(t - 5, HitResult.Great, new HitObject(), null, null)); + + var unstableRate = new UnstableRate(events); + + Assert.IsTrue(Precision.AlmostEquals(unstableRate.Value, 10 * Math.Sqrt(10))); + } + + [Test] + public void TestMissesAndEmptyWindows() + { + var events = new[] + { + new HitEvent(-100, HitResult.Miss, new HitObject(), null, null), + new HitEvent(0, HitResult.Great, new HitObject(), null, null), + new HitEvent(200, HitResult.Meh, new HitObject { HitWindows = HitWindows.Empty }, null, null), + }; + + var unstableRate = new UnstableRate(events); + + Assert.IsTrue(Precision.AlmostEquals(0, unstableRate.Value)); + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs index 3d9ba2f225..6fe7e4eda8 100644 --- a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs @@ -59,12 +59,19 @@ namespace osu.Game.Screens.Ranking.Statistics /// public class SimpleStatisticItem : SimpleStatisticItem { + private TValue value; + /// /// The statistic's value to be displayed. /// public new TValue Value { - set => base.Value = DisplayValue(value); + get => value; + set + { + this.value = value; + base.Value = DisplayValue(value); + } } /// From 3ca2a7767a04d2911d8244bb1d2755747e099a45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Aug 2020 14:29:01 +0200 Subject: [PATCH 2949/6909] Exclude misses and empty window hits from UR calculation --- osu.Game/Screens/Ranking/Statistics/UnstableRate.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs index 5b368c3e8d..18a2238784 100644 --- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -20,7 +20,8 @@ namespace osu.Game.Screens.Ranking.Statistics public UnstableRate(IEnumerable hitEvents) : base("Unstable Rate") { - var timeOffsets = hitEvents.Select(ev => ev.TimeOffset).ToArray(); + var timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss) + .Select(ev => ev.TimeOffset).ToArray(); Value = 10 * standardDeviation(timeOffsets); } From 0980f97ea2c8da99030f6f9d7b16425c35865ba5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Aug 2020 16:06:24 +0200 Subject: [PATCH 2950/6909] Replace precision check with absolute equality assert --- osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs index bf4145754a..ad6f01881b 100644 --- a/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs +++ b/osu.Game.Tests/NonVisual/Ranking/UnstableRateTest.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.NonVisual.Ranking var unstableRate = new UnstableRate(events); - Assert.IsTrue(Precision.AlmostEquals(0, unstableRate.Value)); + Assert.AreEqual(0, unstableRate.Value); } } } From fde4b03dabe1f58d871f7785f431a6c68f5b5ee5 Mon Sep 17 00:00:00 2001 From: Pavle Aleksov Date: Mon, 31 Aug 2020 16:21:00 +0200 Subject: [PATCH 2951/6909] added spinner duration check - skip HitObjectReplay if duration is 0 --- osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 4cb2cd6539..5a439734c6 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -156,6 +156,9 @@ namespace osu.Game.Rulesets.Osu.Replays // TODO: Shouldn't the spinner always spin in the same direction? if (h is Spinner) { + if ((h as Spinner).Duration == 0) + return; + calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection); Vector2 spinCentreOffset = SPINNER_CENTRE - ((OsuReplayFrame)Frames[^1]).Position; From 0655fc14737c76def98ebb375329044c4ff0392b Mon Sep 17 00:00:00 2001 From: PajLe Date: Mon, 31 Aug 2020 16:50:31 +0200 Subject: [PATCH 2952/6909] changed comparing Duration to autoplay's reactionTime instead of 0 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 5a439734c6..9ef2ff9ebb 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -154,9 +154,9 @@ namespace osu.Game.Rulesets.Osu.Replays // The startPosition for the slider should not be its .Position, but the point on the circle whose tangent crosses the current cursor position // We also modify spinnerDirection so it spins in the direction it enters the spin circle, to make a smooth transition. // TODO: Shouldn't the spinner always spin in the same direction? - if (h is Spinner) + if (h is Spinner spinner) { - if ((h as Spinner).Duration == 0) + if (spinner.Duration < reactionTime) return; calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection); From 69ec2a76ef1586f76bb26658fec7ab183cc7762e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Aug 2020 17:20:45 +0200 Subject: [PATCH 2953/6909] Replace reaction time check with spins required check --- osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 9ef2ff9ebb..76b2631894 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -156,7 +156,8 @@ namespace osu.Game.Rulesets.Osu.Replays // TODO: Shouldn't the spinner always spin in the same direction? if (h is Spinner spinner) { - if (spinner.Duration < reactionTime) + // spinners with 0 spins required will auto-complete - don't bother + if (spinner.SpinsRequired == 0) return; calcSpinnerStartPosAndDirection(((OsuReplayFrame)Frames[^1]).Position, out startPosition, out spinnerDirection); From eafa97af17a1ef203ccd4f25759401ccce169cc0 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 31 Aug 2020 17:23:42 +0200 Subject: [PATCH 2954/6909] Revert changes done to SkinConfiguration and IHasCustomColours --- osu.Game/Beatmaps/Formats/IHasCustomColours.cs | 2 +- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 2 +- osu.Game/Skinning/SkinConfiguration.cs | 8 ++------ 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs index 1ac5ca83cb..dba3a37545 100644 --- a/osu.Game/Beatmaps/Formats/IHasCustomColours.cs +++ b/osu.Game/Beatmaps/Formats/IHasCustomColours.cs @@ -8,6 +8,6 @@ namespace osu.Game.Beatmaps.Formats { public interface IHasCustomColours { - IDictionary CustomColours { get; } + Dictionary CustomColours { get; } } } diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 7972cc7d06..65d5851455 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -23,7 +23,7 @@ namespace osu.Game.Skinning public readonly int Keys; - public IDictionary CustomColours { get; } = new SortedDictionary(); + public Dictionary CustomColours { get; set; } = new Dictionary(); public Dictionary ImageLookups = new Dictionary(); diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index 18d970dd64..2857ad3824 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.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 System.Linq; using osu.Game.Beatmaps.Formats; using osuTK.Graphics; @@ -12,7 +10,7 @@ namespace osu.Game.Skinning /// /// An empty skin configuration. /// - public class SkinConfiguration : IEquatable, IHasComboColours, IHasCustomColours + public class SkinConfiguration : IHasComboColours, IHasCustomColours { public readonly SkinInfo SkinInfo = new SkinInfo(); @@ -47,10 +45,8 @@ namespace osu.Game.Skinning public void AddComboColours(params Color4[] colours) => comboColours.AddRange(colours); - public IDictionary CustomColours { get; } = new SortedDictionary(); + public Dictionary CustomColours { get; set; } = new Dictionary(); public readonly SortedDictionary ConfigDictionary = new SortedDictionary(); - - public bool Equals(SkinConfiguration other) => other != null && ConfigDictionary.SequenceEqual(other.ConfigDictionary) && ((ComboColours == null && other.ComboColours == null) || ComboColours.SequenceEqual(other.ComboColours)) && CustomColours.SequenceEqual(other.CustomColours); } } From 1484e78654743b8e68ead43811ed5735d681b13d Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 31 Aug 2020 17:24:00 +0200 Subject: [PATCH 2955/6909] Update xmldoc --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 89a776dd31..f725d55970 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -195,7 +195,7 @@ namespace osu.Game.Beatmaps /// /// The to save the content against. The file referenced by will be replaced. /// The content to write. - /// The beatmap content to write, or null if not to be changed. + /// The beatmap content to write, null if to be omitted. public void Save(BeatmapInfo info, IBeatmap beatmapContent, IBeatmapSkin beatmapSkin = null) { var setInfo = QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == info.ID)); From fb37a14d577416754f17a569b9658989d7327c07 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 31 Aug 2020 17:24:03 +0200 Subject: [PATCH 2956/6909] Update LegacyBeatmapEncoder.cs --- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index cae6a43cd4..53ce1c831c 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -25,6 +25,8 @@ namespace osu.Game.Beatmaps.Formats public const int LATEST_VERSION = 128; private readonly IBeatmap beatmap; + + [CanBeNull] private readonly IBeatmapSkin skin; /// @@ -64,7 +66,7 @@ namespace osu.Game.Beatmaps.Formats handleControlPoints(writer); writer.WriteLine(); - handleComboColours(writer); + handleColours(writer); writer.WriteLine(); handleHitObjects(writer); @@ -209,9 +211,9 @@ namespace osu.Game.Beatmaps.Formats } } - private void handleComboColours(TextWriter writer) + private void handleColours(TextWriter writer) { - var colours = skin.GetConfig>(GlobalSkinColours.ComboColours)?.Value; + var colours = skin?.GetConfig>(GlobalSkinColours.ComboColours)?.Value; if (colours == null || colours.Count == 0) return; From a893aa8af86b5037dc1662adfe789112a61afea7 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 31 Aug 2020 17:24:24 +0200 Subject: [PATCH 2957/6909] Cut down changes done to LegacyBeatmapEncoderTest --- .../Formats/LegacyBeatmapEncoderTest.cs | 71 +++++-------------- 1 file changed, 16 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index a8a3f266fc..bcc5970a27 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -7,10 +7,8 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Audio.Track; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Beatmaps; @@ -44,7 +42,19 @@ namespace osu.Game.Tests.Beatmaps.Formats sort(decodedAfterEncode); Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); - Assert.IsTrue(decodedAfterEncode.beatmapSkin.Configuration.Equals(decoded.beatmapSkin.Configuration)); + Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration)); + } + + private bool areComboColoursEqual(IHasComboColours a, IHasComboColours b) + { + // equal to null, no need to SequenceEqual + if (a.ComboColours == null && b.ComboColours == null) + return true; + + if (a.ComboColours == null || b.ComboColours == null) + return false; + + return a.ComboColours.SequenceEqual(b.ComboColours); } private void sort((IBeatmap beatmap, IBeatmapSkin beatmapSkin) tuple) @@ -62,63 +72,14 @@ namespace osu.Game.Tests.Beatmaps.Formats using (var reader = new LineBufferedReader(stream)) { var beatmap = new LegacyBeatmapDecoder { ApplyOffsets = false }.Decode(reader); - - using (var rs = new MemoryBeatmapResourceStore(stream, name)) - { - var beatmapSkin = new TestLegacySkin(beatmap, rs, name); - return (convert(beatmap), beatmapSkin); - } + var beatmapSkin = new TestLegacySkin(beatmaps_resource_store, name); + return (convert(beatmap), beatmapSkin); } } - private class MemoryBeatmapResourceStore : IResourceStore - { - private readonly Stream beatmapData; - private readonly string beatmapName; - - public MemoryBeatmapResourceStore(Stream beatmapData, string beatmapName) - { - this.beatmapData = beatmapData; - this.beatmapName = beatmapName; - } - - public void Dispose() => beatmapData.Dispose(); - - public byte[] Get(string name) - { - if (name != beatmapName) - return null; - - byte[] buffer = new byte[beatmapData.Length]; - beatmapData.Read(buffer, 0, buffer.Length); - return buffer; - } - - public async Task GetAsync(string name) - { - if (name != beatmapName) - return null; - - byte[] buffer = new byte[beatmapData.Length]; - await beatmapData.ReadAsync(buffer.AsMemory()); - return buffer; - } - - public Stream GetStream(string name) - { - if (name != beatmapName) - return null; - - beatmapData.Seek(0, SeekOrigin.Begin); - return beatmapData; - } - - public IEnumerable GetAvailableResources() => beatmapName.Yield(); - } - private class TestLegacySkin : LegacySkin, IBeatmapSkin { - public TestLegacySkin(Beatmap beatmap, IResourceStore storage, string fileName) + public TestLegacySkin(IResourceStore storage, string fileName) : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName) { } From a290f7eeec4ecefb46dc4288eb98f94c909f492b Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 31 Aug 2020 17:34:18 +0200 Subject: [PATCH 2958/6909] Revert left-over type change in SkinConfiguration --- osu.Game/Skinning/SkinConfiguration.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index 2857ad3824..a55870aa6d 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -47,6 +47,6 @@ namespace osu.Game.Skinning public Dictionary CustomColours { get; set; } = new Dictionary(); - public readonly SortedDictionary ConfigDictionary = new SortedDictionary(); + public readonly Dictionary ConfigDictionary = new Dictionary(); } } From 3cc169c933f2e4518fb9756d7905641d4fcf4167 Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 31 Aug 2020 17:48:36 +0200 Subject: [PATCH 2959/6909] Remove set from properties in SkinConfiguration classes I don't get why this wasn't resolved in the first place when this file was originally written. *sigh* --- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 2 +- osu.Game/Skinning/SkinConfiguration.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 65d5851455..35a6140cbc 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -23,7 +23,7 @@ namespace osu.Game.Skinning public readonly int Keys; - public Dictionary CustomColours { get; set; } = new Dictionary(); + public Dictionary CustomColours { get; } = new Dictionary(); public Dictionary ImageLookups = new Dictionary(); diff --git a/osu.Game/Skinning/SkinConfiguration.cs b/osu.Game/Skinning/SkinConfiguration.cs index a55870aa6d..25a924c929 100644 --- a/osu.Game/Skinning/SkinConfiguration.cs +++ b/osu.Game/Skinning/SkinConfiguration.cs @@ -45,7 +45,7 @@ namespace osu.Game.Skinning public void AddComboColours(params Color4[] colours) => comboColours.AddRange(colours); - public Dictionary CustomColours { get; set; } = new Dictionary(); + public Dictionary CustomColours { get; } = new Dictionary(); public readonly Dictionary ConfigDictionary = new Dictionary(); } From 9b3a48ee5e3426c8474232518755b429d563ff5d Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Mon, 31 Aug 2020 18:29:46 +0200 Subject: [PATCH 2960/6909] Revert "Add marker interface for beatmap skins" --- .../TestSceneLegacyBeatmapSkin.cs | 2 +- .../TestSceneSkinFallbacks.cs | 18 +++++------------- .../Gameplay/TestSceneStoryboardSamples.cs | 4 ++-- .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/IWorkingBeatmap.cs | 4 ++-- osu.Game/Beatmaps/WorkingBeatmap.cs | 8 ++++---- .../Skinning/BeatmapSkinProvidingContainer.cs | 4 ++-- osu.Game/Skinning/DefaultBeatmapSkin.cs | 9 --------- osu.Game/Skinning/IBeatmapSkin.cs | 12 ------------ osu.Game/Skinning/LegacyBeatmapSkin.cs | 2 +- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 2 +- 11 files changed, 19 insertions(+), 48 deletions(-) delete mode 100644 osu.Game/Skinning/DefaultBeatmapSkin.cs delete mode 100644 osu.Game/Skinning/IBeatmapSkin.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index 03d18cefef..3ff37c4147 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.Osu.Tests this.hasColours = hasColours; } - protected override IBeatmapSkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours); + protected override ISkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours); } private class TestBeatmapSkin : LegacyBeatmapSkin diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 64da80a88e..075bf314bc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Osu.Tests public class TestSceneSkinFallbacks : TestSceneOsuPlayer { private readonly TestSource testUserSkin; - private readonly BeatmapTestSource testBeatmapSkin; + private readonly TestSource testBeatmapSkin; public TestSceneSkinFallbacks() { testUserSkin = new TestSource("user"); - testBeatmapSkin = new BeatmapTestSource(); + testBeatmapSkin = new TestSource("beatmap"); } [Test] @@ -80,15 +80,15 @@ namespace osu.Game.Rulesets.Osu.Tests public class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap { - private readonly IBeatmapSkin skin; + private readonly ISkinSource skin; - public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, IBeatmapSkin skin) + public CustomSkinWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock frameBasedClock, AudioManager audio, ISkinSource skin) : base(beatmap, storyboard, frameBasedClock, audio) { this.skin = skin; } - protected override IBeatmapSkin GetSkin() => skin; + protected override ISkin GetSkin() => skin; } public class SkinProvidingPlayer : TestPlayer @@ -112,14 +112,6 @@ namespace osu.Game.Rulesets.Osu.Tests } } - private class BeatmapTestSource : TestSource, IBeatmapSkin - { - public BeatmapTestSource() - : base("beatmap") - { - } - } - public class TestSource : ISkinSource { private readonly string identifier; diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index bc9528beb6..b30870d057 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -116,7 +116,7 @@ namespace osu.Game.Tests.Gameplay AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate); } - private class TestSkin : LegacySkin, IBeatmapSkin + private class TestSkin : LegacySkin { public TestSkin(string resourceName, AudioManager audioManager) : base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), audioManager, "skin.ini") @@ -156,7 +156,7 @@ namespace osu.Game.Tests.Gameplay this.audio = audio; } - protected override IBeatmapSkin GetSkin() => new TestSkin("test-sample", audio); + protected override ISkin GetSkin() => new TestSkin("test-sample", audio); } private class TestDrawableStoryboardSample : DrawableStoryboardSample diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 44728cc251..39c5ccab27 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -140,7 +140,7 @@ namespace osu.Game.Beatmaps return storyboard; } - protected override IBeatmapSkin GetSkin() + protected override ISkin GetSkin() { try { diff --git a/osu.Game/Beatmaps/IWorkingBeatmap.cs b/osu.Game/Beatmaps/IWorkingBeatmap.cs index aac41725a9..31975157a0 100644 --- a/osu.Game/Beatmaps/IWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/IWorkingBeatmap.cs @@ -42,9 +42,9 @@ namespace osu.Game.Beatmaps Storyboard Storyboard { get; } /// - /// Retrieves the which this provides. + /// Retrieves the which this provides. /// - IBeatmapSkin Skin { get; } + ISkin Skin { get; } /// /// Constructs a playable from using the applicable converters for a specific . diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 163b62a55c..b4bcf285b9 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -44,7 +44,7 @@ namespace osu.Game.Beatmaps background = new RecyclableLazy(GetBackground, BackgroundStillValid); waveform = new RecyclableLazy(GetWaveform); storyboard = new RecyclableLazy(GetStoryboard); - skin = new RecyclableLazy(GetSkin); + skin = new RecyclableLazy(GetSkin); total_count.Value++; } @@ -275,10 +275,10 @@ namespace osu.Game.Beatmaps private readonly RecyclableLazy storyboard; public bool SkinLoaded => skin.IsResultAvailable; - public IBeatmapSkin Skin => skin.Value; + public ISkin Skin => skin.Value; - protected virtual IBeatmapSkin GetSkin() => new DefaultBeatmapSkin(); - private readonly RecyclableLazy skin; + protected virtual ISkin GetSkin() => new DefaultSkin(); + private readonly RecyclableLazy skin; /// /// Transfer pieces of a beatmap to a new one, where possible, to save on loading. diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs index 346bfe53b8..40335db697 100644 --- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs +++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs @@ -11,7 +11,7 @@ namespace osu.Game.Skinning /// /// A container which overrides existing skin options with beatmap-local values. /// - public class BeatmapSkinProvidingContainer : SkinProvidingContainer, IBeatmapSkin + public class BeatmapSkinProvidingContainer : SkinProvidingContainer { private readonly Bindable beatmapSkins = new Bindable(); private readonly Bindable beatmapHitsounds = new Bindable(); @@ -21,7 +21,7 @@ namespace osu.Game.Skinning protected override bool AllowTextureLookup(string componentName) => beatmapSkins.Value; protected override bool AllowSampleLookup(ISampleInfo componentName) => beatmapHitsounds.Value; - public BeatmapSkinProvidingContainer(IBeatmapSkin skin) + public BeatmapSkinProvidingContainer(ISkin skin) : base(skin) { } diff --git a/osu.Game/Skinning/DefaultBeatmapSkin.cs b/osu.Game/Skinning/DefaultBeatmapSkin.cs deleted file mode 100644 index 7b5ccd45c3..0000000000 --- a/osu.Game/Skinning/DefaultBeatmapSkin.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.Skinning -{ - public class DefaultBeatmapSkin : DefaultSkin, IBeatmapSkin - { - } -} diff --git a/osu.Game/Skinning/IBeatmapSkin.cs b/osu.Game/Skinning/IBeatmapSkin.cs deleted file mode 100644 index 91caaed557..0000000000 --- a/osu.Game/Skinning/IBeatmapSkin.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. - -namespace osu.Game.Skinning -{ - /// - /// Marker interface for skins that originate from beatmaps. - /// - public interface IBeatmapSkin : ISkin - { - } -} diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index d53349dd11..d647bc4a2d 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Skinning { - public class LegacyBeatmapSkin : LegacySkin, IBeatmapSkin + public class LegacyBeatmapSkin : LegacySkin { protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index db080d889f..ab4fb38657 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Beatmaps this.resourceStore = resourceStore; } - protected override IBeatmapSkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); + protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); } } } From 2e2f26449d1304e6bcd0af00a7aa2e130bb9919d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Aug 2020 19:23:19 +0200 Subject: [PATCH 2961/6909] Change anchoring to TopRight --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 2b2c40411d..44d7d0f765 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -227,13 +227,13 @@ namespace osu.Game.Screens.Select { createStarRatingDisplay(beatmapInfo).With(display => { - display.Anchor = Anchor.CentreRight; - display.Origin = Anchor.CentreRight; + display.Anchor = Anchor.TopRight; + display.Origin = Anchor.TopRight; }), StatusPill = new BeatmapSetOnlineStatusPill { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, TextSize = 11, TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, Status = beatmapInfo.Status, From 876fd21230a401d0d75877a3d681d167c2f38a9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 31 Aug 2020 19:31:47 +0200 Subject: [PATCH 2962/6909] Apply shear to right-anchored items --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 44d7d0f765..ad977c70b5 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -223,17 +223,20 @@ namespace osu.Game.Screens.Select Direction = FillDirection.Vertical, Padding = new MarginPadding { Top = 14, Right = shear_width / 2 }, AutoSizeAxes = Axes.Both, + Shear = wedged_container_shear, Children = new[] { createStarRatingDisplay(beatmapInfo).With(display => { display.Anchor = Anchor.TopRight; display.Origin = Anchor.TopRight; + display.Shear = -wedged_container_shear; }), StatusPill = new BeatmapSetOnlineStatusPill { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + Shear = -wedged_container_shear, TextSize = 11, TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, Status = beatmapInfo.Status, From c8aa197e5b472f9b3389382106253d0eeea61cb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Sep 2020 11:36:18 +0900 Subject: [PATCH 2963/6909] 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 1a76a24496..d4a6d6759e 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 d1e2033596..5cc2f61e86 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 9b25eaab41..e7addc1c2c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From d45a1521a1e6441ec47f391c2575c5ac79239fb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Sep 2020 11:56:23 +0900 Subject: [PATCH 2964/6909] Update BindableList usages --- osu.Game/Overlays/ChatOverlay.cs | 63 ++++++++++--------- .../Sections/Graphics/LayoutSettings.cs | 3 +- .../Screens/Edit/Timing/ControlPointTable.cs | 3 +- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 3 +- .../Multi/Lounge/Components/RoomsContainer.cs | 18 +++++- 5 files changed, 53 insertions(+), 37 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 5ba55f6d45..692175603c 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osuTK; using osuTK.Graphics; @@ -218,14 +219,13 @@ namespace osu.Game.Overlays Schedule(() => { // TODO: consider scheduling bindable callbacks to not perform when overlay is not present. - channelManager.JoinedChannels.ItemsAdded += onChannelAddedToJoinedChannels; - channelManager.JoinedChannels.ItemsRemoved += onChannelRemovedFromJoinedChannels; + channelManager.JoinedChannels.CollectionChanged += joinedChannelsChanged; + foreach (Channel channel in channelManager.JoinedChannels) ChannelTabControl.AddChannel(channel); - channelManager.AvailableChannels.ItemsAdded += availableChannelsChanged; - channelManager.AvailableChannels.ItemsRemoved += availableChannelsChanged; - ChannelSelectionOverlay.UpdateAvailableChannels(channelManager.AvailableChannels); + channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged; + availableChannelsChanged(null, null); currentChannel = channelManager.CurrentChannel.GetBoundCopy(); currentChannel.BindValueChanged(currentChannelChanged, true); @@ -384,34 +384,41 @@ namespace osu.Game.Overlays base.PopOut(); } - private void onChannelAddedToJoinedChannels(IEnumerable channels) + private void joinedChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) { - foreach (Channel channel in channels) - ChannelTabControl.AddChannel(channel); - } - - private void onChannelRemovedFromJoinedChannels(IEnumerable channels) - { - foreach (Channel channel in channels) + switch (args.Action) { - ChannelTabControl.RemoveChannel(channel); + case NotifyCollectionChangedAction.Add: + foreach (Channel channel in args.NewItems.Cast()) + ChannelTabControl.AddChannel(channel); + break; - var loaded = loadedChannels.Find(c => c.Channel == channel); + case NotifyCollectionChangedAction.Remove: + foreach (Channel channel in args.OldItems.Cast()) + { + ChannelTabControl.RemoveChannel(channel); - if (loaded != null) - { - loadedChannels.Remove(loaded); + var loaded = loadedChannels.Find(c => c.Channel == channel); - // Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared - // to ensure that the previous channel doesn't get updated after it's disposed - currentChannelContainer.Remove(loaded); - loaded.Dispose(); - } + if (loaded != null) + { + loadedChannels.Remove(loaded); + + // Because the container is only cleared in the async load callback of a new channel, it is forcefully cleared + // to ensure that the previous channel doesn't get updated after it's disposed + currentChannelContainer.Remove(loaded); + loaded.Dispose(); + } + } + + break; } } - private void availableChannelsChanged(IEnumerable channels) - => ChannelSelectionOverlay.UpdateAvailableChannels(channelManager.AvailableChannels); + private void availableChannelsChanged(object sender, NotifyCollectionChangedEventArgs args) + { + ChannelSelectionOverlay.UpdateAvailableChannels(channelManager.AvailableChannels); + } protected override void Dispose(bool isDisposing) { @@ -420,10 +427,8 @@ namespace osu.Game.Overlays if (channelManager != null) { channelManager.CurrentChannel.ValueChanged -= currentChannelChanged; - channelManager.JoinedChannels.ItemsAdded -= onChannelAddedToJoinedChannels; - channelManager.JoinedChannels.ItemsRemoved -= onChannelRemovedFromJoinedChannels; - channelManager.AvailableChannels.ItemsAdded -= availableChannelsChanged; - channelManager.AvailableChannels.ItemsRemoved -= availableChannelsChanged; + channelManager.JoinedChannels.CollectionChanged -= joinedChannelsChanged; + channelManager.AvailableChannels.CollectionChanged -= availableChannelsChanged; } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 00b7643332..4312b319c0 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -163,8 +163,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything); }, true); - windowModes.ItemsAdded += _ => windowModesChanged(); - windowModes.ItemsRemoved += _ => windowModesChanged(); + windowModes.CollectionChanged += (sender, args) => windowModesChanged(); windowModesChanged(); } diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 5c59cfbfe8..c0c0bcead2 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -112,8 +112,7 @@ namespace osu.Game.Screens.Edit.Timing }; controlPoints = group.ControlPoints.GetBoundCopy(); - controlPoints.ItemsAdded += _ => createChildren(); - controlPoints.ItemsRemoved += _ => createChildren(); + controlPoints.CollectionChanged += (_, __) => createChildren(); createChildren(); } diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index a08a660e7e..8c40c8e721 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -124,8 +124,7 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); - controlGroups.ItemsAdded += _ => createContent(); - controlGroups.ItemsRemoved += _ => createContent(); + controlGroups.CollectionChanged += (sender, args) => createContent(); createContent(); } diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index 447c99039a..321d7b0a19 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -53,8 +54,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components protected override void LoadComplete() { - rooms.ItemsAdded += addRooms; - rooms.ItemsRemoved += removeRooms; + rooms.CollectionChanged += roomsChanged; roomManager.RoomsUpdated += updateSorting; rooms.BindTo(roomManager.Rooms); @@ -82,6 +82,20 @@ namespace osu.Game.Screens.Multi.Lounge.Components }); } + private void roomsChanged(object sender, NotifyCollectionChangedEventArgs args) + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + addRooms(args.NewItems.Cast()); + break; + + case NotifyCollectionChangedAction.Remove: + removeRooms(args.OldItems.Cast()); + break; + } + } + private void addRooms(IEnumerable rooms) { foreach (var room in rooms) From d1f79a6a488a9b8f07f88e35d7b96ac71192a458 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Aug 2020 20:00:24 +0900 Subject: [PATCH 2965/6909] Fix potentially incorrect zoom level getting set on very short audio track --- 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 717d60b4f3..ce2954f301 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, true); } - private float getZoomLevelForVisibleMilliseconds(double milliseconds) => (float)(track.Length / milliseconds); + private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds)); /// /// The timeline's scroll position in the last frame. From 9e3b809cab6f61489f90379327928768778672fb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Sep 2020 15:42:47 +0900 Subject: [PATCH 2966/6909] Rename to user_score to match API --- osu.Game/Online/Multiplayer/APILeaderboard.cs | 4 ++-- osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/APILeaderboard.cs b/osu.Game/Online/Multiplayer/APILeaderboard.cs index 96fe7cefb0..65863d6e0e 100644 --- a/osu.Game/Online/Multiplayer/APILeaderboard.cs +++ b/osu.Game/Online/Multiplayer/APILeaderboard.cs @@ -12,7 +12,7 @@ namespace osu.Game.Online.Multiplayer [JsonProperty("leaderboard")] public List Leaderboard; - [JsonProperty("own_score")] - public APIUserScoreAggregate OwnScore; + [JsonProperty("user_score")] + public APIUserScoreAggregate UserScore; } } diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index 50afbb39fe..5bf61eb4ee 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Multi.Match.Components req.Success += r => { scoresCallback?.Invoke(r.Leaderboard); - TopScore = r.OwnScore; + TopScore = r.UserScore; }; return req; From 26b4226b5538c9f95657fdc985b9a873c763b4f1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Sep 2020 16:55:10 +0900 Subject: [PATCH 2967/6909] Fix ModTimeRamp not working --- osu.Game/Screens/Play/Player.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9be4fd6a65..07be482529 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -52,6 +52,9 @@ namespace osu.Game.Screens.Play public override OverlayActivation InitialOverlayActivationMode => OverlayActivation.UserTriggered; + // We are managing our own adjustments (see OnEntering/OnExiting). + public override bool AllowRateAdjustments => false; + /// /// Whether gameplay should pause when the game window focus is lost. /// @@ -627,6 +630,10 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHUD(HUDOverlay); + + Beatmap.Value.Track.ResetSpeedAdjustments(); + foreach (var mod in Mods.Value.OfType()) + mod.ApplyToTrack(Beatmap.Value.Track); } public override void OnSuspending(IScreen next) @@ -660,6 +667,8 @@ namespace osu.Game.Screens.Play // as we are no longer the current screen, we cannot guarantee the track is still usable. GameplayClockContainer?.StopUsingBeatmapClock(); + Beatmap.Value.Track.ResetSpeedAdjustments(); + fadeOut(); return base.OnExiting(next); } From 7e1844ed773368a4b932ea0e2d7fc87fa0fc53b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Sep 2020 18:07:19 +0900 Subject: [PATCH 2968/6909] Fix track adjusments being reset incorrectly --- osu.Game/Screens/Play/Player.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 07be482529..82c446f5e4 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -80,6 +80,9 @@ namespace osu.Game.Screens.Play [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private MusicController musicController { get; set; } + private SampleChannel sampleRestart; public BreakOverlay BreakOverlay; @@ -631,9 +634,12 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHUD(HUDOverlay); - Beatmap.Value.Track.ResetSpeedAdjustments(); + // Our mods are local copies of the global mods so they need to be re-applied to the track. + // This is done through the music controller (for now), because resetting speed adjustments on the beatmap track also removes adjustments provided by DrawableTrack. + // Todo: In the future, player will receive in a track and will probably not have to worry about this... + musicController.ResetTrackAdjustments(); foreach (var mod in Mods.Value.OfType()) - mod.ApplyToTrack(Beatmap.Value.Track); + mod.ApplyToTrack(musicController.CurrentTrack); } public override void OnSuspending(IScreen next) @@ -667,7 +673,7 @@ namespace osu.Game.Screens.Play // as we are no longer the current screen, we cannot guarantee the track is still usable. GameplayClockContainer?.StopUsingBeatmapClock(); - Beatmap.Value.Track.ResetSpeedAdjustments(); + musicController.ResetTrackAdjustments(); fadeOut(); return base.OnExiting(next); From e4cb7eb964b6b5b7e1072c0a1efa9d613351da59 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Sep 2020 17:28:41 +0900 Subject: [PATCH 2969/6909] Initial structure --- osu.Game/Collections/CollectionManager.cs | 93 +++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 osu.Game/Collections/CollectionManager.cs diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs new file mode 100644 index 0000000000..1058e7b5b8 --- /dev/null +++ b/osu.Game/Collections/CollectionManager.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.IO.Legacy; + +namespace osu.Game.Collections +{ + public class CollectionManager + { + private const string import_from_stable_path = "collection.db"; + + private readonly BeatmapManager beatmaps; + + public CollectionManager(BeatmapManager beatmaps) + { + this.beatmaps = beatmaps; + } + + /// + /// Set a storage with access to an osu-stable install for import purposes. + /// + public Func GetStableStorage { private get; set; } + + /// + /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. + /// + public Task ImportFromStableAsync() + { + var stable = GetStableStorage?.Invoke(); + + if (stable == null) + { + Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); + return Task.CompletedTask; + } + + if (!stable.ExistsDirectory(import_from_stable_path)) + { + // This handles situations like when the user does not have a Skins folder + Logger.Log($"No {import_from_stable_path} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); + return Task.CompletedTask; + } + + return Task.Run(async () => await Import(GetStableImportPaths(GetStableStorage()).Select(f => stable.GetFullPath(f)).ToArray())); + } + + private List readCollection(Stream stream) + { + var result = new List(); + + using (var reader = new SerializationReader(stream)) + { + reader.ReadInt32(); // Version + + int collectionCount = reader.ReadInt32(); + result.Capacity = collectionCount; + + for (int i = 0; i < collectionCount; i++) + { + var collection = new BeatmapCollection { Name = reader.ReadString() }; + + int mapCount = reader.ReadInt32(); + collection.Beatmaps.Capacity = mapCount; + + for (int j = 0; j < mapCount; j++) + { + string checksum = reader.ReadString(); + + var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum); + if (beatmap != null) + collection.Beatmaps.Add(beatmap); + } + } + } + + return result; + } + } + + public class BeatmapCollection + { + public string Name; + + public readonly List Beatmaps = new List(); + } +} From 78648cb90d409878171b9b9cc500a4d9adae28cc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Sep 2020 19:33:06 +0900 Subject: [PATCH 2970/6909] Add reading from local file --- osu.Game/Collections/CollectionManager.cs | 65 +++++++++++++---------- osu.Game/OsuGameBase.cs | 7 +++ 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 1058e7b5b8..302d892efb 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -4,23 +4,33 @@ using System; using System.Collections.Generic; using System.IO; -using System.Threading.Tasks; -using osu.Framework.Logging; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; namespace osu.Game.Collections { - public class CollectionManager + public class CollectionManager : CompositeDrawable { - private const string import_from_stable_path = "collection.db"; + private const string database_name = "collection.db"; - private readonly BeatmapManager beatmaps; + public IBindableList Collections => collections; + private readonly BindableList collections = new BindableList(); - public CollectionManager(BeatmapManager beatmaps) + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [BackgroundDependencyLoader] + private void load(GameHost host) { - this.beatmaps = beatmaps; + if (host.Storage.Exists(database_name)) + { + using (var stream = host.Storage.GetStream(database_name)) + collections.AddRange(readCollection(stream)); + } } /// @@ -31,26 +41,25 @@ namespace osu.Game.Collections /// /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// - public Task ImportFromStableAsync() - { - var stable = GetStableStorage?.Invoke(); - - if (stable == null) - { - Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); - return Task.CompletedTask; - } - - if (!stable.ExistsDirectory(import_from_stable_path)) - { - // This handles situations like when the user does not have a Skins folder - Logger.Log($"No {import_from_stable_path} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); - return Task.CompletedTask; - } - - return Task.Run(async () => await Import(GetStableImportPaths(GetStableStorage()).Select(f => stable.GetFullPath(f)).ToArray())); - } - + // public Task ImportFromStableAsync() + // { + // var stable = GetStableStorage?.Invoke(); + // + // if (stable == null) + // { + // Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); + // return Task.CompletedTask; + // } + // + // if (!stable.ExistsDirectory(database_name)) + // { + // // This handles situations like when the user does not have a Skins folder + // Logger.Log($"No {database_name} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); + // return Task.CompletedTask; + // } + // + // return Task.Run(async () => await Import(GetStableImportPaths(GetStableStorage()).Select(f => stable.GetFullPath(f)).ToArray())); + // } private List readCollection(Stream stream) { var result = new List(); @@ -77,6 +86,8 @@ namespace osu.Game.Collections if (beatmap != null) collection.Beatmaps.Add(beatmap); } + + result.Add(collection); } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 98f60d52d3..3ba164e87f 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -26,6 +26,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Logging; using osu.Game.Audio; +using osu.Game.Collections; using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; @@ -55,6 +56,8 @@ namespace osu.Game protected BeatmapManager BeatmapManager; + protected CollectionManager CollectionManager; + protected ScoreManager ScoreManager; protected SkinManager SkinManager; @@ -222,6 +225,10 @@ namespace osu.Game dependencies.Cache(difficultyManager); AddInternal(difficultyManager); + var collectionManager = new CollectionManager(); + dependencies.Cache(collectionManager); + AddInternal(collectionManager); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); From c2ade44656c3ea7d397d39652c77a70056d8fe1c Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 1 Sep 2020 17:58:06 +0200 Subject: [PATCH 2971/6909] Change types back --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 6 +++--- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 4 ++-- osu.Game/Screens/Edit/EditorBeatmap.cs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index bcc5970a27..613db79242 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Beatmaps.Formats return a.ComboColours.SequenceEqual(b.ComboColours); } - private void sort((IBeatmap beatmap, IBeatmapSkin beatmapSkin) tuple) + private void sort((IBeatmap beatmap, ISkin beatmapSkin) tuple) { // 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 tuple.beatmap.ControlPointInfo.Groups) @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private class TestLegacySkin : LegacySkin, IBeatmapSkin + private class TestLegacySkin : LegacySkin, ISkin { public TestLegacySkin(IResourceStore storage, string fileName) : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName) @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private Stream encodeToLegacy((IBeatmap beatmap, IBeatmapSkin beatmapSkin) fullBeatmap) + private Stream encodeToLegacy((IBeatmap beatmap, ISkin beatmapSkin) fullBeatmap) { var (beatmap, beatmapSkin) = fullBeatmap; var stream = new MemoryStream(); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index f725d55970..a96af68714 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -196,7 +196,7 @@ namespace osu.Game.Beatmaps /// The to save the content against. The file referenced by will be replaced. /// The content to write. /// The beatmap content to write, null if to be omitted. - public void Save(BeatmapInfo info, IBeatmap beatmapContent, IBeatmapSkin beatmapSkin = null) + public void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) { var setInfo = QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == info.ID)); diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 53ce1c831c..80a4d6dea4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -27,14 +27,14 @@ namespace osu.Game.Beatmaps.Formats private readonly IBeatmap beatmap; [CanBeNull] - private readonly IBeatmapSkin skin; + private readonly ISkin skin; /// /// Creates a new . /// /// The beatmap to encode. /// The beatmap's skin, used for encoding combo colours. - public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] IBeatmapSkin skin) + public LegacyBeatmapEncoder(IBeatmap beatmap, [CanBeNull] ISkin skin) { this.beatmap = beatmap; this.skin = skin; diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index a314d50e60..061009e519 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Edit public readonly IBeatmap PlayableBeatmap; - public readonly IBeatmapSkin BeatmapSkin; + public readonly ISkin BeatmapSkin; [Resolved] private BindableBeatDivisor beatDivisor { get; set; } @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Edit private readonly Dictionary> startTimeBindables = new Dictionary>(); - public EditorBeatmap(IBeatmap playableBeatmap, IBeatmapSkin beatmapSkin = null) + public EditorBeatmap(IBeatmap playableBeatmap, ISkin beatmapSkin = null) { PlayableBeatmap = playableBeatmap; BeatmapSkin = beatmapSkin; From 2a7259f7aa76d1dac116af9b6d0f016ab15db2bb Mon Sep 17 00:00:00 2001 From: Craftplacer Date: Tue, 1 Sep 2020 18:15:46 +0200 Subject: [PATCH 2972/6909] Update LegacyBeatmapEncoderTest.cs --- 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 613db79242..6e103af3f0 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -77,7 +77,7 @@ namespace osu.Game.Tests.Beatmaps.Formats } } - private class TestLegacySkin : LegacySkin, ISkin + private class TestLegacySkin : LegacySkin { public TestLegacySkin(IResourceStore storage, string fileName) : base(new SkinInfo { Name = "Test Skin", Creator = "Craftplacer" }, storage, null, fileName) From ba8a4eb6f0ffd493b346701a8d1886796f6736c5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Aug 2020 23:14:29 +0300 Subject: [PATCH 2973/6909] Move osu!catch combo counter display to inside CatcherArea --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 19 ++----------------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index d4a1740c12..409ea6dbc6 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -28,7 +28,8 @@ namespace osu.Game.Rulesets.Catch.UI public const float CENTER_X = WIDTH / 2; internal readonly CatcherArea CatcherArea; - private readonly CatchComboDisplay comboDisplay; + + private CatchComboDisplay comboDisplay => CatcherArea.ComboDisplay; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => // only check the X position; handle all vertical space. @@ -49,22 +50,12 @@ namespace osu.Game.Rulesets.Catch.UI Origin = Anchor.TopLeft, }; - comboDisplay = new CatchComboDisplay - { - RelativeSizeAxes = Axes.None, - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.Centre, - Y = 30f, - }; - InternalChildren = new[] { explodingFruitContainer, CatcherArea.MovableCatcher.CreateProxiedContent(), HitObjectContainer, CatcherArea, - comboDisplay, }; } @@ -81,12 +72,6 @@ namespace osu.Game.Rulesets.Catch.UI fruit.CheckPosition = CheckIfWeCanCatch; } - protected override void Update() - { - base.Update(); - comboDisplay.X = CatcherArea.MovableCatcher.X; - } - private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { var catchObject = (DrawableCatchHitObject)judgedObject; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 03ebf01b9b..9cfb9f41d7 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI public Func> CreateDrawableRepresentation; public readonly Catcher MovableCatcher; + internal readonly CatchComboDisplay ComboDisplay; public Container ExplodingFruitTarget { @@ -34,7 +35,19 @@ namespace osu.Game.Rulesets.Catch.UI public CatcherArea(BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); - Child = MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X }; + Children = new Drawable[] + { + ComboDisplay = new CatchComboDisplay + { + RelativeSizeAxes = Axes.None, + AutoSizeAxes = Axes.Both, + Anchor = Anchor.TopLeft, + Origin = Anchor.Centre, + Margin = new MarginPadding { Bottom = 350f }, + X = CatchPlayfield.CENTER_X + }, + MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X }, + }; } public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) @@ -105,6 +118,8 @@ namespace osu.Game.Rulesets.Catch.UI if (state?.CatcherX != null) MovableCatcher.X = state.CatcherX.Value; + + ComboDisplay.X = MovableCatcher.X; } } } From a0a45010080308898c088430c8aa3e66db4ce98d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 29 Aug 2020 23:23:36 +0300 Subject: [PATCH 2974/6909] Merge remote-tracking branch 'upstream/master' into catch-combo-counter --- .../Mods/TestSceneCatchModRelax.cs | 84 ++++++++++++ .../TestSceneHyperDash.cs | 42 ++++-- .../Beatmaps/CatchBeatmapProcessor.cs | 6 + osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs | 2 +- .../Skinning/CatchLegacySkinTransformer.cs | 8 +- .../Skinning/LegacyFruitPiece.cs | 3 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 9 +- .../UI/CatcherTrailDisplay.cs | 8 +- .../Skinning/ColumnTestContainer.cs | 24 ++-- .../Skinning/ManiaHitObjectTestScene.cs | 4 +- .../Skinning/TestSceneStageBackground.cs | 4 +- .../Skinning/TestSceneStageForeground.cs | 3 +- .../Beatmaps/StageDefinition.cs | 4 +- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 10 ++ osu.Game.Rulesets.Mania/ManiaSkinComponent.cs | 11 +- .../Objects/Drawables/DrawableHoldNote.cs | 92 ++++++++++++- .../Objects/Drawables/DrawableHoldNoteHead.cs | 8 ++ .../Drawables/DrawableManiaHitObject.cs | 2 +- .../Skinning/HitTargetInsetContainer.cs | 46 +++++++ .../Skinning/LegacyBodyPiece.cs | 109 ++++++++++++++-- .../Skinning/LegacyColumnBackground.cs | 65 +-------- .../Skinning/LegacyHitTarget.cs | 7 +- .../Skinning/LegacyKeyArea.cs | 3 + .../Skinning/LegacyStageBackground.cs | 88 ++++++++++++- .../Skinning/ManiaLegacySkinTransformer.cs | 10 +- osu.Game.Rulesets.Mania/UI/Column.cs | 2 - osu.Game.Rulesets.Mania/UI/ColumnFlow.cs | 105 +++++++++++++++ osu.Game.Rulesets.Mania/UI/Stage.cs | 69 ++-------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 52 +++++--- .../Skinning/LegacyMainCirclePiece.cs | 3 +- .../Skinning/LegacySliderBall.cs | 6 +- .../Skinning/LegacyCirclePiece.cs | 2 +- .../Skinning/LegacyDrumRoll.cs | 8 +- osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs | 8 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 33 +++-- .../Beatmaps/BeatmapDifficultyManagerTest.cs | 32 +++++ .../Formats/LegacyScoreDecoderTest.cs | 70 ++++++++++ ...tSceneHitObjectComposerDistanceSnapping.cs | 14 +- .../Filtering/FilterQueryParserTest.cs | 16 ++- .../Resources/Replays/mania-replay.osr | Bin 0 -> 1012 bytes .../Resources/skin-zero-alpha-colour.ini | 5 - osu.Game.Tests/Skins/LegacySkinDecoderTest.cs | 10 -- .../TestSceneBeatmapSetOverlaySuccessRate.cs | 28 ++++ .../Ranking/TestSceneSimpleStatisticTable.cs | 68 ++++++++++ osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 14 +- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 4 - osu.Game/Online/Chat/MessageFormatter.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 13 +- .../Rulesets/Edit/IPositionSnapProvider.cs | 1 + osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs | 26 ++-- osu.Game/Screens/Menu/IntroSequence.cs | 1 + .../Ranking/Statistics/SimpleStatisticItem.cs | 81 ++++++++++++ .../Statistics/SimpleStatisticTable.cs | 123 ++++++++++++++++++ .../Ranking/Statistics/StatisticContainer.cs | 60 +++++---- .../Ranking/Statistics/StatisticItem.cs | 2 +- .../Ranking/Statistics/StatisticsPanel.cs | 6 +- .../Ranking/Statistics/UnstableRate.cs | 39 ++++++ osu.Game/Screens/Select/BeatmapDetails.cs | 2 +- .../Screens/Select/Details/FailRetryGraph.cs | 20 ++- osu.Game/Screens/Select/FilterQueryParser.cs | 3 +- .../Skinning/LegacyColourCompatibility.cs | 46 +++++++ .../Skinning/LegacyManiaSkinConfiguration.cs | 3 + .../LegacyManiaSkinConfigurationLookup.cs | 3 + osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 9 ++ osu.Game/Skinning/LegacySkin.cs | 20 +++ 65 files changed, 1352 insertions(+), 309 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs create mode 100644 osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs create mode 100644 osu.Game.Rulesets.Mania/UI/ColumnFlow.cs create mode 100644 osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs create mode 100644 osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs create mode 100644 osu.Game.Tests/Resources/Replays/mania-replay.osr delete mode 100644 osu.Game.Tests/Resources/skin-zero-alpha-colour.ini create mode 100644 osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs create mode 100644 osu.Game/Screens/Ranking/Statistics/UnstableRate.cs create mode 100644 osu.Game/Skinning/LegacyColourCompatibility.cs diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs new file mode 100644 index 0000000000..1eb0975010 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +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.Rulesets.Objects.Types; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Tests.Mods +{ + public class TestSceneCatchModRelax : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + + [Test] + public void TestModRelax() => CreateModTest(new ModTestData + { + Mod = new CatchModRelax(), + Autoplay = false, + PassCondition = passCondition, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Fruit + { + X = CatchPlayfield.CENTER_X, + StartTime = 0 + }, + new Fruit + { + X = 0, + StartTime = 250 + }, + new Fruit + { + X = CatchPlayfield.WIDTH, + StartTime = 500 + }, + new JuiceStream + { + X = CatchPlayfield.CENTER_X, + StartTime = 750, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) + } + } + } + }); + + private bool passCondition() + { + var playfield = this.ChildrenOfType().Single(); + + switch (Player.ScoreProcessor.Combo.Value) + { + case 0: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre); + break; + + case 1: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomLeft); + break; + + case 2: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.BottomRight); + break; + + case 3: + InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre); + break; + } + + return Player.ScoreProcessor.Combo.Value >= 6; + } + } +} diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 6dab2a0b56..db09b2bc6b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -19,25 +19,43 @@ namespace osu.Game.Rulesets.Catch.Tests { protected override bool Autoplay => true; + private int hyperDashCount; + private bool inHyperDash; + [Test] public void TestHyperDash() { - AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); - AddUntilStep("wait for right movement", () => getCatcher().Scale.X > 0); // don't check hyperdashing as it happens too fast. - - AddUntilStep("wait for left movement", () => getCatcher().Scale.X < 0); - - for (int i = 0; i < 3; i++) + AddStep("reset count", () => { - AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); - AddUntilStep("wait for left hyperdash", () => getCatcher().Scale.X < 0 && getCatcher().HyperDashing); + inHyperDash = false; + hyperDashCount = 0; + + // this needs to be done within the frame stable context due to how quickly hyperdash state changes occur. + Player.DrawableRuleset.FrameStableComponents.OnUpdate += d => + { + var catcher = Player.ChildrenOfType().FirstOrDefault()?.MovableCatcher; + + if (catcher == null) + return; + + if (catcher.HyperDashing != inHyperDash) + { + inHyperDash = catcher.HyperDashing; + if (catcher.HyperDashing) + hyperDashCount++; + } + }; + }); + + AddAssert("First note is hyperdash", () => Beatmap.Value.Beatmap.HitObjects[0] is Fruit f && f.HyperDash); + + for (int i = 0; i < 9; i++) + { + int count = i + 1; + AddUntilStep($"wait for hyperdash #{count}", () => hyperDashCount >= count); } - - AddUntilStep("wait for right hyperdash", () => getCatcher().Scale.X > 0 && getCatcher().HyperDashing); } - private Catcher getCatcher() => Player.ChildrenOfType().First().MovableCatcher; - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) { var beatmap = new Beatmap diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index 15e6e98f5a..a08c5b6fb1 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -212,6 +212,12 @@ namespace osu.Game.Rulesets.Catch.Beatmaps objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; + + // Todo: This is wrong. osu!stable calculated hyperdashes using the full catcher size, excluding the margins. + // This should theoretically cause impossible scenarios, but practically, likely due to the size of the playfield, it doesn't seem possible. + // For now, to bring gameplay (and diffcalc!) completely in-line with stable, this code also uses the full catcher size. + halfCatcherWidth /= Catcher.ALLOWED_CATCH_RANGE; + int lastDirection = 0; double lastExcess = halfCatcherWidth; diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs index c1d24395e4..1e42c6a240 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModRelax.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Mods protected override bool OnMouseMove(MouseMoveEvent e) { - catcher.UpdatePosition(e.MousePosition.X / DrawSize.X); + catcher.UpdatePosition(e.MousePosition.X / DrawSize.X * CatchPlayfield.WIDTH); return base.OnMouseMove(e); } } diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 28cd0fb65b..47224bd195 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Catch.Skinning @@ -71,7 +72,12 @@ namespace osu.Game.Rulesets.Catch.Skinning switch (lookup) { case CatchSkinColour colour: - return Source.GetConfig(new SkinCustomColourLookup(colour)); + var result = (Bindable)Source.GetConfig(new SkinCustomColourLookup(colour)); + if (result == null) + return null; + + result.Value = LegacyColourCompatibility.DisallowZeroAlpha(result.Value); + return (IBindable)result; } return Source.GetConfig(lookup); diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 5be54d3882..381d066750 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -40,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Skinning colouredSprite = new Sprite { Texture = skin.GetTexture(lookupName), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -76,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Skinning { base.LoadComplete(); - accentColour.BindValueChanged(colour => colouredSprite.Colour = colour.NewValue, true); + accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); } } } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 952ff6b0ce..9289a6162c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The width of the catcher which can receive fruit. Equivalent to "catchMargin" in osu-stable. /// - private const float allowed_catch_range = 0.8f; + public const float ALLOWED_CATCH_RANGE = 0.8f; /// /// The drawable catcher for . @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The scale of the catcher. internal static float CalculateCatchWidth(Vector2 scale) - => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * allowed_catch_range; + => CatcherArea.CATCHER_SIZE * Math.Abs(scale.X) * ALLOWED_CATCH_RANGE; /// /// Calculates the width of the area used for attempting catches in gameplay. @@ -285,8 +285,6 @@ namespace osu.Game.Rulesets.Catch.UI private void runHyperDashStateTransition(bool hyperDashing) { - trails.HyperDashTrailsColour = hyperDashColour; - trails.EndGlowSpritesColour = hyperDashEndGlowColour; updateTrailVisibility(); if (hyperDashing) @@ -403,6 +401,9 @@ namespace osu.Game.Rulesets.Catch.UI skin.GetConfig(CatchSkinColour.HyperDashAfterImage)?.Value ?? hyperDashColour; + trails.HyperDashTrailsColour = hyperDashColour; + trails.EndGlowSpritesColour = hyperDashEndGlowColour; + runHyperDashStateTransition(HyperDashing); } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index bab3cb748b..f7e9fd19a7 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Catch.UI private readonly Container hyperDashTrails; private readonly Container endGlowSprites; - private Color4 hyperDashTrailsColour; + private Color4 hyperDashTrailsColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; public Color4 HyperDashTrailsColour { @@ -35,11 +35,11 @@ namespace osu.Game.Rulesets.Catch.UI return; hyperDashTrailsColour = value; - hyperDashTrails.FadeColour(hyperDashTrailsColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + hyperDashTrails.Colour = hyperDashTrailsColour; } } - private Color4 endGlowSpritesColour; + private Color4 endGlowSpritesColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; public Color4 EndGlowSpritesColour { @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.UI return; endGlowSpritesColour = value; - endGlowSprites.FadeColour(endGlowSpritesColour, Catcher.HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + endGlowSprites.Colour = endGlowSpritesColour; } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs index ff4865c71d..8ba58e3af3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ColumnTestContainer.cs @@ -22,18 +22,22 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [Cached] private readonly Column column; - public ColumnTestContainer(int column, ManiaAction action) + public ColumnTestContainer(int column, ManiaAction action, bool showColumn = false) { - this.column = new Column(column) + InternalChildren = new[] { - Action = { Value = action }, - AccentColour = Color4.Orange, - ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd - }; - - InternalChild = content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) - { - RelativeSizeAxes = Axes.Both + this.column = new Column(column) + { + Action = { Value = action }, + AccentColour = Color4.Orange, + ColumnType = column % 2 == 0 ? ColumnType.Even : ColumnType.Odd, + Alpha = showColumn ? 1 : 0 + }, + content = new ManiaInputManager(new ManiaRuleset().RulesetInfo, 4) + { + RelativeSizeAxes = Axes.Both + }, + this.column.TopLevelContainer.CreateProxy() }; } } diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index 18eebada00..d24c81dac6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning Direction = FillDirection.Horizontal, Children = new Drawable[] { - new ColumnTestContainer(0, ManiaAction.Key1) + new ColumnTestContainer(0, ManiaAction.Key1, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning })); }) }, - new ColumnTestContainer(1, ManiaAction.Key2) + new ColumnTestContainer(1, ManiaAction.Key2, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs index 87c84cf89c..a15fb392d6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageBackground.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI.Components; using osu.Game.Skinning; @@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [BackgroundDependencyLoader] private void load() { - SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) + SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: new StageDefinition { Columns = 4 }), + _ => new DefaultStageBackground()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs index 4e99068ed5..bceee1c599 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneStageForeground.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; namespace osu.Game.Rulesets.Mania.Tests.Skinning @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning [BackgroundDependencyLoader] private void load() { - SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) + SetContents(() => new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: new StageDefinition { Columns = 4 }), _ => null) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs index 2557f2acdf..3052fc7d34 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/StageDefinition.cs @@ -21,14 +21,14 @@ 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; + public readonly 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) + public readonly ColumnType GetTypeOfColumn(int column) { if (IsSpecialColumn(column)) return ColumnType.Special; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 2795868c97..f7098faa5d 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -326,6 +326,16 @@ namespace osu.Game.Rulesets.Mania Height = 250 }), } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(score.HitEvents) + })) + } } }; } diff --git a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs b/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs index c0c8505f44..f078345fc1 100644 --- a/osu.Game.Rulesets.Mania/ManiaSkinComponent.cs +++ b/osu.Game.Rulesets.Mania/ManiaSkinComponent.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.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; @@ -14,15 +15,23 @@ namespace osu.Game.Rulesets.Mania /// public readonly int? TargetColumn; + /// + /// The intended for this component. + /// May be null if the component is not a direct member of a . + /// + public readonly StageDefinition? StageDefinition; + /// /// 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) + /// The intended for this component. May be null if the component is not a direct member of a . + public ManiaSkinComponent(ManiaSkinComponents component, int? targetColumn = null, StageDefinition? stageDefinition = null) : base(component) { TargetColumn = targetColumn; + StageDefinition = stageDefinition; } 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 a44f8a8886..549a71daaa 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; @@ -32,6 +33,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables private readonly Container tailContainer; private readonly Container tickContainer; + /// + /// Contains the size of the hold note covering the whole head/tail bounds. The size of this container changes as the hold note is being pressed. + /// + private readonly Container sizingContainer; + + /// + /// Contains the contents of the hold note that should be masked as the hold note is being pressed. Follows changes in the size of . + /// + private readonly Container maskingContainer; + private readonly SkinnableDrawable bodyPiece; /// @@ -44,24 +55,54 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public bool HasBroken { get; private set; } + /// + /// Whether the hold note has been released potentially without having caused a break. + /// + private double? releaseTime; + public DrawableHoldNote(HoldNote hitObject) : base(hitObject) { RelativeSizeAxes = Axes.X; + Container maskedContents; + AddRangeInternal(new Drawable[] { + sizingContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + maskingContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Child = maskedContents = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + } + }, + headContainer = new Container { RelativeSizeAxes = Axes.Both } + } + }, bodyPiece = new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.HoldNoteBody, hitObject.Column), _ => new DefaultBodyPiece { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, }) { RelativeSizeAxes = Axes.X }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, - headContainer = new Container { RelativeSizeAxes = Axes.Both }, tailContainer = new Container { RelativeSizeAxes = Axes.Both }, }); + + maskedContents.AddRange(new[] + { + bodyPiece.CreateProxy(), + tickContainer.CreateProxy(), + tailContainer.CreateProxy(), + }); } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -127,7 +168,16 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.OnDirectionChanged(e); - bodyPiece.Anchor = bodyPiece.Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopLeft : Anchor.BottomLeft; + if (e.NewValue == ScrollingDirection.Up) + { + bodyPiece.Anchor = bodyPiece.Origin = Anchor.TopLeft; + sizingContainer.Anchor = sizingContainer.Origin = Anchor.BottomLeft; + } + else + { + bodyPiece.Anchor = bodyPiece.Origin = Anchor.BottomLeft; + sizingContainer.Anchor = sizingContainer.Origin = Anchor.TopLeft; + } } public override void PlaySamples() @@ -145,9 +195,38 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { base.Update(); - // Make the body piece not lie under the head note + if (Time.Current < releaseTime) + releaseTime = null; + + // Pad the full size container so its contents (i.e. the masking container) reach under the tail. + // This is required for the tail to not be masked away, since it lies outside the bounds of the hold note. + sizingContainer.Padding = new MarginPadding + { + Top = Direction.Value == ScrollingDirection.Down ? -Tail.Height : 0, + Bottom = Direction.Value == ScrollingDirection.Up ? -Tail.Height : 0, + }; + + // Pad the masking container to the starting position of the body piece (half-way under the head). + // This is required to make the body start getting masked immediately as soon as the note is held. + maskingContainer.Padding = new MarginPadding + { + Top = Direction.Value == ScrollingDirection.Up ? Head.Height / 2 : 0, + Bottom = Direction.Value == ScrollingDirection.Down ? Head.Height / 2 : 0, + }; + + // Position and resize the body to lie half-way under the head and the tail notes. bodyPiece.Y = (Direction.Value == ScrollingDirection.Up ? 1 : -1) * Head.Height / 2; bodyPiece.Height = DrawHeight - Head.Height / 2 + Tail.Height / 2; + + // As the note is being held, adjust the size of the sizing container. This has two effects: + // 1. The contained masking container will mask the body and ticks. + // 2. The head note will move along with the new "head position" in the container. + if (Head.IsHit && releaseTime == null) + { + // How far past the hit target this hold note is. Always a positive value. + float yOffset = Math.Max(0, Direction.Value == ScrollingDirection.Up ? -Y : Y); + sizingContainer.Height = Math.Clamp(1 - yOffset / DrawHeight, 0, 1); + } } protected override void UpdateStateTransforms(ArmedState state) @@ -159,7 +238,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Tail.AllJudged) + { ApplyResult(r => r.Type = HitResult.Perfect); + endHold(); + } if (Tail.Result.Type == HitResult.Miss) HasBroken = true; @@ -212,6 +294,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // If the key has been released too early, the user should not receive full score for the release if (!Tail.IsHit) HasBroken = true; + + releaseTime = Time.Current; } private void endHold() diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs index a73fe259e4..cd56b81e10 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Objects.Drawables; + namespace osu.Game.Rulesets.Mania.Objects.Drawables { /// @@ -17,6 +19,12 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); + protected override void UpdateStateTransforms(ArmedState state) + { + // This hitobject should never expire, so this is just a safe maximum. + LifetimeEnd = LifetimeStart + 30000; + } + public override bool OnPressed(ManiaAction action) => false; // Handled by the hold note public override void OnReleased(ManiaAction action) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index a44d8b09aa..ab76a5b8f8 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables break; case ArmedState.Hit: - this.FadeOut(150, Easing.OutQuint); + this.FadeOut(); break; } } diff --git a/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs new file mode 100644 index 0000000000..c8b05ed2f8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.Skinning +{ + public class HitTargetInsetContainer : Container + { + private readonly IBindable direction = new Bindable(); + + protected override Container Content => content; + private readonly Container content; + + private float hitPosition; + + public HitTargetInsetContainer() + { + RelativeSizeAxes = Axes.Both; + + InternalChild = content = new Container { RelativeSizeAxes = Axes.Both }; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin, IScrollingInfo scrollingInfo) + { + hitPosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.HitPosition)?.Value ?? Stage.HIT_TARGET_POSITION; + + direction.BindTo(scrollingInfo.Direction); + direction.BindValueChanged(onDirectionChanged, true); + } + + private void onDirectionChanged(ValueChangedEvent direction) + { + content.Padding = direction.NewValue == ScrollingDirection.Up + ? new MarginPadding { Top = hitPosition } + : new MarginPadding { Bottom = hitPosition }; + } + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 9f716428c0..c0f0fcb4af 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.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; @@ -19,7 +21,14 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly IBindable direction = new Bindable(); private readonly IBindable isHitting = new Bindable(); - private Drawable sprite; + [CanBeNull] + private Drawable bodySprite; + + [CanBeNull] + private Drawable lightContainer; + + [CanBeNull] + private Drawable light; public LegacyBodyPiece() { @@ -32,7 +41,39 @@ namespace osu.Game.Rulesets.Mania.Skinning string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value ?? $"mania-note{FallbackColumnIndex}L"; - sprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => + string lightImage = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightImage)?.Value + ?? "lightingL"; + + float lightScale = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteLightScale)?.Value + ?? 1; + + // 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(lightImage, true, false); + double frameLength = 0; + if (tmp is IFramedAnimation tmpAnimation && tmpAnimation.FrameCount > 0) + frameLength = Math.Max(1000 / 60.0, 170.0 / tmpAnimation.FrameCount); + + light = skin.GetAnimation(lightImage, true, true, frameLength: frameLength).With(d => + { + if (d == null) + return; + + d.Origin = Anchor.Centre; + d.Blending = BlendingParameters.Additive; + d.Scale = new Vector2(lightScale); + }); + + if (light != null) + { + lightContainer = new HitTargetInsetContainer + { + Alpha = 0, + Child = light + }; + } + + bodySprite = skin.GetAnimation(imageName, WrapMode.ClampToEdge, WrapMode.ClampToEdge, true, true).With(d => { if (d == null) return; @@ -47,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Skinning // Todo: Wrap }); - if (sprite != null) - InternalChild = sprite; + if (bodySprite != null) + InternalChild = bodySprite; direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); @@ -60,28 +101,68 @@ namespace osu.Game.Rulesets.Mania.Skinning private void onIsHittingChanged(ValueChangedEvent isHitting) { - if (!(sprite is TextureAnimation animation)) + if (bodySprite is TextureAnimation bodyAnimation) + { + bodyAnimation.GotoFrame(0); + bodyAnimation.IsPlaying = isHitting.NewValue; + } + + if (lightContainer == null) return; - animation.GotoFrame(0); - animation.IsPlaying = isHitting.NewValue; + if (isHitting.NewValue) + { + // Clear the fade out and, more importantly, the removal. + lightContainer.ClearTransforms(); + + // Only add the container if the removal has taken place. + if (lightContainer.Parent == null) + Column.TopLevelContainer.Add(lightContainer); + + // The light must be seeked only after being loaded, otherwise a nullref occurs (https://github.com/ppy/osu-framework/issues/3847). + if (light is TextureAnimation lightAnimation) + lightAnimation.GotoFrame(0); + + lightContainer.FadeIn(80); + } + else + { + lightContainer.FadeOut(120) + .OnComplete(d => Column.TopLevelContainer.Remove(d)); + } } private void onDirectionChanged(ValueChangedEvent direction) { - if (sprite == null) - return; - if (direction.NewValue == ScrollingDirection.Up) { - sprite.Origin = Anchor.BottomCentre; - sprite.Scale = new Vector2(1, -1); + if (bodySprite != null) + { + bodySprite.Origin = Anchor.BottomCentre; + bodySprite.Scale = new Vector2(1, -1); + } + + if (light != null) + light.Anchor = Anchor.TopCentre; } else { - sprite.Origin = Anchor.TopCentre; - sprite.Scale = Vector2.One; + if (bodySprite != null) + { + bodySprite.Origin = Anchor.TopCentre; + bodySprite.Scale = Vector2.One; + } + + if (light != null) + light.Anchor = Anchor.BottomCentre; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + lightContainer?.Expire(); + } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs index f9286b5095..3bf51b3073 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs @@ -5,10 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.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; @@ -19,17 +17,12 @@ namespace osu.Game.Rulesets.Mania.Skinning public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler { private readonly IBindable direction = new Bindable(); - private readonly bool isLastColumn; - private Container borderLineContainer; private Container lightContainer; private Sprite light; - private float hitPosition; - - public LegacyColumnBackground(bool isLastColumn) + public LegacyColumnBackground() { - this.isLastColumn = isLastColumn; RelativeSizeAxes = Axes.Both; } @@ -39,62 +32,14 @@ namespace osu.Game.Rulesets.Mania.Skinning string lightImage = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LightImage)?.Value ?? "mania-stage-light"; - float leftLineWidth = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LeftLineWidth) - ?.Value ?? 1; - 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; - - hitPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HitPosition)?.Value - ?? Stage.HIT_TARGET_POSITION; - float lightPosition = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.LightPosition)?.Value ?? 0; - Color4 lineColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLineColour)?.Value - ?? Color4.White; - - Color4 backgroundColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour)?.Value - ?? Color4.Black; - Color4 lightColour = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.ColumnLightColour)?.Value ?? Color4.White; - InternalChildren = new Drawable[] + InternalChildren = new[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = backgroundColour - }, - borderLineContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new Box - { - RelativeSizeAxes = Axes.Y, - Width = leftLineWidth, - Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasLeftLine ? 1 : 0 - }, - new Box - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.Y, - Width = rightLineWidth, - Scale = new Vector2(0.740f, 1), - Colour = lineColour, - Alpha = hasRightLine ? 1 : 0 - } - } - }, lightContainer = new Container { Origin = Anchor.BottomCentre, @@ -104,7 +49,7 @@ namespace osu.Game.Rulesets.Mania.Skinning { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, - Colour = lightColour, + Colour = LegacyColourCompatibility.DisallowZeroAlpha(lightColour), Texture = skin.GetTexture(lightImage), RelativeSizeAxes = Axes.X, Width = 1, @@ -123,15 +68,11 @@ namespace osu.Game.Rulesets.Mania.Skinning { lightContainer.Anchor = Anchor.TopCentre; lightContainer.Scale = new Vector2(1, -1); - - borderLineContainer.Padding = new MarginPadding { Top = hitPosition }; } else { lightContainer.Anchor = Anchor.BottomCentre; lightContainer.Scale = Vector2.One; - - borderLineContainer.Padding = new MarginPadding { Bottom = hitPosition }; } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs index d055ef3480..6eced571d2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs @@ -20,11 +20,6 @@ namespace osu.Game.Rulesets.Mania.Skinning private Container directionContainer; - public LegacyHitTarget() - { - RelativeSizeAxes = Axes.Both; - } - [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo) { @@ -56,7 +51,7 @@ namespace osu.Game.Rulesets.Mania.Skinning Anchor = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, Height = 1, - Colour = lineColour, + Colour = LegacyColourCompatibility.DisallowZeroAlpha(lineColour), Alpha = showJudgementLine ? 0.9f : 0 } } diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs index 44f3e7d7b3..b269ea25d4 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs @@ -65,6 +65,9 @@ namespace osu.Game.Rulesets.Mania.Skinning direction.BindTo(scrollingInfo.Direction); direction.BindValueChanged(onDirectionChanged, true); + + if (GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.KeysUnderNotes)?.Value ?? false) + Column.UnderlayElements.Add(CreateProxy()); } private void onDirectionChanged(ValueChangedEvent direction) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs index 7f5de601ca..b0bab8e760 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs @@ -4,19 +4,27 @@ 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.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Skinning { public class LegacyStageBackground : CompositeDrawable { + private readonly StageDefinition stageDefinition; + private Drawable leftSprite; private Drawable rightSprite; + private ColumnFlow columnBackgrounds; - public LegacyStageBackground() + public LegacyStageBackground(StageDefinition stageDefinition) { + this.stageDefinition = stageDefinition; RelativeSizeAxes = Axes.Both; } @@ -44,8 +52,19 @@ namespace osu.Game.Rulesets.Mania.Skinning Origin = Anchor.TopLeft, X = -0.05f, Texture = skin.GetTexture(rightImage) + }, + columnBackgrounds = new ColumnFlow(stageDefinition) + { + RelativeSizeAxes = Axes.Y + }, + new HitTargetInsetContainer + { + Child = new LegacyHitTarget { RelativeSizeAxes = Axes.Both } } }; + + for (int i = 0; i < stageDefinition.Columns; i++) + columnBackgrounds.SetContentForColumn(i, new ColumnBackground(i, i == stageDefinition.Columns - 1)); } protected override void Update() @@ -58,5 +77,72 @@ namespace osu.Game.Rulesets.Mania.Skinning if (rightSprite?.Height > 0) rightSprite.Scale = new Vector2(1, DrawHeight / rightSprite.Height); } + + private class ColumnBackground : CompositeDrawable + { + private readonly int columnIndex; + private readonly bool isLastColumn; + + public ColumnBackground(int columnIndex, bool isLastColumn) + { + this.columnIndex = columnIndex; + this.isLastColumn = isLastColumn; + + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + float leftLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.LeftLineWidth, columnIndex)?.Value ?? 1; + float rightLineWidth = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.RightLineWidth, columnIndex)?.Value ?? 1; + + bool hasLeftLine = leftLineWidth > 0; + bool hasRightLine = rightLineWidth > 0 && skin.GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value >= 2.4m + || isLastColumn; + + Color4 lineColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnLineColour, columnIndex)?.Value ?? Color4.White; + Color4 backgroundColour = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ColumnBackgroundColour, columnIndex)?.Value ?? Color4.Black; + + InternalChildren = new Drawable[] + { + LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, backgroundColour), + new HitTargetInsetContainer + { + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + Width = leftLineWidth, + Scale = new Vector2(0.740f, 1), + Alpha = hasLeftLine ? 1 : 0, + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) + }, + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Y, + Width = rightLineWidth, + Scale = new Vector2(0.740f, 1), + Alpha = hasRightLine ? 1 : 0, + Child = LegacyColourCompatibility.ApplyWithDoubledAlpha(new Box + { + RelativeSizeAxes = Axes.Both + }, lineColour) + }, + } + } + }; + } + } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index e167135556..439e6f7df2 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Skinning; using System.Collections.Generic; +using System.Diagnostics; using osu.Framework.Audio.Sample; using osu.Game.Audio; using osu.Game.Rulesets.Objects.Legacy; @@ -88,10 +89,12 @@ namespace osu.Game.Rulesets.Mania.Skinning switch (maniaComponent.Component) { case ManiaSkinComponents.ColumnBackground: - return new LegacyColumnBackground(maniaComponent.TargetColumn == beatmap.TotalColumns - 1); + return new LegacyColumnBackground(); case ManiaSkinComponents.HitTarget: - return new LegacyHitTarget(); + // Legacy skins sandwich the hit target between the column background and the column light. + // To preserve this ordering, it's created manually inside LegacyStageBackground. + return Drawable.Empty(); case ManiaSkinComponents.KeyArea: return new LegacyKeyArea(); @@ -112,7 +115,8 @@ namespace osu.Game.Rulesets.Mania.Skinning return new LegacyHitExplosion(); case ManiaSkinComponents.StageBackground: - return new LegacyStageBackground(); + Debug.Assert(maniaComponent.StageDefinition != null); + return new LegacyStageBackground(maniaComponent.StageDefinition.Value); case ManiaSkinComponents.StageForeground: return new LegacyStageForeground(); diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 255ce4c064..de4648e4fa 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -68,8 +68,6 @@ namespace osu.Game.Rulesets.Mania.UI TopLevelContainer.Add(HitObjectArea.Explosions.CreateProxy()); } - public override Axes RelativeSizeAxes => Axes.Y; - public ColumnType ColumnType { get; set; } public bool IsSpecial => ColumnType == ColumnType.Special; diff --git a/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs new file mode 100644 index 0000000000..aef82d4c08 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/ColumnFlow.cs @@ -0,0 +1,105 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.Beatmaps; +using osu.Game.Rulesets.Mania.Skinning; +using osu.Game.Skinning; + +namespace osu.Game.Rulesets.Mania.UI +{ + /// + /// A which flows its contents according to the s in a . + /// Content can be added to individual columns via . + /// + /// The type of content in each column. + public class ColumnFlow : CompositeDrawable + where TContent : Drawable + { + /// + /// All contents added to this . + /// + public IReadOnlyList Content => columns.Children.Select(c => c.Count == 0 ? null : (TContent)c.Child).ToList(); + + private readonly FillFlowContainer columns; + private readonly StageDefinition stageDefinition; + + public ColumnFlow(StageDefinition stageDefinition) + { + this.stageDefinition = stageDefinition; + + AutoSizeAxes = Axes.X; + + InternalChild = columns = new FillFlowContainer + { + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X, + Direction = FillDirection.Horizontal, + }; + + for (int i = 0; i < stageDefinition.Columns; i++) + columns.Add(new Container { RelativeSizeAxes = Axes.Y }); + } + + private ISkinSource currentSkin; + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + currentSkin = skin; + + skin.SourceChanged += onSkinChanged; + onSkinChanged(); + } + + private void onSkinChanged() + { + for (int i = 0; i < stageDefinition.Columns; i++) + { + if (i > 0) + { + float spacing = currentSkin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, i - 1)) + ?.Value ?? Stage.COLUMN_SPACING; + + columns[i].Margin = new MarginPadding { Left = spacing }; + } + + float? width = currentSkin.GetConfig( + new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, i)) + ?.Value; + + if (width == null) + // only used by default skin (legacy skins get defaults set in LegacyManiaSkinConfiguration) + columns[i].Width = stageDefinition.IsSpecialColumn(i) ? Column.SPECIAL_COLUMN_WIDTH : Column.COLUMN_WIDTH; + else + columns[i].Width = width.Value; + } + } + + /// + /// Sets the content of one of the columns of this . + /// + /// The index of the column to set the content of. + /// The content. + public void SetContentForColumn(int column, TContent content) => columns[column].Child = content; + + public new MarginPadding Padding + { + get => base.Padding; + set => base.Padding = value; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (currentSkin != null) + currentSkin.SourceChanged -= onSkinChanged; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 36780b0f80..e7a2de266d 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; @@ -11,7 +10,6 @@ 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; @@ -31,14 +29,13 @@ namespace osu.Game.Rulesets.Mania.UI public const float HIT_TARGET_POSITION = 110; - public IReadOnlyList Columns => columnFlow.Children; - private readonly FillFlowContainer columnFlow; + public IReadOnlyList Columns => columnFlow.Content; + private readonly ColumnFlow columnFlow; private readonly JudgementContainer judgements; private readonly DrawablePool judgementPool; private readonly Drawable barLineContainer; - private readonly Container topLevelContainer; private readonly Dictionary columnColours = new Dictionary { @@ -62,6 +59,8 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; + Container topLevelContainer; + InternalChildren = new Drawable[] { judgementPool = new DrawablePool(2), @@ -73,17 +72,13 @@ namespace osu.Game.Rulesets.Mania.UI AutoSizeAxes = Axes.X, Children = new Drawable[] { - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground), _ => new DefaultStageBackground()) + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageBackground, stageDefinition: definition), _ => new DefaultStageBackground()) { RelativeSizeAxes = Axes.Both }, - columnFlow = new FillFlowContainer + columnFlow = new ColumnFlow(definition) { - Name = "Columns", RelativeSizeAxes = Axes.Y, - AutoSizeAxes = Axes.X, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Left = COLUMN_SPACING, Right = COLUMN_SPACING }, }, new Container { @@ -102,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI RelativeSizeAxes = Axes.Y, } }, - new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground), _ => null) + new SkinnableDrawable(new ManiaSkinComponent(ManiaSkinComponents.StageForeground, stageDefinition: definition), _ => null) { RelativeSizeAxes = Axes.Both }, @@ -121,60 +116,22 @@ namespace osu.Game.Rulesets.Mania.UI for (int i = 0; i < definition.Columns; i++) { var columnType = definition.GetTypeOfColumn(i); + var column = new Column(firstColumnIndex + i) { + RelativeSizeAxes = Axes.Both, + Width = 1, ColumnType = columnType, AccentColour = columnColours[columnType], Action = { Value = columnType == ColumnType.Special ? specialColumnStartAction++ : normalColumnStartAction++ } }; - AddColumn(column); + topLevelContainer.Add(column.TopLevelContainer.CreateProxy()); + columnFlow.SetContentForColumn(i, column); + AddNested(column); } } - 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 ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnSpacing, col.Index - 1)) - ?.Value ?? COLUMN_SPACING; - - col.Margin = new MarginPadding { Left = spacing }; - } - - float? width = currentSkin.GetConfig( - new ManiaSkinConfigurationLookup(LegacyManiaSkinConfigurationLookups.ColumnWidth, col.Index)) - ?.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; - } - } - - public void AddColumn(Column c) - { - topLevelContainer.Add(c.TopLevelContainer.CreateProxy()); - columnFlow.Add(c); - AddNested(c); - } - public override void Add(DrawableHitObject h) { var maniaObject = (ManiaHitObject)h.HitObject; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index eaa5d8937a..f527eb2312 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -193,30 +193,46 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { - new StatisticRow + var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList(); + + return new[] { - Columns = new[] + new StatisticRow { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList()) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), - } - }, - new StatisticRow - { - Columns = new[] + new StatisticItem("Timing Distribution", + new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow { - new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Accuracy Heatmap", new AccuracyHeatmap(score, playableBeatmap) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) + } } - } - }; + }; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 0ab3e8825b..d15a0a3203 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -59,7 +59,6 @@ namespace osu.Game.Rulesets.Osu.Skinning hitCircleSprite = new Sprite { Texture = getTextureWithFallback(string.Empty), - Colour = drawableObject.AccentColour.Value, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, @@ -107,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Skinning base.LoadComplete(); state.BindValueChanged(updateState, true); - accentColour.BindValueChanged(colour => hitCircleSprite.Colour = colour.NewValue, true); + accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index 0f586034d5..25ab96445a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableObject) { - animationContent.Colour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; + var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; InternalChildren = new[] { @@ -39,11 +39,11 @@ namespace osu.Game.Rulesets.Osu.Skinning Texture = skin.GetTexture("sliderb-nd"), Colour = new Color4(5, 5, 5, 255), }, - animationContent.With(d => + LegacyColourCompatibility.ApplyWithDoubledAlpha(animationContent.With(d => { d.Anchor = Anchor.Centre; d.Origin = Anchor.Centre; - }), + }), ballColour), layerSpec = new Sprite { Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs index bfcf268c3d..9b73ccd248 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - backgroundLayer.Colour = accentColour; + backgroundLayer.Colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs index 8223e3bc01..5ab8e3a8c8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs @@ -76,9 +76,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning private void updateAccentColour() { - headCircle.AccentColour = accentColour; - body.Colour = accentColour; - end.Colour = accentColour; + var colour = LegacyColourCompatibility.DisallowZeroAlpha(accentColour); + + headCircle.AccentColour = colour; + body.Colour = colour; + end.Colour = colour; } } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs index 656728f6e4..b11b64c22c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Skinning; using osuTK.Graphics; namespace osu.Game.Rulesets.Taiko.Skinning @@ -18,9 +19,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning [BackgroundDependencyLoader] private void load() { - AccentColour = component == TaikoSkinComponents.CentreHit - ? new Color4(235, 69, 44, 255) - : new Color4(67, 142, 172, 255); + AccentColour = LegacyColourCompatibility.DisallowZeroAlpha( + component == TaikoSkinComponents.CentreHit + ? new Color4(235, 69, 44, 255) + : new Color4(67, 142, 172, 255)); } } } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 2011842591..dbc32f2c3e 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -161,19 +161,34 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); - public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { - new StatisticRow + var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList(); + + return new[] { - Columns = new[] + new StatisticRow { - new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(score.HitEvents.Where(e => e.HitObject is Hit).ToList()) + Columns = new[] { - RelativeSizeAxes = Axes.X, - Height = 250 - }), + new StatisticItem("Timing Distribution", new HitEventTimingDistributionGraph(timedHitEvents) + { + RelativeSizeAxes = Axes.X, + Height = 250 + }), + } + }, + new StatisticRow + { + Columns = new[] + { + new StatisticItem(string.Empty, new SimpleStatisticTable(3, new SimpleStatisticItem[] + { + new UnstableRate(timedHitEvents) + })) + } } - } - }; + }; + } } } diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs new file mode 100644 index 0000000000..0f6d956b3c --- /dev/null +++ b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; + +namespace osu.Game.Tests.Beatmaps +{ + [TestFixture] + public class BeatmapDifficultyManagerTest + { + [Test] + public void TestKeyEqualsWithDifferentModInstances() + { + var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + + Assert.That(key1, Is.EqualTo(key2)); + } + + [Test] + public void TestKeyEqualsWithDifferentModOrder() + { + var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); + + Assert.That(key1, Is.EqualTo(key2)); + } + } +} diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs new file mode 100644 index 0000000000..9c71466489 --- /dev/null +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyScoreDecoderTest.cs @@ -0,0 +1,70 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mania; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko; +using osu.Game.Scoring; +using osu.Game.Scoring.Legacy; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Beatmaps.Formats +{ + [TestFixture] + public class LegacyScoreDecoderTest + { + [Test] + public void TestDecodeManiaReplay() + { + var decoder = new TestLegacyScoreDecoder(); + + using (var resourceStream = TestResources.OpenResource("Replays/mania-replay.osr")) + { + var score = decoder.Parse(resourceStream); + + Assert.AreEqual(3, score.ScoreInfo.Ruleset.ID); + + Assert.AreEqual(2, score.ScoreInfo.Statistics[HitResult.Great]); + Assert.AreEqual(1, score.ScoreInfo.Statistics[HitResult.Good]); + + Assert.AreEqual(829_931, score.ScoreInfo.TotalScore); + Assert.AreEqual(3, score.ScoreInfo.MaxCombo); + Assert.IsTrue(Precision.AlmostEquals(0.8889, score.ScoreInfo.Accuracy, 0.0001)); + Assert.AreEqual(ScoreRank.B, score.ScoreInfo.Rank); + + Assert.That(score.Replay.Frames, Is.Not.Empty); + } + } + + private class TestLegacyScoreDecoder : LegacyScoreDecoder + { + private static readonly Dictionary rulesets = new Ruleset[] + { + new OsuRuleset(), + new TaikoRuleset(), + new CatchRuleset(), + new ManiaRuleset() + }.ToDictionary(ruleset => ((ILegacyRuleset)ruleset).LegacyID); + + protected override Ruleset GetRuleset(int rulesetId) => rulesets[rulesetId]; + + protected override WorkingBeatmap GetBeatmap(string md5Hash) => new TestWorkingBeatmap(new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + MD5Hash = md5Hash, + Ruleset = new OsuRuleset().RulesetInfo, + BaseDifficulty = new BeatmapDifficulty() + } + }); + } + } +} diff --git a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs index 168ec0f09d..bd34eaff63 100644 --- a/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs +++ b/osu.Game.Tests/Editing/TestSceneHitObjectComposerDistanceSnapping.cs @@ -169,17 +169,17 @@ namespace osu.Game.Tests.Editing [Test] public void GetSnappedDistanceFromDistance() { - assertSnappedDistance(50, 100); + assertSnappedDistance(50, 0); assertSnappedDistance(100, 100); - assertSnappedDistance(150, 200); + assertSnappedDistance(150, 100); assertSnappedDistance(200, 200); - assertSnappedDistance(250, 300); + assertSnappedDistance(250, 200); AddStep("set slider multiplier = 2", () => composer.EditorBeatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier = 2); assertSnappedDistance(50, 0); - assertSnappedDistance(100, 200); - assertSnappedDistance(150, 200); + assertSnappedDistance(100, 0); + assertSnappedDistance(150, 0); assertSnappedDistance(200, 200); assertSnappedDistance(250, 200); @@ -190,8 +190,8 @@ namespace osu.Game.Tests.Editing }); assertSnappedDistance(50, 0); - assertSnappedDistance(100, 200); - assertSnappedDistance(150, 200); + assertSnappedDistance(100, 0); + assertSnappedDistance(150, 0); assertSnappedDistance(200, 200); assertSnappedDistance(250, 200); assertSnappedDistance(400, 400); diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index 7b2913b817..d15682b1eb 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.NonVisual.Filtering } [Test] - public void TestApplyDrainRateQueries() + public void TestApplyDrainRateQueriesByDrKeyword() { const string query = "dr>2 quite specific dr<:6"; var filterCriteria = new FilterCriteria(); @@ -73,6 +73,20 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.Less(filterCriteria.DrainRate.Min, 6.1f); } + [Test] + public void TestApplyDrainRateQueriesByHpKeyword() + { + const string query = "hp>2 quite specific hp<=6"; + var filterCriteria = new FilterCriteria(); + FilterQueryParser.ApplyQueries(filterCriteria, query); + Assert.AreEqual("quite specific", filterCriteria.SearchText.Trim()); + Assert.AreEqual(2, filterCriteria.SearchTerms.Length); + Assert.Greater(filterCriteria.DrainRate.Min, 2.0f); + Assert.Less(filterCriteria.DrainRate.Min, 2.1f); + Assert.Greater(filterCriteria.DrainRate.Max, 6.0f); + Assert.Less(filterCriteria.DrainRate.Min, 6.1f); + } + [Test] public void TestApplyBPMQueries() { diff --git a/osu.Game.Tests/Resources/Replays/mania-replay.osr b/osu.Game.Tests/Resources/Replays/mania-replay.osr new file mode 100644 index 0000000000000000000000000000000000000000..da1a7bdd28e5b89eddd9742bce2b27bc8e75e6f5 GIT binary patch literal 1012 zcmV2Fk>_^VL323Gh;F`G&3?}Wi$c+0000000031008T$3;+WF00000 z01FT?FgZ6ed@(FBI50Uld@(FxP`z80O4tZ&0{{SB001BWz5CvGFroXr>*+lrR^_@@;>&b?uOx<^mMIMT(+z#h{kY?X9fc(g(Lwebtx# z7Q6Lh=|qk7uqqmlbl#mwF~34O;`FY?03m(j9XrY3L)0)1?Fw}$4J_CNJKO;4?X$~q z;+wGa#wD!!0q2PlCzFrV3*3>Lz{D|fez@9JR^@L#NXJJJ;9{+KP~}bh@_eim%+NSk zzegnGWya)SS7pW`<9yI$g`b0t!?MZslqBX+4bVy#tZV0pDNGmeSXKuOXGQ|f3zg8y zGrMu+gxo2?L-AA5RBaNH&*&Dv9Dc!%ufdTEO? zp@}%SBej)D4N{g#xwHF)8|Ks=@OJ2^BGnjH?@$dDOeELCl()%CSSocJ#J0 zrMz`0akC4=AW51K>p71-r!saF*3AP)S9{~98w6pYSsUr1=*o_Q#S)6A-dHA@d9~(^0+ax|D@!;n;Z|| zFvVlqvtKZJf2x8EVoS#`3YPzS-*vaun`i1N*BG`MvK_+XgTPh^GgHpdy!e==KZIaT z3!YC}(8nGj*i;imyixwy9zyF`AooHY90sBzR$Obzb-Vi~&7$v#i3~s7*O{}|SLmeL zoMGZCnRwCc_XbwVIh5g#M@wt{FGra$aqhxs9vn18+0v`>fSkg3XKs=0k_AUDZ~8>a zDG78!sD4dHOSBcg!q83SKA#^J)eG3=oY`jU3QIF2QVgemNl8@7S4(W%aYN@e0jDu% zmqiUorSh0*IbBRsW~4cH;@gJJOutPU2TVP<=OEnHm=mZ{2D~6Z=U*j5r!6K@YHyj* zwhC8cbzc~TkcHU?&2W;Km_To^u4o{p7%^`#txPglTyDDFl%vh successRate.Beatmap = new BeatmapInfo + { + Metrics = new BeatmapMetrics + { + Fails = Enumerable.Range(1, 100).ToArray(), + } + }); + AddAssert("graph max values correct", + () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 100)); + } + + [Test] + public void TestEmptyMetrics() + { + AddStep("set beatmap", () => successRate.Beatmap = new BeatmapInfo + { + Metrics = new BeatmapMetrics() + }); + + AddAssert("graph max values correct", + () => successRate.ChildrenOfType().All(graph => graph.MaxValue == 0)); + } + private class GraphExposingSuccessRate : SuccessRate { public new FailRetryGraph Graph => base.Graph; diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs new file mode 100644 index 0000000000..07a0bcc8d8 --- /dev/null +++ b/osu.Game.Tests/Visual/Ranking/TestSceneSimpleStatisticTable.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using Humanizer; +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.Utils; +using osu.Game.Screens.Ranking.Statistics; + +namespace osu.Game.Tests.Visual.Ranking +{ + public class TestSceneSimpleStatisticTable : OsuTestScene + { + private Container container; + + [SetUp] + public void SetUp() => Schedule(() => + { + Child = new Container + { + AutoSizeAxes = Axes.Y, + Width = 700, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#333"), + }, + container = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(20) + } + } + }; + }); + + [Test] + public void TestEmpty() + { + AddStep("create with no items", + () => container.Add(new SimpleStatisticTable(2, Enumerable.Empty()))); + } + + [Test] + public void TestManyItems( + [Values(1, 2, 3, 4, 12)] int itemCount, + [Values(1, 3, 5)] int columnCount) + { + AddStep($"create with {"item".ToQuantity(itemCount)}", () => + { + var items = Enumerable.Range(1, itemCount) + .Select(i => new SimpleStatisticItem($"Statistic #{i}") + { + Value = RNG.Next(100) + }); + + container.Add(new SimpleStatisticTable(columnCount, items)); + }); + } + } +} diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index b80b4e45ed..0100c9b210 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -89,8 +89,14 @@ namespace osu.Game.Beatmaps if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) return existing; - return await Task.Factory.StartNew(() => computeDifficulty(key, beatmapInfo, rulesetInfo), cancellationToken, - TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + return await Task.Factory.StartNew(() => + { + // Computation may have finished in a previous task. + if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out existing, out _)) + return existing; + + return computeDifficulty(key, beatmapInfo, rulesetInfo); + }, cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } /// @@ -245,7 +251,7 @@ namespace osu.Game.Beatmaps updateScheduler?.Dispose(); } - private readonly struct DifficultyCacheLookup : IEquatable + public readonly struct DifficultyCacheLookup : IEquatable { public readonly int BeatmapId; public readonly int RulesetId; @@ -261,7 +267,7 @@ namespace osu.Game.Beatmaps public bool Equals(DifficultyCacheLookup other) => BeatmapId == other.BeatmapId && RulesetId == other.RulesetId - && Mods.SequenceEqual(other.Mods); + && Mods.Select(m => m.Acronym).SequenceEqual(other.Mods.Select(m => m.Acronym)); public override int GetHashCode() { diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 44ef9bcacc..c15240a4f6 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -104,10 +104,6 @@ namespace osu.Game.Beatmaps.Formats try { 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/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 6af2561c89..648e4a762b 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -119,7 +119,7 @@ namespace osu.Game.Online.Chat case "http": case "https": // length > 3 since all these links need another argument to work - if (args.Length > 3 && (args[1] == "osu.ppy.sh" || args[1] == "new.ppy.sh")) + if (args.Length > 3 && args[1] == "osu.ppy.sh") { switch (args[2]) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c25fb03fd0..f134db1ffe 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -25,7 +25,7 @@ using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components; using osuTK; -using Key = osuTK.Input.Key; +using osuTK.Input; namespace osu.Game.Rulesets.Edit { @@ -293,7 +293,16 @@ namespace osu.Game.Rulesets.Edit public override float GetSnappedDistanceFromDistance(double referenceTime, float distance) { - var snappedEndTime = BeatSnapProvider.SnapTime(referenceTime + DistanceToDuration(referenceTime, distance), referenceTime); + double actualDuration = referenceTime + DistanceToDuration(referenceTime, distance); + + double snappedEndTime = BeatSnapProvider.SnapTime(actualDuration, referenceTime); + + double beatLength = BeatSnapProvider.GetBeatLengthAtTime(referenceTime); + + // we don't want to exceed the actual duration and snap to a point in the future. + // as we are snapping to beat length via SnapTime (which will round-to-nearest), check for snapping in the forward direction and reverse it. + if (snappedEndTime > actualDuration + 1) + snappedEndTime -= beatLength; return DurationToDistance(referenceTime, snappedEndTime - referenceTime); } diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index c854c06031..cce631464f 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -47,6 +47,7 @@ namespace osu.Game.Rulesets.Edit /// /// Converts an unsnapped distance to a snapped distance. + /// The returned distance will always be floored (as to never exceed the provided . /// /// The time of the timing point which resides in. /// The distance to convert. diff --git a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs index a4a560c8e4..97cb5ca7ab 100644 --- a/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs +++ b/osu.Game/Scoring/Legacy/LegacyScoreDecoder.cs @@ -13,7 +13,6 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; -using osu.Game.Rulesets.Scoring; using osu.Game.Users; using SharpCompress.Compressors.LZMA; @@ -123,12 +122,12 @@ namespace osu.Game.Scoring.Legacy protected void CalculateAccuracy(ScoreInfo score) { - score.Statistics.TryGetValue(HitResult.Miss, out int countMiss); - score.Statistics.TryGetValue(HitResult.Meh, out int count50); - score.Statistics.TryGetValue(HitResult.Good, out int count100); - score.Statistics.TryGetValue(HitResult.Great, out int count300); - score.Statistics.TryGetValue(HitResult.Perfect, out int countGeki); - score.Statistics.TryGetValue(HitResult.Ok, out int countKatu); + int countMiss = score.GetCountMiss() ?? 0; + int count50 = score.GetCount50() ?? 0; + int count100 = score.GetCount100() ?? 0; + int count300 = score.GetCount300() ?? 0; + int countGeki = score.GetCountGeki() ?? 0; + int countKatu = score.GetCountKatu() ?? 0; switch (score.Ruleset.ID) { @@ -241,12 +240,15 @@ namespace osu.Game.Scoring.Legacy } var diff = Parsing.ParseFloat(split[0]); + var mouseX = Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE); + var mouseY = Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE); lastTime += diff; - if (i == 0 && diff == 0) - // osu-stable adds a zero-time frame before potentially valid negative user frames. - // we need to ignore this. + if (i < 2 && mouseX == 256 && mouseY == -500) + // at the start of the replay, stable places two replay frames, at time 0 and SkipBoundary - 1, respectively. + // both frames use a position of (256, -500). + // ignore these frames as they serve no real purpose (and can even mislead ruleset-specific handlers - see mania) continue; // Todo: At some point we probably want to rewind and play back the negative-time frames @@ -255,8 +257,8 @@ namespace osu.Game.Scoring.Legacy continue; currentFrame = convertFrame(new LegacyReplayFrame(lastTime, - Parsing.ParseFloat(split[1], Parsing.MAX_COORDINATE_VALUE), - Parsing.ParseFloat(split[2], Parsing.MAX_COORDINATE_VALUE), + mouseX, + mouseY, (ReplayButtonState)Parsing.ParseInt(split[3])), currentFrame); replay.Frames.Add(currentFrame); diff --git a/osu.Game/Screens/Menu/IntroSequence.cs b/osu.Game/Screens/Menu/IntroSequence.cs index 6731fef6f7..d92d38da45 100644 --- a/osu.Game/Screens/Menu/IntroSequence.cs +++ b/osu.Game/Screens/Menu/IntroSequence.cs @@ -205,6 +205,7 @@ namespace osu.Game.Screens.Menu const int line_end_offset = 120; smallRing.Foreground.ResizeTo(1, line_duration, Easing.OutQuint); + smallRing.Delay(400).FadeColour(Color4.Black, 300); lineTopLeft.MoveTo(new Vector2(-line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); lineTopRight.MoveTo(new Vector2(line_end_offset, -line_end_offset), line_duration, Easing.OutQuint); diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs new file mode 100644 index 0000000000..3d9ba2f225 --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticItem.cs @@ -0,0 +1,81 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Represents a simple statistic item (one that only needs textual display). + /// Richer visualisations should be done with s. + /// + public abstract class SimpleStatisticItem : Container + { + /// + /// The text to display as the statistic's value. + /// + protected string Value + { + set => this.value.Text = value; + } + + private readonly OsuSpriteText value; + + /// + /// Creates a new simple statistic item. + /// + /// The name of the statistic. + protected SimpleStatisticItem(string name) + { + Name = name; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + AddRange(new[] + { + new OsuSpriteText + { + Text = Name, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14) + }, + value = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) + } + }); + } + } + + /// + /// Strongly-typed generic specialisation for . + /// + public class SimpleStatisticItem : SimpleStatisticItem + { + /// + /// The statistic's value to be displayed. + /// + public new TValue Value + { + set => base.Value = DisplayValue(value); + } + + /// + /// Used to convert to a text representation. + /// Defaults to using . + /// + protected virtual string DisplayValue(TValue value) => value.ToString(); + + public SimpleStatisticItem(string name) + : base(name) + { + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs new file mode 100644 index 0000000000..8b503cc04e --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/SimpleStatisticTable.cs @@ -0,0 +1,123 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Represents a table with simple statistics (ones that only need textual display). + /// Richer visualisations should be done with s and s. + /// + public class SimpleStatisticTable : CompositeDrawable + { + private readonly SimpleStatisticItem[] items; + private readonly int columnCount; + + private FillFlowContainer[] columns; + + /// + /// Creates a statistic row for the supplied s. + /// + /// The number of columns to layout the into. + /// The s to display in this row. + public SimpleStatisticTable(int columnCount, [ItemNotNull] IEnumerable items) + { + if (columnCount < 1) + throw new ArgumentOutOfRangeException(nameof(columnCount)); + + this.columnCount = columnCount; + this.items = items.ToArray(); + } + + [BackgroundDependencyLoader] + private void load() + { + columns = new FillFlowContainer[columnCount]; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + ColumnDimensions = createColumnDimensions().ToArray(), + Content = new[] { createColumns().ToArray() } + }; + + for (int i = 0; i < items.Length; ++i) + columns[i % columnCount].Add(items[i]); + } + + private IEnumerable createColumnDimensions() + { + for (int column = 0; column < columnCount; ++column) + { + if (column > 0) + yield return new Dimension(GridSizeMode.Absolute, 30); + + yield return new Dimension(); + } + } + + private IEnumerable createColumns() + { + for (int column = 0; column < columnCount; ++column) + { + if (column > 0) + { + yield return new Spacer + { + Alpha = items.Length > column ? 1 : 0 + }; + } + + yield return columns[column] = createColumn(); + } + } + + private FillFlowContainer createColumn() => new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical + }; + + private class Spacer : CompositeDrawable + { + public Spacer() + { + RelativeSizeAxes = Axes.Both; + Padding = new MarginPadding { Vertical = 4 }; + + InternalChild = new CircularContainer + { + RelativeSizeAxes = Axes.Y, + Width = 3, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + CornerRadius = 2, + Masking = true, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("#222") + } + }; + } + } + } +} diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs index ed98698411..485d24d024 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticContainer.cs @@ -32,33 +32,9 @@ namespace osu.Game.Screens.Ranking.Statistics AutoSizeAxes = Axes.Y, Content = new[] { - new Drawable[] + new[] { - 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 = item.Name, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), - } - } - } + createHeader(item) }, new Drawable[] { @@ -78,5 +54,37 @@ namespace osu.Game.Screens.Ranking.Statistics } }; } + + private static Drawable createHeader(StatisticItem item) + { + if (string.IsNullOrEmpty(item.Name)) + return Empty(); + + return 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 = item.Name, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), + } + } + }; + } } } diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs index e959ed24fc..4903983759 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticItem.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Ranking.Statistics /// /// Creates a new , to be displayed inside a in the results screen. /// - /// The name of the item. + /// The name of the item. Can be to hide the item header. /// The content to be displayed. /// 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) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index 7f406331cd..c2ace6a04e 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -94,14 +94,15 @@ namespace osu.Game.Screens.Ranking.Statistics RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), + Alpha = 0 }; foreach (var row in newScore.Ruleset.CreateInstance().CreateStatisticsForScore(newScore, playableBeatmap)) { rows.Add(new GridContainer { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Content = new[] @@ -125,6 +126,7 @@ namespace osu.Game.Screens.Ranking.Statistics spinner.Hide(); content.Add(d); + d.FadeIn(250, Easing.OutQuint); }, localCancellationSource.Token); }), localCancellationSource.Token); } diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs new file mode 100644 index 0000000000..5b368c3e8d --- /dev/null +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Ranking.Statistics +{ + /// + /// Displays the unstable rate statistic for a given play. + /// + public class UnstableRate : SimpleStatisticItem + { + /// + /// Creates and computes an statistic. + /// + /// Sequence of s to calculate the unstable rate based on. + public UnstableRate(IEnumerable hitEvents) + : base("Unstable Rate") + { + var timeOffsets = hitEvents.Select(ev => ev.TimeOffset).ToArray(); + Value = 10 * standardDeviation(timeOffsets); + } + + private static double standardDeviation(double[] timeOffsets) + { + if (timeOffsets.Length == 0) + return double.NaN; + + var mean = timeOffsets.Average(); + var squares = timeOffsets.Select(offset => Math.Pow(offset - mean, 2)).Sum(); + return Math.Sqrt(squares / timeOffsets.Length); + } + + protected override string DisplayValue(double value) => double.IsNaN(value) ? "(not available)" : value.ToString("N2"); + } +} diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 9669a1391c..0ee52f3e48 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -236,7 +236,7 @@ namespace osu.Game.Screens.Select private void updateMetrics() { var hasRatings = beatmap?.BeatmapSet?.Metrics?.Ratings?.Any() ?? false; - var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) && (beatmap?.Metrics.Fails?.Any() ?? false); + var hasRetriesFails = (beatmap?.Metrics?.Retries?.Any() ?? false) || (beatmap?.Metrics?.Fails?.Any() ?? false); if (hasRatings) { diff --git a/osu.Game/Screens/Select/Details/FailRetryGraph.cs b/osu.Game/Screens/Select/Details/FailRetryGraph.cs index 134fd0598a..7cc80acfd3 100644 --- a/osu.Game/Screens/Select/Details/FailRetryGraph.cs +++ b/osu.Game/Screens/Select/Details/FailRetryGraph.cs @@ -29,16 +29,30 @@ namespace osu.Game.Screens.Select.Details var retries = Metrics?.Retries ?? Array.Empty(); var fails = Metrics?.Fails ?? Array.Empty(); + var retriesAndFails = sumRetriesAndFails(retries, fails); - float maxValue = fails.Any() ? fails.Zip(retries, (fail, retry) => fail + retry).Max() : 0; + float maxValue = retriesAndFails.Any() ? retriesAndFails.Max() : 0; failGraph.MaxValue = maxValue; retryGraph.MaxValue = maxValue; - failGraph.Values = fails.Select(f => (float)f); - retryGraph.Values = retries.Zip(fails, (retry, fail) => retry + Math.Clamp(fail, 0, maxValue)); + failGraph.Values = fails.Select(v => (float)v); + retryGraph.Values = retriesAndFails.Select(v => (float)v); } } + private int[] sumRetriesAndFails(int[] retries, int[] fails) + { + var result = new int[Math.Max(retries.Length, fails.Length)]; + + for (int i = 0; i < retries.Length; ++i) + result[i] = retries[i]; + + for (int i = 0; i < fails.Length; ++i) + result[i] += fails[i]; + + return result; + } + public FailRetryGraph() { Children = new[] diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 89afc729fe..39fa4f777d 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.Select internal static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( - @"\b(?stars|ar|dr|cs|divisor|length|objects|bpm|status|creator|artist)(?[=:><]+)(?("".*"")|(\S*))", + @"\b(?stars|ar|dr|hp|cs|divisor|length|objects|bpm|status|creator|artist)(?[=:><]+)(?("".*"")|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static void ApplyQueries(FilterCriteria criteria, string query) @@ -43,6 +43,7 @@ namespace osu.Game.Screens.Select break; case "dr" when parseFloatWithPoint(value, out var dr): + case "hp" when parseFloatWithPoint(value, out dr): updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); break; diff --git a/osu.Game/Skinning/LegacyColourCompatibility.cs b/osu.Game/Skinning/LegacyColourCompatibility.cs new file mode 100644 index 0000000000..b842b50426 --- /dev/null +++ b/osu.Game/Skinning/LegacyColourCompatibility.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Skinning +{ + /// + /// Compatibility methods to convert osu!stable colours to osu!lazer-compatible ones. Should be used for legacy skins only. + /// + public static class LegacyColourCompatibility + { + /// + /// Forces an alpha of 1 if a given is fully transparent. + /// + /// + /// This is equivalent to setting colour post-constructor in osu!stable. + /// + /// The to disallow zero alpha on. + /// The resultant . + public static Color4 DisallowZeroAlpha(Color4 colour) + { + if (colour.A == 0) + colour.A = 1; + return colour; + } + + /// + /// Applies a to a , doubling the alpha value into the property. + /// + /// + /// This is equivalent to setting colour in the constructor in osu!stable. + /// + /// The to apply the colour to. + /// The to apply. + /// The given . + public static T ApplyWithDoubledAlpha(T drawable, Color4 colour) + where T : Drawable + { + drawable.Alpha = colour.A; + drawable.Colour = DisallowZeroAlpha(colour); + return drawable; + } + } +} diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index af7d6007f3..65d5851455 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -31,10 +31,12 @@ namespace osu.Game.Skinning public readonly float[] ColumnSpacing; public readonly float[] ColumnWidth; public readonly float[] ExplosionWidth; + public readonly float[] HoldNoteLightWidth; public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; + public bool KeysUnderNotes; public LegacyManiaSkinConfiguration(int keys) { @@ -44,6 +46,7 @@ namespace osu.Game.Skinning ColumnSpacing = new float[keys - 1]; ColumnWidth = new float[keys]; ExplosionWidth = new float[keys]; + HoldNoteLightWidth = 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 4990ca8e60..a99710ea96 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -34,6 +34,8 @@ namespace osu.Game.Skinning HoldNoteHeadImage, HoldNoteTailImage, HoldNoteBodyImage, + HoldNoteLightImage, + HoldNoteLightScale, ExplosionImage, ExplosionScale, ColumnLineColour, @@ -50,5 +52,6 @@ namespace osu.Game.Skinning Hit100, Hit50, Hit0, + KeysUnderNotes, } } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index aebc229f7c..a9d88e77ad 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -97,10 +97,18 @@ namespace osu.Game.Skinning currentConfig.ShowJudgementLine = pair.Value == "1"; break; + case "KeysUnderNotes": + currentConfig.KeysUnderNotes = pair.Value == "1"; + break; + case "LightingNWidth": parseArrayValue(pair.Value, currentConfig.ExplosionWidth); break; + case "LightingLWidth": + parseArrayValue(pair.Value, currentConfig.HoldNoteLightWidth); + break; + case "WidthForNoteHeightScale": float minWidth = float.Parse(pair.Value, CultureInfo.InvariantCulture) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; if (minWidth > 0) @@ -116,6 +124,7 @@ namespace osu.Game.Skinning case string _ when pair.Key.StartsWith("KeyImage"): case string _ when pair.Key.StartsWith("Hit"): case string _ when pair.Key.StartsWith("Stage"): + case string _ when pair.Key.StartsWith("Lighting"): currentConfig.ImageLookups[pair.Key] = pair.Value; break; } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 02d07eee45..5caf07b554 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -173,6 +173,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.ShowJudgementLine: return SkinUtils.As(new Bindable(existing.ShowJudgementLine)); + case LegacyManiaSkinConfigurationLookups.ExplosionImage: + return SkinUtils.As(getManiaImage(existing, "LightingN")); + case LegacyManiaSkinConfigurationLookups.ExplosionScale: Debug.Assert(maniaLookup.TargetColumn != null); @@ -217,6 +220,20 @@ namespace osu.Game.Skinning Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"NoteImage{maniaLookup.TargetColumn}L")); + case LegacyManiaSkinConfigurationLookups.HoldNoteLightImage: + return SkinUtils.As(getManiaImage(existing, "LightingL")); + + case LegacyManiaSkinConfigurationLookups.HoldNoteLightScale: + Debug.Assert(maniaLookup.TargetColumn != null); + + if (GetConfig(LegacySkinConfiguration.LegacySetting.Version)?.Value < 2.5m) + return SkinUtils.As(new Bindable(1)); + + if (existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] != 0) + return SkinUtils.As(new Bindable(existing.HoldNoteLightWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + + return SkinUtils.As(new Bindable(existing.ColumnWidth[maniaLookup.TargetColumn.Value] / LegacyManiaSkinConfiguration.DEFAULT_COLUMN_SIZE)); + case LegacyManiaSkinConfigurationLookups.KeyImage: Debug.Assert(maniaLookup.TargetColumn != null); return SkinUtils.As(getManiaImage(existing, $"KeyImage{maniaLookup.TargetColumn}")); @@ -255,6 +272,9 @@ namespace osu.Game.Skinning case LegacyManiaSkinConfigurationLookups.Hit300: case LegacyManiaSkinConfigurationLookups.Hit300g: return SkinUtils.As(getManiaImage(existing, maniaLookup.Lookup.ToString())); + + case LegacyManiaSkinConfigurationLookups.KeysUnderNotes: + return SkinUtils.As(new Bindable(existing.KeysUnderNotes)); } return null; From 112ecf085d6a44b59be0c12f131d7fee2a20d4f1 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2020 17:19:04 +0000 Subject: [PATCH 2975/6909] Bump Sentry from 2.1.5 to 2.1.6 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 2.1.5 to 2.1.6. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/2.1.5...2.1.6) 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 d1e2033596..c021770d7b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + From 846189659b935cf970a22fa17e23a63f1353604a Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2020 17:19:29 +0000 Subject: [PATCH 2976/6909] Bump Microsoft.Build.Traversal from 2.0.52 to 2.1.1 Bumps [Microsoft.Build.Traversal](https://github.com/Microsoft/MSBuildSdks) from 2.0.52 to 2.1.1. - [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.52...Microsoft.Build.Traversal.2.1.1) 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 233a040d18..a9a531f59c 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.0.52" + "Microsoft.Build.Traversal": "2.1.1" } } \ No newline at end of file From 66c0d12da619a86b43cbbb594987c59712a8acff Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2020 17:19:46 +0000 Subject: [PATCH 2977/6909] Bump Microsoft.NET.Test.Sdk from 16.7.0 to 16.7.1 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.7.0 to 16.7.1. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.7.0...v16.7.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 f9d56dfa78..dfe3bf8af4 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 ed00ed0b4c..892f27d27f 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 f3837ea6b1..3639c3616f 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 e896606ee8..b59f3a4344 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 d767973528..c692bcd5e4 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 95f5deb2cc..5d55196dcf 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 8bf679db8be69f18ac06aba4972a14bcf5e8f513 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 13:17:17 +0900 Subject: [PATCH 2978/6909] Fix nullref in date text box --- osu.Game.Tournament/Components/DateTextBox.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index ee7e350970..aee5241e35 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -22,11 +22,12 @@ namespace osu.Game.Tournament.Components } // hold a reference to the provided bindable so we don't have to in every settings section. - private Bindable bindable; + private Bindable bindable = new Bindable(); public DateTextBox() { base.Bindable = new Bindable(); + ((OsuTextBox)Control).OnCommit = (sender, newText) => { try From 8f90cc182099075a3651d22d1c31b9303aabd6ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 13:21:51 +0900 Subject: [PATCH 2979/6909] Add test --- .../Components/TestSceneDateTextBox.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs b/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.cs new file mode 100644 index 0000000000..33165d385a --- /dev/null +++ b/osu.Game.Tournament.Tests/Components/TestSceneDateTextBox.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.Tests.Visual; +using osu.Game.Tournament.Components; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tournament.Tests.Components +{ + public class TestSceneDateTextBox : OsuManualInputManagerTestScene + { + private DateTextBox textBox; + + [SetUp] + public void Setup() => Schedule(() => + { + Child = textBox = new DateTextBox + { + Width = 0.3f + }; + }); + + [Test] + public void TestCommitWithoutSettingBindable() + { + AddStep("click textbox", () => + { + InputManager.MoveMouseTo(textBox); + InputManager.Click(MouseButton.Left); + }); + + AddStep("unfocus", () => + { + InputManager.MoveMouseTo(Vector2.Zero); + InputManager.Click(MouseButton.Left); + }); + } + } +} From f793bf66e5e34c94c374b4f19391c83ce08fd361 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 14:25:24 +0900 Subject: [PATCH 2980/6909] Remove rate adjustment from player test scene --- osu.Game/Tests/Visual/PlayerTestScene.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 7d06c99133..2c46e7f6d3 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -29,8 +29,6 @@ namespace osu.Game.Tests.Visual { Dependencies.Cache(LocalConfig = new OsuConfigManager(LocalStorage)); LocalConfig.GetBindable(OsuSetting.DimLevel).Value = 1.0; - - MusicController.AllowRateAdjustments = true; } [SetUpSteps] From 7a6e02c558cffb2eaa5f665611f6ce778359d035 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 14:28:31 +0900 Subject: [PATCH 2981/6909] Allow null rank --- osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs | 2 +- .../Screens/Multi/Match/Components/MatchLeaderboardScore.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index 5bf61eb4ee..f2409d64e7 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Multi.Match.Components protected override LeaderboardScore CreateDrawableScore(APIUserScoreAggregate model, int index) => new MatchLeaderboardScore(model, index); - protected override LeaderboardScore CreateDrawableTopScore(APIUserScoreAggregate model) => new MatchLeaderboardScore(model, model.Position ?? 0, false); + protected override LeaderboardScore CreateDrawableTopScore(APIUserScoreAggregate model) => new MatchLeaderboardScore(model, model.Position, false); } public enum MatchLeaderboardScope diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs index c4e2b332b3..1fabdbb86a 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Multi.Match.Components { private readonly APIUserScoreAggregate score; - public MatchLeaderboardScore(APIUserScoreAggregate score, int rank, bool allowHighlight = true) + public MatchLeaderboardScore(APIUserScoreAggregate score, int? rank, bool allowHighlight = true) : base(score.CreateScoreInfo(), rank, allowHighlight) { this.score = score; From bff652a26f4755e3b18546af495d8bab778ef67f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 14:29:46 +0900 Subject: [PATCH 2982/6909] Persist nulls to the top score bindable --- osu.Game/Online/Leaderboards/Leaderboard.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 003d90d400..db0f835c67 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -93,13 +93,12 @@ namespace osu.Game.Online.Leaderboards get => topScoreContainer.Score.Value; set { + topScoreContainer.Score.Value = value; + if (value == null) topScoreContainer.Hide(); else - { topScoreContainer.Show(); - topScoreContainer.Score.Value = value; - } } } From 5195da3ceb6abc93db51e711c94086a009bf197d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Sep 2020 15:18:51 +0900 Subject: [PATCH 2983/6909] Add message box in bracket editor explaining how to get started --- .../Screens/Editors/LadderEditorScreen.cs | 12 ++++++ osu.Game.Tournament/TournamentGame.cs | 25 +----------- osu.Game.Tournament/WarningBox.cs | 40 +++++++++++++++++++ 3 files changed, 53 insertions(+), 24 deletions(-) create mode 100644 osu.Game.Tournament/WarningBox.cs diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index f3eecf8afe..efec4cffdd 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -26,6 +26,8 @@ namespace osu.Game.Tournament.Screens.Editors [Cached] private LadderEditorInfo editorInfo = new LadderEditorInfo(); + private WarningBox rightClickMessage; + protected override bool DrawLoserPaths => true; [BackgroundDependencyLoader] @@ -37,6 +39,16 @@ namespace osu.Game.Tournament.Screens.Editors Origin = Anchor.TopRight, Margin = new MarginPadding(5) }); + + AddInternal(rightClickMessage = new WarningBox("Right click to place and link matches")); + + LadderInfo.Matches.CollectionChanged += (_, __) => updateMessage(); + updateMessage(); + } + + private void updateMessage() + { + rightClickMessage.Alpha = LadderInfo.Matches.Count > 0 ? 0 : 1; } public void BeginJoin(TournamentMatch match, bool losers) diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index 307ee1c773..bbe4a53d8f 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -87,30 +87,7 @@ namespace osu.Game.Tournament }, } }, - 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) - } - } - }, + heightWarning = new WarningBox("Please make the window wider"), new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tournament/WarningBox.cs b/osu.Game.Tournament/WarningBox.cs new file mode 100644 index 0000000000..814482aea4 --- /dev/null +++ b/osu.Game.Tournament/WarningBox.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osuTK.Graphics; + +namespace osu.Game.Tournament +{ + internal class WarningBox : Container + { + public WarningBox(string text) + { + 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 = text, + Font = OsuFont.Torus.With(weight: FontWeight.Bold), + Colour = Color4.White, + Padding = new MarginPadding(20) + } + }; + } + } +} From 555b2196b734cbce88a2acd2a0832367bf1c33d4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 15:23:50 +0900 Subject: [PATCH 2984/6909] Add xmldoc to MusicController.ResetTrackAdjustments() --- osu.Game/Overlays/MusicController.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 6d5b5d43cd..c831584248 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -382,6 +382,12 @@ namespace osu.Game.Overlays } } + /// + /// Resets the speed adjustments currently applied on and applies the mod adjustments if is true. + /// + /// + /// Does not reset speed adjustments applied directly to the beatmap track. + /// public void ResetTrackAdjustments() { CurrentTrack.ResetSpeedAdjustments(); From 6a765d2d765dd01f4bd145560d6b30542aae7813 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Sep 2020 20:04:56 +0900 Subject: [PATCH 2985/6909] Add smooth fading between audio tracks on transition --- osu.Game/Overlays/MusicController.cs | 6 +++++- osu.Game/Screens/Menu/IntroScreen.cs | 7 +++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index c831584248..17877a69a5 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -341,10 +342,13 @@ namespace osu.Game.Overlays // but the mutation of the hierarchy is scheduled to avoid exceptions. Schedule(() => { - lastTrack.Expire(); + lastTrack.VolumeTo(0, 500, Easing.Out).Expire(); if (queuedTrack == CurrentTrack) + { AddInternal(queuedTrack); + queuedTrack.VolumeTo(0).Then().VolumeTo(1, 300, Easing.Out); + } else { // If the track has changed since the call to changeTrack, it is safe to dispose the diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 884cbfe107..473e6b0364 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -12,6 +12,7 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.IO.Archives; +using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Skinning; using osuTK; @@ -60,6 +61,9 @@ namespace osu.Game.Screens.Menu [Resolved] private AudioManager audio { get; set; } + [Resolved] + private MusicController musicController { get; set; } + /// /// Whether the is provided by osu! resources, rather than a user beatmap. /// Only valid during or after . @@ -167,6 +171,9 @@ namespace osu.Game.Screens.Menu Track = initialBeatmap.Track; UsingThemedIntro = !initialBeatmap.Track.IsDummyDevice; + // ensure the track starts at maximum volume + musicController.CurrentTrack.FinishTransforms(); + logo.MoveTo(new Vector2(0.5f)); logo.ScaleTo(Vector2.One); logo.Hide(); From 4459287b35f1a85461661f5d98042dbb2334b066 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 20:25:12 +0900 Subject: [PATCH 2986/6909] Add filter control test scene --- .../SongSelect/TestSceneFilterControl.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs new file mode 100644 index 0000000000..f89300661c --- /dev/null +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Screens.Select; + +namespace osu.Game.Tests.Visual.SongSelect +{ + public class TestSceneFilterControl : OsuTestScene + { + public TestSceneFilterControl() + { + Child = new FilterControl + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = FilterControl.HEIGHT, + }; + } + } +} From bb090a55e03b35c47192ef9c6f8bce46a84c74e3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 20:25:25 +0900 Subject: [PATCH 2987/6909] Add dropdown to filter control --- osu.Game/Screens/Select/FilterControl.cs | 196 +++++++++++++++++------ 1 file changed, 150 insertions(+), 46 deletions(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index e111ec4b15..24ea4946af 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -2,12 +2,17 @@ // 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; 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.Collections; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -21,7 +26,8 @@ namespace osu.Game.Screens.Select { public class FilterControl : Container { - public const float HEIGHT = 100; + public const float HEIGHT = 2 * side_margin + 85; + private const float side_margin = 20; public Action FilterChanged; @@ -41,6 +47,7 @@ namespace osu.Game.Screens.Select Sort = sortMode.Value, AllowConvertedBeatmaps = showConverted.Value, Ruleset = ruleset.Value, + Collection = collectionDropdown?.Current.Value }; if (!minimumStars.IsDefault) @@ -54,6 +61,7 @@ namespace osu.Game.Screens.Select } private SeekLimitedSearchTextBox searchTextBox; + private CollectionFilterDropdown collectionDropdown; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) || sortTabs.ReceivePositionalInputAt(screenSpacePos); @@ -90,73 +98,169 @@ namespace osu.Game.Screens.Select }, new Container { - Padding = new MarginPadding(20), + Padding = new MarginPadding(side_margin), RelativeSizeAxes = Axes.Both, Width = 0.5f, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Children = new Drawable[] + Child = new GridContainer { - searchTextBox = new SeekLimitedSearchTextBox { RelativeSizeAxes = Axes.X }, - new Box + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - RelativeSizeAxes = Axes.X, - Height = 1, - Colour = OsuColour.Gray(80), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, + new Dimension(GridSizeMode.Absolute, 60), + new Dimension(GridSizeMode.Absolute, 5), + new Dimension(GridSizeMode.Absolute, 20), }, - new FillFlowContainer + Content = new[] { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Direction = FillDirection.Horizontal, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(OsuTabControl.HORIZONTAL_SPACING, 0), - Children = new Drawable[] + new Drawable[] { - new OsuTabControlCheckbox + new Container { - Text = "Show converted", - Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - }, - sortTabs = new OsuTabControl + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + searchTextBox = new SeekLimitedSearchTextBox { RelativeSizeAxes = Axes.X }, + new Box + { + RelativeSizeAxes = Axes.X, + Height = 1, + Colour = OsuColour.Gray(80), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + }, + new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Direction = FillDirection.Horizontal, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + 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, + 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, + }, + } + }, + } + } + }, + null, + new Drawable[] + { + new Container { - 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, - }, - } - }, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + collectionDropdown = new CollectionFilterDropdown + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + Width = 0.4f, + } + } + } + }, + } } } }; - searchTextBox.Current.ValueChanged += _ => FilterChanged?.Invoke(CreateCriteria()); + collectionDropdown.Current.ValueChanged += _ => updateCriteria(); + searchTextBox.Current.ValueChanged += _ => updateCriteria(); updateCriteria(); } + private class CollectionFilterDropdown : OsuDropdown + { + private readonly IBindableList collections = new BindableList(); + private readonly BindableList filters = new BindableList(); + + public CollectionFilterDropdown() + { + ItemSource = filters; + } + + [BackgroundDependencyLoader] + private void load(CollectionManager collectionManager) + { + collections.BindTo(collectionManager.Collections); + collections.CollectionChanged += (_, __) => updateItems(); + updateItems(); + } + + private void updateItems() + { + var selectedItem = SelectedItem?.Value?.Collection; + + filters.Clear(); + filters.Add(new CollectionFilter(null)); + filters.AddRange(collections.Select(c => new CollectionFilter(c))); + + Current.Value = filters.FirstOrDefault(f => f.Collection == selectedItem) ?? filters[0]; + } + + protected override string GenerateItemText(CollectionFilter item) => item.Collection?.Name ?? "All beatmaps"; + + protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader(); + + private class CollectionDropdownHeader : OsuDropdownHeader + { + public CollectionDropdownHeader() + { + Height = 25; + Icon.Size = new Vector2(16); + Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 }; + } + } + } + + public class CollectionFilter + { + [CanBeNull] + public readonly BeatmapCollection Collection; + + public CollectionFilter([CanBeNull] BeatmapCollection collection) + { + Collection = collection; + } + + public virtual bool ContainsBeatmap(BeatmapInfo beatmap) + => Collection?.Beatmaps.Any(b => b.Equals(beatmap)) ?? true; + } + public void Deactivate() { searchTextBox.ReadOnly = true; - searchTextBox.HoldFocus = false; if (searchTextBox.HasFocus) GetContainingInputManager().ChangeFocus(searchTextBox); From 9dde37fe40b7a49e5743844de3bb42007d49ea9e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 20:25:36 +0900 Subject: [PATCH 2988/6909] Hook up collection filter --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 3 +++ osu.Game/Screens/Select/FilterCriteria.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index ed54c158db..8e5655e514 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -60,6 +60,9 @@ namespace osu.Game.Screens.Select.Carousel match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0); } + if (match) + match &= criteria.Collection?.ContainsBeatmap(Beatmap) ?? true; + Filtered.Value = !match; } diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 18be4fcac8..5a5c0e1b50 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -51,6 +51,8 @@ namespace osu.Game.Screens.Select } } + public FilterControl.CollectionFilter Collection; + public struct OptionalRange : IEquatable> where T : struct { From 6d5e155106d721940bb6a9f56822511986b86853 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 20:44:26 +0900 Subject: [PATCH 2989/6909] Change to BindableList to notify of changes --- osu.Game/Collections/CollectionManager.cs | 4 +-- osu.Game/Screens/Select/FilterControl.cs | 41 ++++++++++++++++++++--- 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 302d892efb..3ba604cc39 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -74,9 +74,7 @@ namespace osu.Game.Collections for (int i = 0; i < collectionCount; i++) { var collection = new BeatmapCollection { Name = reader.ReadString() }; - int mapCount = reader.ReadInt32(); - collection.Beatmaps.Capacity = mapCount; for (int j = 0; j < mapCount; j++) { @@ -99,6 +97,6 @@ namespace osu.Game.Collections { public string Name; - public readonly List Beatmaps = new List(); + public readonly BindableList Beatmaps = new BindableList(); } } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 24ea4946af..58c27751fd 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Specialized; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -214,11 +215,21 @@ namespace osu.Game.Screens.Select private void load(CollectionManager collectionManager) { collections.BindTo(collectionManager.Collections); - collections.CollectionChanged += (_, __) => updateItems(); - updateItems(); + collections.CollectionChanged += (_, __) => collectionsChanged(); + collectionsChanged(); } - private void updateItems() + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(filterChanged); + } + + /// + /// Occurs when a collection has been added or removed. + /// + private void collectionsChanged() { var selectedItem = SelectedItem?.Value?.Collection; @@ -226,7 +237,29 @@ namespace osu.Game.Screens.Select filters.Add(new CollectionFilter(null)); filters.AddRange(collections.Select(c => new CollectionFilter(c))); - Current.Value = filters.FirstOrDefault(f => f.Collection == selectedItem) ?? filters[0]; + Current.Value = filters.SingleOrDefault(f => f.Collection == selectedItem) ?? filters[0]; + } + + /// + /// Occurs when the selection has changed. + /// + private void filterChanged(ValueChangedEvent filter) + { + if (filter.OldValue?.Collection != null) + filter.OldValue.Collection.Beatmaps.CollectionChanged -= filterBeatmapsChanged; + + if (filter.NewValue?.Collection != null) + filter.NewValue.Collection.Beatmaps.CollectionChanged += filterBeatmapsChanged; + } + + /// + /// Occurs when the beatmaps contained by a have changed. + /// + private void filterBeatmapsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + // The filtered beatmaps have changed, without the filter having changed itself. So a change in filter must be notified. + // Note that this does NOT propagate to bound bindables, so the FilterControl must bind directly to the value change event of this bindable. + Current.TriggerChange(); } protected override string GenerateItemText(CollectionFilter item) => item.Collection?.Name ?? "All beatmaps"; From 094ddecc9510f5e5e020c24b5952f979ad673741 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 21:08:31 +0900 Subject: [PATCH 2990/6909] Add dropdowns to carousel items --- .../Carousel/DrawableCarouselBeatmap.cs | 26 ++++++++++ .../Carousel/DrawableCarouselBeatmapSet.cs | 48 +++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index c559b4f8f5..760b888288 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -17,6 +18,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Collections; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Sprites; @@ -46,6 +48,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private BeatmapDifficultyManager difficultyManager { get; set; } + [Resolved] + private CollectionManager collectionManager { get; set; } + private IBindable starDifficultyBindable; private CancellationTokenSource starDifficultyCancellationSource; @@ -219,10 +224,31 @@ namespace osu.Game.Screens.Select.Carousel if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); + items.Add(new OsuMenuItem("Add to...") + { + Items = collectionManager.Collections.Take(3).Select(createCollectionMenuItem) + .Append(new OsuMenuItem("More...", MenuItemType.Standard, () => { })) + .ToArray() + }); + return items.ToArray(); } } + private MenuItem createCollectionMenuItem(BeatmapCollection collection) + { + return new ToggleMenuItem(collection.Name, MenuItemType.Standard, s => + { + if (s) + collection.Beatmaps.Add(beatmap); + else + collection.Beatmaps.Remove(beatmap); + }) + { + State = { Value = collection.Beatmaps.Contains(beatmap) } + }; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 5acb6d1946..66851657bc 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -16,6 +16,7 @@ using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Collections; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -34,6 +35,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private DialogOverlay dialogOverlay { get; set; } + [Resolved] + private CollectionManager collectionManager { get; set; } + private readonly BeatmapSetInfo beatmapSet; public DrawableCarouselBeatmapSet(CarouselBeatmapSet set) @@ -141,10 +145,54 @@ namespace osu.Game.Screens.Select.Carousel if (dialogOverlay != null) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); + items.Add(new OsuMenuItem("Add all to...") + { + Items = collectionManager.Collections.Take(3).Select(createCollectionMenuItem) + .Append(new OsuMenuItem("More...", MenuItemType.Standard, () => { })) + .ToArray() + }); + return items.ToArray(); } } + private MenuItem createCollectionMenuItem(BeatmapCollection collection) + { + TernaryState state; + + var countExisting = beatmapSet.Beatmaps.Count(b => collection.Beatmaps.Contains(b)); + + if (countExisting == beatmapSet.Beatmaps.Count) + state = TernaryState.True; + else if (countExisting > 0) + state = TernaryState.Indeterminate; + else + state = TernaryState.False; + + return new TernaryStateMenuItem(collection.Name, MenuItemType.Standard, s => + { + foreach (var b in beatmapSet.Beatmaps) + { + switch (s) + { + case TernaryState.True: + if (collection.Beatmaps.Contains(b)) + continue; + + collection.Beatmaps.Add(b); + break; + + case TernaryState.False: + collection.Beatmaps.Remove(b); + break; + } + } + }) + { + State = { Value = state } + }; + } + private class PanelBackground : BufferedContainer { public PanelBackground(WorkingBeatmap working) From d363a5d164755423b23c5bd1c3531605d0866f2b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 21:19:15 +0900 Subject: [PATCH 2991/6909] Add basic ordering --- osu.Game/Collections/CollectionManager.cs | 9 +++++++++ .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 3ba604cc39..c18cc30427 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -98,5 +98,14 @@ namespace osu.Game.Collections public string Name; public readonly BindableList Beatmaps = new BindableList(); + + public DateTimeOffset LastModifyTime { get; private set; } + + public BeatmapCollection() + { + LastModifyTime = DateTimeOffset.UtcNow; + + Beatmaps.CollectionChanged += (_, __) => LastModifyTime = DateTimeOffset.Now; + } } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 760b888288..1bd4447248 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Add to...") { - Items = collectionManager.Collections.Take(3).Select(createCollectionMenuItem) + Items = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem) .Append(new OsuMenuItem("More...", MenuItemType.Standard, () => { })) .ToArray() }); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 66851657bc..e05b5ee951 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Add all to...") { - Items = collectionManager.Collections.Take(3).Select(createCollectionMenuItem) + Items = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem) .Append(new OsuMenuItem("More...", MenuItemType.Standard, () => { })) .ToArray() }); From 5ebead2bfd27a6df4c7150625f9d9e0da38b1116 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 21:44:07 +0900 Subject: [PATCH 2992/6909] Prevent ValueChanged binds to external bindable --- osu.Game/Screens/Select/FilterControl.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 58c27751fd..d49d0c57e6 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -204,6 +204,7 @@ namespace osu.Game.Screens.Select private class CollectionFilterDropdown : OsuDropdown { private readonly IBindableList collections = new BindableList(); + private readonly IBindableList beatmaps = new BindableList(); private readonly BindableList filters = new BindableList(); public CollectionFilterDropdown() @@ -223,7 +224,8 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); - Current.BindValueChanged(filterChanged); + beatmaps.CollectionChanged += filterBeatmapsChanged; + Current.BindValueChanged(filterChanged, true); } /// @@ -246,10 +248,10 @@ namespace osu.Game.Screens.Select private void filterChanged(ValueChangedEvent filter) { if (filter.OldValue?.Collection != null) - filter.OldValue.Collection.Beatmaps.CollectionChanged -= filterBeatmapsChanged; + beatmaps.UnbindFrom(filter.OldValue.Collection.Beatmaps); if (filter.NewValue?.Collection != null) - filter.NewValue.Collection.Beatmaps.CollectionChanged += filterBeatmapsChanged; + beatmaps.BindTo(filter.NewValue.Collection.Beatmaps); } /// From 02a908752f61509ed45df94d6db41712647f11d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 21:52:56 +0900 Subject: [PATCH 2993/6909] Fix stackoverflow --- osu.Game/Screens/Select/FilterControl.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index d49d0c57e6..fcaacef97b 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -224,7 +224,6 @@ namespace osu.Game.Screens.Select { base.LoadComplete(); - beatmaps.CollectionChanged += filterBeatmapsChanged; Current.BindValueChanged(filterChanged, true); } @@ -247,11 +246,15 @@ namespace osu.Game.Screens.Select /// private void filterChanged(ValueChangedEvent filter) { + beatmaps.CollectionChanged -= filterBeatmapsChanged; + if (filter.OldValue?.Collection != null) beatmaps.UnbindFrom(filter.OldValue.Collection.Beatmaps); if (filter.NewValue?.Collection != null) beatmaps.BindTo(filter.NewValue.Collection.Beatmaps); + + beatmaps.CollectionChanged += filterBeatmapsChanged; } /// From 686257167223fec69675cbe5d0b6c2999132c68f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 22:02:50 +0900 Subject: [PATCH 2994/6909] Fix IconButton sometimes not recolourising --- osu.Game/Graphics/UserInterface/IconButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/IconButton.cs b/osu.Game/Graphics/UserInterface/IconButton.cs index d7e5666545..858f517985 100644 --- a/osu.Game/Graphics/UserInterface/IconButton.cs +++ b/osu.Game/Graphics/UserInterface/IconButton.cs @@ -24,7 +24,7 @@ namespace osu.Game.Graphics.UserInterface set { iconColour = value; - icon.Colour = value; + icon.FadeColour(value); } } From 661eac8f1dd0731fe0476038be5a3c37b72d3b95 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 22:03:38 +0900 Subject: [PATCH 2995/6909] Add add/remove button to dropdown items --- osu.Game/Screens/Select/FilterControl.cs | 82 ++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index fcaacef97b..31c3bd6d1c 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -10,12 +11,14 @@ 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.UserInterface; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; @@ -271,6 +274,8 @@ namespace osu.Game.Screens.Select protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader(); + protected override DropdownMenu CreateMenu() => new CollectionDropdownMenu(); + private class CollectionDropdownHeader : OsuDropdownHeader { public CollectionDropdownHeader() @@ -280,6 +285,83 @@ namespace osu.Game.Screens.Select Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 }; } } + + private class CollectionDropdownMenu : OsuDropdownMenu + { + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item); + } + + private class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem + { + [Resolved] + private OsuColour colours { get; set; } + + [Resolved] + private IBindable beatmap { get; set; } + + [CanBeNull] + private readonly BindableList collectionBeatmaps; + + private IconButton addOrRemoveButton; + + public CollectionDropdownMenuItem(MenuItem item) + : base(item) + { + collectionBeatmaps = ((DropdownMenuItem)item).Value.Collection?.Beatmaps.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + addOrRemoveButton = new IconButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -OsuScrollContainer.SCROLL_BAR_HEIGHT, + Scale = new Vector2(0.75f), + Alpha = collectionBeatmaps == null ? 0 : 1, + Action = addOrRemove + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (collectionBeatmaps != null) + { + collectionBeatmaps.CollectionChanged += (_, __) => collectionChanged(); + collectionChanged(); + } + } + + private void collectionChanged() + { + Debug.Assert(collectionBeatmaps != null); + + if (collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo)) + { + addOrRemoveButton.Icon = FontAwesome.Solid.MinusSquare; + addOrRemoveButton.IconColour = colours.Red; + } + else + { + addOrRemoveButton.Icon = FontAwesome.Solid.PlusSquare; + addOrRemoveButton.IconColour = colours.Green; + } + } + + private void addOrRemove() + { + Debug.Assert(collectionBeatmaps != null); + + if (!collectionBeatmaps.Remove(beatmap.Value.BeatmapInfo)) + collectionBeatmaps.Add(beatmap.Value.BeatmapInfo); + } + } } public class CollectionFilter From 7fcbc3a814b24c78f8a2facf901112ec0e0bc561 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 22:06:17 +0900 Subject: [PATCH 2996/6909] Respond to changes in beatmap --- osu.Game/Screens/Select/FilterControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 31c3bd6d1c..a8f9835240 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -334,7 +334,7 @@ namespace osu.Game.Screens.Select if (collectionBeatmaps != null) { collectionBeatmaps.CollectionChanged += (_, __) => collectionChanged(); - collectionChanged(); + beatmap.BindValueChanged(_ => collectionChanged(), true); } } From d83264f5385d2890f4b9ccd4e021ac77498e1d84 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 22:56:13 +0900 Subject: [PATCH 2997/6909] Add max height --- osu.Game/Screens/Select/FilterControl.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index a8f9835240..0ae5e70fc2 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -288,6 +288,11 @@ namespace osu.Game.Screens.Select private class CollectionDropdownMenu : OsuDropdownMenu { + public CollectionDropdownMenu() + { + MaxHeight = 200; + } + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item); } From b7adb4b1fd1c17b0044cb572b329fd7c96f1780f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 23:31:37 +0900 Subject: [PATCH 2998/6909] Add background save support + read safety --- osu.Game/Collections/CollectionManager.cs | 130 +++++++++++++++++++--- 1 file changed, 112 insertions(+), 18 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index c18cc30427..3f89866749 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -4,9 +4,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; @@ -15,8 +18,16 @@ namespace osu.Game.Collections { public class CollectionManager : CompositeDrawable { + /// + /// Database version in YYYYMMDD format (matching stable). + /// + private const int database_version = 30000000; + private const string database_name = "collection.db"; + [Resolved] + private GameHost host { get; set; } + public IBindableList Collections => collections; private readonly BindableList collections = new BindableList(); @@ -24,13 +35,17 @@ namespace osu.Game.Collections private BeatmapManager beatmaps { get; set; } [BackgroundDependencyLoader] - private void load(GameHost host) + private void load() { if (host.Storage.Exists(database_name)) { using (var stream = host.Storage.GetStream(database_name)) collections.AddRange(readCollection(stream)); } + + foreach (var c in collections) + c.Changed += backgroundSave; + collections.CollectionChanged += (_, __) => backgroundSave(); } /// @@ -64,37 +79,112 @@ namespace osu.Game.Collections { var result = new List(); - using (var reader = new SerializationReader(stream)) + try { - reader.ReadInt32(); // Version - - int collectionCount = reader.ReadInt32(); - result.Capacity = collectionCount; - - for (int i = 0; i < collectionCount; i++) + using (var sr = new SerializationReader(stream)) { - var collection = new BeatmapCollection { Name = reader.ReadString() }; - int mapCount = reader.ReadInt32(); + sr.ReadInt32(); // Version - for (int j = 0; j < mapCount; j++) + int collectionCount = sr.ReadInt32(); + result.Capacity = collectionCount; + + for (int i = 0; i < collectionCount; i++) { - string checksum = reader.ReadString(); + var collection = new BeatmapCollection { Name = sr.ReadString() }; + int mapCount = sr.ReadInt32(); - var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum); - if (beatmap != null) - collection.Beatmaps.Add(beatmap); + for (int j = 0; j < mapCount; j++) + { + string checksum = sr.ReadString(); + + var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum); + if (beatmap != null) + collection.Beatmaps.Add(beatmap); + } + + result.Add(collection); } - - result.Add(collection); } } + catch (Exception e) + { + Logger.Error(e, "Failed to read collections"); + } return result; } + + private readonly object saveLock = new object(); + private int lastSave; + private int saveFailures; + + /// + /// Perform a save with debounce. + /// + private void backgroundSave() + { + var current = Interlocked.Increment(ref lastSave); + Task.Delay(100).ContinueWith(task => + { + if (current != lastSave) + return; + + if (!save()) + backgroundSave(); + }); + } + + private bool save() + { + lock (saveLock) + { + Interlocked.Increment(ref lastSave); + + try + { + // This is NOT thread-safe!! + + using (var sw = new SerializationWriter(host.Storage.GetStream(database_name, FileAccess.Write))) + { + sw.Write(database_version); + sw.Write(collections.Count); + + foreach (var c in collections) + { + sw.Write(c.Name); + sw.Write(c.Beatmaps.Count); + + foreach (var b in c.Beatmaps) + sw.Write(b.MD5Hash); + } + } + + saveFailures = 0; + return true; + } + catch (Exception e) + { + // Since this code is not thread-safe, we may run into random exceptions (such as collection enumeration or out of range indexing). + // Failures are thus only alerted if they exceed a threshold to indicate "actual" errors. + if (++saveFailures >= 10) + { + Logger.Error(e, "Failed to save collections"); + saveFailures = 0; + } + } + + return false; + } + } } public class BeatmapCollection { + /// + /// Invoked whenever any change occurs on this . + /// + public event Action Changed; + public string Name; public readonly BindableList Beatmaps = new BindableList(); @@ -105,7 +195,11 @@ namespace osu.Game.Collections { LastModifyTime = DateTimeOffset.UtcNow; - Beatmaps.CollectionChanged += (_, __) => LastModifyTime = DateTimeOffset.Now; + Beatmaps.CollectionChanged += (_, __) => + { + LastModifyTime = DateTimeOffset.Now; + Changed?.Invoke(); + }; } } } From fd3ab417312e56ded809bb98a86f54f23ddaa0df Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 23:32:08 +0900 Subject: [PATCH 2999/6909] Save on disposal --- osu.Game/Collections/CollectionManager.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 3f89866749..5427f6a489 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -176,6 +176,12 @@ namespace osu.Game.Collections return false; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + save(); + } } public class BeatmapCollection From fca03242644bf67d679158588b4224ac0b1bfd57 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 23:34:38 +0900 Subject: [PATCH 3000/6909] Disallow being able to add dummy beatmap --- osu.Game/Screens/Select/FilterControl.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 0ae5e70fc2..0b85ae0e6a 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -347,6 +347,8 @@ namespace osu.Game.Screens.Select { Debug.Assert(collectionBeatmaps != null); + addOrRemoveButton.Enabled.Value = !beatmap.IsDefault; + if (collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo)) { addOrRemoveButton.Icon = FontAwesome.Solid.MinusSquare; From ae1de1adcb7b95e84b60f28d84cf67a21f9034f0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 23:42:44 +0900 Subject: [PATCH 3001/6909] Adjust to prevent runaway errors --- osu.Game/Collections/CollectionManager.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 5427f6a489..1388a27806 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -159,18 +159,16 @@ namespace osu.Game.Collections } } - saveFailures = 0; + if (saveFailures < 10) + saveFailures = 0; return true; } catch (Exception e) { // Since this code is not thread-safe, we may run into random exceptions (such as collection enumeration or out of range indexing). - // Failures are thus only alerted if they exceed a threshold to indicate "actual" errors. - if (++saveFailures >= 10) - { + // Failures are thus only alerted if they exceed a threshold (once) to indicate "actual" errors having occurred. + if (++saveFailures == 10) Logger.Error(e, "Failed to save collections"); - saveFailures = 0; - } } return false; From 39c304d008ca981ffae95004db43c57789f57435 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Sep 2020 23:47:42 +0900 Subject: [PATCH 3002/6909] Adjust error messages --- osu.Game/Collections/CollectionManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 1388a27806..e6eed40dfc 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -108,7 +108,7 @@ namespace osu.Game.Collections } catch (Exception e) { - Logger.Error(e, "Failed to read collections"); + Logger.Error(e, "Failed to read collection database."); } return result; @@ -168,7 +168,7 @@ namespace osu.Game.Collections // Since this code is not thread-safe, we may run into random exceptions (such as collection enumeration or out of range indexing). // Failures are thus only alerted if they exceed a threshold (once) to indicate "actual" errors having occurred. if (++saveFailures == 10) - Logger.Error(e, "Failed to save collections"); + Logger.Error(e, "Failed to save collection database!"); } return false; From a56f9d6770e0ea9f3392a59dd2a2e99706c2654e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Sep 2020 00:08:33 +0900 Subject: [PATCH 3003/6909] Implement collection import --- osu.Game/Collections/CollectionManager.cs | 88 +++++++++++++------ osu.Game/OsuGame.cs | 2 + osu.Game/OsuGameBase.cs | 5 +- .../Sections/Maintenance/GeneralSettings.cs | 45 +++++++--- 4 files changed, 99 insertions(+), 41 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index e6eed40dfc..20bf96da9d 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -4,8 +4,10 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading; using System.Threading.Tasks; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; @@ -25,12 +27,13 @@ namespace osu.Game.Collections private const string database_name = "collection.db"; + public readonly BindableList Collections = new BindableList(); + + public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; + [Resolved] private GameHost host { get; set; } - public IBindableList Collections => collections; - private readonly BindableList collections = new BindableList(); - [Resolved] private BeatmapManager beatmaps { get; set; } @@ -40,12 +43,12 @@ namespace osu.Game.Collections if (host.Storage.Exists(database_name)) { using (var stream = host.Storage.GetStream(database_name)) - collections.AddRange(readCollection(stream)); + importCollections(readCollections(stream)); } - foreach (var c in collections) + foreach (var c in Collections) c.Changed += backgroundSave; - collections.CollectionChanged += (_, __) => backgroundSave(); + Collections.CollectionChanged += (_, __) => backgroundSave(); } /// @@ -56,26 +59,55 @@ namespace osu.Game.Collections /// /// This is a temporary method and will likely be replaced by a full-fledged (and more correctly placed) migration process in the future. /// - // public Task ImportFromStableAsync() - // { - // var stable = GetStableStorage?.Invoke(); - // - // if (stable == null) - // { - // Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); - // return Task.CompletedTask; - // } - // - // if (!stable.ExistsDirectory(database_name)) - // { - // // This handles situations like when the user does not have a Skins folder - // Logger.Log($"No {database_name} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); - // return Task.CompletedTask; - // } - // - // return Task.Run(async () => await Import(GetStableImportPaths(GetStableStorage()).Select(f => stable.GetFullPath(f)).ToArray())); - // } - private List readCollection(Stream stream) + public Task ImportFromStableAsync() + { + var stable = GetStableStorage?.Invoke(); + + if (stable == null) + { + Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); + return Task.CompletedTask; + } + + if (!stable.Exists(database_name)) + { + // This handles situations like when the user does not have a collections.db file + Logger.Log($"No {database_name} available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); + return Task.CompletedTask; + } + + return Task.Run(() => + { + var storage = GetStableStorage(); + + if (storage.Exists(database_name)) + { + using (var stream = storage.GetStream(database_name)) + { + var collection = readCollections(stream); + Schedule(() => importCollections(collection)); + } + } + }); + } + + private void importCollections(List newCollections) + { + foreach (var newCol in newCollections) + { + var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name); + if (existing == null) + Collections.Add(existing = new BeatmapCollection { Name = newCol.Name }); + + foreach (var newBeatmap in newCol.Beatmaps) + { + if (!existing.Beatmaps.Contains(newBeatmap)) + existing.Beatmaps.Add(newBeatmap); + } + } + } + + private List readCollections(Stream stream) { var result = new List(); @@ -147,9 +179,9 @@ namespace osu.Game.Collections using (var sw = new SerializationWriter(host.Storage.GetStream(database_name, FileAccess.Write))) { sw.Write(database_version); - sw.Write(collections.Count); + sw.Write(Collections.Count); - foreach (var c in collections) + foreach (var c in Collections) { sw.Write(c.Name); sw.Write(c.Beatmaps.Count); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 053eb01dcd..5008a3cf3b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -549,6 +549,8 @@ namespace osu.Game ScoreManager.GetStableStorage = GetStorageForStableInstall; ScoreManager.PresentImport = items => PresentScore(items.First()); + CollectionManager.GetStableStorage = GetStorageForStableInstall; + Container logoContainer; BackButton.Receptor receptor; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3ba164e87f..d512f57ca5 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -225,9 +225,8 @@ namespace osu.Game dependencies.Cache(difficultyManager); AddInternal(difficultyManager); - var collectionManager = new CollectionManager(); - dependencies.Cache(collectionManager); - AddInternal(collectionManager); + dependencies.Cache(CollectionManager = new CollectionManager()); + AddInternal(CollectionManager); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 832673703b..21a5ed6b31 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -6,6 +6,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; using osu.Game.Skinning; @@ -19,6 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private TriangleButton importBeatmapsButton; private TriangleButton importScoresButton; private TriangleButton importSkinsButton; + private TriangleButton importCollectionsButton; private TriangleButton deleteBeatmapsButton; private TriangleButton deleteScoresButton; private TriangleButton deleteSkinsButton; @@ -26,7 +28,7 @@ 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, CollectionManager collections, DialogOverlay dialogOverlay) { if (beatmaps.SupportsImportFromStable) { @@ -93,20 +95,43 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance }); } - AddRange(new Drawable[] + Add(deleteSkinsButton = new DangerousSettingsButton { - deleteSkinsButton = new DangerousSettingsButton + Text = "Delete ALL skins", + Action = () => { - Text = "Delete ALL skins", + dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => + { + deleteSkinsButton.Enabled.Value = false; + Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true)); + })); + } + }); + + if (collections.SupportsImportFromStable) + { + Add(importCollectionsButton = new SettingsButton + { + Text = "Import collections from stable", Action = () => { - dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => - { - deleteSkinsButton.Enabled.Value = false; - Task.Run(() => skins.Delete(skins.GetAllUserSkins())).ContinueWith(t => Schedule(() => deleteSkinsButton.Enabled.Value = true)); - })); + importCollectionsButton.Enabled.Value = false; + collections.ImportFromStableAsync().ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true)); } - }, + }); + } + + Add(new DangerousSettingsButton + { + Text = "Delete ALL collections", + Action = () => + { + dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => collections.Collections.Clear())); + } + }); + + AddRange(new Drawable[] + { restoreButton = new SettingsButton { Text = "Restore all hidden difficulties", From 3fc6a74fdfa1bfd30c05b4748b7712c97530a923 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 2 Sep 2020 19:55:46 +0200 Subject: [PATCH 3004/6909] Expose an immutable bindable in interface. --- osu.Game/Screens/IOsuScreen.cs | 2 +- osu.Game/Screens/OsuScreen.cs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index ead8e4bc22..e19037c2c4 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens /// /// Whether overlays should be able to be opened when this screen is current. /// - Bindable OverlayActivationMode { get; } + IBindable OverlayActivationMode { get; } /// /// The amount of parallax to be applied while this screen is displayed. diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index c10deaf1e5..cb8f2d21fe 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -48,7 +48,9 @@ namespace osu.Game.Screens /// protected virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All; - public Bindable OverlayActivationMode { get; } + protected readonly Bindable OverlayActivationMode; + + IBindable IOsuScreen.OverlayActivationMode => OverlayActivationMode; public virtual bool CursorVisible => true; From 754274a146522ce3a8e69bda0ff4541819b539a2 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 2 Sep 2020 20:55:26 +0200 Subject: [PATCH 3005/6909] Fix and add XMLDoc --- osu.Game/OsuGame.cs | 3 +++ osu.Game/Screens/OsuScreen.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e6d96df927..31926a6845 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -88,6 +88,9 @@ namespace osu.Game private IdleTracker idleTracker; + /// + /// Whether overlays should be able to be opened game-wide. Value is sourced from the current active screen. + /// public readonly IBindable OverlayActivationMode = new Bindable(); protected OsuScreenStack ScreenStack; diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index cb8f2d21fe..a44d14fb5c 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens public virtual bool HideOverlaysOnEnter => false; /// - /// The initial initial overlay activation mode to use when this screen is entered for the first time. + /// The initial overlay activation mode to use when this screen is entered for the first time. /// protected virtual OverlayActivation InitialOverlayActivationMode => OverlayActivation.All; From e7eaaf8b02efd43448b6dbbb11b4e98e7062aca7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 3 Sep 2020 04:42:05 +0300 Subject: [PATCH 3006/6909] Bring legacy slider border width closer to osu!stable --- osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs index 21df49d80b..28277ac443 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS); + protected new float CalculatedBorderPortion => base.CalculatedBorderPortion * 0.77f; + public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); protected override Color4 ColourAt(float position) From 5180d71fd9fda153ac8c895b5fad8cc7c345d0f7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 3 Sep 2020 06:09:52 +0300 Subject: [PATCH 3007/6909] Attach an inline comment explaining how the value was reached --- osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs index 28277ac443..aad8b189d9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -18,7 +18,9 @@ namespace osu.Game.Rulesets.Osu.Skinning { private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS); - protected new float CalculatedBorderPortion => base.CalculatedBorderPortion * 0.77f; + protected new float CalculatedBorderPortion + // Roughly matches osu!stable's slider border portions. + => base.CalculatedBorderPortion * 0.77f; public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); From 547c8090e5a5ae5d3c232debdfe5cbb95ce2f8ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 13:13:48 +0900 Subject: [PATCH 3008/6909] Improve game exit music fade --- osu.Game/Screens/Menu/IntroScreen.cs | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index 473e6b0364..bed8dbcdcb 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -46,8 +46,6 @@ namespace osu.Game.Screens.Menu protected ITrack Track { get; private set; } - private readonly BindableDouble exitingVolumeFade = new BindableDouble(1); - private const int exit_delay = 3000; private SampleChannel seeya; @@ -127,17 +125,29 @@ namespace osu.Game.Screens.Menu this.FadeIn(300); double fadeOutTime = exit_delay; + // we also handle the exit transition. if (MenuVoice.Value) + { seeya.Play(); + + // if playing the outro voice, we have more time to have fun with the background track. + // initially fade to almost silent then ramp out over the remaining time. + const double initial_fade = 200; + musicController.CurrentTrack + .VolumeTo(0.03f, initial_fade).Then() + .VolumeTo(0, fadeOutTime - initial_fade, Easing.In); + } else + { fadeOutTime = 500; - audio.AddAdjustment(AdjustableProperty.Volume, exitingVolumeFade); - this.TransformBindableTo(exitingVolumeFade, 0, fadeOutTime).OnComplete(_ => this.Exit()); + // if outro voice is turned off, just do a simple fade out. + musicController.CurrentTrack.VolumeTo(0, fadeOutTime, Easing.Out); + } //don't want to fade out completely else we will stop running updates. - Game.FadeTo(0.01f, fadeOutTime); + Game.FadeTo(0.01f, fadeOutTime).OnComplete(_ => this.Exit()); base.OnResuming(last); } From 2f42c57f4b7cbe4a9ae4f2ab0ed5fb5d2e65a53d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 13:15:16 +0900 Subject: [PATCH 3009/6909] Add safeties to ensure the current track doesn't loop or change --- osu.Game/Screens/Menu/IntroScreen.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index bed8dbcdcb..1df5c503d6 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -126,6 +126,12 @@ namespace osu.Game.Screens.Menu double fadeOutTime = exit_delay; + var track = musicController.CurrentTrack; + + // ensure the track doesn't change or loop as we are exiting. + track.Looping = false; + Beatmap.Disabled = true; + // we also handle the exit transition. if (MenuVoice.Value) { @@ -134,16 +140,16 @@ namespace osu.Game.Screens.Menu // if playing the outro voice, we have more time to have fun with the background track. // initially fade to almost silent then ramp out over the remaining time. const double initial_fade = 200; - musicController.CurrentTrack - .VolumeTo(0.03f, initial_fade).Then() - .VolumeTo(0, fadeOutTime - initial_fade, Easing.In); + track + .VolumeTo(0.03f, initial_fade).Then() + .VolumeTo(0, fadeOutTime - initial_fade, Easing.In); } else { fadeOutTime = 500; // if outro voice is turned off, just do a simple fade out. - musicController.CurrentTrack.VolumeTo(0, fadeOutTime, Easing.Out); + track.VolumeTo(0, fadeOutTime, Easing.Out); } //don't want to fade out completely else we will stop running updates. From e0328445709f5218befafe0eb091ab7d0e1e738a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 24 Aug 2020 19:38:05 +0900 Subject: [PATCH 3010/6909] Start with a fresh beatmap when entering editor from main menu --- osu.Game/Beatmaps/BeatmapManager.cs | 25 +++++++++++++++++++ .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 3 +++ .../Menus/ScreenSelectionTabControl.cs | 2 -- osu.Game/Screens/Edit/Editor.cs | 9 +++++++ osu.Game/Screens/Menu/MainMenu.cs | 6 ++++- 5 files changed, 42 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 0cadcf5947..9289ed29d9 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -27,6 +27,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; +using osu.Game.Users; using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps @@ -94,6 +95,30 @@ namespace osu.Game.Beatmaps protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; + public WorkingBeatmap CreateNew(RulesetInfo ruleset) + { + var set = new BeatmapSetInfo + { + Metadata = new BeatmapMetadata + { + Artist = "unknown", + Title = "unknown", + Author = User.SYSTEM_USER, + }, + Beatmaps = new List + { + new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + Ruleset = ruleset + } + } + }; + + var working = Import(set).Result; + return GetWorkingBeatmap(working.Beatmaps.First()); + } + protected override async Task Populate(BeatmapSetInfo beatmapSet, ArchiveReader archive, CancellationToken cancellationToken = default) { if (archive != null) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 92199789ec..63b1647f33 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -33,6 +33,9 @@ namespace osu.Game.Beatmaps protected override IBeatmap GetBeatmap() { + if (BeatmapInfo.Path == null) + return BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapConverter(new Beatmap()).Beatmap; + try { using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) diff --git a/osu.Game/Screens/Edit/Components/Menus/ScreenSelectionTabControl.cs b/osu.Game/Screens/Edit/Components/Menus/ScreenSelectionTabControl.cs index 089da4f222..b8bc5cdf36 100644 --- a/osu.Game/Screens/Edit/Components/Menus/ScreenSelectionTabControl.cs +++ b/osu.Game/Screens/Edit/Components/Menus/ScreenSelectionTabControl.cs @@ -32,8 +32,6 @@ namespace osu.Game.Screens.Edit.Components.Menus Height = 1, Colour = Color4.White.Opacity(0.2f), }); - - Current.Value = EditorScreenMode.Compose; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index e178459d5c..dfba5f457b 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -89,6 +89,14 @@ namespace osu.Game.Screens.Edit // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); + bool isNewBeatmap = false; + + if (Beatmap.Value is DummyWorkingBeatmap) + { + isNewBeatmap = true; + Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value); + } + try { playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); @@ -148,6 +156,7 @@ namespace osu.Game.Screens.Edit Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, + Mode = { Value = isNewBeatmap ? EditorScreenMode.SongSetup : EditorScreenMode.Compose }, Items = new[] { new MenuItem("File") diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index fac9e9eb49..e0ac19cbaf 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -98,7 +98,11 @@ namespace osu.Game.Screens.Menu { buttons = new ButtonSystem { - OnEdit = delegate { this.Push(new Editor()); }, + OnEdit = delegate + { + Beatmap.SetDefault(); + this.Push(new Editor()); + }, OnSolo = onSolo, OnMulti = delegate { this.Push(new Multiplayer()); }, OnExit = confirmAndExit, From faf9b0a52864b2d5f8beedb12ab02cefb6bdb15a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Sep 2020 15:48:13 +0900 Subject: [PATCH 3011/6909] Fix hard crash when trying to retrieve a beatmap's track when no file is present --- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 63b1647f33..9e4c29a8d4 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -70,6 +70,9 @@ namespace osu.Game.Beatmaps protected override Track GetBeatmapTrack() { + if (Metadata?.AudioFile == null) + return null; + try { return trackStore.Get(getPathForFile(Metadata.AudioFile)); @@ -83,6 +86,9 @@ namespace osu.Game.Beatmaps protected override Waveform GetWaveform() { + if (Metadata?.AudioFile == null) + return null; + try { var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); From 218cc39a4c46ce467264d11600a15b1e169795de Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Sep 2020 15:49:21 +0900 Subject: [PATCH 3012/6909] Avoid throwing exceptions when MutatePath is called with null path --- osu.Game/IO/WrappedStorage.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index 1dd3afbfae..766e36ef14 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -25,7 +25,7 @@ namespace osu.Game.IO this.subPath = subPath; } - protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path; + protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) && !string.IsNullOrEmpty(path) ? Path.Combine(subPath, path) : path; protected virtual void ChangeTargetStorage(Storage newStorage) { From e80ef341d2663ea07c272154b8ddb1e0bde13467 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Sep 2020 15:50:08 +0900 Subject: [PATCH 3013/6909] Allow UpdateFile to be called when a previous file doesn't exist --- osu.Game/Database/ArchiveModelManager.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 915d980d24..49d7edd56c 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -397,15 +397,24 @@ namespace osu.Game.Database } } + /// + /// Update an existing file, or create a new entry if not already part of the 's files. + /// + /// The item to operate on. + /// The file model to be updated or added. + /// The new file contents. public void UpdateFile(TModel model, TFileModel file, Stream contents) { using (var usage = ContextFactory.GetForWrite()) { // Dereference the existing file info, since the file model will be removed. - Files.Dereference(file.FileInfo); + if (file.FileInfo != null) + { + Files.Dereference(file.FileInfo); - // Remove the file model. - usage.Context.Set().Remove(file); + // Remove the file model. + usage.Context.Set().Remove(file); + } // Add the new file info and containing file model. model.Files.Remove(file); From e337e6b3b0d804c40aac0667471c52a205584a20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Sep 2020 18:55:49 +0900 Subject: [PATCH 3014/6909] Use a more correct filename when saving --- osu.Game/Beatmaps/BeatmapManager.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 9289ed29d9..fee7a71df4 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -237,10 +237,20 @@ namespace osu.Game.Beatmaps using (ContextFactory.GetForWrite()) { var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); + var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; + + // metadata may have changed; update the path with the standard format. + beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}]"; + beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); + var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo + { + Filename = beatmapInfo.Path + }; + stream.Seek(0, SeekOrigin.Begin); - UpdateFile(setInfo, setInfo.Files.Single(f => string.Equals(f.Filename, info.Path, StringComparison.OrdinalIgnoreCase)), stream); + UpdateFile(setInfo, fileInfo, stream); } } From d849f7f2b5b83b76b1e2a1803aadcbb10f6e3d1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Sep 2020 18:56:49 +0900 Subject: [PATCH 3015/6909] Use the local user's username when saving a new beatmap --- osu.Game/Beatmaps/BeatmapManager.cs | 18 +++++++++++------- osu.Game/Screens/Edit/Editor.cs | 14 +++++++++++++- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index fee7a71df4..325e6c6e98 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -97,20 +97,24 @@ namespace osu.Game.Beatmaps public WorkingBeatmap CreateNew(RulesetInfo ruleset) { + var metadata = new BeatmapMetadata + { + Artist = "artist", + Title = "title", + Author = User.SYSTEM_USER, + }; + var set = new BeatmapSetInfo { - Metadata = new BeatmapMetadata - { - Artist = "unknown", - Title = "unknown", - Author = User.SYSTEM_USER, - }, + Metadata = metadata, Beatmaps = new List { new BeatmapInfo { BaseDifficulty = new BeatmapDifficulty(), - Ruleset = ruleset + Ruleset = ruleset, + Metadata = metadata, + Version = "difficulty" } } }; diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index dfba5f457b..b8c1932186 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -28,6 +28,7 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; +using osu.Game.Online.API; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Setup; @@ -72,6 +73,9 @@ namespace osu.Game.Screens.Edit protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + [Resolved] + private IAPIProvider api { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours, GameHost host) { @@ -95,6 +99,7 @@ namespace osu.Game.Screens.Edit { isNewBeatmap = true; Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value); + Beatmap.Value.BeatmapSetInfo.Metadata.Author = api.LocalUser.Value; } try @@ -408,7 +413,14 @@ namespace osu.Game.Screens.Edit clock.SeekForward(!clock.IsRunning, amount); } - private void saveBeatmap() => beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap); + private void saveBeatmap() + { + // apply any set-level metadata changes. + beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); + + // save the loaded beatmap's data stream. + beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap); + } private void exportBeatmap() { From 0530c4b8a740d63f6c4c06faab5dac23cf517c63 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 14:58:22 +0900 Subject: [PATCH 3016/6909] 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 d4a6d6759e..2d3bfaf7ce 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 2a592108b7..166910b165 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 e7addc1c2c..51f8141bac 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From dceae21bbf4083f912b61b5fcb9d4b3b9422ffb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 15:46:56 +0900 Subject: [PATCH 3017/6909] Centralise fetching of overlay component titles and textures --- .../BeatmapListing/BeatmapListingHeader.cs | 3 ++- osu.Game/Overlays/BeatmapSetOverlay.cs | 4 +++- .../Overlays/Changelog/ChangelogHeader.cs | 1 + osu.Game/Overlays/ChangelogOverlay.cs | 9 ++++---- osu.Game/Overlays/ChatOverlay.cs | 6 ++++- .../Dashboard/DashboardOverlayHeader.cs | 3 ++- osu.Game/Overlays/DashboardOverlay.cs | 2 +- osu.Game/Overlays/FullscreenOverlay.cs | 8 ++++++- osu.Game/Overlays/INamedOverlayComponent.cs | 14 ++++++++++++ osu.Game/Overlays/News/NewsHeader.cs | 1 + osu.Game/Overlays/NewsOverlay.cs | 5 +++-- osu.Game/Overlays/NotificationOverlay.cs | 6 ++++- osu.Game/Overlays/NowPlayingOverlay.cs | 6 ++++- osu.Game/Overlays/OverlayHeader.cs | 4 +++- osu.Game/Overlays/OverlayTitle.cs | 22 +++++++++++++------ .../Rankings/RankingsOverlayHeader.cs | 1 + osu.Game/Overlays/RankingsOverlay.cs | 2 +- .../SearchableList/SearchableListOverlay.cs | 2 +- osu.Game/Overlays/SettingsOverlay.cs | 6 ++++- .../Toolbar/ToolbarBeatmapListingButton.cs | 5 ----- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 11 ---------- .../Toolbar/ToolbarChangelogButton.cs | 8 ------- .../Overlays/Toolbar/ToolbarChatButton.cs | 5 ----- .../Overlays/Toolbar/ToolbarHomeButton.cs | 3 +-- .../Overlays/Toolbar/ToolbarMusicButton.cs | 5 ----- .../Overlays/Toolbar/ToolbarNewsButton.cs | 8 ------- .../Toolbar/ToolbarNotificationButton.cs | 5 ----- .../Toolbar/ToolbarOverlayToggleButton.cs | 16 ++++++++++++++ .../Overlays/Toolbar/ToolbarRankingsButton.cs | 8 ------- .../Overlays/Toolbar/ToolbarSettingsButton.cs | 5 ----- .../Overlays/Toolbar/ToolbarSocialButton.cs | 5 ----- osu.Game/Overlays/UserProfileOverlay.cs | 4 ++-- 32 files changed, 98 insertions(+), 95 deletions(-) create mode 100644 osu.Game/Overlays/INamedOverlayComponent.cs diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs index 1bab200fec..1cf86b78cf 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -12,7 +12,8 @@ namespace osu.Game.Overlays.BeatmapListing public BeatmapListingTitle() { Title = "beatmap listing"; - IconTexture = "Icons/changelog"; + Description = "Browse for new beatmaps"; + IconTexture = "Icons/Hexacons/music"; } } } diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 3e23442023..2dfd1fa20f 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -24,7 +24,9 @@ namespace osu.Game.Overlays public const float X_PADDING = 40; public const float Y_PADDING = 25; public const float RIGHT_WIDTH = 275; - protected readonly Header Header; + + //todo: should be an OverlayHeader? or maybe not? + protected new readonly Header Header; [Resolved] private RulesetStore rulesets { get; set; } diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 050bdea03a..35a4fc7014 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -115,6 +115,7 @@ namespace osu.Game.Overlays.Changelog public ChangelogHeaderTitle() { Title = "changelog"; + Description = "Track recent dev updates in the osu! ecosystem"; IconTexture = "Icons/changelog"; } } diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 726be9e194..e9520906ea 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -25,10 +25,10 @@ namespace osu.Game.Overlays { public readonly Bindable Current = new Bindable(); - protected ChangelogHeader Header; - private Container content; + protected new ChangelogHeader Header; + private SampleChannel sampleBack; private List builds; @@ -61,9 +61,10 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - Header = new ChangelogHeader + base.Header = Header = new ChangelogHeader { ListingSelected = ShowListing, + Build = { BindTarget = Current } }, content = new Container { @@ -77,8 +78,6 @@ namespace osu.Game.Overlays sampleBack = audio.Samples.Get(@"UI/generic-select-soft"); - Header.Build.BindTo(Current); - Current.BindValueChanged(e => { if (e.NewValue != null) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 692175603c..8e34f5d2c0 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -26,8 +26,12 @@ using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays { - public class ChatOverlay : OsuFocusedOverlayContainer + public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { + public string IconTexture => "Icons/chat"; + public string Title => "Chat"; + public string Description => "Join the real-time discussion"; + private const float textbox_height = 60; private const float channel_selection_min_height = 0.3f; diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 9ee679a866..1330a44374 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -12,7 +12,8 @@ namespace osu.Game.Overlays.Dashboard public DashboardTitle() { Title = "dashboard"; - IconTexture = "Icons/changelog"; + Description = "View your friends and other top level information"; + IconTexture = "Icons/hexacons/dashboard"; } } } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index e3a4b0e152..68eb35c7da 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new DashboardOverlayHeader + Header = header = new DashboardOverlayHeader { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 3464ce6086..6d0441ff46 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -12,8 +12,14 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public abstract class FullscreenOverlay : WaveOverlayContainer, IOnlineComponent + public abstract class FullscreenOverlay : WaveOverlayContainer, IOnlineComponent, INamedOverlayComponent { + public virtual string IconTexture => Header?.Title.IconTexture ?? string.Empty; + public virtual string Title => Header?.Title.Title ?? string.Empty; + public virtual string Description => Header?.Title.Description ?? string.Empty; + + public OverlayHeader Header { get; protected set; } + [Resolved] protected IAPIProvider API { get; private set; } diff --git a/osu.Game/Overlays/INamedOverlayComponent.cs b/osu.Game/Overlays/INamedOverlayComponent.cs new file mode 100644 index 0000000000..38fb8679a0 --- /dev/null +++ b/osu.Game/Overlays/INamedOverlayComponent.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Overlays +{ + public interface INamedOverlayComponent + { + string IconTexture { get; } + + string Title { get; } + + string Description { get; } + } +} diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index ddada2bdaf..f85d765d46 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -57,6 +57,7 @@ namespace osu.Game.Overlays.News public NewsHeaderTitle() { Title = "news"; + Description = "Get up-to-date on community happenings"; IconTexture = "Icons/news"; } } diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 09fb445b1f..bc3e080158 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -19,7 +19,6 @@ namespace osu.Game.Overlays private Container content; private LoadingLayer loading; - private NewsHeader header; private OverlayScrollContainer scrollFlow; public NewsOverlay() @@ -48,7 +47,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new NewsHeader + Header = new NewsHeader { ShowFrontPage = ShowFrontPage }, @@ -110,6 +109,8 @@ namespace osu.Game.Overlays cancellationToken?.Cancel(); loading.Show(); + var header = (NewsHeader)Header; + if (e.NewValue == null) { header.SetFrontPage(); diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 41160d10ec..6bdacb9c5e 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -16,8 +16,12 @@ using osu.Framework.Threading; namespace osu.Game.Overlays { - public class NotificationOverlay : OsuFocusedOverlayContainer + public class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { + public string IconTexture => "Icons/Hexacons/"; + public string Title => "Notifications"; + public string Description => "Waiting for 'ya"; + private const float width = 320; public const float TRANSITION_LENGTH = 600; diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index af692486b7..f19f7bbc61 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -25,8 +25,12 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public class NowPlayingOverlay : OsuFocusedOverlayContainer + public class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { + public string IconTexture => "Icons/Hexacons/music"; + public string Title => "Now playing"; + public string Description => "Manage the currently playing track"; + private const float player_height = 130; private const float transition_length = 800; private const float progress_height = 10; diff --git a/osu.Game/Overlays/OverlayHeader.cs b/osu.Game/Overlays/OverlayHeader.cs index cc7f798c4a..fed1e57686 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 OverlayTitle Title { get; } + private float contentSidePadding; /// @@ -73,7 +75,7 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.Y, Children = new[] { - CreateTitle().With(title => + Title = CreateTitle().With(title => { title.Anchor = Anchor.CentreLeft; title.Origin = Anchor.CentreLeft; diff --git a/osu.Game/Overlays/OverlayTitle.cs b/osu.Game/Overlays/OverlayTitle.cs index 1c9567428c..17eeece1f8 100644 --- a/osu.Game/Overlays/OverlayTitle.cs +++ b/osu.Game/Overlays/OverlayTitle.cs @@ -12,19 +12,27 @@ using osuTK; namespace osu.Game.Overlays { - public abstract class OverlayTitle : CompositeDrawable + public abstract class OverlayTitle : CompositeDrawable, INamedOverlayComponent { - private readonly OsuSpriteText title; + private readonly OsuSpriteText titleText; private readonly Container icon; - protected string Title + private string title; + + public string Title { - set => title.Text = value; + get => title; + protected set => titleText.Text = title = value; } - protected string IconTexture + public string Description { get; protected set; } + + private string iconTexture; + + public string IconTexture { - set => icon.Child = new OverlayTitleIcon(value); + get => iconTexture; + protected set => icon.Child = new OverlayTitleIcon(iconTexture = value); } protected OverlayTitle() @@ -45,7 +53,7 @@ namespace osu.Game.Overlays 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 + titleText = new OsuSpriteText { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index e30c6f07a8..b12294c6c1 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -30,6 +30,7 @@ namespace osu.Game.Overlays.Rankings public RankingsTitle() { Title = "ranking"; + Description = "Find out who's the best right now"; IconTexture = "Icons/rankings"; } } diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index 7b200d4226..6e8a7d8554 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new RankingsOverlayHeader + Header = header = new RankingsOverlayHeader { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs index 4ab2de06b6..da2066e677 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.SearchableList { private readonly Container scrollContainer; - protected readonly SearchableListHeader Header; + protected new readonly SearchableListHeader Header; protected readonly SearchableListFilterControl Filter; protected readonly FillFlowContainer ScrollFlow; diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index bb84de5d3a..9a7937dfce 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -13,8 +13,12 @@ using osu.Framework.Bindables; namespace osu.Game.Overlays { - public class SettingsOverlay : SettingsPanel + public class SettingsOverlay : SettingsPanel, INamedOverlayComponent { + public string IconTexture => "Icons/Hexacons/settings"; + public string Title => "Settings"; + public string Description => "Change your settings"; + protected override IEnumerable CreateSections() => new SettingsSection[] { new GeneralSection(), diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs index cde305fffd..0363873326 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -11,10 +10,6 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarBeatmapListingButton() { - SetIcon(OsuIcon.ChevronDownCircle); - TooltipMain = "Beatmap listing"; - TooltipSub = "Browse for new beatmaps"; - Hotkey = GlobalAction.ToggleDirect; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 0afc6642b2..5d402c9a23 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -35,17 +35,6 @@ namespace osu.Game.Overlays.Toolbar IconContainer.Show(); } - public void SetIcon(IconUsage icon) => SetIcon(new SpriteIcon - { - Size = new Vector2(20), - Icon = icon - }); - - public IconUsage Icon - { - set => SetIcon(value); - } - public string Text { get => DrawableText.Text; diff --git a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs index c88b418853..23f8b141b2 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs @@ -2,19 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Toolbar { public class ToolbarChangelogButton : ToolbarOverlayToggleButton { - public ToolbarChangelogButton() - { - SetIcon(FontAwesome.Solid.Bullhorn); - TooltipMain = "Changelog"; - TooltipSub = "Track recent dev updates in the osu! ecosystem"; - } - [BackgroundDependencyLoader(true)] private void load(ChangelogOverlay changelog) { diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs index dee4be0c1f..f9a66ae7bb 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -11,10 +10,6 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarChatButton() { - SetIcon(FontAwesome.Solid.Comments); - TooltipMain = "Chat"; - TooltipSub = "Join the real-time discussion"; - Hotkey = GlobalAction.ToggleChat; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index 4845c9a99f..08ba65fc47 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.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.Sprites; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -10,7 +9,7 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarHomeButton() { - Icon = FontAwesome.Solid.Home; + // todo: icon TooltipMain = "Home"; TooltipSub = "Return to the main menu"; diff --git a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs index f9aa2de497..0f5e8e5456 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarMusicButton.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -14,10 +13,6 @@ namespace osu.Game.Overlays.Toolbar public ToolbarMusicButton() { - Icon = FontAwesome.Solid.Music; - TooltipMain = "Now playing"; - TooltipSub = "Manage the currently playing track"; - Hotkey = GlobalAction.ToggleNowPlaying; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs index 106c67a041..0ba2935c80 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs @@ -2,19 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Toolbar { public class ToolbarNewsButton : ToolbarOverlayToggleButton { - public ToolbarNewsButton() - { - Icon = FontAwesome.Solid.Newspaper; - TooltipMain = "News"; - TooltipSub = "Get up-to-date on community happenings"; - } - [BackgroundDependencyLoader(true)] private void load(NewsOverlay news) { diff --git a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs index a699fd907f..79d0fc74c1 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNotificationButton.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Input.Bindings; @@ -25,10 +24,6 @@ namespace osu.Game.Overlays.Toolbar public ToolbarNotificationButton() { - Icon = FontAwesome.Solid.Bars; - TooltipMain = "Notifications"; - TooltipSub = "Waiting for 'ya"; - Hotkey = GlobalAction.ToggleNotifications; Add(countDisplay = new CountCircle diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index 36387bb00d..a76ca26a47 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar @@ -18,6 +21,9 @@ namespace osu.Game.Overlays.Toolbar private readonly Bindable overlayState = new Bindable(); + [Resolved] + private TextureStore textures { get; set; } + public OverlayContainer StateContainer { get => stateContainer; @@ -32,6 +38,16 @@ namespace osu.Game.Overlays.Toolbar Action = stateContainer.ToggleVisibility; overlayState.BindTo(stateContainer.State); } + + if (stateContainer is INamedOverlayComponent named) + { + TooltipMain = named.Title; + TooltipSub = named.Description; + SetIcon(new Sprite + { + Texture = textures.Get(named.IconTexture), + }); + } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs index c026ce99fe..22a01bcdb5 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs @@ -2,19 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; namespace osu.Game.Overlays.Toolbar { public class ToolbarRankingsButton : ToolbarOverlayToggleButton { - public ToolbarRankingsButton() - { - SetIcon(FontAwesome.Regular.ChartBar); - TooltipMain = "Ranking"; - TooltipSub = "Find out who's the best right now"; - } - [BackgroundDependencyLoader(true)] private void load(RankingsOverlay rankings) { diff --git a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs index ed2a23ec2a..4051a2a194 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -11,10 +10,6 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarSettingsButton() { - Icon = FontAwesome.Solid.Cog; - TooltipMain = "Settings"; - TooltipSub = "Change your settings"; - Hotkey = GlobalAction.ToggleSettings; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index 6faa58c559..e62c7bc807 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics.Sprites; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -11,10 +10,6 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarSocialButton() { - Icon = FontAwesome.Solid.Users; - TooltipMain = "Friends"; - TooltipSub = "Interact with those close to you"; - Hotkey = GlobalAction.ToggleSocial; } diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index b4c8a2d3ca..625758614e 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays private ProfileSection lastSection; private ProfileSection[] sections; private GetUserRequest userReq; - protected ProfileHeader Header; + protected new ProfileHeader Header; private ProfileSectionsContainer sectionsContainer; private ProfileSectionTabControl tabs; @@ -77,7 +77,7 @@ namespace osu.Game.Overlays Add(sectionsContainer = new ProfileSectionsContainer { - ExpandableHeader = Header = new ProfileHeader(), + ExpandableHeader = base.Header = Header = new ProfileHeader(), FixedHeader = tabs, HeaderBackground = new Box { From dbf44fbaf265b2bf7142e57370e5764d7f7269ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 16:26:09 +0900 Subject: [PATCH 3018/6909] Update names and icons to match new designs --- osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs | 2 +- osu.Game/Overlays/Changelog/ChangelogHeader.cs | 2 +- osu.Game/Overlays/ChatOverlay.cs | 4 ++-- osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs | 4 ++-- osu.Game/Overlays/News/NewsHeader.cs | 2 +- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- osu.Game/Overlays/Profile/ProfileHeader.cs | 2 +- osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs index 4626589d81..329045c743 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapSet public BeatmapHeaderTitle() { Title = "beatmap info"; - IconTexture = "Icons/changelog"; + IconTexture = "Icons/Hexacons/music"; } } } diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index 35a4fc7014..bdc59297bb 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Changelog { Title = "changelog"; Description = "Track recent dev updates in the osu! ecosystem"; - IconTexture = "Icons/changelog"; + IconTexture = "Icons/Hexacons/devtools"; } } } diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 8e34f5d2c0..bcc2227be8 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -28,8 +28,8 @@ namespace osu.Game.Overlays { public class ChatOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { - public string IconTexture => "Icons/chat"; - public string Title => "Chat"; + public string IconTexture => "Icons/Hexacons/messaging"; + public string Title => "chat"; public string Description => "Join the real-time discussion"; private const float textbox_height = 60; diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 1330a44374..a964d84c4f 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -12,8 +12,8 @@ namespace osu.Game.Overlays.Dashboard public DashboardTitle() { Title = "dashboard"; - Description = "View your friends and other top level information"; - IconTexture = "Icons/hexacons/dashboard"; + Description = "View your friends and other information"; + IconTexture = "Icons/Hexacons/social"; } } } diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index f85d765d46..38ac519387 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -58,7 +58,7 @@ namespace osu.Game.Overlays.News { Title = "news"; Description = "Get up-to-date on community happenings"; - IconTexture = "Icons/news"; + IconTexture = "Icons/Hexacons/news"; } } } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index f19f7bbc61..d1df1fa936 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays public class NowPlayingOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { public string IconTexture => "Icons/Hexacons/music"; - public string Title => "Now playing"; + public string Title => "now playing"; public string Description => "Manage the currently playing track"; private const float player_height = 130; diff --git a/osu.Game/Overlays/Profile/ProfileHeader.cs b/osu.Game/Overlays/Profile/ProfileHeader.cs index 55474c9d3e..c947ef0781 100644 --- a/osu.Game/Overlays/Profile/ProfileHeader.cs +++ b/osu.Game/Overlays/Profile/ProfileHeader.cs @@ -97,7 +97,7 @@ namespace osu.Game.Overlays.Profile public ProfileHeaderTitle() { Title = "player info"; - IconTexture = "Icons/profile"; + IconTexture = "Icons/Hexacons/profile"; } } diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index b12294c6c1..7039ab8214 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -31,7 +31,7 @@ namespace osu.Game.Overlays.Rankings { Title = "ranking"; Description = "Find out who's the best right now"; - IconTexture = "Icons/rankings"; + IconTexture = "Icons/Hexacons/rankings"; } } } From 7bcbac6f45000482ed566b7be6b221eef639d05b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 16:27:31 +0900 Subject: [PATCH 3019/6909] Move header setting to FullscreenOverlay --- .../Online/TestSceneFullscreenOverlay.cs | 6 ++--- osu.Game/Overlays/BeatmapListingOverlay.cs | 6 ++--- osu.Game/Overlays/BeatmapSetOverlay.cs | 4 ++-- osu.Game/Overlays/ChangelogOverlay.cs | 14 +++++------ osu.Game/Overlays/DashboardOverlay.cs | 23 +++++++++---------- osu.Game/Overlays/FullscreenOverlay.cs | 9 +++++--- osu.Game/Overlays/NewsOverlay.cs | 16 ++++++------- osu.Game/Overlays/OverlayScrollContainer.cs | 2 +- osu.Game/Overlays/OverlayView.cs | 2 +- osu.Game/Overlays/RankingsOverlay.cs | 23 +++++++++---------- osu.Game/Overlays/UserProfileOverlay.cs | 7 +++--- 11 files changed, 54 insertions(+), 58 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs index e60adcee34..8f20bcdcc1 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs @@ -12,7 +12,7 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneFullscreenOverlay : OsuTestScene { - private FullscreenOverlay overlay; + private FullscreenOverlay overlay; protected override void LoadComplete() { @@ -38,10 +38,10 @@ namespace osu.Game.Tests.Visual.Online AddAssert("fire count 3", () => fireCount == 3); } - private class TestFullscreenOverlay : FullscreenOverlay + private class TestFullscreenOverlay : FullscreenOverlay { public TestFullscreenOverlay() - : base(OverlayColourScheme.Pink) + : base(OverlayColourScheme.Pink, null) { Children = new Drawable[] { diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 225a8a0578..144af91145 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -24,7 +24,7 @@ using osuTK; namespace osu.Game.Overlays { - public class BeatmapListingOverlay : FullscreenOverlay + public class BeatmapListingOverlay : FullscreenOverlay { [Resolved] private PreviewTrackManager previewTrackManager { get; set; } @@ -38,7 +38,7 @@ namespace osu.Game.Overlays private OverlayScrollContainer resultScrollContainer; public BeatmapListingOverlay() - : base(OverlayColourScheme.Blue) + : base(OverlayColourScheme.Blue, new BeatmapListingHeader()) { } @@ -65,7 +65,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - new BeatmapListingHeader(), + Header, filterControl = new BeatmapListingFilterControl { SearchStarted = onSearchStarted, diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 2dfd1fa20f..bbec62a85a 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -19,7 +19,7 @@ using osuTK; namespace osu.Game.Overlays { - public class BeatmapSetOverlay : FullscreenOverlay + public class BeatmapSetOverlay : FullscreenOverlay // we don't provide a standard header for now. { public const float X_PADDING = 40; public const float Y_PADDING = 25; @@ -39,7 +39,7 @@ namespace osu.Game.Overlays private readonly Box background; public BeatmapSetOverlay() - : base(OverlayColourScheme.Blue) + : base(OverlayColourScheme.Blue, null) { OverlayScrollContainer scroll; Info info; diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index e9520906ea..c7e9a86fa4 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -21,14 +21,12 @@ using osu.Game.Overlays.Changelog; namespace osu.Game.Overlays { - public class ChangelogOverlay : FullscreenOverlay + public class ChangelogOverlay : FullscreenOverlay { public readonly Bindable Current = new Bindable(); private Container content; - protected new ChangelogHeader Header; - private SampleChannel sampleBack; private List builds; @@ -36,7 +34,7 @@ namespace osu.Game.Overlays protected List Streams; public ChangelogOverlay() - : base(OverlayColourScheme.Purple) + : base(OverlayColourScheme.Purple, new ChangelogHeader()) { } @@ -61,11 +59,11 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - base.Header = Header = new ChangelogHeader + Header.With(h => { - ListingSelected = ShowListing, - Build = { BindTarget = Current } - }, + h.ListingSelected = ShowListing; + h.Build.BindTarget = Current; + }), content = new Container { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 68eb35c7da..8135b83a03 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -15,17 +15,21 @@ using osu.Game.Overlays.Dashboard.Friends; namespace osu.Game.Overlays { - public class DashboardOverlay : FullscreenOverlay + public class DashboardOverlay : FullscreenOverlay { private CancellationTokenSource cancellationToken; private Container content; - private DashboardOverlayHeader header; private LoadingLayer loading; private OverlayScrollContainer scrollFlow; public DashboardOverlay() - : base(OverlayColourScheme.Purple) + : base(OverlayColourScheme.Purple, new DashboardOverlayHeader + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Depth = -float.MaxValue + }) { } @@ -50,12 +54,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - Header = header = new DashboardOverlayHeader - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Depth = -float.MaxValue - }, + Header, content = new Container { RelativeSizeAxes = Axes.X, @@ -72,7 +71,7 @@ namespace osu.Game.Overlays { base.LoadComplete(); - header.Current.BindValueChanged(onTabChanged); + Header.Current.BindValueChanged(onTabChanged); } private bool displayUpdateRequired = true; @@ -84,7 +83,7 @@ namespace osu.Game.Overlays // We don't want to create a new display on every call, only when exiting from fully closed state. if (displayUpdateRequired) { - header.Current.TriggerChange(); + Header.Current.TriggerChange(); displayUpdateRequired = false; } } @@ -136,7 +135,7 @@ namespace osu.Game.Overlays if (State.Value == Visibility.Hidden) return; - header.Current.TriggerChange(); + Header.Current.TriggerChange(); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 6d0441ff46..bd6b07c65f 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -12,13 +12,14 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public abstract class FullscreenOverlay : WaveOverlayContainer, IOnlineComponent, INamedOverlayComponent + public abstract class FullscreenOverlay : WaveOverlayContainer, IOnlineComponent, INamedOverlayComponent + where T : OverlayHeader { public virtual string IconTexture => Header?.Title.IconTexture ?? string.Empty; public virtual string Title => Header?.Title.Title ?? string.Empty; public virtual string Description => Header?.Title.Description ?? string.Empty; - public OverlayHeader Header { get; protected set; } + public T Header { get; } [Resolved] protected IAPIProvider API { get; private set; } @@ -26,8 +27,10 @@ namespace osu.Game.Overlays [Cached] protected readonly OverlayColourProvider ColourProvider; - protected FullscreenOverlay(OverlayColourScheme colourScheme) + protected FullscreenOverlay(OverlayColourScheme colourScheme, T header) { + Header = header; + ColourProvider = new OverlayColourProvider(colourScheme); RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index bc3e080158..c8c1db012f 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -13,7 +13,7 @@ using osu.Game.Overlays.News.Displays; namespace osu.Game.Overlays { - public class NewsOverlay : FullscreenOverlay + public class NewsOverlay : FullscreenOverlay { private readonly Bindable article = new Bindable(null); @@ -22,7 +22,7 @@ namespace osu.Game.Overlays private OverlayScrollContainer scrollFlow; public NewsOverlay() - : base(OverlayColourScheme.Purple) + : base(OverlayColourScheme.Purple, new NewsHeader()) { } @@ -47,10 +47,10 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - Header = new NewsHeader + Header.With(h => { - ShowFrontPage = ShowFrontPage - }, + h.ShowFrontPage = ShowFrontPage; + }), content = new Container { RelativeSizeAxes = Axes.X, @@ -109,16 +109,14 @@ namespace osu.Game.Overlays cancellationToken?.Cancel(); loading.Show(); - var header = (NewsHeader)Header; - if (e.NewValue == null) { - header.SetFrontPage(); + Header.SetFrontPage(); LoadDisplay(new FrontPageDisplay()); return; } - header.SetArticle(e.NewValue); + Header.SetArticle(e.NewValue); LoadDisplay(Empty()); } diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index e7415e6f74..b67d5db1a4 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { /// - /// which provides . Mostly used in . + /// which provides . Mostly used in . /// public class OverlayScrollContainer : OsuScrollContainer { diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index 3e2c54c726..312271316a 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -9,7 +9,7 @@ using osu.Game.Online.API; namespace osu.Game.Overlays { /// - /// A subview containing online content, to be displayed inside a . + /// A subview containing online content, to be displayed inside a . /// /// /// Automatically performs a data fetch on load. diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index 6e8a7d8554..ae6d49960a 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -17,17 +17,16 @@ using osu.Game.Overlays.Rankings.Tables; namespace osu.Game.Overlays { - public class RankingsOverlay : FullscreenOverlay + public class RankingsOverlay : FullscreenOverlay { - protected Bindable Country => header.Country; + protected Bindable Country => Header.Country; - protected Bindable Scope => header.Current; + protected Bindable Scope => Header.Current; private readonly OverlayScrollContainer scrollFlow; private readonly Container contentContainer; private readonly LoadingLayer loading; private readonly Box background; - private readonly RankingsOverlayHeader header; private APIRequest lastRequest; private CancellationTokenSource cancellationToken; @@ -36,7 +35,12 @@ namespace osu.Game.Overlays private IAPIProvider api { get; set; } public RankingsOverlay() - : base(OverlayColourScheme.Green) + : base(OverlayColourScheme.Green, new RankingsOverlayHeader + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Depth = -float.MaxValue + }) { Children = new Drawable[] { @@ -55,12 +59,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - Header = header = new RankingsOverlayHeader - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Depth = -float.MaxValue - }, + Header, new Container { RelativeSizeAxes = Axes.X, @@ -97,7 +96,7 @@ namespace osu.Game.Overlays { base.LoadComplete(); - header.Ruleset.BindTo(ruleset); + Header.Ruleset.BindTo(ruleset); Country.BindValueChanged(_ => { diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 625758614e..965ad790ed 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -17,19 +17,18 @@ using osuTK; namespace osu.Game.Overlays { - public class UserProfileOverlay : FullscreenOverlay + public class UserProfileOverlay : FullscreenOverlay { private ProfileSection lastSection; private ProfileSection[] sections; private GetUserRequest userReq; - protected new ProfileHeader Header; private ProfileSectionsContainer sectionsContainer; private ProfileSectionTabControl tabs; public const float CONTENT_X_MARGIN = 70; public UserProfileOverlay() - : base(OverlayColourScheme.Pink) + : base(OverlayColourScheme.Pink, new ProfileHeader()) { } @@ -77,7 +76,7 @@ namespace osu.Game.Overlays Add(sectionsContainer = new ProfileSectionsContainer { - ExpandableHeader = base.Header = Header = new ProfileHeader(), + ExpandableHeader = Header, FixedHeader = tabs, HeaderBackground = new Box { From 942276d88f10e83fb8eb1ddb4894583f94876a0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 16:28:14 +0900 Subject: [PATCH 3020/6909] Remove outdated SearchableList classes --- .../Chat/Selection/ChannelSelectionOverlay.cs | 2 +- .../SearchableListFilterControl.cs | 2 +- .../SearchableList/SearchableListHeader.cs | 82 ----------- .../SearchableList/SearchableListOverlay.cs | 128 ------------------ osu.Game/Overlays/WaveOverlayContainer.cs | 2 + osu.Game/Screens/Multi/Header.cs | 4 +- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 5 +- .../Match/Components/MatchSettingsOverlay.cs | 4 +- 8 files changed, 10 insertions(+), 219 deletions(-) delete mode 100644 osu.Game/Overlays/SearchableList/SearchableListHeader.cs delete mode 100644 osu.Game/Overlays/SearchableList/SearchableListOverlay.cs diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs index b46ca6b040..be9ecc6746 100644 --- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Chat.Selection { public class ChannelSelectionOverlay : WaveOverlayContainer { - public const float WIDTH_PADDING = 170; + public new const float WIDTH_PADDING = 170; private const float transition_duration = 500; diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs index e0163b5b0c..1990674aa9 100644 --- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs +++ b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.SearchableList /// /// The amount of padding added to content (does not affect background or tab control strip). /// - protected virtual float ContentHorizontalPadding => SearchableListOverlay.WIDTH_PADDING; + protected virtual float ContentHorizontalPadding => WaveOverlayContainer.WIDTH_PADDING; protected SearchableListFilterControl() { diff --git a/osu.Game/Overlays/SearchableList/SearchableListHeader.cs b/osu.Game/Overlays/SearchableList/SearchableListHeader.cs deleted file mode 100644 index 66fedf0a56..0000000000 --- a/osu.Game/Overlays/SearchableList/SearchableListHeader.cs +++ /dev/null @@ -1,82 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osuTK; -using osuTK.Graphics; -using osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Graphics; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; - -namespace osu.Game.Overlays.SearchableList -{ - public abstract class SearchableListHeader : Container - where T : struct, Enum - { - public readonly HeaderTabControl Tabs; - - protected abstract Color4 BackgroundColour { get; } - protected abstract T DefaultTab { get; } - protected abstract Drawable CreateHeaderText(); - protected abstract IconUsage Icon { get; } - - protected SearchableListHeader() - { - RelativeSizeAxes = Axes.X; - Height = 90; - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = BackgroundColour, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = SearchableListOverlay.WIDTH_PADDING, Right = SearchableListOverlay.WIDTH_PADDING }, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.BottomLeft, - Position = new Vector2(-35f, 5f), - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10f, 0f), - Children = new[] - { - new SpriteIcon - { - Size = new Vector2(25), - Icon = Icon, - }, - CreateHeaderText(), - }, - }, - Tabs = new HeaderTabControl - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - }, - }, - }, - }; - - Tabs.Current.Value = DefaultTab; - Tabs.Current.TriggerChange(); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Tabs.StripColour = colours.Green; - } - } -} diff --git a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs b/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs deleted file mode 100644 index da2066e677..0000000000 --- a/osu.Game/Overlays/SearchableList/SearchableListOverlay.cs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osuTK.Graphics; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Input.Events; -using osu.Game.Graphics.Backgrounds; -using osu.Game.Graphics.Cursor; - -namespace osu.Game.Overlays.SearchableList -{ - public abstract class SearchableListOverlay : FullscreenOverlay - { - public const float WIDTH_PADDING = 80; - - protected SearchableListOverlay(OverlayColourScheme colourScheme) - : base(colourScheme) - { - } - } - - public abstract class SearchableListOverlay : SearchableListOverlay - where THeader : struct, Enum - where TTab : struct, Enum - where TCategory : struct, Enum - { - private readonly Container scrollContainer; - - protected new readonly SearchableListHeader Header; - protected readonly SearchableListFilterControl Filter; - protected readonly FillFlowContainer ScrollFlow; - - protected abstract Color4 BackgroundColour { get; } - protected abstract Color4 TrianglesColourLight { get; } - protected abstract Color4 TrianglesColourDark { get; } - protected abstract SearchableListHeader CreateHeader(); - protected abstract SearchableListFilterControl CreateFilterControl(); - - protected SearchableListOverlay(OverlayColourScheme colourScheme) - : base(colourScheme) - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = BackgroundColour, - }, - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Children = new[] - { - new Triangles - { - RelativeSizeAxes = Axes.Both, - TriangleScale = 5, - ColourLight = TrianglesColourLight, - ColourDark = TrianglesColourDark, - }, - }, - }, - scrollContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Child = new OsuContextMenuContainer - { - RelativeSizeAxes = Axes.Both, - Masking = true, - Child = new OverlayScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = ScrollFlow = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 10, Bottom = 50 }, - Direction = FillDirection.Vertical, - }, - }, - }, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - Header = CreateHeader(), - Filter = CreateFilterControl(), - }, - }, - }; - } - - protected override void Update() - { - base.Update(); - - scrollContainer.Padding = new MarginPadding { Top = Header.Height + Filter.Height }; - } - - protected override void OnFocus(FocusEvent e) - { - Filter.Search.TakeFocus(); - } - - protected override void PopIn() - { - base.PopIn(); - - Filter.Search.HoldFocus = true; - } - - protected override void PopOut() - { - base.PopOut(); - - Filter.Search.HoldFocus = false; - } - } -} diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index 5c87096dd4..d0fa9987d5 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -14,6 +14,8 @@ namespace osu.Game.Overlays protected override bool BlockNonPositionalInput => true; protected override Container Content => Waves; + public const float WIDTH_PADDING = 80; + protected override bool StartHidden => true; protected WaveOverlayContainer() diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index 653cb3791a..cd8695286b 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -11,8 +11,8 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; -using osu.Game.Overlays.SearchableList; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Multi Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = SearchableListOverlay.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, + Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, Children = new Drawable[] { title = new MultiHeaderTitle diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index f9c5fd13a4..a1e99c83b2 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -12,7 +12,6 @@ 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; @@ -102,8 +101,8 @@ namespace osu.Game.Screens.Multi.Lounge content.Padding = new MarginPadding { Top = Filter.DrawHeight, - Left = SearchableListOverlay.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING, - Right = SearchableListOverlay.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING, + Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING, + Right = WaveOverlayContainer.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING, }; } diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index 49a0fc434b..caefc194b1 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -using osu.Game.Overlays.SearchableList; +using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -117,7 +117,7 @@ namespace osu.Game.Screens.Multi.Match.Components { new Container { - Padding = new MarginPadding { Horizontal = SearchableListOverlay.WIDTH_PADDING }, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Children = new Drawable[] From 98c5a04a09b715de91ed305c4313e6d2055db963 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 16:28:53 +0900 Subject: [PATCH 3021/6909] Update home button --- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 9 +++++++++ osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs | 12 +++++++++--- .../Overlays/Toolbar/ToolbarOverlayToggleButton.cs | 11 +---------- 3 files changed, 19 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 5d402c9a23..d0787664a0 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -35,6 +35,15 @@ namespace osu.Game.Overlays.Toolbar IconContainer.Show(); } + [Resolved] + private TextureStore textures { get; set; } + + public void SetIcon(string texture) => + SetIcon(new Sprite + { + Texture = textures.Get(texture), + }); + public string Text { get => DrawableText.Text; diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index 08ba65fc47..6b2c24c0f3 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.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.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar @@ -9,11 +10,16 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarHomeButton() { - // todo: icon + Width *= 1.4f; + Hotkey = GlobalAction.Home; + } + + [BackgroundDependencyLoader] + private void load() + { TooltipMain = "Home"; TooltipSub = "Return to the main menu"; - - Hotkey = GlobalAction.Home; + SetIcon("Icons/Hexacons/home"); } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs index a76ca26a47..0dea71cc08 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarOverlayToggleButton.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; using osu.Game.Graphics; namespace osu.Game.Overlays.Toolbar @@ -21,9 +18,6 @@ namespace osu.Game.Overlays.Toolbar private readonly Bindable overlayState = new Bindable(); - [Resolved] - private TextureStore textures { get; set; } - public OverlayContainer StateContainer { get => stateContainer; @@ -43,10 +37,7 @@ namespace osu.Game.Overlays.Toolbar { TooltipMain = named.Title; TooltipSub = named.Description; - SetIcon(new Sprite - { - Texture = textures.Get(named.IconTexture), - }); + SetIcon(named.IconTexture); } } } From 2fac0a180e0640362b8ce0294e261b3ac2134d5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 16:29:15 +0900 Subject: [PATCH 3022/6909] Adjust toolbar button sizing --- osu.Game/OsuGame.cs | 4 ++-- osu.Game/Overlays/Settings/Sidebar.cs | 7 +++---- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 7 +++---- osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs | 6 ------ osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs | 1 + 6 files changed, 10 insertions(+), 17 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 6b8e70e546..164a40c6a5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -698,9 +698,9 @@ namespace osu.Game float offset = 0; if (Settings.State.Value == Visibility.Visible) - offset += ToolbarButton.WIDTH / 2; + offset += Toolbar.HEIGHT / 2; if (notifications.State.Value == Visibility.Visible) - offset -= ToolbarButton.WIDTH / 2; + offset -= Toolbar.HEIGHT / 2; screenContainer.MoveToX(offset, SettingsPanel.TRANSITION_LENGTH, Easing.OutQuint); } diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index 358f94b659..3783d15e5a 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -4,22 +4,21 @@ using System; using System.Linq; using osu.Framework; -using osuTK; -using osuTK.Graphics; 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.Game.Graphics.Containers; -using osu.Game.Overlays.Toolbar; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays.Settings { public class Sidebar : Container, IStateful { private readonly FillFlowContainer content; - public const float DEFAULT_WIDTH = ToolbarButton.WIDTH; + public const float DEFAULT_WIDTH = Toolbar.Toolbar.HEIGHT; public const int EXPANDED_WIDTH = 200; public event Action StateChanged; diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index d0787664a0..49b9c62d85 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -9,6 +9,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.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Graphics; @@ -25,8 +26,6 @@ namespace osu.Game.Overlays.Toolbar { public abstract class ToolbarButton : OsuClickableContainer, IKeyBindingHandler { - public const float WIDTH = Toolbar.HEIGHT * 1.4f; - protected GlobalAction? Hotkey { get; set; } public void SetIcon(Drawable icon) @@ -80,7 +79,7 @@ namespace osu.Game.Overlays.Toolbar protected ToolbarButton() : base(HoverSampleSet.Loud) { - Width = WIDTH; + Width = Toolbar.HEIGHT; RelativeSizeAxes = Axes.Y; Children = new Drawable[] @@ -114,7 +113,7 @@ namespace osu.Game.Overlays.Toolbar { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(20), + Size = new Vector2(26), Alpha = 0, }, DrawableText = new OsuSpriteText diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs index 422bf00c6d..905d5b44c6 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetSelector.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Toolbar }, ModeButtonLine = new Container { - Size = new Vector2(ToolbarButton.WIDTH, 3), + Size = new Vector2(Toolbar.HEIGHT, 3), Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, Masking = true, diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index a5194ea752..754b679599 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -65,12 +65,6 @@ namespace osu.Game.Overlays.Toolbar Parent.Click(); return base.OnClick(e); } - - protected override void LoadComplete() - { - base.LoadComplete(); - IconContainer.Scale *= 1.4f; - } } } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs index 4051a2a194..c53f4a55d9 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSettingsButton.cs @@ -10,6 +10,7 @@ namespace osu.Game.Overlays.Toolbar { public ToolbarSettingsButton() { + Width *= 1.4f; Hotkey = GlobalAction.ToggleSettings; } From 0d1674ca5e28fce53ab48fe478e44faa376b7caa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 16:32:43 +0900 Subject: [PATCH 3023/6909] Combine settings strings to read from same location --- osu.Game/Overlays/SettingsOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 9a7937dfce..0532a031f3 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -16,8 +16,8 @@ namespace osu.Game.Overlays public class SettingsOverlay : SettingsPanel, INamedOverlayComponent { public string IconTexture => "Icons/Hexacons/settings"; - public string Title => "Settings"; - public string Description => "Change your settings"; + public string Title => "settings"; + public string Description => "Change the way osu! behaves"; protected override IEnumerable CreateSections() => new SettingsSection[] { @@ -34,7 +34,7 @@ namespace osu.Game.Overlays private readonly List subPanels = new List(); - protected override Drawable CreateHeader() => new SettingsHeader("settings", "Change the way osu! behaves"); + protected override Drawable CreateHeader() => new SettingsHeader(Title, Description); protected override Drawable CreateFooter() => new SettingsFooter(); public SettingsOverlay() From f5a73130e1aa32ca1001669f4118a15decfce8c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 16:34:47 +0900 Subject: [PATCH 3024/6909] Fix regression in sidebar button sizing --- osu.Game/Overlays/Settings/Sidebar.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index 3783d15e5a..031ecaae46 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays.Settings public class Sidebar : Container, IStateful { private readonly FillFlowContainer content; - public const float DEFAULT_WIDTH = Toolbar.Toolbar.HEIGHT; + public const float DEFAULT_WIDTH = Toolbar.Toolbar.HEIGHT * 1.4f; public const int EXPANDED_WIDTH = 200; public event Action StateChanged; From 99e34d85621b283476376d72d8cb70c3f9a50eb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 17:05:45 +0900 Subject: [PATCH 3025/6909] Update with missing icons --- osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs | 2 +- osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs | 2 +- osu.Game/Overlays/NotificationOverlay.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs index 1cf86b78cf..b7f511271c 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -13,7 +13,7 @@ namespace osu.Game.Overlays.BeatmapListing { Title = "beatmap listing"; Description = "Browse for new beatmaps"; - IconTexture = "Icons/Hexacons/music"; + IconTexture = "Icons/Hexacons/beatmap"; } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs index 329045c743..6511b15fc8 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapSet public BeatmapHeaderTitle() { Title = "beatmap info"; - IconTexture = "Icons/Hexacons/music"; + IconTexture = "Icons/Hexacons/beatmap"; } } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 6bdacb9c5e..b7d916c48f 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -18,7 +18,7 @@ namespace osu.Game.Overlays { public class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { - public string IconTexture => "Icons/Hexacons/"; + public string IconTexture => "Icons/Hexacons/notification"; public string Title => "Notifications"; public string Description => "Waiting for 'ya"; From d55c9c3cc2e6f951ff5bf8776c9cff638f3f5726 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 17:11:34 +0900 Subject: [PATCH 3026/6909] Fix UserProfile weirdness --- osu.Game/Graphics/Containers/SectionsContainer.cs | 5 ++++- osu.Game/Overlays/UserProfileOverlay.cs | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index d739f56828..f32f8e0c67 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -26,8 +26,11 @@ namespace osu.Game.Graphics.Containers { if (value == expandableHeader) return; - expandableHeader?.Expire(); + if (expandableHeader != null) + RemoveInternal(expandableHeader); + expandableHeader = value; + if (value == null) return; AddInternal(expandableHeader); diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 965ad790ed..2b316c0e34 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -44,6 +44,9 @@ namespace osu.Game.Overlays if (user.Id == Header?.User.Value?.Id) return; + if (sectionsContainer != null) + sectionsContainer.ExpandableHeader = null; + userReq?.Cancel(); Clear(); lastSection = null; From 72cb65c22f55c89677a4bf3a466e082788831a99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 17:51:54 +0900 Subject: [PATCH 3027/6909] Update and add missing beatmap statistic icons to info wedge --- .../Beatmaps/CatchBeatmap.cs | 7 +++--- .../Beatmaps/ManiaBeatmap.cs | 5 ++-- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 7 +++--- .../Beatmaps/TaikoBeatmap.cs | 7 +++--- osu.Game/Beatmaps/BeatmapStatistic.cs | 18 ++++++++++++- osu.Game/Beatmaps/BeatmapStatisticSprite.cs | 25 +++++++++++++++++++ osu.Game/Screens/Select/BeatmapInfoWedge.cs | 16 +++++++++--- 7 files changed, 65 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapStatisticSprite.cs diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index 18cc300ff9..5dc19ce15b 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; @@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { Name = @"Fruit Count", Content = fruits.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticSprite("circles"), }, new BeatmapStatistic { Name = @"Juice Stream Count", Content = juiceStreams.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticSprite("sliders"), }, new BeatmapStatistic { Name = @"Banana Shower Count", Content = bananaShowers.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticSprite("spinners"), } }; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index dc24a344e9..f6b460f269 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.UI; @@ -41,14 +40,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps new BeatmapStatistic { Name = @"Note Count", + CreateIcon = () => new BeatmapStatisticSprite("circles"), Content = notes.ToString(), - Icon = FontAwesome.Regular.Circle }, new BeatmapStatistic { Name = @"Hold Note Count", + CreateIcon = () => new BeatmapStatisticSprite("sliders"), Content = holdnotes.ToString(), - Icon = FontAwesome.Regular.Circle }, }; } diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index 491d82b89e..513a9254ec 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; @@ -23,19 +22,19 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { Name = @"Circle Count", Content = circles.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticSprite("circles"), }, new BeatmapStatistic { Name = @"Slider Count", Content = sliders.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticSprite("sliders"), }, new BeatmapStatistic { Name = @"Spinner Count", Content = spinners.ToString(), - Icon = FontAwesome.Regular.Circle + CreateIcon = () => new BeatmapStatisticSprite("spinners"), } }; } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index b595f43fbb..c0f8af4fff 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Rulesets.Taiko.Objects; @@ -22,20 +21,20 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps new BeatmapStatistic { Name = @"Hit Count", + CreateIcon = () => new BeatmapStatisticSprite("circles"), Content = hits.ToString(), - Icon = FontAwesome.Regular.Circle }, new BeatmapStatistic { Name = @"Drumroll Count", + CreateIcon = () => new BeatmapStatisticSprite("sliders"), Content = drumrolls.ToString(), - Icon = FontAwesome.Regular.Circle }, new BeatmapStatistic { Name = @"Swell Count", + CreateIcon = () => new BeatmapStatisticSprite("spinners"), Content = swells.ToString(), - Icon = FontAwesome.Regular.Circle } }; } diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 0745ec5222..5a466c24be 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -1,14 +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; +using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; namespace osu.Game.Beatmaps { public class BeatmapStatistic { - public IconUsage Icon; + [Obsolete("Use CreateIcon instead")] // can be removed 20210203 + public IconUsage Icon = FontAwesome.Regular.QuestionCircle; + + /// + /// A function to create the icon for display purposes. + /// + public Func CreateIcon; + public string Content; public string Name; + + public BeatmapStatistic() + { +#pragma warning disable 618 + CreateIcon = () => new SpriteIcon { Icon = Icon }; +#pragma warning restore 618 + } } } diff --git a/osu.Game/Beatmaps/BeatmapStatisticSprite.cs b/osu.Game/Beatmaps/BeatmapStatisticSprite.cs new file mode 100644 index 0000000000..1cb0bacf0f --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapStatisticSprite.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.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Beatmaps +{ + public class BeatmapStatisticSprite : Sprite + { + private readonly string iconName; + + public BeatmapStatisticSprite(string iconName) + { + this.iconName = iconName; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get($"Icons/BeatmapDetails/{iconName}"); + } + } +} diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index ad977c70b5..4ef074b967 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -318,14 +318,14 @@ namespace osu.Game.Screens.Select labels.Add(new InfoLabel(new BeatmapStatistic { Name = "Length", - Icon = FontAwesome.Regular.Clock, + CreateIcon = () => new BeatmapStatisticSprite("length"), Content = TimeSpan.FromMilliseconds(b.BeatmapInfo.Length).ToString(@"m\:ss"), })); labels.Add(new InfoLabel(new BeatmapStatistic { Name = "BPM", - Icon = FontAwesome.Regular.Circle, + CreateIcon = () => new BeatmapStatisticSprite("bpm"), Content = getBPMRange(b), })); @@ -418,10 +418,18 @@ namespace osu.Game.Screens.Select Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Scale = new Vector2(0.8f), Colour = Color4Extensions.FromHex(@"f7dd55"), - Icon = statistic.Icon, + Icon = FontAwesome.Regular.Circle, + Scale = new Vector2(0.8f) }, + statistic.CreateIcon().With(i => + { + i.Anchor = Anchor.Centre; + i.Origin = Anchor.Centre; + i.RelativeSizeAxes = Axes.Both; + i.Size = new Vector2(1.2f); + i.Colour = Color4Extensions.FromHex(@"f7dd55"); + }), } }, new OsuSpriteText From 58e84760b959362ee49fd98d36b8222f30298f70 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 19:17:07 +0900 Subject: [PATCH 3028/6909] Fix path empty string check causing regression in behaviour --- osu.Game/IO/WrappedStorage.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/WrappedStorage.cs b/osu.Game/IO/WrappedStorage.cs index 766e36ef14..5b2549d2ee 100644 --- a/osu.Game/IO/WrappedStorage.cs +++ b/osu.Game/IO/WrappedStorage.cs @@ -25,7 +25,13 @@ namespace osu.Game.IO this.subPath = subPath; } - protected virtual string MutatePath(string path) => !string.IsNullOrEmpty(subPath) && !string.IsNullOrEmpty(path) ? Path.Combine(subPath, path) : path; + protected virtual string MutatePath(string path) + { + if (path == null) + return null; + + return !string.IsNullOrEmpty(subPath) ? Path.Combine(subPath, path) : path; + } protected virtual void ChangeTargetStorage(Storage newStorage) { From a555407f3772353b2b4f8b9b3a93179ec2212585 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 19:20:42 +0900 Subject: [PATCH 3029/6909] Fix various test failures due to missing beatmap info in empty beatmap --- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 9e4c29a8d4..11b6ad8c29 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps protected override IBeatmap GetBeatmap() { if (BeatmapInfo.Path == null) - return BeatmapInfo.Ruleset.CreateInstance().CreateBeatmapConverter(new Beatmap()).Beatmap; + return BeatmapInfo?.Ruleset.CreateInstance().CreateBeatmapConverter(new Beatmap { BeatmapInfo = BeatmapInfo }).Beatmap; try { diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 6a161e6e04..aefeb48453 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -53,7 +53,7 @@ namespace osu.Game.Beatmaps { const double excess_length = 1000; - var lastObject = Beatmap.HitObjects.LastOrDefault(); + var lastObject = Beatmap?.HitObjects.LastOrDefault(); double length; From 1c1c583d3b9bf0ed2b306f7fef8ebae0bd11f1b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 19:31:40 +0900 Subject: [PATCH 3030/6909] Fix regression in file update logic (filename set too early) --- osu.Game/Beatmaps/BeatmapManager.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 325e6c6e98..1456e7487b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -243,15 +243,15 @@ namespace osu.Game.Beatmaps var beatmapInfo = setInfo.Beatmaps.Single(b => b.ID == info.ID); var metadata = beatmapInfo.Metadata ?? setInfo.Metadata; + // grab the original file (or create a new one if not found). + var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); + // metadata may have changed; update the path with the standard format. beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}]"; - beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); - var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo - { - Filename = beatmapInfo.Path - }; + // update existing or populate new file's filename. + fileInfo.Filename = beatmapInfo.Path; stream.Seek(0, SeekOrigin.Begin); UpdateFile(setInfo, fileInfo, stream); From c9a73926a68cd4104eb719f8b365be601c67373c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 19:38:01 +0900 Subject: [PATCH 3031/6909] Add basic test coverage --- .../Beatmaps/IO/ImportBeatmapTest.cs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0151678db3..4547613b5a 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -15,6 +15,7 @@ using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.IO; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Resources; using SharpCompress.Archives; @@ -756,6 +757,63 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public void TestCreateNewEmptyBeatmap() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapFile))) + { + try + { + var osu = loadOsu(host); + var manager = osu.Dependencies.Get(); + + var working = osu.Dependencies.Get().CreateNew(new OsuRuleset().RulesetInfo); + + manager.Save(working.BeatmapInfo, working.Beatmap); + + var retrievedSet = manager.GetAllUsableBeatmapSets()[0]; + + // Check that the new file is referenced correctly by attempting a retrieval + Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap; + Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(0)); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public void TestCreateNewBeatmapWithObject() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapFile))) + { + try + { + var osu = loadOsu(host); + var manager = osu.Dependencies.Get(); + + var working = osu.Dependencies.Get().CreateNew(new OsuRuleset().RulesetInfo); + + ((Beatmap)working.Beatmap).HitObjects.Add(new HitCircle { StartTime = 5000 }); + + manager.Save(working.BeatmapInfo, working.Beatmap); + + var retrievedSet = manager.GetAllUsableBeatmapSets()[0]; + + // Check that the new file is referenced correctly by attempting a retrieval + Beatmap updatedBeatmap = (Beatmap)manager.GetWorkingBeatmap(retrievedSet.Beatmaps[0]).Beatmap; + Assert.That(updatedBeatmap.HitObjects.Count, Is.EqualTo(1)); + Assert.That(updatedBeatmap.HitObjects[0].StartTime, Is.EqualTo(5000)); + } + finally + { + host.Exit(); + } + } + } + public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) { var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); From d32b77f045a120ee21645baff1209f5c9215118c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 21:33:25 +0900 Subject: [PATCH 3032/6909] Add missing extension to filename Co-authored-by: Dan Balasescu --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 1456e7487b..12add76b46 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -247,7 +247,7 @@ namespace osu.Game.Beatmaps var fileInfo = setInfo.Files.SingleOrDefault(f => string.Equals(f.Filename, beatmapInfo.Path, StringComparison.OrdinalIgnoreCase)) ?? new BeatmapSetFileInfo(); // metadata may have changed; update the path with the standard format. - beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}]"; + beatmapInfo.Path = $"{metadata.Artist} - {metadata.Title} ({metadata.Author}) [{beatmapInfo.Version}].osu"; beatmapInfo.MD5Hash = stream.ComputeMD5Hash(); // update existing or populate new file's filename. From ebed7d09e3412206660666a3231b406e4550206d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Sep 2020 21:56:36 +0900 Subject: [PATCH 3033/6909] 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 2d3bfaf7ce..a41c1a5864 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 166910b165..d79806883e 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 51f8141bac..16a8a1acb7 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 18927304f10bd912fc9a09cb22aaed3dd38974d0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 3 Sep 2020 16:29:25 +0300 Subject: [PATCH 3034/6909] Move adjustment to LegacySkinConfiguration as a default value --- osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs | 4 ---- osu.Game/Skinning/LegacySkinConfiguration.cs | 9 +++++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs index aad8b189d9..21df49d80b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -18,10 +18,6 @@ namespace osu.Game.Rulesets.Osu.Skinning { private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS); - protected new float CalculatedBorderPortion - // Roughly matches osu!stable's slider border portions. - => base.CalculatedBorderPortion * 0.77f; - public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); protected override Color4 ColourAt(float position) diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index 41b7aea34b..b980d727ed 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Globalization; + namespace osu.Game.Skinning { public class LegacySkinConfiguration : SkinConfiguration @@ -12,6 +14,13 @@ namespace osu.Game.Skinning /// public decimal? LegacyVersion { get; internal set; } + public LegacySkinConfiguration() + { + // Roughly matches osu!stable's slider border portions. + // Can't use nameof(SliderBorderSize) as the lookup enum is declared in the osu! ruleset. + ConfigDictionary["SliderBorderSize"] = 0.77f.ToString(CultureInfo.InvariantCulture); + } + public enum LegacySetting { Version, From d6b46936a0f7100617815b67130252f413ef03d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 12:55:28 +0900 Subject: [PATCH 3035/6909] Adjust sizing to match updated textures with less padding --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 4ef074b967..b3bf306431 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -420,15 +420,15 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.Both, Colour = Color4Extensions.FromHex(@"f7dd55"), Icon = FontAwesome.Regular.Circle, - Scale = new Vector2(0.8f) + Size = new Vector2(0.8f) }, statistic.CreateIcon().With(i => { i.Anchor = Anchor.Centre; i.Origin = Anchor.Centre; i.RelativeSizeAxes = Axes.Both; - i.Size = new Vector2(1.2f); i.Colour = Color4Extensions.FromHex(@"f7dd55"); + i.Size = new Vector2(0.8f); }), } }, From 9d2dff2cb871403637511e2d7545dfad89d59c68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 12:55:39 +0900 Subject: [PATCH 3036/6909] Add scale to allow legacy icons to display correctly sized --- osu.Game/Beatmaps/BeatmapStatistic.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 5a466c24be..15036d1cd6 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osuTK; namespace osu.Game.Beatmaps { @@ -23,7 +24,7 @@ namespace osu.Game.Beatmaps public BeatmapStatistic() { #pragma warning disable 618 - CreateIcon = () => new SpriteIcon { Icon = Icon }; + CreateIcon = () => new SpriteIcon { Icon = Icon, Scale = new Vector2(0.6f) }; #pragma warning restore 618 } } From cd253ab055e97b67cdc7b59a2fcea6bdeb971a39 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 13:05:39 +0900 Subject: [PATCH 3037/6909] Further tweaks to get closer to design originals --- osu.Game/Beatmaps/BeatmapStatistic.cs | 2 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 15036d1cd6..825bb08246 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps public BeatmapStatistic() { #pragma warning disable 618 - CreateIcon = () => new SpriteIcon { Icon = Icon, Scale = new Vector2(0.6f) }; + CreateIcon = () => new SpriteIcon { Icon = Icon, Scale = new Vector2(0.7f) }; #pragma warning restore 618 } } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index b3bf306431..400f3e3063 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -428,7 +428,7 @@ namespace osu.Game.Screens.Select i.Origin = Anchor.Centre; i.RelativeSizeAxes = Axes.Both; i.Colour = Color4Extensions.FromHex(@"f7dd55"); - i.Size = new Vector2(0.8f); + i.Size = new Vector2(0.64f); }), } }, From f14a82e3a927950e23631ed1c5eeb430c94c8957 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 13:13:53 +0900 Subject: [PATCH 3038/6909] Remove unnecessary conversion --- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 11b6ad8c29..362c99ea3f 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -34,7 +34,7 @@ namespace osu.Game.Beatmaps protected override IBeatmap GetBeatmap() { if (BeatmapInfo.Path == null) - return BeatmapInfo?.Ruleset.CreateInstance().CreateBeatmapConverter(new Beatmap { BeatmapInfo = BeatmapInfo }).Beatmap; + return new Beatmap { BeatmapInfo = BeatmapInfo }; try { From d3fbc7cc53ea92a28e76ed07f11a6edc855c584e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 13:16:35 +0900 Subject: [PATCH 3039/6909] Use more direct reference in tests --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 4547613b5a..6c60d7b467 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -767,7 +767,7 @@ namespace osu.Game.Tests.Beatmaps.IO var osu = loadOsu(host); var manager = osu.Dependencies.Get(); - var working = osu.Dependencies.Get().CreateNew(new OsuRuleset().RulesetInfo); + var working = manager.CreateNew(new OsuRuleset().RulesetInfo); manager.Save(working.BeatmapInfo, working.Beatmap); @@ -794,7 +794,7 @@ namespace osu.Game.Tests.Beatmaps.IO var osu = loadOsu(host); var manager = osu.Dependencies.Get(); - var working = osu.Dependencies.Get().CreateNew(new OsuRuleset().RulesetInfo); + var working = manager.CreateNew(new OsuRuleset().RulesetInfo); ((Beatmap)working.Beatmap).HitObjects.Add(new HitCircle { StartTime = 5000 }); From fba253f131e7f5d279a0f011d404ef7685ab2c1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 13:17:43 +0900 Subject: [PATCH 3040/6909] Take user argument in CreateNew method parameters --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 5 +++-- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Screens/Edit/Editor.cs | 3 +-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 6c60d7b467..cef8105490 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -18,6 +18,7 @@ using osu.Game.IO; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Resources; +using osu.Game.Users; using SharpCompress.Archives; using SharpCompress.Archives.Zip; using SharpCompress.Common; @@ -767,7 +768,7 @@ namespace osu.Game.Tests.Beatmaps.IO var osu = loadOsu(host); var manager = osu.Dependencies.Get(); - var working = manager.CreateNew(new OsuRuleset().RulesetInfo); + var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER); manager.Save(working.BeatmapInfo, working.Beatmap); @@ -794,7 +795,7 @@ namespace osu.Game.Tests.Beatmaps.IO var osu = loadOsu(host); var manager = osu.Dependencies.Get(); - var working = manager.CreateNew(new OsuRuleset().RulesetInfo); + var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER); ((Beatmap)working.Beatmap).HitObjects.Add(new HitCircle { StartTime = 5000 }); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 12add76b46..844af31a16 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -95,13 +95,13 @@ namespace osu.Game.Beatmaps protected override bool ShouldDeleteArchive(string path) => Path.GetExtension(path)?.ToLowerInvariant() == ".osz"; - public WorkingBeatmap CreateNew(RulesetInfo ruleset) + public WorkingBeatmap CreateNew(RulesetInfo ruleset, User user) { var metadata = new BeatmapMetadata { Artist = "artist", Title = "title", - Author = User.SYSTEM_USER, + Author = user, }; var set = new BeatmapSetInfo diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b8c1932186..ef497e3246 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -98,8 +98,7 @@ namespace osu.Game.Screens.Edit if (Beatmap.Value is DummyWorkingBeatmap) { isNewBeatmap = true; - Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value); - Beatmap.Value.BeatmapSetInfo.Metadata.Author = api.LocalUser.Value; + Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); } try From 7c99f66cf518fe4696ac33c5a4e2ad1a01a7825f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 14:13:42 +0900 Subject: [PATCH 3041/6909] 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 a41c1a5864..7dfda5babb 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 d79806883e..cd7dcbb8db 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 16a8a1acb7..284b717a0f 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 25e142965d925ef6936b6424175b3736041dbcfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 15:01:32 +0900 Subject: [PATCH 3042/6909] Strongly type and expose default beatmap information icon implementations for other rulesets --- .../Beatmaps/CatchBeatmap.cs | 6 +-- .../Beatmaps/ManiaBeatmap.cs | 4 +- osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs | 6 +-- .../Beatmaps/TaikoBeatmap.cs | 6 +-- osu.Game/Beatmaps/BeatmapStatistic.cs | 2 +- osu.Game/Beatmaps/BeatmapStatisticIcon.cs | 43 +++++++++++++++++++ osu.Game/Beatmaps/BeatmapStatisticSprite.cs | 25 ----------- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 4 +- 8 files changed, 57 insertions(+), 39 deletions(-) create mode 100644 osu.Game/Beatmaps/BeatmapStatisticIcon.cs delete mode 100644 osu.Game/Beatmaps/BeatmapStatisticSprite.cs diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs index 5dc19ce15b..f009c10a9c 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmap.cs @@ -22,19 +22,19 @@ namespace osu.Game.Rulesets.Catch.Beatmaps { Name = @"Fruit Count", Content = fruits.ToString(), - CreateIcon = () => new BeatmapStatisticSprite("circles"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), }, new BeatmapStatistic { Name = @"Juice Stream Count", Content = juiceStreams.ToString(), - CreateIcon = () => new BeatmapStatisticSprite("sliders"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), }, new BeatmapStatistic { Name = @"Banana Shower Count", Content = bananaShowers.ToString(), - CreateIcon = () => new BeatmapStatisticSprite("spinners"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), } }; } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index f6b460f269..d1d5adea75 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -40,13 +40,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps new BeatmapStatistic { Name = @"Note Count", - CreateIcon = () => new BeatmapStatisticSprite("circles"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = notes.ToString(), }, new BeatmapStatistic { Name = @"Hold Note Count", - CreateIcon = () => new BeatmapStatisticSprite("sliders"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = holdnotes.ToString(), }, }; diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs index 513a9254ec..2d3cc3c103 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmap.cs @@ -22,19 +22,19 @@ namespace osu.Game.Rulesets.Osu.Beatmaps { Name = @"Circle Count", Content = circles.ToString(), - CreateIcon = () => new BeatmapStatisticSprite("circles"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), }, new BeatmapStatistic { Name = @"Slider Count", Content = sliders.ToString(), - CreateIcon = () => new BeatmapStatisticSprite("sliders"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), }, new BeatmapStatistic { Name = @"Spinner Count", Content = spinners.ToString(), - CreateIcon = () => new BeatmapStatisticSprite("spinners"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), } }; } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs index c0f8af4fff..16a0726c8c 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmap.cs @@ -21,19 +21,19 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps new BeatmapStatistic { Name = @"Hit Count", - CreateIcon = () => new BeatmapStatisticSprite("circles"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles), Content = hits.ToString(), }, new BeatmapStatistic { Name = @"Drumroll Count", - CreateIcon = () => new BeatmapStatisticSprite("sliders"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders), Content = drumrolls.ToString(), }, new BeatmapStatistic { Name = @"Swell Count", - CreateIcon = () => new BeatmapStatisticSprite("spinners"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners), Content = swells.ToString(), } }; diff --git a/osu.Game/Beatmaps/BeatmapStatistic.cs b/osu.Game/Beatmaps/BeatmapStatistic.cs index 825bb08246..9d87a20d60 100644 --- a/osu.Game/Beatmaps/BeatmapStatistic.cs +++ b/osu.Game/Beatmaps/BeatmapStatistic.cs @@ -14,7 +14,7 @@ namespace osu.Game.Beatmaps public IconUsage Icon = FontAwesome.Regular.QuestionCircle; /// - /// A function to create the icon for display purposes. + /// A function to create the icon for display purposes. Use default icons available via whenever possible for conformity. /// public Func CreateIcon; diff --git a/osu.Game/Beatmaps/BeatmapStatisticIcon.cs b/osu.Game/Beatmaps/BeatmapStatisticIcon.cs new file mode 100644 index 0000000000..181fb540df --- /dev/null +++ b/osu.Game/Beatmaps/BeatmapStatisticIcon.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 Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Beatmaps +{ + /// + /// A default implementation of an icon used to represent beatmap statistics. + /// + public class BeatmapStatisticIcon : Sprite + { + private readonly BeatmapStatisticsIconType iconType; + + public BeatmapStatisticIcon(BeatmapStatisticsIconType iconType) + { + this.iconType = iconType; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + Texture = textures.Get($"Icons/BeatmapDetails/{iconType.ToString().Kebaberize()}"); + } + } + + public enum BeatmapStatisticsIconType + { + Accuracy, + ApproachRate, + Bpm, + Circles, + HpDrain, + Length, + OverallDifficulty, + Size, + Sliders, + Spinners, + } +} diff --git a/osu.Game/Beatmaps/BeatmapStatisticSprite.cs b/osu.Game/Beatmaps/BeatmapStatisticSprite.cs deleted file mode 100644 index 1cb0bacf0f..0000000000 --- a/osu.Game/Beatmaps/BeatmapStatisticSprite.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.Allocation; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Graphics.Textures; - -namespace osu.Game.Beatmaps -{ - public class BeatmapStatisticSprite : Sprite - { - private readonly string iconName; - - public BeatmapStatisticSprite(string iconName) - { - this.iconName = iconName; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - Texture = textures.Get($"Icons/BeatmapDetails/{iconName}"); - } - } -} diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 400f3e3063..2a3eb8c67a 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -318,14 +318,14 @@ namespace osu.Game.Screens.Select labels.Add(new InfoLabel(new BeatmapStatistic { Name = "Length", - CreateIcon = () => new BeatmapStatisticSprite("length"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), Content = TimeSpan.FromMilliseconds(b.BeatmapInfo.Length).ToString(@"m\:ss"), })); labels.Add(new InfoLabel(new BeatmapStatistic { Name = "BPM", - CreateIcon = () => new BeatmapStatisticSprite("bpm"), + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), Content = getBPMRange(b), })); From 4399f5976c2e380937311e925652c4a4be60accc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 4 Sep 2020 15:20:55 +0900 Subject: [PATCH 3043/6909] Fix global mods being retained by rooms --- .../Multiplayer/TestSceneMatchSongSelect.cs | 22 +++++++++++++++++++ osu.Game/Screens/Select/MatchSongSelect.cs | 4 +--- 2 files changed, 23 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index 3d225aa0a9..faea32f90f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -162,6 +162,28 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("item 2 has rate 2", () => Precision.AlmostEquals(2, ((OsuModDoubleTime)Room.Playlist.Last().RequiredMods[0]).SpeedChange.Value)); } + /// + /// Tests that the global mod instances are not retained by the rooms, as global mod instances are retained and re-used by the mod select overlay. + /// + [Test] + public void TestGlobalModInstancesNotRetained() + { + OsuModDoubleTime mod = null; + + AddStep("set dt mod and store", () => + { + SelectedMods.Value = new[] { new OsuModDoubleTime() }; + + // Mod select overlay replaces our mod. + mod = (OsuModDoubleTime)SelectedMods.Value[0]; + }); + + AddStep("create item", () => songSelect.BeatmapDetails.CreateNewItem()); + + AddStep("change stored mod rate", () => mod.SpeedChange.Value = 2); + AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value)); + } + private class TestMatchSongSelect : MatchSongSelect { public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails; diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 96a48fa3ac..8692833a21 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -76,9 +76,7 @@ namespace osu.Game.Screens.Select item.Ruleset.Value = Ruleset.Value; item.RequiredMods.Clear(); - item.RequiredMods.AddRange(Mods.Value); - - Mods.Value = Mods.Value.Select(m => m.CreateCopy()).ToArray(); + item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); } } } From 0b3f2fe7df1d956d6cf3d53a263461957cb580ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 15:21:48 +0900 Subject: [PATCH 3044/6909] 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 a41c1a5864..a096370a05 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 d79806883e..cb8e30a084 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 16a8a1acb7..72b17d216d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From a15653c77cf79c1a6fc8628979d30c2dbb95492e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 16:15:57 +0900 Subject: [PATCH 3045/6909] Fix potential hard crash if ruleset settings fail to construct --- .../Overlays/Settings/Sections/GameplaySection.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index aca507f20a..c09e3a227d 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.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; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays.Settings.Sections.Gameplay; using osu.Game.Rulesets; using System.Linq; using osu.Framework.Graphics.Sprites; +using osu.Framework.Logging; namespace osu.Game.Overlays.Settings.Sections { @@ -35,8 +37,18 @@ namespace osu.Game.Overlays.Settings.Sections foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance())) { SettingsSubsection section = ruleset.CreateSettings(); + if (section != null) - Add(section); + { + try + { + Add(section); + } + catch (Exception e) + { + Logger.Error(e, $"Failed to load ruleset settings"); + } + } } } } From 54013790fc9c4d86612c9bcce0609c76aeaada9d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Sep 2020 09:45:24 +0300 Subject: [PATCH 3046/6909] Fix MusicController raising TrackChanged event twice --- osu.Game/Overlays/MusicController.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 17877a69a5..119aad5226 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -279,6 +279,10 @@ namespace osu.Game.Overlays private void changeBeatmap(WorkingBeatmap newWorking) { + // The provided beatmap is same as current, no need to do any changes. + if (newWorking == current) + return; + var lastWorking = current; TrackChangeDirection direction = TrackChangeDirection.None; From 42895e27b6f1188e6e623d5a921c99f4b4f0038f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Sep 2020 10:10:14 +0300 Subject: [PATCH 3047/6909] Expose track change results on the methods --- osu.Game/Overlays/MusicController.cs | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 119aad5226..ea72ef0b84 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -207,7 +207,18 @@ namespace osu.Game.Overlays /// /// Play the previous track or restart the current track if it's current time below . /// - public void PreviousTrack() => Schedule(() => prev()); + /// + /// Invoked when the operation has been performed successfully. + /// The result isn't returned directly to the caller because + /// the operation is scheduled and isn't performed immediately. + /// + /// A of the operation. + public ScheduledDelegate PreviousTrack(Action onSuccess = null) => Schedule(() => + { + PreviousTrackResult res = prev(); + if (res != PreviousTrackResult.None) + onSuccess?.Invoke(res); + }); /// /// Play the previous track or restart the current track if it's current time below . @@ -243,7 +254,18 @@ namespace osu.Game.Overlays /// /// Play the next random or playlist track. /// - public void NextTrack() => Schedule(() => next()); + /// + /// Invoked when the operation has been performed successfully. + /// The result isn't returned directly to the caller because + /// the operation is scheduled and isn't performed immediately. + /// + /// A of the operation. + public ScheduledDelegate NextTrack(Action onSuccess = null) => Schedule(() => + { + bool res = next(); + if (res) + onSuccess?.Invoke(); + }); private bool next() { From 001509df55015a32b5b6f6d582c805f0a4f459f8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Sep 2020 10:22:37 +0300 Subject: [PATCH 3048/6909] Move music global action handling to an own component Due to requiring components that are added at an OsuGame-level --- osu.Game/OsuGame.cs | 2 + osu.Game/Overlays/Music/MusicActionHandler.cs | 82 +++++++++++++++++++ osu.Game/Overlays/MusicController.cs | 56 +------------ 3 files changed, 85 insertions(+), 55 deletions(-) create mode 100644 osu.Game/Overlays/Music/MusicActionHandler.cs diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 164a40c6a5..a73469d836 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -38,6 +38,7 @@ using osu.Game.Input; using osu.Game.Overlays.Notifications; using osu.Game.Input.Bindings; using osu.Game.Online.Chat; +using osu.Game.Overlays.Music; using osu.Game.Skinning; using osuTK.Graphics; using osu.Game.Overlays.Volume; @@ -647,6 +648,7 @@ namespace osu.Game chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; Add(externalLinkOpener = new ExternalLinkOpener()); + Add(new MusicActionHandler()); // side overlays which cancel each other. var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications }; diff --git a/osu.Game/Overlays/Music/MusicActionHandler.cs b/osu.Game/Overlays/Music/MusicActionHandler.cs new file mode 100644 index 0000000000..17aa4c1d32 --- /dev/null +++ b/osu.Game/Overlays/Music/MusicActionHandler.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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Input.Bindings; +using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; +using osu.Game.Overlays.OSD; + +namespace osu.Game.Overlays.Music +{ + /// + /// Handles relating to music playback, and displays a via the cached accordingly. + /// + public class MusicActionHandler : Component, IKeyBindingHandler + { + [Resolved] + private IBindable beatmap { get; set; } + + [Resolved] + private MusicController musicController { get; set; } + + [Resolved] + private OnScreenDisplay onScreenDisplay { get; set; } + + public bool OnPressed(GlobalAction action) + { + if (beatmap.Disabled) + return false; + + switch (action) + { + case GlobalAction.MusicPlay: + if (musicController.TogglePause()) + onScreenDisplay.Display(new MusicActionToast(musicController.IsPlaying ? "Play track" : "Pause track")); + + return true; + + case GlobalAction.MusicNext: + musicController.NextTrack(() => + { + onScreenDisplay.Display(new MusicActionToast("Next track")); + }).RunTask(); + + return true; + + case GlobalAction.MusicPrev: + musicController.PreviousTrack(res => + { + switch (res) + { + case PreviousTrackResult.Restart: + onScreenDisplay.Display(new MusicActionToast("Restart track")); + break; + + case PreviousTrackResult.Previous: + onScreenDisplay.Display(new MusicActionToast("Previous track")); + break; + } + }).RunTask(); + + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } + + private class MusicActionToast : Toast + { + public MusicActionToast(string action) + : base("Music Playback", action, string.Empty) + { + } + } + } +} diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index ea72ef0b84..69722a8c0c 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -12,12 +12,9 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Framework.Threading; using osu.Game.Beatmaps; -using osu.Game.Input.Bindings; -using osu.Game.Overlays.OSD; using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays @@ -25,7 +22,7 @@ namespace osu.Game.Overlays /// /// Handles playback of the global music track. /// - public class MusicController : CompositeDrawable, IKeyBindingHandler + public class MusicController : CompositeDrawable { [Resolved] private BeatmapManager beatmaps { get; set; } @@ -62,9 +59,6 @@ namespace osu.Game.Overlays [Resolved] private IBindable> mods { get; set; } - [Resolved(canBeNull: true)] - private OnScreenDisplay onScreenDisplay { get; set; } - [NotNull] public DrawableTrack CurrentTrack { get; private set; } = new DrawableTrack(new TrackVirtual(1000)); @@ -428,54 +422,6 @@ namespace osu.Game.Overlays mod.ApplyToTrack(CurrentTrack); } } - - public bool OnPressed(GlobalAction action) - { - if (beatmap.Disabled) - return false; - - switch (action) - { - case GlobalAction.MusicPlay: - if (TogglePause()) - onScreenDisplay?.Display(new MusicControllerToast(IsPlaying ? "Play track" : "Pause track")); - return true; - - case GlobalAction.MusicNext: - if (next()) - onScreenDisplay?.Display(new MusicControllerToast("Next track")); - - return true; - - case GlobalAction.MusicPrev: - switch (prev()) - { - case PreviousTrackResult.Restart: - onScreenDisplay?.Display(new MusicControllerToast("Restart track")); - break; - - case PreviousTrackResult.Previous: - onScreenDisplay?.Display(new MusicControllerToast("Previous track")); - break; - } - - return true; - } - - return false; - } - - public void OnReleased(GlobalAction action) - { - } - - public class MusicControllerToast : Toast - { - public MusicControllerToast(string action) - : base("Music Playback", action, string.Empty) - { - } - } } public enum TrackChangeDirection From 4d9a06bde993bd416604e0b13e71a4fedf72873f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Sep 2020 10:23:06 +0300 Subject: [PATCH 3049/6909] Expose the global binding container to OsuGameTestScene --- osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs | 3 +++ osu.Game/OsuGameBase.cs | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index c4acf4f7da..e29d23ba75 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -14,6 +14,7 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -109,6 +110,8 @@ namespace osu.Game.Tests.Visual.Navigation public new OsuConfigManager LocalConfig => base.LocalConfig; + public new GlobalActionContainer GlobalBinding => base.GlobalBinding; + public new Bindable Beatmap => base.Beatmap; public new Bindable Ruleset => base.Ruleset; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 51b9b7278d..8e01bda6ec 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -64,6 +64,8 @@ namespace osu.Game protected FileStore FileStore; + protected GlobalActionContainer GlobalBinding; + protected KeyBindingStore KeyBindingStore; protected SettingsStore SettingsStore; @@ -250,10 +252,8 @@ namespace osu.Game AddInternal(apiAccess); AddInternal(RulesetConfigCache); - GlobalActionContainer globalBinding; - MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }; - MenuCursorContainer.Child = globalBinding = new GlobalActionContainer(this) + MenuCursorContainer.Child = GlobalBinding = new GlobalActionContainer(this) { RelativeSizeAxes = Axes.Both, Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both } @@ -261,8 +261,8 @@ namespace osu.Game base.Content.Add(CreateScalingContainer().WithChild(MenuCursorContainer)); - KeyBindingStore.Register(globalBinding); - dependencies.Cache(globalBinding); + KeyBindingStore.Register(GlobalBinding); + dependencies.Cache(GlobalBinding); PreviewTrackManager previewTrackManager; dependencies.Cache(previewTrackManager = new PreviewTrackManager()); From 314031d56d342f722d998d0897769ea2de4bde9c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Sep 2020 10:23:34 +0300 Subject: [PATCH 3050/6909] Add test cases ensuring music actions are handled from a game instance --- .../Menus/TestSceneMusicActionHandling.cs | 80 +++++++++++++++++++ 1 file changed, 80 insertions(+) create mode 100644 osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs new file mode 100644 index 0000000000..9121309489 --- /dev/null +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Input.Bindings; +using osu.Game.Overlays; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.Navigation; + +namespace osu.Game.Tests.Visual.Menus +{ + public class TestSceneMusicActionHandling : OsuGameTestScene + { + [Test] + public void TestMusicPlayAction() + { + AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething()); + AddStep("trigger music playback toggle action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPlay)); + AddAssert("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.IsUserPaused); + AddStep("trigger music playback toggle action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPlay)); + AddAssert("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.IsUserPaused); + } + + [Test] + public void TestMusicNavigationActions() + { + int importId = 0; + Queue<(WorkingBeatmap working, TrackChangeDirection dir)> trackChangeQueue = null; + + // ensure we have at least two beatmaps available to identify the direction the music controller navigated to. + AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(new BeatmapSetInfo + { + Beatmaps = new List + { + new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty(), + } + }, + Metadata = new BeatmapMetadata + { + Artist = $"a test map {importId++}", + Title = "title", + } + }).Wait(), 5); + + AddStep("import beatmap with track", () => + { + var setWithTrack = Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result; + Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Beatmaps.First()); + }); + + AddStep("bind to track change", () => + { + trackChangeQueue = new Queue<(WorkingBeatmap working, TrackChangeDirection dir)>(); + Game.MusicController.TrackChanged += (working, dir) => trackChangeQueue.Enqueue((working, dir)); + }); + + AddStep("seek track to 6 second", () => Game.MusicController.SeekTo(6000)); + AddUntilStep("wait for current time to update", () => Game.MusicController.CurrentTrack.CurrentTime > 5000); + + AddStep("trigger music prev action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPrev)); + AddAssert("no track change", () => trackChangeQueue.Count == 0); + AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000); + + AddStep("trigger music prev action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPrev)); + AddAssert("track changed to previous", () => + trackChangeQueue.Count == 1 && + trackChangeQueue.Dequeue().dir == TrackChangeDirection.Prev); + + AddStep("trigger music next action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicNext)); + AddAssert("track changed to next", () => + trackChangeQueue.Count == 1 && + trackChangeQueue.Dequeue().dir == TrackChangeDirection.Next); + } + } +} From 0500f24a1dbdede80d403e1f9b03fea9cfabc901 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Sep 2020 10:24:07 +0300 Subject: [PATCH 3051/6909] Remove now-redundant test case --- .../TestSceneNowPlayingOverlay.cs | 58 +------------------ 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs index c14a1ddbf2..475ab0c414 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNowPlayingOverlay.cs @@ -1,18 +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.Collections.Generic; -using System.Linq; 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; -using osu.Game.Tests.Resources; namespace osu.Game.Tests.Visual.UserInterface { @@ -24,14 +17,9 @@ namespace osu.Game.Tests.Visual.UserInterface private NowPlayingOverlay nowPlayingOverlay; - private RulesetStore rulesets; - [BackgroundDependencyLoader] - private void load(AudioManager audio, GameHost host) + private void load() { - 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 @@ -51,49 +39,5 @@ namespace osu.Game.Tests.Visual.UserInterface AddToggleStep(@"toggle beatmap lock", state => Beatmap.Disabled = state); AddStep(@"hide", () => nowPlayingOverlay.Hide()); } - - private BeatmapManager manager { get; set; } - - private int importId; - - [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 {importId++}", - Title = "title", - } - }).Wait(), 5); - - WorkingBeatmap currentBeatmap = null; - - AddStep("import beatmap with track", () => - { - var setWithTrack = manager.Import(TestResources.GetTestBeatmapForImport()).Result; - Beatmap.Value = currentBeatmap = manager.GetWorkingBeatmap(setWithTrack.Beatmaps.First()); - }); - - AddStep(@"Seek track to 6 second", () => musicController.SeekTo(6000)); - AddUntilStep(@"Wait for current time to update", () => musicController.CurrentTrack.CurrentTime > 5000); - - AddStep(@"Set previous", () => musicController.PreviousTrack()); - - AddAssert(@"Check beatmap didn't change", () => currentBeatmap == Beatmap.Value); - AddUntilStep("Wait for current time to update", () => musicController.CurrentTrack.CurrentTime < 5000); - - AddStep(@"Set previous", () => musicController.PreviousTrack()); - AddAssert(@"Check beatmap did change", () => currentBeatmap != Beatmap.Value); - } } } From 644f3375ac2210428cf7dfbd8afb58fb9e0dd0aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 16:28:19 +0900 Subject: [PATCH 3052/6909] Also catch exceptions in the construction call --- .../Settings/Sections/GameplaySection.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index c09e3a227d..f76b8e085b 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -36,18 +36,16 @@ namespace osu.Game.Overlays.Settings.Sections { foreach (Ruleset ruleset in rulesets.AvailableRulesets.Select(info => info.CreateInstance())) { - SettingsSubsection section = ruleset.CreateSettings(); - - if (section != null) + try { - try - { + SettingsSubsection section = ruleset.CreateSettings(); + + if (section != null) Add(section); - } - catch (Exception e) - { - Logger.Error(e, $"Failed to load ruleset settings"); - } + } + catch (Exception e) + { + Logger.Error(e, $"Failed to load ruleset settings"); } } } From ab057e6c654886d961b5abd7b2bdd795a93ed28f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Sep 2020 16:28:35 +0900 Subject: [PATCH 3053/6909] Remove unnecessary string interpolation --- osu.Game/Overlays/Settings/Sections/GameplaySection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index f76b8e085b..e5cebd28e2 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Settings.Sections } catch (Exception e) { - Logger.Error(e, $"Failed to load ruleset settings"); + Logger.Error(e, "Failed to load ruleset settings"); } } } From 65d541456ab8a408095c1b028930f72dfc72f902 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Sep 2020 11:11:07 +0300 Subject: [PATCH 3054/6909] Slight rewording --- osu.Game/Overlays/MusicController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 119aad5226..74a438a124 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -279,7 +279,7 @@ namespace osu.Game.Overlays private void changeBeatmap(WorkingBeatmap newWorking) { - // The provided beatmap is same as current, no need to do any changes. + // If the provided beatmap is same as current, then there is no need to do any changes. if (newWorking == current) return; From 4236e5fe71ceec15ffd6881532d8fda26dbd8f38 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Sep 2020 11:31:54 +0300 Subject: [PATCH 3055/6909] Replace useless "matching-code" comment with explanation of how it could happen Co-authored-by: Dean Herbert --- osu.Game/Overlays/MusicController.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 74a438a124..edef4d8589 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -279,7 +279,8 @@ namespace osu.Game.Overlays private void changeBeatmap(WorkingBeatmap newWorking) { - // If the provided beatmap is same as current, then there is no need to do any changes. + // This method can potentially be triggered multiple times as it is eagerly fired in next() / prev() to ensure correct execution order + // (changeBeatmap must be called before consumers receive the bindable changed event, which is not the case when called from the bindable itself). if (newWorking == current) return; From 3239576a239922e8dad80f61bff40a1b42205618 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Sep 2020 11:50:49 +0300 Subject: [PATCH 3056/6909] Minor rewording of new comment Co-authored-by: Dean Herbert --- osu.Game/Overlays/MusicController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index edef4d8589..31bd80d6f3 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -280,7 +280,7 @@ namespace osu.Game.Overlays private void changeBeatmap(WorkingBeatmap newWorking) { // This method can potentially be triggered multiple times as it is eagerly fired in next() / prev() to ensure correct execution order - // (changeBeatmap must be called before consumers receive the bindable changed event, which is not the case when called from the bindable itself). + // (changeBeatmap must be called before consumers receive the bindable changed event, which is not the case when the local beatmap bindable is updated directly). if (newWorking == current) return; From 569a56eccb85307cd3e4c080cb22492a116c2480 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 4 Sep 2020 13:33:23 +0300 Subject: [PATCH 3057/6909] Revert "Move adjustment to LegacySkinConfiguration as a default value" This reverts commit 18927304f10bd912fc9a09cb22aaed3dd38974d0. --- osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs | 4 ++++ osu.Game/Skinning/LegacySkinConfiguration.cs | 9 --------- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs index 21df49d80b..aad8b189d9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs @@ -18,6 +18,10 @@ namespace osu.Game.Rulesets.Osu.Skinning { private const float shadow_portion = 1 - (OsuLegacySkinTransformer.LEGACY_CIRCLE_RADIUS / OsuHitObject.OBJECT_RADIUS); + protected new float CalculatedBorderPortion + // Roughly matches osu!stable's slider border portions. + => base.CalculatedBorderPortion * 0.77f; + public new Color4 AccentColour => new Color4(base.AccentColour.R, base.AccentColour.G, base.AccentColour.B, base.AccentColour.A * 0.70f); protected override Color4 ColourAt(float position) diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index b980d727ed..41b7aea34b 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.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.Globalization; - namespace osu.Game.Skinning { public class LegacySkinConfiguration : SkinConfiguration @@ -14,13 +12,6 @@ namespace osu.Game.Skinning /// public decimal? LegacyVersion { get; internal set; } - public LegacySkinConfiguration() - { - // Roughly matches osu!stable's slider border portions. - // Can't use nameof(SliderBorderSize) as the lookup enum is declared in the osu! ruleset. - ConfigDictionary["SliderBorderSize"] = 0.77f.ToString(CultureInfo.InvariantCulture); - } - public enum LegacySetting { Version, From 1143d5d9928d58fb2dd058b2b1dca31f1b868281 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 4 Sep 2020 20:34:26 +0900 Subject: [PATCH 3058/6909] Update class exclusion for dynamic compilation --- 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 -- .../Navigation/TestScenePresentScore.cs | 1 + osu.Game/Beatmaps/BeatmapInfo.cs | 2 ++ osu.Game/Beatmaps/BeatmapMetadata.cs | 2 ++ osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 ++ osu.Game/Beatmaps/WorkingBeatmap.cs | 2 ++ osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Input/GameIdleTracker.cs | 25 +++++++++++++++++++ osu.Game/OsuGame.cs | 24 ------------------ osu.Game/Rulesets/Mods/Mod.cs | 2 ++ osu.Game/Rulesets/Ruleset.cs | 2 ++ osu.Game/Rulesets/RulesetInfo.cs | 2 ++ osu.Game/Screens/ScorePresentType.cs | 11 ++++++++ osu.Game/Tests/Visual/OsuTestScene.cs | 1 + 17 files changed, 54 insertions(+), 32 deletions(-) create mode 100644 osu.Game/Input/GameIdleTracker.cs create mode 100644 osu.Game/Screens/ScorePresentType.cs diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 9437023c70..ca75a816f1 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -21,13 +21,11 @@ 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 37b34d1721..71ac85dd1b 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -12,7 +12,6 @@ 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; @@ -35,7 +34,6 @@ 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 f527eb2312..7f4a0dcbbb 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -30,14 +30,12 @@ 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 dbc32f2c3e..9d485e3f20 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -22,7 +22,6 @@ 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; @@ -32,7 +31,6 @@ 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); diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index b2e18849c9..a899d072ac 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Scoring; +using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index 3860f12baa..c5be5810e9 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.IO.Serialization; using osu.Game.Rulesets; @@ -14,6 +15,7 @@ using osu.Game.Scoring; namespace osu.Game.Beatmaps { + [ExcludeFromDynamicCompile] [Serializable] public class BeatmapInfo : IEquatable, IJsonSerializable, IHasPrimaryKey { diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 775d78f1fb..39b3c23ddd 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -6,11 +6,13 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Testing; using osu.Game.Database; using osu.Game.Users; namespace osu.Game.Beatmaps { + [ExcludeFromDynamicCompile] [Serializable] public class BeatmapMetadata : IEquatable, IHasPrimaryKey { diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index a8b83dca38..b76d780860 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; +using osu.Framework.Testing; using osu.Game.Database; namespace osu.Game.Beatmaps { + [ExcludeFromDynamicCompile] public class BeatmapSetInfo : IHasPrimaryKey, IHasFiles, ISoftDelete, IEquatable { public int ID { get; set; } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 6a161e6e04..19b54e1783 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -13,6 +13,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Framework.Statistics; +using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Types; @@ -22,6 +23,7 @@ using osu.Game.Storyboards; namespace osu.Game.Beatmaps { + [ExcludeFromDynamicCompile] public abstract class WorkingBeatmap : IWorkingBeatmap { public readonly BeatmapInfo BeatmapInfo; diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a8a8794320..e5432cb84e 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -6,6 +6,7 @@ using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; @@ -13,6 +14,7 @@ using osu.Game.Screens.Select.Filter; namespace osu.Game.Configuration { + [ExcludeFromDynamicCompile] public class OsuConfigManager : IniConfigManager { protected override void InitialiseDefaults() diff --git a/osu.Game/Input/GameIdleTracker.cs b/osu.Game/Input/GameIdleTracker.cs new file mode 100644 index 0000000000..260be7e5c9 --- /dev/null +++ b/osu.Game/Input/GameIdleTracker.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.Input; + +namespace osu.Game.Input +{ + public class GameIdleTracker : IdleTracker + { + private InputManager inputManager; + + public GameIdleTracker(int time) + : base(time) + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + inputManager = GetContainingInputManager(); + } + + protected override bool AllowIdle => inputManager.FocusedDrawable == null; + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 164a40c6a5..a8722d03ab 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -718,24 +718,6 @@ namespace osu.Game overlayContent.ChangeChildDepth(overlay, (float)-Clock.CurrentTime); } - public class GameIdleTracker : IdleTracker - { - private InputManager inputManager; - - public GameIdleTracker(int time) - : base(time) - { - } - - protected override void LoadComplete() - { - base.LoadComplete(); - inputManager = GetContainingInputManager(); - } - - protected override bool AllowIdle => inputManager.FocusedDrawable == null; - } - private void forwardLoggedErrorsToNotifications() { int recentLogCount = 0; @@ -991,10 +973,4 @@ namespace osu.Game Exit(); } } - - public enum ScorePresentType - { - Results, - Gameplay - } } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 52ffa0ad2a..b8dc7a2661 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -9,6 +9,7 @@ using System.Reflection; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.IO.Serialization; using osu.Game.Rulesets.UI; @@ -18,6 +19,7 @@ namespace osu.Game.Rulesets.Mods /// /// The base class for gameplay modifiers. /// + [ExcludeFromDynamicCompile] public abstract class Mod : IMod, IJsonSerializable { /// diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 3a7f433a37..915544d010 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -23,10 +23,12 @@ using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Users; using JetBrains.Annotations; +using osu.Framework.Testing; using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets { + [ExcludeFromDynamicCompile] public abstract class Ruleset { public RulesetInfo RulesetInfo { get; internal set; } diff --git a/osu.Game/Rulesets/RulesetInfo.cs b/osu.Game/Rulesets/RulesetInfo.cs index 2e32b96084..d5aca8c650 100644 --- a/osu.Game/Rulesets/RulesetInfo.cs +++ b/osu.Game/Rulesets/RulesetInfo.cs @@ -5,9 +5,11 @@ using System; using System.Diagnostics.CodeAnalysis; using System.Linq; using Newtonsoft.Json; +using osu.Framework.Testing; namespace osu.Game.Rulesets { + [ExcludeFromDynamicCompile] public class RulesetInfo : IEquatable { public int? ID { get; set; } diff --git a/osu.Game/Screens/ScorePresentType.cs b/osu.Game/Screens/ScorePresentType.cs new file mode 100644 index 0000000000..3216f92091 --- /dev/null +++ b/osu.Game/Screens/ScorePresentType.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 +{ + public enum ScorePresentType + { + Results, + Gameplay + } +} diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 756074c0b3..4db5139813 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -30,6 +30,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual { + [ExcludeFromDynamicCompile] public abstract class OsuTestScene : TestScene { protected Bindable Beatmap { get; private set; } From ebd11ae0b7a3afe13d0054ed845dc42d9efa944c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 5 Sep 2020 03:52:07 +0900 Subject: [PATCH 3059/6909] Add a collection management dialog --- .../Collections/TestSceneCollectionDialog.cs | 26 +++ osu.Game/Collections/CollectionDialog.cs | 125 ++++++++++++++ osu.Game/Collections/CollectionList.cs | 27 ++++ osu.Game/Collections/CollectionListItem.cs | 152 ++++++++++++++++++ .../Collections/DeleteCollectionDialog.cs | 36 +++++ osu.Game/OsuGame.cs | 2 + .../Carousel/DrawableCarouselBeatmap.cs | 14 +- .../Carousel/DrawableCarouselBeatmapSet.cs | 14 +- 8 files changed, 384 insertions(+), 12 deletions(-) create mode 100644 osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs create mode 100644 osu.Game/Collections/CollectionDialog.cs create mode 100644 osu.Game/Collections/CollectionList.cs create mode 100644 osu.Game/Collections/CollectionListItem.cs create mode 100644 osu.Game/Collections/DeleteCollectionDialog.cs diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs new file mode 100644 index 0000000000..f810361610 --- /dev/null +++ b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Collections; +using osu.Game.Overlays; + +namespace osu.Game.Tests.Visual.Collections +{ + public class TestSceneCollectionDialog : OsuTestScene + { + [Cached] + private DialogOverlay dialogOverlay; + + public TestSceneCollectionDialog() + { + Children = new Drawable[] + { + new CollectionDialog { State = { Value = Visibility.Visible } }, + dialogOverlay = new DialogOverlay() + }; + } + } +} diff --git a/osu.Game/Collections/CollectionDialog.cs b/osu.Game/Collections/CollectionDialog.cs new file mode 100644 index 0000000000..e911b507e5 --- /dev/null +++ b/osu.Game/Collections/CollectionDialog.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.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Collections +{ + public class CollectionDialog : OsuFocusedOverlayContainer + { + private const double enter_duration = 500; + private const double exit_duration = 200; + + [Resolved] + private CollectionManager collectionManager { get; set; } + + public CollectionDialog() + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(0.5f, 0.8f); + + Masking = true; + CornerRadius = 10; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Children = new Drawable[] + { + new Box + { + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 50), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 50), + }, + Content = new[] + { + new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Manage collections", + Font = OsuFont.GetFont(size: 30), + } + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.GreySeafoamDarker + }, + new CollectionList + { + RelativeSizeAxes = Axes.Both, + Items = { BindTarget = collectionManager.Collections } + } + } + } + }, + new Drawable[] + { + new OsuButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Padding = new MarginPadding(10), + Text = "Create new collection", + Action = () => collectionManager.Collections.Add(new BeatmapCollection { Name = "My new collection" }) + }, + }, + } + } + } + }; + } + + protected override void PopIn() + { + base.PopIn(); + + this.FadeIn(enter_duration, Easing.OutQuint); + this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutElastic); + } + + protected override void PopOut() + { + base.PopOut(); + + this.FadeOut(exit_duration, Easing.OutQuint); + this.ScaleTo(0.9f, exit_duration); + } + } +} diff --git a/osu.Game/Collections/CollectionList.cs b/osu.Game/Collections/CollectionList.cs new file mode 100644 index 0000000000..990f82e702 --- /dev/null +++ b/osu.Game/Collections/CollectionList.cs @@ -0,0 +1,27 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osuTK; + +namespace osu.Game.Collections +{ + public class CollectionList : OsuRearrangeableListContainer + { + protected override ScrollContainer CreateScrollContainer() => base.CreateScrollContainer().With(d => + { + d.ScrollbarVisible = false; + }); + + protected override FillFlowContainer> CreateListFillFlowContainer() => new FillFlowContainer> + { + LayoutDuration = 200, + LayoutEasing = Easing.OutQuint, + Spacing = new Vector2(0, 2) + }; + + protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapCollection item) => new CollectionListItem(item); + } +} diff --git a/osu.Game/Collections/CollectionListItem.cs b/osu.Game/Collections/CollectionListItem.cs new file mode 100644 index 0000000000..527ed57cd0 --- /dev/null +++ b/osu.Game/Collections/CollectionListItem.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 System; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Collections +{ + public class CollectionListItem : OsuRearrangeableListItem + { + private const float item_height = 35; + + public CollectionListItem(BeatmapCollection item) + : base(item) + { + Padding = new MarginPadding { Right = 20 }; + } + + protected override Drawable CreateContent() => new ItemContent(Model); + + private class ItemContent : CircularContainer + { + private readonly BeatmapCollection collection; + + private ItemTextBox textBox; + + public ItemContent(BeatmapCollection collection) + { + this.collection = collection; + + RelativeSizeAxes = Axes.X; + Height = item_height; + Masking = true; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Children = new Drawable[] + { + new DeleteButton(collection) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v) + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Right = item_height / 2 }, + Children = new Drawable[] + { + textBox = new ItemTextBox + { + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + CornerRadius = item_height / 2, + Text = collection.Name + }, + } + }, + }; + } + } + + private class ItemTextBox : OsuTextBox + { + protected override float LeftRightPadding => item_height / 2; + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundUnfocused = colours.GreySeafoamDarker.Darken(0.5f); + BackgroundFocused = colours.GreySeafoam; + } + } + + private class DeleteButton : CompositeDrawable + { + public Func IsTextBoxHovered; + + [Resolved(CanBeNull = true)] + private DialogOverlay dialogOverlay { get; set; } + + private readonly BeatmapCollection collection; + + private Drawable background; + + public DeleteButton(BeatmapCollection collection) + { + this.collection = collection; + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + + Alpha = 0.1f; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChildren = new[] + { + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Red + }, + new SpriteIcon + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -6, + Size = new Vector2(10), + Icon = FontAwesome.Solid.Trash + } + }; + } + + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos); + + protected override bool OnHover(HoverEvent e) + { + this.FadeTo(1f, 100, Easing.Out); + return false; + } + + protected override void OnHoverLost(HoverLostEvent e) + { + this.FadeTo(0.1f, 100); + } + + protected override bool OnClick(ClickEvent e) + { + background.FlashColour(Color4.White, 150); + dialogOverlay?.Push(new DeleteCollectionDialog(collection)); + return true; + } + } + } +} diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs new file mode 100644 index 0000000000..a7677e9de2 --- /dev/null +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -0,0 +1,36 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Collections +{ + public class DeleteCollectionDialog : PopupDialog + { + [Resolved] + private CollectionManager collectionManager { get; set; } + + public DeleteCollectionDialog(BeatmapCollection collection) + { + HeaderText = "Confirm deletion of"; + BodyText = collection.Name; + + Icon = FontAwesome.Regular.TrashAlt; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Yes. Go for it.", + Action = () => collectionManager.Collections.Remove(collection) + }, + new PopupDialogCancelButton + { + Text = @"No! Abort mission!", + }, + }; + } + } +} diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5008a3cf3b..1f69888d2e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -31,6 +31,7 @@ using osu.Framework.Input.Events; using osu.Framework.Platform; using osu.Framework.Threading; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -632,6 +633,7 @@ namespace osu.Game loadComponentSingleFile(CreateUpdateManager(), Add, true); // overlay elements + loadComponentSingleFile(new CollectionDialog(), overlayContent.Add, true); loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 1bd4447248..fe4c95f4e3 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -51,6 +51,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private CollectionManager collectionManager { get; set; } + [Resolved(CanBeNull = true)] + private CollectionDialog collectionDialog { get; set; } + private IBindable starDifficultyBindable; private CancellationTokenSource starDifficultyCancellationSource; @@ -224,12 +227,11 @@ namespace osu.Game.Screens.Select.Carousel if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); - items.Add(new OsuMenuItem("Add to...") - { - Items = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem) - .Append(new OsuMenuItem("More...", MenuItemType.Standard, () => { })) - .ToArray() - }); + var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem).ToList(); + if (collectionDialog != null) + collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, collectionDialog.Show)); + + items.Add(new OsuMenuItem("Add to...") { Items = collectionItems }); return items.ToArray(); } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index e05b5ee951..8a9b0dc5b3 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -38,6 +38,9 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private CollectionManager collectionManager { get; set; } + [Resolved(CanBeNull = true)] + private CollectionDialog collectionDialog { get; set; } + private readonly BeatmapSetInfo beatmapSet; public DrawableCarouselBeatmapSet(CarouselBeatmapSet set) @@ -145,12 +148,11 @@ namespace osu.Game.Screens.Select.Carousel if (dialogOverlay != null) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); - items.Add(new OsuMenuItem("Add all to...") - { - Items = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem) - .Append(new OsuMenuItem("More...", MenuItemType.Standard, () => { })) - .ToArray() - }); + var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem).ToList(); + if (collectionDialog != null) + collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, collectionDialog.Show)); + + items.Add(new OsuMenuItem("Add all to...") { Items = collectionItems }); return items.ToArray(); } From 345fb9d8e02f057c33a8430126463285c9a27c1e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 5 Sep 2020 03:55:43 +0900 Subject: [PATCH 3060/6909] Rename classes --- .../Visual/Collections/TestSceneCollectionDialog.cs | 2 +- .../{CollectionManager.cs => BeatmapCollectionManager.cs} | 2 +- osu.Game/Collections/DeleteCollectionDialog.cs | 2 +- .../{CollectionList.cs => DrawableCollectionList.cs} | 4 ++-- ...ollectionListItem.cs => DrawableCollectionListItem.cs} | 4 ++-- .../{CollectionDialog.cs => ManageCollectionDialog.cs} | 8 ++++---- osu.Game/OsuGame.cs | 2 +- osu.Game/OsuGameBase.cs | 4 ++-- .../Settings/Sections/Maintenance/GeneralSettings.cs | 8 ++++---- .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 8 ++++---- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 8 ++++---- osu.Game/Screens/Select/FilterControl.cs | 2 +- 12 files changed, 27 insertions(+), 27 deletions(-) rename osu.Game/Collections/{CollectionManager.cs => BeatmapCollectionManager.cs} (99%) rename osu.Game/Collections/{CollectionList.cs => DrawableCollectionList.cs} (83%) rename osu.Game/Collections/{CollectionListItem.cs => DrawableCollectionListItem.cs} (96%) rename osu.Game/Collections/{CollectionDialog.cs => ManageCollectionDialog.cs} (94%) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs index f810361610..247d27f67a 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Collections { Children = new Drawable[] { - new CollectionDialog { State = { Value = Visibility.Visible } }, + new ManageCollectionDialog { State = { Value = Visibility.Visible } }, dialogOverlay = new DialogOverlay() }; } diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/BeatmapCollectionManager.cs similarity index 99% rename from osu.Game/Collections/CollectionManager.cs rename to osu.Game/Collections/BeatmapCollectionManager.cs index 20bf96da9d..0b066708d8 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/BeatmapCollectionManager.cs @@ -18,7 +18,7 @@ using osu.Game.IO.Legacy; namespace osu.Game.Collections { - public class CollectionManager : CompositeDrawable + public class BeatmapCollectionManager : CompositeDrawable { /// /// Database version in YYYYMMDD format (matching stable). diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index a7677e9de2..81bedca638 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -10,7 +10,7 @@ namespace osu.Game.Collections public class DeleteCollectionDialog : PopupDialog { [Resolved] - private CollectionManager collectionManager { get; set; } + private BeatmapCollectionManager collectionManager { get; set; } public DeleteCollectionDialog(BeatmapCollection collection) { diff --git a/osu.Game/Collections/CollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs similarity index 83% rename from osu.Game/Collections/CollectionList.cs rename to osu.Game/Collections/DrawableCollectionList.cs index 990f82e702..ab146c17b6 100644 --- a/osu.Game/Collections/CollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -8,7 +8,7 @@ using osuTK; namespace osu.Game.Collections { - public class CollectionList : OsuRearrangeableListContainer + public class DrawableCollectionList : OsuRearrangeableListContainer { protected override ScrollContainer CreateScrollContainer() => base.CreateScrollContainer().With(d => { @@ -22,6 +22,6 @@ namespace osu.Game.Collections Spacing = new Vector2(0, 2) }; - protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapCollection item) => new CollectionListItem(item); + protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapCollection item) => new DrawableCollectionListItem(item); } } diff --git a/osu.Game/Collections/CollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs similarity index 96% rename from osu.Game/Collections/CollectionListItem.cs rename to osu.Game/Collections/DrawableCollectionListItem.cs index 527ed57cd0..67756cdb43 100644 --- a/osu.Game/Collections/CollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -18,11 +18,11 @@ using osuTK.Graphics; namespace osu.Game.Collections { - public class CollectionListItem : OsuRearrangeableListItem + public class DrawableCollectionListItem : OsuRearrangeableListItem { private const float item_height = 35; - public CollectionListItem(BeatmapCollection item) + public DrawableCollectionListItem(BeatmapCollection item) : base(item) { Padding = new MarginPadding { Right = 20 }; diff --git a/osu.Game/Collections/CollectionDialog.cs b/osu.Game/Collections/ManageCollectionDialog.cs similarity index 94% rename from osu.Game/Collections/CollectionDialog.cs rename to osu.Game/Collections/ManageCollectionDialog.cs index e911b507e5..6a0d815e43 100644 --- a/osu.Game/Collections/CollectionDialog.cs +++ b/osu.Game/Collections/ManageCollectionDialog.cs @@ -13,15 +13,15 @@ using osuTK; namespace osu.Game.Collections { - public class CollectionDialog : OsuFocusedOverlayContainer + public class ManageCollectionDialog : OsuFocusedOverlayContainer { private const double enter_duration = 500; private const double exit_duration = 200; [Resolved] - private CollectionManager collectionManager { get; set; } + private BeatmapCollectionManager collectionManager { get; set; } - public CollectionDialog() + public ManageCollectionDialog() { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -79,7 +79,7 @@ namespace osu.Game.Collections RelativeSizeAxes = Axes.Both, Colour = colours.GreySeafoamDarker }, - new CollectionList + new DrawableCollectionList { RelativeSizeAxes = Axes.Both, Items = { BindTarget = collectionManager.Collections } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1f69888d2e..2f8f1b2f17 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -633,7 +633,7 @@ namespace osu.Game loadComponentSingleFile(CreateUpdateManager(), Add, true); // overlay elements - loadComponentSingleFile(new CollectionDialog(), overlayContent.Add, true); + loadComponentSingleFile(new ManageCollectionDialog(), overlayContent.Add, true); loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d512f57ca5..a7efecadfd 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -56,7 +56,7 @@ namespace osu.Game protected BeatmapManager BeatmapManager; - protected CollectionManager CollectionManager; + protected BeatmapCollectionManager CollectionManager; protected ScoreManager ScoreManager; @@ -225,7 +225,7 @@ namespace osu.Game dependencies.Cache(difficultyManager); AddInternal(difficultyManager); - dependencies.Cache(CollectionManager = new CollectionManager()); + dependencies.Cache(CollectionManager = new BeatmapCollectionManager()); AddInternal(CollectionManager); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 21a5ed6b31..74f9920ae0 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private TriangleButton undeleteButton; [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, CollectionManager collections, DialogOverlay dialogOverlay) + private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, BeatmapCollectionManager collectionManager, DialogOverlay dialogOverlay) { if (beatmaps.SupportsImportFromStable) { @@ -108,7 +108,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance } }); - if (collections.SupportsImportFromStable) + if (collectionManager.SupportsImportFromStable) { Add(importCollectionsButton = new SettingsButton { @@ -116,7 +116,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Action = () => { importCollectionsButton.Enabled.Value = false; - collections.ImportFromStableAsync().ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true)); + collectionManager.ImportFromStableAsync().ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true)); } }); } @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Text = "Delete ALL collections", Action = () => { - dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => collections.Collections.Clear())); + dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => collectionManager.Collections.Clear())); } }); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index fe4c95f4e3..8fd428d26e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -49,10 +49,10 @@ namespace osu.Game.Screens.Select.Carousel private BeatmapDifficultyManager difficultyManager { get; set; } [Resolved] - private CollectionManager collectionManager { get; set; } + private BeatmapCollectionManager collectionManager { get; set; } [Resolved(CanBeNull = true)] - private CollectionDialog collectionDialog { get; set; } + private ManageCollectionDialog manageCollectionDialog { get; set; } private IBindable starDifficultyBindable; private CancellationTokenSource starDifficultyCancellationSource; @@ -228,8 +228,8 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem).ToList(); - if (collectionDialog != null) - collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, collectionDialog.Show)); + if (manageCollectionDialog != null) + collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionDialog.Show)); items.Add(new OsuMenuItem("Add to...") { Items = collectionItems }); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 8a9b0dc5b3..12c6b320e9 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -36,10 +36,10 @@ namespace osu.Game.Screens.Select.Carousel private DialogOverlay dialogOverlay { get; set; } [Resolved] - private CollectionManager collectionManager { get; set; } + private BeatmapCollectionManager collectionManager { get; set; } [Resolved(CanBeNull = true)] - private CollectionDialog collectionDialog { get; set; } + private ManageCollectionDialog manageCollectionDialog { get; set; } private readonly BeatmapSetInfo beatmapSet; @@ -149,8 +149,8 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem).ToList(); - if (collectionDialog != null) - collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, collectionDialog.Show)); + if (manageCollectionDialog != null) + collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionDialog.Show)); items.Add(new OsuMenuItem("Add all to...") { Items = collectionItems }); diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 0b85ae0e6a..0db24f0738 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(CollectionManager collectionManager) + private void load(BeatmapCollectionManager collectionManager) { collections.BindTo(collectionManager.Collections); collections.CollectionChanged += (_, __) => collectionsChanged(); From 4b4dd02942c6e8d1ec7faa6490cce8051958c47c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 5 Sep 2020 04:43:51 +0900 Subject: [PATCH 3061/6909] Make collection name a bindable --- osu.Game/Collections/BeatmapCollectionManager.cs | 8 ++++---- osu.Game/Collections/DeleteCollectionDialog.cs | 2 +- osu.Game/Collections/DrawableCollectionListItem.cs | 2 +- osu.Game/Collections/ManageCollectionDialog.cs | 5 ++++- .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- osu.Game/Screens/Select/FilterControl.cs | 2 +- 7 files changed, 13 insertions(+), 10 deletions(-) diff --git a/osu.Game/Collections/BeatmapCollectionManager.cs b/osu.Game/Collections/BeatmapCollectionManager.cs index 0b066708d8..3e5976300f 100644 --- a/osu.Game/Collections/BeatmapCollectionManager.cs +++ b/osu.Game/Collections/BeatmapCollectionManager.cs @@ -97,7 +97,7 @@ namespace osu.Game.Collections { var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name); if (existing == null) - Collections.Add(existing = new BeatmapCollection { Name = newCol.Name }); + Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } }); foreach (var newBeatmap in newCol.Beatmaps) { @@ -122,7 +122,7 @@ namespace osu.Game.Collections for (int i = 0; i < collectionCount; i++) { - var collection = new BeatmapCollection { Name = sr.ReadString() }; + var collection = new BeatmapCollection { Name = { Value = sr.ReadString() } }; int mapCount = sr.ReadInt32(); for (int j = 0; j < mapCount; j++) @@ -183,7 +183,7 @@ namespace osu.Game.Collections foreach (var c in Collections) { - sw.Write(c.Name); + sw.Write(c.Name.Value); sw.Write(c.Beatmaps.Count); foreach (var b in c.Beatmaps) @@ -221,7 +221,7 @@ namespace osu.Game.Collections /// public event Action Changed; - public string Name; + public readonly Bindable Name = new Bindable(); public readonly BindableList Beatmaps = new BindableList(); diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index 81bedca638..f2b8de7c1e 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -15,7 +15,7 @@ namespace osu.Game.Collections public DeleteCollectionDialog(BeatmapCollection collection) { HeaderText = "Confirm deletion of"; - BodyText = collection.Name; + BodyText = collection.Name.Value; Icon = FontAwesome.Regular.TrashAlt; diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 67756cdb43..7c1a2e1287 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -67,7 +67,7 @@ namespace osu.Game.Collections RelativeSizeAxes = Axes.Both, Size = Vector2.One, CornerRadius = item_height / 2, - Text = collection.Name + Current = collection.Name }, } }, diff --git a/osu.Game/Collections/ManageCollectionDialog.cs b/osu.Game/Collections/ManageCollectionDialog.cs index 6a0d815e43..1e222a9c71 100644 --- a/osu.Game/Collections/ManageCollectionDialog.cs +++ b/osu.Game/Collections/ManageCollectionDialog.cs @@ -97,7 +97,7 @@ namespace osu.Game.Collections Size = Vector2.One, Padding = new MarginPadding(10), Text = "Create new collection", - Action = () => collectionManager.Collections.Add(new BeatmapCollection { Name = "My new collection" }) + Action = () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "My new collection" } }) }, }, } @@ -120,6 +120,9 @@ namespace osu.Game.Collections this.FadeOut(exit_duration, Easing.OutQuint); this.ScaleTo(0.9f, exit_duration); + + // Ensure that textboxes commit + GetContainingInputManager()?.TriggerFocusContention(this); } } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 8fd428d26e..6c43bf5bed 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -239,7 +239,7 @@ namespace osu.Game.Screens.Select.Carousel private MenuItem createCollectionMenuItem(BeatmapCollection collection) { - return new ToggleMenuItem(collection.Name, MenuItemType.Standard, s => + return new ToggleMenuItem(collection.Name.Value, MenuItemType.Standard, s => { if (s) collection.Beatmaps.Add(beatmap); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 12c6b320e9..fc262730cb 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -171,7 +171,7 @@ namespace osu.Game.Screens.Select.Carousel else state = TernaryState.False; - return new TernaryStateMenuItem(collection.Name, MenuItemType.Standard, s => + return new TernaryStateMenuItem(collection.Name.Value, MenuItemType.Standard, s => { foreach (var b in beatmapSet.Beatmaps) { diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 0db24f0738..a66bcfb3b1 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -270,7 +270,7 @@ namespace osu.Game.Screens.Select Current.TriggerChange(); } - protected override string GenerateItemText(CollectionFilter item) => item.Collection?.Name ?? "All beatmaps"; + protected override string GenerateItemText(CollectionFilter item) => item.Collection?.Name.Value ?? "All beatmaps"; protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader(); From 33b76015d80df41618867a94227fbe2251bae200 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 6 Sep 2020 01:54:08 +0300 Subject: [PATCH 3062/6909] Fix MusicActionHandler unnecessarily depending on OnScreenDisplay's existance --- osu.Game/Overlays/Music/MusicActionHandler.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Music/MusicActionHandler.cs b/osu.Game/Overlays/Music/MusicActionHandler.cs index 17aa4c1d32..cd8548c1c0 100644 --- a/osu.Game/Overlays/Music/MusicActionHandler.cs +++ b/osu.Game/Overlays/Music/MusicActionHandler.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Music [Resolved] private MusicController musicController { get; set; } - [Resolved] + [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } public bool OnPressed(GlobalAction action) @@ -34,14 +34,14 @@ namespace osu.Game.Overlays.Music { case GlobalAction.MusicPlay: if (musicController.TogglePause()) - onScreenDisplay.Display(new MusicActionToast(musicController.IsPlaying ? "Play track" : "Pause track")); + onScreenDisplay?.Display(new MusicActionToast(musicController.IsPlaying ? "Play track" : "Pause track")); return true; case GlobalAction.MusicNext: musicController.NextTrack(() => { - onScreenDisplay.Display(new MusicActionToast("Next track")); + onScreenDisplay?.Display(new MusicActionToast("Next track")); }).RunTask(); return true; @@ -52,11 +52,11 @@ namespace osu.Game.Overlays.Music switch (res) { case PreviousTrackResult.Restart: - onScreenDisplay.Display(new MusicActionToast("Restart track")); + onScreenDisplay?.Display(new MusicActionToast("Restart track")); break; case PreviousTrackResult.Previous: - onScreenDisplay.Display(new MusicActionToast("Previous track")); + onScreenDisplay?.Display(new MusicActionToast("Previous track")); break; } }).RunTask(); From e37a3a84fd2e232d6f1eeb0f71eec3c64417256d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 5 Sep 2020 15:40:26 +0200 Subject: [PATCH 3063/6909] Use legible tuple member name --- .../Visual/Menus/TestSceneMusicActionHandling.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 9121309489..9ee86eaf78 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Menus public void TestMusicNavigationActions() { int importId = 0; - Queue<(WorkingBeatmap working, TrackChangeDirection dir)> trackChangeQueue = null; + Queue<(WorkingBeatmap working, TrackChangeDirection changeDirection)> trackChangeQueue = null; // ensure we have at least two beatmaps available to identify the direction the music controller navigated to. AddRepeatStep("import beatmap", () => Game.BeatmapManager.Import(new BeatmapSetInfo @@ -55,8 +55,8 @@ namespace osu.Game.Tests.Visual.Menus AddStep("bind to track change", () => { - trackChangeQueue = new Queue<(WorkingBeatmap working, TrackChangeDirection dir)>(); - Game.MusicController.TrackChanged += (working, dir) => trackChangeQueue.Enqueue((working, dir)); + trackChangeQueue = new Queue<(WorkingBeatmap, TrackChangeDirection)>(); + Game.MusicController.TrackChanged += (working, changeDirection) => trackChangeQueue.Enqueue((working, changeDirection)); }); AddStep("seek track to 6 second", () => Game.MusicController.SeekTo(6000)); @@ -69,12 +69,12 @@ namespace osu.Game.Tests.Visual.Menus AddStep("trigger music prev action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPrev)); AddAssert("track changed to previous", () => trackChangeQueue.Count == 1 && - trackChangeQueue.Dequeue().dir == TrackChangeDirection.Prev); + trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Prev); AddStep("trigger music next action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicNext)); AddAssert("track changed to next", () => trackChangeQueue.Count == 1 && - trackChangeQueue.Dequeue().dir == TrackChangeDirection.Next); + trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Next); } } } From 8b1151284c507b12cbb56811a90018f916645873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 5 Sep 2020 15:43:16 +0200 Subject: [PATCH 3064/6909] Simplify overly verbose step names --- .../Visual/Menus/TestSceneMusicActionHandling.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 9ee86eaf78..9b8ba47992 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -18,9 +18,9 @@ namespace osu.Game.Tests.Visual.Menus public void TestMusicPlayAction() { AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething()); - AddStep("trigger music playback toggle action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPlay)); + AddStep("toggle playback", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPlay)); AddAssert("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.IsUserPaused); - AddStep("trigger music playback toggle action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPlay)); + AddStep("toggle playback", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPlay)); AddAssert("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.IsUserPaused); } @@ -62,16 +62,16 @@ namespace osu.Game.Tests.Visual.Menus AddStep("seek track to 6 second", () => Game.MusicController.SeekTo(6000)); AddUntilStep("wait for current time to update", () => Game.MusicController.CurrentTrack.CurrentTime > 5000); - AddStep("trigger music prev action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPrev)); + AddStep("press previous", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPrev)); AddAssert("no track change", () => trackChangeQueue.Count == 0); AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000); - AddStep("trigger music prev action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPrev)); + AddStep("press previous", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPrev)); AddAssert("track changed to previous", () => trackChangeQueue.Count == 1 && trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Prev); - AddStep("trigger music next action", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicNext)); + AddStep("press next", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicNext)); AddAssert("track changed to next", () => trackChangeQueue.Count == 1 && trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Next); From 2b16e2535304263507f338bad1591e4d25cf70e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Sep 2020 18:44:41 +0200 Subject: [PATCH 3065/6909] Revert unnecessary passing down of tuple in test --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 6e103af3f0..17d910036f 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -38,8 +38,8 @@ namespace osu.Game.Tests.Beatmaps.Formats var decoded = decodeFromLegacy(beatmaps_resource_store.GetStream(name), name); var decodedAfterEncode = decodeFromLegacy(encodeToLegacy(decoded), name); - sort(decoded); - sort(decodedAfterEncode); + sort(decoded.beatmap); + sort(decodedAfterEncode.beatmap); Assert.That(decodedAfterEncode.beatmap.Serialize(), Is.EqualTo(decoded.beatmap.Serialize())); Assert.IsTrue(areComboColoursEqual(decodedAfterEncode.beatmapSkin.Configuration, decoded.beatmapSkin.Configuration)); @@ -57,10 +57,10 @@ namespace osu.Game.Tests.Beatmaps.Formats return a.ComboColours.SequenceEqual(b.ComboColours); } - private void sort((IBeatmap beatmap, ISkin beatmapSkin) tuple) + 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 tuple.beatmap.ControlPointInfo.Groups) + 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))); From b4b9c71f00eab44bb189a3d1936b71edc6219c03 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 6 Sep 2020 10:13:06 -0700 Subject: [PATCH 3066/6909] Make all toolbar tooltips lowercase --- osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs | 2 +- osu.Game/Overlays/Changelog/ChangelogHeader.cs | 2 +- osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs | 2 +- osu.Game/Overlays/News/NewsHeader.cs | 2 +- osu.Game/Overlays/NotificationOverlay.cs | 4 ++-- osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs | 2 +- osu.Game/Overlays/SettingsOverlay.cs | 2 +- osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs | 4 ++-- osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs index b7f511271c..6a9a71210a 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingHeader.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.BeatmapListing public BeatmapListingTitle() { Title = "beatmap listing"; - Description = "Browse for new beatmaps"; + Description = "browse for new beatmaps"; IconTexture = "Icons/Hexacons/beatmap"; } } diff --git a/osu.Game/Overlays/Changelog/ChangelogHeader.cs b/osu.Game/Overlays/Changelog/ChangelogHeader.cs index bdc59297bb..f4be4328e7 100644 --- a/osu.Game/Overlays/Changelog/ChangelogHeader.cs +++ b/osu.Game/Overlays/Changelog/ChangelogHeader.cs @@ -115,7 +115,7 @@ namespace osu.Game.Overlays.Changelog public ChangelogHeaderTitle() { Title = "changelog"; - Description = "Track recent dev updates in the osu! ecosystem"; + Description = "track recent dev updates in the osu! ecosystem"; IconTexture = "Icons/Hexacons/devtools"; } } diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index a964d84c4f..36bf589877 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Dashboard public DashboardTitle() { Title = "dashboard"; - Description = "View your friends and other information"; + Description = "view your friends and other information"; IconTexture = "Icons/Hexacons/social"; } } diff --git a/osu.Game/Overlays/News/NewsHeader.cs b/osu.Game/Overlays/News/NewsHeader.cs index 38ac519387..63174128e7 100644 --- a/osu.Game/Overlays/News/NewsHeader.cs +++ b/osu.Game/Overlays/News/NewsHeader.cs @@ -57,7 +57,7 @@ namespace osu.Game.Overlays.News public NewsHeaderTitle() { Title = "news"; - Description = "Get up-to-date on community happenings"; + Description = "get up-to-date on community happenings"; IconTexture = "Icons/Hexacons/news"; } } diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index b7d916c48f..b5714fbcae 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -19,8 +19,8 @@ namespace osu.Game.Overlays public class NotificationOverlay : OsuFocusedOverlayContainer, INamedOverlayComponent { public string IconTexture => "Icons/Hexacons/notification"; - public string Title => "Notifications"; - public string Description => "Waiting for 'ya"; + public string Title => "notifications"; + public string Description => "waiting for 'ya"; private const float width = 320; diff --git a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs index 7039ab8214..92e22f5873 100644 --- a/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs +++ b/osu.Game/Overlays/Rankings/RankingsOverlayHeader.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Rankings public RankingsTitle() { Title = "ranking"; - Description = "Find out who's the best right now"; + Description = "find out who's the best right now"; IconTexture = "Icons/Hexacons/rankings"; } } diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index 0532a031f3..e1bcdbbaf0 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays { public string IconTexture => "Icons/Hexacons/settings"; public string Title => "settings"; - public string Description => "Change the way osu! behaves"; + public string Description => "change the way osu! behaves"; protected override IEnumerable CreateSections() => new SettingsSection[] { diff --git a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs index 6b2c24c0f3..76fbd40d66 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarHomeButton.cs @@ -17,8 +17,8 @@ namespace osu.Game.Overlays.Toolbar [BackgroundDependencyLoader] private void load() { - TooltipMain = "Home"; - TooltipSub = "Return to the main menu"; + TooltipMain = "home"; + TooltipSub = "return to the main menu"; SetIcon("Icons/Hexacons/home"); } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs index 754b679599..564fd65719 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRulesetTabButton.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Toolbar var rInstance = value.CreateInstance(); ruleset.TooltipMain = rInstance.Description; - ruleset.TooltipSub = $"Play some {rInstance.Description}"; + ruleset.TooltipSub = $"play some {rInstance.Description}"; ruleset.SetIcon(rInstance.CreateIcon()); } From 8f8f907fc7c3dda37646e8e6a8eca762ca47d32a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Sep 2020 13:27:28 +0900 Subject: [PATCH 3067/6909] Fix missed string --- osu.Game/Overlays/NowPlayingOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index d1df1fa936..55adf02a45 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays { public string IconTexture => "Icons/Hexacons/music"; public string Title => "now playing"; - public string Description => "Manage the currently playing track"; + public string Description => "manage the currently playing track"; private const float player_height = 130; private const float transition_length = 800; From daff060c9a0ff86c12c662583b641871d23c8a5e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Sep 2020 15:18:15 +0900 Subject: [PATCH 3068/6909] Hide the game-wide cursor on touch input --- osu.Game/Graphics/Cursor/MenuCursorContainer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs index 3015c44613..8da80f25ff 100644 --- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs +++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Input; +using osu.Framework.Input.StateChanges; namespace osu.Game.Graphics.Cursor { @@ -47,7 +48,10 @@ namespace osu.Game.Graphics.Cursor { base.Update(); - if (!CanShowCursor) + var lastMouseSource = GetContainingInputManager().CurrentState.Mouse.LastSource; + bool hasValidInput = lastMouseSource != null && !(lastMouseSource is ISourcedFromTouch); + + if (!hasValidInput || !CanShowCursor) { currentTarget?.Cursor?.Hide(); currentTarget = null; From 1a55d92c719c0d2db2eb0d2c977be242fe2afda8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Sep 2020 15:31:05 +0900 Subject: [PATCH 3069/6909] Use local input manager --- osu.Game/Graphics/Cursor/MenuCursorContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs index 8da80f25ff..4c7f7957e9 100644 --- a/osu.Game/Graphics/Cursor/MenuCursorContainer.cs +++ b/osu.Game/Graphics/Cursor/MenuCursorContainer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Graphics.Cursor { base.Update(); - var lastMouseSource = GetContainingInputManager().CurrentState.Mouse.LastSource; + var lastMouseSource = inputManager.CurrentState.Mouse.LastSource; bool hasValidInput = lastMouseSource != null && !(lastMouseSource is ISourcedFromTouch); if (!hasValidInput || !CanShowCursor) From ecc9c2957ffa287c6eff47fcc49b7dd544f7d0f1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Sep 2020 16:30:05 +0900 Subject: [PATCH 3070/6909] Avoid float precision error in mania conversion --- .../Beatmaps/ManiaBeatmapConverter.cs | 5 ++-- .../Legacy/DistanceObjectPatternGenerator.cs | 23 +++++++++---------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index b025ac7992..211905835c 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -5,7 +5,6 @@ using osu.Game.Rulesets.Mania.Objects; 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; @@ -167,8 +166,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps var positionData = original as IHasPosition; - for (double time = original.StartTime; !Precision.DefinitelyBigger(time, generator.EndTime); time += generator.SegmentDuration) + for (int i = 0; i <= generator.SpanCount; i++) { + double time = original.StartTime + generator.SegmentDuration * i; + recordNote(time, positionData?.Position ?? Vector2.Zero); computeDensity(time); } diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index d03eb0b3c9..fe146c5324 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -27,8 +27,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy public readonly double EndTime; public readonly double SegmentDuration; - - private readonly int spanCount; + public readonly int SpanCount; private PatternType convertType; @@ -42,20 +41,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var distanceData = hitObject as IHasDistance; var repeatsData = hitObject as IHasRepeats; - spanCount = repeatsData?.SpanCount() ?? 1; + SpanCount = repeatsData?.SpanCount() ?? 1; TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime); // The true distance, accounting for any repeats - double distance = (distanceData?.Distance ?? 0) * spanCount; + double distance = (distanceData?.Distance ?? 0) * SpanCount; // The velocity of the osu! hit object - calculated as the velocity of a slider double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength; // The duration of the osu! hit object double osuDuration = distance / osuVelocity; EndTime = hitObject.StartTime + osuDuration; - SegmentDuration = (EndTime - HitObject.StartTime) / spanCount; + SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount; } public override IEnumerable Generate() @@ -96,7 +95,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (spanCount > 1) + if (SpanCount > 1) { if (SegmentDuration <= 90) return generateRandomHoldNotes(HitObject.StartTime, 1); @@ -104,7 +103,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (SegmentDuration <= 120) { convertType |= PatternType.ForceNotStack; - return generateRandomNotes(HitObject.StartTime, spanCount + 1); + return generateRandomNotes(HitObject.StartTime, SpanCount + 1); } if (SegmentDuration <= 160) @@ -117,7 +116,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (duration >= 4000) return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0); - if (SegmentDuration > 400 && spanCount < TotalColumns - 1 - RandomStart) + if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart) return generateTiledHoldNotes(HitObject.StartTime); return generateHoldAndNormalNotes(HitObject.StartTime); @@ -251,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int column = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); bool increasing = Random.NextDouble() > 0.5; - for (int i = 0; i <= spanCount; i++) + for (int i = 0; i <= SpanCount; i++) { addToPattern(pattern, column, startTime, startTime); startTime += SegmentDuration; @@ -302,7 +301,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - for (int i = 0; i <= spanCount; i++) + for (int i = 0; i <= SpanCount; i++) { addToPattern(pattern, nextColumn, startTime, startTime); @@ -393,7 +392,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); - int columnRepeat = Math.Min(spanCount, TotalColumns); + int columnRepeat = Math.Min(SpanCount, TotalColumns); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) @@ -447,7 +446,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var rowPattern = new Pattern(); - for (int i = 0; i <= spanCount; i++) + for (int i = 0; i <= SpanCount; i++) { if (!(ignoreHead && startTime == HitObject.StartTime)) { From c72a8d475552049d602b69a8867ff5f5b440e081 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Sep 2020 17:18:40 +0900 Subject: [PATCH 3071/6909] Add zero-length slider test --- .../ManiaBeatmapConversionTest.cs | 1 + ...ero-length-slider-expected-conversion.json | 14 +++++++++++++ .../Testing/Beatmaps/zero-length-slider.osu | 20 +++++++++++++++++++ 3 files changed, 35 insertions(+) create mode 100644 osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json create mode 100644 osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index d0ff1fab43..d1e1280c7f 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Mania.Tests protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; [TestCase("basic")] + [TestCase("zero-length-slider")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json new file mode 100644 index 0000000000..229760cd1c --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider-expected-conversion.json @@ -0,0 +1,14 @@ +{ + "Mappings": [{ + "RandomW": 3083084786, + "RandomX": 273326509, + "RandomY": 273553282, + "RandomZ": 2659838971, + "StartTime": 4836, + "Objects": [{ + "StartTime": 4836, + "EndTime": 4836, + "Column": 0 + }] + }] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu new file mode 100644 index 0000000000..9b8ac1f9db --- /dev/null +++ b/osu.Game.Rulesets.Mania/Resources/Testing/Beatmaps/zero-length-slider.osu @@ -0,0 +1,20 @@ +osu file format v14 + +[General] +StackLeniency: 0.7 +Mode: 0 + +[Difficulty] +HPDrainRate:1 +CircleSize:4 +OverallDifficulty:1 +ApproachRate:9 +SliderMultiplier:2.5 +SliderTickRate:0.5 + +[TimingPoints] +34,431.654676258993,4,1,0,50,1,0 +4782,-66.6666666666667,4,1,0,20,0,0 + +[HitObjects] +15,199,4836,22,0,L,1,46.8750017881394 \ No newline at end of file From 679dc34aa410ec3c6732c52b981537136f5ab0c7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Sep 2020 17:18:54 +0900 Subject: [PATCH 3072/6909] Add test timeouts --- osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs | 1 + osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs | 1 + osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs | 1 + osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs | 1 + 4 files changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs index 8c48158acd..466cbdaf8d 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchBeatmapConversionTest.cs @@ -14,6 +14,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] + [Timeout(10000)] public class CatchBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Catch"; diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index d1e1280c7f..0c57267970 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -14,6 +14,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests { [TestFixture] + [Timeout(10000)] public class ManiaBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; diff --git a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs index cd3daf18a9..7d32895083 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuBeatmapConversionTest.cs @@ -12,6 +12,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Osu.Tests { [TestFixture] + [Timeout(10000)] public class OsuBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Osu"; diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index d0c57b20c0..5e550a5d03 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -12,6 +12,7 @@ using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Taiko.Tests { [TestFixture] + [Timeout(10000)] public class TaikoBeatmapConversionTest : BeatmapConversionTest { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; From a8a0bfb8aa26012fd6d4d3343eadf58bede72752 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Sep 2020 18:01:56 +0900 Subject: [PATCH 3073/6909] 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 f62ba48953..62397ca028 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 526dca421a..1de0633d1f 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 0cbbba70b9..7187b48907 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 6091714f158ff4675612d43de3bf1be99b1910f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Sep 2020 17:34:30 +0900 Subject: [PATCH 3074/6909] Limit BPM entry via slider to a sane range --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 39 ++++++++++++++----- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 906644ce14..8e6ea90797 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -12,7 +13,7 @@ namespace osu.Game.Screens.Edit.Timing { internal class TimingSection : Section { - private SettingsSlider bpm; + private SettingsSlider bpmSlider; private SettingsEnumDropdown timeSignature; [BackgroundDependencyLoader] @@ -20,7 +21,7 @@ namespace osu.Game.Screens.Edit.Timing { Flow.AddRange(new Drawable[] { - bpm = new BPMSlider + bpmSlider = new BPMSlider { Bindable = new TimingControlPoint().BeatLengthBindable, LabelText = "BPM", @@ -36,7 +37,7 @@ namespace osu.Game.Screens.Edit.Timing { if (point.NewValue != null) { - bpm.Bindable = point.NewValue.BeatLengthBindable; + bpmSlider.Bindable = point.NewValue.BeatLengthBindable; timeSignature.Bindable = point.NewValue.TimeSignatureBindable; } } @@ -54,6 +55,9 @@ namespace osu.Game.Screens.Edit.Timing private class BPMSlider : SettingsSlider { + private const double sane_minimum = 60; + private const double sane_maximum = 200; + private readonly BindableDouble beatLengthBindable = new BindableDouble(); private BindableDouble bpmBindable; @@ -63,22 +67,39 @@ namespace osu.Game.Screens.Edit.Timing get => base.Bindable; set { - // incoming will be beatlength - + // incoming will be beat length, not bpm beatLengthBindable.UnbindBindings(); beatLengthBindable.BindTo(value); - base.Bindable = bpmBindable = new BindableDouble(beatLengthToBpm(beatLengthBindable.Value)) + double initial = beatLengthToBpm(beatLengthBindable.Value); + + bpmBindable = new BindableDouble(initial) { - MinValue = beatLengthToBpm(beatLengthBindable.MaxValue), - MaxValue = beatLengthToBpm(beatLengthBindable.MinValue), Default = beatLengthToBpm(beatLengthBindable.Default), }; - bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); + updateCurrent(initial); + + bpmBindable.BindValueChanged(bpm => + { + updateCurrent(bpm.NewValue); + beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue); + }); + + base.Bindable = bpmBindable; } } + private void updateCurrent(double newValue) + { + // we use a more sane range for the slider display unless overridden by the user. + // if a value comes in outside our range, we should expand temporarily. + bpmBindable.MinValue = Math.Min(newValue, sane_minimum); + bpmBindable.MaxValue = Math.Max(newValue, sane_maximum); + + bpmBindable.Value = newValue; + } + private double beatLengthToBpm(double beatLength) => 60000 / beatLength; } } From 86512d6e8d4ad6ba2d20c14bcc76a6fd2708f372 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Sep 2020 17:39:13 +0900 Subject: [PATCH 3075/6909] Add BPM entry textbox --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 81 ++++++++++++++----- 1 file changed, 60 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 8e6ea90797..6fed4589ce 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Edit.Timing @@ -15,16 +16,21 @@ namespace osu.Game.Screens.Edit.Timing { private SettingsSlider bpmSlider; private SettingsEnumDropdown timeSignature; + private BPMTextBox bpmTextEntry; [BackgroundDependencyLoader] private void load() { Flow.AddRange(new Drawable[] { + bpmTextEntry = new BPMTextBox + { + Bindable = new TimingControlPoint().BeatLengthBindable, + Label = "BPM", + }, bpmSlider = new BPMSlider { Bindable = new TimingControlPoint().BeatLengthBindable, - LabelText = "BPM", }, timeSignature = new SettingsEnumDropdown { @@ -38,6 +44,7 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { bpmSlider.Bindable = point.NewValue.BeatLengthBindable; + bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; timeSignature.Bindable = point.NewValue.TimeSignatureBindable; } } @@ -53,14 +60,63 @@ namespace osu.Game.Screens.Edit.Timing }; } + private class BPMTextBox : LabelledTextBox + { + public BPMTextBox() + { + OnCommit += (val, isNew) => + { + if (!isNew) return; + + if (double.TryParse(Current.Value, out double doubleVal)) + { + try + { + beatLengthBindable.Value = beatLengthToBpm(doubleVal); + } + catch + { + // will restore the previous text value on failure. + beatLengthBindable.TriggerChange(); + } + } + }; + + beatLengthBindable.BindValueChanged(val => + { + Current.Value = beatLengthToBpm(val.NewValue).ToString(); + }); + } + + private readonly BindableDouble beatLengthBindable = new BindableDouble(); + + public Bindable Bindable + { + get => beatLengthBindable; + set + { + // incoming will be beat length, not bpm + beatLengthBindable.UnbindBindings(); + beatLengthBindable.BindTo(value); + } + } + } + private class BPMSlider : SettingsSlider { private const double sane_minimum = 60; private const double sane_maximum = 200; private readonly BindableDouble beatLengthBindable = new BindableDouble(); + private readonly BindableDouble bpmBindable = new BindableDouble(); - private BindableDouble bpmBindable; + public BPMSlider() + { + beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue))); + bpmBindable.BindValueChanged(bpm => bpmBindable.Default = beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); + + base.Bindable = bpmBindable; + } public override Bindable Bindable { @@ -70,23 +126,6 @@ namespace osu.Game.Screens.Edit.Timing // incoming will be beat length, not bpm beatLengthBindable.UnbindBindings(); beatLengthBindable.BindTo(value); - - double initial = beatLengthToBpm(beatLengthBindable.Value); - - bpmBindable = new BindableDouble(initial) - { - Default = beatLengthToBpm(beatLengthBindable.Default), - }; - - updateCurrent(initial); - - bpmBindable.BindValueChanged(bpm => - { - updateCurrent(bpm.NewValue); - beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue); - }); - - base.Bindable = bpmBindable; } } @@ -99,8 +138,8 @@ namespace osu.Game.Screens.Edit.Timing bpmBindable.Value = newValue; } - - private double beatLengthToBpm(double beatLength) => 60000 / beatLength; } + + private static double beatLengthToBpm(double beatLength) => 60000 / beatLength; } } From 98676af7bb3b737aeb07f8fbb8f7a821e21372d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Sep 2020 18:18:34 +0900 Subject: [PATCH 3076/6909] Move default declarations for readability --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 6fed4589ce..0112471522 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -23,15 +23,8 @@ namespace osu.Game.Screens.Edit.Timing { Flow.AddRange(new Drawable[] { - bpmTextEntry = new BPMTextBox - { - Bindable = new TimingControlPoint().BeatLengthBindable, - Label = "BPM", - }, - bpmSlider = new BPMSlider - { - Bindable = new TimingControlPoint().BeatLengthBindable, - }, + bpmTextEntry = new BPMTextBox(), + bpmSlider = new BPMSlider(), timeSignature = new SettingsEnumDropdown { LabelText = "Time Signature" @@ -62,8 +55,12 @@ namespace osu.Game.Screens.Edit.Timing private class BPMTextBox : LabelledTextBox { + private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable; + public BPMTextBox() { + Label = "BPM"; + OnCommit += (val, isNew) => { if (!isNew) return; @@ -84,12 +81,10 @@ namespace osu.Game.Screens.Edit.Timing beatLengthBindable.BindValueChanged(val => { - Current.Value = beatLengthToBpm(val.NewValue).ToString(); - }); + Current.Value = beatLengthToBpm(val.NewValue).ToString("N2"); + }, true); } - private readonly BindableDouble beatLengthBindable = new BindableDouble(); - public Bindable Bindable { get => beatLengthBindable; @@ -107,12 +102,12 @@ namespace osu.Game.Screens.Edit.Timing private const double sane_minimum = 60; private const double sane_maximum = 200; - private readonly BindableDouble beatLengthBindable = new BindableDouble(); + private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable; private readonly BindableDouble bpmBindable = new BindableDouble(); public BPMSlider() { - beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue))); + beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true); bpmBindable.BindValueChanged(bpm => bpmBindable.Default = beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); base.Bindable = bpmBindable; From 1468b9589fd7e843cc53c27dcfa6cddcb7bcf1a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Sep 2020 18:20:47 +0900 Subject: [PATCH 3077/6909] Increase max sane BPM value --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 0112471522..879363ba08 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Edit.Timing private class BPMSlider : SettingsSlider { private const double sane_minimum = 60; - private const double sane_maximum = 200; + private const double sane_maximum = 240; private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable; private readonly BindableDouble bpmBindable = new BindableDouble(); From b91a376f0a9438191b285510b88aa2c6b693632b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Sep 2020 20:06:38 +0900 Subject: [PATCH 3078/6909] Split dropdown into separate file --- .../Select/CollectionFilterDropdown.cs | 188 ++++++++++++++++++ osu.Game/Screens/Select/FilterControl.cs | 172 ---------------- 2 files changed, 188 insertions(+), 172 deletions(-) create mode 100644 osu.Game/Screens/Select/CollectionFilterDropdown.cs diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs new file mode 100644 index 0000000000..883c2c69f0 --- /dev/null +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -0,0 +1,188 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Screens.Select +{ + public class CollectionFilterDropdown : OsuDropdown + { + private readonly IBindableList collections = new BindableList(); + private readonly IBindableList beatmaps = new BindableList(); + private readonly BindableList filters = new BindableList(); + + public CollectionFilterDropdown() + { + ItemSource = filters; + } + + [BackgroundDependencyLoader] + private void load(BeatmapCollectionManager collectionManager) + { + collections.BindTo(collectionManager.Collections); + collections.CollectionChanged += (_, __) => collectionsChanged(); + collectionsChanged(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(filterChanged, true); + } + + /// + /// Occurs when a collection has been added or removed. + /// + private void collectionsChanged() + { + var selectedItem = SelectedItem?.Value?.Collection; + + filters.Clear(); + filters.Add(new FilterControl.CollectionFilter(null)); + filters.AddRange(collections.Select(c => new FilterControl.CollectionFilter(c))); + + Current.Value = filters.SingleOrDefault(f => f.Collection == selectedItem) ?? filters[0]; + } + + /// + /// Occurs when the selection has changed. + /// + private void filterChanged(ValueChangedEvent filter) + { + beatmaps.CollectionChanged -= filterBeatmapsChanged; + + if (filter.OldValue?.Collection != null) + beatmaps.UnbindFrom(filter.OldValue.Collection.Beatmaps); + + if (filter.NewValue?.Collection != null) + beatmaps.BindTo(filter.NewValue.Collection.Beatmaps); + + beatmaps.CollectionChanged += filterBeatmapsChanged; + } + + /// + /// Occurs when the beatmaps contained by a have changed. + /// + private void filterBeatmapsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + // The filtered beatmaps have changed, without the filter having changed itself. So a change in filter must be notified. + // Note that this does NOT propagate to bound bindables, so the FilterControl must bind directly to the value change event of this bindable. + Current.TriggerChange(); + } + + protected override string GenerateItemText(FilterControl.CollectionFilter item) => item.Collection?.Name.Value ?? "All beatmaps"; + + protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader(); + + protected override DropdownMenu CreateMenu() => new CollectionDropdownMenu(); + + private class CollectionDropdownHeader : OsuDropdownHeader + { + public CollectionDropdownHeader() + { + Height = 25; + Icon.Size = new Vector2(16); + Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 }; + } + } + + private class CollectionDropdownMenu : OsuDropdownMenu + { + public CollectionDropdownMenu() + { + MaxHeight = 200; + } + + protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item); + } + + private class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem + { + [Resolved] + private OsuColour colours { get; set; } + + [Resolved] + private IBindable beatmap { get; set; } + + [CanBeNull] + private readonly BindableList collectionBeatmaps; + + private IconButton addOrRemoveButton; + + public CollectionDropdownMenuItem(MenuItem item) + : base(item) + { + collectionBeatmaps = ((DropdownMenuItem)item).Value.Collection?.Beatmaps.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + addOrRemoveButton = new IconButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -OsuScrollContainer.SCROLL_BAR_HEIGHT, + Scale = new Vector2(0.75f), + Alpha = collectionBeatmaps == null ? 0 : 1, + Action = addOrRemove + } + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (collectionBeatmaps != null) + { + collectionBeatmaps.CollectionChanged += (_, __) => collectionChanged(); + beatmap.BindValueChanged(_ => collectionChanged(), true); + } + } + + private void collectionChanged() + { + Debug.Assert(collectionBeatmaps != null); + + addOrRemoveButton.Enabled.Value = !beatmap.IsDefault; + + if (collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo)) + { + addOrRemoveButton.Icon = FontAwesome.Solid.MinusSquare; + addOrRemoveButton.IconColour = colours.Red; + } + else + { + addOrRemoveButton.Icon = FontAwesome.Solid.PlusSquare; + addOrRemoveButton.IconColour = colours.Green; + } + } + + private void addOrRemove() + { + Debug.Assert(collectionBeatmaps != null); + + if (!collectionBeatmaps.Remove(beatmap.Value.BeatmapInfo)) + collectionBeatmaps.Add(beatmap.Value.BeatmapInfo); + } + } + } +} diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index a66bcfb3b1..706909e71e 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Specialized; -using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -11,14 +9,11 @@ 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.UserInterface; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; @@ -204,173 +199,6 @@ namespace osu.Game.Screens.Select updateCriteria(); } - private class CollectionFilterDropdown : OsuDropdown - { - private readonly IBindableList collections = new BindableList(); - private readonly IBindableList beatmaps = new BindableList(); - private readonly BindableList filters = new BindableList(); - - public CollectionFilterDropdown() - { - ItemSource = filters; - } - - [BackgroundDependencyLoader] - private void load(BeatmapCollectionManager collectionManager) - { - collections.BindTo(collectionManager.Collections); - collections.CollectionChanged += (_, __) => collectionsChanged(); - collectionsChanged(); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Current.BindValueChanged(filterChanged, true); - } - - /// - /// Occurs when a collection has been added or removed. - /// - private void collectionsChanged() - { - var selectedItem = SelectedItem?.Value?.Collection; - - filters.Clear(); - filters.Add(new CollectionFilter(null)); - filters.AddRange(collections.Select(c => new CollectionFilter(c))); - - Current.Value = filters.SingleOrDefault(f => f.Collection == selectedItem) ?? filters[0]; - } - - /// - /// Occurs when the selection has changed. - /// - private void filterChanged(ValueChangedEvent filter) - { - beatmaps.CollectionChanged -= filterBeatmapsChanged; - - if (filter.OldValue?.Collection != null) - beatmaps.UnbindFrom(filter.OldValue.Collection.Beatmaps); - - if (filter.NewValue?.Collection != null) - beatmaps.BindTo(filter.NewValue.Collection.Beatmaps); - - beatmaps.CollectionChanged += filterBeatmapsChanged; - } - - /// - /// Occurs when the beatmaps contained by a have changed. - /// - private void filterBeatmapsChanged(object sender, NotifyCollectionChangedEventArgs e) - { - // The filtered beatmaps have changed, without the filter having changed itself. So a change in filter must be notified. - // Note that this does NOT propagate to bound bindables, so the FilterControl must bind directly to the value change event of this bindable. - Current.TriggerChange(); - } - - protected override string GenerateItemText(CollectionFilter item) => item.Collection?.Name.Value ?? "All beatmaps"; - - protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader(); - - protected override DropdownMenu CreateMenu() => new CollectionDropdownMenu(); - - private class CollectionDropdownHeader : OsuDropdownHeader - { - public CollectionDropdownHeader() - { - Height = 25; - Icon.Size = new Vector2(16); - Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 }; - } - } - - private class CollectionDropdownMenu : OsuDropdownMenu - { - public CollectionDropdownMenu() - { - MaxHeight = 200; - } - - protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item); - } - - private class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem - { - [Resolved] - private OsuColour colours { get; set; } - - [Resolved] - private IBindable beatmap { get; set; } - - [CanBeNull] - private readonly BindableList collectionBeatmaps; - - private IconButton addOrRemoveButton; - - public CollectionDropdownMenuItem(MenuItem item) - : base(item) - { - collectionBeatmaps = ((DropdownMenuItem)item).Value.Collection?.Beatmaps.GetBoundCopy(); - } - - [BackgroundDependencyLoader] - private void load() - { - AddRangeInternal(new Drawable[] - { - addOrRemoveButton = new IconButton - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - X = -OsuScrollContainer.SCROLL_BAR_HEIGHT, - Scale = new Vector2(0.75f), - Alpha = collectionBeatmaps == null ? 0 : 1, - Action = addOrRemove - } - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - if (collectionBeatmaps != null) - { - collectionBeatmaps.CollectionChanged += (_, __) => collectionChanged(); - beatmap.BindValueChanged(_ => collectionChanged(), true); - } - } - - private void collectionChanged() - { - Debug.Assert(collectionBeatmaps != null); - - addOrRemoveButton.Enabled.Value = !beatmap.IsDefault; - - if (collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo)) - { - addOrRemoveButton.Icon = FontAwesome.Solid.MinusSquare; - addOrRemoveButton.IconColour = colours.Red; - } - else - { - addOrRemoveButton.Icon = FontAwesome.Solid.PlusSquare; - addOrRemoveButton.IconColour = colours.Green; - } - } - - private void addOrRemove() - { - Debug.Assert(collectionBeatmaps != null); - - if (!collectionBeatmaps.Remove(beatmap.Value.BeatmapInfo)) - collectionBeatmaps.Add(beatmap.Value.BeatmapInfo); - } - } - } - public class CollectionFilter { [CanBeNull] From 120dfd50a6c6ddb4f74f8cb63dd81c9026920672 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Sep 2020 20:29:28 +0900 Subject: [PATCH 3079/6909] Fix collection names not updating in dropdown --- .../Select/CollectionFilterDropdown.cs | 47 ++++++++++++++++++- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index 883c2c69f0..6b5d63771f 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -87,18 +87,47 @@ namespace osu.Game.Screens.Select protected override string GenerateItemText(FilterControl.CollectionFilter item) => item.Collection?.Name.Value ?? "All beatmaps"; - protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader(); + protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader + { + SelectedItem = { BindTarget = Current } + }; protected override DropdownMenu CreateMenu() => new CollectionDropdownMenu(); private class CollectionDropdownHeader : OsuDropdownHeader { + public readonly Bindable SelectedItem = new Bindable(); + private readonly Bindable collectionName = new Bindable(); + + protected override string Label + { + get => base.Label; + set { } // See updateText(). + } + public CollectionDropdownHeader() { Height = 25; Icon.Size = new Vector2(16); Foreground.Padding = new MarginPadding { Top = 4, Bottom = 4, Left = 8, Right = 4 }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedItem.BindValueChanged(_ => updateBindable(), true); + } + + private void updateBindable() + { + collectionName.UnbindAll(); + collectionName.BindTo(SelectedItem.Value.Collection?.Name ?? new Bindable("All beatmaps")); + collectionName.BindValueChanged(_ => updateText(), true); + } + + // Dropdowns don't bind to value changes, so the real name is copied directly from the selected item here. + private void updateText() => base.Label = collectionName.Value; } private class CollectionDropdownMenu : OsuDropdownMenu @@ -113,6 +142,9 @@ namespace osu.Game.Screens.Select private class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem { + [NotNull] + protected new FilterControl.CollectionFilter Item => ((DropdownMenuItem)base.Item).Value; + [Resolved] private OsuColour colours { get; set; } @@ -122,12 +154,17 @@ namespace osu.Game.Screens.Select [CanBeNull] private readonly BindableList collectionBeatmaps; + [NotNull] + private readonly Bindable collectionName; + private IconButton addOrRemoveButton; + private Content content; public CollectionDropdownMenuItem(MenuItem item) : base(item) { - collectionBeatmaps = ((DropdownMenuItem)item).Value.Collection?.Beatmaps.GetBoundCopy(); + collectionBeatmaps = Item.Collection?.Beatmaps.GetBoundCopy(); + collectionName = Item.Collection?.Name.GetBoundCopy() ?? new Bindable("All beatmaps"); } [BackgroundDependencyLoader] @@ -156,6 +193,10 @@ namespace osu.Game.Screens.Select collectionBeatmaps.CollectionChanged += (_, __) => collectionChanged(); beatmap.BindValueChanged(_ => collectionChanged(), true); } + + // Although the DrawableMenuItem binds to value changes of the item's text, the item is an internal implementation detail of Dropdown that has no knowledge + // of the underlying CollectionFilter value and its accompanying name, so the real name has to be copied here. Without this, the collection name wouldn't update when changed. + collectionName.BindValueChanged(name => content.Text = name.NewValue, true); } private void collectionChanged() @@ -183,6 +224,8 @@ namespace osu.Game.Screens.Select if (!collectionBeatmaps.Remove(beatmap.Value.BeatmapInfo)) collectionBeatmaps.Add(beatmap.Value.BeatmapInfo); } + + protected override Drawable CreateContent() => content = (Content)base.CreateContent(); } } } From c1d255a04c74c05949bfbe3d9b3ec784a4834047 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Sep 2020 20:44:39 +0900 Subject: [PATCH 3080/6909] Split filter control into separate class --- osu.Game/Screens/Select/CollectionFilter.cs | 24 +++++++++++++++++++ .../Select/CollectionFilterDropdown.cs | 18 +++++++------- osu.Game/Screens/Select/FilterControl.cs | 18 -------------- osu.Game/Screens/Select/FilterCriteria.cs | 2 +- 4 files changed, 34 insertions(+), 28 deletions(-) create mode 100644 osu.Game/Screens/Select/CollectionFilter.cs diff --git a/osu.Game/Screens/Select/CollectionFilter.cs b/osu.Game/Screens/Select/CollectionFilter.cs new file mode 100644 index 0000000000..e1f19b41c3 --- /dev/null +++ b/osu.Game/Screens/Select/CollectionFilter.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using JetBrains.Annotations; +using osu.Game.Beatmaps; +using osu.Game.Collections; + +namespace osu.Game.Screens.Select +{ + public class CollectionFilter + { + [CanBeNull] + public readonly BeatmapCollection Collection; + + public CollectionFilter([CanBeNull] BeatmapCollection collection) + { + Collection = collection; + } + + public virtual bool ContainsBeatmap(BeatmapInfo beatmap) + => Collection?.Beatmaps.Any(b => b.Equals(beatmap)) ?? true; + } +} diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index 6b5d63771f..ae2f09e11a 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -19,11 +19,11 @@ using osuTK; namespace osu.Game.Screens.Select { - public class CollectionFilterDropdown : OsuDropdown + public class CollectionFilterDropdown : OsuDropdown { private readonly IBindableList collections = new BindableList(); private readonly IBindableList beatmaps = new BindableList(); - private readonly BindableList filters = new BindableList(); + private readonly BindableList filters = new BindableList(); public CollectionFilterDropdown() { @@ -53,16 +53,16 @@ namespace osu.Game.Screens.Select var selectedItem = SelectedItem?.Value?.Collection; filters.Clear(); - filters.Add(new FilterControl.CollectionFilter(null)); - filters.AddRange(collections.Select(c => new FilterControl.CollectionFilter(c))); + filters.Add(new CollectionFilter(null)); + filters.AddRange(collections.Select(c => new CollectionFilter(c))); Current.Value = filters.SingleOrDefault(f => f.Collection == selectedItem) ?? filters[0]; } /// - /// Occurs when the selection has changed. + /// Occurs when the selection has changed. /// - private void filterChanged(ValueChangedEvent filter) + private void filterChanged(ValueChangedEvent filter) { beatmaps.CollectionChanged -= filterBeatmapsChanged; @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Select Current.TriggerChange(); } - protected override string GenerateItemText(FilterControl.CollectionFilter item) => item.Collection?.Name.Value ?? "All beatmaps"; + protected override string GenerateItemText(CollectionFilter item) => item.Collection?.Name.Value ?? "All beatmaps"; protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader { @@ -96,7 +96,7 @@ namespace osu.Game.Screens.Select private class CollectionDropdownHeader : OsuDropdownHeader { - public readonly Bindable SelectedItem = new Bindable(); + public readonly Bindable SelectedItem = new Bindable(); private readonly Bindable collectionName = new Bindable(); protected override string Label @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Select private class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem { [NotNull] - protected new FilterControl.CollectionFilter Item => ((DropdownMenuItem)base.Item).Value; + protected new CollectionFilter Item => ((DropdownMenuItem)base.Item).Value; [Resolved] private OsuColour colours { get; set; } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 706909e71e..41ce0d65cd 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -2,16 +2,12 @@ // 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; 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.Collections; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -199,20 +195,6 @@ namespace osu.Game.Screens.Select updateCriteria(); } - public class CollectionFilter - { - [CanBeNull] - public readonly BeatmapCollection Collection; - - public CollectionFilter([CanBeNull] BeatmapCollection collection) - { - Collection = collection; - } - - public virtual bool ContainsBeatmap(BeatmapInfo beatmap) - => Collection?.Beatmaps.Any(b => b.Equals(beatmap)) ?? true; - } - public void Deactivate() { searchTextBox.ReadOnly = true; diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 5a5c0e1b50..af4802f308 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Select } } - public FilterControl.CollectionFilter Collection; + public CollectionFilter Collection; public struct OptionalRange : IEquatable> where T : struct From 98e9c4dc256e3397d3ed2fa15c269bc7f0239943 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Sep 2020 21:08:48 +0900 Subject: [PATCH 3081/6909] General refactorings --- .../Collections/TestSceneCollectionDialog.cs | 2 +- osu.Game/Collections/BeatmapCollection.cs | 47 +++++++++++++++++++ .../Collections/BeatmapCollectionManager.cs | 27 +---------- ...onDialog.cs => ManageCollectionsDialog.cs} | 4 +- osu.Game/OsuGame.cs | 2 +- .../Carousel/DrawableCarouselBeatmap.cs | 8 ++-- .../Carousel/DrawableCarouselBeatmapSet.cs | 8 ++-- osu.Game/Screens/Select/CollectionFilter.cs | 24 ++++++++++ .../Select/CollectionFilterDropdown.cs | 8 +++- osu.Game/Screens/Select/FilterCriteria.cs | 5 ++ 10 files changed, 95 insertions(+), 40 deletions(-) create mode 100644 osu.Game/Collections/BeatmapCollection.cs rename osu.Game/Collections/{ManageCollectionDialog.cs => ManageCollectionsDialog.cs} (97%) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs index 247d27f67a..5782e627ba 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Collections { Children = new Drawable[] { - new ManageCollectionDialog { State = { Value = Visibility.Visible } }, + new ManageCollectionsDialog { State = { Value = Visibility.Visible } }, dialogOverlay = new DialogOverlay() }; } diff --git a/osu.Game/Collections/BeatmapCollection.cs b/osu.Game/Collections/BeatmapCollection.cs new file mode 100644 index 0000000000..7e4b15ecf9 --- /dev/null +++ b/osu.Game/Collections/BeatmapCollection.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.Bindables; +using osu.Game.Beatmaps; + +namespace osu.Game.Collections +{ + /// + /// A collection of beatmaps grouped by a name. + /// + public class BeatmapCollection + { + /// + /// Invoked whenever any change occurs on this . + /// + public event Action Changed; + + /// + /// The collection's name. + /// + public readonly Bindable Name = new Bindable(); + + /// + /// The beatmaps contained by the collection. + /// + public readonly BindableList Beatmaps = new BindableList(); + + /// + /// The date when this collection was last modified. + /// + public DateTimeOffset LastModifyDate { get; private set; } = DateTimeOffset.UtcNow; + + public BeatmapCollection() + { + Beatmaps.CollectionChanged += (_, __) => onChange(); + Name.ValueChanged += _ => onChange(); + } + + private void onChange() + { + LastModifyDate = DateTimeOffset.Now; + Changed?.Invoke(); + } + } +} diff --git a/osu.Game/Collections/BeatmapCollectionManager.cs b/osu.Game/Collections/BeatmapCollectionManager.cs index 3e5976300f..ed07f0d3e2 100644 --- a/osu.Game/Collections/BeatmapCollectionManager.cs +++ b/osu.Game/Collections/BeatmapCollectionManager.cs @@ -21,7 +21,7 @@ namespace osu.Game.Collections public class BeatmapCollectionManager : CompositeDrawable { /// - /// Database version in YYYYMMDD format (matching stable). + /// Database version in stable-compatible YYYYMMDD format. /// private const int database_version = 30000000; @@ -213,29 +213,4 @@ namespace osu.Game.Collections save(); } } - - public class BeatmapCollection - { - /// - /// Invoked whenever any change occurs on this . - /// - public event Action Changed; - - public readonly Bindable Name = new Bindable(); - - public readonly BindableList Beatmaps = new BindableList(); - - public DateTimeOffset LastModifyTime { get; private set; } - - public BeatmapCollection() - { - LastModifyTime = DateTimeOffset.UtcNow; - - Beatmaps.CollectionChanged += (_, __) => - { - LastModifyTime = DateTimeOffset.Now; - Changed?.Invoke(); - }; - } - } } diff --git a/osu.Game/Collections/ManageCollectionDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs similarity index 97% rename from osu.Game/Collections/ManageCollectionDialog.cs rename to osu.Game/Collections/ManageCollectionsDialog.cs index 1e222a9c71..f2aedb1c29 100644 --- a/osu.Game/Collections/ManageCollectionDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -13,7 +13,7 @@ using osuTK; namespace osu.Game.Collections { - public class ManageCollectionDialog : OsuFocusedOverlayContainer + public class ManageCollectionsDialog : OsuFocusedOverlayContainer { private const double enter_duration = 500; private const double exit_duration = 200; @@ -21,7 +21,7 @@ namespace osu.Game.Collections [Resolved] private BeatmapCollectionManager collectionManager { get; set; } - public ManageCollectionDialog() + public ManageCollectionsDialog() { Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 701a65dbeb..8434ee11fa 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -618,7 +618,7 @@ namespace osu.Game loadComponentSingleFile(CreateUpdateManager(), Add, true); // overlay elements - loadComponentSingleFile(new ManageCollectionDialog(), overlayContent.Add, true); + loadComponentSingleFile(new ManageCollectionsDialog(), overlayContent.Add, true); loadComponentSingleFile(beatmapListing = new BeatmapListingOverlay(), overlayContent.Add, true); loadComponentSingleFile(dashboard = new DashboardOverlay(), overlayContent.Add, true); loadComponentSingleFile(news = new NewsOverlay(), overlayContent.Add, true); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 6c43bf5bed..008cf85018 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Select.Carousel private BeatmapCollectionManager collectionManager { get; set; } [Resolved(CanBeNull = true)] - private ManageCollectionDialog manageCollectionDialog { get; set; } + private ManageCollectionsDialog manageCollectionsDialog { get; set; } private IBindable starDifficultyBindable; private CancellationTokenSource starDifficultyCancellationSource; @@ -227,9 +227,9 @@ namespace osu.Game.Screens.Select.Carousel if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); - var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem).ToList(); - if (manageCollectionDialog != null) - collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionDialog.Show)); + var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyDate).Take(3).Select(createCollectionMenuItem).ToList(); + if (manageCollectionsDialog != null) + collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionsDialog.Show)); items.Add(new OsuMenuItem("Add to...") { Items = collectionItems }); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index fc262730cb..fe0ad31b32 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Select.Carousel private BeatmapCollectionManager collectionManager { get; set; } [Resolved(CanBeNull = true)] - private ManageCollectionDialog manageCollectionDialog { get; set; } + private ManageCollectionsDialog manageCollectionsDialog { get; set; } private readonly BeatmapSetInfo beatmapSet; @@ -148,9 +148,9 @@ namespace osu.Game.Screens.Select.Carousel if (dialogOverlay != null) items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); - var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyTime).Take(3).Select(createCollectionMenuItem).ToList(); - if (manageCollectionDialog != null) - collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionDialog.Show)); + var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyDate).Take(3).Select(createCollectionMenuItem).ToList(); + if (manageCollectionsDialog != null) + collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionsDialog.Show)); items.Add(new OsuMenuItem("Add all to...") { Items = collectionItems }); diff --git a/osu.Game/Screens/Select/CollectionFilter.cs b/osu.Game/Screens/Select/CollectionFilter.cs index e1f19b41c3..7628ed391e 100644 --- a/osu.Game/Screens/Select/CollectionFilter.cs +++ b/osu.Game/Screens/Select/CollectionFilter.cs @@ -3,21 +3,45 @@ using System.Linq; using JetBrains.Annotations; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Collections; namespace osu.Game.Screens.Select { + /// + /// A filter. + /// public class CollectionFilter { + /// + /// The collection to filter beatmaps from. + /// May be null to not filter by collection (include all beatmaps). + /// [CanBeNull] public readonly BeatmapCollection Collection; + /// + /// The name of the collection. + /// + [NotNull] + public readonly Bindable CollectionName; + + /// + /// Creates a new . + /// + /// The collection to filter beatmaps from. public CollectionFilter([CanBeNull] BeatmapCollection collection) { Collection = collection; + CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable("All beatmaps"); } + /// + /// Whether the collection contains a given beatmap. + /// + /// The beatmap to check. + /// Whether contains . public virtual bool ContainsBeatmap(BeatmapInfo beatmap) => Collection?.Beatmaps.Any(b => b.Equals(beatmap)) ?? true; } diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index ae2f09e11a..02484f6c64 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -19,6 +19,9 @@ using osuTK; namespace osu.Game.Screens.Select { + /// + /// A dropdown to select the to filter beatmaps using. + /// public class CollectionFilterDropdown : OsuDropdown { private readonly IBindableList collections = new BindableList(); @@ -64,6 +67,7 @@ namespace osu.Game.Screens.Select /// private void filterChanged(ValueChangedEvent filter) { + // Binding the beatmaps will trigger a collection change event, which results in an infinite-loop. This is rebound later, when it's safe to do so. beatmaps.CollectionChanged -= filterBeatmapsChanged; if (filter.OldValue?.Collection != null) @@ -122,7 +126,7 @@ namespace osu.Game.Screens.Select private void updateBindable() { collectionName.UnbindAll(); - collectionName.BindTo(SelectedItem.Value.Collection?.Name ?? new Bindable("All beatmaps")); + collectionName.BindTo(SelectedItem.Value.CollectionName); collectionName.BindValueChanged(_ => updateText(), true); } @@ -164,7 +168,7 @@ namespace osu.Game.Screens.Select : base(item) { collectionBeatmaps = Item.Collection?.Beatmaps.GetBoundCopy(); - collectionName = Item.Collection?.Name.GetBoundCopy() ?? new Bindable("All beatmaps"); + collectionName = Item.CollectionName.GetBoundCopy(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index af4802f308..acab982945 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Screens.Select.Filter; @@ -51,6 +52,10 @@ namespace osu.Game.Screens.Select } } + /// + /// The collection to filter beatmaps from. + /// + [CanBeNull] public CollectionFilter Collection; public struct OptionalRange : IEquatable> From ad625ecc7a199e3f93a4e827312d07b4fbeee957 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Sep 2020 22:10:12 +0900 Subject: [PATCH 3082/6909] Add collection IO tests --- .../Collections/IO/ImportCollectionsTest.cs | 215 ++++++++++++++++++ .../Resources/Collections/collections.db | Bin 0 -> 473 bytes .../Collections/BeatmapCollectionManager.cs | 22 +- 3 files changed, 232 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs create mode 100644 osu.Game.Tests/Resources/Collections/collections.db diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs new file mode 100644 index 0000000000..7d772d3989 --- /dev/null +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -0,0 +1,215 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests.Collections.IO +{ + [TestFixture] + public class ImportCollectionsTest + { + [Test] + public async Task TestImportEmptyDatabase() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportEmptyDatabase")) + { + try + { + var osu = await loadOsu(host); + + var collectionManager = osu.Dependencies.Get(); + await collectionManager.Import(new MemoryStream()); + + Assert.That(collectionManager.Collections.Count, Is.Zero); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public async Task TestImportWithNoBeatmaps() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithNoBeatmaps")) + { + try + { + var osu = await loadOsu(host); + + var collectionManager = osu.Dependencies.Get(); + await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); + + Assert.That(collectionManager.Collections.Count, Is.EqualTo(2)); + + Assert.That(collectionManager.Collections[0].Name.Value, Is.EqualTo("First")); + Assert.That(collectionManager.Collections[0].Beatmaps.Count, Is.Zero); + + Assert.That(collectionManager.Collections[1].Name.Value, Is.EqualTo("Second")); + Assert.That(collectionManager.Collections[1].Beatmaps.Count, Is.Zero); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public async Task TestImportWithBeatmaps() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithBeatmaps")) + { + try + { + var osu = await loadOsu(host, true); + + var collectionManager = osu.Dependencies.Get(); + await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); + + Assert.That(collectionManager.Collections.Count, Is.EqualTo(2)); + + Assert.That(collectionManager.Collections[0].Name.Value, Is.EqualTo("First")); + Assert.That(collectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(1)); + + Assert.That(collectionManager.Collections[1].Name.Value, Is.EqualTo("Second")); + Assert.That(collectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(12)); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public async Task TestImportMalformedDatabase() + { + bool exceptionThrown = false; + UnhandledExceptionEventHandler setException = (_, __) => exceptionThrown = true; + + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportMalformedDatabase")) + { + try + { + AppDomain.CurrentDomain.UnhandledException += setException; + + var osu = await loadOsu(host, true); + + var collectionManager = osu.Dependencies.Get(); + + using (var ms = new MemoryStream()) + { + using (var bw = new BinaryWriter(ms, Encoding.UTF8, true)) + { + for (int i = 0; i < 10000; i++) + bw.Write((byte)i); + } + + ms.Seek(0, SeekOrigin.Begin); + + await collectionManager.Import(ms); + } + + Assert.That(host.UpdateThread.Running, Is.True); + Assert.That(exceptionThrown, Is.False); + Assert.That(collectionManager.Collections.Count, Is.EqualTo(0)); + } + finally + { + host.Exit(); + AppDomain.CurrentDomain.UnhandledException -= setException; + } + } + } + + [Test] + public async Task TestSaveAndReload() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestSaveAndReload")) + { + try + { + var osu = await loadOsu(host, true); + + var collectionManager = osu.Dependencies.Get(); + await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); + + // Move first beatmap from second collection into the first. + collectionManager.Collections[0].Beatmaps.Add(collectionManager.Collections[1].Beatmaps[0]); + collectionManager.Collections[1].Beatmaps.RemoveAt(0); + + // Rename the second collecction. + collectionManager.Collections[1].Name.Value = "Another"; + } + finally + { + host.Exit(); + } + } + + using (HeadlessGameHost host = new HeadlessGameHost("TestSaveAndReload")) + { + try + { + var osu = await loadOsu(host, true); + + var collectionManager = osu.Dependencies.Get(); + + Assert.That(collectionManager.Collections.Count, Is.EqualTo(2)); + + Assert.That(collectionManager.Collections[0].Name.Value, Is.EqualTo("First")); + Assert.That(collectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(2)); + + Assert.That(collectionManager.Collections[1].Name.Value, Is.EqualTo("Another")); + Assert.That(collectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(11)); + } + finally + { + host.Exit(); + } + } + } + + private async Task loadOsu(GameHost host, bool withBeatmap = false) + { + var osu = new OsuGameBase(); + +#pragma warning disable 4014 + Task.Run(() => host.Run(osu)); +#pragma warning restore 4014 + + waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); + + if (withBeatmap) + { + var beatmapFile = TestResources.GetTestBeatmapForImport(); + var beatmapManager = osu.Dependencies.Get(); + await beatmapManager.Import(beatmapFile); + } + + return osu; + } + + private 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.Tests/Resources/Collections/collections.db b/osu.Game.Tests/Resources/Collections/collections.db new file mode 100644 index 0000000000000000000000000000000000000000..83e1c0f10a9058f3f376e75d3b5a88f671a95f63 GIT binary patch literal 473 zcmah_%W0lL4E!CiFJFrIO3>=Ld+;?4qyjxw;7bCryL3}or-29rgV2mL^ZCk8-yV<0 z_59=Q&-=&I7rdJX!-hP)U7D7xkTeI>lFo6x{M`BbSAGAty`>hfB!!t?h{`*ueUU(z zwqLg>Kq9oOjOI0BLY+xkyAg0+_rhpwBocF}ptLdFxMa3XucLvnYP9pNF;*O~cDSX^ zm8A@4W6w#hixgKP0&(j^RSP^kU2@%VUNsbpr9-6>Ab1NHf^gOz*PS64xg(D-ELaUW z$6;(dB@EZi9CA)@46-aEqHY}+Vltepue&O_nhD)wo)}crPm*6o_5?jw{+sW8lH + return Task.Run(async () => { var storage = GetStableStorage(); if (storage.Exists(database_name)) { using (var stream = storage.GetStream(database_name)) - { - var collection = readCollections(stream); - Schedule(() => importCollections(collection)); - } + await Import(stream); } }); } + public async Task Import(Stream stream) => await Task.Run(async () => + { + var collection = readCollections(stream); + bool importCompleted = false; + + Schedule(() => + { + importCollections(collection); + importCompleted = true; + }); + + while (!IsDisposed && !importCompleted) + await Task.Delay(10); + }); + private void importCollections(List newCollections) { foreach (var newCol in newCollections) From 0d5d293279eb96bcefc739c19fd69da4fcdcdad2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Sep 2020 22:47:19 +0900 Subject: [PATCH 3083/6909] Add manage collections dialog tests --- .../Collections/TestSceneCollectionDialog.cs | 26 --- .../TestSceneManageCollectionsDialog.cs | 198 ++++++++++++++++++ .../Collections/BeatmapCollectionManager.cs | 19 +- .../Collections/DrawableCollectionListItem.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- 5 files changed, 212 insertions(+), 35 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs create mode 100644 osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs diff --git a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.cs deleted file mode 100644 index 5782e627ba..0000000000 --- a/osu.Game.Tests/Visual/Collections/TestSceneCollectionDialog.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 osu.Framework.Allocation; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Collections; -using osu.Game.Overlays; - -namespace osu.Game.Tests.Visual.Collections -{ - public class TestSceneCollectionDialog : OsuTestScene - { - [Cached] - private DialogOverlay dialogOverlay; - - public TestSceneCollectionDialog() - { - Children = new Drawable[] - { - new ManageCollectionsDialog { State = { Value = Visibility.Visible } }, - dialogOverlay = new DialogOverlay() - }; - } - } -} diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs new file mode 100644 index 0000000000..2d6f8abd8b --- /dev/null +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -0,0 +1,198 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Testing; +using osu.Game.Collections; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Collections +{ + public class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene + { + [Cached] + private readonly DialogOverlay dialogOverlay; + + protected override Container Content => content; + + private readonly Container content; + + private BeatmapCollectionManager manager; + private ManageCollectionsDialog dialog; + + public TestSceneManageCollectionsDialog() + { + base.Content.AddRange(new Drawable[] + { + content = new Container { RelativeSizeAxes = Axes.Both }, + dialogOverlay = new DialogOverlay() + }); + } + + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(manager = new BeatmapCollectionManager(LocalStorage)); + Add(manager); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + manager.Collections.Clear(); + Child = dialog = new ManageCollectionsDialog(); + }); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("show dialog", () => dialog.Show()); + } + + [Test] + public void TestHideDialog() + { + AddWaitStep("wait for animation", 3); + AddStep("hide dialog", () => dialog.Hide()); + } + + [Test] + public void TestAddCollectionExternal() + { + AddStep("add collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "First collection" } })); + assertCollectionCount(1); + assertCollectionName(0, "First collection"); + + AddStep("add another collection", () => manager.Collections.Add(new BeatmapCollection { Name = { Value = "Second collection" } })); + assertCollectionCount(2); + assertCollectionName(1, "Second collection"); + } + + [Test] + public void TestAddCollectionViaButton() + { + AddStep("press new collection button", () => + { + InputManager.MoveMouseTo(dialog.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + assertCollectionCount(1); + + AddStep("press again", () => + { + InputManager.MoveMouseTo(dialog.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + assertCollectionCount(2); + } + + [Test] + public void TestRemoveCollectionExternal() + { + AddStep("add two collections", () => manager.Collections.AddRange(new[] + { + new BeatmapCollection { Name = { Value = "1" } }, + new BeatmapCollection { Name = { Value = "2" } }, + })); + + AddStep("remove first collection", () => manager.Collections.RemoveAt(0)); + assertCollectionCount(1); + assertCollectionName(0, "2"); + } + + [Test] + public void TestRemoveCollectionViaButton() + { + AddStep("add two collections", () => manager.Collections.AddRange(new[] + { + new BeatmapCollection { Name = { Value = "1" } }, + new BeatmapCollection { Name = { Value = "2" } }, + })); + + AddStep("click first delete button", () => + { + InputManager.MoveMouseTo(dialog.ChildrenOfType().First(), new Vector2(5, 0)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is DeleteCollectionDialog); + AddStep("click confirmation", () => + { + InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType().First()); + InputManager.Click(MouseButton.Left); + }); + + assertCollectionCount(1); + assertCollectionName(0, "2"); + } + + [Test] + public void TestCollectionNotRemovedWhenDialogCancelled() + { + AddStep("add two collections", () => manager.Collections.AddRange(new[] + { + new BeatmapCollection { Name = { Value = "1" } }, + new BeatmapCollection { Name = { Value = "2" } }, + })); + + AddStep("click first delete button", () => + { + InputManager.MoveMouseTo(dialog.ChildrenOfType().First(), new Vector2(5, 0)); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is DeleteCollectionDialog); + AddStep("click confirmation", () => + { + InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Left); + }); + + assertCollectionCount(2); + } + + [Test] + public void TestCollectionRenamedExternal() + { + AddStep("add two collections", () => manager.Collections.AddRange(new[] + { + new BeatmapCollection { Name = { Value = "1" } }, + new BeatmapCollection { Name = { Value = "2" } }, + })); + + AddStep("change first collection name", () => manager.Collections[0].Name.Value = "First"); + + assertCollectionName(0, "First"); + } + + [Test] + public void TestCollectionRenamedOnTextChange() + { + AddStep("add two collections", () => manager.Collections.AddRange(new[] + { + new BeatmapCollection { Name = { Value = "1" } }, + new BeatmapCollection { Name = { Value = "2" } }, + })); + + AddStep("change first collection name", () => dialog.ChildrenOfType().First().Text = "First"); + AddAssert("collection has new name", () => manager.Collections[0].Name.Value == "First"); + } + + private void assertCollectionCount(int count) + => AddAssert($"{count} collections shown", () => dialog.ChildrenOfType().Count() == count); + + private void assertCollectionName(int index, string name) + => AddAssert($"item {index + 1} has correct name", () => dialog.ChildrenOfType().ElementAt(index).ChildrenOfType().First().Text == name); + } +} diff --git a/osu.Game/Collections/BeatmapCollectionManager.cs b/osu.Game/Collections/BeatmapCollectionManager.cs index 3d3e9e0e07..a553ac632e 100644 --- a/osu.Game/Collections/BeatmapCollectionManager.cs +++ b/osu.Game/Collections/BeatmapCollectionManager.cs @@ -37,12 +37,19 @@ namespace osu.Game.Collections [Resolved] private BeatmapManager beatmaps { get; set; } + private readonly Storage storage; + + public BeatmapCollectionManager(Storage storage) + { + this.storage = storage; + } + [BackgroundDependencyLoader] private void load() { - if (host.Storage.Exists(database_name)) + if (storage.Exists(database_name)) { - using (var stream = host.Storage.GetStream(database_name)) + using (var stream = storage.GetStream(database_name)) importCollections(readCollections(stream)); } @@ -78,11 +85,9 @@ namespace osu.Game.Collections return Task.Run(async () => { - var storage = GetStableStorage(); - - if (storage.Exists(database_name)) + if (stable.Exists(database_name)) { - using (var stream = storage.GetStream(database_name)) + using (var stream = stable.GetStream(database_name)) await Import(stream); } }); @@ -188,7 +193,7 @@ namespace osu.Game.Collections { // This is NOT thread-safe!! - using (var sw = new SerializationWriter(host.Storage.GetStream(database_name, FileAccess.Write))) + using (var sw = new SerializationWriter(storage.GetStream(database_name, FileAccess.Write))) { sw.Write(database_version); sw.Write(Collections.Count); diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 7c1a2e1287..c7abf58d10 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -87,7 +87,7 @@ namespace osu.Game.Collections } } - private class DeleteButton : CompositeDrawable + public class DeleteButton : CompositeDrawable { public Func IsTextBoxHovered; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 30494d18fb..3114727d54 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -228,7 +228,7 @@ namespace osu.Game dependencies.Cache(difficultyManager); AddInternal(difficultyManager); - dependencies.Cache(CollectionManager = new BeatmapCollectionManager()); + dependencies.Cache(CollectionManager = new BeatmapCollectionManager(Storage)); AddInternal(CollectionManager); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); From a1214512bc96532f984166b20b77ecaa56bd890d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Sep 2020 23:57:49 +0900 Subject: [PATCH 3084/6909] Add filter control tests --- .../SongSelect/TestSceneFilterControl.cs | 213 +++++++++++++++++- .../Select/CollectionFilterDropdown.cs | 28 +-- 2 files changed, 225 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index f89300661c..fe1c194c5b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -1,22 +1,231 @@ // 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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Collections; +using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; using osu.Game.Screens.Select; +using osu.Game.Tests.Resources; +using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { - public class TestSceneFilterControl : OsuTestScene + public class TestSceneFilterControl : OsuManualInputManagerTestScene { + protected override Container Content => content; + private readonly Container content; + + [Cached] + private readonly BeatmapCollectionManager collectionManager; + + private RulesetStore rulesets; + private BeatmapManager beatmapManager; + + private FilterControl control; + public TestSceneFilterControl() { - Child = new FilterControl + base.Content.AddRange(new Drawable[] + { + collectionManager = new BeatmapCollectionManager(LocalStorage), + content = new Container { RelativeSizeAxes = Axes.Both } + }); + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.Cache(collectionManager); + return dependencies; + } + + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); + + beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + collectionManager.Collections.Clear(); + + Child = control = new FilterControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, Height = FilterControl.HEIGHT, }; + }); + + [Test] + public void TestEmptyCollectionFilterContainsAllBeatmaps() + { + assertCollectionDropdownContains("All beatmaps"); + assertCollectionHeaderDisplays("All beatmaps"); } + + [Test] + public void TestCollectionAddedToDropdown() + { + AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); + AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } })); + assertCollectionDropdownContains("1"); + assertCollectionDropdownContains("2"); + } + + [Test] + public void TestCollectionRemovedFromDropdown() + { + AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); + AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "2" } })); + AddStep("remove collection", () => collectionManager.Collections.RemoveAt(0)); + + assertCollectionDropdownContains("1", false); + assertCollectionDropdownContains("2"); + } + + [Test] + public void TestCollectionRenamed() + { + AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); + AddStep("select collection", () => + { + var dropdown = control.ChildrenOfType().Single(); + dropdown.Current.Value = dropdown.ItemSource.ElementAt(1); + }); + + AddStep("expand header", () => + { + InputManager.MoveMouseTo(control.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("change name", () => collectionManager.Collections[0].Name.Value = "First"); + + assertCollectionDropdownContains("First"); + assertCollectionHeaderDisplays("First"); + } + + [Test] + public void TestAllBeatmapFilterDoesNotHaveAddButton() + { + AddAssert("'All beatmaps' filter does not have add button", () => !getCollectionDropdownItems().First().ChildrenOfType().Single().IsPresent); + } + + [Test] + public void TestCollectionFilterHasAddButton() + { + AddStep("expand header", () => + { + InputManager.MoveMouseTo(control.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); + AddAssert("collection has add button", () => !getAddOrRemoveButton(0).IsPresent); + } + + [Test] + public void TestButtonDisabledAndEnabledWithBeatmapChanges() + { + AddStep("expand header", () => + { + InputManager.MoveMouseTo(control.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); + AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value); + + AddStep("set dummy beatmap", () => Beatmap.SetDefault()); + AddAssert("button enabled", () => !getAddOrRemoveButton(1).Enabled.Value); + } + + [Test] + public void TestButtonChangesWhenAddedAndRemovedFromCollection() + { + AddStep("expand header", () => + { + InputManager.MoveMouseTo(control.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + + AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.PlusSquare)); + + AddStep("add beatmap to collection", () => collectionManager.Collections[0].Beatmaps.Add(Beatmap.Value.BeatmapInfo)); + AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.MinusSquare)); + + AddStep("remove beatmap from collection", () => collectionManager.Collections[0].Beatmaps.Clear()); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.PlusSquare)); + } + + [Test] + public void TestButtonAddsAndRemovesBeatmap() + { + AddStep("expand header", () => + { + InputManager.MoveMouseTo(control.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); + + AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.PlusSquare)); + + addClickAddOrRemoveButtonStep(1); + AddAssert("collection contains beatmap", () => collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo)); + AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.MinusSquare)); + + addClickAddOrRemoveButtonStep(1); + AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.PlusSquare)); + } + + private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) + => AddAssert($"collection dropdown header displays '{collectionName}'", + () => shouldDisplay == (control.ChildrenOfType().Single().ChildrenOfType().First().Text == collectionName)); + + private void assertCollectionDropdownContains(string collectionName, bool shouldContain = true) => + AddAssert($"collection dropdown {(shouldContain ? "contains" : "does not contain")} '{collectionName}'", + // A bit of a roundabout way of going about this, see: https://github.com/ppy/osu-framework/issues/3871 + https://github.com/ppy/osu-framework/issues/3872 + () => shouldContain == (getCollectionDropdownItems().Any(i => i.ChildrenOfType().OfType().First().Text == collectionName))); + + private IconButton getAddOrRemoveButton(int index) + => getCollectionDropdownItems().ElementAt(index).ChildrenOfType().Single(); + + private void addClickAddOrRemoveButtonStep(int index) + { + AddStep("click add or remove button", () => + { + InputManager.MoveMouseTo(getAddOrRemoveButton(index)); + InputManager.Click(MouseButton.Left); + }); + } + + private IEnumerable.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems() + => control.ChildrenOfType().Single().ChildrenOfType.DropdownMenu.DrawableDropdownMenuItem>(); } } diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index 02484f6c64..2d30263d78 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -98,7 +98,7 @@ namespace osu.Game.Screens.Select protected override DropdownMenu CreateMenu() => new CollectionDropdownMenu(); - private class CollectionDropdownHeader : OsuDropdownHeader + public class CollectionDropdownHeader : OsuDropdownHeader { public readonly Bindable SelectedItem = new Bindable(); private readonly Bindable collectionName = new Bindable(); @@ -126,7 +126,10 @@ namespace osu.Game.Screens.Select private void updateBindable() { collectionName.UnbindAll(); - collectionName.BindTo(SelectedItem.Value.CollectionName); + + if (SelectedItem.Value != null) + collectionName.BindTo(SelectedItem.Value.CollectionName); + collectionName.BindValueChanged(_ => updateText(), true); } @@ -174,17 +177,14 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load() { - AddRangeInternal(new Drawable[] + AddInternal(addOrRemoveButton = new IconButton { - addOrRemoveButton = new IconButton - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - X = -OsuScrollContainer.SCROLL_BAR_HEIGHT, - Scale = new Vector2(0.75f), - Alpha = collectionBeatmaps == null ? 0 : 1, - Action = addOrRemove - } + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + X = -OsuScrollContainer.SCROLL_BAR_HEIGHT, + Scale = new Vector2(0.75f), + Alpha = collectionBeatmaps == null ? 0 : 1, + Action = addOrRemove }); } @@ -211,12 +211,12 @@ namespace osu.Game.Screens.Select if (collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo)) { - addOrRemoveButton.Icon = FontAwesome.Solid.MinusSquare; + addOrRemoveButton.Icon = FontAwesome.Regular.MinusSquare; addOrRemoveButton.IconColour = colours.Red; } else { - addOrRemoveButton.Icon = FontAwesome.Solid.PlusSquare; + addOrRemoveButton.Icon = FontAwesome.Regular.PlusSquare; addOrRemoveButton.IconColour = colours.Green; } } From e37c04cb6d97c96df6d2858d4d8f31152ac3b1bc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 00:04:03 +0900 Subject: [PATCH 3085/6909] Change back to solid icon --- .../Visual/SongSelect/TestSceneFilterControl.cs | 12 ++++++------ osu.Game/Screens/Select/CollectionFilterDropdown.cs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index fe1c194c5b..89a9536a04 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -172,13 +172,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.PlusSquare)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); AddStep("add beatmap to collection", () => collectionManager.Collections[0].Beatmaps.Add(Beatmap.Value.BeatmapInfo)); - AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.MinusSquare)); + AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare)); AddStep("remove beatmap from collection", () => collectionManager.Collections[0].Beatmaps.Clear()); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.PlusSquare)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); } [Test] @@ -193,15 +193,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.PlusSquare)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); addClickAddOrRemoveButtonStep(1); AddAssert("collection contains beatmap", () => collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo)); - AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.MinusSquare)); + AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare)); addClickAddOrRemoveButtonStep(1); AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo)); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Regular.PlusSquare)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); } private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index 2d30263d78..e2e8fbe0ea 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -211,12 +211,12 @@ namespace osu.Game.Screens.Select if (collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo)) { - addOrRemoveButton.Icon = FontAwesome.Regular.MinusSquare; + addOrRemoveButton.Icon = FontAwesome.Solid.MinusSquare; addOrRemoveButton.IconColour = colours.Red; } else { - addOrRemoveButton.Icon = FontAwesome.Regular.PlusSquare; + addOrRemoveButton.Icon = FontAwesome.Solid.PlusSquare; addOrRemoveButton.IconColour = colours.Green; } } From ca4423af74bd57088baa5b96abdf66d0fd6fc5ba Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 00:07:12 +0900 Subject: [PATCH 3086/6909] Fix tests --- .../Visual/SongSelect/TestSceneFilterControl.cs | 15 +++++++-------- .../Screens/Select/CollectionFilterDropdown.cs | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 89a9536a04..c2dd652b3a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -151,13 +151,12 @@ namespace osu.Game.Tests.Visual.SongSelect }); AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); - AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value); AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); AddAssert("button enabled", () => getAddOrRemoveButton(1).Enabled.Value); AddStep("set dummy beatmap", () => Beatmap.SetDefault()); - AddAssert("button enabled", () => !getAddOrRemoveButton(1).Enabled.Value); + AddAssert("button disabled", () => !getAddOrRemoveButton(1).Enabled.Value); } [Test] @@ -172,13 +171,13 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusCircle)); AddStep("add beatmap to collection", () => collectionManager.Collections[0].Beatmaps.Add(Beatmap.Value.BeatmapInfo)); - AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare)); + AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusCircle)); AddStep("remove beatmap from collection", () => collectionManager.Collections[0].Beatmaps.Clear()); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusCircle)); } [Test] @@ -193,15 +192,15 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusCircle)); addClickAddOrRemoveButtonStep(1); AddAssert("collection contains beatmap", () => collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo)); - AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare)); + AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusCircle)); addClickAddOrRemoveButtonStep(1); AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo)); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusCircle)); } private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index e2e8fbe0ea..18caae9545 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -211,12 +211,12 @@ namespace osu.Game.Screens.Select if (collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo)) { - addOrRemoveButton.Icon = FontAwesome.Solid.MinusSquare; + addOrRemoveButton.Icon = FontAwesome.Solid.MinusCircle; addOrRemoveButton.IconColour = colours.Red; } else { - addOrRemoveButton.Icon = FontAwesome.Solid.PlusSquare; + addOrRemoveButton.Icon = FontAwesome.Solid.PlusCircle; addOrRemoveButton.IconColour = colours.Green; } } From 01c0b61b203df181285cc052fb73509073ffefbb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 01:52:31 +0900 Subject: [PATCH 3087/6909] Fix incorrect test names --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index cef8105490..0702b02bb1 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -761,7 +761,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public void TestCreateNewEmptyBeatmap() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapFile))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestCreateNewEmptyBeatmap))) { try { @@ -788,7 +788,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public void TestCreateNewBeatmapWithObject() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapFile))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestCreateNewBeatmapWithObject))) { try { From 2b62579488c76a5f3bc65455085178912081e459 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 7 Sep 2020 10:18:22 -0700 Subject: [PATCH 3088/6909] Lowercase one more toolbar tooltip --- osu.Game/Overlays/ChatOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index bcc2227be8..25a59e9b25 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays { public string IconTexture => "Icons/Hexacons/messaging"; public string Title => "chat"; - public string Description => "Join the real-time discussion"; + public string Description => "join the real-time discussion"; private const float textbox_height = 60; private const float channel_selection_min_height = 0.3f; From 3a24cc1aa976e746e57509a5b0ddb4f71eed4340 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 7 Sep 2020 22:13:29 +0300 Subject: [PATCH 3089/6909] Implement PaginatedContainerHeader component --- .../TestScenePaginatedContainerHeader.cs | 77 +++++++++++ .../Overlays/Profile/Sections/CounterPill.cs | 14 +- .../Sections/PaginatedContainerHeader.cs | 129 ++++++++++++++++++ 3 files changed, 208 insertions(+), 12 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs create mode 100644 osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs new file mode 100644 index 0000000000..114a3af1d9 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs @@ -0,0 +1,77 @@ +using NUnit.Framework; +using osu.Game.Overlays.Profile.Sections; +using osu.Framework.Testing; +using System.Linq; +using osu.Framework.Graphics; +using osu.Game.Overlays; +using osu.Framework.Allocation; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestScenePaginatedContainerHeader : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + private PaginatedContainerHeader header; + + [Test] + public void TestHiddenCounter() + { + AddStep("Create header", () => createHeader("Header with hidden counter", CounterVisibilityState.AlwaysHidden)); + AddAssert("Value is 0", () => header.Current.Value == 0); + AddAssert("Counter is hidden", () => header.ChildrenOfType().First().Alpha == 0); + AddStep("Set count 10", () => header.Current.Value = 10); + AddAssert("Value is 10", () => header.Current.Value == 10); + AddAssert("Counter is hidden", () => header.ChildrenOfType().First().Alpha == 0); + } + + [Test] + public void TestVisibleCounter() + { + AddStep("Create header", () => createHeader("Header with visible counter", CounterVisibilityState.AlwaysVisible)); + AddAssert("Value is 0", () => header.Current.Value == 0); + AddAssert("Counter is visible", () => header.ChildrenOfType().First().Alpha == 1); + AddStep("Set count 10", () => header.Current.Value = 10); + AddAssert("Value is 10", () => header.Current.Value == 10); + AddAssert("Counter is visible", () => header.ChildrenOfType().First().Alpha == 1); + } + + [Test] + public void TestVisibleWhenZeroCounter() + { + AddStep("Create header", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenNonZero)); + AddAssert("Value is 0", () => header.Current.Value == 0); + AddAssert("Counter is visible", () => header.ChildrenOfType().First().Alpha == 1); + AddStep("Set count 10", () => header.Current.Value = 10); + AddAssert("Value is 10", () => header.Current.Value == 10); + AddAssert("Counter is hidden", () => header.ChildrenOfType().First().Alpha == 0); + AddStep("Set count 0", () => header.Current.Value = 0); + AddAssert("Value is 0", () => header.Current.Value == 0); + AddAssert("Counter is visible", () => header.ChildrenOfType().First().Alpha == 1); + } + + [Test] + public void TestInitialVisibility() + { + AddStep("Create header with 0 value", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenNonZero, 0)); + AddAssert("Value is 0", () => header.Current.Value == 0); + AddAssert("Counter is visible", () => header.ChildrenOfType().First().Alpha == 1); + + AddStep("Create header with 1 value", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenNonZero, 1)); + AddAssert("Value is 1", () => header.Current.Value == 1); + AddAssert("Counter is hidden", () => header.ChildrenOfType().First().Alpha == 0); + } + + private void createHeader(string text, CounterVisibilityState state, int initialValue = 0) + { + Clear(); + Add(header = new PaginatedContainerHeader(text, state) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { Value = initialValue } + }); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/CounterPill.cs b/osu.Game/Overlays/Profile/Sections/CounterPill.cs index 52adefa4ad..131df105ad 100644 --- a/osu.Game/Overlays/Profile/Sections/CounterPill.cs +++ b/osu.Game/Overlays/Profile/Sections/CounterPill.cs @@ -13,8 +13,6 @@ namespace osu.Game.Overlays.Profile.Sections { public class CounterPill : CircularContainer { - private const int duration = 200; - public readonly BindableInt Current = new BindableInt(); private OsuSpriteText counter; @@ -23,7 +21,6 @@ namespace osu.Game.Overlays.Profile.Sections private void load(OverlayColourProvider colourProvider) { AutoSizeAxes = Axes.Both; - Alpha = 0; Masking = true; Children = new Drawable[] { @@ -36,8 +33,8 @@ namespace osu.Game.Overlays.Profile.Sections { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Margin = new MarginPadding { Horizontal = 10, Vertical = 5 }, - Font = OsuFont.GetFont(weight: FontWeight.Bold), + Margin = new MarginPadding { Horizontal = 10, Bottom = 1 }, + Font = OsuFont.GetFont(size: 14 * 0.8f, weight: FontWeight.Bold), Colour = colourProvider.Foreground1 } }; @@ -51,14 +48,7 @@ namespace osu.Game.Overlays.Profile.Sections private void onCurrentChanged(ValueChangedEvent value) { - if (value.NewValue == 0) - { - this.FadeOut(duration, Easing.OutQuint); - return; - } - counter.Text = value.NewValue.ToString("N0"); - this.FadeIn(duration, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs new file mode 100644 index 0000000000..e965b83682 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs @@ -0,0 +1,129 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Bindables; +using System; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics; + +namespace osu.Game.Overlays.Profile.Sections +{ + public class PaginatedContainerHeader : CompositeDrawable, IHasCurrentValue + { + public Bindable Current + { + get => current; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + current.UnbindBindings(); + current.BindTo(value); + } + } + + private readonly Bindable current = new Bindable(); + + private readonly string text; + private readonly CounterVisibilityState counterState; + + private CounterPill counterPill; + + public PaginatedContainerHeader(string text, CounterVisibilityState counterState) + { + this.text = text; + this.counterState = counterState; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + AutoSizeAxes = Axes.Both; + Padding = new MarginPadding { Vertical = 10 }; + InternalChildren = new Drawable[] + { + new CircularContainer + { + RelativeSizeAxes = Axes.Y, + Height = 0.65f, + Width = 3, + Masking = true, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10 }, + Child = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Highlight1 + } + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Text = text, + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), + }, + counterPill = new CounterPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Alpha = getInitialCounterAlpha(), + Current = { BindTarget = current } + } + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + current.BindValueChanged(onCurrentChanged); + } + + private float getInitialCounterAlpha() + { + switch (counterState) + { + case CounterVisibilityState.AlwaysHidden: + return 0; + + case CounterVisibilityState.AlwaysVisible: + return 1; + + case CounterVisibilityState.VisibleWhenNonZero: + return current.Value == 0 ? 1 : 0; + + default: + throw new NotImplementedException($"{counterState} has an incorrect value."); + } + } + + private void onCurrentChanged(ValueChangedEvent countValue) + { + if (counterState == CounterVisibilityState.VisibleWhenNonZero) + { + counterPill.Alpha = countValue.NewValue == 0 ? 1 : 0; + } + } + } + + public enum CounterVisibilityState + { + AlwaysHidden, + AlwaysVisible, + VisibleWhenNonZero + } +} From 33f14fe7b7b7f4c10b629b582dfd6e9c6219e8f7 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 7 Sep 2020 22:19:19 +0300 Subject: [PATCH 3090/6909] Remove no longer needed test --- .../Online/TestSceneProfileCounterPill.cs | 40 ------------------- 1 file changed, 40 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.cs deleted file mode 100644 index eaa989f0de..0000000000 --- a/osu.Game.Tests/Visual/Online/TestSceneProfileCounterPill.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 NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Overlays; -using osu.Game.Overlays.Profile.Sections; - -namespace osu.Game.Tests.Visual.Online -{ - public class TestSceneProfileCounterPill : OsuTestScene - { - [Cached] - private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red); - - private readonly CounterPill pill; - private readonly BindableInt value = new BindableInt(); - - public TestSceneProfileCounterPill() - { - Child = pill = new CounterPill - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Current = { BindTarget = value } - }; - } - - [Test] - public void TestVisibility() - { - AddStep("Set value to 0", () => value.Value = 0); - AddAssert("Check hidden", () => !pill.IsPresent); - AddStep("Set value to 10", () => value.Value = 10); - AddAssert("Check visible", () => pill.IsPresent); - } - } -} From 1c55039994e80d827d5739610d2e6b4710c9b04f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 7 Sep 2020 22:24:10 +0300 Subject: [PATCH 3091/6909] Remove old header from PaginatedContainer --- .../Beatmaps/PaginatedBeatmapContainer.cs | 14 ++------- .../Profile/Sections/BeatmapsSection.cs | 10 +++---- .../PaginatedMostPlayedBeatmapContainer.cs | 3 +- .../Profile/Sections/HistoricalSection.cs | 2 +- .../Kudosu/PaginatedKudosuHistoryContainer.cs | 4 +-- .../Profile/Sections/KudosuSection.cs | 2 +- .../Profile/Sections/PaginatedContainer.cs | 29 +------------------ .../Sections/Ranks/PaginatedScoreContainer.cs | 11 ++----- .../Overlays/Profile/Sections/RanksSection.cs | 4 +-- .../PaginatedRecentActivityContainer.cs | 4 +-- .../Profile/Sections/RecentSection.cs | 2 +- 11 files changed, 20 insertions(+), 65 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 191f3c908a..1936cb6188 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -18,8 +18,8 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps private const float panel_padding = 10f; private readonly BeatmapSetType type; - public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, string header, string missing = "None... yet.") - : base(user, header, missing) + public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user) + : base(user, "None... yet.") { this.type = type; @@ -38,15 +38,5 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }; - - protected override int GetCount(User user) => type switch - { - BeatmapSetType.Favourite => user.FavouriteBeatmapsetCount, - BeatmapSetType.Graveyard => user.GraveyardBeatmapsetCount, - BeatmapSetType.Loved => user.LovedBeatmapsetCount, - BeatmapSetType.RankedAndApproved => user.RankedAndApprovedBeatmapsetCount, - BeatmapSetType.Unranked => user.UnrankedBeatmapsetCount, - _ => 0 - }; } } diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs index 37f017277f..156696da16 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs @@ -16,11 +16,11 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, "Favourite Beatmaps"), - new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User, "Ranked & Approved Beatmaps"), - new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, "Loved Beatmaps"), - new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, "Pending Beatmaps"), - new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, "Graveyarded Beatmaps"), + new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User), + new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User), + new PaginatedBeatmapContainer(BeatmapSetType.Loved, User), + new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User), + new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User) }; } } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 6e6d6272c7..f16842f4ab 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -15,10 +15,9 @@ namespace osu.Game.Overlays.Profile.Sections.Historical public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer { public PaginatedMostPlayedBeatmapContainer(Bindable user) - : base(user, "Most Played Beatmaps", "No records. :(") + : base(user, "No records. :(") { ItemsPerPage = 5; - ItemsContainer.Direction = FillDirection.Vertical; } diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs index 4bdd25ee66..3d1a1efe6e 100644 --- a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Sections Children = new Drawable[] { new PaginatedMostPlayedBeatmapContainer(User), - new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", "No performance records. :("), + new PaginatedScoreContainer(ScoreType.Recent, User, "No performance records. :("), }; } } diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs index 0e7cfc37c0..923316d8c5 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs @@ -13,8 +13,8 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu { public class PaginatedKudosuHistoryContainer : PaginatedContainer { - public PaginatedKudosuHistoryContainer(Bindable user, string header, string missing) - : base(user, header, missing) + public PaginatedKudosuHistoryContainer(Bindable user, string missing) + : base(user, missing) { ItemsPerPage = 5; } diff --git a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs index 9ccce7d837..7e75e7e3e4 100644 --- a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs +++ b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Sections Children = new Drawable[] { new KudosuInfo(User), - new PaginatedKudosuHistoryContainer(User, null, @"This user hasn't received any kudosu!"), + new PaginatedKudosuHistoryContainer(User, "This user hasn't received any kudosu!"), }; } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 9720469548..87472e77ea 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -24,7 +24,6 @@ namespace osu.Game.Overlays.Profile.Sections private readonly OsuSpriteText missingText; private APIRequest> retrievalRequest; private CancellationTokenSource loadCancellation; - private readonly BindableInt count = new BindableInt(); [Resolved] private IAPIProvider api { get; set; } @@ -36,7 +35,7 @@ namespace osu.Game.Overlays.Profile.Sections protected readonly FillFlowContainer ItemsContainer; protected RulesetStore Rulesets; - protected PaginatedContainer(Bindable user, string header, string missing) + protected PaginatedContainer(Bindable user, string missing) { User.BindTo(user); @@ -46,29 +45,6 @@ namespace osu.Game.Overlays.Profile.Sections Children = new Drawable[] { - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(5, 0), - Margin = new MarginPadding { Top = 10, Bottom = 10 }, - Children = new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Text = header, - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Bold), - }, - new CounterPill - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Current = { BindTarget = count } - } - } - }, ItemsContainer = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -112,7 +88,6 @@ namespace osu.Game.Overlays.Profile.Sections if (e.NewValue != null) { showMore(); - count.Value = GetCount(e.NewValue); } } @@ -146,8 +121,6 @@ namespace osu.Game.Overlays.Profile.Sections }, loadCancellation.Token); }); - protected virtual int GetCount(User user) => 0; - protected abstract APIRequest> CreateRequest(); protected abstract Drawable CreateDrawableItem(TModel model); diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 64494f9814..2cefe45e4a 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -17,25 +17,18 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { private readonly ScoreType type; - public PaginatedScoreContainer(ScoreType type, Bindable user, string header, string missing) - : base(user, header, missing) + public PaginatedScoreContainer(ScoreType type, Bindable user, string missing) + : base(user, missing) { this.type = type; ItemsPerPage = 5; - ItemsContainer.Direction = FillDirection.Vertical; } protected override APIRequest> CreateRequest() => new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); - protected override int GetCount(User user) => type switch - { - ScoreType.Firsts => user.ScoresFirstCount, - _ => 0 - }; - protected override Drawable CreateDrawableItem(APILegacyScoreInfo model) { switch (type) diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index dbdff3a273..40bd050955 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -16,8 +16,8 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - new PaginatedScoreContainer(ScoreType.Best, User, "Best Performance", "No performance records. :("), - new PaginatedScoreContainer(ScoreType.Firsts, User, "First Place Ranks", "No awesome performance records yet. :("), + new PaginatedScoreContainer(ScoreType.Best, User, "No performance records. :("), + new PaginatedScoreContainer(ScoreType.Firsts, User, "No awesome performance records yet. :("), }; } } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index a37f398272..4c828ef0c1 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -14,8 +14,8 @@ namespace osu.Game.Overlays.Profile.Sections.Recent { public class PaginatedRecentActivityContainer : PaginatedContainer { - public PaginatedRecentActivityContainer(Bindable user, string header, string missing) - : base(user, header, missing) + public PaginatedRecentActivityContainer(Bindable user, string missing) + : base(user, missing) { ItemsPerPage = 10; ItemsContainer.Spacing = new Vector2(0, 8); diff --git a/osu.Game/Overlays/Profile/Sections/RecentSection.cs b/osu.Game/Overlays/Profile/Sections/RecentSection.cs index 8fcc5cc7c0..0c118b80b5 100644 --- a/osu.Game/Overlays/Profile/Sections/RecentSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RecentSection.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - new PaginatedRecentActivityContainer(User, null, @"This user hasn't done anything notable recently!"), + new PaginatedRecentActivityContainer(User, "This user hasn't done anything notable recently!"), }; } } From b7bd084296a90a50672b7355b41842a405ad79d1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 7 Sep 2020 22:30:43 +0300 Subject: [PATCH 3092/6909] Remove missing text where not needed --- .../Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs | 2 +- osu.Game/Overlays/Profile/Sections/HistoricalSection.cs | 2 +- .../Sections/Kudosu/PaginatedKudosuHistoryContainer.cs | 4 ++-- osu.Game/Overlays/Profile/Sections/KudosuSection.cs | 2 +- osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs | 7 +++++-- .../Profile/Sections/Ranks/PaginatedScoreContainer.cs | 4 ++-- osu.Game/Overlays/Profile/Sections/RanksSection.cs | 4 ++-- .../Sections/Recent/PaginatedRecentActivityContainer.cs | 4 ++-- osu.Game/Overlays/Profile/Sections/RecentSection.cs | 2 +- 9 files changed, 17 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 1936cb6188..1f99b75909 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps private readonly BeatmapSetType type; public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user) - : base(user, "None... yet.") + : base(user) { this.type = type; diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs index 3d1a1efe6e..e021f16e5e 100644 --- a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Sections Children = new Drawable[] { new PaginatedMostPlayedBeatmapContainer(User), - new PaginatedScoreContainer(ScoreType.Recent, User, "No performance records. :("), + new PaginatedScoreContainer(ScoreType.Recent, User), }; } } diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs index 923316d8c5..c823053c4b 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs @@ -13,8 +13,8 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu { public class PaginatedKudosuHistoryContainer : PaginatedContainer { - public PaginatedKudosuHistoryContainer(Bindable user, string missing) - : base(user, missing) + public PaginatedKudosuHistoryContainer(Bindable user) + : base(user, "This user hasn't received any kudosu!") { ItemsPerPage = 5; } diff --git a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs index 7e75e7e3e4..a9e9952257 100644 --- a/osu.Game/Overlays/Profile/Sections/KudosuSection.cs +++ b/osu.Game/Overlays/Profile/Sections/KudosuSection.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Sections Children = new Drawable[] { new KudosuInfo(User), - new PaginatedKudosuHistoryContainer(User, "This user hasn't received any kudosu!"), + new PaginatedKudosuHistoryContainer(User), }; } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 87472e77ea..9ddca48298 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Profile.Sections protected readonly FillFlowContainer ItemsContainer; protected RulesetStore Rulesets; - protected PaginatedContainer(Bindable user, string missing) + protected PaginatedContainer(Bindable user, string missing = "") { User.BindTo(user); @@ -107,7 +107,10 @@ namespace osu.Game.Overlays.Profile.Sections { moreButton.Hide(); moreButton.IsLoading = false; - missingText.Show(); + + if (!string.IsNullOrEmpty(missingText.Text)) + missingText.Show(); + return; } diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 2cefe45e4a..fbf92fd2e6 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -17,8 +17,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { private readonly ScoreType type; - public PaginatedScoreContainer(ScoreType type, Bindable user, string missing) - : base(user, missing) + public PaginatedScoreContainer(ScoreType type, Bindable user) + : base(user) { this.type = type; diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 40bd050955..18bf4f31d8 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -16,8 +16,8 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - new PaginatedScoreContainer(ScoreType.Best, User, "No performance records. :("), - new PaginatedScoreContainer(ScoreType.Firsts, User, "No awesome performance records yet. :("), + new PaginatedScoreContainer(ScoreType.Best, User), + new PaginatedScoreContainer(ScoreType.Firsts, User), }; } } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index 4c828ef0c1..a2f844503f 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -14,8 +14,8 @@ namespace osu.Game.Overlays.Profile.Sections.Recent { public class PaginatedRecentActivityContainer : PaginatedContainer { - public PaginatedRecentActivityContainer(Bindable user, string missing) - : base(user, missing) + public PaginatedRecentActivityContainer(Bindable user) + : base(user) { ItemsPerPage = 10; ItemsContainer.Spacing = new Vector2(0, 8); diff --git a/osu.Game/Overlays/Profile/Sections/RecentSection.cs b/osu.Game/Overlays/Profile/Sections/RecentSection.cs index 0c118b80b5..1e6cfcc9fd 100644 --- a/osu.Game/Overlays/Profile/Sections/RecentSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RecentSection.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - new PaginatedRecentActivityContainer(User, "This user hasn't done anything notable recently!"), + new PaginatedRecentActivityContainer(User), }; } } From e39609d3ca6bf4b55009def24f13997c5ea7efc4 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 7 Sep 2020 23:08:50 +0300 Subject: [PATCH 3093/6909] Implement PaginatedContainerWithHeader component --- .../TestScenePaginatedContainerHeader.cs | 11 ++++-- .../Beatmaps/PaginatedBeatmapContainer.cs | 37 +++++++++++++++++-- .../Profile/Sections/BeatmapsSection.cs | 10 ++--- .../PaginatedMostPlayedBeatmapContainer.cs | 10 ++++- .../Profile/Sections/HistoricalSection.cs | 2 +- .../Profile/Sections/PaginatedContainer.cs | 33 +++++++++++------ .../Sections/PaginatedContainerHeader.cs | 11 ++++-- .../Sections/PaginatedContainerWithHeader.cs | 34 +++++++++++++++++ .../Sections/Ranks/PaginatedScoreContainer.cs | 24 ++++++++++-- .../Overlays/Profile/Sections/RanksSection.cs | 4 +- .../PaginatedRecentActivityContainer.cs | 8 +++- 11 files changed, 147 insertions(+), 37 deletions(-) create mode 100644 osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs index 114a3af1d9..2e9f919cfd 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs @@ -1,4 +1,7 @@ -using NUnit.Framework; +// 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.Overlays.Profile.Sections; using osu.Framework.Testing; using System.Linq; @@ -40,7 +43,7 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestVisibleWhenZeroCounter() { - AddStep("Create header", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenNonZero)); + AddStep("Create header", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenZero)); AddAssert("Value is 0", () => header.Current.Value == 0); AddAssert("Counter is visible", () => header.ChildrenOfType().First().Alpha == 1); AddStep("Set count 10", () => header.Current.Value = 10); @@ -54,11 +57,11 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestInitialVisibility() { - AddStep("Create header with 0 value", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenNonZero, 0)); + AddStep("Create header with 0 value", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenZero, 0)); AddAssert("Value is 0", () => header.Current.Value == 0); AddAssert("Counter is visible", () => header.ChildrenOfType().First().Alpha == 1); - AddStep("Create header with 1 value", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenNonZero, 1)); + AddStep("Create header with 1 value", () => createHeader("Header with visible when zero counter", CounterVisibilityState.VisibleWhenZero, 1)); AddAssert("Value is 1", () => header.Current.Value == 1); AddAssert("Counter is hidden", () => header.ChildrenOfType().First().Alpha == 0); } diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 1f99b75909..ea700a812f 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.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.Bindables; using osu.Framework.Graphics; using osu.Game.Online.API; @@ -13,21 +14,49 @@ using osuTK; namespace osu.Game.Overlays.Profile.Sections.Beatmaps { - public class PaginatedBeatmapContainer : PaginatedContainer + public class PaginatedBeatmapContainer : PaginatedContainerWithHeader { private const float panel_padding = 10f; private readonly BeatmapSetType type; - public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user) - : base(user) + public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, string headerText) + : base(user, headerText, CounterVisibilityState.AlwaysVisible) { this.type = type; - ItemsPerPage = 6; + } + [BackgroundDependencyLoader] + private void load() + { ItemsContainer.Spacing = new Vector2(panel_padding); } + protected override int GetCount(User user) + { + switch (type) + { + case BeatmapSetType.Favourite: + return user.FavouriteBeatmapsetCount; + + case BeatmapSetType.Graveyard: + return user.GraveyardBeatmapsetCount; + + case BeatmapSetType.Loved: + return user.LovedBeatmapsetCount; + + case BeatmapSetType.RankedAndApproved: + return user.RankedAndApprovedBeatmapsetCount; + + case BeatmapSetType.Unranked: + return user.UnrankedBeatmapsetCount; + + default: + return 0; + } + } + + protected override APIRequest> CreateRequest() => new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); diff --git a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs index 156696da16..c283de42f3 100644 --- a/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs +++ b/osu.Game/Overlays/Profile/Sections/BeatmapsSection.cs @@ -16,11 +16,11 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User), - new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User), - new PaginatedBeatmapContainer(BeatmapSetType.Loved, User), - new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User), - new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User) + new PaginatedBeatmapContainer(BeatmapSetType.Favourite, User, "Favourite Beatmaps"), + new PaginatedBeatmapContainer(BeatmapSetType.RankedAndApproved, User, "Ranked & Approved Beatmaps"), + new PaginatedBeatmapContainer(BeatmapSetType.Loved, User, "Loved Beatmaps"), + new PaginatedBeatmapContainer(BeatmapSetType.Unranked, User, "Pending Beatmaps"), + new PaginatedBeatmapContainer(BeatmapSetType.Graveyard, User, "Graveyarded Beatmaps") }; } } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index f16842f4ab..ad35ea1460 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,12 +13,17 @@ using osu.Game.Users; namespace osu.Game.Overlays.Profile.Sections.Historical { - public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer + public class PaginatedMostPlayedBeatmapContainer : PaginatedContainerWithHeader { public PaginatedMostPlayedBeatmapContainer(Bindable user) - : base(user, "No records. :(") + : base(user, "Most Played Beatmaps", CounterVisibilityState.AlwaysHidden, "No records. :(") { ItemsPerPage = 5; + } + + [BackgroundDependencyLoader] + private void load() + { ItemsContainer.Direction = FillDirection.Vertical; } diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs index e021f16e5e..bfc47bd88c 100644 --- a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Sections Children = new Drawable[] { new PaginatedMostPlayedBeatmapContainer(User), - new PaginatedScoreContainer(ScoreType.Recent, User), + new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", CounterVisibilityState.VisibleWhenZero), }; } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 9ddca48298..1bc8ffe671 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -20,11 +20,6 @@ namespace osu.Game.Overlays.Profile.Sections { public abstract class PaginatedContainer : FillFlowContainer { - private readonly ShowMoreButton moreButton; - private readonly OsuSpriteText missingText; - private APIRequest> retrievalRequest; - private CancellationTokenSource loadCancellation; - [Resolved] private IAPIProvider api { get; set; } @@ -32,19 +27,32 @@ namespace osu.Game.Overlays.Profile.Sections protected int ItemsPerPage; protected readonly Bindable User = new Bindable(); - protected readonly FillFlowContainer ItemsContainer; + protected FillFlowContainer ItemsContainer; protected RulesetStore Rulesets; + private APIRequest> retrievalRequest; + private CancellationTokenSource loadCancellation; + + private readonly string missing; + private ShowMoreButton moreButton; + private OsuSpriteText missingText; + protected PaginatedContainer(Bindable user, string missing = "") { + this.missing = missing; User.BindTo(user); + } + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; Children = new Drawable[] { + CreateHeaderContent, ItemsContainer = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -66,11 +74,7 @@ namespace osu.Game.Overlays.Profile.Sections Alpha = 0, }, }; - } - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) - { Rulesets = rulesets; User.ValueChanged += onUserChanged; @@ -87,7 +91,7 @@ namespace osu.Game.Overlays.Profile.Sections if (e.NewValue != null) { - showMore(); + OnUserChanged(e.NewValue); } } @@ -124,6 +128,13 @@ namespace osu.Game.Overlays.Profile.Sections }, loadCancellation.Token); }); + protected virtual void OnUserChanged(User user) + { + showMore(); + } + + protected virtual Drawable CreateHeaderContent => Empty(); + protected abstract APIRequest> CreateRequest(); protected abstract Drawable CreateDrawableItem(TModel model); diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs index e965b83682..4779b44eb0 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.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.Containers; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; @@ -103,7 +106,7 @@ namespace osu.Game.Overlays.Profile.Sections case CounterVisibilityState.AlwaysVisible: return 1; - case CounterVisibilityState.VisibleWhenNonZero: + case CounterVisibilityState.VisibleWhenZero: return current.Value == 0 ? 1 : 0; default: @@ -113,7 +116,7 @@ namespace osu.Game.Overlays.Profile.Sections private void onCurrentChanged(ValueChangedEvent countValue) { - if (counterState == CounterVisibilityState.VisibleWhenNonZero) + if (counterState == CounterVisibilityState.VisibleWhenZero) { counterPill.Alpha = countValue.NewValue == 0 ? 1 : 0; } @@ -124,6 +127,6 @@ namespace osu.Game.Overlays.Profile.Sections { AlwaysHidden, AlwaysVisible, - VisibleWhenNonZero + VisibleWhenZero } } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs new file mode 100644 index 0000000000..cf88b290ae --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs @@ -0,0 +1,34 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Users; + +namespace osu.Game.Overlays.Profile.Sections +{ + public abstract class PaginatedContainerWithHeader : PaginatedContainer + { + private readonly string headerText; + private readonly CounterVisibilityState counterVisibilityState; + + private PaginatedContainerHeader header; + + public PaginatedContainerWithHeader(Bindable user, string headerText, CounterVisibilityState counterVisibilityState, string missing = "") + : base(user, missing) + { + this.headerText = headerText; + this.counterVisibilityState = counterVisibilityState; + } + + protected override Drawable CreateHeaderContent => header = new PaginatedContainerHeader(headerText, counterVisibilityState); + + protected override void OnUserChanged(User user) + { + base.OnUserChanged(user); + header.Current.Value = GetCount(user); + } + + protected virtual int GetCount(User user) => 0; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index fbf92fd2e6..f1cf3e632c 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -10,22 +10,40 @@ using osu.Framework.Graphics; using osu.Game.Online.API.Requests.Responses; using System.Collections.Generic; using osu.Game.Online.API; +using osu.Framework.Allocation; namespace osu.Game.Overlays.Profile.Sections.Ranks { - public class PaginatedScoreContainer : PaginatedContainer + public class PaginatedScoreContainer : PaginatedContainerWithHeader { private readonly ScoreType type; - public PaginatedScoreContainer(ScoreType type, Bindable user) - : base(user) + public PaginatedScoreContainer(ScoreType type, Bindable user, string headerText, CounterVisibilityState counterVisibilityState, string missingText = "") + : base(user, headerText, counterVisibilityState, missingText) { this.type = type; ItemsPerPage = 5; + } + + [BackgroundDependencyLoader] + private void load() + { ItemsContainer.Direction = FillDirection.Vertical; } + protected override int GetCount(User user) + { + switch (type) + { + case ScoreType.Firsts: + return user.ScoresFirstCount; + + default: + return 0; + } + } + protected override APIRequest> CreateRequest() => new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); diff --git a/osu.Game/Overlays/Profile/Sections/RanksSection.cs b/osu.Game/Overlays/Profile/Sections/RanksSection.cs index 18bf4f31d8..e41e414893 100644 --- a/osu.Game/Overlays/Profile/Sections/RanksSection.cs +++ b/osu.Game/Overlays/Profile/Sections/RanksSection.cs @@ -16,8 +16,8 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new[] { - new PaginatedScoreContainer(ScoreType.Best, User), - new PaginatedScoreContainer(ScoreType.Firsts, User), + new PaginatedScoreContainer(ScoreType.Best, User, "Best Performance", CounterVisibilityState.AlwaysHidden, "No performance records. :("), + new PaginatedScoreContainer(ScoreType.Firsts, User, "First Place Ranks", CounterVisibilityState.AlwaysVisible) }; } } diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index a2f844503f..adfe31109b 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -9,15 +9,21 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.API; using System.Collections.Generic; using osuTK; +using osu.Framework.Allocation; namespace osu.Game.Overlays.Profile.Sections.Recent { public class PaginatedRecentActivityContainer : PaginatedContainer { public PaginatedRecentActivityContainer(Bindable user) - : base(user) + : base(user, "This user hasn't done anything notable recently!") { ItemsPerPage = 10; + } + + [BackgroundDependencyLoader] + private void load() + { ItemsContainer.Spacing = new Vector2(0, 8); } From c72a192cb5f59c5e85ac7b1ec381f16ab760a0f9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 7 Sep 2020 23:33:04 +0300 Subject: [PATCH 3094/6909] Fix recent plays counter is always zero --- .../Overlays/Profile/Sections/PaginatedContainer.cs | 6 ++++++ .../Profile/Sections/PaginatedContainerWithHeader.cs | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 1bc8ffe671..9693c8b5f3 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -107,6 +107,8 @@ namespace osu.Game.Overlays.Profile.Sections protected virtual void UpdateItems(List items) => Schedule(() => { + OnItemsReceived(items); + if (!items.Any() && VisiblePages == 1) { moreButton.Hide(); @@ -133,6 +135,10 @@ namespace osu.Game.Overlays.Profile.Sections showMore(); } + protected virtual void OnItemsReceived(List items) + { + } + protected virtual Drawable CreateHeaderContent => Empty(); protected abstract APIRequest> CreateRequest(); diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs index cf88b290ae..f27ea7a626 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Users; @@ -29,6 +30,17 @@ namespace osu.Game.Overlays.Profile.Sections header.Current.Value = GetCount(user); } + protected override void OnItemsReceived(List items) + { + base.OnItemsReceived(items); + + if (counterVisibilityState == CounterVisibilityState.VisibleWhenZero) + { + header.Current.Value = items.Count; + header.Current.TriggerChange(); + } + } + protected virtual int GetCount(User user) => 0; } } From f88b2509f862062383e32bd1a78c585fcaa8fc09 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 7 Sep 2020 23:43:26 +0300 Subject: [PATCH 3095/6909] Fix ProfileSection header margin is too small --- osu.Game/Overlays/Profile/ProfileSection.cs | 2 +- .../Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs | 1 - osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs | 2 +- .../Overlays/Profile/Sections/PaginatedContainerWithHeader.cs | 2 +- 4 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/ProfileSection.cs b/osu.Game/Overlays/Profile/ProfileSection.cs index 2e19ae4b64..21f7921da6 100644 --- a/osu.Game/Overlays/Profile/ProfileSection.cs +++ b/osu.Game/Overlays/Profile/ProfileSection.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Profile { Horizontal = UserProfileOverlay.CONTENT_X_MARGIN, Top = 15, - Bottom = 10, + Bottom = 20, }, Children = new Drawable[] { diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index ea700a812f..d7c72131ea 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -56,7 +56,6 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps } } - protected override APIRequest> CreateRequest() => new GetUserBeatmapsRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 9693c8b5f3..c22e5660e6 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.Profile.Sections AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; - Children = new Drawable[] + Children = new[] { CreateHeaderContent, ItemsContainer = new FillFlowContainer diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs index f27ea7a626..9d8ed89053 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs @@ -15,7 +15,7 @@ namespace osu.Game.Overlays.Profile.Sections private PaginatedContainerHeader header; - public PaginatedContainerWithHeader(Bindable user, string headerText, CounterVisibilityState counterVisibilityState, string missing = "") + protected PaginatedContainerWithHeader(Bindable user, string headerText, CounterVisibilityState counterVisibilityState, string missing = "") : base(user, missing) { this.headerText = headerText; From 1bc41bcfd7d61fb44e3a6fed3a1171664a1f658b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 8 Sep 2020 00:04:14 +0300 Subject: [PATCH 3096/6909] Move scores counter logic to a better place --- .../Sections/PaginatedContainerWithHeader.cs | 18 +++--------------- .../Sections/Ranks/PaginatedScoreContainer.cs | 13 +++++++++++++ 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs index 9d8ed89053..32c589e342 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Users; @@ -13,7 +12,7 @@ namespace osu.Game.Overlays.Profile.Sections private readonly string headerText; private readonly CounterVisibilityState counterVisibilityState; - private PaginatedContainerHeader header; + protected PaginatedContainerHeader Header; protected PaginatedContainerWithHeader(Bindable user, string headerText, CounterVisibilityState counterVisibilityState, string missing = "") : base(user, missing) @@ -22,23 +21,12 @@ namespace osu.Game.Overlays.Profile.Sections this.counterVisibilityState = counterVisibilityState; } - protected override Drawable CreateHeaderContent => header = new PaginatedContainerHeader(headerText, counterVisibilityState); + protected override Drawable CreateHeaderContent => Header = new PaginatedContainerHeader(headerText, counterVisibilityState); protected override void OnUserChanged(User user) { base.OnUserChanged(user); - header.Current.Value = GetCount(user); - } - - protected override void OnItemsReceived(List items) - { - base.OnItemsReceived(items); - - if (counterVisibilityState == CounterVisibilityState.VisibleWhenZero) - { - header.Current.Value = items.Count; - header.Current.TriggerChange(); - } + Header.Current.Value = GetCount(user); } protected virtual int GetCount(User user) => 0; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index f1cf3e632c..0b2bddabbc 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -44,6 +44,19 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks } } + protected override void OnItemsReceived(List items) + { + base.OnItemsReceived(items); + + if (type == ScoreType.Recent) + { + var count = items.Count; + + Header.Current.Value = count == 0 ? 0 : -1; + Header.Current.TriggerChange(); + } + } + protected override APIRequest> CreateRequest() => new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); From 5268eee0fb7a51866d78eec067a4d5e2e069f264 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 11:31:42 +0900 Subject: [PATCH 3097/6909] Avoid requiring sending the calling method for CleanRunHeadlessGameHost --- .../Beatmaps/IO/ImportBeatmapTest.cs | 38 +++++++++---------- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 10 ++--- osu.Game/Tests/CleanRunHeadlessGameHost.cs | 12 +++++- 3 files changed, 34 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 0702b02bb1..dd3dba1274 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportWhenClosed() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenClosed))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenDelete() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDelete))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImport))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportWithReZip() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithReZip))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -156,7 +156,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportWithChangedFile() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithChangedFile))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -207,7 +207,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportWithDifferentFilename() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenImportWithDifferentFilename))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -259,7 +259,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportCorruptThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportCorruptThenImport))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -301,7 +301,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestRollbackOnFailure() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestRollbackOnFailure))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -378,7 +378,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenDeleteThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportThenDeleteThenImport))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -406,7 +406,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) { // 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}")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(set.ToString())) { try { @@ -440,7 +440,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportWithDuplicateBeatmapIDs() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateBeatmapIDs))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -526,7 +526,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWhenFileOpen() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWhenFileOpen))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -548,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWithDuplicateHashes() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithDuplicateHashes))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -590,7 +590,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportNestedStructure() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportNestedStructure))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -635,7 +635,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWithIgnoredDirectoryInArchive() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestImportWithIgnoredDirectoryInArchive))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -689,7 +689,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestUpdateBeatmapInfo() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapInfo))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -719,7 +719,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestUpdateBeatmapFile() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestUpdateBeatmapFile))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -761,7 +761,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public void TestCreateNewEmptyBeatmap() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestCreateNewEmptyBeatmap))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -788,7 +788,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public void TestCreateNewBeatmapWithObject() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestCreateNewBeatmapWithObject))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index 57f0d7e957..a4d20714fa 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestBasicImport() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestBasicImport")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -66,7 +66,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestImportMods() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportMods")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestImportStatistics() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportStatistics")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -122,7 +122,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestImportWithDeletedBeatmapSet() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithDeletedBeatmapSet")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { @@ -159,7 +159,7 @@ namespace osu.Game.Tests.Scores.IO [Test] public async Task TestOnlineScoreIsAvailableLocally() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestOnlineScoreIsAvailableLocally")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { diff --git a/osu.Game/Tests/CleanRunHeadlessGameHost.cs b/osu.Game/Tests/CleanRunHeadlessGameHost.cs index bfbf7bb9da..baa7b27d28 100644 --- a/osu.Game/Tests/CleanRunHeadlessGameHost.cs +++ b/osu.Game/Tests/CleanRunHeadlessGameHost.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.Runtime.CompilerServices; using osu.Framework.Platform; namespace osu.Game.Tests @@ -10,8 +11,15 @@ namespace osu.Game.Tests /// public class CleanRunHeadlessGameHost : HeadlessGameHost { - public CleanRunHeadlessGameHost(string gameName = @"", bool bindIPC = false, bool realtime = true) - : base(gameName, bindIPC, realtime) + /// + /// Create a new instance. + /// + /// An optional suffix which will isolate this host from others called from the same method source. + /// Whether to bind IPC channels. + /// Whether the host should be forced to run in realtime, rather than accelerated test time. + /// The name of the calling method, used for test file isolation and clean-up. + public CleanRunHeadlessGameHost(string gameSuffix = @"", bool bindIPC = false, bool realtime = true, [CallerMemberName] string callingMethodName = @"") + : base(callingMethodName + gameSuffix, bindIPC, realtime) { } From 3e5ea6c42fc43c6a31635863481cb0186e799952 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 11:59:47 +0900 Subject: [PATCH 3098/6909] Change "Add to" to "Collections" Doesn't make send to be 'add to' when it can also remove --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 008cf85018..5618f8f97f 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -231,7 +231,7 @@ namespace osu.Game.Screens.Select.Carousel if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionsDialog.Show)); - items.Add(new OsuMenuItem("Add to...") { Items = collectionItems }); + items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); return items.ToArray(); } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index fe0ad31b32..78ec59eb5b 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Select.Carousel if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionsDialog.Show)); - items.Add(new OsuMenuItem("Add all to...") { Items = collectionItems }); + items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); return items.ToArray(); } From b15bbc882af8150af0681dac588a18afe17bc61a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 12:04:35 +0900 Subject: [PATCH 3099/6909] Move items up in menu --- .../Select/Carousel/DrawableCarouselBeatmap.cs | 6 +++--- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 11 +++++------ 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 5618f8f97f..28c3529fc0 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -221,9 +221,6 @@ namespace osu.Game.Screens.Select.Carousel if (editRequested != null) items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmap))); - 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))); @@ -233,6 +230,9 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); + if (hideRequested != null) + items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmap))); + return items.ToArray(); } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 78ec59eb5b..327cbc4765 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -142,18 +142,17 @@ namespace osu.Game.Screens.Select.Carousel 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(beatmapSet))); - - if (dialogOverlay != null) - items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); - var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyDate).Take(3).Select(createCollectionMenuItem).ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionsDialog.Show)); items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); + if (beatmapSet.Beatmaps.Any(b => b.Hidden)) + items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet))); + + if (dialogOverlay != null) + items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); return items.ToArray(); } } From 8b770626fa877e08eef3baf54aa3657c84b4b664 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 12:18:08 +0900 Subject: [PATCH 3100/6909] Add missing '...' from some popup menu items --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 28c3529fc0..9f21ec1ad1 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -222,7 +222,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Edit", MenuItemType.Standard, () => editRequested(beatmap))); if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) - items.Add(new OsuMenuItem("Details", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); + items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyDate).Take(3).Select(createCollectionMenuItem).ToList(); if (manageCollectionsDialog != null) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 327cbc4765..19ecc277c4 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -152,7 +152,7 @@ namespace osu.Game.Screens.Select.Carousel items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet))); if (dialogOverlay != null) - items.Add(new OsuMenuItem("Delete", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); + items.Add(new OsuMenuItem("Delete...", MenuItemType.Destructive, () => dialogOverlay.Push(new BeatmapDeleteDialog(beatmapSet)))); return items.ToArray(); } } From ab58f60529d5a53cda28ee8cc010ac3f13d23cf2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 12:47:21 +0900 Subject: [PATCH 3101/6909] Remove elasticity from dialog appearing --- osu.Game/Collections/ManageCollectionsDialog.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index f2aedb1c29..036a745913 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -111,7 +111,7 @@ namespace osu.Game.Collections base.PopIn(); this.FadeIn(enter_duration, Easing.OutQuint); - this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutElastic); + this.ScaleTo(0.9f).Then().ScaleTo(1f, enter_duration, Easing.OutQuint); } protected override void PopOut() From 3e96c6d036116cd8ef477dd529a26ccc90f90f6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 12:51:42 +0900 Subject: [PATCH 3102/6909] Improve paddings of collection management dialog --- osu.Game/Collections/DrawableCollectionList.cs | 3 ++- osu.Game/Collections/DrawableCollectionListItem.cs | 1 - 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index ab146c17b6..e8bde9066f 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -13,13 +13,14 @@ namespace osu.Game.Collections protected override ScrollContainer CreateScrollContainer() => base.CreateScrollContainer().With(d => { d.ScrollbarVisible = false; + d.Padding = new MarginPadding(10); }); protected override FillFlowContainer> CreateListFillFlowContainer() => new FillFlowContainer> { LayoutDuration = 200, LayoutEasing = Easing.OutQuint, - Spacing = new Vector2(0, 2) + Spacing = new Vector2(0, 5) }; protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapCollection item) => new DrawableCollectionListItem(item); diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index c7abf58d10..e11f14ccae 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -25,7 +25,6 @@ namespace osu.Game.Collections public DrawableCollectionListItem(BeatmapCollection item) : base(item) { - Padding = new MarginPadding { Right = 20 }; } protected override Drawable CreateContent() => new ItemContent(Model); From 0e93bbb62df3c7197513b414e2612559f0f5e2f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 13:02:58 +0900 Subject: [PATCH 3103/6909] Adjust sizing of delete button --- osu.Game/Collections/DrawableCollectionListItem.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index e11f14ccae..9b7505f7c3 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -22,6 +22,8 @@ namespace osu.Game.Collections { private const float item_height = 35; + private const float button_width = item_height * 0.75f; + public DrawableCollectionListItem(BeatmapCollection item) : base(item) { @@ -58,7 +60,7 @@ namespace osu.Game.Collections new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = item_height / 2 }, + Padding = new MarginPadding { Right = button_width }, Children = new Drawable[] { textBox = new ItemTextBox @@ -100,8 +102,9 @@ namespace osu.Game.Collections public DeleteButton(BeatmapCollection collection) { this.collection = collection; - RelativeSizeAxes = Axes.Both; - FillMode = FillMode.Fit; + RelativeSizeAxes = Axes.Y; + + Width = button_width + item_height / 2; // add corner radius to cover with fill Alpha = 0.1f; } @@ -119,8 +122,8 @@ namespace osu.Game.Collections new SpriteIcon { Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - X = -6, + Origin = Anchor.Centre, + X = -button_width * 0.6f, Size = new Vector2(10), Icon = FontAwesome.Solid.Trash } From 525026e7f09d788f5abdce40b546b0c7617569d5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 13:23:50 +0900 Subject: [PATCH 3104/6909] Fix tests failing due to timings --- .../TestSceneManageCollectionsDialog.cs | 26 ++++++++++++------- .../SongSelect/TestSceneFilterControl.cs | 1 - 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 2d6f8abd8b..fdaded6a5c 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -19,30 +19,30 @@ namespace osu.Game.Tests.Visual.Collections { public class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene { - [Cached] - private readonly DialogOverlay dialogOverlay; - protected override Container Content => content; private readonly Container content; + private readonly DialogOverlay dialogOverlay; + private readonly BeatmapCollectionManager manager; - private BeatmapCollectionManager manager; private ManageCollectionsDialog dialog; public TestSceneManageCollectionsDialog() { base.Content.AddRange(new Drawable[] { + manager = new BeatmapCollectionManager(LocalStorage), content = new Container { RelativeSizeAxes = Axes.Both }, dialogOverlay = new DialogOverlay() }); } - [BackgroundDependencyLoader] - private void load() + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - Dependencies.Cache(manager = new BeatmapCollectionManager(LocalStorage)); - Add(manager); + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.Cache(manager); + dependencies.Cache(dialogOverlay); + return dependencies; } [SetUp] @@ -120,6 +120,8 @@ namespace osu.Game.Tests.Visual.Collections new BeatmapCollection { Name = { Value = "2" } }, })); + assertCollectionCount(2); + AddStep("click first delete button", () => { InputManager.MoveMouseTo(dialog.ChildrenOfType().First(), new Vector2(5, 0)); @@ -146,6 +148,8 @@ namespace osu.Game.Tests.Visual.Collections new BeatmapCollection { Name = { Value = "2" } }, })); + assertCollectionCount(2); + AddStep("click first delete button", () => { InputManager.MoveMouseTo(dialog.ChildrenOfType().First(), new Vector2(5, 0)); @@ -185,14 +189,16 @@ namespace osu.Game.Tests.Visual.Collections new BeatmapCollection { Name = { Value = "2" } }, })); + assertCollectionCount(2); + AddStep("change first collection name", () => dialog.ChildrenOfType().First().Text = "First"); AddAssert("collection has new name", () => manager.Collections[0].Name.Value == "First"); } private void assertCollectionCount(int count) - => AddAssert($"{count} collections shown", () => dialog.ChildrenOfType().Count() == count); + => AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType().Count() == count); private void assertCollectionName(int index, string name) - => AddAssert($"item {index + 1} has correct name", () => dialog.ChildrenOfType().ElementAt(index).ChildrenOfType().First().Text == name); + => AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType().ElementAt(index).ChildrenOfType().First().Text == name); } } diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index c2dd652b3a..dea1c4b9b4 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -26,7 +26,6 @@ namespace osu.Game.Tests.Visual.SongSelect protected override Container Content => content; private readonly Container content; - [Cached] private readonly BeatmapCollectionManager collectionManager; private RulesetStore rulesets; From 32e3f5d0919abc55ab518e6d4c6dfddd9d7feb5a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 13:45:26 +0900 Subject: [PATCH 3105/6909] Adjust button styling --- .../Select/CollectionFilterDropdown.cs | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index 18caae9545..b0bd91b07d 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Graphics; @@ -166,6 +167,7 @@ namespace osu.Game.Screens.Select private IconButton addOrRemoveButton; private Content content; + private bool beatmapInCollection; public CollectionDropdownMenuItem(MenuItem item) : base(item) @@ -182,9 +184,10 @@ namespace osu.Game.Screens.Select Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, X = -OsuScrollContainer.SCROLL_BAR_HEIGHT, - Scale = new Vector2(0.75f), + Scale = new Vector2(0.7f), + AlwaysPresent = true, Alpha = collectionBeatmaps == null ? 0 : 1, - Action = addOrRemove + Action = addOrRemove, }); } @@ -203,24 +206,33 @@ namespace osu.Game.Screens.Select collectionName.BindValueChanged(name => content.Text = name.NewValue, true); } + protected override bool OnHover(HoverEvent e) + { + updateButtonVisibility(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + updateButtonVisibility(); + base.OnHoverLost(e); + } + private void collectionChanged() { Debug.Assert(collectionBeatmaps != null); - addOrRemoveButton.Enabled.Value = !beatmap.IsDefault; + beatmapInCollection = collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo); - if (collectionBeatmaps.Contains(beatmap.Value.BeatmapInfo)) - { - addOrRemoveButton.Icon = FontAwesome.Solid.MinusCircle; - addOrRemoveButton.IconColour = colours.Red; - } - else - { - addOrRemoveButton.Icon = FontAwesome.Solid.PlusCircle; - addOrRemoveButton.IconColour = colours.Green; - } + addOrRemoveButton.Enabled.Value = !beatmap.IsDefault; + addOrRemoveButton.Icon = beatmapInCollection ? FontAwesome.Solid.MinusSquare : FontAwesome.Solid.PlusSquare; + addOrRemoveButton.TooltipText = beatmapInCollection ? "Remove selected beatmap" : "Add selected beatmap"; + + updateButtonVisibility(); } + private void updateButtonVisibility() => addOrRemoveButton.Alpha = IsHovered || beatmapInCollection ? 1 : 0; + private void addOrRemove() { Debug.Assert(collectionBeatmaps != null); From 8a3c8a61854d123b74b23caf9b5c389727a59592 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 14:03:49 +0900 Subject: [PATCH 3106/6909] Show button when selected or preselected --- osu.Game/Screens/Select/CollectionFilterDropdown.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index b0bd91b07d..c4b98fa854 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -231,7 +231,13 @@ namespace osu.Game.Screens.Select updateButtonVisibility(); } - private void updateButtonVisibility() => addOrRemoveButton.Alpha = IsHovered || beatmapInCollection ? 1 : 0; + protected override void OnSelectChange() + { + base.OnSelectChange(); + updateButtonVisibility(); + } + + private void updateButtonVisibility() => addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0; private void addOrRemove() { From c2da3d9c84edfb98b6ca09efa1d9505267e3ceb0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 14:36:38 +0900 Subject: [PATCH 3107/6909] Fix button input and tests --- .../SongSelect/TestSceneFilterControl.cs | 67 +++++++------------ .../Select/CollectionFilterDropdown.cs | 14 ++-- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index dea1c4b9b4..65b554b27b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -109,11 +109,7 @@ namespace osu.Game.Tests.Visual.SongSelect dropdown.Current.Value = dropdown.ItemSource.ElementAt(1); }); - AddStep("expand header", () => - { - InputManager.MoveMouseTo(control.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); + addExpandHeaderStep(); AddStep("change name", () => collectionManager.Collections[0].Name.Value = "First"); @@ -124,30 +120,24 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestAllBeatmapFilterDoesNotHaveAddButton() { - AddAssert("'All beatmaps' filter does not have add button", () => !getCollectionDropdownItems().First().ChildrenOfType().Single().IsPresent); + addExpandHeaderStep(); + AddStep("hover all beatmaps", () => InputManager.MoveMouseTo(getAddOrRemoveButton(0))); + AddAssert("'All beatmaps' filter does not have add button", () => !getAddOrRemoveButton(0).IsPresent); } [Test] public void TestCollectionFilterHasAddButton() { - AddStep("expand header", () => - { - InputManager.MoveMouseTo(control.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); - + addExpandHeaderStep(); AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); - AddAssert("collection has add button", () => !getAddOrRemoveButton(0).IsPresent); + AddStep("hover collection", () => InputManager.MoveMouseTo(getAddOrRemoveButton(1))); + AddAssert("collection has add button", () => getAddOrRemoveButton(1).IsPresent); } [Test] public void TestButtonDisabledAndEnabledWithBeatmapChanges() { - AddStep("expand header", () => - { - InputManager.MoveMouseTo(control.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); + addExpandHeaderStep(); AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); @@ -161,45 +151,37 @@ namespace osu.Game.Tests.Visual.SongSelect [Test] public void TestButtonChangesWhenAddedAndRemovedFromCollection() { - AddStep("expand header", () => - { - InputManager.MoveMouseTo(control.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); + addExpandHeaderStep(); AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusCircle)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); AddStep("add beatmap to collection", () => collectionManager.Collections[0].Beatmaps.Add(Beatmap.Value.BeatmapInfo)); - AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusCircle)); + AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare)); AddStep("remove beatmap from collection", () => collectionManager.Collections[0].Beatmaps.Clear()); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusCircle)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); } [Test] public void TestButtonAddsAndRemovesBeatmap() { - AddStep("expand header", () => - { - InputManager.MoveMouseTo(control.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); + addExpandHeaderStep(); AddStep("select available beatmap", () => Beatmap.Value = beatmapManager.GetWorkingBeatmap(beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0])); AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusCircle)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); addClickAddOrRemoveButtonStep(1); AddAssert("collection contains beatmap", () => collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo)); - AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusCircle)); + AddAssert("button is minus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.MinusSquare)); addClickAddOrRemoveButtonStep(1); AddAssert("collection does not contain beatmap", () => !collectionManager.Collections[0].Beatmaps.Contains(Beatmap.Value.BeatmapInfo)); - AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusCircle)); + AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); } private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) @@ -214,14 +196,17 @@ namespace osu.Game.Tests.Visual.SongSelect private IconButton getAddOrRemoveButton(int index) => getCollectionDropdownItems().ElementAt(index).ChildrenOfType().Single(); - private void addClickAddOrRemoveButtonStep(int index) + private void addExpandHeaderStep() => AddStep("expand header", () => { - AddStep("click add or remove button", () => - { - InputManager.MoveMouseTo(getAddOrRemoveButton(index)); - InputManager.Click(MouseButton.Left); - }); - } + InputManager.MoveMouseTo(control.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + private void addClickAddOrRemoveButtonStep(int index) => AddStep("click add or remove button", () => + { + InputManager.MoveMouseTo(getAddOrRemoveButton(index)); + InputManager.Click(MouseButton.Left); + }); private IEnumerable.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems() => control.ChildrenOfType().Single().ChildrenOfType.DropdownMenu.DrawableDropdownMenuItem>(); diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index c4b98fa854..4e9e12fcaf 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -184,9 +184,7 @@ namespace osu.Game.Screens.Select Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, X = -OsuScrollContainer.SCROLL_BAR_HEIGHT, - Scale = new Vector2(0.7f), - AlwaysPresent = true, - Alpha = collectionBeatmaps == null ? 0 : 1, + Scale = new Vector2(0.65f), Action = addOrRemove, }); } @@ -204,6 +202,8 @@ namespace osu.Game.Screens.Select // Although the DrawableMenuItem binds to value changes of the item's text, the item is an internal implementation detail of Dropdown that has no knowledge // of the underlying CollectionFilter value and its accompanying name, so the real name has to be copied here. Without this, the collection name wouldn't update when changed. collectionName.BindValueChanged(name => content.Text = name.NewValue, true); + + updateButtonVisibility(); } protected override bool OnHover(HoverEvent e) @@ -237,7 +237,13 @@ namespace osu.Game.Screens.Select updateButtonVisibility(); } - private void updateButtonVisibility() => addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0; + private void updateButtonVisibility() + { + if (collectionBeatmaps == null) + addOrRemoveButton.Alpha = 0; + else + addOrRemoveButton.Alpha = IsHovered || IsPreSelected || beatmapInCollection ? 1 : 0; + } private void addOrRemove() { From 17e8171827c7b8150d376683491ded9c59b5264d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 14:38:25 +0900 Subject: [PATCH 3108/6909] Don't prompt to remove empty collection --- osu.Game/Collections/DeleteCollectionDialog.cs | 9 +++------ osu.Game/Collections/DrawableCollectionListItem.cs | 12 +++++++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index f2b8de7c1e..8c8c897146 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.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.Framework.Allocation; +using System; using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; @@ -9,10 +9,7 @@ namespace osu.Game.Collections { public class DeleteCollectionDialog : PopupDialog { - [Resolved] - private BeatmapCollectionManager collectionManager { get; set; } - - public DeleteCollectionDialog(BeatmapCollection collection) + public DeleteCollectionDialog(BeatmapCollection collection, Action deleteAction) { HeaderText = "Confirm deletion of"; BodyText = collection.Name.Value; @@ -24,7 +21,7 @@ namespace osu.Game.Collections new PopupDialogOkButton { Text = @"Yes. Go for it.", - Action = () => collectionManager.Collections.Remove(collection) + Action = deleteAction }, new PopupDialogCancelButton { diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 9b7505f7c3..a1fc55556e 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -95,6 +95,9 @@ namespace osu.Game.Collections [Resolved(CanBeNull = true)] private DialogOverlay dialogOverlay { get; set; } + [Resolved] + private BeatmapCollectionManager collectionManager { get; set; } + private readonly BeatmapCollection collection; private Drawable background; @@ -146,9 +149,16 @@ namespace osu.Game.Collections protected override bool OnClick(ClickEvent e) { background.FlashColour(Color4.White, 150); - dialogOverlay?.Push(new DeleteCollectionDialog(collection)); + + if (collection.Beatmaps.Count == 0) + deleteCollection(); + else + dialogOverlay?.Push(new DeleteCollectionDialog(collection, deleteCollection)); + return true; } + + private void deleteCollection() => collectionManager.Collections.Remove(collection); } } } From 1260e30cde06611ca403a972a54403879c338223 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 16:36:36 +0900 Subject: [PATCH 3109/6909] Make ShowDragHandle into a bindable --- .../OsuRearrangeableListContainer.cs | 4 ++-- .../Containers/OsuRearrangeableListItem.cs | 20 ++++++++++--------- .../Screens/Multi/DrawableRoomPlaylistItem.cs | 5 ++--- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs b/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs index 47aed1c500..1048fd094c 100644 --- a/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs +++ b/osu.Game/Graphics/Containers/OsuRearrangeableListContainer.cs @@ -12,13 +12,13 @@ namespace osu.Game.Graphics.Containers /// /// Whether any item is currently being dragged. Used to hide other items' drag handles. /// - private readonly BindableBool playlistDragActive = new BindableBool(); + protected readonly BindableBool DragActive = new BindableBool(); protected override ScrollContainer CreateScrollContainer() => new OsuScrollContainer(); protected sealed override RearrangeableListItem CreateDrawable(TModel item) => CreateOsuDrawable(item).With(d => { - d.PlaylistDragActive.BindTo(playlistDragActive); + d.DragActive.BindTo(DragActive); }); protected abstract OsuRearrangeableListItem CreateOsuDrawable(TModel item); diff --git a/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs b/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs index 29553954fe..9cdcb19a81 100644 --- a/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs +++ b/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs @@ -19,7 +19,7 @@ namespace osu.Game.Graphics.Containers /// /// Whether any item is currently being dragged. Used to hide other items' drag handles. /// - public readonly BindableBool PlaylistDragActive = new BindableBool(); + public readonly BindableBool DragActive = new BindableBool(); private Color4 handleColour = Color4.White; @@ -44,8 +44,9 @@ namespace osu.Game.Graphics.Containers /// /// Whether the drag handle should be shown. /// - protected virtual bool ShowDragHandle => true; + protected readonly Bindable ShowDragHandle = new Bindable(); + private Container handleContainer; private PlaylistItemHandle handle; protected OsuRearrangeableListItem(TModel item) @@ -58,8 +59,6 @@ namespace osu.Game.Graphics.Containers [BackgroundDependencyLoader] private void load() { - Container handleContainer; - InternalChild = new GridContainer { RelativeSizeAxes = Axes.X, @@ -88,9 +87,12 @@ namespace osu.Game.Graphics.Containers ColumnDimensions = new[] { new Dimension(GridSizeMode.AutoSize) }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize) } }; + } - if (!ShowDragHandle) - handleContainer.Alpha = 0; + protected override void LoadComplete() + { + base.LoadComplete(); + ShowDragHandle.BindValueChanged(show => handleContainer.Alpha = show.NewValue ? 1 : 0, true); } protected override bool OnDragStart(DragStartEvent e) @@ -98,13 +100,13 @@ namespace osu.Game.Graphics.Containers if (!base.OnDragStart(e)) return false; - PlaylistDragActive.Value = true; + DragActive.Value = true; return true; } protected override void OnDragEnd(DragEndEvent e) { - PlaylistDragActive.Value = false; + DragActive.Value = false; base.OnDragEnd(e); } @@ -112,7 +114,7 @@ namespace osu.Game.Graphics.Containers protected override bool OnHover(HoverEvent e) { - handle.UpdateHoverState(IsDragged || !PlaylistDragActive.Value); + handle.UpdateHoverState(IsDragged || !DragActive.Value); return base.OnHover(e); } diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index c0892235f2..b007e0349d 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -37,8 +37,6 @@ namespace osu.Game.Screens.Multi public readonly Bindable SelectedItem = new Bindable(); - protected override bool ShowDragHandle => allowEdit; - private Container maskingContainer; private Container difficultyIconContainer; private LinkFlowContainer beatmapText; @@ -63,12 +61,13 @@ namespace osu.Game.Screens.Multi // TODO: edit support should be moved out into a derived class this.allowEdit = allowEdit; - this.allowSelection = allowSelection; beatmap.BindTo(item.Beatmap); ruleset.BindTo(item.Ruleset); requiredMods.BindTo(item.RequiredMods); + + ShowDragHandle.Value = allowEdit; } [BackgroundDependencyLoader] From 0bf6bfe5ee4569bc7b2c0e83e92dd659d33299fc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 16:43:07 +0900 Subject: [PATCH 3110/6909] Create a new collection via a placeholder item --- .../Collections/DrawableCollectionList.cs | 97 +++++++++++++++++-- .../Collections/DrawableCollectionListItem.cs | 94 ++++++++++++++---- .../Collections/ManageCollectionsDialog.cs | 19 +--- 3 files changed, 163 insertions(+), 47 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index e8bde9066f..f4b5a89b3e 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.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.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; @@ -10,19 +12,94 @@ namespace osu.Game.Collections { public class DrawableCollectionList : OsuRearrangeableListContainer { - protected override ScrollContainer CreateScrollContainer() => base.CreateScrollContainer().With(d => - { - d.ScrollbarVisible = false; - d.Padding = new MarginPadding(10); - }); + private Scroll scroll; - protected override FillFlowContainer> CreateListFillFlowContainer() => new FillFlowContainer> + protected override ScrollContainer CreateScrollContainer() => scroll = new Scroll(); + + protected override FillFlowContainer> CreateListFillFlowContainer() => new Flow { - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, - Spacing = new Vector2(0, 5) + DragActive = { BindTarget = DragActive } }; - protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapCollection item) => new DrawableCollectionListItem(item); + protected override OsuRearrangeableListItem CreateOsuDrawable(BeatmapCollection item) + { + if (item == scroll.PlaceholderItem.Model) + return scroll.ReplacePlaceholder(); + + return new DrawableCollectionListItem(item, true); + } + + private class Scroll : OsuScrollContainer + { + public DrawableCollectionListItem PlaceholderItem { get; private set; } + + protected override Container Content => content; + private readonly Container content; + + private readonly Container placeholderContainer; + + public Scroll() + { + ScrollbarVisible = false; + Padding = new MarginPadding(10); + + base.Content.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + LayoutDuration = 200, + LayoutEasing = Easing.OutQuint, + Children = new Drawable[] + { + content = new Container { RelativeSizeAxes = Axes.X }, + placeholderContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + }); + + ReplacePlaceholder(); + } + + protected override void Update() + { + base.Update(); + + // AutoSizeAxes cannot be used as the height should represent the post-layout-transform height at all times, so that the placeholder doesn't bounce around. + content.Height = ((Flow)Child).Children.Sum(c => c.DrawHeight + 5); + } + + /// + /// Replaces the current with a new one, and returns the previous. + /// + public DrawableCollectionListItem ReplacePlaceholder() + { + var previous = PlaceholderItem; + + placeholderContainer.Clear(false); + placeholderContainer.Add(PlaceholderItem = new DrawableCollectionListItem(new BeatmapCollection(), false)); + + return previous; + } + } + + private class Flow : FillFlowContainer> + { + public readonly IBindable DragActive = new Bindable(); + + public Flow() + { + Spacing = new Vector2(0, 5); + LayoutEasing = Easing.OutQuint; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + DragActive.BindValueChanged(active => LayoutDuration = active.NewValue ? 200 : 0); + } + } } } diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index a1fc55556e..90d5bae223 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -3,6 +3,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; @@ -21,20 +22,33 @@ namespace osu.Game.Collections public class DrawableCollectionListItem : OsuRearrangeableListItem { private const float item_height = 35; - private const float button_width = item_height * 0.75f; - public DrawableCollectionListItem(BeatmapCollection item) + private readonly Bindable isCreated = new Bindable(); + + public DrawableCollectionListItem(BeatmapCollection item, bool isCreated) : base(item) { + this.isCreated.Value = isCreated; + + ShowDragHandle.BindTo(this.isCreated); } - protected override Drawable CreateContent() => new ItemContent(Model); + protected override Drawable CreateContent() => new ItemContent(Model) + { + IsCreated = { BindTarget = isCreated } + }; private class ItemContent : CircularContainer { + public readonly Bindable IsCreated = new Bindable(); + + private readonly IBindable collectionName; private readonly BeatmapCollection collection; + [Resolved] + private BeatmapCollectionManager collectionManager { get; set; } + private ItemTextBox textBox; public ItemContent(BeatmapCollection collection) @@ -44,6 +58,8 @@ namespace osu.Game.Collections RelativeSizeAxes = Axes.X; Height = item_height; Masking = true; + + collectionName = collection.Name.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -55,6 +71,7 @@ namespace osu.Game.Collections { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + IsCreated = { BindTarget = IsCreated }, IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v) }, new Container @@ -68,12 +85,37 @@ namespace osu.Game.Collections RelativeSizeAxes = Axes.Both, Size = Vector2.One, CornerRadius = item_height / 2, - Current = collection.Name + Current = collection.Name, + PlaceholderText = IsCreated.Value ? string.Empty : "Create a new collection" }, } }, }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + collectionName.BindValueChanged(_ => createNewCollection(), true); + } + + private void createNewCollection() + { + if (IsCreated.Value) + return; + + if (string.IsNullOrEmpty(collectionName.Value)) + return; + + // Add the new collection and disable our placeholder. If all text is removed, the placeholder should not show back again. + collectionManager.Collections.Add(collection); + textBox.PlaceholderText = string.Empty; + + // When this item changes from placeholder to non-placeholder (via changing containers), its textbox will lose focus, so it needs to be re-focused. + Schedule(() => GetContainingInputManager().ChangeFocus(textBox)); + + IsCreated.Value = true; + } } private class ItemTextBox : OsuTextBox @@ -90,6 +132,8 @@ namespace osu.Game.Collections public class DeleteButton : CompositeDrawable { + public readonly IBindable IsCreated = new Bindable(); + public Func IsTextBoxHovered; [Resolved(CanBeNull = true)] @@ -100,6 +144,7 @@ namespace osu.Game.Collections private readonly BeatmapCollection collection; + private Drawable fadeContainer; private Drawable background; public DeleteButton(BeatmapCollection collection) @@ -108,42 +153,51 @@ namespace osu.Game.Collections RelativeSizeAxes = Axes.Y; Width = button_width + item_height / 2; // add corner radius to cover with fill - - Alpha = 0.1f; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - InternalChildren = new[] + InternalChild = fadeContainer = new Container { - background = new Box + RelativeSizeAxes = Axes.Both, + Alpha = 0.1f, + Children = new[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.Red - }, - new SpriteIcon - { - Anchor = Anchor.CentreRight, - Origin = Anchor.Centre, - X = -button_width * 0.6f, - Size = new Vector2(10), - Icon = FontAwesome.Solid.Trash + background = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Red + }, + new SpriteIcon + { + Anchor = Anchor.CentreRight, + Origin = Anchor.Centre, + X = -button_width * 0.6f, + Size = new Vector2(10), + Icon = FontAwesome.Solid.Trash + } } }; } + protected override void LoadComplete() + { + base.LoadComplete(); + IsCreated.BindValueChanged(created => Alpha = created.NewValue ? 1 : 0, true); + } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => base.ReceivePositionalInputAt(screenSpacePos) && !IsTextBoxHovered(screenSpacePos); protected override bool OnHover(HoverEvent e) { - this.FadeTo(1f, 100, Easing.Out); + fadeContainer.FadeTo(1f, 100, Easing.Out); return false; } protected override void OnHoverLost(HoverLostEvent e) { - this.FadeTo(0.1f, 100); + fadeContainer.FadeTo(0.1f, 100); } protected override bool OnClick(ClickEvent e) diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 036a745913..8f8ac9542c 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osuTK; namespace osu.Game.Collections @@ -51,9 +50,7 @@ namespace osu.Game.Collections RelativeSizeAxes = Axes.Both, RowDimensions = new[] { - new Dimension(GridSizeMode.Absolute, 50), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 50), + new Dimension(GridSizeMode.AutoSize), }, Content = new[] { @@ -65,6 +62,7 @@ namespace osu.Game.Collections Origin = Anchor.Centre, Text = "Manage collections", Font = OsuFont.GetFont(size: 30), + Padding = new MarginPadding { Vertical = 10 }, } }, new Drawable[] @@ -87,19 +85,6 @@ namespace osu.Game.Collections } } }, - new Drawable[] - { - new OsuButton - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = Vector2.One, - Padding = new MarginPadding(10), - Text = "Create new collection", - Action = () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "My new collection" } }) - }, - }, } } } From 38ade433a62b449be687ba3409cfd854d4d04876 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 16:50:51 +0900 Subject: [PATCH 3111/6909] Add some xmldocs --- osu.Game/Collections/DrawableCollectionList.cs | 17 +++++++++++++++++ .../Collections/DrawableCollectionListItem.cs | 11 +++++++++++ 2 files changed, 28 insertions(+) diff --git a/osu.Game/Collections/DrawableCollectionList.cs b/osu.Game/Collections/DrawableCollectionList.cs index f4b5a89b3e..3c664a11d9 100644 --- a/osu.Game/Collections/DrawableCollectionList.cs +++ b/osu.Game/Collections/DrawableCollectionList.cs @@ -10,6 +10,9 @@ using osuTK; namespace osu.Game.Collections { + /// + /// Visualises a list of s. + /// public class DrawableCollectionList : OsuRearrangeableListContainer { private Scroll scroll; @@ -29,8 +32,18 @@ namespace osu.Game.Collections return new DrawableCollectionListItem(item, true); } + /// + /// The scroll container for this . + /// Contains the main flow of and attaches a placeholder item to the end of the list. + /// + /// + /// Use to transfer the placeholder into the main list. + /// private class Scroll : OsuScrollContainer { + /// + /// The currently-displayed placeholder item. + /// public DrawableCollectionListItem PlaceholderItem { get; private set; } protected override Container Content => content; @@ -74,6 +87,7 @@ namespace osu.Game.Collections /// /// Replaces the current with a new one, and returns the previous. /// + /// The current . public DrawableCollectionListItem ReplacePlaceholder() { var previous = PlaceholderItem; @@ -85,6 +99,9 @@ namespace osu.Game.Collections } } + /// + /// The flow of . Disables layout easing unless a drag is in progress. + /// private class Flow : FillFlowContainer> { public readonly IBindable DragActive = new Bindable(); diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 90d5bae223..489382ec9e 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -19,6 +19,9 @@ using osuTK.Graphics; namespace osu.Game.Collections { + /// + /// Visualises a inside a . + /// public class DrawableCollectionListItem : OsuRearrangeableListItem { private const float item_height = 35; @@ -26,6 +29,11 @@ namespace osu.Game.Collections private readonly Bindable isCreated = new Bindable(); + /// + /// Creates a new . + /// + /// The . + /// Whether currently exists inside the . public DrawableCollectionListItem(BeatmapCollection item, bool isCreated) : base(item) { @@ -39,6 +47,9 @@ namespace osu.Game.Collections IsCreated = { BindTarget = isCreated } }; + /// + /// The main content of the . + /// private class ItemContent : CircularContainer { public readonly Bindable IsCreated = new Bindable(); From 2e40ff25f7228ed74aaf12fbd83ada10b4a0a917 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 17:05:31 +0900 Subject: [PATCH 3112/6909] Only pad textbox after collection is created --- osu.Game/Collections/DrawableCollectionListItem.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 489382ec9e..c67946977d 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -60,6 +60,7 @@ namespace osu.Game.Collections [Resolved] private BeatmapCollectionManager collectionManager { get; set; } + private Container textBoxPaddingContainer; private ItemTextBox textBox; public ItemContent(BeatmapCollection collection) @@ -85,7 +86,7 @@ namespace osu.Game.Collections IsCreated = { BindTarget = IsCreated }, IsTextBoxHovered = v => textBox.ReceivePositionalInputAt(v) }, - new Container + textBoxPaddingContainer = new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = button_width }, @@ -107,7 +108,9 @@ namespace osu.Game.Collections protected override void LoadComplete() { base.LoadComplete(); + collectionName.BindValueChanged(_ => createNewCollection(), true); + IsCreated.BindValueChanged(created => textBoxPaddingContainer.Padding = new MarginPadding { Right = created.NewValue ? button_width : 0 }, true); } private void createNewCollection() From bee450ae1ecba24a161e681e53e53f94462428fd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 17:05:43 +0900 Subject: [PATCH 3113/6909] Fix tests/add placeholder item tests --- .../TestSceneManageCollectionsDialog.cs | 80 ++++++++++++++----- .../SongSelect/TestSceneFilterControl.cs | 14 ++-- .../Collections/DrawableCollectionListItem.cs | 5 ++ 3 files changed, 72 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index fdaded6a5c..0c57c27911 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -7,11 +7,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Game.Beatmaps; using osu.Game.Collections; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; +using osu.Game.Rulesets; +using osu.Game.Tests.Resources; using osuTK; using osuTK.Input; @@ -25,6 +28,9 @@ namespace osu.Game.Tests.Visual.Collections private readonly DialogOverlay dialogOverlay; private readonly BeatmapCollectionManager manager; + private RulesetStore rulesets; + private BeatmapManager beatmapManager; + private ManageCollectionsDialog dialog; public TestSceneManageCollectionsDialog() @@ -37,6 +43,15 @@ namespace osu.Game.Tests.Visual.Collections }); } + [BackgroundDependencyLoader] + private void load(GameHost host) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); + + beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -65,6 +80,12 @@ namespace osu.Game.Tests.Visual.Collections AddStep("hide dialog", () => dialog.Hide()); } + [Test] + public void TestLastItemIsPlaceholder() + { + AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType().Last().Model)); + } + [Test] public void TestAddCollectionExternal() { @@ -78,23 +99,34 @@ namespace osu.Game.Tests.Visual.Collections } [Test] - public void TestAddCollectionViaButton() + public void TestFocusPlaceholderDoesNotCreateCollection() { - AddStep("press new collection button", () => + AddStep("focus placeholder", () => { - InputManager.MoveMouseTo(dialog.ChildrenOfType().Single()); + InputManager.MoveMouseTo(dialog.ChildrenOfType().Last()); InputManager.Click(MouseButton.Left); }); + assertCollectionCount(0); + } + + [Test] + public void TestAddCollectionViaPlaceholder() + { + DrawableCollectionListItem placeholderItem = null; + + AddStep("focus placeholder", () => + { + InputManager.MoveMouseTo(placeholderItem = dialog.ChildrenOfType().Last()); + InputManager.Click(MouseButton.Left); + }); + + // Done directly via the collection since InputManager methods cannot add text to textbox... + AddStep("change collection name", () => placeholderItem.Model.Name.Value = "a"); assertCollectionCount(1); + AddAssert("collection now exists", () => manager.Collections.Contains(placeholderItem.Model)); - AddStep("press again", () => - { - InputManager.MoveMouseTo(dialog.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); - - assertCollectionCount(2); + AddAssert("last item is placeholder", () => !manager.Collections.Contains(dialog.ChildrenOfType().Last().Model)); } [Test] @@ -117,7 +149,7 @@ namespace osu.Game.Tests.Visual.Collections AddStep("add two collections", () => manager.Collections.AddRange(new[] { new BeatmapCollection { Name = { Value = "1" } }, - new BeatmapCollection { Name = { Value = "2" } }, + new BeatmapCollection { Name = { Value = "2" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } }, })); assertCollectionCount(2); @@ -128,6 +160,16 @@ namespace osu.Game.Tests.Visual.Collections InputManager.Click(MouseButton.Left); }); + AddAssert("dialog not displayed", () => dialogOverlay.CurrentDialog == null); + assertCollectionCount(1); + assertCollectionName(0, "2"); + + AddStep("click first delete button", () => + { + InputManager.MoveMouseTo(dialog.ChildrenOfType().First(), new Vector2(5, 0)); + InputManager.Click(MouseButton.Left); + }); + AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is DeleteCollectionDialog); AddStep("click confirmation", () => { @@ -135,8 +177,7 @@ namespace osu.Game.Tests.Visual.Collections InputManager.Click(MouseButton.Left); }); - assertCollectionCount(1); - assertCollectionName(0, "2"); + assertCollectionCount(0); } [Test] @@ -144,11 +185,10 @@ namespace osu.Game.Tests.Visual.Collections { AddStep("add two collections", () => manager.Collections.AddRange(new[] { - new BeatmapCollection { Name = { Value = "1" } }, - new BeatmapCollection { Name = { Value = "2" } }, + new BeatmapCollection { Name = { Value = "1" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } }, })); - assertCollectionCount(2); + assertCollectionCount(1); AddStep("click first delete button", () => { @@ -157,13 +197,13 @@ namespace osu.Game.Tests.Visual.Collections }); AddAssert("dialog displayed", () => dialogOverlay.CurrentDialog is DeleteCollectionDialog); - AddStep("click confirmation", () => + AddStep("click cancellation", () => { InputManager.MoveMouseTo(dialogOverlay.CurrentDialog.ChildrenOfType().Last()); InputManager.Click(MouseButton.Left); }); - assertCollectionCount(2); + assertCollectionCount(1); } [Test] @@ -196,7 +236,7 @@ namespace osu.Game.Tests.Visual.Collections } private void assertCollectionCount(int count) - => AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType().Count() == count); + => AddUntilStep($"{count} collections shown", () => dialog.ChildrenOfType().Count(i => i.IsCreated.Value) == count); private void assertCollectionName(int index, string name) => AddUntilStep($"item {index + 1} has correct name", () => dialog.ChildrenOfType().ElementAt(index).ChildrenOfType().First().Text == name); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 65b554b27b..4606c7b0c3 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -42,13 +42,6 @@ namespace osu.Game.Tests.Visual.SongSelect }); } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(collectionManager); - return dependencies; - } - [BackgroundDependencyLoader] private void load(GameHost host) { @@ -58,6 +51,13 @@ namespace osu.Game.Tests.Visual.SongSelect beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.Cache(collectionManager); + return dependencies; + } + [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index c67946977d..a7075c3179 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -27,6 +27,11 @@ namespace osu.Game.Collections private const float item_height = 35; private const float button_width = item_height * 0.75f; + /// + /// Whether the currently exists inside the . + /// + public IBindable IsCreated => isCreated; + private readonly Bindable isCreated = new Bindable(); /// From d2650fc1a05e012a58a2283a59ce4dfdc6e634e3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 17:12:58 +0900 Subject: [PATCH 3114/6909] Add count to deletion dialog --- osu.Game/Collections/DeleteCollectionDialog.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Collections/DeleteCollectionDialog.cs b/osu.Game/Collections/DeleteCollectionDialog.cs index 8c8c897146..e5a2f6fb81 100644 --- a/osu.Game/Collections/DeleteCollectionDialog.cs +++ b/osu.Game/Collections/DeleteCollectionDialog.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Humanizer; using osu.Framework.Graphics.Sprites; using osu.Game.Overlays.Dialog; @@ -12,7 +13,7 @@ namespace osu.Game.Collections public DeleteCollectionDialog(BeatmapCollection collection, Action deleteAction) { HeaderText = "Confirm deletion of"; - BodyText = collection.Name.Value; + BodyText = $"{collection.Name.Value} ({"beatmap".ToQuantity(collection.Beatmaps.Count)})"; Icon = FontAwesome.Regular.TrashAlt; From 4737add00bc99290652f585bf85a6acbc4d70c55 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 17:21:29 +0900 Subject: [PATCH 3115/6909] Add close button to dialog --- .../Collections/ManageCollectionsDialog.cs | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index 8f8ac9542c..f6964191a1 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -5,9 +5,11 @@ 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.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osuTK; namespace osu.Game.Collections @@ -56,13 +58,31 @@ namespace osu.Game.Collections { new Drawable[] { - new OsuSpriteText + new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Text = "Manage collections", - Font = OsuFont.GetFont(size: 30), - Padding = new MarginPadding { Vertical = 10 }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Text = "Manage collections", + Font = OsuFont.GetFont(size: 30), + Padding = new MarginPadding { Vertical = 10 }, + }, + new IconButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Icon = FontAwesome.Solid.Times, + Colour = colours.GreySeafoamDarker, + Scale = new Vector2(0.8f), + X = -10, + Action = () => State.Value = Visibility.Hidden + } + } } }, new Drawable[] From c3123bf11712937fed58039477442717e7e8076b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 17:22:59 +0900 Subject: [PATCH 3116/6909] Rename drag blueprint selection method for discoverability --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index fcff672045..865e225645 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Edit.Compose.Components AddRangeInternal(new[] { - DragBox = CreateDragBox(select), + DragBox = CreateDragBox(selectBlueprintsFromDragRectangle), selectionHandler, SelectionBlueprints = CreateSelectionBlueprintContainer(), selectionHandler.CreateProxy(), @@ -326,7 +326,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Select all masks in a given rectangle selection area. /// /// The rectangle to perform a selection on in screen-space coordinates. - private void select(RectangleF rect) + private void selectBlueprintsFromDragRectangle(RectangleF rect) { foreach (var blueprint in SelectionBlueprints) { From 06328e00001315d83d70cfabef4a350d434d8399 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 17:58:56 +0900 Subject: [PATCH 3117/6909] Add import/deletion progress notifications --- .../Collections/BeatmapCollectionManager.cs | 56 ++++++++++++++++++- osu.Game/OsuGame.cs | 1 + .../Sections/Maintenance/GeneralSettings.cs | 2 +- 3 files changed, 55 insertions(+), 4 deletions(-) diff --git a/osu.Game/Collections/BeatmapCollectionManager.cs b/osu.Game/Collections/BeatmapCollectionManager.cs index a553ac632e..6a5ed6bbbc 100644 --- a/osu.Game/Collections/BeatmapCollectionManager.cs +++ b/osu.Game/Collections/BeatmapCollectionManager.cs @@ -57,6 +57,10 @@ namespace osu.Game.Collections c.Changed += backgroundSave; Collections.CollectionChanged += (_, __) => backgroundSave(); } + /// + /// Set an endpoint for notifications to be posted to. + /// + public Action PostNotification { protected get; set; } /// /// Set a storage with access to an osu-stable install for import purposes. @@ -93,9 +97,25 @@ namespace osu.Game.Collections }); } - public async Task Import(Stream stream) => await Task.Run(async () => + public async Task Import(Stream stream) { - var collection = readCollections(stream); + var notification = new ProgressNotification + { + State = ProgressNotificationState.Active, + Text = "Collections import is initialising..." + }; + + PostNotification?.Invoke(notification); + + await import(stream, notification); + } + + private async Task import(Stream stream, ProgressNotification notification = null) => await Task.Run(async () => + { + if (notification != null) + notification.Progress = 0; + + var collection = readCollections(stream, notification); bool importCompleted = false; Schedule(() => @@ -106,6 +126,12 @@ namespace osu.Game.Collections while (!IsDisposed && !importCompleted) await Task.Delay(10); + + if (notification != null) + { + notification.CompletionText = $"Imported {collection.Count} collections"; + notification.State = ProgressNotificationState.Completed; + } }); private void importCollections(List newCollections) @@ -124,8 +150,14 @@ namespace osu.Game.Collections } } - private List readCollections(Stream stream) + private List readCollections(Stream stream, ProgressNotification notification = null) { + if (notification != null) + { + notification.Text = "Reading collections..."; + notification.Progress = 0; + } + var result = new List(); try @@ -139,11 +171,17 @@ namespace osu.Game.Collections for (int i = 0; i < collectionCount; i++) { + if (notification?.CancellationToken.IsCancellationRequested == true) + return result; + var collection = new BeatmapCollection { Name = { Value = sr.ReadString() } }; int mapCount = sr.ReadInt32(); for (int j = 0; j < mapCount; j++) { + if (notification?.CancellationToken.IsCancellationRequested == true) + return result; + string checksum = sr.ReadString(); var beatmap = beatmaps.QueryBeatmap(b => b.MD5Hash == checksum); @@ -151,6 +189,12 @@ namespace osu.Game.Collections collection.Beatmaps.Add(beatmap); } + if (notification != null) + { + notification.Text = $"Imported {i + 1} of {collectionCount} collections"; + notification.Progress = (float)(i + 1) / collectionCount; + } + result.Add(collection); } } @@ -163,6 +207,12 @@ namespace osu.Game.Collections return result; } + public void DeleteAll() + { + Collections.Clear(); + PostNotification?.Invoke(new SimpleNotification { Text = "Deleted all collections!" }); + } + private readonly object saveLock = new object(); private int lastSave; private int saveFailures; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 8434ee11fa..33a353742d 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -537,6 +537,7 @@ namespace osu.Game ScoreManager.GetStableStorage = GetStorageForStableInstall; ScoreManager.PresentImport = items => PresentScore(items.First()); + CollectionManager.PostNotification = n => notifications.Post(n); CollectionManager.GetStableStorage = GetStorageForStableInstall; Container logoContainer; diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 74f9920ae0..30fd5921eb 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance Text = "Delete ALL collections", Action = () => { - dialogOverlay?.Push(new DeleteAllBeatmapsDialog(() => collectionManager.Collections.Clear())); + dialogOverlay?.Push(new DeleteAllBeatmapsDialog(collectionManager.DeleteAll)); } }); From 070704cba719a124bd48c6b5a9133ddaae157e92 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 17:59:38 +0900 Subject: [PATCH 3118/6909] Asyncify initial load --- .../Collections/BeatmapCollectionManager.cs | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/osu.Game/Collections/BeatmapCollectionManager.cs b/osu.Game/Collections/BeatmapCollectionManager.cs index 6a5ed6bbbc..e4fc4c377b 100644 --- a/osu.Game/Collections/BeatmapCollectionManager.cs +++ b/osu.Game/Collections/BeatmapCollectionManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.IO; using System.Linq; using System.Threading; @@ -15,6 +16,7 @@ using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.IO.Legacy; +using osu.Game.Overlays.Notifications; namespace osu.Game.Collections { @@ -46,17 +48,46 @@ namespace osu.Game.Collections [BackgroundDependencyLoader] private void load() + { + Collections.CollectionChanged += collectionsChanged; + loadDatabase(); + } + + private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (var c in e.NewItems.Cast()) + c.Changed += backgroundSave; + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var c in e.OldItems.Cast()) + c.Changed -= backgroundSave; + break; + + case NotifyCollectionChangedAction.Replace: + foreach (var c in e.OldItems.Cast()) + c.Changed -= backgroundSave; + + foreach (var c in e.NewItems.Cast()) + c.Changed += backgroundSave; + break; + } + + backgroundSave(); + } + + private void loadDatabase() => Task.Run(async () => { if (storage.Exists(database_name)) { using (var stream = storage.GetStream(database_name)) - importCollections(readCollections(stream)); + await import(stream); } + }); - foreach (var c in Collections) - c.Changed += backgroundSave; - Collections.CollectionChanged += (_, __) => backgroundSave(); - } /// /// Set an endpoint for notifications to be posted to. /// From b1110e5e3a1778a4cf5075728194e524c468f0c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 18:10:14 +0900 Subject: [PATCH 3119/6909] Rename class to match derived class --- osu.Game/OsuGame.cs | 2 +- .../Music/{MusicActionHandler.cs => MusicKeyBindingHandler.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game/Overlays/Music/{MusicActionHandler.cs => MusicKeyBindingHandler.cs} (96%) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a73469d836..b4e671d0b0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -648,7 +648,7 @@ namespace osu.Game chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; Add(externalLinkOpener = new ExternalLinkOpener()); - Add(new MusicActionHandler()); + Add(new MusicKeyBindingHandler()); // side overlays which cancel each other. var singleDisplaySideOverlays = new OverlayContainer[] { Settings, notifications }; diff --git a/osu.Game/Overlays/Music/MusicActionHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs similarity index 96% rename from osu.Game/Overlays/Music/MusicActionHandler.cs rename to osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index cd8548c1c0..78e6ba1381 100644 --- a/osu.Game/Overlays/Music/MusicActionHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Music /// /// Handles relating to music playback, and displays a via the cached accordingly. /// - public class MusicActionHandler : Component, IKeyBindingHandler + public class MusicKeyBindingHandler : Component, IKeyBindingHandler { [Resolved] private IBindable beatmap { get; set; } From a46be45a71b9b70ca1b70b68d54d33a625070a4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 18:12:03 +0900 Subject: [PATCH 3120/6909] Fix OSD occasionally display incorrect play/pause state --- osu.Game/Overlays/Music/MusicKeyBindingHandler.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index 78e6ba1381..277fb1a35d 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -33,9 +33,11 @@ namespace osu.Game.Overlays.Music switch (action) { case GlobalAction.MusicPlay: - if (musicController.TogglePause()) - onScreenDisplay?.Display(new MusicActionToast(musicController.IsPlaying ? "Play track" : "Pause track")); + // use previous state as TogglePause may not update the track's state immediately (state update is run on the audio thread see https://github.com/ppy/osu/issues/9880#issuecomment-674668842) + bool wasPlaying = musicController.IsPlaying; + if (musicController.TogglePause()) + onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track")); return true; case GlobalAction.MusicNext: From 14bf2ab936b38c16cccbac254293d28791002b33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 18:21:26 +0900 Subject: [PATCH 3121/6909] Fix grammar in xmldoc --- osu.Game/Overlays/Music/MusicKeyBindingHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index 78e6ba1381..02196348ae 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -12,7 +12,7 @@ using osu.Game.Overlays.OSD; namespace osu.Game.Overlays.Music { /// - /// Handles relating to music playback, and displays a via the cached accordingly. + /// Handles s related to music playback, and displays s via the global accordingly. /// public class MusicKeyBindingHandler : Component, IKeyBindingHandler { From f581df47c8edcf03771ae1e019323f0a23301995 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 18:25:09 +0900 Subject: [PATCH 3122/6909] Add "New collection..." item to dropdown --- .../SongSelect/TestSceneFilterControl.cs | 23 +++++++++++++++++++ osu.Game/Screens/Select/CollectionFilter.cs | 17 ++++++++++++++ .../Select/CollectionFilterDropdown.cs | 18 ++++++++++++--- 3 files changed, 55 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 4606c7b0c3..955fe04c8c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -184,6 +184,29 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("button is plus", () => getAddOrRemoveButton(1).Icon.Equals(FontAwesome.Solid.PlusSquare)); } + [Test] + public void TestNewCollectionFilterIsNotSelected() + { + addExpandHeaderStep(); + + AddStep("add collection", () => collectionManager.Collections.Add(new BeatmapCollection { Name = { Value = "1" } })); + AddStep("select collection", () => + { + InputManager.MoveMouseTo(getCollectionDropdownItems().ElementAt(1)); + InputManager.Click(MouseButton.Left); + }); + + addExpandHeaderStep(); + + AddStep("click manage collections filter", () => + { + InputManager.MoveMouseTo(getCollectionDropdownItems().Last()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.CollectionName.Value == "1"); + } + private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) => AddAssert($"collection dropdown header displays '{collectionName}'", () => shouldDisplay == (control.ChildrenOfType().Single().ChildrenOfType().First().Text == collectionName)); diff --git a/osu.Game/Screens/Select/CollectionFilter.cs b/osu.Game/Screens/Select/CollectionFilter.cs index 7628ed391e..9e36e3e089 100644 --- a/osu.Game/Screens/Select/CollectionFilter.cs +++ b/osu.Game/Screens/Select/CollectionFilter.cs @@ -45,4 +45,21 @@ namespace osu.Game.Screens.Select public virtual bool ContainsBeatmap(BeatmapInfo beatmap) => Collection?.Beatmaps.Any(b => b.Equals(beatmap)) ?? true; } + + public class AllBeatmapCollectionFilter : CollectionFilter + { + public AllBeatmapCollectionFilter() + : base(null) + { + } + } + + public class NewCollectionFilter : CollectionFilter + { + public NewCollectionFilter() + : base(null) + { + CollectionName.Value = "New collection..."; + } + } } diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index 4e9e12fcaf..5e5c684fe2 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -29,6 +29,9 @@ namespace osu.Game.Screens.Select private readonly IBindableList beatmaps = new BindableList(); private readonly BindableList filters = new BindableList(); + [Resolved(CanBeNull = true)] + private ManageCollectionsDialog manageCollectionsDialog { get; set; } + public CollectionFilterDropdown() { ItemSource = filters; @@ -57,10 +60,11 @@ namespace osu.Game.Screens.Select var selectedItem = SelectedItem?.Value?.Collection; filters.Clear(); - filters.Add(new CollectionFilter(null)); + filters.Add(new AllBeatmapCollectionFilter()); filters.AddRange(collections.Select(c => new CollectionFilter(c))); + filters.Add(new NewCollectionFilter()); - Current.Value = filters.SingleOrDefault(f => f.Collection == selectedItem) ?? filters[0]; + Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection == selectedItem) ?? filters[0]; } /// @@ -78,6 +82,14 @@ namespace osu.Game.Screens.Select beatmaps.BindTo(filter.NewValue.Collection.Beatmaps); beatmaps.CollectionChanged += filterBeatmapsChanged; + + // Never select the manage collection filter - rollback to the previous filter. + // This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value. + if (filter.NewValue is NewCollectionFilter) + { + Current.Value = filter.OldValue; + manageCollectionsDialog?.Show(); + } } /// @@ -90,7 +102,7 @@ namespace osu.Game.Screens.Select Current.TriggerChange(); } - protected override string GenerateItemText(CollectionFilter item) => item.Collection?.Name.Value ?? "All beatmaps"; + protected override string GenerateItemText(CollectionFilter item) => item.CollectionName.Value; protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader { From ad5d6117c76856237d2215a89f8c2f7c2ab7524d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 18:26:13 +0900 Subject: [PATCH 3123/6909] Remove unnecessary RunTask calls --- .../Overlays/Music/MusicKeyBindingHandler.cs | 7 ++----- osu.Game/Overlays/MusicController.cs | 17 ++++------------- 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index 02196348ae..f5968614cd 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -39,10 +39,7 @@ namespace osu.Game.Overlays.Music return true; case GlobalAction.MusicNext: - musicController.NextTrack(() => - { - onScreenDisplay?.Display(new MusicActionToast("Next track")); - }).RunTask(); + musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track"))); return true; @@ -59,7 +56,7 @@ namespace osu.Game.Overlays.Music onScreenDisplay?.Display(new MusicActionToast("Previous track")); break; } - }).RunTask(); + }); return true; } diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index a405be1b74..b568e4d02b 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -201,13 +201,8 @@ namespace osu.Game.Overlays /// /// Play the previous track or restart the current track if it's current time below . /// - /// - /// Invoked when the operation has been performed successfully. - /// The result isn't returned directly to the caller because - /// the operation is scheduled and isn't performed immediately. - /// - /// A of the operation. - public ScheduledDelegate PreviousTrack(Action onSuccess = null) => Schedule(() => + /// Invoked when the operation has been performed successfully. + public void PreviousTrack(Action onSuccess = null) => Schedule(() => { PreviousTrackResult res = prev(); if (res != PreviousTrackResult.None) @@ -248,13 +243,9 @@ namespace osu.Game.Overlays /// /// Play the next random or playlist track. /// - /// - /// Invoked when the operation has been performed successfully. - /// The result isn't returned directly to the caller because - /// the operation is scheduled and isn't performed immediately. - /// + /// Invoked when the operation has been performed successfully. /// A of the operation. - public ScheduledDelegate NextTrack(Action onSuccess = null) => Schedule(() => + public void NextTrack(Action onSuccess = null) => Schedule(() => { bool res = next(); if (res) From e1053c4b6f9376884786084e0d48a26962f6edcf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 18:36:11 +0900 Subject: [PATCH 3124/6909] Revert exposure changes to GlobalActionContainer --- .../Visual/Menus/TestSceneMusicActionHandling.cs | 13 ++++++++----- .../Visual/Navigation/OsuGameTestScene.cs | 3 --- osu.Game/OsuGameBase.cs | 11 ++++++----- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 9b8ba47992..4cad2b19d5 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -14,13 +15,15 @@ namespace osu.Game.Tests.Visual.Menus { public class TestSceneMusicActionHandling : OsuGameTestScene { + private GlobalActionContainer globalActionContainer => Game.ChildrenOfType().First(); + [Test] public void TestMusicPlayAction() { AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething()); - AddStep("toggle playback", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPlay)); + AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); AddAssert("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.IsUserPaused); - AddStep("toggle playback", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPlay)); + AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); AddAssert("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.IsUserPaused); } @@ -62,16 +65,16 @@ namespace osu.Game.Tests.Visual.Menus AddStep("seek track to 6 second", () => Game.MusicController.SeekTo(6000)); AddUntilStep("wait for current time to update", () => Game.MusicController.CurrentTrack.CurrentTime > 5000); - AddStep("press previous", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPrev)); + AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev)); AddAssert("no track change", () => trackChangeQueue.Count == 0); AddUntilStep("track restarted", () => Game.MusicController.CurrentTrack.CurrentTime < 5000); - AddStep("press previous", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicPrev)); + AddStep("press previous", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPrev)); AddAssert("track changed to previous", () => trackChangeQueue.Count == 1 && trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Prev); - AddStep("press next", () => Game.GlobalBinding.TriggerPressed(GlobalAction.MusicNext)); + AddStep("press next", () => globalActionContainer.TriggerPressed(GlobalAction.MusicNext)); AddAssert("track changed to next", () => trackChangeQueue.Count == 1 && trackChangeQueue.Dequeue().changeDirection == TrackChangeDirection.Next); diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index e29d23ba75..c4acf4f7da 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -14,7 +14,6 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; -using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -110,8 +109,6 @@ namespace osu.Game.Tests.Visual.Navigation public new OsuConfigManager LocalConfig => base.LocalConfig; - public new GlobalActionContainer GlobalBinding => base.GlobalBinding; - public new Bindable Beatmap => base.Beatmap; public new Bindable Ruleset => base.Ruleset; diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 8e01bda6ec..4bc8f4c527 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -64,8 +64,6 @@ namespace osu.Game protected FileStore FileStore; - protected GlobalActionContainer GlobalBinding; - protected KeyBindingStore KeyBindingStore; protected SettingsStore SettingsStore; @@ -253,7 +251,10 @@ namespace osu.Game AddInternal(RulesetConfigCache); MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }; - MenuCursorContainer.Child = GlobalBinding = new GlobalActionContainer(this) + + GlobalActionContainer globalBindings; + + MenuCursorContainer.Child = globalBindings = new GlobalActionContainer(this) { RelativeSizeAxes = Axes.Both, Child = content = new OsuTooltipContainer(MenuCursorContainer.Cursor) { RelativeSizeAxes = Axes.Both } @@ -261,8 +262,8 @@ namespace osu.Game base.Content.Add(CreateScalingContainer().WithChild(MenuCursorContainer)); - KeyBindingStore.Register(GlobalBinding); - dependencies.Cache(GlobalBinding); + KeyBindingStore.Register(globalBindings); + dependencies.Cache(globalBindings); PreviewTrackManager previewTrackManager; dependencies.Cache(previewTrackManager = new PreviewTrackManager()); From 4962213cc499eecf9df2f876fc8b14672b11b104 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 18:42:55 +0900 Subject: [PATCH 3125/6909] Rename manage collections filter/text --- osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs | 2 +- osu.Game/Screens/Select/CollectionFilter.cs | 6 +++--- osu.Game/Screens/Select/CollectionFilterDropdown.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 955fe04c8c..5b0e244bbe 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -185,7 +185,7 @@ namespace osu.Game.Tests.Visual.SongSelect } [Test] - public void TestNewCollectionFilterIsNotSelected() + public void TestManageCollectionsFilterIsNotSelected() { addExpandHeaderStep(); diff --git a/osu.Game/Screens/Select/CollectionFilter.cs b/osu.Game/Screens/Select/CollectionFilter.cs index 9e36e3e089..883019ab06 100644 --- a/osu.Game/Screens/Select/CollectionFilter.cs +++ b/osu.Game/Screens/Select/CollectionFilter.cs @@ -54,12 +54,12 @@ namespace osu.Game.Screens.Select } } - public class NewCollectionFilter : CollectionFilter + public class ManageCollectionsFilter : CollectionFilter { - public NewCollectionFilter() + public ManageCollectionsFilter() : base(null) { - CollectionName.Value = "New collection..."; + CollectionName.Value = "Manage collections..."; } } } diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index 5e5c684fe2..6b9ae1b5c8 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Select filters.Clear(); filters.Add(new AllBeatmapCollectionFilter()); filters.AddRange(collections.Select(c => new CollectionFilter(c))); - filters.Add(new NewCollectionFilter()); + filters.Add(new ManageCollectionsFilter()); Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection == selectedItem) ?? filters[0]; } @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Select // Never select the manage collection filter - rollback to the previous filter. // This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value. - if (filter.NewValue is NewCollectionFilter) + if (filter.NewValue is ManageCollectionsFilter) { Current.Value = filter.OldValue; manageCollectionsDialog?.Show(); From ae022d755964536843265ab1e1b7169edeef40a3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 18:55:53 +0900 Subject: [PATCH 3126/6909] Show all items in dropdown, set global max height --- osu.Game/Graphics/UserInterface/OsuContextMenu.cs | 2 ++ osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs index 4b629080e1..8c7b44f952 100644 --- a/osu.Game/Graphics/UserInterface/OsuContextMenu.cs +++ b/osu.Game/Graphics/UserInterface/OsuContextMenu.cs @@ -26,6 +26,8 @@ namespace osu.Game.Graphics.UserInterface }; ItemsContainer.Padding = new MarginPadding { Vertical = DrawableOsuMenuItem.MARGIN_VERTICAL }; + + MaxHeight = 250; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 9f21ec1ad1..cf1c51acd1 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -224,7 +224,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); - var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyDate).Take(3).Select(createCollectionMenuItem).ToList(); + var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionsDialog.Show)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 19ecc277c4..2c098291fa 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.OnlineBeatmapSetID != null && viewDetails != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineBeatmapSetID.Value))); - var collectionItems = collectionManager.Collections.OrderByDescending(c => c.LastModifyDate).Take(3).Select(createCollectionMenuItem).ToList(); + var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList(); if (manageCollectionsDialog != null) collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionsDialog.Show)); From a5e1e8d043c98438e748d4c956febb6e29900a56 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 18:57:18 +0900 Subject: [PATCH 3127/6909] Rename More... to Manage... --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index cf1c51acd1..e9990ab078 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -226,7 +226,7 @@ namespace osu.Game.Screens.Select.Carousel var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList(); if (manageCollectionsDialog != null) - collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionsDialog.Show)); + collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 2c098291fa..fe700f12df 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -144,7 +144,7 @@ namespace osu.Game.Screens.Select.Carousel var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList(); if (manageCollectionsDialog != null) - collectionItems.Add(new OsuMenuItem("More...", MenuItemType.Standard, manageCollectionsDialog.Show)); + collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); From 2b4e2d8ed63c4500aba1fb3236fb481469caab6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 19:04:46 +0900 Subject: [PATCH 3128/6909] Standardise corner radius of dropdowns --- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index fc3a7229fa..cc76c12975 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -17,6 +17,8 @@ namespace osu.Game.Graphics.UserInterface { public class OsuDropdown : Dropdown, IHasAccentColour { + private const float corner_radius = 4; + private Color4 accentColour; public Color4 AccentColour @@ -57,9 +59,11 @@ namespace osu.Game.Graphics.UserInterface // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring public OsuDropdownMenu() { - CornerRadius = 4; + CornerRadius = corner_radius; BackgroundColour = Color4.Black.Opacity(0.5f); + MaskingContainer.CornerRadius = corner_radius; + // todo: this uses the same styling as OsuMenu. hopefully we can just use OsuMenu in the future with some refactoring ItemsContainer.Padding = new MarginPadding(5); } @@ -138,7 +142,7 @@ namespace osu.Game.Graphics.UserInterface Foreground.Padding = new MarginPadding(2); Masking = true; - CornerRadius = 6; + CornerRadius = corner_radius; } [BackgroundDependencyLoader] @@ -237,7 +241,7 @@ namespace osu.Game.Graphics.UserInterface AutoSizeAxes = Axes.None; Margin = new MarginPadding { Bottom = 4 }; - CornerRadius = 4; + CornerRadius = corner_radius; Height = 40; Foreground.Children = new Drawable[] From b7ca0039282769ea2388da23af8e9884a7c265c4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 19:14:48 +0900 Subject: [PATCH 3129/6909] Remove unnecessary check --- osu.Game/Collections/BeatmapCollectionManager.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Collections/BeatmapCollectionManager.cs b/osu.Game/Collections/BeatmapCollectionManager.cs index e4fc4c377b..c14b67a7e8 100644 --- a/osu.Game/Collections/BeatmapCollectionManager.cs +++ b/osu.Game/Collections/BeatmapCollectionManager.cs @@ -120,11 +120,8 @@ namespace osu.Game.Collections return Task.Run(async () => { - if (stable.Exists(database_name)) - { - using (var stream = stable.GetStream(database_name)) - await Import(stream); - } + using (var stream = stable.GetStream(database_name)) + await Import(stream); }); } From 8e2f5d4ea85be3d3f62678d2a84094e52ed15d37 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Sep 2020 19:41:05 +0900 Subject: [PATCH 3130/6909] Fix test failures --- .../Collections/IO/ImportCollectionsTest.cs | 55 ++++++++++++------- .../Collections/BeatmapCollectionManager.cs | 7 +++ 2 files changed, 42 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 7d772d3989..95013859f0 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -8,8 +8,8 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Platform; -using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Tests.Resources; @@ -21,11 +21,11 @@ namespace osu.Game.Tests.Collections.IO [Test] public async Task TestImportEmptyDatabase() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportEmptyDatabase")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { - var osu = await loadOsu(host); + var osu = loadOsu(host); var collectionManager = osu.Dependencies.Get(); await collectionManager.Import(new MemoryStream()); @@ -42,11 +42,11 @@ namespace osu.Game.Tests.Collections.IO [Test] public async Task TestImportWithNoBeatmaps() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithNoBeatmaps")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { - var osu = await loadOsu(host); + var osu = loadOsu(host); var collectionManager = osu.Dependencies.Get(); await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); @@ -69,11 +69,11 @@ namespace osu.Game.Tests.Collections.IO [Test] public async Task TestImportWithBeatmaps() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportWithBeatmaps")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { - var osu = await loadOsu(host, true); + var osu = loadOsu(host, true); var collectionManager = osu.Dependencies.Get(); await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); @@ -99,13 +99,13 @@ namespace osu.Game.Tests.Collections.IO bool exceptionThrown = false; UnhandledExceptionEventHandler setException = (_, __) => exceptionThrown = true; - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestImportMalformedDatabase")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { AppDomain.CurrentDomain.UnhandledException += setException; - var osu = await loadOsu(host, true); + var osu = loadOsu(host, true); var collectionManager = osu.Dependencies.Get(); @@ -137,11 +137,11 @@ namespace osu.Game.Tests.Collections.IO [Test] public async Task TestSaveAndReload() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("TestSaveAndReload")) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { - var osu = await loadOsu(host, true); + var osu = loadOsu(host, true); var collectionManager = osu.Dependencies.Get(); await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); @@ -163,7 +163,7 @@ namespace osu.Game.Tests.Collections.IO { try { - var osu = await loadOsu(host, true); + var osu = loadOsu(host, true); var collectionManager = osu.Dependencies.Get(); @@ -182,9 +182,9 @@ namespace osu.Game.Tests.Collections.IO } } - private async Task loadOsu(GameHost host, bool withBeatmap = false) + private OsuGameBase loadOsu(GameHost host, bool withBeatmap = false) { - var osu = new OsuGameBase(); + var osu = new TestOsuGameBase(withBeatmap); #pragma warning disable 4014 Task.Run(() => host.Run(osu)); @@ -192,12 +192,8 @@ namespace osu.Game.Tests.Collections.IO waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); - if (withBeatmap) - { - var beatmapFile = TestResources.GetTestBeatmapForImport(); - var beatmapManager = osu.Dependencies.Get(); - await beatmapManager.Import(beatmapFile); - } + var collectionManager = osu.Dependencies.Get(); + waitForOrAssert(() => collectionManager.DatabaseLoaded, "Collection database did not load in a reasonable amount of time"); return osu; } @@ -211,5 +207,24 @@ namespace osu.Game.Tests.Collections.IO Assert.IsTrue(task.Wait(timeout), failureMessage); } + + private class TestOsuGameBase : OsuGameBase + { + private readonly bool withBeatmap; + + public TestOsuGameBase(bool withBeatmap) + { + this.withBeatmap = withBeatmap; + } + + protected override void AddInternal(Drawable drawable) + { + // The beatmap must be imported just before the collection manager is loaded. + if (drawable is BeatmapCollectionManager && withBeatmap) + BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + + base.AddInternal(drawable); + } + } } } diff --git a/osu.Game/Collections/BeatmapCollectionManager.cs b/osu.Game/Collections/BeatmapCollectionManager.cs index c14b67a7e8..ed41627d63 100644 --- a/osu.Game/Collections/BeatmapCollectionManager.cs +++ b/osu.Game/Collections/BeatmapCollectionManager.cs @@ -33,6 +33,11 @@ namespace osu.Game.Collections public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; + /// + /// Whether the user's database has finished loading. + /// + public bool DatabaseLoaded { get; private set; } + [Resolved] private GameHost host { get; set; } @@ -86,6 +91,8 @@ namespace osu.Game.Collections using (var stream = storage.GetStream(database_name)) await import(stream); } + + DatabaseLoaded = true; }); /// From a501df954b3337ef6782a07aeb565a0172c7c7b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 19:50:29 +0900 Subject: [PATCH 3131/6909] Avoid multiple editor screens potentially loading on top of each other --- osu.Game/Screens/Edit/Editor.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3ba3eb108a..ac1f61c4fd 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -398,7 +398,11 @@ namespace osu.Game.Screens.Edit break; } - LoadComponentAsync(currentScreen, screenContainer.Add); + LoadComponentAsync(currentScreen, newScreen => + { + if (newScreen == currentScreen) + screenContainer.Add(newScreen); + }); } private void seek(UIEvent e, int direction) From 379fdadbe54e34ea99e57a86106b94bdd9b8bcd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Sep 2020 18:47:14 +0900 Subject: [PATCH 3132/6909] Add test scene for setup screen --- .../Visual/Editing/TestSceneSetupScreen.cs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.cs new file mode 100644 index 0000000000..62e12158ab --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneSetupScreen.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Screens.Edit; +using osu.Game.Screens.Edit.Setup; + +namespace osu.Game.Tests.Visual.Editing +{ + [TestFixture] + public class TestSceneSetupScreen : EditorClockTestScene + { + [Cached(typeof(EditorBeatmap))] + [Cached(typeof(IBeatSnapProvider))] + private readonly EditorBeatmap editorBeatmap; + + public TestSceneSetupScreen() + { + editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + } + + [BackgroundDependencyLoader] + private void load() + { + Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); + Child = new SetupScreen(); + } + } +} From f43f8cf6b95bebf5c18683acdb0e96a6ff731fb3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Sep 2020 19:21:35 +0900 Subject: [PATCH 3133/6909] Add basic setup for song select screen --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 74 +++++++++++++++++++++- 1 file changed, 72 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 758dbc6e16..84e96a14e2 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -1,13 +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.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + namespace osu.Game.Screens.Edit.Setup { public class SetupScreen : EditorScreen { - public SetupScreen() + [BackgroundDependencyLoader] + private void load(OsuColour colours) { - Child = new ScreenWhiteBox.UnderConstructionMessage("Setup mode"); + Children = new Drawable[] + { + new Box + { + Colour = colours.Gray0, + RelativeSizeAxes = Axes.Both, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(50), + Child = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + Height = 250, + Masking = true, + CornerRadius = 50, + Child = new BeatmapBackgroundSprite(Beatmap.Value) + { + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fill, + }, + }, + new OsuSpriteText + { + Text = "Beatmap metadata" + }, + new LabelledTextBox + { + Label = "Artist", + Current = { Value = Beatmap.Value.Metadata.Artist } + }, + new LabelledTextBox + { + Label = "Title", + Current = { Value = Beatmap.Value.Metadata.Title } + }, + new LabelledTextBox + { + Label = "Creator", + Current = { Value = Beatmap.Value.Metadata.AuthorString } + }, + new LabelledTextBox + { + Label = "Difficulty Name", + Current = { Value = Beatmap.Value.BeatmapInfo.Version } + }, + } + }, + }, + }; } } } From fe31edfa26a126c1a3d55a1cad2c51e60ce3aaa7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 19:28:20 +0900 Subject: [PATCH 3134/6909] Add rudimentary saving logic --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 34 ++++++++++++++++++---- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 84e96a14e2..7ea810c514 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -16,6 +18,12 @@ namespace osu.Game.Screens.Edit.Setup { public class SetupScreen : EditorScreen { + private FillFlowContainer flow; + private LabelledTextBox artistTextBox; + private LabelledTextBox titleTextBox; + private LabelledTextBox creatorTextBox; + private LabelledTextBox difficultyTextBox; + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -24,13 +32,14 @@ namespace osu.Game.Screens.Edit.Setup new Box { Colour = colours.Gray0, + Alpha = 0.4f, RelativeSizeAxes = Axes.Both, }, new OsuScrollContainer { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding(50), - Child = new FillFlowContainer + Child = flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -54,22 +63,22 @@ namespace osu.Game.Screens.Edit.Setup { Text = "Beatmap metadata" }, - new LabelledTextBox + artistTextBox = new LabelledTextBox { Label = "Artist", Current = { Value = Beatmap.Value.Metadata.Artist } }, - new LabelledTextBox + titleTextBox = new LabelledTextBox { Label = "Title", Current = { Value = Beatmap.Value.Metadata.Title } }, - new LabelledTextBox + creatorTextBox = new LabelledTextBox { Label = "Creator", Current = { Value = Beatmap.Value.Metadata.AuthorString } }, - new LabelledTextBox + difficultyTextBox = new LabelledTextBox { Label = "Difficulty Name", Current = { Value = Beatmap.Value.BeatmapInfo.Version } @@ -78,6 +87,21 @@ namespace osu.Game.Screens.Edit.Setup }, }, }; + + foreach (var item in flow.OfType()) + item.OnCommit += onCommit; + } + + private void onCommit(TextBox sender, bool newText) + { + if (!newText) return; + + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Value.Metadata.Artist = artistTextBox.Current.Value; + Beatmap.Value.Metadata.Title = titleTextBox.Current.Value; + Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value; + Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; } } } From c8281b17bdee45478edfbb71ecfec3541d1e1e7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 19:49:26 +0900 Subject: [PATCH 3135/6909] Remove editor screen fade (looks bad) --- osu.Game/Screens/Edit/EditorScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index d42447ac4b..8b5f0aaa71 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Edit public void Exit() { - this.FadeOut(250).Expire(); + Expire(); } } } From b55b6e374699e06eed4c0178ad88eec195b4d972 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 19:50:44 +0900 Subject: [PATCH 3136/6909] Bring design somewhat in line with collections dialog --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 112 ++++++++++++--------- 1 file changed, 62 insertions(+), 50 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 7ea810c514..da8eb3a3b3 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -27,65 +27,77 @@ namespace osu.Game.Screens.Edit.Setup [BackgroundDependencyLoader] private void load(OsuColour colours) { - Children = new Drawable[] + Child = new Container { - new Box - { - Colour = colours.Gray0, - Alpha = 0.4f, - RelativeSizeAxes = Axes.Both, - }, - new OsuScrollContainer + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(50), + Child = new Container { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(50), - Child = flow = new FillFlowContainer + Masking = true, + CornerRadius = 10, + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(20), - Direction = FillDirection.Vertical, - Children = new Drawable[] + new Box { - new Container + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding(10), + Child = flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, - Height = 250, - Masking = true, - CornerRadius = 50, - Child = new BeatmapBackgroundSprite(Beatmap.Value) + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Direction = FillDirection.Vertical, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fill, - }, + new Container + { + RelativeSizeAxes = Axes.X, + Height = 250, + Masking = true, + CornerRadius = 10, + Child = new BeatmapBackgroundSprite(Beatmap.Value) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + }, + }, + new OsuSpriteText + { + Text = "Beatmap metadata" + }, + artistTextBox = new LabelledTextBox + { + Label = "Artist", + Current = { Value = Beatmap.Value.Metadata.Artist } + }, + titleTextBox = new LabelledTextBox + { + Label = "Title", + Current = { Value = Beatmap.Value.Metadata.Title } + }, + creatorTextBox = new LabelledTextBox + { + Label = "Creator", + Current = { Value = Beatmap.Value.Metadata.AuthorString } + }, + difficultyTextBox = new LabelledTextBox + { + Label = "Difficulty Name", + Current = { Value = Beatmap.Value.BeatmapInfo.Version } + }, + } }, - new OsuSpriteText - { - Text = "Beatmap metadata" - }, - artistTextBox = new LabelledTextBox - { - Label = "Artist", - Current = { Value = Beatmap.Value.Metadata.Artist } - }, - titleTextBox = new LabelledTextBox - { - Label = "Title", - Current = { Value = Beatmap.Value.Metadata.Title } - }, - creatorTextBox = new LabelledTextBox - { - Label = "Creator", - Current = { Value = Beatmap.Value.Metadata.AuthorString } - }, - difficultyTextBox = new LabelledTextBox - { - Label = "Difficulty Name", - Current = { Value = Beatmap.Value.BeatmapInfo.Version } - }, - } - }, - }, + }, + } + } }; foreach (var item in flow.OfType()) From c38e7d796a577c477fbb844376dc6902667aa015 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Sep 2020 19:51:31 +0900 Subject: [PATCH 3137/6909] Fix tab key not working --- osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 6 ++++++ osu.Game/Screens/Edit/Setup/SetupScreen.cs | 12 ++++++++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 2cbe095d0b..290aba3468 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; @@ -32,6 +33,11 @@ namespace osu.Game.Graphics.UserInterfaceV2 set => Component.Text = value; } + public Container TabbableContentContainer + { + set => Component.TabbableContentContainer = value; + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index da8eb3a3b3..a2c8f19016 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -76,22 +76,26 @@ namespace osu.Game.Screens.Edit.Setup artistTextBox = new LabelledTextBox { Label = "Artist", - Current = { Value = Beatmap.Value.Metadata.Artist } + Current = { Value = Beatmap.Value.Metadata.Artist }, + TabbableContentContainer = this }, titleTextBox = new LabelledTextBox { Label = "Title", - Current = { Value = Beatmap.Value.Metadata.Title } + Current = { Value = Beatmap.Value.Metadata.Title }, + TabbableContentContainer = this }, creatorTextBox = new LabelledTextBox { Label = "Creator", - Current = { Value = Beatmap.Value.Metadata.AuthorString } + Current = { Value = Beatmap.Value.Metadata.AuthorString }, + TabbableContentContainer = this }, difficultyTextBox = new LabelledTextBox { Label = "Difficulty Name", - Current = { Value = Beatmap.Value.BeatmapInfo.Version } + Current = { Value = Beatmap.Value.BeatmapInfo.Version }, + TabbableContentContainer = this }, } }, From 95eeebd93fd8aba3d3d2b837273944ad21328fdb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 8 Sep 2020 15:31:00 +0300 Subject: [PATCH 3138/6909] Fix setting count for recent scores is overcomplicated --- .../Profile/Sections/Ranks/PaginatedScoreContainer.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 0b2bddabbc..7dbdf47cad 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -49,12 +49,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks base.OnItemsReceived(items); if (type == ScoreType.Recent) - { - var count = items.Count; - - Header.Current.Value = count == 0 ? 0 : -1; - Header.Current.TriggerChange(); - } + Header.Current.Value = items.Count; } protected override APIRequest> CreateRequest() => From 2cd07b2d3c8d6e54e82c352b17870e48bcd0c060 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 12:48:11 +0900 Subject: [PATCH 3139/6909] Fix editor crash on saving more than once I'm fixing this in the simplest way possible as this kind of issue is specific to EF core, which may cease to exist quite soon. Turns out the re-retrieval of the beatmap set causes concurrency confusion and wasn't actually needed in my final iteration of the new beatmap logic. --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 34bb578b2a..4496f3b330 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -231,7 +231,7 @@ namespace osu.Game.Beatmaps /// The beatmap content to write, null if to be omitted. public void Save(BeatmapInfo info, IBeatmap beatmapContent, ISkin beatmapSkin = null) { - var setInfo = QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == info.ID)); + var setInfo = info.BeatmapSet; using (var stream = new MemoryStream()) { From 8cd0bbe469436ce28999541fa9545c0193193cdb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 14:31:23 +0900 Subject: [PATCH 3140/6909] Make BeatmapCollectionManager a component --- osu.Game/Collections/BeatmapCollectionManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Collections/BeatmapCollectionManager.cs b/osu.Game/Collections/BeatmapCollectionManager.cs index ed41627d63..00ca660381 100644 --- a/osu.Game/Collections/BeatmapCollectionManager.cs +++ b/osu.Game/Collections/BeatmapCollectionManager.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -20,7 +20,7 @@ using osu.Game.Overlays.Notifications; namespace osu.Game.Collections { - public class BeatmapCollectionManager : CompositeDrawable + public class BeatmapCollectionManager : Component { /// /// Database version in stable-compatible YYYYMMDD format. From 5d9ce0df980bf7ea645c9362889f5ffa363c4750 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 14:44:04 +0900 Subject: [PATCH 3141/6909] Add remark about temporary nature of database format --- osu.Game/Collections/BeatmapCollectionManager.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Collections/BeatmapCollectionManager.cs b/osu.Game/Collections/BeatmapCollectionManager.cs index 00ca660381..0e78c44024 100644 --- a/osu.Game/Collections/BeatmapCollectionManager.cs +++ b/osu.Game/Collections/BeatmapCollectionManager.cs @@ -20,6 +20,13 @@ using osu.Game.Overlays.Notifications; namespace osu.Game.Collections { + /// + /// Handles user-defined collections of beatmaps. + /// + /// + /// This is currently reading and writing from the osu-stable file format. This is a temporary arrangement until we refactor the + /// database backing the game. Going forward writing should be done in a similar way to other model stores. + /// public class BeatmapCollectionManager : Component { /// From 4ddf5f054ba3422d6e7c092ff8873e2a3815dea7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 15:31:08 +0900 Subject: [PATCH 3142/6909] Rename BeatmapCollectionManager -> CollectionManager --- .../Collections/IO/ImportCollectionsTest.cs | 16 ++++++++-------- .../TestSceneManageCollectionsDialog.cs | 4 ++-- .../Visual/SongSelect/TestSceneFilterControl.cs | 4 ++-- ...CollectionManager.cs => CollectionManager.cs} | 4 ++-- .../Collections/DrawableCollectionListItem.cs | 8 ++++---- osu.Game/Collections/ManageCollectionsDialog.cs | 2 +- osu.Game/OsuGameBase.cs | 4 ++-- .../Sections/Maintenance/GeneralSettings.cs | 2 +- .../Select/Carousel/DrawableCarouselBeatmap.cs | 2 +- .../Carousel/DrawableCarouselBeatmapSet.cs | 2 +- .../Screens/Select/CollectionFilterDropdown.cs | 2 +- 11 files changed, 25 insertions(+), 25 deletions(-) rename osu.Game/Collections/{BeatmapCollectionManager.cs => CollectionManager.cs} (99%) diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index 95013859f0..e2335b4d3c 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Collections.IO { var osu = loadOsu(host); - var collectionManager = osu.Dependencies.Get(); + var collectionManager = osu.Dependencies.Get(); await collectionManager.Import(new MemoryStream()); Assert.That(collectionManager.Collections.Count, Is.Zero); @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Collections.IO { var osu = loadOsu(host); - var collectionManager = osu.Dependencies.Get(); + var collectionManager = osu.Dependencies.Get(); await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); Assert.That(collectionManager.Collections.Count, Is.EqualTo(2)); @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Collections.IO { var osu = loadOsu(host, true); - var collectionManager = osu.Dependencies.Get(); + var collectionManager = osu.Dependencies.Get(); await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); Assert.That(collectionManager.Collections.Count, Is.EqualTo(2)); @@ -107,7 +107,7 @@ namespace osu.Game.Tests.Collections.IO var osu = loadOsu(host, true); - var collectionManager = osu.Dependencies.Get(); + var collectionManager = osu.Dependencies.Get(); using (var ms = new MemoryStream()) { @@ -143,7 +143,7 @@ namespace osu.Game.Tests.Collections.IO { var osu = loadOsu(host, true); - var collectionManager = osu.Dependencies.Get(); + var collectionManager = osu.Dependencies.Get(); await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); // Move first beatmap from second collection into the first. @@ -165,7 +165,7 @@ namespace osu.Game.Tests.Collections.IO { var osu = loadOsu(host, true); - var collectionManager = osu.Dependencies.Get(); + var collectionManager = osu.Dependencies.Get(); Assert.That(collectionManager.Collections.Count, Is.EqualTo(2)); @@ -192,7 +192,7 @@ namespace osu.Game.Tests.Collections.IO waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); - var collectionManager = osu.Dependencies.Get(); + var collectionManager = osu.Dependencies.Get(); waitForOrAssert(() => collectionManager.DatabaseLoaded, "Collection database did not load in a reasonable amount of time"); return osu; @@ -220,7 +220,7 @@ namespace osu.Game.Tests.Collections.IO protected override void AddInternal(Drawable drawable) { // The beatmap must be imported just before the collection manager is loaded. - if (drawable is BeatmapCollectionManager && withBeatmap) + if (drawable is CollectionManager && withBeatmap) BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); base.AddInternal(drawable); diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 0c57c27911..54ab20af7f 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Collections private readonly Container content; private readonly DialogOverlay dialogOverlay; - private readonly BeatmapCollectionManager manager; + private readonly CollectionManager manager; private RulesetStore rulesets; private BeatmapManager beatmapManager; @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Collections { base.Content.AddRange(new Drawable[] { - manager = new BeatmapCollectionManager(LocalStorage), + manager = new CollectionManager(LocalStorage), content = new Container { RelativeSizeAxes = Axes.Both }, dialogOverlay = new DialogOverlay() }); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 5b0e244bbe..6012150513 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.SongSelect protected override Container Content => content; private readonly Container content; - private readonly BeatmapCollectionManager collectionManager; + private readonly CollectionManager collectionManager; private RulesetStore rulesets; private BeatmapManager beatmapManager; @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.SongSelect { base.Content.AddRange(new Drawable[] { - collectionManager = new BeatmapCollectionManager(LocalStorage), + collectionManager = new CollectionManager(LocalStorage), content = new Container { RelativeSizeAxes = Axes.Both } }); } diff --git a/osu.Game/Collections/BeatmapCollectionManager.cs b/osu.Game/Collections/CollectionManager.cs similarity index 99% rename from osu.Game/Collections/BeatmapCollectionManager.cs rename to osu.Game/Collections/CollectionManager.cs index 0e78c44024..8b91ab219f 100644 --- a/osu.Game/Collections/BeatmapCollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Collections /// This is currently reading and writing from the osu-stable file format. This is a temporary arrangement until we refactor the /// database backing the game. Going forward writing should be done in a similar way to other model stores. /// - public class BeatmapCollectionManager : Component + public class CollectionManager : Component { /// /// Database version in stable-compatible YYYYMMDD format. @@ -53,7 +53,7 @@ namespace osu.Game.Collections private readonly Storage storage; - public BeatmapCollectionManager(Storage storage) + public CollectionManager(Storage storage) { this.storage = storage; } diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index a7075c3179..7d158f182f 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -28,7 +28,7 @@ namespace osu.Game.Collections private const float button_width = item_height * 0.75f; /// - /// Whether the currently exists inside the . + /// Whether the currently exists inside the . /// public IBindable IsCreated => isCreated; @@ -38,7 +38,7 @@ namespace osu.Game.Collections /// Creates a new . /// /// The . - /// Whether currently exists inside the . + /// Whether currently exists inside the . public DrawableCollectionListItem(BeatmapCollection item, bool isCreated) : base(item) { @@ -63,7 +63,7 @@ namespace osu.Game.Collections private readonly BeatmapCollection collection; [Resolved] - private BeatmapCollectionManager collectionManager { get; set; } + private CollectionManager collectionManager { get; set; } private Container textBoxPaddingContainer; private ItemTextBox textBox; @@ -159,7 +159,7 @@ namespace osu.Game.Collections private DialogOverlay dialogOverlay { get; set; } [Resolved] - private BeatmapCollectionManager collectionManager { get; set; } + private CollectionManager collectionManager { get; set; } private readonly BeatmapCollection collection; diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index f6964191a1..cfde9d5550 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -20,7 +20,7 @@ namespace osu.Game.Collections private const double exit_duration = 200; [Resolved] - private BeatmapCollectionManager collectionManager { get; set; } + private CollectionManager collectionManager { get; set; } public ManageCollectionsDialog() { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d98d4e2123..d4741f9e69 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -57,7 +57,7 @@ namespace osu.Game protected BeatmapManager BeatmapManager; - protected BeatmapCollectionManager CollectionManager; + protected CollectionManager CollectionManager; protected ScoreManager ScoreManager; @@ -228,7 +228,7 @@ namespace osu.Game dependencies.Cache(difficultyManager); AddInternal(difficultyManager); - dependencies.Cache(CollectionManager = new BeatmapCollectionManager(Storage)); + dependencies.Cache(CollectionManager = new CollectionManager(Storage)); AddInternal(CollectionManager); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 30fd5921eb..83ee5e497a 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -28,7 +28,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private TriangleButton undeleteButton; [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, BeatmapCollectionManager collectionManager, DialogOverlay dialogOverlay) + private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, CollectionManager collectionManager, DialogOverlay dialogOverlay) { if (beatmaps.SupportsImportFromStable) { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index e9990ab078..1db73702bb 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Select.Carousel private BeatmapDifficultyManager difficultyManager { get; set; } [Resolved] - private BeatmapCollectionManager collectionManager { get; set; } + private CollectionManager collectionManager { get; set; } [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index fe700f12df..fd66315f67 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Select.Carousel private DialogOverlay dialogOverlay { get; set; } [Resolved] - private BeatmapCollectionManager collectionManager { get; set; } + private CollectionManager collectionManager { get; set; } [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index 6b9ae1b5c8..7270354e87 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader] - private void load(BeatmapCollectionManager collectionManager) + private void load(CollectionManager collectionManager) { collections.BindTo(collectionManager.Collections); collections.CollectionChanged += (_, __) => collectionsChanged(); From 0360f7d8456b0dd43f27713a55e81d9319d96ae0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 15:39:15 +0900 Subject: [PATCH 3143/6909] Move CollectionManager to OsuGame --- osu.Game/Collections/DrawableCollectionListItem.cs | 8 ++++---- osu.Game/Collections/ManageCollectionsDialog.cs | 5 +++-- osu.Game/OsuGame.cs | 9 ++++++--- osu.Game/OsuGameBase.cs | 6 ------ .../Select/Carousel/DrawableCarouselBeatmap.cs | 13 ++++++++----- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 13 ++++++++----- 6 files changed, 29 insertions(+), 25 deletions(-) diff --git a/osu.Game/Collections/DrawableCollectionListItem.cs b/osu.Game/Collections/DrawableCollectionListItem.cs index 7d158f182f..988a3443c3 100644 --- a/osu.Game/Collections/DrawableCollectionListItem.cs +++ b/osu.Game/Collections/DrawableCollectionListItem.cs @@ -62,7 +62,7 @@ namespace osu.Game.Collections private readonly IBindable collectionName; private readonly BeatmapCollection collection; - [Resolved] + [Resolved(CanBeNull = true)] private CollectionManager collectionManager { get; set; } private Container textBoxPaddingContainer; @@ -127,7 +127,7 @@ namespace osu.Game.Collections return; // Add the new collection and disable our placeholder. If all text is removed, the placeholder should not show back again. - collectionManager.Collections.Add(collection); + collectionManager?.Collections.Add(collection); textBox.PlaceholderText = string.Empty; // When this item changes from placeholder to non-placeholder (via changing containers), its textbox will lose focus, so it needs to be re-focused. @@ -158,7 +158,7 @@ namespace osu.Game.Collections [Resolved(CanBeNull = true)] private DialogOverlay dialogOverlay { get; set; } - [Resolved] + [Resolved(CanBeNull = true)] private CollectionManager collectionManager { get; set; } private readonly BeatmapCollection collection; @@ -231,7 +231,7 @@ namespace osu.Game.Collections return true; } - private void deleteCollection() => collectionManager.Collections.Remove(collection); + private void deleteCollection() => collectionManager?.Collections.Remove(collection); } } } diff --git a/osu.Game/Collections/ManageCollectionsDialog.cs b/osu.Game/Collections/ManageCollectionsDialog.cs index cfde9d5550..680fec904f 100644 --- a/osu.Game/Collections/ManageCollectionsDialog.cs +++ b/osu.Game/Collections/ManageCollectionsDialog.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -19,7 +20,7 @@ namespace osu.Game.Collections private const double enter_duration = 500; private const double exit_duration = 200; - [Resolved] + [Resolved(CanBeNull = true)] private CollectionManager collectionManager { get; set; } public ManageCollectionsDialog() @@ -100,7 +101,7 @@ namespace osu.Game.Collections new DrawableCollectionList { RelativeSizeAxes = Axes.Both, - Items = { BindTarget = collectionManager.Collections } + Items = { BindTarget = collectionManager?.Collections ?? new BindableList() } } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0977f6c242..4a699dc82e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -538,9 +538,6 @@ namespace osu.Game ScoreManager.GetStableStorage = GetStorageForStableInstall; ScoreManager.PresentImport = items => PresentScore(items.First()); - CollectionManager.PostNotification = n => notifications.Post(n); - CollectionManager.GetStableStorage = GetStorageForStableInstall; - Container logoContainer; BackButton.Receptor receptor; @@ -614,6 +611,12 @@ namespace osu.Game d.Origin = Anchor.TopRight; }), rightFloatingOverlayContent.Add, true); + loadComponentSingleFile(new CollectionManager(Storage) + { + PostNotification = n => notifications.Post(n), + GetStableStorage = GetStorageForStableInstall + }, Add, true); + loadComponentSingleFile(screenshotManager, Add); // dependency on notification overlay, dependent by settings overlay diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d4741f9e69..4bc8f4c527 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -26,7 +26,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input; using osu.Framework.Logging; using osu.Game.Audio; -using osu.Game.Collections; using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; @@ -57,8 +56,6 @@ namespace osu.Game protected BeatmapManager BeatmapManager; - protected CollectionManager CollectionManager; - protected ScoreManager ScoreManager; protected SkinManager SkinManager; @@ -228,9 +225,6 @@ namespace osu.Game dependencies.Cache(difficultyManager); AddInternal(difficultyManager); - dependencies.Cache(CollectionManager = new CollectionManager(Storage)); - AddInternal(CollectionManager); - dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 1db73702bb..10745fe3c1 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -48,7 +48,7 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private BeatmapDifficultyManager difficultyManager { get; set; } - [Resolved] + [Resolved(CanBeNull = true)] private CollectionManager collectionManager { get; set; } [Resolved(CanBeNull = true)] @@ -224,11 +224,14 @@ namespace osu.Game.Screens.Select.Carousel if (beatmap.OnlineBeatmapID.HasValue && beatmapOverlay != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => beatmapOverlay.FetchAndShowBeatmap(beatmap.OnlineBeatmapID.Value))); - var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList(); - if (manageCollectionsDialog != null) - collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); + if (collectionManager != null) + { + var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList(); + if (manageCollectionsDialog != null) + collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); - items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); + items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); + } if (hideRequested != null) items.Add(new OsuMenuItem("Hide", MenuItemType.Destructive, () => hideRequested(beatmap))); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index fd66315f67..3c8ac69dd2 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private DialogOverlay dialogOverlay { get; set; } - [Resolved] + [Resolved(CanBeNull = true)] private CollectionManager collectionManager { get; set; } [Resolved(CanBeNull = true)] @@ -142,11 +142,14 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapSet.OnlineBeatmapSetID != null && viewDetails != null) items.Add(new OsuMenuItem("Details...", MenuItemType.Standard, () => viewDetails(beatmapSet.OnlineBeatmapSetID.Value))); - var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList(); - if (manageCollectionsDialog != null) - collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); + if (collectionManager != null) + { + var collectionItems = collectionManager.Collections.Select(createCollectionMenuItem).ToList(); + if (manageCollectionsDialog != null) + collectionItems.Add(new OsuMenuItem("Manage...", MenuItemType.Standard, manageCollectionsDialog.Show)); - items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); + items.Add(new OsuMenuItem("Collections") { Items = collectionItems }); + } if (beatmapSet.Beatmaps.Any(b => b.Hidden)) items.Add(new OsuMenuItem("Restore all hidden", MenuItemType.Standard, () => restoreHiddenRequested(beatmapSet))); From 2d7e85f62203d5c017acd893d60d90fe22bc1325 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 15:40:45 +0900 Subject: [PATCH 3144/6909] Remove async load (now using loadComponentSingleFile) --- osu.Game/Collections/CollectionManager.cs | 40 +++++------------------ 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 8b91ab219f..a50ab5b07a 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -40,11 +40,6 @@ namespace osu.Game.Collections public bool SupportsImportFromStable => RuntimeInfo.IsDesktop; - /// - /// Whether the user's database has finished loading. - /// - public bool DatabaseLoaded { get; private set; } - [Resolved] private GameHost host { get; set; } @@ -62,7 +57,12 @@ namespace osu.Game.Collections private void load() { Collections.CollectionChanged += collectionsChanged; - loadDatabase(); + + if (storage.Exists(database_name)) + { + using (var stream = storage.GetStream(database_name)) + importCollections(readCollections(stream)); + } } private void collectionsChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -91,17 +91,6 @@ namespace osu.Game.Collections backgroundSave(); } - private void loadDatabase() => Task.Run(async () => - { - if (storage.Exists(database_name)) - { - using (var stream = storage.GetStream(database_name)) - await import(stream); - } - - DatabaseLoaded = true; - }); - /// /// Set an endpoint for notifications to be posted to. /// @@ -149,14 +138,6 @@ namespace osu.Game.Collections PostNotification?.Invoke(notification); - await import(stream, notification); - } - - private async Task import(Stream stream, ProgressNotification notification = null) => await Task.Run(async () => - { - if (notification != null) - notification.Progress = 0; - var collection = readCollections(stream, notification); bool importCompleted = false; @@ -169,12 +150,9 @@ namespace osu.Game.Collections while (!IsDisposed && !importCompleted) await Task.Delay(10); - if (notification != null) - { - notification.CompletionText = $"Imported {collection.Count} collections"; - notification.State = ProgressNotificationState.Completed; - } - }); + notification.CompletionText = $"Imported {collection.Count} collections"; + notification.State = ProgressNotificationState.Completed; + } private void importCollections(List newCollections) { From b1b99e4d6f3d54bea94cd65b8b38f649a3037c25 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 15:55:56 +0900 Subject: [PATCH 3145/6909] Fix tests --- .../Collections/IO/ImportCollectionsTest.cs | 75 ++++++++----------- 1 file changed, 33 insertions(+), 42 deletions(-) diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index e2335b4d3c..a79e0d0338 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -8,7 +8,6 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Game.Collections; using osu.Game.Tests.Resources; @@ -27,10 +26,9 @@ namespace osu.Game.Tests.Collections.IO { var osu = loadOsu(host); - var collectionManager = osu.Dependencies.Get(); - await collectionManager.Import(new MemoryStream()); + await osu.CollectionManager.Import(new MemoryStream()); - Assert.That(collectionManager.Collections.Count, Is.Zero); + Assert.That(osu.CollectionManager.Collections.Count, Is.Zero); } finally { @@ -48,16 +46,15 @@ namespace osu.Game.Tests.Collections.IO { var osu = loadOsu(host); - var collectionManager = osu.Dependencies.Get(); - await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); + await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); - Assert.That(collectionManager.Collections.Count, Is.EqualTo(2)); + Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); - Assert.That(collectionManager.Collections[0].Name.Value, Is.EqualTo("First")); - Assert.That(collectionManager.Collections[0].Beatmaps.Count, Is.Zero); + Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First")); + Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.Zero); - Assert.That(collectionManager.Collections[1].Name.Value, Is.EqualTo("Second")); - Assert.That(collectionManager.Collections[1].Beatmaps.Count, Is.Zero); + Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second")); + Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.Zero); } finally { @@ -75,16 +72,15 @@ namespace osu.Game.Tests.Collections.IO { var osu = loadOsu(host, true); - var collectionManager = osu.Dependencies.Get(); - await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); + await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); - Assert.That(collectionManager.Collections.Count, Is.EqualTo(2)); + Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); - Assert.That(collectionManager.Collections[0].Name.Value, Is.EqualTo("First")); - Assert.That(collectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(1)); + Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First")); + Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(1)); - Assert.That(collectionManager.Collections[1].Name.Value, Is.EqualTo("Second")); - Assert.That(collectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(12)); + Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Second")); + Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(12)); } finally { @@ -107,8 +103,6 @@ namespace osu.Game.Tests.Collections.IO var osu = loadOsu(host, true); - var collectionManager = osu.Dependencies.Get(); - using (var ms = new MemoryStream()) { using (var bw = new BinaryWriter(ms, Encoding.UTF8, true)) @@ -119,12 +113,12 @@ namespace osu.Game.Tests.Collections.IO ms.Seek(0, SeekOrigin.Begin); - await collectionManager.Import(ms); + await osu.CollectionManager.Import(ms); } Assert.That(host.UpdateThread.Running, Is.True); Assert.That(exceptionThrown, Is.False); - Assert.That(collectionManager.Collections.Count, Is.EqualTo(0)); + Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(0)); } finally { @@ -143,15 +137,14 @@ namespace osu.Game.Tests.Collections.IO { var osu = loadOsu(host, true); - var collectionManager = osu.Dependencies.Get(); - await collectionManager.Import(TestResources.OpenResource("Collections/collections.db")); + await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); // Move first beatmap from second collection into the first. - collectionManager.Collections[0].Beatmaps.Add(collectionManager.Collections[1].Beatmaps[0]); - collectionManager.Collections[1].Beatmaps.RemoveAt(0); + osu.CollectionManager.Collections[0].Beatmaps.Add(osu.CollectionManager.Collections[1].Beatmaps[0]); + osu.CollectionManager.Collections[1].Beatmaps.RemoveAt(0); // Rename the second collecction. - collectionManager.Collections[1].Name.Value = "Another"; + osu.CollectionManager.Collections[1].Name.Value = "Another"; } finally { @@ -165,15 +158,13 @@ namespace osu.Game.Tests.Collections.IO { var osu = loadOsu(host, true); - var collectionManager = osu.Dependencies.Get(); + Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); - Assert.That(collectionManager.Collections.Count, Is.EqualTo(2)); + Assert.That(osu.CollectionManager.Collections[0].Name.Value, Is.EqualTo("First")); + Assert.That(osu.CollectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(2)); - Assert.That(collectionManager.Collections[0].Name.Value, Is.EqualTo("First")); - Assert.That(collectionManager.Collections[0].Beatmaps.Count, Is.EqualTo(2)); - - Assert.That(collectionManager.Collections[1].Name.Value, Is.EqualTo("Another")); - Assert.That(collectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(11)); + Assert.That(osu.CollectionManager.Collections[1].Name.Value, Is.EqualTo("Another")); + Assert.That(osu.CollectionManager.Collections[1].Beatmaps.Count, Is.EqualTo(11)); } finally { @@ -182,7 +173,7 @@ namespace osu.Game.Tests.Collections.IO } } - private OsuGameBase loadOsu(GameHost host, bool withBeatmap = false) + private TestOsuGameBase loadOsu(GameHost host, bool withBeatmap = false) { var osu = new TestOsuGameBase(withBeatmap); @@ -192,9 +183,6 @@ namespace osu.Game.Tests.Collections.IO waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); - var collectionManager = osu.Dependencies.Get(); - waitForOrAssert(() => collectionManager.DatabaseLoaded, "Collection database did not load in a reasonable amount of time"); - return osu; } @@ -210,6 +198,8 @@ namespace osu.Game.Tests.Collections.IO private class TestOsuGameBase : OsuGameBase { + public CollectionManager CollectionManager { get; private set; } + private readonly bool withBeatmap; public TestOsuGameBase(bool withBeatmap) @@ -217,13 +207,14 @@ namespace osu.Game.Tests.Collections.IO this.withBeatmap = withBeatmap; } - protected override void AddInternal(Drawable drawable) + [BackgroundDependencyLoader] + private void load() { - // The beatmap must be imported just before the collection manager is loaded. - if (drawable is CollectionManager && withBeatmap) + // Beatmap must be imported before the collection manager is loaded. + if (withBeatmap) BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); - base.AddInternal(drawable); + AddInternal(CollectionManager = new CollectionManager(Storage)); } } } From 1a023d2c887ab72795c34e435cb22b4a130e2a6f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 16:33:48 +0900 Subject: [PATCH 3146/6909] Fix a few more tests --- .../Sections/Maintenance/GeneralSettings.cs | 36 ++++++++++--------- .../Select/CollectionFilterDropdown.cs | 8 +++-- 2 files changed, 25 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs index 83ee5e497a..848ce381a9 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/GeneralSettings.cs @@ -3,6 +3,7 @@ using System.Linq; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; @@ -27,8 +28,8 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private TriangleButton restoreButton; private TriangleButton undeleteButton; - [BackgroundDependencyLoader] - private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, CollectionManager collectionManager, DialogOverlay dialogOverlay) + [BackgroundDependencyLoader(permitNulls: true)] + private void load(BeatmapManager beatmaps, ScoreManager scores, SkinManager skins, [CanBeNull] CollectionManager collectionManager, DialogOverlay dialogOverlay) { if (beatmaps.SupportsImportFromStable) { @@ -108,28 +109,31 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance } }); - if (collectionManager.SupportsImportFromStable) + if (collectionManager != null) { - Add(importCollectionsButton = new SettingsButton + if (collectionManager.SupportsImportFromStable) { - Text = "Import collections from stable", + Add(importCollectionsButton = new SettingsButton + { + Text = "Import collections from stable", + Action = () => + { + importCollectionsButton.Enabled.Value = false; + collectionManager.ImportFromStableAsync().ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true)); + } + }); + } + + Add(new DangerousSettingsButton + { + Text = "Delete ALL collections", Action = () => { - importCollectionsButton.Enabled.Value = false; - collectionManager.ImportFromStableAsync().ContinueWith(t => Schedule(() => importCollectionsButton.Enabled.Value = true)); + dialogOverlay?.Push(new DeleteAllBeatmapsDialog(collectionManager.DeleteAll)); } }); } - Add(new DangerousSettingsButton - { - Text = "Delete ALL collections", - Action = () => - { - dialogOverlay?.Push(new DeleteAllBeatmapsDialog(collectionManager.DeleteAll)); - } - }); - AddRange(new Drawable[] { restoreButton = new SettingsButton diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index 7270354e87..1e2a3d0aa7 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -37,10 +37,12 @@ namespace osu.Game.Screens.Select ItemSource = filters; } - [BackgroundDependencyLoader] - private void load(CollectionManager collectionManager) + [BackgroundDependencyLoader(permitNulls: true)] + private void load([CanBeNull] CollectionManager collectionManager) { - collections.BindTo(collectionManager.Collections); + if (collectionManager != null) + collections.BindTo(collectionManager.Collections); + collections.CollectionChanged += (_, __) => collectionsChanged(); collectionsChanged(); } From e271408fca532fd6e1dcd93fc68c380e1b19eab4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 16:51:53 +0900 Subject: [PATCH 3147/6909] Move max score calculation inside ScoreProcessor --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 8 ++++---- osu.Game/Scoring/ScoreManager.cs | 5 +---- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 7d138bd878..46994d4f18 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Scoring private readonly double accuracyPortion; private readonly double comboPortion; - private double maxHighestCombo; + private int maxHighestCombo; private double maxBaseScore; private double rollingMaxBaseScore; private double baseScore; @@ -204,10 +204,10 @@ namespace osu.Game.Rulesets.Scoring private double getScore(ScoringMode mode) { - return GetScore(mode, maxBaseScore, maxHighestCombo, baseScore / maxBaseScore, HighestCombo.Value / maxHighestCombo, bonusScore); + return GetScore(mode, maxHighestCombo, baseScore / maxBaseScore, (double)HighestCombo.Value / maxHighestCombo, bonusScore); } - public double GetScore(ScoringMode mode, double maxBaseScore, double maxHighestCombo, double accuracyRatio, double comboRatio, double bonusScore) + public double GetScore(ScoringMode mode, int maxCombo, double accuracyRatio, double comboRatio, double bonusScore) { switch (mode) { @@ -220,7 +220,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 + (accuracyRatio * maxBaseScore) * (1 + Math.Max(0, (comboRatio * maxHighestCombo) - 1) * scoreMultiplier / 25); + return bonusScore + (accuracyRatio * maxCombo * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25); } } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 634cca159a..5518c86910 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -154,10 +154,7 @@ namespace osu.Game.Scoring scoreProcessor.Mods.Value = score.Mods; - double maxBaseScore = 300 * beatmapMaxCombo; - double maxHighestCombo = beatmapMaxCombo; - - Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, maxBaseScore, maxHighestCombo, score.Accuracy, score.MaxCombo / maxHighestCombo, 0)); + Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo, 0)); } } From 37a659b2af22ca0246cd9833ed1d21fe37726fa6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 17:04:02 +0900 Subject: [PATCH 3148/6909] Refactor/add xmldocs --- .../Online/Leaderboards/LeaderboardScore.cs | 2 +- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 2 +- .../Scores/TopScoreStatisticsSection.cs | 4 +-- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 14 +++++--- osu.Game/Scoring/ScoreManager.cs | 33 ++++++++++++++++--- .../ContractedPanelMiddleContent.cs | 2 +- .../Expanded/ExpandedPanelMiddleContent.cs | 2 +- 7 files changed, 45 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index 846bebe347..dcd0cb435a 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -194,7 +194,7 @@ namespace osu.Game.Online.Leaderboards { TextColour = Color4.White, GlowColour = Color4Extensions.FromHex(@"83ccfa"), - Current = scoreManager.GetTotalScoreString(score), + Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.Numeric.With(size: 23), }, RankContainer = new Container diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 6bebd98eef..56866765b6 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -124,7 +124,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores new OsuSpriteText { Margin = new MarginPadding { Right = horizontal_inset }, - Current = scoreManager.GetTotalScoreString(score), + Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: text_size, weight: index == 0 ? FontWeight.Bold : FontWeight.Medium) }, new OsuSpriteText diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 507c692eb1..2fd522dc9d 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -95,7 +95,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores private void load() { if (score != null) - totalScoreColumn.Current = scoreManager.GetTotalScoreString(score); + totalScoreColumn.Current = scoreManager.GetBindableTotalScoreString(score); } private ScoreInfo score; @@ -121,7 +121,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores modsColumn.Mods = value.Mods; if (IsLoaded) - totalScoreColumn.Current = scoreManager.GetTotalScoreString(value); + totalScoreColumn.Current = scoreManager.GetBindableTotalScoreString(value); } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 46994d4f18..983f9a3abf 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -202,11 +202,17 @@ namespace osu.Game.Rulesets.Scoring TotalScore.Value = getScore(Mode.Value); } - private double getScore(ScoringMode mode) - { - return GetScore(mode, maxHighestCombo, baseScore / maxBaseScore, (double)HighestCombo.Value / maxHighestCombo, bonusScore); - } + private double getScore(ScoringMode mode) => GetScore(mode, maxHighestCombo, baseScore / maxBaseScore, (double)HighestCombo.Value / maxHighestCombo, bonusScore); + /// + /// Computes the total score. + /// + /// The to compute the total score in. + /// The maximum combo achievable in the beatmap. + /// The accuracy percentage achieved by the player. + /// The proportion of achieved by the player. + /// Any bonus score to be added. + /// The total score. public double GetScore(ScoringMode mode, int maxCombo, double accuracyRatio, double comboRatio, double bonusScore) { switch (mode) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 5518c86910..5a6ef6945c 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -86,15 +86,34 @@ namespace osu.Game.Scoring => base.CheckLocalAvailability(model, items) || (model.OnlineScoreID != null && items.Any(i => i.OnlineScoreID == model.OnlineScoreID)); - public Bindable GetTotalScore(ScoreInfo score) + /// + /// Retrieves a bindable that represents the total score of a . + /// + /// + /// Responds to changes in the currently-selected . + /// + /// The to retrieve the bindable for. + /// The bindable containing the total score. + public Bindable GetBindableTotalScore(ScoreInfo score) { var bindable = new TotalScoreBindable(score, difficulties); configManager?.BindWith(OsuSetting.ScoreDisplayMode, bindable.ScoringMode); return bindable; } - public Bindable GetTotalScoreString(ScoreInfo score) => new TotalScoreStringBindable(GetTotalScore(score)); + /// + /// Retrieves a bindable that represents the formatted total score string of a . + /// + /// + /// Responds to changes in the currently-selected . + /// + /// The to retrieve the bindable for. + /// The bindable containing the formatted total score string. + public Bindable GetBindableTotalScoreString(ScoreInfo score) => new TotalScoreStringBindable(GetBindableTotalScore(score)); + /// + /// Provides the total score of a . Responds to changes in the currently-selected . + /// private class TotalScoreBindable : Bindable { public readonly Bindable ScoringMode = new Bindable(); @@ -102,13 +121,16 @@ namespace osu.Game.Scoring private readonly ScoreInfo score; private readonly Func difficulties; + /// + /// Creates a new . + /// + /// The to provide the total score of. + /// A function to retrieve the . public TotalScoreBindable(ScoreInfo score, Func difficulties) { this.score = score; this.difficulties = difficulties; - Value = 0; - ScoringMode.BindValueChanged(onScoringModeChanged, true); } @@ -158,6 +180,9 @@ namespace osu.Game.Scoring } } + /// + /// Provides the total score of a as a formatted string. Responds to changes in the currently-selected . + /// private class TotalScoreStringBindable : Bindable { // ReSharper disable once PrivateFieldCanBeConvertedToLocalVariable (need to hold a reference) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index b37b89e6c0..0b85eeafa8 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Ranking.Contracted { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Current = scoreManager.GetTotalScoreString(score), + Current = scoreManager.GetBindableTotalScoreString(score), Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, fixedWidth: true), Spacing = new Vector2(-1, 0) }, diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 3433410d3c..0033cd1f43 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -239,7 +239,7 @@ namespace osu.Game.Screens.Ranking.Expanded using (BeginDelayedSequence(AccuracyCircle.ACCURACY_TRANSFORM_DELAY, true)) { scoreCounter.FadeIn(); - scoreCounter.Current = scoreManager.GetTotalScore(score); + scoreCounter.Current = scoreManager.GetBindableTotalScore(score); double delay = 0; From 5cdc8d2e7b46c092031a7a0a7daf8f645cae4c13 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 17:37:11 +0900 Subject: [PATCH 3149/6909] Add cancellation support --- osu.Game/Scoring/ScoreManager.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 5a6ef6945c..619ca76598 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Linq.Expressions; +using System.Threading; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework.Bindables; @@ -135,9 +136,13 @@ namespace osu.Game.Scoring } private IBindable difficultyBindable; + private CancellationTokenSource difficultyCancellationSource; private void onScoringModeChanged(ValueChangedEvent mode) { + difficultyCancellationSource?.Cancel(); + difficultyCancellationSource = null; + if (score.Beatmap == null) { Value = score.TotalScore; @@ -156,7 +161,7 @@ namespace osu.Game.Scoring } // We can compute the max combo locally after the async beatmap difficulty computation. - difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods); + difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token); difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true); } else From b1daca6cd33e19c71b91f90ad86f0247ebd4f628 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 18:05:44 +0900 Subject: [PATCH 3150/6909] Fix overlay sound effects playing when open requested while disabled --- .../Graphics/Containers/OsuFocusedOverlayContainer.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 751ccc8f15..1d96e602d0 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -103,6 +103,8 @@ namespace osu.Game.Graphics.Containers { } + private bool playedPopInSound; + protected override void UpdateState(ValueChangedEvent state) { switch (state.NewValue) @@ -115,11 +117,18 @@ namespace osu.Game.Graphics.Containers } samplePopIn?.Play(); + playedPopInSound = true; + if (BlockScreenWideMouse && DimMainContent) game?.AddBlockingOverlay(this); break; case Visibility.Hidden: - samplePopOut?.Play(); + if (playedPopInSound) + { + samplePopOut?.Play(); + playedPopInSound = false; + } + if (BlockScreenWideMouse) game?.RemoveBlockingOverlay(this); break; } From cdf3e206857186de7052061b5210642a421f6524 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 18:07:58 +0900 Subject: [PATCH 3151/6909] Add comment regarding feedback --- osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 1d96e602d0..41fd37a0d7 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -112,6 +112,7 @@ namespace osu.Game.Graphics.Containers case Visibility.Visible: if (OverlayActivationMode.Value == OverlayActivation.Disabled) { + // todo: visual/audible feedback that this operation could not complete. State.Value = Visibility.Hidden; return; } From c9f5005efd19270f54ab6834c5c3be301d38c11e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 18:35:25 +0900 Subject: [PATCH 3152/6909] Add icons for editor toolbox tools --- .../Edit/HitCircleCompositionTool.cs | 4 +++ .../Edit/SliderCompositionTool.cs | 4 +++ .../Edit/SpinnerCompositionTool.cs | 4 +++ .../TestSceneEditorComposeRadioButtons.cs | 3 ++- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- .../Edit/Tools/HitObjectCompositionTool.cs | 4 +++ osu.Game/Rulesets/Edit/Tools/SelectTool.cs | 5 ++++ .../RadioButtons/DrawableRadioButton.cs | 27 +++++++------------ .../Components/RadioButtons/RadioButton.cs | 9 ++++++- 9 files changed, 42 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs index 9c94fe0e3d..5f7c8b77b0 100644 --- a/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/HitCircleCompositionTool.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.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit { } + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); + public override PlacementBlueprint CreatePlacementBlueprint() => new HitCirclePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs index a377deb35f..596224e5c6 100644 --- a/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SliderCompositionTool.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.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; @@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit { } + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); + public override PlacementBlueprint CreatePlacementBlueprint() => new SliderPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs index 0de0af8f8c..c5e90da3bd 100644 --- a/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.cs +++ b/osu.Game.Rulesets.Osu/Edit/SpinnerCompositionTool.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.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners; @@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Osu.Edit { } + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); + public override PlacementBlueprint CreatePlacementBlueprint() => new SpinnerPlacementBlueprint(); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs index e4d7e025a8..0b52ae2b95 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorComposeRadioButtons.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Game.Screens.Edit.Components.RadioButtons; namespace osu.Game.Tests.Visual.Editing @@ -22,7 +23,7 @@ namespace osu.Game.Tests.Visual.Editing { new RadioButton("Item 1", () => { }), new RadioButton("Item 2", () => { }), - new RadioButton("Item 3", () => { }), + new RadioButton("Item 3", () => { }, () => new SpriteIcon { Icon = FontAwesome.Regular.Angry }), new RadioButton("Item 4", () => { }), new RadioButton("Item 5", () => { }) } diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index f134db1ffe..955548fee9 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Edit toolboxCollection.Items = CompositionTools .Prepend(new SelectTool()) - .Select(t => new RadioButton(t.Name, () => toolSelected(t))) + .Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon)) .ToList(); setSelectTool(); diff --git a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs index 0631031302..0a01ac4320 100644 --- a/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/HitObjectCompositionTool.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; + namespace osu.Game.Rulesets.Edit.Tools { public abstract class HitObjectCompositionTool @@ -14,6 +16,8 @@ namespace osu.Game.Rulesets.Edit.Tools public abstract PlacementBlueprint CreatePlacementBlueprint(); + public virtual Drawable CreateIcon() => null; + public override string ToString() => Name; } } diff --git a/osu.Game/Rulesets/Edit/Tools/SelectTool.cs b/osu.Game/Rulesets/Edit/Tools/SelectTool.cs index b96eeb0790..c050766b23 100644 --- a/osu.Game/Rulesets/Edit/Tools/SelectTool.cs +++ b/osu.Game/Rulesets/Edit/Tools/SelectTool.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 osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; + namespace osu.Game.Rulesets.Edit.Tools { public class SelectTool : HitObjectCompositionTool @@ -10,6 +13,8 @@ namespace osu.Game.Rulesets.Edit.Tools { } + public override Drawable CreateIcon() => new SpriteIcon { Icon = FontAwesome.Solid.MousePointer }; + public override PlacementBlueprint CreatePlacementBlueprint() => null; } } diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/DrawableRadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/DrawableRadioButton.cs index 7be91f4e8e..0cf7b83f3b 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/DrawableRadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/DrawableRadioButton.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; @@ -29,7 +28,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons private Color4 selectedBackgroundColour; private Color4 selectedBubbleColour; - private readonly Drawable bubble; + private Drawable icon; private readonly RadioButton button; public DrawableRadioButton(RadioButton button) @@ -40,19 +39,6 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons Action = button.Select; RelativeSizeAxes = Axes.X; - - bubble = new CircularContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - FillMode = FillMode.Fit, - Scale = new Vector2(0.5f), - X = 10, - Masking = true, - Blending = BlendingParameters.Additive, - Child = new Box { RelativeSizeAxes = Axes.Both } - }; } [BackgroundDependencyLoader] @@ -73,7 +59,14 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons Colour = Color4.Black.Opacity(0.5f) }; - Add(bubble); + Add(icon = (button.CreateIcon?.Invoke() ?? new Circle()).With(b => + { + b.Blending = BlendingParameters.Additive; + b.Anchor = Anchor.CentreLeft; + b.Origin = Anchor.CentreLeft; + b.Size = new Vector2(20); + b.X = 10; + })); } protected override void LoadComplete() @@ -96,7 +89,7 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons return; BackgroundColour = button.Selected.Value ? selectedBackgroundColour : defaultBackgroundColour; - bubble.Colour = button.Selected.Value ? selectedBubbleColour : defaultBubbleColour; + icon.Colour = button.Selected.Value ? selectedBubbleColour : defaultBubbleColour; } protected override SpriteText CreateText() => new OsuSpriteText diff --git a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs index b515d7c8bd..a7b0fb05e3 100644 --- a/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs +++ b/osu.Game/Screens/Edit/Components/RadioButtons/RadioButton.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Bindables; +using osu.Framework.Graphics; namespace osu.Game.Screens.Edit.Components.RadioButtons { @@ -19,11 +20,17 @@ namespace osu.Game.Screens.Edit.Components.RadioButtons /// public object Item; + /// + /// A function which creates a drawable icon to represent this item. If null, a sane default should be used. + /// + public readonly Func CreateIcon; + private readonly Action action; - public RadioButton(object item, Action action) + public RadioButton(object item, Action action, Func createIcon = null) { Item = item; + CreateIcon = createIcon; this.action = action; Selected = new BindableBool(); } From a65f564e45b9cedee263ae7016d24b7c1f2928f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 18:39:55 +0900 Subject: [PATCH 3153/6909] Add icons for other ruleset editors --- osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs | 4 ++++ osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs | 4 ++++ osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs | 4 ++++ osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs | 4 ++++ osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs index 295bf417c4..a5f10ed436 100644 --- a/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.cs +++ b/osu.Game.Rulesets.Mania/Edit/HoldNoteCompositionTool.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.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mania.Edit.Blueprints; @@ -14,6 +16,8 @@ namespace osu.Game.Rulesets.Mania.Edit { } + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); + public override PlacementBlueprint CreatePlacementBlueprint() => new HoldNotePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs index 50b5f9a8fe..9f54152596 100644 --- a/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.cs +++ b/osu.Game.Rulesets.Mania/Edit/NoteCompositionTool.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.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mania.Edit.Blueprints; @@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Mania.Edit { } + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); + public override PlacementBlueprint CreatePlacementBlueprint() => new NotePlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs index bf77c76670..587a4efecb 100644 --- a/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/DrumRollCompositionTool.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.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Taiko.Edit.Blueprints; @@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit { } + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Sliders); + public override PlacementBlueprint CreatePlacementBlueprint() => new DrumRollPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs index e877cf6240..3e97b4e322 100644 --- a/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/HitCompositionTool.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.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Taiko.Edit.Blueprints; @@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit { } + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Circles); + public override PlacementBlueprint CreatePlacementBlueprint() => new HitPlacementBlueprint(); } } diff --git a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs index a6191fcedc..918afde1dd 100644 --- a/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.cs +++ b/osu.Game.Rulesets.Taiko/Edit/SwellCompositionTool.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.Game.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Taiko.Edit.Blueprints; @@ -15,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Edit { } + public override Drawable CreateIcon() => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Spinners); + public override PlacementBlueprint CreatePlacementBlueprint() => new SwellPlacementBlueprint(); } } From d3957e6155de4871e74d41fc7efe91b6eda53d6e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 18:48:02 +0900 Subject: [PATCH 3154/6909] Move title specification for settings groups to constructor Using an abstract property was awkward for this as it is being consumed in the underlying constructor but could not be dynamically set in time from a derived class. --- .../Gameplay/TestSceneReplaySettingsOverlay.cs | 5 ++++- .../Ladder/Components/LadderEditorSettings.cs | 7 +++++-- osu.Game/Rulesets/Edit/ToolboxGroup.cs | 3 +-- .../Play/PlayerSettings/CollectionSettings.cs | 5 ++++- .../Play/PlayerSettings/DiscussionSettings.cs | 5 ++++- .../Screens/Play/PlayerSettings/InputSettings.cs | 3 +-- .../Screens/Play/PlayerSettings/PlaybackSettings.cs | 3 +-- .../Play/PlayerSettings/PlayerSettingsGroup.cs | 13 ++++++------- .../Screens/Play/PlayerSettings/VisualSettings.cs | 3 +-- 9 files changed, 27 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs index cdfb3beb19..f8fab784cc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplaySettingsOverlay.cs @@ -48,7 +48,10 @@ namespace osu.Game.Tests.Visual.Gameplay private class ExampleContainer : PlayerSettingsGroup { - protected override string Title => @"example"; + public ExampleContainer() + : base("example") + { + } } } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs index fa530ea2c4..b60eb814e5 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs @@ -20,8 +20,6 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { private const int padding = 10; - protected override string Title => @"ladder"; - private SettingsDropdown roundDropdown; private PlayerCheckbox losersCheckbox; private DateTextBox dateTimeBox; @@ -34,6 +32,11 @@ namespace osu.Game.Tournament.Screens.Ladder.Components [Resolved] private LadderInfo ladderInfo { get; set; } + public LadderEditorSettings() + : base("ladder") + { + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Rulesets/Edit/ToolboxGroup.cs b/osu.Game/Rulesets/Edit/ToolboxGroup.cs index eabb834616..7e17d88e17 100644 --- a/osu.Game/Rulesets/Edit/ToolboxGroup.cs +++ b/osu.Game/Rulesets/Edit/ToolboxGroup.cs @@ -8,9 +8,8 @@ namespace osu.Game.Rulesets.Edit { public class ToolboxGroup : PlayerSettingsGroup { - protected override string Title => "toolbox"; - public ToolboxGroup() + : base("toolbox") { RelativeSizeAxes = Axes.X; Width = 1; diff --git a/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs b/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs index d3570a8d2d..9e7f8e7394 100644 --- a/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs @@ -10,7 +10,10 @@ namespace osu.Game.Screens.Play.PlayerSettings { public class CollectionSettings : PlayerSettingsGroup { - protected override string Title => @"collections"; + public CollectionSettings() + : base("collections") + { + } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs b/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs index bb4eea47ca..ac040774ee 100644 --- a/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/DiscussionSettings.cs @@ -10,7 +10,10 @@ namespace osu.Game.Screens.Play.PlayerSettings { public class DiscussionSettings : PlayerSettingsGroup { - protected override string Title => @"discussions"; + public DiscussionSettings() + : base("discussions") + { + } [BackgroundDependencyLoader] private void load(OsuConfigManager config) diff --git a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs index 7a8696e27c..725a6e86bf 100644 --- a/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/InputSettings.cs @@ -9,11 +9,10 @@ namespace osu.Game.Screens.Play.PlayerSettings { public class InputSettings : PlayerSettingsGroup { - protected override string Title => "Input settings"; - private readonly PlayerCheckbox mouseButtonsCheckbox; public InputSettings() + : base("Input Settings") { Children = new Drawable[] { diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index c691d161ed..24ddc277cd 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -13,8 +13,6 @@ namespace osu.Game.Screens.Play.PlayerSettings { private const int padding = 10; - protected override string Title => @"playback"; - public readonly Bindable UserPlaybackRate = new BindableDouble(1) { Default = 1, @@ -28,6 +26,7 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly OsuSpriteText multiplierText; public PlaybackSettings() + : base("playback") { Children = new Drawable[] { diff --git a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs index 90424ec007..7928d41e3b 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlayerSettingsGroup.cs @@ -17,11 +17,6 @@ namespace osu.Game.Screens.Play.PlayerSettings { public abstract class PlayerSettingsGroup : Container { - /// - /// The title to be displayed in the header of this group. - /// - protected abstract string Title { get; } - private const float transition_duration = 250; private const int container_width = 270; private const int border_thickness = 2; @@ -58,7 +53,11 @@ namespace osu.Game.Screens.Play.PlayerSettings private Color4 expandedColour; - protected PlayerSettingsGroup() + /// + /// Create a new instance. + /// + /// The title to be displayed in the header of this group. + protected PlayerSettingsGroup(string title) { AutoSizeAxes = Axes.Y; Width = container_width; @@ -95,7 +94,7 @@ namespace osu.Game.Screens.Play.PlayerSettings { Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, - Text = Title.ToUpperInvariant(), + Text = title.ToUpperInvariant(), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 17), Margin = new MarginPadding { Left = 10 }, }, diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index d6c66d0751..e06cf5c6d5 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -10,8 +10,6 @@ namespace osu.Game.Screens.Play.PlayerSettings { public class VisualSettings : PlayerSettingsGroup { - protected override string Title => "Visual settings"; - private readonly PlayerSliderBar dimSliderBar; private readonly PlayerSliderBar blurSliderBar; private readonly PlayerCheckbox showStoryboardToggle; @@ -19,6 +17,7 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerCheckbox beatmapHitsoundsToggle; public VisualSettings() + : base("Visual Settings") { Children = new Drawable[] { From fb2aced3ac32d4e312913e410557885700c85933 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 19:14:28 +0900 Subject: [PATCH 3155/6909] Add toggle for distance snap --- .../Edit/OsuHitObjectComposer.cs | 13 +++++++++++++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 18 +++++++++++++++++- osu.Game/Rulesets/Edit/ToolboxGroup.cs | 4 ++-- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 37019a7a05..f87bd53ec3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.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.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -38,6 +39,13 @@ namespace osu.Game.Rulesets.Osu.Edit new SpinnerCompositionTool() }; + private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" }; + + protected override IEnumerable Toggles => new[] + { + distanceSnapToggle + }; + [BackgroundDependencyLoader] private void load() { @@ -45,6 +53,7 @@ namespace osu.Game.Rulesets.Osu.Edit EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid(); + distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); } protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) @@ -87,6 +96,10 @@ namespace osu.Game.Rulesets.Osu.Edit { distanceSnapGridContainer.Clear(); distanceSnapGridCache.Invalidate(); + distanceSnapGrid = null; + + if (!distanceSnapToggle.Value) + return; switch (BlueprintContainer.CurrentTool) { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index f134db1ffe..ee42cd9bae 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -13,6 +14,7 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -94,7 +96,15 @@ namespace osu.Game.Rulesets.Edit Padding = new MarginPadding { Right = 10 }, Children = new Drawable[] { - new ToolboxGroup { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } } + new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }, + new ToolboxGroup("toggles") + { + ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox + { + Bindable = b, + LabelText = b?.Description ?? "unknown" + }) + } } }, new Container @@ -156,6 +166,12 @@ namespace osu.Game.Rulesets.Edit /// protected abstract IReadOnlyList CompositionTools { get; } + /// + /// A collection of toggles which will be displayed to the user. + /// The display name will be decided by . + /// + protected virtual IEnumerable Toggles => Enumerable.Empty(); + /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// diff --git a/osu.Game/Rulesets/Edit/ToolboxGroup.cs b/osu.Game/Rulesets/Edit/ToolboxGroup.cs index 7e17d88e17..22b2b05657 100644 --- a/osu.Game/Rulesets/Edit/ToolboxGroup.cs +++ b/osu.Game/Rulesets/Edit/ToolboxGroup.cs @@ -8,8 +8,8 @@ namespace osu.Game.Rulesets.Edit { public class ToolboxGroup : PlayerSettingsGroup { - public ToolboxGroup() - : base("toolbox") + public ToolboxGroup(string title) + : base(title) { RelativeSizeAxes = Axes.X; Width = 1; From d210e056294b8bd92e9828e6a7c30c3ae960d239 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 19:20:11 +0900 Subject: [PATCH 3156/6909] Add a touch of spacing between toolbox groups --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index ee42cd9bae..928cdd2ea0 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -94,6 +94,7 @@ namespace osu.Game.Rulesets.Edit Name = "Sidebar", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Right = 10 }, + Spacing = new Vector2(10), Children = new Drawable[] { new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }, From ac0c4fcb8c2bfb162b820c9b03a128304fe31d0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 19:31:18 +0900 Subject: [PATCH 3157/6909] Add prompt to save beatmap on exiting editor --- osu.Game/Screens/Edit/Editor.cs | 57 ++++++++++++++------ osu.Game/Screens/Edit/PromptForSaveDialog.cs | 33 ++++++++++++ 2 files changed, 74 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Screens/Edit/PromptForSaveDialog.cs diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ac1f61c4fd..7e17225846 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -2,39 +2,40 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK.Graphics; -using osu.Framework.Screens; +using System.Collections.Generic; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics; -using osu.Game.Screens.Edit.Components.Timelines.Summary; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Framework.Platform; -using osu.Framework.Timing; -using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Edit.Components; -using osu.Game.Screens.Edit.Components.Menus; -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.Input.Events; using osu.Framework.Logging; +using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.Cursor; +using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.API; +using osu.Game.Overlays; using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components; +using osu.Game.Screens.Edit.Components.Menus; +using osu.Game.Screens.Edit.Components.Timelines.Summary; using osu.Game.Screens.Edit.Compose; +using osu.Game.Screens.Edit.Design; using osu.Game.Screens.Edit.Setup; using osu.Game.Screens.Edit.Timing; using osu.Game.Screens.Play; using osu.Game.Users; +using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Screens.Edit { @@ -54,6 +55,11 @@ namespace osu.Game.Screens.Edit [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved(canBeNull: true)] + private DialogOverlay dialogOverlay { get; set; } + + private bool exitConfirmed; + private Box bottomBackground; private Container screenContainer; @@ -346,12 +352,31 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { + if (!exitConfirmed && dialogOverlay != null) + { + dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); + return true; + } + Background.FadeColour(Color4.White, 500); resetTrack(); return base.OnExiting(next); } + private void confirmExitWithSave() + { + exitConfirmed = true; + saveBeatmap(); + this.Exit(); + } + + private void confirmExit() + { + exitConfirmed = true; + this.Exit(); + } + protected void Undo() => changeHandler.RestoreState(-1); protected void Redo() => changeHandler.RestoreState(1); diff --git a/osu.Game/Screens/Edit/PromptForSaveDialog.cs b/osu.Game/Screens/Edit/PromptForSaveDialog.cs new file mode 100644 index 0000000000..38d956557d --- /dev/null +++ b/osu.Game/Screens/Edit/PromptForSaveDialog.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 osu.Framework.Graphics.Sprites; +using osu.Game.Overlays.Dialog; + +namespace osu.Game.Screens.Edit +{ + public class PromptForSaveDialog : PopupDialog + { + public PromptForSaveDialog(Action exit, Action saveAndExit) + { + HeaderText = "Did you want to save your changes?"; + + Icon = FontAwesome.Regular.Save; + + Buttons = new PopupDialogButton[] + { + new PopupDialogCancelButton + { + Text = @"Save my masterpiece!", + Action = saveAndExit + }, + new PopupDialogOkButton + { + Text = @"Forget all changes", + Action = exit + }, + }; + } + } +} From 6f067ff300910cf82a0cfac72d3578fd73c520d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 19:40:41 +0900 Subject: [PATCH 3158/6909] Only show confirmation if changes have been made since last save --- osu.Game/Screens/Edit/Editor.cs | 13 ++++++++++++- osu.Game/Screens/Edit/EditorChangeHandler.cs | 13 +++++++++++++ 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 7e17225846..58395e4848 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -60,6 +60,8 @@ namespace osu.Game.Screens.Edit private bool exitConfirmed; + private string lastSavedHash; + private Box bottomBackground; private Container screenContainer; @@ -124,6 +126,8 @@ namespace osu.Game.Screens.Edit changeHandler = new EditorChangeHandler(editorBeatmap); dependencies.CacheAs(changeHandler); + updateLastSavedHash(); + EditorMenuBar menuBar; OsuMenuItem undoMenuItem; OsuMenuItem redoMenuItem; @@ -352,7 +356,7 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { - if (!exitConfirmed && dialogOverlay != null) + if (!exitConfirmed && dialogOverlay != null && changeHandler.CurrentStateHash != lastSavedHash) { dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); return true; @@ -447,6 +451,8 @@ namespace osu.Game.Screens.Edit // save the loaded beatmap's data stream. beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin); + + updateLastSavedHash(); } private void exportBeatmap() @@ -455,6 +461,11 @@ namespace osu.Game.Screens.Edit beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); } + private void updateLastSavedHash() + { + lastSavedHash = changeHandler.CurrentStateHash; + } + public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime); diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 927c823c64..aa0f89912a 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Text; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; @@ -24,6 +25,18 @@ namespace osu.Game.Screens.Edit private int currentState = -1; + /// + /// A SHA-2 hash representing the current visible editor state. + /// + public string CurrentStateHash + { + get + { + using (var stream = new MemoryStream(savedStates[currentState])) + return stream.ComputeSHA2Hash(); + } + } + private readonly EditorBeatmap editorBeatmap; private int bulkChangesStarted; private bool isRestoring; From 327179a81efbc9524be1a3a7d0ba1d54a3e46dff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 19:42:03 +0900 Subject: [PATCH 3159/6909] Expose unsaved changes state --- osu.Game/Screens/Edit/Editor.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 58395e4848..34c69d09e0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -52,6 +52,8 @@ namespace osu.Game.Screens.Edit public override bool AllowRateAdjustments => false; + public bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash; + [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -356,7 +358,7 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { - if (!exitConfirmed && dialogOverlay != null && changeHandler.CurrentStateHash != lastSavedHash) + if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges) { dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); return true; From c6e72dabd372c35a589e2b5c23220e5478b43536 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 19:57:28 +0900 Subject: [PATCH 3160/6909] Add test coverage --- .../Editing/TestSceneEditorChangeStates.cs | 29 +++++++++++++++-- osu.Game/Screens/Edit/Editor.cs | 32 +++++++++---------- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 1 + 3 files changed, 43 insertions(+), 19 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs index 293a6e6869..c8a32d966f 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs @@ -18,6 +18,8 @@ namespace osu.Game.Tests.Visual.Editing protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + protected new TestEditor Editor => (TestEditor)base.Editor; + public override void SetUpSteps() { base.SetUpSteps(); @@ -35,6 +37,7 @@ namespace osu.Game.Tests.Visual.Editing addUndoSteps(); AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } [Test] @@ -47,6 +50,7 @@ namespace osu.Game.Tests.Visual.Editing addRedoSteps(); AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } [Test] @@ -64,9 +68,11 @@ namespace osu.Game.Tests.Visual.Editing AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); AddAssert("hitobject added", () => addedObject == expectedObject); + AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); addUndoSteps(); AddAssert("hitobject removed", () => removedObject == expectedObject); + AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } [Test] @@ -94,6 +100,17 @@ namespace osu.Game.Tests.Visual.Editing addRedoSteps(); AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) AddAssert("no hitobject removed", () => removedObject == null); + AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); + } + + [Test] + public void TestAddObjectThenSaveHasNoUnsavedChanges() + { + AddStep("add hitobject", () => editorBeatmap.Add(new HitCircle { StartTime = 1000 })); + + AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); + AddStep("save changes", () => Editor.Save()); + AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } [Test] @@ -120,6 +137,7 @@ namespace osu.Game.Tests.Visual.Editing addUndoSteps(); AddAssert("hitobject added", () => addedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance) AddAssert("no hitobject removed", () => removedObject == null); + AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); // 2 steps performed, 1 undone } [Test] @@ -148,19 +166,24 @@ namespace osu.Game.Tests.Visual.Editing addRedoSteps(); AddAssert("hitobject removed", () => removedObject.StartTime == expectedObject.StartTime); // Can't compare via equality (new hitobject instance after undo) AddAssert("no hitobject added", () => addedObject == null); + AddAssert("no changes", () => !Editor.HasUnsavedChanges); // end result is empty beatmap, matching original state } - private void addUndoSteps() => AddStep("undo", () => ((TestEditor)Editor).Undo()); + private void addUndoSteps() => AddStep("undo", () => Editor.Undo()); - private void addRedoSteps() => AddStep("redo", () => ((TestEditor)Editor).Redo()); + private void addRedoSteps() => AddStep("redo", () => Editor.Redo()); protected override Editor CreateEditor() => new TestEditor(); - private class TestEditor : Editor + protected class TestEditor : Editor { public new void Undo() => base.Undo(); public new void Redo() => base.Redo(); + + public new void Save() => base.Save(); + + public new bool HasUnsavedChanges => base.HasUnsavedChanges; } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 34c69d09e0..23eb704920 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit public override bool AllowRateAdjustments => false; - public bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash; + protected bool HasUnsavedChanges => lastSavedHash != changeHandler.CurrentStateHash; [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Edit var fileMenuItems = new List { - new EditorMenuItem("Save", MenuItemType.Standard, saveBeatmap) + new EditorMenuItem("Save", MenuItemType.Standard, Save) }; if (RuntimeInfo.IsDesktop) @@ -249,6 +249,17 @@ namespace osu.Game.Screens.Edit bottomBackground.Colour = colours.Gray2; } + protected void Save() + { + // apply any set-level metadata changes. + beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); + + // save the loaded beatmap's data stream. + beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin); + + updateLastSavedHash(); + } + protected override void Update() { base.Update(); @@ -268,7 +279,7 @@ namespace osu.Game.Screens.Edit return true; case PlatformActionType.Save: - saveBeatmap(); + Save(); return true; } @@ -373,7 +384,7 @@ namespace osu.Game.Screens.Edit private void confirmExitWithSave() { exitConfirmed = true; - saveBeatmap(); + Save(); this.Exit(); } @@ -446,20 +457,9 @@ namespace osu.Game.Screens.Edit clock.SeekForward(!clock.IsRunning, amount); } - private void saveBeatmap() - { - // apply any set-level metadata changes. - beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); - - // save the loaded beatmap's data stream. - beatmapManager.Save(playableBeatmap.BeatmapInfo, editorBeatmap, editorBeatmap.BeatmapSkin); - - updateLastSavedHash(); - } - private void exportBeatmap() { - saveBeatmap(); + Save(); beatmapManager.Export(Beatmap.Value.BeatmapSetInfo); } diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 9fc20fd0f2..a375a17bcf 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tests.Beatmaps BeatmapInfo.Ruleset = ruleset; BeatmapInfo.RulesetID = ruleset.ID ?? 0; BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; + BeatmapInfo.BeatmapSet.Files = new List(); BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo }; BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo { From 1803ecad8001db57c39b62fbab2ecacc07d7a9fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Sep 2020 20:00:38 +0900 Subject: [PATCH 3161/6909] Add cancel exit button --- osu.Game/Screens/Edit/PromptForSaveDialog.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/PromptForSaveDialog.cs b/osu.Game/Screens/Edit/PromptForSaveDialog.cs index 38d956557d..16504b47bd 100644 --- a/osu.Game/Screens/Edit/PromptForSaveDialog.cs +++ b/osu.Game/Screens/Edit/PromptForSaveDialog.cs @@ -27,6 +27,10 @@ namespace osu.Game.Screens.Edit Text = @"Forget all changes", Action = exit }, + new PopupDialogCancelButton + { + Text = @"Oops, continue editing", + }, }; } } From aeae009512aad6f33a7ba5b4a54b161460cc3ead Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 20:11:29 +0900 Subject: [PATCH 3162/6909] Disable online beatmap lookups in tests --- osu.Game/Beatmaps/BeatmapManager.cs | 12 ++++++++---- osu.Game/OsuGameBase.cs | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 34bb578b2a..d53f85c68d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -66,12 +66,14 @@ namespace osu.Game.Beatmaps private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; private readonly AudioManager audioManager; - private readonly BeatmapOnlineLookupQueue onlineLookupQueue; private readonly TextureStore textureStore; private readonly ITrackStore trackStore; + [CanBeNull] + private readonly BeatmapOnlineLookupQueue onlineLookupQueue; + public BeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, GameHost host = null, - WorkingBeatmap defaultBeatmap = null) + WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) : base(storage, contextFactory, api, new BeatmapStore(contextFactory), host) { this.rulesets = rulesets; @@ -85,7 +87,8 @@ namespace osu.Game.Beatmaps beatmaps.ItemRemoved += removeWorkingCache; beatmaps.ItemUpdated += removeWorkingCache; - onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); + if (performOnlineLookups) + onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); textureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); trackStore = audioManager.GetTrackStore(Files.Store); @@ -142,7 +145,8 @@ namespace osu.Game.Beatmaps bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); - await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken); + if (onlineLookupQueue != null) + 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/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4bc8f4c527..b61017f038 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -198,7 +198,7 @@ namespace osu.Game // ordering is important here to ensure foreign keys rules are not broken in ModelStore.Cleanup() dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host)); - dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap)); + dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap, true)); // 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 From bbef7ff720c91dce79839d9d3182fbc712ec388b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 20:19:07 +0900 Subject: [PATCH 3163/6909] Fix leaderboard loading spinner disappearing too early --- osu.Game/Online/Leaderboards/Leaderboard.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index db0f835c67..084ba89f6e 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -56,13 +56,14 @@ namespace osu.Game.Online.Leaderboards scrollFlow?.FadeOut(fade_duration, Easing.OutQuint).Expire(); scrollFlow = null; - loading.Hide(); - showScoresDelegate?.Cancel(); showScoresCancellationSource?.Cancel(); if (scores == null || !scores.Any()) + { + loading.Hide(); return; + } // ensure placeholder is hidden when displaying scores PlaceholderState = PlaceholderState.Successful; @@ -84,6 +85,7 @@ namespace osu.Game.Online.Leaderboards } scrollContainer.ScrollTo(0f, false); + loading.Hide(); }, (showScoresCancellationSource = new CancellationTokenSource()).Token)); } } From 12188ec3c931bc30e5bdb7e1c99f988027dbc33b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 20:49:59 +0900 Subject: [PATCH 3164/6909] Fix broken RollingCounter current value --- osu.Game/Graphics/UserInterface/RollingCounter.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index ece1b8e22c..91a557094d 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -9,16 +9,20 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterface { - public abstract class RollingCounter : Container + public abstract class RollingCounter : Container, IHasCurrentValue where T : struct, IEquatable { - /// - /// The current value. - /// - public Bindable Current = new Bindable(); + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } private SpriteText displayedCountSpriteText; From d7ca2cf1cca15da8fe476dfd4292d5bbd77cd167 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 22:01:09 +0900 Subject: [PATCH 3165/6909] Replace loaded check with better variation --- .../Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 2fd522dc9d..3a842d0a43 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -120,7 +120,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores statisticsColumns.ChildrenEnumerable = value.SortedStatistics.Select(kvp => createStatisticsColumn(kvp.Key, kvp.Value)); modsColumn.Mods = value.Mods; - if (IsLoaded) + if (scoreManager != null) totalScoreColumn.Current = scoreManager.GetBindableTotalScoreString(value); } } From 43525614adefa151a20805225646b4421c9cbd3c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 23:10:21 +0900 Subject: [PATCH 3166/6909] Store raw BeatmapCollection in filter control --- osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs | 2 +- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/FilterControl.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 6012150513..f89f22bf23 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -204,7 +204,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.CollectionName.Value == "1"); + AddAssert("collection filter still selected", () => control.CreateCriteria().Collection?.Name.Value == "1"); } private void assertCollectionHeaderDisplays(string collectionName, bool shouldDisplay = true) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 8e5655e514..3892e02a8f 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 } if (match) - match &= criteria.Collection?.ContainsBeatmap(Beatmap) ?? true; + match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true; Filtered.Value = !match; } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 41ce0d65cd..9128160608 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Select Sort = sortMode.Value, AllowConvertedBeatmaps = showConverted.Value, Ruleset = ruleset.Value, - Collection = collectionDropdown?.Current.Value + Collection = collectionDropdown?.Current.Value.Collection }; if (!minimumStars.IsDefault) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index acab982945..66f164bca8 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Game.Beatmaps; +using osu.Game.Collections; using osu.Game.Rulesets; using osu.Game.Screens.Select.Filter; @@ -56,7 +57,7 @@ namespace osu.Game.Screens.Select /// The collection to filter beatmaps from. /// [CanBeNull] - public CollectionFilter Collection; + public BeatmapCollection Collection; public struct OptionalRange : IEquatable> where T : struct From 6b56c6e83ff29b2c375af41c69ed89c344d4c72a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 9 Sep 2020 23:11:19 +0900 Subject: [PATCH 3167/6909] Rename to CollectionMenuItem --- .../SongSelect/TestSceneFilterControl.cs | 4 ++-- .../Select/CollectionFilterDropdown.cs | 24 +++++++++---------- ...lectionFilter.cs => CollectionMenuItem.cs} | 24 ++++++------------- 3 files changed, 21 insertions(+), 31 deletions(-) rename osu.Game/Screens/Select/{CollectionFilter.cs => CollectionMenuItem.cs} (60%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index f89f22bf23..23feb1466e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -231,7 +231,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - private IEnumerable.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems() - => control.ChildrenOfType().Single().ChildrenOfType.DropdownMenu.DrawableDropdownMenuItem>(); + private IEnumerable.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems() + => control.ChildrenOfType().Single().ChildrenOfType.DropdownMenu.DrawableDropdownMenuItem>(); } } diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Screens/Select/CollectionFilterDropdown.cs index 1e2a3d0aa7..b08c3b167d 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Screens/Select/CollectionFilterDropdown.cs @@ -21,13 +21,13 @@ using osuTK; namespace osu.Game.Screens.Select { /// - /// A dropdown to select the to filter beatmaps using. + /// A dropdown to select the to filter beatmaps using. /// - public class CollectionFilterDropdown : OsuDropdown + public class CollectionFilterDropdown : OsuDropdown { private readonly IBindableList collections = new BindableList(); private readonly IBindableList beatmaps = new BindableList(); - private readonly BindableList filters = new BindableList(); + private readonly BindableList filters = new BindableList(); [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } @@ -62,17 +62,17 @@ namespace osu.Game.Screens.Select var selectedItem = SelectedItem?.Value?.Collection; filters.Clear(); - filters.Add(new AllBeatmapCollectionFilter()); - filters.AddRange(collections.Select(c => new CollectionFilter(c))); - filters.Add(new ManageCollectionsFilter()); + filters.Add(new AllBeatmapsCollectionMenuItem()); + filters.AddRange(collections.Select(c => new CollectionMenuItem(c))); + filters.Add(new ManageCollectionsMenuItem()); Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection == selectedItem) ?? filters[0]; } /// - /// Occurs when the selection has changed. + /// Occurs when the selection has changed. /// - private void filterChanged(ValueChangedEvent filter) + private void filterChanged(ValueChangedEvent filter) { // Binding the beatmaps will trigger a collection change event, which results in an infinite-loop. This is rebound later, when it's safe to do so. beatmaps.CollectionChanged -= filterBeatmapsChanged; @@ -87,7 +87,7 @@ namespace osu.Game.Screens.Select // Never select the manage collection filter - rollback to the previous filter. // This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value. - if (filter.NewValue is ManageCollectionsFilter) + if (filter.NewValue is ManageCollectionsMenuItem) { Current.Value = filter.OldValue; manageCollectionsDialog?.Show(); @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Select Current.TriggerChange(); } - protected override string GenerateItemText(CollectionFilter item) => item.CollectionName.Value; + protected override string GenerateItemText(CollectionMenuItem item) => item.CollectionName.Value; protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader { @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Select public class CollectionDropdownHeader : OsuDropdownHeader { - public readonly Bindable SelectedItem = new Bindable(); + public readonly Bindable SelectedItem = new Bindable(); private readonly Bindable collectionName = new Bindable(); protected override string Label @@ -165,7 +165,7 @@ namespace osu.Game.Screens.Select private class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem { [NotNull] - protected new CollectionFilter Item => ((DropdownMenuItem)base.Item).Value; + protected new CollectionMenuItem Item => ((DropdownMenuItem)base.Item).Value; [Resolved] private OsuColour colours { get; set; } diff --git a/osu.Game/Screens/Select/CollectionFilter.cs b/osu.Game/Screens/Select/CollectionMenuItem.cs similarity index 60% rename from osu.Game/Screens/Select/CollectionFilter.cs rename to osu.Game/Screens/Select/CollectionMenuItem.cs index 883019ab06..995651de19 100644 --- a/osu.Game/Screens/Select/CollectionFilter.cs +++ b/osu.Game/Screens/Select/CollectionMenuItem.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.Linq; using JetBrains.Annotations; using osu.Framework.Bindables; -using osu.Game.Beatmaps; using osu.Game.Collections; namespace osu.Game.Screens.Select @@ -12,7 +10,7 @@ namespace osu.Game.Screens.Select /// /// A filter. /// - public class CollectionFilter + public class CollectionMenuItem { /// /// The collection to filter beatmaps from. @@ -28,35 +26,27 @@ namespace osu.Game.Screens.Select public readonly Bindable CollectionName; /// - /// Creates a new . + /// Creates a new . /// /// The collection to filter beatmaps from. - public CollectionFilter([CanBeNull] BeatmapCollection collection) + public CollectionMenuItem([CanBeNull] BeatmapCollection collection) { Collection = collection; CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable("All beatmaps"); } - - /// - /// Whether the collection contains a given beatmap. - /// - /// The beatmap to check. - /// Whether contains . - public virtual bool ContainsBeatmap(BeatmapInfo beatmap) - => Collection?.Beatmaps.Any(b => b.Equals(beatmap)) ?? true; } - public class AllBeatmapCollectionFilter : CollectionFilter + public class AllBeatmapsCollectionMenuItem : CollectionMenuItem { - public AllBeatmapCollectionFilter() + public AllBeatmapsCollectionMenuItem() : base(null) { } } - public class ManageCollectionsFilter : CollectionFilter + public class ManageCollectionsMenuItem : CollectionMenuItem { - public ManageCollectionsFilter() + public ManageCollectionsMenuItem() : base(null) { CollectionName.Value = "Manage collections..."; From df1537f2a03e55aad03e83223ed4656b146cb126 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Sep 2020 18:09:03 +0900 Subject: [PATCH 3168/6909] Update framework --- osu.Android.props | 2 +- osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 62397ca028..a2686c380e 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs index 20adbc1c02..88c855d768 100644 --- a/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs +++ b/osu.Game.Rulesets.Osu/Statistics/AccuracyHeatmap.cs @@ -192,7 +192,7 @@ namespace osu.Game.Rulesets.Osu.Statistics protected void AddPoint(Vector2 start, Vector2 end, Vector2 hitPoint, float radius) { - if (pointGrid.Content.Length == 0) + if (pointGrid.Content.Count == 0) return; double angle1 = Math.Atan2(end.Y - hitPoint.Y, hitPoint.X - end.X); // Angle between the end point and the hit point. diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1de0633d1f..48582ae29d 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 7187b48907..0eed2fa911 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 18d96738a11133e3b82f7d8f3049e8d7d27d1aa4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Sep 2020 19:37:40 +0900 Subject: [PATCH 3169/6909] Fix hard crash on deleting a collection with no collection selected --- osu.Game/Screens/Select/FilterControl.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 9128160608..3f1c88a1e3 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -189,7 +189,15 @@ namespace osu.Game.Screens.Select } }; - collectionDropdown.Current.ValueChanged += _ => updateCriteria(); + collectionDropdown.Current.ValueChanged += val => + { + if (val.NewValue == null) + // may be null briefly while menu is repopulated. + return; + + updateCriteria(); + }; + searchTextBox.Current.ValueChanged += _ => updateCriteria(); updateCriteria(); From 74eea8900bc2015bdb5f33a7e9f8e4f504bc8ce4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 10 Sep 2020 20:00:57 +0900 Subject: [PATCH 3170/6909] Remove unnecessary check for negative durations --- .../Difficulty/TaikoDifficultyCalculator.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index ef43fc6d1e..e5485db4df 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -51,18 +51,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty for (int i = 2; i < beatmap.HitObjects.Count; i++) { - // Check for negative durations - var currentAfterLast = beatmap.HitObjects[i].StartTime > beatmap.HitObjects[i - 1].StartTime; - var lastAfterSecondLast = beatmap.HitObjects[i - 1].StartTime > beatmap.HitObjects[i - 2].StartTime; - - if (currentAfterLast && lastAfterSecondLast) - { - taikoDifficultyHitObjects.Add( - new TaikoDifficultyHitObject( - beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i - ) - ); - } + taikoDifficultyHitObjects.Add( + new TaikoDifficultyHitObject( + beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, i + ) + ); } new StaminaCheeseDetector(taikoDifficultyHitObjects).FindCheese(); From 314cd13b7446c5497ff2e2b0a331a3d744ffbb52 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 10 Sep 2020 23:36:22 +0900 Subject: [PATCH 3171/6909] Fix song select filter ordering --- osu.Game/Screens/Select/FilterControl.cs | 132 +++++++++++------------ 1 file changed, 62 insertions(+), 70 deletions(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 3f1c88a1e3..079667a457 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; @@ -98,89 +99,80 @@ namespace osu.Game.Screens.Select Width = 0.5f, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Child = new GridContainer + // Reverse ChildID so that dropdowns in the top section appear on top of the bottom section. + Child = new ReverseChildIDFillFlowContainer { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Spacing = new Vector2(0, 5), + Children = new[] { - new Dimension(GridSizeMode.Absolute, 60), - new Dimension(GridSizeMode.Absolute, 5), - new Dimension(GridSizeMode.Absolute, 20), - }, - Content = new[] - { - new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.X, + Height = 60, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + searchTextBox = new SeekLimitedSearchTextBox { RelativeSizeAxes = Axes.X }, + new Box { - searchTextBox = new SeekLimitedSearchTextBox { RelativeSizeAxes = Axes.X }, - new Box + RelativeSizeAxes = Axes.X, + Height = 1, + Colour = OsuColour.Gray(80), + Origin = Anchor.BottomLeft, + Anchor = Anchor.BottomLeft, + }, + new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Direction = FillDirection.Horizontal, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(OsuTabControl.HORIZONTAL_SPACING, 0), + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - Height = 1, - Colour = OsuColour.Gray(80), - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - }, - new FillFlowContainer - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Direction = FillDirection.Horizontal, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(OsuTabControl.HORIZONTAL_SPACING, 0), - Children = new Drawable[] + new OsuTabControlCheckbox { - new OsuTabControlCheckbox - { - Text = "Show converted", - Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - }, - 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, - }, - } - }, - } + Text = "Show converted", + Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + 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, + }, + } + }, } }, - null, - new Drawable[] + new Container { - new Container + RelativeSizeAxes = Axes.X, + Height = 20, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + collectionDropdown = new CollectionFilterDropdown { - collectionDropdown = new CollectionFilterDropdown - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - RelativeSizeAxes = Axes.X, - Width = 0.4f, - } + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + Width = 0.4f, } } }, From 447fd07b4ed4b7bf7f71862946ad975d8c07526f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Sep 2020 01:13:55 +0900 Subject: [PATCH 3172/6909] Fix maps with only bonus score having NaN scores --- .../Gameplay/TestSceneScoreProcessor.cs | 26 +++++++++++++++++++ osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 8 +++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index b0baf0385e..c9ab4fa489 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -28,6 +28,20 @@ namespace osu.Game.Tests.Gameplay Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0)); } + [Test] + public void TestOnlyBonusScore() + { + var beatmap = new Beatmap { HitObjects = { new TestBonusHitObject() } }; + + var scoreProcessor = new ScoreProcessor(); + scoreProcessor.ApplyBeatmap(beatmap); + + // Apply a judgement + scoreProcessor.ApplyResult(new JudgementResult(new TestBonusHitObject(), new TestBonusJudgement()) { Type = HitResult.Perfect }); + + Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(100)); + } + private class TestHitObject : HitObject { public override Judgement CreateJudgement() => new TestJudgement(); @@ -37,5 +51,17 @@ namespace osu.Game.Tests.Gameplay { protected override int NumericResultFor(HitResult result) => 100; } + + private class TestBonusHitObject : HitObject + { + public override Judgement CreateJudgement() => new TestBonusJudgement(); + } + + private class TestBonusJudgement : Judgement + { + public override bool AffectsCombo => false; + + protected override int NumericResultFor(HitResult result) => 100; + } } } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 983f9a3abf..6fa5a87c8e 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -202,7 +202,13 @@ namespace osu.Game.Rulesets.Scoring TotalScore.Value = getScore(Mode.Value); } - private double getScore(ScoringMode mode) => GetScore(mode, maxHighestCombo, baseScore / maxBaseScore, (double)HighestCombo.Value / maxHighestCombo, bonusScore); + private double getScore(ScoringMode mode) + { + return GetScore(mode, maxHighestCombo, + maxBaseScore > 0 ? baseScore / maxBaseScore : 0, + maxHighestCombo > 0 ? (double)HighestCombo.Value / maxHighestCombo : 0, + bonusScore); + } /// /// Computes the total score. From 6e5c5ab9015e9b98ab52e344d38e5f97ffe57d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Sep 2020 18:22:49 +0200 Subject: [PATCH 3173/6909] Fix invalid initial value of currentMonoLength --- osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 9fad83c6a1..ecd74f54ed 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Length of the current mono pattern. /// - private int currentMonoLength = 1; + private int currentMonoLength; protected override double StrainValueOf(DifficultyHitObject current) { From 9b504272e41e9e6fadcb315eb732c05c9a458c0b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 10 Sep 2020 20:24:43 +0300 Subject: [PATCH 3174/6909] Make Header a property --- .../Overlays/Profile/Sections/PaginatedContainerWithHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs index 32c589e342..5e175a2203 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Profile.Sections private readonly string headerText; private readonly CounterVisibilityState counterVisibilityState; - protected PaginatedContainerHeader Header; + protected PaginatedContainerHeader Header { get; private set; } protected PaginatedContainerWithHeader(Bindable user, string headerText, CounterVisibilityState counterVisibilityState, string missing = "") : base(user, missing) From 931e567c7e809401526f3dda607ace733c07212c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 10 Sep 2020 20:25:35 +0300 Subject: [PATCH 3175/6909] Replace counter font size with an actual value --- osu.Game/Overlays/Profile/Sections/CounterPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/CounterPill.cs b/osu.Game/Overlays/Profile/Sections/CounterPill.cs index 131df105ad..ca8abcfe5a 100644 --- a/osu.Game/Overlays/Profile/Sections/CounterPill.cs +++ b/osu.Game/Overlays/Profile/Sections/CounterPill.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.Profile.Sections Anchor = Anchor.Centre, Origin = Anchor.Centre, Margin = new MarginPadding { Horizontal = 10, Bottom = 1 }, - Font = OsuFont.GetFont(size: 14 * 0.8f, weight: FontWeight.Bold), + Font = OsuFont.GetFont(size: 11.2f, weight: FontWeight.Bold), Colour = colourProvider.Foreground1 } }; From e5f70d8eae57ae2892130730cb86ec2fdb990087 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 10 Sep 2020 20:31:00 +0300 Subject: [PATCH 3176/6909] Simplify counter visibility changes in PaginatedContainerHeader --- .../Sections/PaginatedContainerHeader.cs | 57 ++++++++----------- 1 file changed, 24 insertions(+), 33 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs index 4779b44eb0..8c617e5fbd 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs @@ -16,21 +16,14 @@ namespace osu.Game.Overlays.Profile.Sections { public class PaginatedContainerHeader : CompositeDrawable, IHasCurrentValue { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + public Bindable Current { - get => current; - set - { - if (value == null) - throw new ArgumentNullException(nameof(value)); - - current.UnbindBindings(); - current.BindTo(value); - } + get => current.Current; + set => current.Current = value; } - private readonly Bindable current = new Bindable(); - private readonly string text; private readonly CounterVisibilityState counterState; @@ -82,7 +75,6 @@ namespace osu.Game.Overlays.Profile.Sections { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Alpha = getInitialCounterAlpha(), Current = { BindTarget = current } } } @@ -93,33 +85,32 @@ namespace osu.Game.Overlays.Profile.Sections protected override void LoadComplete() { base.LoadComplete(); - current.BindValueChanged(onCurrentChanged); - } - - private float getInitialCounterAlpha() - { - switch (counterState) - { - case CounterVisibilityState.AlwaysHidden: - return 0; - - case CounterVisibilityState.AlwaysVisible: - return 1; - - case CounterVisibilityState.VisibleWhenZero: - return current.Value == 0 ? 1 : 0; - - default: - throw new NotImplementedException($"{counterState} has an incorrect value."); - } + current.BindValueChanged(onCurrentChanged, true); } private void onCurrentChanged(ValueChangedEvent countValue) { - if (counterState == CounterVisibilityState.VisibleWhenZero) + float alpha; + + switch (counterState) { - counterPill.Alpha = countValue.NewValue == 0 ? 1 : 0; + case CounterVisibilityState.AlwaysHidden: + alpha = 0; + break; + + case CounterVisibilityState.AlwaysVisible: + alpha = 1; + break; + + case CounterVisibilityState.VisibleWhenZero: + alpha = current.Value == 0 ? 1 : 0; + break; + + default: + throw new NotImplementedException($"{counterState} has an incorrect value."); } + + counterPill.Alpha = alpha; } } From 6c9fcae69f3f55c6b23d8fbcc47c41df3aa5d88c Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 10 Sep 2020 10:48:00 -0700 Subject: [PATCH 3177/6909] Fix drag handles not showing on now playing playlist items --- osu.Game/Overlays/Music/PlaylistItem.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 840fa51b4f..12f7c7e09d 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -39,6 +39,8 @@ namespace osu.Game.Overlays.Music Padding = new MarginPadding { Left = 5 }; FilterTerms = item.Metadata.SearchableTerms; + + ShowDragHandle.Value = true; } [BackgroundDependencyLoader] From 913e3faf606130e9c1f4eb6308a403357e6b5ff1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 10 Sep 2020 20:48:06 +0300 Subject: [PATCH 3178/6909] Move PaginatedContainerWithHeader logic to a base class --- .../Beatmaps/PaginatedBeatmapContainer.cs | 4 +-- .../PaginatedMostPlayedBeatmapContainer.cs | 4 +-- .../Profile/Sections/PaginatedContainer.cs | 27 +++++++++------ .../Sections/PaginatedContainerWithHeader.cs | 34 ------------------- .../Sections/Ranks/PaginatedScoreContainer.cs | 6 ++-- 5 files changed, 24 insertions(+), 51 deletions(-) delete mode 100644 osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index d7c72131ea..265972bb86 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -14,13 +14,13 @@ using osuTK; namespace osu.Game.Overlays.Profile.Sections.Beatmaps { - public class PaginatedBeatmapContainer : PaginatedContainerWithHeader + public class PaginatedBeatmapContainer : PaginatedContainer { private const float panel_padding = 10f; private readonly BeatmapSetType type; public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, string headerText) - : base(user, headerText, CounterVisibilityState.AlwaysVisible) + : base(user, "", headerText, CounterVisibilityState.AlwaysVisible) { this.type = type; ItemsPerPage = 6; diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index ad35ea1460..8f1d894379 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -13,10 +13,10 @@ using osu.Game.Users; namespace osu.Game.Overlays.Profile.Sections.Historical { - public class PaginatedMostPlayedBeatmapContainer : PaginatedContainerWithHeader + public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer { public PaginatedMostPlayedBeatmapContainer(Bindable user) - : base(user, "Most Played Beatmaps", CounterVisibilityState.AlwaysHidden, "No records. :(") + : base(user, "No records. :(") { ItemsPerPage = 5; } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index c22e5660e6..f0b11dc147 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -36,10 +36,16 @@ namespace osu.Game.Overlays.Profile.Sections private readonly string missing; private ShowMoreButton moreButton; private OsuSpriteText missingText; + private PaginatedContainerHeader header; - protected PaginatedContainer(Bindable user, string missing = "") + private readonly string headerText; + private readonly CounterVisibilityState counterVisibilityState; + + protected PaginatedContainer(Bindable user, string missing = "", string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) { + this.headerText = headerText; this.missing = missing; + this.counterVisibilityState = counterVisibilityState; User.BindTo(user); } @@ -50,9 +56,12 @@ namespace osu.Game.Overlays.Profile.Sections AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; - Children = new[] + Children = new Drawable[] { - CreateHeaderContent, + header = new PaginatedContainerHeader(headerText, counterVisibilityState) + { + Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1 + }, ItemsContainer = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -91,7 +100,8 @@ namespace osu.Game.Overlays.Profile.Sections if (e.NewValue != null) { - OnUserChanged(e.NewValue); + showMore(); + SetCount(GetCount(e.NewValue)); } } @@ -130,17 +140,14 @@ namespace osu.Game.Overlays.Profile.Sections }, loadCancellation.Token); }); - protected virtual void OnUserChanged(User user) - { - showMore(); - } + protected virtual int GetCount(User user) => 0; + + protected void SetCount(int value) => header.Current.Value = value; protected virtual void OnItemsReceived(List items) { } - protected virtual Drawable CreateHeaderContent => Empty(); - protected abstract APIRequest> CreateRequest(); protected abstract Drawable CreateDrawableItem(TModel model); diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs deleted file mode 100644 index 5e175a2203..0000000000 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerWithHeader.cs +++ /dev/null @@ -1,34 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Game.Users; - -namespace osu.Game.Overlays.Profile.Sections -{ - public abstract class PaginatedContainerWithHeader : PaginatedContainer - { - private readonly string headerText; - private readonly CounterVisibilityState counterVisibilityState; - - protected PaginatedContainerHeader Header { get; private set; } - - protected PaginatedContainerWithHeader(Bindable user, string headerText, CounterVisibilityState counterVisibilityState, string missing = "") - : base(user, missing) - { - this.headerText = headerText; - this.counterVisibilityState = counterVisibilityState; - } - - protected override Drawable CreateHeaderContent => Header = new PaginatedContainerHeader(headerText, counterVisibilityState); - - protected override void OnUserChanged(User user) - { - base.OnUserChanged(user); - Header.Current.Value = GetCount(user); - } - - protected virtual int GetCount(User user) => 0; - } -} diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 7dbdf47cad..71ee89d526 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -14,12 +14,12 @@ using osu.Framework.Allocation; namespace osu.Game.Overlays.Profile.Sections.Ranks { - public class PaginatedScoreContainer : PaginatedContainerWithHeader + public class PaginatedScoreContainer : PaginatedContainer { private readonly ScoreType type; public PaginatedScoreContainer(ScoreType type, Bindable user, string headerText, CounterVisibilityState counterVisibilityState, string missingText = "") - : base(user, headerText, counterVisibilityState, missingText) + : base(user, missingText, headerText, counterVisibilityState) { this.type = type; @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks base.OnItemsReceived(items); if (type == ScoreType.Recent) - Header.Current.Value = items.Count; + SetCount(items.Count); } protected override APIRequest> CreateRequest() => From cfc6e2175d2fc9ae36b60402ed4a210d55ff33df Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 10 Sep 2020 20:58:37 +0300 Subject: [PATCH 3179/6909] Add missing header to MostPlayedBeatmapsContainer --- .../Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 8f1d894379..30284818a6 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer { public PaginatedMostPlayedBeatmapContainer(Bindable user) - : base(user, "No records. :(") + : base(user, "No records. :(", "Most Played Beatmaps") { ItemsPerPage = 5; } From 370f22f975268dcea64886e36f6fe0d3079f4502 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 10 Sep 2020 11:11:45 -0700 Subject: [PATCH 3180/6909] Show drag handle by default on main class --- osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs | 2 +- osu.Game/Overlays/Music/PlaylistItem.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs b/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs index 9cdcb19a81..911d47704a 100644 --- a/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs +++ b/osu.Game/Graphics/Containers/OsuRearrangeableListItem.cs @@ -44,7 +44,7 @@ namespace osu.Game.Graphics.Containers /// /// Whether the drag handle should be shown. /// - protected readonly Bindable ShowDragHandle = new Bindable(); + protected readonly Bindable ShowDragHandle = new Bindable(true); private Container handleContainer; private PlaylistItemHandle handle; diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 12f7c7e09d..840fa51b4f 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -39,8 +39,6 @@ namespace osu.Game.Overlays.Music Padding = new MarginPadding { Left = 5 }; FilterTerms = item.Metadata.SearchableTerms; - - ShowDragHandle.Value = true; } [BackgroundDependencyLoader] From a350802158d7413c9bdb37319b47d92efb17a1be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Sep 2020 19:21:16 +0200 Subject: [PATCH 3181/6909] Fix wrong mono streak length handling in corner case --- .../Difficulty/Skills/Colour.cs | 7 ++++++- .../NonVisual/LimitedCapacityQueueTest.cs | 21 +++++++++++++++++++ .../Difficulty/Utils/LimitedCapacityQueue.cs | 9 ++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index ecd74f54ed..32421ee00a 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -45,7 +45,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills // hits spaced more than a second apart are also exempt from colour strain. if (!(current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000)) { - previousHitType = null; + monoHistory.Clear(); + + var currentHit = current.BaseObject as Hit; + currentMonoLength = currentHit != null ? 1 : 0; + previousHitType = currentHit?.Type; + return 0.0; } diff --git a/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs index 52463dd7eb..a04415bc7f 100644 --- a/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs +++ b/osu.Game.Tests/NonVisual/LimitedCapacityQueueTest.cs @@ -94,5 +94,26 @@ namespace osu.Game.Tests.NonVisual Assert.Throws(() => queue.Dequeue()); } + + [Test] + public void TestClearQueue() + { + queue.Enqueue(3); + queue.Enqueue(5); + Assert.AreEqual(2, queue.Count); + + queue.Clear(); + Assert.AreEqual(0, queue.Count); + Assert.Throws(() => _ = queue[0]); + + queue.Enqueue(7); + Assert.AreEqual(1, queue.Count); + Assert.AreEqual(7, queue[0]); + Assert.Throws(() => _ = queue[1]); + + queue.Enqueue(9); + Assert.AreEqual(2, queue.Count); + Assert.AreEqual(9, queue[1]); + } } } diff --git a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs index 0f014e8a8c..bc0eb8af88 100644 --- a/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs +++ b/osu.Game/Rulesets/Difficulty/Utils/LimitedCapacityQueue.cs @@ -40,8 +40,17 @@ namespace osu.Game.Rulesets.Difficulty.Utils this.capacity = capacity; array = new T[capacity]; + Clear(); + } + + /// + /// Removes all elements from the . + /// + public void Clear() + { start = 0; end = -1; + Count = 0; } /// From 64b1a009efb5f80f5c78faae44682b5895c2fd2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 10 Sep 2020 20:56:55 +0200 Subject: [PATCH 3182/6909] Adjust diffcalc test case to pass --- .../TaikoDifficultyCalculatorTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 2d51e82bc4..71b3c23b50 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Taiko"; - [TestCase(2.2905937546434592d, "diffcalc-test")] - [TestCase(2.2905937546434592d, "diffcalc-test-strong")] + [TestCase(2.2867022617692685d, "diffcalc-test")] + [TestCase(2.2867022617692685d, "diffcalc-test-strong")] public void Test(double expected, string name) => base.Test(expected, name); From 97690c818c444190dab9f3f589ee4aeefd3d41f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 11 Sep 2020 00:12:05 +0200 Subject: [PATCH 3183/6909] Add regression test coverage --- .../UserInterface/TestScenePlaylistOverlay.cs | 51 +++++++++++++++++-- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs index a470244f53..52141dea1a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestScenePlaylistOverlay.cs @@ -2,32 +2,35 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Overlays.Music; using osuTK; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestScenePlaylistOverlay : OsuTestScene + public class TestScenePlaylistOverlay : OsuManualInputManagerTestScene { private readonly BindableList beatmapSets = new BindableList(); + private PlaylistOverlay playlistOverlay; + [SetUp] public void Setup() => Schedule(() => { - PlaylistOverlay overlay; - Child = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(300, 500), - Child = overlay = new PlaylistOverlay + Child = playlistOverlay = new PlaylistOverlay { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -53,7 +56,45 @@ namespace osu.Game.Tests.Visual.UserInterface }); } - overlay.BeatmapSets.BindTo(beatmapSets); + playlistOverlay.BeatmapSets.BindTo(beatmapSets); }); + + [Test] + public void TestRearrangeItems() + { + AddUntilStep("wait for animations to complete", () => !playlistOverlay.Transforms.Any()); + + AddStep("hold 1st item handle", () => + { + var handle = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(handle.ScreenSpaceDrawQuad.Centre); + InputManager.PressButton(MouseButton.Left); + }); + + AddStep("drag to 5th", () => + { + var item = this.ChildrenOfType().ElementAt(4); + InputManager.MoveMouseTo(item.ScreenSpaceDrawQuad.Centre); + }); + + AddAssert("song 1 is 5th", () => beatmapSets[4].Metadata.Title == "Some Song 1"); + + AddStep("release handle", () => InputManager.ReleaseButton(MouseButton.Left)); + } + + [Test] + public void TestFiltering() + { + AddStep("set filter to \"10\"", () => + { + var filterControl = playlistOverlay.ChildrenOfType().Single(); + filterControl.Search.Current.Value = "10"; + }); + + AddAssert("results filtered correctly", + () => playlistOverlay.ChildrenOfType() + .Where(item => item.MatchingFilter) + .All(item => item.FilterTerms.Any(term => term.Contains("10")))); + } } } From b594a2a507d82ab1cc45b8161e1874383cb10608 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 11:15:50 +0900 Subject: [PATCH 3184/6909] Import collections on initial import-from-stable step --- osu.Game/Screens/Select/ImportFromStablePopup.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/ImportFromStablePopup.cs b/osu.Game/Screens/Select/ImportFromStablePopup.cs index 272f9566d5..8dab83b24c 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?\nThis will create a second copy of all files on disk."; + BodyText = "An existing copy of osu! was found, though.\nWould you like to import your beatmaps, skins, collections and scores?\nThis will create a second copy of all files on disk."; Icon = FontAwesome.Solid.Plane; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index ddbb021054..d313f67446 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -34,6 +34,7 @@ using System.Threading.Tasks; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; +using osu.Game.Collections; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; @@ -103,7 +104,7 @@ namespace osu.Game.Screens.Select private MusicController music { get; set; } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores) + private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections) { // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). transferRulesetValue(); @@ -294,7 +295,12 @@ namespace osu.Game.Screens.Select { dialogOverlay.Push(new ImportFromStablePopup(() => { - Task.Run(beatmaps.ImportFromStableAsync).ContinueWith(_ => scores.ImportFromStableAsync(), TaskContinuationOptions.OnlyOnRanToCompletion); + Task.Run(beatmaps.ImportFromStableAsync) + .ContinueWith(_ => + { + Task.Run(scores.ImportFromStableAsync); + Task.Run(collections.ImportFromStableAsync); + }, TaskContinuationOptions.OnlyOnRanToCompletion); Task.Run(skins.ImportFromStableAsync); })); } From be5d143b5a6b28030a2f39c7e5c03c3ec803318e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Sep 2020 12:17:12 +0900 Subject: [PATCH 3185/6909] Reorder params --- .../Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs | 2 +- .../Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs | 2 +- .../Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs | 2 +- osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs | 2 +- .../Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs | 2 +- .../Profile/Sections/Recent/PaginatedRecentActivityContainer.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 265972bb86..4b7de8de90 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Profile.Sections.Beatmaps private readonly BeatmapSetType type; public PaginatedBeatmapContainer(BeatmapSetType type, Bindable user, string headerText) - : base(user, "", headerText, CounterVisibilityState.AlwaysVisible) + : base(user, headerText, "", CounterVisibilityState.AlwaysVisible) { this.type = type; ItemsPerPage = 6; diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 30284818a6..8f19cd900c 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer { public PaginatedMostPlayedBeatmapContainer(Bindable user) - : base(user, "No records. :(", "Most Played Beatmaps") + : base(user, "Most Played Beatmaps", "No records. :(") { ItemsPerPage = 5; } diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs index c823053c4b..b968edcb5a 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu public class PaginatedKudosuHistoryContainer : PaginatedContainer { public PaginatedKudosuHistoryContainer(Bindable user) - : base(user, "This user hasn't received any kudosu!") + : base(user, missing: "This user hasn't received any kudosu!") { ItemsPerPage = 5; } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index f0b11dc147..6e681a779f 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Overlays.Profile.Sections private readonly string headerText; private readonly CounterVisibilityState counterVisibilityState; - protected PaginatedContainer(Bindable user, string missing = "", string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + protected PaginatedContainer(Bindable user, string headerText = "", string missing = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) { this.headerText = headerText; this.missing = missing; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 71ee89d526..3c540d6fbb 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks private readonly ScoreType type; public PaginatedScoreContainer(ScoreType type, Bindable user, string headerText, CounterVisibilityState counterVisibilityState, string missingText = "") - : base(user, missingText, headerText, counterVisibilityState) + : base(user, headerText, missingText, counterVisibilityState) { this.type = type; diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index adfe31109b..4901789963 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent public class PaginatedRecentActivityContainer : PaginatedContainer { public PaginatedRecentActivityContainer(Bindable user) - : base(user, "This user hasn't done anything notable recently!") + : base(user, missing: "This user hasn't done anything notable recently!") { ItemsPerPage = 10; } From 22c5e9f64f03e5cdca648028442e46e3c33439a8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Sep 2020 12:19:26 +0900 Subject: [PATCH 3186/6909] Rename missing parameter --- .../Kudosu/PaginatedKudosuHistoryContainer.cs | 2 +- .../Profile/Sections/PaginatedContainer.cs | 18 +++++++++--------- .../Recent/PaginatedRecentActivityContainer.cs | 2 +- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs index b968edcb5a..1b8bd23eb4 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Sections.Kudosu public class PaginatedKudosuHistoryContainer : PaginatedContainer { public PaginatedKudosuHistoryContainer(Bindable user) - : base(user, missing: "This user hasn't received any kudosu!") + : base(user, missingText: "This user hasn't received any kudosu!") { ItemsPerPage = 5; } diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index 6e681a779f..c1107ce907 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -33,18 +33,18 @@ namespace osu.Game.Overlays.Profile.Sections private APIRequest> retrievalRequest; private CancellationTokenSource loadCancellation; - private readonly string missing; + private readonly string missingText; private ShowMoreButton moreButton; - private OsuSpriteText missingText; + private OsuSpriteText missing; private PaginatedContainerHeader header; private readonly string headerText; private readonly CounterVisibilityState counterVisibilityState; - protected PaginatedContainer(Bindable user, string headerText = "", string missing = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + protected PaginatedContainer(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) { this.headerText = headerText; - this.missing = missing; + this.missingText = missingText; this.counterVisibilityState = counterVisibilityState; User.BindTo(user); } @@ -76,10 +76,10 @@ namespace osu.Game.Overlays.Profile.Sections Margin = new MarginPadding { Top = 10 }, Action = showMore, }, - missingText = new OsuSpriteText + missing = new OsuSpriteText { Font = OsuFont.GetFont(size: 15), - Text = missing, + Text = missingText, Alpha = 0, }, }; @@ -124,15 +124,15 @@ namespace osu.Game.Overlays.Profile.Sections moreButton.Hide(); moreButton.IsLoading = false; - if (!string.IsNullOrEmpty(missingText.Text)) - missingText.Show(); + if (!string.IsNullOrEmpty(missing.Text)) + missing.Show(); return; } LoadComponentsAsync(items.Select(CreateDrawableItem).Where(d => d != null), drawables => { - missingText.Hide(); + missing.Hide(); moreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0); moreButton.IsLoading = false; diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index 4901789963..08f39c6272 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent public class PaginatedRecentActivityContainer : PaginatedContainer { public PaginatedRecentActivityContainer(Bindable user) - : base(user, missing: "This user hasn't done anything notable recently!") + : base(user, missingText: "This user hasn't done anything notable recently!") { ItemsPerPage = 10; } From 5b80a7db5fcd6d2dd09079a286b8f838bc531aa3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Sep 2020 16:01:01 +0900 Subject: [PATCH 3187/6909] Re-namespace collections dropdown --- .../Select => Collections}/CollectionFilterDropdown.cs | 7 +++---- .../{Screens/Select => Collections}/CollectionMenuItem.cs | 3 +-- osu.Game/Screens/Select/FilterControl.cs | 1 + 3 files changed, 5 insertions(+), 6 deletions(-) rename osu.Game/{Screens/Select => Collections}/CollectionFilterDropdown.cs (97%) rename osu.Game/{Screens/Select => Collections}/CollectionMenuItem.cs (96%) diff --git a/osu.Game/Screens/Select/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs similarity index 97% rename from osu.Game/Screens/Select/CollectionFilterDropdown.cs rename to osu.Game/Collections/CollectionFilterDropdown.cs index b08c3b167d..c85efb73f5 100644 --- a/osu.Game/Screens/Select/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -12,13 +12,12 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Beatmaps; -using osu.Game.Collections; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osuTK; -namespace osu.Game.Screens.Select +namespace osu.Game.Collections { /// /// A dropdown to select the to filter beatmaps using. @@ -152,7 +151,7 @@ namespace osu.Game.Screens.Select private void updateText() => base.Label = collectionName.Value; } - private class CollectionDropdownMenu : OsuDropdownMenu + protected class CollectionDropdownMenu : OsuDropdownMenu { public CollectionDropdownMenu() { @@ -162,7 +161,7 @@ namespace osu.Game.Screens.Select protected override DrawableDropdownMenuItem CreateDrawableDropdownMenuItem(MenuItem item) => new CollectionDropdownMenuItem(item); } - private class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem + protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem { [NotNull] protected new CollectionMenuItem Item => ((DropdownMenuItem)base.Item).Value; diff --git a/osu.Game/Screens/Select/CollectionMenuItem.cs b/osu.Game/Collections/CollectionMenuItem.cs similarity index 96% rename from osu.Game/Screens/Select/CollectionMenuItem.cs rename to osu.Game/Collections/CollectionMenuItem.cs index 995651de19..0560e03956 100644 --- a/osu.Game/Screens/Select/CollectionMenuItem.cs +++ b/osu.Game/Collections/CollectionMenuItem.cs @@ -3,9 +3,8 @@ using JetBrains.Annotations; using osu.Framework.Bindables; -using osu.Game.Collections; -namespace osu.Game.Screens.Select +namespace osu.Game.Collections { /// /// A filter. diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 079667a457..c82a3742cc 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Game.Collections; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; From 4061480419f3fcc3c2d4758c2da380e7ac33d070 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Sep 2020 16:02:46 +0900 Subject: [PATCH 3188/6909] Rename menu item --- .../SongSelect/TestSceneFilterControl.cs | 4 ++-- .../Collections/CollectionFilterDropdown.cs | 24 +++++++++---------- ...enuItem.cs => CollectionFilterMenuItem.cs} | 14 +++++------ 3 files changed, 21 insertions(+), 21 deletions(-) rename osu.Game/Collections/{CollectionMenuItem.cs => CollectionFilterMenuItem.cs} (73%) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 23feb1466e..7cd4791acb 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -231,7 +231,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.Click(MouseButton.Left); }); - private IEnumerable.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems() - => control.ChildrenOfType().Single().ChildrenOfType.DropdownMenu.DrawableDropdownMenuItem>(); + private IEnumerable.DropdownMenu.DrawableDropdownMenuItem> getCollectionDropdownItems() + => control.ChildrenOfType().Single().ChildrenOfType.DropdownMenu.DrawableDropdownMenuItem>(); } } diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index c85efb73f5..bdd6e73f3f 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -20,13 +20,13 @@ using osuTK; namespace osu.Game.Collections { /// - /// A dropdown to select the to filter beatmaps using. + /// A dropdown to select the to filter beatmaps using. /// - public class CollectionFilterDropdown : OsuDropdown + public class CollectionFilterDropdown : OsuDropdown { private readonly IBindableList collections = new BindableList(); private readonly IBindableList beatmaps = new BindableList(); - private readonly BindableList filters = new BindableList(); + private readonly BindableList filters = new BindableList(); [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } @@ -61,17 +61,17 @@ namespace osu.Game.Collections var selectedItem = SelectedItem?.Value?.Collection; filters.Clear(); - filters.Add(new AllBeatmapsCollectionMenuItem()); - filters.AddRange(collections.Select(c => new CollectionMenuItem(c))); - filters.Add(new ManageCollectionsMenuItem()); + filters.Add(new AllBeatmapsCollectionFilterMenuItem()); + filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c))); + filters.Add(new ManageCollectionsFilterMenuItem()); Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection == selectedItem) ?? filters[0]; } /// - /// Occurs when the selection has changed. + /// Occurs when the selection has changed. /// - private void filterChanged(ValueChangedEvent filter) + private void filterChanged(ValueChangedEvent filter) { // Binding the beatmaps will trigger a collection change event, which results in an infinite-loop. This is rebound later, when it's safe to do so. beatmaps.CollectionChanged -= filterBeatmapsChanged; @@ -86,7 +86,7 @@ namespace osu.Game.Collections // Never select the manage collection filter - rollback to the previous filter. // This is done after the above since it is important that bindable is unbound from OldValue, which is lost after forcing it back to the old value. - if (filter.NewValue is ManageCollectionsMenuItem) + if (filter.NewValue is ManageCollectionsFilterMenuItem) { Current.Value = filter.OldValue; manageCollectionsDialog?.Show(); @@ -103,7 +103,7 @@ namespace osu.Game.Collections Current.TriggerChange(); } - protected override string GenerateItemText(CollectionMenuItem item) => item.CollectionName.Value; + protected override string GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value; protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader { @@ -114,7 +114,7 @@ namespace osu.Game.Collections public class CollectionDropdownHeader : OsuDropdownHeader { - public readonly Bindable SelectedItem = new Bindable(); + public readonly Bindable SelectedItem = new Bindable(); private readonly Bindable collectionName = new Bindable(); protected override string Label @@ -164,7 +164,7 @@ namespace osu.Game.Collections protected class CollectionDropdownMenuItem : OsuDropdownMenu.DrawableOsuDropdownMenuItem { [NotNull] - protected new CollectionMenuItem Item => ((DropdownMenuItem)base.Item).Value; + protected new CollectionFilterMenuItem Item => ((DropdownMenuItem)base.Item).Value; [Resolved] private OsuColour colours { get; set; } diff --git a/osu.Game/Collections/CollectionMenuItem.cs b/osu.Game/Collections/CollectionFilterMenuItem.cs similarity index 73% rename from osu.Game/Collections/CollectionMenuItem.cs rename to osu.Game/Collections/CollectionFilterMenuItem.cs index 0560e03956..4a489d2945 100644 --- a/osu.Game/Collections/CollectionMenuItem.cs +++ b/osu.Game/Collections/CollectionFilterMenuItem.cs @@ -9,7 +9,7 @@ namespace osu.Game.Collections /// /// A filter. /// - public class CollectionMenuItem + public class CollectionFilterMenuItem { /// /// The collection to filter beatmaps from. @@ -25,27 +25,27 @@ namespace osu.Game.Collections public readonly Bindable CollectionName; /// - /// Creates a new . + /// Creates a new . /// /// The collection to filter beatmaps from. - public CollectionMenuItem([CanBeNull] BeatmapCollection collection) + public CollectionFilterMenuItem([CanBeNull] BeatmapCollection collection) { Collection = collection; CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable("All beatmaps"); } } - public class AllBeatmapsCollectionMenuItem : CollectionMenuItem + public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem { - public AllBeatmapsCollectionMenuItem() + public AllBeatmapsCollectionFilterMenuItem() : base(null) { } } - public class ManageCollectionsMenuItem : CollectionMenuItem + public class ManageCollectionsFilterMenuItem : CollectionFilterMenuItem { - public ManageCollectionsMenuItem() + public ManageCollectionsFilterMenuItem() : base(null) { CollectionName.Value = "Manage collections..."; From 06c49070b130c7f9a7a394475b5db51009a045fa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Sep 2020 16:03:59 +0900 Subject: [PATCH 3189/6909] Remove player collection settings --- .../Play/PlayerSettings/CollectionSettings.cs | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs diff --git a/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs b/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs deleted file mode 100644 index 9e7f8e7394..0000000000 --- a/osu.Game/Screens/Play/PlayerSettings/CollectionSettings.cs +++ /dev/null @@ -1,35 +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.Game.Graphics.Sprites; -using osu.Game.Overlays.Music; - -namespace osu.Game.Screens.Play.PlayerSettings -{ - public class CollectionSettings : PlayerSettingsGroup - { - public CollectionSettings() - : base("collections") - { - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - new OsuSpriteText - { - Text = @"Add current song to", - }, - new CollectionsDropdown - { - RelativeSizeAxes = Axes.X, - Items = new[] { PlaylistCollection.All }, - }, - }; - } - } -} From a6a76de7a9254f9bc90bc4d8752035fa7bdf0d74 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Sep 2020 16:08:49 +0900 Subject: [PATCH 3190/6909] Re-expose sealed methods --- osu.Game/Collections/CollectionFilterDropdown.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index bdd6e73f3f..486f0a4fff 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -105,12 +105,16 @@ namespace osu.Game.Collections protected override string GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value; - protected override DropdownHeader CreateHeader() => new CollectionDropdownHeader + protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d => { - SelectedItem = { BindTarget = Current } - }; + d.SelectedItem.BindTarget = Current; + }); - protected override DropdownMenu CreateMenu() => new CollectionDropdownMenu(); + protected sealed override DropdownMenu CreateMenu() => CreateCollectionMenu(); + + protected virtual CollectionDropdownHeader CreateCollectionHeader() => new CollectionDropdownHeader(); + + protected virtual CollectionDropdownMenu CreateCollectionMenu() => new CollectionDropdownMenu(); public class CollectionDropdownHeader : OsuDropdownHeader { From 15b533f2a4dcf427b9ddfe5838e237427a7f0bbb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 15:06:10 +0900 Subject: [PATCH 3191/6909] Hash skins based on name, not skin.ini contents It is feasible that a user may be changing the contents of skin.ini without changing the skin name / author. Such changes should not create a new skin if already imported. --- osu.Game/Database/ArchiveModelManager.cs | 10 +++++++--- osu.Game/Skinning/SkinManager.cs | 8 ++++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 49d7edd56c..e87ab8167a 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -253,6 +253,9 @@ namespace osu.Game.Database /// Generally should include all file types which determine the file's uniqueness. /// Large files should be avoided if possible. /// + /// + /// This is only used by the default hash implementation. If is overridden, it will not be used. + /// protected abstract string[] HashableFileTypes { get; } internal static void LogForModel(TModel model, string message, Exception e = null) @@ -271,7 +274,7 @@ namespace osu.Game.Database /// /// In the case of no matching files, a hash will be generated from the passed archive's . /// - private string computeHash(TModel item, ArchiveReader reader = null) + protected virtual string ComputeHash(TModel item, ArchiveReader reader = null) { // for now, concatenate all .osu files in the set to create a unique hash. MemoryStream hashable = new MemoryStream(); @@ -318,10 +321,11 @@ namespace osu.Game.Database LogForModel(item, "Beginning import..."); item.Files = archive != null ? createFileInfos(archive, Files) : new List(); - item.Hash = computeHash(item, archive); await Populate(item, archive, cancellationToken); + item.Hash = ComputeHash(item, archive); + using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. { try @@ -437,7 +441,7 @@ namespace osu.Game.Database { using (ContextFactory.GetForWrite()) { - item.Hash = computeHash(item); + item.Hash = ComputeHash(item); ModelStore.Update(item); } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index e1f713882a..46130cbdd4 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -12,6 +12,7 @@ using Microsoft.EntityFrameworkCore; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; @@ -86,6 +87,13 @@ namespace osu.Game.Skinning protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name }; + protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null) + { + // this is the optimal way to hash legacy skins, but will need to be reconsidered when we move forward with skin implementation. + // likely, the skin should expose a real version (ie. the version of the skin, not the skin.ini version it's targeting). + return item.ToString().ComputeSHA2Hash(); + } + protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) { await base.Populate(model, archive, cancellationToken); From 62e5c9d2636cd1b21b064633471e0e6e9c4844d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 16:20:30 +0900 Subject: [PATCH 3192/6909] Add test coverage --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 170 ++++++++++++++++++++++ 1 file changed, 170 insertions(+) create mode 100644 osu.Game.Tests/Skins/IO/ImportSkinTest.cs diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs new file mode 100644 index 0000000000..af38d0f3c4 --- /dev/null +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -0,0 +1,170 @@ +// Copyright (c) ppy Pty Ltd . 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; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Platform; +using osu.Game.Beatmaps; +using osu.Game.IO.Archives; +using osu.Game.Skinning; +using osu.Game.Tests.Resources; +using SharpCompress.Archives.Zip; + +namespace osu.Game.Tests.Skins.IO +{ + public class ImportSkinTest + { + [Test] + public async Task TestBasicImport() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = await loadOsu(host); + + var imported = await loadIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk")); + + Assert.That(imported.Name, Is.EqualTo("test skin")); + Assert.That(imported.Creator, Is.EqualTo("skinner")); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public async Task TestImportTwiceWithSameMetadata() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = await loadOsu(host); + + var imported = await loadIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk")); + var imported2 = await loadIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk")); + + Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(1)); + + // the first should be overwritten by the second import. + Assert.That(osu.Dependencies.Get().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); + } + finally + { + host.Exit(); + } + } + } + + [Test] + public async Task TestImportTwiceWithDifferentMetadata() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = await loadOsu(host); + + var imported = await loadIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2", "skinner"), "skin.osk")); + var imported2 = await loadIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2.1", "skinner"), "skin2.osk")); + + Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(2)); + + Assert.That(osu.Dependencies.Get().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); + } + finally + { + host.Exit(); + } + } + } + + private MemoryStream createOsk(string name, string author) + { + var zipStream = new MemoryStream(); + using var zip = ZipArchive.Create(); + zip.AddEntry("skin.ini", generateSkinIni(name, author)); + zip.SaveTo(zipStream); + return zipStream; + } + + private MemoryStream generateSkinIni(string name, string author) + { + var stream = new MemoryStream(); + var writer = new StreamWriter(stream); + + writer.WriteLine("[General]"); + writer.WriteLine($"Name: {name}"); + writer.WriteLine($"Author: {author}"); + writer.WriteLine(); + writer.WriteLine($"# unique {Guid.NewGuid()}"); + + writer.Flush(); + + return stream; + } + + private async Task loadIntoOsu(OsuGameBase osu, ArchiveReader archive = null) + { + var beatmapManager = osu.Dependencies.Get(); + + var skinManager = osu.Dependencies.Get(); + return await skinManager.Import(archive); + } + + private async Task loadOsu(GameHost host) + { + var osu = new OsuGameBase(); + +#pragma warning disable 4014 + Task.Run(() => host.Run(osu)); +#pragma warning restore 4014 + + waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); + + var beatmapFile = TestResources.GetTestBeatmapForImport(); + var beatmapManager = osu.Dependencies.Get(); + await beatmapManager.Import(beatmapFile); + + return osu; + } + + private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) + { + Task task = Task.Run(() => + { + while (!result()) Thread.Sleep(200); + }); + + Assert.IsTrue(task.Wait(timeout), failureMessage); + } + + private class TestArchiveReader : ArchiveReader + { + public TestArchiveReader() + : base("test_archive") + { + } + + public override Stream GetStream(string name) => new MemoryStream(); + + public override void Dispose() + { + } + + public override IEnumerable Filenames => new[] { "test_file.osr" }; + } + } +} From ef77658311f2d7471e1bcbc56f252862131e1615 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 16:29:14 +0900 Subject: [PATCH 3193/6909] Add coverage of case where skin.ini doesn't specify name/author --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 26 +++++++++++++++++++++++ osu.Game/Skinning/SkinManager.cs | 16 ++++++++++---- 2 files changed, 38 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index af38d0f3c4..ad1a41a2b3 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -66,6 +66,32 @@ namespace osu.Game.Tests.Skins.IO } } + [Test] + public async Task TestImportTwiceWithNoMetadata() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + { + try + { + var osu = await loadOsu(host); + + // if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety. + var imported = await loadIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk")); + var imported2 = await loadIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk")); + + Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(2)); + + Assert.That(osu.Dependencies.Get().GetAllUserSkins().First().Files.First().FileInfoID, Is.EqualTo(imported.Files.First().FileInfoID)); + Assert.That(osu.Dependencies.Get().GetAllUserSkins().Last().Files.First().FileInfoID, Is.EqualTo(imported2.Files.First().FileInfoID)); + } + finally + { + host.Exit(); + } + } + } + [Test] public async Task TestImportTwiceWithDifferentMetadata() { diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 46130cbdd4..eacfdaec4a 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -87,11 +87,19 @@ namespace osu.Game.Skinning protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name }; + private const string unknown_creator_string = "Unknown"; + protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null) { - // this is the optimal way to hash legacy skins, but will need to be reconsidered when we move forward with skin implementation. - // likely, the skin should expose a real version (ie. the version of the skin, not the skin.ini version it's targeting). - return item.ToString().ComputeSHA2Hash(); + if (item.Creator != null && item.Creator != unknown_creator_string) + { + // this is the optimal way to hash legacy skins, but will need to be reconsidered when we move forward with skin implementation. + // likely, the skin should expose a real version (ie. the version of the skin, not the skin.ini version it's targeting). + return item.ToString().ComputeSHA2Hash(); + } + + // if there was no creator, the ToString above would give the filename, which along isn't really enough to base any decisions on. + return base.ComputeHash(item, reader); } protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) @@ -108,7 +116,7 @@ namespace osu.Game.Skinning else { model.Name = model.Name.Replace(".osk", ""); - model.Creator ??= "Unknown"; + model.Creator ??= unknown_creator_string; } } From 948437865b32bafbe7af9df7c51c675f041e30d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 16:42:11 +0900 Subject: [PATCH 3194/6909] Remove unused code --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index ad1a41a2b3..14eff4c5e3 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; @@ -144,8 +143,6 @@ namespace osu.Game.Tests.Skins.IO private async Task loadIntoOsu(OsuGameBase osu, ArchiveReader archive = null) { - var beatmapManager = osu.Dependencies.Get(); - var skinManager = osu.Dependencies.Get(); return await skinManager.Import(archive); } @@ -176,21 +173,5 @@ namespace osu.Game.Tests.Skins.IO Assert.IsTrue(task.Wait(timeout), failureMessage); } - - private class TestArchiveReader : ArchiveReader - { - public TestArchiveReader() - : base("test_archive") - { - } - - public override Stream GetStream(string name) => new MemoryStream(); - - public override void Dispose() - { - } - - public override IEnumerable Filenames => new[] { "test_file.osr" }; - } } } From fcc868362971a6ac8b9dd013157aab91fd6a2bf7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Sep 2020 16:46:11 +0900 Subject: [PATCH 3195/6909] Hook up now playing overlay to collections --- ...tionsDropdown.cs => CollectionDropdown.cs} | 16 +++++----- osu.Game/Overlays/Music/FilterControl.cs | 28 +++++++++++------- osu.Game/Overlays/Music/FilterCriteria.cs | 22 ++++++++++++++ osu.Game/Overlays/Music/Playlist.cs | 10 ++++++- osu.Game/Overlays/Music/PlaylistItem.cs | 29 +++++++++++++++---- osu.Game/Overlays/Music/PlaylistOverlay.cs | 8 +---- 6 files changed, 82 insertions(+), 31 deletions(-) rename osu.Game/Overlays/Music/{CollectionsDropdown.cs => CollectionDropdown.cs} (75%) create mode 100644 osu.Game/Overlays/Music/FilterCriteria.cs diff --git a/osu.Game/Overlays/Music/CollectionsDropdown.cs b/osu.Game/Overlays/Music/CollectionDropdown.cs similarity index 75% rename from osu.Game/Overlays/Music/CollectionsDropdown.cs rename to osu.Game/Overlays/Music/CollectionDropdown.cs index 5bd321f31e..4ab0ad643c 100644 --- a/osu.Game/Overlays/Music/CollectionsDropdown.cs +++ b/osu.Game/Overlays/Music/CollectionDropdown.cs @@ -7,13 +7,15 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; -using osu.Framework.Graphics.UserInterface; +using osu.Game.Collections; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Music { - public class CollectionsDropdown : OsuDropdown + /// + /// A for use in the . + /// + public class CollectionDropdown : CollectionFilterDropdown { [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -21,11 +23,11 @@ namespace osu.Game.Overlays.Music AccentColour = colours.Gray6; } - protected override DropdownHeader CreateHeader() => new CollectionsHeader(); + protected override CollectionDropdownHeader CreateCollectionHeader() => new CollectionsHeader(); - protected override DropdownMenu CreateMenu() => new CollectionsMenu(); + protected override CollectionDropdownMenu CreateCollectionMenu() => new CollectionsMenu(); - private class CollectionsMenu : OsuDropdownMenu + private class CollectionsMenu : CollectionDropdownMenu { public CollectionsMenu() { @@ -40,7 +42,7 @@ namespace osu.Game.Overlays.Music } } - private class CollectionsHeader : OsuDropdownHeader + private class CollectionsHeader : CollectionDropdownHeader { [BackgroundDependencyLoader] private void load(OsuColour colours) diff --git a/osu.Game/Overlays/Music/FilterControl.cs b/osu.Game/Overlays/Music/FilterControl.cs index 278bb55170..3e43387035 100644 --- a/osu.Game/Overlays/Music/FilterControl.cs +++ b/osu.Game/Overlays/Music/FilterControl.cs @@ -8,13 +8,15 @@ using osu.Game.Graphics.UserInterface; using osuTK; using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; namespace osu.Game.Overlays.Music { public class FilterControl : Container { + public Action FilterChanged; + public readonly FilterTextBox Search; + private readonly CollectionDropdown collectionDropdown; public FilterControl() { @@ -32,21 +34,27 @@ namespace osu.Game.Overlays.Music RelativeSizeAxes = Axes.X, Height = 40, }, - new CollectionsDropdown - { - RelativeSizeAxes = Axes.X, - Items = new[] { PlaylistCollection.All }, - } + collectionDropdown = new CollectionDropdown { RelativeSizeAxes = Axes.X } }, }, }; - - Search.Current.ValueChanged += current_ValueChanged; } - private void current_ValueChanged(ValueChangedEvent e) => FilterChanged?.Invoke(e.NewValue); + protected override void LoadComplete() + { + base.LoadComplete(); - public Action FilterChanged; + Search.Current.BindValueChanged(_ => updateCriteria()); + collectionDropdown.Current.BindValueChanged(_ => updateCriteria(), true); + } + + private void updateCriteria() => FilterChanged?.Invoke(createCriteria()); + + private FilterCriteria createCriteria() => new FilterCriteria + { + SearchText = Search.Text, + Collection = collectionDropdown.Current.Value?.Collection + }; public class FilterTextBox : SearchTextBox { diff --git a/osu.Game/Overlays/Music/FilterCriteria.cs b/osu.Game/Overlays/Music/FilterCriteria.cs new file mode 100644 index 0000000000..f15edff4d0 --- /dev/null +++ b/osu.Game/Overlays/Music/FilterCriteria.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 JetBrains.Annotations; +using osu.Game.Collections; + +namespace osu.Game.Overlays.Music +{ + public class FilterCriteria + { + /// + /// The search text. + /// + public string SearchText; + + /// + /// The collection to filter beatmaps from. + /// + [CanBeNull] + public BeatmapCollection Collection; + } +} diff --git a/osu.Game/Overlays/Music/Playlist.cs b/osu.Game/Overlays/Music/Playlist.cs index 621a533dd6..4fe338926f 100644 --- a/osu.Game/Overlays/Music/Playlist.cs +++ b/osu.Game/Overlays/Music/Playlist.cs @@ -24,7 +24,15 @@ namespace osu.Game.Overlays.Music set => base.Padding = value; } - public void Filter(string searchTerm) => ((SearchContainer>)ListContainer).SearchTerm = searchTerm; + public void Filter(FilterCriteria criteria) + { + var items = (SearchContainer>)ListContainer; + + foreach (var item in items.OfType()) + item.InSelectedCollection = criteria.Collection?.Beatmaps.Any(b => b.BeatmapSet.Equals(item.Model)) ?? true; + + items.SearchTerm = criteria.SearchText; + } public BeatmapSetInfo FirstVisibleSet => Items.FirstOrDefault(i => ((PlaylistItem)ItemMap[i]).MatchingFilter); diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 840fa51b4f..96dff39fae 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -95,23 +95,40 @@ namespace osu.Game.Overlays.Music return true; } + private bool inSelectedCollection = true; + + public bool InSelectedCollection + { + get => inSelectedCollection; + set + { + if (inSelectedCollection == value) + return; + + inSelectedCollection = value; + updateFilter(); + } + } + public IEnumerable FilterTerms { get; } - private bool matching = true; + private bool matchingFilter = true; public bool MatchingFilter { - get => matching; + get => matchingFilter && inSelectedCollection; set { - if (matching == value) return; + if (matchingFilter == value) + return; - matching = value; - - this.FadeTo(matching ? 1 : 0, 200); + matchingFilter = value; + updateFilter(); } } + private void updateFilter() => this.FadeTo(MatchingFilter ? 1 : 0, 200); + public bool FilteringActive { get; set; } } } diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index b45d84049f..050e687dfb 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Music { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - FilterChanged = search => list.Filter(search), + FilterChanged = criteria => list.Filter(criteria), Padding = new MarginPadding(10), }, }, @@ -124,10 +124,4 @@ namespace osu.Game.Overlays.Music beatmap.Value.Track.Restart(); } } - - //todo: placeholder - public enum PlaylistCollection - { - All - } } From 6327f12fe4a0fa587257ab2559d36f09c7a8a0e8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Sep 2020 16:58:18 +0900 Subject: [PATCH 3196/6909] Disable manage collections item in now playing overlay --- osu.Game/Collections/CollectionFilterDropdown.cs | 9 ++++++++- osu.Game/Overlays/Music/CollectionDropdown.cs | 2 ++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index 486f0a4fff..ec0e9d5a89 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -24,6 +24,11 @@ namespace osu.Game.Collections /// public class CollectionFilterDropdown : OsuDropdown { + /// + /// Whether to show the "manage collections..." menu item in the dropdown. + /// + protected virtual bool ShowManageCollectionsItem => true; + private readonly IBindableList collections = new BindableList(); private readonly IBindableList beatmaps = new BindableList(); private readonly BindableList filters = new BindableList(); @@ -63,7 +68,9 @@ namespace osu.Game.Collections filters.Clear(); filters.Add(new AllBeatmapsCollectionFilterMenuItem()); filters.AddRange(collections.Select(c => new CollectionFilterMenuItem(c))); - filters.Add(new ManageCollectionsFilterMenuItem()); + + if (ShowManageCollectionsItem) + filters.Add(new ManageCollectionsFilterMenuItem()); Current.Value = filters.SingleOrDefault(f => f.Collection != null && f.Collection == selectedItem) ?? filters[0]; } diff --git a/osu.Game/Overlays/Music/CollectionDropdown.cs b/osu.Game/Overlays/Music/CollectionDropdown.cs index 4ab0ad643c..ed0ebf696b 100644 --- a/osu.Game/Overlays/Music/CollectionDropdown.cs +++ b/osu.Game/Overlays/Music/CollectionDropdown.cs @@ -17,6 +17,8 @@ namespace osu.Game.Overlays.Music /// public class CollectionDropdown : CollectionFilterDropdown { + protected override bool ShowManageCollectionsItem => false; + [BackgroundDependencyLoader] private void load(OsuColour colours) { From b047fbb8ee98de23bc4e23aa0856b38a1b66d44d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 11 Sep 2020 17:45:57 +0900 Subject: [PATCH 3197/6909] Use bindable value for search text --- osu.Game/Overlays/Music/FilterControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Music/FilterControl.cs b/osu.Game/Overlays/Music/FilterControl.cs index 3e43387035..66adbeebe8 100644 --- a/osu.Game/Overlays/Music/FilterControl.cs +++ b/osu.Game/Overlays/Music/FilterControl.cs @@ -52,7 +52,7 @@ namespace osu.Game.Overlays.Music private FilterCriteria createCriteria() => new FilterCriteria { - SearchText = Search.Text, + SearchText = Search.Current.Value, Collection = collectionDropdown.Current.Value?.Collection }; From 8bae00454edbeffdfef9e47a3f5c7ccf87a5f508 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 19:53:55 +0900 Subject: [PATCH 3198/6909] Fix slider serialization --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 705e88040f..5aeb23a425 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -112,8 +112,9 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double TickDistanceMultiplier = 1; - public HitCircle HeadCircle; - public SliderTailCircle TailCircle; + public HitCircle HeadCircle { get; protected set; } + + public SliderTailCircle TailCircle { get; protected set; } public Slider() { From 8e028dd88fa5c8ac1feda1128fb403fb22519c88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 19:54:11 +0900 Subject: [PATCH 3199/6909] Fix incorrect ordering of ApplyDefaults for newly added objects --- osu.Game/Screens/Edit/EditorBeatmap.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 061009e519..fd5270653d 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -79,11 +79,11 @@ namespace osu.Game.Screens.Edit private void updateHitObject([CanBeNull] HitObject hitObject, bool silent) { - scheduledUpdate?.Cancel(); - if (hitObject != null) pendingUpdates.Add(hitObject); + if (scheduledUpdate?.Completed == false) return; + scheduledUpdate = Schedule(() => { beatmapProcessor?.PreProcess(); @@ -158,10 +158,14 @@ namespace osu.Game.Screens.Edit { trackStartTime(hitObject); - mutableHitObjects.Insert(index, hitObject); - - HitObjectAdded?.Invoke(hitObject); updateHitObject(hitObject, true); + + // must occur after the batch-scheduled ApplyDefaults. + Schedule(() => + { + mutableHitObjects.Insert(index, hitObject); + HitObjectAdded?.Invoke(hitObject); + }); } /// From 7d7401123c05a2179bab664c027c5bdba252fd90 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 19:54:20 +0900 Subject: [PATCH 3200/6909] Add initial implementation of editor clipboard --- osu.Game/Screens/Edit/ClipboardContent.cs | 27 ++++++++++++ osu.Game/Screens/Edit/Editor.cs | 52 ++++++++++++++++++++++- 2 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Edit/ClipboardContent.cs diff --git a/osu.Game/Screens/Edit/ClipboardContent.cs b/osu.Game/Screens/Edit/ClipboardContent.cs new file mode 100644 index 0000000000..b2edbedccc --- /dev/null +++ b/osu.Game/Screens/Edit/ClipboardContent.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 System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using osu.Game.IO.Serialization; +using osu.Game.IO.Serialization.Converters; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Screens.Edit +{ + public class ClipboardContent : IJsonSerializable + { + [JsonConverter(typeof(TypedListConverter))] + public IList HitObjects; + + public ClipboardContent() + { + } + + public ClipboardContent(EditorBeatmap editorBeatmap) + { + HitObjects = editorBeatmap.SelectedHitObjects.ToList(); + } + } +} diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 23eb704920..a063b0a303 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -22,6 +23,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; +using osu.Game.IO.Serialization; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets.Edit; @@ -131,9 +133,14 @@ namespace osu.Game.Screens.Edit updateLastSavedHash(); EditorMenuBar menuBar; + OsuMenuItem undoMenuItem; OsuMenuItem redoMenuItem; + EditorMenuItem cutMenuItem; + EditorMenuItem copyMenuItem; + EditorMenuItem pasteMenuItem; + var fileMenuItems = new List { new EditorMenuItem("Save", MenuItemType.Standard, Save) @@ -183,7 +190,11 @@ namespace osu.Game.Screens.Edit Items = new[] { undoMenuItem = new EditorMenuItem("Undo", MenuItemType.Standard, Undo), - redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo) + redoMenuItem = new EditorMenuItem("Redo", MenuItemType.Standard, Redo), + new EditorMenuItemSpacer(), + cutMenuItem = new EditorMenuItem("Cut", MenuItemType.Standard, Cut), + copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), + pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), } } } @@ -244,6 +255,17 @@ 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); + // todo: BindCollectionChanged + editorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => + { + var hasObjects = editorBeatmap.SelectedHitObjects.Count > 0; + + cutMenuItem.Action.Disabled = !hasObjects; + copyMenuItem.Action.Disabled = !hasObjects; + }; + + clipboard.BindValueChanged(content => pasteMenuItem.Action.Disabled = string.IsNullOrEmpty(content.NewValue)); + menuBar.Mode.ValueChanged += onModeChanged; bottomBackground.Colour = colours.Gray2; @@ -394,6 +416,34 @@ namespace osu.Game.Screens.Edit this.Exit(); } + private readonly Bindable clipboard = new Bindable(); + + protected void Cut() + { + Copy(); + foreach (var h in editorBeatmap.SelectedHitObjects.ToArray()) + editorBeatmap.Remove(h); + } + + protected void Copy() + { + clipboard.Value = new ClipboardContent(editorBeatmap).Serialize(); + } + + protected void Paste() + { + if (string.IsNullOrEmpty(clipboard.Value)) + return; + + var objects = clipboard.Value.Deserialize().HitObjects; + double timeOffset = clock.CurrentTime - objects.First().StartTime; + + foreach (var h in objects) + h.StartTime += timeOffset; + + editorBeatmap.AddRange(objects); + } + protected void Undo() => changeHandler.RestoreState(-1); protected void Redo() => changeHandler.RestoreState(1); From de3d8e83e178986573108086bc59ecf7601aa74f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 19:55:41 +0900 Subject: [PATCH 3201/6909] Add keyboard shortcuts --- osu.Game/Screens/Edit/Editor.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a063b0a303..2af319870d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -292,6 +292,18 @@ namespace osu.Game.Screens.Edit { switch (action.ActionType) { + case PlatformActionType.Cut: + Cut(); + return true; + + case PlatformActionType.Copy: + Copy(); + return true; + + case PlatformActionType.Paste: + Paste(); + return true; + case PlatformActionType.Undo: Undo(); return true; From 2858296c255a5c2427e756366cd4364a0bd30c01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 19:58:34 +0900 Subject: [PATCH 3202/6909] Avoid editor confirm-save dialog looping infinitely when using keyboard shortcut to exit Will now exit without saving if the keyboard shortcut is activated twice in a row, as expected. Closes #10136. --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 23eb704920..ce34c1dac0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -369,7 +369,7 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { - if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges) + if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges && !(dialogOverlay.CurrentDialog is PromptForSaveDialog)) { dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); return true; From 139a5acd1b98ec8ae9e2775f3480ab3bf5052519 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 20:14:12 +0900 Subject: [PATCH 3203/6909] Fix editor hitobjects getting masked weirdly Closes #10124 --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b9cc054ed3..abb32bb6a8 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -112,7 +112,6 @@ namespace osu.Game.Rulesets.Edit { Name = "Content", RelativeSizeAxes = Axes.Both, - Masking = true, Children = new Drawable[] { // layers below playfield From 432c3e17ebd3fc7d05bf53c861e7946ee302febc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 20:23:34 +0900 Subject: [PATCH 3204/6909] Fix toolbox becoming inoperable due to incorrect ordering --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 81 ++++++++++----------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index abb32bb6a8..e42a359d2e 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -82,56 +82,49 @@ namespace osu.Game.Rulesets.Edit return; } - InternalChild = new GridContainer + const float toolbar_width = 200; + + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Content = new[] + new Container { - new Drawable[] + Name = "Content", + Padding = new MarginPadding { Left = toolbar_width }, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - new FillFlowContainer + // layers below playfield + drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[] { - Name = "Sidebar", - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Right = 10 }, - Spacing = new Vector2(10), - Children = new Drawable[] - { - new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }, - new ToolboxGroup("toggles") - { - ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox - { - Bindable = b, - LabelText = b?.Description ?? "unknown" - }) - } - } - }, - new Container - { - Name = "Content", - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - // layers below playfield - drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[] - { - LayerBelowRuleset, - new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both } - }), - drawableRulesetWrapper, - // layers above playfield - drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() - .WithChild(BlueprintContainer = CreateBlueprintContainer(HitObjects)) - } - } - }, + LayerBelowRuleset, + new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both } + }), + drawableRulesetWrapper, + // layers above playfield + drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() + .WithChild(BlueprintContainer = CreateBlueprintContainer(HitObjects)) + } }, - ColumnDimensions = new[] + new FillFlowContainer { - new Dimension(GridSizeMode.Absolute, 200), - } + Name = "Sidebar", + RelativeSizeAxes = Axes.Y, + Width = toolbar_width, + Padding = new MarginPadding { Right = 10 }, + Spacing = new Vector2(10), + Children = new Drawable[] + { + new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }, + new ToolboxGroup("toggles") + { + ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox + { + Bindable = b, + LabelText = b?.Description ?? "unknown" + }) + } + } + }, }; toolboxCollection.Items = CompositionTools From 73dd21c8fcdaa49157287747b8be14abb1388296 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 20:27:04 +0900 Subject: [PATCH 3205/6909] Add failing test --- .../Visual/Editing/TestSceneEditorChangeStates.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs index c8a32d966f..dfe1e434dc 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs @@ -27,6 +27,17 @@ namespace osu.Game.Tests.Visual.Editing AddStep("get beatmap", () => editorBeatmap = Editor.ChildrenOfType().Single()); } + [Test] + public void TestSelectedObjects() + { + HitCircle obj = null; + AddStep("add hitobject", () => editorBeatmap.Add(obj = new HitCircle { StartTime = 1000 })); + AddStep("select hitobject", () => editorBeatmap.SelectedHitObjects.Add(obj)); + AddAssert("confirm 1 selected", () => editorBeatmap.SelectedHitObjects.Count == 1); + AddStep("deselect hitobject", () => editorBeatmap.SelectedHitObjects.Remove(obj)); + AddAssert("confirm 0 selected", () => editorBeatmap.SelectedHitObjects.Count == 0); + } + [Test] public void TestUndoFromInitialState() { From 22e6df02b650d72d6d22a0787c446984e9df982b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 20:13:54 +0900 Subject: [PATCH 3206/6909] Fix editor selected hitobjects containing the selection up to five times --- .../Compose/Components/BlueprintContainer.cs | 2 -- .../Compose/Components/SelectionHandler.cs | 20 +++++++++++++------ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 865e225645..b7b222d87b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -367,14 +367,12 @@ namespace osu.Game.Screens.Edit.Compose.Components { selectionHandler.HandleSelected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 1); - beatmap.SelectedHitObjects.Add(blueprint.HitObject); } private void onBlueprintDeselected(SelectionBlueprint blueprint) { selectionHandler.HandleDeselected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 0); - beatmap.SelectedHitObjects.Remove(blueprint.HitObject); } #endregion diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 9700cb8c8e..f397ee1596 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -145,10 +145,16 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. internal void HandleSelected(SelectionBlueprint blueprint) { - selectedBlueprints.Add(blueprint); - EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); + if (!selectedBlueprints.Contains(blueprint)) + { + selectedBlueprints.Add(blueprint); - UpdateVisibility(); + // need to check this as well, as there are potentially multiple SelectionHandlers and the above check is not enough. + if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject)) + EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); + + UpdateVisibility(); + } } /// @@ -157,10 +163,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. internal void HandleDeselected(SelectionBlueprint blueprint) { - selectedBlueprints.Remove(blueprint); - EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); + if (selectedBlueprints.Remove(blueprint)) + { + EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); - UpdateVisibility(); + UpdateVisibility(); + } } /// From 94d929d8cdf64a648afca7408ecd192287fc2479 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 22:03:19 +0900 Subject: [PATCH 3207/6909] Remove unnecessary contains checks --- .../Compose/Components/SelectionHandler.cs | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index f397ee1596..0a3c9072cf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -145,16 +145,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. internal void HandleSelected(SelectionBlueprint blueprint) { - if (!selectedBlueprints.Contains(blueprint)) - { - selectedBlueprints.Add(blueprint); + selectedBlueprints.Add(blueprint); - // need to check this as well, as there are potentially multiple SelectionHandlers and the above check is not enough. - if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject)) - EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); + // need to check this as well, as there are potentially multiple SelectionHandlers and the above check is not enough. + if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject)) + EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); - UpdateVisibility(); - } + UpdateVisibility(); } /// @@ -163,12 +160,9 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. internal void HandleDeselected(SelectionBlueprint blueprint) { - if (selectedBlueprints.Remove(blueprint)) - { - EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); + EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); - UpdateVisibility(); - } + UpdateVisibility(); } /// From cb14d847deec463a2d236e33f516d74c61f80340 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 22:40:12 +0900 Subject: [PATCH 3208/6909] 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 a2686c380e..6cbb4b2e68 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 48582ae29d..8d23a32c3c 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 0eed2fa911..d00b174195 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 001cd1194c934d02c326ce55a8da991396e568a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 22:53:03 +0900 Subject: [PATCH 3209/6909] Consume BindCollectionChanged --- 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 896c4f9f61..d6b2b4ba3a 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -256,13 +256,13 @@ namespace osu.Game.Screens.Edit changeHandler.CanRedo.BindValueChanged(v => redoMenuItem.Action.Disabled = !v.NewValue, true); // todo: BindCollectionChanged - editorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => + editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) => { var hasObjects = editorBeatmap.SelectedHitObjects.Count > 0; cutMenuItem.Action.Disabled = !hasObjects; copyMenuItem.Action.Disabled = !hasObjects; - }; + }, true); clipboard.BindValueChanged(content => pasteMenuItem.Action.Disabled = string.IsNullOrEmpty(content.NewValue)); From 2d9b0acabe7fd4826e7d791d88c89a348afc0601 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Sep 2020 15:33:13 +0900 Subject: [PATCH 3210/6909] Fix empty selection via keyboard shortcuts crashing --- osu.Game/Screens/Edit/Editor.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d6b2b4ba3a..19bac2a778 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework; using osu.Framework.Allocation; @@ -439,6 +440,9 @@ namespace osu.Game.Screens.Edit protected void Copy() { + if (editorBeatmap.SelectedHitObjects.Count == 0) + return; + clipboard.Value = new ClipboardContent(editorBeatmap).Serialize(); } @@ -448,6 +452,9 @@ namespace osu.Game.Screens.Edit return; var objects = clipboard.Value.Deserialize().HitObjects; + + Debug.Assert(objects.Any()); + double timeOffset = clock.CurrentTime - objects.First().StartTime; foreach (var h in objects) From 81f30cd2649a6edd2abc28868e13f492fc95bf1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Sep 2020 20:31:50 +0900 Subject: [PATCH 3211/6909] Select blueprint if object is already selected at the point of adding --- osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index b7b222d87b..bf1e18771f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -271,6 +271,9 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.Selected += onBlueprintSelected; blueprint.Deselected += onBlueprintDeselected; + if (beatmap.SelectedHitObjects.Contains(hitObject)) + blueprint.Select(); + SelectionBlueprints.Add(blueprint); } From 3854caae9b0a6df9ba6599960a0a02fade064ae5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Sep 2020 21:20:37 +0900 Subject: [PATCH 3212/6909] Remove secondary schedule logic --- osu.Game/Screens/Edit/EditorBeatmap.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index fd5270653d..5272530228 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Edit /// The to update. public void UpdateHitObject([NotNull] HitObject hitObject) => updateHitObject(hitObject, false); - private void updateHitObject([CanBeNull] HitObject hitObject, bool silent) + private void updateHitObject([CanBeNull] HitObject hitObject, bool silent, bool performAdd = false) { if (hitObject != null) pendingUpdates.Add(hitObject); @@ -93,6 +93,12 @@ namespace osu.Game.Screens.Edit beatmapProcessor?.PostProcess(); + if (performAdd) + { + foreach (var obj in pendingUpdates) + HitObjectAdded?.Invoke(obj); + } + if (!silent) { foreach (var obj in pendingUpdates) @@ -158,14 +164,8 @@ namespace osu.Game.Screens.Edit { trackStartTime(hitObject); - updateHitObject(hitObject, true); - - // must occur after the batch-scheduled ApplyDefaults. - Schedule(() => - { - mutableHitObjects.Insert(index, hitObject); - HitObjectAdded?.Invoke(hitObject); - }); + mutableHitObjects.Insert(index, hitObject); + updateHitObject(hitObject, true, true); } /// From 1a9f0ac16afbc396728521cc2664509b07695808 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Sep 2020 23:02:23 +0900 Subject: [PATCH 3213/6909] Select new objects --- osu.Game/Screens/Edit/Editor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 19bac2a778..ee3befe8bd 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -460,7 +460,10 @@ namespace osu.Game.Screens.Edit foreach (var h in objects) h.StartTime += timeOffset; + editorBeatmap.SelectedHitObjects.Clear(); + editorBeatmap.AddRange(objects); + editorBeatmap.SelectedHitObjects.AddRange(objects); } protected void Undo() => changeHandler.RestoreState(-1); From f17b2f1359eba9e9296f9e0a4ba1caac57f52f6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Sep 2020 20:43:17 +0200 Subject: [PATCH 3214/6909] Ensure track is looping in song select immediately --- osu.Game/Screens/Select/SongSelect.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index d313f67446..683e3abcc2 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -517,6 +517,8 @@ namespace osu.Game.Screens.Select FilterControl.Activate(); ModSelect.SelectedMods.BindTo(selectedMods); + + music.TrackChanged += ensureTrackLooping; } private const double logo_transition = 250; @@ -568,6 +570,7 @@ namespace osu.Game.Screens.Select BeatmapDetails.Refresh(); music.CurrentTrack.Looping = true; + music.TrackChanged += ensureTrackLooping; music.ResetTrackAdjustments(); if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) @@ -593,6 +596,7 @@ namespace osu.Game.Screens.Select BeatmapOptions.Hide(); music.CurrentTrack.Looping = false; + music.TrackChanged -= ensureTrackLooping; this.ScaleTo(1.1f, 250, Easing.InSine); @@ -614,10 +618,14 @@ namespace osu.Game.Screens.Select FilterControl.Deactivate(); music.CurrentTrack.Looping = false; + music.TrackChanged -= ensureTrackLooping; return false; } + private void ensureTrackLooping(WorkingBeatmap beatmap, TrackChangeDirection changeDirection) + => music.CurrentTrack.Looping = true; + public override bool OnBackButton() { if (ModSelect.State.Value == Visibility.Visible) @@ -653,8 +661,6 @@ namespace osu.Game.Screens.Select beatmapInfoWedge.Beatmap = beatmap; BeatmapDetails.Beatmap = beatmap; - - music.CurrentTrack.Looping = true; } private readonly WeakReference lastTrack = new WeakReference(null); From 3db0e7cd75d55126d45b94661a865b17a1b8babd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Sep 2020 22:34:57 +0200 Subject: [PATCH 3215/6909] Generalise LegacyRollingCounter --- .../Skinning/LegacyComboCounter.cs | 33 ------------ osu.Game/Skinning/LegacyRollingCounter.cs | 51 +++++++++++++++++++ 2 files changed, 51 insertions(+), 33 deletions(-) create mode 100644 osu.Game/Skinning/LegacyRollingCounter.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index e03b30f58f..ccfabdc5fd 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Catch.UI; using osu.Game.Screens.Play; using osu.Game.Skinning; @@ -128,35 +125,5 @@ namespace osu.Game.Rulesets.Catch.Skinning lastExplosion = explosion; } - - private class LegacyRollingCounter : RollingCounter - { - private readonly ISkin skin; - - private readonly string fontName; - private readonly float fontOverlap; - - protected override bool IsRollingProportional => true; - - public LegacyRollingCounter(ISkin skin, string fontName, float fontOverlap) - { - this.skin = skin; - this.fontName = fontName; - this.fontOverlap = fontOverlap; - } - - protected override double GetProportionalDuration(int currentValue, int newValue) - { - return Math.Abs(newValue - currentValue) * 75.0; - } - - protected override OsuSpriteText CreateSpriteText() - { - return new LegacySpriteText(skin, fontName) - { - Spacing = new Vector2(-fontOverlap, 0f) - }; - } - } } } diff --git a/osu.Game/Skinning/LegacyRollingCounter.cs b/osu.Game/Skinning/LegacyRollingCounter.cs new file mode 100644 index 0000000000..8aa9d4e9af --- /dev/null +++ b/osu.Game/Skinning/LegacyRollingCounter.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Skinning +{ + /// + /// An integer that uses number sprites from a legacy skin. + /// + public class LegacyRollingCounter : RollingCounter + { + private readonly ISkin skin; + + private readonly string fontName; + private readonly float fontOverlap; + + protected override bool IsRollingProportional => true; + + /// + /// Creates a new . + /// + /// The from which to get counter number sprites. + /// The name of the legacy font to use. + /// + /// The numeric overlap of number sprites to use. + /// A positive number will bring the number sprites closer together, while a negative number + /// will split them apart more. + /// + public LegacyRollingCounter(ISkin skin, string fontName, float fontOverlap) + { + this.skin = skin; + this.fontName = fontName; + this.fontOverlap = fontOverlap; + } + + protected override double GetProportionalDuration(int currentValue, int newValue) + { + return Math.Abs(newValue - currentValue) * 75.0; + } + + protected sealed override OsuSpriteText CreateSpriteText() => + new LegacySpriteText(skin, fontName) + { + Spacing = new Vector2(-fontOverlap, 0f) + }; + } +} From fcf3a1d13c9dcad3652062c613c741e049e9ea43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Sep 2020 22:39:06 +0200 Subject: [PATCH 3216/6909] Encapsulate combo display better --- .../TestSceneCatcherArea.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 13 ++----------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 13 +++++++++---- 3 files changed, 12 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index b4f123598b..e055f08dc2 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Catch.Tests Schedule(() => { area.AttemptCatch(fruit); - area.OnResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great }); + area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great }); drawable.Expire(); }); diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 409ea6dbc6..735d7fc300 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.Catch.UI internal readonly CatcherArea CatcherArea; - private CatchComboDisplay comboDisplay => CatcherArea.ComboDisplay; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => // only check the X position; handle all vertical space. base.ReceivePositionalInputAt(new Vector2(screenSpacePos.X, ScreenSpaceDrawQuad.Centre.Y)); @@ -73,16 +71,9 @@ namespace osu.Game.Rulesets.Catch.UI } private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) - { - var catchObject = (DrawableCatchHitObject)judgedObject; - CatcherArea.OnResult(catchObject, result); - - comboDisplay.OnNewResult(catchObject, result); - } + => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); private void onRevertResult(DrawableHitObject judgedObject, JudgementResult result) - { - comboDisplay.OnRevertResult((DrawableCatchHitObject)judgedObject, result); - } + => CatcherArea.OnRevertResult((DrawableCatchHitObject)judgedObject, result); } } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 9cfb9f41d7..d3e63b0333 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.UI public Func> CreateDrawableRepresentation; public readonly Catcher MovableCatcher; - internal readonly CatchComboDisplay ComboDisplay; + private readonly CatchComboDisplay comboDisplay; public Container ExplodingFruitTarget { @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.UI Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); Children = new Drawable[] { - ComboDisplay = new CatchComboDisplay + comboDisplay = new CatchComboDisplay { RelativeSizeAxes = Axes.None, AutoSizeAxes = Axes.Both, @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.UI }; } - public void OnResult(DrawableCatchHitObject fruit, JudgementResult result) + public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result) { if (result.Judgement is IgnoreJudgement) return; @@ -99,8 +99,13 @@ namespace osu.Game.Rulesets.Catch.UI else MovableCatcher.Drop(); } + + comboDisplay.OnNewResult(fruit, result); } + public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) + => comboDisplay.OnRevertResult(fruit, result); + public void OnReleased(CatchAction action) { } @@ -119,7 +124,7 @@ namespace osu.Game.Rulesets.Catch.UI if (state?.CatcherX != null) MovableCatcher.X = state.CatcherX.Value; - ComboDisplay.X = MovableCatcher.X; + comboDisplay.X = MovableCatcher.X; } } } From c573392bb2db182233f2b45c17aa1c0c565c9c54 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 13 Sep 2020 22:31:59 +0900 Subject: [PATCH 3217/6909] Remove completed todo --- osu.Game/Screens/Edit/Editor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ee3befe8bd..d80f899f90 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -256,7 +256,6 @@ 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); - // todo: BindCollectionChanged editorBeatmap.SelectedHitObjects.BindCollectionChanged((_, __) => { var hasObjects = editorBeatmap.SelectedHitObjects.Count > 0; From 320e3143565023804e2e5279a6fc8267c49ec87e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 13 Sep 2020 22:53:30 +0900 Subject: [PATCH 3218/6909] Use minimum start time to handle SelectedHitObjects not being sorted --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index d80f899f90..64365bd512 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -454,7 +454,7 @@ namespace osu.Game.Screens.Edit Debug.Assert(objects.Any()); - double timeOffset = clock.CurrentTime - objects.First().StartTime; + double timeOffset = clock.CurrentTime - objects.Min(o => o.StartTime); foreach (var h in objects) h.StartTime += timeOffset; From 3e37f27a66f65d7a7b938fdb8c8bc1c5edc03d76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 13 Sep 2020 23:22:19 +0900 Subject: [PATCH 3219/6909] Fix regressed tests due to schedule changes --- .../Beatmaps/TestSceneEditorBeatmap.cs | 50 ++++++++----------- 1 file changed, 20 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs index b7b48ec06a..902f0d7c23 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs @@ -23,15 +23,19 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestHitObjectAddEvent() { - var editorBeatmap = new EditorBeatmap(new OsuBeatmap()); - - HitObject addedObject = null; - editorBeatmap.HitObjectAdded += h => addedObject = h; - var hitCircle = new HitCircle(); - editorBeatmap.Add(hitCircle); - Assert.That(addedObject, Is.EqualTo(hitCircle)); + HitObject addedObject = null; + EditorBeatmap editorBeatmap = null; + + AddStep("add beatmap", () => + { + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap.HitObjectAdded += h => addedObject = h; + }); + + AddStep("add hitobject", () => editorBeatmap.Add(hitCircle)); + AddAssert("received add event", () => addedObject == hitCircle); } /// @@ -41,13 +45,15 @@ namespace osu.Game.Tests.Beatmaps public void HitObjectRemoveEvent() { var hitCircle = new HitCircle(); - var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); - HitObject removedObject = null; - editorBeatmap.HitObjectRemoved += h => removedObject = h; - - editorBeatmap.Remove(hitCircle); - Assert.That(removedObject, Is.EqualTo(hitCircle)); + EditorBeatmap editorBeatmap = null; + AddStep("add beatmap", () => + { + Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); + editorBeatmap.HitObjectRemoved += h => removedObject = h; + }); + AddStep("remove hitobject", () => editorBeatmap.Remove(editorBeatmap.HitObjects.First())); + AddAssert("received remove event", () => removedObject == hitCircle); } /// @@ -58,9 +64,7 @@ namespace osu.Game.Tests.Beatmaps public void TestInitialHitObjectStartTimeChangeEvent() { var hitCircle = new HitCircle(); - HitObject changedObject = null; - AddStep("add beatmap", () => { EditorBeatmap editorBeatmap; @@ -68,7 +72,6 @@ namespace osu.Game.Tests.Beatmaps 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); } @@ -82,18 +85,14 @@ namespace osu.Game.Tests.Beatmaps { EditorBeatmap editorBeatmap = null; HitObject changedObject = null; - AddStep("add beatmap", () => { Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); editorBeatmap.HitObjectUpdated += h => changedObject = h; }); - var hitCircle = new HitCircle(); - AddStep("add object", () => editorBeatmap.Add(hitCircle)); AddAssert("event not received", () => changedObject == null); - AddStep("change start time", () => hitCircle.StartTime = 1000); AddAssert("event received", () => changedObject == hitCircle); } @@ -106,13 +105,10 @@ namespace osu.Game.Tests.Beatmaps { var hitCircle = new HitCircle(); var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); - HitObject changedObject = null; editorBeatmap.HitObjectUpdated += h => changedObject = h; - editorBeatmap.Remove(hitCircle); Assert.That(changedObject, Is.Null); - hitCircle.StartTime = 1000; Assert.That(changedObject, Is.Null); } @@ -147,6 +143,7 @@ namespace osu.Game.Tests.Beatmaps public void TestResortWhenStartTimeChanged() { var hitCircle = new HitCircle { StartTime = 1000 }; + var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = @@ -173,7 +170,6 @@ namespace osu.Game.Tests.Beatmaps var updatedObjects = new List(); var allHitObjects = new List(); EditorBeatmap editorBeatmap = null; - AddStep("add beatmap", () => { updatedObjects.Clear(); @@ -187,11 +183,9 @@ namespace osu.Game.Tests.Beatmaps 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; }); @@ -208,7 +202,6 @@ namespace osu.Game.Tests.Beatmaps { var updatedObjects = new List(); EditorBeatmap editorBeatmap = null; - AddStep("add beatmap", () => { updatedObjects.Clear(); @@ -216,15 +209,12 @@ namespace osu.Game.Tests.Beatmaps 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 18ae17e1290a936a4f854d6cbf740c2ad9dbe1f0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 13 Sep 2020 19:55:21 +0200 Subject: [PATCH 3220/6909] Add scale to GroupTeam and remove unnecessary sizing and scaling in other scenes --- osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs | 1 + osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs | 1 - osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs | 2 -- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs index 4f0ce0bbe7..119f71ebfa 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components AcronymText.Origin = Anchor.TopCentre; AcronymText.Text = team.Acronym.Value.ToUpperInvariant(); AcronymText.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 10); + Flag.Scale = new Vector2(0.5f); InternalChildren = new Drawable[] { diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index b01c93ae03..48aea46497 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -29,7 +29,6 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components var anchor = flip ? Anchor.TopLeft : Anchor.TopRight; Flag.RelativeSizeAxes = Axes.None; - Flag.Size = new Vector2(60, 40); Flag.Origin = anchor; Flag.Anchor = anchor; diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 3870f486e1..dde140ab91 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -90,8 +90,6 @@ namespace osu.Game.Tournament.Screens.TeamWin { new DrawableTeamFlag(match.Winner) { - Size = new Vector2(300, 200), - Scale = new Vector2(0.5f), Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(-300, 10), From e328b791dfbaff551cccda920cef2cae410eeee8 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 13 Sep 2020 11:49:16 -0700 Subject: [PATCH 3221/6909] Add failing mod select input test --- .../Navigation/TestSceneScreenNavigation.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 73a833c15d..0901976af2 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -6,9 +6,11 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Mods; +using osu.Game.Overlays.Toolbar; using osu.Game.Screens.Play; using osu.Game.Screens.Select; using osu.Game.Tests.Beatmaps.IO; @@ -143,6 +145,29 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("Track was restarted", () => Game.MusicController.IsPlaying); } + [Test] + public void TestModSelectInput() + { + TestSongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new TestSongSelect()); + + AddStep("Show mods overlay", () => songSelect.ModSelectOverlay.Show()); + + AddStep("Change ruleset to osu!taiko", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.PressKey(Key.Number2); + + InputManager.ReleaseKey(Key.ControlLeft); + InputManager.ReleaseKey(Key.Number2); + }); + + AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType().Single().Current.Value.ID == 1); + + AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); + } + private void pushEscape() => AddStep("Press escape", () => pressAndRelease(Key.Escape)); From 4dacdb9994eecdc9043b19706b6683be1d892025 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 13 Sep 2020 11:50:21 -0700 Subject: [PATCH 3222/6909] Fix mod select overlay absorbing input from toolbar ruleset selector --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 8a5e4d2683..4eb4fc6501 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -390,6 +390,9 @@ namespace osu.Game.Overlays.Mods protected override bool OnKeyDown(KeyDownEvent e) { + // don't absorb control as ToolbarRulesetSelector uses control + number to navigate + if (e.ControlPressed) return false; + switch (e.Key) { case Key.Number1: From 9f1a231f929677d806f0a1ecb4afb9499dda66a0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 13 Sep 2020 21:03:46 +0200 Subject: [PATCH 3223/6909] Add anchor to the fillflowcontainer in TeamDisplay --- osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index b01c93ae03..1b4a769b84 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -62,6 +62,8 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, Spacing = new Vector2(5), + Origin = anchor, + Anchor = anchor, Children = new Drawable[] { new DrawableTeamHeader(colour) From 692f2c8489751852c0ed717d8f9373f3a34c5ffd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 14:45:49 +0900 Subject: [PATCH 3224/6909] Simplify debounced update pathway --- osu.Game/Screens/Edit/Editor.cs | 4 + osu.Game/Screens/Edit/EditorBeatmap.cs | 123 ++++++++++++------------- 2 files changed, 63 insertions(+), 64 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 64365bd512..71340041f0 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -459,10 +459,14 @@ namespace osu.Game.Screens.Edit foreach (var h in objects) h.StartTime += timeOffset; + changeHandler.BeginChange(); + editorBeatmap.SelectedHitObjects.Clear(); editorBeatmap.AddRange(objects); editorBeatmap.SelectedHitObjects.AddRange(objects); + + changeHandler.EndChange(); } protected void Undo() => changeHandler.RestoreState(-1); diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 5272530228..3876fb0903 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -9,7 +9,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; @@ -68,47 +67,6 @@ namespace osu.Game.Screens.Edit trackStartTime(obj); } - private readonly HashSet pendingUpdates = new HashSet(); - private ScheduledDelegate scheduledUpdate; - - /// - /// Updates a , invoking and re-processing the beatmap. - /// - /// The to update. - public void UpdateHitObject([NotNull] HitObject hitObject) => updateHitObject(hitObject, false); - - private void updateHitObject([CanBeNull] HitObject hitObject, bool silent, bool performAdd = false) - { - if (hitObject != null) - pendingUpdates.Add(hitObject); - - if (scheduledUpdate?.Completed == false) return; - - scheduledUpdate = Schedule(() => - { - beatmapProcessor?.PreProcess(); - - foreach (var obj in pendingUpdates) - obj.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); - - beatmapProcessor?.PostProcess(); - - if (performAdd) - { - foreach (var obj in pendingUpdates) - HitObjectAdded?.Invoke(obj); - } - - if (!silent) - { - foreach (var obj in pendingUpdates) - HitObjectUpdated?.Invoke(obj); - } - - pendingUpdates.Clear(); - }); - } - public BeatmapInfo BeatmapInfo { get => PlayableBeatmap.BeatmapInfo; @@ -131,6 +89,8 @@ namespace osu.Game.Screens.Edit private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; + private readonly HashSet pendingUpdates = new HashSet(); + /// /// Adds a collection of s to this . /// @@ -165,23 +125,40 @@ namespace osu.Game.Screens.Edit trackStartTime(hitObject); mutableHitObjects.Insert(index, hitObject); - updateHitObject(hitObject, true, true); + + // must be run after any change to hitobject ordering + beatmapProcessor?.PreProcess(); + processHitObject(hitObject); + beatmapProcessor?.PostProcess(); + + HitObjectAdded?.Invoke(hitObject); + } + + /// + /// Updates a , invoking and re-processing the beatmap. + /// + /// The to update. + public void UpdateHitObject([NotNull] HitObject hitObject) + { + pendingUpdates.Add(hitObject); } /// /// Removes a from this . /// - /// The to add. + /// All to remove. /// True if the has been removed, false otherwise. - public bool Remove(HitObject hitObject) + public void Remove(params HitObject[] hitObjects) { - int index = FindIndex(hitObject); + foreach (var h in hitObjects) + { + int index = FindIndex(h); - if (index == -1) - return false; + if (index == -1) + continue; - RemoveAt(index); - return true; + RemoveAt(index); + } } /// @@ -203,11 +180,14 @@ namespace osu.Game.Screens.Edit var bindable = startTimeBindables[hitObject]; bindable.UnbindAll(); - startTimeBindables.Remove(hitObject); - HitObjectRemoved?.Invoke(hitObject); - updateHitObject(null, true); + // must be run after any change to hitobject ordering + beatmapProcessor?.PreProcess(); + processHitObject(hitObject); + beatmapProcessor?.PostProcess(); + + HitObjectRemoved?.Invoke(hitObject); } /// @@ -215,20 +195,35 @@ namespace osu.Game.Screens.Edit /// public void Clear() { - var removed = HitObjects.ToList(); + var removable = 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); + foreach (var h in removable) + Remove(h); } + protected override void Update() + { + base.Update(); + + // debounce updates as they are common and may come from input events, which can run needlessly many times per update frame. + if (pendingUpdates.Count > 0) + { + beatmapProcessor?.PreProcess(); + + foreach (var hitObject in pendingUpdates) + { + processHitObject(hitObject); + HitObjectUpdated?.Invoke(hitObject); + } + + pendingUpdates.Clear(); + + beatmapProcessor?.PostProcess(); + } + } + + private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); + private void trackStartTime(HitObject hitObject) { startTimeBindables[hitObject] = hitObject.StartTimeBindable.GetBoundCopy(); From da02ee88283a1c9012d4a26dbb6aff5385280671 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 15:26:57 +0900 Subject: [PATCH 3225/6909] Add ability to create a TestBeatmap with no HitObjects --- osu.Game/Tests/Beatmaps/TestBeatmap.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index a375a17bcf..87b77f4616 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -15,14 +15,16 @@ namespace osu.Game.Tests.Beatmaps { public class TestBeatmap : Beatmap { - public TestBeatmap(RulesetInfo ruleset) + public TestBeatmap(RulesetInfo ruleset, bool withHitObjects = true) { var baseBeatmap = CreateBeatmap(); BeatmapInfo = baseBeatmap.BeatmapInfo; ControlPointInfo = baseBeatmap.ControlPointInfo; Breaks = baseBeatmap.Breaks; - HitObjects = baseBeatmap.HitObjects; + + if (withHitObjects) + HitObjects = baseBeatmap.HitObjects; BeatmapInfo.Ruleset = ruleset; BeatmapInfo.RulesetID = ruleset.ID ?? 0; From 0ef4dfc192a6725c9e62c7871bcb749bfdd3bb5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 15:27:08 +0900 Subject: [PATCH 3226/6909] Move more logic to base EditorTestScene --- .../Editing/TestSceneEditorChangeStates.cs | 75 ++++++------------- osu.Game/Tests/Visual/EditorTestScene.cs | 27 ++++++- 2 files changed, 49 insertions(+), 53 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs index dfe1e434dc..ab53f4fd93 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorChangeStates.cs @@ -1,41 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.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; -using osu.Game.Screens.Edit; namespace osu.Game.Tests.Visual.Editing { public class TestSceneEditorChangeStates : EditorTestScene { - private EditorBeatmap editorBeatmap; - protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); - protected new TestEditor Editor => (TestEditor)base.Editor; - - public override void SetUpSteps() - { - base.SetUpSteps(); - - AddStep("get beatmap", () => editorBeatmap = Editor.ChildrenOfType().Single()); - } - [Test] public void TestSelectedObjects() { HitCircle obj = null; - AddStep("add hitobject", () => editorBeatmap.Add(obj = new HitCircle { StartTime = 1000 })); - AddStep("select hitobject", () => editorBeatmap.SelectedHitObjects.Add(obj)); - AddAssert("confirm 1 selected", () => editorBeatmap.SelectedHitObjects.Count == 1); - AddStep("deselect hitobject", () => editorBeatmap.SelectedHitObjects.Remove(obj)); - AddAssert("confirm 0 selected", () => editorBeatmap.SelectedHitObjects.Count == 0); + AddStep("add hitobject", () => EditorBeatmap.Add(obj = new HitCircle { StartTime = 1000 })); + AddStep("select hitobject", () => EditorBeatmap.SelectedHitObjects.Add(obj)); + AddAssert("confirm 1 selected", () => EditorBeatmap.SelectedHitObjects.Count == 1); + AddStep("deselect hitobject", () => EditorBeatmap.SelectedHitObjects.Remove(obj)); + AddAssert("confirm 0 selected", () => EditorBeatmap.SelectedHitObjects.Count == 0); } [Test] @@ -43,11 +29,11 @@ namespace osu.Game.Tests.Visual.Editing { int hitObjectCount = 0; - AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count); + AddStep("get initial state", () => hitObjectCount = EditorBeatmap.HitObjects.Count); addUndoSteps(); - AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + AddAssert("no change occurred", () => hitObjectCount == EditorBeatmap.HitObjects.Count); AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } @@ -56,11 +42,11 @@ namespace osu.Game.Tests.Visual.Editing { int hitObjectCount = 0; - AddStep("get initial state", () => hitObjectCount = editorBeatmap.HitObjects.Count); + AddStep("get initial state", () => hitObjectCount = EditorBeatmap.HitObjects.Count); addRedoSteps(); - AddAssert("no change occurred", () => hitObjectCount == editorBeatmap.HitObjects.Count); + AddAssert("no change occurred", () => hitObjectCount == EditorBeatmap.HitObjects.Count); AddAssert("no unsaved changes", () => !Editor.HasUnsavedChanges); } @@ -73,11 +59,11 @@ namespace osu.Game.Tests.Visual.Editing AddStep("bind removal", () => { - editorBeatmap.HitObjectAdded += h => addedObject = h; - editorBeatmap.HitObjectRemoved += h => removedObject = h; + EditorBeatmap.HitObjectAdded += h => addedObject = h; + EditorBeatmap.HitObjectRemoved += h => removedObject = h; }); - AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); AddAssert("hitobject added", () => addedObject == expectedObject); AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); @@ -95,11 +81,11 @@ namespace osu.Game.Tests.Visual.Editing AddStep("bind removal", () => { - editorBeatmap.HitObjectAdded += h => addedObject = h; - editorBeatmap.HitObjectRemoved += h => removedObject = h; + EditorBeatmap.HitObjectAdded += h => addedObject = h; + EditorBeatmap.HitObjectRemoved += h => removedObject = h; }); - AddStep("add hitobject", () => editorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); addUndoSteps(); AddStep("reset variables", () => @@ -117,7 +103,7 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestAddObjectThenSaveHasNoUnsavedChanges() { - AddStep("add hitobject", () => editorBeatmap.Add(new HitCircle { StartTime = 1000 })); + AddStep("add hitobject", () => EditorBeatmap.Add(new HitCircle { StartTime = 1000 })); AddAssert("unsaved changes", () => Editor.HasUnsavedChanges); AddStep("save changes", () => Editor.Save()); @@ -133,12 +119,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("bind removal", () => { - editorBeatmap.HitObjectAdded += h => addedObject = h; - editorBeatmap.HitObjectRemoved += h => removedObject = h; + 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("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("remove object", () => EditorBeatmap.Remove(expectedObject)); AddStep("reset variables", () => { addedObject = null; @@ -160,12 +146,12 @@ namespace osu.Game.Tests.Visual.Editing AddStep("bind removal", () => { - editorBeatmap.HitObjectAdded += h => addedObject = h; - editorBeatmap.HitObjectRemoved += h => removedObject = h; + 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("add hitobject", () => EditorBeatmap.Add(expectedObject = new HitCircle { StartTime = 1000 })); + AddStep("remove object", () => EditorBeatmap.Remove(expectedObject)); addUndoSteps(); AddStep("reset variables", () => @@ -183,18 +169,5 @@ namespace osu.Game.Tests.Visual.Editing private void addUndoSteps() => AddStep("undo", () => Editor.Undo()); private void addRedoSteps() => AddStep("redo", () => Editor.Redo()); - - protected override Editor CreateEditor() => new TestEditor(); - - protected class TestEditor : Editor - { - public new void Undo() => base.Undo(); - - public new void Redo() => base.Redo(); - - public new void Save() => base.Save(); - - public new bool HasUnsavedChanges => base.HasUnsavedChanges; - } } } diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index cd08f4712a..8f76f247cf 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -14,7 +14,11 @@ namespace osu.Game.Tests.Visual { public abstract class EditorTestScene : ScreenTestScene { - protected Editor Editor { get; private set; } + protected EditorBeatmap EditorBeatmap; + + protected TestEditor Editor { get; private set; } + + protected EditorClock EditorClock { get; private set; } [BackgroundDependencyLoader] private void load() @@ -29,6 +33,8 @@ namespace osu.Game.Tests.Visual AddStep("load editor", () => LoadScreen(Editor = CreateEditor())); 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 clock", () => EditorClock = Editor.ChildrenOfType().Single()); } /// @@ -39,6 +45,23 @@ namespace osu.Game.Tests.Visual protected sealed override Ruleset CreateRuleset() => CreateEditorRuleset(); - protected virtual Editor CreateEditor() => new Editor(); + protected virtual TestEditor CreateEditor() => new TestEditor(); + + protected class TestEditor : Editor + { + public new void Undo() => base.Undo(); + + public new void Redo() => base.Redo(); + + public new void Save() => base.Save(); + + public new void Cut() => base.Cut(); + + public new void Copy() => base.Copy(); + + public new void Paste() => base.Paste(); + + public new bool HasUnsavedChanges => base.HasUnsavedChanges; + } } } From 66faae2a6b9634fc8c7ed3502ae88e22032e480e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 15:27:16 +0900 Subject: [PATCH 3227/6909] Add basic clipboards tests --- .../Editing/TestSceneEditorClipboard.cs | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs new file mode 100644 index 0000000000..284127d66b --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Tests.Beatmaps; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorClipboard : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + [Test] + public void TestCutRemovesObjects() + { + var addedObject = new HitCircle { StartTime = 1000 }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("cut hitobject", () => Editor.Cut()); + + AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); + } + + [TestCase(1000)] + [TestCase(2000)] + public void TestCutPaste(double newTime) + { + var addedObject = new HitCircle { StartTime = 1000 }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("cut hitobject", () => Editor.Cut()); + + AddStep("move forward in time", () => EditorClock.Seek(newTime)); + + AddStep("paste hitobject", () => Editor.Paste()); + + AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1); + + AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == newTime); + } + + [Test] + public void TestCopyPaste() + { + var addedObject = new HitCircle { StartTime = 1000 }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("copy hitobject", () => Editor.Copy()); + + AddStep("move forward in time", () => EditorClock.Seek(2000)); + + AddStep("paste hitobject", () => Editor.Paste()); + + AddAssert("are two objects", () => EditorBeatmap.HitObjects.Count == 2); + + AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == 2000); + } + + [Test] + public void TestCutNothing() + { + AddStep("cut hitobject", () => Editor.Cut()); + AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0); + } + + [Test] + public void TestCopyNothing() + { + AddStep("copy hitobject", () => Editor.Copy()); + AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0); + } + + [Test] + public void TestPasteNothing() + { + AddStep("paste hitobject", () => Editor.Paste()); + AddAssert("are no objects", () => EditorBeatmap.HitObjects.Count == 0); + } + } +} From 75e4f224e5a1d0a2f21db48d711dce86f1ee4239 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 15:47:04 +0900 Subject: [PATCH 3228/6909] Add back accidentally removed remove --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 0a3c9072cf..60e25b01df 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -160,6 +160,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. internal void HandleDeselected(SelectionBlueprint blueprint) { + selectedBlueprints.Remove(blueprint); + EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); UpdateVisibility(); From b7a06524fb94c77f0c1719047b0f58466ab2a52a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 15:47:10 +0900 Subject: [PATCH 3229/6909] Update comment to make more sense --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 60e25b01df..f95bf350b6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { selectedBlueprints.Add(blueprint); - // need to check this as well, as there are potentially multiple SelectionHandlers and the above check is not enough. + // there are potentially multiple SelectionHandlers active, but we only want to add hitobjects to the selected list once. if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject)) EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); From 3e7f70e225625429fd3cdc1b1d68b9746b2abd09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 17:27:25 +0900 Subject: [PATCH 3230/6909] Add failing test covering post-converted json serializing --- .../Beatmaps/Formats/OsuJsonDecoderTest.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs index b4c78ce273..e97c83e2c2 100644 --- a/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/OsuJsonDecoderTest.cs @@ -11,6 +11,8 @@ using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.IO.Serialization; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Resources; using osuTK; @@ -90,6 +92,38 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(2, difficulty.SliderTickRate); } + [Test] + public void TestDecodePostConverted() + { + var converted = new OsuBeatmapConverter(decodeAsJson(normal), new OsuRuleset()).Convert(); + + var processor = new OsuBeatmapProcessor(converted); + + processor.PreProcess(); + foreach (var o in converted.HitObjects) + o.ApplyDefaults(converted.ControlPointInfo, converted.BeatmapInfo.BaseDifficulty); + processor.PostProcess(); + + var beatmap = converted.Serialize().Deserialize(); + + var curveData = beatmap.HitObjects[0] as IHasPathWithRepeats; + var positionData = beatmap.HitObjects[0] as IHasPosition; + + Assert.IsNotNull(positionData); + Assert.IsNotNull(curveData); + Assert.AreEqual(90, curveData.Path.Distance); + Assert.AreEqual(new Vector2(192, 168), positionData.Position); + Assert.AreEqual(956, beatmap.HitObjects[0].StartTime); + Assert.IsTrue(beatmap.HitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL)); + + positionData = beatmap.HitObjects[1] as IHasPosition; + + Assert.IsNotNull(positionData); + Assert.AreEqual(new Vector2(304, 56), positionData.Position); + Assert.AreEqual(1285, beatmap.HitObjects[1].StartTime); + Assert.IsTrue(beatmap.HitObjects[1].Samples.Any(s => s.Name == HitSampleInfo.HIT_CLAP)); + } + [Test] public void TestDecodeHitObjects() { @@ -100,6 +134,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsNotNull(positionData); Assert.IsNotNull(curveData); + Assert.AreEqual(90, curveData.Path.Distance); Assert.AreEqual(new Vector2(192, 168), positionData.Position); Assert.AreEqual(956, beatmap.HitObjects[0].StartTime); Assert.IsTrue(beatmap.HitObjects[0].Samples.Any(s => s.Name == HitSampleInfo.HIT_NORMAL)); From a8b405791a76c320dfd397ac7be261dac07f2c7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 17:08:22 +0900 Subject: [PATCH 3231/6909] Fix non-convert slider and spinner serialization --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 9 +++++++-- osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs | 2 ++ osu.Game/Rulesets/Objects/PathControlPoint.cs | 3 +++ osu.Game/Rulesets/Objects/Types/IHasDuration.cs | 3 --- 4 files changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 705e88040f..51f6a44a87 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using osu.Game.Rulesets.Objects; using System.Linq; using System.Threading; +using Newtonsoft.Json; using osu.Framework.Caching; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -21,6 +22,7 @@ namespace osu.Game.Rulesets.Osu.Objects { public double EndTime => StartTime + this.SpanCount() * Path.Distance / Velocity; + [JsonIgnore] public double Duration { get => EndTime - StartTime; @@ -112,8 +114,11 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double TickDistanceMultiplier = 1; - public HitCircle HeadCircle; - public SliderTailCircle TailCircle; + [JsonIgnore] + public HitCircle HeadCircle { get; protected set; } + + [JsonIgnore] + public SliderTailCircle TailCircle { get; protected set; } public Slider() { diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs index c522dc623c..36b421586e 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -3,6 +3,7 @@ using osu.Game.Rulesets.Objects.Types; using System.Collections.Generic; +using Newtonsoft.Json; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -29,6 +30,7 @@ namespace osu.Game.Rulesets.Objects.Legacy public List> NodeSamples { get; set; } public int RepeatCount { get; set; } + [JsonIgnore] public double Duration { get => this.SpanCount() * Distance / Velocity; diff --git a/osu.Game/Rulesets/Objects/PathControlPoint.cs b/osu.Game/Rulesets/Objects/PathControlPoint.cs index 0336f94313..f11917f4f4 100644 --- a/osu.Game/Rulesets/Objects/PathControlPoint.cs +++ b/osu.Game/Rulesets/Objects/PathControlPoint.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; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -13,12 +14,14 @@ namespace osu.Game.Rulesets.Objects /// /// The position of this . /// + [JsonProperty] public readonly Bindable Position = new Bindable(); /// /// The type of path segment starting at this . /// If null, this will be a part of the previous path segment. /// + [JsonProperty] public readonly Bindable Type = new Bindable(); /// diff --git a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs index 185fd5977b..b558273650 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasDuration.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 Newtonsoft.Json; - namespace osu.Game.Rulesets.Objects.Types { /// @@ -28,7 +26,6 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The duration of the HitObject. /// - [JsonIgnore] new double Duration { get; set; } } } From 36a234e5d95c283ef1cbeed754eefe4e2bcd9874 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 17:43:27 +0900 Subject: [PATCH 3232/6909] Add slider specific clipboard test --- .../Editing/TestSceneEditorClipboard.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs index 284127d66b..808d471e36 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs @@ -5,9 +5,12 @@ using System.Linq; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Tests.Beatmaps; +using osuTK; namespace osu.Game.Tests.Visual.Editing { @@ -52,6 +55,39 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("new object selected", () => EditorBeatmap.SelectedHitObjects.Single().StartTime == newTime); } + [Test] + public void TestCutPasteSlider() + { + var addedObject = new Slider + { + StartTime = 1000, + Path = new SliderPath + { + ControlPoints = + { + new PathControlPoint(), + new PathControlPoint(new Vector2(100, 0), PathType.Bezier) + } + } + }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("cut hitobject", () => Editor.Cut()); + + AddStep("paste hitobject", () => Editor.Paste()); + + AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1); + + AddAssert("path matches", () => + { + var path = ((Slider)EditorBeatmap.HitObjects.Single()).Path; + return path.ControlPoints.Count == 2 && path.ControlPoints.SequenceEqual(addedObject.Path.ControlPoints); + }); + } + [Test] public void TestCopyPaste() { From dafbeda68136ad134f5df3e09e7c93d902717264 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 17:48:29 +0900 Subject: [PATCH 3233/6909] Add test coverage for spinners too --- .../Editing/TestSceneEditorClipboard.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs index 808d471e36..29046c82a6 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorClipboard.cs @@ -88,6 +88,28 @@ namespace osu.Game.Tests.Visual.Editing }); } + [Test] + public void TestCutPasteSpinner() + { + var addedObject = new Spinner + { + StartTime = 1000, + Duration = 5000 + }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("cut hitobject", () => Editor.Cut()); + + AddStep("paste hitobject", () => Editor.Paste()); + + AddAssert("is one object", () => EditorBeatmap.HitObjects.Count == 1); + + AddAssert("duration matches", () => ((Spinner)EditorBeatmap.HitObjects.Single()).Duration == 5000); + } + [Test] public void TestCopyPaste() { From 70bc0b2bd025e234880bca5408b3ea6116ef8d7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 17:52:59 +0900 Subject: [PATCH 3234/6909] Add back inadvertently removed spacing --- .../Beatmaps/TestSceneEditorBeatmap.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs index 902f0d7c23..bf5b517603 100644 --- a/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs +++ b/osu.Game.Tests/Beatmaps/TestSceneEditorBeatmap.cs @@ -64,7 +64,9 @@ namespace osu.Game.Tests.Beatmaps public void TestInitialHitObjectStartTimeChangeEvent() { var hitCircle = new HitCircle(); + HitObject changedObject = null; + AddStep("add beatmap", () => { EditorBeatmap editorBeatmap; @@ -72,6 +74,7 @@ namespace osu.Game.Tests.Beatmaps 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); } @@ -85,14 +88,18 @@ namespace osu.Game.Tests.Beatmaps { EditorBeatmap editorBeatmap = null; HitObject changedObject = null; + AddStep("add beatmap", () => { Child = editorBeatmap = new EditorBeatmap(new OsuBeatmap()); editorBeatmap.HitObjectUpdated += h => changedObject = h; }); + var hitCircle = new HitCircle(); + AddStep("add object", () => editorBeatmap.Add(hitCircle)); AddAssert("event not received", () => changedObject == null); + AddStep("change start time", () => hitCircle.StartTime = 1000); AddAssert("event received", () => changedObject == hitCircle); } @@ -105,10 +112,13 @@ namespace osu.Game.Tests.Beatmaps { var hitCircle = new HitCircle(); var editorBeatmap = new EditorBeatmap(new OsuBeatmap { HitObjects = { hitCircle } }); + HitObject changedObject = null; editorBeatmap.HitObjectUpdated += h => changedObject = h; + editorBeatmap.Remove(hitCircle); Assert.That(changedObject, Is.Null); + hitCircle.StartTime = 1000; Assert.That(changedObject, Is.Null); } @@ -170,6 +180,7 @@ namespace osu.Game.Tests.Beatmaps var updatedObjects = new List(); var allHitObjects = new List(); EditorBeatmap editorBeatmap = null; + AddStep("add beatmap", () => { updatedObjects.Clear(); @@ -183,9 +194,11 @@ namespace osu.Game.Tests.Beatmaps 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; }); @@ -202,6 +215,7 @@ namespace osu.Game.Tests.Beatmaps { var updatedObjects = new List(); EditorBeatmap editorBeatmap = null; + AddStep("add beatmap", () => { updatedObjects.Clear(); @@ -209,12 +223,15 @@ namespace osu.Game.Tests.Beatmaps 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 daf54c7eb962c6abe35c04202da6c7c9d71e12b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 17:55:41 +0900 Subject: [PATCH 3235/6909] Revert EditorBeatmap.Remove API --- osu.Game/Screens/Edit/EditorBeatmap.cs | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 3876fb0903..3a9bd85b0f 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -146,19 +146,17 @@ namespace osu.Game.Screens.Edit /// /// Removes a from this . /// - /// All to remove. + /// The to remove. /// True if the has been removed, false otherwise. - public void Remove(params HitObject[] hitObjects) + public bool Remove(HitObject hitObject) { - foreach (var h in hitObjects) - { - int index = FindIndex(h); + int index = FindIndex(hitObject); - if (index == -1) - continue; + if (index == -1) + return false; - RemoveAt(index); - } + RemoveAt(index); + return true; } /// @@ -195,9 +193,7 @@ namespace osu.Game.Screens.Edit /// public void Clear() { - var removable = HitObjects.ToList(); - - foreach (var h in removable) + foreach (var h in HitObjects.ToArray()) Remove(h); } From 91d37e0459e37f1caacfb7c623b05cd83d454848 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 20:17:00 +0900 Subject: [PATCH 3236/6909] Fix typo in comment --- osu.Game/Skinning/SkinManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index eacfdaec4a..303c59b05e 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -98,7 +98,7 @@ namespace osu.Game.Skinning return item.ToString().ComputeSHA2Hash(); } - // if there was no creator, the ToString above would give the filename, which along isn't really enough to base any decisions on. + // if there was no creator, the ToString above would give the filename, which alone isn't really enough to base any decisions on. return base.ComputeHash(item, reader); } From 1884e0167bdb5bfa1d2769e00874520b39af8c71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Sep 2020 23:31:03 +0900 Subject: [PATCH 3237/6909] Eagerly populate skin metadata to allow usage in hashing computation --- osu.Game/Database/ArchiveModelManager.cs | 3 +-- osu.Game/Skinning/SkinManager.cs | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index e87ab8167a..76bc4f7755 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -321,11 +321,10 @@ namespace osu.Game.Database LogForModel(item, "Beginning import..."); item.Files = archive != null ? createFileInfos(archive, Files) : new List(); + item.Hash = ComputeHash(item, archive); await Populate(item, archive, cancellationToken); - item.Hash = ComputeHash(item, archive); - using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. { try diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 303c59b05e..ee4b7bc8e7 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -91,6 +91,10 @@ namespace osu.Game.Skinning protected override string ComputeHash(SkinInfo item, ArchiveReader reader = null) { + // we need to populate early to create a hash based off skin.ini contents + if (item.Name?.Contains(".osk") == true) + populateMetadata(item); + if (item.Creator != null && item.Creator != unknown_creator_string) { // this is the optimal way to hash legacy skins, but will need to be reconsidered when we move forward with skin implementation. @@ -106,17 +110,23 @@ namespace osu.Game.Skinning { await base.Populate(model, archive, cancellationToken); - Skin reference = GetSkin(model); + if (model.Name?.Contains(".osk") == true) + populateMetadata(model); + } + + private void populateMetadata(SkinInfo item) + { + Skin reference = GetSkin(item); if (!string.IsNullOrEmpty(reference.Configuration.SkinInfo.Name)) { - model.Name = reference.Configuration.SkinInfo.Name; - model.Creator = reference.Configuration.SkinInfo.Creator; + item.Name = reference.Configuration.SkinInfo.Name; + item.Creator = reference.Configuration.SkinInfo.Creator; } else { - model.Name = model.Name.Replace(".osk", ""); - model.Creator ??= unknown_creator_string; + item.Name = item.Name.Replace(".osk", ""); + item.Creator ??= unknown_creator_string; } } From a377cccb4db62c9ed91c843c3511bdcd7f6f1786 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Sep 2020 17:03:09 +0200 Subject: [PATCH 3238/6909] Unsubscribe from track changed event on disposal --- osu.Game/Screens/Select/SongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 683e3abcc2..2312985e1b 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -642,6 +642,7 @@ namespace osu.Game.Screens.Select base.Dispose(isDisposing); decoupledRuleset.UnbindAll(); + music.TrackChanged -= ensureTrackLooping; } /// From 368aca015a43d918c6e06fe7d5f25e83e8f01678 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 14 Sep 2020 11:18:00 -0700 Subject: [PATCH 3239/6909] Move override methods to bottom --- .../Select/Options/BeatmapOptionsOverlay.cs | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index c01970f536..87e4505cd2 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -27,33 +27,6 @@ namespace osu.Game.Screens.Select.Options public override bool BlockScreenWideMouse => false; - protected override void PopIn() - { - base.PopIn(); - - this.FadeIn(transition_duration, Easing.OutQuint); - - if (buttonsContainer.Position.X == 1 || Alpha == 0) - buttonsContainer.MoveToX(x_position - x_movement); - - holder.ScaleTo(new Vector2(1, 1), transition_duration / 2, Easing.OutQuint); - - buttonsContainer.MoveToX(x_position, transition_duration, Easing.OutQuint); - buttonsContainer.TransformSpacingTo(Vector2.Zero, transition_duration, Easing.OutQuint); - } - - protected override void PopOut() - { - base.PopOut(); - - holder.ScaleTo(new Vector2(1, 0), transition_duration / 2, Easing.InSine); - - buttonsContainer.MoveToX(x_position + x_movement, transition_duration, Easing.InSine); - buttonsContainer.TransformSpacingTo(new Vector2(200f, 0f), transition_duration, Easing.InSine); - - this.FadeOut(transition_duration, Easing.InQuint); - } - public BeatmapOptionsOverlay() { AutoSizeAxes = Axes.Y; @@ -107,5 +80,32 @@ namespace osu.Game.Screens.Select.Options buttonsContainer.Add(button); } + + protected override void PopIn() + { + base.PopIn(); + + this.FadeIn(transition_duration, Easing.OutQuint); + + if (buttonsContainer.Position.X == 1 || Alpha == 0) + buttonsContainer.MoveToX(x_position - x_movement); + + holder.ScaleTo(new Vector2(1, 1), transition_duration / 2, Easing.OutQuint); + + buttonsContainer.MoveToX(x_position, transition_duration, Easing.OutQuint); + buttonsContainer.TransformSpacingTo(Vector2.Zero, transition_duration, Easing.OutQuint); + } + + protected override void PopOut() + { + base.PopOut(); + + holder.ScaleTo(new Vector2(1, 0), transition_duration / 2, Easing.InSine); + + buttonsContainer.MoveToX(x_position + x_movement, transition_duration, Easing.InSine); + buttonsContainer.TransformSpacingTo(new Vector2(200f, 0f), transition_duration, Easing.InSine); + + this.FadeOut(transition_duration, Easing.InQuint); + } } } From 1a8a7ae7f8fd0271c779ba15dec8aace09ac5cb4 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 14 Sep 2020 11:19:18 -0700 Subject: [PATCH 3240/6909] Remove hardcoded key param from AddButton --- .../TestSceneBeatmapOptionsOverlay.cs | 9 +++---- .../Select/Options/BeatmapOptionsButton.cs | 13 ---------- .../Select/Options/BeatmapOptionsOverlay.cs | 25 ++++++++++++++++--- osu.Game/Screens/Select/PlaySongSelect.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 6 ++--- 5 files changed, 30 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs index f55c099d83..82d0c63917 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs @@ -5,7 +5,6 @@ using System.ComponentModel; using osu.Framework.Graphics.Sprites; using osu.Game.Screens.Select.Options; using osuTK.Graphics; -using osuTK.Input; namespace osu.Game.Tests.Visual.SongSelect { @@ -16,10 +15,10 @@ namespace osu.Game.Tests.Visual.SongSelect { var overlay = new BeatmapOptionsOverlay(); - overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, Color4.Purple, null, Key.Number1); - overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, Color4.Purple, null, Key.Number2); - overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, Color4.Pink, null, Key.Number3); - overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null, Key.Number4); + overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, Color4.Purple, null); + overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, Color4.Purple, null); + overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, Color4.Pink, null); + overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null); Add(overlay); diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 4e4653cb57..bd610608b9 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -52,8 +52,6 @@ namespace osu.Game.Screens.Select.Options set => secondLine.Text = value; } - public Key? HotKey; - protected override bool OnMouseDown(MouseDownEvent e) { flash.FadeTo(0.1f, 1000, Easing.OutQuint); @@ -75,17 +73,6 @@ namespace osu.Game.Screens.Select.Options return base.OnClick(e); } - protected override bool OnKeyDown(KeyDownEvent e) - { - if (!e.Repeat && e.Key == HotKey) - { - Click(); - return true; - } - - return false; - } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); public BeatmapOptionsButton() diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index 87e4505cd2..70cbc7d588 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -11,6 +11,8 @@ using osuTK; using osuTK.Graphics; using osuTK.Input; using osu.Game.Graphics.Containers; +using osu.Framework.Input.Events; +using System.Linq; namespace osu.Game.Screens.Select.Options { @@ -60,9 +62,8 @@ namespace osu.Game.Screens.Select.Options /// Text in the second line. /// Colour of the button. /// Icon of the button. - /// Hotkey of the button. /// Binding the button does. - public void AddButton(string firstLine, string secondLine, IconUsage icon, Color4 colour, Action action, Key? hotkey = null) + public void AddButton(string firstLine, string secondLine, IconUsage icon, Color4 colour, Action action) { var button = new BeatmapOptionsButton { @@ -75,7 +76,6 @@ namespace osu.Game.Screens.Select.Options Hide(); action?.Invoke(); }, - HotKey = hotkey }; buttonsContainer.Add(button); @@ -107,5 +107,24 @@ namespace osu.Game.Screens.Select.Options this.FadeOut(transition_duration, Easing.InQuint); } + + protected override bool OnKeyDown(KeyDownEvent e) + { + if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9) + { + int requested = e.Key - Key.Number1; + + // go reverse as buttonsContainer is a ReverseChildIDFillFlowContainer + BeatmapOptionsButton found = buttonsContainer.Children.ElementAtOrDefault((buttonsContainer.Children.Count - 1) - requested); + + if (found != null) + { + found.Click(); + return true; + } + } + + return base.OnKeyDown(e); + } } } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 2236aa4d72..19769f487d 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Select { ValidForResume = false; Edit(); - }, Key.Number4); + }); ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore; } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index d313f67446..f5a7c54519 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -275,9 +275,9 @@ namespace osu.Game.Screens.Select Footer.AddButton(new FooterButtonRandom { Action = triggerRandom }); Footer.AddButton(new FooterButtonOptions(), BeatmapOptions); - BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null, Key.Number1); - BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo), Key.Number2); - BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo), Key.Number3); + BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); + BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); + BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); } dialogOverlay = dialog; From ce9c63970cce934caa8b06303780160251d7ff72 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 14 Sep 2020 11:20:43 -0700 Subject: [PATCH 3241/6909] Fix button colors in beatmap options test --- .../SongSelect/TestSceneBeatmapOptionsOverlay.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs index 82d0c63917..61e61af028 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs @@ -3,8 +3,8 @@ using System.ComponentModel; using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Screens.Select.Options; -using osuTK.Graphics; namespace osu.Game.Tests.Visual.SongSelect { @@ -15,10 +15,12 @@ namespace osu.Game.Tests.Visual.SongSelect { var overlay = new BeatmapOptionsOverlay(); - overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, Color4.Purple, null); - overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, Color4.Purple, null); - overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, Color4.Pink, null); - overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, Color4.Yellow, null); + var colours = new OsuColour(); + + overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); + overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, null); + overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, null); + overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, null); Add(overlay); From c30174cea36744b881bc236d2f8085c887854972 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 14 Sep 2020 11:21:23 -0700 Subject: [PATCH 3242/6909] Add manage collections button to beatmap options --- .../Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs | 1 + osu.Game/Screens/Select/SongSelect.cs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs index 61e61af028..cab47dca65 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs @@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.SongSelect var colours = new OsuColour(); + overlay.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, null); overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, null); overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, null); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index f5a7c54519..482d469cc3 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -103,6 +103,9 @@ namespace osu.Game.Screens.Select [Resolved] private MusicController music { get; set; } + [Resolved(CanBeNull = true)] + private ManageCollectionsDialog manageCollectionsDialog { get; set; } + [BackgroundDependencyLoader(true)] private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections) { @@ -275,6 +278,7 @@ namespace osu.Game.Screens.Select Footer.AddButton(new FooterButtonRandom { Action = triggerRandom }); Footer.AddButton(new FooterButtonOptions(), BeatmapOptions); + BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); From a09bd787f0b5b20b6a678e2a8cdeb1ba2c5c614a Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 14 Sep 2020 11:21:39 -0700 Subject: [PATCH 3243/6909] Add failing beatmap options input test --- .../Navigation/TestSceneScreenNavigation.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 0901976af2..c96952431a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -13,6 +13,7 @@ using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; using osu.Game.Screens.Play; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; using osuTK; using osuTK.Input; @@ -168,6 +169,29 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Mods overlay still visible", () => songSelect.ModSelectOverlay.State.Value == Visibility.Visible); } + [Test] + public void TestBeatmapOptionsInput() + { + TestSongSelect songSelect = null; + + PushAndConfirm(() => songSelect = new TestSongSelect()); + + AddStep("Show options overlay", () => songSelect.BeatmapOptionsOverlay.Show()); + + AddStep("Change ruleset to osu!taiko", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.PressKey(Key.Number2); + + InputManager.ReleaseKey(Key.ControlLeft); + InputManager.ReleaseKey(Key.Number2); + }); + + AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType().Single().Current.Value.ID == 1); + + AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible); + } + private void pushEscape() => AddStep("Press escape", () => pressAndRelease(Key.Escape)); @@ -193,6 +217,8 @@ namespace osu.Game.Tests.Visual.Navigation private class TestSongSelect : PlaySongSelect { public ModSelectOverlay ModSelectOverlay => ModSelect; + + public BeatmapOptionsOverlay BeatmapOptionsOverlay => BeatmapOptions; } } } From 57610ddad51c1a07bc346f2e6beab161b97058ee Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 14 Sep 2020 11:22:16 -0700 Subject: [PATCH 3244/6909] Fix beatmap options absorbing input from toolbar ruleset selector --- osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs index 70cbc7d588..2676635764 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsOverlay.cs @@ -110,6 +110,9 @@ namespace osu.Game.Screens.Select.Options protected override bool OnKeyDown(KeyDownEvent e) { + // don't absorb control as ToolbarRulesetSelector uses control + number to navigate + if (e.ControlPressed) return false; + if (!e.Repeat && e.Key >= Key.Number1 && e.Key <= Key.Number9) { int requested = e.Key - Key.Number1; From c833f5fcc4b046da16676b0a4a5aae58d799927d Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 14 Sep 2020 11:23:41 -0700 Subject: [PATCH 3245/6909] Reorder buttons to match stable --- .../Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs index cab47dca65..e9742acdde 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapOptionsOverlay.cs @@ -18,9 +18,9 @@ namespace osu.Game.Tests.Visual.SongSelect var colours = new OsuColour(); overlay.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, null); + overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, null); overlay.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); overlay.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, null); - overlay.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, null); overlay.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, null); Add(overlay); diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 482d469cc3..ed2e24c5bf 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -279,9 +279,9 @@ namespace osu.Game.Screens.Select Footer.AddButton(new FooterButtonOptions(), BeatmapOptions); BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); + BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); BeatmapOptions.AddButton(@"Remove", @"from unplayed", FontAwesome.Regular.TimesCircle, colours.Purple, null); BeatmapOptions.AddButton(@"Clear", @"local scores", FontAwesome.Solid.Eraser, colours.Purple, () => clearScores(Beatmap.Value.BeatmapInfo)); - BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); } dialogOverlay = dialog; From 43daabc98242bec098d78285f542d0179b817495 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 14 Sep 2020 12:10:00 -0700 Subject: [PATCH 3246/6909] Remove unused using and move dialog to BDL --- osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs | 1 - osu.Game/Screens/Select/SongSelect.cs | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index bd610608b9..6e2f3cc9df 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -12,7 +12,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; using osuTK.Graphics; -using osuTK.Input; using osu.Game.Graphics.Containers; namespace osu.Game.Screens.Select.Options diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index ed2e24c5bf..180752a579 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -103,11 +103,8 @@ namespace osu.Game.Screens.Select [Resolved] private MusicController music { get; set; } - [Resolved(CanBeNull = true)] - private ManageCollectionsDialog manageCollectionsDialog { get; set; } - [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections) + private void load(AudioManager audio, DialogOverlay dialog, OsuColour colours, SkinManager skins, ScoreManager scores, CollectionManager collections, ManageCollectionsDialog manageCollectionsDialog) { // initial value transfer is required for FilterControl (it uses our re-cached bindables in its async load for the initial filter). transferRulesetValue(); From 15e423157b4c49db9764ad99162341f11a37110c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 14:01:29 +0900 Subject: [PATCH 3247/6909] Fix tests that access LocalStorage before BDL --- .../TestSceneManageCollectionsDialog.cs | 27 +++++++------------ .../SongSelect/TestSceneFilterControl.cs | 22 +++++---------- 2 files changed, 15 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 54ab20af7f..55b61bc54a 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -22,44 +22,35 @@ namespace osu.Game.Tests.Visual.Collections { public class TestSceneManageCollectionsDialog : OsuManualInputManagerTestScene { - protected override Container Content => content; + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - private readonly Container content; - private readonly DialogOverlay dialogOverlay; - private readonly CollectionManager manager; + private DialogOverlay dialogOverlay; + private CollectionManager manager; private RulesetStore rulesets; private BeatmapManager beatmapManager; private ManageCollectionsDialog dialog; - public TestSceneManageCollectionsDialog() + [BackgroundDependencyLoader] + private void load(GameHost host) { base.Content.AddRange(new Drawable[] { manager = new CollectionManager(LocalStorage), - content = new Container { RelativeSizeAxes = Axes.Both }, + Content, dialogOverlay = new DialogOverlay() }); - } - [BackgroundDependencyLoader] - private void load(GameHost host) - { + Dependencies.Cache(manager); + Dependencies.Cache(dialogOverlay); + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(manager); - dependencies.Cache(dialogOverlay); - return dependencies; - } - [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 7cd4791acb..0f03368296 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -23,41 +23,31 @@ namespace osu.Game.Tests.Visual.SongSelect { public class TestSceneFilterControl : OsuManualInputManagerTestScene { - protected override Container Content => content; - private readonly Container content; + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; - private readonly CollectionManager collectionManager; + private CollectionManager collectionManager; private RulesetStore rulesets; private BeatmapManager beatmapManager; private FilterControl control; - public TestSceneFilterControl() + [BackgroundDependencyLoader] + private void load(GameHost host) { base.Content.AddRange(new Drawable[] { collectionManager = new CollectionManager(LocalStorage), - content = new Container { RelativeSizeAxes = Axes.Both } + Content }); - } - [BackgroundDependencyLoader] - private void load(GameHost host) - { + Dependencies.Cache(collectionManager); Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.Cache(collectionManager); - return dependencies; - } - [SetUp] public void SetUp() => Schedule(() => { From 234152b2fe6a886051acf5aea1d49edaf343bac9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 14:03:36 +0900 Subject: [PATCH 3248/6909] Use host storage as LocalStorage for headless test runs --- osu.Game/Tests/Visual/OsuTestScene.cs | 32 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 4db5139813..6286809595 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -46,7 +46,7 @@ namespace osu.Game.Tests.Visual private Lazy localStorage; protected Storage LocalStorage => localStorage.Value; - private readonly Lazy contextFactory; + private Lazy contextFactory; protected IAPIProvider API { @@ -71,6 +71,17 @@ namespace osu.Game.Tests.Visual protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { + contextFactory = new Lazy(() => + { + var factory = new DatabaseContextFactory(LocalStorage); + factory.ResetDatabase(); + using (var usage = factory.Get()) + usage.Migrate(); + return factory; + }); + + RecycleLocalStorage(); + var baseDependencies = base.CreateChildDependencies(parent); var providedRuleset = CreateRuleset(); @@ -104,16 +115,6 @@ namespace osu.Game.Tests.Visual protected OsuTestScene() { - RecycleLocalStorage(); - contextFactory = new Lazy(() => - { - var factory = new DatabaseContextFactory(LocalStorage); - factory.ResetDatabase(); - using (var usage = factory.Get()) - usage.Migrate(); - return factory; - }); - base.Content.Add(content = new DrawSizePreservingFillContainer()); } @@ -131,9 +132,14 @@ namespace osu.Game.Tests.Visual } } - localStorage = new Lazy(() => new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); + localStorage = host is HeadlessGameHost + ? new Lazy(() => host.Storage) + : new Lazy(() => new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); } + [Resolved] + private GameHost host { get; set; } + [Resolved] protected AudioManager Audio { get; private set; } @@ -172,7 +178,7 @@ namespace osu.Game.Tests.Visual if (MusicController?.TrackLoaded == true) MusicController.CurrentTrack.Stop(); - if (contextFactory.IsValueCreated) + if (contextFactory?.IsValueCreated == true) contextFactory.Value.ResetDatabase(); RecycleLocalStorage(); From 879979ef57f4b8139c1f37d117f2229d84f3d45b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 14:25:31 +0900 Subject: [PATCH 3249/6909] Move host lookup to inside lazy retrieval to handle edge cases --- osu.Game/Tests/Visual/OsuTestScene.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 6286809595..611bc8f30f 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -132,9 +132,8 @@ namespace osu.Game.Tests.Visual } } - localStorage = host is HeadlessGameHost - ? new Lazy(() => host.Storage) - : new Lazy(() => new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); + localStorage = + new Lazy(() => host is HeadlessGameHost ? host.Storage : new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); } [Resolved] From 2c7492d717fad46a677b09749b37aabeb1781f5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 14:34:58 +0900 Subject: [PATCH 3250/6909] Add null check in SongSelect disposal for safety --- osu.Game/Screens/Select/SongSelect.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2312985e1b..260ab0e89f 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -642,7 +642,9 @@ namespace osu.Game.Screens.Select base.Dispose(isDisposing); decoupledRuleset.UnbindAll(); - music.TrackChanged -= ensureTrackLooping; + + if (music != null) + music.TrackChanged -= ensureTrackLooping; } /// From 0446bc861043a80862bb39a2743c5087579774a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 14:43:24 +0900 Subject: [PATCH 3251/6909] Fix game.ini getting left over by PlayerTestScene subclasses --- osu.Game/Tests/Visual/PlayerTestScene.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index 2c46e7f6d3..aa3bd2e4b7 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -81,6 +81,12 @@ namespace osu.Game.Tests.Visual LoadScreen(Player); } + protected override void Dispose(bool isDisposing) + { + LocalConfig?.Dispose(); + base.Dispose(isDisposing); + } + /// /// Creates the ruleset for setting up the component. /// From 3242b10187f77e55f171e71610e17026187e30e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 15:00:04 +0900 Subject: [PATCH 3252/6909] Change order of dependency caching to promote use of locals --- .../Collections/TestSceneManageCollectionsDialog.cs | 10 +++++----- .../Visual/SongSelect/TestSceneFilterControl.cs | 9 +++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 55b61bc54a..fef1605f0c 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -35,6 +35,11 @@ namespace osu.Game.Tests.Visual.Collections [BackgroundDependencyLoader] private void load(GameHost host) { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); + + beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + base.Content.AddRange(new Drawable[] { manager = new CollectionManager(LocalStorage), @@ -44,11 +49,6 @@ namespace osu.Game.Tests.Visual.Collections Dependencies.Cache(manager); Dependencies.Cache(dialogOverlay); - - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); - - beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); } [SetUp] diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 0f03368296..5d0fb248df 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -35,6 +35,11 @@ namespace osu.Game.Tests.Visual.SongSelect [BackgroundDependencyLoader] private void load(GameHost host) { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); + + beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + base.Content.AddRange(new Drawable[] { collectionManager = new CollectionManager(LocalStorage), @@ -42,10 +47,6 @@ namespace osu.Game.Tests.Visual.SongSelect }); Dependencies.Cache(collectionManager); - Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); - Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); - - beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); } [SetUp] From 9e73237a900e8b9cb3bdf9689a140f4ee11ce2ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 15:21:03 +0900 Subject: [PATCH 3253/6909] Fix score present tests potentially succeeding a step when they shouldn't --- .../Visual/Navigation/TestScenePresentScore.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index a899d072ac..74037dd3ec 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -133,6 +133,12 @@ namespace osu.Game.Tests.Visual.Navigation return () => imported; } + /// + /// Some tests test waiting for a particular screen twice in a row, but expect a new instance each time. + /// There's a case where they may succeed incorrectly if we don't compare against the previous instance. + /// + private IScreen lastWaitedScreen; + private void presentAndConfirm(Func getImport, ScorePresentType type) { AddStep("present score", () => Game.PresentScore(getImport(), type)); @@ -140,13 +146,15 @@ namespace osu.Game.Tests.Visual.Navigation switch (type) { case ScorePresentType.Results: - AddUntilStep("wait for results", () => Game.ScreenStack.CurrentScreen is ResultsScreen); + AddUntilStep("wait for results", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ResultsScreen); + AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen); 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("wait for player loader", () => lastWaitedScreen != Game.ScreenStack.CurrentScreen && Game.ScreenStack.CurrentScreen is ReplayPlayerLoader); + AddStep("store last waited screen", () => lastWaitedScreen = Game.ScreenStack.CurrentScreen); AddUntilStep("correct score displayed", () => ((ReplayPlayerLoader)Game.ScreenStack.CurrentScreen).Score.ID == getImport().ID); AddAssert("correct ruleset selected", () => Game.Ruleset.Value.ID == getImport().Ruleset.ID); break; From 9c041dbac2fba1bafe1f9290b091048e8c6438f3 Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Tue, 15 Sep 2020 08:24:01 +0200 Subject: [PATCH 3254/6909] Fix mania scrollspeed slider precision --- .../Configuration/ManiaRulesetConfigManager.cs | 2 +- osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs index 7e84f17809..756f2b7b2f 100644 --- a/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Mania/Configuration/ManiaRulesetConfigManager.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mania.Configuration { base.InitialiseDefaults(); - Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 1); + Set(ManiaRulesetSetting.ScrollTime, 1500.0, DrawableManiaRuleset.MIN_TIME_RANGE, DrawableManiaRuleset.MAX_TIME_RANGE, 5); Set(ManiaRulesetSetting.ScrollDirection, ManiaScrollingDirection.Down); } diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index 2ebfd0cfc1..b470405df2 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -34,7 +34,8 @@ namespace osu.Game.Rulesets.Mania new SettingsSlider { LabelText = "Scroll speed", - Bindable = config.GetBindable(ManiaRulesetSetting.ScrollTime) + Bindable = config.GetBindable(ManiaRulesetSetting.ScrollTime), + KeyboardStep = 5 }, }; } From f7c9c805665152a526f9b67b48281fc51ca7e4ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 19:01:32 +0900 Subject: [PATCH 3255/6909] Force OsuGameTests to use a unique storage each run --- osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs | 2 ++ osu.Game/Tests/Visual/OsuTestScene.cs | 4 +++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index c4acf4f7da..4c18cfa61c 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Navigation protected TestOsuGame Game; + protected override bool UseFreshStoragePerRun => true; + [BackgroundDependencyLoader] private void load(GameHost host) { diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 611bc8f30f..f00cefaefd 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -118,6 +118,8 @@ namespace osu.Game.Tests.Visual base.Content.Add(content = new DrawSizePreservingFillContainer()); } + protected virtual bool UseFreshStoragePerRun => false; + public virtual void RecycleLocalStorage() { if (localStorage?.IsValueCreated == true) @@ -133,7 +135,7 @@ namespace osu.Game.Tests.Visual } localStorage = - new Lazy(() => host is HeadlessGameHost ? host.Storage : new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); + new Lazy(() => !UseFreshStoragePerRun && host is HeadlessGameHost ? host.Storage : new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); } [Resolved] From e43e12cb2dd0504917f27fb2b83efd637885366b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 20:17:59 +0900 Subject: [PATCH 3256/6909] Pause playback in present tests to avoid track inadvertently changing at menu --- .../Visual/Navigation/TestScenePresentBeatmap.cs | 7 +++++++ osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs index 27f5b29738..a003b9ae4d 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentBeatmap.cs @@ -74,6 +74,13 @@ namespace osu.Game.Tests.Visual.Navigation private void returnToMenu() { + // if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track). + AddStep("pause audio", () => + { + if (Game.MusicController.IsPlaying) + Game.MusicController.TogglePause(); + }); + AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit()); AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); } diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs index 74037dd3ec..52b577b402 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePresentScore.cs @@ -110,6 +110,13 @@ namespace osu.Game.Tests.Visual.Navigation private void returnToMenu() { + // if we don't pause, there's a chance the track may change at the main menu out of our control (due to reaching the end of the track). + AddStep("pause audio", () => + { + if (Game.MusicController.IsPlaying) + Game.MusicController.TogglePause(); + }); + AddStep("return to menu", () => Game.ScreenStack.CurrentScreen.Exit()); AddUntilStep("wait for menu", () => Game.ScreenStack.CurrentScreen is MainMenu); } From dbfaa4a0df5ae5924d1c640fa200ecea39d318f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 22:50:44 +0900 Subject: [PATCH 3257/6909] Remove beatmap paths from tests where they would result in exceptions --- osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs | 1 - osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 3 --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 1 - 3 files changed, 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index faea32f90f..55b8902d7b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -54,7 +54,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { Ruleset = new OsuRuleset().RulesetInfo, OnlineBeatmapID = beatmapId, - Path = "normal.osu", Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})", Length = length, BPM = bpm, diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index a3ea4619cc..3aff390a47 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -839,7 +839,6 @@ namespace osu.Game.Tests.Visual.SongSelect new BeatmapInfo { OnlineBeatmapID = id * 10, - Path = "normal.osu", Version = "Normal", StarDifficulty = 2, BaseDifficulty = new BeatmapDifficulty @@ -850,7 +849,6 @@ namespace osu.Game.Tests.Visual.SongSelect new BeatmapInfo { OnlineBeatmapID = id * 10 + 1, - Path = "hard.osu", Version = "Hard", StarDifficulty = 5, BaseDifficulty = new BeatmapDifficulty @@ -861,7 +859,6 @@ namespace osu.Game.Tests.Visual.SongSelect new BeatmapInfo { OnlineBeatmapID = id * 10 + 2, - Path = "insane.osu", Version = "Insane", StarDifficulty = 6, BaseDifficulty = new BeatmapDifficulty diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index f7d66ca5cf..0299b7a084 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -879,7 +879,6 @@ namespace osu.Game.Tests.Visual.SongSelect { Ruleset = getRuleset(), OnlineBeatmapID = beatmapId, - Path = "normal.osu", Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})", Length = length, BPM = bpm, From 3c70b3127c31a9098a294f09749e49d7e9ca6da9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Sep 2020 23:19:31 +0900 Subject: [PATCH 3258/6909] Fix potential nullref in FilterControl during asynchronous load --- osu.Game/Screens/Select/FilterControl.cs | 30 ++++++++++++------------ 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index c82a3742cc..952a5d1eaa 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -66,24 +66,9 @@ namespace osu.Game.Screens.Select [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[] { new Box @@ -182,6 +167,21 @@ namespace osu.Game.Screens.Select } }; + 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()); + + groupMode.BindValueChanged(_ => updateCriteria()); + sortMode.BindValueChanged(_ => updateCriteria()); + collectionDropdown.Current.ValueChanged += val => { if (val.NewValue == null) From 35c7677d0a0996d4df518aa6da5c7b464f0059a3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 16 Sep 2020 01:59:07 +0300 Subject: [PATCH 3259/6909] Fix gameplay samples potentially start playing while player is paused --- osu.Game/Skinning/SkinnableSound.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index cf629f231f..9e49134806 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -114,6 +114,8 @@ namespace osu.Game.Skinning protected override void SkinChanged(ISkinSource skin, bool allowFallback) { + bool wasPlaying = IsPlaying; + var channels = hitSamples.Select(s => { var ch = skin.GetSample(s); @@ -138,8 +140,10 @@ namespace osu.Game.Skinning samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); - if (requestedPlaying) - Play(); + // Sample channels have been reloaded to new ones because skin has changed. + // Start playback internally for them if they were playing previously. + if (wasPlaying) + play(); } #region Re-expose AudioContainer From 105634c09936295a4083fef5b3f7c7d56f10a56a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 16 Sep 2020 01:59:41 +0300 Subject: [PATCH 3260/6909] Add test case ensuring correct behaviour --- .../Gameplay/TestSceneSkinnableSound.cs | 61 ++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index e0a1f947ec..5f39a57d8a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -1,12 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; 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.Audio; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Audio; @@ -20,6 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached] private GameplayClock gameplayClock = new GameplayClock(new FramedClock()); + private TestSkinSourceContainer skinSource; private SkinnableSound skinnableSound; [SetUp] @@ -29,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay Children = new Drawable[] { - new Container + skinSource = new TestSkinSourceContainer { Clock = gameplayClock, RelativeSizeAxes = Axes.Both, @@ -101,5 +107,58 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sample not playing", () => !sample.Playing); AddAssert("sample not playing", () => !sample.Playing); } + + [Test] + public void TestSkinChangeDoesntPlayOnPause() + { + DrawableSample sample = null; + AddStep("start sample", () => + { + skinnableSound.Play(); + sample = skinnableSound.ChildrenOfType().Single(); + }); + + AddAssert("sample playing", () => sample.Playing); + + AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); + AddUntilStep("wait for sample to stop playing", () => !sample.Playing); + + AddStep("trigger skin change", () => + { + skinSource.TriggerSourceChanged(); + }); + + AddStep("retrieve new sample", () => + { + DrawableSample newSample = skinnableSound.ChildrenOfType().Single(); + Assert.IsTrue(newSample != sample, "Sample still hasn't been updated after a skin change event"); + sample = newSample; + }); + + AddAssert("new sample paused", () => !sample.Playing); + AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); + + AddWaitStep("wait a bit", 5); + AddAssert("new sample not played", () => !sample.Playing); + } + + [Cached(typeof(ISkinSource))] + private class TestSkinSourceContainer : Container, ISkinSource + { + [Resolved] + private ISkinSource source { get; set; } + + public event Action SourceChanged; + + public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component); + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT); + public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); + public IBindable GetConfig(TLookup lookup) => source?.GetConfig(lookup); + + public void TriggerSourceChanged() + { + SourceChanged?.Invoke(); + } + } } } From c6386ea60505e10b56e2189423fa36d4d0f6a87a Mon Sep 17 00:00:00 2001 From: Joehu Date: Tue, 15 Sep 2020 21:33:52 -0700 Subject: [PATCH 3261/6909] Remember leaderboard mods filter selection in song select --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Screens/Select/BeatmapDetailArea.cs | 2 ++ osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs | 6 ++++++ osu.Game/Screens/Select/PlayBeatmapDetailArea.cs | 7 +++++++ 4 files changed, 17 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 71820dea55..207a3f01d3 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -24,6 +24,7 @@ namespace osu.Game.Configuration Set(OsuSetting.Skin, 0, -1, int.MaxValue); Set(OsuSetting.BeatmapDetailTab, PlayBeatmapDetailArea.TabType.Details); + Set(OsuSetting.BeatmapDetailModsFilter, false); Set(OsuSetting.ShowConvertedBeatmaps, true); Set(OsuSetting.DisplayStarsMinimum, 0.0, 0, 10, 0.1); @@ -200,6 +201,7 @@ namespace osu.Game.Configuration CursorRotation, MenuParallax, BeatmapDetailTab, + BeatmapDetailModsFilter, Username, ReleaseStream, SavePassword, diff --git a/osu.Game/Screens/Select/BeatmapDetailArea.cs b/osu.Game/Screens/Select/BeatmapDetailArea.cs index 2e78b1aed2..89ae92ec91 100644 --- a/osu.Game/Screens/Select/BeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/BeatmapDetailArea.cs @@ -30,6 +30,8 @@ namespace osu.Game.Screens.Select protected Bindable CurrentTab => tabControl.Current; + protected Bindable CurrentModsFilter => tabControl.CurrentModsFilter; + private readonly Container content; protected override Container Content => content; diff --git a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs index 63711e3e50..df8c68a0dd 100644 --- a/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs +++ b/osu.Game/Screens/Select/BeatmapDetailAreaTabControl.cs @@ -26,6 +26,12 @@ namespace osu.Game.Screens.Select set => tabs.Current = value; } + public Bindable CurrentModsFilter + { + get => modsCheckbox.Current; + set => modsCheckbox.Current = value; + } + public Action OnFilter; // passed the selected tab and if mods is checked public IReadOnlyList TabItems diff --git a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs index d719502a4f..c87a4bbc54 100644 --- a/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs +++ b/osu.Game/Screens/Select/PlayBeatmapDetailArea.cs @@ -29,6 +29,8 @@ namespace osu.Game.Screens.Select private Bindable selectedTab; + private Bindable selectedModsFilter; + public PlayBeatmapDetailArea() { Add(Leaderboard = new BeatmapLeaderboard { RelativeSizeAxes = Axes.Both }); @@ -38,8 +40,13 @@ namespace osu.Game.Screens.Select private void load(OsuConfigManager config) { selectedTab = config.GetBindable(OsuSetting.BeatmapDetailTab); + selectedModsFilter = config.GetBindable(OsuSetting.BeatmapDetailModsFilter); + selectedTab.BindValueChanged(tab => CurrentTab.Value = getTabItemFromTabType(tab.NewValue), true); CurrentTab.BindValueChanged(tab => selectedTab.Value = getTabTypeFromTabItem(tab.NewValue)); + + selectedModsFilter.BindValueChanged(checkbox => CurrentModsFilter.Value = checkbox.NewValue, true); + CurrentModsFilter.BindValueChanged(checkbox => selectedModsFilter.Value = checkbox.NewValue); } public override void Refresh() From ff5b29230261ac3ef26b1e1e375652b29357ddfe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Sep 2020 19:36:36 +0900 Subject: [PATCH 3262/6909] Fix global bindings being lost when running tests under headless contexts --- osu.Game/Tests/Visual/OsuTestScene.cs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index f00cefaefd..b59a1db403 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -69,12 +69,26 @@ namespace osu.Game.Tests.Visual /// protected virtual bool UseOnlineAPI => false; + /// + /// When running headless, there is an opportunity to use the host storage rather than creating a second isolated one. + /// This is because the host is recycled per TestScene execution in headless at an nunit level. + /// + private Storage isolatedHostStorage; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { + if (!UseFreshStoragePerRun) + isolatedHostStorage = (parent.Get() as HeadlessGameHost)?.Storage; + contextFactory = new Lazy(() => { var factory = new DatabaseContextFactory(LocalStorage); - factory.ResetDatabase(); + + // only reset the database if not using the host storage. + // if we reset the host storage, it will delete global key bindings. + if (isolatedHostStorage == null) + factory.ResetDatabase(); + using (var usage = factory.Get()) usage.Migrate(); return factory; @@ -135,12 +149,9 @@ namespace osu.Game.Tests.Visual } localStorage = - new Lazy(() => !UseFreshStoragePerRun && host is HeadlessGameHost ? host.Storage : new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); + new Lazy(() => isolatedHostStorage ?? new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); } - [Resolved] - private GameHost host { get; set; } - [Resolved] protected AudioManager Audio { get; private set; } From 9063c60b9cb79ee5fb6db07f736a8510d8371861 Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 16 Sep 2020 12:00:27 -0700 Subject: [PATCH 3263/6909] Fix profile section tab control not absorbing input from behind --- osu.Game/Overlays/UserProfileOverlay.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 2b316c0e34..d52ad84592 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -8,6 +8,7 @@ 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.Containers; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Profile; @@ -176,6 +177,10 @@ namespace osu.Game.Overlays AccentColour = colourProvider.Highlight1; } + protected override bool OnClick(ClickEvent e) => true; + + protected override bool OnHover(HoverEvent e) => true; + private class ProfileSectionTabItem : OverlayTabItem { public ProfileSectionTabItem(ProfileSection value) From 3529a1bfeacf3c765731871d54db2b7f01911fb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Sep 2020 19:36:36 +0900 Subject: [PATCH 3264/6909] Fix global bindings being lost when running tests under headless contexts --- osu.Game/Tests/Visual/OsuTestScene.cs | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index f00cefaefd..b59a1db403 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -69,12 +69,26 @@ namespace osu.Game.Tests.Visual /// protected virtual bool UseOnlineAPI => false; + /// + /// When running headless, there is an opportunity to use the host storage rather than creating a second isolated one. + /// This is because the host is recycled per TestScene execution in headless at an nunit level. + /// + private Storage isolatedHostStorage; + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { + if (!UseFreshStoragePerRun) + isolatedHostStorage = (parent.Get() as HeadlessGameHost)?.Storage; + contextFactory = new Lazy(() => { var factory = new DatabaseContextFactory(LocalStorage); - factory.ResetDatabase(); + + // only reset the database if not using the host storage. + // if we reset the host storage, it will delete global key bindings. + if (isolatedHostStorage == null) + factory.ResetDatabase(); + using (var usage = factory.Get()) usage.Migrate(); return factory; @@ -135,12 +149,9 @@ namespace osu.Game.Tests.Visual } localStorage = - new Lazy(() => !UseFreshStoragePerRun && host is HeadlessGameHost ? host.Storage : new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); + new Lazy(() => isolatedHostStorage ?? new NativeStorage(Path.Combine(RuntimeInfo.StartupDirectory, $"{GetType().Name}-{Guid.NewGuid()}"))); } - [Resolved] - private GameHost host { get; set; } - [Resolved] protected AudioManager Audio { get; private set; } From d2580ebc7023b9730dbf4fe4e047dcd597946803 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Sep 2020 13:01:34 +0900 Subject: [PATCH 3265/6909] Attempt to fix tests by avoiding clash between import tests names --- osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index 14eff4c5e3..ef5ff0e75d 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Skins.IO [Test] public async Task TestBasicImport() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest))) { try { @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Skins.IO [Test] public async Task TestImportTwiceWithSameMetadata() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest))) { try { @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Skins.IO [Test] public async Task TestImportTwiceWithNoMetadata() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest))) { try { @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Skins.IO [Test] public async Task TestImportTwiceWithDifferentMetadata() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportSkinTest))) { try { From 0b289d2e779140930d8fd90c3de13bd3b0dcef8d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Sep 2020 13:07:05 +0900 Subject: [PATCH 3266/6909] Add hostname differentiation to beatmap tests too --- .../Beatmaps/IO/ImportBeatmapTest.cs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index dd3dba1274..bc6fbed07a 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportWhenClosed() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenDelete() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportWithReZip() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -156,7 +156,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportWithChangedFile() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -207,7 +207,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportThenImportWithDifferentFilename() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -259,7 +259,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportCorruptThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -301,7 +301,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestRollbackOnFailure() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -378,7 +378,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenDeleteThenImport() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -406,7 +406,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportThenDeleteThenImportWithOnlineIDMismatch(bool set) { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(set.ToString())) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-{set}")) { try { @@ -440,7 +440,7 @@ namespace osu.Game.Tests.Beatmaps.IO public async Task TestImportWithDuplicateBeatmapIDs() { // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -496,8 +496,8 @@ namespace osu.Game.Tests.Beatmaps.IO [Ignore("Binding IPC on Appveyor isn't working (port in use). Need to figure out why")] public void TestImportOverIPC() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost("host", true)) - using (HeadlessGameHost client = new CleanRunHeadlessGameHost("client", true)) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-host", true)) + using (HeadlessGameHost client = new CleanRunHeadlessGameHost($"{nameof(ImportBeatmapTest)}-client", true)) { try { @@ -526,7 +526,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWhenFileOpen() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -548,7 +548,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWithDuplicateHashes() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -590,7 +590,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportNestedStructure() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -635,7 +635,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestImportWithIgnoredDirectoryInArchive() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -689,7 +689,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestUpdateBeatmapInfo() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -719,7 +719,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public async Task TestUpdateBeatmapFile() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -761,7 +761,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public void TestCreateNewEmptyBeatmap() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { @@ -788,7 +788,7 @@ namespace osu.Game.Tests.Beatmaps.IO [Test] public void TestCreateNewBeatmapWithObject() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) { try { From 835c8d74b77298faef44e1b37a45596e9ff93cd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Sep 2020 16:12:18 +0900 Subject: [PATCH 3267/6909] Wait for two update frames before attempting to migrate storage --- osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 199e69a19d..17e6b712f3 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -272,8 +272,16 @@ 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"); + bool ready = false; + // wait for two update frames to be executed. this ensures that all components have had a change to run LoadComplete and hopefully avoid + // database access (GlobalActionContainer is one to do this). + host.UpdateThread.Scheduler.Add(() => host.UpdateThread.Scheduler.Add(() => ready = true)); + + waitForOrAssert(() => ready, @"osu! failed to start in a reasonable amount of time"); + return osu; } From 89a2f20922fea81dc585b9681d43a2f995157c17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Sep 2020 16:12:30 +0900 Subject: [PATCH 3268/6909] Use new CleanRun host class in import tests --- .../NonVisual/CustomDataDirectoryTest.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 17e6b712f3..211fa4ca42 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -4,6 +4,7 @@ using System; using System.IO; using System.Linq; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -22,7 +23,7 @@ namespace osu.Game.Tests.NonVisual [Test] public void TestDefaultDirectory() { - using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestDefaultDirectory))) + using (var host = new CustomTestHeadlessGameHost()) { try { @@ -45,7 +46,7 @@ namespace osu.Game.Tests.NonVisual { string customPath = prepareCustomPath(); - using (var host = new CustomTestHeadlessGameHost(nameof(TestCustomDirectory))) + using (var host = new CustomTestHeadlessGameHost()) { using (var storageConfig = new StorageConfigManager(host.InitialStorage)) storageConfig.Set(StorageConfig.FullPath, customPath); @@ -71,7 +72,7 @@ namespace osu.Game.Tests.NonVisual { string customPath = prepareCustomPath(); - using (var host = new CustomTestHeadlessGameHost(nameof(TestSubDirectoryLookup))) + using (var host = new CustomTestHeadlessGameHost()) { using (var storageConfig = new StorageConfigManager(host.InitialStorage)) storageConfig.Set(StorageConfig.FullPath, customPath); @@ -104,7 +105,7 @@ namespace osu.Game.Tests.NonVisual { string customPath = prepareCustomPath(); - using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigration))) + using (var host = new CustomTestHeadlessGameHost()) { try { @@ -165,7 +166,7 @@ namespace osu.Game.Tests.NonVisual string customPath = prepareCustomPath(); string customPath2 = prepareCustomPath("-2"); - using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationBetweenTwoTargets))) + using (var host = new CustomTestHeadlessGameHost()) { try { @@ -194,7 +195,7 @@ namespace osu.Game.Tests.NonVisual { string customPath = prepareCustomPath(); - using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSameTargetFails))) + using (var host = new CustomTestHeadlessGameHost()) { try { @@ -215,7 +216,7 @@ namespace osu.Game.Tests.NonVisual { string customPath = prepareCustomPath(); - using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToNestedTargetFails))) + using (var host = new CustomTestHeadlessGameHost()) { try { @@ -244,7 +245,7 @@ namespace osu.Game.Tests.NonVisual { string customPath = prepareCustomPath(); - using (HeadlessGameHost host = new CustomTestHeadlessGameHost(nameof(TestMigrationToSeeminglyNestedTarget))) + using (var host = new CustomTestHeadlessGameHost()) { try { @@ -315,14 +316,14 @@ namespace osu.Game.Tests.NonVisual return path; } - public class CustomTestHeadlessGameHost : HeadlessGameHost + public class CustomTestHeadlessGameHost : CleanRunHeadlessGameHost { public Storage InitialStorage { get; } - public CustomTestHeadlessGameHost(string name) - : base(name) + public CustomTestHeadlessGameHost([CallerMemberName] string callingMethodName = @"") + : base(callingMethodName: callingMethodName) { - string defaultStorageLocation = getDefaultLocationFor(name); + string defaultStorageLocation = getDefaultLocationFor(callingMethodName); InitialStorage = new DesktopStorage(defaultStorageLocation, this); InitialStorage.DeleteDirectory(string.Empty); From 81f0a06fc41a5d3332303133ac885c4595717d5a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 17 Sep 2020 16:30:34 +0900 Subject: [PATCH 3269/6909] Fix potential endless taiko beatmap conversion --- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 2a1aa5d1df..e6f6b9faac 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Taiko.Objects; using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; @@ -87,6 +88,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { List> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples }); + if (Precision.AlmostEquals(0, tickSpacing)) + yield break; + int i = 0; for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing) From 73a7b759cb048146ff1afb27e63367c5eb95eb27 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 17 Sep 2020 17:04:44 +0900 Subject: [PATCH 3270/6909] Add missing obsoletion notice --- osu.Game/Rulesets/Objects/HitObject.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 1d60b266e3..0dfde834ee 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -145,6 +145,7 @@ namespace osu.Game.Rulesets.Objects #pragma warning restore 618 } + [Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318 protected virtual void CreateNestedHitObjects() { } From 009e1b44450309991b4e660fef58b41a4df4ad58 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 17 Sep 2020 17:05:24 +0900 Subject: [PATCH 3271/6909] Make Spinner use cancellation token --- osu.Game.Rulesets.Osu/Objects/Spinner.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Spinner.cs b/osu.Game.Rulesets.Osu/Objects/Spinner.cs index 1658a4e7c2..194aa640f9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Spinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Spinner.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.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; @@ -48,14 +49,16 @@ namespace osu.Game.Rulesets.Osu.Objects MaximumBonusSpins = (int)((maximum_rotations_per_second - minimumRotationsPerSecond) * secondsDuration); } - protected override void CreateNestedHitObjects() + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) { - base.CreateNestedHitObjects(); + base.CreateNestedHitObjects(cancellationToken); int totalSpins = MaximumBonusSpins + SpinsRequired; for (int i = 0; i < totalSpins; i++) { + cancellationToken.ThrowIfCancellationRequested(); + double startTime = StartTime + (float)(i + 1) / totalSpins * Duration; AddNested(i < SpinsRequired From c7d24203ceb00022fe10d74be6e81d9434e89d6c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 17 Sep 2020 17:40:05 +0900 Subject: [PATCH 3272/6909] Make beatmap conversion support cancellation tokens --- .../Beatmaps/CatchBeatmapConverter.cs | 3 ++- .../Beatmaps/ManiaBeatmapConverter.cs | 7 ++++--- .../Beatmaps/OsuBeatmapConverter.cs | 3 ++- .../Beatmaps/TaikoBeatmapConverter.cs | 7 ++++--- .../TestSceneDrawableScrollingRuleset.cs | 3 ++- osu.Game/Beatmaps/BeatmapConverter.cs | 21 +++++++++---------- osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 3 ++- osu.Game/Beatmaps/IBeatmapConverter.cs | 5 ++++- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- 9 files changed, 31 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs index 145a40f5f5..34964fc4ae 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -5,6 +5,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using System.Collections.Generic; using System.Linq; +using System.Threading; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Objects; using osu.Framework.Extensions.IEnumerableExtensions; @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); - protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken) { var positionData = obj as IHasXPosition; var comboData = obj as IHasCombo; diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 211905835c..524ea27efa 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -5,6 +5,7 @@ using osu.Game.Rulesets.Mania.Objects; using System; using System.Linq; using System.Collections.Generic; +using System.Threading; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; @@ -68,14 +69,14 @@ namespace osu.Game.Rulesets.Mania.Beatmaps public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); - protected override Beatmap ConvertBeatmap(IBeatmap original) + protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { BeatmapDifficulty difficulty = original.BeatmapInfo.BaseDifficulty; int seed = (int)MathF.Round(difficulty.DrainRate + difficulty.CircleSize) * 20 + (int)(difficulty.OverallDifficulty * 41.2) + (int)MathF.Round(difficulty.ApproachRate); Random = new FastRandom(seed); - return base.ConvertBeatmap(original); + return base.ConvertBeatmap(original, cancellationToken); } protected override Beatmap CreateBeatmap() @@ -88,7 +89,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps return beatmap; } - protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) { if (original is ManiaHitObject maniaOriginal) { diff --git a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs index fcad356a1c..a2fc4848af 100644 --- a/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs +++ b/osu.Game.Rulesets.Osu/Beatmaps/OsuBeatmapConverter.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Osu.Objects; using System.Collections.Generic; using osu.Game.Rulesets.Objects.Types; using System.Linq; +using System.Threading; using osu.Game.Rulesets.Osu.UI; using osu.Framework.Extensions.IEnumerableExtensions; @@ -22,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Beatmaps public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasPosition); - protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) { var positionData = original as IHasPosition; var comboData = original as IHasCombo; diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 2a1aa5d1df..91e31aeced 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -8,6 +8,7 @@ using osu.Game.Rulesets.Taiko.Objects; using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using osu.Game.Audio; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Formats; @@ -48,14 +49,14 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps public override bool CanConvert() => true; - protected override Beatmap ConvertBeatmap(IBeatmap original) + protected override Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { // 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; - Beatmap converted = base.ConvertBeatmap(original); + Beatmap converted = base.ConvertBeatmap(original, cancellationToken); if (original.BeatmapInfo.RulesetID == 3) { @@ -72,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps return converted; } - protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject obj, IBeatmap beatmap, CancellationToken cancellationToken) { // Old osu! used hit sounding to determine various hit type information IList samples = obj.Samples; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index bd7e894cf8..1a1babb4a8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -276,7 +277,7 @@ namespace osu.Game.Tests.Visual.Gameplay public override bool CanConvert() => true; - protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) { yield return new TestHitObject { diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 11fee030f8..3083cee07e 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -36,34 +37,31 @@ namespace osu.Game.Beatmaps /// public abstract bool CanConvert(); - /// - /// Converts . - /// - /// The converted Beatmap. - public IBeatmap Convert() + public IBeatmap Convert(CancellationToken cancellationToken = default) { // We always operate on a clone of the original beatmap, to not modify it game-wide - return ConvertBeatmap(Beatmap.Clone()); + return ConvertBeatmap(Beatmap.Clone(), cancellationToken); } /// /// Performs the conversion of a Beatmap using this Beatmap Converter. /// /// The un-converted Beatmap. + /// The cancellation token. /// The converted Beatmap. - protected virtual Beatmap ConvertBeatmap(IBeatmap original) + protected virtual Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { var beatmap = CreateBeatmap(); beatmap.BeatmapInfo = original.BeatmapInfo; beatmap.ControlPointInfo = original.ControlPointInfo; - beatmap.HitObjects = convertHitObjects(original.HitObjects, original).OrderBy(s => s.StartTime).ToList(); + beatmap.HitObjects = convertHitObjects(original.HitObjects, original, cancellationToken).OrderBy(s => s.StartTime).ToList(); beatmap.Breaks = original.Breaks; return beatmap; } - private List convertHitObjects(IReadOnlyList hitObjects, IBeatmap beatmap) + private List convertHitObjects(IReadOnlyList hitObjects, IBeatmap beatmap, CancellationToken cancellationToken) { var result = new List(hitObjects.Count); @@ -75,7 +73,7 @@ namespace osu.Game.Beatmaps continue; } - var converted = ConvertHitObject(obj, beatmap); + var converted = ConvertHitObject(obj, beatmap, cancellationToken); if (ObjectConverted != null) { @@ -104,7 +102,8 @@ namespace osu.Game.Beatmaps /// /// The hit object to convert. /// The un-converted Beatmap. + /// The cancellation token. /// The converted hit object. - protected abstract IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap); + protected abstract IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken); } } diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index af2a2ac250..fdc839ccff 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Threading; using JetBrains.Annotations; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -76,7 +77,7 @@ namespace osu.Game.Beatmaps public bool CanConvert() => true; - public IBeatmap Convert() + public IBeatmap Convert(CancellationToken cancellationToken) { foreach (var obj in Beatmap.HitObjects) ObjectConverted?.Invoke(obj, obj.Yield()); diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs index 173d5494ba..83d0ada1b9 100644 --- a/osu.Game/Beatmaps/IBeatmapConverter.cs +++ b/osu.Game/Beatmaps/IBeatmapConverter.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Threading; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; @@ -30,6 +31,8 @@ namespace osu.Game.Beatmaps /// /// Converts . /// - IBeatmap Convert(); + /// The cancellation token. + /// The converted Beatmap. + IBeatmap Convert(CancellationToken cancellationToken); } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index d9780233d1..30382c444f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -109,7 +109,7 @@ namespace osu.Game.Beatmaps } // Convert - IBeatmap converted = converter.Convert(); + IBeatmap converted = converter.Convert(cancellationSource.Token); // Apply conversion mods to the result foreach (var mod in mods.OfType()) From e71991a53c068cb86bdc77396d7a6ec40640a9fe Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 17 Sep 2020 18:37:48 +0900 Subject: [PATCH 3273/6909] Add default token --- osu.Game/Beatmaps/DummyWorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/IBeatmapConverter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs index fdc839ccff..c114358771 100644 --- a/osu.Game/Beatmaps/DummyWorkingBeatmap.cs +++ b/osu.Game/Beatmaps/DummyWorkingBeatmap.cs @@ -77,7 +77,7 @@ namespace osu.Game.Beatmaps public bool CanConvert() => true; - public IBeatmap Convert(CancellationToken cancellationToken) + public IBeatmap Convert(CancellationToken cancellationToken = default) { foreach (var obj in Beatmap.HitObjects) ObjectConverted?.Invoke(obj, obj.Yield()); diff --git a/osu.Game/Beatmaps/IBeatmapConverter.cs b/osu.Game/Beatmaps/IBeatmapConverter.cs index 83d0ada1b9..2833af8ca2 100644 --- a/osu.Game/Beatmaps/IBeatmapConverter.cs +++ b/osu.Game/Beatmaps/IBeatmapConverter.cs @@ -33,6 +33,6 @@ namespace osu.Game.Beatmaps /// /// The cancellation token. /// The converted Beatmap. - IBeatmap Convert(CancellationToken cancellationToken); + IBeatmap Convert(CancellationToken cancellationToken = default); } } From de5ef8a4715dc1c138c6cd3aba93f02765bdae95 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 17 Sep 2020 21:36:55 +0900 Subject: [PATCH 3274/6909] Rework to support obsoletion --- osu.Game/Beatmaps/BeatmapConverter.cs | 34 ++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index 3083cee07e..cb0b3a8d09 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -27,6 +27,8 @@ namespace osu.Game.Beatmaps public IBeatmap Beatmap { get; } + private CancellationToken cancellationToken; + protected BeatmapConverter(IBeatmap beatmap, Ruleset ruleset) { Beatmap = beatmap; @@ -39,6 +41,8 @@ namespace osu.Game.Beatmaps public IBeatmap Convert(CancellationToken cancellationToken = default) { + this.cancellationToken = cancellationToken; + // We always operate on a clone of the original beatmap, to not modify it game-wide return ConvertBeatmap(Beatmap.Clone(), cancellationToken); } @@ -51,6 +55,19 @@ namespace osu.Game.Beatmaps /// The converted Beatmap. protected virtual Beatmap ConvertBeatmap(IBeatmap original, CancellationToken cancellationToken) { +#pragma warning disable 618 + return ConvertBeatmap(original); +#pragma warning restore 618 + } + + /// + /// Performs the conversion of a Beatmap using this Beatmap Converter. + /// + /// The un-converted Beatmap. + /// The converted Beatmap. + [Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318 + protected virtual Beatmap ConvertBeatmap(IBeatmap original) + { var beatmap = CreateBeatmap(); beatmap.BeatmapInfo = original.BeatmapInfo; @@ -104,6 +121,21 @@ namespace osu.Game.Beatmaps /// The un-converted Beatmap. /// The cancellation token. /// The converted hit object. - protected abstract IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken); + protected virtual IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) + { +#pragma warning disable 618 + return ConvertHitObject(original, beatmap); +#pragma warning restore 618 + } + + /// + /// Performs the conversion of a hit object. + /// This method is generally executed sequentially for all objects in a beatmap. + /// + /// The hit object to convert. + /// The un-converted Beatmap. + /// The converted hit object. + [Obsolete("Use the cancellation-supporting override")] // Can be removed 20210318 + protected virtual IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap) => Enumerable.Empty(); } } From 83d23c954712e401758a2591ceff241c2384ae14 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 17 Sep 2020 14:56:08 -0700 Subject: [PATCH 3275/6909] Use new icon in chat overlay --- osu.Game/Overlays/ChatOverlay.cs | 11 ++++++----- osu.Game/Overlays/OverlayTitle.cs | 4 +++- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 25a59e9b25..c53eccf78b 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -23,6 +23,7 @@ using osu.Game.Overlays.Chat.Selection; using osu.Game.Overlays.Chat.Tabs; using osuTK.Input; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; namespace osu.Game.Overlays { @@ -78,7 +79,7 @@ namespace osu.Game.Overlays } [BackgroundDependencyLoader] - private void load(OsuConfigManager config, OsuColour colours) + private void load(OsuConfigManager config, OsuColour colours, TextureStore textures) { const float padding = 5; @@ -163,13 +164,13 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Colour = Color4.Black, }, - new SpriteIcon + new Sprite { - Icon = FontAwesome.Solid.Comments, + Texture = textures.Get(IconTexture), Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Size = new Vector2(20), - Margin = new MarginPadding(10), + Size = new Vector2(OverlayTitle.ICON_SIZE), + Margin = new MarginPadding { Left = 10 }, }, ChannelTabControl = CreateChannelTabControl().With(d => { diff --git a/osu.Game/Overlays/OverlayTitle.cs b/osu.Game/Overlays/OverlayTitle.cs index 17eeece1f8..c3ea35adfc 100644 --- a/osu.Game/Overlays/OverlayTitle.cs +++ b/osu.Game/Overlays/OverlayTitle.cs @@ -14,6 +14,8 @@ namespace osu.Game.Overlays { public abstract class OverlayTitle : CompositeDrawable, INamedOverlayComponent { + public const float ICON_SIZE = 30; + private readonly OsuSpriteText titleText; private readonly Container icon; @@ -51,7 +53,7 @@ namespace osu.Game.Overlays 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) + Size = new Vector2(ICON_SIZE) }, titleText = new OsuSpriteText { From 2ad7e6ca880ec24bc87d0ee3e8bfa6cba8a478b4 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 17 Sep 2020 19:10:58 -0700 Subject: [PATCH 3276/6909] Fix hovered channel tabs color when unselected --- osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs index 09dc06b95f..cca4dc33e5 100644 --- a/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/ChannelTabItem.cs @@ -211,7 +211,7 @@ namespace osu.Game.Overlays.Chat.Tabs TweenEdgeEffectTo(deactivateEdgeEffect, TRANSITION_LENGTH); - box.FadeColour(BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint); + box.FadeColour(IsHovered ? backgroundHover : BackgroundInactive, TRANSITION_LENGTH, Easing.OutQuint); highlightBox.FadeOut(TRANSITION_LENGTH, Easing.OutQuint); Text.Font = Text.Font.With(weight: FontWeight.Medium); From c62e4ef5e5b81954db33625748179186947bf53a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Sep 2020 13:06:41 +0900 Subject: [PATCH 3277/6909] Allow one hitobject in taiko beatmap converter edge case --- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 9e82161a61..ed7b8589ba 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -89,9 +89,6 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps { List> allSamples = obj is IHasPathWithRepeats curveData ? curveData.NodeSamples : new List>(new[] { samples }); - if (Precision.AlmostEquals(0, tickSpacing)) - yield break; - int i = 0; for (double j = obj.StartTime; j <= obj.StartTime + taikoDuration + tickSpacing / 8; j += tickSpacing) @@ -109,6 +106,9 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps }; i = (i + 1) % allSamples.Count; + + if (Precision.AlmostEquals(0, tickSpacing)) + break; } } else From 393ee1c9f5e4719efc9eb35ee97752cd7ae6d4ba Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 17 Sep 2020 23:09:09 -0700 Subject: [PATCH 3278/6909] Fix hovered osu tab items not showing hover state when deselected --- osu.Game/Graphics/UserInterface/OsuTabControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuTabControl.cs b/osu.Game/Graphics/UserInterface/OsuTabControl.cs index 61501b0cd8..dbcce9a84a 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControl.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControl.cs @@ -123,8 +123,8 @@ namespace osu.Game.Graphics.UserInterface protected void FadeUnhovered() { - Bar.FadeOut(transition_length, Easing.OutQuint); - Text.FadeColour(AccentColour, transition_length, Easing.OutQuint); + Bar.FadeTo(IsHovered ? 1 : 0, transition_length, Easing.OutQuint); + Text.FadeColour(IsHovered ? Color4.White : AccentColour, transition_length, Easing.OutQuint); } protected override bool OnHover(HoverEvent e) From 3cef93ee27dac06ea85c761bc4c24f19e17637ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Sep 2020 18:05:33 +0900 Subject: [PATCH 3279/6909] Centralise import test helper methods --- .../Beatmaps/IO/ImportBeatmapTest.cs | 50 ++++++-------- .../Collections/IO/ImportCollectionsTest.cs | 62 ++--------------- osu.Game.Tests/ImportTest.cs | 66 +++++++++++++++++++ .../NonVisual/CustomDataDirectoryTest.cs | 47 +++---------- osu.Game.Tests/Scores/IO/ImportScoreTest.cs | 55 ++++------------ osu.Game.Tests/Skins/IO/ImportSkinTest.cs | 56 ++++------------ 6 files changed, 129 insertions(+), 207 deletions(-) create mode 100644 osu.Game.Tests/ImportTest.cs diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index bc6fbed07a..80fbda8e1d 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -28,7 +28,7 @@ using FileInfo = System.IO.FileInfo; namespace osu.Game.Tests.Beatmaps.IO { [TestFixture] - public class ImportBeatmapTest + public class ImportBeatmapTest : ImportTest { [Test] public async Task TestImportWhenClosed() @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - await LoadOszIntoOsu(loadOsu(host)); + await LoadOszIntoOsu(LoadOsuIntoHost(host)); } finally { @@ -55,7 +55,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var imported = await LoadOszIntoOsu(osu); @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var imported = await LoadOszIntoOsu(osu); var importedSecondTime = await LoadOszIntoOsu(osu); @@ -102,7 +102,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var temp = TestResources.GetTestBeatmapForImport(); @@ -160,7 +160,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var temp = TestResources.GetTestBeatmapForImport(); @@ -211,7 +211,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var temp = TestResources.GetTestBeatmapForImport(); @@ -263,7 +263,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var imported = await LoadOszIntoOsu(osu); @@ -314,7 +314,7 @@ namespace osu.Game.Tests.Beatmaps.IO Interlocked.Increment(ref loggedExceptionCount); }; - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var manager = osu.Dependencies.Get(); // ReSharper disable once AccessToModifiedClosure @@ -382,7 +382,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var imported = await LoadOszIntoOsu(osu); @@ -410,7 +410,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var imported = await LoadOszIntoOsu(osu); @@ -444,7 +444,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var metadata = new BeatmapMetadata { @@ -504,7 +504,7 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.IsTrue(host.IsPrimaryInstance); Assert.IsFalse(client.IsPrimaryInstance); - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var temp = TestResources.GetTestBeatmapForImport(); @@ -530,7 +530,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var temp = TestResources.GetTestBeatmapForImport(); using (File.OpenRead(temp)) await osu.Dependencies.Get().Import(temp); @@ -552,7 +552,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var temp = TestResources.GetTestBeatmapForImport(); @@ -594,7 +594,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var temp = TestResources.GetTestBeatmapForImport(); @@ -639,7 +639,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var temp = TestResources.GetTestBeatmapForImport(); @@ -693,7 +693,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var manager = osu.Dependencies.Get(); var temp = TestResources.GetTestBeatmapForImport(); @@ -723,7 +723,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var manager = osu.Dependencies.Get(); var temp = TestResources.GetTestBeatmapForImport(); @@ -765,7 +765,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var manager = osu.Dependencies.Get(); var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER); @@ -792,7 +792,7 @@ namespace osu.Game.Tests.Beatmaps.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var manager = osu.Dependencies.Get(); var working = manager.CreateNew(new OsuRuleset().RulesetInfo, User.SYSTEM_USER); @@ -863,14 +863,6 @@ namespace osu.Game.Tests.Beatmaps.IO Assert.AreEqual(expected, osu.Dependencies.Get().QueryFiles(f => f.ReferenceCount == 1).Count()); } - 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 ensureLoaded(OsuGameBase osu, int timeout = 60000) { IEnumerable resultSets = null; diff --git a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs index a79e0d0338..a8ee1bcc2e 100644 --- a/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs +++ b/osu.Game.Tests/Collections/IO/ImportCollectionsTest.cs @@ -4,18 +4,15 @@ using System; using System.IO; using System.Text; -using System.Threading; using System.Threading.Tasks; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Platform; -using osu.Game.Collections; using osu.Game.Tests.Resources; namespace osu.Game.Tests.Collections.IO { [TestFixture] - public class ImportCollectionsTest + public class ImportCollectionsTest : ImportTest { [Test] public async Task TestImportEmptyDatabase() @@ -24,7 +21,7 @@ namespace osu.Game.Tests.Collections.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); await osu.CollectionManager.Import(new MemoryStream()); @@ -44,7 +41,7 @@ namespace osu.Game.Tests.Collections.IO { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); @@ -70,7 +67,7 @@ namespace osu.Game.Tests.Collections.IO { try { - var osu = loadOsu(host, true); + var osu = LoadOsuIntoHost(host, true); await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); @@ -101,7 +98,7 @@ namespace osu.Game.Tests.Collections.IO { AppDomain.CurrentDomain.UnhandledException += setException; - var osu = loadOsu(host, true); + var osu = LoadOsuIntoHost(host, true); using (var ms = new MemoryStream()) { @@ -135,7 +132,7 @@ namespace osu.Game.Tests.Collections.IO { try { - var osu = loadOsu(host, true); + var osu = LoadOsuIntoHost(host, true); await osu.CollectionManager.Import(TestResources.OpenResource("Collections/collections.db")); @@ -156,7 +153,7 @@ namespace osu.Game.Tests.Collections.IO { try { - var osu = loadOsu(host, true); + var osu = LoadOsuIntoHost(host, true); Assert.That(osu.CollectionManager.Collections.Count, Is.EqualTo(2)); @@ -172,50 +169,5 @@ namespace osu.Game.Tests.Collections.IO } } } - - private TestOsuGameBase loadOsu(GameHost host, bool withBeatmap = false) - { - var osu = new TestOsuGameBase(withBeatmap); - -#pragma warning disable 4014 - Task.Run(() => host.Run(osu)); -#pragma warning restore 4014 - - waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); - - return osu; - } - - private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) - { - Task task = Task.Run(() => - { - while (!result()) Thread.Sleep(200); - }); - - Assert.IsTrue(task.Wait(timeout), failureMessage); - } - - private class TestOsuGameBase : OsuGameBase - { - public CollectionManager CollectionManager { get; private set; } - - private readonly bool withBeatmap; - - public TestOsuGameBase(bool withBeatmap) - { - this.withBeatmap = withBeatmap; - } - - [BackgroundDependencyLoader] - private void load() - { - // Beatmap must be imported before the collection manager is loaded. - if (withBeatmap) - BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); - - AddInternal(CollectionManager = new CollectionManager(Storage)); - } - } } } diff --git a/osu.Game.Tests/ImportTest.cs b/osu.Game.Tests/ImportTest.cs new file mode 100644 index 0000000000..ea351e0d45 --- /dev/null +++ b/osu.Game.Tests/ImportTest.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.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Platform; +using osu.Game.Collections; +using osu.Game.Tests.Resources; + +namespace osu.Game.Tests +{ + public abstract class ImportTest + { + protected virtual TestOsuGameBase LoadOsuIntoHost(GameHost host, bool withBeatmap = false) + { + var osu = new TestOsuGameBase(withBeatmap); + Task.Run(() => host.Run(osu)); + + waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); + + bool ready = false; + // wait for two update frames to be executed. this ensures that all components have had a change to run LoadComplete and hopefully avoid + // database access (GlobalActionContainer is one to do this). + host.UpdateThread.Scheduler.Add(() => host.UpdateThread.Scheduler.Add(() => ready = true)); + + waitForOrAssert(() => ready, @"osu! failed to start in a reasonable amount of time"); + + return osu; + } + + private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) + { + Task task = Task.Run(() => + { + while (!result()) Thread.Sleep(200); + }); + + Assert.IsTrue(task.Wait(timeout), failureMessage); + } + + public class TestOsuGameBase : OsuGameBase + { + public CollectionManager CollectionManager { get; private set; } + + private readonly bool withBeatmap; + + public TestOsuGameBase(bool withBeatmap) + { + this.withBeatmap = withBeatmap; + } + + [BackgroundDependencyLoader] + private void load() + { + // Beatmap must be imported before the collection manager is loaded. + if (withBeatmap) + BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + + AddInternal(CollectionManager = new CollectionManager(Storage)); + } + } + } +} diff --git a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs index 211fa4ca42..b6ab73eceb 100644 --- a/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs +++ b/osu.Game.Tests/NonVisual/CustomDataDirectoryTest.cs @@ -5,8 +5,6 @@ using System; using System.IO; using System.Linq; using System.Runtime.CompilerServices; -using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework; using osu.Framework.Allocation; @@ -18,7 +16,7 @@ using osu.Game.IO; namespace osu.Game.Tests.NonVisual { [TestFixture] - public class CustomDataDirectoryTest + public class CustomDataDirectoryTest : ImportTest { [Test] public void TestDefaultDirectory() @@ -29,7 +27,7 @@ namespace osu.Game.Tests.NonVisual { string defaultStorageLocation = getDefaultLocationFor(nameof(TestDefaultDirectory)); - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); var storage = osu.Dependencies.Get(); Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorageLocation)); @@ -53,7 +51,7 @@ namespace osu.Game.Tests.NonVisual try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); // switch to DI'd storage var storage = osu.Dependencies.Get(); @@ -79,7 +77,7 @@ namespace osu.Game.Tests.NonVisual try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); // switch to DI'd storage var storage = osu.Dependencies.Get(); @@ -111,7 +109,7 @@ namespace osu.Game.Tests.NonVisual { string defaultStorageLocation = getDefaultLocationFor(nameof(TestMigration)); - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(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. @@ -170,7 +168,7 @@ namespace osu.Game.Tests.NonVisual { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); const string database_filename = "client.db"; @@ -199,7 +197,7 @@ namespace osu.Game.Tests.NonVisual { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); Assert.DoesNotThrow(() => osu.Migrate(customPath)); Assert.Throws(() => osu.Migrate(customPath)); @@ -220,7 +218,7 @@ namespace osu.Game.Tests.NonVisual { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); Assert.DoesNotThrow(() => osu.Migrate(customPath)); @@ -249,7 +247,7 @@ namespace osu.Game.Tests.NonVisual { try { - var osu = loadOsu(host); + var osu = LoadOsuIntoHost(host); Assert.DoesNotThrow(() => osu.Migrate(customPath)); @@ -269,33 +267,6 @@ namespace osu.Game.Tests.NonVisual } } - 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"); - - bool ready = false; - // wait for two update frames to be executed. this ensures that all components have had a change to run LoadComplete and hopefully avoid - // database access (GlobalActionContainer is one to do this). - host.UpdateThread.Scheduler.Add(() => host.UpdateThread.Scheduler.Add(() => ready = true)); - - waitForOrAssert(() => ready, @"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); - } - private static string getDefaultLocationFor(string testTypeName) { string path = Path.Combine(RuntimeInfo.StartupDirectory, "headless", testTypeName); diff --git a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs index a4d20714fa..7522aca5dc 100644 --- a/osu.Game.Tests/Scores/IO/ImportScoreTest.cs +++ b/osu.Game.Tests/Scores/IO/ImportScoreTest.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; @@ -17,12 +16,11 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Tests.Resources; using osu.Game.Users; namespace osu.Game.Tests.Scores.IO { - public class ImportScoreTest + public class ImportScoreTest : ImportTest { [Test] public async Task TestBasicImport() @@ -31,7 +29,7 @@ namespace osu.Game.Tests.Scores.IO { try { - var osu = await loadOsu(host); + var osu = LoadOsuIntoHost(host, true); var toImport = new ScoreInfo { @@ -45,7 +43,7 @@ namespace osu.Game.Tests.Scores.IO OnlineScoreID = 12345, }; - var imported = await loadIntoOsu(osu, toImport); + var imported = await loadScoreIntoOsu(osu, toImport); Assert.AreEqual(toImport.Rank, imported.Rank); Assert.AreEqual(toImport.TotalScore, imported.TotalScore); @@ -70,14 +68,14 @@ namespace osu.Game.Tests.Scores.IO { try { - var osu = await loadOsu(host); + var osu = LoadOsuIntoHost(host, true); var toImport = new ScoreInfo { Mods = new Mod[] { new OsuModHardRock(), new OsuModDoubleTime() }, }; - var imported = await loadIntoOsu(osu, toImport); + var imported = await loadScoreIntoOsu(osu, toImport); Assert.IsTrue(imported.Mods.Any(m => m is OsuModHardRock)); Assert.IsTrue(imported.Mods.Any(m => m is OsuModDoubleTime)); @@ -96,7 +94,7 @@ namespace osu.Game.Tests.Scores.IO { try { - var osu = await loadOsu(host); + var osu = LoadOsuIntoHost(host, true); var toImport = new ScoreInfo { @@ -107,7 +105,7 @@ namespace osu.Game.Tests.Scores.IO } }; - var imported = await loadIntoOsu(osu, toImport); + var imported = await loadScoreIntoOsu(osu, toImport); Assert.AreEqual(toImport.Statistics[HitResult.Perfect], imported.Statistics[HitResult.Perfect]); Assert.AreEqual(toImport.Statistics[HitResult.Miss], imported.Statistics[HitResult.Miss]); @@ -126,7 +124,7 @@ namespace osu.Game.Tests.Scores.IO { try { - var osu = await loadOsu(host); + var osu = LoadOsuIntoHost(host, true); var toImport = new ScoreInfo { @@ -138,7 +136,7 @@ namespace osu.Game.Tests.Scores.IO } }; - var imported = await loadIntoOsu(osu, toImport); + var imported = await loadScoreIntoOsu(osu, toImport); var beatmapManager = osu.Dependencies.Get(); var scoreManager = osu.Dependencies.Get(); @@ -146,7 +144,7 @@ namespace osu.Game.Tests.Scores.IO beatmapManager.Delete(beatmapManager.QueryBeatmapSet(s => s.Beatmaps.Any(b => b.ID == imported.Beatmap.ID))); Assert.That(scoreManager.Query(s => s.ID == imported.ID).DeletePending, Is.EqualTo(true)); - var secondImport = await loadIntoOsu(osu, imported); + var secondImport = await loadScoreIntoOsu(osu, imported); Assert.That(secondImport, Is.Null); } finally @@ -163,9 +161,9 @@ namespace osu.Game.Tests.Scores.IO { try { - var osu = await loadOsu(host); + var osu = LoadOsuIntoHost(host, true); - await loadIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader()); + await loadScoreIntoOsu(osu, new ScoreInfo { OnlineScoreID = 2 }, new TestArchiveReader()); var scoreManager = osu.Dependencies.Get(); @@ -179,7 +177,7 @@ namespace osu.Game.Tests.Scores.IO } } - private async Task loadIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) + private async Task loadScoreIntoOsu(OsuGameBase osu, ScoreInfo score, ArchiveReader archive = null) { var beatmapManager = osu.Dependencies.Get(); @@ -192,33 +190,6 @@ namespace osu.Game.Tests.Scores.IO return scoreManager.GetAllUsableScores().FirstOrDefault(); } - private async Task loadOsu(GameHost host) - { - var osu = new OsuGameBase(); - -#pragma warning disable 4014 - Task.Run(() => host.Run(osu)); -#pragma warning restore 4014 - - waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); - - var beatmapFile = TestResources.GetTestBeatmapForImport(); - var beatmapManager = osu.Dependencies.Get(); - await beatmapManager.Import(beatmapFile); - - return osu; - } - - private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) - { - Task task = Task.Run(() => - { - while (!result()) Thread.Sleep(200); - }); - - Assert.IsTrue(task.Wait(timeout), failureMessage); - } - private class TestArchiveReader : ArchiveReader { public TestArchiveReader() diff --git a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs index ef5ff0e75d..a5b4b04ef5 100644 --- a/osu.Game.Tests/Skins/IO/ImportSkinTest.cs +++ b/osu.Game.Tests/Skins/IO/ImportSkinTest.cs @@ -4,20 +4,17 @@ using System; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Platform; -using osu.Game.Beatmaps; using osu.Game.IO.Archives; using osu.Game.Skinning; -using osu.Game.Tests.Resources; using SharpCompress.Archives.Zip; namespace osu.Game.Tests.Skins.IO { - public class ImportSkinTest + public class ImportSkinTest : ImportTest { [Test] public async Task TestBasicImport() @@ -26,9 +23,9 @@ namespace osu.Game.Tests.Skins.IO { try { - var osu = await loadOsu(host); + var osu = LoadOsuIntoHost(host); - var imported = await loadIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk")); + var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk")); Assert.That(imported.Name, Is.EqualTo("test skin")); Assert.That(imported.Creator, Is.EqualTo("skinner")); @@ -47,10 +44,10 @@ namespace osu.Game.Tests.Skins.IO { try { - var osu = await loadOsu(host); + var osu = LoadOsuIntoHost(host); - var imported = await loadIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk")); - var imported2 = await loadIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk")); + var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin.osk")); + var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin", "skinner"), "skin2.osk")); Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(1)); @@ -72,11 +69,11 @@ namespace osu.Game.Tests.Skins.IO { try { - var osu = await loadOsu(host); + var osu = LoadOsuIntoHost(host); // if a user downloads two skins that do have skin.ini files but don't have any creator metadata in the skin.ini, they should both import separately just for safety. - var imported = await loadIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk")); - var imported2 = await loadIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk")); + var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk")); + var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk(string.Empty, string.Empty), "download.osk")); Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(2)); @@ -98,10 +95,10 @@ namespace osu.Game.Tests.Skins.IO { try { - var osu = await loadOsu(host); + var osu = LoadOsuIntoHost(host); - var imported = await loadIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2", "skinner"), "skin.osk")); - var imported2 = await loadIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2.1", "skinner"), "skin2.osk")); + var imported = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2", "skinner"), "skin.osk")); + var imported2 = await loadSkinIntoOsu(osu, new ZipArchiveReader(createOsk("test skin v2.1", "skinner"), "skin2.osk")); Assert.That(imported2.ID, Is.Not.EqualTo(imported.ID)); Assert.That(osu.Dependencies.Get().GetAllUserSkins().Count, Is.EqualTo(2)); @@ -141,37 +138,10 @@ namespace osu.Game.Tests.Skins.IO return stream; } - private async Task loadIntoOsu(OsuGameBase osu, ArchiveReader archive = null) + private async Task loadSkinIntoOsu(OsuGameBase osu, ArchiveReader archive = null) { var skinManager = osu.Dependencies.Get(); return await skinManager.Import(archive); } - - private async Task loadOsu(GameHost host) - { - var osu = new OsuGameBase(); - -#pragma warning disable 4014 - Task.Run(() => host.Run(osu)); -#pragma warning restore 4014 - - waitForOrAssert(() => osu.IsLoaded, @"osu! failed to start in a reasonable amount of time"); - - var beatmapFile = TestResources.GetTestBeatmapForImport(); - var beatmapManager = osu.Dependencies.Get(); - await beatmapManager.Import(beatmapFile); - - return osu; - } - - private void waitForOrAssert(Func result, string failureMessage, int timeout = 60000) - { - Task task = Task.Run(() => - { - while (!result()) Thread.Sleep(200); - }); - - Assert.IsTrue(task.Wait(timeout), failureMessage); - } } } From 1fcf443314f2d75d34d791d4a13c8a2e3af8547f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Sep 2020 19:33:03 +0900 Subject: [PATCH 3280/6909] Ensure BeatmapProcessor.PostProcess is run before firing HitObjectUpdated events --- osu.Game/Screens/Edit/EditorBeatmap.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 3a9bd85b0f..3248c5b8be 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -207,14 +207,15 @@ namespace osu.Game.Screens.Edit beatmapProcessor?.PreProcess(); foreach (var hitObject in pendingUpdates) - { processHitObject(hitObject); - HitObjectUpdated?.Invoke(hitObject); - } - - pendingUpdates.Clear(); beatmapProcessor?.PostProcess(); + + // explicitly needs to be fired after PostProcess + foreach (var hitObject in pendingUpdates) + HitObjectUpdated?.Invoke(hitObject); + + pendingUpdates.Clear(); } } From 735b6b0d6ffd51dc96d9b74c6e5a07af5193aed4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Sep 2020 05:54:06 +0300 Subject: [PATCH 3281/6909] Remove a pointless portion of the inline comment --- osu.Game/Skinning/SkinnableSound.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 9e49134806..ba14049b41 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -140,8 +140,7 @@ namespace osu.Game.Skinning samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); - // Sample channels have been reloaded to new ones because skin has changed. - // Start playback internally for them if they were playing previously. + // Start playback internally for the new samples if the previous ones were playing beforehand. if (wasPlaying) play(); } From b3ffd36b656ea39a6d5b6d3cb2a344716c67aa17 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Sep 2020 05:55:28 +0300 Subject: [PATCH 3282/6909] Use lambda expression instead --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 5f39a57d8a..eb3636ab66 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -123,10 +123,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); AddUntilStep("wait for sample to stop playing", () => !sample.Playing); - AddStep("trigger skin change", () => - { - skinSource.TriggerSourceChanged(); - }); + AddStep("trigger skin change", () => skinSource.TriggerSourceChanged()); AddStep("retrieve new sample", () => { From 1e1422c16a52219c1a2b9c79f546172d1a943e6a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Sep 2020 05:55:39 +0300 Subject: [PATCH 3283/6909] Samples don't get paused... --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index eb3636ab66..ab66ee252c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -132,7 +132,7 @@ namespace osu.Game.Tests.Visual.Gameplay sample = newSample; }); - AddAssert("new sample paused", () => !sample.Playing); + AddAssert("new sample stopped", () => !sample.Playing); AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); AddWaitStep("wait a bit", 5); From 522e2cdbcdf41c3e7816caa51032ee30bf6ca6c6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 19 Sep 2020 05:56:35 +0300 Subject: [PATCH 3284/6909] Avoid embedding NUnit Assert inside test steps if possible --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index ab66ee252c..ed75d83151 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -125,11 +125,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("trigger skin change", () => skinSource.TriggerSourceChanged()); - AddStep("retrieve new sample", () => + AddAssert("retrieve and ensure current sample is different", () => { - DrawableSample newSample = skinnableSound.ChildrenOfType().Single(); - Assert.IsTrue(newSample != sample, "Sample still hasn't been updated after a skin change event"); - sample = newSample; + DrawableSample oldSample = sample; + sample = skinnableSound.ChildrenOfType().Single(); + return sample != oldSample; }); AddAssert("new sample stopped", () => !sample.Playing); From 847ec8c248e640227d781e06ab4f3188d7c3c3b5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Sep 2020 14:52:05 +0900 Subject: [PATCH 3285/6909] Fix n^2 characteristic in taiko diffcalc --- .../Preprocessing/StaminaCheeseDetector.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index d07bff4369..3b1a9ad777 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Utils; using osu.Game.Rulesets.Taiko.Objects; @@ -67,6 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing // as that index can be simply subtracted from the current index to get the number of elements in between // without off-by-one errors int indexBeforeLastRepeat = -1; + int lastMarkEnd = 0; for (int i = 0; i < hitObjects.Count; i++) { @@ -87,7 +89,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (repeatedLength < roll_min_repetitions) continue; - markObjectsAsCheese(i, repeatedLength); + markObjectsAsCheese(Math.Max(lastMarkEnd, i - repeatedLength + 1), i); + lastMarkEnd = i; } } @@ -113,6 +116,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing private void findTlTap(int parity, HitType type) { int tlLength = -2; + int lastMarkEnd = 0; for (int i = parity; i < hitObjects.Count; i += 2) { @@ -124,17 +128,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (tlLength < tl_min_repetitions) continue; - markObjectsAsCheese(i, tlLength); + markObjectsAsCheese(Math.Max(lastMarkEnd, i - tlLength + 1), i); + lastMarkEnd = i; } } /// - /// Marks elements counting backwards from as . + /// Marks all objects from to (inclusive) as . /// - private void markObjectsAsCheese(int end, int count) + private void markObjectsAsCheese(int start, int end) { - for (int i = 0; i < count; ++i) - hitObjects[end - i].StaminaCheese = true; + for (int i = start; i <= end; i++) + hitObjects[i].StaminaCheese = true; } } } From e0cef6686d5b80c53d460d540ab4843c12471d30 Mon Sep 17 00:00:00 2001 From: S Stewart Date: Sat, 19 Sep 2020 14:54:14 -0500 Subject: [PATCH 3286/6909] Change collection deletion notif to be consistent --- osu.Game/Collections/CollectionManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index a50ab5b07a..f96a689faf 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -12,6 +12,7 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -230,7 +231,7 @@ namespace osu.Game.Collections public void DeleteAll() { Collections.Clear(); - PostNotification?.Invoke(new SimpleNotification { Text = "Deleted all collections!" }); + PostNotification?.Invoke(new ProgressCompletionNotification { Text = "Deleted all collections!"}); } private readonly object saveLock = new object(); From c49dcca1ff9b2946efe38548ed5ffe7f4e8356b8 Mon Sep 17 00:00:00 2001 From: S Stewart Date: Sat, 19 Sep 2020 14:55:52 -0500 Subject: [PATCH 3287/6909] spacing oops --- osu.Game/Collections/CollectionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index f96a689faf..5f0f52125b 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -231,7 +231,7 @@ namespace osu.Game.Collections public void DeleteAll() { Collections.Clear(); - PostNotification?.Invoke(new ProgressCompletionNotification { Text = "Deleted all collections!"}); + PostNotification?.Invoke(new ProgressCompletionNotification { Text = "Deleted all collections!" }); } private readonly object saveLock = new object(); From d2f498a2689031099233203121e171c192fee810 Mon Sep 17 00:00:00 2001 From: S Stewart Date: Sat, 19 Sep 2020 15:13:52 -0500 Subject: [PATCH 3288/6909] remove unnec using --- osu.Game/Collections/CollectionManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 5f0f52125b..569ac749a4 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -12,7 +12,6 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Sprites; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; From 026fc2023b9d47352ce302f5a1dff3747cec6245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Sep 2020 18:10:44 +0200 Subject: [PATCH 3289/6909] Add visual tests for strong hit explosions --- .../DrawableTestStrongHit.cs | 44 +++++++++++++++++++ .../Skinning/TestSceneHitExplosion.cs | 25 +++++++---- 2 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs new file mode 100644 index 0000000000..7cb984b254 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.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; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class DrawableTestStrongHit : DrawableHit + { + private readonly HitResult type; + private readonly bool hitBoth; + + public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true) + : base(new Hit + { + IsStrong = true, + StartTime = startTime, + }) + { + // in order to create nested strong hit + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + this.type = type; + this.hitBoth = hitBoth; + } + + protected override void LoadAsyncComplete() + { + base.LoadAsyncComplete(); + + Result.Type = type; + + var nestedStrongHit = (DrawableStrongNestedHit)NestedHitObjects.Single(); + nestedStrongHit.Result.Type = hitBoth ? type : HitResult.Miss; + } + + 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 2b5efec7f9..48969e0f5a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -2,7 +2,6 @@ // 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.Scoring; @@ -15,24 +14,29 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning [TestFixture] public class TestSceneHitExplosion : TaikoSkinnableTestScene { - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestNormalHit() { - AddStep("Great", () => SetContents(() => getContentFor(HitResult.Great))); - AddStep("Good", () => SetContents(() => getContentFor(HitResult.Good))); - AddStep("Miss", () => SetContents(() => getContentFor(HitResult.Miss))); + AddStep("Great", () => SetContents(() => getContentFor(createHit(HitResult.Great)))); + AddStep("Good", () => SetContents(() => getContentFor(createHit(HitResult.Good)))); + AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss)))); } - private Drawable getContentFor(HitResult type) + [Test] + public void TestStrongHit([Values(false, true)] bool hitBoth) { - DrawableTaikoHitObject hit; + AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth)))); + AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth)))); + } + private Drawable getContentFor(DrawableTaikoHitObject hit) + { return new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - hit = createHit(type), + hit, new HitExplosion(hit) { Anchor = Anchor.Centre, @@ -43,5 +47,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning } private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type); + + private DrawableTaikoHitObject createStrongHit(HitResult type, bool hitBoth) + => new DrawableTestStrongHit(Time.Current, type, hitBoth); } } From 919b19612f06e622c799a001ac65aa508a1b0cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Sep 2020 16:29:36 +0200 Subject: [PATCH 3290/6909] Add lookups for strong hit explosions --- .../Skinning/TaikoLegacySkinTransformer.cs | 8 ++++++++ osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs | 2 ++ 2 files changed, 10 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index f032c5f485..c222ccb51f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -75,7 +75,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; case TaikoSkinComponents.TaikoExplosionGood: + case TaikoSkinComponents.TaikoExplosionGoodStrong: case TaikoSkinComponents.TaikoExplosionGreat: + case TaikoSkinComponents.TaikoExplosionGreatStrong: case TaikoSkinComponents.TaikoExplosionMiss: var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false); @@ -107,8 +109,14 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.TaikoExplosionGood: return "taiko-hit100"; + case TaikoSkinComponents.TaikoExplosionGoodStrong: + return "taiko-hit100k"; + case TaikoSkinComponents.TaikoExplosionGreat: return "taiko-hit300"; + + case TaikoSkinComponents.TaikoExplosionGreatStrong: + return "taiko-hit300k"; } throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type"); diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index ac4fb51661..0d785adb4a 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -17,7 +17,9 @@ namespace osu.Game.Rulesets.Taiko BarLine, TaikoExplosionMiss, TaikoExplosionGood, + TaikoExplosionGoodStrong, TaikoExplosionGreat, + TaikoExplosionGreatStrong, Scroller, Mascot, } From 074387c6763ed34c157fc85ef8a4b950e4b8a6ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Sep 2020 18:07:57 +0200 Subject: [PATCH 3291/6909] Show strong hit explosion where applicable --- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 29 ++++++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index f0585b9c50..e3eabbf88f 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using osuTK; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -9,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI @@ -45,24 +47,41 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject.Result?.Type ?? HitResult.Great)), _ => new DefaultHitExplosion()); + Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion()); } - private TaikoSkinComponents getComponentName(HitResult resultType) + private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject) { + var resultType = judgedObject.Result?.Type ?? HitResult.Great; + switch (resultType) { case HitResult.Miss: return TaikoSkinComponents.TaikoExplosionMiss; case HitResult.Good: - return TaikoSkinComponents.TaikoExplosionGood; + return useStrongExplosion(judgedObject) + ? TaikoSkinComponents.TaikoExplosionGoodStrong + : TaikoSkinComponents.TaikoExplosionGood; case HitResult.Great: - return TaikoSkinComponents.TaikoExplosionGreat; + return useStrongExplosion(judgedObject) + ? TaikoSkinComponents.TaikoExplosionGreatStrong + : TaikoSkinComponents.TaikoExplosionGreat; } - throw new ArgumentOutOfRangeException(nameof(resultType), "Invalid result type"); + throw new ArgumentOutOfRangeException(nameof(judgedObject), "Invalid result type"); + } + + private bool useStrongExplosion(DrawableHitObject judgedObject) + { + if (!(judgedObject.HitObject is Hit)) + return false; + + if (!(judgedObject.NestedHitObjects.SingleOrDefault() is DrawableStrongNestedHit nestedHit)) + return false; + + return judgedObject.Result.Type == nestedHit.Result.Type; } /// From 1c7556ea5d67793c6f363a6e661b2d042896baf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Sep 2020 19:38:57 +0200 Subject: [PATCH 3292/6909] Schedule explosion addition to ensure both hits are processed --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index dabdfe6f44..0e241be2bd 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -218,12 +218,16 @@ namespace osu.Game.Rulesets.Taiko.UI private void addDrumRollHit(DrawableDrumRollTick drawableTick) => drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick)); - private void addExplosion(DrawableHitObject drawableObject, HitType type) + /// + /// As legacy skins have different explosions for singular and double strong hits, + /// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame. + /// + private void addExplosion(DrawableHitObject drawableObject, HitType type) => Schedule(() => { hitExplosionContainer.Add(new HitExplosion(drawableObject)); if (drawableObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); - } + }); private class ProxyContainer : LifetimeManagementContainer { From 4072abaed8ba980a5890656f6aa20b693cecf295 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Sep 2020 16:26:33 +0200 Subject: [PATCH 3293/6909] Allow miss explosions to be displayed --- 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 0e241be2bd..7976d5bc6d 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -205,9 +205,6 @@ namespace osu.Game.Rulesets.Taiko.UI X = result.IsHit ? judgedObject.Position.X : 0, }); - if (!result.IsHit) - break; - var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre; addExplosion(judgedObject, type); From a0573af0e1edd56f58f8af59674de1ad95fa067a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Sep 2020 20:44:31 +0200 Subject: [PATCH 3294/6909] Fix test failure due to uninitialised drawable hit object --- osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 44452d70c1..99d1b72ea4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -174,7 +174,9 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addMissJudgement() { - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(new DrawableTestHit(new Hit()), new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); + DrawableTestHit h; + Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); + ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); } private void addBarLine(bool major, double delay = scroll_time) From 5b697580afcf26ce430d6dc7abc991f7a1769946 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Sep 2020 16:38:16 +0900 Subject: [PATCH 3295/6909] Add mention of ruleset templates to readme --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d3e9ca5121..7c749f3422 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,13 @@ If you are looking to install or test osu! without setting up a development envi If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. -## Developing or debugging +## Developing a custom ruleset + +osu! is designed to have extensible modular gameplay modes, called "rulesets". Building one of these allows a developer to harness the power of osu! for their own game style. To get started working on a ruleset, we have some templates available [here](https://github.com/ppy/osu-templates). + +You can see some examples of custom rulesets by visiting the [custom ruleset directory](https://github.com/ppy/osu/issues/5852). + +## Developing osu! Please make sure you have the following prerequisites: From 842f8bea55bce02fffffe3980719a871c6d0422f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Sep 2020 18:15:33 +0900 Subject: [PATCH 3296/6909] Fix bindings not correctly being cleaned up in OsuHitObjectComposer --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index f87bd53ec3..6513334977 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -46,13 +46,20 @@ namespace osu.Game.Rulesets.Osu.Edit distanceSnapToggle }; + private BindableList selectedHitObjects; + + private Bindable placementObject; + [BackgroundDependencyLoader] private void load() { LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both }); - EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); - EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid(); + selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); + selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); + + placementObject = EditorBeatmap.PlacementObject.GetBoundCopy(); + placementObject.ValueChanged += _ => updateDistanceSnapGrid(); distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); } From dd5b15c64fb422ca50475b879ec9130f55f528e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Sep 2020 18:27:15 +0900 Subject: [PATCH 3297/6909] Fix HitObjectContainer not correctly unbinding from startTime fast enough --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index f4f66f1272..9a0217a1eb 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -43,10 +43,20 @@ namespace osu.Game.Rulesets.UI return true; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + unbindStartTimeMap(); + } + public virtual void Clear(bool disposeChildren = true) { ClearInternal(disposeChildren); + unbindStartTimeMap(); + } + private void unbindStartTimeMap() + { foreach (var kvp in startTimeMap) kvp.Value.bindable.UnbindAll(); startTimeMap.Clear(); From 0cecb2bba348e9d178704d911339ba6df7a1b26b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Sep 2020 19:33:19 +0900 Subject: [PATCH 3298/6909] Remove incorrect assumption from tests --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index f7909071ea..9e78185272 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -7,7 +7,6 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Timing; @@ -194,13 +193,7 @@ namespace osu.Game.Rulesets.Osu.Tests addSeekStep(0); - AddStep("adjust track rate", () => MusicController.CurrentTrack.AddAdjustment(AdjustableProperty.Tempo, new BindableDouble(rate))); - // autoplay replay frames use track time; - // if a spin takes 1000ms in track time and we're playing with a 2x rate adjustment, the spin will take 500ms of *real* time. - // therefore we need to apply the rate adjustment to the replay itself to change from track time to real time, - // as real time is what we care about for spinners - // (so we're making the spin take 1000ms in real time *always*, regardless of the track clock's rate). - transformReplay(replay => applyRateAdjustment(replay, rate)); + AddStep("adjust track rate", () => Player.GameplayClockContainer.UserPlaybackRate.Value = rate); addSeekStep(1000); AddAssert("progress almost same", () => Precision.AlmostEquals(expectedProgress, drawableSpinner.Progress, 0.05)); From 3f788da06d0aa4379f8133ce319dcde18cabe1fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Sep 2020 19:39:54 +0900 Subject: [PATCH 3299/6909] Fix SPM changing incorrectly with playback rate changes --- .../Pieces/SpinnerRotationTracker.cs | 7 +++- .../Rulesets/UI/FrameStabilityContainer.cs | 8 +++- osu.Game/Screens/Play/GameplayClock.cs | 24 +++++++++++ .../Screens/Play/GameplayClockContainer.cs | 42 +++++++++++++++++-- 4 files changed, 75 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index f1a782cbb5..e949017ccf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces @@ -77,6 +79,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private bool rotationTransferred; + [Resolved] + private GameplayClock gameplayClock { get; set; } + protected override void Update() { base.Update(); @@ -126,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces currentRotation += angle; // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // (see: ModTimeRamp) - RateAdjustedRotation += (float)(Math.Abs(angle) * Clock.Rate); + RateAdjustedRotation += (float)(Math.Abs(angle) * gameplayClock.TrueGameplayRate); } } } diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index d574991fa0..b585a78f42 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; @@ -59,7 +61,7 @@ namespace osu.Game.Rulesets.UI { if (clock != null) { - stabilityGameplayClock.ParentGameplayClock = parentGameplayClock = clock; + parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock; GameplayClock.IsPaused.BindTo(clock.IsPaused); } } @@ -191,7 +193,9 @@ namespace osu.Game.Rulesets.UI private class StabilityGameplayClock : GameplayClock { - public IFrameBasedClock ParentGameplayClock; + public GameplayClock ParentGameplayClock; + + public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock.NonGameplayAdjustments; public StabilityGameplayClock(FramedClock underlyingClock) : base(underlyingClock) diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 4f2cf5005c..45da8816d6 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.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 System.Linq; using osu.Framework.Bindables; using osu.Framework.Timing; @@ -20,6 +22,11 @@ namespace osu.Game.Screens.Play public readonly BindableBool IsPaused = new BindableBool(); + /// + /// All adjustments applied to this clock which don't come from gameplay or mods. + /// + public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>(); + public GameplayClock(IFrameBasedClock underlyingClock) { this.underlyingClock = underlyingClock; @@ -29,6 +36,23 @@ namespace osu.Game.Screens.Play public double Rate => underlyingClock.Rate; + /// + /// The rate of gameplay when playback is at 100%. + /// This excludes any seeking / user adjustments. + /// + public double TrueGameplayRate + { + get + { + double baseRate = Rate; + + foreach (var adjustment in NonGameplayAdjustments) + baseRate /= adjustment.Value; + + return baseRate; + } + } + public bool IsRunning => underlyingClock.IsRunning; /// diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 7a9cb3dddd..d5c3a7232f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.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.Diagnostics.CodeAnalysis; using System.Linq; using System.Threading.Tasks; @@ -50,8 +51,10 @@ namespace osu.Game.Screens.Play /// /// The final clock which is exposed to underlying components. /// - [Cached] - public readonly GameplayClock GameplayClock; + public GameplayClock GameplayClock => localGameplayClock; + + [Cached(typeof(GameplayClock))] + private readonly LocalGameplayClock localGameplayClock; private Bindable userAudioOffset; @@ -79,7 +82,7 @@ namespace osu.Game.Screens.Play userOffsetClock = new HardwareCorrectionOffsetClock(platformOffsetClock); // the clock to be exposed via DI to children. - GameplayClock = new GameplayClock(userOffsetClock); + localGameplayClock = new LocalGameplayClock(userOffsetClock); GameplayClock.IsPaused.BindTo(IsPaused); } @@ -200,11 +203,26 @@ namespace osu.Game.Screens.Play protected override void Update() { if (!IsPaused.Value) + { userOffsetClock.ProcessFrame(); + } base.Update(); } + private double getTrueGameplayRate() + { + double baseRate = track.Rate; + + if (speedAdjustmentsApplied) + { + baseRate /= UserPlaybackRate.Value; + baseRate /= pauseFreqAdjust.Value; + } + + return baseRate; + } + private bool speedAdjustmentsApplied; private void updateRate() @@ -215,6 +233,9 @@ namespace osu.Game.Screens.Play track.AddAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.AddAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + localGameplayClock.MutableNonGameplayAdjustments.Add(pauseFreqAdjust); + localGameplayClock.MutableNonGameplayAdjustments.Add(UserPlaybackRate); + speedAdjustmentsApplied = true; } @@ -231,9 +252,24 @@ namespace osu.Game.Screens.Play track.RemoveAdjustment(AdjustableProperty.Frequency, pauseFreqAdjust); track.RemoveAdjustment(AdjustableProperty.Tempo, UserPlaybackRate); + localGameplayClock.MutableNonGameplayAdjustments.Remove(pauseFreqAdjust); + localGameplayClock.MutableNonGameplayAdjustments.Remove(UserPlaybackRate); + speedAdjustmentsApplied = false; } + public class LocalGameplayClock : GameplayClock + { + public readonly List> MutableNonGameplayAdjustments = new List>(); + + public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + + public LocalGameplayClock(FramedOffsetClock underlyingClock) + : base(underlyingClock) + { + } + } + private class HardwareCorrectionOffsetClock : FramedOffsetClock { // 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. From 508278505f71a7b72531787364f8ece28f07f9d7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Sep 2020 19:40:57 +0900 Subject: [PATCH 3300/6909] Make local clock private --- 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 d5c3a7232f..4094de1c4f 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -258,7 +258,7 @@ namespace osu.Game.Screens.Play speedAdjustmentsApplied = false; } - public class LocalGameplayClock : GameplayClock + private class LocalGameplayClock : GameplayClock { public readonly List> MutableNonGameplayAdjustments = new List>(); From bfe332909c5312df0e1e338217463305a5a5b691 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 21 Sep 2020 14:25:36 +0300 Subject: [PATCH 3301/6909] Remove "hide combo counter on break time" feature for being too complex The combo counter will be hidden at most one second after the break has started anyways, so why not just remove this feature if the way of implementing it is complicated to be merged within the legacy counter implementation. --- .../TestSceneComboCounter.cs | 15 -------------- .../Skinning/LegacyComboCounter.cs | 20 ------------------- osu.Game/Screens/Play/GameplayBeatmap.cs | 5 ----- osu.Game/Screens/Play/Player.cs | 1 - 4 files changed, 41 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs index 89521d616d..e79792e04a 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -3,8 +3,6 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects; @@ -12,7 +10,6 @@ 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.Screens.Play; using osuTK; using osuTK.Graphics; @@ -21,20 +18,9 @@ namespace osu.Game.Rulesets.Catch.Tests public class TestSceneComboCounter : CatchSkinnableTestScene { private ScoreProcessor scoreProcessor; - private GameplayBeatmap gameplayBeatmap; - private readonly Bindable isBreakTime = new BindableBool(); private Color4 judgedObjectColour = Color4.White; - [BackgroundDependencyLoader] - private void load() - { - gameplayBeatmap = new GameplayBeatmap(CreateBeatmapForSkinProvider()); - gameplayBeatmap.IsBreakTime.BindTo(isBreakTime); - Dependencies.Cache(gameplayBeatmap); - Add(gameplayBeatmap); - } - [SetUp] public void SetUp() => Schedule(() => { @@ -54,7 +40,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20); AddStep("perform miss", () => performJudgement(HitResult.Miss)); - AddToggleStep("toggle gameplay break", v => isBreakTime.Value = v); AddStep("randomize judged object colour", () => { judgedObjectColour = new Color4( diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index ccfabdc5fd..6a10ba5eb3 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.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 osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; -using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -47,23 +44,6 @@ namespace osu.Game.Rulesets.Catch.Skinning }; } - private IBindable isBreakTime; - - [Resolved(canBeNull: true)] - private GameplayBeatmap beatmap { get; set; } - - protected override void LoadComplete() - { - base.LoadComplete(); - - isBreakTime = beatmap?.IsBreakTime.GetBoundCopy(); - isBreakTime?.BindValueChanged(b => - { - if (b.NewValue) - this.FadeOut(400.0, Easing.OutQuint); - }); - } - public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index d7eed73275..64894544f4 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -16,11 +16,6 @@ namespace osu.Game.Screens.Play { public readonly IBeatmap PlayableBeatmap; - /// - /// Whether the gameplay is currently in a break. - /// - public IBindable IsBreakTime { get; } = new Bindable(); - public GameplayBeatmap(IBeatmap playableBeatmap) { PlayableBeatmap = playableBeatmap; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 478f88ab11..539f9227a3 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -632,7 +632,6 @@ namespace osu.Game.Screens.Play // bind component bindables. Background.IsBreakTime.BindTo(breakTracker.IsBreakTime); - gameplayBeatmap.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); From 25bf160d942d8c1bdb6dac7951073a145fe57656 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Sep 2020 22:30:14 +0900 Subject: [PATCH 3302/6909] Fix missing GameplayClock in some tests --- .../Objects/Drawables/Pieces/SpinnerRotationTracker.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index e949017ccf..05ed38d241 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private bool rotationTransferred; - [Resolved] + [Resolved(canBeNull: true)] private GameplayClock gameplayClock { get; set; } protected override void Update() @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces currentRotation += angle; // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // (see: ModTimeRamp) - RateAdjustedRotation += (float)(Math.Abs(angle) * gameplayClock.TrueGameplayRate); + RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate)); } } } From f629c33dc0544e920aeb18f0de40d0e4e1ea9887 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:14:31 +0900 Subject: [PATCH 3303/6909] Make explosion additive to match stable --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 6a10ba5eb3..cce8a81c00 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -89,6 +89,7 @@ namespace osu.Game.Rulesets.Catch.Skinning var explosion = new LegacyRollingCounter(skin, fontName, fontOverlap) { Alpha = 0.65f, + Blending = BlendingParameters.Additive, Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(1.5f), From a27a65bf03d1a2d5ac3a8ef98a41f867ef4528d8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:24:26 +0900 Subject: [PATCH 3304/6909] Don't recreate explosion counter each increment --- .../Skinning/LegacyComboCounter.cs | 59 +++++++------------ 1 file changed, 22 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index cce8a81c00..c3231e1e55 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -16,19 +16,14 @@ namespace osu.Game.Rulesets.Catch.Skinning /// public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter { - private readonly ISkin skin; - - private readonly string fontName; - private readonly float fontOverlap; - private readonly LegacyRollingCounter counter; + private readonly LegacyRollingCounter explosion; + public LegacyComboCounter(ISkin skin) { - this.skin = skin; - - fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; - fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; + var fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; + var fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; AutoSizeAxes = Axes.Both; @@ -37,18 +32,27 @@ namespace osu.Game.Rulesets.Catch.Skinning Origin = Anchor.Centre; Scale = new Vector2(0.8f); - InternalChild = counter = new LegacyRollingCounter(skin, fontName, fontOverlap) + InternalChildren = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + explosion = new LegacyRollingCounter(skin, fontName, fontOverlap) + { + Alpha = 0.65f, + Blending = BlendingParameters.Additive, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.5f), + }, + counter = new LegacyRollingCounter(skin, fontName, fontOverlap) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, }; } public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); - private LegacyRollingCounter lastExplosion; - private void updateCombo(int combo, Color4? hitObjectColour, bool immediate) { // There may still be existing transforms to the counter (including value change after 250ms), @@ -59,17 +63,12 @@ namespace osu.Game.Rulesets.Catch.Skinning if (combo == 0) { counter.Current.Value = 0; - if (lastExplosion != null) - lastExplosion.Current.Value = 0; + explosion.Current.Value = 0; this.FadeOut(immediate ? 0.0 : 400.0, Easing.Out); return; } - // Remove last explosion to not conflict with the upcoming one. - if (lastExplosion != null) - RemoveInternal(lastExplosion); - this.FadeIn().Delay(1000.0).FadeOut(300.0); // For simplicity, in the case of rewinding we'll just set the counter to the current combo value. @@ -86,25 +85,11 @@ namespace osu.Game.Rulesets.Catch.Skinning counter.Delay(250.0).ScaleTo(1f).ScaleTo(1.1f, 60.0).Then().ScaleTo(1f, 30.0); - var explosion = new LegacyRollingCounter(skin, fontName, fontOverlap) - { - Alpha = 0.65f, - Blending = BlendingParameters.Additive, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.5f), - Colour = hitObjectColour ?? Color4.White, - Depth = 1f, - }; - - AddInternal(explosion); + explosion.Colour = hitObjectColour ?? Color4.White; explosion.SetCountWithoutRolling(combo); - explosion.ScaleTo(1.9f, 400.0, Easing.Out) - .FadeOut(400.0) - .Expire(true); - - lastExplosion = explosion; + explosion.ScaleTo(1.5f).ScaleTo(1.9f, 400.0, Easing.Out) + .FadeOutFromOne(400.0); } } } From 92cda6bccb2e2ae4e5ca461b92d0fa42322726f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:27:47 +0900 Subject: [PATCH 3305/6909] Adjust xmldoc slightly --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index c3231e1e55..320fc9c440 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -12,7 +12,7 @@ using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Catch.Skinning { /// - /// A combo counter implementation that visually behaves almost similar to osu!stable's combo counter. + /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter. /// public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter { From 08d8975566b9a3474c3df1b3882e86e49fd3f649 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:35:18 +0900 Subject: [PATCH 3306/6909] Remove DisplayInitialCombo method for simplicity --- .../Skinning/LegacyComboCounter.cs | 12 +++++++++--- osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs | 2 +- osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs | 12 +----------- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 320fc9c440..047d9b3602 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -50,11 +50,17 @@ namespace osu.Game.Rulesets.Catch.Skinning }; } - public void DisplayInitialCombo(int combo) => updateCombo(combo, null, true); - public void UpdateCombo(int combo, Color4? hitObjectColour) => updateCombo(combo, hitObjectColour, false); + private int lastDisplayedCombo; - private void updateCombo(int combo, Color4? hitObjectColour, bool immediate) + public void UpdateCombo(int combo, Color4? hitObjectColour = null) { + bool immediate = Time.Elapsed < 0; + + if (combo == lastDisplayedCombo) + return; + + lastDisplayedCombo = combo; + // There may still be existing transforms to the counter (including value change after 250ms), // finish them immediately before new transforms. counter.FinishTransforms(); diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index b53711e4ed..deb2cb99ed 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Catch.UI protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); - ComboCounter?.DisplayInitialCombo(currentCombo); + ComboCounter?.UpdateCombo(currentCombo); } public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result) diff --git a/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs index 1363ed1352..cfb6879067 100644 --- a/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/UI/ICatchComboCounter.cs @@ -11,16 +11,6 @@ namespace osu.Game.Rulesets.Catch.UI /// public interface ICatchComboCounter : IDrawable { - /// - /// Updates the counter to display the provided as initial value. - /// The value should be immediately displayed without any animation. - /// - /// - /// This is required for when instantiating a combo counter in middle of accumulating combo (via skin change). - /// - /// The combo value to be displayed as initial. - void DisplayInitialCombo(int combo); - /// /// Updates the counter to animate a transition from the old combo value it had to the current provided one. /// @@ -29,6 +19,6 @@ namespace osu.Game.Rulesets.Catch.UI /// /// The new combo value. /// The colour of the object if hit, null on miss. - void UpdateCombo(int combo, Color4? hitObjectColour); + void UpdateCombo(int combo, Color4? hitObjectColour = null); } } From ffd4874ac0f6e98df62d7e09520e8ce46cead0b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:37:18 +0900 Subject: [PATCH 3307/6909] Remove unnecessary double suffixes --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 047d9b3602..5dcc532a08 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -86,16 +86,16 @@ namespace osu.Game.Rulesets.Catch.Skinning return; } - counter.ScaleTo(1.5f).ScaleTo(0.8f, 250.0, Easing.Out) + counter.ScaleTo(1.5f).ScaleTo(0.8f, 250, Easing.Out) .OnComplete(c => c.SetCountWithoutRolling(combo)); - counter.Delay(250.0).ScaleTo(1f).ScaleTo(1.1f, 60.0).Then().ScaleTo(1f, 30.0); + counter.Delay(250).ScaleTo(1f).ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30); explosion.Colour = hitObjectColour ?? Color4.White; explosion.SetCountWithoutRolling(combo); - explosion.ScaleTo(1.5f).ScaleTo(1.9f, 400.0, Easing.Out) - .FadeOutFromOne(400.0); + explosion.ScaleTo(1.5f).ScaleTo(1.9f, 400, Easing.Out) + .FadeOutFromOne(400); } } } From 1c58f568d61584ef5a5568b52c1eeefa9bcdf1d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 12:54:21 +0900 Subject: [PATCH 3308/6909] Simplify and reformat rewind/transform logic --- .../Skinning/LegacyComboCounter.cs | 48 ++++++++----------- .../UI/CatchComboDisplay.cs | 6 --- 2 files changed, 21 insertions(+), 33 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index 5dcc532a08..a87286da89 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -54,16 +54,14 @@ namespace osu.Game.Rulesets.Catch.Skinning public void UpdateCombo(int combo, Color4? hitObjectColour = null) { - bool immediate = Time.Elapsed < 0; - if (combo == lastDisplayedCombo) return; - lastDisplayedCombo = combo; - // There may still be existing transforms to the counter (including value change after 250ms), // finish them immediately before new transforms. - counter.FinishTransforms(); + counter.SetCountWithoutRolling(lastDisplayedCombo); + + lastDisplayedCombo = combo; // Combo fell to zero, roll down and fade out the counter. if (combo == 0) @@ -71,31 +69,27 @@ namespace osu.Game.Rulesets.Catch.Skinning counter.Current.Value = 0; explosion.Current.Value = 0; - this.FadeOut(immediate ? 0.0 : 400.0, Easing.Out); - return; + this.FadeOut(400, Easing.Out); } - - this.FadeIn().Delay(1000.0).FadeOut(300.0); - - // For simplicity, in the case of rewinding we'll just set the counter to the current combo value. - immediate |= Time.Elapsed < 0; - - if (immediate) + else { - counter.SetCountWithoutRolling(combo); - return; + this.FadeInFromZero().Delay(1000).FadeOut(300); + + counter.ScaleTo(1.5f) + .ScaleTo(0.8f, 250, Easing.Out) + .OnComplete(c => c.SetCountWithoutRolling(combo)); + + counter.Delay(250) + .ScaleTo(1f) + .ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30); + + explosion.Colour = hitObjectColour ?? Color4.White; + + explosion.SetCountWithoutRolling(combo); + explosion.ScaleTo(1.5f) + .ScaleTo(1.9f, 400, Easing.Out) + .FadeOutFromOne(400); } - - counter.ScaleTo(1.5f).ScaleTo(0.8f, 250, Easing.Out) - .OnComplete(c => c.SetCountWithoutRolling(combo)); - - counter.Delay(250).ScaleTo(1f).ScaleTo(1.1f, 60).Then().ScaleTo(1f, 30); - - explosion.Colour = hitObjectColour ?? Color4.White; - - explosion.SetCountWithoutRolling(combo); - explosion.ScaleTo(1.5f).ScaleTo(1.9f, 400, Easing.Out) - .FadeOutFromOne(400); } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index deb2cb99ed..58a3140bb5 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -50,12 +50,6 @@ namespace osu.Game.Rulesets.Catch.UI if (!result.Judgement.AffectsCombo || !result.HasResult) return; - if (result.Type == HitResult.Miss) - { - updateCombo(result.ComboAtJudgement, null); - return; - } - updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value); } From 1b261f177f8ea0f4b1ffc5bf554d5588daddeac6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 13:17:53 +0900 Subject: [PATCH 3309/6909] Disable rewind handling --- osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs index a87286da89..c8abc9e832 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs @@ -63,6 +63,14 @@ namespace osu.Game.Rulesets.Catch.Skinning lastDisplayedCombo = combo; + if (Time.Elapsed < 0) + { + // needs more work to make rewind somehow look good. + // basically we want the previous increment to play... or turning off RemoveCompletedTransforms (not feasible from a performance angle). + Hide(); + return; + } + // Combo fell to zero, roll down and fade out the counter. if (combo == 0) { @@ -73,7 +81,7 @@ namespace osu.Game.Rulesets.Catch.Skinning } else { - this.FadeInFromZero().Delay(1000).FadeOut(300); + this.FadeInFromZero().Then().Delay(1000).FadeOut(300); counter.ScaleTo(1.5f) .ScaleTo(0.8f, 250, Easing.Out) From 7c40071b21d047d3f14e92a4d876d8d11e8fc4c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 13:32:00 +0900 Subject: [PATCH 3310/6909] Revert changes to SkinnableTestScene but change load order --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 33 ++++++++------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 58e0b23fab..c0db05d5c5 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -153,38 +153,29 @@ namespace osu.Game.Tests.Visual { private readonly bool extrapolateAnimations; - private readonly HashSet legacyFontPrefixes = new HashSet(); - public TestLegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, bool extrapolateAnimations) : base(skin, storage, audioManager, "skin.ini") { this.extrapolateAnimations = extrapolateAnimations; - - // use a direct string lookup instead of enum to avoid having to reference ruleset assemblies. - legacyFontPrefixes.Add(GetConfig("HitCirclePrefix")?.Value ?? "default"); - legacyFontPrefixes.Add(GetConfig("ScorePrefix")?.Value ?? "score"); - legacyFontPrefixes.Add(GetConfig("ComboPrefix")?.Value ?? "score"); } public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { + var lookup = base.GetTexture(componentName, wrapModeS, wrapModeT); + + if (lookup != null) + return lookup; + // extrapolate frames to test longer animations - if (extrapolateAnimations && isAnimationComponent(componentName, out var number) && number < 60) - return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT); + if (extrapolateAnimations) + { + var match = Regex.Match(componentName, "-([0-9]*)"); - return base.GetTexture(componentName, wrapModeS, wrapModeT); - } + if (match.Length > 0 && int.TryParse(match.Groups[1].Value, out var number) && number < 60) + return base.GetTexture(componentName.Replace($"-{number}", $"-{number % 2}"), wrapModeS, wrapModeT); + } - private bool isAnimationComponent(string componentName, out int number) - { - number = 0; - - // legacy font glyph textures have the pattern "{fontPrefix}-{character}", which could be mistaken for an animation frame. - if (legacyFontPrefixes.Any(p => componentName.StartsWith($"{p}-"))) - return false; - - var match = Regex.Match(componentName, "-([0-9]*)"); - return match.Length > 0 && int.TryParse(match.Groups[1].Value, out number); + return null; } } } From 552968f65f7c5451faef6c96c144efe3bcefce31 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 13:38:52 +0900 Subject: [PATCH 3311/6909] Remove unnecessary using --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index c0db05d5c5..a856789d96 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using System.Text.RegularExpressions; using JetBrains.Annotations; using osu.Framework.Allocation; From 3276b9ae9cba2aaeb7b9d60001bfe05353931b0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 15:08:53 +0900 Subject: [PATCH 3312/6909] Fix fail animation breaking on post-fail judgements --- osu.Game/Screens/Play/FailAnimation.cs | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 54c644c999..608f20affd 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -89,6 +89,8 @@ namespace osu.Game.Screens.Play private void applyToPlayfield(Playfield playfield) { + double failTime = playfield.Time.Current; + foreach (var nested in playfield.NestedPlayfields) applyToPlayfield(nested); @@ -97,13 +99,29 @@ namespace osu.Game.Screens.Play if (appliedObjects.Contains(obj)) continue; - obj.RotateTo(RNG.NextSingle(-90, 90), duration); - obj.ScaleTo(obj.Scale * 0.5f, duration); - obj.MoveToOffset(new Vector2(0, 400), duration); + float rotation = RNG.NextSingle(-90, 90); + Vector2 originalPosition = obj.Position; + Vector2 originalScale = obj.Scale; + + dropOffScreen(obj, failTime, rotation, originalScale, originalPosition); + + // need to reapply the fail drop after judgement state changes + obj.ApplyCustomUpdateState += (o, _) => dropOffScreen(obj, failTime, rotation, originalScale, originalPosition); + appliedObjects.Add(obj); } } + private void dropOffScreen(DrawableHitObject obj, double failTime, float randomRotation, Vector2 originalScale, Vector2 originalPosition) + { + using (obj.BeginAbsoluteSequence(failTime)) + { + obj.RotateTo(randomRotation, duration); + obj.ScaleTo(originalScale * 0.5f, duration); + obj.MoveTo(originalPosition + new Vector2(0, 400), duration); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 3062fe44113c0ea8ef8e829db291297eac7d0f65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 15:55:25 +0900 Subject: [PATCH 3313/6909] Add editor key bindings to switch between screens --- .../Input/Bindings/GlobalActionContainer.cs | 27 +++++++++++++-- .../KeyBinding/GlobalKeyBindingsSection.cs | 12 +++++++ osu.Game/Screens/Edit/Editor.cs | 34 ++++++++++++++----- 3 files changed, 62 insertions(+), 11 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 45b07581ec..3cabfce7bb 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Input.Bindings handler = game; } - public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings); + public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings); public IEnumerable GlobalKeyBindings => new[] { @@ -50,6 +50,14 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select), }; + public IEnumerable EditorKeyBindings => new[] + { + new KeyBinding(new[] { InputKey.F1 }, GlobalAction.EditorComposeMode), + new KeyBinding(new[] { InputKey.F2 }, GlobalAction.EditorDesignMode), + new KeyBinding(new[] { InputKey.F3 }, GlobalAction.EditorTimingMode), + new KeyBinding(new[] { InputKey.F4 }, GlobalAction.EditorSetupMode), + }; + public IEnumerable InGameKeyBindings => new[] { new KeyBinding(InputKey.Space, GlobalAction.SkipCutscene), @@ -68,7 +76,7 @@ namespace osu.Game.Input.Bindings 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.Mute, GlobalAction.ToggleMute), new KeyBinding(InputKey.TrackPrevious, GlobalAction.MusicPrev), new KeyBinding(InputKey.F1, GlobalAction.MusicPrev), @@ -139,7 +147,7 @@ namespace osu.Game.Input.Bindings [Description("Quick exit (Hold)")] QuickExit, - // Game-wide beatmap msi ccotolle keybindings + // Game-wide beatmap music controller keybindings [Description("Next track")] MusicNext, @@ -166,5 +174,18 @@ namespace osu.Game.Input.Bindings [Description("Pause")] PauseGameplay, + + // Editor + [Description("Setup Mode")] + EditorSetupMode, + + [Description("Compose Mode")] + EditorComposeMode, + + [Description("Design Mode")] + EditorDesignMode, + + [Description("Timing Mode")] + EditorTimingMode, } } diff --git a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs index 5b44c486a3..9a27c55c53 100644 --- a/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs +++ b/osu.Game/Overlays/KeyBinding/GlobalKeyBindingsSection.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays.KeyBinding Add(new DefaultBindingsSubsection(manager)); Add(new AudioControlKeyBindingsSubsection(manager)); Add(new InGameKeyBindingsSubsection(manager)); + Add(new EditorKeyBindingsSubsection(manager)); } private class DefaultBindingsSubsection : KeyBindingsSubsection @@ -56,5 +57,16 @@ namespace osu.Game.Overlays.KeyBinding Defaults = manager.AudioControlKeyBindings; } } + + private class EditorKeyBindingsSubsection : KeyBindingsSubsection + { + protected override string Header => "Editor"; + + public EditorKeyBindingsSubsection(GlobalActionContainer manager) + : base(null) + { + Defaults = manager.EditorKeyBindings; + } + } } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 71340041f0..b7a59bc2e2 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -79,6 +79,8 @@ namespace osu.Game.Screens.Edit private EditorBeatmap editorBeatmap; private EditorChangeHandler changeHandler; + private EditorMenuBar menuBar; + private DependencyContainer dependencies; protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo); @@ -133,8 +135,6 @@ namespace osu.Game.Screens.Edit updateLastSavedHash(); - EditorMenuBar menuBar; - OsuMenuItem undoMenuItem; OsuMenuItem redoMenuItem; @@ -374,14 +374,32 @@ namespace osu.Game.Screens.Edit public bool OnPressed(GlobalAction action) { - if (action == GlobalAction.Back) + switch (action) { - // as we don't want to display the back button, manual handling of exit action is required. - this.Exit(); - return true; - } + case GlobalAction.Back: + // as we don't want to display the back button, manual handling of exit action is required. + this.Exit(); + return true; - return false; + case GlobalAction.EditorComposeMode: + menuBar.Mode.Value = EditorScreenMode.Compose; + return true; + + case GlobalAction.EditorDesignMode: + menuBar.Mode.Value = EditorScreenMode.Design; + return true; + + case GlobalAction.EditorTimingMode: + menuBar.Mode.Value = EditorScreenMode.Timing; + return true; + + case GlobalAction.EditorSetupMode: + menuBar.Mode.Value = EditorScreenMode.SongSetup; + return true; + + default: + return false; + } } public void OnReleased(GlobalAction action) From 8e432664600f4fb2c3ad2c19991ff61ae385fa5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 16:02:07 +0900 Subject: [PATCH 3314/6909] Fix compose mode not showing distance snap grid when entering with a selection --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index f87bd53ec3..f086b92b60 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -54,6 +54,9 @@ namespace osu.Game.Rulesets.Osu.Edit EditorBeatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); EditorBeatmap.PlacementObject.ValueChanged += _ => updateDistanceSnapGrid(); distanceSnapToggle.ValueChanged += _ => updateDistanceSnapGrid(); + + // we may be entering the screen with a selection already active + updateDistanceSnapGrid(); } protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) From b1f7cfbd5b81b397dddf3487f23bf62a03fb1bc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 17:34:14 +0900 Subject: [PATCH 3315/6909] Reduce children levels in RingPiece --- .../Spinners/Components/SpinnerPiece.cs | 6 +---- .../Objects/Drawables/Pieces/RingPiece.cs | 24 +++++++------------ 2 files changed, 10 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs index 65c8720031..2347d8a34c 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs @@ -34,11 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components Alpha = 0.5f, Child = new Box { RelativeSizeAxes = Axes.Both } }, - ring = new RingPiece - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - } + ring = new RingPiece() }; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs index 82e4383143..bcf64b81a6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics.Shapes; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { - public class RingPiece : Container + public class RingPiece : CircularContainer { public RingPiece() { @@ -18,21 +18,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Anchor = Anchor.Centre; Origin = Anchor.Centre; - InternalChild = new CircularContainer + Masking = true; + BorderThickness = 10; + BorderColour = Color4.White; + + Child = new Box { - Masking = true, - BorderThickness = 10, - BorderColour = Color4.White, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0, - RelativeSizeAxes = Axes.Both - } - } + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both }; } } From e0a2321822f5ddd9c9a2cf443703bd4f38f62801 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Sep 2020 18:17:04 +0900 Subject: [PATCH 3316/6909] Reduce complexity of AllHitObjects enumerator when nested playfields are not present --- osu.Game/Rulesets/UI/Playfield.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index c52183f3f2..d92ba210db 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -37,7 +37,21 @@ namespace osu.Game.Rulesets.UI /// /// All the s contained in this and all . /// - public IEnumerable AllHitObjects => HitObjectContainer?.Objects.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)) ?? Enumerable.Empty(); + public IEnumerable AllHitObjects + { + get + { + if (HitObjectContainer == null) + return Enumerable.Empty(); + + var enumerable = HitObjectContainer.Objects; + + if (nestedPlayfields.IsValueCreated) + enumerable = enumerable.Concat(NestedPlayfields.SelectMany(p => p.AllHitObjects)); + + return enumerable; + } + } /// /// All s nested inside this . From 260ca31df038384f63d3addbbe83c013883dad18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 12:31:50 +0900 Subject: [PATCH 3317/6909] Change default mute key to Ctrl+F4 for now --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 3cabfce7bb..41be4cfcc3 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -76,7 +76,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Alt, InputKey.Down }, GlobalAction.DecreaseVolume), new KeyBinding(new[] { InputKey.Alt, InputKey.MouseWheelDown }, GlobalAction.DecreaseVolume), - new KeyBinding(InputKey.Mute, GlobalAction.ToggleMute), + new KeyBinding(new[] { InputKey.Control, InputKey.F4 }, GlobalAction.ToggleMute), new KeyBinding(InputKey.TrackPrevious, GlobalAction.MusicPrev), new KeyBinding(InputKey.F1, GlobalAction.MusicPrev), From c38cd50723a204bd2fecbe357f5fd81ef01e1ac0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 13:16:46 +0900 Subject: [PATCH 3318/6909] Fix editor not using beatmap combo colours initially on load --- .../Screens/Edit/EditorScreenWithTimeline.cs | 1 + .../Skinning/BeatmapSkinProvidingContainer.cs | 58 +++++++++++++++---- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 67442aa55e..66d90809db 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -94,6 +94,7 @@ namespace osu.Game.Screens.Edit } }, }; + LoadComponentAsync(CreateMainContent(), content => { spinner.State.Value = Visibility.Hidden; diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs index 40335db697..fc01f0bd31 100644 --- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs +++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Audio; @@ -13,25 +14,62 @@ namespace osu.Game.Skinning /// public class BeatmapSkinProvidingContainer : SkinProvidingContainer { - private readonly Bindable beatmapSkins = new Bindable(); - private readonly Bindable beatmapHitsounds = new Bindable(); + private Bindable beatmapSkins; + private Bindable beatmapHitsounds; - protected override bool AllowConfigurationLookup => beatmapSkins.Value; - protected override bool AllowDrawableLookup(ISkinComponent component) => beatmapSkins.Value; - protected override bool AllowTextureLookup(string componentName) => beatmapSkins.Value; - protected override bool AllowSampleLookup(ISampleInfo componentName) => beatmapHitsounds.Value; + protected override bool AllowConfigurationLookup + { + get + { + if (beatmapSkins == null) + throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed."); + + return beatmapSkins.Value; + } + } + + protected override bool AllowDrawableLookup(ISkinComponent component) + { + if (beatmapSkins == null) + throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed."); + + return beatmapSkins.Value; + } + + protected override bool AllowTextureLookup(string componentName) + { + if (beatmapSkins == null) + throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed."); + + return beatmapSkins.Value; + } + + protected override bool AllowSampleLookup(ISampleInfo componentName) + { + if (beatmapSkins == null) + throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed."); + + return beatmapHitsounds.Value; + } public BeatmapSkinProvidingContainer(ISkin skin) : base(skin) { } - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); - config.BindWith(OsuSetting.BeatmapHitsounds, beatmapHitsounds); + var config = parent.Get(); + beatmapSkins = config.GetBindable(OsuSetting.BeatmapSkins); + beatmapHitsounds = config.GetBindable(OsuSetting.BeatmapHitsounds); + + return base.CreateChildDependencies(parent); + } + + [BackgroundDependencyLoader] + private void load() + { beatmapSkins.BindValueChanged(_ => TriggerSourceChanged()); beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged()); } From ba160aab76bbdc32484f08fe1751a2f5bd2501d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 15:41:43 +0900 Subject: [PATCH 3319/6909] Fix large construction/disposal overhead on beatmaps with hitobjects at same point in time --- .../Drawables/Connections/FollowPointRenderer.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 4d73e711bb..11571ea761 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -46,7 +46,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections private void addConnection(FollowPointConnection connection) { // Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections - int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => g1.StartTime.Value.CompareTo(g2.StartTime.Value))); + int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => + { + int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value); + + if (comp != 0) + return comp; + + // we always want to insert the new item after equal ones. + // this is important for beatmaps with multiple hitobjects at the same point in time. + // if we use standard comparison insert order, there will be a churn of connections getting re-updated to + // the next object at the point-in-time, adding a construction/disposal overhead (see FollowPointConnection.End implementation's ClearInternal). + // this is easily visible on https://osu.ppy.sh/beatmapsets/150945#osu/372245 + return -1; + })); if (index < connections.Count - 1) { From c5b684bd2e5b6862da0248ce8ce9d86f7e0b9a94 Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 23 Sep 2020 00:30:20 -0700 Subject: [PATCH 3320/6909] Fix typo in log when beatmap fails to load --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- osu.Game/Screens/Play/Player.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index e42a359d2e..28a77a8bdf 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Edit } catch (Exception e) { - Logger.Error(e, "Could not load beatmap sucessfully!"); + Logger.Error(e, "Could not load beatmap successfully!"); return; } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 539f9227a3..8e2ed583f2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -399,7 +399,7 @@ namespace osu.Game.Screens.Play } catch (Exception e) { - Logger.Error(e, "Could not load beatmap sucessfully!"); + Logger.Error(e, "Could not load beatmap successfully!"); //couldn't load, hard abort! return null; } From a1ec167982705b3802c75dad79a1d032f918a4a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 16:38:16 +0900 Subject: [PATCH 3321/6909] Add the ability to toggle new combo state from composer context menu --- .../TestSceneHitObjectAccentColour.cs | 2 +- .../Objects/Types/IHasComboInformation.cs | 5 ++ .../Compose/Components/SelectionHandler.cs | 57 +++++++++++++++++++ 3 files changed, 63 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index 2d5e4b911e..58cc324233 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -81,7 +81,7 @@ namespace osu.Game.Tests.Gameplay private class TestHitObjectWithCombo : ConvertHitObject, IHasComboInformation { - public bool NewCombo { get; } = false; + public bool NewCombo { get; set; } = false; public int ComboOffset { get; } = 0; public Bindable IndexInCurrentComboBindable { get; } = new Bindable(); diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index 4e3de04278..211c077d4f 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Objects.Types /// int ComboIndex { get; set; } + /// + /// Whether the HitObject starts a new combo. + /// + new bool NewCombo { get; set; } + Bindable LastInComboBindable { get; } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index f95bf350b6..c33c755940 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -20,6 +20,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Types; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components @@ -268,6 +269,24 @@ namespace osu.Game.Screens.Edit.Compose.Components changeHandler?.EndChange(); } + public void SetNewCombo(bool state) + { + changeHandler?.BeginChange(); + + foreach (var h in SelectedHitObjects) + { + var comboInfo = h as IHasComboInformation; + + if (comboInfo == null) + throw new InvalidOperationException($"Tried to change combo state of a {h.GetType()}, which doesn't implement {nameof(IHasComboInformation)}"); + + comboInfo.NewCombo = state; + EditorBeatmap?.UpdateHitObject(h); + } + + changeHandler?.EndChange(); + } + /// /// Removes a hit sample from all selected s. /// @@ -297,6 +316,9 @@ namespace osu.Game.Screens.Edit.Compose.Components items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints)); + if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation)) + items.Add(createNewComboMenuItem()); + if (selectedBlueprints.Count == 1) items.AddRange(selectedBlueprints[0].ContextMenuItems); @@ -326,6 +348,41 @@ namespace osu.Game.Screens.Edit.Compose.Components protected virtual IEnumerable GetContextMenuItemsForSelection(IEnumerable selection) => Enumerable.Empty(); + private MenuItem createNewComboMenuItem() + { + return new TernaryStateMenuItem("New combo", MenuItemType.Standard, setNewComboState) + { + State = { Value = getHitSampleState() } + }; + + void setNewComboState(TernaryState state) + { + switch (state) + { + case TernaryState.False: + SetNewCombo(false); + break; + + case TernaryState.True: + SetNewCombo(true); + break; + } + } + + TernaryState getHitSampleState() + { + int countExisting = selectedBlueprints.Select(b => b.HitObject as IHasComboInformation).Count(h => h.NewCombo); + + if (countExisting == 0) + return TernaryState.False; + + if (countExisting < SelectedHitObjects.Count()) + return TernaryState.Indeterminate; + + return TernaryState.True; + } + } + private MenuItem createHitSampleMenuItem(string name, string sampleName) { return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState) From 2d67faeb7250687ccf2457502452fffce705c839 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 16:40:56 +0900 Subject: [PATCH 3322/6909] Add xmldoc --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index c33c755940..71177fe3e4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -269,6 +269,11 @@ namespace osu.Game.Screens.Edit.Compose.Components changeHandler?.EndChange(); } + /// + /// Set the new combo state of all selected s. + /// + /// Whether to set or unset. + /// Throws if any selected object doesn't implement public void SetNewCombo(bool state) { changeHandler?.BeginChange(); From 487fc2a2c6912de760946f22bd31a5c9c7384901 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 16:58:22 +0900 Subject: [PATCH 3323/6909] Add missing change handler scopings to taiko context menu operations --- .../Edit/TaikoSelectionHandler.cs | 8 ++++++++ .../Compose/Components/SelectionHandler.cs | 18 +++++++++--------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index eebf6980fe..40565048c2 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Taiko.Edit yield return new TernaryStateMenuItem("Rim", action: state => { + ChangeHandler.BeginChange(); + foreach (var h in hits) { switch (state) @@ -35,6 +37,8 @@ namespace osu.Game.Rulesets.Taiko.Edit break; } } + + ChangeHandler.EndChange(); }) { State = { Value = getTernaryState(hits, h => h.Type == HitType.Rim) } @@ -47,6 +51,8 @@ namespace osu.Game.Rulesets.Taiko.Edit yield return new TernaryStateMenuItem("Strong", action: state => { + ChangeHandler.BeginChange(); + foreach (var h in hits) { switch (state) @@ -62,6 +68,8 @@ namespace osu.Game.Rulesets.Taiko.Edit EditorBeatmap?.UpdateHitObject(h); } + + ChangeHandler.EndChange(); }) { State = { Value = getTernaryState(hits, h => h.IsStrong) } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 71177fe3e4..ca824a7cef 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected EditorBeatmap EditorBeatmap { get; private set; } [Resolved(CanBeNull = true)] - private IEditorChangeHandler changeHandler { get; set; } + protected IEditorChangeHandler ChangeHandler { get; private set; } public SelectionHandler() { @@ -194,12 +194,12 @@ namespace osu.Game.Screens.Edit.Compose.Components private void deleteSelected() { - changeHandler?.BeginChange(); + ChangeHandler?.BeginChange(); foreach (var h in selectedBlueprints.ToList()) EditorBeatmap?.Remove(h.HitObject); - changeHandler?.EndChange(); + ChangeHandler?.EndChange(); } #endregion @@ -255,7 +255,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void AddHitSample(string sampleName) { - changeHandler?.BeginChange(); + ChangeHandler?.BeginChange(); foreach (var h in SelectedHitObjects) { @@ -266,7 +266,7 @@ namespace osu.Game.Screens.Edit.Compose.Components h.Samples.Add(new HitSampleInfo { Name = sampleName }); } - changeHandler?.EndChange(); + ChangeHandler?.EndChange(); } /// @@ -276,7 +276,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Throws if any selected object doesn't implement public void SetNewCombo(bool state) { - changeHandler?.BeginChange(); + ChangeHandler?.BeginChange(); foreach (var h in SelectedHitObjects) { @@ -289,7 +289,7 @@ namespace osu.Game.Screens.Edit.Compose.Components EditorBeatmap?.UpdateHitObject(h); } - changeHandler?.EndChange(); + ChangeHandler?.EndChange(); } /// @@ -298,12 +298,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void RemoveHitSample(string sampleName) { - changeHandler?.BeginChange(); + ChangeHandler?.BeginChange(); foreach (var h in SelectedHitObjects) h.SamplesBindable.RemoveAll(s => s.Name == sampleName); - changeHandler?.EndChange(); + ChangeHandler?.EndChange(); } #endregion From 02201d0ec66a23dbfa05001397bc216667e610e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 17:08:25 +0900 Subject: [PATCH 3324/6909] Fix incorrect cast logic --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 71177fe3e4..e85dbee6d9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -376,7 +376,7 @@ namespace osu.Game.Screens.Edit.Compose.Components TernaryState getHitSampleState() { - int countExisting = selectedBlueprints.Select(b => b.HitObject as IHasComboInformation).Count(h => h.NewCombo); + int countExisting = selectedBlueprints.Select(b => (IHasComboInformation)b.HitObject).Count(h => h.NewCombo); if (countExisting == 0) return TernaryState.False; From 8f3eb9a422c25472c35f08cc6c82f98881eadf84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 17:57:57 +0900 Subject: [PATCH 3325/6909] Fix taiko sample selection not updating when changing strong/rim type --- .../Objects/Drawables/DrawableHit.cs | 32 +++++++++++++++---- .../Drawables/DrawableTaikoHitObject.cs | 28 +++++++++++++++- 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 92ae7e0fd3..f4234445d6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -42,6 +42,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables : base(hit) { FillMode = FillMode.Fit; + + updateActionsFromType(); } [BackgroundDependencyLoader] @@ -50,21 +52,39 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables type = HitObject.TypeBindable.GetBoundCopy(); type.BindValueChanged(_ => { - updateType(); + updateActionsFromType(); + + // will overwrite samples, should only be called on change. + updateSamplesFromTypeChange(); + RecreatePieces(); }); - - updateType(); } - private void updateType() + private void updateSamplesFromTypeChange() + { + var rimSamples = HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); + + bool isRimType = HitObject.Type == HitType.Rim; + + if (isRimType != rimSamples.Any()) + { + if (isRimType) + HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }); + else + { + foreach (var sample in rimSamples) + HitObject.Samples.Remove(sample); + } + } + } + + private void updateActionsFromType() { 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 929cf8a937..0474de8453 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -141,7 +141,31 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private void load() { isStrong = HitObject.IsStrongBindable.GetBoundCopy(); - isStrong.BindValueChanged(_ => RecreatePieces(), true); + isStrong.BindValueChanged(_ => + { + // will overwrite samples, should only be called on change. + updateSamplesFromStrong(); + + RecreatePieces(); + }); + + RecreatePieces(); + } + + private void updateSamplesFromStrong() + { + var strongSamples = HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); + + if (isStrong.Value != strongSamples.Any()) + { + if (isStrong.Value) + HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH }); + else + { + foreach (var sample in strongSamples) + HitObject.Samples.Remove(sample); + } + } } protected virtual void RecreatePieces() @@ -150,6 +174,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece?.Expire(); Content.Add(MainPiece = CreateMainPiece()); + + LoadSamples(); } protected override void AddNestedHitObject(DrawableHitObject hitObject) From 9a0e5ac154242ee6fd1764582899e6c1c48114b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 18:09:40 +0900 Subject: [PATCH 3326/6909] Handle type/strength changes from samples changes --- .../Objects/Drawables/DrawableSlider.cs | 4 +-- .../Objects/Drawables/DrawableSpinner.cs | 4 +-- .../Objects/Drawables/DrawableHit.cs | 16 ++++++++-- .../Drawables/DrawableTaikoHitObject.cs | 30 ++++++++++++------- .../Objects/Drawables/DrawableHitObject.cs | 10 +++++-- 5 files changed, 43 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 07f40f763b..fc3bb76ae1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -89,9 +89,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private SkinnableSound slidingSample; - protected override void LoadSamples() + protected override void LoadSamples(bool changed) { - base.LoadSamples(); + base.LoadSamples(changed); slidingSample?.Expire(); slidingSample = null; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a57bb466c7..e78c886beb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -88,9 +88,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; - protected override void LoadSamples() + protected override void LoadSamples(bool changed) { - base.LoadSamples(); + base.LoadSamples(changed); spinningSample?.Expire(); spinningSample = null; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index f4234445d6..7608514817 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -61,19 +61,29 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables }); } + private HitSampleInfo[] rimSamples => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); + + protected override void LoadSamples(bool changed) + { + base.LoadSamples(changed); + + if (changed) + type.Value = rimSamples.Any() ? HitType.Rim : HitType.Centre; + } + private void updateSamplesFromTypeChange() { - var rimSamples = HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); + var samples = rimSamples; bool isRimType = HitObject.Type == HitType.Rim; - if (isRimType != rimSamples.Any()) + if (isRimType != samples.Any()) { if (isRimType) HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }); else { - foreach (var sample in rimSamples) + foreach (var sample in samples) HitObject.Samples.Remove(sample); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 0474de8453..4b1cd80967 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -1,19 +1,19 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics; -using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Objects.Drawables; -using osuTK; -using System.Linq; -using osu.Game.Audio; 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.Primitives; +using osu.Framework.Input.Bindings; +using osu.Game.Audio; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { @@ -152,17 +152,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables RecreatePieces(); } + private HitSampleInfo[] strongSamples => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); + + protected override void LoadSamples(bool changed) + { + base.LoadSamples(changed); + + if (changed) + isStrong.Value = strongSamples.Any(); + } + private void updateSamplesFromStrong() { - var strongSamples = HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); + var samples = strongSamples; - if (isStrong.Value != strongSamples.Any()) + if (isStrong.Value != samples.Any()) { if (isStrong.Value) HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH }); else { - foreach (var sample in strongSamples) + foreach (var sample in samples) HitObject.Samples.Remove(sample); } } @@ -174,8 +184,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables MainPiece?.Expire(); Content.Add(MainPiece = CreateMainPiece()); - - LoadSamples(); } protected override void AddNestedHitObject(DrawableHitObject hitObject) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 581617b567..4521556182 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (Result == null) throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); - LoadSamples(); + LoadSamples(false); } protected override void LoadAsyncComplete() @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } samplesBindable = HitObject.SamplesBindable.GetBoundCopy(); - samplesBindable.CollectionChanged += (_, __) => LoadSamples(); + samplesBindable.CollectionChanged += (_, __) => LoadSamples(true); apply(HitObject); } @@ -157,7 +157,11 @@ namespace osu.Game.Rulesets.Objects.Drawables updateState(ArmedState.Idle, true); } - protected virtual void LoadSamples() + /// + /// Called to perform sample-related logic. + /// + /// True if triggered from a post-load change to samples. + protected virtual void LoadSamples(bool changed) { if (Samples != null) { From fee379b4b9b997bd53e73e76fe626a2bd71fff93 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 18:12:07 +0900 Subject: [PATCH 3327/6909] Reword xmldoc for legibility --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 4521556182..08235e4ff8 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -158,9 +158,9 @@ namespace osu.Game.Rulesets.Objects.Drawables } /// - /// Called to perform sample-related logic. + /// Invoked by the base to populate samples, once on initial load and potentially again on any change to the samples collection. /// - /// True if triggered from a post-load change to samples. + /// True if triggered from a change to the samples collection. protected virtual void LoadSamples(bool changed) { if (Samples != null) From e8d099c01d842822acffe33a8dea781edba509a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 19:28:20 +0900 Subject: [PATCH 3328/6909] 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 6cbb4b2e68..d701aaf199 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 8d23a32c3c..71826e161c 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 d00b174195..90aa903318 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 673a75c46c6f0e57e903bb4b8c9f81111ca76587 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Sep 2020 21:06:11 +0900 Subject: [PATCH 3329/6909] Fix failing test --- osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs index 08130e60db..c2a18330c9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCommentsContainer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Tests.Visual.Online public void TestMultipleLoads() { var comments = exampleComments; - int topLevelCommentCount = exampleComments.Comments.Count(comment => comment.IsTopLevel); + int topLevelCommentCount = exampleComments.Comments.Count; AddStep("hide container", () => commentsContainer.Hide()); setUpCommentsResponse(comments); From f4d2c2684dd6dda8e53f0338a586d18c9e180491 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 12:21:08 +0900 Subject: [PATCH 3330/6909] Add more descriptive description and download button when statistics not available --- .../Ranking/Statistics/StatisticsPanel.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index c2ace6a04e..b0cff244b2 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -75,7 +75,21 @@ namespace osu.Game.Screens.Ranking.Statistics return; if (newScore.HitEvents == null || newScore.HitEvents.Count == 0) - content.Add(new MessagePlaceholder("Score has no statistics :(")); + content.Add(new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new MessagePlaceholder("Extended statistics are only available after watching a replay!"), + new ReplayDownloadButton(newScore) + { + Scale = new Vector2(1.5f), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } + }); else { spinner.Show(); From cb903ec9e2a378575a5b686a5c543f6883761036 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 12:21:46 +0900 Subject: [PATCH 3331/6909] Fix extended statistics not being vertically centered --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index c2ace6a04e..bd1b038181 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -91,7 +91,10 @@ namespace osu.Game.Screens.Ranking.Statistics { var rows = new FillFlowContainer { - RelativeSizeAxes = Axes.Both, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Direction = FillDirection.Vertical, Spacing = new Vector2(30, 15), Alpha = 0 From fda6e88dd364eb53e851e44abb5e390b09abd461 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 12:39:08 +0900 Subject: [PATCH 3332/6909] Fix braces style --- osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs index b0cff244b2..997a5b47ac 100644 --- a/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs +++ b/osu.Game/Screens/Ranking/Statistics/StatisticsPanel.cs @@ -75,6 +75,7 @@ namespace osu.Game.Screens.Ranking.Statistics return; if (newScore.HitEvents == null || newScore.HitEvents.Count == 0) + { content.Add(new FillFlowContainer { RelativeSizeAxes = Axes.Both, @@ -90,6 +91,7 @@ namespace osu.Game.Screens.Ranking.Statistics }, } }); + } else { spinner.Show(); From 56123575747a57bf2eca568f1d5b484a00bba7ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 12:49:32 +0900 Subject: [PATCH 3333/6909] Fix score panel being incorrectly vertically aligned on screen resize --- osu.Game/Screens/Ranking/ResultsScreen.cs | 6 +++--- osu.Game/Screens/Ranking/ScorePanelList.cs | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index c95cf1066e..c48cd238c0 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -273,10 +273,10 @@ namespace osu.Game.Screens.Ranking detachedPanelContainer.Add(expandedPanel); // Move into its original location in the local container first, then to the final location. - var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos); - expandedPanel.MoveTo(origLocation) + var origLocation = detachedPanelContainer.ToLocalSpace(screenSpacePos).X; + expandedPanel.MoveToX(origLocation) .Then() - .MoveTo(new Vector2(StatisticsPanel.SIDE_PADDING, origLocation.Y), 150, Easing.OutQuint); + .MoveToX(StatisticsPanel.SIDE_PADDING, 150, Easing.OutQuint); // Hide 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 10bd99c8ce..0d7d339df0 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -99,6 +99,8 @@ namespace osu.Game.Screens.Ranking { var panel = new ScorePanel(score) { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, PostExpandAction = () => PostExpandAction?.Invoke() }.With(p => { From 9c074e0ffbf10fe23af9ddd867c7b6eadd5c25e7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 13:10:54 +0900 Subject: [PATCH 3334/6909] Fix editor not showing sign when time goes negative --- .../Screens/Edit/Components/TimeInfoContainer.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index c1f54d7938..c68eeeb4f9 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -22,10 +22,12 @@ namespace osu.Game.Screens.Edit.Components { trackTimer = new OsuSpriteText { - Origin = Anchor.BottomLeft, - RelativePositionAxes = Axes.Y, - Font = OsuFont.GetFont(size: 22, fixedWidth: true), - Y = 0.5f, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + // intentionally fudged centre to avoid movement of the number portion when + // going negative. + X = -35, + Font = OsuFont.GetFont(size: 25, fixedWidth: true), } }; } @@ -34,7 +36,8 @@ namespace osu.Game.Screens.Edit.Components { base.Update(); - trackTimer.Text = TimeSpan.FromMilliseconds(editorClock.CurrentTime).ToString(@"mm\:ss\:fff"); + var timespan = TimeSpan.FromMilliseconds(editorClock.CurrentTime); + trackTimer.Text = $"{(timespan < TimeSpan.Zero ? "-" : string.Empty)}{timespan:mm\\:ss\\:fff}"; } } } From eb39f6dbd8f68244f6e91dca0706aa737636d46c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 13:17:03 +0900 Subject: [PATCH 3335/6909] Update failing test to find correct download button --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 03cb5fa3db..ff96a999ec 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -220,7 +220,7 @@ namespace osu.Game.Tests.Visual.Ranking AddStep("load results", () => Child = new TestResultsContainer(screen = createResultsScreen())); - AddAssert("download button is disabled", () => !screen.ChildrenOfType().Single().Enabled.Value); + AddAssert("download button is disabled", () => !screen.ChildrenOfType().Last().Enabled.Value); AddStep("click contracted panel", () => { @@ -229,7 +229,7 @@ namespace osu.Game.Tests.Visual.Ranking InputManager.Click(MouseButton.Left); }); - AddAssert("download button is enabled", () => screen.ChildrenOfType().Single().Enabled.Value); + AddAssert("download button is enabled", () => screen.ChildrenOfType().Last().Enabled.Value); } private class TestResultsContainer : Container From 156edf24c2736b28d134b9675e64aab08fd1f708 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 13:22:14 +0900 Subject: [PATCH 3336/6909] Change properties to methods and improve naming --- .../Objects/Drawables/DrawableHit.cs | 10 +++++----- .../Objects/Drawables/DrawableTaikoHitObject.cs | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 7608514817..7cf77885cf 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -61,29 +61,29 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables }); } - private HitSampleInfo[] rimSamples => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); + private HitSampleInfo[] getRimSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); protected override void LoadSamples(bool changed) { base.LoadSamples(changed); if (changed) - type.Value = rimSamples.Any() ? HitType.Rim : HitType.Centre; + type.Value = getRimSamples().Any() ? HitType.Rim : HitType.Centre; } private void updateSamplesFromTypeChange() { - var samples = rimSamples; + var rimSamples = getRimSamples(); bool isRimType = HitObject.Type == HitType.Rim; - if (isRimType != samples.Any()) + if (isRimType != rimSamples.Any()) { if (isRimType) HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }); else { - foreach (var sample in samples) + foreach (var sample in rimSamples) HitObject.Samples.Remove(sample); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 4b1cd80967..f3790e65af 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -152,27 +152,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables RecreatePieces(); } - private HitSampleInfo[] strongSamples => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); + private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); protected override void LoadSamples(bool changed) { base.LoadSamples(changed); if (changed) - isStrong.Value = strongSamples.Any(); + isStrong.Value = getStrongSamples().Any(); } private void updateSamplesFromStrong() { - var samples = strongSamples; + var strongSamples = getStrongSamples(); - if (isStrong.Value != samples.Any()) + if (isStrong.Value != strongSamples.Any()) { if (isStrong.Value) HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH }); else { - foreach (var sample in samples) + foreach (var sample in strongSamples) HitObject.Samples.Remove(sample); } } From 33fad27ec21705999cb6ad123d6c588fe60ff802 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 13:28:29 +0900 Subject: [PATCH 3337/6909] Avoid API change to DrawableHitObject --- .../Objects/Drawables/DrawableSlider.cs | 4 ++-- .../Objects/Drawables/DrawableSpinner.cs | 4 ++-- .../Objects/Drawables/DrawableHit.cs | 11 +++++------ .../Objects/Drawables/DrawableTaikoHitObject.cs | 11 +++++------ .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 7 +++---- 5 files changed, 17 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index fc3bb76ae1..07f40f763b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -89,9 +89,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private SkinnableSound slidingSample; - protected override void LoadSamples(bool changed) + protected override void LoadSamples() { - base.LoadSamples(changed); + base.LoadSamples(); slidingSample?.Expire(); slidingSample = null; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index e78c886beb..a57bb466c7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -88,9 +88,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; - protected override void LoadSamples(bool changed) + protected override void LoadSamples() { - base.LoadSamples(changed); + base.LoadSamples(); spinningSample?.Expire(); spinningSample = null; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 7cf77885cf..3a6eaa83db 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -36,11 +36,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private bool pressHandledThisFrame; - private Bindable type; + private readonly Bindable type; public DrawableHit(Hit hit) : base(hit) { + type = HitObject.TypeBindable.GetBoundCopy(); FillMode = FillMode.Fit; updateActionsFromType(); @@ -49,7 +50,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables [BackgroundDependencyLoader] private void load() { - type = HitObject.TypeBindable.GetBoundCopy(); type.BindValueChanged(_ => { updateActionsFromType(); @@ -63,12 +63,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private HitSampleInfo[] getRimSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); - protected override void LoadSamples(bool changed) + protected override void LoadSamples() { - base.LoadSamples(changed); + base.LoadSamples(); - if (changed) - type.Value = getRimSamples().Any() ? HitType.Rim : HitType.Centre; + type.Value = getRimSamples().Any() ? HitType.Rim : HitType.Centre; } private void updateSamplesFromTypeChange() diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index f3790e65af..9cd23383c4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected Vector2 BaseSize; protected SkinnableDrawable MainPiece; - private Bindable isStrong; + private readonly Bindable isStrong; private readonly Container strongHitContainer; @@ -128,6 +128,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables : base(hitObject) { HitObject = hitObject; + isStrong = HitObject.IsStrongBindable.GetBoundCopy(); Anchor = Anchor.CentreLeft; Origin = Anchor.Custom; @@ -140,7 +141,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables [BackgroundDependencyLoader] private void load() { - isStrong = HitObject.IsStrongBindable.GetBoundCopy(); isStrong.BindValueChanged(_ => { // will overwrite samples, should only be called on change. @@ -154,12 +154,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); - protected override void LoadSamples(bool changed) + protected override void LoadSamples() { - base.LoadSamples(changed); + base.LoadSamples(); - if (changed) - isStrong.Value = getStrongSamples().Any(); + isStrong.Value = getStrongSamples().Any(); } private void updateSamplesFromStrong() diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 08235e4ff8..28d3a39096 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (Result == null) throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); - LoadSamples(false); + LoadSamples(); } protected override void LoadAsyncComplete() @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } samplesBindable = HitObject.SamplesBindable.GetBoundCopy(); - samplesBindable.CollectionChanged += (_, __) => LoadSamples(true); + samplesBindable.CollectionChanged += (_, __) => LoadSamples(); apply(HitObject); } @@ -160,8 +160,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Invoked by the base to populate samples, once on initial load and potentially again on any change to the samples collection. /// - /// True if triggered from a change to the samples collection. - protected virtual void LoadSamples(bool changed) + protected virtual void LoadSamples() { if (Samples != null) { From 600b823a30aa562b350351c155db3f13fbfec5ee Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 24 Sep 2020 14:29:44 +0900 Subject: [PATCH 3338/6909] Fix game texture store being disposed by rulesets --- .../UI/DrawableRulesetDependencies.cs | 49 ++++++++++++++----- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index 83a1077d70..c1742970c7 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -10,6 +10,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Game.Rulesets.Configuration; @@ -46,12 +47,11 @@ namespace osu.Game.Rulesets.UI if (resources != null) { TextureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); - TextureStore.AddStore(parent.Get()); - Cache(TextureStore); + CacheAs(TextureStore = new FallbackTextureStore(TextureStore, parent.Get())); SampleStore = parent.Get().GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); SampleStore.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; - CacheAs(new FallbackSampleStore(SampleStore, parent.Get())); + CacheAs(SampleStore = new FallbackSampleStore(SampleStore, parent.Get())); } RulesetConfigManager = parent.Get().GetConfigFor(ruleset); @@ -82,6 +82,7 @@ namespace osu.Game.Rulesets.UI isDisposed = true; SampleStore?.Dispose(); + TextureStore?.Dispose(); RulesetConfigManager = null; } @@ -89,27 +90,24 @@ namespace osu.Game.Rulesets.UI } /// - /// A sample store which adds a fallback source. + /// A sample store which adds a fallback source and prevents disposal of the fallback source. /// - /// - /// This is a temporary implementation to workaround ISampleStore limitations. - /// public class FallbackSampleStore : ISampleStore { private readonly ISampleStore primary; - private readonly ISampleStore secondary; + private readonly ISampleStore fallback; - public FallbackSampleStore(ISampleStore primary, ISampleStore secondary) + public FallbackSampleStore(ISampleStore primary, ISampleStore fallback) { this.primary = primary; - this.secondary = secondary; + this.fallback = fallback; } - public SampleChannel Get(string name) => primary.Get(name) ?? secondary.Get(name); + public SampleChannel Get(string name) => primary.Get(name) ?? fallback.Get(name); - public Task GetAsync(string name) => primary.GetAsync(name) ?? secondary.GetAsync(name); + public Task GetAsync(string name) => primary.GetAsync(name) ?? fallback.GetAsync(name); - public Stream GetStream(string name) => primary.GetStream(name) ?? secondary.GetStream(name); + public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.GetStream(name); public IEnumerable GetAvailableResources() => throw new NotSupportedException(); @@ -145,6 +143,31 @@ namespace osu.Game.Rulesets.UI public void Dispose() { + primary?.Dispose(); + } + } + + /// + /// A texture store which adds a fallback source and prevents disposal of the fallback source. + /// + public class FallbackTextureStore : TextureStore + { + private readonly TextureStore primary; + private readonly TextureStore fallback; + + public FallbackTextureStore(TextureStore primary, TextureStore fallback) + { + this.primary = primary; + this.fallback = fallback; + } + + public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) + => primary.Get(name, wrapModeS, wrapModeT) ?? fallback.Get(name, wrapModeS, wrapModeT); + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + primary?.Dispose(); } } } From 62c2dbc3104157b88b981634e8a3d42f322eb7cf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 24 Sep 2020 14:33:43 +0900 Subject: [PATCH 3339/6909] Nest classes + make private --- .../UI/DrawableRulesetDependencies.cs | 146 +++++++++--------- 1 file changed, 73 insertions(+), 73 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index c1742970c7..a9b2a15b35 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -87,87 +87,87 @@ namespace osu.Game.Rulesets.UI } #endregion - } - /// - /// A sample store which adds a fallback source and prevents disposal of the fallback source. - /// - public class FallbackSampleStore : ISampleStore - { - private readonly ISampleStore primary; - private readonly ISampleStore fallback; - - public FallbackSampleStore(ISampleStore primary, ISampleStore fallback) + /// + /// A sample store which adds a fallback source and prevents disposal of the fallback source. + /// + private class FallbackSampleStore : ISampleStore { - this.primary = primary; - this.fallback = fallback; + private readonly ISampleStore primary; + private readonly ISampleStore fallback; + + public FallbackSampleStore(ISampleStore primary, ISampleStore fallback) + { + this.primary = primary; + this.fallback = fallback; + } + + public SampleChannel Get(string name) => primary.Get(name) ?? fallback.Get(name); + + public Task GetAsync(string name) => primary.GetAsync(name) ?? fallback.GetAsync(name); + + public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.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 void RemoveAllAdjustments(AdjustableProperty type) => 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() + { + primary?.Dispose(); + } } - public SampleChannel Get(string name) => primary.Get(name) ?? fallback.Get(name); - - public Task GetAsync(string name) => primary.GetAsync(name) ?? fallback.GetAsync(name); - - public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.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 void RemoveAllAdjustments(AdjustableProperty type) => 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 + /// + /// A texture store which adds a fallback source and prevents disposal of the fallback source. + /// + private class FallbackTextureStore : TextureStore { - get => throw new NotSupportedException(); - set => throw new NotSupportedException(); - } + private readonly TextureStore primary; + private readonly TextureStore fallback; - public void Dispose() - { - primary?.Dispose(); - } - } + public FallbackTextureStore(TextureStore primary, TextureStore fallback) + { + this.primary = primary; + this.fallback = fallback; + } - /// - /// A texture store which adds a fallback source and prevents disposal of the fallback source. - /// - public class FallbackTextureStore : TextureStore - { - private readonly TextureStore primary; - private readonly TextureStore fallback; + public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) + => primary.Get(name, wrapModeS, wrapModeT) ?? fallback.Get(name, wrapModeS, wrapModeT); - public FallbackTextureStore(TextureStore primary, TextureStore fallback) - { - this.primary = primary; - this.fallback = fallback; - } - - public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) - => primary.Get(name, wrapModeS, wrapModeT) ?? fallback.Get(name, wrapModeS, wrapModeT); - - protected override void Dispose(bool disposing) - { - base.Dispose(disposing); - primary?.Dispose(); + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + primary?.Dispose(); + } } } } From d666db362366bee77515635474325a7633138607 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 24 Sep 2020 14:46:28 +0900 Subject: [PATCH 3340/6909] Add test --- .../TestSceneDrawableRulesetDependencies.cs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs new file mode 100644 index 0000000000..89e3b48aa3 --- /dev/null +++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs @@ -0,0 +1,134 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Textures; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Rulesets +{ + public class TestSceneDrawableRulesetDependencies : OsuTestScene + { + [Test] + public void TestDisposalDoesNotDisposeParentStores() + { + DrawableWithDependencies drawable = null; + TestTextureStore textureStore = null; + TestSampleStore sampleStore = null; + + AddStep("add dependencies", () => + { + Child = drawable = new DrawableWithDependencies(); + textureStore = drawable.ParentTextureStore; + sampleStore = drawable.ParentSampleStore; + }); + + AddStep("clear children", Clear); + AddUntilStep("wait for disposal", () => drawable.IsDisposed); + + AddStep("GC", () => + { + drawable = null; + + GC.Collect(); + GC.WaitForPendingFinalizers(); + }); + + AddAssert("parent texture store not disposed", () => !textureStore.IsDisposed); + AddAssert("parent sample store not disposed", () => !sampleStore.IsDisposed); + } + + private class DrawableWithDependencies : CompositeDrawable + { + public TestTextureStore ParentTextureStore { get; private set; } + public TestSampleStore ParentSampleStore { get; private set; } + + public DrawableWithDependencies() + { + InternalChild = new Box { RelativeSizeAxes = Axes.Both }; + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + dependencies.CacheAs(ParentTextureStore = new TestTextureStore()); + dependencies.CacheAs(ParentSampleStore = new TestSampleStore()); + + return new DrawableRulesetDependencies(new OsuRuleset(), dependencies); + } + + public new bool IsDisposed { get; private set; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + IsDisposed = true; + } + } + + private class TestTextureStore : TextureStore + { + public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) => null; + + public bool IsDisposed { get; private set; } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + IsDisposed = true; + } + } + + private class TestSampleStore : ISampleStore + { + public bool IsDisposed { get; private set; } + + public void Dispose() + { + IsDisposed = true; + } + + public SampleChannel Get(string name) => null; + + public Task GetAsync(string name) => null; + + public Stream GetStream(string name) => null; + + public IEnumerable GetAvailableResources() => throw new NotImplementedException(); + + public BindableNumber Volume => throw new NotImplementedException(); + public BindableNumber Balance => throw new NotImplementedException(); + public BindableNumber Frequency => throw new NotImplementedException(); + public BindableNumber Tempo => throw new NotImplementedException(); + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException(); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException(); + + public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotImplementedException(); + + public IBindable AggregateVolume => throw new NotImplementedException(); + public IBindable AggregateBalance => throw new NotImplementedException(); + public IBindable AggregateFrequency => throw new NotImplementedException(); + public IBindable AggregateTempo => throw new NotImplementedException(); + + public int PlaybackConcurrency { get; set; } + } + } +} From 6ebea3f6f2c9f706b49ed5e367da4904c15a574c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 16:09:06 +0900 Subject: [PATCH 3341/6909] Add ability to toggle editor toggles using keyboard shortcuts (Q~P) --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 83 ++++++++++++++++++++- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 28a77a8bdf..b81e0ce159 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -58,6 +58,8 @@ namespace osu.Game.Rulesets.Edit private RadioButtonCollection toolboxCollection; + private ToolboxGroup togglesCollection; + protected HitObjectComposer(Ruleset ruleset) { Ruleset = ruleset; @@ -115,7 +117,7 @@ namespace osu.Game.Rulesets.Edit Children = new Drawable[] { new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }, - new ToolboxGroup("toggles") + togglesCollection = new ToolboxGroup("toggles") { ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox { @@ -190,9 +192,9 @@ namespace osu.Game.Rulesets.Edit protected override bool OnKeyDown(KeyDownEvent e) { - if (e.Key >= Key.Number1 && e.Key <= Key.Number9) + if (checkLeftToggleFromKey(e.Key, out var leftIndex)) { - var item = toolboxCollection.Items.ElementAtOrDefault(e.Key - Key.Number1); + var item = toolboxCollection.Items.ElementAtOrDefault(leftIndex); if (item != null) { @@ -201,9 +203,84 @@ namespace osu.Game.Rulesets.Edit } } + if (checkRightToggleFromKey(e.Key, out var rightIndex)) + { + var item = togglesCollection.Children[rightIndex]; + + if (item is SettingsCheckbox checkbox) + { + checkbox.Bindable.Value = !checkbox.Bindable.Value; + return true; + } + } + return base.OnKeyDown(e); } + private bool checkLeftToggleFromKey(Key key, out int index) + { + if (key < Key.Number1 || key > Key.Number9) + { + index = -1; + return false; + } + + index = key - Key.Number1; + return true; + } + + private bool checkRightToggleFromKey(Key key, out int index) + { + switch (key) + { + case Key.Q: + index = 0; + break; + + case Key.W: + index = 1; + break; + + case Key.E: + index = 2; + break; + + case Key.R: + index = 3; + break; + + case Key.T: + index = 4; + break; + + case Key.Y: + index = 5; + break; + + case Key.U: + index = 6; + break; + + case Key.I: + index = 7; + break; + + case Key.O: + index = 8; + break; + + case Key.P: + index = 9; + break; + + default: + index = -1; + break; + } + + return index >= 0; + } + private void selectionChanged(object sender, NotifyCollectionChangedEventArgs changedArgs) { if (EditorBeatmap.SelectedHitObjects.Any()) From 44be0ab76220d8c0e1d720fb26181e2c54c00788 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 14:22:18 +0900 Subject: [PATCH 3342/6909] Add basic osu! object to object snapping --- .../Edit/OsuHitObjectComposer.cs | 41 +++++++++++++++++++ .../Compose/Components/BlueprintContainer.cs | 2 +- 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index e1cbfa93f6..9fb9e49ad3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -15,6 +15,7 @@ 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.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; @@ -94,6 +95,10 @@ namespace osu.Game.Rulesets.Osu.Edit public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) { + if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) + return snapResult; + + // will be null if distance snap is disabled or not feasible for the current time value. if (distanceSnapGrid == null) return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); @@ -102,6 +107,42 @@ namespace osu.Game.Rulesets.Osu.Edit return new SnapResult(distanceSnapGrid.ToScreenSpace(pos), time, PlayfieldAtScreenSpacePosition(screenSpacePosition)); } + private bool snapToVisibleBlueprints(Vector2 screenSpacePosition, out SnapResult snapResult) + { + // check other on-screen objects for snapping/stacking + var blueprints = BlueprintContainer.SelectionBlueprints.AliveChildren; + + var playfield = PlayfieldAtScreenSpacePosition(screenSpacePosition); + + float snapRadius = + playfield.GamefieldToScreenSpace(new Vector2(OsuHitObject.OBJECT_RADIUS / 5)).X - + playfield.GamefieldToScreenSpace(Vector2.Zero).X; + + foreach (var b in blueprints) + { + if (b.IsSelected) + continue; + + var hitObject = b.HitObject; + + Vector2 startPos = ((IHasPosition)hitObject).Position; + + Vector2 objectScreenPos = playfield.GamefieldToScreenSpace(startPos); + + if (Vector2.Distance(objectScreenPos, screenSpacePosition) < snapRadius) + { + // bypasses time snapping + { + snapResult = new SnapResult(objectScreenPos, null, playfield); + return true; + } + } + } + + snapResult = null; + return false; + } + private void updateDistanceSnapGrid() { distanceSnapGridContainer.Clear(); diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index bf1e18771f..d5e4b4fee5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { protected DragBox DragBox { get; private set; } - protected Container SelectionBlueprints { get; private set; } + public Container SelectionBlueprints { get; private set; } private SelectionHandler selectionHandler; From d9e8ac6842f1ea3ed145ee07c8885964276b3a18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 14:34:41 +0900 Subject: [PATCH 3343/6909] Add support for slider end snapping --- .../Edit/OsuHitObjectComposer.cs | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 9fb9e49ad3..ad92016cd0 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -15,7 +15,6 @@ 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.Osu.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens.Edit.Compose.Components; @@ -123,19 +122,27 @@ namespace osu.Game.Rulesets.Osu.Edit if (b.IsSelected) continue; - var hitObject = b.HitObject; + var hitObject = (OsuHitObject)b.HitObject; - Vector2 startPos = ((IHasPosition)hitObject).Position; + Vector2? snap = checkSnap(hitObject.Position); + if (snap == null && hitObject.Position != hitObject.EndPosition) + snap = checkSnap(hitObject.EndPosition); - Vector2 objectScreenPos = playfield.GamefieldToScreenSpace(startPos); - - if (Vector2.Distance(objectScreenPos, screenSpacePosition) < snapRadius) + if (snap != null) { - // bypasses time snapping - { - snapResult = new SnapResult(objectScreenPos, null, playfield); - return true; - } + // only return distance portion, since time is not really valid + snapResult = new SnapResult(snap.Value, null, playfield); + return true; + } + + Vector2? checkSnap(Vector2 checkPos) + { + Vector2 checkScreenPos = playfield.GamefieldToScreenSpace(checkPos); + + if (Vector2.Distance(checkScreenPos, screenSpacePosition) < snapRadius) + return checkScreenPos; + + return null; } } From 1a98e8d7156c5dd624821e20ac59c168d6ebbdc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 14:46:03 +0900 Subject: [PATCH 3344/6909] Add test coverage of object-object snapping --- .../Editor/TestSceneObjectObjectSnap.cs | 67 +++++++++++++++++++ .../TestSceneOsuEditor.cs} | 4 +- 2 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs rename osu.Game.Rulesets.Osu.Tests/{TestSceneEditor.cs => Editor/TestSceneOsuEditor.cs} (76%) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs new file mode 100644 index 0000000000..b20c790bcb --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Tests.Beatmaps; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + [TestFixture] + public class TestSceneObjectObjectSnap : TestSceneOsuEditor + { + private OsuPlayfield playfield; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false); + + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First()); + } + + [Test] + public void TestHitCircleSnapsToOtherHitCircle() + { + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); + + AddStep("disable distance snap", () => + { + InputManager.PressKey(Key.Q); + InputManager.ReleaseKey(Key.Q); + }); + + AddStep("enter placement mode", () => + { + InputManager.PressKey(Key.Number2); + InputManager.ReleaseKey(Key.Number2); + }); + + AddStep("place first object", () => InputManager.Click(MouseButton.Left)); + + AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(5))); + + AddStep("place second object", () => InputManager.Click(MouseButton.Left)); + + AddAssert("both objects at same location", () => + { + var objects = EditorBeatmap.HitObjects; + + var first = (OsuHitObject)objects.First(); + var second = (OsuHitObject)objects.Last(); + + return first.Position == second.Position; + }); + + // TODO: remove + AddWaitStep("wait", 10); + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs similarity index 76% rename from osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs index 9239034a53..e1ca3ddd61 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuEditor.cs @@ -4,10 +4,10 @@ using NUnit.Framework; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { [TestFixture] - public class TestSceneEditor : EditorTestScene + public class TestSceneOsuEditor : EditorTestScene { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); } From 89ded2903c70a92d8a69951c3929b907d092507a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 16:22:47 +0900 Subject: [PATCH 3345/6909] Add test coverage of circle-slider snapping --- .../Editor/TestSceneObjectObjectSnap.cs | 47 +++++++++++++++++-- 1 file changed, 44 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs index b20c790bcb..94f826d01f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -4,8 +4,8 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.UI; using osu.Game.Tests.Beatmaps; @@ -59,9 +59,50 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return first.Position == second.Position; }); + } - // TODO: remove - AddWaitStep("wait", 10); + [Test] + public void TestHitCircleSnapsToSliderEnd() + { + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); + + AddStep("disable distance snap", () => + { + InputManager.PressKey(Key.Q); + InputManager.ReleaseKey(Key.Q); + }); + + AddStep("enter slider placement mode", () => + { + InputManager.PressKey(Key.Number3); + InputManager.ReleaseKey(Key.Number3); + }); + + AddStep("start slider placement", () => InputManager.Click(MouseButton.Left)); + + AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(120, 0))); + + AddStep("end slider placement", () => InputManager.Click(MouseButton.Right)); + + AddStep("enter circle placement mode", () => + { + InputManager.PressKey(Key.Number2); + InputManager.ReleaseKey(Key.Number2); + }); + + AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(130, 0))); + + AddStep("place second object", () => InputManager.Click(MouseButton.Left)); + + AddAssert("circle is at slider's end", () => + { + var objects = EditorBeatmap.HitObjects; + + var first = (Slider)objects.First(); + var second = (OsuHitObject)objects.Last(); + + return Precision.AlmostEquals(first.EndPosition, second.Position); + }); } } } From ead6479442614d54987f51a9c0ae9efb21e4cb67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 16:31:30 +0900 Subject: [PATCH 3346/6909] Also test with distance snap enabled for sanity --- .../Editor/TestSceneObjectObjectSnap.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs index 94f826d01f..1638ec8224 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -27,16 +27,20 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First()); } - [Test] - public void TestHitCircleSnapsToOtherHitCircle() + [TestCase(true)] + [TestCase(false)] + public void TestHitCircleSnapsToOtherHitCircle(bool distanceSnapEnabled) { AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); - AddStep("disable distance snap", () => + if (!distanceSnapEnabled) { - InputManager.PressKey(Key.Q); - InputManager.ReleaseKey(Key.Q); - }); + AddStep("disable distance snap", () => + { + InputManager.PressKey(Key.Q); + InputManager.ReleaseKey(Key.Q); + }); + } AddStep("enter placement mode", () => { From 15b1069099a8ff53e719839e570f08a8c1cd9fa1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 16:37:08 +0900 Subject: [PATCH 3347/6909] Fix tests not being relative to screen space --- .../Editor/TestSceneObjectObjectSnap.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs index 1638ec8224..da987c7f47 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("place first object", () => InputManager.Click(MouseButton.Left)); - AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(5))); + AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0))); AddStep("place second object", () => InputManager.Click(MouseButton.Left)); @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("start slider placement", () => InputManager.Click(MouseButton.Left)); - AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(120, 0))); + AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.185f, 0))); AddStep("end slider placement", () => InputManager.Click(MouseButton.Right)); @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.ReleaseKey(Key.Number2); }); - AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(130, 0))); + AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.20f, 0))); AddStep("place second object", () => InputManager.Click(MouseButton.Left)); From 158d3071261592bc6c5e41b39ac0909e6e938661 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 17:03:54 +0900 Subject: [PATCH 3348/6909] Avoid destroying editor screens when changing between modes --- .../Screens/Edit/Compose/ComposeScreen.cs | 5 +++++ osu.Game/Screens/Edit/Design/DesignScreen.cs | 1 + osu.Game/Screens/Edit/Editor.cs | 20 ++++++++++++++++--- osu.Game/Screens/Edit/EditorScreen.cs | 6 +++++- .../Screens/Edit/EditorScreenWithTimeline.cs | 5 +++++ osu.Game/Screens/Edit/Setup/SetupScreen.cs | 5 +++++ osu.Game/Screens/Edit/Timing/TimingScreen.cs | 5 +++++ 7 files changed, 43 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 04983ca597..d7a4661fa0 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -13,6 +13,11 @@ namespace osu.Game.Screens.Edit.Compose { private HitObjectComposer composer; + public ComposeScreen() + : base(EditorScreenMode.Compose) + { + } + protected override Drawable CreateMainContent() { var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance(); diff --git a/osu.Game/Screens/Edit/Design/DesignScreen.cs b/osu.Game/Screens/Edit/Design/DesignScreen.cs index 9f1fcf55b2..f15639733c 100644 --- a/osu.Game/Screens/Edit/Design/DesignScreen.cs +++ b/osu.Game/Screens/Edit/Design/DesignScreen.cs @@ -6,6 +6,7 @@ namespace osu.Game.Screens.Edit.Design public class DesignScreen : EditorScreen { public DesignScreen() + : base(EditorScreenMode.Design) { Child = new ScreenWhiteBox.UnderConstructionMessage("Design mode"); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b7a59bc2e2..2ffffe1c66 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Edit private string lastSavedHash; private Box bottomBackground; - private Container screenContainer; + private Container screenContainer; private EditorScreen currentScreen; @@ -163,7 +163,7 @@ namespace osu.Game.Screens.Edit Name = "Screen container", RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Top = 40, Bottom = 60 }, - Child = screenContainer = new Container + Child = screenContainer = new Container { RelativeSizeAxes = Axes.Both, Masking = true @@ -512,7 +512,21 @@ namespace osu.Game.Screens.Edit private void onModeChanged(ValueChangedEvent e) { - currentScreen?.Exit(); + var lastScreen = currentScreen; + + lastScreen? + .ScaleTo(0.98f, 200, Easing.OutQuint) + .FadeOut(200, Easing.OutQuint); + + if ((currentScreen = screenContainer.FirstOrDefault(s => s.Type == e.NewValue)) != null) + { + screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0); + + currentScreen + .ScaleTo(1, 200, Easing.OutQuint) + .FadeIn(200, Easing.OutQuint); + return; + } switch (e.NewValue) { diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index 8b5f0aaa71..52bffc4342 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -23,8 +23,12 @@ namespace osu.Game.Screens.Edit protected override Container Content => content; private readonly Container content; - protected EditorScreen() + public readonly EditorScreenMode Type; + + protected EditorScreen(EditorScreenMode type) { + Type = type; + Anchor = Anchor.Centre; Origin = Anchor.Centre; RelativeSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 66d90809db..34eddbefad 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -25,6 +25,11 @@ namespace osu.Game.Screens.Edit private Container timelineContainer; + protected EditorScreenWithTimeline(EditorScreenMode type) + : base(type) + { + } + [BackgroundDependencyLoader(true)] private void load([CanBeNull] BindableBeatDivisor beatDivisor) { diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index a2c8f19016..9dcdcb25f5 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -24,6 +24,11 @@ namespace osu.Game.Screens.Edit.Setup private LabelledTextBox creatorTextBox; private LabelledTextBox difficultyTextBox; + public SetupScreen() + : base(EditorScreenMode.SongSetup) + { + } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 8c40c8e721..d7da29218f 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -24,6 +24,11 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private EditorClock clock { get; set; } + public TimingScreen() + : base(EditorScreenMode.Timing) + { + } + protected override Drawable CreateMainContent() => new GridContainer { RelativeSizeAxes = Axes.Both, From 937d5870b3bde89750e79e3f0f5237641114a026 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 18:18:34 +0900 Subject: [PATCH 3349/6909] Add a basic file selector with extension filtering support --- .../Settings/TestSceneDirectorySelector.cs | 3 +- .../Visual/Settings/TestSceneFileSelector.cs | 18 +++++ .../Screens/StablePathSelectScreen.cs | 2 +- .../UserInterfaceV2/DirectorySelector.cs | 79 ++++++++++++------- .../Graphics/UserInterfaceV2/FileSelector.cs | 68 ++++++++++++++++ .../Maintenance/MigrationSelectScreen.cs | 2 +- 6 files changed, 138 insertions(+), 34 deletions(-) create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/FileSelector.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs index 0cd0f13b5f..082d85603e 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneDirectorySelector.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Platform; using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Tests.Visual.Settings @@ -11,7 +10,7 @@ namespace osu.Game.Tests.Visual.Settings public class TestSceneDirectorySelector : OsuTestScene { [BackgroundDependencyLoader] - private void load(GameHost host) + private void load() { Add(new DirectorySelector { RelativeSizeAxes = Axes.Both }); } diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs new file mode 100644 index 0000000000..08173148b0 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.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; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Tests.Visual.Settings +{ + public class TestSceneFileSelector : OsuTestScene + { + [BackgroundDependencyLoader] + private void load() + { + Add(new FileSelector { RelativeSizeAxes = Axes.Both }); + } + } +} diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs index b4d56f60c7..717b43f704 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/StablePathSelectScreen.cs @@ -129,7 +129,7 @@ namespace osu.Game.Tournament.Screens protected virtual void ChangePath() { - var target = directorySelector.CurrentDirectory.Value.FullName; + var target = directorySelector.CurrentPath.Value.FullName; var fileBasedIpc = ipc as FileBasedIPC; Logger.Log($"Changing Stable CE location to {target}"); diff --git a/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs b/osu.Game/Graphics/UserInterfaceV2/DirectorySelector.cs index ae34281bfb..a1cd074619 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] - public readonly Bindable CurrentDirectory = new Bindable(); + public readonly Bindable CurrentPath = new Bindable(); public DirectorySelector(string initialPath = null) { - CurrentDirectory.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); + CurrentPath.Value = new DirectoryInfo(initialPath ?? Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)); } [BackgroundDependencyLoader] @@ -74,7 +74,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 } }; - CurrentDirectory.BindValueChanged(updateDisplay, true); + CurrentPath.BindValueChanged(updateDisplay, true); } private void updateDisplay(ValueChangedEvent directory) @@ -92,22 +92,27 @@ namespace osu.Game.Graphics.UserInterfaceV2 } else { - directoryFlow.Add(new ParentDirectoryPiece(CurrentDirectory.Value.Parent)); + directoryFlow.Add(new ParentDirectoryPiece(CurrentPath.Value.Parent)); - foreach (var dir in CurrentDirectory.Value.GetDirectories().OrderBy(d => d.Name)) - { - if ((dir.Attributes & FileAttributes.Hidden) == 0) - directoryFlow.Add(new DirectoryPiece(dir)); - } + directoryFlow.AddRange(GetEntriesForPath(CurrentPath.Value)); } } catch (Exception) { - CurrentDirectory.Value = directory.OldValue; + CurrentPath.Value = directory.OldValue; this.FlashColour(Color4.Red, 300); } } + protected virtual IEnumerable GetEntriesForPath(DirectoryInfo path) + { + foreach (var dir in path.GetDirectories().OrderBy(d => d.Name)) + { + if ((dir.Attributes & FileAttributes.Hidden) == 0) + yield return new DirectoryPiece(dir); + } + } + private class CurrentDirectoryDisplay : CompositeDrawable { [Resolved] @@ -126,7 +131,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, Spacing = new Vector2(5), - Height = DirectoryPiece.HEIGHT, + Height = DisplayPiece.HEIGHT, Direction = FillDirection.Horizontal, }, }; @@ -150,7 +155,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 flow.ChildrenEnumerable = new Drawable[] { - new OsuSpriteText { Text = "Current Directory: ", Font = OsuFont.Default.With(size: DirectoryPiece.HEIGHT), }, + new OsuSpriteText { Text = "Current Directory: ", Font = OsuFont.Default.With(size: DisplayPiece.HEIGHT), }, new ComputerPiece(), }.Concat(pathPieces); } @@ -198,24 +203,44 @@ namespace osu.Game.Graphics.UserInterfaceV2 } } - private class DirectoryPiece : CompositeDrawable + protected class DirectoryPiece : DisplayPiece { - public const float HEIGHT = 20; - - protected const float FONT_SIZE = 16; - protected readonly DirectoryInfo Directory; - private readonly string displayName; - - protected FillFlowContainer Flow; - [Resolved] private Bindable currentDirectory { get; set; } public DirectoryPiece(DirectoryInfo directory, string displayName = null) + : base(displayName) { Directory = directory; + } + + protected override bool OnClick(ClickEvent e) + { + currentDirectory.Value = Directory; + return true; + } + + protected override string FallbackName => Directory.Name; + + protected override IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) + ? FontAwesome.Solid.Database + : FontAwesome.Regular.Folder; + } + + protected abstract class DisplayPiece : CompositeDrawable + { + public const float HEIGHT = 20; + + protected const float FONT_SIZE = 16; + + private readonly string displayName; + + protected FillFlowContainer Flow; + + protected DisplayPiece(string displayName = null) + { this.displayName = displayName; } @@ -259,20 +284,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Text = displayName ?? Directory.Name, + Text = displayName ?? FallbackName, Font = OsuFont.Default.With(size: FONT_SIZE) }); } - protected override bool OnClick(ClickEvent e) - { - currentDirectory.Value = Directory; - return true; - } + protected abstract string FallbackName { get; } - protected virtual IconUsage? Icon => Directory.Name.Contains(Path.DirectorySeparatorChar) - ? FontAwesome.Solid.Database - : FontAwesome.Regular.Folder; + protected abstract IconUsage? Icon { get; } } } } diff --git a/osu.Game/Graphics/UserInterfaceV2/FileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelector.cs new file mode 100644 index 0000000000..861d1887e1 --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelector.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class FileSelector : DirectorySelector + { + private readonly string[] validFileExtensions; + + [Cached] + public readonly Bindable CurrentFile = new Bindable(); + + public FileSelector(string initialPath = null, string[] validFileExtensions = null) + : base(initialPath) + { + this.validFileExtensions = validFileExtensions ?? Array.Empty(); + } + + protected override IEnumerable GetEntriesForPath(DirectoryInfo path) + { + foreach (var dir in base.GetEntriesForPath(path)) + yield return dir; + + IEnumerable files = path.GetFiles(); + + if (validFileExtensions.Length > 0) + files = files.Where(f => validFileExtensions.Contains(f.Extension)); + + foreach (var file in files.OrderBy(d => d.Name)) + { + if ((file.Attributes & FileAttributes.Hidden) == 0) + yield return new FilePiece(file); + } + } + + protected class FilePiece : DisplayPiece + { + private readonly FileInfo file; + + [Resolved] + private Bindable currentFile { get; set; } + + public FilePiece(FileInfo file) + { + this.file = file; + } + + protected override bool OnClick(ClickEvent e) + { + currentFile.Value = file; + return true; + } + + protected override string FallbackName => file.Name; + + protected override IconUsage? Icon => FontAwesome.Regular.FileAudio; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs index 79d842a617..ad540e3691 100644 --- a/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs +++ b/osu.Game/Overlays/Settings/Sections/Maintenance/MigrationSelectScreen.cs @@ -106,7 +106,7 @@ namespace osu.Game.Overlays.Settings.Sections.Maintenance private void start() { - var target = directorySelector.CurrentDirectory.Value; + var target = directorySelector.CurrentPath.Value; try { From ea77ea4a08757dc77cf2d1470666395efe79f349 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 17:24:05 +0900 Subject: [PATCH 3350/6909] Add basic testing of new beatmaps persistence --- .../Editing/TestSceneEditorBeatmapCreation.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs new file mode 100644 index 0000000000..3cc95dec9e --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.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 NUnit.Framework; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Storyboards; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorBeatmapCreation : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new DummyWorkingBeatmap(Audio, null); + + [Test] + public void TestCreateNewBeatmap() + { + AddStep("save beatmap", () => Editor.Save()); + AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0); + } + } +} From 4b9581bca0cbab33263239a8204dece936de3faa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 18:18:50 +0900 Subject: [PATCH 3351/6909] Add audio selection to song setup screen --- .../UserInterfaceV2/LabelledTextBox.cs | 9 ++- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 80 +++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 290aba3468..72d25a7836 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -44,12 +44,17 @@ namespace osu.Game.Graphics.UserInterfaceV2 Component.BorderColour = colours.Blue; } - protected override OsuTextBox CreateComponent() => new OsuTextBox + protected virtual OsuTextBox CreateTextBox() => new OsuTextBox { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, CornerRadius = CORNER_RADIUS, - }.With(t => t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText)); + }; + + protected override OsuTextBox CreateComponent() => CreateTextBox().With(t => + { + t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText); + }); } } diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index a2c8f19016..b0de9e6674 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -1,16 +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 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.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osuTK; @@ -23,10 +28,17 @@ namespace osu.Game.Screens.Edit.Setup private LabelledTextBox titleTextBox; private LabelledTextBox creatorTextBox; private LabelledTextBox difficultyTextBox; + private LabelledTextBox audioTrackTextBox; [BackgroundDependencyLoader] private void load(OsuColour colours) { + Container audioTrackFileChooserContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + Child = new Container { RelativeSizeAxes = Axes.Both, @@ -70,6 +82,18 @@ namespace osu.Game.Screens.Edit.Setup }, }, new OsuSpriteText + { + Text = "Resources" + }, + audioTrackTextBox = new FileChooserLabelledTextBox + { + Label = "Audio Track", + Current = { Value = Beatmap.Value.Metadata.AudioFile ?? "Click to select a track" }, + Target = audioTrackFileChooserContainer, + TabbableContentContainer = this + }, + audioTrackFileChooserContainer, + new OsuSpriteText { Text = "Beatmap metadata" }, @@ -120,4 +144,60 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; } } + + internal class FileChooserLabelledTextBox : LabelledTextBox + { + public Container Target; + + private readonly IBindable currentFile = new Bindable(); + + public FileChooserLabelledTextBox() + { + currentFile.BindValueChanged(onFileSelected); + } + + private void onFileSelected(ValueChangedEvent file) + { + if (file.NewValue == null) + return; + + Target.Clear(); + Current.Value = file.NewValue.FullName; + } + + protected override OsuTextBox CreateTextBox() => + new FileChooserOsuTextBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + CornerRadius = CORNER_RADIUS, + OnFocused = DisplayFileChooser + }; + + public void DisplayFileChooser() + { + Target.Child = new FileSelector("/Users/Dean/.osu/Songs", new[] { ".mp3", ".ogg" }) + { + RelativeSizeAxes = Axes.X, + Height = 400, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + CurrentFile = { BindTarget = currentFile } + }; + } + + internal class FileChooserOsuTextBox : OsuTextBox + { + public Action OnFocused; + + protected override void OnFocus(FocusEvent e) + { + OnFocused?.Invoke(); + base.OnFocus(e); + + GetContainingInputManager().TriggerFocusContention(this); + } + } + } } From 4d714866cdba4d3ef7c171c6d3f9316eb8cd5d43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 18:30:02 +0900 Subject: [PATCH 3352/6909] Add ability to actually import a new audio file to the beatmap / database --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 30 +++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index b0de9e6674..02b8afffe9 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -2,7 +2,6 @@ // 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; @@ -11,13 +10,16 @@ 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.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.IO; using osuTK; +using FileInfo = System.IO.FileInfo; namespace osu.Game.Screens.Edit.Setup { @@ -128,10 +130,36 @@ namespace osu.Game.Screens.Edit.Setup } }; + audioTrackTextBox.Current.BindValueChanged(audioTrackChanged); + foreach (var item in flow.OfType()) item.OnCommit += onCommit; } + [Resolved] + private FileStore files { get; set; } + + private void audioTrackChanged(ValueChangedEvent filePath) + { + var info = new FileInfo(filePath.NewValue); + + if (!info.Exists) + audioTrackTextBox.Current.Value = filePath.OldValue; + + IO.FileInfo osuFileInfo; + + using (var stream = info.OpenRead()) + osuFileInfo = files.Add(stream); + + Beatmap.Value.BeatmapSetInfo.Files.Add(new BeatmapSetFileInfo + { + FileInfo = osuFileInfo, + Filename = info.Name + }); + + Beatmap.Value.Metadata.AudioFile = info.Name; + } + private void onCommit(TextBox sender, bool newText) { if (!newText) return; From 65e6dd2ac3e01f205b9d4b838696bd49acf2a7d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 18:34:17 +0900 Subject: [PATCH 3353/6909] Remove the previous audio file before adding a new one --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 02b8afffe9..ce201e544a 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -144,14 +144,29 @@ namespace osu.Game.Screens.Edit.Setup var info = new FileInfo(filePath.NewValue); if (!info.Exists) + { audioTrackTextBox.Current.Value = filePath.OldValue; + return; + } + var beatmapFiles = Beatmap.Value.BeatmapSetInfo.Files; + + // remove the old file + var oldFile = beatmapFiles.FirstOrDefault(f => f.Filename == filePath.OldValue); + + if (oldFile != null) + { + beatmapFiles.Remove(oldFile); + files.Dereference(oldFile.FileInfo); + } + + // add the new file IO.FileInfo osuFileInfo; using (var stream = info.OpenRead()) osuFileInfo = files.Add(stream); - Beatmap.Value.BeatmapSetInfo.Files.Add(new BeatmapSetFileInfo + beatmapFiles.Add(new BeatmapSetFileInfo { FileInfo = osuFileInfo, Filename = info.Name From 978f6edf38488d7299f6e6df27a2887c74a077a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 18:55:49 +0900 Subject: [PATCH 3354/6909] Add basic track reloading support while inside the editor --- osu.Game/Overlays/MusicController.cs | 5 +++++ osu.Game/Screens/Edit/Editor.cs | 17 +++++++++++++++-- osu.Game/Screens/Edit/EditorClock.cs | 9 +++++++-- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 11 +++++++++++ 4 files changed, 38 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index b568e4d02b..66c9b15c0a 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -81,6 +81,11 @@ namespace osu.Game.Overlays mods.BindValueChanged(_ => ResetTrackAdjustments(), true); } + /// + /// Forcefully reload the current 's track from disk. + /// + public void ForceReloadCurrentBeatmap() => changeTrack(); + /// /// Change the position of a in the current playlist. /// diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b7a59bc2e2..74b92f3168 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -43,6 +43,7 @@ using osuTK.Input; namespace osu.Game.Screens.Edit { [Cached(typeof(IBeatSnapProvider))] + [Cached] public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider { public override float BackgroundParallaxAmount => 0.1f; @@ -91,6 +92,9 @@ namespace osu.Game.Screens.Edit [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private MusicController music { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours, GameHost host) { @@ -98,9 +102,9 @@ namespace osu.Game.Screens.Edit beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; - clock.ChangeSource(sourceClock); + + UpdateClockSource(); dependencies.CacheAs(clock); AddInternal(clock); @@ -271,6 +275,15 @@ namespace osu.Game.Screens.Edit bottomBackground.Colour = colours.Gray2; } + /// + /// If the beatmap's track has changed, this method must be called to keep the editor in a valid state. + /// + public void UpdateClockSource() + { + var sourceClock = (IAdjustableClock)Beatmap.Value.Track ?? new StopwatchClock(); + clock.ChangeSource(sourceClock); + } + protected void Save() { // apply any set-level metadata changes. diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index d4d0feb813..a829f23697 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Audio.Track; using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; using osu.Framework.Utils; @@ -17,7 +18,7 @@ namespace osu.Game.Screens.Edit /// public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { - public readonly double TrackLength; + public double TrackLength; public ControlPointInfo ControlPointInfo; @@ -190,7 +191,11 @@ namespace osu.Game.Screens.Edit public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; - public void ChangeSource(IClock source) => underlyingClock.ChangeSource(source); + public void ChangeSource(IClock source) + { + underlyingClock.ChangeSource(source); + TrackLength = (source as Track)?.Length ?? 60000; + } public IClock Source => underlyingClock.Source; diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index ce201e544a..075815203c 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osu.Game.IO; +using osu.Game.Overlays; using osuTK; using FileInfo = System.IO.FileInfo; @@ -139,6 +140,12 @@ namespace osu.Game.Screens.Edit.Setup [Resolved] private FileStore files { get; set; } + [Resolved] + private MusicController music { get; set; } + + [Resolved] + private Editor editor { get; set; } + private void audioTrackChanged(ValueChangedEvent filePath) { var info = new FileInfo(filePath.NewValue); @@ -173,6 +180,10 @@ namespace osu.Game.Screens.Edit.Setup }); Beatmap.Value.Metadata.AudioFile = info.Name; + + music.ForceReloadCurrentBeatmap(); + + editor.UpdateClockSource(); } private void onCommit(TextBox sender, bool newText) From 7e7e2fd64a6c9e95919028e838fd5439971d83b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 19:01:28 +0900 Subject: [PATCH 3355/6909] Use bindable for track to fix rate adjustments not applying correctly --- osu.Game/Screens/Edit/Components/BottomBarContainer.cs | 7 +++++-- osu.Game/Screens/Edit/Components/PlaybackControl.cs | 4 ++-- osu.Game/Screens/Edit/EditorClock.cs | 10 +++++++--- 3 files changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs index cb5078a479..08091fc3f7 100644 --- a/osu.Game/Screens/Edit/Components/BottomBarContainer.cs +++ b/osu.Game/Screens/Edit/Components/BottomBarContainer.cs @@ -18,7 +18,8 @@ namespace osu.Game.Screens.Edit.Components private const float contents_padding = 15; protected readonly IBindable Beatmap = new Bindable(); - protected Track Track => Beatmap.Value.Track; + + protected readonly IBindable Track = new Bindable(); private readonly Drawable background; private readonly Container content; @@ -42,9 +43,11 @@ namespace osu.Game.Screens.Edit.Components } [BackgroundDependencyLoader] - private void load(IBindable beatmap, OsuColour colours) + private void load(IBindable beatmap, OsuColour colours, EditorClock clock) { Beatmap.BindTo(beatmap); + Track.BindTo(clock.Track); + background.Colour = colours.Gray1; } } diff --git a/osu.Game/Screens/Edit/Components/PlaybackControl.cs b/osu.Game/Screens/Edit/Components/PlaybackControl.cs index 59b3d1c565..9739f2876a 100644 --- a/osu.Game/Screens/Edit/Components/PlaybackControl.cs +++ b/osu.Game/Screens/Edit/Components/PlaybackControl.cs @@ -62,12 +62,12 @@ namespace osu.Game.Screens.Edit.Components } }; - Track?.AddAdjustment(AdjustableProperty.Tempo, tempo); + Track.BindValueChanged(tr => tr.NewValue?.AddAdjustment(AdjustableProperty.Tempo, tempo), true); } protected override void Dispose(bool isDisposing) { - Track?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); + Track.Value?.RemoveAdjustment(AdjustableProperty.Tempo, tempo); base.Dispose(isDisposing); } diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index a829f23697..ec203df064 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Audio.Track; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; using osu.Framework.Utils; @@ -18,7 +19,11 @@ namespace osu.Game.Screens.Edit /// public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { - public double TrackLength; + public IBindable Track => track; + + private readonly Bindable track = new Bindable(); + + public double TrackLength => track.Value?.Length ?? 60000; public ControlPointInfo ControlPointInfo; @@ -36,7 +41,6 @@ namespace osu.Game.Screens.Edit this.beatDivisor = beatDivisor; ControlPointInfo = controlPointInfo; - TrackLength = trackLength; underlyingClock = new DecoupleableInterpolatingFramedClock(); } @@ -193,8 +197,8 @@ namespace osu.Game.Screens.Edit public void ChangeSource(IClock source) { + track.Value = source as Track; underlyingClock.ChangeSource(source); - TrackLength = (source as Track)?.Length ?? 60000; } public IClock Source => underlyingClock.Source; From 833ff1c1d77df62adf003cfa30c1753699e59a77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 19:12:12 +0900 Subject: [PATCH 3356/6909] Fix test failures due to editor dependency --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 075815203c..7090987093 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Edit.Setup [Resolved] private MusicController music { get; set; } - [Resolved] + [Resolved(canBeNull: true)] private Editor editor { get; set; } private void audioTrackChanged(ValueChangedEvent filePath) From cc9ae328116900fee199aff64e87d44d32f5be7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 21:05:29 +0900 Subject: [PATCH 3357/6909] Fix summary timeline not updating to new track length correctly --- .../Components/Timelines/Summary/Parts/TimelinePart.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 4a7c3f26bc..5b8f7c747b 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osuTK; using osu.Framework.Graphics; @@ -22,6 +23,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { protected readonly IBindable Beatmap = new Bindable(); + protected readonly IBindable Track = new Bindable(); + private readonly Container content; protected override Container Content => content; @@ -35,12 +38,15 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts updateRelativeChildSize(); LoadBeatmap(b.NewValue); }; + + Track.ValueChanged += _ => updateRelativeChildSize(); } [BackgroundDependencyLoader] - private void load(IBindable beatmap) + private void load(IBindable beatmap, EditorClock clock) { Beatmap.BindTo(beatmap); + Track.BindTo(clock.Track); } private void updateRelativeChildSize() From 011b17624429504950f3befcf6159a931a806f0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 22:00:13 +0900 Subject: [PATCH 3358/6909] Add test coverage of audio track changing --- .../Editing/TestSceneEditorBeatmapCreation.cs | 35 ++++++++++++++++++ osu.Game/Screens/Edit/Setup/SetupScreen.cs | 36 ++++++++++--------- osu.Game/Tests/Visual/EditorTestScene.cs | 6 ++-- 3 files changed, 59 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 3cc95dec9e..8ba172fc70 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -1,11 +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.IO; +using System.Linq; using NUnit.Framework; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Edit.Setup; using osu.Game.Storyboards; +using osu.Game.Tests.Resources; +using SharpCompress.Archives; +using SharpCompress.Archives.Zip; namespace osu.Game.Tests.Visual.Editing { @@ -13,6 +20,8 @@ namespace osu.Game.Tests.Visual.Editing { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + protected override bool EditorComponentsReady => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new DummyWorkingBeatmap(Audio, null); [Test] @@ -21,5 +30,31 @@ namespace osu.Game.Tests.Visual.Editing AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0); } + + [Test] + public void TestAddAudioTrack() + { + AddAssert("switch track to real track", () => + { + var setup = Editor.ChildrenOfType().First(); + + var temp = TestResources.GetTestBeatmapForImport(); + + string extractedFolder = $"{temp}_extracted"; + Directory.CreateDirectory(extractedFolder); + + using (var zip = ZipArchive.Open(temp)) + zip.WriteToDirectory(extractedFolder); + + bool success = setup.ChangeAudioTrack(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")); + + File.Delete(temp); + Directory.Delete(extractedFolder, true); + + return success; + }); + + AddAssert("track length changed", () => Beatmap.Value.Track.Length > 60000); + } } } diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 7090987093..e238e1fcf0 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -33,6 +33,15 @@ namespace osu.Game.Screens.Edit.Setup private LabelledTextBox difficultyTextBox; private LabelledTextBox audioTrackTextBox; + [Resolved] + private FileStore files { get; set; } + + [Resolved] + private MusicController music { get; set; } + + [Resolved(canBeNull: true)] + private Editor editor { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -137,29 +146,17 @@ namespace osu.Game.Screens.Edit.Setup item.OnCommit += onCommit; } - [Resolved] - private FileStore files { get; set; } - - [Resolved] - private MusicController music { get; set; } - - [Resolved(canBeNull: true)] - private Editor editor { get; set; } - - private void audioTrackChanged(ValueChangedEvent filePath) + public bool ChangeAudioTrack(string path) { - var info = new FileInfo(filePath.NewValue); + var info = new FileInfo(path); if (!info.Exists) - { - audioTrackTextBox.Current.Value = filePath.OldValue; - return; - } + return false; var beatmapFiles = Beatmap.Value.BeatmapSetInfo.Files; // remove the old file - var oldFile = beatmapFiles.FirstOrDefault(f => f.Filename == filePath.OldValue); + var oldFile = beatmapFiles.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile); if (oldFile != null) { @@ -184,6 +181,13 @@ namespace osu.Game.Screens.Edit.Setup music.ForceReloadCurrentBeatmap(); editor.UpdateClockSource(); + return true; + } + + private void audioTrackChanged(ValueChangedEvent filePath) + { + if (!ChangeAudioTrack(filePath.NewValue)) + audioTrackTextBox.Current.Value = filePath.OldValue; } private void onCommit(TextBox sender, bool newText) diff --git a/osu.Game/Tests/Visual/EditorTestScene.cs b/osu.Game/Tests/Visual/EditorTestScene.cs index 8f76f247cf..a9ee8e2668 100644 --- a/osu.Game/Tests/Visual/EditorTestScene.cs +++ b/osu.Game/Tests/Visual/EditorTestScene.cs @@ -26,13 +26,15 @@ namespace osu.Game.Tests.Visual Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); } + protected virtual bool EditorComponentsReady => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true + && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; + public override void SetUpSteps() { base.SetUpSteps(); AddStep("load editor", () => LoadScreen(Editor = CreateEditor())); - AddUntilStep("wait for editor to load", () => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true - && Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true); + AddUntilStep("wait for editor to load", () => EditorComponentsReady); AddStep("get beatmap", () => EditorBeatmap = Editor.ChildrenOfType().Single()); AddStep("get clock", () => EditorClock = Editor.ChildrenOfType().Single()); } From 94c1cc8ffa4b8eefb0c7bd498084e0d2dbee9c6f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 22:25:04 +0900 Subject: [PATCH 3359/6909] Fix test runs under headless --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 8ba172fc70..7215b80a97 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.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 NUnit.Framework; @@ -22,11 +23,17 @@ namespace osu.Game.Tests.Visual.Editing protected override bool EditorComponentsReady => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; - protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new DummyWorkingBeatmap(Audio, null); + public override void SetUpSteps() + { + AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)); + + base.SetUpSteps(); + } [Test] public void TestCreateNewBeatmap() { + AddStep("add random hitobject", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString()); AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0); } From dbc522aedea4e0cf3e5b36b07e4e5f04d36f4b0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Sep 2020 22:41:52 +0900 Subject: [PATCH 3360/6909] Remove weird using --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 7215b80a97..df2fdfa79d 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -10,7 +10,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Edit.Setup; -using osu.Game.Storyboards; using osu.Game.Tests.Resources; using SharpCompress.Archives; using SharpCompress.Archives.Zip; From c3df7e1fa8c897971e9662f909ada3444e203930 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 01:05:12 +0900 Subject: [PATCH 3361/6909] Fix scroll container's scrollbar not respecting minimum size on first resize --- osu.Game/Graphics/Containers/OsuScrollContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index d504a11b22..ed5c73bee6 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -112,6 +112,9 @@ namespace osu.Game.Graphics.Containers CornerRadius = 5; + // needs to be set initially for the ResizeTo to respect minimum size + Size = new Vector2(SCROLL_BAR_HEIGHT); + const float margin = 3; Margin = new MarginPadding From 6ff26f6b8c943e1178a5e4ebf78c086501cc75cb Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 24 Sep 2020 12:52:42 -0700 Subject: [PATCH 3362/6909] Fix anchor of tournament ruleset selector dropdown --- osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs index 0e995ca73d..af0043436a 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs @@ -73,8 +73,8 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, new Container { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Child = Component = CreateComponent().With(d => From 8a0c79466d8fdf159b07d360b150b27b9e73b23d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:16:50 +0900 Subject: [PATCH 3363/6909] Use simplified methods for press/release key --- .../Editor/TestSceneObjectObjectSnap.cs | 32 +++---------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs index da987c7f47..1ca94df26b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -34,19 +34,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); if (!distanceSnapEnabled) - { - AddStep("disable distance snap", () => - { - InputManager.PressKey(Key.Q); - InputManager.ReleaseKey(Key.Q); - }); - } + AddStep("disable distance snap", () => InputManager.Key(Key.Q)); - AddStep("enter placement mode", () => - { - InputManager.PressKey(Key.Number2); - InputManager.ReleaseKey(Key.Number2); - }); + AddStep("enter placement mode", () => InputManager.Key(Key.Number2)); AddStep("place first object", () => InputManager.Click(MouseButton.Left)); @@ -70,17 +60,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); - AddStep("disable distance snap", () => - { - InputManager.PressKey(Key.Q); - InputManager.ReleaseKey(Key.Q); - }); + AddStep("disable distance snap", () => InputManager.Key(Key.Q)); - AddStep("enter slider placement mode", () => - { - InputManager.PressKey(Key.Number3); - InputManager.ReleaseKey(Key.Number3); - }); + AddStep("enter slider placement mode", () => InputManager.Key(Key.Number3)); AddStep("start slider placement", () => InputManager.Click(MouseButton.Left)); @@ -88,11 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("end slider placement", () => InputManager.Click(MouseButton.Right)); - AddStep("enter circle placement mode", () => - { - InputManager.PressKey(Key.Number2); - InputManager.ReleaseKey(Key.Number2); - }); + AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2)); AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.20f, 0))); From 44a6637c36a065f13b22d7786a8f22ed16c40840 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:20:37 +0900 Subject: [PATCH 3364/6909] Use SingleOrDefault --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 2ffffe1c66..c9d57785f9 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -518,7 +518,7 @@ namespace osu.Game.Screens.Edit .ScaleTo(0.98f, 200, Easing.OutQuint) .FadeOut(200, Easing.OutQuint); - if ((currentScreen = screenContainer.FirstOrDefault(s => s.Type == e.NewValue)) != null) + if ((currentScreen = screenContainer.SingleOrDefault(s => s.Type == e.NewValue)) != null) { screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0); From d602072ee3ddb834899295a7c664b6f170d73175 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:24:41 +0900 Subject: [PATCH 3365/6909] Use SingleOrDefault where feasible --- osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index df2fdfa79d..d4976c3d48 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Editing { protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); - protected override bool EditorComponentsReady => Editor.ChildrenOfType().FirstOrDefault()?.IsLoaded == true; + protected override bool EditorComponentsReady => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true; public override void SetUpSteps() { From 9846d87eb0787ec65f0df9ec6558448dbb18f0b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:25:50 +0900 Subject: [PATCH 3366/6909] Fix misleading step name (and add comment as to its purpose) --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index d4976c3d48..ceacbd51a2 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -32,7 +32,10 @@ namespace osu.Game.Tests.Visual.Editing [Test] public void TestCreateNewBeatmap() { - AddStep("add random hitobject", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString()); + // if we save a beatmap with a hash collision, things fall over. + // probably needs a more solid resolution in the future but this will do for now. + AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString()); + AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0); } From a17eac3692441e5664bc98c31c6b1c71a5c8ece2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:27:08 +0900 Subject: [PATCH 3367/6909] Rename reload method to not mention beatmap unnecessarily --- osu.Game/Overlays/MusicController.cs | 2 +- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 66c9b15c0a..0764f34697 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -84,7 +84,7 @@ namespace osu.Game.Overlays /// /// Forcefully reload the current 's track from disk. /// - public void ForceReloadCurrentBeatmap() => changeTrack(); + public void ReloadCurrentTrack() => changeTrack(); /// /// Change the position of a in the current playlist. diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index e238e1fcf0..9c72268eea 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -178,7 +178,7 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.Value.Metadata.AudioFile = info.Name; - music.ForceReloadCurrentBeatmap(); + music.ReloadCurrentTrack(); editor.UpdateClockSource(); return true; From b1e72c311edc68d3c98136349b4597418ba5e6c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:28:41 +0900 Subject: [PATCH 3368/6909] Add null check because we can --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 9c72268eea..4d553756b8 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -180,7 +180,7 @@ namespace osu.Game.Screens.Edit.Setup music.ReloadCurrentTrack(); - editor.UpdateClockSource(); + editor?.UpdateClockSource(); return true; } From f047ff10bfcb616b236b864cd5ae663d74875100 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:30:05 +0900 Subject: [PATCH 3369/6909] Remove local specification for file selector search path --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 4d553756b8..edbe75448b 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -235,7 +235,7 @@ namespace osu.Game.Screens.Edit.Setup public void DisplayFileChooser() { - Target.Child = new FileSelector("/Users/Dean/.osu/Songs", new[] { ".mp3", ".ogg" }) + Target.Child = new FileSelector(validFileExtensions: new[] { ".mp3", ".ogg" }) { RelativeSizeAxes = Axes.X, Height = 400, From a890e5830d8e2af9a200fd170d91202bc87fd514 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:42:28 +0900 Subject: [PATCH 3370/6909] Add more file icons --- .../Graphics/UserInterfaceV2/FileSelector.cs | 28 ++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterfaceV2/FileSelector.cs b/osu.Game/Graphics/UserInterfaceV2/FileSelector.cs index 861d1887e1..e10b8f7033 100644 --- a/osu.Game/Graphics/UserInterfaceV2/FileSelector.cs +++ b/osu.Game/Graphics/UserInterfaceV2/FileSelector.cs @@ -62,7 +62,33 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override string FallbackName => file.Name; - protected override IconUsage? Icon => FontAwesome.Regular.FileAudio; + protected override IconUsage? Icon + { + get + { + switch (file.Extension) + { + case ".ogg": + case ".mp3": + case ".wav": + return FontAwesome.Regular.FileAudio; + + case ".jpg": + case ".jpeg": + case ".png": + return FontAwesome.Regular.FileImage; + + case ".mp4": + case ".avi": + case ".mov": + case ".flv": + return FontAwesome.Regular.FileVideo; + + default: + return FontAwesome.Regular.File; + } + } + } } } } From a8c85ed882f2e7c7424e86d4a79ac7a6e05387ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:42:37 +0900 Subject: [PATCH 3371/6909] Add test for filtered mode --- .../Visual/Settings/TestSceneFileSelector.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs index 08173148b0..311e4c3362 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneFileSelector.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.Framework.Allocation; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterfaceV2; @@ -9,10 +9,16 @@ namespace osu.Game.Tests.Visual.Settings { public class TestSceneFileSelector : OsuTestScene { - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestAllFiles() { - Add(new FileSelector { RelativeSizeAxes = Axes.Both }); + AddStep("create", () => Child = new FileSelector { RelativeSizeAxes = Axes.Both }); + } + + [Test] + public void TestJpgFilesOnly() + { + AddStep("create", () => Child = new FileSelector(validFileExtensions: new[] { ".jpg" }) { RelativeSizeAxes = Axes.Both }); } } } From c21745eb075e2f0e44c3d8ea5b314f1224234622 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:43:41 +0900 Subject: [PATCH 3372/6909] Fix missing HeadlessTest specification in new test --- osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs index 89e3b48aa3..33e3c7cb8c 100644 --- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs +++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs @@ -16,12 +16,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; using osu.Game.Tests.Visual; namespace osu.Game.Tests.Rulesets { + [HeadlessTest] public class TestSceneDrawableRulesetDependencies : OsuTestScene { [Test] From eff6af3111eba46adb782205eca4f2b7a40347c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 12:45:13 +0900 Subject: [PATCH 3373/6909] Add "bindables" to dictionary --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 29ca385275..64f3d41acb 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -909,6 +909,7 @@ private void load() True True True + True True True True From 50ba320a5118e3bcd6c5d40022ca2df22ac4ab89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 13:10:04 +0900 Subject: [PATCH 3374/6909] Expand available file operations in ArchiveModelManager --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 44 +++++++++++++++++++----- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index e9f41f6bff..b48ab6112e 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -260,7 +260,7 @@ namespace osu.Game.Beatmaps fileInfo.Filename = beatmapInfo.Path; stream.Seek(0, SeekOrigin.Begin); - UpdateFile(setInfo, fileInfo, stream); + ReplaceFile(setInfo, fileInfo, stream); } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 76bc4f7755..c39b71b058 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -401,29 +401,55 @@ namespace osu.Game.Database } /// - /// Update an existing file, or create a new entry if not already part of the 's files. + /// Replace an existing file with a new version. /// /// The item to operate on. - /// The file model to be updated or added. + /// The existing file to be replaced. /// The new file contents. - public void UpdateFile(TModel model, TFileModel file, Stream contents) + /// An optional filename for the new file. Will use the previous filename if not specified. + public void ReplaceFile(TModel model, TFileModel file, Stream contents, string filename = null) + { + using (ContextFactory.GetForWrite()) + { + DeleteFile(model, file); + AddFile(model, contents, filename ?? file.Filename); + } + } + + /// + /// Delete new file. + /// + /// The item to operate on. + /// The existing file to be deleted. + public void DeleteFile(TModel model, TFileModel file) { using (var usage = ContextFactory.GetForWrite()) { // Dereference the existing file info, since the file model will be removed. if (file.FileInfo != null) - { Files.Dereference(file.FileInfo); - // Remove the file model. - usage.Context.Set().Remove(file); - } + // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked + // Definitely can be removed once we rework the database backend. + usage.Context.Set().Remove(file); - // Add the new file info and containing file model. model.Files.Remove(file); + } + } + + /// + /// Add a new file. + /// + /// The item to operate on. + /// The new file contents. + /// The filename for the new file. + public void AddFile(TModel model, Stream contents, string filename) + { + using (ContextFactory.GetForWrite()) + { model.Files.Add(new TFileModel { - Filename = file.Filename, + Filename = filename, FileInfo = Files.Add(contents) }); From ea971ecb9008f2b09cee1fe546cf8a6ce4c65c81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 13:11:34 +0900 Subject: [PATCH 3375/6909] Remove local file handling from SetupScreen --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 34 ++++++++-------------- 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index edbe75448b..802f304835 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -2,6 +2,7 @@ // 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; @@ -17,10 +18,8 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.IO; using osu.Game.Overlays; using osuTK; -using FileInfo = System.IO.FileInfo; namespace osu.Game.Screens.Edit.Setup { @@ -34,10 +33,10 @@ namespace osu.Game.Screens.Edit.Setup private LabelledTextBox audioTrackTextBox; [Resolved] - private FileStore files { get; set; } + private MusicController music { get; set; } [Resolved] - private MusicController music { get; set; } + private BeatmapManager beatmaps { get; set; } [Resolved(canBeNull: true)] private Editor editor { get; set; } @@ -153,28 +152,19 @@ namespace osu.Game.Screens.Edit.Setup if (!info.Exists) return false; - var beatmapFiles = Beatmap.Value.BeatmapSetInfo.Files; + var set = Beatmap.Value.BeatmapSetInfo; - // remove the old file - var oldFile = beatmapFiles.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile); - - if (oldFile != null) - { - beatmapFiles.Remove(oldFile); - files.Dereference(oldFile.FileInfo); - } - - // add the new file - IO.FileInfo osuFileInfo; + // remove the previous audio track for now. + // in the future we probably want to check if this is being used elsewhere (other difficulties?) + var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile); using (var stream = info.OpenRead()) - osuFileInfo = files.Add(stream); - - beatmapFiles.Add(new BeatmapSetFileInfo { - FileInfo = osuFileInfo, - Filename = info.Name - }); + if (oldFile != null) + beatmaps.ReplaceFile(set, oldFile, stream, info.Name); + else + beatmaps.AddFile(set, stream, info.Name); + } Beatmap.Value.Metadata.AudioFile = info.Name; From 892d440ed0f337ac47ed192f6db3fdebfbfa5b19 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 13:19:07 +0900 Subject: [PATCH 3376/6909] Add fallback path for potential null ParentGameplayClock --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index b585a78f42..6716f828ed 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.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.Graphics; @@ -195,7 +196,7 @@ namespace osu.Game.Rulesets.UI { public GameplayClock ParentGameplayClock; - public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock.NonGameplayAdjustments; + public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); public StabilityGameplayClock(FramedClock underlyingClock) : base(underlyingClock) From 26ba7d3100ff77a91cf01ae4737f3c014b7fe5ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 13:20:19 +0900 Subject: [PATCH 3377/6909] Remove unused method (was moved to a more local location) --- osu.Game/Screens/Play/GameplayClockContainer.cs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 4094de1c4f..6679e56871 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -210,19 +210,6 @@ namespace osu.Game.Screens.Play base.Update(); } - private double getTrueGameplayRate() - { - double baseRate = track.Rate; - - if (speedAdjustmentsApplied) - { - baseRate /= UserPlaybackRate.Value; - baseRate /= pauseFreqAdjust.Value; - } - - return baseRate; - } - private bool speedAdjustmentsApplied; private void updateRate() From 325bfdbf7139a779341d7ecd48fb758b2de4556a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 13:25:42 +0900 Subject: [PATCH 3378/6909] Fix hard crash on hitting an out of range key (Q~P) --- 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 b81e0ce159..e2a49221c0 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Edit if (checkRightToggleFromKey(e.Key, out var rightIndex)) { - var item = togglesCollection.Children[rightIndex]; + var item = togglesCollection.ElementAtOrDefault(rightIndex); if (item is SettingsCheckbox checkbox) { From 3c191cfe257280d2ef89983435cb4b3f22fff3a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 14:08:47 +0900 Subject: [PATCH 3379/6909] Add basic xmldoc to HitObjectComposer --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b81e0ce159..0ea57ef4e1 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -31,6 +31,11 @@ using osuTK.Input; namespace osu.Game.Rulesets.Edit { + /// + /// Top level container for editor compose mode. + /// Responsible for providing snapping and generally gluing components together. + /// + /// The base type of supported objects. [Cached(Type = typeof(IPlacementHandler))] public abstract class HitObjectComposer : HitObjectComposer, IPlacementHandler where TObject : HitObject From bca774a0d4c16b8ca53ddd5dc7d19061dbf35971 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 14:09:31 +0900 Subject: [PATCH 3380/6909] Allow BlueprintContainer to specify toggles --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 4 ++-- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index e1cbfa93f6..2127385ab5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -41,10 +41,10 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" }; - protected override IEnumerable Toggles => new[] + protected override IEnumerable> Toggles => base.Toggles.Concat(new[] { distanceSnapToggle - }; + }); private BindableList selectedHitObjects; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 0ea57ef4e1..b2d7c40a22 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -170,7 +170,7 @@ namespace osu.Game.Rulesets.Edit /// A collection of toggles which will be displayed to the user. /// The display name will be decided by . /// - protected virtual IEnumerable Toggles => Enumerable.Empty(); + protected virtual IEnumerable> Toggles => BlueprintContainer.Toggles; /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. From e009264f1029fa336439f2a2d10d081b6b2d9c03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 14:10:30 +0900 Subject: [PATCH 3381/6909] Add new combo toggle to main composer interface --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 +- .../Compose/Components/BlueprintContainer.cs | 18 ++++---- .../Components/ComposeBlueprintContainer.cs | 41 +++++++++++++++++++ .../Compose/Components/SelectionHandler.cs | 2 + 4 files changed, 53 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 02d5955ae6..d986b71380 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Edit /// /// The that is being placed. /// - protected readonly HitObject HitObject; + public readonly HitObject HitObject; [Resolved(canBeNull: true)] protected EditorClock EditorClock { get; private set; } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index bf1e18771f..aa567dbdf4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private EditorClock editorClock { get; set; } [Resolved] - private EditorBeatmap beatmap { get; set; } + protected EditorBeatmap Beatmap { get; private set; } private readonly BindableList selectedHitObjects = new BindableList(); @@ -68,10 +68,10 @@ namespace osu.Game.Screens.Edit.Compose.Components DragBox.CreateProxy().With(p => p.Depth = float.MinValue) }); - foreach (var obj in beatmap.HitObjects) + foreach (var obj in Beatmap.HitObjects) AddBlueprintFor(obj); - selectedHitObjects.BindTo(beatmap.SelectedHitObjects); + selectedHitObjects.BindTo(Beatmap.SelectedHitObjects); selectedHitObjects.CollectionChanged += (selectedObjects, args) => { switch (args.Action) @@ -94,8 +94,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); - beatmap.HitObjectAdded += AddBlueprintFor; - beatmap.HitObjectRemoved += removeBlueprintFor; + Beatmap.HitObjectAdded += AddBlueprintFor; + Beatmap.HitObjectRemoved += removeBlueprintFor; } protected virtual Container CreateSelectionBlueprintContainer() => @@ -271,7 +271,7 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.Selected += onBlueprintSelected; blueprint.Deselected += onBlueprintDeselected; - if (beatmap.SelectedHitObjects.Contains(hitObject)) + if (Beatmap.SelectedHitObjects.Contains(hitObject)) blueprint.Select(); SelectionBlueprints.Add(blueprint); @@ -460,10 +460,10 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.Dispose(isDisposing); - if (beatmap != null) + if (Beatmap != null) { - beatmap.HitObjectAdded -= AddBlueprintFor; - beatmap.HitObjectRemoved -= removeBlueprintFor; + Beatmap.HitObjectAdded -= AddBlueprintFor; + Beatmap.HitObjectRemoved -= removeBlueprintFor; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index e1f311f1b8..2a7c52579f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -4,6 +4,7 @@ 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.Input; @@ -11,6 +12,7 @@ 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 osuTK; namespace osu.Game.Screens.Edit.Compose.Components @@ -54,8 +56,45 @@ namespace osu.Game.Screens.Edit.Compose.Components base.LoadComplete(); inputManager = GetContainingInputManager(); + + Beatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateTogglesFromSelection(); + + // the updated object may be in the selection + Beatmap.HitObjectUpdated += _ => updateTogglesFromSelection(); + + NewCombo.ValueChanged += combo => + { + if (Beatmap.SelectedHitObjects.Count > 0) + { + foreach (var h in Beatmap.SelectedHitObjects) + { + if (h is IHasComboInformation c) + { + c.NewCombo = combo.NewValue; + Beatmap.UpdateHitObject(h); + } + } + } + else if (currentPlacement != null) + { + // update placement object from toggle + if (currentPlacement.HitObject is IHasComboInformation c) + c.NewCombo = combo.NewValue; + } + }; } + private void updateTogglesFromSelection() => + NewCombo.Value = Beatmap.SelectedHitObjects.OfType().All(c => c.NewCombo); + + public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; + + public virtual IEnumerable> Toggles => new[] + { + //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. + NewCombo + }; + #region Placement /// @@ -86,7 +125,9 @@ namespace osu.Game.Screens.Edit.Compose.Components removePlacement(); if (currentPlacement != null) + { updatePlacementPosition(); + } } protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 6e2c8bd01c..ca22b443fb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -35,6 +35,8 @@ namespace osu.Game.Screens.Edit.Compose.Components public IEnumerable SelectedBlueprints => selectedBlueprints; private readonly List selectedBlueprints; + public int SelectedCount => selectedBlueprints.Count; + public IEnumerable SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject); private Drawable content; From a6adf8334eea25818c0c38ba7dc59ff7358ee910 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 14:19:35 +0900 Subject: [PATCH 3382/6909] Use existing method to update combo state of selection --- .../Compose/Components/BlueprintContainer.cs | 44 +++++++++---------- .../Components/ComposeBlueprintContainer.cs | 9 +--- .../Compose/Components/SelectionHandler.cs | 2 +- 3 files changed, 24 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index aa567dbdf4..fc37aa577c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { /// /// A container which provides a "blueprint" display of hitobjects. - /// Includes selection and manipulation support via a . + /// Includes selection and manipulation support via a . /// public abstract class BlueprintContainer : CompositeDrawable, IKeyBindingHandler { @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected Container SelectionBlueprints { get; private set; } - private SelectionHandler selectionHandler; + protected SelectionHandler SelectionHandler; [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -56,15 +56,15 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { - selectionHandler = CreateSelectionHandler(); - selectionHandler.DeselectAll = deselectAll; + SelectionHandler = CreateSelectionHandler(); + SelectionHandler.DeselectAll = deselectAll; AddRangeInternal(new[] { DragBox = CreateDragBox(selectBlueprintsFromDragRectangle), - selectionHandler, + SelectionHandler, SelectionBlueprints = CreateSelectionBlueprintContainer(), - selectionHandler.CreateProxy(), + SelectionHandler.CreateProxy(), DragBox.CreateProxy().With(p => p.Depth = float.MinValue) }); @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Edit.Compose.Components new Container { RelativeSizeAxes = Axes.Both }; /// - /// Creates a which outlines s and handles movement of selections. + /// Creates a which outlines s and handles movement of selections. /// protected virtual SelectionHandler CreateSelectionHandler() => new SelectionHandler(); @@ -130,7 +130,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; // store for double-click handling - clickedBlueprint = selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered); + clickedBlueprint = SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered); // Deselection should only occur if no selected blueprints are hovered // A special case for when a blueprint was selected via this click is added since OnClick() may occur outside the hitobject and should not trigger deselection @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Edit.Compose.Components return false; // ensure the blueprint which was hovered for the first click is still the hovered blueprint. - if (clickedBlueprint == null || selectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint) + if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint) return false; editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); @@ -208,7 +208,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (DragBox.State == Visibility.Visible) { DragBox.Hide(); - selectionHandler.UpdateVisibility(); + SelectionHandler.UpdateVisibility(); } } @@ -217,7 +217,7 @@ namespace osu.Game.Screens.Edit.Compose.Components switch (e.Key) { case Key.Escape: - if (!selectionHandler.SelectedBlueprints.Any()) + if (!SelectionHandler.SelectedBlueprints.Any()) return false; deselectAll(); @@ -298,14 +298,14 @@ namespace osu.Game.Screens.Edit.Compose.Components bool allowDeselection = e.ControlPressed && e.Button == MouseButton.Left; // Todo: This is probably incorrectly disallowing multiple selections on stacked objects - if (!allowDeselection && selectionHandler.SelectedBlueprints.Any(s => s.IsHovered)) + if (!allowDeselection && SelectionHandler.SelectedBlueprints.Any(s => s.IsHovered)) return; foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) { if (blueprint.IsHovered) { - selectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); + SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); clickSelectionBegan = true; break; } @@ -358,23 +358,23 @@ namespace osu.Game.Screens.Edit.Compose.Components private void selectAll() { SelectionBlueprints.ToList().ForEach(m => m.Select()); - selectionHandler.UpdateVisibility(); + SelectionHandler.UpdateVisibility(); } /// /// Deselects all selected s. /// - private void deselectAll() => selectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect()); + private void deselectAll() => SelectionHandler.SelectedBlueprints.ToList().ForEach(m => m.Deselect()); private void onBlueprintSelected(SelectionBlueprint blueprint) { - selectionHandler.HandleSelected(blueprint); + SelectionHandler.HandleSelected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 1); } private void onBlueprintDeselected(SelectionBlueprint blueprint) { - selectionHandler.HandleDeselected(blueprint); + SelectionHandler.HandleDeselected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 0); } @@ -391,16 +391,16 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private void prepareSelectionMovement() { - if (!selectionHandler.SelectedBlueprints.Any()) + if (!SelectionHandler.SelectedBlueprints.Any()) return; // Any selected blueprint that is hovered can begin the movement of the group, however only the earliest hitobject is used for movement // A special case is added for when a click selection occurred before the drag - if (!clickSelectionBegan && !selectionHandler.SelectedBlueprints.Any(b => b.IsHovered)) + if (!clickSelectionBegan && !SelectionHandler.SelectedBlueprints.Any(b => b.IsHovered)) return; // 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(); + movementBlueprint = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).First(); movementBlueprintOriginalPosition = movementBlueprint.ScreenSpaceSelectionPoint; // todo: unsure if correct } @@ -425,14 +425,14 @@ namespace osu.Game.Screens.Edit.Compose.Components var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); // Move the hitobjects. - if (!selectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition))) + if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition))) return true; 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) + foreach (HitObject obj in SelectionHandler.SelectedHitObjects) obj.StartTime += offset; } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 2a7c52579f..6f66c1bd6f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -66,14 +66,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { if (Beatmap.SelectedHitObjects.Count > 0) { - foreach (var h in Beatmap.SelectedHitObjects) - { - if (h is IHasComboInformation c) - { - c.NewCombo = combo.NewValue; - Beatmap.UpdateHitObject(h); - } - } + SelectionHandler.SetNewCombo(combo.NewValue); } else if (currentPlacement != null) { diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index ca22b443fb..1c5c3179ca 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -285,7 +285,7 @@ namespace osu.Game.Screens.Edit.Compose.Components var comboInfo = h as IHasComboInformation; if (comboInfo == null) - throw new InvalidOperationException($"Tried to change combo state of a {h.GetType()}, which doesn't implement {nameof(IHasComboInformation)}"); + continue; comboInfo.NewCombo = state; EditorBeatmap?.UpdateHitObject(h); From 7f9a5f5f0df1bceb5c8f1d3d1aedf2be9d6acadf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 14:25:24 +0900 Subject: [PATCH 3383/6909] Ensure setup screen text boxes commit on losing focus --- osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 290aba3468..902c23c3c6 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -46,6 +46,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 protected override OsuTextBox CreateComponent() => new OsuTextBox { + CommitOnFocusLost = true, Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.X, From 50290f3cb4730ca93a1ed388ce432241520bd8e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 15:09:47 +0900 Subject: [PATCH 3384/6909] Rework ternary states to fix context menus not updating after already displayed --- .../Compose/Components/SelectionHandler.cs | 170 ++++++++++-------- 1 file changed, 93 insertions(+), 77 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 6e2c8bd01c..1c564c6605 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -4,7 +4,9 @@ using System; using System.Collections.Generic; using System.Linq; +using Humanizer; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; @@ -59,6 +61,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load(OsuColour colours) { + createStateBindables(); + InternalChild = content = new Container { Children = new Drawable[] @@ -308,6 +312,90 @@ namespace osu.Game.Screens.Edit.Compose.Components #endregion + #region Selection State + + private readonly Bindable selectionNewComboState = new Bindable(); + + private readonly Dictionary> selectionSampleStates = new Dictionary>(); + + /// + /// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions) + /// + private void createStateBindables() + { + // hit samples + var sampleTypes = new[] { HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_FINISH }; + + foreach (var sampleName in sampleTypes) + { + var bindable = new Bindable + { + Description = sampleName.Replace("hit", string.Empty).Titleize() + }; + + bindable.ValueChanged += state => + { + switch (state.NewValue) + { + case TernaryState.False: + RemoveHitSample(sampleName); + break; + + case TernaryState.True: + AddHitSample(sampleName); + break; + } + }; + + selectionSampleStates[sampleName] = bindable; + } + + // new combo + selectionNewComboState.ValueChanged += state => + { + switch (state.NewValue) + { + case TernaryState.False: + SetNewCombo(false); + break; + + case TernaryState.True: + SetNewCombo(true); + break; + } + }; + + // bring in updates from selection changes + EditorBeatmap.HitObjectUpdated += _ => updateTernaryStates(); + EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => updateTernaryStates(); + } + + private void updateTernaryStates() + { + selectionNewComboState.Value = getStateFromBlueprints(selectedBlueprints.Select(b => (IHasComboInformation)b.HitObject).Count(h => h.NewCombo)); + + foreach (var (sampleName, bindable) in selectionSampleStates) + { + bindable.Value = getStateFromBlueprints(SelectedHitObjects.Count(h => h.Samples.Any(s => s.Name == sampleName))); + } + } + + /// + /// Given a count of "true" blueprints, retrieve the correct ternary display state. + /// + private TernaryState getStateFromBlueprints(int count) + { + if (count == 0) + return TernaryState.False; + + if (count < SelectedHitObjects.Count()) + return TernaryState.Indeterminate; + + return TernaryState.True; + } + + #endregion + #region Context Menu public MenuItem[] ContextMenuItems @@ -322,7 +410,9 @@ namespace osu.Game.Screens.Edit.Compose.Components items.AddRange(GetContextMenuItemsForSelection(selectedBlueprints)); if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation)) - items.Add(createNewComboMenuItem()); + { + items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = selectionNewComboState } }); + } if (selectedBlueprints.Count == 1) items.AddRange(selectedBlueprints[0].ContextMenuItems); @@ -331,12 +421,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { new OsuMenuItem("Sound") { - Items = new[] - { - createHitSampleMenuItem("Whistle", HitSampleInfo.HIT_WHISTLE), - createHitSampleMenuItem("Clap", HitSampleInfo.HIT_CLAP), - createHitSampleMenuItem("Finish", HitSampleInfo.HIT_FINISH) - } + Items = selectionSampleStates.Select(kvp => + new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray() }, new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected), }); @@ -353,76 +439,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected virtual IEnumerable GetContextMenuItemsForSelection(IEnumerable selection) => Enumerable.Empty(); - private MenuItem createNewComboMenuItem() - { - return new TernaryStateMenuItem("New combo", MenuItemType.Standard, setNewComboState) - { - State = { Value = getHitSampleState() } - }; - - void setNewComboState(TernaryState state) - { - switch (state) - { - case TernaryState.False: - SetNewCombo(false); - break; - - case TernaryState.True: - SetNewCombo(true); - break; - } - } - - TernaryState getHitSampleState() - { - int countExisting = selectedBlueprints.Select(b => (IHasComboInformation)b.HitObject).Count(h => h.NewCombo); - - if (countExisting == 0) - return TernaryState.False; - - if (countExisting < SelectedHitObjects.Count()) - return TernaryState.Indeterminate; - - return TernaryState.True; - } - } - - private MenuItem createHitSampleMenuItem(string name, string sampleName) - { - return new TernaryStateMenuItem(name, MenuItemType.Standard, setHitSampleState) - { - State = { Value = getHitSampleState() } - }; - - void setHitSampleState(TernaryState state) - { - switch (state) - { - case TernaryState.False: - RemoveHitSample(sampleName); - break; - - case TernaryState.True: - AddHitSample(sampleName); - break; - } - } - - TernaryState getHitSampleState() - { - int countExisting = SelectedHitObjects.Count(h => h.Samples.Any(s => s.Name == sampleName)); - - if (countExisting == 0) - return TernaryState.False; - - if (countExisting < SelectedHitObjects.Count()) - return TernaryState.Indeterminate; - - return TernaryState.True; - } - } - #endregion } } From a859fe78ee52841fd458f4949e2b1e72d3c59a1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 15:27:45 +0900 Subject: [PATCH 3385/6909] Expose update ternary state method and use better state determination function --- .../Compose/Components/SelectionHandler.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 1c564c6605..8a152a9c57 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -366,32 +366,32 @@ namespace osu.Game.Screens.Edit.Compose.Components }; // bring in updates from selection changes - EditorBeatmap.HitObjectUpdated += _ => updateTernaryStates(); - EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => updateTernaryStates(); + EditorBeatmap.HitObjectUpdated += _ => UpdateTernaryStates(); + EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => UpdateTernaryStates(); } - private void updateTernaryStates() + /// + /// Called when context menu ternary states may need to be recalculated (selection changed or hitobject updated). + /// + protected virtual void UpdateTernaryStates() { - selectionNewComboState.Value = getStateFromBlueprints(selectedBlueprints.Select(b => (IHasComboInformation)b.HitObject).Count(h => h.NewCombo)); + selectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.NewCombo); foreach (var (sampleName, bindable) in selectionSampleStates) { - bindable.Value = getStateFromBlueprints(SelectedHitObjects.Count(h => h.Samples.Any(s => s.Name == sampleName))); + bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName)); } } /// - /// Given a count of "true" blueprints, retrieve the correct ternary display state. + /// Given a selection target and a function of truth, retrieve the correct ternary state for display. /// - private TernaryState getStateFromBlueprints(int count) + protected TernaryState GetStateFromSelection(IEnumerable selection, Func func) { - if (count == 0) - return TernaryState.False; + if (selection.Any(func)) + return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate; - if (count < SelectedHitObjects.Count()) - return TernaryState.Indeterminate; - - return TernaryState.True; + return TernaryState.False; } #endregion From 727ab98d22d2b748f1148f8bcc1f7cb27b41f538 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 15:32:45 +0900 Subject: [PATCH 3386/6909] Update taiko selection handler with new logic --- .../Edit/TaikoSelectionHandler.cs | 128 +++++++++--------- 1 file changed, 67 insertions(+), 61 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index 40565048c2..a3ecf7ed95 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -1,9 +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.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; @@ -14,75 +15,80 @@ namespace osu.Game.Rulesets.Taiko.Edit { public class TaikoSelectionHandler : SelectionHandler { + private readonly Bindable selectionRimState = new Bindable(); + private readonly Bindable selectionStrongState = new Bindable(); + + [BackgroundDependencyLoader] + private void load() + { + selectionStrongState.ValueChanged += state => + { + switch (state.NewValue) + { + case TernaryState.False: + SetStrongState(false); + break; + + case TernaryState.True: + SetStrongState(true); + break; + } + }; + + selectionRimState.ValueChanged += state => + { + switch (state.NewValue) + { + case TernaryState.False: + SetRimState(false); + break; + + case TernaryState.True: + SetRimState(true); + break; + } + }; + } + + public void SetStrongState(bool state) + { + var hits = SelectedHitObjects.OfType(); + + ChangeHandler.BeginChange(); + + foreach (var h in hits) + h.IsStrong = state; + + ChangeHandler.EndChange(); + } + + public void SetRimState(bool state) + { + var hits = SelectedHitObjects.OfType(); + + ChangeHandler.BeginChange(); + + foreach (var h in hits) + h.Type = state ? HitType.Rim : HitType.Centre; + + ChangeHandler.EndChange(); + } + 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 => - { - ChangeHandler.BeginChange(); - - foreach (var h in hits) - { - switch (state) - { - case TernaryState.True: - h.Type = HitType.Rim; - break; - - case TernaryState.False: - h.Type = HitType.Centre; - break; - } - } - - ChangeHandler.EndChange(); - }) - { - State = { Value = getTernaryState(hits, h => h.Type == HitType.Rim) } - }; - } + yield return new TernaryStateMenuItem("Rim") { State = { BindTarget = selectionRimState } }; if (selection.All(s => s.HitObject is TaikoHitObject)) - { - var hits = selection.Select(s => s.HitObject).OfType(); - - yield return new TernaryStateMenuItem("Strong", action: state => - { - ChangeHandler.BeginChange(); - - foreach (var h in hits) - { - switch (state) - { - case TernaryState.True: - h.IsStrong = true; - break; - - case TernaryState.False: - h.IsStrong = false; - break; - } - - EditorBeatmap?.UpdateHitObject(h); - } - - ChangeHandler.EndChange(); - }) - { - State = { Value = getTernaryState(hits, h => h.IsStrong) } - }; - } + yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } }; } - private TernaryState getTernaryState(IEnumerable selection, Func func) + protected override void UpdateTernaryStates() { - if (selection.Any(func)) - return selection.All(func) ? TernaryState.True : TernaryState.Indeterminate; + base.UpdateTernaryStates(); - return TernaryState.False; + selectionRimState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.Type == HitType.Rim); + selectionStrongState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.IsStrong); } } } From ae68dcd962090efe418228f4c11c09d166a12af1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 16:33:22 +0900 Subject: [PATCH 3387/6909] Add ternary toggle buttons to editor toolbox selection --- .../Edit/OsuHitObjectComposer.cs | 11 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 14 +-- .../TernaryButtons/DrawableTernaryButton.cs | 112 ++++++++++++++++++ .../TernaryButtons/TernaryButton.cs | 44 +++++++ .../Components/ComposeBlueprintContainer.cs | 29 ++--- .../Compose/Components/SelectionHandler.cs | 22 ++-- 6 files changed, 194 insertions(+), 38 deletions(-) create mode 100644 osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs create mode 100644 osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 0d0a139a8a..49af80dd63 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -9,7 +9,9 @@ using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; 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; @@ -17,6 +19,7 @@ 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.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -39,11 +42,11 @@ namespace osu.Game.Rulesets.Osu.Edit new SpinnerCompositionTool() }; - private readonly BindableBool distanceSnapToggle = new BindableBool(true) { Description = "Distance Snap" }; + private readonly Bindable distanceSnapToggle = new Bindable(); - protected override IEnumerable> Toggles => base.Toggles.Concat(new[] + protected override IEnumerable Toggles => base.Toggles.Concat(new[] { - distanceSnapToggle + new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) }); private BindableList selectedHitObjects; @@ -156,7 +159,7 @@ namespace osu.Game.Rulesets.Osu.Edit distanceSnapGridCache.Invalidate(); distanceSnapGrid = null; - if (!distanceSnapToggle.Value) + if (distanceSnapToggle.Value != TernaryState.True) return; switch (BlueprintContainer.CurrentTool) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b2d7c40a22..afef542e36 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -14,7 +14,6 @@ using osu.Framework.Input.Events; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Configuration; using osu.Game.Rulesets.Edit.Tools; using osu.Game.Rulesets.Mods; @@ -24,6 +23,7 @@ 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.Components.TernaryButtons; using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -124,11 +124,7 @@ namespace osu.Game.Rulesets.Edit new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }, togglesCollection = new ToolboxGroup("toggles") { - ChildrenEnumerable = Toggles.Select(b => new SettingsCheckbox - { - Bindable = b, - LabelText = b?.Description ?? "unknown" - }) + ChildrenEnumerable = Toggles.Select(b => new DrawableTernaryButton(b)) } } }, @@ -170,7 +166,7 @@ namespace osu.Game.Rulesets.Edit /// A collection of toggles which will be displayed to the user. /// The display name will be decided by . /// - protected virtual IEnumerable> Toggles => BlueprintContainer.Toggles; + protected virtual IEnumerable Toggles => BlueprintContainer.Toggles; /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. @@ -212,9 +208,9 @@ namespace osu.Game.Rulesets.Edit { var item = togglesCollection.Children[rightIndex]; - if (item is SettingsCheckbox checkbox) + if (item is DrawableTernaryButton button) { - checkbox.Bindable.Value = !checkbox.Bindable.Value; + button.Button.Toggle(); return true; } } diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs new file mode 100644 index 0000000000..c72fff5c91 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/DrawableTernaryButton.cs @@ -0,0 +1,112 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Components.TernaryButtons +{ + internal class DrawableTernaryButton : TriangleButton + { + private Color4 defaultBackgroundColour; + private Color4 defaultBubbleColour; + private Color4 selectedBackgroundColour; + private Color4 selectedBubbleColour; + + private Drawable icon; + + public readonly TernaryButton Button; + + public DrawableTernaryButton(TernaryButton button) + { + Button = button; + + Text = button.Description; + + RelativeSizeAxes = Axes.X; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + defaultBackgroundColour = colours.Gray3; + defaultBubbleColour = defaultBackgroundColour.Darken(0.5f); + selectedBackgroundColour = colours.BlueDark; + selectedBubbleColour = selectedBackgroundColour.Lighten(0.5f); + + Triangles.Alpha = 0; + + Content.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, + Radius = 2, + Offset = new Vector2(0, 1), + Colour = Color4.Black.Opacity(0.5f) + }; + + Add(icon = (Button.CreateIcon?.Invoke() ?? new Circle()).With(b => + { + b.Blending = BlendingParameters.Additive; + b.Anchor = Anchor.CentreLeft; + b.Origin = Anchor.CentreLeft; + b.Size = new Vector2(20); + b.X = 10; + })); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Button.Bindable.BindValueChanged(selected => updateSelectionState(), true); + + Action = onAction; + } + + private void onAction() + { + Button.Toggle(); + } + + private void updateSelectionState() + { + if (!IsLoaded) + return; + + switch (Button.Bindable.Value) + { + case TernaryState.Indeterminate: + icon.Colour = selectedBubbleColour.Darken(0.5f); + BackgroundColour = selectedBackgroundColour.Darken(0.5f); + break; + + case TernaryState.False: + icon.Colour = defaultBubbleColour; + BackgroundColour = defaultBackgroundColour; + break; + + case TernaryState.True: + icon.Colour = selectedBubbleColour; + BackgroundColour = selectedBackgroundColour; + break; + } + } + + protected override SpriteText CreateText() => new OsuSpriteText + { + Depth = -1, + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + X = 40f + }; + } +} diff --git a/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs b/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs new file mode 100644 index 0000000000..7f64695bde --- /dev/null +++ b/osu.Game/Screens/Edit/Components/TernaryButtons/TernaryButton.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit.Components.TernaryButtons +{ + public class TernaryButton + { + public readonly Bindable Bindable; + + public readonly string Description; + + /// + /// A function which creates a drawable icon to represent this item. If null, a sane default should be used. + /// + public readonly Func CreateIcon; + + public TernaryButton(Bindable bindable, string description, Func createIcon = null) + { + Bindable = bindable; + Description = description; + CreateIcon = createIcon; + } + + public void Toggle() + { + switch (Bindable.Value) + { + case TernaryState.False: + case TernaryState.Indeterminate: + Bindable.Value = TernaryState.True; + break; + + case TernaryState.True: + Bindable.Value = TernaryState.False; + break; + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 6f66c1bd6f..91eab18acb 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -7,12 +7,16 @@ 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; +using osu.Game.Graphics; +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.Screens.Edit.Components.TernaryButtons; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components @@ -57,35 +61,26 @@ namespace osu.Game.Screens.Edit.Compose.Components inputManager = GetContainingInputManager(); - Beatmap.SelectedHitObjects.CollectionChanged += (_, __) => updateTogglesFromSelection(); - - // the updated object may be in the selection - Beatmap.HitObjectUpdated += _ => updateTogglesFromSelection(); + // updates to selected are handled for us by SelectionHandler. + NewCombo.BindTo(SelectionHandler.SelectionNewComboState); + // we are responsible for current placement blueprint updated based on state changes. NewCombo.ValueChanged += combo => { - if (Beatmap.SelectedHitObjects.Count > 0) + if (currentPlacement != null) { - SelectionHandler.SetNewCombo(combo.NewValue); - } - else if (currentPlacement != null) - { - // update placement object from toggle if (currentPlacement.HitObject is IHasComboInformation c) - c.NewCombo = combo.NewValue; + c.NewCombo = combo.NewValue == TernaryState.True; } }; } - private void updateTogglesFromSelection() => - NewCombo.Value = Beatmap.SelectedHitObjects.OfType().All(c => c.NewCombo); + public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; - public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; - - public virtual IEnumerable> Toggles => new[] + public virtual IEnumerable Toggles => new[] { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. - NewCombo + new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle }) }; #region Placement diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index a316f34ad0..ed956357b6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -316,9 +316,15 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection State - private readonly Bindable selectionNewComboState = new Bindable(); + /// + /// The state of "new combo" for all selected hitobjects. + /// + public readonly Bindable SelectionNewComboState = new Bindable(); - private readonly Dictionary> selectionSampleStates = new Dictionary>(); + /// + /// The state of each sample type for all selected hitobjects. Keys match with constant specifications. + /// + public readonly Dictionary> SelectionSampleStates = new Dictionary>(); /// /// Set up ternary state bindables and bind them to selection/hitobject changes (in both directions) @@ -349,11 +355,11 @@ namespace osu.Game.Screens.Edit.Compose.Components } }; - selectionSampleStates[sampleName] = bindable; + SelectionSampleStates[sampleName] = bindable; } // new combo - selectionNewComboState.ValueChanged += state => + SelectionNewComboState.ValueChanged += state => { switch (state.NewValue) { @@ -377,9 +383,9 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected virtual void UpdateTernaryStates() { - selectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.NewCombo); + SelectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.NewCombo); - foreach (var (sampleName, bindable) in selectionSampleStates) + foreach (var (sampleName, bindable) in SelectionSampleStates) { bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName)); } @@ -413,7 +419,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (selectedBlueprints.All(b => b.HitObject is IHasComboInformation)) { - items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = selectionNewComboState } }); + items.Add(new TernaryStateMenuItem("New combo") { State = { BindTarget = SelectionNewComboState } }); } if (selectedBlueprints.Count == 1) @@ -423,7 +429,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { new OsuMenuItem("Sound") { - Items = selectionSampleStates.Select(kvp => + Items = SelectionSampleStates.Select(kvp => new TernaryStateMenuItem(kvp.Value.Description) { State = { BindTarget = kvp.Value } }).ToArray() }, new OsuMenuItem("Delete", MenuItemType.Destructive, deleteSelected), From a6298c60eb30d5c7f0bf5dc8422148f531c315ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 16:44:37 +0900 Subject: [PATCH 3388/6909] Fix button spacing --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index afef542e36..f823d37060 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Edit private RadioButtonCollection toolboxCollection; - private ToolboxGroup togglesCollection; + private FillFlowContainer togglesCollection; protected HitObjectComposer(Ruleset ruleset) { @@ -121,10 +121,20 @@ namespace osu.Game.Rulesets.Edit Spacing = new Vector2(10), Children = new Drawable[] { - new ToolboxGroup("toolbox") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }, - togglesCollection = new ToolboxGroup("toggles") + new ToolboxGroup("toolbox") { - ChildrenEnumerable = Toggles.Select(b => new DrawableTernaryButton(b)) + Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } + }, + new ToolboxGroup("toggles") + { + Child = togglesCollection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + ChildrenEnumerable = Toggles.Select(b => new DrawableTernaryButton(b)) + }, } } }, From da820c815e5fc97a350669e9904d2700c25fbe44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 16:46:06 +0900 Subject: [PATCH 3389/6909] Add shortcut keys to toolbox gorup titles --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index f823d37060..a592500a87 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -121,11 +121,11 @@ namespace osu.Game.Rulesets.Edit Spacing = new Vector2(10), Children = new Drawable[] { - new ToolboxGroup("toolbox") + new ToolboxGroup("toolbox (1-9)") { Child = toolboxCollection = new RadioButtonCollection { RelativeSizeAxes = Axes.X } }, - new ToolboxGroup("toggles") + new ToolboxGroup("toggles (Q~P)") { Child = togglesCollection = new FillFlowContainer { From b70a20e7f198b236b4b404cb3eea54dca3ae105e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 16:56:39 +0900 Subject: [PATCH 3390/6909] Avoid consuming keystrokes in editor when a modifier key is held down --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index b81e0ce159..7c9d996039 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -192,6 +192,9 @@ namespace osu.Game.Rulesets.Edit protected override bool OnKeyDown(KeyDownEvent e) { + if (e.ControlPressed || e.AltPressed || e.SuperPressed) + return false; + if (checkLeftToggleFromKey(e.Key, out var leftIndex)) { var item = toolboxCollection.Items.ElementAtOrDefault(leftIndex); From 98c6027352deb80fd0d80e9bc5abcb12ead00552 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 17:07:58 +0900 Subject: [PATCH 3391/6909] Remove unused using --- .../Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 91eab18acb..cab277f10b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; From b8e9f19b92195f8c6e270540c9d77f8a17b5e2c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 17:30:31 +0900 Subject: [PATCH 3392/6909] Move common HitSampleInfo lookup to static method --- osu.Game/Audio/HitSampleInfo.cs | 5 +++++ osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 5 +---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index f6b0107bd2..8b1f5a366a 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -17,6 +17,11 @@ namespace osu.Game.Audio public const string HIT_NORMAL = @"hitnormal"; public const string HIT_CLAP = @"hitclap"; + /// + /// All valid sample addition constants. + /// + public static IEnumerable AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH }; + /// /// The bank to load the sample from. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index ed956357b6..6ca85fe026 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -331,10 +331,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private void createStateBindables() { - // hit samples - var sampleTypes = new[] { HitSampleInfo.HIT_WHISTLE, HitSampleInfo.HIT_CLAP, HitSampleInfo.HIT_FINISH }; - - foreach (var sampleName in sampleTypes) + foreach (var sampleName in HitSampleInfo.AllAdditions) { var bindable = new Bindable { From 51cc644b7b203110f88b7bee33cd6321ab1fb66b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 17:42:49 +0900 Subject: [PATCH 3393/6909] Fix set access to SelectionHandler Co-authored-by: Dan Balasescu --- osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 8934a1b6d3..8908520cd7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public Container SelectionBlueprints { get; private set; } - protected SelectionHandler SelectionHandler; + protected SelectionHandler SelectionHandler { get; private set; } [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } From 22511c36c3b0d306418b55adbb61376796f85187 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 17:40:43 +0900 Subject: [PATCH 3394/6909] Ensure toggles are not instantiated more than once for safety --- .../Edit/OsuHitObjectComposer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 8 ++++++-- .../Components/ComposeBlueprintContainer.cs | 19 +++++++++++-------- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 49af80dd63..a4dd463ea5 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Bindable distanceSnapToggle = new Bindable(); - protected override IEnumerable Toggles => base.Toggles.Concat(new[] + protected override IEnumerable CreateToggles() => base.CreateToggles().Concat(new[] { new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) }); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index a592500a87..e692f8d606 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -133,7 +133,6 @@ namespace osu.Game.Rulesets.Edit AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 5), - ChildrenEnumerable = Toggles.Select(b => new DrawableTernaryButton(b)) }, } } @@ -145,6 +144,9 @@ namespace osu.Game.Rulesets.Edit .Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon)) .ToList(); + Toggles = CreateToggles().ToArray(); + togglesCollection.AddRange(Toggles.Select(b => new DrawableTernaryButton(b))); + setSelectTool(); EditorBeatmap.SelectedHitObjects.CollectionChanged += selectionChanged; @@ -176,7 +178,9 @@ namespace osu.Game.Rulesets.Edit /// A collection of toggles which will be displayed to the user. /// The display name will be decided by . /// - protected virtual IEnumerable Toggles => BlueprintContainer.Toggles; + public TernaryButton[] Toggles { get; private set; } + + protected virtual IEnumerable CreateToggles() => BlueprintContainer.Toggles; /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index cab277f10b..0f4bab8b33 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -51,6 +51,8 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { + Toggles = CreateToggles().ToArray(); + AddInternal(placementBlueprintContainer); } @@ -66,21 +68,22 @@ namespace osu.Game.Screens.Edit.Compose.Components // we are responsible for current placement blueprint updated based on state changes. NewCombo.ValueChanged += combo => { - if (currentPlacement != null) - { - if (currentPlacement.HitObject is IHasComboInformation c) - c.NewCombo = combo.NewValue == TernaryState.True; - } + if (currentPlacement == null) return; + + if (currentPlacement.HitObject is IHasComboInformation c) + c.NewCombo = combo.NewValue == TernaryState.True; }; } public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; - public virtual IEnumerable Toggles => new[] + public TernaryButton[] Toggles { get; private set; } + + protected virtual IEnumerable CreateToggles() { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. - new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle }) - }; + yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle }); + } #region Placement From 346d14d40bfc09d0da125c0850dafe587eccb376 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 17:45:19 +0900 Subject: [PATCH 3395/6909] Rename variables to match --- .../Edit/OsuHitObjectComposer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 15 ++++++++------- .../Components/ComposeBlueprintContainer.cs | 12 +++++++++--- 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index a4dd463ea5..912a705d16 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit private readonly Bindable distanceSnapToggle = new Bindable(); - protected override IEnumerable CreateToggles() => base.CreateToggles().Concat(new[] + protected override IEnumerable CreateTernaryButtons() => base.CreateTernaryButtons().Concat(new[] { new TernaryButton(distanceSnapToggle, "Distance Snap", () => new SpriteIcon { Icon = FontAwesome.Solid.Ruler }) }); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index e692f8d606..3ad2394420 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -144,8 +143,8 @@ namespace osu.Game.Rulesets.Edit .Select(t => new RadioButton(t.Name, () => toolSelected(t), t.CreateIcon)) .ToList(); - Toggles = CreateToggles().ToArray(); - togglesCollection.AddRange(Toggles.Select(b => new DrawableTernaryButton(b))); + TernaryStates = CreateTernaryButtons().ToArray(); + togglesCollection.AddRange(TernaryStates.Select(b => new DrawableTernaryButton(b))); setSelectTool(); @@ -175,12 +174,14 @@ namespace osu.Game.Rulesets.Edit protected abstract IReadOnlyList CompositionTools { get; } /// - /// A collection of toggles which will be displayed to the user. - /// The display name will be decided by . + /// A collection of states which will be displayed to the user in the toolbox. /// - public TernaryButton[] Toggles { get; private set; } + public TernaryButton[] TernaryStates { get; private set; } - protected virtual IEnumerable CreateToggles() => BlueprintContainer.Toggles; + /// + /// Create all ternary states required to be displayed to the user. + /// + protected virtual IEnumerable CreateTernaryButtons() => BlueprintContainer.TernaryStates; /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 0f4bab8b33..88c3170c34 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Compose.Components [BackgroundDependencyLoader] private void load() { - Toggles = CreateToggles().ToArray(); + TernaryStates = CreateTernaryButtons().ToArray(); AddInternal(placementBlueprintContainer); } @@ -77,9 +77,15 @@ namespace osu.Game.Screens.Edit.Compose.Components public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; - public TernaryButton[] Toggles { get; private set; } + /// + /// A collection of states which will be displayed to the user in the toolbox. + /// + public TernaryButton[] TernaryStates { get; private set; } - protected virtual IEnumerable CreateToggles() + /// + /// Create all ternary states required to be displayed to the user. + /// + protected virtual IEnumerable CreateTernaryButtons() { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle }); From b561429f920d52c53e5ae33f1efbd1e1e9f0f1be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 17:53:41 +0900 Subject: [PATCH 3396/6909] Add toolbar toggle buttons for hit samples --- .../Components/ComposeBlueprintContainer.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 88c3170c34..a83977c15f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -3,12 +3,14 @@ using System.Collections.Generic; using System.Linq; +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; +using osu.Game.Audio; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Edit.Tools; @@ -73,6 +75,34 @@ namespace osu.Game.Screens.Edit.Compose.Components if (currentPlacement.HitObject is IHasComboInformation c) c.NewCombo = combo.NewValue == TernaryState.True; }; + + // we own SelectionHandler so don't need to worry about making bindable copies (for simplicity) + foreach (var kvp in SelectionHandler.SelectionSampleStates) + { + kvp.Value.BindValueChanged(c => sampleChanged(kvp.Key, c.NewValue)); + } + } + + private void sampleChanged(string sampleName, TernaryState state) + { + if (currentPlacement == null) return; + + var samples = currentPlacement.HitObject.Samples; + + var existingSample = samples.FirstOrDefault(s => s.Name == sampleName); + + switch (state) + { + case TernaryState.False: + if (existingSample != null) + samples.Remove(existingSample); + break; + + case TernaryState.True: + if (existingSample == null) + samples.Add(new HitSampleInfo { Name = sampleName }); + break; + } } public readonly Bindable NewCombo = new Bindable { Description = "New Combo" }; @@ -89,6 +119,26 @@ namespace osu.Game.Screens.Edit.Compose.Components { //TODO: this should only be enabled (visible?) for rulesets that provide combo-supporting HitObjects. yield return new TernaryButton(NewCombo, "New combo", () => new SpriteIcon { Icon = FontAwesome.Regular.DotCircle }); + + foreach (var kvp in SelectionHandler.SelectionSampleStates) + yield return new TernaryButton(kvp.Value, kvp.Key.Replace("hit", string.Empty).Titleize(), () => getIconForSample(kvp.Key)); + } + + private Drawable getIconForSample(string sampleName) + { + switch (sampleName) + { + case HitSampleInfo.HIT_CLAP: + return new SpriteIcon { Icon = FontAwesome.Solid.Hands }; + + case HitSampleInfo.HIT_WHISTLE: + return new SpriteIcon { Icon = FontAwesome.Solid.Bullhorn }; + + case HitSampleInfo.HIT_FINISH: + return new SpriteIcon { Icon = FontAwesome.Solid.DrumSteelpan }; + } + + return null; } #region Placement From dbfa05d3b34339ecb139de74a4c3c2aa24f48342 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 18:00:17 +0900 Subject: [PATCH 3397/6909] Fix placement object not getting updated with initial state --- .../Components/ComposeBlueprintContainer.cs | 30 ++++++++++++++----- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index a83977c15f..81d7fa4b32 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -68,21 +68,31 @@ namespace osu.Game.Screens.Edit.Compose.Components NewCombo.BindTo(SelectionHandler.SelectionNewComboState); // we are responsible for current placement blueprint updated based on state changes. - NewCombo.ValueChanged += combo => - { - if (currentPlacement == null) return; - - if (currentPlacement.HitObject is IHasComboInformation c) - c.NewCombo = combo.NewValue == TernaryState.True; - }; + NewCombo.ValueChanged += _ => updatePlacementNewCombo(); // we own SelectionHandler so don't need to worry about making bindable copies (for simplicity) foreach (var kvp in SelectionHandler.SelectionSampleStates) { - kvp.Value.BindValueChanged(c => sampleChanged(kvp.Key, c.NewValue)); + kvp.Value.BindValueChanged(_ => updatePlacementSamples()); } } + private void updatePlacementNewCombo() + { + if (currentPlacement == null) return; + + if (currentPlacement.HitObject is IHasComboInformation c) + c.NewCombo = NewCombo.Value == TernaryState.True; + } + + private void updatePlacementSamples() + { + if (currentPlacement == null) return; + + foreach (var kvp in SelectionHandler.SelectionSampleStates) + sampleChanged(kvp.Key, kvp.Value.Value); + } + private void sampleChanged(string sampleName, TernaryState state) { if (currentPlacement == null) return; @@ -206,6 +216,10 @@ namespace osu.Game.Screens.Edit.Compose.Components // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame updatePlacementPosition(); + + updatePlacementSamples(); + + updatePlacementNewCombo(); } } From 4cc02abc76be15a30aeb82427aa04a80de2c696a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 18:11:49 +0900 Subject: [PATCH 3398/6909] 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 d701aaf199..dc3e14c141 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 71826e161c..6412f707d0 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 90aa903318..f1e13169a5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 59bfa086847f8b89874a88b2257c5bd105eb9bd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 18:26:54 +0900 Subject: [PATCH 3399/6909] Forcefully re-apply DrawableHitObject state transforms on post-load DefaultsApplied --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 28d3a39096..7c05bc9aa7 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -186,6 +186,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onDefaultsApplied(HitObject hitObject) { apply(hitObject); + updateState(state.Value, true); DefaultsApplied?.Invoke(this); } From 8b255f457971745411f896764889edc92a650a98 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 18:40:20 +0900 Subject: [PATCH 3400/6909] Fix test failures The issue was the ArchiveModelManager change; the test local change is just there because it makes more sense to run for every test in that scene. --- .../Visual/Editing/TestSceneEditorBeatmapCreation.cs | 8 ++++---- osu.Game/Database/ArchiveModelManager.cs | 8 +++++--- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index ceacbd51a2..720cf51f2c 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -27,15 +27,15 @@ namespace osu.Game.Tests.Visual.Editing AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)); base.SetUpSteps(); + + // if we save a beatmap with a hash collision, things fall over. + // probably needs a more solid resolution in the future but this will do for now. + AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString()); } [Test] public void TestCreateNewBeatmap() { - // if we save a beatmap with a hash collision, things fall over. - // probably needs a more solid resolution in the future but this will do for now. - AddStep("make new beatmap unique", () => EditorBeatmap.Metadata.Title = Guid.NewGuid().ToString()); - AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0); } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index c39b71b058..bbe2604216 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -427,11 +427,13 @@ namespace osu.Game.Database { // Dereference the existing file info, since the file model will be removed. if (file.FileInfo != null) + { Files.Dereference(file.FileInfo); - // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked - // Definitely can be removed once we rework the database backend. - usage.Context.Set().Remove(file); + // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked + // Definitely can be removed once we rework the database backend. + usage.Context.Set().Remove(file); + } model.Files.Remove(file); } From c41fb67e730cba4dc9c8b706a511acf430b3938a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Sep 2020 18:48:04 +0900 Subject: [PATCH 3401/6909] Move all ruleset editor tests to their own namespace --- .../{ => Editor}/ManiaPlacementBlueprintTestScene.cs | 2 +- .../{ => Editor}/ManiaSelectionBlueprintTestScene.cs | 2 +- osu.Game.Rulesets.Mania.Tests/{ => Editor}/TestSceneEditor.cs | 2 +- .../{ => Editor}/TestSceneHoldNotePlacementBlueprint.cs | 2 +- .../{ => Editor}/TestSceneHoldNoteSelectionBlueprint.cs | 2 +- .../{ => Editor}/TestSceneManiaBeatSnapGrid.cs | 2 +- .../{ => Editor}/TestSceneManiaHitObjectComposer.cs | 2 +- .../{ => Editor}/TestSceneNotePlacementBlueprint.cs | 2 +- .../{ => Editor}/TestSceneNoteSelectionBlueprint.cs | 2 +- .../{ => Editor}/TestSceneHitCirclePlacementBlueprint.cs | 2 +- .../{ => Editor}/TestSceneHitCircleSelectionBlueprint.cs | 2 +- .../{ => Editor}/TestSceneOsuDistanceSnapGrid.cs | 2 +- .../{ => Editor}/TestScenePathControlPointVisualiser.cs | 2 +- .../{ => Editor}/TestSceneSliderPlacementBlueprint.cs | 2 +- .../{ => Editor}/TestSceneSliderSelectionBlueprint.cs | 2 +- .../{ => Editor}/TestSceneSpinnerPlacementBlueprint.cs | 2 +- .../{ => Editor}/TestSceneSpinnerSelectionBlueprint.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/{ => Editor}/TestSceneEditor.cs | 2 +- .../{ => Editor}/TestSceneTaikoHitObjectComposer.cs | 2 +- 19 files changed, 19 insertions(+), 19 deletions(-) rename osu.Game.Rulesets.Mania.Tests/{ => Editor}/ManiaPlacementBlueprintTestScene.cs (97%) rename osu.Game.Rulesets.Mania.Tests/{ => Editor}/ManiaSelectionBlueprintTestScene.cs (95%) rename osu.Game.Rulesets.Mania.Tests/{ => Editor}/TestSceneEditor.cs (96%) rename osu.Game.Rulesets.Mania.Tests/{ => Editor}/TestSceneHoldNotePlacementBlueprint.cs (93%) rename osu.Game.Rulesets.Mania.Tests/{ => Editor}/TestSceneHoldNoteSelectionBlueprint.cs (97%) rename osu.Game.Rulesets.Mania.Tests/{ => Editor}/TestSceneManiaBeatSnapGrid.cs (98%) rename osu.Game.Rulesets.Mania.Tests/{ => Editor}/TestSceneManiaHitObjectComposer.cs (99%) rename osu.Game.Rulesets.Mania.Tests/{ => Editor}/TestSceneNotePlacementBlueprint.cs (97%) rename osu.Game.Rulesets.Mania.Tests/{ => Editor}/TestSceneNoteSelectionBlueprint.cs (96%) rename osu.Game.Rulesets.Osu.Tests/{ => Editor}/TestSceneHitCirclePlacementBlueprint.cs (94%) rename osu.Game.Rulesets.Osu.Tests/{ => Editor}/TestSceneHitCircleSelectionBlueprint.cs (98%) rename osu.Game.Rulesets.Osu.Tests/{ => Editor}/TestSceneOsuDistanceSnapGrid.cs (99%) rename osu.Game.Rulesets.Osu.Tests/{ => Editor}/TestScenePathControlPointVisualiser.cs (97%) rename osu.Game.Rulesets.Osu.Tests/{ => Editor}/TestSceneSliderPlacementBlueprint.cs (99%) rename osu.Game.Rulesets.Osu.Tests/{ => Editor}/TestSceneSliderSelectionBlueprint.cs (99%) rename osu.Game.Rulesets.Osu.Tests/{ => Editor}/TestSceneSpinnerPlacementBlueprint.cs (94%) rename osu.Game.Rulesets.Osu.Tests/{ => Editor}/TestSceneSpinnerSelectionBlueprint.cs (96%) rename osu.Game.Rulesets.Taiko.Tests/{ => Editor}/TestSceneEditor.cs (88%) rename osu.Game.Rulesets.Taiko.Tests/{ => Editor}/TestSceneTaikoHitObjectComposer.cs (97%) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs similarity index 97% rename from osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs index 0fe4a3c669..ece523e84c 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaPlacementBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaPlacementBlueprintTestScene.cs @@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public abstract class ManiaPlacementBlueprintTestScene : PlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs similarity index 95% rename from osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs index 149f6582ab..176fbba921 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaSelectionBlueprintTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/ManiaSelectionBlueprintTestScene.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.UI; using osu.Game.Tests.Visual; using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public abstract class ManiaSelectionBlueprintTestScene : SelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs similarity index 96% rename from osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs index 3b9c03b86a..d3afbc63eb 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneEditor.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.UI; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { [TestFixture] public class TestSceneEditor : EditorTestScene diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs similarity index 93% rename from osu.Game.Rulesets.Mania.Tests/TestSceneHoldNotePlacementBlueprint.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs index b4332264b9..87c74a12cf 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNotePlacementBlueprint.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneHoldNotePlacementBlueprint : ManiaPlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs similarity index 97% rename from osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs index 90394f3d1b..24f4c6858e 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneHoldNoteSelectionBlueprint.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneHoldNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs similarity index 98% rename from osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 639be0bc11..654b752001 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -20,7 +20,7 @@ using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneManiaBeatSnapGrid : EditorClockTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs similarity index 99% rename from osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index 1a3fa29d4a..c9551ee79e 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -23,7 +23,7 @@ using osu.Game.Tests.Visual; using osuTK; using osuTK.Input; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneManiaHitObjectComposer : EditorClockTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs similarity index 97% rename from osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs index 2d97e61aa5..36c34a8fb9 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNotePlacementBlueprint.cs @@ -18,7 +18,7 @@ using osu.Game.Tests.Visual; using osuTK; using osuTK.Input; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneNotePlacementBlueprint : ManiaPlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs similarity index 96% rename from osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs rename to osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs index 1514bdf0bd..0e47a12a8e 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNoteSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneNoteSelectionBlueprint.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Mania.Tests +namespace osu.Game.Rulesets.Mania.Tests.Editor { public class TestSceneNoteSelectionBlueprint : ManiaSelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs similarity index 94% rename from osu.Game.Rulesets.Osu.Tests/TestSceneHitCirclePlacementBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs index 4c6abc45f7..7bccec6c97 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCirclePlacementBlueprint.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneHitCirclePlacementBlueprint : PlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs similarity index 98% rename from osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs index 0ecce42e88..66cd405195 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneHitCircleSelectionBlueprint.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneHitCircleSelectionBlueprint : SelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs similarity index 99% rename from osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 0d0be2953b..1232369a0b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -19,7 +19,7 @@ using osu.Game.Tests.Visual; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneOsuDistanceSnapGrid : OsuManualInputManagerTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs similarity index 97% rename from osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs index 21fa283b6d..738a21b17e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestScenePathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestScenePathControlPointVisualiser.cs @@ -12,7 +12,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.Editor { public class TestScenePathControlPointVisualiser : OsuTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs similarity index 99% rename from osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index fe9973f4d8..49d7d9249c 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -14,7 +14,7 @@ using osu.Game.Tests.Visual; using osuTK; using osuTK.Input; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneSliderPlacementBlueprint : PlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs similarity index 99% rename from osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index d5be538d94..f6e1be693b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -16,7 +16,7 @@ using osu.Game.Tests.Visual; using osuTK; using osuTK.Input; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneSliderSelectionBlueprint : SelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs similarity index 94% rename from osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerPlacementBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs index d74d072857..fa6c660b01 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerPlacementBlueprint.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneSpinnerPlacementBlueprint : PlacementBlueprintTestScene { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs similarity index 96% rename from osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs index 011463ab14..4248f68a60 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSpinnerSelectionBlueprint.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Tests.Visual; using osuTK; -namespace osu.Game.Rulesets.Osu.Tests +namespace osu.Game.Rulesets.Osu.Tests.Editor { public class TestSceneSpinnerSelectionBlueprint : SelectionBlueprintTestScene { diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs similarity index 88% rename from osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs rename to osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs index 411fe08bcf..e3c1613bd9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneEditor.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneEditor.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Taiko.Tests +namespace osu.Game.Rulesets.Taiko.Tests.Editor { [TestFixture] public class TestSceneEditor : EditorTestScene diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs similarity index 97% rename from osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs rename to osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs index 34d5fdf857..626537053a 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneTaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Editor/TestSceneTaikoHitObjectComposer.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.Taiko.Objects; using osu.Game.Screens.Edit; using osu.Game.Tests.Visual; -namespace osu.Game.Rulesets.Taiko.Tests +namespace osu.Game.Rulesets.Taiko.Tests.Editor { public class TestSceneTaikoHitObjectComposer : EditorClockTestScene { From acfa62bb50e669cdcfdc45f24f67339df7799eba Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 19:25:58 +0900 Subject: [PATCH 3402/6909] Fix potential taiko crash on rewind --- osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs | 13 +++++++++---- .../DrawableTestStrongHit.cs | 15 +++------------ .../Skinning/TestSceneHitExplosion.cs | 10 ++++------ osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs | 12 +++++++++--- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 11 +++++------ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 8 ++++---- 6 files changed, 34 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index 1db07b3244..3ffc6187b7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -2,26 +2,31 @@ // 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.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 + public class DrawableTestHit : DrawableHit { - private readonly HitResult type; + public readonly HitResult Type; public DrawableTestHit(Hit hit, HitResult type = HitResult.Great) : base(hit) { - this.type = type; + Type = type; + + // in order to create nested strong hit + HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } [BackgroundDependencyLoader] private void load() { - Result.Type = type; + Result.Type = Type; } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs index 7cb984b254..829bcf34a1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestStrongHit.cs @@ -2,17 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -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; namespace osu.Game.Rulesets.Taiko.Tests { - public class DrawableTestStrongHit : DrawableHit + public class DrawableTestStrongHit : DrawableTestHit { - private readonly HitResult type; private readonly bool hitBoth; public DrawableTestStrongHit(double startTime, HitResult type = HitResult.Great, bool hitBoth = true) @@ -20,12 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { IsStrong = true, StartTime = startTime, - }) + }, type) { - // in order to create nested strong hit - HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - this.type = type; this.hitBoth = hitBoth; } @@ -33,10 +26,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { base.LoadAsyncComplete(); - Result.Type = type; - var nestedStrongHit = (DrawableStrongNestedHit)NestedHitObjects.Single(); - nestedStrongHit.Result.Type = hitBoth ? type : HitResult.Miss; + nestedStrongHit.Result.Type = hitBoth ? Type : HitResult.Miss; } 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 48969e0f5a..19cc56527e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics; 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.UI; namespace osu.Game.Rulesets.Taiko.Tests.Skinning @@ -29,7 +28,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth)))); } - private Drawable getContentFor(DrawableTaikoHitObject hit) + private Drawable getContentFor(DrawableTestHit hit) { return new Container { @@ -37,7 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning Children = new Drawable[] { hit, - new HitExplosion(hit) + new HitExplosion(hit, hit.Type) { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -46,9 +45,8 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning }; } - private DrawableTaikoHitObject createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type); + private DrawableTestHit createHit(HitResult type) => new DrawableTestHit(new Hit { StartTime = Time.Current }, type); - private DrawableTaikoHitObject createStrongHit(HitResult type, bool hitBoth) - => new DrawableTestStrongHit(Time.Current, type, hitBoth); + private DrawableTestHit createStrongHit(HitResult type, bool hitBoth) => new DrawableTestStrongHit(Time.Current, type, hitBoth); } } diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs index 9943a58e3e..7b8ab89233 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs @@ -15,8 +15,14 @@ namespace osu.Game.Rulesets.Taiko.UI { internal class DefaultHitExplosion : CircularContainer { - [Resolved] - private DrawableHitObject judgedObject { get; set; } + private readonly DrawableHitObject judgedObject; + private readonly HitResult result; + + public DefaultHitExplosion(DrawableHitObject judgedObject, HitResult result) + { + this.judgedObject = judgedObject; + this.result = result; + } [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -31,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI Alpha = 0.15f; Masking = true; - if (judgedObject.Result.Type == HitResult.Miss) + if (result == HitResult.Miss) return; bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index e3eabbf88f..16300d5715 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -22,8 +22,8 @@ namespace osu.Game.Rulesets.Taiko.UI { public override bool RemoveWhenNotAlive => true; - [Cached(typeof(DrawableHitObject))] public readonly DrawableHitObject JudgedObject; + private readonly HitResult result; private SkinnableDrawable skinnable; @@ -31,9 +31,10 @@ namespace osu.Game.Rulesets.Taiko.UI public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd; - public HitExplosion(DrawableHitObject judgedObject) + public HitExplosion(DrawableHitObject judgedObject, HitResult result) { JudgedObject = judgedObject; + this.result = result; Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -47,14 +48,12 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion()); + Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion(JudgedObject, result)); } private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject) { - var resultType = judgedObject.Result?.Type ?? HitResult.Great; - - switch (resultType) + switch (result) { case HitResult.Miss: return TaikoSkinComponents.TaikoExplosionMiss; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 7976d5bc6d..7b3fbb1faf 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -9,6 +9,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Rulesets.Taiko.Objects.Drawables; @@ -206,8 +207,7 @@ namespace osu.Game.Rulesets.Taiko.UI }); var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre; - - addExplosion(judgedObject, type); + addExplosion(judgedObject, result.Type, type); break; } } @@ -219,9 +219,9 @@ namespace osu.Game.Rulesets.Taiko.UI /// As legacy skins have different explosions for singular and double strong hits, /// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame. /// - private void addExplosion(DrawableHitObject drawableObject, HitType type) => Schedule(() => + private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) => Schedule(() => { - hitExplosionContainer.Add(new HitExplosion(drawableObject)); + hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); }); From 480eeb5fbee83ce1ac7eb5c28fb55e71b78c4640 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 19:37:34 +0900 Subject: [PATCH 3403/6909] Add back caching --- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index 16300d5715..efd1b25046 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -22,7 +22,9 @@ namespace osu.Game.Rulesets.Taiko.UI { public override bool RemoveWhenNotAlive => true; + [Cached(typeof(DrawableHitObject))] public readonly DrawableHitObject JudgedObject; + private readonly HitResult result; private SkinnableDrawable skinnable; From 0853f0e128dc14a880bc8d7ae0c2553fd67b4b9f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 19:38:23 +0900 Subject: [PATCH 3404/6909] Remove comment --- osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index 3ffc6187b7..e0af973b53 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -19,7 +19,6 @@ namespace osu.Game.Rulesets.Taiko.Tests { Type = type; - // in order to create nested strong hit HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } From 1c4baa4e2adf718272cede63b8ae0393a3b77ef3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 20:11:27 +0900 Subject: [PATCH 3405/6909] Add bonus hit results and orderings --- osu.Game/Rulesets/Scoring/HitResult.cs | 103 ++++++++++++++++++++++++- 1 file changed, 102 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index b057af2a50..370ffb3a7f 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -2,17 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel; +using osu.Game.Utils; namespace osu.Game.Rulesets.Scoring { + [HasOrderedElements] public enum HitResult { /// /// Indicates that the object has not been judged yet. /// [Description(@"")] + [Order(13)] None, + [Order(12)] + Ignore, + /// /// Indicates that the object has been judged as a miss. /// @@ -21,47 +27,142 @@ namespace osu.Game.Rulesets.Scoring /// "too far in the future). It should also define when a forced miss should be triggered (as a result of no user input in time). /// [Description(@"Miss")] + [Order(5)] Miss, [Description(@"Meh")] + [Order(4)] Meh, /// /// Optional judgement. /// [Description(@"OK")] + [Order(3)] Ok, [Description(@"Good")] + [Order(2)] Good, [Description(@"Great")] + [Order(1)] Great, /// /// Optional judgement. /// [Description(@"Perfect")] + [Order(0)] Perfect, /// /// Indicates small tick miss. /// + [Order(11)] SmallTickMiss, /// /// Indicates a small tick hit. /// + [Description(@"S Tick")] + [Order(7)] SmallTickHit, /// /// Indicates a large tick miss. /// + [Order(10)] LargeTickMiss, /// /// Indicates a large tick hit. /// - LargeTickHit + [Description(@"L Tick")] + [Order(6)] + LargeTickHit, + + /// + /// Indicates a small bonus. + /// + [Description("S Bonus")] + [Order(9)] + SmallBonus, + + /// + /// Indicate a large bonus. + /// + [Description("L Bonus")] + [Order(8)] + LargeBonus, + } + + public static class HitResultExtensions + { + /// + /// Whether a affects the combo. + /// + public static bool AffectsCombo(this HitResult result) + { + switch (result) + { + case HitResult.Miss: + case HitResult.Meh: + case HitResult.Ok: + case HitResult.Good: + case HitResult.Great: + case HitResult.Perfect: + case HitResult.LargeTickHit: + case HitResult.LargeTickMiss: + return true; + + default: + return false; + } + } + + /// + /// Whether a should be counted as combo score. + /// + /// + /// This is not the reciprocal of , as and do not affect combo + /// but are still considered as part of the accuracy (not bonus) portion of the score. + /// + public static bool IsBonus(this HitResult result) + { + switch (result) + { + case HitResult.SmallBonus: + case HitResult.LargeBonus: + return true; + + default: + return false; + } + } + + /// + /// Whether a represents a successful hit. + /// + public static bool IsHit(this HitResult result) + { + switch (result) + { + case HitResult.None: + case HitResult.Ignore: + case HitResult.Miss: + case HitResult.SmallTickMiss: + case HitResult.LargeTickMiss: + return false; + + default: + return true; + } + } + + /// + /// Whether a is scorable. + /// + public static bool IsScorable(this HitResult result) => result > HitResult.Ignore; } } From a07597c3691ef68583269ef5e2b41487e64458b8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 20:22:59 +0900 Subject: [PATCH 3406/6909] Adjust displays to use new results/orderings --- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 12 +++-- .../Scores/TopScoreStatisticsSection.cs | 6 +-- osu.Game/Scoring/ScoreInfo.cs | 45 ++++++++++++++++++- .../ContractedPanelMiddleContent.cs | 6 ++- .../Expanded/ExpandedPanelMiddleContent.cs | 5 ++- .../Expanded/Statistics/CounterStatistic.cs | 34 +++++++++++++- .../Expanded/Statistics/HitResultStatistic.cs | 4 +- osu.Game/Tests/TestScoreInfo.cs | 6 +++ 8 files changed, 99 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 56866765b6..edf04dc55a 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -92,10 +92,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores new TableColumn("max combo", Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 70, maxSize: 120)) }; - foreach (var statistic in score.SortedStatistics.Take(score.SortedStatistics.Count() - 1)) - columns.Add(new TableColumn(statistic.Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); - - columns.Add(new TableColumn(score.SortedStatistics.LastOrDefault().Key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 45, maxSize: 95))); + foreach (var (key, _, _) in score.GetStatisticsForDisplay()) + columns.Add(new TableColumn(key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); if (showPerformancePoints) columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); @@ -148,13 +146,13 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } }; - foreach (var kvp in score.SortedStatistics) + foreach (var (_, value, maxCount) in score.GetStatisticsForDisplay()) { content.Add(new OsuSpriteText { - Text = $"{kvp.Value}", + Text = maxCount == null ? $"{value}" : $"{value}/{maxCount}", Font = OsuFont.GetFont(size: text_size), - Colour = kvp.Value == 0 ? Color4.Gray : Color4.White + Colour = value == 0 ? Color4.Gray : Color4.White }); } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 3a842d0a43..05789e1fc0 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores 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)); + statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(s => createStatisticsColumn(s.result, s.count, s.maxCount)); modsColumn.Mods = value.Mods; if (scoreManager != null) @@ -125,9 +125,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } } - private TextColumn createStatisticsColumn(HitResult hitResult, int count) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width) + private TextColumn createStatisticsColumn(HitResult hitResult, int count, int? maxCount) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width) { - Text = count.ToString() + Text = maxCount == null ? $"{count}" : $"{count}/{maxCount}" }; private class InfoColumn : CompositeDrawable diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index efcf1737c9..4ed3f92e25 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -7,6 +7,7 @@ using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Converters; +using osu.Framework.Extensions; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Rulesets; @@ -147,8 +148,6 @@ namespace osu.Game.Scoring [JsonProperty("statistics")] public Dictionary Statistics = new Dictionary(); - public IOrderedEnumerable> SortedStatistics => Statistics.OrderByDescending(pair => pair.Key); - [JsonIgnore] [Column("Statistics")] public string StatisticsJson @@ -186,6 +185,48 @@ namespace osu.Game.Scoring [JsonProperty("position")] public int? Position { get; set; } + public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay() + { + foreach (var key in OrderAttributeUtils.GetValuesInOrder()) + { + if (key.IsBonus()) + continue; + + int value = Statistics.GetOrDefault(key); + + switch (key) + { + case HitResult.SmallTickHit: + { + int total = value + Statistics.GetOrDefault(HitResult.SmallTickMiss); + if (total > 0) + yield return (key, value, total); + + break; + } + + case HitResult.LargeTickHit: + { + int total = value + Statistics.GetOrDefault(HitResult.LargeTickMiss); + if (total > 0) + yield return (key, value, total); + + break; + } + + case HitResult.SmallTickMiss: + case HitResult.LargeTickMiss: + break; + + default: + if (value > 0 || key == HitResult.Miss) + yield return (key, value, null); + + break; + } + } + } + [Serializable] protected class DeserializedMod : IMod { diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 0b85eeafa8..95ece1a9fb 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Users; @@ -116,7 +117,7 @@ namespace osu.Game.Screens.Ranking.Contracted AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 5), - ChildrenEnumerable = score.SortedStatistics.Select(s => createStatistic(s.Key.GetDescription(), s.Value.ToString())) + ChildrenEnumerable = score.GetStatisticsForDisplay().Select(s => createStatistic(s.result, s.count, s.maxCount)) }, new FillFlowContainer { @@ -198,6 +199,9 @@ namespace osu.Game.Screens.Ranking.Contracted }; } + private Drawable createStatistic(HitResult result, int count, int? maxCount) + => createStatistic(result.GetDescription(), maxCount == null ? $"{count}" : $"{count}/{maxCount}"); + private Drawable createStatistic(string key, string value) => new Container { RelativeSizeAxes = Axes.X, diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 0033cd1f43..ebab8c88f6 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -65,8 +65,9 @@ namespace osu.Game.Screens.Ranking.Expanded }; var bottomStatistics = new List(); - foreach (var stat in score.SortedStatistics) - bottomStatistics.Add(new HitResultStatistic(stat.Key, stat.Value)); + + foreach (var (key, value, maxCount) in score.GetStatisticsForDisplay()) + bottomStatistics.Add(new HitResultStatistic(key, value, maxCount)); statisticDisplays.AddRange(topStatistics); statisticDisplays.AddRange(bottomStatistics); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index e820831809..fc01f5e9c4 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -2,6 +2,7 @@ // 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.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -16,6 +17,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public class CounterStatistic : StatisticDisplay { private readonly int count; + private readonly int? maxCount; private RollingCounter counter; @@ -24,10 +26,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics /// /// The name of the statistic. /// The value to display. - public CounterStatistic(string header, int count) + /// The maximum value of . Not displayed if null. + public CounterStatistic(string header, int count, int? maxCount = null) : base(header) { this.count = count; + this.maxCount = maxCount; } public override void Appear() @@ -36,7 +40,33 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics counter.Current.Value = count; } - protected override Drawable CreateContent() => counter = new Counter(); + protected override Drawable CreateContent() + { + var container = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Child = counter = new Counter + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + } + }; + + if (maxCount != null) + { + container.Add(new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.Torus.With(size: 12, fixedWidth: true), + Spacing = new Vector2(-2, 0), + Text = $"/{maxCount}" + }); + } + + return container; + } private class Counter : RollingCounter { diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs index faa4a6a96c..a86033713f 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs @@ -12,8 +12,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { private readonly HitResult result; - public HitResultStatistic(HitResult result, int count) - : base(result.GetDescription(), count) + public HitResultStatistic(HitResult result, int count, int? maxCount = null) + : base(result.GetDescription(), count, maxCount) { this.result = result; } diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs index 31cced6ce4..704d01e479 100644 --- a/osu.Game/Tests/TestScoreInfo.cs +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -37,6 +37,12 @@ namespace osu.Game.Tests Statistics[HitResult.Meh] = 50; Statistics[HitResult.Good] = 100; Statistics[HitResult.Great] = 300; + Statistics[HitResult.SmallTickHit] = 50; + Statistics[HitResult.SmallTickMiss] = 25; + Statistics[HitResult.LargeTickHit] = 100; + Statistics[HitResult.LargeTickMiss] = 50; + Statistics[HitResult.SmallBonus] = 10; + Statistics[HitResult.SmallBonus] = 50; Position = 1; } From 2517fffb7e61b4bad9f0f9da41a834d1c42a14d8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 20:48:16 +0900 Subject: [PATCH 3407/6909] Fix incorrect display in beatmap overlay table --- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 36 +++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index edf04dc55a..968355c377 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -11,9 +11,11 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Users.Drawables; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -55,6 +57,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores highAccuracyColour = colours.GreenLight; } + /// + /// The statistics that appear in the table, in order of appearance. + /// + private readonly List statisticResultTypes = new List(); + private bool showPerformancePoints; public void DisplayScores(IReadOnlyList scores, bool showPerformanceColumn) @@ -65,11 +72,12 @@ namespace osu.Game.Overlays.BeatmapSet.Scores return; showPerformancePoints = showPerformanceColumn; + statisticResultTypes.Clear(); for (int i = 0; i < scores.Count; i++) backgroundFlow.Add(new ScoreTableRowBackground(i, scores[i], row_height)); - Columns = createHeaders(scores.FirstOrDefault()); + Columns = createHeaders(scores); Content = scores.Select((s, i) => createContent(i, s)).ToArray().ToRectangular(); } @@ -79,7 +87,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores backgroundFlow.Clear(); } - private TableColumn[] createHeaders(ScoreInfo score) + private TableColumn[] createHeaders(IReadOnlyList scores) { var columns = new List { @@ -92,8 +100,17 @@ namespace osu.Game.Overlays.BeatmapSet.Scores new TableColumn("max combo", Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 70, maxSize: 120)) }; - foreach (var (key, _, _) in score.GetStatisticsForDisplay()) - columns.Add(new TableColumn(key.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); + // All statistics across all scores, unordered. + var allScoreStatistics = scores.SelectMany(s => s.GetStatisticsForDisplay().Select(stat => stat.result)).ToHashSet(); + + foreach (var result in OrderAttributeUtils.GetValuesInOrder()) + { + if (!allScoreStatistics.Contains(result)) + continue; + + columns.Add(new TableColumn(result.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); + statisticResultTypes.Add(result); + } if (showPerformancePoints) columns.Add(new TableColumn("pp", Anchor.CentreLeft, new Dimension(GridSizeMode.Absolute, 30))); @@ -146,13 +163,18 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } }; - foreach (var (_, value, maxCount) in score.GetStatisticsForDisplay()) + var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.result); + + foreach (var result in statisticResultTypes) { + if (!availableStatistics.TryGetValue(result, out var stat)) + stat = (result, 0, null); + content.Add(new OsuSpriteText { - Text = maxCount == null ? $"{value}" : $"{value}/{maxCount}", + Text = stat.maxCount == null ? $"{stat.count}" : $"{stat.count}/{stat.maxCount}", Font = OsuFont.GetFont(size: text_size), - Colour = value == 0 ? Color4.Gray : Color4.White + Colour = stat.count == 0 ? Color4.Gray : Color4.White }); } From 4bcc3ca82883588cd152a1d8efa58e5a60711b76 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 22:16:14 +0900 Subject: [PATCH 3408/6909] Add AffectsAccuracy extension --- osu.Game/Rulesets/Scoring/HitResult.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 370ffb3a7f..1de62cf8e5 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Scoring public static class HitResultExtensions { /// - /// Whether a affects the combo. + /// Whether a increases/decreases the combo, and affects the combo portion of the score. /// public static bool AffectsCombo(this HitResult result) { @@ -122,12 +122,14 @@ namespace osu.Game.Rulesets.Scoring } /// - /// Whether a should be counted as combo score. + /// Whether a affects the accuracy portion of the score. + /// + public static bool AffectsAccuracy(this HitResult result) + => IsScorable(result) && !IsBonus(result); + + /// + /// Whether a should be counted as bonus score. /// - /// - /// This is not the reciprocal of , as and do not affect combo - /// but are still considered as part of the accuracy (not bonus) portion of the score. - /// public static bool IsBonus(this HitResult result) { switch (result) From 9a24346a008ed8c730bea94adb9271324c5a79a7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 25 Sep 2020 23:29:40 +0900 Subject: [PATCH 3409/6909] Fix HP drain edgecase potentially causing insta-fails --- .../TestSceneDrainingHealthProcessor.cs | 39 +++++++++++++++++++ .../Scoring/DrainingHealthProcessor.cs | 2 +- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 460ad1b898..4876d051aa 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -1,15 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Threading; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Framework.Testing; using osu.Framework.Timing; 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.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Visual; @@ -175,6 +178,24 @@ namespace osu.Game.Tests.Gameplay assertHealthNotEqualTo(0); } + [Test] + public void TestSingleLongObjectDoesNotDrain() + { + var beatmap = new Beatmap + { + HitObjects = { new JudgeableLongHitObject() } + }; + + beatmap.HitObjects[0].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + createProcessor(beatmap); + setTime(0); + assertHealthEqualTo(1); + + setTime(5000); + assertHealthEqualTo(1); + } + private Beatmap createBeatmap(double startTime, double endTime, params BreakPeriod[] breaks) { var beatmap = new Beatmap @@ -235,5 +256,23 @@ namespace osu.Game.Tests.Gameplay } } } + + private class JudgeableLongHitObject : JudgeableHitObject, IHasDuration + { + public double EndTime => StartTime + Duration; + public double Duration { get; set; } = 5000; + + public JudgeableLongHitObject() + : base(false) + { + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + base.CreateNestedHitObjects(cancellationToken); + + AddNested(new JudgeableHitObject()); + } + } } } diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 130907b242..ba9bbb055f 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -133,7 +133,7 @@ namespace osu.Game.Rulesets.Scoring private double computeDrainRate() { - if (healthIncreases.Count == 0) + if (healthIncreases.Count <= 1) return 0; int adjustment = 1; From 4d743f64f52ec3ce7123ccab545f5e1445b69db1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 23 Sep 2020 21:15:41 +0200 Subject: [PATCH 3410/6909] Add a method to calculate asynchronously performance on a beatmap. --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index e9d26683c3..d9fb6ccd81 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -16,6 +16,7 @@ using osu.Framework.Lists; using osu.Framework.Threading; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Scoring; namespace osu.Game.Beatmaps { @@ -114,6 +115,28 @@ namespace osu.Game.Beatmaps return computeDifficulty(key, beatmapInfo, rulesetInfo); } + /// + /// Calculates performance for the given on a given . + /// + /// The to do the calculation on. + /// The score to do the calculation on. + /// An optional to cancel the operation. + /// + public async Task CalculateScorePerformance([NotNull] WorkingBeatmap beatmap, [NotNull] ScoreInfo score, CancellationToken token = default) + { + return await Task.Factory.StartNew(() => + { + if (token.IsCancellationRequested) + return default; + + var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(beatmap, score); + var total = calculator.Calculate(null); + + return total; + + }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + } + private CancellationTokenSource trackedUpdateCancellationSource; private readonly List linkedCancellationSources = new List(); From 77a9d92f4221a21b75e597696a796244e2c899c8 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 25 Sep 2020 19:15:40 +0200 Subject: [PATCH 3411/6909] Add dynamic pp calculation to score panels for local scores --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 1 - .../Expanded/ExpandedPanelMiddleContent.cs | 2 +- .../Statistics/PerformanceStatistic.cs | 76 +++++++++++++++++++ 3 files changed, 77 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index d9fb6ccd81..280e1f5a67 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -133,7 +133,6 @@ namespace osu.Game.Beatmaps var total = calculator.Calculate(null); return total; - }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 0033cd1f43..88c61ce267 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Ranking.Expanded { new AccuracyStatistic(score.Accuracy), new ComboStatistic(score.MaxCombo, !score.Statistics.TryGetValue(HitResult.Miss, out var missCount) || missCount == 0), - new CounterStatistic("pp", (int)(score.PP ?? 0)), + new PerformanceStatistic(score), }; var bottomStatistics = new List(); diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs new file mode 100644 index 0000000000..e92e3df2dc --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.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.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class PerformanceStatistic : StatisticDisplay + { + private readonly ScoreInfo score; + + private readonly Bindable performance = new Bindable(); + + private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + + private RollingCounter counter; + + public PerformanceStatistic(ScoreInfo score) + : base("PP") + { + this.score = score; + } + + [BackgroundDependencyLoader] + private void load(BeatmapManager beatmapManager, BeatmapDifficultyManager difficultyManager) + { + if (score.PP.HasValue) + { + performance.Value = (int)score.PP.Value; + } + else + { + var beatmap = beatmapManager.GetWorkingBeatmap(score.Beatmap); + difficultyManager.CalculateScorePerformance(beatmap, score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => performance.Value = (int)t.Result), cancellationTokenSource.Token); + } + } + + public override void Appear() + { + base.Appear(); + counter.Current.BindTo(performance); + } + + 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; + + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => + { + s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + s.Spacing = new Vector2(-2, 0); + }); + } + + protected override void Dispose(bool isDisposing) + { + cancellationTokenSource.Cancel(); + base.Dispose(isDisposing); + } + } +} From 4d94bf3163e76240ade710e775f54da14b6e62a5 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 25 Sep 2020 19:16:33 +0200 Subject: [PATCH 3412/6909] Rename CalculateScorePerformance -> CalculatePerformance --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 2 +- .../Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 280e1f5a67..73834a137f 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -122,7 +122,7 @@ namespace osu.Game.Beatmaps /// The score to do the calculation on. /// An optional to cancel the operation. /// - public async Task CalculateScorePerformance([NotNull] WorkingBeatmap beatmap, [NotNull] ScoreInfo score, CancellationToken token = default) + public async Task CalculatePerformance([NotNull] WorkingBeatmap beatmap, [NotNull] ScoreInfo score, CancellationToken token = default) { return await Task.Factory.StartNew(() => { diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index e92e3df2dc..11edab6636 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics else { var beatmap = beatmapManager.GetWorkingBeatmap(score.Beatmap); - difficultyManager.CalculateScorePerformance(beatmap, score, cancellationTokenSource.Token) + difficultyManager.CalculatePerformance(beatmap, score, cancellationTokenSource.Token) .ContinueWith(t => Schedule(() => performance.Value = (int)t.Result), cancellationTokenSource.Token); } } From e7d04564545cb4fe87d04add0792d6d14c1284c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Sat, 26 Sep 2020 16:25:17 +0200 Subject: [PATCH 3413/6909] Add SpinnerNoBlink to LegacySettings --- osu.Game/Skinning/LegacySkinConfiguration.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index 1d5412d93f..a0e8fb2f92 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.cs @@ -19,6 +19,7 @@ namespace osu.Game.Skinning ComboOverlap, AnimationFramerate, LayeredHitSounds, + SpinnerNoBlink } } } From 33d000e5325af0ca813c9c26de761761332f51b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Sat, 26 Sep 2020 16:25:57 +0200 Subject: [PATCH 3414/6909] Add support for SpinnerNoBlink in legacy spinner --- .../Skinning/LegacyOldStyleSpinner.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index e157842fd1..ada3a825d0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; +using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Osu.Skinning { @@ -25,12 +26,16 @@ namespace osu.Game.Rulesets.Osu.Skinning private Sprite metreSprite; private Container metre; + private bool spinnerNoBlink; + private const float sprite_scale = 1 / 1.6f; private const float final_metre_height = 692 * sprite_scale; [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) { + spinnerNoBlink = source.GetConfig(LegacySetting.SpinnerNoBlink)?.Value ?? false; + drawableSpinner = (DrawableSpinner)drawableObject; RelativeSizeAxes = Axes.Both; @@ -117,12 +122,15 @@ namespace osu.Game.Rulesets.Osu.Skinning private float getMetreHeight(float progress) { - progress = Math.Min(99, progress * 100); + progress *= 100; + + // the spinner should still blink at 100% progress. + if (!spinnerNoBlink) + progress = Math.Min(99, progress); int barCount = (int)progress / 10; - // todo: add SpinnerNoBlink support - if (RNG.NextBool(((int)progress % 10) / 10f)) + if (!spinnerNoBlink && RNG.NextBool(((int)progress % 10) / 10f)) barCount++; return (float)barCount / total_bars * final_metre_height; From b64e69fabd46f83e64853e8630ac06090032f904 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Sep 2020 17:18:50 +0200 Subject: [PATCH 3415/6909] Add test hits to playfields directly where possible --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs | 6 +++--- 2 files 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 47d8a5c012..36289bda93 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning foreach (var playfield in playfields) { var hit = new DrawableTestHit(new Hit(), judgementResult.Type); - Add(hit); + playfield.Add(hit); playfield.OnNewResult(hit, judgementResult); } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 99d1b72ea4..9fc07340c6 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -149,7 +149,7 @@ 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); + drawableRuleset.Playfield.Add(h); ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); } @@ -166,7 +166,7 @@ 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); + drawableRuleset.Playfield.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 }); @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addMissJudgement() { DrawableTestHit h; - Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); + drawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); } From 095686a320823d80be303f2d6028a0da1bf4bb8a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Sep 2020 17:26:26 +0200 Subject: [PATCH 3416/6909] Hide test hit directly in explosion scene --- osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs | 6 ++++++ .../Skinning/TestSceneHitExplosion.cs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index e0af973b53..4eeb4a1475 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -22,6 +22,12 @@ namespace osu.Game.Rulesets.Taiko.Tests HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); } + protected override void UpdateInitialTransforms() + { + // base implementation in DrawableHitObject forces alpha to 1. + // suppress locally to allow hiding the visuals wherever necessary. + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index 19cc56527e..45c94a8a86 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -35,7 +35,9 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - hit, + // the hit needs to be added to hierarchy in order for nested objects to be created correctly. + // setting zero alpha is supposed to prevent the test from looking broken. + hit.With(h => h.Alpha = 0), new HitExplosion(hit, hit.Type) { Anchor = Anchor.Centre, From b1e02db8742e0510d88c68cbe0e15823abee86e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Sep 2020 20:36:38 +0200 Subject: [PATCH 3417/6909] Extract base taiko drawable ruleset scene --- .../DrawableTaikoRulesetTestScene.cs | 55 ++++++++++++++ .../TestSceneHits.cs | 74 +++++-------------- 2 files changed, 75 insertions(+), 54 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs new file mode 100644 index 0000000000..d1c4a1c56d --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs @@ -0,0 +1,55 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public abstract class DrawableTaikoRulesetTestScene : OsuTestScene + { + protected DrawableTaikoRuleset DrawableRuleset { get; private set; } + protected Container PlayfieldContainer { get; private set; } + + [BackgroundDependencyLoader] + private void load() + { + var controlPointInfo = new ControlPointInfo(); + controlPointInfo.Add(0, new TimingControlPoint()); + + WorkingBeatmap beatmap = 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 = @"peppy", + }, + Ruleset = new TaikoRuleset().RulesetInfo + }, + ControlPointInfo = controlPointInfo + }); + + Add(PlayfieldContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Height = 768, + Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) } + }); + } + } +} diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 9fc07340c6..b6cfe368f7 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -2,11 +2,9 @@ // 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.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -18,13 +16,12 @@ 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.Tests.Visual; using osuTK; namespace osu.Game.Rulesets.Taiko.Tests { [TestFixture] - public class TestSceneHits : OsuTestScene + public class TestSceneHits : DrawableTaikoRulesetTestScene { private const double default_duration = 3000; private const float scroll_time = 1000; @@ -32,8 +29,6 @@ namespace osu.Game.Rulesets.Taiko.Tests protected override double TimePerAction => default_duration * 2; private readonly Random rng = new Random(1337); - private DrawableTaikoRuleset drawableRuleset; - private Container playfieldContainer; [BackgroundDependencyLoader] private void load() @@ -64,35 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Tests AddStep("Height test 4", () => changePlayfieldSize(4)); AddStep("Height test 5", () => changePlayfieldSize(5)); AddStep("Reset height", () => changePlayfieldSize(6)); - - var controlPointInfo = new ControlPointInfo(); - controlPointInfo.Add(0, new TimingControlPoint()); - - WorkingBeatmap beatmap = 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 = @"peppy", - }, - Ruleset = new TaikoRuleset().RulesetInfo - }, - ControlPointInfo = controlPointInfo - }); - - Add(playfieldContainer = new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - Height = 768, - Children = new[] { drawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) } - }); } private void changePlayfieldSize(int step) @@ -128,11 +94,11 @@ namespace osu.Game.Rulesets.Taiko.Tests switch (step) { default: - playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500); + PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, rng.Next(25, 400)), 500); break; case 6: - playfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500); + PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500); break; } } @@ -149,9 +115,9 @@ 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) }; - drawableRuleset.Playfield.Add(h); + DrawableRuleset.Playfield.Add(h); - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = hitResult }); } private void addStrongHitJudgement(bool kiai) @@ -166,37 +132,37 @@ 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) }; - drawableRuleset.Playfield.Add(h); + DrawableRuleset.Playfield.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 }); + ((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 }); } private void addMissJudgement() { DrawableTestHit h; - drawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); - ((TaikoPlayfield)drawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); + DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); } private void addBarLine(bool major, double delay = scroll_time) { - BarLine bl = new BarLine { StartTime = drawableRuleset.Playfield.Time.Current + delay }; + BarLine bl = new BarLine { StartTime = DrawableRuleset.Playfield.Time.Current + delay }; - drawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl)); + DrawableRuleset.Playfield.Add(major ? new DrawableBarLineMajor(bl) : new DrawableBarLine(bl)); } private void addSwell(double duration = default_duration) { var swell = new Swell { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, Duration = duration, }; swell.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableSwell(swell)); + DrawableRuleset.Playfield.Add(new DrawableSwell(swell)); } private void addDrumRoll(bool strong, double duration = default_duration, bool kiai = false) @@ -206,7 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Tests var d = new DrumRoll { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong, Duration = duration, TickRate = 8, @@ -217,33 +183,33 @@ namespace osu.Game.Rulesets.Taiko.Tests d.ApplyDefaults(cpi, new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableDrumRoll(d)); + DrawableRuleset.Playfield.Add(new DrawableDrumRoll(d)); } private void addCentreHit(bool strong) { Hit h = new Hit { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong }; h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableHit(h)); + DrawableRuleset.Playfield.Add(new DrawableHit(h)); } private void addRimHit(bool strong) { Hit h = new Hit { - StartTime = drawableRuleset.Playfield.Time.Current + scroll_time, + StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong }; h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - drawableRuleset.Playfield.Add(new DrawableHit(h)); + DrawableRuleset.Playfield.Add(new DrawableHit(h)); } private class TestStrongNestedHit : DrawableStrongNestedHit From 0563a488f479083260753908b2ca8c140d3d98b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Sep 2020 20:37:23 +0200 Subject: [PATCH 3418/6909] Add failing test case --- .../TestSceneFlyingHits.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs new file mode 100644 index 0000000000..7492a76a67 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.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.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets.Judgements; +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; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + [TestFixture] + public class TestSceneFlyingHits : DrawableTaikoRulesetTestScene + { + [TestCase(HitType.Centre)] + [TestCase(HitType.Rim)] + public void TestFlyingHits(HitType hitType) + { + DrawableFlyingHit flyingHit = null; + + AddStep("add flying hit", () => + { + addFlyingHit(hitType); + + // flying hits all land in one common scrolling container (and stay there for rewind purposes), + // so we need to manually get the latest one. + flyingHit = this.ChildrenOfType() + .OrderByDescending(h => h.HitObject.StartTime) + .FirstOrDefault(); + }); + + AddAssert("hit type is correct", () => flyingHit.HitObject.Type == hitType); + } + + private void addFlyingHit(HitType hitType) + { + var tick = new DrumRollTick { HitWindows = HitWindows.Empty, StartTime = DrawableRuleset.Playfield.Time.Current }; + + DrawableDrumRollTick h; + DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(tick, new TaikoDrumRollTickJudgement()) { Type = HitResult.Perfect }); + } + } +} From d61a8327da94243b65d7761ee23d9a5f4649a5fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Sep 2020 20:59:55 +0200 Subject: [PATCH 3419/6909] Fix rim flying hits changing colour --- .../Objects/Drawables/DrawableFlyingHit.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs index 460e760629..3253c1ce5a 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableFlyingHit.cs @@ -27,5 +27,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables base.LoadComplete(); ApplyResult(r => r.Type = r.Judgement.MaxResult); } + + protected override void LoadSamples() + { + // block base call - flying hits are not supposed to play samples + // the base call could overwrite the type of this hit + } } } From c5cf0d0410e57ffd63f105d4467ba9b4c05df1b1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 26 Sep 2020 21:50:39 +0200 Subject: [PATCH 3420/6909] Fix tests failing. --- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 19294d12fc..ce73e9061b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -194,7 +194,8 @@ namespace osu.Game.Tests.Visual.Background AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo + Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, + Ruleset = new OsuRuleset().RulesetInfo, }))); AddUntilStep("Wait for results is current", () => results.IsCurrentScreen()); From 84cc6068f5819a86c57e17dbdf4dc8e9270e8db4 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 27 Sep 2020 09:25:01 +0200 Subject: [PATCH 3421/6909] Remove unnecessary XMLDoc comment and remove unecessary implicit null parm --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 73834a137f..1acc4b290f 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -121,7 +121,6 @@ namespace osu.Game.Beatmaps /// The to do the calculation on. /// The score to do the calculation on. /// An optional to cancel the operation. - /// public async Task CalculatePerformance([NotNull] WorkingBeatmap beatmap, [NotNull] ScoreInfo score, CancellationToken token = default) { return await Task.Factory.StartNew(() => @@ -130,7 +129,7 @@ namespace osu.Game.Beatmaps return default; var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(beatmap, score); - var total = calculator.Calculate(null); + var total = calculator.Calculate(); return total; }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); From 3cb9103fe094fdfe5845a877e9bd3bf4307f4dd2 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 27 Sep 2020 09:37:57 +0200 Subject: [PATCH 3422/6909] Inherit PerformanceStatistic from CounterStatistic --- .../Expanded/Statistics/CounterStatistic.cs | 8 ++--- .../Statistics/PerformanceStatistic.cs | 29 ++----------------- 2 files changed, 7 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index e820831809..1f8deb4d59 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { private readonly int count; - private RollingCounter counter; + protected RollingCounter Counter; /// /// Creates a new . @@ -33,12 +33,12 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public override void Appear() { base.Appear(); - counter.Current.Value = count; + Counter.Current.Value = count; } - protected override Drawable CreateContent() => counter = new Counter(); + protected override Drawable CreateContent() => Counter = new StatisticCounter(); - private class Counter : RollingCounter + private class StatisticCounter : RollingCounter { protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 11edab6636..b84d0b7ff7 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -4,18 +4,12 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Game.Beatmaps; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; -using osu.Game.Screens.Ranking.Expanded.Accuracy; -using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatistic : StatisticDisplay + public class PerformanceStatistic : CounterStatistic { private readonly ScoreInfo score; @@ -23,10 +17,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - private RollingCounter counter; - public PerformanceStatistic(ScoreInfo score) - : base("PP") + : base("PP", 0) { this.score = score; } @@ -49,22 +41,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public override void Appear() { base.Appear(); - counter.Current.BindTo(performance); - } - - 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; - - protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => - { - s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); - s.Spacing = new Vector2(-2, 0); - }); + Counter.Current.BindTo(performance); } protected override void Dispose(bool isDisposing) From 00aea7748970cb3d29bde9f7a171d6ba5a545480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Sep 2020 11:18:13 +0200 Subject: [PATCH 3423/6909] Fix potential instability of overlay activation tests --- .../Visual/Gameplay/TestSceneOverlayActivation.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 3ee0f4e720..e36cc6861d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -12,6 +12,14 @@ namespace osu.Game.Tests.Visual.Gameplay { protected new OverlayTestPlayer Player => base.Player as OverlayTestPlayer; + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddUntilStep("gameplay has started", + () => Player.GameplayClockContainer.GameplayClock.CurrentTime > Player.DrawableRuleset.GameplayStartTime); + } + [Test] public void TestGameplayOverlayActivation() { From 8d9945dea895ab03f7fcb9562a1d533ebc502d81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Sep 2020 11:28:20 +0200 Subject: [PATCH 3424/6909] Change until step to assert for consistency --- osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index e36cc6861d..ce04b940e7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -29,7 +29,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestGameplayOverlayActivationPaused() { - AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddStep("pause gameplay", () => Player.Pause()); AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); } From ddede857043f8a7a189cd69dcffde9503bccdf88 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 27 Sep 2020 12:44:29 +0200 Subject: [PATCH 3425/6909] Split performance calculation to its own class. --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 21 ---------- osu.Game/OsuGameBase.cs | 5 +++ osu.Game/Scoring/ScorePerformanceManager.cs | 39 +++++++++++++++++++ .../Statistics/PerformanceStatistic.cs | 8 ++-- 4 files changed, 47 insertions(+), 26 deletions(-) create mode 100644 osu.Game/Scoring/ScorePerformanceManager.cs diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 1acc4b290f..e9d26683c3 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -16,7 +16,6 @@ using osu.Framework.Lists; using osu.Framework.Threading; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Scoring; namespace osu.Game.Beatmaps { @@ -115,26 +114,6 @@ namespace osu.Game.Beatmaps return computeDifficulty(key, beatmapInfo, rulesetInfo); } - /// - /// Calculates performance for the given on a given . - /// - /// The to do the calculation on. - /// The score to do the calculation on. - /// An optional to cancel the operation. - public async Task CalculatePerformance([NotNull] WorkingBeatmap beatmap, [NotNull] ScoreInfo score, CancellationToken token = default) - { - return await Task.Factory.StartNew(() => - { - if (token.IsCancellationRequested) - return default; - - var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(beatmap, score); - var total = calculator.Calculate(); - - return total; - }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); - } - private CancellationTokenSource trackedUpdateCancellationSource; private readonly List linkedCancellationSources = new List(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index b1269e9300..9a4710d576 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -58,6 +58,8 @@ namespace osu.Game protected ScoreManager ScoreManager; + protected ScorePerformanceManager ScorePerformanceManager; + protected BeatmapDifficultyManager DifficultyManager; protected SkinManager SkinManager; @@ -226,6 +228,9 @@ namespace osu.Game dependencies.Cache(DifficultyManager = new BeatmapDifficultyManager()); AddInternal(DifficultyManager); + dependencies.Cache(ScorePerformanceManager = new ScorePerformanceManager()); + AddInternal(ScorePerformanceManager); + dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); dependencies.Cache(RulesetConfigCache = new RulesetConfigCache(SettingsStore)); diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs new file mode 100644 index 0000000000..c8fec3b40c --- /dev/null +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; + +namespace osu.Game.Scoring +{ + public class ScorePerformanceManager : Component + { + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + /// + /// Calculates performance for the given . + /// + /// The score to do the calculation on. + /// An optional to cancel the operation. + public async Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) + { + return await Task.Factory.StartNew(() => + { + if (token.IsCancellationRequested) + return default; + + var beatmap = beatmapManager.GetWorkingBeatmap(score.Beatmap); + + var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(beatmap, score); + var total = calculator.Calculate(); + + return total; + }, token); + } + } +} diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index b84d0b7ff7..e014258fd4 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -4,7 +4,6 @@ using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Beatmaps; using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics @@ -24,7 +23,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } [BackgroundDependencyLoader] - private void load(BeatmapManager beatmapManager, BeatmapDifficultyManager difficultyManager) + private void load(ScorePerformanceManager performanceManager) { if (score.PP.HasValue) { @@ -32,9 +31,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } else { - var beatmap = beatmapManager.GetWorkingBeatmap(score.Beatmap); - difficultyManager.CalculatePerformance(beatmap, score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => performance.Value = (int)t.Result), cancellationTokenSource.Token); + performanceManager.CalculatePerformanceAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => performance.Value = (int)t.Result), cancellationTokenSource.Token); } } From deb207001a8fc34feea461fd6cc6d7f275b0df27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Sep 2020 15:23:34 +0200 Subject: [PATCH 3426/6909] Remove schedule causing default skin explosion regression --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 7b3fbb1faf..120cf264c3 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -215,16 +215,12 @@ namespace osu.Game.Rulesets.Taiko.UI private void addDrumRollHit(DrawableDrumRollTick drawableTick) => drumRollHitContainer.Add(new DrawableFlyingHit(drawableTick)); - /// - /// As legacy skins have different explosions for singular and double strong hits, - /// explosion addition is scheduled to ensure that both hits are processed if they occur on the same frame. - /// - private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) => Schedule(() => + private void addExplosion(DrawableHitObject drawableObject, HitResult result, HitType type) { hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); - }); + } private class ProxyContainer : LifetimeManagementContainer { From d5f1d94b517703f202bfcf6dfe695eb46ee02f33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Sep 2020 15:29:04 +0200 Subject: [PATCH 3427/6909] Allow specifying two sprites for legacy hit explosions --- .../Skinning/LegacyHitExplosion.cs | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index b5ec2e8def..ca0a8f601c 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -8,14 +9,36 @@ namespace osu.Game.Rulesets.Taiko.Skinning { public class LegacyHitExplosion : CompositeDrawable { - public LegacyHitExplosion(Drawable sprite) - { - InternalChild = sprite; + private readonly Drawable sprite; + private readonly Drawable strongSprite; + /// + /// Creates a new legacy hit explosion. + /// + /// + /// Contrary to stable's, this implementation doesn't require a frame-perfect hit + /// for the strong sprite to be displayed. + /// + /// The normal legacy explosion sprite. + /// The strong legacy explosion sprite. + public LegacyHitExplosion(Drawable sprite, Drawable strongSprite = null) + { + this.sprite = sprite; + this.strongSprite = strongSprite; + } + + [BackgroundDependencyLoader] + private void load() + { Anchor = Anchor.Centre; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; + + AddInternal(sprite); + + if (strongSprite != null) + AddInternal(strongSprite.With(s => s.Alpha = 0)); } protected override void LoadComplete() From eb62ad4e551e72c3642ba56db21c954a4c850c19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Sep 2020 15:33:18 +0200 Subject: [PATCH 3428/6909] Look up both sprites for legacy explosions --- .../Skinning/TaikoLegacySkinTransformer.cs | 30 ++++++++++--------- .../TaikoSkinComponents.cs | 2 -- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 27 ++++------------- 3 files changed, 21 insertions(+), 38 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index c222ccb51f..d320b824e6 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -74,15 +74,23 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; - case TaikoSkinComponents.TaikoExplosionGood: - case TaikoSkinComponents.TaikoExplosionGoodStrong: - case TaikoSkinComponents.TaikoExplosionGreat: - case TaikoSkinComponents.TaikoExplosionGreatStrong: case TaikoSkinComponents.TaikoExplosionMiss: - var sprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false); - if (sprite != null) - return new LegacyHitExplosion(sprite); + var missSprite = this.GetAnimation(getHitName(taikoComponent.Component), true, false); + if (missSprite != null) + return new LegacyHitExplosion(missSprite); + + return null; + + case TaikoSkinComponents.TaikoExplosionGood: + case TaikoSkinComponents.TaikoExplosionGreat: + + var hitName = getHitName(taikoComponent.Component); + var hitSprite = this.GetAnimation(hitName, true, false); + var strongHitSprite = this.GetAnimation($"{hitName}k", true, false); + + if (hitSprite != null) + return new LegacyHitExplosion(hitSprite, strongHitSprite); return null; @@ -109,17 +117,11 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.TaikoExplosionGood: return "taiko-hit100"; - case TaikoSkinComponents.TaikoExplosionGoodStrong: - return "taiko-hit100k"; - case TaikoSkinComponents.TaikoExplosionGreat: return "taiko-hit300"; - - case TaikoSkinComponents.TaikoExplosionGreatStrong: - return "taiko-hit300k"; } - throw new ArgumentOutOfRangeException(nameof(component), "Invalid result type"); + throw new ArgumentOutOfRangeException(nameof(component), $"Invalid component type: {component}"); } public override 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 0d785adb4a..ac4fb51661 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -17,9 +17,7 @@ namespace osu.Game.Rulesets.Taiko BarLine, TaikoExplosionMiss, TaikoExplosionGood, - TaikoExplosionGoodStrong, TaikoExplosionGreat, - TaikoExplosionGreatStrong, Scroller, Mascot, } diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index efd1b25046..53765f04dd 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; using osuTK; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -10,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.UI @@ -50,10 +48,10 @@ namespace osu.Game.Rulesets.Taiko.UI [BackgroundDependencyLoader] private void load() { - Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(JudgedObject)), _ => new DefaultHitExplosion(JudgedObject, result)); + Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(getComponentName(result)), _ => new DefaultHitExplosion(JudgedObject, result)); } - private TaikoSkinComponents getComponentName(DrawableHitObject judgedObject) + private static TaikoSkinComponents getComponentName(HitResult result) { switch (result) { @@ -61,28 +59,13 @@ namespace osu.Game.Rulesets.Taiko.UI return TaikoSkinComponents.TaikoExplosionMiss; case HitResult.Good: - return useStrongExplosion(judgedObject) - ? TaikoSkinComponents.TaikoExplosionGoodStrong - : TaikoSkinComponents.TaikoExplosionGood; + return TaikoSkinComponents.TaikoExplosionGood; case HitResult.Great: - return useStrongExplosion(judgedObject) - ? TaikoSkinComponents.TaikoExplosionGreatStrong - : TaikoSkinComponents.TaikoExplosionGreat; + return TaikoSkinComponents.TaikoExplosionGreat; } - throw new ArgumentOutOfRangeException(nameof(judgedObject), "Invalid result type"); - } - - private bool useStrongExplosion(DrawableHitObject judgedObject) - { - if (!(judgedObject.HitObject is Hit)) - return false; - - if (!(judgedObject.NestedHitObjects.SingleOrDefault() is DrawableStrongNestedHit nestedHit)) - return false; - - return judgedObject.Result.Type == nestedHit.Result.Type; + throw new ArgumentOutOfRangeException(nameof(result), $"Invalid result type: {result}"); } /// From 2f7c0b49344127ea1ccbe286f021edc71285352b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Sep 2020 16:07:19 +0200 Subject: [PATCH 3429/6909] Allow switching between legacy sprites --- .../Skinning/LegacyHitExplosion.cs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index ca0a8f601c..0f91239819 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.Skinning { @@ -12,6 +15,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning private readonly Drawable sprite; private readonly Drawable strongSprite; + private DrawableHit hit; + private DrawableStrongNestedHit nestedStrongHit; + /// /// Creates a new legacy hit explosion. /// @@ -28,7 +34,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning } [BackgroundDependencyLoader] - private void load() + private void load(DrawableHitObject judgedObject) { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -39,6 +45,12 @@ namespace osu.Game.Rulesets.Taiko.Skinning if (strongSprite != null) AddInternal(strongSprite.With(s => s.Alpha = 0)); + + if (judgedObject is DrawableHit h) + { + hit = h; + nestedStrongHit = hit.NestedHitObjects.SingleOrDefault() as DrawableStrongNestedHit; + } } protected override void LoadComplete() @@ -56,5 +68,24 @@ namespace osu.Game.Rulesets.Taiko.Skinning Expire(true); } + + protected override void Update() + { + base.Update(); + + if (shouldSwitchToStrongSprite() && strongSprite != null) + { + sprite.FadeOut(50, Easing.OutQuint); + strongSprite.FadeIn(50, Easing.OutQuint); + } + } + + private bool shouldSwitchToStrongSprite() + { + if (hit == null || nestedStrongHit == null) + return false; + + return hit.Result.Type == nestedStrongHit.Result.Type; + } } } From 49441286311c0c052a1100c13324b9c5b5f90f09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Sep 2020 18:11:12 +0200 Subject: [PATCH 3430/6909] Ensure both sprites are centered --- .../Skinning/LegacyHitExplosion.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index 0f91239819..f24f75f097 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -41,10 +41,21 @@ namespace osu.Game.Rulesets.Taiko.Skinning AutoSizeAxes = Axes.Both; - AddInternal(sprite); + AddInternal(sprite.With(s => + { + s.Anchor = Anchor.Centre; + s.Origin = Anchor.Centre; + })); if (strongSprite != null) - AddInternal(strongSprite.With(s => s.Alpha = 0)); + { + AddInternal(strongSprite.With(s => + { + s.Alpha = 0; + s.Anchor = Anchor.Centre; + s.Origin = Anchor.Centre; + })); + } if (judgedObject is DrawableHit h) { From 3cf430f49434ed3883ca1c5dc06844ca3d287327 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 15:30:18 +0900 Subject: [PATCH 3431/6909] Avoid saving state changes if nothing has changed (via binary comparison) --- .../Editing/EditorChangeHandlerTest.cs | 57 ++++++++++++++++++- osu.Game/Screens/Edit/EditorChangeHandler.cs | 28 +++++---- 2 files changed, 71 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index feda1ae0e9..ff2c9fb1a9 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -2,7 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; namespace osu.Game.Tests.Editing @@ -13,11 +15,12 @@ namespace osu.Game.Tests.Editing [Test] public void TestSaveRestoreState() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var (handler, beatmap) = createChangeHandler(); Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.False); + addArbitraryChange(beatmap); handler.SaveState(); Assert.That(handler.CanUndo.Value, Is.True); @@ -29,15 +32,48 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanRedo.Value, Is.True); } + [Test] + public void TestSaveSameStateDoesNotSave() + { + var (handler, beatmap) = createChangeHandler(); + + Assert.That(handler.CanUndo.Value, Is.False); + Assert.That(handler.CanRedo.Value, Is.False); + + addArbitraryChange(beatmap); + handler.SaveState(); + + Assert.That(handler.CanUndo.Value, Is.True); + Assert.That(handler.CanRedo.Value, Is.False); + + string hash = handler.CurrentStateHash; + + // save a save without making any changes + handler.SaveState(); + + Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); + + handler.RestoreState(-1); + + Assert.That(hash, Is.Not.EqualTo(handler.CurrentStateHash)); + + // we should only be able to restore once even though we saved twice. + Assert.That(handler.CanUndo.Value, Is.False); + Assert.That(handler.CanRedo.Value, Is.True); + } + [Test] public void TestMaxStatesSaved() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var (handler, beatmap) = createChangeHandler(); Assert.That(handler.CanUndo.Value, Is.False); for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) + { + addArbitraryChange(beatmap); handler.SaveState(); + } Assert.That(handler.CanUndo.Value, Is.True); @@ -53,12 +89,15 @@ namespace osu.Game.Tests.Editing [Test] public void TestMaxStatesExceeded() { - var handler = new EditorChangeHandler(new EditorBeatmap(new Beatmap())); + var (handler, beatmap) = createChangeHandler(); Assert.That(handler.CanUndo.Value, Is.False); for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES * 2; i++) + { + addArbitraryChange(beatmap); handler.SaveState(); + } Assert.That(handler.CanUndo.Value, Is.True); @@ -70,5 +109,17 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanUndo.Value, Is.False); } + + private (EditorChangeHandler, EditorBeatmap) createChangeHandler() + { + var beatmap = new EditorBeatmap(new Beatmap()); + + return (new EditorChangeHandler(beatmap), beatmap); + } + + private void addArbitraryChange(EditorBeatmap beatmap) + { + beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) }); + } } } diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index aa0f89912a..286fdbb020 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -4,9 +4,11 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Text; using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Logging; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; @@ -89,23 +91,27 @@ namespace osu.Game.Screens.Edit if (isRestoring) return; - 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)) new LegacyBeatmapEncoder(editorBeatmap, editorBeatmap.BeatmapSkin).Encode(sw); - savedStates.Add(stream.ToArray()); + var newState = stream.ToArray(); + + // if the previous state is binary equal we don't need to push a new one, unless this is the initial state. + if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return; + + if (currentState < savedStates.Count - 1) + savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); + + if (savedStates.Count > MAX_SAVED_STATES) + savedStates.RemoveAt(0); + + savedStates.Add(newState); + + currentState = savedStates.Count - 1; + updateBindables(); } - - currentState = savedStates.Count - 1; - - updateBindables(); } /// From 1aa8b400d432b17031d4d5a37898dc0a3ac66d2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:45:36 +0900 Subject: [PATCH 3432/6909] Avoid unnecessary object updates from SelectionHandlers --- osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs | 8 +++++++- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 3 +-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index a3ecf7ed95..d5dd758e10 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -57,7 +57,13 @@ namespace osu.Game.Rulesets.Taiko.Edit ChangeHandler.BeginChange(); foreach (var h in hits) - h.IsStrong = state; + { + if (h.IsStrong != state) + { + h.IsStrong = state; + EditorBeatmap.UpdateHitObject(h); + } + } ChangeHandler.EndChange(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 6ca85fe026..a0220cf987 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -288,8 +288,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { var comboInfo = h as IHasComboInformation; - if (comboInfo == null) - continue; + if (comboInfo == null || comboInfo.NewCombo == state) continue; comboInfo.NewCombo = state; EditorBeatmap?.UpdateHitObject(h); From 0ae2266b8229ba5c8192385230c4506b2bf3e5a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:51:28 +0900 Subject: [PATCH 3433/6909] Fix new placement hitobjects in the editor not getting the default sample added --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 81d7fa4b32..9b3314e2ad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -212,6 +212,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if (blueprint != null) { + // doing this post-creations as adding the default hit sample should be the case regardless of the ruleset. + blueprint.HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL }); + placementBlueprintContainer.Child = currentPlacement = blueprint; // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame From a4e9c85333b06d293cc88a921ac38612490b1c04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:37:55 +0900 Subject: [PATCH 3434/6909] Trigger a hitobject update after blueprint drag ends --- .../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 8908520cd7..970e16d1c3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -201,6 +201,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (isDraggingBlueprint) { + // handle positional change etc. + foreach (var obj in selectedHitObjects) + Beatmap.UpdateHitObject(obj); + changeHandler?.EndChange(); isDraggingBlueprint = false; } From 6095446f10df0a34b1676aa1bf46deb9cceb7a26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:15:26 +0900 Subject: [PATCH 3435/6909] Fix autoplay generators failing on empty hitobjects lists --- osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs | 3 +++ osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs | 3 +++ osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 3 +++ osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs | 3 +++ 4 files changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 5d11c574b1..a4f54bfe82 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -31,6 +31,9 @@ namespace osu.Game.Rulesets.Catch.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + // todo: add support for HT DT const double dash_speed = Catcher.BASE_SPEED; const double movement_speed = dash_speed / 2; diff --git a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs index 483327d5b3..3ebbe5af8e 100644 --- a/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs +++ b/osu.Game.Rulesets.Mania/Replays/ManiaAutoGenerator.cs @@ -46,6 +46,9 @@ namespace osu.Game.Rulesets.Mania.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + var pointGroups = generateActionPoints().GroupBy(a => a.Time).OrderBy(g => g.First().Time); var actions = new List(); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 76b2631894..9b350278f3 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -72,6 +72,9 @@ namespace osu.Game.Rulesets.Osu.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + buttonIndex = 0; AddFrameToReplay(new OsuReplayFrame(-100000, new Vector2(256, 500))); diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index 273f4e4105..db2e5948f5 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -30,6 +30,9 @@ namespace osu.Game.Rulesets.Taiko.Replays public override Replay Generate() { + if (Beatmap.HitObjects.Count == 0) + return Replay; + bool hitButton = true; Frames.Add(new TaikoReplayFrame(-100000)); From e8220cf1b62dd7fcdd2afa13bb39b53b8a3771a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:03:56 +0900 Subject: [PATCH 3436/6909] Allow attaching a replay to a FrameStabilityContainer when FrameStablePlayback is off --- .../Rulesets/UI/FrameStabilityContainer.cs | 72 ++++++++++++------- 1 file changed, 48 insertions(+), 24 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index d574991fa0..a4af92749f 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -123,39 +123,63 @@ namespace osu.Game.Rulesets.UI try { - if (!FrameStablePlayback) - return; - - if (firstConsumption) + if (FrameStablePlayback) { - // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. - // Instead we perform an initial seek to the proposed time. + if (firstConsumption) + { + // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. + // Instead we perform an initial seek to the proposed time. - // process frame (in addition to finally clause) to clear out ElapsedTime - manualClock.CurrentTime = newProposedTime; - framedClock.ProcessFrame(); + // process frame (in addition to finally clause) to clear out ElapsedTime + manualClock.CurrentTime = newProposedTime; + framedClock.ProcessFrame(); - firstConsumption = false; - } - else if (manualClock.CurrentTime < gameplayStartTime) - manualClock.CurrentTime = newProposedTime = Math.Min(gameplayStartTime, newProposedTime); - else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) - { - newProposedTime = newProposedTime > manualClock.CurrentTime - ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time) - : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time); + firstConsumption = false; + } + else if (manualClock.CurrentTime < gameplayStartTime) + manualClock.CurrentTime = newProposedTime = Math.Min(gameplayStartTime, newProposedTime); + else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) + { + newProposedTime = newProposedTime > manualClock.CurrentTime + ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time) + : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time); + } } if (isAttached) { - double? newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime); + double? newTime; - if (newTime == null) + if (FrameStablePlayback) { - // we shouldn't execute for this time value. probably waiting on more replay data. - validState = false; - requireMoreUpdateLoops = true; - return; + // when stability is turned on, we shouldn't execute for time values the replay is unable to satisfy. + if ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) == null) + { + // setting invalid state here ensures that gameplay will not continue (ie. our child + // hierarchy won't be updated). + validState = false; + + // potentially loop to catch-up playback. + requireMoreUpdateLoops = true; + + return; + } + } + else + { + // when stability is disabled, we don't really care about accuracy. + // looping over the replay will allow it to catch up and feed out the required values + // for the current time. + while ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) != newProposedTime) + { + if (newTime == null) + { + // special case for when the replay actually can't arrive at the required time. + // protects from potential endless loop. + validState = false; + return; + } + } } newProposedTime = newTime.Value; From ff7c904996083e985dd41b389656b190f357b202 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:03:37 +0900 Subject: [PATCH 3437/6909] Add autoplay mod in editor specific ruleset construction --- 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 b9b7c1ef54..6e377ff207 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Edit try { - drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap)) + drawableRulesetWrapper = new DrawableEditRulesetWrapper(CreateDrawableRuleset(Ruleset, EditorBeatmap.PlayableBeatmap, new[] { Ruleset.GetAutoplayMod() })) { Clock = EditorClock, ProcessCustomClock = false From 524c2b678c68b71d604342b32e5274fbb7684607 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 14:15:54 +0900 Subject: [PATCH 3438/6909] Forcefully regenerate autoplay on editor changes --- .../Rulesets/Edit/DrawableEditRulesetWrapper.cs | 8 ++++++++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 89e7866707..1070b8cbd2 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -45,15 +45,21 @@ namespace osu.Game.Rulesets.Edit base.LoadComplete(); beatmap.HitObjectAdded += addHitObject; + beatmap.HitObjectUpdated += updateReplay; beatmap.HitObjectRemoved += removeHitObject; } + private void updateReplay(HitObject obj = null) => + drawableRuleset.RegenerateAutoplay(); + private void addHitObject(HitObject hitObject) { var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject); drawableRuleset.Playfield.Add(drawableObject); drawableRuleset.Playfield.PostProcess(); + + updateReplay(); } private void removeHitObject(HitObject hitObject) @@ -62,6 +68,8 @@ namespace osu.Game.Rulesets.Edit drawableRuleset.Playfield.Remove(drawableObject); drawableRuleset.Playfield.PostProcess(); + + drawableRuleset.RegenerateAutoplay(); } public override bool PropagatePositionalInputSubTree => false; diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index fbb9acfe90..50e9a93e22 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -151,8 +151,11 @@ namespace osu.Game.Rulesets.UI public virtual PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new PlayfieldAdjustmentContainer(); + [Resolved] + private OsuConfigManager config { get; set; } + [BackgroundDependencyLoader] - private void load(OsuConfigManager config, CancellationToken? cancellationToken) + private void load(CancellationToken? cancellationToken) { InternalChildren = new Drawable[] { @@ -178,11 +181,18 @@ namespace osu.Game.Rulesets.UI .WithChild(ResumeOverlay))); } - applyRulesetMods(Mods, config); + RegenerateAutoplay(); loadObjects(cancellationToken); } + public void RegenerateAutoplay() + { + // for now this is applying mods which aren't just autoplay. + // we'll need to reconsider this flow in the future. + applyRulesetMods(Mods, config); + } + /// /// Creates and adds drawable representations of hit objects to the play field. /// From 7949eabaac45fe2a67ab5a4f283c8851e314300c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 15:49:45 +0900 Subject: [PATCH 3439/6909] Remove left-over using --- osu.Game/Screens/Edit/EditorChangeHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 286fdbb020..617c436ee0 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -8,7 +8,6 @@ using System.Linq; using System.Text; using osu.Framework.Bindables; using osu.Framework.Extensions; -using osu.Framework.Logging; using osu.Game.Beatmaps.Formats; using osu.Game.Rulesets.Objects; From 467a16bf750600b0edc47674acd022334c632d89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 16:21:20 +0900 Subject: [PATCH 3440/6909] Fix fade out extension logic (and make it generally look better for sliders) --- .../Edit/DrawableOsuEditRuleset.cs | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index a8719e0aa8..01e59c9598 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -8,6 +8,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -20,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. /// - private const double editor_hit_object_fade_out_extension = 500; + private const double editor_hit_object_fade_out_extension = 700; public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) : base(ruleset, beatmap, mods) @@ -32,20 +33,37 @@ namespace osu.Game.Rulesets.Osu.Edit private void updateState(DrawableHitObject hitObject, ArmedState state) { - switch (state) + if (state == ArmedState.Idle) + return; + + // adjust the visuals of certain object types to make them stay on screen for longer than usual. + switch (hitObject) { - case ArmedState.Miss: - // Get the existing fade out transform - var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); - if (existing == null) - return; - - hitObject.RemoveTransform(existing); - - using (hitObject.BeginAbsoluteSequence(existing.StartTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + case DrawableSlider slider: + // no specifics to sliders but let them fade slower below. break; + + case DrawableHitCircle circle: // also handles slider heads + circle.ApproachCircle + .FadeOutFromOne(editor_hit_object_fade_out_extension) + .Expire(); + break; + + default: + // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) + return; } + + // Get the existing fade out transform + var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); + + if (existing == null) + return; + + hitObject.RemoveTransform(existing); + + using (hitObject.BeginAbsoluteSequence(existing.StartTime)) + hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); } protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor(); From 5237fa7bf24cdaca9e2a8c2e24bdceff5906ff84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 16:37:54 +0900 Subject: [PATCH 3441/6909] Remove unused local in case statement --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 01e59c9598..746ff4ac19 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -39,7 +39,11 @@ namespace osu.Game.Rulesets.Osu.Edit // adjust the visuals of certain object types to make them stay on screen for longer than usual. switch (hitObject) { - case DrawableSlider slider: + default: + // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) + return; + + case DrawableSlider _: // no specifics to sliders but let them fade slower below. break; @@ -48,10 +52,6 @@ namespace osu.Game.Rulesets.Osu.Edit .FadeOutFromOne(editor_hit_object_fade_out_extension) .Expire(); break; - - default: - // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) - return; } // Get the existing fade out transform From 8692c24dfc624f739d10209cafa173b125ff024d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 17:20:36 +0900 Subject: [PATCH 3442/6909] Fix extending spinners in editor causing them to disappear temporarily --- .../Objects/Drawables/Pieces/DefaultSpinnerDisc.cs | 9 ++++----- osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs | 5 ++--- osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 5 ++--- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 1476fe6010..e45ea9c6cc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -93,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces base.LoadComplete(); drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); - drawableSpinner.State.BindValueChanged(updateStateTransforms, true); + drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; } protected override void Update() @@ -123,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation; } - private void updateStateTransforms(ValueChangedEvent state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { centre.ScaleTo(0); mainContainer.ScaleTo(0); @@ -144,11 +143,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } // transforms we have from completing the spinner will be rolled back, so reapply immediately. - updateComplete(state.NewValue == ArmedState.Hit, 0); + updateComplete(state == ArmedState.Hit, 0); using (BeginDelayedSequence(spinner.Duration, true)) { - switch (state.NewValue) + switch (state) { case ArmedState.Hit: this.ScaleTo(Scale * 1.2f, 320, Easing.Out); diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 739c87e037..734c66ce7d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -2,7 +2,6 @@ // 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; @@ -72,10 +71,10 @@ namespace osu.Game.Rulesets.Osu.Skinning base.LoadComplete(); this.FadeOut(); - drawableSpinner.State.BindValueChanged(updateStateTransforms, true); + drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; } - private void updateStateTransforms(ValueChangedEvent state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { var spinner = (Spinner)drawableSpinner.HitObject; diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index e157842fd1..09f8894d53 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -86,10 +85,10 @@ namespace osu.Game.Rulesets.Osu.Skinning base.LoadComplete(); this.FadeOut(); - drawableSpinner.State.BindValueChanged(updateStateTransforms, true); + drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; } - private void updateStateTransforms(ValueChangedEvent state) + private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { var spinner = drawableSpinner.HitObject; From 63b5b8b84187251bb51e72da4e295d00b7a8226d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 17:32:57 +0900 Subject: [PATCH 3443/6909] Fix sliders not dragging correctly after snaking has begun Closes #10278. --- .../Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs | 5 +++++ .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index 78f4c4d992..9349ef7a18 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -15,6 +15,11 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { private readonly ManualSliderBody body; + /// + /// Offset in absolute (local) coordinates from the start of the curve. + /// + public Vector2 PathStartLocation => body.PathOffset; + public SliderBodyPiece() { InternalChild = body = new ManualSliderBody diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 6633136673..94862eb205 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 ScreenSpaceSelectionPoint => ((DrawableSlider)DrawableObject).HeadCircle.ScreenSpaceDrawQuad.Centre; + public override Vector2 ScreenSpaceSelectionPoint => BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos); From e60e47ff66b623c230cdfe832bc4b12d2688e479 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 17:41:10 +0900 Subject: [PATCH 3444/6909] Unbind events on disposal --- .../Objects/Drawables/Pieces/DefaultSpinnerDisc.cs | 6 ++++++ osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs | 6 ++++++ osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index e45ea9c6cc..51d67c7f67 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -184,5 +184,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return true; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 734c66ce7d..8baa6a3dd3 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -94,5 +94,11 @@ namespace osu.Game.Rulesets.Osu.Skinning Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * 0.2f)); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 09f8894d53..a895298ec3 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -126,5 +126,11 @@ namespace osu.Game.Rulesets.Osu.Skinning return (float)barCount / total_bars * final_metre_height; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + } } } From b6bc829bd5a8ea160a26fd66f7f18362395d9bb5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 28 Sep 2020 17:46:22 +0900 Subject: [PATCH 3445/6909] Guard against nulls (load not run) --- .../Objects/Drawables/Pieces/DefaultSpinnerDisc.cs | 4 +++- osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs | 4 +++- osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 51d67c7f67..2862fe49bd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -188,7 +188,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + + if (drawableSpinner != null) + drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 8baa6a3dd3..bcb2af8e3e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -98,7 +98,9 @@ namespace osu.Game.Rulesets.Osu.Skinning protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + + if (drawableSpinner != null) + drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index a895298ec3..a45d91801d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -130,7 +130,9 @@ namespace osu.Game.Rulesets.Osu.Skinning protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + + if (drawableSpinner != null) + drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; } } } From 4f0c0ea5f9bb4d3dc7b671af349cf6ab87d15313 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Sep 2020 18:16:19 +0900 Subject: [PATCH 3446/6909] Fix hit samples playing while paused / seeking in the editor --- .../Objects/Drawables/DrawableHitObject.cs | 4 ++-- osu.Game/Screens/Edit/Editor.cs | 1 + osu.Game/Screens/Edit/EditorClock.cs | 22 +++++++++++++++++-- osu.Game/Screens/Play/GameplayClock.cs | 2 +- osu.Game/Screens/Play/ISeekableClock.cs | 13 +++++++++++ 5 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Screens/Play/ISeekableClock.cs diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 7c05bc9aa7..5b26607bf7 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -360,7 +360,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } [Resolved(canBeNull: true)] - private GameplayClock gameplayClock { get; set; } + private ISeekableClock seekableClock { get; set; } /// /// Calculate the position to be used for sample playback at a specified X position (0..1). @@ -377,7 +377,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Whether samples should currently be playing. Will be false during seek operations. /// - protected bool ShouldPlaySamples => gameplayClock?.IsSeeking != true; + protected bool ShouldPlaySamples => seekableClock?.IsSeeking != true; /// /// Plays all the hit sounds for this . diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index fd090e0959..1f5e261588 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -107,6 +107,7 @@ namespace osu.Game.Screens.Edit UpdateClockSource(); dependencies.CacheAs(clock); + dependencies.CacheAs(clock); AddInternal(clock); // todo: remove caching of this and consume via editorBeatmap? diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index ec203df064..ebc73c2bb8 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -7,17 +7,18 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Transforms; -using osu.Framework.Utils; using osu.Framework.Timing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Play; 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 : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock + public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISeekableClock { public IBindable Track => track; @@ -211,8 +212,25 @@ namespace osu.Game.Screens.Edit private const double transform_time = 300; + public bool IsSeeking { get; private set; } + + protected override void Update() + { + base.Update(); + + if (IsSeeking) + { + // we are either running a seek tween or doing an immediate seek. + // in the case of an immediate seek the seeking bool will be set to false after one update. + // this allows for silencing hit sounds and the likes. + IsSeeking = Transforms.Any(); + } + } + public void SeekTo(double seekDestination) { + IsSeeking = true; + if (IsRunning) Seek(seekDestination); else diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 4f2cf5005c..b10e50882c 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play /// , as this should only be done once to ensure accuracy. /// /// - public class GameplayClock : IFrameBasedClock + public class GameplayClock : IFrameBasedClock, ISeekableClock { private readonly IFrameBasedClock underlyingClock; diff --git a/osu.Game/Screens/Play/ISeekableClock.cs b/osu.Game/Screens/Play/ISeekableClock.cs new file mode 100644 index 0000000000..9d992a45fd --- /dev/null +++ b/osu.Game/Screens/Play/ISeekableClock.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.Screens.Play +{ + public interface ISeekableClock + { + /// + /// Whether an ongoing seek operation is active. + /// + bool IsSeeking { get; } + } +} From 40a4654ef91c2ec9f92cf8b944eb48edd1ab9972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Mon, 28 Sep 2020 12:21:43 +0200 Subject: [PATCH 3447/6909] Invert spinnerNoBlink to spinnerBlink locally --- osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index ada3a825d0..cce50b24ac 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning private Sprite metreSprite; private Container metre; - private bool spinnerNoBlink; + private bool spinnerBlink; private const float sprite_scale = 1 / 1.6f; private const float final_metre_height = 692 * sprite_scale; @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) { - spinnerNoBlink = source.GetConfig(LegacySetting.SpinnerNoBlink)?.Value ?? false; + spinnerBlink = !source.GetConfig(LegacySetting.SpinnerNoBlink)?.Value ?? true; drawableSpinner = (DrawableSpinner)drawableObject; @@ -125,12 +125,12 @@ namespace osu.Game.Rulesets.Osu.Skinning progress *= 100; // the spinner should still blink at 100% progress. - if (!spinnerNoBlink) + if (spinnerBlink) progress = Math.Min(99, progress); int barCount = (int)progress / 10; - if (!spinnerNoBlink && RNG.NextBool(((int)progress % 10) / 10f)) + if (spinnerBlink && RNG.NextBool(((int)progress % 10) / 10f)) barCount++; return (float)barCount / total_bars * final_metre_height; From 54852991f36f2136dd5240058e5db758f81e65ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Mon, 28 Sep 2020 12:24:30 +0200 Subject: [PATCH 3448/6909] Move SpinnerNoBlink to OsuSkinConfiguration --- osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 3 +-- osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs | 3 ++- osu.Game/Skinning/LegacySkinConfiguration.cs | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index cce50b24ac..63cd48676e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -12,7 +12,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; -using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Rulesets.Osu.Skinning { @@ -34,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) { - spinnerBlink = !source.GetConfig(LegacySetting.SpinnerNoBlink)?.Value ?? true; + spinnerBlink = !source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value ?? true; drawableSpinner = (DrawableSpinner)drawableObject; diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs index e034e14eb0..63c9b53278 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuSkinConfiguration.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu.Skinning CursorRotate, HitCircleOverlayAboveNumber, HitCircleOverlayAboveNumer, // Some old skins will have this typo - SpinnerFrequencyModulate + SpinnerFrequencyModulate, + SpinnerNoBlink } } diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index a0e8fb2f92..828804b9cb 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.cs @@ -18,8 +18,7 @@ namespace osu.Game.Skinning ComboPrefix, ComboOverlap, AnimationFramerate, - LayeredHitSounds, - SpinnerNoBlink + LayeredHitSounds } } } From 0900661b23e0cd3aea39142d551d442e8f084c91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Sep 2020 16:34:04 +0200 Subject: [PATCH 3449/6909] Use IsHit for strong hit instead of checking result type --- osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index f24f75f097..58dfa1747a 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning if (hit == null || nestedStrongHit == null) return false; - return hit.Result.Type == nestedStrongHit.Result.Type; + return nestedStrongHit.IsHit; } } } From f6f267a43a1651deaddb1f2a013b6efdde043a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Sep 2020 16:38:30 +0200 Subject: [PATCH 3450/6909] Switch to strong sprite exactly once --- osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index 58dfa1747a..cd5c9c757f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -17,6 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning private DrawableHit hit; private DrawableStrongNestedHit nestedStrongHit; + private bool switchedToStrongSprite; /// /// Creates a new legacy hit explosion. @@ -84,16 +85,17 @@ namespace osu.Game.Rulesets.Taiko.Skinning { base.Update(); - if (shouldSwitchToStrongSprite() && strongSprite != null) + if (shouldSwitchToStrongSprite() && !switchedToStrongSprite) { sprite.FadeOut(50, Easing.OutQuint); strongSprite.FadeIn(50, Easing.OutQuint); + switchedToStrongSprite = true; } } private bool shouldSwitchToStrongSprite() { - if (hit == null || nestedStrongHit == null) + if (hit == null || nestedStrongHit == null || strongSprite == null) return false; return nestedStrongHit.IsHit; From 2fb9a5d7342e63569dff7705450539ece330736c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Sep 2020 16:59:33 +0200 Subject: [PATCH 3451/6909] Remove no longer required field --- osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs index cd5c9c757f..19493271be 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs @@ -15,7 +15,6 @@ namespace osu.Game.Rulesets.Taiko.Skinning private readonly Drawable sprite; private readonly Drawable strongSprite; - private DrawableHit hit; private DrawableStrongNestedHit nestedStrongHit; private bool switchedToStrongSprite; @@ -58,11 +57,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning })); } - if (judgedObject is DrawableHit h) - { - hit = h; + if (judgedObject is DrawableHit hit) nestedStrongHit = hit.NestedHitObjects.SingleOrDefault() as DrawableStrongNestedHit; - } } protected override void LoadComplete() @@ -95,7 +91,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning private bool shouldSwitchToStrongSprite() { - if (hit == null || nestedStrongHit == null || strongSprite == null) + if (nestedStrongHit == null || strongSprite == null) return false; return nestedStrongHit.IsHit; From 6efc4c42505bbfd373a196a8639b5954869335fc Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 28 Sep 2020 19:04:39 +0200 Subject: [PATCH 3452/6909] Cache performance calculations to prevent recomputations. --- osu.Game/Scoring/ScorePerformanceManager.cs | 78 ++++++++++++++++--- .../Statistics/PerformanceStatistic.cs | 11 +-- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index c8fec3b40c..0a57ccbd1f 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -1,17 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; namespace osu.Game.Scoring { public class ScorePerformanceManager : Component { + private readonly ConcurrentDictionary performanceCache = new ConcurrentDictionary(); + [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -22,18 +27,73 @@ namespace osu.Game.Scoring /// An optional to cancel the operation. public async Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) { + if (score.PP.HasValue) + return score.PP.Value; + + if (tryGetExisting(score, out var perf, out var lookupKey)) + return perf; + return await Task.Factory.StartNew(() => { - if (token.IsCancellationRequested) - return default; - - var beatmap = beatmapManager.GetWorkingBeatmap(score.Beatmap); - - var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(beatmap, score); - var total = calculator.Calculate(); - - return total; + return computePerformance(score, lookupKey, token); }, token); } + + private bool tryGetExisting(ScoreInfo score, out double performance, out PerformanceCacheLookup lookupKey) + { + lookupKey = new PerformanceCacheLookup(score); + + return performanceCache.TryGetValue(lookupKey, out performance); + } + + private double computePerformance(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) + { + var beatmap = beatmapManager.GetWorkingBeatmap(score.Beatmap); + + if (token.IsCancellationRequested) + return default; + + var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(beatmap, score); + var total = calculator.Calculate(); + + performanceCache[lookupKey] = total; + + return total; + } + + public readonly struct PerformanceCacheLookup + { + public readonly double Accuracy; + public readonly int BeatmapId; + public readonly long TotalScore; + public readonly int Combo; + public readonly Mod[] Mods; + public readonly int RulesetId; + + public PerformanceCacheLookup(ScoreInfo info) + { + Accuracy = info.Accuracy; + BeatmapId = info.Beatmap.ID; + TotalScore = info.TotalScore; + Combo = info.Combo; + Mods = info.Mods; + RulesetId = info.Ruleset.ID.Value; + } + + public override int GetHashCode() + { + var hash = new HashCode(); + + hash.Add(Accuracy); + hash.Add(BeatmapId); + hash.Add(TotalScore); + hash.Add(Combo); + hash.Add(RulesetId); + foreach (var mod in Mods) + hash.Add(mod); + + return hash.ToHashCode(); + } + } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index e014258fd4..6c2ad5844b 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -25,15 +25,8 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics [BackgroundDependencyLoader] private void load(ScorePerformanceManager performanceManager) { - if (score.PP.HasValue) - { - performance.Value = (int)score.PP.Value; - } - else - { - performanceManager.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => performance.Value = (int)t.Result), cancellationTokenSource.Token); - } + performanceManager.CalculatePerformanceAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => performance.Value = (int)t.Result), cancellationTokenSource.Token); } public override void Appear() From 585b857a0c7f417a5fdfeb877d032324b4879f13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 12:17:38 +0900 Subject: [PATCH 3453/6909] Handle paused state correctly --- osu.Game/Screens/Edit/EditorClock.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index ebc73c2bb8..99e5044b1f 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -220,10 +220,12 @@ namespace osu.Game.Screens.Edit if (IsSeeking) { + bool isPaused = track.Value?.IsRunning != true; + // we are either running a seek tween or doing an immediate seek. // in the case of an immediate seek the seeking bool will be set to false after one update. // this allows for silencing hit sounds and the likes. - IsSeeking = Transforms.Any(); + IsSeeking = isPaused || Transforms.Any(); } } From d6f3beffb648f1a0e059a5d641984522c799b77b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 12:45:20 +0900 Subject: [PATCH 3454/6909] Use existing bindable flow instead --- .../Objects/Drawables/DrawableSlider.cs | 5 +-- .../Objects/Drawables/DrawableSpinner.cs | 5 +-- .../Gameplay/TestSceneSkinnableSound.cs | 2 +- .../Objects/Drawables/DrawableHitObject.cs | 9 ++---- osu.Game/Screens/Edit/Editor.cs | 2 +- osu.Game/Screens/Edit/EditorClock.cs | 29 +++++++++++++---- osu.Game/Screens/Play/GameplayClock.cs | 4 ++- .../Screens/Play/ISamplePlaybackDisabler.cs | 20 ++++++++++++ osu.Game/Screens/Play/ISeekableClock.cs | 13 -------- osu.Game/Skinning/SkinnableSound.cs | 32 +++++++++++-------- 10 files changed, 70 insertions(+), 51 deletions(-) create mode 100644 osu.Game/Screens/Play/ISamplePlaybackDisabler.cs delete mode 100644 osu.Game/Screens/Play/ISeekableClock.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 07f40f763b..68f203db47 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -112,10 +112,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void updateSlidingSample(ValueChangedEvent tracking) { - // note that samples will not start playing if exiting a seek operation in the middle of a slider. - // may be something we want to address at a later point, but not so easy to make happen right now - // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update). - if (tracking.NewValue && ShouldPlaySamples) + if (tracking.NewValue) slidingSample?.Play(); else slidingSample?.Stop(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a57bb466c7..b2a706833c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -113,10 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void updateSpinningSample(ValueChangedEvent tracking) { - // note that samples will not start playing if exiting a seek operation in the middle of a spinner. - // may be something we want to address at a later point, but not so easy to make happen right now - // (SkinnableSound would need to expose whether the sample is already playing and this logic would need to run in Update). - if (tracking.NewValue && ShouldPlaySamples) + if (tracking.NewValue) { spinningSample?.Play(); spinningSample?.VolumeTo(1, 200); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index ed75d83151..8b37cbd06f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -22,7 +22,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneSkinnableSound : OsuTestScene { - [Cached] + [Cached(typeof(ISamplePlaybackDisabler))] private GameplayClock gameplayClock = new GameplayClock(new FramedClock()); private TestSkinSourceContainer skinSource; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5b26607bf7..796b8f7aae 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -360,7 +360,7 @@ namespace osu.Game.Rulesets.Objects.Drawables } [Resolved(canBeNull: true)] - private ISeekableClock seekableClock { get; set; } + private ISamplePlaybackDisabler samplePlaybackDisabler { get; set; } /// /// Calculate the position to be used for sample playback at a specified X position (0..1). @@ -374,18 +374,13 @@ namespace osu.Game.Rulesets.Objects.Drawables return balance_adjust_amount * (userPositionalHitSounds.Value ? position - 0.5f : 0); } - /// - /// Whether samples should currently be playing. Will be false during seek operations. - /// - protected bool ShouldPlaySamples => seekableClock?.IsSeeking != true; - /// /// Plays all the hit sounds for this . /// This is invoked automatically when this is hit. /// public virtual void PlaySamples() { - if (Samples != null && ShouldPlaySamples) + if (Samples != null) { Samples.Balance.Value = CalculateSamplePlaybackBalance(SamplePlaybackPosition); Samples.Play(); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 1f5e261588..a0692d94e6 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit UpdateClockSource(); dependencies.CacheAs(clock); - dependencies.CacheAs(clock); + dependencies.CacheAs(clock); AddInternal(clock); // todo: remove caching of this and consume via editorBeatmap? diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 99e5044b1f..4b7cd82637 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -18,7 +18,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 : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISeekableClock + public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISamplePlaybackDisabler { public IBindable Track => track; @@ -32,6 +32,10 @@ namespace osu.Game.Screens.Edit private readonly DecoupleableInterpolatingFramedClock underlyingClock; + public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; + + private readonly Bindable samplePlaybackDisabled = new Bindable(); + public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor) { @@ -167,11 +171,14 @@ namespace osu.Game.Screens.Edit public void Stop() { + samplePlaybackDisabled.Value = true; underlyingClock.Stop(); } public bool Seek(double position) { + samplePlaybackDisabled.Value = true; + ClearTransforms(); return underlyingClock.Seek(position); } @@ -212,26 +219,34 @@ namespace osu.Game.Screens.Edit private const double transform_time = 300; - public bool IsSeeking { get; private set; } - protected override void Update() { base.Update(); - if (IsSeeking) + updateSeekingState(); + } + + private void updateSeekingState() + { + if (samplePlaybackDisabled.Value) { - bool isPaused = track.Value?.IsRunning != true; + if (track.Value?.IsRunning != true) + { + // seeking in the editor can happen while the track isn't running. + // in this case we always want to expose ourselves as seeking (to avoid sample playback). + return; + } // we are either running a seek tween or doing an immediate seek. // in the case of an immediate seek the seeking bool will be set to false after one update. // this allows for silencing hit sounds and the likes. - IsSeeking = isPaused || Transforms.Any(); + samplePlaybackDisabled.Value = Transforms.Any(); } } public void SeekTo(double seekDestination) { - IsSeeking = true; + samplePlaybackDisabled.Value = true; if (IsRunning) Seek(seekDestination); diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index b10e50882c..da4648fd2b 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play /// , as this should only be done once to ensure accuracy. /// /// - public class GameplayClock : IFrameBasedClock, ISeekableClock + public class GameplayClock : IFrameBasedClock, ISamplePlaybackDisabler { private readonly IFrameBasedClock underlyingClock; @@ -48,5 +48,7 @@ namespace osu.Game.Screens.Play public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; public IClock Source => underlyingClock; + + public IBindable SamplePlaybackDisabled => IsPaused; } } diff --git a/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs b/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs new file mode 100644 index 0000000000..83e89d654b --- /dev/null +++ b/osu.Game/Screens/Play/ISamplePlaybackDisabler.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.Bindables; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Play +{ + /// + /// Allows a component to disable sample playback dynamically as required. + /// Handled by . + /// + public interface ISamplePlaybackDisabler + { + /// + /// Whether sample playback should be disabled (or paused for looping samples). + /// + IBindable SamplePlaybackDisabled { get; } + } +} diff --git a/osu.Game/Screens/Play/ISeekableClock.cs b/osu.Game/Screens/Play/ISeekableClock.cs deleted file mode 100644 index 9d992a45fd..0000000000 --- a/osu.Game/Screens/Play/ISeekableClock.cs +++ /dev/null @@ -1,13 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -namespace osu.Game.Screens.Play -{ - public interface ISeekableClock - { - /// - /// Whether an ongoing seek operation is active. - /// - bool IsSeeking { get; } - } -} diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index ba14049b41..704ba099c1 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -50,25 +50,28 @@ namespace osu.Game.Skinning InternalChild = samplesContainer = new AudioContainer(); } - private Bindable gameplayClockPaused; + private readonly IBindable samplePlaybackDisabled = new Bindable(); [BackgroundDependencyLoader(true)] - private void load(GameplayClock gameplayClock) + private void load(ISamplePlaybackDisabler samplePlaybackDisabler) { // if in a gameplay context, pause sample playback when gameplay is paused. - gameplayClockPaused = gameplayClock?.IsPaused.GetBoundCopy(); - gameplayClockPaused?.BindValueChanged(paused => + if (samplePlaybackDisabler != null) { - if (requestedPlaying) + samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); + samplePlaybackDisabled.BindValueChanged(disabled => { - if (paused.NewValue) - stop(); - // it's not easy to know if a sample has finished playing (to end). - // to keep things simple only resume playing looping samples. - else if (Looping) - play(); - } - }); + if (requestedPlaying) + { + if (disabled.NewValue) + stop(); + // it's not easy to know if a sample has finished playing (to end). + // to keep things simple only resume playing looping samples. + else if (Looping) + play(); + } + }); + } } private bool looping; @@ -94,6 +97,9 @@ namespace osu.Game.Skinning private void play() { + if (samplePlaybackDisabled.Value) + return; + samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) From c5f6b77bbaa03be219cf5978391852ff5019287a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 13:42:17 +0900 Subject: [PATCH 3455/6909] Add missing cached type --- osu.Game/Screens/Play/GameplayClockContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 7a9cb3dddd..cc25a733f1 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -51,6 +51,7 @@ namespace osu.Game.Screens.Play /// The final clock which is exposed to underlying components. /// [Cached] + [Cached(typeof(ISamplePlaybackDisabler))] public readonly GameplayClock GameplayClock; private Bindable userAudioOffset; From 74e74e1c31acdd58bfb258ca6a3dc0b291c548e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 14:20:41 +0900 Subject: [PATCH 3456/6909] Fix pause loop sound not working because paused --- osu.Game/Screens/Play/PauseOverlay.cs | 12 +++++++++++- osu.Game/Skinning/SkinnableSound.cs | 6 ++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 65f34aba3e..9494971f8a 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); - AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("pause-loop")) + AddInternal(pauseLoop = new UnpausableSkinnableSound(new SampleInfo("pause-loop")) { Looping = true, Volume = { Value = 0 } @@ -54,5 +54,15 @@ namespace osu.Game.Screens.Play pauseLoop.VolumeTo(0, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop()); } + + private class UnpausableSkinnableSound : SkinnableSound + { + protected override bool PlayWhenPaused => true; + + public UnpausableSkinnableSound(SampleInfo sampleInfo) + : base(sampleInfo) + { + } + } } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 704ba099c1..f3ab8b86bc 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -37,6 +37,8 @@ namespace osu.Game.Skinning /// protected bool PlayWhenZeroVolume => Looping; + protected virtual bool PlayWhenPaused => false; + private readonly AudioContainer samplesContainer; public SkinnableSound(ISampleInfo hitSamples) @@ -63,7 +65,7 @@ namespace osu.Game.Skinning { if (requestedPlaying) { - if (disabled.NewValue) + if (disabled.NewValue && !PlayWhenPaused) stop(); // it's not easy to know if a sample has finished playing (to end). // to keep things simple only resume playing looping samples. @@ -97,7 +99,7 @@ namespace osu.Game.Skinning private void play() { - if (samplePlaybackDisabled.Value) + if (samplePlaybackDisabled.Value && !PlayWhenPaused) return; samplesContainer.ForEach(c => From 136843c8e450507ad0527622af851d648afb1545 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 14:09:51 +0900 Subject: [PATCH 3457/6909] Make DrawableStoryboardSample a SkinnableSound Allows sharing pause logic with gameplay samples. --- .../Gameplay/TestSceneStoryboardSamples.cs | 15 ++-- osu.Game/Rulesets/Mods/IApplicableToSample.cs | 4 +- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 4 +- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 6 +- osu.Game/Screens/Play/Player.cs | 80 ++++++++++--------- osu.Game/Skinning/SkinnableSound.cs | 39 ++++----- .../Drawables/DrawableStoryboardSample.cs | 41 ++++------ 7 files changed, 94 insertions(+), 95 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index a690eb3b59..d46769a7c0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Graphics.Audio; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Audio; @@ -106,9 +108,14 @@ namespace osu.Game.Tests.Gameplay Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); SelectedMods.Value = new[] { testedMod }; - Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0)); + var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); - gameplayContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) + Add(gameplayContainer = new GameplayClockContainer(Beatmap.Value, 0) + { + Child = beatmapSkinSourceContainer + }); + + beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) { Clock = gameplayContainer.GameplayClock }); @@ -116,7 +123,7 @@ namespace osu.Game.Tests.Gameplay AddStep("start", () => gameplayContainer.Start()); - AddAssert("sample playback rate matches mod rates", () => sample.Channel.AggregateFrequency.Value == expectedRate); + AddAssert("sample playback rate matches mod rates", () => sample.ChildrenOfType().First().AggregateFrequency.Value == expectedRate); } private class TestSkin : LegacySkin @@ -168,8 +175,6 @@ namespace osu.Game.Tests.Gameplay : base(sampleInfo) { } - - public new SampleChannel Channel => base.Channel; } } } diff --git a/osu.Game/Rulesets/Mods/IApplicableToSample.cs b/osu.Game/Rulesets/Mods/IApplicableToSample.cs index 559d127cfc..50a6d501b6 100644 --- a/osu.Game/Rulesets/Mods/IApplicableToSample.cs +++ b/osu.Game/Rulesets/Mods/IApplicableToSample.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.Framework.Audio.Sample; +using osu.Framework.Graphics.Audio; namespace osu.Game.Rulesets.Mods { @@ -10,6 +10,6 @@ namespace osu.Game.Rulesets.Mods /// public interface IApplicableToSample : IApplicableMod { - void ApplyToSample(SampleChannel sample); + void ApplyToSample(DrawableSample sample); } } diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index fec21764b0..2150b0fb68 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -2,9 +2,9 @@ // 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; +using osu.Framework.Graphics.Audio; namespace osu.Game.Rulesets.Mods { @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Mods track.AddAdjustment(AdjustableProperty.Tempo, SpeedChange); } - public virtual void ApplyToSample(SampleChannel sample) + public virtual void ApplyToSample(DrawableSample sample) { sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 20c8d0f3e7..4d43ae73d3 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -6,11 +6,11 @@ using System.Linq; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Graphics.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Objects; -using osu.Framework.Audio.Sample; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods { @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Mods AdjustPitch.TriggerChange(); } - public void ApplyToSample(SampleChannel sample) + public void ApplyToSample(DrawableSample sample) { sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8e2ed583f2..175722c44e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -191,9 +191,25 @@ namespace osu.Game.Screens.Play dependencies.CacheAs(gameplayBeatmap); - addUnderlayComponents(GameplayClockContainer); - addGameplayComponents(GameplayClockContainer, Beatmap.Value, playableBeatmap); - addOverlayComponents(GameplayClockContainer, Beatmap.Value); + var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.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, 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. + GameplayClockContainer.Add(beatmapSkinProvider.WithChild(rulesetSkinProvider)); + + rulesetSkinProvider.AddRange(new[] + { + // underlay and gameplay should have access the to skinning sources. + createUnderlayComponents(), + createGameplayComponents(Beatmap.Value, playableBeatmap) + }); + + // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. + GameplayClockContainer.Add(createOverlayComponents(Beatmap.Value)); if (!DrawableRuleset.AllowGameplayOverlays) { @@ -238,45 +254,31 @@ namespace osu.Game.Screens.Play breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } - private void addUnderlayComponents(Container target) + private Drawable createUnderlayComponents() => + DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; + + private Drawable createGameplayComponents(WorkingBeatmap working, IBeatmap playableBeatmap) => new ScalingContainer(ScalingMode.Gameplay) { - target.Add(DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }); - } - - 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, 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. - target.Add(new ScalingContainer(ScalingMode.Gameplay) - .WithChild(beatmapSkinProvider - .WithChild(target = rulesetSkinProvider))); - - target.AddRange(new Drawable[] + Children = new Drawable[] { - DrawableRuleset, + DrawableRuleset.With(r => + r.FrameStableComponents.Children = new Drawable[] + { + ScoreProcessor, + HealthProcessor, + breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) + { + Breaks = working.Beatmap.Breaks + } + }), new ComboEffects(ScoreProcessor) - }); + } + }; - DrawableRuleset.FrameStableComponents.AddRange(new Drawable[] - { - ScoreProcessor, - HealthProcessor, - breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) - { - Breaks = working.Beatmap.Breaks - } - }); - } - - private void addOverlayComponents(Container target, WorkingBeatmap working) + private Drawable createOverlayComponents(WorkingBeatmap working) => new Container { - target.AddRange(new[] + RelativeSizeAxes = Axes.Both, + Children = new[] { DimmableStoryboard.OverlayLayerContainer.CreateProxy(), BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) @@ -342,8 +344,8 @@ namespace osu.Game.Screens.Play }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, - }); - } + } + }; private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index f3ab8b86bc..07b759843c 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -22,7 +22,10 @@ namespace osu.Game.Skinning [Resolved] private ISampleStore samples { get; set; } - private bool requestedPlaying; + /// + /// Whether playback of this sound has been requested, regardless of whether it could be played or not (due to being paused, for instance). + /// + protected bool PlaybackRequested; public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; @@ -39,7 +42,7 @@ namespace osu.Game.Skinning protected virtual bool PlayWhenPaused => false; - private readonly AudioContainer samplesContainer; + protected readonly AudioContainer SamplesContainer; public SkinnableSound(ISampleInfo hitSamples) : this(new[] { hitSamples }) @@ -49,7 +52,7 @@ namespace osu.Game.Skinning public SkinnableSound(IEnumerable hitSamples) { this.hitSamples = hitSamples.ToArray(); - InternalChild = samplesContainer = new AudioContainer(); + InternalChild = SamplesContainer = new AudioContainer(); } private readonly IBindable samplePlaybackDisabled = new Bindable(); @@ -63,7 +66,7 @@ namespace osu.Game.Skinning samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); samplePlaybackDisabled.BindValueChanged(disabled => { - if (requestedPlaying) + if (PlaybackRequested) { if (disabled.NewValue && !PlayWhenPaused) stop(); @@ -87,13 +90,13 @@ namespace osu.Game.Skinning looping = value; - samplesContainer.ForEach(c => c.Looping = looping); + SamplesContainer.ForEach(c => c.Looping = looping); } } public void Play() { - requestedPlaying = true; + PlaybackRequested = true; play(); } @@ -102,7 +105,7 @@ namespace osu.Game.Skinning if (samplePlaybackDisabled.Value && !PlayWhenPaused) return; - samplesContainer.ForEach(c => + SamplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) c.Play(); @@ -111,13 +114,13 @@ namespace osu.Game.Skinning public void Stop() { - requestedPlaying = false; + PlaybackRequested = false; stop(); } private void stop() { - samplesContainer.ForEach(c => c.Stop()); + SamplesContainer.ForEach(c => c.Stop()); } protected override void SkinChanged(ISkinSource skin, bool allowFallback) @@ -146,7 +149,7 @@ namespace osu.Game.Skinning return ch; }).Where(c => c != null); - samplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); + SamplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); // Start playback internally for the new samples if the previous ones were playing beforehand. if (wasPlaying) @@ -155,24 +158,24 @@ namespace osu.Game.Skinning #region Re-expose AudioContainer - public BindableNumber Volume => samplesContainer.Volume; + public BindableNumber Volume => SamplesContainer.Volume; - public BindableNumber Balance => samplesContainer.Balance; + public BindableNumber Balance => SamplesContainer.Balance; - public BindableNumber Frequency => samplesContainer.Frequency; + public BindableNumber Frequency => SamplesContainer.Frequency; - public BindableNumber Tempo => samplesContainer.Tempo; + public BindableNumber Tempo => SamplesContainer.Tempo; public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => samplesContainer.AddAdjustment(type, adjustBindable); + => SamplesContainer.AddAdjustment(type, adjustBindable); public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => samplesContainer.RemoveAdjustment(type, adjustBindable); + => SamplesContainer.RemoveAdjustment(type, adjustBindable); public void RemoveAllAdjustments(AdjustableProperty type) - => samplesContainer.RemoveAllAdjustments(type); + => SamplesContainer.RemoveAllAdjustments(type); - public bool IsPlaying => samplesContainer.Any(s => s.Playing); + public bool IsPlaying => SamplesContainer.Any(s => s.Playing); #endregion } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 119c48836b..83e3b8203e 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -4,15 +4,13 @@ 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; +using osu.Game.Skinning; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardSample : Component + public class DrawableStoryboardSample : SkinnableSound { /// /// The amount of time allowable beyond the start time of the sample, for the sample to start. @@ -21,38 +19,37 @@ namespace osu.Game.Storyboards.Drawables private readonly StoryboardSampleInfo sampleInfo; - protected SampleChannel Channel { get; private set; } - public override bool RemoveWhenNotAlive => false; public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo) + : base(sampleInfo) { this.sampleInfo = sampleInfo; LifetimeStart = sampleInfo.StartTime; } - [BackgroundDependencyLoader] - private void load(IBindable beatmap, IBindable> mods) - { - Channel = beatmap.Value.Skin.GetSample(sampleInfo); - if (Channel == null) - return; + [Resolved] + private IBindable> mods { get; set; } - Channel.Volume.Value = sampleInfo.Volume / 100.0; + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); foreach (var mod in mods.Value.OfType()) - mod.ApplyToSample(Channel); + { + foreach (var sample in SamplesContainer) + mod.ApplyToSample(sample); + } } protected override void Update() { base.Update(); - // TODO: this logic will need to be consolidated with other game samples like hit sounds. if (Time.Current < sampleInfo.StartTime) { // We've rewound before the start time of the sample - Channel?.Stop(); + 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) @@ -63,8 +60,8 @@ 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(); + if (!PlaybackRequested && Time.Current - sampleInfo.StartTime < allowable_late_start) + 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) @@ -72,13 +69,5 @@ namespace osu.Game.Storyboards.Drawables LifetimeEnd = sampleInfo.StartTime; } } - - protected override void Dispose(bool isDisposing) - { - Channel?.Stop(); - Channel = null; - - base.Dispose(isDisposing); - } } } From 56c8e4dacfba027675db746c668ac2541efddda0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 14:18:54 +0900 Subject: [PATCH 3458/6909] Fix failing tests --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 420bf29429..ac0e8eb0d4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -158,7 +158,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestQuickRetryFromFailedGameplay() { AddUntilStep("wait for fail", () => Player.HasFailed); - AddStep("quick retry", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke()); + AddStep("quick retry", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); } @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestQuickExitFromFailedGameplay() { AddUntilStep("wait for fail", () => Player.HasFailed); - AddStep("quick exit", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke()); + AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); } @@ -183,7 +183,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestQuickExitFromGameplay() { - AddStep("quick exit", () => Player.GameplayClockContainer.OfType().First().Action?.Invoke()); + AddStep("quick exit", () => Player.GameplayClockContainer.ChildrenOfType().First().Action?.Invoke()); confirmExited(); } From 1a70002cdd0bfe2c0fafd2d5f8a9e552ebf5c267 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 14:41:50 +0900 Subject: [PATCH 3459/6909] Split ignore into hit/miss --- osu.Game/Rulesets/Scoring/HitResult.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 1de62cf8e5..f5f2e269f0 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -13,12 +13,9 @@ namespace osu.Game.Rulesets.Scoring /// Indicates that the object has not been judged yet. /// [Description(@"")] - [Order(13)] + [Order(14)] None, - [Order(12)] - Ignore, - /// /// Indicates that the object has been judged as a miss. /// @@ -95,6 +92,12 @@ namespace osu.Game.Rulesets.Scoring [Description("L Bonus")] [Order(8)] LargeBonus, + + [Order(13)] + IgnoreMiss, + + [Order(12)] + IgnoreHit, } public static class HitResultExtensions @@ -151,7 +154,7 @@ namespace osu.Game.Rulesets.Scoring switch (result) { case HitResult.None: - case HitResult.Ignore: + case HitResult.IgnoreMiss: case HitResult.Miss: case HitResult.SmallTickMiss: case HitResult.LargeTickMiss: @@ -165,6 +168,6 @@ namespace osu.Game.Rulesets.Scoring /// /// Whether a is scorable. /// - public static bool IsScorable(this HitResult result) => result > HitResult.Ignore; + public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss; } } From 5d1c3773790beb4b40ae02b11a230ddef47d7e05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 15:07:55 +0900 Subject: [PATCH 3460/6909] Fix HitObject samples getting stuck in a playing state on seeking far into the future --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 6 ++++++ osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 6 ++++++ osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 7 +++++++ osu.Game/Skinning/SkinnableSound.cs | 3 ++- 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 68f203db47..ba328e15c6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -110,6 +110,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + public override void StopAllSamples() + { + base.StopAllSamples(); + slidingSample?.Stop(); + } + private void updateSlidingSample(ValueChangedEvent tracking) { if (tracking.NewValue) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index b2a706833c..9e552981ea 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -124,6 +124,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + public override void StopAllSamples() + { + base.StopAllSamples(); + spinningSample?.Stop(); + } + protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 796b8f7aae..56e3a98ca3 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -387,6 +387,11 @@ namespace osu.Game.Rulesets.Objects.Drawables } } + /// + /// Stops playback of all samples. Automatically called when 's lifetime has been exceeded. + /// + public virtual void StopAllSamples() => Samples?.Stop(); + protected override void Update() { base.Update(); @@ -455,6 +460,8 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var nested in NestedHitObjects) nested.OnKilled(); + StopAllSamples(); + UpdateResult(false); } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 07b759843c..c1f0b78d3b 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -73,7 +73,8 @@ namespace osu.Game.Skinning // it's not easy to know if a sample has finished playing (to end). // to keep things simple only resume playing looping samples. else if (Looping) - play(); + // schedule so we don't start playing a sample which is no longer alive. + Schedule(play); } }); } From 2f26728cdb6a28235a9e15c9e1acd0296938fb05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 15:29:56 +0900 Subject: [PATCH 3461/6909] Add test coverage of editor sample playback --- .../Editing/TestSceneEditorSamplePlayback.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs new file mode 100644 index 0000000000..039a21fd94 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs @@ -0,0 +1,47 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Audio; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorSamplePlayback : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + [Test] + public void TestSlidingSampleStopsOnSeek() + { + DrawableSlider slider = null; + DrawableSample[] samples = null; + + AddStep("get first slider", () => + { + slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); + samples = slider.ChildrenOfType().ToArray(); + }); + + AddStep("start playback", () => EditorClock.Start()); + + AddUntilStep("wait for slider sliding then seek", () => + { + if (!slider.Tracking.Value) + return false; + + if (!samples.Any(s => s.Playing)) + return false; + + EditorClock.Seek(20000); + return true; + }); + + AddAssert("slider samples are not playing", () => samples.Length == 5 && samples.All(s => s.Played && !s.Playing)); + } + } +} From cee58e89a34e6e072a5d6adeaefafb90bec7aa0e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 16:32:02 +0900 Subject: [PATCH 3462/6909] Pad hit results --- osu.Game/Rulesets/Scoring/HitResult.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index f5f2e269f0..6abf91e9d3 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Scoring /// [Description(@"")] [Order(14)] - None, + None = 0, /// /// Indicates that the object has been judged as a miss. @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Scoring /// [Description(@"Miss")] [Order(5)] - Miss, + Miss = 64, [Description(@"Meh")] [Order(4)] @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates small tick miss. /// [Order(11)] - SmallTickMiss, + SmallTickMiss = 128, /// /// Indicates a small tick hit. @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a large tick miss. /// [Order(10)] - LargeTickMiss, + LargeTickMiss = 192, /// /// Indicates a large tick hit. @@ -84,17 +84,17 @@ namespace osu.Game.Rulesets.Scoring /// [Description("S Bonus")] [Order(9)] - SmallBonus, + SmallBonus = 254, /// /// Indicate a large bonus. /// [Description("L Bonus")] [Order(8)] - LargeBonus, + LargeBonus = 320, [Order(13)] - IgnoreMiss, + IgnoreMiss = 384, [Order(12)] IgnoreHit, From 07226c79b64918aeefcdb53df6dd339359700312 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 16:32:50 +0900 Subject: [PATCH 3463/6909] Add xmldocs --- osu.Game/Rulesets/Scoring/HitResult.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 6abf91e9d3..fc33ff9df2 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -87,15 +87,21 @@ namespace osu.Game.Rulesets.Scoring SmallBonus = 254, /// - /// Indicate a large bonus. + /// Indicates a large bonus. /// [Description("L Bonus")] [Order(8)] LargeBonus = 320, + /// + /// Indicates a miss that should be ignored for scoring purposes. + /// [Order(13)] IgnoreMiss = 384, + /// + /// Indicates a hit that should be ignored for scoring purposes. + /// [Order(12)] IgnoreHit, } From 519f376e7b5377b8965e05aa34815cee399428c4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 14:26:12 +0900 Subject: [PATCH 3464/6909] Standardise Judgement across all rulesets --- osu.Game/Rulesets/Judgements/Judgement.cs | 122 +++++++++++++++++++--- 1 file changed, 108 insertions(+), 14 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 9105b920ca..ea7a8d8d25 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.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; using osu.Game.Rulesets.Scoring; @@ -11,31 +12,69 @@ namespace osu.Game.Rulesets.Judgements /// public class Judgement { + /// + /// The score awarded for a small bonus. + /// + public const double SMALL_BONUS_SCORE = 10; + + /// + /// The score awarded for a large bonus. + /// + public const double LARGE_BONUS_SCORE = 50; + /// /// The default health increase for a maximum judgement, as a proportion of total health. /// By default, each maximum judgement restores 5% of total health. /// protected const double DEFAULT_MAX_HEALTH_INCREASE = 0.05; + /// + /// Whether this should affect the current combo. + /// + [Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328 + public virtual bool AffectsCombo => true; + + /// + /// Whether this should be counted as base (combo) or bonus score. + /// + [Obsolete("Has no effect. Use HitResult members instead (e.g. use small-tick or bonus to not affect combo).")] // Can be removed 20210328 + public virtual bool IsBonus => !AffectsCombo; + /// /// The maximum that can be achieved. /// public virtual HitResult MaxResult => HitResult.Perfect; /// - /// Whether this should affect the current combo. + /// The minimum that can be achieved - the inverse of . /// - public virtual bool AffectsCombo => true; + public HitResult MinResult + { + get + { + switch (MaxResult) + { + case HitResult.SmallBonus: + case HitResult.LargeBonus: + case HitResult.IgnoreHit: + return HitResult.IgnoreMiss; - /// - /// Whether this should be counted as base (combo) or bonus score. - /// - public virtual bool IsBonus => !AffectsCombo; + case HitResult.SmallTickHit: + return HitResult.SmallTickMiss; + + case HitResult.LargeTickHit: + return HitResult.LargeTickMiss; + + default: + return HitResult.Miss; + } + } + } /// /// The numeric score representation for the maximum achievable result. /// - public int MaxNumericResult => NumericResultFor(MaxResult); + public double MaxNumericResult => ToNumericResult(MaxResult); /// /// The health increase for the maximum achievable result. @@ -43,18 +82,19 @@ namespace osu.Game.Rulesets.Judgements public double MaxHealthIncrease => HealthIncreaseFor(MaxResult); /// - /// Retrieves the numeric score representation of a . + /// Retrieves the numeric score representation of a . /// - /// The to find the numeric score representation for. + /// The to find the numeric score representation for. /// The numeric score representation of . - protected virtual int NumericResultFor(HitResult result) => result > HitResult.Miss ? 1 : 0; + [Obsolete("Has no effect. Use ToNumericResult(HitResult) (standardised across all rulesets).")] // Can be removed 20210328 + protected virtual int NumericResultFor(HitResult result) => result == HitResult.Miss ? 0 : 1; /// /// Retrieves the numeric score representation of a . /// /// The to find the numeric score representation for. /// The numeric score representation of . - public int NumericResultFor(JudgementResult result) => NumericResultFor(result.Type); + public double NumericResultFor(JudgementResult result) => ToNumericResult(result.Type); /// /// Retrieves the numeric health increase of a . @@ -65,6 +105,21 @@ namespace osu.Game.Rulesets.Judgements { switch (result) { + default: + return 0; + + case HitResult.SmallTickHit: + return DEFAULT_MAX_HEALTH_INCREASE * 0.05; + + case HitResult.SmallTickMiss: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; + + case HitResult.LargeTickHit: + return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + + case HitResult.LargeTickMiss: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.1; + case HitResult.Miss: return -DEFAULT_MAX_HEALTH_INCREASE; @@ -83,8 +138,11 @@ namespace osu.Game.Rulesets.Judgements case HitResult.Perfect: return DEFAULT_MAX_HEALTH_INCREASE * 1.05; - default: - return 0; + case HitResult.SmallBonus: + return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + + case HitResult.LargeBonus: + return DEFAULT_MAX_HEALTH_INCREASE * 0.2; } } @@ -95,6 +153,42 @@ namespace osu.Game.Rulesets.Judgements /// The numeric health increase of . public double HealthIncreaseFor(JudgementResult result) => HealthIncreaseFor(result.Type); - public override string ToString() => $"AffectsCombo:{AffectsCombo} MaxResult:{MaxResult} MaxScore:{MaxNumericResult}"; + public override string ToString() => $"MaxResult:{MaxResult} MaxScore:{MaxNumericResult}"; + + public static double ToNumericResult(HitResult result) + { + switch (result) + { + default: + return 0; + + case HitResult.SmallTickHit: + return 1 / 30d; + + case HitResult.LargeTickHit: + return 1 / 10d; + + case HitResult.Meh: + return 1 / 6d; + + case HitResult.Ok: + return 1 / 3d; + + case HitResult.Good: + return 2 / 3d; + + case HitResult.Great: + return 1d; + + case HitResult.Perfect: + return 7 / 6d; + + case HitResult.SmallBonus: + return SMALL_BONUS_SCORE; + + case HitResult.LargeBonus: + return LARGE_BONUS_SCORE; + } + } } } From 4ca9a69de25dcf23cdfab050ed2632057ce50e19 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 14:26:36 +0900 Subject: [PATCH 3465/6909] Use new hit results in catch --- .../Judgements/CatchBananaJudgement.cs | 26 +------------------ .../Judgements/CatchDropletJudgement.cs | 12 +-------- .../Judgements/CatchJudgement.cs | 14 +--------- .../Judgements/CatchTinyDropletJudgement.cs | 26 +------------------ .../Drawables/DrawableCatchHitObject.cs | 3 +-- .../UI/CatchComboDisplay.cs | 4 +-- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 3 ++- 7 files changed, 9 insertions(+), 79 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs index a7449ba4e1..b919102215 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchBananaJudgement.cs @@ -8,31 +8,7 @@ namespace osu.Game.Rulesets.Catch.Judgements { public class CatchBananaJudgement : CatchJudgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 1100; - } - } - - protected override double HealthIncreaseFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return DEFAULT_MAX_HEALTH_INCREASE * 0.75; - } - } + public override HitResult MaxResult => HitResult.LargeBonus; public override bool ShouldExplodeFor(JudgementResult result) => true; } diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs index e87ecba749..8fd7b93e4c 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchDropletJudgement.cs @@ -7,16 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements { public class CatchDropletJudgement : CatchJudgement { - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 30; - } - } + public override HitResult MaxResult => HitResult.LargeTickHit; } } diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs index 2149ed9712..ccafe0abc4 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs @@ -9,19 +9,7 @@ namespace osu.Game.Rulesets.Catch.Judgements { public class CatchJudgement : Judgement { - public override HitResult MaxResult => HitResult.Perfect; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 300; - } - } + public override HitResult MaxResult => HitResult.Great; /// /// Whether fruit on the platter should explode or drop. diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs index d607b49ea4..d957d4171b 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchTinyDropletJudgement.cs @@ -7,30 +7,6 @@ namespace osu.Game.Rulesets.Catch.Judgements { public class CatchTinyDropletJudgement : CatchJudgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 10; - } - } - - protected override double HealthIncreaseFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 0.02; - } - } + public override HitResult MaxResult => HitResult.SmallTickHit; } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 2fe017dc62..d03a764bda 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; 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; @@ -86,7 +85,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables if (CheckPosition == null) return; if (timeOffset >= 0 && Result != null) - ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? HitResult.Perfect : HitResult.Miss); + ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void UpdateStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index 58a3140bb5..cc01009dd9 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.UI public void OnNewResult(DrawableCatchHitObject judgedObject, JudgementResult result) { - if (!result.Judgement.AffectsCombo || !result.HasResult) + if (!result.Type.AffectsCombo() || !result.HasResult) return; if (result.Type == HitResult.Miss) @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Catch.UI public void OnRevertResult(DrawableCatchHitObject judgedObject, JudgementResult result) { - if (!result.Judgement.AffectsCombo || !result.HasResult) + if (!result.Type.AffectsCombo() || !result.HasResult) return; updateCombo(result.ComboAtJudgement, judgedObject.AccentColour.Value); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index d3e63b0333..5e794a76aa 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osuTK; @@ -52,7 +53,7 @@ namespace osu.Game.Rulesets.Catch.UI public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result) { - if (result.Judgement is IgnoreJudgement) + if (!result.Type.IsScorable()) return; void runAfterLoaded(Action action) From b1877b649ba53a00dc7c083c6d5799f6652e5d68 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 14:29:43 +0900 Subject: [PATCH 3466/6909] Use new hit results in mania --- .../Judgements/HoldNoteTickJudgement.cs | 14 +---------- .../Judgements/ManiaJudgement.cs | 24 ------------------- .../Objects/Drawables/DrawableHoldNote.cs | 2 +- .../Objects/Drawables/DrawableHoldNoteTick.cs | 5 ++-- .../Skinning/ManiaLegacySkinTransformer.cs | 3 +++ 5 files changed, 7 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs index 28e5d2cc1b..ee6cbbc828 100644 --- a/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/HoldNoteTickJudgement.cs @@ -7,18 +7,6 @@ namespace osu.Game.Rulesets.Mania.Judgements { public class HoldNoteTickJudgement : ManiaJudgement { - protected override int NumericResultFor(HitResult result) => result == MaxResult ? 20 : 0; - - protected override double HealthIncreaseFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Perfect: - return 0.01; - } - } + public override HitResult MaxResult => HitResult.LargeTickHit; } } diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index 53967ffa05..220dedc4a4 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -2,34 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Judgements { public class ManiaJudgement : Judgement { - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Meh: - return 50; - - case HitResult.Ok: - return 100; - - case HitResult.Good: - return 200; - - case HitResult.Great: - return 300; - - case HitResult.Perfect: - return 350; - } - } } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 2ebcc5451a..ba6cad978d 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { - ApplyResult(r => r.Type = HitResult.Perfect); + ApplyResult(r => r.Type = r.Judgement.MaxResult); endHold(); } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index 9b0322a6cd..98931dceed 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -73,9 +72,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables var startTime = HoldStartTime?.Invoke(); if (startTime == null || startTime > HitObject.StartTime) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); else - ApplyResult(r => r.Type = HitResult.Perfect); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } } } diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs index 439e6f7df2..3724269f4d 100644 --- a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs @@ -130,6 +130,9 @@ namespace osu.Game.Rulesets.Mania.Skinning private Drawable getResult(HitResult result) { + if (!hitresult_mapping.ContainsKey(result)) + return null; + string filename = this.GetManiaSkinConfig(hitresult_mapping[result])?.Value ?? default_hitresult_skin_filenames[result]; From a77741927cf3e0b84e72cfde6210769047dd8d58 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 14:35:43 +0900 Subject: [PATCH 3467/6909] Use new hit results in osu --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs | 2 +- osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs | 6 +----- .../Objects/Drawables/DrawableSliderRepeat.cs | 3 +-- .../Objects/Drawables/DrawableSliderTail.cs | 3 +-- .../Objects/Drawables/DrawableSliderTick.cs | 3 +-- .../Objects/Drawables/DrawableSpinnerTick.cs | 4 +--- .../Objects/Drawables/Pieces/SpinnerBonusDisplay.cs | 3 ++- osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs | 2 +- osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 4 +--- osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 2 +- osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs | 6 +----- osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs | 8 +------- 12 files changed, 13 insertions(+), 33 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index f7909071ea..dbab048b25 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -146,7 +146,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * SpinnerTick.SCORE_PER_TICK; + return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs index e528f65dca..1999785efe 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuIgnoreJudgement.cs @@ -7,10 +7,6 @@ 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; + public override HitResult MaxResult => HitResult.IgnoreHit; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index d79ecb7b4e..f65077685f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; -using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -50,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (sliderRepeat.StartTime <= Time.Current) - ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 29a4929c1b..0939e2847a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -3,7 +3,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -46,7 +45,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 ? r.Judgement.MaxResult : HitResult.Miss); + ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); } 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 66eb60aa28..9b68b446a4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -10,7 +10,6 @@ using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Skinning; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -64,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset >= 0) - ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : HitResult.Miss); + ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index c390b673be..e9cede1398 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Scoring; - namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject @@ -18,6 +16,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// Apply a judgement result. /// /// Whether this tick was reached. - internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : HitResult.Miss); + internal void TriggerResult(bool hit) => ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs index b499d7a92b..1668cd73bd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Judgements; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -36,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; displayedCount = count; - bonusCounter.Text = $"{SpinnerBonusTick.SCORE_PER_TICK * count}"; + bonusCounter.Text = $"{Judgement.LARGE_BONUS_SCORE * count}"; bonusCounter.FadeOutFromOne(1500); bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index ac6c6905e4..b6c58a75d1 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderRepeatJudgement : OsuJudgement { - protected override int NumericResultFor(HitResult result) => result == MaxResult ? 30 : 0; + public override HitResult MaxResult => HitResult.LargeTickHit; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 1e54b576f1..aff3f38e17 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -29,9 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderTailJudgement : OsuJudgement { - protected override int NumericResultFor(HitResult result) => 0; - - public override bool AffectsCombo => false; + public override HitResult MaxResult => HitResult.IgnoreHit; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index 22f3f559db..a427ee1955 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderTickJudgement : OsuJudgement { - protected override int NumericResultFor(HitResult result) => result == MaxResult ? 10 : 0; + public override HitResult MaxResult => HitResult.LargeTickHit; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 0b1232b8db..235dc8710a 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SpinnerBonusTick : SpinnerTick { - public new const int SCORE_PER_TICK = 50; - public SpinnerBonusTick() { Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); @@ -20,9 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class OsuSpinnerBonusTickJudgement : OsuSpinnerTickJudgement { - protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0; - - protected override double HealthIncreaseFor(HitResult result) => base.HealthIncreaseFor(result) * 2; + public override HitResult MaxResult => HitResult.LargeBonus; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs index f54e7a9a15..d715b9a428 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerTick.cs @@ -9,19 +9,13 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SpinnerTick : OsuHitObject { - public const int SCORE_PER_TICK = 10; - public override Judgement CreateJudgement() => new OsuSpinnerTickJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; public class OsuSpinnerTickJudgement : OsuJudgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) => result == MaxResult ? SCORE_PER_TICK : 0; - - protected override double HealthIncreaseFor(HitResult result) => result == MaxResult ? 0.6 * base.HealthIncreaseFor(result) : 0; + public override HitResult MaxResult => HitResult.SmallBonus; } } } From c45b5690cfcc0469d01e27689f73e8c262a4a449 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 15:13:11 +0900 Subject: [PATCH 3468/6909] Use new hit results in taiko --- .../Judgements/TaikoDrumRollTickJudgement.cs | 14 +------------- .../Judgements/TaikoJudgement.cs | 15 --------------- .../Judgements/TaikoStrongJudgement.cs | 4 ++-- .../Objects/Drawables/DrawableDrumRoll.cs | 4 ++-- .../Objects/Drawables/DrawableDrumRollTick.cs | 7 +++---- .../Objects/Drawables/DrawableHit.cs | 6 +++--- .../Objects/Drawables/DrawableSwell.cs | 6 +++--- .../Objects/Drawables/DrawableSwellTick.cs | 5 ++--- .../Skinning/LegacyTaikoScroller.cs | 2 +- osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs | 5 +++-- 10 files changed, 20 insertions(+), 48 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs index a617028f1c..0551df3211 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs @@ -7,19 +7,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoDrumRollTickJudgement : TaikoJudgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - case HitResult.Great: - return 200; - - default: - return 0; - } - } + public override HitResult MaxResult => HitResult.SmallTickHit; protected override double HealthIncreaseFor(HitResult result) { diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs index eb5f443365..3d22860814 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs @@ -10,21 +10,6 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public override HitResult MaxResult => HitResult.Great; - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - case HitResult.Good: - return 100; - - case HitResult.Great: - return 300; - - default: - return 0; - } - } - protected override double HealthIncreaseFor(HitResult result) { switch (result) diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs index e045ea324f..06495ad9f4 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoStrongJudgement.cs @@ -7,9 +7,9 @@ namespace osu.Game.Rulesets.Taiko.Judgements { public class TaikoStrongJudgement : TaikoJudgement { + public override HitResult MaxResult => HitResult.SmallBonus; + // MainObject already changes the HP protected override double HealthIncreaseFor(HitResult result) => 0; - - public override bool AffectsCombo => false; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 2c1c2d2bc1..286feac5ba 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (countHit >= HitObject.RequiredGoodHits) { - ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good); + ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok); } else ApplyResult(r => r.Type = HitResult.Miss); @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!MainObject.Judged) return; - ApplyResult(r => r.Type = MainObject.IsHit ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 62405cf047..9d7dcc7218 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -4,7 +4,6 @@ using System; 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; @@ -34,14 +33,14 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (timeOffset > HitObject.HitWindow) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } if (Math.Abs(timeOffset) > HitObject.HitWindow) return; - ApplyResult(r => r.Type = HitResult.Great); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } protected override void UpdateStateTransforms(ArmedState state) @@ -74,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!MainObject.Judged) return; - ApplyResult(r => r.Type = MainObject.IsHit ? HitResult.Great : HitResult.Miss); + ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 3a6eaa83db..03df28f850 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -257,19 +257,19 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!MainObject.Result.IsHit) { - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } if (!userTriggered) { if (timeOffset - MainObject.Result.TimeOffset > second_hit_window) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } if (Math.Abs(timeOffset - MainObject.Result.TimeOffset) <= second_hit_window) - ApplyResult(r => r.Type = MainObject.Result.Type); + ApplyResult(r => r.Type = r.Judgement.MaxResult); } public override bool OnPressed(TaikoAction action) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 7294587b10..11ff0729e2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - nextTick?.TriggerResult(HitResult.Great); + nextTick?.TriggerResult(true); var numHits = ticks.Count(r => r.IsHit); @@ -208,10 +208,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables continue; } - tick.TriggerResult(HitResult.Miss); + tick.TriggerResult(false); } - var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Good : HitResult.Miss; + var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : HitResult.Miss; ApplyResult(r => r.Type = hitResult); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 1685576f0d..6202583494 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -19,10 +18,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void UpdateInitialTransforms() => this.FadeOut(); - public void TriggerResult(HitResult type) + public void TriggerResult(bool hit) { HitObject.StartTime = Time.Current; - ApplyResult(r => r.Type = type); + ApplyResult(r => r.Type = hit ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 03813e0a99..928072c491 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning var r = result.NewValue; // always ignore hitobjects that don't affect combo (drumroll ticks etc.) - if (r?.Judgement.AffectsCombo == false) + if (r?.Type.AffectsCombo() == false) return; passing = r == null || r.Type > HitResult.Miss; diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs index b937beae3c..6a16f311bf 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoMascot.cs @@ -11,6 +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; using osu.Game.Screens.Play; @@ -77,7 +78,7 @@ namespace osu.Game.Rulesets.Taiko.UI lastObjectHit = true; } - if (!result.Judgement.AffectsCombo) + if (!result.Type.AffectsCombo()) return; lastObjectHit = result.IsHit; @@ -115,7 +116,7 @@ namespace osu.Game.Rulesets.Taiko.UI } private bool triggerComboClear(JudgementResult judgementResult) - => (judgementResult.ComboAtJudgement + 1) % 50 == 0 && judgementResult.Judgement.AffectsCombo && judgementResult.IsHit; + => (judgementResult.ComboAtJudgement + 1) % 50 == 0 && judgementResult.Type.AffectsCombo() && judgementResult.IsHit; private bool triggerSwellClear(JudgementResult judgementResult) => judgementResult.Judgement is TaikoSwellJudgement && judgementResult.IsHit; From 6264a01eccff56acb06f74b04d2a40534bf263d8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 15:14:03 +0900 Subject: [PATCH 3469/6909] Add guard against using the wrong hit result --- .../Objects/Drawables/DrawableHitObject.cs | 15 ++++++++++++++ osu.Game/Rulesets/Scoring/HitResult.cs | 20 +++++++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 7c05bc9aa7..2a3e3716e5 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -475,6 +475,21 @@ namespace osu.Game.Rulesets.Objects.Drawables if (!Result.HasResult) throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); + // Some (especially older) rulesets use scorable judgements instead of the newer ignorehit/ignoremiss judgements. + if (Result.Judgement.MaxResult == HitResult.IgnoreHit) + { + if (Result.Type == HitResult.Miss) + Result.Type = HitResult.IgnoreMiss; + else if (Result.Type >= HitResult.Meh && Result.Type <= HitResult.Perfect) + Result.Type = HitResult.IgnoreHit; + } + + if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult)) + { + throw new InvalidOperationException( + $"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}])."); + } + // Ensure that the judgement is given a valid time offset, because this may not get set by the caller var endTime = HitObject.GetEndTime(); diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index fc33ff9df2..7a02db190a 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.ComponentModel; +using System.Diagnostics; using osu.Game.Utils; namespace osu.Game.Rulesets.Scoring @@ -175,5 +176,24 @@ namespace osu.Game.Rulesets.Scoring /// Whether a is scorable. /// public static bool IsScorable(this HitResult result) => result >= HitResult.Miss && result < HitResult.IgnoreMiss; + + /// + /// Whether a is valid within a given range. + /// + /// The to check. + /// The minimum . + /// The maximum . + /// Whether falls between and . + public static bool IsValidHitResult(this HitResult result, HitResult minResult, HitResult maxResult) + { + if (result == HitResult.None) + return false; + + if (result == minResult || result == maxResult) + return true; + + Debug.Assert(minResult <= maxResult); + return result > minResult && result < maxResult; + } } } From a1394c183056c52a4d1648d6120a2a65e85fa21b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 15:20:47 +0900 Subject: [PATCH 3470/6909] Fix a few missed judgements --- .../Judgements/OsuJudgement.cs | 18 ------------------ .../Rulesets/Judgements/IgnoreJudgement.cs | 6 +----- .../Rulesets/Judgements/JudgementResult.cs | 2 +- 3 files changed, 2 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs index bf30fbc351..1a88e2a8b2 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuJudgement.cs @@ -9,23 +9,5 @@ namespace osu.Game.Rulesets.Osu.Judgements public class OsuJudgement : Judgement { public override HitResult MaxResult => HitResult.Great; - - protected override int NumericResultFor(HitResult result) - { - switch (result) - { - default: - return 0; - - case HitResult.Meh: - return 50; - - case HitResult.Good: - return 100; - - case HitResult.Great: - return 300; - } - } } } diff --git a/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs b/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs index 1871249c94..d2a434058d 100644 --- a/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs +++ b/osu.Game/Rulesets/Judgements/IgnoreJudgement.cs @@ -7,10 +7,6 @@ namespace osu.Game.Rulesets.Judgements { public class IgnoreJudgement : Judgement { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) => 0; - - protected override double HealthIncreaseFor(HitResult result) => 0; + public override HitResult MaxResult => HitResult.IgnoreHit; } } diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 59a7917e55..3a35fd4433 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Judgements /// /// Whether a successful hit occurred. /// - public bool IsHit => Type > HitResult.Miss; + public bool IsHit => Type.IsHit(); /// /// Creates a new . From 31fae045fa26854c3d4af4c988b03ea792aa9137 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 15:25:31 +0900 Subject: [PATCH 3471/6909] Update judgement processors with new hit results --- .../Scoring/CatchScoreProcessor.cs | 1 - .../Scoring/ManiaScoreProcessor.cs | 2 - .../Scoring/OsuScoreProcessor.cs | 2 - .../Scoring/TaikoScoreProcessor.cs | 2 - .../TestSceneDrainingHealthProcessor.cs | 16 ++--- .../Gameplay/TestSceneScoreProcessor.cs | 32 +++------- .../Scoring/DrainingHealthProcessor.cs | 2 +- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 61 ++++++++----------- osu.Game/Scoring/ScoreManager.cs | 2 +- 9 files changed, 47 insertions(+), 73 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs index 4c7bc4ab73..2cc05826b4 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchScoreProcessor.cs @@ -7,6 +7,5 @@ namespace osu.Game.Rulesets.Catch.Scoring { public class CatchScoreProcessor : ScoreProcessor { - public override HitWindows CreateHitWindows() => new CatchHitWindows(); } } diff --git a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs index 4b2f643333..71cc0bdf1f 100644 --- a/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs +++ b/osu.Game.Rulesets.Mania/Scoring/ManiaScoreProcessor.cs @@ -10,7 +10,5 @@ namespace osu.Game.Rulesets.Mania.Scoring protected override double DefaultAccuracyPortion => 0.95; protected override double DefaultComboPortion => 0.05; - - public override HitWindows CreateHitWindows() => new ManiaHitWindows(); } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs index 86ec76e373..44118227d9 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuScoreProcessor.cs @@ -25,7 +25,5 @@ namespace osu.Game.Rulesets.Osu.Scoring return new OsuJudgementResult(hitObject, judgement); } } - - public override HitWindows CreateHitWindows() => new OsuHitWindows(); } } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs index e29ea87d25..1829ea2513 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoScoreProcessor.cs @@ -10,7 +10,5 @@ namespace osu.Game.Rulesets.Taiko.Scoring protected override double DefaultAccuracyPortion => 0.75; protected override double DefaultComboPortion => 0.25; - - public override HitWindows CreateHitWindows() => new TaikoHitWindows(); } } diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index 460ad1b898..0bb2c4b60c 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -167,7 +167,7 @@ namespace osu.Game.Tests.Gameplay 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(HitResult.LargeBonus) { StartTime = time }); beatmap.HitObjects.Add(new JudgeableHitObject { StartTime = 5000 }); createProcessor(beatmap); @@ -215,23 +215,23 @@ namespace osu.Game.Tests.Gameplay private class JudgeableHitObject : HitObject { - private readonly bool affectsCombo; + private readonly HitResult maxResult; - public JudgeableHitObject(bool affectsCombo = true) + public JudgeableHitObject(HitResult maxResult = HitResult.Perfect) { - this.affectsCombo = affectsCombo; + this.maxResult = maxResult; } - public override Judgement CreateJudgement() => new TestJudgement(affectsCombo); + public override Judgement CreateJudgement() => new TestJudgement(maxResult); protected override HitWindows CreateHitWindows() => new HitWindows(); private class TestJudgement : Judgement { - public override bool AffectsCombo { get; } + public override HitResult MaxResult { get; } - public TestJudgement(bool affectsCombo) + public TestJudgement(HitResult maxResult) { - AffectsCombo = affectsCombo; + MaxResult = maxResult; } } } diff --git a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs index c9ab4fa489..432e3df95e 100644 --- a/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneScoreProcessor.cs @@ -17,13 +17,13 @@ namespace osu.Game.Tests.Gameplay [Test] public void TestNoScoreIncreaseFromMiss() { - var beatmap = new Beatmap { HitObjects = { new TestHitObject() } }; + var beatmap = new Beatmap { HitObjects = { new HitObject() } }; var scoreProcessor = new ScoreProcessor(); scoreProcessor.ApplyBeatmap(beatmap); // Apply a miss judgement - scoreProcessor.ApplyResult(new JudgementResult(new TestHitObject(), new TestJudgement()) { Type = HitResult.Miss }); + scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement()) { Type = HitResult.Miss }); Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(0.0)); } @@ -31,37 +31,25 @@ namespace osu.Game.Tests.Gameplay [Test] public void TestOnlyBonusScore() { - var beatmap = new Beatmap { HitObjects = { new TestBonusHitObject() } }; + var beatmap = new Beatmap { HitObjects = { new HitObject() } }; var scoreProcessor = new ScoreProcessor(); scoreProcessor.ApplyBeatmap(beatmap); // Apply a judgement - scoreProcessor.ApplyResult(new JudgementResult(new TestBonusHitObject(), new TestBonusJudgement()) { Type = HitResult.Perfect }); + scoreProcessor.ApplyResult(new JudgementResult(new HitObject(), new TestJudgement(HitResult.LargeBonus)) { Type = HitResult.LargeBonus }); - Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(100)); - } - - private class TestHitObject : HitObject - { - public override Judgement CreateJudgement() => new TestJudgement(); + Assert.That(scoreProcessor.TotalScore.Value, Is.EqualTo(Judgement.LARGE_BONUS_SCORE)); } private class TestJudgement : Judgement { - protected override int NumericResultFor(HitResult result) => 100; - } + public override HitResult MaxResult { get; } - private class TestBonusHitObject : HitObject - { - public override Judgement CreateJudgement() => new TestBonusJudgement(); - } - - private class TestBonusJudgement : Judgement - { - public override bool AffectsCombo => false; - - protected override int NumericResultFor(HitResult result) => 100; + public TestJudgement(HitResult maxResult = HitResult.Perfect) + { + MaxResult = maxResult; + } } } } diff --git a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs index 130907b242..91793e3f70 100644 --- a/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs +++ b/osu.Game/Rulesets/Scoring/DrainingHealthProcessor.cs @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Scoring { base.ApplyResultInternal(result); - if (!result.Judgement.IsBonus) + if (!result.Type.IsBonus()) healthIncreases.Add((result.HitObject.GetEndTime() + result.TimeOffset, GetHealthIncreaseFor(result))); } diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 6fa5a87c8e..7a5b707357 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -71,7 +71,6 @@ namespace osu.Game.Rulesets.Scoring private double maxBaseScore; private double rollingMaxBaseScore; private double baseScore; - private double bonusScore; private readonly List hitEvents = new List(); private HitObject lastHitObject; @@ -116,14 +115,15 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; - if (result.Judgement.AffectsCombo) + if (!result.Type.IsScorable()) + return; + + if (result.Type.AffectsCombo()) { switch (result.Type) { - case HitResult.None: - break; - case HitResult.Miss: + case HitResult.LargeTickMiss: Combo.Value = 0; break; @@ -133,22 +133,16 @@ namespace osu.Game.Rulesets.Scoring } } - double scoreIncrease = result.Type == HitResult.Miss ? 0 : result.Judgement.NumericResultFor(result); + double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; - if (result.Judgement.IsBonus) + if (!result.Type.IsBonus()) { - if (result.IsHit) - bonusScore += scoreIncrease; - } - else - { - if (result.HasResult) - scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; - baseScore += scoreIncrease; rollingMaxBaseScore += result.Judgement.MaxNumericResult; } + scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) + 1; + hitEvents.Add(CreateHitEvent(result)); lastHitObject = result.HitObject; @@ -171,22 +165,19 @@ namespace osu.Game.Rulesets.Scoring if (result.FailedAtJudgement) return; - double scoreIncrease = result.Type == HitResult.Miss ? 0 : result.Judgement.NumericResultFor(result); + if (!result.Type.IsScorable()) + return; - if (result.Judgement.IsBonus) - { - if (result.IsHit) - bonusScore -= scoreIncrease; - } - else - { - if (result.HasResult) - scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1; + double scoreIncrease = result.Type.IsHit() ? result.Judgement.NumericResultFor(result) : 0; + if (!result.Type.IsBonus()) + { baseScore -= scoreIncrease; rollingMaxBaseScore -= result.Judgement.MaxNumericResult; } + scoreResultCounts[result.Type] = scoreResultCounts.GetOrDefault(result.Type) - 1; + Debug.Assert(hitEvents.Count > 0); lastHitObject = hitEvents[^1].LastHitObject; hitEvents.RemoveAt(hitEvents.Count - 1); @@ -207,7 +198,7 @@ namespace osu.Game.Rulesets.Scoring return GetScore(mode, maxHighestCombo, maxBaseScore > 0 ? baseScore / maxBaseScore : 0, maxHighestCombo > 0 ? (double)HighestCombo.Value / maxHighestCombo : 0, - bonusScore); + scoreResultCounts); } /// @@ -217,9 +208,9 @@ namespace osu.Game.Rulesets.Scoring /// The maximum combo achievable in the beatmap. /// The accuracy percentage achieved by the player. /// The proportion of achieved by the player. - /// Any bonus score to be added. + /// Any statistics to be factored in. /// The total score. - public double GetScore(ScoringMode mode, int maxCombo, double accuracyRatio, double comboRatio, double bonusScore) + public double GetScore(ScoringMode mode, int maxCombo, double accuracyRatio, double comboRatio, Dictionary statistics) { switch (mode) { @@ -228,14 +219,18 @@ namespace osu.Game.Rulesets.Scoring double accuracyScore = accuracyPortion * accuracyRatio; double comboScore = comboPortion * comboRatio; - return (max_score * (accuracyScore + comboScore) + bonusScore) * scoreMultiplier; + return (max_score * (accuracyScore + comboScore) + getBonusScore(statistics)) * scoreMultiplier; case ScoringMode.Classic: // should emulate osu-stable's scoring as closely as we can (https://osu.ppy.sh/help/wiki/Score/ScoreV1) - return bonusScore + (accuracyRatio * maxCombo * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25); + return getBonusScore(statistics) + (accuracyRatio * maxCombo * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25); } } + private double getBonusScore(Dictionary statistics) + => statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE + + statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE; + private ScoreRank rankFrom(double acc) { if (acc == 1) @@ -282,7 +277,6 @@ namespace osu.Game.Rulesets.Scoring baseScore = 0; rollingMaxBaseScore = 0; - bonusScore = 0; TotalScore.Value = 0; Accuracy.Value = 1; @@ -309,9 +303,7 @@ namespace osu.Game.Rulesets.Scoring score.Rank = Rank.Value; score.Date = DateTimeOffset.Now; - var hitWindows = CreateHitWindows(); - - foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) + foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.IsScorable())) score.Statistics[result] = GetStatistic(result); score.HitEvents = hitEvents; @@ -320,6 +312,7 @@ namespace osu.Game.Rulesets.Scoring /// /// Create a for this processor. /// + [Obsolete("Method is now unused.")] // Can be removed 20210328 public virtual HitWindows CreateHitWindows() => new HitWindows(); } diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 619ca76598..561ca631b3 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -181,7 +181,7 @@ namespace osu.Game.Scoring scoreProcessor.Mods.Value = score.Mods; - Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo, 0)); + Value = (long)Math.Round(scoreProcessor.GetScore(ScoringMode.Value, beatmapMaxCombo, score.Accuracy, (double)score.MaxCombo / beatmapMaxCombo, score.Statistics)); } } From bc8f6a58fd307302cb998e1e7af9fc79dd443740 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 15:26:42 +0900 Subject: [PATCH 3472/6909] Update PF/SD with new hit results --- osu.Game/Rulesets/Mods/ModPerfect.cs | 3 +-- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index 65f1a972ed..df0fc9c4b6 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -16,8 +16,7 @@ namespace osu.Game.Rulesets.Mods public override string Description => "SS or quit."; protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) - => !(result.Judgement is IgnoreJudgement) - && result.Judgement.AffectsCombo + => result.Type.AffectsAccuracy() && result.Type != result.Judgement.MaxResult; } } diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index df10262845..ae71041a64 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs @@ -29,6 +29,8 @@ namespace osu.Game.Rulesets.Mods healthProcessor.FailConditions += FailCondition; } - protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => !result.IsHit && result.Judgement.AffectsCombo; + protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + => result.Type.AffectsCombo() + && !result.IsHit; } } From 4ef7ab28728e9c4543b5a049b153f742a925378b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 15:36:08 +0900 Subject: [PATCH 3473/6909] Fix tests --- .../Mods/TestSceneCatchModPerfect.cs | 2 +- .../Skinning/TestSceneDrawableTaikoMascot.cs | 4 ++-- osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs | 2 +- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs index c1b7214d72..3e06e78dba 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModPerfect.cs @@ -50,7 +50,7 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods 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(false)] + [TestCase(true)] public void TestTinyDroplet(bool shouldMiss) => CreateHitObjectTest(new HitObjectTestData(new TinyDroplet { StartTime = 1000 }), shouldMiss); } } diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 36289bda93..ae42bf8a90 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning createDrawableRuleset(); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); - assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Idle); + assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); } [Test] @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning createDrawableRuleset(); 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 TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Kiai); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); } diff --git a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs index efeb5eeba2..7264083338 100644 --- a/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs +++ b/osu.Game.Tests/Gameplay/TestSceneDrainingHealthProcessor.cs @@ -263,7 +263,7 @@ namespace osu.Game.Tests.Gameplay public double Duration { get; set; } = 5000; public JudgeableLongHitObject() - : base(false) + : base(HitResult.LargeBonus) { } diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 64d1024efb..84b7bc3723 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -35,10 +35,10 @@ namespace osu.Game.Tests.Rulesets.Scoring } [TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)] - [TestCase(ScoringMode.Standardised, HitResult.Good, 800_000)] + [TestCase(ScoringMode.Standardised, HitResult.Good, 900_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.Good, 200)] [TestCase(ScoringMode.Classic, HitResult.Great, 300)] public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore) { From e789e06c86ac973754a4f71b053b7b1becdb97f7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 16:00:45 +0900 Subject: [PATCH 3474/6909] Don't display hold note tick judgements --- .../Objects/Drawables/DrawableHoldNoteTick.cs | 2 ++ osu.Game.Rulesets.Mania/UI/Column.cs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index 98931dceed..f265419aa0 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableHoldNoteTick : DrawableManiaHitObject { + public override bool DisplayResult => false; + /// /// References the time at which the user started holding the hold note. /// diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 9aabcc6699..c28a1c13d8 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI if (result.IsHit) hitPolicy.HandleHit(judgedObject); - if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) + if (!result.IsHit || !DisplayJudgements.Value) return; HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result))); From 903bcd747e777757150310877bf256594b818bcf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 16:39:29 +0900 Subject: [PATCH 3475/6909] Revert unintended changes --- osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs index ccafe0abc4..fd61647a7c 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Judgements { public class CatchJudgement : Judgement { - public override HitResult MaxResult => HitResult.Great; + public override HitResult MaxResult => HitResult.Perfect; /// /// Whether fruit on the platter should explode or drop. diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 286feac5ba..dfab24f239 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (countHit >= HitObject.RequiredGoodHits) { - ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok); + ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good); } else ApplyResult(r => r.Type = HitResult.Miss); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 11ff0729e2..999a159cce 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : HitResult.Miss; + var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Good : HitResult.Miss; ApplyResult(r => r.Type = hitResult); } From f439c1afbc7310991681ba2bd1867a7540494508 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 17:16:55 +0900 Subject: [PATCH 3476/6909] Make osu/taiko/catch use Ok+Great --- .../TestSceneComboCounter.cs | 2 +- .../Difficulty/CatchPerformanceCalculator.cs | 2 +- .../Judgements/CatchJudgement.cs | 2 +- .../Scoring/CatchHitWindows.cs | 2 +- .../Difficulty/OsuPerformanceCalculator.cs | 10 ++--- .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Replays/OsuAutoGenerator.cs | 6 +-- .../Scoring/OsuHitWindows.cs | 4 +- .../Skinning/TestSceneDrawableTaikoMascot.cs | 6 +-- .../Skinning/TestSceneHitExplosion.cs | 4 +- .../Skinning/TestSceneTaikoScroller.cs | 2 +- .../TestSceneFlyingHits.cs | 2 +- .../TestSceneHits.cs | 8 ++-- .../Difficulty/TaikoPerformanceCalculator.cs | 6 +-- .../Judgements/TaikoJudgement.cs | 2 +- .../Objects/Drawables/DrawableDrumRoll.cs | 2 +- .../Objects/Drawables/DrawableSwell.cs | 2 +- .../Scoring/TaikoHitWindows.cs | 4 +- .../Skinning/TaikoLegacySkinTransformer.cs | 8 ++-- .../TaikoSkinComponents.cs | 4 +- .../UI/DrawableTaikoJudgement.cs | 2 +- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 6 +-- .../Rulesets/Scoring/ScoreProcessorTest.cs | 4 +- .../Visual/Gameplay/TestSceneHitErrorMeter.cs | 2 +- .../Scoring/Legacy/ScoreInfoExtensions.cs | 37 +------------------ osu.Game/Skinning/LegacySkin.cs | 2 +- osu.Game/Tests/TestScoreInfo.cs | 4 +- 27 files changed, 53 insertions(+), 84 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs index e79792e04a..c7b322c8a0 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneComboCounter.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Tests [Test] public void TestCatchComboCounter() { - AddRepeatStep("perform hit", () => performJudgement(HitResult.Perfect), 20); + AddRepeatStep("perform hit", () => performJudgement(HitResult.Great), 20); AddStep("perform miss", () => performJudgement(HitResult.Miss)); AddStep("randomize judged object colour", () => diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index d700f79e5b..a4b9ca35eb 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty { mods = Score.Mods; - fruitsHit = Score.Statistics.GetOrDefault(HitResult.Perfect); + fruitsHit = Score.Statistics.GetOrDefault(HitResult.Great); ticksHit = Score.Statistics.GetOrDefault(HitResult.LargeTickHit); tinyTicksHit = Score.Statistics.GetOrDefault(HitResult.SmallTickHit); tinyTicksMissed = Score.Statistics.GetOrDefault(HitResult.SmallTickMiss); diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs index fd61647a7c..ccafe0abc4 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgement.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Judgements { public class CatchJudgement : Judgement { - public override HitResult MaxResult => HitResult.Perfect; + public override HitResult MaxResult => HitResult.Great; /// /// Whether fruit on the platter should explode or drop. diff --git a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs index ff793a372e..0a444d923e 100644 --- a/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs +++ b/osu.Game.Rulesets.Catch/Scoring/CatchHitWindows.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Catch.Scoring { switch (result) { - case HitResult.Perfect: + case HitResult.Great: case HitResult.Miss: return true; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 6f4c0f9cfa..02577461f0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty private double accuracy; private int scoreMaxCombo; private int countGreat; - private int countGood; + private int countOk; private int countMeh; private int countMiss; @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty accuracy = Score.Accuracy; scoreMaxCombo = Score.MaxCombo; 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); @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty int amountHitObjectsWithAccuracy = countHitCircles; if (amountHitObjectsWithAccuracy > 0) - betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countGood * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); + betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); else betterAccuracyPercentage = 0; @@ -204,7 +204,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty return accuracyValue; } - private int totalHits => countGreat + countGood + countMeh + countMiss; - private int totalSuccessfulHits => countGreat + countGood + countMeh; + private int totalHits => countGreat + countOk + countMeh + countMiss; + private int totalSuccessfulHits => countGreat + countOk + countMeh; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index a57bb466c7..d77213f3ed 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (Progress >= 1) r.Type = HitResult.Great; else if (Progress > .9) - r.Type = HitResult.Good; + r.Type = HitResult.Ok; else if (Progress > .75) r.Type = HitResult.Meh; else if (Time.Current >= Spinner.EndTime) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 9b350278f3..954a217473 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -137,13 +137,13 @@ namespace osu.Game.Rulesets.Osu.Replays if (!(h is Spinner)) AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Meh), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } - else if (h.StartTime - hitWindows.WindowFor(HitResult.Good) > endTime + hitWindows.WindowFor(HitResult.Good) + 50) + else if (h.StartTime - hitWindows.WindowFor(HitResult.Ok) > endTime + hitWindows.WindowFor(HitResult.Ok) + 50) { if (!(prev is Spinner) && h.StartTime - endTime < 1000) - AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Good), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); + AddFrameToReplay(new OsuReplayFrame(endTime + hitWindows.WindowFor(HitResult.Ok), new Vector2(prev.StackedEndPosition.X, prev.StackedEndPosition.Y))); if (!(h is Spinner)) - AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Good), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); + AddFrameToReplay(new OsuReplayFrame(h.StartTime - hitWindows.WindowFor(HitResult.Ok), new Vector2(h.StackedPosition.X, h.StackedPosition.Y))); } } diff --git a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs index 6f2998006f..dafe63a6d1 100644 --- a/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs +++ b/osu.Game.Rulesets.Osu/Scoring/OsuHitWindows.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Osu.Scoring private static readonly DifficultyRange[] osu_ranges = { new DifficultyRange(HitResult.Great, 80, 50, 20), - new DifficultyRange(HitResult.Good, 140, 100, 60), + new DifficultyRange(HitResult.Ok, 140, 100, 60), new DifficultyRange(HitResult.Meh, 200, 150, 100), new DifficultyRange(HitResult.Miss, 400, 400, 400), }; @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Scoring switch (result) { case HitResult.Great: - case HitResult.Good: + case HitResult.Ok: case HitResult.Meh: case HitResult.Miss: return true; diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index ae42bf8a90..99e103da3b 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning createDrawableRuleset(); - assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Kiai); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Kiai); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Kiai); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Miss }, TaikoMascotAnimationState.Fail); } @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning 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.Idle); - assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Good }, TaikoMascotAnimationState.Idle); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Idle); } [TestCase(true)] @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning 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); + assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Ok }, TaikoMascotAnimationState.Clear); } [TestCase(true, TaikoMascotAnimationState.Kiai)] diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs index 45c94a8a86..fecb5d4a74 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneHitExplosion.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public void TestNormalHit() { AddStep("Great", () => SetContents(() => getContentFor(createHit(HitResult.Great)))); - AddStep("Good", () => SetContents(() => getContentFor(createHit(HitResult.Good)))); + AddStep("Ok", () => SetContents(() => getContentFor(createHit(HitResult.Ok)))); AddStep("Miss", () => SetContents(() => getContentFor(createHit(HitResult.Miss)))); } @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning public void TestStrongHit([Values(false, true)] bool hitBoth) { AddStep("Great", () => SetContents(() => getContentFor(createStrongHit(HitResult.Great, hitBoth)))); - AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Good, hitBoth)))); + AddStep("Good", () => SetContents(() => getContentFor(createStrongHit(HitResult.Ok, hitBoth)))); } private Drawable getContentFor(DrawableTestHit hit) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index 16ef5b968d..114038b81c 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning })); AddToggleStep("Toggle passing", passing => this.ChildrenOfType().ForEach(s => s.LastResult.Value = - new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Perfect : HitResult.Miss })); + new JudgementResult(null, new Judgement()) { Type = passing ? HitResult.Great : HitResult.Miss })); AddToggleStep("toggle playback direction", reversed => this.reversed = reversed); } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs index 7492a76a67..63854e7ead 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneFlyingHits.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Taiko.Tests DrawableDrumRollTick h; DrawableRuleset.Playfield.Add(h = new DrawableDrumRollTick(tick) { JudgementType = hitType }); - ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(tick, new TaikoDrumRollTickJudgement()) { Type = HitResult.Perfect }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(tick, new TaikoDrumRollTickJudgement()) { Type = HitResult.Great }); } } } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index b6cfe368f7..0f605be8f9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addHitJudgement(bool kiai) { - HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great; + HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great; var cpi = new ControlPointInfo(); cpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); @@ -113,7 +113,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Hit hit = new Hit(); hit.ApplyDefaults(cpi, new BeatmapDifficulty()); - var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; + var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) }; DrawableRuleset.Playfield.Add(h); @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addStrongHitJudgement(bool kiai) { - HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Good : HitResult.Great; + HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great; var cpi = new ControlPointInfo(); cpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Taiko.Tests Hit hit = new Hit(); hit.ApplyDefaults(cpi, new BeatmapDifficulty()); - var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Good ? -0.1f : -0.05f, hitResult == HitResult.Good ? 0.1f : 0.05f) }; + var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) }; DrawableRuleset.Playfield.Add(h); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index b9d95a6ba6..c04fffa2e7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private Mod[] mods; private int countGreat; - private int countGood; + private int countOk; private int countMeh; private int countMiss; @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { mods = Score.Mods; 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); @@ -102,6 +102,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty return accValue * Math.Min(1.15, Math.Pow(totalHits / 1500.0, 0.3)); } - private int totalHits => countGreat + countGood + countMeh + countMiss; + private int totalHits => countGreat + countOk + countMeh + countMiss; } } diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs index 3d22860814..e272c1a4ef 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoJudgement.cs @@ -17,7 +17,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements case HitResult.Miss: return -1.0; - case HitResult.Good: + case HitResult.Ok: return 1.1; case HitResult.Great: diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index dfab24f239..286feac5ba 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -129,7 +129,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (countHit >= HitObject.RequiredGoodHits) { - ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Good); + ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok); } else ApplyResult(r => r.Type = HitResult.Miss); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 999a159cce..11ff0729e2 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -211,7 +211,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Good : HitResult.Miss; + var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : HitResult.Miss; ApplyResult(r => r.Type = hitResult); } diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs index 9d273392ff..cf806c0c97 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHitWindows.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring private static readonly DifficultyRange[] taiko_ranges = { new DifficultyRange(HitResult.Great, 50, 35, 20), - new DifficultyRange(HitResult.Good, 120, 80, 50), + new DifficultyRange(HitResult.Ok, 120, 80, 50), new DifficultyRange(HitResult.Miss, 135, 95, 70), }; @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring switch (result) { case HitResult.Great: - case HitResult.Good: + case HitResult.Ok: case HitResult.Miss: return true; } diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index c222ccb51f..73a56f3fbc 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -74,8 +74,8 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; - case TaikoSkinComponents.TaikoExplosionGood: - case TaikoSkinComponents.TaikoExplosionGoodStrong: + case TaikoSkinComponents.TaikoExplosionOk: + case TaikoSkinComponents.TaikoExplosionOkStrong: case TaikoSkinComponents.TaikoExplosionGreat: case TaikoSkinComponents.TaikoExplosionGreatStrong: case TaikoSkinComponents.TaikoExplosionMiss: @@ -106,10 +106,10 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.TaikoExplosionMiss: return "taiko-hit0"; - case TaikoSkinComponents.TaikoExplosionGood: + case TaikoSkinComponents.TaikoExplosionOk: return "taiko-hit100"; - case TaikoSkinComponents.TaikoExplosionGoodStrong: + case TaikoSkinComponents.TaikoExplosionOkStrong: return "taiko-hit100k"; case TaikoSkinComponents.TaikoExplosionGreat: diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 0d785adb4a..b274608d84 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -16,8 +16,8 @@ namespace osu.Game.Rulesets.Taiko PlayfieldBackgroundRight, BarLine, TaikoExplosionMiss, - TaikoExplosionGood, - TaikoExplosionGoodStrong, + TaikoExplosionOk, + TaikoExplosionOkStrong, TaikoExplosionGreat, TaikoExplosionGreatStrong, Scroller, diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index f91bbb14e8..cbfc5a8628 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI { switch (Result.Type) { - case HitResult.Good: + case HitResult.Ok: JudgementBody.Colour = colours.GreenLight; break; diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index efd1b25046..6d800b812d 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -60,10 +60,10 @@ namespace osu.Game.Rulesets.Taiko.UI case HitResult.Miss: return TaikoSkinComponents.TaikoExplosionMiss; - case HitResult.Good: + case HitResult.Ok: return useStrongExplosion(judgedObject) - ? TaikoSkinComponents.TaikoExplosionGoodStrong - : TaikoSkinComponents.TaikoExplosionGood; + ? TaikoSkinComponents.TaikoExplosionOkStrong + : TaikoSkinComponents.TaikoExplosionOk; case HitResult.Great: return useStrongExplosion(judgedObject) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 84b7bc3723..ace57aad1d 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -35,10 +35,10 @@ namespace osu.Game.Tests.Rulesets.Scoring } [TestCase(ScoringMode.Standardised, HitResult.Meh, 750_000)] - [TestCase(ScoringMode.Standardised, HitResult.Good, 900_000)] + [TestCase(ScoringMode.Standardised, HitResult.Ok, 800_000)] [TestCase(ScoringMode.Standardised, HitResult.Great, 1_000_000)] [TestCase(ScoringMode.Classic, HitResult.Meh, 50)] - [TestCase(ScoringMode.Classic, HitResult.Good, 200)] + [TestCase(ScoringMode.Classic, HitResult.Ok, 100)] [TestCase(ScoringMode.Classic, HitResult.Great, 300)] public void TestSingleOsuHit(ScoringMode scoringMode, HitResult hitResult, int expectedScore) { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index 253b8d9c55..377f305d63 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.Gameplay Children = new[] { new OsuSpriteText { Text = $@"Great: {hitWindows?.WindowFor(HitResult.Great)}" }, - new OsuSpriteText { Text = $@"Good: {hitWindows?.WindowFor(HitResult.Good)}" }, + new OsuSpriteText { Text = $@"Good: {hitWindows?.WindowFor(HitResult.Ok)}" }, new OsuSpriteText { Text = $@"Meh: {hitWindows?.WindowFor(HitResult.Meh)}" }, } }); diff --git a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs index 6f73a284a2..b58f65800d 100644 --- a/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs +++ b/osu.Game/Scoring/Legacy/ScoreInfoExtensions.cs @@ -28,37 +28,9 @@ namespace osu.Game.Scoring.Legacy } } - public static int? GetCount300(this ScoreInfo scoreInfo) - { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) - { - case 0: - case 1: - case 3: - return getCount(scoreInfo, HitResult.Great); + public static int? GetCount300(this ScoreInfo scoreInfo) => getCount(scoreInfo, HitResult.Great); - case 2: - return getCount(scoreInfo, HitResult.Perfect); - } - - return null; - } - - public static void SetCount300(this ScoreInfo scoreInfo, int value) - { - switch (scoreInfo.Ruleset?.ID ?? scoreInfo.RulesetID) - { - case 0: - case 1: - case 3: - scoreInfo.Statistics[HitResult.Great] = value; - break; - - case 2: - scoreInfo.Statistics[HitResult.Perfect] = value; - break; - } - } + public static void SetCount300(this ScoreInfo scoreInfo, int value) => scoreInfo.Statistics[HitResult.Great] = value; public static int? GetCountKatu(this ScoreInfo scoreInfo) { @@ -94,8 +66,6 @@ namespace osu.Game.Scoring.Legacy { case 0: case 1: - return getCount(scoreInfo, HitResult.Good); - case 3: return getCount(scoreInfo, HitResult.Ok); @@ -112,9 +82,6 @@ namespace osu.Game.Scoring.Legacy { case 0: case 1: - scoreInfo.Statistics[HitResult.Good] = value; - break; - case 3: scoreInfo.Statistics[HitResult.Ok] = value; break; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 5caf07b554..e38913b13a 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -336,7 +336,7 @@ namespace osu.Game.Skinning case HitResult.Meh: return this.GetAnimation("hit50", true, false); - case HitResult.Good: + case HitResult.Ok: return this.GetAnimation("hit100", true, false); case HitResult.Great: diff --git a/osu.Game/Tests/TestScoreInfo.cs b/osu.Game/Tests/TestScoreInfo.cs index 704d01e479..9090a12d3f 100644 --- a/osu.Game/Tests/TestScoreInfo.cs +++ b/osu.Game/Tests/TestScoreInfo.cs @@ -35,8 +35,10 @@ namespace osu.Game.Tests Statistics[HitResult.Miss] = 1; Statistics[HitResult.Meh] = 50; - Statistics[HitResult.Good] = 100; + Statistics[HitResult.Ok] = 100; + Statistics[HitResult.Good] = 200; Statistics[HitResult.Great] = 300; + Statistics[HitResult.Perfect] = 320; Statistics[HitResult.SmallTickHit] = 50; Statistics[HitResult.SmallTickMiss] = 25; Statistics[HitResult.LargeTickHit] = 100; From 91262620d3c72bb14880d1e7262c734aa4672fd2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 17:17:06 +0900 Subject: [PATCH 3477/6909] Remove XMLDocs from Ok/Perfect hit results --- osu.Game/Rulesets/Scoring/HitResult.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 7a02db190a..d979c342e1 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -32,9 +32,6 @@ namespace osu.Game.Rulesets.Scoring [Order(4)] Meh, - /// - /// Optional judgement. - /// [Description(@"OK")] [Order(3)] Ok, @@ -47,9 +44,6 @@ namespace osu.Game.Rulesets.Scoring [Order(1)] Great, - /// - /// Optional judgement. - /// [Description(@"Perfect")] [Order(0)] Perfect, From 53b3d238427d456be3e5acbd79d221b7d1937136 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 17:26:49 +0900 Subject: [PATCH 3478/6909] Expose HitObjectComposer for other components in the Compose csreen to use --- .../Screens/Edit/Compose/ComposeScreen.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index d7a4661fa0..5282b4d998 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -1,8 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osu.Game.Skinning; @@ -18,11 +22,23 @@ namespace osu.Game.Screens.Edit.Compose { } - protected override Drawable CreateMainContent() + private Ruleset ruleset; + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { - var ruleset = Beatmap.Value.BeatmapInfo.Ruleset?.CreateInstance(); + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + + ruleset = parent.Get>().Value.BeatmapInfo.Ruleset?.CreateInstance(); composer = ruleset?.CreateHitObjectComposer(); + // make the composer available to the timeline and other components in this screen. + dependencies.CacheAs(composer); + + return dependencies; + } + + protected override Drawable CreateMainContent() + { if (ruleset == null || composer == null) return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); From f16fc2907188da26ff64cb7743eb1e0a2319f444 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 16:42:54 +0900 Subject: [PATCH 3479/6909] Add combo colour display support --- .../Timeline/TimelineHitObjectBlueprint.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index b95b3842b3..7dfaf02af4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -15,6 +16,7 @@ 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; using osu.Game.Rulesets.Objects.Types; using osuTK; using osuTK.Graphics; @@ -34,6 +36,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly List shadowComponents = new List(); + private DrawableHitObject drawableHitObject; + + private Bindable comboColour; + private const float thickness = 5; private const float shadow_radius = 5; @@ -123,6 +129,30 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateShadows(); } + [BackgroundDependencyLoader(true)] + private void load(HitObjectComposer composer) + { + if (composer != null) + { + // best effort to get the drawable representation for grabbing colour and what not. + drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject); + } + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (drawableHitObject != null) + { + comboColour = drawableHitObject.AccentColour.GetBoundCopy(); + comboColour.BindValueChanged(colour => + { + Colour = drawableHitObject.AccentColour.Value; + }, true); + } + } + protected override void Update() { base.Update(); From 8d8d45a0c068a9348e1ba37eecdf673900dae70d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 17:04:59 +0900 Subject: [PATCH 3480/6909] Add combo index display support --- .../Timeline/TimelineHitObjectBlueprint.cs | 48 +++++++++++++++++-- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 7dfaf02af4..9aa0fee96e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -13,6 +13,8 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Primitives; 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.Rulesets.Edit; using osu.Game.Rulesets.Objects; @@ -40,11 +42,17 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private Bindable comboColour; + private readonly Container mainComponents; + + private readonly OsuSpriteText comboIndexText; + + private Bindable comboIndex; + private const float thickness = 5; private const float shadow_radius = 5; - private const float circle_size = 16; + private const float circle_size = 24; public TimelineHitObjectBlueprint(HitObject hitObject) : base(hitObject) @@ -60,6 +68,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + AddRangeInternal(new Drawable[] + { + mainComponents = new Container + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + comboIndexText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + Font = OsuFont.Numeric.With(size: circle_size / 2, weight: FontWeight.Black), + }, + }); + circle = new Circle { Size = new Vector2(circle_size), @@ -83,7 +108,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline DragBar dragBarUnderlay; Container extensionBar; - AddRangeInternal(new Drawable[] + mainComponents.AddRange(new Drawable[] { extensionBar = new Container { @@ -123,7 +148,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } else { - AddInternal(circle); + mainComponents.Add(circle); } updateShadows(); @@ -143,12 +168,27 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadComplete(); + if (HitObject is IHasComboInformation comboInfo) + { + comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); + comboIndex.BindValueChanged(combo => + { + comboIndexText.Text = (combo.NewValue + 1).ToString(); + }, true); + } + if (drawableHitObject != null) { comboColour = drawableHitObject.AccentColour.GetBoundCopy(); comboColour.BindValueChanged(colour => { - Colour = drawableHitObject.AccentColour.Value; + mainComponents.Colour = drawableHitObject.AccentColour.Value; + + var col = mainComponents.Colour.AverageColour.Linear; + float brightness = col.R + col.G + col.B; + + // decide the combo index colour based on brightness? + comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White; }, true); } } From c47652c97a22350e3d14269fc4d6936c2856e444 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 17:22:47 +0900 Subject: [PATCH 3481/6909] Add gradient to hide subtractive colour issues Good thing is looks better than without. --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 9aa0fee96e..11f44c59ac 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Primitives; @@ -182,7 +183,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline comboColour = drawableHitObject.AccentColour.GetBoundCopy(); comboColour.BindValueChanged(colour => { - mainComponents.Colour = drawableHitObject.AccentColour.Value; + mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White); var col = mainComponents.Colour.AverageColour.Linear; float brightness = col.R + col.G + col.B; From 6e1ea004436fcdc09da0b6599d37a75e778b5a18 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 17:34:50 +0900 Subject: [PATCH 3482/6909] Don't apply gradient to non-duration objects --- .../Components/Timeline/TimelineHitObjectBlueprint.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 11f44c59ac..455f1e17bd 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -91,9 +91,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Size = new Vector2(circle_size), Anchor = Anchor.CentreLeft, Origin = Anchor.Centre, - RelativePositionAxes = Axes.X, - AlwaysPresent = true, - Colour = Color4.White, EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, @@ -183,7 +180,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline comboColour = drawableHitObject.AccentColour.GetBoundCopy(); comboColour.BindValueChanged(colour => { - mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White); + if (HitObject is IHasDuration) + mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White); + else + mainComponents.Colour = drawableHitObject.AccentColour.Value; var col = mainComponents.Colour.AverageColour.Linear; float brightness = col.R + col.G + col.B; From 379a4cca85bec432dc2fa18da48fbdd7e09e8158 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 17:48:44 +0900 Subject: [PATCH 3483/6909] Adjust hold note tests --- .../TestSceneHoldNoteInput.cs | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 95072cf4f8..5cb1519196 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Miss); + assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); - assertNoteJudgement(HitResult.Perfect); + assertNoteJudgement(HitResult.IgnoreHit); } /// @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Miss); + assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Miss); + assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Perfect); } @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.Miss); + assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); } @@ -161,7 +161,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Perfect); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Meh); } @@ -199,7 +199,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Miss); } @@ -217,7 +217,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Perfect); + assertTickJudgement(HitResult.LargeTickHit); assertTailJudgement(HitResult.Meh); } @@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Mania.Tests }); assertHeadJudgement(HitResult.Miss); - assertTickJudgement(HitResult.Miss); + assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Meh); } @@ -280,10 +280,10 @@ namespace osu.Game.Rulesets.Mania.Tests }, beatmap); AddAssert("first hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[0].NestedHitObjects.Contains(j.HitObject)) - .All(j => j.Type == HitResult.Miss)); + .All(j => !j.Type.IsHit())); AddAssert("second hold note missed", () => judgementResults.Where(j => beatmap.HitObjects[1].NestedHitObjects.Contains(j.HitObject)) - .All(j => j.Type == HitResult.Perfect)); + .All(j => j.Type.IsHit())); } private void assertHeadJudgement(HitResult result) From cc9fa4675c00a905272c63d2e963d48d0fb2b94a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 17:59:42 +0900 Subject: [PATCH 3484/6909] Adjust HP increases --- osu.Game/Rulesets/Judgements/Judgement.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index ea7a8d8d25..d80ebfe402 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -124,19 +124,19 @@ namespace osu.Game.Rulesets.Judgements return -DEFAULT_MAX_HEALTH_INCREASE; case HitResult.Meh: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; + return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.Ok: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.01; + return -DEFAULT_MAX_HEALTH_INCREASE * 0.3; case HitResult.Good: - return DEFAULT_MAX_HEALTH_INCREASE * 0.5; + return DEFAULT_MAX_HEALTH_INCREASE * 0.1; case HitResult.Great: - return DEFAULT_MAX_HEALTH_INCREASE; + return DEFAULT_MAX_HEALTH_INCREASE * 0.8; case HitResult.Perfect: - return DEFAULT_MAX_HEALTH_INCREASE * 1.05; + return DEFAULT_MAX_HEALTH_INCREASE; case HitResult.SmallBonus: return DEFAULT_MAX_HEALTH_INCREASE * 0.1; From 4c872094c9a3eb1ff64468b30f999c486ae8d73f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 18:29:50 +0900 Subject: [PATCH 3485/6909] Adjust slider tests --- osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs | 10 +++++----- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 854626d362..32a36ab317 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -209,9 +209,9 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.IgnoreHit); addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Miss); - addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); } /// @@ -252,9 +252,9 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.IgnoreHit); addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.Great); - addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.Great); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); } /// @@ -331,7 +331,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); addJudgementAssert(hitObjects[0], HitResult.Great); - addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.IgnoreHit); } private void addJudgementAssert(OsuHitObject hitObject, HitResult result) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index b543b6fa94..3d8a52a864 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -312,13 +312,13 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Tracking dropped", assertMidSliderJudgementFail); } - private bool assertGreatJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == HitResult.Great); + private bool assertGreatJudge() => judgementResults.Any() && judgementResults.All(t => t.Type.IsHit()); - private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.Great && judgementResults.First().Type == HitResult.Miss; + private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.IgnoreHit && judgementResults.First().Type == HitResult.Miss; - private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.Great; + private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.IgnoreHit; - private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.Miss; + private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.IgnoreMiss; private ScoreAccessibleReplayPlayer currentPlayer; From 297168ecc41e7dd053fac1d0a17c0ac898315e79 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 29 Sep 2020 18:55:06 +0900 Subject: [PATCH 3486/6909] Fix scores sometimes not being re-standardised correctly --- .../Requests/Responses/APILegacyScoreInfo.cs | 1 + osu.Game/Scoring/ScoreInfo.cs | 28 +++++++++++++++ osu.Game/Scoring/ScoreManager.cs | 34 ++++++++++++++----- 3 files changed, 54 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs index b941cd8973..3d3c07a5ad 100644 --- a/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs +++ b/osu.Game/Online/API/Requests/Responses/APILegacyScoreInfo.cs @@ -38,6 +38,7 @@ namespace osu.Game.Online.API.Requests.Responses Rank = Rank, Ruleset = ruleset, Mods = mods, + IsLegacyScore = true }; if (Statistics != null) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 4ed3f92e25..0206989231 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -185,6 +185,34 @@ namespace osu.Game.Scoring [JsonProperty("position")] public int? Position { get; set; } + private bool isLegacyScore; + + /// + /// Whether this represents a legacy (osu!stable) score. + /// + [JsonIgnore] + [NotMapped] + public bool IsLegacyScore + { + get + { + if (isLegacyScore) + return true; + + // The above check will catch legacy online scores that have an appropriate UserString + UserId. + // For non-online scores such as those imported in, a heuristic is used based on the following table: + // + // Mode | UserString | UserId + // --------------- | ---------- | --------- + // stable | | 1 + // lazer | | + // lazer (offline) | Guest | 1 + + return ID > 0 && UserID == 1 && UserString != "Guest"; + } + set => isLegacyScore = value; + } + public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay() { foreach (var key in OrderAttributeUtils.GetValuesInOrder()) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 561ca631b3..8e8147ff39 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -10,6 +10,7 @@ using System.Threading; using JetBrains.Annotations; using Microsoft.EntityFrameworkCore; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.Beatmaps; @@ -149,23 +150,38 @@ namespace osu.Game.Scoring return; } - int? beatmapMaxCombo = score.Beatmap.MaxCombo; + int beatmapMaxCombo; - if (beatmapMaxCombo == null) + if (score.IsLegacyScore) { - if (score.Beatmap.ID == 0 || difficulties == null) + // This score is guaranteed to be an osu!stable score. + // The combo must be determined through either the beatmap's max combo value or the difficulty calculator, as lazer's scoring has changed and the score statistics cannot be used. + if (score.Beatmap.MaxCombo == null) { - // We don't have enough information (max combo) to compute the score, so let's use the provided score. - Value = score.TotalScore; + if (score.Beatmap.ID == 0 || difficulties == null) + { + // We don't have enough information (max combo) to compute the score, so use the provided score. + Value = score.TotalScore; + return; + } + + // We can compute the max combo locally after the async beatmap difficulty computation. + difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token); + difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true); + return; } - // We can compute the max combo locally after the async beatmap difficulty computation. - difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token); - difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true); + beatmapMaxCombo = score.Beatmap.MaxCombo.Value; } else - updateScore(beatmapMaxCombo.Value); + { + // This score is guaranteed to be an osu!lazer score. + // The combo must be determined through the score's statistics, as both the beatmap's max combo and the difficulty calculator will provide osu!stable combo values. + beatmapMaxCombo = Enum.GetValues(typeof(HitResult)).OfType().Where(r => r.AffectsCombo()).Select(r => score.Statistics.GetOrDefault(r)).Sum(); + } + + updateScore(beatmapMaxCombo); } private void updateScore(int beatmapMaxCombo) From cd794eaa65ac483ce7f59d8a322b7e5890ba16fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 19:07:40 +0900 Subject: [PATCH 3487/6909] Add basic selection box with drag handles --- .../Editing/TestSceneBlueprintSelectBox.cs | 39 +++ .../Compose/Components/ComposeSelectionBox.cs | 308 ++++++++++++++++++ .../Edit/Compose/Components/DragBox.cs | 2 +- .../Compose/Components/SelectionHandler.cs | 16 +- 4 files changed, 349 insertions(+), 16 deletions(-) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelectBox.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelectBox.cs new file mode 100644 index 0000000000..dd44472c09 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelectBox.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneComposeSelectBox : OsuTestScene + { + public TestSceneComposeSelectBox() + { + ComposeSelectionBox selectionBox = null; + + AddStep("create box", () => + Child = new Container + { + Size = new Vector2(300), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + selectionBox = new ComposeSelectionBox + { + CanRotate = true, + CanScaleX = true, + CanScaleY = true + } + } + }); + + AddToggleStep("toggle rotation", state => selectionBox.CanRotate = state); + AddToggleStep("toggle x", state => selectionBox.CanScaleX = state); + AddToggleStep("toggle y", state => selectionBox.CanScaleY = state); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs new file mode 100644 index 0000000000..c7fc078b98 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs @@ -0,0 +1,308 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public class ComposeSelectionBox : CompositeDrawable + { + public Action OnRotation; + public Action OnScaleX; + public Action OnScaleY; + + private bool canRotate; + + public bool CanRotate + { + get => canRotate; + set + { + canRotate = value; + recreate(); + } + } + + private bool canScaleX; + + public bool CanScaleX + { + get => canScaleX; + set + { + canScaleX = value; + recreate(); + } + } + + private bool canScaleY; + + public bool CanScaleY + { + get => canScaleY; + set + { + canScaleY = value; + recreate(); + } + } + + public const float BORDER_RADIUS = 3; + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + recreate(); + } + + private void recreate() + { + if (LoadState < LoadState.Loading) + return; + + InternalChildren = new Drawable[] + { + new Container + { + Masking = true, + BorderThickness = BORDER_RADIUS, + BorderColour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + + AlwaysPresent = true, + Alpha = 0 + }, + } + }, + }; + + if (CanRotate) + { + const float separation = 40; + + AddRangeInternal(new Drawable[] + { + new Box + { + Colour = colours.YellowLight, + Blending = BlendingParameters.Additive, + Alpha = 0.3f, + Size = new Vector2(BORDER_RADIUS, separation), + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, + new RotationDragHandle + { + Anchor = Anchor.TopCentre, + Y = -separation, + HandleDrag = e => OnRotation?.Invoke(e) + } + }); + } + + if (CanScaleY) + { + AddRangeInternal(new[] + { + new DragHandle + { + Anchor = Anchor.TopCentre, + HandleDrag = e => OnScaleY?.Invoke(e, Anchor.TopCentre) + }, + new DragHandle + { + Anchor = Anchor.BottomCentre, + HandleDrag = e => OnScaleY?.Invoke(e, Anchor.BottomCentre) + }, + }); + } + + if (CanScaleX) + { + AddRangeInternal(new[] + { + new DragHandle + { + Anchor = Anchor.CentreLeft, + HandleDrag = e => OnScaleX?.Invoke(e, Anchor.CentreLeft) + }, + new DragHandle + { + Anchor = Anchor.CentreRight, + HandleDrag = e => OnScaleX?.Invoke(e, Anchor.CentreRight) + }, + }); + } + + if (CanScaleX && CanScaleY) + { + AddRangeInternal(new[] + { + new DragHandle + { + Anchor = Anchor.TopLeft, + HandleDrag = e => + { + OnScaleX?.Invoke(e, Anchor.TopLeft); + OnScaleY?.Invoke(e, Anchor.TopLeft); + } + }, + new DragHandle + { + Anchor = Anchor.TopRight, + HandleDrag = e => + { + OnScaleX?.Invoke(e, Anchor.TopRight); + OnScaleY?.Invoke(e, Anchor.TopRight); + } + }, + new DragHandle + { + Anchor = Anchor.BottomLeft, + HandleDrag = e => + { + OnScaleX?.Invoke(e, Anchor.BottomLeft); + OnScaleY?.Invoke(e, Anchor.BottomLeft); + } + }, + new DragHandle + { + Anchor = Anchor.BottomRight, + HandleDrag = e => + { + OnScaleX?.Invoke(e, Anchor.BottomRight); + OnScaleY?.Invoke(e, Anchor.BottomRight); + } + }, + }); + } + } + + private class RotationDragHandle : DragHandle + { + private SpriteIcon icon; + + [BackgroundDependencyLoader] + private void load() + { + Size *= 2; + + AddInternal(icon = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Icon = FontAwesome.Solid.Redo, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + + protected override void UpdateHoverState() + { + base.UpdateHoverState(); + icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black; + } + } + + private class DragHandle : Container + { + public Action HandleDrag { get; set; } + + private Circle circle; + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(10); + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + circle = new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + UpdateHoverState(); + } + + protected override bool OnHover(HoverEvent e) + { + UpdateHoverState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + UpdateHoverState(); + } + + protected bool HandlingMouse; + + protected override bool OnMouseDown(MouseDownEvent e) + { + HandlingMouse = true; + UpdateHoverState(); + return true; + } + + protected override bool OnDragStart(DragStartEvent e) => true; + + protected override void OnDrag(DragEvent e) + { + HandleDrag?.Invoke(e); + base.OnDrag(e); + } + + protected override void OnDragEnd(DragEndEvent e) + { + HandlingMouse = false; + UpdateHoverState(); + base.OnDragEnd(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + HandlingMouse = false; + UpdateHoverState(); + base.OnMouseUp(e); + } + + protected virtual void UpdateHoverState() + { + circle.Colour = HandlingMouse ? colours.GrayF : (IsHovered ? colours.Red : colours.YellowDark); + this.ScaleTo(HandlingMouse || IsHovered ? 1.5f : 1, 100, Easing.OutQuint); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index 0615ebfc20..0ec981203a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Masking = true, BorderColour = Color4.White, - BorderThickness = SelectionHandler.BORDER_RADIUS, + BorderThickness = ComposeSelectionBox.BORDER_RADIUS, Child = new Box { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index a0220cf987..ef97403d02 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -32,8 +32,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { - public const float BORDER_RADIUS = 2; - public IEnumerable SelectedBlueprints => selectedBlueprints; private readonly List selectedBlueprints; @@ -69,19 +67,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Children = new Drawable[] { - new Container - { - RelativeSizeAxes = Axes.Both, - Masking = true, - BorderThickness = BORDER_RADIUS, - BorderColour = colours.YellowDark, - Child = new Box - { - RelativeSizeAxes = Axes.Both, - AlwaysPresent = true, - Alpha = 0 - } - }, + new ComposeSelectionBox(), new Container { Name = "info text", From 265bba1a886db2e988bbed5a502597128badda1b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 19:19:48 +0900 Subject: [PATCH 3488/6909] Add test coverage of event handling --- .../Editing/TestSceneBlueprintSelectBox.cs | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelectBox.cs index dd44472c09..4b12000fc3 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelectBox.cs @@ -3,6 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -10,23 +11,29 @@ namespace osu.Game.Tests.Visual.Editing { public class TestSceneComposeSelectBox : OsuTestScene { + private Container selectionArea; + public TestSceneComposeSelectBox() { ComposeSelectionBox selectionBox = null; AddStep("create box", () => - Child = new Container + Child = selectionArea = new Container { Size = new Vector2(300), + Position = -new Vector2(150), Anchor = Anchor.Centre, - Origin = Anchor.Centre, Children = new Drawable[] { selectionBox = new ComposeSelectionBox { CanRotate = true, CanScaleX = true, - CanScaleY = true + CanScaleY = true, + + OnRotation = handleRotation, + OnScaleX = handleScaleX, + OnScaleY = handleScaleY, } } }); @@ -35,5 +42,26 @@ namespace osu.Game.Tests.Visual.Editing AddToggleStep("toggle x", state => selectionBox.CanScaleX = state); AddToggleStep("toggle y", state => selectionBox.CanScaleY = state); } + + private void handleScaleY(DragEvent e, Anchor reference) + { + int direction = (reference & Anchor.y0) > 0 ? -1 : 1; + if (direction < 0) + selectionArea.Y += e.Delta.Y; + selectionArea.Height += direction * e.Delta.Y; + } + + private void handleScaleX(DragEvent e, Anchor reference) + { + int direction = (reference & Anchor.x0) > 0 ? -1 : 1; + if (direction < 0) + selectionArea.X += e.Delta.X; + selectionArea.Width += direction * e.Delta.X; + } + + private void handleRotation(DragEvent e) + { + selectionArea.Rotation += e.Delta.X; + } } } From 0a10e40ce0091d40dcd0bc7e7cecebffe912ea49 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 19:43:50 +0900 Subject: [PATCH 3489/6909] Add scaling support to osu! editor --- .../Edit/OsuSelectionHandler.cs | 97 ++++++++++++++++++- .../Compose/Components/SelectionHandler.cs | 4 +- 2 files changed, 96 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 9418565907..e29536d6b2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -10,7 +12,55 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuSelectionHandler : SelectionHandler { - public override bool HandleMovement(MoveSelectionEvent moveEvent) + public override ComposeSelectionBox CreateSelectionBox() + => new ComposeSelectionBox + { + CanRotate = true, + CanScaleX = true, + CanScaleY = true, + + // OnRotation = handleRotation, + OnScaleX = handleScaleX, + OnScaleY = handleScaleY, + }; + + private void handleScaleY(DragEvent e, Anchor reference) + { + int direction = (reference & Anchor.y0) > 0 ? -1 : 1; + + if (direction < 0) + { + // when resizing from a top drag handle, we want to move the selection first + if (!moveSelection(new Vector2(0, e.Delta.Y))) + return; + } + + scaleSelection(new Vector2(0, direction * e.Delta.Y)); + } + + private void handleScaleX(DragEvent e, Anchor reference) + { + int direction = (reference & Anchor.x0) > 0 ? -1 : 1; + + if (direction < 0) + { + // when resizing from a top drag handle, we want to move the selection first + if (!moveSelection(new Vector2(e.Delta.X, 0))) + return; + } + + scaleSelection(new Vector2(direction * e.Delta.X, 0)); + } + + private void handleRotation(DragEvent e) + { + // selectionArea.Rotation += e.Delta.X; + } + + public override bool HandleMovement(MoveSelectionEvent moveEvent) => + moveSelection(moveEvent.InstantDelta); + + private bool scaleSelection(Vector2 scale) { Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); @@ -25,8 +75,47 @@ namespace osu.Game.Rulesets.Osu.Edit } // Stacking is not considered - minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta)); - maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + moveEvent.InstantDelta, h.Position + moveEvent.InstantDelta)); + minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition, h.Position)); + maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition, h.Position)); + } + + Vector2 size = maxPosition - minPosition; + Vector2 newSize = size + scale; + + foreach (var h in SelectedHitObjects.OfType()) + { + if (h is Spinner) + { + // Spinners don't support position adjustments + continue; + } + + if (scale.X != 1) + h.Position = new Vector2(minPosition.X + (h.X - minPosition.X) / size.X * newSize.X, h.Y); + if (scale.Y != 1) + h.Position = new Vector2(h.X, minPosition.Y + (h.Y - minPosition.Y) / size.Y * newSize.Y); + } + + return true; + } + + private bool moveSelection(Vector2 delta) + { + Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); + Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); + + // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted + foreach (var h in SelectedHitObjects.OfType()) + { + if (h is Spinner) + { + // Spinners don't support position adjustments + continue; + } + + // Stacking is not considered + minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + delta, h.Position + delta)); + maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + delta, h.Position + delta)); } if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight) @@ -40,7 +129,7 @@ namespace osu.Game.Rulesets.Osu.Edit continue; } - h.Position += moveEvent.InstantDelta; + h.Position += delta; } return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index ef97403d02..39e413ef05 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Children = new Drawable[] { - new ComposeSelectionBox(), + CreateSelectionBox(), new Container { Name = "info text", @@ -91,6 +91,8 @@ namespace osu.Game.Screens.Edit.Compose.Components }; } + public virtual ComposeSelectionBox CreateSelectionBox() => new ComposeSelectionBox(); + #region User Input Handling /// From 33b24b6f46a6b9ffa596a443d5ddaf67aa40940e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 19:50:03 +0900 Subject: [PATCH 3490/6909] Refactor to be able to get a quad for the current selection --- .../Edit/OsuSelectionHandler.cs | 69 ++++++++++++++----- 1 file changed, 52 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index e29536d6b2..7f4ee54243 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.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.Linq; using osu.Framework.Graphics; +using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Events; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -54,7 +56,6 @@ namespace osu.Game.Rulesets.Osu.Edit private void handleRotation(DragEvent e) { - // selectionArea.Rotation += e.Delta.X; } public override bool HandleMovement(MoveSelectionEvent moveEvent) => @@ -62,24 +63,11 @@ namespace osu.Game.Rulesets.Osu.Edit private bool scaleSelection(Vector2 scale) { - Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); - Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); + Quad quad = getSelectionQuad(); - // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted - foreach (var h in SelectedHitObjects.OfType()) - { - if (h is Spinner) - { - // Spinners don't support position adjustments - continue; - } + Vector2 minPosition = quad.TopLeft; - // Stacking is not considered - minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition, h.Position)); - maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition, h.Position)); - } - - Vector2 size = maxPosition - minPosition; + Vector2 size = quad.Size; Vector2 newSize = size + scale; foreach (var h in SelectedHitObjects.OfType()) @@ -134,5 +122,52 @@ namespace osu.Game.Rulesets.Osu.Edit return true; } + + private Quad getSelectionQuad() + { + Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); + Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); + + // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted + foreach (var h in SelectedHitObjects.OfType()) + { + if (h is Spinner) + { + // Spinners don't support position adjustments + continue; + } + + // Stacking is not considered + minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition, h.Position)); + maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition, h.Position)); + } + + Vector2 size = maxPosition - minPosition; + + return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); + } + + /// + /// Returns rotated position from a given point. + /// + /// The point. + /// The center to rotate around. + /// The angle to rotate (in degrees). + internal static Vector2 Rotate(Vector2 p, Vector2 center, int angle) + { + angle = -angle; + + p.X -= center.X; + p.Y -= center.Y; + + Vector2 ret; + ret.X = (float)(p.X * Math.Cos(angle / 180f * Math.PI) + p.Y * Math.Sin(angle / 180f * Math.PI)); + ret.Y = (float)(p.X * -Math.Sin(angle / 180f * Math.PI) + p.Y * Math.Cos(angle / 180f * Math.PI)); + + ret.X += center.X; + ret.Y += center.Y; + + return ret; + } } } From 934db14e037cedb82666904b7f390df62b426f90 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 20:00:19 +0900 Subject: [PATCH 3491/6909] Add rotation support --- .../Edit/OsuSelectionHandler.cs | 37 ++++++++++++++++++- 1 file changed, 35 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 7f4ee54243..84056a69c7 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Events; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleX = true, CanScaleY = true, - // OnRotation = handleRotation, + OnRotation = handleRotation, OnScaleX = handleScaleX, OnScaleY = handleScaleY, }; @@ -54,13 +55,45 @@ namespace osu.Game.Rulesets.Osu.Edit scaleSelection(new Vector2(direction * e.Delta.X, 0)); } + private Vector2? centre; + private void handleRotation(DragEvent e) { + rotateSelection(e.Delta.X); } public override bool HandleMovement(MoveSelectionEvent moveEvent) => moveSelection(moveEvent.InstantDelta); + private bool rotateSelection(in float delta) + { + Quad quad = getSelectionQuad(); + + if (!centre.HasValue) + centre = quad.Centre; + + foreach (var h in SelectedHitObjects.OfType()) + { + if (h is Spinner) + { + // Spinners don't support position adjustments + continue; + } + + h.Position = Rotate(h.Position, centre.Value, delta); + + if (h is IHasPath path) + { + foreach (var point in path.Path.ControlPoints) + { + point.Position.Value = Rotate(point.Position.Value, Vector2.Zero, delta); + } + } + } + + return true; + } + private bool scaleSelection(Vector2 scale) { Quad quad = getSelectionQuad(); @@ -153,7 +186,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// The point. /// The center to rotate around. /// The angle to rotate (in degrees). - internal static Vector2 Rotate(Vector2 p, Vector2 center, int angle) + internal static Vector2 Rotate(Vector2 p, Vector2 center, float angle) { angle = -angle; From a2e2cca396e2765221185f95644157afc0b51bd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 20:08:28 +0900 Subject: [PATCH 3492/6909] Add proper change handler support --- .../Edit/OsuSelectionHandler.cs | 14 ++++ .../Compose/Components/ComposeSelectionBox.cs | 64 ++++++++++++++++--- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 84056a69c7..126fdf0932 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -22,11 +22,25 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleX = true, CanScaleY = true, + OperationStarted = onStart, + OperationEnded = onEnd, + OnRotation = handleRotation, OnScaleX = handleScaleX, OnScaleY = handleScaleY, }; + private void onEnd() + { + ChangeHandler.EndChange(); + centre = null; + } + + private void onStart() + { + ChangeHandler.BeginChange(); + } + private void handleScaleY(DragEvent e, Anchor reference) { int direction = (reference & Anchor.y0) > 0 ? -1 : 1; diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs index c7fc078b98..dba1965569 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs @@ -20,6 +20,9 @@ namespace osu.Game.Screens.Edit.Compose.Components public Action OnScaleX; public Action OnScaleY; + public Action OperationStarted; + public Action OperationEnded; + private bool canRotate; public bool CanRotate @@ -114,7 +117,9 @@ namespace osu.Game.Screens.Edit.Compose.Components { Anchor = Anchor.TopCentre, Y = -separation, - HandleDrag = e => OnRotation?.Invoke(e) + HandleDrag = e => OnRotation?.Invoke(e), + OperationStarted = operationStarted, + OperationEnded = operationEnded } }); } @@ -126,12 +131,16 @@ namespace osu.Game.Screens.Edit.Compose.Components new DragHandle { Anchor = Anchor.TopCentre, - HandleDrag = e => OnScaleY?.Invoke(e, Anchor.TopCentre) + HandleDrag = e => OnScaleY?.Invoke(e, Anchor.TopCentre), + OperationStarted = operationStarted, + OperationEnded = operationEnded }, new DragHandle { Anchor = Anchor.BottomCentre, - HandleDrag = e => OnScaleY?.Invoke(e, Anchor.BottomCentre) + HandleDrag = e => OnScaleY?.Invoke(e, Anchor.BottomCentre), + OperationStarted = operationStarted, + OperationEnded = operationEnded }, }); } @@ -143,12 +152,16 @@ namespace osu.Game.Screens.Edit.Compose.Components new DragHandle { Anchor = Anchor.CentreLeft, - HandleDrag = e => OnScaleX?.Invoke(e, Anchor.CentreLeft) + HandleDrag = e => OnScaleX?.Invoke(e, Anchor.CentreLeft), + OperationStarted = operationStarted, + OperationEnded = operationEnded }, new DragHandle { Anchor = Anchor.CentreRight, - HandleDrag = e => OnScaleX?.Invoke(e, Anchor.CentreRight) + HandleDrag = e => OnScaleX?.Invoke(e, Anchor.CentreRight), + OperationStarted = operationStarted, + OperationEnded = operationEnded }, }); } @@ -164,7 +177,9 @@ namespace osu.Game.Screens.Edit.Compose.Components { OnScaleX?.Invoke(e, Anchor.TopLeft); OnScaleY?.Invoke(e, Anchor.TopLeft); - } + }, + OperationStarted = operationStarted, + OperationEnded = operationEnded }, new DragHandle { @@ -173,7 +188,9 @@ namespace osu.Game.Screens.Edit.Compose.Components { OnScaleX?.Invoke(e, Anchor.TopRight); OnScaleY?.Invoke(e, Anchor.TopRight); - } + }, + OperationStarted = operationStarted, + OperationEnded = operationEnded }, new DragHandle { @@ -182,7 +199,9 @@ namespace osu.Game.Screens.Edit.Compose.Components { OnScaleX?.Invoke(e, Anchor.BottomLeft); OnScaleY?.Invoke(e, Anchor.BottomLeft); - } + }, + OperationStarted = operationStarted, + OperationEnded = operationEnded }, new DragHandle { @@ -191,12 +210,28 @@ namespace osu.Game.Screens.Edit.Compose.Components { OnScaleX?.Invoke(e, Anchor.BottomRight); OnScaleY?.Invoke(e, Anchor.BottomRight); - } + }, + OperationStarted = operationStarted, + OperationEnded = operationEnded }, }); } } + private int activeOperations; + + private void operationEnded() + { + if (--activeOperations == 0) + OperationEnded?.Invoke(); + } + + private void operationStarted() + { + if (activeOperations++ == 0) + OperationStarted?.Invoke(); + } + private class RotationDragHandle : DragHandle { private SpriteIcon icon; @@ -225,6 +260,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private class DragHandle : Container { + public Action OperationStarted; + public Action OperationEnded; + public Action HandleDrag { get; set; } private Circle circle; @@ -276,7 +314,11 @@ namespace osu.Game.Screens.Edit.Compose.Components return true; } - protected override bool OnDragStart(DragStartEvent e) => true; + protected override bool OnDragStart(DragStartEvent e) + { + OperationStarted?.Invoke(); + return true; + } protected override void OnDrag(DragEvent e) { @@ -287,6 +329,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void OnDragEnd(DragEndEvent e) { HandlingMouse = false; + OperationEnded?.Invoke(); + UpdateHoverState(); base.OnDragEnd(e); } From 5ae6b2cf5b0bea7c5f53fd3c84934a4b57492f34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 20:10:17 +0900 Subject: [PATCH 3493/6909] Fix syntax --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 126fdf0932..505b84e699 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -83,8 +83,7 @@ namespace osu.Game.Rulesets.Osu.Edit { Quad quad = getSelectionQuad(); - if (!centre.HasValue) - centre = quad.Centre; + centre ??= quad.Centre; foreach (var h in SelectedHitObjects.OfType()) { From f93c72dd920a2d239919380bb954dfa2cfdd4cb7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Sep 2020 20:21:13 +0900 Subject: [PATCH 3494/6909] Fix non-matching filename --- ...estSceneBlueprintSelectBox.cs => TestSceneComposeSelectBox.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Tests/Visual/Editing/{TestSceneBlueprintSelectBox.cs => TestSceneComposeSelectBox.cs} (100%) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs similarity index 100% rename from osu.Game.Tests/Visual/Editing/TestSceneBlueprintSelectBox.cs rename to osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs From 42f666cd24456ac823736fb2a6f5a876c092e735 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Tue, 29 Sep 2020 23:04:03 +0930 Subject: [PATCH 3495/6909] Set icon for SDL desktop window --- osu.Desktop/OsuGameDesktop.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 2079f136d2..659730630a 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -138,6 +138,7 @@ namespace osu.Desktop // SDL2 DesktopWindow case DesktopWindow desktopWindow: desktopWindow.CursorState.Value |= CursorState.Hidden; + desktopWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico")); desktopWindow.Title = Name; desktopWindow.DragDrop += f => fileDrop(new[] { f }); break; From 35f7de2084ada88bbe7c25de3e36d4e4266c521c Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 28 Sep 2020 19:05:22 +0200 Subject: [PATCH 3496/6909] Apply review suggestions. --- osu.Game/Scoring/ScorePerformanceManager.cs | 3 --- .../Ranking/Expanded/Statistics/CounterStatistic.cs | 2 +- .../Expanded/Statistics/PerformanceStatistic.cs | 13 ++++++++++--- 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index 0a57ccbd1f..a27e38fb07 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -27,9 +27,6 @@ namespace osu.Game.Scoring /// An optional to cancel the operation. public async Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) { - if (score.PP.HasValue) - return score.PP.Value; - if (tryGetExisting(score, out var perf, out var lookupKey)) return perf; diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index 1f8deb4d59..368075b13b 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { private readonly int count; - protected RollingCounter Counter; + protected RollingCounter Counter { get; private set; } /// /// Creates a new . diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 6c2ad5844b..7e342a33f1 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -25,8 +25,15 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics [BackgroundDependencyLoader] private void load(ScorePerformanceManager performanceManager) { - performanceManager.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => performance.Value = (int)t.Result), cancellationTokenSource.Token); + if (score.PP.HasValue) + { + performance.Value = (int)score.PP.Value; + } + else + { + performanceManager.CalculatePerformanceAsync(score, cancellationTokenSource.Token) + .ContinueWith(t => Schedule(() => performance.Value = (int)t.Result), cancellationTokenSource.Token); + } } public override void Appear() @@ -37,7 +44,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics protected override void Dispose(bool isDisposing) { - cancellationTokenSource.Cancel(); + cancellationTokenSource?.Cancel(); base.Dispose(isDisposing); } } From 2766cf73b492ea751723b387399a40c4b6e20bc3 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 29 Sep 2020 18:32:02 +0200 Subject: [PATCH 3497/6909] Reuse BeatmapDifficultyManager cache for beatmap difficulty attributes. --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- .../Difficulty/CatchPerformanceCalculator.cs | 4 ++-- .../Difficulty/ManiaPerformanceCalculator.cs | 4 ++-- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Difficulty/OsuPerformanceCalculator.cs | 4 ++-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Difficulty/TaikoPerformanceCalculator.cs | 4 ++-- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 9 ++++++--- .../Rulesets/Difficulty/PerformanceCalculator.cs | 4 ++-- osu.Game/Rulesets/Ruleset.cs | 2 +- osu.Game/Scoring/ScorePerformanceManager.cs | 15 ++++++++------- 12 files changed, 29 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index ca75a816f1..1a9a79f6ff 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Catch public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) => new CatchPerformanceCalculator(this, beatmap, score); public int LegacyID => 2; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index d700f79e5b..33c807333f 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -25,8 +25,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty private int tinyTicksMissed; private int misses; - public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) - : base(ruleset, beatmap, score) + public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) + : base(ruleset, beatmap, score, attributes) { } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 91383c5548..5a32509b8d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -29,8 +29,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty private int countMeh; private int countMiss; - public ManiaPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) - : base(ruleset, beatmap, score) + public ManiaPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes) + : base(ruleset, beatmap, score, attributes) { } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 71ac85dd1b..93390c1f0a 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 IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) => new ManiaPerformanceCalculator(this, beatmap, score, attributes); public const string SHORT_NAME = "mania"; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 6f4c0f9cfa..e3faa55d9e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int countMeh; private int countMiss; - public OsuPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) - : base(ruleset, beatmap, score) + public OsuPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes) + : base(ruleset, beatmap, score, attributes) { countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 7f4a0dcbbb..452491361a 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new OsuPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) => new OsuPerformanceCalculator(this, beatmap, score, attributes); public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index b9d95a6ba6..26840d72f1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -24,8 +24,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countMeh; private int countMiss; - public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) - : base(ruleset, beatmap, score) + public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) + : base(ruleset, beatmap, score, attributes) { } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 9d485e3f20..093084805d 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new TaikoPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) => new TaikoPerformanceCalculator(this, beatmap, score, attributes); public int LegacyID => 1; diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index e9d26683c3..ee85908aa2 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Lists; using osu.Framework.Threading; using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; namespace osu.Game.Beatmaps @@ -207,7 +208,7 @@ namespace osu.Game.Beatmaps var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); var attributes = calculator.Calculate(key.Mods); - return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo); + return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo, attributes); } catch { @@ -300,11 +301,13 @@ namespace osu.Game.Beatmaps public readonly double Stars; public readonly int MaxCombo; - public StarDifficulty(double stars, int maxCombo) + public readonly DifficultyAttributes Attributes; + + public StarDifficulty(double stars, int maxCombo, DifficultyAttributes attributes = null) { Stars = stars; MaxCombo = maxCombo; - + Attributes = attributes; // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) } } diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index ac3b817840..fc1d232a00 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -21,14 +21,14 @@ namespace osu.Game.Rulesets.Difficulty protected double TimeRate { get; private set; } = 1; - protected PerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) + protected PerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) { Ruleset = ruleset; Score = score; Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); - Attributes = ruleset.CreateDifficultyCalculator(beatmap).Calculate(score.Mods); + Attributes = attributes ?? ruleset.CreateDifficultyCalculator(beatmap).Calculate(score.Mods); ApplyMods(score.Mods); } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 915544d010..a8f62bca4a 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -158,7 +158,7 @@ namespace osu.Game.Rulesets public abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap); - public virtual PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => null; + public virtual PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score, DifficultyAttributes attributes = null) => null; public virtual HitObjectComposer CreateHitObjectComposer() => null; diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index a27e38fb07..97b4be7edc 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -20,6 +20,9 @@ namespace osu.Game.Scoring [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved] + private BeatmapDifficultyManager difficultyManager { get; set; } + /// /// Calculates performance for the given . /// @@ -30,10 +33,7 @@ namespace osu.Game.Scoring if (tryGetExisting(score, out var perf, out var lookupKey)) return perf; - return await Task.Factory.StartNew(() => - { - return computePerformance(score, lookupKey, token); - }, token); + return await computePerformanceAsync(score, lookupKey, token); } private bool tryGetExisting(ScoreInfo score, out double performance, out PerformanceCacheLookup lookupKey) @@ -43,14 +43,15 @@ namespace osu.Game.Scoring return performanceCache.TryGetValue(lookupKey, out performance); } - private double computePerformance(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) + private async Task computePerformanceAsync(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) { var beatmap = beatmapManager.GetWorkingBeatmap(score.Beatmap); + var attributes = await difficultyManager.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); if (token.IsCancellationRequested) return default; - var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(beatmap, score); + var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(beatmap, score, attributes.Attributes); var total = calculator.Calculate(); performanceCache[lookupKey] = total; @@ -74,7 +75,7 @@ namespace osu.Game.Scoring TotalScore = info.TotalScore; Combo = info.Combo; Mods = info.Mods; - RulesetId = info.Ruleset.ID.Value; + RulesetId = info.Ruleset.ID ?? 0; } public override int GetHashCode() From 1386c9fe668e13c89259c44d92e704503eff0e64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 12:45:43 +0900 Subject: [PATCH 3498/6909] Standardise time display formats across the editor --- .../Extensions/EditorDisplayExtensions.cs | 26 +++++++++++++++++++ .../Edit/Components/TimeInfoContainer.cs | 6 ++--- .../Screens/Edit/Timing/ControlPointTable.cs | 3 ++- 3 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Extensions/EditorDisplayExtensions.cs diff --git a/osu.Game/Extensions/EditorDisplayExtensions.cs b/osu.Game/Extensions/EditorDisplayExtensions.cs new file mode 100644 index 0000000000..f749b88b46 --- /dev/null +++ b/osu.Game/Extensions/EditorDisplayExtensions.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; + +namespace osu.Game.Extensions +{ + public static class EditorDisplayExtensions + { + /// + /// Get an editor formatted string (mm:ss:mss) + /// + /// A time value in milliseconds. + /// An editor formatted display string. + public static string ToEditorFormattedString(this double milliseconds) => + ToEditorFormattedString(TimeSpan.FromMilliseconds(milliseconds)); + + /// + /// Get an editor formatted string (mm:ss:mss) + /// + /// A time value. + /// An editor formatted display string. + public static string ToEditorFormattedString(this TimeSpan timeSpan) => + $"{(timeSpan < TimeSpan.Zero ? "-" : string.Empty)}{timeSpan:mm\\:ss\\:fff}"; + } +} diff --git a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs index c68eeeb4f9..0a8c339559 100644 --- a/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs +++ b/osu.Game/Screens/Edit/Components/TimeInfoContainer.cs @@ -3,8 +3,8 @@ using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; -using System; using osu.Framework.Allocation; +using osu.Game.Extensions; using osu.Game.Graphics; namespace osu.Game.Screens.Edit.Components @@ -35,9 +35,7 @@ namespace osu.Game.Screens.Edit.Components protected override void Update() { base.Update(); - - var timespan = TimeSpan.FromMilliseconds(editorClock.CurrentTime); - trackTimer.Text = $"{(timespan < TimeSpan.Zero ? "-" : string.Empty)}{timespan:mm\\:ss\\:fff}"; + trackTimer.Text = editorClock.CurrentTime.ToEditorFormattedString(); } } } diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index c0c0bcead2..87af4546f1 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -89,7 +90,7 @@ namespace osu.Game.Screens.Edit.Timing }, new OsuSpriteText { - Text = $"{group.Time:n0}ms", + Text = group.Time.ToEditorFormattedString(), Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold) }, new ControlGroupAttributes(group), From 99a3801267d7e45daea36638c695d146385c7072 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 13:02:05 +0900 Subject: [PATCH 3499/6909] Tidy up scale/rotation operation code --- .../Edit/OsuSelectionHandler.cs | 64 ++++++++----------- 1 file changed, 28 insertions(+), 36 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 505b84e699..c7be921a4e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -22,24 +23,25 @@ namespace osu.Game.Rulesets.Osu.Edit CanScaleX = true, CanScaleY = true, - OperationStarted = onStart, - OperationEnded = onEnd, + OperationStarted = () => ChangeHandler.BeginChange(), + OperationEnded = () => + { + ChangeHandler.EndChange(); + referenceOrigin = null; + }, - OnRotation = handleRotation, + OnRotation = e => rotateSelection(e.Delta.X), OnScaleX = handleScaleX, OnScaleY = handleScaleY, }; - private void onEnd() - { - ChangeHandler.EndChange(); - centre = null; - } + public override bool HandleMovement(MoveSelectionEvent moveEvent) => + moveSelection(moveEvent.InstantDelta); - private void onStart() - { - ChangeHandler.BeginChange(); - } + /// + /// During a transform, the initial origin is stored so it can be used throughout the operation. + /// + private Vector2? referenceOrigin; private void handleScaleY(DragEvent e, Anchor reference) { @@ -61,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Edit if (direction < 0) { - // when resizing from a top drag handle, we want to move the selection first + // when resizing from a left drag handle, we want to move the selection first if (!moveSelection(new Vector2(e.Delta.X, 0))) return; } @@ -69,21 +71,11 @@ namespace osu.Game.Rulesets.Osu.Edit scaleSelection(new Vector2(direction * e.Delta.X, 0)); } - private Vector2? centre; - - private void handleRotation(DragEvent e) - { - rotateSelection(e.Delta.X); - } - - public override bool HandleMovement(MoveSelectionEvent moveEvent) => - moveSelection(moveEvent.InstantDelta); - private bool rotateSelection(in float delta) { Quad quad = getSelectionQuad(); - centre ??= quad.Centre; + referenceOrigin ??= quad.Centre; foreach (var h in SelectedHitObjects.OfType()) { @@ -93,13 +85,13 @@ namespace osu.Game.Rulesets.Osu.Edit continue; } - h.Position = Rotate(h.Position, centre.Value, delta); + h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); if (h is IHasPath path) { foreach (var point in path.Path.ControlPoints) { - point.Position.Value = Rotate(point.Position.Value, Vector2.Zero, delta); + point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta); } } } @@ -194,24 +186,24 @@ namespace osu.Game.Rulesets.Osu.Edit } /// - /// Returns rotated position from a given point. + /// Rotate a point around an arbitrary origin. /// - /// The point. - /// The center to rotate around. + /// The point. + /// The centre origin to rotate around. /// The angle to rotate (in degrees). - internal static Vector2 Rotate(Vector2 p, Vector2 center, float angle) + private static Vector2 rotatePointAroundOrigin(Vector2 point, Vector2 origin, float angle) { angle = -angle; - p.X -= center.X; - p.Y -= center.Y; + point.X -= origin.X; + point.Y -= origin.Y; Vector2 ret; - ret.X = (float)(p.X * Math.Cos(angle / 180f * Math.PI) + p.Y * Math.Sin(angle / 180f * Math.PI)); - ret.Y = (float)(p.X * -Math.Sin(angle / 180f * Math.PI) + p.Y * Math.Cos(angle / 180f * Math.PI)); + ret.X = (float)(point.X * Math.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * Math.Sin(angle / 180f * Math.PI)); + ret.Y = (float)(point.X * -Math.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * Math.Cos(angle / 180f * Math.PI)); - ret.X += center.X; - ret.Y += center.Y; + ret.X += origin.X; + ret.Y += origin.Y; return ret; } From f2c26c0927c1cc7dfc5d55b74218be066eead32b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 13:07:24 +0900 Subject: [PATCH 3500/6909] Move information text underneath the selection box --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 39e413ef05..afaa5b0f3d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Children = new Drawable[] { - CreateSelectionBox(), + // todo: should maybe be inside the SelectionBox? new Container { Name = "info text", @@ -86,7 +86,8 @@ namespace osu.Game.Screens.Edit.Compose.Components Font = OsuFont.Default.With(size: 11) } } - } + }, + CreateSelectionBox(), } }; } From 39b55a85df11a2dc36257990d176245ebb9d2500 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 13:52:57 +0900 Subject: [PATCH 3501/6909] Move a lot of the implementation to base SelectionHandler --- .../Edit/OsuSelectionHandler.cs | 56 +++++++++------- .../Compose/Components/SelectionHandler.cs | 67 ++++++++++++++++++- 2 files changed, 94 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index c7be921a4e..a2642bda83 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -5,7 +5,6 @@ using System; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; -using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; @@ -16,24 +15,22 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuSelectionHandler : SelectionHandler { - public override ComposeSelectionBox CreateSelectionBox() - => new ComposeSelectionBox - { - CanRotate = true, - CanScaleX = true, - CanScaleY = true, + protected override void OnSelectionChanged() + { + base.OnSelectionChanged(); - OperationStarted = () => ChangeHandler.BeginChange(), - OperationEnded = () => - { - ChangeHandler.EndChange(); - referenceOrigin = null; - }, + bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider); - OnRotation = e => rotateSelection(e.Delta.X), - OnScaleX = handleScaleX, - OnScaleY = handleScaleY, - }; + SelectionBox.CanRotate = canOperate; + SelectionBox.CanScaleX = canOperate; + SelectionBox.CanScaleY = canOperate; + } + + protected override void OnDragOperationEnded() + { + base.OnDragOperationEnded(); + referenceOrigin = null; + } public override bool HandleMovement(MoveSelectionEvent moveEvent) => moveSelection(moveEvent.InstantDelta); @@ -43,35 +40,35 @@ namespace osu.Game.Rulesets.Osu.Edit /// private Vector2? referenceOrigin; - private void handleScaleY(DragEvent e, Anchor reference) + public override bool HandleScaleY(in float scale, Anchor reference) { int direction = (reference & Anchor.y0) > 0 ? -1 : 1; if (direction < 0) { // when resizing from a top drag handle, we want to move the selection first - if (!moveSelection(new Vector2(0, e.Delta.Y))) - return; + if (!moveSelection(new Vector2(0, scale))) + return false; } - scaleSelection(new Vector2(0, direction * e.Delta.Y)); + return scaleSelection(new Vector2(0, direction * scale)); } - private void handleScaleX(DragEvent e, Anchor reference) + public override bool HandleScaleX(in float scale, Anchor reference) { int direction = (reference & Anchor.x0) > 0 ? -1 : 1; if (direction < 0) { // when resizing from a left drag handle, we want to move the selection first - if (!moveSelection(new Vector2(e.Delta.X, 0))) - return; + if (!moveSelection(new Vector2(scale, 0))) + return false; } - scaleSelection(new Vector2(direction * e.Delta.X, 0)); + return scaleSelection(new Vector2(direction * scale, 0)); } - private bool rotateSelection(in float delta) + public override bool HandleRotation(float delta) { Quad quad = getSelectionQuad(); @@ -96,6 +93,7 @@ namespace osu.Game.Rulesets.Osu.Edit } } + // todo: not always return true; } @@ -161,8 +159,14 @@ namespace osu.Game.Rulesets.Osu.Edit return true; } + /// + /// Returns a gamefield-space quad surrounding the current selection. + /// private Quad getSelectionQuad() { + if (!SelectedHitObjects.Any()) + return new Quad(); + Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index afaa5b0f3d..6cd503b580 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -43,6 +43,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private OsuSpriteText selectionDetailsText; + protected ComposeSelectionBox SelectionBox { get; private set; } + [Resolved(CanBeNull = true)] protected EditorBeatmap EditorBeatmap { get; private set; } @@ -87,12 +89,37 @@ namespace osu.Game.Screens.Edit.Compose.Components } } }, - CreateSelectionBox(), + SelectionBox = CreateSelectionBox(), } }; } - public virtual ComposeSelectionBox CreateSelectionBox() => new ComposeSelectionBox(); + public ComposeSelectionBox CreateSelectionBox() + => new ComposeSelectionBox + { + OperationStarted = OnDragOperationBegan, + OperationEnded = OnDragOperationEnded, + + OnRotation = e => HandleRotation(e.Delta.X), + OnScaleX = (e, anchor) => HandleScaleX(e.Delta.X, anchor), + OnScaleY = (e, anchor) => HandleScaleY(e.Delta.Y, anchor), + }; + + /// + /// Fired when a drag operation ends from the selection box. + /// + protected virtual void OnDragOperationBegan() + { + ChangeHandler.BeginChange(); + } + + /// + /// Fired when a drag operation begins from the selection box. + /// + protected virtual void OnDragOperationEnded() + { + ChangeHandler.EndChange(); + } #region User Input Handling @@ -108,7 +135,30 @@ namespace osu.Game.Screens.Edit.Compose.Components /// 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 virtual bool HandleMovement(MoveSelectionEvent moveEvent) => false; + + /// + /// Handles the selected s being rotated. + /// + /// The delta angle to apply to the selection. + /// Whether any s could be moved. + public virtual bool HandleRotation(float angle) => false; + + /// + /// Handles the selected s being scaled in a vertical direction. + /// + /// The delta scale to apply. + /// The point of reference where the scale is originating from. + /// Whether any s could be moved. + public virtual bool HandleScaleY(in float scale, Anchor anchor) => false; + + /// + /// Handles the selected s being scaled in a horizontal direction. + /// + /// The delta scale to apply. + /// The point of reference where the scale is originating from. + /// Whether any s could be moved. + public virtual bool HandleScaleX(in float scale, Anchor anchor) => false; public bool OnPressed(PlatformAction action) { @@ -211,11 +261,22 @@ namespace osu.Game.Screens.Edit.Compose.Components selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; if (count > 0) + { Show(); + OnSelectionChanged(); + } else Hide(); } + /// + /// Triggered whenever more than one object is selected, on each change. + /// Should update the selection box's state to match supported operations. + /// + protected virtual void OnSelectionChanged() + { + } + protected override void Update() { base.Update(); From 313b0d149fa8d5da93dade143312f8d0d437d0f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 14:41:32 +0900 Subject: [PATCH 3502/6909] Refactor scale and rotation operations to share code better Also adds support for scaling individual sliders. --- .../Edit/OsuSelectionHandler.cs | 160 ++++++++---------- 1 file changed, 69 insertions(+), 91 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index a2642bda83..706c41c2e3 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; @@ -40,84 +41,70 @@ namespace osu.Game.Rulesets.Osu.Edit /// private Vector2? referenceOrigin; - public override bool HandleScaleY(in float scale, Anchor reference) - { - int direction = (reference & Anchor.y0) > 0 ? -1 : 1; + public override bool HandleScaleY(in float scale, Anchor reference) => + scaleSelection(new Vector2(0, ((reference & Anchor.y0) > 0 ? -1 : 1) * scale), reference); - if (direction < 0) - { - // when resizing from a top drag handle, we want to move the selection first - if (!moveSelection(new Vector2(0, scale))) - return false; - } - - return scaleSelection(new Vector2(0, direction * scale)); - } - - public override bool HandleScaleX(in float scale, Anchor reference) - { - int direction = (reference & Anchor.x0) > 0 ? -1 : 1; - - if (direction < 0) - { - // when resizing from a left drag handle, we want to move the selection first - if (!moveSelection(new Vector2(scale, 0))) - return false; - } - - return scaleSelection(new Vector2(direction * scale, 0)); - } + public override bool HandleScaleX(in float scale, Anchor reference) => + scaleSelection(new Vector2(((reference & Anchor.x0) > 0 ? -1 : 1) * scale, 0), reference); public override bool HandleRotation(float delta) { - Quad quad = getSelectionQuad(); + var hitObjects = selectedMovableObjects; + + Quad quad = getSurroundingQuad(hitObjects); referenceOrigin ??= quad.Centre; - foreach (var h in SelectedHitObjects.OfType()) + foreach (var h in hitObjects) { - if (h is Spinner) - { - // Spinners don't support position adjustments - continue; - } - h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); if (h is IHasPath path) { foreach (var point in path.Path.ControlPoints) - { point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta); - } } } - // todo: not always + // this isn't always the case but let's be lenient for now. return true; } - private bool scaleSelection(Vector2 scale) + private bool scaleSelection(Vector2 scale, Anchor reference) { - Quad quad = getSelectionQuad(); + var hitObjects = selectedMovableObjects; - Vector2 minPosition = quad.TopLeft; - - Vector2 size = quad.Size; - Vector2 newSize = size + scale; - - foreach (var h in SelectedHitObjects.OfType()) + // for the time being, allow resizing of slider paths only if the slider is + // the only hit object selected. with a group selection, it's likely the user + // is not looking to change the duration of the slider but expand the whole pattern. + if (hitObjects.Length == 1 && hitObjects.First() is Slider slider) { - if (h is Spinner) - { - // Spinners don't support position adjustments - continue; - } + var quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); + Vector2 delta = Vector2.One + new Vector2(scale.X / quad.Width, scale.Y / quad.Height); - if (scale.X != 1) - h.Position = new Vector2(minPosition.X + (h.X - minPosition.X) / size.X * newSize.X, h.Y); - if (scale.Y != 1) - h.Position = new Vector2(h.X, minPosition.Y + (h.Y - minPosition.Y) / size.Y * newSize.Y); + foreach (var point in slider.Path.ControlPoints) + point.Position.Value *= delta; + } + else + { + // move the selection before scaling if dragging from top or left anchors. + if ((reference & Anchor.x0) > 0 && !moveSelection(new Vector2(-scale.X, 0))) return false; + if ((reference & Anchor.y0) > 0 && !moveSelection(new Vector2(0, -scale.Y))) return false; + + Quad quad = getSurroundingQuad(hitObjects); + + Vector2 minPosition = quad.TopLeft; + + Vector2 size = quad.Size; + Vector2 newSize = size + scale; + + foreach (var h in hitObjects) + { + if (scale.X != 1) + h.Position = new Vector2(minPosition.X + (h.X - minPosition.X) / size.X * newSize.X, h.Y); + if (scale.Y != 1) + h.Position = new Vector2(h.X, minPosition.Y + (h.Y - minPosition.Y) / size.Y * newSize.Y); + } } return true; @@ -125,44 +112,34 @@ namespace osu.Game.Rulesets.Osu.Edit private bool moveSelection(Vector2 delta) { - Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); - Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); + var hitObjects = selectedMovableObjects; - // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted - foreach (var h in SelectedHitObjects.OfType()) - { - if (h is Spinner) - { - // Spinners don't support position adjustments - continue; - } + Quad quad = getSurroundingQuad(hitObjects); - // Stacking is not considered - minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition + delta, h.Position + delta)); - maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition + delta, h.Position + delta)); - } - - if (minPosition.X < 0 || minPosition.Y < 0 || maxPosition.X > DrawWidth || maxPosition.Y > DrawHeight) + if (quad.TopLeft.X + delta.X < 0 || + quad.TopLeft.Y + delta.Y < 0 || + quad.BottomRight.X + delta.X > DrawWidth || + quad.BottomRight.Y + delta.Y > DrawHeight) return false; - foreach (var h in SelectedHitObjects.OfType()) - { - if (h is Spinner) - { - // Spinners don't support position adjustments - continue; - } - + foreach (var h in hitObjects) h.Position += delta; - } return true; } /// - /// Returns a gamefield-space quad surrounding the current selection. + /// Returns a gamefield-space quad surrounding the provided hit objects. /// - private Quad getSelectionQuad() + /// The hit objects to calculate a quad for. + private Quad getSurroundingQuad(OsuHitObject[] hitObjects) => + getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition })); + + /// + /// Returns a gamefield-space quad surrounding the provided points. + /// + /// The points to calculate a quad for. + private Quad getSurroundingQuad(IEnumerable points) { if (!SelectedHitObjects.Any()) return new Quad(); @@ -171,17 +148,10 @@ namespace osu.Game.Rulesets.Osu.Edit Vector2 maxPosition = new Vector2(float.MinValue, float.MinValue); // Go through all hitobjects to make sure they would remain in the bounds of the editor after movement, before any movement is attempted - foreach (var h in SelectedHitObjects.OfType()) + foreach (var p in points) { - if (h is Spinner) - { - // Spinners don't support position adjustments - continue; - } - - // Stacking is not considered - minPosition = Vector2.ComponentMin(minPosition, Vector2.ComponentMin(h.EndPosition, h.Position)); - maxPosition = Vector2.ComponentMax(maxPosition, Vector2.ComponentMax(h.EndPosition, h.Position)); + minPosition = Vector2.ComponentMin(minPosition, p); + maxPosition = Vector2.ComponentMax(maxPosition, p); } Vector2 size = maxPosition - minPosition; @@ -189,6 +159,14 @@ namespace osu.Game.Rulesets.Osu.Edit return new Quad(minPosition.X, minPosition.Y, size.X, size.Y); } + /// + /// All osu! hitobjects which can be moved/rotated/scaled. + /// + private OsuHitObject[] selectedMovableObjects => SelectedHitObjects + .OfType() + .Where(h => !(h is Spinner)) + .ToArray(); + /// /// Rotate a point around an arbitrary origin. /// From f1298bed798f8d31de5b17571f0c4f7bb4362f7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 15:08:56 +0900 Subject: [PATCH 3503/6909] Combine scale operations and tidy up scale drag handle construction --- .../Edit/OsuSelectionHandler.cs | 58 +++++------ .../Editing/TestSceneComposeSelectBox.cs | 31 +++--- .../Compose/Components/ComposeSelectionBox.cs | 99 +++++-------------- .../Compose/Components/SelectionHandler.cs | 19 +--- 4 files changed, 77 insertions(+), 130 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 706c41c2e3..1f250f078d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -41,37 +41,16 @@ namespace osu.Game.Rulesets.Osu.Edit /// private Vector2? referenceOrigin; - public override bool HandleScaleY(in float scale, Anchor reference) => - scaleSelection(new Vector2(0, ((reference & Anchor.y0) > 0 ? -1 : 1) * scale), reference); - - public override bool HandleScaleX(in float scale, Anchor reference) => - scaleSelection(new Vector2(((reference & Anchor.x0) > 0 ? -1 : 1) * scale, 0), reference); - - public override bool HandleRotation(float delta) + public override bool HandleScale(Vector2 scale, Anchor reference) { - var hitObjects = selectedMovableObjects; + // cancel out scale in axes we don't care about (based on which drag handle was used). + if ((reference & Anchor.x1) > 0) scale.X = 0; + if ((reference & Anchor.y1) > 0) scale.Y = 0; - Quad quad = getSurroundingQuad(hitObjects); + // reverse the scale direction if dragging from top or left. + if ((reference & Anchor.x0) > 0) scale.X = -scale.X; + if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; - referenceOrigin ??= quad.Centre; - - foreach (var h in hitObjects) - { - h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); - - if (h is IHasPath path) - { - foreach (var point in path.Path.ControlPoints) - point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta); - } - } - - // this isn't always the case but let's be lenient for now. - return true; - } - - private bool scaleSelection(Vector2 scale, Anchor reference) - { var hitObjects = selectedMovableObjects; // for the time being, allow resizing of slider paths only if the slider is @@ -110,6 +89,29 @@ namespace osu.Game.Rulesets.Osu.Edit return true; } + public override bool HandleRotation(float delta) + { + var hitObjects = selectedMovableObjects; + + Quad quad = getSurroundingQuad(hitObjects); + + referenceOrigin ??= quad.Centre; + + foreach (var h in hitObjects) + { + h.Position = rotatePointAroundOrigin(h.Position, referenceOrigin.Value, delta); + + if (h is IHasPath path) + { + foreach (var point in path.Path.ControlPoints) + point.Position.Value = rotatePointAroundOrigin(point.Position.Value, Vector2.Zero, delta); + } + } + + // this isn't always the case but let's be lenient for now. + return true; + } + private bool moveSelection(Vector2 delta) { var hitObjects = selectedMovableObjects; diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 4b12000fc3..a1fb91024b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -32,8 +32,7 @@ namespace osu.Game.Tests.Visual.Editing CanScaleY = true, OnRotation = handleRotation, - OnScaleX = handleScaleX, - OnScaleY = handleScaleY, + OnScale = handleScale } } }); @@ -43,24 +42,28 @@ namespace osu.Game.Tests.Visual.Editing AddToggleStep("toggle y", state => selectionBox.CanScaleY = state); } - private void handleScaleY(DragEvent e, Anchor reference) + private void handleScale(DragEvent e, Anchor reference) { - int direction = (reference & Anchor.y0) > 0 ? -1 : 1; - if (direction < 0) - selectionArea.Y += e.Delta.Y; - selectionArea.Height += direction * e.Delta.Y; - } + if ((reference & Anchor.y1) == 0) + { + int directionY = (reference & Anchor.y0) > 0 ? -1 : 1; + if (directionY < 0) + selectionArea.Y += e.Delta.Y; + selectionArea.Height += directionY * e.Delta.Y; + } - private void handleScaleX(DragEvent e, Anchor reference) - { - int direction = (reference & Anchor.x0) > 0 ? -1 : 1; - if (direction < 0) - selectionArea.X += e.Delta.X; - selectionArea.Width += direction * e.Delta.X; + if ((reference & Anchor.x1) == 0) + { + int directionX = (reference & Anchor.x0) > 0 ? -1 : 1; + if (directionX < 0) + selectionArea.X += e.Delta.X; + selectionArea.Width += directionX * e.Delta.X; + } } private void handleRotation(DragEvent e) { + // kinda silly and wrong, but just showing that the drag handles work. selectionArea.Rotation += e.Delta.X; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs index dba1965569..424705c755 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs @@ -17,8 +17,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public class ComposeSelectionBox : CompositeDrawable { public Action OnRotation; - public Action OnScaleX; - public Action OnScaleY; + public Action OnScale; public Action OperationStarted; public Action OperationEnded; @@ -128,20 +127,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { AddRangeInternal(new[] { - new DragHandle - { - Anchor = Anchor.TopCentre, - HandleDrag = e => OnScaleY?.Invoke(e, Anchor.TopCentre), - OperationStarted = operationStarted, - OperationEnded = operationEnded - }, - new DragHandle - { - Anchor = Anchor.BottomCentre, - HandleDrag = e => OnScaleY?.Invoke(e, Anchor.BottomCentre), - OperationStarted = operationStarted, - OperationEnded = operationEnded - }, + createDragHandle(Anchor.TopCentre), + createDragHandle(Anchor.BottomCentre), }); } @@ -149,20 +136,8 @@ namespace osu.Game.Screens.Edit.Compose.Components { AddRangeInternal(new[] { - new DragHandle - { - Anchor = Anchor.CentreLeft, - HandleDrag = e => OnScaleX?.Invoke(e, Anchor.CentreLeft), - OperationStarted = operationStarted, - OperationEnded = operationEnded - }, - new DragHandle - { - Anchor = Anchor.CentreRight, - HandleDrag = e => OnScaleX?.Invoke(e, Anchor.CentreRight), - OperationStarted = operationStarted, - OperationEnded = operationEnded - }, + createDragHandle(Anchor.CentreLeft), + createDragHandle(Anchor.CentreRight), }); } @@ -170,52 +145,20 @@ namespace osu.Game.Screens.Edit.Compose.Components { AddRangeInternal(new[] { - new DragHandle - { - Anchor = Anchor.TopLeft, - HandleDrag = e => - { - OnScaleX?.Invoke(e, Anchor.TopLeft); - OnScaleY?.Invoke(e, Anchor.TopLeft); - }, - OperationStarted = operationStarted, - OperationEnded = operationEnded - }, - new DragHandle - { - Anchor = Anchor.TopRight, - HandleDrag = e => - { - OnScaleX?.Invoke(e, Anchor.TopRight); - OnScaleY?.Invoke(e, Anchor.TopRight); - }, - OperationStarted = operationStarted, - OperationEnded = operationEnded - }, - new DragHandle - { - Anchor = Anchor.BottomLeft, - HandleDrag = e => - { - OnScaleX?.Invoke(e, Anchor.BottomLeft); - OnScaleY?.Invoke(e, Anchor.BottomLeft); - }, - OperationStarted = operationStarted, - OperationEnded = operationEnded - }, - new DragHandle - { - Anchor = Anchor.BottomRight, - HandleDrag = e => - { - OnScaleX?.Invoke(e, Anchor.BottomRight); - OnScaleY?.Invoke(e, Anchor.BottomRight); - }, - OperationStarted = operationStarted, - OperationEnded = operationEnded - }, + createDragHandle(Anchor.TopLeft), + createDragHandle(Anchor.TopRight), + createDragHandle(Anchor.BottomLeft), + createDragHandle(Anchor.BottomRight), }); } + + ScaleDragHandle createDragHandle(Anchor anchor) => + new ScaleDragHandle(anchor) + { + HandleDrag = e => OnScale?.Invoke(e, anchor), + OperationStarted = operationStarted, + OperationEnded = operationEnded + }; } private int activeOperations; @@ -232,6 +175,14 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted?.Invoke(); } + private class ScaleDragHandle : DragHandle + { + public ScaleDragHandle(Anchor anchor) + { + Anchor = anchor; + } + } + private class RotationDragHandle : DragHandle { private SpriteIcon icon; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 6cd503b580..5ed9bb65a8 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.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; @@ -101,8 +101,7 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationEnded = OnDragOperationEnded, OnRotation = e => HandleRotation(e.Delta.X), - OnScaleX = (e, anchor) => HandleScaleX(e.Delta.X, anchor), - OnScaleY = (e, anchor) => HandleScaleY(e.Delta.Y, anchor), + OnScale = (e, anchor) => HandleScale(e.Delta, anchor), }; /// @@ -145,20 +144,12 @@ namespace osu.Game.Screens.Edit.Compose.Components public virtual bool HandleRotation(float angle) => false; /// - /// Handles the selected s being scaled in a vertical direction. + /// Handles the selected s being scaled. /// - /// The delta scale to apply. + /// The delta scale to apply, in playfield local coordinates. /// The point of reference where the scale is originating from. /// Whether any s could be moved. - public virtual bool HandleScaleY(in float scale, Anchor anchor) => false; - - /// - /// Handles the selected s being scaled in a horizontal direction. - /// - /// The delta scale to apply. - /// The point of reference where the scale is originating from. - /// Whether any s could be moved. - public virtual bool HandleScaleX(in float scale, Anchor anchor) => false; + public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; public bool OnPressed(PlatformAction action) { From 7fad9ce34ac7a94847143b3a5ffeeb62fba5c1b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 15:17:27 +0900 Subject: [PATCH 3504/6909] Simplify HandleScale method --- .../Edit/OsuSelectionHandler.cs | 38 +++++++++---------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 1f250f078d..6b4f13db35 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -43,13 +43,7 @@ namespace osu.Game.Rulesets.Osu.Edit public override bool HandleScale(Vector2 scale, Anchor reference) { - // cancel out scale in axes we don't care about (based on which drag handle was used). - if ((reference & Anchor.x1) > 0) scale.X = 0; - if ((reference & Anchor.y1) > 0) scale.Y = 0; - - // reverse the scale direction if dragging from top or left. - if ((reference & Anchor.x0) > 0) scale.X = -scale.X; - if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; + adjustScaleFromAnchor(ref scale, reference); var hitObjects = selectedMovableObjects; @@ -58,11 +52,11 @@ namespace osu.Game.Rulesets.Osu.Edit // is not looking to change the duration of the slider but expand the whole pattern. if (hitObjects.Length == 1 && hitObjects.First() is Slider slider) { - var quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); - Vector2 delta = Vector2.One + new Vector2(scale.X / quad.Width, scale.Y / quad.Height); + Quad quad = getSurroundingQuad(slider.Path.ControlPoints.Select(p => p.Position.Value)); + Vector2 pathRelativeDeltaScale = new Vector2(1 + scale.X / quad.Width, 1 + scale.Y / quad.Height); foreach (var point in slider.Path.ControlPoints) - point.Position.Value *= delta; + point.Position.Value *= pathRelativeDeltaScale; } else { @@ -72,23 +66,29 @@ namespace osu.Game.Rulesets.Osu.Edit Quad quad = getSurroundingQuad(hitObjects); - Vector2 minPosition = quad.TopLeft; - - Vector2 size = quad.Size; - Vector2 newSize = size + scale; - foreach (var h in hitObjects) { - if (scale.X != 1) - h.Position = new Vector2(minPosition.X + (h.X - minPosition.X) / size.X * newSize.X, h.Y); - if (scale.Y != 1) - h.Position = new Vector2(h.X, minPosition.Y + (h.Y - minPosition.Y) / size.Y * newSize.Y); + h.Position = new Vector2( + quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X), + quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y) + ); } } return true; } + private static void adjustScaleFromAnchor(ref Vector2 scale, Anchor reference) + { + // cancel out scale in axes we don't care about (based on which drag handle was used). + if ((reference & Anchor.x1) > 0) scale.X = 0; + if ((reference & Anchor.y1) > 0) scale.Y = 0; + + // reverse the scale direction if dragging from top or left. + if ((reference & Anchor.x0) > 0) scale.X = -scale.X; + if ((reference & Anchor.y0) > 0) scale.Y = -scale.Y; + } + public override bool HandleRotation(float delta) { var hitObjects = selectedMovableObjects; From ae9e884a483fd5940a429a4b924c5d1cb059653e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 15:35:25 +0900 Subject: [PATCH 3505/6909] Fix header casing --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 5ed9bb65a8..ee094c6246 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.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; From 414c40d29849932734343123a8b89bf0725381c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 15:45:14 +0900 Subject: [PATCH 3506/6909] Reverse inheritance order of SkinnableSound's pause logic --- .../Objects/Drawables/DrawableSlider.cs | 4 +- .../Objects/Drawables/DrawableSpinner.cs | 4 +- .../Audio/DrumSampleContainer.cs | 8 +-- .../Gameplay/TestSceneSkinnableSound.cs | 4 +- osu.Game/Rulesets/Mods/ModNightcore.cs | 16 ++--- .../Objects/Drawables/DrawableHitObject.cs | 4 +- .../Screens/Play/ISamplePlaybackDisabler.cs | 2 +- osu.Game/Screens/Play/PauseOverlay.cs | 12 +--- osu.Game/Skinning/PausableSkinnableSound.cs | 66 +++++++++++++++++++ osu.Game/Skinning/SkinnableSound.cs | 50 +------------- 10 files changed, 91 insertions(+), 79 deletions(-) create mode 100644 osu.Game/Skinning/PausableSkinnableSound.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 68f203db47..b73ae5eeb9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking.BindValueChanged(updateSlidingSample); } - private SkinnableSound slidingSample; + private PausableSkinnableSound slidingSample; protected override void LoadSamples() { @@ -103,7 +103,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "sliderslide"; - AddInternal(slidingSample = new SkinnableSound(clone) + AddInternal(slidingSample = new PausableSkinnableSound(clone) { Looping = true }); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index b2a706833c..6488c60acf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -84,7 +84,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables isSpinning.BindValueChanged(updateSpinningSample); } - private SkinnableSound spinningSample; + private PausableSkinnableSound spinningSample; private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "spinnerspin"; - AddInternal(spinningSample = new SkinnableSound(clone) + AddInternal(spinningSample = new PausableSkinnableSound(clone) { Volume = { Value = 0 }, Looping = true, diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs index 7c39c040b1..fcf7c529f5 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs @@ -42,9 +42,9 @@ namespace osu.Game.Rulesets.Taiko.Audio } } - private SkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd) + private PausableSkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd) { - var drawable = new SkinnableSound(hitSampleInfo) + var drawable = new PausableSkinnableSound(hitSampleInfo) { LifetimeStart = lifetimeStart, LifetimeEnd = lifetimeEnd @@ -57,8 +57,8 @@ namespace osu.Game.Rulesets.Taiko.Audio public class DrumSample { - public SkinnableSound Centre; - public SkinnableSound Rim; + public PausableSkinnableSound Centre; + public PausableSkinnableSound Rim; } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 8b37cbd06f..8f2011e5dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay private GameplayClock gameplayClock = new GameplayClock(new FramedClock()); private TestSkinSourceContainer skinSource; - private SkinnableSound skinnableSound; + private PausableSkinnableSound skinnableSound; [SetUp] public void SetUp() => Schedule(() => @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay { Clock = gameplayClock, RelativeSizeAxes = Axes.Both, - Child = skinnableSound = new SkinnableSound(new SampleInfo("normal-sliderslide")) + Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide")) }, }; }); diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 4004953cd1..282de3a8e1 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -52,10 +52,10 @@ namespace osu.Game.Rulesets.Mods public class NightcoreBeatContainer : BeatSyncedContainer { - private SkinnableSound hatSample; - private SkinnableSound clapSample; - private SkinnableSound kickSample; - private SkinnableSound finishSample; + private PausableSkinnableSound hatSample; + private PausableSkinnableSound clapSample; + private PausableSkinnableSound kickSample; + private PausableSkinnableSound finishSample; private int? firstBeat; @@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Mods { InternalChildren = new Drawable[] { - hatSample = new SkinnableSound(new SampleInfo("nightcore-hat")), - clapSample = new SkinnableSound(new SampleInfo("nightcore-clap")), - kickSample = new SkinnableSound(new SampleInfo("nightcore-kick")), - finishSample = new SkinnableSound(new SampleInfo("nightcore-finish")), + hatSample = new PausableSkinnableSound(new SampleInfo("nightcore-hat")), + clapSample = new PausableSkinnableSound(new SampleInfo("nightcore-clap")), + kickSample = new PausableSkinnableSound(new SampleInfo("nightcore-kick")), + finishSample = new PausableSkinnableSound(new SampleInfo("nightcore-finish")), }; } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 796b8f7aae..59a3381b8b 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public readonly Bindable AccentColour = new Bindable(Color4.Gray); - protected SkinnableSound Samples { get; private set; } + protected PausableSkinnableSound Samples { get; private set; } public virtual IEnumerable GetSamples() => HitObject.Samples; @@ -179,7 +179,7 @@ namespace osu.Game.Rulesets.Objects.Drawables + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } - Samples = new SkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); + Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); AddInternal(Samples); } diff --git a/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs b/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs index 83e89d654b..6b37021fe6 100644 --- a/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs +++ b/osu.Game/Screens/Play/ISamplePlaybackDisabler.cs @@ -8,7 +8,7 @@ namespace osu.Game.Screens.Play { /// /// Allows a component to disable sample playback dynamically as required. - /// Handled by . + /// Handled by . /// public interface ISamplePlaybackDisabler { diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 9494971f8a..65f34aba3e 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); - AddInternal(pauseLoop = new UnpausableSkinnableSound(new SampleInfo("pause-loop")) + AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("pause-loop")) { Looping = true, Volume = { Value = 0 } @@ -54,15 +54,5 @@ namespace osu.Game.Screens.Play pauseLoop.VolumeTo(0, TRANSITION_DURATION, Easing.OutQuad).Finally(_ => pauseLoop.Stop()); } - - private class UnpausableSkinnableSound : SkinnableSound - { - protected override bool PlayWhenPaused => true; - - public UnpausableSkinnableSound(SampleInfo sampleInfo) - : base(sampleInfo) - { - } - } } } diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs new file mode 100644 index 0000000000..991278fb98 --- /dev/null +++ b/osu.Game/Skinning/PausableSkinnableSound.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.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Audio; +using osu.Game.Screens.Play; + +namespace osu.Game.Skinning +{ + public class PausableSkinnableSound : SkinnableSound + { + protected bool RequestedPlaying { get; private set; } + + public PausableSkinnableSound(ISampleInfo hitSamples) + : base(hitSamples) + { + } + + public PausableSkinnableSound(IEnumerable hitSamples) + : base(hitSamples) + { + } + + private readonly IBindable samplePlaybackDisabled = new Bindable(); + + [BackgroundDependencyLoader(true)] + private void load(ISamplePlaybackDisabler samplePlaybackDisabler) + { + // if in a gameplay context, pause sample playback when gameplay is paused. + if (samplePlaybackDisabler != null) + { + samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); + samplePlaybackDisabled.BindValueChanged(disabled => + { + if (RequestedPlaying) + { + if (disabled.NewValue) + base.Stop(); + // it's not easy to know if a sample has finished playing (to end). + // to keep things simple only resume playing looping samples. + else if (Looping) + base.Play(); + } + }); + } + } + + public override void Play() + { + RequestedPlaying = true; + + if (samplePlaybackDisabled.Value) + return; + + base.Play(); + } + + public override void Stop() + { + RequestedPlaying = false; + base.Stop(); + } + } +} diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index f3ab8b86bc..91bdcd7444 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -11,7 +11,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Game.Audio; -using osu.Game.Screens.Play; namespace osu.Game.Skinning { @@ -22,8 +21,6 @@ namespace osu.Game.Skinning [Resolved] private ISampleStore samples { get; set; } - private bool requestedPlaying; - public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; @@ -37,8 +34,6 @@ namespace osu.Game.Skinning /// protected bool PlayWhenZeroVolume => Looping; - protected virtual bool PlayWhenPaused => false; - private readonly AudioContainer samplesContainer; public SkinnableSound(ISampleInfo hitSamples) @@ -52,30 +47,6 @@ namespace osu.Game.Skinning InternalChild = samplesContainer = new AudioContainer(); } - private readonly IBindable samplePlaybackDisabled = new Bindable(); - - [BackgroundDependencyLoader(true)] - private void load(ISamplePlaybackDisabler samplePlaybackDisabler) - { - // if in a gameplay context, pause sample playback when gameplay is paused. - if (samplePlaybackDisabler != null) - { - samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); - samplePlaybackDisabled.BindValueChanged(disabled => - { - if (requestedPlaying) - { - if (disabled.NewValue && !PlayWhenPaused) - stop(); - // it's not easy to know if a sample has finished playing (to end). - // to keep things simple only resume playing looping samples. - else if (Looping) - play(); - } - }); - } - } - private bool looping; public bool Looping @@ -91,17 +62,8 @@ namespace osu.Game.Skinning } } - public void Play() + public virtual void Play() { - requestedPlaying = true; - play(); - } - - private void play() - { - if (samplePlaybackDisabled.Value && !PlayWhenPaused) - return; - samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) @@ -109,13 +71,7 @@ namespace osu.Game.Skinning }); } - public void Stop() - { - requestedPlaying = false; - stop(); - } - - private void stop() + public virtual void Stop() { samplesContainer.ForEach(c => c.Stop()); } @@ -150,7 +106,7 @@ namespace osu.Game.Skinning // Start playback internally for the new samples if the previous ones were playing beforehand. if (wasPlaying) - play(); + Play(); } #region Re-expose AudioContainer From 6cceb42ad5f21861b839fca68f358183ad1b3da6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 15:50:53 +0900 Subject: [PATCH 3507/6909] Remove unused DI resolution --- 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 59a3381b8b..eb12c1cdfc 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -17,7 +17,6 @@ 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 @@ -359,9 +358,6 @@ namespace osu.Game.Rulesets.Objects.Drawables { } - [Resolved(canBeNull: true)] - private ISamplePlaybackDisabler samplePlaybackDisabler { get; set; } - /// /// Calculate the position to be used for sample playback at a specified X position (0..1). /// From a40c2ea5ee54863c0991c5be9fdcfc5fa76cbc03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 16:02:22 +0900 Subject: [PATCH 3508/6909] Simplify control point group binding/update logic --- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index d7da29218f..1d550b7cc3 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Edit.Timing private OsuButton deleteButton; private ControlPointTable table; - private IBindableList controlGroups; + private BindableList controlGroups; [Resolved] private EditorClock clock { get; set; } @@ -128,12 +128,14 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); - controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); - controlGroups.CollectionChanged += (sender, args) => createContent(); - createContent(); - } + // todo: remove cast after https://github.com/ppy/osu-framework/pull/3906 is merged + controlGroups = (BindableList)Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); - private void createContent() => table.ControlGroups = controlGroups; + controlGroups.BindCollectionChanged((sender, args) => + { + table.ControlGroups = controlGroups; + }, true); + } private void delete() { From 2ef09a8730085609f3ecb5eeba827e5a15554f60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 16:06:58 +0900 Subject: [PATCH 3509/6909] Populate test scene with control points --- osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 09f5ac2224..cda4ade6e1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Timing; @@ -19,7 +20,7 @@ namespace osu.Game.Tests.Visual.Editing public TestSceneTimingScreen() { - editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); } [BackgroundDependencyLoader] From 1dd354120b1013759d210e49902e08393c6d18b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 16:16:14 +0900 Subject: [PATCH 3510/6909] Fix beatmap potentially changing in test scene --- osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index cda4ade6e1..04cdfd7a67 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -5,7 +5,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Osu; -using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Timing; @@ -27,7 +26,15 @@ namespace osu.Game.Tests.Visual.Editing private void load() { Beatmap.Value = CreateWorkingBeatmap(editorBeatmap.PlayableBeatmap); + Beatmap.Disabled = true; + Child = new TimingScreen(); } + + protected override void Dispose(bool isDisposing) + { + Beatmap.Disabled = false; + base.Dispose(isDisposing); + } } } From e760ed8e01921c42c6adc6e8039b475f0b5758b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 16:39:02 +0900 Subject: [PATCH 3511/6909] Fix scroll wheel being handled by base test scene --- osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs | 2 ++ osu.Game/Tests/Visual/EditorClockTestScene.cs | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs index 04cdfd7a67..b82e776164 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimingScreen.cs @@ -17,6 +17,8 @@ namespace osu.Game.Tests.Visual.Editing [Cached(typeof(IBeatSnapProvider))] private readonly EditorBeatmap editorBeatmap; + protected override bool ScrollUsingMouseWheel => false; + public TestSceneTimingScreen() { editorBeatmap = new EditorBeatmap(CreateBeatmap(new OsuRuleset().RulesetInfo)); diff --git a/osu.Game/Tests/Visual/EditorClockTestScene.cs b/osu.Game/Tests/Visual/EditorClockTestScene.cs index f0ec638fc9..693c9cb792 100644 --- a/osu.Game/Tests/Visual/EditorClockTestScene.cs +++ b/osu.Game/Tests/Visual/EditorClockTestScene.cs @@ -20,6 +20,8 @@ namespace osu.Game.Tests.Visual protected readonly BindableBeatDivisor BeatDivisor = new BindableBeatDivisor(); protected new readonly EditorClock Clock; + protected virtual bool ScrollUsingMouseWheel => true; + protected EditorClockTestScene() { Clock = new EditorClock(new ControlPointInfo(), 5000, BeatDivisor) { IsCoupled = false }; @@ -57,6 +59,9 @@ namespace osu.Game.Tests.Visual protected override bool OnScroll(ScrollEvent e) { + if (!ScrollUsingMouseWheel) + return false; + if (e.ScrollDelta.Y > 0) Clock.SeekBackward(true); else From 5b200a8ca41f44084a3c0cc3703b50042a76a3bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 16:39:27 +0900 Subject: [PATCH 3512/6909] Change default zoom of timing screen timeline to most zoomed out --- .../Edit/Compose/Components/Timeline/TimelineArea.cs | 10 +++++----- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 10 +++++++++- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 7 +++++++ 3 files changed, 21 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index b99a053859..d870eb5279 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -14,9 +14,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimelineArea : Container { - private readonly Timeline timeline = new Timeline { RelativeSizeAxes = Axes.Both }; + public readonly Timeline Timeline = new Timeline { RelativeSizeAxes = Axes.Both }; - protected override Container Content => timeline; + protected override Container Content => Timeline; [BackgroundDependencyLoader] private void load() @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } }, - timeline + Timeline }, }, ColumnDimensions = new[] @@ -121,9 +121,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveformCheckbox.Current.Value = true; - timeline.WaveformVisible.BindTo(waveformCheckbox.Current); + Timeline.WaveformVisible.BindTo(waveformCheckbox.Current); } - private void changeZoom(float change) => timeline.Zoom += change; + private void changeZoom(float change) => Timeline.Zoom += change; } } diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index 34eddbefad..d6d782e70c 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -115,10 +115,18 @@ namespace osu.Game.Screens.Edit new TimelineTickDisplay(), CreateTimelineContent(), } - }, timelineContainer.Add); + }, t => + { + timelineContainer.Add(t); + OnTimelineLoaded(t); + }); }); } + protected virtual void OnTimelineLoaded(TimelineArea timelineArea) + { + } + protected abstract Drawable CreateMainContent(); protected virtual Drawable CreateTimelineContent() => new Container(); diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 1d550b7cc3..66c1f4895b 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; namespace osu.Game.Screens.Edit.Timing @@ -58,6 +59,12 @@ namespace osu.Game.Screens.Edit.Timing }); } + protected override void OnTimelineLoaded(TimelineArea timelineArea) + { + base.OnTimelineLoaded(timelineArea); + timelineArea.Timeline.Zoom = timelineArea.Timeline.MinZoom; + } + public class ControlPointList : CompositeDrawable { private OsuButton deleteButton; From 698042268ff4463a4b18544b68e3dea806c6524a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 16:58:19 +0900 Subject: [PATCH 3513/6909] Show control points in timing screen timeline --- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 66c1f4895b..4ab27ff1c0 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -12,6 +12,7 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; @@ -30,6 +31,11 @@ namespace osu.Game.Screens.Edit.Timing { } + protected override Drawable CreateTimelineContent() => new ControlPointPart + { + RelativeSizeAxes = Axes.Both, + }; + protected override Drawable CreateMainContent() => new GridContainer { RelativeSizeAxes = Axes.Both, From 3422db1bb291f7daf0b42887edb24a54ac3637b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 17:10:22 +0900 Subject: [PATCH 3514/6909] Use top-left colour for deciding the text colour (gradient was added in some cases) --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 455f1e17bd..bc2ccfc605 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -185,7 +185,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline else mainComponents.Colour = drawableHitObject.AccentColour.Value; - var col = mainComponents.Colour.AverageColour.Linear; + var col = mainComponents.Colour.TopLeft.Linear; float brightness = col.R + col.G + col.B; // decide the combo index colour based on brightness? From b0f8e11bd46b54ab14ee678a10b576daa7ed4278 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 17:34:14 +0900 Subject: [PATCH 3515/6909] Fix incorrect caching --- 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 e404d7ca42..9f8e55f577 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play /// public GameplayClock GameplayClock => localGameplayClock; - [Cached] + [Cached(typeof(GameplayClock))] [Cached(typeof(ISamplePlaybackDisabler))] private readonly LocalGameplayClock localGameplayClock; From bc943dee53e7208e57aa1dc77f38bfcac3e818a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 17:52:12 +0900 Subject: [PATCH 3516/6909] Add textbox entry for speed multiplier and volume --- .../Screens/Edit/Timing/DifficultySection.cs | 12 +-- osu.Game/Screens/Edit/Timing/SampleSection.cs | 12 ++- .../Edit/Timing/SliderWithTextBoxInput.cs | 74 +++++++++++++++++++ 3 files changed, 83 insertions(+), 15 deletions(-) create mode 100644 osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs index 58a7f97e5f..78766d9777 100644 --- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs @@ -2,27 +2,23 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Bindables; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Edit.Timing { internal class DifficultySection : Section { - private SettingsSlider multiplier; + private SliderWithTextBoxInput multiplierSlider; [BackgroundDependencyLoader] private void load() { Flow.AddRange(new[] { - multiplier = new SettingsSlider + multiplierSlider = new SliderWithTextBoxInput("Speed Multiplier") { - LabelText = "Speed Multiplier", - Bindable = new DifficultyControlPoint().SpeedMultiplierBindable, - RelativeSizeAxes = Axes.X, + Current = new DifficultyControlPoint().SpeedMultiplierBindable } }); } @@ -31,7 +27,7 @@ namespace osu.Game.Screens.Edit.Timing { if (point.NewValue != null) { - multiplier.Bindable = point.NewValue.SpeedMultiplierBindable; + multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable; } } diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs index 4665c77991..de986e28ca 100644 --- a/osu.Game/Screens/Edit/Timing/SampleSection.cs +++ b/osu.Game/Screens/Edit/Timing/SampleSection.cs @@ -2,18 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Edit.Timing { internal class SampleSection : Section { private LabelledTextBox bank; - private SettingsSlider volume; + private SliderWithTextBoxInput volume; [BackgroundDependencyLoader] private void load() @@ -24,10 +23,9 @@ namespace osu.Game.Screens.Edit.Timing { Label = "Bank Name", }, - volume = new SettingsSlider + volume = new SliderWithTextBoxInput("Volume") { - Bindable = new SampleControlPoint().SampleVolumeBindable, - LabelText = "Volume", + Current = new SampleControlPoint().SampleVolumeBindable, } }); } @@ -37,7 +35,7 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { bank.Current = point.NewValue.SampleBankBindable; - volume.Bindable = point.NewValue.SampleVolumeBindable; + volume.Current = point.NewValue.SampleVolumeBindable; } } diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs new file mode 100644 index 0000000000..07b914c506 --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Screens.Edit.Timing +{ + internal class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue + where T : struct, IEquatable, IComparable, IConvertible + { + private readonly SettingsSlider slider; + + public SliderWithTextBoxInput(string labelText) + { + LabelledTextBox textbox; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + textbox = new LabelledTextBox + { + Label = labelText, + }, + slider = new SettingsSlider + { + RelativeSizeAxes = Axes.X, + } + } + }, + }; + + textbox.OnCommit += (t, isNew) => + { + if (!isNew) return; + + try + { + slider.Bindable.Parse(t.Text); + } + catch + { + // will restore the previous text value on failure. + Current.TriggerChange(); + } + }; + + Current.BindValueChanged(val => + { + textbox.Text = val.NewValue.ToString(); + }, true); + } + + public Bindable Current + { + get => slider.Bindable; + set => slider.Bindable = value; + } + } +} From 44fc0c672304486c7706334ab990802f03aaa793 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 18:08:55 +0900 Subject: [PATCH 3517/6909] Fix default value of bpm being too high --- osu.Game/Screens/Edit/Timing/TimingSection.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 879363ba08..cc79dd2acc 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -103,12 +103,17 @@ namespace osu.Game.Screens.Edit.Timing private const double sane_maximum = 240; private readonly BindableNumber beatLengthBindable = new TimingControlPoint().BeatLengthBindable; - private readonly BindableDouble bpmBindable = new BindableDouble(); + + private readonly BindableDouble bpmBindable = new BindableDouble(60000 / TimingControlPoint.DEFAULT_BEAT_LENGTH) + { + MinValue = sane_minimum, + MaxValue = sane_maximum, + }; public BPMSlider() { beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true); - bpmBindable.BindValueChanged(bpm => bpmBindable.Default = beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); + bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); base.Bindable = bpmBindable; } From 5242f5648dd26d89829d130f78459589f220434b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 18:34:13 +0900 Subject: [PATCH 3518/6909] Fix timeline control point display not updating with changes --- .../Summary/Parts/ControlPointPart.cs | 70 +++++++------------ .../Summary/Parts/GroupVisualisation.cs | 46 ++++++++++++ 2 files changed, 71 insertions(+), 45 deletions(-) create mode 100644 osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs index 102955657e..6edb49ba1b 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs @@ -1,70 +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.Specialized; using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Graphics; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { /// /// The part of the timeline that displays the control points. /// - public class ControlPointPart : TimelinePart + public class ControlPointPart : TimelinePart { + private BindableList controlPointGroups; + protected override void LoadBeatmap(WorkingBeatmap beatmap) { base.LoadBeatmap(beatmap); - ControlPointInfo cpi = beatmap.Beatmap.ControlPointInfo; - - cpi.TimingPoints.ForEach(addTimingPoint); - - // Consider all non-timing points as the same type - cpi.SamplePoints.Select(c => (ControlPoint)c) - .Concat(cpi.EffectPoints) - .Concat(cpi.DifficultyPoints) - .Distinct() - // Non-timing points should not be added where there are timing points - .Where(c => cpi.TimingPointAt(c.Time).Time != c.Time) - .ForEach(addNonTimingPoint); - } - - private void addTimingPoint(ControlPoint controlPoint) => Add(new TimingPointVisualisation(controlPoint)); - private void addNonTimingPoint(ControlPoint controlPoint) => Add(new NonTimingPointVisualisation(controlPoint)); - - private class TimingPointVisualisation : ControlPointVisualisation - { - public TimingPointVisualisation(ControlPoint controlPoint) - : base(controlPoint) + controlPointGroups = (BindableList)beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); + controlPointGroups.BindCollectionChanged((sender, args) => { - } + switch (args.Action) + { + case NotifyCollectionChangedAction.Reset: + Clear(); + break; - [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.YellowDark; - } + case NotifyCollectionChangedAction.Add: + foreach (var group in args.NewItems.OfType()) + Add(new GroupVisualisation(group)); + break; - private class NonTimingPointVisualisation : ControlPointVisualisation - { - public NonTimingPointVisualisation(ControlPoint controlPoint) - : base(controlPoint) - { - } + case NotifyCollectionChangedAction.Remove: + foreach (var group in args.OldItems.OfType()) + { + var matching = Children.SingleOrDefault(gv => gv.Group == group); - [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.Green; - } + matching?.Expire(); + } - private abstract class ControlPointVisualisation : PointVisualisation - { - protected ControlPointVisualisation(ControlPoint controlPoint) - : base(controlPoint.Time) - { - } + break; + } + }, true); } } } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs new file mode 100644 index 0000000000..b9eb53b697 --- /dev/null +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts +{ + public class GroupVisualisation : PointVisualisation + { + public readonly ControlPointGroup Group; + + private BindableList controlPoints; + + [Resolved] + private OsuColour colours { get; set; } + + public GroupVisualisation(ControlPointGroup group) + : base(group.Time) + { + Group = group; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy(); + controlPoints.BindCollectionChanged((_, __) => + { + if (controlPoints.Count == 0) + { + Colour = Color4.Transparent; + return; + } + + Colour = controlPoints.Any(c => c is TimingControlPoint) ? colours.YellowDark : colours.Green; + }, true); + } + } +} From fab11a8241e470cd428d33a9084e0c927242c542 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 18:36:40 +0900 Subject: [PATCH 3519/6909] 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 dc3e14c141..afc5d4ec52 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 6412f707d0..5fa1685d9b 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 f1e13169a5..60708a39e2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From a11c74d60044d0ba9ee39e7a1e8e91674577b3cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 20:27:02 +0900 Subject: [PATCH 3520/6909] Update to consume framework fixes --- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 4ab27ff1c0..0a0cfe193d 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit.Timing private OsuButton deleteButton; private ControlPointTable table; - private BindableList controlGroups; + private IBindableList controlGroups; [Resolved] private EditorClock clock { get; set; } @@ -141,8 +141,7 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); - // todo: remove cast after https://github.com/ppy/osu-framework/pull/3906 is merged - controlGroups = (BindableList)Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); + controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); controlGroups.BindCollectionChanged((sender, args) => { From fa742a2ef1567072065c8bb3d7b8de7ccf5def1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Sep 2020 20:28:02 +0900 Subject: [PATCH 3521/6909] Update to consume framework fixes --- .../Components/Timelines/Summary/Parts/ControlPointPart.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs index 6edb49ba1b..8c0e31c04c 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs @@ -14,13 +14,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// public class ControlPointPart : TimelinePart { - private BindableList controlPointGroups; + private IBindableList controlPointGroups; protected override void LoadBeatmap(WorkingBeatmap beatmap) { base.LoadBeatmap(beatmap); - controlPointGroups = (BindableList)beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); + controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); controlPointGroups.BindCollectionChanged((sender, args) => { switch (args.Action) From 77651be2caff6e95fb52801462130197d9e02183 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 30 Sep 2020 21:32:50 +0900 Subject: [PATCH 3522/6909] Remove padding from HitResult --- osu.Game/Rulesets/Scoring/HitResult.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index fc33ff9df2..2b9b1a6c8e 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Scoring /// [Description(@"")] [Order(14)] - None = 0, + None, /// /// Indicates that the object has been judged as a miss. @@ -25,7 +25,7 @@ namespace osu.Game.Rulesets.Scoring /// [Description(@"Miss")] [Order(5)] - Miss = 64, + Miss, [Description(@"Meh")] [Order(4)] @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates small tick miss. /// [Order(11)] - SmallTickMiss = 128, + SmallTickMiss, /// /// Indicates a small tick hit. @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Scoring /// Indicates a large tick miss. /// [Order(10)] - LargeTickMiss = 192, + LargeTickMiss, /// /// Indicates a large tick hit. @@ -84,20 +84,20 @@ namespace osu.Game.Rulesets.Scoring /// [Description("S Bonus")] [Order(9)] - SmallBonus = 254, + SmallBonus, /// /// Indicates a large bonus. /// [Description("L Bonus")] [Order(8)] - LargeBonus = 320, + LargeBonus, /// /// Indicates a miss that should be ignored for scoring purposes. /// [Order(13)] - IgnoreMiss = 384, + IgnoreMiss, /// /// Indicates a hit that should be ignored for scoring purposes. From 917e8fc3ba041d40e09396f39f020d2b508ac16e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 00:53:01 +0900 Subject: [PATCH 3523/6909] Add difficulty rating to StarDifficulty --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index e9d26683c3..159a229499 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -307,5 +307,19 @@ namespace osu.Game.Beatmaps // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) } + + public DifficultyRating DifficultyRating + { + get + { + if (Stars < 2.0) return DifficultyRating.Easy; + if (Stars < 2.7) return DifficultyRating.Normal; + if (Stars < 4.0) return DifficultyRating.Hard; + if (Stars < 5.3) return DifficultyRating.Insane; + if (Stars < 6.5) return DifficultyRating.Expert; + + return DifficultyRating.ExpertPlus; + } + } } } From fde00d343197d16ed0140d412b4fa4017e369907 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 00:53:25 +0900 Subject: [PATCH 3524/6909] Make DifficultyIcon support dynamic star rating --- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 135 +++++++++++++++--- .../Drawables/GroupedDifficultyIcon.cs | 2 +- .../Screens/Multi/Components/ModeTypeInfo.cs | 2 +- .../Screens/Multi/DrawableRoomPlaylistItem.cs | 2 +- 4 files changed, 117 insertions(+), 24 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 8a0d981e49..d5e4b13a84 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -2,7 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Threading; +using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,6 +18,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osuTK; using osuTK.Graphics; @@ -21,9 +26,6 @@ namespace osu.Game.Beatmaps.Drawables { public class DifficultyIcon : CompositeDrawable, IHasCustomTooltip { - private readonly BeatmapInfo beatmap; - private readonly RulesetInfo ruleset; - private readonly Container iconContainer; /// @@ -35,23 +37,49 @@ namespace osu.Game.Beatmaps.Drawables set => iconContainer.Size = value; } - public DifficultyIcon(BeatmapInfo beatmap, RulesetInfo ruleset = null, bool shouldShowTooltip = true) + [NotNull] + private readonly BeatmapInfo beatmap; + + [CanBeNull] + private readonly RulesetInfo ruleset; + + [CanBeNull] + private readonly IReadOnlyList mods; + + private readonly bool shouldShowTooltip; + private readonly IBindable difficultyBindable = new Bindable(); + + private Drawable background; + + /// + /// Creates a new with a given and combination. + /// + /// The beatmap to show the difficulty of. + /// The ruleset to show the difficulty with. + /// The mods to show the difficulty with. + /// Whether to display a tooltip when hovered. + public DifficultyIcon([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, [CanBeNull] IReadOnlyList mods, bool shouldShowTooltip = true) + : this(beatmap, shouldShowTooltip) + { + this.ruleset = ruleset ?? beatmap.Ruleset; + this.mods = mods ?? Array.Empty(); + } + + /// + /// Creates a new that follows the currently-selected ruleset and mods. + /// + /// The beatmap to show the difficulty of. + /// Whether to display a tooltip when hovered. + public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true) { this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap)); - - this.ruleset = ruleset ?? beatmap.Ruleset; - if (shouldShowTooltip) - TooltipContent = beatmap; + this.shouldShowTooltip = shouldShowTooltip; AutoSizeAxes = Axes.Both; InternalChild = iconContainer = new Container { Size = new Vector2(20f) }; } - public ITooltip GetCustomTooltip() => new DifficultyIconTooltip(); - - public object TooltipContent { get; } - [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -70,10 +98,10 @@ namespace osu.Game.Beatmaps.Drawables Type = EdgeEffectType.Shadow, Radius = 5, }, - Child = new Box + Child = background = new Box { RelativeSizeAxes = Axes.Both, - Colour = colours.ForDifficultyRating(beatmap.DifficultyRating), + Colour = colours.ForDifficultyRating(beatmap.DifficultyRating) // Default value that will be re-populated once difficulty calculation completes }, }, new ConstrainedIconContainer @@ -82,16 +110,73 @@ namespace osu.Game.Beatmaps.Drawables Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) - Icon = ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } - } + Icon = beatmap.Ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } + }, + new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0), }; + + difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating)); + } + + public ITooltip GetCustomTooltip() => new DifficultyIconTooltip(); + + public object TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null; + + private class DifficultyRetriever : Drawable + { + public readonly Bindable StarDifficulty = new Bindable(); + + private readonly BeatmapInfo beatmap; + private readonly RulesetInfo ruleset; + private readonly IReadOnlyList mods; + + private CancellationTokenSource difficultyCancellation; + + [Resolved] + private BeatmapDifficultyManager difficultyManager { get; set; } + + public DifficultyRetriever(BeatmapInfo beatmap, RulesetInfo ruleset, IReadOnlyList mods) + { + this.beatmap = beatmap; + this.ruleset = ruleset; + this.mods = mods; + } + + private IBindable localStarDifficulty; + + [BackgroundDependencyLoader] + private void load() + { + difficultyCancellation = new CancellationTokenSource(); + localStarDifficulty = ruleset != null + ? difficultyManager.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token) + : difficultyManager.GetBindableDifficulty(beatmap, difficultyCancellation.Token); + localStarDifficulty.BindValueChanged(difficulty => StarDifficulty.Value = difficulty.NewValue); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + difficultyCancellation?.Cancel(); + } + } + + private class DifficultyIconTooltipContent + { + public readonly BeatmapInfo Beatmap; + public readonly IBindable Difficulty; + + public DifficultyIconTooltipContent(BeatmapInfo beatmap, IBindable difficulty) + { + Beatmap = beatmap; + Difficulty = difficulty; + } } private class DifficultyIconTooltip : VisibilityContainer, ITooltip { private readonly OsuSpriteText difficultyName, starRating; private readonly Box background; - private readonly FillFlowContainer difficultyFlow; public DifficultyIconTooltip() @@ -159,14 +244,22 @@ namespace osu.Game.Beatmaps.Drawables background.Colour = colours.Gray3; } + private readonly IBindable starDifficulty = new Bindable(); + public bool SetContent(object content) { - if (!(content is BeatmapInfo beatmap)) + if (!(content is DifficultyIconTooltipContent iconContent)) return false; - difficultyName.Text = beatmap.Version; - starRating.Text = $"{beatmap.StarDifficulty:0.##}"; - difficultyFlow.Colour = colours.ForDifficultyRating(beatmap.DifficultyRating, true); + difficultyName.Text = iconContent.Beatmap.Version; + + starDifficulty.UnbindAll(); + starDifficulty.BindTo(iconContent.Difficulty); + starDifficulty.BindValueChanged(difficulty => + { + starRating.Text = $"{difficulty.NewValue.Stars:0.##}"; + difficultyFlow.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating, true); + }, true); return true; } diff --git a/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs index fbad113caa..fcee4c2f1a 100644 --- a/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/GroupedDifficultyIcon.cs @@ -20,7 +20,7 @@ namespace osu.Game.Beatmaps.Drawables public class GroupedDifficultyIcon : DifficultyIcon { public GroupedDifficultyIcon(List beatmaps, RulesetInfo ruleset, Color4 counterColour) - : base(beatmaps.OrderBy(b => b.StarDifficulty).Last(), ruleset, false) + : base(beatmaps.OrderBy(b => b.StarDifficulty).Last(), ruleset, null, false) { AddInternal(new OsuSpriteText { diff --git a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs index 0015feb26a..f07bd8c3b2 100644 --- a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs @@ -60,7 +60,7 @@ namespace osu.Game.Screens.Multi.Components if (item?.Beatmap != null) { drawableRuleset.FadeIn(transition_duration); - drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value) { Size = new Vector2(height) }; + drawableRuleset.Child = new DifficultyIcon(item.Beatmap.Value, item.Ruleset.Value, item.RequiredMods) { Size = new Vector2(height) }; } else drawableRuleset.FadeOut(transition_duration); diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index b007e0349d..bda00b65b5 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -103,7 +103,7 @@ namespace osu.Game.Screens.Multi private void refresh() { - difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value) { Size = new Vector2(32) }; + difficultyIconContainer.Child = new DifficultyIcon(beatmap.Value, ruleset.Value, requiredMods) { Size = new Vector2(32) }; beatmapText.Clear(); beatmapText.AddLink(Item.Beatmap.ToString(), LinkAction.OpenBeatmap, Item.Beatmap.Value.OnlineBeatmapID.ToString()); From 2213db20886ab1952bf33dc370cb67efa3b0e681 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 00:59:41 +0900 Subject: [PATCH 3525/6909] Use the given ruleset by default --- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index d5e4b13a84..9ffe813187 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -110,7 +110,7 @@ namespace osu.Game.Beatmaps.Drawables Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) - Icon = beatmap.Ruleset?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } + Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } }, new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0), }; From ca9f5b447ee643dc3ebe558b9e21707e1e41d2e3 Mon Sep 17 00:00:00 2001 From: Ganendra Afrasya Date: Thu, 1 Oct 2020 02:02:27 +0700 Subject: [PATCH 3526/6909] Fix UserListPanel background position --- osu.Game/Users/UserListPanel.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Users/UserListPanel.cs b/osu.Game/Users/UserListPanel.cs index 9c95eff739..cc4fca9b94 100644 --- a/osu.Game/Users/UserListPanel.cs +++ b/osu.Game/Users/UserListPanel.cs @@ -26,6 +26,8 @@ namespace osu.Game.Users private void load() { Background.Width = 0.5f; + Background.Origin = Anchor.CentreRight; + Background.Anchor = Anchor.CentreRight; Background.Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(1), Color4.White.Opacity(0.3f)); } From 6b416c9881ab19dcc2291849777f729b6a422e57 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 12:09:12 +0900 Subject: [PATCH 3527/6909] Rename method and improve method implementation --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 3d8a52a864..d5c3538c81 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_2 }, }); - AddAssert("Tracking retained", assertGreatJudge); + AddAssert("Tracking retained", assertMaxJudge); } /// @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking retained", assertGreatJudge); + AddAssert("Tracking retained", assertMaxJudge); } /// @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(0, 0), Actions = { OsuAction.RightButton }, Time = time_during_slide_1 }, }); - AddAssert("Tracking retained", assertGreatJudge); + AddAssert("Tracking retained", assertMaxJudge); } /// @@ -288,7 +288,7 @@ namespace osu.Game.Rulesets.Osu.Tests new OsuReplayFrame { Position = new Vector2(slider_path_length, OsuHitObject.OBJECT_RADIUS * 1.199f), Actions = { OsuAction.LeftButton }, Time = time_slider_end }, }); - AddAssert("Tracking kept", assertGreatJudge); + AddAssert("Tracking kept", assertMaxJudge); } /// @@ -312,7 +312,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("Tracking dropped", assertMidSliderJudgementFail); } - private bool assertGreatJudge() => judgementResults.Any() && judgementResults.All(t => t.Type.IsHit()); + private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult); private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.IgnoreHit && judgementResults.First().Type == HitResult.Miss; From 806d8b4b1dddcc35bb7512dd60cc078a6000e95f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 12:13:24 +0900 Subject: [PATCH 3528/6909] Make scoring int-based again --- osu.Game/Rulesets/Judgements/Judgement.cs | 24 +++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index ea7a8d8d25..c7d572b629 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -15,12 +15,12 @@ namespace osu.Game.Rulesets.Judgements /// /// The score awarded for a small bonus. /// - public const double SMALL_BONUS_SCORE = 10; + public const int SMALL_BONUS_SCORE = 10; /// /// The score awarded for a large bonus. /// - public const double LARGE_BONUS_SCORE = 50; + public const int LARGE_BONUS_SCORE = 50; /// /// The default health increase for a maximum judgement, as a proportion of total health. @@ -74,7 +74,7 @@ namespace osu.Game.Rulesets.Judgements /// /// The numeric score representation for the maximum achievable result. /// - public double MaxNumericResult => ToNumericResult(MaxResult); + public int MaxNumericResult => ToNumericResult(MaxResult); /// /// The health increase for the maximum achievable result. @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Judgements /// /// The to find the numeric score representation for. /// The numeric score representation of . - public double NumericResultFor(JudgementResult result) => ToNumericResult(result.Type); + public int NumericResultFor(JudgementResult result) => ToNumericResult(result.Type); /// /// Retrieves the numeric health increase of a . @@ -155,7 +155,7 @@ namespace osu.Game.Rulesets.Judgements public override string ToString() => $"MaxResult:{MaxResult} MaxScore:{MaxNumericResult}"; - public static double ToNumericResult(HitResult result) + public static int ToNumericResult(HitResult result) { switch (result) { @@ -163,25 +163,25 @@ namespace osu.Game.Rulesets.Judgements return 0; case HitResult.SmallTickHit: - return 1 / 30d; + return 10; case HitResult.LargeTickHit: - return 1 / 10d; + return 30; case HitResult.Meh: - return 1 / 6d; + return 50; case HitResult.Ok: - return 1 / 3d; + return 100; case HitResult.Good: - return 2 / 3d; + return 200; case HitResult.Great: - return 1d; + return 300; case HitResult.Perfect: - return 7 / 6d; + return 350; case HitResult.SmallBonus: return SMALL_BONUS_SCORE; From 3a26bd8d9ba87590f2e6f8d35afd9c255f543979 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 12:14:16 +0900 Subject: [PATCH 3529/6909] Adjust obsoletion + xmldoc of NumericResultFor() --- osu.Game/Rulesets/Judgements/Judgement.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index c7d572b629..738ae0d6d2 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -82,12 +82,12 @@ namespace osu.Game.Rulesets.Judgements public double MaxHealthIncrease => HealthIncreaseFor(MaxResult); /// - /// Retrieves the numeric score representation of a . + /// Retrieves the numeric score representation of a . /// - /// The to find the numeric score representation for. + /// The to find the numeric score representation for. /// The numeric score representation of . - [Obsolete("Has no effect. Use ToNumericResult(HitResult) (standardised across all rulesets).")] // Can be removed 20210328 - protected virtual int NumericResultFor(HitResult result) => result == HitResult.Miss ? 0 : 1; + [Obsolete("Has no effect. Use ToNumericResult(HitResult) (standardised across all rulesets).")] // Can be made non-virtual 20210328 + protected virtual int NumericResultFor(HitResult result) => ToNumericResult(result); /// /// Retrieves the numeric score representation of a . From 3c9ee6abc11f227eb6a18d10b66f0c7e6aedf04b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 12:15:34 +0900 Subject: [PATCH 3530/6909] Use local static to determine score per spinner tick --- .../Objects/Drawables/Pieces/SpinnerBonusDisplay.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs index 1668cd73bd..f483bb1b26 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets.Judgements; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { @@ -14,6 +13,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces /// public class SpinnerBonusDisplay : CompositeDrawable { + private static readonly int score_per_tick = new SpinnerBonusTick().CreateJudgement().MaxNumericResult; + private readonly OsuSpriteText bonusCounter; public SpinnerBonusDisplay() @@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces return; displayedCount = count; - bonusCounter.Text = $"{Judgement.LARGE_BONUS_SCORE * count}"; + bonusCounter.Text = $"{score_per_tick * count}"; bonusCounter.FadeOutFromOne(1500); bonusCounter.ScaleTo(1.5f).Then().ScaleTo(1f, 1000, Easing.OutQuint); } From c9f38f7bb6af116fa7ec813ceb3f100dcad30adb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 12:28:33 +0900 Subject: [PATCH 3531/6909] Add obsoletion notice --- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 2a3e3716e5..a48627208d 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -10,6 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; +using osu.Framework.Logging; using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Rulesets.Judgements; @@ -476,12 +477,21 @@ namespace osu.Game.Rulesets.Objects.Drawables throw new InvalidOperationException($"{GetType().ReadableName()} applied a {nameof(JudgementResult)} but did not update {nameof(JudgementResult.Type)}."); // Some (especially older) rulesets use scorable judgements instead of the newer ignorehit/ignoremiss judgements. + // Can be removed 20210328 if (Result.Judgement.MaxResult == HitResult.IgnoreHit) { + HitResult originalType = Result.Type; + if (Result.Type == HitResult.Miss) Result.Type = HitResult.IgnoreMiss; else if (Result.Type >= HitResult.Meh && Result.Type <= HitResult.Perfect) Result.Type = HitResult.IgnoreHit; + + if (Result.Type != originalType) + { + Logger.Log($"{GetType().ReadableName()} applied an invalid hit result ({originalType}) when {nameof(HitResult.IgnoreMiss)} or {nameof(HitResult.IgnoreHit)} is expected.\n" + + $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2020-03-28 onwards.", level: LogLevel.Important); + } } if (!Result.Type.IsValidHitResult(Result.Judgement.MinResult, Result.Judgement.MaxResult)) From 61e62929eeccb402000af318ba438c0c78e2e6ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 12:51:33 +0900 Subject: [PATCH 3532/6909] Apply changes in line with framework event logic update --- osu.Game.Tournament/Components/DateTextBox.cs | 2 +- osu.Game/Online/Chat/StandAloneChatDisplay.cs | 3 ++- osu.Game/Overlays/ChatOverlay.cs | 3 ++- osu.Game/Overlays/Music/PlaylistOverlay.cs | 2 +- osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs | 3 ++- 5 files changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index aee5241e35..a1b5ac38ea 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -28,7 +28,7 @@ namespace osu.Game.Tournament.Components { base.Bindable = new Bindable(); - ((OsuTextBox)Control).OnCommit = (sender, newText) => + ((OsuTextBox)Control).OnCommit += (sender, newText) => { try { diff --git a/osu.Game/Online/Chat/StandAloneChatDisplay.cs b/osu.Game/Online/Chat/StandAloneChatDisplay.cs index f8810c778f..8b0caddbc6 100644 --- a/osu.Game/Online/Chat/StandAloneChatDisplay.cs +++ b/osu.Game/Online/Chat/StandAloneChatDisplay.cs @@ -59,12 +59,13 @@ namespace osu.Game.Online.Chat RelativeSizeAxes = Axes.X, Height = textbox_height, PlaceholderText = "type your message", - OnCommit = postMessage, ReleaseFocusOnCommit = false, HoldFocus = true, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, }); + + textbox.OnCommit += postMessage; } Channel.BindValueChanged(channelChanged); diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index c53eccf78b..8bc7e21047 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -146,7 +146,6 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both, Height = 1, PlaceholderText = "type your message", - OnCommit = postMessage, ReleaseFocusOnCommit = false, HoldFocus = true, } @@ -186,6 +185,8 @@ namespace osu.Game.Overlays }, }; + textbox.OnCommit += postMessage; + ChannelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue; ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden; ChannelSelectionOverlay.State.ValueChanged += state => diff --git a/osu.Game/Overlays/Music/PlaylistOverlay.cs b/osu.Game/Overlays/Music/PlaylistOverlay.cs index 050e687dfb..b8d04eab4e 100644 --- a/osu.Game/Overlays/Music/PlaylistOverlay.cs +++ b/osu.Game/Overlays/Music/PlaylistOverlay.cs @@ -75,7 +75,7 @@ namespace osu.Game.Overlays.Music }, }; - filter.Search.OnCommit = (sender, newText) => + filter.Search.OnCommit += (sender, newText) => { BeatmapInfo toSelect = list.FirstVisibleSet?.Beatmaps?.FirstOrDefault(); diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 34e5da4ef4..f96e204f62 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -236,7 +236,6 @@ namespace osu.Game.Overlays.Settings.Sections.General PlaceholderText = "password", RelativeSizeAxes = Axes.X, TabbableContentContainer = this, - OnCommit = (sender, newText) => performLogin() }, new SettingsCheckbox { @@ -276,6 +275,8 @@ namespace osu.Game.Overlays.Settings.Sections.General } } }; + + password.OnCommit += (sender, newText) => performLogin(); } public override bool AcceptsFocus => true; From e0a0902a15f9e1dac34b8795035c2ef48e47dbec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 13:06:24 +0900 Subject: [PATCH 3533/6909] Ensure textbox always reverts to sane state on out-of-range failures --- .../Edit/Timing/SliderWithTextBoxInput.cs | 7 +++++-- osu.Game/Screens/Edit/Timing/TimingSection.cs | 19 ++++++++++--------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs index 07b914c506..14023b0c35 100644 --- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs @@ -54,9 +54,12 @@ namespace osu.Game.Screens.Edit.Timing } catch { - // will restore the previous text value on failure. - Current.TriggerChange(); + // TriggerChange below will restore the previous text value on failure. } + + // This is run regardless of parsing success as the parsed number may not actually trigger a change + // due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state. + Current.TriggerChange(); }; Current.BindValueChanged(val => diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index cc79dd2acc..0202441537 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -65,18 +65,19 @@ namespace osu.Game.Screens.Edit.Timing { if (!isNew) return; - if (double.TryParse(Current.Value, out double doubleVal)) + try { - try - { + if (double.TryParse(Current.Value, out double doubleVal) && doubleVal > 0) beatLengthBindable.Value = beatLengthToBpm(doubleVal); - } - catch - { - // will restore the previous text value on failure. - beatLengthBindable.TriggerChange(); - } } + catch + { + // TriggerChange below will restore the previous text value on failure. + } + + // This is run regardless of parsing success as the parsed number may not actually trigger a change + // due to bindable clamping. Even in such a case we want to update the textbox to a sane visual state. + beatLengthBindable.TriggerChange(); }; beatLengthBindable.BindValueChanged(val => From b1f2bdd579ee4a6b91d3f5b3b78e62ea204a97fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 13:47:49 +0900 Subject: [PATCH 3534/6909] Add missing xmldoc --- .../Edit/Compose/Components/ComposeSelectionBox.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs index 424705c755..530c6007cf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs @@ -24,6 +24,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool canRotate; + /// + /// Whether rotation support should be enabled. + /// public bool CanRotate { get => canRotate; @@ -36,6 +39,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool canScaleX; + /// + /// Whether vertical scale support should be enabled. + /// public bool CanScaleX { get => canScaleX; @@ -48,6 +54,9 @@ namespace osu.Game.Screens.Edit.Compose.Components private bool canScaleY; + /// + /// Whether horizontal scale support should be enabled. + /// public bool CanScaleY { get => canScaleY; From 02f14ab4b0045249c9a9bf681ee324c8ed5dfdbc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 16:24:04 +0900 Subject: [PATCH 3535/6909] Rename operation start/end to be more encompassing --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++-- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 6b4f13db35..f275e08234 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -27,9 +27,9 @@ namespace osu.Game.Rulesets.Osu.Edit SelectionBox.CanScaleY = canOperate; } - protected override void OnDragOperationEnded() + protected override void OnOperationEnded() { - base.OnDragOperationEnded(); + base.OnOperationEnded(); referenceOrigin = null; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index ee094c6246..435f84996a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -97,8 +97,8 @@ namespace osu.Game.Screens.Edit.Compose.Components public ComposeSelectionBox CreateSelectionBox() => new ComposeSelectionBox { - OperationStarted = OnDragOperationBegan, - OperationEnded = OnDragOperationEnded, + OperationStarted = OnOperationBegan, + OperationEnded = OnOperationEnded, OnRotation = e => HandleRotation(e.Delta.X), OnScale = (e, anchor) => HandleScale(e.Delta, anchor), @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Fired when a drag operation ends from the selection box. /// - protected virtual void OnDragOperationBegan() + protected virtual void OnOperationBegan() { ChangeHandler.BeginChange(); } @@ -115,7 +115,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Fired when a drag operation begins from the selection box. /// - protected virtual void OnDragOperationEnded() + protected virtual void OnOperationEnded() { ChangeHandler.EndChange(); } From 983b693858195cfbcece3c1f76a52c2dc7e59a91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 16:24:50 +0900 Subject: [PATCH 3536/6909] Add flip logic to OsuSelectionHandler --- .../Edit/OsuSelectionHandler.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index f275e08234..2bd4bc5015 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -41,6 +41,45 @@ namespace osu.Game.Rulesets.Osu.Edit /// private Vector2? referenceOrigin; + public override bool HandleFlip(Direction direction) + { + var hitObjects = selectedMovableObjects; + + var selectedObjectsQuad = getSurroundingQuad(hitObjects); + var centre = selectedObjectsQuad.Centre; + + foreach (var h in hitObjects) + { + var pos = h.Position; + + switch (direction) + { + case Direction.Horizontal: + pos.X = centre.X - (pos.X - centre.X); + break; + + case Direction.Vertical: + pos.Y = centre.Y - (pos.Y - centre.Y); + break; + } + + h.Position = pos; + + if (h is Slider slider) + { + foreach (var point in slider.Path.ControlPoints) + { + point.Position.Value = new Vector2( + (direction == Direction.Horizontal ? -1 : 1) * point.Position.Value.X, + (direction == Direction.Vertical ? -1 : 1) * point.Position.Value.Y + ); + } + } + } + + return true; + } + public override bool HandleScale(Vector2 scale, Anchor reference) { adjustScaleFromAnchor(ref scale, reference); From 78c5d5707496f4b7af3277c805063a0b7e5a5ec7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 16:25:29 +0900 Subject: [PATCH 3537/6909] Add flip event flow and stop passing raw input events to handle methods --- .../Visual/Editing/TestSceneComposeSelectBox.cs | 17 ++++++++--------- .../Compose/Components/ComposeSelectionBox.cs | 5 +++-- .../Edit/Compose/Components/SelectionHandler.cs | 12 ++++++++++-- 3 files changed, 21 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index a1fb91024b..2e0be95ff7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Screens.Edit.Compose.Components; using osuTK; @@ -20,7 +19,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("create box", () => Child = selectionArea = new Container { - Size = new Vector2(300), + Size = new Vector2(400), Position = -new Vector2(150), Anchor = Anchor.Centre, Children = new Drawable[] @@ -42,29 +41,29 @@ namespace osu.Game.Tests.Visual.Editing AddToggleStep("toggle y", state => selectionBox.CanScaleY = state); } - private void handleScale(DragEvent e, Anchor reference) + private void handleScale(Vector2 amount, Anchor reference) { if ((reference & Anchor.y1) == 0) { int directionY = (reference & Anchor.y0) > 0 ? -1 : 1; if (directionY < 0) - selectionArea.Y += e.Delta.Y; - selectionArea.Height += directionY * e.Delta.Y; + selectionArea.Y += amount.Y; + selectionArea.Height += directionY * amount.Y; } if ((reference & Anchor.x1) == 0) { int directionX = (reference & Anchor.x0) > 0 ? -1 : 1; if (directionX < 0) - selectionArea.X += e.Delta.X; - selectionArea.Width += directionX * e.Delta.X; + selectionArea.X += amount.X; + selectionArea.Width += directionX * amount.X; } } - private void handleRotation(DragEvent e) + private void handleRotation(float angle) { // kinda silly and wrong, but just showing that the drag handles work. - selectionArea.Rotation += e.Delta.X; + selectionArea.Rotation += angle; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs index 530c6007cf..c457a68368 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs @@ -16,8 +16,9 @@ namespace osu.Game.Screens.Edit.Compose.Components { public class ComposeSelectionBox : CompositeDrawable { - public Action OnRotation; - public Action OnScale; + public Action OnRotation; + public Action OnScale; + public Action OnFlip; public Action OperationStarted; public Action OperationEnded; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 435f84996a..1c2f09f831 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -100,8 +100,9 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, - OnRotation = e => HandleRotation(e.Delta.X), - OnScale = (e, anchor) => HandleScale(e.Delta, anchor), + OnRotation = angle => HandleRotation(angle), + OnScale = (amount, anchor) => HandleScale(amount, anchor), + OnFlip = direction => HandleFlip(direction), }; /// @@ -151,6 +152,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any s could be moved. public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; + /// + /// Handled the selected s being flipped. + /// + /// The direction to flip + /// Whether any s could be moved. + public virtual bool HandleFlip(Direction direction) => false; + public bool OnPressed(PlatformAction action) { switch (action.ActionMethod) From 4e6a505a99740bee31ace700aa7994b994d0188d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 16:25:40 +0900 Subject: [PATCH 3538/6909] Add new icons and tooltips --- .../Compose/Components/ComposeSelectionBox.cs | 180 ++++++++++++------ 1 file changed, 124 insertions(+), 56 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs index c457a68368..a26533fdb5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs @@ -5,6 +5,7 @@ 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.Framework.Graphics.Sprites; using osu.Framework.Input.Events; @@ -68,6 +69,8 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + private FillFlowContainer buttons; + public const float BORDER_RADIUS = 3; [Resolved] @@ -105,72 +108,114 @@ namespace osu.Game.Screens.Edit.Compose.Components }, } }, + buttons = new FillFlowContainer + { + Y = 20, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre + } }; - if (CanRotate) + if (CanScaleX) addXScaleComponents(); + if (CanScaleX && CanScaleY) addFullScaleComponents(); + if (CanScaleY) addYScaleComponents(); + if (CanRotate) addRotationComponents(); + } + + private void addRotationComponents() + { + const float separation = 40; + + buttons.Insert(-1, new DragHandleButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise") { - const float separation = 40; + OperationStarted = operationStarted, + OperationEnded = operationEnded, + Action = () => OnRotation?.Invoke(-90) + }); - AddRangeInternal(new Drawable[] - { - new Box - { - Colour = colours.YellowLight, - Blending = BlendingParameters.Additive, - Alpha = 0.3f, - Size = new Vector2(BORDER_RADIUS, separation), - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - }, - new RotationDragHandle - { - Anchor = Anchor.TopCentre, - Y = -separation, - HandleDrag = e => OnRotation?.Invoke(e), - OperationStarted = operationStarted, - OperationEnded = operationEnded - } - }); - } - - if (CanScaleY) + buttons.Add(new DragHandleButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise") { - AddRangeInternal(new[] - { - createDragHandle(Anchor.TopCentre), - createDragHandle(Anchor.BottomCentre), - }); - } + OperationStarted = operationStarted, + OperationEnded = operationEnded, + Action = () => OnRotation?.Invoke(90) + }); - if (CanScaleX) + AddRangeInternal(new Drawable[] { - AddRangeInternal(new[] + new Box { - createDragHandle(Anchor.CentreLeft), - createDragHandle(Anchor.CentreRight), - }); - } - - if (CanScaleX && CanScaleY) - { - AddRangeInternal(new[] + Depth = float.MaxValue, + Colour = colours.YellowLight, + Blending = BlendingParameters.Additive, + Alpha = 0.3f, + Size = new Vector2(BORDER_RADIUS, separation), + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, + new DragHandleButton(FontAwesome.Solid.Redo, "Free rotate") { - createDragHandle(Anchor.TopLeft), - createDragHandle(Anchor.TopRight), - createDragHandle(Anchor.BottomLeft), - createDragHandle(Anchor.BottomRight), - }); - } - - ScaleDragHandle createDragHandle(Anchor anchor) => - new ScaleDragHandle(anchor) - { - HandleDrag = e => OnScale?.Invoke(e, anchor), + Anchor = Anchor.TopCentre, + Y = -separation, + HandleDrag = e => OnRotation?.Invoke(e.Delta.X), OperationStarted = operationStarted, OperationEnded = operationEnded - }; + } + }); } + private void addYScaleComponents() + { + buttons.Add(new DragHandleButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically") + { + OperationStarted = operationStarted, + OperationEnded = operationEnded, + Action = () => OnFlip?.Invoke(Direction.Vertical) + }); + + AddRangeInternal(new[] + { + createDragHandle(Anchor.TopCentre), + createDragHandle(Anchor.BottomCentre), + }); + } + + private void addFullScaleComponents() + { + AddRangeInternal(new[] + { + createDragHandle(Anchor.TopLeft), + createDragHandle(Anchor.TopRight), + createDragHandle(Anchor.BottomLeft), + createDragHandle(Anchor.BottomRight), + }); + } + + private void addXScaleComponents() + { + buttons.Add(new DragHandleButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally") + { + OperationStarted = operationStarted, + OperationEnded = operationEnded, + Action = () => OnFlip?.Invoke(Direction.Horizontal) + }); + + AddRangeInternal(new[] + { + createDragHandle(Anchor.CentreLeft), + createDragHandle(Anchor.CentreRight), + }); + } + + private ScaleDragHandle createDragHandle(Anchor anchor) => + new ScaleDragHandle(anchor) + { + HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), + OperationStarted = operationStarted, + OperationEnded = operationEnded + }; + private int activeOperations; private void operationEnded() @@ -193,30 +238,53 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private class RotationDragHandle : DragHandle + private sealed class DragHandleButton : DragHandle, IHasTooltip { private SpriteIcon icon; + private readonly IconUsage iconUsage; + + public Action Action; + + public DragHandleButton(IconUsage iconUsage, string tooltip) + { + this.iconUsage = iconUsage; + + TooltipText = tooltip; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + [BackgroundDependencyLoader] private void load() { Size *= 2; - AddInternal(icon = new SpriteIcon { RelativeSizeAxes = Axes.Both, Size = new Vector2(0.5f), - Icon = FontAwesome.Solid.Redo, + Icon = iconUsage, Anchor = Anchor.Centre, Origin = Anchor.Centre, }); } + protected override bool OnClick(ClickEvent e) + { + OperationStarted?.Invoke(); + Action?.Invoke(); + OperationEnded?.Invoke(); + return true; + } + protected override void UpdateHoverState() { base.UpdateHoverState(); icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black; } + + public string TooltipText { get; } } private class DragHandle : Container From db1ad4243ec35e224580d00af45d9ee288296958 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 16:27:42 +0900 Subject: [PATCH 3539/6909] Remove need for ScaleDragHandle class --- .../Edit/Compose/Components/ComposeSelectionBox.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs index a26533fdb5..ef7bc0ba36 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs @@ -208,9 +208,10 @@ namespace osu.Game.Screens.Edit.Compose.Components }); } - private ScaleDragHandle createDragHandle(Anchor anchor) => - new ScaleDragHandle(anchor) + private DragHandle createDragHandle(Anchor anchor) => + new DragHandle { + Anchor = anchor, HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), OperationStarted = operationStarted, OperationEnded = operationEnded @@ -230,14 +231,6 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted?.Invoke(); } - private class ScaleDragHandle : DragHandle - { - public ScaleDragHandle(Anchor anchor) - { - Anchor = anchor; - } - } - private sealed class DragHandleButton : DragHandle, IHasTooltip { private SpriteIcon icon; From 1aff263419080160c9bbe078fb26005ae41ef0cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 16:34:34 +0900 Subject: [PATCH 3540/6909] Split out classes and simplify construction of buttons --- .../Editing/TestSceneComposeSelectBox.cs | 4 +- .../Compose/Components/ComposeSelectionBox.cs | 374 ------------------ .../Edit/Compose/Components/DragBox.cs | 2 +- .../Edit/Compose/Components/SelectionBox.cs | 210 ++++++++++ .../Components/SelectionBoxDragHandle.cs | 105 +++++ .../SelectionBoxDragHandleButton.cs | 66 ++++ .../Compose/Components/SelectionHandler.cs | 6 +- 7 files changed, 387 insertions(+), 380 deletions(-) delete mode 100644 osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandle.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleButton.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index 2e0be95ff7..da98a7a024 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Editing public TestSceneComposeSelectBox() { - ComposeSelectionBox selectionBox = null; + SelectionBox selectionBox = null; AddStep("create box", () => Child = selectionArea = new Container @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Editing Anchor = Anchor.Centre, Children = new Drawable[] { - selectionBox = new ComposeSelectionBox + selectionBox = new SelectionBox { CanRotate = true, CanScaleX = true, diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs deleted file mode 100644 index ef7bc0ba36..0000000000 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeSelectionBox.cs +++ /dev/null @@ -1,374 +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.Containers; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Sprites; -using osu.Framework.Input.Events; -using osu.Game.Graphics; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Screens.Edit.Compose.Components -{ - public class ComposeSelectionBox : CompositeDrawable - { - public Action OnRotation; - public Action OnScale; - public Action OnFlip; - - public Action OperationStarted; - public Action OperationEnded; - - private bool canRotate; - - /// - /// Whether rotation support should be enabled. - /// - public bool CanRotate - { - get => canRotate; - set - { - canRotate = value; - recreate(); - } - } - - private bool canScaleX; - - /// - /// Whether vertical scale support should be enabled. - /// - public bool CanScaleX - { - get => canScaleX; - set - { - canScaleX = value; - recreate(); - } - } - - private bool canScaleY; - - /// - /// Whether horizontal scale support should be enabled. - /// - public bool CanScaleY - { - get => canScaleY; - set - { - canScaleY = value; - recreate(); - } - } - - private FillFlowContainer buttons; - - public const float BORDER_RADIUS = 3; - - [Resolved] - private OsuColour colours { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - RelativeSizeAxes = Axes.Both; - - recreate(); - } - - private void recreate() - { - if (LoadState < LoadState.Loading) - return; - - InternalChildren = new Drawable[] - { - new Container - { - Masking = true, - BorderThickness = BORDER_RADIUS, - BorderColour = colours.YellowDark, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - - AlwaysPresent = true, - Alpha = 0 - }, - } - }, - buttons = new FillFlowContainer - { - Y = 20, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Anchor = Anchor.BottomCentre, - Origin = Anchor.Centre - } - }; - - if (CanScaleX) addXScaleComponents(); - if (CanScaleX && CanScaleY) addFullScaleComponents(); - if (CanScaleY) addYScaleComponents(); - if (CanRotate) addRotationComponents(); - } - - private void addRotationComponents() - { - const float separation = 40; - - buttons.Insert(-1, new DragHandleButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise") - { - OperationStarted = operationStarted, - OperationEnded = operationEnded, - Action = () => OnRotation?.Invoke(-90) - }); - - buttons.Add(new DragHandleButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise") - { - OperationStarted = operationStarted, - OperationEnded = operationEnded, - Action = () => OnRotation?.Invoke(90) - }); - - AddRangeInternal(new Drawable[] - { - new Box - { - Depth = float.MaxValue, - Colour = colours.YellowLight, - Blending = BlendingParameters.Additive, - Alpha = 0.3f, - Size = new Vector2(BORDER_RADIUS, separation), - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - }, - new DragHandleButton(FontAwesome.Solid.Redo, "Free rotate") - { - Anchor = Anchor.TopCentre, - Y = -separation, - HandleDrag = e => OnRotation?.Invoke(e.Delta.X), - OperationStarted = operationStarted, - OperationEnded = operationEnded - } - }); - } - - private void addYScaleComponents() - { - buttons.Add(new DragHandleButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically") - { - OperationStarted = operationStarted, - OperationEnded = operationEnded, - Action = () => OnFlip?.Invoke(Direction.Vertical) - }); - - AddRangeInternal(new[] - { - createDragHandle(Anchor.TopCentre), - createDragHandle(Anchor.BottomCentre), - }); - } - - private void addFullScaleComponents() - { - AddRangeInternal(new[] - { - createDragHandle(Anchor.TopLeft), - createDragHandle(Anchor.TopRight), - createDragHandle(Anchor.BottomLeft), - createDragHandle(Anchor.BottomRight), - }); - } - - private void addXScaleComponents() - { - buttons.Add(new DragHandleButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally") - { - OperationStarted = operationStarted, - OperationEnded = operationEnded, - Action = () => OnFlip?.Invoke(Direction.Horizontal) - }); - - AddRangeInternal(new[] - { - createDragHandle(Anchor.CentreLeft), - createDragHandle(Anchor.CentreRight), - }); - } - - private DragHandle createDragHandle(Anchor anchor) => - new DragHandle - { - Anchor = anchor, - HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), - OperationStarted = operationStarted, - OperationEnded = operationEnded - }; - - private int activeOperations; - - private void operationEnded() - { - if (--activeOperations == 0) - OperationEnded?.Invoke(); - } - - private void operationStarted() - { - if (activeOperations++ == 0) - OperationStarted?.Invoke(); - } - - private sealed class DragHandleButton : DragHandle, IHasTooltip - { - private SpriteIcon icon; - - private readonly IconUsage iconUsage; - - public Action Action; - - public DragHandleButton(IconUsage iconUsage, string tooltip) - { - this.iconUsage = iconUsage; - - TooltipText = tooltip; - - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - } - - [BackgroundDependencyLoader] - private void load() - { - Size *= 2; - AddInternal(icon = new SpriteIcon - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - Icon = iconUsage, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - } - - protected override bool OnClick(ClickEvent e) - { - OperationStarted?.Invoke(); - Action?.Invoke(); - OperationEnded?.Invoke(); - return true; - } - - protected override void UpdateHoverState() - { - base.UpdateHoverState(); - icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black; - } - - public string TooltipText { get; } - } - - private class DragHandle : Container - { - public Action OperationStarted; - public Action OperationEnded; - - public Action HandleDrag { get; set; } - - private Circle circle; - - [Resolved] - private OsuColour colours { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - Size = new Vector2(10); - Origin = Anchor.Centre; - - InternalChildren = new Drawable[] - { - circle = new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - }; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - UpdateHoverState(); - } - - protected override bool OnHover(HoverEvent e) - { - UpdateHoverState(); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - base.OnHoverLost(e); - UpdateHoverState(); - } - - protected bool HandlingMouse; - - protected override bool OnMouseDown(MouseDownEvent e) - { - HandlingMouse = true; - UpdateHoverState(); - return true; - } - - protected override bool OnDragStart(DragStartEvent e) - { - OperationStarted?.Invoke(); - return true; - } - - protected override void OnDrag(DragEvent e) - { - HandleDrag?.Invoke(e); - base.OnDrag(e); - } - - protected override void OnDragEnd(DragEndEvent e) - { - HandlingMouse = false; - OperationEnded?.Invoke(); - - UpdateHoverState(); - base.OnDragEnd(e); - } - - protected override void OnMouseUp(MouseUpEvent e) - { - HandlingMouse = false; - UpdateHoverState(); - base.OnMouseUp(e); - } - - protected virtual void UpdateHoverState() - { - circle.Colour = HandlingMouse ? colours.GrayF : (IsHovered ? colours.Red : colours.YellowDark); - this.ScaleTo(HandlingMouse || IsHovered ? 1.5f : 1, 100, Easing.OutQuint); - } - } - } -} diff --git a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs index 0ec981203a..eaee2cd1e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/DragBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/DragBox.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Masking = true, BorderColour = Color4.White, - BorderThickness = ComposeSelectionBox.BORDER_RADIUS, + BorderThickness = SelectionBox.BORDER_RADIUS, Child = new Box { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs new file mode 100644 index 0000000000..ac6a7da361 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -0,0 +1,210 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public class SelectionBox : CompositeDrawable + { + public Action OnRotation; + public Action OnScale; + public Action OnFlip; + + public Action OperationStarted; + public Action OperationEnded; + + private bool canRotate; + + /// + /// Whether rotation support should be enabled. + /// + public bool CanRotate + { + get => canRotate; + set + { + canRotate = value; + recreate(); + } + } + + private bool canScaleX; + + /// + /// Whether vertical scale support should be enabled. + /// + public bool CanScaleX + { + get => canScaleX; + set + { + canScaleX = value; + recreate(); + } + } + + private bool canScaleY; + + /// + /// Whether horizontal scale support should be enabled. + /// + public bool CanScaleY + { + get => canScaleY; + set + { + canScaleY = value; + recreate(); + } + } + + private FillFlowContainer buttons; + + public const float BORDER_RADIUS = 3; + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.Both; + + recreate(); + } + + private void recreate() + { + if (LoadState < LoadState.Loading) + return; + + InternalChildren = new Drawable[] + { + new Container + { + Masking = true, + BorderThickness = BORDER_RADIUS, + BorderColour = colours.YellowDark, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + + AlwaysPresent = true, + Alpha = 0 + }, + } + }, + buttons = new FillFlowContainer + { + Y = 20, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.BottomCentre, + Origin = Anchor.Centre + } + }; + + if (CanScaleX) addXScaleComponents(); + if (CanScaleX && CanScaleY) addFullScaleComponents(); + if (CanScaleY) addYScaleComponents(); + if (CanRotate) addRotationComponents(); + } + + private void addRotationComponents() + { + const float separation = 40; + + addButton(FontAwesome.Solid.Undo, "Rotate 90 degrees counter-clockwise", () => OnRotation?.Invoke(-90)); + addButton(FontAwesome.Solid.Redo, "Rotate 90 degrees clockwise", () => OnRotation?.Invoke(90)); + + AddRangeInternal(new Drawable[] + { + new Box + { + Depth = float.MaxValue, + Colour = colours.YellowLight, + Blending = BlendingParameters.Additive, + Alpha = 0.3f, + Size = new Vector2(BORDER_RADIUS, separation), + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, + new SelectionBoxDragHandleButton(FontAwesome.Solid.Redo, "Free rotate") + { + Anchor = Anchor.TopCentre, + Y = -separation, + HandleDrag = e => OnRotation?.Invoke(e.Delta.X), + OperationStarted = operationStarted, + OperationEnded = operationEnded + } + }); + } + + private void addYScaleComponents() + { + addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical)); + + addDragHandle(Anchor.TopCentre); + addDragHandle(Anchor.BottomCentre); + } + + private void addFullScaleComponents() + { + addDragHandle(Anchor.TopLeft); + addDragHandle(Anchor.TopRight); + addDragHandle(Anchor.BottomLeft); + addDragHandle(Anchor.BottomRight); + } + + private void addXScaleComponents() + { + addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally", () => OnFlip?.Invoke(Direction.Horizontal)); + + addDragHandle(Anchor.CentreLeft); + addDragHandle(Anchor.CentreRight); + } + + private void addButton(IconUsage icon, string tooltip, Action action) + { + buttons.Add(new SelectionBoxDragHandleButton(icon, tooltip) + { + OperationStarted = operationStarted, + OperationEnded = operationEnded, + Action = action + }); + } + + private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle + { + Anchor = anchor, + HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), + OperationStarted = operationStarted, + OperationEnded = operationEnded + }); + + private int activeOperations; + + private void operationEnded() + { + if (--activeOperations == 0) + OperationEnded?.Invoke(); + } + + private void operationStarted() + { + if (activeOperations++ == 0) + OperationStarted?.Invoke(); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandle.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandle.cs new file mode 100644 index 0000000000..921b4eb042 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandle.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; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public class SelectionBoxDragHandle : Container + { + public Action OperationStarted; + public Action OperationEnded; + + public Action HandleDrag { get; set; } + + private Circle circle; + + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Size = new Vector2(10); + Origin = Anchor.Centre; + + InternalChildren = new Drawable[] + { + circle = new Circle + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + UpdateHoverState(); + } + + protected override bool OnHover(HoverEvent e) + { + UpdateHoverState(); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + base.OnHoverLost(e); + UpdateHoverState(); + } + + protected bool HandlingMouse; + + protected override bool OnMouseDown(MouseDownEvent e) + { + HandlingMouse = true; + UpdateHoverState(); + return true; + } + + protected override bool OnDragStart(DragStartEvent e) + { + OperationStarted?.Invoke(); + return true; + } + + protected override void OnDrag(DragEvent e) + { + HandleDrag?.Invoke(e); + base.OnDrag(e); + } + + protected override void OnDragEnd(DragEndEvent e) + { + HandlingMouse = false; + OperationEnded?.Invoke(); + + UpdateHoverState(); + base.OnDragEnd(e); + } + + protected override void OnMouseUp(MouseUpEvent e) + { + HandlingMouse = false; + UpdateHoverState(); + base.OnMouseUp(e); + } + + protected virtual void UpdateHoverState() + { + circle.Colour = HandlingMouse ? colours.GrayF : (IsHovered ? colours.Red : colours.YellowDark); + this.ScaleTo(HandlingMouse || IsHovered ? 1.5f : 1, 100, Easing.OutQuint); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleButton.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleButton.cs new file mode 100644 index 0000000000..74ae949389 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBoxDragHandleButton.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 osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + /// + /// A drag "handle" which shares the visual appearance but behaves more like a clickable button. + /// + public sealed class SelectionBoxDragHandleButton : SelectionBoxDragHandle, IHasTooltip + { + private SpriteIcon icon; + + private readonly IconUsage iconUsage; + + public Action Action; + + public SelectionBoxDragHandleButton(IconUsage iconUsage, string tooltip) + { + this.iconUsage = iconUsage; + + TooltipText = tooltip; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load() + { + Size *= 2; + AddInternal(icon = new SpriteIcon + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Icon = iconUsage, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + + protected override bool OnClick(ClickEvent e) + { + OperationStarted?.Invoke(); + Action?.Invoke(); + OperationEnded?.Invoke(); + return true; + } + + protected override void UpdateHoverState() + { + base.UpdateHoverState(); + icon.Colour = !HandlingMouse && IsHovered ? Color4.White : Color4.Black; + } + + public string TooltipText { get; } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 1c2f09f831..fdf8dbe44e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private OsuSpriteText selectionDetailsText; - protected ComposeSelectionBox SelectionBox { get; private set; } + protected SelectionBox SelectionBox { get; private set; } [Resolved(CanBeNull = true)] protected EditorBeatmap EditorBeatmap { get; private set; } @@ -94,8 +94,8 @@ namespace osu.Game.Screens.Edit.Compose.Components }; } - public ComposeSelectionBox CreateSelectionBox() - => new ComposeSelectionBox + public SelectionBox CreateSelectionBox() + => new SelectionBox { OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, From 60e6cfa45cbfdd11768610ac6e10eb68497ea834 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 16:36:03 +0900 Subject: [PATCH 3541/6909] Avoid recreating child hierarchy when unnecessary --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index ac6a7da361..64191e48e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -31,6 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components get => canRotate; set { + if (canRotate == value) return; + canRotate = value; recreate(); } @@ -46,6 +48,8 @@ namespace osu.Game.Screens.Edit.Compose.Components get => canScaleX; set { + if (canScaleX == value) return; + canScaleX = value; recreate(); } @@ -61,6 +65,8 @@ namespace osu.Game.Screens.Edit.Compose.Components get => canScaleY; set { + if (canScaleY == value) return; + canScaleY = value; recreate(); } From 482c23901b4b9aed6f62be5d2fbe719874e72ff6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 16:54:56 +0900 Subject: [PATCH 3542/6909] Check RequestedPlaying state before allowing scheduled resume of looped sample --- osu.Game/Skinning/PausableSkinnableSound.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index d080e2ccd9..9819574b1d 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -41,8 +41,14 @@ namespace osu.Game.Skinning // it's not easy to know if a sample has finished playing (to end). // to keep things simple only resume playing looping samples. else if (Looping) + { // schedule so we don't start playing a sample which is no longer alive. - Schedule(base.Play); + Schedule(() => + { + if (RequestedPlaying) + base.Play(); + }); + } } }); } From 538973e3942ea8c589e5e98f35890605cb69f5b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 17:06:05 +0900 Subject: [PATCH 3543/6909] Use float methods for math operations --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 6b4f13db35..daf4a0102b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -183,8 +183,8 @@ namespace osu.Game.Rulesets.Osu.Edit point.Y -= origin.Y; Vector2 ret; - ret.X = (float)(point.X * Math.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * Math.Sin(angle / 180f * Math.PI)); - ret.Y = (float)(point.X * -Math.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * Math.Cos(angle / 180f * Math.PI)); + ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(angle / 180f * MathF.PI); + ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(angle / 180f * MathF.PI); ret.X += origin.X; ret.Y += origin.Y; From b6dc8bb2d3f16fa41934187fe9957865db1d2f20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 18:10:05 +0900 Subject: [PATCH 3544/6909] Fix remaining manual degree-to-radian conversions --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index daf4a0102b..a0f70ce408 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -183,8 +183,8 @@ namespace osu.Game.Rulesets.Osu.Edit point.Y -= origin.Y; Vector2 ret; - ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(angle / 180f * MathF.PI); - ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(angle / 180f * MathF.PI); + ret.X = point.X * MathF.Cos(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Sin(MathUtils.DegreesToRadians(angle)); + ret.Y = point.X * -MathF.Sin(MathUtils.DegreesToRadians(angle)) + point.Y * MathF.Cos(MathUtils.DegreesToRadians(angle)); ret.X += origin.X; ret.Y += origin.Y; From 0d03084cdc03c849e25320913ae2cff97bdda723 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 17:19:35 +0900 Subject: [PATCH 3545/6909] Move control point display to the base timeline class We want them to display on all screens with a timeline as they are quite useful in all cases. --- .../Compose/Components/Timeline/Timeline.cs | 28 ++++++++++++++----- .../Components/Timeline/TimelineArea.cs | 17 ++++++++--- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 6 ---- 3 files changed, 34 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index ed3d328330..a93ad9ac0d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -12,6 +12,7 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -21,6 +22,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public class Timeline : ZoomableScrollContainer, IPositionSnapProvider { public readonly Bindable WaveformVisible = new Bindable(); + + public readonly Bindable ControlPointsVisible = new Bindable(); + public readonly IBindable Beatmap = new Bindable(); [Resolved] @@ -56,24 +60,34 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } private WaveformGraph waveform; + private ControlPointPart controlPoints; [BackgroundDependencyLoader] private void load(IBindable beatmap, OsuColour colours) { - Add(waveform = new WaveformGraph + AddRange(new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = colours.Blue.Opacity(0.2f), - LowColour = colours.BlueLighter, - MidColour = colours.BlueDark, - HighColour = colours.BlueDarker, - Depth = float.MaxValue + waveform = new WaveformGraph + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Blue.Opacity(0.2f), + LowColour = colours.BlueLighter, + MidColour = colours.BlueDark, + HighColour = colours.BlueDarker, + Depth = float.MaxValue + }, + controlPoints = new ControlPointPart + { + RelativeSizeAxes = Axes.Both + }, + new TimelineTickDisplay(), }); // We don't want the centre marker to scroll AddInternal(new CentreMarker { Depth = float.MaxValue }); WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); + ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); Beatmap.BindTo(beatmap); Beatmap.BindValueChanged(b => diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index d870eb5279..1d2d46517b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline CornerRadius = 5; OsuCheckbox waveformCheckbox; + OsuCheckbox controlPointsCheckbox; InternalChildren = new Drawable[] { @@ -57,12 +58,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Origin = Anchor.CentreLeft, AutoSizeAxes = Axes.Y, Width = 160, - Padding = new MarginPadding { Horizontal = 15 }, + Padding = new MarginPadding { Horizontal = 10 }, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 4), Children = new[] { - waveformCheckbox = new OsuCheckbox { LabelText = "Waveform" } + waveformCheckbox = new OsuCheckbox + { + LabelText = "Waveform", + Current = { Value = true }, + }, + controlPointsCheckbox = new OsuCheckbox + { + LabelText = "Control Points", + Current = { Value = true }, + } } } } @@ -119,9 +129,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } }; - waveformCheckbox.Current.Value = true; - Timeline.WaveformVisible.BindTo(waveformCheckbox.Current); + Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current); } private void changeZoom(float change) => Timeline.Zoom += change; diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 0a0cfe193d..269874fea8 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -12,7 +12,6 @@ using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; @@ -31,11 +30,6 @@ namespace osu.Game.Screens.Edit.Timing { } - protected override Drawable CreateTimelineContent() => new ControlPointPart - { - RelativeSizeAxes = Axes.Both, - }; - protected override Drawable CreateMainContent() => new GridContainer { RelativeSizeAxes = Axes.Both, From b654396a4cb89def74d240b795cc73dbf2602534 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 17:40:31 +0900 Subject: [PATCH 3546/6909] Move ticks display to timeline --- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index d6d782e70c..b9457f422a 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -112,7 +112,6 @@ namespace osu.Game.Screens.Edit RelativeSizeAxes = Axes.Both, Children = new[] { - new TimelineTickDisplay(), CreateTimelineContent(), } }, t => From 00a19b4879954b5ab4f143f417eb477763f4e5f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 18:14:10 +0900 Subject: [PATCH 3547/6909] Also add toggle for ticks display --- .../Screens/Edit/Compose/Components/Timeline/Timeline.cs | 6 +++++- .../Edit/Compose/Components/Timeline/TimelineArea.cs | 7 +++++++ 2 files changed, 12 insertions(+), 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 a93ad9ac0d..e6b0dd715a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -25,6 +25,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public readonly Bindable ControlPointsVisible = new Bindable(); + public readonly Bindable TicksVisible = new Bindable(); + public readonly IBindable Beatmap = new Bindable(); [Resolved] @@ -61,6 +63,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private WaveformGraph waveform; private ControlPointPart controlPoints; + private TimelineTickDisplay ticks; [BackgroundDependencyLoader] private void load(IBindable beatmap, OsuColour colours) @@ -80,7 +83,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { RelativeSizeAxes = Axes.Both }, - new TimelineTickDisplay(), + ticks = new TimelineTickDisplay(), }); // We don't want the centre marker to scroll @@ -88,6 +91,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); + TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); Beatmap.BindTo(beatmap); Beatmap.BindValueChanged(b => diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs index 1d2d46517b..0ec48e04c6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineArea.cs @@ -26,6 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline OsuCheckbox waveformCheckbox; OsuCheckbox controlPointsCheckbox; + OsuCheckbox ticksCheckbox; InternalChildren = new Drawable[] { @@ -72,6 +73,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { LabelText = "Control Points", Current = { Value = true }, + }, + ticksCheckbox = new OsuCheckbox + { + LabelText = "Ticks", + Current = { Value = true }, } } } @@ -131,6 +137,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Timeline.WaveformVisible.BindTo(waveformCheckbox.Current); Timeline.ControlPointsVisible.BindTo(controlPointsCheckbox.Current); + Timeline.TicksVisible.BindTo(ticksCheckbox.Current); } private void changeZoom(float change) => Timeline.Zoom += change; From ffc1e9c35881288b24a2e11b5a405190bb14e860 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 18:23:38 +0900 Subject: [PATCH 3548/6909] 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 afc5d4ec52..78ceaa8616 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 5fa1685d9b..3a839ac1a4 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 60708a39e2..48c91f0d53 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 70d475be1fec41d3e565fb38cd1e1f768c8525a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 18:54:59 +0900 Subject: [PATCH 3549/6909] Fix elements appearing in front of hitobjects --- .../Compose/Components/Timeline/Timeline.cs | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index e6b0dd715a..3e54813a14 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -8,6 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -70,20 +71,27 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { AddRange(new Drawable[] { - waveform = new WaveformGraph + new Container { RelativeSizeAxes = Axes.Both, - Colour = colours.Blue.Opacity(0.2f), - LowColour = colours.BlueLighter, - MidColour = colours.BlueDark, - HighColour = colours.BlueDarker, - Depth = float.MaxValue + Depth = float.MaxValue, + Children = new Drawable[] + { + waveform = new WaveformGraph + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Blue.Opacity(0.2f), + LowColour = colours.BlueLighter, + MidColour = colours.BlueDark, + HighColour = colours.BlueDarker, + }, + controlPoints = new ControlPointPart + { + RelativeSizeAxes = Axes.Both + }, + ticks = new TimelineTickDisplay(), + } }, - controlPoints = new ControlPointPart - { - RelativeSizeAxes = Axes.Both - }, - ticks = new TimelineTickDisplay(), }); // We don't want the centre marker to scroll From 70931abcb0a2b80cfc9aaecbddbc71e6c7ff5b89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 17:54:54 +0900 Subject: [PATCH 3550/6909] Separate out timeline control point display from summary timeline display --- .../Timeline/DifficultyPointPiece.cs | 66 +++++++++++++++++++ .../Compose/Components/Timeline/Timeline.cs | 10 ++- .../Timeline/TimelineControlPointDisplay.cs | 57 ++++++++++++++++ .../Timeline/TimelineControlPointGroup.cs | 55 ++++++++++++++++ 4 files changed, 182 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs create mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs new file mode 100644 index 0000000000..4d5970d7e7 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.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.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class DifficultyPointPiece : CompositeDrawable + { + private OsuSpriteText speedMultiplierText; + private readonly BindableNumber speedMultiplier; + + public DifficultyPointPiece(DifficultyControlPoint point) + { + speedMultiplier = point.SpeedMultiplierBindable.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + RelativeSizeAxes = Axes.Y; + AutoSizeAxes = Axes.X; + + Color4 colour = colours.GreenDark; + + InternalChildren = new Drawable[] + { + new Box + { + Colour = colour, + Width = 2, + RelativeSizeAxes = Axes.Y, + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = colour, + RelativeSizeAxes = Axes.Both, + }, + speedMultiplierText = new OsuSpriteText + { + Font = OsuFont.Default.With(weight: FontWeight.Bold), + Colour = Color4.White, + } + } + }, + }; + + speedMultiplier.BindValueChanged(multiplier => speedMultiplierText.Text = $"{multiplier.NewValue:n2}x", true); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 3e54813a14..3d2e2ebef7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -13,7 +13,6 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -63,9 +62,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } private WaveformGraph waveform; - private ControlPointPart controlPoints; + private TimelineTickDisplay ticks; + private TimelineControlPointDisplay controlPoints; + [BackgroundDependencyLoader] private void load(IBindable beatmap, OsuColour colours) { @@ -85,10 +86,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline MidColour = colours.BlueDark, HighColour = colours.BlueDarker, }, - controlPoints = new ControlPointPart - { - RelativeSizeAxes = Axes.Both - }, + controlPoints = new TimelineControlPointDisplay(), ticks = new TimelineTickDisplay(), } }, diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs new file mode 100644 index 0000000000..3f13e8e5d4 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.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.Specialized; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + /// + /// The part of the timeline that displays the control points. + /// + public class TimelineControlPointDisplay : TimelinePart + { + private IBindableList controlPointGroups; + + public TimelineControlPointDisplay() + { + RelativeSizeAxes = Axes.Both; + } + + protected override void LoadBeatmap(WorkingBeatmap beatmap) + { + base.LoadBeatmap(beatmap); + + controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); + controlPointGroups.BindCollectionChanged((sender, args) => + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Reset: + Clear(); + break; + + case NotifyCollectionChangedAction.Add: + foreach (var group in args.NewItems.OfType()) + Add(new TimelineControlPointGroup(group)); + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var group in args.OldItems.OfType()) + { + var matching = Children.SingleOrDefault(gv => gv.Group == group); + + matching?.Expire(); + } + + break; + } + }, true); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs new file mode 100644 index 0000000000..5429e7c55b --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.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.Framework.Graphics.Containers; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class TimelineControlPointGroup : CompositeDrawable + { + public readonly ControlPointGroup Group; + + private BindableList controlPoints; + + [Resolved] + private OsuColour colours { get; set; } + + public TimelineControlPointGroup(ControlPointGroup group) + { + Origin = Anchor.TopCentre; + + Group = group; + + RelativePositionAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + + Width = 1; + + X = (float)group.Time; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy(); + controlPoints.BindCollectionChanged((_, __) => + { + foreach (var point in controlPoints) + { + switch (point) + { + case DifficultyControlPoint difficultyPoint: + AddInternal(new DifficultyPointPiece(difficultyPoint)); + break; + } + } + }, true); + } + } +} From 0bced34272de9f403bd85c47a5d99ffb844870e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 18:07:39 +0900 Subject: [PATCH 3551/6909] Add visualisation of bpm (timing) changes to timeline --- .../Timeline/TimelineControlPointGroup.cs | 11 ++-- .../Components/Timeline/TimingPointPiece.cs | 57 +++++++++++++++++++ 2 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index 5429e7c55b..05a7f6e493 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -21,14 +21,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public TimelineControlPointGroup(ControlPointGroup group) { - Origin = Anchor.TopCentre; - Group = group; RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Y; - - Width = 1; + AutoSizeAxes = Axes.X; X = (float)group.Time; } @@ -40,6 +37,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy(); controlPoints.BindCollectionChanged((_, __) => { + ClearInternal(); + foreach (var point in controlPoints) { switch (point) @@ -47,6 +46,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline case DifficultyControlPoint difficultyPoint: AddInternal(new DifficultyPointPiece(difficultyPoint)); break; + + case TimingControlPoint timingPoint: + AddInternal(new TimingPointPiece(timingPoint)); + break; } } }, true); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs new file mode 100644 index 0000000000..de7cfecbf0 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.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.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class TimingPointPiece : CompositeDrawable + { + private readonly BindableNumber beatLength; + private OsuSpriteText bpmText; + + public TimingPointPiece(TimingControlPoint point) + { + beatLength = point.BeatLengthBindable.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Origin = Anchor.CentreLeft; + Anchor = Anchor.CentreLeft; + + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Box + { + Alpha = 0.9f, + Colour = ColourInfo.GradientHorizontal(colours.YellowDark, colours.YellowDark.Opacity(0.5f)), + RelativeSizeAxes = Axes.Both, + }, + bpmText = new OsuSpriteText + { + Alpha = 0.9f, + Padding = new MarginPadding(3), + Font = OsuFont.Default.With(size: 40) + } + }; + + beatLength.BindValueChanged(beatLength => + { + bpmText.Text = $"{60000 / beatLength.NewValue:n1} BPM"; + }, true); + } + } +} From b75c202a7e5547b4cf4d12c15bc1537418150ab8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 18:49:48 +0900 Subject: [PATCH 3552/6909] Add sample control point display in timeline --- .../Timeline/DifficultyPointPiece.cs | 2 - .../Components/Timeline/SamplePointPiece.cs | 81 +++++++++++++++++++ .../Timeline/TimelineControlPointGroup.cs | 4 + 3 files changed, 85 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 4d5970d7e7..31cc768056 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -41,8 +41,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, new Container { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, AutoSizeAxes = Axes.Both, Children = new Drawable[] { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs new file mode 100644 index 0000000000..67da335f6b --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -0,0 +1,81 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK.Graphics; + +namespace osu.Game.Screens.Edit.Compose.Components.Timeline +{ + public class SamplePointPiece : CompositeDrawable + { + private readonly Bindable bank; + private readonly BindableNumber volume; + + private OsuSpriteText text; + private Box volumeBox; + + public SamplePointPiece(SampleControlPoint samplePoint) + { + volume = samplePoint.SampleVolumeBindable.GetBoundCopy(); + bank = samplePoint.SampleBankBindable.GetBoundCopy(); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Origin = Anchor.TopLeft; + Anchor = Anchor.TopLeft; + + AutoSizeAxes = Axes.X; + RelativeSizeAxes = Axes.Y; + + Color4 colour = colours.BlueDarker; + + InternalChildren = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Y, + Width = 20, + Children = new Drawable[] + { + volumeBox = new Box + { + X = 2, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Colour = ColourInfo.GradientVertical(colour, Color4.Black), + RelativeSizeAxes = Axes.Both, + }, + new Box + { + Colour = colours.Blue, + Width = 2, + RelativeSizeAxes = Axes.Y, + }, + } + }, + text = new OsuSpriteText + { + X = 2, + Y = -5, + Anchor = Anchor.BottomLeft, + Alpha = 0.9f, + Rotation = -90, + Font = OsuFont.Default.With(weight: FontWeight.SemiBold) + } + }; + + volume.BindValueChanged(volume => volumeBox.Height = volume.NewValue / 100f, true); + bank.BindValueChanged(bank => text.Text = $"{bank.NewValue}", true); + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index 05a7f6e493..1a09a05a6c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -50,6 +50,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline case TimingControlPoint timingPoint: AddInternal(new TimingPointPiece(timingPoint)); break; + + case SampleControlPoint samplePoint: + AddInternal(new SamplePointPiece(samplePoint)); + break; } } }, true); From 589a26a149d11b99c70560b117d79913729d4f59 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 18:59:35 +0900 Subject: [PATCH 3553/6909] Ensure stable display order for control points in the same group --- .../Compose/Components/Timeline/TimelineControlPointGroup.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index 1a09a05a6c..e32616a574 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline switch (point) { case DifficultyControlPoint difficultyPoint: - AddInternal(new DifficultyPointPiece(difficultyPoint)); + AddInternal(new DifficultyPointPiece(difficultyPoint) { Depth = -2 }); break; case TimingControlPoint timingPoint: @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline break; case SampleControlPoint samplePoint: - AddInternal(new SamplePointPiece(samplePoint)); + AddInternal(new SamplePointPiece(samplePoint) { Depth = -1 }); break; } } From fcccce8b4e2f5466059906b82c4ad1f76ba45df7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 19:03:17 +0900 Subject: [PATCH 3554/6909] Use pink for sample control points to avoid clash with waveform blue --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 67da335f6b..6a6e947343 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AutoSizeAxes = Axes.X; RelativeSizeAxes = Axes.Y; - Color4 colour = colours.BlueDarker; + Color4 colour = colours.PinkDarker; InternalChildren = new Drawable[] { @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, new Box { - Colour = colours.Blue, + Colour = colours.Pink, Width = 2, RelativeSizeAxes = Axes.Y, }, From e96e30a19d3bf861ada8cac8d85c59852e63c25f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 19:29:34 +0900 Subject: [PATCH 3555/6909] Move control point colour specifications to common location and use for formatting timing screen table --- osu.Game/Beatmaps/ControlPoints/ControlPoint.cs | 4 ++++ .../ControlPoints/DifficultyControlPoint.cs | 4 ++++ .../ControlPoints/EffectControlPoint.cs | 4 ++++ .../ControlPoints/SampleControlPoint.cs | 4 ++++ .../ControlPoints/TimingControlPoint.cs | 4 ++++ .../Components/Timeline/DifficultyPointPiece.cs | 9 ++++++--- .../Components/Timeline/SamplePointPiece.cs | 8 ++++++-- .../Components/Timeline/TimingPointPiece.cs | 8 +++++++- .../Screens/Edit/Timing/ControlPointTable.cs | 17 +++++++++++++---- osu.Game/Screens/Edit/Timing/RowAttribute.cs | 7 +++++-- 10 files changed, 57 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index a1822a1163..c6649f6af1 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { @@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints public int CompareTo(ControlPoint other) => Time.CompareTo(other.Time); + public virtual Color4 GetRepresentingColour(OsuColour colours) => colours.Yellow; + /// /// Determines whether this results in a meaningful change when placed alongside another. /// diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 1d38790f87..283bf76572 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { @@ -23,6 +25,8 @@ namespace osu.Game.Beatmaps.ControlPoints MaxValue = 10 }; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.GreenDark; + /// /// The speed multiplier at this control point. /// diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index 9e8e3978be..ea28fca170 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { @@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints /// public readonly BindableBool OmitFirstBarLineBindable = new BindableBool(); + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Purple; + /// /// Whether the first bar line of this control point is ignored. /// diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index c052c04ea0..f57ecfb9e3 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -3,6 +3,8 @@ using osu.Framework.Bindables; using osu.Game.Audio; +using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { @@ -16,6 +18,8 @@ namespace osu.Game.Beatmaps.ControlPoints SampleVolumeBindable = { Disabled = true } }; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.Pink; + /// /// The default sample bank at this control point. /// diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index 9345299c3a..d9378bca4a 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -3,6 +3,8 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps.Timing; +using osu.Game.Graphics; +using osuTK.Graphics; namespace osu.Game.Beatmaps.ControlPoints { @@ -18,6 +20,8 @@ namespace osu.Game.Beatmaps.ControlPoints /// private const double default_beat_length = 60000.0 / 60.0; + public override Color4 GetRepresentingColour(OsuColour colours) => colours.YellowDark; + public static readonly TimingControlPoint DEFAULT = new TimingControlPoint { BeatLengthBindable = diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs index 31cc768056..510ba8c094 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/DifficultyPointPiece.cs @@ -15,12 +15,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class DifficultyPointPiece : CompositeDrawable { + private readonly DifficultyControlPoint difficultyPoint; + private OsuSpriteText speedMultiplierText; private readonly BindableNumber speedMultiplier; - public DifficultyPointPiece(DifficultyControlPoint point) + public DifficultyPointPiece(DifficultyControlPoint difficultyPoint) { - speedMultiplier = point.SpeedMultiplierBindable.GetBoundCopy(); + this.difficultyPoint = difficultyPoint; + speedMultiplier = difficultyPoint.SpeedMultiplierBindable.GetBoundCopy(); } [BackgroundDependencyLoader] @@ -29,7 +32,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Y; AutoSizeAxes = Axes.X; - Color4 colour = colours.GreenDark; + Color4 colour = difficultyPoint.GetRepresentingColour(colours); InternalChildren = new Drawable[] { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 6a6e947343..ffc0e55940 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -3,6 +3,7 @@ using 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; @@ -16,6 +17,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class SamplePointPiece : CompositeDrawable { + private readonly SampleControlPoint samplePoint; + private readonly Bindable bank; private readonly BindableNumber volume; @@ -24,6 +27,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public SamplePointPiece(SampleControlPoint samplePoint) { + this.samplePoint = samplePoint; volume = samplePoint.SampleVolumeBindable.GetBoundCopy(); bank = samplePoint.SampleBankBindable.GetBoundCopy(); } @@ -37,7 +41,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AutoSizeAxes = Axes.X; RelativeSizeAxes = Axes.Y; - Color4 colour = colours.PinkDarker; + Color4 colour = samplePoint.GetRepresentingColour(colours); InternalChildren = new Drawable[] { @@ -57,7 +61,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, new Box { - Colour = colours.Pink, + Colour = colour.Lighten(0.2f), Width = 2, RelativeSizeAxes = Axes.Y, }, diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs index de7cfecbf0..ba94916458 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimingPointPiece.cs @@ -11,16 +11,20 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimingPointPiece : CompositeDrawable { + private readonly TimingControlPoint point; + private readonly BindableNumber beatLength; private OsuSpriteText bpmText; public TimingPointPiece(TimingControlPoint point) { + this.point = point; beatLength = point.BeatLengthBindable.GetBoundCopy(); } @@ -32,12 +36,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline AutoSizeAxes = Axes.Both; + Color4 colour = point.GetRepresentingColour(colours); + InternalChildren = new Drawable[] { new Box { Alpha = 0.9f, - Colour = ColourInfo.GradientHorizontal(colours.YellowDark, colours.YellowDark.Opacity(0.5f)), + Colour = ColourInfo.GradientHorizontal(colour, colour.Opacity(0.5f)), RelativeSizeAxes = Axes.Both, }, bpmText = new OsuSpriteText diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 87af4546f1..4121e1f7bb 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -114,7 +114,14 @@ namespace osu.Game.Screens.Edit.Timing controlPoints = group.ControlPoints.GetBoundCopy(); controlPoints.CollectionChanged += (_, __) => createChildren(); + } + [Resolved] + private OsuColour colours { get; set; } + + [BackgroundDependencyLoader] + private void load() + { createChildren(); } @@ -125,20 +132,22 @@ namespace osu.Game.Screens.Edit.Timing private Drawable createAttribute(ControlPoint controlPoint) { + Color4 colour = controlPoint.GetRepresentingColour(colours); + switch (controlPoint) { case TimingControlPoint timing: - return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}"); + return new RowAttribute("timing", () => $"{60000 / timing.BeatLength:n1}bpm {timing.TimeSignature}", colour); case DifficultyControlPoint difficulty: - return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x"); + return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x", colour); case EffectControlPoint effect: - return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}"); + return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}", colour); case SampleControlPoint sample: - return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%"); + return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%", colour); } return null; diff --git a/osu.Game/Screens/Edit/Timing/RowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttribute.cs index be8f693683..c45995ee83 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttribute.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osuTK.Graphics; namespace osu.Game.Screens.Edit.Timing { @@ -16,11 +17,13 @@ namespace osu.Game.Screens.Edit.Timing { private readonly string header; private readonly Func content; + private readonly Color4 colour; - public RowAttribute(string header, Func content) + public RowAttribute(string header, Func content, Color4 colour) { this.header = header; this.content = content; + this.colour = colour; } [BackgroundDependencyLoader] @@ -40,7 +43,7 @@ namespace osu.Game.Screens.Edit.Timing { new Box { - Colour = colours.Yellow, + Colour = colour, RelativeSizeAxes = Axes.Both, }, new OsuSpriteText From 5ad2944e26e9714a10006ad2879a0840623b46d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 1 Oct 2020 19:31:41 +0900 Subject: [PATCH 3556/6909] Fix ticks displaying higher than control point info --- 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 3d2e2ebef7..be3bca3242 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -86,8 +86,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline MidColour = colours.BlueDark, HighColour = colours.BlueDarker, }, - controlPoints = new TimelineControlPointDisplay(), ticks = new TimelineTickDisplay(), + controlPoints = new TimelineControlPointDisplay(), } }, }); From 7e5ecd84bc39dabad82b32e926e3a73ceee4ae46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 1 Oct 2020 12:41:44 +0200 Subject: [PATCH 3557/6909] Add braces to clear up operator precedence --- osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 63cd48676e..a5f20378fe 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) { - spinnerBlink = !source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value ?? true; + spinnerBlink = !(source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value) ?? true; drawableSpinner = (DrawableSpinner)drawableObject; From 3e6af7ce43975aea05208ef52654038a7984360a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 20:09:09 +0900 Subject: [PATCH 3558/6909] Refactor for readability --- osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index a5f20378fe..c952500bbf 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) { - spinnerBlink = !(source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value) ?? true; + spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true; drawableSpinner = (DrawableSpinner)drawableObject; From ba76089219cd9489cf10b160898cafea7414192c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 20:24:32 +0900 Subject: [PATCH 3559/6909] Fix spinner flashing yellow glow before completion --- .../Objects/Drawables/Pieces/DefaultSpinnerDisc.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 2862fe49bd..587bd415ee 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -93,6 +93,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; + + updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); } protected override void Update() @@ -124,6 +126,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { + if (!(drawableHitObject is DrawableSpinner)) + return; + centre.ScaleTo(0); mainContainer.ScaleTo(0); From 6d3f4c8699f713ab5146401254647c9aab421f6b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 20:38:47 +0900 Subject: [PATCH 3560/6909] Fix a few more similar cases --- osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs | 5 +++++ osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index bcb2af8e3e..1dfc9c0772 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -72,10 +72,15 @@ namespace osu.Game.Rulesets.Osu.Skinning this.FadeOut(); drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; + + updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { + if (!(drawableHitObject is DrawableSpinner)) + return; + var spinner = (Spinner)drawableSpinner.HitObject; using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index a45d91801d..c498179eef 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -86,10 +86,15 @@ namespace osu.Game.Rulesets.Osu.Skinning this.FadeOut(); drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; + + updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); } private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { + if (!(drawableHitObject is DrawableSpinner)) + return; + var spinner = drawableSpinner.HitObject; using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) From 62b55c4c9cb57eb436c8a3a4447f6f20c691dade Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 20:50:47 +0900 Subject: [PATCH 3561/6909] Use static method, add xmldoc + link to wiki --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 33 +++++++++++-------- osu.Game/Beatmaps/BeatmapInfo.cs | 16 +-------- 2 files changed, 21 insertions(+), 28 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 159a229499..945a60fb62 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -114,6 +114,25 @@ namespace osu.Game.Beatmaps return computeDifficulty(key, beatmapInfo, rulesetInfo); } + /// + /// Retrieves the that describes a star rating. + /// + /// + /// For more information, see: https://osu.ppy.sh/help/wiki/Difficulties + /// + /// The star rating. + /// The that best describes . + public static DifficultyRating GetDifficultyRating(double starRating) + { + if (starRating < 2.0) return DifficultyRating.Easy; + if (starRating < 2.7) return DifficultyRating.Normal; + if (starRating < 4.0) return DifficultyRating.Hard; + if (starRating < 5.3) return DifficultyRating.Insane; + if (starRating < 6.5) return DifficultyRating.Expert; + + return DifficultyRating.ExpertPlus; + } + private CancellationTokenSource trackedUpdateCancellationSource; private readonly List linkedCancellationSources = new List(); @@ -308,18 +327,6 @@ namespace osu.Game.Beatmaps // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) } - public DifficultyRating DifficultyRating - { - get - { - if (Stars < 2.0) return DifficultyRating.Easy; - if (Stars < 2.7) return DifficultyRating.Normal; - if (Stars < 4.0) return DifficultyRating.Hard; - if (Stars < 5.3) return DifficultyRating.Insane; - if (Stars < 6.5) return DifficultyRating.Expert; - - return DifficultyRating.ExpertPlus; - } - } + public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(Stars); } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index c5be5810e9..acab525821 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -135,21 +135,7 @@ namespace osu.Game.Beatmaps public List Scores { get; set; } [JsonIgnore] - public DifficultyRating DifficultyRating - { - get - { - var rating = StarDifficulty; - - if (rating < 2.0) return DifficultyRating.Easy; - if (rating < 2.7) return DifficultyRating.Normal; - if (rating < 4.0) return DifficultyRating.Hard; - if (rating < 5.3) return DifficultyRating.Insane; - if (rating < 6.5) return DifficultyRating.Expert; - - return DifficultyRating.ExpertPlus; - } - } + public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(StarDifficulty); public string[] SearchableTerms => new[] { From d7f9b8045cc99a153342527217246953c146401f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 21:33:54 +0900 Subject: [PATCH 3562/6909] Safeguard againts multiple ApplyResult() invocations --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index f159d28eed..abfe7eb58c 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -469,6 +469,9 @@ namespace osu.Game.Rulesets.Objects.Drawables /// The callback that applies changes to the . protected void ApplyResult(Action application) { + if (Result.HasResult) + throw new InvalidOperationException($"Cannot apply result on a hitobject that already has a result."); + application?.Invoke(Result); if (!Result.HasResult) From 40c153e705f2bcb9cbcfa96615fe853edfd62a01 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 21:39:40 +0900 Subject: [PATCH 3563/6909] Use component instead of drawable --- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 9ffe813187..45327d4514 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -122,7 +122,7 @@ namespace osu.Game.Beatmaps.Drawables public object TooltipContent => shouldShowTooltip ? new DifficultyIconTooltipContent(beatmap, difficultyBindable) : null; - private class DifficultyRetriever : Drawable + private class DifficultyRetriever : Component { public readonly Bindable StarDifficulty = new Bindable(); From 042c39ae1b04f3653cb068acb2089f01213a3aed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 21:48:45 +0900 Subject: [PATCH 3564/6909] Remove redundant string interpolation --- 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 abfe7eb58c..a2d222d0a8 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -470,7 +470,7 @@ namespace osu.Game.Rulesets.Objects.Drawables protected void ApplyResult(Action application) { if (Result.HasResult) - throw new InvalidOperationException($"Cannot apply result on a hitobject that already has a result."); + throw new InvalidOperationException("Cannot apply result on a hitobject that already has a result."); application?.Invoke(Result); From ab33434a8a80f3c50228e52e7e7f643a3f71e40c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 1 Oct 2020 21:54:43 +0900 Subject: [PATCH 3565/6909] Reword xmldocs to better describe nested events --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index f159d28eed..24ee3f629d 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -51,12 +51,12 @@ namespace osu.Game.Rulesets.Objects.Drawables public override bool PropagateNonPositionalInputSubTree => HandleUserInput; /// - /// Invoked when a has been applied by this or a nested . + /// Invoked by this or a nested after a has been applied. /// public event Action OnNewResult; /// - /// Invoked when a is being reverted by this or a nested . + /// Invoked by this or a nested prior to a being reverted. /// public event Action OnRevertResult; @@ -236,7 +236,7 @@ namespace osu.Game.Rulesets.Objects.Drawables #region State / Transform Management /// - /// Bind to apply a custom state which can override the default implementation. + /// Invoked by this or a nested to apply a custom state that can override the default implementation. /// public event Action ApplyCustomUpdateState; From c72dbf1ba0e84ff3d19c2eb780d292ad1c4d56cf Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Thu, 1 Oct 2020 17:15:49 +0000 Subject: [PATCH 3566/6909] Bump ppy.osu.Framework.NativeLibs from 2020.213.0 to 2020.923.0 Bumps [ppy.osu.Framework.NativeLibs](https://github.com/ppy/osu-framework) from 2020.213.0 to 2020.923.0. - [Release notes](https://github.com/ppy/osu-framework/releases) - [Commits](https://github.com/ppy/osu-framework/compare/2020.213.0...2020.923.0) Signed-off-by: dependabot-preview[bot] --- osu.iOS.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.iOS.props b/osu.iOS.props index 48c91f0d53..31f1af135d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -85,6 +85,6 @@ - + From 9e52f9c8582ec697d8ba3658a035747e04336697 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 1 Oct 2020 23:23:28 +0300 Subject: [PATCH 3567/6909] Consider cursor size in trail interval --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 9bcb3abc63..546bb3f233 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Runtime.InteropServices; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Batches; using osu.Framework.Graphics.OpenGL.Vertices; @@ -15,6 +16,7 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Framework.Timing; +using osu.Game.Configuration; using osuTK; using osuTK.Graphics; using osuTK.Graphics.ES30; @@ -28,6 +30,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly TrailPart[] parts = new TrailPart[max_sprites]; private int currentIndex; private IShader shader; + private Bindable cursorSize; private double timeOffset; private float time; @@ -48,9 +51,10 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } [BackgroundDependencyLoader] - private void load(ShaderManager shaders) + private void load(ShaderManager shaders, OsuConfigManager config) { shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); + cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize); } protected override void LoadComplete() @@ -147,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor float distance = diff.Length; Vector2 direction = diff / distance; - float interval = partSize.X / 2.5f; + float interval = partSize.X / 2.5f / cursorSize.Value; for (float d = interval; d < distance; d += interval) { From abf1afd3f125ac970ab1924c7d956629f5477e10 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 1 Oct 2020 23:27:57 +0300 Subject: [PATCH 3568/6909] Do not decrease density --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 546bb3f233..8a1dc9b8cb 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor float distance = diff.Length; Vector2 direction = diff / distance; - float interval = partSize.X / 2.5f / cursorSize.Value; + float interval = partSize.X / 2.5f / Math.Max(cursorSize.Value, 1); for (float d = interval; d < distance; d += interval) { From fa1903cd03c4e5f4efa2e796379ee041f1772ac8 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 1 Oct 2020 23:41:24 +0300 Subject: [PATCH 3569/6909] Get bound copy instead --- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index 8a1dc9b8cb..fb8a850223 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private void load(ShaderManager shaders, OsuConfigManager config) { shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); - cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize); + cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); } protected override void LoadComplete() From 50722cc754f8cfc20042c11e2a27da2ffb5cdd7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 14:48:56 +0900 Subject: [PATCH 3570/6909] Update slider test scene sliders to fit better --- .../TestSceneSlider.cs | 74 ++++++++++--------- 1 file changed, 39 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index 6a689a1f80..c79cae2fe5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - StartTime = Time.Current + 1000, + StartTime = Time.Current + time_offset, Position = new Vector2(239, 176), Path = new SliderPath(PathType.PerfectCurve, new[] { @@ -185,22 +185,26 @@ namespace osu.Game.Rulesets.Osu.Tests private Drawable testSlowSpeed() => createSlider(speedMultiplier: 0.5); - private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 0.5); + private Drawable testShortSlowSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 0.5); private Drawable testHighSpeed(int repeats = 0) => createSlider(repeats: repeats, speedMultiplier: 15); - private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: 100, repeats: repeats, speedMultiplier: 15); + private Drawable testShortHighSpeed(int repeats = 0) => createSlider(distance: max_length / 4, repeats: repeats, speedMultiplier: 15); - private Drawable createSlider(float circleSize = 2, float distance = 400, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0) + private const double time_offset = 1500; + + private const float max_length = 200; + + private Drawable createSlider(float circleSize = 2, float distance = max_length, int repeats = 0, double speedMultiplier = 2, int stackHeight = 0) { var slider = new Slider { - StartTime = Time.Current + 1000, - Position = new Vector2(-(distance / 2), 0), + StartTime = Time.Current + time_offset, + Position = new Vector2(0, -(distance / 2)), Path = new SliderPath(PathType.PerfectCurve, new[] { Vector2.Zero, - new Vector2(distance, 0), + new Vector2(0, distance), }, distance), RepeatCount = repeats, StackHeight = stackHeight @@ -213,14 +217,14 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - StartTime = Time.Current + 1000, - Position = new Vector2(-200, 0), + StartTime = Time.Current + time_offset, + Position = new Vector2(-max_length / 2, 0), Path = new SliderPath(PathType.PerfectCurve, new[] { Vector2.Zero, - new Vector2(200, 200), - new Vector2(400, 0) - }, 600), + new Vector2(max_length / 2, max_length / 2), + new Vector2(max_length, 0) + }, max_length * 1.5f), RepeatCount = repeats, }; @@ -233,16 +237,16 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - StartTime = Time.Current + 1000, - Position = new Vector2(-200, 0), + StartTime = Time.Current + time_offset, + Position = new Vector2(-max_length / 2, 0), Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, - new Vector2(150, 75), - new Vector2(200, 0), - new Vector2(300, -200), - new Vector2(400, 0), - new Vector2(430, 0) + new Vector2(max_length * 0.375f, max_length * 0.18f), + new Vector2(max_length / 2, 0), + new Vector2(max_length * 0.75f, -max_length / 2), + new Vector2(max_length * 0.95f, 0), + new Vector2(max_length, 0) }), RepeatCount = repeats, }; @@ -256,15 +260,15 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - StartTime = Time.Current + 1000, - Position = new Vector2(-200, 0), + StartTime = Time.Current + time_offset, + Position = new Vector2(-max_length / 2, 0), Path = new SliderPath(PathType.Bezier, new[] { Vector2.Zero, - new Vector2(150, 75), - new Vector2(200, 100), - new Vector2(300, -200), - new Vector2(430, 0) + new Vector2(max_length * 0.375f, max_length * 0.18f), + new Vector2(max_length / 2, max_length / 4), + new Vector2(max_length * 0.75f, -max_length / 2), + new Vector2(max_length, 0) }), RepeatCount = repeats, }; @@ -278,16 +282,16 @@ namespace osu.Game.Rulesets.Osu.Tests { var slider = new Slider { - StartTime = Time.Current + 1000, + StartTime = Time.Current + time_offset, Position = new Vector2(0, 0), Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, - new Vector2(-200, 0), + new Vector2(-max_length / 2, 0), new Vector2(0, 0), - new Vector2(0, -200), - new Vector2(-200, -200), - new Vector2(0, -200) + new Vector2(0, -max_length / 2), + new Vector2(-max_length / 2, -max_length / 2), + new Vector2(0, -max_length / 2) }), RepeatCount = repeats, }; @@ -305,14 +309,14 @@ namespace osu.Game.Rulesets.Osu.Tests var slider = new Slider { - StartTime = Time.Current + 1000, - Position = new Vector2(-100, 0), + StartTime = Time.Current + time_offset, + Position = new Vector2(-max_length / 4, 0), Path = new SliderPath(PathType.Catmull, new[] { Vector2.Zero, - new Vector2(50, -50), - new Vector2(150, 50), - new Vector2(200, 0) + new Vector2(max_length * 0.125f, max_length * 0.125f), + new Vector2(max_length * 0.375f, max_length * 0.125f), + new Vector2(max_length / 2, 0) }), RepeatCount = repeats, NodeSamples = repeatSamples From 78bf58f4f8c469f4dce04f7b3db7c31174a04cd3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 13:38:13 +0900 Subject: [PATCH 3571/6909] Add metrics skin elements for sliderendcircle --- .../metrics-skin/sliderendcircle@2x.png | Bin 0 -> 18105 bytes .../metrics-skin/sliderendcircleoverlay@2x.png | Bin 0 -> 45734 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png create mode 100644 osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircleoverlay@2x.png diff --git a/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png b/osu.Game.Rulesets.Osu.Tests/Resources/metrics-skin/sliderendcircle@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..c6c3771593701a825f5cf61c8e05be66bd30699f GIT binary patch literal 18105 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 literal 0 HcmV?d00001 From fce3eacd7de3254ce75619efaa2d15d59d564623 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 13:38:48 +0900 Subject: [PATCH 3572/6909] Move tail circle to display beneath ticks etc. --- 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 9abcef83c4..e77bca1e20 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -52,6 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables InternalChildren = new Drawable[] { Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), + tailContainer = new Container { RelativeSizeAxes = Axes.Both }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, Ball = new SliderBall(s, this) @@ -63,7 +64,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Alpha = 0 }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, - tailContainer = new Container { RelativeSizeAxes = Axes.Both }, }; } From fc7f3173e19aa8d47ebba85490425eb9e434407c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 13:40:24 +0900 Subject: [PATCH 3573/6909] Add the ability to use LegacyMainCirclePiece with no combo number displayed --- .../Skinning/LegacyMainCirclePiece.cs | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index d15a0a3203..f051cbfa3b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -21,10 +21,12 @@ namespace osu.Game.Rulesets.Osu.Skinning public class LegacyMainCirclePiece : CompositeDrawable { private readonly string priorityLookup; + private readonly bool hasNumber; - public LegacyMainCirclePiece(string priorityLookup = null) + public LegacyMainCirclePiece(string priorityLookup = null, bool hasNumber = true) { this.priorityLookup = priorityLookup; + this.hasNumber = hasNumber; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); } @@ -70,7 +72,11 @@ namespace osu.Game.Rulesets.Osu.Skinning } } }, - hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText + }; + + if (hasNumber) + { + AddInternal(hitCircleText = new SkinnableSpriteText(new OsuSkinComponent(OsuSkinComponents.HitCircleText), _ => new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, @@ -78,8 +84,8 @@ namespace osu.Game.Rulesets.Osu.Skinning { Anchor = Anchor.Centre, Origin = Anchor.Centre, - }, - }; + }); + } bool overlayAboveNumber = skin.GetConfig(OsuSkinConfiguration.HitCircleOverlayAboveNumber)?.Value ?? true; @@ -107,7 +113,8 @@ namespace osu.Game.Rulesets.Osu.Skinning state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); - indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); + if (hasNumber) + indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); } private void updateState(ValueChangedEvent state) @@ -120,16 +127,19 @@ namespace osu.Game.Rulesets.Osu.Skinning circleSprites.FadeOut(legacy_fade_duration, Easing.Out); circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; - - if (legacyVersion >= 2.0m) - // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. - hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); - else + if (hasNumber) { - // old skins scale and fade it normally along other pieces. - hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); - hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; + + if (legacyVersion >= 2.0m) + // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. + hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); + else + { + // old skins scale and fade it normally along other pieces. + hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); + hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + } } break; From 5d2a8ec7640fff9ff189f9adca1e9e5c381d29c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 13:41:22 +0900 Subject: [PATCH 3574/6909] Add final sliderendcircle display support --- .../Objects/Drawables/DrawableSliderRepeat.cs | 25 ++++-- .../Objects/Drawables/DrawableSliderTail.cs | 79 ++++++++++++++++--- .../{SliderCircle.cs => SliderEndCircle.cs} | 2 +- osu.Game.Rulesets.Osu/OsuSkinComponents.cs | 1 + .../Skinning/OsuLegacySkinTransformer.cs | 6 ++ 5 files changed, 94 insertions(+), 19 deletions(-) rename osu.Game.Rulesets.Osu/Objects/{SliderCircle.cs => SliderEndCircle.cs} (82%) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index f65077685f..9d775de7df 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -6,9 +6,11 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -34,7 +36,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre; - InternalChild = scaleContainer = new ReverseArrowPiece(); + InternalChild = scaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + // no default for this; only visible in legacy skins. + new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()), + arrow = new ReverseArrowPiece(), + } + }; } private readonly IBindable scaleBindable = new BindableFloat(); @@ -85,6 +98,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private bool hasRotation; + private readonly ReverseArrowPiece arrow; + public void UpdateSnakingPosition(Vector2 start, Vector2 end) { // When the repeat is hit, the arrow should fade out on spot rather than following the slider @@ -114,18 +129,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } float aimRotation = MathUtils.RadiansToDegrees(MathF.Atan2(aimRotationVector.Y - Position.Y, aimRotationVector.X - Position.X)); - while (Math.Abs(aimRotation - Rotation) > 180) - aimRotation += aimRotation < Rotation ? 360 : -360; + while (Math.Abs(aimRotation - arrow.Rotation) > 180) + aimRotation += aimRotation < arrow.Rotation ? 360 : -360; if (!hasRotation) { - Rotation = aimRotation; + arrow.Rotation = aimRotation; hasRotation = true; } else { // If we're already snaking, interpolate to smooth out sharp curves (linear sliders, mainly). - Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), Rotation, aimRotation, 0, 50, Easing.OutQuint); + arrow.Rotation = Interpolation.ValueAt(Math.Clamp(Clock.ElapsedFrameTime, 0, 100), arrow.Rotation, aimRotation, 0, 50, Easing.OutQuint); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 0939e2847a..3751ff0975 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -1,13 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Diagnostics; +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.Skinning; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking + public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking { private readonly Slider slider; @@ -18,28 +23,73 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool Tracking { get; set; } - private readonly IBindable positionBindable = new Bindable(); - private readonly IBindable pathVersion = new Bindable(); + private readonly IBindable scaleBindable = new BindableFloat(); + + private readonly SkinnableDrawable circlePiece; + + private readonly Container scaleContainer; public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle) : base(hitCircle) { this.slider = slider; - Origin = Anchor.Centre; - RelativeSizeAxes = Axes.Both; - FillMode = FillMode.Fit; + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); - AlwaysPresent = true; + InternalChildren = new Drawable[] + { + scaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + Children = new Drawable[] + { + // no default for this; only visible in legacy skins. + circlePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()) + } + }, + }; + } - positionBindable.BindTo(hitCircle.PositionBindable); - pathVersion.BindTo(slider.Path.Version); + [BackgroundDependencyLoader] + private void load() + { + scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + scaleBindable.BindTo(HitObject.ScaleBindable); + } - positionBindable.BindValueChanged(_ => updatePosition()); - pathVersion.BindValueChanged(_ => updatePosition(), true); + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); - // TODO: This has no drawable content. Support for skins should be added. + circlePiece.FadeInFromZero(HitObject.TimeFadeIn); + } + + protected override void UpdateStateTransforms(ArmedState state) + { + base.UpdateStateTransforms(state); + + Debug.Assert(HitObject.HitWindows != null); + + switch (state) + { + case ArmedState.Idle: + this.Delay(HitObject.TimePreempt).FadeOut(500); + + Expire(true); + break; + + case ArmedState.Miss: + this.FadeOut(100); + break; + + case ArmedState.Hit: + // todo: temporary / arbitrary + this.Delay(800).FadeOut(); + break; + } } protected override void CheckForResult(bool userTriggered, double timeOffset) @@ -48,6 +98,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); } - private void updatePosition() => Position = HitObject.Position - slider.Position; + public void UpdateSnakingPosition(Vector2 start, Vector2 end) + { + Position = end; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs similarity index 82% rename from osu.Game.Rulesets.Osu/Objects/SliderCircle.cs rename to osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index 151902a752..d9ae520f5c 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -3,7 +3,7 @@ namespace osu.Game.Rulesets.Osu.Objects { - public class SliderCircle : HitCircle + public class SliderEndCircle : HitCircle { } } diff --git a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs index 5468764692..2883f0c187 100644 --- a/osu.Game.Rulesets.Osu/OsuSkinComponents.cs +++ b/osu.Game.Rulesets.Osu/OsuSkinComponents.cs @@ -14,6 +14,7 @@ namespace osu.Game.Rulesets.Osu ReverseArrow, HitCircleText, SliderHeadHitCircle, + SliderTailHitCircle, SliderFollowCircle, SliderBall, SliderBody, diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 851a8d56c9..78bc26eff7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -66,6 +66,12 @@ namespace osu.Game.Rulesets.Osu.Skinning return null; + case OsuSkinComponents.SliderTailHitCircle: + if (hasHitCircle.Value) + return new LegacyMainCirclePiece("sliderendcircle", false); + + return null; + case OsuSkinComponents.SliderHeadHitCircle: if (hasHitCircle.Value) return new LegacyMainCirclePiece("sliderstartcircle"); From 2427ae43da2284d31e5c2b26662f6df93c0739ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 14:20:55 +0900 Subject: [PATCH 3575/6909] Share fade in logic with repeats --- .../Objects/Drawables/DrawableSliderTail.cs | 14 +++++----- osu.Game.Rulesets.Osu/Objects/Slider.cs | 4 ++- .../Objects/SliderEndCircle.cs | 27 ++++++++++++++++++- osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs | 23 +--------------- .../Objects/SliderTailCircle.cs | 13 +-------- 5 files changed, 37 insertions(+), 44 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 3751ff0975..f5bcecccdf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking { - private readonly Slider slider; + private readonly SliderTailCircle tailCircle; ///

    /// The judgement text is provided by the . @@ -29,10 +29,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Container scaleContainer; - public DrawableSliderTail(Slider slider, SliderTailCircle hitCircle) - : base(hitCircle) + public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle) + : base(tailCircle) { - this.slider = slider; + this.tailCircle = tailCircle; Origin = Anchor.Centre; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); @@ -98,9 +98,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ApplyResult(r => r.Type = Tracking ? r.Judgement.MaxResult : r.Judgement.MinResult); } - public void UpdateSnakingPosition(Vector2 start, Vector2 end) - { - Position = end; - } + public void UpdateSnakingPosition(Vector2 start, Vector2 end) => + Position = tailCircle.RepeatIndex % 2 == 0 ? end : start; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 51f6a44a87..9cc3f17c55 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -174,8 +174,10 @@ namespace osu.Game.Rulesets.Osu.Objects // we need to use the LegacyLastTick here for compatibility reasons (difficulty). // it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay. // if this is to change, we should revisit this. - AddNested(TailCircle = new SliderTailCircle(this) + AddNested(TailCircle = new SliderTailCircle { + RepeatIndex = e.SpanIndex, + SpanDuration = SpanDuration, StartTime = e.Time, Position = EndPosition, StackHeight = StackHeight diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index d9ae520f5c..a34eec0c79 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -1,9 +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.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Scoring; + namespace osu.Game.Rulesets.Osu.Objects { - public class SliderEndCircle : HitCircle + /// + /// A hitcircle which is at the end of a slider path (either repeat or final tail). + /// + public abstract class SliderEndCircle : HitCircle { + public int RepeatIndex { get; set; } + public double SpanDuration { get; set; } + + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + + // Out preempt should be one span early to give the user ample warning. + TimePreempt += SpanDuration; + + // We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders + // we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time. + if (RepeatIndex > 0) + TimePreempt = Math.Min(SpanDuration * 2, TimePreempt); + } + + protected override HitWindows CreateHitWindows() => HitWindows.Empty; } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index b6c58a75d1..6bf0ec0355 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -1,35 +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 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 { - public class SliderRepeat : OsuHitObject + public class SliderRepeat : SliderEndCircle { - public int RepeatIndex { get; set; } - public double SpanDuration { get; set; } - - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) - { - base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - - // Out preempt should be one span early to give the user ample warning. - TimePreempt += SpanDuration; - - // We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders - // we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time. - if (RepeatIndex > 0) - TimePreempt = Math.Min(SpanDuration * 2, TimePreempt); - } - - protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public override Judgement CreateJudgement() => new SliderRepeatJudgement(); public class SliderRepeatJudgement : OsuJudgement diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index aff3f38e17..2f1bfdfcc0 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Judgements; @@ -13,18 +12,8 @@ namespace osu.Game.Rulesets.Osu.Objects /// Note that this should not be used for timing correctness. /// See usage in for more information. /// - public class SliderTailCircle : SliderCircle + public class SliderTailCircle : SliderEndCircle { - private readonly IBindable pathVersion = new Bindable(); - - public SliderTailCircle(Slider slider) - { - pathVersion.BindTo(slider.Path.Version); - pathVersion.BindValueChanged(_ => Position = slider.EndPosition); - } - - protected override HitWindows CreateHitWindows() => HitWindows.Empty; - public override Judgement CreateJudgement() => new SliderTailJudgement(); public class SliderTailJudgement : OsuJudgement From 2975ea9210a7e329a2ae35dfb7f0ef57a283fd74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 14:37:07 +0900 Subject: [PATCH 3576/6909] Adjust repeat/tail fade in to match stable closer --- osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index a34eec0c79..e0bbac67fc 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.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.Beatmaps.ControlPoints; using osu.Game.Rulesets.Scoring; @@ -20,13 +19,14 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - // Out preempt should be one span early to give the user ample warning. - TimePreempt += SpanDuration; - - // We want to show the first RepeatPoint as the TimePreempt dictates but on short (and possibly fast) sliders - // we may need to cut down this time on following RepeatPoints to only show up to two RepeatPoints at any given time. if (RepeatIndex > 0) - TimePreempt = Math.Min(SpanDuration * 2, TimePreempt); + { + // Repeat points after the first span should appear behind the still-visible one. + TimeFadeIn = 0; + + // The next end circle should appear exactly after the previous circle (on the same end) is hit. + TimePreempt = SpanDuration * 2; + } } protected override HitWindows CreateHitWindows() => HitWindows.Empty; From ad4cac13acccaa6a30b81470afa0a4a74f5a166c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 15:21:52 +0900 Subject: [PATCH 3577/6909] Add preempt adjustment and fade in first end circle with slider to match stable --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 6 ++---- .../Objects/SliderEndCircle.cs | 20 +++++++++++++++++-- osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs | 5 +++++ .../Objects/SliderTailCircle.cs | 5 +++++ 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 9cc3f17c55..917382eccf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -174,10 +174,9 @@ namespace osu.Game.Rulesets.Osu.Objects // we need to use the LegacyLastTick here for compatibility reasons (difficulty). // it is *okay* to use this because the TailCircle is not used for any meaningful purpose in gameplay. // if this is to change, we should revisit this. - AddNested(TailCircle = new SliderTailCircle + AddNested(TailCircle = new SliderTailCircle(this) { RepeatIndex = e.SpanIndex, - SpanDuration = SpanDuration, StartTime = e.Time, Position = EndPosition, StackHeight = StackHeight @@ -185,10 +184,9 @@ namespace osu.Game.Rulesets.Osu.Objects break; case SliderEventType.Repeat: - AddNested(new SliderRepeat + AddNested(new SliderRepeat(this) { RepeatIndex = e.SpanIndex, - SpanDuration = SpanDuration, StartTime = StartTime + (e.SpanIndex + 1) * SpanDuration, Position = Position + Path.PositionAt(e.PathProgress), StackHeight = StackHeight, diff --git a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs index e0bbac67fc..a6aed2c00e 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderEndCircle.cs @@ -8,12 +8,20 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects { /// - /// A hitcircle which is at the end of a slider path (either repeat or final tail). + /// A hit circle which is at the end of a slider path (either repeat or final tail). /// public abstract class SliderEndCircle : HitCircle { + private readonly Slider slider; + + protected SliderEndCircle(Slider slider) + { + this.slider = slider; + } + public int RepeatIndex { get; set; } - public double SpanDuration { get; set; } + + public double SpanDuration => slider.SpanDuration; protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { @@ -27,6 +35,14 @@ namespace osu.Game.Rulesets.Osu.Objects // The next end circle should appear exactly after the previous circle (on the same end) is hit. TimePreempt = SpanDuration * 2; } + else + { + // taken from osu-stable + const float first_end_circle_preempt_adjust = 2 / 3f; + + // The first end circle should fade in with the slider. + TimePreempt = (StartTime - slider.StartTime) + slider.TimePreempt * first_end_circle_preempt_adjust; + } } protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs index 6bf0ec0355..cca86361c2 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderRepeat.cs @@ -9,6 +9,11 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SliderRepeat : SliderEndCircle { + public SliderRepeat(Slider slider) + : base(slider) + { + } + public override Judgement CreateJudgement() => new SliderRepeatJudgement(); public class SliderRepeatJudgement : OsuJudgement diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index 2f1bfdfcc0..5aa2940e10 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -14,6 +14,11 @@ namespace osu.Game.Rulesets.Osu.Objects ///
    public class SliderTailCircle : SliderEndCircle { + public SliderTailCircle(Slider slider) + : base(slider) + { + } + public override Judgement CreateJudgement() => new SliderTailJudgement(); public class SliderTailJudgement : OsuJudgement From d6fe5482d30fe8f5197d18145a47ec8a2644dcca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 15:28:08 +0900 Subject: [PATCH 3578/6909] Add failing test showing missing control point removal --- osu.Game.Tests/NonVisual/ControlPointInfoTest.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index 830e4bc603..90a487c0ac 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -139,6 +139,22 @@ namespace osu.Game.Tests.NonVisual Assert.That(cpi.Groups.Count, Is.EqualTo(0)); } + [Test] + public void TestRemoveGroupAlsoRemovedControlPoints() + { + var cpi = new ControlPointInfo(); + + var group = cpi.GroupAt(1000, true); + + group.Add(new SampleControlPoint()); + + Assert.That(cpi.SamplePoints.Count, Is.EqualTo(1)); + + cpi.RemoveGroup(group); + + Assert.That(cpi.SamplePoints.Count, Is.EqualTo(0)); + } + [Test] public void TestAddControlPointToGroup() { From f501c88b46f09b6bbcda0ddcc520151a882bdc64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 15:25:35 +0900 Subject: [PATCH 3579/6909] Fix individual control points not being removed from group when group is removed --- osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index e7788b75f3..22314f28c7 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -158,6 +158,9 @@ namespace osu.Game.Beatmaps.ControlPoints public void RemoveGroup(ControlPointGroup group) { + foreach (var item in group.ControlPoints.ToArray()) + group.Remove(item); + group.ItemAdded -= groupItemAdded; group.ItemRemoved -= groupItemRemoved; From 959c8730f6c673d1f4ac3970ca43426b4ac97e13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 15:25:48 +0900 Subject: [PATCH 3580/6909] Add settings section from TimingPointGroups on timing screen --- .../Edit/Timing/ControlPointSettings.cs | 1 + osu.Game/Screens/Edit/Timing/GroupSection.cs | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 osu.Game/Screens/Edit/Timing/GroupSection.cs diff --git a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs index e1182d9fa4..c40061b97c 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointSettings.cs @@ -41,6 +41,7 @@ namespace osu.Game.Screens.Edit.Timing private IReadOnlyList createSections() => new Drawable[] { + new GroupSection(), new TimingSection(), new DifficultySection(), new SampleSection(), diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs new file mode 100644 index 0000000000..2c3c393e3c --- /dev/null +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; + +namespace osu.Game.Screens.Edit.Timing +{ + internal class GroupSection : CompositeDrawable + { + private LabelledTextBox textBox; + + [Resolved] + protected Bindable SelectedGroup { get; private set; } + + [Resolved] + protected IBindable Beatmap { get; private set; } + + [Resolved] + private EditorClock clock { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Padding = new MarginPadding(10); + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + textBox = new LabelledTextBox + { + Label = "Time" + }, + new TriangleButton + { + Text = "Use current time", + RelativeSizeAxes = Axes.X, + Action = () => changeSelectedGroupTime(clock.CurrentTime) + } + } + }, + }; + + textBox.OnCommit += (sender, isNew) => + { + if (double.TryParse(sender.Text, out var newTime)) + { + changeSelectedGroupTime(newTime); + } + }; + + SelectedGroup.BindValueChanged(group => + { + if (group.NewValue == null) + { + textBox.Text = string.Empty; + textBox.Current.Disabled = true; + return; + } + + textBox.Current.Disabled = false; + textBox.Text = $"{group.NewValue.Time:n0}"; + }, true); + } + + private void changeSelectedGroupTime(in double time) + { + var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray(); + + Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value); + + foreach (var cp in currentGroupItems) + Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp); + + SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time); + } + } +} From 2698dc513f31d4d2a0a0f75b1615c50cdc106800 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 15:33:33 +0900 Subject: [PATCH 3581/6909] Add basic textbox error handling --- osu.Game/Screens/Edit/Timing/GroupSection.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index 2c3c393e3c..ac9c4be97a 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -61,10 +61,17 @@ namespace osu.Game.Screens.Edit.Timing textBox.OnCommit += (sender, isNew) => { + if (!isNew) + return; + if (double.TryParse(sender.Text, out var newTime)) { changeSelectedGroupTime(newTime); } + else + { + SelectedGroup.TriggerChange(); + } }; SelectedGroup.BindValueChanged(group => From 0cb3926e1d090b7c336b16a5512f31f696d18661 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 15:44:32 +0900 Subject: [PATCH 3582/6909] Add event on EditorChangeHandler state change --- .../Editing/EditorChangeHandlerTest.cs | 22 ++++++++++++++++++- osu.Game/Screens/Edit/EditorChangeHandler.cs | 5 +++++ osu.Game/Screens/Edit/IEditorChangeHandler.cs | 6 +++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index ff2c9fb1a9..b7a41ffd1c 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -12,6 +12,14 @@ namespace osu.Game.Tests.Editing [TestFixture] public class EditorChangeHandlerTest { + private int stateChangedFired; + + [SetUp] + public void SetUp() + { + stateChangedFired = 0; + } + [Test] public void TestSaveRestoreState() { @@ -23,6 +31,8 @@ namespace osu.Game.Tests.Editing addArbitraryChange(beatmap); handler.SaveState(); + Assert.That(stateChangedFired, Is.EqualTo(1)); + Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanRedo.Value, Is.False); @@ -30,6 +40,8 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.True); + + Assert.That(stateChangedFired, Is.EqualTo(2)); } [Test] @@ -45,6 +57,7 @@ namespace osu.Game.Tests.Editing Assert.That(handler.CanUndo.Value, Is.True); Assert.That(handler.CanRedo.Value, Is.False); + Assert.That(stateChangedFired, Is.EqualTo(1)); string hash = handler.CurrentStateHash; @@ -52,6 +65,7 @@ namespace osu.Game.Tests.Editing handler.SaveState(); Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); + Assert.That(stateChangedFired, Is.EqualTo(1)); handler.RestoreState(-1); @@ -60,6 +74,7 @@ namespace osu.Game.Tests.Editing // we should only be able to restore once even though we saved twice. Assert.That(handler.CanUndo.Value, Is.False); Assert.That(handler.CanRedo.Value, Is.True); + Assert.That(stateChangedFired, Is.EqualTo(2)); } [Test] @@ -71,6 +86,8 @@ namespace osu.Game.Tests.Editing for (int i = 0; i < EditorChangeHandler.MAX_SAVED_STATES; i++) { + Assert.That(stateChangedFired, Is.EqualTo(i)); + addArbitraryChange(beatmap); handler.SaveState(); } @@ -114,7 +131,10 @@ namespace osu.Game.Tests.Editing { var beatmap = new EditorBeatmap(new Beatmap()); - return (new EditorChangeHandler(beatmap), beatmap); + var changeHandler = new EditorChangeHandler(beatmap); + + changeHandler.OnStateChange += () => stateChangedFired++; + return (changeHandler, beatmap); } private void addArbitraryChange(EditorBeatmap beatmap) diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 617c436ee0..616d0608c0 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -21,6 +21,8 @@ namespace osu.Game.Screens.Edit public readonly Bindable CanUndo = new Bindable(); public readonly Bindable CanRedo = new Bindable(); + public event Action OnStateChange; + private readonly LegacyEditorBeatmapPatcher patcher; private readonly List savedStates = new List(); @@ -109,6 +111,8 @@ namespace osu.Game.Screens.Edit savedStates.Add(newState); currentState = savedStates.Count - 1; + + OnStateChange?.Invoke(); updateBindables(); } } @@ -136,6 +140,7 @@ namespace osu.Game.Screens.Edit isRestoring = false; + OnStateChange?.Invoke(); updateBindables(); } diff --git a/osu.Game/Screens/Edit/IEditorChangeHandler.cs b/osu.Game/Screens/Edit/IEditorChangeHandler.cs index c1328252d4..a23a956e14 100644 --- a/osu.Game/Screens/Edit/IEditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/IEditorChangeHandler.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; namespace osu.Game.Screens.Edit @@ -10,6 +11,11 @@ namespace osu.Game.Screens.Edit ///
    public interface IEditorChangeHandler { + /// + /// Fired whenever a state change occurs. + /// + public event Action OnStateChange; + /// /// Begins a bulk state change event. should be invoked soon after. /// From 501e02db097eab45553f0376caf420457b6cbb2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 15:44:37 +0900 Subject: [PATCH 3583/6909] Only regenerate autoplay on editor state change --- osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 1070b8cbd2..d259a89055 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -40,17 +40,21 @@ namespace osu.Game.Rulesets.Edit Playfield.DisplayJudgements.Value = false; } + [Resolved] + private IEditorChangeHandler changeHandler { get; set; } + protected override void LoadComplete() { base.LoadComplete(); beatmap.HitObjectAdded += addHitObject; - beatmap.HitObjectUpdated += updateReplay; beatmap.HitObjectRemoved += removeHitObject; + + // for now only regenerate replay on a finalised state change, not HitObjectUpdated. + changeHandler.OnStateChange += updateReplay; } - private void updateReplay(HitObject obj = null) => - drawableRuleset.RegenerateAutoplay(); + private void updateReplay() => drawableRuleset.RegenerateAutoplay(); private void addHitObject(HitObject hitObject) { @@ -69,7 +73,7 @@ namespace osu.Game.Rulesets.Edit drawableRuleset.Playfield.Remove(drawableObject); drawableRuleset.Playfield.PostProcess(); - drawableRuleset.RegenerateAutoplay(); + updateReplay(); } public override bool PropagatePositionalInputSubTree => false; From dde7f706aafae01330c084719124c504b1abc0ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 01:48:41 +0900 Subject: [PATCH 3584/6909] Avoid rapid triangle repositioning during editor slider placement --- .../Objects/Drawables/Pieces/CirclePiece.cs | 5 +++-- .../Drawables/Pieces/TrianglesPiece.cs | 3 ++- osu.Game/Graphics/Backgrounds/Triangles.cs | 21 ++++++++++++++----- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index aab01f45d4..e95cdc7ee3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Rulesets.Objects.Drawables; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces @@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(TextureStore textures, DrawableHitObject drawableHitObject) { InternalChildren = new Drawable[] { @@ -35,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Origin = Anchor.Centre, Texture = textures.Get(@"Gameplay/osu/disc"), }, - new TrianglesPiece + new TrianglesPiece((int)drawableHitObject.HitObject.StartTime) { RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs index 0e29a1dcd8..6cdb0d3df3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs @@ -11,7 +11,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces protected override bool CreateNewTriangles => false; protected override float SpawnRatio => 0.5f; - public TrianglesPiece() + public TrianglesPiece(int? seed = null) + : base(seed) { TriangleScale = 1.2f; HideAlphaDiscrepancies = false; diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 27027202ce..5b0fa44444 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -86,13 +86,24 @@ namespace osu.Game.Graphics.Backgrounds ///
    public float Velocity = 1; + private readonly Random stableRandom; + + private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle()); + private readonly SortedList parts = new SortedList(Comparer.Default); private IShader shader; private readonly Texture texture; - public Triangles() + /// + /// Construct a new triangle visualisation. + /// + /// An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time. + public Triangles(int? seed = null) { + if (seed != null) + stableRandom = new Random(seed.Value); + texture = Texture.WhitePixel; } @@ -175,8 +186,8 @@ namespace osu.Game.Graphics.Backgrounds { TriangleParticle particle = CreateTriangle(); - particle.Position = new Vector2(RNG.NextSingle(), randomY ? RNG.NextSingle() : 1); - particle.ColourShade = RNG.NextSingle(); + particle.Position = new Vector2(nextRandom(), randomY ? nextRandom() : 1); + particle.ColourShade = nextRandom(); particle.Colour = CreateTriangleShade(particle.ColourShade); return particle; @@ -191,8 +202,8 @@ namespace osu.Game.Graphics.Backgrounds const float std_dev = 0.16f; const float mean = 0.5f; - float u1 = 1 - RNG.NextSingle(); //uniform(0,1] random floats - float u2 = 1 - RNG.NextSingle(); + float u1 = 1 - nextRandom(); //uniform(0,1] random floats + float u2 = 1 - nextRandom(); 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) From e49ec092c9a35f2aa414102153829aa4ea221402 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 16:08:11 +0900 Subject: [PATCH 3585/6909] Expose ability to register a component as an import handler --- osu.Game/OsuGameBase.cs | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index b1269e9300..11c1f6c5cf 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -232,9 +232,9 @@ namespace osu.Game dependencies.Cache(new SessionStatics()); dependencies.Cache(new OsuColour()); - fileImporters.Add(BeatmapManager); - fileImporters.Add(ScoreManager); - fileImporters.Add(SkinManager); + RegisterImportHandler(BeatmapManager); + RegisterImportHandler(ScoreManager); + RegisterImportHandler(SkinManager); // tracks play so loud our samples can't keep up. // this adds a global reduction of track volume for the time being. @@ -341,7 +341,19 @@ namespace osu.Game protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage); - private readonly List fileImporters = new List(); + private readonly HashSet fileImporters = new HashSet(); + + /// + /// Register a global handler for file imports. + /// + /// The handler to register. + public void RegisterImportHandler(ICanAcceptFiles handler) => fileImporters.Add(handler); + + /// + /// Unregister a global handler for file imports. + /// + /// The previously registered handler. + public void UnregisterImportHandler(ICanAcceptFiles handler) => fileImporters.Remove(handler); public async Task Import(params string[] paths) { From fc65cb43759477e96d48337513a1e9565b10082d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 16:14:21 +0900 Subject: [PATCH 3586/6909] Ensure precedence is given to newer registered handlers --- osu.Game/OsuGameBase.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 11c1f6c5cf..dfda0d0118 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -341,13 +341,13 @@ namespace osu.Game protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage); - private readonly HashSet fileImporters = new HashSet(); + private readonly List fileImporters = new List(); /// - /// Register a global handler for file imports. + /// Register a global handler for file imports. Most recently registered will have precedence. /// /// The handler to register. - public void RegisterImportHandler(ICanAcceptFiles handler) => fileImporters.Add(handler); + public void RegisterImportHandler(ICanAcceptFiles handler) => fileImporters.Insert(0, handler); /// /// Unregister a global handler for file imports. From f3c8cd91f4e87c461b2cee362a84fd096f838d05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 16:14:27 +0900 Subject: [PATCH 3587/6909] Remove unused method --- osu.Game/Screens/Edit/EditorScreen.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index 52bffc4342..4d62a7d3cd 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -44,10 +44,5 @@ namespace osu.Game.Screens.Edit .Then() .FadeTo(1f, 250, Easing.OutQuint); } - - public void Exit() - { - Expire(); - } } } From 50eca202f48a08bbeb0ac5c8867a81507a4a2881 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 16:17:10 +0900 Subject: [PATCH 3588/6909] User IEnumerable for HandledExtensions --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 2 +- osu.Game/Database/ICanAcceptFiles.cs | 3 ++- osu.Game/OsuGameBase.cs | 2 +- osu.Game/Scoring/ScoreManager.cs | 2 +- osu.Game/Skinning/SkinManager.cs | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index b48ab6112e..4c75069f08 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps /// public readonly WorkingBeatmap DefaultBeatmap; - public override string[] HandledExtensions => new[] { ".osz" }; + public override IEnumerable HandledExtensions => new[] { ".osz" }; protected override string[] HashableFileTypes => new[] { ".osu" }; diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index bbe2604216..3292936f5f 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -70,7 +70,7 @@ namespace osu.Game.Database private readonly Bindable> itemRemoved = new Bindable>(); - public virtual string[] HandledExtensions => new[] { ".zip" }; + public virtual IEnumerable HandledExtensions => new[] { ".zip" }; public virtual bool SupportsImportFromStable => RuntimeInfo.IsDesktop; diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index b9f882468d..e4d92d957c 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.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.Threading.Tasks; namespace osu.Game.Database @@ -19,6 +20,6 @@ namespace osu.Game.Database /// /// An array of accepted file extensions (in the standard format of ".abc"). /// - string[] HandledExtensions { get; } + IEnumerable HandledExtensions { get; } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index dfda0d0118..f61ff43ca9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -366,7 +366,7 @@ namespace osu.Game } } - public string[] HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions).ToArray(); + public IEnumerable HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 8e8147ff39..5a6da53839 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Scoring { public class ScoreManager : DownloadableArchiveModelManager { - public override string[] HandledExtensions => new[] { ".osr" }; + public override IEnumerable HandledExtensions => new[] { ".osr" }; protected override string[] HashableFileTypes => new[] { ".osr" }; diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index ee4b7bc8e7..7af400e807 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -33,7 +33,7 @@ namespace osu.Game.Skinning public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin()); public readonly Bindable CurrentSkinInfo = new Bindable(SkinInfo.Default) { Default = SkinInfo.Default }; - public override string[] HandledExtensions => new[] { ".osk" }; + public override IEnumerable HandledExtensions => new[] { ".osk" }; protected override string[] HashableFileTypes => new[] { ".ini" }; From fe818a020a52896aa082de261d4dee9d2ec6a37e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 2 Oct 2020 16:17:57 +0900 Subject: [PATCH 3589/6909] Fix spinners not transforming correctly --- .../Drawables/Pieces/DefaultSpinnerDisc.cs | 74 +++++++++++-------- .../Skinning/LegacyNewStyleSpinner.cs | 15 ++-- .../Skinning/LegacyOldStyleSpinner.cs | 5 +- 3 files changed, 57 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 587bd415ee..e855317544 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -20,6 +20,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private Spinner spinner; + private const float initial_scale = 1.3f; private const float idle_alpha = 0.2f; private const float tracking_alpha = 0.4f; @@ -41,7 +42,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces // we are slightly bigger than our parent, to clip the top and bottom of the circle // this should probably be revisited when scaled spinners are a thing. - Scale = new Vector2(1.3f); + Scale = new Vector2(initial_scale); Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -117,8 +118,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces fill.Alpha = (float)Interpolation.Damp(fill.Alpha, drawableSpinner.RotationTracker.Tracking ? tracking_alpha : idle_alpha, 0.98f, (float)Math.Abs(Clock.ElapsedFrameTime)); } - const float initial_scale = 0.2f; - float targetScale = initial_scale + (1 - initial_scale) * drawableSpinner.Progress; + const float initial_fill_scale = 0.2f; + float targetScale = initial_fill_scale + (1 - initial_fill_scale) * drawableSpinner.Progress; fill.Scale = new Vector2((float)Interpolation.Lerp(fill.Scale.X, targetScale, Math.Clamp(Math.Abs(Time.Elapsed) / 100, 0, 1))); mainContainer.Rotation = drawableSpinner.RotationTracker.Rotation; @@ -129,41 +130,54 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (!(drawableHitObject is DrawableSpinner)) return; - centre.ScaleTo(0); - mainContainer.ScaleTo(0); - - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt / 2, true)) + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) { - // constant ambient rotation to give the spinner "spinning" character. - this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); - - centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); - mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint); + this.ScaleTo(initial_scale); + this.RotateTo(0); using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) { - centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint); - mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); + // constant ambient rotation to give the spinner "spinning" character. + this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); + } + + using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true)) + { + switch (state) + { + case ArmedState.Hit: + this.ScaleTo(initial_scale * 1.2f, 320, Easing.Out); + this.RotateTo(mainContainer.Rotation + 180, 320); + break; + + case ArmedState.Miss: + this.ScaleTo(initial_scale * 0.8f, 320, Easing.In); + break; + } + } + } + + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + { + centre.ScaleTo(0); + mainContainer.ScaleTo(0); + + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) + { + centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); + mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint); + + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) + { + centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint); + mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); + } } } // transforms we have from completing the spinner will be rolled back, so reapply immediately. - updateComplete(state == ArmedState.Hit, 0); - - using (BeginDelayedSequence(spinner.Duration, true)) - { - switch (state) - { - case ArmedState.Hit: - this.ScaleTo(Scale * 1.2f, 320, Easing.Out); - this.RotateTo(mainContainer.Rotation + 180, 320); - break; - - case ArmedState.Miss: - this.ScaleTo(Scale * 0.8f, 320, Easing.In); - break; - } - } + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + updateComplete(state == ArmedState.Hit, 0); } private void updateComplete(bool complete, double duration) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 1dfc9c0772..56b5571ce1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -70,9 +70,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - this.FadeOut(); drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); } @@ -83,12 +81,19 @@ namespace osu.Game.Rulesets.Osu.Skinning var spinner = (Spinner)drawableSpinner.HitObject; + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + this.FadeOut(); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) this.FadeInFromZero(spinner.TimeFadeIn / 2); - fixedMiddle.FadeColour(Color4.White); - using (BeginAbsoluteSequence(spinner.StartTime, true)) - fixedMiddle.FadeColour(Color4.Red, spinner.Duration); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + { + fixedMiddle.FadeColour(Color4.White); + + using (BeginDelayedSequence(spinner.TimePreempt, true)) + fixedMiddle.FadeColour(Color4.Red, spinner.Duration); + } } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index eba9abda0b..7b0d7acbbc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -88,9 +88,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - this.FadeOut(); drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); } @@ -101,6 +99,9 @@ namespace osu.Game.Rulesets.Osu.Skinning var spinner = drawableSpinner.HitObject; + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + this.FadeOut(); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) this.FadeInFromZero(spinner.TimeFadeIn / 2); } From b7c276093db90227293a4fc8505e3d3aaa46f5cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 16:21:50 +0900 Subject: [PATCH 3590/6909] Add fallback case when EditorChangeHandler is not present (for tests) --- .../Rulesets/Edit/DrawableEditRulesetWrapper.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index d259a89055..43e5153f24 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Edit Playfield.DisplayJudgements.Value = false; } - [Resolved] + [Resolved(canBeNull: true)] private IEditorChangeHandler changeHandler { get; set; } protected override void LoadComplete() @@ -50,8 +50,15 @@ namespace osu.Game.Rulesets.Edit beatmap.HitObjectAdded += addHitObject; beatmap.HitObjectRemoved += removeHitObject; - // for now only regenerate replay on a finalised state change, not HitObjectUpdated. - changeHandler.OnStateChange += updateReplay; + if (changeHandler != null) + { + // for now only regenerate replay on a finalised state change, not HitObjectUpdated. + changeHandler.OnStateChange += updateReplay; + } + else + { + beatmap.HitObjectUpdated += _ => updateReplay(); + } } private void updateReplay() => drawableRuleset.RegenerateAutoplay(); From b7aba194411ea28ab6de45246edce05e62964ac1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 16:31:11 +0900 Subject: [PATCH 3591/6909] Add audio file drag-drop support at editor setup screen --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 44 +++++++++++++++++++++- 1 file changed, 42 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index f6eb92e1ec..7bb4e8bbc4 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -13,6 +15,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; +using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -23,8 +26,14 @@ using osuTK; namespace osu.Game.Screens.Edit.Setup { - public class SetupScreen : EditorScreen + public class SetupScreen : EditorScreen, ICanAcceptFiles { + public IEnumerable HandledExtensions => ImageExtensions.Concat(AudioExtensions); + + public static string[] ImageExtensions { get; } = { ".jpg", ".jpeg", ".png" }; + + public static string[] AudioExtensions { get; } = { ".mp3", ".ogg" }; + private FillFlowContainer flow; private LabelledTextBox artistTextBox; private LabelledTextBox titleTextBox; @@ -32,6 +41,9 @@ namespace osu.Game.Screens.Edit.Setup private LabelledTextBox difficultyTextBox; private LabelledTextBox audioTrackTextBox; + [Resolved] + private OsuGameBase game { get; set; } + [Resolved] private MusicController music { get; set; } @@ -150,6 +162,12 @@ namespace osu.Game.Screens.Edit.Setup item.OnCommit += onCommit; } + protected override void LoadComplete() + { + base.LoadComplete(); + game.RegisterImportHandler(this); + } + public bool ChangeAudioTrack(string path) { var info = new FileInfo(path); @@ -196,6 +214,28 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value; Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; } + + public Task Import(params string[] paths) + { + var firstFile = new FileInfo(paths.First()); + + if (ImageExtensions.Contains(firstFile.Extension)) + { + // todo: add image drag drop support + } + else if (AudioExtensions.Contains(firstFile.Extension)) + { + audioTrackTextBox.Text = firstFile.FullName; + } + + return Task.CompletedTask; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + game.UnregisterImportHandler(this); + } } internal class FileChooserLabelledTextBox : LabelledTextBox @@ -230,7 +270,7 @@ namespace osu.Game.Screens.Edit.Setup public void DisplayFileChooser() { - Target.Child = new FileSelector(validFileExtensions: new[] { ".mp3", ".ogg" }) + Target.Child = new FileSelector(validFileExtensions: SetupScreen.AudioExtensions) { RelativeSizeAxes = Axes.X, Height = 400, From 4139301afa172f2edc0eb734fa05dfe0596a30f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 16:49:47 +0900 Subject: [PATCH 3592/6909] Exit import process after first handler is run --- osu.Game/OsuGameBase.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index f61ff43ca9..611bd783cd 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -362,7 +362,10 @@ namespace osu.Game foreach (var importer in fileImporters) { if (importer.HandledExtensions.Contains(extension)) + { await importer.Import(paths); + continue; + } } } From 2a02f8f3f3ba5dbbf86d807e0ddb29b4a26b4ebc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 16:49:55 +0900 Subject: [PATCH 3593/6909] Add support for background changing --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 89 ++++++++++++++++------ 1 file changed, 65 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 7bb4e8bbc4..bbd0e23210 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -40,6 +40,7 @@ namespace osu.Game.Screens.Edit.Setup private LabelledTextBox creatorTextBox; private LabelledTextBox difficultyTextBox; private LabelledTextBox audioTrackTextBox; + private Container backgroundSpriteContainer; [Resolved] private OsuGameBase game { get; set; } @@ -95,19 +96,12 @@ namespace osu.Game.Screens.Edit.Setup Direction = FillDirection.Vertical, Children = new Drawable[] { - new Container + backgroundSpriteContainer = new Container { RelativeSizeAxes = Axes.X, Height = 250, Masking = true, CornerRadius = 10, - Child = new BeatmapBackgroundSprite(Beatmap.Value) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - }, }, new OsuSpriteText { @@ -156,18 +150,81 @@ namespace osu.Game.Screens.Edit.Setup } }; + updateBackgroundSprite(); + audioTrackTextBox.Current.BindValueChanged(audioTrackChanged); foreach (var item in flow.OfType()) item.OnCommit += onCommit; } + Task ICanAcceptFiles.Import(params string[] paths) + { + Schedule(() => + { + var firstFile = new FileInfo(paths.First()); + + if (ImageExtensions.Contains(firstFile.Extension)) + { + ChangeBackgroundImage(firstFile.FullName); + } + else if (AudioExtensions.Contains(firstFile.Extension)) + { + audioTrackTextBox.Text = firstFile.FullName; + } + }); + + return Task.CompletedTask; + } + + private void updateBackgroundSprite() + { + LoadComponentAsync(new BeatmapBackgroundSprite(Beatmap.Value) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + }, background => + { + backgroundSpriteContainer.Child = background; + background.FadeInFromZero(500); + }); + } + protected override void LoadComplete() { base.LoadComplete(); game.RegisterImportHandler(this); } + public bool ChangeBackgroundImage(string path) + { + var info = new FileInfo(path); + + if (!info.Exists) + return false; + + var set = Beatmap.Value.BeatmapSetInfo; + + // remove the previous background for now. + // in the future we probably want to check if this is being used elsewhere (other difficulties?) + var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.BackgroundFile); + + using (var stream = info.OpenRead()) + { + if (oldFile != null) + beatmaps.ReplaceFile(set, oldFile, stream, info.Name); + else + beatmaps.AddFile(set, stream, info.Name); + } + + Beatmap.Value.Metadata.BackgroundFile = info.Name; + updateBackgroundSprite(); + + return true; + } + public bool ChangeAudioTrack(string path) { var info = new FileInfo(path); @@ -215,22 +272,6 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; } - public Task Import(params string[] paths) - { - var firstFile = new FileInfo(paths.First()); - - if (ImageExtensions.Contains(firstFile.Extension)) - { - // todo: add image drag drop support - } - else if (AudioExtensions.Contains(firstFile.Extension)) - { - audioTrackTextBox.Text = firstFile.FullName; - } - - return Task.CompletedTask; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From faeb9910e5e98bae54ffc7503e554134ded98a85 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 17:06:55 +0900 Subject: [PATCH 3594/6909] Revert "Exit import process after first handler is run" This reverts commit 4139301afa172f2edc0eb734fa05dfe0596a30f9. --- osu.Game/OsuGameBase.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 611bd783cd..f61ff43ca9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -362,10 +362,7 @@ namespace osu.Game foreach (var importer in fileImporters) { if (importer.HandledExtensions.Contains(extension)) - { await importer.Import(paths); - continue; - } } } From fc920a8899478ff4e00ff5be84188386799148f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 17:32:34 +0900 Subject: [PATCH 3595/6909] Add change handler logic --- osu.Game/Screens/Edit/Timing/GroupSection.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index ac9c4be97a..0cc78315d2 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -27,6 +27,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private EditorClock clock { get; set; } + [Resolved(canBeNull: true)] + private IEditorChangeHandler changeHandler { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -90,6 +93,8 @@ namespace osu.Game.Screens.Edit.Timing private void changeSelectedGroupTime(in double time) { + changeHandler?.BeginChange(); + var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray(); Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value); @@ -98,6 +103,8 @@ namespace osu.Game.Screens.Edit.Timing Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp); SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time); + + changeHandler?.EndChange(); } } } From 00eed295272b57ae3570fa9ef8e87274e9b1870f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 17:35:41 +0900 Subject: [PATCH 3596/6909] Don't update time if it hasn't changed --- osu.Game/Screens/Edit/Timing/GroupSection.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index 0cc78315d2..ee19aaface 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -93,6 +93,9 @@ namespace osu.Game.Screens.Edit.Timing private void changeSelectedGroupTime(in double time) { + if (time == SelectedGroup.Value.Time) + return; + changeHandler?.BeginChange(); var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray(); From 436cc572d3666353d24669846155b6c709f0b4f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 17:15:28 +0900 Subject: [PATCH 3597/6909] Expose ChangeHandler.SaveState via interface --- osu.Game/Screens/Edit/EditorChangeHandler.cs | 3 --- osu.Game/Screens/Edit/IEditorChangeHandler.cs | 6 ++++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 617c436ee0..66331d54c0 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -79,9 +79,6 @@ namespace osu.Game.Screens.Edit SaveState(); } - /// - /// Saves the current state. - /// public void SaveState() { if (bulkChangesStarted > 0) diff --git a/osu.Game/Screens/Edit/IEditorChangeHandler.cs b/osu.Game/Screens/Edit/IEditorChangeHandler.cs index c1328252d4..f95df76907 100644 --- a/osu.Game/Screens/Edit/IEditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/IEditorChangeHandler.cs @@ -29,5 +29,11 @@ namespace osu.Game.Screens.Edit /// This should be invoked as soon as possible after to cause a state change. /// void EndChange(); + + /// + /// Immediately saves the current state. + /// Note that this will be a no-op if there is a change in progress via . + /// + void SaveState(); } } From c1c5b5da8e703e7fc37b9585c4c63f743d4a7180 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 17:15:58 +0900 Subject: [PATCH 3598/6909] Push state change on control point group addition / removal --- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 0a0cfe193d..3b3ae949c1 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -87,6 +87,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] private Bindable selectedGroup { get; set; } + [Resolved(canBeNull: true)] + private IEditorChangeHandler changeHandler { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -146,6 +149,7 @@ namespace osu.Game.Screens.Edit.Timing controlGroups.BindCollectionChanged((sender, args) => { table.ControlGroups = controlGroups; + changeHandler.SaveState(); }, true); } From 98fd661b239dbd189cc7fb36cd1a30b8e20083c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 17:55:47 +0900 Subject: [PATCH 3599/6909] Add change handling for timing section --- osu.Game/Screens/Edit/Timing/Section.cs | 3 +++ osu.Game/Screens/Edit/Timing/TimingSection.cs | 7 +++++++ 2 files changed, 10 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/Section.cs b/osu.Game/Screens/Edit/Timing/Section.cs index 603fb77f31..7a81eeb1a4 100644 --- a/osu.Game/Screens/Edit/Timing/Section.cs +++ b/osu.Game/Screens/Edit/Timing/Section.cs @@ -32,6 +32,9 @@ namespace osu.Game.Screens.Edit.Timing [Resolved] protected Bindable SelectedGroup { get; private set; } + [Resolved(canBeNull: true)] + protected IEditorChangeHandler ChangeHandler { get; private set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 0202441537..2ab8703cc4 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -37,8 +37,13 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { bpmSlider.Bindable = point.NewValue.BeatLengthBindable; + bpmSlider.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState()); + bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; + // no need to hook change handler here as it's the same bindable as above + timeSignature.Bindable = point.NewValue.TimeSignatureBindable; + timeSignature.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState()); } } @@ -117,6 +122,8 @@ namespace osu.Game.Screens.Edit.Timing bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); base.Bindable = bpmBindable; + + TransferValueOnCommit = true; } public override Bindable Bindable From 693a4ff474ea957bd1d8bc4276b3d75616904278 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 17:56:30 +0900 Subject: [PATCH 3600/6909] Add change handling for effects section --- osu.Game/Screens/Edit/Timing/EffectSection.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index 71e7f42713..2f143108a9 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -28,7 +28,10 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { kiai.Current = point.NewValue.KiaiModeBindable; + kiai.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + omitBarLine.Current = point.NewValue.OmitFirstBarLineBindable; + omitBarLine.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } From 08faef694bde8b66b7234c231ea58b89a058a951 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 17:58:22 +0900 Subject: [PATCH 3601/6909] Add change handling for difficulty section --- osu.Game/Screens/Edit/Timing/DifficultySection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs index 78766d9777..b55d74e3b4 100644 --- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs @@ -28,6 +28,7 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { multiplierSlider.Current = point.NewValue.SpeedMultiplierBindable; + multiplierSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } From 9fc9009dbe1a6bba52686e41413aae20c4804652 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 17:59:47 +0900 Subject: [PATCH 3602/6909] Add change handling for sample section --- osu.Game/Screens/Edit/Timing/SampleSection.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs index de986e28ca..280e19c99a 100644 --- a/osu.Game/Screens/Edit/Timing/SampleSection.cs +++ b/osu.Game/Screens/Edit/Timing/SampleSection.cs @@ -35,7 +35,10 @@ namespace osu.Game.Screens.Edit.Timing if (point.NewValue != null) { bank.Current = point.NewValue.SampleBankBindable; + bank.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); + volume.Current = point.NewValue.SampleVolumeBindable; + volume.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } From 519c3ac2bdb6a23e30f48cac57db9e4f62c858e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 17:59:57 +0900 Subject: [PATCH 3603/6909] Change SliderWithTextBoxInput to transfer on commit --- osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs index 14023b0c35..d5afc8978d 100644 --- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs @@ -38,6 +38,7 @@ namespace osu.Game.Screens.Edit.Timing }, slider = new SettingsSlider { + TransferValueOnCommit = true, RelativeSizeAxes = Axes.X, } } From 66f5187e6a26ea480fb777d9f5abef93ce7a4e13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 18:20:59 +0900 Subject: [PATCH 3604/6909] Remove redundant access permission --- osu.Game/Screens/Edit/IEditorChangeHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/IEditorChangeHandler.cs b/osu.Game/Screens/Edit/IEditorChangeHandler.cs index a23a956e14..1774ec6c04 100644 --- a/osu.Game/Screens/Edit/IEditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/IEditorChangeHandler.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Edit /// /// Fired whenever a state change occurs. /// - public event Action OnStateChange; + event Action OnStateChange; /// /// Begins a bulk state change event. should be invoked soon after. From 575046e5fdc34bc13797cab78517f337136734c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 18:21:13 +0900 Subject: [PATCH 3605/6909] Don't update reply on add/remove (will be automatically handled by change handler events) --- osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 43e5153f24..8ed7885101 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -69,8 +69,6 @@ namespace osu.Game.Rulesets.Edit drawableRuleset.Playfield.Add(drawableObject); drawableRuleset.Playfield.PostProcess(); - - updateReplay(); } private void removeHitObject(HitObject hitObject) @@ -79,8 +77,6 @@ namespace osu.Game.Rulesets.Edit drawableRuleset.Playfield.Remove(drawableObject); drawableRuleset.Playfield.PostProcess(); - - updateReplay(); } public override bool PropagatePositionalInputSubTree => false; From 1a0171fb2dc2a4fea5eaa8c57a86cd74932d4ff9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 18:18:14 +0900 Subject: [PATCH 3606/6909] Fix tests specifying steps in their constructors --- osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs | 4 +++- osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs | 4 ++-- osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs | 3 ++- osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs | 3 ++- osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs | 5 ++--- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs index 95e86de884..9c4c2b3d5b 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Testing; using osu.Game.Beatmaps; @@ -13,7 +14,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning { public class TestSceneHoldNote : ManiaHitObjectTestScene { - public TestSceneHoldNote() + [Test] + public void TestHoldNote() { AddToggleStep("toggle hitting", v => { diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index dd5fd93710..76c1b47cca 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -28,8 +28,8 @@ namespace osu.Game.Rulesets.Mania.Tests [TestFixture] public class TestSceneNotes : OsuTestScene { - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestVariousNotes() { Child = new FillFlowContainer { diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 37df0d6e37..596bc06c68 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs @@ -20,7 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; - public TestSceneHitCircle() + [Test] + public void TestVariousHitCircles() { AddStep("Miss Big Single", () => SetContents(() => testSingle(2))); AddStep("Miss Medium Single", () => SetContents(() => testSingle(5))); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index c79cae2fe5..c9e112f76d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -27,7 +27,8 @@ namespace osu.Game.Rulesets.Osu.Tests { private int depthIndex; - public TestSceneSlider() + [Test] + public void TestVariousSliders() { AddStep("Big Single", () => SetContents(() => testSimpleBig())); AddStep("Medium Single", () => SetContents(() => testSimpleMedium())); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 0f605be8f9..e4c0766844 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -3,7 +3,6 @@ using System; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -30,8 +29,8 @@ namespace osu.Game.Rulesets.Taiko.Tests private readonly Random rng = new Random(1337); - [BackgroundDependencyLoader] - private void load() + [Test] + public void TestVariousHits() { AddStep("Hit", () => addHitJudgement(false)); AddStep("Strong hit", () => addStrongHitJudgement(false)); From 5a6c45e2ff43fa7e4c811a46883d577db715faea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 18:41:28 +0900 Subject: [PATCH 3607/6909] Fix hidden mod support for sliderendcircle --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 31 +++++++++++++++++++ .../Objects/Drawables/DrawableSliderRepeat.cs | 4 ++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 08fd13915d..80e40af717 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -39,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods base.ApplyToDrawableHitObjects(drawables); } + private double lastSliderHeadFadeOutStartTime; + private double lastSliderHeadFadeOutDuration; + protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) { if (!(drawable is DrawableOsuHitObject d)) @@ -54,7 +57,35 @@ namespace osu.Game.Rulesets.Osu.Mods switch (drawable) { + case DrawableSliderTail sliderTail: + // use stored values from head circle to achieve same fade sequence. + fadeOutDuration = lastSliderHeadFadeOutDuration; + fadeOutStartTime = lastSliderHeadFadeOutStartTime; + + using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) + sliderTail.FadeOut(fadeOutDuration); + + break; + + case DrawableSliderRepeat sliderRepeat: + // use stored values from head circle to achieve same fade sequence. + fadeOutDuration = lastSliderHeadFadeOutDuration; + fadeOutStartTime = lastSliderHeadFadeOutStartTime; + + using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) + // only apply to circle piece – reverse arrow is not affected by hidden. + sliderRepeat.CirclePiece.FadeOut(fadeOutDuration); + + break; + case DrawableHitCircle circle: + + if (circle is DrawableSliderHead) + { + lastSliderHeadFadeOutDuration = fadeOutDuration; + lastSliderHeadFadeOutStartTime = fadeOutStartTime; + } + // we don't want to see the approach circle using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) circle.ApproachCircle.Hide(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 9d775de7df..46d47a8c94 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -24,6 +24,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private readonly Drawable scaleContainer; + public readonly Drawable CirclePiece; + public override bool DisplayResult => false; public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) @@ -44,7 +46,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Children = new Drawable[] { // no default for this; only visible in legacy skins. - new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()), + CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()), arrow = new ReverseArrowPiece(), } }; From ed34985fdde5b8bcf86169a1f66219def3e0ac9f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 18:47:11 +0900 Subject: [PATCH 3608/6909] Add step for mania note construction --- .../TestSceneNotes.cs | 37 ++++++++++++------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index 76c1b47cca..fd8a01766b 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.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.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -31,22 +32,30 @@ namespace osu.Game.Rulesets.Mania.Tests [Test] public void TestVariousNotes() { - Child = new FillFlowContainer + DrawableNote note1 = null; + DrawableNote note2 = null; + DrawableHoldNote holdNote1 = null; + DrawableHoldNote holdNote2 = null; + + AddStep("create notes", () => { - Clock = new FramedClock(new ManualClock()), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(20), - Children = new[] + Child = new FillFlowContainer { - createNoteDisplay(ScrollingDirection.Down, 1, out var note1), - createNoteDisplay(ScrollingDirection.Up, 2, out var note2), - createHoldNoteDisplay(ScrollingDirection.Down, 1, out var holdNote1), - createHoldNoteDisplay(ScrollingDirection.Up, 2, out var holdNote2), - } - }; + Clock = new FramedClock(new ManualClock()), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(20), + Children = new[] + { + createNoteDisplay(ScrollingDirection.Down, 1, out note1), + createNoteDisplay(ScrollingDirection.Up, 2, out note2), + createHoldNoteDisplay(ScrollingDirection.Down, 1, out holdNote1), + createHoldNoteDisplay(ScrollingDirection.Up, 2, out holdNote2), + } + }; + }); AddAssert("note 1 facing downwards", () => verifyAnchors(note1, Anchor.y2)); AddAssert("note 2 facing upwards", () => verifyAnchors(note2, Anchor.y0)); From fcc6cb36e4c1e204e4ea106e6756b2cdb442bb48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 18:50:47 +0900 Subject: [PATCH 3609/6909] Change text colour to black --- osu.Game/Screens/Edit/Timing/RowAttribute.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/RowAttribute.cs b/osu.Game/Screens/Edit/Timing/RowAttribute.cs index c45995ee83..2757e08026 100644 --- a/osu.Game/Screens/Edit/Timing/RowAttribute.cs +++ b/osu.Game/Screens/Edit/Timing/RowAttribute.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Edit.Timing Origin = Anchor.Centre, Font = OsuFont.Default.With(weight: FontWeight.SemiBold, size: 12), Text = header, - Colour = colours.Gray3 + Colour = colours.Gray0 }, }; } From 0d3a95d8fca626aededdff8bbc4e329b4401818c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 19:54:13 +0900 Subject: [PATCH 3610/6909] Remove unnecessary string interpolation --- .../Edit/Compose/Components/Timeline/SamplePointPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs index 6a6e947343..37c8c8402a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/SamplePointPiece.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }; volume.BindValueChanged(volume => volumeBox.Height = volume.NewValue / 100f, true); - bank.BindValueChanged(bank => text.Text = $"{bank.NewValue}", true); + bank.BindValueChanged(bank => text.Text = bank.NewValue, true); } } } From a3ecc6c5a4bb55553b9a4ab97f1859e3f665ec0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 19:56:24 +0900 Subject: [PATCH 3611/6909] Remove redundant array type specification --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 46d47a8c94..2a88f11f69 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Children = new Drawable[] + Children = new[] { // no default for this; only visible in legacy skins. CirclePiece = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderTailHitCircle), _ => Empty()), From 75ae9f1b30c47e3802fa7b2170e8f9d4d695cc52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 2 Oct 2020 19:57:14 +0900 Subject: [PATCH 3612/6909] Remove unused using --- osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index fd8a01766b..6b8f5d5d9d 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -9,7 +9,6 @@ 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.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; From dab50bff6f28dcddc8c9ed68ed2a3e0be71e9822 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 3 Oct 2020 01:27:42 +0900 Subject: [PATCH 3613/6909] Protect "use current time" button against crash when no timing point is selected --- osu.Game/Screens/Edit/Timing/GroupSection.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index ee19aaface..c77d48ef0a 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -18,6 +18,8 @@ namespace osu.Game.Screens.Edit.Timing { private LabelledTextBox textBox; + private TriangleButton button; + [Resolved] protected Bindable SelectedGroup { get; private set; } @@ -52,7 +54,7 @@ namespace osu.Game.Screens.Edit.Timing { Label = "Time" }, - new TriangleButton + button = new TriangleButton { Text = "Use current time", RelativeSizeAxes = Axes.X, @@ -82,18 +84,22 @@ namespace osu.Game.Screens.Edit.Timing if (group.NewValue == null) { textBox.Text = string.Empty; + textBox.Current.Disabled = true; + button.Enabled.Value = false; return; } textBox.Current.Disabled = false; + button.Enabled.Value = true; + textBox.Text = $"{group.NewValue.Time:n0}"; }, true); } private void changeSelectedGroupTime(in double time) { - if (time == SelectedGroup.Value.Time) + if (SelectedGroup.Value == null || time == SelectedGroup.Value.Time) return; changeHandler?.BeginChange(); From 16f331cf6d09601c655234cfa245252a90adbe03 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 2 Oct 2020 19:34:06 +0300 Subject: [PATCH 3614/6909] Move implementation to LegacyCursorTrail --- osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs | 10 +++++++++- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 10 ++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs index 1885c76fcc..eabf797607 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.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 osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Events; +using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; @@ -15,6 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning private bool disjointTrail; private double lastTrailTime; + private IBindable cursorSize; public LegacyCursorTrail() { @@ -22,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(ISkinSource skin, OsuConfigManager config) { Texture = skin.GetTexture("cursortrail"); disjointTrail = skin.GetTexture("cursormiddle") == null; @@ -32,12 +36,16 @@ namespace osu.Game.Rulesets.Osu.Skinning // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation. Texture.ScaleAdjust *= 1.6f; } + + cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); } protected override double FadeDuration => disjointTrail ? 150 : 500; protected override bool InterpolateMovements => !disjointTrail; + protected override float IntervalMultiplier => Math.Max(cursorSize.Value, 1); + protected override bool OnMouseMove(MouseMoveEvent e) { if (!disjointTrail) diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index fb8a850223..c30615e6e9 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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Batches; using osu.Framework.Graphics.OpenGL.Vertices; @@ -16,7 +15,6 @@ using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Framework.Layout; using osu.Framework.Timing; -using osu.Game.Configuration; using osuTK; using osuTK.Graphics; using osuTK.Graphics.ES30; @@ -30,7 +28,6 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor private readonly TrailPart[] parts = new TrailPart[max_sprites]; private int currentIndex; private IShader shader; - private Bindable cursorSize; private double timeOffset; private float time; @@ -51,10 +48,9 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor } [BackgroundDependencyLoader] - private void load(ShaderManager shaders, OsuConfigManager config) + private void load(ShaderManager shaders) { shader = shaders.Load(@"CursorTrail", FragmentShaderDescriptor.TEXTURE); - cursorSize = config.GetBindable(OsuSetting.GameplayCursorSize).GetBoundCopy(); } protected override void LoadComplete() @@ -123,6 +119,8 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor /// protected virtual bool InterpolateMovements => true; + protected virtual float IntervalMultiplier => 1.0f; + private Vector2? lastPosition; private readonly InputResampler resampler = new InputResampler(); @@ -151,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor float distance = diff.Length; Vector2 direction = diff / distance; - float interval = partSize.X / 2.5f / Math.Max(cursorSize.Value, 1); + float interval = partSize.X / 2.5f / IntervalMultiplier; for (float d = interval; d < distance; d += interval) { From 8cd13729eeabb94dccdea6057994007ad4dbeac4 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 2 Oct 2020 19:34:49 +0300 Subject: [PATCH 3615/6909] Actually multiply by the multiplier --- osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs | 2 +- osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs index eabf797607..e6cd7bc59d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Osu.Skinning protected override bool InterpolateMovements => !disjointTrail; - protected override float IntervalMultiplier => Math.Max(cursorSize.Value, 1); + protected override float IntervalMultiplier => 1 / Math.Max(cursorSize.Value, 1); protected override bool OnMouseMove(MouseMoveEvent e) { diff --git a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs index c30615e6e9..0b30c28b8d 100644 --- a/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs +++ b/osu.Game.Rulesets.Osu/UI/Cursor/CursorTrail.cs @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Osu.UI.Cursor float distance = diff.Length; Vector2 direction = diff / distance; - float interval = partSize.X / 2.5f / IntervalMultiplier; + float interval = partSize.X / 2.5f * IntervalMultiplier; for (float d = interval; d < distance; d += interval) { From 0163688a174d84305b191caa1a2a42ce48ed3a6f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 2 Oct 2020 19:24:30 +0200 Subject: [PATCH 3616/6909] Remove IBeatmap from PerformanceCalculator. --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- .../Difficulty/CatchPerformanceCalculator.cs | 4 ++-- .../Difficulty/ManiaPerformanceCalculator.cs | 4 ++-- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Difficulty/OsuPerformanceCalculator.cs | 4 ++-- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Difficulty/TaikoPerformanceCalculator.cs | 4 ++-- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs | 9 +++------ osu.Game/Rulesets/Ruleset.cs | 2 +- 10 files changed, 16 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index ca75a816f1..cb7cac436b 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Catch public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new CatchPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score); public int LegacyID => 2; diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index a4b9ca35eb..e671e581cf 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -25,8 +25,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty private int tinyTicksMissed; private int misses; - public CatchPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) - : base(ruleset, beatmap, score) + public CatchPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) + : base(ruleset, attributes, score) { } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 91383c5548..086afb3254 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -29,8 +29,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty private int countMeh; private int countMiss; - public ManiaPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) - : base(ruleset, beatmap, score) + public ManiaPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) + : base(ruleset, attributes, score) { } diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 71ac85dd1b..8bf6b5e064 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 IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new ManiaPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score); public const string SHORT_NAME = "mania"; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 02577461f0..9e08163329 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -31,8 +31,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty private int countMeh; private int countMiss; - public OsuPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) - : base(ruleset, beatmap, score) + public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) + : base(ruleset, attributes, score) { countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle); diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 7f4a0dcbbb..9798f15f21 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new OsuPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score); public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this); diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index c04fffa2e7..2505300425 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -24,8 +24,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private int countMeh; private int countMiss; - public TaikoPerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) - : base(ruleset, beatmap, score) + public TaikoPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) + : base(ruleset, attributes, score) { } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 9d485e3f20..3bc749b868 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => new TaikoPerformanceCalculator(this, beatmap, score); + public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score); public int LegacyID => 1; diff --git a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs index ac3b817840..58427f6945 100644 --- a/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/PerformanceCalculator.cs @@ -1,11 +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.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; @@ -16,19 +16,16 @@ namespace osu.Game.Rulesets.Difficulty protected readonly DifficultyAttributes Attributes; protected readonly Ruleset Ruleset; - protected readonly IBeatmap Beatmap; protected readonly ScoreInfo Score; protected double TimeRate { get; private set; } = 1; - protected PerformanceCalculator(Ruleset ruleset, WorkingBeatmap beatmap, ScoreInfo score) + protected PerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) { Ruleset = ruleset; Score = score; - Beatmap = beatmap.GetPlayableBeatmap(ruleset.RulesetInfo, score.Mods); - - Attributes = ruleset.CreateDifficultyCalculator(beatmap).Calculate(score.Mods); + Attributes = attributes ?? throw new ArgumentNullException(nameof(attributes)); ApplyMods(score.Mods); } diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 915544d010..25c5f41b5b 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -158,7 +158,7 @@ namespace osu.Game.Rulesets public abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap); - public virtual PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) => null; + public virtual PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, DifficultyAttributes attributes, ScoreInfo score) => null; public virtual HitObjectComposer CreateHitObjectComposer() => null; From cb2f695fddf3fca6c2fd6654a25984be5b721dcf Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 2 Oct 2020 19:34:41 +0200 Subject: [PATCH 3617/6909] Calculate hit circle count in OsuPerformanceCalculator. --- .../Difficulty/OsuDifficultyAttributes.cs | 1 + .../Difficulty/OsuDifficultyCalculator.cs | 3 +++ .../Difficulty/OsuPerformanceCalculator.cs | 20 ++++++------------- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index a9879013f8..50f060cf06 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -11,5 +11,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double SpeedStrain; public double ApproachRate; public double OverallDifficulty; + public int HitCirclesCount; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index b0d261a1cc..86c7cd2298 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -47,6 +47,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Add the ticks + tail of the slider. 1 is subtracted because the head circle would be counted twice (once for the slider itself in the line above) maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); + int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); + return new OsuDifficultyAttributes { StarRating = starRating, @@ -56,6 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, MaxCombo = maxCombo, + HitCirclesCount = hitCirclesCount, Skills = skills }; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 9e08163329..6acd8f9a87 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -19,9 +19,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty { public new OsuDifficultyAttributes Attributes => (OsuDifficultyAttributes)base.Attributes; - private readonly int countHitCircles; - private readonly int beatmapMaxCombo; - private Mod[] mods; private double accuracy; @@ -34,11 +31,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty public OsuPerformanceCalculator(Ruleset ruleset, DifficultyAttributes attributes, ScoreInfo score) : base(ruleset, attributes, score) { - countHitCircles = Beatmap.HitObjects.Count(h => h is HitCircle); - - beatmapMaxCombo = Beatmap.HitObjects.Count; - // Add the ticks + tail of the slider. 1 is subtracted because the "headcircle" would be counted twice (once for the slider itself in the line above) - beatmapMaxCombo += Beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); } public override double Calculate(Dictionary categoryRatings = null) @@ -81,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty categoryRatings.Add("Accuracy", accuracyValue); categoryRatings.Add("OD", Attributes.OverallDifficulty); categoryRatings.Add("AR", Attributes.ApproachRate); - categoryRatings.Add("Max Combo", beatmapMaxCombo); + categoryRatings.Add("Max Combo", Attributes.MaxCombo); } return totalValue; @@ -106,8 +98,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= Math.Pow(0.97, countMiss); // Combo scaling - if (beatmapMaxCombo > 0) - aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0); + if (Attributes.MaxCombo > 0) + aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); double approachRateFactor = 1.0; @@ -154,8 +146,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= Math.Pow(0.97, countMiss); // Combo scaling - if (beatmapMaxCombo > 0) - speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(beatmapMaxCombo, 0.8), 1.0); + if (Attributes.MaxCombo > 0) + speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); double approachRateFactor = 1.0; if (Attributes.ApproachRate > 10.33) @@ -178,7 +170,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window double betterAccuracyPercentage; - int amountHitObjectsWithAccuracy = countHitCircles; + int amountHitObjectsWithAccuracy = Attributes.HitCirclesCount; if (amountHitObjectsWithAccuracy > 0) betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); From abd395a03098808ff3926be50d7063394d572342 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 2 Oct 2020 19:41:24 +0200 Subject: [PATCH 3618/6909] Remove unecessary using references. --- .../Difficulty/CatchPerformanceCalculator.cs | 1 - .../Difficulty/ManiaPerformanceCalculator.cs | 1 - osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 -- .../Difficulty/TaikoPerformanceCalculator.cs | 1 - 4 files changed, 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs index e671e581cf..6a3a16ed33 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchPerformanceCalculator.cs @@ -5,7 +5,6 @@ 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; diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs index 086afb3254..00bec18a45 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaPerformanceCalculator.cs @@ -5,7 +5,6 @@ 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; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 6acd8f9a87..fed0a12536 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -5,11 +5,9 @@ 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.Osu.Mods; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 2505300425..2d9b95ae88 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -5,7 +5,6 @@ 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; From 2b1ef16f89ef4e7d9b1646ee0fe5d183176d49fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Oct 2020 22:57:49 +0200 Subject: [PATCH 3619/6909] Replace comparison references to HitResult.Miss with IsHit --- osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs | 2 +- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs | 2 +- .../TestSceneMissHitWindowJudgements.cs | 4 ++-- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableOsuJudgement.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 2 +- osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs | 4 ++-- osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs | 2 +- osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs | 2 +- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs | 2 +- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- osu.Game/Screens/Ranking/Statistics/UnstableRate.cs | 2 +- 15 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs index cc01009dd9..75feb21298 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchComboDisplay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI if (!result.Type.AffectsCombo() || !result.HasResult) return; - if (result.Type == HitResult.Miss) + if (!result.IsHit) { updateCombo(0, null); return; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index ba6cad978d..f6d539c91b 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -243,7 +243,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables endHold(); } - if (Tail.Result.Type == HitResult.Miss) + if (Tail.Judged && !Tail.IsHit) HasBroken = true; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index f3221ffe32..39deba2f57 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Osu.Tests { 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 + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset < -hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit }); } @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Autoplay = false, Beatmap = beatmap, - PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && Player.Results[0].Type == HitResult.Miss + PassCondition = () => Player.Results.Count > 0 && Player.Results[0].TimeOffset >= hitWindows.WindowFor(HitResult.Meh) && !Player.Results[0].IsHit }); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index d5c3538c81..844449851f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -314,7 +314,7 @@ namespace osu.Game.Rulesets.Osu.Tests private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult); - private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.IgnoreHit && judgementResults.First().Type == HitResult.Miss; + private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.IgnoreHit && !judgementResults.First().IsHit; private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.IgnoreHit; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index a438dc8be4..6d6bd7fc97 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -143,7 +143,7 @@ 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) + if (result.IsHit()) { var localMousePosition = ToLocalSpace(inputManager.CurrentState.Mouse.Position); circleResult.CursorPositionAtHit = HitObject.StackedPosition + (localMousePosition - DrawSize / 2); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 012d9f8878..46f6276a85 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (JudgedObject != 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.IsHit ? colour.NewValue : Color4.Transparent, true); } else { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 9abcef83c4..21c7d49961 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -250,7 +250,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { // 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) + if (TailCircle.IsHit) base.PlaySamples(); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 286feac5ba..677e63c993 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!(obj is DrawableDrumRollTick)) return; - if (result.Type > HitResult.Miss) + if (result.IsHit) rollingHits++; else rollingHits--; diff --git a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs index dd3c2289ea..f7a1d130eb 100644 --- a/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs +++ b/osu.Game.Rulesets.Taiko/Scoring/TaikoHealthProcessor.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Taiko.Scoring private double hpMultiplier; /// - /// HP multiplier for a . + /// HP multiplier for a that does not satisfy . /// private double hpMissMultiplier; @@ -45,6 +45,6 @@ namespace osu.Game.Rulesets.Taiko.Scoring } protected override double GetHealthIncreaseFor(JudgementResult result) - => base.GetHealthIncreaseFor(result) * (result.Type == HitResult.Miss ? hpMissMultiplier : hpMultiplier); + => base.GetHealthIncreaseFor(result) * (result.IsHit ? hpMultiplier : hpMissMultiplier); } } diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs index 928072c491..e029040ef3 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning if (r?.Type.AffectsCombo() == false) return; - passing = r == null || r.Type > HitResult.Miss; + passing = r == null || r.IsHit; foreach (var sprite in InternalChildren.OfType()) sprite.Passing = passing; diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs index 7b8ab89233..3bd20e4bb4 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultHitExplosion.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Taiko.UI Alpha = 0.15f; Masking = true; - if (result == HitResult.Miss) + if (!result.IsHit()) return; bool isRim = (judgedObject.HitObject as Hit)?.Type == HitType.Rim; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 29c25f20a4..9af7ae12a2 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -511,7 +511,7 @@ namespace osu.Game.Rulesets.Objects.Drawables case HitResult.None: break; - case HitResult.Miss: + case { } result when !result.IsHit(): updateState(ArmedState.Miss); break; diff --git a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs index 7736541c92..aff5a36c81 100644 --- a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Play.HUD public void Flash(JudgementResult result) { - if (result.Type == HitResult.Miss) + if (!result.IsHit) return; Scheduler.AddOnce(flash); diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 45fdc3ff33..aa2a83774e 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.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss).ToList(); + this.hitEvents = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()).ToList(); } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs index 18a2238784..055db143d1 100644 --- a/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs +++ b/osu.Game/Screens/Ranking/Statistics/UnstableRate.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Ranking.Statistics public UnstableRate(IEnumerable hitEvents) : base("Unstable Rate") { - var timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result != HitResult.Miss) + var timeOffsets = hitEvents.Where(e => !(e.HitObject.HitWindows is HitWindows.EmptyHitWindows) && e.Result.IsHit()) .Select(ev => ev.TimeOffset).ToArray(); Value = 10 * standardDeviation(timeOffsets); } From 1f0620ffd49921a28a4edf9abcc61805f97d3243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 2 Oct 2020 22:58:10 +0200 Subject: [PATCH 3620/6909] Replace assignment references to HitResult.Miss with Judgement.MinResult --- .../Objects/Drawables/DrawableHoldNoteTail.cs | 2 +- .../Objects/Drawables/DrawableManiaHitObject.cs | 3 +-- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableOsuHitObject.cs | 3 +-- .../Objects/Drawables/DrawableOsuJudgement.cs | 1 - osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 1 - osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 4 ++-- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 4 +--- osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs | 1 - 12 files changed, 10 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index 31e43d3ee2..c780c0836e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 08c41b0d75..27960b3f3a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -9,7 +9,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.Scoring; namespace osu.Game.Rulesets.Mania.Objects.Drawables { @@ -136,7 +135,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// /// Causes this to get missed, disregarding all conditions in implementations of . /// - public void MissForcefully() => ApplyResult(r => r.Type = HitResult.Miss); + public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); } public abstract class DrawableManiaHitObject : DrawableManiaHitObject diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index 973dc06e05..b3402d13e4 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 6d6bd7fc97..b5ac26c824 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 2946331bc6..45c664ba3b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -8,7 +8,6 @@ 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 { @@ -68,7 +67,7 @@ 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); + public void MissForcefully() => ApplyResult(r => r.Type = r.Judgement.MinResult); protected override JudgementResult CreateResult(Judgement judgement) => new OsuJudgementResult(HitObject, judgement); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 46f6276a85..49535e7fff 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -8,7 +8,6 @@ using osu.Game.Configuration; using osuTK; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK.Graphics; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 21c7d49961..280ca33234 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index fe7cb278b0..130b4e6e53 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -224,7 +224,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables else if (Progress > .75) r.Type = HitResult.Meh; else if (Time.Current >= Spinner.EndTime) - r.Type = HitResult.Miss; + r.Type = r.Judgement.MinResult; }); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 677e63c993..8f268dc1c7 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = countHit >= HitObject.RequiredGreatHits ? HitResult.Great : HitResult.Ok); } else - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); } protected override void UpdateStateTransforms(ArmedState state) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 03df28f850..bb42240f25 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { if (!HitObject.HitWindows.CanBeHit(timeOffset)) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); return; } @@ -152,7 +152,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return; if (!validActionPressed) - ApplyResult(r => r.Type = HitResult.Miss); + ApplyResult(r => r.Type = r.Judgement.MinResult); else ApplyResult(r => r.Type = result); } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 11ff0729e2..8ee4a5db71 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -211,9 +211,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables tick.TriggerResult(false); } - var hitResult = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : HitResult.Miss; - - ApplyResult(r => r.Type = hitResult); + ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult); } } diff --git a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs index aff5a36c81..fc4a1a5d83 100644 --- a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs @@ -13,7 +13,6 @@ using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Framework.Utils; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD { From 7359c422dd377f7b589bbb12ce8662c41060124d Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Sat, 3 Oct 2020 12:58:43 +0930 Subject: [PATCH 3621/6909] Hoist icon stream --- osu.Desktop/OsuGameDesktop.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 659730630a..836b968a67 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -125,12 +125,14 @@ namespace osu.Desktop { base.SetHost(host); + var iconStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico"); + switch (host.Window) { // Legacy osuTK DesktopGameWindow case DesktopGameWindow desktopGameWindow: desktopGameWindow.CursorState |= CursorState.Hidden; - desktopGameWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico")); + desktopGameWindow.SetIconFromStream(iconStream); desktopGameWindow.Title = Name; desktopGameWindow.FileDrop += (_, e) => fileDrop(e.FileNames); break; @@ -138,7 +140,7 @@ namespace osu.Desktop // SDL2 DesktopWindow case DesktopWindow desktopWindow: desktopWindow.CursorState.Value |= CursorState.Hidden; - desktopWindow.SetIconFromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream(GetType(), "lazer.ico")); + desktopWindow.SetIconFromStream(iconStream); desktopWindow.Title = Name; desktopWindow.DragDrop += f => fileDrop(new[] { f }); break; From 2ddfd799230c0336bb3ffa6b215281032e996ad0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 3 Oct 2020 08:09:10 +0200 Subject: [PATCH 3622/6909] Replace object pattern match with simple conditional --- .../Objects/Drawables/DrawableHitObject.cs | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 9af7ae12a2..66fc61720a 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -506,19 +506,8 @@ namespace osu.Game.Rulesets.Objects.Drawables Result.TimeOffset = Math.Min(HitObject.HitWindows.WindowFor(HitResult.Miss), Time.Current - endTime); - switch (Result.Type) - { - case HitResult.None: - break; - - case { } result when !result.IsHit(): - updateState(ArmedState.Miss); - break; - - default: - updateState(ArmedState.Hit); - break; - } + if (Result.HasResult) + updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); OnNewResult?.Invoke(this, Result); } From feb39920c5d9e3d5353ab71eb5be230767af1273 Mon Sep 17 00:00:00 2001 From: tytydraco Date: Sat, 3 Oct 2020 00:48:49 -0700 Subject: [PATCH 3623/6909] Allow rotation lock on Android to function properly According to Google's documentation, fullSensor will ignore rotation locking preferences, while fullUser will obey them. Signed-off-by: tytydraco --- 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 9839d16030..db73bb7e7f 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 = false)] + [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] public class OsuGameActivity : AndroidGameActivity { protected override Framework.Game CreateGame() => new OsuGameAndroid(); From 309714081fa99023d06560f19b293acee4783df1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 3 Oct 2020 11:08:51 +0200 Subject: [PATCH 3624/6909] Make new health increase values mania-specific --- .../Judgements/ManiaJudgement.cs | 30 +++++++++++++++++++ osu.Game/Rulesets/Judgements/Judgement.cs | 10 +++---- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs index 220dedc4a4..d28b7bdf58 100644 --- a/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/Judgements/ManiaJudgement.cs @@ -2,10 +2,40 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Judgements { public class ManiaJudgement : Judgement { + protected override double HealthIncreaseFor(HitResult result) + { + switch (result) + { + case HitResult.LargeTickHit: + return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + + case HitResult.LargeTickMiss: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.1; + + case HitResult.Meh: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; + + case HitResult.Ok: + return -DEFAULT_MAX_HEALTH_INCREASE * 0.3; + + case HitResult.Good: + return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + + case HitResult.Great: + return DEFAULT_MAX_HEALTH_INCREASE * 0.8; + + case HitResult.Perfect: + return DEFAULT_MAX_HEALTH_INCREASE; + + default: + return base.HealthIncreaseFor(result); + } + } } } diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 4ee0ce437c..5d7444e9b0 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -124,19 +124,19 @@ namespace osu.Game.Rulesets.Judgements return -DEFAULT_MAX_HEALTH_INCREASE; case HitResult.Meh: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; + return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; case HitResult.Ok: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.3; + return DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.Good: - return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + return DEFAULT_MAX_HEALTH_INCREASE * 0.75; case HitResult.Great: - return DEFAULT_MAX_HEALTH_INCREASE * 0.8; + return DEFAULT_MAX_HEALTH_INCREASE; case HitResult.Perfect: - return DEFAULT_MAX_HEALTH_INCREASE; + return DEFAULT_MAX_HEALTH_INCREASE * 1.05; case HitResult.SmallBonus: return DEFAULT_MAX_HEALTH_INCREASE * 0.1; From 601675db073b9f12f83a20f8462825ef3d19a725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 3 Oct 2020 11:10:08 +0200 Subject: [PATCH 3625/6909] Adjust health increase values to match old ones better --- osu.Game/Rulesets/Judgements/Judgement.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/Judgement.cs b/osu.Game/Rulesets/Judgements/Judgement.cs index 5d7444e9b0..89a3a2b855 100644 --- a/osu.Game/Rulesets/Judgements/Judgement.cs +++ b/osu.Game/Rulesets/Judgements/Judgement.cs @@ -109,16 +109,16 @@ namespace osu.Game.Rulesets.Judgements return 0; case HitResult.SmallTickHit: - return DEFAULT_MAX_HEALTH_INCREASE * 0.05; + return DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.SmallTickMiss: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.05; + return -DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.LargeTickHit: - return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + return DEFAULT_MAX_HEALTH_INCREASE; case HitResult.LargeTickMiss: - return -DEFAULT_MAX_HEALTH_INCREASE * 0.1; + return -DEFAULT_MAX_HEALTH_INCREASE; case HitResult.Miss: return -DEFAULT_MAX_HEALTH_INCREASE; @@ -139,10 +139,10 @@ namespace osu.Game.Rulesets.Judgements return DEFAULT_MAX_HEALTH_INCREASE * 1.05; case HitResult.SmallBonus: - return DEFAULT_MAX_HEALTH_INCREASE * 0.1; + return DEFAULT_MAX_HEALTH_INCREASE * 0.5; case HitResult.LargeBonus: - return DEFAULT_MAX_HEALTH_INCREASE * 0.2; + return DEFAULT_MAX_HEALTH_INCREASE; } } From db31280671cf969e09000ac135455094dc1c012d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 3 Oct 2020 11:10:28 +0200 Subject: [PATCH 3626/6909] Award health for completed slider tails --- osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs index aff3f38e17..3afd36669f 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTailCircle.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects public class SliderTailJudgement : OsuJudgement { - public override HitResult MaxResult => HitResult.IgnoreHit; + public override HitResult MaxResult => HitResult.SmallTickHit; } } } From 682b5fb056ae9ecc50dd863b6490f851d1508a70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 3 Oct 2020 11:41:26 +0200 Subject: [PATCH 3627/6909] Adjust health increase for drum roll tick to match new max result --- .../Judgements/TaikoDrumRollTickJudgement.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs index 0551df3211..647ad7853d 100644 --- a/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs +++ b/osu.Game.Rulesets.Taiko/Judgements/TaikoDrumRollTickJudgement.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Judgements { switch (result) { - case HitResult.Great: + case HitResult.SmallTickHit: return 0.15; default: From 7e7f225eee6bd5ea896944d65c509d5b87f1bf95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 3 Oct 2020 12:34:34 +0200 Subject: [PATCH 3628/6909] Adjust slider input test to match new judgement result --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index d5c3538c81..1810ef4353 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -314,11 +314,11 @@ namespace osu.Game.Rulesets.Osu.Tests private bool assertMaxJudge() => judgementResults.Any() && judgementResults.All(t => t.Type == t.Judgement.MaxResult); - private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.IgnoreHit && judgementResults.First().Type == HitResult.Miss; + private bool assertHeadMissTailTracked() => judgementResults[^2].Type == HitResult.SmallTickHit && judgementResults.First().Type == HitResult.Miss; - private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.IgnoreHit; + private bool assertMidSliderJudgements() => judgementResults[^2].Type == HitResult.SmallTickHit; - private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.IgnoreMiss; + private bool assertMidSliderJudgementFail() => judgementResults[^2].Type == HitResult.SmallTickMiss; private ScoreAccessibleReplayPlayer currentPlayer; From d87e4c524c1c03539881df35fa97ccf800751aae Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 3 Oct 2020 14:21:40 +0300 Subject: [PATCH 3629/6909] Test HitResultExtensions methods --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index ace57aad1d..38d2b4a47f 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -53,5 +53,105 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); } + + [TestCase(HitResult.None, false)] + [TestCase(HitResult.IgnoreMiss, false)] + [TestCase(HitResult.IgnoreHit, false)] + [TestCase(HitResult.Miss, true)] + [TestCase(HitResult.Meh, true)] + [TestCase(HitResult.Ok, true)] + [TestCase(HitResult.Good, true)] + [TestCase(HitResult.Great, true)] + [TestCase(HitResult.Perfect, true)] + [TestCase(HitResult.SmallTickMiss, false)] + [TestCase(HitResult.SmallTickHit, false)] + [TestCase(HitResult.LargeTickMiss, true)] + [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SmallBonus, false)] + [TestCase(HitResult.LargeBonus, false)] + public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue) + { + Assert.IsTrue(hitResult.AffectsCombo() == expectedReturnValue); + } + + [TestCase(HitResult.None, false)] + [TestCase(HitResult.IgnoreMiss, false)] + [TestCase(HitResult.IgnoreHit, false)] + [TestCase(HitResult.Miss, true)] + [TestCase(HitResult.Meh, true)] + [TestCase(HitResult.Ok, true)] + [TestCase(HitResult.Good, true)] + [TestCase(HitResult.Great, true)] + [TestCase(HitResult.Perfect, true)] + [TestCase(HitResult.SmallTickMiss, true)] + [TestCase(HitResult.SmallTickHit, true)] + [TestCase(HitResult.LargeTickMiss, true)] + [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SmallBonus, false)] + [TestCase(HitResult.LargeBonus, false)] + public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue) + { + Assert.IsTrue(hitResult.AffectsAccuracy() == expectedReturnValue); + } + + [TestCase(HitResult.None, false)] + [TestCase(HitResult.IgnoreMiss, false)] + [TestCase(HitResult.IgnoreHit, false)] + [TestCase(HitResult.Miss, false)] + [TestCase(HitResult.Meh, false)] + [TestCase(HitResult.Ok, false)] + [TestCase(HitResult.Good, false)] + [TestCase(HitResult.Great, false)] + [TestCase(HitResult.Perfect, false)] + [TestCase(HitResult.SmallTickMiss, false)] + [TestCase(HitResult.SmallTickHit, false)] + [TestCase(HitResult.LargeTickMiss, false)] + [TestCase(HitResult.LargeTickHit, false)] + [TestCase(HitResult.SmallBonus, true)] + [TestCase(HitResult.LargeBonus, true)] + public void TestIsBonus(HitResult hitResult, bool expectedReturnValue) + { + Assert.IsTrue(hitResult.IsBonus() == expectedReturnValue); + } + + [TestCase(HitResult.None, false)] + [TestCase(HitResult.IgnoreMiss, false)] + [TestCase(HitResult.IgnoreHit, true)] + [TestCase(HitResult.Miss, false)] + [TestCase(HitResult.Meh, true)] + [TestCase(HitResult.Ok, true)] + [TestCase(HitResult.Good, true)] + [TestCase(HitResult.Great, true)] + [TestCase(HitResult.Perfect, true)] + [TestCase(HitResult.SmallTickMiss, false)] + [TestCase(HitResult.SmallTickHit, true)] + [TestCase(HitResult.LargeTickMiss, false)] + [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SmallBonus, true)] + [TestCase(HitResult.LargeBonus, true)] + public void TestIsHit(HitResult hitResult, bool expectedReturnValue) + { + Assert.IsTrue(hitResult.IsHit() == expectedReturnValue); + } + + [TestCase(HitResult.None, false)] + [TestCase(HitResult.IgnoreMiss, false)] + [TestCase(HitResult.IgnoreHit, false)] + [TestCase(HitResult.Miss, true)] + [TestCase(HitResult.Meh, true)] + [TestCase(HitResult.Ok, true)] + [TestCase(HitResult.Good, true)] + [TestCase(HitResult.Great, true)] + [TestCase(HitResult.Perfect, true)] + [TestCase(HitResult.SmallTickMiss, true)] + [TestCase(HitResult.SmallTickHit, true)] + [TestCase(HitResult.LargeTickMiss, true)] + [TestCase(HitResult.LargeTickHit, true)] + [TestCase(HitResult.SmallBonus, true)] + [TestCase(HitResult.LargeBonus, true)] + public void TestIsScorable(HitResult hitResult, bool expectedReturnValue) + { + Assert.IsTrue(hitResult.IsScorable() == expectedReturnValue); + } } } From d7747ebb2d5ba27bf6e5272e381dbdafc6c921bc Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 3 Oct 2020 16:51:22 +0200 Subject: [PATCH 3630/6909] Remove unused WorkingBeatmap argument. --- 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 +- osu.Game/Rulesets/Ruleset.cs | 10 +++++++++- 5 files changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index cb7cac436b..1f27de3352 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Catch public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score); + public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new CatchPerformanceCalculator(this, attributes, score); public int LegacyID => 2; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 8bf6b5e064..ecb09ebe85 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 IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new ManiaBeatmapConverter(beatmap, this); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score); + public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new ManiaPerformanceCalculator(this, attributes, score); public const string SHORT_NAME = "mania"; diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 9798f15f21..cc2eebdd36 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(this, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score); + public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new OsuPerformanceCalculator(this, attributes, score); public override HitObjectComposer CreateHitObjectComposer() => new OsuHitObjectComposer(this); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 3bc749b868..642eb0ddcc 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(this, beatmap); - public override PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score); + public override PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => new TaikoPerformanceCalculator(this, attributes, score); public int LegacyID => 1; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 25c5f41b5b..2ba884efc2 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -158,7 +158,15 @@ namespace osu.Game.Rulesets public abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap); - public virtual PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, DifficultyAttributes attributes, ScoreInfo score) => null; + public virtual PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; + + [Obsolete("Use the DifficultyAttributes overload instead.")] + public PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) + { + var difficultyCalculator = CreateDifficultyCalculator(beatmap); + var difficultyAttributes = difficultyCalculator.Calculate(score.Mods); + return CreatePerformanceCalculator(difficultyAttributes, score); + } public virtual HitObjectComposer CreateHitObjectComposer() => null; From 27cc6c50467616f63daa1b1de1e43a9306bc30f1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 3 Oct 2020 16:52:33 +0200 Subject: [PATCH 3631/6909] Rename HitCirclesCount -> HitCircleCount. --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 +- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index 50f060cf06..fff033357d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -11,6 +11,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double SpeedStrain; public double ApproachRate; public double OverallDifficulty; - public int HitCirclesCount; + public int HitCircleCount; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 86c7cd2298..6027635b75 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty ApproachRate = preempt > 1200 ? (1800 - preempt) / 120 : (1200 - preempt) / 150 + 5, OverallDifficulty = (80 - hitWindowGreat) / 6, MaxCombo = maxCombo, - HitCirclesCount = hitCirclesCount, + HitCircleCount = hitCirclesCount, Skills = skills }; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index fed0a12536..063cde8747 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty { // This percentage only considers HitCircles of any value - in this part of the calculation we focus on hitting the timing hit window double betterAccuracyPercentage; - int amountHitObjectsWithAccuracy = Attributes.HitCirclesCount; + int amountHitObjectsWithAccuracy = Attributes.HitCircleCount; if (amountHitObjectsWithAccuracy > 0) betterAccuracyPercentage = ((countGreat - (totalHits - amountHitObjectsWithAccuracy)) * 6 + countOk * 2 + countMeh) / (double)(amountHitObjectsWithAccuracy * 6); From 5888ecdeb16a6db3d8acd2825f086c0fd323d5a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 4 Oct 2020 01:08:24 +0900 Subject: [PATCH 3632/6909] Fix spinner crashing on rewind --- 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 fe7cb278b0..0f249c8bbf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -268,7 +268,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables while (wholeSpins != spins) { - var tick = ticks.FirstOrDefault(t => !t.IsHit); + var tick = ticks.FirstOrDefault(t => !t.Result.HasResult); // tick may be null if we've hit the spin limit. if (tick != null) From 26eff0120db42daed55b375cb22db982b8d60d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 3 Oct 2020 21:11:34 +0200 Subject: [PATCH 3633/6909] Apply same fix for miss-triggering case See 5888ecd - the same fix is applied here, but in the miss case. --- 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 0f249c8bbf..5e1b7bdcae 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return; // Trigger a miss result for remaining ticks to avoid infinite gameplay. - foreach (var tick in ticks.Where(t => !t.IsHit)) + foreach (var tick in ticks.Where(t => !t.Result.HasResult)) tick.TriggerResult(false); ApplyResult(r => From ad42ce5639d5ae92dd6fc4e9bc067f446da04214 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 4 Oct 2020 14:50:25 +0200 Subject: [PATCH 3634/6909] Add failing test cases --- osu.Game.Tests/NonVisual/GameplayClockTest.cs | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 osu.Game.Tests/NonVisual/GameplayClockTest.cs diff --git a/osu.Game.Tests/NonVisual/GameplayClockTest.cs b/osu.Game.Tests/NonVisual/GameplayClockTest.cs new file mode 100644 index 0000000000..3fd7c364b7 --- /dev/null +++ b/osu.Game.Tests/NonVisual/GameplayClockTest.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Timing; +using osu.Game.Screens.Play; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class GameplayClockTest + { + [TestCase(0)] + [TestCase(1)] + public void TestTrueGameplayRateWithZeroAdjustment(double underlyingClockRate) + { + var framedClock = new FramedClock(new ManualClock { Rate = underlyingClockRate }); + var gameplayClock = new TestGameplayClock(framedClock); + + gameplayClock.MutableNonGameplayAdjustments.Add(new BindableDouble()); + + Assert.That(gameplayClock.TrueGameplayRate, Is.EqualTo(0)); + } + + private class TestGameplayClock : GameplayClock + { + public List> MutableNonGameplayAdjustments { get; } = new List>(); + + public override IEnumerable> NonGameplayAdjustments => MutableNonGameplayAdjustments; + + public TestGameplayClock(IFrameBasedClock underlyingClock) + : base(underlyingClock) + { + } + } + } +} From 6f2b991b329cb9b44980995e668ecc5d7e5980a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 4 Oct 2020 14:51:27 +0200 Subject: [PATCH 3635/6909] Ensure true gameplay rate is finite when paused externally --- osu.Game/Screens/Play/GameplayClock.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 9d04722c12..9f2868573e 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Timing; +using osu.Framework.Utils; namespace osu.Game.Screens.Play { @@ -47,7 +48,12 @@ namespace osu.Game.Screens.Play double baseRate = Rate; foreach (var adjustment in NonGameplayAdjustments) + { + if (Precision.AlmostEquals(adjustment.Value, 0)) + return 0; + baseRate /= adjustment.Value; + } return baseRate; } From 02e4f3ddafc4678df1965523d29296f058523708 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 4 Oct 2020 23:47:16 +0900 Subject: [PATCH 3636/6909] Fix the editor saving new beatmaps even when the user chooses not to --- osu.Game/Screens/Edit/Editor.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index a0692d94e6..956b77b0d4 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -84,6 +84,8 @@ namespace osu.Game.Screens.Edit private DependencyContainer dependencies; + private bool isNewBeatmap; + protected override UserActivity InitialActivity => new UserActivity.Editing(Beatmap.Value.BeatmapInfo); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -113,8 +115,6 @@ namespace osu.Game.Screens.Edit // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); - bool isNewBeatmap = false; - if (Beatmap.Value is DummyWorkingBeatmap) { isNewBeatmap = true; @@ -287,6 +287,9 @@ namespace osu.Game.Screens.Edit protected void Save() { + // no longer new after first user-triggered save. + isNewBeatmap = false; + // apply any set-level metadata changes. beatmapManager.Update(playableBeatmap.BeatmapInfo.BeatmapSet); @@ -435,7 +438,7 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { - if (!exitConfirmed && dialogOverlay != null && HasUnsavedChanges && !(dialogOverlay.CurrentDialog is PromptForSaveDialog)) + if (!exitConfirmed && dialogOverlay != null && (isNewBeatmap || HasUnsavedChanges) && !(dialogOverlay.CurrentDialog is PromptForSaveDialog)) { dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); return true; @@ -456,6 +459,12 @@ namespace osu.Game.Screens.Edit private void confirmExit() { + if (isNewBeatmap) + { + // confirming exit without save means we should delete the new beatmap completely. + beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet); + } + exitConfirmed = true; this.Exit(); } From 1b02c814d6ba33141cc71461ffc876c20e97035b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 4 Oct 2020 23:47:47 +0900 Subject: [PATCH 3637/6909] 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 78ceaa8616..d7817cf4cf 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 3a839ac1a4..fa2135580d 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 31f1af135d..20a51e5feb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 9ca0e48accc80a4778c60b7049e994a7abd4d58e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 4 Oct 2020 23:57:28 +0900 Subject: [PATCH 3638/6909] Change exit logic to be more test-friendly --- osu.Game/Screens/Edit/Editor.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 956b77b0d4..875ab25003 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -438,10 +438,20 @@ namespace osu.Game.Screens.Edit public override bool OnExiting(IScreen next) { - if (!exitConfirmed && dialogOverlay != null && (isNewBeatmap || HasUnsavedChanges) && !(dialogOverlay.CurrentDialog is PromptForSaveDialog)) + if (!exitConfirmed) { - dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); - return true; + // if the confirm dialog is already showing (or we can't show it, ie. in tests) exit without save. + if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog) + { + confirmExit(); + return true; + } + + if (isNewBeatmap || HasUnsavedChanges) + { + dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); + return true; + } } Background.FadeColour(Color4.White, 500); From 432ba7cdf953f1806b914769518d0e6f1cf3c23c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 4 Oct 2020 23:57:35 +0900 Subject: [PATCH 3639/6909] Add test coverage of exit-without-save --- .../Editing/TestSceneEditorBeatmapCreation.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 720cf51f2c..13a3195824 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -5,6 +5,8 @@ using System; using System.IO; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets; @@ -22,6 +24,9 @@ namespace osu.Game.Tests.Visual.Editing protected override bool EditorComponentsReady => Editor.ChildrenOfType().SingleOrDefault()?.IsLoaded == true; + [Resolved] + private BeatmapManager beatmapManager { get; set; } + public override void SetUpSteps() { AddStep("set dummy", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null)); @@ -38,6 +43,15 @@ namespace osu.Game.Tests.Visual.Editing { AddStep("save beatmap", () => Editor.Save()); AddAssert("new beatmap persisted", () => EditorBeatmap.BeatmapInfo.ID > 0); + AddAssert("new beatmap in database", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == false); + } + + [Test] + public void TestExitWithoutSave() + { + AddStep("exit without save", () => Editor.Exit()); + AddUntilStep("wait for exit", () => !Editor.IsCurrentScreen()); + AddAssert("new beatmap not persisted", () => beatmapManager.QueryBeatmapSet(s => s.ID == EditorBeatmap.BeatmapInfo.BeatmapSet.ID)?.DeletePending == true); } [Test] From 5859755886ac3e141e00e72a421bf61d19d6524e Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Mon, 5 Oct 2020 11:11:46 +1030 Subject: [PATCH 3640/6909] Use current OverlayActivationMode to determine confine logic --- osu.Game/Input/ConfineMouseTracker.cs | 33 +++++++++++---------------- osu.Game/Screens/Play/Player.cs | 5 ++-- 2 files changed, 15 insertions(+), 23 deletions(-) diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index b111488a5b..6565967d1d 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -7,44 +7,37 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Configuration; +using osu.Game.Overlays; using osu.Game.Screens.Play; namespace osu.Game.Input { /// - /// Connects with - /// while providing a property for to indicate whether gameplay is currently active. + /// Connects with , + /// while optionally binding an mode, usually that of the current . + /// It is assumed that while overlay activation is , we should also confine the + /// mouse cursor if it has been requested with . /// public class ConfineMouseTracker : Component { private Bindable frameworkConfineMode; private Bindable osuConfineMode; - private bool gameplayActive; - /// - /// Indicates whether osu! is currently considered "in gameplay" for the - /// purposes of . + /// The bindable used to indicate whether gameplay is active. + /// Should be bound to the corresponding bindable of the current . + /// Defaults to to assume that all other screens are considered "not gameplay". /// - public bool GameplayActive - { - get => gameplayActive; - set - { - if (gameplayActive == value) - return; - - gameplayActive = value; - updateConfineMode(); - } - } + public IBindable OverlayActivationMode { get; } = new Bindable(OverlayActivation.All); [BackgroundDependencyLoader] private void load(FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) { frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); - osuConfineMode.BindValueChanged(_ => updateConfineMode(), true); + osuConfineMode.ValueChanged += _ => updateConfineMode(); + + OverlayActivationMode.BindValueChanged(_ => updateConfineMode(), true); } private void updateConfineMode() @@ -60,7 +53,7 @@ namespace osu.Game.Input break; case OsuConfineMouseMode.DuringGameplay: - frameworkConfineMode.Value = GameplayActive ? ConfineMouseMode.Always : ConfineMouseMode.Never; + frameworkConfineMode.Value = OverlayActivationMode.Value == OverlayActivation.Disabled ? ConfineMouseMode.Always : ConfineMouseMode.Never; break; case OsuConfineMouseMode.Always: diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 77f873083a..6d2f61bdf1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -229,6 +229,8 @@ namespace osu.Game.Screens.Play DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); + confineMouseTracker.OverlayActivationMode.BindTo(OverlayActivationMode); + // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); @@ -365,9 +367,6 @@ namespace osu.Game.Screens.Play OverlayActivationMode.Value = OverlayActivation.UserTriggered; else OverlayActivationMode.Value = OverlayActivation.Disabled; - - if (confineMouseTracker != null) - confineMouseTracker.GameplayActive = !GameplayClockContainer.IsPaused.Value && !DrawableRuleset.HasReplayLoaded.Value && !HasFailed; } private void updatePauseOnFocusLostState() => From a483dfd2d7157131d886d8b7c92a4b08defdbf63 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Mon, 5 Oct 2020 11:54:39 +1030 Subject: [PATCH 3641/6909] Allow confineMouseTracker to be null --- 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 6d2f61bdf1..de67b2d46d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -229,7 +229,7 @@ namespace osu.Game.Screens.Play DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); - confineMouseTracker.OverlayActivationMode.BindTo(OverlayActivationMode); + confineMouseTracker?.OverlayActivationMode.BindTo(OverlayActivationMode); // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); From a8cbd400d36b8997a0ba87ae1ec55227381764fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 13:17:13 +0900 Subject: [PATCH 3642/6909] Ensure virtual track time is long enough for test beatmaps --- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 5 ++--- osu.Game/Tests/Visual/OsuTestScene.cs | 10 ++++++---- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index ab4fb38657..1e43e5d148 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -180,9 +180,8 @@ namespace osu.Game.Tests.Beatmaps 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) + public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio) + : base(beatmap, storyboard, referenceClock, audio) { this.skinBeatmapInfo = skinBeatmapInfo; this.resourceStore = resourceStore; diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index b59a1db403..6e2fd0a6d7 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -23,6 +23,7 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI; using osu.Game.Screens; using osu.Game.Storyboards; @@ -222,18 +223,19 @@ namespace osu.Game.Tests.Visual /// The storyboard. /// An optional clock which should be used instead of a stopwatch for virtual time progression. /// 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) + public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio) : base(beatmap, storyboard, audio) { + double lastObjectTime = beatmap.HitObjects.LastOrDefault()?.GetEndTime() ?? 60000; + if (referenceClock != null) { store = new TrackVirtualStore(referenceClock); audio.AddItem(store); - track = store.GetVirtual(length); + track = store.GetVirtual(lastObjectTime); } else - track = audio?.Tracks.GetVirtual(length); + track = audio?.Tracks.GetVirtual(lastObjectTime); } ~ClockBackedTestWorkingBeatmap() From 21bf93a7c2b6d07e4e825c6b14f59a4ea3edd0af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 13:29:36 +0900 Subject: [PATCH 3643/6909] Ensure there's a buffer after the last hitobject to allow certain replay tests to complete correctly --- osu.Game/Tests/Visual/OsuTestScene.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 6e2fd0a6d7..e3f07dbad4 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -226,16 +226,18 @@ namespace osu.Game.Tests.Visual public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio) : base(beatmap, storyboard, audio) { - double lastObjectTime = beatmap.HitObjects.LastOrDefault()?.GetEndTime() ?? 60000; + var lastHitObject = beatmap.HitObjects.LastOrDefault(); + + double trackLength = lastHitObject?.GetEndTime() + 2000 ?? 60000; if (referenceClock != null) { store = new TrackVirtualStore(referenceClock); audio.AddItem(store); - track = store.GetVirtual(lastObjectTime); + track = store.GetVirtual(trackLength); } else - track = audio?.Tracks.GetVirtual(lastObjectTime); + track = audio?.Tracks.GetVirtual(trackLength); } ~ClockBackedTestWorkingBeatmap() From 4b8188065504cf88de4c8cb487a180f1a0696904 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 14:04:04 +0900 Subject: [PATCH 3644/6909] Account for potentially longer non-last objects --- osu.Game/Tests/Visual/OsuTestScene.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index e3f07dbad4..8886188d95 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -226,9 +226,11 @@ namespace osu.Game.Tests.Visual public ClockBackedTestWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio) : base(beatmap, storyboard, audio) { - var lastHitObject = beatmap.HitObjects.LastOrDefault(); + double trackLength = 60000; - double trackLength = lastHitObject?.GetEndTime() + 2000 ?? 60000; + if (beatmap.HitObjects.Count > 0) + // add buffer after last hitobject to allow for final replay frames etc. + trackLength = beatmap.HitObjects.Max(h => h.GetEndTime()) + 2000; if (referenceClock != null) { From 7fead6ee41dcf4a242eec2d0b13df47d6c2fd50c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 14:22:32 +0900 Subject: [PATCH 3645/6909] Add comment making mania test behaviour clearer --- osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index ab840e1c46..e8c2472c3b 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.Mania.Tests objects.Add(new Note { StartTime = time }); + // don't hit the first note if (i > 0) { frames.Add(new ManiaReplayFrame(time + 10, ManiaAction.Key1)); From e1c4c8f3d5401ce298c4f3392a1464103a931385 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 13:16:45 +0900 Subject: [PATCH 3646/6909] Add failing test coverage of gameplay sample pausing (during seek) --- .../TestSceneGameplaySamplePlayback.cs | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs new file mode 100644 index 0000000000..3ab4df20df --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.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.Linq; +using NUnit.Framework; +using osu.Framework.Graphics.Audio; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; +using osu.Game.Skinning; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneGameplaySamplePlayback : PlayerTestScene + { + [Test] + public void TestAllSamplesStopDuringSeek() + { + DrawableSlider slider = null; + DrawableSample[] samples = null; + ISamplePlaybackDisabler gameplayClock = null; + + AddStep("get variables", () => + { + gameplayClock = Player.ChildrenOfType().First().GameplayClock; + slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); + samples = slider.ChildrenOfType().ToArray(); + }); + + AddUntilStep("wait for slider sliding then seek", () => + { + if (!slider.Tracking.Value) + return false; + + if (!samples.Any(s => s.Playing)) + return false; + + Player.ChildrenOfType().First().Seek(40000); + return true; + }); + + AddAssert("sample playback disabled", () => gameplayClock.SamplePlaybackDisabled.Value); + + // because we are in frame stable context, it's quite likely that not all samples are "played" at this point. + // the important thing is that at least one started, and that sample has since stopped. + AddAssert("no samples are playing", () => Player.ChildrenOfType().All(s => !s.IsPlaying)); + + AddAssert("sample playback still disabled", () => gameplayClock.SamplePlaybackDisabled.Value); + + AddUntilStep("seek finished, sample playback enabled", () => !gameplayClock.SamplePlaybackDisabled.Value); + AddUntilStep("any sample is playing", () => Player.ChildrenOfType().Any(s => s.IsPlaying)); + } + + protected override bool Autoplay => true; + + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + } +} From 2a46f905ff130c465676019d2e9daed638543870 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 12:45:37 +0900 Subject: [PATCH 3647/6909] Remove unnecessary IsSeeking checks from taiko drum implementation --- osu.Game.Rulesets.Taiko/UI/InputDrum.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs index 5966b24b34..1ca1be1bdf 100644 --- a/osu.Game.Rulesets.Taiko/UI/InputDrum.cs +++ b/osu.Game.Rulesets.Taiko/UI/InputDrum.cs @@ -163,16 +163,14 @@ namespace osu.Game.Rulesets.Taiko.UI target = centreHit; back = centre; - if (gameplayClock?.IsSeeking != true) - drumSample.Centre?.Play(); + drumSample.Centre?.Play(); } else if (action == RimAction) { target = rimHit; back = rim; - if (gameplayClock?.IsSeeking != true) - drumSample.Rim?.Play(); + drumSample.Rim?.Play(); } if (target != null) From af7d10afe0f532e7d339a0c28675817cd0b11226 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 12:45:57 +0900 Subject: [PATCH 3648/6909] Fix FrameStabilityContainer not re-caching its GameplayClock correctly --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 55c4edfbd1..668cbbdc35 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -35,6 +35,7 @@ namespace osu.Game.Rulesets.UI public GameplayClock GameplayClock => stabilityGameplayClock; [Cached(typeof(GameplayClock))] + [Cached(typeof(ISamplePlaybackDisabler))] private readonly StabilityGameplayClock stabilityGameplayClock; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) From e4710f82ec5a06258970ec01d9073838b8d7e581 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 12:46:15 +0900 Subject: [PATCH 3649/6909] Fix sample disabled status not being updated correctly from seek state --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 4 +++- osu.Game/Screens/Play/GameplayClock.cs | 12 ++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 668cbbdc35..6956d3c31a 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -228,7 +228,9 @@ namespace osu.Game.Rulesets.UI { } - public override bool IsSeeking => ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200; + protected override bool ShouldDisableSamplePlayback => + // handle the case where playback is catching up to real-time. + base.ShouldDisableSamplePlayback || (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 9f2868573e..eeea6777c6 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -28,6 +28,8 @@ namespace osu.Game.Screens.Play ///
    public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>(); + private readonly Bindable samplePlaybackDisabled = new Bindable(); + public GameplayClock(IFrameBasedClock underlyingClock) { this.underlyingClock = underlyingClock; @@ -62,13 +64,15 @@ namespace osu.Game.Screens.Play public bool IsRunning => underlyingClock.IsRunning; /// - /// Whether an ongoing seek operation is active. + /// Whether nested samples supporting the interface should be paused. /// - public virtual bool IsSeeking => false; + protected virtual bool ShouldDisableSamplePlayback => IsPaused.Value; public void ProcessFrame() { - // we do not want to process the underlying clock. + // intentionally not updating the underlying clock (handled externally). + + samplePlaybackDisabled.Value = ShouldDisableSamplePlayback; } public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; @@ -79,6 +83,6 @@ namespace osu.Game.Screens.Play public IClock Source => underlyingClock; - public IBindable SamplePlaybackDisabled => IsPaused; + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; } } From ae8bf8cdd4ca70eb5455b4389f4be459783b8c4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 12:47:00 +0900 Subject: [PATCH 3650/6909] Fix StabilityGameClock not being updated --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 6956d3c31a..f32f8d177b 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -208,11 +208,15 @@ namespace osu.Game.Rulesets.UI private void setClock() { - // in case a parent gameplay clock isn't available, just use the parent clock. - parentGameplayClock ??= Clock; - - Clock = GameplayClock; - ProcessCustomClock = false; + if (parentGameplayClock == null) + { + // in case a parent gameplay clock isn't available, just use the parent clock. + parentGameplayClock ??= Clock; + } + else + { + Clock = GameplayClock; + } } public ReplayInputHandler ReplayInputHandler { get; set; } From 758088672cf9b7e58c8c83a5c4aebae143063d56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 15:07:46 +0900 Subject: [PATCH 3651/6909] Don't stop non-looping samples immediately when pausing --- .../Objects/Drawables/DrawableSlider.cs | 4 +-- .../Objects/Drawables/DrawableSpinner.cs | 4 +-- .../Objects/Drawables/DrawableHitObject.cs | 10 +++++-- osu.Game/Skinning/PausableSkinnableSound.cs | 26 +++++++++---------- 4 files changed, 25 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 280ca33234..4433aac9b5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -109,9 +109,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - public override void StopAllSamples() + public override void StopLoopingSamples() { - base.StopAllSamples(); + base.StopLoopingSamples(); slidingSample?.Stop(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 130b4e6e53..dda0c94982 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -124,9 +124,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - public override void StopAllSamples() + public override void StopLoopingSamples() { - base.StopAllSamples(); + base.StopLoopingSamples(); spinningSample?.Stop(); } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 66fc61720a..7a4970d172 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -18,6 +18,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 @@ -387,7 +388,10 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Stops playback of all samples. Automatically called when 's lifetime has been exceeded. /// - public virtual void StopAllSamples() => Samples?.Stop(); + public virtual void StopLoopingSamples() + { + if (Samples?.Looping == true) + Samples.Stop(); protected override void Update() { @@ -457,7 +461,9 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var nested in NestedHitObjects) nested.OnKilled(); - StopAllSamples(); + // failsafe to ensure looping samples don't get stuck in a playing state. + // this could occur in a non-frame-stable context where DrawableHitObjects get killed before a SkinnableSound has the chance to be stopped. + StopLoopingSamples(); UpdateResult(false); } diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 9819574b1d..d340f67575 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -34,21 +34,21 @@ namespace osu.Game.Skinning samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); samplePlaybackDisabled.BindValueChanged(disabled => { - if (RequestedPlaying) + if (!RequestedPlaying) return; + + // let non-looping samples that have already been started play out to completion (sounds better than abruptly cutting off). + if (!Looping) return; + + if (disabled.NewValue) + base.Stop(); + else { - if (disabled.NewValue) - base.Stop(); - // it's not easy to know if a sample has finished playing (to end). - // to keep things simple only resume playing looping samples. - else if (Looping) + // schedule so we don't start playing a sample which is no longer alive. + Schedule(() => { - // schedule so we don't start playing a sample which is no longer alive. - Schedule(() => - { - if (RequestedPlaying) - base.Play(); - }); - } + if (RequestedPlaying) + base.Play(); + }); } }); } From 9f43dedf59da624a4ea1381cedb982dce40d1b24 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 15:12:34 +0900 Subject: [PATCH 3652/6909] Fix missing line --- 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 7a4970d172..11f84d370a 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -18,7 +18,6 @@ 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 @@ -392,6 +391,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { if (Samples?.Looping == true) Samples.Stop(); + } protected override void Update() { From a69b1636be75e094a7323a0961a45a1703a878f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 15:18:28 +0900 Subject: [PATCH 3653/6909] Update tests --- .../Visual/Editing/TestSceneEditorSamplePlayback.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs index 039a21fd94..f182023c0e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs @@ -19,12 +19,14 @@ namespace osu.Game.Tests.Visual.Editing public void TestSlidingSampleStopsOnSeek() { DrawableSlider slider = null; - DrawableSample[] samples = null; + DrawableSample[] loopingSamples = null; + DrawableSample[] onceOffSamples = null; AddStep("get first slider", () => { slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); - samples = slider.ChildrenOfType().ToArray(); + onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray(); + loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray(); }); AddStep("start playback", () => EditorClock.Start()); @@ -34,14 +36,15 @@ namespace osu.Game.Tests.Visual.Editing if (!slider.Tracking.Value) return false; - if (!samples.Any(s => s.Playing)) + if (!loopingSamples.Any(s => s.Playing)) return false; EditorClock.Seek(20000); return true; }); - AddAssert("slider samples are not playing", () => samples.Length == 5 && samples.All(s => s.Played && !s.Playing)); + AddAssert("non-looping samples are playing", () => onceOffSamples.Length == 4 && loopingSamples.All(s => s.Played || s.Playing)); + AddAssert("looping samples are not playing", () => loopingSamples.Length == 1 && loopingSamples.All(s => s.Played && !s.Playing)); } } } From 0605bb9b8d565b843dda3fdbf9702474fc8a5592 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 16:20:29 +0900 Subject: [PATCH 3654/6909] Fix incorrect parent state transfer --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index f32f8d177b..70b3d0c7d4 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -59,13 +59,16 @@ namespace osu.Game.Rulesets.UI private int direction; [BackgroundDependencyLoader(true)] - private void load(GameplayClock clock) + private void load(GameplayClock clock, ISamplePlaybackDisabler sampleDisabler) { if (clock != null) { parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock; GameplayClock.IsPaused.BindTo(clock.IsPaused); } + + // this is a bit temporary. should really be done inside of GameplayClock (but requires large structural changes). + stabilityGameplayClock.ParentSampleDisabler = sampleDisabler; } protected override void LoadComplete() @@ -225,6 +228,8 @@ namespace osu.Game.Rulesets.UI { public GameplayClock ParentGameplayClock; + public ISamplePlaybackDisabler ParentSampleDisabler; + public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); public StabilityGameplayClock(FramedClock underlyingClock) @@ -234,7 +239,9 @@ namespace osu.Game.Rulesets.UI protected override bool ShouldDisableSamplePlayback => // handle the case where playback is catching up to real-time. - base.ShouldDisableSamplePlayback || (ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200); + base.ShouldDisableSamplePlayback + || ParentSampleDisabler?.SamplePlaybackDisabled.Value == true + || (ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200); } } } From c622adde7a9374459a3a9a6ed93b7064685eb14d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 16:24:02 +0900 Subject: [PATCH 3655/6909] Rename method back and add xmldoc --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 4 ++-- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 ++-- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 4433aac9b5..280ca33234 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -109,9 +109,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - public override void StopLoopingSamples() + public override void StopAllSamples() { - base.StopLoopingSamples(); + base.StopAllSamples(); slidingSample?.Stop(); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index dda0c94982..130b4e6e53 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -124,9 +124,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - public override void StopLoopingSamples() + public override void StopAllSamples() { - base.StopLoopingSamples(); + base.StopAllSamples(); spinningSample?.Stop(); } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 11f84d370a..8012b4d95c 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -385,9 +385,10 @@ namespace osu.Game.Rulesets.Objects.Drawables } /// - /// Stops playback of all samples. Automatically called when 's lifetime has been exceeded. + /// Stops playback of all relevant samples. Generally only looping samples should be stopped by this, and the rest let to play out. + /// Automatically called when 's lifetime has been exceeded. /// - public virtual void StopLoopingSamples() + public virtual void StopAllSamples() { if (Samples?.Looping == true) Samples.Stop(); @@ -463,7 +464,7 @@ namespace osu.Game.Rulesets.Objects.Drawables // failsafe to ensure looping samples don't get stuck in a playing state. // this could occur in a non-frame-stable context where DrawableHitObjects get killed before a SkinnableSound has the chance to be stopped. - StopLoopingSamples(); + StopAllSamples(); UpdateResult(false); } From 2b824787c1c4a2620f6bf3ab4e43a3fbee78b807 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 5 Oct 2020 19:28:13 +0900 Subject: [PATCH 3656/6909] Guard against potential nullref --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index bbd0e23210..3d94737e59 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -275,7 +275,7 @@ namespace osu.Game.Screens.Edit.Setup protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - game.UnregisterImportHandler(this); + game?.UnregisterImportHandler(this); } } From 606a08c6ad38b967a557a2605e0c1184bedb33eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 20:01:12 +0900 Subject: [PATCH 3657/6909] Temporarily ignore failing gameplay samples test --- .../Visual/Gameplay/TestSceneGameplaySamplePlayback.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index 3ab4df20df..f0d39a8b18 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -17,6 +17,7 @@ namespace osu.Game.Tests.Visual.Gameplay public class TestSceneGameplaySamplePlayback : PlayerTestScene { [Test] + [Ignore("temporarily disabled pending investigation")] public void TestAllSamplesStopDuringSeek() { DrawableSlider slider = null; From 6bc0afdafb32c9e5728458fa8e099b39e9f7902a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 5 Oct 2020 20:09:18 +0900 Subject: [PATCH 3658/6909] Fix remaining conflicts --- osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs | 3 --- 1 file changed, 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 0bb3d25011..be3bca3242 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -13,7 +13,6 @@ using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; -using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; using osuTK; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -63,8 +62,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } private WaveformGraph waveform; - private ControlPointPart controlPoints; - private TimelineTickDisplay ticks; private TimelineTickDisplay ticks; From 9eeac759b8e5fd150db97118c15a99fb2496c658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 5 Oct 2020 21:22:07 +0200 Subject: [PATCH 3659/6909] Re-enable and fix gameplay sample playback test --- .../Visual/Gameplay/TestSceneGameplaySamplePlayback.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index f0d39a8b18..5bb3851264 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Audio; @@ -17,7 +18,6 @@ namespace osu.Game.Tests.Visual.Gameplay public class TestSceneGameplaySamplePlayback : PlayerTestScene { [Test] - [Ignore("temporarily disabled pending investigation")] public void TestAllSamplesStopDuringSeek() { DrawableSlider slider = null; @@ -47,7 +47,8 @@ namespace osu.Game.Tests.Visual.Gameplay // because we are in frame stable context, it's quite likely that not all samples are "played" at this point. // the important thing is that at least one started, and that sample has since stopped. - AddAssert("no samples are playing", () => Player.ChildrenOfType().All(s => !s.IsPlaying)); + AddAssert("all looping samples stopped immediately", () => allStopped(allLoopingSounds)); + AddUntilStep("all samples stopped eventually", () => allStopped(allSounds)); AddAssert("sample playback still disabled", () => gameplayClock.SamplePlaybackDisabled.Value); @@ -55,6 +56,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("any sample is playing", () => Player.ChildrenOfType().Any(s => s.IsPlaying)); } + private IEnumerable allSounds => Player.ChildrenOfType(); + private IEnumerable allLoopingSounds => allSounds.Where(sound => sound.Looping); + + private bool allStopped(IEnumerable sounds) => sounds.All(sound => !sound.IsPlaying); + protected override bool Autoplay => true; protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); From 46f6e84a3351dd77e4de78f785670788ab92a908 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 12:33:57 +0900 Subject: [PATCH 3660/6909] Fix disclaimer potentially running same code from two different threads --- osu.Game/Screens/Menu/Disclaimer.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index fcb9aacd76..8368047d5a 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -42,8 +42,11 @@ namespace osu.Game.Screens.Menu ValidForResume = false; } + [Resolved] + private IAPIProvider api { get; set; } + [BackgroundDependencyLoader] - private void load(OsuColour colours, IAPIProvider api) + private void load(OsuColour colours) { InternalChildren = new Drawable[] { @@ -104,7 +107,9 @@ namespace osu.Game.Screens.Menu iconColour = colours.Yellow; - currentUser.BindTo(api.LocalUser); + // manually transfer the user once, but only do the final bind in LoadComplete to avoid thread woes (API scheduler could run while this screen is still loading). + // the manual transfer is here to ensure all text content is loaded ahead of time as this is very early in the game load process and we want to avoid stutters. + currentUser.Value = api.LocalUser.Value; currentUser.BindValueChanged(e => { supportFlow.Children.ForEach(d => d.FadeOut().Expire()); @@ -141,6 +146,8 @@ namespace osu.Game.Screens.Menu base.LoadComplete(); if (nextScreen != null) LoadComponentAsync(nextScreen); + + currentUser.BindTo(api.LocalUser); } public override void OnEntering(IScreen last) From 22b0105d629a70344609d33314a76978053e633d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 13:00:02 +0900 Subject: [PATCH 3661/6909] Show a notification if checking for updates via button and there are none available --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 8 ++++--- .../Sections/General/UpdateSettings.cs | 21 +++++++++++++++++-- osu.Game/Updater/SimpleUpdateManager.cs | 7 ++++++- osu.Game/Updater/UpdateManager.cs | 19 ++++++++++------- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 05c8e835ac..b9b148b383 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -37,9 +37,9 @@ namespace osu.Desktop.Updater Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); } - protected override async Task PerformUpdateCheck() => await checkForUpdateAsync(); + protected override async Task PerformUpdateCheck() => await checkForUpdateAsync(); - private async Task 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; @@ -51,7 +51,7 @@ namespace osu.Desktop.Updater var info = await updateManager.CheckForUpdate(!useDeltaPatching); if (info.ReleasesToApply.Count == 0) // no updates available. bail and retry later. - return; + return false; if (notification == null) { @@ -103,6 +103,8 @@ namespace osu.Desktop.Updater Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30); } } + + return true; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 9c7d0b0be4..9b7b7392d8 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -4,9 +4,11 @@ using System.Threading.Tasks; using osu.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics.Sprites; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Configuration; +using osu.Game.Overlays.Notifications; using osu.Game.Overlays.Settings.Sections.Maintenance; using osu.Game.Updater; @@ -21,6 +23,9 @@ namespace osu.Game.Overlays.Settings.Sections.General private SettingsButton checkForUpdatesButton; + [Resolved] + private NotificationOverlay notifications { get; set; } + [BackgroundDependencyLoader(true)] private void load(Storage storage, OsuConfigManager config, OsuGame game) { @@ -30,7 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Bindable = config.GetBindable(OsuSetting.ReleaseStream), }); - if (updateManager?.CanCheckForUpdate == true) + //if (updateManager?.CanCheckForUpdate == true) { Add(checkForUpdatesButton = new SettingsButton { @@ -38,7 +43,19 @@ namespace osu.Game.Overlays.Settings.Sections.General Action = () => { checkForUpdatesButton.Enabled.Value = false; - Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(t => Schedule(() => checkForUpdatesButton.Enabled.Value = true)); + Task.Run(updateManager.CheckForUpdateAsync).ContinueWith(t => Schedule(() => + { + if (!t.Result) + { + notifications.Post(new SimpleNotification + { + Text = $"You are running the latest release ({game.Version})", + Icon = FontAwesome.Solid.CheckCircle, + }); + } + + checkForUpdatesButton.Enabled.Value = true; + })); } }); } diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index ebb9995c66..79b2d46b93 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 PerformUpdateCheck() + protected override async Task PerformUpdateCheck() { try { @@ -53,12 +53,17 @@ namespace osu.Game.Updater return true; } }); + + return true; } } catch { // we shouldn't crash on a web failure. or any failure for the matter. + return true; } + + return false; } private string getBestUrl(GitHubRelease release) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 61775a26b7..30e28f0e95 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -57,25 +57,28 @@ namespace osu.Game.Updater private readonly object updateTaskLock = new object(); - private Task updateCheckTask; + private Task updateCheckTask; - public async Task CheckForUpdateAsync() + public async Task CheckForUpdateAsync() { - if (!CanCheckForUpdate) - return; - - Task waitTask; + Task waitTask; lock (updateTaskLock) waitTask = (updateCheckTask ??= PerformUpdateCheck()); - await waitTask; + bool hasUpdates = await waitTask; lock (updateTaskLock) updateCheckTask = null; + + return hasUpdates; } - protected virtual Task PerformUpdateCheck() => Task.CompletedTask; + /// + /// Performs an asynchronous check for application updates. + /// + /// Whether any update is waiting. May return true if an error occured (there is potentially an update available). + protected virtual Task PerformUpdateCheck() => Task.FromResult(false); private class UpdateCompleteNotification : SimpleNotification { From 5e10ac418bb188044f990d0b96c6544f16eff26b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 13:09:42 +0900 Subject: [PATCH 3662/6909] Add update notifications for iOS builds --- osu.Game/Updater/SimpleUpdateManager.cs | 5 +++++ osu.iOS/OsuGameIOS.cs | 2 ++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index ebb9995c66..48c6722bd9 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -79,6 +79,11 @@ namespace osu.Game.Updater bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage")); break; + case RuntimeInfo.Platform.iOS: + // iOS releases are available via testflight. this link seems to work well enough for now. + // see https://stackoverflow.com/a/32960501 + return "itms-beta://beta.itunes.apple.com/v1/app/1447765923"; + case RuntimeInfo.Platform.Android: // on our testing device this causes the download to magically disappear. //bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".apk")); diff --git a/osu.iOS/OsuGameIOS.cs b/osu.iOS/OsuGameIOS.cs index 3a16f81530..5125ad81e0 100644 --- a/osu.iOS/OsuGameIOS.cs +++ b/osu.iOS/OsuGameIOS.cs @@ -11,5 +11,7 @@ namespace osu.iOS public class OsuGameIOS : OsuGame { public override Version AssemblyVersion => new Version(NSBundle.MainBundle.InfoDictionary["CFBundleVersion"].ToString()); + + protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); } } From de47392e3d11ddf7e7831c029ac4c5bf353007d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 13:18:42 +0900 Subject: [PATCH 3663/6909] Display the "restart to update" notification on checking for update after dismissal --- osu.Desktop/Updater/SquirrelUpdateManager.cs | 50 ++++++++++++++------ 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index b9b148b383..71f9fafe57 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -29,6 +29,11 @@ namespace osu.Desktop.Updater private static readonly Logger logger = Logger.GetLogger("updater"); + /// + /// Whether an update has been downloaded but not yet applied. + /// + private bool updatePending; + [BackgroundDependencyLoader] private void load(NotificationOverlay notification) { @@ -49,9 +54,19 @@ namespace osu.Desktop.Updater updateManager ??= await UpdateManager.GitHubUpdateManager(@"https://github.com/ppy/osu", @"osulazer", null, null, true); var info = await updateManager.CheckForUpdate(!useDeltaPatching); + if (info.ReleasesToApply.Count == 0) + { + if (updatePending) + { + // the user may have dismissed the completion notice, so show it again. + notificationOverlay.Post(new UpdateCompleteNotification(this)); + return true; + } + // no updates available. bail and retry later. return false; + } if (notification == null) { @@ -72,6 +87,7 @@ namespace osu.Desktop.Updater await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f); notification.State = ProgressNotificationState.Completed; + updatePending = true; } catch (Exception e) { @@ -113,10 +129,27 @@ namespace osu.Desktop.Updater updateManager?.Dispose(); } + private class UpdateCompleteNotification : ProgressCompletionNotification + { + [Resolved] + private OsuGame game { get; set; } + + public UpdateCompleteNotification(SquirrelUpdateManager updateManager) + { + Text = @"Update ready to install. Click to restart!"; + + Activated = () => + { + updateManager.PrepareUpdateAsync() + .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit())); + return true; + }; + } + } + private class UpdateProgressNotification : ProgressNotification { private readonly SquirrelUpdateManager updateManager; - private OsuGame game; public UpdateProgressNotification(SquirrelUpdateManager updateManager) { @@ -125,23 +158,12 @@ namespace osu.Desktop.Updater protected override Notification CreateCompletionNotification() { - return new ProgressCompletionNotification - { - Text = @"Update ready to install. Click to restart!", - Activated = () => - { - updateManager.PrepareUpdateAsync() - .ContinueWith(_ => updateManager.Schedule(() => game.GracefullyExit())); - return true; - } - }; + return new UpdateCompleteNotification(updateManager); } [BackgroundDependencyLoader] - private void load(OsuColour colours, OsuGame game) + private void load(OsuColour colours) { - this.game = game; - IconContent.AddRange(new Drawable[] { new Box From 767a2a10bd0f0798eb5b313068c5b43b3f962160 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 13:56:38 +0900 Subject: [PATCH 3664/6909] Fix incorrect sliderendcircle fallback logic Correctly handle the case where a skin has "sliderendcircle.png" but not "sliderendcircleoverlay.png". --- .../Skinning/LegacyMainCirclePiece.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index f051cbfa3b..d556ecb9bc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -49,6 +49,25 @@ namespace osu.Game.Rulesets.Osu.Skinning { OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; + Texture baseTexture; + Texture overlayTexture; + bool allowFallback = false; + + // attempt lookup using priority specification + baseTexture = getTextureWithFallback(string.Empty); + + // if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup. + if (baseTexture == null) + { + allowFallback = true; + baseTexture = getTextureWithFallback(string.Empty); + } + + // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it. + // the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist. + // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin). + overlayTexture = getTextureWithFallback("overlay"); + InternalChildren = new Drawable[] { circleSprites = new Container @@ -60,13 +79,13 @@ namespace osu.Game.Rulesets.Osu.Skinning { hitCircleSprite = new Sprite { - Texture = getTextureWithFallback(string.Empty), + Texture = baseTexture, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, hitCircleOverlay = new Sprite { - Texture = getTextureWithFallback("overlay"), + Texture = overlayTexture, Anchor = Anchor.Centre, Origin = Anchor.Centre, } @@ -101,8 +120,13 @@ namespace osu.Game.Rulesets.Osu.Skinning Texture tex = null; if (!string.IsNullOrEmpty(priorityLookup)) + { tex = skin.GetTexture($"{priorityLookup}{name}"); + if (!allowFallback) + return tex; + } + return tex ?? skin.GetTexture($"hitcircle{name}"); } } From ed982e8dd13f9f4657e1463a7bd19a7aac2f41f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 14:08:55 +0900 Subject: [PATCH 3665/6909] Make stacked hitcircles more visible when using default skin --- osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs index bcf64b81a6..619fea73bc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Origin = Anchor.Centre; Masking = true; - BorderThickness = 10; + BorderThickness = 9; // roughly matches slider borders and makes stacked circles distinctly visible from each other. BorderColour = Color4.White; Child = new Box From 048507478ee199fc155be842a5d008a05dcf1869 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 14:12:46 +0900 Subject: [PATCH 3666/6909] Join declaration and specification --- osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index d556ecb9bc..382d6e53cc 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -49,12 +49,10 @@ namespace osu.Game.Rulesets.Osu.Skinning { OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; - Texture baseTexture; - Texture overlayTexture; bool allowFallback = false; // attempt lookup using priority specification - baseTexture = getTextureWithFallback(string.Empty); + Texture baseTexture = getTextureWithFallback(string.Empty); // if the base texture was not found without a fallback, switch on fallback mode and re-perform the lookup. if (baseTexture == null) @@ -66,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Skinning // at this point, any further texture fetches should be correctly using the priority source if the base texture was retrieved using it. // the flow above handles the case where a sliderendcircle.png is retrieved from the skin, but sliderendcircleoverlay.png doesn't exist. // expected behaviour in this scenario is not showing the overlay, rather than using hitcircleoverlay.png (potentially from the default/fall-through skin). - overlayTexture = getTextureWithFallback("overlay"); + Texture overlayTexture = getTextureWithFallback("overlay"); InternalChildren = new Drawable[] { From 9d7880afdaebdc2f929c5a04f0ce674cfdab8706 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 17:18:41 +0900 Subject: [PATCH 3667/6909] Make SettingsItem conform to IHasCurrentValue --- .../ManiaSettingsSubsection.cs | 4 ++-- .../UI/OsuSettingsSubsection.cs | 6 ++--- osu.Game.Tournament/Components/DateTextBox.cs | 18 +++++++------- .../Screens/Editors/RoundEditorScreen.cs | 12 +++++----- .../Screens/Editors/SeedingEditorScreen.cs | 10 ++++---- .../Screens/Editors/TeamEditorScreen.cs | 12 +++++----- .../Screens/Gameplay/GameplayScreen.cs | 4 ++-- .../Ladder/Components/LadderEditorSettings.cs | 12 +++++----- .../Screens/TeamIntro/SeedingScreen.cs | 2 +- .../Configuration/SettingSourceAttribute.cs | 12 +++++----- .../Sections/Audio/AudioDevicesSettings.cs | 2 +- .../Sections/Audio/MainMenuSettings.cs | 8 +++---- .../Settings/Sections/Audio/OffsetSettings.cs | 2 +- .../Settings/Sections/Audio/VolumeSettings.cs | 8 +++---- .../Sections/Debug/GeneralSettings.cs | 4 ++-- .../Sections/Gameplay/GeneralSettings.cs | 24 +++++++++---------- .../Sections/Gameplay/ModsSettings.cs | 2 +- .../Sections/Gameplay/SongSelectSettings.cs | 10 ++++---- .../Sections/General/LanguageSettings.cs | 2 +- .../Sections/General/LoginSettings.cs | 4 ++-- .../Sections/General/UpdateSettings.cs | 2 +- .../Sections/Graphics/DetailSettings.cs | 8 +++---- .../Sections/Graphics/LayoutSettings.cs | 20 ++++++++-------- .../Sections/Graphics/RendererSettings.cs | 6 ++--- .../Graphics/UserInterfaceSettings.cs | 6 ++--- .../Settings/Sections/Input/MouseSettings.cs | 12 +++++----- .../Settings/Sections/Online/WebSettings.cs | 4 ++-- .../Overlays/Settings/Sections/SkinSection.cs | 12 +++++----- osu.Game/Overlays/Settings/SettingsItem.cs | 4 ++-- .../Edit/Timing/SliderWithTextBoxInput.cs | 8 +++---- osu.Game/Screens/Edit/Timing/TimingSection.cs | 14 +++++------ .../Play/PlayerSettings/PlaybackSettings.cs | 4 ++-- .../Play/PlayerSettings/VisualSettings.cs | 4 ++-- 33 files changed, 131 insertions(+), 131 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index b470405df2..de77af8306 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Mania new SettingsEnumDropdown { LabelText = "Scrolling direction", - Bindable = config.GetBindable(ManiaRulesetSetting.ScrollDirection) + Current = config.GetBindable(ManiaRulesetSetting.ScrollDirection) }, new SettingsSlider { LabelText = "Scroll speed", - Bindable = config.GetBindable(ManiaRulesetSetting.ScrollTime), + Current = config.GetBindable(ManiaRulesetSetting.ScrollTime), KeyboardStep = 5 }, }; diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index 88adf72551..3870f303b4 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -27,17 +27,17 @@ namespace osu.Game.Rulesets.Osu.UI new SettingsCheckbox { LabelText = "Snaking in sliders", - Bindable = config.GetBindable(OsuRulesetSetting.SnakingInSliders) + Current = config.GetBindable(OsuRulesetSetting.SnakingInSliders) }, new SettingsCheckbox { LabelText = "Snaking out sliders", - Bindable = config.GetBindable(OsuRulesetSetting.SnakingOutSliders) + Current = config.GetBindable(OsuRulesetSetting.SnakingOutSliders) }, new SettingsCheckbox { LabelText = "Cursor trail", - Bindable = config.GetBindable(OsuRulesetSetting.ShowCursorTrail) + Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail) }, }; } diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index a1b5ac38ea..5782301a65 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -10,34 +10,34 @@ namespace osu.Game.Tournament.Components { public class DateTextBox : SettingsTextBox { - public new Bindable Bindable + public new Bindable Current { - get => bindable; + get => current; set { - bindable = value.GetBoundCopy(); - bindable.BindValueChanged(dto => - base.Bindable.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); + current = value.GetBoundCopy(); + current.BindValueChanged(dto => + base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); } } // hold a reference to the provided bindable so we don't have to in every settings section. - private Bindable bindable = new Bindable(); + private Bindable current = new Bindable(); public DateTextBox() { - base.Bindable = new Bindable(); + base.Current = new Bindable(); ((OsuTextBox)Control).OnCommit += (sender, newText) => { try { - bindable.Value = DateTimeOffset.Parse(sender.Text); + current.Value = DateTimeOffset.Parse(sender.Text); } catch { // reset textbox content to its last valid state on a parse failure. - bindable.TriggerChange(); + current.TriggerChange(); } }; } diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 8b8078e119..069ddfa4db 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -63,25 +63,25 @@ namespace osu.Game.Tournament.Screens.Editors { LabelText = "Name", Width = 0.33f, - Bindable = Model.Name + Current = Model.Name }, new SettingsTextBox { LabelText = "Description", Width = 0.33f, - Bindable = Model.Description + Current = Model.Description }, new DateTextBox { LabelText = "Start Time", Width = 0.33f, - Bindable = Model.StartDate + Current = Model.StartDate }, new SettingsSlider { LabelText = "Best of", Width = 0.33f, - Bindable = Model.BestOf + Current = Model.BestOf }, new SettingsButton { @@ -186,14 +186,14 @@ namespace osu.Game.Tournament.Screens.Editors LabelText = "Beatmap ID", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = beatmapId, + Current = beatmapId, }, new SettingsTextBox { LabelText = "Mods", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = mods, + Current = mods, }, drawableContainer = new Container { diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 0973a7dc75..7bd8d3f6a0 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -74,13 +74,13 @@ namespace osu.Game.Tournament.Screens.Editors { LabelText = "Mod", Width = 0.33f, - Bindable = Model.Mod + Current = Model.Mod }, new SettingsSlider { LabelText = "Seed", Width = 0.33f, - Bindable = Model.Seed + Current = Model.Seed }, new SettingsButton { @@ -187,21 +187,21 @@ namespace osu.Game.Tournament.Screens.Editors LabelText = "Beatmap ID", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = beatmapId, + Current = beatmapId, }, new SettingsSlider { LabelText = "Seed", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = beatmap.Seed + Current = beatmap.Seed }, new SettingsTextBox { LabelText = "Score", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = score, + Current = score, }, drawableContainer = new Container { diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index dbfcfe4225..7196f47bd6 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -102,31 +102,31 @@ namespace osu.Game.Tournament.Screens.Editors { LabelText = "Name", Width = 0.2f, - Bindable = Model.FullName + Current = Model.FullName }, new SettingsTextBox { LabelText = "Acronym", Width = 0.2f, - Bindable = Model.Acronym + Current = Model.Acronym }, new SettingsTextBox { LabelText = "Flag", Width = 0.2f, - Bindable = Model.FlagName + Current = Model.FlagName }, new SettingsTextBox { LabelText = "Seed", Width = 0.2f, - Bindable = Model.Seed + Current = Model.Seed }, new SettingsSlider { LabelText = "Last Year Placement", Width = 0.33f, - Bindable = Model.LastYearPlacing + Current = Model.LastYearPlacing }, new SettingsButton { @@ -247,7 +247,7 @@ namespace osu.Game.Tournament.Screens.Editors LabelText = "User ID", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = userId, + Current = userId, }, drawableContainer = new Container { diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index e4e3842369..e4ec45c00e 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -113,13 +113,13 @@ namespace osu.Game.Tournament.Screens.Gameplay new SettingsSlider { LabelText = "Chroma width", - Bindable = LadderInfo.ChromaKeyWidth, + Current = LadderInfo.ChromaKeyWidth, KeyboardStep = 1, }, new SettingsSlider { LabelText = "Players per team", - Bindable = LadderInfo.PlayersPerTeam, + Current = LadderInfo.PlayersPerTeam, KeyboardStep = 1, } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs index b60eb814e5..cf4466a2e3 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs @@ -51,15 +51,15 @@ namespace osu.Game.Tournament.Screens.Ladder.Components editorInfo.Selected.ValueChanged += selection => { - roundDropdown.Bindable = selection.NewValue?.Round; + roundDropdown.Current = selection.NewValue?.Round; losersCheckbox.Current = selection.NewValue?.Losers; - dateTimeBox.Bindable = selection.NewValue?.Date; + dateTimeBox.Current = selection.NewValue?.Date; - team1Dropdown.Bindable = selection.NewValue?.Team1; - team2Dropdown.Bindable = selection.NewValue?.Team2; + team1Dropdown.Current = selection.NewValue?.Team1; + team2Dropdown.Current = selection.NewValue?.Team2; }; - roundDropdown.Bindable.ValueChanged += round => + roundDropdown.Current.ValueChanged += round => { if (editorInfo.Selected.Value?.Date.Value < round.NewValue?.StartDate.Value) { @@ -88,7 +88,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { public SettingsRoundDropdown(BindableList rounds) { - Bindable = new Bindable(); + Current = new Bindable(); foreach (var r in rounds.Prepend(new TournamentRound())) add(r); diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index eed3cac9f0..b343608e69 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro new SettingsTeamDropdown(LadderInfo.Teams) { LabelText = "Show specific team", - Bindable = currentTeam, + Current = currentTeam, } } } diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index fe487cb1d0..50069be4b2 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -57,7 +57,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber, + Current = bNumber, KeyboardStep = 0.1f, }; @@ -67,7 +67,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber, + Current = bNumber, KeyboardStep = 0.1f, }; @@ -77,7 +77,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber + Current = bNumber }; break; @@ -86,7 +86,7 @@ namespace osu.Game.Configuration yield return new SettingsCheckbox { LabelText = attr.Label, - Bindable = bBool + Current = bBool }; break; @@ -95,7 +95,7 @@ namespace osu.Game.Configuration yield return new SettingsTextBox { LabelText = attr.Label, - Bindable = bString + Current = bString }; break; @@ -105,7 +105,7 @@ namespace osu.Game.Configuration var dropdown = (Drawable)Activator.CreateInstance(dropdownType); dropdownType.GetProperty(nameof(SettingsDropdown.LabelText))?.SetValue(dropdown, attr.Label); - dropdownType.GetProperty(nameof(SettingsDropdown.Bindable))?.SetValue(dropdown, bindable); + dropdownType.GetProperty(nameof(SettingsDropdown.Current))?.SetValue(dropdown, bindable); yield return dropdown; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index 3da64e0de4..bed74542c9 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio updateItems(); - dropdown.Bindable = audio.AudioDevice; + dropdown.Current = audio.AudioDevice; audio.OnNewDevice += onDeviceChanged; audio.OnLostDevice += onDeviceChanged; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs index a303f93b34..d5de32ed05 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs @@ -21,23 +21,23 @@ namespace osu.Game.Overlays.Settings.Sections.Audio new SettingsCheckbox { LabelText = "Interface voices", - Bindable = config.GetBindable(OsuSetting.MenuVoice) + Current = config.GetBindable(OsuSetting.MenuVoice) }, new SettingsCheckbox { LabelText = "osu! music theme", - Bindable = config.GetBindable(OsuSetting.MenuMusic) + Current = config.GetBindable(OsuSetting.MenuMusic) }, new SettingsDropdown { LabelText = "Intro sequence", - Bindable = config.GetBindable(OsuSetting.IntroSequence), + Current = config.GetBindable(OsuSetting.IntroSequence), Items = Enum.GetValues(typeof(IntroSequence)).Cast() }, new SettingsDropdown { LabelText = "Background source", - Bindable = config.GetBindable(OsuSetting.MenuBackgroundSource), + Current = config.GetBindable(OsuSetting.MenuBackgroundSource), Items = Enum.GetValues(typeof(BackgroundSource)).Cast() } }; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index aaa4302553..c9a81b955b 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio new SettingsSlider { LabelText = "Audio offset", - Bindable = config.GetBindable(OsuSetting.AudioOffset), + Current = config.GetBindable(OsuSetting.AudioOffset), KeyboardStep = 1f }, new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs index bda677ecd6..c172a76ab9 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs @@ -20,28 +20,28 @@ namespace osu.Game.Overlays.Settings.Sections.Audio new SettingsSlider { LabelText = "Master", - Bindable = audio.Volume, + Current = audio.Volume, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Master (window inactive)", - Bindable = config.GetBindable(OsuSetting.VolumeInactive), + Current = config.GetBindable(OsuSetting.VolumeInactive), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Effect", - Bindable = audio.VolumeSample, + Current = audio.VolumeSample, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Music", - Bindable = audio.VolumeTrack, + Current = audio.VolumeTrack, KeyboardStep = 0.01f, DisplayAsPercentage = true }, diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs index 9edb18e065..f05b876d8f 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs @@ -19,12 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Debug new SettingsCheckbox { LabelText = "Show log overlay", - Bindable = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) + Current = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) }, new SettingsCheckbox { LabelText = "Bypass front-to-back render pass", - Bindable = config.GetBindable(DebugSetting.BypassFrontToBackPass) + Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) } }; } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 0149e6c3a6..73968761e2 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -21,62 +21,62 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsSlider { LabelText = "Background dim", - Bindable = config.GetBindable(OsuSetting.DimLevel), + Current = config.GetBindable(OsuSetting.DimLevel), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Background blur", - Bindable = config.GetBindable(OsuSetting.BlurLevel), + Current = config.GetBindable(OsuSetting.BlurLevel), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsCheckbox { LabelText = "Lighten playfield during breaks", - Bindable = config.GetBindable(OsuSetting.LightenDuringBreaks) + Current = config.GetBindable(OsuSetting.LightenDuringBreaks) }, new SettingsCheckbox { LabelText = "Show score overlay", - Bindable = config.GetBindable(OsuSetting.ShowInterface) + Current = config.GetBindable(OsuSetting.ShowInterface) }, new SettingsCheckbox { LabelText = "Show difficulty graph on progress bar", - Bindable = config.GetBindable(OsuSetting.ShowProgressGraph) + Current = config.GetBindable(OsuSetting.ShowProgressGraph) }, new SettingsCheckbox { LabelText = "Show health display even when you can't fail", - Bindable = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), + Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), Keywords = new[] { "hp", "bar" } }, new SettingsCheckbox { LabelText = "Fade playfield to red when health is low", - Bindable = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), + Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), }, new SettingsCheckbox { LabelText = "Always show key overlay", - Bindable = config.GetBindable(OsuSetting.KeyOverlay) + Current = config.GetBindable(OsuSetting.KeyOverlay) }, new SettingsCheckbox { LabelText = "Positional hitsounds", - Bindable = config.GetBindable(OsuSetting.PositionalHitSounds) + Current = config.GetBindable(OsuSetting.PositionalHitSounds) }, new SettingsEnumDropdown { LabelText = "Score meter type", - Bindable = config.GetBindable(OsuSetting.ScoreMeter) + Current = config.GetBindable(OsuSetting.ScoreMeter) }, new SettingsEnumDropdown { LabelText = "Score display mode", - Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) + Current = config.GetBindable(OsuSetting.ScoreDisplayMode) } }; @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Add(new SettingsCheckbox { LabelText = "Disable Windows key during gameplay", - Bindable = config.GetBindable(OsuSetting.GameplayDisableWinKey) + Current = config.GetBindable(OsuSetting.GameplayDisableWinKey) }); } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs index 0babb98066..2b2fb9cef7 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = "Increase visibility of first object when visual impairment mods are enabled", - Bindable = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility), + Current = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility), }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs index 0c42247993..b26876556e 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs @@ -31,31 +31,31 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = "Right mouse drag to absolute scroll", - Bindable = config.GetBindable(OsuSetting.SongSelectRightMouseScroll), + Current = config.GetBindable(OsuSetting.SongSelectRightMouseScroll), }, new SettingsCheckbox { LabelText = "Show converted beatmaps", - Bindable = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), + Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), }, new SettingsSlider { LabelText = "Display beatmaps from", - Bindable = config.GetBindable(OsuSetting.DisplayStarsMinimum), + Current = config.GetBindable(OsuSetting.DisplayStarsMinimum), KeyboardStep = 0.1f, Keywords = new[] { "minimum", "maximum", "star", "difficulty" } }, new SettingsSlider { LabelText = "up to", - Bindable = config.GetBindable(OsuSetting.DisplayStarsMaximum), + Current = config.GetBindable(OsuSetting.DisplayStarsMaximum), KeyboardStep = 0.1f, Keywords = new[] { "minimum", "maximum", "star", "difficulty" } }, new SettingsEnumDropdown { LabelText = "Random selection algorithm", - Bindable = config.GetBindable(OsuSetting.RandomSelectAlgorithm), + Current = config.GetBindable(OsuSetting.RandomSelectAlgorithm), } }; } diff --git a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs index 236bfbecc3..44e42ecbfe 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections.General new SettingsCheckbox { LabelText = "Prefer metadata in original language", - Bindable = frameworkConfig.GetBindable(FrameworkSetting.ShowUnicode) + Current = frameworkConfig.GetBindable(FrameworkSetting.ShowUnicode) }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index f96e204f62..9e358d0cf5 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -240,12 +240,12 @@ namespace osu.Game.Overlays.Settings.Sections.General new SettingsCheckbox { LabelText = "Remember username", - Bindable = config.GetBindable(OsuSetting.SaveUsername), + Current = config.GetBindable(OsuSetting.SaveUsername), }, new SettingsCheckbox { LabelText = "Stay signed in", - Bindable = config.GetBindable(OsuSetting.SavePassword), + Current = config.GetBindable(OsuSetting.SavePassword), }, new Container { diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 9c7d0b0be4..a59a6b00b9 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(new SettingsEnumDropdown { LabelText = "Release stream", - Bindable = config.GetBindable(OsuSetting.ReleaseStream), + Current = config.GetBindable(OsuSetting.ReleaseStream), }); if (updateManager?.CanCheckForUpdate == true) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index 3089040f96..30caa45995 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -19,22 +19,22 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsCheckbox { LabelText = "Storyboard / Video", - Bindable = config.GetBindable(OsuSetting.ShowStoryboard) + Current = config.GetBindable(OsuSetting.ShowStoryboard) }, new SettingsCheckbox { LabelText = "Hit Lighting", - Bindable = config.GetBindable(OsuSetting.HitLighting) + Current = config.GetBindable(OsuSetting.HitLighting) }, new SettingsEnumDropdown { LabelText = "Screenshot format", - Bindable = config.GetBindable(OsuSetting.ScreenshotFormat) + Current = config.GetBindable(OsuSetting.ScreenshotFormat) }, new SettingsCheckbox { LabelText = "Show menu cursor in screenshots", - Bindable = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor) + Current = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor) } }; } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 4312b319c0..14b8dbfac0 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics windowModeDropdown = new SettingsDropdown { LabelText = "Screen mode", - Bindable = config.GetBindable(FrameworkSetting.WindowMode), + Current = config.GetBindable(FrameworkSetting.WindowMode), ItemSource = windowModes, }, resolutionSettingsContainer = new Container @@ -74,14 +74,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { LabelText = "UI Scaling", TransferValueOnCommit = true, - Bindable = osuConfig.GetBindable(OsuSetting.UIScale), + Current = osuConfig.GetBindable(OsuSetting.UIScale), KeyboardStep = 0.01f, Keywords = new[] { "scale", "letterbox" }, }, new SettingsEnumDropdown { LabelText = "Screen Scaling", - Bindable = osuConfig.GetBindable(OsuSetting.Scaling), + Current = osuConfig.GetBindable(OsuSetting.Scaling), Keywords = new[] { "scale", "letterbox" }, }, scalingSettings = new FillFlowContainer> @@ -97,28 +97,28 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsSlider { LabelText = "Horizontal position", - Bindable = scalingPositionX, + Current = scalingPositionX, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Vertical position", - Bindable = scalingPositionY, + Current = scalingPositionY, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Horizontal scale", - Bindable = scalingSizeX, + Current = scalingSizeX, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Vertical scale", - Bindable = scalingSizeY, + Current = scalingSizeY, KeyboardStep = 0.01f, DisplayAsPercentage = true }, @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, }; - scalingSettings.ForEach(s => bindPreviewEvent(s.Bindable)); + scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); var resolutions = getResolutions(); @@ -137,10 +137,10 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = "Resolution", ShowsDefaultIndicator = false, Items = resolutions, - Bindable = sizeFullscreen + Current = sizeFullscreen }; - windowModeDropdown.Bindable.BindValueChanged(mode => + windowModeDropdown.Current.BindValueChanged(mode => { if (mode.NewValue == WindowMode.Fullscreen) { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index 69ff9b43e5..8773e6763c 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -23,17 +23,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsEnumDropdown { LabelText = "Frame limiter", - Bindable = config.GetBindable(FrameworkSetting.FrameSync) + Current = config.GetBindable(FrameworkSetting.FrameSync) }, new SettingsEnumDropdown { LabelText = "Threading mode", - Bindable = config.GetBindable(FrameworkSetting.ExecutionMode) + Current = config.GetBindable(FrameworkSetting.ExecutionMode) }, new SettingsCheckbox { LabelText = "Show FPS", - Bindable = osuConfig.GetBindable(OsuSetting.ShowFpsDisplay) + Current = osuConfig.GetBindable(OsuSetting.ShowFpsDisplay) }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs index a8953ac3a2..38c30ddd64 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs @@ -20,17 +20,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsCheckbox { LabelText = "Rotate cursor when dragging", - Bindable = config.GetBindable(OsuSetting.CursorRotation) + Current = config.GetBindable(OsuSetting.CursorRotation) }, new SettingsCheckbox { LabelText = "Parallax", - Bindable = config.GetBindable(OsuSetting.MenuParallax) + Current = config.GetBindable(OsuSetting.MenuParallax) }, new SettingsSlider { LabelText = "Hold-to-confirm activation time", - Bindable = config.GetBindable(OsuSetting.UIHoldActivationDelay), + Current = config.GetBindable(OsuSetting.UIHoldActivationDelay), KeyboardStep = 50 }, }; diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index d27ab63fb7..5227e328ec 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -35,32 +35,32 @@ namespace osu.Game.Overlays.Settings.Sections.Input new SettingsCheckbox { LabelText = "Raw input", - Bindable = rawInputToggle + Current = rawInputToggle }, new SensitivitySetting { LabelText = "Cursor sensitivity", - Bindable = sensitivityBindable + Current = sensitivityBindable }, new SettingsCheckbox { LabelText = "Map absolute input to window", - Bindable = config.GetBindable(FrameworkSetting.MapAbsoluteInputToWindow) + Current = config.GetBindable(FrameworkSetting.MapAbsoluteInputToWindow) }, new SettingsEnumDropdown { LabelText = "Confine mouse cursor to window", - Bindable = config.GetBindable(FrameworkSetting.ConfineMouseMode), + Current = config.GetBindable(FrameworkSetting.ConfineMouseMode), }, new SettingsCheckbox { LabelText = "Disable mouse wheel during gameplay", - Bindable = osuConfig.GetBindable(OsuSetting.MouseDisableWheel) + Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel) }, new SettingsCheckbox { LabelText = "Disable mouse buttons during gameplay", - Bindable = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) + Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) }, }; diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index 23513eade8..6461bd7b93 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -19,13 +19,13 @@ namespace osu.Game.Overlays.Settings.Sections.Online new SettingsCheckbox { LabelText = "Warn about opening external links", - Bindable = config.GetBindable(OsuSetting.ExternalLinkWarning) + Current = config.GetBindable(OsuSetting.ExternalLinkWarning) }, new SettingsCheckbox { LabelText = "Prefer downloads without video", Keywords = new[] { "no-video" }, - Bindable = config.GetBindable(OsuSetting.PreferNoVideo) + Current = config.GetBindable(OsuSetting.PreferNoVideo) }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 596d3a9801..1ade4befdc 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -47,29 +47,29 @@ namespace osu.Game.Overlays.Settings.Sections new SettingsSlider { LabelText = "Menu cursor size", - Bindable = config.GetBindable(OsuSetting.MenuCursorSize), + Current = config.GetBindable(OsuSetting.MenuCursorSize), KeyboardStep = 0.01f }, new SettingsSlider { LabelText = "Gameplay cursor size", - Bindable = config.GetBindable(OsuSetting.GameplayCursorSize), + Current = config.GetBindable(OsuSetting.GameplayCursorSize), KeyboardStep = 0.01f }, new SettingsCheckbox { LabelText = "Adjust gameplay cursor size based on current beatmap", - Bindable = config.GetBindable(OsuSetting.AutoCursorSize) + Current = config.GetBindable(OsuSetting.AutoCursorSize) }, new SettingsCheckbox { LabelText = "Beatmap skins", - Bindable = config.GetBindable(OsuSetting.BeatmapSkins) + Current = config.GetBindable(OsuSetting.BeatmapSkins) }, new SettingsCheckbox { LabelText = "Beatmap hitsounds", - Bindable = config.GetBindable(OsuSetting.BeatmapHitsounds) + Current = config.GetBindable(OsuSetting.BeatmapHitsounds) }, }; @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Settings.Sections config.BindWith(OsuSetting.Skin, configBindable); - skinDropdown.Bindable = dropdownBindable; + skinDropdown.Current = dropdownBindable; skinDropdown.Items = skins.GetAllUsableSkins().ToArray(); // Todo: This should not be necessary when OsuConfigManager is databased diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index c2dd40d2a6..ad6aaafd9d 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -21,7 +21,7 @@ using osuTK; namespace osu.Game.Overlays.Settings { - public abstract class SettingsItem : Container, IFilterable, ISettingsItem + public abstract class SettingsItem : Container, IFilterable, ISettingsItem, IHasCurrentValue { protected abstract Drawable CreateControl(); @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Settings } } - public virtual Bindable Bindable + public virtual Bindable Current { get => controlWithCurrent.Current; set => controlWithCurrent.Current = value; diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs index d5afc8978d..f2f9f76143 100644 --- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs @@ -11,7 +11,7 @@ using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Edit.Timing { - internal class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue + public class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue where T : struct, IEquatable, IComparable, IConvertible { private readonly SettingsSlider slider; @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Timing try { - slider.Bindable.Parse(t.Text); + slider.Current.Parse(t.Text); } catch { @@ -71,8 +71,8 @@ namespace osu.Game.Screens.Edit.Timing public Bindable Current { - get => slider.Bindable; - set => slider.Bindable = value; + get => slider.Current; + set => slider.Current = value; } } } diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 2ab8703cc4..1ae2a86885 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -36,14 +36,14 @@ namespace osu.Game.Screens.Edit.Timing { if (point.NewValue != null) { - bpmSlider.Bindable = point.NewValue.BeatLengthBindable; - bpmSlider.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState()); + bpmSlider.Current = point.NewValue.BeatLengthBindable; + bpmSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; // no need to hook change handler here as it's the same bindable as above - timeSignature.Bindable = point.NewValue.TimeSignatureBindable; - timeSignature.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState()); + timeSignature.Current = point.NewValue.TimeSignatureBindable; + timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } @@ -121,14 +121,14 @@ namespace osu.Game.Screens.Edit.Timing beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true); bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); - base.Bindable = bpmBindable; + base.Current = bpmBindable; TransferValueOnCommit = true; } - public override Bindable Bindable + public override Bindable Current { - get => base.Bindable; + get => base.Current; set { // incoming will be beat length, not bpm diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 24ddc277cd..16e29ac3c8 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -51,14 +51,14 @@ namespace osu.Game.Screens.Play.PlayerSettings } }, }, - rateSlider = new PlayerSliderBar { Bindable = UserPlaybackRate } + rateSlider = new PlayerSliderBar { Current = UserPlaybackRate } }; } protected override void LoadComplete() { base.LoadComplete(); - rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); + rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index e06cf5c6d5..8f29fe7893 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -50,8 +50,8 @@ namespace osu.Game.Screens.Play.PlayerSettings [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - dimSliderBar.Bindable = config.GetBindable(OsuSetting.DimLevel); - blurSliderBar.Bindable = config.GetBindable(OsuSetting.BlurLevel); + dimSliderBar.Current = config.GetBindable(OsuSetting.DimLevel); + blurSliderBar.Current = config.GetBindable(OsuSetting.BlurLevel); showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); From 28756d862b630eb5b28eecf8c9373dfca97be24b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 14:22:53 +0900 Subject: [PATCH 3668/6909] Add placeholder text/colour when no beatmap background is specified yet --- osu.Game/Screens/Edit/Setup/SetupScreen.cs | 27 ++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 3d94737e59..1e9ebec41d 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -59,8 +59,11 @@ namespace osu.Game.Screens.Edit.Setup { } + [Resolved] + private OsuColour colours { get; set; } + [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load() { Container audioTrackFileChooserContainer = new Container { @@ -187,7 +190,27 @@ namespace osu.Game.Screens.Edit.Setup FillMode = FillMode.Fill, }, background => { - backgroundSpriteContainer.Child = background; + if (background.Texture != null) + backgroundSpriteContainer.Child = background; + else + { + backgroundSpriteContainer.Children = new Drawable[] + { + new Box + { + Colour = colours.GreySeafoamDarker, + RelativeSizeAxes = Axes.Both, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 24)) + { + Text = "Drag image here to set beatmap background!", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.X, + } + }; + } + background.FadeInFromZero(500); }); } From 87bf3bdc161a601893e25ab7899ab2151952e39f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 16:40:47 +0900 Subject: [PATCH 3669/6909] Add the most basic implementation of LabelledSliderBar feasible --- .../TestSceneLabelledSliderBar.cs | 46 +++++++++++++++++++ .../UserInterfaceV2/LabelledSliderBar.cs | 24 ++++++++++ 2 files changed, 70 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs new file mode 100644 index 0000000000..393420e700 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneLabelledSliderBar : OsuTestScene + { + [TestCase(false)] + [TestCase(true)] + public void TestSliderBar(bool hasDescription) => createSliderBar(hasDescription); + + private void createSliderBar(bool hasDescription = false) + { + AddStep("create component", () => + { + LabelledSliderBar component; + + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Child = component = new LabelledSliderBar + { + Current = new BindableDouble(5) + { + MinValue = 0, + MaxValue = 10, + Precision = 1, + } + } + }; + + component.Label = "a sample component"; + component.Description = hasDescription ? "this text describes the component" : string.Empty; + }); + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs new file mode 100644 index 0000000000..cba94e314b --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class LabelledSliderBar : LabelledComponent, TNumber> + where TNumber : struct, IEquatable, IComparable, IConvertible + { + public LabelledSliderBar() + : base(true) + { + } + + protected override SettingsSlider CreateComponent() => new SettingsSlider + { + TransferValueOnCommit = true, + RelativeSizeAxes = Axes.X, + }; + } +} From 98fe5f78ee956d3765324a1d8482fb1945a63673 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 15:17:15 +0900 Subject: [PATCH 3670/6909] Split setup screen up into sections (and use a SectionContainer) --- .../Editing/TestSceneEditorBeatmapCreation.cs | 2 +- .../Edit/Setup/FileChooserLabelledTextBox.cs | 73 ++++ .../Screens/Edit/Setup/MetadataSection.cs | 71 ++++ .../Screens/Edit/Setup/ResourcesSection.cs | 211 ++++++++++++ osu.Game/Screens/Edit/Setup/SetupScreen.cs | 319 +----------------- osu.Game/Screens/Edit/Setup/SetupSection.cs | 43 +++ 6 files changed, 417 insertions(+), 302 deletions(-) create mode 100644 osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs create mode 100644 osu.Game/Screens/Edit/Setup/MetadataSection.cs create mode 100644 osu.Game/Screens/Edit/Setup/ResourcesSection.cs create mode 100644 osu.Game/Screens/Edit/Setup/SetupSection.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs index 13a3195824..7584c74c71 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorBeatmapCreation.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Editing using (var zip = ZipArchive.Open(temp)) zip.WriteToDirectory(extractedFolder); - bool success = setup.ChangeAudioTrack(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")); + bool success = setup.ChildrenOfType().First().ChangeAudioTrack(Path.Combine(extractedFolder, "03. Renatus - Soleily 192kbps.mp3")); File.Delete(temp); Directory.Delete(extractedFolder, true); diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs new file mode 100644 index 0000000000..b802b3405a --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.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 System.IO; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Edit.Setup +{ + /// + /// A labelled textbox which reveals an inline file chooser when clicked. + /// + internal class FileChooserLabelledTextBox : LabelledTextBox + { + public Container Target; + + private readonly IBindable currentFile = new Bindable(); + + public FileChooserLabelledTextBox() + { + currentFile.BindValueChanged(onFileSelected); + } + + private void onFileSelected(ValueChangedEvent file) + { + if (file.NewValue == null) + return; + + Target.Clear(); + Current.Value = file.NewValue.FullName; + } + + protected override OsuTextBox CreateTextBox() => + new FileChooserOsuTextBox + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + CornerRadius = CORNER_RADIUS, + OnFocused = DisplayFileChooser + }; + + public void DisplayFileChooser() + { + Target.Child = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions) + { + RelativeSizeAxes = Axes.X, + Height = 400, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + CurrentFile = { BindTarget = currentFile } + }; + } + + internal class FileChooserOsuTextBox : OsuTextBox + { + public Action OnFocused; + + protected override void OnFocus(FocusEvent e) + { + OnFocused?.Invoke(); + base.OnFocus(e); + + GetContainingInputManager().TriggerFocusContention(this); + } + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs new file mode 100644 index 0000000000..31a2c2ce1a --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Screens.Edit.Setup +{ + internal class MetadataSection : SetupSection + { + private LabelledTextBox artistTextBox; + private LabelledTextBox titleTextBox; + private LabelledTextBox creatorTextBox; + private LabelledTextBox difficultyTextBox; + + [BackgroundDependencyLoader] + private void load() + { + Flow.Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Beatmap metadata" + }, + artistTextBox = new LabelledTextBox + { + Label = "Artist", + Current = { Value = Beatmap.Value.Metadata.Artist }, + TabbableContentContainer = this + }, + titleTextBox = new LabelledTextBox + { + Label = "Title", + Current = { Value = Beatmap.Value.Metadata.Title }, + TabbableContentContainer = this + }, + creatorTextBox = new LabelledTextBox + { + Label = "Creator", + Current = { Value = Beatmap.Value.Metadata.AuthorString }, + TabbableContentContainer = this + }, + difficultyTextBox = new LabelledTextBox + { + Label = "Difficulty Name", + Current = { Value = Beatmap.Value.BeatmapInfo.Version }, + TabbableContentContainer = this + }, + }; + + foreach (var item in Flow.OfType()) + item.OnCommit += onCommit; + } + + private void onCommit(TextBox sender, bool newText) + { + if (!newText) return; + + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Value.Metadata.Artist = artistTextBox.Current.Value; + Beatmap.Value.Metadata.Title = titleTextBox.Current.Value; + Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value; + Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs new file mode 100644 index 0000000000..86d7968856 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -0,0 +1,211 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Threading.Tasks; +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.Beatmaps.Drawables; +using osu.Game.Database; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Overlays; + +namespace osu.Game.Screens.Edit.Setup +{ + internal class ResourcesSection : SetupSection, ICanAcceptFiles + { + private LabelledTextBox audioTrackTextBox; + private Container backgroundSpriteContainer; + + public IEnumerable HandledExtensions => ImageExtensions.Concat(AudioExtensions); + + public static string[] ImageExtensions { get; } = { ".jpg", ".jpeg", ".png" }; + + public static string[] AudioExtensions { get; } = { ".mp3", ".ogg" }; + + [Resolved] + private OsuGameBase game { get; set; } + + [Resolved] + private MusicController music { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Resolved(canBeNull: true)] + private Editor editor { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Container audioTrackFileChooserContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }; + + Flow.Children = new Drawable[] + { + backgroundSpriteContainer = new Container + { + RelativeSizeAxes = Axes.X, + Height = 250, + Masking = true, + CornerRadius = 10, + }, + new OsuSpriteText + { + Text = "Resources" + }, + audioTrackTextBox = new FileChooserLabelledTextBox + { + Label = "Audio Track", + Current = { Value = Beatmap.Value.Metadata.AudioFile ?? "Click to select a track" }, + Target = audioTrackFileChooserContainer, + TabbableContentContainer = this + }, + audioTrackFileChooserContainer, + }; + + updateBackgroundSprite(); + + audioTrackTextBox.Current.BindValueChanged(audioTrackChanged); + } + + Task ICanAcceptFiles.Import(params string[] paths) + { + Schedule(() => + { + var firstFile = new FileInfo(paths.First()); + + if (ImageExtensions.Contains(firstFile.Extension)) + { + ChangeBackgroundImage(firstFile.FullName); + } + else if (AudioExtensions.Contains(firstFile.Extension)) + { + audioTrackTextBox.Text = firstFile.FullName; + } + }); + return Task.CompletedTask; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + game.RegisterImportHandler(this); + } + + public bool ChangeBackgroundImage(string path) + { + var info = new FileInfo(path); + + if (!info.Exists) + return false; + + var set = Beatmap.Value.BeatmapSetInfo; + + // remove the previous background for now. + // in the future we probably want to check if this is being used elsewhere (other difficulties?) + var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.BackgroundFile); + + using (var stream = info.OpenRead()) + { + if (oldFile != null) + beatmaps.ReplaceFile(set, oldFile, stream, info.Name); + else + beatmaps.AddFile(set, stream, info.Name); + } + + Beatmap.Value.Metadata.BackgroundFile = info.Name; + updateBackgroundSprite(); + + return true; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + game?.UnregisterImportHandler(this); + } + + public bool ChangeAudioTrack(string path) + { + var info = new FileInfo(path); + + if (!info.Exists) + return false; + + var set = Beatmap.Value.BeatmapSetInfo; + + // remove the previous audio track for now. + // in the future we probably want to check if this is being used elsewhere (other difficulties?) + var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile); + + using (var stream = info.OpenRead()) + { + if (oldFile != null) + beatmaps.ReplaceFile(set, oldFile, stream, info.Name); + else + beatmaps.AddFile(set, stream, info.Name); + } + + Beatmap.Value.Metadata.AudioFile = info.Name; + + music.ReloadCurrentTrack(); + + editor?.UpdateClockSource(); + return true; + } + + private void audioTrackChanged(ValueChangedEvent filePath) + { + if (!ChangeAudioTrack(filePath.NewValue)) + audioTrackTextBox.Current.Value = filePath.OldValue; + } + + private void updateBackgroundSprite() + { + LoadComponentAsync(new BeatmapBackgroundSprite(Beatmap.Value) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + }, background => + { + if (background.Texture != null) + backgroundSpriteContainer.Child = background; + else + { + backgroundSpriteContainer.Children = new Drawable[] + { + new Box + { + Colour = Colours.GreySeafoamDarker, + RelativeSizeAxes = Axes.Both, + }, + new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 24)) + { + Text = "Drag image here to set beatmap background!", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.X, + } + }; + } + + background.FadeInFromZero(500); + }); + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index 1e9ebec41d..cd4f6733c0 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -1,76 +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.IO; -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.Graphics.Shapes; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Database; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.UserInterfaceV2; using osu.Game.Overlays; -using osuTK; namespace osu.Game.Screens.Edit.Setup { - public class SetupScreen : EditorScreen, ICanAcceptFiles + public class SetupScreen : EditorScreen { - public IEnumerable HandledExtensions => ImageExtensions.Concat(AudioExtensions); - - public static string[] ImageExtensions { get; } = { ".jpg", ".jpeg", ".png" }; - - public static string[] AudioExtensions { get; } = { ".mp3", ".ogg" }; - - private FillFlowContainer flow; - private LabelledTextBox artistTextBox; - private LabelledTextBox titleTextBox; - private LabelledTextBox creatorTextBox; - private LabelledTextBox difficultyTextBox; - private LabelledTextBox audioTrackTextBox; - private Container backgroundSpriteContainer; - [Resolved] - private OsuGameBase game { get; set; } + private OsuColour colours { get; set; } - [Resolved] - private MusicController music { get; set; } - - [Resolved] - private BeatmapManager beatmaps { get; set; } - - [Resolved(canBeNull: true)] - private Editor editor { get; set; } + [Cached] + protected readonly OverlayColourProvider ColourProvider; public SetupScreen() : base(EditorScreenMode.SongSetup) { + ColourProvider = new OverlayColourProvider(OverlayColourScheme.Green); } - [Resolved] - private OsuColour colours { get; set; } - [BackgroundDependencyLoader] private void load() { - Container audioTrackFileChooserContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }; - Child = new Container { RelativeSizeAxes = Axes.Both, @@ -87,273 +44,33 @@ namespace osu.Game.Screens.Edit.Setup Colour = colours.GreySeafoamDark, RelativeSizeAxes = Axes.Both, }, - new OsuScrollContainer + new SectionsContainer { + FixedHeader = new SetupScreenHeader(), RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding(10), - Child = flow = new FillFlowContainer + Children = new SetupSection[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(20), - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - backgroundSpriteContainer = new Container - { - RelativeSizeAxes = Axes.X, - Height = 250, - Masking = true, - CornerRadius = 10, - }, - new OsuSpriteText - { - Text = "Resources" - }, - audioTrackTextBox = new FileChooserLabelledTextBox - { - Label = "Audio Track", - Current = { Value = Beatmap.Value.Metadata.AudioFile ?? "Click to select a track" }, - Target = audioTrackFileChooserContainer, - TabbableContentContainer = this - }, - audioTrackFileChooserContainer, - new OsuSpriteText - { - Text = "Beatmap metadata" - }, - artistTextBox = new LabelledTextBox - { - Label = "Artist", - Current = { Value = Beatmap.Value.Metadata.Artist }, - TabbableContentContainer = this - }, - titleTextBox = new LabelledTextBox - { - Label = "Title", - Current = { Value = Beatmap.Value.Metadata.Title }, - TabbableContentContainer = this - }, - creatorTextBox = new LabelledTextBox - { - Label = "Creator", - Current = { Value = Beatmap.Value.Metadata.AuthorString }, - TabbableContentContainer = this - }, - difficultyTextBox = new LabelledTextBox - { - Label = "Difficulty Name", - Current = { Value = Beatmap.Value.BeatmapInfo.Version }, - TabbableContentContainer = this - }, - } - }, + new ResourcesSection(), + new MetadataSection(), + } }, } } }; - - updateBackgroundSprite(); - - audioTrackTextBox.Current.BindValueChanged(audioTrackChanged); - - foreach (var item in flow.OfType()) - item.OnCommit += onCommit; - } - - Task ICanAcceptFiles.Import(params string[] paths) - { - Schedule(() => - { - var firstFile = new FileInfo(paths.First()); - - if (ImageExtensions.Contains(firstFile.Extension)) - { - ChangeBackgroundImage(firstFile.FullName); - } - else if (AudioExtensions.Contains(firstFile.Extension)) - { - audioTrackTextBox.Text = firstFile.FullName; - } - }); - - return Task.CompletedTask; - } - - private void updateBackgroundSprite() - { - LoadComponentAsync(new BeatmapBackgroundSprite(Beatmap.Value) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - }, background => - { - if (background.Texture != null) - backgroundSpriteContainer.Child = background; - else - { - backgroundSpriteContainer.Children = new Drawable[] - { - new Box - { - Colour = colours.GreySeafoamDarker, - RelativeSizeAxes = Axes.Both, - }, - new OsuTextFlowContainer(t => t.Font = OsuFont.Default.With(size: 24)) - { - Text = "Drag image here to set beatmap background!", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AutoSizeAxes = Axes.X, - } - }; - } - - background.FadeInFromZero(500); - }); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - game.RegisterImportHandler(this); - } - - public bool ChangeBackgroundImage(string path) - { - var info = new FileInfo(path); - - if (!info.Exists) - return false; - - var set = Beatmap.Value.BeatmapSetInfo; - - // remove the previous background for now. - // in the future we probably want to check if this is being used elsewhere (other difficulties?) - var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.BackgroundFile); - - using (var stream = info.OpenRead()) - { - if (oldFile != null) - beatmaps.ReplaceFile(set, oldFile, stream, info.Name); - else - beatmaps.AddFile(set, stream, info.Name); - } - - Beatmap.Value.Metadata.BackgroundFile = info.Name; - updateBackgroundSprite(); - - return true; - } - - public bool ChangeAudioTrack(string path) - { - var info = new FileInfo(path); - - if (!info.Exists) - return false; - - var set = Beatmap.Value.BeatmapSetInfo; - - // remove the previous audio track for now. - // in the future we probably want to check if this is being used elsewhere (other difficulties?) - var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile); - - using (var stream = info.OpenRead()) - { - if (oldFile != null) - beatmaps.ReplaceFile(set, oldFile, stream, info.Name); - else - beatmaps.AddFile(set, stream, info.Name); - } - - Beatmap.Value.Metadata.AudioFile = info.Name; - - music.ReloadCurrentTrack(); - - editor?.UpdateClockSource(); - return true; - } - - private void audioTrackChanged(ValueChangedEvent filePath) - { - if (!ChangeAudioTrack(filePath.NewValue)) - audioTrackTextBox.Current.Value = filePath.OldValue; - } - - private void onCommit(TextBox sender, bool newText) - { - if (!newText) return; - - // for now, update these on commit rather than making BeatmapMetadata bindables. - // after switching database engines we can reconsider if switching to bindables is a good direction. - Beatmap.Value.Metadata.Artist = artistTextBox.Current.Value; - Beatmap.Value.Metadata.Title = titleTextBox.Current.Value; - Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value; - Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - game?.UnregisterImportHandler(this); } } - internal class FileChooserLabelledTextBox : LabelledTextBox + internal class SetupScreenHeader : OverlayHeader { - public Container Target; + protected override OverlayTitle CreateTitle() => new SetupScreenTitle(); - private readonly IBindable currentFile = new Bindable(); - - public FileChooserLabelledTextBox() + private class SetupScreenTitle : OverlayTitle { - currentFile.BindValueChanged(onFileSelected); - } - - private void onFileSelected(ValueChangedEvent file) - { - if (file.NewValue == null) - return; - - Target.Clear(); - Current.Value = file.NewValue.FullName; - } - - protected override OsuTextBox CreateTextBox() => - new FileChooserOsuTextBox + public SetupScreenTitle() { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - CornerRadius = CORNER_RADIUS, - OnFocused = DisplayFileChooser - }; - - public void DisplayFileChooser() - { - Target.Child = new FileSelector(validFileExtensions: SetupScreen.AudioExtensions) - { - RelativeSizeAxes = Axes.X, - Height = 400, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - CurrentFile = { BindTarget = currentFile } - }; - } - - internal class FileChooserOsuTextBox : OsuTextBox - { - public Action OnFocused; - - protected override void OnFocus(FocusEvent e) - { - OnFocused?.Invoke(); - base.OnFocus(e); - - GetContainingInputManager().TriggerFocusContention(this); + Title = "beatmap setup"; + Description = "change general settings of your beatmap"; + IconTexture = "Icons/Hexacons/social"; } } } diff --git a/osu.Game/Screens/Edit/Setup/SetupSection.cs b/osu.Game/Screens/Edit/Setup/SetupSection.cs new file mode 100644 index 0000000000..54e383a4d8 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/SetupSection.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 osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Screens.Edit.Setup +{ + internal class SetupSection : Container + { + protected FillFlowContainer Flow; + + [Resolved] + protected OsuColour Colours { get; private set; } + + [Resolved] + protected IBindable Beatmap { get; private set; } + + public override void Add(Drawable drawable) => throw new InvalidOperationException("Use Flow.Add instead"); + + public SetupSection() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + Padding = new MarginPadding(10); + + InternalChild = Flow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(20), + Direction = FillDirection.Vertical, + }; + } + } +} From 505dd37a75e98261394327771ffa764099716a73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 17:18:41 +0900 Subject: [PATCH 3671/6909] Make SettingsItem conform to IHasCurrentValue --- .../ManiaSettingsSubsection.cs | 4 ++-- .../UI/OsuSettingsSubsection.cs | 6 ++--- osu.Game.Tournament/Components/DateTextBox.cs | 18 +++++++------- .../Screens/Editors/RoundEditorScreen.cs | 12 +++++----- .../Screens/Editors/SeedingEditorScreen.cs | 10 ++++---- .../Screens/Editors/TeamEditorScreen.cs | 12 +++++----- .../Screens/Gameplay/GameplayScreen.cs | 4 ++-- .../Ladder/Components/LadderEditorSettings.cs | 12 +++++----- .../Screens/TeamIntro/SeedingScreen.cs | 2 +- .../Configuration/SettingSourceAttribute.cs | 12 +++++----- .../Sections/Audio/AudioDevicesSettings.cs | 2 +- .../Sections/Audio/MainMenuSettings.cs | 8 +++---- .../Settings/Sections/Audio/OffsetSettings.cs | 2 +- .../Settings/Sections/Audio/VolumeSettings.cs | 8 +++---- .../Sections/Debug/GeneralSettings.cs | 4 ++-- .../Sections/Gameplay/GeneralSettings.cs | 24 +++++++++---------- .../Sections/Gameplay/ModsSettings.cs | 2 +- .../Sections/Gameplay/SongSelectSettings.cs | 10 ++++---- .../Sections/General/LanguageSettings.cs | 2 +- .../Sections/General/LoginSettings.cs | 4 ++-- .../Sections/General/UpdateSettings.cs | 2 +- .../Sections/Graphics/DetailSettings.cs | 8 +++---- .../Sections/Graphics/LayoutSettings.cs | 20 ++++++++-------- .../Sections/Graphics/RendererSettings.cs | 6 ++--- .../Graphics/UserInterfaceSettings.cs | 6 ++--- .../Settings/Sections/Input/MouseSettings.cs | 12 +++++----- .../Settings/Sections/Online/WebSettings.cs | 4 ++-- .../Overlays/Settings/Sections/SkinSection.cs | 12 +++++----- osu.Game/Overlays/Settings/SettingsItem.cs | 4 ++-- .../Edit/Timing/SliderWithTextBoxInput.cs | 8 +++---- osu.Game/Screens/Edit/Timing/TimingSection.cs | 14 +++++------ .../Play/PlayerSettings/PlaybackSettings.cs | 4 ++-- .../Play/PlayerSettings/VisualSettings.cs | 4 ++-- 33 files changed, 131 insertions(+), 131 deletions(-) diff --git a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs index b470405df2..de77af8306 100644 --- a/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs +++ b/osu.Game.Rulesets.Mania/ManiaSettingsSubsection.cs @@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Mania new SettingsEnumDropdown { LabelText = "Scrolling direction", - Bindable = config.GetBindable(ManiaRulesetSetting.ScrollDirection) + Current = config.GetBindable(ManiaRulesetSetting.ScrollDirection) }, new SettingsSlider { LabelText = "Scroll speed", - Bindable = config.GetBindable(ManiaRulesetSetting.ScrollTime), + Current = config.GetBindable(ManiaRulesetSetting.ScrollTime), KeyboardStep = 5 }, }; diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index 88adf72551..3870f303b4 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -27,17 +27,17 @@ namespace osu.Game.Rulesets.Osu.UI new SettingsCheckbox { LabelText = "Snaking in sliders", - Bindable = config.GetBindable(OsuRulesetSetting.SnakingInSliders) + Current = config.GetBindable(OsuRulesetSetting.SnakingInSliders) }, new SettingsCheckbox { LabelText = "Snaking out sliders", - Bindable = config.GetBindable(OsuRulesetSetting.SnakingOutSliders) + Current = config.GetBindable(OsuRulesetSetting.SnakingOutSliders) }, new SettingsCheckbox { LabelText = "Cursor trail", - Bindable = config.GetBindable(OsuRulesetSetting.ShowCursorTrail) + Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail) }, }; } diff --git a/osu.Game.Tournament/Components/DateTextBox.cs b/osu.Game.Tournament/Components/DateTextBox.cs index a1b5ac38ea..5782301a65 100644 --- a/osu.Game.Tournament/Components/DateTextBox.cs +++ b/osu.Game.Tournament/Components/DateTextBox.cs @@ -10,34 +10,34 @@ namespace osu.Game.Tournament.Components { public class DateTextBox : SettingsTextBox { - public new Bindable Bindable + public new Bindable Current { - get => bindable; + get => current; set { - bindable = value.GetBoundCopy(); - bindable.BindValueChanged(dto => - base.Bindable.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); + current = value.GetBoundCopy(); + current.BindValueChanged(dto => + base.Current.Value = dto.NewValue.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ"), true); } } // hold a reference to the provided bindable so we don't have to in every settings section. - private Bindable bindable = new Bindable(); + private Bindable current = new Bindable(); public DateTextBox() { - base.Bindable = new Bindable(); + base.Current = new Bindable(); ((OsuTextBox)Control).OnCommit += (sender, newText) => { try { - bindable.Value = DateTimeOffset.Parse(sender.Text); + current.Value = DateTimeOffset.Parse(sender.Text); } catch { // reset textbox content to its last valid state on a parse failure. - bindable.TriggerChange(); + current.TriggerChange(); } }; } diff --git a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs index 8b8078e119..069ddfa4db 100644 --- a/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/RoundEditorScreen.cs @@ -63,25 +63,25 @@ namespace osu.Game.Tournament.Screens.Editors { LabelText = "Name", Width = 0.33f, - Bindable = Model.Name + Current = Model.Name }, new SettingsTextBox { LabelText = "Description", Width = 0.33f, - Bindable = Model.Description + Current = Model.Description }, new DateTextBox { LabelText = "Start Time", Width = 0.33f, - Bindable = Model.StartDate + Current = Model.StartDate }, new SettingsSlider { LabelText = "Best of", Width = 0.33f, - Bindable = Model.BestOf + Current = Model.BestOf }, new SettingsButton { @@ -186,14 +186,14 @@ namespace osu.Game.Tournament.Screens.Editors LabelText = "Beatmap ID", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = beatmapId, + Current = beatmapId, }, new SettingsTextBox { LabelText = "Mods", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = mods, + Current = mods, }, drawableContainer = new Container { diff --git a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs index 0973a7dc75..7bd8d3f6a0 100644 --- a/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/SeedingEditorScreen.cs @@ -74,13 +74,13 @@ namespace osu.Game.Tournament.Screens.Editors { LabelText = "Mod", Width = 0.33f, - Bindable = Model.Mod + Current = Model.Mod }, new SettingsSlider { LabelText = "Seed", Width = 0.33f, - Bindable = Model.Seed + Current = Model.Seed }, new SettingsButton { @@ -187,21 +187,21 @@ namespace osu.Game.Tournament.Screens.Editors LabelText = "Beatmap ID", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = beatmapId, + Current = beatmapId, }, new SettingsSlider { LabelText = "Seed", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = beatmap.Seed + Current = beatmap.Seed }, new SettingsTextBox { LabelText = "Score", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = score, + Current = score, }, drawableContainer = new Container { diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index dbfcfe4225..7196f47bd6 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -102,31 +102,31 @@ namespace osu.Game.Tournament.Screens.Editors { LabelText = "Name", Width = 0.2f, - Bindable = Model.FullName + Current = Model.FullName }, new SettingsTextBox { LabelText = "Acronym", Width = 0.2f, - Bindable = Model.Acronym + Current = Model.Acronym }, new SettingsTextBox { LabelText = "Flag", Width = 0.2f, - Bindable = Model.FlagName + Current = Model.FlagName }, new SettingsTextBox { LabelText = "Seed", Width = 0.2f, - Bindable = Model.Seed + Current = Model.Seed }, new SettingsSlider { LabelText = "Last Year Placement", Width = 0.33f, - Bindable = Model.LastYearPlacing + Current = Model.LastYearPlacing }, new SettingsButton { @@ -247,7 +247,7 @@ namespace osu.Game.Tournament.Screens.Editors LabelText = "User ID", RelativeSizeAxes = Axes.None, Width = 200, - Bindable = userId, + Current = userId, }, drawableContainer = new Container { diff --git a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs index e4e3842369..e4ec45c00e 100644 --- a/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs +++ b/osu.Game.Tournament/Screens/Gameplay/GameplayScreen.cs @@ -113,13 +113,13 @@ namespace osu.Game.Tournament.Screens.Gameplay new SettingsSlider { LabelText = "Chroma width", - Bindable = LadderInfo.ChromaKeyWidth, + Current = LadderInfo.ChromaKeyWidth, KeyboardStep = 1, }, new SettingsSlider { LabelText = "Players per team", - Bindable = LadderInfo.PlayersPerTeam, + Current = LadderInfo.PlayersPerTeam, KeyboardStep = 1, } } diff --git a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs index b60eb814e5..cf4466a2e3 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/LadderEditorSettings.cs @@ -51,15 +51,15 @@ namespace osu.Game.Tournament.Screens.Ladder.Components editorInfo.Selected.ValueChanged += selection => { - roundDropdown.Bindable = selection.NewValue?.Round; + roundDropdown.Current = selection.NewValue?.Round; losersCheckbox.Current = selection.NewValue?.Losers; - dateTimeBox.Bindable = selection.NewValue?.Date; + dateTimeBox.Current = selection.NewValue?.Date; - team1Dropdown.Bindable = selection.NewValue?.Team1; - team2Dropdown.Bindable = selection.NewValue?.Team2; + team1Dropdown.Current = selection.NewValue?.Team1; + team2Dropdown.Current = selection.NewValue?.Team2; }; - roundDropdown.Bindable.ValueChanged += round => + roundDropdown.Current.ValueChanged += round => { if (editorInfo.Selected.Value?.Date.Value < round.NewValue?.StartDate.Value) { @@ -88,7 +88,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components { public SettingsRoundDropdown(BindableList rounds) { - Bindable = new Bindable(); + Current = new Bindable(); foreach (var r in rounds.Prepend(new TournamentRound())) add(r); diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index eed3cac9f0..b343608e69 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro new SettingsTeamDropdown(LadderInfo.Teams) { LabelText = "Show specific team", - Bindable = currentTeam, + Current = currentTeam, } } } diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index fe487cb1d0..50069be4b2 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -57,7 +57,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber, + Current = bNumber, KeyboardStep = 0.1f, }; @@ -67,7 +67,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber, + Current = bNumber, KeyboardStep = 0.1f, }; @@ -77,7 +77,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, - Bindable = bNumber + Current = bNumber }; break; @@ -86,7 +86,7 @@ namespace osu.Game.Configuration yield return new SettingsCheckbox { LabelText = attr.Label, - Bindable = bBool + Current = bBool }; break; @@ -95,7 +95,7 @@ namespace osu.Game.Configuration yield return new SettingsTextBox { LabelText = attr.Label, - Bindable = bString + Current = bString }; break; @@ -105,7 +105,7 @@ namespace osu.Game.Configuration var dropdown = (Drawable)Activator.CreateInstance(dropdownType); dropdownType.GetProperty(nameof(SettingsDropdown.LabelText))?.SetValue(dropdown, attr.Label); - dropdownType.GetProperty(nameof(SettingsDropdown.Bindable))?.SetValue(dropdown, bindable); + dropdownType.GetProperty(nameof(SettingsDropdown.Current))?.SetValue(dropdown, bindable); yield return dropdown; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index 3da64e0de4..bed74542c9 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -64,7 +64,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio updateItems(); - dropdown.Bindable = audio.AudioDevice; + dropdown.Current = audio.AudioDevice; audio.OnNewDevice += onDeviceChanged; audio.OnLostDevice += onDeviceChanged; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs index a303f93b34..d5de32ed05 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs @@ -21,23 +21,23 @@ namespace osu.Game.Overlays.Settings.Sections.Audio new SettingsCheckbox { LabelText = "Interface voices", - Bindable = config.GetBindable(OsuSetting.MenuVoice) + Current = config.GetBindable(OsuSetting.MenuVoice) }, new SettingsCheckbox { LabelText = "osu! music theme", - Bindable = config.GetBindable(OsuSetting.MenuMusic) + Current = config.GetBindable(OsuSetting.MenuMusic) }, new SettingsDropdown { LabelText = "Intro sequence", - Bindable = config.GetBindable(OsuSetting.IntroSequence), + Current = config.GetBindable(OsuSetting.IntroSequence), Items = Enum.GetValues(typeof(IntroSequence)).Cast() }, new SettingsDropdown { LabelText = "Background source", - Bindable = config.GetBindable(OsuSetting.MenuBackgroundSource), + Current = config.GetBindable(OsuSetting.MenuBackgroundSource), Items = Enum.GetValues(typeof(BackgroundSource)).Cast() } }; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs index aaa4302553..c9a81b955b 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/OffsetSettings.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio new SettingsSlider { LabelText = "Audio offset", - Bindable = config.GetBindable(OsuSetting.AudioOffset), + Current = config.GetBindable(OsuSetting.AudioOffset), KeyboardStep = 1f }, new SettingsButton diff --git a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs index bda677ecd6..c172a76ab9 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/VolumeSettings.cs @@ -20,28 +20,28 @@ namespace osu.Game.Overlays.Settings.Sections.Audio new SettingsSlider { LabelText = "Master", - Bindable = audio.Volume, + Current = audio.Volume, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Master (window inactive)", - Bindable = config.GetBindable(OsuSetting.VolumeInactive), + Current = config.GetBindable(OsuSetting.VolumeInactive), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Effect", - Bindable = audio.VolumeSample, + Current = audio.VolumeSample, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Music", - Bindable = audio.VolumeTrack, + Current = audio.VolumeTrack, KeyboardStep = 0.01f, DisplayAsPercentage = true }, diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs index 9edb18e065..f05b876d8f 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs @@ -19,12 +19,12 @@ namespace osu.Game.Overlays.Settings.Sections.Debug new SettingsCheckbox { LabelText = "Show log overlay", - Bindable = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) + Current = frameworkConfig.GetBindable(FrameworkSetting.ShowLogOverlay) }, new SettingsCheckbox { LabelText = "Bypass front-to-back render pass", - Bindable = config.GetBindable(DebugSetting.BypassFrontToBackPass) + Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) } }; } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 0149e6c3a6..73968761e2 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -21,62 +21,62 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsSlider { LabelText = "Background dim", - Bindable = config.GetBindable(OsuSetting.DimLevel), + Current = config.GetBindable(OsuSetting.DimLevel), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Background blur", - Bindable = config.GetBindable(OsuSetting.BlurLevel), + Current = config.GetBindable(OsuSetting.BlurLevel), KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsCheckbox { LabelText = "Lighten playfield during breaks", - Bindable = config.GetBindable(OsuSetting.LightenDuringBreaks) + Current = config.GetBindable(OsuSetting.LightenDuringBreaks) }, new SettingsCheckbox { LabelText = "Show score overlay", - Bindable = config.GetBindable(OsuSetting.ShowInterface) + Current = config.GetBindable(OsuSetting.ShowInterface) }, new SettingsCheckbox { LabelText = "Show difficulty graph on progress bar", - Bindable = config.GetBindable(OsuSetting.ShowProgressGraph) + Current = config.GetBindable(OsuSetting.ShowProgressGraph) }, new SettingsCheckbox { LabelText = "Show health display even when you can't fail", - Bindable = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), + Current = config.GetBindable(OsuSetting.ShowHealthDisplayWhenCantFail), Keywords = new[] { "hp", "bar" } }, new SettingsCheckbox { LabelText = "Fade playfield to red when health is low", - Bindable = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), + Current = config.GetBindable(OsuSetting.FadePlayfieldWhenHealthLow), }, new SettingsCheckbox { LabelText = "Always show key overlay", - Bindable = config.GetBindable(OsuSetting.KeyOverlay) + Current = config.GetBindable(OsuSetting.KeyOverlay) }, new SettingsCheckbox { LabelText = "Positional hitsounds", - Bindable = config.GetBindable(OsuSetting.PositionalHitSounds) + Current = config.GetBindable(OsuSetting.PositionalHitSounds) }, new SettingsEnumDropdown { LabelText = "Score meter type", - Bindable = config.GetBindable(OsuSetting.ScoreMeter) + Current = config.GetBindable(OsuSetting.ScoreMeter) }, new SettingsEnumDropdown { LabelText = "Score display mode", - Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode) + Current = config.GetBindable(OsuSetting.ScoreDisplayMode) } }; @@ -85,7 +85,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Add(new SettingsCheckbox { LabelText = "Disable Windows key during gameplay", - Bindable = config.GetBindable(OsuSetting.GameplayDisableWinKey) + Current = config.GetBindable(OsuSetting.GameplayDisableWinKey) }); } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs index 0babb98066..2b2fb9cef7 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/ModsSettings.cs @@ -22,7 +22,7 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = "Increase visibility of first object when visual impairment mods are enabled", - Bindable = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility), + Current = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility), }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs index 0c42247993..b26876556e 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs @@ -31,31 +31,31 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsCheckbox { LabelText = "Right mouse drag to absolute scroll", - Bindable = config.GetBindable(OsuSetting.SongSelectRightMouseScroll), + Current = config.GetBindable(OsuSetting.SongSelectRightMouseScroll), }, new SettingsCheckbox { LabelText = "Show converted beatmaps", - Bindable = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), + Current = config.GetBindable(OsuSetting.ShowConvertedBeatmaps), }, new SettingsSlider { LabelText = "Display beatmaps from", - Bindable = config.GetBindable(OsuSetting.DisplayStarsMinimum), + Current = config.GetBindable(OsuSetting.DisplayStarsMinimum), KeyboardStep = 0.1f, Keywords = new[] { "minimum", "maximum", "star", "difficulty" } }, new SettingsSlider { LabelText = "up to", - Bindable = config.GetBindable(OsuSetting.DisplayStarsMaximum), + Current = config.GetBindable(OsuSetting.DisplayStarsMaximum), KeyboardStep = 0.1f, Keywords = new[] { "minimum", "maximum", "star", "difficulty" } }, new SettingsEnumDropdown { LabelText = "Random selection algorithm", - Bindable = config.GetBindable(OsuSetting.RandomSelectAlgorithm), + Current = config.GetBindable(OsuSetting.RandomSelectAlgorithm), } }; } diff --git a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs index 236bfbecc3..44e42ecbfe 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LanguageSettings.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections.General new SettingsCheckbox { LabelText = "Prefer metadata in original language", - Bindable = frameworkConfig.GetBindable(FrameworkSetting.ShowUnicode) + Current = frameworkConfig.GetBindable(FrameworkSetting.ShowUnicode) }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index f96e204f62..9e358d0cf5 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -240,12 +240,12 @@ namespace osu.Game.Overlays.Settings.Sections.General new SettingsCheckbox { LabelText = "Remember username", - Bindable = config.GetBindable(OsuSetting.SaveUsername), + Current = config.GetBindable(OsuSetting.SaveUsername), }, new SettingsCheckbox { LabelText = "Stay signed in", - Bindable = config.GetBindable(OsuSetting.SavePassword), + Current = config.GetBindable(OsuSetting.SavePassword), }, new Container { diff --git a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs index 9c7d0b0be4..a59a6b00b9 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Add(new SettingsEnumDropdown { LabelText = "Release stream", - Bindable = config.GetBindable(OsuSetting.ReleaseStream), + Current = config.GetBindable(OsuSetting.ReleaseStream), }); if (updateManager?.CanCheckForUpdate == true) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs index 3089040f96..30caa45995 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/DetailSettings.cs @@ -19,22 +19,22 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsCheckbox { LabelText = "Storyboard / Video", - Bindable = config.GetBindable(OsuSetting.ShowStoryboard) + Current = config.GetBindable(OsuSetting.ShowStoryboard) }, new SettingsCheckbox { LabelText = "Hit Lighting", - Bindable = config.GetBindable(OsuSetting.HitLighting) + Current = config.GetBindable(OsuSetting.HitLighting) }, new SettingsEnumDropdown { LabelText = "Screenshot format", - Bindable = config.GetBindable(OsuSetting.ScreenshotFormat) + Current = config.GetBindable(OsuSetting.ScreenshotFormat) }, new SettingsCheckbox { LabelText = "Show menu cursor in screenshots", - Bindable = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor) + Current = config.GetBindable(OsuSetting.ScreenshotCaptureMenuCursor) } }; } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 4312b319c0..14b8dbfac0 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -62,7 +62,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics windowModeDropdown = new SettingsDropdown { LabelText = "Screen mode", - Bindable = config.GetBindable(FrameworkSetting.WindowMode), + Current = config.GetBindable(FrameworkSetting.WindowMode), ItemSource = windowModes, }, resolutionSettingsContainer = new Container @@ -74,14 +74,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { LabelText = "UI Scaling", TransferValueOnCommit = true, - Bindable = osuConfig.GetBindable(OsuSetting.UIScale), + Current = osuConfig.GetBindable(OsuSetting.UIScale), KeyboardStep = 0.01f, Keywords = new[] { "scale", "letterbox" }, }, new SettingsEnumDropdown { LabelText = "Screen Scaling", - Bindable = osuConfig.GetBindable(OsuSetting.Scaling), + Current = osuConfig.GetBindable(OsuSetting.Scaling), Keywords = new[] { "scale", "letterbox" }, }, scalingSettings = new FillFlowContainer> @@ -97,28 +97,28 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsSlider { LabelText = "Horizontal position", - Bindable = scalingPositionX, + Current = scalingPositionX, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Vertical position", - Bindable = scalingPositionY, + Current = scalingPositionY, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Horizontal scale", - Bindable = scalingSizeX, + Current = scalingSizeX, KeyboardStep = 0.01f, DisplayAsPercentage = true }, new SettingsSlider { LabelText = "Vertical scale", - Bindable = scalingSizeY, + Current = scalingSizeY, KeyboardStep = 0.01f, DisplayAsPercentage = true }, @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, }; - scalingSettings.ForEach(s => bindPreviewEvent(s.Bindable)); + scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); var resolutions = getResolutions(); @@ -137,10 +137,10 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics LabelText = "Resolution", ShowsDefaultIndicator = false, Items = resolutions, - Bindable = sizeFullscreen + Current = sizeFullscreen }; - windowModeDropdown.Bindable.BindValueChanged(mode => + windowModeDropdown.Current.BindValueChanged(mode => { if (mode.NewValue == WindowMode.Fullscreen) { diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs index 69ff9b43e5..8773e6763c 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/RendererSettings.cs @@ -23,17 +23,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsEnumDropdown { LabelText = "Frame limiter", - Bindable = config.GetBindable(FrameworkSetting.FrameSync) + Current = config.GetBindable(FrameworkSetting.FrameSync) }, new SettingsEnumDropdown { LabelText = "Threading mode", - Bindable = config.GetBindable(FrameworkSetting.ExecutionMode) + Current = config.GetBindable(FrameworkSetting.ExecutionMode) }, new SettingsCheckbox { LabelText = "Show FPS", - Bindable = osuConfig.GetBindable(OsuSetting.ShowFpsDisplay) + Current = osuConfig.GetBindable(OsuSetting.ShowFpsDisplay) }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs index a8953ac3a2..38c30ddd64 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs @@ -20,17 +20,17 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics new SettingsCheckbox { LabelText = "Rotate cursor when dragging", - Bindable = config.GetBindable(OsuSetting.CursorRotation) + Current = config.GetBindable(OsuSetting.CursorRotation) }, new SettingsCheckbox { LabelText = "Parallax", - Bindable = config.GetBindable(OsuSetting.MenuParallax) + Current = config.GetBindable(OsuSetting.MenuParallax) }, new SettingsSlider { LabelText = "Hold-to-confirm activation time", - Bindable = config.GetBindable(OsuSetting.UIHoldActivationDelay), + Current = config.GetBindable(OsuSetting.UIHoldActivationDelay), KeyboardStep = 50 }, }; diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index d27ab63fb7..5227e328ec 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -35,32 +35,32 @@ namespace osu.Game.Overlays.Settings.Sections.Input new SettingsCheckbox { LabelText = "Raw input", - Bindable = rawInputToggle + Current = rawInputToggle }, new SensitivitySetting { LabelText = "Cursor sensitivity", - Bindable = sensitivityBindable + Current = sensitivityBindable }, new SettingsCheckbox { LabelText = "Map absolute input to window", - Bindable = config.GetBindable(FrameworkSetting.MapAbsoluteInputToWindow) + Current = config.GetBindable(FrameworkSetting.MapAbsoluteInputToWindow) }, new SettingsEnumDropdown { LabelText = "Confine mouse cursor to window", - Bindable = config.GetBindable(FrameworkSetting.ConfineMouseMode), + Current = config.GetBindable(FrameworkSetting.ConfineMouseMode), }, new SettingsCheckbox { LabelText = "Disable mouse wheel during gameplay", - Bindable = osuConfig.GetBindable(OsuSetting.MouseDisableWheel) + Current = osuConfig.GetBindable(OsuSetting.MouseDisableWheel) }, new SettingsCheckbox { LabelText = "Disable mouse buttons during gameplay", - Bindable = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) + Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) }, }; diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index 23513eade8..6461bd7b93 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -19,13 +19,13 @@ namespace osu.Game.Overlays.Settings.Sections.Online new SettingsCheckbox { LabelText = "Warn about opening external links", - Bindable = config.GetBindable(OsuSetting.ExternalLinkWarning) + Current = config.GetBindable(OsuSetting.ExternalLinkWarning) }, new SettingsCheckbox { LabelText = "Prefer downloads without video", Keywords = new[] { "no-video" }, - Bindable = config.GetBindable(OsuSetting.PreferNoVideo) + Current = config.GetBindable(OsuSetting.PreferNoVideo) }, }; } diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 596d3a9801..1ade4befdc 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -47,29 +47,29 @@ namespace osu.Game.Overlays.Settings.Sections new SettingsSlider { LabelText = "Menu cursor size", - Bindable = config.GetBindable(OsuSetting.MenuCursorSize), + Current = config.GetBindable(OsuSetting.MenuCursorSize), KeyboardStep = 0.01f }, new SettingsSlider { LabelText = "Gameplay cursor size", - Bindable = config.GetBindable(OsuSetting.GameplayCursorSize), + Current = config.GetBindable(OsuSetting.GameplayCursorSize), KeyboardStep = 0.01f }, new SettingsCheckbox { LabelText = "Adjust gameplay cursor size based on current beatmap", - Bindable = config.GetBindable(OsuSetting.AutoCursorSize) + Current = config.GetBindable(OsuSetting.AutoCursorSize) }, new SettingsCheckbox { LabelText = "Beatmap skins", - Bindable = config.GetBindable(OsuSetting.BeatmapSkins) + Current = config.GetBindable(OsuSetting.BeatmapSkins) }, new SettingsCheckbox { LabelText = "Beatmap hitsounds", - Bindable = config.GetBindable(OsuSetting.BeatmapHitsounds) + Current = config.GetBindable(OsuSetting.BeatmapHitsounds) }, }; @@ -81,7 +81,7 @@ namespace osu.Game.Overlays.Settings.Sections config.BindWith(OsuSetting.Skin, configBindable); - skinDropdown.Bindable = dropdownBindable; + skinDropdown.Current = dropdownBindable; skinDropdown.Items = skins.GetAllUsableSkins().ToArray(); // Todo: This should not be necessary when OsuConfigManager is databased diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index c2dd40d2a6..ad6aaafd9d 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -21,7 +21,7 @@ using osuTK; namespace osu.Game.Overlays.Settings { - public abstract class SettingsItem : Container, IFilterable, ISettingsItem + public abstract class SettingsItem : Container, IFilterable, ISettingsItem, IHasCurrentValue { protected abstract Drawable CreateControl(); @@ -54,7 +54,7 @@ namespace osu.Game.Overlays.Settings } } - public virtual Bindable Bindable + public virtual Bindable Current { get => controlWithCurrent.Current; set => controlWithCurrent.Current = value; diff --git a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs index d5afc8978d..f2f9f76143 100644 --- a/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs +++ b/osu.Game/Screens/Edit/Timing/SliderWithTextBoxInput.cs @@ -11,7 +11,7 @@ using osu.Game.Overlays.Settings; namespace osu.Game.Screens.Edit.Timing { - internal class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue + public class SliderWithTextBoxInput : CompositeDrawable, IHasCurrentValue where T : struct, IEquatable, IComparable, IConvertible { private readonly SettingsSlider slider; @@ -51,7 +51,7 @@ namespace osu.Game.Screens.Edit.Timing try { - slider.Bindable.Parse(t.Text); + slider.Current.Parse(t.Text); } catch { @@ -71,8 +71,8 @@ namespace osu.Game.Screens.Edit.Timing public Bindable Current { - get => slider.Bindable; - set => slider.Bindable = value; + get => slider.Current; + set => slider.Current = value; } } } diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 2ab8703cc4..1ae2a86885 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -36,14 +36,14 @@ namespace osu.Game.Screens.Edit.Timing { if (point.NewValue != null) { - bpmSlider.Bindable = point.NewValue.BeatLengthBindable; - bpmSlider.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState()); + bpmSlider.Current = point.NewValue.BeatLengthBindable; + bpmSlider.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); bpmTextEntry.Bindable = point.NewValue.BeatLengthBindable; // no need to hook change handler here as it's the same bindable as above - timeSignature.Bindable = point.NewValue.TimeSignatureBindable; - timeSignature.Bindable.BindValueChanged(_ => ChangeHandler?.SaveState()); + timeSignature.Current = point.NewValue.TimeSignatureBindable; + timeSignature.Current.BindValueChanged(_ => ChangeHandler?.SaveState()); } } @@ -121,14 +121,14 @@ namespace osu.Game.Screens.Edit.Timing beatLengthBindable.BindValueChanged(beatLength => updateCurrent(beatLengthToBpm(beatLength.NewValue)), true); bpmBindable.BindValueChanged(bpm => beatLengthBindable.Value = beatLengthToBpm(bpm.NewValue)); - base.Bindable = bpmBindable; + base.Current = bpmBindable; TransferValueOnCommit = true; } - public override Bindable Bindable + public override Bindable Current { - get => base.Bindable; + get => base.Current; set { // incoming will be beat length, not bpm diff --git a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs index 24ddc277cd..16e29ac3c8 100644 --- a/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/PlaybackSettings.cs @@ -51,14 +51,14 @@ namespace osu.Game.Screens.Play.PlayerSettings } }, }, - rateSlider = new PlayerSliderBar { Bindable = UserPlaybackRate } + rateSlider = new PlayerSliderBar { Current = UserPlaybackRate } }; } protected override void LoadComplete() { base.LoadComplete(); - rateSlider.Bindable.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); + rateSlider.Current.BindValueChanged(multiplier => multiplierText.Text = $"{multiplier.NewValue:0.0}x", true); } } } diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index e06cf5c6d5..8f29fe7893 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -50,8 +50,8 @@ namespace osu.Game.Screens.Play.PlayerSettings [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - dimSliderBar.Bindable = config.GetBindable(OsuSetting.DimLevel); - blurSliderBar.Bindable = config.GetBindable(OsuSetting.BlurLevel); + dimSliderBar.Current = config.GetBindable(OsuSetting.DimLevel); + blurSliderBar.Current = config.GetBindable(OsuSetting.BlurLevel); showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); From e1a6f47d90b226ed0525d4e113cdac3b59e05afb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 16:40:47 +0900 Subject: [PATCH 3672/6909] Add the most basic implementation of LabelledSliderBar feasible --- .../TestSceneLabelledSliderBar.cs | 46 +++++++++++++++++++ .../UserInterfaceV2/LabelledSliderBar.cs | 24 ++++++++++ 2 files changed, 70 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs create mode 100644 osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs new file mode 100644 index 0000000000..393420e700 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLabelledSliderBar.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterfaceV2; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneLabelledSliderBar : OsuTestScene + { + [TestCase(false)] + [TestCase(true)] + public void TestSliderBar(bool hasDescription) => createSliderBar(hasDescription); + + private void createSliderBar(bool hasDescription = false) + { + AddStep("create component", () => + { + LabelledSliderBar component; + + Child = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 500, + AutoSizeAxes = Axes.Y, + Child = component = new LabelledSliderBar + { + Current = new BindableDouble(5) + { + MinValue = 0, + MaxValue = 10, + Precision = 1, + } + } + }; + + component.Label = "a sample component"; + component.Description = hasDescription ? "this text describes the component" : string.Empty; + }); + } + } +} diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs new file mode 100644 index 0000000000..cba94e314b --- /dev/null +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledSliderBar.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Graphics.UserInterfaceV2 +{ + public class LabelledSliderBar : LabelledComponent, TNumber> + where TNumber : struct, IEquatable, IComparable, IConvertible + { + public LabelledSliderBar() + : base(true) + { + } + + protected override SettingsSlider CreateComponent() => new SettingsSlider + { + TransferValueOnCommit = true, + RelativeSizeAxes = Axes.X, + }; + } +} From 13b67b93a5ba3ff79a78b3a92a7ae1c60f6d0f7c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 15:51:40 +0900 Subject: [PATCH 3673/6909] Add difficulty section --- .../Screens/Edit/Setup/DifficultySection.cs | 25 +++++++++++++++++++ osu.Game/Screens/Edit/Setup/SetupScreen.cs | 1 + 2 files changed, 26 insertions(+) create mode 100644 osu.Game/Screens/Edit/Setup/DifficultySection.cs diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs new file mode 100644 index 0000000000..952ce90273 --- /dev/null +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.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.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.Edit.Setup +{ + internal class DifficultySection : SetupSection + { + [BackgroundDependencyLoader] + private void load() + { + Flow.Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Difficulty settings" + }, + new LabelledSlider() + }; + } + } +} diff --git a/osu.Game/Screens/Edit/Setup/SetupScreen.cs b/osu.Game/Screens/Edit/Setup/SetupScreen.cs index cd4f6733c0..1c3cbb7206 100644 --- a/osu.Game/Screens/Edit/Setup/SetupScreen.cs +++ b/osu.Game/Screens/Edit/Setup/SetupScreen.cs @@ -52,6 +52,7 @@ namespace osu.Game.Screens.Edit.Setup { new ResourcesSection(), new MetadataSection(), + new DifficultySection(), } }, } From 6d7f12ad4bc68c1573a71b8811e8b0a4adebc09e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 17:01:50 +0900 Subject: [PATCH 3674/6909] Add basic difficulty setting sliders --- .../Screens/Edit/Setup/DifficultySection.cs | 63 ++++++++++++++++++- 1 file changed, 62 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 952ce90273..0434c1cf1f 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -1,14 +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.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterfaceV2; namespace osu.Game.Screens.Edit.Setup { internal class DifficultySection : SetupSection { + private LabelledSliderBar circleSizeSlider; + private LabelledSliderBar healthDrainSlider; + private LabelledSliderBar approachRateSlider; + private LabelledSliderBar overallDifficultySlider; + [BackgroundDependencyLoader] private void load() { @@ -18,8 +27,60 @@ namespace osu.Game.Screens.Edit.Setup { Text = "Difficulty settings" }, - new LabelledSlider() + circleSizeSlider = new LabelledSliderBar + { + Label = "Circle Size", + Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 2, + MaxValue = 7 + } + }, + healthDrainSlider = new LabelledSliderBar + { + Label = "Health Drain", + Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10 + } + }, + approachRateSlider = new LabelledSliderBar + { + Label = "Approach Rate", + Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10 + } + }, + overallDifficultySlider = new LabelledSliderBar + { + Label = "Overall Difficulty", + Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty) + { + Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, + MinValue = 0, + MaxValue = 10 + } + }, }; + + foreach (var item in Flow.OfType>()) + item.Current.ValueChanged += onValueChanged; + } + + private void onValueChanged(ValueChangedEvent args) + { + // for now, update these on commit rather than making BeatmapMetadata bindables. + // after switching database engines we can reconsider if switching to bindables is a good direction. + Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value; + Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value; + Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value; } } } From 7a20a34aff82cbf491a21da19c18ae156d346378 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 17:12:19 +0900 Subject: [PATCH 3675/6909] Add support to EditorBeatmap to update all hitobjects --- osu.Game/Screens/Edit/EditorBeatmap.cs | 9 +++++++++ osu.Game/Screens/Edit/Setup/DifficultySection.cs | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 3248c5b8be..d37b7dd2b5 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -258,5 +258,14 @@ namespace osu.Game.Screens.Edit public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor; public int BeatDivisor => beatDivisor?.Value ?? 1; + + /// + /// Update all hit objects with potentially changed difficulty or control point data. + /// + public void UpdateBeatmap() + { + foreach (var h in HitObjects) + pendingUpdates.Add(h); + } } } diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 0434c1cf1f..ce6f617f37 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -13,6 +13,9 @@ namespace osu.Game.Screens.Edit.Setup { internal class DifficultySection : SetupSection { + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } + private LabelledSliderBar circleSizeSlider; private LabelledSliderBar healthDrainSlider; private LabelledSliderBar approachRateSlider; @@ -81,6 +84,8 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value; Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value; Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value; + + editorBeatmap.UpdateBeatmap(); } } } From 7e8ab1cb9587913ba6c50ec362c871e7b4240e73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 17:17:03 +0900 Subject: [PATCH 3676/6909] Add description text --- osu.Game/Screens/Edit/Setup/DifficultySection.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index ce6f617f37..4ed8d0164b 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -32,7 +32,8 @@ namespace osu.Game.Screens.Edit.Setup }, circleSizeSlider = new LabelledSliderBar { - Label = "Circle Size", + Label = "Object Size", + Description = "The size of all hit objects", Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, @@ -43,6 +44,7 @@ namespace osu.Game.Screens.Edit.Setup healthDrainSlider = new LabelledSliderBar { Label = "Health Drain", + Description = "The rate of passive health drain throughout playable time", Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, @@ -53,6 +55,7 @@ namespace osu.Game.Screens.Edit.Setup approachRateSlider = new LabelledSliderBar { Label = "Approach Rate", + Description = "The speed at which objects are presented to the player", Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, @@ -63,6 +66,7 @@ namespace osu.Game.Screens.Edit.Setup overallDifficultySlider = new LabelledSliderBar { Label = "Overall Difficulty", + Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)", Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, From 3ce234d552bb5ae708b8915c4bfad7b7b8e7c896 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 17:47:22 +0900 Subject: [PATCH 3677/6909] Seek at 4x normal speed when holding shift This matches osu-stable 1:1. Not sure if it feels better or not but let's stick with what people are used to for the time being. --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 875ab25003..4b4266d049 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -589,7 +589,7 @@ namespace osu.Game.Screens.Edit private void seek(UIEvent e, int direction) { - double amount = e.ShiftPressed ? 2 : 1; + double amount = e.ShiftPressed ? 4 : 1; if (direction < 1) clock.SeekBackward(!clock.IsRunning, amount); From b1a64f89d78603ef86157940ec582127698f1707 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 17:49:12 +0900 Subject: [PATCH 3678/6909] Increase backwards seek magnitude when the track is running This matches osu-stable. When the track is running, seeking backwards (against the flow) is harder than seeking forwards. Adding a mutliplier makes it feel much better. Note that this is additive not multiplicative because for larger seeks the (where `amount` > 1) we don't want to jump an insanely huge amount - just offset the seek slightly to account for playing audio. --- osu.Game/Screens/Edit/EditorClock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 4b7cd82637..64ed34f5ec 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.Edit /// /// Whether to snap to the closest beat after seeking. /// The relative amount (magnitude) which should be seeked. - public void SeekBackward(bool snapped = false, double amount = 1) => seek(-1, snapped, amount); + public void SeekBackward(bool snapped = false, double amount = 1) => seek(-1, snapped, amount + (IsRunning ? 1.5 : 0)); /// /// Seeks forwards by one beat length. From 1877312a917ce017b5fa8e58951a4c373cb7cecb Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Tue, 6 Oct 2020 19:52:18 +1030 Subject: [PATCH 3679/6909] Rename DuringGameplay --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- osu.Game/Input/ConfineMouseTracker.cs | 4 ++-- osu.Game/Input/OsuConfineMouseMode.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 78179a781a..71cbdb345c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -70,7 +70,7 @@ namespace osu.Game.Configuration Set(OsuSetting.MouseDisableButtons, false); Set(OsuSetting.MouseDisableWheel, false); - Set(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay); + Set(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.WhenOverlaysDisabled); // Graphics Set(OsuSetting.ShowFpsDisplay, false); diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index 6565967d1d..653622e881 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -16,7 +16,7 @@ namespace osu.Game.Input /// Connects with , /// while optionally binding an mode, usually that of the current . /// It is assumed that while overlay activation is , we should also confine the - /// mouse cursor if it has been requested with . + /// mouse cursor if it has been requested with . /// public class ConfineMouseTracker : Component { @@ -52,7 +52,7 @@ namespace osu.Game.Input frameworkConfineMode.Value = ConfineMouseMode.Fullscreen; break; - case OsuConfineMouseMode.DuringGameplay: + case OsuConfineMouseMode.WhenOverlaysDisabled: frameworkConfineMode.Value = OverlayActivationMode.Value == OverlayActivation.Disabled ? ConfineMouseMode.Always : ConfineMouseMode.Never; break; diff --git a/osu.Game/Input/OsuConfineMouseMode.cs b/osu.Game/Input/OsuConfineMouseMode.cs index 32b456395c..53e352c8bd 100644 --- a/osu.Game/Input/OsuConfineMouseMode.cs +++ b/osu.Game/Input/OsuConfineMouseMode.cs @@ -27,7 +27,7 @@ namespace osu.Game.Input /// but may otherwise move freely. /// [Description("During Gameplay")] - DuringGameplay, + WhenOverlaysDisabled, /// /// The mouse cursor will always be locked to the window bounds while the game has focus. From 782fc1d60fe1c10768ef393ba3102a77e3237662 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Tue, 6 Oct 2020 20:11:48 +1030 Subject: [PATCH 3680/6909] Use OsuGame.OverlayActivationMode rather than per-Player --- osu.Game/Input/ConfineMouseTracker.cs | 19 ++++++------------- osu.Game/Input/OsuConfineMouseMode.cs | 2 +- osu.Game/Screens/Play/Player.cs | 6 ------ 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index 653622e881..3b54c03bb0 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -8,13 +8,12 @@ using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Configuration; using osu.Game.Overlays; -using osu.Game.Screens.Play; namespace osu.Game.Input { /// /// Connects with , - /// while optionally binding an mode, usually that of the current . + /// while binding . /// It is assumed that while overlay activation is , we should also confine the /// mouse cursor if it has been requested with . /// @@ -22,22 +21,16 @@ namespace osu.Game.Input { private Bindable frameworkConfineMode; private Bindable osuConfineMode; - - /// - /// The bindable used to indicate whether gameplay is active. - /// Should be bound to the corresponding bindable of the current . - /// Defaults to to assume that all other screens are considered "not gameplay". - /// - public IBindable OverlayActivationMode { get; } = new Bindable(OverlayActivation.All); + private IBindable overlayActivationMode; [BackgroundDependencyLoader] - private void load(FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) + private void load(OsuGame game, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) { frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); osuConfineMode.ValueChanged += _ => updateConfineMode(); - - OverlayActivationMode.BindValueChanged(_ => updateConfineMode(), true); + overlayActivationMode = game.OverlayActivationMode.GetBoundCopy(); + overlayActivationMode.BindValueChanged(_ => updateConfineMode(), true); } private void updateConfineMode() @@ -53,7 +46,7 @@ namespace osu.Game.Input break; case OsuConfineMouseMode.WhenOverlaysDisabled: - frameworkConfineMode.Value = OverlayActivationMode.Value == OverlayActivation.Disabled ? ConfineMouseMode.Always : ConfineMouseMode.Never; + frameworkConfineMode.Value = overlayActivationMode?.Value == OverlayActivation.Disabled ? ConfineMouseMode.Always : ConfineMouseMode.Never; break; case OsuConfineMouseMode.Always: diff --git a/osu.Game/Input/OsuConfineMouseMode.cs b/osu.Game/Input/OsuConfineMouseMode.cs index 53e352c8bd..e8411a3d9f 100644 --- a/osu.Game/Input/OsuConfineMouseMode.cs +++ b/osu.Game/Input/OsuConfineMouseMode.cs @@ -23,7 +23,7 @@ namespace osu.Game.Input Fullscreen, /// - /// The mouse cursor will be locked to the window bounds during gameplay, + /// The mouse cursor will be locked to the window bounds while overlays are disabled, /// but may otherwise move freely. /// [Description("During Gameplay")] diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index de67b2d46d..175722c44e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -18,7 +18,6 @@ using osu.Framework.Threading; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.Containers; -using osu.Game.Input; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Overlays; @@ -67,9 +66,6 @@ namespace osu.Game.Screens.Play private Bindable mouseWheelDisabled; - [Resolved(CanBeNull = true)] - private ConfineMouseTracker confineMouseTracker { get; set; } - private readonly Bindable storyboardReplacesBackground = new Bindable(); public int RestartCount; @@ -229,8 +225,6 @@ namespace osu.Game.Screens.Play DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); - confineMouseTracker?.OverlayActivationMode.BindTo(OverlayActivationMode); - // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); From e64cee10b8fa82ca05e2d365c760924a75fd3cfe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 19:07:31 +0900 Subject: [PATCH 3681/6909] Add obsoleted Bindable property back to SettingsItem for compatibility --- osu.Game/Overlays/Settings/SettingsItem.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index ad6aaafd9d..278479e04f 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -54,6 +54,13 @@ namespace osu.Game.Overlays.Settings } } + [Obsolete("Use Current instead")] // Can be removed 20210406 + public Bindable Bindable + { + get => Current; + set => Current = value; + } + public virtual Bindable Current { get => controlWithCurrent.Current; From a2796d2c017bce185aaca88e3b581d56599888b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 19:20:53 +0900 Subject: [PATCH 3682/6909] Add repeats display to timeline blueprints --- .../Timeline/TimelineHitObjectBlueprint.cs | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index bc2ccfc605..f0757a3dda 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -199,7 +199,40 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline base.Update(); // no bindable so we perform this every update - Width = (float)(HitObject.GetEndTime() - HitObject.StartTime); + float duration = (float)(HitObject.GetEndTime() - HitObject.StartTime); + + if (Width != duration) + { + Width = duration; + + // kind of haphazard but yeah, no bindables. + if (HitObject is IHasRepeats repeats) + updateRepeats(repeats); + } + } + + private Container repeatsContainer; + + private void updateRepeats(IHasRepeats repeats) + { + repeatsContainer?.Expire(); + + mainComponents.Add(repeatsContainer = new Container + { + RelativeSizeAxes = Axes.Both, + }); + + for (int i = 0; i < repeats.RepeatCount; i++) + { + repeatsContainer.Add(new Circle + { + Size = new Vector2(circle_size / 2), + Anchor = Anchor.CentreLeft, + Origin = Anchor.Centre, + RelativePositionAxes = Axes.X, + X = (float)(i + 1) / (repeats.RepeatCount + 1), + }); + } } protected override bool ShouldBeConsideredForInput(Drawable child) => true; From 06a51297a3f2b20d40a99e31d9ee024dea020261 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 19:26:57 +0900 Subject: [PATCH 3683/6909] Use content instead of exposing the flow container --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 4 ++-- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 2 +- osu.Game/Screens/Edit/Setup/SetupSection.cs | 7 +++---- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 31a2c2ce1a..4ddee2acc6 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Edit.Setup [BackgroundDependencyLoader] private void load() { - Flow.Children = new Drawable[] + Children = new Drawable[] { new OsuSpriteText { @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Edit.Setup }, }; - foreach (var item in Flow.OfType()) + foreach (var item in Children.OfType()) item.OnCommit += onCommit; } diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 86d7968856..17ecfdd52e 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Edit.Setup AutoSizeAxes = Axes.Y, }; - Flow.Children = new Drawable[] + Children = new Drawable[] { backgroundSpriteContainer = new Container { diff --git a/osu.Game/Screens/Edit/Setup/SetupSection.cs b/osu.Game/Screens/Edit/Setup/SetupSection.cs index 54e383a4d8..cdf17d355e 100644 --- a/osu.Game/Screens/Edit/Setup/SetupSection.cs +++ b/osu.Game/Screens/Edit/Setup/SetupSection.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; @@ -14,7 +13,7 @@ namespace osu.Game.Screens.Edit.Setup { internal class SetupSection : Container { - protected FillFlowContainer Flow; + private readonly FillFlowContainer flow; [Resolved] protected OsuColour Colours { get; private set; } @@ -22,7 +21,7 @@ namespace osu.Game.Screens.Edit.Setup [Resolved] protected IBindable Beatmap { get; private set; } - public override void Add(Drawable drawable) => throw new InvalidOperationException("Use Flow.Add instead"); + protected override Container Content => flow; public SetupSection() { @@ -31,7 +30,7 @@ namespace osu.Game.Screens.Edit.Setup Padding = new MarginPadding(10); - InternalChild = Flow = new FillFlowContainer + InternalChild = flow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, From 461be02e6f9ff563ebe458603f9ecaaf6fd9aa26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 19:34:21 +0900 Subject: [PATCH 3684/6909] Update with underlying changes --- osu.Game/Screens/Edit/Setup/DifficultySection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 4ed8d0164b..f23bc0f3b2 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Edit.Setup [BackgroundDependencyLoader] private void load() { - Flow.Children = new Drawable[] + Children = new Drawable[] { new OsuSpriteText { @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit.Setup }, }; - foreach (var item in Flow.OfType>()) + foreach (var item in Children.OfType>()) item.Current.ValueChanged += onValueChanged; } From e8b34ba4acd0d5fb61e06bb3f1ce4c14bffdded4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 20:57:39 +0900 Subject: [PATCH 3685/6909] Fix incorrectly committed testing change --- 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 9b7b7392d8..c216d1efe9 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Bindable = config.GetBindable(OsuSetting.ReleaseStream), }); - //if (updateManager?.CanCheckForUpdate == true) + if (updateManager?.CanCheckForUpdate == true) { Add(checkForUpdatesButton = new SettingsButton { From 478f2dec9624f13c9fdf8503774094a19da2e9da Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Tue, 6 Oct 2020 22:39:35 +1030 Subject: [PATCH 3686/6909] Maintain the current gameplay state in OsuGame --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- osu.Game/Input/ConfineMouseTracker.cs | 19 +++++++-------- osu.Game/Input/OsuConfineMouseMode.cs | 4 ++-- osu.Game/OsuGame.cs | 5 ++++ osu.Game/Screens/Play/Player.cs | 28 ++++++++++++---------- 5 files changed, 33 insertions(+), 25 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 71cbdb345c..78179a781a 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -70,7 +70,7 @@ namespace osu.Game.Configuration Set(OsuSetting.MouseDisableButtons, false); Set(OsuSetting.MouseDisableWheel, false); - Set(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.WhenOverlaysDisabled); + Set(OsuSetting.ConfineMouseMode, OsuConfineMouseMode.DuringGameplay); // Graphics Set(OsuSetting.ShowFpsDisplay, false); diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index 3b54c03bb0..c1089874ae 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -7,30 +7,29 @@ using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Configuration; -using osu.Game.Overlays; namespace osu.Game.Input { /// - /// Connects with , - /// while binding . - /// It is assumed that while overlay activation is , we should also confine the - /// mouse cursor if it has been requested with . + /// Connects with . + /// If is true, we should also confine the mouse cursor if it has been + /// requested with . /// public class ConfineMouseTracker : Component { private Bindable frameworkConfineMode; private Bindable osuConfineMode; - private IBindable overlayActivationMode; + private IBindable isGameplay; [BackgroundDependencyLoader] private void load(OsuGame game, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) { frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); + isGameplay = game.IsGameplay.GetBoundCopy(); + osuConfineMode.ValueChanged += _ => updateConfineMode(); - overlayActivationMode = game.OverlayActivationMode.GetBoundCopy(); - overlayActivationMode.BindValueChanged(_ => updateConfineMode(), true); + isGameplay.BindValueChanged(_ => updateConfineMode(), true); } private void updateConfineMode() @@ -45,8 +44,8 @@ namespace osu.Game.Input frameworkConfineMode.Value = ConfineMouseMode.Fullscreen; break; - case OsuConfineMouseMode.WhenOverlaysDisabled: - frameworkConfineMode.Value = overlayActivationMode?.Value == OverlayActivation.Disabled ? ConfineMouseMode.Always : ConfineMouseMode.Never; + case OsuConfineMouseMode.DuringGameplay: + frameworkConfineMode.Value = isGameplay.Value ? ConfineMouseMode.Always : ConfineMouseMode.Never; break; case OsuConfineMouseMode.Always: diff --git a/osu.Game/Input/OsuConfineMouseMode.cs b/osu.Game/Input/OsuConfineMouseMode.cs index e8411a3d9f..32b456395c 100644 --- a/osu.Game/Input/OsuConfineMouseMode.cs +++ b/osu.Game/Input/OsuConfineMouseMode.cs @@ -23,11 +23,11 @@ namespace osu.Game.Input Fullscreen, /// - /// The mouse cursor will be locked to the window bounds while overlays are disabled, + /// The mouse cursor will be locked to the window bounds during gameplay, /// but may otherwise move freely. /// [Description("During Gameplay")] - WhenOverlaysDisabled, + DuringGameplay, /// /// The mouse cursor will always be locked to the window bounds while the game has focus. diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 06e6e4bfb0..466ff13615 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -97,6 +97,11 @@ namespace osu.Game /// public readonly IBindable OverlayActivationMode = new Bindable(); + /// + /// Whether gameplay is currently active. + /// + public readonly Bindable IsGameplay = new BindableBool(); + protected OsuScreenStack ScreenStack; protected BackButton BackButton; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 175722c44e..a217d2a396 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,6 +68,8 @@ namespace osu.Game.Screens.Play private readonly Bindable storyboardReplacesBackground = new Bindable(); + private readonly Bindable isGameplay = new Bindable(); + public int RestartCount; [Resolved] @@ -156,7 +158,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuConfigManager config) + private void load(AudioManager audio, OsuConfigManager config, OsuGame game) { Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray(); @@ -172,6 +174,8 @@ namespace osu.Game.Screens.Play mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); + isGameplay.BindTo(game.IsGameplay); + DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); ScoreProcessor = ruleset.CreateScoreProcessor(); @@ -219,9 +223,9 @@ namespace osu.Game.Screens.Play skipOverlay.Hide(); } - DrawableRuleset.IsPaused.BindValueChanged(_ => updateOverlayActivationMode()); - DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateOverlayActivationMode()); - breakTracker.IsBreakTime.BindValueChanged(_ => updateOverlayActivationMode()); + DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState()); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState()); + breakTracker.IsBreakTime.BindValueChanged(_ => updateGameplayState()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); @@ -353,14 +357,11 @@ namespace osu.Game.Screens.Play HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue; } - private void updateOverlayActivationMode() + private void updateGameplayState() { - bool canTriggerOverlays = DrawableRuleset.IsPaused.Value || breakTracker.IsBreakTime.Value; - - if (DrawableRuleset.HasReplayLoaded.Value || canTriggerOverlays) - OverlayActivationMode.Value = OverlayActivation.UserTriggered; - else - OverlayActivationMode.Value = OverlayActivation.Disabled; + bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value; + OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; + isGameplay.Value = inGameplay; } private void updatePauseOnFocusLostState() => @@ -657,7 +658,7 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToTrack(musicController.CurrentTrack); - updateOverlayActivationMode(); + updateGameplayState(); } public override void OnSuspending(IScreen next) @@ -693,6 +694,9 @@ namespace osu.Game.Screens.Play musicController.ResetTrackAdjustments(); + // Ensure we reset the IsGameplay state + isGameplay.Value = false; + fadeOut(); return base.OnExiting(next); } From b2dad67adead750b3b3f377d61449bf53e00d5a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 21:28:59 +0900 Subject: [PATCH 3687/6909] Fix unresolvable dependency in settings test scene --- 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 c216d1efe9..c0a525e012 100644 --- a/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/UpdateSettings.cs @@ -23,7 +23,7 @@ namespace osu.Game.Overlays.Settings.Sections.General private SettingsButton checkForUpdatesButton; - [Resolved] + [Resolved(CanBeNull = true)] private NotificationOverlay notifications { get; set; } [BackgroundDependencyLoader(true)] @@ -47,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.General { if (!t.Result) { - notifications.Post(new SimpleNotification + notifications?.Post(new SimpleNotification { Text = $"You are running the latest release ({game.Version})", Icon = FontAwesome.Solid.CheckCircle, From 14c734c24407d7e8250ccf8dcba113065c37d623 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 21:21:09 +0900 Subject: [PATCH 3688/6909] Add a very simple method of applying batch changes to EditorBeatmap --- osu.Game/Screens/Edit/EditorBeatmap.cs | 69 ++++++++++++++++--- .../Edit/LegacyEditorBeatmapPatcher.cs | 21 +++--- 2 files changed, 73 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 3248c5b8be..549423dfb8 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -91,6 +91,8 @@ namespace osu.Game.Screens.Edit private readonly HashSet pendingUpdates = new HashSet(); + private bool isBatchApplying; + /// /// Adds a collection of s to this . /// @@ -126,12 +128,17 @@ namespace osu.Game.Screens.Edit mutableHitObjects.Insert(index, hitObject); - // must be run after any change to hitobject ordering - beatmapProcessor?.PreProcess(); - processHitObject(hitObject); - beatmapProcessor?.PostProcess(); + if (isBatchApplying) + batchPendingInserts.Add(hitObject); + else + { + // must be run after any change to hitobject ordering + beatmapProcessor?.PreProcess(); + processHitObject(hitObject); + beatmapProcessor?.PostProcess(); - HitObjectAdded?.Invoke(hitObject); + HitObjectAdded?.Invoke(hitObject); + } } /// @@ -180,12 +187,58 @@ namespace osu.Game.Screens.Edit bindable.UnbindAll(); startTimeBindables.Remove(hitObject); - // must be run after any change to hitobject ordering + if (isBatchApplying) + batchPendingDeletes.Add(hitObject); + else + { + // must be run after any change to hitobject ordering + beatmapProcessor?.PreProcess(); + processHitObject(hitObject); + beatmapProcessor?.PostProcess(); + + HitObjectRemoved?.Invoke(hitObject); + } + } + + private readonly List batchPendingInserts = new List(); + + private readonly List batchPendingDeletes = new List(); + + /// + /// Apply a batch of operations in one go, without performing Pre/Postprocessing each time. + /// + /// The function which will apply the batch changes. + public void ApplyBatchChanges(Action applyFunction) + { + if (isBatchApplying) + throw new InvalidOperationException("Attempting to perform a batch application from within an existing batch"); + + isBatchApplying = true; + + applyFunction(this); + beatmapProcessor?.PreProcess(); - processHitObject(hitObject); beatmapProcessor?.PostProcess(); - HitObjectRemoved?.Invoke(hitObject); + isBatchApplying = false; + + foreach (var h in batchPendingDeletes) + { + processHitObject(h); + HitObjectRemoved?.Invoke(h); + } + + batchPendingDeletes.Clear(); + + foreach (var h in batchPendingInserts) + { + processHitObject(h); + HitObjectAdded?.Invoke(h); + } + + batchPendingInserts.Clear(); + + isBatchApplying = false; } /// diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index 57b7ce6940..fb7d0dd826 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -68,16 +68,19 @@ namespace osu.Game.Screens.Edit toRemove.Sort(); toAdd.Sort(); - // Apply the changes. - for (int i = toRemove.Count - 1; i >= 0; i--) - editorBeatmap.RemoveAt(toRemove[i]); - - if (toAdd.Count > 0) + editorBeatmap.ApplyBatchChanges(eb => { - IBeatmap newBeatmap = readBeatmap(newState); - foreach (var i in toAdd) - editorBeatmap.Insert(i, newBeatmap.HitObjects[i]); - } + // Apply the changes. + for (int i = toRemove.Count - 1; i >= 0; i--) + eb.RemoveAt(toRemove[i]); + + if (toAdd.Count > 0) + { + IBeatmap newBeatmap = readBeatmap(newState); + foreach (var i in toAdd) + eb.Insert(i, newBeatmap.HitObjects[i]); + } + }); } private string readString(byte[] state) => Encoding.UTF8.GetString(state); From 09f5e9c9eb545bdc8880a6362e1de61f39077fb9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 22:09:48 +0900 Subject: [PATCH 3689/6909] Use batch change application in many places that can benefit from it --- .../Compose/Components/SelectionHandler.cs | 5 +--- osu.Game/Screens/Edit/Editor.cs | 3 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 28 +++++++++++++------ 3 files changed, 21 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index fdf8dbe44e..7808d7a5bc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -239,10 +239,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void deleteSelected() { ChangeHandler?.BeginChange(); - - foreach (var h in selectedBlueprints.ToList()) - EditorBeatmap?.Remove(h.HitObject); - + EditorBeatmap?.RemoveRange(selectedBlueprints.Select(b => b.HitObject)); ChangeHandler?.EndChange(); } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 4b4266d049..3c5cbf30e9 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -484,8 +484,7 @@ namespace osu.Game.Screens.Edit protected void Cut() { Copy(); - foreach (var h in editorBeatmap.SelectedHitObjects.ToArray()) - editorBeatmap.Remove(h); + editorBeatmap.RemoveRange(editorBeatmap.SelectedHitObjects.ToArray()); } protected void Copy() diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 549423dfb8..1357f12055 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -99,8 +99,11 @@ namespace osu.Game.Screens.Edit /// The s to add. public void AddRange(IEnumerable hitObjects) { - foreach (var h in hitObjects) - Add(h); + ApplyBatchChanges(_ => + { + foreach (var h in hitObjects) + Add(h); + }); } /// @@ -166,6 +169,19 @@ namespace osu.Game.Screens.Edit return true; } + /// + /// Removes a collection of s to this . + /// + /// The s to remove. + public void RemoveRange(IEnumerable hitObjects) + { + ApplyBatchChanges(_ => + { + foreach (var h in hitObjects) + Remove(h); + }); + } + /// /// Finds the index of a in this . /// @@ -237,18 +253,12 @@ namespace osu.Game.Screens.Edit } batchPendingInserts.Clear(); - - isBatchApplying = false; } /// /// Clears all from this . /// - public void Clear() - { - foreach (var h in HitObjects.ToArray()) - Remove(h); - } + public void Clear() => RemoveRange(HitObjects.ToArray()); protected override void Update() { From afe3d3989a08a4e6aa0e8d88cc01d7cff78e40ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 19:04:50 +0900 Subject: [PATCH 3690/6909] Force first hitobject to be a NewCombo in BeatmapProcessor preprocessing step --- osu.Game/Beatmaps/BeatmapProcessor.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapProcessor.cs b/osu.Game/Beatmaps/BeatmapProcessor.cs index 250cc49ad4..b7b5adc52e 100644 --- a/osu.Game/Beatmaps/BeatmapProcessor.cs +++ b/osu.Game/Beatmaps/BeatmapProcessor.cs @@ -22,8 +22,18 @@ namespace osu.Game.Beatmaps { IHasComboInformation lastObj = null; + bool isFirst = true; + foreach (var obj in Beatmap.HitObjects.OfType()) { + if (isFirst) + { + obj.NewCombo = true; + + // first hitobject should always be marked as a new combo for sanity. + isFirst = false; + } + if (obj.NewCombo) { obj.IndexInCurrentCombo = 0; From 1f2dd13b49ccaeb572150b159bac8420c22f2dd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 6 Oct 2020 19:04:58 +0900 Subject: [PATCH 3691/6909] Update tests --- .../Editing/LegacyEditorBeatmapPatcherTest.cs | 45 ++++++++++--------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index b491157627..afaaafdd26 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Editing { HitObjects = { - new HitCircle { StartTime = 1000 } + new HitCircle { StartTime = 1000, NewCombo = true } } }; @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new HitCircle { StartTime = 3000 }, }); @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new HitCircle { StartTime = 2000 }, new HitCircle { StartTime = 3000 }, }); @@ -100,7 +100,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new HitCircle { StartTime = 2000 }, new HitCircle { StartTime = 3000 }, }); @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Editing { HitObjects = { - new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 500, NewCombo = true }, (OsuHitObject)current.HitObjects[1], (OsuHitObject)current.HitObjects[2], } @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new HitCircle { StartTime = 2000 }, new HitCircle { StartTime = 3000 }, }); @@ -146,7 +146,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new OsuHitObject[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new Slider { StartTime = 2000, @@ -188,7 +188,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 1000, NewCombo = true }, new HitCircle { StartTime = 2000 }, new HitCircle { StartTime = 3000 }, }); @@ -197,7 +197,7 @@ namespace osu.Game.Tests.Editing { HitObjects = { - new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 500, NewCombo = true }, (OsuHitObject)current.HitObjects[0], new HitCircle { StartTime = 1500 }, (OsuHitObject)current.HitObjects[1], @@ -216,7 +216,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 500, NewCombo = true }, new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1500 }, new HitCircle { StartTime = 2000 }, @@ -226,6 +226,9 @@ namespace osu.Game.Tests.Editing new HitCircle { StartTime = 3500 }, }); + var patchedFirst = (HitCircle)current.HitObjects[1]; + patchedFirst.NewCombo = true; + var patch = new OsuBeatmap { HitObjects = @@ -244,7 +247,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 500, NewCombo = true }, new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1500 }, new HitCircle { StartTime = 2000 }, @@ -277,7 +280,7 @@ namespace osu.Game.Tests.Editing { current.AddRange(new[] { - new HitCircle { StartTime = 500 }, + new HitCircle { StartTime = 500, NewCombo = true }, new HitCircle { StartTime = 1000 }, new HitCircle { StartTime = 1500 }, new HitCircle { StartTime = 2000 }, @@ -291,7 +294,7 @@ namespace osu.Game.Tests.Editing { HitObjects = { - new HitCircle { StartTime = 750 }, + new HitCircle { StartTime = 750, NewCombo = true }, (OsuHitObject)current.HitObjects[1], (OsuHitObject)current.HitObjects[4], (OsuHitObject)current.HitObjects[5], @@ -309,20 +312,20 @@ namespace osu.Game.Tests.Editing { 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) }, + new HitCircle { StartTime = 500, Position = new Vector2(50), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(100), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(150), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(200), NewCombo = true }, }); 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) }, + new HitCircle { StartTime = 500, Position = new Vector2(150), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(100), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(50), NewCombo = true }, + new HitCircle { StartTime = 500, Position = new Vector2(200), NewCombo = true }, } }; From 01636d501a0fc5dbf4a1959c23e9d3eaf577817f Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 6 Oct 2020 18:36:15 +0300 Subject: [PATCH 3692/6909] Add MinResults test and starts of score portion tests --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 38d2b4a47f..52848cb716 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.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; @@ -9,6 +10,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; @@ -54,6 +56,44 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); } + [TestCase(ScoringMode.Standardised, "osu", typeof(HitCircle), HitResult.Great, 575_000)] + public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, string rulesetName, Type hitObjectType, HitResult hitResult, int expectedScore) + { + IBeatmap fourObjectBeatmap = new TestBeatmap(new OsuRuleset().RulesetInfo) + { + HitObjects = new List(Enumerable.Repeat((HitObject)Activator.CreateInstance(hitObjectType), 4)) + }; + scoreProcessor.Mode.Value = scoringMode; + scoreProcessor.ApplyBeatmap(fourObjectBeatmap); + + for (int i = 0; i < 4; i++) + { + var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new Judgement()) + { + Type = i == 2 ? HitResult.Miss : hitResult + }; + scoreProcessor.ApplyResult(judgementResult); + } + + Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); + } + + [TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)] + [TestCase(HitResult.Meh, HitResult.Miss)] + [TestCase(HitResult.Ok, HitResult.Miss)] + [TestCase(HitResult.Good, HitResult.Miss)] + [TestCase(HitResult.Great, HitResult.Miss)] + [TestCase(HitResult.Perfect, HitResult.Miss)] + [TestCase(HitResult.SmallTickHit, HitResult.SmallTickMiss)] + [TestCase(HitResult.LargeTickHit, HitResult.LargeTickMiss)] + [TestCase(HitResult.SmallBonus, HitResult.IgnoreMiss)] + [TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)] + public void TestMinResults(HitResult hitResult, HitResult expectedMinResult) + { + var result = new JudgementResult(new HitObject(), new TestJudgement(hitResult)); + Assert.IsTrue(result.Judgement.MinResult == expectedMinResult); + } + [TestCase(HitResult.None, false)] [TestCase(HitResult.IgnoreMiss, false)] [TestCase(HitResult.IgnoreHit, false)] @@ -153,5 +193,15 @@ namespace osu.Game.Tests.Rulesets.Scoring { Assert.IsTrue(hitResult.IsScorable() == expectedReturnValue); } + + private class TestJudgement : Judgement + { + public override HitResult MaxResult { get; } + + public TestJudgement(HitResult maxResult) + { + MaxResult = maxResult; + } + } } } From bdc84c529114b14957aeb9959f2fddca675956f2 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 6 Oct 2020 19:53:24 +0300 Subject: [PATCH 3693/6909] Finish score portion tests for standardised scoring mode --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 38 +++++++++++++++---- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 52848cb716..d38a2a89cc 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.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; @@ -10,7 +9,6 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; @@ -56,12 +54,23 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); } - [TestCase(ScoringMode.Standardised, "osu", typeof(HitCircle), HitResult.Great, 575_000)] - public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, string rulesetName, Type hitObjectType, HitResult hitResult, int expectedScore) + [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] + [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] + [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] + [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] + [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] + [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 0)] // TODO: idk, this should be 225_000 from accuracy portion + [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] + [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] + [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 30)] + [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 150)] + public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) { - IBeatmap fourObjectBeatmap = new TestBeatmap(new OsuRuleset().RulesetInfo) + var minResult = new JudgementResult(new HitObject(), new TestJudgement(hitResult)).Judgement.MinResult; + + IBeatmap fourObjectBeatmap = new TestBeatmap(new RulesetInfo()) { - HitObjects = new List(Enumerable.Repeat((HitObject)Activator.CreateInstance(hitObjectType), 4)) + HitObjects = new List(Enumerable.Repeat(new TestHitObject(maxResult), 4)) }; scoreProcessor.Mode.Value = scoringMode; scoreProcessor.ApplyBeatmap(fourObjectBeatmap); @@ -70,7 +79,7 @@ namespace osu.Game.Tests.Rulesets.Scoring { var judgementResult = new JudgementResult(fourObjectBeatmap.HitObjects[i], new Judgement()) { - Type = i == 2 ? HitResult.Miss : hitResult + Type = i == 2 ? minResult : hitResult }; scoreProcessor.ApplyResult(judgementResult); } @@ -203,5 +212,20 @@ namespace osu.Game.Tests.Rulesets.Scoring MaxResult = maxResult; } } + + private class TestHitObject : HitObject + { + private readonly HitResult maxResult; + + public override Judgement CreateJudgement() + { + return new TestJudgement(maxResult); + } + + public TestHitObject(HitResult maxResult) + { + this.maxResult = maxResult; + } + } } } From f5a6beb4e55f17407640c32f85cd21d1baf86284 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 6 Oct 2020 19:01:03 +0200 Subject: [PATCH 3694/6909] Remove obsoletion notice. --- osu.Game/Rulesets/Ruleset.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 2ba884efc2..ae1407fb8f 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -160,7 +160,6 @@ namespace osu.Game.Rulesets public virtual PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; - [Obsolete("Use the DifficultyAttributes overload instead.")] public PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) { var difficultyCalculator = CreateDifficultyCalculator(beatmap); From 879131c6752fcad96f42fe4bb17de0dc75b4095a Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 6 Oct 2020 20:02:33 +0300 Subject: [PATCH 3695/6909] Also test Goods and Perfects --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index d38a2a89cc..e1afb82e81 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -57,14 +57,16 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] + [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 3_350_000 / 7.0)] [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] + [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 0)] // TODO: idk, this should be 225_000 from accuracy portion [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 30)] [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 150)] - public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) + public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, double expectedScore) { var minResult = new JudgementResult(new HitObject(), new TestJudgement(hitResult)).Judgement.MinResult; From 6684a98a321ff086811225f44b1d3c13b8039ec2 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 6 Oct 2020 20:24:42 +0300 Subject: [PATCH 3696/6909] Also test Classic scoring --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index e1afb82e81..dd364a645c 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -61,11 +61,23 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] - [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 0)] // TODO: idk, this should be 225_000 from accuracy portion + [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 0)] // TODO: this should probably be 225_000 from accuracy portion [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 30)] [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 150)] + [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] + [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] + [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] + [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 3744 / 7.0)] + [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] + [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 0)] + [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] + [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] + [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] + [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, double expectedScore) { var minResult = new JudgementResult(new HitObject(), new TestJudgement(hitResult)).Judgement.MinResult; From a31fe5f5ff9d23532c2b3c681a335236e51a72a7 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 6 Oct 2020 22:26:18 +0300 Subject: [PATCH 3697/6909] Temporarily remove SmallTickHit tests --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index dd364a645c..2ad9837654 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -60,8 +60,6 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 3_350_000 / 7.0)] [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] - [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] - [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 0)] // TODO: this should probably be 225_000 from accuracy portion [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 30)] @@ -72,8 +70,6 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 3744 / 7.0)] [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 0)] [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] From 4c9840ccf1f8bf91801f099b1c3d8ac43f9cde2c Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 6 Oct 2020 22:57:55 +0300 Subject: [PATCH 3698/6909] Apply review suggestions --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 2ad9837654..1181d82d09 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, double expectedScore) { - var minResult = new JudgementResult(new HitObject(), new TestJudgement(hitResult)).Judgement.MinResult; + var minResult = new TestJudgement(hitResult).MinResult; IBeatmap fourObjectBeatmap = new TestBeatmap(new RulesetInfo()) { @@ -109,8 +109,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.LargeBonus, HitResult.IgnoreMiss)] public void TestMinResults(HitResult hitResult, HitResult expectedMinResult) { - var result = new JudgementResult(new HitObject(), new TestJudgement(hitResult)); - Assert.IsTrue(result.Judgement.MinResult == expectedMinResult); + Assert.AreEqual(expectedMinResult, new TestJudgement(hitResult).MinResult); } [TestCase(HitResult.None, false)] @@ -130,7 +129,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.LargeBonus, false)] public void TestAffectsCombo(HitResult hitResult, bool expectedReturnValue) { - Assert.IsTrue(hitResult.AffectsCombo() == expectedReturnValue); + Assert.AreEqual(expectedReturnValue, hitResult.AffectsCombo()); } [TestCase(HitResult.None, false)] @@ -150,7 +149,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.LargeBonus, false)] public void TestAffectsAccuracy(HitResult hitResult, bool expectedReturnValue) { - Assert.IsTrue(hitResult.AffectsAccuracy() == expectedReturnValue); + Assert.AreEqual(expectedReturnValue, hitResult.AffectsAccuracy()); } [TestCase(HitResult.None, false)] @@ -170,7 +169,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.LargeBonus, true)] public void TestIsBonus(HitResult hitResult, bool expectedReturnValue) { - Assert.IsTrue(hitResult.IsBonus() == expectedReturnValue); + Assert.AreEqual(expectedReturnValue, hitResult.IsBonus()); } [TestCase(HitResult.None, false)] @@ -190,7 +189,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.LargeBonus, true)] public void TestIsHit(HitResult hitResult, bool expectedReturnValue) { - Assert.IsTrue(hitResult.IsHit() == expectedReturnValue); + Assert.AreEqual(expectedReturnValue, hitResult.IsHit()); } [TestCase(HitResult.None, false)] @@ -210,7 +209,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(HitResult.LargeBonus, true)] public void TestIsScorable(HitResult hitResult, bool expectedReturnValue) { - Assert.IsTrue(hitResult.IsScorable() == expectedReturnValue); + Assert.AreEqual(expectedReturnValue, hitResult.IsScorable()); } private class TestJudgement : Judgement From 5e314c0662919757871761414682e3389886bfa3 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 6 Oct 2020 22:58:09 +0300 Subject: [PATCH 3699/6909] Write new test for small ticks --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 1181d82d09..f81ab6c866 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -97,6 +97,40 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); } + [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 6_850_000 / 7.0)] + [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 6_400_000 / 7.0)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 1950 / 7.0)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 1500 / 7.0)] + public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, double expectedScore) + { + IEnumerable hitObjects = Enumerable + .Repeat(new TestHitObject(HitResult.SmallTickHit), 4) + .Append(new TestHitObject(HitResult.Ok)); + IBeatmap fiveObjectBeatmap = new TestBeatmap(new RulesetInfo()) + { + HitObjects = hitObjects.ToList() + }; + scoreProcessor.Mode.Value = scoringMode; + scoreProcessor.ApplyBeatmap(fiveObjectBeatmap); + + for (int i = 0; i < 4; i++) + { + var judgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects[i], new Judgement()) + { + Type = i == 2 ? HitResult.SmallTickMiss : hitResult + }; + scoreProcessor.ApplyResult(judgementResult); + } + + var lastJudgementResult = new JudgementResult(fiveObjectBeatmap.HitObjects.Last(), new Judgement()) + { + Type = HitResult.Ok + }; + scoreProcessor.ApplyResult(lastJudgementResult); + + Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); + } + [TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)] [TestCase(HitResult.Meh, HitResult.Miss)] [TestCase(HitResult.Ok, HitResult.Miss)] From 8847b88e653490a62d5319bc066702d9151d100c Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 7 Oct 2020 11:44:41 +1030 Subject: [PATCH 3700/6909] Fix unit tests trying to resolve OsuGame --- osu.Game/Screens/Play/Player.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a217d2a396..932c5ba1df 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -158,7 +158,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuConfigManager config, OsuGame game) + private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) { Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray(); @@ -174,7 +174,8 @@ namespace osu.Game.Screens.Play mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); - isGameplay.BindTo(game.IsGameplay); + if (game is OsuGame osuGame) + isGameplay.BindTo(osuGame.IsGameplay); DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); From c1a8fe01ef6a5253e060fa1db27cbb730882c4a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 11:09:45 +0900 Subject: [PATCH 3701/6909] Fix postprocess order in batch events --- osu.Game/Screens/Edit/EditorBeatmap.cs | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 1357f12055..be032d3104 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -234,25 +234,19 @@ namespace osu.Game.Screens.Edit applyFunction(this); beatmapProcessor?.PreProcess(); + + foreach (var h in batchPendingDeletes) processHitObject(h); + foreach (var h in batchPendingInserts) processHitObject(h); + beatmapProcessor?.PostProcess(); - isBatchApplying = false; - - foreach (var h in batchPendingDeletes) - { - processHitObject(h); - HitObjectRemoved?.Invoke(h); - } + foreach (var h in batchPendingDeletes) HitObjectRemoved?.Invoke(h); + foreach (var h in batchPendingInserts) HitObjectAdded?.Invoke(h); batchPendingDeletes.Clear(); - - foreach (var h in batchPendingInserts) - { - processHitObject(h); - HitObjectAdded?.Invoke(h); - } - batchPendingInserts.Clear(); + + isBatchApplying = false; } /// From a8151d5c635c0803dc9fc5812904156ce49b405e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 13:45:41 +0900 Subject: [PATCH 3702/6909] Fix HitWindows getting serialized alongside HitObjects These were being serialized as the base type. On deserialization, due to the HitWindow of objects being non-null, they would not get correctly initialised by the CreateHitWindows() virtual method. - Closes #10403 --- osu.Game/Rulesets/Objects/HitObject.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 0dfde834ee..826d411822 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -77,6 +77,7 @@ namespace osu.Game.Rulesets.Objects /// /// The hit windows for this . /// + [JsonIgnore] public HitWindows HitWindows { get; set; } private readonly List nestedHitObjects = new List(); From a6d1484ad5af2f5eb921133a283477b235da0d56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 14:26:01 +0900 Subject: [PATCH 3703/6909] Add arbirary precision specification for now --- osu.Game/Screens/Edit/Setup/DifficultySection.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index f23bc0f3b2..2d8031c3c8 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -38,7 +38,8 @@ namespace osu.Game.Screens.Edit.Setup { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 2, - MaxValue = 7 + MaxValue = 7, + Precision = 0.1f, } }, healthDrainSlider = new LabelledSliderBar @@ -49,7 +50,8 @@ namespace osu.Game.Screens.Edit.Setup { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, - MaxValue = 10 + MaxValue = 10, + Precision = 0.1f, } }, approachRateSlider = new LabelledSliderBar @@ -60,7 +62,8 @@ namespace osu.Game.Screens.Edit.Setup { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, - MaxValue = 10 + MaxValue = 10, + Precision = 0.1f, } }, overallDifficultySlider = new LabelledSliderBar @@ -71,7 +74,8 @@ namespace osu.Game.Screens.Edit.Setup { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, - MaxValue = 10 + MaxValue = 10, + Precision = 0.1f, } }, }; From c8c5998af475a9860041eea732e61a10f949f069 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 7 Oct 2020 16:02:35 +1030 Subject: [PATCH 3704/6909] Bail if FrameworkSetting.ConfineMouseMode is unavailable --- osu.Game/Input/ConfineMouseTracker.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index c1089874ae..3d16e44607 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -34,6 +34,10 @@ namespace osu.Game.Input private void updateConfineMode() { + // confine mode is unavailable on some platforms + if (frameworkConfineMode.Disabled) + return; + switch (osuConfineMode.Value) { case OsuConfineMouseMode.Never: From 7fff762dfc91222fe1823f3b73f90aa588b5129c Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 7 Oct 2020 16:14:49 +1030 Subject: [PATCH 3705/6909] Rename IsGameplay --- osu.Game/Input/ConfineMouseTracker.cs | 10 +++++----- osu.Game/OsuGame.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 10 +++++----- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index 3d16e44607..3dadae6317 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -12,24 +12,24 @@ namespace osu.Game.Input { /// /// Connects with . - /// If is true, we should also confine the mouse cursor if it has been + /// If is true, we should also confine the mouse cursor if it has been /// requested with . /// public class ConfineMouseTracker : Component { private Bindable frameworkConfineMode; private Bindable osuConfineMode; - private IBindable isGameplay; + private IBindable localUserPlaying; [BackgroundDependencyLoader] private void load(OsuGame game, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) { frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); - isGameplay = game.IsGameplay.GetBoundCopy(); + localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); osuConfineMode.ValueChanged += _ => updateConfineMode(); - isGameplay.BindValueChanged(_ => updateConfineMode(), true); + localUserPlaying.BindValueChanged(_ => updateConfineMode(), true); } private void updateConfineMode() @@ -49,7 +49,7 @@ namespace osu.Game.Input break; case OsuConfineMouseMode.DuringGameplay: - frameworkConfineMode.Value = isGameplay.Value ? ConfineMouseMode.Always : ConfineMouseMode.Never; + frameworkConfineMode.Value = localUserPlaying.Value ? ConfineMouseMode.Always : ConfineMouseMode.Never; break; case OsuConfineMouseMode.Always: diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 466ff13615..c09ad0eeb9 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -98,9 +98,9 @@ namespace osu.Game public readonly IBindable OverlayActivationMode = new Bindable(); /// - /// Whether gameplay is currently active. + /// Whether the local user is currently interacting with the game in a way that should not be interrupted. /// - public readonly Bindable IsGameplay = new BindableBool(); + public readonly Bindable LocalUserPlaying = new BindableBool(); protected OsuScreenStack ScreenStack; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 932c5ba1df..e6c15521af 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play private readonly Bindable storyboardReplacesBackground = new Bindable(); - private readonly Bindable isGameplay = new Bindable(); + private readonly Bindable localUserPlaying = new Bindable(); public int RestartCount; @@ -175,7 +175,7 @@ namespace osu.Game.Screens.Play mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); if (game is OsuGame osuGame) - isGameplay.BindTo(osuGame.IsGameplay); + localUserPlaying.BindTo(osuGame.LocalUserPlaying); DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); @@ -362,7 +362,7 @@ namespace osu.Game.Screens.Play { bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value; OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; - isGameplay.Value = inGameplay; + localUserPlaying.Value = inGameplay; } private void updatePauseOnFocusLostState() => @@ -695,8 +695,8 @@ namespace osu.Game.Screens.Play musicController.ResetTrackAdjustments(); - // Ensure we reset the IsGameplay state - isGameplay.Value = false; + // Ensure we reset the LocalUserPlaying state + localUserPlaying.Value = false; fadeOut(); return base.OnExiting(next); From 485bd962c7d52d738547af4bcd43af535e2e935a Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 7 Oct 2020 16:15:17 +1030 Subject: [PATCH 3706/6909] Also reset LocalUserPlaying in OnSuspending --- 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 e6c15521af..43dce0786d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -666,6 +666,9 @@ namespace osu.Game.Screens.Play { screenSuspension?.Expire(); + // Ensure we reset the LocalUserPlaying state + localUserPlaying.Value = false; + fadeOut(); base.OnSuspending(next); } From d1ec3806927a3bb042e47b76be921582ed87575e Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 7 Oct 2020 16:15:32 +1030 Subject: [PATCH 3707/6909] Don't cache ConfineMouseTracker --- osu.Game/OsuGame.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index c09ad0eeb9..d22ac1aec8 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -549,7 +549,6 @@ namespace osu.Game BackButton.Receptor receptor; dependencies.CacheAs(idleTracker = new GameIdleTracker(6000)); - dependencies.Cache(confineMouseTracker = new ConfineMouseTracker()); AddRange(new Drawable[] { @@ -586,7 +585,7 @@ namespace osu.Game leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, topMostOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, idleTracker, - confineMouseTracker + confineMouseTracker = new ConfineMouseTracker() }); ScreenStack.ScreenPushed += screenPushed; From 8b8eb00bd75c4790bec72bdd8a79f5e6ddddf457 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 7 Oct 2020 16:16:58 +1030 Subject: [PATCH 3708/6909] Permit nulls rather than casting to OsuGame --- osu.Game/Screens/Play/Player.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 43dce0786d..39a6ac4ded 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -157,8 +157,8 @@ namespace osu.Game.Screens.Play DrawableRuleset.SetRecordTarget(recordingReplay = new Replay()); } - [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) + [BackgroundDependencyLoader(true)] + private void load(AudioManager audio, OsuConfigManager config, OsuGame game) { Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray(); @@ -174,8 +174,8 @@ namespace osu.Game.Screens.Play mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); - if (game is OsuGame osuGame) - localUserPlaying.BindTo(osuGame.LocalUserPlaying); + if (game != null) + localUserPlaying.BindTo(game.LocalUserPlaying); DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); From d6d0bd90a39e48747ce34ba08629373470127829 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 15:34:03 +0900 Subject: [PATCH 3709/6909] Extract tuple into class --- osu.Game/Scoring/HitResultDisplayStatistic.cs | 41 +++++++++++++++++++ osu.Game/Scoring/ScoreInfo.cs | 17 ++++---- 2 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Scoring/HitResultDisplayStatistic.cs diff --git a/osu.Game/Scoring/HitResultDisplayStatistic.cs b/osu.Game/Scoring/HitResultDisplayStatistic.cs new file mode 100644 index 0000000000..d43d8bf0ba --- /dev/null +++ b/osu.Game/Scoring/HitResultDisplayStatistic.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.Game.Rulesets.Scoring; + +namespace osu.Game.Scoring +{ + /// + /// Compiled result data for a specific in a score. + /// + public class HitResultDisplayStatistic + { + /// + /// The associated result type. + /// + public HitResult Result { get; } + + /// + /// The count of successful hits of this type. + /// + public int Count { get; } + + /// + /// The maximum achievable hits of this type. May be null if undetermined. + /// + public int? MaxCount { get; } + + /// + /// A custom display name for the result type. May be provided by rulesets to give better clarity. + /// + public string DisplayName { get; } + + public HitResultDisplayStatistic(HitResult result, int count, int? maxCount, string displayName) + { + Result = result; + Count = count; + MaxCount = maxCount; + DisplayName = displayName; + } + } +} diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 0206989231..7cd9578ff1 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -213,22 +213,22 @@ namespace osu.Game.Scoring set => isLegacyScore = value; } - public IEnumerable<(HitResult result, int count, int? maxCount)> GetStatisticsForDisplay() + public IEnumerable GetStatisticsForDisplay() { - foreach (var key in OrderAttributeUtils.GetValuesInOrder()) + foreach (var r in Ruleset.CreateInstance().GetHitResults()) { - if (key.IsBonus()) + if (r.result.IsBonus()) continue; - int value = Statistics.GetOrDefault(key); + int value = Statistics.GetOrDefault(r.result); - switch (key) + switch (r.result) { case HitResult.SmallTickHit: { int total = value + Statistics.GetOrDefault(HitResult.SmallTickMiss); if (total > 0) - yield return (key, value, total); + yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); break; } @@ -237,7 +237,7 @@ namespace osu.Game.Scoring { int total = value + Statistics.GetOrDefault(HitResult.LargeTickMiss); if (total > 0) - yield return (key, value, total); + yield return new HitResultDisplayStatistic(r.result, value, total, r.displayName); break; } @@ -247,8 +247,7 @@ namespace osu.Game.Scoring break; default: - if (value > 0 || key == HitResult.Miss) - yield return (key, value, null); + yield return new HitResultDisplayStatistic(r.result, value, null, r.displayName); break; } From 3363c3399eaa8a5e0a4efbb2e1742d694639c232 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 15:34:23 +0900 Subject: [PATCH 3710/6909] Allow rulesets to specify valid HitResult types (and display names for them) --- osu.Game/Rulesets/Ruleset.cs | 49 ++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 915544d010..48d94d2b3f 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -23,8 +23,10 @@ using osu.Game.Scoring; using osu.Game.Skinning; using osu.Game.Users; using JetBrains.Annotations; +using osu.Framework.Extensions; using osu.Framework.Testing; using osu.Game.Screens.Ranking.Statistics; +using osu.Game.Utils; namespace osu.Game.Rulesets { @@ -220,5 +222,52 @@ namespace osu.Game.Rulesets /// The s to display. Each may contain 0 or more . [NotNull] public virtual StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => Array.Empty(); + + /// + /// Get all valid s for this ruleset. + /// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset. + /// + /// + /// All valid s along with a display-friendly name. + /// + public IEnumerable<(HitResult result, string displayName)> GetHitResults() + { + var validResults = GetValidHitResults(); + + // enumerate over ordered list to guarantee return order is stable. + foreach (var result in OrderAttributeUtils.GetValuesInOrder()) + { + switch (result) + { + // hard blocked types, should never be displayed even if the ruleset tells us to. + case HitResult.None: + case HitResult.IgnoreHit: + case HitResult.IgnoreMiss: + // display is handled as a completion count with corresponding "hit" type. + case HitResult.LargeTickMiss: + case HitResult.SmallTickMiss: + continue; + } + + if (result == HitResult.Miss || validResults.Contains(result)) + yield return (result, GetDisplayNameForHitResult(result)); + } + } + + /// + /// Get all valid s for this ruleset. + /// Generally used for results display purposes, where it can't be determined if zero-count means the user has not achieved any or the type is not used by this ruleset. + /// + /// + /// is implicitly included. Special types like are ignored even when specified. + /// + protected virtual IEnumerable GetValidHitResults() => OrderAttributeUtils.GetValuesInOrder(); + + /// + /// Get a display friendly name for the specified result type. + /// + /// The result type to get the name for. + /// The display name. + public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription(); } } From 6020ec9ca3e786d95640da84dfc573ef181c452a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 15:34:36 +0900 Subject: [PATCH 3711/6909] Add valid result types for all rulesets --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 29 ++++++++++++++++++++ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 25 ++++++++++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 35 +++++++++++++++++++++++++ osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 22 ++++++++++++++++ 4 files changed, 111 insertions(+) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index ca75a816f1..eb845cdea6 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -141,6 +141,35 @@ namespace osu.Game.Rulesets.Catch public override Drawable CreateIcon() => new SpriteIcon { Icon = OsuIcon.RulesetCatch }; + protected override IEnumerable GetValidHitResults() + { + return new[] + { + HitResult.Great, + + HitResult.LargeTickHit, + HitResult.SmallTickHit, + HitResult.LargeBonus, + }; + } + + public override string GetDisplayNameForHitResult(HitResult result) + { + switch (result) + { + case HitResult.LargeTickHit: + return "large droplet"; + + case HitResult.SmallTickHit: + return "small droplet"; + + case HitResult.LargeBonus: + return "bananas"; + } + + return base.GetDisplayNameForHitResult(result); + } + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(this, beatmap); public override ISkin CreateLegacySkinProvider(ISkinSource source, IBeatmap beatmap) => new CatchLegacySkinTransformer(source); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 71ac85dd1b..d2feeb03af 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -319,6 +319,31 @@ namespace osu.Game.Rulesets.Mania return (PlayfieldType)Enum.GetValues(typeof(PlayfieldType)).Cast().OrderByDescending(i => i).First(v => variant >= v); } + protected override IEnumerable GetValidHitResults() + { + return new[] + { + HitResult.Perfect, + HitResult.Great, + HitResult.Good, + HitResult.Ok, + HitResult.Meh, + + HitResult.LargeTickHit, + }; + } + + public override string GetDisplayNameForHitResult(HitResult result) + { + switch (result) + { + case HitResult.LargeTickHit: + return "hold tick"; + } + + return base.GetDisplayNameForHitResult(result); + } + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) => new[] { new StatisticRow diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 7f4a0dcbbb..d946e7a113 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -191,6 +191,41 @@ namespace osu.Game.Rulesets.Osu public override IRulesetConfigManager CreateConfig(SettingsStore settings) => new OsuRulesetConfigManager(settings, RulesetInfo); + protected override IEnumerable GetValidHitResults() + { + return new[] + { + HitResult.Great, + HitResult.Ok, + HitResult.Meh, + + HitResult.LargeTickHit, + HitResult.SmallTickHit, + HitResult.SmallBonus, + HitResult.LargeBonus, + }; + } + + public override string GetDisplayNameForHitResult(HitResult result) + { + switch (result) + { + case HitResult.LargeTickHit: + return "slider tick"; + + case HitResult.SmallTickHit: + return "slider end"; + + case HitResult.SmallBonus: + return "spinner spin"; + + case HitResult.LargeBonus: + return "spinner bonus"; + } + + return base.GetDisplayNameForHitResult(result); + } + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { var timedHitEvents = score.HitEvents.Where(e => e.HitObject is HitCircle && !(e.HitObject is SliderTailCircle)).ToList(); diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 9d485e3f20..f4c94c9248 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -159,6 +159,28 @@ namespace osu.Game.Rulesets.Taiko public override IConvertibleReplayFrame CreateConvertibleReplayFrame() => new TaikoReplayFrame(); + protected override IEnumerable GetValidHitResults() + { + return new[] + { + HitResult.Great, + HitResult.Ok, + + HitResult.SmallTickHit, + }; + } + + public override string GetDisplayNameForHitResult(HitResult result) + { + switch (result) + { + case HitResult.SmallTickHit: + return "drum tick"; + } + + return base.GetDisplayNameForHitResult(result); + } + public override StatisticRow[] CreateStatisticsForScore(ScoreInfo score, IBeatmap playableBeatmap) { var timedHitEvents = score.HitEvents.Where(e => e.HitObject is Hit).ToList(); From e281d724b85feabb494169b6bd007e8e91621e1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 15:35:04 +0900 Subject: [PATCH 3712/6909] Consume display name logic --- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 22 +++++++++++-------- .../Scores/TopScoreStatisticsSection.cs | 8 +++---- .../ContractedPanelMiddleContent.cs | 8 +++---- .../Expanded/ExpandedPanelMiddleContent.cs | 4 ++-- .../Expanded/Statistics/HitResultStatistic.cs | 8 +++---- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 968355c377..231d888a4e 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -60,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores /// /// The statistics that appear in the table, in order of appearance. /// - private readonly List statisticResultTypes = new List(); + private readonly List<(HitResult result, string displayName)> statisticResultTypes = new List<(HitResult, string)>(); private bool showPerformancePoints; @@ -101,15 +101,19 @@ namespace osu.Game.Overlays.BeatmapSet.Scores }; // All statistics across all scores, unordered. - var allScoreStatistics = scores.SelectMany(s => s.GetStatisticsForDisplay().Select(stat => stat.result)).ToHashSet(); + var allScoreStatistics = scores.SelectMany(s => s.GetStatisticsForDisplay().Select(stat => stat.Result)).ToHashSet(); + + var ruleset = scores.First().Ruleset.CreateInstance(); foreach (var result in OrderAttributeUtils.GetValuesInOrder()) { if (!allScoreStatistics.Contains(result)) continue; - columns.Add(new TableColumn(result.GetDescription(), Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); - statisticResultTypes.Add(result); + string displayName = ruleset.GetDisplayNameForHitResult(result); + + columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); + statisticResultTypes.Add((result, displayName)); } if (showPerformancePoints) @@ -163,18 +167,18 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } }; - var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.result); + var availableStatistics = score.GetStatisticsForDisplay().ToDictionary(tuple => tuple.Result); foreach (var result in statisticResultTypes) { - if (!availableStatistics.TryGetValue(result, out var stat)) - stat = (result, 0, null); + if (!availableStatistics.TryGetValue(result.result, out var stat)) + stat = new HitResultDisplayStatistic(result.result, 0, null, result.displayName); content.Add(new OsuSpriteText { - Text = stat.maxCount == null ? $"{stat.count}" : $"{stat.count}/{stat.maxCount}", + Text = stat.MaxCount == null ? $"{stat.Count}" : $"{stat.Count}/{stat.MaxCount}", Font = OsuFont.GetFont(size: text_size), - Colour = stat.count == 0 ? Color4.Gray : Color4.White + Colour = stat.Count == 0 ? Color4.Gray : Color4.White }); } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 05789e1fc0..93744dd6a3 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.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.Graphics.Shapes; @@ -15,7 +14,6 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osuTK; @@ -117,7 +115,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores ppColumn.Alpha = value.Beatmap?.Status == BeatmapSetOnlineStatus.Ranked ? 1 : 0; ppColumn.Text = $@"{value.PP:N0}"; - statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(s => createStatisticsColumn(s.result, s.count, s.maxCount)); + statisticsColumns.ChildrenEnumerable = value.GetStatisticsForDisplay().Select(createStatisticsColumn); modsColumn.Mods = value.Mods; if (scoreManager != null) @@ -125,9 +123,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } } - private TextColumn createStatisticsColumn(HitResult hitResult, int count, int? maxCount) => new TextColumn(hitResult.GetDescription(), smallFont, bottom_columns_min_width) + private TextColumn createStatisticsColumn(HitResultDisplayStatistic stat) => new TextColumn(stat.DisplayName, smallFont, bottom_columns_min_width) { - Text = maxCount == null ? $"{count}" : $"{count}/{maxCount}" + Text = stat.MaxCount == null ? $"{stat.Count}" : $"{stat.Count}/{stat.MaxCount}" }; private class InfoColumn : CompositeDrawable diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 95ece1a9fb..9481f07342 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -3,7 +3,6 @@ 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; @@ -13,7 +12,6 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; -using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Users; @@ -117,7 +115,7 @@ namespace osu.Game.Screens.Ranking.Contracted AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 5), - ChildrenEnumerable = score.GetStatisticsForDisplay().Select(s => createStatistic(s.result, s.count, s.maxCount)) + ChildrenEnumerable = score.GetStatisticsForDisplay().Select(createStatistic) }, new FillFlowContainer { @@ -199,8 +197,8 @@ namespace osu.Game.Screens.Ranking.Contracted }; } - private Drawable createStatistic(HitResult result, int count, int? maxCount) - => createStatistic(result.GetDescription(), maxCount == null ? $"{count}" : $"{count}/{maxCount}"); + private Drawable createStatistic(HitResultDisplayStatistic result) + => createStatistic(result.DisplayName, result.MaxCount == null ? $"{result.Count}" : $"{result.Count}/{result.MaxCount}"); private Drawable createStatistic(string key, string value) => new Container { diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index ebab8c88f6..30b9f47f71 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -66,8 +66,8 @@ namespace osu.Game.Screens.Ranking.Expanded var bottomStatistics = new List(); - foreach (var (key, value, maxCount) in score.GetStatisticsForDisplay()) - bottomStatistics.Add(new HitResultStatistic(key, value, maxCount)); + foreach (var result in score.GetStatisticsForDisplay()) + bottomStatistics.Add(new HitResultStatistic(result)); 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 index a86033713f..31ef51a031 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs @@ -2,9 +2,9 @@ // 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; +using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics { @@ -12,10 +12,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { private readonly HitResult result; - public HitResultStatistic(HitResult result, int count, int? maxCount = null) - : base(result.GetDescription(), count, maxCount) + public HitResultStatistic(HitResultDisplayStatistic result) + : base(result.DisplayName, result.Count, result.MaxCount) { - this.result = result; + this.result = result.Result; } [BackgroundDependencyLoader] From c0bc6a75b35cb73cf960c1c444125cbea6c1155f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 16:17:08 +0900 Subject: [PATCH 3713/6909] Show auxiliary judgements on next line --- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 14 ++++++++++++-- .../Expanded/Statistics/HitResultStatistic.cs | 6 +++--- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 30b9f47f71..b2d0c7a874 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Ranking.Expanded new CounterStatistic("pp", (int)(score.PP ?? 0)), }; - var bottomStatistics = new List(); + var bottomStatistics = new List(); foreach (var result in score.GetStatisticsForDisplay()) bottomStatistics.Add(new HitResultStatistic(result)); @@ -198,7 +198,17 @@ namespace osu.Game.Screens.Ranking.Expanded { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Content = new[] { bottomStatistics.Cast().ToArray() }, + Content = new[] { bottomStatistics.Where(s => s.Result <= HitResult.Perfect).ToArray() }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + }, + new GridContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { bottomStatistics.Where(s => s.Result > HitResult.Perfect).ToArray() }, RowDimensions = new[] { new Dimension(GridSizeMode.AutoSize), diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs index 31ef51a031..ada8dfabf0 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/HitResultStatistic.cs @@ -10,18 +10,18 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { public class HitResultStatistic : CounterStatistic { - private readonly HitResult result; + public readonly HitResult Result; public HitResultStatistic(HitResultDisplayStatistic result) : base(result.DisplayName, result.Count, result.MaxCount) { - this.result = result.Result; + Result = result.Result; } [BackgroundDependencyLoader] private void load(OsuColour colours) { - HeaderText.Colour = colours.ForHitResult(result); + HeaderText.Colour = colours.ForHitResult(Result); } } } From 6ac70945f2be7f3be6c6daabaf12a5bda8619cf2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 16:17:28 +0900 Subject: [PATCH 3714/6909] Show bonus judgements on expanded panel --- osu.Game/Scoring/ScoreInfo.cs | 3 --- .../Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs | 3 ++- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 7cd9578ff1..596e98a6bd 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -217,9 +217,6 @@ namespace osu.Game.Scoring { foreach (var r in Ruleset.CreateInstance().GetHitResults()) { - if (r.result.IsBonus()) - continue; - int value = Statistics.GetOrDefault(r.result); switch (r.result) diff --git a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs index 9481f07342..24f1116d0e 100644 --- a/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Contracted/ContractedPanelMiddleContent.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Leaderboards; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; using osu.Game.Users; @@ -115,7 +116,7 @@ namespace osu.Game.Screens.Ranking.Contracted AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 5), - ChildrenEnumerable = score.GetStatisticsForDisplay().Select(createStatistic) + ChildrenEnumerable = score.GetStatisticsForDisplay().Where(s => !s.Result.IsBonus()).Select(createStatistic) }, new FillFlowContainer { From 2e0a9f53c11a4d1e4fe2b63e22a3af3de579904d Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 7 Oct 2020 17:52:39 +1030 Subject: [PATCH 3715/6909] Add test coverage --- .../Visual/Gameplay/TestSceneOverlayActivation.cs | 10 ++++++++++ osu.Game/Screens/Play/Player.cs | 10 +++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index ce04b940e7..41f7582d31 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Bindables; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -23,32 +24,40 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestGameplayOverlayActivation() { + AddAssert("local user playing", () => Player.LocalUserPlaying.Value); AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); } [Test] public void TestGameplayOverlayActivationPaused() { + AddAssert("local user playing", () => Player.LocalUserPlaying.Value); AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddStep("pause gameplay", () => Player.Pause()); + AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value); AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); } [Test] public void TestGameplayOverlayActivationReplayLoaded() { + AddAssert("local user playing", () => Player.LocalUserPlaying.Value); AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddStep("load a replay", () => Player.DrawableRuleset.HasReplayLoaded.Value = true); + AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value); AddAssert("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); } [Test] public void TestGameplayOverlayActivationBreaks() { + AddAssert("local user playing", () => Player.LocalUserPlaying.Value); AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddStep("seek to break", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime)); + AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value); AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); AddStep("seek to break end", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().EndTime)); + AddAssert("local user playing", () => Player.LocalUserPlaying.Value); AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); } @@ -57,6 +66,7 @@ namespace osu.Game.Tests.Visual.Gameplay protected class OverlayTestPlayer : TestPlayer { public new OverlayActivation OverlayActivationMode => base.OverlayActivationMode.Value; + public new Bindable LocalUserPlaying => base.LocalUserPlaying; } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 39a6ac4ded..8830884a40 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -68,7 +68,7 @@ namespace osu.Game.Screens.Play private readonly Bindable storyboardReplacesBackground = new Bindable(); - private readonly Bindable localUserPlaying = new Bindable(); + protected readonly Bindable LocalUserPlaying = new Bindable(); public int RestartCount; @@ -175,7 +175,7 @@ namespace osu.Game.Screens.Play mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); if (game != null) - localUserPlaying.BindTo(game.LocalUserPlaying); + LocalUserPlaying.BindTo(game.LocalUserPlaying); DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); @@ -362,7 +362,7 @@ namespace osu.Game.Screens.Play { bool inGameplay = !DrawableRuleset.HasReplayLoaded.Value && !DrawableRuleset.IsPaused.Value && !breakTracker.IsBreakTime.Value; OverlayActivationMode.Value = inGameplay ? OverlayActivation.Disabled : OverlayActivation.UserTriggered; - localUserPlaying.Value = inGameplay; + LocalUserPlaying.Value = inGameplay; } private void updatePauseOnFocusLostState() => @@ -667,7 +667,7 @@ namespace osu.Game.Screens.Play screenSuspension?.Expire(); // Ensure we reset the LocalUserPlaying state - localUserPlaying.Value = false; + LocalUserPlaying.Value = false; fadeOut(); base.OnSuspending(next); @@ -699,7 +699,7 @@ namespace osu.Game.Screens.Play musicController.ResetTrackAdjustments(); // Ensure we reset the LocalUserPlaying state - localUserPlaying.Value = false; + LocalUserPlaying.Value = false; fadeOut(); return base.OnExiting(next); From 67398b5d9551b7e147fa7398b337ee110ee456c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 16:30:14 +0900 Subject: [PATCH 3716/6909] Move timestamp text out of flow and attach to bottom edge --- .../Expanded/ExpandedPanelMiddleContent.cs | 265 +++++++++--------- 1 file changed, 134 insertions(+), 131 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index b2d0c7a874..5aac449adb 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -72,157 +72,160 @@ namespace osu.Game.Screens.Ranking.Expanded statisticDisplays.AddRange(topStatistics); statisticDisplays.AddRange(bottomStatistics); - InternalChild = new FillFlowContainer + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(20), - Children = new Drawable[] + new FillFlowContainer { - new FillFlowContainer + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(20), + Children = new Drawable[] { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] + new FillFlowContainer { - new OsuSpriteText + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Anchor = Anchor.TopCentre, - 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), - MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, - Truncate = true, - }, - new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding { Top = 40 }, - RelativeSizeAxes = Axes.X, - Height = 230, - Child = new AccuracyCircle(score) + new OsuSpriteText { - 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 - }, - starAndModDisplay = new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5, 0), - Children = new Drawable[] + Anchor = Anchor.TopCentre, + 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 { - new StarRatingDisplay(beatmap) - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft - }, - } - }, - new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Direction = FillDirection.Vertical, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)), + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), + MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, + Truncate = true, + }, + new Container { - new OsuSpriteText + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Margin = new MarginPadding { Top = 40 }, + RelativeSizeAxes = Axes.X, + Height = 230, + Child = new AccuracyCircle(score) { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = beatmap.Version, - Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), - }, - new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) + 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 + }, + starAndModDisplay = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5, 0), + Children = new Drawable[] { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - }.With(t => - { - if (!string.IsNullOrEmpty(creator)) + new StarRatingDisplay(beatmap) { - t.AddText("mapped by "); - t.AddText(creator, 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[] + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft + }, + } + }, + 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 = beatmap.Version, + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), + }, + new OsuTextFlowContainer(s => s.Font = OsuFont.Torus.With(size: 12)) + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + }.With(t => + { + if (!string.IsNullOrEmpty(creator)) + { + t.AddText("mapped by "); + t.AddText(creator, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + } + }) + } + }, + } + }, + new FillFlowContainer { - new GridContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] { topStatistics.Cast().ToArray() }, - RowDimensions = new[] + new GridContainer { - new Dimension(GridSizeMode.AutoSize), - } - }, - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] { bottomStatistics.Where(s => s.Result <= HitResult.Perfect).ToArray() }, - RowDimensions = new[] + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { topStatistics.Cast().ToArray() }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + }, + new GridContainer { - new Dimension(GridSizeMode.AutoSize), - } - }, - new GridContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Content = new[] { bottomStatistics.Where(s => s.Result > HitResult.Perfect).ToArray() }, - RowDimensions = new[] + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { bottomStatistics.Where(s => s.Result <= HitResult.Perfect).ToArray() }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } + }, + new GridContainer { - new Dimension(GridSizeMode.AutoSize), + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Content = new[] { bottomStatistics.Where(s => s.Result > HitResult.Perfect).ToArray() }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + } } } } - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), - Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}" } + }, + new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), + Text = $"Played on {score.Date.ToLocalTime():d MMMM yyyy HH:mm}" } }; From f88ba1734bd6cc687d86de8a2a02a87d386dbbb9 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 7 Oct 2020 18:11:47 +1030 Subject: [PATCH 3717/6909] Remove ConfineMouseTracker field --- osu.Game/OsuGame.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d22ac1aec8..772f9ff145 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -90,8 +90,6 @@ namespace osu.Game private IdleTracker idleTracker; - private ConfineMouseTracker confineMouseTracker; - /// /// Whether overlays should be able to be opened game-wide. Value is sourced from the current active screen. /// @@ -585,7 +583,7 @@ namespace osu.Game leftFloatingOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, topMostOverlayContent = new Container { RelativeSizeAxes = Axes.Both }, idleTracker, - confineMouseTracker = new ConfineMouseTracker() + new ConfineMouseTracker() }); ScreenStack.ScreenPushed += screenPushed; From 31d347be5cfd24d63d3e39b78ecec916bab843c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 16:30:26 +0900 Subject: [PATCH 3718/6909] Make extended score panel taller to better fit all information --- osu.Game/Screens/Ranking/ScorePanel.cs | 7 ++++++- osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 1904da7094..8c8a547277 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Ranking /// /// Height of the panel when expanded. /// - private const float expanded_height = 560; + private const float expanded_height = 586; /// /// Height of the top layer when the panel is expanded. @@ -105,11 +105,16 @@ namespace osu.Game.Screens.Ranking [BackgroundDependencyLoader] private void load() { + // ScorePanel doesn't include the top extruding area in its own size. + // Adding a manual offset here allows the expanded version to take on an "acceptable" vertical centre when at 100% UI scale. + const float vertical_fudge = 20; + InternalChild = content = new Container { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(40), + Y = vertical_fudge, Children = new Drawable[] { topLayerContainer = new Container diff --git a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs index c8010d1c32..67533aaa24 100644 --- a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs +++ b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs @@ -2,6 +2,7 @@ // 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 From f77ad8cf3907327834db7d12c129511f70c5b819 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 17:03:34 +0900 Subject: [PATCH 3719/6909] Remove unused using --- osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs index 67533aaa24..c8010d1c32 100644 --- a/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs +++ b/osu.Game/Screens/Ranking/ScorePanelTrackingContainer.cs @@ -2,7 +2,6 @@ // 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 From f90ac2e76c2bf20c88fafef834b3c9110156f660 Mon Sep 17 00:00:00 2001 From: Shane Woolcock Date: Wed, 7 Oct 2020 18:50:02 +1030 Subject: [PATCH 3720/6909] Ensure we assert after the seek has completed --- osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs index 41f7582d31..4fa4c00981 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneOverlayActivation.cs @@ -54,11 +54,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("local user playing", () => Player.LocalUserPlaying.Value); AddAssert("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); AddStep("seek to break", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().StartTime)); - AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value); AddUntilStep("activation mode is user triggered", () => Player.OverlayActivationMode == OverlayActivation.UserTriggered); + AddAssert("local user not playing", () => !Player.LocalUserPlaying.Value); AddStep("seek to break end", () => Player.GameplayClockContainer.Seek(Beatmap.Value.Beatmap.Breaks.First().EndTime)); - AddAssert("local user playing", () => Player.LocalUserPlaying.Value); AddUntilStep("activation mode is disabled", () => Player.OverlayActivationMode == OverlayActivation.Disabled); + AddAssert("local user playing", () => Player.LocalUserPlaying.Value); } protected override TestPlayer CreatePlayer(Ruleset ruleset) => new OverlayTestPlayer(); From 0f6eb9d4cb7750c1890da24f26d5080f42e643ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 17:40:54 +0900 Subject: [PATCH 3721/6909] Ensure music playback is stopped when retrying by any means --- osu.Game/Screens/Play/Player.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 175722c44e..90a0eb0027 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -441,6 +441,10 @@ namespace osu.Game.Screens.Play /// public void Restart() { + // at the point of restarting the track should either already be paused or the volume should be zero. + // stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader. + musicController.Stop(); + sampleRestart?.Play(); RestartRequested?.Invoke(); From 04fa0bff9d8c6bbb2d1b656619a103c7e17d4211 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 17:46:57 +0900 Subject: [PATCH 3722/6909] Add CanBeNull spec and xmldoc --- osu.Game/Rulesets/Ruleset.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index ae1407fb8f..fef36ef16a 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -158,8 +158,22 @@ namespace osu.Game.Rulesets public abstract DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap); + /// + /// Optionally creates a to generate performance data from the provided score. + /// + /// Difficulty attributes for the beatmap related to the provided score. + /// The score to be processed. + /// A performance calculator instance for the provided score. + [CanBeNull] public virtual PerformanceCalculator CreatePerformanceCalculator(DifficultyAttributes attributes, ScoreInfo score) => null; + /// + /// Optionally creates a to generate performance data from the provided score. + /// + /// The beatmap to use as a source for generating . + /// The score to be processed. + /// A performance calculator instance for the provided score. + [CanBeNull] public PerformanceCalculator CreatePerformanceCalculator(WorkingBeatmap beatmap, ScoreInfo score) { var difficultyCalculator = CreateDifficultyCalculator(beatmap); From 6487f58e9ad11cc8f28592593e693057e3ecb38e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 17:52:35 +0900 Subject: [PATCH 3723/6909] Fix failing tests --- osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 19294d12fc..528689e67c 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -193,6 +193,7 @@ namespace osu.Game.Tests.Visual.Background AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo { + Ruleset = new OsuRuleset().RulesetInfo, User = new User { Username = "osu!" }, Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }))); From 3c3c1ce8855487356f0c6afec1fc6b25d70542c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 18:18:01 +0900 Subject: [PATCH 3724/6909] Don't force playback of (non-looping) DrawableHitObject samples after skin change --- 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 8012b4d95c..1ef6c8c207 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -273,7 +273,7 @@ namespace osu.Game.Rulesets.Objects.Drawables // apply any custom state overrides ApplyCustomUpdateState?.Invoke(this, newState); - if (newState == ArmedState.Hit) + if (!force && newState == ArmedState.Hit) PlaySamples(); } From 8c528c89108ca7a7ffde30f2a355f214205e3e88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 18:36:34 +0900 Subject: [PATCH 3725/6909] Fix legacy taiko skins showing double judgements --- .../Skinning/TaikoLegacySkinTransformer.cs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 93c9deec1f..a804ea5f82 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -7,6 +7,7 @@ using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Audio; +using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Skinning; @@ -14,13 +15,29 @@ namespace osu.Game.Rulesets.Taiko.Skinning { public class TaikoLegacySkinTransformer : LegacySkinTransformer { + private Lazy hasExplosion; + public TaikoLegacySkinTransformer(ISkinSource source) : base(source) { + Source.SourceChanged += sourceChanged; + sourceChanged(); + } + + private void sourceChanged() + { + hasExplosion = new Lazy(() => Source.GetTexture(getHitName(TaikoSkinComponents.TaikoExplosionGreat)) != null); } public override Drawable GetDrawableComponent(ISkinComponent component) { + if (component is GameplaySkinComponent) + { + // if a taiko skin is providing explosion sprites, hide the judgements completely + if (hasExplosion.Value) + return Drawable.Empty(); + } + if (!(component is TaikoSkinComponent taikoComponent)) return null; @@ -87,10 +104,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning var hitName = getHitName(taikoComponent.Component); var hitSprite = this.GetAnimation(hitName, true, false); - var strongHitSprite = this.GetAnimation($"{hitName}k", true, false); if (hitSprite != null) + { + var strongHitSprite = this.GetAnimation($"{hitName}k", true, false); + return new LegacyHitExplosion(hitSprite, strongHitSprite); + } return null; From 94a6e2856570bcb281d2073cd7eb6f716362b0bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 7 Oct 2020 18:40:09 +0900 Subject: [PATCH 3726/6909] Add back second removed condition --- osu.Game/Updater/UpdateManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index 30e28f0e95..f772c6d282 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -61,6 +61,9 @@ namespace osu.Game.Updater public async Task CheckForUpdateAsync() { + if (!CanCheckForUpdate) + return false; + Task waitTask; lock (updateTaskLock) From a425cf4a31372b7fee31b71ae2748ba57f84a0ff Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 7 Oct 2020 13:29:10 +0200 Subject: [PATCH 3727/6909] Fix broken class reference. --- .../Screens/Ranking/Expanded/Statistics/CounterStatistic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index e070f76289..08a9714fd8 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Child = Counter = new Counter + Child = Counter = new StatisticCounter { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre From cd15f83f85341f86ac5f0e3f112ac4bb46c8e3ac Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 7 Oct 2020 14:10:25 +0200 Subject: [PATCH 3728/6909] Update ScorePerformanceCalculator code path. --- osu.Game/Scoring/ScorePerformanceManager.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index 97b4be7edc..0189a86172 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -17,9 +17,6 @@ namespace osu.Game.Scoring { private readonly ConcurrentDictionary performanceCache = new ConcurrentDictionary(); - [Resolved] - private BeatmapManager beatmapManager { get; set; } - [Resolved] private BeatmapDifficultyManager difficultyManager { get; set; } @@ -45,14 +42,13 @@ namespace osu.Game.Scoring private async Task computePerformanceAsync(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) { - var beatmap = beatmapManager.GetWorkingBeatmap(score.Beatmap); var attributes = await difficultyManager.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); if (token.IsCancellationRequested) return default; - var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(beatmap, score, attributes.Attributes); - var total = calculator.Calculate(); + var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Attributes, score); + var total = calculator?.Calculate() ?? default; performanceCache[lookupKey] = total; From 74af7cc5036911b47238d9cde41691c0bcf5cf83 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Wed, 7 Oct 2020 17:00:00 +0300 Subject: [PATCH 3729/6909] Rework ScoreProcessor --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 7a5b707357..ca6a8622f7 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -223,7 +223,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 getBonusScore(statistics) + (accuracyRatio * maxCombo * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25); + return getBonusScore(statistics) + (accuracyRatio * Math.Max(1, maxCombo) * 300) * (1 + Math.Max(0, (comboRatio * maxCombo) - 1) * scoreMultiplier / 25); } } @@ -267,12 +267,6 @@ namespace osu.Game.Rulesets.Scoring { maxHighestCombo = HighestCombo.Value; maxBaseScore = baseScore; - - if (maxBaseScore == 0 || maxHighestCombo == 0) - { - Mode.Value = ScoringMode.Classic; - Mode.Disabled = true; - } } baseScore = 0; From 2b6e4e575e2668e102f491a66ff070cac66c5fdd Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Wed, 7 Oct 2020 17:04:55 +0300 Subject: [PATCH 3730/6909] Award max combo portion score if max achievable is 0 --- 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 ca6a8622f7..9bfd737f7e 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -197,7 +197,7 @@ namespace osu.Game.Rulesets.Scoring { return GetScore(mode, maxHighestCombo, maxBaseScore > 0 ? baseScore / maxBaseScore : 0, - maxHighestCombo > 0 ? (double)HighestCombo.Value / maxHighestCombo : 0, + maxHighestCombo > 0 ? (double)HighestCombo.Value / maxHighestCombo : 1, scoreResultCounts); } From 6113557acc346a572d710229553b133a9ebfdd91 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Wed, 7 Oct 2020 17:11:48 +0300 Subject: [PATCH 3731/6909] Add back small tick tests --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index f81ab6c866..dd191b03c2 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -60,16 +60,20 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 3_350_000 / 7.0)] [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] + [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] + [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] - [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 30)] - [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 150)] + [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 700_030)] + [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 700_150)] [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 3744 / 7.0)] [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] From 7109c3b6cd1777d5aa1b3eafceb547ff6de7742f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 Oct 2020 21:06:24 +0200 Subject: [PATCH 3732/6909] Rename variable as suggested --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 9bfd737f7e..33271d9689 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Scoring private readonly double accuracyPortion; private readonly double comboPortion; - private int maxHighestCombo; + private int maxAchievableCombo; private double maxBaseScore; private double rollingMaxBaseScore; private double baseScore; @@ -195,9 +195,9 @@ namespace osu.Game.Rulesets.Scoring private double getScore(ScoringMode mode) { - return GetScore(mode, maxHighestCombo, + return GetScore(mode, maxAchievableCombo, maxBaseScore > 0 ? baseScore / maxBaseScore : 0, - maxHighestCombo > 0 ? (double)HighestCombo.Value / maxHighestCombo : 1, + maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1, scoreResultCounts); } @@ -265,7 +265,7 @@ namespace osu.Game.Rulesets.Scoring if (storeResults) { - maxHighestCombo = HighestCombo.Value; + maxAchievableCombo = HighestCombo.Value; maxBaseScore = baseScore; } From 2d070934d912b9b05d0d587a30e80dee5ff0d838 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 Oct 2020 21:12:48 +0200 Subject: [PATCH 3733/6909] Add test coverage for empty beatmaps --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index dd191b03c2..e89562f893 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -135,6 +135,17 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); } + [Test] + public void TestEmptyBeatmap( + [Values(ScoringMode.Standardised, ScoringMode.Classic)] + ScoringMode scoringMode) + { + scoreProcessor.Mode.Value = scoringMode; + scoreProcessor.ApplyBeatmap(new TestBeatmap(new RulesetInfo())); + + Assert.IsTrue(Precision.AlmostEquals(0, scoreProcessor.TotalScore.Value)); + } + [TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)] [TestCase(HitResult.Meh, HitResult.Miss)] [TestCase(HitResult.Ok, HitResult.Miss)] From b1029a124ca6efd53a24a950d290c33f5efed148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 Oct 2020 22:57:20 +0200 Subject: [PATCH 3734/6909] Move event subscription to LoadComplete Prevents attempting to read from the `colours` field before it is actually injected. --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 4121e1f7bb..c8982b819a 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -113,7 +113,6 @@ namespace osu.Game.Screens.Edit.Timing }; controlPoints = group.ControlPoints.GetBoundCopy(); - controlPoints.CollectionChanged += (_, __) => createChildren(); } [Resolved] @@ -125,6 +124,12 @@ namespace osu.Game.Screens.Edit.Timing createChildren(); } + protected override void LoadComplete() + { + base.LoadComplete(); + controlPoints.CollectionChanged += (_, __) => createChildren(); + } + private void createChildren() { fill.ChildrenEnumerable = controlPoints.Select(createAttribute).Where(c => c != null); From ac44f6f679504554485edb3877f76615f463f1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 7 Oct 2020 23:10:28 +0200 Subject: [PATCH 3735/6909] Ensure control point group exists after move If the control point group moved was empty, it would not be created due to a lack of ControlPointInfo.Add() calls. --- osu.Game/Screens/Edit/Timing/GroupSection.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index c77d48ef0a..d76b5e7406 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -111,7 +111,8 @@ namespace osu.Game.Screens.Edit.Timing foreach (var cp in currentGroupItems) Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp); - SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time); + // the control point might not necessarily exist yet, if currentGroupItems was empty. + SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time, true); changeHandler?.EndChange(); } From d9089ef93c7d6a052a86fa55f27129903d1fa649 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 12:52:52 +0900 Subject: [PATCH 3736/6909] Add missing bonus type for taiko ruleset --- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index f4c94c9248..7f8289aa91 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -167,6 +167,8 @@ namespace osu.Game.Rulesets.Taiko HitResult.Ok, HitResult.SmallTickHit, + + HitResult.SmallBonus, }; } @@ -176,6 +178,9 @@ namespace osu.Game.Rulesets.Taiko { case HitResult.SmallTickHit: return "drum tick"; + + case HitResult.SmallBonus: + return "strong bonus"; } return base.GetDisplayNameForHitResult(result); From f70252d07bc4ebea18b4cdf0d1e4bd44477354c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 12:52:58 +0900 Subject: [PATCH 3737/6909] Match plurality --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index eb845cdea6..bda595e840 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Catch return "small droplet"; case HitResult.LargeBonus: - return "bananas"; + return "banana"; } return base.GetDisplayNameForHitResult(result); From ef092de9baaf3bde1e50abf5441fcefd847b6007 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 16:56:55 +0900 Subject: [PATCH 3738/6909] Add missing UpdateHitObject calls and move local to usages (not via bindables) --- .../Compose/Components/BlueprintContainer.cs | 4 ++ .../Timeline/TimelineHitObjectBlueprint.cs | 4 +- osu.Game/Screens/Edit/EditorBeatmap.cs | 42 +++++++------------ 3 files changed, 22 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 970e16d1c3..addb970e8a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -436,8 +436,12 @@ namespace osu.Game.Screens.Edit.Compose.Components { // 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; + Beatmap.UpdateHitObject(obj); + } } return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index f0757a3dda..6c3bcfae32 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -392,6 +392,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return; repeatHitObject.RepeatCount = proposedCount; + beatmap.UpdateHitObject(hitObject); break; case IHasDuration endTimeHitObject: @@ -401,10 +402,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return; endTimeHitObject.Duration = snappedTime - hitObject.StartTime; + beatmap.UpdateHitObject(hitObject); break; } - - beatmap.UpdateHitObject(hitObject); } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index fb75d91d16..d02841a95f 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -89,8 +89,6 @@ namespace osu.Game.Screens.Edit private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; - private readonly HashSet pendingUpdates = new HashSet(); - private bool isBatchApplying; /// @@ -150,7 +148,16 @@ namespace osu.Game.Screens.Edit /// The to update. public void UpdateHitObject([NotNull] HitObject hitObject) { - pendingUpdates.Add(hitObject); + if (isBatchApplying) + batchPendingUpdates.Add(hitObject); + else + { + beatmapProcessor?.PreProcess(); + processHitObject(hitObject); + beatmapProcessor?.PostProcess(); + + HitObjectUpdated?.Invoke(hitObject); + } } /// @@ -220,6 +227,8 @@ namespace osu.Game.Screens.Edit private readonly List batchPendingDeletes = new List(); + private readonly HashSet batchPendingUpdates = new HashSet(); + /// /// Apply a batch of operations in one go, without performing Pre/Postprocessing each time. /// @@ -237,14 +246,17 @@ namespace osu.Game.Screens.Edit foreach (var h in batchPendingDeletes) processHitObject(h); foreach (var h in batchPendingInserts) processHitObject(h); + foreach (var h in batchPendingUpdates) processHitObject(h); beatmapProcessor?.PostProcess(); foreach (var h in batchPendingDeletes) HitObjectRemoved?.Invoke(h); foreach (var h in batchPendingInserts) HitObjectAdded?.Invoke(h); + foreach (var h in batchPendingUpdates) HitObjectUpdated?.Invoke(h); batchPendingDeletes.Clear(); batchPendingInserts.Clear(); + batchPendingUpdates.Clear(); isBatchApplying = false; } @@ -254,28 +266,6 @@ namespace osu.Game.Screens.Edit /// public void Clear() => RemoveRange(HitObjects.ToArray()); - protected override void Update() - { - base.Update(); - - // debounce updates as they are common and may come from input events, which can run needlessly many times per update frame. - if (pendingUpdates.Count > 0) - { - beatmapProcessor?.PreProcess(); - - foreach (var hitObject in pendingUpdates) - processHitObject(hitObject); - - beatmapProcessor?.PostProcess(); - - // explicitly needs to be fired after PostProcess - foreach (var hitObject in pendingUpdates) - HitObjectUpdated?.Invoke(hitObject); - - pendingUpdates.Clear(); - } - } - private void processHitObject(HitObject hitObject) => hitObject.ApplyDefaults(ControlPointInfo, BeatmapInfo.BaseDifficulty); private void trackStartTime(HitObject hitObject) @@ -322,7 +312,7 @@ namespace osu.Game.Screens.Edit public void UpdateBeatmap() { foreach (var h in HitObjects) - pendingUpdates.Add(h); + batchPendingUpdates.Add(h); } } } From ce04daf053e20ba3a6506f5bd887696d6d6761a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 16:57:04 +0900 Subject: [PATCH 3739/6909] Split transaction handling code out into base class --- osu.Game/Screens/Edit/EditorChangeHandler.cs | 21 ++------- .../Edit/LegacyEditorBeatmapPatcher.cs | 25 ++++++----- .../Edit/TransactionalCommitComponent.cs | 45 +++++++++++++++++++ 3 files changed, 61 insertions(+), 30 deletions(-) create mode 100644 osu.Game/Screens/Edit/TransactionalCommitComponent.cs diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index b69e9c4c51..0c80a3e187 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit /// /// Tracks changes to the . /// - public class EditorChangeHandler : IEditorChangeHandler + public class EditorChangeHandler : TransactionalCommitComponent, IEditorChangeHandler { public readonly Bindable CanUndo = new Bindable(); public readonly Bindable CanRedo = new Bindable(); @@ -41,7 +41,6 @@ namespace osu.Game.Screens.Edit } private readonly EditorBeatmap editorBeatmap; - private int bulkChangesStarted; private bool isRestoring; public const int MAX_SAVED_STATES = 50; @@ -70,22 +69,8 @@ namespace osu.Game.Screens.Edit private void hitObjectUpdated(HitObject obj) => SaveState(); - public void BeginChange() => bulkChangesStarted++; - - public void EndChange() + protected override void UpdateState() { - if (bulkChangesStarted == 0) - throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}."); - - if (--bulkChangesStarted == 0) - SaveState(); - } - - public void SaveState() - { - if (bulkChangesStarted > 0) - return; - if (isRestoring) return; @@ -120,7 +105,7 @@ namespace osu.Game.Screens.Edit /// 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) + if (TransactionActive) return; if (savedStates.Count == 0) diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index fb7d0dd826..72d3421755 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -68,19 +68,20 @@ namespace osu.Game.Screens.Edit toRemove.Sort(); toAdd.Sort(); - editorBeatmap.ApplyBatchChanges(eb => - { - // Apply the changes. - for (int i = toRemove.Count - 1; i >= 0; i--) - eb.RemoveAt(toRemove[i]); + editorBeatmap.BeginChange(); - if (toAdd.Count > 0) - { - IBeatmap newBeatmap = readBeatmap(newState); - foreach (var i in toAdd) - eb.Insert(i, newBeatmap.HitObjects[i]); - } - }); + // 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.Insert(i, newBeatmap.HitObjects[i]); + } + + editorBeatmap.EndChange(); } private string readString(byte[] state) => Encoding.UTF8.GetString(state); diff --git a/osu.Game/Screens/Edit/TransactionalCommitComponent.cs b/osu.Game/Screens/Edit/TransactionalCommitComponent.cs new file mode 100644 index 0000000000..87a29a6237 --- /dev/null +++ b/osu.Game/Screens/Edit/TransactionalCommitComponent.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; + +namespace osu.Game.Screens.Edit +{ + /// + /// A component that tracks a batch change, only applying after all active changes are completed. + /// + public abstract class TransactionalCommitComponent + { + public bool TransactionActive => bulkChangesStarted > 0; + + private int bulkChangesStarted; + + /// + /// Signal the beginning of a change. + /// + public void BeginChange() => bulkChangesStarted++; + + /// + /// Signal the end of a change. + /// + /// Throws if was not first called. + public void EndChange() + { + if (bulkChangesStarted == 0) + throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}."); + + if (--bulkChangesStarted == 0) + UpdateState(); + } + + public void SaveState() + { + if (bulkChangesStarted > 0) + return; + + UpdateState(); + } + + protected abstract void UpdateState(); + } +} From a9bca671d0e3ae2d4a14cf36e5ac541a78bbb63e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 17:17:52 +0900 Subject: [PATCH 3740/6909] Make component and add hooking events --- .../Edit/TransactionalCommitComponent.cs | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/TransactionalCommitComponent.cs b/osu.Game/Screens/Edit/TransactionalCommitComponent.cs index 87a29a6237..3d3539ee2f 100644 --- a/osu.Game/Screens/Edit/TransactionalCommitComponent.cs +++ b/osu.Game/Screens/Edit/TransactionalCommitComponent.cs @@ -2,14 +2,30 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Graphics; namespace osu.Game.Screens.Edit { /// /// A component that tracks a batch change, only applying after all active changes are completed. /// - public abstract class TransactionalCommitComponent + public abstract class TransactionalCommitComponent : Component { + /// + /// Fires whenever a transaction begins. Will not fire on nested transactions. + /// + public event Action TransactionBegan; + + /// + /// Fires when the last transaction completes. + /// + public event Action TransactionEnded; + + /// + /// Fires when is called and results in a non-transactional state save. + /// + public event Action SaveStateTriggered; + public bool TransactionActive => bulkChangesStarted > 0; private int bulkChangesStarted; @@ -17,7 +33,11 @@ namespace osu.Game.Screens.Edit /// /// Signal the beginning of a change. /// - public void BeginChange() => bulkChangesStarted++; + public void BeginChange() + { + if (bulkChangesStarted++ == 0) + TransactionBegan?.Invoke(); + } /// /// Signal the end of a change. @@ -29,14 +49,22 @@ namespace osu.Game.Screens.Edit throw new InvalidOperationException($"Cannot call {nameof(EndChange)} without a previous call to {nameof(BeginChange)}."); if (--bulkChangesStarted == 0) + { UpdateState(); + TransactionEnded?.Invoke(); + } } + /// + /// Force an update of the state with no attached transaction. + /// This is a no-op if a transaction is already active. Should generally be used as a safety measure to ensure granular changes are not left outside a transaction. + /// public void SaveState() { if (bulkChangesStarted > 0) return; + SaveStateTriggered?.Invoke(); UpdateState(); } From 0781fbd44360daccc23d0fe585a98c4b91b5676d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 17:18:20 +0900 Subject: [PATCH 3741/6909] Make EditorBeatmap implement TransactionalCommitComponent --- osu.Game/Screens/Edit/EditorBeatmap.cs | 61 +++++++------------ .../Screens/Edit/Setup/DifficultySection.cs | 2 +- 2 files changed, 24 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index d02841a95f..f776908a0b 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -8,7 +8,6 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Timing; @@ -18,7 +17,7 @@ using osu.Game.Skinning; namespace osu.Game.Screens.Edit { - public class EditorBeatmap : Component, IBeatmap, IBeatSnapProvider + public class EditorBeatmap : TransactionalCommitComponent, IBeatmap, IBeatSnapProvider { /// /// Invoked when a is added to this . @@ -89,19 +88,16 @@ namespace osu.Game.Screens.Edit private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; - private bool isBatchApplying; - /// /// Adds a collection of s to this . /// /// The s to add. public void AddRange(IEnumerable hitObjects) { - ApplyBatchChanges(_ => - { - foreach (var h in hitObjects) - Add(h); - }); + BeginChange(); + foreach (var h in hitObjects) + Add(h); + EndChange(); } /// @@ -129,7 +125,7 @@ namespace osu.Game.Screens.Edit mutableHitObjects.Insert(index, hitObject); - if (isBatchApplying) + if (TransactionActive) batchPendingInserts.Add(hitObject); else { @@ -148,16 +144,8 @@ namespace osu.Game.Screens.Edit /// The to update. public void UpdateHitObject([NotNull] HitObject hitObject) { - if (isBatchApplying) - batchPendingUpdates.Add(hitObject); - else - { - beatmapProcessor?.PreProcess(); - processHitObject(hitObject); - beatmapProcessor?.PostProcess(); - - HitObjectUpdated?.Invoke(hitObject); - } + // updates are debounced regardless of whether a batch is active. + batchPendingUpdates.Add(hitObject); } /// @@ -182,11 +170,10 @@ namespace osu.Game.Screens.Edit /// The s to remove. public void RemoveRange(IEnumerable hitObjects) { - ApplyBatchChanges(_ => - { - foreach (var h in hitObjects) - Remove(h); - }); + BeginChange(); + foreach (var h in hitObjects) + Remove(h); + EndChange(); } /// @@ -210,7 +197,7 @@ namespace osu.Game.Screens.Edit bindable.UnbindAll(); startTimeBindables.Remove(hitObject); - if (isBatchApplying) + if (TransactionActive) batchPendingDeletes.Add(hitObject); else { @@ -229,18 +216,18 @@ namespace osu.Game.Screens.Edit private readonly HashSet batchPendingUpdates = new HashSet(); - /// - /// Apply a batch of operations in one go, without performing Pre/Postprocessing each time. - /// - /// The function which will apply the batch changes. - public void ApplyBatchChanges(Action applyFunction) + protected override void Update() { - if (isBatchApplying) - throw new InvalidOperationException("Attempting to perform a batch application from within an existing batch"); + base.Update(); - isBatchApplying = true; + if (batchPendingUpdates.Count > 0) + UpdateState(); + } - applyFunction(this); + protected override void UpdateState() + { + if (batchPendingUpdates.Count == 0 && batchPendingDeletes.Count == 0 && batchPendingInserts.Count == 0) + return; beatmapProcessor?.PreProcess(); @@ -257,8 +244,6 @@ namespace osu.Game.Screens.Edit batchPendingDeletes.Clear(); batchPendingInserts.Clear(); batchPendingUpdates.Clear(); - - isBatchApplying = false; } /// @@ -309,7 +294,7 @@ namespace osu.Game.Screens.Edit /// /// Update all hit objects with potentially changed difficulty or control point data. /// - public void UpdateBeatmap() + public void UpdateAllHitObjects() { foreach (var h in HitObjects) batchPendingUpdates.Add(h); diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 2d8031c3c8..aa1d57db31 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -93,7 +93,7 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value; Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value; - editorBeatmap.UpdateBeatmap(); + editorBeatmap.UpdateAllHitObjects(); } } } From b2d93f799f8cc269f037baccf6ad7e903cbb5b00 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 17:22:35 +0900 Subject: [PATCH 3742/6909] Hook ChangeHandler to transactional events rather than individual ones --- osu.Game/Screens/Edit/EditorChangeHandler.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 0c80a3e187..62187aed24 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -53,9 +53,9 @@ namespace osu.Game.Screens.Edit { this.editorBeatmap = editorBeatmap; - editorBeatmap.HitObjectAdded += hitObjectAdded; - editorBeatmap.HitObjectRemoved += hitObjectRemoved; - editorBeatmap.HitObjectUpdated += hitObjectUpdated; + editorBeatmap.TransactionBegan += BeginChange; + editorBeatmap.TransactionEnded += EndChange; + editorBeatmap.SaveStateTriggered += SaveState; patcher = new LegacyEditorBeatmapPatcher(editorBeatmap); @@ -63,12 +63,6 @@ namespace osu.Game.Screens.Edit SaveState(); } - private void hitObjectAdded(HitObject obj) => SaveState(); - - private void hitObjectRemoved(HitObject obj) => SaveState(); - - private void hitObjectUpdated(HitObject obj) => SaveState(); - protected override void UpdateState() { if (isRestoring) From 7ffab38728014fffdeedbc9906258f693c26167a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 17:43:08 +0900 Subject: [PATCH 3743/6909] Add test coverage of TransactionalCommitComponent --- .../TransactionalCommitComponentTest.cs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 osu.Game.Tests/Editing/TransactionalCommitComponentTest.cs diff --git a/osu.Game.Tests/Editing/TransactionalCommitComponentTest.cs b/osu.Game.Tests/Editing/TransactionalCommitComponentTest.cs new file mode 100644 index 0000000000..4ce9115ec4 --- /dev/null +++ b/osu.Game.Tests/Editing/TransactionalCommitComponentTest.cs @@ -0,0 +1,100 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Game.Screens.Edit; + +namespace osu.Game.Tests.Editing +{ + [TestFixture] + public class TransactionalCommitComponentTest + { + private TestHandler handler; + + [SetUp] + public void SetUp() + { + handler = new TestHandler(); + } + + [Test] + public void TestCommitTransaction() + { + Assert.That(handler.StateUpdateCount, Is.EqualTo(0)); + + handler.BeginChange(); + Assert.That(handler.StateUpdateCount, Is.EqualTo(0)); + handler.EndChange(); + + Assert.That(handler.StateUpdateCount, Is.EqualTo(1)); + } + + [Test] + public void TestSaveOutsideOfTransactionTriggersUpdates() + { + Assert.That(handler.StateUpdateCount, Is.EqualTo(0)); + + handler.SaveState(); + Assert.That(handler.StateUpdateCount, Is.EqualTo(1)); + + handler.SaveState(); + Assert.That(handler.StateUpdateCount, Is.EqualTo(2)); + } + + [Test] + public void TestEventsFire() + { + int transactionBegan = 0; + int transactionEnded = 0; + int stateSaved = 0; + + handler.TransactionBegan += () => transactionBegan++; + handler.TransactionEnded += () => transactionEnded++; + handler.SaveStateTriggered += () => stateSaved++; + + handler.BeginChange(); + Assert.That(transactionBegan, Is.EqualTo(1)); + + handler.EndChange(); + Assert.That(transactionEnded, Is.EqualTo(1)); + + Assert.That(stateSaved, Is.EqualTo(0)); + handler.SaveState(); + Assert.That(stateSaved, Is.EqualTo(1)); + } + + [Test] + public void TestSaveDuringTransactionDoesntTriggerUpdate() + { + Assert.That(handler.StateUpdateCount, Is.EqualTo(0)); + + handler.BeginChange(); + + handler.SaveState(); + Assert.That(handler.StateUpdateCount, Is.EqualTo(0)); + + handler.EndChange(); + + Assert.That(handler.StateUpdateCount, Is.EqualTo(1)); + } + + [Test] + public void TestEndWithoutBeginThrows() + { + handler.BeginChange(); + handler.EndChange(); + Assert.That(() => handler.EndChange(), Throws.TypeOf()); + } + + private class TestHandler : TransactionalCommitComponent + { + public int StateUpdateCount { get; private set; } + + protected override void UpdateState() + { + StateUpdateCount++; + } + } + } +} From 38babf3de587e86d83655f2c056d690ca4ab9a44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 17:43:27 +0900 Subject: [PATCH 3744/6909] Update usages of ChangeHandler to EditorBeatmap where relevant --- .../Edit/TaikoSelectionHandler.cs | 8 ++++---- .../Edit/Compose/Components/SelectionHandler.cs | 14 ++++++-------- osu.Game/Screens/Edit/Editor.cs | 4 ++-- 3 files changed, 12 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index d5dd758e10..97998c0f76 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Taiko.Edit { var hits = SelectedHitObjects.OfType(); - ChangeHandler.BeginChange(); + EditorBeatmap.BeginChange(); foreach (var h in hits) { @@ -65,19 +65,19 @@ namespace osu.Game.Rulesets.Taiko.Edit } } - ChangeHandler.EndChange(); + EditorBeatmap.EndChange(); } public void SetRimState(bool state) { var hits = SelectedHitObjects.OfType(); - ChangeHandler.BeginChange(); + EditorBeatmap.BeginChange(); foreach (var h in hits) h.Type = state ? HitType.Rim : HitType.Centre; - ChangeHandler.EndChange(); + EditorBeatmap.EndChange(); } protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable selection) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 7808d7a5bc..6a180c439b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -238,9 +238,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void deleteSelected() { - ChangeHandler?.BeginChange(); EditorBeatmap?.RemoveRange(selectedBlueprints.Select(b => b.HitObject)); - ChangeHandler?.EndChange(); } #endregion @@ -307,7 +305,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void AddHitSample(string sampleName) { - ChangeHandler?.BeginChange(); + EditorBeatmap?.BeginChange(); foreach (var h in SelectedHitObjects) { @@ -318,7 +316,7 @@ namespace osu.Game.Screens.Edit.Compose.Components h.Samples.Add(new HitSampleInfo { Name = sampleName }); } - ChangeHandler?.EndChange(); + EditorBeatmap?.EndChange(); } /// @@ -328,7 +326,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Throws if any selected object doesn't implement public void SetNewCombo(bool state) { - ChangeHandler?.BeginChange(); + EditorBeatmap?.BeginChange(); foreach (var h in SelectedHitObjects) { @@ -340,7 +338,7 @@ namespace osu.Game.Screens.Edit.Compose.Components EditorBeatmap?.UpdateHitObject(h); } - ChangeHandler?.EndChange(); + EditorBeatmap?.EndChange(); } /// @@ -349,12 +347,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void RemoveHitSample(string sampleName) { - ChangeHandler?.BeginChange(); + EditorBeatmap?.BeginChange(); foreach (var h in SelectedHitObjects) h.SamplesBindable.RemoveAll(s => s.Name == sampleName); - ChangeHandler?.EndChange(); + EditorBeatmap?.EndChange(); } #endregion diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3c5cbf30e9..74f324364a 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -509,14 +509,14 @@ namespace osu.Game.Screens.Edit foreach (var h in objects) h.StartTime += timeOffset; - changeHandler.BeginChange(); + editorBeatmap.BeginChange(); editorBeatmap.SelectedHitObjects.Clear(); editorBeatmap.AddRange(objects); editorBeatmap.SelectedHitObjects.AddRange(objects); - changeHandler.EndChange(); + editorBeatmap.EndChange(); } protected void Undo() => changeHandler.RestoreState(-1); From 1027b608ffb86df7c35afff6a9dd8a34124d4607 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 17:52:49 +0900 Subject: [PATCH 3745/6909] Copy list content before firing events to avoid pollution --- osu.Game/Screens/Edit/EditorBeatmap.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index f776908a0b..098d05471f 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -237,13 +237,19 @@ namespace osu.Game.Screens.Edit beatmapProcessor?.PostProcess(); - foreach (var h in batchPendingDeletes) HitObjectRemoved?.Invoke(h); - foreach (var h in batchPendingInserts) HitObjectAdded?.Invoke(h); - foreach (var h in batchPendingUpdates) HitObjectUpdated?.Invoke(h); - + // callbacks may modify the lists so let's be safe about it + var deletes = batchPendingDeletes.ToArray(); batchPendingDeletes.Clear(); + + var inserts = batchPendingInserts.ToArray(); batchPendingInserts.Clear(); + + var updates = batchPendingUpdates.ToArray(); batchPendingUpdates.Clear(); + + foreach (var h in deletes) HitObjectRemoved?.Invoke(h); + foreach (var h in inserts) HitObjectAdded?.Invoke(h); + foreach (var h in updates) HitObjectUpdated?.Invoke(h); } /// From afed832b19e3e8dfe3d3bf94105e0b84d2073f76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 18:06:46 +0900 Subject: [PATCH 3746/6909] Tidy up EditorBeatmap slightly --- .../Sliders/SliderSelectionBlueprint.cs | 2 +- .../Edit/TaikoSelectionHandler.cs | 2 +- .../Compose/Components/BlueprintContainer.cs | 4 +-- .../Compose/Components/SelectionHandler.cs | 2 +- .../Timeline/TimelineHitObjectBlueprint.cs | 4 +-- osu.Game/Screens/Edit/EditorBeatmap.cs | 34 +++++++++---------- 6 files changed, 24 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 94862eb205..f260c5a8fa 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -182,7 +182,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; - editorBeatmap?.UpdateHitObject(HitObject); + editorBeatmap?.Update(HitObject); } public override MenuItem[] ContextMenuItems => new MenuItem[] diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index 97998c0f76..ee92936fc2 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Edit if (h.IsStrong != state) { h.IsStrong = state; - EditorBeatmap.UpdateHitObject(h); + EditorBeatmap.Update(h); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index addb970e8a..c7f87ae08e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -203,7 +203,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { // handle positional change etc. foreach (var obj in selectedHitObjects) - Beatmap.UpdateHitObject(obj); + Beatmap.Update(obj); changeHandler?.EndChange(); isDraggingBlueprint = false; @@ -440,7 +440,7 @@ namespace osu.Game.Screens.Edit.Compose.Components foreach (HitObject obj in SelectionHandler.SelectedHitObjects) { obj.StartTime += offset; - Beatmap.UpdateHitObject(obj); + Beatmap.Update(obj); } } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 6a180c439b..e8ab09df85 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -335,7 +335,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (comboInfo == null || comboInfo.NewCombo == state) continue; comboInfo.NewCombo = state; - EditorBeatmap?.UpdateHitObject(h); + EditorBeatmap?.Update(h); } EditorBeatmap?.EndChange(); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 6c3bcfae32..975433d407 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -392,7 +392,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return; repeatHitObject.RepeatCount = proposedCount; - beatmap.UpdateHitObject(hitObject); + beatmap.Update(hitObject); break; case IHasDuration endTimeHitObject: @@ -402,7 +402,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return; endTimeHitObject.Duration = snappedTime - hitObject.StartTime; - beatmap.UpdateHitObject(hitObject); + beatmap.Update(hitObject); break; } } diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 098d05471f..3278f44e06 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -88,6 +88,12 @@ namespace osu.Game.Screens.Edit private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; + private readonly List batchPendingInserts = new List(); + + private readonly List batchPendingDeletes = new List(); + + private readonly HashSet batchPendingUpdates = new HashSet(); + /// /// Adds a collection of s to this . /// @@ -142,12 +148,21 @@ namespace osu.Game.Screens.Edit /// Updates a , invoking and re-processing the beatmap. /// /// The to update. - public void UpdateHitObject([NotNull] HitObject hitObject) + public void Update([NotNull] HitObject hitObject) { // updates are debounced regardless of whether a batch is active. batchPendingUpdates.Add(hitObject); } + /// + /// Update all hit objects with potentially changed difficulty or control point data. + /// + public void UpdateAllHitObjects() + { + foreach (var h in HitObjects) + batchPendingUpdates.Add(h); + } + /// /// Removes a from this . /// @@ -210,12 +225,6 @@ namespace osu.Game.Screens.Edit } } - private readonly List batchPendingInserts = new List(); - - private readonly List batchPendingDeletes = new List(); - - private readonly HashSet batchPendingUpdates = new HashSet(); - protected override void Update() { base.Update(); @@ -270,7 +279,7 @@ namespace osu.Game.Screens.Edit var insertionIndex = findInsertionIndex(PlayableBeatmap.HitObjects, hitObject.StartTime); mutableHitObjects.Insert(insertionIndex + 1, hitObject); - UpdateHitObject(hitObject); + Update(hitObject); }; } @@ -296,14 +305,5 @@ namespace osu.Game.Screens.Edit public double GetBeatLengthAtTime(double referenceTime) => ControlPointInfo.TimingPointAt(referenceTime).BeatLength / BeatDivisor; public int BeatDivisor => beatDivisor?.Value ?? 1; - - /// - /// Update all hit objects with potentially changed difficulty or control point data. - /// - public void UpdateAllHitObjects() - { - foreach (var h in HitObjects) - batchPendingUpdates.Add(h); - } } } From c9f069d7edd6add1e6625f0e28921acbd9ffa610 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 18:17:57 +0900 Subject: [PATCH 3747/6909] Fix taiko's HitObjectComposer not allowing movement o f selected hitobjects --- osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index d5dd758e10..ac14e6131a 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -89,6 +89,8 @@ namespace osu.Game.Rulesets.Taiko.Edit yield return new TernaryStateMenuItem("Strong") { State = { BindTarget = selectionStrongState } }; } + public override bool HandleMovement(MoveSelectionEvent moveEvent) => true; + protected override void UpdateTernaryStates() { base.UpdateTernaryStates(); From 0967db768ffb922e0d6e46fe71ada6bf94731533 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 18:25:40 +0900 Subject: [PATCH 3748/6909] Add xmldoc covering usage restrictions --- osu.Game/OsuGame.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 772f9ff145..e6f6d526cf 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -98,7 +98,11 @@ namespace osu.Game /// /// Whether the local user is currently interacting with the game in a way that should not be interrupted. /// - public readonly Bindable LocalUserPlaying = new BindableBool(); + /// + /// This is exclusively managed by . If other components are mutating this state, a more + /// resilient method should be used to ensure correct state. + /// + public Bindable LocalUserPlaying = new BindableBool(); protected OsuScreenStack ScreenStack; From 43a575484ad1440a799aa41433840b47f5299f1e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 18:25:43 +0900 Subject: [PATCH 3749/6909] Remove pointless comments --- osu.Game/Screens/Play/Player.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8830884a40..80dd8ae92c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -666,7 +666,6 @@ namespace osu.Game.Screens.Play { screenSuspension?.Expire(); - // Ensure we reset the LocalUserPlaying state LocalUserPlaying.Value = false; fadeOut(); @@ -698,7 +697,6 @@ namespace osu.Game.Screens.Play musicController.ResetTrackAdjustments(); - // Ensure we reset the LocalUserPlaying state LocalUserPlaying.Value = false; fadeOut(); From dbdb25ccf756cf48ec3ecff87a81aec86f0f0224 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 18:29:19 +0900 Subject: [PATCH 3750/6909] Move reset logic to OsuGame --- osu.Game/OsuGame.cs | 3 +++ osu.Game/Screens/Play/Player.cs | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e6f6d526cf..d315b213ab 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -957,6 +957,9 @@ namespace osu.Game break; } + // reset on screen change for sanity. + LocalUserPlaying.Value = false; + if (current is IOsuScreen currentOsuScreen) OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 80dd8ae92c..45f194fc29 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -666,8 +666,6 @@ namespace osu.Game.Screens.Play { screenSuspension?.Expire(); - LocalUserPlaying.Value = false; - fadeOut(); base.OnSuspending(next); } @@ -697,8 +695,6 @@ namespace osu.Game.Screens.Play musicController.ResetTrackAdjustments(); - LocalUserPlaying.Value = false; - fadeOut(); return base.OnExiting(next); } From 3114174e098a898bf09a9946b374fd99fc90ff48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 18:41:03 +0900 Subject: [PATCH 3751/6909] Add missing non-transactional SaveState calls --- osu.Game/Screens/Edit/EditorBeatmap.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 3278f44e06..946c6905db 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -141,6 +141,7 @@ namespace osu.Game.Screens.Edit beatmapProcessor?.PostProcess(); HitObjectAdded?.Invoke(hitObject); + SaveState(); } } @@ -222,6 +223,7 @@ namespace osu.Game.Screens.Edit beatmapProcessor?.PostProcess(); HitObjectRemoved?.Invoke(hitObject); + SaveState(); } } From 4ccd751604fdc8ceb9561f66f0dd5b5d50d66db6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 8 Oct 2020 18:42:53 +0900 Subject: [PATCH 3752/6909] Further simplify non-transactional change logic --- osu.Game/Screens/Edit/EditorBeatmap.cs | 30 ++++++-------------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 946c6905db..165d2ba278 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -131,18 +131,9 @@ namespace osu.Game.Screens.Edit mutableHitObjects.Insert(index, hitObject); - if (TransactionActive) - batchPendingInserts.Add(hitObject); - else - { - // must be run after any change to hitobject ordering - beatmapProcessor?.PreProcess(); - processHitObject(hitObject); - beatmapProcessor?.PostProcess(); - - HitObjectAdded?.Invoke(hitObject); - SaveState(); - } + BeginChange(); + batchPendingInserts.Add(hitObject); + EndChange(); } /// @@ -213,18 +204,9 @@ namespace osu.Game.Screens.Edit bindable.UnbindAll(); startTimeBindables.Remove(hitObject); - if (TransactionActive) - batchPendingDeletes.Add(hitObject); - else - { - // must be run after any change to hitobject ordering - beatmapProcessor?.PreProcess(); - processHitObject(hitObject); - beatmapProcessor?.PostProcess(); - - HitObjectRemoved?.Invoke(hitObject); - SaveState(); - } + BeginChange(); + batchPendingDeletes.Add(hitObject); + EndChange(); } protected override void Update() From fa201be2adb1ef8c7382ea578585854d476bc128 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 8 Oct 2020 18:31:29 +0200 Subject: [PATCH 3753/6909] Simplify PerformanceCacheLookup --- osu.Game/Scoring/ScorePerformanceManager.cs | 26 +++++---------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index 0189a86172..783425052f 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -9,7 +9,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mods; namespace osu.Game.Scoring { @@ -57,34 +56,21 @@ namespace osu.Game.Scoring public readonly struct PerformanceCacheLookup { - public readonly double Accuracy; - public readonly int BeatmapId; - public readonly long TotalScore; - public readonly int Combo; - public readonly Mod[] Mods; - public readonly int RulesetId; + public readonly string ScoreHash; + public readonly int LocalId; public PerformanceCacheLookup(ScoreInfo info) { - Accuracy = info.Accuracy; - BeatmapId = info.Beatmap.ID; - TotalScore = info.TotalScore; - Combo = info.Combo; - Mods = info.Mods; - RulesetId = info.Ruleset.ID ?? 0; + ScoreHash = info.Hash; + LocalId = info.ID; } public override int GetHashCode() { var hash = new HashCode(); - hash.Add(Accuracy); - hash.Add(BeatmapId); - hash.Add(TotalScore); - hash.Add(Combo); - hash.Add(RulesetId); - foreach (var mod in Mods) - hash.Add(mod); + hash.Add(ScoreHash); + hash.Add(LocalId); return hash.ToHashCode(); } From 2d0275ba958e9de5fbb8c68635177026a86487e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 03:07:01 +0900 Subject: [PATCH 3754/6909] Fix first hitobject in osu! hidden mod not getting correct fade applied --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 15 +++++++++++---- osu.Game/Rulesets/Mods/ModHidden.cs | 20 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 80e40af717..d354a8a726 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -42,7 +42,11 @@ namespace osu.Game.Rulesets.Osu.Mods private double lastSliderHeadFadeOutStartTime; private double lastSliderHeadFadeOutDuration; - protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) + protected override void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, true); + + protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, false); + + private void applyState(DrawableHitObject drawable, bool increaseVisibility) { if (!(drawable is DrawableOsuHitObject d)) return; @@ -86,9 +90,12 @@ namespace osu.Game.Rulesets.Osu.Mods lastSliderHeadFadeOutStartTime = fadeOutStartTime; } - // we don't want to see the approach circle - using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) - circle.ApproachCircle.Hide(); + if (!increaseVisibility) + { + // we don't want to see the approach circle + using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + circle.ApproachCircle.Hide(); + } // fade out immediately after fade in. using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index a1915b974c..d81b439f6a 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -38,7 +38,13 @@ namespace osu.Game.Rulesets.Mods public virtual void ApplyToDrawableHitObjects(IEnumerable drawables) { if (IncreaseFirstObjectVisibility.Value) + { + var firstObject = drawables.FirstOrDefault(); + if (firstObject != null) + firstObject.ApplyCustomUpdateState += ApplyFirstObjectIncreaseVisibilityState; + drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)).Skip(1); + } foreach (var dho in drawables) dho.ApplyCustomUpdateState += ApplyHiddenState; @@ -65,6 +71,20 @@ namespace osu.Game.Rulesets.Mods } } + /// + /// Apply a special visibility state to the first object in a beatmap, if the user chooses to turn on the "increase first object visibility" setting. + /// + /// The hit object to apply the state change to. + /// The state of the hit object. + protected virtual void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject hitObject, ArmedState state) + { + } + + /// + /// Apply a hidden state to the provided object. + /// + /// The hit object to apply the state change to. + /// The state of the hit object. protected virtual void ApplyHiddenState(DrawableHitObject hitObject, ArmedState state) { } From e7eda19b0723b8edd16d68989434e340c8e9992d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 03:31:01 +0900 Subject: [PATCH 3755/6909] Reset new combo button state after successful placement --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 9b3314e2ad..0336c74386 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -201,7 +201,12 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override void AddBlueprintFor(HitObject hitObject) { refreshTool(); + base.AddBlueprintFor(hitObject); + + // on successful placement, the new combo button should be reset as this is the most common user interaction. + if (Beatmap.SelectedHitObjects.Count == 0) + NewCombo.Value = TernaryState.False; } private void createPlacement() From 5966205037867aefc6c0a4ed4ea5d0ecbe312b46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 04:31:33 +0900 Subject: [PATCH 3756/6909] Fix ternary button states not updating correctly after a paste operation --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 7808d7a5bc..c5e88ade84 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit.Compose.Components public int SelectedCount => selectedBlueprints.Count; - public IEnumerable SelectedHitObjects => selectedBlueprints.Select(b => b.HitObject); + public IEnumerable SelectedHitObjects => EditorBeatmap.SelectedHitObjects; private Drawable content; From a5b2c4195efb48684a69a44655a1165c1168563b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 04:41:45 +0900 Subject: [PATCH 3757/6909] Fix incorrect timing distribution display due to lack of rounding --- .../Ranking/TestSceneHitEventTimingDistributionGraph.cs | 6 ++++++ .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 144f8da2fa..9059fe34af 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -23,6 +23,12 @@ namespace osu.Game.Tests.Visual.Ranking createTest(CreateDistributedHitEvents()); } + [Test] + public void TestAroundCentre() + { + createTest(Enumerable.Range(-100, 100).Select(i => new HitEvent(i / 50f, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList()); + } + [Test] public void TestZeroTimeOffset() { diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index aa2a83774e..980fc68788 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Ranking.Statistics foreach (var e in hitEvents) { - int binOffset = (int)(e.TimeOffset / binSize); + int binOffset = (int)Math.Round(e.TimeOffset / binSize); bins[timing_distribution_centre_bin_index + binOffset]++; } From ff5a1937f5f17cba7eadd82d3157a9499f6e06ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 05:04:03 +0900 Subject: [PATCH 3758/6909] Fix test logic and stabilise rounding direction --- .../Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs | 2 +- .../Ranking/Statistics/HitEventTimingDistributionGraph.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs index 9059fe34af..4bc843096f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneHitEventTimingDistributionGraph.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Ranking [Test] public void TestAroundCentre() { - createTest(Enumerable.Range(-100, 100).Select(i => new HitEvent(i / 50f, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList()); + createTest(Enumerable.Range(-150, 300).Select(i => new HitEvent(i / 50f, HitResult.Perfect, new HitCircle(), new HitCircle(), null)).ToList()); } [Test] diff --git a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs index 980fc68788..93885b6e02 100644 --- a/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs +++ b/osu.Game/Screens/Ranking/Statistics/HitEventTimingDistributionGraph.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Ranking.Statistics foreach (var e in hitEvents) { - int binOffset = (int)Math.Round(e.TimeOffset / binSize); + int binOffset = (int)Math.Round(e.TimeOffset / binSize, MidpointRounding.AwayFromZero); bins[timing_distribution_centre_bin_index + binOffset]++; } From 85b33fffd028c2a5153c038a13f116c9f19c7e0a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 05:14:44 +0900 Subject: [PATCH 3759/6909] Fix incorrect comments --- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 7808d7a5bc..8a80ddd17c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -141,7 +141,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Handles the selected s being rotated. /// /// The delta angle to apply to the selection. - /// Whether any s could be moved. + /// Whether any s could be rotated. public virtual bool HandleRotation(float angle) => false; /// @@ -149,14 +149,14 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The delta scale to apply, in playfield local coordinates. /// The point of reference where the scale is originating from. - /// Whether any s could be moved. + /// Whether any s could be scaled. public virtual bool HandleScale(Vector2 scale, Anchor anchor) => false; /// - /// Handled the selected s being flipped. + /// Handles the selected s being flipped. /// /// The direction to flip - /// Whether any s could be moved. + /// Whether any s could be flipped. public virtual bool HandleFlip(Direction direction) => false; public bool OnPressed(PlatformAction action) From eacc7dca9a0d3815e7a4055f50b77c92505be911 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 06:31:59 +0900 Subject: [PATCH 3760/6909] Fix SliderPath not handling Clear correctly --- osu.Game/Rulesets/Objects/SliderPath.cs | 1 + .../Screens/Edit/Compose/Components/SelectionHandler.cs | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Rulesets/Objects/SliderPath.cs b/osu.Game/Rulesets/Objects/SliderPath.cs index d577e8fdda..3083fcfccb 100644 --- a/osu.Game/Rulesets/Objects/SliderPath.cs +++ b/osu.Game/Rulesets/Objects/SliderPath.cs @@ -57,6 +57,7 @@ namespace osu.Game.Rulesets.Objects c.Changed += invalidate; break; + case NotifyCollectionChangedAction.Reset: case NotifyCollectionChangedAction.Remove: foreach (var c in args.OldItems.Cast()) c.Changed -= invalidate; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 8a80ddd17c..3fe0ab4396 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -159,6 +159,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any s could be flipped. public virtual bool HandleFlip(Direction direction) => false; + /// + /// Handles the selected s being reversed pattern-wise. + /// + /// Whether any s could be reversed. + public virtual bool HandleReverse() => false; + public bool OnPressed(PlatformAction action) { switch (action.ActionMethod) From 825e10ec8c350bae42e546c86370fada762473d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 06:32:23 +0900 Subject: [PATCH 3761/6909] Add reverse handler button to selection box --- .../Edit/Compose/Components/SelectionBox.cs | 19 +++++++++++++++++++ .../Compose/Components/SelectionHandler.cs | 1 + 2 files changed, 20 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 64191e48e2..b753c45cca 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -17,10 +17,28 @@ namespace osu.Game.Screens.Edit.Compose.Components public Action OnRotation; public Action OnScale; public Action OnFlip; + public Action OnReverse; public Action OperationStarted; public Action OperationEnded; + private bool canReverse; + + /// + /// Whether pattern reversing support should be enabled. + /// + public bool CanReverse + { + get => canReverse; + set + { + if (canReverse == value) return; + + canReverse = value; + recreate(); + } + } + private bool canRotate; /// @@ -125,6 +143,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (CanScaleX && CanScaleY) addFullScaleComponents(); if (CanScaleY) addYScaleComponents(); if (CanRotate) addRotationComponents(); + if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern", () => OnReverse?.Invoke()); } private void addRotationComponents() diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 3fe0ab4396..b74095455c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -103,6 +103,7 @@ namespace osu.Game.Screens.Edit.Compose.Components OnRotation = angle => HandleRotation(angle), OnScale = (amount, anchor) => HandleScale(amount, anchor), OnFlip = direction => HandleFlip(direction), + OnReverse = () => HandleReverse(), }; /// From 2a790c76d5f9c668a842bbc72b0ddf54c076d28a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 06:32:33 +0900 Subject: [PATCH 3762/6909] Add reverse implementation for osu! --- .../Edit/OsuSelectionHandler.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 7ae0730e39..762c4a04e7 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -7,6 +7,7 @@ using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Primitives; using osu.Framework.Utils; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit.Compose.Components; @@ -25,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Edit SelectionBox.CanRotate = canOperate; SelectionBox.CanScaleX = canOperate; SelectionBox.CanScaleY = canOperate; + SelectionBox.CanReverse = canOperate; } protected override void OnOperationEnded() @@ -41,6 +43,54 @@ namespace osu.Game.Rulesets.Osu.Edit /// private Vector2? referenceOrigin; + public override bool HandleReverse() + { + var hitObjects = selectedMovableObjects; + + double endTime = hitObjects.Max(h => h.GetEndTime()); + double startTime = hitObjects.Min(h => h.StartTime); + + bool moreThanOneObject = hitObjects.Length > 1; + + foreach (var h in hitObjects) + { + if (moreThanOneObject) + h.StartTime = endTime - (h.GetEndTime() - startTime); + + if (h is Slider slider) + { + var points = slider.Path.ControlPoints.ToArray(); + Vector2 endPos = points.Last().Position.Value; + + slider.Path.ControlPoints.Clear(); + + slider.Position += endPos; + + PathType? lastType = null; + + for (var i = 0; i < points.Length; i++) + { + var p = points[i]; + p.Position.Value -= endPos; + + // propagate types forwards to last null type + if (i == points.Length - 1) + p.Type.Value = lastType; + else if (p.Type.Value != null) + { + var newType = p.Type.Value; + p.Type.Value = lastType; + lastType = newType; + } + + slider.Path.ControlPoints.Insert(0, p); + } + } + } + + return true; + } + public override bool HandleFlip(Direction direction) { var hitObjects = selectedMovableObjects; From 6649cb220431e7b3807fc8ae2d98c40e4f9d3811 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 06:41:53 +0900 Subject: [PATCH 3763/6909] Fix incorrect first object logic --- osu.Game/Rulesets/Mods/ModHidden.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index d81b439f6a..ad01bf036c 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -39,11 +39,13 @@ namespace osu.Game.Rulesets.Mods { if (IncreaseFirstObjectVisibility.Value) { + drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)); + var firstObject = drawables.FirstOrDefault(); if (firstObject != null) firstObject.ApplyCustomUpdateState += ApplyFirstObjectIncreaseVisibilityState; - drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)).Skip(1); + drawables = drawables.Skip(1); } foreach (var dho in drawables) From c86b37f60d0f1c036ff4bdd2964ce7532d71bfe0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 13:11:24 +0900 Subject: [PATCH 3764/6909] Add check to ensure MusicController doesn't play a delete pending beatmap's track --- osu.Game/Overlays/MusicController.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 0764f34697..12caf98021 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -150,7 +150,7 @@ namespace osu.Game.Overlays { if (IsUserPaused) return; - if (CurrentTrack.IsDummyDevice) + if (CurrentTrack.IsDummyDevice || beatmap.Value.BeatmapSetInfo.DeletePending) { if (beatmap.Disabled) return; From 68039cff4089bf84e233a3c99ef1f24ffd40836d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 13:11:44 +0900 Subject: [PATCH 3765/6909] Set beatmap to sane default on exiting editor --- osu.Game/Screens/Edit/Editor.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3c5cbf30e9..d6dbfef2bd 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -469,10 +469,17 @@ namespace osu.Game.Screens.Edit private void confirmExit() { + // stop the track if playing to allow the parent screen to choose a suitable playback mode. + Beatmap.Value.Track.Stop(); + if (isNewBeatmap) { // confirming exit without save means we should delete the new beatmap completely. beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet); + + // in theory this shouldn't be required but due to EF core not sharing instance states 100% + // MusicController is unaware of the changed DeletePending state. + Beatmap.SetDefault(); } exitConfirmed = true; From 389ffe7da5b799e1b7e800e07d2ccce894e7cbb8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 13:23:18 +0900 Subject: [PATCH 3766/6909] Hide bonus result types from score table for the time being --- osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 231d888a4e..324299ccba 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs @@ -110,6 +110,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores if (!allScoreStatistics.Contains(result)) continue; + // for the time being ignore bonus result types. + // this is not being sent from the API and will be empty in all cases. + if (result.IsBonus()) + continue; + string displayName = ruleset.GetDisplayNameForHitResult(result); columns.Add(new TableColumn(displayName, Anchor.CentreLeft, new Dimension(GridSizeMode.Distributed, minSize: 35, maxSize: 60))); From 8be19fd82059e79f9141027b9e4e3cda086e32fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 13:26:09 +0900 Subject: [PATCH 3767/6909] Increase height of contracted score panel to fit mods again --- osu.Game/Screens/Ranking/ScorePanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 8c8a547277..ee97ee55eb 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Ranking /// /// Height of the panel when contracted. /// - private const float contracted_height = 355; + private const float contracted_height = 385; /// /// Width of the panel when expanded. From beec0e41930ee69aafe15c7303101cca09f3dcaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 14:03:13 +0900 Subject: [PATCH 3768/6909] Hide children of SelectionBlueprint when not selected --- osu.Game/Rulesets/Edit/SelectionBlueprint.cs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index 71256093d5..4abdbfc244 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -89,9 +89,23 @@ namespace osu.Game.Rulesets.Edit } } - protected virtual void OnDeselected() => Hide(); + protected virtual void OnDeselected() + { + // selection blueprints are AlwaysPresent while the related DrawableHitObject is visible + // set the body piece's alpha directly to avoid arbitrarily rendering frame buffers etc. of children. + foreach (var d in InternalChildren) + d.Hide(); - protected virtual void OnSelected() => Show(); + Hide(); + } + + protected virtual void OnSelected() + { + foreach (var d in InternalChildren) + d.Show(); + + Show(); + } // When not selected, input is only required for the blueprint itself to receive IsHovering protected override bool ShouldBeConsideredForInput(Drawable child) => State == SelectionState.Selected; From 34d1439f8ef598cf155b2ef8fa21a003b009d112 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 14:04:26 +0900 Subject: [PATCH 3769/6909] Only update slider selection blueprints paths when visible --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 94862eb205..8fe4b8a9cf 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -66,13 +66,16 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders pathVersion = HitObject.Path.Version.GetBoundCopy(); pathVersion.BindValueChanged(_ => updatePath()); + + BodyPiece.UpdateFrom(HitObject); } protected override void Update() { base.Update(); - BodyPiece.UpdateFrom(HitObject); + if (IsSelected) + BodyPiece.UpdateFrom(HitObject); } private Vector2 rightClickPosition; From 6b9e94ae93370a686aa62f7efeca2aaa0f9b721b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 14:05:00 +0900 Subject: [PATCH 3770/6909] Avoid retaining slider selection blueprints FBO backing textures after deselection --- .../Sliders/Components/SliderBodyPiece.cs | 2 + .../Sliders/SliderSelectionBlueprint.cs | 39 ++++++++++++++----- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index 9349ef7a18..5581ce4bfd 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -49,6 +49,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components OriginPosition = body.PathOffset; } + public void RecyclePath() => body.RecyclePath(); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => body.ReceivePositionalInputAt(screenSpacePos); } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 8fe4b8a9cf..67f8088dbb 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -24,10 +24,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { public class SliderSelectionBlueprint : OsuSelectionBlueprint { - protected readonly SliderBodyPiece BodyPiece; - protected readonly SliderCircleSelectionBlueprint HeadBlueprint; - protected readonly SliderCircleSelectionBlueprint TailBlueprint; - protected readonly PathControlPointVisualiser ControlPointVisualiser; + protected SliderBodyPiece BodyPiece; + protected SliderCircleSelectionBlueprint HeadBlueprint; + protected SliderCircleSelectionBlueprint TailBlueprint; + protected PathControlPointVisualiser ControlPointVisualiser; + + private readonly DrawableSlider slider; [Resolved(CanBeNull = true)] private HitObjectComposer composer { get; set; } @@ -44,17 +46,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public SliderSelectionBlueprint(DrawableSlider slider) : base(slider) { - var sliderObject = (Slider)slider.HitObject; + this.slider = slider; + } + [BackgroundDependencyLoader] + private void load() + { InternalChildren = new Drawable[] { BodyPiece = new SliderBodyPiece(), HeadBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.Start), TailBlueprint = CreateCircleSelectionBlueprint(slider, SliderPosition.End), - ControlPointVisualiser = new PathControlPointVisualiser(sliderObject, true) - { - RemoveControlPointsRequested = removeControlPoints - } }; } @@ -78,6 +80,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders BodyPiece.UpdateFrom(HitObject); } + protected override void OnSelected() + { + AddInternal(ControlPointVisualiser = new PathControlPointVisualiser((Slider)slider.HitObject, true) + { + RemoveControlPointsRequested = removeControlPoints + }); + + base.OnSelected(); + } + + protected override void OnDeselected() + { + base.OnDeselected(); + + // throw away frame buffers on deselection. + ControlPointVisualiser?.Expire(); + BodyPiece.RecyclePath(); + } + private Vector2 rightClickPosition; protected override bool OnMouseDown(MouseDownEvent e) From 9baf704942288a18b60011c5ab9dbee80ca14633 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 15:38:58 +0900 Subject: [PATCH 3771/6909] Add local pooling to TimelineTickDisplay --- .../Visualisations/PointVisualisation.cs | 13 ++- .../Timeline/TimelineTickDisplay.cs | 102 ++++++++++++------ 2 files changed, 78 insertions(+), 37 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs index 1ac960039e..ea093e6a4e 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.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 osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osuTK; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations { @@ -13,15 +13,20 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations public class PointVisualisation : Box { public PointVisualisation(double startTime) + : this() + { + X = (float)startTime; + } + + public PointVisualisation() { Origin = Anchor.TopCentre; + RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Y; + Width = 1; EdgeSmoothness = new Vector2(1, 0); - - RelativePositionAxes = Axes.X; - X = (float)startTime; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 36ee976bf7..745f3e393b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.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; @@ -12,7 +13,7 @@ using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; namespace osu.Game.Screens.Edit.Compose.Components.Timeline { - public class TimelineTickDisplay : TimelinePart + public class TimelineTickDisplay : TimelinePart { [Resolved] private EditorBeatmap beatmap { get; set; } @@ -31,15 +32,23 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load() - { - beatDivisor.BindValueChanged(_ => createLines(), true); - } + [Resolved(canBeNull: true)] + private Timeline timeline { get; set; } - private void createLines() + protected override void Update() { - Clear(); + base.Update(); + + int drawableIndex = 0; + + double minVisibleTime = double.MinValue; + double maxVisibleTime = double.MaxValue; + + if (timeline != null) + { + minVisibleTime = ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X / DrawWidth * Content.RelativeChildSize.X; + maxVisibleTime = ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X / DrawWidth * Content.RelativeChildSize.X; + } for (var i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++) { @@ -50,41 +59,68 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value) { - var indexInBeat = beat % beatDivisor.Value; - - if (indexInBeat == 0) + if (t >= minVisibleTime && t <= maxVisibleTime) { - Add(new PointVisualisation(t) - { - Colour = BindableBeatDivisor.GetColourFor(1, colours), - Origin = Anchor.TopCentre, - }); - } - else - { - var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); - var colour = BindableBeatDivisor.GetColourFor(divisor, colours); - var height = 0.1f - (float)divisor / BindableBeatDivisor.VALID_DIVISORS.Last() * 0.08f; + var indexInBeat = beat % beatDivisor.Value; - Add(new PointVisualisation(t) + if (indexInBeat == 0) { - Colour = colour, - Height = height, - Origin = Anchor.TopCentre, - }); + var downbeatPoint = getNextUsablePoint(); + downbeatPoint.X = (float)t; - Add(new PointVisualisation(t) + downbeatPoint.Colour = BindableBeatDivisor.GetColourFor(1, colours); + downbeatPoint.Anchor = Anchor.TopLeft; + downbeatPoint.Origin = Anchor.TopCentre; + downbeatPoint.Height = 1; + } + else { - Colour = colour, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomCentre, - Height = height, - }); + var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); + var colour = BindableBeatDivisor.GetColourFor(divisor, colours); + var height = 0.1f - (float)divisor / BindableBeatDivisor.VALID_DIVISORS.Last() * 0.08f; + + var topPoint = getNextUsablePoint(); + topPoint.X = (float)t; + topPoint.Colour = colour; + topPoint.Height = height; + topPoint.Anchor = Anchor.TopLeft; + topPoint.Origin = Anchor.TopCentre; + + var bottomPoint = getNextUsablePoint(); + bottomPoint.X = (float)t; + bottomPoint.Colour = colour; + bottomPoint.Anchor = Anchor.BottomLeft; + bottomPoint.Origin = Anchor.BottomCentre; + bottomPoint.Height = height; + } } beat++; } } + + int usedDrawables = drawableIndex; + + // save a few drawables beyond the currently used for edge cases. + while (drawableIndex < Math.Min(usedDrawables + 16, Count)) + Children[drawableIndex++].Hide(); + + // expire any excess + while (drawableIndex < Count) + Children[drawableIndex++].Expire(); + + Drawable getNextUsablePoint() + { + PointVisualisation point; + if (drawableIndex >= Count) + Add(point = new PointVisualisation()); + else + point = Children[drawableIndex++]; + + point.Show(); + + return point; + } } } } From 017a8ce496f74e19bf726ac9ab7a5fdf139aa679 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 15:57:31 +0900 Subject: [PATCH 3772/6909] Only recalculate when display actually changes --- .../Timeline/TimelineTickDisplay.cs | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 745f3e393b..76428d6fbc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -32,6 +33,16 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Both; } + private readonly Cached tickCache = new Cached(); + + [BackgroundDependencyLoader] + private void load() + { + beatDivisor.BindValueChanged(_ => tickCache.Invalidate()); + } + + private (float min, float max) visibleRange = (float.MinValue, float.MaxValue); + [Resolved(canBeNull: true)] private Timeline timeline { get; set; } @@ -39,17 +50,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.Update(); - int drawableIndex = 0; - - double minVisibleTime = double.MinValue; - double maxVisibleTime = double.MaxValue; - if (timeline != null) { - minVisibleTime = ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X / DrawWidth * Content.RelativeChildSize.X; - maxVisibleTime = ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X / DrawWidth * Content.RelativeChildSize.X; + var newRange = ( + ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X / DrawWidth * Content.RelativeChildSize.X, + ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X / DrawWidth * Content.RelativeChildSize.X); + + if (visibleRange != newRange) + tickCache.Invalidate(); + + visibleRange = newRange; } + if (!tickCache.IsValid) + createTicks(); + } + + private void createTicks() + { + int drawableIndex = 0; + for (var i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++) { var point = beatmap.ControlPointInfo.TimingPoints[i]; @@ -59,7 +79,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value) { - if (t >= minVisibleTime && t <= maxVisibleTime) + if (t >= visibleRange.min && t <= visibleRange.max) { var indexInBeat = beat % beatDivisor.Value; @@ -109,6 +129,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline while (drawableIndex < Count) Children[drawableIndex++].Expire(); + tickCache.Validate(); + Drawable getNextUsablePoint() { PointVisualisation point; From 955836916b340c3400efac07e4e385fbb6b4b744 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 16:45:11 +0900 Subject: [PATCH 3773/6909] Fix timeline tick display test making two instances of the component --- osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs index e33040acdc..20e58c3d2a 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineTickDisplay.cs @@ -5,7 +5,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Screens.Edit.Compose.Components; -using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; namespace osu.Game.Tests.Visual.Editing @@ -13,7 +12,7 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneTimelineTickDisplay : TimelineTestScene { - public override Drawable CreateTestComponent() => new TimelineTickDisplay(); + public override Drawable CreateTestComponent() => Empty(); // tick display is implicitly inside the timeline. [BackgroundDependencyLoader] private void load() From ceb1494c33285a0219f58111e3e10b1183509c55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 16:46:54 +0900 Subject: [PATCH 3774/6909] Only run regeneration when passing a new min/max tick boundary --- .../Timeline/TimelineTickDisplay.cs | 83 ++++++++++++------- 1 file changed, 52 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 76428d6fbc..c6e435b6ae 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -41,8 +41,21 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline beatDivisor.BindValueChanged(_ => tickCache.Invalidate()); } + /// + /// The visible time/position range of the timeline. + /// private (float min, float max) visibleRange = (float.MinValue, float.MaxValue); + /// + /// The next time/position value to the left of the display when tick regeneration needs to be run. + /// + private float? nextMinTick; + + /// + /// The next time/position value to the right of the display when tick regeneration needs to be run. + /// + private float? nextMaxTick; + [Resolved(canBeNull: true)] private Timeline timeline { get; set; } @@ -57,9 +70,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X / DrawWidth * Content.RelativeChildSize.X); if (visibleRange != newRange) - tickCache.Invalidate(); + { + visibleRange = newRange; - visibleRange = newRange; + // actual regeneration only needs to occur if we've passed one of the known next min/max tick boundaries. + if (nextMinTick == null || nextMaxTick == null || (visibleRange.min < nextMinTick || visibleRange.max > nextMaxTick)) + tickCache.Invalidate(); + } } if (!tickCache.IsValid) @@ -69,6 +86,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private void createTicks() { int drawableIndex = 0; + int highestDivisor = BindableBeatDivisor.VALID_DIVISORS.Last(); + + nextMinTick = null; + nextMaxTick = null; for (var i = 0; i < beatmap.ControlPointInfo.TimingPoints.Count; i++) { @@ -79,40 +100,39 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline for (double t = point.Time; t < until; t += point.BeatLength / beatDivisor.Value) { - if (t >= visibleRange.min && t <= visibleRange.max) + float xPos = (float)t; + + if (t < visibleRange.min) + nextMinTick = xPos; + else if (t > visibleRange.max) + nextMaxTick ??= xPos; + else { + // if this is the first beat in the beatmap, there is no next min tick + if (beat == 0 && i == 0) + nextMinTick = float.MinValue; + var indexInBeat = beat % beatDivisor.Value; - if (indexInBeat == 0) - { - var downbeatPoint = getNextUsablePoint(); - downbeatPoint.X = (float)t; + var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); + var colour = BindableBeatDivisor.GetColourFor(divisor, colours); - downbeatPoint.Colour = BindableBeatDivisor.GetColourFor(1, colours); - downbeatPoint.Anchor = Anchor.TopLeft; - downbeatPoint.Origin = Anchor.TopCentre; - downbeatPoint.Height = 1; - } - else - { - var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); - var colour = BindableBeatDivisor.GetColourFor(divisor, colours); - var height = 0.1f - (float)divisor / BindableBeatDivisor.VALID_DIVISORS.Last() * 0.08f; + // even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn. + var height = indexInBeat == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f; - var topPoint = getNextUsablePoint(); - topPoint.X = (float)t; - topPoint.Colour = colour; - topPoint.Height = height; - topPoint.Anchor = Anchor.TopLeft; - topPoint.Origin = Anchor.TopCentre; + var topPoint = getNextUsablePoint(); + topPoint.X = xPos; + topPoint.Colour = colour; + topPoint.Height = height; + topPoint.Anchor = Anchor.TopLeft; + topPoint.Origin = Anchor.TopCentre; - var bottomPoint = getNextUsablePoint(); - bottomPoint.X = (float)t; - bottomPoint.Colour = colour; - bottomPoint.Anchor = Anchor.BottomLeft; - bottomPoint.Origin = Anchor.BottomCentre; - bottomPoint.Height = height; - } + var bottomPoint = getNextUsablePoint(); + bottomPoint.X = xPos; + bottomPoint.Colour = colour; + bottomPoint.Anchor = Anchor.BottomLeft; + bottomPoint.Origin = Anchor.BottomCentre; + bottomPoint.Height = height; } beat++; @@ -137,8 +157,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (drawableIndex >= Count) Add(point = new PointVisualisation()); else - point = Children[drawableIndex++]; + point = Children[drawableIndex]; + drawableIndex++; point.Show(); return point; From 5d888f687ae809c1086ebce68033978475a16c56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 16:49:51 +0900 Subject: [PATCH 3775/6909] Account for the width of points so they don't suddenly appear at timeline edges --- .../Timelines/Summary/Visualisations/PointVisualisation.cs | 6 ++++-- .../Edit/Compose/Components/Timeline/TimelineTickDisplay.cs | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs index ea093e6a4e..b0ecffdd24 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Visualisations/PointVisualisation.cs @@ -12,6 +12,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations /// public class PointVisualisation : Box { + public const float WIDTH = 1; + public PointVisualisation(double startTime) : this() { @@ -25,8 +27,8 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations RelativePositionAxes = Axes.X; RelativeSizeAxes = Axes.Y; - Width = 1; - EdgeSmoothness = new Vector2(1, 0); + Width = WIDTH; + EdgeSmoothness = new Vector2(WIDTH, 0); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index c6e435b6ae..ce73a2b50b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -66,8 +66,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (timeline != null) { var newRange = ( - ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X / DrawWidth * Content.RelativeChildSize.X, - ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X / DrawWidth * Content.RelativeChildSize.X); + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopLeft).X - PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X, + (ToLocalSpace(timeline.ScreenSpaceDrawQuad.TopRight).X + PointVisualisation.WIDTH * 2) / DrawWidth * Content.RelativeChildSize.X); if (visibleRange != newRange) { From a0af2eb6c880fc727d15fcfdb662914a89d32d37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 16:54:43 +0900 Subject: [PATCH 3776/6909] Private protect setters --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 67f8088dbb..9cfb02ab20 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -24,10 +24,10 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { public class SliderSelectionBlueprint : OsuSelectionBlueprint { - protected SliderBodyPiece BodyPiece; - protected SliderCircleSelectionBlueprint HeadBlueprint; - protected SliderCircleSelectionBlueprint TailBlueprint; - protected PathControlPointVisualiser ControlPointVisualiser; + protected SliderBodyPiece BodyPiece { get; private set; } + protected SliderCircleSelectionBlueprint HeadBlueprint { get; private set; } + protected SliderCircleSelectionBlueprint TailBlueprint { get; private set; } + protected PathControlPointVisualiser ControlPointVisualiser { get; private set; } private readonly DrawableSlider slider; From 144726e3c691aa2c0259cdae6e03f7d05759c8e5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 17:12:01 +0900 Subject: [PATCH 3777/6909] Better guard against taiko swells becoming strong --- .../Beatmaps/TaikoBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Swell.cs | 2 ++ osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs | 14 +++++++++++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index ed7b8589ba..607eaf5dbd 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => { TaikoHitObject first = x.First(); - if (x.Skip(1).Any() && !(first is Swell)) + if (x.Skip(1).Any() && first.CanBeStrong) first.IsStrong = true; return first; }).ToList(); diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index eeae6e79f8..bf8b7bc178 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Taiko.Objects set => Duration = value - StartTime; } + public override bool CanBeStrong => false; + public double Duration { get; set; } /// diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 2922010001..d2c37d965c 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; using System.Threading; using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; @@ -30,6 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Objects public readonly Bindable IsStrongBindable = new BindableBool(); + /// + /// Whether this can be made a "strong" (large) hit. + /// + public virtual bool CanBeStrong => true; + /// /// Whether this HitObject is a "strong" type. /// Strong hit objects give more points for hitting the hit object with both keys. @@ -37,7 +43,13 @@ namespace osu.Game.Rulesets.Taiko.Objects public bool IsStrong { get => IsStrongBindable.Value; - set => IsStrongBindable.Value = value; + set + { + if (value && !CanBeStrong) + throw new InvalidOperationException($"Object of type {GetType()} cannot be strong"); + + IsStrongBindable.Value = value; + } } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From cb96a40dd6eb389668ff2cfec630f47ec027029a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 17:12:10 +0900 Subject: [PATCH 3778/6909] Fix bindable propagation potentially making swells strong --- .../Objects/Drawables/DrawableTaikoHitObject.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 9cd23383c4..d8d75a7614 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -158,7 +158,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.LoadSamples(); - isStrong.Value = getStrongSamples().Any(); + if (HitObject.CanBeStrong) + isStrong.Value = getStrongSamples().Any(); } private void updateSamplesFromStrong() From 21c6242f9064b8a8ac51af371640df29eb876718 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 18:35:44 +0900 Subject: [PATCH 3779/6909] Fix bar lines ("down beat" as people call it) showing up too often in timeline --- .../Edit/Compose/Components/Timeline/TimelineTickDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index ce73a2b50b..724256af8b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -112,13 +112,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (beat == 0 && i == 0) nextMinTick = float.MinValue; - var indexInBeat = beat % beatDivisor.Value; + var indexInBar = beat % ((int)point.TimeSignature * beatDivisor.Value); var divisor = BindableBeatDivisor.GetDivisorForBeatIndex(beat, beatDivisor.Value); var colour = BindableBeatDivisor.GetColourFor(divisor, colours); // even though "bar lines" take up the full vertical space, we render them in two pieces because it allows for less anchor/origin churn. - var height = indexInBeat == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f; + var height = indexInBar == 0 ? 0.5f : 0.1f - (float)divisor / highestDivisor * 0.08f; var topPoint = getNextUsablePoint(); topPoint.X = xPos; From febfe9cdd049e6bef918074c6406411f1db6884d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 18:43:16 +0900 Subject: [PATCH 3780/6909] Don't fade the approach circle on increased visibility --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index d354a8a726..f69cacd432 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -90,7 +90,14 @@ namespace osu.Game.Rulesets.Osu.Mods lastSliderHeadFadeOutStartTime = fadeOutStartTime; } - if (!increaseVisibility) + Drawable fadeTarget = circle; + + if (increaseVisibility) + { + // only fade the circle piece (not the approach circle) for the increased visibility object. + fadeTarget = circle.CirclePiece; + } + else { // we don't want to see the approach circle using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) @@ -99,8 +106,7 @@ namespace osu.Game.Rulesets.Osu.Mods // fade out immediately after fade in. using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) - circle.FadeOut(fadeOutDuration); - + fadeTarget.FadeOut(fadeOutDuration); break; case DrawableSlider slider: From edaf6db5c6890a8475dd31b4dbfab45d3ddd9fa6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 18:44:23 +0900 Subject: [PATCH 3781/6909] Reference EditorBeatmap directly for selected objects --- .../Edit/Compose/Components/SelectionHandler.cs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index c5e88ade84..8902e8119d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -37,8 +37,6 @@ namespace osu.Game.Screens.Edit.Compose.Components public int SelectedCount => selectedBlueprints.Count; - public IEnumerable SelectedHitObjects => EditorBeatmap.SelectedHitObjects; - private Drawable content; private OsuSpriteText selectionDetailsText; @@ -309,7 +307,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { ChangeHandler?.BeginChange(); - foreach (var h in SelectedHitObjects) + foreach (var h in EditorBeatmap.SelectedHitObjects) { // Make sure there isn't already an existing sample if (h.Samples.Any(s => s.Name == sampleName)) @@ -330,7 +328,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { ChangeHandler?.BeginChange(); - foreach (var h in SelectedHitObjects) + foreach (var h in EditorBeatmap.SelectedHitObjects) { var comboInfo = h as IHasComboInformation; @@ -351,7 +349,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { ChangeHandler?.BeginChange(); - foreach (var h in SelectedHitObjects) + foreach (var h in EditorBeatmap.SelectedHitObjects) h.SamplesBindable.RemoveAll(s => s.Name == sampleName); ChangeHandler?.EndChange(); @@ -425,11 +423,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected virtual void UpdateTernaryStates() { - SelectionNewComboState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.NewCombo); + SelectionNewComboState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType(), h => h.NewCombo); foreach (var (sampleName, bindable) in SelectionSampleStates) { - bindable.Value = GetStateFromSelection(SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName)); + bindable.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects, h => h.Samples.Any(s => s.Name == sampleName)); } } From 3838f405dd6ac1d00ddfad5c86c7d1619982c517 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 18:50:05 +0900 Subject: [PATCH 3782/6909] Fix missed usages --- .../Edit/ManiaSelectionHandler.cs | 4 ++-- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 12 ++++++------ .../Edit/TaikoSelectionHandler.cs | 8 ++++---- .../Edit/Compose/Components/BlueprintContainer.cs | 2 +- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 65f40d7d0a..50629f41a9 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit int minColumn = int.MaxValue; int maxColumn = int.MinValue; - foreach (var obj in SelectedHitObjects.OfType()) + foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType()) { if (obj.Column < minColumn) minColumn = obj.Column; @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Mania.Edit columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn); - foreach (var obj in SelectedHitObjects.OfType()) + foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType()) obj.Column += columnDelta; } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 7ae0730e39..68c4869a45 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit { base.OnSelectionChanged(); - bool canOperate = SelectedHitObjects.Count() > 1 || SelectedHitObjects.Any(s => s is Slider); + bool canOperate = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); SelectionBox.CanRotate = canOperate; SelectionBox.CanScaleX = canOperate; @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// The points to calculate a quad for. private Quad getSurroundingQuad(IEnumerable points) { - if (!SelectedHitObjects.Any()) + if (!EditorBeatmap.SelectedHitObjects.Any()) return new Quad(); Vector2 minPosition = new Vector2(float.MaxValue, float.MaxValue); @@ -203,10 +203,10 @@ namespace osu.Game.Rulesets.Osu.Edit /// /// All osu! hitobjects which can be moved/rotated/scaled. /// - private OsuHitObject[] selectedMovableObjects => SelectedHitObjects - .OfType() - .Where(h => !(h is Spinner)) - .ToArray(); + private OsuHitObject[] selectedMovableObjects => EditorBeatmap.SelectedHitObjects + .OfType() + .Where(h => !(h is Spinner)) + .ToArray(); /// /// Rotate a point around an arbitrary origin. diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index d5dd758e10..6e940be54d 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Taiko.Edit public void SetStrongState(bool state) { - var hits = SelectedHitObjects.OfType(); + var hits = EditorBeatmap.SelectedHitObjects.OfType(); ChangeHandler.BeginChange(); @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Taiko.Edit public void SetRimState(bool state) { - var hits = SelectedHitObjects.OfType(); + var hits = EditorBeatmap.SelectedHitObjects.OfType(); ChangeHandler.BeginChange(); @@ -93,8 +93,8 @@ namespace osu.Game.Rulesets.Taiko.Edit { base.UpdateTernaryStates(); - selectionRimState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.Type == HitType.Rim); - selectionStrongState.Value = GetStateFromSelection(SelectedHitObjects.OfType(), h => h.IsStrong); + selectionRimState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType(), h => h.Type == HitType.Rim); + selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType(), h => h.IsStrong); } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 970e16d1c3..1eff716b3d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -436,7 +436,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { // Apply the start time at the newly snapped-to position double offset = result.Time.Value - draggedObject.StartTime; - foreach (HitObject obj in SelectionHandler.SelectedHitObjects) + foreach (HitObject obj in Beatmap.SelectedHitObjects) obj.StartTime += offset; } From 021777145fd279b0a2b80c9ba7e8cc9992b7729b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 18:51:52 +0900 Subject: [PATCH 3783/6909] 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 d7817cf4cf..3df894fbcc 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 fa2135580d..8b10f0a7f7 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 20a51e5feb..88abbca73d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From e618b62ccd2367a67b06b414bf47d487f9d71fca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 19:02:53 +0900 Subject: [PATCH 3784/6909] Update waveform tests --- osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs b/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs index 0c1296b82c..c3a5a0e944 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneWaveform.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneWaveform.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; @@ -76,7 +77,7 @@ namespace osu.Game.Tests.Visual.Editing }; }); - AddUntilStep("wait for load", () => graph.ResampledWaveform != null); + AddUntilStep("wait for load", () => graph.Loaded.IsSet); } [Test] @@ -98,12 +99,18 @@ namespace osu.Game.Tests.Visual.Editing }; }); - AddUntilStep("wait for load", () => graph.ResampledWaveform != null); + AddUntilStep("wait for load", () => graph.Loaded.IsSet); } public class TestWaveformGraph : WaveformGraph { - public new Waveform ResampledWaveform => base.ResampledWaveform; + public readonly ManualResetEventSlim Loaded = new ManualResetEventSlim(); + + protected override void OnWaveformRegenerated(Waveform waveform) + { + base.OnWaveformRegenerated(waveform); + Loaded.Set(); + } } } } From 573336cb47d2549a670a90c8ce1200411f89e376 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 20:12:17 +0900 Subject: [PATCH 3785/6909] Ensure stable sorting order in beatmap conversion tests --- .../ManiaBeatmapConversionTest.cs | 18 +++++++++++++++++- .../Tests/Beatmaps/BeatmapConversionTest.cs | 13 +++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs index 0c57267970..3d4bc4748b 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaBeatmapConversionTest.cs @@ -83,11 +83,17 @@ namespace osu.Game.Rulesets.Mania.Tests RandomZ = snapshot.RandomZ; } + public override void PostProcess() + { + base.PostProcess(); + Objects.Sort(); + } + public bool Equals(ManiaConvertMapping other) => other != null && RandomW == other.RandomW && RandomX == other.RandomX && RandomY == other.RandomY && RandomZ == other.RandomZ; public override bool Equals(ConvertMapping other) => base.Equals(other) && Equals(other as ManiaConvertMapping); } - public struct ConvertValue : IEquatable + public struct ConvertValue : IEquatable, IComparable { /// /// A sane value to account for osu!stable using ints everwhere. @@ -102,5 +108,15 @@ namespace osu.Game.Rulesets.Mania.Tests => Precision.AlmostEquals(StartTime, other.StartTime, conversion_lenience) && Precision.AlmostEquals(EndTime, other.EndTime, conversion_lenience) && Column == other.Column; + + public int CompareTo(ConvertValue other) + { + var result = StartTime.CompareTo(other.StartTime); + + if (result != 0) + return result; + + return Column.CompareTo(other.Column); + } } } diff --git a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs index e492069c5e..fcf20a2eb2 100644 --- a/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/BeatmapConversionTest.cs @@ -34,6 +34,12 @@ namespace osu.Game.Tests.Beatmaps var ourResult = convert(name, mods.Select(m => (Mod)Activator.CreateInstance(m)).ToArray()); var expectedResult = read(name); + foreach (var m in ourResult.Mappings) + m.PostProcess(); + + foreach (var m in expectedResult.Mappings) + m.PostProcess(); + Assert.Multiple(() => { int mappingCounter = 0; @@ -239,6 +245,13 @@ namespace osu.Game.Tests.Beatmaps set => Objects = value; } + /// + /// Invoked after this is populated to post-process the contained data. + /// + public virtual void PostProcess() + { + } + public virtual bool Equals(ConvertMapping other) => StartTime == other?.StartTime; } } From 696e3d53afdc3754c7d8a2565d022520664d1c4c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 20:50:09 +0900 Subject: [PATCH 3786/6909] Fix slider samples being overwritten by the last node --- osu.Game.Rulesets.Catch/Objects/JuiceStream.cs | 6 ++++-- osu.Game.Rulesets.Osu/Objects/Slider.cs | 11 ++++++----- .../Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 3 --- osu.Game/Rulesets/Objects/Types/IHasRepeats.cs | 10 ++++++++++ 4 files changed, 20 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 6b8b70ed54..e209d012fa 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.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 System.Threading; @@ -56,6 +57,7 @@ namespace osu.Game.Rulesets.Catch.Objects Volume = s.Volume }).ToList(); + int nodeIndex = 0; SliderEventDescriptor? lastEvent = null; foreach (var e in SliderEventGenerator.Generate(StartTime, SpanDuration, Velocity, TickDistance, Path.Distance, this.SpanCount(), LegacyLastTickOffset, cancellationToken)) @@ -105,7 +107,7 @@ namespace osu.Game.Rulesets.Catch.Objects case SliderEventType.Repeat: AddNested(new Fruit { - Samples = Samples, + Samples = this.GetNodeSamples(nodeIndex++), StartTime = e.Time, X = X + Path.PositionAt(e.PathProgress).X, }); @@ -119,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.Objects public double Duration { get => this.SpanCount() * Path.Distance / Velocity; - set => throw new System.NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. + set => throw new NotSupportedException($"Adjust via {nameof(RepeatCount)} instead"); // can be implemented if/when needed. } public double EndTime => StartTime + Duration; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 917382eccf..755ce0866a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -137,6 +137,10 @@ namespace osu.Game.Rulesets.Osu.Objects Velocity = scoringDistance / timingPoint.BeatLength; TickDistance = scoringDistance / difficulty.SliderTickRate * TickDistanceMultiplier; + + // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. + // For now, the samples are attached to and played by the slider itself at the correct end time. + Samples = this.GetNodeSamples(repeatCount + 1); } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) @@ -230,15 +234,12 @@ namespace osu.Game.Rulesets.Osu.Objects tick.Samples = sampleList; foreach (var repeat in NestedHitObjects.OfType()) - repeat.Samples = getNodeSamples(repeat.RepeatIndex + 1); + repeat.Samples = this.GetNodeSamples(repeat.RepeatIndex + 1); if (HeadCircle != null) - HeadCircle.Samples = getNodeSamples(0); + HeadCircle.Samples = this.GetNodeSamples(0); } - private IList getNodeSamples(int nodeIndex) => - nodeIndex < NodeSamples.Count ? NodeSamples[nodeIndex] : Samples; - public override Judgement CreateJudgement() => new OsuIgnoreJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 9afc0ecaf4..f6adeced96 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -184,9 +184,6 @@ namespace osu.Game.Rulesets.Objects.Legacy nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples); - - // The samples are played when the slider ends, which is the last node - result.Samples = nodeSamples[^1]; } else if (type.HasFlag(LegacyHitObjectType.Spinner)) { diff --git a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs index 7a3fb16196..674e2aee88 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasRepeats.cs @@ -35,5 +35,15 @@ namespace osu.Game.Rulesets.Objects.Types /// /// The object that has repeats. public static int SpanCount(this IHasRepeats obj) => obj.RepeatCount + 1; + + /// + /// Retrieves the samples at a particular node in a object. + /// + /// The . + /// The node to attempt to retrieve the samples at. + /// The samples at the given node index, or 's default samples if the given node doesn't exist. + public static IList GetNodeSamples(this T obj, int nodeIndex) + where T : HitObject, IHasRepeats + => nodeIndex < obj.NodeSamples.Count ? obj.NodeSamples[nodeIndex] : obj.Samples; } } From d536a1f75e71a8075334b941aff91b9ab6c737c7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 21:04:56 +0900 Subject: [PATCH 3787/6909] Fix breaks being culled too early --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 7 +------ osu.Game/Rulesets/Mods/ModFlashlight.cs | 3 +++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index b30ec0ca2c..6dadbbd2da 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -307,12 +307,7 @@ namespace osu.Game.Beatmaps.Formats double start = getOffsetTime(Parsing.ParseDouble(split[1])); double end = Math.Max(start, getOffsetTime(Parsing.ParseDouble(split[2]))); - var breakEvent = new BreakPeriod(start, end); - - if (!breakEvent.HasEffect) - return; - - beatmap.Breaks.Add(breakEvent); + beatmap.Breaks.Add(new BreakPeriod(start, end)); break; } } diff --git a/osu.Game/Rulesets/Mods/ModFlashlight.cs b/osu.Game/Rulesets/Mods/ModFlashlight.cs index 6e94a84e7d..08f2ccb75c 100644 --- a/osu.Game/Rulesets/Mods/ModFlashlight.cs +++ b/osu.Game/Rulesets/Mods/ModFlashlight.cs @@ -107,6 +107,9 @@ namespace osu.Game.Rulesets.Mods { foreach (var breakPeriod in Breaks) { + if (!breakPeriod.HasEffect) + continue; + if (breakPeriod.Duration < FLASHLIGHT_FADE_DURATION * 2) continue; this.Delay(breakPeriod.StartTime + FLASHLIGHT_FADE_DURATION).FadeOutFromOne(FLASHLIGHT_FADE_DURATION); From 4d0e4f4adeb0fb1c5c7ac0312b054dd4312d21fb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 21:11:12 +0900 Subject: [PATCH 3788/6909] Fix incorrect initial density --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 524ea27efa..c0fbd47899 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -116,7 +116,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps prevNoteTimes.RemoveAt(0); prevNoteTimes.Add(newNoteTime); - density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count; + if (prevNoteTimes.Count >= 2) + density = (prevNoteTimes[^1] - prevNoteTimes[0]) / prevNoteTimes.Count; } private double lastTime; From 9d09503ace3762d213bc15a664c5f02c7d9c984c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 21:12:38 +0900 Subject: [PATCH 3789/6909] Fix spinner conversion not considering stacking + forced initial column --- .../Beatmaps/ManiaBeatmapConverter.cs | 2 +- .../Legacy/EndTimeObjectPatternGenerator.cs | 26 ++++++++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index c0fbd47899..b17ab3f375 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -181,7 +181,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps case IHasDuration endTimeData: { - conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, originalBeatmap); + conversion = new EndTimeObjectPatternGenerator(Random, original, beatmap, lastPattern, originalBeatmap); recordNote(endTimeData.EndTime, new Vector2(256, 192)); computeDensity(endTimeData.EndTime); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs index d5286a3779..f816a70ab3 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/EndTimeObjectPatternGenerator.cs @@ -14,12 +14,17 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { internal class EndTimeObjectPatternGenerator : PatternGenerator { - private readonly double endTime; + private readonly int endTime; + private readonly PatternType convertType; - public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, IBeatmap originalBeatmap) - : base(random, hitObject, beatmap, new Pattern(), originalBeatmap) + public EndTimeObjectPatternGenerator(FastRandom random, HitObject hitObject, ManiaBeatmap beatmap, Pattern previousPattern, IBeatmap originalBeatmap) + : base(random, hitObject, beatmap, previousPattern, originalBeatmap) { - endTime = (HitObject as IHasDuration)?.EndTime ?? 0; + endTime = (int)((HitObject as IHasDuration)?.EndTime ?? 0); + + convertType = PreviousPattern.ColumnWithObjects == TotalColumns + ? PatternType.None + : PatternType.ForceNotStack; } public override IEnumerable Generate() @@ -40,18 +45,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy break; case 8: - addToPattern(pattern, FindAvailableColumn(GetRandomColumn(), PreviousPattern), generateHold); + addToPattern(pattern, getRandomColumn(), generateHold); break; default: - if (TotalColumns > 0) - addToPattern(pattern, GetRandomColumn(), generateHold); + addToPattern(pattern, getRandomColumn(0), generateHold); break; } return pattern; } + private int getRandomColumn(int? lowerBound = null) + { + if ((convertType & PatternType.ForceNotStack) > 0) + return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound, patterns: PreviousPattern); + + return FindAvailableColumn(GetRandomColumn(lowerBound), lowerBound); + } + /// /// Constructs and adds a note to a pattern. /// From 5f19081db69efe63a0f96a52771d3cefbffb661e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 21:20:00 +0900 Subject: [PATCH 3790/6909] Fix incorrect probability calculation for hitobject conversion --- .../Legacy/HitObjectPatternGenerator.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 84f950997d..bc4ab55767 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -397,7 +397,11 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy case 4: centreProbability = 0; - p2 = Math.Min(p2 * 2, 0.2); + + // Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x). + // But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer), + // so it needs to be converted to from a probability and then back after the multiplication. + p2 = 1 - Math.Max((1 - p2) * 2, 0.8); p3 = 0; break; @@ -408,11 +412,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy case 6: centreProbability = 0; - p2 = Math.Min(p2 * 2, 0.5); - p3 = Math.Min(p3 * 2, 0.15); + + // Stable requires rngValue > x, which is an inverse-probability. Lazer uses true probability (1 - x). + // But multiplying this value by 2 (stable) is not the same operation as dividing it by 2 (lazer), + // so it needs to be converted to from a probability and then back after the multiplication. + p2 = 1 - Math.Max((1 - p2) * 2, 0.5); + p3 = 1 - Math.Max((1 - p3) * 2, 0.85); break; } + // The stable values were allowed to exceed 1, which indicate <0% probability. + // These values needs to be clamped otherwise GetRandomNoteCount() will throw an exception. + p2 = Math.Clamp(p2, 0, 1); + p3 = Math.Clamp(p3, 0, 1); + double centreVal = Random.NextDouble(); int noteCount = GetRandomNoteCount(p2, p3); From 08f3481b592c74a2151632661759c9e0380a2d65 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 21:22:13 +0900 Subject: [PATCH 3791/6909] Use integer calculations to replicate stable's slider conversion --- .../Legacy/DistanceObjectPatternGenerator.cs | 101 ++++++++++-------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index fe146c5324..415201951b 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -3,8 +3,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; -using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.MathUtils; @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Beatmaps.Formats; namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { @@ -25,8 +26,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// private const float osu_base_scoring_distance = 100; - public readonly double EndTime; - public readonly double SegmentDuration; + public readonly int StartTime; + public readonly int EndTime; + public readonly int SegmentDuration; public readonly int SpanCount; private PatternType convertType; @@ -41,20 +43,25 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var distanceData = hitObject as IHasDistance; var repeatsData = hitObject as IHasRepeats; - SpanCount = repeatsData?.SpanCount() ?? 1; + Debug.Assert(distanceData != null); TimingControlPoint timingPoint = beatmap.ControlPointInfo.TimingPointAt(hitObject.StartTime); DifficultyControlPoint difficultyPoint = beatmap.ControlPointInfo.DifficultyPointAt(hitObject.StartTime); - // The true distance, accounting for any repeats - double distance = (distanceData?.Distance ?? 0) * SpanCount; - // The velocity of the osu! hit object - calculated as the velocity of a slider - double osuVelocity = osu_base_scoring_distance * beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier * difficultyPoint.SpeedMultiplier / timingPoint.BeatLength; - // The duration of the osu! hit object - double osuDuration = distance / osuVelocity; + double beatLength; +#pragma warning disable 618 + if (difficultyPoint is LegacyBeatmapDecoder.LegacyDifficultyControlPoint legacyDifficultyPoint) +#pragma warning restore 618 + beatLength = timingPoint.BeatLength * legacyDifficultyPoint.BpmMultiplier; + else + beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier; - EndTime = hitObject.StartTime + osuDuration; - SegmentDuration = (EndTime - HitObject.StartTime) / SpanCount; + SpanCount = repeatsData?.SpanCount() ?? 1; + + StartTime = (int)Math.Round(hitObject.StartTime); + EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier); + + SegmentDuration = (EndTime - StartTime) / SpanCount; } public override IEnumerable Generate() @@ -76,7 +83,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy foreach (var obj in originalPattern.HitObjects) { - if (!Precision.AlmostEquals(EndTime, obj.GetEndTime())) + if (EndTime != (int)Math.Round(obj.GetEndTime())) intermediatePattern.Add(obj); else endTimePattern.Add(obj); @@ -91,35 +98,35 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (TotalColumns == 1) { var pattern = new Pattern(); - addToPattern(pattern, 0, HitObject.StartTime, EndTime); + addToPattern(pattern, 0, StartTime, EndTime); return pattern; } if (SpanCount > 1) { if (SegmentDuration <= 90) - return generateRandomHoldNotes(HitObject.StartTime, 1); + return generateRandomHoldNotes(StartTime, 1); if (SegmentDuration <= 120) { convertType |= PatternType.ForceNotStack; - return generateRandomNotes(HitObject.StartTime, SpanCount + 1); + return generateRandomNotes(StartTime, SpanCount + 1); } if (SegmentDuration <= 160) - return generateStair(HitObject.StartTime); + return generateStair(StartTime); if (SegmentDuration <= 200 && ConversionDifficulty > 3) - return generateRandomMultipleNotes(HitObject.StartTime); + return generateRandomMultipleNotes(StartTime); - double duration = EndTime - HitObject.StartTime; + double duration = EndTime - StartTime; if (duration >= 4000) - return generateNRandomNotes(HitObject.StartTime, 0.23, 0, 0); + return generateNRandomNotes(StartTime, 0.23, 0, 0); if (SegmentDuration > 400 && SpanCount < TotalColumns - 1 - RandomStart) - return generateTiledHoldNotes(HitObject.StartTime); + return generateTiledHoldNotes(StartTime); - return generateHoldAndNormalNotes(HitObject.StartTime); + return generateHoldAndNormalNotes(StartTime); } if (SegmentDuration <= 110) @@ -128,37 +135,37 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy convertType |= PatternType.ForceNotStack; else convertType &= ~PatternType.ForceNotStack; - return generateRandomNotes(HitObject.StartTime, SegmentDuration < 80 ? 1 : 2); + return generateRandomNotes(StartTime, SegmentDuration < 80 ? 1 : 2); } if (ConversionDifficulty > 6.5) { if (convertType.HasFlag(PatternType.LowProbability)) - return generateNRandomNotes(HitObject.StartTime, 0.78, 0.3, 0); + return generateNRandomNotes(StartTime, 0.78, 0.3, 0); - return generateNRandomNotes(HitObject.StartTime, 0.85, 0.36, 0.03); + return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03); } if (ConversionDifficulty > 4) { if (convertType.HasFlag(PatternType.LowProbability)) - return generateNRandomNotes(HitObject.StartTime, 0.43, 0.08, 0); + return generateNRandomNotes(StartTime, 0.43, 0.08, 0); - return generateNRandomNotes(HitObject.StartTime, 0.56, 0.18, 0); + return generateNRandomNotes(StartTime, 0.56, 0.18, 0); } if (ConversionDifficulty > 2.5) { if (convertType.HasFlag(PatternType.LowProbability)) - return generateNRandomNotes(HitObject.StartTime, 0.3, 0, 0); + return generateNRandomNotes(StartTime, 0.3, 0, 0); - return generateNRandomNotes(HitObject.StartTime, 0.37, 0.08, 0); + return generateNRandomNotes(StartTime, 0.37, 0.08, 0); } if (convertType.HasFlag(PatternType.LowProbability)) - return generateNRandomNotes(HitObject.StartTime, 0.17, 0, 0); + return generateNRandomNotes(StartTime, 0.17, 0, 0); - return generateNRandomNotes(HitObject.StartTime, 0.27, 0, 0); + return generateNRandomNotes(StartTime, 0.27, 0, 0); } /// @@ -167,7 +174,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// Start time of each hold note. /// Number of hold notes. /// The containing the hit objects. - private Pattern generateRandomHoldNotes(double startTime, int noteCount) + private Pattern generateRandomHoldNotes(int startTime, int noteCount) { // - - - - // ■ - ■ ■ @@ -202,7 +209,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// The start time. /// The number of notes. /// The containing the hit objects. - private Pattern generateRandomNotes(double startTime, int noteCount) + private Pattern generateRandomNotes(int startTime, int noteCount) { // - - - - // x - - - @@ -234,7 +241,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// The start time. /// The containing the hit objects. - private Pattern generateStair(double startTime) + private Pattern generateStair(int startTime) { // - - - - // x - - - @@ -286,7 +293,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// The start time. /// The containing the hit objects. - private Pattern generateRandomMultipleNotes(double startTime) + private Pattern generateRandomMultipleNotes(int startTime) { // - - - - // x - - - @@ -329,7 +336,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// The probability required for 3 hold notes to be generated. /// The probability required for 4 hold notes to be generated. /// The containing the hit objects. - private Pattern generateNRandomNotes(double startTime, double p2, double p3, double p4) + private Pattern generateNRandomNotes(int startTime, double p2, double p3, double p4) { // - - - - // ■ - ■ ■ @@ -366,7 +373,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH; bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability); - canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(HitObject.StartTime).Any(isDoubleSample); + canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample); if (canGenerateTwoNotes) p2 = 1; @@ -379,7 +386,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// The first hold note start time. /// The containing the hit objects. - private Pattern generateTiledHoldNotes(double startTime) + private Pattern generateTiledHoldNotes(int startTime) { // - - - - // ■ ■ ■ ■ @@ -394,6 +401,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int columnRepeat = Math.Min(SpanCount, TotalColumns); + // Due to integer rounding, this is not guaranteed to be the same as EndTime (the class-level variable). + int endTime = startTime + SegmentDuration * SpanCount; + int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); @@ -401,7 +411,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy for (int i = 0; i < columnRepeat; i++) { nextColumn = FindAvailableColumn(nextColumn, pattern); - addToPattern(pattern, nextColumn, startTime, EndTime); + addToPattern(pattern, nextColumn, startTime, endTime); startTime += SegmentDuration; } @@ -413,7 +423,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// The start time of notes. /// The containing the hit objects. - private Pattern generateHoldAndNormalNotes(double startTime) + private Pattern generateHoldAndNormalNotes(int startTime) { // - - - - // ■ x x - @@ -448,7 +458,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy for (int i = 0; i <= SpanCount; i++) { - if (!(ignoreHead && startTime == HitObject.StartTime)) + if (!(ignoreHead && startTime == StartTime)) { for (int j = 0; j < noteCount; j++) { @@ -471,19 +481,18 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// /// The time to retrieve the sample info list from. /// - private IList sampleInfoListAt(double time) => nodeSamplesAt(time)?.First() ?? HitObject.Samples; + private IList sampleInfoListAt(int 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 List> nodeSamplesAt(double time) + private List> nodeSamplesAt(int time) { if (!(HitObject is IHasPathWithRepeats curveData)) return null; - // 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); + var index = SegmentDuration == 0 ? 0 : (time - StartTime) / SegmentDuration; // avoid slicing the list & creating copies, if at all possible. return index == 0 ? curveData.NodeSamples : curveData.NodeSamples.Skip(index).ToList(); @@ -496,7 +505,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// The column to add the note to. /// The start time of the note. /// The end time of the note (set to for a non-hold note). - private void addToPattern(Pattern pattern, int column, double startTime, double endTime) + private void addToPattern(Pattern pattern, int column, int startTime, int endTime) { ManiaHitObject newObject; From 485a951281f62bb3ff7afad7b369e4442b61ab24 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 21:42:43 +0900 Subject: [PATCH 3792/6909] Expose current strain and retrieval of peak strain --- osu.Game/Rulesets/Difficulty/Skills/Skill.cs | 25 ++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 227f2f4018..1063a24b27 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -41,7 +41,11 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// protected readonly LimitedCapacityStack Previous = new LimitedCapacityStack(2); // Contained objects not used yet - private double currentStrain = 1; // We keep track of the strain level at all times throughout the beatmap. + /// + /// The current strain level. + /// + protected double CurrentStrain { get; private set; } = 1; + private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section. private readonly List strainPeaks = new List(); @@ -51,10 +55,10 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// public void Process(DifficultyHitObject current) { - currentStrain *= strainDecay(current.DeltaTime); - currentStrain += StrainValueOf(current) * SkillMultiplier; + CurrentStrain *= strainDecay(current.DeltaTime); + CurrentStrain += StrainValueOf(current) * SkillMultiplier; - currentSectionPeak = Math.Max(currentStrain, currentSectionPeak); + currentSectionPeak = Math.Max(CurrentStrain, currentSectionPeak); Previous.Push(current); } @@ -71,15 +75,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// /// Sets the initial strain level for a new section. /// - /// The beginning of the new section in milliseconds. - public void StartNewSectionFrom(double offset) + /// The beginning of the new section in milliseconds. + public void StartNewSectionFrom(double time) { // The maximum strain of the new section is not zero by default, strain decays as usual regardless of section boundaries. // This means we need to capture the strain level at the beginning of the new section, and use that as the initial peak level. if (Previous.Count > 0) - currentSectionPeak = currentStrain * strainDecay(offset - Previous[0].BaseObject.StartTime); + currentSectionPeak = GetPeakStrain(time); } + /// + /// Retrieves the peak strain at a point in time. + /// + /// The time to retrieve the peak strain at. + /// The peak strain. + protected virtual double GetPeakStrain(double time) => CurrentStrain * strainDecay(time - Previous[0].BaseObject.StartTime); + /// /// Returns the calculated difficulty value representing all processed s. /// From 8f37d2290a4321bc569bc73f30cdbdb7755f1ed7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 21:43:46 +0900 Subject: [PATCH 3793/6909] Expose sorting of hitobjects --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 1902de5bda..e80a4e4b1c 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Difficulty if (!beatmap.HitObjects.Any()) return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); - var difficultyHitObjects = CreateDifficultyHitObjects(beatmap, clockRate).OrderBy(h => h.BaseObject.StartTime).ToList(); + var difficultyHitObjects = SortObjects(CreateDifficultyHitObjects(beatmap, clockRate)).ToList(); double sectionLength = SectionLength * clockRate; @@ -100,6 +100,14 @@ namespace osu.Game.Rulesets.Difficulty return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); } + /// + /// Sorts a given set of s. + /// + /// The s to sort. + /// The sorted s. + protected virtual IEnumerable SortObjects(IEnumerable input) + => input.OrderBy(h => h.BaseObject.StartTime); + /// /// Creates all combinations which adjust the difficulty. /// From b0f8a7794a58a573a839e237d103bc4d50ac3eaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 9 Oct 2020 21:44:10 +0900 Subject: [PATCH 3794/6909] Make SelectionHandler require EditorBeatmap presence --- .../Compose/Components/SelectionHandler.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 6e144fd5ff..4caceedc5a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected SelectionBox SelectionBox { get; private set; } - [Resolved(CanBeNull = true)] + [Resolved] protected EditorBeatmap EditorBeatmap { get; private set; } [Resolved(CanBeNull = true)] @@ -243,7 +243,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void deleteSelected() { - EditorBeatmap?.RemoveRange(selectedBlueprints.Select(b => b.HitObject)); + EditorBeatmap.RemoveRange(selectedBlueprints.Select(b => b.HitObject)); } #endregion @@ -310,7 +310,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void AddHitSample(string sampleName) { - EditorBeatmap?.BeginChange(); + EditorBeatmap.BeginChange(); foreach (var h in EditorBeatmap.SelectedHitObjects) { @@ -321,7 +321,7 @@ namespace osu.Game.Screens.Edit.Compose.Components h.Samples.Add(new HitSampleInfo { Name = sampleName }); } - EditorBeatmap?.EndChange(); + EditorBeatmap.EndChange(); } /// @@ -331,7 +331,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Throws if any selected object doesn't implement public void SetNewCombo(bool state) { - EditorBeatmap?.BeginChange(); + EditorBeatmap.BeginChange(); foreach (var h in EditorBeatmap.SelectedHitObjects) { @@ -340,10 +340,10 @@ namespace osu.Game.Screens.Edit.Compose.Components if (comboInfo == null || comboInfo.NewCombo == state) continue; comboInfo.NewCombo = state; - EditorBeatmap?.Update(h); + EditorBeatmap.Update(h); } - EditorBeatmap?.EndChange(); + EditorBeatmap.EndChange(); } /// @@ -352,12 +352,12 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void RemoveHitSample(string sampleName) { - EditorBeatmap?.BeginChange(); + EditorBeatmap.BeginChange(); foreach (var h in EditorBeatmap.SelectedHitObjects) h.SamplesBindable.RemoveAll(s => s.Name == sampleName); - EditorBeatmap?.EndChange(); + EditorBeatmap.EndChange(); } #endregion From 5017c92fe84973d58f3fa6c4c90af0b0858c2591 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 21:47:34 +0900 Subject: [PATCH 3795/6909] Combine mania skills --- .../Difficulty/ManiaDifficultyCalculator.cs | 47 +---------- .../Difficulty/Skills/Individual.cs | 47 ----------- .../Difficulty/Skills/Overall.cs | 56 ------------- .../Difficulty/Skills/Strain.cs | 80 +++++++++++++++++++ 4 files changed, 84 insertions(+), 146 deletions(-) delete mode 100644 osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs delete mode 100644 osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs create mode 100644 osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index b08c520c54..7dd1755742 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty return new ManiaDifficultyAttributes { - StarRating = difficultyValue(skills) * star_scaling_factor, + StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, @@ -49,55 +49,16 @@ namespace osu.Game.Rulesets.Mania.Difficulty }; } - private double difficultyValue(Skill[] skills) - { - // Preprocess the strains to find the maximum overall + individual (aggregate) strain from each section - var overall = skills.OfType().Single(); - var aggregatePeaks = new List(Enumerable.Repeat(0.0, overall.StrainPeaks.Count)); - - foreach (var individual in skills.OfType()) - { - for (int i = 0; i < individual.StrainPeaks.Count; i++) - { - double aggregate = individual.StrainPeaks[i] + overall.StrainPeaks[i]; - - if (aggregate > aggregatePeaks[i]) - aggregatePeaks[i] = aggregate; - } - } - - aggregatePeaks.Sort((a, b) => b.CompareTo(a)); // Sort from highest to lowest strain. - - double difficulty = 0; - double weight = 1; - - // Difficulty is the weighted sum of the highest strains from every section. - foreach (double strain in aggregatePeaks) - { - difficulty += strain * weight; - weight *= 0.9; - } - - return difficulty; - } - protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { for (int i = 1; i < beatmap.HitObjects.Count; i++) yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate); } - protected override Skill[] CreateSkills(IBeatmap beatmap) + protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { - int columnCount = ((ManiaBeatmap)beatmap).TotalColumns; - - var skills = new List { new Overall(columnCount) }; - - for (int i = 0; i < columnCount; i++) - skills.Add(new Individual(i, columnCount)); - - return skills.ToArray(); - } + new Strain(((ManiaBeatmap)beatmap).TotalColumns) + }; protected override Mod[] DifficultyAdjustmentMods { diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.cs deleted file mode 100644 index 4f7ab87fad..0000000000 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Individual.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 System.Linq; -using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; -using osu.Game.Rulesets.Objects; - -namespace osu.Game.Rulesets.Mania.Difficulty.Skills -{ - public class Individual : Skill - { - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.125; - - private readonly double[] holdEndTimes; - - private readonly int column; - - public Individual(int column, int columnCount) - { - this.column = column; - - holdEndTimes = new double[columnCount]; - } - - protected override double StrainValueOf(DifficultyHitObject current) - { - var maniaCurrent = (ManiaDifficultyHitObject)current; - var endTime = maniaCurrent.BaseObject.GetEndTime(); - - try - { - if (maniaCurrent.BaseObject.Column != column) - return 0; - - // We give a slight bonus if something is held meanwhile - return holdEndTimes.Any(t => t > endTime) ? 2.5 : 2; - } - finally - { - holdEndTimes[maniaCurrent.BaseObject.Column] = endTime; - } - } - } -} diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs deleted file mode 100644 index bbbb93fd8b..0000000000 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Overall.cs +++ /dev/null @@ -1,56 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; -using osu.Game.Rulesets.Objects; - -namespace osu.Game.Rulesets.Mania.Difficulty.Skills -{ - public class Overall : Skill - { - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.3; - - private readonly double[] holdEndTimes; - - private readonly int columnCount; - - public Overall(int columnCount) - { - this.columnCount = columnCount; - - holdEndTimes = new double[columnCount]; - } - - protected override double StrainValueOf(DifficultyHitObject current) - { - var maniaCurrent = (ManiaDifficultyHitObject)current; - var endTime = maniaCurrent.BaseObject.GetEndTime(); - - double holdFactor = 1.0; // Factor in case something else is held - double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly - - for (int i = 0; i < columnCount; i++) - { - // If there is at least one other overlapping end or note, then we get an addition, buuuuuut... - if (current.BaseObject.StartTime < holdEndTimes[i] && endTime > holdEndTimes[i]) - holdAddition = 1.0; - - // ... this addition only is valid if there is _no_ other note with the same ending. - // Releasing multiple notes at the same time is just as easy as releasing one - if (endTime == holdEndTimes[i]) - holdAddition = 0; - - // We give a slight bonus if something is held meanwhile - if (holdEndTimes[i] > endTime) - holdFactor = 1.25; - } - - holdEndTimes[maniaCurrent.BaseObject.Column] = endTime; - - return (1 + holdAddition) * holdFactor; - } - } -} diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs new file mode 100644 index 0000000000..7ebc1ff752 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.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 osu.Framework.Utils; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Mania.Difficulty.Skills +{ + public class Strain : Skill + { + private const double individual_decay_base = 0.125; + private const double overall_decay_base = 0.30; + + protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 1; + + private readonly double[] holdEndTimes; + private readonly double[] individualStrains; + + private double individualStrain; + private double overallStrain; + + public Strain(int totalColumns) + { + holdEndTimes = new double[totalColumns]; + individualStrains = new double[totalColumns]; + overallStrain = 1; + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + var maniaCurrent = (ManiaDifficultyHitObject)current; + var endTime = maniaCurrent.BaseObject.GetEndTime(); + var column = maniaCurrent.BaseObject.Column; + + double holdFactor = 1.0; // Factor to all additional strains in case something else is held + double holdAddition = 0; // Addition to the current note in case it's a hold and has to be released awkwardly + + // Fill up the holdEndTimes array + for (int i = 0; i < holdEndTimes.Length; ++i) + { + // If there is at least one other overlapping end or note, then we get an addition, buuuuuut... + if (Precision.DefinitelyBigger(holdEndTimes[i], maniaCurrent.BaseObject.StartTime, 1) && Precision.DefinitelyBigger(endTime, holdEndTimes[i], 1)) + holdAddition = 1.0; + + // ... this addition only is valid if there is _no_ other note with the same ending. Releasing multiple notes at the same time is just as easy as releasing 1 + if (Precision.AlmostEquals(endTime, holdEndTimes[i], 1)) + holdAddition = 0; + + // We give a slight bonus to everything if something is held meanwhile + if (Precision.DefinitelyBigger(holdEndTimes[i], endTime, 1)) + holdFactor = 1.25; + + // Decay individual strains + individualStrains[i] = applyDecay(individualStrains[i], current.DeltaTime, individual_decay_base); + } + + holdEndTimes[column] = endTime; + + // Increase individual strain in own column + individualStrains[column] += 2.0 * holdFactor; + individualStrain = individualStrains[column]; + + overallStrain = applyDecay(overallStrain, current.DeltaTime, overall_decay_base) + (1 + holdAddition) * holdFactor; + + return individualStrain + overallStrain - CurrentStrain; + } + + protected override double GetPeakStrain(double offset) + => applyDecay(individualStrain, offset - Previous[0].BaseObject.StartTime, individual_decay_base) + + applyDecay(overallStrain, offset - Previous[0].BaseObject.StartTime, overall_decay_base); + + private double applyDecay(double value, double deltaTime, double decayBase) + => value * Math.Pow(decayBase, deltaTime / 1000); + } +} From 306d876d22042d3e6d61b41e3bb534b254e7c276 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 21:50:11 +0900 Subject: [PATCH 3796/6909] Replicate stable's unstable sort --- .../Difficulty/ManiaDifficultyCalculator.cs | 14 +- .../MathUtils/LegacySortHelper.cs | 164 ++++++++++++++++++ 2 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 7dd1755742..a3694f354b 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; @@ -10,10 +11,12 @@ using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; using osu.Game.Rulesets.Mania.Difficulty.Skills; +using osu.Game.Rulesets.Mania.MathUtils; using osu.Game.Rulesets.Mania.Mods; using osu.Game.Rulesets.Mania.Objects; using osu.Game.Rulesets.Mania.Scoring; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Difficulty @@ -51,10 +54,17 @@ namespace osu.Game.Rulesets.Mania.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - for (int i = 1; i < beatmap.HitObjects.Count; i++) - yield return new ManiaDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate); + var sortedObjects = beatmap.HitObjects.ToArray(); + + LegacySortHelper.Sort(sortedObjects, Comparer.Create((a, b) => (int)Math.Round(a.StartTime) - (int)Math.Round(b.StartTime))); + + for (int i = 1; i < sortedObjects.Length; i++) + yield return new ManiaDifficultyHitObject(sortedObjects[i], sortedObjects[i - 1], clockRate); } + // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required. + protected override IEnumerable SortObjects(IEnumerable input) => input; + protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain(((ManiaBeatmap)beatmap).TotalColumns) diff --git a/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs new file mode 100644 index 0000000000..421cc0ae04 --- /dev/null +++ b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs @@ -0,0 +1,164 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Contracts; + +namespace osu.Game.Rulesets.Mania.MathUtils +{ + /// + /// Provides access to .NET4.0 unstable sorting methods. + /// + /// + /// Source: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/arraysorthelper.cs + /// + internal static class LegacySortHelper + { + private const int quick_sort_depth_threshold = 32; + + public static void Sort(T[] keys, IComparer comparer) + { + if (keys == null) + throw new ArgumentNullException(nameof(keys)); + + if (keys.Length == 0) + return; + + comparer ??= Comparer.Default; + depthLimitedQuickSort(keys, 0, keys.Length - 1, comparer, quick_sort_depth_threshold); + } + + private static void depthLimitedQuickSort(T[] keys, int left, int right, IComparer comparer, int depthLimit) + { + do + { + if (depthLimit == 0) + { + heapsort(keys, left, right, comparer); + return; + } + + int i = left; + int j = right; + + // pre-sort the low, middle (pivot), and high values in place. + // this improves performance in the face of already sorted data, or + // data that is made up of multiple sorted runs appended together. + int middle = i + ((j - i) >> 1); + swapIfGreater(keys, comparer, i, middle); // swap the low with the mid point + swapIfGreater(keys, comparer, i, j); // swap the low with the high + swapIfGreater(keys, comparer, middle, j); // swap the middle with the high + + T x = keys[middle]; + + do + { + while (comparer.Compare(keys[i], x) < 0) i++; + while (comparer.Compare(x, keys[j]) < 0) j--; + Contract.Assert(i >= left && j <= right, "(i>=left && j<=right) Sort failed - Is your IComparer bogus?"); + if (i > j) break; + + if (i < j) + { + T key = keys[i]; + keys[i] = keys[j]; + keys[j] = key; + } + + i++; + j--; + } while (i <= j); + + // The next iteration of the while loop is to "recursively" sort the larger half of the array and the + // following calls recrusively sort the smaller half. So we subtrack one from depthLimit here so + // both sorts see the new value. + depthLimit--; + + if (j - left <= right - i) + { + if (left < j) depthLimitedQuickSort(keys, left, j, comparer, depthLimit); + left = i; + } + else + { + if (i < right) depthLimitedQuickSort(keys, i, right, comparer, depthLimit); + right = j; + } + } while (left < right); + } + + private static void heapsort(T[] keys, int lo, int hi, IComparer comparer) + { + Contract.Requires(keys != null); + Contract.Requires(comparer != null); + Contract.Requires(lo >= 0); + Contract.Requires(hi > lo); + Contract.Requires(hi < keys.Length); + + int n = hi - lo + 1; + + for (int i = n / 2; i >= 1; i = i - 1) + { + downHeap(keys, i, n, lo, comparer); + } + + for (int i = n; i > 1; i = i - 1) + { + swap(keys, lo, lo + i - 1); + downHeap(keys, 1, i - 1, lo, comparer); + } + } + + private static void downHeap(T[] keys, int i, int n, int lo, IComparer comparer) + { + Contract.Requires(keys != null); + Contract.Requires(comparer != null); + Contract.Requires(lo >= 0); + Contract.Requires(lo < keys.Length); + + T d = keys[lo + i - 1]; + + while (i <= n / 2) + { + var child = 2 * i; + + if (child < n && comparer.Compare(keys[lo + child - 1], keys[lo + child]) < 0) + { + child++; + } + + if (!(comparer.Compare(d, keys[lo + child - 1]) < 0)) + break; + + keys[lo + i - 1] = keys[lo + child - 1]; + i = child; + } + + keys[lo + i - 1] = d; + } + + private static void swap(T[] a, int i, int j) + { + if (i != j) + { + T t = a[i]; + a[i] = a[j]; + a[j] = t; + } + } + + private static void swapIfGreater(T[] keys, IComparer comparer, int a, int b) + { + if (a != b) + { + if (comparer.Compare(keys[a], keys[b]) > 0) + { + T key = keys[a]; + keys[a] = keys[b]; + keys[b] = key; + } + } + } + } +} From 65d8530a11008333c8fc3b75c9fbee2a74c3d96b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 9 Oct 2020 22:47:32 +0900 Subject: [PATCH 3797/6909] Fix tests --- .../Testing/Beatmaps/convert-samples-expected-conversion.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 fec1360b26..d49ffa01c5 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 @@ -10,7 +10,7 @@ ["soft-hitnormal"], ["drum-hitnormal"] ], - "Samples": ["drum-hitnormal"] + "Samples": ["-hitnormal"] }, { "StartTime": 1875.0, "EndTime": 2750.0, @@ -19,7 +19,7 @@ ["soft-hitnormal"], ["drum-hitnormal"] ], - "Samples": ["drum-hitnormal"] + "Samples": ["-hitnormal"] }] }, { "StartTime": 3750.0, From 6e8011a7ee51a0f1a9a6abf528f227595abb4e6e Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 9 Oct 2020 17:28:59 +0300 Subject: [PATCH 3798/6909] Write xmldoc for TestFourVariousResultsOneMiss --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index dd191b03c2..f7144beda7 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -54,6 +54,21 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); } + /// + /// Test to see that all contribute to score portions in correct amounts. + /// + /// Scoring mode to test + /// HitResult that will be applied to HitObjects + /// HitResult used for accuracy calcualtion + /// Expected score after 3/4 hitobjects have been hit + /// + /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo + /// expectedScore is calcualted using this algorithm for standardised scoring: 1_000_000 * ((75% * score_per_hitobject / max_per_hitobject * 30%) + (50% * 70%)) + /// "75% * score_per_hitobject / max_per_hitobject" is the accuracy we would get for hitting 3/4 hitobjects that award "score_per_hitobject / max_per_hitobject" accuracy each hit + /// + /// expectedScore is calculated using this algorithm for classic scoring: score_per_hitobject / max_per_hitobject * 936 + /// "936" is simplified from "75% * 4 * 300 * (1 + 1/25)" + /// [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] From a279c38af49a57ea7a6b668d37b774c5863335f2 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Fri, 9 Oct 2020 17:33:13 +0300 Subject: [PATCH 3799/6909] Convert all expectedScore values to int --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index f7144beda7..4f4503faea 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Rulesets.Scoring /// Scoring mode to test /// HitResult that will be applied to HitObjects /// HitResult used for accuracy calcualtion - /// Expected score after 3/4 hitobjects have been hit + /// Expected score after 3/4 hitobjects have been hit rounded to nearest integer /// /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo /// expectedScore is calcualted using this algorithm for standardised scoring: 1_000_000 * ((75% * score_per_hitobject / max_per_hitobject * 30%) + (50% * 70%)) @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] - [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 3_350_000 / 7.0)] + [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 478_571)] [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] - [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 3744 / 7.0)] + [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 535)] [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] @@ -93,7 +93,7 @@ namespace osu.Game.Tests.Rulesets.Scoring [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] - public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, double expectedScore) + public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) { var minResult = new TestJudgement(hitResult).MinResult; @@ -113,14 +113,14 @@ namespace osu.Game.Tests.Rulesets.Scoring scoreProcessor.ApplyResult(judgementResult); } - Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); + Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5)); } - [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 6_850_000 / 7.0)] - [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 6_400_000 / 7.0)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 1950 / 7.0)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 1500 / 7.0)] - public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, double expectedScore) + [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] + [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] + [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 214)] + public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore) { IEnumerable hitObjects = Enumerable .Repeat(new TestHitObject(HitResult.SmallTickHit), 4) @@ -147,7 +147,7 @@ namespace osu.Game.Tests.Rulesets.Scoring }; scoreProcessor.ApplyResult(lastJudgementResult); - Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value)); + Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5)); } [TestCase(HitResult.IgnoreHit, HitResult.IgnoreMiss)] From cf76d777623d32947d1285384c71f540a586dea3 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Fri, 9 Oct 2020 17:34:01 +0200 Subject: [PATCH 3800/6909] Fix osu!classic skin elements not showing up in storyboards --- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 45c74da892..cd09cafbce 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.IO; using osuTK; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -138,7 +139,8 @@ namespace osu.Game.Storyboards.Drawables protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); - var newTexture = skin?.GetTexture(Sprite.Path) ?? storyboardTextureStore?.Get(texturePath); + + var newTexture = skin?.GetTexture(Path.GetFileNameWithoutExtension(Sprite.Path)) ?? storyboardTextureStore?.Get(texturePath); if (drawableSprite.Texture == newTexture) return; From f41fc71e42c9301889a8cff230a723a8ba8007d8 Mon Sep 17 00:00:00 2001 From: Sebastian Krajewski Date: Fri, 9 Oct 2020 18:02:21 +0200 Subject: [PATCH 3801/6909] Allow storyboard animations to load textures from skins --- .../Drawables/DrawableStoryboardAnimation.cs | 47 +++++++++++++++---- 1 file changed, 39 insertions(+), 8 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 72e52f6106..963cf37fea 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.IO; using osuTK; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -10,13 +12,22 @@ using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Skinning; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardAnimation : TextureAnimation, IFlippable, IVectorScalable + public class DrawableStoryboardAnimation : SkinReloadableDrawable, IFlippable, IVectorScalable { public StoryboardAnimation Animation { get; } + private TextureAnimation drawableTextureAnimation; + + [Resolved] + private TextureStore storyboardTextureStore { get; set; } + + private readonly List texturePathsRaw = new List(); + private readonly List texturePaths = new List(); + private bool flipH; public bool FlipH @@ -108,28 +119,48 @@ namespace osu.Game.Storyboards.Drawables Animation = animation; Origin = animation.Origin; Position = animation.InitialPosition; - Loop = animation.LoopType == AnimationLoopType.LoopForever; LifetimeStart = animation.StartTime; LifetimeEnd = animation.EndTime; } [BackgroundDependencyLoader] - private void load(IBindable beatmap, TextureStore textureStore) + private void load(IBindable beatmap) { + InternalChild = drawableTextureAnimation = new TextureAnimation + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Loop = Animation.LoopType == AnimationLoopType.LoopForever + }; + for (var frame = 0; frame < Animation.FrameCount; frame++) { var framePath = Animation.Path.Replace(".", frame + "."); + texturePathsRaw.Add(Path.GetFileNameWithoutExtension(framePath)); var path = beatmap.Value.BeatmapSetInfo.Files.Find(f => f.Filename.Equals(framePath, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - if (path == null) - continue; - - var texture = textureStore.Get(path); - AddFrame(texture, Animation.FrameDelay); + texturePaths.Add(path); } Animation.ApplyTransforms(this); } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + drawableTextureAnimation.ClearFrames(); + + for (var frame = 0; frame < Animation.FrameCount; frame++) + { + var texture = skin?.GetTexture(texturePathsRaw[frame]) ?? storyboardTextureStore?.Get(texturePaths[frame]); + + if (texture == null) + continue; + + drawableTextureAnimation.AddFrame(texture, Animation.FrameDelay); + } + } } } From 6459ce28a34654c94c351e22340ce166c46b2ed8 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 9 Oct 2020 18:32:03 +0200 Subject: [PATCH 3802/6909] Don't calculate performance if difficulty attributes aren't locally computable. --- osu.Game/Scoring/ScorePerformanceManager.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index 783425052f..c2e36ae798 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -43,6 +43,10 @@ namespace osu.Game.Scoring { var attributes = await difficultyManager.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); + // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. + if (attributes.Attributes == null) + return default; + if (token.IsCancellationRequested) return default; From 20f1eb2b33765d477fdabf04a7f3e287fe2b9170 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Oct 2020 13:11:36 +0900 Subject: [PATCH 3803/6909] Fix windows key blocking applying when window is inactive / when watching a replay Closes #10467. --- osu.Desktop/Windows/GameplayWinKeyBlocker.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs index 86174ceb90..07af009b81 100644 --- a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs +++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs @@ -5,24 +5,24 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; +using osu.Game; using osu.Game.Configuration; namespace osu.Desktop.Windows { public class GameplayWinKeyBlocker : Component { - private Bindable allowScreenSuspension; private Bindable disableWinKey; + private Bindable localUserPlaying; - private GameHost host; + [Resolved] + private GameHost host { get; set; } - [BackgroundDependencyLoader] - private void load(GameHost host, OsuConfigManager config) + [BackgroundDependencyLoader(true)] + private void load(OsuGame game, OsuConfigManager config) { - this.host = host; - - allowScreenSuspension = host.AllowScreenSuspension.GetBoundCopy(); - allowScreenSuspension.BindValueChanged(_ => updateBlocking()); + localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); + localUserPlaying.BindValueChanged(_ => updateBlocking()); disableWinKey = config.GetBindable(OsuSetting.GameplayDisableWinKey); disableWinKey.BindValueChanged(_ => updateBlocking(), true); @@ -30,7 +30,7 @@ namespace osu.Desktop.Windows private void updateBlocking() { - bool shouldDisable = disableWinKey.Value && !allowScreenSuspension.Value; + bool shouldDisable = disableWinKey.Value && localUserPlaying.Value; if (shouldDisable) host.InputThread.Scheduler.Add(WindowsKey.Disable); From 09e350d14d069def409a82a1b56128bdd79fb17d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Oct 2020 13:28:24 +0900 Subject: [PATCH 3804/6909] Remove canBNull specification --- osu.Desktop/Windows/GameplayWinKeyBlocker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs index 07af009b81..efc3f21149 100644 --- a/osu.Desktop/Windows/GameplayWinKeyBlocker.cs +++ b/osu.Desktop/Windows/GameplayWinKeyBlocker.cs @@ -18,7 +18,7 @@ namespace osu.Desktop.Windows [Resolved] private GameHost host { get; set; } - [BackgroundDependencyLoader(true)] + [BackgroundDependencyLoader] private void load(OsuGame game, OsuConfigManager config) { localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); From df9c4bf0a554dd9df255a84546b05aac2b605e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 10 Oct 2020 13:01:52 +0200 Subject: [PATCH 3805/6909] Improve test xmldoc slightly --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 40e6589ac4..b83b97a539 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -55,19 +55,24 @@ namespace osu.Game.Tests.Rulesets.Scoring } /// - /// Test to see that all contribute to score portions in correct amounts. + /// Test to see that all s contribute to score portions in correct amounts. /// - /// Scoring mode to test - /// HitResult that will be applied to HitObjects - /// HitResult used for accuracy calcualtion - /// Expected score after 3/4 hitobjects have been hit rounded to nearest integer + /// Scoring mode to test. + /// The that will be applied to selected hit objects. + /// The maximum achievable. + /// Expected score after all objects have been judged, rounded to the nearest integer. /// - /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo - /// expectedScore is calcualted using this algorithm for standardised scoring: 1_000_000 * ((75% * score_per_hitobject / max_per_hitobject * 30%) + (50% * 70%)) - /// "75% * score_per_hitobject / max_per_hitobject" is the accuracy we would get for hitting 3/4 hitobjects that award "score_per_hitobject / max_per_hitobject" accuracy each hit - /// - /// expectedScore is calculated using this algorithm for classic scoring: score_per_hitobject / max_per_hitobject * 936 - /// "936" is simplified from "75% * 4 * 300 * (1 + 1/25)" + /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and exactly 50% max combo. + /// + /// For standardised scoring, is calculated using the following formula: + /// 1_000_000 * ((3 * / 4 * ) * 30% + 50% * 70%) + /// + /// + /// For classic scoring, is calculated using the following formula: + /// / * 936 + /// where 936 is simplified from: + /// 75% * 4 * 300 * (1 + 1/25) + /// /// [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] From 73c238fae3d395f65f4089c2c68179ea828c4a77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 10 Oct 2020 21:34:01 +0900 Subject: [PATCH 3806/6909] Add the ability to search for local beatmaps via online IDs Closes #10470. --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 8 ++++++++ osu.Game/Screens/Select/FilterCriteria.cs | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 3892e02a8f..83e3c84f39 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -58,6 +58,14 @@ namespace osu.Game.Screens.Select.Carousel foreach (var criteriaTerm in criteria.SearchTerms) match &= terms.Any(term => term.IndexOf(criteriaTerm, StringComparison.InvariantCultureIgnoreCase) >= 0); + + // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. + // this should be done after text matching so we can prioritise matching numbers in metadata. + if (!match && criteria.SearchNumber.HasValue) + { + match = (Beatmap.OnlineBeatmapID == criteria.SearchNumber.Value) || + (Beatmap.BeatmapSet?.OnlineBeatmapSetID == criteria.SearchNumber.Value); + } } if (match) diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 66f164bca8..f34f8f6505 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -43,6 +43,11 @@ namespace osu.Game.Screens.Select private string searchText; + /// + /// as a number (if it can be parsed as one). + /// + public int? SearchNumber { get; private set; } + public string SearchText { get => searchText; @@ -50,6 +55,11 @@ namespace osu.Game.Screens.Select { searchText = value; SearchTerms = searchText.Split(new[] { ',', ' ', '!' }, StringSplitOptions.RemoveEmptyEntries).ToArray(); + + SearchNumber = null; + + if (SearchTerms.Length == 1 && int.TryParse(SearchTerms[0], out int parsed)) + SearchNumber = parsed; } } From 7bbdd6ab25917834bf04ac46a57424f53b97efa3 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 10 Oct 2020 21:07:17 +0800 Subject: [PATCH 3807/6909] expose break time bindable --- osu.Game/Screens/Play/Player.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 74714e7e59..6d910e39ed 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -89,6 +89,8 @@ namespace osu.Game.Screens.Play public BreakOverlay BreakOverlay; + public IBindable IsBreakTime => breakTracker?.IsBreakTime; + private BreakTracker breakTracker; private SkipOverlay skipOverlay; From a7c43e17c2a5a72ae358334f49ae46da5fbcb4a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 10 Oct 2020 15:41:48 +0200 Subject: [PATCH 3808/6909] Add test coverage --- .../NonVisual/Filtering/FilterMatchingTest.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 30686cb947..24a0a662ba 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -197,5 +197,22 @@ namespace osu.Game.Tests.NonVisual.Filtering carouselItem.Filter(criteria); Assert.AreEqual(filtered, carouselItem.Filtered.Value); } + + [TestCase("202010", true)] + [TestCase("20201010", false)] + [TestCase("153", true)] + [TestCase("1535", false)] + public void TestCriteriaMatchingBeatmapIDs(string query, bool filtered) + { + var beatmap = getExampleBeatmap(); + beatmap.OnlineBeatmapID = 20201010; + beatmap.BeatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = 1535 }; + + var criteria = new FilterCriteria { SearchText = query }; + var carouselItem = new CarouselBeatmap(beatmap); + carouselItem.Filter(criteria); + + Assert.AreEqual(filtered, carouselItem.Filtered.Value); + } } } From f41879ee7c84509bbab4018e7112e08a9a7bfd1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 10 Oct 2020 17:54:37 +0200 Subject: [PATCH 3809/6909] Show current hit circle placement in timeline --- .../Blueprints/HitCircles/HitCirclePlacementBlueprint.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index 3dbbdcc5d0..e14d6647d2 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -21,6 +21,12 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles InternalChild = circlePiece = new HitCirclePiece(); } + protected override void LoadComplete() + { + base.LoadComplete(); + BeginPlacement(); + } + protected override void Update() { base.Update(); From 75b26d0cdecd5335c4009009036e0b2c160bde63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 10 Oct 2020 18:08:19 +0200 Subject: [PATCH 3810/6909] Add failing test cases --- .../Beatmaps/BeatmapDifficultyManagerTest.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs index 0f6d956b3c..7c1ddd757f 100644 --- a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs +++ b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs @@ -28,5 +28,28 @@ namespace osu.Game.Tests.Beatmaps Assert.That(key1, Is.EqualTo(key2)); } + + [TestCase(1.3, DifficultyRating.Easy)] + [TestCase(1.993, DifficultyRating.Easy)] + [TestCase(1.998, DifficultyRating.Normal)] + [TestCase(2.4, DifficultyRating.Normal)] + [TestCase(2.693, DifficultyRating.Normal)] + [TestCase(2.698, DifficultyRating.Hard)] + [TestCase(3.5, DifficultyRating.Hard)] + [TestCase(3.993, DifficultyRating.Hard)] + [TestCase(3.997, DifficultyRating.Insane)] + [TestCase(5.0, DifficultyRating.Insane)] + [TestCase(5.292, DifficultyRating.Insane)] + [TestCase(5.297, DifficultyRating.Expert)] + [TestCase(6.2, DifficultyRating.Expert)] + [TestCase(6.493, DifficultyRating.Expert)] + [TestCase(6.498, DifficultyRating.ExpertPlus)] + [TestCase(8.3, DifficultyRating.ExpertPlus)] + public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket) + { + var actualBracket = BeatmapDifficultyManager.GetDifficultyRating(starRating); + + Assert.AreEqual(expectedBracket, actualBracket); + } } } From 8af78656e456b70f2f0366c8c3f4741147104037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 10 Oct 2020 18:15:52 +0200 Subject: [PATCH 3811/6909] Add precision tolerance to difficulty rating range checks --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 945a60fb62..8c12ca6f6e 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -14,6 +14,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Lists; using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -124,13 +125,22 @@ namespace osu.Game.Beatmaps /// The that best describes . public static DifficultyRating GetDifficultyRating(double starRating) { - if (starRating < 2.0) return DifficultyRating.Easy; - if (starRating < 2.7) return DifficultyRating.Normal; - if (starRating < 4.0) return DifficultyRating.Hard; - if (starRating < 5.3) return DifficultyRating.Insane; - if (starRating < 6.5) return DifficultyRating.Expert; + if (Precision.AlmostBigger(starRating, 6.5, 0.005)) + return DifficultyRating.ExpertPlus; - return DifficultyRating.ExpertPlus; + if (Precision.AlmostBigger(starRating, 5.3, 0.005)) + return DifficultyRating.Expert; + + if (Precision.AlmostBigger(starRating, 4.0, 0.005)) + return DifficultyRating.Insane; + + if (Precision.AlmostBigger(starRating, 2.7, 0.005)) + return DifficultyRating.Hard; + + if (Precision.AlmostBigger(starRating, 2.0, 0.005)) + return DifficultyRating.Normal; + + return DifficultyRating.Easy; } private CancellationTokenSource trackedUpdateCancellationSource; From de522d53eaf408238b81df3e8eb3dd3603b68549 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 10 Oct 2020 19:16:21 +0200 Subject: [PATCH 3812/6909] Make CalculatePerformanceAsync() nullable. --- osu.Game/Scoring/ScorePerformanceManager.cs | 14 +++++++------- .../Expanded/Statistics/PerformanceStatistic.cs | 6 +++++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index c2e36ae798..21b1b679eb 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -24,7 +24,7 @@ namespace osu.Game.Scoring /// /// The score to do the calculation on. /// An optional to cancel the operation. - public async Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) + public async Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) { if (tryGetExisting(score, out var perf, out var lookupKey)) return perf; @@ -39,21 +39,21 @@ namespace osu.Game.Scoring return performanceCache.TryGetValue(lookupKey, out performance); } - private async Task computePerformanceAsync(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) + private async Task computePerformanceAsync(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) { var attributes = await difficultyManager.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. if (attributes.Attributes == null) - return default; + return null; - if (token.IsCancellationRequested) - return default; + token.ThrowIfCancellationRequested(); var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Attributes, score); - var total = calculator?.Calculate() ?? default; + var total = calculator?.Calculate(); - performanceCache[lookupKey] = total; + if (total.HasValue) + performanceCache[lookupKey] = total.Value; return total; } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 7e342a33f1..068745ca17 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -32,7 +32,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics else { performanceManager.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => performance.Value = (int)t.Result), cancellationTokenSource.Token); + .ContinueWith(t => Schedule(() => + { + if (t.Result.HasValue) + performance.Value = (int)t.Result.Value; + }), cancellationTokenSource.Token); } } From a0e6226b7aef1decee427628fa0e2f54db2ad9ad Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 10 Oct 2020 19:19:24 +0200 Subject: [PATCH 3813/6909] Rename LocalId -> LocalScoreID --- osu.Game/Scoring/ScorePerformanceManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index 21b1b679eb..d8daf7189a 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -61,12 +61,12 @@ namespace osu.Game.Scoring public readonly struct PerformanceCacheLookup { public readonly string ScoreHash; - public readonly int LocalId; + public readonly int LocalScoreID; public PerformanceCacheLookup(ScoreInfo info) { ScoreHash = info.Hash; - LocalId = info.ID; + LocalScoreID = info.ID; } public override int GetHashCode() @@ -74,7 +74,7 @@ namespace osu.Game.Scoring var hash = new HashCode(); hash.Add(ScoreHash); - hash.Add(LocalId); + hash.Add(LocalScoreID); return hash.ToHashCode(); } From e845cc92b8d656373969dda9b767994fae0846ed Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 10 Oct 2020 19:58:06 +0200 Subject: [PATCH 3814/6909] Round pp values to nearest integer. --- .../Ranking/Expanded/Statistics/PerformanceStatistic.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 068745ca17..3976682241 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.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.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -27,7 +28,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { if (score.PP.HasValue) { - performance.Value = (int)score.PP.Value; + performance.Value = (int)Math.Round(score.PP.Value, MidpointRounding.AwayFromZero); } else { @@ -35,7 +36,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics .ContinueWith(t => Schedule(() => { if (t.Result.HasValue) - performance.Value = (int)t.Result.Value; + performance.Value = (int)Math.Round(t.Result.Value, MidpointRounding.AwayFromZero); }), cancellationTokenSource.Token); } } From 6a52c98a428b5c3c032c5908d863ce4bcfc9ca89 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 11 Oct 2020 06:15:20 +0800 Subject: [PATCH 3815/6909] make IsBreakTime its own bindable and bind it to BreakTracker on load --- osu.Game/Screens/Play/Player.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 6d910e39ed..4f13843503 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -89,7 +89,10 @@ namespace osu.Game.Screens.Play public BreakOverlay BreakOverlay; - public IBindable IsBreakTime => breakTracker?.IsBreakTime; + /// + /// Whether the gameplay is currently in a break. + /// + public readonly BindableBool IsBreakTime = new BindableBool(); private BreakTracker breakTracker; @@ -259,6 +262,7 @@ namespace osu.Game.Screens.Play mod.ApplyToHealthProcessor(HealthProcessor); breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); + IsBreakTime.BindTo((BindableBool)breakTracker.IsBreakTime); } private Drawable createUnderlayComponents() => From 8faa86b048e982eaa75e70abc49308a53000306e Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 10 Oct 2020 18:30:13 -0700 Subject: [PATCH 3816/6909] Add ability to toggle extended statistics using space or enter --- osu.Game/Screens/Ranking/ResultsScreen.cs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index c48cd238c0..026ce01857 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -10,9 +10,11 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Bindings; using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; @@ -22,7 +24,7 @@ using osuTK; namespace osu.Game.Screens.Ranking { - public abstract class ResultsScreen : OsuScreen + public abstract class ResultsScreen : OsuScreen, IKeyBindingHandler { protected const float BACKGROUND_BLUR = 20; private static readonly float screen_height = 768 - TwoLayerButton.SIZE_EXTENDED.Y; @@ -314,6 +316,22 @@ namespace osu.Game.Screens.Ranking } } + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.Select: + statisticsPanel.ToggleVisibility(); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } + private class VerticalScrollContainer : OsuScrollContainer { protected override Container Content => content; From 5fcdee6fd8e2fe9631b0564878aa2b63179dc6c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 11 Oct 2020 21:46:55 +0900 Subject: [PATCH 3817/6909] Remove cast and expose as IBindable --- 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 4f13843503..4dfa609a2e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -92,7 +92,7 @@ namespace osu.Game.Screens.Play /// /// Whether the gameplay is currently in a break. /// - public readonly BindableBool IsBreakTime = new BindableBool(); + public readonly IBindable IsBreakTime = new BindableBool(); private BreakTracker breakTracker; @@ -261,8 +261,8 @@ namespace osu.Game.Screens.Play foreach (var mod in Mods.Value.OfType()) mod.ApplyToHealthProcessor(HealthProcessor); + IsBreakTime.BindTo(breakTracker.IsBreakTime); breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); - IsBreakTime.BindTo((BindableBool)breakTracker.IsBreakTime); } private Drawable createUnderlayComponents() => From de6fe34361424c9ea1fa15697b90667e0d95b1e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 11 Oct 2020 21:51:48 +0900 Subject: [PATCH 3818/6909] Bind to local bindable and combine dual bindings --- 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 4dfa609a2e..a2a53b4b75 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -231,7 +231,6 @@ namespace osu.Game.Screens.Play DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState()); - breakTracker.IsBreakTime.BindValueChanged(_ => updateGameplayState()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); @@ -262,7 +261,7 @@ namespace osu.Game.Screens.Play mod.ApplyToHealthProcessor(HealthProcessor); IsBreakTime.BindTo(breakTracker.IsBreakTime); - breakTracker.IsBreakTime.BindValueChanged(onBreakTimeChanged, true); + IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } private Drawable createUnderlayComponents() => @@ -360,6 +359,7 @@ namespace osu.Game.Screens.Play private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { + updateGameplayState(); updatePauseOnFocusLostState(); HUDOverlay.KeyCounter.IsCounting = !isBreakTime.NewValue; } From e5548a12161afb7bf77c67f3cb68bcd662b61998 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Mon, 12 Oct 2020 00:16:18 +0200 Subject: [PATCH 3819/6909] Move ModSettingsContainer class inside ModSelectOverlay --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 4eb4fc6501..3b158ee98d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -284,7 +284,7 @@ namespace osu.Game.Overlays.Mods }, }, }, - ModSettingsContainer = new Container + ModSettingsContainer = new CModSettingsContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, @@ -495,5 +495,19 @@ namespace osu.Game.Overlays.Mods } #endregion + + protected class CModSettingsContainer : Container + { + protected override bool OnMouseDown(MouseDownEvent e) + { + return true; + } + + protected override bool OnHover(HoverEvent e) + { + return true; + } + } + } } From ac4290dfb65c19374e66e6c47c7d132c4440dd03 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Oct 2020 15:27:33 +0900 Subject: [PATCH 3820/6909] Add comment about stable calculation --- .../Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 415201951b..30d33de06e 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -57,8 +57,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy beatLength = timingPoint.BeatLength / difficultyPoint.SpeedMultiplier; SpanCount = repeatsData?.SpanCount() ?? 1; - StartTime = (int)Math.Round(hitObject.StartTime); + + // This matches stable's calculation. EndTime = (int)Math.Floor(StartTime + distanceData.Distance * beatLength * SpanCount * 0.01 / beatmap.BeatmapInfo.BaseDifficulty.SliderMultiplier); SegmentDuration = (EndTime - StartTime) / SpanCount; From 379971578dbc7cdc80c352ae35a46d0025602784 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Oct 2020 15:28:16 +0900 Subject: [PATCH 3821/6909] Remove culling notice from HasEffect --- osu.Game/Beatmaps/Timing/BreakPeriod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Timing/BreakPeriod.cs b/osu.Game/Beatmaps/Timing/BreakPeriod.cs index bb8ae4a66a..4c90b16745 100644 --- a/osu.Game/Beatmaps/Timing/BreakPeriod.cs +++ b/osu.Game/Beatmaps/Timing/BreakPeriod.cs @@ -28,7 +28,7 @@ namespace osu.Game.Beatmaps.Timing public double Duration => EndTime - StartTime; /// - /// Whether the break has any effect. Breaks that are too short are culled before they are added to the beatmap. + /// Whether the break has any effect. /// public bool HasEffect => Duration >= MIN_BREAK_DURATION; From ccf7e2c49a0818c51c669fc620dcfa13edbe083a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Oct 2020 16:31:42 +0900 Subject: [PATCH 3822/6909] Fallback to default ruleset star rating if conversion fails --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index 8c12ca6f6e..c1f4c07833 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -13,10 +13,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Lists; +using osu.Framework.Logging; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; namespace osu.Game.Beatmaps { @@ -238,6 +240,24 @@ namespace osu.Game.Beatmaps return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo); } + catch (BeatmapInvalidForRulesetException e) + { + // Conversion has failed for the given ruleset, so return the difficulty in the beatmap's default ruleset. + + // Ensure the beatmap's default ruleset isn't the one already being converted to. + // This shouldn't happen as it means something went seriously wrong, but if it does an endless loop should be avoided. + if (rulesetInfo.Equals(beatmapInfo.Ruleset)) + { + Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset})."); + return difficultyCache[key] = new StarDifficulty(); + } + + // Check the cache first because this is now a different ruleset than the one previously guarded against. + if (tryGetExisting(beatmapInfo, beatmapInfo.Ruleset, Array.Empty(), out var existingDefault, out var existingDefaultKey)) + return existingDefault; + + return computeDifficulty(existingDefaultKey, beatmapInfo, beatmapInfo.Ruleset); + } catch { return difficultyCache[key] = new StarDifficulty(); From e70d2614747fafb47b6d0393117c890d0de176e3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Oct 2020 18:03:41 +0900 Subject: [PATCH 3823/6909] Add failing test --- .../Formats/LegacyBeatmapDecoderTest.cs | 39 +++++++++++++++++++ .../Resources/multi-segment-slider.osu | 8 ++++ 2 files changed, 47 insertions(+) create mode 100644 osu.Game.Tests/Resources/multi-segment-slider.osu diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index dab923d75b..74055ca3ce 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -651,5 +651,44 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.IsInstanceOf(decoder); } } + + [Test] + public void TestMultiSegmentSliders() + { + var decoder = new LegacyBeatmapDecoder { ApplyOffsets = false }; + + using (var resStream = TestResources.OpenResource("multi-segment-slider.osu")) + using (var stream = new LineBufferedReader(resStream)) + { + var decoded = decoder.Decode(stream); + + // Multi-segment + var first = ((IHasPath)decoded.HitObjects[0]).Path; + + Assert.That(first.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero)); + Assert.That(first.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve)); + Assert.That(first.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244))); + Assert.That(first.ControlPoints[1].Type.Value, Is.EqualTo(null)); + + Assert.That(first.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3))); + Assert.That(first.ControlPoints[2].Type.Value, Is.EqualTo(PathType.Bezier)); + Assert.That(first.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(68, 15))); + Assert.That(first.ControlPoints[3].Type.Value, Is.EqualTo(null)); + Assert.That(first.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(259, -132))); + Assert.That(first.ControlPoints[4].Type.Value, Is.EqualTo(null)); + Assert.That(first.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(92, -107))); + Assert.That(first.ControlPoints[5].Type.Value, Is.EqualTo(null)); + + // Single-segment + var second = ((IHasPath)decoded.HitObjects[1]).Path; + + Assert.That(second.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero)); + Assert.That(second.ControlPoints[0].Type.Value, Is.EqualTo(PathType.PerfectCurve)); + Assert.That(second.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(161, -244))); + Assert.That(second.ControlPoints[1].Type.Value, Is.EqualTo(null)); + Assert.That(second.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3))); + Assert.That(second.ControlPoints[2].Type.Value, Is.EqualTo(null)); + } + } } } diff --git a/osu.Game.Tests/Resources/multi-segment-slider.osu b/osu.Game.Tests/Resources/multi-segment-slider.osu new file mode 100644 index 0000000000..03cddba5e5 --- /dev/null +++ b/osu.Game.Tests/Resources/multi-segment-slider.osu @@ -0,0 +1,8 @@ +osu file format v128 + +[HitObjects] +// Multi-segment +63,301,1000,6,0,P|224:57|B|439:298|131:316|322:169|155:194,1,1040,0|0,0:0|0:0,0:0:0:0: + +// Single-segment +63,301,2000,6,0,P|224:57|439:298,1,1040,0|0,0:0|0:0,0:0:0:0: \ No newline at end of file From 48c0ae40effb05113829dc5c6dffc8ac985ada28 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Oct 2020 18:04:28 +0900 Subject: [PATCH 3824/6909] Fix multi-segment sliders not parsing correctly --- .../Objects/Legacy/ConvertHitObjectParser.cs | 157 ++++++++++++------ 1 file changed, 102 insertions(+), 55 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index f6adeced96..c5ea26bc20 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -70,53 +70,33 @@ namespace osu.Game.Rulesets.Objects.Legacy } else if (type.HasFlag(LegacyHitObjectType.Slider)) { - PathType pathType = PathType.Catmull; double? length = null; string[] pointSplit = split[5].Split('|'); - int pointCount = 1; + var controlPoints = new List>(); + int startIndex = 0; + int endIndex = 0; + bool first = true; - foreach (var t in pointSplit) + while (++endIndex < pointSplit.Length) { - if (t.Length > 1) - pointCount++; - } - - var points = new Vector2[pointCount]; - - int pointIndex = 1; - - foreach (string t in pointSplit) - { - if (t.Length == 1) - { - switch (t) - { - case @"C": - pathType = PathType.Catmull; - break; - - case @"B": - pathType = PathType.Bezier; - break; - - case @"L": - pathType = PathType.Linear; - break; - - case @"P": - pathType = PathType.PerfectCurve; - break; - } - + // Keep incrementing endIndex while it's not the start of a new segment (indicated by having a type descriptor of length 1). + if (pointSplit[endIndex].Length > 1) continue; - } - string[] temp = t.Split(':'); - points[pointIndex++] = new Vector2((int)Parsing.ParseDouble(temp[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(temp[1], Parsing.MAX_COORDINATE_VALUE)) - pos; + // Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment. + // The start of the next segment is the index after the type descriptor. + string endPoint = endIndex < pointSplit.Length - 1 ? pointSplit[endIndex + 1] : null; + + controlPoints.Add(convertControlPoints(pointSplit.AsSpan().Slice(startIndex, endIndex - startIndex), endPoint, first, pos)); + startIndex = endIndex; + first = false; } + if (endIndex > startIndex) + controlPoints.Add(convertControlPoints(pointSplit.AsSpan().Slice(startIndex, endIndex - startIndex), null, first, pos)); + int repeatCount = Parsing.ParseInt(split[6]); if (repeatCount > 9000) @@ -183,7 +163,7 @@ namespace osu.Game.Rulesets.Objects.Legacy for (int i = 0; i < nodes; i++) nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - result = CreateSlider(pos, combo, comboOffset, convertControlPoints(points, pathType), length, repeatCount, nodeSamples); + result = CreateSlider(pos, combo, comboOffset, mergeControlPoints(controlPoints), length, repeatCount, nodeSamples); } else if (type.HasFlag(LegacyHitObjectType.Spinner)) { @@ -252,8 +232,56 @@ namespace osu.Game.Rulesets.Objects.Legacy bankInfo.Filename = split.Length > 4 ? split[4] : null; } - private PathControlPoint[] convertControlPoints(Vector2[] vertices, PathType type) + private PathType convertPathType(string input) { + switch (input[0]) + { + default: + case 'C': + return PathType.Catmull; + + case 'B': + return PathType.Bezier; + + case 'L': + return PathType.Linear; + + case 'P': + return PathType.PerfectCurve; + } + } + + /// + /// Converts a given point list into a set of s. + /// + /// The point list. + /// Any extra endpoint to consider as part of the points. This will NOT be returned. + /// Whether this is the first point list in the set. If true the returned set will contain a zero point. + /// The positional offset to apply to the control points. + /// The set of points contained by , prepended by an extra zero point if is true. + private Memory convertControlPoints(ReadOnlySpan pointSpan, string endPoint, bool first, Vector2 offset) + { + PathType type = convertPathType(pointSpan[0]); + + int readOffset = first ? 1 : 0; // First control point is zero for the first segment. + int readablePoints = pointSpan.Length - 1; // Total points readable from the base point span. + int endPointLength = endPoint != null ? 1 : 0; // Extra length if an endpoint is given that lies outside the base point span. + + var vertices = new PathControlPoint[readOffset + readablePoints + endPointLength]; + + // Fill any non-read points. + for (int i = 0; i < readOffset; i++) + vertices[i] = new PathControlPoint(); + + // Parse into control points. + for (int i = 1; i < pointSpan.Length; i++) + readPoint(pointSpan[i], offset, out vertices[readOffset + i - 1]); + + // If an endpoint is given, add it now. + if (endPoint != null) + readPoint(endPoint, offset, out vertices[^1]); + + // Edge-case rules. if (type == PathType.PerfectCurve) { if (vertices.Length != 3) @@ -265,29 +293,48 @@ namespace osu.Game.Rulesets.Objects.Legacy } } - var points = new List(vertices.Length) - { - new PathControlPoint - { - Position = { Value = vertices[0] }, - Type = { Value = type } - } - }; + // Set a definite type for the first control point. + vertices[0].Type.Value = type; + // A path can have multiple segments of the same type if there are two sequential control points with the same position. for (int i = 1; i < vertices.Length; i++) { if (vertices[i] == vertices[i - 1]) - { - points[^1].Type.Value = type; - continue; - } - - points.Add(new PathControlPoint { Position = { Value = vertices[i] } }); + vertices[i].Type.Value = type; } - return points.ToArray(); + return vertices.AsMemory().Slice(0, vertices.Length - endPointLength); - static bool isLinear(Vector2[] p) => Precision.AlmostEquals(0, (p[1].Y - p[0].Y) * (p[2].X - p[0].X) - (p[1].X - p[0].X) * (p[2].Y - p[0].Y)); + static void readPoint(string value, Vector2 startPos, out PathControlPoint point) + { + string[] vertexSplit = value.Split(':'); + + Vector2 pos = new Vector2((int)Parsing.ParseDouble(vertexSplit[0], Parsing.MAX_COORDINATE_VALUE), (int)Parsing.ParseDouble(vertexSplit[1], Parsing.MAX_COORDINATE_VALUE)) - startPos; + point = new PathControlPoint { Position = { Value = pos } }; + } + + static bool isLinear(PathControlPoint[] p) => Precision.AlmostEquals(0, (p[1].Position.Value.Y - p[0].Position.Value.Y) * (p[2].Position.Value.X - p[0].Position.Value.X) + - (p[1].Position.Value.X - p[0].Position.Value.X) * (p[2].Position.Value.Y - p[0].Position.Value.Y)); + } + + private PathControlPoint[] mergeControlPoints(List> controlPointList) + { + int totalCount = 0; + + foreach (var arr in controlPointList) + totalCount += arr.Length; + + var mergedArray = new PathControlPoint[totalCount]; + var mergedArrayMemory = mergedArray.AsMemory(); + int copyIndex = 0; + + foreach (var arr in controlPointList) + { + arr.CopyTo(mergedArrayMemory.Slice(copyIndex)); + copyIndex += arr.Length; + } + + return mergedArray; } /// From 36a8f61d264372e26b9da448c441519fd4a3be31 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Oct 2020 18:58:07 +0900 Subject: [PATCH 3825/6909] Add failing test for implicit segments --- .../Formats/LegacyBeatmapDecoderTest.cs | 20 +++++++++++++++++++ .../Resources/multi-segment-slider.osu | 5 ++++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 74055ca3ce..58fd6b0448 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -688,6 +688,26 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(second.ControlPoints[1].Type.Value, Is.EqualTo(null)); Assert.That(second.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(376, -3))); Assert.That(second.ControlPoints[2].Type.Value, Is.EqualTo(null)); + + // Implicit multi-segment + var third = ((IHasPath)decoded.HitObjects[2]).Path; + + Assert.That(third.ControlPoints[0].Position.Value, Is.EqualTo(Vector2.Zero)); + Assert.That(third.ControlPoints[0].Type.Value, Is.EqualTo(PathType.Bezier)); + Assert.That(third.ControlPoints[1].Position.Value, Is.EqualTo(new Vector2(0, 192))); + Assert.That(third.ControlPoints[1].Type.Value, Is.EqualTo(null)); + Assert.That(third.ControlPoints[2].Position.Value, Is.EqualTo(new Vector2(224, 192))); + Assert.That(third.ControlPoints[2].Type.Value, Is.EqualTo(null)); + + Assert.That(third.ControlPoints[3].Position.Value, Is.EqualTo(new Vector2(224, 0))); + Assert.That(third.ControlPoints[3].Type.Value, Is.EqualTo(PathType.Bezier)); + Assert.That(third.ControlPoints[4].Position.Value, Is.EqualTo(new Vector2(224, -192))); + Assert.That(third.ControlPoints[4].Type.Value, Is.EqualTo(null)); + Assert.That(third.ControlPoints[5].Position.Value, Is.EqualTo(new Vector2(480, -192))); + Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null)); + Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0))); + Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null)); + } } } diff --git a/osu.Game.Tests/Resources/multi-segment-slider.osu b/osu.Game.Tests/Resources/multi-segment-slider.osu index 03cddba5e5..6eabe640e4 100644 --- a/osu.Game.Tests/Resources/multi-segment-slider.osu +++ b/osu.Game.Tests/Resources/multi-segment-slider.osu @@ -5,4 +5,7 @@ osu file format v128 63,301,1000,6,0,P|224:57|B|439:298|131:316|322:169|155:194,1,1040,0|0,0:0|0:0,0:0:0:0: // Single-segment -63,301,2000,6,0,P|224:57|439:298,1,1040,0|0,0:0|0:0,0:0:0:0: \ No newline at end of file +63,301,2000,6,0,P|224:57|439:298,1,1040,0|0,0:0|0:0,0:0:0:0: + +// Implicit multi-segment +32,192,3000,6,0,B|32:384|256:384|256:192|256:192|256:0|512:0|512:192,1,800 From eb4ef157ca3d1d9059bcc7f8a41690a3fa32773b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Oct 2020 19:16:37 +0900 Subject: [PATCH 3826/6909] Fix implicit segments not being constructed correctly --- .../Objects/Legacy/ConvertHitObjectParser.cs | 121 ++++++++++++------ 1 file changed, 80 insertions(+), 41 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index c5ea26bc20..22447180ec 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -72,31 +72,6 @@ namespace osu.Game.Rulesets.Objects.Legacy { double? length = null; - string[] pointSplit = split[5].Split('|'); - - var controlPoints = new List>(); - int startIndex = 0; - int endIndex = 0; - bool first = true; - - while (++endIndex < pointSplit.Length) - { - // Keep incrementing endIndex while it's not the start of a new segment (indicated by having a type descriptor of length 1). - if (pointSplit[endIndex].Length > 1) - continue; - - // Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment. - // The start of the next segment is the index after the type descriptor. - string endPoint = endIndex < pointSplit.Length - 1 ? pointSplit[endIndex + 1] : null; - - controlPoints.Add(convertControlPoints(pointSplit.AsSpan().Slice(startIndex, endIndex - startIndex), endPoint, first, pos)); - startIndex = endIndex; - first = false; - } - - if (endIndex > startIndex) - controlPoints.Add(convertControlPoints(pointSplit.AsSpan().Slice(startIndex, endIndex - startIndex), null, first, pos)); - int repeatCount = Parsing.ParseInt(split[6]); if (repeatCount > 9000) @@ -163,7 +138,7 @@ namespace osu.Game.Rulesets.Objects.Legacy for (int i = 0; i < nodes; i++) nodeSamples.Add(convertSoundType(nodeSoundTypes[i], nodeBankInfos[i])); - result = CreateSlider(pos, combo, comboOffset, mergeControlPoints(controlPoints), length, repeatCount, nodeSamples); + result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples); } else if (type.HasFlag(LegacyHitObjectType.Spinner)) { @@ -252,19 +227,71 @@ namespace osu.Game.Rulesets.Objects.Legacy } /// - /// Converts a given point list into a set of s. + /// Converts a given point string into a set of path control points. /// - /// The point list. - /// Any extra endpoint to consider as part of the points. This will NOT be returned. - /// Whether this is the first point list in the set. If true the returned set will contain a zero point. + /// + /// A point string takes the form: X|1:1|2:2|2:2|3:3|Y|1:1|2:2. + /// This has three segments: + /// + /// + /// X: { (1,1), (2,2) } (implicit segment) + /// + /// + /// X: { (2,2), (3,3) } (implicit segment) + /// + /// + /// Y: { (3,3), (1,1), (2, 2) } (explicit segment) + /// + /// + /// + /// The point string. /// The positional offset to apply to the control points. - /// The set of points contained by , prepended by an extra zero point if is true. - private Memory convertControlPoints(ReadOnlySpan pointSpan, string endPoint, bool first, Vector2 offset) + /// All control points in the resultant path. + private PathControlPoint[] convertPathString(string pointString, Vector2 offset) { - PathType type = convertPathType(pointSpan[0]); + // This code takes on the responsibility of handling explicit segments of the path ("X" & "Y" from above). Implicit segments are handled by calls to convertPoints(). + string[] pointSplit = pointString.Split('|'); + + var controlPoints = new List>(); + int startIndex = 0; + int endIndex = 0; + bool first = true; + + while (++endIndex < pointSplit.Length) + { + // Keep incrementing endIndex while it's not the start of a new segment (indicated by having a type descriptor of length 1). + if (pointSplit[endIndex].Length > 1) + continue; + + // Multi-segmented sliders DON'T contain the end point as part of the current segment as it's assumed to be the start of the next segment. + // The start of the next segment is the index after the type descriptor. + string endPoint = endIndex < pointSplit.Length - 1 ? pointSplit[endIndex + 1] : null; + + controlPoints.AddRange(convertPoints(pointSplit.AsMemory().Slice(startIndex, endIndex - startIndex), endPoint, first, offset)); + startIndex = endIndex; + first = false; + } + + if (endIndex > startIndex) + controlPoints.AddRange(convertPoints(pointSplit.AsMemory().Slice(startIndex, endIndex - startIndex), null, first, offset)); + + return mergePointsLists(controlPoints); + } + + /// + /// Converts a given point list into a set of path segments. + /// + /// The point list. + /// Any extra endpoint to consider as part of the points. This will NOT be returned. + /// Whether this is the first segment in the set. If true the first of the returned segments will contain a zero point. + /// The positional offset to apply to the control points. + /// The set of points contained by as one or more segments of the path, prepended by an extra zero point if is true. + private IEnumerable> convertPoints(ReadOnlyMemory points, string endPoint, bool first, Vector2 offset) + { + PathType type = convertPathType(points.Span[0]); int readOffset = first ? 1 : 0; // First control point is zero for the first segment. - int readablePoints = pointSpan.Length - 1; // Total points readable from the base point span. + int readablePoints = points.Length - 1; // Total points readable from the base point span. int endPointLength = endPoint != null ? 1 : 0; // Extra length if an endpoint is given that lies outside the base point span. var vertices = new PathControlPoint[readOffset + readablePoints + endPointLength]; @@ -274,8 +301,8 @@ namespace osu.Game.Rulesets.Objects.Legacy vertices[i] = new PathControlPoint(); // Parse into control points. - for (int i = 1; i < pointSpan.Length; i++) - readPoint(pointSpan[i], offset, out vertices[readOffset + i - 1]); + for (int i = 1; i < points.Length; i++) + readPoint(points.Span[i], offset, out vertices[readOffset + i - 1]); // If an endpoint is given, add it now. if (endPoint != null) @@ -297,13 +324,25 @@ namespace osu.Game.Rulesets.Objects.Legacy vertices[0].Type.Value = type; // A path can have multiple segments of the same type if there are two sequential control points with the same position. - for (int i = 1; i < vertices.Length; i++) + // To handle such cases, this code may return multiple path segments with the final control point in each segment having a non-null type. + int startIndex = 0; + int endIndex = 0; + + while (++endIndex < vertices.Length - endPointLength) { - if (vertices[i] == vertices[i - 1]) - vertices[i].Type.Value = type; + if (vertices[endIndex].Position.Value != vertices[endIndex - 1].Position.Value) + continue; + + // Force a type on the last point, and return the current control point set as a segment. + vertices[endIndex - 1].Type.Value = type; + yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex); + + // Skip the current control point - as it's the same as the one that's just been returned. + startIndex = endIndex + 1; } - return vertices.AsMemory().Slice(0, vertices.Length - endPointLength); + if (endIndex > startIndex) + yield return vertices.AsMemory().Slice(startIndex, endIndex - startIndex); static void readPoint(string value, Vector2 startPos, out PathControlPoint point) { @@ -317,7 +356,7 @@ namespace osu.Game.Rulesets.Objects.Legacy - (p[1].Position.Value.X - p[0].Position.Value.X) * (p[2].Position.Value.Y - p[0].Position.Value.Y)); } - private PathControlPoint[] mergeControlPoints(List> controlPointList) + private PathControlPoint[] mergePointsLists(List> controlPointList) { int totalCount = 0; From 372761a46ff3fdd8693f4646d43542dfe21e4af3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Oct 2020 19:22:34 +0900 Subject: [PATCH 3827/6909] More/better commenting --- .../Objects/Legacy/ConvertHitObjectParser.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 22447180ec..7dcbc52cea 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -304,11 +304,11 @@ namespace osu.Game.Rulesets.Objects.Legacy for (int i = 1; i < points.Length; i++) readPoint(points.Span[i], offset, out vertices[readOffset + i - 1]); - // If an endpoint is given, add it now. + // If an endpoint is given, add it to the end. if (endPoint != null) readPoint(endPoint, offset, out vertices[^1]); - // Edge-case rules. + // Edge-case rules (to match stable). if (type == PathType.PerfectCurve) { if (vertices.Length != 3) @@ -320,11 +320,15 @@ namespace osu.Game.Rulesets.Objects.Legacy } } - // Set a definite type for the first control point. + // The first control point must have a definite type. vertices[0].Type.Value = type; - // A path can have multiple segments of the same type if there are two sequential control points with the same position. + // A path can have multiple implicit segments of the same type if there are two sequential control points with the same position. // To handle such cases, this code may return multiple path segments with the final control point in each segment having a non-null type. + // For the point string X|1:1|2:2|2:2|3:3, this code returns the segments: + // X: { (1,1), (2, 2) } + // X: { (3, 3) } + // Note: (2, 2) is not returned in the second segments, as it is implicit in the path. int startIndex = 0; int endIndex = 0; From 58194b4a31169f1cb65b4a6a742078e2040a2d40 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 12 Oct 2020 19:36:35 +0900 Subject: [PATCH 3828/6909] Fix incorrect blank lines --- osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index 58fd6b0448..b6e1af57fd 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -707,7 +707,6 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.That(third.ControlPoints[5].Type.Value, Is.EqualTo(null)); Assert.That(third.ControlPoints[6].Position.Value, Is.EqualTo(new Vector2(480, 0))); Assert.That(third.ControlPoints[6].Type.Value, Is.EqualTo(null)); - } } } From 8768891b12a34c0473f460dea604ddb3d1747d2d Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Mon, 12 Oct 2020 14:41:05 +0200 Subject: [PATCH 3829/6909] Add testing for clicking mods through customisation menu --- .../UserInterface/TestSceneModSettings.cs | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index c5ce3751ef..d4c8b850d3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -18,10 +18,11 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.UI; +using osuTK.Input; namespace osu.Game.Tests.Visual.UserInterface { - public class TestSceneModSettings : OsuTestScene + public class TestSceneModSettings : OsuManualInputManagerTestScene { private TestModSelectOverlay modSelect; @@ -29,6 +30,8 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly Mod testCustomisableAutoOpenMod = new TestModCustomisable2(); + private readonly Mod testCustomisableMenuCoveredMod = new TestModCustomisable1(); + [SetUp] public void SetUp() => Schedule(() => { @@ -95,6 +98,30 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value)); } + [Test] + public void TestCustomisationMenuNoClickthrough() + { + + createModSelect(); + openModSelect(); + + AddStep("change mod settings menu width to full screen", () => modSelect.SetModSettingsWidth(1.0f)); + AddStep("select cm2", () => modSelect.SelectMod(testCustomisableAutoOpenMod)); + AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.Alpha == 1); + AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMenuCoveredMod))); + AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMenuCoveredMod).IsHovered); + AddStep("left click mod", () => InputManager.Click(MouseButton.Left)); + AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1); + AddStep("right click mod", () => InputManager.Click(MouseButton.Right)); + AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1); + } + + + + private void clickPosition(int x, int y) { + //Move cursor to coordinates + //Click coordinates + } private void createModSelect() { AddStep("create mod select", () => @@ -124,6 +151,19 @@ namespace osu.Game.Tests.Visual.UserInterface public void SelectMod(Mod mod) => ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1); + + public ModButton GetModButton(Mod mod) + { + return ModSectionsContainer.Children.Single(s => s.ModType == mod.Type). + ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())); + } + + public float SetModSettingsWidth(float NewWidth) + { + float oldWidth = ModSettingsContainer.Width; + ModSettingsContainer.Width = NewWidth; + return oldWidth; + } } public class TestRulesetInfo : RulesetInfo From 7df9282727c6997ccccc0df7ee55e930ffb674fe Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Mon, 12 Oct 2020 15:58:34 +0200 Subject: [PATCH 3830/6909] CodeAnalysis fixes --- .../UserInterface/TestSceneModSettings.cs | 22 ++++++------------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 1 - 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index d4c8b850d3..a31e244ca5 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -101,7 +101,6 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestCustomisationMenuNoClickthrough() { - createModSelect(); openModSelect(); @@ -116,12 +115,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1); } - - - private void clickPosition(int x, int y) { - //Move cursor to coordinates - //Click coordinates - } private void createModSelect() { AddStep("create mod select", () => @@ -148,20 +141,19 @@ namespace osu.Game.Tests.Visual.UserInterface public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded); - public void SelectMod(Mod mod) => - ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) - .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())).SelectNext(1); - public ModButton GetModButton(Mod mod) { - return ModSectionsContainer.Children.Single(s => s.ModType == mod.Type). - ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())); + return ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) + .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())); } - public float SetModSettingsWidth(float NewWidth) + public void SelectMod(Mod mod) => + GetModButton(mod).SelectNext(1); + + public float SetModSettingsWidth(float newWidth) { float oldWidth = ModSettingsContainer.Width; - ModSettingsContainer.Width = NewWidth; + ModSettingsContainer.Width = newWidth; return oldWidth; } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 3b158ee98d..37541358b8 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -508,6 +508,5 @@ namespace osu.Game.Overlays.Mods return true; } } - } } From 3224aa7a695828835a8aa00556614c50ecf53e8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Oct 2020 17:31:46 +0200 Subject: [PATCH 3831/6909] Clarify test math even further --- .../Rulesets/Scoring/ScoreProcessorTest.cs | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index b83b97a539..30b47d9bd7 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Rulesets.Scoring /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and exactly 50% max combo. /// /// For standardised scoring, is calculated using the following formula: - /// 1_000_000 * ((3 * / 4 * ) * 30% + 50% * 70%) + /// 1_000_000 * (((3 * ) / (4 * )) * 30% + 50% * 70%) /// /// /// For classic scoring, is calculated using the following formula: @@ -74,30 +74,30 @@ namespace osu.Game.Tests.Rulesets.Scoring /// 75% * 4 * 300 * (1 + 1/25) /// /// - [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] - [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] - [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] - [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 478_571)] - [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] - [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] - [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] - [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] - [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] - [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] - [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 700_030)] - [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 700_150)] - [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] - [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] - [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] - [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 535)] - [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] - [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] - [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] - [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] - [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] - [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] + [TestCase(ScoringMode.Standardised, HitResult.Miss, HitResult.Great, 0)] // (3 * 0) / (4 * 300) * 300_000 + (0 / 4) * 700_000 + [TestCase(ScoringMode.Standardised, HitResult.Meh, HitResult.Great, 387_500)] // (3 * 50) / (4 * 300) * 300_000 + (2 / 4) * 700_000 + [TestCase(ScoringMode.Standardised, HitResult.Ok, HitResult.Great, 425_000)] // (3 * 100) / (4 * 300) * 300_000 + (2 / 4) * 700_000 + [TestCase(ScoringMode.Standardised, HitResult.Good, HitResult.Perfect, 478_571)] // (3 * 200) / (4 * 350) * 300_000 + (2 / 4) * 700_000 + [TestCase(ScoringMode.Standardised, HitResult.Great, HitResult.Great, 575_000)] // (3 * 300) / (4 * 300) * 300_000 + (2 / 4) * 700_000 + [TestCase(ScoringMode.Standardised, HitResult.Perfect, HitResult.Perfect, 575_000)] // (3 * 350) / (4 * 350) * 300_000 + (2 / 4) * 700_000 + [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, HitResult.SmallTickHit, 700_000)] // (3 * 0) / (4 * 10) * 300_000 + 700_000 (max combo 0) + [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, HitResult.SmallTickHit, 925_000)] // (3 * 10) / (4 * 10) * 300_000 + 700_000 (max combo 0) + [TestCase(ScoringMode.Standardised, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (3 * 0) / (4 * 30) * 300_000 + (0 / 4) * 700_000 + [TestCase(ScoringMode.Standardised, HitResult.LargeTickHit, HitResult.LargeTickHit, 575_000)] // (3 * 30) / (4 * 30) * 300_000 + (0 / 4) * 700_000 + [TestCase(ScoringMode.Standardised, HitResult.SmallBonus, HitResult.SmallBonus, 700_030)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 10 (bonus points) + [TestCase(ScoringMode.Standardised, HitResult.LargeBonus, HitResult.LargeBonus, 700_150)] // 0 * 300_000 + 700_000 (max combo 0) + 3 * 50 (bonus points) + [TestCase(ScoringMode.Classic, HitResult.Miss, HitResult.Great, 0)] // (0 * 4 * 300) * (1 + 0 / 25) + [TestCase(ScoringMode.Classic, HitResult.Meh, HitResult.Great, 156)] // (((3 * 50) / (4 * 300)) * 4 * 300) * (1 + 1 / 25) + [TestCase(ScoringMode.Classic, HitResult.Ok, HitResult.Great, 312)] // (((3 * 100) / (4 * 300)) * 4 * 300) * (1 + 1 / 25) + [TestCase(ScoringMode.Classic, HitResult.Good, HitResult.Perfect, 535)] // (((3 * 200) / (4 * 350)) * 4 * 300) * (1 + 1 / 25) + [TestCase(ScoringMode.Classic, HitResult.Great, HitResult.Great, 936)] // (((3 * 300) / (4 * 300)) * 4 * 300) * (1 + 1 / 25) + [TestCase(ScoringMode.Classic, HitResult.Perfect, HitResult.Perfect, 936)] // (((3 * 350) / (4 * 350)) * 4 * 300) * (1 + 1 / 25) + [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, HitResult.SmallTickHit, 0)] // (0 * 1 * 300) * (1 + 0 / 25) + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, HitResult.SmallTickHit, 225)] // (((3 * 10) / (4 * 10)) * 1 * 300) * (1 + 0 / 25) + [TestCase(ScoringMode.Classic, HitResult.LargeTickMiss, HitResult.LargeTickHit, 0)] // (0 * 4 * 300) * (1 + 0 / 25) + [TestCase(ScoringMode.Classic, HitResult.LargeTickHit, HitResult.LargeTickHit, 936)] // (((3 * 50) / (4 * 50)) * 4 * 300) * (1 + 1 / 25) + [TestCase(ScoringMode.Classic, HitResult.SmallBonus, HitResult.SmallBonus, 30)] // (0 * 1 * 300) * (1 + 0 / 25) + 3 * 10 (bonus points) + [TestCase(ScoringMode.Classic, HitResult.LargeBonus, HitResult.LargeBonus, 150)] // (0 * 1 * 300) * (1 + 0 / 25) * 3 * 50 (bonus points) public void TestFourVariousResultsOneMiss(ScoringMode scoringMode, HitResult hitResult, HitResult maxResult, int expectedScore) { var minResult = new TestJudgement(hitResult).MinResult; @@ -121,10 +121,10 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5)); } - [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] - [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] - [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 214)] + [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000 + [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000 + [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25) + [TestCase(ScoringMode.Classic, HitResult.SmallTickMiss, 214)] // (((3 * 0 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25) public void TestSmallTicksAccuracy(ScoringMode scoringMode, HitResult hitResult, int expectedScore) { IEnumerable hitObjects = Enumerable From 82a28d4655277a6a061b5d09ce0b33afc1dd4317 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Oct 2020 17:34:20 +0200 Subject: [PATCH 3832/6909] Fix some inaccuracies --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 30b47d9bd7..0ca5101292 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -62,10 +62,10 @@ namespace osu.Game.Tests.Rulesets.Scoring /// The maximum achievable. /// Expected score after all objects have been judged, rounded to the nearest integer. /// - /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and exactly 50% max combo. + /// This test intentionally misses the 3rd hitobject to achieve lower than 75% accuracy and 50% max combo. /// /// For standardised scoring, is calculated using the following formula: - /// 1_000_000 * (((3 * ) / (4 * )) * 30% + 50% * 70%) + /// 1_000_000 * (((3 * ) / (4 * )) * 30% + (bestCombo / maxCombo) * 70%) /// /// /// For classic scoring, is calculated using the following formula: From 25d9b1ecd08ba1b467c18b3b2ce3c4f762230697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Oct 2020 17:36:07 +0200 Subject: [PATCH 3833/6909] Clarify purpose and construction of extra test --- osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs index 0ca5101292..9f16312121 100644 --- a/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs +++ b/osu.Game.Tests/Rulesets/Scoring/ScoreProcessorTest.cs @@ -121,6 +121,12 @@ namespace osu.Game.Tests.Rulesets.Scoring Assert.IsTrue(Precision.AlmostEquals(expectedScore, scoreProcessor.TotalScore.Value, 0.5)); } + /// + /// This test uses a beatmap with four small ticks and one object with the of . + /// Its goal is to ensure that with the of , + /// small ticks contribute to the accuracy portion, but not the combo portion. + /// In contrast, does not have separate combo and accuracy portion (they are multiplied by each other). + /// [TestCase(ScoringMode.Standardised, HitResult.SmallTickHit, 978_571)] // (3 * 10 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000 [TestCase(ScoringMode.Standardised, HitResult.SmallTickMiss, 914_286)] // (3 * 0 + 100) / (4 * 10 + 100) * 300_000 + (1 / 1) * 700_000 [TestCase(ScoringMode.Classic, HitResult.SmallTickHit, 279)] // (((3 * 10 + 100) / (4 * 10 + 100)) * 1 * 300) * (1 + 0 / 25) From 1a85123b890c8630459561c80095bba22e1df7de Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Mon, 12 Oct 2020 21:24:42 +0200 Subject: [PATCH 3834/6909] rename container class to be more descriptive --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 37541358b8..f74f40b9b4 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -284,7 +284,7 @@ namespace osu.Game.Overlays.Mods }, }, }, - ModSettingsContainer = new CModSettingsContainer + ModSettingsContainer = new MouseInputAbsorbingContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, @@ -496,17 +496,11 @@ namespace osu.Game.Overlays.Mods #endregion - protected class CModSettingsContainer : Container + protected class MouseInputAbsorbingContainer : Container { - protected override bool OnMouseDown(MouseDownEvent e) - { - return true; - } + protected override bool OnMouseDown(MouseDownEvent e) => true; - protected override bool OnHover(HoverEvent e) - { - return true; - } + protected override bool OnHover(HoverEvent e) => true; } } } From 779e6e10a7516f2d3f961af5f9f9a5ba81880c00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Oct 2020 21:43:14 +0200 Subject: [PATCH 3835/6909] Split ctors to avoid passing fields one by one --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index bdabe148a7..bbd204dede 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -227,7 +227,7 @@ namespace osu.Game.Beatmaps var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); var attributes = calculator.Calculate(key.Mods); - return difficultyCache[key] = new StarDifficulty(attributes.StarRating, attributes.MaxCombo, attributes); + return difficultyCache[key] = new StarDifficulty(attributes); } catch { @@ -322,14 +322,21 @@ namespace osu.Game.Beatmaps public readonly DifficultyAttributes Attributes; - public StarDifficulty(double stars, int maxCombo, DifficultyAttributes attributes = null) + public StarDifficulty([NotNull] DifficultyAttributes attributes) { - Stars = stars; - MaxCombo = maxCombo; + Stars = attributes.StarRating; + MaxCombo = attributes.MaxCombo; Attributes = attributes; // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) } + public StarDifficulty(double starDifficulty, int maxCombo) + { + Stars = starDifficulty; + MaxCombo = maxCombo; + Attributes = null; + } + public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(Stars); } } From 7117fd0fbaacaa1a7aa959bfa52fbc0ab5958f80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Oct 2020 21:44:04 +0200 Subject: [PATCH 3836/6909] Add xmldoc and nullability annotations --- osu.Game/Beatmaps/BeatmapDifficultyManager.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs index bbd204dede..3a21df8aeb 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyManager.cs @@ -317,11 +317,27 @@ namespace osu.Game.Beatmaps public readonly struct StarDifficulty { + /// + /// The star difficulty rating for the given beatmap. + /// public readonly double Stars; + + /// + /// The maximum combo achievable on the given beatmap. + /// public readonly int MaxCombo; + /// + /// The difficulty attributes computed for the given beatmap. + /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. + /// + [CanBeNull] public readonly DifficultyAttributes Attributes; + /// + /// Creates a structure based on computed + /// by a . + /// public StarDifficulty([NotNull] DifficultyAttributes attributes) { Stars = attributes.StarRating; @@ -330,6 +346,10 @@ namespace osu.Game.Beatmaps // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) } + /// + /// Creates a structure with a pre-populated star difficulty and max combo + /// in scenarios where computing is not feasible (i.e. when working with online sources). + /// public StarDifficulty(double starDifficulty, int maxCombo) { Stars = starDifficulty; From d4ba9d268254278bef604aa16e80e8cb901d8d27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Oct 2020 22:10:02 +0200 Subject: [PATCH 3837/6909] Simplify implementation of CalculatePerformanceAsync --- osu.Game/Scoring/ScorePerformanceManager.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index d8daf7189a..b7657c73c6 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -26,19 +26,14 @@ namespace osu.Game.Scoring /// An optional to cancel the operation. public async Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) { - if (tryGetExisting(score, out var perf, out var lookupKey)) - return perf; + var lookupKey = new PerformanceCacheLookup(score); + + if (performanceCache.TryGetValue(lookupKey, out double performance)) + return performance; return await computePerformanceAsync(score, lookupKey, token); } - private bool tryGetExisting(ScoreInfo score, out double performance, out PerformanceCacheLookup lookupKey) - { - lookupKey = new PerformanceCacheLookup(score); - - return performanceCache.TryGetValue(lookupKey, out performance); - } - private async Task computePerformanceAsync(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) { var attributes = await difficultyManager.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); From 68b505ab86df0f77bc0888c780189e6da0970122 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 12 Oct 2020 22:14:39 +0200 Subject: [PATCH 3838/6909] Extract helper function for pp value handling --- .../Expanded/Statistics/PerformanceStatistic.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 3976682241..1b4edb99d7 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -28,19 +28,21 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { if (score.PP.HasValue) { - performance.Value = (int)Math.Round(score.PP.Value, MidpointRounding.AwayFromZero); + setPerformanceValue(score.PP.Value); } else { performanceManager.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => - { - if (t.Result.HasValue) - performance.Value = (int)Math.Round(t.Result.Value, MidpointRounding.AwayFromZero); - }), cancellationTokenSource.Token); + .ContinueWith(t => Schedule(() => setPerformanceValue(t.Result)), cancellationTokenSource.Token); } } + private void setPerformanceValue(double? pp) + { + if (pp.HasValue) + performance.Value = (int)Math.Round(pp.Value, MidpointRounding.AwayFromZero); + } + public override void Appear() { base.Appear(); From 7e709349b829a98660fd6dd839c5c10932c4ee50 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 06:26:14 +0900 Subject: [PATCH 3839/6909] Use already available test ruleset --- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index ce73e9061b..9ef9649f77 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -19,7 +19,6 @@ 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; @@ -194,8 +193,8 @@ namespace osu.Game.Tests.Visual.Background AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo { User = new User { Username = "osu!" }, - Beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo, - Ruleset = new OsuRuleset().RulesetInfo, + Beatmap = new TestBeatmap(Ruleset.Value).BeatmapInfo, + Ruleset = Ruleset.Value, }))); AddUntilStep("Wait for results is current", () => results.IsCurrentScreen()); From 41d82e3e8ab5377955e7b978e749de7f52fc82ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 17:34:11 +0900 Subject: [PATCH 3840/6909] 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 3df894fbcc..c3c755ecd7 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 8b10f0a7f7..e8498129a1 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 88abbca73d..4b38d9f22d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From b08e7ce1f52743307e4c78c3feafc298b0c4dfc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 17:37:24 +0900 Subject: [PATCH 3841/6909] Update BaseColour specification --- 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 be3bca3242..9aff4ddf8f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveform = new WaveformGraph { RelativeSizeAxes = Axes.Both, - Colour = colours.Blue.Opacity(0.2f), + BaseColour = colours.Blue.Opacity(0.2f), LowColour = colours.BlueLighter, MidColour = colours.BlueDark, HighColour = colours.BlueDarker, From a393bbe8f7c905997d5e0c5717609d31848e4b99 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 12:37:32 +0900 Subject: [PATCH 3842/6909] Remove direct drawable storage from carousel models --- .../Select/Carousel/CarouselBeatmap.cs | 2 +- .../Select/Carousel/CarouselBeatmapSet.cs | 2 +- .../Screens/Select/Carousel/CarouselGroup.cs | 18 +-------------- .../Screens/Select/Carousel/CarouselItem.cs | 23 ++----------------- 4 files changed, 5 insertions(+), 40 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 83e3c84f39..6a18e2d6a3 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Select.Carousel State.Value = CarouselItemState.Collapsed; } - protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmap(this); + public override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmap(this); public override void Filter(FilterCriteria criteria) { diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 92ccfde14b..75d40a6b03 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Select.Carousel .ForEach(AddChild); } - protected override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this); + public override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this); protected override CarouselItem GetNextToSelect() { diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs index aa48d1a04e..b85e868b89 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroup.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroup.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.Select.Carousel /// public class CarouselGroup : CarouselItem { - protected override DrawableCarouselItem CreateDrawableRepresentation() => null; + public override DrawableCarouselItem CreateDrawableRepresentation() => null; public IReadOnlyList Children => InternalChildren; @@ -23,22 +23,6 @@ namespace osu.Game.Screens.Select.Carousel /// private ulong currentChildID; - public override List Drawables - { - get - { - var drawables = base.Drawables; - - // if we are explicitly not present, don't ever present children. - // without this check, children drawables can potentially be presented without their group header. - if (DrawableRepresentation.Value?.IsPresent == false) return drawables; - - foreach (var c in InternalChildren) - drawables.AddRange(c.Drawables); - return drawables; - } - } - public virtual void RemoveChild(CarouselItem i) { InternalChildren.Remove(i); diff --git a/osu.Game/Screens/Select/Carousel/CarouselItem.cs b/osu.Game/Screens/Select/Carousel/CarouselItem.cs index 79c1a4cb6b..555c32c041 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselItem.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; namespace osu.Game.Screens.Select.Carousel @@ -18,23 +16,8 @@ namespace osu.Game.Screens.Select.Carousel /// public bool Visible => State.Value != CarouselItemState.Collapsed && !Filtered.Value; - public virtual List Drawables - { - get - { - var items = new List(); - - var self = DrawableRepresentation.Value; - if (self?.IsPresent == true) items.Add(self); - - return items; - } - } - protected CarouselItem() { - DrawableRepresentation = new Lazy(CreateDrawableRepresentation); - Filtered.ValueChanged += filtered => { if (filtered.NewValue && State.Value == CarouselItemState.Selected) @@ -42,17 +25,15 @@ namespace osu.Game.Screens.Select.Carousel }; } - protected readonly Lazy DrawableRepresentation; - /// /// Used as a default sort method for s of differing types. /// internal ulong ChildID; /// - /// Create a fresh drawable version of this item. If you wish to consume the current representation, use instead. + /// Create a fresh drawable version of this item. /// - protected abstract DrawableCarouselItem CreateDrawableRepresentation(); + public abstract DrawableCarouselItem CreateDrawableRepresentation(); public virtual void Filter(FilterCriteria criteria) { From 9193f5b0ba5b7208818898612e4fb2cefaeef379 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 12:37:41 +0900 Subject: [PATCH 3843/6909] Expose panel height from non-drawable models --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 2 ++ osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 2 ++ osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 ++ 3 files changed, 6 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 75d40a6b03..44b8e72d51 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -12,6 +12,8 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselBeatmapSet : CarouselGroupEagerSelect { + public float TotalHeight => DrawableCarouselBeatmapSet.HEIGHT + BeatmapSet.Beatmaps.Count * DrawableCarouselBeatmap.HEIGHT; + public IEnumerable Beatmaps => InternalChildren.OfType(); public BeatmapSetInfo BeatmapSet; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 10745fe3c1..c8f9507b91 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -31,6 +31,8 @@ namespace osu.Game.Screens.Select.Carousel { public class DrawableCarouselBeatmap : DrawableCarouselItem, IHasContextMenu { + public const float HEIGHT = MAX_HEIGHT; + private readonly BeatmapInfo beatmap; private Sprite background; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 3c8ac69dd2..8332093a3c 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -29,6 +29,8 @@ namespace osu.Game.Screens.Select.Carousel { public class DrawableCarouselBeatmapSet : DrawableCarouselItem, IHasContextMenu { + public const float HEIGHT = MAX_HEIGHT; + private Action restoreHiddenRequested; private Action viewDetails; From 3143224e5b5b51f939c6130dd026de24bb3e96db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 14:23:18 +0900 Subject: [PATCH 3844/6909] Refactor how drawable carousel items are constructed --- .../SongSelect/TestSceneBeatmapCarousel.cs | 29 +-- osu.Game/Screens/Select/BeatmapCarousel.cs | 187 ++++++++---------- .../Select/Carousel/CarouselBeatmap.cs | 2 + .../Select/Carousel/CarouselBeatmapSet.cs | 15 +- .../Screens/Select/Carousel/CarouselItem.cs | 2 + .../Carousel/DrawableCarouselBeatmap.cs | 6 +- .../Carousel/DrawableCarouselBeatmapSet.cs | 43 ++++ .../Select/Carousel/DrawableCarouselItem.cs | 35 ++-- 8 files changed, 177 insertions(+), 142 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 3aff390a47..680928b331 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -709,19 +709,20 @@ namespace osu.Game.Tests.Visual.SongSelect private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null) { - createCarousel(carouselAdjust); - - if (beatmapSets == null) - { - beatmapSets = new List(); - - for (int i = 1; i <= set_count; i++) - beatmapSets.Add(createTestBeatmapSet(i)); - } - bool changed = false; - AddStep($"Load {(beatmapSets.Count > 0 ? beatmapSets.Count.ToString() : "some")} beatmaps", () => + + createCarousel(c => { + carouselAdjust?.Invoke(c); + + if (beatmapSets == null) + { + beatmapSets = new List(); + + for (int i = 1; i <= set_count; i++) + beatmapSets.Add(createTestBeatmapSet(i)); + } + carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria()); carousel.BeatmapSetsChanged = () => changed = true; carousel.BeatmapSets = beatmapSets; @@ -807,7 +808,7 @@ namespace osu.Game.Tests.Visual.SongSelect private bool selectedBeatmapVisible() { - var currentlySelected = carousel.Items.Find(s => s.Item is CarouselBeatmap && s.Item.State.Value == CarouselItemState.Selected); + var currentlySelected = carousel.Items.FirstOrDefault(s => s.Item is CarouselBeatmap && s.Item.State.Value == CarouselItemState.Selected); if (currentlySelected == null) return true; @@ -908,10 +909,10 @@ namespace osu.Game.Tests.Visual.SongSelect private class TestBeatmapCarousel : BeatmapCarousel { - public new List Items => base.Items; - public bool PendingFilterTask => PendingFilter != null; + public IEnumerable Items => InternalChildren.OfType(); + protected override IEnumerable GetLoadableBeatmaps() => Enumerable.Empty(); } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 5f6f859d66..ef533a1456 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -96,9 +96,6 @@ namespace osu.Game.Screens.Select beatmapSets.Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild); - // preload drawables as the ctor overhead is quite high currently. - _ = newRoot.Drawables; - root = newRoot; if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) selectedBeatmapSet = null; @@ -119,6 +116,8 @@ namespace osu.Game.Screens.Select } private readonly List yPositions = new List(); + private readonly List visibleItems = new List(); + private readonly Cached itemsCache = new Cached(); private readonly Cached scrollPositionCache = new Cached(); @@ -130,8 +129,6 @@ namespace osu.Game.Screens.Select private readonly List previouslyVisitedRandomSets = new List(); private readonly Stack randomSelectedBeatmaps = new Stack(); - protected List Items = new List(); - private CarouselRoot root; private IBindable> itemUpdated; @@ -178,7 +175,8 @@ namespace osu.Game.Screens.Select itemRestored = beatmaps.BeatmapRestored.GetBoundCopy(); itemRestored.BindValueChanged(beatmapRestored); - loadBeatmapSets(GetLoadableBeatmaps()); + if (!beatmapSets.Any()) + loadBeatmapSets(GetLoadableBeatmaps()); } protected virtual IEnumerable GetLoadableBeatmaps() => beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.AllButFiles); @@ -558,71 +556,78 @@ namespace osu.Game.Screens.Select { base.Update(); + //todo: this should only refresh items, not everything here if (!itemsCache.IsValid) + { updateItems(); - // Remove all items that should no longer be on-screen - scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent); + // Remove all items that should no longer be on-screen + scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent); - // Find index range of all items that should be on-screen - Trace.Assert(Items.Count == yPositions.Count); + // Find index range of all items that should be on-screen + int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT); + if (firstIndex < 0) firstIndex = ~firstIndex; + int lastIndex = yPositions.BinarySearch(visibleBottomBound); + if (lastIndex < 0) lastIndex = ~lastIndex; - int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT); - if (firstIndex < 0) firstIndex = ~firstIndex; - int lastIndex = yPositions.BinarySearch(visibleBottomBound); - if (lastIndex < 0) lastIndex = ~lastIndex; + scrollableContent.Clear(); - int notVisibleCount = 0; - - // Add those items within the previously found index range that should be displayed. - for (int i = firstIndex; i < lastIndex; ++i) - { - DrawableCarouselItem item = Items[i]; - - if (!item.Item.Visible) + // Add those items within the previously found index range that should be displayed. + for (int i = firstIndex; i < lastIndex; ++i) { - if (!item.IsPresent) - notVisibleCount++; - continue; - } + DrawableCarouselItem item = visibleItems[i].CreateDrawableRepresentation(); - float depth = i + (item is DrawableCarouselBeatmapSet ? -Items.Count : 0); + item.Y = yPositions[i]; + item.Depth = i; - // Only add if we're not already part of the content. - if (!scrollableContent.Contains(item)) - { - // Makes sure headers are always _below_ items, - // and depth flows downward. - item.Depth = depth; + scrollableContent.Add(item); - switch (item.LoadState) + // if (!item.Item.Visible) + // { + // if (!item.IsPresent) + // notVisibleCount++; + // continue; + // } + + // Only add if we're not already part of the content. + /* + if (!scrollableContent.Contains(item)) { - case LoadState.NotLoaded: - LoadComponentAsync(item); - break; + // Makes sure headers are always _below_ items, + // and depth flows downward. + item.Depth = depth; - case LoadState.Loading: - break; + switch (item.LoadState) + { + case LoadState.NotLoaded: + LoadComponentAsync(item); + break; - default: - scrollableContent.Add(item); - break; + case LoadState.Loading: + break; + + default: + scrollableContent.Add(item); + break; + } } - } - else - { - scrollableContent.ChangeChildDepth(item, depth); + else + { + scrollableContent.ChangeChildDepth(item, depth); + } + */ } } - // this is not actually useful right now, but once we have groups may well be. - if (notVisibleCount > 50) - itemsCache.Invalidate(); - // Update externally controlled state of currently visible items // (e.g. x-offset and opacity). foreach (DrawableCarouselItem p in scrollableContent.Children) + { updateItem(p); + + // foreach (var pChild in p.ChildItems) + // updateItem(pChild, p); + } } protected override void UpdateAfterChildren() @@ -633,15 +638,6 @@ namespace osu.Game.Screens.Select updateScrollPosition(); } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - // aggressively dispose "off-screen" items to reduce GC pressure. - foreach (var i in Items) - i.Dispose(); - } - private void beatmapRemoved(ValueChangedEvent> weakItem) { if (weakItem.NewValue.TryGetTarget(out var item)) @@ -704,69 +700,39 @@ namespace osu.Game.Screens.Select /// The Y position of the currently selected item. private void updateItems() { - Items = root.Drawables.ToList(); - yPositions.Clear(); + visibleItems.Clear(); float currentY = visibleHalfHeight; - DrawableCarouselBeatmapSet lastSet = null; scrollTarget = null; - foreach (DrawableCarouselItem d in Items) + foreach (CarouselItem item in root.Children) { - if (d.IsPresent) + if (item.Filtered.Value) + continue; + + switch (item) { - switch (d) + case CarouselBeatmapSet set: { - case DrawableCarouselBeatmapSet set: - { - lastSet = set; + visibleItems.Add(set); + yPositions.Add(currentY); + //lastSet = set; - set.MoveToX(set.Item.State.Value == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo); - set.MoveToY(currentY, 750, Easing.OutExpo); - break; - } - - 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'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 - visibleHalfHeight + BleedTop + beatmap.DrawHeight / 2; - - void performMove(float y, float? startY = null) - { - if (startY != null) beatmap.MoveTo(new Vector2(0, startY.Value)); - beatmap.MoveToX(beatmap.Item.State.Value == CarouselItemState.Selected ? -50 : 0, 500, Easing.OutExpo); - beatmap.MoveToY(y, 750, Easing.OutExpo); - } - - Debug.Assert(lastSet != null); - - float? setY = null; - if (!d.IsLoaded || beatmap.Alpha == 0) // can't use IsPresent due to DrawableCarouselItem override. - setY = lastSet.Y + lastSet.DrawHeight + 5; - - if (d.IsLoaded) - performMove(currentY, setY); - else - { - float y = currentY; - d.OnLoadComplete += _ => performMove(y, setY); - } - - break; - } + // TODO: move this logic to DCBS too. + // set.MoveToX(set.Item.State.Value == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo); + // set.MoveToY(currentY, 750, Easing.OutExpo); + currentY += set.TotalHeight; + break; } + + default: + continue; + // + // break; + // } } - - yPositions.Add(currentY); - - if (d.Item.Visible) - currentY += d.DrawHeight + 5; } currentY += visibleHalfHeight; @@ -869,6 +835,7 @@ namespace osu.Game.Screens.Select /// public bool UserScrolling { get; private set; } + // ReSharper disable once OptionalParameterHierarchyMismatch fuck off rider protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { UserScrolling = true; diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 6a18e2d6a3..dce4028f17 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -10,6 +10,8 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselBeatmap : CarouselItem { + public override float TotalHeight => DrawableCarouselBeatmap.HEIGHT; + public readonly BeatmapInfo Beatmap; public CarouselBeatmap(BeatmapInfo beatmap) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 44b8e72d51..e34710f71d 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -12,7 +12,20 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselBeatmapSet : CarouselGroupEagerSelect { - public float TotalHeight => DrawableCarouselBeatmapSet.HEIGHT + BeatmapSet.Beatmaps.Count * DrawableCarouselBeatmap.HEIGHT; + public override float TotalHeight + { + get + { + switch (State.Value) + { + case CarouselItemState.Selected: + return DrawableCarouselBeatmapSet.HEIGHT + Children.Count * DrawableCarouselBeatmap.HEIGHT; + + default: + return DrawableCarouselBeatmapSet.HEIGHT; + } + } + } public IEnumerable Beatmaps => InternalChildren.OfType(); diff --git a/osu.Game/Screens/Select/Carousel/CarouselItem.cs b/osu.Game/Screens/Select/Carousel/CarouselItem.cs index 555c32c041..004d9779ef 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselItem.cs @@ -7,6 +7,8 @@ namespace osu.Game.Screens.Select.Carousel { public abstract class CarouselItem { + public virtual float TotalHeight => 0; + public readonly BindableBool Filtered = new BindableBool(); public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index c8f9507b91..7d69251265 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Select.Carousel { public class DrawableCarouselBeatmap : DrawableCarouselItem, IHasContextMenu { - public const float HEIGHT = MAX_HEIGHT; + public const float HEIGHT = MAX_HEIGHT * 0.6f; private readonly BeatmapInfo beatmap; @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Select.Carousel : base(panel) { beatmap = panel.Beatmap; - Height *= 0.60f; + Height = HEIGHT; } [BackgroundDependencyLoader(true)] @@ -170,6 +170,8 @@ namespace osu.Game.Screens.Select.Carousel { base.Selected(); + BorderContainer.MoveToX(Item.State.Value == CarouselItemState.Selected ? -50 : 0, 500, Easing.OutExpo); + background.Colour = ColourInfo.GradientVertical( new Color4(20, 43, 51, 255), new Color4(40, 86, 102, 255)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 8332093a3c..aaa925c6f8 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -43,8 +43,13 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } + public override IEnumerable ChildItems => beatmapContainer?.Children ?? base.ChildItems; + private readonly BeatmapSetInfo beatmapSet; + private Container beatmapContainer; + private Bindable beatmapSetState; + public DrawableCarouselBeatmapSet(CarouselBeatmapSet set) : base(set) { @@ -119,6 +124,44 @@ namespace osu.Game.Screens.Select.Carousel } } }; + + // TODO: temporary. we probably want to *not* inherit DrawableCarouselItem for this class, but only the above header portion. + AddRangeInternal(new Drawable[] + { + beatmapContainer = new Container + { + X = 50, + Y = MAX_HEIGHT, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }); + + beatmapSetState = Item.State.GetBoundCopy(); + beatmapSetState.BindValueChanged(setSelected, true); + } + + private void setSelected(ValueChangedEvent obj) + { + switch (obj.NewValue) + { + default: + beatmapContainer.Clear(); + break; + + case CarouselItemState.Selected: + + float yPos = 0; + + foreach (var item in ((CarouselBeatmapSet)Item).Beatmaps.Select(b => b.CreateDrawableRepresentation()).OfType()) + { + item.Y = yPos; + beatmapContainer.Add(item); + yPos += item.Item.TotalHeight; + } + + break; + } } private const int maximum_difficulty_icons = 18; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 121491d6ca..c0405b373d 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.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 System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -27,10 +29,13 @@ namespace osu.Game.Screens.Select.Carousel public readonly CarouselItem Item; - private Container nestedContainer; - private Container borderContainer; + public virtual IEnumerable ChildItems => Enumerable.Empty(); - private Box hoverLayer; + private readonly Container nestedContainer; + + protected readonly Container BorderContainer; + + private readonly Box hoverLayer; protected override Container Content => nestedContainer; @@ -41,14 +46,8 @@ namespace osu.Game.Screens.Select.Carousel Height = MAX_HEIGHT; RelativeSizeAxes = Axes.X; Alpha = 0; - } - private SampleChannel sampleHover; - - [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuColour colours) - { - InternalChild = borderContainer = new Container + InternalChild = BorderContainer = new Container { RelativeSizeAxes = Axes.Both, Masking = true, @@ -68,7 +67,13 @@ namespace osu.Game.Screens.Select.Carousel }, } }; + } + private SampleChannel sampleHover; + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}"); hoverLayer.Colour = colours.Blue.Opacity(0.1f); } @@ -87,7 +92,7 @@ namespace osu.Game.Screens.Select.Carousel base.OnHoverLost(e); } - public void SetMultiplicativeAlpha(float alpha) => borderContainer.Alpha = alpha; + public void SetMultiplicativeAlpha(float alpha) => BorderContainer.Alpha = alpha; protected override void LoadComplete() { @@ -123,8 +128,8 @@ namespace osu.Game.Screens.Select.Carousel { Item.State.Value = CarouselItemState.Selected; - borderContainer.BorderThickness = 2.5f; - borderContainer.EdgeEffect = new EdgeEffectParameters + BorderContainer.BorderThickness = 2.5f; + BorderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = new Color4(130, 204, 255, 150), @@ -137,8 +142,8 @@ namespace osu.Game.Screens.Select.Carousel { Item.State.Value = CarouselItemState.NotSelected; - borderContainer.BorderThickness = 0; - borderContainer.EdgeEffect = new EdgeEffectParameters + BorderContainer.BorderThickness = 0; + BorderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Shadow, Offset = new Vector2(1), From f17d661c1a1e1aa67865cb09e5e9e492cfbaa448 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 14:46:51 +0900 Subject: [PATCH 3845/6909] Add basic range-based invalidation --- osu.Game/Screens/Select/BeatmapCarousel.cs | 70 +++++++--------------- 1 file changed, 22 insertions(+), 48 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index ef533a1456..94ba0caefb 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -552,70 +552,44 @@ namespace osu.Game.Screens.Select #endregion + private (int first, int last) displayedRange; + protected override void Update() { base.Update(); + bool revalidateItems = !itemsCache.IsValid; + //todo: this should only refresh items, not everything here - if (!itemsCache.IsValid) - { + if (revalidateItems) updateItems(); - // Remove all items that should no longer be on-screen - scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent); + // Remove all items that should no longer be on-screen + scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent); - // Find index range of all items that should be on-screen - int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT); - if (firstIndex < 0) firstIndex = ~firstIndex; - int lastIndex = yPositions.BinarySearch(visibleBottomBound); - if (lastIndex < 0) lastIndex = ~lastIndex; + // Find index range of all items that should be on-screen + int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT); + if (firstIndex < 0) firstIndex = ~firstIndex; + int lastIndex = yPositions.BinarySearch(visibleBottomBound); + if (lastIndex < 0) lastIndex = ~lastIndex; - scrollableContent.Clear(); + if (revalidateItems || firstIndex != displayedRange.first || lastIndex != displayedRange.last) + { + displayedRange = (firstIndex, lastIndex); // Add those items within the previously found index range that should be displayed. for (int i = firstIndex; i < lastIndex; ++i) { - DrawableCarouselItem item = visibleItems[i].CreateDrawableRepresentation(); + var panel = scrollableContent.FirstOrDefault(c => c.Item == visibleItems[i]); - item.Y = yPositions[i]; - item.Depth = i; - - scrollableContent.Add(item); - - // if (!item.Item.Visible) - // { - // if (!item.IsPresent) - // notVisibleCount++; - // continue; - // } - - // Only add if we're not already part of the content. - /* - if (!scrollableContent.Contains(item)) + if (panel == null) { - // Makes sure headers are always _below_ items, - // and depth flows downward. - item.Depth = depth; - - switch (item.LoadState) - { - case LoadState.NotLoaded: - LoadComponentAsync(item); - break; - - case LoadState.Loading: - break; - - default: - scrollableContent.Add(item); - break; - } + panel = visibleItems[i].CreateDrawableRepresentation(); + scrollableContent.Add(panel); } - else - { - scrollableContent.ChangeChildDepth(item, depth); - } - */ + + panel.Y = yPositions[i]; + scrollableContent.ChangeChildDepth(panel, i); } } From 0a978c6131352794a60ea3c00db373c59d0144af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 15:36:03 +0900 Subject: [PATCH 3846/6909] Add basic pooling setup --- osu.Game/Screens/Select/BeatmapCarousel.cs | 17 ++++-- .../Select/Carousel/CarouselBeatmapSet.cs | 2 - .../Carousel/DrawableCarouselBeatmap.cs | 6 +- .../Carousel/DrawableCarouselBeatmapSet.cs | 55 ++++++++++-------- .../Select/Carousel/DrawableCarouselItem.cs | 58 +++++++++++++++---- 5 files changed, 95 insertions(+), 43 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 94ba0caefb..f5b524e57c 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -16,6 +16,7 @@ using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Threading; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Beatmaps; @@ -136,6 +137,8 @@ namespace osu.Game.Screens.Select private IBindable> itemHidden; private IBindable> itemRestored; + private readonly DrawablePool setPool = new DrawablePool(100); + public BeatmapCarousel() { root = new CarouselRoot(this); @@ -146,9 +149,13 @@ namespace osu.Game.Screens.Select { Masking = false, RelativeSizeAxes = Axes.Both, - Child = scrollableContent = new Container + Children = new Drawable[] { - RelativeSizeAxes = Axes.X, + setPool, + scrollableContent = new Container + { + RelativeSizeAxes = Axes.X, + } } } }; @@ -580,11 +587,13 @@ namespace osu.Game.Screens.Select // Add those items within the previously found index range that should be displayed. for (int i = firstIndex; i < lastIndex; ++i) { - var panel = scrollableContent.FirstOrDefault(c => c.Item == visibleItems[i]); + var item = visibleItems[i]; + + var panel = scrollableContent.FirstOrDefault(c => c.Item == item); if (panel == null) { - panel = visibleItems[i].CreateDrawableRepresentation(); + panel = setPool.Get(p => p.Item = item); scrollableContent.Add(panel); } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index e34710f71d..15f622f3c4 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -43,8 +43,6 @@ namespace osu.Game.Screens.Select.Carousel .ForEach(AddChild); } - public override DrawableCarouselItem CreateDrawableRepresentation() => new DrawableCarouselBeatmapSet(this); - protected override CarouselItem GetNextToSelect() { if (LastSelected == null) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 7d69251265..4135960e06 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -60,10 +60,12 @@ namespace osu.Game.Screens.Select.Carousel private CancellationTokenSource starDifficultyCancellationSource; public DrawableCarouselBeatmap(CarouselBeatmap panel) - : base(panel) { beatmap = panel.Beatmap; Height = HEIGHT; + + // todo: temporary + Item = panel; } [BackgroundDependencyLoader(true)] @@ -79,7 +81,7 @@ namespace osu.Game.Screens.Select.Carousel if (manager != null) hideRequested = manager.Hide; - Children = new Drawable[] + Content.Children = new Drawable[] { background = new Box { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index aaa925c6f8..0bfec4f509 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -45,26 +45,40 @@ namespace osu.Game.Screens.Select.Carousel public override IEnumerable ChildItems => beatmapContainer?.Children ?? base.ChildItems; - private readonly BeatmapSetInfo beatmapSet; + private BeatmapSetInfo beatmapSet => (Item as CarouselBeatmapSet)?.BeatmapSet; private Container beatmapContainer; private Bindable beatmapSetState; - public DrawableCarouselBeatmapSet(CarouselBeatmapSet set) - : base(set) - { - beatmapSet = set.BeatmapSet; - } + [Resolved] + private BeatmapManager manager { get; set; } [BackgroundDependencyLoader(true)] - private void load(BeatmapManager manager, BeatmapSetOverlay beatmapOverlay) + private void load(BeatmapSetOverlay beatmapOverlay) { restoreHiddenRequested = s => s.Beatmaps.ForEach(manager.Restore); if (beatmapOverlay != null) viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; - Children = new Drawable[] + // TODO: temporary. we probably want to *not* inherit DrawableCarouselItem for this class, but only the above header portion. + AddRangeInternal(new Drawable[] + { + beatmapContainer = new Container + { + X = 50, + Y = MAX_HEIGHT, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + }); + } + + protected override void UpdateItem() + { + base.UpdateItem(); + + Content.Children = new Drawable[] { new DelayedLoadUnloadWrapper(() => { @@ -125,17 +139,7 @@ namespace osu.Game.Screens.Select.Carousel } }; - // TODO: temporary. we probably want to *not* inherit DrawableCarouselItem for this class, but only the above header portion. - AddRangeInternal(new Drawable[] - { - beatmapContainer = new Container - { - X = 50, - Y = MAX_HEIGHT, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - }); + beatmapContainer.Clear(); beatmapSetState = Item.State.GetBoundCopy(); beatmapSetState.BindValueChanged(setSelected, true); @@ -153,11 +157,16 @@ namespace osu.Game.Screens.Select.Carousel float yPos = 0; - foreach (var item in ((CarouselBeatmapSet)Item).Beatmaps.Select(b => b.CreateDrawableRepresentation()).OfType()) + var carouselBeatmapSet = (CarouselBeatmapSet)Item; + + foreach (var item in carouselBeatmapSet.Children) { - item.Y = yPos; - beatmapContainer.Add(item); - yPos += item.Item.TotalHeight; + var beatmapPanel = item.CreateDrawableRepresentation(); + + beatmapPanel.Y = yPos; + yPos += item.TotalHeight; + + beatmapContainer.Add((DrawableCarouselBeatmap)beatmapPanel); } break; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index c0405b373d..4c4932c22a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -2,14 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +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.Pooling; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; @@ -19,15 +22,32 @@ using osuTK.Graphics; namespace osu.Game.Screens.Select.Carousel { - public abstract class DrawableCarouselItem : Container + public abstract class DrawableCarouselItem : PoolableDrawable { public const float MAX_HEIGHT = 80; - public override bool RemoveWhenNotAlive => false; + public override bool IsPresent => base.IsPresent || Item?.Visible == true; - public override bool IsPresent => base.IsPresent || Item.Visible; + public CarouselItem Item + { + get => item; + set + { + if (item == value) + return; - public readonly CarouselItem Item; + if (item != null) + { + Item.Filtered.ValueChanged -= onStateChange; + Item.State.ValueChanged -= onStateChange; + } + + item = value; + + if (IsLoaded) + UpdateItem(); + } + } public virtual IEnumerable ChildItems => Enumerable.Empty(); @@ -37,12 +57,10 @@ namespace osu.Game.Screens.Select.Carousel private readonly Box hoverLayer; - protected override Container Content => nestedContainer; + protected Container Content => nestedContainer; - protected DrawableCarouselItem(CarouselItem item) + protected DrawableCarouselItem() { - Item = item; - Height = MAX_HEIGHT; RelativeSizeAxes = Axes.X; Alpha = 0; @@ -70,6 +88,7 @@ namespace osu.Game.Screens.Select.Carousel } private SampleChannel sampleHover; + private CarouselItem item; [BackgroundDependencyLoader] private void load(AudioManager audio, OsuColour colours) @@ -98,14 +117,27 @@ namespace osu.Game.Screens.Select.Carousel { base.LoadComplete(); - ApplyState(); - Item.Filtered.ValueChanged += _ => Schedule(ApplyState); - Item.State.ValueChanged += _ => Schedule(ApplyState); + UpdateItem(); } + protected virtual void UpdateItem() + { + if (item == null) + return; + + ApplyState(); + + Item.Filtered.ValueChanged += onStateChange; + Item.State.ValueChanged += onStateChange; + } + + private void onStateChange(ValueChangedEvent obj) => Schedule(ApplyState); + + private void onStateChange(ValueChangedEvent _) => Schedule(ApplyState); + protected virtual void ApplyState() { - if (!IsLoaded) return; + Debug.Assert(Item != null); switch (Item.State.Value) { @@ -126,6 +158,8 @@ namespace osu.Game.Screens.Select.Carousel protected virtual void Selected() { + Debug.Assert(Item != null); + Item.State.Value = CarouselItemState.Selected; BorderContainer.BorderThickness = 2.5f; From f3b24b9bb5bd9a5fa9cf28e5c190a565a8ce9df3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 15:36:26 +0900 Subject: [PATCH 3847/6909] Avoid performing eager selection constantly on adding ranges of new children --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- .../Screens/Select/Carousel/CarouselGroupEagerSelect.cs | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index f5b524e57c..a8212f7e59 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -95,7 +95,7 @@ namespace osu.Game.Screens.Select { CarouselRoot newRoot = new CarouselRoot(this); - beatmapSets.Select(createCarouselSet).Where(g => g != null).ForEach(newRoot.AddChild); + newRoot.AddChildren(beatmapSets.Select(createCarouselSet).Where(g => g != null)); root = newRoot; if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) diff --git a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs index 262bea9c71..9e8aad4b6f 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselGroupEagerSelect.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; namespace osu.Game.Screens.Select.Carousel @@ -54,6 +55,14 @@ namespace osu.Game.Screens.Select.Carousel updateSelectedIndex(); } + public void AddChildren(IEnumerable items) + { + foreach (var i in items) + base.AddChild(i); + + attemptSelection(); + } + public override void AddChild(CarouselItem i) { base.AddChild(i); From 580ea62710810d80a92059fced01560b7085ee26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 15:36:36 +0900 Subject: [PATCH 3848/6909] Temporarily increase test beatmap count for perf testing --- .../SongSelect/TestSceneBeatmapCarousel.cs | 54 ++++++++----------- 1 file changed, 21 insertions(+), 33 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 680928b331..7d850a0d13 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets; @@ -835,42 +836,29 @@ namespace osu.Game.Tests.Visual.SongSelect Title = $"test set #{id}!", AuthorString = string.Concat(Enumerable.Repeat((char)('z' - Math.Min(25, id - 1)), 5)) }, - Beatmaps = new List(new[] - { - new BeatmapInfo - { - OnlineBeatmapID = id * 10, - Version = "Normal", - StarDifficulty = 2, - BaseDifficulty = new BeatmapDifficulty - { - OverallDifficulty = 3.5f, - } - }, - new BeatmapInfo - { - OnlineBeatmapID = id * 10 + 1, - Version = "Hard", - StarDifficulty = 5, - BaseDifficulty = new BeatmapDifficulty - { - OverallDifficulty = 5, - } - }, - new BeatmapInfo - { - OnlineBeatmapID = id * 10 + 2, - Version = "Insane", - StarDifficulty = 6, - BaseDifficulty = new BeatmapDifficulty - { - OverallDifficulty = 7, - } - }, - }), + Beatmaps = getBeatmaps(RNG.Next(1, 20)).ToList() }; } + private IEnumerable getBeatmaps(int count) + { + int id = 0; + + for (int i = 0; i < count; i++) + { + yield return new BeatmapInfo + { + OnlineBeatmapID = id++ * 10, + Version = "Normal", + StarDifficulty = RNG.NextSingle() * 10, + BaseDifficulty = new BeatmapDifficulty + { + OverallDifficulty = RNG.NextSingle() * 10, + } + }; + } + } + private BeatmapSetInfo createTestBeatmapSetWithManyDifficulties(int id) { var toReturn = new BeatmapSetInfo From 0400b34349dd805750f78a8ac7b326c0a8299b88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 15:55:47 +0900 Subject: [PATCH 3849/6909] Load more components asynchronously after pool resolution --- osu.Game/Screens/Select/BeatmapCarousel.cs | 1 - .../Carousel/DrawableCarouselBeatmapSet.cs | 129 ++++++++++-------- 2 files changed, 70 insertions(+), 60 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index a8212f7e59..00f62aa515 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -15,7 +15,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Threading; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 0bfec4f509..6717788506 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.Select.Carousel private BeatmapSetInfo beatmapSet => (Item as CarouselBeatmapSet)?.BeatmapSet; - private Container beatmapContainer; + private Container beatmapContainer; private Bindable beatmapSetState; [Resolved] @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Select.Carousel // TODO: temporary. we probably want to *not* inherit DrawableCarouselItem for this class, but only the above header portion. AddRangeInternal(new Drawable[] { - beatmapContainer = new Container + beatmapContainer = new Container { X = 50, Y = MAX_HEIGHT, @@ -81,62 +81,68 @@ namespace osu.Game.Screens.Select.Carousel Content.Children = new Drawable[] { new DelayedLoadUnloadWrapper(() => - { - var background = new PanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) - { - RelativeSizeAxes = Axes.Both, - }; - - background.OnLoadComplete += d => d.FadeInFromZero(1000, Easing.OutQuint); - - return background; - }, 300, 5000 - ), - new FillFlowContainer { - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 5, Left = 18, Right = 10, Bottom = 10 }, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + var background = new PanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { - new OsuSpriteText + RelativeSizeAxes = Axes.Both, + }; + + background.OnLoadComplete += d => d.FadeInFromZero(1000, Easing.OutQuint); + + return background; + }, 300, 5000), + new DelayedLoadUnloadWrapper(() => + { + var mainFlow = new FillFlowContainer + { + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 5, Left = 18, Right = 10, Bottom = 10 }, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] { - Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)), - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), - Shadow = true, - }, - new OsuSpriteText - { - Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)), - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - Shadow = true, - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5 }, - Children = new Drawable[] + new OsuSpriteText { - new BeatmapSetOnlineStatusPill + Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)), + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + new OsuSpriteText + { + Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5 }, + Children = new Drawable[] { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5 }, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Status = beatmapSet.Status - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(3), - ChildrenEnumerable = getDifficultyIcons(), - }, + new BeatmapSetOnlineStatusPill + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5 }, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Status = beatmapSet.Status + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(3), + ChildrenEnumerable = getDifficultyIcons(), + }, + } } } - } - } + }; + + mainFlow.OnLoadComplete += d => d.FadeInFromZero(1000, Easing.OutQuint); + + return mainFlow; + }, 100, 5000) }; beatmapContainer.Clear(); @@ -155,19 +161,24 @@ namespace osu.Game.Screens.Select.Carousel case CarouselItemState.Selected: - float yPos = 0; - var carouselBeatmapSet = (CarouselBeatmapSet)Item; - foreach (var item in carouselBeatmapSet.Children) + LoadComponentsAsync(carouselBeatmapSet.Children.Select(c => c.CreateDrawableRepresentation()), loaded => { - var beatmapPanel = item.CreateDrawableRepresentation(); + // make sure the pooled target hasn't changed. + if (carouselBeatmapSet != Item) + return; - beatmapPanel.Y = yPos; - yPos += item.TotalHeight; + float yPos = 0; - beatmapContainer.Add((DrawableCarouselBeatmap)beatmapPanel); - } + foreach (var item in loaded) + { + item.Y = yPos; + yPos += item.Item.TotalHeight; + + beatmapContainer.Add(item); + } + }); break; } From ca1f5dcada5affbb485c93c67716c400d8eb4f8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 18:11:41 +0900 Subject: [PATCH 3850/6909] Add back panel padding --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 00f62aa515..09c7322c58 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -676,6 +676,8 @@ namespace osu.Game.Screens.Select return set; } + private const float panel_padding = 5; + /// /// Computes the target Y positions for every item in the carousel. /// @@ -705,7 +707,7 @@ namespace osu.Game.Screens.Select // TODO: move this logic to DCBS too. // set.MoveToX(set.Item.State.Value == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo); // set.MoveToY(currentY, 750, Easing.OutExpo); - currentY += set.TotalHeight; + currentY += set.TotalHeight + panel_padding; break; } From 954d43ef56a67d01680fd6f551a78f0d3e2ee725 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 18:13:25 +0900 Subject: [PATCH 3851/6909] Debounce state application events --- osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 4c4932c22a..4ba827de86 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -125,15 +125,15 @@ namespace osu.Game.Screens.Select.Carousel if (item == null) return; - ApplyState(); + Scheduler.AddOnce(ApplyState); Item.Filtered.ValueChanged += onStateChange; Item.State.ValueChanged += onStateChange; } - private void onStateChange(ValueChangedEvent obj) => Schedule(ApplyState); + private void onStateChange(ValueChangedEvent obj) => Scheduler.AddOnce(ApplyState); - private void onStateChange(ValueChangedEvent _) => Schedule(ApplyState); + private void onStateChange(ValueChangedEvent _) => Scheduler.AddOnce(ApplyState); protected virtual void ApplyState() { From 3cfc0dc82daf6846090eb4b1db81b12e8f8527d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 18:13:39 +0900 Subject: [PATCH 3852/6909] Add safeties to beatmap panel loading code --- .../Carousel/DrawableCarouselBeatmapSet.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 6717788506..f59a6a26e6 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -78,6 +79,9 @@ namespace osu.Game.Screens.Select.Carousel { base.UpdateItem(); + beatmapContainer.Clear(); + beatmapSetState?.UnbindAll(); + Content.Children = new Drawable[] { new DelayedLoadUnloadWrapper(() => @@ -145,8 +149,6 @@ namespace osu.Game.Screens.Select.Carousel }, 100, 5000) }; - beatmapContainer.Clear(); - beatmapSetState = Item.State.GetBoundCopy(); beatmapSetState.BindValueChanged(setSelected, true); } @@ -156,14 +158,16 @@ namespace osu.Game.Screens.Select.Carousel switch (obj.NewValue) { default: - beatmapContainer.Clear(); + foreach (var beatmap in beatmapContainer) + beatmap.FadeOut(50).Expire(); break; case CarouselItemState.Selected: var carouselBeatmapSet = (CarouselBeatmapSet)Item; - LoadComponentsAsync(carouselBeatmapSet.Children.Select(c => c.CreateDrawableRepresentation()), loaded => + // ToArray() in this line is required due to framework oversight: https://github.com/ppy/osu-framework/pull/3929 + LoadComponentsAsync(carouselBeatmapSet.Children.Select(c => c.CreateDrawableRepresentation()).ToArray(), loaded => { // make sure the pooled target hasn't changed. if (carouselBeatmapSet != Item) @@ -175,9 +179,9 @@ namespace osu.Game.Screens.Select.Carousel { item.Y = yPos; yPos += item.Item.TotalHeight; - - beatmapContainer.Add(item); } + + beatmapContainer.ChildrenEnumerable = loaded; }); break; From 5c2f1346658f1a5a8d624da91040ba80db69b7f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 18:19:10 +0900 Subject: [PATCH 3853/6909] Add back left offset for selected set --- osu.Game/Screens/Select/BeatmapCarousel.cs | 10 ---------- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 7 ++++--- 2 files changed, 4 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 09c7322c58..956b2977bd 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -702,20 +702,10 @@ namespace osu.Game.Screens.Select { visibleItems.Add(set); yPositions.Add(currentY); - //lastSet = set; - // TODO: move this logic to DCBS too. - // set.MoveToX(set.Item.State.Value == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo); - // set.MoveToY(currentY, 750, Easing.OutExpo); currentY += set.TotalHeight + panel_padding; break; } - - default: - continue; - // - // break; - // } } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index f59a6a26e6..d92077cf36 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -4,7 +4,6 @@ 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; @@ -153,9 +152,11 @@ namespace osu.Game.Screens.Select.Carousel beatmapSetState.BindValueChanged(setSelected, true); } - private void setSelected(ValueChangedEvent obj) + private void setSelected(ValueChangedEvent selected) { - switch (obj.NewValue) + BorderContainer.MoveToX(selected.NewValue == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo); + + switch (selected.NewValue) { default: foreach (var beatmap in beatmapContainer) From 5c29aa8cce966d27c92963ded66e48878eb9f70e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 18:19:20 +0900 Subject: [PATCH 3854/6909] Fix multiple difficulties being expanded at once --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 4135960e06..656d1e39ba 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -172,7 +172,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Selected(); - BorderContainer.MoveToX(Item.State.Value == CarouselItemState.Selected ? -50 : 0, 500, Easing.OutExpo); + BorderContainer.MoveToX(-50, 500, Easing.OutExpo); background.Colour = ColourInfo.GradientVertical( new Color4(20, 43, 51, 255), @@ -185,6 +185,8 @@ namespace osu.Game.Screens.Select.Carousel { base.Deselected(); + BorderContainer.MoveToX(0, 500, Easing.OutExpo); + background.Colour = new Color4(20, 43, 51, 255); triangles.Colour = OsuColour.Gray(0.5f); } From c5a6f4b453f241b9269bc31fe183ae6888e2cf26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 18:32:29 +0900 Subject: [PATCH 3855/6909] Fix scroll to selected beatmap --- osu.Game/Screens/Select/BeatmapCarousel.cs | 25 +++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 956b2977bd..9f5ee22106 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -604,12 +604,7 @@ namespace osu.Game.Screens.Select // Update externally controlled state of currently visible items // (e.g. x-offset and opacity). foreach (DrawableCarouselItem p in scrollableContent.Children) - { updateItem(p); - - // foreach (var pChild in p.ChildItems) - // updateItem(pChild, p); - } } protected override void UpdateAfterChildren() @@ -703,6 +698,26 @@ namespace osu.Game.Screens.Select visibleItems.Add(set); yPositions.Add(currentY); + if (item.State.Value == CarouselItemState.Selected) + { + // 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 + DrawableCarouselBeatmapSet.HEIGHT - visibleHalfHeight + BleedTop; + + foreach (var b in set.Beatmaps) + { + if (b.State.Value == CarouselItemState.Selected) + { + scrollTarget += b.TotalHeight / 2; + break; + } + + scrollTarget += b.TotalHeight; + } + } + currentY += set.TotalHeight + panel_padding; break; } From 0a144a1388df5ac6849ffc7e4d31bf304c77cff2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 18:50:10 +0900 Subject: [PATCH 3856/6909] Correctly free panels after use to avoid finalizer disposal of subtree --- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index d92077cf36..1cdc496af8 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -53,6 +53,12 @@ namespace osu.Game.Screens.Select.Carousel [Resolved] private BeatmapManager manager { get; set; } + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + Item = null; + } + [BackgroundDependencyLoader(true)] private void load(BeatmapSetOverlay beatmapOverlay) { @@ -81,6 +87,9 @@ namespace osu.Game.Screens.Select.Carousel beatmapContainer.Clear(); beatmapSetState?.UnbindAll(); + if (Item == null) + return; + Content.Children = new Drawable[] { new DelayedLoadUnloadWrapper(() => From 524419d5e4ebff561ed99d332e666732a6479eaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 19:02:08 +0900 Subject: [PATCH 3857/6909] Fix filtered items being considered for height calculation --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 15f622f3c4..7935debac7 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Select.Carousel switch (State.Value) { case CarouselItemState.Selected: - return DrawableCarouselBeatmapSet.HEIGHT + Children.Count * DrawableCarouselBeatmap.HEIGHT; + return DrawableCarouselBeatmapSet.HEIGHT + Children.Count(c => c.Visible) * DrawableCarouselBeatmap.HEIGHT; default: return DrawableCarouselBeatmapSet.HEIGHT; From bb03c5d77c15d0b0230e2ce8ef1ec7f67d15a483 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 19:11:02 +0900 Subject: [PATCH 3858/6909] Temporarily disable masking temporarily to fix panels disappearing at extents --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9f5ee22106..20a36dbdc5 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -571,7 +571,7 @@ namespace osu.Game.Screens.Select updateItems(); // Remove all items that should no longer be on-screen - scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent); + //scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent); // Find index range of all items that should be on-screen int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 1cdc496af8..daee7abcc0 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; @@ -31,6 +32,9 @@ namespace osu.Game.Screens.Select.Carousel { public const float HEIGHT = MAX_HEIGHT; + // TODO: don't do this. need to split out the base class' style so our height isn't fixed to the panel display height (and autosize?). + protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; + private Action restoreHiddenRequested; private Action viewDetails; From 15325f5f510da118d146fa7543b2dff3d4a1406e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 19:12:00 +0900 Subject: [PATCH 3859/6909] Base bounds checks on +1 (to avoid worrying about current item heights) --- osu.Game/Screens/Select/BeatmapCarousel.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 20a36dbdc5..3c3804389b 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -574,11 +574,16 @@ namespace osu.Game.Screens.Select //scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent); // Find index range of all items that should be on-screen - int firstIndex = yPositions.BinarySearch(visibleUpperBound - DrawableCarouselItem.MAX_HEIGHT); + int firstIndex = yPositions.BinarySearch(visibleUpperBound); if (firstIndex < 0) firstIndex = ~firstIndex; int lastIndex = yPositions.BinarySearch(visibleBottomBound); if (lastIndex < 0) lastIndex = ~lastIndex; + // as we can't be 100% sure on the size of individual carousel drawables, + // always play it safe and extend bounds by one. + firstIndex = Math.Max(0, firstIndex - 1); + lastIndex = Math.Min(yPositions.Count - 1, lastIndex + 1); + if (revalidateItems || firstIndex != displayedRange.first || lastIndex != displayedRange.last) { displayedRange = (firstIndex, lastIndex); From 8847cedf296f633008e3b7d8a38f90d8606f9fb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 19:55:17 +0900 Subject: [PATCH 3860/6909] Add initial pass of vertical transforms --- osu.Game/Screens/Select/BeatmapCarousel.cs | 26 +++++++++++++++------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3c3804389b..9c49f4bf67 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -566,12 +566,8 @@ namespace osu.Game.Screens.Select bool revalidateItems = !itemsCache.IsValid; - //todo: this should only refresh items, not everything here if (revalidateItems) - updateItems(); - - // Remove all items that should no longer be on-screen - //scrollableContent.RemoveAll(p => p.Y < visibleUpperBound - p.DrawHeight || p.Y > visibleBottomBound || !p.IsPresent); + updateYPositions(); // Find index range of all items that should be on-screen int firstIndex = yPositions.BinarySearch(visibleUpperBound); @@ -586,6 +582,9 @@ namespace osu.Game.Screens.Select if (revalidateItems || firstIndex != displayedRange.first || lastIndex != displayedRange.last) { + // Remove all items that should no longer be on-screen + scrollableContent.RemoveAll(p => p.Y + p.Item.TotalHeight < visibleUpperBound || p.Y > visibleBottomBound); + displayedRange = (firstIndex, lastIndex); // Add those items within the previously found index range that should be displayed. @@ -598,11 +597,22 @@ namespace osu.Game.Screens.Select if (panel == null) { panel = setPool.Get(p => p.Item = item); + panel.Y = yPositions[i]; + panel.Depth = i; + + panel.ClearTransforms(); + scrollableContent.Add(panel); } + else + { + if (panel.IsPresent) + panel.MoveToY(yPositions[i], 800, Easing.OutQuint); + else + panel.Y = yPositions[i]; - panel.Y = yPositions[i]; - scrollableContent.ChangeChildDepth(panel, i); + scrollableContent.ChangeChildDepth(panel, i); + } } } @@ -682,7 +692,7 @@ namespace osu.Game.Screens.Select /// Computes the target Y positions for every item in the carousel. /// /// The Y position of the currently selected item. - private void updateItems() + private void updateYPositions() { yPositions.Clear(); visibleItems.Clear(); From 813ee1972895350f5ea563e5929acc4f5ca52659 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 19:55:33 +0900 Subject: [PATCH 3861/6909] Use existing event flow for rendering beatmap difficulties --- .../Carousel/DrawableCarouselBeatmapSet.cs | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index daee7abcc0..f03b6fff4a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -52,7 +52,6 @@ namespace osu.Game.Screens.Select.Carousel private BeatmapSetInfo beatmapSet => (Item as CarouselBeatmapSet)?.BeatmapSet; private Container beatmapContainer; - private Bindable beatmapSetState; [Resolved] private BeatmapManager manager { get; set; } @@ -89,7 +88,6 @@ namespace osu.Game.Screens.Select.Carousel base.UpdateItem(); beatmapContainer.Clear(); - beatmapSetState?.UnbindAll(); if (Item == null) return; @@ -160,46 +158,48 @@ namespace osu.Game.Screens.Select.Carousel return mainFlow; }, 100, 5000) }; - - beatmapSetState = Item.State.GetBoundCopy(); - beatmapSetState.BindValueChanged(setSelected, true); } - private void setSelected(ValueChangedEvent selected) + protected override void Deselected() { - BorderContainer.MoveToX(selected.NewValue == CarouselItemState.Selected ? -100 : 0, 500, Easing.OutExpo); + base.Deselected(); - switch (selected.NewValue) + BorderContainer.MoveToX(0, 500, Easing.OutExpo); + + foreach (var beatmap in beatmapContainer) + beatmap.FadeOut(50).Expire(); + } + + protected override void Selected() + { + base.Selected(); + + BorderContainer.MoveToX(-100, 500, Easing.OutExpo); + + var carouselBeatmapSet = (CarouselBeatmapSet)Item; + + // ToArray() in this line is required due to framework oversight: https://github.com/ppy/osu-framework/pull/3929 + var visibleBeatmaps = carouselBeatmapSet.Children + .Where(c => c.Visible) + .Select(c => c.CreateDrawableRepresentation()) + .ToArray(); + + LoadComponentsAsync(visibleBeatmaps, loaded => { - default: - foreach (var beatmap in beatmapContainer) - beatmap.FadeOut(50).Expire(); - break; + // make sure the pooled target hasn't changed. + if (carouselBeatmapSet != Item) + return; - case CarouselItemState.Selected: + float yPos = 0; - var carouselBeatmapSet = (CarouselBeatmapSet)Item; + foreach (var item in loaded) + { + item.Y = yPos; + yPos += item.Item.TotalHeight; + } - // ToArray() in this line is required due to framework oversight: https://github.com/ppy/osu-framework/pull/3929 - LoadComponentsAsync(carouselBeatmapSet.Children.Select(c => c.CreateDrawableRepresentation()).ToArray(), loaded => - { - // make sure the pooled target hasn't changed. - if (carouselBeatmapSet != Item) - return; - - float yPos = 0; - - foreach (var item in loaded) - { - item.Y = yPos; - yPos += item.Item.TotalHeight; - } - - beatmapContainer.ChildrenEnumerable = loaded; - }); - - break; - } + beatmapContainer.ChildrenEnumerable = loaded; + }); } private const int maximum_difficulty_icons = 18; From 82f9ca3de98d1cef76f64a6c67217384e4700460 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 20:02:06 +0900 Subject: [PATCH 3862/6909] Bind to filter event changes in base drawable item --- .../Screens/Select/Carousel/DrawableCarouselItem.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 4ba827de86..8cf63d184c 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -40,6 +40,12 @@ namespace osu.Game.Screens.Select.Carousel { Item.Filtered.ValueChanged -= onStateChange; Item.State.ValueChanged -= onStateChange; + + if (item is CarouselGroup group) + { + foreach (var c in group.Children) + c.Filtered.ValueChanged -= onStateChange; + } } item = value; @@ -129,6 +135,12 @@ namespace osu.Game.Screens.Select.Carousel Item.Filtered.ValueChanged += onStateChange; Item.State.ValueChanged += onStateChange; + + if (Item is CarouselGroup group) + { + foreach (var c in group.Children) + c.Filtered.ValueChanged += onStateChange; + } } private void onStateChange(ValueChangedEvent obj) => Scheduler.AddOnce(ApplyState); From 220c8ba2c47a8f9ffc0df5cca1f1b465088c0838 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 20:02:45 +0900 Subject: [PATCH 3863/6909] Fix incorrect vertical offsets when difficulties are filtered away --- 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 9c49f4bf67..79ad9c4584 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -723,6 +723,9 @@ namespace osu.Game.Screens.Select foreach (var b in set.Beatmaps) { + if (!b.Visible) + continue; + if (b.State.Value == CarouselItemState.Selected) { scrollTarget += b.TotalHeight / 2; From ce67f6508477ba96726bbb0c92e8397333fb030d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 20:11:10 +0900 Subject: [PATCH 3864/6909] Fix single results not showing up --- 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 79ad9c4584..9e295b6f53 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -578,7 +578,7 @@ namespace osu.Game.Screens.Select // as we can't be 100% sure on the size of individual carousel drawables, // always play it safe and extend bounds by one. firstIndex = Math.Max(0, firstIndex - 1); - lastIndex = Math.Min(yPositions.Count - 1, lastIndex + 1); + lastIndex = Math.Min(yPositions.Count, lastIndex + 1); if (revalidateItems || firstIndex != displayedRange.first || lastIndex != displayedRange.last) { From fd8654cff3c9343a78cdda2749f5f5a26d293846 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 12 Oct 2020 20:26:20 +0900 Subject: [PATCH 3865/6909] Add back difficulty panel spacing --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 2 +- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 4 +++- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 4 ++-- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 9e295b6f53..04af07846f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -732,7 +732,7 @@ namespace osu.Game.Screens.Select break; } - scrollTarget += b.TotalHeight; + scrollTarget += b.TotalHeight + DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; } } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 7935debac7..f2f8cb9bd9 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Select.Carousel switch (State.Value) { case CarouselItemState.Selected: - return DrawableCarouselBeatmapSet.HEIGHT + Children.Count(c => c.Visible) * DrawableCarouselBeatmap.HEIGHT; + return DrawableCarouselBeatmapSet.HEIGHT + Children.Count(c => c.Visible) * (DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING + DrawableCarouselBeatmap.HEIGHT); default: return DrawableCarouselBeatmapSet.HEIGHT; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 656d1e39ba..23404a6c6e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -31,7 +31,9 @@ namespace osu.Game.Screens.Select.Carousel { public class DrawableCarouselBeatmap : DrawableCarouselItem, IHasContextMenu { - public const float HEIGHT = MAX_HEIGHT * 0.6f; + public const float CAROUSEL_BEATMAP_SPACING = 5; + + public const float HEIGHT = MAX_HEIGHT * 0.6f; // TODO: add once base class is fixed + CAROUSEL_BEATMAP_SPACING; private readonly BeatmapInfo beatmap; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index f03b6fff4a..20e6258433 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -190,12 +190,12 @@ namespace osu.Game.Screens.Select.Carousel if (carouselBeatmapSet != Item) return; - float yPos = 0; + float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; foreach (var item in loaded) { item.Y = yPos; - yPos += item.Item.TotalHeight; + yPos += item.Item.TotalHeight + DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; } beatmapContainer.ChildrenEnumerable = loaded; From 975cd5a84021b5320207e0bfb7ce0d46c6757aa5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 12:47:12 +0900 Subject: [PATCH 3866/6909] Add back beatmap difficulty appear/disappear movement --- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 20e6258433..660d5d5b31 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -167,7 +167,10 @@ namespace osu.Game.Screens.Select.Carousel BorderContainer.MoveToX(0, 500, Easing.OutExpo); foreach (var beatmap in beatmapContainer) - beatmap.FadeOut(50).Expire(); + { + beatmap.MoveToY(0, 800, Easing.OutQuint); + beatmap.FadeOut(80).Expire(); + } } protected override void Selected() @@ -176,6 +179,9 @@ namespace osu.Game.Screens.Select.Carousel BorderContainer.MoveToX(-100, 500, Easing.OutExpo); + // on selection we show our child beatmaps. + // for now this is a simple drawable construction each selection. + // can be improved in the future. var carouselBeatmapSet = (CarouselBeatmapSet)Item; // ToArray() in this line is required due to framework oversight: https://github.com/ppy/osu-framework/pull/3929 @@ -190,15 +196,15 @@ namespace osu.Game.Screens.Select.Carousel if (carouselBeatmapSet != Item) return; + beatmapContainer.ChildrenEnumerable = loaded; + float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; foreach (var item in loaded) { - item.Y = yPos; + item.MoveToY(yPos, 800, Easing.OutQuint); yPos += item.Item.TotalHeight + DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; } - - beatmapContainer.ChildrenEnumerable = loaded; }); } From 9814e9ba7f098a4dfd78bb6ae3770e20c87592ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 12:50:39 +0900 Subject: [PATCH 3867/6909] Split classes out to reduce loc burder on DrawableCarouselBeatmapSet --- .../SongSelect/TestScenePlaySongSelect.cs | 22 ++-- .../Carousel/DrawableCarouselBeatmapSet.cs | 117 ------------------ .../Carousel/FilterableDifficultyIcon.cs | 32 +++++ .../FilterableGroupedDifficultyIcon.cs | 38 ++++++ .../Select/Carousel/PanelBackground.cs | 69 +++++++++++ 5 files changed, 150 insertions(+), 128 deletions(-) create mode 100644 osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs create mode 100644 osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs create mode 100644 osu.Game/Screens/Select/Carousel/PanelBackground.cs diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 0299b7a084..cd97ffe9e7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -507,7 +507,7 @@ namespace osu.Game.Tests.Visual.SongSelect var selectedPanel = songSelect.Carousel.ChildrenOfType().First(s => s.Item.State.Value == CarouselItemState.Selected); // special case for converts checked here. - return selectedPanel.ChildrenOfType().All(i => + return selectedPanel.ChildrenOfType().All(i => i.IsFiltered || i.Item.Beatmap.Ruleset.ID == targetRuleset || i.Item.Beatmap.Ruleset.ID == 0); }); @@ -606,10 +606,10 @@ namespace osu.Game.Tests.Visual.SongSelect set = songSelect.Carousel.ChildrenOfType().First(); }); - DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null; + FilterableDifficultyIcon difficultyIcon = null; AddStep("Find an icon", () => { - difficultyIcon = set.ChildrenOfType() + difficultyIcon = set.ChildrenOfType() .First(icon => getDifficultyIconIndex(set, icon) != getCurrentBeatmapIndex()); }); @@ -634,13 +634,13 @@ namespace osu.Game.Tests.Visual.SongSelect })); BeatmapInfo filteredBeatmap = null; - DrawableCarouselBeatmapSet.FilterableDifficultyIcon filteredIcon = null; + FilterableDifficultyIcon filteredIcon = null; AddStep("Get filtered icon", () => { filteredBeatmap = songSelect.Carousel.SelectedBeatmapSet.Beatmaps.First(b => b.BPM < maxBPM); int filteredBeatmapIndex = getBeatmapIndex(filteredBeatmap.BeatmapSet, filteredBeatmap); - filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex); + filteredIcon = set.ChildrenOfType().ElementAt(filteredBeatmapIndex); }); AddStep("Click on a filtered difficulty", () => @@ -674,10 +674,10 @@ namespace osu.Game.Tests.Visual.SongSelect return set != null; }); - DrawableCarouselBeatmapSet.FilterableDifficultyIcon difficultyIcon = null; + FilterableDifficultyIcon difficultyIcon = null; AddStep("Find an icon for different ruleset", () => { - difficultyIcon = set.ChildrenOfType() + difficultyIcon = set.ChildrenOfType() .First(icon => icon.Item.Beatmap.Ruleset.ID == 3); }); @@ -725,10 +725,10 @@ namespace osu.Game.Tests.Visual.SongSelect return set != null; }); - DrawableCarouselBeatmapSet.FilterableGroupedDifficultyIcon groupIcon = null; + FilterableGroupedDifficultyIcon groupIcon = null; AddStep("Find group icon for different ruleset", () => { - groupIcon = set.ChildrenOfType() + groupIcon = set.ChildrenOfType() .First(icon => icon.Items.First().Beatmap.Ruleset.ID == 3); }); @@ -821,9 +821,9 @@ namespace osu.Game.Tests.Visual.SongSelect private int getCurrentBeatmapIndex() => getBeatmapIndex(songSelect.Carousel.SelectedBeatmapSet, songSelect.Carousel.SelectedBeatmap); - private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, DrawableCarouselBeatmapSet.FilterableDifficultyIcon icon) + private int getDifficultyIconIndex(DrawableCarouselBeatmapSet set, FilterableDifficultyIcon icon) { - return set.ChildrenOfType().ToList().FindIndex(i => i == icon); + return set.ChildrenOfType().ToList().FindIndex(i => i == icon); } private void addRulesetImportStep(int id) => AddStep($"import test map for ruleset {id}", () => importForRuleset(id)); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 660d5d5b31..8ab7a054ff 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -5,15 +5,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.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -22,9 +18,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osu.Game.Rulesets; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Select.Carousel { @@ -285,116 +279,5 @@ namespace osu.Game.Screens.Select.Carousel State = { Value = state } }; } - - private class PanelBackground : BufferedContainer - { - public PanelBackground(WorkingBeatmap working) - { - CacheDrawnFrameBuffer = true; - RedrawOnScale = false; - - Children = new Drawable[] - { - new BeatmapBackgroundSprite(working) - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - FillMode = FillMode.Fill, - }, - 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, - Children = new[] - { - // The left half with no gradient applied - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Width = 0.4f, - }, - // Piecewise-linear gradient with 3 segments to make it appear smoother - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), - Width = 0.05f, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), - Width = 0.2f, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), - Width = 0.05f, - }, - } - }, - }; - } - } - - public class FilterableDifficultyIcon : DifficultyIcon - { - private readonly BindableBool filtered = new BindableBool(); - - public bool IsFiltered => filtered.Value; - - public readonly CarouselBeatmap Item; - - public FilterableDifficultyIcon(CarouselBeatmap item) - : base(item.Beatmap) - { - filtered.BindTo(item.Filtered); - filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); - filtered.TriggerChange(); - - Item = item; - } - - protected override bool OnClick(ClickEvent e) - { - Item.State.Value = CarouselItemState.Selected; - return true; - } - } - - public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon - { - public readonly List Items; - - public FilterableGroupedDifficultyIcon(List items, RulesetInfo ruleset) - : base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White) - { - Items = items; - - foreach (var item in items) - item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay)); - - 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); - } - } } } diff --git a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs new file mode 100644 index 0000000000..591e9fea22 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs @@ -0,0 +1,32 @@ +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps.Drawables; + +namespace osu.Game.Screens.Select.Carousel +{ + public class FilterableDifficultyIcon : DifficultyIcon + { + private readonly BindableBool filtered = new BindableBool(); + + public bool IsFiltered => filtered.Value; + + public readonly CarouselBeatmap Item; + + public FilterableDifficultyIcon(CarouselBeatmap item) + : base(item.Beatmap) + { + filtered.BindTo(item.Filtered); + filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); + filtered.TriggerChange(); + + Item = item; + } + + protected override bool OnClick(ClickEvent e) + { + Item.State.Value = CarouselItemState.Selected; + return true; + } + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs new file mode 100644 index 0000000000..73b5781a37 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Input.Events; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Rulesets; +using osuTK.Graphics; + +namespace osu.Game.Screens.Select.Carousel +{ + public class FilterableGroupedDifficultyIcon : GroupedDifficultyIcon + { + public readonly List Items; + + public FilterableGroupedDifficultyIcon(List items, RulesetInfo ruleset) + : base(items.Select(i => i.Beatmap).ToList(), ruleset, Color4.White) + { + Items = items; + + foreach (var item in items) + item.Filtered.BindValueChanged(_ => Scheduler.AddOnce(updateFilteredDisplay)); + + 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); + } + } +} \ No newline at end of file diff --git a/osu.Game/Screens/Select/Carousel/PanelBackground.cs b/osu.Game/Screens/Select/Carousel/PanelBackground.cs new file mode 100644 index 0000000000..587aa0a74e --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/PanelBackground.cs @@ -0,0 +1,69 @@ +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Select.Carousel +{ + public class PanelBackground : BufferedContainer + { + public PanelBackground(WorkingBeatmap working) + { + CacheDrawnFrameBuffer = true; + RedrawOnScale = false; + + Children = new Drawable[] + { + new BeatmapBackgroundSprite(working) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + FillMode = FillMode.Fill, + }, + 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, + Children = new[] + { + // The left half with no gradient applied + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Width = 0.4f, + }, + // Piecewise-linear gradient with 3 segments to make it appear smoother + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), + Width = 0.05f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), + Width = 0.2f, + }, + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), + Width = 0.05f, + }, + } + }, + }; + } + } +} From b92c22ad42af399e43a40bfe6703790a4ce75ff6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 13:11:47 +0900 Subject: [PATCH 3868/6909] Add logging --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 ++++ .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 04af07846f..78012e8bfd 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -18,6 +18,7 @@ using osu.Framework.Threading; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Extensions; using osu.Game.Graphics.Containers; @@ -582,6 +583,8 @@ namespace osu.Game.Screens.Select if (revalidateItems || firstIndex != displayedRange.first || lastIndex != displayedRange.last) { + Logger.Log("revalidation requested"); + // Remove all items that should no longer be on-screen scrollableContent.RemoveAll(p => p.Y + p.Item.TotalHeight < visibleUpperBound || p.Y > visibleBottomBound); @@ -596,6 +599,7 @@ namespace osu.Game.Screens.Select if (panel == null) { + Logger.Log($"getting panel for {item} from pool"); panel = setPool.Get(p => p.Item = item); panel.Y = yPositions[i]; panel.Depth = i; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 8ab7a054ff..fed32b5849 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Localisation; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Collections; @@ -86,10 +87,12 @@ namespace osu.Game.Screens.Select.Carousel if (Item == null) return; + Logger.Log($"updating item {beatmapSet}"); Content.Children = new Drawable[] { new DelayedLoadUnloadWrapper(() => { + Logger.Log($"loaded background item {beatmapSet}"); var background = new PanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { RelativeSizeAxes = Axes.Both, From f6aa448523b5e5fafe2c694d215671c08cd431df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 13:21:21 +0900 Subject: [PATCH 3869/6909] Store y positions inside items rather than in a separate array --- osu.Game/Screens/Select/BeatmapCarousel.cs | 54 +++++++++++++------ .../Screens/Select/Carousel/CarouselItem.cs | 10 +++- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 78012e8bfd..87e930fe8a 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -116,7 +116,6 @@ namespace osu.Game.Screens.Select }); } - private readonly List yPositions = new List(); private readonly List visibleItems = new List(); private readonly Cached itemsCache = new Cached(); @@ -570,23 +569,15 @@ namespace osu.Game.Screens.Select if (revalidateItems) updateYPositions(); - // Find index range of all items that should be on-screen - int firstIndex = yPositions.BinarySearch(visibleUpperBound); - if (firstIndex < 0) firstIndex = ~firstIndex; - int lastIndex = yPositions.BinarySearch(visibleBottomBound); - if (lastIndex < 0) lastIndex = ~lastIndex; - - // as we can't be 100% sure on the size of individual carousel drawables, - // always play it safe and extend bounds by one. - firstIndex = Math.Max(0, firstIndex - 1); - lastIndex = Math.Min(yPositions.Count, lastIndex + 1); + var (firstIndex, lastIndex) = getDisplayRange(); if (revalidateItems || firstIndex != displayedRange.first || lastIndex != displayedRange.last) { Logger.Log("revalidation requested"); // Remove all items that should no longer be on-screen - scrollableContent.RemoveAll(p => p.Y + p.Item.TotalHeight < visibleUpperBound || p.Y > visibleBottomBound); + // TODO: figure out a more resilient way of doing this removal. + // scrollableContent.RemoveAll(p => p.Y + p.Item.TotalHeight < visibleUpperBound || p.Y > visibleBottomBound); displayedRange = (firstIndex, lastIndex); @@ -601,7 +592,7 @@ namespace osu.Game.Screens.Select { Logger.Log($"getting panel for {item} from pool"); panel = setPool.Get(p => p.Item = item); - panel.Y = yPositions[i]; + panel.Y = item.CarouselYPosition; panel.Depth = i; panel.ClearTransforms(); @@ -611,9 +602,9 @@ namespace osu.Game.Screens.Select else { if (panel.IsPresent) - panel.MoveToY(yPositions[i], 800, Easing.OutQuint); + panel.MoveToY(item.CarouselYPosition, 800, Easing.OutQuint); else - panel.Y = yPositions[i]; + panel.Y = item.CarouselYPosition; scrollableContent.ChangeChildDepth(panel, i); } @@ -626,6 +617,22 @@ namespace osu.Game.Screens.Select updateItem(p); } + private (int firstIndex, int lastIndex) getDisplayRange() + { + // Find index range of all items that should be on-screen + // TODO: reduce allocs of CarouselBoundsItem. + int firstIndex = visibleItems.BinarySearch(new CarouselBoundsItem(visibleUpperBound)); + if (firstIndex < 0) firstIndex = ~firstIndex; + int lastIndex = visibleItems.BinarySearch(new CarouselBoundsItem(visibleBottomBound)); + if (lastIndex < 0) lastIndex = ~lastIndex; + + // as we can't be 100% sure on the size of individual carousel drawables, + // always play it safe and extend bounds by one. + firstIndex = Math.Max(0, firstIndex - 1); + lastIndex = Math.Min(visibleItems.Count, lastIndex + 1); + return (firstIndex, lastIndex); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); @@ -698,7 +705,6 @@ namespace osu.Game.Screens.Select /// The Y position of the currently selected item. private void updateYPositions() { - yPositions.Clear(); visibleItems.Clear(); float currentY = visibleHalfHeight; @@ -715,7 +721,7 @@ namespace osu.Game.Screens.Select case CarouselBeatmapSet set: { visibleItems.Add(set); - yPositions.Add(currentY); + set.CarouselYPosition = currentY; if (item.State.Value == CarouselItemState.Selected) { @@ -815,6 +821,20 @@ namespace osu.Game.Screens.Select p.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); } + /// + /// A carousel item strictly used for binary search purposes. + /// + private class CarouselBoundsItem : CarouselItem + { + public CarouselBoundsItem(in float pos) + { + CarouselYPosition = pos; + } + + public override DrawableCarouselItem CreateDrawableRepresentation() => + throw new NotImplementedException(); + } + private class CarouselRoot : CarouselGroupEagerSelect { private readonly BeatmapCarousel carousel; diff --git a/osu.Game/Screens/Select/Carousel/CarouselItem.cs b/osu.Game/Screens/Select/Carousel/CarouselItem.cs index 004d9779ef..4bd477412d 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselItem.cs @@ -1,14 +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 osu.Framework.Bindables; namespace osu.Game.Screens.Select.Carousel { - public abstract class CarouselItem + public abstract class CarouselItem : IComparable { public virtual float TotalHeight => 0; + /// + /// An externally defined value used to determine this item's vertical display offset relative to the carousel. + /// + public float CarouselYPosition; + public readonly BindableBool Filtered = new BindableBool(); public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); @@ -42,6 +48,8 @@ namespace osu.Game.Screens.Select.Carousel } public virtual int CompareTo(FilterCriteria criteria, CarouselItem other) => ChildID.CompareTo(other.ChildID); + + public int CompareTo(CarouselItem other) => CarouselYPosition.CompareTo(other.CarouselYPosition); } public enum CarouselItemState From 20b54fb904957a208adb2fdbdfb9351f6a3ebe73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 13:25:45 +0900 Subject: [PATCH 3870/6909] Move transform clean-up to pooling free call --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 -- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 87e930fe8a..c15652b132 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -595,8 +595,6 @@ namespace osu.Game.Screens.Select panel.Y = item.CarouselYPosition; panel.Depth = i; - panel.ClearTransforms(); - scrollableContent.Add(panel); } else diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index fed32b5849..635eb6f375 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -54,7 +54,9 @@ namespace osu.Game.Screens.Select.Carousel protected override void FreeAfterUse() { base.FreeAfterUse(); + Item = null; + ClearTransforms(); } [BackgroundDependencyLoader(true)] From 06e84c8eb3137c451fa0595fb5b55e92cbb9368f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 13:44:32 +0900 Subject: [PATCH 3871/6909] Add comments and split out update steps into a more logical flow --- osu.Game/Screens/Select/BeatmapCarousel.cs | 57 ++++++++++++---------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c15652b132..ad2c4c2ca8 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -566,53 +566,56 @@ namespace osu.Game.Screens.Select bool revalidateItems = !itemsCache.IsValid; + // First we iterate over all non-filtered carousel items and populate their + // vertical position data. if (revalidateItems) updateYPositions(); - var (firstIndex, lastIndex) = getDisplayRange(); + // This data is consumed to find the currently displayable range. + // This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn. + var newDisplayRange = getDisplayRange(); - if (revalidateItems || firstIndex != displayedRange.first || lastIndex != displayedRange.last) + // If the filtered items or visible range has changed, pooling requirements need to be checked. + // This involves fetching new items from the pool, returning no-longer required items. + if (revalidateItems || newDisplayRange != displayedRange) { Logger.Log("revalidation requested"); - // Remove all items that should no longer be on-screen - // TODO: figure out a more resilient way of doing this removal. - // scrollableContent.RemoveAll(p => p.Y + p.Item.TotalHeight < visibleUpperBound || p.Y > visibleBottomBound); - - displayedRange = (firstIndex, lastIndex); + displayedRange = newDisplayRange; // Add those items within the previously found index range that should be displayed. - for (int i = firstIndex; i < lastIndex; ++i) + for (int i = displayedRange.first; i < displayedRange.last; ++i) { var item = visibleItems[i]; - var panel = scrollableContent.FirstOrDefault(c => c.Item == item); + if (scrollableContent.Any(c => c.Item == item)) continue; - if (panel == null) - { - Logger.Log($"getting panel for {item} from pool"); - panel = setPool.Get(p => p.Item = item); - panel.Y = item.CarouselYPosition; - panel.Depth = i; + Logger.Log($"getting panel for {item} from pool"); + var panel = setPool.Get(p => p.Item = item); + panel.Depth = i; + panel.Y = item.CarouselYPosition; + scrollableContent.Add(panel); + } - scrollableContent.Add(panel); - } - else - { - if (panel.IsPresent) - panel.MoveToY(item.CarouselYPosition, 800, Easing.OutQuint); - else - panel.Y = item.CarouselYPosition; + // Remove any items which are far out of the visible range. + } - scrollableContent.ChangeChildDepth(panel, i); - } + // Finally, if the filtered items have changed, animate drawables to their new locations. + // This is common if a selected/collapsed state has changed. + if (revalidateItems) + { + foreach (DrawableCarouselItem panel in scrollableContent.Children) + { + panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint); } } - // Update externally controlled state of currently visible items - // (e.g. x-offset and opacity). + // Update externally controlled state of currently visible items (e.g. x-offset and opacity). + // This is a per-frame update on all drawable panels. foreach (DrawableCarouselItem p in scrollableContent.Children) + { updateItem(p); + } } private (int firstIndex, int lastIndex) getDisplayRange() From 29983afceffd4f44d91d114b49929cd4b27b584a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 13:51:27 +0900 Subject: [PATCH 3872/6909] Replace pool/cleanup logic with simplest form possible This will temporarily break panels that go off-screen, as they will disappear immediately --- osu.Game/Screens/Select/BeatmapCarousel.cs | 32 ++++++++++++++-------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index ad2c4c2ca8..56fe28fd14 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -583,21 +583,31 @@ namespace osu.Game.Screens.Select displayedRange = newDisplayRange; - // Add those items within the previously found index range that should be displayed. - for (int i = displayedRange.first; i < displayedRange.last; ++i) + var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first); + + foreach (var panel in scrollableContent.Children) { - var item = visibleItems[i]; + if (toDisplay.Remove(panel.Item)) + { + // panel already displayed. + continue; + } - if (scrollableContent.Any(c => c.Item == item)) continue; - - Logger.Log($"getting panel for {item} from pool"); - var panel = setPool.Get(p => p.Item = item); - panel.Depth = i; - panel.Y = item.CarouselYPosition; - scrollableContent.Add(panel); + // panel displayed but not required + scrollableContent.Remove(panel); } - // Remove any items which are far out of the visible range. + // Add those items within the previously found index range that should be displayed. + foreach (var item in toDisplay) + { + Logger.Log($"getting panel for {item} from pool"); + var panel = setPool.Get(p => p.Item = item); + + panel.Depth = item.CarouselYPosition; // todo: i think this is correct + panel.Y = item.CarouselYPosition; + + scrollableContent.Add(panel); + } } // Finally, if the filtered items have changed, animate drawables to their new locations. From 075bf23714fe358b09481a7bab008042e4f3fa8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 14:14:39 +0900 Subject: [PATCH 3873/6909] Better track off-screen drawables (and return to pool less often) --- osu.Game/Screens/Select/BeatmapCarousel.cs | 24 ++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 56fe28fd14..c8ba98f6be 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -560,6 +560,16 @@ namespace osu.Game.Screens.Select private (int first, int last) displayedRange; + /// + /// Extend the range to retain already loaded pooled drawables. + /// + private const float distance_offscreen_before_unload = 1024; + + /// + /// Extend the range to update positions / retrieve pooled drawables outside of visible range. + /// + private const float distance_offscreen_to_preload = 256; + protected override void Update() { base.Update(); @@ -585,7 +595,7 @@ namespace osu.Game.Screens.Select var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first); - foreach (var panel in scrollableContent.Children) + foreach (var panel in scrollableContent.Children.ToArray()) { if (toDisplay.Remove(panel.Item)) { @@ -593,8 +603,10 @@ namespace osu.Game.Screens.Select continue; } - // panel displayed but not required - scrollableContent.Remove(panel); + // panel loaded as drawable but not required by visible range. + // remove but only if too far off-screen + if (panel.Y < visibleUpperBound - distance_offscreen_before_unload || panel.Y > visibleBottomBound + distance_offscreen_before_unload) + scrollableContent.Remove(panel); } // Add those items within the previously found index range that should be displayed. @@ -603,7 +615,7 @@ namespace osu.Game.Screens.Select Logger.Log($"getting panel for {item} from pool"); var panel = setPool.Get(p => p.Item = item); - panel.Depth = item.CarouselYPosition; // todo: i think this is correct + panel.Depth = item.CarouselYPosition; panel.Y = item.CarouselYPosition; scrollableContent.Add(panel); @@ -632,9 +644,9 @@ namespace osu.Game.Screens.Select { // Find index range of all items that should be on-screen // TODO: reduce allocs of CarouselBoundsItem. - int firstIndex = visibleItems.BinarySearch(new CarouselBoundsItem(visibleUpperBound)); + int firstIndex = visibleItems.BinarySearch(new CarouselBoundsItem(visibleUpperBound - distance_offscreen_to_preload)); if (firstIndex < 0) firstIndex = ~firstIndex; - int lastIndex = visibleItems.BinarySearch(new CarouselBoundsItem(visibleBottomBound)); + int lastIndex = visibleItems.BinarySearch(new CarouselBoundsItem(visibleBottomBound + distance_offscreen_to_preload)); if (lastIndex < 0) lastIndex = ~lastIndex; // as we can't be 100% sure on the size of individual carousel drawables, From 1b7e3397c6df1e04233d3c37e3c83d9b4aeb90b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 14:23:29 +0900 Subject: [PATCH 3874/6909] Use expiry to avoid ToArray --- osu.Game/Screens/Select/BeatmapCarousel.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c8ba98f6be..65bd607f85 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -595,7 +595,7 @@ namespace osu.Game.Screens.Select var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first); - foreach (var panel in scrollableContent.Children.ToArray()) + foreach (var panel in scrollableContent.Children) { if (toDisplay.Remove(panel.Item)) { @@ -606,7 +606,11 @@ namespace osu.Game.Screens.Select // panel loaded as drawable but not required by visible range. // remove but only if too far off-screen if (panel.Y < visibleUpperBound - distance_offscreen_before_unload || panel.Y > visibleBottomBound + distance_offscreen_before_unload) - scrollableContent.Remove(panel); + { + // todo: may want a fade effect here (could be seen if a huge change happens, like a set with 20 difficulties becomes selected). + panel.ClearTransforms(); + panel.Expire(); + } } // Add those items within the previously found index range that should be displayed. From 2aad482545ce8a95fe3de3630278ddaf4d39b048 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 14:37:44 +0900 Subject: [PATCH 3875/6909] Fix x offsets of difficulties not being updated --- osu.Game/Screens/Select/BeatmapCarousel.cs | 27 +++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 65bd607f85..775b4ad7b6 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -638,9 +638,15 @@ namespace osu.Game.Screens.Select // Update externally controlled state of currently visible items (e.g. x-offset and opacity). // This is a per-frame update on all drawable panels. - foreach (DrawableCarouselItem p in scrollableContent.Children) + foreach (DrawableCarouselItem item in scrollableContent.Children) { - updateItem(p); + updateItem(item); + + if (item is DrawableCarouselBeatmapSet set) + { + foreach (var diff in set.ChildItems) + updateItem(diff, item); + } } } @@ -831,21 +837,20 @@ namespace osu.Game.Screens.Select /// Update a item's x position and multiplicative alpha based on its y position and /// the current scroll position. /// - /// The item to be updated. - private void updateItem(DrawableCarouselItem p) + /// The item to be updated. + /// For nested items, the parent of the item to be updated. + private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null) { - float itemDrawY = p.Position.Y - visibleUpperBound + p.DrawHeight / 2; + Vector2 posInScroll = scrollableContent.ToLocalSpace(item.ScreenSpaceDrawQuad.Centre); + float itemDrawY = posInScroll.Y - visibleUpperBound; float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); - // Setting the origin position serves as an additive position on top of potential - // local transformation we may want to apply (e.g. when a item gets selected, we - // may want to smoothly transform it leftwards.) - p.OriginPosition = new Vector2(-offsetX(dist, visibleHalfHeight), 0); + item.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0); // We are applying a multiplicative alpha (which is internally done by nesting an // additional container and setting that container's alpha) such that we can - // layer transformations on top, with a similar reasoning to the previous comment. - p.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); + // layer alpha transformations on top. + item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); } /// From cfec4f4fc14833562f464ec79eea0d4ee9f65549 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 15:19:32 +0900 Subject: [PATCH 3876/6909] Extract header element from base DrawableCarouselItem class --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- .../Screens/Select/Carousel/CarouselHeader.cs | 108 +++++++++++++++++ .../Carousel/DrawableCarouselBeatmap.cs | 11 +- .../Carousel/DrawableCarouselBeatmapSet.cs | 20 ++-- .../Select/Carousel/DrawableCarouselItem.cs | 113 +++++------------- 5 files changed, 152 insertions(+), 102 deletions(-) create mode 100644 osu.Game/Screens/Select/Carousel/CarouselHeader.cs diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 775b4ad7b6..bcdbc53e26 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -841,7 +841,7 @@ namespace osu.Game.Screens.Select /// For nested items, the parent of the item to be updated. private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null) { - Vector2 posInScroll = scrollableContent.ToLocalSpace(item.ScreenSpaceDrawQuad.Centre); + Vector2 posInScroll = scrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); float itemDrawY = posInScroll.Y - visibleUpperBound; float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs new file mode 100644 index 0000000000..f59cccd8b6 --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -0,0 +1,108 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; +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.Input.Events; +using osu.Framework.Utils; +using osu.Game.Graphics; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Select.Carousel +{ + public class CarouselHeader : Container + { + private SampleChannel sampleHover; + + private readonly Box hoverLayer; + + public Container BorderContainer; + + public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); + + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + + public CarouselHeader() + { + RelativeSizeAxes = Axes.X; + Height = DrawableCarouselItem.MAX_HEIGHT; + + InternalChild = BorderContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 10, + BorderColour = new Color4(221, 255, 255, 255), + Children = new Drawable[] + { + Content, + hoverLayer = new Box + { + RelativeSizeAxes = Axes.Both, + Alpha = 0, + Blending = BlendingParameters.Additive, + }, + } + }; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}"); + hoverLayer.Colour = colours.Blue.Opacity(0.1f); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + State.BindValueChanged(updateState, true); + } + + private void updateState(ValueChangedEvent state) + { + switch (state.NewValue) + { + case CarouselItemState.Collapsed: + case CarouselItemState.NotSelected: + BorderContainer.BorderThickness = 0; + BorderContainer.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Shadow, Offset = new Vector2(1), Radius = 10, Colour = Color4.Black.Opacity(100), + }; + break; + + case CarouselItemState.Selected: + BorderContainer.BorderThickness = 2.5f; + BorderContainer.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, Colour = new Color4(130, 204, 255, 150), Radius = 20, Roundness = 10, + }; + break; + } + } + + protected override bool OnHover(HoverEvent e) + { + sampleHover?.Play(); + + hoverLayer.FadeIn(100, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + hoverLayer.FadeOut(1000, Easing.OutQuint); + base.OnHoverLost(e); + } + } +} diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 23404a6c6e..b06b60e6c5 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -64,15 +64,14 @@ namespace osu.Game.Screens.Select.Carousel public DrawableCarouselBeatmap(CarouselBeatmap panel) { beatmap = panel.Beatmap; - Height = HEIGHT; - - // todo: temporary Item = panel; } [BackgroundDependencyLoader(true)] private void load(BeatmapManager manager, SongSelect songSelect) { + Header.Height = HEIGHT; + if (songSelect != null) { startRequested = b => songSelect.FinaliseSelection(b); @@ -83,7 +82,7 @@ namespace osu.Game.Screens.Select.Carousel if (manager != null) hideRequested = manager.Hide; - Content.Children = new Drawable[] + Header.Children = new Drawable[] { background = new Box { @@ -174,7 +173,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Selected(); - BorderContainer.MoveToX(-50, 500, Easing.OutExpo); + Header.MoveToX(-50, 500, Easing.OutExpo); background.Colour = ColourInfo.GradientVertical( new Color4(20, 43, 51, 255), @@ -187,7 +186,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Deselected(); - BorderContainer.MoveToX(0, 500, Easing.OutExpo); + Header.MoveToX(0, 500, Easing.OutExpo); background.Colour = new Color4(20, 43, 51, 255); triangles.Colour = OsuColour.Gray(0.5f); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 635eb6f375..e1a2ce3962 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -67,16 +67,12 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapOverlay != null) viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; - // TODO: temporary. we probably want to *not* inherit DrawableCarouselItem for this class, but only the above header portion. - AddRangeInternal(new Drawable[] + Content.Add(beatmapContainer = new Container { - beatmapContainer = new Container - { - X = 50, - Y = MAX_HEIGHT, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, + X = 50, + Y = MAX_HEIGHT, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, }); } @@ -90,7 +86,7 @@ namespace osu.Game.Screens.Select.Carousel return; Logger.Log($"updating item {beatmapSet}"); - Content.Children = new Drawable[] + Header.Children = new Drawable[] { new DelayedLoadUnloadWrapper(() => { @@ -163,7 +159,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Deselected(); - BorderContainer.MoveToX(0, 500, Easing.OutExpo); + MovementContainer.MoveToX(0, 500, Easing.OutExpo); foreach (var beatmap in beatmapContainer) { @@ -176,7 +172,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Selected(); - BorderContainer.MoveToX(-100, 500, Easing.OutExpo); + MovementContainer.MoveToX(-100, 500, Easing.OutExpo); // on selection we show our child beatmaps. // for now this is a simple drawable construction each selection. diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 8cf63d184c..6a9b60fa57 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -4,21 +4,11 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Pooling; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Utils; -using osu.Game.Graphics; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Select.Carousel { @@ -28,6 +18,14 @@ namespace osu.Game.Screens.Select.Carousel public override bool IsPresent => base.IsPresent || Item?.Visible == true; + public readonly CarouselHeader Header; + + protected readonly Container Content; + + protected readonly Container MovementContainer; + + private CarouselItem item; + public CarouselItem Item { get => item; @@ -38,8 +36,10 @@ namespace osu.Game.Screens.Select.Carousel if (item != null) { - Item.Filtered.ValueChanged -= onStateChange; - Item.State.ValueChanged -= onStateChange; + item.Filtered.ValueChanged -= onStateChange; + item.State.ValueChanged -= onStateChange; + + Header.State.UnbindFrom(item.State); if (item is CarouselGroup group) { @@ -57,67 +57,33 @@ namespace osu.Game.Screens.Select.Carousel public virtual IEnumerable ChildItems => Enumerable.Empty(); - private readonly Container nestedContainer; - - protected readonly Container BorderContainer; - - private readonly Box hoverLayer; - - protected Container Content => nestedContainer; - protected DrawableCarouselItem() { - Height = MAX_HEIGHT; RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Alpha = 0; - InternalChild = BorderContainer = new Container + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Masking = true, - CornerRadius = 10, - BorderColour = new Color4(221, 255, 255, 255), - Children = new Drawable[] + MovementContainer = new Container { - nestedContainer = new Container + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - }, - hoverLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Blending = BlendingParameters.Additive, - }, - } + Header = new CarouselHeader(), + Content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + } + } + }, }; } - private SampleChannel sampleHover; - private CarouselItem item; - - [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuColour colours) - { - sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}"); - hoverLayer.Colour = colours.Blue.Opacity(0.1f); - } - - protected override bool OnHover(HoverEvent e) - { - sampleHover?.Play(); - - hoverLayer.FadeIn(100, Easing.OutQuint); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - hoverLayer.FadeOut(1000, Easing.OutQuint); - base.OnHoverLost(e); - } - - public void SetMultiplicativeAlpha(float alpha) => BorderContainer.Alpha = alpha; + public void SetMultiplicativeAlpha(float alpha) => Header.BorderContainer.Alpha = alpha; protected override void LoadComplete() { @@ -136,6 +102,8 @@ namespace osu.Game.Screens.Select.Carousel Item.Filtered.ValueChanged += onStateChange; Item.State.ValueChanged += onStateChange; + Header.State.BindTo(Item.State); + if (Item is CarouselGroup group) { foreach (var c in group.Children) @@ -171,31 +139,10 @@ namespace osu.Game.Screens.Select.Carousel protected virtual void Selected() { Debug.Assert(Item != null); - - Item.State.Value = CarouselItemState.Selected; - - BorderContainer.BorderThickness = 2.5f; - BorderContainer.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = new Color4(130, 204, 255, 150), - Radius = 20, - Roundness = 10, - }; } protected virtual void Deselected() { - Item.State.Value = CarouselItemState.NotSelected; - - BorderContainer.BorderThickness = 0; - BorderContainer.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Shadow, - Offset = new Vector2(1), - Radius = 10, - Colour = Color4.Black.Opacity(100), - }; } protected override bool OnClick(ClickEvent e) From dd8943eb7f5f3816fc2f3b1d218a41166e373b78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 15:19:41 +0900 Subject: [PATCH 3877/6909] Update test scene to fix crash --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 7d850a0d13..d3ae3a19ea 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -427,7 +427,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 3; i++) { - var set = createTestBeatmapSet(i); + var set = createTestBeatmapSet(i, 3); set.Beatmaps[0].StarDifficulty = 3 - i; set.Beatmaps[2].StarDifficulty = 6 + i; sets.Add(set); @@ -822,7 +822,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selection is visible", selectedBeatmapVisible); } - private BeatmapSetInfo createTestBeatmapSet(int id) + private BeatmapSetInfo createTestBeatmapSet(int id, int minimumDifficulties = 1) { return new BeatmapSetInfo { @@ -836,7 +836,7 @@ namespace osu.Game.Tests.Visual.SongSelect Title = $"test set #{id}!", AuthorString = string.Concat(Enumerable.Repeat((char)('z' - Math.Min(25, id - 1)), 5)) }, - Beatmaps = getBeatmaps(RNG.Next(1, 20)).ToList() + Beatmaps = getBeatmaps(RNG.Next(minimumDifficulties, 20)).ToList() }; } From c08b5e8d03b83221ac0c56688d42374262f9c6ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 15:23:27 +0900 Subject: [PATCH 3878/6909] Align beatmap difficulties correctly --- 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 e1a2ce3962..64a990a27d 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -69,7 +69,7 @@ namespace osu.Game.Screens.Select.Carousel Content.Add(beatmapContainer = new Container { - X = 50, + X = 100, Y = MAX_HEIGHT, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, From 1da49073abd01ec14b84b3e32c9fa353eae7d738 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 15:32:37 +0900 Subject: [PATCH 3879/6909] Calculate content height automatically --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 1 - osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 6 ++++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 64a990a27d..b83915e0c6 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -70,7 +70,6 @@ namespace osu.Game.Screens.Select.Carousel Content.Add(beatmapContainer = new Container { X = 100, - Y = MAX_HEIGHT, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 6a9b60fa57..5bcaffa3cf 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -20,6 +20,9 @@ namespace osu.Game.Screens.Select.Carousel public readonly CarouselHeader Header; + /// + /// Optional content which sits below the header. + /// protected readonly Container Content; protected readonly Container MovementContainer; @@ -89,6 +92,9 @@ namespace osu.Game.Screens.Select.Carousel { base.LoadComplete(); + // avoid using fill flow for performance reasons. header size doesn't change after load. + Content.Y = Header.Height; + UpdateItem(); } From cecdf14f5363d9590b130526a8e24e8c3bbddefd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 16:04:37 +0900 Subject: [PATCH 3880/6909] Avoid reconstructing beatmap difficulties that were recently displayed --- .../Carousel/DrawableCarouselBeatmapSet.cs | 78 ++++++++++++------- 1 file changed, 49 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index b83915e0c6..9a822f83e3 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -56,6 +56,7 @@ namespace osu.Game.Screens.Select.Carousel base.FreeAfterUse(); Item = null; + ClearTransforms(); } @@ -66,20 +67,14 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapOverlay != null) viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; - - Content.Add(beatmapContainer = new Container - { - X = 100, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }); } protected override void UpdateItem() { base.UpdateItem(); - beatmapContainer.Clear(); + Content.Clear(); + beatmapContainer = null; if (Item == null) return; @@ -160,10 +155,13 @@ namespace osu.Game.Screens.Select.Carousel MovementContainer.MoveToX(0, 500, Easing.OutExpo); - foreach (var beatmap in beatmapContainer) + if (beatmapContainer != null) { - beatmap.MoveToY(0, 800, Easing.OutQuint); - beatmap.FadeOut(80).Expire(); + foreach (var beatmap in beatmapContainer) + { + beatmap.MoveToY(0, 800, Easing.OutQuint); + beatmap.FadeOut(80); + } } } @@ -173,33 +171,55 @@ namespace osu.Game.Screens.Select.Carousel MovementContainer.MoveToX(-100, 500, Easing.OutExpo); - // on selection we show our child beatmaps. - // for now this is a simple drawable construction each selection. - // can be improved in the future. - var carouselBeatmapSet = (CarouselBeatmapSet)Item; - - // ToArray() in this line is required due to framework oversight: https://github.com/ppy/osu-framework/pull/3929 - var visibleBeatmaps = carouselBeatmapSet.Children - .Where(c => c.Visible) - .Select(c => c.CreateDrawableRepresentation()) - .ToArray(); - - LoadComponentsAsync(visibleBeatmaps, loaded => + if (beatmapContainer != null) { - // make sure the pooled target hasn't changed. - if (carouselBeatmapSet != Item) - return; + // if already loaded, we only need to re-animate. + animateBeatmaps(); + } + else + { + // on selection we show our child beatmaps. + // for now this is a simple drawable construction each selection. + // can be improved in the future. + var carouselBeatmapSet = (CarouselBeatmapSet)Item; - beatmapContainer.ChildrenEnumerable = loaded; + // ToArray() in this line is required due to framework oversight: https://github.com/ppy/osu-framework/pull/3929 + var visibleBeatmaps = carouselBeatmapSet.Children + .Where(c => c.Visible) + .Select(c => c.CreateDrawableRepresentation()) + .ToArray(); + beatmapContainer = new Container + { + X = 100, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + ChildrenEnumerable = visibleBeatmaps + }; + + Logger.Log($"loading {visibleBeatmaps.Length} beatmaps for {Item}"); + + LoadComponentAsync(beatmapContainer, loaded => + { + // make sure the pooled target hasn't changed. + if (carouselBeatmapSet != Item) + return; + + Content.Child = loaded; + animateBeatmaps(); + }); + } + + void animateBeatmaps() + { float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; - foreach (var item in loaded) + foreach (var item in beatmapContainer.Children) { item.MoveToY(yPos, 800, Easing.OutQuint); yPos += item.Item.TotalHeight + DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; } - }); + } } private const int maximum_difficulty_icons = 18; From ded09b78cbbda03d95df2c75f17785a02b84c434 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 16:33:37 +0900 Subject: [PATCH 3881/6909] Avoid usage of AutoSize for DrawableCarouselItems in general --- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 3 +-- .../Screens/Select/Carousel/DrawableCarouselItem.cs | 11 ++++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 9a822f83e3..a6cea5ee53 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -192,8 +192,7 @@ namespace osu.Game.Screens.Select.Carousel beatmapContainer = new Container { X = 100, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, ChildrenEnumerable = visibleBeatmaps }; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 5bcaffa3cf..68825873ff 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -63,7 +63,6 @@ namespace osu.Game.Screens.Select.Carousel protected DrawableCarouselItem() { RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; Alpha = 0; @@ -71,15 +70,13 @@ namespace osu.Game.Screens.Select.Carousel { MovementContainer = new Container { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, Children = new Drawable[] { Header = new CarouselHeader(), Content = new Container { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.Both, } } }, @@ -123,6 +120,10 @@ namespace osu.Game.Screens.Select.Carousel protected virtual void ApplyState() { + // Use the fact that we know the precise height of the item from the model to avoid the need for AutoSize overhead. + // Additionally, AutoSize doesn't work well due to content starting off-screen and being masked away. + Height = Item.TotalHeight; + Debug.Assert(Item != null); switch (Item.State.Value) From b536f571fd7885ef2048b871997d5a012fb2cedb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 16:35:06 +0900 Subject: [PATCH 3882/6909] Move header height propagation to update for safety --- osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index 68825873ff..bafb338a04 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -89,10 +89,15 @@ namespace osu.Game.Screens.Select.Carousel { base.LoadComplete(); + UpdateItem(); + } + + protected override void Update() + { + base.Update(); + // avoid using fill flow for performance reasons. header size doesn't change after load. Content.Y = Header.Height; - - UpdateItem(); } protected virtual void UpdateItem() From 1f0aa974dd0a0155e0703036f3c2825e76c94e2b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 17:09:54 +0900 Subject: [PATCH 3883/6909] Fix failing tests --- .../SongSelect/TestSceneBeatmapCarousel.cs | 37 +++++++++++++++---- osu.Game/Screens/Select/BeatmapCarousel.cs | 18 ++++----- 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index d3ae3a19ea..8e4e2ac257 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -427,7 +427,7 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < 3; i++) { - var set = createTestBeatmapSet(i, 3); + var set = createTestBeatmapSet(i); set.Beatmaps[0].StarDifficulty = 3 - i; set.Beatmaps[2].StarDifficulty = 6 + i; sets.Add(set); @@ -822,7 +822,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selection is visible", selectedBeatmapVisible); } - private BeatmapSetInfo createTestBeatmapSet(int id, int minimumDifficulties = 1) + private BeatmapSetInfo createTestBeatmapSet(int id, bool randomDifficultyCount = false) { return new BeatmapSetInfo { @@ -836,7 +836,7 @@ namespace osu.Game.Tests.Visual.SongSelect Title = $"test set #{id}!", AuthorString = string.Concat(Enumerable.Repeat((char)('z' - Math.Min(25, id - 1)), 5)) }, - Beatmaps = getBeatmaps(RNG.Next(minimumDifficulties, 20)).ToList() + Beatmaps = getBeatmaps(randomDifficultyCount ? RNG.Next(1, 20) : 3).ToList() }; } @@ -846,14 +846,22 @@ namespace osu.Game.Tests.Visual.SongSelect for (int i = 0; i < count; i++) { + float diff = (float)i / count * 10; + + string version = "Normal"; + if (diff > 6.6) + version = "Insane"; + else if (diff > 3.3) + version = "Hard"; + yield return new BeatmapInfo { OnlineBeatmapID = id++ * 10, - Version = "Normal", - StarDifficulty = RNG.NextSingle() * 10, + Version = version, + StarDifficulty = diff, BaseDifficulty = new BeatmapDifficulty { - OverallDifficulty = RNG.NextSingle() * 10, + OverallDifficulty = diff, } }; } @@ -899,7 +907,22 @@ namespace osu.Game.Tests.Visual.SongSelect { public bool PendingFilterTask => PendingFilter != null; - public IEnumerable Items => InternalChildren.OfType(); + public IEnumerable Items + { + get + { + foreach (var item in ScrollableContent) + { + yield return item; + + if (item is DrawableCarouselBeatmapSet set) + { + foreach (var difficulty in set.ChildItems) + yield return difficulty; + } + } + } + } protected override IEnumerable GetLoadableBeatmaps() => Enumerable.Empty(); } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index bcdbc53e26..b6ca6a242d 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Select if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) selectedBeatmapSet = null; - scrollableContent.Clear(false); + ScrollableContent.Clear(false); itemsCache.Invalidate(); scrollPositionCache.Invalidate(); @@ -121,7 +121,7 @@ namespace osu.Game.Screens.Select private readonly Cached itemsCache = new Cached(); private readonly Cached scrollPositionCache = new Cached(); - private readonly Container scrollableContent; + protected readonly Container ScrollableContent; public Bindable RightClickScrollingEnabled = new Bindable(); @@ -151,7 +151,7 @@ namespace osu.Game.Screens.Select Children = new Drawable[] { setPool, - scrollableContent = new Container + ScrollableContent = new Container { RelativeSizeAxes = Axes.X, } @@ -595,7 +595,7 @@ namespace osu.Game.Screens.Select var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first); - foreach (var panel in scrollableContent.Children) + foreach (var panel in ScrollableContent.Children) { if (toDisplay.Remove(panel.Item)) { @@ -622,7 +622,7 @@ namespace osu.Game.Screens.Select panel.Depth = item.CarouselYPosition; panel.Y = item.CarouselYPosition; - scrollableContent.Add(panel); + ScrollableContent.Add(panel); } } @@ -630,7 +630,7 @@ namespace osu.Game.Screens.Select // This is common if a selected/collapsed state has changed. if (revalidateItems) { - foreach (DrawableCarouselItem panel in scrollableContent.Children) + foreach (DrawableCarouselItem panel in ScrollableContent.Children) { panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint); } @@ -638,7 +638,7 @@ namespace osu.Game.Screens.Select // Update externally controlled state of currently visible items (e.g. x-offset and opacity). // This is a per-frame update on all drawable panels. - foreach (DrawableCarouselItem item in scrollableContent.Children) + foreach (DrawableCarouselItem item in ScrollableContent.Children) { updateItem(item); @@ -786,7 +786,7 @@ namespace osu.Game.Screens.Select } currentY += visibleHalfHeight; - scrollableContent.Height = currentY; + ScrollableContent.Height = currentY; if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) { @@ -841,7 +841,7 @@ namespace osu.Game.Screens.Select /// For nested items, the parent of the item to be updated. private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null) { - Vector2 posInScroll = scrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); + Vector2 posInScroll = ScrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); float itemDrawY = posInScroll.Y - visibleUpperBound; float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); From fdd4d95cdc6cdbb209b2dd99d2794dd45e428a4c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 17:24:41 +0900 Subject: [PATCH 3884/6909] Fix difficulties being at incorrect vertical positions after filter is applied --- .../Carousel/DrawableCarouselBeatmapSet.cs | 39 +++++++++++-------- 1 file changed, 23 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index a6cea5ee53..6c35ebfd97 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -171,29 +171,36 @@ namespace osu.Game.Screens.Select.Carousel MovementContainer.MoveToX(-100, 500, Easing.OutExpo); - if (beatmapContainer != null) + updateBeatmapDifficulties(); + } + + private void updateBeatmapDifficulties() + { + var carouselBeatmapSet = (CarouselBeatmapSet)Item; + + var visibleBeatmaps = carouselBeatmapSet.Children + .Where(c => c.Visible) + .ToArray(); + + // if we are already displaying all the correct beatmaps, only run animation updates. + // note that the displayed beatmaps may change due to the applied filter. + // a future optimisation could add/remove only changed difficulties rather than reinitialise. + if (beatmapContainer != null && visibleBeatmaps.Length == beatmapContainer.Count && visibleBeatmaps.All(b => beatmapContainer.Any(c => c.Item == b))) { - // if already loaded, we only need to re-animate. - animateBeatmaps(); + updateBeatmapYPositions(); } else { // on selection we show our child beatmaps. // for now this is a simple drawable construction each selection. // can be improved in the future. - var carouselBeatmapSet = (CarouselBeatmapSet)Item; - - // ToArray() in this line is required due to framework oversight: https://github.com/ppy/osu-framework/pull/3929 - var visibleBeatmaps = carouselBeatmapSet.Children - .Where(c => c.Visible) - .Select(c => c.CreateDrawableRepresentation()) - .ToArray(); beatmapContainer = new Container { X = 100, RelativeSizeAxes = Axes.Both, - ChildrenEnumerable = visibleBeatmaps + // ToArray() in this line is required due to framework oversight: https://github.com/ppy/osu-framework/pull/3929 + ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()).ToArray() }; Logger.Log($"loading {visibleBeatmaps.Length} beatmaps for {Item}"); @@ -205,18 +212,18 @@ namespace osu.Game.Screens.Select.Carousel return; Content.Child = loaded; - animateBeatmaps(); + updateBeatmapYPositions(); }); } - void animateBeatmaps() + void updateBeatmapYPositions() { float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; - foreach (var item in beatmapContainer.Children) + foreach (var panel in beatmapContainer.Children) { - item.MoveToY(yPos, 800, Easing.OutQuint); - yPos += item.Item.TotalHeight + DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; + panel.MoveToY(yPos, 800, Easing.OutQuint); + yPos += panel.Item.TotalHeight + DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; } } } From 4160feb3da3dd6fc5c3154dd964ca2c8d61f92ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 17:26:17 +0900 Subject: [PATCH 3885/6909] Add test specifically for many panels visible --- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 8e4e2ac257..239fa9ec4e 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -41,6 +41,12 @@ namespace osu.Game.Tests.Visual.SongSelect this.rulesets = rulesets; } + [Test] + public void TestManyPanels() + { + loadBeatmaps(count: 5000, randomDifficulties: true); + } + [Test] public void TestKeyRepeat() { @@ -708,7 +714,7 @@ namespace osu.Game.Tests.Visual.SongSelect checkVisibleItemCount(true, 15); } - private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null) + private void loadBeatmaps(List beatmapSets = null, Func initialCriteria = null, Action carouselAdjust = null, int? count = null, bool randomDifficulties = false) { bool changed = false; @@ -720,8 +726,8 @@ namespace osu.Game.Tests.Visual.SongSelect { beatmapSets = new List(); - for (int i = 1; i <= set_count; i++) - beatmapSets.Add(createTestBeatmapSet(i)); + for (int i = 1; i <= (count ?? set_count); i++) + beatmapSets.Add(createTestBeatmapSet(i, randomDifficulties)); } carousel.Filter(initialCriteria?.Invoke() ?? new FilterCriteria()); From f3b937e358a8489d3559e5972010caa193b4655a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 17:33:35 +0900 Subject: [PATCH 3886/6909] Fix masking issues with certain aspect ratio displays --- osu.Game/Screens/Select/BeatmapCarousel.cs | 4 +++- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b6ca6a242d..bc2494e9b7 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -845,7 +845,9 @@ namespace osu.Game.Screens.Select float itemDrawY = posInScroll.Y - visibleUpperBound; float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); - item.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0); + // adjusting the item's overall X position can cause it to become masked away when + // child items (difficulties) are still visible. + item.Header.X = offsetX(dist, visibleHalfHeight) - (parent?.X ?? 0); // We are applying a multiplicative alpha (which is internally done by nesting an // additional container and setting that container's alpha) such that we can diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index b06b60e6c5..a655186986 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -173,7 +173,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Selected(); - Header.MoveToX(-50, 500, Easing.OutExpo); + MovementContainer.MoveToX(-50, 500, Easing.OutExpo); background.Colour = ColourInfo.GradientVertical( new Color4(20, 43, 51, 255), @@ -186,7 +186,7 @@ namespace osu.Game.Screens.Select.Carousel { base.Deselected(); - Header.MoveToX(0, 500, Easing.OutExpo); + MovementContainer.MoveToX(0, 500, Easing.OutExpo); background.Colour = new Color4(20, 43, 51, 255); triangles.Colour = OsuColour.Gray(0.5f); From 9b2ebb8f0f07fef7f781edb9cf60a62a80206976 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 17:45:41 +0900 Subject: [PATCH 3887/6909] Fix main content DelayedLoadUnloadWrapper not getting a valid size before load --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 6c35ebfd97..b54325cf05 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -100,7 +100,8 @@ namespace osu.Game.Screens.Select.Carousel { Direction = FillDirection.Vertical, Padding = new MarginPadding { Top = 5, Left = 18, Right = 10, Bottom = 10 }, - AutoSizeAxes = Axes.Both, + // required to ensure we load as soon as any part of the panel comes on screen + RelativeSizeAxes = Axes.Both, Children = new Drawable[] { new OsuSpriteText From d9a6a6b24543ddc7d1d7a259f37723700473376d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 17:55:56 +0900 Subject: [PATCH 3888/6909] Split content out into own class --- .../Carousel/DrawableCarouselBeatmapSet.cs | 66 +------------ ...nelBackground.cs => SetPanelBackground.cs} | 4 +- .../Select/Carousel/SetPanelContent.cs | 93 +++++++++++++++++++ 3 files changed, 98 insertions(+), 65 deletions(-) rename osu.Game/Screens/Select/Carousel/{PanelBackground.cs => SetPanelBackground.cs} (95%) create mode 100644 osu.Game/Screens/Select/Carousel/SetPanelContent.cs diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index b54325cf05..ad5c4e5e4f 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -10,16 +10,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; using osu.Game.Collections; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; -using osuTK; namespace osu.Game.Screens.Select.Carousel { @@ -85,7 +80,7 @@ namespace osu.Game.Screens.Select.Carousel new DelayedLoadUnloadWrapper(() => { Logger.Log($"loaded background item {beatmapSet}"); - var background = new PanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) + var background = new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { RelativeSizeAxes = Axes.Both, }; @@ -96,52 +91,8 @@ namespace osu.Game.Screens.Select.Carousel }, 300, 5000), new DelayedLoadUnloadWrapper(() => { - var mainFlow = new FillFlowContainer - { - Direction = FillDirection.Vertical, - Padding = new MarginPadding { Top = 5, Left = 18, Right = 10, Bottom = 10 }, - // required to ensure we load as soon as any part of the panel comes on screen - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new OsuSpriteText - { - Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)), - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), - Shadow = true, - }, - new OsuSpriteText - { - Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)), - Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), - Shadow = true, - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 5 }, - Children = new Drawable[] - { - new BeatmapSetOnlineStatusPill - { - Origin = Anchor.CentreLeft, - Anchor = Anchor.CentreLeft, - Margin = new MarginPadding { Right = 5 }, - TextSize = 11, - TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, - Status = beatmapSet.Status - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(3), - ChildrenEnumerable = getDifficultyIcons(), - }, - } - } - } - }; + // main content split into own class to reduce allocation before load operation triggers. + var mainFlow = new SetPanelContent((CarouselBeatmapSet)Item); mainFlow.OnLoadComplete += d => d.FadeInFromZero(1000, Easing.OutQuint); @@ -229,17 +180,6 @@ namespace osu.Game.Screens.Select.Carousel } } - private const int maximum_difficulty_icons = 18; - - private IEnumerable getDifficultyIcons() - { - var beatmaps = ((CarouselBeatmapSet)Item).Beatmaps.ToList(); - - return beatmaps.Count > maximum_difficulty_icons - ? (IEnumerable)beatmaps.GroupBy(b => b.Beatmap.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key)) - : beatmaps.Select(b => new FilterableDifficultyIcon(b)); - } - public MenuItem[] ContextMenuItems { get diff --git a/osu.Game/Screens/Select/Carousel/PanelBackground.cs b/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs similarity index 95% rename from osu.Game/Screens/Select/Carousel/PanelBackground.cs rename to osu.Game/Screens/Select/Carousel/SetPanelBackground.cs index 587aa0a74e..6af0cbf4ab 100644 --- a/osu.Game/Screens/Select/Carousel/PanelBackground.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs @@ -9,9 +9,9 @@ using osuTK.Graphics; namespace osu.Game.Screens.Select.Carousel { - public class PanelBackground : BufferedContainer + public class SetPanelBackground : BufferedContainer { - public PanelBackground(WorkingBeatmap working) + public SetPanelBackground(WorkingBeatmap working) { CacheDrawnFrameBuffer = true; RedrawOnScale = false; diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs new file mode 100644 index 0000000000..4e8d27f14d --- /dev/null +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Select.Carousel +{ + public class SetPanelContent : CompositeDrawable + { + private readonly CarouselBeatmapSet carouselSet; + + public SetPanelContent(CarouselBeatmapSet carouselSet) + { + this.carouselSet = carouselSet; + + // required to ensure we load as soon as any part of the panel comes on screen + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + var beatmapSet = carouselSet.BeatmapSet; + + InternalChild = new FillFlowContainer + { + // required to ensure we load as soon as any part of the panel comes on screen + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Padding = new MarginPadding { Top = 5, Left = 18, Right = 10, Bottom = 10 }, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)), + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), + Shadow = true, + }, + new OsuSpriteText + { + Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)), + Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), + Shadow = true, + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 5 }, + Children = new Drawable[] + { + new BeatmapSetOnlineStatusPill + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 5 }, + TextSize = 11, + TextPadding = new MarginPadding { Horizontal = 8, Vertical = 2 }, + Status = beatmapSet.Status + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(3), + ChildrenEnumerable = getDifficultyIcons(), + }, + } + } + } + }; + } + + private const int maximum_difficulty_icons = 18; + + private IEnumerable getDifficultyIcons() + { + var beatmaps = carouselSet.Beatmaps.ToList(); + + return beatmaps.Count > maximum_difficulty_icons + ? (IEnumerable)beatmaps.GroupBy(b => b.Beatmap.Ruleset).Select(group => new FilterableGroupedDifficultyIcon(group.ToList(), group.Key)) + : beatmaps.Select(b => new FilterableDifficultyIcon(b)); + } + } +} From b1ddb08a4efefd378d65ec8756e21c597b089030 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 17:57:38 +0900 Subject: [PATCH 3889/6909] Fix right click context menus appearing in incorrect locations --- osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index bafb338a04..a66135196a 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Events; +using osuTK; namespace osu.Game.Screens.Select.Carousel { @@ -27,6 +28,9 @@ namespace osu.Game.Screens.Select.Carousel protected readonly Container MovementContainer; + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + Header.ReceivePositionalInputAt(screenSpacePos); + private CarouselItem item; public CarouselItem Item From 69650c16fc539d66a589054792a1b5524d92a842 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 18:13:36 +0900 Subject: [PATCH 3890/6909] Simplify vertical position calculations by including spacing in height definition --- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 2 +- .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 9 +++++++-- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index bc2494e9b7..955693a67c 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -775,7 +775,7 @@ namespace osu.Game.Screens.Select break; } - scrollTarget += b.TotalHeight + DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; + scrollTarget += b.TotalHeight; } } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index f2f8cb9bd9..7935debac7 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Select.Carousel switch (State.Value) { case CarouselItemState.Selected: - return DrawableCarouselBeatmapSet.HEIGHT + Children.Count(c => c.Visible) * (DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING + DrawableCarouselBeatmap.HEIGHT); + return DrawableCarouselBeatmapSet.HEIGHT + Children.Count(c => c.Visible) * DrawableCarouselBeatmap.HEIGHT; default: return DrawableCarouselBeatmapSet.HEIGHT; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index a655186986..49a370724e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -33,7 +33,12 @@ namespace osu.Game.Screens.Select.Carousel { public const float CAROUSEL_BEATMAP_SPACING = 5; - public const float HEIGHT = MAX_HEIGHT * 0.6f; // TODO: add once base class is fixed + CAROUSEL_BEATMAP_SPACING; + /// + /// The height of a carousel beatmap, including vertical spacing. + /// + public const float HEIGHT = height + CAROUSEL_BEATMAP_SPACING; + + private const float height = MAX_HEIGHT * 0.6f; private readonly BeatmapInfo beatmap; @@ -70,7 +75,7 @@ namespace osu.Game.Screens.Select.Carousel [BackgroundDependencyLoader(true)] private void load(BeatmapManager manager, SongSelect songSelect) { - Header.Height = HEIGHT; + Header.Height = height; if (songSelect != null) { diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index ad5c4e5e4f..68fea14757 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -175,7 +175,7 @@ namespace osu.Game.Screens.Select.Carousel foreach (var panel in beatmapContainer.Children) { panel.MoveToY(yPos, 800, Easing.OutQuint); - yPos += panel.Item.TotalHeight + DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; + yPos += panel.Item.TotalHeight; } } } From 3d9ea852ecf48cf40f8669665815b8c2bfb18abb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 18:14:23 +0900 Subject: [PATCH 3891/6909] Remove masking override (no longer needed as our size is now correct) --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 68fea14757..b80e789429 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -8,7 +8,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; using osu.Framework.Logging; using osu.Game.Beatmaps; @@ -22,9 +21,6 @@ namespace osu.Game.Screens.Select.Carousel { public const float HEIGHT = MAX_HEIGHT; - // TODO: don't do this. need to split out the base class' style so our height isn't fixed to the panel display height (and autosize?). - protected override bool ComputeIsMaskedAway(RectangleF maskingBounds) => false; - private Action restoreHiddenRequested; private Action viewDetails; From 83358d487fb5770877948e671621eada6cc91abb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 18:18:22 +0900 Subject: [PATCH 3892/6909] Remove logging --- osu.Game/Screens/Select/BeatmapCarousel.cs | 20 ++++++++----------- .../Carousel/DrawableCarouselBeatmapSet.cs | 5 ----- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 955693a67c..e53beaef0b 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -1,30 +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; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using System; using System.Collections.Generic; -using System.Linq; -using osu.Game.Configuration; -using osuTK.Input; -using osu.Framework.Utils; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Caching; -using osu.Framework.Threading; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Framework.Logging; +using osu.Framework.Threading; +using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Extensions; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osu.Game.Input.Bindings; using osu.Game.Screens.Select.Carousel; +using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Select { @@ -589,8 +588,6 @@ namespace osu.Game.Screens.Select // This involves fetching new items from the pool, returning no-longer required items. if (revalidateItems || newDisplayRange != displayedRange) { - Logger.Log("revalidation requested"); - displayedRange = newDisplayRange; var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first); @@ -616,7 +613,6 @@ namespace osu.Game.Screens.Select // Add those items within the previously found index range that should be displayed. foreach (var item in toDisplay) { - Logger.Log($"getting panel for {item} from pool"); var panel = setPool.Get(p => p.Item = item); panel.Depth = item.CarouselYPosition; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index b80e789429..bf76adc6ac 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Graphics.UserInterface; @@ -70,12 +69,10 @@ namespace osu.Game.Screens.Select.Carousel if (Item == null) return; - Logger.Log($"updating item {beatmapSet}"); Header.Children = new Drawable[] { new DelayedLoadUnloadWrapper(() => { - Logger.Log($"loaded background item {beatmapSet}"); var background = new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { RelativeSizeAxes = Axes.Both, @@ -151,8 +148,6 @@ namespace osu.Game.Screens.Select.Carousel ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()).ToArray() }; - Logger.Log($"loading {visibleBeatmaps.Length} beatmaps for {Item}"); - LoadComponentAsync(beatmapContainer, loaded => { // make sure the pooled target hasn't changed. From 4f4f2225143c425e9ec9f35592adddffa16284f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 18:28:28 +0900 Subject: [PATCH 3893/6909] Remove unnecessary fade (already applied by base DrawableCarouselItem) --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index bf76adc6ac..aae4d0df5d 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -103,10 +103,7 @@ namespace osu.Game.Screens.Select.Carousel if (beatmapContainer != null) { foreach (var beatmap in beatmapContainer) - { beatmap.MoveToY(0, 800, Easing.OutQuint); - beatmap.FadeOut(80); - } } } From 40a0ab7aaaf1139fbcf068ad0e7d358772c43118 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 18:33:31 +0900 Subject: [PATCH 3894/6909] Avoid allocating CarouselItems for bounds checks --- osu.Game/Screens/Select/BeatmapCarousel.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index e53beaef0b..340f30c120 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -646,19 +646,24 @@ namespace osu.Game.Screens.Select } } + private readonly CarouselBoundsItem carouselBoundsItem = new CarouselBoundsItem(); + private (int firstIndex, int lastIndex) getDisplayRange() { // Find index range of all items that should be on-screen - // TODO: reduce allocs of CarouselBoundsItem. - int firstIndex = visibleItems.BinarySearch(new CarouselBoundsItem(visibleUpperBound - distance_offscreen_to_preload)); + carouselBoundsItem.CarouselYPosition = visibleUpperBound - distance_offscreen_to_preload; + int firstIndex = visibleItems.BinarySearch(carouselBoundsItem); if (firstIndex < 0) firstIndex = ~firstIndex; - int lastIndex = visibleItems.BinarySearch(new CarouselBoundsItem(visibleBottomBound + distance_offscreen_to_preload)); + + carouselBoundsItem.CarouselYPosition = visibleBottomBound + distance_offscreen_to_preload; + int lastIndex = visibleItems.BinarySearch(carouselBoundsItem); if (lastIndex < 0) lastIndex = ~lastIndex; // as we can't be 100% sure on the size of individual carousel drawables, // always play it safe and extend bounds by one. firstIndex = Math.Max(0, firstIndex - 1); lastIndex = Math.Min(visibleItems.Count, lastIndex + 1); + return (firstIndex, lastIndex); } @@ -856,11 +861,6 @@ namespace osu.Game.Screens.Select /// private class CarouselBoundsItem : CarouselItem { - public CarouselBoundsItem(in float pos) - { - CarouselYPosition = pos; - } - public override DrawableCarouselItem CreateDrawableRepresentation() => throw new NotImplementedException(); } From a1801f8ae40f7e65da4305f7ab08dbb91ce0f7f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 18:33:55 +0900 Subject: [PATCH 3895/6909] Unmark todo for now --- 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 340f30c120..dcebabae4d 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -604,7 +604,7 @@ namespace osu.Game.Screens.Select // remove but only if too far off-screen if (panel.Y < visibleUpperBound - distance_offscreen_before_unload || panel.Y > visibleBottomBound + distance_offscreen_before_unload) { - // todo: may want a fade effect here (could be seen if a huge change happens, like a set with 20 difficulties becomes selected). + // may want a fade effect here (could be seen if a huge change happens, like a set with 20 difficulties becomes selected). panel.ClearTransforms(); panel.Expire(); } From 2346644c048b781fa6eae9fb144d038e9ed1ee40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 18:47:35 +0900 Subject: [PATCH 3896/6909] Switch DelayedLoadUnloadWrappers to DelayedLoadWrappers Due to pooling usage, there is no time we need to unload. Switching to DelayedLoadWrapper cleans up the code and reduces overhead substantially. --- .../Carousel/DrawableCarouselBeatmapSet.cs | 34 ++++++++----------- 1 file changed, 15 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index aae4d0df5d..42d073976e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -69,29 +69,25 @@ namespace osu.Game.Screens.Select.Carousel if (Item == null) return; + DelayedLoadWrapper background; + DelayedLoadWrapper mainFlow; + Header.Children = new Drawable[] { - new DelayedLoadUnloadWrapper(() => + background = new DelayedLoadWrapper(new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { - var background = new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) - { - RelativeSizeAxes = Axes.Both, - }; - - background.OnLoadComplete += d => d.FadeInFromZero(1000, Easing.OutQuint); - - return background; - }, 300, 5000), - new DelayedLoadUnloadWrapper(() => - { - // main content split into own class to reduce allocation before load operation triggers. - var mainFlow = new SetPanelContent((CarouselBeatmapSet)Item); - - mainFlow.OnLoadComplete += d => d.FadeInFromZero(1000, Easing.OutQuint); - - return mainFlow; - }, 100, 5000) + RelativeSizeAxes = Axes.Both, + }, 300), + mainFlow = new DelayedLoadWrapper(new SetPanelContent((CarouselBeatmapSet)Item), 100), }; + + background.DelayedLoadComplete += fadeContentIn; + mainFlow.DelayedLoadComplete += fadeContentIn; + } + + private void fadeContentIn(Drawable d) + { + d.FadeInFromZero(1000, Easing.OutQuint); } protected override void Deselected() From 834b0186f41f64b12a23f70abfdb34bc923d5074 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 18:50:10 +0900 Subject: [PATCH 3897/6909] Adjust fade duration to be slightly shorter --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 42d073976e..e83c460f15 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -85,10 +85,7 @@ namespace osu.Game.Screens.Select.Carousel mainFlow.DelayedLoadComplete += fadeContentIn; } - private void fadeContentIn(Drawable d) - { - d.FadeInFromZero(1000, Easing.OutQuint); - } + private void fadeContentIn(Drawable d) => d.FadeInFromZero(750, Easing.OutQuint); protected override void Deselected() { From 8eca28e8bc3986ed99c78f54fbe6769dca3af6cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 19:10:35 +0900 Subject: [PATCH 3898/6909] Add comment about off-screen loading --- osu.Game/Screens/Select/BeatmapCarousel.cs | 24 +++++++++++----------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index dcebabae4d..21777c5c7c 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -74,6 +74,18 @@ namespace osu.Game.Screens.Select public override bool PropagatePositionalInputSubTree => AllowSelection; public override bool PropagateNonPositionalInputSubTree => AllowSelection; + private (int first, int last) displayedRange; + + /// + /// Extend the range to retain already loaded pooled drawables. + /// + private const float distance_offscreen_before_unload = 1024; + + /// + /// Extend the range to update positions / retrieve pooled drawables outside of visible range. + /// + private const float distance_offscreen_to_preload = 512; // todo: adjust this appropriately once we can make set panel contents load while off-screen. + /// /// Whether carousel items have completed asynchronously loaded. /// @@ -557,18 +569,6 @@ namespace osu.Game.Screens.Select #endregion - private (int first, int last) displayedRange; - - /// - /// Extend the range to retain already loaded pooled drawables. - /// - private const float distance_offscreen_before_unload = 1024; - - /// - /// Extend the range to update positions / retrieve pooled drawables outside of visible range. - /// - private const float distance_offscreen_to_preload = 256; - protected override void Update() { base.Update(); From 37daefc2b59da55abe3fcc9a6fc62cf7deeee623 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 19:12:33 +0900 Subject: [PATCH 3899/6909] Remove outdated comment --- osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index a66135196a..df8d5cd565 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs @@ -99,8 +99,6 @@ namespace osu.Game.Screens.Select.Carousel protected override void Update() { base.Update(); - - // avoid using fill flow for performance reasons. header size doesn't change after load. Content.Y = Header.Height; } From 5d11db7753212439c219b7c60863eb4bfb6680cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 19:15:56 +0900 Subject: [PATCH 3900/6909] Locallise ChildItems to DrawableCarouselBeatmapSet for clarity --- osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 2 +- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 2 +- osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs | 4 ---- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 239fa9ec4e..0ba8cfeb28 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -923,7 +923,7 @@ namespace osu.Game.Tests.Visual.SongSelect if (item is DrawableCarouselBeatmapSet set) { - foreach (var difficulty in set.ChildItems) + foreach (var difficulty in set.DrawableBeatmaps) yield return difficulty; } } diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 21777c5c7c..c011ea7e05 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -640,7 +640,7 @@ namespace osu.Game.Screens.Select if (item is DrawableCarouselBeatmapSet set) { - foreach (var diff in set.ChildItems) + foreach (var diff in set.DrawableBeatmaps) updateItem(diff, item); } } diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index e83c460f15..79387a9905 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } - public override IEnumerable ChildItems => beatmapContainer?.Children ?? base.ChildItems; + public IEnumerable DrawableBeatmaps => beatmapContainer?.Children ?? Enumerable.Empty(); private BeatmapSetInfo beatmapSet => (Item as CarouselBeatmapSet)?.BeatmapSet; diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs index df8d5cd565..cde3edad39 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselItem.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.Collections.Generic; using System.Diagnostics; -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -62,8 +60,6 @@ namespace osu.Game.Screens.Select.Carousel } } - public virtual IEnumerable ChildItems => Enumerable.Empty(); - protected DrawableCarouselItem() { RelativeSizeAxes = Axes.X; From 75b6a5e17e23377b33f5ea463ecff07e40ac5b56 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 19:17:23 +0900 Subject: [PATCH 3901/6909] Remove unnecessary hack (fixed via framework update) --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 79387a9905..cd1c0a08c7 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -113,9 +113,7 @@ namespace osu.Game.Screens.Select.Carousel { var carouselBeatmapSet = (CarouselBeatmapSet)Item; - var visibleBeatmaps = carouselBeatmapSet.Children - .Where(c => c.Visible) - .ToArray(); + var visibleBeatmaps = carouselBeatmapSet.Children.Where(c => c.Visible).ToArray(); // if we are already displaying all the correct beatmaps, only run animation updates. // note that the displayed beatmaps may change due to the applied filter. @@ -129,13 +127,11 @@ namespace osu.Game.Screens.Select.Carousel // on selection we show our child beatmaps. // for now this is a simple drawable construction each selection. // can be improved in the future. - beatmapContainer = new Container { X = 100, RelativeSizeAxes = Axes.Both, - // ToArray() in this line is required due to framework oversight: https://github.com/ppy/osu-framework/pull/3929 - ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()).ToArray() + ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()) }; LoadComponentAsync(beatmapContainer, loaded => From 3d416f4d6475c815424e71a8e97a34cb6b933b0a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 19:20:46 +0900 Subject: [PATCH 3902/6909] Clean up beatmapSet resolution in DrawableCarouselBeatmapSet --- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index cd1c0a08c7..93dc79242e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -34,10 +35,10 @@ namespace osu.Game.Screens.Select.Carousel public IEnumerable DrawableBeatmaps => beatmapContainer?.Children ?? Enumerable.Empty(); - private BeatmapSetInfo beatmapSet => (Item as CarouselBeatmapSet)?.BeatmapSet; - private Container beatmapContainer; + private BeatmapSetInfo beatmapSet; + [Resolved] private BeatmapManager manager { get; set; } @@ -69,6 +70,8 @@ namespace osu.Game.Screens.Select.Carousel if (Item == null) return; + beatmapSet = ((CarouselBeatmapSet)Item).BeatmapSet; + DelayedLoadWrapper background; DelayedLoadWrapper mainFlow; @@ -161,6 +164,8 @@ namespace osu.Game.Screens.Select.Carousel { get { + Debug.Assert(beatmapSet != null); + List items = new List(); if (Item.State.Value == CarouselItemState.NotSelected) @@ -189,6 +194,8 @@ namespace osu.Game.Screens.Select.Carousel private MenuItem createCollectionMenuItem(BeatmapCollection collection) { + Debug.Assert(beatmapSet != null); + TernaryState state; var countExisting = beatmapSet.Beatmaps.Count(b => collection.Beatmaps.Contains(b)); From 8057ea1097b8d3fe570a4183d40e3369ecb82ee8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 20:50:36 +0900 Subject: [PATCH 3903/6909] Fix formatting issues --- osu.Game/Screens/Select/Carousel/CarouselHeader.cs | 10 ++++++++-- .../Select/Carousel/FilterableDifficultyIcon.cs | 2 +- .../Select/Carousel/FilterableGroupedDifficultyIcon.cs | 2 +- 3 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index f59cccd8b6..f1120f55a6 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -77,7 +77,10 @@ namespace osu.Game.Screens.Select.Carousel BorderContainer.BorderThickness = 0; BorderContainer.EdgeEffect = new EdgeEffectParameters { - Type = EdgeEffectType.Shadow, Offset = new Vector2(1), Radius = 10, Colour = Color4.Black.Opacity(100), + Type = EdgeEffectType.Shadow, + Offset = new Vector2(1), + Radius = 10, + Colour = Color4.Black.Opacity(100), }; break; @@ -85,7 +88,10 @@ namespace osu.Game.Screens.Select.Carousel BorderContainer.BorderThickness = 2.5f; BorderContainer.EdgeEffect = new EdgeEffectParameters { - Type = EdgeEffectType.Glow, Colour = new Color4(130, 204, 255, 150), Radius = 20, Roundness = 10, + Type = EdgeEffectType.Glow, + Colour = new Color4(130, 204, 255, 150), + Radius = 20, + Roundness = 10, }; break; } diff --git a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs index 591e9fea22..aa832623fe 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs @@ -29,4 +29,4 @@ namespace osu.Game.Screens.Select.Carousel return true; } } -} \ No newline at end of file +} diff --git a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs index 73b5781a37..31a651d2c8 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs @@ -35,4 +35,4 @@ namespace osu.Game.Screens.Select.Carousel this.FadeTo(1 - 0.9f * ((float)Items.Count(i => i.Filtered.Value) / Items.Count), 100); } } -} \ No newline at end of file +} From e662dc5342b26ecedd265b55c121a058006e2ad3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 13 Oct 2020 20:57:26 +0900 Subject: [PATCH 3904/6909] Add missing licence headers --- osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs | 3 +++ .../Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs | 3 +++ osu.Game/Screens/Select/Carousel/SetPanelBackground.cs | 3 +++ 3 files changed, 9 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs index aa832623fe..dce593b85c 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.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.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Events; diff --git a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs index 31a651d2c8..d2f9ed3a6a 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableGroupedDifficultyIcon.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 System.Linq; using osu.Framework.Graphics; diff --git a/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs b/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs index 6af0cbf4ab..25139b27db 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelBackground.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelBackground.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.Colour; using osu.Framework.Graphics.Containers; From 663b8069746a5b79c1acc0b276a31be0e8710825 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 13 Oct 2020 17:45:40 +0200 Subject: [PATCH 3905/6909] move ModSettingsContainer to seperate component --- .../Overlays/Mods/CModSettingsContainer.cs | 71 +++++++++++++++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 56 +-------------- 2 files changed, 74 insertions(+), 53 deletions(-) create mode 100644 osu.Game/Overlays/Mods/CModSettingsContainer.cs diff --git a/osu.Game/Overlays/Mods/CModSettingsContainer.cs b/osu.Game/Overlays/Mods/CModSettingsContainer.cs new file mode 100644 index 0000000000..a5f33e46c4 --- /dev/null +++ b/osu.Game/Overlays/Mods/CModSettingsContainer.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.Collections.Generic; +using System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input.Events; +using osu.Game.Configuration; +using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Mods; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Mods +{ + public class CModSettingsContainer : Container + { + private readonly FillFlowContainer modSettingsContent; + + public CModSettingsContainer() + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = new Color4(0, 0, 0, 192) + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = modSettingsContent = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Padding = new MarginPadding(20), + } + } + }; + } + + ///Bool indicating whether any settings are listed + public bool UpdateModSettings(ValueChangedEvent> mods) + { + modSettingsContent.Clear(); + + foreach (var mod in mods.NewValue) + { + var settings = mod.CreateSettingsControls().ToList(); + if (settings.Count > 0) + modSettingsContent.Add(new ModControlSection(mod, settings)); + } + + bool hasSettings = modSettingsContent.Count > 0; + + if (!hasSettings) + Hide(); + + return hasSettings; + } + + protected override bool OnMouseDown(MouseDownEvent e) => true; + protected override bool OnHover(HoverEvent e) => true; + } +} diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index f74f40b9b4..b9a37094d7 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; @@ -45,9 +44,7 @@ namespace osu.Game.Overlays.Mods protected readonly FillFlowContainer ModSectionsContainer; - protected readonly FillFlowContainer ModSettingsContent; - - protected readonly Container ModSettingsContainer; + protected readonly CModSettingsContainer ModSettingsContainer; public readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); @@ -284,7 +281,7 @@ namespace osu.Game.Overlays.Mods }, }, }, - ModSettingsContainer = new MouseInputAbsorbingContainer + ModSettingsContainer = new CModSettingsContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, @@ -292,27 +289,6 @@ namespace osu.Game.Overlays.Mods Width = 0.25f, Alpha = 0, X = -100, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = new Color4(0, 0, 0, 192) - }, - new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - Child = ModSettingsContent = new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 10f), - Padding = new MarginPadding(20), - } - } - } } }; } @@ -424,7 +400,7 @@ namespace osu.Game.Overlays.Mods updateMods(); - updateModSettings(mods); + CustomiseButton.Enabled.Value = ModSettingsContainer.UpdateModSettings(mods); } private void updateMods() @@ -445,25 +421,6 @@ namespace osu.Game.Overlays.Mods MultiplierLabel.FadeColour(Color4.White, 200); } - private void updateModSettings(ValueChangedEvent> selectedMods) - { - ModSettingsContent.Clear(); - - foreach (var mod in selectedMods.NewValue) - { - var settings = mod.CreateSettingsControls().ToList(); - if (settings.Count > 0) - ModSettingsContent.Add(new ModControlSection(mod, settings)); - } - - bool hasSettings = ModSettingsContent.Count > 0; - - CustomiseButton.Enabled.Value = hasSettings; - - if (!hasSettings) - ModSettingsContainer.Hide(); - } - private void modButtonPressed(Mod selectedMod) { if (selectedMod != null) @@ -495,12 +452,5 @@ namespace osu.Game.Overlays.Mods } #endregion - - protected class MouseInputAbsorbingContainer : Container - { - protected override bool OnMouseDown(MouseDownEvent e) => true; - - protected override bool OnHover(HoverEvent e) => true; - } } } From 28d3295f9f25df1cf60f77dd42353ef736390d6f Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 13 Oct 2020 19:20:15 +0200 Subject: [PATCH 3906/6909] Test Class Fixes --- .../Visual/UserInterface/TestSceneModSettings.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index a31e244ca5..6a46ff2666 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; @@ -30,8 +31,6 @@ namespace osu.Game.Tests.Visual.UserInterface private readonly Mod testCustomisableAutoOpenMod = new TestModCustomisable2(); - private readonly Mod testCustomisableMenuCoveredMod = new TestModCustomisable1(); - [SetUp] public void SetUp() => Schedule(() => { @@ -107,8 +106,8 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("change mod settings menu width to full screen", () => modSelect.SetModSettingsWidth(1.0f)); AddStep("select cm2", () => modSelect.SelectMod(testCustomisableAutoOpenMod)); AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.Alpha == 1); - AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMenuCoveredMod))); - AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMenuCoveredMod).IsHovered); + AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMod))); + AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMod).IsHovered); AddStep("left click mod", () => InputManager.Click(MouseButton.Left)); AddAssert("only cm2 is active", () => SelectedMods.Value.Count == 1); AddStep("right click mod", () => InputManager.Click(MouseButton.Right)); @@ -143,19 +142,14 @@ namespace osu.Game.Tests.Visual.UserInterface public ModButton GetModButton(Mod mod) { - return ModSectionsContainer.Children.Single(s => s.ModType == mod.Type) - .ButtonsContainer.OfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())); + return ModSectionsContainer.ChildrenOfType().Single(b => b.Mods.Any(m => m.GetType() == mod.GetType())); } public void SelectMod(Mod mod) => GetModButton(mod).SelectNext(1); - public float SetModSettingsWidth(float newWidth) - { - float oldWidth = ModSettingsContainer.Width; + public void SetModSettingsWidth(float newWidth) => ModSettingsContainer.Width = newWidth; - return oldWidth; - } } public class TestRulesetInfo : RulesetInfo From 3fd913b13f5a4b317aa5edd45b5cf00de6d45b21 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 13 Oct 2020 19:25:42 +0200 Subject: [PATCH 3907/6909] rename customisation container class --- ...{CModSettingsContainer.cs => ModCustomisationContainer.cs} | 4 ++-- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game/Overlays/Mods/{CModSettingsContainer.cs => ModCustomisationContainer.cs} (95%) diff --git a/osu.Game/Overlays/Mods/CModSettingsContainer.cs b/osu.Game/Overlays/Mods/ModCustomisationContainer.cs similarity index 95% rename from osu.Game/Overlays/Mods/CModSettingsContainer.cs rename to osu.Game/Overlays/Mods/ModCustomisationContainer.cs index a5f33e46c4..487d92882a 100644 --- a/osu.Game/Overlays/Mods/CModSettingsContainer.cs +++ b/osu.Game/Overlays/Mods/ModCustomisationContainer.cs @@ -16,11 +16,11 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Mods { - public class CModSettingsContainer : Container + public class ModCustomisationContainer : Container { private readonly FillFlowContainer modSettingsContent; - public CModSettingsContainer() + public ModCustomisationContainer() { Children = new Drawable[] { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b9a37094d7..b1ffd26bb9 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Mods protected readonly FillFlowContainer ModSectionsContainer; - protected readonly CModSettingsContainer ModSettingsContainer; + protected readonly ModCustomisationContainer ModSettingsContainer; public readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); @@ -281,7 +281,7 @@ namespace osu.Game.Overlays.Mods }, }, }, - ModSettingsContainer = new CModSettingsContainer + ModSettingsContainer = new ModCustomisationContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, From 07e6609e6de38054cbc5e9c64e04f8317da75e8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 14:15:53 +0900 Subject: [PATCH 3908/6909] Disable difficulty calculation for set-level difficulty icons --- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 15 ++++++++++++--- .../Select/Carousel/FilterableDifficultyIcon.cs | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 45327d4514..a1d5e33d1e 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -47,7 +47,10 @@ namespace osu.Game.Beatmaps.Drawables private readonly IReadOnlyList mods; private readonly bool shouldShowTooltip; - private readonly IBindable difficultyBindable = new Bindable(); + + private readonly bool performBackgroundDifficultyLookup; + + private readonly Bindable difficultyBindable = new Bindable(); private Drawable background; @@ -70,10 +73,12 @@ namespace osu.Game.Beatmaps.Drawables /// /// The beatmap to show the difficulty of. /// Whether to display a tooltip when hovered. - public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true) + /// Whether to perform difficulty lookup (including calculation if necessary). + public DifficultyIcon([NotNull] BeatmapInfo beatmap, bool shouldShowTooltip = true, bool performBackgroundDifficultyLookup = true) { this.beatmap = beatmap ?? throw new ArgumentNullException(nameof(beatmap)); this.shouldShowTooltip = shouldShowTooltip; + this.performBackgroundDifficultyLookup = performBackgroundDifficultyLookup; AutoSizeAxes = Axes.Both; @@ -112,9 +117,13 @@ namespace osu.Game.Beatmaps.Drawables // the null coalesce here is only present to make unit tests work (ruleset dlls aren't copied correctly for testing at the moment) Icon = (ruleset ?? beatmap.Ruleset)?.CreateInstance()?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Regular.QuestionCircle } }, - new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0), }; + if (performBackgroundDifficultyLookup) + iconContainer.Add(new DelayedLoadUnloadWrapper(() => new DifficultyRetriever(beatmap, ruleset, mods) { StarDifficulty = { BindTarget = difficultyBindable } }, 0)); + else + difficultyBindable.Value = new StarDifficulty(beatmap.StarDifficulty, 0); + difficultyBindable.BindValueChanged(difficulty => background.Colour = colours.ForDifficultyRating(difficulty.NewValue.DifficultyRating)); } diff --git a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs index dce593b85c..51fe7796c7 100644 --- a/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs +++ b/osu.Game/Screens/Select/Carousel/FilterableDifficultyIcon.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Select.Carousel public readonly CarouselBeatmap Item; public FilterableDifficultyIcon(CarouselBeatmap item) - : base(item.Beatmap) + : base(item.Beatmap, performBackgroundDifficultyLookup: false) { filtered.BindTo(item.Filtered); filtered.ValueChanged += isFiltered => Schedule(() => this.FadeTo(isFiltered.NewValue ? 0.1f : 1, 100)); From 30e1fce7a4e82c7b595eb25115c9b0c38f0b7da7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 15:10:50 +0900 Subject: [PATCH 3909/6909] Reduce alloc overhead of DrawableCarouselBeatmapSet using new function-based ctor --- .../Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 93dc79242e..703b91c517 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -77,11 +77,11 @@ namespace osu.Game.Screens.Select.Carousel Header.Children = new Drawable[] { - background = new DelayedLoadWrapper(new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) + background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { RelativeSizeAxes = Axes.Both, }, 300), - mainFlow = new DelayedLoadWrapper(new SetPanelContent((CarouselBeatmapSet)Item), 100), + mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100), }; background.DelayedLoadComplete += fadeContentIn; From 24eff8c66d8aab85a6509f9ea095c13cd5e8a09e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 15:13:49 +0900 Subject: [PATCH 3910/6909] Rename container to match "settings" term used everywhere --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- .../{ModCustomisationContainer.cs => ModSettingsContainer.cs} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game/Overlays/Mods/{ModCustomisationContainer.cs => ModSettingsContainer.cs} (95%) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b1ffd26bb9..2d8b4dba7c 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Mods protected readonly FillFlowContainer ModSectionsContainer; - protected readonly ModCustomisationContainer ModSettingsContainer; + protected readonly ModSettingsContainer ModSettingsContainer; public readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); @@ -281,7 +281,7 @@ namespace osu.Game.Overlays.Mods }, }, }, - ModSettingsContainer = new ModCustomisationContainer + ModSettingsContainer = new ModSettingsContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.BottomRight, diff --git a/osu.Game/Overlays/Mods/ModCustomisationContainer.cs b/osu.Game/Overlays/Mods/ModSettingsContainer.cs similarity index 95% rename from osu.Game/Overlays/Mods/ModCustomisationContainer.cs rename to osu.Game/Overlays/Mods/ModSettingsContainer.cs index 487d92882a..0521bc35b8 100644 --- a/osu.Game/Overlays/Mods/ModCustomisationContainer.cs +++ b/osu.Game/Overlays/Mods/ModSettingsContainer.cs @@ -16,11 +16,11 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Mods { - public class ModCustomisationContainer : Container + public class ModSettingsContainer : Container { private readonly FillFlowContainer modSettingsContent; - public ModCustomisationContainer() + public ModSettingsContainer() { Children = new Drawable[] { From 3e326a9234cb99d743f8ae80de42d85b4a43428c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 15:21:28 +0900 Subject: [PATCH 3911/6909] Use bindable flow for event propagation --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 5 +++-- .../Overlays/Mods/ModSettingsContainer.cs | 19 ++++++++++++++++--- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 2d8b4dba7c..31adf47456 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -289,8 +289,11 @@ namespace osu.Game.Overlays.Mods Width = 0.25f, Alpha = 0, X = -100, + SelectedMods = { BindTarget = SelectedMods }, } }; + + ((IBindable)CustomiseButton.Enabled).BindTo(ModSettingsContainer.HasSettingsForSelection); } [BackgroundDependencyLoader(true)] @@ -399,8 +402,6 @@ namespace osu.Game.Overlays.Mods section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList()); updateMods(); - - CustomiseButton.Enabled.Value = ModSettingsContainer.UpdateModSettings(mods); } private void updateMods() diff --git a/osu.Game/Overlays/Mods/ModSettingsContainer.cs b/osu.Game/Overlays/Mods/ModSettingsContainer.cs index 0521bc35b8..b185b56ecd 100644 --- a/osu.Game/Overlays/Mods/ModSettingsContainer.cs +++ b/osu.Game/Overlays/Mods/ModSettingsContainer.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; @@ -18,6 +19,12 @@ namespace osu.Game.Overlays.Mods { public class ModSettingsContainer : Container { + public readonly IBindable> SelectedMods = new Bindable>(Array.Empty()); + + public IBindable HasSettingsForSelection => hasSettingsForSelection; + + private readonly Bindable hasSettingsForSelection = new Bindable(); + private readonly FillFlowContainer modSettingsContent; public ModSettingsContainer() @@ -45,8 +52,14 @@ namespace osu.Game.Overlays.Mods }; } - ///Bool indicating whether any settings are listed - public bool UpdateModSettings(ValueChangedEvent> mods) + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedMods.BindValueChanged(modsChanged, true); + } + + private void modsChanged(ValueChangedEvent> mods) { modSettingsContent.Clear(); @@ -62,7 +75,7 @@ namespace osu.Game.Overlays.Mods if (!hasSettings) Hide(); - return hasSettings; + hasSettingsForSelection.Value = hasSettings; } protected override bool OnMouseDown(MouseDownEvent e) => true; From 4eccb03d71de9dd3ddb8e60c5a854bb92df1515e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Wed, 14 Oct 2020 17:08:14 +0900 Subject: [PATCH 3912/6909] Add copyright notice Co-authored-by: Dean Herbert --- osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs index 421cc0ae04..0f4829028f 100644 --- a/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs +++ b/osu.Game.Rulesets.Mania/MathUtils/LegacySortHelper.cs @@ -12,6 +12,7 @@ namespace osu.Game.Rulesets.Mania.MathUtils /// /// /// Source: https://referencesource.microsoft.com/#mscorlib/system/collections/generic/arraysorthelper.cs + /// Copyright (c) Microsoft Corporation. All rights reserved. /// internal static class LegacySortHelper { From f9bdb664ee7b8fd1c2a041f6d91b92fedca32d3b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 17:09:01 +0900 Subject: [PATCH 3913/6909] Update diffcalc test --- osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs index 2c36e81190..a25551f854 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Mania.Tests { protected override string ResourceAssembly => "osu.Game.Rulesets.Mania"; - [TestCase(2.3683365342338796d, "diffcalc-test")] + [TestCase(2.3449735700206298d, "diffcalc-test")] public void Test(double expected, string name) => base.Test(expected, name); From 3e6ed6c9ffe327c9767b9fb054e396bf1a295b9d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 17:53:28 +0900 Subject: [PATCH 3914/6909] Add support for dual stages (keycoop) and score multiplier --- .../Beatmaps/ManiaBeatmap.cs | 9 ++++- .../Beatmaps/ManiaBeatmapConverter.cs | 6 +++- .../Difficulty/ManiaDifficultyAttributes.cs | 1 + .../Difficulty/ManiaDifficultyCalculator.cs | 33 +++++++++++++++++++ 4 files changed, 47 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs index d1d5adea75..93a9ce3dbd 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmap.cs @@ -21,13 +21,20 @@ namespace osu.Game.Rulesets.Mania.Beatmaps /// public int TotalColumns => Stages.Sum(g => g.Columns); + /// + /// The total number of columns that were present in this before any user adjustments. + /// + public readonly int OriginalTotalColumns; + /// /// Creates a new . /// /// The initial stages. - public ManiaBeatmap(StageDefinition defaultStage) + /// The total number of columns present before any user adjustments. Defaults to the total columns in . + public ManiaBeatmap(StageDefinition defaultStage, int? originalTotalColumns = null) { Stages.Add(defaultStage); + OriginalTotalColumns = originalTotalColumns ?? defaultStage.Columns; } public override IEnumerable GetStatistics() diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index b17ab3f375..757329c525 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps public bool Dual; public readonly bool IsForCurrentRuleset; + private int originalTargetColumns; + // Internal for testing purposes internal FastRandom Random { get; private set; } @@ -65,6 +67,8 @@ namespace osu.Game.Rulesets.Mania.Beatmaps else TargetColumns = Math.Max(4, Math.Min((int)roundedOverallDifficulty + 1, 7)); } + + originalTargetColumns = TargetColumns; } public override bool CanConvert() => Beatmap.HitObjects.All(h => h is IHasXPosition); @@ -81,7 +85,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps protected override Beatmap CreateBeatmap() { - beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }); + beatmap = new ManiaBeatmap(new StageDefinition { Columns = TargetColumns }, originalTargetColumns); if (Dual) beatmap.Stages.Add(new StageDefinition { Columns = TargetColumns }); diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs index 3ff665d2c8..0b58d1efc6 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyAttributes.cs @@ -8,5 +8,6 @@ namespace osu.Game.Rulesets.Mania.Difficulty public class ManiaDifficultyAttributes : DifficultyAttributes { public double GreatHitWindow; + public double ScoreMultiplier; } } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index a3694f354b..356621acda 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -47,6 +47,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, + ScoreMultiplier = getScoreMultiplier(beatmap, mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), Skills = skills }; @@ -93,12 +94,44 @@ namespace osu.Game.Rulesets.Mania.Difficulty new ManiaModKey3(), new ManiaModKey4(), new ManiaModKey5(), + new MultiMod(new ManiaModKey5(), new ManiaModDualStages()), new ManiaModKey6(), + new MultiMod(new ManiaModKey6(), new ManiaModDualStages()), new ManiaModKey7(), + new MultiMod(new ManiaModKey7(), new ManiaModDualStages()), new ManiaModKey8(), + new MultiMod(new ManiaModKey8(), new ManiaModDualStages()), new ManiaModKey9(), + new MultiMod(new ManiaModKey9(), new ManiaModDualStages()), }).ToArray(); } } + + private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods) + { + double scoreMultiplier = 1; + + foreach (var m in mods) + { + switch (m) + { + case ManiaModNoFail _: + case ManiaModEasy _: + case ManiaModHalfTime _: + scoreMultiplier *= 0.5; + break; + } + } + + var maniaBeatmap = (ManiaBeatmap)beatmap; + int diff = maniaBeatmap.TotalColumns - maniaBeatmap.OriginalTotalColumns; + + if (diff > 0) + scoreMultiplier *= 0.9; + else if (diff < 0) + scoreMultiplier *= 0.9 + 0.04 * diff; + + return scoreMultiplier; + } } } From f04aec538fa6286743147eb2e6f7c83c1b6c4a6b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 18:12:19 +0900 Subject: [PATCH 3915/6909] Fix MultiMod throwing exceptions when creating copies --- .../UserInterface/TestSceneModSettings.cs | 18 ++++++++++++++++++ osu.Game/Rulesets/Mods/MultiMod.cs | 4 +++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index c5ce3751ef..0d43be3f65 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -95,6 +95,24 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, copy.SpeedChange.Value)); } + [Test] + public void TestMultiModSettingsUnboundWhenCopied() + { + MultiMod original = null; + MultiMod copy = null; + + AddStep("create mods", () => + { + original = new MultiMod(new OsuModDoubleTime()); + copy = (MultiMod)original.CreateCopy(); + }); + + AddStep("change property", () => ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value = 2); + + AddAssert("original has new value", () => Precision.AlmostEquals(2.0, ((OsuModDoubleTime)original.Mods[0]).SpeedChange.Value)); + AddAssert("copy has original value", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)copy.Mods[0]).SpeedChange.Value)); + } + private void createModSelect() { AddStep("create mod select", () => diff --git a/osu.Game/Rulesets/Mods/MultiMod.cs b/osu.Game/Rulesets/Mods/MultiMod.cs index f7d574d3c7..2107009dbb 100644 --- a/osu.Game/Rulesets/Mods/MultiMod.cs +++ b/osu.Game/Rulesets/Mods/MultiMod.cs @@ -6,7 +6,7 @@ using System.Linq; namespace osu.Game.Rulesets.Mods { - public class MultiMod : Mod + public sealed class MultiMod : Mod { public override string Name => string.Empty; public override string Acronym => string.Empty; @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Mods Mods = mods; } + public override Mod CreateCopy() => new MultiMod(Mods.Select(m => m.CreateCopy()).ToArray()); + public override Type[] IncompatibleMods => Mods.SelectMany(m => m.IncompatibleMods).ToArray(); } } From da8565c0fa653c7b8dee30ec71d8a51d0cdf97f1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 18:28:19 +0900 Subject: [PATCH 3916/6909] Add 10K mod to incompatibility list --- osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs index 13fdd74113..8fd5950dfb 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaKeyMod.cs @@ -39,6 +39,7 @@ namespace osu.Game.Rulesets.Mania.Mods typeof(ManiaModKey7), typeof(ManiaModKey8), typeof(ManiaModKey9), + typeof(ManiaModKey10), }.Except(new[] { GetType() }).ToArray(); } } From ace9fbc8d392c6acae2b7a076b6134d5415d5e68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 18:15:29 +0900 Subject: [PATCH 3917/6909] Confine available area for HUD components to excluse the song progress area --- osu.Game/Screens/Play/HUDOverlay.cs | 53 +++++++++++++++++++++-------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 26aefa138b..f20127bc63 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -80,26 +80,49 @@ namespace osu.Game.Screens.Play visibilityContainer = new Container { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Child = new GridContainer { - HealthDisplay = CreateHealthDisplay(), - topScoreContainer = new Container + RelativeSizeAxes = Axes.Both, + Content = new[] { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] + new Drawable[] { - AccuracyCounter = CreateAccuracyCounter(), - ScoreCounter = CreateScoreCounter(), - ComboCounter = CreateComboCounter(), + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + HealthDisplay = CreateHealthDisplay(), + topScoreContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + AccuracyCounter = CreateAccuracyCounter(), + ScoreCounter = CreateScoreCounter(), + ComboCounter = CreateComboCounter(), + }, + }, + ComboCounter = CreateComboCounter(), + ModDisplay = CreateModsContainer(), + HitErrorDisplay = CreateHitErrorDisplayOverlay(), + PlayerSettingsOverlay = CreatePlayerSettingsOverlay(), + } + }, }, + new Drawable[] + { + Progress = CreateProgress(), + } }, - Progress = CreateProgress(), - ModDisplay = CreateModsContainer(), - HitErrorDisplay = CreateHitErrorDisplayOverlay(), - PlayerSettingsOverlay = CreatePlayerSettingsOverlay(), - } + RowDimensions = new[] + { + new Dimension(GridSizeMode.Distributed), + new Dimension(GridSizeMode.AutoSize) + } + }, }, new FillFlowContainer { From 0cf3e909042f301788d2abd682cc23dfc9013e6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 18:15:58 +0900 Subject: [PATCH 3918/6909] Update SongProgress height based on its dynamic height during resize --- osu.Game/Screens/Play/SongProgress.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/SongProgress.cs b/osu.Game/Screens/Play/SongProgress.cs index aa745f5ba2..acf4640aa4 100644 --- a/osu.Game/Screens/Play/SongProgress.cs +++ b/osu.Game/Screens/Play/SongProgress.cs @@ -70,7 +70,6 @@ namespace osu.Game.Screens.Play public SongProgress() { Masking = true; - Height = bottom_bar_height + graph_height + handle_size.Y + info_height; Children = new Drawable[] { @@ -148,6 +147,8 @@ namespace osu.Game.Screens.Play bar.CurrentTime = gameplayTime; graph.Progress = (int)(graph.ColumnCount * progress); + + Height = bottom_bar_height + graph_height + handle_size.Y + info_height - graph.Y; } private void updateBarVisibility() From a7f8e26e3572f97b9bd085c202dc214e88bcac5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 18:51:53 +0900 Subject: [PATCH 3919/6909] Adjust bottom-right elements positions based on song progress display --- osu.Game/Screens/Play/HUDOverlay.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index f20127bc63..9d7b3f55be 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -63,6 +63,8 @@ namespace osu.Game.Screens.Play private readonly Container topScoreContainer; + private FillFlowContainer bottomRightElements; + private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods) @@ -119,16 +121,16 @@ namespace osu.Game.Screens.Play }, RowDimensions = new[] { - new Dimension(GridSizeMode.Distributed), + new Dimension(), new Dimension(GridSizeMode.AutoSize) } }, }, - new FillFlowContainer + bottomRightElements = new FillFlowContainer { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Position = -new Vector2(5, TwoLayerButton.SIZE_RETRACTED.Y), + X = -5, AutoSizeAxes = Axes.Both, LayoutDuration = fade_duration / 2, LayoutEasing = fade_easing, @@ -209,6 +211,12 @@ namespace osu.Game.Screens.Play replayLoaded.BindValueChanged(replayLoadedValueChanged, true); } + protected override void Update() + { + base.Update(); + bottomRightElements.Y = -Progress.Height; + } + private void replayLoadedValueChanged(ValueChangedEvent e) { PlayerSettingsOverlay.ReplayLoaded = e.NewValue; From d7a52e97fffd13fd780f6d9a8f283c68f3c637c3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 19:03:11 +0900 Subject: [PATCH 3920/6909] Fix multimod difficulty combinations not generating correctly --- ...DifficultyAdjustmentModCombinationsTest.cs | 39 +++++++++++++++++++ .../Difficulty/DifficultyCalculator.cs | 31 +++++++++++++-- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 760a033aff..de0397dc84 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -94,6 +94,38 @@ namespace osu.Game.Tests.NonVisual Assert.IsTrue(combinations[2] is ModIncompatibleWithAofA); } + [Test] + public void TestMultiMod1() + { + var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(4, combinations.Length); + Assert.IsTrue(combinations[0] is ModNoMod); + Assert.IsTrue(combinations[1] is ModA); + Assert.IsTrue(combinations[2] is MultiMod); + Assert.IsTrue(combinations[3] is MultiMod); + + Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA); + Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB); + Assert.IsTrue(((MultiMod)combinations[2]).Mods[2] is ModC); + Assert.IsTrue(((MultiMod)combinations[3]).Mods[0] is ModB); + Assert.IsTrue(((MultiMod)combinations[3]).Mods[1] is ModC); + } + + [Test] + public void TestMultiMod2() + { + var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(3, combinations.Length); + Assert.IsTrue(combinations[0] is ModNoMod); + Assert.IsTrue(combinations[1] is ModA); + Assert.IsTrue(combinations[2] is MultiMod); + + Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModB); + Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA); + } + private class ModA : Mod { public override string Name => nameof(ModA); @@ -112,6 +144,13 @@ namespace osu.Game.Tests.NonVisual public override Type[] IncompatibleMods => new[] { typeof(ModIncompatibleWithAAndB) }; } + private class ModC : Mod + { + public override string Name => nameof(ModC); + public override string Acronym => nameof(ModC); + public override double ScoreMultiplier => 1; + } + private class ModIncompatibleWithA : Mod { public override string Name => $"Incompatible With {nameof(ModA)}"; diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 1902de5bda..9989c750ee 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Difficulty { return createDifficultyAdjustmentModCombinations(Array.Empty(), DifficultyAdjustmentMods).ToArray(); - IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) + static IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) { switch (currentSetCount) { @@ -133,13 +133,36 @@ namespace osu.Game.Rulesets.Difficulty for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++) { var adjustmentMod = adjustmentSet[i]; - 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)) + if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)) + || adjustmentMod.IncompatibleMods.Any(m => m.IsInstanceOfType(c)))) + { + continue; + } + + // Append the new mod. + int newSetCount = currentSetCount; + var newSet = append(currentSet, adjustmentMod, ref newSetCount); + + foreach (var combo in createDifficultyAdjustmentModCombinations(newSet, adjustmentSet, newSetCount, i + 1)) yield return combo; } } + + // Appends a mod to an existing enumerable, returning the result. Recurses for MultiMod. + static IEnumerable append(IEnumerable existing, Mod mod, ref int count) + { + if (mod is MultiMod multi) + { + foreach (var nested in multi.Mods) + existing = append(existing, nested, ref count); + + return existing; + } + + count++; + return existing.Append(mod); + } } /// From 98acf1e31dc86ea3a0872a4eea5f0043ea2ca4b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 19:16:25 +0900 Subject: [PATCH 3921/6909] Make field read only --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 9d7b3f55be..14ceadac81 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Play private readonly Container topScoreContainer; - private FillFlowContainer bottomRightElements; + private readonly FillFlowContainer bottomRightElements; private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; From 60603d2918b81eeecf6efb721913a825079e7664 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 16:45:40 +0900 Subject: [PATCH 3922/6909] Add skin components and interfaces --- osu.Game/Screens/Play/HUD/IComboCounter.cs | 19 +++++++++++++++++++ osu.Game/Skinning/HUDSkinComponent.cs | 22 ++++++++++++++++++++++ osu.Game/Skinning/HUDSkinComponents.cs | 10 ++++++++++ 3 files changed, 51 insertions(+) create mode 100644 osu.Game/Screens/Play/HUD/IComboCounter.cs create mode 100644 osu.Game/Skinning/HUDSkinComponent.cs create mode 100644 osu.Game/Skinning/HUDSkinComponents.cs diff --git a/osu.Game/Screens/Play/HUD/IComboCounter.cs b/osu.Game/Screens/Play/HUD/IComboCounter.cs new file mode 100644 index 0000000000..ff235bf04e --- /dev/null +++ b/osu.Game/Screens/Play/HUD/IComboCounter.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.Bindables; +using osu.Framework.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// An interface providing a set of methods to update a combo counter. + /// + public interface IComboCounter : IDrawable + { + /// + /// The current combo to be displayed. + /// + Bindable Current { get; } + } +} diff --git a/osu.Game/Skinning/HUDSkinComponent.cs b/osu.Game/Skinning/HUDSkinComponent.cs new file mode 100644 index 0000000000..041beb68f2 --- /dev/null +++ b/osu.Game/Skinning/HUDSkinComponent.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; + +namespace osu.Game.Skinning +{ + public class HUDSkinComponent : ISkinComponent + { + public readonly HUDSkinComponents Component; + + public HUDSkinComponent(HUDSkinComponents component) + { + Component = component; + } + + protected virtual string ComponentName => Component.ToString(); + + public string LookupName => + string.Join("/", new[] { "HUD", ComponentName }.Where(s => !string.IsNullOrEmpty(s))); + } +} diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs new file mode 100644 index 0000000000..6f3e2cbaf5 --- /dev/null +++ b/osu.Game/Skinning/HUDSkinComponents.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.Skinning +{ + public enum HUDSkinComponents + { + ComboCounter + } +} From 375146b4898e61e3378e9834a1a024b5c4804529 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 16:45:48 +0900 Subject: [PATCH 3923/6909] Make HUDOverlay test scene skinnable --- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index c192a7b0e0..e2b831b144 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -9,13 +9,15 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Configuration; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneHUDOverlay : OsuManualInputManagerTestScene + public class TestSceneHUDOverlay : SkinnableTestScene { private HUDOverlay hudOverlay; @@ -107,13 +109,20 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create overlay", () => { - Child = hudOverlay = new HUDOverlay(null, null, null, Array.Empty()); + SetContents(() => + { + hudOverlay = new HUDOverlay(null, null, null, Array.Empty()); - // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); + // Add any key just to display the key counter visually. + hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); - action?.Invoke(hudOverlay); + action?.Invoke(hudOverlay); + + return hudOverlay; + }); }); } + + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); } } From f5623ee21e2a229b4de574b961037ba487195bf1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 16:46:13 +0900 Subject: [PATCH 3924/6909] Setup skinnable combo counter component with default implementation --- .../UserInterface/SimpleComboCounter.cs | 3 +- .../Screens/Play/HUD/SkinnableComboCounter.cs | 58 +++++++++++++++++++ osu.Game/Screens/Play/HUDOverlay.cs | 10 +--- 3 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs diff --git a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs b/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs index c9790aed46..59e31eff55 100644 --- a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs +++ b/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs @@ -5,13 +5,14 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Graphics.UserInterface { /// /// Used as an accuracy counter. Represented visually as a percentage. /// - public class SimpleComboCounter : RollingCounter + public class SimpleComboCounter : RollingCounter, IComboCounter { protected override double RollingDuration => 750; diff --git a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs new file mode 100644 index 0000000000..a67953c790 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.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 osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + public class SkinnableComboCounter : SkinnableDrawable, IComboCounter + { + public SkinnableComboCounter() + : base(new HUDSkinComponent(HUDSkinComponents.ComboCounter), createDefault) + { + } + + private IComboCounter skinnedCounter; + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + // todo: unnecessary? + if (skinnedCounter != null) + { + Current.UnbindFrom(skinnedCounter.Current); + } + + base.SkinChanged(skin, allowFallback); + + // temporary layout code, will eventually be replaced by the skin layout system. + if (Drawable is SimpleComboCounter) + { + Drawable.BypassAutoSizeAxes = Axes.X; + Drawable.Anchor = Anchor.TopRight; + Drawable.Origin = Anchor.TopLeft; + Drawable.Margin = new MarginPadding { Top = 5, Left = 20 }; + } + else + { + Drawable.BypassAutoSizeAxes = Axes.X; + Drawable.Anchor = Anchor.BottomLeft; + Drawable.Origin = Anchor.BottomLeft; + Drawable.Margin = new MarginPadding { Top = 5, Left = 20 }; + } + + skinnedCounter = (IComboCounter)Drawable; + + Current.BindTo(skinnedCounter.Current); + } + + private static Drawable createDefault(ISkinComponent skinComponent) => new SimpleComboCounter(); + + public Bindable Current { get; } = new Bindable(); + + public void UpdateCombo(int combo, Color4? hitObjectColour = null) => Current.Value = combo; + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 14ceadac81..ee5b4e3f34 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Play private const Easing fade_easing = Easing.Out; public readonly KeyCounterDisplay KeyCounter; - public readonly RollingCounter ComboCounter; + public readonly SkinnableComboCounter ComboCounter; public readonly ScoreCounter ScoreCounter; public readonly RollingCounter AccuracyCounter; public readonly HealthDisplay HealthDisplay; @@ -275,13 +275,7 @@ namespace osu.Game.Screens.Play Origin = Anchor.TopCentre, }; - protected virtual RollingCounter CreateComboCounter() => new SimpleComboCounter - { - BypassAutoSizeAxes = Axes.X, - Anchor = Anchor.TopRight, - Origin = Anchor.TopLeft, - Margin = new MarginPadding { Top = 5, Left = 20 }, - }; + protected virtual SkinnableComboCounter CreateComboCounter() => new SkinnableComboCounter(); protected virtual HealthDisplay CreateHealthDisplay() => new StandardHealthDisplay { From 899bac6ca535a33b8434a79d941134c874547c68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 17:02:12 +0900 Subject: [PATCH 3925/6909] Rename catch combo counter for clarity --- .../Skinning/CatchLegacySkinTransformer.cs | 2 +- .../{LegacyComboCounter.cs => LegacyCatchComboCounter.cs} | 4 ++-- .../{SimpleComboCounter.cs => DefaultComboCounter.cs} | 0 .../HUD/{StandardComboCounter.cs => LegacyComboCounter.cs} | 0 4 files changed, 3 insertions(+), 3 deletions(-) rename osu.Game.Rulesets.Catch/Skinning/{LegacyComboCounter.cs => LegacyCatchComboCounter.cs} (96%) rename osu.Game/Graphics/UserInterface/{SimpleComboCounter.cs => DefaultComboCounter.cs} (100%) rename osu.Game/Screens/Play/HUD/{StandardComboCounter.cs => LegacyComboCounter.cs} (100%) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 47224bd195..916b4c5192 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Skinning // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. if (this.HasFont(comboFont)) - return new LegacyComboCounter(Source); + return new LegacyCatchComboCounter(Source); break; } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs similarity index 96% rename from osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs rename to osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs index c8abc9e832..34608b07ff 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Skinning /// /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter. /// - public class LegacyComboCounter : CompositeDrawable, ICatchComboCounter + public class LegacyCatchComboCounter : CompositeDrawable, ICatchComboCounter { private readonly LegacyRollingCounter counter; private readonly LegacyRollingCounter explosion; - public LegacyComboCounter(ISkin skin) + public LegacyCatchComboCounter(ISkin skin) { var fontName = skin.GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; var fontOverlap = skin.GetConfig(LegacySetting.ComboOverlap)?.Value ?? -2f; diff --git a/osu.Game/Graphics/UserInterface/SimpleComboCounter.cs b/osu.Game/Graphics/UserInterface/DefaultComboCounter.cs similarity index 100% rename from osu.Game/Graphics/UserInterface/SimpleComboCounter.cs rename to osu.Game/Graphics/UserInterface/DefaultComboCounter.cs diff --git a/osu.Game/Screens/Play/HUD/StandardComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs similarity index 100% rename from osu.Game/Screens/Play/HUD/StandardComboCounter.cs rename to osu.Game/Screens/Play/HUD/LegacyComboCounter.cs From 6a6718ebab2963afefacb3f05628ffdb7d48367c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 17:20:10 +0900 Subject: [PATCH 3926/6909] Allow bypassing origin/anchor setting of skinnable components It makes little sense to set these when using RelativeSizeAxes.Both --- .../Screens/Play/HUD/SkinnableComboCounter.cs | 1 + osu.Game/Skinning/SkinnableDrawable.cs | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs index a67953c790..36f615e9d0 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs @@ -14,6 +14,7 @@ namespace osu.Game.Screens.Play.HUD public SkinnableComboCounter() : base(new HUDSkinComponent(HUDSkinComponents.ComboCounter), createDefault) { + CentreComponent = false; } private IComboCounter skinnedCounter; diff --git a/osu.Game/Skinning/SkinnableDrawable.cs b/osu.Game/Skinning/SkinnableDrawable.cs index d9a5036649..5a48bc4baf 100644 --- a/osu.Game/Skinning/SkinnableDrawable.cs +++ b/osu.Game/Skinning/SkinnableDrawable.cs @@ -19,6 +19,12 @@ namespace osu.Game.Skinning /// public Drawable Drawable { get; private set; } + /// + /// Whether the drawable component should be centered in available space. + /// Defaults to true. + /// + public bool CentreComponent { get; set; } = true; + public new Axes AutoSizeAxes { get => base.AutoSizeAxes; @@ -84,8 +90,13 @@ namespace osu.Game.Skinning if (Drawable != null) { scaling.Invalidate(); - Drawable.Origin = Anchor.Centre; - Drawable.Anchor = Anchor.Centre; + + if (CentreComponent) + { + Drawable.Origin = Anchor.Centre; + Drawable.Anchor = Anchor.Centre; + } + InternalChild = Drawable; } else From 6eb3176776808d3737923a76171293f642ffa95a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 17:20:44 +0900 Subject: [PATCH 3927/6909] Add combo incrementing tests to hud overlay test suite --- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index e2b831b144..c02075bea9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.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.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; @@ -21,6 +23,8 @@ namespace osu.Game.Tests.Visual.Gameplay { private HUDOverlay hudOverlay; + private IEnumerable hudOverlays => CreatedDrawables.OfType(); + // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First(); @@ -28,6 +32,24 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuConfigManager config { get; set; } + [Test] + public void TestComboCounterIncrementing() + { + createNew(); + + AddRepeatStep("increase combo", () => + { + foreach (var hud in hudOverlays) + hud.ComboCounter.Current.Value++; + }, 10); + + AddStep("reset combo", () => + { + foreach (var hud in hudOverlays) + hud.ComboCounter.Current.Value = 0; + }); + } + [Test] public void TestShownByDefault() { @@ -55,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay { createNew(); - AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); + AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false)); AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); @@ -91,14 +113,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set keycounter visible false", () => { config.Set(OsuSetting.KeyOverlay, false); - hudOverlay.KeyCounter.AlwaysVisible.Value = false; + hudOverlays.ForEach(h => h.KeyCounter.AlwaysVisible.Value = false); }); - AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); + AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false)); AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent); - AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); + AddStep("set showhud true", () => hudOverlays.ForEach(h => h.ShowHud.Value = true)); AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent); AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); @@ -116,6 +138,8 @@ namespace osu.Game.Tests.Visual.Gameplay // Add any key just to display the key counter visually. hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); + hudOverlay.ComboCounter.Current.Value = 1; + action?.Invoke(hudOverlay); return hudOverlay; From 2fce064e32d5a57b6c26a56b79a3d029246ddaae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 17:21:56 +0900 Subject: [PATCH 3928/6909] Add basic legacy combo counter and updating positioning logic --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 2 +- osu.Game/Screens/Play/HUD/ComboCounter.cs | 4 +-- .../Play/HUD}/DefaultComboCounter.cs | 34 +++++++++++++++---- .../Screens/Play/HUD/LegacyComboCounter.cs | 10 +++++- .../Screens/Play/HUD/SkinnableComboCounter.cs | 31 ++--------------- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 12 +++++++ 7 files changed, 54 insertions(+), 41 deletions(-) rename osu.Game/{Graphics/UserInterface => Screens/Play/HUD}/DefaultComboCounter.cs (54%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index 09b4f9b761..43b3dd501d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; Add(score); - ComboCounter comboCounter = new StandardComboCounter + ComboCounter comboCounter = new LegacyComboCounter { Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, diff --git a/osu.Game/Screens/Play/HUD/ComboCounter.cs b/osu.Game/Screens/Play/HUD/ComboCounter.cs index ea50a4a578..d15a8d25ec 100644 --- a/osu.Game/Screens/Play/HUD/ComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ComboCounter.cs @@ -9,9 +9,9 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Screens.Play.HUD { - public abstract class ComboCounter : Container + public abstract class ComboCounter : Container, IComboCounter { - public BindableInt Current = new BindableInt + public Bindable Current { get; } = new BindableInt { MinValue = 0, }; diff --git a/osu.Game/Graphics/UserInterface/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs similarity index 54% rename from osu.Game/Graphics/UserInterface/DefaultComboCounter.cs rename to osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index 59e31eff55..1e23319c28 100644 --- a/osu.Game/Graphics/UserInterface/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -4,26 +4,46 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Screens.Play.HUD; +using osu.Game.Graphics.UserInterface; +using osuTK; -namespace osu.Game.Graphics.UserInterface +namespace osu.Game.Screens.Play.HUD { - /// - /// Used as an accuracy counter. Represented visually as a percentage. - /// - public class SimpleComboCounter : RollingCounter, IComboCounter + public class DefaultComboCounter : RollingCounter, IComboCounter { + private readonly Vector2 offset = new Vector2(20, 5); + protected override double RollingDuration => 750; - public SimpleComboCounter() + [Resolved(canBeNull: true)] + private HUDOverlay hud { get; set; } + + public DefaultComboCounter() { Current.Value = DisplayedCount = 0; + + Anchor = Anchor.TopCentre; + Origin = Anchor.TopLeft; + + Position = offset; } [BackgroundDependencyLoader] private void load(OsuColour colours) => Colour = colours.BlueLighter; + protected override void Update() + { + base.Update(); + + if (hud != null) + { + // for now align with the score counter. eventually this will be user customisable. + Position += ToLocalSpace(hud.ScoreCounter.ScreenSpaceDrawQuad.TopRight) + offset; + } + } + protected override string FormatCount(int count) { return $@"{count}x"; diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 7301300b8d..8a94d19609 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.Play.HUD /// /// Uses the 'x' symbol and has a pop-out effect while rolling over. /// - public class StandardComboCounter : ComboCounter + public class LegacyComboCounter : ComboCounter { protected uint ScheduledPopOutCurrentId; @@ -18,6 +18,14 @@ namespace osu.Game.Screens.Play.HUD public new Vector2 PopOutScale = new Vector2(1.6f); + public LegacyComboCounter() + { + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + + Margin = new MarginPadding { Top = 5, Left = 20 }; + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs index 36f615e9d0..9f8ad758e4 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs @@ -3,9 +3,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Skinning; -using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { @@ -21,39 +19,14 @@ namespace osu.Game.Screens.Play.HUD protected override void SkinChanged(ISkinSource skin, bool allowFallback) { - // todo: unnecessary? - if (skinnedCounter != null) - { - Current.UnbindFrom(skinnedCounter.Current); - } - base.SkinChanged(skin, allowFallback); - // temporary layout code, will eventually be replaced by the skin layout system. - if (Drawable is SimpleComboCounter) - { - Drawable.BypassAutoSizeAxes = Axes.X; - Drawable.Anchor = Anchor.TopRight; - Drawable.Origin = Anchor.TopLeft; - Drawable.Margin = new MarginPadding { Top = 5, Left = 20 }; - } - else - { - Drawable.BypassAutoSizeAxes = Axes.X; - Drawable.Anchor = Anchor.BottomLeft; - Drawable.Origin = Anchor.BottomLeft; - Drawable.Margin = new MarginPadding { Top = 5, Left = 20 }; - } - skinnedCounter = (IComboCounter)Drawable; - - Current.BindTo(skinnedCounter.Current); + skinnedCounter.Current.BindTo(Current); } - private static Drawable createDefault(ISkinComponent skinComponent) => new SimpleComboCounter(); + private static Drawable createDefault(ISkinComponent skinComponent) => new DefaultComboCounter(); public Bindable Current { get; } = new Bindable(); - - public void UpdateCombo(int combo, Color4? hitObjectColour = null) => Current.Value = combo; } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index ee5b4e3f34..a3547bbc68 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -22,6 +22,7 @@ using osuTK.Input; namespace osu.Game.Screens.Play { + [Cached] public class HUDOverlay : Container { private const float fade_duration = 400; @@ -104,7 +105,6 @@ namespace osu.Game.Screens.Play { AccuracyCounter = CreateAccuracyCounter(), ScoreCounter = CreateScoreCounter(), - ComboCounter = CreateComboCounter(), }, }, ComboCounter = CreateComboCounter(), diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e38913b13a..b8b9349cc0 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -18,6 +18,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; using osuTK.Graphics; namespace osu.Game.Skinning @@ -327,6 +328,17 @@ namespace osu.Game.Skinning { switch (component) { + case HUDSkinComponent hudComponent: + { + switch (hudComponent.Component) + { + case HUDSkinComponents.ComboCounter: + return new LegacyComboCounter(); + } + + return null; + } + case GameplaySkinComponent resultComponent: switch (resultComponent.Component) { From fbbea48c8c45f5dfc1360c6cf749dc35123751a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 17:51:03 +0900 Subject: [PATCH 3929/6909] Add score text skinnability --- osu.Game/Screens/Play/HUD/ComboCounter.cs | 43 ++++++++----------- .../Screens/Play/HUD/LegacyComboCounter.cs | 27 ++++++++++-- osu.Game/Skinning/HUDSkinComponents.cs | 3 +- osu.Game/Skinning/LegacySkin.cs | 9 ++++ 4 files changed, 52 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ComboCounter.cs b/osu.Game/Screens/Play/HUD/ComboCounter.cs index d15a8d25ec..5bffa18032 100644 --- a/osu.Game/Screens/Play/HUD/ComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/ComboCounter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,7 +19,7 @@ namespace osu.Game.Screens.Play.HUD public bool IsRolling { get; protected set; } - protected SpriteText PopOutCount; + protected Drawable PopOutCount; protected virtual double PopOutDuration => 150; protected virtual float PopOutScale => 2.0f; @@ -37,7 +38,7 @@ namespace osu.Game.Screens.Play.HUD /// protected Easing RollingEasing => Easing.None; - protected SpriteText DisplayedCountSpriteText; + protected Drawable DisplayedCountSpriteText; private int previousValue; @@ -47,30 +48,34 @@ namespace osu.Game.Screens.Play.HUD protected ComboCounter() { AutoSizeAxes = Axes.Both; + } + [BackgroundDependencyLoader] + private void load() + { Children = new Drawable[] { - DisplayedCountSpriteText = new OsuSpriteText + DisplayedCountSpriteText = CreateSpriteText().With(s => { - Alpha = 0, - }, - PopOutCount = new OsuSpriteText + s.Alpha = 0; + }), + PopOutCount = CreateSpriteText().With(s => { - Alpha = 0, - Margin = new MarginPadding(0.05f), - } + s.Alpha = 0; + s.Margin = new MarginPadding(0.05f); + }) }; - TextSize = 80; - Current.ValueChanged += combo => updateCount(combo.NewValue == 0); } + protected virtual Drawable CreateSpriteText() => new OsuSpriteText(); + protected override void LoadComplete() { base.LoadComplete(); - DisplayedCountSpriteText.Text = FormatCount(Current.Value); + ((IHasText)DisplayedCountSpriteText).Text = FormatCount(Current.Value); DisplayedCountSpriteText.Anchor = Anchor; DisplayedCountSpriteText.Origin = Origin; @@ -94,20 +99,6 @@ namespace osu.Game.Screens.Play.HUD } } - private float textSize; - - public float TextSize - { - get => textSize; - set - { - textSize = value; - - DisplayedCountSpriteText.Font = DisplayedCountSpriteText.Font.With(size: TextSize); - PopOutCount.Font = PopOutCount.Font.With(size: TextSize); - } - } - /// /// Increments the combo by an amount. /// diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 8a94d19609..54a4338885 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -1,8 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Allocation; using osuTK; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { @@ -26,6 +31,22 @@ namespace osu.Game.Screens.Play.HUD Margin = new MarginPadding { Top = 5, Left = 20 }; } + [Resolved] + private ISkinSource skin { get; set; } + + protected override Drawable CreateSpriteText() + { + return skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) ?? new OsuSpriteText(); + + /* + new OsuSpriteText + { + Font = OsuFont.Numeric.With(size: 40), + UseFullGlyphHeight = false, + }); + */ + } + protected override void LoadComplete() { base.LoadComplete(); @@ -41,7 +62,7 @@ namespace osu.Game.Screens.Play.HUD protected virtual void TransformPopOut(int newValue) { - PopOutCount.Text = FormatCount(newValue); + ((IHasText)PopOutCount).Text = FormatCount(newValue); PopOutCount.ScaleTo(PopOutScale); PopOutCount.FadeTo(PopOutInitialAlpha); @@ -60,13 +81,13 @@ namespace osu.Game.Screens.Play.HUD protected virtual void TransformNoPopOut(int newValue) { - DisplayedCountSpriteText.Text = FormatCount(newValue); + ((IHasText)DisplayedCountSpriteText).Text = FormatCount(newValue); DisplayedCountSpriteText.ScaleTo(1); } protected virtual void TransformPopOutSmall(int newValue) { - DisplayedCountSpriteText.Text = FormatCount(newValue); + ((IHasText)DisplayedCountSpriteText).Text = FormatCount(newValue); DisplayedCountSpriteText.ScaleTo(PopOutSmallScale); DisplayedCountSpriteText.ScaleTo(1, PopOutDuration, PopOutEasing); } diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs index 6f3e2cbaf5..7577ba066c 100644 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ b/osu.Game/Skinning/HUDSkinComponents.cs @@ -5,6 +5,7 @@ namespace osu.Game.Skinning { public enum HUDSkinComponents { - ComboCounter + ComboCounter, + ScoreText } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index b8b9349cc0..fddea40b04 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -19,6 +19,7 @@ using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; +using osuTK; using osuTK.Graphics; namespace osu.Game.Skinning @@ -334,6 +335,14 @@ namespace osu.Game.Skinning { case HUDSkinComponents.ComboCounter: return new LegacyComboCounter(); + + case HUDSkinComponents.ScoreText: + const string font = "score"; + + if (!this.HasFont(font)) + return null; + + return new LegacySpriteText(this, font); } return null; From 9bb8a43bcee61d11f26598fff2228af1f07a4d17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 18:15:06 +0900 Subject: [PATCH 3930/6909] Combine LegacyComboCounter and ComboCounter classes --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 2 +- osu.Game/Screens/Play/HUD/ComboCounter.cs | 191 ------------------ .../Screens/Play/HUD/LegacyComboCounter.cs | 176 ++++++++++++++-- osu.Game/Skinning/LegacySkin.cs | 1 - 4 files changed, 155 insertions(+), 215 deletions(-) delete mode 100644 osu.Game/Screens/Play/HUD/ComboCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index 43b3dd501d..29e4b1f0cb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Gameplay }; Add(score); - ComboCounter comboCounter = new LegacyComboCounter + LegacyComboCounter comboCounter = new LegacyComboCounter { Origin = Anchor.BottomLeft, Anchor = Anchor.BottomLeft, diff --git a/osu.Game/Screens/Play/HUD/ComboCounter.cs b/osu.Game/Screens/Play/HUD/ComboCounter.cs deleted file mode 100644 index 5bffa18032..0000000000 --- a/osu.Game/Screens/Play/HUD/ComboCounter.cs +++ /dev/null @@ -1,191 +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.Sprites; -using osu.Game.Graphics.Sprites; - -namespace osu.Game.Screens.Play.HUD -{ - public abstract class ComboCounter : Container, IComboCounter - { - public Bindable Current { get; } = new BindableInt - { - MinValue = 0, - }; - - public bool IsRolling { get; protected set; } - - protected Drawable PopOutCount; - - protected virtual double PopOutDuration => 150; - protected virtual float PopOutScale => 2.0f; - protected virtual Easing PopOutEasing => Easing.None; - protected virtual float PopOutInitialAlpha => 0.75f; - - protected virtual double FadeOutDuration => 100; - - /// - /// Duration in milliseconds for the counter roll-up animation for each element. - /// - protected virtual double RollingDuration => 20; - - /// - /// Easing for the counter rollover animation. - /// - protected Easing RollingEasing => Easing.None; - - protected Drawable DisplayedCountSpriteText; - - private int previousValue; - - /// - /// Base of all combo counters. - /// - protected ComboCounter() - { - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - DisplayedCountSpriteText = CreateSpriteText().With(s => - { - s.Alpha = 0; - }), - PopOutCount = CreateSpriteText().With(s => - { - s.Alpha = 0; - s.Margin = new MarginPadding(0.05f); - }) - }; - - Current.ValueChanged += combo => updateCount(combo.NewValue == 0); - } - - protected virtual Drawable CreateSpriteText() => new OsuSpriteText(); - - protected override void LoadComplete() - { - base.LoadComplete(); - - ((IHasText)DisplayedCountSpriteText).Text = FormatCount(Current.Value); - DisplayedCountSpriteText.Anchor = Anchor; - DisplayedCountSpriteText.Origin = Origin; - - StopRolling(); - } - - private int displayedCount; - - /// - /// Value shown at the current moment. - /// - public virtual int DisplayedCount - { - get => displayedCount; - protected set - { - if (displayedCount.Equals(value)) - return; - - updateDisplayedCount(displayedCount, value, IsRolling); - } - } - - /// - /// Increments the combo by an amount. - /// - /// - public void Increment(int amount = 1) - { - Current.Value += amount; - } - - /// - /// Stops rollover animation, forcing the displayed count to be the actual count. - /// - public void StopRolling() - { - updateCount(false); - } - - protected virtual string FormatCount(int count) - { - return count.ToString(); - } - - protected virtual void OnCountRolling(int currentValue, int newValue) - { - transformRoll(currentValue, newValue); - } - - protected virtual void OnCountIncrement(int currentValue, int newValue) - { - DisplayedCount = newValue; - } - - protected virtual void OnCountChange(int currentValue, int newValue) - { - DisplayedCount = newValue; - } - - private double getProportionalDuration(int currentValue, int newValue) - { - double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; - return difference * RollingDuration; - } - - private void updateDisplayedCount(int currentValue, int newValue, bool rolling) - { - displayedCount = newValue; - if (rolling) - OnDisplayedCountRolling(currentValue, newValue); - else if (currentValue + 1 == newValue) - OnDisplayedCountIncrement(newValue); - else - OnDisplayedCountChange(newValue); - } - - private void updateCount(bool rolling) - { - int prev = previousValue; - previousValue = Current.Value; - - if (!IsLoaded) - return; - - if (!rolling) - { - FinishTransforms(false, nameof(DisplayedCount)); - IsRolling = false; - DisplayedCount = prev; - - if (prev + 1 == Current.Value) - OnCountIncrement(prev, Current.Value); - else - OnCountChange(prev, Current.Value); - } - else - { - OnCountRolling(displayedCount, Current.Value); - IsRolling = true; - } - } - - private void transformRoll(int currentValue, int newValue) - { - this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue), RollingEasing); - } - - protected abstract void OnDisplayedCountRolling(int currentValue, int newValue); - protected abstract void OnDisplayedCountIncrement(int newValue); - protected abstract void OnDisplayedCountChange(int newValue); - } -} diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 54a4338885..b62cd1c309 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -2,62 +2,124 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osuTK; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Screens.Play.HUD { /// /// Uses the 'x' symbol and has a pop-out effect while rolling over. /// - public class LegacyComboCounter : ComboCounter + public class LegacyComboCounter : CompositeDrawable, IComboCounter { protected uint ScheduledPopOutCurrentId; protected virtual float PopOutSmallScale => 1.1f; protected virtual bool CanPopOutWhileRolling => false; - public new Vector2 PopOutScale = new Vector2(1.6f); + protected Drawable PopOutCount; + protected Drawable DisplayedCountSpriteText; + private int previousValue; + private int displayedCount; public LegacyComboCounter() { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; - Margin = new MarginPadding { Top = 5, Left = 20 }; + Margin = new MarginPadding { Bottom = 20, Left = 20 }; + + Scale = new Vector2(1.6f); } [Resolved] private ISkinSource skin { get; set; } - protected override Drawable CreateSpriteText() + public Bindable Current { get; } = new BindableInt { - return skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) ?? new OsuSpriteText(); + MinValue = 0, + }; - /* - new OsuSpriteText + public bool IsRolling { get; protected set; } + protected virtual double PopOutDuration => 150; + protected virtual float PopOutScale => 1.6f; + protected virtual Easing PopOutEasing => Easing.None; + protected virtual float PopOutInitialAlpha => 0.75f; + protected virtual double FadeOutDuration => 100; + + /// + /// Duration in milliseconds for the counter roll-up animation for each element. + /// + protected virtual double RollingDuration => 20; + + /// + /// Easing for the counter rollover animation. + /// + protected Easing RollingEasing => Easing.None; + + /// + /// Value shown at the current moment. + /// + public virtual int DisplayedCount + { + get => displayedCount; + protected set + { + if (displayedCount.Equals(value)) + return; + + updateDisplayedCount(displayedCount, value, IsRolling); + } + } + + protected Drawable CreateSpriteText() + { + return skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) ?? new OsuSpriteText { Font = OsuFont.Numeric.With(size: 40), UseFullGlyphHeight = false, - }); - */ + }; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + DisplayedCountSpriteText = CreateSpriteText().With(s => + { + s.Alpha = 0; + }), + PopOutCount = CreateSpriteText().With(s => + { + s.Alpha = 0; + s.Margin = new MarginPadding(0.05f); + }) + }; + + Current.ValueChanged += combo => updateCount(combo.NewValue == 0); } protected override void LoadComplete() { base.LoadComplete(); + ((IHasText)DisplayedCountSpriteText).Text = FormatCount(Current.Value); + + DisplayedCountSpriteText.Anchor = Anchor; + DisplayedCountSpriteText.Origin = Origin; PopOutCount.Origin = Origin; PopOutCount.Anchor = Anchor; - } - protected override string FormatCount(int count) - { - return $@"{count}x"; + StopRolling(); } protected virtual void TransformPopOut(int newValue) @@ -101,7 +163,7 @@ namespace osu.Game.Screens.Play.HUD DisplayedCount++; } - protected override void OnCountRolling(int currentValue, int newValue) + protected void OnCountRolling(int currentValue, int newValue) { ScheduledPopOutCurrentId++; @@ -109,10 +171,10 @@ namespace osu.Game.Screens.Play.HUD if (currentValue == 0 && newValue == 0) DisplayedCountSpriteText.FadeOut(FadeOutDuration); - base.OnCountRolling(currentValue, newValue); + transformRoll(currentValue, newValue); } - protected override void OnCountIncrement(int currentValue, int newValue) + protected void OnCountIncrement(int currentValue, int newValue) { ScheduledPopOutCurrentId++; @@ -130,17 +192,17 @@ namespace osu.Game.Screens.Play.HUD }, PopOutDuration); } - protected override void OnCountChange(int currentValue, int newValue) + protected void OnCountChange(int currentValue, int newValue) { ScheduledPopOutCurrentId++; if (newValue == 0) DisplayedCountSpriteText.FadeOut(); - base.OnCountChange(currentValue, newValue); + DisplayedCount = newValue; } - protected override void OnDisplayedCountRolling(int currentValue, int newValue) + protected void OnDisplayedCountRolling(int currentValue, int newValue) { if (newValue == 0) DisplayedCountSpriteText.FadeOut(FadeOutDuration); @@ -153,18 +215,88 @@ namespace osu.Game.Screens.Play.HUD TransformNoPopOut(newValue); } - protected override void OnDisplayedCountChange(int newValue) + protected void OnDisplayedCountChange(int newValue) { DisplayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1); TransformNoPopOut(newValue); } - protected override void OnDisplayedCountIncrement(int newValue) + protected void OnDisplayedCountIncrement(int newValue) { DisplayedCountSpriteText.Show(); TransformPopOutSmall(newValue); } + + /// + /// Increments the combo by an amount. + /// + /// + public void Increment(int amount = 1) + { + Current.Value += amount; + } + + /// + /// Stops rollover animation, forcing the displayed count to be the actual count. + /// + public void StopRolling() + { + updateCount(false); + } + + protected string FormatCount(int count) + { + return $@"{count}x"; + } + + private double getProportionalDuration(int currentValue, int newValue) + { + double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; + return difference * RollingDuration; + } + + private void updateDisplayedCount(int currentValue, int newValue, bool rolling) + { + displayedCount = newValue; + if (rolling) + OnDisplayedCountRolling(currentValue, newValue); + else if (currentValue + 1 == newValue) + OnDisplayedCountIncrement(newValue); + else + OnDisplayedCountChange(newValue); + } + + private void updateCount(bool rolling) + { + int prev = previousValue; + previousValue = Current.Value; + + if (!IsLoaded) + return; + + if (!rolling) + { + FinishTransforms(false, nameof(DisplayedCount)); + IsRolling = false; + DisplayedCount = prev; + + if (prev + 1 == Current.Value) + OnCountIncrement(prev, Current.Value); + else + OnCountChange(prev, Current.Value); + } + else + { + OnCountRolling(displayedCount, Current.Value); + IsRolling = true; + } + } + + private void transformRoll(int currentValue, int newValue) + { + this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue), RollingEasing); + } } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index fddea40b04..a4d47dd2f1 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -19,7 +19,6 @@ using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; -using osuTK; using osuTK.Graphics; namespace osu.Game.Skinning From 7f5ea57bd483bd528accc4b26f6e0f1f808bf660 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 18:34:51 +0900 Subject: [PATCH 3931/6909] Clean-up pass (best effort) on LegacyComboCounter --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 2 +- .../Screens/Play/HUD/LegacyComboCounter.cs | 359 ++++++++---------- 2 files changed, 158 insertions(+), 203 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index 29e4b1f0cb..ab010bee9f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep(@"Hit! :D", delegate { score.Current.Value += 300 + (ulong)(300.0 * (comboCounter.Current.Value > 0 ? comboCounter.Current.Value - 1 : 0) / 25.0); - comboCounter.Increment(); + comboCounter.Current.Value++; numerator++; denominator++; accuracyCounter.SetFraction(numerator, denominator); diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index b62cd1c309..c96a20405c 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -18,16 +18,34 @@ namespace osu.Game.Screens.Play.HUD /// public class LegacyComboCounter : CompositeDrawable, IComboCounter { - protected uint ScheduledPopOutCurrentId; + public Bindable Current { get; } = new BindableInt { MinValue = 0, }; - protected virtual float PopOutSmallScale => 1.1f; - protected virtual bool CanPopOutWhileRolling => false; + private uint scheduledPopOutCurrentId; + + private const double pop_out_duration = 150; + + private const Easing pop_out_easing = Easing.None; + + private const double fade_out_duration = 100; + + /// + /// Duration in milliseconds for the counter roll-up animation for each element. + /// + private const double rolling_duration = 20; + + private Drawable popOutCount; + + private Drawable displayedCountSpriteText; - protected Drawable PopOutCount; - protected Drawable DisplayedCountSpriteText; private int previousValue; + private int displayedCount; + private bool isRolling; + + [Resolved] + private ISkinSource skin { get; set; } + public LegacyComboCounter() { AutoSizeAxes = Axes.Both; @@ -40,65 +58,38 @@ namespace osu.Game.Screens.Play.HUD Scale = new Vector2(1.6f); } - [Resolved] - private ISkinSource skin { get; set; } - - public Bindable Current { get; } = new BindableInt - { - MinValue = 0, - }; - - public bool IsRolling { get; protected set; } - protected virtual double PopOutDuration => 150; - protected virtual float PopOutScale => 1.6f; - protected virtual Easing PopOutEasing => Easing.None; - protected virtual float PopOutInitialAlpha => 0.75f; - protected virtual double FadeOutDuration => 100; - - /// - /// Duration in milliseconds for the counter roll-up animation for each element. - /// - protected virtual double RollingDuration => 20; - - /// - /// Easing for the counter rollover animation. - /// - protected Easing RollingEasing => Easing.None; - /// /// Value shown at the current moment. /// public virtual int DisplayedCount { get => displayedCount; - protected set + private set { if (displayedCount.Equals(value)) return; - updateDisplayedCount(displayedCount, value, IsRolling); - } - } + if (isRolling) + onDisplayedCountRolling(displayedCount, value); + else if (displayedCount + 1 == value) + onDisplayedCountIncrement(value); + else + onDisplayedCountChange(value); - protected Drawable CreateSpriteText() - { - return skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) ?? new OsuSpriteText - { - Font = OsuFont.Numeric.With(size: 40), - UseFullGlyphHeight = false, - }; + displayedCount = value; + } } [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + InternalChildren = new[] { - DisplayedCountSpriteText = CreateSpriteText().With(s => + displayedCountSpriteText = createSpriteText().With(s => { s.Alpha = 0; }), - PopOutCount = CreateSpriteText().With(s => + popOutCount = createSpriteText().With(s => { s.Alpha = 0; s.Margin = new MarginPadding(0.05f); @@ -112,162 +103,16 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - ((IHasText)DisplayedCountSpriteText).Text = FormatCount(Current.Value); + ((IHasText)displayedCountSpriteText).Text = formatCount(Current.Value); - DisplayedCountSpriteText.Anchor = Anchor; - DisplayedCountSpriteText.Origin = Origin; - PopOutCount.Origin = Origin; - PopOutCount.Anchor = Anchor; + displayedCountSpriteText.Anchor = Anchor; + displayedCountSpriteText.Origin = Origin; + popOutCount.Origin = Origin; + popOutCount.Anchor = Anchor; - StopRolling(); - } - - protected virtual void TransformPopOut(int newValue) - { - ((IHasText)PopOutCount).Text = FormatCount(newValue); - - PopOutCount.ScaleTo(PopOutScale); - PopOutCount.FadeTo(PopOutInitialAlpha); - PopOutCount.MoveTo(Vector2.Zero); - - PopOutCount.ScaleTo(1, PopOutDuration, PopOutEasing); - PopOutCount.FadeOut(PopOutDuration, PopOutEasing); - PopOutCount.MoveTo(DisplayedCountSpriteText.Position, PopOutDuration, PopOutEasing); - } - - protected virtual void TransformPopOutRolling(int newValue) - { - TransformPopOut(newValue); - TransformPopOutSmall(newValue); - } - - protected virtual void TransformNoPopOut(int newValue) - { - ((IHasText)DisplayedCountSpriteText).Text = FormatCount(newValue); - DisplayedCountSpriteText.ScaleTo(1); - } - - protected virtual void TransformPopOutSmall(int newValue) - { - ((IHasText)DisplayedCountSpriteText).Text = FormatCount(newValue); - DisplayedCountSpriteText.ScaleTo(PopOutSmallScale); - DisplayedCountSpriteText.ScaleTo(1, PopOutDuration, PopOutEasing); - } - - protected virtual void ScheduledPopOutSmall(uint id) - { - // Too late; scheduled task invalidated - if (id != ScheduledPopOutCurrentId) - return; - - DisplayedCount++; - } - - protected void OnCountRolling(int currentValue, int newValue) - { - ScheduledPopOutCurrentId++; - - // Hides displayed count if was increasing from 0 to 1 but didn't finish - if (currentValue == 0 && newValue == 0) - DisplayedCountSpriteText.FadeOut(FadeOutDuration); - - transformRoll(currentValue, newValue); - } - - protected void OnCountIncrement(int currentValue, int newValue) - { - ScheduledPopOutCurrentId++; - - if (DisplayedCount < currentValue) - DisplayedCount++; - - DisplayedCountSpriteText.Show(); - - TransformPopOut(newValue); - - uint newTaskId = ScheduledPopOutCurrentId; - Scheduler.AddDelayed(delegate - { - ScheduledPopOutSmall(newTaskId); - }, PopOutDuration); - } - - protected void OnCountChange(int currentValue, int newValue) - { - ScheduledPopOutCurrentId++; - - if (newValue == 0) - DisplayedCountSpriteText.FadeOut(); - - DisplayedCount = newValue; - } - - protected void OnDisplayedCountRolling(int currentValue, int newValue) - { - if (newValue == 0) - DisplayedCountSpriteText.FadeOut(FadeOutDuration); - else - DisplayedCountSpriteText.Show(); - - if (CanPopOutWhileRolling) - TransformPopOutRolling(newValue); - else - TransformNoPopOut(newValue); - } - - protected void OnDisplayedCountChange(int newValue) - { - DisplayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1); - - TransformNoPopOut(newValue); - } - - protected void OnDisplayedCountIncrement(int newValue) - { - DisplayedCountSpriteText.Show(); - - TransformPopOutSmall(newValue); - } - - /// - /// Increments the combo by an amount. - /// - /// - public void Increment(int amount = 1) - { - Current.Value += amount; - } - - /// - /// Stops rollover animation, forcing the displayed count to be the actual count. - /// - public void StopRolling() - { updateCount(false); } - protected string FormatCount(int count) - { - return $@"{count}x"; - } - - private double getProportionalDuration(int currentValue, int newValue) - { - double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; - return difference * RollingDuration; - } - - private void updateDisplayedCount(int currentValue, int newValue, bool rolling) - { - displayedCount = newValue; - if (rolling) - OnDisplayedCountRolling(currentValue, newValue); - else if (currentValue + 1 == newValue) - OnDisplayedCountIncrement(newValue); - else - OnDisplayedCountChange(newValue); - } - private void updateCount(bool rolling) { int prev = previousValue; @@ -279,24 +124,134 @@ namespace osu.Game.Screens.Play.HUD if (!rolling) { FinishTransforms(false, nameof(DisplayedCount)); - IsRolling = false; + isRolling = false; DisplayedCount = prev; if (prev + 1 == Current.Value) - OnCountIncrement(prev, Current.Value); + onCountIncrement(prev, Current.Value); else - OnCountChange(prev, Current.Value); + onCountChange(prev, Current.Value); } else { - OnCountRolling(displayedCount, Current.Value); - IsRolling = true; + onCountRolling(displayedCount, Current.Value); + isRolling = true; } } - private void transformRoll(int currentValue, int newValue) + private void transformPopOut(int newValue) { - this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue), RollingEasing); + ((IHasText)popOutCount).Text = formatCount(newValue); + + popOutCount.ScaleTo(1.6f); + popOutCount.FadeTo(0.75f); + popOutCount.MoveTo(Vector2.Zero); + + popOutCount.ScaleTo(1, pop_out_duration, pop_out_easing); + popOutCount.FadeOut(pop_out_duration, pop_out_easing); + popOutCount.MoveTo(displayedCountSpriteText.Position, pop_out_duration, pop_out_easing); } + + private void transformNoPopOut(int newValue) + { + ((IHasText)displayedCountSpriteText).Text = formatCount(newValue); + + displayedCountSpriteText.ScaleTo(1); + } + + private void transformPopOutSmall(int newValue) + { + ((IHasText)displayedCountSpriteText).Text = formatCount(newValue); + displayedCountSpriteText.ScaleTo(1.1f); + displayedCountSpriteText.ScaleTo(1, pop_out_duration, pop_out_easing); + } + + private void scheduledPopOutSmall(uint id) + { + // Too late; scheduled task invalidated + if (id != scheduledPopOutCurrentId) + return; + + DisplayedCount++; + } + + private void onCountIncrement(int currentValue, int newValue) + { + scheduledPopOutCurrentId++; + + if (DisplayedCount < currentValue) + DisplayedCount++; + + displayedCountSpriteText.Show(); + + transformPopOut(newValue); + + uint newTaskId = scheduledPopOutCurrentId; + + Scheduler.AddDelayed(delegate + { + scheduledPopOutSmall(newTaskId); + }, pop_out_duration); + } + + private void onCountRolling(int currentValue, int newValue) + { + scheduledPopOutCurrentId++; + + // Hides displayed count if was increasing from 0 to 1 but didn't finish + if (currentValue == 0 && newValue == 0) + displayedCountSpriteText.FadeOut(fade_out_duration); + + transformRoll(currentValue, newValue); + } + + private void onCountChange(int currentValue, int newValue) + { + scheduledPopOutCurrentId++; + + if (newValue == 0) + displayedCountSpriteText.FadeOut(); + + DisplayedCount = newValue; + } + + private void onDisplayedCountRolling(int currentValue, int newValue) + { + if (newValue == 0) + displayedCountSpriteText.FadeOut(fade_out_duration); + else + displayedCountSpriteText.Show(); + + transformNoPopOut(newValue); + } + + private void onDisplayedCountChange(int newValue) + { + displayedCountSpriteText.FadeTo(newValue == 0 ? 0 : 1); + transformNoPopOut(newValue); + } + + private void onDisplayedCountIncrement(int newValue) + { + displayedCountSpriteText.Show(); + transformPopOutSmall(newValue); + } + + private void transformRoll(int currentValue, int newValue) => + this.TransformTo(nameof(DisplayedCount), newValue, getProportionalDuration(currentValue, newValue), Easing.None); + + private string formatCount(int count) => $@"{count}x"; + + private double getProportionalDuration(int currentValue, int newValue) + { + double difference = currentValue > newValue ? currentValue - newValue : newValue - currentValue; + return difference * rolling_duration; + } + + private Drawable createSpriteText() => skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) ?? new OsuSpriteText + { + Font = OsuFont.Numeric.With(size: 40), + UseFullGlyphHeight = false, + }; } } From ac4f56403df80e1cfda6c9fb9d94879d63aa0930 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 19:15:52 +0900 Subject: [PATCH 3932/6909] Adjust size/position --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index c96a20405c..55ce68fcc8 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -53,9 +53,9 @@ namespace osu.Game.Screens.Play.HUD Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; - Margin = new MarginPadding { Bottom = 20, Left = 20 }; + Margin = new MarginPadding { Bottom = 10, Left = 10 }; - Scale = new Vector2(1.6f); + Scale = new Vector2(1.2f); } /// From 7d2eeb979532fc643eb6df12fed2691a2c417d66 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 19:18:04 +0900 Subject: [PATCH 3933/6909] Fix test names --- .../NonVisual/DifficultyAdjustmentModCombinationsTest.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index de0397dc84..917f245f4f 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.NonVisual } [Test] - public void TestMultiMod1() + public void TestMultiModFlattening() { var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModC())).CreateDifficultyAdjustmentModCombinations(); @@ -113,7 +113,7 @@ namespace osu.Game.Tests.NonVisual } [Test] - public void TestMultiMod2() + public void TestIncompatibleThroughMultiMod() { var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModB(), new ModIncompatibleWithA())).CreateDifficultyAdjustmentModCombinations(); From e9ebeedbe2edd7b1d4e62f9d01d8940b4cf5255a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 19:31:31 +0900 Subject: [PATCH 3934/6909] Refactor generation --- .../Difficulty/DifficultyCalculator.cs | 39 ++++++++++--------- 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 9989c750ee..70f248e072 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -105,10 +105,11 @@ namespace osu.Game.Rulesets.Difficulty /// public Mod[] CreateDifficultyAdjustmentModCombinations() { - return createDifficultyAdjustmentModCombinations(Array.Empty(), DifficultyAdjustmentMods).ToArray(); + return createDifficultyAdjustmentModCombinations(DifficultyAdjustmentMods, Array.Empty()).ToArray(); - static IEnumerable createDifficultyAdjustmentModCombinations(IEnumerable currentSet, Mod[] adjustmentSet, int currentSetCount = 0, int adjustmentSetStart = 0) + static IEnumerable createDifficultyAdjustmentModCombinations(ReadOnlyMemory remainingMods, IEnumerable currentSet, int currentSetCount = 0) { + // Return the current set. switch (currentSetCount) { case 0: @@ -128,11 +129,10 @@ namespace osu.Game.Rulesets.Difficulty break; } - // Apply mods in the adjustment set recursively. Using the entire adjustment set would result in duplicate multi-mod mod - // combinations in further recursions, so a moving subset is used to eliminate this effect - for (int i = adjustmentSetStart; i < adjustmentSet.Length; i++) + // Apply the rest of the remaining mods recursively. + for (int i = 0; i < remainingMods.Length; i++) { - var adjustmentMod = adjustmentSet[i]; + var adjustmentMod = remainingMods.Span[i]; if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)) || adjustmentMod.IncompatibleMods.Any(m => m.IsInstanceOfType(c)))) @@ -141,27 +141,30 @@ namespace osu.Game.Rulesets.Difficulty } // Append the new mod. - int newSetCount = currentSetCount; - var newSet = append(currentSet, adjustmentMod, ref newSetCount); + var (newSet, newSetCount) = flatten(adjustmentMod); - foreach (var combo in createDifficultyAdjustmentModCombinations(newSet, adjustmentSet, newSetCount, i + 1)) + foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(newSet), currentSetCount + newSetCount)) yield return combo; } } - // Appends a mod to an existing enumerable, returning the result. Recurses for MultiMod. - static IEnumerable append(IEnumerable existing, Mod mod, ref int count) + // Flattens a mod hierarchy (through MultiMod) as an IEnumerable + static (IEnumerable set, int count) flatten(Mod mod) { - if (mod is MultiMod multi) - { - foreach (var nested in multi.Mods) - existing = append(existing, nested, ref count); + if (!(mod is MultiMod multi)) + return (mod.Yield(), 1); - return existing; + IEnumerable set = Enumerable.Empty(); + int count = 0; + + foreach (var nested in multi.Mods) + { + var (nestedSet, nestedCount) = flatten(nested); + set = set.Concat(nestedSet); + count += nestedCount; } - count++; - return existing.Append(mod); + return (set, count); } } From e3eaba7b2ca46685780041b788dd2d3a229bbd57 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 19:39:48 +0900 Subject: [PATCH 3935/6909] Move ISampleDisabler implementation to Player and FrameStabilityContainer --- .../Gameplay/TestSceneGameplaySamplePlayback.cs | 2 +- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 12 +++++++++--- osu.Game/Screens/Play/GameplayClock.cs | 10 ++-------- osu.Game/Screens/Play/GameplayClockContainer.cs | 1 - osu.Game/Screens/Play/Player.cs | 13 +++++++++++-- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index 5bb3851264..6e505b16c2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("get variables", () => { - gameplayClock = Player.ChildrenOfType().First().GameplayClock; + gameplayClock = Player.ChildrenOfType().First(); slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); samples = slider.ChildrenOfType().ToArray(); }); diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 70b3d0c7d4..e4a3a2fe3d 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -18,8 +18,11 @@ namespace osu.Game.Rulesets.UI /// A container which consumes a parent gameplay clock and standardises frame counts for children. /// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks. /// - public class FrameStabilityContainer : Container, IHasReplayHandler + [Cached(typeof(ISamplePlaybackDisabler))] + public class FrameStabilityContainer : Container, IHasReplayHandler, ISamplePlaybackDisabler { + private readonly Bindable samplePlaybackDisabled = new Bindable(); + private readonly double gameplayStartTime; /// @@ -35,7 +38,6 @@ namespace osu.Game.Rulesets.UI public GameplayClock GameplayClock => stabilityGameplayClock; [Cached(typeof(GameplayClock))] - [Cached(typeof(ISamplePlaybackDisabler))] private readonly StabilityGameplayClock stabilityGameplayClock; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) @@ -102,6 +104,8 @@ namespace osu.Game.Rulesets.UI requireMoreUpdateLoops = true; validState = !GameplayClock.IsPaused.Value; + samplePlaybackDisabled.Value = stabilityGameplayClock.ShouldDisableSamplePlayback; + int loops = 0; while (validState && requireMoreUpdateLoops && loops++ < MaxCatchUpFrames) @@ -224,6 +228,8 @@ namespace osu.Game.Rulesets.UI public ReplayInputHandler ReplayInputHandler { get; set; } + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; + private class StabilityGameplayClock : GameplayClock { public GameplayClock ParentGameplayClock; @@ -237,7 +243,7 @@ namespace osu.Game.Rulesets.UI { } - protected override bool ShouldDisableSamplePlayback => + public override bool ShouldDisableSamplePlayback => // handle the case where playback is catching up to real-time. base.ShouldDisableSamplePlayback || ParentSampleDisabler?.SamplePlaybackDisabled.Value == true diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index eeea6777c6..4d0872e5bb 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.Play /// , as this should only be done once to ensure accuracy. /// /// - public class GameplayClock : IFrameBasedClock, ISamplePlaybackDisabler + public class GameplayClock : IFrameBasedClock { private readonly IFrameBasedClock underlyingClock; @@ -28,8 +28,6 @@ namespace osu.Game.Screens.Play /// public virtual IEnumerable> NonGameplayAdjustments => Enumerable.Empty>(); - private readonly Bindable samplePlaybackDisabled = new Bindable(); - public GameplayClock(IFrameBasedClock underlyingClock) { this.underlyingClock = underlyingClock; @@ -66,13 +64,11 @@ namespace osu.Game.Screens.Play /// /// Whether nested samples supporting the interface should be paused. /// - protected virtual bool ShouldDisableSamplePlayback => IsPaused.Value; + public virtual bool ShouldDisableSamplePlayback => IsPaused.Value; public void ProcessFrame() { // intentionally not updating the underlying clock (handled externally). - - samplePlaybackDisabled.Value = ShouldDisableSamplePlayback; } public double ElapsedFrameTime => underlyingClock.ElapsedFrameTime; @@ -82,7 +78,5 @@ namespace osu.Game.Screens.Play public FrameTimeInfo TimeInfo => underlyingClock.TimeInfo; public IClock Source => underlyingClock; - - IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 9f8e55f577..6679e56871 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -54,7 +54,6 @@ namespace osu.Game.Screens.Play public GameplayClock GameplayClock => localGameplayClock; [Cached(typeof(GameplayClock))] - [Cached(typeof(ISamplePlaybackDisabler))] private readonly LocalGameplayClock localGameplayClock; private Bindable userAudioOffset; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a2a53b4b75..56b212291a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -35,7 +35,8 @@ using osu.Game.Users; namespace osu.Game.Screens.Play { [Cached] - public class Player : ScreenWithBeatmapBackground + [Cached(typeof(ISamplePlaybackDisabler))] + public class Player : ScreenWithBeatmapBackground, ISamplePlaybackDisabler { /// /// The delay upon completion of the beatmap before displaying the results screen. @@ -55,6 +56,8 @@ namespace osu.Game.Screens.Play // We are managing our own adjustments (see OnEntering/OnExiting). public override bool AllowRateAdjustments => false; + private readonly Bindable samplePlaybackDisabled = new Bindable(); + /// /// Whether gameplay should pause when the game window focus is lost. /// @@ -229,7 +232,11 @@ namespace osu.Game.Screens.Play skipOverlay.Hide(); } - DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState()); + DrawableRuleset.IsPaused.BindValueChanged(paused => + { + updateGameplayState(); + samplePlaybackDisabled.Value = paused.NewValue; + }); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); @@ -752,5 +759,7 @@ namespace osu.Game.Screens.Play } #endregion + + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; } } From c4fdd35223e85e383b26b82eec0e8c1212eb11f9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 19:53:37 +0900 Subject: [PATCH 3936/6909] Fix same-type incompatibility through multimod --- .../DifficultyAdjustmentModCombinationsTest.cs | 14 ++++++++++++++ .../Difficulty/DifficultyCalculator.cs | 18 ++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 917f245f4f..5c7adb3f49 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -126,6 +126,20 @@ namespace osu.Game.Tests.NonVisual Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModIncompatibleWithA); } + [Test] + public void TestIncompatibleWithSameInstanceViaMultiMod() + { + var combinations = new TestLegacyDifficultyCalculator(new ModA(), new MultiMod(new ModA(), new ModB())).CreateDifficultyAdjustmentModCombinations(); + + Assert.AreEqual(3, combinations.Length); + Assert.IsTrue(combinations[0] is ModNoMod); + Assert.IsTrue(combinations[1] is ModA); + Assert.IsTrue(combinations[2] is MultiMod); + + Assert.IsTrue(((MultiMod)combinations[2]).Mods[0] is ModA); + Assert.IsTrue(((MultiMod)combinations[2]).Mods[1] is ModB); + } + private class ModA : Mod { public override string Name => nameof(ModA); diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 70f248e072..55b3d6607c 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; @@ -11,6 +12,7 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using Sentry; namespace osu.Game.Rulesets.Difficulty { @@ -132,18 +134,18 @@ namespace osu.Game.Rulesets.Difficulty // Apply the rest of the remaining mods recursively. for (int i = 0; i < remainingMods.Length; i++) { - var adjustmentMod = remainingMods.Span[i]; + var (nextSet, nextCount) = flatten(remainingMods.Span[i]); - if (currentSet.Any(c => c.IncompatibleMods.Any(m => m.IsInstanceOfType(adjustmentMod)) - || adjustmentMod.IncompatibleMods.Any(m => m.IsInstanceOfType(c)))) - { + // Check if any mods in the next set are incompatible with any of the current set. + if (currentSet.SelectMany(m => m.IncompatibleMods).Any(c => nextSet.Any(c.IsInstanceOfType))) continue; - } - // Append the new mod. - var (newSet, newSetCount) = flatten(adjustmentMod); + // Check if any mods in the next set are the same type as the current set. Mods of the exact same type are not incompatible with themselves. + if (currentSet.Any(c => nextSet.Any(n => c.GetType() == n.GetType()))) + continue; - foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(newSet), currentSetCount + newSetCount)) + // If all's good, attach the next set to the current set and recurse further. + foreach (var combo in createDifficultyAdjustmentModCombinations(remainingMods.Slice(i + 1), currentSet.Concat(nextSet), currentSetCount + nextCount)) yield return combo; } } From ed57b1363fdef33c350d1c65404739aca92750bd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 20:08:46 +0900 Subject: [PATCH 3937/6909] Remove unused usings --- osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index 55b3d6607c..7616c48150 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Reflection; using osu.Framework.Audio.Track; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Beatmaps; @@ -12,7 +11,6 @@ using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; -using Sentry; namespace osu.Game.Rulesets.Difficulty { From 1a2dc8374052f770fbd936de980901ba69909aeb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 20:40:17 +0900 Subject: [PATCH 3938/6909] Make field readonly --- osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs index 757329c525..7a0e3b2b76 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/ManiaBeatmapConverter.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps public bool Dual; public readonly bool IsForCurrentRuleset; - private int originalTargetColumns; + private readonly int originalTargetColumns; // Internal for testing purposes internal FastRandom Random { get; private set; } From 26dffbfd3bed7b5eafc28d8885b276bfb778e9a6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 14 Oct 2020 20:40:29 +0900 Subject: [PATCH 3939/6909] Replicate hit window calculation --- .../Difficulty/ManiaDifficultyCalculator.cs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index 356621acda..ade830764d 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -26,11 +26,13 @@ namespace osu.Game.Rulesets.Mania.Difficulty private const double star_scaling_factor = 0.018; private readonly bool isForCurrentRuleset; + private readonly double originalOverallDifficulty; public ManiaDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { isForCurrentRuleset = beatmap.BeatmapInfo.Ruleset.Equals(ruleset.RulesetInfo); + originalOverallDifficulty = beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty; } protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) @@ -46,7 +48,7 @@ namespace osu.Game.Rulesets.Mania.Difficulty StarRating = skills[0].DifficultyValue() * star_scaling_factor, Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future - GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, + GreatHitWindow = (int)Math.Ceiling(getHitWindow300(mods) / clockRate), ScoreMultiplier = getScoreMultiplier(beatmap, mods), MaxCombo = beatmap.HitObjects.Sum(h => h is HoldNote ? 2 : 1), Skills = skills @@ -107,6 +109,35 @@ namespace osu.Game.Rulesets.Mania.Difficulty } } + private int getHitWindow300(Mod[] mods) + { + if (isForCurrentRuleset) + { + double od = Math.Min(10.0, Math.Max(0, 10.0 - originalOverallDifficulty)); + return applyModAdjustments(34 + 3 * od, mods); + } + + if (Math.Round(originalOverallDifficulty) > 4) + return applyModAdjustments(34, mods); + + return applyModAdjustments(47, mods); + + static int applyModAdjustments(double value, Mod[] mods) + { + if (mods.Any(m => m is ManiaModHardRock)) + value /= 1.4; + else if (mods.Any(m => m is ManiaModEasy)) + value *= 1.4; + + if (mods.Any(m => m is ManiaModDoubleTime)) + value *= 1.5; + else if (mods.Any(m => m is ManiaModHalfTime)) + value *= 0.75; + + return (int)value; + } + } + private double getScoreMultiplier(IBeatmap beatmap, Mod[] mods) { double scoreMultiplier = 1; From b63303a2a813aef2b4a574ab5657157f52e1e2ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 21:40:49 +0900 Subject: [PATCH 3940/6909] Fix tests --- .../Gameplay/TestSceneSkinnableSound.cs | 54 ++++++++++--------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 8f2011e5dd..18eeb0a0e7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Audio; using osu.Game.Screens.Play; using osu.Game.Skinning; @@ -22,27 +21,24 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneSkinnableSound : OsuTestScene { - [Cached(typeof(ISamplePlaybackDisabler))] - private GameplayClock gameplayClock = new GameplayClock(new FramedClock()); - private TestSkinSourceContainer skinSource; private PausableSkinnableSound skinnableSound; [SetUp] - public void SetUp() => Schedule(() => + public void SetUpSteps() { - gameplayClock.IsPaused.Value = false; - - Children = new Drawable[] + AddStep("setup heirarchy", () => { - skinSource = new TestSkinSourceContainer + Children = new Drawable[] { - Clock = gameplayClock, - RelativeSizeAxes = Axes.Both, - Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide")) - }, - }; - }); + skinSource = new TestSkinSourceContainer + { + RelativeSizeAxes = Axes.Both, + Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide")) + }, + }; + }); + } [Test] public void TestStoppedSoundDoesntResumeAfterPause() @@ -62,8 +58,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for sample to stop playing", () => !sample.Playing); - AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); - AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); + AddStep("pause gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = true); + + AddStep("resume gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = false); AddWaitStep("wait a bit", 5); AddAssert("sample not playing", () => !sample.Playing); @@ -82,8 +79,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for sample to start playing", () => sample.Playing); - AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); + AddStep("pause gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = true); AddUntilStep("wait for sample to stop playing", () => !sample.Playing); + + AddStep("resume gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = false); + AddUntilStep("wait for sample to start playing", () => sample.Playing); } [Test] @@ -98,10 +98,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sample playing", () => sample.Playing); - AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); - AddUntilStep("wait for sample to stop playing", () => !sample.Playing); + AddStep("pause gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = true); - AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); + AddUntilStep("sample not playing", () => !sample.Playing); + + AddStep("resume gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = false); AddAssert("sample not playing", () => !sample.Playing); AddAssert("sample not playing", () => !sample.Playing); @@ -120,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sample playing", () => sample.Playing); - AddStep("pause gameplay clock", () => gameplayClock.IsPaused.Value = true); + AddStep("pause gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = true); AddUntilStep("wait for sample to stop playing", () => !sample.Playing); AddStep("trigger skin change", () => skinSource.TriggerSourceChanged()); @@ -133,20 +134,25 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddAssert("new sample stopped", () => !sample.Playing); - AddStep("resume gameplay clock", () => gameplayClock.IsPaused.Value = false); + AddStep("resume gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = false); AddWaitStep("wait a bit", 5); AddAssert("new sample not played", () => !sample.Playing); } [Cached(typeof(ISkinSource))] - private class TestSkinSourceContainer : Container, ISkinSource + [Cached(typeof(ISamplePlaybackDisabler))] + private class TestSkinSourceContainer : Container, ISkinSource, ISamplePlaybackDisabler { [Resolved] private ISkinSource source { get; set; } public event Action SourceChanged; + public Bindable SamplePlaybackDisabled { get; } = new Bindable(); + + IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => SamplePlaybackDisabled; + public Drawable GetDrawableComponent(ISkinComponent component) => source?.GetDrawableComponent(component); public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => source?.GetTexture(componentName, wrapModeS, wrapModeT); public SampleChannel GetSample(ISampleInfo sampleInfo) => source?.GetSample(sampleInfo); From e0210f5c4ce2c8ab00a74b35f9103da01824f148 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 14 Oct 2020 23:52:58 +0900 Subject: [PATCH 3941/6909] Ignore failed casts to make tests happy --- osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs index 9f8ad758e4..f7b6e419ea 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs @@ -21,8 +21,8 @@ namespace osu.Game.Screens.Play.HUD { base.SkinChanged(skin, allowFallback); - skinnedCounter = (IComboCounter)Drawable; - skinnedCounter.Current.BindTo(Current); + skinnedCounter = Drawable as IComboCounter; + skinnedCounter?.Current.BindTo(Current); } private static Drawable createDefault(ISkinComponent skinComponent) => new DefaultComboCounter(); From 2ca6c4e377fd861e989649e3be5295826866022d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 14 Oct 2020 23:24:16 +0200 Subject: [PATCH 3942/6909] Adjust test step names --- .../Visual/Gameplay/TestSceneSkinnableSound.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 18eeb0a0e7..864e88d023 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUp] public void SetUpSteps() { - AddStep("setup heirarchy", () => + AddStep("setup hierarchy", () => { Children = new Drawable[] { @@ -58,9 +58,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for sample to stop playing", () => !sample.Playing); - AddStep("pause gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = true); + AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true); - AddStep("resume gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = false); + AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false); AddWaitStep("wait a bit", 5); AddAssert("sample not playing", () => !sample.Playing); @@ -79,10 +79,10 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for sample to start playing", () => sample.Playing); - AddStep("pause gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = true); + AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true); AddUntilStep("wait for sample to stop playing", () => !sample.Playing); - AddStep("resume gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = false); + AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false); AddUntilStep("wait for sample to start playing", () => sample.Playing); } @@ -98,11 +98,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sample playing", () => sample.Playing); - AddStep("pause gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = true); + AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true); AddUntilStep("sample not playing", () => !sample.Playing); - AddStep("resume gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = false); + AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false); AddAssert("sample not playing", () => !sample.Playing); AddAssert("sample not playing", () => !sample.Playing); @@ -121,7 +121,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("sample playing", () => sample.Playing); - AddStep("pause gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = true); + AddStep("disable sample playback", () => skinSource.SamplePlaybackDisabled.Value = true); AddUntilStep("wait for sample to stop playing", () => !sample.Playing); AddStep("trigger skin change", () => skinSource.TriggerSourceChanged()); @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddAssert("new sample stopped", () => !sample.Playing); - AddStep("resume gameplay clock", () => skinSource.SamplePlaybackDisabled.Value = false); + AddStep("enable sample playback", () => skinSource.SamplePlaybackDisabled.Value = false); AddWaitStep("wait a bit", 5); AddAssert("new sample not played", () => !sample.Playing); From b06f59ffdcf99454bb4447b7e4efb936ebb0f399 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 15:35:33 +0900 Subject: [PATCH 3943/6909] Split out test for combo counter specifically --- .../Visual/Gameplay/TestSceneComboCounter.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.cs new file mode 100644 index 0000000000..d0c2fb5064 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneComboCounter.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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneComboCounter : SkinnableTestScene + { + private IEnumerable comboCounters => CreatedDrawables.OfType(); + + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create combo counters", () => SetContents(() => + { + var comboCounter = new SkinnableComboCounter(); + comboCounter.Current.Value = 1; + return comboCounter; + })); + } + + [Test] + public void TestComboCounterIncrementing() + { + AddRepeatStep("increase combo", () => + { + foreach (var counter in comboCounters) + counter.Current.Value++; + }, 10); + + AddStep("reset combo", () => + { + foreach (var counter in comboCounters) + counter.Current.Value = 0; + }); + } + } +} From d5f2aab52e4ba4f155118fe3450dc7e57a3979a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 15:37:40 +0900 Subject: [PATCH 3944/6909] Tidy up SkinnableComboCounter class slightly --- osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs index f7b6e419ea..c04c50141a 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableComboCounter.cs @@ -2,15 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD { public class SkinnableComboCounter : SkinnableDrawable, IComboCounter { + public Bindable Current { get; } = new Bindable(); + public SkinnableComboCounter() - : base(new HUDSkinComponent(HUDSkinComponents.ComboCounter), createDefault) + : base(new HUDSkinComponent(HUDSkinComponents.ComboCounter), skinComponent => new DefaultComboCounter()) { CentreComponent = false; } @@ -24,9 +25,5 @@ namespace osu.Game.Screens.Play.HUD skinnedCounter = Drawable as IComboCounter; skinnedCounter?.Current.BindTo(Current); } - - private static Drawable createDefault(ISkinComponent skinComponent) => new DefaultComboCounter(); - - public Bindable Current { get; } = new Bindable(); } } From 219cbec6bdaae9f8b2c987a1be923b55d6d9a596 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 16:31:21 +0900 Subject: [PATCH 3945/6909] Split out DefaultScoreCounter and make ScoreCounter abstract --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 2 +- .../Graphics/UserInterface/ScoreCounter.cs | 16 +++++++----- .../Screens/Play/HUD/DefaultScoreCounter.cs | 26 +++++++++++++++++++ 3 files changed, 36 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index ab010bee9f..ba165a70f4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Gameplay { int numerator = 0, denominator = 0; - ScoreCounter score = new ScoreCounter(7) + ScoreCounter score = new DefaultScoreCounter() { Origin = Anchor.TopRight, Anchor = Anchor.TopRight, diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 73bbe5f03e..17e5ceedb9 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -1,18 +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.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Graphics.UserInterface { - public class ScoreCounter : RollingCounter + public abstract class ScoreCounter : RollingCounter, IScoreCounter { protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; - public bool UseCommaSeparator; + /// + /// Whether comma separators should be displayed. + /// + public bool UseCommaSeparator { get; } /// /// How many leading zeroes the counter has. @@ -23,14 +26,13 @@ namespace osu.Game.Graphics.UserInterface /// Displays score. /// /// How many leading zeroes the counter will have. - public ScoreCounter(uint leading = 0) + /// Whether comma separators should be displayed. + protected ScoreCounter(uint leading = 0, bool useCommaSeparator = false) { + UseCommaSeparator = useCommaSeparator; LeadingZeroes = leading; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.BlueLighter; - protected override double GetProportionalDuration(double currentValue, double newValue) { return currentValue > newValue ? currentValue - newValue : newValue - currentValue; diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs new file mode 100644 index 0000000000..a461b6a067 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -0,0 +1,26 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Play.HUD +{ + public class DefaultScoreCounter : ScoreCounter + { + public DefaultScoreCounter() + : base(6) + { + Anchor = Anchor.TopCentre; + Origin = Anchor.TopCentre; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.BlueLighter; + } + } +} From e1da64398e279d0eb6fe53bce2a9e2936403558f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 16:32:20 +0900 Subject: [PATCH 3946/6909] Add and consume skinnable score counter --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 2 +- .../TestSceneSkinnableScoreCounter.cs | 47 +++++++++++++++++++ osu.Game/Screens/Play/HUD/IScoreCounter.cs | 19 ++++++++ .../Screens/Play/HUD/SkinnableScoreCounter.cs | 29 ++++++++++++ osu.Game/Screens/Play/HUDOverlay.cs | 8 +--- osu.Game/Skinning/HUDSkinComponents.cs | 3 +- osu.Game/Skinning/LegacyScoreCounter.cs | 42 +++++++++++++++++ osu.Game/Skinning/LegacySkin.cs | 3 ++ 8 files changed, 145 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs create mode 100644 osu.Game/Screens/Play/HUD/IScoreCounter.cs create mode 100644 osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs create mode 100644 osu.Game/Skinning/LegacyScoreCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs index ba165a70f4..34c657bf7f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs @@ -16,7 +16,7 @@ namespace osu.Game.Tests.Visual.Gameplay { int numerator = 0, denominator = 0; - ScoreCounter score = new DefaultScoreCounter() + ScoreCounter score = new DefaultScoreCounter { Origin = Anchor.TopRight, Anchor = Anchor.TopRight, diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs new file mode 100644 index 0000000000..2d5003d1da --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSkinnableScoreCounter : SkinnableTestScene + { + private IEnumerable scoreCounters => CreatedDrawables.OfType(); + + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create combo counters", () => SetContents(() => + { + var comboCounter = new SkinnableScoreCounter(); + comboCounter.Current.Value = 1; + return comboCounter; + })); + } + + [Test] + public void TestScoreCounterIncrementing() + { + AddStep(@"Reset all", delegate + { + foreach (var s in scoreCounters) + s.Current.Value = 0; + }); + + AddStep(@"Hit! :D", delegate + { + foreach (var s in scoreCounters) + s.Current.Value += 300; + }); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/IScoreCounter.cs b/osu.Game/Screens/Play/HUD/IScoreCounter.cs new file mode 100644 index 0000000000..2d39a64cfe --- /dev/null +++ b/osu.Game/Screens/Play/HUD/IScoreCounter.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.Bindables; +using osu.Framework.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// An interface providing a set of methods to update a score counter. + /// + public interface IScoreCounter : IDrawable + { + /// + /// The current score to be displayed. + /// + Bindable Current { get; } + } +} diff --git a/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs new file mode 100644 index 0000000000..a442ad0d9a --- /dev/null +++ b/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.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.Bindables; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Play.HUD +{ + public class SkinnableScoreCounter : SkinnableDrawable, IScoreCounter + { + public Bindable Current { get; } = new Bindable(); + + public SkinnableScoreCounter() + : base(new HUDSkinComponent(HUDSkinComponents.ScoreCounter), _ => new DefaultScoreCounter()) + { + CentreComponent = false; + } + + private IScoreCounter skinnedCounter; + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + skinnedCounter = Drawable as IScoreCounter; + skinnedCounter?.Current.BindTo(Current); + } + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index a3547bbc68..56b8d60bd4 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Play public readonly KeyCounterDisplay KeyCounter; public readonly SkinnableComboCounter ComboCounter; - public readonly ScoreCounter ScoreCounter; + public readonly SkinnableScoreCounter ScoreCounter; public readonly RollingCounter AccuracyCounter; public readonly HealthDisplay HealthDisplay; public readonly SongProgress Progress; @@ -269,11 +269,7 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding { Top = 5, Right = 20 }, }; - protected virtual ScoreCounter CreateScoreCounter() => new ScoreCounter(6) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - }; + protected virtual SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter(); protected virtual SkinnableComboCounter CreateComboCounter() => new SkinnableComboCounter(); diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs index 7577ba066c..7863161971 100644 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ b/osu.Game/Skinning/HUDSkinComponents.cs @@ -6,6 +6,7 @@ namespace osu.Game.Skinning public enum HUDSkinComponents { ComboCounter, - ScoreText + ScoreText, + ScoreCounter } } diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs new file mode 100644 index 0000000000..0e1d4fba7f --- /dev/null +++ b/osu.Game/Skinning/LegacyScoreCounter.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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Skinning +{ + public class LegacyScoreCounter : ScoreCounter + { + private readonly ISkin skin; + + protected override double RollingDuration => 1000; + protected override Easing RollingEasing => Easing.Out; + + public new Bindable Current { get; } = new Bindable(); + + public LegacyScoreCounter(ISkin skin) + : base(6) + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + + this.skin = skin; + + // base class uses int for display, but externally we bind to ScoreProcesssor as a double for now. + Current.BindValueChanged(v => base.Current.Value = (int)v.NewValue); + + Margin = new MarginPadding { Bottom = 10, Left = 10 }; + Scale = new Vector2(1.2f); + } + + protected sealed override OsuSpriteText CreateSpriteText() => + new LegacySpriteText(skin, "score" /*, true*/) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }; + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index a4d47dd2f1..8f4539ca6d 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -335,6 +335,9 @@ namespace osu.Game.Skinning case HUDSkinComponents.ComboCounter: return new LegacyComboCounter(); + case HUDSkinComponents.ScoreCounter: + return new LegacyScoreCounter(this); + case HUDSkinComponents.ScoreText: const string font = "score"; From 950c47287ca4ce9c4b71f3dc346ba75918341c6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 16:56:05 +0900 Subject: [PATCH 3947/6909] Fix positioning of score display in HUD overlay --- .../Screens/Play/HUD/DefaultScoreCounter.cs | 12 ++++++ osu.Game/Screens/Play/HUDOverlay.cs | 41 ++++--------------- osu.Game/Skinning/LegacyScoreCounter.cs | 3 +- 3 files changed, 22 insertions(+), 34 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index a461b6a067..af78ce4be2 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -5,11 +5,14 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Screens.Play.HUD { public class DefaultScoreCounter : ScoreCounter { + private readonly Vector2 offset = new Vector2(20, 5); + public DefaultScoreCounter() : base(6) { @@ -17,10 +20,19 @@ namespace osu.Game.Screens.Play.HUD Origin = Anchor.TopCentre; } + [Resolved(canBeNull: true)] + private HUDOverlay hud { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours) { Colour = colours.BlueLighter; + + // todo: check if default once health display is skinnable + hud?.ShowHealthbar.BindValueChanged(healthBar => + { + this.MoveToY(healthBar.NewValue ? 30 : 0, HUDOverlay.FADE_DURATION, HUDOverlay.FADE_EASING); + }, true); } } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 56b8d60bd4..a507eaaa8d 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -25,8 +25,9 @@ namespace osu.Game.Screens.Play [Cached] public class HUDOverlay : Container { - private const float fade_duration = 400; - private const Easing fade_easing = Easing.Out; + public const float FADE_DURATION = 400; + + public const Easing FADE_EASING = Easing.Out; public readonly KeyCounterDisplay KeyCounter; public readonly SkinnableComboCounter ComboCounter; @@ -62,8 +63,6 @@ namespace osu.Game.Screens.Play public Action RequestSeek; - private readonly Container topScoreContainer; - private readonly FillFlowContainer bottomRightElements; private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; @@ -96,17 +95,8 @@ namespace osu.Game.Screens.Play Children = new Drawable[] { HealthDisplay = CreateHealthDisplay(), - topScoreContainer = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - AccuracyCounter = CreateAccuracyCounter(), - ScoreCounter = CreateScoreCounter(), - }, - }, + AccuracyCounter = CreateAccuracyCounter(), + ScoreCounter = CreateScoreCounter(), ComboCounter = CreateComboCounter(), ModDisplay = CreateModsContainer(), HitErrorDisplay = CreateHitErrorDisplayOverlay(), @@ -132,8 +122,8 @@ namespace osu.Game.Screens.Play Origin = Anchor.BottomRight, X = -5, AutoSizeAxes = Axes.Both, - LayoutDuration = fade_duration / 2, - LayoutEasing = fade_easing, + LayoutDuration = FADE_DURATION / 2, + LayoutEasing = FADE_EASING, Direction = FillDirection.Vertical, Children = new Drawable[] { @@ -186,21 +176,8 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, fade_duration, fade_easing))); - - ShowHealthbar.BindValueChanged(healthBar => - { - if (healthBar.NewValue) - { - HealthDisplay.FadeIn(fade_duration, fade_easing); - topScoreContainer.MoveToY(30, fade_duration, fade_easing); - } - else - { - HealthDisplay.FadeOut(fade_duration, fade_easing); - topScoreContainer.MoveToY(0, fade_duration, fade_easing); - } - }, true); + ShowHealthbar.BindValueChanged(healthBar => HealthDisplay.FadeTo(healthBar.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING), true); + ShowHud.BindValueChanged(visible => hideTargets.ForEach(d => d.FadeTo(visible.NewValue ? 1 : 0, FADE_DURATION, FADE_EASING))); configShowHud.BindValueChanged(visible => { diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 0e1d4fba7f..f94bef6652 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -28,8 +28,7 @@ namespace osu.Game.Skinning // base class uses int for display, but externally we bind to ScoreProcesssor as a double for now. Current.BindValueChanged(v => base.Current.Value = (int)v.NewValue); - Margin = new MarginPadding { Bottom = 10, Left = 10 }; - Scale = new Vector2(1.2f); + Margin = new MarginPadding(10); } protected sealed override OsuSpriteText CreateSpriteText() => From b210147c2e4424f5c935ae28558c10b80a9aa61c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 16:55:47 +0900 Subject: [PATCH 3948/6909] Update combo counter to read from default score display's position correctly --- osu.Game/Screens/Play/HUD/DefaultComboCounter.cs | 4 ++-- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index 1e23319c28..5ffaf0d388 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -37,10 +37,10 @@ namespace osu.Game.Screens.Play.HUD { base.Update(); - if (hud != null) + if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score) { // for now align with the score counter. eventually this will be user customisable. - Position += ToLocalSpace(hud.ScoreCounter.ScreenSpaceDrawQuad.TopRight) + offset; + Position += ToLocalSpace(score.ScreenSpaceDrawQuad.TopRight) + offset; } } diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 55ce68fcc8..66f4c5edb8 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.Play.HUD Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; - Margin = new MarginPadding { Bottom = 10, Left = 10 }; + Margin = new MarginPadding(10); Scale = new Vector2(1.2f); } From 74c031cfbb385d4f081f0f110b66a33e6c7376f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 17:10:35 +0900 Subject: [PATCH 3949/6909] Fix ModOverlay not including "UNRANKED" text in size --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 33 +++++++++++++++---------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 99c31241f1..68d019bf71 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -48,22 +48,29 @@ namespace osu.Game.Screens.Play.HUD { AutoSizeAxes = Axes.Both; - Children = new Drawable[] + Child = new FillFlowContainer { - iconsContainer = new ReverseChildIDFillFlowContainer + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, + iconsContainer = new ReverseChildIDFillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + }, + unrankedText = new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Text = @"/ UNRANKED /", + Font = OsuFont.Numeric.With(size: 12) + } }, - unrankedText = new OsuSpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.TopCentre, - Text = @"/ UNRANKED /", - Font = OsuFont.Numeric.With(size: 12) - } }; Current.ValueChanged += mods => From d8d085ede94aedf9051ab85ab035235bab38d215 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 17:11:02 +0900 Subject: [PATCH 3950/6909] Align top-right elements with lowest point in score display --- .../Screens/Play/HUD/PlayerSettingsOverlay.cs | 10 ++++---- osu.Game/Screens/Play/HUDOverlay.cs | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs index fc80983834..ffcbb06fb3 100644 --- a/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs +++ b/osu.Game/Screens/Play/HUD/PlayerSettingsOverlay.cs @@ -20,14 +20,13 @@ namespace osu.Game.Screens.Play.HUD public readonly VisualSettings VisualSettings; - //public readonly CollectionSettings CollectionSettings; - - //public readonly DiscussionSettings DiscussionSettings; - public PlayerSettingsOverlay() { AlwaysPresent = true; - RelativeSizeAxes = Axes.Both; + + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + AutoSizeAxes = Axes.Both; Child = new FillFlowContainer { @@ -36,7 +35,6 @@ namespace osu.Game.Screens.Play.HUD AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, Spacing = new Vector2(0, 20), - Margin = new MarginPadding { Top = 100, Right = 10 }, Children = new PlayerSettingsGroup[] { //CollectionSettings = new CollectionSettings(), diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index a507eaaa8d..639da7a3b6 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -64,6 +64,7 @@ namespace osu.Game.Screens.Play public Action RequestSeek; private readonly FillFlowContainer bottomRightElements; + private readonly FillFlowContainer topRightElements; private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; @@ -98,9 +99,7 @@ namespace osu.Game.Screens.Play AccuracyCounter = CreateAccuracyCounter(), ScoreCounter = CreateScoreCounter(), ComboCounter = CreateComboCounter(), - ModDisplay = CreateModsContainer(), HitErrorDisplay = CreateHitErrorDisplayOverlay(), - PlayerSettingsOverlay = CreatePlayerSettingsOverlay(), } }, }, @@ -116,11 +115,26 @@ namespace osu.Game.Screens.Play } }, }, + topRightElements = new FillFlowContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Margin = new MarginPadding(10), + Spacing = new Vector2(10), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + ModDisplay = CreateModsContainer(), + PlayerSettingsOverlay = CreatePlayerSettingsOverlay(), + } + }, bottomRightElements = new FillFlowContainer { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - X = -5, + Margin = new MarginPadding(10), + Spacing = new Vector2(10), AutoSizeAxes = Axes.Both, LayoutDuration = FADE_DURATION / 2, LayoutEasing = FADE_EASING, @@ -191,6 +205,8 @@ namespace osu.Game.Screens.Play protected override void Update() { base.Update(); + + topRightElements.Y = ToLocalSpace(ScoreCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y; bottomRightElements.Y = -Progress.Height; } @@ -266,7 +282,6 @@ namespace osu.Game.Screens.Play { Anchor = Anchor.BottomRight, Origin = Anchor.BottomRight, - Margin = new MarginPadding(10), }; protected virtual SongProgress CreateProgress() => new SongProgress @@ -287,7 +302,6 @@ namespace osu.Game.Screens.Play Anchor = Anchor.TopRight, Origin = Anchor.TopRight, AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 20, Right = 20 }, }; protected virtual HitErrorDisplay CreateHitErrorDisplayOverlay() => new HitErrorDisplay(scoreProcessor, drawableRuleset?.FirstAvailableHitWindows); From 5b5ba7df936f159df034467c0786e6ff4d161838 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 17:22:34 +0900 Subject: [PATCH 3951/6909] Remove unused offset --- osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs index af78ce4be2..1dcfe2e067 100644 --- a/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultScoreCounter.cs @@ -5,14 +5,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; -using osuTK; namespace osu.Game.Screens.Play.HUD { public class DefaultScoreCounter : ScoreCounter { - private readonly Vector2 offset = new Vector2(20, 5); - public DefaultScoreCounter() : base(6) { From 9f51327e4b409382f6750c4c8687a66ee59a6d7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 17:29:40 +0900 Subject: [PATCH 3952/6909] Fix completely incorrect default positioning logic --- osu.Game/Screens/Play/HUD/DefaultComboCounter.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index 1e23319c28..d6a4d30af6 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -23,11 +23,6 @@ namespace osu.Game.Screens.Play.HUD public DefaultComboCounter() { Current.Value = DisplayedCount = 0; - - Anchor = Anchor.TopCentre; - Origin = Anchor.TopLeft; - - Position = offset; } [BackgroundDependencyLoader] @@ -40,7 +35,7 @@ namespace osu.Game.Screens.Play.HUD if (hud != null) { // for now align with the score counter. eventually this will be user customisable. - Position += ToLocalSpace(hud.ScoreCounter.ScreenSpaceDrawQuad.TopRight) + offset; + Position = Parent.ToLocalSpace(hud.ScoreCounter.ScreenSpaceDrawQuad.TopRight) + offset; } } From 37e9f331ad78fb9ff10701888169e5ea47ddea7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 17:48:50 +0900 Subject: [PATCH 3953/6909] Simplify score font lookup --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 8 +------- osu.Game/Skinning/HUDSkinComponents.cs | 1 - osu.Game/Skinning/LegacySkin.cs | 15 +++++++-------- osu.Game/Skinning/LegacySpriteText.cs | 2 +- 4 files changed, 9 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 55ce68fcc8..5d96a48117 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -6,8 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Skinning; using osuTK; @@ -248,10 +246,6 @@ namespace osu.Game.Screens.Play.HUD return difference * rolling_duration; } - private Drawable createSpriteText() => skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) ?? new OsuSpriteText - { - Font = OsuFont.Numeric.With(size: 40), - UseFullGlyphHeight = false, - }; + private Drawable createSpriteText() => new LegacySpriteText(skin); } } diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs index 7577ba066c..06b22dc693 100644 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ b/osu.Game/Skinning/HUDSkinComponents.cs @@ -6,6 +6,5 @@ namespace osu.Game.Skinning public enum HUDSkinComponents { ComboCounter, - ScoreText } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index a4d47dd2f1..ea5a6e4e20 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -324,24 +324,23 @@ namespace osu.Game.Skinning return null; } + private const string score_font = "score"; + + private bool hasScoreFont => this.HasFont(score_font); + public override Drawable GetDrawableComponent(ISkinComponent component) { switch (component) { case HUDSkinComponent hudComponent: { + if (!hasScoreFont) + return null; + switch (hudComponent.Component) { case HUDSkinComponents.ComboCounter: return new LegacyComboCounter(); - - case HUDSkinComponents.ScoreText: - const string font = "score"; - - if (!this.HasFont(font)) - return null; - - return new LegacySpriteText(this, font); } return null; diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 773a9dc5c6..858bbcd6a8 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -12,7 +12,7 @@ namespace osu.Game.Skinning { private readonly LegacyGlyphStore glyphStore; - public LegacySpriteText(ISkin skin, string font) + public LegacySpriteText(ISkin skin, string font = "score") { Shadow = false; UseFullGlyphHeight = false; From 254eba90080f04398ade62a52c85b67345068954 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 17:48:50 +0900 Subject: [PATCH 3954/6909] Add and consume skinnable accuracy counter --- .../UserInterface/PercentageCounter.cs | 4 -- .../Play/HUD/DefaultAccuracyCounter.cs | 41 ++++++++++++++++ osu.Game/Screens/Play/HUD/IAccuracyCounter.cs | 19 ++++++++ .../Play/HUD/SkinnableAccuracyCounter.cs | 29 ++++++++++++ osu.Game/Screens/Play/HUDOverlay.cs | 11 +---- osu.Game/Skinning/HUDSkinComponents.cs | 3 +- osu.Game/Skinning/LegacyAccuracyCounter.cs | 47 +++++++++++++++++++ osu.Game/Skinning/LegacySkin.cs | 3 ++ 8 files changed, 143 insertions(+), 14 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs create mode 100644 osu.Game/Screens/Play/HUD/IAccuracyCounter.cs create mode 100644 osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.cs create mode 100644 osu.Game/Skinning/LegacyAccuracyCounter.cs diff --git a/osu.Game/Graphics/UserInterface/PercentageCounter.cs b/osu.Game/Graphics/UserInterface/PercentageCounter.cs index 1ccf7798e5..2d53ec066b 100644 --- a/osu.Game/Graphics/UserInterface/PercentageCounter.cs +++ b/osu.Game/Graphics/UserInterface/PercentageCounter.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Utils; @@ -28,9 +27,6 @@ namespace osu.Game.Graphics.UserInterface Current.Value = DisplayedCount = 1.0f; } - [BackgroundDependencyLoader] - private void load(OsuColour colours) => Colour = colours.BlueLighter; - protected override string FormatCount(double count) => count.FormatAccuracy(); protected override double GetProportionalDuration(double currentValue, double newValue) diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs new file mode 100644 index 0000000000..b286b380e0 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.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.Game.Graphics; +using osu.Game.Graphics.UserInterface; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public class DefaultAccuracyCounter : PercentageCounter, IAccuracyCounter + { + private readonly Vector2 offset = new Vector2(-20, 5); + + public DefaultAccuracyCounter() + { + Origin = Anchor.TopRight; + } + + [Resolved(canBeNull: true)] + private HUDOverlay hud { get; set; } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Colour = colours.BlueLighter; + } + + protected override void Update() + { + base.Update(); + + if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score) + { + // for now align with the score counter. eventually this will be user customisable. + Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopLeft) + offset; + } + } + } +} diff --git a/osu.Game/Screens/Play/HUD/IAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/IAccuracyCounter.cs new file mode 100644 index 0000000000..0199250a08 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/IAccuracyCounter.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.Bindables; +using osu.Framework.Graphics; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// An interface providing a set of methods to update a accuracy counter. + /// + public interface IAccuracyCounter : IDrawable + { + /// + /// The current accuracy to be displayed. + /// + Bindable Current { get; } + } +} diff --git a/osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.cs new file mode 100644 index 0000000000..76c9c30813 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/SkinnableAccuracyCounter.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.Bindables; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Play.HUD +{ + public class SkinnableAccuracyCounter : SkinnableDrawable, IAccuracyCounter + { + public Bindable Current { get; } = new Bindable(); + + public SkinnableAccuracyCounter() + : base(new HUDSkinComponent(HUDSkinComponents.AccuracyCounter), _ => new DefaultAccuracyCounter()) + { + CentreComponent = false; + } + + private IAccuracyCounter skinnedCounter; + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + skinnedCounter = Drawable as IAccuracyCounter; + skinnedCounter?.Current.BindTo(Current); + } + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 639da7a3b6..bb35bd3d69 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Mods; @@ -32,7 +31,7 @@ namespace osu.Game.Screens.Play public readonly KeyCounterDisplay KeyCounter; public readonly SkinnableComboCounter ComboCounter; public readonly SkinnableScoreCounter ScoreCounter; - public readonly RollingCounter AccuracyCounter; + public readonly SkinnableAccuracyCounter AccuracyCounter; public readonly HealthDisplay HealthDisplay; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; @@ -254,13 +253,7 @@ namespace osu.Game.Screens.Play return base.OnKeyDown(e); } - protected virtual RollingCounter CreateAccuracyCounter() => new PercentageCounter - { - BypassAutoSizeAxes = Axes.X, - Anchor = Anchor.TopLeft, - Origin = Anchor.TopRight, - Margin = new MarginPadding { Top = 5, Right = 20 }, - }; + protected virtual SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter(); protected virtual SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter(); diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs index d810fc31d4..d690a23dee 100644 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ b/osu.Game/Skinning/HUDSkinComponents.cs @@ -6,6 +6,7 @@ namespace osu.Game.Skinning public enum HUDSkinComponents { ComboCounter, - ScoreCounter + ScoreCounter, + AccuracyCounter } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs new file mode 100644 index 0000000000..0f3ac19ce6 --- /dev/null +++ b/osu.Game/Skinning/LegacyAccuracyCounter.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.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Skinning +{ + public class LegacyAccuracyCounter : PercentageCounter, IAccuracyCounter + { + private readonly ISkin skin; + + public LegacyAccuracyCounter(ISkin skin) + { + Origin = Anchor.TopRight; + Scale = new Vector2(0.75f); + + this.skin = skin; + } + + [Resolved(canBeNull: true)] + private HUDOverlay hud { get; set; } + + protected sealed override OsuSpriteText CreateSpriteText() => + new LegacySpriteText(skin, "score" /*, true*/) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }; + + protected override void Update() + { + base.Update(); + + if (hud?.ScoreCounter.Drawable is LegacyScoreCounter score) + { + // for now align with the score counter. eventually this will be user customisable. + Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight); + } + } + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index c8460ad797..e1cd095ba8 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -344,6 +344,9 @@ namespace osu.Game.Skinning case HUDSkinComponents.ScoreCounter: return new LegacyScoreCounter(this); + + case HUDSkinComponents.AccuracyCounter: + return new LegacyAccuracyCounter(this); } return null; From 4f6dd1586939eef71e4578131e4e5f55155421ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 17:56:37 +0900 Subject: [PATCH 3955/6909] Add legacy font lookup support for comma/percent --- osu.Game/Skinning/LegacySpriteText.cs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 858bbcd6a8..8394657b1c 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -34,7 +34,9 @@ namespace osu.Game.Skinning public ITexturedCharacterGlyph Get(string fontName, char character) { - var texture = skin.GetTexture($"{fontName}-{character}"); + var lookup = getLookupName(character); + + var texture = skin.GetTexture($"{fontName}-{lookup}"); if (texture == null) return null; @@ -42,6 +44,24 @@ namespace osu.Game.Skinning return new TexturedCharacterGlyph(new CharacterGlyph(character, 0, 0, texture.Width, null), texture, 1f / texture.ScaleAdjust); } + private static string getLookupName(char character) + { + switch (character) + { + case ',': + return "comma"; + + case '.': + return "dot"; + + case '%': + return "percent"; + + default: + return character.ToString(); + } + } + public Task GetAsync(string fontName, char character) => Task.Run(() => Get(fontName, character)); } } From b31a3fbabbe83ec779f789b9286c0f1bb025c1cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 18:11:30 +0900 Subject: [PATCH 3956/6909] Add test --- .../TestSceneSkinnableAccuracyCounter.cs | 49 +++++++++++++++++++ .../Play/HUD/DefaultAccuracyCounter.cs | 2 + osu.Game/Skinning/LegacyAccuracyCounter.cs | 4 +- 3 files changed, 54 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.cs new file mode 100644 index 0000000000..709929dcb0 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableAccuracyCounter.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.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSkinnableAccuracyCounter : SkinnableTestScene + { + private IEnumerable accuracyCounters => CreatedDrawables.OfType(); + + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create combo counters", () => SetContents(() => + { + var accuracyCounter = new SkinnableAccuracyCounter(); + + accuracyCounter.Current.Value = 1; + + return accuracyCounter; + })); + } + + [Test] + public void TestChangingAccuracy() + { + AddStep(@"Reset all", delegate + { + foreach (var s in accuracyCounters) + s.Current.Value = 1; + }); + + AddStep(@"Hit! :D", delegate + { + foreach (var s in accuracyCounters) + s.Current.Value -= 0.023f; + }); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs index b286b380e0..d5d8ec570a 100644 --- a/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultAccuracyCounter.cs @@ -16,6 +16,7 @@ namespace osu.Game.Screens.Play.HUD public DefaultAccuracyCounter() { Origin = Anchor.TopRight; + Anchor = Anchor.TopRight; } [Resolved(canBeNull: true)] @@ -34,6 +35,7 @@ namespace osu.Game.Screens.Play.HUD if (hud?.ScoreCounter.Drawable is DefaultScoreCounter score) { // for now align with the score counter. eventually this will be user customisable. + Anchor = Anchor.TopLeft; Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.TopLeft) + offset; } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 0f3ac19ce6..815580e85f 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -17,7 +17,9 @@ namespace osu.Game.Skinning public LegacyAccuracyCounter(ISkin skin) { + Anchor = Anchor.TopRight; Origin = Anchor.TopRight; + Scale = new Vector2(0.75f); this.skin = skin; @@ -40,7 +42,7 @@ namespace osu.Game.Skinning if (hud?.ScoreCounter.Drawable is LegacyScoreCounter score) { // for now align with the score counter. eventually this will be user customisable. - Position = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight); + Y = Parent.ToLocalSpace(score.ScreenSpaceDrawQuad.BottomRight).Y; } } } From ca74cf824c6fd01b21b78e862cae58bd6eca534b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 18:24:28 +0900 Subject: [PATCH 3957/6909] Add padding --- osu.Game/Skinning/LegacyAccuracyCounter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 815580e85f..9354b2b3bc 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -21,6 +21,7 @@ namespace osu.Game.Skinning Origin = Anchor.TopRight; Scale = new Vector2(0.75f); + Margin = new MarginPadding(10); this.skin = skin; } From 6983978c989c5063d8d7fb77cbdc5bed95ede85b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 18:30:44 +0900 Subject: [PATCH 3958/6909] Correct top-right element offset by finding the lower top anchor element --- osu.Game/Screens/Play/HUDOverlay.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index bb35bd3d69..7553f332cd 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; using osuTK; using osuTK.Input; @@ -65,6 +66,8 @@ namespace osu.Game.Screens.Play private readonly FillFlowContainer bottomRightElements; private readonly FillFlowContainer topRightElements; + private Container mainUIElements; + private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods) @@ -89,7 +92,7 @@ namespace osu.Game.Screens.Play { new Drawable[] { - new Container + mainUIElements = new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] @@ -205,7 +208,17 @@ namespace osu.Game.Screens.Play { base.Update(); - topRightElements.Y = ToLocalSpace(ScoreCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y; + float topRightOffset = 0; + + // fetch the bottom-most position of any main ui element that is anchored to the top of the screen. + // consider this kind of temporary. + foreach (var d in mainUIElements) + { + if (d is SkinnableDrawable sd && (sd.Drawable.Anchor & Anchor.y0) > 0) + topRightOffset = Math.Max(sd.Drawable.ScreenSpaceDrawQuad.BottomRight.Y, topRightOffset); + } + + topRightElements.Y = ToLocalSpace(new Vector2(0, topRightOffset)).Y; bottomRightElements.Y = -Progress.Height; } From d76365ed1b26252b3c54aef204bbd3312526ad4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 18:38:41 +0900 Subject: [PATCH 3959/6909] Make container readonly --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 7553f332cd..fa914c0ebc 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -66,7 +66,7 @@ namespace osu.Game.Screens.Play private readonly FillFlowContainer bottomRightElements; private readonly FillFlowContainer topRightElements; - private Container mainUIElements; + private readonly Container mainUIElements; private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; From 70806deba1195c41e5c59414482b6613d6965b33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 15 Oct 2020 19:11:40 +0900 Subject: [PATCH 3960/6909] Add support for bottom-anchored hit error display --- .../Visual/Gameplay/TestSceneHitErrorMeter.cs | 19 ++++++++ osu.Game/Configuration/ScoreMeterType.cs | 10 ++++- osu.Game/Screens/Play/HUD/HitErrorDisplay.cs | 43 +++++++++++++------ .../HUD/HitErrorMeters/BarHitErrorMeter.cs | 8 +++- 4 files changed, 62 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs index 377f305d63..1021ac3760 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHitErrorMeter.cs @@ -22,8 +22,10 @@ namespace osu.Game.Tests.Visual.Gameplay { private BarHitErrorMeter barMeter; private BarHitErrorMeter barMeter2; + private BarHitErrorMeter barMeter3; private ColourHitErrorMeter colourMeter; private ColourHitErrorMeter colourMeter2; + private ColourHitErrorMeter colourMeter3; private HitWindows hitWindows; public TestSceneHitErrorMeter() @@ -115,6 +117,13 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.CentreLeft, }); + Add(barMeter3 = new BarHitErrorMeter(hitWindows, true) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.CentreLeft, + Rotation = 270, + }); + Add(colourMeter = new ColourHitErrorMeter(hitWindows) { Anchor = Anchor.CentreRight, @@ -128,6 +137,14 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.CentreLeft, Margin = new MarginPadding { Left = 50 } }); + + Add(colourMeter3 = new ColourHitErrorMeter(hitWindows) + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.CentreLeft, + Rotation = 270, + Margin = new MarginPadding { Left = 50 } + }); } private void newJudgement(double offset = 0) @@ -140,8 +157,10 @@ namespace osu.Game.Tests.Visual.Gameplay barMeter.OnNewJudgement(judgement); barMeter2.OnNewJudgement(judgement); + barMeter3.OnNewJudgement(judgement); colourMeter.OnNewJudgement(judgement); colourMeter2.OnNewJudgement(judgement); + colourMeter3.OnNewJudgement(judgement); } } } diff --git a/osu.Game/Configuration/ScoreMeterType.cs b/osu.Game/Configuration/ScoreMeterType.cs index 156c4b1377..b9499c758e 100644 --- a/osu.Game/Configuration/ScoreMeterType.cs +++ b/osu.Game/Configuration/ScoreMeterType.cs @@ -16,7 +16,10 @@ namespace osu.Game.Configuration [Description("Hit Error (right)")] HitErrorRight, - [Description("Hit Error (both)")] + [Description("Hit Error (bottom)")] + HitErrorBottom, + + [Description("Hit Error (left+right)")] HitErrorBoth, [Description("Colour (left)")] @@ -25,7 +28,10 @@ namespace osu.Game.Configuration [Description("Colour (right)")] ColourRight, - [Description("Colour (both)")] + [Description("Colour (left+right)")] ColourBoth, + + [Description("Colour (bottom)")] + ColourBottom, } } diff --git a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs index 4d28f00f39..37d10a5320 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorDisplay.cs @@ -66,54 +66,69 @@ namespace osu.Game.Screens.Play.HUD switch (type.NewValue) { case ScoreMeterType.HitErrorBoth: - createBar(false); - createBar(true); + createBar(Anchor.CentreLeft); + createBar(Anchor.CentreRight); break; case ScoreMeterType.HitErrorLeft: - createBar(false); + createBar(Anchor.CentreLeft); break; case ScoreMeterType.HitErrorRight: - createBar(true); + createBar(Anchor.CentreRight); + break; + + case ScoreMeterType.HitErrorBottom: + createBar(Anchor.BottomCentre); break; case ScoreMeterType.ColourBoth: - createColour(false); - createColour(true); + createColour(Anchor.CentreLeft); + createColour(Anchor.CentreRight); break; case ScoreMeterType.ColourLeft: - createColour(false); + createColour(Anchor.CentreLeft); break; case ScoreMeterType.ColourRight: - createColour(true); + createColour(Anchor.CentreRight); + break; + + case ScoreMeterType.ColourBottom: + createColour(Anchor.BottomCentre); break; } } - private void createBar(bool rightAligned) + private void createBar(Anchor anchor) { + bool rightAligned = (anchor & Anchor.x2) > 0; + bool bottomAligned = (anchor & Anchor.y2) > 0; + var display = new BarHitErrorMeter(hitWindows, rightAligned) { Margin = new MarginPadding(margin), - Anchor = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft, + Anchor = anchor, + Origin = bottomAligned ? Anchor.CentreLeft : anchor, Alpha = 0, + Rotation = bottomAligned ? 270 : 0 }; completeDisplayLoading(display); } - private void createColour(bool rightAligned) + private void createColour(Anchor anchor) { + bool bottomAligned = (anchor & Anchor.y2) > 0; + var display = new ColourHitErrorMeter(hitWindows) { Margin = new MarginPadding(margin), - Anchor = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft, - Origin = rightAligned ? Anchor.CentreRight : Anchor.CentreLeft, + Anchor = anchor, + Origin = bottomAligned ? Anchor.CentreLeft : anchor, Alpha = 0, + Rotation = bottomAligned ? 270 : 0 }; completeDisplayLoading(display); diff --git a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs index f99c84fc01..89f135de7f 100644 --- a/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs +++ b/osu.Game/Screens/Play/HUD/HitErrorMeters/BarHitErrorMeter.cs @@ -99,7 +99,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters Size = new Vector2(10), Icon = FontAwesome.Solid.ShippingFast, Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, + Origin = Anchor.Centre, + // undo any layout rotation to display the icon the correct orientation + Rotation = -Rotation, }, new SpriteIcon { @@ -107,7 +109,9 @@ namespace osu.Game.Screens.Play.HUD.HitErrorMeters Size = new Vector2(10), Icon = FontAwesome.Solid.Bicycle, Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, + Origin = Anchor.Centre, + // undo any layout rotation to display the icon the correct orientation + Rotation = -Rotation, } } }, From e8235757512030f48f8ce423ba57f46b7c153702 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 14 Oct 2020 13:43:56 +0200 Subject: [PATCH 3961/6909] Lock screen rotation while in gameplay. --- osu.Android/GameplayScreenRotationLocker.cs | 31 +++++++++++++++++++++ osu.Android/OsuGameActivity.cs | 4 +++ osu.Android/OsuGameAndroid.cs | 6 ++++ osu.Android/osu.Android.csproj | 3 +- 4 files changed, 43 insertions(+), 1 deletion(-) create mode 100644 osu.Android/GameplayScreenRotationLocker.cs diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs new file mode 100644 index 0000000000..d1f4caba52 --- /dev/null +++ b/osu.Android/GameplayScreenRotationLocker.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 Android.Content.PM; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game; + +namespace osu.Android +{ + public class GameplayScreenRotationLocker : Component + { + private Bindable localUserPlaying; + + [BackgroundDependencyLoader] + private void load(OsuGame game) + { + localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); + localUserPlaying.BindValueChanged(_ => updateLock()); + } + + private void updateLock() + { + OsuGameActivity.Activity.RunOnUiThread(() => + { + OsuGameActivity.Activity.RequestedOrientation = localUserPlaying.Value ? ScreenOrientation.Locked : ScreenOrientation.FullUser; + }); + } + } +} diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index db73bb7e7f..c2b28f3de4 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -12,10 +12,14 @@ namespace osu.Android [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] public class OsuGameActivity : AndroidGameActivity { + internal static Activity Activity; + protected override Framework.Game CreateGame() => new OsuGameAndroid(); protected override void OnCreate(Bundle savedInstanceState) { + Activity = this; + // The default current directory on android is '/'. // On some devices '/' maps to the app data directory. On others it maps to the root of the internal storage. // In order to have a consistent current directory on all devices the full path of the app data directory is set as the current directory. diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 7542a2b997..887a8395e3 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -55,6 +55,12 @@ namespace osu.Android } } + protected override void LoadComplete() + { + base.LoadComplete(); + LoadComponentAsync(new GameplayScreenRotationLocker(), Add); + } + protected override UpdateManager CreateUpdateManager() => new SimpleUpdateManager(); } } diff --git a/osu.Android/osu.Android.csproj b/osu.Android/osu.Android.csproj index 0598a50530..a2638e95c8 100644 --- a/osu.Android/osu.Android.csproj +++ b/osu.Android/osu.Android.csproj @@ -21,6 +21,7 @@ r8 + @@ -53,4 +54,4 @@ - + \ No newline at end of file From 703f58bb2f0cae677c237a8c206458ce7df34180 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 11:54:11 +0900 Subject: [PATCH 3962/6909] Remove last.fm support Has been broken for ages, and their service isn't really something people use these days. --- osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs | 1 - osu.Game/Users/User.cs | 3 --- 2 files changed, 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index c27b5f4b4a..946831d13b 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -135,7 +135,6 @@ namespace osu.Game.Overlays.Profile.Header anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Twitter, "@" + user.Twitter, $@"https://twitter.com/{user.Twitter}"); anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Discord, user.Discord); anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Skype, user.Skype, @"skype:" + user.Skype + @"?chat"); - anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Lastfm, user.Lastfm, $@"https://last.fm/users/{user.Lastfm}"); anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Link, websiteWithoutProtocol, user.Website); // If no information was added to the bottomLinkContainer, hide it to avoid unwanted padding diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index f8bb8f4c6a..89786e3bd8 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -111,9 +111,6 @@ namespace osu.Game.Users [JsonProperty(@"twitter")] public string Twitter; - [JsonProperty(@"lastfm")] - public string Lastfm; - [JsonProperty(@"skype")] public string Skype; From 39a74536f24d8bafd6bf787311024db647fd2757 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 12:48:31 +0900 Subject: [PATCH 3963/6909] Update inspections --- .idea/.idea.osu.Desktop/.idea/modules.xml | 2 +- osu.sln.DotSettings | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.idea/.idea.osu.Desktop/.idea/modules.xml b/.idea/.idea.osu.Desktop/.idea/modules.xml index fe63f5faf3..680312ad27 100644 --- a/.idea/.idea.osu.Desktop/.idea/modules.xml +++ b/.idea/.idea.osu.Desktop/.idea/modules.xml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 64f3d41acb..3ef419c572 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -199,7 +199,9 @@ WARNING WARNING WARNING + WARNING HINT + WARNING WARNING DO_NOT_SHOW DO_NOT_SHOW @@ -773,6 +775,7 @@ See the LICENCE file in the repository root for full licence text. <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + True True True True From cc41845f56bc1a65fa10e01a1584334b6fd7c063 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 12:49:31 +0900 Subject: [PATCH 3964/6909] Add missing string function ordinal specifications --- .../Screens/Drawings/DrawingsScreen.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 2 +- osu.Game/OsuGame.cs | 4 ++-- osu.Game/Rulesets/RulesetStore.cs | 2 +- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 13 +++++++------ osu.Game/Utils/SentryLogger.cs | 2 +- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs index e10154b722..4c3adeae76 100644 --- a/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs +++ b/osu.Game.Tournament/Screens/Drawings/DrawingsScreen.cs @@ -234,7 +234,7 @@ namespace osu.Game.Tournament.Screens.Drawings if (string.IsNullOrEmpty(line)) continue; - if (line.ToUpperInvariant().StartsWith("GROUP")) + if (line.ToUpperInvariant().StartsWith("GROUP", StringComparison.Ordinal)) continue; // ReSharper disable once AccessToModifiedClosure diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index c15240a4f6..7b377e481f 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -92,7 +92,7 @@ namespace osu.Game.Beatmaps.Formats { var pair = SplitKeyVal(line); - bool isCombo = pair.Key.StartsWith(@"Combo"); + bool isCombo = pair.Key.StartsWith(@"Combo", StringComparison.Ordinal); string[] split = pair.Value.Split(','); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d315b213ab..56cced9c04 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -181,7 +181,7 @@ namespace osu.Game if (args?.Length > 0) { - var paths = args.Where(a => !a.StartsWith(@"-")).ToArray(); + var paths = args.Where(a => !a.StartsWith(@"-", StringComparison.Ordinal)).ToArray(); if (paths.Length > 0) Task.Run(() => Import(paths)); } @@ -289,7 +289,7 @@ namespace osu.Game public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ => { - if (url.StartsWith("/")) + if (url.StartsWith("/", StringComparison.Ordinal)) url = $"{API.Endpoint}{url}"; externalLinkOpener.OpenUrlExternally(url); diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index 5d93f5186b..c12d418771 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -100,7 +100,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 => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo)) == null) + if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) context.RulesetInfo.Add(r.RulesetInfo); } diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index a9d88e77ad..3dbec23194 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.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.Globalization; @@ -115,16 +116,16 @@ namespace osu.Game.Skinning currentConfig.MinimumColumnWidth = minWidth; break; - case string _ when pair.Key.StartsWith("Colour"): + case string _ when pair.Key.StartsWith("Colour", StringComparison.Ordinal): HandleColours(currentConfig, line); break; // Custom sprite paths - 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"): - case string _ when pair.Key.StartsWith("Lighting"): + case string _ when pair.Key.StartsWith("NoteImage", StringComparison.Ordinal): + case string _ when pair.Key.StartsWith("KeyImage", StringComparison.Ordinal): + case string _ when pair.Key.StartsWith("Hit", StringComparison.Ordinal): + case string _ when pair.Key.StartsWith("Stage", StringComparison.Ordinal): + case string _ when pair.Key.StartsWith("Lighting", StringComparison.Ordinal): currentConfig.ImageLookups[pair.Key] = pair.Value; break; } diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index 981251784e..e8e41cdbbe 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -45,7 +45,7 @@ namespace osu.Game.Utils // since we let unhandled exceptions go ignored at times, we want to ensure they don't get submitted on subsequent reports. if (lastException != null && - lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace)) + lastException.Message == exception.Message && exception.StackTrace.StartsWith(lastException.StackTrace, StringComparison.Ordinal)) return; lastException = exception; From 88f74921fb9de5f01ddfb7be72cb145d9ca14a2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 12:49:39 +0900 Subject: [PATCH 3965/6909] Update with new r# inspections --- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index c02075bea9..603b5d4956 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.Gameplay createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha); AddUntilStep("wait for load", () => hudOverlay.IsAlive); - AddAssert("initial alpha was less than 1", () => initialAlpha != null && initialAlpha < 1); + AddAssert("initial alpha was less than 1", () => initialAlpha < 1); } [Test] diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 0336c74386..1527d20f54 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -79,9 +79,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementNewCombo() { - if (currentPlacement == null) return; - - if (currentPlacement.HitObject is IHasComboInformation c) + if (currentPlacement?.HitObject is IHasComboInformation c) c.NewCombo = NewCombo.Value == TernaryState.True; } From 88ffcb923408c0e9485f2e28bf38efa98409901a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 12:58:34 +0900 Subject: [PATCH 3966/6909] Update EndsWith usages --- .../Beatmaps/Formats/LegacyBeatmapEncoderTest.cs | 2 +- .../Visual/SongSelect/TestSceneBeatmapCarousel.cs | 2 +- osu.Game.Tests/WaveformTestBeatmap.cs | 5 +++-- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Beatmaps/BeatmapSetInfo.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 4 ++-- osu.Game/Screens/Select/FilterQueryParser.cs | 8 ++++---- osu.Game/Skinning/LegacySkin.cs | 4 ++-- osu.Game/Updater/SimpleUpdateManager.cs | 7 ++++--- 9 files changed, 20 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs index 8b22309033..0784109158 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapEncoderTest.cs @@ -30,7 +30,7 @@ namespace osu.Game.Tests.Beatmaps.Formats { private static readonly DllResourceStore beatmaps_resource_store = TestResources.GetStore(); - private static IEnumerable allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu")); + private static IEnumerable allBeatmaps = beatmaps_resource_store.GetAvailableResources().Where(res => res.EndsWith(".osu", StringComparison.Ordinal)); [TestCaseSource(nameof(allBeatmaps))] public void TestEncodeDecodeStability(string name) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 3aff390a47..8669235a7a 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -394,7 +394,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("Sort by author", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Author }, false)); AddAssert("Check zzzzz is at bottom", () => carousel.BeatmapSets.Last().Metadata.AuthorString == "zzzzz"); AddStep("Sort by artist", () => carousel.Filter(new FilterCriteria { Sort = SortMode.Artist }, false)); - AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!")); + AddAssert($"Check #{set_count} is at bottom", () => carousel.BeatmapSets.Last().Metadata.Title.EndsWith($"#{set_count}!", StringComparison.Ordinal)); } [Test] diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index 7dc5ce1d7f..f9613d9e25 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.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.Audio; @@ -59,7 +60,7 @@ namespace osu.Game.Tests get { using (var reader = getZipReader()) - return reader.Filenames.First(f => f.EndsWith(".mp3")); + return reader.Filenames.First(f => f.EndsWith(".mp3", StringComparison.Ordinal)); } } @@ -73,7 +74,7 @@ namespace osu.Game.Tests protected override Beatmap CreateBeatmap() { using (var reader = getZipReader()) - using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu")))) + using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu", StringComparison.Ordinal)))) using (var beatmapReader = new LineBufferedReader(beatmapStream)) return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader); } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4c75069f08..370e82b468 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -389,7 +389,7 @@ namespace osu.Game.Beatmaps protected override BeatmapSetInfo CreateModel(ArchiveReader reader) { // let's make sure there are actually .osu files to import. - string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu")); + string mapName = reader.Filenames.FirstOrDefault(f => f.EndsWith(".osu", StringComparison.OrdinalIgnoreCase)); if (string.IsNullOrEmpty(mapName)) { @@ -417,7 +417,7 @@ namespace osu.Game.Beatmaps { var beatmapInfos = new List(); - foreach (var file in files.Where(f => f.Filename.EndsWith(".osu"))) + foreach (var file in files.Where(f => f.Filename.EndsWith(".osu", StringComparison.OrdinalIgnoreCase))) { using (var raw = Files.Store.GetStream(file.FileInfo.StoragePath)) using (var ms = new MemoryStream()) // we need a memory stream so we can seek diff --git a/osu.Game/Beatmaps/BeatmapSetInfo.cs b/osu.Game/Beatmaps/BeatmapSetInfo.cs index b76d780860..7bc1c8c7b9 100644 --- a/osu.Game/Beatmaps/BeatmapSetInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetInfo.cs @@ -57,7 +57,7 @@ namespace osu.Game.Beatmaps public string Hash { get; set; } - public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb"))?.Filename; + public string StoryboardFile => Files?.Find(f => f.Filename.EndsWith(".osb", StringComparison.OrdinalIgnoreCase))?.Filename; public List Files { get; set; } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 3292936f5f..b947056ebd 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -279,7 +279,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)).OrderBy(f => f.Filename)) + foreach (TFileModel file in item.Files.Where(f => HashableFileTypes.Any(ext => f.Filename.EndsWith(ext, StringComparison.OrdinalIgnoreCase))).OrderBy(f => f.Filename)) { using (Stream s = Files.Store.GetStream(file.FileInfo.StoragePath)) s.CopyTo(hashable); @@ -593,7 +593,7 @@ namespace osu.Game.Database var fileInfos = new List(); string prefix = reader.Filenames.GetCommonPrefix(); - if (!(prefix.EndsWith("/") || prefix.EndsWith("\\"))) + if (!(prefix.EndsWith("/", StringComparison.Ordinal) || prefix.EndsWith("\\", StringComparison.Ordinal))) prefix = string.Empty; // import files to manager diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 39fa4f777d..fa2beb2652 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -79,10 +79,10 @@ namespace osu.Game.Screens.Select } private static int getLengthScale(string value) => - value.EndsWith("ms") ? 1 : - value.EndsWith("s") ? 1000 : - value.EndsWith("m") ? 60000 : - value.EndsWith("h") ? 3600000 : 1000; + value.EndsWith("ms", StringComparison.Ordinal) ? 1 : + value.EndsWith("s", StringComparison.Ordinal) ? 1000 : + value.EndsWith("m", StringComparison.Ordinal) ? 60000 : + value.EndsWith("h", StringComparison.Ordinal) ? 3600000 : 1000; private static bool parseFloatWithPoint(string value, out float result) => float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result); diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e1cd095ba8..069a887f63 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -422,7 +422,7 @@ namespace osu.Game.Skinning // Fall back to using the last piece for components coming from lazer (e.g. "Gameplay/osu/approachcircle" -> "approachcircle"). string lastPiece = componentName.Split('/').Last(); - yield return componentName.StartsWith("Gameplay/taiko/") ? "taiko-" + lastPiece : lastPiece; + yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece; } private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample) @@ -433,7 +433,7 @@ namespace osu.Game.Skinning // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin. // using .EndsWith() is intentional as it ensures parity in all edge cases // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not). - lookupNames = hitSample.LookupNames.Where(name => !name.EndsWith(hitSample.Suffix)); + lookupNames = hitSample.LookupNames.Where(name => !name.EndsWith(hitSample.Suffix, StringComparison.Ordinal)); // also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort. // going forward specifying banks shall always be required, even for elements that wouldn't require it on stable, diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index b5fcb56c06..4ebf2a7368 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.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.Tasks; using Newtonsoft.Json; @@ -73,15 +74,15 @@ namespace osu.Game.Updater switch (RuntimeInfo.OS) { case RuntimeInfo.Platform.Windows: - bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe")); + bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe", StringComparison.Ordinal)); break; case RuntimeInfo.Platform.MacOsx: - bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip")); + bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip", StringComparison.Ordinal)); break; case RuntimeInfo.Platform.Linux: - bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage")); + bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".AppImage", StringComparison.Ordinal)); break; case RuntimeInfo.Platform.iOS: From aea31d1582e2c6120088e864479c2d6131d3e978 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 13:07:00 +0900 Subject: [PATCH 3967/6909] Fix editor not seeking by full beat when track is playing This is expected behaviour as my osu-stable, and I still stand behind the reasoning behind it. Closes #10519. --- osu.Game/Screens/Edit/Editor.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 7444369e84..c3560dff38 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -597,10 +597,20 @@ namespace osu.Game.Screens.Edit { double amount = e.ShiftPressed ? 4 : 1; + bool trackPlaying = clock.IsRunning; + + if (trackPlaying) + { + // generally users are not looking to perform tiny seeks when the track is playing, + // so seeks should always be by one full beat, bypassing the beatDivisor. + // this multiplication undoes the division that will be applied in the underlying seek operation. + amount *= beatDivisor.Value; + } + if (direction < 1) - clock.SeekBackward(!clock.IsRunning, amount); + clock.SeekBackward(!trackPlaying, amount); else - clock.SeekForward(!clock.IsRunning, amount); + clock.SeekForward(!trackPlaying, amount); } private void exportBeatmap() From 085d8d0ecbfa233c8ad6864ecf03885a3ba9cc7a Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Fri, 16 Oct 2020 06:16:20 +0200 Subject: [PATCH 3968/6909] Add support for ScorePrefix and ScoreOverlap values in legacy skins --- osu.Game/Skinning/LegacyAccuracyCounter.cs | 8 +++++++- osu.Game/Skinning/LegacyScoreCounter.cs | 11 +++++++++-- osu.Game/Skinning/LegacySkinConfiguration.cs | 2 ++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 9354b2b3bc..0a64545aee 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -15,6 +15,9 @@ namespace osu.Game.Skinning { private readonly ISkin skin; + private readonly string scorePrefix; + private readonly int scoreOverlap; + public LegacyAccuracyCounter(ISkin skin) { Anchor = Anchor.TopRight; @@ -24,16 +27,19 @@ namespace osu.Game.Skinning Margin = new MarginPadding(10); this.skin = skin; + scorePrefix = skin.GetConfig(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score"; + scoreOverlap = skin.GetConfig(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2; } [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } protected sealed override OsuSpriteText CreateSpriteText() => - new LegacySpriteText(skin, "score" /*, true*/) + new LegacySpriteText(skin, scorePrefix) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + Spacing = new Vector2(-scoreOverlap, 0) }; protected override void Update() diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index f94bef6652..e931497564 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Skinning { @@ -12,6 +13,9 @@ namespace osu.Game.Skinning { private readonly ISkin skin; + private readonly string scorePrefix; + private readonly int scoreOverlap; + protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; @@ -24,18 +28,21 @@ namespace osu.Game.Skinning Origin = Anchor.TopRight; this.skin = skin; + scorePrefix = skin.GetConfig(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score"; + scoreOverlap = skin.GetConfig(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2; - // base class uses int for display, but externally we bind to ScoreProcesssor as a double for now. + // base class uses int for display, but externally we bind to ScoreProcessor as a double for now. Current.BindValueChanged(v => base.Current.Value = (int)v.NewValue); Margin = new MarginPadding(10); } protected sealed override OsuSpriteText CreateSpriteText() => - new LegacySpriteText(skin, "score" /*, true*/) + new LegacySpriteText(skin, scorePrefix) { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + Spacing = new Vector2(-scoreOverlap, 0) }; } } diff --git a/osu.Game/Skinning/LegacySkinConfiguration.cs b/osu.Game/Skinning/LegacySkinConfiguration.cs index 828804b9cb..84a834ec22 100644 --- a/osu.Game/Skinning/LegacySkinConfiguration.cs +++ b/osu.Game/Skinning/LegacySkinConfiguration.cs @@ -17,6 +17,8 @@ namespace osu.Game.Skinning Version, ComboPrefix, ComboOverlap, + ScorePrefix, + ScoreOverlap, AnimationFramerate, LayeredHitSounds } From 83482ca15c53d95b47bb0324cad7ec45ad298170 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 13:21:47 +0900 Subject: [PATCH 3969/6909] Fix one more missed occurrence --- osu.Game/Scoring/ScoreManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 5a6da53839..cce6153953 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -57,7 +57,7 @@ namespace osu.Game.Scoring if (archive == null) return null; - using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr")))) + using (var stream = archive.GetStream(archive.Filenames.First(f => f.EndsWith(".osr", StringComparison.OrdinalIgnoreCase)))) { try { From df1db8611c73c0f2d87154e3895d6f8b0f156705 Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Fri, 16 Oct 2020 08:36:20 +0200 Subject: [PATCH 3970/6909] move skin-specific config retrieval to GetDrawableComponent --- osu.Game/Skinning/HUDSkinComponents.cs | 4 +++- osu.Game/Skinning/LegacyAccuracyCounter.cs | 13 +------------ osu.Game/Skinning/LegacyScoreCounter.cs | 14 +------------- osu.Game/Skinning/LegacySkin.cs | 10 ++++++++++ 4 files changed, 15 insertions(+), 26 deletions(-) diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs index d690a23dee..6ec575e106 100644 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ b/osu.Game/Skinning/HUDSkinComponents.cs @@ -7,6 +7,8 @@ namespace osu.Game.Skinning { ComboCounter, ScoreCounter, - AccuracyCounter + ScoreText, + AccuracyCounter, + AccuracyText } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 0a64545aee..6c194a06d3 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -15,9 +15,6 @@ namespace osu.Game.Skinning { private readonly ISkin skin; - private readonly string scorePrefix; - private readonly int scoreOverlap; - public LegacyAccuracyCounter(ISkin skin) { Anchor = Anchor.TopRight; @@ -27,20 +24,12 @@ namespace osu.Game.Skinning Margin = new MarginPadding(10); this.skin = skin; - scorePrefix = skin.GetConfig(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score"; - scoreOverlap = skin.GetConfig(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2; } [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - protected sealed override OsuSpriteText CreateSpriteText() => - new LegacySpriteText(skin, scorePrefix) - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Spacing = new Vector2(-scoreOverlap, 0) - }; + protected sealed override OsuSpriteText CreateSpriteText() => skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyText)) as OsuSpriteText ?? new OsuSpriteText(); protected override void Update() { diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index e931497564..41bf35722b 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -5,7 +5,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osuTK; namespace osu.Game.Skinning { @@ -13,9 +12,6 @@ namespace osu.Game.Skinning { private readonly ISkin skin; - private readonly string scorePrefix; - private readonly int scoreOverlap; - protected override double RollingDuration => 1000; protected override Easing RollingEasing => Easing.Out; @@ -28,8 +24,6 @@ namespace osu.Game.Skinning Origin = Anchor.TopRight; this.skin = skin; - scorePrefix = skin.GetConfig(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score"; - scoreOverlap = skin.GetConfig(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2; // base class uses int for display, but externally we bind to ScoreProcessor as a double for now. Current.BindValueChanged(v => base.Current.Value = (int)v.NewValue); @@ -37,12 +31,6 @@ namespace osu.Game.Skinning Margin = new MarginPadding(10); } - protected sealed override OsuSpriteText CreateSpriteText() => - new LegacySpriteText(skin, scorePrefix) - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Spacing = new Vector2(-scoreOverlap, 0) - }; + protected sealed override OsuSpriteText CreateSpriteText() => skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) as OsuSpriteText ?? new OsuSpriteText(); } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e1cd095ba8..f5265f2d6e 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -19,6 +19,7 @@ using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Play.HUD; +using osuTK; using osuTK.Graphics; namespace osu.Game.Skinning @@ -347,6 +348,15 @@ namespace osu.Game.Skinning case HUDSkinComponents.AccuracyCounter: return new LegacyAccuracyCounter(this); + + case HUDSkinComponents.ScoreText: + case HUDSkinComponents.AccuracyText: + string scorePrefix = GetConfig(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score"; + int scoreOverlap = GetConfig(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2; + return new LegacySpriteText(this, scorePrefix) + { + Spacing = new Vector2(-scoreOverlap, 0) + }; } return null; From c0a1f2158cdfbc5539a8dd8e9d462c49e1b17c95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 13:42:50 +0900 Subject: [PATCH 3971/6909] Add basic component structure for skinnable health displays --- .../TestSceneSkinnableHealthDisplay.cs | 62 +++++++++++++++++++ ...althDisplay.cs => DefaultHealthDisplay.cs} | 10 ++- osu.Game/Screens/Play/HUD/HealthDisplay.cs | 9 ++- osu.Game/Screens/Play/HUD/IHealthDisplay.cs | 26 ++++++++ osu.Game/Screens/Play/HUDOverlay.cs | 11 +--- .../Screens/Play/SkinnableHealthDisplay.cs | 47 ++++++++++++++ osu.Game/Skinning/HUDSkinComponents.cs | 3 +- 7 files changed, 154 insertions(+), 14 deletions(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs rename osu.Game/Screens/Play/HUD/{StandardHealthDisplay.cs => DefaultHealthDisplay.cs} (92%) create mode 100644 osu.Game/Screens/Play/HUD/IHealthDisplay.cs create mode 100644 osu.Game/Screens/Play/SkinnableHealthDisplay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs new file mode 100644 index 0000000000..181fc8ce98 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Judgements; +using osu.Game.Screens.Play; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSkinnableHealthDisplay : SkinnableTestScene + { + private IEnumerable healthDisplays => CreatedDrawables.OfType(); + + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("Create health displays", () => + { + SetContents(() => new SkinnableHealthDisplay()); + }); + AddStep(@"Reset all", delegate + { + foreach (var s in healthDisplays) + s.Current.Value = 1; + }); + } + + [Test] + public void TestHealthDisplayIncrementing() + { + AddRepeatStep(@"decrease hp", delegate + { + foreach (var healthDisplay in healthDisplays) + healthDisplay.Current.Value -= 0.08f; + }, 10); + + AddRepeatStep(@"increase hp without flash", delegate + { + foreach (var healthDisplay in healthDisplays) + healthDisplay.Current.Value += 0.1f; + }, 3); + + AddRepeatStep(@"increase hp with flash", delegate + { + foreach (var healthDisplay in healthDisplays) + { + healthDisplay.Current.Value += 0.1f; + healthDisplay.Flash(new JudgementResult(null, new OsuJudgement())); + } + }, 3); + } + } +} diff --git a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs similarity index 92% rename from osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs rename to osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index fc4a1a5d83..ae78d19c2d 100644 --- a/osu.Game/Screens/Play/HUD/StandardHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -16,7 +16,7 @@ using osu.Framework.Utils; namespace osu.Game.Screens.Play.HUD { - public class StandardHealthDisplay : HealthDisplay, IHasAccentColour + public class DefaultHealthDisplay : HealthDisplay, IHasAccentColour { /// /// The base opacity of the glow. @@ -71,8 +71,12 @@ namespace osu.Game.Screens.Play.HUD } } - public StandardHealthDisplay() + public DefaultHealthDisplay() { + Size = new Vector2(1, 5); + RelativeSizeAxes = Axes.X; + Margin = new MarginPadding { Top = 20 }; + Children = new Drawable[] { new Box @@ -103,7 +107,7 @@ namespace osu.Game.Screens.Play.HUD GlowColour = colours.BlueDarker; } - public void Flash(JudgementResult result) + public override void Flash(JudgementResult result) { if (!result.IsHit) return; diff --git a/osu.Game/Screens/Play/HUD/HealthDisplay.cs b/osu.Game/Screens/Play/HUD/HealthDisplay.cs index edc9dedf24..5c43e00192 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.Judgements; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; @@ -12,14 +13,18 @@ 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 abstract class HealthDisplay : Container, IHealthDisplay { - public readonly BindableDouble Current = new BindableDouble(1) + public Bindable Current { get; } = new BindableDouble(1) { MinValue = 0, MaxValue = 1 }; + public virtual void Flash(JudgementResult result) + { + } + /// /// Bind the tracked fields of to this health display. /// diff --git a/osu.Game/Screens/Play/HUD/IHealthDisplay.cs b/osu.Game/Screens/Play/HUD/IHealthDisplay.cs new file mode 100644 index 0000000000..b1a64bd844 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/IHealthDisplay.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.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Judgements; + +namespace osu.Game.Screens.Play.HUD +{ + /// + /// An interface providing a set of methods to update a health display. + /// + public interface IHealthDisplay : IDrawable + { + /// + /// The current health to be displayed. + /// + Bindable Current { get; } + + /// + /// Flash the display for a specified result type. + /// + /// The result type. + void Flash(JudgementResult result); + } +} diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index fa914c0ebc..0d92611e0e 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play public readonly SkinnableComboCounter ComboCounter; public readonly SkinnableScoreCounter ScoreCounter; public readonly SkinnableAccuracyCounter AccuracyCounter; - public readonly HealthDisplay HealthDisplay; + public readonly SkinnableHealthDisplay HealthDisplay; public readonly SongProgress Progress; public readonly ModDisplay ModDisplay; public readonly HitErrorDisplay HitErrorDisplay; @@ -272,12 +272,7 @@ namespace osu.Game.Screens.Play protected virtual SkinnableComboCounter CreateComboCounter() => new SkinnableComboCounter(); - protected virtual HealthDisplay CreateHealthDisplay() => new StandardHealthDisplay - { - Size = new Vector2(1, 5), - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 20 } - }; + protected virtual SkinnableHealthDisplay CreateHealthDisplay() => new SkinnableHealthDisplay(); protected virtual FailingLayer CreateFailingLayer() => new FailingLayer { @@ -320,7 +315,7 @@ namespace osu.Game.Screens.Play AccuracyCounter?.Current.BindTo(processor.Accuracy); ComboCounter?.Current.BindTo(processor.Combo); - if (HealthDisplay is StandardHealthDisplay shd) + if (HealthDisplay.Drawable is IHealthDisplay shd) processor.NewJudgement += shd.Flash; } diff --git a/osu.Game/Screens/Play/SkinnableHealthDisplay.cs b/osu.Game/Screens/Play/SkinnableHealthDisplay.cs new file mode 100644 index 0000000000..5b77343278 --- /dev/null +++ b/osu.Game/Screens/Play/SkinnableHealthDisplay.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.Bindables; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Play.HUD; +using osu.Game.Skinning; + +namespace osu.Game.Screens.Play +{ + public class SkinnableHealthDisplay : SkinnableDrawable, IHealthDisplay + { + public Bindable Current { get; } = new Bindable(); + + public void Flash(JudgementResult result) => skinnedCounter?.Flash(result); + + private HealthProcessor processor; + + public void BindHealthProcessor(HealthProcessor processor) + { + if (this.processor != null) + throw new InvalidOperationException("Can't bind to a processor more than once"); + + this.processor = processor; + + Current.BindTo(processor.Health); + } + + public SkinnableHealthDisplay() + : base(new HUDSkinComponent(HUDSkinComponents.HealthDisplay), _ => new DefaultHealthDisplay()) + { + CentreComponent = false; + } + + private IHealthDisplay skinnedCounter; + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + + skinnedCounter = Drawable as IHealthDisplay; + skinnedCounter?.Current.BindTo(Current); + } + } +} diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs index d690a23dee..8772704cef 100644 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ b/osu.Game/Skinning/HUDSkinComponents.cs @@ -7,6 +7,7 @@ namespace osu.Game.Skinning { ComboCounter, ScoreCounter, - AccuracyCounter + AccuracyCounter, + HealthDisplay } } From e89c5c3b3cadfa20f529b76028cf8d9bba5d3fa0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 14:39:02 +0900 Subject: [PATCH 3972/6909] Add dynamic compile exceptions to fix skin test scenes --- osu.Game/Beatmaps/BeatmapManager.cs | 2 ++ osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs | 2 ++ osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 2 ++ osu.Game/Skinning/SkinManager.cs | 2 ++ 4 files changed, 8 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4c75069f08..f3586ec0ec 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -19,6 +19,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.Database; using osu.Game.IO; @@ -36,6 +37,7 @@ namespace osu.Game.Beatmaps /// /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// + [ExcludeFromDynamicCompile] public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable { /// diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index 16207c7d2a..cb4884aa51 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -13,6 +13,7 @@ using osu.Framework.Development; using osu.Framework.IO.Network; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -23,6 +24,7 @@ namespace osu.Game.Beatmaps { public partial class BeatmapManager { + [ExcludeFromDynamicCompile] private class BeatmapOnlineLookupQueue : IDisposable { private readonly IAPIProvider api; diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 362c99ea3f..f5c0d97c1f 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -8,6 +8,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Logging; +using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.IO; using osu.Game.Skinning; @@ -17,6 +18,7 @@ namespace osu.Game.Beatmaps { public partial class BeatmapManager { + [ExcludeFromDynamicCompile] private class BeatmapManagerWorkingBeatmap : WorkingBeatmap { private readonly IResourceStore store; diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 7af400e807..37a2309e01 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -18,12 +18,14 @@ using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Game.Audio; using osu.Game.Database; using osu.Game.IO.Archives; namespace osu.Game.Skinning { + [ExcludeFromDynamicCompile] public class SkinManager : ArchiveModelManager, ISkinSource { private readonly AudioManager audio; From 5be9e30cd0a8005eba7c1592c16016cbcb126e92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 14:39:45 +0900 Subject: [PATCH 3973/6909] Add legacy implementation --- osu.Game/Skinning/LegacyHealthDisplay.cs | 101 +++++++++++++++++++++++ osu.Game/Skinning/LegacySkin.cs | 3 + 2 files changed, 104 insertions(+) create mode 100644 osu.Game/Skinning/LegacyHealthDisplay.cs diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs new file mode 100644 index 0000000000..26617ea422 --- /dev/null +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -0,0 +1,101 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using 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.Judgements; +using osu.Game.Screens.Play.HUD; +using osuTK; + +namespace osu.Game.Skinning +{ + public class LegacyHealthDisplay : CompositeDrawable, IHealthDisplay + { + private readonly Skin skin; + private Sprite fill; + private Marker marker; + public Bindable Current { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; + + public LegacyHealthDisplay(Skin skin) + { + this.skin = skin; + } + + [BackgroundDependencyLoader] + private void load() + { + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new Sprite + { + Texture = skin.GetTexture("scorebar-bg") + }, + fill = new Sprite + { + Texture = skin.GetTexture("scorebar-colour"), + Position = new Vector2(7.5f, 7.8f) * 1.6f + }, + marker = new Marker(skin) + { + Current = { BindTarget = Current }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(updateHp, true); + } + + private void updateHp(ValueChangedEvent hp) + { + if (fill.Texture != null) + fill.ResizeWidthTo((float)(fill.Texture.DisplayWidth * hp.NewValue), 500, Easing.OutQuint); + } + + protected override void Update() + { + base.Update(); + + marker.Position = fill.Position + new Vector2(fill.DrawWidth, fill.DrawHeight / 2); + } + + public void Flash(JudgementResult result) + { + marker.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); + } + + private class Marker : CompositeDrawable + { + public Bindable Current { get; } = new Bindable(); + + public Marker(Skin skin) + { + Origin = Anchor.Centre; + + if (skin.GetTexture("scorebar-ki") != null) + { + // TODO: old style (marker changes as health decreases) + } + else + { + InternalChildren = new Drawable[] + { + new Sprite + { + Texture = skin.GetTexture("scorebar-marker"), + Origin = Anchor.Centre, + } + }; + } + } + } + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e1cd095ba8..f02d70fc2a 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -347,6 +347,9 @@ namespace osu.Game.Skinning case HUDSkinComponents.AccuracyCounter: return new LegacyAccuracyCounter(this); + + case HUDSkinComponents.HealthDisplay: + return new LegacyHealthDisplay(this); } return null; From a810f56ec87f0aeeb1d454bceb72eb6a30cc083d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 14:49:05 +0900 Subject: [PATCH 3974/6909] Move "flash on hit only" logic to binding --- osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs | 8 +------- osu.Game/Screens/Play/HUDOverlay.cs | 10 ++++++++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs index ae78d19c2d..b550b469e9 100644 --- a/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/DefaultHealthDisplay.cs @@ -107,13 +107,7 @@ namespace osu.Game.Screens.Play.HUD GlowColour = colours.BlueDarker; } - public override void Flash(JudgementResult result) - { - if (!result.IsHit) - return; - - Scheduler.AddOnce(flash); - } + public override void Flash(JudgementResult result) => Scheduler.AddOnce(flash); private void flash() { diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 0d92611e0e..ac74dc22d3 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -315,8 +315,14 @@ namespace osu.Game.Screens.Play AccuracyCounter?.Current.BindTo(processor.Accuracy); ComboCounter?.Current.BindTo(processor.Combo); - if (HealthDisplay.Drawable is IHealthDisplay shd) - processor.NewJudgement += shd.Flash; + if (HealthDisplay is IHealthDisplay shd) + { + processor.NewJudgement += judgement => + { + if (judgement.IsHit) + shd.Flash(judgement); + }; + } } protected virtual void BindHealthProcessor(HealthProcessor processor) From f28bcabae72a49841b59f7428455004944a9ace6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 14:54:46 +0900 Subject: [PATCH 3975/6909] Avoid transforms per hp change --- osu.Game/Skinning/LegacyHealthDisplay.cs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 26617ea422..2fac11d7a4 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -1,11 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Screens.Play.HUD; using osuTK; @@ -17,6 +19,9 @@ namespace osu.Game.Skinning private readonly Skin skin; private Sprite fill; private Marker marker; + + private float maxFillWidth; + public Bindable Current { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; public LegacyHealthDisplay(Skin skin) @@ -45,25 +50,18 @@ namespace osu.Game.Skinning Current = { BindTarget = Current }, } }; - } - protected override void LoadComplete() - { - base.LoadComplete(); - - Current.BindValueChanged(updateHp, true); - } - - private void updateHp(ValueChangedEvent hp) - { - if (fill.Texture != null) - fill.ResizeWidthTo((float)(fill.Texture.DisplayWidth * hp.NewValue), 500, Easing.OutQuint); + maxFillWidth = fill.Width; } protected override void Update() { base.Update(); + fill.Width = Interpolation.ValueAt( + Math.Clamp(Clock.ElapsedFrameTime, 0, 200), + fill.Width, (float)Current.Value * maxFillWidth, 0, 200, Easing.OutQuint); + marker.Position = fill.Position + new Vector2(fill.DrawWidth, fill.DrawHeight / 2); } From 6d3a106a868774862b1fd4187cece7729723c30e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 15:10:39 +0900 Subject: [PATCH 3976/6909] Simplify texture lookups --- osu.Game/Skinning/LegacyHealthDisplay.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 2fac11d7a4..3691dbc731 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -7,6 +7,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.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Screens.Play.HUD; @@ -22,6 +23,8 @@ namespace osu.Game.Skinning private float maxFillWidth; + private Texture isNewStyle; + public Bindable Current { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; public LegacyHealthDisplay(Skin skin) @@ -34,15 +37,17 @@ namespace osu.Game.Skinning { AutoSizeAxes = Axes.Both; + isNewStyle = getTexture(skin, "marker"); + InternalChildren = new Drawable[] { new Sprite { - Texture = skin.GetTexture("scorebar-bg") + Texture = getTexture(skin, "bg") }, fill = new Sprite { - Texture = skin.GetTexture("scorebar-colour"), + Texture = getTexture(skin, "colour"), Position = new Vector2(7.5f, 7.8f) * 1.6f }, marker = new Marker(skin) @@ -78,7 +83,7 @@ namespace osu.Game.Skinning { Origin = Anchor.Centre; - if (skin.GetTexture("scorebar-ki") != null) + if (getTexture(skin, "ki") != null) { // TODO: old style (marker changes as health decreases) } @@ -88,12 +93,14 @@ namespace osu.Game.Skinning { new Sprite { - Texture = skin.GetTexture("scorebar-marker"), + Texture = getTexture(skin, "marker"), Origin = Anchor.Centre, } }; } } } + + private static Texture getTexture(Skin skin, string name) => skin.GetTexture($"scorebar-{name}"); } } From bdebf2f1a4f8127393115d99c8c172ce27fe85a8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 16:17:36 +0900 Subject: [PATCH 3977/6909] Fix skinnable test scene still not working with dynamic compilation --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index a856789d96..fe4f735325 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -35,12 +35,12 @@ namespace osu.Game.Tests.Visual } [BackgroundDependencyLoader] - private void load(AudioManager audio, SkinManager skinManager) + private void load(AudioManager audio, SkinManager skinManager, OsuGameBase game) { 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); + defaultSkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), audio); specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore(dllStore, "Resources/special_skin"), audio, true); oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore(dllStore, "Resources/old_skin"), audio, true); } From f0b15813e206bc8074102905cd52c6a986e414f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 16:18:29 +0900 Subject: [PATCH 3978/6909] Add support for both legacy styles --- .../Screens/Play/SkinnableHealthDisplay.cs | 6 +- osu.Game/Skinning/LegacyHealthDisplay.cs | 140 ++++++++++++------ 2 files changed, 103 insertions(+), 43 deletions(-) diff --git a/osu.Game/Screens/Play/SkinnableHealthDisplay.cs b/osu.Game/Screens/Play/SkinnableHealthDisplay.cs index 5b77343278..d35d15d665 100644 --- a/osu.Game/Screens/Play/SkinnableHealthDisplay.cs +++ b/osu.Game/Screens/Play/SkinnableHealthDisplay.cs @@ -12,7 +12,11 @@ namespace osu.Game.Screens.Play { public class SkinnableHealthDisplay : SkinnableDrawable, IHealthDisplay { - public Bindable Current { get; } = new Bindable(); + public Bindable Current { get; } = new BindableDouble(1) + { + MinValue = 0, + MaxValue = 1 + }; public void Flash(JudgementResult result) => skinnedCounter?.Flash(result); diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 3691dbc731..7d9a1dfc15 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -18,14 +18,18 @@ namespace osu.Game.Skinning public class LegacyHealthDisplay : CompositeDrawable, IHealthDisplay { private readonly Skin skin; - private Sprite fill; - private Marker marker; + private Drawable fill; + private LegacyMarker marker; private float maxFillWidth; - private Texture isNewStyle; + private bool isNewStyle; - public Bindable Current { get; } = new BindableDouble { MinValue = 0, MaxValue = 1 }; + public Bindable Current { get; } = new BindableDouble(1) + { + MinValue = 0, + MaxValue = 1 + }; public LegacyHealthDisplay(Skin skin) { @@ -37,25 +41,29 @@ namespace osu.Game.Skinning { AutoSizeAxes = Axes.Both; - isNewStyle = getTexture(skin, "marker"); + isNewStyle = getTexture(skin, "marker") != null; - InternalChildren = new Drawable[] + // background implementation is the same for both versions. + AddInternal(new Sprite { Texture = getTexture(skin, "bg") }); + + if (isNewStyle) { - new Sprite + AddRangeInternal(new[] { - Texture = getTexture(skin, "bg") - }, - fill = new Sprite + fill = new LegacyNewStyleFill(skin), + marker = new LegacyNewStyleMarker(skin), + }); + } + else + { + AddRangeInternal(new[] { - Texture = getTexture(skin, "colour"), - Position = new Vector2(7.5f, 7.8f) * 1.6f - }, - marker = new Marker(skin) - { - Current = { BindTarget = Current }, - } - }; + fill = new LegacyOldStyleFill(skin), + marker = new LegacyOldStyleMarker(skin), + }); + } + marker.Current.BindTo(Current); maxFillWidth = fill.Width; } @@ -70,37 +78,85 @@ namespace osu.Game.Skinning marker.Position = fill.Position + new Vector2(fill.DrawWidth, fill.DrawHeight / 2); } - public void Flash(JudgementResult result) - { - marker.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); - } + public void Flash(JudgementResult result) => marker.Flash(result); - private class Marker : CompositeDrawable - { - public Bindable Current { get; } = new Bindable(); + private static Texture getTexture(Skin skin, string name) => skin.GetTexture($"scorebar-{name}"); - public Marker(Skin skin) + public class LegacyOldStyleMarker : LegacyMarker + { + public LegacyOldStyleMarker(Skin skin) { - Origin = Anchor.Centre; - - if (getTexture(skin, "ki") != null) + InternalChildren = new Drawable[] { - // TODO: old style (marker changes as health decreases) - } - else - { - InternalChildren = new Drawable[] + new Sprite { - new Sprite - { - Texture = getTexture(skin, "marker"), - Origin = Anchor.Centre, - } - }; - } + Texture = getTexture(skin, "ki"), + Origin = Anchor.Centre, + } + }; } } - private static Texture getTexture(Skin skin, string name) => skin.GetTexture($"scorebar-{name}"); + public class LegacyNewStyleMarker : LegacyMarker + { + public LegacyNewStyleMarker(Skin skin) + { + InternalChildren = new Drawable[] + { + new Sprite + { + Texture = getTexture(skin, "marker"), + Origin = Anchor.Centre, + } + }; + } + } + + public class LegacyMarker : CompositeDrawable, IHealthDisplay + { + public Bindable Current { get; } = new Bindable(); + + public LegacyMarker() + { + Origin = Anchor.Centre; + } + + public void Flash(JudgementResult result) + { + this.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); + } + } + + internal class LegacyOldStyleFill : CompositeDrawable + { + public LegacyOldStyleFill(Skin skin) + { + // required for sizing correctly.. + var firstFrame = getTexture(skin, "colour-0"); + + if (firstFrame == null) + { + InternalChild = new Sprite { Texture = getTexture(skin, "colour") }; + Size = InternalChild.Size; + } + else + { + InternalChild = skin.GetAnimation("scorebar-colour", true, true, startAtCurrentTime: false, applyConfigFrameRate: true) ?? Drawable.Empty(); + Size = new Vector2(firstFrame.DisplayWidth, firstFrame.DisplayHeight); + } + + Position = new Vector2(3, 10) * 1.6f; + Masking = true; + } + } + + internal class LegacyNewStyleFill : Sprite + { + public LegacyNewStyleFill(Skin skin) + { + Texture = getTexture(skin, "colour"); + Position = new Vector2(7.5f, 7.8f) * 1.6f; + } + } } } From 9837286aea6ed1ce625197737d587749e29977f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 16:18:57 +0900 Subject: [PATCH 3979/6909] Add test resources --- osu.Game.Tests/Resources/old-skin/score-0.png | Bin 0 -> 3092 bytes osu.Game.Tests/Resources/old-skin/score-1.png | Bin 0 -> 1237 bytes osu.Game.Tests/Resources/old-skin/score-2.png | Bin 0 -> 3134 bytes osu.Game.Tests/Resources/old-skin/score-3.png | Bin 0 -> 3712 bytes osu.Game.Tests/Resources/old-skin/score-4.png | Bin 0 -> 2395 bytes osu.Game.Tests/Resources/old-skin/score-5.png | Bin 0 -> 3067 bytes osu.Game.Tests/Resources/old-skin/score-6.png | Bin 0 -> 3337 bytes osu.Game.Tests/Resources/old-skin/score-7.png | Bin 0 -> 1910 bytes osu.Game.Tests/Resources/old-skin/score-8.png | Bin 0 -> 3652 bytes osu.Game.Tests/Resources/old-skin/score-9.png | Bin 0 -> 3561 bytes .../Resources/old-skin/score-comma.png | Bin 0 -> 865 bytes osu.Game.Tests/Resources/old-skin/score-dot.png | Bin 0 -> 771 bytes .../Resources/old-skin/score-percent.png | Bin 0 -> 4904 bytes osu.Game.Tests/Resources/old-skin/score-x.png | Bin 0 -> 2536 bytes .../Resources/old-skin/scorebar-bg.png | Bin 0 -> 7087 bytes .../Resources/old-skin/scorebar-colour-0.png | Bin 0 -> 465 bytes .../Resources/old-skin/scorebar-colour-1.png | Bin 0 -> 475 bytes .../Resources/old-skin/scorebar-colour-2.png | Bin 0 -> 466 bytes .../Resources/old-skin/scorebar-colour-3.png | Bin 0 -> 464 bytes .../Resources/old-skin/scorebar-ki.png | Bin 0 -> 8579 bytes .../Resources/old-skin/scorebar-kidanger.png | Bin 0 -> 7361 bytes .../Resources/old-skin/scorebar-kidanger2.png | Bin 0 -> 9360 bytes osu.Game.Tests/Resources/old-skin/skin.ini | 2 ++ 23 files changed, 2 insertions(+) create mode 100644 osu.Game.Tests/Resources/old-skin/score-0.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-1.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-2.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-3.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-4.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-5.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-6.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-7.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-8.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-9.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-comma.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-dot.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-percent.png create mode 100644 osu.Game.Tests/Resources/old-skin/score-x.png create mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-bg.png create mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-colour-0.png create mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png create mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png create mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png create mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-ki.png create mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png create mode 100644 osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png create mode 100644 osu.Game.Tests/Resources/old-skin/skin.ini diff --git a/osu.Game.Tests/Resources/old-skin/score-0.png b/osu.Game.Tests/Resources/old-skin/score-0.png new file mode 100644 index 0000000000000000000000000000000000000000..8304617d8c94a8400b50a90f364941bb02983065 GIT binary patch literal 3092 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 Y0OO;-v8NaCZ2$lO07*qoM6N<$f&jDokN^Mx literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/score-3.png b/osu.Game.Tests/Resources/old-skin/score-3.png new file mode 100644 index 0000000000000000000000000000000000000000..82bec3babebd59263dc486e5900a3a01ea4aaed7 GIT binary patch literal 3712 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 JPDHLkV1kmK?iT<6 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/score-6.png b/osu.Game.Tests/Resources/old-skin/score-6.png new file mode 100644 index 0000000000000000000000000000000000000000..b4cf81f26e5cab5a068ce282ee22b15b92d0df12 GIT binary patch literal 3337 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 TZ*tsI00000NkvXXu0mjfuoP_o literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/score-7.png b/osu.Game.Tests/Resources/old-skin/score-7.png new file mode 100644 index 0000000000000000000000000000000000000000..a23f5379b223d61079e055162fdd93f107f0ec02 GIT binary patch literal 1910 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/score-comma.png b/osu.Game.Tests/Resources/old-skin/score-comma.png new file mode 100644 index 0000000000000000000000000000000000000000..f68d32957ff8dc2e6e26e2b739eab85385548faf GIT binary patch literal 865 zcmV-n1D^beP) z8ION{U-N9C`MxinYv|$`M+}Q$F)W6~uoxD@Vpt6OzhDOaIXgR>o2*u=8V;)D@TF?C z+T17S{{H?ym;%w+TADXmhT?$-O-NWvN0;UR_GXqT)+8VcT7_OfX+F1jnFIZS?x8!# zh8}URz_+nygyLs1rfHeIXpZ(*xcNFpGu`( zwcG8nVHo$9mzUrA{eGSAow*&jOrP%XnP4jnsY0KQkB@6tS64o6^aq1MaO`wCezV#1 zPfkt<_8IyJm7o-H)Y71MrP!@^ySuwPg+f7WY;1^ZHX9uC`MfBVO5*6~=mXZgWiM7_ zg2J#BUQj3&i!V*nT&N{Y(}Zo?M9#738`=bqh+giU0>f>loV9|82+ zaU80#CVuw0IXW_vYCm)NDP(>JPj7d--Su=j9k?q&C<*&31~y-*}fwjTm!p*P&Jvpnyz z)m?^=o3|X6i423GymcH7s?X++lz!!LDcrGp4_y83T(4ux)LkG8*W#nj518@;Wz|pB rcm*5_XP0^3eKYf&7eE$5NN3S}(it^R$P@B}JRwiW6Y_*SAy4SP5!!0C zTE9FJKtC>>NlHB>x7U?fC3eMrUE}-6&6|p1Lzp5^Zgp4;-?Tka0Ir& z1rTBaEJ%Zoz;`eQ3$UbTh&3rgX9|7}x(Kd<2wyLO^E@RI`3er0F^e8jrdymkWwHaJ zU>EG6-At#`*8_n7WhgqG&Jb^YGairM;r=~O+gUez%_x%?%@8~chr`cux!mVouO}vx ziI~l1Vmh6Q;czJ0?Y5Q8W=ZG?xDRe~_LPX7{ta*sJS`LoBN8x8Qyg!)u8YB7AXw}% zxJ%jddP>*89q=fXO1TEF6vaC~s48xE{ zqtTROY?u4ELr$I4>-A5|~39QXWMIpY}VCkbsmq$cd@;G3M5vml{BBvrEa%t<@5R1rBdky#=iw0 zIjg12+aCmBcDQpy_7jQ3UL+DBYmj&PwOZ{J`d!cgAHWx$%}VBs{OkvExyH8F_z-XN zhAjBd4tYuwo)#^QA>$xAWkltmAR9EuCtu}5>6DRg%ppIcnq=Aa2%R;=oZ@xF169W7 zDr`CzH^QVBIE8JK`;M1drwJD)wp9Li|3LU5zyS0^RhH-3lmq|(002ovPDHLkV1mea BUy%R+ literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/score-percent.png b/osu.Game.Tests/Resources/old-skin/score-percent.png new file mode 100644 index 0000000000000000000000000000000000000000..fc750abc7e80287192efb65633e58b6b19e47125 GIT binary patch literal 4904 zcmV+@6W8pCP)>HPEA?bv`x0yR5sa^IhC4IP-&%2W!qCT)6~GN6jP8z zltl$(hYNRk=iGM=@9+a<{p0;+&Rp*D`+dJ>|D1Ea=v3#IPV=h$PLh+zA^z^}r3W$! zGSIW_KsG`4u0GIsjfJ#}={1b#=re=(4i|0#YNprjY=)m@0_q3k3FJZV@v;Kd0W|n>QZAbRG^nNiO?^UR0E#l_jgTq9~z?!8NzghdU+poJL2g+hsf zr%s(3>2NrVMMXvR`T6-ZbeqY?$H!~YqD2vtCQb6+wr$(ljEsy-_`CopALtxOAAjEj zYuGe?=SEiIO=||zdXeGb;n7b$^;GQbx8L61U@)iw0|q>G>#eu0fbXu5AobnyNm!iC z4z)E9C>-cIe}DhP6)RT!WB&a4{vIA4N~hB)tJSLZ?Ahb^^2;xA!^W3iepw$D7N$%l zlbSejV#3_Hb7Ph-U;alyK|vN-05e)gekv`XA88NQ97OAmNJ~pgUb=LtH!!Mj-IkUX zjSNns|XW)x3H0^tau1n;SBX zn?MHf>esKInlWRBY5Vr=6Ysg_p1_kQPX@rpK|sYoRTMCIU1YF7xdbv8mYSNH^vENR z1SBLRC|uKMG%Balsp{(L)P@ZkPK)5?=uYJ3;9;i4!hqsIgHtwc+&E|Y^yx<2I5HzI zWE2-FuvbA>_Bc1})?TD*91RaRCO`cV;31^JRgM9}_V?m^S0O^aH#Y*}zzT%68g zvABfB&uwmQR^WS%l$4au;q4xv<76FG@VTw4nq($@$l0SFeDJ|3@X&S)+U<7L(9obR zT)3dX9M#ydW6_M1o10sk-?_WHtEi|bwPVK)pC5nxal+1>J4ff`miEGX(4U%`nq05AY1g8_2nq&;fsEnr zQ2Y3bpW%0`#RCTps3}vXd=GDT0Uf4*hJnW_qjhJRG*S3QMnje?S(4!E>)VDwyc!!D zReE|lGMG=%(|Yvi(Sbkw@IxZ_uK~=i)i+zSJD4+W8cANrELsIJhMFoVDN)~j_nrFn z*I(7<&70NW!Gm4DBQ0Vo$^*JUDGBp%OII0GBFb^v7<|huw|JS&<~BdV4OLZDsWofX zoQJoVoN_3L1ikalJI!a#oSE?ItFNli(9kx*vDs`cM(}=S3_n8+6&4oS_U+r(yldAk z=P$qff>B+qUcFkOfKk)7lu8+O)|5 zzIY7Y{t0x3g2*6X-n4b=R=?S^XGaeiGNcWYcpoHjmlc3kTPiCnFJFQSHeYt-%9SQs z8pH|t~*}3o*C3~41znaK}Kr%Bk?P!W^q8gaph!G?3&%c4t3bKg) zB0(YZEr4n)vu4e5`7Q>ZnwlEa;sv3_ik@mw1|Aps04W-@riwlVzGD+uw3;xglR`O~ zT@z^B4rI^<9(bgc@+4Y7E_lKK39#2FM#W>fwhU z_C*NCxcKLve{T8x_ur5Al@^tzbaF6UlNv!k0}NWu0R-qnZ@lrwfvgb;1OTcvLDDaw&AO2 zbN~JK>m)wK`w_8h*|H^%Hrz~t8Vs=QI!OG%oH9_0APDrulN3><7Hz5zE@l!V*v0(( z{8PJk?`}fL@`V}#O${Q(VzAx>N+gPZ;DHB*vDry54^h&(fB*iY6g-^NoC>?^uDcRP zjT)unNQ^>-#9v2REYxVx(N|11(?ikG^Im-M#RE_)I@vd|ℜ1r5i{-0$U=)+!&zI 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-bg.png b/osu.Game.Tests/Resources/old-skin/scorebar-bg.png new file mode 100644 index 0000000000000000000000000000000000000000..1e94f464ca497c1eb0645586af13f489c28bf6f3 GIT binary patch literal 7087 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 Hu0mjf)F0A( literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-1.png new file mode 100644 index 0000000000000000000000000000000000000000..7669474d8bcce7afd6a0bc8c247b2c0ccef46c22 GIT binary patch literal 475 zcmV<10VMv3P)p000dL0ssI2y8t*G0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzeMv+?RCwC#+uKfqKoo`HwP^v37s}Mv z^v!%U1!Ihfi4kX}CPfX0X%(^kzi_c2n-*@mIwiGMK`l9bAd|@>+2kxH3;8PBW#@C; z{Cu^Jc@@viL-4zIex{JtVGGC4)cTyOZsjiG@m%+Q=2AMB3N?PF(pxDjRlkO;GHmsW zDi?wks-NXH;lwsq^CWj2U&p`iW6Y~g&f;}G#QE*L)Y`aRc23sGTTLhT%?uwWlgXpm zbdw3dY%FoWz8$vl;lR0=*8l*=7DjA@#JG(ZmZjD|Aj#jH%Dj8Vb z0@~6~q?G|y$p8**XzN)n_m!^o7qL;+0F?}&zc{Cs002ovPDHLkV1lE5)RO=J literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-2.png new file mode 100644 index 0000000000000000000000000000000000000000..70fdb4b14637abdf6237c4bf883fac33542d91f1 GIT binary patch literal 466 zcmV;@0WJQCP)p000dL0ssI2y8t*G0000PbVXQnQ*UN; zcVTj606}DLVr3vnZDD6+Qe|Oed2z{QJOBUzbV)=(RCwC#o83->Fc^m45BKBF0C$_a zGcQd`G{%^iSZSw`Wa%hu62#{T2XC9NWt{Nvv0z?bIH@L+$vN5NEM^PGL-C!9SaTCX zJA5o+j>AyD4&h@JYqruV9Br;{rFG10%`RlWm220Q(nhwE^D0YOyQH0eS6zqTH)`Hg z?PZ&|_Eq&>KJL@JgsgaK_uR=(F&}?t6ZeTU?k78^yG}mNTPLS#&d-y{}3GHw0Ev%NR~I1fq&n1`AM`PHtbK|Bh8k^vBQXJ~o4HM|P2SiJ60GQj*AXdAu| zD+ACa1Hh$*wnV#}lCGsiY{(h_B?CZOoVk+hwQ{%8GWND>UJwAFWPn9@I%QDH(4Zwu zpDAwy41kgW;6n9f343z8qHO?_4DbgPo7n{Gh`%H^(FVYC}$xlkqqJN(O*oGR(I!04(ILP<;w80AoP?p8+Di_5c6?07*qo IM6N<$f~e2NAOHXW literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png b/osu.Game.Tests/Resources/old-skin/scorebar-colour-3.png new file mode 100644 index 0000000000000000000000000000000000000000..18ac6976c95a162914d6cdd371abf71aca48c72a GIT binary patch literal 464 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 JPDHLkV1iG9rlSA= literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png b/osu.Game.Tests/Resources/old-skin/scorebar-kidanger.png new file mode 100644 index 0000000000000000000000000000000000000000..ac5a2c5893b635520d3f8e3804ff87101664d591 GIT binary patch literal 7361 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 literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png b/osu.Game.Tests/Resources/old-skin/scorebar-kidanger2.png new file mode 100644 index 0000000000000000000000000000000000000000..507be0463f0732a07aa7df87cd57c1f273c7d55b GIT binary patch literal 9360 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 KMNUMnLSTX Date: Fri, 16 Oct 2020 16:29:10 +0900 Subject: [PATCH 3980/6909] Add support for old marker style danger textures --- osu.Game/Skinning/LegacyHealthDisplay.cs | 27 +++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 7d9a1dfc15..0da2de4f09 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -84,17 +84,42 @@ namespace osu.Game.Skinning public class LegacyOldStyleMarker : LegacyMarker { + private readonly Sprite sprite; + + private readonly Texture normalTexture; + private readonly Texture dangerTexture; + private readonly Texture superDangerTexture; + public LegacyOldStyleMarker(Skin skin) { + normalTexture = getTexture(skin, "ki"); + dangerTexture = getTexture(skin, "kidanger"); + superDangerTexture = getTexture(skin, "kidanger2"); + InternalChildren = new Drawable[] { - new Sprite + sprite = new Sprite { Texture = getTexture(skin, "ki"), Origin = Anchor.Centre, } }; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(hp => + { + if (hp.NewValue < 0.2f) + sprite.Texture = superDangerTexture; + else if (hp.NewValue < 0.5f) + sprite.Texture = dangerTexture; + else + sprite.Texture = normalTexture; + }); + } } public class LegacyNewStyleMarker : LegacyMarker From 8104bd0f74b0874d6cc5f38f6aec0479aee4587b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 16:45:28 +0900 Subject: [PATCH 3981/6909] Add fill colour changes --- osu.Game/Skinning/LegacyHealthDisplay.cs | 76 +++++++++++++++++++----- 1 file changed, 60 insertions(+), 16 deletions(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 0da2de4f09..f44dd2b864 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -12,14 +12,15 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Screens.Play.HUD; using osuTK; +using osuTK.Graphics; namespace osu.Game.Skinning { public class LegacyHealthDisplay : CompositeDrawable, IHealthDisplay { private readonly Skin skin; - private Drawable fill; - private LegacyMarker marker; + private LegacyHealthPiece fill; + private LegacyHealthPiece marker; private float maxFillWidth; @@ -63,7 +64,9 @@ namespace osu.Game.Skinning }); } + fill.Current.BindTo(Current); marker.Current.BindTo(Current); + maxFillWidth = fill.Width; } @@ -82,7 +85,18 @@ namespace osu.Game.Skinning private static Texture getTexture(Skin skin, string name) => skin.GetTexture($"scorebar-{name}"); - public class LegacyOldStyleMarker : LegacyMarker + private static Color4 getFillColour(double hp) + { + if (hp < 0.2) + return Interpolation.ValueAt(0.2 - hp, Color4.Black, Color4.Red, 0, 0.2); + + if (hp < 0.5) + return Interpolation.ValueAt(0.5 - hp, Color4.White, Color4.Black, 0, 0.5); + + return Color4.White; + } + + public class LegacyOldStyleMarker : LegacyHealthPiece { private readonly Sprite sprite; @@ -92,6 +106,8 @@ namespace osu.Game.Skinning public LegacyOldStyleMarker(Skin skin) { + Origin = Anchor.Centre; + normalTexture = getTexture(skin, "ki"); dangerTexture = getTexture(skin, "kidanger"); superDangerTexture = getTexture(skin, "kidanger2"); @@ -120,39 +136,46 @@ namespace osu.Game.Skinning sprite.Texture = normalTexture; }); } + + public override void Flash(JudgementResult result) + { + this.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); + } } - public class LegacyNewStyleMarker : LegacyMarker + public class LegacyNewStyleMarker : LegacyHealthPiece { + private readonly Sprite sprite; + public LegacyNewStyleMarker(Skin skin) { + Origin = Anchor.Centre; + InternalChildren = new Drawable[] { - new Sprite + sprite = new Sprite { Texture = getTexture(skin, "marker"), Origin = Anchor.Centre, } }; } - } - public class LegacyMarker : CompositeDrawable, IHealthDisplay - { - public Bindable Current { get; } = new Bindable(); - - public LegacyMarker() + protected override void Update() { - Origin = Anchor.Centre; + base.Update(); + + sprite.Colour = getFillColour(Current.Value); + sprite.Blending = Current.Value < 0.5f ? BlendingParameters.Inherit : BlendingParameters.Additive; } - public void Flash(JudgementResult result) + public override void Flash(JudgementResult result) { this.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); } } - internal class LegacyOldStyleFill : CompositeDrawable + internal class LegacyOldStyleFill : LegacyHealthPiece { public LegacyOldStyleFill(Skin skin) { @@ -175,12 +198,33 @@ namespace osu.Game.Skinning } } - internal class LegacyNewStyleFill : Sprite + internal class LegacyNewStyleFill : LegacyHealthPiece { public LegacyNewStyleFill(Skin skin) { - Texture = getTexture(skin, "colour"); + InternalChild = new Sprite + { + Texture = getTexture(skin, "colour"), + }; + + Size = InternalChild.Size; Position = new Vector2(7.5f, 7.8f) * 1.6f; + Masking = true; + } + + protected override void Update() + { + base.Update(); + this.Colour = getFillColour(Current.Value); + } + } + + public class LegacyHealthPiece : CompositeDrawable, IHealthDisplay + { + public Bindable Current { get; } = new Bindable(); + + public virtual void Flash(JudgementResult result) + { } } } From 9572260e6da84c1a4b762a5a16376e4b4fcaafa0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 17:09:00 +0900 Subject: [PATCH 3982/6909] Add bulge and explode support --- osu.Game/Skinning/LegacyHealthDisplay.cs | 114 ++++++++++++++--------- 1 file changed, 72 insertions(+), 42 deletions(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index f44dd2b864..fece590f03 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -96,32 +96,25 @@ namespace osu.Game.Skinning return Color4.White; } - public class LegacyOldStyleMarker : LegacyHealthPiece + public class LegacyOldStyleMarker : LegacyMarker { - private readonly Sprite sprite; - private readonly Texture normalTexture; private readonly Texture dangerTexture; private readonly Texture superDangerTexture; public LegacyOldStyleMarker(Skin skin) { - Origin = Anchor.Centre; - normalTexture = getTexture(skin, "ki"); dangerTexture = getTexture(skin, "kidanger"); superDangerTexture = getTexture(skin, "kidanger2"); - - InternalChildren = new Drawable[] - { - sprite = new Sprite - { - Texture = getTexture(skin, "ki"), - Origin = Anchor.Centre, - } - }; } + public override Sprite CreateSprite() => new Sprite + { + Texture = normalTexture, + Origin = Anchor.Centre, + }; + protected override void LoadComplete() { base.LoadComplete(); @@ -129,49 +122,36 @@ namespace osu.Game.Skinning Current.BindValueChanged(hp => { if (hp.NewValue < 0.2f) - sprite.Texture = superDangerTexture; + Main.Texture = superDangerTexture; else if (hp.NewValue < 0.5f) - sprite.Texture = dangerTexture; + Main.Texture = dangerTexture; else - sprite.Texture = normalTexture; + Main.Texture = normalTexture; }); } - - public override void Flash(JudgementResult result) - { - this.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); - } } - public class LegacyNewStyleMarker : LegacyHealthPiece + public class LegacyNewStyleMarker : LegacyMarker { - private readonly Sprite sprite; + private readonly Skin skin; public LegacyNewStyleMarker(Skin skin) { - Origin = Anchor.Centre; - - InternalChildren = new Drawable[] - { - sprite = new Sprite - { - Texture = getTexture(skin, "marker"), - Origin = Anchor.Centre, - } - }; + this.skin = skin; } + public override Sprite CreateSprite() => new Sprite + { + Texture = getTexture(skin, "marker"), + Origin = Anchor.Centre, + }; + protected override void Update() { base.Update(); - sprite.Colour = getFillColour(Current.Value); - sprite.Blending = Current.Value < 0.5f ? BlendingParameters.Inherit : BlendingParameters.Additive; - } - - public override void Flash(JudgementResult result) - { - this.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); + Main.Colour = getFillColour(Current.Value); + Main.Blending = Current.Value < 0.5f ? BlendingParameters.Inherit : BlendingParameters.Additive; } } @@ -215,10 +195,60 @@ namespace osu.Game.Skinning protected override void Update() { base.Update(); - this.Colour = getFillColour(Current.Value); + Colour = getFillColour(Current.Value); } } + public abstract class LegacyMarker : LegacyHealthPiece + { + protected Sprite Main; + + private Sprite explode; + + protected LegacyMarker() + { + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + Main = CreateSprite(), + explode = CreateSprite().With(s => + { + s.Alpha = 0; + s.Blending = BlendingParameters.Additive; + }), + }; + } + + public abstract Sprite CreateSprite(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(val => + { + if (val.NewValue > val.OldValue) + bulgeMain(); + }); + } + + public override void Flash(JudgementResult result) + { + bulgeMain(); + + explode.FadeOutFromOne(120); + explode.ScaleTo(1).Then().ScaleTo(Current.Value > 0.5f ? 2 : 1.6f, 120); + } + + private void bulgeMain() => + Main.ScaleTo(1.4f).Then().ScaleTo(1, 200, Easing.Out); + } + public class LegacyHealthPiece : CompositeDrawable, IHealthDisplay { public Bindable Current { get; } = new Bindable(); From 77bf050a80733bba4b3a5a6434dc66dad933555e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 17:24:43 +0900 Subject: [PATCH 3983/6909] Ignore IgnoreHits for flashiness --- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index ac74dc22d3..c3de249bf8 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -319,7 +319,7 @@ namespace osu.Game.Screens.Play { processor.NewJudgement += judgement => { - if (judgement.IsHit) + if (judgement.IsHit && judgement.Type != HitResult.IgnoreHit) shd.Flash(judgement); }; } From a1892aa0a7472605ea389bc48db9a465d484f4ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 17:24:56 +0900 Subject: [PATCH 3984/6909] Only additive flash explosions over the epic cutoff --- osu.Game/Skinning/LegacyHealthDisplay.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index fece590f03..489e23ab7a 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -18,6 +18,8 @@ namespace osu.Game.Skinning { public class LegacyHealthDisplay : CompositeDrawable, IHealthDisplay { + private const double epic_cutoff = 0.5; + private readonly Skin skin; private LegacyHealthPiece fill; private LegacyHealthPiece marker; @@ -90,7 +92,7 @@ namespace osu.Game.Skinning if (hp < 0.2) return Interpolation.ValueAt(0.2 - hp, Color4.Black, Color4.Red, 0, 0.2); - if (hp < 0.5) + if (hp < epic_cutoff) return Interpolation.ValueAt(0.5 - hp, Color4.White, Color4.Black, 0, 0.5); return Color4.White; @@ -123,7 +125,7 @@ namespace osu.Game.Skinning { if (hp.NewValue < 0.2f) Main.Texture = superDangerTexture; - else if (hp.NewValue < 0.5f) + else if (hp.NewValue < epic_cutoff) Main.Texture = dangerTexture; else Main.Texture = normalTexture; @@ -151,7 +153,7 @@ namespace osu.Game.Skinning base.Update(); Main.Colour = getFillColour(Current.Value); - Main.Blending = Current.Value < 0.5f ? BlendingParameters.Inherit : BlendingParameters.Additive; + Main.Blending = Current.Value < epic_cutoff ? BlendingParameters.Inherit : BlendingParameters.Additive; } } @@ -241,8 +243,11 @@ namespace osu.Game.Skinning { bulgeMain(); + bool isEpic = Current.Value >= epic_cutoff; + + explode.Blending = isEpic ? BlendingParameters.Additive : BlendingParameters.Inherit; + explode.ScaleTo(1).Then().ScaleTo(isEpic ? 2 : 1.6f, 120); explode.FadeOutFromOne(120); - explode.ScaleTo(1).Then().ScaleTo(Current.Value > 0.5f ? 2 : 1.6f, 120); } private void bulgeMain() => From de60374c88a5521cfeeb6d5d0d942b0cd1a719c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 17:26:14 +0900 Subject: [PATCH 3985/6909] Remove unused using --- .../Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index 181fc8ce98..e1b0820662 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Screens.Play; -using osuTK; namespace osu.Game.Tests.Visual.Gameplay { From 05f1017c282317d848265d15564ed8e48c7582f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 17:35:21 +0900 Subject: [PATCH 3986/6909] Fix lookup check not being updated to use prefix --- osu.Game/Skinning/LegacySkin.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index f5265f2d6e..06539d0f63 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -325,9 +325,9 @@ namespace osu.Game.Skinning return null; } - private const string score_font = "score"; + private string scorePrefix => GetConfig(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score"; - private bool hasScoreFont => this.HasFont(score_font); + private bool hasScoreFont => this.HasFont(scorePrefix); public override Drawable GetDrawableComponent(ISkinComponent component) { @@ -351,7 +351,6 @@ namespace osu.Game.Skinning case HUDSkinComponents.ScoreText: case HUDSkinComponents.AccuracyText: - string scorePrefix = GetConfig(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score"; int scoreOverlap = GetConfig(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2; return new LegacySpriteText(this, scorePrefix) { From e9c4b67cf4688154c1b044b20335a772103e996f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 17:35:35 +0900 Subject: [PATCH 3987/6909] Inline variable --- osu.Game/Skinning/LegacySkin.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 06539d0f63..22ddd45851 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -351,10 +351,9 @@ namespace osu.Game.Skinning case HUDSkinComponents.ScoreText: case HUDSkinComponents.AccuracyText: - int scoreOverlap = GetConfig(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2; return new LegacySpriteText(this, scorePrefix) { - Spacing = new Vector2(-scoreOverlap, 0) + Spacing = new Vector2(-(GetConfig(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2), 0) }; } From 3ce6d1fea103cf3c3d96df2f77684ffa6964cd4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 17:36:15 +0900 Subject: [PATCH 3988/6909] Remove unnecessary AccuracyText enum All elements use "score" regardless. --- osu.Game/Skinning/HUDSkinComponents.cs | 1 - osu.Game/Skinning/LegacyAccuracyCounter.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 1 - 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs index 6ec575e106..cb35425981 100644 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ b/osu.Game/Skinning/HUDSkinComponents.cs @@ -9,6 +9,5 @@ namespace osu.Game.Skinning ScoreCounter, ScoreText, AccuracyCounter, - AccuracyText } } diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 6c194a06d3..27d5aa4dbd 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Skinning [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - protected sealed override OsuSpriteText CreateSpriteText() => skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.AccuracyText)) as OsuSpriteText ?? new OsuSpriteText(); + protected sealed override OsuSpriteText CreateSpriteText() => skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) as OsuSpriteText ?? new OsuSpriteText(); protected override void Update() { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 22ddd45851..cd9809a22b 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -350,7 +350,6 @@ namespace osu.Game.Skinning return new LegacyAccuracyCounter(this); case HUDSkinComponents.ScoreText: - case HUDSkinComponents.AccuracyText: return new LegacySpriteText(this, scorePrefix) { Spacing = new Vector2(-(GetConfig(LegacySkinConfiguration.LegacySetting.ScoreOverlap)?.Value ?? -2), 0) From 24b0a1b84b75b4ea10b92e54aaa6066b08e4452a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 17:38:21 +0900 Subject: [PATCH 3989/6909] Switch to direct casts (we can be sure LegacySpriteText is present at this point) --- osu.Game/Skinning/LegacyAccuracyCounter.cs | 2 +- osu.Game/Skinning/LegacyScoreCounter.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 27d5aa4dbd..a4a432ece2 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -29,7 +29,7 @@ namespace osu.Game.Skinning [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - protected sealed override OsuSpriteText CreateSpriteText() => skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) as OsuSpriteText ?? new OsuSpriteText(); + protected sealed override OsuSpriteText CreateSpriteText() => (OsuSpriteText)skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)); protected override void Update() { diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index 41bf35722b..39c90211f2 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -31,6 +31,6 @@ namespace osu.Game.Skinning Margin = new MarginPadding(10); } - protected sealed override OsuSpriteText CreateSpriteText() => skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) as OsuSpriteText ?? new OsuSpriteText(); + protected sealed override OsuSpriteText CreateSpriteText() => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)); } } From a774de2270c722de1d00c4cdef37a7d9f38c2aeb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 17:40:15 +0900 Subject: [PATCH 3990/6909] Also add support in LegacyComboCounter --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 3 ++- osu.Game/Skinning/HUDSkinComponents.cs | 1 + osu.Game/Skinning/LegacySkin.cs | 8 ++++++++ 3 files changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index cc9398bc35..4784bca7dd 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.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.Game.Graphics.Sprites; using osu.Game.Skinning; using osuTK; @@ -246,6 +247,6 @@ namespace osu.Game.Screens.Play.HUD return difference * rolling_duration; } - private Drawable createSpriteText() => new LegacySpriteText(skin); + private OsuSpriteText createSpriteText() => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ComboText)); } } diff --git a/osu.Game/Skinning/HUDSkinComponents.cs b/osu.Game/Skinning/HUDSkinComponents.cs index cb35425981..c5dead7858 100644 --- a/osu.Game/Skinning/HUDSkinComponents.cs +++ b/osu.Game/Skinning/HUDSkinComponents.cs @@ -8,6 +8,7 @@ namespace osu.Game.Skinning ComboCounter, ScoreCounter, ScoreText, + ComboText, AccuracyCounter, } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index cd9809a22b..db7307b3fe 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -327,6 +327,8 @@ namespace osu.Game.Skinning private string scorePrefix => GetConfig(LegacySkinConfiguration.LegacySetting.ScorePrefix)?.Value ?? "score"; + private string comboPrefix => GetConfig(LegacySkinConfiguration.LegacySetting.ComboPrefix)?.Value ?? "score"; + private bool hasScoreFont => this.HasFont(scorePrefix); public override Drawable GetDrawableComponent(ISkinComponent component) @@ -349,6 +351,12 @@ namespace osu.Game.Skinning case HUDSkinComponents.AccuracyCounter: return new LegacyAccuracyCounter(this); + case HUDSkinComponents.ComboText: + return new LegacySpriteText(this, comboPrefix) + { + Spacing = new Vector2(-(GetConfig(LegacySkinConfiguration.LegacySetting.ComboOverlap)?.Value ?? -2), 0) + }; + case HUDSkinComponents.ScoreText: return new LegacySpriteText(this, scorePrefix) { From 8a3bce3cc3efe0fbfd07bda9ad9fb3ec6c6b528c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 18:19:09 +0900 Subject: [PATCH 3991/6909] Fix osu!catch showing two combo counters for legacy skins --- .../Skinning/CatchLegacySkinTransformer.cs | 19 ++++++++++++++++--- osu.Game/Screens/Play/Player.cs | 6 +++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 916b4c5192..22db147e32 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Catch.Skinning { public class CatchLegacySkinTransformer : LegacySkinTransformer { + /// + /// For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. + /// + private bool providesComboCounter => this.HasFont(GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"); + public CatchLegacySkinTransformer(ISkinSource source) : base(source) { @@ -20,6 +25,16 @@ namespace osu.Game.Rulesets.Catch.Skinning public override Drawable GetDrawableComponent(ISkinComponent component) { + if (component is HUDSkinComponent hudComponent) + { + switch (hudComponent.Component) + { + case HUDSkinComponents.ComboCounter: + // catch may provide its own combo counter; hide the default. + return providesComboCounter ? Drawable.Empty() : null; + } + } + if (!(component is CatchSkinComponent catchSkinComponent)) return null; @@ -55,10 +70,8 @@ namespace osu.Game.Rulesets.Catch.Skinning this.GetAnimation("fruit-ryuuta", true, true, true); case CatchSkinComponents.CatchComboCounter: - var comboFont = GetConfig(LegacySetting.ComboPrefix)?.Value ?? "score"; - // For simplicity, let's use legacy combo font texture existence as a way to identify legacy skins from default. - if (this.HasFont(comboFont)) + if (providesComboCounter) return new LegacyCatchComboCounter(Source); break; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 56b212291a..df0a52a0e8 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -221,8 +221,12 @@ namespace osu.Game.Screens.Play createGameplayComponents(Beatmap.Value, playableBeatmap) }); + // also give the HUD a ruleset container to allow rulesets to potentially override HUD elements (used to disable combo counters etc.) + // we may want to limit this in the future to disallow rulesets from outright replacing elements the user expects to be there. + var hudRulesetContainer = new SkinProvidingContainer(ruleset.CreateLegacySkinProvider(beatmapSkinProvider, playableBeatmap)); + // add the overlay components as a separate step as they proxy some elements from the above underlay/gameplay components. - GameplayClockContainer.Add(createOverlayComponents(Beatmap.Value)); + GameplayClockContainer.Add(hudRulesetContainer.WithChild(createOverlayComponents(Beatmap.Value))); if (!DrawableRuleset.AllowGameplayOverlays) { From 0437f7e7e982fef41611ab428c949744432a4a11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 18:22:18 +0900 Subject: [PATCH 3992/6909] Delete outdated test scene Has been replaced by the four new skinnable tests for each component. --- .../Visual/Gameplay/TestSceneScoreCounter.cs | 68 ------------------- 1 file changed, 68 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.cs deleted file mode 100644 index 34c657bf7f..0000000000 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneScoreCounter.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 NUnit.Framework; -using osu.Framework.Graphics; -using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Play.HUD; -using osuTK; - -namespace osu.Game.Tests.Visual.Gameplay -{ - [TestFixture] - public class TestSceneScoreCounter : OsuTestScene - { - public TestSceneScoreCounter() - { - int numerator = 0, denominator = 0; - - ScoreCounter score = new DefaultScoreCounter - { - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Margin = new MarginPadding(20), - }; - Add(score); - - LegacyComboCounter comboCounter = new LegacyComboCounter - { - Origin = Anchor.BottomLeft, - Anchor = Anchor.BottomLeft, - Margin = new MarginPadding(10), - }; - Add(comboCounter); - - PercentageCounter accuracyCounter = new PercentageCounter - { - Origin = Anchor.TopRight, - Anchor = Anchor.TopRight, - Position = new Vector2(-20, 60), - }; - Add(accuracyCounter); - - AddStep(@"Reset all", delegate - { - score.Current.Value = 0; - comboCounter.Current.Value = 0; - numerator = denominator = 0; - accuracyCounter.SetFraction(0, 0); - }); - - AddStep(@"Hit! :D", delegate - { - score.Current.Value += 300 + (ulong)(300.0 * (comboCounter.Current.Value > 0 ? comboCounter.Current.Value - 1 : 0) / 25.0); - comboCounter.Current.Value++; - numerator++; - denominator++; - accuracyCounter.SetFraction(numerator, denominator); - }); - - AddStep(@"miss...", delegate - { - comboCounter.Current.Value = 0; - denominator++; - accuracyCounter.SetFraction(numerator, denominator); - }); - } - } -} From cc1128314354b6393d5622f27dd032ac58e56968 Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Fri, 16 Oct 2020 11:27:02 +0200 Subject: [PATCH 3993/6909] Use string.Starts-/EndsWith char overloads --- osu.Game/Database/ArchiveModelManager.cs | 2 +- osu.Game/OsuGame.cs | 4 ++-- osu.Game/Screens/Select/FilterQueryParser.cs | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index b947056ebd..8bdc804311 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -593,7 +593,7 @@ namespace osu.Game.Database var fileInfos = new List(); string prefix = reader.Filenames.GetCommonPrefix(); - if (!(prefix.EndsWith("/", StringComparison.Ordinal) || prefix.EndsWith("\\", StringComparison.Ordinal))) + if (!(prefix.EndsWith('/') || prefix.EndsWith('\\'))) prefix = string.Empty; // import files to manager diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 56cced9c04..a0ddab702e 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -181,7 +181,7 @@ namespace osu.Game if (args?.Length > 0) { - var paths = args.Where(a => !a.StartsWith(@"-", StringComparison.Ordinal)).ToArray(); + var paths = args.Where(a => !a.StartsWith('-')).ToArray(); if (paths.Length > 0) Task.Run(() => Import(paths)); } @@ -289,7 +289,7 @@ namespace osu.Game public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ => { - if (url.StartsWith("/", StringComparison.Ordinal)) + if (url.StartsWith('/')) url = $"{API.Endpoint}{url}"; externalLinkOpener.OpenUrlExternally(url); diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index fa2beb2652..4b6b3be45c 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -80,9 +80,9 @@ namespace osu.Game.Screens.Select private static int getLengthScale(string value) => value.EndsWith("ms", StringComparison.Ordinal) ? 1 : - value.EndsWith("s", StringComparison.Ordinal) ? 1000 : - value.EndsWith("m", StringComparison.Ordinal) ? 60000 : - value.EndsWith("h", StringComparison.Ordinal) ? 3600000 : 1000; + value.EndsWith('s') ? 1000 : + value.EndsWith('m') ? 60000 : + value.EndsWith('h') ? 3600000 : 1000; private static bool parseFloatWithPoint(string value, out float result) => float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result); From cbaad4eb56bf69a733b15c46f0332ec8b78f82cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 18:34:14 +0900 Subject: [PATCH 3994/6909] Adjust accuracy display to match stable --- osu.Game/Skinning/LegacyAccuracyCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 9354b2b3bc..0d3adeb3ea 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -20,7 +20,7 @@ namespace osu.Game.Skinning Anchor = Anchor.TopRight; Origin = Anchor.TopRight; - Scale = new Vector2(0.75f); + Scale = new Vector2(0.6f); Margin = new MarginPadding(10); this.skin = skin; From 2ba8bc45fd1f68b54df68435eb0d88c3d28fff1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 16 Oct 2020 18:37:24 +0900 Subject: [PATCH 3995/6909] Also add slight adjustment to score display --- osu.Game/Skinning/LegacyScoreCounter.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index f94bef6652..93b50e0ac1 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -5,6 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osuTK; namespace osu.Game.Skinning { @@ -28,6 +29,7 @@ namespace osu.Game.Skinning // base class uses int for display, but externally we bind to ScoreProcesssor as a double for now. Current.BindValueChanged(v => base.Current.Value = (int)v.NewValue); + Scale = new Vector2(0.96f); Margin = new MarginPadding(10); } From fe3a23750c6bbda7427e765aa93007b1ee13a6b7 Mon Sep 17 00:00:00 2001 From: Berkan Diler Date: Fri, 16 Oct 2020 11:52:29 +0200 Subject: [PATCH 3996/6909] Use char overloads for string methods --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Online/Chat/ChannelManager.cs | 2 +- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs | 2 +- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- osu.Game/Skinning/GameplaySkinComponent.cs | 2 +- osu.Game/Skinning/HUDSkinComponent.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index acab525821..8d1f0e59bf 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -98,7 +98,7 @@ namespace osu.Game.Beatmaps [JsonIgnore] public string StoredBookmarks { - get => string.Join(",", Bookmarks); + get => string.Join(',', Bookmarks); set { if (string.IsNullOrEmpty(value)) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index f7ed57f207..16f46581c5 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -196,7 +196,7 @@ namespace osu.Game.Online.Chat if (target == null) return; - var parameters = text.Split(new[] { ' ' }, 2); + var parameters = text.Split(' ', 2); string command = parameters[0]; string content = parameters.Length == 2 ? parameters[1] : string.Empty; diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 648e4a762b..d2a117876d 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -111,7 +111,7 @@ namespace osu.Game.Online.Chat public static LinkDetails GetLinkDetails(string url) { - var args = url.Split(new[] { '/' }, StringSplitOptions.RemoveEmptyEntries); + var args = url.Split('/', StringSplitOptions.RemoveEmptyEntries); args[0] = args[0].TrimEnd(':'); switch (args[0]) diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index 946831d13b..ebee377a51 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -148,7 +148,7 @@ namespace osu.Game.Overlays.Profile.Header if (string.IsNullOrEmpty(content)) return false; // newlines could be contained in API returned user content. - content = content.Replace("\n", " "); + content = content.Replace('\n', ' '); bottomLinkContainer.AddIcon(icon, text => { diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 7dcbc52cea..44b22033dc 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Objects.Legacy { string[] ss = split[5].Split(':'); endTime = Math.Max(startTime, Parsing.ParseDouble(ss[0])); - readCustomSampleBanks(string.Join(":", ss.Skip(1)), bankInfo); + readCustomSampleBanks(string.Join(':', ss.Skip(1)), bankInfo); } result = CreateHold(pos, combo, comboOffset, endTime + Offset - startTime); diff --git a/osu.Game/Skinning/GameplaySkinComponent.cs b/osu.Game/Skinning/GameplaySkinComponent.cs index 2aa380fa90..80f6efc07a 100644 --- a/osu.Game/Skinning/GameplaySkinComponent.cs +++ b/osu.Game/Skinning/GameplaySkinComponent.cs @@ -18,6 +18,6 @@ namespace osu.Game.Skinning protected virtual string ComponentName => Component.ToString(); public string LookupName => - string.Join("/", new[] { "Gameplay", RulesetPrefix, ComponentName }.Where(s => !string.IsNullOrEmpty(s))); + string.Join('/', new[] { "Gameplay", RulesetPrefix, ComponentName }.Where(s => !string.IsNullOrEmpty(s))); } } diff --git a/osu.Game/Skinning/HUDSkinComponent.cs b/osu.Game/Skinning/HUDSkinComponent.cs index 041beb68f2..cc053421b7 100644 --- a/osu.Game/Skinning/HUDSkinComponent.cs +++ b/osu.Game/Skinning/HUDSkinComponent.cs @@ -17,6 +17,6 @@ namespace osu.Game.Skinning protected virtual string ComponentName => Component.ToString(); public string LookupName => - string.Join("/", new[] { "HUD", ComponentName }.Where(s => !string.IsNullOrEmpty(s))); + string.Join('/', new[] { "HUD", ComponentName }.Where(s => !string.IsNullOrEmpty(s))); } } From 2586990301e0da95f5ffd272f942b86859bb595a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Oct 2020 23:19:34 +0900 Subject: [PATCH 3997/6909] 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 3df894fbcc..1d2cf22b28 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 8b10f0a7f7..133855c6c4 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 88abbca73d..73faa8541e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From 81cc5e1c42e787f906fda4c6c881474c74f33aac Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Oct 2020 23:31:01 +0900 Subject: [PATCH 3998/6909] Silence EF warning due to ordinal being unsupported --- osu.Game/Rulesets/RulesetStore.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index c12d418771..c4639375da 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -100,7 +100,8 @@ 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 => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) + // ReSharper disable once StringStartsWithIsCultureSpecific (silences EF warning of ordinal being unsupported) + if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo)) == null) context.RulesetInfo.Add(r.RulesetInfo); } From 6385d5f3692b95bbdcea9819292ce221e2795999 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 16 Oct 2020 23:40:44 +0900 Subject: [PATCH 3999/6909] Replace with local tolist --- osu.Game/Rulesets/RulesetStore.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index c4639375da..d422bca087 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -96,12 +96,13 @@ namespace osu.Game.Rulesets context.SaveChanges(); // add any other modes + var existingRulesets = context.RulesetInfo.ToList(); + foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) { // 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). - // ReSharper disable once StringStartsWithIsCultureSpecific (silences EF warning of ordinal being unsupported) - if (context.RulesetInfo.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo)) == null) + if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) context.RulesetInfo.Add(r.RulesetInfo); } From bba9a0b2fe5be16ae37338cddb307121de41aaf1 Mon Sep 17 00:00:00 2001 From: unknown Date: Sat, 17 Oct 2020 00:25:16 +0800 Subject: [PATCH 4000/6909] set sprite text anchor and origin to top right --- osu.Game/Skinning/LegacyScoreCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index e54c4e8eb4..fc7863fc4e 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -33,6 +33,6 @@ namespace osu.Game.Skinning Margin = new MarginPadding(10); } - protected sealed override OsuSpriteText CreateSpriteText() => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)); + protected sealed override OsuSpriteText CreateSpriteText() => ((OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText))).With(s => s.Anchor = s.Origin = Anchor.TopRight); } } From b60dfc55b6625bea0f803c2ddd9d3c389d9ca7cb Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 16 Oct 2020 19:21:51 +0200 Subject: [PATCH 4001/6909] Apply review suggestions. --- osu.Android/GameplayScreenRotationLocker.cs | 6 +++--- osu.Android/OsuGameActivity.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index d1f4caba52..07cca8c2f1 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -17,14 +17,14 @@ namespace osu.Android private void load(OsuGame game) { localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); - localUserPlaying.BindValueChanged(_ => updateLock()); + localUserPlaying.BindValueChanged(userPlaying => updateLock(userPlaying)); } - private void updateLock() + private void updateLock(ValueChangedEvent userPlaying) { OsuGameActivity.Activity.RunOnUiThread(() => { - OsuGameActivity.Activity.RequestedOrientation = localUserPlaying.Value ? ScreenOrientation.Locked : ScreenOrientation.FullUser; + OsuGameActivity.Activity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser; }); } } diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index c2b28f3de4..d4d2b83502 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -12,7 +12,7 @@ namespace osu.Android [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] public class OsuGameActivity : AndroidGameActivity { - internal static Activity Activity; + internal static Activity Activity { get; private set; } protected override Framework.Game CreateGame() => new OsuGameAndroid(); From e4463254d7feab088262dfb84826f4ce5a04ba43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 17 Oct 2020 15:29:30 +0200 Subject: [PATCH 4002/6909] Add test coverage for score counter alignment --- .../Visual/Gameplay/TestSceneSkinnableScoreCounter.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs index 2d5003d1da..fc63340f20 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; @@ -43,5 +44,11 @@ namespace osu.Game.Tests.Visual.Gameplay s.Current.Value += 300; }); } + + [Test] + public void TestVeryLargeScore() + { + AddStep("set large score", () => scoreCounters.ForEach(counter => counter.Current.Value = 1_00_000_000)); + } } } From 0acc86f75724e6d1e5f348ee7878b7249be6078c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 17 Oct 2020 15:31:35 +0200 Subject: [PATCH 4003/6909] Split line for readability --- osu.Game/Skinning/LegacyScoreCounter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyScoreCounter.cs b/osu.Game/Skinning/LegacyScoreCounter.cs index fc7863fc4e..5bffeff5a8 100644 --- a/osu.Game/Skinning/LegacyScoreCounter.cs +++ b/osu.Game/Skinning/LegacyScoreCounter.cs @@ -33,6 +33,8 @@ namespace osu.Game.Skinning Margin = new MarginPadding(10); } - protected sealed override OsuSpriteText CreateSpriteText() => ((OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText))).With(s => s.Anchor = s.Origin = Anchor.TopRight); + protected sealed override OsuSpriteText CreateSpriteText() + => (OsuSpriteText)skin.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) + .With(s => s.Anchor = s.Origin = Anchor.TopRight); } } From a5b0307cfb472342bb56a08548b8245d7a8604be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 17 Oct 2020 15:36:21 +0200 Subject: [PATCH 4004/6909] Apply same fix to legacy accuracy counter --- osu.Game/Skinning/LegacyAccuracyCounter.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyAccuracyCounter.cs b/osu.Game/Skinning/LegacyAccuracyCounter.cs index 29d7046694..5eda374337 100644 --- a/osu.Game/Skinning/LegacyAccuracyCounter.cs +++ b/osu.Game/Skinning/LegacyAccuracyCounter.cs @@ -29,7 +29,9 @@ namespace osu.Game.Skinning [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } - protected sealed override OsuSpriteText CreateSpriteText() => (OsuSpriteText)skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)); + protected sealed override OsuSpriteText CreateSpriteText() + => (OsuSpriteText)skin?.GetDrawableComponent(new HUDSkinComponent(HUDSkinComponents.ScoreText)) + ?.With(s => s.Anchor = s.Origin = Anchor.TopRight); protected override void Update() { From 8aeeed9402e2de7d6de6e477adc69bf914ed6f0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 17 Oct 2020 15:47:37 +0200 Subject: [PATCH 4005/6909] Fix weird number formatting in test --- .../Visual/Gameplay/TestSceneSkinnableScoreCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs index fc63340f20..e212ceeba7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableScoreCounter.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestVeryLargeScore() { - AddStep("set large score", () => scoreCounters.ForEach(counter => counter.Current.Value = 1_00_000_000)); + AddStep("set large score", () => scoreCounters.ForEach(counter => counter.Current.Value = 1_000_000_000)); } } } From 5b96f0156413e2694853b1a67557189fd4c7fb01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 17 Oct 2020 14:53:29 +0200 Subject: [PATCH 4006/6909] Fix key counter actions displaying out of order --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index f2ac61eaf4..07de2bf601 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -136,7 +136,11 @@ namespace osu.Game.Rulesets.UI KeyBindingContainer.Add(receptor); keyCounter.SetReceptor(receptor); - keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings.Select(b => b.GetAction()).Distinct().Select(b => new KeyCounterAction(b))); + keyCounter.AddRange(KeyBindingContainer.DefaultKeyBindings + .Select(b => b.GetAction()) + .Distinct() + .OrderBy(action => action) + .Select(action => new KeyCounterAction(action))); } public class ActionReceptor : KeyCounterDisplay.Receptor, IKeyBindingHandler From 9cd595800a5b826f8fac7a62c8eabfd7157a32f2 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 18 Oct 2020 19:42:05 +0200 Subject: [PATCH 4007/6909] Subscribe to event handler instead. --- osu.Android/GameplayScreenRotationLocker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index 07cca8c2f1..d25e22a0c2 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -17,7 +17,7 @@ namespace osu.Android private void load(OsuGame game) { localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); - localUserPlaying.BindValueChanged(userPlaying => updateLock(userPlaying)); + localUserPlaying.ValueChanged += updateLock; } private void updateLock(ValueChangedEvent userPlaying) From 371aecfca0856499cd0b014b4afab71f0bcd4b9f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 18 Oct 2020 20:07:42 +0200 Subject: [PATCH 4008/6909] Fetch OsuGameActivity through DI instead. --- osu.Android/GameplayScreenRotationLocker.cs | 7 +++++-- osu.Android/OsuGameActivity.cs | 6 +----- osu.Android/OsuGameAndroid.cs | 10 ++++++++++ 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index d25e22a0c2..fb471ceb78 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -13,6 +13,9 @@ namespace osu.Android { private Bindable localUserPlaying; + [Resolved] + private OsuGameActivity gameActivity { get; set; } + [BackgroundDependencyLoader] private void load(OsuGame game) { @@ -22,9 +25,9 @@ namespace osu.Android private void updateLock(ValueChangedEvent userPlaying) { - OsuGameActivity.Activity.RunOnUiThread(() => + gameActivity.RunOnUiThread(() => { - OsuGameActivity.Activity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser; + gameActivity.RequestedOrientation = userPlaying.NewValue ? ScreenOrientation.Locked : ScreenOrientation.FullUser; }); } } diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index d4d2b83502..7e250dce0e 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -12,14 +12,10 @@ namespace osu.Android [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] public class OsuGameActivity : AndroidGameActivity { - internal static Activity Activity { get; private set; } - - protected override Framework.Game CreateGame() => new OsuGameAndroid(); + protected override Framework.Game CreateGame() => new OsuGameAndroid(this); protected override void OnCreate(Bundle savedInstanceState) { - Activity = this; - // The default current directory on android is '/'. // On some devices '/' maps to the app data directory. On others it maps to the root of the internal storage. // In order to have a consistent current directory on all devices the full path of the app data directory is set as the current directory. diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 887a8395e3..21d6336b2c 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -4,6 +4,7 @@ using System; using Android.App; using Android.OS; +using osu.Framework.Allocation; using osu.Game; using osu.Game.Updater; @@ -11,6 +12,15 @@ namespace osu.Android { public class OsuGameAndroid : OsuGame { + [Cached] + private readonly OsuGameActivity gameActivity; + + public OsuGameAndroid(OsuGameActivity activity) + : base(null) + { + gameActivity = activity; + } + public override Version AssemblyVersion { get From 4590d9b93b874550fde5a921bedff9353d48b69a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 13:04:12 +0900 Subject: [PATCH 4009/6909] Remove outdated comment logic --- osu.Game/Screens/Select/BeatmapCarousel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c011ea7e05..83e20909a1 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -762,7 +762,6 @@ namespace osu.Game.Screens.Select // 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 + DrawableCarouselBeatmapSet.HEIGHT - visibleHalfHeight + BleedTop; foreach (var b in set.Beatmaps) From ee0efa0b4c1a5e19b665e3bf89999714d24408c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 13:05:42 +0900 Subject: [PATCH 4010/6909] Fix off-by-one in display range retrieval 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 83e20909a1..04eff79533 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -590,7 +590,7 @@ namespace osu.Game.Screens.Select { displayedRange = newDisplayRange; - var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first); + var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); foreach (var panel in ScrollableContent.Children) { From bff3856c83ec9459185734fffeeef1aee3a41a29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 13:13:32 +0900 Subject: [PATCH 4011/6909] Account for panel height when removing as off-screen --- 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 04eff79533..06327bac3f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -602,7 +602,7 @@ namespace osu.Game.Screens.Select // panel loaded as drawable but not required by visible range. // remove but only if too far off-screen - if (panel.Y < visibleUpperBound - distance_offscreen_before_unload || panel.Y > visibleBottomBound + distance_offscreen_before_unload) + if (panel.Y + panel.DrawHeight < visibleUpperBound - distance_offscreen_before_unload || panel.Y > visibleBottomBound + distance_offscreen_before_unload) { // may want a fade effect here (could be seen if a huge change happens, like a set with 20 difficulties becomes selected). panel.ClearTransforms(); From cb1784a846901a15673575d25d2dcfc92ce85515 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 14:05:28 +0900 Subject: [PATCH 4012/6909] Fix score displays using non-matching zero padding depending on user score display mode --- .../Graphics/UserInterface/RollingCounter.cs | 12 +++++-- .../Graphics/UserInterface/ScoreCounter.cs | 14 ++++---- osu.Game/Screens/Play/HUD/IScoreCounter.cs | 6 ++++ .../Screens/Play/HUD/SkinnableScoreCounter.cs | 32 +++++++++++++++++++ 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/RollingCounter.cs b/osu.Game/Graphics/UserInterface/RollingCounter.cs index 91a557094d..b96181416d 100644 --- a/osu.Game/Graphics/UserInterface/RollingCounter.cs +++ b/osu.Game/Graphics/UserInterface/RollingCounter.cs @@ -56,8 +56,7 @@ namespace osu.Game.Graphics.UserInterface return; displayedCount = value; - if (displayedCountSpriteText != null) - displayedCountSpriteText.Text = FormatCount(value); + UpdateDisplay(); } } @@ -73,10 +72,17 @@ namespace osu.Game.Graphics.UserInterface private void load() { displayedCountSpriteText = CreateSpriteText(); - displayedCountSpriteText.Text = FormatCount(DisplayedCount); + + UpdateDisplay(); Child = displayedCountSpriteText; } + protected void UpdateDisplay() + { + if (displayedCountSpriteText != null) + displayedCountSpriteText.Text = FormatCount(DisplayedCount); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/Graphics/UserInterface/ScoreCounter.cs b/osu.Game/Graphics/UserInterface/ScoreCounter.cs index 17e5ceedb9..d75e49a4ce 100644 --- a/osu.Game/Graphics/UserInterface/ScoreCounter.cs +++ b/osu.Game/Graphics/UserInterface/ScoreCounter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Screens.Play.HUD; @@ -17,20 +18,19 @@ namespace osu.Game.Graphics.UserInterface /// public bool UseCommaSeparator { get; } - /// - /// How many leading zeroes the counter has. - /// - public uint LeadingZeroes { get; } + public Bindable RequiredDisplayDigits { get; } = new Bindable(); /// /// Displays score. /// /// How many leading zeroes the counter will have. /// Whether comma separators should be displayed. - protected ScoreCounter(uint leading = 0, bool useCommaSeparator = false) + protected ScoreCounter(int leading = 0, bool useCommaSeparator = false) { UseCommaSeparator = useCommaSeparator; - LeadingZeroes = leading; + + RequiredDisplayDigits.Value = leading; + RequiredDisplayDigits.BindValueChanged(_ => UpdateDisplay()); } protected override double GetProportionalDuration(double currentValue, double newValue) @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.UserInterface protected override string FormatCount(double count) { - string format = new string('0', (int)LeadingZeroes); + string format = new string('0', RequiredDisplayDigits.Value); if (UseCommaSeparator) { diff --git a/osu.Game/Screens/Play/HUD/IScoreCounter.cs b/osu.Game/Screens/Play/HUD/IScoreCounter.cs index 2d39a64cfe..7f5e81d5ef 100644 --- a/osu.Game/Screens/Play/HUD/IScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/IScoreCounter.cs @@ -15,5 +15,11 @@ namespace osu.Game.Screens.Play.HUD /// The current score to be displayed. /// Bindable Current { get; } + + /// + /// The number of digits required to display most sane scores. + /// This may be exceeded in very rare cases, but is useful to pad or space the display to avoid it jumping around. + /// + Bindable RequiredDisplayDigits { get; } } } diff --git a/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs b/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs index a442ad0d9a..b46f5684b1 100644 --- a/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableScoreCounter.cs @@ -1,7 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Configuration; +using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; namespace osu.Game.Screens.Play.HUD @@ -10,12 +14,38 @@ namespace osu.Game.Screens.Play.HUD { public Bindable Current { get; } = new Bindable(); + private Bindable scoreDisplayMode; + + public Bindable RequiredDisplayDigits { get; } = new Bindable(); + public SkinnableScoreCounter() : base(new HUDSkinComponent(HUDSkinComponents.ScoreCounter), _ => new DefaultScoreCounter()) { CentreComponent = false; } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + scoreDisplayMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + scoreDisplayMode.BindValueChanged(scoreMode => + { + switch (scoreMode.NewValue) + { + case ScoringMode.Standardised: + RequiredDisplayDigits.Value = 6; + break; + + case ScoringMode.Classic: + RequiredDisplayDigits.Value = 8; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(scoreMode)); + } + }, true); + } + private IScoreCounter skinnedCounter; protected override void SkinChanged(ISkinSource skin, bool allowFallback) @@ -23,7 +53,9 @@ namespace osu.Game.Screens.Play.HUD base.SkinChanged(skin, allowFallback); skinnedCounter = Drawable as IScoreCounter; + skinnedCounter?.Current.BindTo(Current); + skinnedCounter?.RequiredDisplayDigits.BindTo(RequiredDisplayDigits); } } } From e3b47083fc85ebc324575645b8d4c33c5661253f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 14:05:41 +0900 Subject: [PATCH 4013/6909] Add "scoring" as keyword to more easily find score display mode setting --- .../Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 73968761e2..66b3b8c4ca 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -76,7 +76,8 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsEnumDropdown { LabelText = "Score display mode", - Current = config.GetBindable(OsuSetting.ScoreDisplayMode) + Current = config.GetBindable(OsuSetting.ScoreDisplayMode), + Keywords = new[] { "scoring" } } }; From cdb649476b8bf327c9ca561a26ce9ffabd660e32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 14:33:53 +0900 Subject: [PATCH 4014/6909] Allow legacy text to display fixed width correctly --- osu.Game/Skinning/LegacySpriteText.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index 8394657b1c..d7a3975c72 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using osu.Framework.Graphics.Sprites; using osu.Framework.Text; using osu.Game.Graphics.Sprites; +using osuTK; namespace osu.Game.Skinning { @@ -17,11 +18,12 @@ namespace osu.Game.Skinning Shadow = false; UseFullGlyphHeight = false; - Font = new FontUsage(font, 1); + Font = new FontUsage(font, 1, fixedWidth: true); glyphStore = new LegacyGlyphStore(skin); } - protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); + protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => + new TextBuilder(glyphStore, Font, MaxWidth, UseFullGlyphHeight, Vector2.Zero, Spacing, CharactersBacking, neverFixedWidthCharacters: new[] { ',', '.', '%', 'x' }, fixedWidthCalculationCharacter: '5'); private class LegacyGlyphStore : ITexturedGlyphLookupStore { From ba99c5c134d5563725c46b57b6854befa2c91ac8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 14:39:02 +0900 Subject: [PATCH 4015/6909] Remove rolling delay on default combo counter --- osu.Game/Screens/Play/HUD/DefaultComboCounter.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs index a5c33f6dbe..63e7a88550 100644 --- a/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/DefaultComboCounter.cs @@ -15,8 +15,6 @@ namespace osu.Game.Screens.Play.HUD { private readonly Vector2 offset = new Vector2(20, 5); - protected override double RollingDuration => 750; - [Resolved(canBeNull: true)] private HUDOverlay hud { get; set; } From 39cf27637e157ec13d8378ba89999d8a64809fd2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 14:59:03 +0900 Subject: [PATCH 4016/6909] Update to use virtual methods instead of reconstructing TextBuilder --- osu.Game/Skinning/LegacySpriteText.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Skinning/LegacySpriteText.cs b/osu.Game/Skinning/LegacySpriteText.cs index d7a3975c72..5d0e312f7c 100644 --- a/osu.Game/Skinning/LegacySpriteText.cs +++ b/osu.Game/Skinning/LegacySpriteText.cs @@ -5,7 +5,6 @@ using System.Threading.Tasks; using osu.Framework.Graphics.Sprites; using osu.Framework.Text; using osu.Game.Graphics.Sprites; -using osuTK; namespace osu.Game.Skinning { @@ -13,6 +12,10 @@ namespace osu.Game.Skinning { private readonly LegacyGlyphStore glyphStore; + protected override char FixedWidthReferenceCharacter => '5'; + + protected override char[] FixedWidthExcludeCharacters => new[] { ',', '.', '%', 'x' }; + public LegacySpriteText(ISkin skin, string font = "score") { Shadow = false; @@ -22,8 +25,7 @@ namespace osu.Game.Skinning glyphStore = new LegacyGlyphStore(skin); } - protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => - new TextBuilder(glyphStore, Font, MaxWidth, UseFullGlyphHeight, Vector2.Zero, Spacing, CharactersBacking, neverFixedWidthCharacters: new[] { ',', '.', '%', 'x' }, fixedWidthCalculationCharacter: '5'); + protected override TextBuilder CreateTextBuilder(ITexturedGlyphLookupStore store) => base.CreateTextBuilder(glyphStore); private class LegacyGlyphStore : ITexturedGlyphLookupStore { From 7ed862edd7f81ff83a8b1dc98bed01a402977af6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 15:08:49 +0900 Subject: [PATCH 4017/6909] Add comment about migration code --- osu.Game.Tournament/IO/TournamentStorage.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 6505135b42..49906eff6d 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -36,6 +36,10 @@ namespace osu.Game.Tournament.IO public override void Migrate(Storage newStorage) { + // this migration only happens once on moving to the per-tournament storage system. + // listed files are those known at that point in time. + // this can be removed at some point in the future (6 months obsoletion would mean 2021-04-19) + var source = new DirectoryInfo(storage.GetFullPath("tournament")); var destination = new DirectoryInfo(newStorage.GetFullPath(".")); @@ -50,6 +54,7 @@ namespace osu.Game.Tournament.IO moveFileIfExists("drawings.txt", destination); moveFileIfExists("drawings_results.txt", destination); moveFileIfExists("drawings.ini", destination); + ChangeTargetStorage(newStorage); storageConfig.Set(StorageConfig.CurrentTournament, default_tournament); storageConfig.Save(); From 9c566e7ffbe2c289306a17d494f82802d8fc4ec7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 15:34:46 +0900 Subject: [PATCH 4018/6909] Update tests to use correct parameters of CleanRunGameHost --- .../NonVisual/CustomTourneyDirectoryTest.cs | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs index b75a9a6929..567d9f0d62 100644 --- a/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs +++ b/osu.Game.Tournament.Tests/NonVisual/CustomTourneyDirectoryTest.cs @@ -20,14 +20,14 @@ namespace osu.Game.Tournament.Tests.NonVisual [Test] public void TestDefaultDirectory() { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(TestDefaultDirectory))) + using (HeadlessGameHost host = new CleanRunHeadlessGameHost()) { try { var osu = loadOsu(host); var storage = osu.Dependencies.Get(); - var defaultStorage = Path.Combine(tournamentBasePath(nameof(TestDefaultDirectory)), "default"); - Assert.That(storage.GetFullPath("."), Is.EqualTo(defaultStorage)); + + Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default"))); } finally { @@ -39,7 +39,7 @@ namespace osu.Game.Tournament.Tests.NonVisual [Test] public void TestCustomDirectory() { - using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestCustomDirectory))) + using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestCustomDirectory))) // don't use clean run as we are writing a config file. { string osuDesktopStorage = basePath(nameof(TestCustomDirectory)); const string custom_tournament = "custom"; @@ -58,7 +58,7 @@ namespace osu.Game.Tournament.Tests.NonVisual storage = osu.Dependencies.Get(); - Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(tournamentBasePath(nameof(TestCustomDirectory)), custom_tournament))); + Assert.That(storage.GetFullPath("."), Is.EqualTo(Path.Combine(host.Storage.GetFullPath("."), "tournaments", custom_tournament))); } finally { @@ -70,7 +70,7 @@ namespace osu.Game.Tournament.Tests.NonVisual [Test] public void TestMigration() { - using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) + using (HeadlessGameHost host = new HeadlessGameHost(nameof(TestMigration))) // don't use clean run as we are writing test files for migration. { string osuRoot = basePath(nameof(TestMigration)); string configFile = Path.Combine(osuRoot, "tournament.ini"); @@ -115,7 +115,7 @@ namespace osu.Game.Tournament.Tests.NonVisual var storage = osu.Dependencies.Get(); - var migratedPath = Path.Combine(tournamentBasePath(nameof(TestMigration)), "default"); + string migratedPath = Path.Combine(host.Storage.GetFullPath("."), "tournaments", "default"); videosPath = Path.Combine(migratedPath, "videos"); modsPath = Path.Combine(migratedPath, "mods"); @@ -165,7 +165,5 @@ namespace osu.Game.Tournament.Tests.NonVisual } private string basePath(string testInstance) => Path.Combine(RuntimeInfo.StartupDirectory, "headless", testInstance); - - private string tournamentBasePath(string testInstance) => Path.Combine(basePath(testInstance), "tournaments"); } } From 31f6051db9504e4cef04fcc7fdd819a7f0827343 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 15:36:27 +0900 Subject: [PATCH 4019/6909] Add missing xmldoc --- osu.Game/IO/MigratableStorage.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/IO/MigratableStorage.cs b/osu.Game/IO/MigratableStorage.cs index 21087d7dc6..1b76725b04 100644 --- a/osu.Game/IO/MigratableStorage.cs +++ b/osu.Game/IO/MigratableStorage.cs @@ -14,7 +14,14 @@ namespace osu.Game.IO /// public abstract class MigratableStorage : WrappedStorage { + /// + /// A relative list of directory paths which should not be migrated. + /// public virtual string[] IgnoreDirectories => Array.Empty(); + + /// + /// A relative list of file paths which should not be migrated. + /// public virtual string[] IgnoreFiles => Array.Empty(); protected MigratableStorage(Storage storage, string subPath = null) From 3f41003d355c82b28fffd11f27394283f2e67847 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 15:48:15 +0900 Subject: [PATCH 4020/6909] Move video store out of TournamentStorage There was no reason it should be nested inside. --- osu.Game.Tournament/Components/TourneyVideo.cs | 5 ++--- osu.Game.Tournament/IO/TournamentStorage.cs | 2 -- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/Components/TourneyVideo.cs b/osu.Game.Tournament/Components/TourneyVideo.cs index 794b72b3a9..2709580385 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; using osu.Game.Tournament.IO; @@ -28,9 +27,9 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader] - private void load(Storage storage) + private void load(TournamentVideoResourceStore storage) { - var stream = (storage as TournamentStorage)?.VideoStore.GetStream(filename); + var stream = storage.GetStream(filename); if (stream != null) { diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 49906eff6d..2e8a6ce667 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -14,7 +14,6 @@ namespace osu.Game.Tournament.IO private const string default_tournament = "default"; private readonly Storage storage; private readonly TournamentStorageManager storageConfig; - public TournamentVideoResourceStore VideoStore { get; } public TournamentStorage(Storage storage) : base(storage.GetStorageForDirectory("tournaments"), string.Empty) @@ -30,7 +29,6 @@ namespace osu.Game.Tournament.IO else Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament)); - VideoStore = new TournamentVideoResourceStore(this); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); } From daceb0c04991d0b103ed81434cd05db4decd64f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 15:48:33 +0900 Subject: [PATCH 4021/6909] Fix texture store not being initialised correctly Without this change flags/mods would not work as expected. The video store was being added as the texture store incorrectly. --- osu.Game.Tournament/TournamentGameBase.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 6a533f96d8..dbda6aa023 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -40,8 +40,9 @@ namespace osu.Game.Tournament Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); dependencies.CacheAs(storage = new TournamentStorage(baseStorage)); + dependencies.Cache(new TournamentVideoResourceStore(storage)); - Textures.AddStore(new TextureLoaderStore(storage.VideoStore)); + Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage))); readBracket(); From f597572d739eeb71ac1a086cbf353e7233d236e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 16:02:39 +0900 Subject: [PATCH 4022/6909] Add comment with reasoning for TopRight anchor --- osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs index af0043436a..ec68223a3d 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledDrawable.cs @@ -73,6 +73,7 @@ namespace osu.Game.Graphics.UserInterfaceV2 }, new Container { + // top right works better when the vertical height of the component changes smoothly (avoids weird layout animations). Anchor = Anchor.TopRight, Origin = Anchor.TopRight, RelativeSizeAxes = Axes.X, From 401dd2db37a55460a8f1332daa3d174e59f14a41 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 16:55:00 +0900 Subject: [PATCH 4023/6909] 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 1d2cf22b28..2d531cf01e 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 133855c6c4..de7bde824f 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 73faa8541e..9c22dec330 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 79a17b23715b9fe0982d11975494de0cb43413da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 16:57:08 +0900 Subject: [PATCH 4024/6909] Reapply waveform colour fix --- 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 be3bca3242..9aff4ddf8f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline waveform = new WaveformGraph { RelativeSizeAxes = Axes.Both, - Colour = colours.Blue.Opacity(0.2f), + BaseColour = colours.Blue.Opacity(0.2f), LowColour = colours.BlueLighter, MidColour = colours.BlueDark, HighColour = colours.BlueDarker, From cd7c3021caf54210b5f2e3d80fb54d8d9a2917f8 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 19 Oct 2020 10:01:24 +0200 Subject: [PATCH 4025/6909] Trigger lock update on loading. --- osu.Android/GameplayScreenRotationLocker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Android/GameplayScreenRotationLocker.cs b/osu.Android/GameplayScreenRotationLocker.cs index fb471ceb78..25bd659a5d 100644 --- a/osu.Android/GameplayScreenRotationLocker.cs +++ b/osu.Android/GameplayScreenRotationLocker.cs @@ -20,7 +20,7 @@ namespace osu.Android private void load(OsuGame game) { localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); - localUserPlaying.ValueChanged += updateLock; + localUserPlaying.BindValueChanged(updateLock, true); } private void updateLock(ValueChangedEvent userPlaying) From 6d22f0e1962dd0530b473aca1105d6a6b01258c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 17:15:13 +0900 Subject: [PATCH 4026/6909] Use existing copy method and update xmldoc --- osu.Game/Online/Multiplayer/Room.cs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 5feebe8da1..9a21543b2e 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -104,21 +104,17 @@ namespace osu.Game.Online.Multiplayer public readonly Bindable Position = new Bindable(-1); /// - /// Create a copy of this room, without information specific to it, such as Room ID or host + /// Create a copy of this room without online information. + /// Should be used to create a local copy of a room for submitting in the future. /// public Room CreateCopy() { - Room newRoom = new Room - { - Name = { Value = Name.Value }, - Availability = { Value = Availability.Value }, - Type = { Value = Type.Value }, - MaxParticipants = { Value = MaxParticipants.Value } - }; + var copy = new Room(); - newRoom.Playlist.AddRange(Playlist); + copy.CopyFrom(this); + copy.RoomID.Value = null; - return newRoom; + return copy; } public void CopyFrom(Room other) From 437ca91b9441dc5891b39f81301b2c2af0989bf3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 17:15:35 +0900 Subject: [PATCH 4027/6909] Use DI to open the copy rather than passing in an ugly action --- .../Screens/Multi/Lounge/Components/DrawableRoom.cs | 10 +++++++--- .../Screens/Multi/Lounge/Components/RoomsContainer.cs | 8 -------- osu.Game/Screens/Multi/Multiplayer.cs | 11 ++++++----- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index db75df6054..01a85382e4 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -38,11 +38,12 @@ namespace osu.Game.Screens.Multi.Lounge.Components public event Action StateChanged; - public Action DuplicateRoom; - private readonly Box selectionBox; private CachedModelDependencyContainer dependencies; + [Resolved(canBeNull: true)] + private Multiplayer multiplayer { get; set; } + [Resolved] private BeatmapManager beatmaps { get; set; } @@ -239,7 +240,10 @@ namespace osu.Game.Screens.Multi.Lounge.Components public MenuItem[] ContextMenuItems => new MenuItem[] { - new OsuMenuItem("Create copy", MenuItemType.Standard, DuplicateRoom) + new OsuMenuItem("Create copy", MenuItemType.Standard, () => + { + multiplayer?.CreateRoom(Room.CreateCopy()); + }) }; } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index d9af38d19e..60c6aa1d8a 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -111,14 +111,6 @@ namespace osu.Game.Screens.Multi.Lounge.Components { roomFlow.Add(new DrawableRoom(room) { - DuplicateRoom = () => - { - Room newRoom = room.CreateCopy(); - if (!newRoom.Name.Value.StartsWith("Copy of ")) - newRoom.Name.Value = $"Copy of {room.Name.Value}"; - - loungeSubScreen?.Open(newRoom); - }, Action = () => { if (room == selectedRoom.Value) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index cdaeebefb7..27f774e9ec 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -134,7 +134,7 @@ namespace osu.Game.Screens.Multi { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Action = createRoom + Action = () => CreateRoom() }, roomManager = new RoomManager() } @@ -289,10 +289,11 @@ namespace osu.Game.Screens.Multi logo.Delay(WaveContainer.DISAPPEAR_DURATION / 2).FadeOut(); } - private void createRoom() - { - loungeSubScreen.Open(new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } }); - } + /// + /// Create a new room. + /// + /// An optional template to use when creating the room. + public void CreateRoom(Room room = null) => loungeSubScreen.Open(room ?? new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } }); private void beginHandlingTrack() { From 4024b44a53b0642bb15a18871caa9ecd80342925 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 17:41:21 +0900 Subject: [PATCH 4028/6909] Fix unsafe manipulation of parent's children from child --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 9289a6162c..a221ca7966 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -145,11 +145,19 @@ namespace osu.Game.Rulesets.Catch.UI } }; - trailsTarget.Add(trails = new CatcherTrailDisplay(this)); + trails = new CatcherTrailDisplay(this); updateCatcher(); } + protected override void LoadComplete() + { + base.LoadComplete(); + + // don't add in above load as we may potentially modify a parent in an unsafe manner. + trailsTarget.Add(trails); + } + /// /// Creates proxied content to be displayed beneath hitobjects. /// From 044622a7a60f3e588b2d2b6543ab6e81547bedc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 18:41:17 +0900 Subject: [PATCH 4029/6909] Fix out of bounds issues --- 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 06327bac3f..54860a894b 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -662,7 +662,7 @@ namespace osu.Game.Screens.Select // as we can't be 100% sure on the size of individual carousel drawables, // always play it safe and extend bounds by one. firstIndex = Math.Max(0, firstIndex - 1); - lastIndex = Math.Min(visibleItems.Count, lastIndex + 1); + lastIndex = Math.Clamp(lastIndex + 1, firstIndex, visibleItems.Count - 1); return (firstIndex, lastIndex); } From 1c2185e9691c1a595145d123555ad7decd5e2ffb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 18:41:28 +0900 Subject: [PATCH 4030/6909] Replace comment with link to issue --- 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 54860a894b..e3cd3461c2 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -895,7 +895,7 @@ namespace osu.Game.Screens.Select /// public bool UserScrolling { get; private set; } - // ReSharper disable once OptionalParameterHierarchyMismatch fuck off rider + // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { UserScrolling = true; From 28eae5d26b0a1e8b69cfa4dbf7ea6b5f03f62b9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 19:03:22 +0900 Subject: [PATCH 4031/6909] Fix migration test failures due to finalizer disposal of LocalConfigManager --- osu.Game/OsuGameBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 84766f196a..2d609668af 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -371,8 +371,10 @@ namespace osu.Game protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + RulesetStore?.Dispose(); BeatmapManager?.Dispose(); + LocalConfig?.Dispose(); contextFactory.FlushConnections(); } From 9106e97c37697812b3af9f508724273c27d4f457 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 19:10:01 +0900 Subject: [PATCH 4032/6909] Ensure max value in clamp is at least zero --- 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 e3cd3461c2..92abec0eee 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -662,7 +662,7 @@ namespace osu.Game.Screens.Select // as we can't be 100% sure on the size of individual carousel drawables, // always play it safe and extend bounds by one. firstIndex = Math.Max(0, firstIndex - 1); - lastIndex = Math.Clamp(lastIndex + 1, firstIndex, visibleItems.Count - 1); + lastIndex = Math.Clamp(lastIndex + 1, firstIndex, Math.Max(0, visibleItems.Count - 1)); return (firstIndex, lastIndex); } From d5940193a2dce5dd5e35568b0784c624f1117500 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 19:55:20 +0900 Subject: [PATCH 4033/6909] Ensure visible items is greater than zero before trying to display a range --- osu.Game/Screens/Select/BeatmapCarousel.cs | 49 ++++++++++++---------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 92abec0eee..83631fd383 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -590,36 +590,39 @@ namespace osu.Game.Screens.Select { displayedRange = newDisplayRange; - var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); - - foreach (var panel in ScrollableContent.Children) + if (visibleItems.Count > 0) { - if (toDisplay.Remove(panel.Item)) + var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); + + foreach (var panel in ScrollableContent.Children) { - // panel already displayed. - continue; + if (toDisplay.Remove(panel.Item)) + { + // panel already displayed. + continue; + } + + // panel loaded as drawable but not required by visible range. + // remove but only if too far off-screen + if (panel.Y + panel.DrawHeight < visibleUpperBound - distance_offscreen_before_unload || panel.Y > visibleBottomBound + distance_offscreen_before_unload) + { + // may want a fade effect here (could be seen if a huge change happens, like a set with 20 difficulties becomes selected). + panel.ClearTransforms(); + panel.Expire(); + } } - // panel loaded as drawable but not required by visible range. - // remove but only if too far off-screen - if (panel.Y + panel.DrawHeight < visibleUpperBound - distance_offscreen_before_unload || panel.Y > visibleBottomBound + distance_offscreen_before_unload) + // Add those items within the previously found index range that should be displayed. + foreach (var item in toDisplay) { - // may want a fade effect here (could be seen if a huge change happens, like a set with 20 difficulties becomes selected). - panel.ClearTransforms(); - panel.Expire(); + var panel = setPool.Get(p => p.Item = item); + + panel.Depth = item.CarouselYPosition; + panel.Y = item.CarouselYPosition; + + ScrollableContent.Add(panel); } } - - // Add those items within the previously found index range that should be displayed. - foreach (var item in toDisplay) - { - var panel = setPool.Get(p => p.Item = item); - - panel.Depth = item.CarouselYPosition; - panel.Y = item.CarouselYPosition; - - ScrollableContent.Add(panel); - } } // Finally, if the filtered items have changed, animate drawables to their new locations. From a50ca0a1edba6a40af2ad24cc30c1abb86a6a95d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 19 Oct 2020 20:00:17 +0900 Subject: [PATCH 4034/6909] Fix osu!catch test failures due to trying to retrieve container too early --- osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 1e708cce4b..1b8368794c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -123,7 +123,10 @@ namespace osu.Game.Rulesets.Catch.Tests Origin = Anchor.Centre, Scale = new Vector2(4f), }, skin); + }); + AddStep("get trails container", () => + { trails = catcherArea.OfType().Single(); catcherArea.MovableCatcher.SetHyperDashState(2); }); From 053c7a69a64ee67243f6a10d81c92644a824febb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 20:22:48 +0200 Subject: [PATCH 4035/6909] Fix code style issues & compilation failures --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 ++-- .../Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 5 ++--- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index a7ccc62e40..1d5d2f9431 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -20,6 +20,7 @@ using osu.Game.Skinning; using osu.Framework.Allocation; using osu.Game.Configuration; using osu.Framework.Bindables; +using osu.Game.Screens; using osuTK; namespace osu.Game.Rulesets.Osu.UI @@ -36,12 +37,10 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); - private Bindable showPlayfieldBorder; private readonly IDictionary> poolDictionary = new Dictionary>(); - public OsuPlayfield() { InternalChildren = new Drawable[] @@ -87,6 +86,7 @@ namespace osu.Game.Rulesets.Osu.UI private void load(OsuConfigManager config) { showPlayfieldBorder = config.GetBindable(OsuSetting.ShowPlayfieldBorder); + if (showPlayfieldBorder.Value) { AddInternal(new PlayfieldBorder diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 755f4e990c..e8a07fc831 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -76,14 +76,13 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay new SettingsEnumDropdown { LabelText = "Score display mode", - Bindable = config.GetBindable(OsuSetting.ScoreDisplayMode), + Current = config.GetBindable(OsuSetting.ScoreDisplayMode), Keywords = new[] { "scoring" } }, new SettingsCheckbox { LabelText = "Show playfield border", - Bindable = config.GetBindable(OsuSetting.ShowPlayfieldBorder) - Current = config.GetBindable(OsuSetting.ScoreDisplayMode), + Current = config.GetBindable(OsuSetting.ShowPlayfieldBorder), }, }; diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index fc80d4f3df..f4457c6aeb 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Edit drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[] { LayerBelowRuleset, - new EditorPlayfieldBorder { RelativeSizeAxes = Axes.Both } + new PlayfieldBorder { RelativeSizeAxes = Axes.Both } }), drawableRulesetWrapper, // layers above playfield From 4af3fd1ed68830f250704246e3957bcfb36e92ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 20:41:45 +0200 Subject: [PATCH 4036/6909] Allow toggling border on & off during gameplay --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 18 ++++++++++-------- osu.Game/Screens/PlayfieldBorder.cs | 7 ++++++- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 1d5d2f9431..8904345825 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -27,6 +27,7 @@ namespace osu.Game.Rulesets.Osu.UI { public class OsuPlayfield : Playfield { + private readonly PlayfieldBorder playfieldBorder; private readonly ProxyContainer approachCircles; private readonly ProxyContainer spinnerProxies; private readonly JudgementContainer judgementLayer; @@ -45,6 +46,11 @@ namespace osu.Game.Rulesets.Osu.UI { InternalChildren = new Drawable[] { + playfieldBorder = new PlayfieldBorder + { + RelativeSizeAxes = Axes.Both, + Depth = 3 + }, spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both @@ -86,16 +92,12 @@ namespace osu.Game.Rulesets.Osu.UI private void load(OsuConfigManager config) { showPlayfieldBorder = config.GetBindable(OsuSetting.ShowPlayfieldBorder); - - if (showPlayfieldBorder.Value) - { - AddInternal(new PlayfieldBorder - { - RelativeSizeAxes = Axes.Both - }); - } + showPlayfieldBorder.BindValueChanged(updateBorderVisibility, true); } + private void updateBorderVisibility(ValueChangedEvent settingChange) + => playfieldBorder.State.Value = settingChange.NewValue ? Visibility.Visible : Visibility.Hidden; + public override void Add(DrawableHitObject h) { h.OnNewResult += onNewResult; diff --git a/osu.Game/Screens/PlayfieldBorder.cs b/osu.Game/Screens/PlayfieldBorder.cs index a3be38f0a2..e88b73bc71 100644 --- a/osu.Game/Screens/PlayfieldBorder.cs +++ b/osu.Game/Screens/PlayfieldBorder.cs @@ -11,8 +11,10 @@ namespace osu.Game.Screens /// /// Provides a border around the playfield. /// - public class PlayfieldBorder : CompositeDrawable + public class PlayfieldBorder : VisibilityContainer { + private const int fade_duration = 200; + public PlayfieldBorder() { RelativeSizeAxes = Axes.Both; @@ -28,5 +30,8 @@ namespace osu.Game.Screens AlwaysPresent = true }; } + + protected override void PopIn() => this.FadeIn(fade_duration, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(fade_duration, Easing.OutQuint); } } From 4267d23d598f2635df363b5b42019c91ddc72ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 20:56:34 +0200 Subject: [PATCH 4037/6909] Move border to more appropriate namespace --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 1 - osu.Game/Rulesets/Edit/HitObjectComposer.cs | 1 - osu.Game/{Screens => Rulesets/UI}/PlayfieldBorder.cs | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) rename osu.Game/{Screens => Rulesets/UI}/PlayfieldBorder.cs (97%) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 8904345825..e536bd1503 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -20,7 +20,6 @@ using osu.Game.Skinning; using osu.Framework.Allocation; using osu.Game.Configuration; using osu.Framework.Bindables; -using osu.Game.Screens; using osuTK; namespace osu.Game.Rulesets.Osu.UI diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index f4457c6aeb..4c552c5083 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -20,7 +20,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; using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.RadioButtons; using osu.Game.Screens.Edit.Components.TernaryButtons; diff --git a/osu.Game/Screens/PlayfieldBorder.cs b/osu.Game/Rulesets/UI/PlayfieldBorder.cs similarity index 97% rename from osu.Game/Screens/PlayfieldBorder.cs rename to osu.Game/Rulesets/UI/PlayfieldBorder.cs index e88b73bc71..66371c89ad 100644 --- a/osu.Game/Screens/PlayfieldBorder.cs +++ b/osu.Game/Rulesets/UI/PlayfieldBorder.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osuTK.Graphics; -namespace osu.Game.Screens +namespace osu.Game.Rulesets.UI { /// /// Provides a border around the playfield. From dbda18acea2f9644bfc5a618c52ecb08de85a55f Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 19 Oct 2020 12:04:23 -0700 Subject: [PATCH 4038/6909] Fix autoplay/replay settings going off screen on some legacy skins --- osu.Game/Screens/Play/HUDOverlay.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index c3de249bf8..af5e2b80e1 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -208,17 +208,9 @@ namespace osu.Game.Screens.Play { base.Update(); - float topRightOffset = 0; + // for now align with the accuracy counter. eventually this will be user customisable. + topRightElements.Y = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y; - // fetch the bottom-most position of any main ui element that is anchored to the top of the screen. - // consider this kind of temporary. - foreach (var d in mainUIElements) - { - if (d is SkinnableDrawable sd && (sd.Drawable.Anchor & Anchor.y0) > 0) - topRightOffset = Math.Max(sd.Drawable.ScreenSpaceDrawQuad.BottomRight.Y, topRightOffset); - } - - topRightElements.Y = ToLocalSpace(new Vector2(0, topRightOffset)).Y; bottomRightElements.Y = -Progress.Height; } From bca05397350e213213aabce18d4c233187809cb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 21:00:49 +0200 Subject: [PATCH 4039/6909] Move setting to osu! ruleset subsection --- .../Configuration/OsuRulesetConfigManager.cs | 4 +++- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 10 +++++----- osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs | 5 +++++ osu.Game/Configuration/OsuConfigManager.cs | 3 --- .../Settings/Sections/Gameplay/GeneralSettings.cs | 5 ----- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index f76635a932..da8767c017 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -19,6 +19,7 @@ namespace osu.Game.Rulesets.Osu.Configuration Set(OsuRulesetSetting.SnakingInSliders, true); Set(OsuRulesetSetting.SnakingOutSliders, true); Set(OsuRulesetSetting.ShowCursorTrail, true); + Set(OsuRulesetSetting.ShowPlayfieldBorder, false); } } @@ -26,6 +27,7 @@ namespace osu.Game.Rulesets.Osu.Configuration { SnakingInSliders, SnakingOutSliders, - ShowCursorTrail + ShowCursorTrail, + ShowPlayfieldBorder, } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index e536bd1503..26fe686b0d 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -18,8 +18,8 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Skinning; using osu.Framework.Allocation; -using osu.Game.Configuration; using osu.Framework.Bindables; +using osu.Game.Rulesets.Osu.Configuration; using osuTK; namespace osu.Game.Rulesets.Osu.UI @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); - private Bindable showPlayfieldBorder; + private readonly Bindable showPlayfieldBorder = new BindableBool(); private readonly IDictionary> poolDictionary = new Dictionary>(); @@ -87,10 +87,10 @@ namespace osu.Game.Rulesets.Osu.UI AddRangeInternal(poolDictionary.Values); } - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + [BackgroundDependencyLoader(true)] + private void load(OsuRulesetConfigManager config) { - showPlayfieldBorder = config.GetBindable(OsuSetting.ShowPlayfieldBorder); + config?.BindWith(OsuRulesetSetting.ShowPlayfieldBorder, showPlayfieldBorder); showPlayfieldBorder.BindValueChanged(updateBorderVisibility, true); } diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index 3870f303b4..28c609f412 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -39,6 +39,11 @@ namespace osu.Game.Rulesets.Osu.UI LabelText = "Cursor trail", Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail) }, + new SettingsCheckbox + { + LabelText = "Show playfield border", + Current = config.GetBindable(OsuRulesetSetting.ShowPlayfieldBorder), + }, }; } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 14be15bb94..78179a781a 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -106,8 +106,6 @@ namespace osu.Game.Configuration Set(OsuSetting.IncreaseFirstObjectVisibility, true); Set(OsuSetting.GameplayDisableWinKey, true); - Set(OsuSetting.ShowPlayfieldBorder, false); - // Update Set(OsuSetting.ReleaseStream, ReleaseStream.Lazer); @@ -241,6 +239,5 @@ namespace osu.Game.Configuration HitLighting, MenuBackgroundSource, GameplayDisableWinKey, - ShowPlayfieldBorder, } } diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index e8a07fc831..07ad3c6b64 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -79,11 +79,6 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay Current = config.GetBindable(OsuSetting.ScoreDisplayMode), Keywords = new[] { "scoring" } }, - new SettingsCheckbox - { - LabelText = "Show playfield border", - Current = config.GetBindable(OsuSetting.ShowPlayfieldBorder), - }, }; if (RuntimeInfo.OS == RuntimeInfo.Platform.Windows) From 7c388f1132cad566f0b8ef05f8326fd1a383899c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 21:20:13 +0200 Subject: [PATCH 4040/6909] Move editor playfield border locally to osu! composer --- osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs | 5 +++++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 6 +----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index 912a705d16..d9de9d2c5d 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -56,6 +56,11 @@ namespace osu.Game.Rulesets.Osu.Edit [BackgroundDependencyLoader] private void load() { + LayerBelowRuleset.Add(new PlayfieldBorder + { + RelativeSizeAxes = Axes.Both, + State = { Value = Visibility.Visible } + }); LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both }); selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 4c552c5083..c9dd061b48 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -100,11 +100,7 @@ namespace osu.Game.Rulesets.Edit Children = new Drawable[] { // layers below playfield - drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChildren(new Drawable[] - { - LayerBelowRuleset, - new PlayfieldBorder { RelativeSizeAxes = Axes.Both } - }), + drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer().WithChild(LayerBelowRuleset), drawableRulesetWrapper, // layers above playfield drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() From fef6e55b397130d90b055b2939088cddb346d3b8 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 19 Oct 2020 12:32:16 -0700 Subject: [PATCH 4041/6909] Remove unused using and field --- osu.Game/Screens/Play/HUDOverlay.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index af5e2b80e1..0a45df51a0 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -16,7 +16,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; -using osu.Game.Skinning; using osuTK; using osuTK.Input; @@ -66,8 +65,6 @@ namespace osu.Game.Screens.Play private readonly FillFlowContainer bottomRightElements; private readonly FillFlowContainer topRightElements; - private readonly Container mainUIElements; - private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods) @@ -92,7 +89,7 @@ namespace osu.Game.Screens.Play { new Drawable[] { - mainUIElements = new Container + new Container { RelativeSizeAxes = Axes.Both, Children = new Drawable[] From 6e4b28ed1e69cdb3aaa574b035ce0dd040d82933 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 20 Oct 2020 00:32:44 +0300 Subject: [PATCH 4042/6909] Different version of epilepsy warning display --- .../Screens/Play/BeatmapMetadataDisplay.cs | 44 ------- osu.Game/Screens/Play/PlayerLoader.cs | 114 +++++++++++++++++- 2 files changed, 113 insertions(+), 45 deletions(-) diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index bab141a75e..5530b4beac 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -49,44 +49,6 @@ namespace osu.Game.Screens.Play } } - private class EpilepsyWarning : FillFlowContainer - { - public EpilepsyWarning(bool warning) - { - if (warning) - this.Show(); - else - this.Hide(); - - AutoSizeAxes = Axes.Both; - Direction = FillDirection.Vertical; - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Icon = FontAwesome.Solid.ExclamationTriangle, - Size = new Vector2(40), - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "This beatmap contains scenes with rapidly flashing colours.", - Font = OsuFont.GetFont(size: 20), - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Text = "Please take caution if you are affected by epilepsy.", - Font = OsuFont.GetFont(size: 20), - } - }; - } - } - private readonly WorkingBeatmap beatmap; private readonly Bindable> mods; private readonly Drawable facade; @@ -201,12 +163,6 @@ namespace osu.Game.Screens.Play Margin = new MarginPadding { Top = 20 }, Current = mods }, - new EpilepsyWarning(beatmap.BeatmapInfo.EpilepsyWarning) - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Margin = new MarginPadding { Top = 20 }, - } }, } }; diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 93a734589c..4c04627651 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -6,16 +6,21 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; using osu.Game.Input; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; @@ -84,6 +89,8 @@ namespace osu.Game.Screens.Play private bool hideOverlays; + private bool epilepsyShown; + private InputManager inputManager; private IdleTracker idleTracker; @@ -308,7 +315,27 @@ namespace osu.Game.Screens.Play { contentOut(); - this.Delay(250).Schedule(() => + if (true && !epilepsyShown) + { + EpilepsyWarning warning; + + AddInternal(warning = new EpilepsyWarning + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + State = { Value = Visibility.Visible } + }); + + epilepsyShown = true; + + this.Delay(2000).Schedule(() => + { + warning.Hide(); + warning.Expire(); + }); + } + + this.Delay(epilepsyShown ? 2500 : 250).Schedule(() => { if (!this.IsCurrentScreen()) return; @@ -398,5 +425,90 @@ namespace osu.Game.Screens.Play } #endregion + + private class EpilepsyWarning : VisibilityContainer + { + private readonly BindableDouble trackVolumeOnEpilepsyWarning = new BindableDouble(1f); + + private Track track; + private FillFlowContainer warningContent; + + public EpilepsyWarning() + { + RelativeSizeAxes = Axes.Both; + Alpha = 0f; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, IBindable beatmap) + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f), + }, + warningContent = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new SpriteIcon + { + Colour = colours.Yellow, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ExclamationTriangle, + Size = new Vector2(50), + }, + new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 25)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.Centre, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }.With(tfc => + { + tfc.AddText("This beatmap contains scenes with "); + tfc.AddText("rapidly flashing colours", s => + { + s.Font = s.Font.With(weight: FontWeight.Bold); + s.Colour = colours.Yellow; + }); + tfc.AddText("."); + + tfc.NewParagraph(); + tfc.AddText("Please take caution if you are affected by epilepsy."); + }), + } + } + }; + + track = beatmap.Value.Track; + track.AddAdjustment(AdjustableProperty.Volume, trackVolumeOnEpilepsyWarning); + } + + protected override void PopIn() + { + this.FadeIn(500, Easing.InQuint) + .TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, 500, Easing.InQuint); + + warningContent.FadeIn(500, Easing.InQuint); + } + + protected override void PopOut() => this.FadeOut(500, Easing.InQuint); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + track?.RemoveAdjustment(AdjustableProperty.Volume, trackVolumeOnEpilepsyWarning); + } + } } } From afa86f959f184acb76ace6b5b7ae68ff490b8440 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 19 Oct 2020 23:38:06 +0200 Subject: [PATCH 4043/6909] Changed scales of Seeding and Win screen to match the original These were measured by pixel-to-pixel comparing master vs this branch in ShareX at the same resolution. --- osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs | 3 +-- osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index b343608e69..32830713f6 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -288,8 +288,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro AutoSizeAxes = Axes.Both; Flag.RelativeSizeAxes = Axes.None; - Flag.Size = new Vector2(300, 200); - Flag.Scale = new Vector2(0.3f); + Flag.Scale = new Vector2(1.4f); InternalChild = new FillFlowContainer { diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index dde140ab91..3972c590ea 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -93,6 +93,7 @@ namespace osu.Game.Tournament.Screens.TeamWin Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(-300, 10), + Scale = new Vector2(2.2f) }, new FillFlowContainer { From 44279ed34741fc9a376735d45620bb371b44e79f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 23:46:09 +0200 Subject: [PATCH 4044/6909] Remove unused using directive --- osu.Game/Screens/Play/PlayerLoader.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 4c04627651..76020f5385 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -20,7 +20,6 @@ using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.Sprites; using osu.Game.Input; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; From aeca61eb3ecf652ba1dab3ec0abc3bc935f535ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 23:48:02 +0200 Subject: [PATCH 4045/6909] Split warning to separate file --- osu.Game/Screens/Play/EpilepsyWarning.cs | 105 +++++++++++++++++++++++ osu.Game/Screens/Play/PlayerLoader.cs | 89 ------------------- 2 files changed, 105 insertions(+), 89 deletions(-) create mode 100644 osu.Game/Screens/Play/EpilepsyWarning.cs diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs new file mode 100644 index 0000000000..e115c7e057 --- /dev/null +++ b/osu.Game/Screens/Play/EpilepsyWarning.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 osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Play +{ + public class EpilepsyWarning : VisibilityContainer + { + private readonly BindableDouble trackVolumeOnEpilepsyWarning = new BindableDouble(1f); + + private Track track; + private FillFlowContainer warningContent; + + public EpilepsyWarning() + { + RelativeSizeAxes = Axes.Both; + Alpha = 0f; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours, IBindable beatmap) + { + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black.Opacity(0.5f), + }, + warningContent = new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new SpriteIcon + { + Colour = colours.Yellow, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Icon = FontAwesome.Solid.ExclamationTriangle, + Size = new Vector2(50), + }, + new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 25)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.Centre, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }.With(tfc => + { + tfc.AddText("This beatmap contains scenes with "); + tfc.AddText("rapidly flashing colours", s => + { + s.Font = s.Font.With(weight: FontWeight.Bold); + s.Colour = colours.Yellow; + }); + tfc.AddText("."); + + tfc.NewParagraph(); + tfc.AddText("Please take caution if you are affected by epilepsy."); + }), + } + } + }; + + track = beatmap.Value.Track; + track.AddAdjustment(AdjustableProperty.Volume, trackVolumeOnEpilepsyWarning); + } + + protected override void PopIn() + { + this.FadeIn(500, Easing.InQuint) + .TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, 500, Easing.InQuint); + + warningContent.FadeIn(500, Easing.InQuint); + } + + protected override void PopOut() => this.FadeOut(500, Easing.InQuint); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + track?.RemoveAdjustment(AdjustableProperty.Volume, trackVolumeOnEpilepsyWarning); + } + } +} diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 76020f5385..801feb7d01 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -6,17 +6,13 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Threading; -using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -424,90 +420,5 @@ namespace osu.Game.Screens.Play } #endregion - - private class EpilepsyWarning : VisibilityContainer - { - private readonly BindableDouble trackVolumeOnEpilepsyWarning = new BindableDouble(1f); - - private Track track; - private FillFlowContainer warningContent; - - public EpilepsyWarning() - { - RelativeSizeAxes = Axes.Both; - Alpha = 0f; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours, IBindable beatmap) - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.5f), - }, - warningContent = new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new SpriteIcon - { - Colour = colours.Yellow, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = FontAwesome.Solid.ExclamationTriangle, - Size = new Vector2(50), - }, - new OsuTextFlowContainer(s => s.Font = OsuFont.GetFont(size: 25)) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - TextAnchor = Anchor.Centre, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }.With(tfc => - { - tfc.AddText("This beatmap contains scenes with "); - tfc.AddText("rapidly flashing colours", s => - { - s.Font = s.Font.With(weight: FontWeight.Bold); - s.Colour = colours.Yellow; - }); - tfc.AddText("."); - - tfc.NewParagraph(); - tfc.AddText("Please take caution if you are affected by epilepsy."); - }), - } - } - }; - - track = beatmap.Value.Track; - track.AddAdjustment(AdjustableProperty.Volume, trackVolumeOnEpilepsyWarning); - } - - protected override void PopIn() - { - this.FadeIn(500, Easing.InQuint) - .TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, 500, Easing.InQuint); - - warningContent.FadeIn(500, Easing.InQuint); - } - - protected override void PopOut() => this.FadeOut(500, Easing.InQuint); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - track?.RemoveAdjustment(AdjustableProperty.Volume, trackVolumeOnEpilepsyWarning); - } - } } } From a9f27a71a2ff60b848632cf5595b354b49db760a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 23:53:41 +0200 Subject: [PATCH 4046/6909] Fix code formatting issues --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 6 +++--- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 341924ae6d..1efc71cad3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -35,7 +35,7 @@ namespace osu.Game.Tests.Visual.Gameplay private TestPlayerLoaderContainer container; private TestPlayer player; - private bool EpilepsyWarning = false; + private bool epilepsyWarning; [Resolved] private AudioManager audioManager { get; set; } @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Gameplay beforeLoadAction?.Invoke(); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); - Beatmap.Value.BeatmapInfo.EpilepsyWarning = EpilepsyWarning; + Beatmap.Value.BeatmapInfo.EpilepsyWarning = epilepsyWarning; foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToTrack(Beatmap.Value.Track); @@ -247,7 +247,7 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(false)] public void TestEpilepsyWarning(bool warning) { - AddStep("change epilepsy warning", () => EpilepsyWarning = warning); + AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("load dummy beatmap", () => ResetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index fd17e38a4f..36a3880890 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -175,6 +175,7 @@ namespace osu.Game.Beatmaps.Formats case @"WidescreenStoryboard": beatmap.BeatmapInfo.WidescreenStoryboard = Parsing.ParseInt(pair.Value) == 1; break; + case @"EpilepsyWarning": beatmap.BeatmapInfo.EpilepsyWarning = Parsing.ParseInt(pair.Value) == 1; break; From 850590304142d76ccd59d130765283ec7020ce44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 00:08:05 +0200 Subject: [PATCH 4047/6909] Move warning construction to load() --- osu.Game/Screens/Play/PlayerLoader.cs | 34 ++++++++++++++------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 801feb7d01..8d66eb67d3 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -84,14 +85,15 @@ namespace osu.Game.Screens.Play private bool hideOverlays; - private bool epilepsyShown; - private InputManager inputManager; private IdleTracker idleTracker; private ScheduledDelegate scheduledPushPlayer; + [CanBeNull] + private EpilepsyWarning epilepsyWarning; + [Resolved(CanBeNull = true)] private NotificationOverlay notificationOverlay { get; set; } @@ -140,6 +142,15 @@ namespace osu.Game.Screens.Play }, idleTracker = new IdleTracker(750) }); + + if (Beatmap.Value.BeatmapInfo.EpilepsyWarning) + { + AddInternal(epilepsyWarning = new EpilepsyWarning + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } } protected override void LoadComplete() @@ -310,27 +321,18 @@ namespace osu.Game.Screens.Play { contentOut(); - if (true && !epilepsyShown) + if (epilepsyWarning != null) { - EpilepsyWarning warning; - - AddInternal(warning = new EpilepsyWarning - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - State = { Value = Visibility.Visible } - }); - - epilepsyShown = true; + epilepsyWarning.State.Value = Visibility.Visible; this.Delay(2000).Schedule(() => { - warning.Hide(); - warning.Expire(); + epilepsyWarning.Hide(); + epilepsyWarning.Expire(); }); } - this.Delay(epilepsyShown ? 2500 : 250).Schedule(() => + this.Delay(epilepsyWarning?.State.Value == Visibility.Visible ? 2500 : 250).Schedule(() => { if (!this.IsCurrentScreen()) return; From 1ac0b3b13d5cd90028b2fae6596741a01178c949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 00:08:26 +0200 Subject: [PATCH 4048/6909] Add asserts to tests --- osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 1efc71cad3..38eca72bd1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Overlays; @@ -249,7 +250,10 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("change epilepsy warning", () => epilepsyWarning = warning); AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType().Any() == warning); } private class TestPlayerLoaderContainer : Container From 6e50ae045834f85310fa2943a12bd17e69fcbee7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 00:22:30 +0200 Subject: [PATCH 4049/6909] Reformulate push sequence code --- osu.Game/Screens/Play/PlayerLoader.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 8d66eb67d3..eabe090725 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Transforms; using osu.Framework.Input; using osu.Framework.Screens; using osu.Framework.Threading; @@ -321,18 +322,24 @@ namespace osu.Game.Screens.Play { contentOut(); + TransformSequence pushSequence; + if (epilepsyWarning != null) { epilepsyWarning.State.Value = Visibility.Visible; - this.Delay(2000).Schedule(() => + pushSequence = this.Delay(3000).Schedule(() => { epilepsyWarning.Hide(); epilepsyWarning.Expire(); }); } + else + { + pushSequence = this.Delay(0); + } - this.Delay(epilepsyWarning?.State.Value == Visibility.Visible ? 2500 : 250).Schedule(() => + pushSequence.Delay(250).Schedule(() => { if (!this.IsCurrentScreen()) return; From 1238e6c30fc27f905bdf9274fe0feb976b07cd27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 00:46:08 +0200 Subject: [PATCH 4050/6909] Add flag value to database Unfortunately required, as loadBeatmaps() refreshes the decoded beatmap with the database-stored values, which can end up overwriting the decoded ones. --- osu.Game/Beatmaps/BeatmapInfo.cs | 2 - ...01019224408_AddEpilepsyWarning.Designer.cs | 508 ++++++++++++++++++ .../20201019224408_AddEpilepsyWarning.cs | 23 + .../Migrations/OsuDbContextModelSnapshot.cs | 2 + 4 files changed, 533 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs create mode 100644 osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index b7946d53ca..1512240f8a 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -90,8 +90,6 @@ namespace osu.Game.Beatmaps public bool LetterboxInBreaks { get; set; } public bool WidescreenStoryboard { get; set; } - - [NotMapped] public bool EpilepsyWarning { get; set; } // Editor diff --git a/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.Designer.cs new file mode 100644 index 0000000000..1c05de832e --- /dev/null +++ b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.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("20201019224408_AddEpilepsyWarning")] + partial class AddEpilepsyWarning + { + 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("EpilepsyWarning"); + + 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/20201019224408_AddEpilepsyWarning.cs b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs new file mode 100644 index 0000000000..be6968aa5d --- /dev/null +++ b/osu.Game/Migrations/20201019224408_AddEpilepsyWarning.cs @@ -0,0 +1,23 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace osu.Game.Migrations +{ + public partial class AddEpilepsyWarning : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "EpilepsyWarning", + table: "BeatmapInfo", + nullable: false, + defaultValue: false); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropColumn( + name: "EpilepsyWarning", + table: "BeatmapInfo"); + } + } +} diff --git a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs index bc4fc3342d..ec4461ca56 100644 --- a/osu.Game/Migrations/OsuDbContextModelSnapshot.cs +++ b/osu.Game/Migrations/OsuDbContextModelSnapshot.cs @@ -57,6 +57,8 @@ namespace osu.Game.Migrations b.Property("DistanceSpacing"); + b.Property("EpilepsyWarning"); + b.Property("GridSize"); b.Property("Hash"); From a164d330e57744b80f0834dfa8d5601c4baab9f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 00:51:31 +0200 Subject: [PATCH 4051/6909] Improve feel of transition --- osu.Game/Screens/Play/EpilepsyWarning.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs index e115c7e057..244a903bdd 100644 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ b/osu.Game/Screens/Play/EpilepsyWarning.cs @@ -89,12 +89,12 @@ namespace osu.Game.Screens.Play protected override void PopIn() { this.FadeIn(500, Easing.InQuint) - .TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, 500, Easing.InQuint); + .TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, 500, Easing.InSine); warningContent.FadeIn(500, Easing.InQuint); } - protected override void PopOut() => this.FadeOut(500, Easing.InQuint); + protected override void PopOut() => this.FadeOut(500, Easing.OutQuint); protected override void Dispose(bool isDisposing) { From 1fc22bdbffdace861ad701520e6cc7d44055d5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 00:59:30 +0200 Subject: [PATCH 4052/6909] Only show warning once on given map --- osu.Game/Screens/Play/PlayerLoader.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index eabe090725..fb03f09d8e 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -324,7 +324,9 @@ namespace osu.Game.Screens.Play TransformSequence pushSequence; - if (epilepsyWarning != null) + // only show if the warning was created (i.e. the beatmap needs it) + // and this is not a restart of the map (the warning expires after first load). + if (epilepsyWarning?.IsAlive == true) { epilepsyWarning.State.Value = Visibility.Visible; From 05251c646ed52a55027d558136f7f4a48a967ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 01:06:20 +0200 Subject: [PATCH 4053/6909] Fade volume back up on pop out --- osu.Game/Screens/Play/EpilepsyWarning.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs index 244a903bdd..051604f115 100644 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ b/osu.Game/Screens/Play/EpilepsyWarning.cs @@ -89,12 +89,14 @@ namespace osu.Game.Screens.Play protected override void PopIn() { this.FadeIn(500, Easing.InQuint) - .TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, 500, Easing.InSine); + .TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, 500, Easing.InQuint); warningContent.FadeIn(500, Easing.InQuint); } - protected override void PopOut() => this.FadeOut(500, Easing.OutQuint); + protected override void PopOut() + => this.FadeOut(500, Easing.OutQuint) + .TransformBindableTo(trackVolumeOnEpilepsyWarning, 1, 500, Easing.OutQuint); protected override void Dispose(bool isDisposing) { From c57fecd1fc4379b7d15e58c5612e8c425ddea375 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Oct 2020 12:43:57 +0900 Subject: [PATCH 4054/6909] Update comment to make it clear this is a hack --- osu.Game/Screens/Play/HUDOverlay.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 0a45df51a0..6425f8123d 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -205,7 +205,9 @@ namespace osu.Game.Screens.Play { base.Update(); - // for now align with the accuracy counter. eventually this will be user customisable. + // HACK: for now align with the accuracy counter. + // this is done for the sake of hacky legacy skins which extend the health bar to take up the full screen area. + // it only works with the default skin due to padding offsetting it *just enough* to coexist. topRightElements.Y = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y; bottomRightElements.Y = -Progress.Height; From 267b399f9f120f1d92500f775fbbbb949ee0290d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Oct 2020 13:59:03 +0900 Subject: [PATCH 4055/6909] Add some simple border styles --- .../Configuration/OsuRulesetConfigManager.cs | 5 +- .../Edit/OsuHitObjectComposer.cs | 14 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 8 +- .../UI/OsuSettingsSubsection.cs | 7 +- osu.Game/Rulesets/UI/PlayfieldBorder.cs | 137 ++++++++++++++++-- osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs | 12 ++ 6 files changed, 156 insertions(+), 27 deletions(-) create mode 100644 osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs diff --git a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs index da8767c017..e8272057f3 100644 --- a/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs +++ b/osu.Game.Rulesets.Osu/Configuration/OsuRulesetConfigManager.cs @@ -3,6 +3,7 @@ using osu.Game.Configuration; using osu.Game.Rulesets.Configuration; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Configuration { @@ -19,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Configuration Set(OsuRulesetSetting.SnakingInSliders, true); Set(OsuRulesetSetting.SnakingOutSliders, true); Set(OsuRulesetSetting.ShowCursorTrail, true); - Set(OsuRulesetSetting.ShowPlayfieldBorder, false); + Set(OsuRulesetSetting.PlayfieldBorderStyle, PlayfieldBorderStyle.None); } } @@ -28,6 +29,6 @@ namespace osu.Game.Rulesets.Osu.Configuration SnakingInSliders, SnakingOutSliders, ShowCursorTrail, - ShowPlayfieldBorder, + PlayfieldBorderStyle, } } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index d9de9d2c5d..edd684d886 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -56,12 +56,18 @@ namespace osu.Game.Rulesets.Osu.Edit [BackgroundDependencyLoader] private void load() { - LayerBelowRuleset.Add(new PlayfieldBorder + LayerBelowRuleset.AddRange(new Drawable[] { - RelativeSizeAxes = Axes.Both, - State = { Value = Visibility.Visible } + new PlayfieldBorder + { + RelativeSizeAxes = Axes.Both, + PlayfieldBorderStyle = { Value = PlayfieldBorderStyle.Corners } + }, + distanceSnapGridContainer = new Container + { + RelativeSizeAxes = Axes.Both + } }); - LayerBelowRuleset.Add(distanceSnapGridContainer = new Container { RelativeSizeAxes = Axes.Both }); selectedHitObjects = EditorBeatmap.SelectedHitObjects.GetBoundCopy(); selectedHitObjects.CollectionChanged += (_, __) => updateDistanceSnapGrid(); diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 26fe686b0d..50727d590a 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); - private readonly Bindable showPlayfieldBorder = new BindableBool(); + private readonly Bindable playfieldBorderStyle = new BindableBool(); private readonly IDictionary> poolDictionary = new Dictionary>(); @@ -90,13 +90,9 @@ namespace osu.Game.Rulesets.Osu.UI [BackgroundDependencyLoader(true)] private void load(OsuRulesetConfigManager config) { - config?.BindWith(OsuRulesetSetting.ShowPlayfieldBorder, showPlayfieldBorder); - showPlayfieldBorder.BindValueChanged(updateBorderVisibility, true); + config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); } - private void updateBorderVisibility(ValueChangedEvent settingChange) - => playfieldBorder.State.Value = settingChange.NewValue ? Visibility.Visible : Visibility.Hidden; - public override void Add(DrawableHitObject h) { h.OnNewResult += onNewResult; diff --git a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs index 28c609f412..705ba3e929 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuSettingsSubsection.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Overlays.Settings; using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI { @@ -39,10 +40,10 @@ namespace osu.Game.Rulesets.Osu.UI LabelText = "Cursor trail", Current = config.GetBindable(OsuRulesetSetting.ShowCursorTrail) }, - new SettingsCheckbox + new SettingsEnumDropdown { - LabelText = "Show playfield border", - Current = config.GetBindable(OsuRulesetSetting.ShowPlayfieldBorder), + LabelText = "Playfield border style", + Current = config.GetBindable(OsuRulesetSetting.PlayfieldBorderStyle), }, }; } diff --git a/osu.Game/Rulesets/UI/PlayfieldBorder.cs b/osu.Game/Rulesets/UI/PlayfieldBorder.cs index 66371c89ad..c83d1e7842 100644 --- a/osu.Game/Rulesets/UI/PlayfieldBorder.cs +++ b/osu.Game/Rulesets/UI/PlayfieldBorder.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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.UI @@ -11,27 +15,136 @@ namespace osu.Game.Rulesets.UI /// /// Provides a border around the playfield. /// - public class PlayfieldBorder : VisibilityContainer + public class PlayfieldBorder : CompositeDrawable { - private const int fade_duration = 200; + public Bindable PlayfieldBorderStyle { get; } = new Bindable(); + + private const int fade_duration = 500; + + private const float corner_length = 0.05f; + private const float corner_thickness = 2; public PlayfieldBorder() { RelativeSizeAxes = Axes.Both; - Masking = true; - BorderColour = Color4.White; - BorderThickness = 2; - - InternalChild = new Box + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true + new Line(Direction.Horizontal) + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, + new Line(Direction.Horizontal) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + new Line(Direction.Horizontal) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + new Line(Direction.Horizontal) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, + new Line(Direction.Vertical) + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, + new Line(Direction.Vertical) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + new Line(Direction.Vertical) + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + }, + new Line(Direction.Vertical) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + }, }; } - protected override void PopIn() => this.FadeIn(fade_duration, Easing.OutQuint); - protected override void PopOut() => this.FadeOut(fade_duration, Easing.OutQuint); + protected override void LoadComplete() + { + base.LoadComplete(); + + PlayfieldBorderStyle.BindValueChanged(updateStyle, true); + } + + private void updateStyle(ValueChangedEvent style) + { + switch (style.NewValue) + { + case UI.PlayfieldBorderStyle.None: + this.FadeOut(fade_duration, Easing.OutQuint); + foreach (var line in InternalChildren.OfType()) + line.TweenLength(0); + + break; + + case UI.PlayfieldBorderStyle.Corners: + this.FadeIn(fade_duration, Easing.OutQuint); + foreach (var line in InternalChildren.OfType()) + line.TweenLength(corner_length); + + break; + + case UI.PlayfieldBorderStyle.Full: + this.FadeIn(fade_duration, Easing.OutQuint); + foreach (var line in InternalChildren.OfType()) + line.TweenLength(0.5f); + + break; + } + } + + private class Line : Box + { + private readonly Direction direction; + + public Line(Direction direction) + { + this.direction = direction; + + Colour = Color4.White; + // starting in relative avoids the framework thinking it knows best and setting the width to 1 initially. + + switch (direction) + { + case Direction.Horizontal: + RelativeSizeAxes = Axes.X; + Size = new Vector2(0, corner_thickness); + break; + + case Direction.Vertical: + RelativeSizeAxes = Axes.Y; + Size = new Vector2(corner_thickness, 0); + break; + } + } + + public void TweenLength(float value) + { + switch (direction) + { + case Direction.Horizontal: + this.ResizeWidthTo(value, fade_duration, Easing.OutQuint); + break; + + case Direction.Vertical: + this.ResizeHeightTo(value, fade_duration, Easing.OutQuint); + break; + } + } + } } } diff --git a/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs b/osu.Game/Rulesets/UI/PlayfieldBorderStyle.cs new file mode 100644 index 0000000000..0a0aad884e --- /dev/null +++ b/osu.Game/Rulesets/UI/PlayfieldBorderStyle.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.UI +{ + public enum PlayfieldBorderStyle + { + None, + Corners, + Full + } +} From f9ca47ca86a2d7e40f6b05a46e3e9ddf5d85f37e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Oct 2020 13:59:07 +0900 Subject: [PATCH 4056/6909] Add test coverage --- .../TestPlayfieldBorder.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs b/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.cs new file mode 100644 index 0000000000..23d9d265be --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestPlayfieldBorder.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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.UI; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestPlayfieldBorder : OsuTestScene + { + public TestPlayfieldBorder() + { + Bindable playfieldBorderStyle = new Bindable(); + + AddStep("add drawables", () => + { + Child = new Container + { + Size = new Vector2(400, 300), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] + { + new PlayfieldBorder + { + PlayfieldBorderStyle = { BindTarget = playfieldBorderStyle } + } + } + }; + }); + + AddStep("Set none", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.None); + AddStep("Set corners", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Corners); + AddStep("Set full", () => playfieldBorderStyle.Value = PlayfieldBorderStyle.Full); + } + } +} From 024009e1749aee8c7a147c1ac2645080b88b25f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Oct 2020 14:19:15 +0900 Subject: [PATCH 4057/6909] Change default to "always visible" --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index e95f8b571a..7d601c0cb9 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -90,7 +90,7 @@ namespace osu.Game.Configuration Set(OsuSetting.HitLighting, true); - Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.DuringGameplay); + Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Always); Set(OsuSetting.ShowProgressGraph, true); Set(OsuSetting.ShowHealthDisplayWhenCantFail, true); Set(OsuSetting.FadePlayfieldWhenHealthLow, true); From 4f8a755518d45ba9d4c40b717e0729cb8ebbac82 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Oct 2020 14:20:44 +0900 Subject: [PATCH 4058/6909] Add "hide during gameplay" mode --- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- osu.Game/Configuration/HUDVisibilityMode.cs | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 55d6b38b66..6ec673704c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExternalHideDoesntAffectConfig() { - HUDVisibilityMode originalConfigValue = HUDVisibilityMode.DuringGameplay; + HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringBreaks; createNew(); diff --git a/osu.Game/Configuration/HUDVisibilityMode.cs b/osu.Game/Configuration/HUDVisibilityMode.cs index 2b133b1bcf..b0b55dd811 100644 --- a/osu.Game/Configuration/HUDVisibilityMode.cs +++ b/osu.Game/Configuration/HUDVisibilityMode.cs @@ -9,8 +9,11 @@ namespace osu.Game.Configuration { Never, + [Description("Hide during gameplay")] + HideDuringGameplay, + [Description("Hide during breaks")] - DuringGameplay, + HideDuringBreaks, Always } From 4e57751ca1ebe5284a58d783bea35c6786e7b8df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Oct 2020 15:03:12 +0900 Subject: [PATCH 4059/6909] Fix background dim application to avoid overdraw, transition smoother --- osu.Game/Screens/Play/EpilepsyWarning.cs | 18 +++++++----------- osu.Game/Screens/Play/PlayerLoader.cs | 3 +++ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs index 051604f115..960d549ab6 100644 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ b/osu.Game/Screens/Play/EpilepsyWarning.cs @@ -5,16 +5,14 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Screens.Backgrounds; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Play { @@ -31,16 +29,13 @@ namespace osu.Game.Screens.Play Alpha = 0f; } + public BackgroundScreenBeatmap DimmableBackground { get; set; } + [BackgroundDependencyLoader] private void load(OsuColour colours, IBindable beatmap) { Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4.Black.Opacity(0.5f), - }, warningContent = new FillFlowContainer { Anchor = Anchor.Centre, @@ -88,10 +83,11 @@ namespace osu.Game.Screens.Play protected override void PopIn() { - this.FadeIn(500, Easing.InQuint) - .TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, 500, Easing.InQuint); + this.TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, FADE_DURATION); - warningContent.FadeIn(500, Easing.InQuint); + DimmableBackground?.FadeColour(OsuColour.Gray(0.5f), FADE_DURATION, Easing.OutQuint); + + this.FadeIn(FADE_DURATION, Easing.OutQuint); } protected override void PopOut() diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index f68246c928..ef01072a62 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -167,6 +167,9 @@ namespace osu.Game.Screens.Play { base.OnEntering(last); + if (epilepsyWarning != null) + epilepsyWarning.DimmableBackground = Background; + content.ScaleTo(0.7f); Background?.FadeColour(Color4.White, 800, Easing.OutQuint); From 7a68636f71195e9003e84d52b3f1e598e3089ad4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Oct 2020 15:03:33 +0900 Subject: [PATCH 4060/6909] Adjust fade sequence and durations to feel better --- osu.Game/Screens/Play/EpilepsyWarning.cs | 6 +++--- osu.Game/Screens/Play/PlayerLoader.cs | 23 +++++++++++++---------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs index 960d549ab6..1c8e8c2f4e 100644 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ b/osu.Game/Screens/Play/EpilepsyWarning.cs @@ -18,6 +18,8 @@ namespace osu.Game.Screens.Play { public class EpilepsyWarning : VisibilityContainer { + public const double FADE_DURATION = 500; + private readonly BindableDouble trackVolumeOnEpilepsyWarning = new BindableDouble(1f); private Track track; @@ -90,9 +92,7 @@ namespace osu.Game.Screens.Play this.FadeIn(FADE_DURATION, Easing.OutQuint); } - protected override void PopOut() - => this.FadeOut(500, Easing.OutQuint) - .TransformBindableTo(trackVolumeOnEpilepsyWarning, 1, 500, Easing.OutQuint); + protected override void PopOut() => this.FadeOut(FADE_DURATION); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index ef01072a62..fae0bfb295 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -323,26 +323,29 @@ namespace osu.Game.Screens.Play { contentOut(); - TransformSequence pushSequence; + TransformSequence pushSequence = this.Delay(250); // only show if the warning was created (i.e. the beatmap needs it) // and this is not a restart of the map (the warning expires after first load). if (epilepsyWarning?.IsAlive == true) { - epilepsyWarning.State.Value = Visibility.Visible; + const double epilepsy_display_length = 3000; - pushSequence = this.Delay(3000).Schedule(() => + pushSequence.Schedule(() => { - epilepsyWarning.Hide(); - epilepsyWarning.Expire(); + epilepsyWarning.State.Value = Visibility.Visible; + + this.Delay(epilepsy_display_length).Schedule(() => + { + epilepsyWarning.Hide(); + epilepsyWarning.Expire(); + }); }); - } - else - { - pushSequence = this.Delay(0); + + pushSequence.Delay(epilepsy_display_length); } - pushSequence.Delay(250).Schedule(() => + pushSequence.Schedule(() => { if (!this.IsCurrentScreen()) return; From 411ae38605bd601336de5f01e88e48e65d2453be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Oct 2020 15:06:31 +0900 Subject: [PATCH 4061/6909] Remove unused using --- osu.Game/Rulesets/UI/PlayfieldBorder.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/PlayfieldBorder.cs b/osu.Game/Rulesets/UI/PlayfieldBorder.cs index c83d1e7842..458b88c6db 100644 --- a/osu.Game/Rulesets/UI/PlayfieldBorder.cs +++ b/osu.Game/Rulesets/UI/PlayfieldBorder.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.Framework.Graphics; From 55d08fad5cdca600405551729126dbfe2634f687 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 20 Oct 2020 15:18:15 +0900 Subject: [PATCH 4062/6909] Remove unused field --- osu.Game/Screens/Play/EpilepsyWarning.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs index 1c8e8c2f4e..e3cf0cd227 100644 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ b/osu.Game/Screens/Play/EpilepsyWarning.cs @@ -23,7 +23,6 @@ namespace osu.Game.Screens.Play private readonly BindableDouble trackVolumeOnEpilepsyWarning = new BindableDouble(1f); private Track track; - private FillFlowContainer warningContent; public EpilepsyWarning() { @@ -38,7 +37,7 @@ namespace osu.Game.Screens.Play { Children = new Drawable[] { - warningContent = new FillFlowContainer + new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, From 2c7880e9d6118d01ca5c0150780f1cf136c357df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 18:27:03 +0200 Subject: [PATCH 4063/6909] Add failing test case --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 888a2f2197..9b31dd045a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -265,6 +265,26 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddAssert($"epilepsy warning {(warning ? "present" : "absent")}", () => this.ChildrenOfType().Any() == warning); + + if (warning) + { + AddUntilStep("sound volume decreased", () => Beatmap.Value.Track.AggregateVolume.Value == 0.25); + AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); + } + } + + [Test] + public void TestEpilepsyWarningEarlyExit() + { + AddStep("set epilepsy warning", () => epilepsyWarning = true); + AddStep("load dummy beatmap", () => ResetPlayer(false)); + + AddUntilStep("wait for current", () => loader.IsCurrentScreen()); + + AddUntilStep("wait for epilepsy warning", () => loader.ChildrenOfType().Single().Alpha > 0); + AddStep("exit early", () => loader.Exit()); + + AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } private class TestPlayerLoaderContainer : Container From e54836a63e4051bc517d1d044cfe566490fabf33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 22:27:59 +0200 Subject: [PATCH 4064/6909] Use SkinnableSprite to avoid unnecessary reloads --- .../Drawables/DrawableStoryboardAnimation.cs | 52 +++++-------------- .../Drawables/DrawableStoryboardSprite.cs | 39 ++++---------- 2 files changed, 24 insertions(+), 67 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 963cf37fea..c3b6dde44b 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -2,13 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.IO; using osuTK; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -16,18 +15,10 @@ using osu.Game.Skinning; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardAnimation : SkinReloadableDrawable, IFlippable, IVectorScalable + public class DrawableStoryboardAnimation : DrawableAnimation, IFlippable, IVectorScalable { public StoryboardAnimation Animation { get; } - private TextureAnimation drawableTextureAnimation; - - [Resolved] - private TextureStore storyboardTextureStore { get; set; } - - private readonly List texturePathsRaw = new List(); - private readonly List texturePaths = new List(); - private bool flipH; public bool FlipH @@ -119,48 +110,31 @@ namespace osu.Game.Storyboards.Drawables Animation = animation; Origin = animation.Origin; Position = animation.InitialPosition; + Loop = animation.LoopType == AnimationLoopType.LoopForever; LifetimeStart = animation.StartTime; LifetimeEnd = animation.EndTime; } [BackgroundDependencyLoader] - private void load(IBindable beatmap) + private void load(IBindable beatmap, TextureStore textureStore) { - InternalChild = drawableTextureAnimation = new TextureAnimation - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Loop = Animation.LoopType == AnimationLoopType.LoopForever - }; - for (var frame = 0; frame < Animation.FrameCount; frame++) { var framePath = Animation.Path.Replace(".", frame + "."); - texturePathsRaw.Add(Path.GetFileNameWithoutExtension(framePath)); - var path = beatmap.Value.BeatmapSetInfo.Files.Find(f => f.Filename.Equals(framePath, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - texturePaths.Add(path); + var storyboardPath = beatmap.Value.BeatmapSetInfo.Files.Find(f => f.Filename.Equals(framePath, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; + var frameSprite = storyboardPath != null + ? (Drawable)new Sprite + { + Texture = textureStore.Get(storyboardPath) + } + : new SkinnableSprite(framePath); // fall back to skin textures if not found in storyboard files. + + AddFrame(frameSprite, Animation.FrameDelay); } Animation.ApplyTransforms(this); } - - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - base.SkinChanged(skin, allowFallback); - - drawableTextureAnimation.ClearFrames(); - - for (var frame = 0; frame < Animation.FrameCount; frame++) - { - var texture = skin?.GetTexture(texturePathsRaw[frame]) ?? storyboardTextureStore?.Get(texturePaths[frame]); - - if (texture == null) - continue; - - drawableTextureAnimation.AddFrame(texture, Animation.FrameDelay); - } - } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index cd09cafbce..95774de898 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.IO; using osuTK; 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.Framework.Utils; @@ -15,17 +15,10 @@ using osu.Game.Skinning; namespace osu.Game.Storyboards.Drawables { - public class DrawableStoryboardSprite : SkinReloadableDrawable, IFlippable, IVectorScalable + public class DrawableStoryboardSprite : CompositeDrawable, IFlippable, IVectorScalable { public StoryboardSprite Sprite { get; } - private Sprite drawableSprite; - - [Resolved] - private TextureStore storyboardTextureStore { get; set; } - - private string texturePath; - private bool flipH; public bool FlipH @@ -123,29 +116,19 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(IBindable beatmap) + private void load(IBindable beatmap, TextureStore textureStore) { - InternalChild = drawableSprite = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }; + var storyboardPath = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; + var sprite = storyboardPath != null + ? (Drawable)new Sprite + { + Texture = textureStore.Get(storyboardPath) + } + : new SkinnableSprite(Sprite.Path); // fall back to skin textures if not found in storyboard files. - texturePath = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; + InternalChild = sprite.With(s => s.Anchor = s.Origin = Anchor.Centre); Sprite.ApplyTransforms(this); } - - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - base.SkinChanged(skin, allowFallback); - - var newTexture = skin?.GetTexture(Path.GetFileNameWithoutExtension(Sprite.Path)) ?? storyboardTextureStore?.Get(texturePath); - - if (drawableSprite.Texture == newTexture) return; - - drawableSprite.Size = Vector2.Zero; // Sprite size needs to be recalculated (e.g. aspect ratio of combo number textures may differ between skins) - drawableSprite.Texture = newTexture; - } } } From cdd56ece871a9999288196a859033a44665a3c10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 23:32:04 +0200 Subject: [PATCH 4065/6909] Read UseSkinSprites when decoding storyboards --- .../Beatmaps/Formats/LegacyStoryboardDecoder.cs | 16 ++++++++++++++++ osu.Game/Storyboards/Storyboard.cs | 5 +++++ 2 files changed, 21 insertions(+) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 269449ef80..e2550d1ca4 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -48,6 +48,10 @@ namespace osu.Game.Beatmaps.Formats switch (section) { + case Section.General: + handleGeneral(storyboard, line); + return; + case Section.Events: handleEvents(line); return; @@ -60,6 +64,18 @@ namespace osu.Game.Beatmaps.Formats base.ParseLine(storyboard, section, line); } + private void handleGeneral(Storyboard storyboard, string line) + { + var pair = SplitKeyVal(line); + + switch (pair.Key) + { + case "UseSkinSprites": + storyboard.UseSkinSprites = pair.Value == "1"; + break; + } + } + private void handleEvents(string line) { var depth = 0; diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index b0fb583d62..daafdf015d 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -15,6 +15,11 @@ namespace osu.Game.Storyboards public BeatmapInfo BeatmapInfo = new BeatmapInfo(); + /// + /// Whether the storyboard can fall back to skin sprites in case no matching storyboard sprites are found. + /// + public bool UseSkinSprites { get; set; } + public bool HasDrawable => Layers.Any(l => l.Elements.Any(e => e.IsDrawable)); public double FirstEventTime => Layers.Min(l => l.Elements.FirstOrDefault()?.StartTime ?? 0); From 58a54c5b6c71c01315dd5effeb33f08a61449db0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 19 Oct 2020 23:40:20 +0200 Subject: [PATCH 4066/6909] Utilise UseSkinSprites value in storyboard sprite logic --- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 1 + .../Drawables/DrawableStoryboardAnimation.cs | 11 ++++------- .../Storyboards/Drawables/DrawableStoryboardSprite.cs | 11 ++++------- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index ec461fa095..4bc28e6cef 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -15,6 +15,7 @@ namespace osu.Game.Storyboards.Drawables { public class DrawableStoryboard : Container { + [Cached] public Storyboard Storyboard { get; } protected override Container Content { get; } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index c3b6dde44b..8382f91d1f 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -117,19 +117,16 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(IBindable beatmap, TextureStore textureStore) + private void load(IBindable beatmap, TextureStore textureStore, Storyboard storyboard) { for (var frame = 0; frame < Animation.FrameCount; frame++) { var framePath = Animation.Path.Replace(".", frame + "."); var storyboardPath = beatmap.Value.BeatmapSetInfo.Files.Find(f => f.Filename.Equals(framePath, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - var frameSprite = storyboardPath != null - ? (Drawable)new Sprite - { - Texture = textureStore.Get(storyboardPath) - } - : new SkinnableSprite(framePath); // fall back to skin textures if not found in storyboard files. + var frameSprite = storyboard.UseSkinSprites && storyboardPath == null + ? (Drawable)new SkinnableSprite(framePath) + : new Sprite { Texture = textureStore.Get(storyboardPath) }; AddFrame(frameSprite, Animation.FrameDelay); } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 95774de898..9599375c76 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -116,15 +116,12 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(IBindable beatmap, TextureStore textureStore) + private void load(IBindable beatmap, TextureStore textureStore, Storyboard storyboard) { var storyboardPath = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - var sprite = storyboardPath != null - ? (Drawable)new Sprite - { - Texture = textureStore.Get(storyboardPath) - } - : new SkinnableSprite(Sprite.Path); // fall back to skin textures if not found in storyboard files. + var sprite = storyboard.UseSkinSprites && storyboardPath == null + ? (Drawable)new SkinnableSprite(Sprite.Path) + : new Sprite { Texture = textureStore.Get(storyboardPath) }; InternalChild = sprite.With(s => s.Anchor = s.Origin = Anchor.Centre); From 8c14c9e1c4eb36789b150d14e273e6b6ccf1f772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 22:42:47 +0200 Subject: [PATCH 4067/6909] Add basic test coverage --- .../TestSceneDrawableStoryboardSprite.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs new file mode 100644 index 0000000000..9501026edc --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableStoryboardSprite.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Skinning; +using osu.Game.Storyboards; +using osu.Game.Storyboards.Drawables; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneDrawableStoryboardSprite : SkinnableTestScene + { + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); + + [Cached] + private Storyboard storyboard { get; set; } = new Storyboard(); + + [Test] + public void TestSkinSpriteDisallowedByDefault() + { + const string lookup_name = "hitcircleoverlay"; + + AddStep("allow skin lookup", () => storyboard.UseSkinSprites = false); + + AddStep("create sprites", () => SetContents( + () => createSprite(lookup_name, Anchor.TopLeft, Vector2.Zero))); + + assertSpritesFromSkin(false); + } + + [Test] + public void TestAllowLookupFromSkin() + { + const string lookup_name = "hitcircleoverlay"; + + AddStep("allow skin lookup", () => storyboard.UseSkinSprites = true); + + AddStep("create sprites", () => SetContents( + () => createSprite(lookup_name, Anchor.Centre, Vector2.Zero))); + + assertSpritesFromSkin(true); + } + + private DrawableStoryboardSprite createSprite(string lookupName, Anchor origin, Vector2 initialPosition) + => new DrawableStoryboardSprite( + new StoryboardSprite(lookupName, origin, initialPosition) + ).With(s => + { + s.LifetimeStart = double.MinValue; + s.LifetimeEnd = double.MaxValue; + }); + + private void assertSpritesFromSkin(bool fromSkin) => + AddAssert($"sprites are {(fromSkin ? "from skin" : "from storyboard")}", + () => this.ChildrenOfType() + .All(sprite => sprite.ChildrenOfType().Any() == fromSkin)); + } +} From 22112e4303e8f017730a8c87f8ccb03f8e07c537 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Oct 2020 23:09:39 +0900 Subject: [PATCH 4068/6909] Fix ISourcedFromTouch events being blocked by LoadingLayer --- osu.Game/Graphics/UserInterface/LoadingLayer.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index 35b33c3d03..200edf84a6 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -44,6 +44,11 @@ namespace osu.Game.Graphics.UserInterface // blocking scroll can cause weird behaviour when this layer is used within a ScrollContainer. case ScrollEvent _: return false; + + // blocking touch events causes the ISourcedFromTouch versions to not be fired, potentially impeding behaviour of drawables *above* the loading layer that may utilise these. + // note that this will not work well if touch handling elements are beneath the this loading layer (something to consider for the future). + case TouchEvent _: + return false; } return true; From 58c9e57a685d3cee372857f863aab575fd1cbf71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 21 Oct 2020 17:17:23 +0200 Subject: [PATCH 4069/6909] Fix comment --- osu.Game/Graphics/UserInterface/LoadingLayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index 200edf84a6..c8c4424bee 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Graphics.UserInterface return false; // blocking touch events causes the ISourcedFromTouch versions to not be fired, potentially impeding behaviour of drawables *above* the loading layer that may utilise these. - // note that this will not work well if touch handling elements are beneath the this loading layer (something to consider for the future). + // note that this will not work well if touch handling elements are beneath this loading layer (something to consider for the future). case TouchEvent _: return false; } From 670775cecbe8d642a229b22d5854f0c2f519383d Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 21 Oct 2020 18:57:48 +0200 Subject: [PATCH 4070/6909] Make beatmap wedge difficulty indicator color update dynamically. --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 26 +++++++++++++++------ 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 2a3eb8c67a..6085e266d7 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -39,6 +39,11 @@ namespace osu.Game.Screens.Select private readonly IBindable ruleset = new Bindable(); + [Resolved] + private BeatmapDifficultyManager difficultyManager { get; set; } + + private IBindable beatmapDifficulty; + protected BufferedWedgeInfo Info; public BeatmapInfoWedge() @@ -88,6 +93,11 @@ namespace osu.Game.Screens.Select if (beatmap == value) return; beatmap = value; + + beatmapDifficulty?.UnbindAll(); + beatmapDifficulty = difficultyManager.GetBindableDifficulty(beatmap.BeatmapInfo); + beatmapDifficulty.BindValueChanged(_ => updateDisplay()); + updateDisplay(); } } @@ -113,7 +123,7 @@ namespace osu.Game.Screens.Select return; } - LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value) + LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value) { Shear = -Shear, Depth = Info?.Depth + 1 ?? 0 @@ -141,12 +151,14 @@ namespace osu.Game.Screens.Select private readonly WorkingBeatmap beatmap; private readonly RulesetInfo ruleset; + private readonly StarDifficulty starDifficulty; - public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset) + public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, StarDifficulty difficulty) : base(pixelSnapping: true) { this.beatmap = beatmap; ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; + starDifficulty = difficulty; } [BackgroundDependencyLoader] @@ -190,7 +202,7 @@ namespace osu.Game.Screens.Select }, }, }, - new DifficultyColourBar(beatmapInfo) + new DifficultyColourBar(starDifficulty) { RelativeSizeAxes = Axes.Y, Width = 20, @@ -447,11 +459,11 @@ namespace osu.Game.Screens.Select private class DifficultyColourBar : Container { - private readonly BeatmapInfo beatmap; + private readonly StarDifficulty difficulty; - public DifficultyColourBar(BeatmapInfo beatmap) + public DifficultyColourBar(StarDifficulty difficulty) { - this.beatmap = beatmap; + this.difficulty = difficulty; } [BackgroundDependencyLoader] @@ -459,7 +471,7 @@ namespace osu.Game.Screens.Select { const float full_opacity_ratio = 0.7f; - var difficultyColour = colours.ForDifficultyRating(beatmap.DifficultyRating); + var difficultyColour = colours.ForDifficultyRating(difficulty.DifficultyRating); Children = new Drawable[] { From cf69eacae9164311f973d10eb60bebf96330456c Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 21 Oct 2020 19:05:14 +0200 Subject: [PATCH 4071/6909] Make StarRatingDisplay dynamic. --- .../Ranking/Expanded/StarRatingDisplay.cs | 25 +++++++++++++++---- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 6 ++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs index 4b38b298f1..402ab99908 100644 --- a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs @@ -24,6 +24,8 @@ namespace osu.Game.Screens.Ranking.Expanded { private readonly BeatmapInfo beatmap; + private StarDifficulty? difficulty; + /// /// Creates a new . /// @@ -31,20 +33,33 @@ namespace osu.Game.Screens.Ranking.Expanded public StarRatingDisplay(BeatmapInfo beatmap) { this.beatmap = beatmap; - AutoSizeAxes = Axes.Both; + } + + /// + /// Creates a new using an already computed . + /// + /// The already computed to display the star difficulty of. + public StarRatingDisplay(StarDifficulty starDifficulty) + { + difficulty = starDifficulty; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, BeatmapDifficultyManager difficultyManager) { - var starRatingParts = beatmap.StarDifficulty.ToString("0.00", CultureInfo.InvariantCulture).Split('.'); + AutoSizeAxes = Axes.Both; + + if (!difficulty.HasValue) + difficulty = difficultyManager.GetDifficulty(beatmap); + + var starRatingParts = difficulty.Value.Stars.ToString("0.00", CultureInfo.InvariantCulture).Split('.'); string wholePart = starRatingParts[0]; string fractionPart = starRatingParts[1]; string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; - ColourInfo backgroundColour = beatmap.DifficultyRating == DifficultyRating.ExpertPlus + ColourInfo backgroundColour = difficulty.Value.DifficultyRating == DifficultyRating.ExpertPlus ? ColourInfo.GradientVertical(Color4Extensions.FromHex("#C1C1C1"), Color4Extensions.FromHex("#595959")) - : (ColourInfo)colours.ForDifficultyRating(beatmap.DifficultyRating); + : (ColourInfo)colours.ForDifficultyRating(difficulty.Value.DifficultyRating); InternalChildren = new Drawable[] { diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 6085e266d7..bdfcc2fd96 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -238,7 +238,7 @@ namespace osu.Game.Screens.Select Shear = wedged_container_shear, Children = new[] { - createStarRatingDisplay(beatmapInfo).With(display => + createStarRatingDisplay(starDifficulty).With(display => { display.Anchor = Anchor.TopRight; display.Origin = Anchor.TopRight; @@ -305,8 +305,8 @@ namespace osu.Game.Screens.Select StatusPill.Hide(); } - private static Drawable createStarRatingDisplay(BeatmapInfo beatmapInfo) => beatmapInfo.StarDifficulty > 0 - ? new StarRatingDisplay(beatmapInfo) + private static Drawable createStarRatingDisplay(StarDifficulty difficulty) => difficulty.Stars > 0 + ? new StarRatingDisplay(difficulty) { Margin = new MarginPadding { Bottom = 5 } } From 9753dab93bd1e71324be2a84cda27500877fb38e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 14:19:12 +0900 Subject: [PATCH 4072/6909] Remove IOnlineComponent and change existing components to use bindable flow --- .../Visual/Online/TestSceneFriendDisplay.cs | 11 +-- .../Online/TestSceneOnlineViewContainer.cs | 8 +- ...BeatmapManager_BeatmapOnlineLookupQueue.cs | 2 +- osu.Game/Online/API/APIAccess.cs | 73 +++++-------------- osu.Game/Online/API/DummyAPIAccess.cs | 40 +++------- osu.Game/Online/API/IAPIProvider.cs | 14 +--- osu.Game/Online/API/IOnlineComponent.cs | 10 --- osu.Game/Online/Leaderboards/Leaderboard.cs | 20 +++-- osu.Game/Online/OnlineViewContainer.cs | 28 ++++--- osu.Game/Overlays/AccountCreationOverlay.cs | 14 ++-- osu.Game/Overlays/DashboardOverlay.cs | 13 +++- osu.Game/Overlays/FullscreenOverlay.cs | 18 +---- osu.Game/Overlays/OverlayView.cs | 19 +++-- .../Sections/General/LoginSettings.cs | 21 ++++-- .../Overlays/Toolbar/ToolbarUserButton.cs | 23 ++++-- osu.Game/Screens/Multi/Multiplayer.cs | 25 +++---- .../Screens/Select/DifficultyRecommender.cs | 20 ++--- 17 files changed, 140 insertions(+), 219 deletions(-) delete mode 100644 osu.Game/Online/API/IOnlineComponent.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 72033fc121..8280300caa 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOffline() { - AddStep("Populate", () => display.Users = getUsers()); + AddStep("Populate with offline test users", () => display.Users = getUsers()); } [Test] @@ -80,14 +80,7 @@ namespace osu.Game.Tests.Visual.Online private class TestFriendDisplay : FriendDisplay { - public void Fetch() - { - base.APIStateChanged(API, APIState.Online); - } - - public override void APIStateChanged(IAPIProvider api, APIState state) - { - } + public void Fetch() => PerformFetch(); } } } diff --git a/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs index 9591d53b24..ec183adbbc 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineViewContainer.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOnlineStateVisibility() { - AddStep("set status to online", () => ((DummyAPIAccess)API).State = APIState.Online); + AddStep("set status to online", () => ((DummyAPIAccess)API).SetState(APIState.Online)); AddUntilStep("children are visible", () => onlineView.ViewTarget.IsPresent); AddUntilStep("loading animation is not visible", () => !onlineView.LoadingSpinner.IsPresent); @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOfflineStateVisibility() { - AddStep("set status to offline", () => ((DummyAPIAccess)API).State = APIState.Offline); + AddStep("set status to offline", () => ((DummyAPIAccess)API).SetState(APIState.Offline)); AddUntilStep("children are not visible", () => !onlineView.ViewTarget.IsPresent); AddUntilStep("loading animation is not visible", () => !onlineView.LoadingSpinner.IsPresent); @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestConnectingStateVisibility() { - AddStep("set status to connecting", () => ((DummyAPIAccess)API).State = APIState.Connecting); + AddStep("set status to connecting", () => ((DummyAPIAccess)API).SetState(APIState.Connecting)); AddUntilStep("children are not visible", () => !onlineView.ViewTarget.IsPresent); AddUntilStep("loading animation is visible", () => onlineView.LoadingSpinner.IsPresent); @@ -59,7 +59,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestFailingStateVisibility() { - AddStep("set status to failing", () => ((DummyAPIAccess)API).State = APIState.Failing); + AddStep("set status to failing", () => ((DummyAPIAccess)API).SetState(APIState.Failing)); AddUntilStep("children are not visible", () => !onlineView.ViewTarget.IsPresent); AddUntilStep("loading animation is visible", () => onlineView.LoadingSpinner.IsPresent); diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index cb4884aa51..c4563d5844 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -63,7 +63,7 @@ namespace osu.Game.Beatmaps if (checkLocalCache(set, beatmap)) return; - if (api?.State != APIState.Online) + if (api?.State.Value != APIState.Online) return; var req = new GetBeatmapRequest(beatmap); diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 4ea5c192fe..b916339a53 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -78,26 +78,8 @@ namespace osu.Game.Online.API private void onTokenChanged(ValueChangedEvent e) => config.Set(OsuSetting.Token, config.Get(OsuSetting.SavePassword) ? authentication.TokenString : string.Empty); - private readonly List components = new List(); - internal new void Schedule(Action action) => base.Schedule(action); - /// - /// Register a component to receive API events. - /// Fires once immediately to ensure a correct state. - /// - /// - public void Register(IOnlineComponent component) - { - Schedule(() => components.Add(component)); - component.APIStateChanged(this, state); - } - - public void Unregister(IOnlineComponent component) - { - Schedule(() => components.Remove(component)); - } - public string AccessToken => authentication.RequestAccessToken(); /// @@ -109,7 +91,7 @@ namespace osu.Game.Online.API { while (!cancellationToken.IsCancellationRequested) { - switch (State) + switch (State.Value) { case APIState.Failing: //todo: replace this with a ping request. @@ -131,12 +113,12 @@ namespace osu.Game.Online.API // work to restore a connection... if (!HasLogin) { - State = APIState.Offline; + state.Value = APIState.Offline; Thread.Sleep(50); continue; } - State = APIState.Connecting; + state.Value = APIState.Connecting; // save the username at this point, if the user requested for it to be. config.Set(OsuSetting.Username, config.Get(OsuSetting.SaveUsername) ? ProvidedUsername : string.Empty); @@ -162,20 +144,20 @@ namespace osu.Game.Online.API failureCount = 0; //we're connected! - State = APIState.Online; + state.Value = APIState.Online; }; if (!handleRequest(userReq)) { - if (State == APIState.Connecting) - State = APIState.Failing; + if (State.Value == APIState.Connecting) + state.Value = APIState.Failing; continue; } // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests // before actually going online. - while (State > APIState.Offline && State < APIState.Online) + while (State.Value > APIState.Offline && State.Value < APIState.Online) Thread.Sleep(500); break; @@ -224,7 +206,7 @@ namespace osu.Game.Online.API public void Login(string username, string password) { - Debug.Assert(State == APIState.Offline); + Debug.Assert(State.Value == APIState.Offline); ProvidedUsername = username; this.password = password; @@ -232,7 +214,7 @@ namespace osu.Game.Online.API public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { - Debug.Assert(State == APIState.Offline); + Debug.Assert(State.Value == APIState.Offline); var req = new RegistrationRequest { @@ -276,7 +258,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. - if (IsLoggedIn) State = APIState.Online; + if (IsLoggedIn) state.Value = APIState.Online; failureCount = 0; return true; @@ -293,27 +275,12 @@ namespace osu.Game.Online.API } } - private APIState state; + private readonly Bindable state = new Bindable(); - public APIState State - { - get => state; - private set - { - if (state == value) - return; - - APIState oldState = state; - state = value; - - log.Add($@"We just went {state}!"); - Schedule(() => - { - components.ForEach(c => c.APIStateChanged(this, state)); - OnStateChange?.Invoke(oldState, state); - }); - } - } + /// + /// The current connectivity state of the API. + /// + public IBindable State => state; private bool handleWebException(WebException we) { @@ -343,9 +310,9 @@ namespace osu.Game.Online.API // we might try again at an api level. return false; - if (State == APIState.Online) + if (State.Value == APIState.Online) { - State = APIState.Failing; + state.Value = APIState.Failing; flushQueue(); } @@ -362,10 +329,6 @@ namespace osu.Game.Online.API lock (queue) queue.Enqueue(request); } - public event StateChangeDelegate OnStateChange; - - public delegate void StateChangeDelegate(APIState oldState, APIState newState); - private void flushQueue(bool failOldRequests = true) { lock (queue) @@ -392,7 +355,7 @@ namespace osu.Game.Online.API // Scheduled prior to state change such that the state changed event is invoked with the correct user present Schedule(() => LocalUser.Value = createGuestUser()); - State = APIState.Offline; + state.Value = APIState.Offline; } private static User createGuestUser() => new GuestUser(); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 7800241904..1672d0495d 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -21,34 +21,23 @@ namespace osu.Game.Online.API public Bindable Activity { get; } = new Bindable(); - public bool IsLoggedIn => State == APIState.Online; + public bool IsLoggedIn => State.Value == APIState.Online; public string ProvidedUsername => LocalUser.Value.Username; public string Endpoint => "http://localhost"; - private APIState state = APIState.Online; - - private readonly List components = new List(); - /// /// Provide handling logic for an arbitrary API request. /// public Action HandleRequest; - public APIState State - { - get => state; - set - { - if (state == value) - return; + private readonly Bindable state = new Bindable(APIState.Online); - state = value; - - Scheduler.Add(() => components.ForEach(c => c.APIStateChanged(this, value))); - } - } + /// + /// The current connectivity state of the API. + /// + public IBindable State => state; public DummyAPIAccess() { @@ -72,17 +61,6 @@ namespace osu.Game.Online.API return Task.CompletedTask; } - public void Register(IOnlineComponent component) - { - Scheduler.Add(delegate { components.Add(component); }); - component.APIStateChanged(this, state); - } - - public void Unregister(IOnlineComponent component) - { - Scheduler.Add(delegate { components.Remove(component); }); - } - public void Login(string username, string password) { LocalUser.Value = new User @@ -91,13 +69,13 @@ namespace osu.Game.Online.API Id = 1001, }; - State = APIState.Online; + state.Value = APIState.Online; } public void Logout() { LocalUser.Value = new GuestUser(); - State = APIState.Offline; + state.Value = APIState.Offline; } public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) @@ -105,5 +83,7 @@ namespace osu.Game.Online.API Thread.Sleep(200); return null; } + + public void SetState(APIState newState) => state.Value = newState; } } diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index dff6d0b2ce..256d2ed151 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -35,7 +35,7 @@ namespace osu.Game.Online.API /// string Endpoint { get; } - APIState State { get; } + IBindable State { get; } /// /// Queue a new request. @@ -61,18 +61,6 @@ namespace osu.Game.Online.API /// The request to perform. Task PerformAsync(APIRequest request); - /// - /// Register a component to receive state changes. - /// - /// The component to register. - void Register(IOnlineComponent component); - - /// - /// Unregisters a component to receive state changes. - /// - /// The component to unregister. - void Unregister(IOnlineComponent component); - /// /// Attempt to login using the provided credentials. This is a non-blocking operation. /// diff --git a/osu.Game/Online/API/IOnlineComponent.cs b/osu.Game/Online/API/IOnlineComponent.cs deleted file mode 100644 index da6b784759..0000000000 --- a/osu.Game/Online/API/IOnlineComponent.cs +++ /dev/null @@ -1,10 +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.Online.API -{ - public interface IOnlineComponent - { - void APIStateChanged(IAPIProvider api, APIState state); - } -} diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 2acee394a6..3a5c2e181f 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -22,7 +23,7 @@ using osuTK.Graphics; namespace osu.Game.Online.Leaderboards { - public abstract class Leaderboard : Container, IOnlineComponent + public abstract class Leaderboard : Container { private const double fade_duration = 300; @@ -242,16 +243,13 @@ namespace osu.Game.Online.Leaderboards private ScheduledDelegate pendingUpdateScores; + private readonly IBindable apiState = new Bindable(); + [BackgroundDependencyLoader] private void load() { - api?.Register(this); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - api?.Unregister(this); + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); } public void RefreshScores() => UpdateScores(); @@ -260,9 +258,9 @@ namespace osu.Game.Online.Leaderboards protected abstract bool IsOnlineScope { get; } - public void APIStateChanged(IAPIProvider api, APIState state) + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { - switch (state) + switch (state.NewValue) { case APIState.Online: case APIState.Offline: @@ -271,7 +269,7 @@ namespace osu.Game.Online.Leaderboards break; } - } + }); protected void UpdateScores() { diff --git a/osu.Game/Online/OnlineViewContainer.cs b/osu.Game/Online/OnlineViewContainer.cs index b52e3d9e3c..295d079d29 100644 --- a/osu.Game/Online/OnlineViewContainer.cs +++ b/osu.Game/Online/OnlineViewContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -14,7 +15,7 @@ namespace osu.Game.Online /// A for displaying online content which require a local user to be logged in. /// Shows its children only when the local user is logged in and supports displaying a placeholder if not. /// - public abstract class OnlineViewContainer : Container, IOnlineComponent + public abstract class OnlineViewContainer : Container { protected LoadingSpinner LoadingSpinner { get; private set; } @@ -34,8 +35,10 @@ namespace osu.Game.Online this.placeholderMessage = placeholderMessage; } + private readonly IBindable apiState = new Bindable(); + [BackgroundDependencyLoader] - private void load() + private void load(IAPIProvider api) { InternalChildren = new Drawable[] { @@ -46,18 +49,19 @@ namespace osu.Game.Online Alpha = 0, } }; + + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - - API.Register(this); } - public virtual void APIStateChanged(IAPIProvider api, APIState state) + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { - switch (state) + switch (state.NewValue) { case APIState.Offline: PopContentOut(Content); @@ -79,7 +83,7 @@ namespace osu.Game.Online placeholder.FadeOut(transform_duration / 2, Easing.OutQuint); break; } - } + }); /// /// Applies a transform to the online content to make it hidden. @@ -90,11 +94,5 @@ namespace osu.Game.Online /// Applies a transform to the online content to make it visible. /// protected virtual void PopContentIn(Drawable content) => content.FadeIn(transform_duration, Easing.OutQuint); - - protected override void Dispose(bool isDisposing) - { - API?.Unregister(this); - base.Dispose(isDisposing); - } } } diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs index 89d8cbde11..58ede5502a 100644 --- a/osu.Game/Overlays/AccountCreationOverlay.cs +++ b/osu.Game/Overlays/AccountCreationOverlay.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.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -17,7 +18,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public class AccountCreationOverlay : OsuFocusedOverlayContainer, IOnlineComponent + public class AccountCreationOverlay : OsuFocusedOverlayContainer { private const float transition_time = 400; @@ -30,10 +31,13 @@ namespace osu.Game.Overlays Origin = Anchor.Centre; } + private readonly IBindable apiState = new Bindable(); + [BackgroundDependencyLoader] private void load(OsuColour colours, IAPIProvider api) { - api.Register(this); + apiState.BindTo(api.State); + apiState.BindValueChanged(apiStateChanged, true); Children = new Drawable[] { @@ -97,9 +101,9 @@ namespace osu.Game.Overlays this.FadeOut(100); } - public void APIStateChanged(IAPIProvider api, APIState state) + private void apiStateChanged(ValueChangedEvent state) => Schedule(() => { - switch (state) + switch (state.NewValue) { case APIState.Offline: case APIState.Failing: @@ -112,6 +116,6 @@ namespace osu.Game.Overlays Hide(); break; } - } + }); } } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 8135b83a03..6c58ed50fb 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -33,6 +33,15 @@ namespace osu.Game.Overlays { } + private readonly IBindable apiState = new Bindable(); + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); + } + [BackgroundDependencyLoader] private void load() { @@ -130,13 +139,13 @@ namespace osu.Game.Overlays } } - public override void APIStateChanged(IAPIProvider api, APIState state) + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { if (State.Value == Visibility.Hidden) return; Header.Current.TriggerChange(); - } + }); protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index bd6b07c65f..6f56d95929 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -12,7 +12,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public abstract class FullscreenOverlay : WaveOverlayContainer, IOnlineComponent, INamedOverlayComponent + public abstract class FullscreenOverlay : WaveOverlayContainer, INamedOverlayComponent where T : OverlayHeader { public virtual string IconTexture => Header?.Title.IconTexture ?? string.Empty; @@ -86,21 +86,5 @@ namespace osu.Game.Overlays protected virtual void PopOutComplete() { } - - protected override void LoadComplete() - { - base.LoadComplete(); - API.Register(this); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - API?.Unregister(this); - } - - public virtual void APIStateChanged(IAPIProvider api, APIState state) - { - } } } diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index 312271316a..c254cdf290 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.API; @@ -15,7 +16,7 @@ namespace osu.Game.Overlays /// Automatically performs a data fetch on load. /// /// The type of the API response. - public abstract class OverlayView : CompositeDrawable, IOnlineComponent + public abstract class OverlayView : CompositeDrawable where T : class { [Resolved] @@ -29,10 +30,13 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.Y; } - protected override void LoadComplete() + private readonly IBindable apiState = new Bindable(); + + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - API.Register(this); + apiState.BindTo(API.State); + apiState.BindValueChanged(onlineStateChanged, true); } /// @@ -59,20 +63,19 @@ namespace osu.Game.Overlays API.Queue(request); } - public virtual void APIStateChanged(IAPIProvider api, APIState state) + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { - switch (state) + switch (state.NewValue) { case APIState.Online: PerformFetch(); break; } - } + }); protected override void Dispose(bool isDisposing) { request?.Cancel(); - API?.Unregister(this); base.Dispose(isDisposing); } } diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 9e358d0cf5..873272bf12 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -13,6 +13,7 @@ using osu.Game.Online.API; using osuTK; using osu.Game.Users; using System.ComponentModel; +using osu.Framework.Bindables; using osu.Game.Graphics; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; @@ -25,7 +26,7 @@ using Container = osu.Framework.Graphics.Containers.Container; namespace osu.Game.Overlays.Settings.Sections.General { - public class LoginSettings : FillFlowContainer, IOnlineComponent + public class LoginSettings : FillFlowContainer { private bool bounding = true; private LoginForm form; @@ -41,6 +42,11 @@ namespace osu.Game.Overlays.Settings.Sections.General /// public Action RequestHide; + private readonly IBindable apiState = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } + public override RectangleF BoundingBox => bounding ? base.BoundingBox : RectangleF.Empty; public bool Bounding @@ -61,17 +67,18 @@ namespace osu.Game.Overlays.Settings.Sections.General Spacing = new Vector2(0f, 5f); } - [BackgroundDependencyLoader(permitNulls: true)] - private void load(IAPIProvider api) + [BackgroundDependencyLoader] + private void load() { - api?.Register(this); + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); } - public void APIStateChanged(IAPIProvider api, APIState state) => Schedule(() => + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { form = null; - switch (state) + switch (state.NewValue) { case APIState.Offline: Children = new Drawable[] @@ -107,7 +114,7 @@ namespace osu.Game.Overlays.Settings.Sections.General Origin = Anchor.TopCentre, TextAnchor = Anchor.TopCentre, AutoSizeAxes = Axes.Both, - Text = state == APIState.Failing ? "Connection is failing, will attempt to reconnect... " : "Attempting to connect... ", + Text = state.NewValue == APIState.Failing ? "Connection is failing, will attempt to reconnect... " : "Attempting to connect... ", Margin = new MarginPadding { Top = 10, Bottom = 10 }, }, }; diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index bccef3d9fe..b21bc49a11 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.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.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Effects; @@ -14,10 +15,15 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Toolbar { - public class ToolbarUserButton : ToolbarOverlayToggleButton, IOnlineComponent + public class ToolbarUserButton : ToolbarOverlayToggleButton { private readonly UpdateableAvatar avatar; + [Resolved] + private IAPIProvider api { get; set; } + + private readonly IBindable apiState = new Bindable(); + public ToolbarUserButton() { AutoSizeAxes = Axes.X; @@ -43,17 +49,22 @@ namespace osu.Game.Overlays.Toolbar }); } - [BackgroundDependencyLoader(true)] - private void load(IAPIProvider api, LoginOverlay login) + [BackgroundDependencyLoader(permitNulls: true)] + private void load() { - api.Register(this); + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); + } + [BackgroundDependencyLoader(true)] + private void load(LoginOverlay login) + { StateContainer = login; } - public void APIStateChanged(IAPIProvider api, APIState state) + private void onlineStateChanged(ValueChangedEvent state) { - switch (state) + switch (state.NewValue) { default: Text = @"Guest"; diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 27f774e9ec..e6abde4d43 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -29,7 +29,7 @@ using osuTK; namespace osu.Game.Screens.Multi { [Cached] - public class Multiplayer : OsuScreen, IOnlineComponent + public class Multiplayer : OsuScreen { public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true; @@ -146,15 +146,24 @@ namespace osu.Game.Screens.Multi screenStack.ScreenExited += screenExited; } + private readonly IBindable apiState = new Bindable(); + [BackgroundDependencyLoader(true)] private void load(IdleTracker idleTracker) { - api.Register(this); + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); if (idleTracker != null) isIdle.BindTo(idleTracker.IsIdle); } + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => + { + if (state.NewValue != APIState.Online) + Schedule(forcefullyExit); + }); + protected override void LoadComplete() { base.LoadComplete(); @@ -199,12 +208,6 @@ namespace osu.Game.Screens.Multi Logger.Log($"Polling adjusted (listing: {roomManager.TimeBetweenListingPolls}, selection: {roomManager.TimeBetweenSelectionPolls})"); } - public void APIStateChanged(IAPIProvider api, APIState state) - { - if (state != APIState.Online) - Schedule(forcefullyExit); - } - private void forcefullyExit() { // This is temporary since we don't currently have a way to force screens to be exited @@ -371,12 +374,6 @@ namespace osu.Game.Screens.Multi } } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - api?.Unregister(this); - } - private class MultiplayerWaveContainer : WaveContainer { protected override bool StartHidden => true; diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 0dd3341a93..ff54e0a8df 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, IOnlineComponent + public class DifficultyRecommender : Component { [Resolved] private IAPIProvider api { get; set; } @@ -28,10 +28,13 @@ namespace osu.Game.Screens.Select private readonly Dictionary recommendedStarDifficulty = new Dictionary(); + private readonly IBindable apiState = new Bindable(); + [BackgroundDependencyLoader] private void load() { - api.Register(this); + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); } /// @@ -72,21 +75,14 @@ namespace osu.Game.Screens.Select }); } - public void APIStateChanged(IAPIProvider api, APIState state) + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { - switch (state) + switch (state.NewValue) { case APIState.Online: calculateRecommendedDifficulties(); break; } - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - api?.Unregister(this); - } + }); } } From 303975ccb152823110feea9b5dd8ced8983dca72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 14:27:49 +0900 Subject: [PATCH 4073/6909] Remove unnecessary permitNulls --- osu.Game/Overlays/Toolbar/ToolbarUserButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index b21bc49a11..fc47eced77 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Toolbar }); } - [BackgroundDependencyLoader(permitNulls: true)] + [BackgroundDependencyLoader] private void load() { apiState.BindTo(api.State); From 3fe6f77444eae95d06373b93a5035764e012728f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 14:30:49 +0900 Subject: [PATCH 4074/6909] Fix cases of multiple bdl methods --- osu.Game/Online/OnlineViewContainer.cs | 5 ----- osu.Game/Overlays/DashboardOverlay.cs | 4 ---- osu.Game/Overlays/Toolbar/ToolbarUserButton.cs | 10 +++------- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/osu.Game/Online/OnlineViewContainer.cs b/osu.Game/Online/OnlineViewContainer.cs index 295d079d29..c9fb70f0cc 100644 --- a/osu.Game/Online/OnlineViewContainer.cs +++ b/osu.Game/Online/OnlineViewContainer.cs @@ -54,11 +54,6 @@ namespace osu.Game.Online apiState.BindValueChanged(onlineStateChanged, true); } - [BackgroundDependencyLoader] - private void load() - { - } - private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { switch (state.NewValue) diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 6c58ed50fb..a2490365e4 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -40,11 +40,7 @@ namespace osu.Game.Overlays { apiState.BindTo(api.State); apiState.BindValueChanged(onlineStateChanged, true); - } - [BackgroundDependencyLoader] - private void load() - { Children = new Drawable[] { new Box diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index fc47eced77..9417224049 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -49,16 +49,12 @@ namespace osu.Game.Overlays.Toolbar }); } - [BackgroundDependencyLoader] - private void load() - { - apiState.BindTo(api.State); - apiState.BindValueChanged(onlineStateChanged, true); - } - [BackgroundDependencyLoader(true)] private void load(LoginOverlay login) { + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); + StateContainer = login; } From da573c74870762dd15dcbec52c998e2a5a2567c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 14:43:47 +0900 Subject: [PATCH 4075/6909] Remove unused usings --- .../Visual/Online/TestSceneFriendDisplay.cs | 13 ++++++------- osu.Game/Online/API/DummyAPIAccess.cs | 1 - 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 8280300caa..0cc6e9f358 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -3,14 +3,13 @@ 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; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Overlays; +using osu.Game.Overlays.Dashboard.Friends; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Online { diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 1672d0495d..da22a70bf8 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; From b39a4da6bcec029855c513964a5d29fab3a35977 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 21 Oct 2020 19:05:20 +0900 Subject: [PATCH 4076/6909] Add initial classes for spectator support --- .../Gameplay/TestSceneReplayRecorder.cs | 20 +++++- osu.Game/Online/Spectator/FrameDataBundle.cs | 17 +++++ osu.Game/Online/Spectator/ISpectatorClient.cs | 14 ++++ osu.Game/Online/Spectator/ISpectatorServer.cs | 14 ++++ osu.Game/Online/Spectator/SpectatorClient.cs | 70 +++++++++++++++++++ osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 2 + osu.Game/osu.Game.csproj | 3 + 7 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Online/Spectator/FrameDataBundle.cs create mode 100644 osu.Game/Online/Spectator/ISpectatorClient.cs create mode 100644 osu.Game/Online/Spectator/ISpectatorServer.cs create mode 100644 osu.Game/Online/Spectator/SpectatorClient.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index bc1c10e59d..88a4024576 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -13,7 +15,9 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Spectator; using osu.Game.Replays; +using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; @@ -260,13 +264,27 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { + private readonly SpectatorClient client; + public TestReplayRecorder(Replay target) : base(target) { + var connection = new HubConnectionBuilder() + .WithUrl("http://localhost:5009/spectator") + .AddMessagePackProtocol() + // .ConfigureLogging(logging => { logging.AddConsole(); }) + .Build(); + + connection.StartAsync().Wait(); + + client = new SpectatorClient(connection); } protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) - => new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); + { + client.SendFrames(new FrameDataBundle(new[] { new LegacyReplayFrame(Time.Current, mousePosition.X, mousePosition.Y, ReplayButtonState.None) })); + return new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); + } } } } diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs new file mode 100644 index 0000000000..67f2688289 --- /dev/null +++ b/osu.Game/Online/Spectator/FrameDataBundle.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using osu.Game.Replays.Legacy; + +namespace osu.Game.Online.Spectator +{ + [Serializable] + public class FrameDataBundle + { + public IEnumerable Frames { get; set; } + + public FrameDataBundle(IEnumerable frames) + { + Frames = frames; + } + } +} diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs new file mode 100644 index 0000000000..4741d7409a --- /dev/null +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; +using osu.Game.Online.Spectator; + +namespace osu.Server.Spectator.Hubs +{ + public interface ISpectatorClient + { + Task UserBeganPlaying(string userId, int beatmapId); + + Task UserFinishedPlaying(string userId, int beatmapId); + + Task UserSentFrames(string userId, FrameDataBundle data); + } +} \ No newline at end of file diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs new file mode 100644 index 0000000000..1dcde30221 --- /dev/null +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace osu.Game.Online.Spectator +{ + public interface ISpectatorServer + { + Task BeginPlaySession(int beatmapId); + Task SendFrameData(FrameDataBundle data); + Task EndPlaySession(int beatmapId); + + Task StartWatchingUser(string userId); + Task EndWatchingUser(string userId); + } +} diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs new file mode 100644 index 0000000000..c1414f7914 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -0,0 +1,70 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using osu.Server.Spectator.Hubs; + +namespace osu.Game.Online.Spectator +{ + public class SpectatorClient : ISpectatorClient + { + private readonly HubConnection connection; + + private readonly List watchingUsers = new List(); + + public SpectatorClient(HubConnection connection) + { + this.connection = connection; + + // this is kind of SILLY + // https://github.com/dotnet/aspnetcore/issues/15198 + connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); + connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); + connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + } + + Task ISpectatorClient.UserBeganPlaying(string userId, int beatmapId) + { + if (connection.ConnectionId != userId) + { + if (watchingUsers.Contains(userId)) + { + Console.WriteLine($"{connection.ConnectionId} received began playing for already watched user {userId}"); + } + else + { + Console.WriteLine($"{connection.ConnectionId} requesting watch other user {userId}"); + WatchUser(userId); + watchingUsers.Add(userId); + } + } + else + { + Console.WriteLine($"{connection.ConnectionId} Received user playing event for self {beatmapId}"); + } + + return Task.CompletedTask; + } + + Task ISpectatorClient.UserFinishedPlaying(string userId, int beatmapId) + { + Console.WriteLine($"{connection.ConnectionId} Received user finished event {beatmapId}"); + return Task.CompletedTask; + } + + Task ISpectatorClient.UserSentFrames(string userId, FrameDataBundle data) + { + Console.WriteLine($"{connection.ConnectionId} Received frames from {userId}: {data.Frames.First().ToString()}"); + return Task.CompletedTask; + } + + public Task BeginPlaying(int beatmapId) => connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), beatmapId); + + public Task SendFrames(FrameDataBundle data) => connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); + + public Task EndPlaying(int beatmapId) => connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), beatmapId); + + private Task WatchUser(string userId) => connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); + } +} diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index c3cffa8699..656fd1814e 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.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 MessagePack; using osu.Game.Rulesets.Replays; using osuTK; @@ -8,6 +9,7 @@ namespace osu.Game.Replays.Legacy { public class LegacyReplayFrame : ReplayFrame { + [IgnoreMember] public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); public float? MouseX; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index de7bde824f..fd010fcc43 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,6 +21,9 @@ + + + From db4dd3182b2494f6aa641855a32928be9465c9d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 13:12:58 +0900 Subject: [PATCH 4077/6909] Add xmldoc to spectator interfaces --- osu.Game/Online/Spectator/ISpectatorClient.cs | 23 +++++++++++++-- osu.Game/Online/Spectator/ISpectatorServer.cs | 28 +++++++++++++++++++ osu.Game/Online/Spectator/SpectatorClient.cs | 1 - 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 4741d7409a..ed762ac1fe 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -1,14 +1,31 @@ using System.Threading.Tasks; -using osu.Game.Online.Spectator; -namespace osu.Server.Spectator.Hubs +namespace osu.Game.Online.Spectator { + /// + /// An interface defining a spectator client instance. + /// public interface ISpectatorClient { + /// + /// Signals that a user has begun a new play session. + /// + /// The user. + /// The beatmap the user is playing. Task UserBeganPlaying(string userId, int beatmapId); + /// + /// Signals that a user has finished a play session. + /// + /// The user. + /// The beatmap the user has finished playing. Task UserFinishedPlaying(string userId, int beatmapId); + /// + /// Called when new frames are available for a subscribed user's play session. + /// + /// The user. + /// The frame data. Task UserSentFrames(string userId, FrameDataBundle data); } -} \ No newline at end of file +} diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs index 1dcde30221..03ca37d524 100644 --- a/osu.Game/Online/Spectator/ISpectatorServer.cs +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -2,13 +2,41 @@ using System.Threading.Tasks; namespace osu.Game.Online.Spectator { + /// + /// An interface defining the spectator server instance. + /// public interface ISpectatorServer { + /// + /// Signal the start of a new play session. + /// + /// The beatmap currently being played. Eventually this should be replaced with more complete metadata. Task BeginPlaySession(int beatmapId); + + /// + /// Send a bundle of frame data for the current play session. + /// + /// The frame data. Task SendFrameData(FrameDataBundle data); + + /// + /// Signal the end of a play session. + /// + /// The beatmap that was completed. This should be replaced with a play token once that flow is established. Task EndPlaySession(int beatmapId); + /// + /// Request spectating data for the specified user. May be called on multiple users and offline users. + /// For offline users, a subscription will be created and data will begin streaming on next play. + /// + /// The user to subscribe to. + /// Task StartWatchingUser(string userId); + + /// + /// Stop requesting spectating data for the specified user. Unsubscribes from receiving further data. + /// + /// The user to unsubscribe from. Task EndWatchingUser(string userId); } } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorClient.cs index c1414f7914..4558699618 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorClient.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; -using osu.Server.Spectator.Hubs; namespace osu.Game.Online.Spectator { From 5a00a05a95d3c7c1dba85a8beb2e4758da321942 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 14:49:48 +0900 Subject: [PATCH 4078/6909] Add missing schedule call --- osu.Game/Overlays/Toolbar/ToolbarUserButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs index 9417224049..db4e491d9a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarUserButton.cs @@ -58,7 +58,7 @@ namespace osu.Game.Overlays.Toolbar StateContainer = login; } - private void onlineStateChanged(ValueChangedEvent state) + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { switch (state.NewValue) { @@ -72,6 +72,6 @@ namespace osu.Game.Overlays.Toolbar avatar.User = api.LocalUser.Value; break; } - } + }); } } From c6db832efa940ee40d238fc036ab67b2f51f2c76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 14:56:20 +0900 Subject: [PATCH 4079/6909] Add xmldoc notes about thread safety of api bindables --- osu.Game/Online/API/IAPIProvider.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 256d2ed151..fc675639bf 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -11,11 +11,13 @@ namespace osu.Game.Online.API { /// /// The local user. + /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// Bindable LocalUser { get; } /// /// The current user's activity. + /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// Bindable Activity { get; } @@ -35,6 +37,10 @@ namespace osu.Game.Online.API /// string Endpoint { get; } + /// + /// The current connection state of the API. + /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. + /// IBindable State { get; } /// From 93db75bd414e90bfd9eb655f3b022bdface0f3cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 13:41:54 +0900 Subject: [PATCH 4080/6909] Begin shaping the spectator streaming component --- .../Gameplay/TestSceneReplayRecorder.cs | 14 ----- ...rClient.cs => SpectatorStreamingClient.cs} | 60 ++++++++++++++++--- osu.Game/OsuGameBase.cs | 7 ++- 3 files changed, 58 insertions(+), 23 deletions(-) rename osu.Game/Online/Spectator/{SpectatorClient.cs => SpectatorStreamingClient.cs} (58%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 88a4024576..71cd39953c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -3,8 +3,6 @@ using System.Collections.Generic; using System.Linq; -using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.DependencyInjection; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -264,25 +262,13 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { - private readonly SpectatorClient client; - public TestReplayRecorder(Replay target) : base(target) { - var connection = new HubConnectionBuilder() - .WithUrl("http://localhost:5009/spectator") - .AddMessagePackProtocol() - // .ConfigureLogging(logging => { logging.AddConsole(); }) - .Build(); - - connection.StartAsync().Wait(); - - client = new SpectatorClient(connection); } protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) { - client.SendFrames(new FrameDataBundle(new[] { new LegacyReplayFrame(Time.Current, mousePosition.X, mousePosition.Y, ReplayButtonState.None) })); return new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); } } diff --git a/osu.Game/Online/Spectator/SpectatorClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs similarity index 58% rename from osu.Game/Online/Spectator/SpectatorClient.cs rename to osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 4558699618..a1a4a2774a 100644 --- a/osu.Game/Online/Spectator/SpectatorClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -3,24 +3,70 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Online.API; namespace osu.Game.Online.Spectator { - public class SpectatorClient : ISpectatorClient + public class SpectatorStreamingClient : Component, ISpectatorClient { - private readonly HubConnection connection; + private HubConnection connection; private readonly List watchingUsers = new List(); - public SpectatorClient(HubConnection connection) - { - this.connection = connection; + private readonly IBindable apiState = new Bindable(); - // this is kind of SILLY - // https://github.com/dotnet/aspnetcore/issues/15198 + [Resolved] + private APIAccess api { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + apiState.BindTo(api.State); + apiState.BindValueChanged(apiStateChanged, true); + } + + private void apiStateChanged(ValueChangedEvent state) + { + switch (state.NewValue) + { + case APIState.Failing: + case APIState.Offline: + connection?.StopAsync(); + connection = null; + break; + + case APIState.Online: + connect(); + break; + } + } + +#if DEBUG + private const string endpoint = "http://localhost:5009/spectator"; +#else + private const string endpoint = "https://spectator.ppy.sh/spectator"; +#endif + + private void connect() + { + connection = new HubConnectionBuilder() + .WithUrl(endpoint, options => + { + options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); + }) + .AddMessagePackProtocol() + .Build(); + + // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + + connection.StartAsync(); } Task ISpectatorClient.UserBeganPlaying(string userId, int beatmapId) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 2d609668af..9b43d18a88 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -30,6 +30,7 @@ using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; +using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Resources; using osu.Game.Rulesets; @@ -74,6 +75,8 @@ namespace osu.Game protected IAPIProvider API; + private SpectatorStreamingClient spectatorStreaming; + protected MenuCursorContainer MenuCursorContainer; protected MusicController MusicController; @@ -189,9 +192,9 @@ namespace osu.Game dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); dependencies.CacheAs(SkinManager); - API ??= new APIAccess(LocalConfig); + dependencies.CacheAs(API ??= new APIAccess(LocalConfig)); - dependencies.CacheAs(API); + dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); From 175fd512b00f26402023dcc21d36dfe9f0bddaee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 14:54:27 +0900 Subject: [PATCH 4081/6909] Send frames to streaming client from replay recorder --- .../Online/Spectator/SpectatorStreamingClient.cs | 13 +++++++++++++ osu.Game/OsuGameBase.cs | 3 +++ osu.Game/Rulesets/UI/ReplayRecorder.cs | 9 +++++++++ 3 files changed, 25 insertions(+) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index a1a4a2774a..c784eb09cd 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -7,7 +7,10 @@ using Microsoft.Extensions.DependencyInjection; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Types; namespace osu.Game.Online.Spectator { @@ -22,6 +25,9 @@ namespace osu.Game.Online.Spectator [Resolved] private APIAccess api { get; set; } + [Resolved] + private IBindable beatmap { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -111,5 +117,12 @@ namespace osu.Game.Online.Spectator public Task EndPlaying(int beatmapId) => connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), beatmapId); private Task WatchUser(string userId) => connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); + + public void HandleFrame(ReplayFrame frame) + { + if (frame is IConvertibleReplayFrame convertible) + // TODO: don't send a bundle for each individual frame + SendFrames(new FrameDataBundle(new[] { convertible.ToLegacy(beatmap.Value.Beatmap) })); + } } } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 9b43d18a88..7364cf04b0 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -250,8 +250,11 @@ namespace osu.Game FileStore.Cleanup(); + // add api components to hierarchy. if (API is APIAccess apiAccess) AddInternal(apiAccess); + AddInternal(spectatorStreaming); + AddInternal(RulesetConfigCache); MenuCursorContainer = new MenuCursorContainer { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index c977639584..3203d1afae 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -4,10 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets.Replays; using osuTK; @@ -60,6 +62,9 @@ namespace osu.Game.Rulesets.UI recordFrame(true); } + [Resolved(canBeNull: true)] + private SpectatorStreamingClient spectatorStreaming { get; set; } + private void recordFrame(bool important) { var last = target.Frames.LastOrDefault(); @@ -72,7 +77,11 @@ namespace osu.Game.Rulesets.UI var frame = HandleFrame(position, pressedActions, last); if (frame != null) + { target.Frames.Add(frame); + + spectatorStreaming?.HandleFrame(frame); + } } protected abstract ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame); From 4788b4a643ac8b1465d200053aa6609ac7c8a679 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 15:03:43 +0900 Subject: [PATCH 4082/6909] Expose oauth access token via api interface --- osu.Game/Online/API/DummyAPIAccess.cs | 2 ++ osu.Game/Online/API/IAPIProvider.cs | 5 +++++ osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 2 +- 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index da22a70bf8..e275676cea 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -20,6 +20,8 @@ namespace osu.Game.Online.API public Bindable Activity { get; } = new Bindable(); + public string AccessToken => "token"; + public bool IsLoggedIn => State.Value == APIState.Online; public string ProvidedUsername => LocalUser.Value.Username; diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 256d2ed151..9b7485decd 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -19,6 +19,11 @@ namespace osu.Game.Online.API /// Bindable Activity { get; } + /// + /// Retrieve the OAuth access token. + /// + public string AccessToken { get; } + /// /// Returns whether the local user is logged in. /// diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index c784eb09cd..a9a4987e69 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -23,7 +23,7 @@ namespace osu.Game.Online.Spectator private readonly IBindable apiState = new Bindable(); [Resolved] - private APIAccess api { get; set; } + private IAPIProvider api { get; set; } [Resolved] private IBindable beatmap { get; set; } From 96049c39c9f431f5eaa3709eb1332e83a9bb342b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 15:26:57 +0900 Subject: [PATCH 4083/6909] Add begin/end session logic --- osu.Game/Rulesets/UI/ReplayRecorder.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 3203d1afae..c90b20caeb 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -5,10 +5,12 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Game.Beatmaps; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets.Replays; @@ -27,6 +29,12 @@ namespace osu.Game.Rulesets.UI public int RecordFrameRate = 60; + [Resolved(canBeNull: true)] + private SpectatorStreamingClient spectatorStreaming { get; set; } + + [Resolved] + private IBindable beatmap { get; set; } + protected ReplayRecorder(Replay target) { this.target = target; @@ -41,6 +49,14 @@ namespace osu.Game.Rulesets.UI base.LoadComplete(); inputManager = GetContainingInputManager(); + + spectatorStreaming?.BeginPlaying(beatmap.Value.BeatmapInfo.ID); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + spectatorStreaming?.EndPlaying(beatmap.Value.BeatmapInfo.ID); } protected override bool OnMouseMove(MouseMoveEvent e) @@ -62,9 +78,6 @@ namespace osu.Game.Rulesets.UI recordFrame(true); } - [Resolved(canBeNull: true)] - private SpectatorStreamingClient spectatorStreaming { get; set; } - private void recordFrame(bool important) { var last = target.Frames.LastOrDefault(); From 2021945a8c87e4f8ec9a556705161f7e42b3c804 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 15:27:04 +0900 Subject: [PATCH 4084/6909] Add retry/error handling logic --- .../Spectator/SpectatorStreamingClient.cs | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index a9a4987e69..2a19a665fc 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -22,6 +22,8 @@ namespace osu.Game.Online.Spectator private readonly IBindable apiState = new Bindable(); + private bool isConnected; + [Resolved] private IAPIProvider api { get; set; } @@ -46,7 +48,7 @@ namespace osu.Game.Online.Spectator break; case APIState.Online: - connect(); + Task.Run(connect); break; } } @@ -57,8 +59,11 @@ namespace osu.Game.Online.Spectator private const string endpoint = "https://spectator.ppy.sh/spectator"; #endif - private void connect() + private async Task connect() { + if (connection != null) + return; + connection = new HubConnectionBuilder() .WithUrl(endpoint, options => { @@ -72,7 +77,33 @@ namespace osu.Game.Online.Spectator connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); - connection.StartAsync(); + connection.Closed += async ex => + { + isConnected = false; + if (ex != null) await tryUntilConnected(); + }; + + await tryUntilConnected(); + + async Task tryUntilConnected() + { + while (api.State.Value == APIState.Online) + { + try + { + // reconnect on any failure + await connection.StartAsync(); + + // success + isConnected = true; + break; + } + catch + { + await Task.Delay(5000); + } + } + } } Task ISpectatorClient.UserBeganPlaying(string userId, int beatmapId) @@ -106,17 +137,37 @@ namespace osu.Game.Online.Spectator Task ISpectatorClient.UserSentFrames(string userId, FrameDataBundle data) { - Console.WriteLine($"{connection.ConnectionId} Received frames from {userId}: {data.Frames.First().ToString()}"); + Console.WriteLine($"{connection.ConnectionId} Received frames from {userId}: {data.Frames.First()}"); return Task.CompletedTask; } - public Task BeginPlaying(int beatmapId) => connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), beatmapId); + public void BeginPlaying(int beatmapId) + { + if (!isConnected) return; - public Task SendFrames(FrameDataBundle data) => connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); + connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), beatmapId); + } - public Task EndPlaying(int beatmapId) => connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), beatmapId); + public void SendFrames(FrameDataBundle data) + { + if (!isConnected) return; - private Task WatchUser(string userId) => connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); + connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); + } + + public void EndPlaying(int beatmapId) + { + if (!isConnected) return; + + connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), beatmapId); + } + + public void WatchUser(string userId) + { + if (!isConnected) return; + + connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); + } public void HandleFrame(ReplayFrame frame) { From 05697dfe68a58d5a2e2c780a80bb562e31fcd923 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 17:29:38 +0900 Subject: [PATCH 4085/6909] Add spectator state object support --- osu.Game/Online/Spectator/ISpectatorClient.cs | 8 ++--- osu.Game/Online/Spectator/ISpectatorServer.cs | 9 +++--- osu.Game/Online/Spectator/SpectatorState.cs | 32 +++++++++++++++++++ .../Spectator/SpectatorStreamingClient.cs | 23 ++++++++----- osu.Game/Rulesets/UI/ReplayRecorder.cs | 4 +-- 5 files changed, 57 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Online/Spectator/SpectatorState.cs diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index ed762ac1fe..dcff6e6c1c 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -11,15 +11,15 @@ namespace osu.Game.Online.Spectator /// Signals that a user has begun a new play session. /// /// The user. - /// The beatmap the user is playing. - Task UserBeganPlaying(string userId, int beatmapId); + /// The state of gameplay. + Task UserBeganPlaying(string userId, SpectatorState state); /// /// Signals that a user has finished a play session. /// /// The user. - /// The beatmap the user has finished playing. - Task UserFinishedPlaying(string userId, int beatmapId); + /// The state of gameplay. + Task UserFinishedPlaying(string userId, SpectatorState state); /// /// Called when new frames are available for a subscribed user's play session. diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs index 03ca37d524..018fa6b66b 100644 --- a/osu.Game/Online/Spectator/ISpectatorServer.cs +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -10,8 +10,8 @@ namespace osu.Game.Online.Spectator /// /// Signal the start of a new play session. /// - /// The beatmap currently being played. Eventually this should be replaced with more complete metadata. - Task BeginPlaySession(int beatmapId); + /// The state of gameplay. + Task BeginPlaySession(SpectatorState state); /// /// Send a bundle of frame data for the current play session. @@ -22,15 +22,14 @@ namespace osu.Game.Online.Spectator /// /// Signal the end of a play session. /// - /// The beatmap that was completed. This should be replaced with a play token once that flow is established. - Task EndPlaySession(int beatmapId); + /// The state of gameplay. + Task EndPlaySession(SpectatorState state); /// /// Request spectating data for the specified user. May be called on multiple users and offline users. /// For offline users, a subscription will be created and data will begin streaming on next play. /// /// The user to subscribe to. - /// Task StartWatchingUser(string userId); /// diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs new file mode 100644 index 0000000000..90238bfc38 --- /dev/null +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Online.Spectator +{ + [Serializable] + public class SpectatorState : IEquatable + { + public int? BeatmapID { get; set; } + + [NotNull] + public IEnumerable Mods { get; set; } = Enumerable.Empty(); + + public SpectatorState(int? beatmapId = null, IEnumerable mods = null) + { + BeatmapID = beatmapId; + if (mods != null) + Mods = mods; + } + + public SpectatorState() + { + } + + public bool Equals(SpectatorState other) => this.BeatmapID == other?.BeatmapID && this.Mods.SequenceEqual(other?.Mods); + + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods.SelectMany(m => m.Acronym))}"; + } +} diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 2a19a665fc..d93de3a710 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -30,6 +31,11 @@ namespace osu.Game.Online.Spectator [Resolved] private IBindable beatmap { get; set; } + [Resolved] + private IBindable> mods { get; set; } + + private readonly SpectatorState currentState = new SpectatorState(); + [BackgroundDependencyLoader] private void load() { @@ -73,9 +79,9 @@ namespace osu.Game.Online.Spectator .Build(); // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) - connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); + connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); - connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.Closed += async ex => { @@ -106,7 +112,7 @@ namespace osu.Game.Online.Spectator } } - Task ISpectatorClient.UserBeganPlaying(string userId, int beatmapId) + Task ISpectatorClient.UserBeganPlaying(string userId, SpectatorState state) { if (connection.ConnectionId != userId) { @@ -123,15 +129,15 @@ namespace osu.Game.Online.Spectator } else { - Console.WriteLine($"{connection.ConnectionId} Received user playing event for self {beatmapId}"); + Console.WriteLine($"{connection.ConnectionId} Received user playing event for self {state}"); } return Task.CompletedTask; } - Task ISpectatorClient.UserFinishedPlaying(string userId, int beatmapId) + Task ISpectatorClient.UserFinishedPlaying(string userId, SpectatorState state) { - Console.WriteLine($"{connection.ConnectionId} Received user finished event {beatmapId}"); + Console.WriteLine($"{connection.ConnectionId} Received user finished event {state}"); return Task.CompletedTask; } @@ -155,11 +161,11 @@ namespace osu.Game.Online.Spectator connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); } - public void EndPlaying(int beatmapId) + public void EndPlaying() { if (!isConnected) return; - connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), beatmapId); + connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState); } public void WatchUser(string userId) @@ -171,6 +177,7 @@ namespace osu.Game.Online.Spectator public void HandleFrame(ReplayFrame frame) { + // ReSharper disable once SuspiciousTypeConversion.Global (implemented by rulesets) if (frame is IConvertibleReplayFrame convertible) // TODO: don't send a bundle for each individual frame SendFrames(new FrameDataBundle(new[] { convertible.ToLegacy(beatmap.Value.Beatmap) })); diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index c90b20caeb..a84b4f4ba8 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -50,13 +50,13 @@ namespace osu.Game.Rulesets.UI inputManager = GetContainingInputManager(); - spectatorStreaming?.BeginPlaying(beatmap.Value.BeatmapInfo.ID); + spectatorStreaming?.BeginPlaying(); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - spectatorStreaming?.EndPlaying(beatmap.Value.BeatmapInfo.ID); + spectatorStreaming?.EndPlaying(); } protected override bool OnMouseMove(MouseMoveEvent e) From 0611b30258e1c5c5dcb2c5b346c4658a3773f69a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 17:29:43 +0900 Subject: [PATCH 4086/6909] Drop webpack --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 11 ++++++++--- osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 2 -- osu.Game/osu.Game.csproj | 3 +-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index d93de3a710..a89cc82535 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -75,7 +76,7 @@ namespace osu.Game.Online.Spectator { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }) - .AddMessagePackProtocol() + .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }) .Build(); // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) @@ -147,11 +148,15 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } - public void BeginPlaying(int beatmapId) + public void BeginPlaying() { if (!isConnected) return; - connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), beatmapId); + // transfer state at point of beginning play + currentState.BeatmapID = beatmap.Value.BeatmapInfo.OnlineBeatmapID; + currentState.Mods = mods.Value.ToArray(); + + connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState); } public void SendFrames(FrameDataBundle data) diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index 656fd1814e..c3cffa8699 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.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 MessagePack; using osu.Game.Rulesets.Replays; using osuTK; @@ -9,7 +8,6 @@ namespace osu.Game.Replays.Legacy { public class LegacyReplayFrame : ReplayFrame { - [IgnoreMember] public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); public float? MouseX; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index fd010fcc43..ca588b89d9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,9 +21,8 @@ - - + From c834aa605103b95cd9e074b6bd55ac4861694181 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 17:38:16 +0900 Subject: [PATCH 4087/6909] Use APIMod for mod serialization --- osu.Game/Online/API/APIMod.cs | 8 ++++++++ osu.Game/Online/Spectator/SpectatorState.cs | 11 ++++------- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 46a8db31b7..780e5daa16 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -53,5 +53,13 @@ namespace osu.Game.Online.API } public bool Equals(IMod other) => Acronym == other?.Acronym; + + public override string ToString() + { + if (Settings.Count > 0) + return $"{Acronym} ({string.Join(',', Settings.Select(kvp => $"{kvp.Key}:{kvp.Value}"))})"; + + return $"{Acronym}"; + } } } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index 90238bfc38..3d9997f006 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using osu.Game.Online.API; using osu.Game.Rulesets.Mods; namespace osu.Game.Online.Spectator @@ -12,21 +13,17 @@ namespace osu.Game.Online.Spectator public int? BeatmapID { get; set; } [NotNull] - public IEnumerable Mods { get; set; } = Enumerable.Empty(); + public IEnumerable Mods { get; set; } = Enumerable.Empty(); - public SpectatorState(int? beatmapId = null, IEnumerable mods = null) + public SpectatorState(int? beatmapId = null, IEnumerable mods = null) { BeatmapID = beatmapId; if (mods != null) Mods = mods; } - public SpectatorState() - { - } - public bool Equals(SpectatorState other) => this.BeatmapID == other?.BeatmapID && this.Mods.SequenceEqual(other?.Mods); - public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods.SelectMany(m => m.Acronym))}"; + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)}"; } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index a89cc82535..21259bad5f 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -154,7 +154,7 @@ namespace osu.Game.Online.Spectator // transfer state at point of beginning play currentState.BeatmapID = beatmap.Value.BeatmapInfo.OnlineBeatmapID; - currentState.Mods = mods.Value.ToArray(); + currentState.Mods = mods.Value.Select(m => new APIMod(m)); connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState); } From 1ab6f41b3ba0127db8ae00609a821fbea36ce26a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 18:10:27 +0900 Subject: [PATCH 4088/6909] Add basic send and receive test --- .../Visual/Gameplay/TestSceneSpectator.cs | 264 ++++++++++++++++++ .../Spectator/SpectatorStreamingClient.cs | 3 + 2 files changed, 267 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs new file mode 100644 index 0000000000..665df5f9c7 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -0,0 +1,264 @@ +// 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; +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.Game.Beatmaps; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.Spectator; +using osu.Game.Replays; +using osu.Game.Replays.Legacy; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Rulesets.UI; +using osu.Game.Tests.Visual.UserInterface; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSpectator : OsuManualInputManagerTestScene + { + protected override bool UseOnlineAPI => true; + + private TestRulesetInputManager playbackManager; + private TestRulesetInputManager recordingManager; + + private Replay replay; + + private TestReplayRecorder recorder; + + [Resolved] + private SpectatorStreamingClient streamingClient { get; set; } + + [SetUp] + public void SetUp() => Schedule(() => + { + replay = new Replay(); + + streamingClient.OnNewFrames += frames => + { + foreach (var legacyFrame in frames.Frames) + { + var frame = new TestReplayFrame(); + frame.FromLegacy(legacyFrame, null, null); + replay.Frames.Add(frame); + } + }; + + Add(new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) + { + Recorder = recorder = new TestReplayRecorder + { + ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), + }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + Colour = Color4.Brown, + RelativeSizeAxes = Axes.Both, + }, + new OsuSpriteText + { + Text = "Sending", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + }, + 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 = "Receiving", + Scale = new Vector2(3), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new TestInputConsumer() + } + }, + } + } + } + }); + }); + + [Test] + public void TestBasic() + { + } + + protected override void Update() + { + base.Update(); + playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100); + } + + public class TestFramedReplayInputHandler : FramedReplayInputHandler + { + public TestFramedReplayInputHandler(Replay replay) + : base(replay) + { + } + + public override void CollectPendingInputs(List inputs) + { + inputs.Add(new MousePositionAbsoluteInput { Position = GamefieldToScreenSpace(CurrentFrame?.Position ?? Vector2.Zero) }); + inputs.Add(new ReplayState { PressedActions = CurrentFrame?.Actions ?? new List() }); + } + } + + public class TestInputConsumer : CompositeDrawable, IKeyBindingHandler + { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => Parent.ReceivePositionalInputAt(screenSpacePos); + + private readonly Box box; + + public TestInputConsumer() + { + 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 + { + 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, IConvertibleReplayFrame + { + public Vector2 Position; + + public List Actions = new List(); + + public TestReplayFrame(double time, Vector2 position, params TestAction[] actions) + : base(time) + { + Position = position; + Actions.AddRange(actions); + } + + public TestReplayFrame() + { + } + + public void FromLegacy(LegacyReplayFrame currentFrame, IBeatmap beatmap, ReplayFrame lastFrame = null) + { + Position = currentFrame.Position; + Time = currentFrame.Time; + if (currentFrame.MouseLeft) + Actions.Add(TestAction.Down); + } + + public LegacyReplayFrame ToLegacy(IBeatmap beatmap) + { + ReplayButtonState state = ReplayButtonState.None; + + if (Actions.Contains(TestAction.Down)) + state |= ReplayButtonState.Left1; + + return new LegacyReplayFrame(Time, Position.X, Position.Y, state); + } + } + + public enum TestAction + { + Down, + } + + internal class TestReplayRecorder : ReplayRecorder + { + public TestReplayRecorder() + : base(new Replay()) + { + } + + protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) + { + return new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); + } + } + } +} diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 21259bad5f..608123fbab 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -37,6 +37,8 @@ namespace osu.Game.Online.Spectator private readonly SpectatorState currentState = new SpectatorState(); + public event Action OnNewFrames; + [BackgroundDependencyLoader] private void load() { @@ -145,6 +147,7 @@ namespace osu.Game.Online.Spectator Task ISpectatorClient.UserSentFrames(string userId, FrameDataBundle data) { Console.WriteLine($"{connection.ConnectionId} Received frames from {userId}: {data.Frames.First()}"); + OnNewFrames?.Invoke(data); return Task.CompletedTask; } From 34e889e66e3bfeabd22ea5eb6550b5e338c15bce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 18:37:19 +0900 Subject: [PATCH 4089/6909] Don't watch every user in normal gameplay (but allow so in test) --- .../Visual/Gameplay/TestSceneSpectator.cs | 20 ++++- osu.Game/Online/Spectator/ISpectatorClient.cs | 6 +- osu.Game/Online/Spectator/ISpectatorServer.cs | 4 +- osu.Game/Online/Spectator/SpectatorState.cs | 1 - .../Spectator/SpectatorStreamingClient.cs | 85 ++++++++++++------- 5 files changed, 78 insertions(+), 38 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 665df5f9c7..2ec82ad5fb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -2,8 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.Specialized; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -34,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay private Replay replay; - private TestReplayRecorder recorder; + private IBindableList users; [Resolved] private SpectatorStreamingClient streamingClient { get; set; } @@ -44,7 +46,19 @@ namespace osu.Game.Tests.Visual.Gameplay { replay = new Replay(); - streamingClient.OnNewFrames += frames => + users = streamingClient.PlayingUsers.GetBoundCopy(); + users.BindCollectionChanged((obj, args) => + { + switch (args.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (int user in args.NewItems) + streamingClient.WatchUser(user); + break; + } + }, true); + + streamingClient.OnNewFrames += (userId, frames) => { foreach (var legacyFrame in frames.Frames) { @@ -63,7 +77,7 @@ namespace osu.Game.Tests.Visual.Gameplay { recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Recorder = recorder = new TestReplayRecorder + Recorder = new TestReplayRecorder { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), }, diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index dcff6e6c1c..18c9d61561 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.cs @@ -12,20 +12,20 @@ namespace osu.Game.Online.Spectator /// /// The user. /// The state of gameplay. - Task UserBeganPlaying(string userId, SpectatorState state); + Task UserBeganPlaying(int userId, SpectatorState state); /// /// Signals that a user has finished a play session. /// /// The user. /// The state of gameplay. - Task UserFinishedPlaying(string userId, SpectatorState state); + Task UserFinishedPlaying(int userId, SpectatorState state); /// /// Called when new frames are available for a subscribed user's play session. /// /// The user. /// The frame data. - Task UserSentFrames(string userId, FrameDataBundle data); + Task UserSentFrames(int userId, FrameDataBundle data); } } diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs index 018fa6b66b..99893e385c 100644 --- a/osu.Game/Online/Spectator/ISpectatorServer.cs +++ b/osu.Game/Online/Spectator/ISpectatorServer.cs @@ -30,12 +30,12 @@ namespace osu.Game.Online.Spectator /// For offline users, a subscription will be created and data will begin streaming on next play. /// /// The user to subscribe to. - Task StartWatchingUser(string userId); + Task StartWatchingUser(int userId); /// /// Stop requesting spectating data for the specified user. Unsubscribes from receiving further data. /// /// The user to unsubscribe from. - Task EndWatchingUser(string userId); + Task EndWatchingUser(int userId); } } diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index 3d9997f006..6b2b8b8cb2 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using osu.Game.Online.API; -using osu.Game.Rulesets.Mods; namespace osu.Game.Online.Spectator { diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 608123fbab..2665243e4c 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -20,7 +21,11 @@ namespace osu.Game.Online.Spectator { private HubConnection connection; - private readonly List watchingUsers = new List(); + private readonly List watchingUsers = new List(); + + public IBindableList PlayingUsers => playingUsers; + + private readonly BindableList playingUsers = new BindableList(); private readonly IBindable apiState = new Bindable(); @@ -37,7 +42,12 @@ namespace osu.Game.Online.Spectator private readonly SpectatorState currentState = new SpectatorState(); - public event Action OnNewFrames; + private bool isPlaying; + + /// + /// Called whenever new frames arrive from the server. + /// + public event Action OnNewFrames; [BackgroundDependencyLoader] private void load() @@ -82,13 +92,15 @@ namespace osu.Game.Online.Spectator .Build(); // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) - connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); - connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); - connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); + connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); + connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); connection.Closed += async ex => { isConnected = false; + playingUsers.Clear(); + if (ex != null) await tryUntilConnected(); }; @@ -105,6 +117,17 @@ namespace osu.Game.Online.Spectator // success isConnected = true; + + // resubscribe to watched users + var users = watchingUsers.ToArray(); + watchingUsers.Clear(); + foreach (var userId in users) + WatchUser(userId); + + // re-send state in case it wasn't received + if (isPlaying) + beginPlaying(); + break; } catch @@ -115,39 +138,23 @@ namespace osu.Game.Online.Spectator } } - Task ISpectatorClient.UserBeganPlaying(string userId, SpectatorState state) + Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state) { - if (connection.ConnectionId != userId) - { - if (watchingUsers.Contains(userId)) - { - Console.WriteLine($"{connection.ConnectionId} received began playing for already watched user {userId}"); - } - else - { - Console.WriteLine($"{connection.ConnectionId} requesting watch other user {userId}"); - WatchUser(userId); - watchingUsers.Add(userId); - } - } - else - { - Console.WriteLine($"{connection.ConnectionId} Received user playing event for self {state}"); - } + if (!playingUsers.Contains(userId)) + playingUsers.Add(userId); return Task.CompletedTask; } - Task ISpectatorClient.UserFinishedPlaying(string userId, SpectatorState state) + Task ISpectatorClient.UserFinishedPlaying(int userId, SpectatorState state) { - Console.WriteLine($"{connection.ConnectionId} Received user finished event {state}"); + playingUsers.Remove(userId); return Task.CompletedTask; } - Task ISpectatorClient.UserSentFrames(string userId, FrameDataBundle data) + Task ISpectatorClient.UserSentFrames(int userId, FrameDataBundle data) { - Console.WriteLine($"{connection.ConnectionId} Received frames from {userId}: {data.Frames.First()}"); - OnNewFrames?.Invoke(data); + OnNewFrames?.Invoke(userId, data); return Task.CompletedTask; } @@ -155,10 +162,22 @@ namespace osu.Game.Online.Spectator { if (!isConnected) return; + if (isPlaying) + throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); + + isPlaying = true; + // transfer state at point of beginning play currentState.BeatmapID = beatmap.Value.BeatmapInfo.OnlineBeatmapID; currentState.Mods = mods.Value.Select(m => new APIMod(m)); + beginPlaying(); + } + + private void beginPlaying() + { + Debug.Assert(isPlaying); + connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState); } @@ -173,13 +192,21 @@ namespace osu.Game.Online.Spectator { if (!isConnected) return; + if (!isPlaying) + throw new InvalidOperationException($"Cannot invoke {nameof(EndPlaying)} when not playing"); + + isPlaying = false; connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState); } - public void WatchUser(string userId) + public void WatchUser(int userId) { if (!isConnected) return; + if (watchingUsers.Contains(userId)) + return; + + watchingUsers.Add(userId); connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); } From d659b7739d4c78ff8926565145c1952fdb91b76f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:16:34 +0900 Subject: [PATCH 4090/6909] Correctly stop watching users that leave --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 2ec82ad5fb..be3241c784 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -55,6 +55,11 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (int user in args.NewItems) streamingClient.WatchUser(user); break; + + case NotifyCollectionChangedAction.Remove: + foreach (int user in args.OldItems) + streamingClient.StopWatchingUser(user); + break; } }, true); From 823d717a7d986e5551b50d87f1d3abefdffb560b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:17:10 +0900 Subject: [PATCH 4091/6909] Reduce the serialised size of LegacyReplayFrame --- osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index c3cffa8699..74bacae9e1 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.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 Newtonsoft.Json; using osu.Game.Rulesets.Replays; using osuTK; @@ -8,17 +9,28 @@ namespace osu.Game.Replays.Legacy { public class LegacyReplayFrame : ReplayFrame { + [JsonIgnore] public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); public float? MouseX; public float? MouseY; + [JsonIgnore] public bool MouseLeft => MouseLeft1 || MouseLeft2; + + [JsonIgnore] public bool MouseRight => MouseRight1 || MouseRight2; + [JsonIgnore] public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1); + + [JsonIgnore] public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1); + + [JsonIgnore] public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2); + + [JsonIgnore] public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2); public ReplayButtonState ButtonState; From ee2513bf4b7fc17192dc584c21f1f1a911c59971 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:17:19 +0900 Subject: [PATCH 4092/6909] Add batch sending --- .../Spectator/SpectatorStreamingClient.cs | 56 +++++++++++++++++-- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 2665243e4c..9ebb84c007 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -185,7 +186,7 @@ namespace osu.Game.Online.Spectator { if (!isConnected) return; - connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); + lastSend = connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); } public void EndPlaying() @@ -201,21 +202,64 @@ namespace osu.Game.Online.Spectator public void WatchUser(int userId) { - if (!isConnected) return; - if (watchingUsers.Contains(userId)) return; watchingUsers.Add(userId); + + if (!isConnected) return; + connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); } + public void StopWatchingUser(int userId) + { + watchingUsers.Remove(userId); + + if (!isConnected) return; + + connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId); + } + + private readonly Queue pendingFrames = new Queue(); + + private double lastSendTime; + + private Task lastSend; + + private const double time_between_sends = 200; + + private const int max_pending_frames = 30; + + protected override void Update() + { + base.Update(); + + if (pendingFrames.Count > 0 && Time.Current - lastSendTime > time_between_sends) + purgePendingFrames(); + } + public void HandleFrame(ReplayFrame frame) { - // ReSharper disable once SuspiciousTypeConversion.Global (implemented by rulesets) if (frame is IConvertibleReplayFrame convertible) - // TODO: don't send a bundle for each individual frame - SendFrames(new FrameDataBundle(new[] { convertible.ToLegacy(beatmap.Value.Beatmap) })); + pendingFrames.Enqueue(convertible.ToLegacy(beatmap.Value.Beatmap)); + + if (pendingFrames.Count > max_pending_frames) + purgePendingFrames(); + } + + private void purgePendingFrames() + { + if (lastSend?.IsCompleted == false) + return; + + var frames = pendingFrames.ToArray(); + + pendingFrames.Clear(); + + SendFrames(new FrameDataBundle(frames)); + + lastSendTime = Time.Current; } } } From 04f46bc1f84739780214d468f1d79a298ffd4ee3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:24:32 +0900 Subject: [PATCH 4093/6909] Clean up usings --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 71cd39953c..d464eee7c5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -13,9 +13,7 @@ using osu.Framework.Input.StateChanges; using osu.Framework.Testing; using osu.Framework.Threading; using osu.Game.Graphics.Sprites; -using osu.Game.Online.Spectator; using osu.Game.Replays; -using osu.Game.Replays.Legacy; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; From 147d502da13bb303af11cf53395cd53ed09f4e8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:30:07 +0900 Subject: [PATCH 4094/6909] Fix initial play state not being kept locally if not connected --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 9ebb84c007..6737625818 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -161,8 +161,6 @@ namespace osu.Game.Online.Spectator public void BeginPlaying() { - if (!isConnected) return; - if (isPlaying) throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); @@ -179,6 +177,8 @@ namespace osu.Game.Online.Spectator { Debug.Assert(isPlaying); + if (!isConnected) return; + connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState); } From 51ae93d484f31fd586fddee4095657caaf0924ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:31:56 +0900 Subject: [PATCH 4095/6909] Revert unnecessary file changes --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index d464eee7c5..bc1c10e59d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -266,9 +266,7 @@ namespace osu.Game.Tests.Visual.Gameplay } protected override ReplayFrame HandleFrame(Vector2 mousePosition, List actions, ReplayFrame previousFrame) - { - return new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); - } + => new TestReplayFrame(Time.Current, mousePosition, actions.ToArray()); } } } From 9f2f8d8cc778df600182d50defdf28ae5106f0ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 19:41:10 +0900 Subject: [PATCH 4096/6909] Fix missing licence headers --- osu.Game/Online/Spectator/FrameDataBundle.cs | 3 +++ osu.Game/Online/Spectator/ISpectatorClient.cs | 3 +++ osu.Game/Online/Spectator/ISpectatorServer.cs | 3 +++ osu.Game/Online/Spectator/SpectatorState.cs | 5 ++++- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 3 +++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs index 67f2688289..5281e61f9c 100644 --- a/osu.Game/Online/Spectator/FrameDataBundle.cs +++ b/osu.Game/Online/Spectator/FrameDataBundle.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System; using System.Collections.Generic; using osu.Game.Replays.Legacy; diff --git a/osu.Game/Online/Spectator/ISpectatorClient.cs b/osu.Game/Online/Spectator/ISpectatorClient.cs index 18c9d61561..3acc9b2282 100644 --- a/osu.Game/Online/Spectator/ISpectatorClient.cs +++ b/osu.Game/Online/Spectator/ISpectatorClient.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.Threading.Tasks; namespace osu.Game.Online.Spectator diff --git a/osu.Game/Online/Spectator/ISpectatorServer.cs b/osu.Game/Online/Spectator/ISpectatorServer.cs index 99893e385c..af0196862a 100644 --- a/osu.Game/Online/Spectator/ISpectatorServer.cs +++ b/osu.Game/Online/Spectator/ISpectatorServer.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.Threading.Tasks; namespace osu.Game.Online.Spectator diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index 6b2b8b8cb2..48fad4b3b2 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; @@ -21,7 +24,7 @@ namespace osu.Game.Online.Spectator Mods = mods; } - public bool Equals(SpectatorState other) => this.BeatmapID == other?.BeatmapID && this.Mods.SequenceEqual(other?.Mods); + public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods); public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)}"; } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 6737625818..006f75c1d2 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System; using System.Collections.Generic; using System.Diagnostics; From 54d666604bc0daa207aa1c923715a391b22badb4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 22 Oct 2020 22:56:23 +0900 Subject: [PATCH 4097/6909] Fix incorrect order of flag settings --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 006f75c1d2..2fc1431702 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -194,12 +194,13 @@ namespace osu.Game.Online.Spectator public void EndPlaying() { - if (!isConnected) return; - if (!isPlaying) throw new InvalidOperationException($"Cannot invoke {nameof(EndPlaying)} when not playing"); isPlaying = false; + + if (!isConnected) return; + connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState); } From 2871001cc294da01f2db8e8c35d84a86ab1503ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 22 Oct 2020 18:21:28 +0200 Subject: [PATCH 4098/6909] Add BackgroundSource.Seasonal --- osu.Game/Configuration/BackgroundSource.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/BackgroundSource.cs b/osu.Game/Configuration/BackgroundSource.cs index 5726e96eb1..beef9ef1de 100644 --- a/osu.Game/Configuration/BackgroundSource.cs +++ b/osu.Game/Configuration/BackgroundSource.cs @@ -6,6 +6,7 @@ namespace osu.Game.Configuration public enum BackgroundSource { Skin, - Beatmap + Beatmap, + Seasonal } } From cdb2d23578e6de7ca266a23a51b5fac2ed15b8f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 22 Oct 2020 18:23:03 +0200 Subject: [PATCH 4099/6909] Set BackgroundSource.Seasonal as default setting --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 7d601c0cb9..5c5af701bb 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -130,7 +130,7 @@ namespace osu.Game.Configuration Set(OsuSetting.IntroSequence, IntroSequence.Triangles); - Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); + Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Seasonal); } public OsuConfigManager(Storage storage) From 09d49aa0f7b2d953370e02897ef382bd3ea04f92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 22 Oct 2020 18:25:01 +0200 Subject: [PATCH 4100/6909] Add GetSeasonalBackgroundsRequest --- .../Requests/GetSeasonalBackgroundsRequest.cs | 12 +++++++++++ .../Responses/APISeasonalBackgrounds.cs | 20 +++++++++++++++++++ 2 files changed, 32 insertions(+) create mode 100644 osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.cs create mode 100644 osu.Game/Online/API/Requests/Responses/APISeasonalBackgrounds.cs diff --git a/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.cs b/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.cs new file mode 100644 index 0000000000..941b47244a --- /dev/null +++ b/osu.Game/Online/API/Requests/GetSeasonalBackgroundsRequest.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.Online.API.Requests +{ + public class GetSeasonalBackgroundsRequest : APIRequest + { + protected override string Target => @"seasonal-backgrounds"; + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APISeasonalBackgrounds.cs b/osu.Game/Online/API/Requests/Responses/APISeasonalBackgrounds.cs new file mode 100644 index 0000000000..6996ac4d9b --- /dev/null +++ b/osu.Game/Online/API/Requests/Responses/APISeasonalBackgrounds.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 Newtonsoft.Json; + +namespace osu.Game.Online.API.Requests.Responses +{ + public class APISeasonalBackgrounds + { + [JsonProperty("backgrounds")] + public List Backgrounds { get; set; } + } + + public class APISeasonalBackground + { + [JsonProperty("url")] + public string Url { get; set; } + } +} From f11bcfcb8f301b8d19e465176a7b66ca70f88858 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 10:03:33 +0900 Subject: [PATCH 4101/6909] Remove unnecessary public specification in interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Online/API/IAPIProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 9b7485decd..d10cb4b6d2 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -22,7 +22,7 @@ namespace osu.Game.Online.API /// /// Retrieve the OAuth access token. /// - public string AccessToken { get; } + string AccessToken { get; } /// /// Returns whether the local user is logged in. From e99cf369fac8e35b16a2b9458651f847cb1905f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 13:33:23 +0900 Subject: [PATCH 4102/6909] Don't worry about EndPlaying being invoked when not playing --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 2fc1431702..1ca0a378bb 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -194,9 +194,6 @@ namespace osu.Game.Online.Spectator public void EndPlaying() { - if (!isPlaying) - throw new InvalidOperationException($"Cannot invoke {nameof(EndPlaying)} when not playing"); - isPlaying = false; if (!isConnected) return; From 55f1b05dbf6c74e389cfc6af2133ed03bd7e2da0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 14:47:08 +0900 Subject: [PATCH 4103/6909] Fix test failures due to recorder not stopping in time --- .../Visual/Gameplay/TestSceneReplayRecorder.cs | 6 ++++++ osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 11 ++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index bc1c10e59d..e964d2a40e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -166,6 +166,12 @@ namespace osu.Game.Tests.Visual.Gameplay playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100); } + [TearDownSteps] + public void TearDown() + { + AddStep("stop recorder", () => recorder.Expire()); + } + public class TestFramedReplayInputHandler : FramedReplayInputHandler { public TestFramedReplayInputHandler(Replay replay) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index be3241c784..f8b5d385a9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Online.Spectator; @@ -38,6 +39,8 @@ namespace osu.Game.Tests.Visual.Gameplay private IBindableList users; + private TestReplayRecorder recorder; + [Resolved] private SpectatorStreamingClient streamingClient { get; set; } @@ -82,7 +85,7 @@ namespace osu.Game.Tests.Visual.Gameplay { recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Recorder = new TestReplayRecorder + Recorder = recorder = new TestReplayRecorder { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), }, @@ -153,6 +156,12 @@ namespace osu.Game.Tests.Visual.Gameplay playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100); } + [TearDownSteps] + public void TearDown() + { + AddStep("stop recorder", () => recorder.Expire()); + } + public class TestFramedReplayInputHandler : FramedReplayInputHandler { public TestFramedReplayInputHandler(Replay replay) From 4fca7675b07fbd9c9784560ec22479cc986c0223 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 14:47:21 +0900 Subject: [PATCH 4104/6909] Don't send spectate data when an autoplay mod is active --- osu.Game/Screens/Play/Player.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 9ee0b8a54f..6b2d2f40d0 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -152,7 +152,9 @@ namespace osu.Game.Screens.Play { base.LoadComplete(); - PrepareReplay(); + // replays should never be recorded or played back when autoplay is enabled + if (!Mods.Value.Any(m => m is ModAutoplay)) + PrepareReplay(); } private Replay recordingReplay; From 9141f48b047c0a59fa49c0dfadfef578900753f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 14:57:27 +0900 Subject: [PATCH 4105/6909] Remove beatmap-based ctor to promote single flow --- .../Expanded/ExpandedPanelMiddleContent.cs | 5 +++-- .../Ranking/Expanded/StarRatingDisplay.cs | 22 ++++--------------- 2 files changed, 7 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 5aac449adb..30747438c3 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; 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; @@ -51,7 +52,7 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load() + private void load(BeatmapDifficultyManager beatmapDifficultyManager) { var beatmap = score.Beatmap; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; @@ -138,7 +139,7 @@ namespace osu.Game.Screens.Ranking.Expanded Spacing = new Vector2(5, 0), Children = new Drawable[] { - new StarRatingDisplay(beatmap) + new StarRatingDisplay(beatmapDifficultyManager.GetDifficulty(beatmap, score.Ruleset, score.Mods)) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs index 402ab99908..ffb12d474b 100644 --- a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs @@ -22,18 +22,7 @@ namespace osu.Game.Screens.Ranking.Expanded /// public class StarRatingDisplay : CompositeDrawable { - private readonly BeatmapInfo beatmap; - - private StarDifficulty? difficulty; - - /// - /// Creates a new . - /// - /// The to display the star difficulty of. - public StarRatingDisplay(BeatmapInfo beatmap) - { - this.beatmap = beatmap; - } + private readonly StarDifficulty difficulty; /// /// Creates a new using an already computed . @@ -49,17 +38,14 @@ namespace osu.Game.Screens.Ranking.Expanded { AutoSizeAxes = Axes.Both; - if (!difficulty.HasValue) - difficulty = difficultyManager.GetDifficulty(beatmap); - - var starRatingParts = difficulty.Value.Stars.ToString("0.00", CultureInfo.InvariantCulture).Split('.'); + var starRatingParts = difficulty.Stars.ToString("0.00", CultureInfo.InvariantCulture).Split('.'); string wholePart = starRatingParts[0]; string fractionPart = starRatingParts[1]; string separator = CultureInfo.CurrentCulture.NumberFormat.NumberDecimalSeparator; - ColourInfo backgroundColour = difficulty.Value.DifficultyRating == DifficultyRating.ExpertPlus + ColourInfo backgroundColour = difficulty.DifficultyRating == DifficultyRating.ExpertPlus ? ColourInfo.GradientVertical(Color4Extensions.FromHex("#C1C1C1"), Color4Extensions.FromHex("#595959")) - : (ColourInfo)colours.ForDifficultyRating(difficulty.Value.DifficultyRating); + : (ColourInfo)colours.ForDifficultyRating(difficulty.DifficultyRating); InternalChildren = new Drawable[] { From 9404096a28c49a2c9370d6dd2d07a893d86f82df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 15:06:00 +0900 Subject: [PATCH 4106/6909] Update tests to match new constructor --- .../Visual/Ranking/TestSceneStarRatingDisplay.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs index d12f32e470..d0067c3396 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStarRatingDisplay.cs @@ -18,13 +18,13 @@ namespace osu.Game.Tests.Visual.Ranking 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 }), + new StarRatingDisplay(new StarDifficulty(1.23, 0)), + new StarRatingDisplay(new StarDifficulty(2.34, 0)), + new StarRatingDisplay(new StarDifficulty(3.45, 0)), + new StarRatingDisplay(new StarDifficulty(4.56, 0)), + new StarRatingDisplay(new StarDifficulty(5.67, 0)), + new StarRatingDisplay(new StarDifficulty(6.78, 0)), + new StarRatingDisplay(new StarDifficulty(10.11, 0)), } }; } From 1b84402b966744babc95e20790f83fa3061b9f8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 15:33:38 +0900 Subject: [PATCH 4107/6909] Centralise and share logic for storyboard frame lookup method --- .../Drawables/DrawableStoryboardAnimation.cs | 19 +++++-------------- .../Drawables/DrawableStoryboardSprite.cs | 16 +++++----------- osu.Game/Storyboards/Storyboard.cs | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+), 25 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 8382f91d1f..97de239e4a 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -2,16 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; -using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Skinning; +using osuTK; namespace osu.Game.Storyboards.Drawables { @@ -117,18 +113,13 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(IBindable beatmap, TextureStore textureStore, Storyboard storyboard) + private void load(TextureStore textureStore, Storyboard storyboard) { - for (var frame = 0; frame < Animation.FrameCount; frame++) + for (int frame = 0; frame < Animation.FrameCount; frame++) { - var framePath = Animation.Path.Replace(".", frame + "."); + string framePath = Animation.Path.Replace(".", frame + "."); - var storyboardPath = beatmap.Value.BeatmapSetInfo.Files.Find(f => f.Filename.Equals(framePath, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - var frameSprite = storyboard.UseSkinSprites && storyboardPath == null - ? (Drawable)new SkinnableSprite(framePath) - : new Sprite { Texture = textureStore.Get(storyboardPath) }; - - AddFrame(frameSprite, Animation.FrameDelay); + AddFrame(storyboard.CreateSpriteFromResourcePath(framePath, textureStore), Animation.FrameDelay); } Animation.ApplyTransforms(this); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 9599375c76..1adbe688e7 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -2,16 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK; 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.Framework.Utils; -using osu.Game.Beatmaps; -using osu.Game.Skinning; +using osuTK; namespace osu.Game.Storyboards.Drawables { @@ -116,14 +112,12 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader] - private void load(IBindable beatmap, TextureStore textureStore, Storyboard storyboard) + private void load(TextureStore textureStore, Storyboard storyboard) { - var storyboardPath = beatmap.Value.BeatmapSetInfo?.Files?.Find(f => f.Filename.Equals(Sprite.Path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; - var sprite = storyboard.UseSkinSprites && storyboardPath == null - ? (Drawable)new SkinnableSprite(Sprite.Path) - : new Sprite { Texture = textureStore.Get(storyboardPath) }; + var drawable = storyboard.CreateSpriteFromResourcePath(Sprite.Path, textureStore); - InternalChild = sprite.With(s => s.Anchor = s.Origin = Anchor.Centre); + if (drawable != null) + InternalChild = drawable.With(s => s.Anchor = s.Origin = Anchor.Centre); Sprite.ApplyTransforms(this); } diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index daafdf015d..e0d18eab00 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -1,9 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; using osu.Game.Beatmaps; +using osu.Game.Skinning; using osu.Game.Storyboards.Drawables; namespace osu.Game.Storyboards @@ -69,5 +74,19 @@ namespace osu.Game.Storyboards drawable.Width = drawable.Height * (BeatmapInfo.WidescreenStoryboard ? 16 / 9f : 4 / 3f); return drawable; } + + public Drawable CreateSpriteFromResourcePath(string path, TextureStore textureStore) + { + Drawable drawable = null; + var storyboardPath = BeatmapInfo.BeatmapSet?.Files?.Find(f => f.Filename.Equals(path, StringComparison.OrdinalIgnoreCase))?.FileInfo.StoragePath; + + if (storyboardPath != null) + drawable = new Sprite { Texture = textureStore.Get(storyboardPath) }; + // if the texture isn't available locally in the beatmap, some storyboards choose to source from the underlying skin lookup hierarchy. + else if (UseSkinSprites) + drawable = new SkinnableSprite(path); + + return drawable; + } } } From 4f746792fba1f427357675b0c054ff84b2bd95b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 15:46:24 +0900 Subject: [PATCH 4108/6909] Fix regression causing storyboard sprites to have incorrect origin support --- osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 1adbe688e7..7b1a6d54da 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -109,6 +109,8 @@ namespace osu.Game.Storyboards.Drawables LifetimeStart = sprite.StartTime; LifetimeEnd = sprite.EndTime; + + AutoSizeAxes = Axes.Both; } [BackgroundDependencyLoader] @@ -117,7 +119,7 @@ namespace osu.Game.Storyboards.Drawables var drawable = storyboard.CreateSpriteFromResourcePath(Sprite.Path, textureStore); if (drawable != null) - InternalChild = drawable.With(s => s.Anchor = s.Origin = Anchor.Centre); + InternalChild = drawable; Sprite.ApplyTransforms(this); } From e20a98640199bce08ba1f445ca283027f8fe9282 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 23 Oct 2020 17:24:19 +0900 Subject: [PATCH 4109/6909] Add ruleset to state --- osu.Game/Online/Spectator/SpectatorState.cs | 13 ++++--------- .../Online/Spectator/SpectatorStreamingClient.cs | 5 +++++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index 48fad4b3b2..101ce3d5d5 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -14,18 +14,13 @@ namespace osu.Game.Online.Spectator { public int? BeatmapID { get; set; } + public int? RulesetID { get; set; } + [NotNull] public IEnumerable Mods { get; set; } = Enumerable.Empty(); - public SpectatorState(int? beatmapId = null, IEnumerable mods = null) - { - BeatmapID = beatmapId; - if (mods != null) - Mods = mods; - } + public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods) && RulesetID == other?.RulesetID; - public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods); - - public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)}"; + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 1ca0a378bb..43bc8ff71b 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; @@ -41,6 +42,9 @@ namespace osu.Game.Online.Spectator [Resolved] private IBindable beatmap { get; set; } + [Resolved] + private IBindable ruleset { get; set; } + [Resolved] private IBindable> mods { get; set; } @@ -171,6 +175,7 @@ namespace osu.Game.Online.Spectator // transfer state at point of beginning play currentState.BeatmapID = beatmap.Value.BeatmapInfo.OnlineBeatmapID; + currentState.RulesetID = ruleset.Value.ID; currentState.Mods = mods.Value.Select(m => new APIMod(m)); beginPlaying(); From c1d03a5baceb23be78f0d55458a9085ad5b652ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Fri, 23 Oct 2020 13:40:13 +0200 Subject: [PATCH 4110/6909] Add SeasonalBackgroundLoader and SeasonalBackground --- .../Backgrounds/SeasonalBackgroundLoader.cs | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs new file mode 100644 index 0000000000..af81b25cee --- /dev/null +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Framework.Utils; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Graphics.Backgrounds +{ + [LongRunningLoad] + public class SeasonalBackgroundLoader : Component + { + private List backgrounds = new List(); + private int current; + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + var request = new GetSeasonalBackgroundsRequest(); + request.Success += response => + { + backgrounds = response.Backgrounds ?? backgrounds; + current = RNG.Next(0, backgrounds.Count); + }; + + api.PerformAsync(request); + } + + public SeasonalBackground LoadBackground(string fallbackTextureName) + { + string url = null; + + if (backgrounds.Any()) + { + current = (current + 1) % backgrounds.Count; + url = backgrounds[current].Url; + } + + return new SeasonalBackground(url, fallbackTextureName); + } + } + + [LongRunningLoad] + public class SeasonalBackground : Background + { + private readonly string url; + private readonly string fallbackTextureName; + + public SeasonalBackground([CanBeNull] string url, string fallbackTextureName = @"Backgrounds/bg1") + { + this.url = url; + this.fallbackTextureName = fallbackTextureName; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + Sprite.Texture = textures.Get(url) ?? textures.Get(fallbackTextureName); + } + } +} From 81ebcd879668eb13cb28aa11bf4edfb8afb0fb99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Fri, 23 Oct 2020 13:41:00 +0200 Subject: [PATCH 4111/6909] Load SeasonalBackgroundLoader asynchronously --- .../Backgrounds/BackgroundScreenDefault.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index ef41c5be3d..ec91dcc99f 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.Backgrounds private Bindable skin; private Bindable mode; private Bindable introSequence; + private readonly SeasonalBackgroundLoader seasonalBackgroundLoader = new SeasonalBackgroundLoader(); [Resolved] private IBindable beatmap { get; set; } @@ -50,7 +51,7 @@ namespace osu.Game.Screens.Backgrounds currentDisplay = RNG.Next(0, background_count); - display(createBackground()); + LoadComponentAsync(seasonalBackgroundLoader, _ => LoadComponentAsync(createBackground(), display)); } private void display(Background newBackground) @@ -90,6 +91,10 @@ namespace osu.Game.Screens.Backgrounds { switch (mode.Value) { + case BackgroundSource.Seasonal: + newBackground = seasonalBackgroundLoader.LoadBackground(backgroundName); + break; + case BackgroundSource.Beatmap: newBackground = new BeatmapBackground(beatmap.Value, backgroundName); break; @@ -100,7 +105,18 @@ namespace osu.Game.Screens.Backgrounds } } else - newBackground = new Background(backgroundName); + { + switch (mode.Value) + { + case BackgroundSource.Seasonal: + newBackground = seasonalBackgroundLoader.LoadBackground(backgroundName); + break; + + default: + newBackground = new Background(backgroundName); + break; + } + } newBackground.Depth = currentDisplay; From ae9e60560bc3e00efb4b56692afd387d2a6e1564 Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 23 Oct 2020 14:11:29 +0200 Subject: [PATCH 4112/6909] Fixed gameplay flags being bigger and changed values to make more sense --- osu.Game.Tournament/Components/DrawableTeamFlag.cs | 2 +- osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs | 1 + osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs | 2 +- osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Components/DrawableTeamFlag.cs b/osu.Game.Tournament/Components/DrawableTeamFlag.cs index a2e0bf83be..75991a1ab8 100644 --- a/osu.Game.Tournament/Components/DrawableTeamFlag.cs +++ b/osu.Game.Tournament/Components/DrawableTeamFlag.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tournament.Components { if (team == null) return; - Size = new Vector2(70, 47); + Size = new Vector2(75, 50); Masking = true; CornerRadius = 5; Child = flagSprite = new Sprite diff --git a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs index 44921f06ad..4ba86dcefc 100644 --- a/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs +++ b/osu.Game.Tournament/Screens/Gameplay/Components/TeamDisplay.cs @@ -29,6 +29,7 @@ namespace osu.Game.Tournament.Screens.Gameplay.Components var anchor = flip ? Anchor.TopLeft : Anchor.TopRight; Flag.RelativeSizeAxes = Axes.None; + Flag.Scale = new Vector2(0.8f); Flag.Origin = anchor; Flag.Anchor = anchor; diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index 32830713f6..55fc80dba2 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -288,7 +288,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro AutoSizeAxes = Axes.Both; Flag.RelativeSizeAxes = Axes.None; - Flag.Scale = new Vector2(1.4f); + Flag.Scale = new Vector2(1.2f); InternalChild = new FillFlowContainer { diff --git a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs index 3972c590ea..7ca262a2e8 100644 --- a/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs +++ b/osu.Game.Tournament/Screens/TeamWin/TeamWinScreen.cs @@ -93,7 +93,7 @@ namespace osu.Game.Tournament.Screens.TeamWin Anchor = Anchor.Centre, Origin = Anchor.Centre, Position = new Vector2(-300, 10), - Scale = new Vector2(2.2f) + Scale = new Vector2(2f) }, new FillFlowContainer { From c24a29d1acdde0228ffdd24f7726e0a0100f9e16 Mon Sep 17 00:00:00 2001 From: Shivam Date: Fri, 23 Oct 2020 14:23:08 +0200 Subject: [PATCH 4113/6909] Update flag scale of drawablematchteam --- .../Screens/Ladder/Components/DrawableMatchTeam.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index 030ccb5cb3..ba577888d8 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components this.losers = losers; Size = new Vector2(150, 40); - Flag.Scale = new Vector2(0.6f); + Flag.Scale = new Vector2(0.55f); Flag.Anchor = Flag.Origin = Anchor.CentreLeft; AcronymText.Anchor = AcronymText.Origin = Anchor.CentreLeft; From 73174961f02f01123f1c3cef900bf8a9475d8e90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 20 Oct 2020 21:22:47 +0200 Subject: [PATCH 4114/6909] Rework animation sequence for readability --- osu.Game/Screens/Play/PlayerLoader.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index fae0bfb295..be3bad1517 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -331,18 +331,11 @@ namespace osu.Game.Screens.Play { const double epilepsy_display_length = 3000; - pushSequence.Schedule(() => - { - epilepsyWarning.State.Value = Visibility.Visible; - - this.Delay(epilepsy_display_length).Schedule(() => - { - epilepsyWarning.Hide(); - epilepsyWarning.Expire(); - }); - }); - - pushSequence.Delay(epilepsy_display_length); + pushSequence + .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) + .Delay(epilepsy_display_length) + .Schedule(() => epilepsyWarning.Hide()) + .Delay(EpilepsyWarning.FADE_DURATION); } pushSequence.Schedule(() => From e101ba5cba4eb63ef287a60d7d1bd121893f741d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Oct 2020 22:58:13 +0200 Subject: [PATCH 4115/6909] Move volume manipulations to player loader --- osu.Game/Screens/Play/EpilepsyWarning.cs | 18 ------------------ osu.Game/Screens/Play/PlayerLoader.cs | 23 +++++++++++++++++++++-- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs index e3cf0cd227..6121a0c2a3 100644 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ b/osu.Game/Screens/Play/EpilepsyWarning.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Audio; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,11 +16,6 @@ namespace osu.Game.Screens.Play { public class EpilepsyWarning : VisibilityContainer { - public const double FADE_DURATION = 500; - - private readonly BindableDouble trackVolumeOnEpilepsyWarning = new BindableDouble(1f); - - private Track track; public EpilepsyWarning() { @@ -77,26 +70,15 @@ namespace osu.Game.Screens.Play } } }; - - track = beatmap.Value.Track; - track.AddAdjustment(AdjustableProperty.Volume, trackVolumeOnEpilepsyWarning); } protected override void PopIn() { - this.TransformBindableTo(trackVolumeOnEpilepsyWarning, 0.25, FADE_DURATION); - DimmableBackground?.FadeColour(OsuColour.Gray(0.5f), FADE_DURATION, Easing.OutQuint); this.FadeIn(FADE_DURATION, Easing.OutQuint); } protected override void PopOut() => this.FadeOut(FADE_DURATION); - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - track?.RemoveAdjustment(AdjustableProperty.Volume, trackVolumeOnEpilepsyWarning); - } } } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index be3bad1517..fe774527b8 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -104,6 +104,9 @@ namespace osu.Game.Screens.Play [Resolved] private AudioManager audioManager { get; set; } + [Resolved] + private MusicController musicController { get; set; } + public PlayerLoader(Func createPlayer) { this.createPlayer = createPlayer; @@ -332,9 +335,17 @@ namespace osu.Game.Screens.Play const double epilepsy_display_length = 3000; pushSequence - .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) + .Schedule(() => + { + musicController.CurrentTrack.VolumeTo(0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint); + epilepsyWarning.State.Value = Visibility.Visible; + }) .Delay(epilepsy_display_length) - .Schedule(() => epilepsyWarning.Hide()) + .Schedule(() => + { + epilepsyWarning.Hide(); + epilepsyWarning.Expire(); + }) .Delay(EpilepsyWarning.FADE_DURATION); } @@ -348,6 +359,10 @@ namespace osu.Game.Screens.Play // Note that this may change if the player we load requested a re-run. ValidForResume = false; + // restore full volume immediately - there's a usually a period of silence at start of gameplay anyway. + // note that this is delayed slightly to avoid volume spikes just before push. + musicController.CurrentTrack.Delay(50).VolumeTo(1); + if (player.LoadedBeatmapSuccessfully) this.Push(player); else @@ -363,6 +378,10 @@ namespace osu.Game.Screens.Play private void cancelLoad() { + // in case the epilepsy warning is being displayed, restore full volume. + if (epilepsyWarning?.IsAlive == true) + musicController.CurrentTrack.VolumeTo(1, EpilepsyWarning.FADE_DURATION, Easing.OutQuint); + scheduledPushPlayer?.Cancel(); scheduledPushPlayer = null; } From 85e14f3f0c8b3e194cb51057e4e9b2970dda7a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 24 Oct 2020 22:58:43 +0200 Subject: [PATCH 4116/6909] Shorten fade duration to make fade out snappier --- osu.Game/Screens/Play/EpilepsyWarning.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs index 6121a0c2a3..dc42427fbf 100644 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ b/osu.Game/Screens/Play/EpilepsyWarning.cs @@ -16,6 +16,7 @@ namespace osu.Game.Screens.Play { public class EpilepsyWarning : VisibilityContainer { + public const double FADE_DURATION = 250; public EpilepsyWarning() { From 8b04cd2cb0a79a81432bff9f488d12300240ebac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 25 Oct 2020 20:28:24 +0900 Subject: [PATCH 4117/6909] Fix a potential null reference when loading carousel difficulties --- 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 703b91c517..93f95e76cc 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -140,7 +140,7 @@ namespace osu.Game.Screens.Select.Carousel LoadComponentAsync(beatmapContainer, loaded => { // make sure the pooled target hasn't changed. - if (carouselBeatmapSet != Item) + if (beatmapContainer != loaded) return; Content.Child = loaded; From 0542a45c43a14a16eec8a99570c070e642104fd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 25 Oct 2020 12:33:35 +0100 Subject: [PATCH 4118/6909] Change to manual adjustment add/remove --- osu.Game/Screens/Play/PlayerLoader.cs | 27 +++++++++++---------------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index fe774527b8..42074ac241 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -55,6 +55,8 @@ namespace osu.Game.Screens.Play private bool backgroundBrightnessReduction; + private readonly BindableDouble volumeAdjustment = new BindableDouble(1); + protected bool BackgroundBrightnessReduction { set @@ -104,9 +106,6 @@ namespace osu.Game.Screens.Play [Resolved] private AudioManager audioManager { get; set; } - [Resolved] - private MusicController musicController { get; set; } - public PlayerLoader(Func createPlayer) { this.createPlayer = createPlayer; @@ -172,6 +171,7 @@ namespace osu.Game.Screens.Play if (epilepsyWarning != null) epilepsyWarning.DimmableBackground = Background; + Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); content.ScaleTo(0.7f); Background?.FadeColour(Color4.White, 800, Easing.OutQuint); @@ -200,6 +200,11 @@ namespace osu.Game.Screens.Play cancelLoad(); BackgroundBrightnessReduction = false; + + // we're moving to player, so a period of silence is upcoming. + // stop the track before removing adjustment to avoid a volume spike. + Beatmap.Value.Track.Stop(); + Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); } public override bool OnExiting(IScreen next) @@ -211,6 +216,7 @@ namespace osu.Game.Screens.Play Background.EnableUserDim.Value = false; BackgroundBrightnessReduction = false; + Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); return base.OnExiting(next); } @@ -335,11 +341,8 @@ namespace osu.Game.Screens.Play const double epilepsy_display_length = 3000; pushSequence - .Schedule(() => - { - musicController.CurrentTrack.VolumeTo(0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint); - epilepsyWarning.State.Value = Visibility.Visible; - }) + .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) + .TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint) .Delay(epilepsy_display_length) .Schedule(() => { @@ -359,10 +362,6 @@ namespace osu.Game.Screens.Play // Note that this may change if the player we load requested a re-run. ValidForResume = false; - // restore full volume immediately - there's a usually a period of silence at start of gameplay anyway. - // note that this is delayed slightly to avoid volume spikes just before push. - musicController.CurrentTrack.Delay(50).VolumeTo(1); - if (player.LoadedBeatmapSuccessfully) this.Push(player); else @@ -378,10 +377,6 @@ namespace osu.Game.Screens.Play private void cancelLoad() { - // in case the epilepsy warning is being displayed, restore full volume. - if (epilepsyWarning?.IsAlive == true) - musicController.CurrentTrack.VolumeTo(1, EpilepsyWarning.FADE_DURATION, Easing.OutQuint); - scheduledPushPlayer?.Cancel(); scheduledPushPlayer = null; } From 0a23e994e2de7a23927928e4a051d6372a7c8dfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 25 Oct 2020 23:24:14 +0100 Subject: [PATCH 4119/6909] Hide sliderend & repeat circles in traceable mod --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index d7582f3196..e1d197fb1d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using System.Collections.Generic; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; @@ -49,9 +50,16 @@ namespace osu.Game.Rulesets.Osu.Mods { case DrawableHitCircle circle: // we only want to see the approach circle - using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) - circle.CirclePiece.Hide(); + applyCirclePieceState(circle, circle.CirclePiece); + break; + case DrawableSliderTail sliderTail: + applyCirclePieceState(sliderTail); + break; + + case DrawableSliderRepeat sliderRepeat: + // show only the repeat arrow + applyCirclePieceState(sliderRepeat, sliderRepeat.CirclePiece); break; case DrawableSlider slider: @@ -61,6 +69,13 @@ namespace osu.Game.Rulesets.Osu.Mods } } + private void applyCirclePieceState(DrawableOsuHitObject hitObject, IDrawable hitCircle = null) + { + var h = hitObject.HitObject; + using (hitObject.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + (hitCircle ?? hitObject).Hide(); + } + private void applySliderState(DrawableSlider slider) { ((PlaySliderBody)slider.Body.Drawable).AccentColour = slider.AccentColour.Value.Opacity(0); From 5ef1b5dcb521a7d75f4f8dd6e7e82c934a3c195c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 25 Oct 2020 23:55:22 +0100 Subject: [PATCH 4120/6909] Remove unused locals --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index e1d197fb1d..bb2213aa31 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -39,11 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods protected void ApplyTraceableState(DrawableHitObject drawable, ArmedState state) { - if (!(drawable is DrawableOsuHitObject drawableOsu)) + if (!(drawable is DrawableOsuHitObject)) return; - var h = drawableOsu.HitObject; - //todo: expose and hide spinner background somehow switch (drawable) From 9caa7ff64dc12e6cd6f536ef7cab9a57fa339a55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 13:37:16 +0900 Subject: [PATCH 4121/6909] Remove debug endpoint --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 43bc8ff71b..97901184c7 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -80,11 +80,7 @@ namespace osu.Game.Online.Spectator } } -#if DEBUG - private const string endpoint = "http://localhost:5009/spectator"; -#else private const string endpoint = "https://spectator.ppy.sh/spectator"; -#endif private async Task connect() { From ac13a1d21708b7f54a53d818a7e5ac16f0c35936 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 14:27:55 +0900 Subject: [PATCH 4122/6909] Adjust a couple of flag scales to match previous display size --- osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs | 2 +- .../Screens/Ladder/Components/DrawableMatchTeam.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs index 119f71ebfa..cd252392ba 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/GroupTeam.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components AcronymText.Origin = Anchor.TopCentre; AcronymText.Text = team.Acronym.Value.ToUpperInvariant(); AcronymText.Font = OsuFont.Torus.With(weight: FontWeight.Bold, size: 10); - Flag.Scale = new Vector2(0.5f); + Flag.Scale = new Vector2(0.48f); InternalChildren = new Drawable[] { diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs index ba577888d8..bb1e4d2eff 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableMatchTeam.cs @@ -63,7 +63,7 @@ namespace osu.Game.Tournament.Screens.Ladder.Components this.losers = losers; Size = new Vector2(150, 40); - Flag.Scale = new Vector2(0.55f); + Flag.Scale = new Vector2(0.54f); Flag.Anchor = Flag.Origin = Anchor.CentreLeft; AcronymText.Anchor = AcronymText.Origin = Anchor.CentreLeft; From e941f2fb711d5d57308203c788a9d594e83bae0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 15:24:12 +0900 Subject: [PATCH 4123/6909] Fix playback not being smooth (and event unbinding logic) --- .../Visual/Gameplay/TestSceneSpectator.cs | 53 +++++++++++++++---- 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index f8b5d385a9..4db9d955d4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -12,7 +13,9 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; +using osu.Framework.Logging; using osu.Framework.Testing; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Online.Spectator; @@ -41,6 +44,10 @@ namespace osu.Game.Tests.Visual.Gameplay private TestReplayRecorder recorder; + private readonly ManualClock manualClock = new ManualClock(); + + private OsuSpriteText latencyDisplay; + [Resolved] private SpectatorStreamingClient streamingClient { get; set; } @@ -66,15 +73,7 @@ namespace osu.Game.Tests.Visual.Gameplay } }, true); - streamingClient.OnNewFrames += (userId, frames) => - { - foreach (var legacyFrame in frames.Frames) - { - var frame = new TestReplayFrame(); - frame.FromLegacy(legacyFrame, null, null); - replay.Frames.Add(frame); - } - }; + streamingClient.OnNewFrames += onNewFrames; Add(new GridContainer { @@ -115,6 +114,7 @@ namespace osu.Game.Tests.Visual.Gameplay { playbackManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { + Clock = new FramedClock(manualClock), ReplayInputHandler = new TestFramedReplayInputHandler(replay) { GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), @@ -143,8 +143,22 @@ namespace osu.Game.Tests.Visual.Gameplay } } }); + + Add(latencyDisplay = new OsuSpriteText()); }); + private void onNewFrames(int userId, FrameDataBundle frames) + { + Logger.Log($"Received {frames.Frames.Count()} new frames ({string.Join(',', frames.Frames.Select(f => ((int)f.Time).ToString()))})"); + + foreach (var legacyFrame in frames.Frames) + { + var frame = new TestReplayFrame(); + frame.FromLegacy(legacyFrame, null, null); + replay.Frames.Add(frame); + } + } + [Test] public void TestBasic() { @@ -153,13 +167,30 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void Update() { base.Update(); - playbackManager?.ReplayInputHandler.SetFrameFromTime(Time.Current - 100); + + double elapsed = Time.Elapsed; + double? time = playbackManager?.ReplayInputHandler.SetFrameFromTime(manualClock.CurrentTime + elapsed); + + if (time != null) + { + manualClock.CurrentTime = time.Value; + + latencyDisplay.Text = $"latency: {Time.Current - time.Value:N1}ms"; + } + else + { + manualClock.CurrentTime = Time.Current; + } } [TearDownSteps] public void TearDown() { - AddStep("stop recorder", () => recorder.Expire()); + AddStep("stop recorder", () => + { + recorder.Expire(); + streamingClient.OnNewFrames -= onNewFrames; + }); } public class TestFramedReplayInputHandler : FramedReplayInputHandler From 8508d5f8b94d04a373f49b087d739ee4c93bbf62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 15:24:28 +0900 Subject: [PATCH 4124/6909] Rename test scene to match purpose --- .../{TestSceneSpectator.cs => TestSceneSpectatorPlayback.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Gameplay/{TestSceneSpectator.cs => TestSceneSpectatorPlayback.cs} (99%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs similarity index 99% rename from osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 4db9d955d4..2656b7929c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -31,7 +31,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneSpectator : OsuManualInputManagerTestScene + public class TestSceneSpectatorPlayback : OsuManualInputManagerTestScene { protected override bool UseOnlineAPI => true; From f5dbaa9b0fab2bf2b4b805cec6d914897219ff3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 15:25:09 +0900 Subject: [PATCH 4125/6909] Only watch local user to prevent conflict between testers --- .../Gameplay/TestSceneSpectatorPlayback.cs | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 2656b7929c..e7b7950ad2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -18,6 +18,7 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Replays.Legacy; @@ -48,6 +49,9 @@ namespace osu.Game.Tests.Visual.Gameplay private OsuSpriteText latencyDisplay; + [Resolved] + private IAPIProvider api { get; set; } + [Resolved] private SpectatorStreamingClient streamingClient { get; set; } @@ -63,12 +67,20 @@ namespace osu.Game.Tests.Visual.Gameplay { case NotifyCollectionChangedAction.Add: foreach (int user in args.NewItems) - streamingClient.WatchUser(user); + { + if (user == api.LocalUser.Value.Id) + streamingClient.WatchUser(user); + } + break; case NotifyCollectionChangedAction.Remove: foreach (int user in args.OldItems) - streamingClient.StopWatchingUser(user); + { + if (user == api.LocalUser.Value.Id) + streamingClient.StopWatchingUser(user); + } + break; } }, true); From dfe07271de741378553f6fc872f0072e5a050979 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 16:31:39 +0900 Subject: [PATCH 4126/6909] Add very basic latency handling to spectator test --- .../Gameplay/TestSceneSpectatorPlayback.cs | 43 ++++++++++++++----- .../Spectator/SpectatorStreamingClient.cs | 9 ++-- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index e7b7950ad2..d27a41acd4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.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.Collections.Specialized; using System.Linq; @@ -18,6 +19,7 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.Input.Handlers; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; @@ -49,6 +51,8 @@ namespace osu.Game.Tests.Visual.Gameplay private OsuSpriteText latencyDisplay; + private TestFramedReplayInputHandler replayHandler; + [Resolved] private IAPIProvider api { get; set; } @@ -127,7 +131,7 @@ namespace osu.Game.Tests.Visual.Gameplay playbackManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { Clock = new FramedClock(manualClock), - ReplayInputHandler = new TestFramedReplayInputHandler(replay) + ReplayInputHandler = replayHandler = new TestFramedReplayInputHandler(replay) { GamefieldToScreenSpace = pos => playbackManager.ToScreenSpace(pos), }, @@ -176,22 +180,41 @@ namespace osu.Game.Tests.Visual.Gameplay { } + private double latency = SpectatorStreamingClient.TIME_BETWEEN_SENDS; + protected override void Update() { base.Update(); - double elapsed = Time.Elapsed; - double? time = playbackManager?.ReplayInputHandler.SetFrameFromTime(manualClock.CurrentTime + elapsed); + if (latencyDisplay == null) return; - if (time != null) - { - manualClock.CurrentTime = time.Value; - - latencyDisplay.Text = $"latency: {Time.Current - time.Value:N1}ms"; - } - else + // propagate initial time value + if (manualClock.CurrentTime == 0) { manualClock.CurrentTime = Time.Current; + return; + } + + if (replayHandler.NextFrame != null) + { + var lastFrame = replay.Frames.LastOrDefault(); + + // this isn't perfect as we basically can't be aware of the rate-of-send here (the streamer is not sending data when not being moved). + // in gameplay playback, the case where NextFrame is null would pause gameplay and handle this correctly; it's strictly a test limitation / best effort implementation. + if (lastFrame != null) + latency = Math.Max(latency, Time.Current - lastFrame.Time); + + latencyDisplay.Text = $"latency: {latency:N1}"; + + double proposedTime = Time.Current - latency + Time.Elapsed; + + // this will either advance by one or zero frames. + double? time = replayHandler.SetFrameFromTime(proposedTime); + + if (time == null) + return; + + manualClock.CurrentTime = time.Value; } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 97901184c7..73a18b03b2 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -24,6 +24,11 @@ namespace osu.Game.Online.Spectator { public class SpectatorStreamingClient : Component, ISpectatorClient { + /// + /// The maximum milliseconds between frame bundle sends. + /// + public const double TIME_BETWEEN_SENDS = 200; + private HubConnection connection; private readonly List watchingUsers = new List(); @@ -229,15 +234,13 @@ namespace osu.Game.Online.Spectator private Task lastSend; - private const double time_between_sends = 200; - private const int max_pending_frames = 30; protected override void Update() { base.Update(); - if (pendingFrames.Count > 0 && Time.Current - lastSendTime > time_between_sends) + if (pendingFrames.Count > 0 && Time.Current - lastSendTime > TIME_BETWEEN_SENDS) purgePendingFrames(); } From b1a88a49935c1c497113e5f4de843b72199130a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 16:34:30 +0900 Subject: [PATCH 4127/6909] Remove extra using --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index d27a41acd4..ad11ac45dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -19,7 +19,6 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; -using osu.Game.Input.Handlers; using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays; From 704f8cc4f27b81c92906332a4ac13931cf341c57 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Mon, 26 Oct 2020 18:03:04 +0100 Subject: [PATCH 4128/6909] Fix selection box wandering off into the distance --- .../Timeline/TimelineBlueprintContainer.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 84328466c3..b76032709f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -107,7 +108,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline OnDragHandled = handleScrollViaDrag }; - protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect); + protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect, this); private void handleScrollViaDrag(DragEvent e) { @@ -138,11 +139,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private class TimelineDragBox : DragBox { private Vector2 lastMouseDown; + private float? lastZoom; private float localMouseDown; - public TimelineDragBox(Action performSelect) + private readonly TimelineBlueprintContainer parent; + + public TimelineDragBox(Action performSelect, TimelineBlueprintContainer parent) : base(performSelect) { + this.parent = parent; } protected override Drawable CreateBox() => new Box @@ -158,8 +163,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { lastMouseDown = e.ScreenSpaceMouseDownPosition; localMouseDown = e.MouseDownPosition.X; + lastZoom = null; } + //Zooming the timeline shifts the coordinate system this compensates for this shift + float zoomCorrection = lastZoom.HasValue ? (parent.timeline.Zoom / lastZoom.Value) : 1; + localMouseDown *= zoomCorrection; + lastZoom = parent.timeline.Zoom; + float selection1 = localMouseDown; float selection2 = e.MousePosition.X; From ead3c195674a73f054dabccf0cfdb0d7f193bd58 Mon Sep 17 00:00:00 2001 From: Charlie Date: Mon, 26 Oct 2020 13:40:42 -0500 Subject: [PATCH 4129/6909] added function so circle is deleted when shift+right click --- .../Compose/Components/SelectionHandler.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 4caceedc5a..54e62649e1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Input.States; using osu.Game.Audio; using osu.Game.Graphics; @@ -32,6 +33,8 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { + private bool shiftPressed; + public IEnumerable SelectedBlueprints => selectedBlueprints; private readonly List selectedBlueprints; @@ -164,6 +167,17 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any s could be reversed. public virtual bool HandleReverse() => false; + protected override bool OnKeyDown(KeyDownEvent e) + { + shiftPressed = e.ShiftPressed; + return false; + } + + protected override void OnKeyUp(KeyUpEvent e) + { + shiftPressed = e.ShiftPressed; + } + public bool OnPressed(PlatformAction action) { switch (action.ActionMethod) @@ -455,6 +469,12 @@ namespace osu.Game.Screens.Edit.Compose.Components { get { + if (shiftPressed) + { + deleteSelected(); + return null; + } + if (!selectedBlueprints.Any(b => b.IsHovered)) return Array.Empty(); From 123967056693f0f4b34d0eb04dc103ec39a33548 Mon Sep 17 00:00:00 2001 From: Charlie Date: Mon, 26 Oct 2020 14:28:53 -0500 Subject: [PATCH 4130/6909] moved right click shift delete functionality to HandleSelectionRequested + reduced func size --- .../Compose/Components/SelectionHandler.cs | 35 +++++++++---------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 54e62649e1..eeeacce4a7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -25,6 +25,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { @@ -33,7 +34,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { - private bool shiftPressed; public IEnumerable SelectedBlueprints => selectedBlueprints; private readonly List selectedBlueprints; @@ -167,17 +167,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether any s could be reversed. public virtual bool HandleReverse() => false; - protected override bool OnKeyDown(KeyDownEvent e) - { - shiftPressed = e.ShiftPressed; - return false; - } - - protected override void OnKeyUp(KeyUpEvent e) - { - shiftPressed = e.ShiftPressed; - } - public bool OnPressed(PlatformAction action) { switch (action.ActionMethod) @@ -237,6 +226,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. /// The input state at the point of selection. internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) + { + shiftClickDeleteCheck(blueprint, state); + multiSelectionHandler(blueprint, state); + + } + + private void multiSelectionHandler(SelectionBlueprint blueprint, InputState state) { if (state.Keyboard.ControlPressed) { @@ -255,6 +251,15 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + private void shiftClickDeleteCheck(SelectionBlueprint blueprint, InputState state) + { + if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) + { + EditorBeatmap.Remove(blueprint.HitObject); + return; + } + } + private void deleteSelected() { EditorBeatmap.RemoveRange(selectedBlueprints.Select(b => b.HitObject)); @@ -469,12 +474,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { get { - if (shiftPressed) - { - deleteSelected(); - return null; - } - if (!selectedBlueprints.Any(b => b.IsHovered)) return Array.Empty(); From ccaf6560ec619004ddd67b57dd62674f7b6520db Mon Sep 17 00:00:00 2001 From: Charlie Date: Mon, 26 Oct 2020 14:30:37 -0500 Subject: [PATCH 4131/6909] formatting --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index eeeacce4a7..f4b98c66b1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -34,7 +34,6 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class SelectionHandler : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { - public IEnumerable SelectedBlueprints => selectedBlueprints; private readonly List selectedBlueprints; @@ -229,7 +228,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { shiftClickDeleteCheck(blueprint, state); multiSelectionHandler(blueprint, state); - } private void multiSelectionHandler(SelectionBlueprint blueprint, InputState state) From 255bb9d10092ede439c0d8c5f71b7ca707880a37 Mon Sep 17 00:00:00 2001 From: Charlie Date: Mon, 26 Oct 2020 14:52:59 -0500 Subject: [PATCH 4132/6909] fixed issue with returns --- .../Edit/Compose/Components/SelectionHandler.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index f4b98c66b1..f0a9e69321 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -226,11 +226,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The input state at the point of selection. internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) { - shiftClickDeleteCheck(blueprint, state); - multiSelectionHandler(blueprint, state); + if (!shiftClickDeleteCheck(blueprint, state)) + handleMultiSelection(blueprint, state); } - private void multiSelectionHandler(SelectionBlueprint blueprint, InputState state) + private void handleMultiSelection(SelectionBlueprint blueprint, InputState state) { if (state.Keyboard.ControlPressed) { @@ -249,13 +249,14 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - private void shiftClickDeleteCheck(SelectionBlueprint blueprint, InputState state) + private bool shiftClickDeleteCheck(SelectionBlueprint blueprint, InputState state) { if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) { EditorBeatmap.Remove(blueprint.HitObject); - return; + return true; } + return false; } private void deleteSelected() From 3f8c4c57d0682441a69bfed1c5a69bda07ef0979 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 26 Oct 2020 22:16:28 +0100 Subject: [PATCH 4133/6909] Fix code style issues & restructure --- osu.Game/Rulesets/Edit/SelectionBlueprint.cs | 5 +++ .../Compose/Components/SelectionHandler.cs | 43 ++++++------------- 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index 4abdbfc244..f3816f6218 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -120,6 +120,11 @@ namespace osu.Game.Rulesets.Edit /// public void Deselect() => State = SelectionState.NotSelected; + /// + /// Toggles the selection state of this . + /// + public void ToggleSelection() => State = IsSelected ? SelectionState.NotSelected : SelectionState.Selected; + public bool IsSelected => State == SelectionState.Selected; /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index f0a9e69321..9cddb69d0b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Framework.Input.States; using osu.Game.Audio; using osu.Game.Graphics; @@ -225,38 +224,22 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The blueprint. /// The input state at the point of selection. internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) - { - if (!shiftClickDeleteCheck(blueprint, state)) - handleMultiSelection(blueprint, state); - } - - private void handleMultiSelection(SelectionBlueprint blueprint, InputState state) - { - if (state.Keyboard.ControlPressed) - { - if (blueprint.IsSelected) - blueprint.Deselect(); - else - blueprint.Select(); - } - else - { - if (blueprint.IsSelected) - return; - - DeselectAll?.Invoke(); - blueprint.Select(); - } - } - - private bool shiftClickDeleteCheck(SelectionBlueprint blueprint, InputState state) { if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) - { EditorBeatmap.Remove(blueprint.HitObject); - return true; - } - return false; + else if (state.Keyboard.ControlPressed) + blueprint.ToggleSelection(); + else + ensureSelected(blueprint); + } + + private void ensureSelected(SelectionBlueprint blueprint) + { + if (blueprint.IsSelected) + return; + + DeselectAll?.Invoke(); + blueprint.Select(); } private void deleteSelected() From 7392876b5f71a5bfb4b22fcabb0a0e38cef5a368 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 27 Oct 2020 00:05:03 +0100 Subject: [PATCH 4134/6909] Fix mania crashing due to spectator client handling frames with unconverted beatmap --- .../Visual/Gameplay/TestSceneReplayRecorder.cs | 6 ++++++ .../Visual/Gameplay/TestSceneReplayRecording.cs | 6 ++++++ .../Visual/Gameplay/TestSceneSpectatorPlayback.cs | 4 ++++ .../Online/Spectator/SpectatorStreamingClient.cs | 14 +++++++++----- osu.Game/Rulesets/UI/ReplayRecorder.cs | 7 +++---- 5 files changed, 28 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index e964d2a40e..47dd47959d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -12,11 +13,13 @@ using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; using osu.Framework.Testing; using osu.Framework.Threading; +using osu.Game.Beatmaps; 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.Screens.Play; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; @@ -33,6 +36,9 @@ namespace osu.Game.Tests.Visual.Gameplay private TestReplayRecorder recorder; + [Cached] + private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index c0f99db85d..6872b6a669 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -2,17 +2,20 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Framework.Input.StateChanges; +using osu.Game.Beatmaps; 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.Screens.Play; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; @@ -25,6 +28,9 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly TestRulesetInputManager recordingManager; + [Cached] + private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + public TestSceneReplayRecording() { Replay replay = new Replay(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index ad11ac45dd..1d8231cce7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -27,6 +27,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.UI; +using osu.Game.Screens.Play; using osu.Game.Tests.Visual.UserInterface; using osuTK; using osuTK.Graphics; @@ -58,6 +59,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private SpectatorStreamingClient streamingClient { get; set; } + [Cached] + private GameplayBeatmap gameplayBeatmap = new GameplayBeatmap(new Beatmap()); + [SetUp] public void SetUp() => Schedule(() => { diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 73a18b03b2..7059818b4e 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using JetBrains.Annotations; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; @@ -19,6 +20,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; +using osu.Game.Screens.Play; namespace osu.Game.Online.Spectator { @@ -44,8 +46,8 @@ namespace osu.Game.Online.Spectator [Resolved] private IAPIProvider api { get; set; } - [Resolved] - private IBindable beatmap { get; set; } + [CanBeNull] + private IBeatmap currentBeatmap; [Resolved] private IBindable ruleset { get; set; } @@ -167,7 +169,7 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } - public void BeginPlaying() + public void BeginPlaying(GameplayBeatmap beatmap) { if (isPlaying) throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); @@ -175,10 +177,11 @@ namespace osu.Game.Online.Spectator isPlaying = true; // transfer state at point of beginning play - currentState.BeatmapID = beatmap.Value.BeatmapInfo.OnlineBeatmapID; + currentState.BeatmapID = beatmap.BeatmapInfo.OnlineBeatmapID; currentState.RulesetID = ruleset.Value.ID; currentState.Mods = mods.Value.Select(m => new APIMod(m)); + currentBeatmap = beatmap.PlayableBeatmap; beginPlaying(); } @@ -201,6 +204,7 @@ namespace osu.Game.Online.Spectator public void EndPlaying() { isPlaying = false; + currentBeatmap = null; if (!isConnected) return; @@ -247,7 +251,7 @@ namespace osu.Game.Online.Spectator public void HandleFrame(ReplayFrame frame) { if (frame is IConvertibleReplayFrame convertible) - pendingFrames.Enqueue(convertible.ToLegacy(beatmap.Value.Beatmap)); + pendingFrames.Enqueue(convertible.ToLegacy(currentBeatmap)); if (pendingFrames.Count > max_pending_frames) purgePendingFrames(); diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index a84b4f4ba8..1438ebd37a 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -5,15 +5,14 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; -using osu.Game.Beatmaps; using osu.Game.Online.Spectator; using osu.Game.Replays; using osu.Game.Rulesets.Replays; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.UI @@ -33,7 +32,7 @@ namespace osu.Game.Rulesets.UI private SpectatorStreamingClient spectatorStreaming { get; set; } [Resolved] - private IBindable beatmap { get; set; } + private GameplayBeatmap gameplayBeatmap { get; set; } protected ReplayRecorder(Replay target) { @@ -50,7 +49,7 @@ namespace osu.Game.Rulesets.UI inputManager = GetContainingInputManager(); - spectatorStreaming?.BeginPlaying(); + spectatorStreaming?.BeginPlaying(gameplayBeatmap); } protected override void Dispose(bool isDisposing) From 68719bb23df43e7d18cd429042f05f240e432495 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 10:59:24 +0900 Subject: [PATCH 4135/6909] Rename other variables to match --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 7059818b4e..5a41316f31 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -50,10 +50,10 @@ namespace osu.Game.Online.Spectator private IBeatmap currentBeatmap; [Resolved] - private IBindable ruleset { get; set; } + private IBindable currentRuleset { get; set; } [Resolved] - private IBindable> mods { get; set; } + private IBindable> currentMods { get; set; } private readonly SpectatorState currentState = new SpectatorState(); @@ -178,8 +178,8 @@ namespace osu.Game.Online.Spectator // transfer state at point of beginning play currentState.BeatmapID = beatmap.BeatmapInfo.OnlineBeatmapID; - currentState.RulesetID = ruleset.Value.ID; - currentState.Mods = mods.Value.Select(m => new APIMod(m)); + currentState.RulesetID = currentRuleset.Value.ID; + currentState.Mods = currentMods.Value.Select(m => new APIMod(m)); currentBeatmap = beatmap.PlayableBeatmap; beginPlaying(); From e1f578c590788235640154dd825df2a1ade4e492 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 12:28:10 +0900 Subject: [PATCH 4136/6909] Change editor timing screen seek behaviour to only occur on clicking table rows Previously it would react to any selection changed event, which could in lude time changes (which is done by removing then adding the ControlPointGroup). Closes #10590. --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 9 ++++++++- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 14 -------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index c8982b819a..64f9526816 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -177,6 +177,9 @@ namespace osu.Game.Screens.Edit.Timing private readonly Box hoveredBackground; + [Resolved] + private EditorClock clock { get; set; } + [Resolved] private Bindable selectedGroup { get; set; } @@ -200,7 +203,11 @@ namespace osu.Game.Screens.Edit.Timing }, }; - Action = () => selectedGroup.Value = controlGroup; + Action = () => + { + selectedGroup.Value = controlGroup; + clock.SeekTo(controlGroup.Time); + }; } private Color4 colourHover; diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 0796097186..f511382cde 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -22,9 +22,6 @@ namespace osu.Game.Screens.Edit.Timing [Cached] private Bindable selectedGroup = new Bindable(); - [Resolved] - private EditorClock clock { get; set; } - public TimingScreen() : base(EditorScreenMode.Timing) { @@ -48,17 +45,6 @@ namespace osu.Game.Screens.Edit.Timing } }; - protected override void LoadComplete() - { - base.LoadComplete(); - - selectedGroup.BindValueChanged(selected => - { - if (selected.NewValue != null) - clock.SeekTo(selected.NewValue.Time); - }); - } - protected override void OnTimelineLoaded(TimelineArea timelineArea) { base.OnTimelineLoaded(timelineArea); From 27c1a4c4d3693dbc1f22b2c9427646c8ccf01977 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 12:53:54 +0900 Subject: [PATCH 4137/6909] Move right-click deletion logic to be handled at a SelectionBlueprint level --- .../Sliders/SliderSelectionBlueprint.cs | 4 ++-- osu.Game/Rulesets/Edit/SelectionBlueprint.cs | 17 +++++++++++++++++ .../Edit/Compose/Components/SelectionHandler.cs | 4 +--- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index d3fb5defae..ca9ec886d5 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { case MouseButton.Right: rightClickPosition = e.MouseDownPosition; - return false; // Allow right click to be handled by context menu + break; case MouseButton.Left when e.ControlPressed && IsSelected: placementControlPointIndex = addControlPoint(e.MousePosition); return true; // Stop input from being handled and modifying the selection } - return false; + return base.OnMouseDown(e); } private int? placementControlPointIndex; diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index f3816f6218..87ef7e647f 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -8,10 +8,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Screens.Edit; using osuTK; +using osuTK.Input; namespace osu.Game.Rulesets.Edit { @@ -52,6 +55,20 @@ namespace osu.Game.Rulesets.Edit updateState(); } + [Resolved] + private EditorBeatmap editorBeatmap { get; set; } + + protected override bool OnMouseDown(MouseDownEvent e) + { + if (e.CurrentState.Keyboard.ShiftPressed && e.IsPressed(MouseButton.Right)) + { + editorBeatmap.Remove(HitObject); + return true; + } + + return base.OnMouseDown(e); + } + private SelectionState state; public event Action StateChanged; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 9cddb69d0b..036edbeb84 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -225,9 +225,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The input state at the point of selection. internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) { - if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) - EditorBeatmap.Remove(blueprint.HitObject); - else if (state.Keyboard.ControlPressed) + if (state.Keyboard.ControlPressed) blueprint.ToggleSelection(); else ensureSelected(blueprint); From 3c2e2f29bc8eb190daab56fea86ab00a1925871b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 13:17:44 +0900 Subject: [PATCH 4138/6909] Remove unused using statement --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 036edbeb84..24f88bf36d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -24,7 +24,6 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; -using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { From 6853da459dde1a1f90a0362113cd3121325d2ed7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 13:54:33 +0900 Subject: [PATCH 4139/6909] Move sample pausing logic out of FrameStabilityContainer --- .../TestSceneGameplaySamplePlayback.cs | 2 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 4 +- .../Rulesets/UI/FrameStabilityContainer.cs | 49 +++---------------- osu.Game/Rulesets/UI/FrameStableClock.cs | 28 +++++++++++ osu.Game/Screens/Play/Player.cs | 14 ++++-- 5 files changed, 47 insertions(+), 50 deletions(-) create mode 100644 osu.Game/Rulesets/UI/FrameStableClock.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index 6e505b16c2..e2b867bfb2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("get variables", () => { - gameplayClock = Player.ChildrenOfType().First(); + gameplayClock = Player as ISamplePlaybackDisabler; slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); samples = slider.ChildrenOfType().ToArray(); }); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 50e9a93e22..3f967d489b 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.UI public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; - public override GameplayClock FrameStableClock => frameStabilityContainer.GameplayClock; + public override FrameStableClock FrameStableClock => frameStabilityContainer.FrameStableClock; private bool frameStablePlayback = true; @@ -404,7 +404,7 @@ namespace osu.Game.Rulesets.UI /// /// The frame-stable clock which is being used for playfield display. /// - public abstract GameplayClock FrameStableClock { get; } + public abstract FrameStableClock FrameStableClock { get; } /// ~ /// The associated ruleset. diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index e4a3a2fe3d..4ea5b514c9 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -2,10 +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; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; @@ -18,11 +15,8 @@ namespace osu.Game.Rulesets.UI /// A container which consumes a parent gameplay clock and standardises frame counts for children. /// Will ensure a minimum of 50 frames per clock second is maintained, regardless of any system lag or seeks. /// - [Cached(typeof(ISamplePlaybackDisabler))] - public class FrameStabilityContainer : Container, IHasReplayHandler, ISamplePlaybackDisabler + public class FrameStabilityContainer : Container, IHasReplayHandler { - private readonly Bindable samplePlaybackDisabled = new Bindable(); - private readonly double gameplayStartTime; /// @@ -35,16 +29,14 @@ namespace osu.Game.Rulesets.UI /// internal bool FrameStablePlayback = true; - public GameplayClock GameplayClock => stabilityGameplayClock; - [Cached(typeof(GameplayClock))] - private readonly StabilityGameplayClock stabilityGameplayClock; + public readonly FrameStableClock FrameStableClock; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) { RelativeSizeAxes = Axes.Both; - stabilityGameplayClock = new StabilityGameplayClock(framedClock = new FramedClock(manualClock = new ManualClock())); + FrameStableClock = new FrameStableClock(framedClock = new FramedClock(manualClock = new ManualClock())); this.gameplayStartTime = gameplayStartTime; } @@ -65,12 +57,9 @@ namespace osu.Game.Rulesets.UI { if (clock != null) { - parentGameplayClock = stabilityGameplayClock.ParentGameplayClock = clock; - GameplayClock.IsPaused.BindTo(clock.IsPaused); + parentGameplayClock = FrameStableClock.ParentGameplayClock = clock; + FrameStableClock.IsPaused.BindTo(clock.IsPaused); } - - // this is a bit temporary. should really be done inside of GameplayClock (but requires large structural changes). - stabilityGameplayClock.ParentSampleDisabler = sampleDisabler; } protected override void LoadComplete() @@ -102,9 +91,7 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { requireMoreUpdateLoops = true; - validState = !GameplayClock.IsPaused.Value; - - samplePlaybackDisabled.Value = stabilityGameplayClock.ShouldDisableSamplePlayback; + validState = !FrameStableClock.IsPaused.Value; int loops = 0; @@ -222,32 +209,10 @@ namespace osu.Game.Rulesets.UI } else { - Clock = GameplayClock; + Clock = FrameStableClock; } } public ReplayInputHandler ReplayInputHandler { get; set; } - - IBindable ISamplePlaybackDisabler.SamplePlaybackDisabled => samplePlaybackDisabled; - - private class StabilityGameplayClock : GameplayClock - { - public GameplayClock ParentGameplayClock; - - public ISamplePlaybackDisabler ParentSampleDisabler; - - public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); - - public StabilityGameplayClock(FramedClock underlyingClock) - : base(underlyingClock) - { - } - - public override bool ShouldDisableSamplePlayback => - // handle the case where playback is catching up to real-time. - base.ShouldDisableSamplePlayback - || ParentSampleDisabler?.SamplePlaybackDisabled.Value == true - || (ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200); - } } } diff --git a/osu.Game/Rulesets/UI/FrameStableClock.cs b/osu.Game/Rulesets/UI/FrameStableClock.cs new file mode 100644 index 0000000000..5c81ce3093 --- /dev/null +++ b/osu.Game/Rulesets/UI/FrameStableClock.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 osu.Framework.Bindables; +using osu.Framework.Timing; +using osu.Game.Screens.Play; + +namespace osu.Game.Rulesets.UI +{ + public class FrameStableClock : GameplayClock + { + public GameplayClock ParentGameplayClock; + + public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); + + public FrameStableClock(FramedClock underlyingClock) + : base(underlyingClock) + { + } + + public override bool ShouldDisableSamplePlayback => + // handle the case where playback is catching up to real-time. + base.ShouldDisableSamplePlayback || (ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200); + } +} diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 6b2d2f40d0..b0923ed4c8 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -238,11 +238,8 @@ namespace osu.Game.Screens.Play skipOverlay.Hide(); } - DrawableRuleset.IsPaused.BindValueChanged(paused => - { - updateGameplayState(); - samplePlaybackDisabled.Value = paused.NewValue; - }); + DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState()); + DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); @@ -370,6 +367,13 @@ namespace osu.Game.Screens.Play } }; + protected override void Update() + { + base.Update(); + + samplePlaybackDisabled.Value = DrawableRuleset.FrameStableClock.ShouldDisableSamplePlayback; + } + private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { updateGameplayState(); From 9cfb81589e796d7153d380178fd413c7a794810f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 14:10:12 +0900 Subject: [PATCH 4140/6909] Use bindable flow instead --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 4 +-- .../Rulesets/UI/FrameStabilityContainer.cs | 35 +++++++++++++++---- osu.Game/Rulesets/UI/FrameStableClock.cs | 19 ++-------- osu.Game/Screens/Play/GameplayClock.cs | 5 --- osu.Game/Screens/Play/Player.cs | 20 ++++++----- 6 files changed, 46 insertions(+), 39 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 2263e2b2f4..6e7025847a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Mods private OsuInputManager inputManager; - private GameplayClock gameplayClock; + private IFrameStableClock gameplayClock; private List replayFrames; diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 3f967d489b..f6cf836fe7 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.UI public override Container FrameStableComponents { get; } = new Container { RelativeSizeAxes = Axes.Both }; - public override FrameStableClock FrameStableClock => frameStabilityContainer.FrameStableClock; + public override IFrameStableClock FrameStableClock => frameStabilityContainer.FrameStableClock; private bool frameStablePlayback = true; @@ -404,7 +404,7 @@ namespace osu.Game.Rulesets.UI /// /// The frame-stable clock which is being used for playfield display. /// - public abstract FrameStableClock FrameStableClock { get; } + public abstract IFrameStableClock FrameStableClock { get; } /// ~ /// The associated ruleset. diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 4ea5b514c9..9ffbce991c 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Timing; @@ -29,14 +32,16 @@ namespace osu.Game.Rulesets.UI /// internal bool FrameStablePlayback = true; + public IFrameStableClock FrameStableClock => frameStableClock; + [Cached(typeof(GameplayClock))] - public readonly FrameStableClock FrameStableClock; + private readonly FrameStabilityClock frameStableClock; public FrameStabilityContainer(double gameplayStartTime = double.MinValue) { RelativeSizeAxes = Axes.Both; - FrameStableClock = new FrameStableClock(framedClock = new FramedClock(manualClock = new ManualClock())); + frameStableClock = new FrameStabilityClock(framedClock = new FramedClock(manualClock = new ManualClock())); this.gameplayStartTime = gameplayStartTime; } @@ -57,8 +62,8 @@ namespace osu.Game.Rulesets.UI { if (clock != null) { - parentGameplayClock = FrameStableClock.ParentGameplayClock = clock; - FrameStableClock.IsPaused.BindTo(clock.IsPaused); + parentGameplayClock = frameStableClock.ParentGameplayClock = clock; + frameStableClock.IsPaused.BindTo(clock.IsPaused); } } @@ -91,7 +96,7 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { requireMoreUpdateLoops = true; - validState = !FrameStableClock.IsPaused.Value; + validState = !frameStableClock.IsPaused.Value; int loops = 0; @@ -194,6 +199,8 @@ namespace osu.Game.Rulesets.UI requireMoreUpdateLoops |= manualClock.CurrentTime != parentGameplayClock.CurrentTime; + frameStableClock.IsCatchingUp.Value = requireMoreUpdateLoops; + // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed framedClock.ProcessFrame(); @@ -209,10 +216,26 @@ namespace osu.Game.Rulesets.UI } else { - Clock = FrameStableClock; + Clock = frameStableClock; } } public ReplayInputHandler ReplayInputHandler { get; set; } + + private class FrameStabilityClock : GameplayClock, IFrameStableClock + { + public GameplayClock ParentGameplayClock; + + public readonly Bindable IsCatchingUp = new Bindable(); + + public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); + + public FrameStabilityClock(FramedClock underlyingClock) + : base(underlyingClock) + { + } + + IBindable IFrameStableClock.IsCatchingUp => IsCatchingUp; + } } } diff --git a/osu.Game/Rulesets/UI/FrameStableClock.cs b/osu.Game/Rulesets/UI/FrameStableClock.cs index 5c81ce3093..d888eefdc6 100644 --- a/osu.Game/Rulesets/UI/FrameStableClock.cs +++ b/osu.Game/Rulesets/UI/FrameStableClock.cs @@ -1,28 +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.Bindables; using osu.Framework.Timing; -using osu.Game.Screens.Play; namespace osu.Game.Rulesets.UI { - public class FrameStableClock : GameplayClock + public interface IFrameStableClock : IFrameBasedClock { - public GameplayClock ParentGameplayClock; - - public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); - - public FrameStableClock(FramedClock underlyingClock) - : base(underlyingClock) - { - } - - public override bool ShouldDisableSamplePlayback => - // handle the case where playback is catching up to real-time. - base.ShouldDisableSamplePlayback || (ParentGameplayClock != null && Math.Abs(CurrentTime - ParentGameplayClock.CurrentTime) > 200); + IBindable IsCatchingUp { get; } } } diff --git a/osu.Game/Screens/Play/GameplayClock.cs b/osu.Game/Screens/Play/GameplayClock.cs index 4d0872e5bb..db4b5d300b 100644 --- a/osu.Game/Screens/Play/GameplayClock.cs +++ b/osu.Game/Screens/Play/GameplayClock.cs @@ -61,11 +61,6 @@ namespace osu.Game.Screens.Play public bool IsRunning => underlyingClock.IsRunning; - /// - /// Whether nested samples supporting the interface should be paused. - /// - public virtual bool ShouldDisableSamplePlayback => IsPaused.Value; - public void ProcessFrame() { // intentionally not updating the underlying clock (handled externally). diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b0923ed4c8..3c0c643413 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -238,7 +238,13 @@ namespace osu.Game.Screens.Play skipOverlay.Hide(); } - DrawableRuleset.IsPaused.BindValueChanged(_ => updateGameplayState()); + DrawableRuleset.IsPaused.BindValueChanged(paused => + { + updateGameplayState(); + updateSampleDisabledState(); + }); + + DrawableRuleset.FrameStableClock.IsCatchingUp.BindValueChanged(_ => updateSampleDisabledState()); DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState()); @@ -367,13 +373,6 @@ namespace osu.Game.Screens.Play } }; - protected override void Update() - { - base.Update(); - - samplePlaybackDisabled.Value = DrawableRuleset.FrameStableClock.ShouldDisableSamplePlayback; - } - private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { updateGameplayState(); @@ -388,6 +387,11 @@ namespace osu.Game.Screens.Play LocalUserPlaying.Value = inGameplay; } + private void updateSampleDisabledState() + { + samplePlaybackDisabled.Value = DrawableRuleset.FrameStableClock.IsCatchingUp.Value || GameplayClockContainer.GameplayClock.IsPaused.Value; + } + private void updatePauseOnFocusLostState() => HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost && !DrawableRuleset.HasReplayLoaded.Value From 09087faf3b78908c912b1384014782705bda1228 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 14:23:24 +0900 Subject: [PATCH 4141/6909] Fix non-matching filename --- .../Rulesets/UI/{FrameStableClock.cs => IFrameStableClock.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game/Rulesets/UI/{FrameStableClock.cs => IFrameStableClock.cs} (100%) diff --git a/osu.Game/Rulesets/UI/FrameStableClock.cs b/osu.Game/Rulesets/UI/IFrameStableClock.cs similarity index 100% rename from osu.Game/Rulesets/UI/FrameStableClock.cs rename to osu.Game/Rulesets/UI/IFrameStableClock.cs From 606a4304a85a8c8299a5de5dde08acdd9ac26d06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 14:33:12 +0900 Subject: [PATCH 4142/6909] Remove unused usings --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 1 - .../Visual/Gameplay/TestSceneGameplaySamplePlayback.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 6e7025847a..8c819c4773 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -11,7 +11,6 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; -using osu.Game.Screens.Play; namespace osu.Game.Rulesets.Osu.Mods { diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index e2b867bfb2..af00322cbc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -9,7 +9,6 @@ using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Skinning; From e0ad005cc1c49cc9e9389d4e10df3a65404d7df7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 14:31:56 +0900 Subject: [PATCH 4143/6909] Move editor sample disabling logic to editor class (and support screen switching) --- osu.Game/Screens/Edit/Editor.cs | 78 +++++++++++++++++----------- osu.Game/Screens/Edit/EditorClock.cs | 17 +++--- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c3560dff38..25ebd55f81 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -43,8 +43,9 @@ using osuTK.Input; namespace osu.Game.Screens.Edit { [Cached(typeof(IBeatSnapProvider))] + [Cached(typeof(ISamplePlaybackDisabler))] [Cached] - public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider + public class Editor : ScreenWithBeatmapBackground, IKeyBindingHandler, IKeyBindingHandler, IBeatSnapProvider, ISamplePlaybackDisabler { public override float BackgroundParallaxAmount => 0.1f; @@ -64,6 +65,10 @@ namespace osu.Game.Screens.Edit [Resolved(canBeNull: true)] private DialogOverlay dialogOverlay { get; set; } + public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; + + private readonly Bindable samplePlaybackDisabled = new Bindable(); + private bool exitConfirmed; private string lastSavedHash; @@ -109,9 +114,10 @@ namespace osu.Game.Screens.Edit UpdateClockSource(); dependencies.CacheAs(clock); - dependencies.CacheAs(clock); AddInternal(clock); + clock.SeekingOrStopped.BindValueChanged(_ => updateSampleDisabledState()); + // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); @@ -557,40 +563,52 @@ namespace osu.Game.Screens.Edit .ScaleTo(0.98f, 200, Easing.OutQuint) .FadeOut(200, Easing.OutQuint); - if ((currentScreen = screenContainer.SingleOrDefault(s => s.Type == e.NewValue)) != null) + try { - screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0); + if ((currentScreen = screenContainer.SingleOrDefault(s => s.Type == e.NewValue)) != null) + { + screenContainer.ChangeChildDepth(currentScreen, lastScreen?.Depth + 1 ?? 0); - currentScreen - .ScaleTo(1, 200, Easing.OutQuint) - .FadeIn(200, Easing.OutQuint); - return; + currentScreen + .ScaleTo(1, 200, Easing.OutQuint) + .FadeIn(200, Easing.OutQuint); + return; + } + + switch (e.NewValue) + { + case EditorScreenMode.SongSetup: + currentScreen = new SetupScreen(); + break; + + case EditorScreenMode.Compose: + currentScreen = new ComposeScreen(); + break; + + case EditorScreenMode.Design: + currentScreen = new DesignScreen(); + break; + + case EditorScreenMode.Timing: + currentScreen = new TimingScreen(); + break; + } + + LoadComponentAsync(currentScreen, newScreen => + { + if (newScreen == currentScreen) + screenContainer.Add(newScreen); + }); } - - switch (e.NewValue) + finally { - case EditorScreenMode.SongSetup: - currentScreen = new SetupScreen(); - break; - - case EditorScreenMode.Compose: - currentScreen = new ComposeScreen(); - break; - - case EditorScreenMode.Design: - currentScreen = new DesignScreen(); - break; - - case EditorScreenMode.Timing: - currentScreen = new TimingScreen(); - break; + updateSampleDisabledState(); } + } - LoadComponentAsync(currentScreen, newScreen => - { - if (newScreen == currentScreen) - screenContainer.Add(newScreen); - }); + private void updateSampleDisabledState() + { + samplePlaybackDisabled.Value = clock.SeekingOrStopped.Value || !(currentScreen is ComposeScreen); } private void seek(UIEvent e, int direction) diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 64ed34f5ec..949636f695 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -11,14 +11,13 @@ using osu.Framework.Timing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Screens.Play; 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 : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock, ISamplePlaybackDisabler + public class EditorClock : Component, IFrameBasedClock, IAdjustableClock, ISourceChangeableClock { public IBindable Track => track; @@ -32,9 +31,9 @@ namespace osu.Game.Screens.Edit private readonly DecoupleableInterpolatingFramedClock underlyingClock; - public IBindable SamplePlaybackDisabled => samplePlaybackDisabled; + public IBindable SeekingOrStopped => seekingOrStopped; - private readonly Bindable samplePlaybackDisabled = new Bindable(); + private readonly Bindable seekingOrStopped = new Bindable(true); public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor) @@ -171,13 +170,13 @@ namespace osu.Game.Screens.Edit public void Stop() { - samplePlaybackDisabled.Value = true; + seekingOrStopped.Value = true; underlyingClock.Stop(); } public bool Seek(double position) { - samplePlaybackDisabled.Value = true; + seekingOrStopped.Value = true; ClearTransforms(); return underlyingClock.Seek(position); @@ -228,7 +227,7 @@ namespace osu.Game.Screens.Edit private void updateSeekingState() { - if (samplePlaybackDisabled.Value) + if (seekingOrStopped.Value) { if (track.Value?.IsRunning != true) { @@ -240,13 +239,13 @@ namespace osu.Game.Screens.Edit // we are either running a seek tween or doing an immediate seek. // in the case of an immediate seek the seeking bool will be set to false after one update. // this allows for silencing hit sounds and the likes. - samplePlaybackDisabled.Value = Transforms.Any(); + seekingOrStopped.Value = Transforms.Any(); } } public void SeekTo(double seekDestination) { - samplePlaybackDisabled.Value = true; + seekingOrStopped.Value = true; if (IsRunning) Seek(seekDestination); From 03d566da356cf96199766229d05ab724a0709ac4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 14:35:12 +0900 Subject: [PATCH 4144/6909] Rename test variable and remove unncessary cast --- .../Visual/Gameplay/TestSceneGameplaySamplePlayback.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index af00322cbc..b86cb69eb4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -21,11 +21,11 @@ namespace osu.Game.Tests.Visual.Gameplay { DrawableSlider slider = null; DrawableSample[] samples = null; - ISamplePlaybackDisabler gameplayClock = null; + ISamplePlaybackDisabler sampleDisabler = null; AddStep("get variables", () => { - gameplayClock = Player as ISamplePlaybackDisabler; + sampleDisabler = Player; slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); samples = slider.ChildrenOfType().ToArray(); }); @@ -42,16 +42,16 @@ namespace osu.Game.Tests.Visual.Gameplay return true; }); - AddAssert("sample playback disabled", () => gameplayClock.SamplePlaybackDisabled.Value); + AddAssert("sample playback disabled", () => sampleDisabler.SamplePlaybackDisabled.Value); // because we are in frame stable context, it's quite likely that not all samples are "played" at this point. // the important thing is that at least one started, and that sample has since stopped. AddAssert("all looping samples stopped immediately", () => allStopped(allLoopingSounds)); AddUntilStep("all samples stopped eventually", () => allStopped(allSounds)); - AddAssert("sample playback still disabled", () => gameplayClock.SamplePlaybackDisabled.Value); + AddAssert("sample playback still disabled", () => sampleDisabler.SamplePlaybackDisabled.Value); - AddUntilStep("seek finished, sample playback enabled", () => !gameplayClock.SamplePlaybackDisabled.Value); + AddUntilStep("seek finished, sample playback enabled", () => !sampleDisabler.SamplePlaybackDisabled.Value); AddUntilStep("any sample is playing", () => Player.ChildrenOfType().Any(s => s.IsPlaying)); } From b8beac27cee305d5c8003e66af02993dd9a4d956 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 17:14:41 +0900 Subject: [PATCH 4145/6909] Use previous logic for catching-up mode --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 9ffbce991c..28b7975a89 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -197,9 +197,11 @@ namespace osu.Game.Rulesets.UI manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; manualClock.IsRunning = parentGameplayClock.IsRunning; - requireMoreUpdateLoops |= manualClock.CurrentTime != parentGameplayClock.CurrentTime; + double timeBehind = Math.Abs(manualClock.CurrentTime - parentGameplayClock.CurrentTime); - frameStableClock.IsCatchingUp.Value = requireMoreUpdateLoops; + requireMoreUpdateLoops |= timeBehind != 0; + + frameStableClock.IsCatchingUp.Value = timeBehind > 200; // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed From 5fd97bd0433048b1370b3c8fbee51476d07af984 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 19:47:39 +0900 Subject: [PATCH 4146/6909] Add basic spectator screen --- .../Visual/Gameplay/TestSceneSpectator.cs | 20 +++++++++++ osu.Game/Screens/Play/Spectator.cs | 36 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs create mode 100644 osu.Game/Screens/Play/Spectator.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs new file mode 100644 index 0000000000..25c30dbc5a --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.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 NUnit.Framework; +using osu.Game.Screens.Play; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSpectator : ScreenTestScene + { + private readonly User user = new User { Id = 1234, Username = "Test user" }; + + [Test] + public void TestSpectating() + { + AddStep("load screen", () => LoadScreen(new Spectator(user))); + } + } +} diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs new file mode 100644 index 0000000000..ceac6c737f --- /dev/null +++ b/osu.Game/Screens/Play/Spectator.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; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Users; + +namespace osu.Game.Screens.Play +{ + public class Spectator : OsuScreen + { + private readonly User targetUser; + + public Spectator([NotNull] User targetUser) + { + this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new OsuSpriteText + { + Text = $"Watching {targetUser}", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + }; + } + } +} From 154ccf1b4996401d97a0cf16b0b3d45c17feab1f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 20:05:11 +0900 Subject: [PATCH 4147/6909] Expose events from streaming client --- .../Online/Spectator/SpectatorStreamingClient.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 5a41316f31..9e554d1d43 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -64,6 +64,16 @@ namespace osu.Game.Online.Spectator /// public event Action OnNewFrames; + /// + /// Called whenever a user starts a play session. + /// + public event Action OnUserBeganPlaying; + + /// + /// Called whenever a user starts a play session. + /// + public event Action OnUserFinishedPlaying; + [BackgroundDependencyLoader] private void load() { @@ -154,18 +164,24 @@ namespace osu.Game.Online.Spectator if (!playingUsers.Contains(userId)) playingUsers.Add(userId); + OnUserBeganPlaying?.Invoke(userId, state); + return Task.CompletedTask; } Task ISpectatorClient.UserFinishedPlaying(int userId, SpectatorState state) { playingUsers.Remove(userId); + + OnUserFinishedPlaying?.Invoke(userId, state); + return Task.CompletedTask; } Task ISpectatorClient.UserSentFrames(int userId, FrameDataBundle data) { OnNewFrames?.Invoke(userId, data); + return Task.CompletedTask; } From ac4671c594744ede35448e9045736899d2137606 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 20:59:46 +0900 Subject: [PATCH 4148/6909] Add basic implementation of spectator screen --- osu.Game/Screens/Play/Spectator.cs | 113 +++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index ceac6c737f..d73395cc9e 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -2,10 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.Online.Spectator; +using osu.Game.Replays; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Replays; +using osu.Game.Rulesets.Replays.Types; +using osu.Game.Scoring; using osu.Game.Users; namespace osu.Game.Screens.Play @@ -14,6 +26,26 @@ namespace osu.Game.Screens.Play { private readonly User targetUser; + [Resolved] + private Bindable beatmap { get; set; } + + [Resolved] + private Bindable ruleset { get; set; } + + [Resolved] + private Bindable> mods { get; set; } + + [Resolved] + private SpectatorStreamingClient spectatorStreaming { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + private Replay replay; + public Spectator([NotNull] User targetUser) { this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); @@ -31,6 +63,87 @@ namespace osu.Game.Screens.Play Origin = Anchor.Centre, }, }; + + spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; + spectatorStreaming.OnUserFinishedPlaying += userFinishedPlaying; + spectatorStreaming.OnNewFrames += userSentFrames; + + spectatorStreaming.WatchUser((int)targetUser.Id); + } + + private void userSentFrames(int userId, FrameDataBundle data) + { + if (userId != targetUser.Id) + return; + + var rulesetInstance = ruleset.Value.CreateInstance(); + + foreach (var frame in data.Frames) + { + IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame(); + convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap, null); + + var convertedFrame = (ReplayFrame)convertibleFrame; + convertedFrame.Time = frame.Time; + + replay.Frames.Add(convertedFrame); + } + } + + private void userBeganPlaying(int userId, SpectatorState state) + { + if (userId != targetUser.Id) + return; + + replay ??= new Replay(); + + var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); + + // ruleset not available + if (resolvedRuleset == null) + return; + + var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == state.BeatmapID); + + if (resolvedBeatmap == null) + return; + + var scoreInfo = new ScoreInfo + { + Beatmap = resolvedBeatmap, + Mods = state.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), + Ruleset = resolvedRuleset.RulesetInfo, + }; + + this.MakeCurrent(); + + ruleset.Value = resolvedRuleset.RulesetInfo; + beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); + + this.Push(new ReplayPlayerLoader(new Score + { + ScoreInfo = scoreInfo, + Replay = replay, + })); + } + + private void userFinishedPlaying(int userId, SpectatorState state) + { + // todo: handle this in some way? + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorStreaming != null) + { + spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; + spectatorStreaming.OnUserFinishedPlaying -= userFinishedPlaying; + spectatorStreaming.OnNewFrames -= userSentFrames; + + spectatorStreaming.StopWatchingUser((int)targetUser.Id); + } } } } From 82a27c73a0077412ee1519c63c0b6d9323810e35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 21:17:12 +0900 Subject: [PATCH 4149/6909] Create basic testing setup --- .../Visual/Gameplay/TestSceneSpectator.cs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 25c30dbc5a..75ca970c62 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -2,6 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Online.Spectator; +using osu.Game.Replays.Legacy; using osu.Game.Screens.Play; using osu.Game.Users; @@ -9,12 +12,34 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneSpectator : ScreenTestScene { - private readonly User user = new User { Id = 1234, Username = "Test user" }; + [Cached(typeof(SpectatorStreamingClient))] + private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); [Test] public void TestSpectating() { - AddStep("load screen", () => LoadScreen(new Spectator(user))); + AddStep("load screen", () => LoadScreen(new Spectator(testSpectatorStreamingClient.StreamingUser))); + AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); + } + + internal class TestSpectatorStreamingClient : SpectatorStreamingClient + { + public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; + + public void StartPlay() + { + ((ISpectatorClient)this).UserBeganPlaying((int)StreamingUser.Id, new SpectatorState()); + } + + public void SendFrames() + { + ((ISpectatorClient)this).UserSentFrames((int)StreamingUser.Id, new FrameDataBundle(new[] + { + // todo: populate more frames + new LegacyReplayFrame(0, 0, 0, ReplayButtonState.Left1) + })); + } } } } From 9bb2cff8a54a30e83a3ca801d47e84fd66e0288d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 21:22:01 +0900 Subject: [PATCH 4150/6909] Convey actual beatmap and ruleset for full testing setup --- .../Visual/Gameplay/TestSceneSpectator.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 75ca970c62..1c73427686 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.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.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Game.Beatmaps; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Screens.Play; @@ -15,9 +17,13 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(SpectatorStreamingClient))] private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); + [Resolved] + private OsuGameBase game { get; set; } + [Test] public void TestSpectating() { + AddStep("add streaming client", () => Add(testSpectatorStreamingClient)); AddStep("load screen", () => LoadScreen(new Spectator(testSpectatorStreamingClient.StreamingUser))); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); @@ -25,11 +31,18 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestSpectatorStreamingClient : SpectatorStreamingClient { + [Resolved] + private BeatmapManager beatmaps { get; set; } + public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; public void StartPlay() { - ((ISpectatorClient)this).UserBeganPlaying((int)StreamingUser.Id, new SpectatorState()); + ((ISpectatorClient)this).UserBeganPlaying((int)StreamingUser.Id, new SpectatorState + { + BeatmapID = beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First().OnlineBeatmapID, + RulesetID = 0, + }); } public void SendFrames() From 67f6d52e35a861c97647139dc16b2020fa3c6e38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 21:27:05 +0900 Subject: [PATCH 4151/6909] Setup tests --- .../Visual/Gameplay/TestSceneSpectator.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 1c73427686..49fc81752c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -21,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay private OsuGameBase game { get; set; } [Test] - public void TestSpectating() + public void TestBasicSpectatingFlow() { AddStep("add streaming client", () => Add(testSpectatorStreamingClient)); AddStep("load screen", () => LoadScreen(new Spectator(testSpectatorStreamingClient.StreamingUser))); @@ -29,6 +29,30 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); } + [Test] + public void TestSpectatingDuringGameplay() + { + // should seek immediately to available frames + } + + [Test] + public void TestHostStartsPlayingWhileAlreadyWatching() + { + // should restart either immediately or after running out of frames + } + + [Test] + public void TestHostFails() + { + // should replay until running out of frames then fail + } + + [Test] + public void TestStopWatchingDuringPlay() + { + // should immediately exit and unbind from streaming client + } + internal class TestSpectatorStreamingClient : SpectatorStreamingClient { [Resolved] From 593b0a3adafc9b6683e351ab3ce7db6f060aa57b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 26 Oct 2020 21:45:37 +0900 Subject: [PATCH 4152/6909] Setup tests to run headless, add basic pass support --- .../Visual/Gameplay/TestSceneSpectator.cs | 73 ++++++++++++++++++- 1 file changed, 70 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 49fc81752c..006d64491a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -4,10 +4,13 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Screens.Play; +using osu.Game.Tests.Beatmaps.IO; using osu.Game.Users; namespace osu.Game.Tests.Visual.Gameplay @@ -17,14 +20,28 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(SpectatorStreamingClient))] private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); + private Spectator spectatorScreen; + [Resolved] private OsuGameBase game { get; set; } + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Wait()); + + AddStep("add streaming client", () => + { + Remove(testSpectatorStreamingClient); + Add(testSpectatorStreamingClient); + }); + } + [Test] public void TestBasicSpectatingFlow() { - AddStep("add streaming client", () => Add(testSpectatorStreamingClient)); - AddStep("load screen", () => LoadScreen(new Spectator(testSpectatorStreamingClient.StreamingUser))); + beginSpectating(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); } @@ -32,27 +49,68 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestSpectatingDuringGameplay() { + AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); + // should seek immediately to available frames + beginSpectating(); } [Test] public void TestHostStartsPlayingWhileAlreadyWatching() { + beginSpectating(); + + AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); + + AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); // should restart either immediately or after running out of frames } [Test] public void TestHostFails() { + beginSpectating(); + + AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); + // todo: send fail state + // should replay until running out of frames then fail } [Test] public void TestStopWatchingDuringPlay() { + beginSpectating(); + + AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); + + AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); + // should immediately exit and unbind from streaming client + AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); + + AddUntilStep("spectating stopped", () => spectatorScreen.GetParentScreen() == null); } + [Test] + public void TestWatchingBeatmapThatDoesntExistLocally() + { + beginSpectating(); + + AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); + + // player should never arrive. + } + + private void beginSpectating() => + AddStep("load screen", () => LoadScreen(spectatorScreen = new Spectator(testSpectatorStreamingClient.StreamingUser))); + internal class TestSpectatorStreamingClient : SpectatorStreamingClient { [Resolved] @@ -64,7 +122,16 @@ namespace osu.Game.Tests.Visual.Gameplay { ((ISpectatorClient)this).UserBeganPlaying((int)StreamingUser.Id, new SpectatorState { - BeatmapID = beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First().OnlineBeatmapID, + BeatmapID = beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID, + RulesetID = 0, + }); + } + + public void EndPlay() + { + ((ISpectatorClient)this).UserFinishedPlaying((int)StreamingUser.Id, new SpectatorState + { + BeatmapID = beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID, RulesetID = 0, }); } From 400542bc0ba76826341c549cd18db1f12efa1092 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 14:47:15 +0900 Subject: [PATCH 4153/6909] Ensure frames arrive --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 9 +++++++++ osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 006d64491a..533e535bc6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -9,6 +9,8 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Replays; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Users; @@ -38,12 +40,19 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + private OsuFramedReplayInputHandler replayHandler => + (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; + [Test] public void TestBasicSpectatingFlow() { beginSpectating(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); + + AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); + + AddAssert("ensure frames arrived", () => replayHandler.HasFrames); } [Test] diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index cf5c88b8fd..62b24f6b55 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Replays /// public bool FrameAccuratePlayback = false; - protected bool HasFrames => Frames.Count > 0; + public bool HasFrames => Frames.Count > 0; private bool inImportantSection { From 9bac8f37922276c7b5808383145436884f845819 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 14:57:00 +0900 Subject: [PATCH 4154/6909] Add null check on replay as safety measure --- osu.Game/Screens/Play/Spectator.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index d73395cc9e..24334af8d8 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -63,7 +63,11 @@ namespace osu.Game.Screens.Play Origin = Anchor.Centre, }, }; + } + protected override void LoadComplete() + { + base.LoadComplete(); spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; spectatorStreaming.OnUserFinishedPlaying += userFinishedPlaying; spectatorStreaming.OnNewFrames += userSentFrames; @@ -76,6 +80,11 @@ namespace osu.Game.Screens.Play if (userId != targetUser.Id) return; + // this should never happen as the server sends the user's state on watching, + // but is here as a safety measure. + if (replay == null) + return; + var rulesetInstance = ruleset.Value.CreateInstance(); foreach (var frame in data.Frames) From c1e7cd6e4774cbae012a6bc63f1e73e623427966 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 14:57:23 +0900 Subject: [PATCH 4155/6909] Stop replay playback when frames are starved --- .../Visual/Gameplay/TestSceneSpectator.cs | 31 ++++++++++++++----- .../Spectator/SpectatorStreamingClient.cs | 2 +- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 533e535bc6..8c03de8c13 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -43,6 +43,8 @@ namespace osu.Game.Tests.Visual.Gameplay private OsuFramedReplayInputHandler replayHandler => (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; + private Player player => Stack.CurrentScreen as Player; + [Test] public void TestBasicSpectatingFlow() { @@ -53,6 +55,9 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); AddAssert("ensure frames arrived", () => replayHandler.HasFrames); + + AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); + AddAssert("game is paused", () => !player.ChildrenOfType().First().GameplayClock.IsRunning); } [Test] @@ -127,14 +132,7 @@ namespace osu.Game.Tests.Visual.Gameplay public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; - public void StartPlay() - { - ((ISpectatorClient)this).UserBeganPlaying((int)StreamingUser.Id, new SpectatorState - { - BeatmapID = beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID, - RulesetID = 0, - }); - } + public void StartPlay() => sendState(); public void EndPlay() { @@ -153,6 +151,23 @@ namespace osu.Game.Tests.Visual.Gameplay new LegacyReplayFrame(0, 0, 0, ReplayButtonState.Left1) })); } + + public override void WatchUser(int userId) + { + // usually the server would do this. + sendState(); + + base.WatchUser(userId); + } + + private void sendState() + { + ((ISpectatorClient)this).UserBeganPlaying((int)StreamingUser.Id, new SpectatorState + { + BeatmapID = beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID, + RulesetID = 0, + }); + } } } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 9e554d1d43..481c94e6f3 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -227,7 +227,7 @@ namespace osu.Game.Online.Spectator connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState); } - public void WatchUser(int userId) + public virtual void WatchUser(int userId) { if (watchingUsers.Contains(userId)) return; From b737a8bf6e821d7bdc3c1046d406164b83cae893 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 16:27:15 +0900 Subject: [PATCH 4156/6909] Add field to Replay denoting whether the full replay have been received or not --- osu.Game/Replays/Replay.cs | 6 ++++++ osu.Game/Screens/Play/Spectator.cs | 6 ++++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Replays/Replay.cs b/osu.Game/Replays/Replay.cs index 31d2ed0d70..5430915394 100644 --- a/osu.Game/Replays/Replay.cs +++ b/osu.Game/Replays/Replay.cs @@ -8,6 +8,12 @@ namespace osu.Game.Replays { public class Replay { + /// + /// Whether all frames for this replay have been received. + /// If false, gameplay would be paused to wait for further data, for instance. + /// + public bool HasReceivedAllFrames = true; + public List Frames = new List(); } } diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 24334af8d8..dcfd7e829a 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Play if (userId != targetUser.Id) return; - replay ??= new Replay(); + replay ??= new Replay { HasReceivedAllFrames = false }; var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); @@ -138,7 +138,9 @@ namespace osu.Game.Screens.Play private void userFinishedPlaying(int userId, SpectatorState state) { - // todo: handle this in some way? + if (replay == null) return; + + replay.HasReceivedAllFrames = true; } protected override void Dispose(bool isDisposing) From 4dba96e189c2af01d3c947d43d66ddf17c066d34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 16:28:11 +0900 Subject: [PATCH 4157/6909] Add more useful frame sending logic to tests --- .../Visual/Gameplay/TestSceneSpectator.cs | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 8c03de8c13..f33a6876dd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -1,16 +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.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Replays; +using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Users; @@ -50,22 +53,25 @@ namespace osu.Game.Tests.Visual.Gameplay { beginSpectating(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); - AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); - + sendFrames(); AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); AddAssert("ensure frames arrived", () => replayHandler.HasFrames); AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); - AddAssert("game is paused", () => !player.ChildrenOfType().First().GameplayClock.IsRunning); + AddAssert("game is paused", () => player.ChildrenOfType().First().IsPaused.Value); + } + + private void sendFrames(int count = 10) + { + AddStep("send frames", () => testSpectatorStreamingClient.SendFrames(count)); } [Test] public void TestSpectatingDuringGameplay() { AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); - AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); - + sendFrames(); // should seek immediately to available frames beginSpectating(); } @@ -76,11 +82,9 @@ namespace osu.Game.Tests.Visual.Gameplay beginSpectating(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); - AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); - + sendFrames(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); - AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); - // should restart either immediately or after running out of frames + sendFrames(); } [Test] @@ -89,8 +93,7 @@ namespace osu.Game.Tests.Visual.Gameplay beginSpectating(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); - AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); - // todo: send fail state + sendFrames(); // should replay until running out of frames then fail } @@ -101,8 +104,7 @@ namespace osu.Game.Tests.Visual.Gameplay beginSpectating(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); - AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); - + sendFrames(); AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); // should immediately exit and unbind from streaming client @@ -117,8 +119,7 @@ namespace osu.Game.Tests.Visual.Gameplay beginSpectating(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); - AddStep("send frames", () => testSpectatorStreamingClient.SendFrames()); - + sendFrames(); // player should never arrive. } @@ -143,13 +144,19 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - public void SendFrames() + public void SendFrames(int count) { - ((ISpectatorClient)this).UserSentFrames((int)StreamingUser.Id, new FrameDataBundle(new[] + var frames = new List(); + + for (int i = 0; i < count; i++) { - // todo: populate more frames - new LegacyReplayFrame(0, 0, 0, ReplayButtonState.Left1) - })); + frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), ReplayButtonState.Left1)); + } + + frames.Add(new LegacyReplayFrame(count * 100, 0, 0, ReplayButtonState.None)); + + var bundle = new FrameDataBundle(frames); + ((ISpectatorClient)this).UserSentFrames((int)StreamingUser.Id, bundle); } public override void WatchUser(int userId) From 63131d46aaeb9794859fc52796bd0420bb5999fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 17:10:48 +0900 Subject: [PATCH 4158/6909] Send initial spectator state more correctly in test component --- .../Visual/Gameplay/TestSceneSpectator.cs | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index f33a6876dd..e95f7a50a4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBasicSpectatingFlow() { - beginSpectating(); + loadSpectatingScreen(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); sendFrames(); AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); @@ -73,13 +73,13 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); sendFrames(); // should seek immediately to available frames - beginSpectating(); + loadSpectatingScreen(); } [Test] public void TestHostStartsPlayingWhileAlreadyWatching() { - beginSpectating(); + loadSpectatingScreen(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); sendFrames(); @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestHostFails() { - beginSpectating(); + loadSpectatingScreen(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); sendFrames(); @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestStopWatchingDuringPlay() { - beginSpectating(); + loadSpectatingScreen(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); sendFrames(); @@ -116,14 +116,14 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestWatchingBeatmapThatDoesntExistLocally() { - beginSpectating(); + loadSpectatingScreen(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); sendFrames(); // player should never arrive. } - private void beginSpectating() => + private void loadSpectatingScreen() => AddStep("load screen", () => LoadScreen(spectatorScreen = new Spectator(testSpectatorStreamingClient.StreamingUser))); internal class TestSpectatorStreamingClient : SpectatorStreamingClient @@ -144,6 +144,8 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + private bool sentState; + public void SendFrames(int count) { var frames = new List(); @@ -157,18 +159,25 @@ namespace osu.Game.Tests.Visual.Gameplay var bundle = new FrameDataBundle(frames); ((ISpectatorClient)this).UserSentFrames((int)StreamingUser.Id, bundle); + + if (!sentState) + sendState(); } public override void WatchUser(int userId) { - // usually the server would do this. - sendState(); + if (sentState) + { + // usually the server would do this. + sendState(); + } base.WatchUser(userId); } private void sendState() { + sentState = true; ((ISpectatorClient)this).UserBeganPlaying((int)StreamingUser.Id, new SpectatorState { BeatmapID = beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID, From 3ec3321a3ded49588c927c02dee520a46ffd7852 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 17:11:07 +0900 Subject: [PATCH 4159/6909] Add missing space --- osu.Game/Screens/Play/Spectator.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index dcfd7e829a..522a636dfb 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -68,6 +68,7 @@ namespace osu.Game.Screens.Play protected override void LoadComplete() { base.LoadComplete(); + spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; spectatorStreaming.OnUserFinishedPlaying += userFinishedPlaying; spectatorStreaming.OnNewFrames += userSentFrames; From 9e6b0a42ec4795eb3878e47946370d900aa8d453 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 16:28:50 +0900 Subject: [PATCH 4160/6909] Allow FrameStabilityContainer to handle waiting-for-data state better (and pause outwards) --- .../Replays/FramedReplayInputHandler.cs | 28 +++++++++++++++++-- .../Rulesets/UI/FrameStabilityContainer.cs | 3 +- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 62b24f6b55..ddf85b7300 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -124,20 +124,42 @@ namespace osu.Game.Rulesets.Replays if (HasFrames) { + var next = NextFrame; + // check if the next frame is valid for the current playback direction. // validity is if the next frame is equal or "earlier" - var compare = time.CompareTo(NextFrame?.Time); + var compare = time.CompareTo(next?.Time); - if (compare == 0 || compare == currentDirection) + if (next != null && (compare == 0 || compare == currentDirection)) { if (advanceFrame()) return CurrentTime = CurrentFrame.Time; } else { - // if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null. + // this is the case where the frame can't be advanced (in the replay). + // even so, we may be able to move the clock forward due to being at the end of the replay or + // in a section where replay accuracy doesn't matter. + + // important section is always respected to block the update loop. if (inImportantSection) return null; + + if (next == null) + { + // in the case we have no more frames and haven't received the full replay, block. + if (!replay.HasReceivedAllFrames) + return null; + + // else allow play to end. + } + else if (next.Time.CompareTo(time) != currentDirection) + { + // in the case we have more frames, block if the next frame's time is less than the current time. + return null; + } + + // if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null. } } diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 28b7975a89..fb6c7d4c17 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -96,6 +96,7 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { requireMoreUpdateLoops = true; + validState = !frameStableClock.IsPaused.Value; int loops = 0; @@ -191,7 +192,7 @@ namespace osu.Game.Rulesets.UI finally { if (newProposedTime != manualClock.CurrentTime) - direction = newProposedTime > manualClock.CurrentTime ? 1 : -1; + direction = newProposedTime >= manualClock.CurrentTime ? 1 : -1; manualClock.CurrentTime = newProposedTime; manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; From 851d45d2ebea669f18243e924956a6ab7e0ea695 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 18:13:45 +0900 Subject: [PATCH 4161/6909] Add sane pausing logic --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 12 ++++++++++++ osu.Game/Rulesets/UI/IFrameStableClock.cs | 2 ++ osu.Game/Screens/Play/Player.cs | 8 ++++++++ 3 files changed, 22 insertions(+) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index fb6c7d4c17..987dca7073 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -101,6 +101,13 @@ namespace osu.Game.Rulesets.UI int loops = 0; + if (frameStableClock.WaitingOnFrames.Value) + { + // for now, force one update loop to check if frames have arrived + // this may have to change in the future where we want stable user pausing during replay playback. + validState = true; + } + while (validState && requireMoreUpdateLoops && loops++ < MaxCatchUpFrames) { updateClock(); @@ -203,6 +210,7 @@ namespace osu.Game.Rulesets.UI requireMoreUpdateLoops |= timeBehind != 0; frameStableClock.IsCatchingUp.Value = timeBehind > 200; + frameStableClock.WaitingOnFrames.Value = !validState; // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed @@ -231,6 +239,8 @@ namespace osu.Game.Rulesets.UI public readonly Bindable IsCatchingUp = new Bindable(); + public readonly Bindable WaitingOnFrames = new Bindable(); + public override IEnumerable> NonGameplayAdjustments => ParentGameplayClock?.NonGameplayAdjustments ?? Enumerable.Empty>(); public FrameStabilityClock(FramedClock underlyingClock) @@ -239,6 +249,8 @@ namespace osu.Game.Rulesets.UI } IBindable IFrameStableClock.IsCatchingUp => IsCatchingUp; + + IBindable IFrameStableClock.WaitingOnFrames => WaitingOnFrames; } } } diff --git a/osu.Game/Rulesets/UI/IFrameStableClock.cs b/osu.Game/Rulesets/UI/IFrameStableClock.cs index d888eefdc6..c1d8733d26 100644 --- a/osu.Game/Rulesets/UI/IFrameStableClock.cs +++ b/osu.Game/Rulesets/UI/IFrameStableClock.cs @@ -9,5 +9,7 @@ namespace osu.Game.Rulesets.UI public interface IFrameStableClock : IFrameBasedClock { IBindable IsCatchingUp { get; } + + IBindable WaitingOnFrames { get; } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3c0c643413..3e79ea0840 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -238,6 +238,14 @@ namespace osu.Game.Screens.Play skipOverlay.Hide(); } + DrawableRuleset.FrameStableClock.WaitingOnFrames.BindValueChanged(waiting => + { + if (waiting.NewValue) + GameplayClockContainer.Stop(); + else + GameplayClockContainer.Start(); + }); + DrawableRuleset.IsPaused.BindValueChanged(paused => { updateGameplayState(); From d4467d20a2047020e3c59282e8882d89a05cf48d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 18:13:58 +0900 Subject: [PATCH 4162/6909] Allow tests to continue sending frames from point they left off --- .../Visual/Gameplay/TestSceneSpectator.cs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index e95f7a50a4..98bd90b3b5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -30,10 +30,14 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuGameBase game { get; set; } + private int nextFrame = 0; + public override void SetUpSteps() { base.SetUpSteps(); + AddStep("reset sent frames", () => nextFrame = 0); + AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Wait()); AddStep("add streaming client", () => @@ -53,18 +57,20 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + sendFrames(); + AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); AddAssert("ensure frames arrived", () => replayHandler.HasFrames); AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); AddAssert("game is paused", () => player.ChildrenOfType().First().IsPaused.Value); - } + sendFrames(); + AddUntilStep("game resumed", () => !player.ChildrenOfType().First().IsPaused.Value); - private void sendFrames(int count = 10) - { - AddStep("send frames", () => testSpectatorStreamingClient.SendFrames(count)); + AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); + AddAssert("game is paused", () => player.ChildrenOfType().First().IsPaused.Value); } [Test] @@ -123,6 +129,15 @@ namespace osu.Game.Tests.Visual.Gameplay // player should never arrive. } + private void sendFrames(int count = 10) + { + AddStep("send frames", () => + { + testSpectatorStreamingClient.SendFrames(nextFrame, count); + nextFrame += count; + }); + } + private void loadSpectatingScreen() => AddStep("load screen", () => LoadScreen(spectatorScreen = new Spectator(testSpectatorStreamingClient.StreamingUser))); @@ -146,16 +161,16 @@ namespace osu.Game.Tests.Visual.Gameplay private bool sentState; - public void SendFrames(int count) + public void SendFrames(int index, int count) { var frames = new List(); - for (int i = 0; i < count; i++) + for (int i = index; i < index + count; i++) { - frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), ReplayButtonState.Left1)); - } + var buttonState = i == index + count - 1 ? ReplayButtonState.None : ReplayButtonState.Left1; - frames.Add(new LegacyReplayFrame(count * 100, 0, 0, ReplayButtonState.None)); + frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); + } var bundle = new FrameDataBundle(frames); ((ISpectatorClient)this).UserSentFrames((int)StreamingUser.Id, bundle); From b3d793a505d919902710673de4e1947e228e43f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 18:28:49 +0900 Subject: [PATCH 4163/6909] Fix gameplay proceeding when no frames have been received yet --- .../Visual/Gameplay/TestSceneSpectator.cs | 15 +++++++++++++++ .../Rulesets/Replays/FramedReplayInputHandler.cs | 6 ++++++ 2 files changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 98bd90b3b5..14b0ca4d33 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -56,6 +56,9 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestBasicSpectatingFlow() { loadSpectatingScreen(); + + AddAssert("screen hasn't changed", () => Stack.CurrentScreen is Spectator); + AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); sendFrames(); @@ -73,6 +76,18 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("game is paused", () => player.ChildrenOfType().First().IsPaused.Value); } + [Test] + public void TestPlayStartsWithNoFrames() + { + loadSpectatingScreen(); + + AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + + AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); + + AddUntilStep("game is paused", () => player.ChildrenOfType().First().IsPaused.Value); + } + [Test] public void TestSpectatingDuringGameplay() { diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index ddf85b7300..d7be809d34 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -162,6 +162,12 @@ namespace osu.Game.Rulesets.Replays // if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null. } } + else + { + // if we never received frames and are expecting to, block. + if (!replay.HasReceivedAllFrames) + return null; + } return CurrentTime = time; } From a289b7034f5df01ae442c65af5b8b01fe953a8c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 18:32:05 +0900 Subject: [PATCH 4164/6909] Add test helper functions to promote code share --- .../Visual/Gameplay/TestSceneSpectator.cs | 49 +++++++++++-------- 1 file changed, 28 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 14b0ca4d33..61bcea7bfa 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -59,21 +59,20 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("screen hasn't changed", () => Stack.CurrentScreen is Spectator); - AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); - + start(); sendFrames(); - AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); - + waitForPlayer(); AddAssert("ensure frames arrived", () => replayHandler.HasFrames); AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); - AddAssert("game is paused", () => player.ChildrenOfType().First().IsPaused.Value); - sendFrames(); - AddUntilStep("game resumed", () => !player.ChildrenOfType().First().IsPaused.Value); + checkPaused(true); + sendFrames(); + + checkPaused(false); AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); - AddAssert("game is paused", () => player.ChildrenOfType().First().IsPaused.Value); + checkPaused(true); } [Test] @@ -81,17 +80,19 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); - - AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); - + start(); + waitForPlayer(); AddUntilStep("game is paused", () => player.ChildrenOfType().First().IsPaused.Value); + + sendFrames(); + + checkPaused(false); } [Test] public void TestSpectatingDuringGameplay() { - AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + start(); sendFrames(); // should seek immediately to available frames loadSpectatingScreen(); @@ -102,9 +103,9 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + start(); sendFrames(); - AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + start(); sendFrames(); } @@ -113,10 +114,10 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + start(); sendFrames(); - // should replay until running out of frames then fail + // TODO: should replay until running out of frames then fail } [Test] @@ -124,10 +125,9 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + start(); sendFrames(); - AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); - + waitForPlayer(); // should immediately exit and unbind from streaming client AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); @@ -139,11 +139,18 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + start(); sendFrames(); // player should never arrive. } + private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); + + private void start() => AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + + private void checkPaused(bool state) => + AddAssert($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); + private void sendFrames(int count = 10) { AddStep("send frames", () => From 42b3aa335904c97871e7b44fe34384a4e545109e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 18:56:28 +0900 Subject: [PATCH 4165/6909] Fix spectating when starting from a point that isn't at the beginning of the beatmap --- .../Visual/Gameplay/TestSceneSpectator.cs | 10 ++++-- .../Screens/Play/GameplayClockContainer.cs | 25 +++++++++------ osu.Game/Screens/Play/Player.cs | 4 ++- osu.Game/Screens/Play/ReplayPlayer.cs | 10 +++--- osu.Game/Screens/Play/Spectator.cs | 2 +- osu.Game/Screens/Play/SpectatorPlayer.cs | 28 ++++++++++++++++ .../Screens/Play/SpectatorPlayerLoader.cs | 32 +++++++++++++++++++ 7 files changed, 93 insertions(+), 18 deletions(-) create mode 100644 osu.Game/Screens/Play/SpectatorPlayer.cs create mode 100644 osu.Game/Screens/Play/SpectatorPlayerLoader.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 61bcea7bfa..456fb0b110 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -93,9 +93,15 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestSpectatingDuringGameplay() { start(); - sendFrames(); - // should seek immediately to available frames + loadSpectatingScreen(); + + AddStep("advance frame count", () => nextFrame = 300); + sendFrames(); + + waitForPlayer(); + + AddUntilStep("playing from correct point in time", () => player.ChildrenOfType().First().FrameStableClock.CurrentTime > 30000); } [Test] diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 6679e56871..6154ec67b8 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -37,6 +37,7 @@ namespace osu.Game.Screens.Play private readonly DecoupleableInterpolatingFramedClock adjustableClock; private readonly double gameplayStartTime; + private readonly bool startAtGameplayStart; private readonly double firstHitObjectTime; @@ -62,10 +63,11 @@ namespace osu.Game.Screens.Play private readonly FramedOffsetClock platformOffsetClock; - public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime) + public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) { this.beatmap = beatmap; this.gameplayStartTime = gameplayStartTime; + this.startAtGameplayStart = startAtGameplayStart; track = beatmap.Track; firstHitObjectTime = beatmap.Beatmap.HitObjects.First().StartTime; @@ -103,16 +105,21 @@ namespace osu.Game.Screens.Play userAudioOffset.BindValueChanged(offset => userOffsetClock.Offset = offset.NewValue, true); // sane default provided by ruleset. - double startTime = Math.Min(0, gameplayStartTime); + double startTime = gameplayStartTime; - // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. - // this is commonly used to display an intro before the audio track start. - startTime = Math.Min(startTime, beatmap.Storyboard.FirstEventTime); + if (!startAtGameplayStart) + { + startTime = Math.Min(0, startTime); - // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. - // this is not available as an option in the live editor but can still be applied via .osu editing. - if (beatmap.BeatmapInfo.AudioLeadIn > 0) - startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); + // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. + // this is commonly used to display an intro before the audio track start. + startTime = Math.Min(startTime, beatmap.Storyboard.FirstEventTime); + + // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. + // this is not available as an option in the live editor but can still be applied via .osu editing. + if (beatmap.BeatmapInfo.AudioLeadIn > 0) + startTime = Math.Min(startTime, firstHitObjectTime - beatmap.BeatmapInfo.AudioLeadIn); + } Seek(startTime); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3e79ea0840..f9af1818d0 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -199,7 +199,7 @@ namespace osu.Game.Screens.Play if (!ScoreProcessor.Mode.Disabled) config.BindWith(OsuSetting.ScoreDisplayMode, ScoreProcessor.Mode); - InternalChild = GameplayClockContainer = new GameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); + InternalChild = GameplayClockContainer = CreateGameplayClockContainer(Beatmap.Value, DrawableRuleset.GameplayStartTime); AddInternal(gameplayBeatmap = new GameplayBeatmap(playableBeatmap)); AddInternal(screenSuspension = new ScreenSuspensionHandler(GameplayClockContainer)); @@ -288,6 +288,8 @@ namespace osu.Game.Screens.Play IsBreakTime.BindValueChanged(onBreakTimeChanged, true); } + protected virtual GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) => new GameplayClockContainer(beatmap, gameplayStart); + private Drawable createUnderlayComponents() => DimmableStoryboard = new DimmableStoryboard(Beatmap.Value.Storyboard) { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 7f5c17a265..eeb3b509fe 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -8,7 +8,7 @@ namespace osu.Game.Screens.Play { public class ReplayPlayer : Player { - private readonly Score score; + protected readonly Score Score; // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) protected override bool CheckModsAllowFailure() => false; @@ -16,12 +16,12 @@ namespace osu.Game.Screens.Play public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true) : base(allowPause, showResults) { - this.score = score; + this.Score = score; } protected override void PrepareReplay() { - DrawableRuleset?.SetReplayScore(score); + DrawableRuleset?.SetReplayScore(Score); } protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); @@ -31,9 +31,9 @@ namespace osu.Game.Screens.Play var baseScore = base.CreateScore(); // Since the replay score doesn't contain statistics, we'll pass them through here. - score.ScoreInfo.HitEvents = baseScore.HitEvents; + Score.ScoreInfo.HitEvents = baseScore.HitEvents; - return score.ScoreInfo; + return Score.ScoreInfo; } } } diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 522a636dfb..dcbf502fb2 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -130,7 +130,7 @@ namespace osu.Game.Screens.Play ruleset.Value = resolvedRuleset.RulesetInfo; beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); - this.Push(new ReplayPlayerLoader(new Score + this.Push(new SpectatorPlayerLoader(new Score { ScoreInfo = scoreInfo, Replay = replay, diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs new file mode 100644 index 0000000000..89e5f8f2dc --- /dev/null +++ b/osu.Game/Screens/Play/SpectatorPlayer.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.Linq; +using osu.Game.Beatmaps; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Play +{ + public class SpectatorPlayer : ReplayPlayer + { + public SpectatorPlayer(Score score) + : base(score) + { + } + + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) + { + // if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap. + double? firstFrameTime = Score.Replay.Frames.FirstOrDefault()?.Time; + + if (firstFrameTime == null || firstFrameTime <= gameplayStart + 5000) + return base.CreateGameplayClockContainer(beatmap, gameplayStart); + + return new GameplayClockContainer(beatmap, firstFrameTime.Value, true); + } + } +} diff --git a/osu.Game/Screens/Play/SpectatorPlayerLoader.cs b/osu.Game/Screens/Play/SpectatorPlayerLoader.cs new file mode 100644 index 0000000000..580af81166 --- /dev/null +++ b/osu.Game/Screens/Play/SpectatorPlayerLoader.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.Screens; +using osu.Game.Scoring; + +namespace osu.Game.Screens.Play +{ + public class SpectatorPlayerLoader : PlayerLoader + { + public readonly ScoreInfo Score; + + public SpectatorPlayerLoader(Score score) + : base(() => new SpectatorPlayer(score)) + { + if (score.Replay == null) + throw new ArgumentException($"{nameof(score)} must have a non-null {nameof(score.Replay)}.", nameof(score)); + + Score = score.ScoreInfo; + } + + public override void OnEntering(IScreen last) + { + // these will be reverted thanks to PlayerLoader's lease. + Mods.Value = Score.Mods; + Ruleset.Value = Score.Ruleset; + + base.OnEntering(last); + } + } +} From 2cacdaa11b4c530c425d430a25f613830e112e0a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 19:23:35 +0900 Subject: [PATCH 4166/6909] Add basic beatmap download and play flow --- .../Visual/Gameplay/TestSceneSpectator.cs | 18 ++-- osu.Game/Screens/Play/Spectator.cs | 89 ++++++++++++++++++- 2 files changed, 96 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 456fb0b110..458fc1e58b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -25,6 +25,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(SpectatorStreamingClient))] private TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSpectatorStreamingClient(); + // used just to show beatmap card for the time being. + protected override bool UseOnlineAPI => true; + private Spectator spectatorScreen; [Resolved] @@ -134,9 +137,9 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); waitForPlayer(); + // should immediately exit and unbind from streaming client AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); - AddUntilStep("spectating stopped", () => spectatorScreen.GetParentScreen() == null); } @@ -145,14 +148,15 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - start(); + start(88); sendFrames(); - // player should never arrive. + + AddAssert("screen didn't change", () => Stack.CurrentScreen is Spectator); } private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); - private void start() => AddStep("start play", () => testSpectatorStreamingClient.StartPlay()); + private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(beatmapId)); private void checkPaused(bool state) => AddAssert($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); @@ -176,7 +180,7 @@ namespace osu.Game.Tests.Visual.Gameplay public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; - public void StartPlay() => sendState(); + public void StartPlay(int? beatmapId = null) => sendState(beatmapId); public void EndPlay() { @@ -218,12 +222,12 @@ namespace osu.Game.Tests.Visual.Gameplay base.WatchUser(userId); } - private void sendState() + private void sendState(int? beatmapId = null) { sentState = true; ((ISpectatorClient)this).UserBeganPlaying((int)StreamingUser.Id, new SpectatorState { - BeatmapID = beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID, + BeatmapID = beatmapId ?? beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID, RulesetID = 0, }); } diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index dcbf502fb2..dd6b434889 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -8,10 +8,15 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; +using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -19,6 +24,7 @@ using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Scoring; using osu.Game.Users; +using osuTK; namespace osu.Game.Screens.Play { @@ -35,6 +41,9 @@ namespace osu.Game.Screens.Play [Resolved] private Bindable> mods { get; set; } + [Resolved] + private IAPIProvider api { get; set; } + [Resolved] private SpectatorStreamingClient spectatorStreaming { get; set; } @@ -46,6 +55,12 @@ namespace osu.Game.Screens.Play private Replay replay; + private Container beatmapPanelContainer; + + private SpectatorState state; + + private IBindable> managerUpdated; + public Spectator([NotNull] User targetUser) { this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); @@ -56,11 +71,42 @@ namespace osu.Game.Screens.Play { InternalChildren = new Drawable[] { - new OsuSpriteText + new FillFlowContainer { - Text = $"Watching {targetUser}", + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, Anchor = Anchor.Centre, Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] + { + new OsuSpriteText + { + Text = "Currently spectating", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new UserGridPanel(targetUser) + { + Width = 290, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new OsuSpriteText + { + Text = "playing", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + beatmapPanelContainer = new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + } }, }; } @@ -74,6 +120,15 @@ namespace osu.Game.Screens.Play spectatorStreaming.OnNewFrames += userSentFrames; spectatorStreaming.WatchUser((int)targetUser.Id); + + managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(beatmapUpdated); + } + + private void beatmapUpdated(ValueChangedEvent> beatmap) + { + if (beatmap.NewValue.TryGetTarget(out var beatmapSet) && beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) + attemptStart(); } private void userSentFrames(int userId, FrameDataBundle data) @@ -107,16 +162,31 @@ namespace osu.Game.Screens.Play replay ??= new Replay { HasReceivedAllFrames = false }; + this.state = state; + + attemptStart(); + } + + private void attemptStart() + { var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); // ruleset not available if (resolvedRuleset == null) return; + if (state.BeatmapID == null) + return; + + this.MakeCurrent(); + var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == state.BeatmapID); if (resolvedBeatmap == null) + { + showBeatmapPanel(state.BeatmapID.Value); return; + } var scoreInfo = new ScoreInfo { @@ -125,8 +195,6 @@ namespace osu.Game.Screens.Play Ruleset = resolvedRuleset.RulesetInfo, }; - this.MakeCurrent(); - ruleset.Value = resolvedRuleset.RulesetInfo; beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); @@ -137,6 +205,17 @@ namespace osu.Game.Screens.Play })); } + private void showBeatmapPanel(int beatmapId) + { + var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); + req.Success += res => Schedule(() => + { + beatmapPanelContainer.Child = new GridBeatmapPanel(res.ToBeatmapSet(rulesets)); + }); + + api.Queue(req); + } + private void userFinishedPlaying(int userId, SpectatorState state) { if (replay == null) return; @@ -156,6 +235,8 @@ namespace osu.Game.Screens.Play spectatorStreaming.StopWatchingUser((int)targetUser.Id); } + + managerUpdated.UnbindAll(); } } } From a96c067bead52c21d4950c1d29717b611b077b31 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 27 Oct 2020 13:45:21 +0100 Subject: [PATCH 4167/6909] Remove uncessary async-await state machine level. --- osu.Game/Scoring/ScorePerformanceManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index b7657c73c6..746aa67a55 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -24,14 +24,14 @@ namespace osu.Game.Scoring /// /// The score to do the calculation on. /// An optional to cancel the operation. - public async Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) + public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) { var lookupKey = new PerformanceCacheLookup(score); if (performanceCache.TryGetValue(lookupKey, out double performance)) - return performance; + return Task.FromResult((double?)performance); - return await computePerformanceAsync(score, lookupKey, token); + return computePerformanceAsync(score, lookupKey, token); } private async Task computePerformanceAsync(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) From d5e0fa322b794f987785063f2b5588d63e9f4a14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 27 Oct 2020 22:30:45 +0900 Subject: [PATCH 4168/6909] Fix a couple of inspections --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 +- osu.Game/Screens/Play/ReplayPlayer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 458fc1e58b..a716b0c06b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private OsuGameBase game { get; set; } - private int nextFrame = 0; + private int nextFrame; public override void SetUpSteps() { diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index eeb3b509fe..3a4298f22d 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Play public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true) : base(allowPause, showResults) { - this.Score = score; + Score = score; } protected override void PrepareReplay() From 064c50c3ac8642803c91b6a08a11ea5d64f1d796 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 27 Oct 2020 12:39:50 +0100 Subject: [PATCH 4169/6909] Expose currentZoom to fix selection box wiggle --- .../Components/Timeline/TimelineBlueprintContainer.cs | 10 +++++----- .../Components/Timeline/ZoomableScrollContainer.cs | 8 +++++++- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index b76032709f..008da14a21 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; -using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -139,6 +138,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private class TimelineDragBox : DragBox { private Vector2 lastMouseDown; + private float? lastZoom; private float localMouseDown; @@ -166,13 +166,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline lastZoom = null; } - //Zooming the timeline shifts the coordinate system this compensates for this shift - float zoomCorrection = lastZoom.HasValue ? (parent.timeline.Zoom / lastZoom.Value) : 1; + //Zooming the timeline shifts the coordinate system. zoomCorrection compensates for that + float zoomCorrection = lastZoom.HasValue ? (parent.timeline.CurrentZoom / lastZoom.Value) : 1; localMouseDown *= zoomCorrection; - lastZoom = parent.timeline.Zoom; + lastZoom = parent.timeline.CurrentZoom; float selection1 = localMouseDown; - float selection2 = e.MousePosition.X; + float selection2 = e.MousePosition.X * zoomCorrection; Box.X = Math.Min(selection1, selection2); Box.Width = Math.Abs(selection1 - selection2); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 227eecf9c7..6a9552a2c4 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -29,9 +29,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private readonly Container zoomedContent; protected override Container Content => zoomedContent; - private float currentZoom = 1; + /// + /// The current zoom level of . + /// It may differ from during transitions. + /// + public float CurrentZoom => currentZoom; + + [Resolved(canBeNull: true)] private IFrameBasedClock editorClock { get; set; } From 983a2774e89c4e973c28c08bba669e9ae25f3096 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 27 Oct 2020 15:09:10 +0100 Subject: [PATCH 4170/6909] Code Formatting --- .../Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 6a9552a2c4..f90658e99c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -37,7 +37,6 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public float CurrentZoom => currentZoom; - [Resolved(canBeNull: true)] private IFrameBasedClock editorClock { get; set; } From 742a96484befff6d8c137cd749cfd648c7c65992 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Oct 2020 20:13:18 +0300 Subject: [PATCH 4171/6909] Add ability to set extra parameters to SearchBeatmapSetsRequest --- .../API/Requests/SearchBeatmapSetsRequest.cs | 28 ++++++++++++++++++- .../Overlays/BeatmapListing/SearchExtra.cs | 13 +++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/BeatmapListing/SearchExtra.cs diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index dde45b5aeb..f8cf747757 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -21,6 +21,8 @@ namespace osu.Game.Online.API.Requests public SearchLanguage Language { get; } + public SearchExtra Extra { get; } + private readonly string query; private readonly RulesetInfo ruleset; private readonly Cursor cursor; @@ -35,7 +37,8 @@ namespace osu.Game.Online.API.Requests SortCriteria sortCriteria = SortCriteria.Ranked, SortDirection sortDirection = SortDirection.Descending, SearchGenre genre = SearchGenre.Any, - SearchLanguage language = SearchLanguage.Any) + SearchLanguage language = SearchLanguage.Any, + SearchExtra extra = SearchExtra.Any) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; @@ -46,6 +49,7 @@ namespace osu.Game.Online.API.Requests SortDirection = sortDirection; Genre = genre; Language = language; + Extra = extra; } protected override WebRequest CreateWebRequest() @@ -68,6 +72,28 @@ namespace osu.Game.Online.API.Requests req.AddCursor(cursor); + if (Extra != SearchExtra.Any) + { + string extraString = string.Empty; + + switch (Extra) + { + case SearchExtra.Both: + extraString = "video.storyboard"; + break; + + case SearchExtra.Storyboard: + extraString = "storyboard"; + break; + + case SearchExtra.Video: + extraString = "video"; + break; + } + + req.AddParameter("e", extraString); + } + return req; } diff --git a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs new file mode 100644 index 0000000000..fd4896c46e --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/SearchExtra.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.Overlays.BeatmapListing +{ + public enum SearchExtra + { + Video, + Storyboard, + Both, + Any + } +} From 26a60d898cfd3404275ad59021931bf4a03ec94a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Oct 2020 21:22:20 +0300 Subject: [PATCH 4172/6909] Implement BeatmapSearchExtraFilterRow --- .../TestSceneBeatmapListingSearchControl.cs | 3 + .../BeatmapListingFilterControl.cs | 4 +- .../BeatmapListingSearchControl.cs | 4 + .../BeatmapSearchExtraFilterRow.cs | 97 +++++++++++++ .../BeatmapListing/BeatmapSearchFilterRow.cs | 127 +++++++++--------- .../BeatmapSearchRulesetFilterRow.cs | 3 +- .../Overlays/BeatmapListing/SearchExtra.cs | 4 +- 7 files changed, 176 insertions(+), 66 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index a4698a9a32..9a410dd18c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -27,6 +27,7 @@ namespace osu.Game.Tests.Visual.UserInterface OsuSpriteText category; OsuSpriteText genre; OsuSpriteText language; + OsuSpriteText extra; Add(control = new BeatmapListingSearchControl { @@ -46,6 +47,7 @@ namespace osu.Game.Tests.Visual.UserInterface category = new OsuSpriteText(), genre = new OsuSpriteText(), language = new OsuSpriteText(), + extra = new OsuSpriteText() } }); @@ -54,6 +56,7 @@ namespace osu.Game.Tests.Visual.UserInterface 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); + control.Extra.BindValueChanged(e => extra.Text = $"Extra: {e.NewValue}", true); } [Test] diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 494a0df8f8..37fbfe7093 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -130,6 +130,7 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Category.BindValueChanged(_ => queueUpdateSearch()); searchControl.Genre.BindValueChanged(_ => queueUpdateSearch()); searchControl.Language.BindValueChanged(_ => queueUpdateSearch()); + searchControl.Extra.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch()); @@ -179,7 +180,8 @@ namespace osu.Game.Overlays.BeatmapListing sortControl.Current.Value, sortControl.SortDirection.Value, searchControl.Genre.Value, - searchControl.Language.Value); + searchControl.Language.Value, + searchControl.Extra.Value); getSetsRequest.Success += response => { diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 29c4fe0d2e..437c26e36d 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -28,6 +28,8 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable Language => languageFilter.Current; + public Bindable Extra => extraFilter.Current; + public BeatmapSetInfo BeatmapSet { set @@ -48,6 +50,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchFilterRow categoryFilter; private readonly BeatmapSearchFilterRow genreFilter; private readonly BeatmapSearchFilterRow languageFilter; + private readonly BeatmapSearchExtraFilterRow extraFilter; private readonly Box background; private readonly UpdateableBeatmapSetCover beatmapCover; @@ -105,6 +108,7 @@ namespace osu.Game.Overlays.BeatmapListing categoryFilter = new BeatmapSearchFilterRow(@"Categories"), genreFilter = new BeatmapSearchFilterRow(@"Genre"), languageFilter = new BeatmapSearchFilterRow(@"Language"), + extraFilter = new BeatmapSearchExtraFilterRow() } } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs new file mode 100644 index 0000000000..6e81cd2976 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs @@ -0,0 +1,97 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osuTK; + +namespace osu.Game.Overlays.BeatmapListing +{ + public class BeatmapSearchExtraFilterRow : BeatmapSearchFilterRow + { + public BeatmapSearchExtraFilterRow() + : base("Extra") + { + } + + protected override Drawable CreateFilter() => new ExtraFilter(); + + private class ExtraFilter : FillFlowContainer, IHasCurrentValue + { + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public Bindable Current + { + get => current.Current; + set => current.Current = value; + } + + private readonly ExtraFilterTabItem videoItem; + private readonly ExtraFilterTabItem storyboardItem; + + public ExtraFilter() + { + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + RelativeSizeAxes = Axes.X; + Height = 15; + Spacing = new Vector2(10, 0); + AddRange(new[] + { + videoItem = new ExtraFilterTabItem(SearchExtra.Video), + storyboardItem = new ExtraFilterTabItem(SearchExtra.Storyboard) + }); + + foreach (var item in Children) + item.StateUpdated += updateBindable; + } + + private void updateBindable() + { + if (videoItem.Active.Value && storyboardItem.Active.Value) + { + Current.Value = SearchExtra.Both; + return; + } + + if (videoItem.Active.Value) + { + Current.Value = SearchExtra.Video; + return; + } + + if (storyboardItem.Active.Value) + { + Current.Value = SearchExtra.Storyboard; + return; + } + + Current.Value = SearchExtra.Any; + } + } + + private class ExtraFilterTabItem : FilterTabItem + { + public event Action StateUpdated; + + public ExtraFilterTabItem(SearchExtra value) + : base(value) + { + Active.BindValueChanged(_ => StateUpdated?.Invoke()); + } + + protected override bool OnClick(ClickEvent e) + { + base.OnClick(e); + Active.Value = !Active.Value; + return true; + } + + protected override string CreateText(SearchExtra value) => $@"Has {value.ToString()}"; + } + } +} diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index 45ef793deb..ad32475b25 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -32,6 +32,7 @@ namespace osu.Game.Overlays.BeatmapListing public BeatmapSearchFilterRow(string headerName) { + Drawable filter; AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; AddInternal(new GridContainer @@ -49,7 +50,7 @@ namespace osu.Game.Overlays.BeatmapListing }, Content = new[] { - new Drawable[] + new[] { new OsuSpriteText { @@ -58,17 +59,17 @@ namespace osu.Game.Overlays.BeatmapListing Font = OsuFont.GetFont(size: 13), Text = headerName.Titleize() }, - CreateFilter().With(f => - { - f.Current = current; - }) + filter = CreateFilter() } } }); + + if (filter is IHasCurrentValue filterWithValue) + filterWithValue.Current = current; } [NotNull] - protected virtual BeatmapSearchFilter CreateFilter() => new BeatmapSearchFilter(); + protected virtual Drawable CreateFilter() => new BeatmapSearchFilter(); protected class BeatmapSearchFilter : TabControl { @@ -99,62 +100,6 @@ namespace osu.Game.Overlays.BeatmapListing protected override TabItem CreateTabItem(T value) => new FilterTabItem(value); - protected class FilterTabItem : TabItem - { - protected virtual float TextSize => 13; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } - - private readonly OsuSpriteText text; - - public FilterTabItem(T value) - : base(value) - { - AutoSizeAxes = Axes.Both; - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; - AddRangeInternal(new Drawable[] - { - text = new OsuSpriteText - { - Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Regular), - Text = (value as Enum)?.GetDescription() ?? value.ToString() - }, - new HoverClickSounds() - }); - - Enabled.Value = true; - } - - [BackgroundDependencyLoader] - private void load() - { - 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() => text.FadeColour(Active.Value ? Color4.White : getStateColour(), 200, Easing.OutQuint); - - private Color4 getStateColour() => IsHovered ? colourProvider.Light1 : colourProvider.Light3; - } - private class FilterDropdown : OsuTabDropdown { protected override DropdownHeader CreateHeader() => new FilterHeader @@ -172,5 +117,63 @@ namespace osu.Game.Overlays.BeatmapListing } } } + + protected class FilterTabItem : TabItem + { + protected virtual float TextSize => 13; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + private readonly OsuSpriteText text; + + public FilterTabItem(T value) + : base(value) + { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + AddRangeInternal(new Drawable[] + { + text = new OsuSpriteText + { + Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Regular), + Text = CreateText(value) + }, + new HoverClickSounds() + }); + + Enabled.Value = true; + } + + protected virtual string CreateText(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); + + [BackgroundDependencyLoader] + private void load() + { + 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() => text.FadeColour(Active.Value ? Color4.White : getStateColour(), 200, Easing.OutQuint); + + private Color4 getStateColour() => IsHovered ? colourProvider.Light1 : colourProvider.Light3; + } } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs index eebd896cf9..a8dc088e52 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRulesetFilterRow.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Rulesets; namespace osu.Game.Overlays.BeatmapListing @@ -13,7 +14,7 @@ namespace osu.Game.Overlays.BeatmapListing { } - protected override BeatmapSearchFilter CreateFilter() => new RulesetFilter(); + protected override Drawable CreateFilter() => new RulesetFilter(); private class RulesetFilter : BeatmapSearchFilter { diff --git a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs index fd4896c46e..53900211e1 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs @@ -5,9 +5,9 @@ namespace osu.Game.Overlays.BeatmapListing { public enum SearchExtra { + Any, Video, Storyboard, - Both, - Any + Both } } From 1b40b56d41081be1a199bea0889727360e57de1c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Oct 2020 21:30:53 +0300 Subject: [PATCH 4173/6909] Add ability to search by play criteria --- .../TestSceneBeatmapListingSearchControl.cs | 5 ++++- .../Online/API/Requests/SearchBeatmapSetsRequest.cs | 9 ++++++++- .../BeatmapListing/BeatmapListingFilterControl.cs | 4 +++- .../BeatmapListing/BeatmapListingSearchControl.cs | 6 +++++- osu.Game/Overlays/BeatmapListing/SearchPlayed.cs | 12 ++++++++++++ 5 files changed, 32 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/SearchPlayed.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index 9a410dd18c..5c9431aad1 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -28,6 +28,7 @@ namespace osu.Game.Tests.Visual.UserInterface OsuSpriteText genre; OsuSpriteText language; OsuSpriteText extra; + OsuSpriteText played; Add(control = new BeatmapListingSearchControl { @@ -47,7 +48,8 @@ namespace osu.Game.Tests.Visual.UserInterface category = new OsuSpriteText(), genre = new OsuSpriteText(), language = new OsuSpriteText(), - extra = new OsuSpriteText() + extra = new OsuSpriteText(), + played = new OsuSpriteText() } }); @@ -57,6 +59,7 @@ namespace osu.Game.Tests.Visual.UserInterface control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true); control.Language.BindValueChanged(l => language.Text = $"Language: {l.NewValue}", true); control.Extra.BindValueChanged(e => extra.Text = $"Extra: {e.NewValue}", true); + control.Played.BindValueChanged(p => played.Text = $"Played: {p.NewValue}", true); } [Test] diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index f8cf747757..12383e7457 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -23,6 +23,8 @@ namespace osu.Game.Online.API.Requests public SearchExtra Extra { get; } + public SearchPlayed Played { get; } + private readonly string query; private readonly RulesetInfo ruleset; private readonly Cursor cursor; @@ -38,7 +40,8 @@ namespace osu.Game.Online.API.Requests SortDirection sortDirection = SortDirection.Descending, SearchGenre genre = SearchGenre.Any, SearchLanguage language = SearchLanguage.Any, - SearchExtra extra = SearchExtra.Any) + SearchExtra extra = SearchExtra.Any, + SearchPlayed played = SearchPlayed.Any) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; @@ -50,6 +53,7 @@ namespace osu.Game.Online.API.Requests Genre = genre; Language = language; Extra = extra; + Played = played; } protected override WebRequest CreateWebRequest() @@ -94,6 +98,9 @@ namespace osu.Game.Online.API.Requests req.AddParameter("e", extraString); } + if (Played != SearchPlayed.Any) + req.AddParameter("played", Played.ToString().ToLowerInvariant()); + return req; } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 37fbfe7093..3f09a7e3d1 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -131,6 +131,7 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Genre.BindValueChanged(_ => queueUpdateSearch()); searchControl.Language.BindValueChanged(_ => queueUpdateSearch()); searchControl.Extra.BindValueChanged(_ => queueUpdateSearch()); + searchControl.Played.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch()); @@ -181,7 +182,8 @@ namespace osu.Game.Overlays.BeatmapListing sortControl.SortDirection.Value, searchControl.Genre.Value, searchControl.Language.Value, - searchControl.Extra.Value); + searchControl.Extra.Value, + searchControl.Played.Value); getSetsRequest.Success += response => { diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 437c26e36d..80beed6217 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -30,6 +30,8 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable Extra => extraFilter.Current; + public Bindable Played => playedFilter.Current; + public BeatmapSetInfo BeatmapSet { set @@ -51,6 +53,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchFilterRow genreFilter; private readonly BeatmapSearchFilterRow languageFilter; private readonly BeatmapSearchExtraFilterRow extraFilter; + private readonly BeatmapSearchFilterRow playedFilter; private readonly Box background; private readonly UpdateableBeatmapSetCover beatmapCover; @@ -108,7 +111,8 @@ namespace osu.Game.Overlays.BeatmapListing categoryFilter = new BeatmapSearchFilterRow(@"Categories"), genreFilter = new BeatmapSearchFilterRow(@"Genre"), languageFilter = new BeatmapSearchFilterRow(@"Language"), - extraFilter = new BeatmapSearchExtraFilterRow() + extraFilter = new BeatmapSearchExtraFilterRow(), + playedFilter = new BeatmapSearchFilterRow(@"Played") } } } diff --git a/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs b/osu.Game/Overlays/BeatmapListing/SearchPlayed.cs new file mode 100644 index 0000000000..eb7fb46158 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/SearchPlayed.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.BeatmapListing +{ + public enum SearchPlayed + { + Any, + Played, + Unplayed + } +} From 44471b4596bab45dea5ed4350c763d4522cfe35c Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 27 Oct 2020 20:19:15 +0100 Subject: [PATCH 4174/6909] Fix tests not building. --- osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 1c0af19322..9ef9649f77 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -192,7 +192,6 @@ namespace osu.Game.Tests.Visual.Background AddStep("Transition to Results", () => player.Push(results = new FadeAccessibleResults(new ScoreInfo { - Ruleset = new OsuRuleset().RulesetInfo, User = new User { Username = "osu!" }, Beatmap = new TestBeatmap(Ruleset.Value).BeatmapInfo, Ruleset = Ruleset.Value, From 1710b396e7195f34b024166d74129701ffc21eac Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Oct 2020 22:27:29 +0300 Subject: [PATCH 4175/6909] Implement BeatmapSearchMultipleSelectionFilterRow --- .../TestSceneBeatmapListingSearchControl.cs | 3 +- .../API/Requests/SearchBeatmapSetsRequest.cs | 33 ++------ .../BeatmapListingSearchControl.cs | 3 +- .../BeatmapSearchExtraFilterRow.cs | 79 ++---------------- .../BeatmapListing/BeatmapSearchFilterRow.cs | 64 +------------- ...BeatmapSearchMultipleSelectionFilterRow.cs | 83 +++++++++++++++++++ .../Overlays/BeatmapListing/FilterTabItem.cs | 74 +++++++++++++++++ .../Overlays/BeatmapListing/SearchExtra.cs | 4 +- 8 files changed, 179 insertions(+), 164 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs create mode 100644 osu.Game/Overlays/BeatmapListing/FilterTabItem.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index 5c9431aad1..9b0ef4d6f2 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -58,7 +59,7 @@ namespace osu.Game.Tests.Visual.UserInterface 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); - control.Extra.BindValueChanged(e => extra.Text = $"Extra: {e.NewValue}", true); + control.Extra.BindValueChanged(e => extra.Text = $"Extra: {(e.NewValue == null ? "" : string.Join(".", e.NewValue.Select(i => i.ToString().ToLowerInvariant())))}", true); control.Played.BindValueChanged(p => played.Text = $"Played: {p.NewValue}", true); } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 12383e7457..20bbd46529 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.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 System.Linq; using osu.Framework.IO.Network; using osu.Game.Extensions; using osu.Game.Overlays; @@ -21,7 +23,7 @@ namespace osu.Game.Online.API.Requests public SearchLanguage Language { get; } - public SearchExtra Extra { get; } + public List Extra { get; } public SearchPlayed Played { get; } @@ -40,7 +42,7 @@ namespace osu.Game.Online.API.Requests SortDirection sortDirection = SortDirection.Descending, SearchGenre genre = SearchGenre.Any, SearchLanguage language = SearchLanguage.Any, - SearchExtra extra = SearchExtra.Any, + List extra = null, SearchPlayed played = SearchPlayed.Any) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); @@ -74,33 +76,14 @@ namespace osu.Game.Online.API.Requests req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); - req.AddCursor(cursor); - - if (Extra != SearchExtra.Any) - { - string extraString = string.Empty; - - switch (Extra) - { - case SearchExtra.Both: - extraString = "video.storyboard"; - break; - - case SearchExtra.Storyboard: - extraString = "storyboard"; - break; - - case SearchExtra.Video: - extraString = "video"; - break; - } - - req.AddParameter("e", extraString); - } + if (Extra != null && Extra.Any()) + req.AddParameter("e", string.Join(".", Extra.Select(e => e.ToString().ToLowerInvariant()))); if (Played != SearchPlayed.Any) req.AddParameter("played", Played.ToString().ToLowerInvariant()); + req.AddCursor(cursor); + return req; } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 80beed6217..f390db3f35 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osuTK.Graphics; using osu.Game.Rulesets; +using System.Collections.Generic; namespace osu.Game.Overlays.BeatmapListing { @@ -28,7 +29,7 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable Language => languageFilter.Current; - public Bindable Extra => extraFilter.Current; + public Bindable> Extra => extraFilter.Current; public Bindable Played => playedFilter.Current; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs index 6e81cd2976..385978096c 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs @@ -1,94 +1,31 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; -using osuTK; - namespace osu.Game.Overlays.BeatmapListing { - public class BeatmapSearchExtraFilterRow : BeatmapSearchFilterRow + public class BeatmapSearchExtraFilterRow : BeatmapSearchMultipleSelectionFilterRow { public BeatmapSearchExtraFilterRow() : base("Extra") { } - protected override Drawable CreateFilter() => new ExtraFilter(); + protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new ExtraFilter(); - private class ExtraFilter : FillFlowContainer, IHasCurrentValue + private class ExtraFilter : MultipleSelectionFilter { - private readonly BindableWithCurrent current = new BindableWithCurrent(); - - public Bindable Current + protected override MultipleSelectionFilterTabItem[] CreateItems() => new[] { - get => current.Current; - set => current.Current = value; - } - - private readonly ExtraFilterTabItem videoItem; - private readonly ExtraFilterTabItem storyboardItem; - - public ExtraFilter() - { - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; - RelativeSizeAxes = Axes.X; - Height = 15; - Spacing = new Vector2(10, 0); - AddRange(new[] - { - videoItem = new ExtraFilterTabItem(SearchExtra.Video), - storyboardItem = new ExtraFilterTabItem(SearchExtra.Storyboard) - }); - - foreach (var item in Children) - item.StateUpdated += updateBindable; - } - - private void updateBindable() - { - if (videoItem.Active.Value && storyboardItem.Active.Value) - { - Current.Value = SearchExtra.Both; - return; - } - - if (videoItem.Active.Value) - { - Current.Value = SearchExtra.Video; - return; - } - - if (storyboardItem.Active.Value) - { - Current.Value = SearchExtra.Storyboard; - return; - } - - Current.Value = SearchExtra.Any; - } + new ExtraFilterTabItem(SearchExtra.Video), + new ExtraFilterTabItem(SearchExtra.Storyboard) + }; } - private class ExtraFilterTabItem : FilterTabItem + private class ExtraFilterTabItem : MultipleSelectionFilterTabItem { - public event Action StateUpdated; - public ExtraFilterTabItem(SearchExtra value) : base(value) { - Active.BindValueChanged(_ => StateUpdated?.Invoke()); - } - - protected override bool OnClick(ClickEvent e) - { - base.OnClick(e); - Active.Value = !Active.Value; - return true; } protected override string CreateText(SearchExtra value) => $@"Has {value.ToString()}"; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index ad32475b25..aa0fc0d00e 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -1,20 +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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osuTK; -using osuTK.Graphics; using Humanizer; using osu.Game.Utils; @@ -98,7 +94,7 @@ namespace osu.Game.Overlays.BeatmapListing protected override Dropdown CreateDropdown() => new FilterDropdown(); - protected override TabItem CreateTabItem(T value) => new FilterTabItem(value); + protected override TabItem CreateTabItem(T value) => new FilterTabItem(value); private class FilterDropdown : OsuTabDropdown { @@ -117,63 +113,5 @@ namespace osu.Game.Overlays.BeatmapListing } } } - - protected class FilterTabItem : TabItem - { - protected virtual float TextSize => 13; - - [Resolved] - private OverlayColourProvider colourProvider { get; set; } - - private readonly OsuSpriteText text; - - public FilterTabItem(T value) - : base(value) - { - AutoSizeAxes = Axes.Both; - Anchor = Anchor.BottomLeft; - Origin = Anchor.BottomLeft; - AddRangeInternal(new Drawable[] - { - text = new OsuSpriteText - { - Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Regular), - Text = CreateText(value) - }, - new HoverClickSounds() - }); - - Enabled.Value = true; - } - - protected virtual string CreateText(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); - - [BackgroundDependencyLoader] - private void load() - { - 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() => text.FadeColour(Active.Value ? Color4.White : getStateColour(), 200, Easing.OutQuint); - - private Color4 getStateColour() => IsHovered ? colourProvider.Light1 : colourProvider.Light3; - } } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs new file mode 100644 index 0000000000..c434e00ff3 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; +using osuTK; + +namespace osu.Game.Overlays.BeatmapListing +{ + public abstract class BeatmapSearchMultipleSelectionFilterRow : BeatmapSearchFilterRow> + { + public BeatmapSearchMultipleSelectionFilterRow(string headerName) + : base(headerName) + { + } + + protected override Drawable CreateFilter() => CreateMultipleSelectionFilter(); + + protected abstract MultipleSelectionFilter CreateMultipleSelectionFilter(); + + protected abstract class MultipleSelectionFilter : FillFlowContainer, IHasCurrentValue> + { + private readonly BindableWithCurrent> current = new BindableWithCurrent>(); + + public Bindable> Current + { + get => current.Current; + set => current.Current = value; + } + + public MultipleSelectionFilter() + { + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + RelativeSizeAxes = Axes.X; + Height = 15; + Spacing = new Vector2(10, 0); + AddRange(CreateItems()); + + foreach (var item in Children) + item.StateUpdated += updateBindable; + } + + protected abstract MultipleSelectionFilterTabItem[] CreateItems(); + + private void updateBindable() + { + var selectedValues = new List(); + + foreach (var item in Children) + { + if (item.Active.Value) + selectedValues.Add(item.Value); + } + + Current.Value = selectedValues; + } + } + + protected class MultipleSelectionFilterTabItem : FilterTabItem + { + public event Action StateUpdated; + + public MultipleSelectionFilterTabItem(T value) + : base(value) + { + Active.BindValueChanged(_ => StateUpdated?.Invoke()); + } + + protected override bool OnClick(ClickEvent e) + { + base.OnClick(e); + Active.Value = !Active.Value; + return true; + } + } + } +} diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs new file mode 100644 index 0000000000..32b48862e0 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +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 osuTK.Graphics; + +namespace osu.Game.Overlays.BeatmapListing +{ + public class FilterTabItem : TabItem + { + protected virtual float TextSize => 13; + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + private readonly OsuSpriteText text; + + public FilterTabItem(T value) + : base(value) + { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.BottomLeft; + Origin = Anchor.BottomLeft; + AddRangeInternal(new Drawable[] + { + text = new OsuSpriteText + { + Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Regular), + Text = CreateText(value) + }, + new HoverClickSounds() + }); + + Enabled.Value = true; + } + + protected virtual string CreateText(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); + + [BackgroundDependencyLoader] + private void load() + { + 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() => text.FadeColour(Active.Value ? Color4.White : getStateColour(), 200, Easing.OutQuint); + + private Color4 getStateColour() => IsHovered ? colourProvider.Light1 : colourProvider.Light3; + } +} diff --git a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs index 53900211e1..e9b3165d97 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs @@ -5,9 +5,7 @@ namespace osu.Game.Overlays.BeatmapListing { public enum SearchExtra { - Any, Video, - Storyboard, - Both + Storyboard } } From 008d1d697cee99eee4e5cf076dcb209f71189a9f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Oct 2020 23:14:48 +0300 Subject: [PATCH 4176/6909] Implement filtering by rank achieved --- .../TestSceneBeatmapListingSearchControl.cs | 3 ++ .../API/Requests/SearchBeatmapSetsRequest.cs | 7 ++++ .../BeatmapListingFilterControl.cs | 2 ++ .../BeatmapListingSearchControl.cs | 4 +++ .../BeatmapSearchExtraFilterRow.cs | 2 +- ...BeatmapSearchMultipleSelectionFilterRow.cs | 4 +-- .../BeatmapSearchRankFilterRow.cs | 35 +++++++++++++++++++ .../Overlays/BeatmapListing/FilterTabItem.cs | 10 +++--- .../Overlays/BeatmapListing/SearchRank.cs | 24 +++++++++++++ 9 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapSearchRankFilterRow.cs create mode 100644 osu.Game/Overlays/BeatmapListing/SearchRank.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index 9b0ef4d6f2..ff8162293b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -29,6 +29,7 @@ namespace osu.Game.Tests.Visual.UserInterface OsuSpriteText genre; OsuSpriteText language; OsuSpriteText extra; + OsuSpriteText ranks; OsuSpriteText played; Add(control = new BeatmapListingSearchControl @@ -50,6 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface genre = new OsuSpriteText(), language = new OsuSpriteText(), extra = new OsuSpriteText(), + ranks = new OsuSpriteText(), played = new OsuSpriteText() } }); @@ -60,6 +62,7 @@ namespace osu.Game.Tests.Visual.UserInterface control.Genre.BindValueChanged(g => genre.Text = $"Genre: {g.NewValue}", true); control.Language.BindValueChanged(l => language.Text = $"Language: {l.NewValue}", true); control.Extra.BindValueChanged(e => extra.Text = $"Extra: {(e.NewValue == null ? "" : string.Join(".", e.NewValue.Select(i => i.ToString().ToLowerInvariant())))}", true); + control.Ranks.BindValueChanged(r => ranks.Text = $"Ranks: {(r.NewValue == null ? "" : string.Join(".", r.NewValue.Select(i => i.ToString())))}", true); control.Played.BindValueChanged(p => played.Text = $"Played: {p.NewValue}", true); } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 20bbd46529..f819a5778f 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -27,6 +27,8 @@ namespace osu.Game.Online.API.Requests public SearchPlayed Played { get; } + public List Ranks { get; } + private readonly string query; private readonly RulesetInfo ruleset; private readonly Cursor cursor; @@ -43,6 +45,7 @@ namespace osu.Game.Online.API.Requests SearchGenre genre = SearchGenre.Any, SearchLanguage language = SearchLanguage.Any, List extra = null, + List ranks = null, SearchPlayed played = SearchPlayed.Any) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); @@ -55,6 +58,7 @@ namespace osu.Game.Online.API.Requests Genre = genre; Language = language; Extra = extra; + Ranks = ranks; Played = played; } @@ -79,6 +83,9 @@ namespace osu.Game.Online.API.Requests if (Extra != null && Extra.Any()) req.AddParameter("e", string.Join(".", Extra.Select(e => e.ToString().ToLowerInvariant()))); + if (Ranks != null && Ranks.Any()) + req.AddParameter("r", string.Join(".", Ranks.Select(r => r.ToString()))); + if (Played != SearchPlayed.Any) req.AddParameter("played", Played.ToString().ToLowerInvariant()); diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 3f09a7e3d1..86bf3276fe 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -131,6 +131,7 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Genre.BindValueChanged(_ => queueUpdateSearch()); searchControl.Language.BindValueChanged(_ => queueUpdateSearch()); searchControl.Extra.BindValueChanged(_ => queueUpdateSearch()); + searchControl.Ranks.BindValueChanged(_ => queueUpdateSearch()); searchControl.Played.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); @@ -183,6 +184,7 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Genre.Value, searchControl.Language.Value, searchControl.Extra.Value, + searchControl.Ranks.Value, searchControl.Played.Value); getSetsRequest.Success += response => diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index f390db3f35..64bd7065b5 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -31,6 +31,8 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable> Extra => extraFilter.Current; + public Bindable> Ranks => ranksFilter.Current; + public Bindable Played => playedFilter.Current; public BeatmapSetInfo BeatmapSet @@ -54,6 +56,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchFilterRow genreFilter; private readonly BeatmapSearchFilterRow languageFilter; private readonly BeatmapSearchExtraFilterRow extraFilter; + private readonly BeatmapSearchRankFilterRow ranksFilter; private readonly BeatmapSearchFilterRow playedFilter; private readonly Box background; @@ -113,6 +116,7 @@ namespace osu.Game.Overlays.BeatmapListing genreFilter = new BeatmapSearchFilterRow(@"Genre"), languageFilter = new BeatmapSearchFilterRow(@"Language"), extraFilter = new BeatmapSearchExtraFilterRow(), + ranksFilter = new BeatmapSearchRankFilterRow(), playedFilter = new BeatmapSearchFilterRow(@"Played") } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs index 385978096c..d8bf18fb88 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.BeatmapListing private class ExtraFilter : MultipleSelectionFilter { - protected override MultipleSelectionFilterTabItem[] CreateItems() => new[] + protected override MultipleSelectionFilterTabItem[] CreateItems() => new MultipleSelectionFilterTabItem[] { new ExtraFilterTabItem(SearchExtra.Video), new ExtraFilterTabItem(SearchExtra.Storyboard) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index c434e00ff3..acf2c62afa 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.BeatmapListing { public abstract class BeatmapSearchMultipleSelectionFilterRow : BeatmapSearchFilterRow> { - public BeatmapSearchMultipleSelectionFilterRow(string headerName) + protected BeatmapSearchMultipleSelectionFilterRow(string headerName) : base(headerName) { } @@ -33,7 +33,7 @@ namespace osu.Game.Overlays.BeatmapListing set => current.Current = value; } - public MultipleSelectionFilter() + protected MultipleSelectionFilter() { Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRankFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRankFilterRow.cs new file mode 100644 index 0000000000..d521e8c90f --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRankFilterRow.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.Linq; +using osu.Framework.Extensions; + +namespace osu.Game.Overlays.BeatmapListing +{ + public class BeatmapSearchRankFilterRow : BeatmapSearchMultipleSelectionFilterRow + { + public BeatmapSearchRankFilterRow() + : base("Rank Achieved") + { + } + + protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new RankFilter(); + + private class RankFilter : MultipleSelectionFilter + { + protected override MultipleSelectionFilterTabItem[] CreateItems() + => ((SearchRank[])Enum.GetValues(typeof(SearchRank))).Select(v => new RankFilterTabItem(v)).ToArray(); + } + + private class RankFilterTabItem : MultipleSelectionFilterTabItem + { + public RankFilterTabItem(SearchRank value) + : base(value) + { + } + + protected override string CreateText(SearchRank value) => $@"{value.GetDescription() ?? value.ToString()}"; + } + } +} diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 32b48862e0..9bdd5b3fad 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -16,8 +16,6 @@ namespace osu.Game.Overlays.BeatmapListing { public class FilterTabItem : TabItem { - protected virtual float TextSize => 13; - [Resolved] private OverlayColourProvider colourProvider { get; set; } @@ -33,7 +31,7 @@ namespace osu.Game.Overlays.BeatmapListing { text = new OsuSpriteText { - Font = OsuFont.GetFont(size: TextSize, weight: FontWeight.Regular), + Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), Text = CreateText(value) }, new HoverClickSounds() @@ -67,7 +65,11 @@ namespace osu.Game.Overlays.BeatmapListing protected override void OnDeactivated() => updateState(); - private void updateState() => text.FadeColour(Active.Value ? Color4.White : getStateColour(), 200, Easing.OutQuint); + private void updateState() + { + text.FadeColour(Active.Value ? Color4.White : getStateColour(), 200, Easing.OutQuint); + text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular); + } private Color4 getStateColour() => IsHovered ? colourProvider.Light1 : colourProvider.Light3; } diff --git a/osu.Game/Overlays/BeatmapListing/SearchRank.cs b/osu.Game/Overlays/BeatmapListing/SearchRank.cs new file mode 100644 index 0000000000..8b1882026c --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/SearchRank.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.ComponentModel; + +namespace osu.Game.Overlays.BeatmapListing +{ + public enum SearchRank + { + [Description(@"Silver SS")] + XH, + + [Description(@"SS")] + X, + + [Description(@"Silver S")] + SH, + S, + A, + B, + C, + D + } +} From c4efceceb2a384af3468db13839c6668675038c3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 27 Oct 2020 23:57:11 +0300 Subject: [PATCH 4177/6909] Use char instead of sting for request parameter creation --- .../UserInterface/TestSceneBeatmapListingSearchControl.cs | 4 ++-- osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index ff8162293b..e07aa71b1f 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -61,8 +61,8 @@ namespace osu.Game.Tests.Visual.UserInterface 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); - control.Extra.BindValueChanged(e => extra.Text = $"Extra: {(e.NewValue == null ? "" : string.Join(".", e.NewValue.Select(i => i.ToString().ToLowerInvariant())))}", true); - control.Ranks.BindValueChanged(r => ranks.Text = $"Ranks: {(r.NewValue == null ? "" : string.Join(".", r.NewValue.Select(i => i.ToString())))}", true); + control.Extra.BindValueChanged(e => extra.Text = $"Extra: {(e.NewValue == null ? "" : string.Join('.', e.NewValue.Select(i => i.ToString().ToLowerInvariant())))}", true); + control.Ranks.BindValueChanged(r => ranks.Text = $"Ranks: {(r.NewValue == null ? "" : string.Join('.', r.NewValue.Select(i => i.ToString())))}", true); control.Played.BindValueChanged(p => played.Text = $"Played: {p.NewValue}", true); } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index f819a5778f..248096d8b3 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -81,10 +81,10 @@ namespace osu.Game.Online.API.Requests req.AddParameter("sort", $"{SortCriteria.ToString().ToLowerInvariant()}_{directionString}"); if (Extra != null && Extra.Any()) - req.AddParameter("e", string.Join(".", Extra.Select(e => e.ToString().ToLowerInvariant()))); + req.AddParameter("e", string.Join('.', Extra.Select(e => e.ToString().ToLowerInvariant()))); if (Ranks != null && Ranks.Any()) - req.AddParameter("r", string.Join(".", Ranks.Select(r => r.ToString()))); + req.AddParameter("r", string.Join('.', Ranks.Select(r => r.ToString()))); if (Played != SearchPlayed.Any) req.AddParameter("played", Played.ToString().ToLowerInvariant()); From 0a7f3dc19bd86057a11d129250b852732af39dc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 07:29:07 +0900 Subject: [PATCH 4178/6909] Avoid null reference on finalization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Screens/Play/Spectator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index dd6b434889..898d68a7d6 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -236,7 +236,7 @@ namespace osu.Game.Screens.Play spectatorStreaming.StopWatchingUser((int)targetUser.Id); } - managerUpdated.UnbindAll(); + managerUpdated?.UnbindAll(); } } } From b4ec3b9fefa6206558cf3f3e0ea6541cac0948e6 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Oct 2020 01:41:46 +0300 Subject: [PATCH 4179/6909] Simplify MultipleSelectionFilterTabItem state changes --- .../BeatmapSearchMultipleSelectionFilterRow.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index acf2c62afa..35c982d35a 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.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.Bindables; using osu.Framework.Graphics; @@ -43,7 +42,7 @@ namespace osu.Game.Overlays.BeatmapListing AddRange(CreateItems()); foreach (var item in Children) - item.StateUpdated += updateBindable; + item.Active.BindValueChanged(_ => updateBindable()); } protected abstract MultipleSelectionFilterTabItem[] CreateItems(); @@ -64,18 +63,15 @@ namespace osu.Game.Overlays.BeatmapListing protected class MultipleSelectionFilterTabItem : FilterTabItem { - public event Action StateUpdated; - public MultipleSelectionFilterTabItem(T value) : base(value) { - Active.BindValueChanged(_ => StateUpdated?.Invoke()); } protected override bool OnClick(ClickEvent e) { base.OnClick(e); - Active.Value = !Active.Value; + Active.Toggle(); return true; } } From fd11346a289e03b0a5f2f1f2cd5d1da3a578fffe Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Oct 2020 01:48:24 +0300 Subject: [PATCH 4180/6909] Update button colours --- osu.Game/Overlays/BeatmapListing/FilterTabItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 9bdd5b3fad..721a3c839c 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -67,10 +67,10 @@ namespace osu.Game.Overlays.BeatmapListing private void updateState() { - text.FadeColour(Active.Value ? Color4.White : getStateColour(), 200, Easing.OutQuint); + text.FadeColour(IsHovered ? colourProvider.Light1 : getStateColour(), 200, Easing.OutQuint); text.Font = text.Font.With(weight: Active.Value ? FontWeight.SemiBold : FontWeight.Regular); } - private Color4 getStateColour() => IsHovered ? colourProvider.Light1 : colourProvider.Light3; + private Color4 getStateColour() => Active.Value ? colourProvider.Content1 : colourProvider.Light2; } } From 03c5057a921d8ddf21666671c76e23bc396b8a60 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Oct 2020 02:28:31 +0300 Subject: [PATCH 4181/6909] Simplify BeatmapSearchMultipleSelectionFilterRow --- .../BeatmapListingSearchControl.cs | 8 ++--- .../BeatmapSearchExtraFilterRow.cs | 34 ------------------ ...BeatmapSearchMultipleSelectionFilterRow.cs | 21 ++++++----- .../BeatmapSearchRankFilterRow.cs | 35 ------------------- .../Overlays/BeatmapListing/FilterTabItem.cs | 4 +-- .../Overlays/BeatmapListing/SearchExtra.cs | 4 +++ 6 files changed, 19 insertions(+), 87 deletions(-) delete mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs delete mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapSearchRankFilterRow.cs diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 64bd7065b5..a976890c7c 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -55,8 +55,8 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchFilterRow categoryFilter; private readonly BeatmapSearchFilterRow genreFilter; private readonly BeatmapSearchFilterRow languageFilter; - private readonly BeatmapSearchExtraFilterRow extraFilter; - private readonly BeatmapSearchRankFilterRow ranksFilter; + private readonly BeatmapSearchMultipleSelectionFilterRow extraFilter; + private readonly BeatmapSearchMultipleSelectionFilterRow ranksFilter; private readonly BeatmapSearchFilterRow playedFilter; private readonly Box background; @@ -115,8 +115,8 @@ namespace osu.Game.Overlays.BeatmapListing categoryFilter = new BeatmapSearchFilterRow(@"Categories"), genreFilter = new BeatmapSearchFilterRow(@"Genre"), languageFilter = new BeatmapSearchFilterRow(@"Language"), - extraFilter = new BeatmapSearchExtraFilterRow(), - ranksFilter = new BeatmapSearchRankFilterRow(), + extraFilter = new BeatmapSearchMultipleSelectionFilterRow(@"Extra"), + ranksFilter = new BeatmapSearchMultipleSelectionFilterRow(@"Rank Achieved"), playedFilter = new BeatmapSearchFilterRow(@"Played") } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs deleted file mode 100644 index d8bf18fb88..0000000000 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchExtraFilterRow.cs +++ /dev/null @@ -1,34 +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.Overlays.BeatmapListing -{ - public class BeatmapSearchExtraFilterRow : BeatmapSearchMultipleSelectionFilterRow - { - public BeatmapSearchExtraFilterRow() - : base("Extra") - { - } - - protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new ExtraFilter(); - - private class ExtraFilter : MultipleSelectionFilter - { - protected override MultipleSelectionFilterTabItem[] CreateItems() => new MultipleSelectionFilterTabItem[] - { - new ExtraFilterTabItem(SearchExtra.Video), - new ExtraFilterTabItem(SearchExtra.Storyboard) - }; - } - - private class ExtraFilterTabItem : MultipleSelectionFilterTabItem - { - public ExtraFilterTabItem(SearchExtra value) - : base(value) - { - } - - protected override string CreateText(SearchExtra value) => $@"Has {value.ToString()}"; - } - } -} diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 35c982d35a..cb89560e39 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.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 osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -11,18 +13,16 @@ using osuTK; namespace osu.Game.Overlays.BeatmapListing { - public abstract class BeatmapSearchMultipleSelectionFilterRow : BeatmapSearchFilterRow> + public class BeatmapSearchMultipleSelectionFilterRow : BeatmapSearchFilterRow> { - protected BeatmapSearchMultipleSelectionFilterRow(string headerName) + public BeatmapSearchMultipleSelectionFilterRow(string headerName) : base(headerName) { } - protected override Drawable CreateFilter() => CreateMultipleSelectionFilter(); + protected override Drawable CreateFilter() => new MultipleSelectionFilter(); - protected abstract MultipleSelectionFilter CreateMultipleSelectionFilter(); - - protected abstract class MultipleSelectionFilter : FillFlowContainer, IHasCurrentValue> + private class MultipleSelectionFilter : FillFlowContainer, IHasCurrentValue> { private readonly BindableWithCurrent> current = new BindableWithCurrent>(); @@ -32,21 +32,20 @@ namespace osu.Game.Overlays.BeatmapListing set => current.Current = value; } - protected MultipleSelectionFilter() + public MultipleSelectionFilter() { Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; RelativeSizeAxes = Axes.X; Height = 15; Spacing = new Vector2(10, 0); - AddRange(CreateItems()); + + ((T[])Enum.GetValues(typeof(T))).ForEach(i => Add(new MultipleSelectionFilterTabItem(i))); foreach (var item in Children) item.Active.BindValueChanged(_ => updateBindable()); } - protected abstract MultipleSelectionFilterTabItem[] CreateItems(); - private void updateBindable() { var selectedValues = new List(); @@ -61,7 +60,7 @@ namespace osu.Game.Overlays.BeatmapListing } } - protected class MultipleSelectionFilterTabItem : FilterTabItem + private class MultipleSelectionFilterTabItem : FilterTabItem { public MultipleSelectionFilterTabItem(T value) : base(value) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRankFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchRankFilterRow.cs deleted file mode 100644 index d521e8c90f..0000000000 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchRankFilterRow.cs +++ /dev/null @@ -1,35 +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.Linq; -using osu.Framework.Extensions; - -namespace osu.Game.Overlays.BeatmapListing -{ - public class BeatmapSearchRankFilterRow : BeatmapSearchMultipleSelectionFilterRow - { - public BeatmapSearchRankFilterRow() - : base("Rank Achieved") - { - } - - protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new RankFilter(); - - private class RankFilter : MultipleSelectionFilter - { - protected override MultipleSelectionFilterTabItem[] CreateItems() - => ((SearchRank[])Enum.GetValues(typeof(SearchRank))).Select(v => new RankFilterTabItem(v)).ToArray(); - } - - private class RankFilterTabItem : MultipleSelectionFilterTabItem - { - public RankFilterTabItem(SearchRank value) - : base(value) - { - } - - protected override string CreateText(SearchRank value) => $@"{value.GetDescription() ?? value.ToString()}"; - } - } -} diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 721a3c839c..244ef5a703 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.BeatmapListing text = new OsuSpriteText { Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), - Text = CreateText(value) + Text = (value as Enum)?.GetDescription() ?? value.ToString() }, new HoverClickSounds() }); @@ -40,8 +40,6 @@ namespace osu.Game.Overlays.BeatmapListing Enabled.Value = true; } - protected virtual string CreateText(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs index e9b3165d97..0ee60c4a95 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs @@ -1,11 +1,15 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.ComponentModel; + namespace osu.Game.Overlays.BeatmapListing { public enum SearchExtra { + [Description("Has Video")] Video, + [Description("Has Storyboard")] Storyboard } } From 6fd3686c4d443ca4c4daad14e1f0885d7747b26f Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Oct 2020 02:36:35 +0300 Subject: [PATCH 4182/6909] Use IReadOnlyCollection instead of List in SearchBeatmapSetsRequest --- 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 248096d8b3..708b58d954 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -23,11 +23,11 @@ namespace osu.Game.Online.API.Requests public SearchLanguage Language { get; } - public List Extra { get; } + public IReadOnlyCollection Extra { get; } public SearchPlayed Played { get; } - public List Ranks { get; } + public IReadOnlyCollection Ranks { get; } private readonly string query; private readonly RulesetInfo ruleset; @@ -44,8 +44,8 @@ namespace osu.Game.Online.API.Requests SortDirection sortDirection = SortDirection.Descending, SearchGenre genre = SearchGenre.Any, SearchLanguage language = SearchLanguage.Any, - List extra = null, - List ranks = null, + IReadOnlyCollection extra = null, + IReadOnlyCollection ranks = null, SearchPlayed played = SearchPlayed.Any) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); From 914bd537885c946c8bd5fda14aecde6c8b3bc90d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Oct 2020 02:39:51 +0300 Subject: [PATCH 4183/6909] Add missing blank line --- osu.Game/Overlays/BeatmapListing/SearchExtra.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs index 0ee60c4a95..af37e3264f 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchExtra.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchExtra.cs @@ -9,6 +9,7 @@ namespace osu.Game.Overlays.BeatmapListing { [Description("Has Video")] Video, + [Description("Has Storyboard")] Storyboard } From 01b576c8611bfca9f3d2ba3bad5974ed06688759 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 13:32:39 +0900 Subject: [PATCH 4184/6909] Fix editor crash on exit when forcing exit twice in a row --- osu.Game/Screens/Edit/Editor.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index c3560dff38..0aaa551af9 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -444,12 +444,21 @@ namespace osu.Game.Screens.Edit if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog) { confirmExit(); - return true; + return false; } if (isNewBeatmap || HasUnsavedChanges) { - dialogOverlay?.Push(new PromptForSaveDialog(confirmExit, confirmExitWithSave)); + dialogOverlay?.Push(new PromptForSaveDialog(() => + { + confirmExit(); + this.Exit(); + }, () => + { + confirmExitWithSave(); + this.Exit(); + })); + return true; } } @@ -464,7 +473,6 @@ namespace osu.Game.Screens.Edit { exitConfirmed = true; Save(); - this.Exit(); } private void confirmExit() @@ -483,7 +491,6 @@ namespace osu.Game.Screens.Edit } exitConfirmed = true; - this.Exit(); } private readonly Bindable clipboard = new Bindable(); From 3e5322541d9d25c11f26ca0d8c2a13b070b4ae72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 14:35:42 +0900 Subject: [PATCH 4185/6909] Make direction setting more clear --- .../Replays/FramedReplayInputHandler.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index d7be809d34..120d81816f 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using JetBrains.Annotations; using osu.Game.Input.Handlers; using osu.Game.Replays; @@ -112,15 +113,9 @@ namespace osu.Game.Rulesets.Replays /// The usable time value. If null, we should not advance time as we do not have enough data. public override double? SetFrameFromTime(double time) { - if (!CurrentTime.HasValue) - { - currentDirection = 1; - } - else - { - currentDirection = time.CompareTo(CurrentTime); - if (currentDirection == 0) currentDirection = 1; - } + updateDirection(time); + + Debug.Assert(currentDirection != 0); if (HasFrames) { @@ -171,5 +166,18 @@ namespace osu.Game.Rulesets.Replays return CurrentTime = time; } + + private void updateDirection(double time) + { + if (!CurrentTime.HasValue) + { + currentDirection = 1; + } + else + { + currentDirection = time.CompareTo(CurrentTime); + if (currentDirection == 0) currentDirection = 1; + } + } } } From 9b9a41596f4f651d03321e42eef89609d48954c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 14:42:23 +0900 Subject: [PATCH 4186/6909] Split out frame stability calculation to own method --- .../Rulesets/UI/FrameStabilityContainer.cs | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 28b7975a89..94684f33ed 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -119,35 +119,19 @@ namespace osu.Game.Rulesets.UI if (parentGameplayClock == null) setClock(); // LoadComplete may not be run yet, but we still want the clock. + // each update start with considering things in valid state. validState = true; requireMoreUpdateLoops = false; - var newProposedTime = parentGameplayClock.CurrentTime; + // our goal is to catch up to the time provided by the parent clock. + var proposedTime = parentGameplayClock.CurrentTime; try { if (FrameStablePlayback) - { - if (firstConsumption) - { - // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. - // Instead we perform an initial seek to the proposed time. - - // process frame (in addition to finally clause) to clear out ElapsedTime - manualClock.CurrentTime = newProposedTime; - framedClock.ProcessFrame(); - - firstConsumption = false; - } - else if (manualClock.CurrentTime < gameplayStartTime) - manualClock.CurrentTime = newProposedTime = Math.Min(gameplayStartTime, newProposedTime); - else if (Math.Abs(manualClock.CurrentTime - newProposedTime) > sixty_frame_time * 1.2f) - { - newProposedTime = newProposedTime > manualClock.CurrentTime - ? Math.Min(newProposedTime, manualClock.CurrentTime + sixty_frame_time) - : Math.Max(newProposedTime, manualClock.CurrentTime - sixty_frame_time); - } - } + // if we require frame stability, the proposed time will be adjusted to move at most one known + // frame interval in the current direction. + applyFrameStability(ref proposedTime); if (isAttached) { @@ -156,7 +140,7 @@ namespace osu.Game.Rulesets.UI if (FrameStablePlayback) { // when stability is turned on, we shouldn't execute for time values the replay is unable to satisfy. - if ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) == null) + if ((newTime = ReplayInputHandler.SetFrameFromTime(proposedTime)) == null) { // setting invalid state here ensures that gameplay will not continue (ie. our child // hierarchy won't be updated). @@ -173,7 +157,7 @@ namespace osu.Game.Rulesets.UI // when stability is disabled, we don't really care about accuracy. // looping over the replay will allow it to catch up and feed out the required values // for the current time. - while ((newTime = ReplayInputHandler.SetFrameFromTime(newProposedTime)) != newProposedTime) + while ((newTime = ReplayInputHandler.SetFrameFromTime(proposedTime)) != proposedTime) { if (newTime == null) { @@ -185,15 +169,15 @@ namespace osu.Game.Rulesets.UI } } - newProposedTime = newTime.Value; + proposedTime = newTime.Value; } } finally { - if (newProposedTime != manualClock.CurrentTime) - direction = newProposedTime > manualClock.CurrentTime ? 1 : -1; + if (proposedTime != manualClock.CurrentTime) + direction = proposedTime > manualClock.CurrentTime ? 1 : -1; - manualClock.CurrentTime = newProposedTime; + manualClock.CurrentTime = proposedTime; manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; manualClock.IsRunning = parentGameplayClock.IsRunning; @@ -209,6 +193,35 @@ namespace osu.Game.Rulesets.UI } } + /// + /// Apply frame stability modifier to a time. + /// + /// The time which is to be displayed. + private void applyFrameStability(ref double proposedTime) + { + if (firstConsumption) + { + // On the first update, frame-stability seeking would result in unexpected/unwanted behaviour. + // Instead we perform an initial seek to the proposed time. + + // process frame (in addition to finally clause) to clear out ElapsedTime + manualClock.CurrentTime = proposedTime; + framedClock.ProcessFrame(); + + firstConsumption = false; + return; + } + + if (manualClock.CurrentTime < gameplayStartTime) + manualClock.CurrentTime = proposedTime = Math.Min(gameplayStartTime, proposedTime); + else if (Math.Abs(manualClock.CurrentTime - proposedTime) > sixty_frame_time * 1.2f) + { + proposedTime = proposedTime > manualClock.CurrentTime + ? Math.Min(proposedTime, manualClock.CurrentTime + sixty_frame_time) + : Math.Max(proposedTime, manualClock.CurrentTime - sixty_frame_time); + } + } + private void setClock() { if (parentGameplayClock == null) From 8c9bda2ded2a973bd4896aa7795fb30830804af2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 14:53:31 +0900 Subject: [PATCH 4187/6909] Split out replay update method --- .../Rulesets/UI/FrameStabilityContainer.cs | 85 ++++++++++--------- 1 file changed, 46 insertions(+), 39 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 94684f33ed..7f27b283e3 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.UI protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState; - private bool isAttached => ReplayInputHandler != null; + private bool hasReplayAttached => ReplayInputHandler != null; private const double sixty_frame_time = 1000.0 / 60; @@ -133,44 +133,8 @@ namespace osu.Game.Rulesets.UI // frame interval in the current direction. applyFrameStability(ref proposedTime); - if (isAttached) - { - double? newTime; - - if (FrameStablePlayback) - { - // when stability is turned on, we shouldn't execute for time values the replay is unable to satisfy. - if ((newTime = ReplayInputHandler.SetFrameFromTime(proposedTime)) == null) - { - // setting invalid state here ensures that gameplay will not continue (ie. our child - // hierarchy won't be updated). - validState = false; - - // potentially loop to catch-up playback. - requireMoreUpdateLoops = true; - - return; - } - } - else - { - // when stability is disabled, we don't really care about accuracy. - // looping over the replay will allow it to catch up and feed out the required values - // for the current time. - while ((newTime = ReplayInputHandler.SetFrameFromTime(proposedTime)) != proposedTime) - { - if (newTime == null) - { - // special case for when the replay actually can't arrive at the required time. - // protects from potential endless loop. - validState = false; - return; - } - } - } - - proposedTime = newTime.Value; - } + if (hasReplayAttached) + updateReplay(ref proposedTime); } finally { @@ -193,6 +157,49 @@ namespace osu.Game.Rulesets.UI } } + /// + /// Attempt to advance replay playback for a given time. + /// + /// The time which is to be displayed. + private bool updateReplay(ref double proposedTime) + { + double? newTime; + + if (FrameStablePlayback) + { + // when stability is turned on, we shouldn't execute for time values the replay is unable to satisfy. + if ((newTime = ReplayInputHandler.SetFrameFromTime(proposedTime)) == null) + { + // setting invalid state here ensures that gameplay will not continue (ie. our child + // hierarchy won't be updated). + validState = false; + + // potentially loop to catch-up playback. + requireMoreUpdateLoops = true; + + return false; + } + } + else + { + // when stability is disabled, we don't really care about accuracy. + // looping over the replay will allow it to catch up and feed out the required values + // for the current time. + while ((newTime = ReplayInputHandler.SetFrameFromTime(proposedTime)) != proposedTime) + { + if (newTime == null) + { + // special case for when the replay actually can't arrive at the required time. + // protects from potential endless loop. + return false; + } + } + } + + proposedTime = newTime.Value; + return true; + } + /// /// Apply frame stability modifier to a time. /// From a06516c9004ad7273ace9ce161d9851ec1b055d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 15:11:53 +0900 Subject: [PATCH 4188/6909] Extract out frame stability state into enum for (hopefully) better clarity --- .../Rulesets/UI/FrameStabilityContainer.cs | 81 ++++++++++--------- 1 file changed, 42 insertions(+), 39 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 7f27b283e3..12e4dd8b01 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -73,19 +73,9 @@ namespace osu.Game.Rulesets.UI setClock(); } - /// - /// Whether we are running up-to-date with our parent clock. - /// If not, we will need to keep processing children until we catch up. - /// - private bool requireMoreUpdateLoops; + private PlaybackState state; - /// - /// Whether we are in a valid state (ie. should we keep processing children frames). - /// This should be set to false when the replay is, for instance, waiting for future frames to arrive. - /// - private bool validState; - - protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && validState; + protected override bool RequiresChildrenUpdate => base.RequiresChildrenUpdate && state != PlaybackState.NotValid; private bool hasReplayAttached => ReplayInputHandler != null; @@ -95,20 +85,19 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { - requireMoreUpdateLoops = true; - validState = !frameStableClock.IsPaused.Value; + state = frameStableClock.IsPaused.Value ? PlaybackState.NotValid : PlaybackState.Valid; - int loops = 0; + int loops = MaxCatchUpFrames; - while (validState && requireMoreUpdateLoops && loops++ < MaxCatchUpFrames) + while (state != PlaybackState.NotValid && loops-- > 0) { updateClock(); - if (validState) - { - base.UpdateSubTree(); - UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat); - } + if (state == PlaybackState.NotValid) + break; + + base.UpdateSubTree(); + UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat); } return true; @@ -120,8 +109,7 @@ namespace osu.Game.Rulesets.UI setClock(); // LoadComplete may not be run yet, but we still want the clock. // each update start with considering things in valid state. - validState = true; - requireMoreUpdateLoops = false; + state = PlaybackState.Valid; // our goal is to catch up to the time provided by the parent clock. var proposedTime = parentGameplayClock.CurrentTime; @@ -134,7 +122,7 @@ namespace osu.Game.Rulesets.UI applyFrameStability(ref proposedTime); if (hasReplayAttached) - updateReplay(ref proposedTime); + state = updateReplay(ref proposedTime); } finally { @@ -147,7 +135,9 @@ namespace osu.Game.Rulesets.UI double timeBehind = Math.Abs(manualClock.CurrentTime - parentGameplayClock.CurrentTime); - requireMoreUpdateLoops |= timeBehind != 0; + // determine whether catch-up is required. + if (state != PlaybackState.NotValid) + state = timeBehind > 0 ? PlaybackState.RequiresCatchUp : PlaybackState.Valid; frameStableClock.IsCatchingUp.Value = timeBehind > 200; @@ -161,24 +151,14 @@ namespace osu.Game.Rulesets.UI /// Attempt to advance replay playback for a given time. /// /// The time which is to be displayed. - private bool updateReplay(ref double proposedTime) + private PlaybackState updateReplay(ref double proposedTime) { double? newTime; if (FrameStablePlayback) { // when stability is turned on, we shouldn't execute for time values the replay is unable to satisfy. - if ((newTime = ReplayInputHandler.SetFrameFromTime(proposedTime)) == null) - { - // setting invalid state here ensures that gameplay will not continue (ie. our child - // hierarchy won't be updated). - validState = false; - - // potentially loop to catch-up playback. - requireMoreUpdateLoops = true; - - return false; - } + newTime = ReplayInputHandler.SetFrameFromTime(proposedTime); } else { @@ -191,13 +171,16 @@ namespace osu.Game.Rulesets.UI { // special case for when the replay actually can't arrive at the required time. // protects from potential endless loop. - return false; + break; } } } + if (newTime == null) + return PlaybackState.NotValid; + proposedTime = newTime.Value; - return true; + return PlaybackState.Valid; } /// @@ -244,6 +227,26 @@ namespace osu.Game.Rulesets.UI public ReplayInputHandler ReplayInputHandler { get; set; } + private enum PlaybackState + { + /// + /// Playback is not possible. Child hierarchy should not be processed. + /// + NotValid, + + /// + /// Whether we are running up-to-date with our parent clock. + /// If not, we will need to keep processing children until we catch up. + /// + RequiresCatchUp, + + /// + /// Whether we are in a valid state (ie. should we keep processing children frames). + /// This should be set to false when the replay is, for instance, waiting for future frames to arrive. + /// + Valid + } + private class FrameStabilityClock : GameplayClock, IFrameStableClock { public GameplayClock ParentGameplayClock; From 59e9c2639ad503eff93b7d848de21c12abfecfeb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 15:12:39 +0900 Subject: [PATCH 4189/6909] Remove try-finally --- .../Rulesets/UI/FrameStabilityContainer.cs | 45 +++++++++---------- 1 file changed, 20 insertions(+), 25 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 12e4dd8b01..4386acfcce 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -114,37 +114,32 @@ namespace osu.Game.Rulesets.UI // our goal is to catch up to the time provided by the parent clock. var proposedTime = parentGameplayClock.CurrentTime; - try - { - if (FrameStablePlayback) - // if we require frame stability, the proposed time will be adjusted to move at most one known - // frame interval in the current direction. - applyFrameStability(ref proposedTime); + if (FrameStablePlayback) + // if we require frame stability, the proposed time will be adjusted to move at most one known + // frame interval in the current direction. + applyFrameStability(ref proposedTime); - if (hasReplayAttached) - state = updateReplay(ref proposedTime); - } - finally - { - if (proposedTime != manualClock.CurrentTime) - direction = proposedTime > manualClock.CurrentTime ? 1 : -1; + if (hasReplayAttached) + state = updateReplay(ref proposedTime); - manualClock.CurrentTime = proposedTime; - manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; - manualClock.IsRunning = parentGameplayClock.IsRunning; + if (proposedTime != manualClock.CurrentTime) + direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; - double timeBehind = Math.Abs(manualClock.CurrentTime - parentGameplayClock.CurrentTime); + manualClock.CurrentTime = proposedTime; + manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; + manualClock.IsRunning = parentGameplayClock.IsRunning; - // determine whether catch-up is required. - if (state != PlaybackState.NotValid) - state = timeBehind > 0 ? PlaybackState.RequiresCatchUp : PlaybackState.Valid; + double timeBehind = Math.Abs(manualClock.CurrentTime - parentGameplayClock.CurrentTime); - frameStableClock.IsCatchingUp.Value = timeBehind > 200; + // determine whether catch-up is required. + if (state != PlaybackState.NotValid) + state = timeBehind > 0 ? PlaybackState.RequiresCatchUp : PlaybackState.Valid; - // The manual clock time has changed in the above code. The framed clock now needs to be updated - // to ensure that the its time is valid for our children before input is processed - framedClock.ProcessFrame(); - } + frameStableClock.IsCatchingUp.Value = timeBehind > 200; + + // The manual clock time has changed in the above code. The framed clock now needs to be updated + // to ensure that the its time is valid for our children before input is processed + framedClock.ProcessFrame(); } /// From c9515653b303a8fc3ec8753aae6c7114f86f94fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 15:31:57 +0900 Subject: [PATCH 4190/6909] Restore previous directionality logic to avoid logic differences --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 4386acfcce..7e17c93bed 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.UI state = updateReplay(ref proposedTime); if (proposedTime != manualClock.CurrentTime) - direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; + direction = proposedTime > manualClock.CurrentTime ? 1 : -1; manualClock.CurrentTime = proposedTime; manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; From 2b1e79a4e8f324e4d0dcfa66c94b6db8112112d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 15:32:20 +0900 Subject: [PATCH 4191/6909] Simplify state changes further --- .../Rulesets/UI/FrameStabilityContainer.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 7e17c93bed..6548bee4ef 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -120,7 +120,12 @@ namespace osu.Game.Rulesets.UI applyFrameStability(ref proposedTime); if (hasReplayAttached) - state = updateReplay(ref proposedTime); + { + bool valid = updateReplay(ref proposedTime); + + if (!valid) + state = PlaybackState.NotValid; + } if (proposedTime != manualClock.CurrentTime) direction = proposedTime > manualClock.CurrentTime ? 1 : -1; @@ -132,8 +137,8 @@ namespace osu.Game.Rulesets.UI double timeBehind = Math.Abs(manualClock.CurrentTime - parentGameplayClock.CurrentTime); // determine whether catch-up is required. - if (state != PlaybackState.NotValid) - state = timeBehind > 0 ? PlaybackState.RequiresCatchUp : PlaybackState.Valid; + if (state == PlaybackState.Valid && timeBehind > 0) + state = PlaybackState.RequiresCatchUp; frameStableClock.IsCatchingUp.Value = timeBehind > 200; @@ -146,7 +151,8 @@ namespace osu.Game.Rulesets.UI /// Attempt to advance replay playback for a given time. /// /// The time which is to be displayed. - private PlaybackState updateReplay(ref double proposedTime) + /// Whether playback is still valid. + private bool updateReplay(ref double proposedTime) { double? newTime; @@ -172,10 +178,10 @@ namespace osu.Game.Rulesets.UI } if (newTime == null) - return PlaybackState.NotValid; + return false; proposedTime = newTime.Value; - return PlaybackState.Valid; + return true; } /// From 6eddd76bdcd3917854d8a2eb3e13cf4fc04094bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 15:54:58 +0900 Subject: [PATCH 4192/6909] Simplify FramedReplayInputHandler's SetFrame implementation --- .../Replays/FramedReplayInputHandler.cs | 90 ++++++------------- 1 file changed, 29 insertions(+), 61 deletions(-) diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 120d81816f..8a4451fdca 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -42,40 +42,32 @@ namespace osu.Game.Rulesets.Replays if (!currentFrameIndex.HasValue) return (TFrame)Frames[0]; - if (currentDirection > 0) - return currentFrameIndex == Frames.Count - 1 ? null : (TFrame)Frames[currentFrameIndex.Value + 1]; - else - return currentFrameIndex == 0 ? null : (TFrame)Frames[nextFrameIndex]; + int nextFrame = clampedNextFrameIndex; + + if (nextFrame == currentFrameIndex.Value) + return null; + + return (TFrame)Frames[clampedNextFrameIndex]; } } private int? currentFrameIndex; - private int nextFrameIndex => currentFrameIndex.HasValue ? Math.Clamp(currentFrameIndex.Value + (currentDirection > 0 ? 1 : -1), 0, Frames.Count - 1) : 0; + private int clampedNextFrameIndex => + currentFrameIndex.HasValue ? Math.Clamp(currentFrameIndex.Value + currentDirection, 0, Frames.Count - 1) : 0; protected FramedReplayInputHandler(Replay replay) { this.replay = replay; } - private bool advanceFrame() - { - int newFrame = nextFrameIndex; - - // ensure we aren't at an extent. - if (newFrame == currentFrameIndex) return false; - - currentFrameIndex = newFrame; - return true; - } - private const double sixty_frame_time = 1000.0 / 60; protected virtual double AllowedImportantTimeSpan => sixty_frame_time * 1.2; protected double? CurrentTime { get; private set; } - private int currentDirection; + private int currentDirection = 1; /// /// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data. @@ -117,53 +109,29 @@ namespace osu.Game.Rulesets.Replays Debug.Assert(currentDirection != 0); - if (HasFrames) + TFrame next = NextFrame; + + // check if the next frame is valid for the current playback direction. + // validity is if the next frame is equal or "earlier" than the current point in time (so we can change to it) + int compare = time.CompareTo(next?.Time); + + if (next != null && (compare == 0 || compare == currentDirection)) { - var next = NextFrame; - - // check if the next frame is valid for the current playback direction. - // validity is if the next frame is equal or "earlier" - var compare = time.CompareTo(next?.Time); - - if (next != null && (compare == 0 || compare == currentDirection)) - { - if (advanceFrame()) - return CurrentTime = CurrentFrame.Time; - } - else - { - // this is the case where the frame can't be advanced (in the replay). - // even so, we may be able to move the clock forward due to being at the end of the replay or - // in a section where replay accuracy doesn't matter. - - // important section is always respected to block the update loop. - if (inImportantSection) - return null; - - if (next == null) - { - // in the case we have no more frames and haven't received the full replay, block. - if (!replay.HasReceivedAllFrames) - return null; - - // else allow play to end. - } - else if (next.Time.CompareTo(time) != currentDirection) - { - // in the case we have more frames, block if the next frame's time is less than the current time. - return null; - } - - // if we didn't change frames, we need to ensure we are allowed to run frames in between, else return null. - } - } - else - { - // if we never received frames and are expecting to, block. - if (!replay.HasReceivedAllFrames) - return null; + currentFrameIndex = clampedNextFrameIndex; + return CurrentTime = CurrentFrame.Time; } + // at this point, the frame can't be advanced (in the replay). + // even so, we may be able to move the clock forward due to being at the end of the replay or + // moving towards the next valid frame. + + // the exception is if currently in an important section, which is respected above all. + if (inImportantSection) + return null; + + // in the case we have no next frames and haven't received the full replay, block. + if (next == null && !replay.HasReceivedAllFrames) return null; + return CurrentTime = time; } From 48b0357e7d7daace77365a5507745d589edcda45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 16:11:14 +0900 Subject: [PATCH 4193/6909] Fix "finished playing" events handled for potentially incorrect user --- osu.Game/Screens/Play/Spectator.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 898d68a7d6..f788dcd8c7 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -167,6 +167,16 @@ namespace osu.Game.Screens.Play attemptStart(); } + private void userFinishedPlaying(int userId, SpectatorState state) + { + if (userId != targetUser.Id) + return; + + if (replay == null) return; + + replay.HasReceivedAllFrames = true; + } + private void attemptStart() { var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); @@ -216,13 +226,6 @@ namespace osu.Game.Screens.Play api.Queue(req); } - private void userFinishedPlaying(int userId, SpectatorState state) - { - if (replay == null) return; - - replay.HasReceivedAllFrames = true; - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 5fcd39a43d521e7829b1fd39769d4dd56eab090e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 16:29:06 +0900 Subject: [PATCH 4194/6909] Ensure spectator screen is loaded before continuing --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index a716b0c06b..5a2230dd64 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; @@ -170,8 +171,11 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - private void loadSpectatingScreen() => + private void loadSpectatingScreen() + { AddStep("load screen", () => LoadScreen(spectatorScreen = new Spectator(testSpectatorStreamingClient.StreamingUser))); + AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); + } internal class TestSpectatorStreamingClient : SpectatorStreamingClient { From 730cc645fb134df2a00994d64405dbadb967ca38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 16:33:52 +0900 Subject: [PATCH 4195/6909] Avoid reconstructing ruleset for each frame bundle --- osu.Game/Screens/Play/Spectator.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index f788dcd8c7..2de6c16c45 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -38,6 +38,8 @@ namespace osu.Game.Screens.Play [Resolved] private Bindable ruleset { get; set; } + private Ruleset rulesetInstance; + [Resolved] private Bindable> mods { get; set; } @@ -141,12 +143,10 @@ namespace osu.Game.Screens.Play if (replay == null) return; - var rulesetInstance = ruleset.Value.CreateInstance(); - foreach (var frame in data.Frames) { IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame(); - convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap, null); + convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap); var convertedFrame = (ReplayFrame)convertibleFrame; convertedFrame.Time = frame.Time; @@ -206,6 +206,8 @@ namespace osu.Game.Screens.Play }; ruleset.Value = resolvedRuleset.RulesetInfo; + rulesetInstance = resolvedRuleset; + beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); this.Push(new SpectatorPlayerLoader(new Score From 6169349f7c26f12365fbe91113aa4e89035b0a05 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 17:42:04 +0900 Subject: [PATCH 4196/6909] Fix switching to new beatmap not working correctly --- osu.Game/Screens/Play/Spectator.cs | 6 +++--- osu.Game/Screens/Play/SpectatorPlayer.cs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 2de6c16c45..51cd5b59aa 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -164,7 +164,7 @@ namespace osu.Game.Screens.Play this.state = state; - attemptStart(); + Schedule(attemptStart); } private void userFinishedPlaying(int userId, SpectatorState state) @@ -175,6 +175,7 @@ namespace osu.Game.Screens.Play if (replay == null) return; replay.HasReceivedAllFrames = true; + replay = null; } private void attemptStart() @@ -188,8 +189,6 @@ namespace osu.Game.Screens.Play if (state.BeatmapID == null) return; - this.MakeCurrent(); - var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == state.BeatmapID); if (resolvedBeatmap == null) @@ -201,6 +200,7 @@ namespace osu.Game.Screens.Play var scoreInfo = new ScoreInfo { Beatmap = resolvedBeatmap, + User = targetUser, Mods = state.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), Ruleset = resolvedRuleset.RulesetInfo, }; diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 89e5f8f2dc..fbd21b32ba 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -2,18 +2,36 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Online.Spectator; using osu.Game.Scoring; namespace osu.Game.Screens.Play { public class SpectatorPlayer : ReplayPlayer { + [Resolved] + private SpectatorStreamingClient spectatorStreaming { get; set; } + public SpectatorPlayer(Score score) : base(score) { } + [BackgroundDependencyLoader] + private void load() + { + spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; + } + + private void userBeganPlaying(int userId, SpectatorState state) + { + if (userId == Score.ScoreInfo.UserID) + Schedule(this.Exit); + } + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { // if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap. From ce9dd0c9203a160b20bd4b90fe2c87f078e55e58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 17:19:01 +0900 Subject: [PATCH 4197/6909] Fix enum descriptions not being displayed in OverlayHeaderTabControl --- osu.Game/Overlays/TabControlOverlayHeader.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/TabControlOverlayHeader.cs b/osu.Game/Overlays/TabControlOverlayHeader.cs index 61605d9e9e..7798dfa576 100644 --- a/osu.Game/Overlays/TabControlOverlayHeader.cs +++ b/osu.Game/Overlays/TabControlOverlayHeader.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -104,7 +106,7 @@ namespace osu.Game.Overlays public OverlayHeaderTabItem(T value) : base(value) { - Text.Text = value.ToString().ToLower(); + Text.Text = ((Value as Enum)?.GetDescription() ?? Value.ToString()).ToLower(); Text.Font = OsuFont.GetFont(size: 14); Text.Margin = new MarginPadding { Vertical = 16.5f }; // 15px padding + 1.5px line-height difference compensation Bar.Margin = new MarginPadding { Bottom = bar_height }; From 32becb6882bf7a800565499cc70a38019bb563cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 17:19:14 +0900 Subject: [PATCH 4198/6909] Add simple listing of currently playing users --- .../Dashboard/CurrentlyPlayingDisplay.cs | 95 +++++++++++++++++++ .../Dashboard/DashboardOverlayHeader.cs | 7 +- osu.Game/Overlays/DashboardOverlay.cs | 4 + 3 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs new file mode 100644 index 0000000000..8a98614fa0 --- /dev/null +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.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.Collections.Specialized; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Spectator; +using osu.Game.Screens.Play; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Overlays.Dashboard +{ + internal class CurrentlyPlayingDisplay : CompositeDrawable + { + private IBindableList playingUsers; + + private FillFlowContainer userFlow; + + [Resolved] + private SpectatorStreamingClient spectatorStreaming { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + + InternalChild = userFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + }; + } + + [Resolved] + private IAPIProvider api { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + playingUsers = spectatorStreaming.PlayingUsers.GetBoundCopy(); + playingUsers.BindCollectionChanged((sender, e) => Schedule(() => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (var u in e.NewItems.OfType()) + { + var request = new GetUserRequest(u); + request.Success += user => Schedule(() => + { + if (playingUsers.Contains((int)user.Id)) + userFlow.Add(createUserPanel(user)); + }); + api.Queue(request); + } + + break; + + case NotifyCollectionChangedAction.Remove: + foreach (var u in e.OldItems.OfType()) + userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire(); + break; + + case NotifyCollectionChangedAction.Reset: + userFlow.Clear(); + break; + } + }), true); + } + + [Resolved] + private OsuGame game { get; set; } + + private UserPanel createUserPanel(User user) + { + return new UserGridPanel(user).With(panel => + { + panel.Anchor = Anchor.TopCentre; + panel.Origin = Anchor.TopCentre; + panel.Width = 290; + panel.Action = () => game.PerformFromScreen(s => s.Push(new Spectator(user))); + }); + } + } +} diff --git a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs index 36bf589877..3314ed957a 100644 --- a/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.cs +++ b/osu.Game/Overlays/Dashboard/DashboardOverlayHeader.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.ComponentModel; + namespace osu.Game.Overlays.Dashboard { public class DashboardOverlayHeader : TabControlOverlayHeader @@ -20,6 +22,9 @@ namespace osu.Game.Overlays.Dashboard public enum DashboardOverlayTabs { - Friends + Friends, + + [Description("Currently Playing")] + CurrentlyPlaying } } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index a2490365e4..04defce636 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -130,6 +130,10 @@ namespace osu.Game.Overlays loadDisplay(new FriendDisplay()); break; + case DashboardOverlayTabs.CurrentlyPlaying: + loadDisplay(new CurrentlyPlayingDisplay()); + break; + default: throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented"); } From 84d854e23136434dcb5213abdf028f4c6ef63e6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 17:23:38 +0900 Subject: [PATCH 4199/6909] Avoid having the user profile show when clicking a spectator panel --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 1 + osu.Game/Users/UserPanel.cs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 8a98614fa0..d71e582c05 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -88,6 +88,7 @@ namespace osu.Game.Overlays.Dashboard panel.Anchor = Anchor.TopCentre; panel.Origin = Anchor.TopCentre; panel.Width = 290; + panel.ShowProfileOnClick = false; panel.Action = () => game.PerformFromScreen(s => s.Push(new Spectator(user))); }); } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index 57a87a713d..e97ff4287f 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -22,6 +22,8 @@ namespace osu.Game.Users public new Action Action; + public bool ShowProfileOnClick = true; + protected Action ViewProfile { get; private set; } protected Drawable Background { get; private set; } @@ -68,7 +70,8 @@ namespace osu.Game.Users base.Action = ViewProfile = () => { Action?.Invoke(); - profileOverlay?.ShowUser(User); + if (ShowProfileOnClick) + profileOverlay?.ShowUser(User); }; } From 16b0a7b33e2da40c759142fcfb29097c121e4439 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 18:01:55 +0900 Subject: [PATCH 4200/6909] Add button flow to allow resuming watching after exiting manually --- osu.Game/Screens/Play/Spectator.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 51cd5b59aa..11cdb66087 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -13,6 +13,7 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; @@ -63,6 +64,8 @@ namespace osu.Game.Screens.Play private IBindable> managerUpdated; + private TriangleButton watchButton; + public Spectator([NotNull] User targetUser) { this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); @@ -108,6 +111,14 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, Origin = Anchor.Centre, }, + watchButton = new TriangleButton + { + Text = "Watch", + Width = 250, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = attemptStart + } } }, }; @@ -191,9 +202,11 @@ namespace osu.Game.Screens.Play var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == state.BeatmapID); + showBeatmapPanel(state.BeatmapID.Value); + if (resolvedBeatmap == null) { - showBeatmapPanel(state.BeatmapID.Value); + watchButton.Enabled.Value = false; return; } @@ -209,6 +222,7 @@ namespace osu.Game.Screens.Play rulesetInstance = resolvedRuleset; beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); + watchButton.Enabled.Value = true; this.Push(new SpectatorPlayerLoader(new Score { From c97feb09bf2872a564d452f89442ed9cd2eaafc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 18:05:15 +0900 Subject: [PATCH 4201/6909] Allow continuing to automatically spectate user from results screen --- osu.Game/Screens/Play/SpectatorPlayer.cs | 20 +++++++-- .../Screens/Play/SpectatorResultsScreen.cs | 42 +++++++++++++++++++ 2 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Screens/Play/SpectatorResultsScreen.cs diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index fbd21b32ba..93e755f30a 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -7,19 +7,25 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Online.Spectator; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { public class SpectatorPlayer : ReplayPlayer { - [Resolved] - private SpectatorStreamingClient spectatorStreaming { get; set; } - public SpectatorPlayer(Score score) : base(score) { } + protected override ResultsScreen CreateResults(ScoreInfo score) + { + return new SpectatorResultsScreen(score); + } + + [Resolved] + private SpectatorStreamingClient spectatorStreaming { get; set; } + [BackgroundDependencyLoader] private void load() { @@ -32,6 +38,14 @@ namespace osu.Game.Screens.Play Schedule(this.Exit); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorStreaming != null) + spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; + } + protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { // if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap. diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs new file mode 100644 index 0000000000..bb1bdee3a9 --- /dev/null +++ b/osu.Game/Screens/Play/SpectatorResultsScreen.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.Screens; +using osu.Game.Online.Spectator; +using osu.Game.Scoring; +using osu.Game.Screens.Ranking; + +namespace osu.Game.Screens.Play +{ + public class SpectatorResultsScreen : SoloResultsScreen + { + public SpectatorResultsScreen(ScoreInfo score) + : base(score) + { + } + + [Resolved] + private SpectatorStreamingClient spectatorStreaming { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; + } + + private void userBeganPlaying(int userId, SpectatorState state) + { + if (userId == Score.UserID) + Schedule(this.Exit); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorStreaming != null) + spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; + } + } +} From 2d73dfbe39067052d37310bc7031ad0f3d8681e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 18:16:19 +0900 Subject: [PATCH 4202/6909] Add more safety around beatmap panel and button display logic --- osu.Game/Screens/Play/Spectator.cs | 32 ++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 11cdb66087..d97883069e 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -183,14 +183,22 @@ namespace osu.Game.Screens.Play if (userId != targetUser.Id) return; - if (replay == null) return; + if (replay != null) + { + replay.HasReceivedAllFrames = true; + replay = null; + } - replay.HasReceivedAllFrames = true; - replay = null; + // not really going to start anything, but will clear the beatmap card out. + attemptStart(); } private void attemptStart() { + watchButton.Enabled.Value = false; + + showBeatmapPanel(state); + var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); // ruleset not available @@ -202,13 +210,8 @@ namespace osu.Game.Screens.Play var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == state.BeatmapID); - showBeatmapPanel(state.BeatmapID.Value); - if (resolvedBeatmap == null) - { - watchButton.Enabled.Value = false; return; - } var scoreInfo = new ScoreInfo { @@ -231,11 +234,20 @@ namespace osu.Game.Screens.Play })); } - private void showBeatmapPanel(int beatmapId) + private void showBeatmapPanel(SpectatorState state) { - var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); + if (state.BeatmapID == null) + { + beatmapPanelContainer.Clear(); + return; + } + + var req = new GetBeatmapSetRequest(state.BeatmapID.Value, BeatmapSetLookupType.BeatmapId); req.Success += res => Schedule(() => { + if (state != this.state) + return; + beatmapPanelContainer.Child = new GridBeatmapPanel(res.ToBeatmapSet(rulesets)); }); From 344ff8f4bc9bf76c98abe6c690917ce39d2ef55e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 18:35:20 +0900 Subject: [PATCH 4203/6909] "Improve" visuals of spectator screen --- osu.Game/Screens/Play/Spectator.cs | 134 ++++++++++++++++++----------- 1 file changed, 86 insertions(+), 48 deletions(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index d97883069e..19409ade93 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -9,6 +9,8 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -24,6 +26,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Scoring; +using osu.Game.Screens.Multi.Match.Components; using osu.Game.Users; using osuTK; @@ -72,55 +75,83 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { - InternalChildren = new Drawable[] + InternalChild = new Container { - new FillFlowContainer + Masking = true, + CornerRadius = 20, + AutoSizeAxes = Axes.Both, + AutoSizeDuration = 500, + AutoSizeEasing = Easing.OutQuint, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Spacing = new Vector2(15), - Children = new Drawable[] + new Box { - new OsuSpriteText + Colour = colours.GreySeafoamDark, + RelativeSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Margin = new MarginPadding(20), + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] { - Text = "Currently spectating", - Font = OsuFont.Default.With(size: 30), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new UserGridPanel(targetUser) - { - Width = 290, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new OsuSpriteText - { - Text = "playing", - Font = OsuFont.Default.With(size: 30), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - beatmapPanelContainer = new Container - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - watchButton = new TriangleButton - { - Text = "Watch", - Width = 250, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Action = attemptStart + new OsuSpriteText + { + Text = "Spectator Mode", + Font = OsuFont.Default.With(size: 30), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Spacing = new Vector2(15), + Children = new Drawable[] + { + new UserGridPanel(targetUser) + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Height = 145, + Width = 290, + }, + new SpriteIcon + { + Size = new Vector2(40), + Icon = FontAwesome.Solid.ArrowRight, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + beatmapPanelContainer = new Container + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + } + }, + watchButton = new PurpleTriangleButton + { + Text = "Start Watching", + Width = 250, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = attemptStart + } } } - }, + } }; } @@ -141,7 +172,7 @@ namespace osu.Game.Screens.Play private void beatmapUpdated(ValueChangedEvent> beatmap) { if (beatmap.NewValue.TryGetTarget(out var beatmapSet) && beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID == state.BeatmapID)) - attemptStart(); + Schedule(attemptStart); } private void userSentFrames(int userId, FrameDataBundle data) @@ -189,14 +220,18 @@ namespace osu.Game.Screens.Play replay = null; } - // not really going to start anything, but will clear the beatmap card out. - attemptStart(); + clearDisplay(); + } + + private void clearDisplay() + { + watchButton.Enabled.Value = false; + beatmapPanelContainer.Clear(); } private void attemptStart() { - watchButton.Enabled.Value = false; - + clearDisplay(); showBeatmapPanel(state); var resolvedRuleset = rulesets.AvailableRulesets.FirstOrDefault(r => r.ID == state.RulesetID)?.CreateInstance(); @@ -213,6 +248,9 @@ namespace osu.Game.Screens.Play if (resolvedBeatmap == null) return; + if (replay == null) + return; + var scoreInfo = new ScoreInfo { Beatmap = resolvedBeatmap, @@ -236,7 +274,7 @@ namespace osu.Game.Screens.Play private void showBeatmapPanel(SpectatorState state) { - if (state.BeatmapID == null) + if (state?.BeatmapID == null) { beatmapPanelContainer.Clear(); return; From 93e3e1a4dbcc053f6c6a1f7d319a0a99aca0fc46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 18:40:06 +0900 Subject: [PATCH 4204/6909] Don't inherit ReplayPlayer to make results screen work correctly --- osu.Game/Screens/Play/SpectatorPlayer.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 93e755f30a..bd05665ae7 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -11,11 +11,16 @@ using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { - public class SpectatorPlayer : ReplayPlayer + public class SpectatorPlayer : Player { + private readonly Score score; + + protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap + public SpectatorPlayer(Score score) - : base(score) + : base(true, true) { + this.score = score; } protected override ResultsScreen CreateResults(ScoreInfo score) @@ -32,9 +37,14 @@ namespace osu.Game.Screens.Play spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; } + protected override void PrepareReplay() + { + DrawableRuleset?.SetReplayScore(score); + } + private void userBeganPlaying(int userId, SpectatorState state) { - if (userId == Score.ScoreInfo.UserID) + if (userId == score.ScoreInfo.UserID) Schedule(this.Exit); } @@ -49,7 +59,7 @@ namespace osu.Game.Screens.Play protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { // if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap. - double? firstFrameTime = Score.Replay.Frames.FirstOrDefault()?.Time; + double? firstFrameTime = score.Replay.Frames.FirstOrDefault()?.Time; if (firstFrameTime == null || firstFrameTime <= gameplayStart + 5000) return base.CreateGameplayClockContainer(beatmap, gameplayStart); From 4df811985214df8702d772ecb1590eab0947c7fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 18:45:09 +0900 Subject: [PATCH 4205/6909] Add missing schedule --- osu.Game/Screens/Play/Spectator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 19409ade93..9b26bd5ba3 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -220,7 +220,7 @@ namespace osu.Game.Screens.Play replay = null; } - clearDisplay(); + Schedule(clearDisplay); } private void clearDisplay() From 98070898348848f6e8e7d23bc7de5373fdc46a26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 18:47:15 +0900 Subject: [PATCH 4206/6909] Fix screen exit potentially occuring during transition --- osu.Game/Screens/Play/SpectatorPlayer.cs | 7 ++++++- osu.Game/Screens/Play/SpectatorResultsScreen.cs | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index bd05665ae7..7c2f8742ba 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -45,7 +45,12 @@ namespace osu.Game.Screens.Play private void userBeganPlaying(int userId, SpectatorState state) { if (userId == score.ScoreInfo.UserID) - Schedule(this.Exit); + { + Schedule(() => + { + if (this.IsCurrentScreen()) this.Exit(); + }); + } } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs index bb1bdee3a9..56ccfd2253 100644 --- a/osu.Game/Screens/Play/SpectatorResultsScreen.cs +++ b/osu.Game/Screens/Play/SpectatorResultsScreen.cs @@ -28,7 +28,12 @@ namespace osu.Game.Screens.Play private void userBeganPlaying(int userId, SpectatorState state) { if (userId == Score.UserID) - Schedule(this.Exit); + { + Schedule(() => + { + if (this.IsCurrentScreen()) this.Exit(); + }); + } } protected override void Dispose(bool isDisposing) From 93fd9138762dfcb0a41b07d8f6b04dfe2e2b2129 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 19:03:01 +0900 Subject: [PATCH 4207/6909] Add setting to allow automatically downloading during a spectating session --- osu.Game/Configuration/OsuConfigManager.cs | 3 ++ .../Settings/Sections/Online/WebSettings.cs | 6 +++ osu.Game/Screens/Play/Spectator.cs | 39 ++++++++++++++++++- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 7d601c0cb9..d22daf697c 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -42,6 +42,8 @@ namespace osu.Game.Configuration Set(OsuSetting.Username, string.Empty); Set(OsuSetting.Token, string.Empty); + Set(OsuSetting.AutomaticallyDownloadWhenSpectating, false); + Set(OsuSetting.SavePassword, false).ValueChanged += enabled => { if (enabled.NewValue) Set(OsuSetting.SaveUsername, true); @@ -239,5 +241,6 @@ namespace osu.Game.Configuration HitLighting, MenuBackgroundSource, GameplayDisableWinKey, + AutomaticallyDownloadWhenSpectating, } } diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index 6461bd7b93..8134c350a6 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -27,6 +27,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online Keywords = new[] { "no-video" }, Current = config.GetBindable(OsuSetting.PreferNoVideo) }, + new SettingsCheckbox + { + LabelText = "Automatically download beatmaps when spectating", + Keywords = new[] { "spectator" }, + Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), + }, }; } } diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 9b26bd5ba3..33e97bdff3 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -7,12 +7,14 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -20,6 +22,7 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; using osu.Game.Overlays.BeatmapListing.Panels; +using osu.Game.Overlays.Settings; using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -69,13 +72,17 @@ namespace osu.Game.Screens.Play private TriangleButton watchButton; + private SettingsCheckbox automaticDownload; + + private BeatmapSetInfo onlineBeatmap; + public Spectator([NotNull] User targetUser) { this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(OsuColour colours, OsuConfigManager config) { InternalChild = new Container { @@ -141,6 +148,13 @@ namespace osu.Game.Screens.Play }, } }, + automaticDownload = new SettingsCheckbox + { + LabelText = "Automatically download beatmaps", + Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, watchButton = new PurpleTriangleButton { Text = "Start Watching", @@ -167,6 +181,8 @@ namespace osu.Game.Screens.Play managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); + + automaticDownload.Current.BindValueChanged(_ => checkForAutomaticDownload()); } private void beatmapUpdated(ValueChangedEvent> beatmap) @@ -246,7 +262,9 @@ namespace osu.Game.Screens.Play var resolvedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == state.BeatmapID); if (resolvedBeatmap == null) + { return; + } if (replay == null) return; @@ -277,6 +295,7 @@ namespace osu.Game.Screens.Play if (state?.BeatmapID == null) { beatmapPanelContainer.Clear(); + onlineBeatmap = null; return; } @@ -286,12 +305,28 @@ namespace osu.Game.Screens.Play if (state != this.state) return; - beatmapPanelContainer.Child = new GridBeatmapPanel(res.ToBeatmapSet(rulesets)); + onlineBeatmap = res.ToBeatmapSet(rulesets); + beatmapPanelContainer.Child = new GridBeatmapPanel(onlineBeatmap); + checkForAutomaticDownload(); }); api.Queue(req); } + private void checkForAutomaticDownload() + { + if (onlineBeatmap == null) + return; + + if (!automaticDownload.Current.Value) + return; + + if (beatmaps.IsAvailableLocally(onlineBeatmap)) + return; + + beatmaps.Download(onlineBeatmap); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From dd2f44f393e6cf16ef4f1a40f6b74ff4174219d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 19:39:38 +0900 Subject: [PATCH 4208/6909] Add basic "currently watching" text to player to signify that spectator is active --- osu.Game/Screens/Play/SpectatorPlayer.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index 7c2f8742ba..2f98647d3e 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -3,8 +3,11 @@ using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Online.Spectator; using osu.Game.Scoring; using osu.Game.Screens.Ranking; @@ -35,6 +38,15 @@ namespace osu.Game.Screens.Play private void load() { spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; + + AddInternal(new OsuSpriteText + { + Text = $"Watching {score.ScoreInfo.User.Username} playing live!", + Font = OsuFont.Default.With(size: 30), + Y = 100, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + }); } protected override void PrepareReplay() From 25ab3a5feafcd037bc475aeea1c3d8119185e70b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 22:10:37 +0900 Subject: [PATCH 4209/6909] Construct replay after being sure a ruleset is available to avoid nullrefs --- osu.Game/Screens/Play/Spectator.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 51cd5b59aa..6a11aeb0e9 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -160,8 +160,6 @@ namespace osu.Game.Screens.Play if (userId != targetUser.Id) return; - replay ??= new Replay { HasReceivedAllFrames = false }; - this.state = state; Schedule(attemptStart); @@ -197,6 +195,8 @@ namespace osu.Game.Screens.Play return; } + replay ??= new Replay { HasReceivedAllFrames = false }; + var scoreInfo = new ScoreInfo { Beatmap = resolvedBeatmap, From 5d02de29ca1476a4989ecf74ee6583e32dd12005 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 22:50:45 +0900 Subject: [PATCH 4210/6909] Fix attempt to change ruleset/beatmap bindables while screen is not active --- osu.Game/Screens/Play/Spectator.cs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 6a11aeb0e9..2f65dc06d0 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -63,6 +63,11 @@ namespace osu.Game.Screens.Play private IBindable> managerUpdated; + /// + /// Becomes true if a new state is waiting to be loaded (while this screen was not active). + /// + private bool newStatePending; + public Spectator([NotNull] User targetUser) { this.targetUser = targetUser ?? throw new ArgumentNullException(nameof(targetUser)); @@ -162,7 +167,21 @@ namespace osu.Game.Screens.Play this.state = state; - Schedule(attemptStart); + if (this.IsCurrentScreen()) + Schedule(attemptStart); + else + newStatePending = true; + } + + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + + if (newStatePending) + { + attemptStart(); + newStatePending = false; + } } private void userFinishedPlaying(int userId, SpectatorState state) From 8bbcb9be8a7a8bd11b8192f7ee0c0722fe96b62d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 22:50:57 +0900 Subject: [PATCH 4211/6909] Always use imported beatmap in tests --- .../Visual/Gameplay/TestSceneSpectator.cs | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 5a2230dd64..b4ab22cfad 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -36,13 +36,21 @@ namespace osu.Game.Tests.Visual.Gameplay private int nextFrame; + private BeatmapSetInfo importedBeatmap; + + private int importedBeatmapId; + public override void SetUpSteps() { base.SetUpSteps(); AddStep("reset sent frames", () => nextFrame = 0); - AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Wait()); + AddStep("import beatmap", () => + { + importedBeatmap = ImportBeatmapTest.LoadOszIntoOsu(game, virtualTrack: true).Result; + importedBeatmapId = importedBeatmap.Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID ?? -1; + }); AddStep("add streaming client", () => { @@ -115,6 +123,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); + start(); sendFrames(); } @@ -157,7 +166,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); - private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(beatmapId)); + private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(beatmapId ?? importedBeatmapId)); private void checkPaused(bool state) => AddAssert($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); @@ -179,18 +188,21 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestSpectatorStreamingClient : SpectatorStreamingClient { - [Resolved] - private BeatmapManager beatmaps { get; set; } - public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; - public void StartPlay(int? beatmapId = null) => sendState(beatmapId); + private int beatmapId; - public void EndPlay() + public void StartPlay(int beatmapId) + { + this.beatmapId = beatmapId; + sendState(beatmapId); + } + + public void EndPlay(int beatmapId) { ((ISpectatorClient)this).UserFinishedPlaying((int)StreamingUser.Id, new SpectatorState { - BeatmapID = beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID, + BeatmapID = beatmapId, RulesetID = 0, }); } @@ -212,7 +224,7 @@ namespace osu.Game.Tests.Visual.Gameplay ((ISpectatorClient)this).UserSentFrames((int)StreamingUser.Id, bundle); if (!sentState) - sendState(); + sendState(beatmapId); } public override void WatchUser(int userId) @@ -220,18 +232,18 @@ namespace osu.Game.Tests.Visual.Gameplay if (sentState) { // usually the server would do this. - sendState(); + sendState(beatmapId); } base.WatchUser(userId); } - private void sendState(int? beatmapId = null) + private void sendState(int beatmapId) { sentState = true; ((ISpectatorClient)this).UserBeganPlaying((int)StreamingUser.Id, new SpectatorState { - BeatmapID = beatmapId ?? beatmaps.GetAllUsableBeatmapSets().First().Beatmaps.First(b => b.RulesetID == 0).OnlineBeatmapID, + BeatmapID = beatmapId, RulesetID = 0, }); } From 1d499ec15d5ce31cd05337f9ead39a33562f267e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 22:51:35 +0900 Subject: [PATCH 4212/6909] Change beatmap not existing test to specify a beatmap ID that can't possibly exist --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index b4ab22cfad..0b530a303f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -158,7 +158,7 @@ namespace osu.Game.Tests.Visual.Gameplay { loadSpectatingScreen(); - start(88); + start(-1234); sendFrames(); AddAssert("screen didn't change", () => Stack.CurrentScreen is Spectator); From 7cc4a7cb5cdd419af637f6bcd1e631bc8321f6ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 22:59:54 +0900 Subject: [PATCH 4213/6909] Add more accurate fail scenario test logic --- .../Visual/Gameplay/TestSceneSpectator.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 0b530a303f..7dde493b1a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -94,7 +94,7 @@ namespace osu.Game.Tests.Visual.Gameplay start(); waitForPlayer(); - AddUntilStep("game is paused", () => player.ChildrenOfType().First().IsPaused.Value); + checkPaused(true); sendFrames(); @@ -134,8 +134,13 @@ namespace osu.Game.Tests.Visual.Gameplay loadSpectatingScreen(); start(); - sendFrames(); + waitForPlayer(); + checkPaused(true); + + finish(); + + checkPaused(false); // TODO: should replay until running out of frames then fail } @@ -168,8 +173,10 @@ namespace osu.Game.Tests.Visual.Gameplay private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(beatmapId ?? importedBeatmapId)); + private void finish(int? beatmapId = null) => AddStep("end play", () => testSpectatorStreamingClient.EndPlay(beatmapId ?? importedBeatmapId)); + private void checkPaused(bool state) => - AddAssert($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); + AddUntilStep($"game is {(state ? "paused" : "playing")}", () => player.ChildrenOfType().First().IsPaused.Value == state); private void sendFrames(int count = 10) { From 6c2cee7b3fb187348fc099617bbdde37966404a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 23:13:42 +0900 Subject: [PATCH 4214/6909] Avoid cross-pollution between tests of current playing state --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 7dde493b1a..6485cbdad3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -57,6 +57,8 @@ namespace osu.Game.Tests.Visual.Gameplay Remove(testSpectatorStreamingClient); Add(testSpectatorStreamingClient); }); + + finish(); } private OsuFramedReplayInputHandler replayHandler => @@ -212,6 +214,8 @@ namespace osu.Game.Tests.Visual.Gameplay BeatmapID = beatmapId, RulesetID = 0, }); + + sentState = false; } private bool sentState; From bca317b1518a2f67dc7592736b36552f1bda600d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 28 Oct 2020 23:43:16 +0900 Subject: [PATCH 4215/6909] Remove excess using statement --- osu.Game/Screens/Play/Spectator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 34e7bb1bda..2c8b5c4cad 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -7,7 +7,6 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; From 4f6081c7f3f48fcedc899665a32e89b4a757e45c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Oct 2020 19:44:13 +0300 Subject: [PATCH 4216/6909] Use BindableList --- .../TestSceneBeatmapListingSearchControl.cs | 4 +-- .../BeatmapListingFilterControl.cs | 8 ++--- .../BeatmapListingSearchControl.cs | 5 ++- .../BeatmapListing/BeatmapSearchFilterRow.cs | 2 +- ...BeatmapSearchMultipleSelectionFilterRow.cs | 35 ++++++++----------- 5 files changed, 23 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index e07aa71b1f..3f757031f8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -61,8 +61,8 @@ namespace osu.Game.Tests.Visual.UserInterface 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); - control.Extra.BindValueChanged(e => extra.Text = $"Extra: {(e.NewValue == null ? "" : string.Join('.', e.NewValue.Select(i => i.ToString().ToLowerInvariant())))}", true); - control.Ranks.BindValueChanged(r => ranks.Text = $"Ranks: {(r.NewValue == null ? "" : string.Join('.', r.NewValue.Select(i => i.ToString())))}", true); + control.Extra.BindCollectionChanged((u, v) => extra.Text = $"Extra: {(control.Extra.Any() ? string.Join('.', control.Extra.Select(i => i.ToString().ToLowerInvariant())) : "")}", true); + control.Ranks.BindCollectionChanged((u, v) => ranks.Text = $"Ranks: {(control.Ranks.Any() ? string.Join('.', control.Ranks.Select(i => i.ToString())) : "")}", true); control.Played.BindValueChanged(p => played.Text = $"Played: {p.NewValue}", true); } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 86bf3276fe..71f0d8c522 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -130,8 +130,8 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Category.BindValueChanged(_ => queueUpdateSearch()); searchControl.Genre.BindValueChanged(_ => queueUpdateSearch()); searchControl.Language.BindValueChanged(_ => queueUpdateSearch()); - searchControl.Extra.BindValueChanged(_ => queueUpdateSearch()); - searchControl.Ranks.BindValueChanged(_ => queueUpdateSearch()); + searchControl.Extra.CollectionChanged += (u, v) => queueUpdateSearch(); + searchControl.Ranks.CollectionChanged += (u, v) => queueUpdateSearch(); searchControl.Played.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); @@ -183,8 +183,8 @@ namespace osu.Game.Overlays.BeatmapListing sortControl.SortDirection.Value, searchControl.Genre.Value, searchControl.Language.Value, - searchControl.Extra.Value, - searchControl.Ranks.Value, + searchControl.Extra, + searchControl.Ranks, searchControl.Played.Value); getSetsRequest.Success += response => diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index a976890c7c..4fc5c5315b 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -13,7 +13,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osuTK.Graphics; using osu.Game.Rulesets; -using System.Collections.Generic; namespace osu.Game.Overlays.BeatmapListing { @@ -29,9 +28,9 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable Language => languageFilter.Current; - public Bindable> Extra => extraFilter.Current; + public BindableList Extra => extraFilter.Current; - public Bindable> Ranks => ranksFilter.Current; + public BindableList Ranks => ranksFilter.Current; public Bindable Played => playedFilter.Current; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index aa0fc0d00e..b429a5277b 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.BeatmapListing }); if (filter is IHasCurrentValue filterWithValue) - filterWithValue.Current = current; + Current = filterWithValue.Current; } [NotNull] diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index cb89560e39..993b475f32 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -7,7 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osuTK; @@ -15,22 +14,21 @@ namespace osu.Game.Overlays.BeatmapListing { public class BeatmapSearchMultipleSelectionFilterRow : BeatmapSearchFilterRow> { + public new readonly BindableList Current = new BindableList(); + + private MultipleSelectionFilter filter; + public BeatmapSearchMultipleSelectionFilterRow(string headerName) : base(headerName) { + Current.BindTo(filter.Current); } - protected override Drawable CreateFilter() => new MultipleSelectionFilter(); + protected override Drawable CreateFilter() => filter = new MultipleSelectionFilter(); - private class MultipleSelectionFilter : FillFlowContainer, IHasCurrentValue> + private class MultipleSelectionFilter : FillFlowContainer { - private readonly BindableWithCurrent> current = new BindableWithCurrent>(); - - public Bindable> Current - { - get => current.Current; - set => current.Current = value; - } + public readonly BindableList Current = new BindableList(); public MultipleSelectionFilter() { @@ -43,20 +41,15 @@ namespace osu.Game.Overlays.BeatmapListing ((T[])Enum.GetValues(typeof(T))).ForEach(i => Add(new MultipleSelectionFilterTabItem(i))); foreach (var item in Children) - item.Active.BindValueChanged(_ => updateBindable()); + item.Active.BindValueChanged(active => updateBindable(item.Value, active.NewValue)); } - private void updateBindable() + private void updateBindable(T value, bool active) { - var selectedValues = new List(); - - foreach (var item in Children) - { - if (item.Active.Value) - selectedValues.Add(item.Value); - } - - Current.Value = selectedValues; + if (active) + Current.Add(value); + else + Current.Remove(value); } } From 5c2c5f200075a4f7f19a089a9db3775a11b73668 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 28 Oct 2020 23:35:08 +0300 Subject: [PATCH 4217/6909] Use existing ScoreRank for rank filter --- .../API/Requests/SearchBeatmapSetsRequest.cs | 5 +- .../BeatmapListingSearchControl.cs | 7 +-- ...BeatmapSearchMultipleSelectionFilterRow.cs | 14 ++++-- .../BeatmapSearchScoreFilterRow.cs | 48 +++++++++++++++++++ .../Overlays/BeatmapListing/FilterTabItem.cs | 4 +- .../Overlays/BeatmapListing/SearchRank.cs | 24 ---------- 6 files changed, 68 insertions(+), 34 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs delete mode 100644 osu.Game/Overlays/BeatmapListing/SearchRank.cs diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 708b58d954..ed67c5f5ca 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -8,6 +8,7 @@ using osu.Game.Extensions; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; using osu.Game.Rulesets; +using osu.Game.Scoring; namespace osu.Game.Online.API.Requests { @@ -27,7 +28,7 @@ namespace osu.Game.Online.API.Requests public SearchPlayed Played { get; } - public IReadOnlyCollection Ranks { get; } + public IReadOnlyCollection Ranks { get; } private readonly string query; private readonly RulesetInfo ruleset; @@ -45,7 +46,7 @@ namespace osu.Game.Online.API.Requests SearchGenre genre = SearchGenre.Any, SearchLanguage language = SearchLanguage.Any, IReadOnlyCollection extra = null, - IReadOnlyCollection ranks = null, + IReadOnlyCollection ranks = null, SearchPlayed played = SearchPlayed.Any) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 4fc5c5315b..3694c9855e 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -13,6 +13,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osuTK.Graphics; using osu.Game.Rulesets; +using osu.Game.Scoring; namespace osu.Game.Overlays.BeatmapListing { @@ -30,7 +31,7 @@ namespace osu.Game.Overlays.BeatmapListing public BindableList Extra => extraFilter.Current; - public BindableList Ranks => ranksFilter.Current; + public BindableList Ranks => ranksFilter.Current; public Bindable Played => playedFilter.Current; @@ -55,7 +56,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchFilterRow genreFilter; private readonly BeatmapSearchFilterRow languageFilter; private readonly BeatmapSearchMultipleSelectionFilterRow extraFilter; - private readonly BeatmapSearchMultipleSelectionFilterRow ranksFilter; + private readonly BeatmapSearchScoreFilterRow ranksFilter; private readonly BeatmapSearchFilterRow playedFilter; private readonly Box background; @@ -115,7 +116,7 @@ namespace osu.Game.Overlays.BeatmapListing genreFilter = new BeatmapSearchFilterRow(@"Genre"), languageFilter = new BeatmapSearchFilterRow(@"Language"), extraFilter = new BeatmapSearchMultipleSelectionFilterRow(@"Extra"), - ranksFilter = new BeatmapSearchMultipleSelectionFilterRow(@"Rank Achieved"), + ranksFilter = new BeatmapSearchScoreFilterRow(), playedFilter = new BeatmapSearchFilterRow(@"Played") } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 993b475f32..87e60c5bdd 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -24,9 +24,11 @@ namespace osu.Game.Overlays.BeatmapListing Current.BindTo(filter.Current); } - protected override Drawable CreateFilter() => filter = new MultipleSelectionFilter(); + protected override Drawable CreateFilter() => filter = CreateMultipleSelectionFilter(); - private class MultipleSelectionFilter : FillFlowContainer + protected virtual MultipleSelectionFilter CreateMultipleSelectionFilter() => new MultipleSelectionFilter(); + + protected class MultipleSelectionFilter : FillFlowContainer { public readonly BindableList Current = new BindableList(); @@ -38,12 +40,16 @@ namespace osu.Game.Overlays.BeatmapListing Height = 15; Spacing = new Vector2(10, 0); - ((T[])Enum.GetValues(typeof(T))).ForEach(i => Add(new MultipleSelectionFilterTabItem(i))); + GetValues().ForEach(i => Add(CreateTabItem(i))); foreach (var item in Children) item.Active.BindValueChanged(active => updateBindable(item.Value, active.NewValue)); } + protected virtual T[] GetValues() => (T[])Enum.GetValues(typeof(T)); + + protected virtual MultipleSelectionFilterTabItem CreateTabItem(T value) => new MultipleSelectionFilterTabItem(value); + private void updateBindable(T value, bool active) { if (active) @@ -53,7 +59,7 @@ namespace osu.Game.Overlays.BeatmapListing } } - private class MultipleSelectionFilterTabItem : FilterTabItem + protected class MultipleSelectionFilterTabItem : FilterTabItem { public MultipleSelectionFilterTabItem(T value) : base(value) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs new file mode 100644 index 0000000000..f741850f07 --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.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.Linq; +using osu.Game.Scoring; + +namespace osu.Game.Overlays.BeatmapListing +{ + public class BeatmapSearchScoreFilterRow : BeatmapSearchMultipleSelectionFilterRow + { + public BeatmapSearchScoreFilterRow() + : base(@"Rank Achieved") + { + } + + protected override MultipleSelectionFilter CreateMultipleSelectionFilter() => new RankFilter(); + + private class RankFilter : MultipleSelectionFilter + { + protected override MultipleSelectionFilterTabItem CreateTabItem(ScoreRank value) => new RankItem(value); + + protected override ScoreRank[] GetValues() => base.GetValues().Reverse().ToArray(); + } + + private class RankItem : MultipleSelectionFilterTabItem + { + public RankItem(ScoreRank value) + : base(value) + { + } + + protected override string CreateText(ScoreRank value) + { + switch (value) + { + case ScoreRank.XH: + return @"Silver SS"; + + case ScoreRank.SH: + return @"Silver S"; + + default: + return base.CreateText(value); + } + } + } + } +} diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 244ef5a703..c45a82bef1 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.BeatmapListing text = new OsuSpriteText { Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), - Text = (value as Enum)?.GetDescription() ?? value.ToString() + Text = CreateText(value) }, new HoverClickSounds() }); @@ -63,6 +63,8 @@ namespace osu.Game.Overlays.BeatmapListing protected override void OnDeactivated() => updateState(); + protected virtual string CreateText(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); + private void updateState() { text.FadeColour(IsHovered ? colourProvider.Light1 : getStateColour(), 200, Easing.OutQuint); diff --git a/osu.Game/Overlays/BeatmapListing/SearchRank.cs b/osu.Game/Overlays/BeatmapListing/SearchRank.cs deleted file mode 100644 index 8b1882026c..0000000000 --- a/osu.Game/Overlays/BeatmapListing/SearchRank.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.ComponentModel; - -namespace osu.Game.Overlays.BeatmapListing -{ - public enum SearchRank - { - [Description(@"Silver SS")] - XH, - - [Description(@"SS")] - X, - - [Description(@"Silver S")] - SH, - S, - A, - B, - C, - D - } -} From 202fe093065ef46560555d78402fbf3c49f8a762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Oct 2020 22:03:59 +0100 Subject: [PATCH 4218/6909] Group selection actions back up in SelectionHandler --- .../Sliders/SliderSelectionBlueprint.cs | 4 ++-- osu.Game/Rulesets/Edit/SelectionBlueprint.cs | 17 ----------------- .../Compose/Components/BlueprintContainer.cs | 7 ------- .../Edit/Compose/Components/SelectionHandler.cs | 5 ++++- 4 files changed, 6 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index ca9ec886d5..d3fb5defae 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { case MouseButton.Right: rightClickPosition = e.MouseDownPosition; - break; + return false; // Allow right click to be handled by context menu case MouseButton.Left when e.ControlPressed && IsSelected: placementControlPointIndex = addControlPoint(e.MousePosition); return true; // Stop input from being handled and modifying the selection } - return base.OnMouseDown(e); + return false; } private int? placementControlPointIndex; diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index 87ef7e647f..f3816f6218 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -8,13 +8,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.UserInterface; -using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Screens.Edit; using osuTK; -using osuTK.Input; namespace osu.Game.Rulesets.Edit { @@ -55,20 +52,6 @@ namespace osu.Game.Rulesets.Edit updateState(); } - [Resolved] - private EditorBeatmap editorBeatmap { get; set; } - - protected override bool OnMouseDown(MouseDownEvent e) - { - if (e.CurrentState.Keyboard.ShiftPressed && e.IsPressed(MouseButton.Right)) - { - editorBeatmap.Remove(HitObject); - return true; - } - - return base.OnMouseDown(e); - } - private SelectionState state; public event Action StateChanged; diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 7751df29cf..5ac360d029 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -298,13 +298,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { Debug.Assert(!clickSelectionBegan); - // Deselections are only allowed for control + left clicks - bool allowDeselection = e.ControlPressed && e.Button == MouseButton.Left; - - // Todo: This is probably incorrectly disallowing multiple selections on stacked objects - if (!allowDeselection && SelectionHandler.SelectedBlueprints.Any(s => s.IsHovered)) - return; - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) { if (blueprint.IsHovered) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 24f88bf36d..01e23bafc5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -24,6 +24,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { @@ -224,7 +225,9 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The input state at the point of selection. internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) { - if (state.Keyboard.ControlPressed) + if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) + EditorBeatmap.Remove(blueprint.HitObject); + else if (state.Keyboard.ControlPressed && state.Mouse.IsPressed(MouseButton.Left)) blueprint.ToggleSelection(); else ensureSelected(blueprint); From fa53549ed271a7ff3486ff77835c93baa03ec48d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Oct 2020 22:57:03 +0100 Subject: [PATCH 4219/6909] Mark request fields as possibly-null --- osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index ed67c5f5ca..bbaa7e745f 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.IO.Network; using osu.Game.Extensions; using osu.Game.Overlays; @@ -24,10 +25,12 @@ namespace osu.Game.Online.API.Requests public SearchLanguage Language { get; } + [CanBeNull] public IReadOnlyCollection Extra { get; } public SearchPlayed Played { get; } + [CanBeNull] public IReadOnlyCollection Ranks { get; } private readonly string query; From e77049eae39afb8cf363197079ccd9a4257c07b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Oct 2020 22:58:51 +0100 Subject: [PATCH 4220/6909] Use discard-like lambda parameter names --- .../Overlays/BeatmapListing/BeatmapListingFilterControl.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 71f0d8c522..3be38e3c1d 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -130,8 +130,8 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Category.BindValueChanged(_ => queueUpdateSearch()); searchControl.Genre.BindValueChanged(_ => queueUpdateSearch()); searchControl.Language.BindValueChanged(_ => queueUpdateSearch()); - searchControl.Extra.CollectionChanged += (u, v) => queueUpdateSearch(); - searchControl.Ranks.CollectionChanged += (u, v) => queueUpdateSearch(); + searchControl.Extra.CollectionChanged += (_, __) => queueUpdateSearch(); + searchControl.Ranks.CollectionChanged += (_, __) => queueUpdateSearch(); searchControl.Played.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); From f5aedc96c4244974750c651a94976f17ed283ff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Oct 2020 23:07:54 +0100 Subject: [PATCH 4221/6909] Rework multiple selection filter --- ...BeatmapSearchMultipleSelectionFilterRow.cs | 23 ++++++++++++------- .../BeatmapSearchScoreFilterRow.cs | 3 ++- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index 87e60c5bdd..ae669e91dd 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -3,8 +3,9 @@ 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.Framework.Graphics.Containers; using osu.Framework.Input.Events; @@ -32,7 +33,8 @@ namespace osu.Game.Overlays.BeatmapListing { public readonly BindableList Current = new BindableList(); - public MultipleSelectionFilter() + [BackgroundDependencyLoader] + private void load() { Anchor = Anchor.BottomLeft; Origin = Anchor.BottomLeft; @@ -40,17 +42,22 @@ namespace osu.Game.Overlays.BeatmapListing Height = 15; Spacing = new Vector2(10, 0); - GetValues().ForEach(i => Add(CreateTabItem(i))); - - foreach (var item in Children) - item.Active.BindValueChanged(active => updateBindable(item.Value, active.NewValue)); + AddRange(GetValues().Select(CreateTabItem)); } - protected virtual T[] GetValues() => (T[])Enum.GetValues(typeof(T)); + protected override void LoadComplete() + { + base.LoadComplete(); + + foreach (var item in Children) + item.Active.BindValueChanged(active => toggleItem(item.Value, active.NewValue)); + } + + protected virtual IEnumerable GetValues() => Enum.GetValues(typeof(T)).Cast(); protected virtual MultipleSelectionFilterTabItem CreateTabItem(T value) => new MultipleSelectionFilterTabItem(value); - private void updateBindable(T value, bool active) + private void toggleItem(T value, bool active) { if (active) Current.Add(value); diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs index f741850f07..dc642cb4cd 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using osu.Game.Scoring; @@ -19,7 +20,7 @@ namespace osu.Game.Overlays.BeatmapListing { protected override MultipleSelectionFilterTabItem CreateTabItem(ScoreRank value) => new RankItem(value); - protected override ScoreRank[] GetValues() => base.GetValues().Reverse().ToArray(); + protected override IEnumerable GetValues() => base.GetValues().Reverse(); } private class RankItem : MultipleSelectionFilterTabItem From a8cefb0d4ce014b5944559188d2faf063fdfcf83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Oct 2020 23:12:28 +0100 Subject: [PATCH 4222/6909] Rename method --- .../Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs | 5 +++-- osu.Game/Overlays/BeatmapListing/FilterTabItem.cs | 7 +++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs index dc642cb4cd..804962adfb 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchScoreFilterRow.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions; using osu.Game.Scoring; namespace osu.Game.Overlays.BeatmapListing @@ -30,7 +31,7 @@ namespace osu.Game.Overlays.BeatmapListing { } - protected override string CreateText(ScoreRank value) + protected override string LabelFor(ScoreRank value) { switch (value) { @@ -41,7 +42,7 @@ namespace osu.Game.Overlays.BeatmapListing return @"Silver S"; default: - return base.CreateText(value); + return value.GetDescription(); } } } diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index c45a82bef1..43913c7ce2 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays.BeatmapListing text = new OsuSpriteText { Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), - Text = CreateText(value) + Text = LabelFor(value) }, new HoverClickSounds() }); @@ -63,7 +63,10 @@ namespace osu.Game.Overlays.BeatmapListing protected override void OnDeactivated() => updateState(); - protected virtual string CreateText(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); + /// + /// Returns the label text to be used for the supplied . + /// + protected virtual string LabelFor(T value) => (value as Enum)?.GetDescription() ?? value.ToString(); private void updateState() { From 016e920aa9fff8a7b4c2aaf4236a70cfff5b3038 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Oct 2020 23:14:52 +0100 Subject: [PATCH 4223/6909] Move filter tab item hierarchy construction to BDL --- osu.Game/Overlays/BeatmapListing/FilterTabItem.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs index 43913c7ce2..f02b515755 100644 --- a/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs +++ b/osu.Game/Overlays/BeatmapListing/FilterTabItem.cs @@ -19,10 +19,15 @@ namespace osu.Game.Overlays.BeatmapListing [Resolved] private OverlayColourProvider colourProvider { get; set; } - private readonly OsuSpriteText text; + private OsuSpriteText text; public FilterTabItem(T value) : base(value) + { + } + + [BackgroundDependencyLoader] + private void load() { AutoSizeAxes = Axes.Both; Anchor = Anchor.BottomLeft; @@ -32,17 +37,12 @@ namespace osu.Game.Overlays.BeatmapListing text = new OsuSpriteText { Font = OsuFont.GetFont(size: 13, weight: FontWeight.Regular), - Text = LabelFor(value) + Text = LabelFor(Value) }, new HoverClickSounds() }); Enabled.Value = true; - } - - [BackgroundDependencyLoader] - private void load() - { updateState(); } From 1313ab89e76d0a0f5caaed1274c6a12971a34db1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 28 Oct 2020 23:37:21 +0100 Subject: [PATCH 4224/6909] Add xmldoc to multiple selection row --- .../BeatmapSearchMultipleSelectionFilterRow.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs index ae669e91dd..5dfa8e6109 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchMultipleSelectionFilterRow.cs @@ -25,8 +25,11 @@ namespace osu.Game.Overlays.BeatmapListing Current.BindTo(filter.Current); } - protected override Drawable CreateFilter() => filter = CreateMultipleSelectionFilter(); + protected sealed override Drawable CreateFilter() => filter = CreateMultipleSelectionFilter(); + /// + /// Creates a filter control that can be used to simultaneously select multiple values of type . + /// protected virtual MultipleSelectionFilter CreateMultipleSelectionFilter() => new MultipleSelectionFilter(); protected class MultipleSelectionFilter : FillFlowContainer @@ -53,8 +56,14 @@ namespace osu.Game.Overlays.BeatmapListing item.Active.BindValueChanged(active => toggleItem(item.Value, active.NewValue)); } + /// + /// Returns all values to be displayed in this filter row. + /// protected virtual IEnumerable GetValues() => Enum.GetValues(typeof(T)).Cast(); + /// + /// Creates a representing the supplied . + /// protected virtual MultipleSelectionFilterTabItem CreateTabItem(T value) => new MultipleSelectionFilterTabItem(value); private void toggleItem(T value, bool active) From 2e5a8b2287ce1b74f316671d9533faf87cb967f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 13:16:27 +0900 Subject: [PATCH 4225/6909] Fix xmldoc to read better in new context --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 6548bee4ef..595574115c 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -236,14 +236,13 @@ namespace osu.Game.Rulesets.UI NotValid, /// - /// Whether we are running up-to-date with our parent clock. - /// If not, we will need to keep processing children until we catch up. + /// Playback is running behind real-time. Catch-up will be attempted by processing more than once per + /// game loop (limited to a sane maximum to avoid frame drops). /// RequiresCatchUp, /// - /// Whether we are in a valid state (ie. should we keep processing children frames). - /// This should be set to false when the replay is, for instance, waiting for future frames to arrive. + /// In a valid state, progressing one child hierarchy loop per game loop. /// Valid } From d91456dc2993705d44a30ce74c87bfe1595c973c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 14:25:47 +0900 Subject: [PATCH 4226/6909] Move initial validity check out of loop for clarity --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index f8156164c1..8edeaf851d 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -96,7 +96,10 @@ namespace osu.Game.Rulesets.UI int loops = MaxCatchUpFrames; - while (state != PlaybackState.NotValid && loops-- > 0) + if (state == PlaybackState.NotValid) + return true; + + while (loops-- > 0) { updateClock(); From db2b00068f024589f26b4ab247b824f030a88b2b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 14:42:16 +0900 Subject: [PATCH 4227/6909] Avoid sourcing parent clock when in a paused state --- .../Rulesets/UI/FrameStabilityContainer.cs | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 8edeaf851d..75f3aa90ee 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -85,23 +85,31 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { - state = frameStableClock.IsPaused.Value ? PlaybackState.NotValid : PlaybackState.Valid; + double proposedTime = manualClock.CurrentTime; if (frameStableClock.WaitingOnFrames.Value) { - // for now, force one update loop to check if frames have arrived - // this may have to change in the future where we want stable user pausing during replay playback. + // when waiting on frames, the update loop still needs to be run (at least once) to check for newly arrived frames. + // time should not be sourced from the parent clock in this case. state = PlaybackState.Valid; } + else if (!frameStableClock.IsPaused.Value) + { + state = PlaybackState.Valid; + proposedTime = parentGameplayClock.CurrentTime; + } + else + { + // time should not advance while paused, not should anything run. + state = PlaybackState.NotValid; + return true; + } int loops = MaxCatchUpFrames; - if (state == PlaybackState.NotValid) - return true; - while (loops-- > 0) { - updateClock(); + updateClock(ref proposedTime); if (state == PlaybackState.NotValid) break; @@ -113,7 +121,7 @@ namespace osu.Game.Rulesets.UI return true; } - private void updateClock() + private void updateClock(ref double proposedTime) { if (parentGameplayClock == null) setClock(); // LoadComplete may not be run yet, but we still want the clock. @@ -121,9 +129,6 @@ namespace osu.Game.Rulesets.UI // each update start with considering things in valid state. state = PlaybackState.Valid; - // our goal is to catch up to the time provided by the parent clock. - var proposedTime = parentGameplayClock.CurrentTime; - if (FrameStablePlayback) // if we require frame stability, the proposed time will be adjusted to move at most one known // frame interval in the current direction. From c0960e60cb59a50ace68901ae8a71df26ebc0e7b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 14:52:34 +0900 Subject: [PATCH 4228/6909] Add note about testflight link Sick of getting asked about this. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 7c749f3422..86c42dae12 100644 --- a/README.md +++ b/README.md @@ -34,6 +34,8 @@ If you are looking to install or test osu! without setting up a development envi | [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) | ------------- | ------------- | ------------- | ------------- | ------------- | +- The iOS testflight link may fill up (Apple has a hard limit of 10,000 users). We reset it occasionally when this happens. Please do not ask about this. Check back regularly for link resets or follow [peppy](https://twitter.com/ppy) on twitter for announcements of link resets. + - 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. If your platform is not listed above, there is still a chance you can manually build it by following the instructions below. From a59ea987b7811ee6e815d03e3af85e3d9bb27bae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 14:57:36 +0900 Subject: [PATCH 4229/6909] Make tests more resilient under headless execution --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 6485cbdad3..6c05049864 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -82,13 +82,21 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); checkPaused(true); + double? pausedTime = null; + + AddStep("store time", () => pausedTime = currentFrameStableTime); + sendFrames(); - checkPaused(false); AddUntilStep("wait for frame starvation", () => replayHandler.NextFrame == null); checkPaused(true); + + AddAssert("time advanced", () => currentFrameStableTime > pausedTime); } + private double currentFrameStableTime + => player.ChildrenOfType().First().FrameStableClock.CurrentTime; + [Test] public void TestPlayStartsWithNoFrames() { @@ -98,7 +106,7 @@ namespace osu.Game.Tests.Visual.Gameplay waitForPlayer(); checkPaused(true); - sendFrames(); + sendFrames(1000); // send enough frames to ensure play won't be paused checkPaused(false); } From 7dd3a748be4b0d7e7514d02f46b2a451a3bb3ae9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 15:03:38 +0900 Subject: [PATCH 4230/6909] Add further test logic to ensure retry / restart flow is working correctly --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 6c05049864..2b1bf1810b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -134,8 +134,16 @@ namespace osu.Game.Tests.Visual.Gameplay start(); sendFrames(); + waitForPlayer(); + + Player lastPlayer = null; + AddStep("store first player", () => lastPlayer = player); + start(); sendFrames(); + + waitForPlayer(); + AddAssert("player is different", () => lastPlayer != player); } [Test] From 6a31a313b6236521bb877dced8da5fb394e0dc40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 15:08:06 +0900 Subject: [PATCH 4231/6909] Fix stop watching test to check correct screen presence --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 2b1bf1810b..7c62cf47f4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -127,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestHostStartsPlayingWhileAlreadyWatching() + public void TestHostRetriesWhileWatching() { loadSpectatingScreen(); @@ -171,9 +171,8 @@ namespace osu.Game.Tests.Visual.Gameplay sendFrames(); waitForPlayer(); - // should immediately exit and unbind from streaming client AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); - AddUntilStep("spectating stopped", () => spectatorScreen.GetParentScreen() == null); + AddUntilStep("spectating stopped", () => spectatorScreen.GetChildScreen() == null); } [Test] From ce58bfdc4e845bc7141a7d0123445a3753c60860 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 15:09:12 +0900 Subject: [PATCH 4232/6909] Add test covering host retry after returning to spectator screen --- .../Visual/Gameplay/TestSceneSpectator.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 7c62cf47f4..864e297eda 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -175,6 +175,23 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("spectating stopped", () => spectatorScreen.GetChildScreen() == null); } + [Test] + public void TestStopWatchingThenHostRetries() + { + loadSpectatingScreen(); + + start(); + sendFrames(); + waitForPlayer(); + + AddStep("stop spectating", () => (Stack.CurrentScreen as Player)?.Exit()); + AddUntilStep("spectating stopped", () => spectatorScreen.GetChildScreen() == null); + + // host starts playing a new session + start(); + waitForPlayer(); + } + [Test] public void TestWatchingBeatmapThatDoesntExistLocally() { From fe409a55e63db279d4f45c5b91aeb935ac94d658 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 15:10:11 +0900 Subject: [PATCH 4233/6909] Rename starvation test --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 864e297eda..8821067618 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay private Player player => Stack.CurrentScreen as Player; [Test] - public void TestBasicSpectatingFlow() + public void TestFrameStarvationAndResume() { loadSpectatingScreen(); From fa857514254b32a5b3b6ccc5143c1ad5078ad776 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 15:10:42 +0900 Subject: [PATCH 4234/6909] Move helper functions to bottom of class --- .../Visual/Gameplay/TestSceneSpectator.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 8821067618..a4df450db9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -61,11 +61,6 @@ namespace osu.Game.Tests.Visual.Gameplay finish(); } - private OsuFramedReplayInputHandler replayHandler => - (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; - - private Player player => Stack.CurrentScreen as Player; - [Test] public void TestFrameStarvationAndResume() { @@ -94,9 +89,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("time advanced", () => currentFrameStableTime > pausedTime); } - private double currentFrameStableTime - => player.ChildrenOfType().First().FrameStableClock.CurrentTime; - [Test] public void TestPlayStartsWithNoFrames() { @@ -203,6 +195,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("screen didn't change", () => Stack.CurrentScreen is Spectator); } + private OsuFramedReplayInputHandler replayHandler => + (OsuFramedReplayInputHandler)Stack.ChildrenOfType().First().ReplayInputHandler; + + private Player player => Stack.CurrentScreen as Player; + + private double currentFrameStableTime + => player.ChildrenOfType().First().FrameStableClock.CurrentTime; + private void waitForPlayer() => AddUntilStep("wait for player", () => Stack.CurrentScreen is Player); private void start(int? beatmapId = null) => AddStep("start play", () => testSpectatorStreamingClient.StartPlay(beatmapId ?? importedBeatmapId)); From 3751c357a35b82bbc90cefe72eefe1052582f466 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 15:19:05 +0900 Subject: [PATCH 4235/6909] 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 2d531cf01e..27846fdf53 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 ca588b89d9..609ac0e5f9 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 9c22dec330..ebd38bc334 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 3ea27e23e8b8395b091221a78700f9c4b95aa347 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 15:20:10 +0900 Subject: [PATCH 4236/6909] Update namespace references --- osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs | 1 + osu.Game/Rulesets/UI/HitObjectContainer.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs index fde42bec04..9bfb6aa839 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 9a0217a1eb..4cadfa9ad4 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.UI From 2671d371da4acc2a39c11d8f584a318961a0ac0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 15:28:39 +0900 Subject: [PATCH 4237/6909] Move clock retrieval to new correct location --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 75f3aa90ee..231c5110ea 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -96,6 +96,10 @@ namespace osu.Game.Rulesets.UI else if (!frameStableClock.IsPaused.Value) { state = PlaybackState.Valid; + + if (parentGameplayClock == null) + setClock(); // LoadComplete may not be run yet, but we still want the clock. + proposedTime = parentGameplayClock.CurrentTime; } else @@ -123,9 +127,6 @@ namespace osu.Game.Rulesets.UI private void updateClock(ref double proposedTime) { - if (parentGameplayClock == null) - setClock(); // LoadComplete may not be run yet, but we still want the clock. - // each update start with considering things in valid state. state = PlaybackState.Valid; From a8e9c62583c8951b4e2f86b449fb1da6f75f3433 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 16:11:25 +0900 Subject: [PATCH 4238/6909] Make results panels aware of whether they are a local score that has just been set --- osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs | 2 +- osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs | 2 +- .../Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 7 ++++++- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 8 ++++++-- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- osu.Game/Screens/Ranking/ScorePanel.cs | 7 +++++-- osu.Game/Screens/Ranking/ScorePanelList.cs | 5 +++-- 7 files changed, 23 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 1e87893f39..f69ccc1773 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking } } }, - new AccuracyCircle(score) + new AccuracyCircle(score, false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 250fdc5ebd..5af55e99f8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Ranking private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () => { - Child = panel = new ScorePanel(score) + Child = panel = new ScorePanel(score, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 45da23f1f9..9aeb2b60b7 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -73,14 +73,19 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private readonly ScoreInfo score; + private readonly bool withFlair; + private SmoothCircularProgress accuracyCircle; private SmoothCircularProgress innerMask; private Container badges; private RankText rankText; - public AccuracyCircle(ScoreInfo score) + private SampleChannel applauseSound; + + public AccuracyCircle(ScoreInfo score, bool withFlair) { this.score = score; + this.withFlair = withFlair; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 30747438c3..5f8609d190 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -29,6 +29,8 @@ namespace osu.Game.Screens.Ranking.Expanded private const float padding = 10; private readonly ScoreInfo score; + private readonly bool withFlair; + private readonly List statisticDisplays = new List(); private FillFlowContainer starAndModDisplay; @@ -41,9 +43,11 @@ namespace osu.Game.Screens.Ranking.Expanded /// Creates a new . /// /// The score to display. - public ExpandedPanelMiddleContent(ScoreInfo score) + /// Whether to add flair for a new score being set. + public ExpandedPanelMiddleContent(ScoreInfo score, bool withFlair = false) { this.score = score; + this.withFlair = withFlair; RelativeSizeAxes = Axes.Both; Masking = true; @@ -116,7 +120,7 @@ namespace osu.Game.Screens.Ranking.Expanded Margin = new MarginPadding { Top = 40 }, RelativeSizeAxes = Axes.X, Height = 230, - Child = new AccuracyCircle(score) + Child = new AccuracyCircle(score, withFlair) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 026ce01857..f8bdf0140c 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -149,7 +149,7 @@ namespace osu.Game.Screens.Ranking }; if (Score != null) - ScorePanelList.AddScore(Score); + ScorePanelList.AddScore(Score, true); if (player != null && allowRetry) { diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index ee97ee55eb..6e6227da38 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -85,6 +85,8 @@ namespace osu.Game.Screens.Ranking public readonly ScoreInfo Score; + private readonly bool isNewLocalScore; + private Container content; private Container topLayerContainer; @@ -97,9 +99,10 @@ namespace osu.Game.Screens.Ranking private Container middleLayerContentContainer; private Drawable middleLayerContent; - public ScorePanel(ScoreInfo score) + public ScorePanel(ScoreInfo score, bool isNewLocalScore = false) { Score = score; + this.isNewLocalScore = isNewLocalScore; } [BackgroundDependencyLoader] @@ -209,7 +212,7 @@ namespace osu.Game.Screens.Ranking middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0)); - middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, isNewLocalScore).With(d => d.Alpha = 0)); break; case PanelState.Contracted: diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 0d7d339df0..cc163ba762 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -95,9 +95,10 @@ namespace osu.Game.Screens.Ranking /// Adds a to this list. /// /// The to add. - public ScorePanel AddScore(ScoreInfo score) + /// Whether this is a score that has just been achieved locally. Controls whether flair is added to the display or not. + public ScorePanel AddScore(ScoreInfo score, bool isNewLocalScore = false) { - var panel = new ScorePanel(score) + var panel = new ScorePanel(score, isNewLocalScore) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, From fb82c043a515cb95c3267eacdd915be43ff4e3c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 16:11:37 +0900 Subject: [PATCH 4239/6909] Add rank appear sound (new default) --- .../Ranking/Expanded/Accuracy/AccuracyCircle.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 9aeb2b60b7..0c15aa509f 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -4,6 +4,8 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -89,8 +91,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy } [BackgroundDependencyLoader] - private void load() + private void load(AudioManager audio) { + if (withFlair) + applauseSound = audio.Samples.Get(score.Rank >= ScoreRank.A ? "Results/rankpass" : "Results/rankfail"); + InternalChildren = new Drawable[] { new SmoothCircularProgress @@ -239,11 +244,16 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy continue; using (BeginDelayedSequence(inverseEasing(ACCURACY_TRANSFORM_EASING, Math.Min(1 - virtual_ss_percentage, badge.Accuracy) / targetAccuracy) * ACCURACY_TRANSFORM_DURATION, true)) + { badge.Appear(); + } } using (BeginDelayedSequence(TEXT_APPEAR_DELAY, true)) + { + this.Delay(-1440).Schedule(() => applauseSound?.Play()); rankText.Appear(); + } } } From b49a57941103145f782810e873b811723e593139 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 16:32:03 +0900 Subject: [PATCH 4240/6909] Allow SampleInfo to specify fallback sample lookup names --- osu.Game/Audio/SampleInfo.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index 2406b0bef2..240d70c418 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -10,14 +10,14 @@ namespace osu.Game.Audio /// public class SampleInfo : ISampleInfo { - private readonly string sampleName; + private readonly string[] sampleNames; - public SampleInfo(string sampleName) + public SampleInfo(params string[] sampleNames) { - this.sampleName = sampleName; + this.sampleNames = sampleNames; } - public IEnumerable LookupNames => new[] { sampleName }; + public IEnumerable LookupNames => sampleNames; public int Volume { get; } = 100; } From c863341ca1e7f1965222cbc0db0471da9c61ca29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 16:32:20 +0900 Subject: [PATCH 4241/6909] Don't force Gameplay prefix on all skin sample lookups --- osu.Game/Audio/HitSampleInfo.cs | 4 ++-- osu.Game/Skinning/SkinnableSound.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 8b1f5a366a..8efaeb3795 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -50,9 +50,9 @@ namespace osu.Game.Audio get { if (!string.IsNullOrEmpty(Suffix)) - yield return $"{Bank}-{Name}{Suffix}"; + yield return $"Gameplay/{Bank}-{Name}{Suffix}"; - yield return $"{Bank}-{Name}"; + yield return $"Gameplay/{Bank}-{Name}"; } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index f6e91811dd..ffa0a963ce 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -88,7 +88,7 @@ namespace osu.Game.Skinning { foreach (var lookup in s.LookupNames) { - if ((ch = samples.Get($"Gameplay/{lookup}")) != null) + if ((ch = samples.Get(lookup)) != null) break; } } From 5d5b0221e5199f96e0fda82669862bbe8854ec72 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 16:32:29 +0900 Subject: [PATCH 4242/6909] Add skinning support for legacy applause playback --- .../Ranking/Expanded/Accuracy/AccuracyCircle.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 0c15aa509f..c6d4b66724 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -11,9 +11,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Accuracy @@ -82,7 +84,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private Container badges; private RankText rankText; - private SampleChannel applauseSound; + private SkinnableSound applauseSound; public AccuracyCircle(ScoreInfo score, bool withFlair) { @@ -93,9 +95,6 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy [BackgroundDependencyLoader] private void load(AudioManager audio) { - if (withFlair) - applauseSound = audio.Samples.Get(score.Rank >= ScoreRank.A ? "Results/rankpass" : "Results/rankfail"); - InternalChildren = new Drawable[] { new SmoothCircularProgress @@ -213,6 +212,13 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy }, rankText = new RankText(score.Rank) }; + + if (withFlair) + { + AddInternal(applauseSound = score.Rank >= ScoreRank.A + ? new SkinnableSound(new SampleInfo("Results/rankpass", "applause")) + : new SkinnableSound(new SampleInfo("Results/rankfail"))); + } } private ScoreRank getRank(ScoreRank rank) From f1ce09930eb0fc5ff86b164a01c6edd2b6183894 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 17:03:45 +0900 Subject: [PATCH 4243/6909] Fix panel expanded state being updated multiple times unnecessarily --- osu.Game/Screens/Ranking/ScorePanelList.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index cc163ba762..e85580a734 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -119,7 +119,10 @@ namespace osu.Game.Screens.Ranking })); if (SelectedScore.Value == score) - selectedScoreChanged(new ValueChangedEvent(SelectedScore.Value, SelectedScore.Value)); + { + if (IsLoaded) + SelectedScore.TriggerChange(); + } else { // 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. @@ -143,11 +146,15 @@ namespace osu.Game.Screens.Ranking /// The to present. private void selectedScoreChanged(ValueChangedEvent score) { - // Contract the old panel. - foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue)) + // avoid contracting panels unnecessarily when TriggerChange is fired manually. + if (score.OldValue != score.NewValue) { - t.Panel.State = PanelState.Contracted; - t.Margin = new MarginPadding(); + // Contract the old panel. + foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue)) + { + t.Panel.State = PanelState.Contracted; + t.Margin = new MarginPadding(); + } } // Find the panel corresponding to the new score. From 4a26084df838916ae5cac8124c12d08aba74a106 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 17:04:33 +0900 Subject: [PATCH 4244/6909] Only play results panel animation once (and only for the local user) --- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 3 +++ osu.Game/Screens/Ranking/ScorePanel.cs | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 5f8609d190..711763330c 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -270,6 +270,9 @@ namespace osu.Game.Screens.Ranking.Expanded delay += 200; } } + + if (!withFlair) + FinishTransforms(true); }); } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 6e6227da38..df710e4eb8 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Ranking public readonly ScoreInfo Score; - private readonly bool isNewLocalScore; + private bool displayWithFlair; private Container content; @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Ranking public ScorePanel(ScoreInfo score, bool isNewLocalScore = false) { Score = score; - this.isNewLocalScore = isNewLocalScore; + displayWithFlair = isNewLocalScore; } [BackgroundDependencyLoader] @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Ranking state = value; - if (LoadState >= LoadState.Ready) + if (IsLoaded) updateState(); StateChanged?.Invoke(value); @@ -212,7 +212,10 @@ namespace osu.Game.Screens.Ranking middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0)); - middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, isNewLocalScore).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair).With(d => d.Alpha = 0)); + + // only the first expanded display should happen with flair. + displayWithFlair = false; break; case PanelState.Contracted: From 71e373ff511de83b10282992570165f087741551 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 16:11:25 +0900 Subject: [PATCH 4245/6909] Make results panels aware of whether they are a local score that has just been set --- osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs | 2 +- osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs | 2 +- .../Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 2 +- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 8 ++++++-- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- osu.Game/Screens/Ranking/ScorePanel.cs | 7 +++++-- osu.Game/Screens/Ranking/ScorePanelList.cs | 5 +++-- 7 files changed, 18 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 1e87893f39..f69ccc1773 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking } } }, - new AccuracyCircle(score) + new AccuracyCircle(score, false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs index 250fdc5ebd..5af55e99f8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneScorePanel.cs @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Ranking private void addPanelStep(ScoreInfo score, PanelState state = PanelState.Expanded) => AddStep("add panel", () => { - Child = panel = new ScorePanel(score) + Child = panel = new ScorePanel(score, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 45da23f1f9..337665b51f 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private Container badges; private RankText rankText; - public AccuracyCircle(ScoreInfo score) + public AccuracyCircle(ScoreInfo score, bool withFlair) { this.score = score; } diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 30747438c3..5f8609d190 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -29,6 +29,8 @@ namespace osu.Game.Screens.Ranking.Expanded private const float padding = 10; private readonly ScoreInfo score; + private readonly bool withFlair; + private readonly List statisticDisplays = new List(); private FillFlowContainer starAndModDisplay; @@ -41,9 +43,11 @@ namespace osu.Game.Screens.Ranking.Expanded /// Creates a new . /// /// The score to display. - public ExpandedPanelMiddleContent(ScoreInfo score) + /// Whether to add flair for a new score being set. + public ExpandedPanelMiddleContent(ScoreInfo score, bool withFlair = false) { this.score = score; + this.withFlair = withFlair; RelativeSizeAxes = Axes.Both; Masking = true; @@ -116,7 +120,7 @@ namespace osu.Game.Screens.Ranking.Expanded Margin = new MarginPadding { Top = 40 }, RelativeSizeAxes = Axes.X, Height = 230, - Child = new AccuracyCircle(score) + Child = new AccuracyCircle(score, withFlair) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 026ce01857..f8bdf0140c 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -149,7 +149,7 @@ namespace osu.Game.Screens.Ranking }; if (Score != null) - ScorePanelList.AddScore(Score); + ScorePanelList.AddScore(Score, true); if (player != null && allowRetry) { diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index ee97ee55eb..6e6227da38 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -85,6 +85,8 @@ namespace osu.Game.Screens.Ranking public readonly ScoreInfo Score; + private readonly bool isNewLocalScore; + private Container content; private Container topLayerContainer; @@ -97,9 +99,10 @@ namespace osu.Game.Screens.Ranking private Container middleLayerContentContainer; private Drawable middleLayerContent; - public ScorePanel(ScoreInfo score) + public ScorePanel(ScoreInfo score, bool isNewLocalScore = false) { Score = score; + this.isNewLocalScore = isNewLocalScore; } [BackgroundDependencyLoader] @@ -209,7 +212,7 @@ namespace osu.Game.Screens.Ranking middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0)); - middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, isNewLocalScore).With(d => d.Alpha = 0)); break; case PanelState.Contracted: diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 0d7d339df0..cc163ba762 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -95,9 +95,10 @@ namespace osu.Game.Screens.Ranking /// Adds a to this list. /// /// The to add. - public ScorePanel AddScore(ScoreInfo score) + /// Whether this is a score that has just been achieved locally. Controls whether flair is added to the display or not. + public ScorePanel AddScore(ScoreInfo score, bool isNewLocalScore = false) { - var panel = new ScorePanel(score) + var panel = new ScorePanel(score, isNewLocalScore) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, From 11f85779d5222f24fa9d0edd8097398417407b6a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 17:03:45 +0900 Subject: [PATCH 4246/6909] Fix panel expanded state being updated multiple times unnecessarily --- osu.Game/Screens/Ranking/ScorePanelList.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index cc163ba762..e85580a734 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -119,7 +119,10 @@ namespace osu.Game.Screens.Ranking })); if (SelectedScore.Value == score) - selectedScoreChanged(new ValueChangedEvent(SelectedScore.Value, SelectedScore.Value)); + { + if (IsLoaded) + SelectedScore.TriggerChange(); + } else { // 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. @@ -143,11 +146,15 @@ namespace osu.Game.Screens.Ranking /// The to present. private void selectedScoreChanged(ValueChangedEvent score) { - // Contract the old panel. - foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue)) + // avoid contracting panels unnecessarily when TriggerChange is fired manually. + if (score.OldValue != score.NewValue) { - t.Panel.State = PanelState.Contracted; - t.Margin = new MarginPadding(); + // Contract the old panel. + foreach (var t in flow.Where(t => t.Panel.Score == score.OldValue)) + { + t.Panel.State = PanelState.Contracted; + t.Margin = new MarginPadding(); + } } // Find the panel corresponding to the new score. From 0a0239a7c799b88049f4a4ca524a58d6f6839a2d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 17:04:33 +0900 Subject: [PATCH 4247/6909] Only play results panel animation once (and only for the local user) --- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 3 +++ osu.Game/Screens/Ranking/ScorePanel.cs | 11 +++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 5f8609d190..711763330c 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -270,6 +270,9 @@ namespace osu.Game.Screens.Ranking.Expanded delay += 200; } } + + if (!withFlair) + FinishTransforms(true); }); } } diff --git a/osu.Game/Screens/Ranking/ScorePanel.cs b/osu.Game/Screens/Ranking/ScorePanel.cs index 6e6227da38..df710e4eb8 100644 --- a/osu.Game/Screens/Ranking/ScorePanel.cs +++ b/osu.Game/Screens/Ranking/ScorePanel.cs @@ -85,7 +85,7 @@ namespace osu.Game.Screens.Ranking public readonly ScoreInfo Score; - private readonly bool isNewLocalScore; + private bool displayWithFlair; private Container content; @@ -102,7 +102,7 @@ namespace osu.Game.Screens.Ranking public ScorePanel(ScoreInfo score, bool isNewLocalScore = false) { Score = score; - this.isNewLocalScore = isNewLocalScore; + displayWithFlair = isNewLocalScore; } [BackgroundDependencyLoader] @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Ranking state = value; - if (LoadState >= LoadState.Ready) + if (IsLoaded) updateState(); StateChanged?.Invoke(value); @@ -212,7 +212,10 @@ namespace osu.Game.Screens.Ranking middleLayerBackground.FadeColour(expanded_middle_layer_colour, resize_duration, Easing.OutQuint); topLayerContentContainer.Add(topLayerContent = new ExpandedPanelTopContent(Score.User).With(d => d.Alpha = 0)); - middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, isNewLocalScore).With(d => d.Alpha = 0)); + middleLayerContentContainer.Add(middleLayerContent = new ExpandedPanelMiddleContent(Score, displayWithFlair).With(d => d.Alpha = 0)); + + // only the first expanded display should happen with flair. + displayWithFlair = false; break; case PanelState.Contracted: From 4dec46b33e977e8351f916a2c60c71a53a20b08a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 17:52:58 +0900 Subject: [PATCH 4248/6909] Attempt to fix in a less destructive way for now --- 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 8886188d95..e32ed07863 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -230,7 +230,7 @@ namespace osu.Game.Tests.Visual if (beatmap.HitObjects.Count > 0) // add buffer after last hitobject to allow for final replay frames etc. - trackLength = beatmap.HitObjects.Max(h => h.GetEndTime()) + 2000; + trackLength = Math.Max(trackLength, beatmap.HitObjects.Max(h => h.GetEndTime()) + 2000); if (referenceClock != null) { From 335d150a134073aff3488a0476eff2d970e5d185 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 18:11:50 +0900 Subject: [PATCH 4249/6909] Fix aim time being mutated inside update loop --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 231c5110ea..e29abfd83e 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { - double proposedTime = manualClock.CurrentTime; + double aimTime = manualClock.CurrentTime; if (frameStableClock.WaitingOnFrames.Value) { @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.UI if (parentGameplayClock == null) setClock(); // LoadComplete may not be run yet, but we still want the clock. - proposedTime = parentGameplayClock.CurrentTime; + aimTime = parentGameplayClock.CurrentTime; } else { @@ -113,7 +113,9 @@ namespace osu.Game.Rulesets.UI while (loops-- > 0) { - updateClock(ref proposedTime); + // update clock is always trying to approach the aim time. + // it should be provided as the original value each loop. + updateClock(aimTime); if (state == PlaybackState.NotValid) break; @@ -125,7 +127,7 @@ namespace osu.Game.Rulesets.UI return true; } - private void updateClock(ref double proposedTime) + private void updateClock(double proposedTime) { // each update start with considering things in valid state. state = PlaybackState.Valid; From f1b8a8f7f56337edd8693ea2cb8bf57c6c6bd5ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 18:16:04 +0900 Subject: [PATCH 4250/6909] Remove unused using --- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index c6d4b66724..bca3a07fa6 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -5,7 +5,6 @@ using System; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; From d69d78ab5d9278aaf31d20bd2895f664d1e3c2f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 15:20:10 +0900 Subject: [PATCH 4251/6909] Update namespace references --- osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs | 1 + osu.Game/Rulesets/UI/HitObjectContainer.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs index fde42bec04..9bfb6aa839 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 9a0217a1eb..4cadfa9ad4 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.UI From 3491dea9e2aece3ab76f0b931a5c9ef599e6eba4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 18:51:54 +0900 Subject: [PATCH 4252/6909] Fix scroll logic running before children may be alive in flow --- osu.Game/Screens/Ranking/ScorePanelList.cs | 45 ++++++++++++---------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index e85580a734..4325d317c4 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -118,22 +118,24 @@ namespace osu.Game.Screens.Ranking d.Origin = Anchor.Centre; })); - if (SelectedScore.Value == score) + if (IsLoaded) { - if (IsLoaded) - SelectedScore.TriggerChange(); - } - else - { - // 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) { - // 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; + SelectedScore.TriggerChange(); + } + else + { + // 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; + } } } @@ -170,12 +172,15 @@ namespace osu.Game.Screens.Ranking expandedTrackingComponent.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; expandedPanel.State = PanelState.Expanded; - // 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); + SchedulerAfterChildren.Add(() => + { + // 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); + }); } protected override void Update() From 7be4dfabd8d384107b5a21a78ae6fa97f09fbeaa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 20:23:15 +0900 Subject: [PATCH 4253/6909] Revert "Update namespace references" This reverts commit d69d78ab5d9278aaf31d20bd2895f664d1e3c2f1. --- osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs | 1 - osu.Game/Rulesets/UI/HitObjectContainer.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs index 9bfb6aa839..fde42bec04 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrumRollHitContainer.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 4cadfa9ad4..9a0217a1eb 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Performance; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.UI From 1c353b4745f1ea2bf2cb1a754d47478b7221c2b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 20:38:28 +0900 Subject: [PATCH 4254/6909] 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 27846fdf53..a4bcbd289d 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 609ac0e5f9..9be933c74a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index ebd38bc334..e26f8cc8b4 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -80,7 +80,7 @@ - + From 0c540537c9f3dde85e4ac74718be9d71a07c19b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 29 Oct 2020 14:30:50 +0100 Subject: [PATCH 4255/6909] Revert "Add BackgroundSource.Seasonal" This reverts commit 2871001cc294da01f2db8e8c35d84a86ab1503ac. --- osu.Game/Configuration/BackgroundSource.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Configuration/BackgroundSource.cs b/osu.Game/Configuration/BackgroundSource.cs index beef9ef1de..5726e96eb1 100644 --- a/osu.Game/Configuration/BackgroundSource.cs +++ b/osu.Game/Configuration/BackgroundSource.cs @@ -6,7 +6,6 @@ namespace osu.Game.Configuration public enum BackgroundSource { Skin, - Beatmap, - Seasonal + Beatmap } } From 7d523fee2896c71b7bb919cf99f41e88bb3ed2d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 29 Oct 2020 14:31:07 +0100 Subject: [PATCH 4256/6909] Revert "Set BackgroundSource.Seasonal as default setting" This reverts commit cdb2d23578e6de7ca266a23a51b5fac2ed15b8f4. --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 5c5af701bb..7d601c0cb9 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -130,7 +130,7 @@ namespace osu.Game.Configuration Set(OsuSetting.IntroSequence, IntroSequence.Triangles); - Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Seasonal); + Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); } public OsuConfigManager(Storage storage) From b189e0b7cfd8e761b313ab3b1c27371ff270a056 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 29 Oct 2020 16:01:22 +0100 Subject: [PATCH 4257/6909] Revert "Load SeasonalBackgroundLoader asynchronously" This reverts commit 81ebcd879668eb13cb28aa11bf4edfb8afb0fb99. --- .../Backgrounds/BackgroundScreenDefault.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index ec91dcc99f..ef41c5be3d 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -25,7 +25,6 @@ namespace osu.Game.Screens.Backgrounds private Bindable skin; private Bindable mode; private Bindable introSequence; - private readonly SeasonalBackgroundLoader seasonalBackgroundLoader = new SeasonalBackgroundLoader(); [Resolved] private IBindable beatmap { get; set; } @@ -51,7 +50,7 @@ namespace osu.Game.Screens.Backgrounds currentDisplay = RNG.Next(0, background_count); - LoadComponentAsync(seasonalBackgroundLoader, _ => LoadComponentAsync(createBackground(), display)); + display(createBackground()); } private void display(Background newBackground) @@ -91,10 +90,6 @@ namespace osu.Game.Screens.Backgrounds { switch (mode.Value) { - case BackgroundSource.Seasonal: - newBackground = seasonalBackgroundLoader.LoadBackground(backgroundName); - break; - case BackgroundSource.Beatmap: newBackground = new BeatmapBackground(beatmap.Value, backgroundName); break; @@ -105,18 +100,7 @@ namespace osu.Game.Screens.Backgrounds } } else - { - switch (mode.Value) - { - case BackgroundSource.Seasonal: - newBackground = seasonalBackgroundLoader.LoadBackground(backgroundName); - break; - - default: - newBackground = new Background(backgroundName); - break; - } - } + newBackground = new Background(backgroundName); newBackground.Depth = currentDisplay; From 76c0a790b404ef1afd65e30078473126a8b677c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 29 Oct 2020 17:28:04 +0100 Subject: [PATCH 4258/6909] Add separate Seasonal Backgrounds setting (Always, Sometimes, Never) --- osu.Game/Configuration/OsuConfigManager.cs | 2 ++ osu.Game/Configuration/SeasonalBackgrounds.cs | 12 ++++++++++++ .../Settings/Sections/Audio/MainMenuSettings.cs | 6 ++++++ 3 files changed, 20 insertions(+) create mode 100644 osu.Game/Configuration/SeasonalBackgrounds.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 7d601c0cb9..9f7280eef4 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -131,6 +131,7 @@ namespace osu.Game.Configuration Set(OsuSetting.IntroSequence, IntroSequence.Triangles); Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); + Set(OsuSetting.SeasonalBackgrounds, SeasonalBackgrounds.Sometimes); } public OsuConfigManager(Storage storage) @@ -239,5 +240,6 @@ namespace osu.Game.Configuration HitLighting, MenuBackgroundSource, GameplayDisableWinKey, + SeasonalBackgrounds } } diff --git a/osu.Game/Configuration/SeasonalBackgrounds.cs b/osu.Game/Configuration/SeasonalBackgrounds.cs new file mode 100644 index 0000000000..7708ae584f --- /dev/null +++ b/osu.Game/Configuration/SeasonalBackgrounds.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.Configuration +{ + public enum SeasonalBackgrounds + { + Always, + Sometimes, + Never + } +} diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs index d5de32ed05..ee57c0cfa6 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs @@ -39,6 +39,12 @@ namespace osu.Game.Overlays.Settings.Sections.Audio LabelText = "Background source", Current = config.GetBindable(OsuSetting.MenuBackgroundSource), Items = Enum.GetValues(typeof(BackgroundSource)).Cast() + }, + new SettingsDropdown + { + LabelText = "Seasonal backgrounds", + Current = config.GetBindable(OsuSetting.SeasonalBackgrounds), + Items = Enum.GetValues(typeof(SeasonalBackgrounds)).Cast() } }; } From 907e1921c720fc99cf9d76d135744fdf3d52fbeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 29 Oct 2020 17:31:42 +0100 Subject: [PATCH 4259/6909] Make SeasonalBackgroundLoader read from SessionStatics --- osu.Game/Configuration/SessionStatics.cs | 7 ++++++- .../Backgrounds/SeasonalBackgroundLoader.cs | 19 ++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 40b2adb867..326abed8fe 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.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.Collections.Generic; +using osu.Game.Online.API.Requests.Responses; + namespace osu.Game.Configuration { /// @@ -12,12 +15,14 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); + Set(Static.SeasonalBackgrounds, new List()); } } public enum Static { LoginOverlayDisplayed, - MutedAudioNotificationShownOnce + MutedAudioNotificationShownOnce, + SeasonalBackgrounds } } diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index af81b25cee..72785be3b4 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -5,9 +5,11 @@ using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; +using osu.Game.Configuration; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -17,17 +19,20 @@ namespace osu.Game.Graphics.Backgrounds [LongRunningLoad] public class SeasonalBackgroundLoader : Component { - private List backgrounds = new List(); + private Bindable> backgrounds; private int current; [BackgroundDependencyLoader] - private void load(IAPIProvider api) + private void load(SessionStatics sessionStatics, IAPIProvider api) { + backgrounds = sessionStatics.GetBindable>(Static.SeasonalBackgrounds); + if (backgrounds.Value.Any()) return; + var request = new GetSeasonalBackgroundsRequest(); request.Success += response => { - backgrounds = response.Backgrounds ?? backgrounds; - current = RNG.Next(0, backgrounds.Count); + backgrounds.Value = response.Backgrounds ?? backgrounds.Value; + current = RNG.Next(0, backgrounds.Value.Count); }; api.PerformAsync(request); @@ -37,10 +42,10 @@ namespace osu.Game.Graphics.Backgrounds { string url = null; - if (backgrounds.Any()) + if (backgrounds.Value.Any()) { - current = (current + 1) % backgrounds.Count; - url = backgrounds[current].Url; + current = (current + 1) % backgrounds.Value.Count; + url = backgrounds.Value[current].Url; } return new SeasonalBackground(url, fallbackTextureName); From bf4d99dfe7aa3ad5a4422430ae2079e4ef93d7f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 29 Oct 2020 17:43:10 +0100 Subject: [PATCH 4260/6909] Load SeasonalBackgroundLoader asynchronously --- osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index ef41c5be3d..98552dda71 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.Backgrounds private Bindable skin; private Bindable mode; private Bindable introSequence; + private readonly SeasonalBackgroundLoader seasonalBackgroundLoader = new SeasonalBackgroundLoader(); [Resolved] private IBindable beatmap { get; set; } @@ -50,7 +51,7 @@ namespace osu.Game.Screens.Backgrounds currentDisplay = RNG.Next(0, background_count); - display(createBackground()); + LoadComponentAsync(seasonalBackgroundLoader, _ => LoadComponentAsync(createBackground(), display)); } private void display(Background newBackground) From 34371b8888980ec2de400b16924657449263ea1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 29 Oct 2020 17:44:23 +0100 Subject: [PATCH 4261/6909] Show next Background on showSeasonalBackgrounds.ValueChanged --- osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 98552dda71..1be19c5854 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -25,6 +25,7 @@ namespace osu.Game.Screens.Backgrounds private Bindable skin; private Bindable mode; private Bindable introSequence; + private Bindable showSeasonalBackgrounds; private readonly SeasonalBackgroundLoader seasonalBackgroundLoader = new SeasonalBackgroundLoader(); [Resolved] @@ -42,12 +43,14 @@ namespace osu.Game.Screens.Backgrounds skin = skinManager.CurrentSkin.GetBoundCopy(); mode = config.GetBindable(OsuSetting.MenuBackgroundSource); introSequence = config.GetBindable(OsuSetting.IntroSequence); + showSeasonalBackgrounds = config.GetBindable(OsuSetting.SeasonalBackgrounds); user.ValueChanged += _ => Next(); skin.ValueChanged += _ => Next(); mode.ValueChanged += _ => Next(); beatmap.ValueChanged += _ => Next(); introSequence.ValueChanged += _ => Next(); + showSeasonalBackgrounds.ValueChanged += _ => Next(); currentDisplay = RNG.Next(0, background_count); From d9846fad37d4d2b9453621c2b225b9a3cf12cc8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 29 Oct 2020 18:03:36 +0100 Subject: [PATCH 4262/6909] Remove fallback texture parameter When there isn't a seasonal event, we don't want to fall back to the basic background here, but rather to the user selected background source. --- .../Backgrounds/SeasonalBackgroundLoader.cs | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index 72785be3b4..abd9664106 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -38,17 +37,14 @@ namespace osu.Game.Graphics.Backgrounds api.PerformAsync(request); } - public SeasonalBackground LoadBackground(string fallbackTextureName) + public SeasonalBackground LoadBackground() { - string url = null; + if (!backgrounds.Value.Any()) return null; - if (backgrounds.Value.Any()) - { - current = (current + 1) % backgrounds.Value.Count; - url = backgrounds.Value[current].Url; - } + current = (current + 1) % backgrounds.Value.Count; + string url = backgrounds.Value[current].Url; - return new SeasonalBackground(url, fallbackTextureName); + return new SeasonalBackground(url); } } @@ -56,18 +52,17 @@ namespace osu.Game.Graphics.Backgrounds public class SeasonalBackground : Background { private readonly string url; - private readonly string fallbackTextureName; + private const string fallback_texture_name = @"Backgrounds/bg1"; - public SeasonalBackground([CanBeNull] string url, string fallbackTextureName = @"Backgrounds/bg1") + public SeasonalBackground(string url) { this.url = url; - this.fallbackTextureName = fallbackTextureName; } [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - Sprite.Texture = textures.Get(url) ?? textures.Get(fallbackTextureName); + Sprite.Texture = textures.Get(url) ?? textures.Get(fallback_texture_name); } } } From fb1e09b3e793fae71caada4ad754d586ce7f5a12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Thu, 29 Oct 2020 18:04:48 +0100 Subject: [PATCH 4263/6909] Load seasonal backgrounds according to setting --- .../Screens/Backgrounds/BackgroundScreenDefault.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 1be19c5854..70eafd4aff 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -106,6 +106,18 @@ namespace osu.Game.Screens.Backgrounds else newBackground = new Background(backgroundName); + switch (showSeasonalBackgrounds.Value) + { + case SeasonalBackgrounds.Sometimes: + if (RNG.NextBool()) + goto case SeasonalBackgrounds.Always; + break; + + case SeasonalBackgrounds.Always: + newBackground = seasonalBackgroundLoader.LoadBackground() ?? newBackground; + break; + } + newBackground.Depth = currentDisplay; return newBackground; From 0c1d12460fcc0304ce1889e21c684da5f107f59d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 10:30:11 +0900 Subject: [PATCH 4264/6909] Remove unused parameter --- osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs | 2 +- osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs | 2 +- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index f69ccc1773..1e87893f39 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking } } }, - new AccuracyCircle(score, false) + new AccuracyCircle(score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs index 337665b51f..45da23f1f9 100644 --- a/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs +++ b/osu.Game/Screens/Ranking/Expanded/Accuracy/AccuracyCircle.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Accuracy private Container badges; private RankText rankText; - public AccuracyCircle(ScoreInfo score, bool withFlair) + public AccuracyCircle(ScoreInfo score) { this.score = score; } diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 711763330c..cb4560802b 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -120,7 +120,7 @@ namespace osu.Game.Screens.Ranking.Expanded Margin = new MarginPadding { Top = 40 }, RelativeSizeAxes = Axes.X, Height = 230, - Child = new AccuracyCircle(score, withFlair) + Child = new AccuracyCircle(score) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From c9a85587fb21be9c1d54e448e26f0b930e664ea0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 16:32:03 +0900 Subject: [PATCH 4265/6909] Allow SampleInfo to specify fallback sample lookup names --- osu.Game/Audio/SampleInfo.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index 2406b0bef2..240d70c418 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -10,14 +10,14 @@ namespace osu.Game.Audio /// public class SampleInfo : ISampleInfo { - private readonly string sampleName; + private readonly string[] sampleNames; - public SampleInfo(string sampleName) + public SampleInfo(params string[] sampleNames) { - this.sampleName = sampleName; + this.sampleNames = sampleNames; } - public IEnumerable LookupNames => new[] { sampleName }; + public IEnumerable LookupNames => sampleNames; public int Volume { get; } = 100; } From 0b28cca7e6b53b7e3be67782f00ea9a12b55cfa9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 29 Oct 2020 16:32:20 +0900 Subject: [PATCH 4266/6909] Don't force Gameplay prefix on all skin sample lookups --- osu.Game/Audio/HitSampleInfo.cs | 4 ++-- osu.Game/Skinning/SkinnableSound.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 8b1f5a366a..8efaeb3795 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -50,9 +50,9 @@ namespace osu.Game.Audio get { if (!string.IsNullOrEmpty(Suffix)) - yield return $"{Bank}-{Name}{Suffix}"; + yield return $"Gameplay/{Bank}-{Name}{Suffix}"; - yield return $"{Bank}-{Name}"; + yield return $"Gameplay/{Bank}-{Name}"; } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index f6e91811dd..ffa0a963ce 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -88,7 +88,7 @@ namespace osu.Game.Skinning { foreach (var lookup in s.LookupNames) { - if ((ch = samples.Get($"Gameplay/{lookup}")) != null) + if ((ch = samples.Get(lookup)) != null) break; } } From d319b27b3d9b90c8d69e48dcdd137d8ec08be566 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 11:14:08 +0900 Subject: [PATCH 4267/6909] Run sample lookup logic through getFallbackNames --- osu.Game/Skinning/LegacySkin.cs | 54 +++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 94b09684d3..d927d54abc 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -417,10 +417,14 @@ namespace osu.Game.Skinning public override SampleChannel GetSample(ISampleInfo sampleInfo) { - var lookupNames = sampleInfo.LookupNames; + IEnumerable lookupNames = null; if (sampleInfo is HitSampleInfo hitSample) lookupNames = getLegacyLookupNames(hitSample); + else + { + lookupNames = sampleInfo.LookupNames.SelectMany(getFallbackNames); + } foreach (var lookup in lookupNames) { @@ -433,6 +437,36 @@ namespace osu.Game.Skinning return null; } + private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample) + { + var lookupNames = hitSample.LookupNames.SelectMany(getFallbackNames); + + if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix)) + { + // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin. + // using .EndsWith() is intentional as it ensures parity in all edge cases + // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not). + foreach (var l in lookupNames) + { + if (!l.EndsWith(hitSample.Suffix, StringComparison.Ordinal)) + { + foreach (var n in getFallbackNames(l)) + yield return n; + } + } + } + else + { + foreach (var l in lookupNames) + yield return l; + } + + // also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort. + // going forward specifying banks shall always be required, even for elements that wouldn't require it on stable, + // which is why this is done locally here. + yield return hitSample.Name; + } + private IEnumerable getFallbackNames(string componentName) { // May be something like "Gameplay/osu/approachcircle" from lazer, or "Arrows/note1" from a user skin. @@ -442,23 +476,5 @@ namespace osu.Game.Skinning string lastPiece = componentName.Split('/').Last(); yield return componentName.StartsWith("Gameplay/taiko/", StringComparison.Ordinal) ? "taiko-" + lastPiece : lastPiece; } - - private IEnumerable getLegacyLookupNames(HitSampleInfo hitSample) - { - var lookupNames = hitSample.LookupNames; - - if (!UseCustomSampleBanks && !string.IsNullOrEmpty(hitSample.Suffix)) - // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin. - // using .EndsWith() is intentional as it ensures parity in all edge cases - // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not). - lookupNames = hitSample.LookupNames.Where(name => !name.EndsWith(hitSample.Suffix, StringComparison.Ordinal)); - - // also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort. - // going forward specifying banks shall always be required, even for elements that wouldn't require it on stable, - // which is why this is done locally here. - lookupNames = lookupNames.Append(hitSample.Name); - - return lookupNames; - } } } From 2ea4aa0a37c86b74f67cfb3f493e882e0adbd335 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 11:52:25 +0900 Subject: [PATCH 4268/6909] Fix incorrect specification on some sample lookups --- osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs | 2 +- osu.Game/Rulesets/Mods/ModNightcore.cs | 8 ++++---- osu.Game/Screens/Play/ComboEffects.cs | 2 +- osu.Game/Screens/Play/PauseOverlay.cs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 864e88d023..fc0cda2c1f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Gameplay skinSource = new TestSkinSourceContainer { RelativeSizeAxes = Axes.Both, - Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("normal-sliderslide")) + Child = skinnableSound = new PausableSkinnableSound(new SampleInfo("Gameplay/normal-sliderslide")) }, }; }); diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index 282de3a8e1..e8b051b4d9 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Mods { InternalChildren = new Drawable[] { - hatSample = new PausableSkinnableSound(new SampleInfo("nightcore-hat")), - clapSample = new PausableSkinnableSound(new SampleInfo("nightcore-clap")), - kickSample = new PausableSkinnableSound(new SampleInfo("nightcore-kick")), - finishSample = new PausableSkinnableSound(new SampleInfo("nightcore-finish")), + hatSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-hat")), + clapSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-clap")), + kickSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-kick")), + finishSample = new PausableSkinnableSound(new SampleInfo("Gameplay/nightcore-finish")), }; } diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index 5bcda50399..831b2f593c 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Play [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - InternalChild = comboBreakSample = new SkinnableSound(new SampleInfo("combobreak")); + InternalChild = comboBreakSample = new SkinnableSound(new SampleInfo("Gameplay/combobreak")); alwaysPlay = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak); } diff --git a/osu.Game/Screens/Play/PauseOverlay.cs b/osu.Game/Screens/Play/PauseOverlay.cs index 65f34aba3e..8778cff535 100644 --- a/osu.Game/Screens/Play/PauseOverlay.cs +++ b/osu.Game/Screens/Play/PauseOverlay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Play AddButton("Retry", colours.YellowDark, () => OnRetry?.Invoke()); AddButton("Quit", new Color4(170, 27, 39, 255), () => OnQuit?.Invoke()); - AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("pause-loop")) + AddInternal(pauseLoop = new SkinnableSound(new SampleInfo("Gameplay/pause-loop")) { Looping = true, Volume = { Value = 0 } From 2ec2749cb49aec0683217116904fb1f526ea26a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 11:52:08 +0900 Subject: [PATCH 4269/6909] Fix taiko lookup logic --- osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index a804ea5f82..c88480d18f 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -162,7 +162,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning get { foreach (var name in source.LookupNames) - yield return $"taiko-{name}"; + yield return name.Insert(name.LastIndexOf('/') + 1, "taiko-"); foreach (var name in source.LookupNames) yield return name; From fed4accfeab0100cdbcc3af7e292a1e408cf62c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 12:12:30 +0900 Subject: [PATCH 4270/6909] Update tests to refect new mappings --- .../convert-samples-expected-conversion.json | 16 ++++++++-------- .../mania-samples-expected-conversion.json | 8 ++++---- ...er-convert-samples-expected-conversion.json | 6 +++--- .../Formats/LegacyBeatmapDecoderTest.cs | 18 +++++++++--------- 4 files changed, 24 insertions(+), 24 deletions(-) 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 d49ffa01c5..6f1d45ad8c 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 @@ -6,20 +6,20 @@ "EndTime": 2750.0, "Column": 1, "NodeSamples": [ - ["normal-hitnormal"], - ["soft-hitnormal"], - ["drum-hitnormal"] + ["Gameplay/normal-hitnormal"], + ["Gameplay/soft-hitnormal"], + ["Gameplay/drum-hitnormal"] ], - "Samples": ["-hitnormal"] + "Samples": ["Gameplay/-hitnormal"] }, { "StartTime": 1875.0, "EndTime": 2750.0, "Column": 0, "NodeSamples": [ - ["soft-hitnormal"], - ["drum-hitnormal"] + ["Gameplay/soft-hitnormal"], + ["Gameplay/drum-hitnormal"] ], - "Samples": ["-hitnormal"] + "Samples": ["Gameplay/-hitnormal"] }] }, { "StartTime": 3750.0, @@ -27,7 +27,7 @@ "StartTime": 3750.0, "EndTime": 3750.0, "Column": 3, - "Samples": ["normal-hitnormal"] + "Samples": ["Gameplay/normal-hitnormal"] }] }] } \ No newline at end of file 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 1aca75a796..fd0c0cad60 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 @@ -6,10 +6,10 @@ "EndTime": 1500.0, "Column": 0, "NodeSamples": [ - ["normal-hitnormal"], + ["Gameplay/normal-hitnormal"], [] ], - "Samples": ["normal-hitnormal"] + "Samples": ["Gameplay/normal-hitnormal"] }] }, { "StartTime": 2000.0, @@ -18,10 +18,10 @@ "EndTime": 3000.0, "Column": 2, "NodeSamples": [ - ["drum-hitnormal"], + ["Gameplay/drum-hitnormal"], [] ], - "Samples": ["drum-hitnormal"] + "Samples": ["Gameplay/drum-hitnormal"] }] }] } \ No newline at end of file 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 index e3768a90d7..e07bd3c47c 100644 --- 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 @@ -5,17 +5,17 @@ "StartTime": 8470.0, "EndTime": 8470.0, "Column": 0, - "Samples": ["normal-hitnormal", "normal-hitclap"] + "Samples": ["Gameplay/normal-hitnormal", "Gameplay/normal-hitclap"] }, { "StartTime": 8626.470587768974, "EndTime": 8626.470587768974, "Column": 1, - "Samples": ["normal-hitnormal"] + "Samples": ["Gameplay/normal-hitnormal"] }, { "StartTime": 8782.941175537948, "EndTime": 8782.941175537948, "Column": 2, - "Samples": ["normal-hitnormal", "normal-hitclap"] + "Samples": ["Gameplay/normal-hitnormal", "Gameplay/normal-hitclap"] }] }] } diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs index b6e1af57fd..4b9e9dd88c 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyBeatmapDecoderTest.cs @@ -410,13 +410,13 @@ namespace osu.Game.Tests.Beatmaps.Formats { var hitObjects = decoder.Decode(stream).HitObjects; - Assert.AreEqual("normal-hitnormal", getTestableSampleInfo(hitObjects[0]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[0]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); // The control point at the end time of the slider should be applied - Assert.AreEqual("soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); + Assert.AreEqual("Gameplay/soft-hitnormal8", getTestableSampleInfo(hitObjects[4]).LookupNames.First()); } static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); @@ -432,9 +432,9 @@ namespace osu.Game.Tests.Beatmaps.Formats { var hitObjects = decoder.Decode(stream).HitObjects; - Assert.AreEqual("normal-hitnormal", getTestableSampleInfo(hitObjects[0]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal", getTestableSampleInfo(hitObjects[0]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal3", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); } static HitSampleInfo getTestableSampleInfo(HitObject hitObject) => hitObject.SampleControlPoint.ApplyTo(hitObject.Samples[0]); @@ -452,7 +452,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual("hit_1.wav", getTestableSampleInfo(hitObjects[0]).LookupNames.First()); Assert.AreEqual("hit_2.wav", getTestableSampleInfo(hitObjects[1]).LookupNames.First()); - Assert.AreEqual("normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); + Assert.AreEqual("Gameplay/normal-hitnormal2", getTestableSampleInfo(hitObjects[2]).LookupNames.First()); Assert.AreEqual("hit_1.wav", getTestableSampleInfo(hitObjects[3]).LookupNames.First()); Assert.AreEqual(70, getTestableSampleInfo(hitObjects[3]).Volume); } From b906736b85efdd9bdc739aab9fdeae74a5f43ad5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 12:28:40 +0900 Subject: [PATCH 4271/6909] Remove redundant initialisation --- 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 d927d54abc..4dea42cf92 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -417,7 +417,7 @@ namespace osu.Game.Skinning public override SampleChannel GetSample(ISampleInfo sampleInfo) { - IEnumerable lookupNames = null; + IEnumerable lookupNames; if (sampleInfo is HitSampleInfo hitSample) lookupNames = getLegacyLookupNames(hitSample); From 8e6c803900757b019b8d0db051d445c2b99039c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 12:39:11 +0900 Subject: [PATCH 4272/6909] Avoid running full updateClock loop when waiting on frames --- .../Rulesets/UI/FrameStabilityContainer.cs | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 0990a667ec..4d554124ae 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -85,30 +85,29 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { - double aimTime = manualClock.CurrentTime; + if (parentGameplayClock == null) + setClock(); // LoadComplete may not be run yet, but we still want the clock. + + double aimTime = parentGameplayClock.CurrentTime; if (frameStableClock.WaitingOnFrames.Value) { - // when waiting on frames, the update loop still needs to be run (at least once) to check for newly arrived frames. - // time should not be sourced from the parent clock in this case. - state = PlaybackState.Valid; + // waiting on frames is a special case where we want to avoid doing any update propagation, unless new frame data has arrived. + state = ReplayInputHandler.SetFrameFromTime(aimTime) != null ? PlaybackState.Valid : PlaybackState.NotValid; } else if (!frameStableClock.IsPaused.Value) { state = PlaybackState.Valid; - - if (parentGameplayClock == null) - setClock(); // LoadComplete may not be run yet, but we still want the clock. - - aimTime = parentGameplayClock.CurrentTime; } else { - // time should not advance while paused, not should anything run. + // time should not advance while paused, nor should anything run. state = PlaybackState.NotValid; - return true; } + if (state == PlaybackState.NotValid) + return true; + int loops = MaxCatchUpFrames; while (loops-- > 0) From 46d89d55f4e9b33edc97cb3696cf2a9ebcee7727 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 12:46:48 +0900 Subject: [PATCH 4273/6909] Add note about ScheduleAfterChildren requirement --- osu.Game/Screens/Ranking/ScorePanelList.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ScorePanelList.cs b/osu.Game/Screens/Ranking/ScorePanelList.cs index 4325d317c4..77b3d8fc3b 100644 --- a/osu.Game/Screens/Ranking/ScorePanelList.cs +++ b/osu.Game/Screens/Ranking/ScorePanelList.cs @@ -172,7 +172,8 @@ namespace osu.Game.Screens.Ranking expandedTrackingComponent.Margin = new MarginPadding { Horizontal = expanded_panel_spacing }; expandedPanel.State = PanelState.Expanded; - SchedulerAfterChildren.Add(() => + // requires schedule after children to ensure the flow (and thus ScrollContainer's ScrollableExtent) has been updated. + ScheduleAfterChildren(() => { // Scroll to the new panel. This is done manually since we need: // 1) To scroll after the scroll container's visible range is updated. From 18f92818daed770f18d35fc74cc22c4da392e567 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 13:09:13 +0900 Subject: [PATCH 4274/6909] Show current HUD visibility mode as a tracked setting --- osu.Game/Configuration/OsuConfigManager.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 7d601c0cb9..46c5e61784 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -170,6 +170,7 @@ namespace osu.Game.Configuration public override TrackedSettings CreateTrackedSettings() => new TrackedSettings { new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled")), + new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription())), new TrackedSetting(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())), }; } From 9bb86ccb832d8f838517c5d4ef7af8d018d2ed38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 13:09:22 +0900 Subject: [PATCH 4275/6909] Change shift-tab to cycle available HUD visibility modes --- osu.Game/Screens/Play/HUDOverlay.cs | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index b047d44f8a..623041d9ca 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -277,9 +277,25 @@ namespace osu.Game.Screens.Play switch (e.Key) { case Key.Tab: - configVisibilityMode.Value = configVisibilityMode.Value != HUDVisibilityMode.Never - ? HUDVisibilityMode.Never - : HUDVisibilityMode.HideDuringGameplay; + switch (configVisibilityMode.Value) + { + case HUDVisibilityMode.Never: + configVisibilityMode.Value = HUDVisibilityMode.HideDuringGameplay; + break; + + case HUDVisibilityMode.HideDuringGameplay: + configVisibilityMode.Value = HUDVisibilityMode.HideDuringBreaks; + break; + + case HUDVisibilityMode.HideDuringBreaks: + configVisibilityMode.Value = HUDVisibilityMode.Always; + break; + + case HUDVisibilityMode.Always: + configVisibilityMode.Value = HUDVisibilityMode.Never; + break; + } + return true; } } From f58f8e0f93a66d1553e63f151e9e63ddafe0475b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 13:46:54 +0900 Subject: [PATCH 4276/6909] 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 2d531cf01e..b3100d268b 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 ca588b89d9..54f86e5839 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 9c22dec330..6100b55334 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From c72017a7db4ad00ba2b63d976fca20c5ea9ac583 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 13:49:44 +0900 Subject: [PATCH 4277/6909] Remove "hide during breaks" option Probably wouldn't be used anyway. --- osu.Game/Configuration/HUDVisibilityMode.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Configuration/HUDVisibilityMode.cs b/osu.Game/Configuration/HUDVisibilityMode.cs index b0b55dd811..10f3f65355 100644 --- a/osu.Game/Configuration/HUDVisibilityMode.cs +++ b/osu.Game/Configuration/HUDVisibilityMode.cs @@ -12,9 +12,6 @@ namespace osu.Game.Configuration [Description("Hide during gameplay")] HideDuringGameplay, - [Description("Hide during breaks")] - HideDuringBreaks, - Always } } From b4eda65383cf80c56ac4991887836a12bb5a5be8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 13:53:51 +0900 Subject: [PATCH 4278/6909] Commit missing pieces --- osu.Game/Screens/Play/HUDOverlay.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 623041d9ca..0cfe6effc1 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -223,11 +223,6 @@ namespace osu.Game.Screens.Play ShowHud.Value = false; break; - case HUDVisibilityMode.HideDuringBreaks: - // always show during replay as we want the seek bar to be visible. - ShowHud.Value = replayLoaded.Value || !IsBreakTime.Value; - break; - case HUDVisibilityMode.HideDuringGameplay: // always show during replay as we want the seek bar to be visible. ShowHud.Value = replayLoaded.Value || IsBreakTime.Value; @@ -284,10 +279,6 @@ namespace osu.Game.Screens.Play break; case HUDVisibilityMode.HideDuringGameplay: - configVisibilityMode.Value = HUDVisibilityMode.HideDuringBreaks; - break; - - case HUDVisibilityMode.HideDuringBreaks: configVisibilityMode.Value = HUDVisibilityMode.Always; break; From 53bd31c69e6acada773346e350ffc12430ae651d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 14:00:07 +0900 Subject: [PATCH 4279/6909] Commit missing test pieces --- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 6ec673704c..6764501569 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestExternalHideDoesntAffectConfig() { - HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringBreaks; + HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay; createNew(); From 8928aa6d92990ce761c205e80ac3c20b1a4feffe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 14:19:40 +0900 Subject: [PATCH 4280/6909] Add key binding to show HUD while held --- .../Input/Bindings/GlobalActionContainer.cs | 4 +++ osu.Game/Screens/Play/HUDOverlay.cs | 36 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 41be4cfcc3..3de4bb1f9d 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -67,6 +67,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), + new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), }; public IEnumerable AudioControlKeyBindings => new[] @@ -187,5 +188,8 @@ namespace osu.Game.Input.Bindings [Description("Timing Mode")] EditorTimingMode, + + [Description("Hold for HUD")] + HoldForHUD, } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index b047d44f8a..c38c2ee5f7 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -8,8 +8,10 @@ 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.Input.Events; using osu.Game.Configuration; +using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; using osu.Game.Rulesets.Mods; @@ -22,7 +24,7 @@ using osuTK.Input; namespace osu.Game.Screens.Play { [Cached] - public class HUDOverlay : Container + public class HUDOverlay : Container, IKeyBindingHandler { public const float FADE_DURATION = 400; @@ -67,6 +69,8 @@ namespace osu.Game.Screens.Play internal readonly IBindable IsBreakTime = new Bindable(); + private bool holdingForHUD; + private IEnumerable hideTargets => new Drawable[] { visibilityContainer, KeyCounter }; public HUDOverlay(ScoreProcessor scoreProcessor, HealthProcessor healthProcessor, DrawableRuleset drawableRuleset, IReadOnlyList mods) @@ -217,6 +221,12 @@ namespace osu.Game.Screens.Play if (ShowHud.Disabled) return; + if (holdingForHUD) + { + ShowHud.Value = true; + return; + } + switch (configVisibilityMode.Value) { case HUDVisibilityMode.Never: @@ -351,5 +361,29 @@ namespace osu.Game.Screens.Play HealthDisplay?.BindHealthProcessor(processor); FailingLayer?.BindHealthProcessor(processor); } + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.HoldForHUD: + holdingForHUD = true; + updateVisibility(); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + switch (action) + { + case GlobalAction.HoldForHUD: + holdingForHUD = false; + updateVisibility(); + break; + } + } } } From bd7871d9f511b09ddd224759e3b9365f043c27d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 14:20:00 +0900 Subject: [PATCH 4281/6909] Update test scene to be non-skinnable (and add test covering momentary display) --- .../Visual/Gameplay/TestSceneHUDOverlay.cs | 66 ++++++++++--------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 6ec673704c..136c9e191d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -2,29 +2,23 @@ // 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.Framework.Testing; using osu.Game.Configuration; -using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu; using osu.Game.Screens.Play; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneHUDOverlay : SkinnableTestScene + public class TestSceneHUDOverlay : OsuManualInputManagerTestScene { private HUDOverlay hudOverlay; - private IEnumerable hudOverlays => CreatedDrawables.OfType(); - // best way to check without exposing. private Drawable hideTarget => hudOverlay.KeyCounter; private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First(); @@ -37,17 +31,9 @@ namespace osu.Game.Tests.Visual.Gameplay { createNew(); - AddRepeatStep("increase combo", () => - { - foreach (var hud in hudOverlays) - hud.ComboCounter.Current.Value++; - }, 10); + AddRepeatStep("increase combo", () => { hudOverlay.ComboCounter.Current.Value++; }, 10); - AddStep("reset combo", () => - { - foreach (var hud in hudOverlays) - hud.ComboCounter.Current.Value = 0; - }); + AddStep("reset combo", () => { hudOverlay.ComboCounter.Current.Value = 0; }); } [Test] @@ -77,7 +63,7 @@ namespace osu.Game.Tests.Visual.Gameplay { createNew(); - AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false)); + AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); @@ -86,6 +72,27 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("key counter flow not affected", () => keyCounterFlow.IsPresent); } + [Test] + public void TestMomentaryShowHUD() + { + createNew(); + + HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringBreaks; + AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode)); + + AddStep("set hud to never show", () => config.Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); + + AddUntilStep("wait for fade", () => !hideTarget.IsPresent); + + AddStep("trigger momentary show", () => InputManager.PressKey(Key.ControlLeft)); + AddUntilStep("wait for visible", () => hideTarget.IsPresent); + + AddStep("stop trigering", () => InputManager.ReleaseKey(Key.ControlLeft)); + AddUntilStep("wait for fade", () => !hideTarget.IsPresent); + + AddStep("set original config value", () => config.Set(OsuSetting.HUDVisibilityMode, originalConfigValue)); + } + [Test] public void TestExternalHideDoesntAffectConfig() { @@ -113,14 +120,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set keycounter visible false", () => { config.Set(OsuSetting.KeyOverlay, false); - hudOverlays.ForEach(h => h.KeyCounter.AlwaysVisible.Value = false); + hudOverlay.KeyCounter.AlwaysVisible.Value = false; }); - AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false)); + AddStep("set showhud false", () => hudOverlay.ShowHud.Value = false); AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); AddAssert("key counters hidden", () => !keyCounterFlow.IsPresent); - AddStep("set showhud true", () => hudOverlays.ForEach(h => h.ShowHud.Value = true)); + AddStep("set showhud true", () => hudOverlay.ShowHud.Value = true); AddUntilStep("hidetarget is visible", () => hideTarget.IsPresent); AddAssert("key counters still hidden", () => !keyCounterFlow.IsPresent); @@ -131,22 +138,17 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create overlay", () => { - SetContents(() => - { - hudOverlay = new HUDOverlay(null, null, null, Array.Empty()); + hudOverlay = new HUDOverlay(null, null, null, Array.Empty()); - // Add any key just to display the key counter visually. - hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); + // Add any key just to display the key counter visually. + hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); - hudOverlay.ComboCounter.Current.Value = 1; + hudOverlay.ComboCounter.Current.Value = 1; - action?.Invoke(hudOverlay); + action?.Invoke(hudOverlay); - return hudOverlay; - }); + Child = hudOverlay; }); } - - protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); } } From 984a243eff796505ee9f2ca6e85ae2a00233e3a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 14:23:24 +0900 Subject: [PATCH 4282/6909] Add skinnable test scene for HUD overlay --- .../Gameplay/TestSceneSkinnableHUDOverlay.cs | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs new file mode 100644 index 0000000000..fec1610160 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHUDOverlay.cs @@ -0,0 +1,99 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +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.Framework.Testing; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Play; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSkinnableHUDOverlay : SkinnableTestScene + { + private HUDOverlay hudOverlay; + + private IEnumerable hudOverlays => CreatedDrawables.OfType(); + + // best way to check without exposing. + private Drawable hideTarget => hudOverlay.KeyCounter; + private FillFlowContainer keyCounterFlow => hudOverlay.KeyCounter.ChildrenOfType>().First(); + + [Resolved] + private OsuConfigManager config { get; set; } + + [Test] + public void TestComboCounterIncrementing() + { + createNew(); + + AddRepeatStep("increase combo", () => + { + foreach (var hud in hudOverlays) + hud.ComboCounter.Current.Value++; + }, 10); + + AddStep("reset combo", () => + { + foreach (var hud in hudOverlays) + hud.ComboCounter.Current.Value = 0; + }); + } + + [Test] + public void TestFadesInOnLoadComplete() + { + float? initialAlpha = null; + + createNew(h => h.OnLoadComplete += _ => initialAlpha = hideTarget.Alpha); + AddUntilStep("wait for load", () => hudOverlay.IsAlive); + AddAssert("initial alpha was less than 1", () => initialAlpha < 1); + } + + [Test] + public void TestHideExternally() + { + createNew(); + + AddStep("set showhud false", () => hudOverlays.ForEach(h => h.ShowHud.Value = false)); + + AddUntilStep("hidetarget is hidden", () => !hideTarget.IsPresent); + AddAssert("pause button is still visible", () => hudOverlay.HoldToQuit.IsPresent); + + // Key counter flow container should not be affected by this, only the key counter display will be hidden as checked above. + AddAssert("key counter flow not affected", () => keyCounterFlow.IsPresent); + } + + private void createNew(Action action = null) + { + AddStep("create overlay", () => + { + SetContents(() => + { + hudOverlay = new HUDOverlay(null, null, null, Array.Empty()); + + // Add any key just to display the key counter visually. + hudOverlay.KeyCounter.Add(new KeyCounterKeyboard(Key.Space)); + + hudOverlay.ComboCounter.Current.Value = 1; + + action?.Invoke(hudOverlay); + + return hudOverlay; + }); + }); + } + + protected override Ruleset CreateRulesetForSkinProvider() => new OsuRuleset(); + } +} From 326fd0352568770e0cd0c494863ede623a977a73 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 15:25:53 +0900 Subject: [PATCH 4283/6909] Fix loop not exiting after first valid frame --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 4d554124ae..1ff8fc9715 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -110,7 +110,7 @@ namespace osu.Game.Rulesets.UI int loops = MaxCatchUpFrames; - while (loops-- > 0) + do { // update clock is always trying to approach the aim time. // it should be provided as the original value each loop. @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.UI base.UpdateSubTree(); UpdateSubTreeMasking(this, ScreenSpaceDrawQuad.AABBFloat); - } + } while (state == PlaybackState.RequiresCatchUp && loops-- > 0); return true; } From 0f997386aef703dc440d9afb0e1ee6e0227304ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 15:26:21 +0900 Subject: [PATCH 4284/6909] Fix direction and IsRunning not updating on first frame after becoming valid The parent clock will not unpause until WaitingForFrames becomes false, so I've moved the set of that before we start to propagate its values across. Doesn't fix any visible issue but should make propagation one game loop faster. --- osu.Game/Rulesets/UI/FrameStabilityContainer.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 1ff8fc9715..8a7f8d2739 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -144,22 +144,22 @@ namespace osu.Game.Rulesets.UI state = PlaybackState.NotValid; } - if (proposedTime != manualClock.CurrentTime) + if (state == PlaybackState.Valid) direction = proposedTime >= manualClock.CurrentTime ? 1 : -1; + double timeBehind = Math.Abs(proposedTime - parentGameplayClock.CurrentTime); + + frameStableClock.IsCatchingUp.Value = timeBehind > 200; + frameStableClock.WaitingOnFrames.Value = state == PlaybackState.NotValid; + manualClock.CurrentTime = proposedTime; manualClock.Rate = Math.Abs(parentGameplayClock.Rate) * direction; manualClock.IsRunning = parentGameplayClock.IsRunning; - double timeBehind = Math.Abs(manualClock.CurrentTime - parentGameplayClock.CurrentTime); - // determine whether catch-up is required. if (state == PlaybackState.Valid && timeBehind > 0) state = PlaybackState.RequiresCatchUp; - frameStableClock.IsCatchingUp.Value = timeBehind > 200; - frameStableClock.WaitingOnFrames.Value = state == PlaybackState.NotValid; - // The manual clock time has changed in the above code. The framed clock now needs to be updated // to ensure that the its time is valid for our children before input is processed framedClock.ProcessFrame(); From 32e68a6a3c16df655a597b1b3f770c77d2dec2cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 16:09:01 +0900 Subject: [PATCH 4285/6909] Fix FramedReplayInputHandler incorrectly blocking in streaming mode when time requested is before the first frame Most of this is just tidying up the logic to (hopefully) be better to follow, again (again (again)). The actual fix is that we now allow interpolation/playback when the incoming time is less than the first frame's time, regardless of receiving status. --- .../Replays/FramedReplayInputHandler.cs | 56 +++++++++++++------ 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index 8a4451fdca..b43324bcfa 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Replays return null; if (!currentFrameIndex.HasValue) - return (TFrame)Frames[0]; + return currentDirection > 0 ? (TFrame)Frames[0] : null; int nextFrame = clampedNextFrameIndex; @@ -109,30 +109,54 @@ namespace osu.Game.Rulesets.Replays Debug.Assert(currentDirection != 0); - TFrame next = NextFrame; - - // check if the next frame is valid for the current playback direction. - // validity is if the next frame is equal or "earlier" than the current point in time (so we can change to it) - int compare = time.CompareTo(next?.Time); - - if (next != null && (compare == 0 || compare == currentDirection)) + if (!HasFrames) { - currentFrameIndex = clampedNextFrameIndex; - return CurrentTime = CurrentFrame.Time; + // in the case all frames are received, allow time to progress regardless. + if (replay.HasReceivedAllFrames) + return CurrentTime = time; + + return null; } - // at this point, the frame can't be advanced (in the replay). - // even so, we may be able to move the clock forward due to being at the end of the replay or - // moving towards the next valid frame. + TFrame next = NextFrame; + + // if we have a next frame, check if it is before or at the current time in playback, and advance time to it if so. + if (next != null) + { + int compare = time.CompareTo(next.Time); + + if (compare == 0 || compare == currentDirection) + { + currentFrameIndex = clampedNextFrameIndex; + return CurrentTime = CurrentFrame.Time; + } + } + + // at this point, the frame index can't be advanced. + // even so, we may be able to propose the clock progresses forward due to being at an extent of the replay, + // or moving towards the next valid frame (ie. interpolating in a non-important section). // the exception is if currently in an important section, which is respected above all. if (inImportantSection) + { + Debug.Assert(next != null || !replay.HasReceivedAllFrames); return null; + } - // in the case we have no next frames and haven't received the full replay, block. - if (next == null && !replay.HasReceivedAllFrames) return null; + // if a next frame does exist, allow interpolation. + if (next != null) + return CurrentTime = time; - return CurrentTime = time; + // if all frames have been received, allow playing beyond extents. + if (replay.HasReceivedAllFrames) + return CurrentTime = time; + + // if not all frames are received but we are before the first frame, allow playing. + if (time < Frames[0].Time) + return CurrentTime = time; + + // in the case we have no next frames and haven't received enough frame data, block. + return null; } private void updateDirection(double time) From abaa532766500c71a2dbf5054019dc1b1f7de2be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 16:24:02 +0900 Subject: [PATCH 4286/6909] Add test coverage for streaming replay playback --- .../StreamingFramedReplayInputHandlerTest.cs | 296 ++++++++++++++++++ 1 file changed, 296 insertions(+) create mode 100644 osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs diff --git a/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs b/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs new file mode 100644 index 0000000000..21ec29b10b --- /dev/null +++ b/osu.Game.Tests/NonVisual/StreamingFramedReplayInputHandlerTest.cs @@ -0,0 +1,296 @@ +// Copyright (c) ppy Pty Ltd . 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.Replays; +using osu.Game.Rulesets.Replays; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class StreamingFramedReplayInputHandlerTest + { + private Replay replay; + private TestInputHandler handler; + + [SetUp] + public void SetUp() + { + handler = new TestInputHandler(replay = new Replay + { + HasReceivedAllFrames = false, + Frames = new List + { + new TestReplayFrame(0), + new TestReplayFrame(1000), + new TestReplayFrame(2000), + new TestReplayFrame(3000, true), + new TestReplayFrame(4000, true), + new TestReplayFrame(5000, true), + new TestReplayFrame(7000, true), + new TestReplayFrame(8000), + } + }); + } + + [Test] + public void TestNormalPlayback() + { + Assert.IsNull(handler.CurrentFrame); + + confirmCurrentFrame(null); + confirmNextFrame(0); + + setTime(0, 0); + confirmCurrentFrame(0); + confirmNextFrame(1); + + // 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. + setTime(1200, 1200); + confirmCurrentFrame(1); + + setTime(1400, 1400); + confirmCurrentFrame(1); + + // progressing beyond the next frame should force time to that frame once. + setTime(2200, 2000); + confirmCurrentFrame(2); + + // second attempt should progress to input time + setTime(2200, 2200); + confirmCurrentFrame(2); + + // entering important section + setTime(3000, 3000); + confirmCurrentFrame(3); + + // cannot progress within + setTime(3500, null); + confirmCurrentFrame(3); + + setTime(4000, 4000); + confirmCurrentFrame(4); + + // still cannot progress + setTime(4500, null); + confirmCurrentFrame(4); + + setTime(5200, 5000); + confirmCurrentFrame(5); + + // important section AllowedImportantTimeSpan allowance + setTime(5200, 5200); + confirmCurrentFrame(5); + + setTime(7200, 7000); + confirmCurrentFrame(6); + + setTime(7200, null); + confirmCurrentFrame(6); + + // exited important section + setTime(8200, 8000); + confirmCurrentFrame(7); + confirmNextFrame(null); + + setTime(8200, null); + confirmCurrentFrame(7); + confirmNextFrame(null); + + setTime(8400, null); + confirmCurrentFrame(7); + confirmNextFrame(null); + } + + [Test] + public void TestIntroTime() + { + setTime(-1000, -1000); + confirmCurrentFrame(null); + confirmNextFrame(0); + + setTime(-500, -500); + confirmCurrentFrame(null); + confirmNextFrame(0); + + setTime(0, 0); + confirmCurrentFrame(0); + confirmNextFrame(1); + } + + [Test] + public void TestBasicRewind() + { + setTime(2800, 0); + setTime(2800, 1000); + setTime(2800, 2000); + setTime(2800, 2800); + confirmCurrentFrame(2); + confirmNextFrame(3); + + // pivot without crossing a frame boundary + setTime(2700, 2700); + confirmCurrentFrame(2); + confirmNextFrame(1); + + // cross current frame boundary; should not yet update frame + setTime(1980, 1980); + confirmCurrentFrame(2); + confirmNextFrame(1); + + setTime(1200, 1200); + confirmCurrentFrame(2); + confirmNextFrame(1); + + // ensure each frame plays out until start + setTime(-500, 1000); + confirmCurrentFrame(1); + confirmNextFrame(0); + + setTime(-500, 0); + confirmCurrentFrame(0); + confirmNextFrame(null); + + setTime(-500, -500); + confirmCurrentFrame(0); + confirmNextFrame(null); + } + + [Test] + public void TestRewindInsideImportantSection() + { + fastForwardToPoint(3000); + + setTime(4000, 4000); + confirmCurrentFrame(4); + confirmNextFrame(5); + + setTime(3500, null); + confirmCurrentFrame(4); + confirmNextFrame(3); + + setTime(3000, 3000); + confirmCurrentFrame(3); + confirmNextFrame(2); + + setTime(3500, null); + confirmCurrentFrame(3); + confirmNextFrame(4); + + setTime(4000, 4000); + confirmCurrentFrame(4); + confirmNextFrame(5); + + setTime(4500, null); + confirmCurrentFrame(4); + confirmNextFrame(5); + + setTime(4000, null); + confirmCurrentFrame(4); + confirmNextFrame(5); + + setTime(3500, null); + confirmCurrentFrame(4); + confirmNextFrame(3); + + setTime(3000, 3000); + confirmCurrentFrame(3); + confirmNextFrame(2); + } + + [Test] + public void TestRewindOutOfImportantSection() + { + fastForwardToPoint(3500); + + confirmCurrentFrame(3); + confirmNextFrame(4); + + setTime(3200, null); + // next frame doesn't change even though direction reversed, because of important section. + confirmCurrentFrame(3); + confirmNextFrame(4); + + setTime(3000, null); + confirmCurrentFrame(3); + confirmNextFrame(4); + + setTime(2800, 2800); + confirmCurrentFrame(3); + confirmNextFrame(2); + } + + private void fastForwardToPoint(double destination) + { + for (int i = 0; i < 1000; i++) + { + if (handler.SetFrameFromTime(destination) == null) + return; + } + + throw new TimeoutException("Seek was never fulfilled"); + } + + private void setTime(double set, double? expect) + { + Assert.AreEqual(expect, handler.SetFrameFromTime(set)); + } + + private void confirmCurrentFrame(int? frame) + { + if (frame.HasValue) + { + Assert.IsNotNull(handler.CurrentFrame); + Assert.AreEqual(replay.Frames[frame.Value].Time, handler.CurrentFrame.Time); + } + else + { + Assert.IsNull(handler.CurrentFrame); + } + } + + private void confirmNextFrame(int? frame) + { + if (frame.HasValue) + { + Assert.IsNotNull(handler.NextFrame); + Assert.AreEqual(replay.Frames[frame.Value].Time, handler.NextFrame.Time); + } + else + { + Assert.IsNull(handler.NextFrame); + } + } + + private class TestReplayFrame : ReplayFrame + { + public readonly bool IsImportant; + + public TestReplayFrame(double time, bool isImportant = false) + : base(time) + { + IsImportant = isImportant; + } + } + + private class TestInputHandler : FramedReplayInputHandler + { + public TestInputHandler(Replay replay) + : base(replay) + { + FrameAccuratePlayback = true; + } + + protected override double AllowedImportantTimeSpan => 1000; + + protected override bool IsImportant(TestReplayFrame frame) => frame.IsImportant; + } + } +} From 43f9c1ebead2fb96e7f5994868ce43c93133d3ce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 30 Oct 2020 18:26:38 +0900 Subject: [PATCH 4287/6909] Fix HUD test having out of date value --- osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs index 9744575878..f9914e0193 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneHUDOverlay.cs @@ -77,7 +77,8 @@ namespace osu.Game.Tests.Visual.Gameplay { createNew(); - HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringBreaks; + HUDVisibilityMode originalConfigValue = HUDVisibilityMode.HideDuringGameplay; + AddStep("get original config value", () => originalConfigValue = config.Get(OsuSetting.HUDVisibilityMode)); AddStep("set hud to never show", () => config.Set(OsuSetting.HUDVisibilityMode, HUDVisibilityMode.Never)); From f27ce7521d69295e7af78478118e436db6f400ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Fri, 30 Oct 2020 10:27:43 +0100 Subject: [PATCH 4288/6909] Make "Sometimes" setting depend on season end date, rather than chance --- osu.Game/Configuration/SessionStatics.cs | 5 ++++- osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 8 ++++++++ .../API/Requests/Responses/APISeasonalBackgrounds.cs | 4 ++++ osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 2 +- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 326abed8fe..8100e0fb12 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Game.Online.API.Requests.Responses; @@ -15,6 +16,7 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); + Set(Static.SeasonEndDate, DateTimeOffset.MinValue); Set(Static.SeasonalBackgrounds, new List()); } } @@ -23,6 +25,7 @@ namespace osu.Game.Configuration { LoginOverlayDisplayed, MutedAudioNotificationShownOnce, - SeasonalBackgrounds + SeasonEndDate, + SeasonalBackgrounds, } } diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index abd9664106..d806c62650 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.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; @@ -18,19 +19,24 @@ namespace osu.Game.Graphics.Backgrounds [LongRunningLoad] public class SeasonalBackgroundLoader : Component { + private Bindable endDate; private Bindable> backgrounds; private int current; [BackgroundDependencyLoader] private void load(SessionStatics sessionStatics, IAPIProvider api) { + endDate = sessionStatics.GetBindable(Static.SeasonEndDate); backgrounds = sessionStatics.GetBindable>(Static.SeasonalBackgrounds); + if (backgrounds.Value.Any()) return; var request = new GetSeasonalBackgroundsRequest(); request.Success += response => { + endDate.Value = response.EndDate; backgrounds.Value = response.Backgrounds ?? backgrounds.Value; + current = RNG.Next(0, backgrounds.Value.Count); }; @@ -46,6 +52,8 @@ namespace osu.Game.Graphics.Backgrounds return new SeasonalBackground(url); } + + public bool IsInSeason() => DateTimeOffset.Now < endDate.Value; } [LongRunningLoad] diff --git a/osu.Game/Online/API/Requests/Responses/APISeasonalBackgrounds.cs b/osu.Game/Online/API/Requests/Responses/APISeasonalBackgrounds.cs index 6996ac4d9b..8e395f7397 100644 --- a/osu.Game/Online/API/Requests/Responses/APISeasonalBackgrounds.cs +++ b/osu.Game/Online/API/Requests/Responses/APISeasonalBackgrounds.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 Newtonsoft.Json; @@ -8,6 +9,9 @@ namespace osu.Game.Online.API.Requests.Responses { public class APISeasonalBackgrounds { + [JsonProperty("ends_at")] + public DateTimeOffset EndDate; + [JsonProperty("backgrounds")] public List Backgrounds { get; set; } } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 70eafd4aff..de73c82d5c 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Backgrounds switch (showSeasonalBackgrounds.Value) { case SeasonalBackgrounds.Sometimes: - if (RNG.NextBool()) + if (seasonalBackgroundLoader.IsInSeason()) goto case SeasonalBackgrounds.Always; break; From 1bd461f229a69dc139fc7c36e4f10cb5a874243c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 20:20:34 +0900 Subject: [PATCH 4289/6909] Move clock logic back to inside updateClock method --- .../Rulesets/UI/FrameStabilityContainer.cs | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index 8a7f8d2739..c8f37d75a0 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -85,15 +85,10 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { - if (parentGameplayClock == null) - setClock(); // LoadComplete may not be run yet, but we still want the clock. - - double aimTime = parentGameplayClock.CurrentTime; - if (frameStableClock.WaitingOnFrames.Value) { // waiting on frames is a special case where we want to avoid doing any update propagation, unless new frame data has arrived. - state = ReplayInputHandler.SetFrameFromTime(aimTime) != null ? PlaybackState.Valid : PlaybackState.NotValid; + state = PlaybackState.Valid; } else if (!frameStableClock.IsPaused.Value) { @@ -103,10 +98,8 @@ namespace osu.Game.Rulesets.UI { // time should not advance while paused, nor should anything run. state = PlaybackState.NotValid; - } - - if (state == PlaybackState.NotValid) return true; + } int loops = MaxCatchUpFrames; @@ -114,7 +107,7 @@ namespace osu.Game.Rulesets.UI { // update clock is always trying to approach the aim time. // it should be provided as the original value each loop. - updateClock(aimTime); + updateClock(); if (state == PlaybackState.NotValid) break; @@ -126,8 +119,13 @@ namespace osu.Game.Rulesets.UI return true; } - private void updateClock(double proposedTime) + private void updateClock() { + if (parentGameplayClock == null) + setClock(); // LoadComplete may not be run yet, but we still want the clock. + + double proposedTime = parentGameplayClock.CurrentTime; + // each update start with considering things in valid state. state = PlaybackState.Valid; From b4e53110146b4f7d651cd588d794be0d075e4db1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 20:37:07 +0900 Subject: [PATCH 4290/6909] Move initial state set inside updateClock --- .../Rulesets/UI/FrameStabilityContainer.cs | 35 +++++++++---------- 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs index c8f37d75a0..e9865f6c8b 100644 --- a/osu.Game/Rulesets/UI/FrameStabilityContainer.cs +++ b/osu.Game/Rulesets/UI/FrameStabilityContainer.cs @@ -85,22 +85,6 @@ namespace osu.Game.Rulesets.UI public override bool UpdateSubTree() { - if (frameStableClock.WaitingOnFrames.Value) - { - // waiting on frames is a special case where we want to avoid doing any update propagation, unless new frame data has arrived. - state = PlaybackState.Valid; - } - else if (!frameStableClock.IsPaused.Value) - { - state = PlaybackState.Valid; - } - else - { - // time should not advance while paused, nor should anything run. - state = PlaybackState.NotValid; - return true; - } - int loops = MaxCatchUpFrames; do @@ -121,14 +105,27 @@ namespace osu.Game.Rulesets.UI private void updateClock() { + if (frameStableClock.WaitingOnFrames.Value) + { + // if waiting on frames, run one update loop to determine if frames have arrived. + state = PlaybackState.Valid; + } + else if (frameStableClock.IsPaused.Value) + { + // time should not advance while paused, nor should anything run. + state = PlaybackState.NotValid; + return; + } + else + { + state = PlaybackState.Valid; + } + if (parentGameplayClock == null) setClock(); // LoadComplete may not be run yet, but we still want the clock. double proposedTime = parentGameplayClock.CurrentTime; - // each update start with considering things in valid state. - state = PlaybackState.Valid; - if (FrameStablePlayback) // if we require frame stability, the proposed time will be adjusted to move at most one known // frame interval in the current direction. From a1fa6588f6a933c97551f78b98e21734f06e0c10 Mon Sep 17 00:00:00 2001 From: cadon0 Date: Sat, 31 Oct 2020 01:03:57 +1300 Subject: [PATCH 4291/6909] Fix "bounce" when metadata container text is empty --- osu.Game/Screens/Select/BeatmapDetails.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 0ee52f3e48..71f78c5c95 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -300,6 +300,7 @@ namespace osu.Game.Screens.Select public MetadataSection(string title) { + Alpha = 0; RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; From bc69ed3870457e25e83b32879e3fc36982f4031d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 30 Oct 2020 22:33:05 +0900 Subject: [PATCH 4292/6909] Simplify sample lookup --- osu.Game/Skinning/LegacySkin.cs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 4dea42cf92..fb020f4e39 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -446,21 +446,12 @@ namespace osu.Game.Skinning // for compatibility with stable, exclude the lookup names with the custom sample bank suffix, if they are not valid for use in this skin. // using .EndsWith() is intentional as it ensures parity in all edge cases // (see LegacyTaikoSampleInfo for an example of one - prioritising the taiko prefix should still apply, but the sample bank should not). - foreach (var l in lookupNames) - { - if (!l.EndsWith(hitSample.Suffix, StringComparison.Ordinal)) - { - foreach (var n in getFallbackNames(l)) - yield return n; - } - } - } - else - { - foreach (var l in lookupNames) - yield return l; + lookupNames = lookupNames.Where(name => !name.EndsWith(hitSample.Suffix, StringComparison.Ordinal)); } + foreach (var l in lookupNames) + yield return l; + // also for compatibility, try falling back to non-bank samples (so-called "universal" samples) as the last resort. // going forward specifying banks shall always be required, even for elements that wouldn't require it on stable, // which is why this is done locally here. From 4e3fb615d25c6a2edfbec009401ec73f925840f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Fri, 30 Oct 2020 15:54:10 +0100 Subject: [PATCH 4293/6909] Rename "SeasonalBackgrounds" to "SeasonalBackgroundMode" --- osu.Game/Configuration/OsuConfigManager.cs | 4 ++-- ...nalBackgrounds.cs => SeasonalBackgroundMode.cs} | 2 +- .../Settings/Sections/Audio/MainMenuSettings.cs | 6 +++--- .../Screens/Backgrounds/BackgroundScreenDefault.cs | 14 +++++++------- 4 files changed, 13 insertions(+), 13 deletions(-) rename osu.Game/Configuration/{SeasonalBackgrounds.cs => SeasonalBackgroundMode.cs} (86%) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index e579898c05..e0971d238a 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -131,7 +131,7 @@ namespace osu.Game.Configuration Set(OsuSetting.IntroSequence, IntroSequence.Triangles); Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); - Set(OsuSetting.SeasonalBackgrounds, SeasonalBackgrounds.Sometimes); + Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes); } public OsuConfigManager(Storage storage) @@ -241,6 +241,6 @@ namespace osu.Game.Configuration HitLighting, MenuBackgroundSource, GameplayDisableWinKey, - SeasonalBackgrounds + SeasonalBackgroundMode } } diff --git a/osu.Game/Configuration/SeasonalBackgrounds.cs b/osu.Game/Configuration/SeasonalBackgroundMode.cs similarity index 86% rename from osu.Game/Configuration/SeasonalBackgrounds.cs rename to osu.Game/Configuration/SeasonalBackgroundMode.cs index 7708ae584f..406736b2a4 100644 --- a/osu.Game/Configuration/SeasonalBackgrounds.cs +++ b/osu.Game/Configuration/SeasonalBackgroundMode.cs @@ -3,7 +3,7 @@ namespace osu.Game.Configuration { - public enum SeasonalBackgrounds + public enum SeasonalBackgroundMode { Always, Sometimes, diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs index ee57c0cfa6..7682967d10 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs @@ -40,11 +40,11 @@ namespace osu.Game.Overlays.Settings.Sections.Audio Current = config.GetBindable(OsuSetting.MenuBackgroundSource), Items = Enum.GetValues(typeof(BackgroundSource)).Cast() }, - new SettingsDropdown + new SettingsDropdown { LabelText = "Seasonal backgrounds", - Current = config.GetBindable(OsuSetting.SeasonalBackgrounds), - Items = Enum.GetValues(typeof(SeasonalBackgrounds)).Cast() + Current = config.GetBindable(OsuSetting.SeasonalBackgroundMode), + Items = Enum.GetValues(typeof(SeasonalBackgroundMode)).Cast() } }; } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index de73c82d5c..a5bdcee8d4 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Backgrounds private Bindable skin; private Bindable mode; private Bindable introSequence; - private Bindable showSeasonalBackgrounds; + private Bindable seasonalBackgroundMode; private readonly SeasonalBackgroundLoader seasonalBackgroundLoader = new SeasonalBackgroundLoader(); [Resolved] @@ -43,14 +43,14 @@ namespace osu.Game.Screens.Backgrounds skin = skinManager.CurrentSkin.GetBoundCopy(); mode = config.GetBindable(OsuSetting.MenuBackgroundSource); introSequence = config.GetBindable(OsuSetting.IntroSequence); - showSeasonalBackgrounds = config.GetBindable(OsuSetting.SeasonalBackgrounds); + seasonalBackgroundMode = config.GetBindable(OsuSetting.SeasonalBackgroundMode); user.ValueChanged += _ => Next(); skin.ValueChanged += _ => Next(); mode.ValueChanged += _ => Next(); beatmap.ValueChanged += _ => Next(); introSequence.ValueChanged += _ => Next(); - showSeasonalBackgrounds.ValueChanged += _ => Next(); + seasonalBackgroundMode.ValueChanged += _ => Next(); currentDisplay = RNG.Next(0, background_count); @@ -106,14 +106,14 @@ namespace osu.Game.Screens.Backgrounds else newBackground = new Background(backgroundName); - switch (showSeasonalBackgrounds.Value) + switch (seasonalBackgroundMode.Value) { - case SeasonalBackgrounds.Sometimes: + case SeasonalBackgroundMode.Sometimes: if (seasonalBackgroundLoader.IsInSeason()) - goto case SeasonalBackgrounds.Always; + goto case SeasonalBackgroundMode.Always; break; - case SeasonalBackgrounds.Always: + case SeasonalBackgroundMode.Always: newBackground = seasonalBackgroundLoader.LoadBackground() ?? newBackground; break; } From d19dd4eef6ba621616f684b5b07d41d154d68f87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Fri, 30 Oct 2020 15:56:19 +0100 Subject: [PATCH 4294/6909] IsInSeason() -> IsInSeason --- osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 2 +- osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index d806c62650..ff1a2c9f37 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -53,7 +53,7 @@ namespace osu.Game.Graphics.Backgrounds return new SeasonalBackground(url); } - public bool IsInSeason() => DateTimeOffset.Now < endDate.Value; + public bool IsInSeason => DateTimeOffset.Now < endDate.Value; } [LongRunningLoad] diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index a5bdcee8d4..5fa4ddc041 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -109,7 +109,7 @@ namespace osu.Game.Screens.Backgrounds switch (seasonalBackgroundMode.Value) { case SeasonalBackgroundMode.Sometimes: - if (seasonalBackgroundLoader.IsInSeason()) + if (seasonalBackgroundLoader.IsInSeason) goto case SeasonalBackgroundMode.Always; break; From 6f6a8e2a8fe09581630728d0e347b48e91c3f80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Fri, 30 Oct 2020 16:06:48 +0100 Subject: [PATCH 4295/6909] Convert switch to if --- .../Screens/Backgrounds/BackgroundScreenDefault.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 5fa4ddc041..39ecd70084 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -106,16 +106,10 @@ namespace osu.Game.Screens.Backgrounds else newBackground = new Background(backgroundName); - switch (seasonalBackgroundMode.Value) + if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Always + || seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && seasonalBackgroundLoader.IsInSeason) { - case SeasonalBackgroundMode.Sometimes: - if (seasonalBackgroundLoader.IsInSeason) - goto case SeasonalBackgroundMode.Always; - break; - - case SeasonalBackgroundMode.Always: - newBackground = seasonalBackgroundLoader.LoadBackground() ?? newBackground; - break; + newBackground = seasonalBackgroundLoader.LoadBackground() ?? newBackground; } newBackground.Depth = currentDisplay; From f6eb5680ec53626707452cf08c0926713fbf2ac1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Fri, 30 Oct 2020 16:43:18 +0100 Subject: [PATCH 4296/6909] Save full api response in SessionStatics --- osu.Game/Configuration/SessionStatics.cs | 8 ++----- .../Backgrounds/SeasonalBackgroundLoader.cs | 24 ++++++++----------- 2 files changed, 12 insertions(+), 20 deletions(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 8100e0fb12..199889391b 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.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.Online.API.Requests.Responses; namespace osu.Game.Configuration @@ -16,8 +14,7 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); - Set(Static.SeasonEndDate, DateTimeOffset.MinValue); - Set(Static.SeasonalBackgrounds, new List()); + Set(Static.SeasonalBackgroundsResponse, null); } } @@ -25,7 +22,6 @@ namespace osu.Game.Configuration { LoginOverlayDisplayed, MutedAudioNotificationShownOnce, - SeasonEndDate, - SeasonalBackgrounds, + SeasonalBackgroundsResponse, } } diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index ff1a2c9f37..c884756c80 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.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.Bindables; @@ -19,25 +18,21 @@ namespace osu.Game.Graphics.Backgrounds [LongRunningLoad] public class SeasonalBackgroundLoader : Component { - private Bindable endDate; - private Bindable> backgrounds; + private Bindable cachedResponse; private int current; [BackgroundDependencyLoader] private void load(SessionStatics sessionStatics, IAPIProvider api) { - endDate = sessionStatics.GetBindable(Static.SeasonEndDate); - backgrounds = sessionStatics.GetBindable>(Static.SeasonalBackgrounds); + cachedResponse = sessionStatics.GetBindable(Static.SeasonalBackgroundsResponse); - if (backgrounds.Value.Any()) return; + if (cachedResponse.Value != null) return; var request = new GetSeasonalBackgroundsRequest(); request.Success += response => { - endDate.Value = response.EndDate; - backgrounds.Value = response.Backgrounds ?? backgrounds.Value; - - current = RNG.Next(0, backgrounds.Value.Count); + cachedResponse.Value = response; + current = RNG.Next(0, cachedResponse.Value.Backgrounds.Count); }; api.PerformAsync(request); @@ -45,15 +40,16 @@ namespace osu.Game.Graphics.Backgrounds public SeasonalBackground LoadBackground() { - if (!backgrounds.Value.Any()) return null; + var backgrounds = cachedResponse.Value.Backgrounds; + if (!backgrounds.Any()) return null; - current = (current + 1) % backgrounds.Value.Count; - string url = backgrounds.Value[current].Url; + current = (current + 1) % backgrounds.Count; + string url = backgrounds[current].Url; return new SeasonalBackground(url); } - public bool IsInSeason => DateTimeOffset.Now < endDate.Value; + public bool IsInSeason => DateTimeOffset.Now < cachedResponse.Value.EndDate; } [LongRunningLoad] From 0b46c19b23995f42e35b82877b41404200f109ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Fri, 30 Oct 2020 17:16:51 +0100 Subject: [PATCH 4297/6909] Move seasonalBackgroundMode check up and early return if available --- .../Backgrounds/BackgroundScreenDefault.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 39ecd70084..b65b45060f 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -79,6 +79,18 @@ namespace osu.Game.Screens.Backgrounds Background newBackground; string backgroundName; + if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Always + || seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && seasonalBackgroundLoader.IsInSeason) + { + var seasonalBackground = seasonalBackgroundLoader.LoadBackground(); + + if (seasonalBackground != null) + { + seasonalBackground.Depth = currentDisplay; + return seasonalBackground; + } + } + switch (introSequence.Value) { case IntroSequence.Welcome: @@ -106,12 +118,6 @@ namespace osu.Game.Screens.Backgrounds else newBackground = new Background(backgroundName); - if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Always - || seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && seasonalBackgroundLoader.IsInSeason) - { - newBackground = seasonalBackgroundLoader.LoadBackground() ?? newBackground; - } - newBackground.Depth = currentDisplay; return newBackground; From 51a58269add518f7ea68f590fceeff11612cc5c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Fri, 30 Oct 2020 17:57:29 +0100 Subject: [PATCH 4298/6909] Fix nullref in case of successfull request but no backgrounds available --- osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index c884756c80..daceb05fd7 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -32,7 +32,7 @@ namespace osu.Game.Graphics.Backgrounds request.Success += response => { cachedResponse.Value = response; - current = RNG.Next(0, cachedResponse.Value.Backgrounds.Count); + current = RNG.Next(0, response.Backgrounds?.Count ?? 0); }; api.PerformAsync(request); @@ -41,7 +41,7 @@ namespace osu.Game.Graphics.Backgrounds public SeasonalBackground LoadBackground() { var backgrounds = cachedResponse.Value.Backgrounds; - if (!backgrounds.Any()) return null; + if (backgrounds == null || !backgrounds.Any()) return null; current = (current + 1) % backgrounds.Count; string url = backgrounds[current].Url; From d5dfd1dffeee753cb47d70afc87a1b3055e1962e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Max=20H=C3=BCbner?= Date: Fri, 30 Oct 2020 18:07:07 +0100 Subject: [PATCH 4299/6909] Insert optional parentheses --- osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index b65b45060f..45374e1223 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Backgrounds string backgroundName; if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Always - || seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && seasonalBackgroundLoader.IsInSeason) + || (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && seasonalBackgroundLoader.IsInSeason)) { var seasonalBackground = seasonalBackgroundLoader.LoadBackground(); From 82ef85569bffe30e64a00414ddb8465348d645d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Oct 2020 19:35:08 +0100 Subject: [PATCH 4300/6909] Fix nullref when querying IsInSeason before request completion --- osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index daceb05fd7..2963d57a97 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -49,7 +49,7 @@ namespace osu.Game.Graphics.Backgrounds return new SeasonalBackground(url); } - public bool IsInSeason => DateTimeOffset.Now < cachedResponse.Value.EndDate; + public bool IsInSeason => cachedResponse.Value != null && DateTimeOffset.Now < cachedResponse.Value.EndDate; } [LongRunningLoad] From 20c27c69431eb680781338c4e0deba1f2a1658ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Oct 2020 19:55:17 +0100 Subject: [PATCH 4301/6909] Rename lookup & field --- osu.Game/Configuration/SessionStatics.cs | 4 ++-- .../Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 199889391b..c470058ae8 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -14,7 +14,7 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); - Set(Static.SeasonalBackgroundsResponse, null); + Set(Static.SeasonalBackgrounds, null); } } @@ -22,6 +22,6 @@ namespace osu.Game.Configuration { LoginOverlayDisplayed, MutedAudioNotificationShownOnce, - SeasonalBackgroundsResponse, + SeasonalBackgrounds, } } diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index 2963d57a97..1c38e67451 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -18,20 +18,20 @@ namespace osu.Game.Graphics.Backgrounds [LongRunningLoad] public class SeasonalBackgroundLoader : Component { - private Bindable cachedResponse; + private Bindable seasonalBackgrounds; private int current; [BackgroundDependencyLoader] private void load(SessionStatics sessionStatics, IAPIProvider api) { - cachedResponse = sessionStatics.GetBindable(Static.SeasonalBackgroundsResponse); + seasonalBackgrounds = sessionStatics.GetBindable(Static.SeasonalBackgrounds); - if (cachedResponse.Value != null) return; + if (seasonalBackgrounds.Value != null) return; var request = new GetSeasonalBackgroundsRequest(); request.Success += response => { - cachedResponse.Value = response; + seasonalBackgrounds.Value = response; current = RNG.Next(0, response.Backgrounds?.Count ?? 0); }; @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.Backgrounds public SeasonalBackground LoadBackground() { - var backgrounds = cachedResponse.Value.Backgrounds; + var backgrounds = seasonalBackgrounds.Value.Backgrounds; if (backgrounds == null || !backgrounds.Any()) return null; current = (current + 1) % backgrounds.Count; @@ -49,7 +49,7 @@ namespace osu.Game.Graphics.Backgrounds return new SeasonalBackground(url); } - public bool IsInSeason => cachedResponse.Value != null && DateTimeOffset.Now < cachedResponse.Value.EndDate; + public bool IsInSeason => seasonalBackgrounds.Value != null && DateTimeOffset.Now < seasonalBackgrounds.Value.EndDate; } [LongRunningLoad] From aeab2be5d1968c7a1a6a712afc2897033891bb6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Oct 2020 19:56:52 +0100 Subject: [PATCH 4302/6909] Add xmldoc to SeasonalBackgroundMode --- osu.Game/Configuration/SeasonalBackgroundMode.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Configuration/SeasonalBackgroundMode.cs b/osu.Game/Configuration/SeasonalBackgroundMode.cs index 406736b2a4..6ef835ce5f 100644 --- a/osu.Game/Configuration/SeasonalBackgroundMode.cs +++ b/osu.Game/Configuration/SeasonalBackgroundMode.cs @@ -5,8 +5,19 @@ namespace osu.Game.Configuration { public enum SeasonalBackgroundMode { + /// + /// Seasonal backgrounds are shown regardless of season, if at all available. + /// Always, + + /// + /// Seasonal backgrounds are shown only during their corresponding season. + /// Sometimes, + + /// + /// Seasonal backgrounds are never shown. + /// Never } } From cf0e8e0a620faaa77ba490ab3e71b9901eabd658 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Oct 2020 19:59:52 +0100 Subject: [PATCH 4303/6909] Document nullability of seasonal backgrounds --- osu.Game/Configuration/SessionStatics.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index c470058ae8..03bc434aac 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -22,6 +22,11 @@ namespace osu.Game.Configuration { LoginOverlayDisplayed, MutedAudioNotificationShownOnce, + + /// + /// Info about seasonal backgrounds available fetched from API - see . + /// Value under this lookup can be null if there are no backgrounds available (or API is not reachable). + /// SeasonalBackgrounds, } } From 67a325f47dd608f625711a4e38b8968ee514716e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Oct 2020 20:32:14 +0100 Subject: [PATCH 4304/6909] Move config setting logic to background loader --- .../Backgrounds/SeasonalBackgroundLoader.cs | 17 +++++++++++++---- .../Backgrounds/BackgroundScreenDefault.cs | 14 +++++--------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index 1c38e67451..a9b9929c79 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -18,12 +18,14 @@ namespace osu.Game.Graphics.Backgrounds [LongRunningLoad] public class SeasonalBackgroundLoader : Component { + private Bindable seasonalBackgroundMode; private Bindable seasonalBackgrounds; private int current; [BackgroundDependencyLoader] - private void load(SessionStatics sessionStatics, IAPIProvider api) + private void load(OsuConfigManager config, SessionStatics sessionStatics, IAPIProvider api) { + seasonalBackgroundMode = config.GetBindable(OsuSetting.SeasonalBackgroundMode); seasonalBackgrounds = sessionStatics.GetBindable(Static.SeasonalBackgrounds); if (seasonalBackgrounds.Value != null) return; @@ -38,10 +40,17 @@ namespace osu.Game.Graphics.Backgrounds api.PerformAsync(request); } - public SeasonalBackground LoadBackground() + public SeasonalBackground LoadNextBackground() { + if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Never + || (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && !isInSeason)) + { + return null; + } + var backgrounds = seasonalBackgrounds.Value.Backgrounds; - if (backgrounds == null || !backgrounds.Any()) return null; + if (backgrounds == null || !backgrounds.Any()) + return null; current = (current + 1) % backgrounds.Count; string url = backgrounds[current].Url; @@ -49,7 +58,7 @@ namespace osu.Game.Graphics.Backgrounds return new SeasonalBackground(url); } - public bool IsInSeason => seasonalBackgrounds.Value != null && DateTimeOffset.Now < seasonalBackgrounds.Value.EndDate; + private bool isInSeason => seasonalBackgrounds.Value != null && DateTimeOffset.Now < seasonalBackgrounds.Value.EndDate; } [LongRunningLoad] diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 45374e1223..cbe0841537 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -79,16 +79,12 @@ namespace osu.Game.Screens.Backgrounds Background newBackground; string backgroundName; - if (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Always - || (seasonalBackgroundMode.Value == SeasonalBackgroundMode.Sometimes && seasonalBackgroundLoader.IsInSeason)) - { - var seasonalBackground = seasonalBackgroundLoader.LoadBackground(); + var seasonalBackground = seasonalBackgroundLoader.LoadNextBackground(); - if (seasonalBackground != null) - { - seasonalBackground.Depth = currentDisplay; - return seasonalBackground; - } + if (seasonalBackground != null) + { + seasonalBackground.Depth = currentDisplay; + return seasonalBackground; } switch (introSequence.Value) From 8632f0d77f18b6fb65aee838bced622aba6bdc84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Oct 2020 21:24:20 +0100 Subject: [PATCH 4305/6909] Add test coverage --- .../TestSceneSeasonalBackgroundLoader.cs | 200 ++++++++++++++++++ 1 file changed, 200 insertions(+) create mode 100644 osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs new file mode 100644 index 0000000000..8f5990aeb1 --- /dev/null +++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs @@ -0,0 +1,200 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; +using osu.Game.Configuration; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; + +namespace osu.Game.Tests.Visual.Background +{ + public class TestSceneSeasonalBackgroundLoader : ScreenTestScene + { + [Resolved] + private OsuConfigManager config { get; set; } + + [Resolved] + private SessionStatics statics { get; set; } + + [Cached(typeof(LargeTextureStore))] + private LookupLoggingTextureStore textureStore = new LookupLoggingTextureStore(); + + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + private SeasonalBackgroundLoader backgroundLoader; + private Container backgroundContainer; + + // in real usages these would be online URLs, but correct execution of this test + // shouldn't be coupled to existence of online assets. + private static readonly List seasonal_background_urls = new List + { + "Backgrounds/bg2", + "Backgrounds/bg4", + "Backgrounds/bg3" + }; + + [BackgroundDependencyLoader] + private void load(LargeTextureStore wrappedStore) + { + textureStore.AddStore(wrappedStore); + + Add(backgroundContainer = new Container + { + RelativeSizeAxes = Axes.Both + }); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + // reset API response in statics to avoid test crosstalk. + statics.Set(Static.SeasonalBackgrounds, null); + textureStore.PerformedLookups.Clear(); + dummyAPI.SetState(APIState.Online); + + backgroundContainer.Clear(); + }); + + [TestCase(-5)] + [TestCase(5)] + public void TestAlwaysSeasonal(int daysOffset) + { + registerBackgroundsResponse(DateTimeOffset.Now.AddDays(daysOffset)); + setSeasonalBackgroundMode(SeasonalBackgroundMode.Always); + + createLoader(); + + for (int i = 0; i < 4; ++i) + loadNextBackground(); + + AddAssert("all backgrounds cycled", () => new HashSet(textureStore.PerformedLookups).SetEquals(seasonal_background_urls)); + } + + [TestCase(-5)] + [TestCase(5)] + public void TestNeverSeasonal(int daysOffset) + { + registerBackgroundsResponse(DateTimeOffset.Now.AddDays(daysOffset)); + setSeasonalBackgroundMode(SeasonalBackgroundMode.Never); + + createLoader(); + + assertNoBackgrounds(); + } + + [Test] + public void TestSometimesInSeason() + { + registerBackgroundsResponse(DateTimeOffset.Now.AddDays(5)); + setSeasonalBackgroundMode(SeasonalBackgroundMode.Sometimes); + + createLoader(); + + assertAnyBackground(); + } + + [Test] + public void TestSometimesOutOfSeason() + { + registerBackgroundsResponse(DateTimeOffset.Now.AddDays(-10)); + setSeasonalBackgroundMode(SeasonalBackgroundMode.Sometimes); + + createLoader(); + + assertNoBackgrounds(); + } + + [Test] + public void TestDelayedConnectivity() + { + registerBackgroundsResponse(DateTimeOffset.Now.AddDays(30)); + setSeasonalBackgroundMode(SeasonalBackgroundMode.Always); + AddStep("go offline", () => dummyAPI.SetState(APIState.Offline)); + + createLoader(); + assertNoBackgrounds(); + + AddStep("go online", () => dummyAPI.SetState(APIState.Online)); + + assertAnyBackground(); + } + + private void registerBackgroundsResponse(DateTimeOffset endDate) + => AddStep("setup request handler", () => + { + dummyAPI.HandleRequest = request => + { + if (dummyAPI.State.Value != APIState.Online || !(request is GetSeasonalBackgroundsRequest backgroundsRequest)) + return; + + backgroundsRequest.TriggerSuccess(new APISeasonalBackgrounds + { + Backgrounds = seasonal_background_urls.Select(url => new APISeasonalBackground { Url = url }).ToList(), + EndDate = endDate + }); + }; + }); + + private void setSeasonalBackgroundMode(SeasonalBackgroundMode mode) + => AddStep($"set seasonal mode to {mode}", () => config.Set(OsuSetting.SeasonalBackgroundMode, mode)); + + private void createLoader() + { + AddStep("create loader", () => + { + if (backgroundLoader != null) + Remove(backgroundLoader); + + LoadComponentAsync(backgroundLoader = new SeasonalBackgroundLoader(), Add); + }); + + AddUntilStep("wait for loaded", () => backgroundLoader.IsLoaded); + } + + private void loadNextBackground() + { + SeasonalBackground background = null; + + AddStep("create next background", () => + { + background = backgroundLoader.LoadNextBackground(); + LoadComponentAsync(background, bg => backgroundContainer.Child = bg); + }); + + AddUntilStep("background loaded", () => background.IsLoaded); + } + + private void assertAnyBackground() + { + loadNextBackground(); + AddAssert("background looked up", () => textureStore.PerformedLookups.Any()); + } + + private void assertNoBackgrounds() + { + AddAssert("no background available", () => backgroundLoader.LoadNextBackground() == null); + AddAssert("no lookups performed", () => !textureStore.PerformedLookups.Any()); + } + + private class LookupLoggingTextureStore : LargeTextureStore + { + public List PerformedLookups { get; } = new List(); + + public override Texture Get(string name, WrapMode wrapModeS, WrapMode wrapModeT) + { + PerformedLookups.Add(name); + return base.Get(name, wrapModeS, wrapModeT); + } + } + } +} From 29ad09990259245b8b828a49add4411a90d5f88a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Oct 2020 21:49:14 +0100 Subject: [PATCH 4306/6909] Allow to fetch if going online after launch --- .../Backgrounds/SeasonalBackgroundLoader.cs | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index a9b9929c79..ff290dd99e 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -18,17 +18,29 @@ namespace osu.Game.Graphics.Backgrounds [LongRunningLoad] public class SeasonalBackgroundLoader : Component { + [Resolved] + private IAPIProvider api { get; set; } + + private readonly IBindable apiState = new Bindable(); private Bindable seasonalBackgroundMode; private Bindable seasonalBackgrounds; + private int current; [BackgroundDependencyLoader] - private void load(OsuConfigManager config, SessionStatics sessionStatics, IAPIProvider api) + private void load(OsuConfigManager config, SessionStatics sessionStatics) { seasonalBackgroundMode = config.GetBindable(OsuSetting.SeasonalBackgroundMode); seasonalBackgrounds = sessionStatics.GetBindable(Static.SeasonalBackgrounds); - if (seasonalBackgrounds.Value != null) return; + apiState.BindTo(api.State); + apiState.BindValueChanged(fetchSeasonalBackgrounds, true); + } + + private void fetchSeasonalBackgrounds(ValueChangedEvent stateChanged) + { + if (seasonalBackgrounds.Value != null || stateChanged.NewValue != APIState.Online) + return; var request = new GetSeasonalBackgroundsRequest(); request.Success += response => @@ -48,7 +60,7 @@ namespace osu.Game.Graphics.Backgrounds return null; } - var backgrounds = seasonalBackgrounds.Value.Backgrounds; + var backgrounds = seasonalBackgrounds.Value?.Backgrounds; if (backgrounds == null || !backgrounds.Any()) return null; From 38cf90a69b8734343b68fce3646b618f4d3ad6a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Oct 2020 22:03:26 +0100 Subject: [PATCH 4307/6909] Change background to seasonal right after login --- .../Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 9 +++++++++ osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 4 +--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index ff290dd99e..b439d98083 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -18,6 +18,12 @@ namespace osu.Game.Graphics.Backgrounds [LongRunningLoad] public class SeasonalBackgroundLoader : Component { + /// + /// Fired when background change should be changed due to receiving backgrounds from API + /// or when the user setting is changed (as it might require unloading the seasonal background). + /// + public event Action SeasonalBackgroundChanged; + [Resolved] private IAPIProvider api { get; set; } @@ -31,7 +37,10 @@ namespace osu.Game.Graphics.Backgrounds private void load(OsuConfigManager config, SessionStatics sessionStatics) { seasonalBackgroundMode = config.GetBindable(OsuSetting.SeasonalBackgroundMode); + seasonalBackgroundMode.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke()); + seasonalBackgrounds = sessionStatics.GetBindable(Static.SeasonalBackgrounds); + seasonalBackgrounds.BindValueChanged(_ => SeasonalBackgroundChanged?.Invoke()); apiState.BindTo(api.State); apiState.BindValueChanged(fetchSeasonalBackgrounds, true); diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index cbe0841537..f392386bf9 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -25,7 +25,6 @@ namespace osu.Game.Screens.Backgrounds private Bindable skin; private Bindable mode; private Bindable introSequence; - private Bindable seasonalBackgroundMode; private readonly SeasonalBackgroundLoader seasonalBackgroundLoader = new SeasonalBackgroundLoader(); [Resolved] @@ -43,14 +42,13 @@ namespace osu.Game.Screens.Backgrounds skin = skinManager.CurrentSkin.GetBoundCopy(); mode = config.GetBindable(OsuSetting.MenuBackgroundSource); introSequence = config.GetBindable(OsuSetting.IntroSequence); - seasonalBackgroundMode = config.GetBindable(OsuSetting.SeasonalBackgroundMode); user.ValueChanged += _ => Next(); skin.ValueChanged += _ => Next(); mode.ValueChanged += _ => Next(); beatmap.ValueChanged += _ => Next(); introSequence.ValueChanged += _ => Next(); - seasonalBackgroundMode.ValueChanged += _ => Next(); + seasonalBackgroundLoader.SeasonalBackgroundChanged += Next; currentDisplay = RNG.Next(0, background_count); From 391dd73843b52fcf559b6b23eb56171c89e7c37a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Oct 2020 22:39:34 +0100 Subject: [PATCH 4308/6909] Fix typo in comment --- osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index b439d98083..03b7300011 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -19,7 +19,7 @@ namespace osu.Game.Graphics.Backgrounds public class SeasonalBackgroundLoader : Component { /// - /// Fired when background change should be changed due to receiving backgrounds from API + /// Fired when background should be changed due to receiving backgrounds from API /// or when the user setting is changed (as it might require unloading the seasonal background). /// public event Action SeasonalBackgroundChanged; From 78842ab95ae90862c704ee3c350cd08b8c6126e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 30 Oct 2020 22:40:24 +0100 Subject: [PATCH 4309/6909] Improve look & behaviour of background transitions --- .../Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 2 ++ osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index 03b7300011..99f3a8a6e8 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -97,6 +97,8 @@ namespace osu.Game.Graphics.Backgrounds private void load(LargeTextureStore textures) { Sprite.Texture = textures.Get(url) ?? textures.Get(fallback_texture_name); + // ensure we're not loading in without a transition. + this.FadeInFromZero(200, Easing.InOutSine); } } } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index f392386bf9..3f6210310f 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.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; @@ -52,7 +53,8 @@ namespace osu.Game.Screens.Backgrounds currentDisplay = RNG.Next(0, background_count); - LoadComponentAsync(seasonalBackgroundLoader, _ => LoadComponentAsync(createBackground(), display)); + LoadComponentAsync(seasonalBackgroundLoader); + Next(); } private void display(Background newBackground) @@ -65,11 +67,14 @@ namespace osu.Game.Screens.Backgrounds } private ScheduledDelegate nextTask; + private CancellationTokenSource cancellationTokenSource; public void Next() { nextTask?.Cancel(); - nextTask = Scheduler.AddDelayed(() => { LoadComponentAsync(createBackground(), display); }, 100); + cancellationTokenSource?.Cancel(); + cancellationTokenSource = new CancellationTokenSource(); + nextTask = Scheduler.AddDelayed(() => LoadComponentAsync(createBackground(), display, cancellationTokenSource.Token), 100); } private Background createBackground() From 6a293dd536a9444a52ffd3de7c4992256e04bf64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 31 Oct 2020 18:56:30 +0900 Subject: [PATCH 4310/6909] Add missing ctor parameters back --- osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs | 2 +- osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs index 1e87893f39..2af15923a0 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneAccuracyCircle.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Ranking } } }, - new AccuracyCircle(score) + new AccuracyCircle(score, true) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index cb4560802b..711763330c 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -120,7 +120,7 @@ namespace osu.Game.Screens.Ranking.Expanded Margin = new MarginPadding { Top = 40 }, RelativeSizeAxes = Axes.X, Height = 230, - Child = new AccuracyCircle(score) + Child = new AccuracyCircle(score, withFlair) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From 129b1bc6d3dfcebb51e4e30a7f40d34b6dea9807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 31 Oct 2020 11:35:25 +0100 Subject: [PATCH 4311/6909] Delete all selected objects if shift-clicked on one --- .../Edit/Compose/Components/SelectionHandler.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 01e23bafc5..92c75eae4f 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -226,13 +226,21 @@ namespace osu.Game.Screens.Edit.Compose.Components internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) { if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) - EditorBeatmap.Remove(blueprint.HitObject); + handleQuickDeletion(blueprint); else if (state.Keyboard.ControlPressed && state.Mouse.IsPressed(MouseButton.Left)) blueprint.ToggleSelection(); else ensureSelected(blueprint); } + private void handleQuickDeletion(SelectionBlueprint blueprint) + { + if (!blueprint.IsSelected) + EditorBeatmap.Remove(blueprint.HitObject); + else + deleteSelected(); + } + private void ensureSelected(SelectionBlueprint blueprint) { if (blueprint.IsSelected) From 003994ab7518cf821204a5ba417c9b9bb4c35ac0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 31 Oct 2020 12:21:07 +0100 Subject: [PATCH 4312/6909] Bind UpdateVisibility() directly to source of truth --- .../Edit/Compose/Components/BlueprintContainer.cs | 9 +-------- .../Edit/Compose/Components/SelectionHandler.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 5ac360d029..fa98358dbe 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -210,10 +210,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } if (DragBox.State == Visibility.Visible) - { DragBox.Hide(); - SelectionHandler.UpdateVisibility(); - } } protected override bool OnKeyDown(KeyDownEvent e) @@ -352,11 +349,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Selects all s. /// - private void selectAll() - { - SelectionBlueprints.ToList().ForEach(m => m.Select()); - SelectionHandler.UpdateVisibility(); - } + private void selectAll() => SelectionBlueprints.ToList().ForEach(m => m.Select()); /// /// Deselects all selected s. diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 01e23bafc5..07ae283667 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -201,8 +201,6 @@ namespace osu.Game.Screens.Edit.Compose.Components // there are potentially multiple SelectionHandlers active, but we only want to add hitobjects to the selected list once. if (!EditorBeatmap.SelectedHitObjects.Contains(blueprint.HitObject)) EditorBeatmap.SelectedHitObjects.Add(blueprint.HitObject); - - UpdateVisibility(); } /// @@ -214,8 +212,6 @@ namespace osu.Game.Screens.Edit.Compose.Components selectedBlueprints.Remove(blueprint); EditorBeatmap.SelectedHitObjects.Remove(blueprint.HitObject); - - UpdateVisibility(); } /// @@ -254,7 +250,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Updates whether this is visible. /// - internal void UpdateVisibility() + private void updateVisibility() { int count = selectedBlueprints.Count; @@ -421,7 +417,11 @@ namespace osu.Game.Screens.Edit.Compose.Components // bring in updates from selection changes EditorBeatmap.HitObjectUpdated += _ => UpdateTernaryStates(); - EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => UpdateTernaryStates(); + EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => + { + updateVisibility(); + UpdateTernaryStates(); + }; } /// From 3322b8a7ea03d97e3c18acf58965d0c7798d4ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 31 Oct 2020 12:25:02 +0100 Subject: [PATCH 4313/6909] Run OnSelectionChanged() on each change --- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 07ae283667..0547b15e3d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -257,16 +257,15 @@ namespace osu.Game.Screens.Edit.Compose.Components selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; if (count > 0) - { Show(); - OnSelectionChanged(); - } else Hide(); + + OnSelectionChanged(); } /// - /// Triggered whenever more than one object is selected, on each change. + /// Triggered whenever the set of selected objects changes. /// Should update the selection box's state to match supported operations. /// protected virtual void OnSelectionChanged() From d74c19e2d703f6e57139727692a3473ea7bd55fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 31 Oct 2020 12:28:35 +0100 Subject: [PATCH 4314/6909] Shorten show/hide code --- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 0547b15e3d..41098cc84c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -256,11 +256,7 @@ namespace osu.Game.Screens.Edit.Compose.Components selectionDetailsText.Text = count > 0 ? count.ToString() : string.Empty; - if (count > 0) - Show(); - else - Hide(); - + this.FadeTo(count > 0 ? 1 : 0); OnSelectionChanged(); } From 007c27d3ffa987afdfc5c3502c27d4a89f8538fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 31 Oct 2020 14:45:11 +0100 Subject: [PATCH 4315/6909] Schedule visibility update once per frame --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 41098cc84c..5c1b41d848 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -414,7 +414,7 @@ namespace osu.Game.Screens.Edit.Compose.Components EditorBeatmap.HitObjectUpdated += _ => UpdateTernaryStates(); EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => { - updateVisibility(); + Scheduler.AddOnce(updateVisibility); UpdateTernaryStates(); }; } From a9a3489e92b200d99c335cd52aab2c93e4cf3a17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 31 Oct 2020 22:51:31 +0900 Subject: [PATCH 4316/6909] Fix potential null reference when loading background As seen in https://discordapp.com/channels/188630481301012481/188630652340404224/772094427342569493. Caused due to async load of the loader, which means it may not be ready before Next() is called. --- osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs | 1 - osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs index 99f3a8a6e8..a48da37804 100644 --- a/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs +++ b/osu.Game/Graphics/Backgrounds/SeasonalBackgroundLoader.cs @@ -15,7 +15,6 @@ using osu.Game.Online.API.Requests.Responses; namespace osu.Game.Graphics.Backgrounds { - [LongRunningLoad] public class SeasonalBackgroundLoader : Component { /// diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 3f6210310f..8beb955824 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -44,6 +44,8 @@ namespace osu.Game.Screens.Backgrounds mode = config.GetBindable(OsuSetting.MenuBackgroundSource); introSequence = config.GetBindable(OsuSetting.IntroSequence); + AddInternal(seasonalBackgroundLoader); + user.ValueChanged += _ => Next(); skin.ValueChanged += _ => Next(); mode.ValueChanged += _ => Next(); @@ -53,7 +55,6 @@ namespace osu.Game.Screens.Backgrounds currentDisplay = RNG.Next(0, background_count); - LoadComponentAsync(seasonalBackgroundLoader); Next(); } From 941e8525af5c1b26b5aadc6ffd52554b53f2cf4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 31 Oct 2020 16:06:53 +0100 Subject: [PATCH 4317/6909] Add flag parameter to allow non-user-pause via music controller --- osu.Game/Overlays/MusicController.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 12caf98021..7e7be31de6 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -182,9 +182,14 @@ namespace osu.Game.Overlays /// /// Stop playing the current track and pause at the current position. /// - public void Stop() + /// + /// Whether the request to stop was issued by the user rather than internally. + /// Specifying true will ensure that other methods like + /// will not resume music playback until the next explicit call to . + /// + public void Stop(bool requestedByUser = true) { - IsUserPaused = true; + IsUserPaused |= requestedByUser; if (CurrentTrack.IsRunning) CurrentTrack.Stop(); } From 19023e7d437037172dee9aafc1b252af02108e3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 31 Oct 2020 16:08:15 +0100 Subject: [PATCH 4318/6909] Fix player restart invoking user-level pause --- 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 3c0c643413..4427bb02e2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -472,7 +472,7 @@ namespace osu.Game.Screens.Play { // at the point of restarting the track should either already be paused or the volume should be zero. // stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader. - musicController.Stop(); + musicController.Stop(false); sampleRestart?.Play(); RestartRequested?.Invoke(); From 79f47953a800bcdd35951785ff266919dd39bf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 31 Oct 2020 16:08:27 +0100 Subject: [PATCH 4319/6909] Migrate existing call to new flag parameter --- 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 e32ed07863..1c9bdd43ab 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual rulesetDependencies?.Dispose(); if (MusicController?.TrackLoaded == true) - MusicController.CurrentTrack.Stop(); + MusicController.Stop(false); if (contextFactory?.IsValueCreated == true) contextFactory.Value.ResetDatabase(); From 2065680e9d1d12183c5493dfc639fff9b74ee97c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 31 Oct 2020 17:01:45 +0100 Subject: [PATCH 4320/6909] Simplify test case --- .../Background/TestSceneSeasonalBackgroundLoader.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs index 8f5990aeb1..fba0d92d4b 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneSeasonalBackgroundLoader.cs @@ -149,18 +149,14 @@ namespace osu.Game.Tests.Visual.Background => AddStep($"set seasonal mode to {mode}", () => config.Set(OsuSetting.SeasonalBackgroundMode, mode)); private void createLoader() - { - AddStep("create loader", () => + => AddStep("create loader", () => { if (backgroundLoader != null) Remove(backgroundLoader); - LoadComponentAsync(backgroundLoader = new SeasonalBackgroundLoader(), Add); + Add(backgroundLoader = new SeasonalBackgroundLoader()); }); - AddUntilStep("wait for loaded", () => backgroundLoader.IsLoaded); - } - private void loadNextBackground() { SeasonalBackground background = null; From 6bfff436348cb48de3a2809d4b63b29fe8e0bce1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 1 Nov 2020 13:25:36 +0100 Subject: [PATCH 4321/6909] Extract StatisticCounter to a separate class and use it instead. --- .../Expanded/Statistics/CounterStatistic.cs | 20 +++------------ .../Statistics/PerformanceStatistic.cs | 16 +++++++++--- .../Expanded/Statistics/StatisticCounter.cs | 25 +++++++++++++++++++ 3 files changed, 41 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs index 08a9714fd8..d37f6c5e5f 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/CounterStatistic.cs @@ -6,7 +6,6 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Ranking.Expanded.Accuracy; using osuTK; namespace osu.Game.Screens.Ranking.Expanded.Statistics @@ -19,7 +18,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private readonly int count; private readonly int? maxCount; - protected RollingCounter Counter { get; private set; } + private RollingCounter counter; /// /// Creates a new . @@ -37,7 +36,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public override void Appear() { base.Appear(); - Counter.Current.Value = count; + counter.Current.Value = count; } protected override Drawable CreateContent() @@ -46,7 +45,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Child = Counter = new StatisticCounter + Child = counter = new StatisticCounter { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre @@ -67,18 +66,5 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics return container; } - - private class StatisticCounter : RollingCounter - { - protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; - - protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; - - protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => - { - s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); - s.Spacing = new Vector2(-2, 0); - }); - } } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 1b4edb99d7..cd9d8005c6 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -5,11 +5,13 @@ using System; using System.Threading; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; namespace osu.Game.Screens.Ranking.Expanded.Statistics { - public class PerformanceStatistic : CounterStatistic + public class PerformanceStatistic : StatisticDisplay { private readonly ScoreInfo score; @@ -17,8 +19,10 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); + private RollingCounter counter; + public PerformanceStatistic(ScoreInfo score) - : base("PP", 0) + : base("PP") { this.score = score; } @@ -46,7 +50,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics public override void Appear() { base.Appear(); - Counter.Current.BindTo(performance); + counter.Current.BindTo(performance); } protected override void Dispose(bool isDisposing) @@ -54,5 +58,11 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics cancellationTokenSource?.Cancel(); base.Dispose(isDisposing); } + + protected override Drawable CreateContent() => counter = new StatisticCounter + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre + }; } } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.cs new file mode 100644 index 0000000000..bbcfc43dc8 --- /dev/null +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/StatisticCounter.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.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Screens.Ranking.Expanded.Accuracy; +using osuTK; + +namespace osu.Game.Screens.Ranking.Expanded.Statistics +{ + public class StatisticCounter : RollingCounter + { + protected override double RollingDuration => AccuracyCircle.ACCURACY_TRANSFORM_DURATION; + + protected override Easing RollingEasing => AccuracyCircle.ACCURACY_TRANSFORM_EASING; + + protected override OsuSpriteText CreateSpriteText() => base.CreateSpriteText().With(s => + { + s.Font = OsuFont.Torus.With(size: 20, fixedWidth: true); + s.Spacing = new Vector2(-2, 0); + }); + } +} From 8a54fdd4e6427b079f8d1f45205270d1f487b007 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 1 Nov 2020 14:25:33 +0100 Subject: [PATCH 4322/6909] Ensure LoadOszIntoOsu returns actual imported map --- osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 80fbda8e1d..b941313103 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -821,15 +821,13 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - await manager.Import(temp); - - var imported = manager.GetAllUsableBeatmapSets(); + var importedSet = await manager.Import(temp); ensureLoaded(osu); waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); - return imported.LastOrDefault(); + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); } private void deleteBeatmapSet(BeatmapSetInfo imported, OsuGameBase osu) From 5903c3be9066b6fd101abab48f4eebcb7421a50b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 1 Nov 2020 14:39:10 +0100 Subject: [PATCH 4323/6909] Fix inaccurate xmldoc --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 481c94e6f3..cb170ad298 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Spectator public event Action OnUserBeganPlaying; /// - /// Called whenever a user starts a play session. + /// Called whenever a user finishes a play session. /// public event Action OnUserFinishedPlaying; From b7696c85ad5a42c7c35902579765c4c04c79bffc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 1 Nov 2020 15:21:24 +0100 Subject: [PATCH 4324/6909] Add more xmldocs --- osu.Game/Rulesets/UI/IFrameStableClock.cs | 3 +++ osu.Game/Screens/Play/GameplayClockContainer.cs | 8 ++++++++ 2 files changed, 11 insertions(+) diff --git a/osu.Game/Rulesets/UI/IFrameStableClock.cs b/osu.Game/Rulesets/UI/IFrameStableClock.cs index c1d8733d26..569ef5e06c 100644 --- a/osu.Game/Rulesets/UI/IFrameStableClock.cs +++ b/osu.Game/Rulesets/UI/IFrameStableClock.cs @@ -10,6 +10,9 @@ namespace osu.Game.Rulesets.UI { IBindable IsCatchingUp { get; } + /// + /// Whether the frame stable clock is waiting on new frames to arrive to be able to progress time. + /// IBindable WaitingOnFrames { get; } } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 6154ec67b8..2c83161614 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -63,6 +63,14 @@ namespace osu.Game.Screens.Play private readonly FramedOffsetClock platformOffsetClock; + /// + /// Creates a new . + /// + /// The beatmap being played. + /// The suggested time to start gameplay at. + /// + /// Whether should be used regardless of when storyboard events and hitobjects are supposed to start. + /// public GameplayClockContainer(WorkingBeatmap beatmap, double gameplayStartTime, bool startAtGameplayStart = false) { this.beatmap = beatmap; From 716458344fa374a7b83cbeb64b448d4dd046961d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 1 Nov 2020 16:03:28 +0100 Subject: [PATCH 4325/6909] Ensure spectator player is unsubscribed to prevent leak --- osu.Game/Screens/Play/SpectatorPlayer.cs | 26 ++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index fbd21b32ba..6c1e83f236 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -26,12 +26,6 @@ namespace osu.Game.Screens.Play spectatorStreaming.OnUserBeganPlaying += userBeganPlaying; } - private void userBeganPlaying(int userId, SpectatorState state) - { - if (userId == Score.ScoreInfo.UserID) - Schedule(this.Exit); - } - protected override GameplayClockContainer CreateGameplayClockContainer(WorkingBeatmap beatmap, double gameplayStart) { // if we already have frames, start gameplay at the point in time they exist, should they be too far into the beatmap. @@ -42,5 +36,25 @@ namespace osu.Game.Screens.Play return new GameplayClockContainer(beatmap, firstFrameTime.Value, true); } + + public override bool OnExiting(IScreen next) + { + spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; + return base.OnExiting(next); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (spectatorStreaming != null) + spectatorStreaming.OnUserBeganPlaying -= userBeganPlaying; + } + + private void userBeganPlaying(int userId, SpectatorState state) + { + if (userId == Score.ScoreInfo.UserID) + Schedule(this.Exit); + } } } From 6ff13e399ad66d7bf898630efafb8851e625c688 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2020 17:14:13 +0000 Subject: [PATCH 4326/6909] Bump Microsoft.CodeAnalysis.FxCopAnalyzers from 3.0.0 to 3.3.1 Bumps [Microsoft.CodeAnalysis.FxCopAnalyzers](https://github.com/dotnet/roslyn-analyzers) from 3.0.0 to 3.3.1. - [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/v3.0.0...v3.3.1) 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 2d3478f256..186b2049c6 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ - + $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset From 6e9ed76251ea86fbc6f216953d964ae0ccc61f62 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2020 17:14:57 +0000 Subject: [PATCH 4327/6909] Bump Microsoft.Build.Traversal from 2.1.1 to 2.2.3 Bumps [Microsoft.Build.Traversal](https://github.com/Microsoft/MSBuildSdks) from 2.1.1 to 2.2.3. - [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.1.1...Microsoft.Build.Traversal.2.2.3) 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 a9a531f59c..10b61047ac 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.1.1" + "Microsoft.Build.Traversal": "2.2.3" } } \ No newline at end of file From 79e610d31b9017e9d13e82fd704a32fc72c5e768 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sun, 1 Nov 2020 17:15:23 +0000 Subject: [PATCH 4328/6909] Bump Microsoft.CodeAnalysis.BannedApiAnalyzers from 3.3.0 to 3.3.1 Bumps [Microsoft.CodeAnalysis.BannedApiAnalyzers](https://github.com/dotnet/roslyn-analyzers) from 3.3.0 to 3.3.1. - [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/v3.3.0...v3.3.1) 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 2d3478f256..056216f14b 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,7 +16,7 @@ - + From 2b0bea535efa58df11a2ac216742a2a8521e3e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 1 Nov 2020 18:47:40 +0100 Subject: [PATCH 4329/6909] Resolve CA1805 inspections "Member is explicitly initialized to its default value" --- osu.Game/Database/DatabaseWriteUsage.cs | 2 +- osu.Game/Graphics/Containers/OsuScrollContainer.cs | 2 +- osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs | 2 +- osu.Game/Tests/Visual/PlayerTestScene.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs index 1fd2f23d50..ddafd77066 100644 --- a/osu.Game/Database/DatabaseWriteUsage.cs +++ b/osu.Game/Database/DatabaseWriteUsage.cs @@ -26,7 +26,7 @@ namespace osu.Game.Database /// Whether this write usage will commit a transaction on completion. /// If false, there is a parent usage responsible for transaction commit. /// - public bool IsTransactionLeader = false; + public bool IsTransactionLeader; protected void Dispose(bool disposing) { diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index ed5c73bee6..b9122d254d 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Graphics.Containers /// Allows controlling the scroll bar from any position in the container using the right mouse button. /// Uses the value of to smoothly scroll to the dragged location. /// - public bool RightMouseScrollbar = false; + public bool RightMouseScrollbar; /// /// Controls the rate with which the target position is approached when performing a relative drag. Default is 0.02. diff --git a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs index cf5c88b8fd..b671f4c68c 100644 --- a/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs +++ b/osu.Game/Rulesets/Replays/FramedReplayInputHandler.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Replays /// When set, we will ensure frames executed by nested drawables are frame-accurate to replay data. /// Disabling this can make replay playback smoother (useful for autoplay, currently). /// - public bool FrameAccuratePlayback = false; + public bool FrameAccuratePlayback; protected bool HasFrames => Frames.Count > 0; diff --git a/osu.Game/Tests/Visual/PlayerTestScene.cs b/osu.Game/Tests/Visual/PlayerTestScene.cs index aa3bd2e4b7..088e997de9 100644 --- a/osu.Game/Tests/Visual/PlayerTestScene.cs +++ b/osu.Game/Tests/Visual/PlayerTestScene.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual /// /// Whether custom test steps are provided. Custom tests should invoke to create the test steps. /// - protected virtual bool HasCustomSteps { get; } = false; + protected virtual bool HasCustomSteps => false; protected TestPlayer Player; From ca5de22ca5d8047424958eab6975b17056917055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 1 Nov 2020 18:49:11 +0100 Subject: [PATCH 4330/6909] Resolve CA1834 inspection "Use `StringBuilder.Append(char)` instead of `StringBuilder.Append(string)` when the input is a constant unit string" --- 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 80a4d6dea4..80fd6c22bb 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -417,7 +417,7 @@ namespace osu.Game.Beatmaps.Formats string sampleFilename = samples.FirstOrDefault(s => string.IsNullOrEmpty(s.Name))?.LookupNames.First() ?? string.Empty; int volume = samples.FirstOrDefault()?.Volume ?? 100; - sb.Append(":"); + sb.Append(':'); sb.Append(FormattableString.Invariant($"{customSampleBank}:")); sb.Append(FormattableString.Invariant($"{volume}:")); sb.Append(FormattableString.Invariant($"{sampleFilename}")); From 89bf7b1bd669d83e57b6f299b0489e8cd950b1ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 1 Nov 2020 18:51:39 +0100 Subject: [PATCH 4331/6909] Resolve CA1835 inspection "Change the `ReadAsync` method call to use the `Stream.ReadAsync(Memory, CancellationToken)` overload" --- osu.Game/IO/Archives/ArchiveReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs index a30f961daf..f74574e60c 100644 --- a/osu.Game/IO/Archives/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -41,7 +41,7 @@ namespace osu.Game.IO.Archives return null; byte[] buffer = new byte[input.Length]; - await input.ReadAsync(buffer, 0, buffer.Length); + await input.ReadAsync(buffer); return buffer; } } From 3090b6ccb5eed7b77868cf508b2cf48832f6d0ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 1 Nov 2020 18:54:44 +0100 Subject: [PATCH 4332/6909] Resolve CA2249 inspections "Use `string.Contains` instead of `string.IndexOf` to improve readability" --- osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs | 2 +- osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs | 2 +- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 2 +- osu.Game/Screens/Select/FilterCriteria.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index e2550d1ca4..8d8ca523d5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -347,7 +347,7 @@ namespace osu.Game.Beatmaps.Formats /// The line which may contains variables. private void decodeVariables(ref string line) { - while (line.IndexOf('$') >= 0) + while (line.Contains('$')) { string origLine = line; diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index 60c6aa1d8a..c7c37cbc0d 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -84,7 +84,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components matchingFilter &= r.Room.Playlist.Count == 0 || r.Room.Playlist.Any(i => i.Ruleset.Value.Equals(criteria.Ruleset)); if (!string.IsNullOrEmpty(criteria.SearchString)) - matchingFilter &= r.FilterTerms.Any(term => term.IndexOf(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase) >= 0); + matchingFilter &= r.FilterTerms.Any(term => term.Contains(criteria.SearchString, StringComparison.InvariantCultureIgnoreCase)); r.MatchingFilter = matchingFilter; } diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index dce4028f17..1aab50037a 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Select.Carousel 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.Contains(criteriaTerm, StringComparison.InvariantCultureIgnoreCase)); // if a match wasn't found via text matching of terms, do a second catch-all check matching against online IDs. // this should be done after text matching so we can prioritise matching numbers in metadata. diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index f34f8f6505..7bddb3e51b 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Select if (string.IsNullOrEmpty(value)) return false; - return value.IndexOf(SearchTerm, StringComparison.InvariantCultureIgnoreCase) >= 0; + return value.Contains(SearchTerm, StringComparison.InvariantCultureIgnoreCase); } public string SearchTerm; From 164370bc7da7ed6dfe08507eec0fbafc32cffd0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 1 Nov 2020 20:51:23 +0100 Subject: [PATCH 4333/6909] Resolve more CA1805 inspections --- osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs | 7 +++++-- osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs | 4 ++-- .../Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs | 2 +- .../Visual/SongSelect/TestSceneBeatmapInfoWedge.cs | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs index 937473e824..6841ecd23c 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModBlinds.cs @@ -64,8 +64,8 @@ namespace osu.Game.Rulesets.Osu.Mods /// private const float target_clamp = 1; - private readonly float targetBreakMultiplier = 0; - private readonly float easing = 1; + private readonly float targetBreakMultiplier; + private readonly float easing; private readonly CompositeDrawable restrictTo; @@ -86,6 +86,9 @@ namespace osu.Game.Rulesets.Osu.Mods { this.restrictTo = restrictTo; this.beatmap = beatmap; + + targetBreakMultiplier = 0; + easing = 1; } [BackgroundDependencyLoader] diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs index 58cc324233..de46f9d1cf 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectAccentColour.cs @@ -81,8 +81,8 @@ namespace osu.Game.Tests.Gameplay private class TestHitObjectWithCombo : ConvertHitObject, IHasComboInformation { - public bool NewCombo { get; set; } = false; - public int ComboOffset { get; } = 0; + public bool NewCombo { get; set; } + public int ComboOffset => 0; public Bindable IndexInCurrentComboBindable { get; } = new Bindable(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index fdc20dc477..07ff56b5c3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -135,7 +135,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public Bindable InitialRoomsReceived { get; } = new Bindable(true); - public IBindableList Rooms { get; } = null; + public IBindableList Rooms => null; public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index e02ebf3be1..0b2c0ce63b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -197,8 +197,8 @@ namespace osu.Game.Tests.Visual.SongSelect private class TestHitObject : ConvertHitObject, IHasPosition { - public float X { get; } = 0; - public float Y { get; } = 0; + public float X => 0; + public float Y => 0; public Vector2 Position { get; } = Vector2.Zero; } } From 432282e8de8fb62a50fd2fef4de3f77047889e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 1 Nov 2020 21:22:04 +0100 Subject: [PATCH 4334/6909] Use alternative solution to avoid storing last zoom --- .../Timeline/TimelineBlueprintContainer.cs | 58 +++++++++++-------- 1 file changed, 33 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 008da14a21..10913a8bb9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -9,10 +9,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; -using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Edit.Compose.Components.Timeline @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline OnDragHandled = handleScrollViaDrag }; - protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect, this); + protected override DragBox CreateDragBox(Action performSelect) => new TimelineDragBox(performSelect); private void handleScrollViaDrag(DragEvent e) { @@ -137,17 +137,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private class TimelineDragBox : DragBox { - private Vector2 lastMouseDown; + // the following values hold the start and end X positions of the drag box in the timeline's local space, + // but with zoom unapplied in order to be able to compensate for positional changes + // while the timeline is being zoomed in/out. + private float? selectionStart; + private float selectionEnd; - private float? lastZoom; - private float localMouseDown; + [Resolved] + private Timeline timeline { get; set; } - private readonly TimelineBlueprintContainer parent; - - public TimelineDragBox(Action performSelect, TimelineBlueprintContainer parent) + public TimelineDragBox(Action performSelect) : base(performSelect) { - this.parent = parent; } protected override Drawable CreateBox() => new Box @@ -158,27 +159,34 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline public override bool HandleDrag(MouseButtonEvent e) { - // store the original position of the mouse down, as we may be scrolled during selection. - if (lastMouseDown != e.ScreenSpaceMouseDownPosition) - { - lastMouseDown = e.ScreenSpaceMouseDownPosition; - localMouseDown = e.MouseDownPosition.X; - lastZoom = null; - } + selectionStart ??= e.MouseDownPosition.X / timeline.CurrentZoom; - //Zooming the timeline shifts the coordinate system. zoomCorrection compensates for that - float zoomCorrection = lastZoom.HasValue ? (parent.timeline.CurrentZoom / lastZoom.Value) : 1; - localMouseDown *= zoomCorrection; - lastZoom = parent.timeline.CurrentZoom; + // only calculate end when a transition is not in progress to avoid bouncing. + if (Precision.AlmostEquals(timeline.CurrentZoom, timeline.Zoom)) + selectionEnd = e.MousePosition.X / timeline.CurrentZoom; - float selection1 = localMouseDown; - float selection2 = e.MousePosition.X * zoomCorrection; + updateDragBoxPosition(); + return true; + } - Box.X = Math.Min(selection1, selection2); - Box.Width = Math.Abs(selection1 - selection2); + private void updateDragBoxPosition() + { + if (selectionStart == null) + return; + + float rescaledStart = selectionStart.Value * timeline.CurrentZoom; + float rescaledEnd = selectionEnd * timeline.CurrentZoom; + + Box.X = Math.Min(rescaledStart, rescaledEnd); + Box.Width = Math.Abs(rescaledStart - rescaledEnd); PerformSelection?.Invoke(Box.ScreenSpaceDrawQuad.AABBFloat); - return true; + } + + public override void Hide() + { + base.Hide(); + selectionStart = null; } } From 71d55f16f3853f3e9f3cc56bb4cf981d957e9840 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 1 Nov 2020 13:50:38 -0800 Subject: [PATCH 4335/6909] Fix edit beatmap options button not resuming back to song select --- osu.Game/Screens/Select/PlaySongSelect.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 19769f487d..ee8825640c 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -32,11 +32,7 @@ namespace osu.Game.Screens.Select [BackgroundDependencyLoader] private void load(OsuColour colours) { - BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => - { - ValidForResume = false; - Edit(); - }); + BeatmapOptions.AddButton(@"Edit", @"beatmap", FontAwesome.Solid.PencilAlt, colours.Yellow, () => Edit()); ((PlayBeatmapDetailArea)BeatmapDetails).Leaderboard.ScoreSelected += PresentScore; } From bfa6ae1b66eb38c9c77f4f163c3cb0c3b82a98d9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 14:24:17 +0900 Subject: [PATCH 4336/6909] Fix taiko drum not correct handling sample / group point changes Closes https://github.com/ppy/osu/issues/10642 --- .../Audio/DrumSampleContainer.cs | 79 ++++++++++++++----- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs index fcf7c529f5..fd6eca3850 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs @@ -2,6 +2,9 @@ // 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.Game.Audio; using osu.Game.Beatmaps.ControlPoints; @@ -18,9 +21,24 @@ namespace osu.Game.Rulesets.Taiko.Audio private readonly ControlPointInfo controlPoints; private readonly Dictionary mappings = new Dictionary(); + private IBindableList groups; + public DrumSampleContainer(ControlPointInfo controlPoints) { this.controlPoints = controlPoints; + } + + [BackgroundDependencyLoader] + private void load() + { + groups = controlPoints.Groups.GetBoundCopy(); + groups.BindCollectionChanged((_, __) => recreateMappings(), true); + } + + private void recreateMappings() + { + mappings.Clear(); + ClearInternal(); IReadOnlyList samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints; @@ -28,37 +46,56 @@ namespace osu.Game.Rulesets.Taiko.Audio { var samplePoint = samplePoints[i]; - var centre = samplePoint.GetSampleInfo(); - var rim = samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP); - var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue; var lifetimeEnd = i + 1 < samplePoints.Count ? samplePoints[i + 1].Time : double.MaxValue; - mappings[samplePoint.Time] = new DrumSample + AddInternal(mappings[samplePoint.Time] = new DrumSample(samplePoint) { - Centre = addSound(centre, lifetimeStart, lifetimeEnd), - Rim = addSound(rim, lifetimeStart, lifetimeEnd) - }; + LifetimeStart = lifetimeStart, + LifetimeEnd = lifetimeEnd + }); } } - private PausableSkinnableSound addSound(HitSampleInfo hitSampleInfo, double lifetimeStart, double lifetimeEnd) - { - var drawable = new PausableSkinnableSound(hitSampleInfo) - { - LifetimeStart = lifetimeStart, - LifetimeEnd = lifetimeEnd - }; - AddInternal(drawable); - return drawable; - } - public DrumSample SampleAt(double time) => mappings[controlPoints.SamplePointAt(time).Time]; - public class DrumSample + public class DrumSample : CompositeDrawable { - public PausableSkinnableSound Centre; - public PausableSkinnableSound Rim; + public override bool RemoveWhenNotAlive => false; + + public PausableSkinnableSound Centre { get; private set; } + public PausableSkinnableSound Rim { get; private set; } + + private readonly SampleControlPoint samplePoint; + + private Bindable sampleBank; + private BindableNumber sampleVolume; + + public DrumSample(SampleControlPoint samplePoint) + { + this.samplePoint = samplePoint; + } + + [BackgroundDependencyLoader] + private void load() + { + sampleBank = samplePoint.SampleBankBindable.GetBoundCopy(); + sampleBank.BindValueChanged(_ => recreate()); + + sampleVolume = samplePoint.SampleVolumeBindable.GetBoundCopy(); + sampleVolume.BindValueChanged(_ => recreate()); + + recreate(); + } + + private void recreate() + { + InternalChildren = new Drawable[] + { + Centre = new PausableSkinnableSound(samplePoint.GetSampleInfo()), + Rim = new PausableSkinnableSound(samplePoint.GetSampleInfo(HitSampleInfo.HIT_CLAP)) + }; + } } } } From 3adf451e8277a8862e2221cda8000acc37938b8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 14:39:01 +0900 Subject: [PATCH 4337/6909] Handle changes via SamplePoints list for simplicity --- .../Audio/DrumSampleContainer.cs | 17 ++++++++++------- .../Beatmaps/ControlPoints/ControlPointInfo.cs | 4 ++-- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs index fd6eca3850..4a3dc58604 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -21,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Audio private readonly ControlPointInfo controlPoints; private readonly Dictionary mappings = new Dictionary(); - private IBindableList groups; + private IBindableList samplePoints; public DrumSampleContainer(ControlPointInfo controlPoints) { @@ -31,8 +32,8 @@ namespace osu.Game.Rulesets.Taiko.Audio [BackgroundDependencyLoader] private void load() { - groups = controlPoints.Groups.GetBoundCopy(); - groups.BindCollectionChanged((_, __) => recreateMappings(), true); + samplePoints = controlPoints.SamplePoints.GetBoundCopy(); + samplePoints.BindCollectionChanged((_, __) => recreateMappings(), true); } private void recreateMappings() @@ -40,14 +41,16 @@ namespace osu.Game.Rulesets.Taiko.Audio mappings.Clear(); ClearInternal(); - IReadOnlyList samplePoints = controlPoints.SamplePoints.Count == 0 ? new[] { controlPoints.SamplePointAt(double.MinValue) } : controlPoints.SamplePoints; + SampleControlPoint[] points = samplePoints.Count == 0 + ? new[] { controlPoints.SamplePointAt(double.MinValue) } + : samplePoints.ToArray(); - for (int i = 0; i < samplePoints.Count; i++) + for (int i = 0; i < points.Length; i++) { - var samplePoint = samplePoints[i]; + var samplePoint = points[i]; var lifetimeStart = i > 0 ? samplePoint.Time : double.MinValue; - var lifetimeEnd = i + 1 < samplePoints.Count ? samplePoints[i + 1].Time : double.MaxValue; + var lifetimeEnd = i + 1 < points.Length ? points[i + 1].Time : double.MaxValue; AddInternal(mappings[samplePoint.Time] = new DrumSample(samplePoint) { diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index 22314f28c7..b843aad950 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -41,9 +41,9 @@ namespace osu.Game.Beatmaps.ControlPoints /// All sound points. /// [JsonProperty] - public IReadOnlyList SamplePoints => samplePoints; + public IBindableList SamplePoints => samplePoints; - private readonly SortedList samplePoints = new SortedList(Comparer.Default); + private readonly BindableList samplePoints = new BindableList(); /// /// All effect points. From fb105a1e9c23de90db8c9f5783c79bf4c821b4ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 14:49:25 +0900 Subject: [PATCH 4338/6909] Remove unnecessary field storage --- osu.Game/OsuGameBase.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4db057dccc..4bc54e7e83 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -59,8 +59,6 @@ namespace osu.Game protected ScoreManager ScoreManager; - protected ScorePerformanceManager ScorePerformanceManager; - protected BeatmapDifficultyManager DifficultyManager; protected SkinManager SkinManager; @@ -231,8 +229,9 @@ namespace osu.Game dependencies.Cache(DifficultyManager = new BeatmapDifficultyManager()); AddInternal(DifficultyManager); - dependencies.Cache(ScorePerformanceManager = new ScorePerformanceManager()); - AddInternal(ScorePerformanceManager); + var scorePerformanceManager = new ScorePerformanceManager(); + dependencies.Cache(scorePerformanceManager); + AddInternal(scorePerformanceManager); dependencies.Cache(KeyBindingStore = new KeyBindingStore(contextFactory, RulesetStore)); dependencies.Cache(SettingsStore = new SettingsStore(contextFactory)); From d46f7535c949fdd097e66f09860d2a6618770ed9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 14:50:44 +0900 Subject: [PATCH 4339/6909] Add xmldoc for new component --- osu.Game/Scoring/ScorePerformanceManager.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index 746aa67a55..b63443180f 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -12,6 +12,9 @@ using osu.Game.Beatmaps; namespace osu.Game.Scoring { + /// + /// A global component which calculates and caches results of performance calculations for locally databased scores. + /// public class ScorePerformanceManager : Component { private readonly ConcurrentDictionary performanceCache = new ConcurrentDictionary(); From 7b320a991fd45b24d469ceb904e5c7269abfdfdd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 14:51:19 +0900 Subject: [PATCH 4340/6909] Add note about missing expiration logic --- osu.Game/Scoring/ScorePerformanceManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index b63443180f..ddda1b99af 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -17,6 +17,8 @@ namespace osu.Game.Scoring /// public class ScorePerformanceManager : Component { + // this cache will grow indefinitely per session and should be considered temporary. + // this whole component should likely be replaced with database persistence. private readonly ConcurrentDictionary performanceCache = new ConcurrentDictionary(); [Resolved] From d2f6303988b24991d7b88d7cfe3613df185040da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 14:56:50 +0900 Subject: [PATCH 4341/6909] Change default value of requestedByUser to false --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 +- osu.Game/Overlays/MusicController.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Tests/Visual/OsuTestScene.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index c96952431a..5963f806c6 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); if (withUserPause) - AddStep("pause", () => Game.Dependencies.Get().Stop()); + AddStep("pause", () => Game.Dependencies.Get().Stop(true)); AddStep("press enter", () => pressAndRelease(Key.Enter)); diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index 7e7be31de6..eafbeebbc9 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -187,7 +187,7 @@ namespace osu.Game.Overlays /// Specifying true will ensure that other methods like /// will not resume music playback until the next explicit call to . /// - public void Stop(bool requestedByUser = true) + public void Stop(bool requestedByUser = false) { IsUserPaused |= requestedByUser; if (CurrentTrack.IsRunning) @@ -201,7 +201,7 @@ namespace osu.Game.Overlays public bool TogglePause() { if (CurrentTrack.IsRunning) - Stop(); + Stop(true); else Play(); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4427bb02e2..3c0c643413 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -472,7 +472,7 @@ namespace osu.Game.Screens.Play { // at the point of restarting the track should either already be paused or the volume should be zero. // stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader. - musicController.Stop(false); + musicController.Stop(); sampleRestart?.Play(); RestartRequested?.Invoke(); diff --git a/osu.Game/Tests/Visual/OsuTestScene.cs b/osu.Game/Tests/Visual/OsuTestScene.cs index 1c9bdd43ab..198d22fedd 100644 --- a/osu.Game/Tests/Visual/OsuTestScene.cs +++ b/osu.Game/Tests/Visual/OsuTestScene.cs @@ -189,7 +189,7 @@ namespace osu.Game.Tests.Visual rulesetDependencies?.Dispose(); if (MusicController?.TrackLoaded == true) - MusicController.Stop(false); + MusicController.Stop(); if (contextFactory?.IsValueCreated == true) contextFactory.Value.ResetDatabase(); From 8f2cd0e8c55178b75315c912d1c4eeb25e1d5f08 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 15:01:30 +0900 Subject: [PATCH 4342/6909] Add matching requestedByUser parameter to Play method --- osu.Game/Overlays/MusicController.cs | 13 ++++++++++--- osu.Game/Screens/Select/SongSelect.cs | 3 ++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index eafbeebbc9..d78f387b30 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -166,10 +166,17 @@ namespace osu.Game.Overlays /// /// Start playing the current track (if not already playing). /// + /// Whether to restart the track from the beginning. + /// + /// Whether the request to play was issued by the user rather than internally. + /// Specifying true will ensure that other methods like + /// will resume music playback going forward. + /// /// Whether the operation was successful. - public bool Play(bool restart = false) + public bool Play(bool restart = false, bool requestedByUser = false) { - IsUserPaused = false; + if (requestedByUser) + IsUserPaused = false; if (restart) CurrentTrack.Restart(); @@ -203,7 +210,7 @@ namespace osu.Game.Overlays if (CurrentTrack.IsRunning) Stop(true); else - Play(); + Play(requestedByUser: true); return true; } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a85e1869be..0473efd404 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -579,7 +579,8 @@ namespace osu.Game.Screens.Select updateComponentFromBeatmap(Beatmap.Value); // restart playback on returning to song select, regardless. - music.Play(); + // not sure this should be a permanent thing (we may want to leave a user pause paused even on returning) + music.Play(requestedByUser: true); } this.FadeIn(250); From c9b96a18294b0143a38407951b15072a7cd8f9ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 15:52:57 +0900 Subject: [PATCH 4343/6909] Disable spectator streaming connection logic on iOS to avoid startup crash Due to an [issue](https://github.com/dotnet/runtime/issues/35988) at xamarin's end (which *should* have been fixed via [this pr](https://github.com/xamarin/xamarin-macios/pull/8615) but still fails on latest preview release) we can't support this just yet. --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index cb170ad298..125831035a 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -10,6 +10,7 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -101,6 +102,10 @@ namespace osu.Game.Online.Spectator private async Task connect() { + if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS) + // disabled for now (see https://github.com/dotnet/runtime/issues/35988) + return; + if (connection != null) return; From 6f623d8cca409cc8f2f06e6c1819706bf1e33fe9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Nov 2020 16:08:59 +0900 Subject: [PATCH 4344/6909] Rename IsUserPaused -> UserPauseRequested --- .../Visual/Menus/TestSceneMusicActionHandling.cs | 4 ++-- osu.Game/Overlays/MusicController.cs | 11 +++++++---- osu.Game/Screens/Select/SongSelect.cs | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index 4cad2b19d5..b34e027e9c 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -22,9 +22,9 @@ namespace osu.Game.Tests.Visual.Menus { AddStep("ensure playing something", () => Game.MusicController.EnsurePlayingSomething()); AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); - AddAssert("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.IsUserPaused); + AddAssert("music paused", () => !Game.MusicController.IsPlaying && Game.MusicController.UserPauseRequested); AddStep("toggle playback", () => globalActionContainer.TriggerPressed(GlobalAction.MusicPlay)); - AddAssert("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.IsUserPaused); + AddAssert("music resumed", () => Game.MusicController.IsPlaying && !Game.MusicController.UserPauseRequested); } [Test] diff --git a/osu.Game/Overlays/MusicController.cs b/osu.Game/Overlays/MusicController.cs index d78f387b30..3a9a6261ba 100644 --- a/osu.Game/Overlays/MusicController.cs +++ b/osu.Game/Overlays/MusicController.cs @@ -45,7 +45,10 @@ namespace osu.Game.Overlays private readonly BindableList beatmapSets = new BindableList(); - public bool IsUserPaused { get; private set; } + /// + /// Whether the user has requested the track to be paused. Use to determine whether the track is still playing. + /// + public bool UserPauseRequested { get; private set; } /// /// Fired when the global has changed. @@ -148,7 +151,7 @@ namespace osu.Game.Overlays /// public void EnsurePlayingSomething() { - if (IsUserPaused) return; + if (UserPauseRequested) return; if (CurrentTrack.IsDummyDevice || beatmap.Value.BeatmapSetInfo.DeletePending) { @@ -176,7 +179,7 @@ namespace osu.Game.Overlays public bool Play(bool restart = false, bool requestedByUser = false) { if (requestedByUser) - IsUserPaused = false; + UserPauseRequested = false; if (restart) CurrentTrack.Restart(); @@ -196,7 +199,7 @@ namespace osu.Game.Overlays /// public void Stop(bool requestedByUser = false) { - IsUserPaused |= requestedByUser; + UserPauseRequested |= requestedByUser; if (CurrentTrack.IsRunning) CurrentTrack.Stop(); } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 0473efd404..def620462f 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -682,7 +682,7 @@ namespace osu.Game.Screens.Select track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - if (!track.IsRunning && (music.IsUserPaused != true || isNewTrack)) + if (!track.IsRunning && (music.UserPauseRequested != true || isNewTrack)) music.Play(true); lastTrack.SetTarget(track); From 4e17634ee27ab7dfc33753c1750d8f91e6e2b206 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 17:31:04 +0900 Subject: [PATCH 4345/6909] Add (temporary) local user cache to avoid re-querying API each display --- .../Dashboard/CurrentlyPlayingDisplay.cs | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index d71e582c05..b461da4476 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.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.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; @@ -43,6 +44,9 @@ namespace osu.Game.Overlays.Dashboard [Resolved] private IAPIProvider api { get; set; } + // temporary, should be game-global but i don't want to add more manager classes for now. + private static readonly Dictionary user_cache = new Dictionary(); + protected override void LoadComplete() { base.LoadComplete(); @@ -53,15 +57,24 @@ namespace osu.Game.Overlays.Dashboard switch (e.Action) { case NotifyCollectionChangedAction.Add: - foreach (var u in e.NewItems.OfType()) + foreach (int userId in e.NewItems.OfType()) { - var request = new GetUserRequest(u); - request.Success += user => Schedule(() => + if (user_cache.TryGetValue(userId, out var user)) { - if (playingUsers.Contains((int)user.Id)) - userFlow.Add(createUserPanel(user)); - }); + addUser(user); + continue; + } + + var request = new GetUserRequest(userId); + request.Success += u => Schedule(() => addUser(u)); api.Queue(request); + + void addUser(User u) + { + user_cache[userId] = u; + if (playingUsers.Contains(userId)) + userFlow.Add(createUserPanel(u)); + } } break; From a89aeaf1cedff164f21eb07d78e0095edc417377 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 17:32:10 +0900 Subject: [PATCH 4346/6909] Add very basic connection status logging for spectator streaming client --- .../Online/Spectator/SpectatorStreamingClient.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index cb170ad298..2367651e04 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -13,6 +13,7 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Replays.Legacy; @@ -122,19 +123,26 @@ namespace osu.Game.Online.Spectator isConnected = false; playingUsers.Clear(); - if (ex != null) await tryUntilConnected(); + if (ex != null) + { + Logger.Log($"Spectator client lost connection: {ex}", LoggingTarget.Network); + await tryUntilConnected(); + } }; await tryUntilConnected(); async Task tryUntilConnected() { + Logger.Log("Spectator client connecting...", LoggingTarget.Network); + while (api.State.Value == APIState.Online) { try { // reconnect on any failure await connection.StartAsync(); + Logger.Log("Spectator client connected!", LoggingTarget.Network); // success isConnected = true; @@ -151,8 +159,9 @@ namespace osu.Game.Online.Spectator break; } - catch + catch (Exception e) { + Logger.Log($"Spectator client connection error: {e}", LoggingTarget.Network); await Task.Delay(5000); } } From 5cbfaf3589413c5be4292f97dd91c78b13059534 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 20:19:38 +0900 Subject: [PATCH 4347/6909] Revert "Add (temporary) local user cache to avoid re-querying API each display" This reverts commit 4e17634ee27ab7dfc33753c1750d8f91e6e2b206. --- .../Dashboard/CurrentlyPlayingDisplay.cs | 25 +++++-------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index b461da4476..d71e582c05 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.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.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; @@ -44,9 +43,6 @@ namespace osu.Game.Overlays.Dashboard [Resolved] private IAPIProvider api { get; set; } - // temporary, should be game-global but i don't want to add more manager classes for now. - private static readonly Dictionary user_cache = new Dictionary(); - protected override void LoadComplete() { base.LoadComplete(); @@ -57,24 +53,15 @@ namespace osu.Game.Overlays.Dashboard switch (e.Action) { case NotifyCollectionChangedAction.Add: - foreach (int userId in e.NewItems.OfType()) + foreach (var u in e.NewItems.OfType()) { - if (user_cache.TryGetValue(userId, out var user)) + var request = new GetUserRequest(u); + request.Success += user => Schedule(() => { - addUser(user); - continue; - } - - var request = new GetUserRequest(userId); - request.Success += u => Schedule(() => addUser(u)); + if (playingUsers.Contains((int)user.Id)) + userFlow.Add(createUserPanel(user)); + }); api.Queue(request); - - void addUser(User u) - { - user_cache[userId] = u; - if (playingUsers.Contains(userId)) - userFlow.Add(createUserPanel(u)); - } } break; From c6de0544d2920440ae7aa2cc9b4f5cfed8f23c11 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 20:21:23 +0900 Subject: [PATCH 4348/6909] Disable display for not --- osu.Game/Overlays/DashboardOverlay.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 04defce636..787a4985d7 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -131,7 +131,8 @@ namespace osu.Game.Overlays break; case DashboardOverlayTabs.CurrentlyPlaying: - loadDisplay(new CurrentlyPlayingDisplay()); + //todo: enable once caching logic is better + //loadDisplay(new CurrentlyPlayingDisplay()); break; default: From ed30756c199989f8547dd3d9350581e417ab66ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 20:41:14 +0900 Subject: [PATCH 4349/6909] Add test coverage for new display (and remove live version for now) --- .../Visual/Gameplay/TestSceneSpectator.cs | 9 +++ .../TestSceneCurrentlyPlayingDisplay.cs | 64 +++++++++++++++++++ .../Spectator/SpectatorStreamingClient.cs | 4 +- .../Dashboard/CurrentlyPlayingDisplay.cs | 4 +- 4 files changed, 77 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index a4df450db9..558778c918 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Testing; @@ -231,8 +233,15 @@ namespace osu.Game.Tests.Visual.Gameplay { public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; + public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; + private int beatmapId; + protected override Task Connect() + { + return Task.CompletedTask; + } + public void StartPlay(int beatmapId) { this.beatmapId = beatmapId; diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs new file mode 100644 index 0000000000..137d0c20a3 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Spectator; +using osu.Game.Overlays.Dashboard; +using osu.Game.Tests.Visual.Gameplay; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneCurrentlyPlayingDisplay : OsuTestScene + { + [Cached(typeof(SpectatorStreamingClient))] + private TestSceneSpectator.TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSceneSpectator.TestSpectatorStreamingClient(); + + private CurrentlyPlayingDisplay currentlyPlaying; + + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req => + { + switch (req) + { + case GetUserRequest cRequest: + cRequest.TriggerSuccess(new User { Username = "peppy", Id = 2 }); + break; + } + }); + + AddStep("add streaming client", () => + { + Remove(testSpectatorStreamingClient); + + Children = new Drawable[] + { + testSpectatorStreamingClient, + currentlyPlaying = new CurrentlyPlayingDisplay + { + RelativeSizeAxes = Axes.Both, + } + }; + }); + } + + [Test] + public void TestBasicDisplay() + { + AddStep("Add playing user", () => testSpectatorStreamingClient.PlayingUsers.Add(2)); + AddUntilStep("Panel loaded", () => currentlyPlaying.ChildrenOfType()?.FirstOrDefault()?.User.Id == 2); + AddStep("Remove playing user", () => testSpectatorStreamingClient.PlayingUsers.Remove(2)); + AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType().Any()); + } + } +} diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index cb170ad298..c07c00f3ba 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -92,14 +92,14 @@ namespace osu.Game.Online.Spectator break; case APIState.Online: - Task.Run(connect); + Task.Run(Connect); break; } } private const string endpoint = "https://spectator.ppy.sh/spectator"; - private async Task connect() + protected virtual async Task Connect() { if (connection != null) return; diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index d71e582c05..cb88065ec6 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -78,7 +78,7 @@ namespace osu.Game.Overlays.Dashboard }), true); } - [Resolved] + [Resolved(canBeNull: true)] private OsuGame game { get; set; } private UserPanel createUserPanel(User user) @@ -89,7 +89,7 @@ namespace osu.Game.Overlays.Dashboard panel.Origin = Anchor.TopCentre; panel.Width = 290; panel.ShowProfileOnClick = false; - panel.Action = () => game.PerformFromScreen(s => s.Push(new Spectator(user))); + panel.Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(user))); }); } } From 3956a0669a1cbf6574f6d45bba66c85a03a4b763 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 2 Nov 2020 21:08:58 +0900 Subject: [PATCH 4350/6909] Fix editor seek transform seeking too much --- osu.Game/Screens/Edit/EditorClock.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 949636f695..148eef6c93 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -266,8 +266,15 @@ namespace osu.Game.Screens.Edit { 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 Apply(EditorClock clock, double time) => clock.currentTime = valueAt(time); + + private double valueAt(double time) + { + if (time < StartTime) return StartValue; + if (time >= EndTime) return EndValue; + + return Interpolation.ValueAt(time, StartValue, EndValue, StartTime, EndTime, Easing); + } protected override void ReadIntoStartValue(EditorClock clock) => StartValue = clock.currentTime; } From c1d9a0c92c1c33a229a4df1a28865b2396a3412d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 21:09:47 +0900 Subject: [PATCH 4351/6909] Move click action out of user panel --- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../Dashboard/CurrentlyPlayingDisplay.cs | 57 +++++++++++++++---- osu.Game/Users/UserPanel.cs | 9 +-- 3 files changed, 52 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 558778c918..df4b85b37a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -229,7 +229,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for screen load", () => spectatorScreen.LoadState == LoadState.Loaded); } - internal class TestSpectatorStreamingClient : SpectatorStreamingClient + public class TestSpectatorStreamingClient : SpectatorStreamingClient { public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index cb88065ec6..8832cb52ea 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -11,6 +11,7 @@ using osu.Framework.Screens; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; +using osu.Game.Screens.Multi.Match.Components; using osu.Game.Screens.Play; using osu.Game.Users; using osuTK; @@ -21,7 +22,7 @@ namespace osu.Game.Overlays.Dashboard { private IBindableList playingUsers; - private FillFlowContainer userFlow; + private FillFlowContainer userFlow; [Resolved] private SpectatorStreamingClient spectatorStreaming { get; set; } @@ -32,7 +33,7 @@ namespace osu.Game.Overlays.Dashboard RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; - InternalChild = userFlow = new FillFlowContainer + InternalChild = userFlow = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -78,19 +79,53 @@ namespace osu.Game.Overlays.Dashboard }), true); } - [Resolved(canBeNull: true)] - private OsuGame game { get; set; } - - private UserPanel createUserPanel(User user) - { - return new UserGridPanel(user).With(panel => + private PlayingUserPanel createUserPanel(User user) => + new PlayingUserPanel(user).With(panel => { panel.Anchor = Anchor.TopCentre; panel.Origin = Anchor.TopCentre; - panel.Width = 290; - panel.ShowProfileOnClick = false; - panel.Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(user))); }); + + private class PlayingUserPanel : CompositeDrawable + { + public readonly User User; + + [Resolved(canBeNull: true)] + private OsuGame game { get; set; } + + public PlayingUserPanel(User user) + { + User = user; + + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(2), + Children = new Drawable[] + { + new UserGridPanel(user) + { + Width = 290, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new PurpleTriangleButton + { + Width = 290, + Text = "Watch", + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(user))) + } + } + }, + }; + } } } } diff --git a/osu.Game/Users/UserPanel.cs b/osu.Game/Users/UserPanel.cs index e97ff4287f..0981136dba 100644 --- a/osu.Game/Users/UserPanel.cs +++ b/osu.Game/Users/UserPanel.cs @@ -20,10 +20,12 @@ namespace osu.Game.Users { public readonly User User; + /// + /// Perform an action in addition to showing the user's profile. + /// This should be used to perform auxiliary tasks and not as a primary action for clicking a user panel (to maintain a consistent UX). + /// public new Action Action; - public bool ShowProfileOnClick = true; - protected Action ViewProfile { get; private set; } protected Drawable Background { get; private set; } @@ -70,8 +72,7 @@ namespace osu.Game.Users base.Action = ViewProfile = () => { Action?.Invoke(); - if (ShowProfileOnClick) - profileOverlay?.ShowUser(User); + profileOverlay?.ShowUser(User); }; } From c3d3856a64004bb41cc19065e0d0bf57c9408a67 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 2 Nov 2020 23:51:01 +0900 Subject: [PATCH 4352/6909] Fix looping mode not being set on first track after entering song select Closes #10656. --- osu.Game/Screens/Select/SongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index def620462f..82a2cd790a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -519,6 +519,7 @@ namespace osu.Game.Screens.Select ModSelect.SelectedMods.BindTo(selectedMods); + music.CurrentTrack.Looping = true; music.TrackChanged += ensureTrackLooping; } From d5c95a8b46a21d69d71dbeb4e3b530f023968e49 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 00:45:55 +0900 Subject: [PATCH 4353/6909] Centralise into methods and add assertions for safety --- osu.Game/Screens/Select/SongSelect.cs | 32 ++++++++++++++++++++------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 82a2cd790a..975431fb62 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -37,6 +37,7 @@ using osu.Framework.Input.Bindings; using osu.Game.Collections; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; +using System.Diagnostics; namespace osu.Game.Screens.Select { @@ -519,8 +520,7 @@ namespace osu.Game.Screens.Select ModSelect.SelectedMods.BindTo(selectedMods); - music.CurrentTrack.Looping = true; - music.TrackChanged += ensureTrackLooping; + beginLooping(); } private const double logo_transition = 250; @@ -571,8 +571,7 @@ namespace osu.Game.Screens.Select BeatmapDetails.Refresh(); - music.CurrentTrack.Looping = true; - music.TrackChanged += ensureTrackLooping; + beginLooping(); music.ResetTrackAdjustments(); if (Beatmap != null && !Beatmap.Value.BeatmapSetInfo.DeletePending) @@ -598,8 +597,7 @@ namespace osu.Game.Screens.Select BeatmapOptions.Hide(); - music.CurrentTrack.Looping = false; - music.TrackChanged -= ensureTrackLooping; + endLooping(); this.ScaleTo(1.1f, 250, Easing.InSine); @@ -620,12 +618,30 @@ namespace osu.Game.Screens.Select FilterControl.Deactivate(); - music.CurrentTrack.Looping = false; - music.TrackChanged -= ensureTrackLooping; + endLooping(); return false; } + private bool isHandlingLooping; + + private void beginLooping() + { + Debug.Assert(!isHandlingLooping); + + music.CurrentTrack.Looping = isHandlingLooping = true; + + music.TrackChanged += ensureTrackLooping; + } + + private void endLooping() + { + Debug.Assert(isHandlingLooping); + music.CurrentTrack.Looping = isHandlingLooping = false; + + music.TrackChanged -= ensureTrackLooping; + } + private void ensureTrackLooping(WorkingBeatmap beatmap, TrackChangeDirection changeDirection) => music.CurrentTrack.Looping = true; From ab308d28d2ae40350e41b974b7772afd8e175865 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 01:08:35 +0900 Subject: [PATCH 4354/6909] Debounce calls to UpdateTernaryStates Just something I noticed in passing recently which may help with reducing performance overhead of some batch operations. --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index e346630235..c2441b31a9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -419,11 +419,11 @@ namespace osu.Game.Screens.Edit.Compose.Components }; // bring in updates from selection changes - EditorBeatmap.HitObjectUpdated += _ => UpdateTernaryStates(); + EditorBeatmap.HitObjectUpdated += _ => Scheduler.AddOnce(UpdateTernaryStates); EditorBeatmap.SelectedHitObjects.CollectionChanged += (sender, args) => { Scheduler.AddOnce(updateVisibility); - UpdateTernaryStates(); + Scheduler.AddOnce(UpdateTernaryStates); }; } From 71c04472fa99c5760f19ba2dcf71dd2727433081 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 14:21:19 +0900 Subject: [PATCH 4355/6909] Fix osu!catch replay conversion applying left movements to wrong frame --- osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs index 7efd832f62..1a80adb584 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchReplayFrame.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Replays if (Position > lastCatchFrame.Position) lastCatchFrame.Actions.Add(CatchAction.MoveRight); else if (Position < lastCatchFrame.Position) - Actions.Add(CatchAction.MoveLeft); + lastCatchFrame.Actions.Add(CatchAction.MoveLeft); } } From 34aa59f237a3509d0f9e29394e9497e2d5339801 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 14:58:55 +0900 Subject: [PATCH 4356/6909] Fix editor clock not always remaining stopped when dragging timeline Closes https://github.com/ppy/osu/issues/10461. --- .../Screens/Edit/Compose/Components/Timeline/Timeline.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 9aff4ddf8f..f6b2f99c55 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -165,6 +165,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (!track.IsLoaded || track.Length == 0) return; + // covers the case where the user starts playback after a drag is in progress. + // we want to ensure the clock is always stopped during drags to avoid weird audio playback. + if (handlingDragInput) + editorClock.Stop(); + ScrollTo((float)(editorClock.CurrentTime / track.Length) * Content.DrawWidth, false); } From 6bf35d5767fd880d841d4f387f164f2fc30a373a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 16:00:07 +0900 Subject: [PATCH 4357/6909] Fix editor menu not supporting stateful item drawables --- .../Edit/Components/Menus/EditorMenuBar.cs | 33 +++++++++---------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs index afd9e3d760..c6787a1fb1 100644 --- a/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs +++ b/osu.Game/Screens/Edit/Components/Menus/EditorMenuBar.cs @@ -163,30 +163,27 @@ namespace osu.Game.Screens.Edit.Components.Menus protected override Framework.Graphics.UserInterface.Menu CreateSubMenu() => new SubMenu(); - protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) => new DrawableSubMenuItem(item); - - private class DrawableSubMenuItem : DrawableOsuMenuItem + protected override DrawableMenuItem CreateDrawableMenuItem(MenuItem item) { - public DrawableSubMenuItem(MenuItem item) + switch (item) + { + case EditorMenuItemSpacer spacer: + return new DrawableSpacer(spacer); + } + + return base.CreateDrawableMenuItem(item); + } + + private class DrawableSpacer : DrawableOsuMenuItem + { + public DrawableSpacer(MenuItem item) : base(item) { } - protected override bool OnHover(HoverEvent e) - { - if (Item is EditorMenuItemSpacer) - return true; + protected override bool OnHover(HoverEvent e) => true; - return base.OnHover(e); - } - - protected override bool OnClick(ClickEvent e) - { - if (Item is EditorMenuItemSpacer) - return true; - - return base.OnClick(e); - } + protected override bool OnClick(ClickEvent e) => true; } } } From 7e4314684bd4be4494eb5204a9fb57afc5fac2c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 16:01:14 +0900 Subject: [PATCH 4358/6909] Add editor waveform opacity selection to menu --- osu.Game/Configuration/OsuConfigManager.cs | 5 ++- osu.Game/Screens/Edit/Editor.cs | 10 ++++- osu.Game/Screens/Edit/WaveformOpacityMenu.cs | 46 ++++++++++++++++++++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Screens/Edit/WaveformOpacityMenu.cs diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index e0971d238a..26f72d3455 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -132,6 +132,8 @@ namespace osu.Game.Configuration Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes); + + Set(OsuSetting.EditorWaveformOpacity, 1f); } public OsuConfigManager(Storage storage) @@ -241,6 +243,7 @@ namespace osu.Game.Configuration HitLighting, MenuBackgroundSource, GameplayDisableWinKey, - SeasonalBackgroundMode + SeasonalBackgroundMode, + EditorWaveformOpacity, } } diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index f95c7fe7a6..13d1f378a6 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -20,6 +20,7 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Timing; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; @@ -103,7 +104,7 @@ namespace osu.Game.Screens.Edit private MusicController music { get; set; } [BackgroundDependencyLoader] - private void load(OsuColour colours, GameHost host) + private void load(OsuColour colours, GameHost host, OsuConfigManager config) { beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor; beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); @@ -208,6 +209,13 @@ namespace osu.Game.Screens.Edit copyMenuItem = new EditorMenuItem("Copy", MenuItemType.Standard, Copy), pasteMenuItem = new EditorMenuItem("Paste", MenuItemType.Standard, Paste), } + }, + new MenuItem("View") + { + Items = new[] + { + new WaveformOpacityMenu(config) + } } } } diff --git a/osu.Game/Screens/Edit/WaveformOpacityMenu.cs b/osu.Game/Screens/Edit/WaveformOpacityMenu.cs new file mode 100644 index 0000000000..5d209ae141 --- /dev/null +++ b/osu.Game/Screens/Edit/WaveformOpacityMenu.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Screens.Edit +{ + internal class WaveformOpacityMenu : MenuItem + { + private readonly Bindable waveformOpacity; + + private readonly Dictionary menuItemLookup = new Dictionary(); + + public WaveformOpacityMenu(OsuConfigManager config) + : base("Waveform opacity") + { + Items = new[] + { + createMenuItem(0.25f), + createMenuItem(0.5f), + createMenuItem(0.75f), + createMenuItem(1f), + }; + + waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); + waveformOpacity.BindValueChanged(opacity => + { + foreach (var kvp in menuItemLookup) + kvp.Value.State.Value = kvp.Key == opacity.NewValue; + }, true); + } + + private ToggleMenuItem createMenuItem(float opacity) + { + var item = new ToggleMenuItem($"{opacity * 100}%", MenuItemType.Standard, _ => updateOpacity(opacity)); + menuItemLookup[opacity] = item; + return item; + } + + private void updateOpacity(float opacity) => waveformOpacity.Value = opacity; + } +} From 0dcb4ea4413066850eaea9089cc9cb16fcd2813e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 16:07:01 +0900 Subject: [PATCH 4359/6909] Add handling of opacity to timeline waveform display --- .../Edit/Compose/Components/Timeline/Timeline.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 9aff4ddf8f..1e374d3103 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osuTK; @@ -67,8 +68,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private TimelineControlPointDisplay controlPoints; + private Bindable waveformOpacity; + [BackgroundDependencyLoader] - private void load(IBindable beatmap, OsuColour colours) + private void load(IBindable beatmap, OsuColour colours, OsuConfigManager config) { AddRange(new Drawable[] { @@ -95,7 +98,10 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // We don't want the centre marker to scroll AddInternal(new CentreMarker { Depth = float.MaxValue }); - WaveformVisible.ValueChanged += visible => waveform.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); + waveformOpacity = config.GetBindable(OsuSetting.EditorWaveformOpacity); + waveformOpacity.BindValueChanged(_ => updateWaveformOpacity(), true); + + WaveformVisible.ValueChanged += _ => updateWaveformOpacity(); ControlPointsVisible.ValueChanged += visible => controlPoints.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); TicksVisible.ValueChanged += visible => ticks.FadeTo(visible.NewValue ? 1 : 0, 200, Easing.OutQuint); @@ -115,6 +121,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline }, true); } + private void updateWaveformOpacity() => + waveform.FadeTo(WaveformVisible.Value ? waveformOpacity.Value : 0, 200, Easing.OutQuint); + private float getZoomLevelForVisibleMilliseconds(double milliseconds) => Math.Max(1, (float)(track.Length / milliseconds)); protected override void Update() From c2ffc1837905821157a738ca7a31f19b900a5c8f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 16:30:11 +0900 Subject: [PATCH 4360/6909] Change editor timeline mouse wheel handling to scroll by default (and zoom with alt held) --- .../Timeline/ZoomableScrollContainer.cs | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index f90658e99c..9094b6bc98 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -113,19 +113,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override bool OnScroll(ScrollEvent e) { - if (e.IsPrecise) + if (e.CurrentState.Keyboard.AltPressed) { - // can't handle scroll correctly while playing. - // the editor will handle this case for us. - if (editorClock?.IsRunning == true) - return false; - - // for now, we don't support zoom when using a precision scroll device. this needs gesture support. - return base.OnScroll(e); + // zoom when holding alt. + setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); } - setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); - return true; + // can't handle scroll correctly while playing. + // the editor will handle this case for us. + if (editorClock?.IsRunning == true) + return false; + + return base.OnScroll(e); } private void updateZoomedContentWidth() => zoomedContent.Width = DrawWidth * currentZoom; From df9ff76f23871bfaf991adc2fea9f0086ad9ee50 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 16:49:13 +0900 Subject: [PATCH 4361/6909] Reduce assert to guard in the outwards direction --- osu.Game/Screens/Select/SongSelect.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 975431fb62..b55c0694ef 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -636,7 +636,10 @@ namespace osu.Game.Screens.Select private void endLooping() { - Debug.Assert(isHandlingLooping); + // may be called multiple times during screen exit process. + if (!isHandlingLooping) + return; + music.CurrentTrack.Looping = isHandlingLooping = false; music.TrackChanged -= ensureTrackLooping; From ff4dcf065a2da691a6b19aaaecd1a2a211487188 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 17:06:33 +0900 Subject: [PATCH 4362/6909] Update tests --- .../Visual/Editing/TestSceneZoomableScrollContainer.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs index 082268d824..c8187491b9 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs @@ -7,13 +7,14 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; -using osu.Framework.Utils; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Cursor; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; using osuTK.Graphics; +using osuTK.Input; namespace osu.Game.Tests.Visual.Editing { @@ -88,6 +89,7 @@ namespace osu.Game.Tests.Visual.Editing // Scroll in at 0.25 AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft)); AddStep("Scroll by 3", () => InputManager.ScrollBy(new Vector2(0, 3))); AddAssert("Box not at 0", () => !Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); @@ -96,6 +98,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Scroll by -3", () => InputManager.ScrollBy(new Vector2(0, -3))); AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); AddAssert("Box 1/4 at 1/4", () => Precision.AlmostEquals(boxQuad.TopLeft.X + 0.25f * boxQuad.Size.X, scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X)); + AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft)); } [Test] @@ -103,6 +106,8 @@ namespace osu.Game.Tests.Visual.Editing { reset(); + AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft)); + // Scroll in at 0.25 AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); AddStep("Scroll by 1", () => InputManager.ScrollBy(new Vector2(0, 1))); @@ -124,6 +129,8 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); AddStep("Scroll by -1", () => InputManager.ScrollBy(new Vector2(0, -1))); AddAssert("Box at 0", () => Precision.AlmostEquals(boxQuad.TopLeft, scrollQuad.TopLeft)); + + AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft)); } private void reset() From ff2b01fa6f6fa33035de9378d20a8f2187d54571 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 17:22:45 +0900 Subject: [PATCH 4363/6909] Add test coverage of zoom-then-scroll --- .../TestSceneZoomableScrollContainer.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs index c8187491b9..95d11d6909 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneZoomableScrollContainer.cs @@ -101,6 +101,24 @@ namespace osu.Game.Tests.Visual.Editing AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft)); } + [Test] + public void TestMouseZoomInThenScroll() + { + reset(); + + // Scroll in at 0.25 + AddStep("Move mouse to 0.25x", () => InputManager.MoveMouseTo(new Vector2(scrollQuad.TopLeft.X + 0.25f * scrollQuad.Size.X, scrollQuad.Centre.Y))); + AddStep("Press alt down", () => InputManager.PressKey(Key.AltLeft)); + AddStep("Zoom by 3", () => InputManager.ScrollBy(new Vector2(0, 3))); + AddStep("Release alt", () => InputManager.ReleaseKey(Key.AltLeft)); + + AddStep("Scroll far left", () => InputManager.ScrollBy(new Vector2(0, 30))); + AddUntilStep("Scroll is at start", () => Precision.AlmostEquals(scrollQuad.TopLeft.X, boxQuad.TopLeft.X, 1)); + + AddStep("Scroll far right", () => InputManager.ScrollBy(new Vector2(0, -300))); + AddUntilStep("Scroll is at end", () => Precision.AlmostEquals(scrollQuad.TopRight.X, boxQuad.TopRight.X, 1)); + } + [Test] public void TestMouseZoomInTwiceOutTwice() { From b069372b2952cf95b0928c87ba3939dba6617967 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 17:49:48 +0900 Subject: [PATCH 4364/6909] Fix skin changes resulting in incorrectly applied transforms in MainCirclePiece --- .../Drawables/Pieces/MainCirclePiece.cs | 50 +++++++++++-------- 1 file changed, 28 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs index cb3787a493..e2345de886 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -42,10 +42,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private readonly IBindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); + [Resolved] + private DrawableHitObject drawableObject { get; set; } + [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) + private void load() { - OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; + var osuObject = (OsuHitObject)drawableObject.HitObject; state.BindTo(drawableObject.State); state.BindValueChanged(updateState, true); @@ -64,32 +67,35 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private void updateState(ValueChangedEvent state) { - glow.FadeOut(400); - - switch (state.NewValue) + using (BeginAbsoluteSequence(drawableObject.HitObject.StartTime + (drawableObject.Result?.TimeOffset ?? 0), true)) { - case ArmedState.Hit: - const double flash_in = 40; - const double flash_out = 100; + glow.FadeOut(400); - flash.FadeTo(0.8f, flash_in) - .Then() - .FadeOut(flash_out); + switch (state.NewValue) + { + case ArmedState.Hit: + const double flash_in = 40; + const double flash_out = 100; - explode.FadeIn(flash_in); - this.ScaleTo(1.5f, 400, Easing.OutQuad); + flash.FadeTo(0.8f, flash_in) + .Then() + .FadeOut(flash_out); - using (BeginDelayedSequence(flash_in, true)) - { - // after the flash, we can hide some elements that were behind it - ring.FadeOut(); - circle.FadeOut(); - number.FadeOut(); + explode.FadeIn(flash_in); + this.ScaleTo(1.5f, 400, Easing.OutQuad); - this.FadeOut(800); - } + using (BeginDelayedSequence(flash_in, true)) + { + // after the flash, we can hide some elements that were behind it + ring.FadeOut(); + circle.FadeOut(); + number.FadeOut(); - break; + this.FadeOut(800); + } + + break; + } } } } From 9f8ea93068dc0b53c1390084c95e878703a90036 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 18:45:18 +0900 Subject: [PATCH 4365/6909] Fix osu!catch banana animation not playing due to incorrect lifetimes Closes #10117. --- .../Objects/Drawables/DrawableCatchHitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index d03a764bda..ac1f11e09f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public abstract class DrawableCatchHitObject : DrawableHitObject { + protected override double InitialLifetimeOffset => HitObject.TimePreempt; + public virtual bool StaysOnPlate => HitObject.CanBePlated; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; From d788931661e6110393f011ca3512582ab168c46c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 19:53:35 +0900 Subject: [PATCH 4366/6909] Fix LoadComponentAsync calls potentially occuring after beatmap wedge disposal As seen in https://ci.appveyor.com/project/peppy/osu/builds/36109658/tests. Also adds cancellation logic for good measure. --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index bdfcc2fd96..ad3bce8968 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using JetBrains.Annotations; using osuTK; using osuTK.Graphics; @@ -85,6 +86,8 @@ namespace osu.Game.Screens.Select private WorkingBeatmap beatmap; + private CancellationTokenSource cancellationSource; + public WorkingBeatmap Beatmap { get => beatmap; @@ -93,10 +96,12 @@ namespace osu.Game.Screens.Select if (beatmap == value) return; beatmap = value; + cancellationSource?.Cancel(); + cancellationSource = new CancellationTokenSource(); beatmapDifficulty?.UnbindAll(); - beatmapDifficulty = difficultyManager.GetBindableDifficulty(beatmap.BeatmapInfo); - beatmapDifficulty.BindValueChanged(_ => updateDisplay()); + beatmapDifficulty = difficultyManager.GetBindableDifficulty(beatmap.BeatmapInfo, cancellationSource.Token); + beatmapDifficulty.BindValueChanged(_ => Schedule(updateDisplay)); updateDisplay(); } @@ -137,6 +142,12 @@ namespace osu.Game.Screens.Select }); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + cancellationSource?.Cancel(); + } + public class BufferedWedgeInfo : BufferedContainer { public OsuSpriteText VersionLabel { get; private set; } From d4f8c63f9ee1de0cfcfbaa8b6b6206e42e16ee64 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 19:59:06 +0900 Subject: [PATCH 4367/6909] Fix reference to dummyAPI not using helper property --- .../Visual/Online/TestSceneCurrentlyPlayingDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 137d0c20a3..f85abeb51a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Online [SetUpSteps] public void SetUpSteps() { - AddStep("register request handling", () => ((DummyAPIAccess)API).HandleRequest = req => + AddStep("register request handling", () => dummyAPI.HandleRequest = req => { switch (req) { From aaffd59dfe4a43ebe1d75802355f0ca6453e07f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 20:02:02 +0900 Subject: [PATCH 4368/6909] Add test step to reset players (to better allow multiple runs of tests) --- .../Visual/Online/TestSceneCurrentlyPlayingDisplay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index f85abeb51a..d6fd33bce7 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -50,6 +50,8 @@ namespace osu.Game.Tests.Visual.Online } }; }); + + AddStep("Reset players", () => testSpectatorStreamingClient.PlayingUsers.Clear()); } [Test] From 3e29e468ea3907bad4af44c08e24ad74c17eeeb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 20:06:42 +0900 Subject: [PATCH 4369/6909] Ensure "start watching" button starts in a disabled state --- osu.Game/Screens/Play/Spectator.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 2c8b5c4cad..9ed911efd5 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -165,7 +165,8 @@ namespace osu.Game.Screens.Play Width = 250, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Action = attemptStart + Action = attemptStart, + Enabled = { Value = false } } } } From 1b2bd6a8c9de4abd0539bd217f5db55f65939ce1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 20:10:13 +0900 Subject: [PATCH 4370/6909] Remove redundant base call --- osu.Game/Screens/Play/SpectatorPlayer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/SpectatorPlayer.cs b/osu.Game/Screens/Play/SpectatorPlayer.cs index fbed84b820..fdf996150f 100644 --- a/osu.Game/Screens/Play/SpectatorPlayer.cs +++ b/osu.Game/Screens/Play/SpectatorPlayer.cs @@ -21,7 +21,6 @@ namespace osu.Game.Screens.Play protected override bool CheckModsAllowFailure() => false; // todo: better support starting mid-way through beatmap public SpectatorPlayer(Score score) - : base(true, true) { this.score = score; } From 9f333ac58a379164ece8af22302cd09d639b8480 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 20:45:48 +0900 Subject: [PATCH 4371/6909] Add the ability to delete slider control points using shift+right click Closes https://github.com/ppy/osu/issues/10672. In two minds about how this should be implemented but went in this direction initially. The other way would be to add local handling of Shift-Right Click inside PathControlPointPiece (which is already doing mouse handling itself). --- .../Components/PathControlPointVisualiser.cs | 10 +++++++--- .../Sliders/SliderSelectionBlueprint.cs | 16 +++++++++++++++- osu.Game/Rulesets/Edit/SelectionBlueprint.cs | 6 ++++++ .../Compose/Components/BlueprintContainer.cs | 11 ++++++++--- .../Edit/Compose/Components/SelectionHandler.cs | 14 ++++++++++++-- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index f6354bc612..13dc7886ed 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -18,6 +18,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components @@ -105,7 +106,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components switch (action.ActionMethod) { case PlatformActionMethod.Delete: - return deleteSelected(); + return DeleteSelected(); } return false; @@ -115,6 +116,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { } + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)); + private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) @@ -126,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } - private bool deleteSelected() + public bool DeleteSelected() { List toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList(); @@ -169,7 +173,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components return new MenuItem[] { - new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => deleteSelected()), + new OsuMenuItem($"Delete {"control point".ToQuantity(count, count > 1 ? ShowQuantityAs.Numeric : ShowQuantityAs.None)}", MenuItemType.Destructive, () => DeleteSelected()), new OsuMenuItem("Curve type") { Items = items diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index d3fb5defae..59b087f68f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -72,6 +73,18 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders BodyPiece.UpdateFrom(HitObject); } + public override bool HandleQuickDeletion() + { + var hoveredControlPoint = ControlPointVisualiser.Pieces.FirstOrDefault(p => p.IsHovered); + + if (hoveredControlPoint == null) + return false; + + hoveredControlPoint.IsSelected.Value = true; + ControlPointVisualiser.DeleteSelected(); + return true; + } + protected override void Update() { base.Update(); @@ -216,7 +229,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override Vector2 ScreenSpaceSelectionPoint => BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => BodyPiece.ReceivePositionalInputAt(screenSpacePos); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => + BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.ReceivePositionalInputAt(screenSpacePos) == true; protected virtual SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new SliderCircleSelectionBlueprint(slider, position); } diff --git a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs index f3816f6218..99cdca045b 100644 --- a/osu.Game/Rulesets/Edit/SelectionBlueprint.cs +++ b/osu.Game/Rulesets/Edit/SelectionBlueprint.cs @@ -143,5 +143,11 @@ namespace osu.Game.Rulesets.Edit public virtual Quad SelectionQuad => ScreenSpaceDrawQuad; public virtual Vector2 GetInstantDelta(Vector2 screenSpacePosition) => Parent.ToLocalSpace(screenSpacePosition) - Position; + + /// + /// Handle to perform a partial deletion when the user requests a quick delete (Shift+Right Click). + /// + /// True if the deletion operation was handled by this blueprint. Returning false will delete the full blueprint. + public virtual bool HandleQuickDeletion() => false; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index fa98358dbe..d8f7137fa6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -116,7 +116,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - beginClickSelection(e); + if (beginClickSelection(e)) return true; + prepareSelectionMovement(); return e.Button == MouseButton.Left; @@ -291,19 +292,23 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Attempts to select any hovered blueprints. /// /// The input event that triggered this selection. - private void beginClickSelection(MouseButtonEvent e) + private bool beginClickSelection(MouseButtonEvent e) { Debug.Assert(!clickSelectionBegan); + bool rightClickHandled = false; + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) { if (blueprint.IsHovered) { - SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); + rightClickHandled |= SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); clickSelectionBegan = true; break; } } + + return rightClickHandled; } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index c2441b31a9..5bf6c52e11 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -219,18 +219,28 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The blueprint. /// The input state at the point of selection. - internal void HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) + /// Whether right click was handled. + internal bool HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) { if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) + { handleQuickDeletion(blueprint); - else if (state.Keyboard.ControlPressed && state.Mouse.IsPressed(MouseButton.Left)) + return true; + } + + if (state.Keyboard.ControlPressed && state.Mouse.IsPressed(MouseButton.Left)) blueprint.ToggleSelection(); else ensureSelected(blueprint); + + return false; } private void handleQuickDeletion(SelectionBlueprint blueprint) { + if (blueprint.HandleQuickDeletion()) + return; + if (!blueprint.IsSelected) EditorBeatmap.Remove(blueprint.HitObject); else From 83c004bbdefa4cc9bf4eb9dcfff7d91bcaf8187c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 21:10:31 +0900 Subject: [PATCH 4372/6909] Add key bindings for flip and reverse patterns --- .../Edit/Compose/Components/SelectionBox.cs | 36 +++++++++++++++---- .../Compose/Components/SelectionHandler.cs | 8 ++--- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index b753c45cca..6367dd6b9b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -7,17 +7,19 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; +using osu.Framework.Input.Events; using osu.Game.Graphics; using osuTK; +using osuTK.Input; namespace osu.Game.Screens.Edit.Compose.Components { public class SelectionBox : CompositeDrawable { - public Action OnRotation; - public Action OnScale; - public Action OnFlip; - public Action OnReverse; + public Func OnRotation; + public Func OnScale; + public Func OnFlip; + public Func OnReverse; public Action OperationStarted; public Action OperationEnded; @@ -105,6 +107,26 @@ namespace osu.Game.Screens.Edit.Compose.Components recreate(); } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat || !e.ControlPressed) + return false; + + switch (e.Key) + { + case Key.G: + return OnReverse?.Invoke() == true; + + case Key.H: + return OnFlip?.Invoke(Direction.Horizontal) == true; + + case Key.J: + return OnFlip?.Invoke(Direction.Vertical) == true; + } + + return base.OnKeyDown(e); + } + private void recreate() { if (LoadState < LoadState.Loading) @@ -143,7 +165,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (CanScaleX && CanScaleY) addFullScaleComponents(); if (CanScaleY) addYScaleComponents(); if (CanRotate) addRotationComponents(); - if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern", () => OnReverse?.Invoke()); + if (CanReverse) addButton(FontAwesome.Solid.Backward, "Reverse pattern (Ctrl-G)", () => OnReverse?.Invoke()); } private void addRotationComponents() @@ -178,7 +200,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addYScaleComponents() { - addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically", () => OnFlip?.Invoke(Direction.Vertical)); + addButton(FontAwesome.Solid.ArrowsAltV, "Flip vertically (Ctrl-J)", () => OnFlip?.Invoke(Direction.Vertical)); addDragHandle(Anchor.TopCentre); addDragHandle(Anchor.BottomCentre); @@ -194,7 +216,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addXScaleComponents() { - addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally", () => OnFlip?.Invoke(Direction.Horizontal)); + addButton(FontAwesome.Solid.ArrowsAltH, "Flip horizontally (Ctrl-H)", () => OnFlip?.Invoke(Direction.Horizontal)); addDragHandle(Anchor.CentreLeft); addDragHandle(Anchor.CentreRight); diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index c2441b31a9..406ca07185 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -99,10 +99,10 @@ namespace osu.Game.Screens.Edit.Compose.Components OperationStarted = OnOperationBegan, OperationEnded = OnOperationEnded, - OnRotation = angle => HandleRotation(angle), - OnScale = (amount, anchor) => HandleScale(amount, anchor), - OnFlip = direction => HandleFlip(direction), - OnReverse = () => HandleReverse(), + OnRotation = HandleRotation, + OnScale = HandleScale, + OnFlip = HandleFlip, + OnReverse = HandleReverse, }; /// From d45520be5eb2a7008a815daefea9474563b9d247 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 3 Nov 2020 21:23:10 +0900 Subject: [PATCH 4373/6909] Fix incorrect return types on test methods --- osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs index da98a7a024..cf5f1b8818 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneComposeSelectBox.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Editing AddToggleStep("toggle y", state => selectionBox.CanScaleY = state); } - private void handleScale(Vector2 amount, Anchor reference) + private bool handleScale(Vector2 amount, Anchor reference) { if ((reference & Anchor.y1) == 0) { @@ -58,12 +58,15 @@ namespace osu.Game.Tests.Visual.Editing selectionArea.X += amount.X; selectionArea.Width += directionX * amount.X; } + + return true; } - private void handleRotation(float angle) + private bool handleRotation(float angle) { // kinda silly and wrong, but just showing that the drag handles work. selectionArea.Rotation += angle; + return true; } } } From 18428dff8ee20a62ec8b720357d4a61e3559a8a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Nov 2020 18:01:12 +0100 Subject: [PATCH 4374/6909] Ensure hotkey actions are executable in handler --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 6367dd6b9b..742d433760 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -115,13 +115,13 @@ namespace osu.Game.Screens.Edit.Compose.Components switch (e.Key) { case Key.G: - return OnReverse?.Invoke() == true; + return CanReverse && OnReverse?.Invoke() == true; case Key.H: - return OnFlip?.Invoke(Direction.Horizontal) == true; + return CanScaleX && OnFlip?.Invoke(Direction.Horizontal) == true; case Key.J: - return OnFlip?.Invoke(Direction.Vertical) == true; + return CanScaleY && OnFlip?.Invoke(Direction.Vertical) == true; } return base.OnKeyDown(e); From 86d283ebf4439c940a02f5fd889ff1f27d406447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Nov 2020 19:03:48 +0100 Subject: [PATCH 4375/6909] Adjust layout slightly to avoid specifying width twice --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 8832cb52ea..79af81cfac 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -103,20 +103,21 @@ namespace osu.Game.Overlays.Dashboard { new FillFlowContainer { - AutoSizeAxes = Axes.Both, + AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, Spacing = new Vector2(2), + Width = 290, Children = new Drawable[] { new UserGridPanel(user) { - Width = 290, + RelativeSizeAxes = Axes.X, Anchor = Anchor.Centre, Origin = Anchor.Centre, }, new PurpleTriangleButton { - Width = 290, + RelativeSizeAxes = Axes.X, Text = "Watch", Anchor = Anchor.Centre, Origin = Anchor.Centre, From 211510fe9aa0eff1243249cb3aeb4b2cf8519f59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Nov 2020 19:12:03 +0100 Subject: [PATCH 4376/6909] Fix undesirable vertical spacing in currently playing display --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 79af81cfac..34444c2fa5 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -112,15 +112,15 @@ namespace osu.Game.Overlays.Dashboard new UserGridPanel(user) { RelativeSizeAxes = Axes.X, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, }, new PurpleTriangleButton { RelativeSizeAxes = Axes.X, Text = "Watch", - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(user))) } } From 4b528e80d012d96e5e81a693222fc60761af947d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Nov 2020 21:49:04 +0100 Subject: [PATCH 4377/6909] Use AltPressed shorthand --- .../Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 9094b6bc98..548bb66198 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -113,7 +113,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override bool OnScroll(ScrollEvent e) { - if (e.CurrentState.Keyboard.AltPressed) + if (e.AltPressed) { // zoom when holding alt. setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); From ddf0d75c38d44bab54db52d69309fe6ca3d7c957 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 3 Nov 2020 21:49:21 +0100 Subject: [PATCH 4378/6909] Don't fall through to seek if handling zoom --- .../Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs index 548bb66198..f10eb0d284 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/ZoomableScrollContainer.cs @@ -117,6 +117,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { // zoom when holding alt. setZoomTarget(zoomTarget + e.ScrollDelta.Y, zoomedContent.ToLocalSpace(e.ScreenSpaceMousePosition).X); + return true; } // can't handle scroll correctly while playing. From cfe32182397f79efd8ca30fbd02724c718f16b2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 10:21:42 +0900 Subject: [PATCH 4379/6909] Move scheduler call to inside method itself for added safety --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 57 +++++++++++---------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index ad3bce8968..2634f117de 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Select beatmapDifficulty?.UnbindAll(); beatmapDifficulty = difficultyManager.GetBindableDifficulty(beatmap.BeatmapInfo, cancellationSource.Token); - beatmapDifficulty.BindValueChanged(_ => Schedule(updateDisplay)); + beatmapDifficulty.BindValueChanged(_ => updateDisplay()); updateDisplay(); } @@ -113,33 +113,38 @@ namespace osu.Game.Screens.Select private void updateDisplay() { - void removeOldInfo() - { - State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; + Scheduler.AddOnce(perform); - Info?.FadeOut(250); - Info?.Expire(); - Info = null; + void perform() + { + void removeOldInfo() + { + State.Value = beatmap == null ? Visibility.Hidden : Visibility.Visible; + + Info?.FadeOut(250); + Info?.Expire(); + Info = null; + } + + if (beatmap == null) + { + removeOldInfo(); + return; + } + + LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value) + { + Shear = -Shear, + Depth = Info?.Depth + 1 ?? 0 + }, loaded => + { + // ensure we are the most recent loaded wedge. + if (loaded != loadingInfo) return; + + removeOldInfo(); + Add(Info = loaded); + }); } - - if (beatmap == null) - { - removeOldInfo(); - return; - } - - LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value) - { - Shear = -Shear, - Depth = Info?.Depth + 1 ?? 0 - }, loaded => - { - // ensure we are the most recent loaded wedge. - if (loaded != loadingInfo) return; - - removeOldInfo(); - Add(Info = loaded); - }); } protected override void Dispose(bool isDisposing) From 1d90f5fc082b636eb4f2a6466deada44bdd16d43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 14:16:51 +0900 Subject: [PATCH 4380/6909] Revert "Disable spectator streaming connection logic on iOS to avoid startup crash" This reverts commit c9b96a18294b0143a38407951b15072a7cd8f9ff. --- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 125831035a..cb170ad298 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -10,7 +10,6 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -102,10 +101,6 @@ namespace osu.Game.Online.Spectator private async Task connect() { - if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS) - // disabled for now (see https://github.com/dotnet/runtime/issues/35988) - return; - if (connection != null) return; From 5316d4c3696d7939f47b5364286e3e9c0c463dd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 14:16:34 +0900 Subject: [PATCH 4381/6909] Force using older signalr version for iOS --- osu.iOS/osu.iOS.csproj | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.iOS/osu.iOS.csproj b/osu.iOS/osu.iOS.csproj index 1e9a21865d..2c6489a5d3 100644 --- a/osu.iOS/osu.iOS.csproj +++ b/osu.iOS/osu.iOS.csproj @@ -116,5 +116,9 @@ false + + + + From 6c4acb4d11c404602085f5e9a4132da44ff37faf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 14:45:13 +0900 Subject: [PATCH 4382/6909] Move to props and ignore downgrade warning --- osu.iOS.props | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.iOS.props b/osu.iOS.props index 76c496cd2d..61bb690225 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -73,6 +73,14 @@ + + + NU1605 + + + + + From d1829820e09559791f9436ac6c1c410faf47893c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 14:53:29 +0900 Subject: [PATCH 4383/6909] Remove local changes from csproj --- osu.iOS/osu.iOS.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.iOS/osu.iOS.csproj b/osu.iOS/osu.iOS.csproj index 2c6489a5d3..1e9a21865d 100644 --- a/osu.iOS/osu.iOS.csproj +++ b/osu.iOS/osu.iOS.csproj @@ -116,9 +116,5 @@ false - - - - From 7d0a7f170c388e90cf0f4f004fbd260e02499e92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 14:57:47 +0900 Subject: [PATCH 4384/6909] Avoid overwriting inherited nowarns Co-authored-by: Dan Balasescu --- osu.iOS.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.iOS.props b/osu.iOS.props index 61bb690225..40ecfffcca 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -75,7 +75,7 @@ - NU1605 + $(NoWarn);NU1605 From 8b1dd31bb4b6525ed33973a388423095f87c4b7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 15:20:41 +0900 Subject: [PATCH 4385/6909] Add gitignore ruile for new msbuild generated editorconfig file --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 732b171f69..d122d25054 100644 --- a/.gitignore +++ b/.gitignore @@ -334,3 +334,5 @@ inspectcode # BenchmarkDotNet /BenchmarkDotNet.Artifacts + +*.GeneratedMSBuildEditorConfig.editorconfig From 9343f84a6881111986abf1835a1d241db0adda96 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 15:21:20 +0900 Subject: [PATCH 4386/6909] Add IBindableList.GetBoudCopy to banned symbols for now --- CodeAnalysis/BannedSymbols.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index e34626a59e..47839608c9 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -4,5 +4,6 @@ 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. +M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods. From 487a5ecdba11f6f2f9aee7e89ccf752bd598bb1f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 15:29:14 +0900 Subject: [PATCH 4387/6909] Replace all usage of IBindableList.GetBoundCopy --- osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs | 4 ++-- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 4 ++-- .../Timelines/Summary/Parts/ControlPointPart.cs | 4 ++-- .../Timelines/Summary/Parts/GroupVisualisation.cs | 4 ++-- .../Components/Timeline/TimelineControlPointDisplay.cs | 4 ++-- .../Components/Timeline/TimelineControlPointGroup.cs | 4 ++-- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 4 ++-- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 9 ++++----- 8 files changed, 18 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs index 4a3dc58604..e4dc261363 100644 --- a/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs +++ b/osu.Game.Rulesets.Taiko/Audio/DrumSampleContainer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Audio private readonly ControlPointInfo controlPoints; private readonly Dictionary mappings = new Dictionary(); - private IBindableList samplePoints; + private readonly IBindableList samplePoints = new BindableList(); public DrumSampleContainer(ControlPointInfo controlPoints) { @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.Audio [BackgroundDependencyLoader] private void load() { - samplePoints = controlPoints.SamplePoints.GetBoundCopy(); + samplePoints.BindTo(controlPoints.SamplePoints); samplePoints.BindCollectionChanged((_, __) => recreateMappings(), true); } diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 34444c2fa5..697ceacf0a 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Dashboard { internal class CurrentlyPlayingDisplay : CompositeDrawable { - private IBindableList playingUsers; + private readonly IBindableList playingUsers = new BindableList(); private FillFlowContainer userFlow; @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Dashboard { base.LoadComplete(); - playingUsers = spectatorStreaming.PlayingUsers.GetBoundCopy(); + playingUsers.BindTo(spectatorStreaming.PlayingUsers); playingUsers.BindCollectionChanged((sender, e) => Schedule(() => { switch (e.Action) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs index 8c0e31c04c..ba3ac9113e 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs @@ -14,13 +14,13 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// public class ControlPointPart : TimelinePart { - private IBindableList controlPointGroups; + private readonly IBindableList controlPointGroups = new BindableList(); protected override void LoadBeatmap(WorkingBeatmap beatmap) { base.LoadBeatmap(beatmap); - controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); + controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((sender, args) => { switch (args.Action) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs index b9eb53b697..93fe6f9989 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/GroupVisualisation.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { public readonly ControlPointGroup Group; - private BindableList controlPoints; + private readonly IBindableList controlPoints = new BindableList(); [Resolved] private OsuColour colours { get; set; } @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { base.LoadComplete(); - controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy(); + controlPoints.BindTo(Group.ControlPoints); controlPoints.BindCollectionChanged((_, __) => { if (controlPoints.Count == 0) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 3f13e8e5d4..0da1b43201 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public class TimelineControlPointDisplay : TimelinePart { - private IBindableList controlPointGroups; + private readonly IBindableList controlPointGroups = new BindableList(); public TimelineControlPointDisplay() { @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadBeatmap(beatmap); - controlPointGroups = beatmap.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); + controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((sender, args) => { switch (args.Action) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs index e32616a574..fb69f16792 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointGroup.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public readonly ControlPointGroup Group; - private BindableList controlPoints; + private readonly IBindableList controlPoints = new BindableList(); [Resolved] private OsuColour colours { get; set; } @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadComplete(); - controlPoints = (BindableList)Group.ControlPoints.GetBoundCopy(); + controlPoints.BindTo(Group.ControlPoints); controlPoints.BindCollectionChanged((_, __) => { ClearInternal(); diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 64f9526816..89d3c36250 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -98,7 +98,7 @@ namespace osu.Game.Screens.Edit.Timing private class ControlGroupAttributes : CompositeDrawable { - private readonly IBindableList controlPoints; + private readonly IBindableList controlPoints = new BindableList(); private readonly FillFlowContainer fill; @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Edit.Timing Spacing = new Vector2(2) }; - controlPoints = group.ControlPoints.GetBoundCopy(); + controlPoints.BindTo(group.ControlPoints); } [Resolved] diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index f511382cde..09d861522a 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Edit.Timing private OsuButton deleteButton; private ControlPointTable table; - private IBindableList controlGroups; + private readonly IBindableList controlPointGroups = new BindableList(); [Resolved] private EditorClock clock { get; set; } @@ -124,11 +124,10 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); - controlGroups = Beatmap.Value.Beatmap.ControlPointInfo.Groups.GetBoundCopy(); - - controlGroups.BindCollectionChanged((sender, args) => + controlPointGroups.BindTo(Beatmap.Value.Beatmap.ControlPointInfo.Groups); + controlPointGroups.BindCollectionChanged((sender, args) => { - table.ControlGroups = controlGroups; + table.ControlGroups = controlPointGroups; changeHandler.SaveState(); }, true); } From ce1743ab283d80fb1868d00d3c7b4169fdf17569 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 15:35:42 +0900 Subject: [PATCH 4388/6909] Fix missed usage in test scene --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 1d8231cce7..35473ee76c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -43,7 +43,7 @@ namespace osu.Game.Tests.Visual.Gameplay private Replay replay; - private IBindableList users; + private readonly IBindableList users = new BindableList(); private TestReplayRecorder recorder; @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Visual.Gameplay { replay = new Replay(); - users = streamingClient.PlayingUsers.GetBoundCopy(); + users.BindTo(streamingClient.PlayingUsers); users.BindCollectionChanged((obj, args) => { switch (args.Action) From 7f30140b7e40f53ccf8c4a67524344d7f172bcfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 16:04:15 +0900 Subject: [PATCH 4389/6909] Add new method handling hit state specifically Until now UpdateStateTransforms was applying results offsets to StartTime. This didn't cover the case of a HitObject with duration, where the call would be made with `StartTime + hitOffset` rather than `EndTime + hitOffset`. To resolve this, a new method has been added which should be used to handle hit-specific state transforms. --- .../Rulesets/Judgements/JudgementResult.cs | 6 +++ .../Objects/Drawables/DrawableHitObject.cs | 37 +++++++++++++++---- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/JudgementResult.cs b/osu.Game/Rulesets/Judgements/JudgementResult.cs index 3a35fd4433..e3b2501cdc 100644 --- a/osu.Game/Rulesets/Judgements/JudgementResult.cs +++ b/osu.Game/Rulesets/Judgements/JudgementResult.cs @@ -36,6 +36,12 @@ namespace osu.Game.Rulesets.Judgements /// public double TimeOffset { get; internal set; } + /// + /// The absolute time at which this occurred. + /// Equal to the (end) time of the + . + /// + public double TimeAbsolute => HitObject.GetEndTime() + TimeOffset; + /// /// The combo prior to this occurring. /// diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1ef6c8c207..5939443cf1 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -255,18 +255,19 @@ namespace osu.Game.Rulesets.Objects.Drawables base.ClearTransformsAfter(double.MinValue, true); using (BeginAbsoluteSequence(transformTime, true)) - { UpdateInitialTransforms(); - var judgementOffset = Result?.TimeOffset ?? 0; + using (BeginAbsoluteSequence(StateUpdateTime, true)) + UpdateStateTransforms(newState); - using (BeginDelayedSequence(InitialLifetimeOffset + judgementOffset, true)) - { - UpdateStateTransforms(newState); - state.Value = newState; - } + if (newState != ArmedState.Idle) + { + using (BeginAbsoluteSequence(HitStateUpdateTime, true)) + UpdateHitStateTransforms(newState); } + state.Value = newState; + if (LifetimeEnd == double.MaxValue && (state.Value != ArmedState.Idle || HitObject.HitWindows == null)) Expire(); @@ -301,6 +302,16 @@ namespace osu.Game.Rulesets.Objects.Drawables { } + /// + /// Apply transforms based on the current . This call is offset by (HitObject.EndTime + Result.Offset), equivalent to when the user hit the object. + /// This method is only called on or . + /// Previous states are automatically cleared. + /// + /// The new armed state. + protected virtual void UpdateHitStateTransforms(ArmedState state) + { + } + public override void ClearTransformsAfter(double time, bool propagateChildren = false, string targetMember = null) { // Parent calls to this should be blocked for safety, as we are manually handling this in updateState. @@ -454,6 +465,18 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual double InitialLifetimeOffset => 10000; + /// + /// The time at which state transforms should be applied that line up to 's StartTime. + /// This is used to offset calls to . + /// + public double StateUpdateTime => HitObject.StartTime; + + /// + /// The time at which judgement dependent state transforms should be applied. This is equivalent of the (end) time of the object, in addition to any judgement offset. + /// This is used to offset calls to . + /// + public double HitStateUpdateTime => Result?.TimeAbsolute ?? HitObject.GetEndTime(); + /// /// Will be called at least once after this has become not alive. /// From 3b6cf95f49781dd8af3840cc7a93308c426e2756 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 16:27:47 +0900 Subject: [PATCH 4390/6909] Remove parameter from StartTime method and add obsoleted previous version --- .../Objects/Drawables/DrawableHitObject.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5939443cf1..318bb323aa 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -257,8 +257,13 @@ namespace osu.Game.Rulesets.Objects.Drawables using (BeginAbsoluteSequence(transformTime, true)) UpdateInitialTransforms(); - using (BeginAbsoluteSequence(StateUpdateTime, true)) +#pragma warning disable 618 + using (BeginAbsoluteSequence(StateUpdateTime + (Result?.TimeOffset ?? 0), true)) UpdateStateTransforms(newState); +#pragma warning restore 618 + + using (BeginAbsoluteSequence(StateUpdateTime, true)) + UpdateStartTimeStateTransforms(); if (newState != ArmedState.Idle) { @@ -298,13 +303,23 @@ namespace osu.Game.Rulesets.Objects.Drawables /// In the case of a non-idle , and if was not set during this call, will be invoked. /// /// The new armed state. + [Obsolete("Use UpdateStartTimeStateTransforms and UpdateHitStateTransforms instead")] // Can be removed 20210504 protected virtual void UpdateStateTransforms(ArmedState state) { } + /// + /// Apply passive transforms at the 's StartTime. + /// This is called each time changes. + /// Previous states are automatically cleared. + /// + protected virtual void UpdateStartTimeStateTransforms() + { + } + /// /// Apply transforms based on the current . This call is offset by (HitObject.EndTime + Result.Offset), equivalent to when the user hit the object. - /// This method is only called on or . + /// This method is only called on or . If was not set during this call, will be invoked. /// Previous states are automatically cleared. /// /// The new armed state. From 831325978a4ae9c1c4b599d65d6bcc779ee9f012 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 16:39:39 +0900 Subject: [PATCH 4391/6909] Always execute UpdateHitStateTransforms (even for idle) --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 318bb323aa..0a7702d971 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -265,11 +265,8 @@ namespace osu.Game.Rulesets.Objects.Drawables using (BeginAbsoluteSequence(StateUpdateTime, true)) UpdateStartTimeStateTransforms(); - if (newState != ArmedState.Idle) - { - using (BeginAbsoluteSequence(HitStateUpdateTime, true)) - UpdateHitStateTransforms(newState); - } + using (BeginAbsoluteSequence(HitStateUpdateTime, true)) + UpdateHitStateTransforms(newState); state.Value = newState; @@ -319,7 +316,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Apply transforms based on the current . This call is offset by (HitObject.EndTime + Result.Offset), equivalent to when the user hit the object. - /// This method is only called on or . If was not set during this call, will be invoked. + /// If was not set during this call, will be invoked. /// Previous states are automatically cleared. /// /// The new armed state. From 68b7efe4d58034e303aba19a61655ca6c1bbfd52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 16:49:34 +0900 Subject: [PATCH 4392/6909] Change order of execution to be chronological --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 0a7702d971..f77953a5c0 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -257,14 +257,14 @@ namespace osu.Game.Rulesets.Objects.Drawables using (BeginAbsoluteSequence(transformTime, true)) UpdateInitialTransforms(); + using (BeginAbsoluteSequence(StateUpdateTime, true)) + UpdateStartTimeStateTransforms(); + #pragma warning disable 618 using (BeginAbsoluteSequence(StateUpdateTime + (Result?.TimeOffset ?? 0), true)) UpdateStateTransforms(newState); #pragma warning restore 618 - using (BeginAbsoluteSequence(StateUpdateTime, true)) - UpdateStartTimeStateTransforms(); - using (BeginAbsoluteSequence(HitStateUpdateTime, true)) UpdateHitStateTransforms(newState); From a3dc1d5730cda9e9fa1860befd66435b16144774 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 16:19:07 +0900 Subject: [PATCH 4393/6909] Update existing implementations --- .../Drawables/DrawableCatchHitObject.cs | 24 +++++-------- .../Edit/ManiaBeatSnapGrid.cs | 4 --- .../Objects/Drawables/DrawableBarLine.cs | 5 ++- .../Objects/Drawables/DrawableHoldNote.cs | 6 ---- .../Objects/Drawables/DrawableHoldNoteHead.cs | 6 ++-- .../Drawables/DrawableManiaHitObject.cs | 2 +- .../Objects/Drawables/DrawableHitCircle.cs | 7 +--- .../Objects/Drawables/DrawableOsuHitObject.cs | 13 +++---- .../Objects/Drawables/DrawableSlider.cs | 34 ++++++++++--------- .../Objects/Drawables/DrawableSliderRepeat.cs | 4 +-- .../Objects/Drawables/DrawableSliderTail.cs | 6 ++-- .../Objects/Drawables/DrawableSliderTick.cs | 4 +-- .../Objects/Drawables/DrawableSpinner.cs | 7 ++-- .../Objects/Drawables/DrawableBarLine.cs | 2 +- .../Objects/Drawables/DrawableDrumRoll.cs | 4 +-- .../Objects/Drawables/DrawableDrumRollTick.cs | 2 +- .../Objects/Drawables/DrawableHit.cs | 2 +- .../Objects/Drawables/DrawableSwell.cs | 16 ++++----- 18 files changed, 58 insertions(+), 90 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index ac1f11e09f..7922510a49 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -6,9 +6,8 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; @@ -90,22 +89,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables ApplyResult(r => r.Type = CheckPosition.Invoke(HitObject) ? r.Judgement.MaxResult : r.Judgement.MinResult); } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { - var endTime = HitObject.GetEndTime(); - - using (BeginAbsoluteSequence(endTime, true)) + switch (state) { - switch (state) - { - case ArmedState.Miss: - this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out); - break; + case ArmedState.Miss: + this.FadeOut(250).RotateTo(Rotation * 2, 250, Easing.Out); + break; - case ArmedState.Hit: - this.FadeOut(); - break; - } + case ArmedState.Hit: + this.FadeOut(); + break; } } } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs index 2028cae9a5..afc08dcc96 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBeatSnapGrid.cs @@ -204,10 +204,6 @@ namespace osu.Game.Rulesets.Mania.Edit protected override void UpdateInitialTransforms() { // don't perform any fading – we are handling that ourselves. - } - - protected override void UpdateStateTransforms(ArmedState state) - { LifetimeEnd = HitObject.StartTime + visible_range; } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs index 08b5b75f9c..074cbf6bd6 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableBarLine.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 osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Objects.Drawables; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Mania.Objects.Drawables @@ -71,6 +70,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { } - protected override void UpdateStateTransforms(ArmedState state) => this.FadeOut(150); + protected override void UpdateStartTimeStateTransforms() => this.FadeOut(150); } } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index f6d539c91b..d9d740c145 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -229,12 +229,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } } - protected override void UpdateStateTransforms(ArmedState state) - { - using (BeginDelayedSequence(HitObject.Duration, true)) - base.UpdateStateTransforms(state); - } - protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Tail.AllJudged) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs index cd56b81e10..75dcf0e55e 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteHead.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Objects.Drawables; - namespace osu.Game.Rulesets.Mania.Objects.Drawables { /// @@ -19,8 +17,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateInitialTransforms() { + base.UpdateInitialTransforms(); + // This hitobject should never expire, so this is just a safe maximum. LifetimeEnd = LifetimeStart + 30000; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs index 27960b3f3a..1550faee50 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableManiaHitObject.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables Anchor = Origin = e.NewValue == ScrollingDirection.Up ? Anchor.TopCentre : Anchor.BottomCentre; } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { switch (state) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index b5ac26c824..f56da65279 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -164,19 +164,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ApproachCircle.Expire(true); } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { - base.UpdateStateTransforms(state); - Debug.Assert(HitObject.HitWindows != null); switch (state) { case ArmedState.Idle: this.Delay(HitObject.TimePreempt).FadeOut(500); - - Expire(true); - HitArea.HitAction = null; break; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 45c664ba3b..3730966fbb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -51,17 +51,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength); - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateInitialTransforms() { - base.UpdateStateTransforms(state); + base.UpdateInitialTransforms(); - switch (state) - { - case ArmedState.Idle: - // Manually set to reduce the number of future alive objects to a bare minimum. - LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; - break; - } + // Manually set to reduce the number of future alive objects to a bare minimum. + LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; } /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index b00d12983d..ddf3950689 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -253,29 +253,31 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.PlaySamples(); } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateStartTimeStateTransforms() { - base.UpdateStateTransforms(state); + base.UpdateStartTimeStateTransforms(); Ball.FadeIn(); Ball.ScaleTo(HitObject.Scale); + } - using (BeginDelayedSequence(slider.Duration, true)) + protected override void UpdateHitStateTransforms(ArmedState state) + { + base.UpdateHitStateTransforms(state); + + const float fade_out_time = 450; + + // intentionally pile on an extra FadeOut to make it happen much faster. + Ball.FadeOut(fade_out_time / 4, Easing.Out); + + switch (state) { - const float fade_out_time = 450; - - // intentionally pile on an extra FadeOut to make it happen much faster. - Ball.FadeOut(fade_out_time / 4, Easing.Out); - - switch (state) - { - case ArmedState.Hit: - Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out); - break; - } - - this.FadeOut(fade_out_time, Easing.OutQuint); + case ArmedState.Hit: + Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out); + break; } + + this.FadeOut(fade_out_time, Easing.OutQuint); } public Drawable ProxiedLayer => HeadCircle.ProxiedLayer; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 2a88f11f69..20159d18c8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -77,9 +77,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ); } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { - base.UpdateStateTransforms(state); + base.UpdateHitStateTransforms(state); switch (state) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index f5bcecccdf..3acd3aefaf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -67,9 +67,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables circlePiece.FadeInFromZero(HitObject.TimeFadeIn); } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { - base.UpdateStateTransforms(state); + base.UpdateHitStateTransforms(state); Debug.Assert(HitObject.HitWindows != null); @@ -77,8 +77,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { case ArmedState.Idle: this.Delay(HitObject.TimePreempt).FadeOut(500); - - Expire(true); break; case ArmedState.Miss: diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 9b68b446a4..b1450f61b1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -72,9 +72,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.ScaleTo(0.5f).ScaleTo(1f, ANIM_DURATION * 4, Easing.OutElasticHalf); } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { - base.UpdateStateTransforms(state); + base.UpdateHitStateTransforms(state); switch (state) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 936bfaeb86..e841473cb1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -142,12 +142,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { - base.UpdateStateTransforms(state); + base.UpdateHitStateTransforms(state); - using (BeginDelayedSequence(Spinner.Duration, true)) - this.FadeOut(160); + this.FadeOut(160); // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback. isSpinning?.TriggerChange(); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs index 1e08e921a6..aadcc420df 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs @@ -58,6 +58,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables }); } - protected override void UpdateStateTransforms(ArmedState state) => this.FadeOut(150); + protected override void UpdateHitStateTransforms(ArmedState state) => this.FadeOut(150); } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 8f268dc1c7..c596fa2c7c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -135,13 +135,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = r.Judgement.MinResult); } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { switch (state) { case ArmedState.Hit: case ArmedState.Miss: - this.Delay(HitObject.Duration).FadeOut(100); + this.FadeOut(100); break; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 9d7dcc7218..bf44a80037 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ApplyResult(r => r.Type = r.Judgement.MaxResult); } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { switch (state) { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index bb42240f25..4a3759794b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -193,7 +193,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables pressHandledThisFrame = false; } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { Debug.Assert(HitObject.HitWindows != null); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 8ee4a5db71..ff0a27023d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -215,15 +215,15 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override void UpdateInitialTransforms() + protected override void UpdateStartTimeStateTransforms() { - base.UpdateInitialTransforms(); + base.UpdateStartTimeStateTransforms(); - using (BeginAbsoluteSequence(HitObject.StartTime - ring_appear_offset, true)) + using (BeginDelayedSequence(-ring_appear_offset, true)) targetRing.ScaleTo(target_ring_scale, 400, Easing.OutQuint); } - protected override void UpdateStateTransforms(ArmedState state) + protected override void UpdateHitStateTransforms(ArmedState state) { const double transition_duration = 300; @@ -235,12 +235,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables case ArmedState.Miss: case ArmedState.Hit: - using (BeginDelayedSequence(HitObject.Duration, true)) - { - this.FadeOut(transition_duration, Easing.Out); - bodyContainer.ScaleTo(1.4f, transition_duration); - } - + this.FadeOut(transition_duration, Easing.Out); + bodyContainer.ScaleTo(1.4f, transition_duration); break; } } From 65fb8628e0f4f8d41d15e6fc1f2d4cb21a87bcc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 17:30:11 +0900 Subject: [PATCH 4394/6909] Use HitStateUpdateTime --- .../Objects/Drawables/Pieces/MainCirclePiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs index e2345de886..bb92f8afb8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -67,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private void updateState(ValueChangedEvent state) { - using (BeginAbsoluteSequence(drawableObject.HitObject.StartTime + (drawableObject.Result?.TimeOffset ?? 0), true)) + using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) { glow.FadeOut(400); From f9fc58c45c01c2547f410e7b1564464c3cb834ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 17:30:23 +0900 Subject: [PATCH 4395/6909] Apply same fix to LegacyMainCirclePiece --- .../Skinning/LegacyMainCirclePiece.cs | 44 +++++++++++-------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 382d6e53cc..a2ae6f4021 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -41,11 +41,14 @@ namespace osu.Game.Rulesets.Osu.Skinning private readonly Bindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); + [Resolved] + private DrawableHitObject drawableObject { get; set; } + [Resolved] private ISkinSource skin { get; set; } [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) + private void load() { OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; @@ -143,28 +146,31 @@ namespace osu.Game.Rulesets.Osu.Skinning { const double legacy_fade_duration = 240; - switch (state.NewValue) + using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) { - case ArmedState.Hit: - circleSprites.FadeOut(legacy_fade_duration, Easing.Out); - circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + switch (state.NewValue) + { + case ArmedState.Hit: + circleSprites.FadeOut(legacy_fade_duration, Easing.Out); + circleSprites.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - if (hasNumber) - { - var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; - - if (legacyVersion >= 2.0m) - // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. - hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); - else + if (hasNumber) { - // old skins scale and fade it normally along other pieces. - hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); - hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); - } - } + var legacyVersion = skin.GetConfig(LegacySetting.Version)?.Value; - break; + if (legacyVersion >= 2.0m) + // legacy skins of version 2.0 and newer only apply very short fade out to the number piece. + hitCircleText.FadeOut(legacy_fade_duration / 4, Easing.Out); + else + { + // old skins scale and fade it normally along other pieces. + hitCircleText.FadeOut(legacy_fade_duration, Easing.Out); + hitCircleText.ScaleTo(1.4f, legacy_fade_duration, Easing.Out); + } + } + + break; + } } } } From d19b799f44edafd9b04c362a7e00bc65c38c0c69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 4 Nov 2020 17:53:03 +0900 Subject: [PATCH 4396/6909] Invert boolean logic --- .../Edit/Compose/Components/BlueprintContainer.cs | 9 +++++---- .../Screens/Edit/Compose/Components/SelectionHandler.cs | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index d8f7137fa6..e7da220946 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -116,7 +116,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override bool OnMouseDown(MouseDownEvent e) { - if (beginClickSelection(e)) return true; + if (!beginClickSelection(e)) return true; prepareSelectionMovement(); @@ -292,23 +292,24 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Attempts to select any hovered blueprints. /// /// The input event that triggered this selection. + /// Whether a selection was performed. private bool beginClickSelection(MouseButtonEvent e) { Debug.Assert(!clickSelectionBegan); - bool rightClickHandled = false; + bool selectedPerformed = true; foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) { if (blueprint.IsHovered) { - rightClickHandled |= SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); + selectedPerformed &= SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); clickSelectionBegan = true; break; } } - return rightClickHandled; + return selectedPerformed; } /// diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 5bf6c52e11..d5c83576e2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -219,13 +219,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// The blueprint. /// The input state at the point of selection. - /// Whether right click was handled. + /// Whether a selection was performed. internal bool HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) { if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) { handleQuickDeletion(blueprint); - return true; + return false; } if (state.Keyboard.ControlPressed && state.Mouse.IsPressed(MouseButton.Left)) @@ -233,7 +233,7 @@ namespace osu.Game.Screens.Edit.Compose.Components else ensureSelected(blueprint); - return false; + return true; } private void handleQuickDeletion(SelectionBlueprint blueprint) From 9a7fcadabc60b0383d72d2933de2df4c38c62e4b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 13:51:46 +0900 Subject: [PATCH 4397/6909] Use BDL for top-level osu! DHOs --- .../TestSceneSliderPlacementBlueprint.cs | 2 +- .../TestSceneSlider.cs | 18 +++--- .../Sliders/SliderSelectionBlueprint.cs | 2 +- .../Objects/Drawables/DrawableHitCircle.cs | 39 ++++-------- .../Objects/Drawables/DrawableOsuHitObject.cs | 22 ++++++- .../Objects/Drawables/DrawableSlider.cs | 60 ++++++++----------- .../Objects/Drawables/DrawableSliderHead.cs | 5 +- .../Objects/Drawables/DrawableSliderRepeat.cs | 25 +++----- .../Objects/Drawables/DrawableSliderTail.cs | 23 +++---- .../Objects/Drawables/DrawableSliderTick.cs | 17 +++--- .../Objects/Drawables/DrawableSpinner.cs | 48 +++++++-------- .../Drawables/Pieces/DefaultSpinnerDisc.cs | 2 +- .../Skinning/LegacyNewStyleSpinner.cs | 3 +- .../Objects/Drawables/DrawableHitObject.cs | 4 +- 14 files changed, 118 insertions(+), 152 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index 49d7d9249c..a452f93676 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor 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; + private Slider getSlider() => HitObjectContainer.Count > 0 ? ((DrawableSlider)HitObjectContainer[0]).HitObject : null; protected override DrawableHitObject CreateHitObject(HitObject hitObject) => new DrawableSlider((Slider)hitObject); protected override PlacementBlueprint CreateBlueprint() => new SliderPlacementBlueprint(); diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index c9e112f76d..c400e2f2ea 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -112,10 +112,10 @@ namespace osu.Game.Rulesets.Osu.Tests new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE }, }); - 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("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0); + AddAssert("head samples updated", () => assertSamples(slider.HitObject.HeadCircle)); + AddAssert("tick samples not updated", () => slider.HitObject.NestedHitObjects.OfType().All(assertTickSamples)); + AddAssert("repeat samples updated", () => slider.HitObject.NestedHitObjects.OfType().All(assertSamples)); + AddAssert("tail has no samples", () => slider.HitObject.TailCircle.Samples.Count == 0); static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick"; @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Osu.Tests slider = (DrawableSlider)createSlider(repeats: 1); for (int i = 0; i < 2; i++) - ((Slider)slider.HitObject).NodeSamples.Add(new List { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } }); + slider.HitObject.NodeSamples.Add(new List { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } }); Add(slider); }); @@ -147,10 +147,10 @@ namespace osu.Game.Rulesets.Osu.Tests new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE }, }); - 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("tail has no samples", () => ((Slider)slider.HitObject).TailCircle.Samples.Count == 0); + AddAssert("head samples not updated", () => assertSamples(slider.HitObject.HeadCircle)); + AddAssert("tick samples not updated", () => slider.HitObject.NestedHitObjects.OfType().All(assertTickSamples)); + AddAssert("repeat samples not updated", () => slider.HitObject.NestedHitObjects.OfType().All(assertSamples)); + AddAssert("tail has no samples", () => slider.HitObject.TailCircle.Samples.Count == 0); static bool assertTickSamples(SliderTick tick) => tick.Samples.Single().Name == "slidertick"; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index d3fb5defae..f851c7bfc9 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -82,7 +82,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders protected override void OnSelected() { - AddInternal(ControlPointVisualiser = new PathControlPointVisualiser((Slider)slider.HitObject, true) + AddInternal(ControlPointVisualiser = new PathControlPointVisualiser(slider.HitObject, true) { RemoveControlPointsRequested = removeControlPoints }); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index b5ac26c824..0c26e2b52f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input; @@ -21,28 +20,25 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach { - public ApproachCircle ApproachCircle { get; } - - private readonly IBindable positionBindable = new Bindable(); - private readonly IBindable stackHeightBindable = new Bindable(); - private readonly IBindable scaleBindable = new BindableFloat(); - public OsuAction? HitAction => HitArea.HitAction; - - public readonly HitReceptor HitArea; - public readonly SkinnableDrawable CirclePiece; - private readonly Container scaleContainer; - protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; + public ApproachCircle ApproachCircle { get; private set; } + public HitReceptor HitArea { get; private set; } + public SkinnableDrawable CirclePiece { get; private set; } + + private Container scaleContainer; private InputManager inputManager; public DrawableHitCircle(HitCircle h) : base(h) { - Origin = Anchor.Centre; + } - Position = HitObject.StackedPosition; + [BackgroundDependencyLoader] + private void load() + { + Origin = Anchor.Centre; InternalChildren = new Drawable[] { @@ -75,19 +71,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }; Size = HitArea.DrawSize; - } - - [BackgroundDependencyLoader] - private void load() - { - positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); - stackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); - scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); - - positionBindable.BindTo(HitObject.PositionBindable); - stackHeightBindable.BindTo(HitObject.StackHeightBindable); - scaleBindable.BindTo(HitObject.ScaleBindable); + PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); + StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 45c664ba3b..c83bdf17d5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -2,18 +2,24 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Drawables; 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 osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableOsuHitObject : DrawableHitObject { - private readonly ShakeContainer shakeContainer; + public readonly IBindable PositionBindable = new Bindable(); + public readonly IBindable StackHeightBindable = new Bindable(); + public readonly IBindable ScaleBindable = new BindableFloat(); + public readonly IBindable IndexInCurrentComboBindable = new Bindable(); // Must be set to update IsHovered as it's used in relax mdo to detect osu hit objects. public override bool HandlePositionalInput => true; @@ -26,16 +32,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables /// public Func CheckHittable; + private ShakeContainer shakeContainer; + protected DrawableOsuHitObject(OsuHitObject hitObject) : base(hitObject) { + } + + [BackgroundDependencyLoader] + private void load() + { + Alpha = 0; + base.AddInternal(shakeContainer = new ShakeContainer { ShakeDuration = 30, RelativeSizeAxes = Axes.Both }); - Alpha = 0; + IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable); + PositionBindable.BindTo(HitObject.PositionBindable); + StackHeightBindable.BindTo(HitObject.StackHeightBindable); + ScaleBindable.BindTo(HitObject.ScaleBindable); } // Forward all internal management to shakeContainer. diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index b00d12983d..b743d2e4d0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -20,62 +20,50 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach { + public new Slider HitObject => (Slider)base.HitObject; + public DrawableSliderHead HeadCircle => headContainer.Child; public DrawableSliderTail TailCircle => tailContainer.Child; - public readonly SliderBall Ball; - public readonly SkinnableDrawable Body; + public SliderBall Ball { get; private set; } + public SkinnableDrawable Body { get; private set; } public override bool DisplayResult => false; private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; - private readonly Container headContainer; - private readonly Container tailContainer; - private readonly Container tickContainer; - private readonly Container repeatContainer; - - private readonly Slider slider; - - private readonly IBindable positionBindable = new Bindable(); - private readonly IBindable stackHeightBindable = new Bindable(); - private readonly IBindable scaleBindable = new BindableFloat(); + private Container headContainer; + private Container tailContainer; + private Container tickContainer; + private Container repeatContainer; public DrawableSlider(Slider s) : base(s) { - slider = s; - - Position = s.StackedPosition; + } + [BackgroundDependencyLoader] + private void load() + { InternalChildren = new Drawable[] { Body = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SliderBody), _ => new DefaultSliderBody(), confineMode: ConfineMode.NoScaling), tailContainer = new Container { RelativeSizeAxes = Axes.Both }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, - Ball = new SliderBall(s, this) + Ball = new SliderBall(HitObject, this) { GetInitialHitAction = () => HeadCircle.HitAction, BypassAutoSizeAxes = Axes.Both, - Scale = new Vector2(s.Scale), AlwaysPresent = true, Alpha = 0 }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, }; - } - [BackgroundDependencyLoader] - private void load() - { - positionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); - stackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); - scaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue)); - - positionBindable.BindTo(HitObject.PositionBindable); - stackHeightBindable.BindTo(HitObject.StackHeightBindable); - scaleBindable.BindTo(HitObject.ScaleBindable); + PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); + StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); + ScaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue), true); AccentColour.BindValueChanged(colour => { @@ -162,20 +150,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables switch (hitObject) { case SliderTailCircle tail: - return new DrawableSliderTail(slider, tail); + return new DrawableSliderTail(tail); case SliderHeadCircle head: - return new DrawableSliderHead(slider, head) + return new DrawableSliderHead(HitObject, head) { OnShake = Shake, CheckHittable = (d, t) => CheckHittable?.Invoke(d, t) ?? true }; case SliderTick tick: - return new DrawableSliderTick(tick) { Position = tick.Position - slider.Position }; + return new DrawableSliderTick(tick) { Position = tick.Position - HitObject.Position }; case SliderRepeat repeat: - return new DrawableSliderRepeat(repeat, this) { Position = repeat.Position - slider.Position }; + return new DrawableSliderRepeat(repeat, this) { Position = repeat.Position - HitObject.Position }; } return base.CreateNestedHitObject(hitObject); @@ -200,14 +188,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // keep the sliding sample playing at the current tracking position slidingSample.Balance.Value = CalculateSamplePlaybackBalance(Ball.X / OsuPlayfield.BASE_SIZE.X); - double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); + double completionProgress = Math.Clamp((Time.Current - HitObject.StartTime) / HitObject.Duration, 0, 1); Ball.UpdateProgress(completionProgress); sliderBody?.UpdateProgress(completionProgress); foreach (DrawableHitObject hitObject in NestedHitObjects) { - if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(slider.Path.PositionAt(sliderBody?.SnakedStart ?? 0), slider.Path.PositionAt(sliderBody?.SnakedEnd ?? 0)); + if (hitObject is ITrackSnaking s) s.UpdateSnakingPosition(HitObject.Path.PositionAt(sliderBody?.SnakedStart ?? 0), HitObject.Path.PositionAt(sliderBody?.SnakedEnd ?? 0)); if (hitObject is IRequireTracking t) t.Tracking = Ball.Tracking; } @@ -239,7 +227,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (userTriggered || Time.Current < slider.EndTime) + if (userTriggered || Time.Current < HitObject.EndTime) return; ApplyResult(r => r.Type = r.Judgement.MaxResult); @@ -260,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Ball.FadeIn(); Ball.ScaleTo(HitObject.Scale); - using (BeginDelayedSequence(slider.Duration, true)) + using (BeginDelayedSequence(HitObject.Duration, true)) { const float fade_out_time = 450; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 04f563eeec..49ed9f12e3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -5,13 +5,11 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Types; -using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderHead : DrawableHitCircle { - private readonly IBindable positionBindable = new Bindable(); private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; @@ -27,10 +25,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load() { - positionBindable.BindTo(HitObject.PositionBindable); pathVersion.BindTo(slider.Path.Version); - positionBindable.BindValueChanged(_ => updatePosition()); + PositionBindable.BindValueChanged(_ => updatePosition()); pathVersion.BindValueChanged(_ => updatePosition(), true); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 2a88f11f69..b57bb8dbbf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; @@ -22,9 +21,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private double animDuration; - private readonly Drawable scaleContainer; - - public readonly Drawable CirclePiece; + public Drawable CirclePiece { get; private set; } + private Drawable scaleContainer; + private ReverseArrowPiece arrow; public override bool DisplayResult => false; @@ -33,10 +32,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { this.sliderRepeat = sliderRepeat; this.drawableSlider = drawableSlider; + } - Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); - + [BackgroundDependencyLoader] + private void load() + { Origin = Anchor.Centre; + Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); InternalChild = scaleContainer = new Container { @@ -50,15 +52,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables arrow = new ReverseArrowPiece(), } }; - } - private readonly IBindable scaleBindable = new BindableFloat(); - - [BackgroundDependencyLoader] - private void load() - { - scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); - scaleBindable.BindTo(HitObject.ScaleBindable); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); } protected override void CheckForResult(bool userTriggered, double timeOffset) @@ -100,8 +95,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private bool hasRotation; - private readonly ReverseArrowPiece arrow; - public void UpdateSnakingPosition(Vector2 start, Vector2 end) { // When the repeat is hit, the arrow should fade out on spot rather than following the slider diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index f5bcecccdf..7e30f4e132 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -3,7 +3,6 @@ using System.Diagnostics; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; @@ -23,18 +22,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool Tracking { get; set; } - private readonly IBindable scaleBindable = new BindableFloat(); + private SkinnableDrawable circlePiece; + private Container scaleContainer; - private readonly SkinnableDrawable circlePiece; - - private readonly Container scaleContainer; - - public DrawableSliderTail(Slider slider, SliderTailCircle tailCircle) + public DrawableSliderTail(SliderTailCircle tailCircle) : base(tailCircle) { this.tailCircle = tailCircle; - Origin = Anchor.Centre; + } + [BackgroundDependencyLoader] + private void load() + { + Origin = Anchor.Centre; Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); InternalChildren = new Drawable[] @@ -51,13 +51,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } }, }; - } - [BackgroundDependencyLoader] - private void load() - { - scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); - scaleBindable.BindTo(HitObject.ScaleBindable); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 9b68b446a4..81d12f3432 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -2,7 +2,6 @@ // 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.Objects.Drawables; using osuTK; @@ -23,10 +22,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; - private readonly SkinnableDrawable scaleContainer; + private SkinnableDrawable scaleContainer; public DrawableSliderTick(SliderTick sliderTick) : base(sliderTick) + { + } + + [BackgroundDependencyLoader] + private void load() { Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); Origin = Anchor.Centre; @@ -49,15 +53,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, }; - } - private readonly IBindable scaleBindable = new BindableFloat(); - - [BackgroundDependencyLoader] - private void load() - { - scaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); - scaleBindable.BindTo(HitObject.ScaleBindable); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 936bfaeb86..50ea45c378 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -16,34 +16,33 @@ using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinner : DrawableOsuHitObject { - protected readonly Spinner Spinner; + public new Spinner HitObject => (Spinner)base.HitObject; - private readonly Container ticks; + public SpinnerRotationTracker RotationTracker { get; private set; } + public SpinnerSpmCounter SpmCounter { get; private set; } - public readonly SpinnerRotationTracker RotationTracker; - public readonly SpinnerSpmCounter SpmCounter; - private readonly SpinnerBonusDisplay bonusDisplay; - - private readonly IBindable positionBindable = new Bindable(); + private Container ticks; + private SpinnerBonusDisplay bonusDisplay; + private Bindable isSpinning; private bool spinnerFrequencyModulate; public DrawableSpinner(Spinner s) : base(s) + { + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) { Origin = Anchor.Centre; - Position = s.Position; - RelativeSizeAxes = Axes.Both; - Spinner = s; - InternalChildren = new Drawable[] { ticks = new Container(), @@ -55,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Children = new Drawable[] { new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()), - RotationTracker = new SpinnerRotationTracker(Spinner) + RotationTracker = new SpinnerRotationTracker(HitObject) } }, SpmCounter = new SpinnerSpmCounter @@ -72,9 +71,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Y = -120, } }; - } - private Bindable isSpinning; + PositionBindable.BindValueChanged(pos => Position = pos.NewValue, true); + } protected override void LoadComplete() { @@ -146,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateStateTransforms(state); - using (BeginDelayedSequence(Spinner.Duration, true)) + using (BeginDelayedSequence(HitObject.Duration, true)) this.FadeOut(160); // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback. @@ -173,13 +172,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return base.CreateNestedHitObject(hitObject); } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - positionBindable.BindValueChanged(pos => Position = pos.NewValue); - positionBindable.BindTo(HitObject.PositionBindable); - } - protected override void ApplySkin(ISkinSource skin, bool allowFallback) { base.ApplySkin(skin, allowFallback); @@ -193,12 +185,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { get { - if (Spinner.SpinsRequired == 0) + if (HitObject.SpinsRequired == 0) // some spinners are so short they can't require an integer spin count. // these become implicitly hit. return 1; - return Math.Clamp(RotationTracker.RateAdjustedRotation / 360 / Spinner.SpinsRequired, 0, 1); + return Math.Clamp(RotationTracker.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1); } } @@ -208,7 +200,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables RotationTracker.Complete.Value = Progress >= 1; - if (userTriggered || Time.Current < Spinner.EndTime) + if (userTriggered || Time.Current < HitObject.EndTime) return; // Trigger a miss result for remaining ticks to avoid infinite gameplay. @@ -223,7 +215,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables r.Type = HitResult.Ok; else if (Progress > .75) r.Type = HitResult.Meh; - else if (Time.Current >= Spinner.EndTime) + else if (Time.Current >= HitObject.EndTime) r.Type = r.Judgement.MinResult; }); } @@ -275,7 +267,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { tick.TriggerResult(true); if (tick is DrawableSpinnerBonusTick) - bonusDisplay.SetBonusCount(spins - Spinner.SpinsRequired); + bonusDisplay.SetBonusCount(spins - HitObject.SpinsRequired); } wholeSpins++; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index e855317544..17a734f0f4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private void load(OsuColour colours, DrawableHitObject drawableHitObject) { drawableSpinner = (DrawableSpinner)drawableHitObject; - spinner = (Spinner)drawableSpinner.HitObject; + spinner = drawableSpinner.HitObject; normalColour = colours.BlueDark; completeColour = colours.YellowLight; diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 56b5571ce1..018dc78ddb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; @@ -79,7 +78,7 @@ namespace osu.Game.Rulesets.Osu.Skinning if (!(drawableHitObject is DrawableSpinner)) return; - var spinner = (Spinner)drawableSpinner.HitObject; + var spinner = drawableSpinner.HitObject; using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) this.FadeOut(); diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1ef6c8c207..cd31e468af 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -125,14 +125,14 @@ namespace osu.Game.Rulesets.Objects.Drawables Result = CreateResult(judgement); if (Result == null) throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); - - LoadSamples(); } protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); + LoadSamples(); + HitObject.DefaultsApplied += onDefaultsApplied; startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy(); From eed9894d3a0bb0d4b68282a171fb286e2d9b176b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 13:58:41 +0900 Subject: [PATCH 4398/6909] Remove usage of case-when (caught me off-gaurd) --- .../Blueprints/Sliders/SliderSelectionBlueprint.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 59b087f68f..1644d5aa4b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -122,9 +122,14 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders rightClickPosition = e.MouseDownPosition; return false; // Allow right click to be handled by context menu - case MouseButton.Left when e.ControlPressed && IsSelected: - placementControlPointIndex = addControlPoint(e.MousePosition); - return true; // Stop input from being handled and modifying the selection + case MouseButton.Left: + if (e.ControlPressed && IsSelected) + { + placementControlPointIndex = addControlPoint(e.MousePosition); + return true; // Stop input from being handled and modifying the selection + } + + break; } return false; From 400fcedf0ff3e7a7de3c73731320124cca8484d1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 14:40:48 +0900 Subject: [PATCH 4399/6909] Remove stored hitobject references from skinnable components --- .../Objects/Drawables/DrawableSlider.cs | 6 ++++- .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/Pieces/CirclePiece.cs | 2 +- .../Drawables/Pieces/DefaultSpinnerDisc.cs | 27 +++++++++---------- .../Drawables/Pieces/MainCirclePiece.cs | 4 +-- .../Drawables/Pieces/PlaySliderBody.cs | 13 +++------ .../Objects/Drawables/Pieces/SliderBall.cs | 12 ++++----- .../Drawables/Pieces/SnakingSliderBody.cs | 24 ++++++++++------- .../Pieces/SpinnerRotationTracker.cs | 10 +++---- .../Skinning/LegacyMainCirclePiece.cs | 5 ++-- .../Skinning/LegacyNewStyleSpinner.cs | 16 +++++------ .../Skinning/LegacyOldStyleSpinner.cs | 10 +++---- .../Skinning/LegacySliderBall.cs | 3 +-- 13 files changed, 65 insertions(+), 69 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index b743d2e4d0..8008d87c6d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -32,6 +32,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; + public readonly IBindable PathVersion = new Bindable(); + private Container headContainer; private Container tailContainer; private Container tickContainer; @@ -51,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables tailContainer = new Container { RelativeSizeAxes = Axes.Both }, tickContainer = new Container { RelativeSizeAxes = Axes.Both }, repeatContainer = new Container { RelativeSizeAxes = Axes.Both }, - Ball = new SliderBall(HitObject, this) + Ball = new SliderBall(this) { GetInitialHitAction = () => HeadCircle.HitAction, BypassAutoSizeAxes = Axes.Both, @@ -61,6 +63,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables headContainer = new Container { RelativeSizeAxes = Axes.Both }, }; + PathVersion.BindTo(HitObject.Path.Version); + PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); ScaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue), true); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 50ea45c378..8dd63018e2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Children = new Drawable[] { new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.SpinnerBody), _ => new DefaultSpinnerDisc()), - RotationTracker = new SpinnerRotationTracker(HitObject) + RotationTracker = new SpinnerRotationTracker(this) } }, SpmCounter = new SpinnerSpmCounter diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index e95cdc7ee3..c455c66e8d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Origin = Anchor.Centre, Texture = textures.Get(@"Gameplay/osu/disc"), }, - new TrianglesPiece((int)drawableHitObject.HitObject.StartTime) + new TrianglesPiece(drawableHitObject.GetHashCode()) { RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 17a734f0f4..4c682d96ce 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -18,8 +18,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { private DrawableSpinner drawableSpinner; - private Spinner spinner; - private const float initial_scale = 1.3f; private const float idle_alpha = 0.2f; private const float tracking_alpha = 0.4f; @@ -52,7 +50,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private void load(OsuColour colours, DrawableHitObject drawableHitObject) { drawableSpinner = (DrawableSpinner)drawableHitObject; - spinner = drawableSpinner.HitObject; normalColour = colours.BlueDark; completeColour = colours.YellowLight; @@ -130,18 +127,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (!(drawableHitObject is DrawableSpinner)) return; - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + using (BeginAbsoluteSequence(drawableSpinner.HitObject.StartTime - drawableSpinner.HitObject.TimePreempt, true)) { this.ScaleTo(initial_scale); this.RotateTo(0); - using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) + using (BeginDelayedSequence(drawableSpinner.HitObject.TimePreempt / 2, true)) { // constant ambient rotation to give the spinner "spinning" character. - this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); + this.RotateTo((float)(25 * drawableSpinner.HitObject.Duration / 2000), drawableSpinner.HitObject.TimePreempt + drawableSpinner.HitObject.Duration); } - using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true)) + using (BeginDelayedSequence(drawableSpinner.HitObject.TimePreempt + drawableSpinner.HitObject.Duration + drawableHitObject.Result.TimeOffset, true)) { switch (state) { @@ -157,26 +154,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + using (BeginAbsoluteSequence(drawableSpinner.HitObject.StartTime - drawableSpinner.HitObject.TimePreempt, true)) { centre.ScaleTo(0); mainContainer.ScaleTo(0); - using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) + using (BeginDelayedSequence(drawableSpinner.HitObject.TimePreempt / 2, true)) { - centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); - mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint); + centre.ScaleTo(0.3f, drawableSpinner.HitObject.TimePreempt / 4, Easing.OutQuint); + mainContainer.ScaleTo(0.2f, drawableSpinner.HitObject.TimePreempt / 4, Easing.OutQuint); - using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) + using (BeginDelayedSequence(drawableSpinner.HitObject.TimePreempt / 2, true)) { - centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint); - mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); + centre.ScaleTo(0.5f, drawableSpinner.HitObject.TimePreempt / 2, Easing.OutQuint); + mainContainer.ScaleTo(1, drawableSpinner.HitObject.TimePreempt / 2, Easing.OutQuint); } } } // transforms we have from completing the spinner will be rolled back, so reapply immediately. - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + using (BeginAbsoluteSequence(drawableSpinner.HitObject.StartTime - drawableSpinner.HitObject.TimePreempt, true)) updateComplete(state == ArmedState.Hit, 0); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs index cb3787a493..d2f4b71f19 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -45,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject) { - OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; + var drawableOsuObject = (DrawableOsuHitObject)drawableObject; state.BindTo(drawableObject.State); state.BindValueChanged(updateState, true); @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces circle.Colour = colour.NewValue; }, true); - indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable); + indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs index cedf2f6e09..29dff53f54 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs @@ -17,23 +17,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private IBindable pathVersion; private IBindable accentColour; - [Resolved] - private DrawableHitObject drawableObject { get; set; } - [Resolved(CanBeNull = true)] private OsuRulesetConfigManager config { get; set; } - private Slider slider; - [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load(ISkinSource skin, DrawableHitObject drawableObject) { - slider = (Slider)drawableObject.HitObject; + var drawableSlider = (DrawableSlider)drawableObject; - scaleBindable = slider.ScaleBindable.GetBoundCopy(); + scaleBindable = drawableSlider.ScaleBindable.GetBoundCopy(); scaleBindable.BindValueChanged(scale => PathRadius = OsuHitObject.OBJECT_RADIUS * scale.NewValue, true); - pathVersion = slider.Path.Version.GetBoundCopy(); + pathVersion = drawableSlider.PathVersion.GetBoundCopy(); pathVersion.BindValueChanged(_ => Refresh()); accentColour = drawableObject.AccentColour.GetBoundCopy(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs index 07dc6021c9..c5bf790377 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -30,15 +30,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces set => ball.Colour = value; } - private readonly Slider slider; private readonly Drawable followCircle; private readonly DrawableSlider drawableSlider; private readonly Drawable ball; - public SliderBall(Slider slider, DrawableSlider drawableSlider = null) + public SliderBall(DrawableSlider drawableSlider) { this.drawableSlider = drawableSlider; - this.slider = slider; Origin = Anchor.Centre; @@ -133,7 +131,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (headCircleHitAction == null) timeToAcceptAnyKeyAfter = null; - var actions = drawableSlider?.OsuActionInputManager?.PressedActions; + var actions = drawableSlider.OsuActionInputManager?.PressedActions; // if the head circle was hit with a specific key, tracking should only occur while that key is pressed. if (headCircleHitAction != null && timeToAcceptAnyKeyAfter == null) @@ -147,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Tracking = // in valid time range - Time.Current >= slider.StartTime && Time.Current < slider.EndTime && + Time.Current >= drawableSlider.HitObject.StartTime && Time.Current < drawableSlider.HitObject.EndTime && // in valid position range lastScreenSpaceMousePosition.HasValue && followCircle.ReceivePositionalInputAt(lastScreenSpaceMousePosition.Value) && // valid action @@ -172,9 +170,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public void UpdateProgress(double completionProgress) { - var newPos = slider.CurvePositionAt(completionProgress); + var newPos = drawableSlider.HitObject.CurvePositionAt(completionProgress); - var diff = lastPosition.HasValue ? lastPosition.Value - newPos : newPos - slider.CurvePositionAt(completionProgress + 0.01f); + var diff = lastPosition.HasValue ? lastPosition.Value - newPos : newPos - drawableSlider.HitObject.CurvePositionAt(completionProgress + 0.01f); if (diff == Vector2.Zero) return; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs index e24fa865ad..2fefc2e08e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs @@ -51,27 +51,30 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces /// private Vector2 snakedPathOffset; - private Slider slider; + private DrawableSlider drawableSlider; [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject) { - slider = (Slider)drawableObject.HitObject; + drawableSlider = (DrawableSlider)drawableObject; Refresh(); } public void UpdateProgress(double completionProgress) { - var span = slider.SpanAt(completionProgress); - var spanProgress = slider.ProgressAt(completionProgress); + if (drawableSlider == null) + return; + + var span = drawableSlider.HitObject.SpanAt(completionProgress); + var spanProgress = drawableSlider.HitObject.ProgressAt(completionProgress); double start = 0; - double end = SnakingIn.Value ? Math.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1; + double end = SnakingIn.Value ? Math.Clamp((Time.Current - (drawableSlider.HitObject.StartTime - drawableSlider.HitObject.TimePreempt)) / (drawableSlider.HitObject.TimePreempt / 3), 0, 1) : 1; - if (span >= slider.SpanCount() - 1) + if (span >= drawableSlider.HitObject.SpanCount() - 1) { - if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1) + if (Math.Min(span, drawableSlider.HitObject.SpanCount() - 1) % 2 == 1) { start = 0; end = SnakingOut.Value ? spanProgress : 1; @@ -87,8 +90,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public void Refresh() { + if (drawableSlider == null) + return; + // Generate the entire curve - slider.Path.GetPathToProgress(CurrentCurve, 0, 1); + drawableSlider.HitObject.Path.GetPathToProgress(CurrentCurve, 0, 1); SetVertices(CurrentCurve); // Force the body to be the final path size to avoid excessive autosize computations @@ -132,7 +138,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces SnakedStart = p0; SnakedEnd = p1; - slider.Path.GetPathToProgress(CurrentCurve, p0, p1); + drawableSlider.HitObject.Path.GetPathToProgress(CurrentCurve, p0, p1); SetVertices(CurrentCurve); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index 05ed38d241..910899c307 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs @@ -15,13 +15,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class SpinnerRotationTracker : CircularContainer { - private readonly Spinner spinner; - public override bool IsPresent => true; // handle input when hidden - public SpinnerRotationTracker(Spinner s) + private readonly DrawableSpinner drawableSpinner; + + public SpinnerRotationTracker(DrawableSpinner drawableSpinner) { - spinner = s; + this.drawableSpinner = drawableSpinner; RelativeSizeAxes = Axes.Both; } @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces /// /// Whether currently in the correct time range to allow spinning. /// - private bool isSpinnableTime => spinner.StartTime <= Time.Current && spinner.EndTime > Time.Current; + private bool isSpinnableTime => drawableSpinner.HitObject.StartTime <= Time.Current && drawableSpinner.HitObject.EndTime > Time.Current; protected override bool OnMouseMove(MouseMoveEvent e) { diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 382d6e53cc..418bf124ab 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -47,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject) { - OsuHitObject osuObject = (OsuHitObject)drawableObject.HitObject; + var drawableOsuObject = (DrawableOsuHitObject)drawableObject; bool allowFallback = false; @@ -111,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Skinning state.BindTo(drawableObject.State); accentColour.BindTo(drawableObject.AccentColour); - indexInCurrentCombo.BindTo(osuObject.IndexInCurrentComboBindable); + indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); Texture getTextureWithFallback(string name) { diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 018dc78ddb..f07b1038fb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -75,23 +75,21 @@ namespace osu.Game.Rulesets.Osu.Skinning private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSpinner)) + if (!(drawableHitObject is DrawableSpinner spinner)) return; - var spinner = drawableSpinner.HitObject; - - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + using (BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt, true)) this.FadeOut(); - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) - this.FadeInFromZero(spinner.TimeFadeIn / 2); + using (BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimeFadeIn / 2, true)) + this.FadeInFromZero(spinner.HitObject.TimeFadeIn / 2); - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + using (BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt, true)) { fixedMiddle.FadeColour(Color4.White); - using (BeginDelayedSequence(spinner.TimePreempt, true)) - fixedMiddle.FadeColour(Color4.Red, spinner.Duration); + using (BeginDelayedSequence(spinner.HitObject.TimePreempt, true)) + fixedMiddle.FadeColour(Color4.Red, spinner.HitObject.Duration); } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 7b0d7acbbc..5fcd8e06b1 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -94,16 +94,14 @@ namespace osu.Game.Rulesets.Osu.Skinning private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSpinner)) + if (!(drawableHitObject is DrawableSpinner spinner)) return; - var spinner = drawableSpinner.HitObject; - - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + using (BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt, true)) this.FadeOut(); - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) - this.FadeInFromZero(spinner.TimeFadeIn / 2); + using (BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimeFadeIn / 2, true)) + this.FadeInFromZero(spinner.HitObject.TimeFadeIn / 2); } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs index 25ab96445a..836069013d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; using osuTK.Graphics; @@ -26,7 +25,7 @@ namespace osu.Game.Rulesets.Osu.Skinning } [BackgroundDependencyLoader] - private void load(ISkinSource skin, DrawableHitObject drawableObject) + private void load(ISkinSource skin) { var ballColour = skin.GetConfig(OsuSkinColour.SliderBall)?.Value ?? Color4.White; From 9ac822beed8432bb5cb00ce9b4fe6aac419bf9a7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 14:49:15 +0900 Subject: [PATCH 4400/6909] Remove AccentColour binding from judgement lighting --- .../Objects/Drawables/DrawableOsuJudgement.cs | 22 ++------- .../Objects/Drawables/SkinnableLighting.cs | 48 +++++++++++++++++++ 2 files changed, 51 insertions(+), 19 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 49535e7fff..98898ce1b4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -2,22 +2,17 @@ // 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.Configuration; using osuTK; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Skinning; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableOsuJudgement : DrawableJudgement { - protected SkinnableSprite Lighting; - - private Bindable lightingColour; + protected SkinnableLighting Lighting { get; private set; } [Resolved] private OsuConfigManager config { get; set; } @@ -34,7 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [BackgroundDependencyLoader] private void load() { - AddInternal(Lighting = new SkinnableSprite("lighting") + AddInternal(Lighting = new SkinnableLighting { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -59,19 +54,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.PrepareForUse(); - lightingColour?.UnbindAll(); - Lighting.ResetAnimation(); - - if (JudgedObject != null) - { - lightingColour = JudgedObject.AccentColour.GetBoundCopy(); - lightingColour.BindValueChanged(colour => Lighting.Colour = Result.IsHit ? colour.NewValue : Color4.Transparent, true); - } - else - { - Lighting.Colour = Color4.White; - } + Lighting.SetColourFrom(JudgedObject, Result); } private double fadeOutDelay; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.cs new file mode 100644 index 0000000000..02dc770285 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SkinnableLighting.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.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Skinning; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables +{ + public class SkinnableLighting : SkinnableSprite + { + private DrawableHitObject targetObject; + private JudgementResult targetResult; + + public SkinnableLighting() + : base("lighting") + { + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateColour(); + } + + /// + /// Updates the lighting colour from a given hitobject and result. + /// + /// The that's been judged. + /// The that was judged with. + public void SetColourFrom(DrawableHitObject targetObject, JudgementResult targetResult) + { + this.targetObject = targetObject; + this.targetResult = targetResult; + + updateColour(); + } + + private void updateColour() + { + if (targetObject == null || targetResult == null) + Colour = Color4.White; + else + Colour = targetResult.IsHit ? targetObject.AccentColour.Value : Color4.Transparent; + } + } +} From 242ec1ca826eb3ed72a52cc686a3cf60f8630289 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 13:58:52 +0900 Subject: [PATCH 4401/6909] Don't override PathControlPointVisualiser's positional handling It turns out it was relying on this to deselect control points on clicking away from them. --- .../Sliders/Components/PathControlPointVisualiser.cs | 4 ---- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 13dc7886ed..b4fc9c2fb9 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -18,7 +18,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; -using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components @@ -116,9 +115,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { } - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => - Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)); - private void selectPiece(PathControlPointPiece piece, MouseButtonEvent e) { if (e.Button == MouseButton.Left && inputManager.CurrentState.Keyboard.ControlPressed) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 1644d5aa4b..1ad2eb83bf 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override Vector2 ScreenSpaceSelectionPoint => BodyPiece.ToScreenSpace(BodyPiece.PathStartLocation); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => - BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.ReceivePositionalInputAt(screenSpacePos) == true; + BodyPiece.ReceivePositionalInputAt(screenSpacePos) || ControlPointVisualiser?.Pieces.Any(p => p.ReceivePositionalInputAt(screenSpacePos)) == true; protected virtual SliderCircleSelectionBlueprint CreateCircleSelectionBlueprint(DrawableSlider slider, SliderPosition position) => new SliderCircleSelectionBlueprint(slider, position); } From 9c1c9945af19ea710a995c4febc931813d0be54a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 15:01:45 +0900 Subject: [PATCH 4402/6909] Make FollowPointRenderer use hitobject models --- .../TestSceneFollowPoints.cs | 10 ++--- .../Connections/FollowPointConnection.cs | 44 +++++++++---------- .../Connections/FollowPointRenderer.cs | 12 ++--- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 5 ++- 4 files changed, 36 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index 87da7ef417..6c077eb214 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -171,7 +171,7 @@ namespace osu.Game.Rulesets.Osu.Tests } hitObjectContainer.Add(drawableObject); - followPointRenderer.AddFollowPoints(drawableObject); + followPointRenderer.AddFollowPoints(objects[i]); } }); } @@ -180,10 +180,10 @@ namespace osu.Game.Rulesets.Osu.Tests { AddStep("remove hitobject", () => { - var drawableObject = getFunc?.Invoke(); + var drawableObject = getFunc.Invoke(); hitObjectContainer.Remove(drawableObject); - followPointRenderer.RemoveFollowPoints(drawableObject); + followPointRenderer.RemoveFollowPoints(drawableObject.HitObject); }); } @@ -215,10 +215,10 @@ namespace osu.Game.Rulesets.Osu.Tests DrawableOsuHitObject expectedStart = getObject(i); DrawableOsuHitObject expectedEnd = i < hitObjectContainer.Count - 1 ? getObject(i + 1) : null; - if (getGroup(i).Start != expectedStart) + if (getGroup(i).Start != expectedStart.HitObject) throw new AssertionException($"Object {i} expected to be the start of group {i}."); - if (getGroup(i).End != expectedEnd) + if (getGroup(i).End != expectedEnd?.HitObject) throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}."); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 2c41e6b0e9..3a9e19b361 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.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.Bindables; using osu.Framework.Graphics; @@ -31,19 +32,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// The which s will exit from. /// [NotNull] - public readonly DrawableOsuHitObject Start; + public readonly OsuHitObject Start; /// /// Creates a new . /// /// The which s will exit from. - public FollowPointConnection([NotNull] DrawableOsuHitObject start) + public FollowPointConnection([NotNull] OsuHitObject start) { Start = start; RelativeSizeAxes = Axes.Both; - StartTime.BindTo(Start.HitObject.StartTimeBindable); + StartTime.BindTo(start.StartTimeBindable); } protected override void LoadComplete() @@ -52,13 +53,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections bindEvents(Start); } - private DrawableOsuHitObject end; + private OsuHitObject end; /// /// The which s will enter. /// [CanBeNull] - public DrawableOsuHitObject End + public OsuHitObject End { get => end; set @@ -75,10 +76,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections } } - private void bindEvents(DrawableOsuHitObject drawableObject) + private void bindEvents(OsuHitObject obj) { - drawableObject.HitObject.PositionBindable.BindValueChanged(_ => scheduleRefresh()); - drawableObject.HitObject.DefaultsApplied += _ => scheduleRefresh(); + obj.PositionBindable.BindValueChanged(_ => scheduleRefresh()); + obj.DefaultsApplied += _ => scheduleRefresh(); } private void scheduleRefresh() @@ -88,23 +89,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections private void refresh() { - OsuHitObject osuStart = Start.HitObject; - double startTime = osuStart.GetEndTime(); + double startTime = Start.GetEndTime(); LifetimeStart = startTime; - OsuHitObject osuEnd = End?.HitObject; - - if (osuEnd == null || osuEnd.NewCombo || osuStart is Spinner || osuEnd is Spinner) + if (End == null || End.NewCombo || Start is Spinner || End is Spinner) { // ensure we always set a lifetime for full LifetimeManagementContainer benefits LifetimeEnd = LifetimeStart; return; } - Vector2 startPosition = osuStart.StackedEndPosition; - Vector2 endPosition = osuEnd.StackedPosition; - double endTime = osuEnd.StartTime; + Vector2 startPosition = Start.StackedEndPosition; + Vector2 endPosition = End.StackedPosition; + double endTime = End.StartTime; Vector2 distanceVector = endPosition - startPosition; int distance = (int)distanceVector.Length; @@ -130,10 +128,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections AddInternal(fp = new FollowPoint()); + Debug.Assert(End != null); + fp.Position = pointStartPosition; fp.Rotation = rotation; fp.Alpha = 0; - fp.Scale = new Vector2(1.5f * osuEnd.Scale); + fp.Scale = new Vector2(1.5f * End.Scale); firstTransformStartTime ??= fadeInTime; @@ -141,12 +141,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections using (fp.BeginAbsoluteSequence(fadeInTime)) { - fp.FadeIn(osuEnd.TimeFadeIn); - fp.ScaleTo(osuEnd.Scale, osuEnd.TimeFadeIn, Easing.Out); - fp.MoveTo(pointEndPosition, osuEnd.TimeFadeIn, Easing.Out); - fp.Delay(fadeOutTime - fadeInTime).FadeOut(osuEnd.TimeFadeIn); + fp.FadeIn(End.TimeFadeIn); + fp.ScaleTo(End.Scale, End.TimeFadeIn, Easing.Out); + fp.MoveTo(pointEndPosition, End.TimeFadeIn, Easing.Out); + fp.Delay(fadeOutTime - fadeInTime).FadeOut(End.TimeFadeIn); - finalTransformEndTime = fadeOutTime + osuEnd.TimeFadeIn; + finalTransformEndTime = fadeOutTime + End.TimeFadeIn; } point++; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 11571ea761..be1392d7c3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -24,19 +24,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections public override bool RemoveCompletedTransforms => false; /// - /// Adds the s around a . + /// Adds the s around an . /// This includes s leading into , and s exiting . /// - /// The to add s for. - public void AddFollowPoints(DrawableOsuHitObject hitObject) + /// The to add s for. + public void AddFollowPoints(OsuHitObject hitObject) => addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g)))); /// - /// Removes the s around a . + /// Removes the s around an . /// This includes s leading into , and s exiting . /// - /// The to remove s for. - public void RemoveFollowPoints(DrawableOsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject)); + /// The to remove s for. + public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject)); /// /// Adds a to this . diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 50727d590a..19502369c4 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -20,6 +20,7 @@ using osu.Game.Skinning; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Osu.Configuration; +using osu.Game.Rulesets.Osu.Objects; using osuTK; namespace osu.Game.Rulesets.Osu.UI @@ -110,7 +111,7 @@ namespace osu.Game.Rulesets.Osu.UI DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; osuHitObject.CheckHittable = hitPolicy.IsHittable; - followPoints.AddFollowPoints(osuHitObject); + followPoints.AddFollowPoints((OsuHitObject)h.HitObject); } public override bool Remove(DrawableHitObject h) @@ -118,7 +119,7 @@ namespace osu.Game.Rulesets.Osu.UI bool result = base.Remove(h); if (result) - followPoints.RemoveFollowPoints((DrawableOsuHitObject)h); + followPoints.RemoveFollowPoints((OsuHitObject)h.HitObject); return result; } From 3f24fabb575e9806d8fbecffce35d4c15b47e295 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 15:05:43 +0900 Subject: [PATCH 4403/6909] Add change handler support for contorl point deletion --- .../Sliders/Components/PathControlPointVisualiser.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index b4fc9c2fb9..17541866ec 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; using Humanizer; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,6 +19,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Edit; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components @@ -126,6 +128,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components } } + [Resolved(CanBeNull = true)] + private IEditorChangeHandler changeHandler { get; set; } + public bool DeleteSelected() { List toRemove = Pieces.Where(p => p.IsSelected.Value).Select(p => p.ControlPoint).ToList(); @@ -134,7 +139,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (toRemove.Count == 0) return false; + changeHandler?.BeginChange(); RemoveControlPointsRequested?.Invoke(toRemove); + changeHandler?.EndChange(); // Since pieces are re-used, they will not point to the deleted control points while remaining selected foreach (var piece in Pieces) From 4457e363d39bfa4f8a6f954ab9dbb280b96dcdc2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 15:18:52 +0900 Subject: [PATCH 4404/6909] Bring back local variables to reduce two-level-deep variable access --- .../Drawables/Pieces/DefaultSpinnerDisc.cs | 26 ++++++++++--------- .../Drawables/Pieces/SnakingSliderBody.cs | 12 +++++---- .../Skinning/LegacyNewStyleSpinner.cs | 17 +++++++----- 3 files changed, 31 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 4c682d96ce..731852c221 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -127,18 +127,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (!(drawableHitObject is DrawableSpinner)) return; - using (BeginAbsoluteSequence(drawableSpinner.HitObject.StartTime - drawableSpinner.HitObject.TimePreempt, true)) + Spinner spinner = drawableSpinner.HitObject; + + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) { this.ScaleTo(initial_scale); this.RotateTo(0); - using (BeginDelayedSequence(drawableSpinner.HitObject.TimePreempt / 2, true)) + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) { // constant ambient rotation to give the spinner "spinning" character. - this.RotateTo((float)(25 * drawableSpinner.HitObject.Duration / 2000), drawableSpinner.HitObject.TimePreempt + drawableSpinner.HitObject.Duration); + this.RotateTo((float)(25 * spinner.Duration / 2000), spinner.TimePreempt + spinner.Duration); } - using (BeginDelayedSequence(drawableSpinner.HitObject.TimePreempt + drawableSpinner.HitObject.Duration + drawableHitObject.Result.TimeOffset, true)) + using (BeginDelayedSequence(spinner.TimePreempt + spinner.Duration + drawableHitObject.Result.TimeOffset, true)) { switch (state) { @@ -154,26 +156,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } } - using (BeginAbsoluteSequence(drawableSpinner.HitObject.StartTime - drawableSpinner.HitObject.TimePreempt, true)) + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) { centre.ScaleTo(0); mainContainer.ScaleTo(0); - using (BeginDelayedSequence(drawableSpinner.HitObject.TimePreempt / 2, true)) + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) { - centre.ScaleTo(0.3f, drawableSpinner.HitObject.TimePreempt / 4, Easing.OutQuint); - mainContainer.ScaleTo(0.2f, drawableSpinner.HitObject.TimePreempt / 4, Easing.OutQuint); + centre.ScaleTo(0.3f, spinner.TimePreempt / 4, Easing.OutQuint); + mainContainer.ScaleTo(0.2f, spinner.TimePreempt / 4, Easing.OutQuint); - using (BeginDelayedSequence(drawableSpinner.HitObject.TimePreempt / 2, true)) + using (BeginDelayedSequence(spinner.TimePreempt / 2, true)) { - centre.ScaleTo(0.5f, drawableSpinner.HitObject.TimePreempt / 2, Easing.OutQuint); - mainContainer.ScaleTo(1, drawableSpinner.HitObject.TimePreempt / 2, Easing.OutQuint); + centre.ScaleTo(0.5f, spinner.TimePreempt / 2, Easing.OutQuint); + mainContainer.ScaleTo(1, spinner.TimePreempt / 2, Easing.OutQuint); } } } // transforms we have from completing the spinner will be rolled back, so reapply immediately. - using (BeginAbsoluteSequence(drawableSpinner.HitObject.StartTime - drawableSpinner.HitObject.TimePreempt, true)) + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) updateComplete(state == ArmedState.Hit, 0); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs index 2fefc2e08e..e63f25b7bc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs @@ -66,15 +66,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces if (drawableSlider == null) return; - var span = drawableSlider.HitObject.SpanAt(completionProgress); - var spanProgress = drawableSlider.HitObject.ProgressAt(completionProgress); + Slider slider = drawableSlider.HitObject; + + var span = slider.SpanAt(completionProgress); + var spanProgress = slider.ProgressAt(completionProgress); double start = 0; - double end = SnakingIn.Value ? Math.Clamp((Time.Current - (drawableSlider.HitObject.StartTime - drawableSlider.HitObject.TimePreempt)) / (drawableSlider.HitObject.TimePreempt / 3), 0, 1) : 1; + double end = SnakingIn.Value ? Math.Clamp((Time.Current - (slider.StartTime - slider.TimePreempt)) / (slider.TimePreempt / 3), 0, 1) : 1; - if (span >= drawableSlider.HitObject.SpanCount() - 1) + if (span >= slider.SpanCount() - 1) { - if (Math.Min(span, drawableSlider.HitObject.SpanCount() - 1) % 2 == 1) + if (Math.Min(span, slider.SpanCount() - 1) % 2 == 1) { start = 0; end = SnakingOut.Value ? spanProgress : 1; diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index f07b1038fb..b65e5a784c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; @@ -75,21 +76,23 @@ namespace osu.Game.Rulesets.Osu.Skinning private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSpinner spinner)) + if (!(drawableHitObject is DrawableSpinner d)) return; - using (BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt, true)) + Spinner spinner = d.HitObject; + + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) this.FadeOut(); - using (BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimeFadeIn / 2, true)) - this.FadeInFromZero(spinner.HitObject.TimeFadeIn / 2); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) + this.FadeInFromZero(spinner.TimeFadeIn / 2); - using (BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt, true)) + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) { fixedMiddle.FadeColour(Color4.White); - using (BeginDelayedSequence(spinner.HitObject.TimePreempt, true)) - fixedMiddle.FadeColour(Color4.Red, spinner.HitObject.Duration); + using (BeginDelayedSequence(spinner.TimePreempt, true)) + fixedMiddle.FadeColour(Color4.Red, spinner.Duration); } } From e2d028908abe2271e1faeb74b9c6f84384ce049c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 15:25:20 +0900 Subject: [PATCH 4405/6909] Fix one more case of local variable preference --- .../Skinning/LegacyOldStyleSpinner.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 5fcd8e06b1..1954ff6e38 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; @@ -94,14 +95,16 @@ namespace osu.Game.Rulesets.Osu.Skinning private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSpinner spinner)) + if (!(drawableHitObject is DrawableSpinner d)) return; - using (BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimePreempt, true)) + Spinner spinner = d.HitObject; + + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) this.FadeOut(); - using (BeginAbsoluteSequence(spinner.HitObject.StartTime - spinner.HitObject.TimeFadeIn / 2, true)) - this.FadeInFromZero(spinner.HitObject.TimeFadeIn / 2); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) + this.FadeInFromZero(spinner.TimeFadeIn / 2); } protected override void Update() From 628b8be15d8aa79fe67944dc20eced20b9f864c8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 15:36:44 +0900 Subject: [PATCH 4406/6909] Implement ModWithVisibilityAdjustment --- .../Mods/CatchModHidden.cs | 6 +- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 14 ++- osu.Game/Rulesets/Mods/ModHidden.cs | 50 ++++---- .../Mods/ModWithVisibilityAdjustment.cs | 114 ++++++++++++++++++ 4 files changed, 150 insertions(+), 34 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs index ee88edbea1..4b008d2734 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModHidden.cs @@ -17,9 +17,11 @@ namespace osu.Game.Rulesets.Catch.Mods private const double fade_out_offset_multiplier = 0.6; private const double fade_out_duration_multiplier = 0.44; - protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { - if (!(drawable is DrawableCatchHitObject catchDrawable)) + base.ApplyNormalVisibilityState(hitObject, state); + + if (!(hitObject is DrawableCatchHitObject catchDrawable)) return; if (catchDrawable.NestedHitObjects.Any()) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index f69cacd432..db5fbb0dd6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -23,7 +23,7 @@ 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); + protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner); public override void ApplyToDrawableHitObjects(IEnumerable drawables) { @@ -42,9 +42,17 @@ namespace osu.Game.Rulesets.Osu.Mods private double lastSliderHeadFadeOutStartTime; private double lastSliderHeadFadeOutDuration; - protected override void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, true); + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) + { + base.ApplyIncreasedVisibilityState(hitObject, state); + applyState(hitObject, true); + } - protected override void ApplyHiddenState(DrawableHitObject drawable, ArmedState state) => applyState(drawable, false); + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) + { + base.ApplyNormalVisibilityState(hitObject, state); + applyState(hitObject, false); + } private void applyState(DrawableHitObject drawable, bool increaseVisibility) { diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index ad01bf036c..f35546d400 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -1,19 +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.Configuration; +using System; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using System.Collections.Generic; -using System.Linq; -using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModHidden : Mod, IReadFromConfig, IApplicableToDrawableHitObjects, IApplicableToScoreProcessor + public abstract class ModHidden : ModWithVisibilityAdjustment, IApplicableToScoreProcessor { public override string Name => "Hidden"; public override string Acronym => "HD"; @@ -21,37 +18,14 @@ namespace osu.Game.Rulesets.Mods public override ModType Type => ModType.DifficultyIncrease; public override bool Ranked => true; - 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. + [Obsolete("Use IsFirstAdjustableObject() instead.")] protected virtual bool IsFirstHideableObject(DrawableHitObject hitObject) => true; - public void ReadFromConfig(OsuConfigManager config) - { - IncreaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); - } - - public virtual void ApplyToDrawableHitObjects(IEnumerable drawables) - { - if (IncreaseFirstObjectVisibility.Value) - { - drawables = drawables.SkipWhile(h => !IsFirstHideableObject(h)); - - var firstObject = drawables.FirstOrDefault(); - if (firstObject != null) - firstObject.ApplyCustomUpdateState += ApplyFirstObjectIncreaseVisibilityState; - - drawables = drawables.Skip(1); - } - - foreach (var dho in drawables) - dho.ApplyCustomUpdateState += ApplyHiddenState; - } - public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) { // Default value of ScoreProcessor's Rank in Hidden Mod should be SS+ @@ -73,11 +47,28 @@ namespace osu.Game.Rulesets.Mods } } + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) + { + base.ApplyIncreasedVisibilityState(hitObject, state); +#pragma warning disable 618 + ApplyFirstObjectIncreaseVisibilityState(hitObject, state); +#pragma warning restore 618 + } + + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) + { + base.ApplyNormalVisibilityState(hitObject, state); +#pragma warning disable 618 + ApplyHiddenState(hitObject, state); +#pragma warning restore 618 + } + /// /// Apply a special visibility state to the first object in a beatmap, if the user chooses to turn on the "increase first object visibility" setting. /// /// The hit object to apply the state change to. /// The state of the hit object. + [Obsolete("Use ApplyIncreasedVisibilityState() instead.")] protected virtual void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject hitObject, ArmedState state) { } @@ -87,6 +78,7 @@ namespace osu.Game.Rulesets.Mods /// /// The hit object to apply the state change to. /// The state of the hit object. + [Obsolete("Use ApplyNormalVisibilityState() instead.")] protected virtual void ApplyHiddenState(DrawableHitObject hitObject, ArmedState state) { } diff --git a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs new file mode 100644 index 0000000000..89d1837348 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs @@ -0,0 +1,114 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObjects + { + /// + /// The first adjustable object. + /// + protected HitObject FirstObject { get; private set; } + + /// + /// Whether the visibility of should be increased. + /// + protected readonly Bindable IncreaseFirstObjectVisibility = new Bindable(); + + /// + /// Check whether the provided hitobject should be considered the "first" adjustable object. + /// Can be used to skip spinners, for instance. + /// + /// The hitobject to check. + protected virtual bool IsFirstAdjustableObject(HitObject hitObject) => true; + + /// + /// Apply a special increased-visibility state to the first adjustable object.. + /// Only applicable if the user chooses to turn on the "increase first object visibility" setting. + /// + /// The hit object to apply the state change to. + /// The state of the hit object. + protected virtual void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) + { + } + + /// + /// Apply a normal visibility state adjustment to an object. + /// + /// The hit object to apply the state change to. + /// The state of the hit object. + protected virtual void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) + { + } + + public virtual void ReadFromConfig(OsuConfigManager config) + { + config.BindWith(OsuSetting.IncreaseFirstObjectVisibility, IncreaseFirstObjectVisibility); + } + + public virtual void ApplyToBeatmap(IBeatmap beatmap) + { + FirstObject = getFirstAdjustableObjectRecursive(beatmap.HitObjects); + + HitObject getFirstAdjustableObjectRecursive(IReadOnlyList hitObjects) + { + foreach (var h in hitObjects) + { + if (IsFirstAdjustableObject(h)) + return h; + + var nestedResult = getFirstAdjustableObjectRecursive(h.NestedHitObjects); + if (nestedResult != null) + return nestedResult; + } + + return null; + } + } + + public virtual void ApplyToDrawableHitObjects(IEnumerable drawables) + { + foreach (var dho in drawables) + { + dho.ApplyCustomUpdateState += (o, state) => + { + // Increased visibility is applied to the entire first object, including all of its nested hitobjects. + if (IncreaseFirstObjectVisibility.Value && isObjectEqualToOrNestedIn(o.HitObject, FirstObject)) + ApplyIncreasedVisibilityState(o, state); + else + ApplyNormalVisibilityState(o, state); + }; + } + } + + /// + /// Checks whether a given object is nested within a target. + /// + /// The to check. + /// The which may be equal to or contain as a nested object. + /// Whether is equal to or nested within . + private bool isObjectEqualToOrNestedIn(HitObject toCheck, HitObject target) + { + if (target == null) + return false; + + if (toCheck == target) + return true; + + foreach (var h in target.NestedHitObjects) + { + if (isObjectEqualToOrNestedIn(toCheck, h)) + return true; + } + + return false; + } + } +} From 54f927ee11e7a114862ad2625f7ca9bc10bfd794 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 15:41:37 +0900 Subject: [PATCH 4407/6909] Move casts to DrawableHitObject instead --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 19502369c4..321eeeab65 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -4,12 +4,15 @@ 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.Pooling; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; using osu.Game.Rulesets.Osu.Scoring; @@ -17,10 +20,6 @@ using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Skinning; -using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Game.Rulesets.Osu.Configuration; -using osu.Game.Rulesets.Osu.Objects; using osuTK; namespace osu.Game.Rulesets.Osu.UI @@ -96,6 +95,8 @@ namespace osu.Game.Rulesets.Osu.UI public override void Add(DrawableHitObject h) { + DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; + h.OnNewResult += onNewResult; h.OnLoadComplete += d => { @@ -108,18 +109,19 @@ namespace osu.Game.Rulesets.Osu.UI base.Add(h); - DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; osuHitObject.CheckHittable = hitPolicy.IsHittable; - followPoints.AddFollowPoints((OsuHitObject)h.HitObject); + followPoints.AddFollowPoints(osuHitObject.HitObject); } public override bool Remove(DrawableHitObject h) { + DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; + bool result = base.Remove(h); if (result) - followPoints.RemoveFollowPoints((OsuHitObject)h.HitObject); + followPoints.RemoveFollowPoints(osuHitObject.HitObject); return result; } From 77a618dd71044d185d69d3e178a5bffcbc0f38ec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 15:52:06 +0900 Subject: [PATCH 4408/6909] Use class with other osu! mods --- .../Mods/OsuModObjectScaleTween.cs | 31 ++++-------------- osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs | 32 ++++--------------- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 20 +++--------- 3 files changed, 19 insertions(+), 64 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index 06ba4cde4a..cd2e2e092a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; -using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -17,7 +14,7 @@ namespace osu.Game.Rulesets.Osu.Mods /// /// Adjusts the size of hit objects during their fade in animation. /// - public abstract class OsuModObjectScaleTween : Mod, IReadFromConfig, IApplicableToDrawableHitObjects + public abstract class OsuModObjectScaleTween : ModWithVisibilityAdjustment { public override ModType Type => ModType.Fun; @@ -27,33 +24,19 @@ namespace osu.Game.Rulesets.Osu.Mods protected virtual float EndScale => 1; - private Bindable increaseFirstObjectVisibility = new Bindable(); - public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) }; - public void ReadFromConfig(OsuConfigManager config) + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { - increaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); + base.ApplyNormalVisibilityState(hitObject, state); + applyCustomState(hitObject, state); } - public void ApplyToDrawableHitObjects(IEnumerable drawables) + private void applyCustomState(DrawableHitObject drawable, ArmedState state) { - foreach (var drawable in drawables.Skip(increaseFirstObjectVisibility.Value ? 1 : 0)) - { - switch (drawable) - { - case DrawableSpinner _: - continue; + if (drawable is DrawableSpinner) + return; - default: - drawable.ApplyCustomUpdateState += ApplyCustomState; - break; - } - } - } - - protected virtual void ApplyCustomState(DrawableHitObject drawable, ArmedState state) - { var h = (OsuHitObject)drawable.HitObject; // apply grow effect diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index 940c888f3a..34c94fa7e0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.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 osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; -using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; @@ -16,7 +12,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModSpinIn : Mod, IApplicableToDrawableHitObjects, IReadFromConfig + public class OsuModSpinIn : ModWithVisibilityAdjustment { public override string Name => "Spin In"; public override string Acronym => "SI"; @@ -31,31 +27,17 @@ namespace osu.Game.Rulesets.Osu.Mods private const int rotate_offset = 360; private const float rotate_starting_width = 2; - private Bindable increaseFirstObjectVisibility = new Bindable(); - - public void ReadFromConfig(OsuConfigManager config) + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { - increaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); - } - - public void ApplyToDrawableHitObjects(IEnumerable drawables) - { - foreach (var drawable in drawables.Skip(increaseFirstObjectVisibility.Value ? 1 : 0)) - { - switch (drawable) - { - case DrawableSpinner _: - continue; - - default: - drawable.ApplyCustomUpdateState += applyZoomState; - break; - } - } + base.ApplyNormalVisibilityState(hitObject, state); + applyZoomState(hitObject, state); } private void applyZoomState(DrawableHitObject drawable, ArmedState state) { + if (drawable is DrawableSpinner) + return; + var h = (OsuHitObject)drawable.HitObject; switch (drawable) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index bb2213aa31..9349dc6a78 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -2,12 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; -using osu.Framework.Bindables; -using System.Collections.Generic; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; -using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -15,7 +11,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModTraceable : Mod, IReadFromConfig, IApplicableToDrawableHitObjects + internal class OsuModTraceable : ModWithVisibilityAdjustment { public override string Name => "Traceable"; public override string Acronym => "TC"; @@ -24,20 +20,14 @@ namespace osu.Game.Rulesets.Osu.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) }; - private Bindable increaseFirstObjectVisibility = new Bindable(); - public void ReadFromConfig(OsuConfigManager config) + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { - increaseFirstObjectVisibility = config.GetBindable(OsuSetting.IncreaseFirstObjectVisibility); + base.ApplyNormalVisibilityState(hitObject, state); + applyTraceableState(hitObject, state); } - public void ApplyToDrawableHitObjects(IEnumerable drawables) - { - foreach (var drawable in drawables.Skip(increaseFirstObjectVisibility.Value ? 1 : 0)) - drawable.ApplyCustomUpdateState += ApplyTraceableState; - } - - protected void ApplyTraceableState(DrawableHitObject drawable, ArmedState state) + private void applyTraceableState(DrawableHitObject drawable, ArmedState state) { if (!(drawable is DrawableOsuHitObject)) return; From a219aa7ba272d31b32a6f5f2c86511f365a8da6e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 15:53:35 +0900 Subject: [PATCH 4409/6909] Add xmldoc --- osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs index 89d1837348..fd563f4261 100644 --- a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs +++ b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs @@ -10,6 +10,10 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Mods { + /// + /// A which applies visibility adjustments to s + /// with an optional increased visibility adjustment depending on the user's "increase first object visibility" setting. + /// public abstract class ModWithVisibilityAdjustment : Mod, IReadFromConfig, IApplicableToBeatmap, IApplicableToDrawableHitObjects { /// From cc518feca7718e8ebb1c89bb6d14f08a0d806a39 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 16:03:10 +0900 Subject: [PATCH 4410/6909] Make methods abstract --- osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs | 6 +++--- osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs | 6 +++--- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 6 +++--- osu.Game/Rulesets/Mods/ModHidden.cs | 2 -- osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs | 8 ++------ 5 files changed, 11 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs index cd2e2e092a..d1be162f73 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModObjectScaleTween.cs @@ -26,12 +26,12 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(OsuModSpinIn), typeof(OsuModTraceable) }; - protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { - base.ApplyNormalVisibilityState(hitObject, state); - applyCustomState(hitObject, state); } + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyCustomState(hitObject, state); + private void applyCustomState(DrawableHitObject drawable, ArmedState state) { if (drawable is DrawableSpinner) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs index 34c94fa7e0..96ba58da23 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModSpinIn.cs @@ -27,12 +27,12 @@ namespace osu.Game.Rulesets.Osu.Mods private const int rotate_offset = 360; private const float rotate_starting_width = 2; - protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { - base.ApplyNormalVisibilityState(hitObject, state); - applyZoomState(hitObject, state); } + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyZoomState(hitObject, state); + private void applyZoomState(DrawableHitObject drawable, ArmedState state) { if (drawable is DrawableSpinner) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index 9349dc6a78..b7e60295cb 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -21,12 +21,12 @@ namespace osu.Game.Rulesets.Osu.Mods public override Type[] IncompatibleMods => new[] { typeof(OsuModHidden), typeof(OsuModSpinIn), typeof(OsuModObjectScaleTween) }; - protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { - base.ApplyNormalVisibilityState(hitObject, state); - applyTraceableState(hitObject, state); } + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTraceableState(hitObject, state); + private void applyTraceableState(DrawableHitObject drawable, ArmedState state) { if (!(drawable is DrawableOsuHitObject)) diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index f35546d400..b88d785ff1 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -49,7 +49,6 @@ namespace osu.Game.Rulesets.Mods protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { - base.ApplyIncreasedVisibilityState(hitObject, state); #pragma warning disable 618 ApplyFirstObjectIncreaseVisibilityState(hitObject, state); #pragma warning restore 618 @@ -57,7 +56,6 @@ namespace osu.Game.Rulesets.Mods protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) { - base.ApplyNormalVisibilityState(hitObject, state); #pragma warning disable 618 ApplyHiddenState(hitObject, state); #pragma warning restore 618 diff --git a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs index fd563f4261..5bbd02cf6c 100644 --- a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs +++ b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs @@ -39,18 +39,14 @@ namespace osu.Game.Rulesets.Mods /// /// The hit object to apply the state change to. /// The state of the hit object. - protected virtual void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) - { - } + protected abstract void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state); /// /// Apply a normal visibility state adjustment to an object. /// /// The hit object to apply the state change to. /// The state of the hit object. - protected virtual void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) - { - } + protected abstract void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state); public virtual void ReadFromConfig(OsuConfigManager config) { From f513c95ab215de244a428a233c2063e00f3d9545 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 16:04:42 +0900 Subject: [PATCH 4411/6909] Use class with transform and wiggle mods --- osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs | 11 ++++------- osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs | 11 ++++------- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs index 5e80d08667..b5905d7015 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTransform.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTransform.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; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Mods; @@ -13,7 +12,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModTransform : Mod, IApplicableToDrawableHitObjects + internal class OsuModTransform : ModWithVisibilityAdjustment { public override string Name => "Transform"; public override string Acronym => "TR"; @@ -25,11 +24,9 @@ namespace osu.Game.Rulesets.Osu.Mods private float theta; - public void ApplyToDrawableHitObjects(IEnumerable drawables) - { - foreach (var drawable in drawables) - drawable.ApplyCustomUpdateState += applyTransform; - } + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state); + + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => applyTransform(hitObject, state); private void applyTransform(DrawableHitObject drawable, ArmedState state) { diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs index 3cad52faeb..9c5e41f245 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModWiggle.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; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Mods; @@ -13,7 +12,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModWiggle : Mod, IApplicableToDrawableHitObjects + internal class OsuModWiggle : ModWithVisibilityAdjustment { public override string Name => "Wiggle"; public override string Acronym => "WG"; @@ -26,11 +25,9 @@ namespace osu.Game.Rulesets.Osu.Mods private const int wiggle_duration = 90; // (ms) Higher = fewer wiggles private const int wiggle_strength = 10; // Higher = stronger wiggles - public void ApplyToDrawableHitObjects(IEnumerable drawables) - { - foreach (var drawable in drawables) - drawable.ApplyCustomUpdateState += drawableOnApplyCustomUpdateState; - } + protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) => drawableOnApplyCustomUpdateState(hitObject, state); + + protected override void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state) => drawableOnApplyCustomUpdateState(hitObject, state); private void drawableOnApplyCustomUpdateState(DrawableHitObject drawable, ArmedState state) { From d1fa391d251e0d67a02b2c8eb68e1b777567c257 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 16:12:55 +0900 Subject: [PATCH 4412/6909] Make OsuModHidden apply fadein adjustment on custom state update --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index db5fbb0dd6..025e202666 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using osu.Framework.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -27,18 +26,20 @@ namespace osu.Game.Rulesets.Osu.Mods public override void ApplyToDrawableHitObjects(IEnumerable drawables) { - static void adjustFadeIn(OsuHitObject h) => h.TimeFadeIn = h.TimePreempt * fade_in_duration_multiplier; - - foreach (var d in drawables.OfType()) - { - adjustFadeIn(d.HitObject); - foreach (var h in d.HitObject.NestedHitObjects.OfType()) - adjustFadeIn(h); - } + foreach (var d in drawables) + d.ApplyCustomUpdateState += applyFadeInAdjustment; base.ApplyToDrawableHitObjects(drawables); } + private void applyFadeInAdjustment(DrawableHitObject hitObject, ArmedState state) + { + if (!(hitObject is DrawableOsuHitObject d)) + return; + + d.HitObject.TimeFadeIn = d.HitObject.TimePreempt * fade_in_duration_multiplier; + } + private double lastSliderHeadFadeOutStartTime; private double lastSliderHeadFadeOutDuration; From 414daab1dc685ec46ffc7f196d236507a66362bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 16:14:22 +0900 Subject: [PATCH 4413/6909] Fix paused samples potentially getting stuck in a playing state in rapid toggling Closes #10693. Should be obvious why. --- osu.Game/Skinning/PausableSkinnableSound.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index d340f67575..4f09aec0b6 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Threading; using osu.Game.Audio; using osu.Game.Screens.Play; @@ -25,6 +26,8 @@ namespace osu.Game.Skinning private readonly IBindable samplePlaybackDisabled = new Bindable(); + private ScheduledDelegate scheduledStart; + [BackgroundDependencyLoader(true)] private void load(ISamplePlaybackDisabler samplePlaybackDisabler) { @@ -39,12 +42,14 @@ namespace osu.Game.Skinning // let non-looping samples that have already been started play out to completion (sounds better than abruptly cutting off). if (!Looping) return; + cancelPendingStart(); + if (disabled.NewValue) base.Stop(); else { // schedule so we don't start playing a sample which is no longer alive. - Schedule(() => + scheduledStart = Schedule(() => { if (RequestedPlaying) base.Play(); @@ -56,6 +61,7 @@ namespace osu.Game.Skinning public override void Play() { + cancelPendingStart(); RequestedPlaying = true; if (samplePlaybackDisabled.Value) @@ -66,8 +72,15 @@ namespace osu.Game.Skinning public override void Stop() { + cancelPendingStart(); RequestedPlaying = false; base.Stop(); } + + private void cancelPendingStart() + { + scheduledStart?.Cancel(); + scheduledStart = null; + } } } From 2d0c62dce274e1d53c3e3adf11cf560844aa8f89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 16:37:47 +0900 Subject: [PATCH 4414/6909] Fix SkinnableTestScene's autosize propagation Regressed with recent DrawableHitObject changes (moving of RelativeSizeAxes specifications out of constructors). --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 41 +++++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index fe4f735325..07e45f25cf 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -65,17 +65,15 @@ namespace osu.Game.Tests.Visual private Drawable createProvider(Skin skin, Func creationFunction, IBeatmap beatmap) { var created = creationFunction(); + createdDrawables.Add(created); - var autoSize = created.RelativeSizeAxes == Axes.None; + SkinProvidingContainer mainProvider; + Container childContainer; + OutlineBox outlineBox; + SkinProvidingContainer skinProvider; - var mainProvider = new SkinProvidingContainer(skin) - { - RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None, - AutoSizeAxes = autoSize ? Axes.Both : Axes.None, - }; - - return new Container + var children = new Container { RelativeSizeAxes = Axes.Both, BorderColour = Color4.White, @@ -96,27 +94,38 @@ namespace osu.Game.Tests.Visual Scale = new Vector2(1.5f), Padding = new MarginPadding(5), }, - new Container + childContainer = new Container { - RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None, - AutoSizeAxes = autoSize ? Axes.Both : Axes.None, Anchor = Anchor.Centre, Origin = Anchor.Centre, Children = new Drawable[] { - new OutlineBox { Alpha = autoSize ? 1 : 0 }, - mainProvider.WithChild( - new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider, beatmap)) + outlineBox = new OutlineBox(), + (mainProvider = new SkinProvidingContainer(skin)).WithChild( + skinProvider = new SkinProvidingContainer(Ruleset.Value.CreateInstance().CreateLegacySkinProvider(mainProvider, beatmap)) { Child = created, - RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None, - AutoSizeAxes = autoSize ? Axes.Both : Axes.None, } ) } }, } }; + + Schedule(() => + { + var autoSize = created.RelativeSizeAxes == Axes.None; + + foreach (var c in new[] { mainProvider, childContainer, skinProvider }) + { + c.RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None; + c.AutoSizeAxes = autoSize ? Axes.Both : Axes.None; + } + + outlineBox.Alpha = autoSize ? 1 : 0; + }); + + return children; } /// From 9d5b1ec28c0281bed94908f6e2557e8d9169f2cf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 17:03:53 +0900 Subject: [PATCH 4415/6909] Add removal dates --- osu.Game/Rulesets/Mods/ModHidden.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModHidden.cs b/osu.Game/Rulesets/Mods/ModHidden.cs index b88d785ff1..df421adbe5 100644 --- a/osu.Game/Rulesets/Mods/ModHidden.cs +++ b/osu.Game/Rulesets/Mods/ModHidden.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Mods /// Can be used to skip spinners, for instance. /// /// The hitobject to check. - [Obsolete("Use IsFirstAdjustableObject() instead.")] + [Obsolete("Use IsFirstAdjustableObject() instead.")] // Can be removed 20210506 protected virtual bool IsFirstHideableObject(DrawableHitObject hitObject) => true; public void ApplyToScoreProcessor(ScoreProcessor scoreProcessor) @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Mods /// /// The hit object to apply the state change to. /// The state of the hit object. - [Obsolete("Use ApplyIncreasedVisibilityState() instead.")] + [Obsolete("Use ApplyIncreasedVisibilityState() instead.")] // Can be removed 20210506 protected virtual void ApplyFirstObjectIncreaseVisibilityState(DrawableHitObject hitObject, ArmedState state) { } @@ -76,7 +76,7 @@ namespace osu.Game.Rulesets.Mods /// /// The hit object to apply the state change to. /// The state of the hit object. - [Obsolete("Use ApplyNormalVisibilityState() instead.")] + [Obsolete("Use ApplyNormalVisibilityState() instead.")] // Can be removed 20210506 protected virtual void ApplyHiddenState(DrawableHitObject hitObject, ArmedState state) { } From 9c91f16e3a2425f248a5d040525322737fd639ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 17:24:59 +0900 Subject: [PATCH 4416/6909] Update sizing as early as possible in addition to scheduling (to handle any dependent edge cases) --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 07e45f25cf..d5fcf8bae3 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -112,7 +112,15 @@ namespace osu.Game.Tests.Visual } }; - Schedule(() => + // run this once initially to bring things into a sane state as early as possible. + updateSizing(); + + // run this once after construction to handle the case the changes are made in a BDL/LoadComplete call. + Schedule(updateSizing); + + return children; + + void updateSizing() { var autoSize = created.RelativeSizeAxes == Axes.None; @@ -123,9 +131,7 @@ namespace osu.Game.Tests.Visual } outlineBox.Alpha = autoSize ? 1 : 0; - }); - - return children; + } } /// From 8aa0a698d986f11a4a054522dee9e81e57937f7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 17:26:41 +0900 Subject: [PATCH 4417/6909] Avoid errors due to second set --- osu.Game/Tests/Visual/SkinnableTestScene.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index d5fcf8bae3..68098f9d3b 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -126,6 +126,9 @@ namespace osu.Game.Tests.Visual foreach (var c in new[] { mainProvider, childContainer, skinProvider }) { + c.RelativeSizeAxes = Axes.None; + c.AutoSizeAxes = Axes.None; + c.RelativeSizeAxes = !autoSize ? Axes.Both : Axes.None; c.AutoSizeAxes = autoSize ? Axes.Both : Axes.None; } From 7d33dc3deae7df07cfa17afd680825788c2df372 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 17:12:18 +0900 Subject: [PATCH 4418/6909] Reorder spinner tests to promote hit being the first to display --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 94d1cb8864..496b1b3559 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -20,8 +20,8 @@ namespace osu.Game.Rulesets.Osu.Tests private TestDrawableSpinner drawableSpinner; - [TestCase(false)] [TestCase(true)] + [TestCase(false)] public void TestVariousSpinners(bool autoplay) { string term = autoplay ? "Hit" : "Miss"; From 82d8c1bbea7a37fde1382fd56b6cb6f0acc1f41b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 17:12:35 +0900 Subject: [PATCH 4419/6909] Add support for spinner glow --- .../Skinning/LegacyNewStyleSpinner.cs | 58 ++++++++++++++----- 1 file changed, 43 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index b65e5a784c..31e2ab1239 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -21,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning /// public class LegacyNewStyleSpinner : CompositeDrawable { + private Sprite glow; private Sprite discBottom; private Sprite discTop; private Sprite spinningMiddle; @@ -30,6 +31,8 @@ namespace osu.Game.Rulesets.Osu.Skinning private const float final_scale = 0.625f; + private readonly Color4 glowColour = new Color4(3, 151, 255, 255); + [BackgroundDependencyLoader] private void load(ISkinSource source, DrawableHitObject drawableObject) { @@ -39,6 +42,14 @@ namespace osu.Game.Rulesets.Osu.Skinning InternalChildren = new Drawable[] { + glow = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-glow"), + Blending = BlendingParameters.Additive, + Colour = glowColour, + }, discBottom = new Sprite { Anchor = Anchor.Centre, @@ -76,23 +87,38 @@ namespace osu.Game.Rulesets.Osu.Skinning private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - if (!(drawableHitObject is DrawableSpinner d)) - return; - - Spinner spinner = d.HitObject; - - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) - this.FadeOut(); - - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) - this.FadeInFromZero(spinner.TimeFadeIn / 2); - - using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + switch (drawableHitObject) { - fixedMiddle.FadeColour(Color4.White); + case DrawableSpinner d: + Spinner spinner = d.HitObject; - using (BeginDelayedSequence(spinner.TimePreempt, true)) - fixedMiddle.FadeColour(Color4.Red, spinner.Duration); + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + this.FadeOut(); + + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimeFadeIn / 2, true)) + this.FadeInFromZero(spinner.TimeFadeIn / 2); + + using (BeginAbsoluteSequence(spinner.StartTime - spinner.TimePreempt, true)) + { + fixedMiddle.FadeColour(Color4.White); + + using (BeginDelayedSequence(spinner.TimePreempt, true)) + fixedMiddle.FadeColour(Color4.Red, spinner.Duration); + } + + if (state == ArmedState.Hit) + { + using (BeginAbsoluteSequence(d.HitStateUpdateTime)) + glow.FadeOut(300); + } + + break; + + case DrawableSpinnerBonusTick _: + if (state == ArmedState.Hit) + glow.FlashColour(Color4.White, 200); + + break; } } @@ -102,6 +128,8 @@ namespace osu.Game.Rulesets.Osu.Skinning spinningMiddle.Rotation = discTop.Rotation = drawableSpinner.RotationTracker.Rotation; discBottom.Rotation = discTop.Rotation / 3; + glow.Alpha = drawableSpinner.Progress; + Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * 0.2f)); } From 2d50a7b61644d0635da7550457957b61d6855005 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 17:38:02 +0900 Subject: [PATCH 4420/6909] Fix a few xmldoc typos --- osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs index 5bbd02cf6c..5b119b5e46 100644 --- a/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs +++ b/osu.Game/Rulesets/Mods/ModWithVisibilityAdjustment.cs @@ -34,18 +34,18 @@ namespace osu.Game.Rulesets.Mods protected virtual bool IsFirstAdjustableObject(HitObject hitObject) => true; /// - /// Apply a special increased-visibility state to the first adjustable object.. + /// Apply a special increased-visibility state to the first adjustable object. /// Only applicable if the user chooses to turn on the "increase first object visibility" setting. /// /// The hit object to apply the state change to. - /// The state of the hit object. + /// The state of the hitobject. protected abstract void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state); /// /// Apply a normal visibility state adjustment to an object. /// /// The hit object to apply the state change to. - /// The state of the hit object. + /// The state of the hitobject. protected abstract void ApplyNormalVisibilityState(DrawableHitObject hitObject, ArmedState state); public virtual void ReadFromConfig(OsuConfigManager config) From 64e3325b4140bf89e30cb8cb2e7f9fa90dd64716 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 18:00:26 +0900 Subject: [PATCH 4421/6909] Abstract out common part of legacy spinner implementations Some elements going forward will be shared, so it makes sense to have a common base class to add these shared elements. --- .../Skinning/LegacyNewStyleSpinner.cs | 33 +++----------- .../Skinning/LegacyOldStyleSpinner.cs | 31 +++---------- .../Skinning/LegacySpinner.cs | 44 +++++++++++++++++++ 3 files changed, 57 insertions(+), 51 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 31e2ab1239..5b6aac8f08 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -3,7 +3,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; @@ -19,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning /// Legacy skinned spinner with two main spinning layers, one fixed overlay and one final spinning overlay. /// No background layer. /// - public class LegacyNewStyleSpinner : CompositeDrawable + public class LegacyNewStyleSpinner : LegacySpinner { private Sprite glow; private Sprite discBottom; @@ -27,17 +26,13 @@ namespace osu.Game.Rulesets.Osu.Skinning private Sprite spinningMiddle; private Sprite fixedMiddle; - private DrawableSpinner drawableSpinner; - private const float final_scale = 0.625f; private readonly Color4 glowColour = new Color4(3, 151, 255, 255); [BackgroundDependencyLoader] - private void load(ISkinSource source, DrawableHitObject drawableObject) + private void load(ISkinSource source) { - drawableSpinner = (DrawableSpinner)drawableObject; - Scale = new Vector2(final_scale); InternalChildren = new Drawable[] @@ -77,16 +72,10 @@ namespace osu.Game.Rulesets.Osu.Skinning }; } - protected override void LoadComplete() + protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - base.LoadComplete(); + base.UpdateStateTransforms(drawableHitObject, state); - drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); - } - - private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) - { switch (drawableHitObject) { case DrawableSpinner d: @@ -125,20 +114,12 @@ namespace osu.Game.Rulesets.Osu.Skinning protected override void Update() { base.Update(); - spinningMiddle.Rotation = discTop.Rotation = drawableSpinner.RotationTracker.Rotation; + spinningMiddle.Rotation = discTop.Rotation = DrawableSpinner.RotationTracker.Rotation; discBottom.Rotation = discTop.Rotation / 3; - glow.Alpha = drawableSpinner.Progress; + glow.Alpha = DrawableSpinner.Progress; - Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, drawableSpinner.Progress) * 0.2f)); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (drawableSpinner != null) - drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; + Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f)); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 1954ff6e38..56702e6712 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -18,9 +18,8 @@ namespace osu.Game.Rulesets.Osu.Skinning /// /// Legacy skinned spinner with one main spinning layer and a background layer. /// - public class LegacyOldStyleSpinner : CompositeDrawable + public class LegacyOldStyleSpinner : LegacySpinner { - private DrawableSpinner drawableSpinner; private Sprite disc; private Sprite metreSprite; private Container metre; @@ -31,14 +30,10 @@ namespace osu.Game.Rulesets.Osu.Skinning private const float final_metre_height = 692 * sprite_scale; [BackgroundDependencyLoader] - private void load(ISkinSource source, DrawableHitObject drawableObject) + private void load(ISkinSource source) { spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true; - drawableSpinner = (DrawableSpinner)drawableObject; - - RelativeSizeAxes = Axes.Both; - InternalChild = new Container { // the old-style spinner relied heavily on absolute screen-space coordinate values. @@ -85,16 +80,10 @@ namespace osu.Game.Rulesets.Osu.Skinning }; } - protected override void LoadComplete() + protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { - base.LoadComplete(); + base.UpdateStateTransforms(drawableHitObject, state); - drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; - updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); - } - - private void updateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) - { if (!(drawableHitObject is DrawableSpinner d)) return; @@ -110,11 +99,11 @@ namespace osu.Game.Rulesets.Osu.Skinning protected override void Update() { base.Update(); - disc.Rotation = drawableSpinner.RotationTracker.Rotation; + disc.Rotation = DrawableSpinner.RotationTracker.Rotation; // careful: need to call this exactly once for all calculations in a frame // as the function has a random factor in it - var metreHeight = getMetreHeight(drawableSpinner.Progress); + var metreHeight = getMetreHeight(DrawableSpinner.Progress); // hack to make the metre blink up from below than down from above. // move down the container to be able to apply masking for the metre, @@ -140,13 +129,5 @@ namespace osu.Game.Rulesets.Osu.Skinning return (float)barCount / total_bars * final_metre_height; } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (drawableSpinner != null) - drawableSpinner.ApplyCustomUpdateState -= updateStateTransforms; - } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs new file mode 100644 index 0000000000..efbafdc17a --- /dev/null +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.Skinning +{ + public abstract class LegacySpinner : CompositeDrawable + { + protected DrawableSpinner DrawableSpinner { get; private set; } + + [BackgroundDependencyLoader] + private void load(DrawableHitObject drawableHitObject) + { + RelativeSizeAxes = Axes.Both; + + DrawableSpinner = (DrawableSpinner)drawableHitObject; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms; + UpdateStateTransforms(DrawableSpinner, DrawableSpinner.State.Value); + } + + protected virtual void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) + { + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (DrawableSpinner != null) + DrawableSpinner.ApplyCustomUpdateState -= UpdateStateTransforms; + } + } +} From 11c18952e3138c7bd2f85a8a9336e55c888f6c38 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 18:03:24 +0900 Subject: [PATCH 4422/6909] Allow children to be added in base class --- osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs | 4 ++-- osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 5b6aac8f08..f77a722376 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { Scale = new Vector2(final_scale); - InternalChildren = new Drawable[] + AddRangeInternal(new Drawable[] { glow = new Sprite { @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Origin = Anchor.Centre, Texture = source.GetTexture("spinner-middle2") } - }; + }); } protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index 56702e6712..dd63879247 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true; - InternalChild = new Container + AddInternal(new Container { // the old-style spinner relied heavily on absolute screen-space coordinate values. // wrap everything in a container simulating absolute coords to preserve alignment @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Osu.Skinning } } } - }; + }); } protected override void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) From 5ec6011340cb05ece29f815e60afc6577c05bc2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 18:12:02 +0900 Subject: [PATCH 4423/6909] Apply new style spinner scale to only local sprites --- .../Skinning/LegacyNewStyleSpinner.cs | 76 ++++++++++--------- 1 file changed, 42 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index f77a722376..3a60f54d24 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -3,6 +3,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; @@ -30,44 +31,51 @@ namespace osu.Game.Rulesets.Osu.Skinning private readonly Color4 glowColour = new Color4(3, 151, 255, 255); + private Container scaleContainer; + [BackgroundDependencyLoader] private void load(ISkinSource source) { - Scale = new Vector2(final_scale); - - AddRangeInternal(new Drawable[] + AddInternal(scaleContainer = new Container { - glow = new Sprite + Scale = new Vector2(final_scale), + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-glow"), - Blending = BlendingParameters.Additive, - Colour = glowColour, - }, - discBottom = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-bottom") - }, - discTop = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-top") - }, - fixedMiddle = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-middle") - }, - spinningMiddle = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-middle2") + glow = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-glow"), + Blending = BlendingParameters.Additive, + Colour = glowColour, + }, + discBottom = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-bottom") + }, + discTop = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-top") + }, + fixedMiddle = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-middle") + }, + spinningMiddle = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-middle2") + } } }); } @@ -119,7 +127,7 @@ namespace osu.Game.Rulesets.Osu.Skinning glow.Alpha = DrawableSpinner.Progress; - Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f)); + scaleContainer.Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f)); } } } From 3ec813da03c636d3fb936c1da04be597a45f95d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 18:12:13 +0900 Subject: [PATCH 4424/6909] Add "spin" sprite --- .../Skinning/LegacySpinner.cs | 38 ++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs index efbafdc17a..62df4989ba 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.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; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Skinning { @@ -13,12 +16,26 @@ namespace osu.Game.Rulesets.Osu.Skinning { protected DrawableSpinner DrawableSpinner { get; private set; } + private Sprite spin; + [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableHitObject) + private void load(DrawableHitObject drawableHitObject, ISkinSource source) { RelativeSizeAxes = Axes.Both; DrawableSpinner = (DrawableSpinner)drawableHitObject; + + AddRangeInternal(new[] + { + spin = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Depth = float.MinValue, + Texture = source.GetTexture("spinner-spin"), + Y = 120 // todo: make match roughly? + }, + }); } protected override void LoadComplete() @@ -31,6 +48,25 @@ namespace osu.Game.Rulesets.Osu.Skinning protected virtual void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { + switch (drawableHitObject) + { + case DrawableSpinner d: + double fadeOutLength = Math.Min(400, d.HitObject.Duration); + + using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true)) + spin.FadeOutFromOne(fadeOutLength); + + break; + + case DrawableSpinnerTick d: + if (state == ArmedState.Hit) + { + using (BeginAbsoluteSequence(d.HitStateUpdateTime, true)) + spin.FadeOut(300); + } + + break; + } } protected override void Dispose(bool isDisposing) From a0b3379909aaaf14fa1bdc5dd31e9d8d380dd5e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 18:58:37 +0900 Subject: [PATCH 4425/6909] Fix judgement offsets being zero when windows are empty --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index c7e2e0638c..5c3f57c2d0 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -546,7 +546,11 @@ namespace osu.Game.Rulesets.Objects.Drawables // Ensure that the judgement is given a valid time offset, because this may not get set by the caller var endTime = HitObject.GetEndTime(); - Result.TimeOffset = Math.Min(HitObject.HitWindows.WindowFor(HitResult.Miss), Time.Current - endTime); + Result.TimeOffset = Time.Current - endTime; + + double missWindow = HitObject.HitWindows.WindowFor(HitResult.Miss); + if (missWindow > 0) + Result.TimeOffset = Math.Min(Result.TimeOffset, missWindow); if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); From 901102918ee29241406f716e3a2c014a8995c116 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 19:05:59 +0900 Subject: [PATCH 4426/6909] Centralise scale and apply to SPIN text --- osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs | 6 ++---- osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs | 9 ++++----- osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs | 4 ++++ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs index 3a60f54d24..05f4c8e307 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs @@ -27,8 +27,6 @@ namespace osu.Game.Rulesets.Osu.Skinning private Sprite spinningMiddle; private Sprite fixedMiddle; - private const float final_scale = 0.625f; - private readonly Color4 glowColour = new Color4(3, 151, 255, 255); private Container scaleContainer; @@ -38,7 +36,7 @@ namespace osu.Game.Rulesets.Osu.Skinning { AddInternal(scaleContainer = new Container { - Scale = new Vector2(final_scale), + Scale = new Vector2(SPRITE_SCALE), Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, @@ -127,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Skinning glow.Alpha = DrawableSpinner.Progress; - scaleContainer.Scale = new Vector2(final_scale * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f)); + scaleContainer.Scale = new Vector2(SPRITE_SCALE * (0.8f + (float)Interpolation.ApplyEasing(Easing.Out, DrawableSpinner.Progress) * 0.2f)); } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs index dd63879247..fba802f085 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs @@ -26,8 +26,7 @@ namespace osu.Game.Rulesets.Osu.Skinning private bool spinnerBlink; - private const float sprite_scale = 1 / 1.6f; - private const float final_metre_height = 692 * sprite_scale; + private const float final_metre_height = 692 * SPRITE_SCALE; [BackgroundDependencyLoader] private void load(ISkinSource source) @@ -50,14 +49,14 @@ namespace osu.Game.Rulesets.Osu.Skinning Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-background"), - Scale = new Vector2(sprite_scale) + Scale = new Vector2(SPRITE_SCALE) }, disc = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-circle"), - Scale = new Vector2(sprite_scale) + Scale = new Vector2(SPRITE_SCALE) }, metre = new Container { @@ -73,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Texture = source.GetTexture("spinner-metre"), Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Scale = new Vector2(0.625f) + Scale = new Vector2(SPRITE_SCALE) } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs index 62df4989ba..2d0d350fce 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs @@ -9,11 +9,14 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Osu.Skinning { public abstract class LegacySpinner : CompositeDrawable { + protected const float SPRITE_SCALE = 0.625f; + protected DrawableSpinner DrawableSpinner { get; private set; } private Sprite spin; @@ -33,6 +36,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Origin = Anchor.Centre, Depth = float.MinValue, Texture = source.GetTexture("spinner-spin"), + Scale = new Vector2(SPRITE_SCALE), Y = 120 // todo: make match roughly? }, }); From 5e387e92cd9e2848c562c0dbc05b72420567de94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 19:16:47 +0900 Subject: [PATCH 4427/6909] Add positional offset to avoid the SPM counter --- osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs index 2d0d350fce..31350791e3 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Osu.Skinning Depth = float.MinValue, Texture = source.GetTexture("spinner-spin"), Scale = new Vector2(SPRITE_SCALE), - Y = 120 // todo: make match roughly? + Y = 120 - 45 // offset temporarily to avoid overlapping default spin counter }, }); } From 71253cb5e9aeba420d4d8062f41d80917abeb8db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 19:35:32 +0900 Subject: [PATCH 4428/6909] Add support for spinner "clear" text in legacy skins --- .../Skinning/LegacySpinner.cs | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs index 31350791e3..6c276f951e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -20,6 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning protected DrawableSpinner DrawableSpinner { get; private set; } private Sprite spin; + private Sprite clear; [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject, ISkinSource source) @@ -39,17 +41,50 @@ namespace osu.Game.Rulesets.Osu.Skinning Scale = new Vector2(SPRITE_SCALE), Y = 120 - 45 // offset temporarily to avoid overlapping default spin counter }, + clear = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Depth = float.MinValue, + Alpha = 0, + Texture = source.GetTexture("spinner-clear"), + Scale = new Vector2(SPRITE_SCALE), + Y = -60 + }, }); } + private readonly Bindable completed = new Bindable(); + protected override void LoadComplete() { base.LoadComplete(); + completed.BindTo(DrawableSpinner.RotationTracker.Complete); + completed.BindValueChanged(onCompletedChanged, true); + DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms; UpdateStateTransforms(DrawableSpinner, DrawableSpinner.State.Value); } + private void onCompletedChanged(ValueChangedEvent completed) + { + if (completed.NewValue) + { + clear.FadeInFromZero(400, Easing.Out); + clear.ScaleTo(SPRITE_SCALE * 2).Then().ScaleTo(SPRITE_SCALE, 400, Easing.OutElastic); + + const double fade_out_duration = 50; + using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration, true)) + clear.FadeOut(fade_out_duration); + } + else + { + clear.ClearTransforms(); + clear.Alpha = 0; + } + } + protected virtual void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { switch (drawableHitObject) @@ -59,7 +94,6 @@ namespace osu.Game.Rulesets.Osu.Skinning using (BeginAbsoluteSequence(drawableHitObject.HitStateUpdateTime - fadeOutLength, true)) spin.FadeOutFromOne(fadeOutLength); - break; case DrawableSpinnerTick d: From d7a912a0d2784b5096e8607388730ee61492af9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 19:44:34 +0900 Subject: [PATCH 4429/6909] Match stable's transform 1:1 --- osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs index 6c276f951e..eb9fa85fde 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs @@ -71,8 +71,16 @@ namespace osu.Game.Rulesets.Osu.Skinning { if (completed.NewValue) { - clear.FadeInFromZero(400, Easing.Out); - clear.ScaleTo(SPRITE_SCALE * 2).Then().ScaleTo(SPRITE_SCALE, 400, Easing.OutElastic); + double startTime = Math.Min(Time.Current, DrawableSpinner.HitStateUpdateTime - 400); + + using (BeginAbsoluteSequence(startTime, true)) + { + clear.FadeInFromZero(400, Easing.Out); + + clear.ScaleTo(SPRITE_SCALE * 2) + .Then().ScaleTo(SPRITE_SCALE * 0.8f, 240, Easing.Out) + .Then().ScaleTo(SPRITE_SCALE, 160); + } const double fade_out_duration = 50; using (BeginAbsoluteSequence(DrawableSpinner.HitStateUpdateTime - fade_out_duration, true)) From d774afd224de8bd014db5ab892f38dd22eb576e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 5 Nov 2020 22:29:44 +0900 Subject: [PATCH 4430/6909] 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 97812402a3..f56baf4e5f 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 e57807e989..3783ae7d5c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 40ecfffcca..ed3ec9e48b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 1e1569eb5302813472df97ea2e8af0ad11eda1cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 12:40:54 +0900 Subject: [PATCH 4431/6909] Use int instead of long for user_id fields for now --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 6 +++--- osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs | 2 +- osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs | 2 +- osu.Game/Online/API/Requests/Responses/APIChangelogUser.cs | 2 +- osu.Game/Online/Chat/Channel.cs | 2 +- osu.Game/OsuGame.cs | 4 ++-- osu.Game/Overlays/UserProfileOverlay.cs | 2 +- osu.Game/Scoring/ScoreInfo.cs | 2 +- osu.Game/Users/User.cs | 2 +- 9 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index df4b85b37a..72c6fd8d44 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -250,7 +250,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void EndPlay(int beatmapId) { - ((ISpectatorClient)this).UserFinishedPlaying((int)StreamingUser.Id, new SpectatorState + ((ISpectatorClient)this).UserFinishedPlaying(StreamingUser.Id, new SpectatorState { BeatmapID = beatmapId, RulesetID = 0, @@ -273,7 +273,7 @@ namespace osu.Game.Tests.Visual.Gameplay } var bundle = new FrameDataBundle(frames); - ((ISpectatorClient)this).UserSentFrames((int)StreamingUser.Id, bundle); + ((ISpectatorClient)this).UserSentFrames(StreamingUser.Id, bundle); if (!sentState) sendState(beatmapId); @@ -293,7 +293,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void sendState(int beatmapId) { sentState = true; - ((ISpectatorClient)this).UserBeganPlaying((int)StreamingUser.Id, new SpectatorState + ((ISpectatorClient)this).UserBeganPlaying(StreamingUser.Id, new SpectatorState { BeatmapID = beatmapId, RulesetID = 0, diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 7196f47bd6..582f72429b 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -277,7 +277,7 @@ namespace osu.Game.Tournament.Screens.Editors userId.Value = user.Id.ToString(); userId.BindValueChanged(idString => { - long.TryParse(idString.NewValue, out var parsed); + int.TryParse(idString.NewValue, out var parsed); user.Id = parsed; diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 1ca14256e5..6d0160fbc4 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -61,7 +61,7 @@ namespace osu.Game.Online.API.Requests.Responses private int[] ratings { get; set; } [JsonProperty(@"user_id")] - private long creatorId + private int creatorId { set => Author.Id = value; } diff --git a/osu.Game/Online/API/Requests/Responses/APIChangelogUser.cs b/osu.Game/Online/API/Requests/Responses/APIChangelogUser.cs index 5891391e83..024e1ce048 100644 --- a/osu.Game/Online/API/Requests/Responses/APIChangelogUser.cs +++ b/osu.Game/Online/API/Requests/Responses/APIChangelogUser.cs @@ -20,7 +20,7 @@ namespace osu.Game.Online.API.Requests.Responses public string OsuUsername { get; set; } [JsonProperty("user_id")] - public long? UserId { get; set; } + public int? UserId { get; set; } [JsonProperty("user_url")] public string UserUrl { get; set; } diff --git a/osu.Game/Online/Chat/Channel.cs b/osu.Game/Online/Chat/Channel.cs index 8c1e1ad128..187a3e5dfc 100644 --- a/osu.Game/Online/Chat/Channel.cs +++ b/osu.Game/Online/Chat/Channel.cs @@ -22,7 +22,7 @@ namespace osu.Game.Online.Chat public readonly ObservableCollection Users = new ObservableCollection(); [JsonProperty(@"users")] - private long[] userIds + private int[] userIds { set { diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a0ddab702e..64f8d4415b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -278,7 +278,7 @@ namespace osu.Game break; case LinkAction.OpenUserProfile: - if (long.TryParse(link.Argument, out long userId)) + if (int.TryParse(link.Argument, out int userId)) ShowUser(userId); break; @@ -321,7 +321,7 @@ namespace osu.Game /// Show a user's profile as an overlay. /// /// The user to display. - public void ShowUser(long userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId)); + public void ShowUser(int userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId)); /// /// Show a beatmap's set as an overlay, displaying the given beatmap. diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index d52ad84592..81027667fa 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -33,7 +33,7 @@ namespace osu.Game.Overlays { } - public void ShowUser(long userId) => ShowUser(new User { Id = userId }); + public void ShowUser(int userId) => ShowUser(new User { Id = userId }); public void ShowUser(User user, bool fetchOnline = true) { diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index 596e98a6bd..f5192f3a40 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -123,7 +123,7 @@ namespace osu.Game.Scoring [JsonIgnore] [Column("UserID")] - public long? UserID + public int? UserID { get => User?.Id ?? 1; set diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 89786e3bd8..1fbc3d06f4 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -12,7 +12,7 @@ namespace osu.Game.Users public class User : IEquatable { [JsonProperty(@"id")] - public long Id = 1; + public int Id = 1; [JsonProperty(@"join_date")] public DateTimeOffset JoinDate; From 5113d4af8ff12c695e1e6838e387d76763ddee75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 13:14:23 +0900 Subject: [PATCH 4432/6909] Rename BeatmapDifficultyManager to BeatmapDifficultyCache --- .../Beatmaps/BeatmapDifficultyManagerTest.cs | 10 +++++----- ...ifficultyManager.cs => BeatmapDifficultyCache.cs} | 12 ++++++++---- osu.Game/Beatmaps/BeatmapInfo.cs | 2 +- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 6 +++--- osu.Game/OsuGameBase.cs | 8 ++++---- osu.Game/Scoring/ScoreManager.cs | 10 +++++----- osu.Game/Scoring/ScorePerformanceManager.cs | 4 ++-- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 4 ++-- .../Screens/Ranking/Expanded/StarRatingDisplay.cs | 2 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 4 ++-- .../Select/Carousel/DrawableCarouselBeatmap.cs | 4 ++-- osu.Game/Screens/Select/Details/AdvancedStats.cs | 6 +++--- 12 files changed, 38 insertions(+), 34 deletions(-) rename osu.Game/Beatmaps/{BeatmapDifficultyManager.cs => BeatmapDifficultyCache.cs} (97%) diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs index 7c1ddd757f..ec77f48063 100644 --- a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs +++ b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs @@ -14,8 +14,8 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestKeyEqualsWithDifferentModInstances() { - var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); - var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); Assert.That(key1, Is.EqualTo(key2)); } @@ -23,8 +23,8 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestKeyEqualsWithDifferentModOrder() { - var key1 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); - var key2 = new BeatmapDifficultyManager.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); Assert.That(key1, Is.EqualTo(key2)); } @@ -47,7 +47,7 @@ namespace osu.Game.Tests.Beatmaps [TestCase(8.3, DifficultyRating.ExpertPlus)] public void TestDifficultyRatingMapping(double starRating, DifficultyRating expectedBracket) { - var actualBracket = BeatmapDifficultyManager.GetDifficultyRating(starRating); + var actualBracket = BeatmapDifficultyCache.GetDifficultyRating(starRating); Assert.AreEqual(expectedBracket, actualBracket); } diff --git a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs similarity index 97% rename from osu.Game/Beatmaps/BeatmapDifficultyManager.cs rename to osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 9e83738e70..dafe7c19e6 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyManager.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -11,7 +11,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Threading; @@ -23,10 +23,14 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Beatmaps { - public class BeatmapDifficultyManager : CompositeDrawable + /// + /// A component which performs and acts as a central cache for difficulty calculations of beatmap/ruleset/mod combinations. + /// Currently not persisted between game sessions. + /// + public class BeatmapDifficultyCache : Component { // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes. - private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyManager)); + private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyCache)); // A permanent cache to prevent re-computations. private readonly ConcurrentDictionary difficultyCache = new ConcurrentDictionary(); @@ -387,6 +391,6 @@ namespace osu.Game.Beatmaps Attributes = null; } - public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(Stars); + public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(Stars); } } diff --git a/osu.Game/Beatmaps/BeatmapInfo.cs b/osu.Game/Beatmaps/BeatmapInfo.cs index ffd8d14048..a898e10e4f 100644 --- a/osu.Game/Beatmaps/BeatmapInfo.cs +++ b/osu.Game/Beatmaps/BeatmapInfo.cs @@ -136,7 +136,7 @@ namespace osu.Game.Beatmaps public List Scores { get; set; } [JsonIgnore] - public DifficultyRating DifficultyRating => BeatmapDifficultyManager.GetDifficultyRating(StarDifficulty); + public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(StarDifficulty); public string[] SearchableTerms => new[] { diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index a1d5e33d1e..96e18f120a 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -142,7 +142,7 @@ namespace osu.Game.Beatmaps.Drawables private CancellationTokenSource difficultyCancellation; [Resolved] - private BeatmapDifficultyManager difficultyManager { get; set; } + private BeatmapDifficultyCache difficultyCache { get; set; } public DifficultyRetriever(BeatmapInfo beatmap, RulesetInfo ruleset, IReadOnlyList mods) { @@ -158,8 +158,8 @@ namespace osu.Game.Beatmaps.Drawables { difficultyCancellation = new CancellationTokenSource(); localStarDifficulty = ruleset != null - ? difficultyManager.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token) - : difficultyManager.GetBindableDifficulty(beatmap, difficultyCancellation.Token); + ? difficultyCache.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token) + : difficultyCache.GetBindableDifficulty(beatmap, difficultyCancellation.Token); localStarDifficulty.BindValueChanged(difficulty => StarDifficulty.Value = difficulty.NewValue); } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 4bc54e7e83..fa183beeb9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -59,7 +59,7 @@ namespace osu.Game protected ScoreManager ScoreManager; - protected BeatmapDifficultyManager DifficultyManager; + protected BeatmapDifficultyCache DifficultyCache; protected SkinManager SkinManager; @@ -202,7 +202,7 @@ namespace osu.Game 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, () => DifficultyManager, LocalConfig)); + dependencies.Cache(ScoreManager = new ScoreManager(RulesetStore, () => BeatmapManager, Storage, API, contextFactory, Host, () => DifficultyCache, LocalConfig)); dependencies.Cache(BeatmapManager = new BeatmapManager(Storage, contextFactory, RulesetStore, API, Audio, Host, defaultBeatmap, true)); // this should likely be moved to ArchiveModelManager when another case appers where it is necessary @@ -226,8 +226,8 @@ namespace osu.Game ScoreManager.Undelete(getBeatmapScores(item), true); }); - dependencies.Cache(DifficultyManager = new BeatmapDifficultyManager()); - AddInternal(DifficultyManager); + dependencies.Cache(DifficultyCache = new BeatmapDifficultyCache()); + AddInternal(DifficultyCache); var scorePerformanceManager = new ScorePerformanceManager(); dependencies.Cache(scorePerformanceManager); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index cce6153953..cf1d123c06 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -37,13 +37,13 @@ namespace osu.Game.Scoring private readonly Func beatmaps; [CanBeNull] - private readonly Func difficulties; + private readonly Func difficulties; [CanBeNull] private readonly OsuConfigManager configManager; public ScoreManager(RulesetStore rulesets, Func beatmaps, Storage storage, IAPIProvider api, IDatabaseContextFactory contextFactory, IIpcHost importHost = null, - Func difficulties = null, OsuConfigManager configManager = null) + Func difficulties = null, OsuConfigManager configManager = null) : base(storage, contextFactory, api, new ScoreStore(contextFactory, storage), importHost) { this.rulesets = rulesets; @@ -121,14 +121,14 @@ namespace osu.Game.Scoring public readonly Bindable ScoringMode = new Bindable(); private readonly ScoreInfo score; - private readonly Func difficulties; + private readonly Func difficulties; /// /// Creates a new . /// /// The to provide the total score of. - /// A function to retrieve the . - public TotalScoreBindable(ScoreInfo score, Func difficulties) + /// A function to retrieve the . + public TotalScoreBindable(ScoreInfo score, Func difficulties) { this.score = score; this.difficulties = difficulties; diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceManager.cs index ddda1b99af..326a2fce7f 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceManager.cs @@ -22,7 +22,7 @@ namespace osu.Game.Scoring private readonly ConcurrentDictionary performanceCache = new ConcurrentDictionary(); [Resolved] - private BeatmapDifficultyManager difficultyManager { get; set; } + private BeatmapDifficultyCache difficultyCache { get; set; } /// /// Calculates performance for the given . @@ -41,7 +41,7 @@ namespace osu.Game.Scoring private async Task computePerformanceAsync(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) { - var attributes = await difficultyManager.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); + var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. if (attributes.Attributes == null) diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index f9b7625913..33ee5d2ee4 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -56,7 +56,7 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load(BeatmapDifficultyManager beatmapDifficultyManager) + private void load(BeatmapDifficultyCache beatmapDifficultyCache) { var beatmap = score.Beatmap; var metadata = beatmap.BeatmapSet?.Metadata ?? beatmap.Metadata; @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Ranking.Expanded Spacing = new Vector2(5, 0), Children = new Drawable[] { - new StarRatingDisplay(beatmapDifficultyManager.GetDifficulty(beatmap, score.Ruleset, score.Mods)) + new StarRatingDisplay(beatmapDifficultyCache.GetDifficulty(beatmap, score.Ruleset, score.Mods)) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft diff --git a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs index ffb12d474b..f7e50fdc8a 100644 --- a/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs +++ b/osu.Game/Screens/Ranking/Expanded/StarRatingDisplay.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Ranking.Expanded } [BackgroundDependencyLoader] - private void load(OsuColour colours, BeatmapDifficultyManager difficultyManager) + private void load(OsuColour colours, BeatmapDifficultyCache difficultyCache) { AutoSizeAxes = Axes.Both; diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 2634f117de..04c1f6efe4 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Select private readonly IBindable ruleset = new Bindable(); [Resolved] - private BeatmapDifficultyManager difficultyManager { get; set; } + private BeatmapDifficultyCache difficultyCache { get; set; } private IBindable beatmapDifficulty; @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Select cancellationSource = new CancellationTokenSource(); beatmapDifficulty?.UnbindAll(); - beatmapDifficulty = difficultyManager.GetBindableDifficulty(beatmap.BeatmapInfo, cancellationSource.Token); + beatmapDifficulty = difficultyCache.GetBindableDifficulty(beatmap.BeatmapInfo, cancellationSource.Token); beatmapDifficulty.BindValueChanged(_ => updateDisplay()); updateDisplay(); diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index 49a370724e..e66469ff8d 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -55,7 +55,7 @@ namespace osu.Game.Screens.Select.Carousel private BeatmapSetOverlay beatmapOverlay { get; set; } [Resolved] - private BeatmapDifficultyManager difficultyManager { get; set; } + private BeatmapDifficultyCache difficultyCache { get; set; } [Resolved(CanBeNull = true)] private CollectionManager collectionManager { get; set; } @@ -216,7 +216,7 @@ namespace osu.Game.Screens.Select.Carousel if (Item.State.Value != CarouselItemState.Collapsed) { // We've potentially cancelled the computation above so a new bindable is required. - starDifficultyBindable = difficultyManager.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); + starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true); } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 44c328187f..44d908fc46 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Select.Details private IBindable ruleset { get; set; } [Resolved] - private BeatmapDifficultyManager difficultyManager { get; set; } + private BeatmapDifficultyCache difficultyCache { get; set; } protected readonly StatisticRow FirstValue, HpDrain, Accuracy, ApproachRate; private readonly StatisticRow starDifficulty; @@ -161,8 +161,8 @@ namespace osu.Game.Screens.Select.Details starDifficultyCancellationSource = new CancellationTokenSource(); - normalStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); - moddedStarDifficulty = difficultyManager.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); + normalStarDifficulty = difficultyCache.GetBindableDifficulty(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); + moddedStarDifficulty = difficultyCache.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); normalStarDifficulty.BindValueChanged(_ => updateDisplay()); moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true); From 14bb079feb84717965b086e0949f865e2aeac486 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 13:15:33 +0900 Subject: [PATCH 4433/6909] Rename ScorePerformanceManager to ScorePerformanceCache --- osu.Game/OsuGameBase.cs | 2 +- .../{ScorePerformanceManager.cs => ScorePerformanceCache.cs} | 5 +++-- .../Ranking/Expanded/Statistics/PerformanceStatistic.cs | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) rename osu.Game/Scoring/{ScorePerformanceManager.cs => ScorePerformanceCache.cs} (92%) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index fa183beeb9..3da692249d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -229,7 +229,7 @@ namespace osu.Game dependencies.Cache(DifficultyCache = new BeatmapDifficultyCache()); AddInternal(DifficultyCache); - var scorePerformanceManager = new ScorePerformanceManager(); + var scorePerformanceManager = new ScorePerformanceCache(); dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); diff --git a/osu.Game/Scoring/ScorePerformanceManager.cs b/osu.Game/Scoring/ScorePerformanceCache.cs similarity index 92% rename from osu.Game/Scoring/ScorePerformanceManager.cs rename to osu.Game/Scoring/ScorePerformanceCache.cs index 326a2fce7f..8b764c75b7 100644 --- a/osu.Game/Scoring/ScorePerformanceManager.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -13,9 +13,10 @@ using osu.Game.Beatmaps; namespace osu.Game.Scoring { /// - /// A global component which calculates and caches results of performance calculations for locally databased scores. + /// A component which performs and acts as a central cache for performance calculations of locally databased scores. + /// Currently not persisted between game sessions. /// - public class ScorePerformanceManager : Component + public class ScorePerformanceCache : Component { // this cache will grow indefinitely per session and should be considered temporary. // this whole component should likely be replaced with database persistence. diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index cd9d8005c6..730221cc4b 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } [BackgroundDependencyLoader] - private void load(ScorePerformanceManager performanceManager) + private void load(ScorePerformanceCache performanceCache) { if (score.PP.HasValue) { @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics } else { - performanceManager.CalculatePerformanceAsync(score, cancellationTokenSource.Token) + performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) .ContinueWith(t => Schedule(() => setPerformanceValue(t.Result)), cancellationTokenSource.Token); } } From 0103b1257574df20dcbe73e19eb0daa2b8ac1a57 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 13:26:18 +0900 Subject: [PATCH 4434/6909] Add basic base class to begin to standardise function across caching components --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 15 ++++++--------- osu.Game/Database/MemoryCachingComponent.cs | 17 +++++++++++++++++ osu.Game/Scoring/ScorePerformanceCache.cs | 13 ++++--------- .../Expanded/Statistics/PerformanceStatistic.cs | 2 +- 4 files changed, 28 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Database/MemoryCachingComponent.cs diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index dafe7c19e6..3ca0b47121 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -11,11 +11,11 @@ using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Threading; using osu.Framework.Utils; +using osu.Game.Database; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; @@ -27,14 +27,11 @@ namespace osu.Game.Beatmaps /// A component which performs and acts as a central cache for difficulty calculations of beatmap/ruleset/mod combinations. /// Currently not persisted between game sessions. /// - public class BeatmapDifficultyCache : Component + public class BeatmapDifficultyCache : MemoryCachingComponent { // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes. private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyCache)); - // A permanent cache to prevent re-computations. - private readonly ConcurrentDictionary difficultyCache = new ConcurrentDictionary(); - // All bindables that should be updated along with the current ruleset + mods. private readonly LockedWeakList trackedBindables = new LockedWeakList(); @@ -243,7 +240,7 @@ namespace osu.Game.Beatmaps var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); var attributes = calculator.Calculate(key.Mods); - return difficultyCache[key] = new StarDifficulty(attributes); + return Cache[key] = new StarDifficulty(attributes); } catch (BeatmapInvalidForRulesetException e) { @@ -254,7 +251,7 @@ namespace osu.Game.Beatmaps if (rulesetInfo.Equals(beatmapInfo.Ruleset)) { Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset})."); - return difficultyCache[key] = new StarDifficulty(); + return Cache[key] = new StarDifficulty(); } // Check the cache first because this is now a different ruleset than the one previously guarded against. @@ -265,7 +262,7 @@ namespace osu.Game.Beatmaps } catch { - return difficultyCache[key] = new StarDifficulty(); + return Cache[key] = new StarDifficulty(); } } @@ -294,7 +291,7 @@ namespace osu.Game.Beatmaps } key = new DifficultyCacheLookup(beatmapInfo.ID, rulesetInfo.ID.Value, mods); - return difficultyCache.TryGetValue(key, out existingDifficulty); + return Cache.TryGetValue(key, out existingDifficulty); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs new file mode 100644 index 0000000000..85cf3b8af1 --- /dev/null +++ b/osu.Game/Database/MemoryCachingComponent.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.Collections.Concurrent; +using osu.Framework.Graphics; + +namespace osu.Game.Database +{ + /// + /// A component which performs lookups (or calculations) and caches the results. + /// Currently not persisted between game sessions. + /// + public abstract class MemoryCachingComponent : Component + { + protected readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); + } +} diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index 8b764c75b7..435b93d7af 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -2,13 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Concurrent; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Game.Beatmaps; +using osu.Game.Database; namespace osu.Game.Scoring { @@ -16,12 +15,8 @@ namespace osu.Game.Scoring /// A component which performs and acts as a central cache for performance calculations of locally databased scores. /// Currently not persisted between game sessions. /// - public class ScorePerformanceCache : Component + public class ScorePerformanceCache : MemoryCachingComponent { - // this cache will grow indefinitely per session and should be considered temporary. - // this whole component should likely be replaced with database persistence. - private readonly ConcurrentDictionary performanceCache = new ConcurrentDictionary(); - [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } @@ -34,7 +29,7 @@ namespace osu.Game.Scoring { var lookupKey = new PerformanceCacheLookup(score); - if (performanceCache.TryGetValue(lookupKey, out double performance)) + if (Cache.TryGetValue(lookupKey, out double performance)) return Task.FromResult((double?)performance); return computePerformanceAsync(score, lookupKey, token); @@ -54,7 +49,7 @@ namespace osu.Game.Scoring var total = calculator?.Calculate(); if (total.HasValue) - performanceCache[lookupKey] = total.Value; + Cache[lookupKey] = total.Value; return total; } diff --git a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs index 730221cc4b..68da4ec724 100644 --- a/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs +++ b/osu.Game/Screens/Ranking/Expanded/Statistics/PerformanceStatistic.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Ranking.Expanded.Statistics else { performanceCache.CalculatePerformanceAsync(score, cancellationTokenSource.Token) - .ContinueWith(t => Schedule(() => setPerformanceValue(t.Result)), cancellationTokenSource.Token); + .ContinueWith(t => Schedule(() => setPerformanceValue(t.Result)), cancellationTokenSource.Token); } } From 07166ec819970757a77a6646e3d6198162c9171f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 13:29:47 +0900 Subject: [PATCH 4435/6909] Fix a couple of remaining unnecessary casts --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- osu.Game/Screens/Play/Spectator.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 697ceacf0a..dae27f35ae 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays.Dashboard var request = new GetUserRequest(u); request.Success += user => Schedule(() => { - if (playingUsers.Contains((int)user.Id)) + if (playingUsers.Contains(user.Id)) userFlow.Add(createUserPanel(user)); }); api.Queue(request); diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 9ed911efd5..0f593db277 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -182,7 +182,7 @@ namespace osu.Game.Screens.Play spectatorStreaming.OnUserFinishedPlaying += userFinishedPlaying; spectatorStreaming.OnNewFrames += userSentFrames; - spectatorStreaming.WatchUser((int)targetUser.Id); + spectatorStreaming.WatchUser(targetUser.Id); managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); @@ -353,7 +353,7 @@ namespace osu.Game.Screens.Play spectatorStreaming.OnUserFinishedPlaying -= userFinishedPlaying; spectatorStreaming.OnNewFrames -= userSentFrames; - spectatorStreaming.StopWatchingUser((int)targetUser.Id); + spectatorStreaming.StopWatchingUser(targetUser.Id); } managerUpdated?.UnbindAll(); From 74ca2faa31805e8a2aeac5def1170ab7945957d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 13:48:06 +0900 Subject: [PATCH 4436/6909] Remove unused using --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 3ca0b47121..be278622b5 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Linq; From 517a65689964581396b85427ab4100eb87ae4cab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 13:51:25 +0900 Subject: [PATCH 4437/6909] Move StarDifficulty to own file --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 46 ------------------ osu.Game/Beatmaps/StarDifficulty.cs | 53 +++++++++++++++++++++ 2 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 osu.Game/Beatmaps/StarDifficulty.cs diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index be278622b5..af1b1de0c1 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -16,7 +16,6 @@ using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Database; using osu.Game.Rulesets; -using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -344,49 +343,4 @@ namespace osu.Game.Beatmaps } } } - - public readonly struct StarDifficulty - { - /// - /// The star difficulty rating for the given beatmap. - /// - public readonly double Stars; - - /// - /// The maximum combo achievable on the given beatmap. - /// - public readonly int MaxCombo; - - /// - /// The difficulty attributes computed for the given beatmap. - /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. - /// - [CanBeNull] - public readonly DifficultyAttributes Attributes; - - /// - /// Creates a structure based on computed - /// by a . - /// - public StarDifficulty([NotNull] DifficultyAttributes attributes) - { - Stars = attributes.StarRating; - MaxCombo = attributes.MaxCombo; - Attributes = attributes; - // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) - } - - /// - /// Creates a structure with a pre-populated star difficulty and max combo - /// in scenarios where computing is not feasible (i.e. when working with online sources). - /// - public StarDifficulty(double starDifficulty, int maxCombo) - { - Stars = starDifficulty; - MaxCombo = maxCombo; - Attributes = null; - } - - public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(Stars); - } } diff --git a/osu.Game/Beatmaps/StarDifficulty.cs b/osu.Game/Beatmaps/StarDifficulty.cs new file mode 100644 index 0000000000..f438b6f0bc --- /dev/null +++ b/osu.Game/Beatmaps/StarDifficulty.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Game.Rulesets.Difficulty; + +namespace osu.Game.Beatmaps +{ + public readonly struct StarDifficulty + { + /// + /// The star difficulty rating for the given beatmap. + /// + public readonly double Stars; + + /// + /// The maximum combo achievable on the given beatmap. + /// + public readonly int MaxCombo; + + /// + /// The difficulty attributes computed for the given beatmap. + /// Might not be available if the star difficulty is associated with a beatmap that's not locally available. + /// + [CanBeNull] + public readonly DifficultyAttributes Attributes; + + /// + /// Creates a structure based on computed + /// by a . + /// + public StarDifficulty([NotNull] DifficultyAttributes attributes) + { + Stars = attributes.StarRating; + MaxCombo = attributes.MaxCombo; + Attributes = attributes; + // Todo: Add more members (BeatmapInfo.DifficultyRating? Attributes? Etc...) + } + + /// + /// Creates a structure with a pre-populated star difficulty and max combo + /// in scenarios where computing is not feasible (i.e. when working with online sources). + /// + public StarDifficulty(double starDifficulty, int maxCombo) + { + Stars = starDifficulty; + MaxCombo = maxCombo; + Attributes = null; + } + + public DifficultyRating DifficultyRating => BeatmapDifficultyCache.GetDifficultyRating(Stars); + } +} From a2606d31c791d35e5d183ae6bfa1d0429fa5c55d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 13:50:51 +0900 Subject: [PATCH 4438/6909] Move lookup/storage/compute logic to base class (and consume in ScorePerformanceCache) --- osu.Game/Database/MemoryCachingComponent.cs | 33 ++++++++++++++++++- osu.Game/Scoring/ScorePerformanceCache.cs | 35 ++++++++------------- 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs index 85cf3b8af1..afdf37fa27 100644 --- a/osu.Game/Database/MemoryCachingComponent.cs +++ b/osu.Game/Database/MemoryCachingComponent.cs @@ -2,6 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Concurrent; +using System.Threading; +using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Graphics; namespace osu.Game.Database @@ -12,6 +15,34 @@ namespace osu.Game.Database /// public abstract class MemoryCachingComponent : Component { - protected readonly ConcurrentDictionary Cache = new ConcurrentDictionary(); + private readonly ConcurrentDictionary cache = new ConcurrentDictionary(); + + protected virtual bool CacheNullValues => true; + + /// + /// Retrieve the cached value for the given . + /// + /// The lookup to retrieve. + /// An optional to cancel the operation. + protected async Task GetAsync([NotNull] TLookup lookup, CancellationToken token = default) + { + if (cache.TryGetValue(lookup, out TValue performance)) + return performance; + + var computed = await ComputeValueAsync(lookup, token); + + if (computed != null || CacheNullValues) + cache[lookup] = computed; + + return computed; + } + + /// + /// Called on cache miss to compute the value for the specified lookup. + /// + /// The lookup to retrieve. + /// An optional to cancel the operation. + /// The computed value. + protected abstract Task ComputeValueAsync(TLookup lookup, CancellationToken token = default); } } diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index 435b93d7af..5f66c13d2f 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -15,28 +15,25 @@ namespace osu.Game.Scoring /// A component which performs and acts as a central cache for performance calculations of locally databased scores. /// Currently not persisted between game sessions. /// - public class ScorePerformanceCache : MemoryCachingComponent + public class ScorePerformanceCache : MemoryCachingComponent { [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } + protected override bool CacheNullValues => false; + /// /// Calculates performance for the given . /// /// The score to do the calculation on. /// An optional to cancel the operation. - public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) + public Task CalculatePerformanceAsync([NotNull] ScoreInfo score, CancellationToken token = default) => + GetAsync(new PerformanceCacheLookup(score), token); + + protected override async Task ComputeValueAsync(PerformanceCacheLookup lookup, CancellationToken token = default) { - var lookupKey = new PerformanceCacheLookup(score); + var score = lookup.ScoreInfo; - if (Cache.TryGetValue(lookupKey, out double performance)) - return Task.FromResult((double?)performance); - - return computePerformanceAsync(score, lookupKey, token); - } - - private async Task computePerformanceAsync(ScoreInfo score, PerformanceCacheLookup lookupKey, CancellationToken token = default) - { var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. @@ -46,31 +43,25 @@ namespace osu.Game.Scoring token.ThrowIfCancellationRequested(); var calculator = score.Ruleset.CreateInstance().CreatePerformanceCalculator(attributes.Attributes, score); - var total = calculator?.Calculate(); - if (total.HasValue) - Cache[lookupKey] = total.Value; - - return total; + return calculator?.Calculate(); } public readonly struct PerformanceCacheLookup { - public readonly string ScoreHash; - public readonly int LocalScoreID; + public readonly ScoreInfo ScoreInfo; public PerformanceCacheLookup(ScoreInfo info) { - ScoreHash = info.Hash; - LocalScoreID = info.ID; + ScoreInfo = info; } public override int GetHashCode() { var hash = new HashCode(); - hash.Add(ScoreHash); - hash.Add(LocalScoreID); + hash.Add(ScoreInfo.Hash); + hash.Add(ScoreInfo.ID); return hash.ToHashCode(); } From b69ada64e85a00cbe64604139fc32954804cd593 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 14:31:21 +0900 Subject: [PATCH 4439/6909] Update BeatmapDifficultyCache to use base implementation logic --- .../Beatmaps/BeatmapDifficultyManagerTest.cs | 9 +- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 130 ++++++++---------- osu.Game/Database/MemoryCachingComponent.cs | 5 +- 3 files changed, 63 insertions(+), 81 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs index ec77f48063..70503bec7a 100644 --- a/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs +++ b/osu.Game.Tests/Beatmaps/BeatmapDifficultyManagerTest.cs @@ -3,6 +3,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Mods; @@ -14,8 +15,8 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestKeyEqualsWithDifferentModInstances() { - var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); - var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); Assert.That(key1, Is.EqualTo(key2)); } @@ -23,8 +24,8 @@ namespace osu.Game.Tests.Beatmaps [Test] public void TestKeyEqualsWithDifferentModOrder() { - var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); - var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(1234, 0, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); + var key1 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHardRock(), new OsuModHidden() }); + var key2 = new BeatmapDifficultyCache.DifficultyCacheLookup(new BeatmapInfo { ID = 1234 }, new RulesetInfo { ID = 0 }, new Mod[] { new OsuModHidden(), new OsuModHardRock() }); Assert.That(key1, Is.EqualTo(key2)); } diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index af1b1de0c1..126e08a173 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -86,20 +86,30 @@ namespace osu.Game.Beatmaps /// The s to get the difficulty with. /// An optional which stops computing the star difficulty. /// The . - public async Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null, - CancellationToken cancellationToken = default) + public Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null, CancellationToken cancellationToken = default) { - if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) - return existing; + // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. + rulesetInfo ??= beatmapInfo.Ruleset; - return await Task.Factory.StartNew(() => + // Difficulty can only be computed if the beatmap and ruleset are locally available. + if (beatmapInfo.ID == 0 || rulesetInfo.ID == null) { - // Computation may have finished in a previous task. - if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out existing, out _)) + // If not, fall back to the existing star difficulty (e.g. from an online source). + return Task.FromResult(new StarDifficulty(beatmapInfo.StarDifficulty, beatmapInfo.MaxCombo ?? 0)); + } + + return GetAsync(new DifficultyCacheLookup(beatmapInfo, rulesetInfo, mods), cancellationToken); + } + + protected override Task ComputeValueAsync(DifficultyCacheLookup lookup, CancellationToken token = default) + { + return Task.Factory.StartNew(() => + { + if (CheckExists(lookup, out var existing)) return existing; - return computeDifficulty(key, beatmapInfo, rulesetInfo); - }, cancellationToken, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); + return computeDifficulty(lookup); + }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } /// @@ -111,10 +121,8 @@ namespace osu.Game.Beatmaps /// The . public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null) { - if (tryGetExisting(beatmapInfo, rulesetInfo, mods, out var existing, out var key)) - return existing; - - return computeDifficulty(key, beatmapInfo, rulesetInfo); + // this is safe in this usage because the only asynchronous part is handled by the local scheduler. + return GetDifficultyAsync(beatmapInfo, rulesetInfo, mods).Result; } /// @@ -207,38 +215,38 @@ namespace osu.Game.Beatmaps /// A token that may be used to cancel this update. private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, CancellationToken cancellationToken = default) { - GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken).ContinueWith(t => - { - // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events. - Schedule(() => + GetAsync(new DifficultyCacheLookup(bindable.Beatmap, rulesetInfo, mods), cancellationToken) + .ContinueWith(t => { - if (!cancellationToken.IsCancellationRequested) - bindable.Value = t.Result; - }); - }, cancellationToken); + // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events. + Schedule(() => + { + if (!cancellationToken.IsCancellationRequested) + bindable.Value = t.Result; + }); + }, cancellationToken); } /// /// Computes the difficulty defined by a key, and stores it to the timed cache. /// /// The that defines the computation parameters. - /// The to compute the difficulty of. - /// The to compute the difficulty with. /// The . - private StarDifficulty computeDifficulty(in DifficultyCacheLookup key, BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo) + private StarDifficulty computeDifficulty(in DifficultyCacheLookup key) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. - rulesetInfo ??= beatmapInfo.Ruleset; + var beatmapInfo = key.Beatmap; + var rulesetInfo = key.Ruleset; try { var ruleset = rulesetInfo.CreateInstance(); Debug.Assert(ruleset != null); - var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(beatmapInfo)); - var attributes = calculator.Calculate(key.Mods); + var calculator = ruleset.CreateDifficultyCalculator(beatmapManager.GetWorkingBeatmap(key.Beatmap)); + var attributes = calculator.Calculate(key.OrderedMods); - return Cache[key] = new StarDifficulty(attributes); + return new StarDifficulty(attributes); } catch (BeatmapInvalidForRulesetException e) { @@ -249,49 +257,17 @@ namespace osu.Game.Beatmaps if (rulesetInfo.Equals(beatmapInfo.Ruleset)) { Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset})."); - return Cache[key] = new StarDifficulty(); + return new StarDifficulty(); } - // Check the cache first because this is now a different ruleset than the one previously guarded against. - if (tryGetExisting(beatmapInfo, beatmapInfo.Ruleset, Array.Empty(), out var existingDefault, out var existingDefaultKey)) - return existingDefault; - - return computeDifficulty(existingDefaultKey, beatmapInfo, beatmapInfo.Ruleset); + return GetAsync(new DifficultyCacheLookup(key.Beatmap, key.Beatmap.Ruleset, key.OrderedMods)).Result; } catch { - return Cache[key] = new StarDifficulty(); + return new StarDifficulty(); } } - /// - /// Attempts to retrieve an existing difficulty for the combination. - /// - /// The . - /// The . - /// The s. - /// The existing difficulty value, if present. - /// The key that was used to perform this lookup. This can be further used to query . - /// Whether an existing difficulty was found. - private bool tryGetExisting(BeatmapInfo beatmapInfo, RulesetInfo rulesetInfo, IEnumerable mods, out StarDifficulty existingDifficulty, out DifficultyCacheLookup key) - { - // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. - rulesetInfo ??= beatmapInfo.Ruleset; - - // Difficulty can only be computed if the beatmap and ruleset are locally available. - if (beatmapInfo.ID == 0 || rulesetInfo.ID == null) - { - // If not, fall back to the existing star difficulty (e.g. from an online source). - existingDifficulty = new StarDifficulty(beatmapInfo.StarDifficulty, beatmapInfo.MaxCombo ?? 0); - key = default; - - return true; - } - - key = new DifficultyCacheLookup(beatmapInfo.ID, rulesetInfo.ID.Value, mods); - return Cache.TryGetValue(key, out existingDifficulty); - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -302,29 +278,31 @@ namespace osu.Game.Beatmaps public readonly struct DifficultyCacheLookup : IEquatable { - public readonly int BeatmapId; - public readonly int RulesetId; - public readonly Mod[] Mods; + public readonly BeatmapInfo Beatmap; + public readonly RulesetInfo Ruleset; - public DifficultyCacheLookup(int beatmapId, int rulesetId, IEnumerable mods) + public readonly Mod[] OrderedMods; + + public DifficultyCacheLookup([NotNull] BeatmapInfo beatmap, [NotNull] RulesetInfo ruleset, IEnumerable mods) { - BeatmapId = beatmapId; - RulesetId = rulesetId; - Mods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); + Beatmap = beatmap; + Ruleset = ruleset; + OrderedMods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); } public bool Equals(DifficultyCacheLookup other) - => BeatmapId == other.BeatmapId - && RulesetId == other.RulesetId - && Mods.Select(m => m.Acronym).SequenceEqual(other.Mods.Select(m => m.Acronym)); + => Beatmap.ID == other.Beatmap.ID + && Ruleset.ID == other.Ruleset.ID + && OrderedMods.Select(m => m.Acronym).SequenceEqual(other.OrderedMods.Select(m => m.Acronym)); public override int GetHashCode() { var hashCode = new HashCode(); - hashCode.Add(BeatmapId); - hashCode.Add(RulesetId); - foreach (var mod in Mods) + hashCode.Add(Beatmap.ID); + hashCode.Add(Ruleset.ID); + + foreach (var mod in OrderedMods) hashCode.Add(mod.Acronym); return hashCode.ToHashCode(); diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs index afdf37fa27..efc31f2b46 100644 --- a/osu.Game/Database/MemoryCachingComponent.cs +++ b/osu.Game/Database/MemoryCachingComponent.cs @@ -20,7 +20,7 @@ namespace osu.Game.Database protected virtual bool CacheNullValues => true; /// - /// Retrieve the cached value for the given . + /// Retrieve the cached value for the given lookup. /// /// The lookup to retrieve. /// An optional to cancel the operation. @@ -37,6 +37,9 @@ namespace osu.Game.Database return computed; } + protected bool CheckExists([NotNull] TLookup lookup, out TValue value) => + cache.TryGetValue(lookup, out value); + /// /// Called on cache miss to compute the value for the specified lookup. /// From c5b172d0dd283f3b494883029728beca8b4fc3a2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 14:53:15 +0900 Subject: [PATCH 4440/6909] Remove synchronous lookup path from BeatmapDifficultyCache --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 13 ------------- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 4 +++- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 126e08a173..40de98f0be 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -112,19 +112,6 @@ namespace osu.Game.Beatmaps }, token, TaskCreationOptions.HideScheduler | TaskCreationOptions.RunContinuationsAsynchronously, updateScheduler); } - /// - /// Retrieves the difficulty of a . - /// - /// The to get the difficulty of. - /// The to get the difficulty with. - /// The s to get the difficulty with. - /// The . - public StarDifficulty GetDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null) - { - // this is safe in this usage because the only asynchronous part is handled by the local scheduler. - return GetDifficultyAsync(beatmapInfo, rulesetInfo, mods).Result; - } - /// /// Retrieves the that describes a star rating. /// diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 33ee5d2ee4..ff6203bc25 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -77,6 +77,8 @@ namespace osu.Game.Screens.Ranking.Expanded statisticDisplays.AddRange(topStatistics); statisticDisplays.AddRange(bottomStatistics); + var starDifficulty = beatmapDifficultyCache.GetDifficultyAsync(beatmap, score.Ruleset, score.Mods).Result; + InternalChildren = new Drawable[] { new FillFlowContainer @@ -143,7 +145,7 @@ namespace osu.Game.Screens.Ranking.Expanded Spacing = new Vector2(5, 0), Children = new Drawable[] { - new StarRatingDisplay(beatmapDifficultyCache.GetDifficulty(beatmap, score.Ruleset, score.Mods)) + new StarRatingDisplay(starDifficulty) { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft From d3a303e2517e8b8c9d449225d1f9e12083e2a806 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 16:57:09 +0900 Subject: [PATCH 4441/6909] Use CheckExists function --- osu.Game/Database/MemoryCachingComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs index efc31f2b46..d913e66428 100644 --- a/osu.Game/Database/MemoryCachingComponent.cs +++ b/osu.Game/Database/MemoryCachingComponent.cs @@ -26,7 +26,7 @@ namespace osu.Game.Database /// An optional to cancel the operation. protected async Task GetAsync([NotNull] TLookup lookup, CancellationToken token = default) { - if (cache.TryGetValue(lookup, out TValue performance)) + if (CheckExists(lookup, out TValue performance)) return performance; var computed = await ComputeValueAsync(lookup, token); From f51cb0dd14aaf023840ed6904f9646c845e1415b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 16:58:53 +0900 Subject: [PATCH 4442/6909] Add ruleset fallback logic into cache lookup class --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 40de98f0be..e62b2b1ff1 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -273,7 +273,8 @@ namespace osu.Game.Beatmaps public DifficultyCacheLookup([NotNull] BeatmapInfo beatmap, [NotNull] RulesetInfo ruleset, IEnumerable mods) { Beatmap = beatmap; - Ruleset = ruleset; + // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. + Ruleset = ruleset ?? Beatmap.Ruleset; OrderedMods = mods?.OrderBy(m => m.Acronym).ToArray() ?? Array.Empty(); } From aa252d562a11e44dbf660213ec38931fce43dd17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 12:42:45 +0900 Subject: [PATCH 4443/6909] Rename top user request to make way for new type --- .../API/Requests/{GetUsersRequest.cs => GetTopUsersRequest.cs} | 2 +- .../Requests/{GetUsersResponse.cs => GetTopUsersResponse.cs} | 2 +- osu.Game/Online/API/Requests/GetUserRankingsRequest.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename osu.Game/Online/API/Requests/{GetUsersRequest.cs => GetTopUsersRequest.cs} (80%) rename osu.Game/Online/API/Requests/{GetUsersResponse.cs => GetTopUsersResponse.cs} (86%) diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetTopUsersRequest.cs similarity index 80% rename from osu.Game/Online/API/Requests/GetUsersRequest.cs rename to osu.Game/Online/API/Requests/GetTopUsersRequest.cs index b75ecd5bd7..dbbd2119db 100644 --- a/osu.Game/Online/API/Requests/GetUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetTopUsersRequest.cs @@ -3,7 +3,7 @@ namespace osu.Game.Online.API.Requests { - public class GetUsersRequest : APIRequest + public class GetTopUsersRequest : APIRequest { protected override string Target => @"rankings/osu/performance"; } diff --git a/osu.Game/Online/API/Requests/GetUsersResponse.cs b/osu.Game/Online/API/Requests/GetTopUsersResponse.cs similarity index 86% rename from osu.Game/Online/API/Requests/GetUsersResponse.cs rename to osu.Game/Online/API/Requests/GetTopUsersResponse.cs index b301f551e3..b37b8b3499 100644 --- a/osu.Game/Online/API/Requests/GetUsersResponse.cs +++ b/osu.Game/Online/API/Requests/GetTopUsersResponse.cs @@ -7,7 +7,7 @@ using osu.Game.Users; namespace osu.Game.Online.API.Requests { - public class GetUsersResponse : ResponseWithCursor + public class GetTopUsersResponse : ResponseWithCursor { [JsonProperty("ranking")] public List Users; diff --git a/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs b/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs index 143d21e40d..bccc3bc0c3 100644 --- a/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs +++ b/osu.Game/Online/API/Requests/GetUserRankingsRequest.cs @@ -6,7 +6,7 @@ using osu.Game.Rulesets; namespace osu.Game.Online.API.Requests { - public class GetUserRankingsRequest : GetRankingsRequest + public class GetUserRankingsRequest : GetRankingsRequest { public readonly UserRankingsType Type; From db039da668755507f5ba34985601c2e733d6e1b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 12:54:08 +0900 Subject: [PATCH 4444/6909] Add and consume multi-lookup API endpoint --- .../Online/API/Requests/GetUsersRequest.cs | 19 +++++++++++++++++++ .../Online/API/Requests/GetUsersResponse.cs | 15 +++++++++++++++ .../Dashboard/CurrentlyPlayingDisplay.cs | 13 +++++++------ osu.Game/Overlays/DashboardOverlay.cs | 3 +-- 4 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Online/API/Requests/GetUsersRequest.cs create mode 100644 osu.Game/Online/API/Requests/GetUsersResponse.cs diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs new file mode 100644 index 0000000000..9a7006f5d6 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetUsersRequest.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.Linq; + +namespace osu.Game.Online.API.Requests +{ + public class GetUsersRequest : APIRequest + { + private readonly int[] userIds; + + public GetUsersRequest(int[] userIds) + { + this.userIds = userIds; + } + + protected override string Target => $@"users/?{userIds.Select(u => $"ids[]={u}&").Aggregate((a, b) => a + b)}"; + } +} diff --git a/osu.Game/Online/API/Requests/GetUsersResponse.cs b/osu.Game/Online/API/Requests/GetUsersResponse.cs new file mode 100644 index 0000000000..6f49d5cd53 --- /dev/null +++ b/osu.Game/Online/API/Requests/GetUsersResponse.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.Collections.Generic; +using Newtonsoft.Json; +using osu.Game.Users; + +namespace osu.Game.Online.API.Requests +{ + public class GetUsersResponse : ResponseWithCursor + { + [JsonProperty("users")] + public List Users; + } +} diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index dae27f35ae..9020b317ef 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -54,17 +54,18 @@ namespace osu.Game.Overlays.Dashboard switch (e.Action) { case NotifyCollectionChangedAction.Add: - foreach (var u in e.NewItems.OfType()) + var request = new GetUsersRequest(e.NewItems.OfType().ToArray()); + + request.Success += users => Schedule(() => { - var request = new GetUserRequest(u); - request.Success += user => Schedule(() => + foreach (var user in users.Users) { if (playingUsers.Contains(user.Id)) userFlow.Add(createUserPanel(user)); - }); - api.Queue(request); - } + } + }); + api.Queue(request); break; case NotifyCollectionChangedAction.Remove: diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 787a4985d7..04defce636 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -131,8 +131,7 @@ namespace osu.Game.Overlays break; case DashboardOverlayTabs.CurrentlyPlaying: - //todo: enable once caching logic is better - //loadDisplay(new CurrentlyPlayingDisplay()); + loadDisplay(new CurrentlyPlayingDisplay()); break; default: From 2457083d8bffda0e1b97c0db609dac0e92331bf8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 13:08:07 +0900 Subject: [PATCH 4445/6909] Add padding to currently playing view --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 9020b317ef..e9915df801 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -37,6 +37,7 @@ namespace osu.Game.Overlays.Dashboard { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Padding = new MarginPadding(10), Spacing = new Vector2(10), }; } From 893979b3deab4b95cc62814c18ba2db2fa3a9d87 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 13:10:18 +0900 Subject: [PATCH 4446/6909] Add exception if attempting to exceed the maximum supported lookup size for one request --- osu.Game/Online/API/Requests/GetUsersRequest.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs index 9a7006f5d6..0ec5173fb6 100644 --- a/osu.Game/Online/API/Requests/GetUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetUsersRequest.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; namespace osu.Game.Online.API.Requests @@ -9,8 +10,13 @@ namespace osu.Game.Online.API.Requests { private readonly int[] userIds; + private const int max_ids_per_request = 50; + public GetUsersRequest(int[] userIds) { + if (userIds.Length > max_ids_per_request) + throw new ArgumentException($"{nameof(GetUsersRequest)} calls only support up to {max_ids_per_request} IDs at once"); + this.userIds = userIds; } From c97c6bbf52046504e31a829435447ddc1f993881 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 16:38:57 +0900 Subject: [PATCH 4447/6909] Add and consume user cache class --- osu.Game/Database/UserLookupCache.cs | 118 ++++++++++++++++++ osu.Game/OsuGameBase.cs | 5 + .../Dashboard/CurrentlyPlayingDisplay.cs | 22 ++-- 3 files changed, 136 insertions(+), 9 deletions(-) create mode 100644 osu.Game/Database/UserLookupCache.cs diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs new file mode 100644 index 0000000000..f5c84c9edf --- /dev/null +++ b/osu.Game/Database/UserLookupCache.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.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Users; + +namespace osu.Game.Database +{ + public class UserLookupCache : MemoryCachingComponent + { + private readonly ConcurrentBag nextTaskIDs = new ConcurrentBag(); + + [Resolved] + private IAPIProvider api { get; set; } + + private readonly object taskAssignmentLock = new object(); + + private Task> pendingRequest; + + /// + /// Whether has already grabbed its IDs. + /// + private bool pendingRequestConsumedIDs; + + public Task GetUser(int userId, CancellationToken token = default) => GetAsync(userId, token); + + protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) + { + var users = await getQueryTaskForUser(lookup); + return users.FirstOrDefault(u => u.Id == lookup); + } + + /// + /// Return the task responsible for fetching the provided user. + /// This may be part of a larger batch lookup to reduce web requests. + /// + /// The user to lookup. + /// The task responsible for the lookup. + private Task> getQueryTaskForUser(int userId) + { + lock (taskAssignmentLock) + { + nextTaskIDs.Add(userId); + + // if there's a pending request which hasn't been started yet (and is not yet full), we can wait on it. + if (pendingRequest != null && !pendingRequestConsumedIDs && nextTaskIDs.Count < 50) + return pendingRequest; + + return queueNextTask(nextLookup); + } + + List nextLookup() + { + int[] lookupItems; + + lock (taskAssignmentLock) + { + pendingRequestConsumedIDs = true; + lookupItems = nextTaskIDs.ToArray(); + nextTaskIDs.Clear(); + + if (lookupItems.Length == 0) + { + queueNextTask(null); + return new List(); + } + } + + var request = new GetUsersRequest(lookupItems); + + // rather than queueing, we maintain our own single-threaded request stream. + request.Perform(api); + + return request.Result.Users; + } + } + + /// + /// Queues new work at the end of the current work tasks. + /// Ensures the provided work is eventually run. + /// + /// The work to run. Can be null to signify the end of available work. + /// The task tracking this work. + private Task> queueNextTask(Func> work) + { + lock (taskAssignmentLock) + { + if (work == null) + { + pendingRequest = null; + pendingRequestConsumedIDs = false; + } + else if (pendingRequest == null) + { + // special case for the first request ever. + pendingRequest = Task.Run(work); + pendingRequestConsumedIDs = false; + } + else + { + // append the new request on to the last to be executed. + pendingRequest = pendingRequest.ContinueWith(_ => work()); + pendingRequestConsumedIDs = false; + } + + return pendingRequest; + } + } + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3da692249d..193f6fe61b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -61,6 +61,8 @@ namespace osu.Game protected BeatmapDifficultyCache DifficultyCache; + protected UserLookupCache UserCache; + protected SkinManager SkinManager; protected RulesetStore RulesetStore; @@ -229,6 +231,9 @@ namespace osu.Game dependencies.Cache(DifficultyCache = new BeatmapDifficultyCache()); AddInternal(DifficultyCache); + dependencies.Cache(UserCache = new UserLookupCache()); + AddInternal(UserCache); + var scorePerformanceManager = new ScorePerformanceCache(); dependencies.Cache(scorePerformanceManager); AddInternal(scorePerformanceManager); diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index e9915df801..a988381f29 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -8,8 +8,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Game.Database; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; using osu.Game.Online.Spectator; using osu.Game.Screens.Multi.Match.Components; using osu.Game.Screens.Play; @@ -45,6 +45,9 @@ namespace osu.Game.Overlays.Dashboard [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private UserLookupCache users { get; set; } + protected override void LoadComplete() { base.LoadComplete(); @@ -55,18 +58,19 @@ namespace osu.Game.Overlays.Dashboard switch (e.Action) { case NotifyCollectionChangedAction.Add: - var request = new GetUsersRequest(e.NewItems.OfType().ToArray()); - request.Success += users => Schedule(() => + foreach (var id in e.NewItems.OfType().ToArray()) { - foreach (var user in users.Users) + users.GetUser(id).ContinueWith(u => { - if (playingUsers.Contains(user.Id)) - userFlow.Add(createUserPanel(user)); - } - }); + Schedule(() => + { + if (playingUsers.Contains(u.Result.Id)) + userFlow.Add(createUserPanel(u.Result)); + }); + }); + } - api.Queue(request); break; case NotifyCollectionChangedAction.Remove: From c1c3d377209b97492955cc1682ce3c60cfcede48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 17:24:28 +0900 Subject: [PATCH 4448/6909] Remove non-null assert --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index e62b2b1ff1..9820d508dd 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -270,7 +270,7 @@ namespace osu.Game.Beatmaps public readonly Mod[] OrderedMods; - public DifficultyCacheLookup([NotNull] BeatmapInfo beatmap, [NotNull] RulesetInfo ruleset, IEnumerable mods) + public DifficultyCacheLookup([NotNull] BeatmapInfo beatmap, [CanBeNull] RulesetInfo ruleset, IEnumerable mods) { Beatmap = beatmap; // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. From 1ca8b03aa5a2ba9be0bd2e813bd5cf01e5677f09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 17:26:23 +0900 Subject: [PATCH 4449/6909] Never disable pause button on now playing overlay --- osu.Game/Overlays/NowPlayingOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 55adf02a45..9beb859f28 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -308,7 +308,6 @@ namespace osu.Game.Overlays if (disabled) playlist.Hide(); - playButton.Enabled.Value = !disabled; prevButton.Enabled.Value = !disabled; nextButton.Enabled.Value = !disabled; playlistButton.Enabled.Value = !disabled; From c3c288145a81d61bf90c823aa7bbab5e302d76d3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 17:55:29 +0900 Subject: [PATCH 4450/6909] Ignore null results for now --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index a988381f29..8299619a18 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -63,6 +63,9 @@ namespace osu.Game.Overlays.Dashboard { users.GetUser(id).ContinueWith(u => { + if (u.Result == null) + return; + Schedule(() => { if (playingUsers.Contains(u.Result.Id)) From 4bbd3fe8864b2e7aea89b971e09913a53a094004 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 18:37:27 +0900 Subject: [PATCH 4451/6909] Handle null result --- osu.Game/Database/UserLookupCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index f5c84c9edf..b7c6c480a9 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -79,7 +79,7 @@ namespace osu.Game.Database // rather than queueing, we maintain our own single-threaded request stream. request.Perform(api); - return request.Result.Users; + return request.Result?.Users; } } From ee84a9827ee702cc1a66c535ab17a1787b71074e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 6 Nov 2020 18:41:05 +0900 Subject: [PATCH 4452/6909] Fix regressed test --- .../TestSceneCurrentlyPlayingDisplay.cs | 44 ++++++++++++------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index d6fd33bce7..7eba64f418 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; -using osu.Game.Online.API; -using osu.Game.Online.API.Requests; +using osu.Game.Database; using osu.Game.Online.Spectator; using osu.Game.Overlays.Dashboard; using osu.Game.Tests.Visual.Gameplay; @@ -22,32 +24,34 @@ namespace osu.Game.Tests.Visual.Online private CurrentlyPlayingDisplay currentlyPlaying; - private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestUserLookupCache(); + + private Container nestedContainer; [SetUpSteps] public void SetUpSteps() { - AddStep("register request handling", () => dummyAPI.HandleRequest = req => - { - switch (req) - { - case GetUserRequest cRequest: - cRequest.TriggerSuccess(new User { Username = "peppy", Id = 2 }); - break; - } - }); - AddStep("add streaming client", () => { - Remove(testSpectatorStreamingClient); + nestedContainer?.Remove(testSpectatorStreamingClient); + Remove(lookupCache); Children = new Drawable[] { - testSpectatorStreamingClient, - currentlyPlaying = new CurrentlyPlayingDisplay + lookupCache, + nestedContainer = new Container { RelativeSizeAxes = Axes.Both, - } + Children = new Drawable[] + { + testSpectatorStreamingClient, + currentlyPlaying = new CurrentlyPlayingDisplay + { + RelativeSizeAxes = Axes.Both, + } + } + }, }; }); @@ -62,5 +66,11 @@ namespace osu.Game.Tests.Visual.Online AddStep("Remove playing user", () => testSpectatorStreamingClient.PlayingUsers.Remove(2)); AddUntilStep("Panel no longer present", () => !currentlyPlaying.ChildrenOfType().Any()); } + + internal class TestUserLookupCache : UserLookupCache + { + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) + => Task.FromResult(new User { Username = "peppy", Id = 2 }); + } } } From 2bef9312d9c88f4424ee0896b38b240bb5a3801d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 6 Nov 2020 22:11:49 +0900 Subject: [PATCH 4453/6909] Make SkinReloadableDrawable poolable --- osu.Game/Skinning/SkinReloadableDrawable.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index 4a1aaa62bf..d0531c46c5 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -4,13 +4,14 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; namespace osu.Game.Skinning { /// /// A drawable which has a callback when the skin changes. /// - public abstract class SkinReloadableDrawable : CompositeDrawable + public abstract class SkinReloadableDrawable : PoolableDrawable { /// /// Invoked when has changed. From 248d342a2f0e487c6b5c000aee604b5337c25c57 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 6 Nov 2020 22:15:00 +0900 Subject: [PATCH 4454/6909] Initial Apply()/FreeAfterUse() DHO implementation --- .../Objects/Drawables/DrawableHitObject.cs | 169 ++++++++++++------ osu.Game/Rulesets/Objects/HitObject.cs | 6 + osu.Game/Rulesets/UI/HitObjectContainer.cs | 89 ++++----- osu.Game/Skinning/SkinReloadableDrawable.cs | 1 - 4 files changed, 168 insertions(+), 97 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5c3f57c2d0..43688a7332 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -27,7 +28,10 @@ namespace osu.Game.Rulesets.Objects.Drawables { public event Action DefaultsApplied; - public readonly HitObject HitObject; + /// + /// The currently represented by this . + /// + public HitObject HitObject { get; private set; } /// /// The colour used for various elements of this DrawableHitObject. @@ -96,10 +100,10 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual float SamplePlaybackPosition => 0.5f; - private BindableList samplesBindable; - private Bindable startTimeBindable; - private Bindable userPositionalHitSounds; - private Bindable comboIndexBindable; + public readonly Bindable StartTimeBindable = new Bindable(); + private readonly BindableList samplesBindable = new BindableList(); + private readonly Bindable userPositionalHitSounds = new Bindable(); + private readonly Bindable comboIndexBindable = new Bindable(); public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; @@ -111,52 +115,120 @@ namespace osu.Game.Rulesets.Objects.Drawables public IBindable State => state; - protected DrawableHitObject([NotNull] HitObject hitObject) + /// + /// Creates a new . + /// + /// + /// The to be initially applied to this . + /// If null, a hitobject is expected to be later applied via (or automatically via pooling). + /// + protected DrawableHitObject([CanBeNull] HitObject initialHitObject = null) { - HitObject = hitObject ?? throw new ArgumentNullException(nameof(hitObject)); + HitObject = initialHitObject; } [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - userPositionalHitSounds = config.GetBindable(OsuSetting.PositionalHitSounds); - var judgement = HitObject.CreateJudgement(); - - Result = CreateResult(judgement); - if (Result == null) - throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); + config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); } protected override void LoadAsyncComplete() { base.LoadAsyncComplete(); - LoadSamples(); - - HitObject.DefaultsApplied += onDefaultsApplied; - - startTimeBindable = HitObject.StartTimeBindable.GetBoundCopy(); - startTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); - - if (HitObject is IHasComboInformation combo) - { - comboIndexBindable = combo.ComboIndexBindable.GetBoundCopy(); - comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); - } - - samplesBindable = HitObject.SamplesBindable.GetBoundCopy(); - samplesBindable.CollectionChanged += (_, __) => LoadSamples(); - - apply(HitObject); + if (HitObject != null) + Apply(HitObject); } protected override void LoadComplete() { base.LoadComplete(); + StartTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); + comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); + updateState(ArmedState.Idle, true); } + /// + /// Removes the currently applied to this , + /// + protected override void FreeAfterUse() + { + StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); + if (HitObject is IHasComboInformation combo) + comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); + + samplesBindable.UnbindFrom(HitObject.SamplesBindable); + + // When a new hitobject is applied, the samples will be cleared before re-populating. + // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). + samplesBindable.CollectionChanged -= onSamplesChanged; + + if (nestedHitObjects.IsValueCreated) + { + foreach (var obj in nestedHitObjects.Value) + { + obj.OnNewResult -= onNewResult; + obj.OnRevertResult -= onRevertResult; + obj.ApplyCustomUpdateState -= onApplyCustomUpdateState; + } + + nestedHitObjects.Value.Clear(); + ClearNestedHitObjects(); + } + + HitObject.DefaultsApplied -= onDefaultsApplied; + HitObject = null; + + base.FreeAfterUse(); + } + + /// + /// Applies a new to be represented by this . + /// + /// + public virtual void Apply(HitObject hitObject) + { + HitObject = hitObject; + + // Copy any existing result from the hitobject (required for rewind / judgement revert). + Result = HitObject.Result; + + // Ensure this DHO has a result. + Result ??= CreateResult(HitObject.CreateJudgement()) + ?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); + + // Ensure the hitobject has a result. + HitObject.Result = Result; + + 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); + } + + StartTimeBindable.BindTo(HitObject.StartTimeBindable); + if (HitObject is IHasComboInformation combo) + comboIndexBindable.BindTo(combo.ComboIndexBindable); + + samplesBindable.BindTo(HitObject.SamplesBindable); + samplesBindable.BindCollectionChanged(onSamplesChanged, true); + + HitObject.DefaultsApplied += onDefaultsApplied; + + // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. + if (IsLoaded) + Schedule(() => updateState(ArmedState.Idle, true)); + } + /// /// Invoked by the base to populate samples, once on initial load and potentially again on any change to the samples collection. /// @@ -183,34 +255,21 @@ namespace osu.Game.Rulesets.Objects.Drawables AddInternal(Samples); } + private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); + + private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); + + private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result); + + private void onApplyCustomUpdateState(DrawableHitObject drawableHitObject, ArmedState state) => ApplyCustomUpdateState?.Invoke(drawableHitObject, state); + private void onDefaultsApplied(HitObject hitObject) { - apply(hitObject); - updateState(state.Value, true); + FreeAfterUse(); + Apply(hitObject); DefaultsApplied?.Invoke(this); } - private void apply(HitObject hitObject) - { - if (nestedHitObjects.IsValueCreated) - { - nestedHitObjects.Value.Clear(); - ClearNestedHitObjects(); - } - - 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); - } - } - /// /// Invoked by the base to add nested s to the hierarchy. /// @@ -600,7 +659,9 @@ namespace osu.Game.Rulesets.Objects.Drawables protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - HitObject.DefaultsApplied -= onDefaultsApplied; + + if (HitObject != null) + HitObject.DefaultsApplied -= onDefaultsApplied; } } diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 826d411822..9ef3ff9c4a 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -12,6 +12,7 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -168,6 +169,11 @@ namespace osu.Game.Rulesets.Objects /// [NotNull] protected virtual HitWindows CreateHitWindows() => new HitWindows(); + + /// + /// The result this was judged with. Used internally for rewinding within . + /// + internal JudgementResult Result; } public static class HitObjectExtensions diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 4cadfa9ad4..4666aa9211 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.UI public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - private readonly Dictionary bindable, double timeAtAdd)> startTimeMap = new Dictionary, double)>(); + private readonly Dictionary startTimeMap = new Dictionary(); public HitObjectContainer() { @@ -25,10 +25,7 @@ namespace osu.Game.Rulesets.UI public virtual void Add(DrawableHitObject hitObject) { - // Added first for the comparer to remain ordered during AddInternal - startTimeMap[hitObject] = (hitObject.HitObject.StartTimeBindable.GetBoundCopy(), hitObject.HitObject.StartTime); - startTimeMap[hitObject].bindable.BindValueChanged(_ => onStartTimeChanged(hitObject)); - + bindStartTime(hitObject); AddInternal(hitObject); } @@ -37,54 +34,19 @@ namespace osu.Game.Rulesets.UI if (!RemoveInternal(hitObject)) return false; - // Removed last for the comparer to remain ordered during RemoveInternal - startTimeMap[hitObject].bindable.UnbindAll(); - startTimeMap.Remove(hitObject); + unbindStartTime(hitObject); return true; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - unbindStartTimeMap(); - } - public virtual void Clear(bool disposeChildren = true) { ClearInternal(disposeChildren); - unbindStartTimeMap(); - } - - private void unbindStartTimeMap() - { - foreach (var kvp in startTimeMap) - kvp.Value.bindable.UnbindAll(); - startTimeMap.Clear(); + unbindAllStartTimes(); } public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); - private void onStartTimeChanged(DrawableHitObject hitObject) - { - if (!RemoveInternal(hitObject)) - return; - - // Update the stored time, preserving the existing bindable - startTimeMap[hitObject] = (startTimeMap[hitObject].bindable, hitObject.HitObject.StartTime); - AddInternal(hitObject); - } - - protected override int Compare(Drawable x, Drawable y) - { - if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) - return base.Compare(x, y); - - // Put earlier hitobjects towards the end of the list, so they handle input first - int i = startTimeMap[yObj].timeAtAdd.CompareTo(startTimeMap[xObj].timeAtAdd); - return i == 0 ? CompareReverseChildID(x, y) : i; - } - protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) { if (!(e.Child is DrawableHitObject hitObject)) @@ -96,5 +58,48 @@ namespace osu.Game.Rulesets.UI hitObject.OnKilled(); } } + + #region Comparator + StartTime tracking + + private void bindStartTime(DrawableHitObject hitObject) + { + var bindable = hitObject.StartTimeBindable.GetBoundCopy(); + bindable.BindValueChanged(_ => onStartTimeChanged(hitObject)); + + startTimeMap[hitObject] = bindable; + } + + private void unbindStartTime(DrawableHitObject hitObject) + { + startTimeMap[hitObject].UnbindAll(); + startTimeMap.Remove(hitObject); + } + + private void unbindAllStartTimes() + { + foreach (var kvp in startTimeMap) + kvp.Value.UnbindAll(); + startTimeMap.Clear(); + } + + private void onStartTimeChanged(DrawableHitObject hitObject) => SortInternal(); + + protected override int Compare(Drawable x, Drawable y) + { + if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) + return base.Compare(x, y); + + // Put earlier hitobjects towards the end of the list, so they handle input first + int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); + return i == 0 ? CompareReverseChildID(x, y) : i; + } + + #endregion + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + unbindAllStartTimes(); + } } } diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index d0531c46c5..cc9cbf7b59 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Allocation; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; namespace osu.Game.Skinning From 2d892c74079cc5b379ab5aabd750fce127907fe0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 6 Nov 2020 23:03:29 +0900 Subject: [PATCH 4455/6909] Allow Apply() to be called multiple times sequentially --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 43688a7332..1e304f3d7d 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -191,6 +191,9 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public virtual void Apply(HitObject hitObject) { + if (HitObject != null) + FreeAfterUse(); + HitObject = hitObject; // Copy any existing result from the hitobject (required for rewind / judgement revert). From 7eceda242bf40887d340d0388e2886e3fe6082e7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 6 Nov 2020 23:04:28 +0900 Subject: [PATCH 4456/6909] Change derived class to use property --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 1e304f3d7d..6e2967062c 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -671,12 +671,11 @@ namespace osu.Game.Rulesets.Objects.Drawables public abstract class DrawableHitObject : DrawableHitObject where TObject : HitObject { - public new readonly TObject HitObject; + public new TObject HitObject => (TObject)base.HitObject; protected DrawableHitObject(TObject hitObject) : base(hitObject) { - HitObject = hitObject; } } } From 33b629a87a86b571d3cc94ffaa02949d13bfd7f1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 6 Nov 2020 23:09:23 +0900 Subject: [PATCH 4457/6909] Make top-level osu! objects use new methods --- .../Objects/Drawables/DrawableHitCircle.cs | 3 ++- .../Objects/Drawables/DrawableOsuHitObject.cs | 16 +++++++++++++ .../Objects/Drawables/DrawableSlider.cs | 24 +++++++++++++++---- .../Objects/Drawables/DrawableSpinner.cs | 3 ++- 4 files changed, 40 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index b0c4e3758d..90a25e9625 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -30,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container scaleContainer; private InputManager inputManager; - public DrawableHitCircle(HitCircle h) + public DrawableHitCircle([CanBeNull] HitCircle h = null) : base(h) { } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 208f79f165..2d0b939e30 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -9,6 +9,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.Objects; using osu.Game.Rulesets.Osu.UI; using osuTK; @@ -49,6 +50,21 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ShakeDuration = 30, RelativeSizeAxes = Axes.Both }); + } + + protected override void FreeAfterUse() + { + IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable); + PositionBindable.UnbindFrom(HitObject.PositionBindable); + StackHeightBindable.UnbindFrom(HitObject.StackHeightBindable); + ScaleBindable.UnbindFrom(HitObject.ScaleBindable); + + base.FreeAfterUse(); + } + + public override void Apply(HitObject hitObject) + { + base.Apply(hitObject); IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable); PositionBindable.BindTo(HitObject.PositionBindable); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index d8dd0d7471..5a47e3bebe 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osuTK; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -32,14 +33,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; - public readonly IBindable PathVersion = new Bindable(); + public IBindable PathVersion => pathVersion; + private readonly Bindable pathVersion = new Bindable(); private Container headContainer; private Container tailContainer; private Container tickContainer; private Container repeatContainer; - public DrawableSlider(Slider s) + public DrawableSlider([CanBeNull] Slider s = null) : base(s) { } @@ -63,8 +65,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables headContainer = new Container { RelativeSizeAxes = Axes.Both }, }; - PathVersion.BindTo(HitObject.Path.Version); - PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); ScaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue), true); @@ -78,6 +78,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking.BindValueChanged(updateSlidingSample); } + protected override void FreeAfterUse() + { + PathVersion.UnbindFrom(HitObject.Path.Version); + + base.FreeAfterUse(); + } + + public override void Apply(HitObject hitObject) + { + base.Apply(hitObject); + + // Ensure that the version will change after the upcoming BindTo(). + pathVersion.Value = int.MaxValue; + PathVersion.BindTo(HitObject.Path.Version); + } + private PausableSkinnableSound slidingSample; protected override void LoadSamples() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 77fbff9c51..d564a76e5e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; @@ -32,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Bindable isSpinning; private bool spinnerFrequencyModulate; - public DrawableSpinner(Spinner s) + public DrawableSpinner([CanBeNull] Spinner s = null) : base(s) { } From e190afbfed30d6ad0d12b289209d8173f92a5985 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 6 Nov 2020 23:35:47 +0900 Subject: [PATCH 4458/6909] Remove initial value changed invocations --- .../Objects/Drawables/DrawableHitCircle.cs | 8 ++++---- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 6 +++--- .../Objects/Drawables/DrawableSpinner.cs | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 90a25e9625..77d24db084 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -73,10 +73,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Size = HitArea.DrawSize; - PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); - StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); - ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); - AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue, true); + PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); + StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); + AccentColour.BindValueChanged(accent => ApproachCircle.Colour = accent.NewValue); } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5a47e3bebe..72b932ea36 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -65,9 +65,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables headContainer = new Container { RelativeSizeAxes = Axes.Both }, }; - PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); - StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition, true); - ScaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue), true); + PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); + StackHeightBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); + ScaleBindable.BindValueChanged(scale => Ball.Scale = new Vector2(scale.NewValue)); AccentColour.BindValueChanged(colour => { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index d564a76e5e..eb125969b0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } }; - PositionBindable.BindValueChanged(pos => Position = pos.NewValue, true); + PositionBindable.BindValueChanged(pos => Position = pos.NewValue); } protected override void LoadComplete() From 1c8d68676eeaa0d93eff7bd5a99d573e7a05bc63 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 6 Nov 2020 23:56:02 +0900 Subject: [PATCH 4459/6909] Add tests --- .../TestSceneHitCircleApplication.cs | 44 +++++++++++++ .../TestSceneSliderApplication.cs | 61 +++++++++++++++++++ .../TestSceneSpinnerApplication.cs | 46 ++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs new file mode 100644 index 0000000000..8b3fead366 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Timing; +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.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneHitCircleApplication : OsuTestScene + { + [Test] + public void TestApplyNewCircle() + { + DrawableHitCircle dho = null; + + AddStep("create circle", () => Child = dho = new DrawableHitCircle(prepareObject(new HitCircle + { + Position = new Vector2(256, 192), + IndexInCurrentCombo = 0 + })) + { + Clock = new FramedClock(new StopwatchClock()) + }); + + AddStep("apply new circle", () => dho.Apply(prepareObject(new HitCircle + { + Position = new Vector2(128, 128), + ComboIndex = 1, + }))); + } + + private HitCircle prepareObject(HitCircle circle) + { + circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + return circle; + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs new file mode 100644 index 0000000000..e0898dc95d --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -0,0 +1,61 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Timing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +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.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneSliderApplication : OsuTestScene + { + [Test] + public void TestApplyNewSlider() + { + DrawableSlider dho = null; + + AddStep("create slider", () => Child = dho = new DrawableSlider(prepareObject(new Slider + { + Position = new Vector2(256, 192), + IndexInCurrentCombo = 0, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(150, 100), + new Vector2(300, 0), + }) + })) + { + Clock = new FramedClock(new StopwatchClock(true)) + }); + + AddWaitStep("wait for progression", 1); + + AddStep("apply new slider", () => dho.Apply(prepareObject(new Slider + { + Position = new Vector2(256, 192), + ComboIndex = 1, + Path = new SliderPath(PathType.Bezier, new[] + { + Vector2.Zero, + new Vector2(150, 100), + new Vector2(300, 0), + }), + RepeatCount = 1 + }))); + } + + private Slider prepareObject(Slider slider) + { + slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + return slider; + } + } +} diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs new file mode 100644 index 0000000000..5951574079 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Timing; +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.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneSpinnerApplication : OsuTestScene + { + [Test] + public void TestApplyNewCircle() + { + DrawableSpinner dho = null; + + AddStep("create spinner", () => Child = dho = new DrawableSpinner(prepareObject(new Spinner + { + Position = new Vector2(256, 192), + IndexInCurrentCombo = 0, + Duration = 0, + })) + { + Clock = new FramedClock(new StopwatchClock()) + }); + + AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner + { + Position = new Vector2(256, 192), + ComboIndex = 1, + Duration = 1000, + }))); + } + + private Spinner prepareObject(Spinner circle) + { + circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + return circle; + } + } +} From 3a4bd73823c9e884de908591d8784a63eae69bbc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 7 Nov 2020 00:25:26 +0900 Subject: [PATCH 4460/6909] Fix DHOs being freed when not expected --- .../Objects/Drawables/DrawableOsuHitObject.cs | 4 +- .../Objects/Drawables/DrawableSlider.cs | 4 +- .../Objects/Drawables/DrawableHitObject.cs | 95 +++++++++++-------- 3 files changed, 60 insertions(+), 43 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 2d0b939e30..0326f38439 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -52,14 +52,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }); } - protected override void FreeAfterUse() + public override void Free() { IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable); PositionBindable.UnbindFrom(HitObject.PositionBindable); StackHeightBindable.UnbindFrom(HitObject.StackHeightBindable); ScaleBindable.UnbindFrom(HitObject.ScaleBindable); - base.FreeAfterUse(); + base.Free(); } public override void Apply(HitObject hitObject) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 72b932ea36..844e95aa7a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -78,11 +78,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking.BindValueChanged(updateSlidingSample); } - protected override void FreeAfterUse() + public override void Free() { PathVersion.UnbindFrom(HitObject.Path.Version); - base.FreeAfterUse(); + base.Free(); } public override void Apply(HitObject hitObject) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 6e2967062c..8e56b07420 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -115,6 +115,11 @@ namespace osu.Game.Rulesets.Objects.Drawables public IBindable State => state; + /// + /// Whether is currently applied. + /// + private bool hasHitObjectApplied; + /// /// Creates a new . /// @@ -151,50 +156,16 @@ namespace osu.Game.Rulesets.Objects.Drawables updateState(ArmedState.Idle, true); } - /// - /// Removes the currently applied to this , - /// - protected override void FreeAfterUse() - { - StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); - if (HitObject is IHasComboInformation combo) - comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); - - samplesBindable.UnbindFrom(HitObject.SamplesBindable); - - // When a new hitobject is applied, the samples will be cleared before re-populating. - // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). - samplesBindable.CollectionChanged -= onSamplesChanged; - - if (nestedHitObjects.IsValueCreated) - { - foreach (var obj in nestedHitObjects.Value) - { - obj.OnNewResult -= onNewResult; - obj.OnRevertResult -= onRevertResult; - obj.ApplyCustomUpdateState -= onApplyCustomUpdateState; - } - - nestedHitObjects.Value.Clear(); - ClearNestedHitObjects(); - } - - HitObject.DefaultsApplied -= onDefaultsApplied; - HitObject = null; - - base.FreeAfterUse(); - } - /// /// Applies a new to be represented by this . /// - /// + /// The to apply. public virtual void Apply(HitObject hitObject) { - if (HitObject != null) - FreeAfterUse(); + if (hasHitObjectApplied) + Free(); - HitObject = hitObject; + HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}."); // Copy any existing result from the hitobject (required for rewind / judgement revert). Result = HitObject.Result; @@ -230,6 +201,52 @@ namespace osu.Game.Rulesets.Objects.Drawables // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. if (IsLoaded) Schedule(() => updateState(ArmedState.Idle, true)); + + hasHitObjectApplied = true; + } + + /// + /// Removes the currently applied + /// + public virtual void Free() + { + StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); + if (HitObject is IHasComboInformation combo) + comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); + + samplesBindable.UnbindFrom(HitObject.SamplesBindable); + + // When a new hitobject is applied, the samples will be cleared before re-populating. + // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). + samplesBindable.CollectionChanged -= onSamplesChanged; + + if (nestedHitObjects.IsValueCreated) + { + foreach (var obj in nestedHitObjects.Value) + { + obj.OnNewResult -= onNewResult; + obj.OnRevertResult -= onRevertResult; + obj.ApplyCustomUpdateState -= onApplyCustomUpdateState; + } + + nestedHitObjects.Value.Clear(); + ClearNestedHitObjects(); + } + + HitObject.DefaultsApplied -= onDefaultsApplied; + HitObject = null; + + hasHitObjectApplied = false; + } + + protected sealed override void FreeAfterUse() + { + base.FreeAfterUse(); + + if (!IsInPool) + return; + + Free(); } /// @@ -268,7 +285,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onDefaultsApplied(HitObject hitObject) { - FreeAfterUse(); + Free(); Apply(hitObject); DefaultsApplied?.Invoke(this); } From b1e039bcec1c45c66f7e622d20b1dbb8c104fea9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 7 Nov 2020 00:40:26 +0900 Subject: [PATCH 4461/6909] Prevent overrides from messing with application/freeing --- .../Objects/Drawables/DrawableOsuHitObject.cs | 24 ++++++------- .../Objects/Drawables/DrawableSlider.cs | 18 +++++----- .../Objects/Drawables/DrawableHitObject.cs | 35 +++++++++++++++---- 3 files changed, 49 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index 0326f38439..d17bf93fa0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -52,19 +52,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }); } - public override void Free() + protected override void OnApply(HitObject hitObject) { - IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable); - PositionBindable.UnbindFrom(HitObject.PositionBindable); - StackHeightBindable.UnbindFrom(HitObject.StackHeightBindable); - ScaleBindable.UnbindFrom(HitObject.ScaleBindable); - - base.Free(); - } - - public override void Apply(HitObject hitObject) - { - base.Apply(hitObject); + base.OnApply(hitObject); IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable); PositionBindable.BindTo(HitObject.PositionBindable); @@ -72,6 +62,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindTo(HitObject.ScaleBindable); } + protected override void OnFree(HitObject hitObject) + { + base.OnFree(hitObject); + + IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable); + PositionBindable.UnbindFrom(HitObject.PositionBindable); + StackHeightBindable.UnbindFrom(HitObject.StackHeightBindable); + ScaleBindable.UnbindFrom(HitObject.ScaleBindable); + } + // Forward all internal management to shakeContainer. // This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690) protected override void AddInternal(Drawable drawable) => shakeContainer.Add(drawable); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 844e95aa7a..3f91a31066 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -78,22 +78,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking.BindValueChanged(updateSlidingSample); } - public override void Free() + protected override void OnApply(HitObject hitObject) { - PathVersion.UnbindFrom(HitObject.Path.Version); - - base.Free(); - } - - public override void Apply(HitObject hitObject) - { - base.Apply(hitObject); + base.OnApply(hitObject); // Ensure that the version will change after the upcoming BindTo(). pathVersion.Value = int.MaxValue; PathVersion.BindTo(HitObject.Path.Version); } + protected override void OnFree(HitObject hitObject) + { + base.OnFree(hitObject); + + PathVersion.UnbindFrom(HitObject.Path.Version); + } + private PausableSkinnableSound slidingSample; protected override void LoadSamples() diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 8e56b07420..ecd345f1a9 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -160,10 +160,9 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Applies a new to be represented by this . /// /// The to apply. - public virtual void Apply(HitObject hitObject) + public void Apply(HitObject hitObject) { - if (hasHitObjectApplied) - Free(); + free(); HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}."); @@ -202,14 +201,18 @@ namespace osu.Game.Rulesets.Objects.Drawables if (IsLoaded) Schedule(() => updateState(ArmedState.Idle, true)); + OnApply(hitObject); hasHitObjectApplied = true; } /// /// Removes the currently applied /// - public virtual void Free() + private void free() { + if (!hasHitObjectApplied) + return; + StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); @@ -234,8 +237,10 @@ namespace osu.Game.Rulesets.Objects.Drawables } HitObject.DefaultsApplied -= onDefaultsApplied; - HitObject = null; + OnFree(HitObject); + + HitObject = null; hasHitObjectApplied = false; } @@ -243,10 +248,27 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.FreeAfterUse(); + // Freeing while not in a pool would cause the DHO to not be usable elsewhere in the hierarchy without being re-applied. if (!IsInPool) return; - Free(); + free(); + } + + /// + /// Invoked for this to take on any values from a newly-applied . + /// + /// The being applied. + protected virtual void OnApply(HitObject hitObject) + { + } + + /// + /// Invoked for this to revert any values previously taken on from the currently-applied . + /// + /// The currently-applied . + protected virtual void OnFree(HitObject hitObject) + { } /// @@ -285,7 +307,6 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onDefaultsApplied(HitObject hitObject) { - Free(); Apply(hitObject); DefaultsApplied?.Invoke(this); } From 4a07a7e757ce283e2445abe4522cfb80bdad6bc6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 7 Nov 2020 00:40:41 +0900 Subject: [PATCH 4462/6909] Refactor test --- osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index e0898dc95d..f76c7e2a3e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; @@ -25,16 +24,14 @@ namespace osu.Game.Rulesets.Osu.Tests { Position = new Vector2(256, 192), IndexInCurrentCombo = 0, + StartTime = Time.Current, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(150, 100), new Vector2(300, 0), }) - })) - { - Clock = new FramedClock(new StopwatchClock(true)) - }); + }))); AddWaitStep("wait for progression", 1); @@ -42,6 +39,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Position = new Vector2(256, 192), ComboIndex = 1, + StartTime = dho.HitObject.StartTime, Path = new SliderPath(PathType.Bezier, new[] { Vector2.Zero, From 91c627c22d2db9001668bfc51a87ae46849db59b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 7 Nov 2020 00:57:33 +0900 Subject: [PATCH 4463/6909] Revert HOC changes --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 89 ++++++++++------------ 1 file changed, 42 insertions(+), 47 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 4666aa9211..4cadfa9ad4 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Rulesets.UI public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - private readonly Dictionary startTimeMap = new Dictionary(); + private readonly Dictionary bindable, double timeAtAdd)> startTimeMap = new Dictionary, double)>(); public HitObjectContainer() { @@ -25,7 +25,10 @@ namespace osu.Game.Rulesets.UI public virtual void Add(DrawableHitObject hitObject) { - bindStartTime(hitObject); + // Added first for the comparer to remain ordered during AddInternal + startTimeMap[hitObject] = (hitObject.HitObject.StartTimeBindable.GetBoundCopy(), hitObject.HitObject.StartTime); + startTimeMap[hitObject].bindable.BindValueChanged(_ => onStartTimeChanged(hitObject)); + AddInternal(hitObject); } @@ -34,19 +37,54 @@ namespace osu.Game.Rulesets.UI if (!RemoveInternal(hitObject)) return false; - unbindStartTime(hitObject); + // Removed last for the comparer to remain ordered during RemoveInternal + startTimeMap[hitObject].bindable.UnbindAll(); + startTimeMap.Remove(hitObject); return true; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + unbindStartTimeMap(); + } + public virtual void Clear(bool disposeChildren = true) { ClearInternal(disposeChildren); - unbindAllStartTimes(); + unbindStartTimeMap(); + } + + private void unbindStartTimeMap() + { + foreach (var kvp in startTimeMap) + kvp.Value.bindable.UnbindAll(); + startTimeMap.Clear(); } public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); + private void onStartTimeChanged(DrawableHitObject hitObject) + { + if (!RemoveInternal(hitObject)) + return; + + // Update the stored time, preserving the existing bindable + startTimeMap[hitObject] = (startTimeMap[hitObject].bindable, hitObject.HitObject.StartTime); + AddInternal(hitObject); + } + + protected override int Compare(Drawable x, Drawable y) + { + if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) + return base.Compare(x, y); + + // Put earlier hitobjects towards the end of the list, so they handle input first + int i = startTimeMap[yObj].timeAtAdd.CompareTo(startTimeMap[xObj].timeAtAdd); + return i == 0 ? CompareReverseChildID(x, y) : i; + } + protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) { if (!(e.Child is DrawableHitObject hitObject)) @@ -58,48 +96,5 @@ namespace osu.Game.Rulesets.UI hitObject.OnKilled(); } } - - #region Comparator + StartTime tracking - - private void bindStartTime(DrawableHitObject hitObject) - { - var bindable = hitObject.StartTimeBindable.GetBoundCopy(); - bindable.BindValueChanged(_ => onStartTimeChanged(hitObject)); - - startTimeMap[hitObject] = bindable; - } - - private void unbindStartTime(DrawableHitObject hitObject) - { - startTimeMap[hitObject].UnbindAll(); - startTimeMap.Remove(hitObject); - } - - private void unbindAllStartTimes() - { - foreach (var kvp in startTimeMap) - kvp.Value.UnbindAll(); - startTimeMap.Clear(); - } - - private void onStartTimeChanged(DrawableHitObject hitObject) => SortInternal(); - - protected override int Compare(Drawable x, Drawable y) - { - if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) - return base.Compare(x, y); - - // Put earlier hitobjects towards the end of the list, so they handle input first - int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); - return i == 0 ? CompareReverseChildID(x, y) : i; - } - - #endregion - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - unbindAllStartTimes(); - } } } From c8ecf15d716831189b771d40d70f76062569e665 Mon Sep 17 00:00:00 2001 From: kamp Date: Sat, 7 Nov 2020 00:35:26 +0100 Subject: [PATCH 4464/6909] Change VSCode build task args to use - instead of / --- .vscode/tasks.json | 50 +++++++++++++++++++++++----------------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index e638dec767..aa77d4f055 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -11,9 +11,9 @@ "build", "--no-restore", "osu.Desktop", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -26,10 +26,10 @@ "build", "--no-restore", "osu.Desktop", - "/p:Configuration=Release", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -42,9 +42,9 @@ "build", "--no-restore", "osu.Game.Tests", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -57,10 +57,10 @@ "build", "--no-restore", "osu.Game.Tests", - "/p:Configuration=Release", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -73,9 +73,9 @@ "build", "--no-restore", "osu.Game.Tournament.Tests", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -88,10 +88,10 @@ "build", "--no-restore", "osu.Game.Tournament.Tests", - "/p:Configuration=Release", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -104,10 +104,10 @@ "build", "--no-restore", "osu.Game.Benchmarks", - "/p:Configuration=Release", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" From ceb17764b18a3655acbce440edb2e4a22bce31b0 Mon Sep 17 00:00:00 2001 From: kamp Date: Sat, 7 Nov 2020 01:09:21 +0100 Subject: [PATCH 4465/6909] Also replace / with - for args in other task.jsons --- osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json | 14 +++++++------- osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json | 14 +++++++------- osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json | 14 +++++++------- osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json | 14 +++++++------- osu.Game.Tournament.Tests/.vscode/tasks.json | 14 +++++++------- 5 files changed, 35 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json index 18a6f8ca70..2c915a31b7 100644 --- a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json @@ -11,9 +11,9 @@ "build", "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -26,10 +26,10 @@ "build", "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", - "/p:Configuration=Release", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json index 608c4340ac..ca03924c70 100644 --- a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json @@ -11,9 +11,9 @@ "build", "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -26,10 +26,10 @@ "build", "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", - "/p:Configuration=Release", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json index ed2a015e11..14ffbfb4ae 100644 --- a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json @@ -11,9 +11,9 @@ "build", "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -26,10 +26,10 @@ "build", "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", - "/p:Configuration=Release", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json index 9b91f2c9b9..09340f6f9f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json @@ -11,9 +11,9 @@ "build", "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -26,10 +26,10 @@ "build", "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", - "/p:Configuration=Release", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" diff --git a/osu.Game.Tournament.Tests/.vscode/tasks.json b/osu.Game.Tournament.Tests/.vscode/tasks.json index 37f2f32874..c69ac0391a 100644 --- a/osu.Game.Tournament.Tests/.vscode/tasks.json +++ b/osu.Game.Tournament.Tests/.vscode/tasks.json @@ -11,9 +11,9 @@ "build", "--no-restore", "osu.Game.Tournament.Tests.csproj", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" @@ -26,10 +26,10 @@ "build", "--no-restore", "osu.Game.Tournament.Tests.csproj", - "/p:Configuration=Release", - "/p:GenerateFullPaths=true", - "/m", - "/verbosity:m" + "-p:Configuration=Release", + "-p:GenerateFullPaths=true", + "-m", + "-verbosity:m" ], "group": "build", "problemMatcher": "$msCompile" From b47a2a03d5563e71bec072ee3921221af30e35af Mon Sep 17 00:00:00 2001 From: kamp Date: Sat, 7 Nov 2020 01:53:14 +0100 Subject: [PATCH 4466/6909] Fix nullref when quickdeleting slider that hasn't been selected yet --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 9b758ec898..34dc356bc3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override bool HandleQuickDeletion() { - var hoveredControlPoint = ControlPointVisualiser.Pieces.FirstOrDefault(p => p.IsHovered); + var hoveredControlPoint = ControlPointVisualiser?.Pieces?.FirstOrDefault(p => p.IsHovered); if (hoveredControlPoint == null) return false; From 42c543472de01cfda43b3f4dfaa41698ced8db52 Mon Sep 17 00:00:00 2001 From: kamp Date: Sat, 7 Nov 2020 01:56:41 +0100 Subject: [PATCH 4467/6909] Remove unnecessary null coalesce --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 34dc356bc3..7ae4f387ca 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders public override bool HandleQuickDeletion() { - var hoveredControlPoint = ControlPointVisualiser?.Pieces?.FirstOrDefault(p => p.IsHovered); + var hoveredControlPoint = ControlPointVisualiser?.Pieces.FirstOrDefault(p => p.IsHovered); if (hoveredControlPoint == null) return false; From c2a5fd2832b12f7bd259d0646c0d6feded3f0b96 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Nov 2020 00:17:09 +0900 Subject: [PATCH 4468/6909] Add test coverage --- osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index afaaafdd26..bb56131b04 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -37,6 +37,12 @@ namespace osu.Game.Tests.Editing })); } + [Test] + public void TestPatchNoObjectChanges() + { + runTest(new OsuBeatmap()); + } + [Test] public void TestAddHitObject() { From c5b6908e71cdeaadad6f52b531a2e30c9b8158b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Nov 2020 00:17:23 +0900 Subject: [PATCH 4469/6909] Always write [HitObjects] to file I think this is expected. If not, there's an alternative solution. --- 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 80fd6c22bb..7ddb0b4caa 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -235,11 +235,11 @@ namespace osu.Game.Beatmaps.Formats private void handleHitObjects(TextWriter writer) { + writer.WriteLine("[HitObjects]"); + if (beatmap.HitObjects.Count == 0) return; - writer.WriteLine("[HitObjects]"); - foreach (var h in beatmap.HitObjects) handleHitObject(writer, h); } From b0052210b6a925d709dff9854b20795702fcfa03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 8 Nov 2020 00:18:25 +0900 Subject: [PATCH 4470/6909] Add asserts of HitObjects indices --- osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs index 72d3421755..f2e0320ce3 100644 --- a/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs +++ b/osu.Game/Screens/Edit/LegacyEditorBeatmapPatcher.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Text; using DiffPlex; @@ -35,6 +36,9 @@ namespace osu.Game.Screens.Edit int oldHitObjectsIndex = Array.IndexOf(result.PiecesOld, "[HitObjects]"); int newHitObjectsIndex = Array.IndexOf(result.PiecesNew, "[HitObjects]"); + Debug.Assert(oldHitObjectsIndex >= 0); + Debug.Assert(newHitObjectsIndex >= 0); + var toRemove = new List(); var toAdd = new List(); From e078b78dcc6e5a244bf95376859390205d1f74c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 7 Nov 2020 20:31:44 +0100 Subject: [PATCH 4471/6909] Ensure callbacks don't fire when restoring default beatmap --- osu.Game/Screens/Edit/Editor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 13d1f378a6..85467d3bbb 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -499,6 +499,9 @@ namespace osu.Game.Screens.Edit // confirming exit without save means we should delete the new beatmap completely. beatmapManager.Delete(playableBeatmap.BeatmapInfo.BeatmapSet); + // eagerly clear contents before restoring default beatmap to prevent value change callbacks from firing. + ClearInternal(); + // in theory this shouldn't be required but due to EF core not sharing instance states 100% // MusicController is unaware of the changed DeletePending state. Beatmap.SetDefault(); From ddbd6df24d825750065ec55942d725320ae211d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 7 Nov 2020 20:59:41 +0100 Subject: [PATCH 4472/6909] Unbind bindable lists for general safety --- .../Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs | 1 + .../Compose/Components/Timeline/TimelineControlPointDisplay.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs index ba3ac9113e..e76ab71e54 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs @@ -20,6 +20,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { base.LoadBeatmap(beatmap); + controlPointGroups.UnbindAll(); controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((sender, args) => { diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 0da1b43201..13191df13c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -27,6 +27,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadBeatmap(beatmap); + controlPointGroups.UnbindAll(); controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((sender, args) => { From 6d4bb4316c4a28f518d6543f28557ad4885cffb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 8 Nov 2020 00:12:25 +0100 Subject: [PATCH 4473/6909] Fix difficulty retrieval for online-sourced beatmaps --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 9820d508dd..eb83c88318 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -202,7 +202,9 @@ namespace osu.Game.Beatmaps /// A token that may be used to cancel this update. private void updateBindable([NotNull] BindableStarDifficulty bindable, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, CancellationToken cancellationToken = default) { - GetAsync(new DifficultyCacheLookup(bindable.Beatmap, rulesetInfo, mods), cancellationToken) + // GetDifficultyAsync will fall back to existing data from BeatmapInfo if not locally available + // (contrary to GetAsync) + GetDifficultyAsync(bindable.Beatmap, rulesetInfo, mods, cancellationToken) .ContinueWith(t => { // We're on a threadpool thread, but we should exit back to the update thread so consumers can safely handle value-changed events. From 790a2ca97d506635f6ed0baddb93dd7050808625 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Nov 2020 12:29:52 +0100 Subject: [PATCH 4474/6909] Extract UserActivity logic to OsuGame. --- osu.Game/OsuGame.cs | 5 +++++ osu.Game/Screens/IOsuScreen.cs | 6 ++++++ osu.Game/Screens/OsuScreen.cs | 31 +++++-------------------------- 3 files changed, 16 insertions(+), 26 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 64f8d4415b..5119f262d5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -51,6 +51,7 @@ using osu.Game.Screens.Select; using osu.Game.Updater; using osu.Game.Utils; using LogLevel = osu.Framework.Logging.LogLevel; +using osu.Game.Users; namespace osu.Game { @@ -961,11 +962,15 @@ namespace osu.Game LocalUserPlaying.Value = false; if (current is IOsuScreen currentOsuScreen) + { OverlayActivationMode.UnbindFrom(currentOsuScreen.OverlayActivationMode); + API.Activity.UnbindFrom(currentOsuScreen.Activity); + } if (newScreen is IOsuScreen newOsuScreen) { OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); + ((IBindable)API.Activity).BindTo(newOsuScreen.Activity); MusicController.AllowRateAdjustments = newOsuScreen.AllowRateAdjustments; diff --git a/osu.Game/Screens/IOsuScreen.cs b/osu.Game/Screens/IOsuScreen.cs index e19037c2c4..cc8778d9ae 100644 --- a/osu.Game/Screens/IOsuScreen.cs +++ b/osu.Game/Screens/IOsuScreen.cs @@ -6,6 +6,7 @@ using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Rulesets; +using osu.Game.Users; namespace osu.Game.Screens { @@ -43,6 +44,11 @@ namespace osu.Game.Screens /// IBindable OverlayActivationMode { get; } + /// + /// The current for this screen. + /// + IBindable Activity { get; } + /// /// The amount of parallax to be applied while this screen is displayed. /// diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index a44d14fb5c..9b508c0cba 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -14,7 +14,6 @@ using osu.Game.Rulesets; using osu.Game.Screens.Menu; using osu.Game.Overlays; using osu.Game.Users; -using osu.Game.Online.API; using osu.Game.Rulesets.Mods; namespace osu.Game.Screens @@ -63,22 +62,12 @@ namespace osu.Game.Screens /// protected virtual UserActivity InitialActivity => null; - private UserActivity activity; - /// /// The current for this screen. /// - protected UserActivity Activity - { - get => activity; - set - { - if (value == activity) return; + protected readonly Bindable Activity; - activity = value; - updateActivity(); - } - } + IBindable IOsuScreen.Activity => Activity; /// /// Whether to disallow changes to game-wise Beatmap/Ruleset bindables for this screen (and all children). @@ -135,15 +124,13 @@ namespace osu.Game.Screens [Resolved(canBeNull: true)] private OsuLogo logo { get; set; } - [Resolved(canBeNull: true)] - private IAPIProvider api { get; set; } - protected OsuScreen() { Anchor = Anchor.Centre; Origin = Anchor.Centre; OverlayActivationMode = new Bindable(InitialOverlayActivationMode); + Activity = new Bindable(); } [BackgroundDependencyLoader(true)] @@ -158,8 +145,6 @@ namespace osu.Game.Screens sampleExit?.Play(); applyArrivingDefaults(true); - updateActivity(); - base.OnResuming(last); } @@ -176,8 +161,8 @@ namespace osu.Game.Screens backgroundStack?.Push(localBackground = CreateBackground()); - if (activity == null) - Activity = InitialActivity; + if (Activity.Value == null) + Activity.Value = InitialActivity; base.OnEntering(last); } @@ -196,12 +181,6 @@ namespace osu.Game.Screens return false; } - private void updateActivity() - { - if (api != null) - api.Activity.Value = activity; - } - /// /// Fired when this screen was entered or resumed and the logo state is required to be adjusted. /// From 55070556a724b0f91618c953229a815e840fa5ce Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Nov 2020 12:53:19 +0100 Subject: [PATCH 4475/6909] Move activity setup to BDL. --- osu.Game/Screens/OsuScreen.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 9b508c0cba..22c8d48606 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -137,6 +137,9 @@ namespace osu.Game.Screens private void load(OsuGame osu, AudioManager audio) { sampleExit = audio.Samples.Get(@"UI/screen-back"); + + if (Activity.Value == null) + Activity.Value = InitialActivity; } public override void OnResuming(IScreen last) @@ -161,9 +164,6 @@ namespace osu.Game.Screens backgroundStack?.Push(localBackground = CreateBackground()); - if (Activity.Value == null) - Activity.Value = InitialActivity; - base.OnEntering(last); } From 89b98b53883e71efc310af04a7720c3202278249 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Nov 2020 13:16:27 +0100 Subject: [PATCH 4476/6909] Make multiplayer screen handle user activity updates from subscreens. --- osu.Game/Screens/Multi/Multiplayer.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index e6abde4d43..a323faeea1 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -24,6 +24,7 @@ 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.Users; using osuTK; namespace osu.Game.Screens.Multi @@ -140,10 +141,10 @@ namespace osu.Game.Screens.Multi } }; - screenStack.Push(loungeSubScreen = new LoungeSubScreen()); - screenStack.ScreenPushed += screenPushed; screenStack.ScreenExited += screenExited; + + screenStack.Push(loungeSubScreen = new LoungeSubScreen()); } private readonly IBindable apiState = new Bindable(); @@ -311,18 +312,18 @@ namespace osu.Game.Screens.Multi private void screenPushed(IScreen lastScreen, IScreen newScreen) { - subScreenChanged(newScreen); + subScreenChanged(lastScreen, newScreen); } private void screenExited(IScreen lastScreen, IScreen newScreen) { - subScreenChanged(newScreen); + subScreenChanged(lastScreen, newScreen); if (screenStack.CurrentScreen == null && this.IsCurrentScreen()) this.Exit(); } - private void subScreenChanged(IScreen newScreen) + private void subScreenChanged(IScreen lastScreen, IScreen newScreen) { switch (newScreen) { @@ -337,6 +338,12 @@ namespace osu.Game.Screens.Multi break; } + if (lastScreen is IOsuScreen lastOsuScreen) + Activity.UnbindFrom(lastOsuScreen.Activity); + + if (newScreen is IOsuScreen newOsuScreen) + ((IBindable)Activity).BindTo(newOsuScreen.Activity); + updatePollingRate(isIdle.Value); createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200); From 48fce8c4b92319e21a4e5bd3bb4d78d550654034 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Nov 2020 13:21:21 +0100 Subject: [PATCH 4477/6909] Add user activities to multi subscreens. --- osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs | 3 +++ osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 2 ++ osu.Game/Users/UserActivity.cs | 13 +++++++++++++ 3 files changed, 18 insertions(+) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index dd40f4adc6..4dc9ba549b 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -14,6 +14,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Match; +using osu.Game.Users; namespace osu.Game.Screens.Multi.Lounge { @@ -24,6 +25,8 @@ namespace osu.Game.Screens.Multi.Lounge protected FilterControl Filter; + protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); + private readonly Bindable initialRoomsReceived = new Bindable(); private Container content; diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 0d2adeb27c..2cbe215a39 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -21,6 +21,7 @@ using osu.Game.Screens.Multi.Play; using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Play; using osu.Game.Screens.Select; +using osu.Game.Users; using Footer = osu.Game.Screens.Multi.Match.Components.Footer; namespace osu.Game.Screens.Multi.Match @@ -60,6 +61,7 @@ namespace osu.Game.Screens.Multi.Match public MatchSubScreen(Room room) { Title = room.RoomID.Value == null ? "New room" : room.Name.Value; + Activity.Value = new UserActivity.InLobby(room); } [BackgroundDependencyLoader] diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 3c9f201805..0b4fa94942 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -3,6 +3,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osuTK.Graphics; @@ -61,9 +62,21 @@ namespace osu.Game.Users public override string Status => @"Spectating a game"; } + public class SearchingForLobby : UserActivity + { + public override string Status => @"Looking for a lobby"; + } + public class InLobby : UserActivity { public override string Status => @"In a multiplayer lobby"; + + public readonly Room Room; + + public InLobby(Room room) + { + Room = room; + } } } } From f1942fdb9cf2ab512976988dc77fc22843073d0e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Nov 2020 14:38:59 +0100 Subject: [PATCH 4478/6909] Fix tests not building. --- osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 9662bd65b4..8e151a987a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -26,7 +26,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestGenericActivity() { - AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby()); + AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby(null)); AddStep("Run command", () => Add(new NowPlayingCommand())); @@ -57,7 +57,7 @@ namespace osu.Game.Tests.Visual.Online [TestCase(false)] public void TestLinkPresence(bool hasOnlineId) { - AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby()); + AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby(null)); AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null) { From d8d402211969d2432ed67e696055f85165928fd3 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Nov 2020 14:39:56 +0100 Subject: [PATCH 4479/6909] Precise XMLDoc --- osu.Game/Screens/OsuScreen.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 22c8d48606..57b201c57a 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -56,9 +56,9 @@ namespace osu.Game.Screens protected new OsuGameBase Game => base.Game as OsuGameBase; /// - /// The to set the user's activity automatically to when this screen is entered - /// This will be automatically set to for this screen on entering unless - /// is manually set before. + /// The to set the user's activity automatically to when this screen is entered. + /// This will be automatically set to for this screen on entering for the first time + /// unless is manually set before. /// protected virtual UserActivity InitialActivity => null; From e44951969f46c0e1e198c827692734e2216ba7b7 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Nov 2020 18:22:19 +0100 Subject: [PATCH 4480/6909] Use ??= operator instead of null check. --- osu.Game/Screens/OsuScreen.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 57b201c57a..e7b872a6fb 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -138,8 +138,7 @@ namespace osu.Game.Screens { sampleExit = audio.Samples.Get(@"UI/screen-back"); - if (Activity.Value == null) - Activity.Value = InitialActivity; + Activity.Value ??= InitialActivity; } public override void OnResuming(IScreen last) From e4fb9b4dd3317d32fa623eb82643522cd3c57116 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 8 Nov 2020 18:42:24 +0100 Subject: [PATCH 4481/6909] Display room name on discord rich presence. --- osu.Desktop/DiscordRichPresence.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 08cc0e7f5f..fa33339696 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -135,6 +135,9 @@ namespace osu.Desktop case UserActivity.Editing edit: return edit.Beatmap.ToString(); + + case UserActivity.InLobby lobby: + return lobby.Room.Name.ToString(); } return string.Empty; From 90ce1bd5f05dae59094aa5ae6921cb056a0ff42e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Nov 2020 10:40:16 +0900 Subject: [PATCH 4482/6909] Add missing async suffix --- osu.Game/Database/UserLookupCache.cs | 2 +- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index b7c6c480a9..01231f3f2b 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -30,7 +30,7 @@ namespace osu.Game.Database /// private bool pendingRequestConsumedIDs; - public Task GetUser(int userId, CancellationToken token = default) => GetAsync(userId, token); + public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token); protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) { diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 8299619a18..f6833385a4 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Dashboard foreach (var id in e.NewItems.OfType().ToArray()) { - users.GetUser(id).ContinueWith(u => + users.GetUserAsync(id).ContinueWith(u => { if (u.Result == null) return; From dc69eefa51490208a965103ca59417f1038a9e7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Nov 2020 11:54:28 +0900 Subject: [PATCH 4483/6909] Use HashSet instead of ConcurentBag --- osu.Game/Database/UserLookupCache.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 01231f3f2b..adf6b4e9f8 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -16,7 +15,7 @@ namespace osu.Game.Database { public class UserLookupCache : MemoryCachingComponent { - private readonly ConcurrentBag nextTaskIDs = new ConcurrentBag(); + private readonly HashSet nextTaskIDs = new HashSet(); [Resolved] private IAPIProvider api { get; set; } From 690e69bcc6738cd68013619248b4c6afee912c91 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Nov 2020 12:22:54 +0900 Subject: [PATCH 4484/6909] Reforamt for legibility --- .../Dashboard/CurrentlyPlayingDisplay.cs | 53 ++++++++++--------- 1 file changed, 28 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index f6833385a4..c3ab9e86d4 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -53,40 +53,43 @@ namespace osu.Game.Overlays.Dashboard base.LoadComplete(); playingUsers.BindTo(spectatorStreaming.PlayingUsers); - playingUsers.BindCollectionChanged((sender, e) => Schedule(() => - { - switch (e.Action) - { - case NotifyCollectionChangedAction.Add: + playingUsers.BindCollectionChanged(onUsersChanged, true); + } - foreach (var id in e.NewItems.OfType().ToArray()) + private void onUsersChanged(object sender, NotifyCollectionChangedEventArgs e) => Schedule(() => + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + foreach (var id in e.NewItems.OfType().ToArray()) + { + users.GetUserAsync(id).ContinueWith(u => { - users.GetUserAsync(id).ContinueWith(u => + if (u.Result == null) return; + + Schedule(() => { - if (u.Result == null) + // user may no longer be playing. + if (!playingUsers.Contains(u.Result.Id)) return; - Schedule(() => - { - if (playingUsers.Contains(u.Result.Id)) - userFlow.Add(createUserPanel(u.Result)); - }); + userFlow.Add(createUserPanel(u.Result)); }); - } + }); + } - break; + break; - case NotifyCollectionChangedAction.Remove: - foreach (var u in e.OldItems.OfType()) - userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire(); - break; + case NotifyCollectionChangedAction.Remove: + foreach (var u in e.OldItems.OfType()) + userFlow.FirstOrDefault(card => card.User.Id == u)?.Expire(); + break; - case NotifyCollectionChangedAction.Reset: - userFlow.Clear(); - break; - } - }), true); - } + case NotifyCollectionChangedAction.Reset: + userFlow.Clear(); + break; + } + }); private PlayingUserPanel createUserPanel(User user) => new PlayingUserPanel(user).With(panel => From cfb42037cff74a3db3dbcf80cb176003f428c7ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Nov 2020 12:23:29 +0900 Subject: [PATCH 4485/6909] Refactor request string logic to avoid linq usage --- osu.Game/Online/API/Requests/GetUsersRequest.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/GetUsersRequest.cs b/osu.Game/Online/API/Requests/GetUsersRequest.cs index 0ec5173fb6..969d7fdba3 100644 --- a/osu.Game/Online/API/Requests/GetUsersRequest.cs +++ b/osu.Game/Online/API/Requests/GetUsersRequest.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; namespace osu.Game.Online.API.Requests { @@ -20,6 +19,6 @@ namespace osu.Game.Online.API.Requests this.userIds = userIds; } - protected override string Target => $@"users/?{userIds.Select(u => $"ids[]={u}&").Aggregate((a, b) => a + b)}"; + protected override string Target => "users/?ids[]=" + string.Join("&ids[]=", userIds); } } From 490fbd1dd8d00b332a2dca9eaa218ad76e996117 Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Mon, 9 Nov 2020 05:54:48 +0100 Subject: [PATCH 4486/6909] Fix ki* textures having an incorrect vertical position --- osu.Game/Skinning/LegacyHealthDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 489e23ab7a..1f18647184 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Skinning Math.Clamp(Clock.ElapsedFrameTime, 0, 200), fill.Width, (float)Current.Value * maxFillWidth, 0, 200, Easing.OutQuint); - marker.Position = fill.Position + new Vector2(fill.DrawWidth, fill.DrawHeight / 2); + marker.Position = fill.Position + new Vector2(fill.DrawWidth, 0); } public void Flash(JudgementResult result) => marker.Flash(result); From 7b0e387dfc1d1b322eada75093bbe9bab063ac9f Mon Sep 17 00:00:00 2001 From: Morilli <35152647+Morilli@users.noreply.github.com> Date: Mon, 9 Nov 2020 08:20:19 +0100 Subject: [PATCH 4487/6909] apply different offset based on whether the marker is newStyle or not --- osu.Game/Skinning/LegacyHealthDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyHealthDisplay.cs b/osu.Game/Skinning/LegacyHealthDisplay.cs index 1f18647184..2921d46467 100644 --- a/osu.Game/Skinning/LegacyHealthDisplay.cs +++ b/osu.Game/Skinning/LegacyHealthDisplay.cs @@ -80,7 +80,7 @@ namespace osu.Game.Skinning Math.Clamp(Clock.ElapsedFrameTime, 0, 200), fill.Width, (float)Current.Value * maxFillWidth, 0, 200, Easing.OutQuint); - marker.Position = fill.Position + new Vector2(fill.DrawWidth, 0); + marker.Position = fill.Position + new Vector2(fill.DrawWidth, isNewStyle ? fill.DrawHeight / 2 : 0); } public void Flash(JudgementResult result) => marker.Flash(result); From 33c3b07101c4ca61c7496b89bb087b56d161142a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 9 Nov 2020 19:06:48 +0900 Subject: [PATCH 4488/6909] Fix events not being bound correctly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ecd345f1a9..6bb22018ec 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -180,9 +180,9 @@ namespace osu.Game.Rulesets.Objects.Drawables { 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); + drawableNested.OnNewResult += onNewResult; + drawableNested.OnRevertResult += onRevertResult; + drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; nestedHitObjects.Value.Add(drawableNested); AddNestedHitObject(drawableNested); From 4d6f0a8ea743cfa7aa67b58cfb629b514f529dfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Nov 2020 19:42:00 +0900 Subject: [PATCH 4489/6909] Fix API request error handling --- osu.Game/Database/UserLookupCache.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index adf6b4e9f8..c85ad6d651 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -76,7 +76,7 @@ namespace osu.Game.Database var request = new GetUsersRequest(lookupItems); // rather than queueing, we maintain our own single-threaded request stream. - request.Perform(api); + api.Perform(request); return request.Result?.Users; } From ba137aadc82d979f970c72f29b7dfe14756d0752 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Nov 2020 20:44:12 +0900 Subject: [PATCH 4490/6909] 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 f56baf4e5f..e3285222f8 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 3783ae7d5c..832722c729 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index ed3ec9e48b..ad6dd2a0b5 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 2e3fdf8116a55b1de874066e27d65f0b3345d2a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 9 Nov 2020 20:50:36 +0900 Subject: [PATCH 4491/6909] Update reference to TK game window class --- osu.Desktop/OsuGameDesktop.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 836b968a67..b17611f23f 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -130,7 +130,7 @@ namespace osu.Desktop switch (host.Window) { // Legacy osuTK DesktopGameWindow - case DesktopGameWindow desktopGameWindow: + case OsuTKDesktopWindow desktopGameWindow: desktopGameWindow.CursorState |= CursorState.Hidden; desktopGameWindow.SetIconFromStream(iconStream); desktopGameWindow.Title = Name; From b8c63e7944335ac8569e7d7209e418caca1a9acb Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 9 Nov 2020 13:39:50 +0100 Subject: [PATCH 4492/6909] Apply review suggestions. --- osu.Desktop/DiscordRichPresence.cs | 2 +- osu.Game/Screens/OsuScreen.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index fa33339696..26d7402a5b 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -137,7 +137,7 @@ namespace osu.Desktop return edit.Beatmap.ToString(); case UserActivity.InLobby lobby: - return lobby.Room.Name.ToString(); + return lobby.Room.Name.Value; } return string.Empty; diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index e7b872a6fb..851aedd84f 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens /// /// The current for this screen. /// - protected readonly Bindable Activity; + protected readonly Bindable Activity = new Bindable(); IBindable IOsuScreen.Activity => Activity; @@ -130,7 +130,6 @@ namespace osu.Game.Screens Origin = Anchor.Centre; OverlayActivationMode = new Bindable(InitialOverlayActivationMode); - Activity = new Bindable(); } [BackgroundDependencyLoader(true)] From ec8b726ea80461024ad3f642b9c4301d00cf8109 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 9 Nov 2020 21:51:58 +0900 Subject: [PATCH 4493/6909] Re-privatise start time bindable --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 6bb22018ec..58b24cb7b2 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual float SamplePlaybackPosition => 0.5f; - public readonly Bindable StartTimeBindable = new Bindable(); + private readonly Bindable startTimeBindable = new Bindable(); private readonly BindableList samplesBindable = new BindableList(); private readonly Bindable userPositionalHitSounds = new Bindable(); private readonly Bindable comboIndexBindable = new Bindable(); @@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.LoadComplete(); - StartTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); + startTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); updateState(ArmedState.Idle, true); @@ -188,7 +188,7 @@ namespace osu.Game.Rulesets.Objects.Drawables AddNestedHitObject(drawableNested); } - StartTimeBindable.BindTo(HitObject.StartTimeBindable); + startTimeBindable.BindTo(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) comboIndexBindable.BindTo(combo.ComboIndexBindable); @@ -213,7 +213,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (!hasHitObjectApplied) return; - StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); + startTimeBindable.UnbindFrom(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); From ac47399e6e0c57044d800f464a419f85a656f31d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 00:30:23 +0900 Subject: [PATCH 4494/6909] Update state after OnApply() --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 58b24cb7b2..77c4ea42df 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -197,11 +197,12 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject.DefaultsApplied += onDefaultsApplied; + OnApply(hitObject); + // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. if (IsLoaded) Schedule(() => updateState(ArmedState.Idle, true)); - OnApply(hitObject); hasHitObjectApplied = true; } From 66ea1572c72452824debf726c0703630b26b0f97 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 01:10:00 +0900 Subject: [PATCH 4495/6909] Fix unsafe list manipulation in BeatmapDifficultyCache --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 44 +++++++++++++-------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index eb83c88318..eeb6075ef5 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -31,7 +31,7 @@ namespace osu.Game.Beatmaps private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyCache)); // All bindables that should be updated along with the current ruleset + mods. - private readonly LockedWeakList trackedBindables = new LockedWeakList(); + private readonly WeakList trackedBindables = new WeakList(); [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -59,7 +59,10 @@ namespace osu.Game.Beatmaps public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) { var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); - trackedBindables.Add(bindable); + + lock (trackedBindables) + trackedBindables.Add(bindable); + return bindable; } @@ -86,7 +89,8 @@ namespace osu.Game.Beatmaps /// The s to get the difficulty with. /// An optional which stops computing the star difficulty. /// The . - public Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null, CancellationToken cancellationToken = default) + public Task GetDifficultyAsync([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo = null, [CanBeNull] IEnumerable mods = null, + CancellationToken cancellationToken = default) { // In the case that the user hasn't given us a ruleset, use the beatmap's default ruleset. rulesetInfo ??= beatmapInfo.Ruleset; @@ -148,15 +152,18 @@ namespace osu.Game.Beatmaps /// private void updateTrackedBindables() { - cancelTrackedBindableUpdate(); - trackedUpdateCancellationSource = new CancellationTokenSource(); - - foreach (var b in trackedBindables) + lock (trackedBindables) { - var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken); - linkedCancellationSources.Add(linkedSource); + cancelTrackedBindableUpdate(); + trackedUpdateCancellationSource = new CancellationTokenSource(); - updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token); + foreach (var b in trackedBindables) + { + var linkedSource = CancellationTokenSource.CreateLinkedTokenSource(trackedUpdateCancellationSource.Token, b.CancellationToken); + linkedCancellationSources.Add(linkedSource); + + updateBindable(b, currentRuleset.Value, currentMods.Value, linkedSource.Token); + } } } @@ -165,15 +172,18 @@ namespace osu.Game.Beatmaps /// private void cancelTrackedBindableUpdate() { - trackedUpdateCancellationSource?.Cancel(); - trackedUpdateCancellationSource = null; - - if (linkedCancellationSources != null) + lock (trackedBindables) { - foreach (var c in linkedCancellationSources) - c.Dispose(); + trackedUpdateCancellationSource?.Cancel(); + trackedUpdateCancellationSource = null; - linkedCancellationSources.Clear(); + if (linkedCancellationSources != null) + { + foreach (var c in linkedCancellationSources) + c.Dispose(); + + linkedCancellationSources.Clear(); + } } } From d4d3a6621e6f512ccfbcf378ab6500d8acc07db1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 01:30:25 +0900 Subject: [PATCH 4496/6909] Disable automatic lifetime management --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 77c4ea42df..05ef9b162f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -109,6 +109,8 @@ namespace osu.Game.Rulesets.Objects.Drawables public override bool RemoveCompletedTransforms => false; protected override bool RequiresChildrenUpdate => true; + public override bool ResetLifetimeWhenAssigned => false; // DHOs do their own lifetime management. + public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart); private readonly Bindable state = new Bindable(); From ef3c918a3cb028fee0b7fc47223732b01bfc25e0 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 5 Nov 2020 06:41:56 -0800 Subject: [PATCH 4497/6909] Simplify input buttons/keys in tests --- .../TestSceneSliderPlacementBlueprint.cs | 3 +- .../Visual/Gameplay/TestSceneAutoplay.cs | 10 +---- .../Gameplay/TestSceneGameplayMenuOverlay.cs | 34 +++++++---------- .../Visual/Gameplay/TestSceneKeyBindings.cs | 6 +-- .../Visual/Gameplay/TestSceneKeyCounter.cs | 6 +-- .../Visual/Gameplay/TestScenePause.cs | 6 +-- .../Gameplay/TestSceneReplayRecorder.cs | 3 +- .../Visual/Menus/TestSceneToolbar.cs | 4 +- .../TestSceneLoungeRoomsContainer.cs | 6 +-- .../Navigation/TestSceneScreenNavigation.cs | 22 +++-------- .../Visual/Online/TestSceneChatOverlay.cs | 9 +---- .../Settings/TestSceneKeyBindingPanel.cs | 3 +- .../SongSelect/TestScenePlaySongSelect.cs | 37 ++++++------------- .../UserInterface/TestSceneCommentEditor.cs | 10 +---- 14 files changed, 46 insertions(+), 113 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs index a452f93676..67a2e5a47c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderPlacementBlueprint.cs @@ -280,8 +280,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private void addClickStep(MouseButton button) { - AddStep($"press {button}", () => InputManager.PressButton(button)); - AddStep($"release {button}", () => InputManager.ReleaseButton(button)); + AddStep($"click {button}", () => InputManager.Click(button)); } private void assertPlaced(bool expected) => AddAssert($"slider {(expected ? "placed" : "not placed")}", () => (getSlider() != null) == expected); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs index 4743317fdd..e198a8504b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneAutoplay.cs @@ -41,11 +41,11 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime); // test seek via keyboard - AddStep("seek with right arrow key", () => press(Key.Right)); + AddStep("seek with right arrow key", () => InputManager.Key(Key.Right)); AddAssert("time seeked forward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime > time + 2000); AddStep("store time", () => time = Player.GameplayClockContainer.GameplayClock.CurrentTime); - AddStep("seek with left arrow key", () => press(Key.Left)); + AddStep("seek with left arrow key", () => InputManager.Key(Key.Left)); AddAssert("time seeked backward", () => Player.GameplayClockContainer.GameplayClock.CurrentTime < time); seekToBreak(0); @@ -67,11 +67,5 @@ namespace osu.Game.Tests.Visual.Gameplay BreakPeriod destBreak() => Beatmap.Value.Beatmap.Breaks.ElementAt(breakIndex); } - - private void press(Key key) - { - InputManager.PressKey(key); - InputManager.ReleaseKey(key); - } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs index fc9cbb073e..d69ac665cc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayMenuOverlay.cs @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Visual.Gameplay { showOverlay(); - AddStep("Up arrow", () => press(Key.Up)); + AddStep("Up arrow", () => InputManager.Key(Key.Up)); AddAssert("Last button selected", () => pauseOverlay.Buttons.Last().Selected.Value); } @@ -98,7 +98,7 @@ namespace osu.Game.Tests.Visual.Gameplay { showOverlay(); - AddStep("Down arrow", () => press(Key.Down)); + AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddAssert("First button selected", () => getButton(0).Selected.Value); } @@ -110,11 +110,11 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("Show overlay", () => failOverlay.Show()); - AddStep("Up arrow", () => press(Key.Up)); + AddStep("Up arrow", () => InputManager.Key(Key.Up)); AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); - AddStep("Up arrow", () => press(Key.Up)); + AddStep("Up arrow", () => InputManager.Key(Key.Up)); AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); - AddStep("Up arrow", () => press(Key.Up)); + AddStep("Up arrow", () => InputManager.Key(Key.Up)); AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); } @@ -126,11 +126,11 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("Show overlay", () => failOverlay.Show()); - AddStep("Down arrow", () => press(Key.Down)); + AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); - AddStep("Down arrow", () => press(Key.Down)); + AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddAssert("Last button selected", () => failOverlay.Buttons.Last().Selected.Value); - AddStep("Down arrow", () => press(Key.Down)); + AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddAssert("First button selected", () => failOverlay.Buttons.First().Selected.Value); } @@ -177,7 +177,7 @@ namespace osu.Game.Tests.Visual.Gameplay { showOverlay(); - AddStep("Down arrow", () => press(Key.Down)); + AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); AddAssert("First button not selected", () => !getButton(0).Selected.Value); AddAssert("Second button selected", () => getButton(1).Selected.Value); @@ -195,7 +195,7 @@ namespace osu.Game.Tests.Visual.Gameplay }); AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); - AddStep("Up arrow", () => press(Key.Up)); + AddStep("Up arrow", () => InputManager.Key(Key.Up)); AddAssert("Second button not selected", () => !getButton(1).Selected.Value); AddAssert("First button selected", () => getButton(0).Selected.Value); } @@ -210,7 +210,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Hover second button", () => InputManager.MoveMouseTo(getButton(1))); AddStep("Unhover second button", () => InputManager.MoveMouseTo(Vector2.Zero)); - AddStep("Down arrow", () => press(Key.Down)); + AddStep("Down arrow", () => InputManager.Key(Key.Down)); AddAssert("First button selected", () => getButton(0).Selected.Value); // Initial state condition } @@ -246,8 +246,8 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("Select second button", () => { - press(Key.Down); - press(Key.Down); + InputManager.Key(Key.Down); + InputManager.Key(Key.Down); }); bool triggered = false; @@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Gameplay { lastAction = pauseOverlay.OnRetry; pauseOverlay.OnRetry = () => triggered = true; - press(Key.Enter); + InputManager.Key(Key.Enter); }); AddAssert("Action was triggered", () => @@ -290,12 +290,6 @@ namespace osu.Game.Tests.Visual.Gameplay private DialogButton getButton(int index) => pauseOverlay.Buttons.Skip(index).First(); - private void press(Key key) - { - InputManager.PressKey(key); - InputManager.ReleaseKey(key); - } - private void press(GlobalAction action) { globalActionContainer.TriggerPressed(action); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs index db65e91d17..6de85499c5 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyBindings.cs @@ -32,11 +32,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestDefaultsWhenNotDatabased() { - AddStep("fire key", () => - { - InputManager.PressKey(Key.A); - InputManager.ReleaseKey(Key.A); - }); + AddStep("fire key", () => InputManager.Key(Key.A)); AddAssert("received key", () => receiver.ReceivedAction); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs index d7a3f80256..87ab42fe60 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneKeyCounter.cs @@ -40,11 +40,7 @@ namespace osu.Game.Tests.Visual.Gameplay void addPressKeyStep() { - AddStep($"Press {testKey} key", () => - { - InputManager.PressKey(testKey); - InputManager.ReleaseKey(testKey); - }); + AddStep($"Press {testKey} key", () => InputManager.Key(testKey)); } addPressKeyStep(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index ac0e8eb0d4..46dd91710a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -59,11 +59,7 @@ namespace osu.Game.Tests.Visual.Gameplay confirmClockRunning(false); confirmPauseOverlayShown(false); - AddStep("click to resume", () => - { - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); - }); + AddStep("click to resume", () => InputManager.Click(MouseButton.Left)); confirmClockRunning(true); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index 47dd47959d..b72960931f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -158,8 +158,7 @@ namespace osu.Game.Tests.Visual.Gameplay 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); + InputManager.Click(MouseButton.Left); }, 10, true)); AddWaitStep("move", 10); AddStep("stop move", () => moveFunction.Cancel()); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs index 2a4486812c..57d60cea9e 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneToolbar.cs @@ -65,10 +65,8 @@ namespace osu.Game.Tests.Visual.Menus AddStep($"switch to ruleset {i} via shortcut", () => { InputManager.PressKey(Key.ControlLeft); - InputManager.PressKey(numberKey); - + InputManager.Key(numberKey); InputManager.ReleaseKey(Key.ControlLeft); - InputManager.ReleaseKey(numberKey); }); AddUntilStep("ruleset switched", () => rulesetSelector.Current.Value.Equals(expected)); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index b1f6ee3e3a..e33d15cfff 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -71,11 +71,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void press(Key down) { - AddStep($"press {down}", () => - { - InputManager.PressKey(down); - InputManager.ReleaseKey(down); - }); + AddStep($"press {down}", () => InputManager.Key(down)); } [Test] diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 5963f806c6..d87854a7ea 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -58,7 +58,7 @@ namespace osu.Game.Tests.Visual.Navigation if (withUserPause) AddStep("pause", () => Game.Dependencies.Get().Stop(true)); - AddStep("press enter", () => pressAndRelease(Key.Enter)); + AddStep("press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); AddUntilStep("wait for fail", () => player.HasFailed); @@ -122,11 +122,11 @@ namespace osu.Game.Tests.Visual.Navigation public void TestOpenOptionsAndExitWithEscape() { AddUntilStep("Wait for options to load", () => Game.Settings.IsLoaded); - AddStep("Enter menu", () => pressAndRelease(Key.Enter)); + AddStep("Enter menu", () => InputManager.Key(Key.Enter)); AddStep("Move mouse to options overlay", () => InputManager.MoveMouseTo(optionsButtonPosition)); AddStep("Click options overlay", () => InputManager.Click(MouseButton.Left)); AddAssert("Options overlay was opened", () => Game.Settings.State.Value == Visibility.Visible); - AddStep("Hide options overlay using escape", () => pressAndRelease(Key.Escape)); + AddStep("Hide options overlay using escape", () => InputManager.Key(Key.Escape)); AddAssert("Options overlay was closed", () => Game.Settings.State.Value == Visibility.Hidden); } @@ -158,10 +158,8 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("Change ruleset to osu!taiko", () => { InputManager.PressKey(Key.ControlLeft); - InputManager.PressKey(Key.Number2); - + InputManager.Key(Key.Number2); InputManager.ReleaseKey(Key.ControlLeft); - InputManager.ReleaseKey(Key.Number2); }); AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType().Single().Current.Value.ID == 1); @@ -181,10 +179,8 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("Change ruleset to osu!taiko", () => { InputManager.PressKey(Key.ControlLeft); - InputManager.PressKey(Key.Number2); - + InputManager.Key(Key.Number2); InputManager.ReleaseKey(Key.ControlLeft); - InputManager.ReleaseKey(Key.Number2); }); AddAssert("Ruleset changed to osu!taiko", () => Game.Toolbar.ChildrenOfType().Single().Current.Value.ID == 1); @@ -193,7 +189,7 @@ namespace osu.Game.Tests.Visual.Navigation } private void pushEscape() => - AddStep("Press escape", () => pressAndRelease(Key.Escape)); + AddStep("Press escape", () => InputManager.Key(Key.Escape)); private void exitViaEscapeAndConfirm() { @@ -208,12 +204,6 @@ namespace osu.Game.Tests.Visual.Navigation ConfirmAtMainMenu(); } - private void pressAndRelease(Key key) - { - InputManager.PressKey(key); - InputManager.ReleaseKey(key); - } - private class TestSongSelect : PlaySongSelect { public ModSelectOverlay ModSelectOverlay => ModSelect; diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs index 0025a26baf..fca642ad6c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatOverlay.cs @@ -103,11 +103,7 @@ namespace osu.Game.Tests.Visual.Online public void TestChannelShortcutKeys() { AddStep("Join channels", () => channels.ForEach(channel => channelManager.JoinChannel(channel))); - AddStep("Close channel selector", () => - { - InputManager.PressKey(Key.Escape); - InputManager.ReleaseKey(Key.Escape); - }); + AddStep("Close channel selector", () => InputManager.Key(Key.Escape)); AddUntilStep("Wait for close", () => chatOverlay.SelectionOverlayState == Visibility.Hidden); for (int zeroBasedIndex = 0; zeroBasedIndex < 10; ++zeroBasedIndex) @@ -216,9 +212,8 @@ namespace osu.Game.Tests.Visual.Online { var channelKey = Key.Number0 + number; InputManager.PressKey(Key.AltLeft); - InputManager.PressKey(channelKey); + InputManager.Key(channelKey); InputManager.ReleaseKey(Key.AltLeft); - InputManager.ReleaseKey(channelKey); } private void clickDrawable(Drawable d) diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 987a4a67fe..8330b9b360 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -51,8 +51,7 @@ namespace osu.Game.Tests.Visual.Settings clickDelegate = Scheduler.AddDelayed(() => { - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); + InputManager.Click(MouseButton.Left); if (++buttonClicks == 2) { diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index cd97ffe9e7..aa531ba106 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -98,10 +98,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select next and enter", () => { - InputManager.PressKey(Key.Down); - InputManager.ReleaseKey(Key.Down); - InputManager.PressKey(Key.Enter); - InputManager.ReleaseKey(Key.Enter); + InputManager.Key(Key.Down); + InputManager.Key(Key.Enter); }); AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); @@ -123,10 +121,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select next and enter", () => { - InputManager.PressKey(Key.Enter); - InputManager.ReleaseKey(Key.Enter); - InputManager.PressKey(Key.Down); - InputManager.ReleaseKey(Key.Down); + InputManager.Key(Key.Enter); + InputManager.Key(Key.Down); }); AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); @@ -151,11 +147,9 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.MoveMouseTo(songSelect.Carousel.ChildrenOfType() .First(b => ((CarouselBeatmap)b.Item).Beatmap != songSelect.Carousel.SelectedBeatmap)); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); + InputManager.Click(MouseButton.Left); - InputManager.PressKey(Key.Enter); - InputManager.ReleaseKey(Key.Enter); + InputManager.Key(Key.Enter); }); AddUntilStep("wait for not current", () => !songSelect.IsCurrentScreen()); @@ -182,8 +176,7 @@ namespace osu.Game.Tests.Visual.SongSelect InputManager.PressButton(MouseButton.Left); - InputManager.PressKey(Key.Enter); - InputManager.ReleaseKey(Key.Enter); + InputManager.Key(Key.Enter); InputManager.ReleaseButton(MouseButton.Left); }); @@ -567,10 +560,8 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("press ctrl+enter", () => { InputManager.PressKey(Key.ControlLeft); - InputManager.PressKey(Key.Enter); - + InputManager.Key(Key.Enter); InputManager.ReleaseKey(Key.ControlLeft); - InputManager.ReleaseKey(Key.Enter); }); AddUntilStep("wait for player", () => Stack.CurrentScreen is PlayerLoader); @@ -617,8 +608,7 @@ namespace osu.Game.Tests.Visual.SongSelect { InputManager.MoveMouseTo(difficultyIcon); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); + InputManager.Click(MouseButton.Left); }); AddAssert("Selected beatmap correct", () => getCurrentBeatmapIndex() == getDifficultyIconIndex(set, difficultyIcon)); @@ -647,8 +637,7 @@ namespace osu.Game.Tests.Visual.SongSelect { InputManager.MoveMouseTo(filteredIcon); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); + InputManager.Click(MouseButton.Left); }); AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); @@ -691,8 +680,7 @@ namespace osu.Game.Tests.Visual.SongSelect { InputManager.MoveMouseTo(difficultyIcon); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); + InputManager.Click(MouseButton.Left); }); AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); @@ -738,8 +726,7 @@ namespace osu.Game.Tests.Visual.SongSelect { InputManager.MoveMouseTo(groupIcon); - InputManager.PressButton(MouseButton.Left); - InputManager.ReleaseButton(MouseButton.Left); + InputManager.Click(MouseButton.Left); }); AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs index d0a2ca83e3..920b437f57 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneCommentEditor.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.UserInterface }); AddStep("enter text", () => commentEditor.Current.Value = "text"); - AddStep("press Enter", () => press(Key.Enter)); + AddStep("press Enter", () => InputManager.Key(Key.Enter)); AddAssert("text committed", () => commentEditor.CommittedText == "text"); AddAssert("button is loading", () => commentEditor.IsLoading); @@ -63,7 +63,7 @@ namespace osu.Game.Tests.Visual.UserInterface InputManager.Click(MouseButton.Left); }); - AddStep("press Enter", () => press(Key.Enter)); + AddStep("press Enter", () => InputManager.Key(Key.Enter)); AddAssert("no text committed", () => commentEditor.CommittedText == null); AddAssert("button is not loading", () => !commentEditor.IsLoading); @@ -101,12 +101,6 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("cancel action fired", () => cancellableCommentEditor.Cancelled); } - private void press(Key key) - { - InputManager.PressKey(key); - InputManager.ReleaseKey(key); - } - private class TestCommentEditor : CommentEditor { public new Bindable Current => base.Current; From 1548c0dc25d192b5c3faeb802f4b7f07f68bef9e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 07:27:27 +0900 Subject: [PATCH 4498/6909] Ensure graph hover state is updated after data changes --- .../Overlays/Profile/Header/Components/RankGraph.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index ffc060b3f1..51a13a1231 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -94,13 +94,18 @@ namespace osu.Game.Overlays.Profile.Header.Components } graph.FadeTo(ranks.Length > 1 ? 1 : 0, fade_duration, Easing.Out); + + if (IsHovered) + graph.UpdateBallPosition(lastHoverPosition); } + private float lastHoverPosition; + protected override bool OnHover(HoverEvent e) { if (ranks?.Length > 1) { - graph.UpdateBallPosition(e.MousePosition.X); + graph.UpdateBallPosition(lastHoverPosition = e.MousePosition.X); graph.ShowBar(); } @@ -117,11 +122,7 @@ namespace osu.Game.Overlays.Profile.Header.Components protected override void OnHoverLost(HoverLostEvent e) { - if (ranks?.Length > 1) - { - graph.HideBar(); - } - + graph.HideBar(); base.OnHoverLost(e); } From 37feedae7a5226c41d2c4cf9ea27d37bce88d3f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 07:27:38 +0900 Subject: [PATCH 4499/6909] Fix potential crash due to stale index --- .../Overlays/Profile/Header/Components/RankGraph.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index 51a13a1231..ffa918e4e8 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private readonly OsuSpriteText placeholder; private KeyValuePair[] ranks; - private int dayIndex; + private int hoveredIndex = -1; public readonly Bindable Statistics = new Bindable(); public RankGraph() @@ -55,7 +55,7 @@ namespace osu.Game.Overlays.Profile.Header.Components } }; - graph.OnBallMove += i => dayIndex = i; + graph.OnBallMove += i => hoveredIndex = i; } [BackgroundDependencyLoader] @@ -74,6 +74,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private void updateStatistics(UserStatistics statistics) { placeholder.FadeIn(fade_duration, Easing.Out); + hoveredIndex = -1; if (statistics?.Ranks.Global == null) { @@ -201,14 +202,14 @@ namespace osu.Game.Overlays.Profile.Header.Components { get { - if (Statistics.Value?.Ranks.Global == null) + if (ranks == null || hoveredIndex == -1) return null; - var days = ranked_days - ranks[dayIndex].Key + 1; + var days = ranked_days - ranks[hoveredIndex].Key + 1; return new TooltipDisplayContent { - Rank = $"#{ranks[dayIndex].Value:#,##0}", + Rank = $"#{ranks[hoveredIndex].Value:#,##0}", Time = days == 0 ? "now" : $"{days} days ago" }; } From 4303a24a6f01c66e92550135a42a467aca861858 Mon Sep 17 00:00:00 2001 From: kamp Date: Tue, 10 Nov 2020 00:53:24 +0100 Subject: [PATCH 4500/6909] Replace rankHistory in JsonProperty with rank_history --- osu.Game/Users/User.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 1fbc3d06f4..2a76a963e1 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -186,7 +186,7 @@ namespace osu.Game.Users } } - [JsonProperty(@"rankHistory")] + [JsonProperty(@"rank_history")] private RankHistoryData rankHistory { set => statistics.RankHistory = value; From c671d97e6f3638b2ceae96986a509eafec0bff99 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 9 Nov 2020 18:39:35 -0800 Subject: [PATCH 4501/6909] Disable watch button on the local user --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index c3ab9e86d4..35cb97adbb 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -102,6 +102,8 @@ namespace osu.Game.Overlays.Dashboard { public readonly User User; + private PurpleTriangleButton watchButton; + [Resolved(canBeNull: true)] private OsuGame game { get; set; } @@ -127,7 +129,7 @@ namespace osu.Game.Overlays.Dashboard Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, - new PurpleTriangleButton + watchButton = new PurpleTriangleButton { RelativeSizeAxes = Axes.X, Text = "Watch", @@ -139,6 +141,12 @@ namespace osu.Game.Overlays.Dashboard }, }; } + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + watchButton.Enabled.Value = User.Id != api.LocalUser.Value.Id; + } } } } From 670d6d87198e97b331095d9d6fb156d0f5ba3840 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 9 Nov 2020 18:59:54 -0800 Subject: [PATCH 4502/6909] Make button field readonly --- osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 35cb97adbb..36e2e9ae43 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -102,7 +102,7 @@ namespace osu.Game.Overlays.Dashboard { public readonly User User; - private PurpleTriangleButton watchButton; + private readonly PurpleTriangleButton watchButton; [Resolved(canBeNull: true)] private OsuGame game { get; set; } From 4af390a1681e4a616989d6483368ffe4d393ec50 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 12:33:07 +0900 Subject: [PATCH 4503/6909] Move hierarchy init to load and remove unnecessary field storage --- .../Dashboard/CurrentlyPlayingDisplay.cs | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index 36e2e9ae43..d39a81f5e8 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -102,8 +102,6 @@ namespace osu.Game.Overlays.Dashboard { public readonly User User; - private readonly PurpleTriangleButton watchButton; - [Resolved(canBeNull: true)] private OsuGame game { get; set; } @@ -112,7 +110,11 @@ namespace osu.Game.Overlays.Dashboard User = user; AutoSizeAxes = Axes.Both; + } + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { InternalChildren = new Drawable[] { new FillFlowContainer @@ -123,30 +125,25 @@ namespace osu.Game.Overlays.Dashboard Width = 290, Children = new Drawable[] { - new UserGridPanel(user) + new UserGridPanel(User) { RelativeSizeAxes = Axes.X, Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, }, - watchButton = new PurpleTriangleButton + new PurpleTriangleButton { RelativeSizeAxes = Axes.X, Text = "Watch", Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(user))) + Action = () => game?.PerformFromScreen(s => s.Push(new Spectator(User))), + Enabled = { Value = User.Id != api.LocalUser.Value.Id } } } }, }; } - - [BackgroundDependencyLoader] - private void load(IAPIProvider api) - { - watchButton.Enabled.Value = User.Id != api.LocalUser.Value.Id; - } } } } From 6cc0bf17a9c06333d35079c46531bbea7fb0fab6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 14:31:27 +0900 Subject: [PATCH 4504/6909] Add explicit lock object and some xmldoc for clarity --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 25 +++++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index eeb6075ef5..3b58062add 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -30,9 +30,23 @@ namespace osu.Game.Beatmaps // Too many simultaneous updates can lead to stutters. One thread seems to work fine for song select display purposes. private readonly ThreadedTaskScheduler updateScheduler = new ThreadedTaskScheduler(1, nameof(BeatmapDifficultyCache)); - // All bindables that should be updated along with the current ruleset + mods. + /// + /// All bindables that should be updated along with the current ruleset + mods. + /// private readonly WeakList trackedBindables = new WeakList(); + /// + /// Cancellation sources used by tracked bindables. + /// + private readonly List linkedCancellationSources = new List(); + + /// + /// Lock to be held when operating on or . + /// + private readonly object bindableUpdateLock = new object(); + + private CancellationTokenSource trackedUpdateCancellationSource; + [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -60,7 +74,7 @@ namespace osu.Game.Beatmaps { var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); - lock (trackedBindables) + lock (bindableUpdateLock) trackedBindables.Add(bindable); return bindable; @@ -144,15 +158,12 @@ namespace osu.Game.Beatmaps return DifficultyRating.Easy; } - private CancellationTokenSource trackedUpdateCancellationSource; - private readonly List linkedCancellationSources = new List(); - /// /// Updates all tracked using the current ruleset and mods. /// private void updateTrackedBindables() { - lock (trackedBindables) + lock (bindableUpdateLock) { cancelTrackedBindableUpdate(); trackedUpdateCancellationSource = new CancellationTokenSource(); @@ -172,7 +183,7 @@ namespace osu.Game.Beatmaps /// private void cancelTrackedBindableUpdate() { - lock (trackedBindables) + lock (bindableUpdateLock) { trackedUpdateCancellationSource?.Cancel(); trackedUpdateCancellationSource = null; From a2ef3aa21a5aec1565347af57be37e4564acb5f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 16:26:30 +0900 Subject: [PATCH 4505/6909] Force beatmap listing overlay's textbox back on screen when a key is pressed Not the cleanest solution, but works for now. Will eventually be replaced after the header is updated to reflect the latest designs (which keeps it on screen in all cases). Closes https://github.com/ppy/osu/issues/10703. --- .../BeatmapListingFilterControl.cs | 10 +++++++++- .../BeatmapListingSearchControl.cs | 19 +++++++++++++++++++ osu.Game/Overlays/BeatmapListingOverlay.cs | 7 +++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 3be38e3c1d..d991dcfcfb 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -32,6 +32,11 @@ namespace osu.Game.Overlays.BeatmapListing /// public Action SearchStarted; + /// + /// Any time the search text box receives key events (even while masked). + /// + public Action TypingStarted; + /// /// True when pagination has reached the end of available results. /// @@ -82,7 +87,10 @@ namespace osu.Game.Overlays.BeatmapListing Radius = 3, Offset = new Vector2(0f, 1f), }, - Child = searchControl = new BeatmapListingSearchControl(), + Child = searchControl = new BeatmapListingSearchControl + { + TypingStarted = () => TypingStarted?.Invoke() + } }, new Container { diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 3694c9855e..758781bb7d 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.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; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osuTK; using osu.Framework.Bindables; +using osu.Framework.Input.Events; using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; @@ -19,6 +21,11 @@ namespace osu.Game.Overlays.BeatmapListing { public class BeatmapListingSearchControl : CompositeDrawable { + /// + /// Any time the text box receives key events (even while masked). + /// + public Action TypingStarted; + public Bindable Query => textBox.Current; public Bindable Ruleset => modeFilter.Current; @@ -102,6 +109,7 @@ namespace osu.Game.Overlays.BeatmapListing textBox = new BeatmapSearchTextBox { RelativeSizeAxes = Axes.X, + TypingStarted = () => TypingStarted?.Invoke(), }, new ReverseChildIDFillFlowContainer { @@ -138,12 +146,23 @@ namespace osu.Game.Overlays.BeatmapListing private class BeatmapSearchTextBox : SearchTextBox { + /// + /// Any time the text box receives key events (even while masked). + /// + public Action TypingStarted; + protected override Color4 SelectionColour => Color4.Gray; public BeatmapSearchTextBox() { PlaceholderText = @"type in keywords..."; } + + protected override bool OnKeyDown(KeyDownEvent e) + { + TypingStarted?.Invoke(); + return base.OnKeyDown(e); + } } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 144af91145..1e29e713af 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -68,6 +68,7 @@ namespace osu.Game.Overlays Header, filterControl = new BeatmapListingFilterControl { + TypingStarted = onTypingStarted, SearchStarted = onSearchStarted, SearchFinished = onSearchFinished, }, @@ -102,6 +103,12 @@ namespace osu.Game.Overlays }; } + private void onTypingStarted() + { + // temporary until the textbox/header is updated to always stay on screen. + resultScrollContainer.ScrollToStart(); + } + protected override void OnFocus(FocusEvent e) { base.OnFocus(e); From 5221a34929719a84562ddafe8c24dc304a0cba90 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 16:32:58 +0900 Subject: [PATCH 4506/6909] Only handle keys which create characters --- .../Overlays/BeatmapListing/BeatmapListingSearchControl.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 758781bb7d..e232bf045f 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -160,8 +160,11 @@ namespace osu.Game.Overlays.BeatmapListing protected override bool OnKeyDown(KeyDownEvent e) { + if (!base.OnKeyDown(e)) + return false; + TypingStarted?.Invoke(); - return base.OnKeyDown(e); + return true; } } } From 109abc0e2945adfa60c35a78e9ddde27d82066a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 08:27:59 +0100 Subject: [PATCH 4507/6909] Always store standardised score when populating ScoreInfo --- 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 33271d9689..499673619f 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -290,7 +290,7 @@ namespace osu.Game.Rulesets.Scoring /// public virtual void PopulateScore(ScoreInfo score) { - score.TotalScore = (long)Math.Round(TotalScore.Value); + score.TotalScore = (long)Math.Round(GetStandardisedScore()); score.Combo = Combo.Value; score.MaxCombo = HighestCombo.Value; score.Accuracy = Math.Round(Accuracy.Value, 4); From a012105dac4ca9af9f1d7d4548857eaba7c69892 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 16:54:33 +0900 Subject: [PATCH 4508/6909] Fix editor quick delete being triggerable from left mouse button Closes https://github.com/ppy/osu/issues/10629. --- .../Edit/Compose/Components/BlueprintContainer.cs | 2 +- .../Edit/Compose/Components/SelectionHandler.cs | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index e7da220946..d5306c3450 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -303,7 +303,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { if (blueprint.IsHovered) { - selectedPerformed &= SelectionHandler.HandleSelectionRequested(blueprint, e.CurrentState); + selectedPerformed &= SelectionHandler.HandleSelectionRequested(blueprint, e); clickSelectionBegan = true; break; } diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 0bbbfaf5e8..21810379cc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -14,7 +14,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input; using osu.Framework.Input.Bindings; -using osu.Framework.Input.States; +using osu.Framework.Input.Events; using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -218,17 +218,17 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Handle a blueprint requesting selection. /// /// The blueprint. - /// The input state at the point of selection. + /// The mouse event responsible for selection. /// Whether a selection was performed. - internal bool HandleSelectionRequested(SelectionBlueprint blueprint, InputState state) + internal bool HandleSelectionRequested(SelectionBlueprint blueprint, MouseButtonEvent e) { - if (state.Keyboard.ShiftPressed && state.Mouse.IsPressed(MouseButton.Right)) + if (e.ShiftPressed && e.Button == MouseButton.Right) { handleQuickDeletion(blueprint); return false; } - if (state.Keyboard.ControlPressed && state.Mouse.IsPressed(MouseButton.Left)) + if (e.ControlPressed && e.Button == MouseButton.Left) blueprint.ToggleSelection(); else ensureSelected(blueprint); From de6fcd1792c6ed43b3e1e3945a0e244308259986 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 17:16:28 +0900 Subject: [PATCH 4509/6909] Fix BlueprintContainer triggering assert when left and right mouse button are pressed together --- .../Compose/Components/BlueprintContainer.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index d5306c3450..b67f6a6ba6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -295,21 +295,15 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. private bool beginClickSelection(MouseButtonEvent e) { - Debug.Assert(!clickSelectionBegan); - - bool selectedPerformed = true; - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) { - if (blueprint.IsHovered) - { - selectedPerformed &= SelectionHandler.HandleSelectionRequested(blueprint, e); - clickSelectionBegan = true; - break; - } + if (!blueprint.IsHovered) continue; + + if (SelectionHandler.HandleSelectionRequested(blueprint, e)) + return clickSelectionBegan = true; } - return selectedPerformed; + return false; } /// From 28daff17164eef0ad7caa55ad0a479ed8d9e5d75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 17:57:57 +0900 Subject: [PATCH 4510/6909] Stop mod select overlay hotkeys handling input when control is pressed Closes https://github.com/ppy/osu/issues/10766 in about the best way we can for now. --- osu.Game/Overlays/Mods/ModSection.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 3701f9ecab..0107f94dcf 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -85,6 +85,8 @@ namespace osu.Game.Overlays.Mods protected override bool OnKeyDown(KeyDownEvent e) { + if (e.ControlPressed) return false; + if (ToggleKeys != null) { var index = Array.IndexOf(ToggleKeys, e.Key); From 833c0b223ef0a184314c46f9d756c2c3a6ca43d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 18:08:12 +0900 Subject: [PATCH 4511/6909] Clamp index to valid bounds --- osu.Game/Overlays/Profile/Header/Components/RankGraph.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs index ffa918e4e8..26126bca58 100644 --- a/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs +++ b/osu.Game/Overlays/Profile/Header/Components/RankGraph.cs @@ -189,7 +189,7 @@ namespace osu.Game.Overlays.Profile.Header.Components public void HideBar() => bar.FadeOut(fade_duration); - private int calculateIndex(float mouseXPosition) => (int)MathF.Round(mouseXPosition / DrawWidth * (DefaultValueCount - 1)); + private int calculateIndex(float mouseXPosition) => (int)Math.Clamp(MathF.Round(mouseXPosition / DrawWidth * (DefaultValueCount - 1)), 0, DefaultValueCount - 1); private Vector2 calculateBallPosition(int index) { From f5076fe3b8f2e46100c4c13a6e96996523345bc5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 18:15:11 +0900 Subject: [PATCH 4512/6909] Revert unnecessary change --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 05ef9b162f..77c4ea42df 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -109,8 +109,6 @@ namespace osu.Game.Rulesets.Objects.Drawables public override bool RemoveCompletedTransforms => false; protected override bool RequiresChildrenUpdate => true; - public override bool ResetLifetimeWhenAssigned => false; // DHOs do their own lifetime management. - public override bool IsPresent => base.IsPresent || (State.Value == ArmedState.Idle && Clock?.CurrentTime >= LifetimeStart); private readonly Bindable state = new Bindable(); From 88112801eba65f5602bc8f5fe16d8c61390f3d79 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 18:56:09 +0900 Subject: [PATCH 4513/6909] Remove result storage from hitobject --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 6 ------ osu.Game/Rulesets/Objects/HitObject.cs | 6 ------ 2 files changed, 12 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 77c4ea42df..7a4e136553 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -166,16 +166,10 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}."); - // Copy any existing result from the hitobject (required for rewind / judgement revert). - Result = HitObject.Result; - // Ensure this DHO has a result. Result ??= CreateResult(HitObject.CreateJudgement()) ?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); - // Ensure the hitobject has a result. - HitObject.Result = Result; - foreach (var h in HitObject.NestedHitObjects) { var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); diff --git a/osu.Game/Rulesets/Objects/HitObject.cs b/osu.Game/Rulesets/Objects/HitObject.cs index 9ef3ff9c4a..826d411822 100644 --- a/osu.Game/Rulesets/Objects/HitObject.cs +++ b/osu.Game/Rulesets/Objects/HitObject.cs @@ -12,7 +12,6 @@ using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; @@ -169,11 +168,6 @@ namespace osu.Game.Rulesets.Objects /// [NotNull] protected virtual HitWindows CreateHitWindows() => new HitWindows(); - - /// - /// The result this was judged with. Used internally for rewinding within . - /// - internal JudgementResult Result; } public static class HitObjectExtensions From 757a4b5c319cf0b382e97db6ae5775c5ff5f1105 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 5 Nov 2020 19:47:23 +0900 Subject: [PATCH 4514/6909] Add hitobject lifetime model --- .../Objects/HitObjectLifetimeEntry.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs new file mode 100644 index 0000000000..f134c66274 --- /dev/null +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Objects +{ + /// + /// A that stores the lifetime for a . + /// + public class HitObjectLifetimeEntry : LifetimeEntry + { + /// + /// The . + /// + public readonly HitObject HitObject; + + /// + /// Creates a new . + /// + /// The to store the lifetime of. + public HitObjectLifetimeEntry(HitObject hitObject) + { + HitObject = hitObject; + ResetLifetimeStart(); + } + + // The lifetime start, as set by the hitobject. + private double realLifetimeStart = double.MinValue; + + /// + /// The time at which the should become alive. + /// + public new double LifetimeStart + { + get => realLifetimeStart; + set => setLifetime(realLifetimeStart = value, LifetimeEnd); + } + + // The lifetime end, as set by the hitobject. + private double realLifetimeEnd = double.MaxValue; + + /// + /// The time at which the should become dead. + /// + public new double LifetimeEnd + { + get => realLifetimeEnd; + set => setLifetime(LifetimeStart, realLifetimeEnd = value); + } + + private void setLifetime(double start, double end) + { + if (keepAlive) + { + start = double.MinValue; + end = double.MaxValue; + } + + base.LifetimeStart = start; + base.LifetimeEnd = end; + } + + private bool keepAlive; + + /// + /// Whether the should be kept always alive. + /// + internal bool KeepAlive + { + set + { + if (keepAlive == value) + return; + + keepAlive = value; + setLifetime(realLifetimeStart, realLifetimeEnd); + } + } + + /// + /// A safe offset prior to the start time of at which it may begin displaying contents. + /// By default, s are assumed to display their contents within 10 seconds prior to their start time. + /// + /// + /// This is only used as an optimisation to delay the initial update of the and may be tuned more aggressively if required. + /// It is indirectly used to decide the automatic transform offset provided to . + /// A more accurate should be set for further optimisation (in , for example). + /// + protected virtual double InitialLifetimeOffset => 10000; + + /// + /// Resets according to the start time of the . + /// + internal void ResetLifetimeStart() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; + } +} From 45e9f16f6b3a3300ce5779c032dbb1e51873a29a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 6 Nov 2020 22:09:54 +0900 Subject: [PATCH 4515/6909] Add initial DrawableRuleset interface --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 77 +++++++++++++++++++++++-- 1 file changed, 72 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index f6cf836fe7..8c6db661b5 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -15,7 +15,9 @@ using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Pooling; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Configuration; @@ -92,11 +94,8 @@ namespace osu.Game.Rulesets.UI protected IRulesetConfigManager Config { get; private set; } - /// - /// The mods which are to be applied. - /// [Cached(typeof(IReadOnlyList))] - protected readonly IReadOnlyList Mods; + protected override IReadOnlyList Mods { get; } private FrameStabilityContainer frameStabilityContainer; @@ -284,12 +283,15 @@ namespace osu.Game.Rulesets.UI } } + public sealed override DrawableHitObject GetDrawableRepresentation(HitObject hitObject) + => base.GetDrawableRepresentation(hitObject) ?? CreateDrawableRepresentation((TObject)hitObject); + /// /// Creates a DrawableHitObject from a HitObject. /// /// The HitObject to make drawable. /// The DrawableHitObject. - public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); + public virtual DrawableHitObject CreateDrawableRepresentation(TObject h) => null; public void Attach(KeyCounterDisplay keyCounter) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter); @@ -406,6 +408,11 @@ namespace osu.Game.Rulesets.UI /// public abstract IFrameStableClock FrameStableClock { get; } + /// + /// The mods which are to be applied. + /// + protected abstract IReadOnlyList Mods { get; } + /// ~ /// The associated ruleset. /// @@ -500,6 +507,66 @@ namespace osu.Game.Rulesets.UI /// Invoked when the user requests to pause while the resume overlay is active. /// public abstract void CancelResume(); + + /// + /// Whether this should retrieve pooled s. + /// + /// + /// Pools must be registered with this via in order for s to be retrieved. + /// + /// If true, hitobjects will be added to the via . + /// If false, will be used instead. + /// + /// + protected virtual bool PoolHitObjects => false; + + private readonly Dictionary pools = new Dictionary(); + + protected void RegisterPool(int initialSize, int? maximumSize = null) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + { + var pool = CreatePool(initialSize, maximumSize); + pools[typeof(TObject)] = pool; + AddInternal(pool); + } + + /// + /// Creates the to retrieve s of the given type from. + /// + /// The number of hitobject to be prepared for initial consumption. + /// An optional maximum size after which the pool will no longer be expanded. + /// The type of retrievable from this pool. + /// The . + protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) + where TDrawable : DrawableHitObject, new() + => new DrawablePool(initialSize, maximumSize); + + /// + /// Retrieves the drawable representation of a . + /// + /// The to retrieve the drawable representation of. + /// The representing . + public virtual DrawableHitObject GetDrawableRepresentation(HitObject hitObject) + { + if (!pools.TryGetValue(hitObject.GetType(), out var pool)) + return null; + + return (DrawableHitObject)pool.Get(d => + { + var dho = (DrawableHitObject)d; + + // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. + // This is done before Apply() so that the state is updated once when the hitobject is applied. + if (!dho.IsLoaded) + { + foreach (var m in Mods.OfType()) + m.ApplyToDrawableHitObjects(dho.Yield()); + } + + dho.Apply(hitObject); + }); + } } public class BeatmapInvalidForRulesetException : ArgumentException From 64460749767d4e3a6b3285aec060d3fc01132e44 Mon Sep 17 00:00:00 2001 From: cadon0 Date: Tue, 10 Nov 2020 01:52:26 +1300 Subject: [PATCH 4516/6909] Fix paths for storyboard animation sprites --- .../Drawables/DrawableStoryboardAnimation.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 97de239e4a..34120f4848 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -118,8 +118,24 @@ namespace osu.Game.Storyboards.Drawables for (int frame = 0; frame < Animation.FrameCount; frame++) { string framePath = Animation.Path.Replace(".", frame + "."); + Drawable sprite = storyboard.CreateSpriteFromResourcePath(framePath, textureStore); - AddFrame(storyboard.CreateSpriteFromResourcePath(framePath, textureStore), Animation.FrameDelay); + if (sprite != null) + { + AddFrame(sprite, Animation.FrameDelay); + continue; + } + + framePath = Animation.Path.Replace("0.", frame + "."); + sprite = storyboard.CreateSpriteFromResourcePath(framePath, textureStore); + + if (sprite != null) + { + AddFrame(sprite, Animation.FrameDelay); + } + + // todo: handle animation intentionally declared with more frames than sprites to cause a blinking effect + // e.g. beatmap 5381's "spr\play-skip.png" } Animation.ApplyTransforms(this); From 539806e9d6869b95f6b9ca0c3a51f45adc0e139c Mon Sep 17 00:00:00 2001 From: cadon0 Date: Tue, 10 Nov 2020 21:57:29 +1300 Subject: [PATCH 4517/6909] Rewrite - Add empty drawable whenever sprite not found --- .../Drawables/DrawableStoryboardAnimation.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 34120f4848..a78d0bf4d7 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -118,24 +118,8 @@ namespace osu.Game.Storyboards.Drawables for (int frame = 0; frame < Animation.FrameCount; frame++) { string framePath = Animation.Path.Replace(".", frame + "."); - Drawable sprite = storyboard.CreateSpriteFromResourcePath(framePath, textureStore); - - if (sprite != null) - { - AddFrame(sprite, Animation.FrameDelay); - continue; - } - - framePath = Animation.Path.Replace("0.", frame + "."); - sprite = storyboard.CreateSpriteFromResourcePath(framePath, textureStore); - - if (sprite != null) - { - AddFrame(sprite, Animation.FrameDelay); - } - - // todo: handle animation intentionally declared with more frames than sprites to cause a blinking effect - // e.g. beatmap 5381's "spr\play-skip.png" + Drawable sprite = storyboard.CreateSpriteFromResourcePath(framePath, textureStore) ?? Drawable.Empty(); + AddFrame(sprite, Animation.FrameDelay); } Animation.ApplyTransforms(this); From 6f3f6dc28b88c2ef5455c80cc6e81ac31423024c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 20:16:52 +0900 Subject: [PATCH 4518/6909] Add hitobject lifetime support --- .../TestSceneHitCircleApplication.cs | 2 +- .../TestSceneSliderApplication.cs | 2 +- .../TestSceneSpinnerApplication.cs | 2 +- .../Objects/Drawables/DrawableHitObject.cs | 53 +++++++++++++++---- .../Objects/HitObjectLifetimeEntry.cs | 18 +++++-- osu.Game/Rulesets/UI/DrawableRuleset.cs | 25 ++++++++- 6 files changed, 86 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs index 8b3fead366..5fc1082743 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircleApplication.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Osu.Tests { Position = new Vector2(128, 128), ComboIndex = 1, - }))); + }), null)); } private HitCircle prepareObject(HitCircle circle) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index f76c7e2a3e..fb1ebbb0d0 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Osu.Tests new Vector2(300, 0), }), RepeatCount = 1 - }))); + }), null)); } private Slider prepareObject(Slider slider) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs index 5951574079..0558dad30d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests Position = new Vector2(256, 192), ComboIndex = 1, Duration = 1000, - }))); + }), null)); } private Spinner prepareObject(Spinner circle) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 7a4e136553..2ac478f640 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -120,6 +120,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// private bool hasHitObjectApplied; + /// + /// The controlling the lifetime of the currently-attached . + /// + [CanBeNull] + private HitObjectLifetimeEntry lifetimeEntry; + /// /// Creates a new . /// @@ -143,7 +149,7 @@ namespace osu.Game.Rulesets.Objects.Drawables base.LoadAsyncComplete(); if (HitObject != null) - Apply(HitObject); + Apply(HitObject, lifetimeEntry); } protected override void LoadComplete() @@ -160,16 +166,33 @@ namespace osu.Game.Rulesets.Objects.Drawables /// Applies a new to be represented by this . /// /// The to apply. - public void Apply(HitObject hitObject) + /// The controlling the lifetime of . + public void Apply([NotNull] HitObject hitObject, [CanBeNull] HitObjectLifetimeEntry lifetimeEntry) { free(); HitObject = hitObject ?? throw new InvalidOperationException($"Cannot apply a null {nameof(HitObject)}."); + this.lifetimeEntry = lifetimeEntry; + + if (lifetimeEntry != null) + { + // Transfer lifetime from the entry. + LifetimeStart = lifetimeEntry.LifetimeStart; + LifetimeEnd = lifetimeEntry.LifetimeEnd; + + // Copy any existing result from the entry (required for rewind / judgement revert). + Result = lifetimeEntry.Result; + } + // Ensure this DHO has a result. Result ??= CreateResult(HitObject.CreateJudgement()) ?? throw new InvalidOperationException($"{GetType().ReadableName()} must provide a {nameof(JudgementResult)} through {nameof(CreateResult)}."); + // Copy back the result to the entry for potential future retrieval. + if (lifetimeEntry != null) + lifetimeEntry.Result = Result; + foreach (var h in HitObject.NestedHitObjects) { var drawableNested = CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); @@ -302,7 +325,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onDefaultsApplied(HitObject hitObject) { - Apply(hitObject); + Apply(hitObject, lifetimeEntry); DefaultsApplied?.Invoke(this); } @@ -549,15 +572,27 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected internal new ScheduledDelegate Schedule(Action action) => base.Schedule(action); - private double? lifetimeStart; - public override double LifetimeStart { - get => lifetimeStart ?? (HitObject.StartTime - InitialLifetimeOffset); - set + get => base.LifetimeStart; + set => setLifetime(value, LifetimeEnd); + } + + public override double LifetimeEnd + { + get => base.LifetimeEnd; + set => setLifetime(LifetimeStart, value); + } + + private void setLifetime(double lifetimeStart, double lifetimeEnd) + { + base.LifetimeStart = lifetimeStart; + base.LifetimeEnd = lifetimeEnd; + + if (lifetimeEntry != null) { - lifetimeStart = value; - base.LifetimeStart = value; + lifetimeEntry.LifetimeStart = lifetimeStart; + lifetimeEntry.LifetimeEnd = lifetimeEnd; } } diff --git a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs index f134c66274..1954d7e6d2 100644 --- a/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.cs +++ b/osu.Game/Rulesets/Objects/HitObjectLifetimeEntry.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.Bindables; using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Objects @@ -16,6 +18,14 @@ namespace osu.Game.Rulesets.Objects /// public readonly HitObject HitObject; + /// + /// The result that was judged with. + /// This is set by the accompanying , and reused when required for rewinding. + /// + internal JudgementResult Result; + + private readonly IBindable startTimeBindable = new BindableDouble(); + /// /// Creates a new . /// @@ -23,7 +33,9 @@ namespace osu.Game.Rulesets.Objects public HitObjectLifetimeEntry(HitObject hitObject) { HitObject = hitObject; - ResetLifetimeStart(); + + startTimeBindable.BindTo(HitObject.StartTimeBindable); + startTimeBindable.BindValueChanged(onStartTimeChanged, true); } // The lifetime start, as set by the hitobject. @@ -91,8 +103,8 @@ namespace osu.Game.Rulesets.Objects protected virtual double InitialLifetimeOffset => 10000; /// - /// Resets according to the start time of the . + /// Resets according to the change in start time of the . /// - internal void ResetLifetimeStart() => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; + private void onStartTimeChanged(ValueChangedEvent startTime) => LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; } } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 8c6db661b5..33c422adb8 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -16,6 +16,7 @@ using System.Threading; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Pooling; using osu.Framework.Input; @@ -246,6 +247,16 @@ namespace osu.Game.Rulesets.UI Playfield.Add(drawableObject); } + protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) + { + if (!(hitObject is TObject tHitObject)) + throw new InvalidOperationException($"Unexpected hitobject type: {hitObject.GetType().ReadableName()}"); + + return CreateLifetimeEntry(tHitObject); + } + + protected virtual HitObjectLifetimeEntry CreateLifetimeEntry(TObject hitObject) => new HitObjectLifetimeEntry(hitObject); + public override void SetRecordTarget(Replay recordingReplay) { if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager)) @@ -564,9 +575,21 @@ namespace osu.Game.Rulesets.UI m.ApplyToDrawableHitObjects(dho.Yield()); } - dho.Apply(hitObject); + dho.Apply(hitObject, GetLifetimeEntry(hitObject)); }); } + + protected abstract HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject); + + private readonly Dictionary lifetimeEntries = new Dictionary(); + + protected HitObjectLifetimeEntry GetLifetimeEntry(HitObject hitObject) + { + if (lifetimeEntries.TryGetValue(hitObject, out var entry)) + return entry; + + return lifetimeEntries[hitObject] = CreateLifetimeEntry(hitObject); + } } public class BeatmapInvalidForRulesetException : ArgumentException From ce837eaba0d6f6dc261614ddb66183b1bf56f4a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 12:20:26 +0100 Subject: [PATCH 4519/6909] Rename variables --- .../Storyboards/Drawables/DrawableStoryboardAnimation.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index a78d0bf4d7..644bb28457 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -115,11 +115,11 @@ namespace osu.Game.Storyboards.Drawables [BackgroundDependencyLoader] private void load(TextureStore textureStore, Storyboard storyboard) { - for (int frame = 0; frame < Animation.FrameCount; frame++) + for (int frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++) { - string framePath = Animation.Path.Replace(".", frame + "."); - Drawable sprite = storyboard.CreateSpriteFromResourcePath(framePath, textureStore) ?? Drawable.Empty(); - AddFrame(sprite, Animation.FrameDelay); + string framePath = Animation.Path.Replace(".", frameIndex + "."); + Drawable frame = storyboard.CreateSpriteFromResourcePath(framePath, textureStore) ?? Drawable.Empty(); + AddFrame(frame, Animation.FrameDelay); } Animation.ApplyTransforms(this); From 61093030ee4b66e8d22c58ed0a6cf4917f2b80eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 12:20:49 +0100 Subject: [PATCH 4520/6909] Remove redundant class name qualifier --- osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 644bb28457..7eac994e07 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -118,7 +118,7 @@ namespace osu.Game.Storyboards.Drawables for (int frameIndex = 0; frameIndex < Animation.FrameCount; frameIndex++) { string framePath = Animation.Path.Replace(".", frameIndex + "."); - Drawable frame = storyboard.CreateSpriteFromResourcePath(framePath, textureStore) ?? Drawable.Empty(); + Drawable frame = storyboard.CreateSpriteFromResourcePath(framePath, textureStore) ?? Empty(); AddFrame(frame, Animation.FrameDelay); } From e1dcac4d8ba4feb73c0fc2088edccc368d2698f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 10 Nov 2020 20:29:29 +0900 Subject: [PATCH 4521/6909] 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 e3285222f8..bbe8426316 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 832722c729..8f0cc58594 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index ad6dd2a0b5..f766e0ec03 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From f24569694716cbbbd92f2470680c4469f5b326bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 13:24:32 +0100 Subject: [PATCH 4522/6909] Move {-> Default}KiaiHitExplosion --- .../UI/{KiaiHitExplosion.cs => DefaultKiaiHitExplosion.cs} | 4 ++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename osu.Game.Rulesets.Taiko/UI/{KiaiHitExplosion.cs => DefaultKiaiHitExplosion.cs} (92%) diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs similarity index 92% rename from osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs rename to osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs index 067d390894..32c9f3ec4f 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs @@ -13,14 +13,14 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.UI { - public class KiaiHitExplosion : CircularContainer + public class DefaultKiaiHitExplosion : CircularContainer { public override bool RemoveWhenNotAlive => true; public readonly DrawableHitObject JudgedObject; private readonly HitType type; - public KiaiHitExplosion(DrawableHitObject judgedObject, HitType type) + public DefaultKiaiHitExplosion(DrawableHitObject judgedObject, HitType type) { JudgedObject = judgedObject; this.type = type; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 120cf264c3..03895dfd68 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI public const float DEFAULT_HEIGHT = 178; private Container hitExplosionContainer; - private Container kiaiExplosionContainer; + private Container kiaiExplosionContainer; private JudgementContainer judgementContainer; private ScrollingHitObjectContainer drumRollHitContainer; internal Drawable HitTarget; @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.UI drumRollHitContainer = new DrumRollHitContainer() } }, - kiaiExplosionContainer = new Container + kiaiExplosionContainer = new Container { Name = "Kiai hit explosions", RelativeSizeAxes = Axes.Both, @@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Taiko.UI { hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) - kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); + kiaiExplosionContainer.Add(new DefaultKiaiHitExplosion(drawableObject, type)); } private class ProxyContainer : LifetimeManagementContainer From 4ea823e4dc85365822fcde88e4d8175ce1a6532d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 22:02:33 +0900 Subject: [PATCH 4523/6909] Fix test failures --- osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index 4eeb4a1475..fb0917341e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.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.Allocation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Scoring; @@ -28,9 +27,10 @@ namespace osu.Game.Rulesets.Taiko.Tests // suppress locally to allow hiding the visuals wherever necessary. } - [BackgroundDependencyLoader] - private void load() + protected override void LoadComplete() { + base.LoadComplete(); + Result.Type = Type; } From ed01d37966a9249bb4f81d81f03a2c01746ec374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 13:35:49 +0100 Subject: [PATCH 4524/6909] Reintroduce KiaiHitExplosion as skinnable --- .../TaikoSkinComponents.cs | 1 + .../UI/DefaultKiaiHitExplosion.cs | 9 +--- .../UI/KiaiHitExplosion.cs | 47 +++++++++++++++++++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 6 +-- 4 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 132d8f8868..bf48898dd2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko TaikoExplosionMiss, TaikoExplosionOk, TaikoExplosionGreat, + TaikoExplosionKiai, Scroller, Mascot, } diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs index 32c9f3ec4f..7ce8b016d5 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs @@ -8,7 +8,6 @@ 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.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.UI @@ -17,19 +16,13 @@ namespace osu.Game.Rulesets.Taiko.UI { public override bool RemoveWhenNotAlive => true; - public readonly DrawableHitObject JudgedObject; private readonly HitType type; - public DefaultKiaiHitExplosion(DrawableHitObject judgedObject, HitType type) + public DefaultKiaiHitExplosion(HitType type) { - JudgedObject = judgedObject; this.type = type; - Anchor = Anchor.CentreLeft; - Origin = Anchor.Centre; - RelativeSizeAxes = Axes.Both; - Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1); Blending = BlendingParameters.Additive; diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs new file mode 100644 index 0000000000..20900a9352 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public class KiaiHitExplosion : Container + { + public override bool RemoveWhenNotAlive => true; + + [Cached(typeof(DrawableHitObject))] + public readonly DrawableHitObject JudgedObject; + + private readonly HitType hitType; + + private SkinnableDrawable skinnable; + + public override double LifetimeStart => skinnable.Drawable.LifetimeStart; + + public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd; + + public KiaiHitExplosion(DrawableHitObject judgedObject, HitType hitType) + { + JudgedObject = judgedObject; + this.hitType = hitType; + + Anchor = Anchor.CentreLeft; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1); + } + + [BackgroundDependencyLoader] + private void load() + { + Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoExplosionKiai), _ => new DefaultKiaiHitExplosion(hitType)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 03895dfd68..120cf264c3 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI public const float DEFAULT_HEIGHT = 178; private Container hitExplosionContainer; - private Container kiaiExplosionContainer; + private Container kiaiExplosionContainer; private JudgementContainer judgementContainer; private ScrollingHitObjectContainer drumRollHitContainer; internal Drawable HitTarget; @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.UI drumRollHitContainer = new DrumRollHitContainer() } }, - kiaiExplosionContainer = new Container + kiaiExplosionContainer = new Container { Name = "Kiai hit explosions", RelativeSizeAxes = Axes.Both, @@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Taiko.UI { hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) - kiaiExplosionContainer.Add(new DefaultKiaiHitExplosion(drawableObject, type)); + kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); } private class ProxyContainer : LifetimeManagementContainer From 35763a74fd75c18897d7236b4f58f2218c80135e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 14:07:39 +0100 Subject: [PATCH 4525/6909] Add test scene --- .../Skinning/TestSceneKiaiHitExplosion.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs new file mode 100644 index 0000000000..b558709592 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + [TestFixture] + public class TestSceneKiaiHitExplosion : TaikoSkinnableTestScene + { + [Test] + public void TestKiaiHits() + { + AddStep("rim hit", () => SetContents(() => getContentFor(createHit(HitType.Rim)))); + AddStep("centre hit", () => SetContents(() => getContentFor(createHit(HitType.Centre)))); + } + + private Drawable getContentFor(DrawableTestHit hit) + { + return new Container + { + RelativeSizeAxes = Axes.Both, + Child = new KiaiHitExplosion(hit, hit.HitObject.Type) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + private DrawableTestHit createHit(HitType type) => new DrawableTestHit(new Hit { StartTime = Time.Current, Type = type }); + } +} From 0387d994bdc6136259d32c3b2181842a6e91f8ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 14:08:27 +0100 Subject: [PATCH 4526/6909] Do not lookup default kiai explosion if skin has own --- .../Skinning/TaikoLegacySkinTransformer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index c88480d18f..ddbf20b827 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -114,6 +114,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; + case TaikoSkinComponents.TaikoExplosionKiai: + // suppress the default kiai explosion if the skin brings its own sprites. + if (hasExplosion.Value) + return Drawable.Empty(); + + return null; + case TaikoSkinComponents.Scroller: if (GetTexture("taiko-slider") != null) return new LegacyTaikoScroller(); From 31e4d71852e34f6daa15fbf9d180462ab232f8c3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 22:49:02 +0900 Subject: [PATCH 4527/6909] Rewrite HitObjectContainer with pooling support --- .../Objects/Drawables/DrawableHitObject.cs | 8 +- osu.Game/Rulesets/UI/HitObjectContainer.cs | 213 ++++++++++++++---- 2 files changed, 168 insertions(+), 53 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 2ac478f640..5299e53d5c 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual float SamplePlaybackPosition => 0.5f; - private readonly Bindable startTimeBindable = new Bindable(); + public readonly Bindable StartTimeBindable = new Bindable(); private readonly BindableList samplesBindable = new BindableList(); private readonly Bindable userPositionalHitSounds = new Bindable(); private readonly Bindable comboIndexBindable = new Bindable(); @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.LoadComplete(); - startTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); + StartTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); updateState(ArmedState.Idle, true); @@ -205,7 +205,7 @@ namespace osu.Game.Rulesets.Objects.Drawables AddNestedHitObject(drawableNested); } - startTimeBindable.BindTo(HitObject.StartTimeBindable); + StartTimeBindable.BindTo(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) comboIndexBindable.BindTo(combo.ComboIndexBindable); @@ -231,7 +231,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (!hasHitObjectApplied) return; - startTimeBindable.UnbindFrom(HitObject.StartTimeBindable); + StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 4cadfa9ad4..97604f62c8 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -1,35 +1,132 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.UI { public class HitObjectContainer : LifetimeManagementContainer { - public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); + /// + /// All currently in-use s. + /// + public IEnumerable Objects => InternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); - private readonly Dictionary bindable, double timeAtAdd)> startTimeMap = new Dictionary, double)>(); + /// + /// All currently in-use s that are alive. + /// + /// + /// If this uses pooled objects, this is equivalent to . + /// + public IEnumerable AliveObjects => AliveInternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); + + public event Action NewResult; + public event Action RevertResult; + + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + public event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + public event Action HitObjectUsageFinished; + + /// + /// The amount of time prior to the current time within which s should be considered alive. + /// + public double PastLifetimeExtension { get; set; } + + /// + /// The amount of time after the current time within which s should be considered alive. + /// + public double FutureLifetimeExtension { get; set; } + + private readonly Dictionary startTimeMap = new Dictionary(); + private readonly Dictionary drawableMap = new Dictionary(); + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); + + [Resolved(CanBeNull = true)] + private DrawableRuleset drawableRuleset { get; set; } public HitObjectContainer() { RelativeSizeAxes = Axes.Both; + + lifetimeManager.EntryBecameAlive += entryBecameAlive; + lifetimeManager.EntryBecameDead += entryBecameDead; } + #region Pooling support + + public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry); + + public void Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry); + + private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry); + + private void entryBecameDead(LifetimeEntry entry) => removeDrawable((HitObjectLifetimeEntry)entry); + + private void addDrawable(HitObjectLifetimeEntry entry) + { + Debug.Assert(!drawableMap.ContainsKey(entry)); + + var drawable = drawableRuleset.GetDrawableRepresentation(entry.HitObject); + drawable.OnNewResult += onNewResult; + drawable.OnRevertResult += onRevertResult; + + bindStartTime(drawable); + AddInternal(drawableMap[entry] = drawable, false); + + HitObjectUsageBegan?.Invoke(entry.HitObject); + } + + private void removeDrawable(HitObjectLifetimeEntry entry) + { + Debug.Assert(drawableMap.ContainsKey(entry)); + + var drawable = drawableMap[entry]; + drawable.OnNewResult -= onNewResult; + drawable.OnRevertResult -= onRevertResult; + drawable.OnKilled(); + + drawableMap.Remove(entry); + + unbindStartTime(drawable); + RemoveInternal(drawable); + + HitObjectUsageFinished?.Invoke(entry.HitObject); + } + + #endregion + + #region Non-pooling support + public virtual void Add(DrawableHitObject hitObject) { - // Added first for the comparer to remain ordered during AddInternal - startTimeMap[hitObject] = (hitObject.HitObject.StartTimeBindable.GetBoundCopy(), hitObject.HitObject.StartTime); - startTimeMap[hitObject].bindable.BindValueChanged(_ => onStartTimeChanged(hitObject)); - + bindStartTime(hitObject); AddInternal(hitObject); + + hitObject.OnNewResult += onNewResult; + hitObject.OnRevertResult += onRevertResult; } public virtual bool Remove(DrawableHitObject hitObject) @@ -37,54 +134,16 @@ namespace osu.Game.Rulesets.UI if (!RemoveInternal(hitObject)) return false; - // Removed last for the comparer to remain ordered during RemoveInternal - startTimeMap[hitObject].bindable.UnbindAll(); - startTimeMap.Remove(hitObject); + hitObject.OnNewResult -= onNewResult; + hitObject.OnRevertResult -= onRevertResult; + + unbindStartTime(hitObject); return true; } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - unbindStartTimeMap(); - } - - public virtual void Clear(bool disposeChildren = true) - { - ClearInternal(disposeChildren); - unbindStartTimeMap(); - } - - private void unbindStartTimeMap() - { - foreach (var kvp in startTimeMap) - kvp.Value.bindable.UnbindAll(); - startTimeMap.Clear(); - } - public int IndexOf(DrawableHitObject hitObject) => IndexOfInternal(hitObject); - private void onStartTimeChanged(DrawableHitObject hitObject) - { - if (!RemoveInternal(hitObject)) - return; - - // Update the stored time, preserving the existing bindable - startTimeMap[hitObject] = (startTimeMap[hitObject].bindable, hitObject.HitObject.StartTime); - AddInternal(hitObject); - } - - protected override int Compare(Drawable x, Drawable y) - { - if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) - return base.Compare(x, y); - - // Put earlier hitobjects towards the end of the list, so they handle input first - int i = startTimeMap[yObj].timeAtAdd.CompareTo(startTimeMap[xObj].timeAtAdd); - return i == 0 ? CompareReverseChildID(x, y) : i; - } - protected override void OnChildLifetimeBoundaryCrossed(LifetimeBoundaryCrossedEvent e) { if (!(e.Child is DrawableHitObject hitObject)) @@ -96,5 +155,61 @@ namespace osu.Game.Rulesets.UI hitObject.OnKilled(); } } + + #endregion + + public virtual void Clear(bool disposeChildren = true) + { + lifetimeManager.ClearEntries(); + + ClearInternal(disposeChildren); + unbindAllStartTimes(); + } + + protected override bool CheckChildrenLife() => base.CheckChildrenLife() | lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + + private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r); + private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r); + + #region Comparator + StartTime tracking + + private void bindStartTime(DrawableHitObject hitObject) + { + var bindable = hitObject.StartTimeBindable.GetBoundCopy(); + bindable.BindValueChanged(_ => SortInternal()); + + startTimeMap[hitObject] = bindable; + } + + private void unbindStartTime(DrawableHitObject hitObject) + { + startTimeMap[hitObject].UnbindAll(); + startTimeMap.Remove(hitObject); + } + + private void unbindAllStartTimes() + { + foreach (var kvp in startTimeMap) + kvp.Value.UnbindAll(); + startTimeMap.Clear(); + } + + protected override int Compare(Drawable x, Drawable y) + { + if (!(x is DrawableHitObject xObj) || !(y is DrawableHitObject yObj)) + return base.Compare(x, y); + + // Put earlier hitobjects towards the end of the list, so they handle input first + int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); + return i == 0 ? CompareReverseChildID(x, y) : i; + } + + #endregion + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + unbindAllStartTimes(); + } } } From be4735cd2ba8f37e853d9a09796d1248cbddc8d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 14:50:19 +0100 Subject: [PATCH 4528/6909] Explicitly set lifetime to ensure empty drawables are cleaned up --- .../Skinning/TaikoLegacySkinTransformer.cs | 3 ++- osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index ddbf20b827..880af3fbd8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -116,8 +116,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.TaikoExplosionKiai: // suppress the default kiai explosion if the skin brings its own sprites. + // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. if (hasExplosion.Value) - return Drawable.Empty(); + return KiaiHitExplosion.EmptyExplosion(); return null; diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs index 20900a9352..326cb23897 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -43,5 +43,11 @@ namespace osu.Game.Rulesets.Taiko.UI { Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoExplosionKiai), _ => new DefaultKiaiHitExplosion(hitType)); } + + /// + /// Helper function to use when an explosion is not desired. + /// Lifetime is set to avoid accumulating empty drawables in the parent container. + /// + public static Drawable EmptyExplosion() => Empty().With(d => d.LifetimeEnd = double.MinValue); } } From 66213f2ed0903dd21809943a10e04aca3f2bf069 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 10 Nov 2020 23:32:30 +0900 Subject: [PATCH 4529/6909] Add pooling support to DrawableRuleset + Playfield --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 58 ++++----- osu.Game/Rulesets/UI/HitObjectContainer.cs | 9 +- osu.Game/Rulesets/UI/Playfield.cs | 136 ++++++++++++++++++++- osu.Game/Screens/Play/Player.cs | 4 +- 4 files changed, 173 insertions(+), 34 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 33c422adb8..87a04312ab 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -41,9 +41,8 @@ namespace osu.Game.Rulesets.UI public abstract class DrawableRuleset : DrawableRuleset, IProvideCursor, ICanAttachKeyCounter where TObject : HitObject { - public override event Action OnNewResult; - - public override event Action OnRevertResult; + public override event Action NewResult; + public override event Action RevertResult; /// /// The selected variant. @@ -125,7 +124,11 @@ namespace osu.Game.Rulesets.UI RelativeSizeAxes = Axes.Both; KeyBindingInputManager = CreateInputManager(); - playfield = new Lazy(CreatePlayfield); + playfield = new Lazy(() => CreatePlayfield().With(p => + { + p.NewResult += (_, r) => NewResult?.Invoke(r); + p.RevertResult += (_, r) => RevertResult?.Invoke(r); + })); IsPaused.ValueChanged += paused => { @@ -183,7 +186,7 @@ namespace osu.Game.Rulesets.UI RegenerateAutoplay(); - loadObjects(cancellationToken); + loadObjects(cancellationToken ?? default); } public void RegenerateAutoplay() @@ -196,15 +199,15 @@ namespace osu.Game.Rulesets.UI /// /// Creates and adds drawable representations of hit objects to the play field. /// - private void loadObjects(CancellationToken? cancellationToken) + private void loadObjects(CancellationToken cancellationToken) { foreach (TObject h in Beatmap.HitObjects) { - cancellationToken?.ThrowIfCancellationRequested(); - addHitObject(h); + cancellationToken.ThrowIfCancellationRequested(); + AddHitObject(h); } - cancellationToken?.ThrowIfCancellationRequested(); + cancellationToken.ThrowIfCancellationRequested(); Playfield.PostProcess(); @@ -230,21 +233,24 @@ namespace osu.Game.Rulesets.UI ResumeOverlay?.Hide(); } - /// - /// Creates and adds the visual representation of a to this . - /// - /// The to add the visual representation for. - private void addHitObject(TObject hitObject) + public void AddHitObject(TObject hitObject) { - var drawableObject = CreateDrawableRepresentation(hitObject); + if (PoolHitObjects) + Playfield.Add(GetLifetimeEntry(hitObject)); + else + Playfield.Add(CreateDrawableRepresentation(hitObject)); + } - if (drawableObject == null) - return; - - drawableObject.OnNewResult += (_, r) => OnNewResult?.Invoke(r); - drawableObject.OnRevertResult += (_, r) => OnRevertResult?.Invoke(r); - - Playfield.Add(drawableObject); + public void RemoveHitObject(TObject hitObject) + { + if (PoolHitObjects) + Playfield.Remove(GetLifetimeEntry(hitObject)); + else + { + var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject); + if (drawableObject != null) + Playfield.Remove(drawableObject); + } } protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) @@ -382,12 +388,12 @@ namespace osu.Game.Rulesets.UI /// /// Invoked when a has been applied by a . /// - public abstract event Action OnNewResult; + public abstract event Action NewResult; /// /// Invoked when a is being reverted by a . /// - public abstract event Action OnRevertResult; + public abstract event Action RevertResult; /// /// Whether a replay is currently loaded. @@ -524,10 +530,6 @@ namespace osu.Game.Rulesets.UI /// /// /// Pools must be registered with this via in order for s to be retrieved. - /// - /// If true, hitobjects will be added to the via . - /// If false, will be used instead. - /// /// protected virtual bool PoolHitObjects => false; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 97604f62c8..3a5e0b64ed 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -31,7 +31,14 @@ namespace osu.Game.Rulesets.UI /// public IEnumerable AliveObjects => AliveInternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); + /// + /// Invoked when a is judged. + /// public event Action NewResult; + + /// + /// Invoked when a judgement is reverted. + /// public event Action RevertResult; /// @@ -79,7 +86,7 @@ namespace osu.Game.Rulesets.UI public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry); - public void Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry); + public bool Remove(HitObjectLifetimeEntry entry) => lifetimeManager.RemoveEntry(entry); private void entryBecameAlive(LifetimeEntry entry) => addDrawable((HitObjectLifetimeEntry)entry); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index d92ba210db..2bd2bb9e06 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -10,13 +10,41 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.UI { public abstract class Playfield : CompositeDrawable { + /// + /// Invoked when a is judged. + /// + public event Action NewResult; + + /// + /// Invoked when a judgement is reverted. + /// + public event Action RevertResult; + + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + public event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + public event Action HitObjectUsageFinished; + /// /// The contained in this Playfield. /// @@ -72,7 +100,13 @@ namespace osu.Game.Rulesets.UI { RelativeSizeAxes = Axes.Both; - hitObjectContainerLazy = new Lazy(CreateHitObjectContainer); + hitObjectContainerLazy = new Lazy(() => CreateHitObjectContainer().With(h => + { + h.NewResult += (d, r) => NewResult?.Invoke(d, r); + h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); + h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); + })); } [Resolved(CanBeNull = true)] @@ -101,13 +135,103 @@ namespace osu.Game.Rulesets.UI /// Adds a DrawableHitObject to this Playfield. /// /// The DrawableHitObject to add. - public virtual void Add(DrawableHitObject h) => HitObjectContainer.Add(h); + public virtual void Add(DrawableHitObject h) + { + HitObjectContainer.Add(h); + + h.OnNewResult += (d, r) => NewResult?.Invoke(d, r); + h.OnRevertResult += (d, r) => RevertResult?.Invoke(d, r); + + OnHitObjectAdded(h.HitObject); + } /// /// Remove a DrawableHitObject from this Playfield. /// /// The DrawableHitObject to remove. - public virtual bool Remove(DrawableHitObject h) => HitObjectContainer.Remove(h); + public virtual bool Remove(DrawableHitObject h) + { + if (!HitObjectContainer.Remove(h)) + return false; + + OnHitObjectRemoved(h.HitObject); + return false; + } + + private readonly Dictionary lifetimeEntryMap = new Dictionary(); + + /// + /// Adds a to this . + /// + /// The controlling the lifetime of the . + public void Add(HitObjectLifetimeEntry entry) + { + HitObjectContainer.Add(entry); + lifetimeEntryMap[entry.HitObject] = entry; + OnHitObjectAdded(entry.HitObject); + } + + /// + /// Removes a to this . + /// + /// The controlling the lifetime of the . + public void Remove(HitObjectLifetimeEntry entry) + { + if (HitObjectContainer.Remove(entry)) + OnHitObjectRemoved(entry.HitObject); + lifetimeEntryMap.Remove(entry.HitObject); + } + + /// + /// Invoked when a is added to this . + /// + /// The added . + protected virtual void OnHitObjectAdded(HitObject hitObject) + { + } + + /// + /// Invoked when a is removed from this . + /// + /// The removed . + protected virtual void OnHitObjectRemoved(HitObject hitObject) + { + } + + /// + /// Sets whether to keep a given always alive within this or any nested . + /// + /// The to set. + /// Whether to keep always alive. + public void SetKeepAlive(HitObject hitObject, bool keepAlive) + { + if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) + { + entry.KeepAlive = keepAlive; + return; + } + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.SetKeepAlive(hitObject, keepAlive); + } + + /// + /// Keeps all s alive within this and all nested s. + /// + public void KeepAllAlive() + { + foreach (var (_, entry) in lifetimeEntryMap) + entry.KeepAlive = true; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.KeepAllAlive(); + } /// /// The cursor currently being used by this . May be null if no cursor is provided. @@ -131,6 +255,12 @@ namespace osu.Game.Rulesets.UI protected void AddNested(Playfield otherPlayfield) { otherPlayfield.DisplayJudgements.BindTo(DisplayJudgements); + + otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); + otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h); + otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h); + nestedPlayfields.Value.Add(otherPlayfield); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f9af1818d0..ee4f835c6f 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -261,14 +261,14 @@ namespace osu.Game.Screens.Play // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); - DrawableRuleset.OnNewResult += r => + DrawableRuleset.NewResult += r => { HealthProcessor.ApplyResult(r); ScoreProcessor.ApplyResult(r); gameplayBeatmap.ApplyResult(r); }; - DrawableRuleset.OnRevertResult += r => + DrawableRuleset.RevertResult += r => { HealthProcessor.RevertResult(r); ScoreProcessor.RevertResult(r); From 99e5450af3930eba7f9431179c8afafe45686a0d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 00:22:36 +0900 Subject: [PATCH 4530/6909] Cache DrawableRuleset --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 87a04312ab..b78cfe9086 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -383,6 +383,7 @@ namespace osu.Game.Rulesets.UI /// Once IDrawable is a thing, this can also become an interface. /// /// + [Cached(typeof(DrawableRuleset))] public abstract class DrawableRuleset : CompositeDrawable { /// From e525784cb2deb8287e3727d3ebc6ed2e639a635a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 00:24:14 +0900 Subject: [PATCH 4531/6909] Clear lifetimeEntry after use --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 5299e53d5c..244cf831c3 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -259,6 +259,8 @@ namespace osu.Game.Rulesets.Objects.Drawables OnFree(HitObject); HitObject = null; + lifetimeEntry = null; + hasHitObjectApplied = false; } From b725c9cce9e938063a64ee2a5709ce5859c8e59e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 00:24:28 +0900 Subject: [PATCH 4532/6909] Fix possible nullrefs --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 244cf831c3..bcf1103f39 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -613,13 +613,13 @@ namespace osu.Game.Rulesets.Objects.Drawables /// The time at which state transforms should be applied that line up to 's StartTime. /// This is used to offset calls to . /// - public double StateUpdateTime => HitObject.StartTime; + public double StateUpdateTime => HitObject?.StartTime ?? 0; /// /// The time at which judgement dependent state transforms should be applied. This is equivalent of the (end) time of the object, in addition to any judgement offset. /// This is used to offset calls to . /// - public double HitStateUpdateTime => Result?.TimeAbsolute ?? HitObject.GetEndTime(); + public double HitStateUpdateTime => Result?.TimeAbsolute ?? HitObject?.GetEndTime() ?? 0; /// /// Will be called at least once after this has become not alive. From 81844878b0e6220a55bd5cf0d496571bda893097 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 00:29:34 +0900 Subject: [PATCH 4533/6909] Fix possible nullref with non-attached hitobjects --- .../Objects/Drawables/Pieces/SnakingSliderBody.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs index e63f25b7bc..8835a0d84a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs @@ -63,7 +63,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public void UpdateProgress(double completionProgress) { - if (drawableSlider == null) + if (drawableSlider?.HitObject == null) return; Slider slider = drawableSlider.HitObject; @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public void Refresh() { - if (drawableSlider == null) + if (drawableSlider?.HitObject == null) return; // Generate the entire curve From 546249b071a21fbea8beee7b92b3f7024585ba87 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 21:32:47 +0100 Subject: [PATCH 4534/6909] Temporarily fix crash on deleting control point groups --- osu.Game/Screens/Edit/Timing/GroupSection.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index d76b5e7406..2605ea8b75 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -85,12 +85,13 @@ namespace osu.Game.Screens.Edit.Timing { textBox.Text = string.Empty; - textBox.Current.Disabled = true; + // cannot use textBox.Current.Disabled due to https://github.com/ppy/osu-framework/issues/3919 + textBox.ReadOnly = true; button.Enabled.Value = false; return; } - textBox.Current.Disabled = false; + textBox.ReadOnly = false; button.Enabled.Value = true; textBox.Text = $"{group.NewValue.Time:n0}"; From 53c968e137488ba4719f5241d0edb3d32940ece3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 10:19:59 +0900 Subject: [PATCH 4535/6909] Fix user profile best performance weighting being out of order --- .../Profile/Sections/Ranks/PaginatedScoreContainer.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 3c540d6fbb..1ce3079d52 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -46,6 +46,9 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks protected override void OnItemsReceived(List items) { + if (VisiblePages == 0) + drawableItemIndex = 0; + base.OnItemsReceived(items); if (type == ScoreType.Recent) @@ -55,6 +58,8 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks protected override APIRequest> CreateRequest() => new GetUserScoresRequest(User.Value.Id, type, VisiblePages++, ItemsPerPage); + private int drawableItemIndex; + protected override Drawable CreateDrawableItem(APILegacyScoreInfo model) { switch (type) @@ -63,7 +68,7 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks return new DrawableProfileScore(model.CreateScoreInfo(Rulesets)); case ScoreType.Best: - return new DrawableProfileWeightedScore(model.CreateScoreInfo(Rulesets), Math.Pow(0.95, ItemsContainer.Count)); + return new DrawableProfileWeightedScore(model.CreateScoreInfo(Rulesets), Math.Pow(0.95, drawableItemIndex++)); } } } From 6b548ef5e4a6517e8ac5ae0689d7827c87cbfab4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 11:54:40 +0900 Subject: [PATCH 4536/6909] Split out SkinInfo ID constants and fix random logic --- .../Overlays/Settings/Sections/SkinSection.cs | 49 ++++++++++--------- osu.Game/Skinning/DefaultLegacySkin.cs | 2 +- osu.Game/Skinning/SkinInfo.cs | 5 ++ 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 412103af38..b21de26dde 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -31,9 +31,13 @@ namespace osu.Game.Overlays.Settings.Sections private readonly Bindable dropdownBindable = new Bindable { Default = SkinInfo.Default }; private readonly Bindable configBindable = new Bindable(); - private static readonly SkinInfo random_skin_info = new RandomSkinInfo(); + private static readonly SkinInfo random_skin_info = new SkinInfo + { + ID = SkinInfo.RANDOM_SKIN, + Name = "", + }; - private List usableSkins; + private List skinItems; [Resolved] private SkinManager skins { get; set; } @@ -98,29 +102,37 @@ namespace osu.Game.Overlays.Settings.Sections dropdownBindable.BindValueChanged(skin => { if (skin.NewValue == random_skin_info) + { randomizeSkin(); - else - configBindable.Value = skin.NewValue.ID; + return; + } + + configBindable.Value = skin.NewValue.ID; }); } private void randomizeSkin() { - int n = usableSkins.Count; - if (n > 1) - configBindable.Value = (configBindable.Value + RNG.Next(n - 1) + 1) % n; // make sure it's always a different one - else - configBindable.Value = 0; + int count = skinItems.Count - 1; // exclude "random" item. + + if (count <= 1) + { + configBindable.Value = SkinInfo.Default.ID; + return; + } + + // ensure the random selection is never the same as the previous. + configBindable.Value = skinItems.Where(s => s.ID != configBindable.Value).ElementAt(RNG.Next(0, count)).ID; } private void updateItems() { - usableSkins = skins.GetAllUsableSkins(); + skinItems = skins.GetAllUsableSkins(); - if (usableSkins.Count > 1) - usableSkins.Add(random_skin_info); + if (skinItems.Count > 1) + skinItems.Add(random_skin_info); - skinDropdown.Items = usableSkins; + skinDropdown.Items = skinItems; } private void itemUpdated(ValueChangedEvent> weakItem) @@ -150,17 +162,6 @@ namespace osu.Game.Overlays.Settings.Sections } } - private class RandomSkinInfo : SkinInfo - { - public RandomSkinInfo() - { - Name = ""; - ID = -1; - } - - public override string ToString() => Name; - } - private class ExportSkinButton : SettingsButton { [Resolved] diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 78d3a37f7c..2758a4cbba 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.cs @@ -25,7 +25,7 @@ namespace osu.Game.Skinning public static SkinInfo Info { get; } = new SkinInfo { - ID = -1, // this is temporary until database storage is decided upon. + ID = SkinInfo.CLASSIC_SKIN, // this is temporary until database storage is decided upon. Name = "osu!classic", Creator = "team osu!" }; diff --git a/osu.Game/Skinning/SkinInfo.cs b/osu.Game/Skinning/SkinInfo.cs index b9fe44ef3b..aaccbefb3d 100644 --- a/osu.Game/Skinning/SkinInfo.cs +++ b/osu.Game/Skinning/SkinInfo.cs @@ -10,6 +10,10 @@ namespace osu.Game.Skinning { public class SkinInfo : IHasFiles, IEquatable, IHasPrimaryKey, ISoftDelete { + internal const int DEFAULT_SKIN = 0; + internal const int CLASSIC_SKIN = -1; + internal const int RANDOM_SKIN = -2; + public int ID { get; set; } public string Name { get; set; } @@ -26,6 +30,7 @@ namespace osu.Game.Skinning public static SkinInfo Default { get; } = new SkinInfo { + ID = DEFAULT_SKIN, Name = "osu!lazer", Creator = "team osu!" }; From 6674628bc7383c1ae896afbce945237666a421d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 12:03:38 +0900 Subject: [PATCH 4537/6909] Only include user skins in random choices --- .../Overlays/Settings/Sections/SkinSection.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index b21de26dde..8297b56db8 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -113,24 +113,28 @@ namespace osu.Game.Overlays.Settings.Sections private void randomizeSkin() { - int count = skinItems.Count - 1; // exclude "random" item. + // choose from only user skins, removing the current selection to ensure a new one is chosen. + var randomChoices = skinItems.Where(s => s.ID > 0 && s.ID != configBindable.Value).ToArray(); - if (count <= 1) + if (randomChoices.Length == 0) { configBindable.Value = SkinInfo.Default.ID; return; } - // ensure the random selection is never the same as the previous. - configBindable.Value = skinItems.Where(s => s.ID != configBindable.Value).ElementAt(RNG.Next(0, count)).ID; + configBindable.Value = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)).ID; } private void updateItems() { skinItems = skins.GetAllUsableSkins(); - if (skinItems.Count > 1) - skinItems.Add(random_skin_info); + // insert after lazer built-in skins + int firstNonDefault = skinItems.FindIndex(s => s.ID > 0); + if (firstNonDefault < 0) + firstNonDefault = skinItems.Count; + + skinItems.Insert(firstNonDefault, random_skin_info); skinDropdown.Items = skinItems; } From 9caa56c64fcac23824932389327cf9945ff6c098 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 12:19:01 +0900 Subject: [PATCH 4538/6909] Display skin changes via on-screen display overlay --- osu.Game/Configuration/OsuConfigManager.cs | 8 ++++++++ osu.Game/OsuGame.cs | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 8206a92a54..b79e99781b 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 System; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; @@ -177,7 +178,14 @@ namespace osu.Game.Configuration new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled")), new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription())), new TrackedSetting(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())), + new TrackedSetting(OsuSetting.Skin, m => + { + string skinName = LookupSkinName?.Invoke(m) ?? string.Empty; + return new SettingDescription(skinName, "skin", skinName); + }) }; + + public Func LookupSkinName { get; set; } } public enum OsuSetting diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5119f262d5..a34272f5f0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -548,6 +548,10 @@ namespace osu.Game ScoreManager.GetStableStorage = GetStorageForStableInstall; ScoreManager.PresentImport = items => PresentScore(items.First()); + // make config aware of how to lookup skins for on-screen display purposes. + // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. + LocalConfig.LookupSkinName = id => SkinManager.GetAllUsableSkins().FirstOrDefault(s => s.ID == id)?.ToString() ?? "Unknown"; + Container logoContainer; BackButton.Receptor receptor; From 1173ef089099e2468b80cf33eeb8f9bf79bbcc80 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 12:37:00 +0900 Subject: [PATCH 4539/6909] Fix mania notelock crashing with overlapping hitwindows --- .../TestSceneHoldNoteInput.cs | 2 +- .../TestSceneOutOfOrderHits.cs | 23 +++++++++++++++++++ .../Objects/Drawables/DrawableHoldNote.cs | 2 +- .../UI/OrderedHitPolicy.cs | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 5cb1519196..6c9f184c2c 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests assertHeadJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); - assertNoteJudgement(HitResult.IgnoreHit); + assertNoteJudgement(HitResult.IgnoreMiss); } /// diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index e8c2472c3b..d699921307 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -54,6 +54,29 @@ namespace osu.Game.Rulesets.Mania.Tests } } + [Test] + public void TestMissAfterNextObjectStartTime() + { + var objects = new List + { + new HoldNote + { + StartTime = 1000, + EndTime = 1200, + }, + new HoldNote + { + StartTime = 1220, + EndTime = 1420 + } + }; + + performTest(objects, new List()); + + addJudgementAssert(objects[0], HitResult.IgnoreMiss); + addJudgementAssert(objects[1], HitResult.IgnoreMiss); + } + private void addJudgementAssert(ManiaHitObject hitObject, HitResult result) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index d9d740c145..3b3f72157a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); endHold(); } diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs index 0f9cd48dd8..9bc577a81e 100644 --- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI /// The that was hit. public void HandleHit(DrawableHitObject hitObject) { - if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) + if (hitObject.IsHit && !IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) From 626231d90626790a063f9b93256276e2b4d8135e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 12:53:32 +0900 Subject: [PATCH 4540/6909] Completely remove check as it can occur for hits too --- .../TestSceneOutOfOrderHits.cs | 51 +++++++++++++++++-- .../UI/OrderedHitPolicy.cs | 4 -- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index d699921307..86a142f2f6 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.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; @@ -55,19 +56,19 @@ namespace osu.Game.Rulesets.Mania.Tests } [Test] - public void TestMissAfterNextObjectStartTime() + public void TestHoldNoteMissAfterNextObjectStartTime() { var objects = new List { new HoldNote { StartTime = 1000, - EndTime = 1200, + EndTime = 1010, }, new HoldNote { - StartTime = 1220, - EndTime = 1420 + StartTime = 1020, + EndTime = 1030 } }; @@ -77,12 +78,54 @@ namespace osu.Game.Rulesets.Mania.Tests addJudgementAssert(objects[1], HitResult.IgnoreMiss); } + [Test] + public void TestHoldNoteReleasedHitAfterNextObjectStartTime() + { + var objects = new List + { + new HoldNote + { + StartTime = 1000, + EndTime = 1010, + }, + new HoldNote + { + StartTime = 1020, + EndTime = 1030 + } + }; + + var frames = new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(1030), + new ManiaReplayFrame(1040, ManiaAction.Key1), + new ManiaReplayFrame(1050) + }; + + performTest(objects, frames); + + addJudgementAssert(objects[0], HitResult.IgnoreHit); + addJudgementAssert("first head", () => ((HoldNote)objects[0]).Head, HitResult.Perfect); + addJudgementAssert("first tail", () => ((HoldNote)objects[0]).Tail, HitResult.Perfect); + + addJudgementAssert(objects[1], HitResult.IgnoreHit); + addJudgementAssert("second head", () => ((HoldNote)objects[1]).Head, HitResult.Great); + addJudgementAssert("second tail", () => ((HoldNote)objects[1]).Tail, HitResult.Perfect); + } + private void addJudgementAssert(ManiaHitObject 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(ManiaHitObject hitObject, double offset) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs index 9bc577a81e..961858b62b 100644 --- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Mania/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 System; using System.Collections.Generic; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -44,9 +43,6 @@ namespace osu.Game.Rulesets.Mania.UI /// The that was hit. public void HandleHit(DrawableHitObject hitObject) { - if (hitObject.IsHit && !IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) - throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); - foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { if (obj.Judged) From 508ae91a978e3d706d654457cbfaff4206aea266 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 12:53:53 +0900 Subject: [PATCH 4541/6909] Revert unnecessary change --- osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs | 2 +- osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs | 4 ++-- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 6c9f184c2c..5cb1519196 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests assertHeadJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); - assertNoteJudgement(HitResult.IgnoreMiss); + assertNoteJudgement(HitResult.IgnoreHit); } /// diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index 86a142f2f6..cecac38f70 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -74,8 +74,8 @@ namespace osu.Game.Rulesets.Mania.Tests performTest(objects, new List()); - addJudgementAssert(objects[0], HitResult.IgnoreMiss); - addJudgementAssert(objects[1], HitResult.IgnoreMiss); + addJudgementAssert(objects[0], HitResult.IgnoreHit); + addJudgementAssert(objects[1], HitResult.IgnoreHit); } [Test] diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 3b3f72157a..d9d740c145 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { - ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = r.Judgement.MaxResult); endHold(); } From 6014751e29f4cd3835d75af104ddeaa50c68b954 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 12:54:39 +0900 Subject: [PATCH 4542/6909] Add the ability for the game OSD to display user bindings Adds binding display for mouse button toggle / HUD toggle keys. - [ ] Depends on #10786 for ease-of-merge --- osu.Game/Configuration/OsuConfigManager.cs | 33 ++++++++++++++-------- osu.Game/Input/KeyBindingStore.cs | 17 +++++++++++ osu.Game/OsuGame.cs | 10 +++++++ 3 files changed, 49 insertions(+), 11 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index b79e99781b..795ad96170 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; @@ -9,6 +10,7 @@ using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Input; +using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Select; @@ -173,19 +175,28 @@ namespace osu.Game.Configuration } } - public override TrackedSettings CreateTrackedSettings() => new TrackedSettings + public override TrackedSettings CreateTrackedSettings() { - new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled")), - new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription())), - new TrackedSetting(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())), - new TrackedSetting(OsuSetting.Skin, m => - { - string skinName = LookupSkinName?.Invoke(m) ?? string.Empty; - return new SettingDescription(skinName, "skin", skinName); - }) - }; + // these need to be assigned in normal game startup scenarios. + Debug.Assert(LookupKeyBindings != null); + Debug.Assert(LookupSkinName != null); - public Func LookupSkinName { get; set; } + return new TrackedSettings + { + new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled", LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))), + new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription(), $"cycle: shift-tab quick view: {LookupKeyBindings(GlobalAction.HoldForHUD)}")), + new TrackedSetting(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())), + new TrackedSetting(OsuSetting.Skin, m => + { + string skinName = LookupSkinName(m) ?? string.Empty; + return new SettingDescription(skinName, "skin", skinName); + }) + }; + } + + public Func LookupSkinName { private get; set; } + + public Func LookupKeyBindings { private get; set; } } public enum OsuSetting diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index 198ab6883d..bc73d74d74 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -32,6 +32,23 @@ namespace osu.Game.Input public void Register(KeyBindingContainer manager) => insertDefaults(manager.DefaultKeyBindings); + /// + /// Retrieve all user-defined key combinations (in a format that can be displayed) for a specific action. + /// + /// The action to lookup. + /// A set of display strings for all the user's key configuration for the action. + public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) + { + foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction)) + { + string str = action.KeyCombination.ReadableString(); + + // even if found, the readable string may be empty for an unbound action. + if (str.Length > 0) + yield return str; + } + } + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = ContextFactory.GetForWrite()) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a34272f5f0..7f0465604b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -552,6 +552,16 @@ namespace osu.Game // if this becomes a more common thing, tracked settings should be reconsidered to allow local DI. LocalConfig.LookupSkinName = id => SkinManager.GetAllUsableSkins().FirstOrDefault(s => s.ID == id)?.ToString() ?? "Unknown"; + LocalConfig.LookupKeyBindings = l => + { + var combinations = KeyBindingStore.GetReadableKeyCombinationsFor(l).ToArray(); + + if (combinations.Length == 0) + return "none"; + + return string.Join(" or ", combinations); + }; + Container logoContainer; BackButton.Receptor receptor; From 8d38d9cc93497a0f32d253afc15e14769670b456 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 13:05:03 +0900 Subject: [PATCH 4543/6909] Add hotkey to select random skin --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- .../Input/Bindings/GlobalActionContainer.cs | 5 +++++ osu.Game/OsuGame.cs | 4 ++++ .../Overlays/Settings/Sections/SkinSection.cs | 17 +---------------- osu.Game/Skinning/SkinManager.cs | 15 +++++++++++++++ 5 files changed, 26 insertions(+), 17 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 795ad96170..a4b99bb6e6 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -189,7 +189,7 @@ namespace osu.Game.Configuration new TrackedSetting(OsuSetting.Skin, m => { string skinName = LookupSkinName(m) ?? string.Empty; - return new SettingDescription(skinName, "skin", skinName); + return new SettingDescription(skinName, "skin", skinName, $"random: {LookupKeyBindings(GlobalAction.RandomSkin)}"); }) }; } diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 3de4bb1f9d..e5d3a89a88 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -48,6 +48,8 @@ namespace osu.Game.Input.Bindings new KeyBinding(InputKey.Space, GlobalAction.Select), new KeyBinding(InputKey.Enter, GlobalAction.Select), new KeyBinding(InputKey.KeypadEnter, GlobalAction.Select), + + new KeyBinding(new[] { InputKey.Control, InputKey.Shift, InputKey.R }, GlobalAction.RandomSkin), }; public IEnumerable EditorKeyBindings => new[] @@ -191,5 +193,8 @@ namespace osu.Game.Input.Bindings [Description("Hold for HUD")] HoldForHUD, + + [Description("Random Skin")] + RandomSkin, } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7f0465604b..1e94becb98 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -887,6 +887,10 @@ namespace osu.Game case GlobalAction.ToggleGameplayMouseButtons: LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get(OsuSetting.MouseDisableButtons)); return true; + + case GlobalAction.RandomSkin: + SkinManager.SelectRandomSkin(); + return true; } return false; diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 8297b56db8..3e7068f1ff 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -9,7 +9,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Logging; -using osu.Framework.Utils; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Skinning; @@ -103,7 +102,7 @@ namespace osu.Game.Overlays.Settings.Sections { if (skin.NewValue == random_skin_info) { - randomizeSkin(); + skins.SelectRandomSkin(); return; } @@ -111,20 +110,6 @@ namespace osu.Game.Overlays.Settings.Sections }); } - private void randomizeSkin() - { - // choose from only user skins, removing the current selection to ensure a new one is chosen. - var randomChoices = skinItems.Where(s => s.ID > 0 && s.ID != configBindable.Value).ToArray(); - - if (randomChoices.Length == 0) - { - configBindable.Value = SkinInfo.Default.ID; - return; - } - - configBindable.Value = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)).ID; - } - private void updateItems() { skinItems = skins.GetAllUsableSkins(); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 2e4c24a89e..bef3e86a4d 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -19,6 +19,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Database; using osu.Game.IO.Archives; @@ -87,6 +88,20 @@ namespace osu.Game.Skinning /// A newly allocated list of available . public List GetAllUserSkins() => ModelStore.ConsumableItems.Where(s => !s.DeletePending).ToList(); + public void SelectRandomSkin() + { + // choose from only user skins, removing the current selection to ensure a new one is chosen. + var randomChoices = GetAllUsableSkins().Where(s => s.ID > 0 && s.ID != CurrentSkinInfo.Value.ID).ToArray(); + + if (randomChoices.Length == 0) + { + CurrentSkinInfo.Value = SkinInfo.Default; + return; + } + + CurrentSkinInfo.Value = randomChoices.ElementAt(RNG.Next(0, randomChoices.Length)); + } + protected override SkinInfo CreateModel(ArchiveReader archive) => new SkinInfo { Name = archive.Name }; private const string unknown_creator_string = "Unknown"; From 11cf04eed171f3dac856929f917febc62b8d9636 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 13:39:42 +0900 Subject: [PATCH 4544/6909] Fix frames potentially getting added to spectator replay in wrong format The way spectator currently works, the `Spectator` screen is responsible for adding new frames to the replay, even when it has a child (`SpectatorPlayer`) present. There was a possibility that a new play had already started, and on returning to the Spectator screen (to initialise the new play) there would be a brief period where the Player instance is still reading from the replay, the `userBeganPlaying` call had not yet finished initialising the new target replay, and `userSentFrames` is run (asynchronously), writing frames to the previous replay using the incorrect ruleset instance). To make this work, it doesn't `Schedule` frame addition to the replay (making things a bit unsafe). Changing this itself isn't such a simple one to do, so I instead opted to fix this via locking. Closes https://github.com/ppy/osu/issues/10777. --- osu.Game/Screens/Play/Spectator.cs | 80 ++++++++++++++++++------------ 1 file changed, 48 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 0f593db277..6f51771c12 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -61,7 +62,9 @@ namespace osu.Game.Screens.Play [Resolved] private RulesetStore rulesets { get; set; } - private Replay replay; + private Score score; + + private readonly object scoreLock = new object(); private Container beatmapPanelContainer; @@ -198,23 +201,32 @@ namespace osu.Game.Screens.Play private void userSentFrames(int userId, FrameDataBundle data) { + // this is not scheduled as it handles propagation of frames even when in a child screen (at which point we are not alive). + // probably not the safest way to handle this. + if (userId != targetUser.Id) return; - // this should never happen as the server sends the user's state on watching, - // but is here as a safety measure. - if (replay == null) - return; - - foreach (var frame in data.Frames) + lock (scoreLock) { - IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame(); - convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap); + // this should never happen as the server sends the user's state on watching, + // but is here as a safety measure. + if (score == null) + return; - var convertedFrame = (ReplayFrame)convertibleFrame; - convertedFrame.Time = frame.Time; + // rulesetInstance should be guaranteed to be in sync with the score via scoreLock. + Debug.Assert(rulesetInstance != null && rulesetInstance.RulesetInfo.Equals(score.ScoreInfo.Ruleset)); - replay.Frames.Add(convertedFrame); + foreach (var frame in data.Frames) + { + IConvertibleReplayFrame convertibleFrame = rulesetInstance.CreateConvertibleReplayFrame(); + convertibleFrame.FromLegacy(frame, beatmap.Value.Beatmap); + + var convertedFrame = (ReplayFrame)convertibleFrame; + convertedFrame.Time = frame.Time; + + score.Replay.Frames.Add(convertedFrame); + } } } @@ -247,10 +259,13 @@ namespace osu.Game.Screens.Play if (userId != targetUser.Id) return; - if (replay != null) + lock (scoreLock) { - replay.HasReceivedAllFrames = true; - replay = null; + if (score != null) + { + score.Replay.HasReceivedAllFrames = true; + score = null; + } } Schedule(clearDisplay); @@ -283,27 +298,28 @@ namespace osu.Game.Screens.Play return; } - replay ??= new Replay { HasReceivedAllFrames = false }; - - var scoreInfo = new ScoreInfo + lock (scoreLock) { - Beatmap = resolvedBeatmap, - User = targetUser, - Mods = state.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), - Ruleset = resolvedRuleset.RulesetInfo, - }; + score = new Score + { + ScoreInfo = new ScoreInfo + { + Beatmap = resolvedBeatmap, + User = targetUser, + Mods = state.Mods.Select(m => m.ToMod(resolvedRuleset)).ToArray(), + Ruleset = resolvedRuleset.RulesetInfo, + }, + Replay = new Replay { HasReceivedAllFrames = false }, + }; - ruleset.Value = resolvedRuleset.RulesetInfo; - rulesetInstance = resolvedRuleset; + ruleset.Value = resolvedRuleset.RulesetInfo; + rulesetInstance = resolvedRuleset; - beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); - watchButton.Enabled.Value = true; + beatmap.Value = beatmaps.GetWorkingBeatmap(resolvedBeatmap); + watchButton.Enabled.Value = true; - this.Push(new SpectatorPlayerLoader(new Score - { - ScoreInfo = scoreInfo, - Replay = replay, - })); + this.Push(new SpectatorPlayerLoader(score)); + } } private void showBeatmapPanel(SpectatorState state) From 324626e0978cb1d401440e516b6c2f74b0408e26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 13:51:20 +0900 Subject: [PATCH 4545/6909] Move default config tracking calls out of OnScreenDisplay itself --- osu.Game/OsuGame.cs | 7 ++++++- osu.Game/Overlays/OnScreenDisplay.cs | 11 +---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7f0465604b..ba856a6e6b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -627,7 +627,12 @@ namespace osu.Game loadComponentSingleFile(volume = new VolumeOverlay(), leftFloatingOverlayContent.Add, true); - loadComponentSingleFile(new OnScreenDisplay(), Add, true); + var onScreenDisplay = new OnScreenDisplay(); + + onScreenDisplay.BeginTracking(this, frameworkConfig); + onScreenDisplay.BeginTracking(this, LocalConfig); + + loadComponentSingleFile(onScreenDisplay, Add, true); loadComponentSingleFile(notifications.With(d => { diff --git a/osu.Game/Overlays/OnScreenDisplay.cs b/osu.Game/Overlays/OnScreenDisplay.cs index e6708093c4..af6d24fc65 100644 --- a/osu.Game/Overlays/OnScreenDisplay.cs +++ b/osu.Game/Overlays/OnScreenDisplay.cs @@ -3,16 +3,14 @@ using System; using System.Collections.Generic; -using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Configuration.Tracking; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osuTK; using osu.Framework.Graphics.Transforms; using osu.Framework.Threading; -using osu.Game.Configuration; using osu.Game.Overlays.OSD; +using osuTK; namespace osu.Game.Overlays { @@ -47,13 +45,6 @@ namespace osu.Game.Overlays }; } - [BackgroundDependencyLoader] - private void load(FrameworkConfigManager frameworkConfig, OsuConfigManager osuConfig) - { - BeginTracking(this, frameworkConfig); - BeginTracking(this, osuConfig); - } - private readonly Dictionary<(object, IConfigManager), TrackedSettings> trackedConfigManagers = new Dictionary<(object, IConfigManager), TrackedSettings>(); /// From c308eb75d991d53cdd28b99968b05e3ee8c3762e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 14:45:50 +0900 Subject: [PATCH 4546/6909] Move logic for performing actions from specific screen to its own component --- osu.Game/OsuGame.cs | 31 +------ osu.Game/PerformFromMenuRunner.cs | 145 ++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 29 deletions(-) create mode 100644 osu.Game/PerformFromMenuRunner.cs diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5119f262d5..cbf463d3bc 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -463,7 +463,7 @@ namespace osu.Game #endregion - private ScheduledDelegate performFromMainMenuTask; + private PerformFromMenuRunner performFromMainMenuTask; /// /// Perform an action only after returning to a specific screen as indicated by . @@ -474,34 +474,7 @@ namespace osu.Game public void PerformFromScreen(Action action, IEnumerable validScreens = null) { performFromMainMenuTask?.Cancel(); - - validScreens ??= Enumerable.Empty(); - validScreens = validScreens.Append(typeof(MainMenu)); - - CloseAllOverlays(false); - - // we may already be at the target screen type. - if (validScreens.Contains(ScreenStack.CurrentScreen?.GetType()) && !Beatmap.Disabled) - { - action(ScreenStack.CurrentScreen); - return; - } - - // find closest valid target - IScreen screen = ScreenStack.CurrentScreen; - - while (screen != null) - { - if (validScreens.Contains(screen.GetType())) - { - screen.MakeCurrent(); - break; - } - - screen = screen.GetParentScreen(); - } - - performFromMainMenuTask = Schedule(() => PerformFromScreen(action, validScreens)); + Add(performFromMainMenuTask = new PerformFromMenuRunner(action, validScreens, () => ScreenStack.CurrentScreen)); } /// diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs new file mode 100644 index 0000000000..8fcea7c277 --- /dev/null +++ b/osu.Game/PerformFromMenuRunner.cs @@ -0,0 +1,145 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Framework.Threading; +using osu.Game.Beatmaps; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; +using osu.Game.Overlays.Notifications; +using osu.Game.Screens.Menu; + +namespace osu.Game +{ + internal class PerformFromMenuRunner : Component + { + private readonly Action finalAction; + private readonly IEnumerable validScreens; + private readonly Func getCurrentScreen; + + [Resolved] + private NotificationOverlay notifications { get; set; } + + [Resolved] + private DialogOverlay dialogOverlay { get; set; } + + [Resolved] + private IBindable beatmap { get; set; } + + [Resolved(canBeNull: true)] + private OsuGame game { get; set; } + + private readonly ScheduledDelegate task; + + private PopupDialog lastEncounteredDialog; + private IScreen lastEncounteredDialogScreen; + + /// + /// Perform an action only after returning to a specific screen as indicated by . + /// Eagerly tries to exit the current screen until it succeeds. + /// + /// The action to perform once we are in the correct state. + /// An optional collection of valid screen types. If any of these screens are already current we can perform the action immediately, else the first valid parent will be made current before performing the action. is used if not specified. + /// A function to retrieve the currently displayed game screen. + public PerformFromMenuRunner(Action finalAction, IEnumerable validScreens, Func getCurrentScreen) + { + validScreens ??= Enumerable.Empty(); + validScreens = validScreens.Append(typeof(MainMenu)); + + this.finalAction = finalAction; + this.validScreens = validScreens; + this.getCurrentScreen = getCurrentScreen; + + Scheduler.Add(task = new ScheduledDelegate(checkCanComplete, 0, 200)); + } + + /// + /// Cancel this runner from running. + /// + public void Cancel() + { + task.Cancel(); + Expire(); + } + + private void checkCanComplete() + { + // find closest valid target + IScreen current = getCurrentScreen(); + + // a dialog may be blocking the execution for now. + if (checkForDialog(current)) return; + + // we may already be at the target screen type. + if (validScreens.Contains(getCurrentScreen().GetType()) && !beatmap.Disabled) + { + complete(); + return; + } + + game.CloseAllOverlays(false); + + while (current != null) + { + if (validScreens.Contains(current.GetType())) + { + current.MakeCurrent(); + break; + } + + current = current.GetParentScreen(); + } + } + + /// + /// Check whether there is currently a dialog requiring user interaction. + /// + /// + /// Whether a dialog blocked interaction. + private bool checkForDialog(IScreen current) + { + var currentDialog = dialogOverlay.CurrentDialog; + + if (lastEncounteredDialog != null) + { + if (lastEncounteredDialog == currentDialog) + // still waiting on user interaction + return true; + + if (lastEncounteredDialogScreen != current) + { + // a dialog was previously encountered but has since been dismissed. + // if the screen changed, the user likely confirmed an exit dialog and we should continue attempting the action. + lastEncounteredDialog = null; + lastEncounteredDialogScreen = null; + return false; + } + + // the last dialog encountered has been dismissed but the screen has not changed, abort. + Cancel(); + notifications.Post(new SimpleNotification { Text = @"An action was interrupted due to a dialog being displayed." }); + return true; + } + + if (currentDialog == null) + return false; + + // a new dialog was encountered. + lastEncounteredDialog = currentDialog; + lastEncounteredDialogScreen = current; + return true; + } + + private void complete() + { + finalAction(getCurrentScreen()); + Cancel(); + } + } +} From 804450e707300b51abfc13014d354f9a9a1123ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 15:49:45 +0900 Subject: [PATCH 4547/6909] Remove duplicate instantiation of externalLinkOpener --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5119f262d5..a4e7214d32 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -662,7 +662,6 @@ namespace osu.Game loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true); - loadComponentSingleFile(externalLinkOpener = new ExternalLinkOpener(), topMostOverlayContent.Add); chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; From b28a0d5cd50333437de4af21ed05c37466eabe06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 15:58:43 +0900 Subject: [PATCH 4548/6909] Add test coverage --- .../Visual/Navigation/OsuGameTestScene.cs | 2 +- .../Navigation/TestScenePerformFromScreen.cs | 130 ++++++++++++++++-- 2 files changed, 116 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index 4c18cfa61c..c5038068ec 100644 --- a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs +++ b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Navigation } [SetUpSteps] - public void SetUpSteps() + public virtual void SetUpSteps() { AddStep("Create new game instance", () => { diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index 75c6a2b733..a4190e0b84 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -2,23 +2,33 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Screens; +using osu.Game.Overlays; +using osu.Game.Screens; using osu.Game.Screens.Menu; using osu.Game.Screens.Play; using osu.Game.Screens.Select; +using osuTK.Input; namespace osu.Game.Tests.Visual.Navigation { public class TestScenePerformFromScreen : OsuGameTestScene { + private bool actionPerformed; + + public override void SetUpSteps() + { + AddStep("reset status", () => actionPerformed = false); + + base.SetUpSteps(); + } + [Test] public void TestPerformAtMenu() { - AddAssert("could perform immediately", () => - { - bool actionPerformed = false; - Game.PerformFromScreen(_ => actionPerformed = true); - return actionPerformed; - }); + AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true)); + AddAssert("did perform", () => actionPerformed); } [Test] @@ -26,12 +36,9 @@ namespace osu.Game.Tests.Visual.Navigation { PushAndConfirm(() => new PlaySongSelect()); - AddAssert("could perform immediately", () => - { - bool actionPerformed = false; - Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) }); - return actionPerformed; - }); + AddStep("perform immediately", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) })); + AddAssert("did perform", () => actionPerformed); + AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); } [Test] @@ -39,7 +46,6 @@ namespace osu.Game.Tests.Visual.Navigation { PushAndConfirm(() => new PlaySongSelect()); - bool actionPerformed = false; AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); AddUntilStep("returned to menu", () => Game.ScreenStack.CurrentScreen is MainMenu); AddAssert("did perform", () => actionPerformed); @@ -51,7 +57,6 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => new PlaySongSelect()); PushAndConfirm(() => new PlayerLoader(() => new Player())); - bool actionPerformed = false; AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true, new[] { typeof(PlaySongSelect) })); AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is PlaySongSelect); AddAssert("did perform", () => actionPerformed); @@ -63,10 +68,105 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => new PlaySongSelect()); PushAndConfirm(() => new PlayerLoader(() => new Player())); - bool actionPerformed = false; AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); AddUntilStep("returned to song select", () => Game.ScreenStack.CurrentScreen is MainMenu); AddAssert("did perform", () => actionPerformed); } + + [TestCase(true)] + [TestCase(false)] + public void TestPerformBlockedByDialog(bool confirmed) + { + DialogBlockingScreen blocker = null; + + PushAndConfirm(() => blocker = new DialogBlockingScreen()); + AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); + + AddWaitStep("wait a bit", 10); + + AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is DialogBlockingScreen); + AddAssert("did not perform", () => !actionPerformed); + AddAssert("only one exit attempt", () => blocker.ExitAttempts == 1); + + AddUntilStep("wait for dialog display", () => Game.Dependencies.Get().IsLoaded); + + if (confirmed) + { + AddStep("accept dialog", () => InputManager.Key(Key.Number1)); + AddUntilStep("wait for dialog dismissed", () => Game.Dependencies.Get().CurrentDialog == null); + AddUntilStep("did perform", () => actionPerformed); + } + else + { + AddStep("cancel dialog", () => InputManager.Key(Key.Number2)); + AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen is DialogBlockingScreen); + AddAssert("did not perform", () => !actionPerformed); + } + } + + [TestCase(true)] + [TestCase(false)] + public void TestPerformBlockedByDialogNested(bool confirmSecond) + { + DialogBlockingScreen blocker = null; + DialogBlockingScreen blocker2 = null; + + PushAndConfirm(() => blocker = new DialogBlockingScreen()); + PushAndConfirm(() => blocker2 = new DialogBlockingScreen()); + + AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); + + AddUntilStep("wait for dialog", () => blocker2.ExitAttempts == 1); + + AddWaitStep("wait a bit", 10); + + AddUntilStep("wait for dialog display", () => Game.Dependencies.Get().IsLoaded); + + AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == blocker2); + AddAssert("did not perform", () => !actionPerformed); + AddAssert("only one exit attempt", () => blocker2.ExitAttempts == 1); + + AddStep("accept dialog", () => InputManager.Key(Key.Number1)); + AddUntilStep("screen changed", () => Game.ScreenStack.CurrentScreen == blocker); + + AddUntilStep("wait for second dialog", () => blocker.ExitAttempts == 1); + AddAssert("did not perform", () => !actionPerformed); + AddAssert("only one exit attempt", () => blocker.ExitAttempts == 1); + + if (confirmSecond) + { + AddStep("accept dialog", () => InputManager.Key(Key.Number1)); + AddUntilStep("did perform", () => actionPerformed); + } + else + { + AddStep("cancel dialog", () => InputManager.Key(Key.Number2)); + AddAssert("screen didn't change", () => Game.ScreenStack.CurrentScreen == blocker); + AddAssert("did not perform", () => !actionPerformed); + } + } + + public class DialogBlockingScreen : OsuScreen + { + [Resolved] + private DialogOverlay dialogOverlay { get; set; } + + private int dialogDisplayCount; + + public int ExitAttempts { get; private set; } + + public override bool OnExiting(IScreen next) + { + ExitAttempts++; + + if (dialogDisplayCount++ < 1) + { + dialogOverlay.Push(new ConfirmExitDialog(this.Exit, () => { })); + return true; + } + + return base.OnExiting(next); + } + } } } From 5d55af58182449a73da8aea2c5da743fb86fc589 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 16:35:48 +0900 Subject: [PATCH 4549/6909] Fix hitobjects sometimes not fading in completely with HD mod --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 10 ++++++++-- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 025e202666..cf7faca9b9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -27,17 +27,23 @@ namespace osu.Game.Rulesets.Osu.Mods public override void ApplyToDrawableHitObjects(IEnumerable drawables) { foreach (var d in drawables) - d.ApplyCustomUpdateState += applyFadeInAdjustment; + { + d.HitObjectApplied += applyFadeInAdjustment; + applyFadeInAdjustment(d); + } base.ApplyToDrawableHitObjects(drawables); } - private void applyFadeInAdjustment(DrawableHitObject hitObject, ArmedState state) + private void applyFadeInAdjustment(DrawableHitObject hitObject) { if (!(hitObject is DrawableOsuHitObject d)) return; d.HitObject.TimeFadeIn = d.HitObject.TimePreempt * fade_in_duration_multiplier; + + foreach (var nested in d.NestedHitObjects) + applyFadeInAdjustment(nested); } private double lastSliderHeadFadeOutStartTime; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 7a4e136553..62709b2900 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -26,8 +26,16 @@ namespace osu.Game.Rulesets.Objects.Drawables [Cached(typeof(DrawableHitObject))] public abstract class DrawableHitObject : SkinReloadableDrawable { + /// + /// Invoked after this 's applied has had its defaults applied. + /// public event Action DefaultsApplied; + /// + /// Invoked after a has been applied to this . + /// + public event Action HitObjectApplied; + /// /// The currently represented by this . /// @@ -192,6 +200,7 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject.DefaultsApplied += onDefaultsApplied; OnApply(hitObject); + HitObjectApplied?.Invoke(this); // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. if (IsLoaded) From 891218ec6b754ef13165c9e95dc88b64d66d9c75 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Nov 2020 09:11:33 +0100 Subject: [PATCH 4550/6909] Inline empty explosion in legacy transformer --- .../Skinning/TaikoLegacySkinTransformer.cs | 2 +- osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 880af3fbd8..96fb065e79 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning // suppress the default kiai explosion if the skin brings its own sprites. // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. if (hasExplosion.Value) - return KiaiHitExplosion.EmptyExplosion(); + return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue); return null; diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs index 326cb23897..20900a9352 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -43,11 +43,5 @@ namespace osu.Game.Rulesets.Taiko.UI { Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoExplosionKiai), _ => new DefaultKiaiHitExplosion(hitType)); } - - /// - /// Helper function to use when an explosion is not desired. - /// Lifetime is set to avoid accumulating empty drawables in the parent container. - /// - public static Drawable EmptyExplosion() => Empty().With(d => d.LifetimeEnd = double.MinValue); } } From a08833f3b3cd3b7581be8e95a0b56d03611c8567 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 18:03:04 +0900 Subject: [PATCH 4551/6909] 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 bbe8426316..5078fee1cf 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 8f0cc58594..405fb1a6ca 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index f766e0ec03..099ecd8319 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 3401b099d4dadbb92ee3471be3ac9f5774684f1e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 18:50:08 +0900 Subject: [PATCH 4552/6909] Add some tests --- .../Gameplay/TestScenePoolingRuleset.cs | 247 ++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs new file mode 100644 index 0000000000..bb09aec416 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -0,0 +1,247 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.UI; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestScenePoolingRuleset : OsuTestScene + { + private const double time_between_objects = 1000; + + private TestDrawablePoolingRuleset drawableRuleset; + + [Test] + public void TestReusedWithHitObjectsSpacedFarApart() + { + ManualClock clock = null; + + createTest(new Beatmap + { + HitObjects = + { + new HitObject(), + new HitObject { StartTime = time_between_objects } + } + }, 1, () => new FramedClock(clock = new ManualClock())); + + DrawableTestHitObject firstObject = null; + AddUntilStep("first object shown", () => this.ChildrenOfType().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[0]); + AddStep("get DHO", () => firstObject = this.ChildrenOfType().Single()); + + AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime); + + AddUntilStep("second object shown", () => this.ChildrenOfType().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[1]); + AddAssert("DHO reused", () => this.ChildrenOfType().Single() == firstObject); + } + + [Test] + public void TestNotReusedWithHitObjectsSpacedClose() + { + ManualClock clock = null; + + createTest(new Beatmap + { + HitObjects = + { + new HitObject(), + new HitObject { StartTime = 250 } + } + }, 2, () => new FramedClock(clock = new ManualClock())); + + AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime); + + AddUntilStep("two DHOs shown", () => this.ChildrenOfType().Count() == 2); + AddAssert("DHOs have different hitobjects", + () => this.ChildrenOfType().ElementAt(0).HitObject != this.ChildrenOfType().ElementAt(1).HitObject); + } + + [Test] + public void TestManyHitObjects() + { + var beatmap = new Beatmap(); + + for (int i = 0; i < 500; i++) + beatmap.HitObjects.Add(new HitObject { StartTime = i * 10 }); + + createTest(beatmap, 100); + + AddUntilStep("any DHOs shown", () => this.ChildrenOfType().Any()); + AddUntilStep("no DHOs shown", () => !this.ChildrenOfType().Any()); + } + + private void createTest(IBeatmap beatmap, int poolSize, Func createClock = null) => AddStep("create test", () => + { + var ruleset = new TestPoolingRuleset(); + + drawableRuleset = (TestDrawablePoolingRuleset)ruleset.CreateDrawableRulesetWith(CreateWorkingBeatmap(beatmap).GetPlayableBeatmap(ruleset.RulesetInfo)); + drawableRuleset.FrameStablePlayback = true; + drawableRuleset.PoolSize = poolSize; + + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Clock = createClock?.Invoke() ?? new FramedOffsetClock(Clock, false) { Offset = -Clock.CurrentTime }, + Child = drawableRuleset + }; + }); + + #region Ruleset + + private class TestPoolingRuleset : Ruleset + { + public override IEnumerable GetModsFor(ModType type) => throw new NotImplementedException(); + + public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => new TestDrawablePoolingRuleset(this, beatmap, mods); + + public override IBeatmapConverter CreateBeatmapConverter(IBeatmap beatmap) => new TestBeatmapConverter(beatmap, this); + + public override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => throw new NotImplementedException(); + + public override string Description { get; } = string.Empty; + + public override string ShortName { get; } = string.Empty; + } + + private class TestDrawablePoolingRuleset : DrawableRuleset + { + public int PoolSize; + + public TestDrawablePoolingRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) + : base(ruleset, beatmap, mods) + { + } + + [BackgroundDependencyLoader] + private void load() + { + RegisterPool(PoolSize); + } + + protected override bool PoolHitObjects => true; + + protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); + + protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); + + protected override Playfield CreatePlayfield() => new TestPlayfield(); + + private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry + { + public TestHitObjectLifetimeEntry(HitObject hitObject) + : base(hitObject) + { + } + + protected override double InitialLifetimeOffset => 0; + } + } + + private class TestPlayfield : Playfield + { + public TestPlayfield() + { + AddInternal(HitObjectContainer); + } + + protected override GameplayCursorContainer CreateCursor() => null; + } + + private class TestBeatmapConverter : BeatmapConverter + { + public TestBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) + : base(beatmap, ruleset) + { + } + + public override bool CanConvert() => true; + + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) + { + yield return new TestHitObject + { + StartTime = original.StartTime, + Duration = 250 + }; + } + } + + #endregion + + #region HitObject + + private class TestHitObject : ConvertHitObject + { + public double EndTime => StartTime + Duration; + + public double Duration { get; set; } + } + + private class DrawableTestHitObject : DrawableHitObject + { + public DrawableTestHitObject() + : base(null) + { + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); + Size = new Vector2(50, 50); + + Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f); + } + + [BackgroundDependencyLoader] + private void load() + { + AddInternal(new Circle + { + RelativeSizeAxes = Axes.Both, + }); + } + + protected override void CheckForResult(bool userTriggered, double timeOffset) + { + if (timeOffset > HitObject.Duration) + ApplyResult(r => r.Type = r.Judgement.MaxResult); + } + + protected override void UpdateHitStateTransforms(ArmedState state) + { + base.UpdateHitStateTransforms(state); + + switch (state) + { + case ArmedState.Hit: + case ArmedState.Miss: + this.FadeOut(250); + break; + } + } + } + + #endregion + } +} From 90499329bd10e75c81ff946f5195039f196ebdab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 18:50:38 +0900 Subject: [PATCH 4553/6909] Fix frame stable playback not being set correctly --- 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 f6cf836fe7..a36b66d62b 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.UI get => frameStablePlayback; set { - frameStablePlayback = false; + frameStablePlayback = value; if (frameStabilityContainer != null) frameStabilityContainer.FrameStablePlayback = value; } From 7d020181343f942159ae21df57b3ca8d07aad9f6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 18:54:40 +0900 Subject: [PATCH 4554/6909] Remove some unnecessary implementations for now --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 32 +------------ osu.Game/Rulesets/UI/Playfield.cs | 55 ---------------------- 2 files changed, 1 insertion(+), 86 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 3a5e0b64ed..a0c95898be 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -41,32 +41,6 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; - /// - /// Invoked when a becomes used by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become alive. - /// - public event Action HitObjectUsageBegan; - - /// - /// Invoked when a becomes unused by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become dead. - /// - public event Action HitObjectUsageFinished; - - /// - /// The amount of time prior to the current time within which s should be considered alive. - /// - public double PastLifetimeExtension { get; set; } - - /// - /// The amount of time after the current time within which s should be considered alive. - /// - public double FutureLifetimeExtension { get; set; } - private readonly Dictionary startTimeMap = new Dictionary(); private readonly Dictionary drawableMap = new Dictionary(); private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); @@ -102,8 +76,6 @@ namespace osu.Game.Rulesets.UI bindStartTime(drawable); AddInternal(drawableMap[entry] = drawable, false); - - HitObjectUsageBegan?.Invoke(entry.HitObject); } private void removeDrawable(HitObjectLifetimeEntry entry) @@ -119,8 +91,6 @@ namespace osu.Game.Rulesets.UI unbindStartTime(drawable); RemoveInternal(drawable); - - HitObjectUsageFinished?.Invoke(entry.HitObject); } #endregion @@ -173,7 +143,7 @@ namespace osu.Game.Rulesets.UI unbindAllStartTimes(); } - protected override bool CheckChildrenLife() => base.CheckChildrenLife() | lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); + protected override bool CheckChildrenLife() => base.CheckChildrenLife() | lifetimeManager.Update(Time.Current, Time.Current); private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r); private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 2bd2bb9e06..cdaf9364af 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -29,22 +29,6 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; - /// - /// Invoked when a becomes used by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become alive. - /// - public event Action HitObjectUsageBegan; - - /// - /// Invoked when a becomes unused by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become dead. - /// - public event Action HitObjectUsageFinished; - /// /// The contained in this Playfield. /// @@ -104,8 +88,6 @@ namespace osu.Game.Rulesets.UI { h.NewResult += (d, r) => NewResult?.Invoke(d, r); h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); - h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); - h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); })); } @@ -198,41 +180,6 @@ namespace osu.Game.Rulesets.UI { } - /// - /// Sets whether to keep a given always alive within this or any nested . - /// - /// The to set. - /// Whether to keep always alive. - public void SetKeepAlive(HitObject hitObject, bool keepAlive) - { - if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) - { - entry.KeepAlive = keepAlive; - return; - } - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var p in nestedPlayfields.Value) - p.SetKeepAlive(hitObject, keepAlive); - } - - /// - /// Keeps all s alive within this and all nested s. - /// - public void KeepAllAlive() - { - foreach (var (_, entry) in lifetimeEntryMap) - entry.KeepAlive = true; - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var p in nestedPlayfields.Value) - p.KeepAllAlive(); - } - /// /// The cursor currently being used by this . May be null if no cursor is provided. /// @@ -258,8 +205,6 @@ namespace osu.Game.Rulesets.UI otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r); - otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h); - otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h); nestedPlayfields.Value.Add(otherPlayfield); } From 606cfacedf959e08727c8e642a35ef83bf8cad96 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 19:01:12 +0900 Subject: [PATCH 4555/6909] Fix state update exception in a better way --- .../Objects/Drawables/Pieces/MainCirclePiece.cs | 11 ++++++++--- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 4 ++-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs index 98432eb4fe..bf2236c945 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -51,9 +51,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces var drawableOsuObject = (DrawableOsuHitObject)drawableObject; state.BindTo(drawableObject.State); - state.BindValueChanged(updateState, true); - accentColour.BindTo(drawableObject.AccentColour); + indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => { explode.Colour = colour.NewValue; @@ -61,7 +67,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces circle.Colour = colour.NewValue; }, true); - indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index bcf1103f39..244cf831c3 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -613,13 +613,13 @@ namespace osu.Game.Rulesets.Objects.Drawables /// The time at which state transforms should be applied that line up to 's StartTime. /// This is used to offset calls to . /// - public double StateUpdateTime => HitObject?.StartTime ?? 0; + public double StateUpdateTime => HitObject.StartTime; /// /// The time at which judgement dependent state transforms should be applied. This is equivalent of the (end) time of the object, in addition to any judgement offset. /// This is used to offset calls to . /// - public double HitStateUpdateTime => Result?.TimeAbsolute ?? HitObject?.GetEndTime() ?? 0; + public double HitStateUpdateTime => Result?.TimeAbsolute ?? HitObject.GetEndTime(); /// /// Will be called at least once after this has become not alive. From 7fdaf69903cc8158850c70523054f09551c26a4f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 19:12:12 +0900 Subject: [PATCH 4556/6909] Add some more xmldocs --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 28 +++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 1120f11df2..2c5fce3e86 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -526,6 +526,9 @@ namespace osu.Game.Rulesets.UI /// public abstract void CancelResume(); + private readonly Dictionary pools = new Dictionary(); + private readonly Dictionary lifetimeEntries = new Dictionary(); + /// /// Whether this should retrieve pooled s. /// @@ -534,8 +537,14 @@ namespace osu.Game.Rulesets.UI /// protected virtual bool PoolHitObjects => false; - private readonly Dictionary pools = new Dictionary(); - + /// + /// Registers a pool with this which is to be used whenever + /// representations are requested for the given type (via ). + /// + /// The number of drawables to be prepared for initial consumption. + /// An optional maximum size after which the pool will no longer be expanded. + /// The type. + /// The receiver for s. protected void RegisterPool(int initialSize, int? maximumSize = null) where TObject : HitObject where TDrawable : DrawableHitObject, new() @@ -582,10 +591,21 @@ namespace osu.Game.Rulesets.UI }); } + /// + /// Creates the for a given . + /// + /// + /// This may be overridden to provide custom lifetime control (e.g. via . + /// + /// The to create the entry for. + /// The . protected abstract HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject); - private readonly Dictionary lifetimeEntries = new Dictionary(); - + /// + /// Retrieves or creates the for a given . + /// + /// The to retrieve or create the for. + /// The for . protected HitObjectLifetimeEntry GetLifetimeEntry(HitObject hitObject) { if (lifetimeEntries.TryGetValue(hitObject, out var entry)) From a8929b07644ab0c5d07a23c988c8497fb9c17477 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 19:27:07 +0900 Subject: [PATCH 4557/6909] Revert unnecessary change of casting --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index a0c95898be..12d78dac2b 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.UI /// /// All currently in-use s. /// - public IEnumerable Objects => InternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); + public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); /// /// All currently in-use s that are alive. @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this is equivalent to . /// - public IEnumerable AliveObjects => AliveInternalChildren.OfType().OrderBy(h => h.HitObject.StartTime); + public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); /// /// Invoked when a is judged. From 85017a009428ecf9a3ee881a28b209ea1e07b207 Mon Sep 17 00:00:00 2001 From: kamp Date: Wed, 11 Nov 2020 20:20:29 +0100 Subject: [PATCH 4558/6909] Add test for accuracy heatmap to TestCaseStatisticsPanel --- .../Ranking/TestSceneStatisticsPanel.cs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 8700fbeb42..6b0ff4b9c9 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -1,19 +1,24 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Collections.Generic; 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; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Tests.Visual.Ranking { public class TestSceneStatisticsPanel : OsuTestScene { [Test] - public void TestScoreWithStatistics() + public void TestScoreWithTimeStatistics() { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { @@ -23,6 +28,17 @@ namespace osu.Game.Tests.Visual.Ranking loadPanel(score); } + [Test] + public void TestScoreWithPositionStatistics() + { + var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) + { + HitEvents = CreatePositionDistributedHitEvents() + }; + + loadPanel(score); + } + [Test] public void TestScoreWithoutStatistics() { @@ -44,5 +60,24 @@ namespace osu.Game.Tests.Visual.Ranking Score = { Value = score } }; }); + + public static List CreatePositionDistributedHitEvents() + { + var hitEvents = new List(); + // Use constant seed for reproducibility + var random = new Random(0); + + for (int i = 0; i < 500; i++) + { + float angle = (float) random.NextDouble() * 2 * (float) Math.PI; + float radius = (float) random.NextDouble() * 0.5f * HitCircle.OBJECT_RADIUS; + + Vector2 position = new Vector2(radius * (float) Math.Cos(angle), radius * (float) Math.Sin(angle)); + + hitEvents.Add(new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), position)); + } + + return hitEvents; + } } } From 8341d3ad741f0e7e47d86f76c0566981b241d632 Mon Sep 17 00:00:00 2001 From: kamp Date: Wed, 11 Nov 2020 21:52:43 +0100 Subject: [PATCH 4559/6909] Fix formatting --- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 6b0ff4b9c9..3185b782db 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -69,10 +69,10 @@ namespace osu.Game.Tests.Visual.Ranking for (int i = 0; i < 500; i++) { - float angle = (float) random.NextDouble() * 2 * (float) Math.PI; - float radius = (float) random.NextDouble() * 0.5f * HitCircle.OBJECT_RADIUS; + float angle = (float)random.NextDouble() * 2 * (float)Math.PI; + float radius = (float)random.NextDouble() * 0.5f * HitCircle.OBJECT_RADIUS; - Vector2 position = new Vector2(radius * (float) Math.Cos(angle), radius * (float) Math.Sin(angle)); + Vector2 position = new Vector2(radius * (float)Math.Cos(angle), radius * (float)Math.Sin(angle)); hitEvents.Add(new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), position)); } From 423f0fbda74313fdef0a8c97691111826895bce2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Nov 2020 22:37:15 +0100 Subject: [PATCH 4560/6909] Reference constant through base class --- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 3185b782db..89a82b01a8 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Ranking for (int i = 0; i < 500; i++) { float angle = (float)random.NextDouble() * 2 * (float)Math.PI; - float radius = (float)random.NextDouble() * 0.5f * HitCircle.OBJECT_RADIUS; + float radius = (float)random.NextDouble() * 0.5f * OsuHitObject.OBJECT_RADIUS; Vector2 position = new Vector2(radius * (float)Math.Cos(angle), radius * (float)Math.Sin(angle)); From 1984a9f70d6e6c17f4d547ffac091b74ccddc9e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Nov 2020 22:40:52 +0100 Subject: [PATCH 4561/6909] Reduce amount of casting --- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index 89a82b01a8..e92ae7c538 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -69,10 +69,10 @@ namespace osu.Game.Tests.Visual.Ranking for (int i = 0; i < 500; i++) { - float angle = (float)random.NextDouble() * 2 * (float)Math.PI; - float radius = (float)random.NextDouble() * 0.5f * OsuHitObject.OBJECT_RADIUS; + double angle = random.NextDouble() * 2 * Math.PI; + double radius = random.NextDouble() * 0.5f * OsuHitObject.OBJECT_RADIUS; - Vector2 position = new Vector2(radius * (float)Math.Cos(angle), radius * (float)Math.Sin(angle)); + var position = new Vector2((float)(radius * Math.Cos(angle)), (float)(radius * Math.Sin(angle))); hitEvents.Add(new HitEvent(0, HitResult.Perfect, new HitCircle(), new HitCircle(), position)); } From 1426530496bf7b1d9ec35a365a30fdf2839c7356 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Nov 2020 22:41:25 +0100 Subject: [PATCH 4562/6909] Make method private --- osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs index e92ae7c538..d91aec753c 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneStatisticsPanel.cs @@ -33,7 +33,7 @@ namespace osu.Game.Tests.Visual.Ranking { var score = new TestScoreInfo(new OsuRuleset().RulesetInfo) { - HitEvents = CreatePositionDistributedHitEvents() + HitEvents = createPositionDistributedHitEvents() }; loadPanel(score); @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Ranking }; }); - public static List CreatePositionDistributedHitEvents() + private static List createPositionDistributedHitEvents() { var hitEvents = new List(); // Use constant seed for reproducibility From f753f138c6bb3df154e109d30ee6bbbaaa841257 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 12 Nov 2020 03:11:29 +0300 Subject: [PATCH 4563/6909] Add counter to most played beatmaps section in user overlay --- .../Historical/PaginatedMostPlayedBeatmapContainer.cs | 4 +++- osu.Game/Users/User.cs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 8f19cd900c..556f3139dd 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer { public PaginatedMostPlayedBeatmapContainer(Bindable user) - : base(user, "Most Played Beatmaps", "No records. :(") + : base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible) { ItemsPerPage = 5; } @@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical ItemsContainer.Direction = FillDirection.Vertical; } + protected override int GetCount(User user) => user.BeatmapPlaycountsCount; + protected override APIRequest> CreateRequest() => new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++, ItemsPerPage); diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 2a76a963e1..d7e78d5b35 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -144,6 +144,9 @@ namespace osu.Game.Users [JsonProperty(@"scores_first_count")] public int ScoresFirstCount; + [JsonProperty(@"beatmap_playcounts_count")] + public int BeatmapPlaycountsCount; + [JsonProperty] private string[] playstyle { From ad79c2bc6286c377dd2c8757abb7fa18a7f81454 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Nov 2020 10:55:22 +0900 Subject: [PATCH 4564/6909] Avoid multiple enumeration by converting to array at construction time --- osu.Game/PerformFromMenuRunner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index 8fcea7c277..9afe87f74f 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -20,7 +20,7 @@ namespace osu.Game internal class PerformFromMenuRunner : Component { private readonly Action finalAction; - private readonly IEnumerable validScreens; + private readonly Type[] validScreens; private readonly Func getCurrentScreen; [Resolved] @@ -53,7 +53,7 @@ namespace osu.Game validScreens = validScreens.Append(typeof(MainMenu)); this.finalAction = finalAction; - this.validScreens = validScreens; + this.validScreens = validScreens.ToArray(); this.getCurrentScreen = getCurrentScreen; Scheduler.Add(task = new ScheduledDelegate(checkCanComplete, 0, 200)); From d7d77460fb80e8a6d8989297621df505b9b552e7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 12:55:42 +0900 Subject: [PATCH 4565/6909] Small refactorings --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 15 ++++++++++++++- osu.Game/Rulesets/UI/HitObjectContainer.cs | 10 ++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 2c5fce3e86..1a00346d6a 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -233,6 +233,13 @@ namespace osu.Game.Rulesets.UI ResumeOverlay?.Hide(); } + /// + /// Adds a to this . + /// + /// + /// This does not add the to the beatmap. + /// + /// The to add. public void AddHitObject(TObject hitObject) { if (PoolHitObjects) @@ -241,6 +248,13 @@ namespace osu.Game.Rulesets.UI Playfield.Add(CreateDrawableRepresentation(hitObject)); } + /// + /// Removes a from this . + /// + /// + /// This does not remove the from the beatmap. + /// + /// The to remove. public void RemoveHitObject(TObject hitObject) { if (PoolHitObjects) @@ -380,7 +394,6 @@ namespace osu.Game.Rulesets.UI /// Displays an interactive ruleset gameplay instance. /// /// This type is required only for adding non-generic type to the draw hierarchy. - /// Once IDrawable is a thing, this can also become an interface. /// /// [Cached(typeof(DrawableRuleset))] diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 12d78dac2b..ff358e2e75 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -100,10 +100,11 @@ namespace osu.Game.Rulesets.UI public virtual void Add(DrawableHitObject hitObject) { bindStartTime(hitObject); - AddInternal(hitObject); hitObject.OnNewResult += onNewResult; hitObject.OnRevertResult += onRevertResult; + + AddInternal(hitObject); } public virtual bool Remove(DrawableHitObject hitObject) @@ -143,7 +144,12 @@ namespace osu.Game.Rulesets.UI unbindAllStartTimes(); } - protected override bool CheckChildrenLife() => base.CheckChildrenLife() | lifetimeManager.Update(Time.Current, Time.Current); + protected override bool CheckChildrenLife() + { + bool aliveChanged = base.CheckChildrenLife(); + aliveChanged |= lifetimeManager.Update(Time.Current, Time.Current); + return aliveChanged; + } private void onNewResult(DrawableHitObject d, JudgementResult r) => NewResult?.Invoke(d, r); private void onRevertResult(DrawableHitObject d, JudgementResult r) => RevertResult?.Invoke(d, r); From f652eb9982b5047dad23f91ed5fb6098d593ca39 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 13:18:44 +0900 Subject: [PATCH 4566/6909] Remove GetDrawableRepresentation() override, add null hinting --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 21 +++++++++++---------- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 1a00346d6a..357f2d27d9 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -314,9 +314,6 @@ namespace osu.Game.Rulesets.UI } } - public sealed override DrawableHitObject GetDrawableRepresentation(HitObject hitObject) - => base.GetDrawableRepresentation(hitObject) ?? CreateDrawableRepresentation((TObject)hitObject); - /// /// Creates a DrawableHitObject from a HitObject. /// @@ -552,7 +549,7 @@ namespace osu.Game.Rulesets.UI /// /// Registers a pool with this which is to be used whenever - /// representations are requested for the given type (via ). + /// representations are requested for the given type (via ). /// /// The number of drawables to be prepared for initial consumption. /// An optional maximum size after which the pool will no longer be expanded. @@ -574,16 +571,18 @@ namespace osu.Game.Rulesets.UI /// An optional maximum size after which the pool will no longer be expanded. /// The type of retrievable from this pool. /// The . + [NotNull] protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) where TDrawable : DrawableHitObject, new() => new DrawablePool(initialSize, maximumSize); /// - /// Retrieves the drawable representation of a . + /// Attempts to retrieve the poolable representation of a . /// - /// The to retrieve the drawable representation of. - /// The representing . - public virtual DrawableHitObject GetDrawableRepresentation(HitObject hitObject) + /// The to retrieve the representation of. + /// The representing , or null if no poolable representation exists. + [CanBeNull] + public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject) { if (!pools.TryGetValue(hitObject.GetType(), out var pool)) return null; @@ -612,14 +611,16 @@ namespace osu.Game.Rulesets.UI /// /// The to create the entry for. /// The . - protected abstract HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject); + [NotNull] + protected abstract HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject); /// /// Retrieves or creates the for a given . /// /// The to retrieve or create the for. /// The for . - protected HitObjectLifetimeEntry GetLifetimeEntry(HitObject hitObject) + [NotNull] + protected HitObjectLifetimeEntry GetLifetimeEntry([NotNull] HitObject hitObject) { if (lifetimeEntries.TryGetValue(hitObject, out var entry)) return entry; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index ff358e2e75..d55288b978 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.UI { Debug.Assert(!drawableMap.ContainsKey(entry)); - var drawable = drawableRuleset.GetDrawableRepresentation(entry.HitObject); + var drawable = drawableRuleset.GetPooledDrawableRepresentation(entry.HitObject); drawable.OnNewResult += onNewResult; drawable.OnRevertResult += onRevertResult; From 5dbbe11fc668b3a54ae2be4a190479ec5aa42126 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 14:04:16 +0900 Subject: [PATCH 4567/6909] Remove PoolHitObjects, use return value of CreateDrawableRepresentation() instead --- .../Gameplay/TestScenePoolingRuleset.cs | 2 - osu.Game/Rulesets/UI/DrawableRuleset.cs | 49 +++++++++++-------- osu.Game/Rulesets/UI/Playfield.cs | 22 ++++++--- 3 files changed, 44 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index bb09aec416..c3ae753eae 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -140,8 +140,6 @@ namespace osu.Game.Tests.Visual.Gameplay RegisterPool(PoolSize); } - protected override bool PoolHitObjects => true; - protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 357f2d27d9..ce4cef4977 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -242,29 +242,38 @@ namespace osu.Game.Rulesets.UI /// The to add. public void AddHitObject(TObject hitObject) { - if (PoolHitObjects) - Playfield.Add(GetLifetimeEntry(hitObject)); + var drawableRepresentation = CreateDrawableRepresentation(hitObject); + + // If a drawable representation exists, use it, otherwise assume the hitobject is being pooled. + if (drawableRepresentation != null) + Playfield.Add(drawableRepresentation); else - Playfield.Add(CreateDrawableRepresentation(hitObject)); + Playfield.Add(GetLifetimeEntry(hitObject)); } /// - /// Removes a from this . + /// Removes a from this . /// /// /// This does not remove the from the beatmap. /// /// The to remove. - public void RemoveHitObject(TObject hitObject) + public bool RemoveHitObject(TObject hitObject) { - if (PoolHitObjects) - Playfield.Remove(GetLifetimeEntry(hitObject)); - else - { - var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject); - if (drawableObject != null) - Playfield.Remove(drawableObject); - } + var entry = GetLifetimeEntry(hitObject); + + // May have been newly-created by the above call - remove it anyway. + RemoveLifetimeEntry(hitObject); + + if (Playfield.Remove(entry)) + return true; + + // If the entry was not removed from the playfield, assume the hitobject is not being pooled and attempt a direct removal. + var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject); + if (drawableObject != null) + return Playfield.Remove(drawableObject); + + return false; } protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) @@ -539,14 +548,6 @@ namespace osu.Game.Rulesets.UI private readonly Dictionary pools = new Dictionary(); private readonly Dictionary lifetimeEntries = new Dictionary(); - /// - /// Whether this should retrieve pooled s. - /// - /// - /// Pools must be registered with this via in order for s to be retrieved. - /// - protected virtual bool PoolHitObjects => false; - /// /// Registers a pool with this which is to be used whenever /// representations are requested for the given type (via ). @@ -627,6 +628,12 @@ namespace osu.Game.Rulesets.UI return lifetimeEntries[hitObject] = CreateLifetimeEntry(hitObject); } + + /// + /// Removes the for a . + /// + /// The to remove the for. + internal void RemoveLifetimeEntry([NotNull] HitObject hitObject) => lifetimeEntries.Remove(hitObject); } public class BeatmapInvalidForRulesetException : ArgumentException diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index cdaf9364af..7c47f046dc 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -143,7 +143,7 @@ namespace osu.Game.Rulesets.UI private readonly Dictionary lifetimeEntryMap = new Dictionary(); /// - /// Adds a to this . + /// Adds a for a pooled to this . /// /// The controlling the lifetime of the . public void Add(HitObjectLifetimeEntry entry) @@ -154,14 +154,24 @@ namespace osu.Game.Rulesets.UI } /// - /// Removes a to this . + /// Removes a for a pooled from this . /// /// The controlling the lifetime of the . - public void Remove(HitObjectLifetimeEntry entry) + /// Whether the was successfully removed. + public bool Remove(HitObjectLifetimeEntry entry) { - if (HitObjectContainer.Remove(entry)) - OnHitObjectRemoved(entry.HitObject); - lifetimeEntryMap.Remove(entry.HitObject); + if (lifetimeEntryMap.Remove(entry.HitObject)) + { + HitObjectContainer.Remove(entry); + return true; + } + + bool removedFromNested = false; + + if (nestedPlayfields.IsValueCreated) + removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(entry)); + + return removedFromNested; } /// From 1f8d376b85d28a2dbdc5ff373e1ebecb99307c32 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 14:17:12 +0900 Subject: [PATCH 4568/6909] Replace CreatePool() with non-virtual RegisterPool() overload --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index ce4cef4977..983667a4dd 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -559,24 +559,23 @@ namespace osu.Game.Rulesets.UI protected void RegisterPool(int initialSize, int? maximumSize = null) where TObject : HitObject where TDrawable : DrawableHitObject, new() + => RegisterPool(new DrawablePool(initialSize, maximumSize)); + + /// + /// Registers a pool with this which is to be used whenever + /// representations are requested for the given type (via ). + /// + /// The to register. + /// The type. + /// The receiver for s. + protected void RegisterPool([NotNull] DrawablePool pool) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() { - var pool = CreatePool(initialSize, maximumSize); pools[typeof(TObject)] = pool; AddInternal(pool); } - /// - /// Creates the to retrieve s of the given type from. - /// - /// The number of hitobject to be prepared for initial consumption. - /// An optional maximum size after which the pool will no longer be expanded. - /// The type of retrievable from this pool. - /// The . - [NotNull] - protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) - where TDrawable : DrawableHitObject, new() - => new DrawablePool(initialSize, maximumSize); - /// /// Attempts to retrieve the poolable representation of a . /// From 16e4e8d032dc44dc97f606922879114e4299ff43 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 14:54:33 +0900 Subject: [PATCH 4569/6909] Fix possible nullref --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index d55288b978..7315ce61e7 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Performance; @@ -71,6 +72,9 @@ namespace osu.Game.Rulesets.UI Debug.Assert(!drawableMap.ContainsKey(entry)); var drawable = drawableRuleset.GetPooledDrawableRepresentation(entry.HitObject); + if (drawable == null) + throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); + drawable.OnNewResult += onNewResult; drawable.OnRevertResult += onRevertResult; From 653f5bce676d70ff2ef63e435f618dc039864102 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:00:58 +0900 Subject: [PATCH 4570/6909] Reword xmldocs --- osu.Game/Rulesets/UI/DrawableRuleset.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 983667a4dd..0429936d8e 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -549,11 +549,15 @@ namespace osu.Game.Rulesets.UI private readonly Dictionary lifetimeEntries = new Dictionary(); /// - /// Registers a pool with this which is to be used whenever + /// Registers a default pool with this which is to be used whenever /// representations are requested for the given type (via ). /// - /// The number of drawables to be prepared for initial consumption. - /// An optional maximum size after which the pool will no longer be expanded. + /// The number of s to be initially stored in the pool. + /// + /// The maximum number of s that can be stored in the pool. + /// If this limit is exceeded, every subsequent will be created anew instead of being retrieved from the pool, + /// until some of the existing s are returned to the pool. + /// /// The type. /// The receiver for s. protected void RegisterPool(int initialSize, int? maximumSize = null) @@ -562,7 +566,7 @@ namespace osu.Game.Rulesets.UI => RegisterPool(new DrawablePool(initialSize, maximumSize)); /// - /// Registers a pool with this which is to be used whenever + /// Registers a custom pool with this which is to be used whenever /// representations are requested for the given type (via ). /// /// The to register. From 4e4323595557813493ad09bd2e1175269fe59f0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:33:49 +0900 Subject: [PATCH 4571/6909] Fix double bind leading to test failures --- osu.Game/Rulesets/UI/Playfield.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 7c47f046dc..9df3bb10ce 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -120,10 +120,6 @@ namespace osu.Game.Rulesets.UI public virtual void Add(DrawableHitObject h) { HitObjectContainer.Add(h); - - h.OnNewResult += (d, r) => NewResult?.Invoke(d, r); - h.OnRevertResult += (d, r) => RevertResult?.Invoke(d, r); - OnHitObjectAdded(h.HitObject); } From 72a6b756268f4e6218814a706d3dfb84f266b1e8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:34:51 +0900 Subject: [PATCH 4572/6909] Add back removed event --- osu.Game/Rulesets/UI/Playfield.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 9df3bb10ce..5e5d17a400 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -159,6 +159,7 @@ namespace osu.Game.Rulesets.UI if (lifetimeEntryMap.Remove(entry.HitObject)) { HitObjectContainer.Remove(entry); + OnHitObjectRemoved(entry.HitObject); return true; } From 974390bda77d551107cb4f08baa63c1a8c8ae1d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:35:58 +0900 Subject: [PATCH 4573/6909] Make Add() + Remove() virtual --- osu.Game/Rulesets/UI/Playfield.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 5e5d17a400..d1cb8ecbbd 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -142,7 +142,7 @@ namespace osu.Game.Rulesets.UI /// Adds a for a pooled to this . /// /// The controlling the lifetime of the . - public void Add(HitObjectLifetimeEntry entry) + public virtual void Add(HitObjectLifetimeEntry entry) { HitObjectContainer.Add(entry); lifetimeEntryMap[entry.HitObject] = entry; @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.UI /// /// The controlling the lifetime of the . /// Whether the was successfully removed. - public bool Remove(HitObjectLifetimeEntry entry) + public virtual bool Remove(HitObjectLifetimeEntry entry) { if (lifetimeEntryMap.Remove(entry.HitObject)) { From 39d37c4779b82c1069a1cc89b4d0342ceb34b686 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:24:45 +0900 Subject: [PATCH 4574/6909] Add support for nested hitobject pooling --- .../Objects/Drawables/DrawableHitObject.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 244cf831c3..2dba83f2be 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Configuration; +using osu.Game.Rulesets.UI; using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables @@ -126,6 +127,9 @@ namespace osu.Game.Rulesets.Objects.Drawables [CanBeNull] private HitObjectLifetimeEntry lifetimeEntry; + [Resolved(CanBeNull = true)] + private DrawableRuleset drawableRuleset { get; set; } + /// /// Creates a new . /// @@ -195,7 +199,9 @@ 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()}."); + var drawableNested = drawableRuleset?.GetPooledDrawableRepresentation(h) + ?? CreateNestedHitObject(h) + ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); drawableNested.OnNewResult += onNewResult; drawableNested.OnRevertResult += onRevertResult; @@ -203,6 +209,8 @@ namespace osu.Game.Rulesets.Objects.Drawables nestedHitObjects.Value.Add(drawableNested); AddNestedHitObject(drawableNested); + + drawableNested.OnParentReceived(this); } StartTimeBindable.BindTo(HitObject.StartTimeBindable); @@ -291,6 +299,14 @@ namespace osu.Game.Rulesets.Objects.Drawables { } + /// + /// Invoked when this receives a new parenting . + /// + /// The parenting . + protected virtual void OnParentReceived(DrawableHitObject parent) + { + } + /// /// Invoked by the base to populate samples, once on initial load and potentially again on any change to the samples collection. /// From bf72961959c081ca9254b4183cf19e85a89c845c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 00:22:06 +0900 Subject: [PATCH 4575/6909] Add top-level osu! hitobject pooling --- .../Objects/Drawables/DrawableHitCircle.cs | 5 ++ .../Objects/Drawables/DrawableSlider.cs | 5 ++ .../Objects/Drawables/DrawableSpinner.cs | 5 ++ .../UI/DrawableOsuRuleset.cs | 48 ++++++++++------- osu.Game.Rulesets.Osu/UI/OsuDrawablePool.cs | 33 ++++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 54 ++++++++++--------- 6 files changed, 108 insertions(+), 42 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/OsuDrawablePool.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 77d24db084..2e63160d36 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -31,6 +31,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container scaleContainer; private InputManager inputManager; + public DrawableHitCircle() + : this(null) + { + } + public DrawableHitCircle([CanBeNull] HitCircle h = null) : base(h) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 3f91a31066..05e4587307 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -41,6 +41,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container tickContainer; private Container repeatContainer; + public DrawableSlider() + : this(null) + { + } + public DrawableSlider([CanBeNull] Slider s = null) : base(s) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index eb125969b0..f04a914fe3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -33,6 +33,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Bindable isSpinning; private bool spinnerFrequencyModulate; + public DrawableSpinner() + : this(null) + { + } + public DrawableSpinner([CanBeNull] Spinner s = null) : base(s) { diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index b2299398e1..1c119e2e95 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -4,12 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Pooling; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -24,11 +26,28 @@ namespace osu.Game.Rulesets.Osu.UI { protected new OsuRulesetConfigManager Config => (OsuRulesetConfigManager)base.Config; + public new OsuPlayfield Playfield => (OsuPlayfield)base.Playfield; + public DrawableOsuRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods = null) : base(ruleset, beatmap, mods) { } + protected override bool PoolHitObjects => true; + + [BackgroundDependencyLoader] + private void load() + { + RegisterPool(10, 100); + RegisterPool(10, 100); + RegisterPool(2, 20); + } + + protected override DrawablePool CreatePool(int initialSize, int? maximumSize = null) + => new OsuDrawablePool(Playfield.CheckHittable, Playfield.OnHitObjectLoaded, initialSize, maximumSize); + + protected override HitObjectLifetimeEntry CreateLifetimeEntry(OsuHitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor protected override Playfield CreatePlayfield() => new OsuPlayfield(); @@ -39,23 +58,6 @@ namespace osu.Game.Rulesets.Osu.UI protected override ResumeOverlay CreateResumeOverlay() => new OsuResumeOverlay(); - public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) - { - switch (h) - { - case HitCircle circle: - return new DrawableHitCircle(circle); - - case Slider slider: - return new DrawableSlider(slider); - - case Spinner spinner: - return new DrawableSpinner(spinner); - } - - return null; - } - protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay); protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new OsuReplayRecorder(replay); @@ -70,5 +72,15 @@ namespace osu.Game.Rulesets.Osu.UI return 0; } } + + private class OsuHitObjectLifetimeEntry : HitObjectLifetimeEntry + { + public OsuHitObjectLifetimeEntry(HitObject hitObject) + : base(hitObject) + { + } + + protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt; + } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuDrawablePool.cs b/osu.Game.Rulesets.Osu/UI/OsuDrawablePool.cs new file mode 100644 index 0000000000..148146f25a --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OsuDrawablePool.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 osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.UI +{ + public class OsuDrawablePool : DrawablePool + where T : DrawableHitObject, new() + { + private readonly Func checkHittable; + private readonly Action onLoaded; + + public OsuDrawablePool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) + : base(initialSize, maximumSize) + { + this.checkHittable = checkHittable; + this.onLoaded = onLoaded; + } + + protected override T CreateNewDrawable() => base.CreateNewDrawable().With(o => + { + var osuObject = (DrawableOsuHitObject)(object)o; + + osuObject.CheckHittable = checkHittable; + osuObject.OnLoadComplete += onLoaded; + }); + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 321eeeab65..5d59a6ff38 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -13,6 +13,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; 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.Connections; using osu.Game.Rulesets.Osu.Scoring; @@ -26,6 +27,8 @@ namespace osu.Game.Rulesets.Osu.UI { public class OsuPlayfield : Playfield { + public readonly Func CheckHittable; + private readonly PlayfieldBorder playfieldBorder; private readonly ProxyContainer approachCircles; private readonly ProxyContainer spinnerProxies; @@ -78,6 +81,7 @@ namespace osu.Game.Rulesets.Osu.UI }; hitPolicy = new OrderedHitPolicy(HitObjectContainer); + CheckHittable = hitPolicy.IsHittable; var hitWindows = new OsuHitWindows(); @@ -85,6 +89,8 @@ namespace osu.Game.Rulesets.Osu.UI poolDictionary.Add(result, new DrawableJudgementPool(result)); AddRangeInternal(poolDictionary.Values); + + NewResult += onNewResult; } [BackgroundDependencyLoader(true)] @@ -93,37 +99,37 @@ namespace osu.Game.Rulesets.Osu.UI config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); } - public override void Add(DrawableHitObject h) + protected override void OnHitObjectAdded(HitObject hitObject) { - DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; - - h.OnNewResult += onNewResult; - h.OnLoadComplete += d => - { - if (d is DrawableSpinner) - spinnerProxies.Add(d.CreateProxy()); - - if (d is IDrawableHitObjectWithProxiedApproach c) - approachCircles.Add(c.ProxiedLayer.CreateProxy()); - }; - - base.Add(h); - - osuHitObject.CheckHittable = hitPolicy.IsHittable; - - followPoints.AddFollowPoints(osuHitObject.HitObject); + base.OnHitObjectAdded(hitObject); + followPoints.AddFollowPoints((OsuHitObject)hitObject); } - public override bool Remove(DrawableHitObject h) + protected override void OnHitObjectRemoved(HitObject hitObject) { - DrawableOsuHitObject osuHitObject = (DrawableOsuHitObject)h; + base.OnHitObjectRemoved(hitObject); + followPoints.RemoveFollowPoints((OsuHitObject)hitObject); + } - bool result = base.Remove(h); + public void OnHitObjectLoaded(Drawable drawable) + { + switch (drawable) + { + case DrawableSliderHead _: + case DrawableSliderTail _: + case DrawableSliderTick _: + case DrawableSliderRepeat _: + case DrawableSpinnerTick _: + break; - if (result) - followPoints.RemoveFollowPoints(osuHitObject.HitObject); + case DrawableSpinner _: + spinnerProxies.Add(drawable.CreateProxy()); + break; - return result; + case IDrawableHitObjectWithProxiedApproach approach: + approachCircles.Add(approach.ProxiedLayer.CreateProxy()); + break; + } } private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) From 1ea526b5ef72ac020ecc3fd3d589cbd23c2e9ff3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 14:19:26 +0900 Subject: [PATCH 4576/6909] Adjust pooling implementation with branch changes --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 1c119e2e95..86c7305439 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -12,6 +12,7 @@ using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; @@ -33,17 +34,21 @@ namespace osu.Game.Rulesets.Osu.UI { } - protected override bool PoolHitObjects => true; - [BackgroundDependencyLoader] private void load() { - RegisterPool(10, 100); - RegisterPool(10, 100); - RegisterPool(2, 20); + registerPool(10, 100); + registerPool(10, 100); + registerPool(2, 20); } - protected override DrawablePool CreatePool(int initialSize, int? maximumSize = null) + private void registerPool(int initialSize, int? maximumSize = null) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + => RegisterPool(CreatePool(initialSize, maximumSize)); + + protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) + where TDrawable : DrawableHitObject, new() => new OsuDrawablePool(Playfield.CheckHittable, Playfield.OnHitObjectLoaded, initialSize, maximumSize); protected override HitObjectLifetimeEntry CreateLifetimeEntry(OsuHitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); From 3f78d81386db5deb57d42f55dcd841ead8d68a4c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:59:48 +0900 Subject: [PATCH 4577/6909] Add nested osu! hitobject pooling --- .../Objects/Drawables/DrawableOsuHitObject.cs | 2 +- .../Objects/Drawables/DrawableSlider.cs | 18 +++----- .../Objects/Drawables/DrawableSliderHead.cs | 44 +++++++++++++++---- .../Objects/Drawables/DrawableSliderRepeat.cs | 31 +++++++++---- .../Objects/Drawables/DrawableSliderTail.cs | 12 +++-- .../Objects/Drawables/DrawableSliderTick.cs | 14 +++++- .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Drawables/DrawableSpinnerBonusTick.cs | 5 +++ .../Objects/Drawables/DrawableSpinnerTick.cs | 5 +++ .../UI/DrawableOsuRuleset.cs | 8 ++++ 10 files changed, 106 insertions(+), 35 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index d17bf93fa0..bcaf73d34f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private OsuInputManager osuActionInputManager; internal OsuInputManager OsuActionInputManager => osuActionInputManager ??= GetContainingInputManager() as OsuInputManager; - protected virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength); + public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength); protected override void UpdateInitialTransforms() { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 05e4587307..5bbdc5ee7b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -164,10 +164,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.ClearNestedHitObjects(); - headContainer.Clear(); - tailContainer.Clear(); - repeatContainer.Clear(); - tickContainer.Clear(); + headContainer.Clear(false); + tailContainer.Clear(false); + repeatContainer.Clear(false); + tickContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) @@ -178,17 +178,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return new DrawableSliderTail(tail); case SliderHeadCircle head: - return new DrawableSliderHead(HitObject, head) - { - OnShake = Shake, - CheckHittable = (d, t) => CheckHittable?.Invoke(d, t) ?? true - }; + return new DrawableSliderHead(head); case SliderTick tick: - return new DrawableSliderTick(tick) { Position = tick.Position - HitObject.Position }; + return new DrawableSliderTick(tick); case SliderRepeat repeat: - return new DrawableSliderRepeat(repeat, this) { Position = repeat.Position - HitObject.Position }; + return new DrawableSliderRepeat(repeat); } return base.CreateNestedHitObject(hitObject); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 49ed9f12e3..fd0f35d20d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -4,6 +4,8 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -14,21 +16,43 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; - private readonly Slider slider; + private DrawableSlider drawableSlider; - public DrawableSliderHead(Slider slider, SliderHeadCircle h) + private Slider slider => drawableSlider?.HitObject; + + public DrawableSliderHead() + { + } + + public DrawableSliderHead(SliderHeadCircle h) : base(h) { - this.slider = slider; } [BackgroundDependencyLoader] private void load() { - pathVersion.BindTo(slider.Path.Version); - PositionBindable.BindValueChanged(_ => updatePosition()); - pathVersion.BindValueChanged(_ => updatePosition(), true); + pathVersion.BindValueChanged(_ => updatePosition()); + } + + protected override void OnFree(HitObject hitObject) + { + base.OnFree(hitObject); + + pathVersion.UnbindFrom(drawableSlider.PathVersion); + } + + protected override void OnParentReceived(DrawableHitObject parent) + { + base.OnParentReceived(parent); + + drawableSlider = (DrawableSlider)parent; + + pathVersion.BindTo(drawableSlider.PathVersion); + + OnShake = drawableSlider.Shake; + CheckHittable = (d, t) => drawableSlider.CheckHittable?.Invoke(d, t) ?? true; } protected override void Update() @@ -44,8 +68,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public Action OnShake; - protected override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength); + public override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength); - private void updatePosition() => Position = HitObject.Position - slider.Position; + private void updatePosition() + { + if (slider != null) + Position = HitObject.Position - slider.Position; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 9c382bd0a7..0735d48ae1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -16,8 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderRepeat : DrawableOsuHitObject, ITrackSnaking { - private readonly SliderRepeat sliderRepeat; - private readonly DrawableSlider drawableSlider; + public new SliderRepeat HitObject => (SliderRepeat)base.HitObject; private double animDuration; @@ -27,11 +26,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; - public DrawableSliderRepeat(SliderRepeat sliderRepeat, DrawableSlider drawableSlider) + private DrawableSlider drawableSlider; + + public DrawableSliderRepeat() + : base(null) + { + } + + public DrawableSliderRepeat(SliderRepeat sliderRepeat) : base(sliderRepeat) { - this.sliderRepeat = sliderRepeat; - this.drawableSlider = drawableSlider; } [BackgroundDependencyLoader] @@ -53,18 +57,27 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } }; - ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); + } + + protected override void OnParentReceived(DrawableHitObject parent) + { + base.OnParentReceived(parent); + + drawableSlider = (DrawableSlider)parent; + + Position = HitObject.Position - drawableSlider.Position; } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (sliderRepeat.StartTime <= Time.Current) + if (HitObject.StartTime <= Time.Current) ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void UpdateInitialTransforms() { - animDuration = Math.Min(300, sliderRepeat.SpanDuration); + animDuration = Math.Min(300, HitObject.SpanDuration); this.Animate( d => d.FadeIn(animDuration), @@ -100,7 +113,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // 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; + bool isRepeatAtEnd = HitObject.RepeatIndex % 2 == 0; List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; Position = isRepeatAtEnd ? end : start; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index 3be5983c57..eff72168ee 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderTail : DrawableOsuHitObject, IRequireTracking, ITrackSnaking { - private readonly SliderTailCircle tailCircle; + public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; /// /// The judgement text is provided by the . @@ -25,10 +25,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private SkinnableDrawable circlePiece; private Container scaleContainer; + public DrawableSliderTail() + : base(null) + { + } + public DrawableSliderTail(SliderTailCircle tailCircle) : base(tailCircle) { - this.tailCircle = tailCircle; } [BackgroundDependencyLoader] @@ -52,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }, }; - ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } protected override void UpdateInitialTransforms() @@ -92,6 +96,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } public void UpdateSnakingPosition(Vector2 start, Vector2 end) => - Position = tailCircle.RepeatIndex % 2 == 0 ? end : start; + Position = HitObject.RepeatIndex % 2 == 0 ? end : start; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index 2af51ea486..faccf5d4d1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -24,6 +24,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private SkinnableDrawable scaleContainer; + public DrawableSliderTick() + : base(null) + { + } + public DrawableSliderTick(SliderTick sliderTick) : base(sliderTick) { @@ -54,7 +59,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre, }; - ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue), true); + ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); + } + + protected override void OnParentReceived(DrawableHitObject parent) + { + base.OnParentReceived(parent); + + Position = HitObject.Position - ((DrawableSlider)parent).HitObject.Position; } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index f04a914fe3..6b33517c33 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - ticks.Clear(); + ticks.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs index 2e1c07c4c6..ffeb14b0a8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerBonusTick.cs @@ -5,6 +5,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerBonusTick : DrawableSpinnerTick { + public DrawableSpinnerBonusTick() + : base(null) + { + } + public DrawableSpinnerBonusTick(SpinnerBonusTick spinnerTick) : base(spinnerTick) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index e9cede1398..fc9a7c00e6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -7,6 +7,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public override bool DisplayResult => false; + public DrawableSpinnerTick() + : base(null) + { + } + public DrawableSpinnerTick(SpinnerTick spinnerTick) : base(spinnerTick) { diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 86c7305439..c89f138bcd 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -38,8 +38,16 @@ namespace osu.Game.Rulesets.Osu.UI private void load() { registerPool(10, 100); + registerPool(10, 100); + registerPool(10, 100); + registerPool(10, 100); + registerPool(10, 100); + registerPool(5, 50); + registerPool(2, 20); + registerPool(10, 100); + registerPool(10, 100); } private void registerPool(int initialSize, int? maximumSize = null) From cf91962865aa659859fb8f73f2f994c8f6027018 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 16:58:40 +0900 Subject: [PATCH 4578/6909] Fix test failures due to on-the-fly starttime changes --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 7315ce61e7..1bb1fd4983 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -57,6 +57,14 @@ namespace osu.Game.Rulesets.UI lifetimeManager.EntryBecameDead += entryBecameDead; } + protected override void LoadComplete() + { + base.LoadComplete(); + + // Application of hitobject during load() may have changed their start times, so ensure the correct sorting order. + SortInternal(); + } + #region Pooling support public void Add(HitObjectLifetimeEntry entry) => lifetimeManager.AddEntry(entry); @@ -163,7 +171,12 @@ namespace osu.Game.Rulesets.UI private void bindStartTime(DrawableHitObject hitObject) { var bindable = hitObject.StartTimeBindable.GetBoundCopy(); - bindable.BindValueChanged(_ => SortInternal()); + + bindable.BindValueChanged(_ => + { + if (IsLoaded) + SortInternal(); + }); startTimeMap[hitObject] = bindable; } From 25af091409142d702041f5a5bd15bea093c78897 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 12 Nov 2020 17:03:42 +0900 Subject: [PATCH 4579/6909] Fix storyboard animations of very old beatmaps playing too slow Closes https://github.com/ppy/osu/issues/10772. --- osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs | 2 -- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 2 ++ osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs | 12 +++++++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 442be6e837..37ab489da5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -16,8 +16,6 @@ namespace osu.Game.Beatmaps.Formats { public class LegacyBeatmapDecoder : LegacyDecoder { - public const int LATEST_VERSION = 14; - private Beatmap beatmap; private ConvertHitObjectParser parser; diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 7b377e481f..de4dc8cdc8 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -16,6 +16,8 @@ namespace osu.Game.Beatmaps.Formats public abstract class LegacyDecoder : Decoder where T : new() { + public const int LATEST_VERSION = 14; + protected readonly int FormatVersion; protected LegacyDecoder(int version) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 8d8ca523d5..9a244c8bb2 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using osu.Framework.Graphics; using osu.Framework.Utils; using osu.Game.Beatmaps.Legacy; @@ -23,15 +24,15 @@ namespace osu.Game.Beatmaps.Formats private readonly Dictionary variables = new Dictionary(); - public LegacyStoryboardDecoder() - : base(0) + public LegacyStoryboardDecoder(int version = LATEST_VERSION) + : base(version) { } public static void Register() { // note that this isn't completely correct - AddDecoder(@"osu file format v", m => new LegacyStoryboardDecoder()); + AddDecoder(@"osu file format v", m => new LegacyStoryboardDecoder(Parsing.ParseInt(m.Split('v').Last()))); AddDecoder(@"[Events]", m => new LegacyStoryboardDecoder()); SetFallbackDecoder(() => new LegacyStoryboardDecoder()); } @@ -133,6 +134,11 @@ namespace osu.Game.Beatmaps.Formats var y = Parsing.ParseFloat(split[5], Parsing.MAX_COORDINATE_VALUE); var frameCount = Parsing.ParseInt(split[6]); var frameDelay = Parsing.ParseDouble(split[7]); + + if (FormatVersion < 6) + // this is random as hell but taken straight from osu-stable. + frameDelay = Math.Round(0.015 * frameDelay) * 1.186 * (1000 / 60f); + 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); From e44a8b3934d769e8455e306e18949be30ee89227 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 17:07:20 +0900 Subject: [PATCH 4580/6909] Resort as early as possible --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 1bb1fd4983..4164681ffc 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -57,11 +57,11 @@ namespace osu.Game.Rulesets.UI lifetimeManager.EntryBecameDead += entryBecameDead; } - protected override void LoadComplete() + protected override void LoadAsyncComplete() { - base.LoadComplete(); + base.LoadAsyncComplete(); - // Application of hitobject during load() may have changed their start times, so ensure the correct sorting order. + // Application of hitobjects during load() may have changed their start times, so ensure the correct sorting order. SortInternal(); } @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.UI bindable.BindValueChanged(_ => { - if (IsLoaded) + if (LoadState >= LoadState.Ready) SortInternal(); }); From 0ae6f8229171c215bf441385e6d80287eecf096a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Nov 2020 21:46:58 +0100 Subject: [PATCH 4581/6909] Fix incorrect fade of slider ends after a rewind --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 46 +++++++++++++--------- 1 file changed, 28 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index cf7faca9b9..84a335750a 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Graphics; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -46,9 +47,6 @@ namespace osu.Game.Rulesets.Osu.Mods applyFadeInAdjustment(nested); } - private double lastSliderHeadFadeOutStartTime; - private double lastSliderHeadFadeOutDuration; - protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) { base.ApplyIncreasedVisibilityState(hitObject, state); @@ -78,33 +76,24 @@ namespace osu.Game.Rulesets.Osu.Mods { case DrawableSliderTail sliderTail: // use stored values from head circle to achieve same fade sequence. - fadeOutDuration = lastSliderHeadFadeOutDuration; - fadeOutStartTime = lastSliderHeadFadeOutStartTime; + var tailFadeOutParameters = getFadeOutParametersFromSliderHead(h); - using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) - sliderTail.FadeOut(fadeOutDuration); + using (drawable.BeginAbsoluteSequence(tailFadeOutParameters.startTime, true)) + sliderTail.FadeOut(tailFadeOutParameters.duration); break; case DrawableSliderRepeat sliderRepeat: // use stored values from head circle to achieve same fade sequence. - fadeOutDuration = lastSliderHeadFadeOutDuration; - fadeOutStartTime = lastSliderHeadFadeOutStartTime; + var repeatFadeOutParameters = getFadeOutParametersFromSliderHead(h); - using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) + using (drawable.BeginAbsoluteSequence(repeatFadeOutParameters.startTime, true)) // only apply to circle piece – reverse arrow is not affected by hidden. - sliderRepeat.CirclePiece.FadeOut(fadeOutDuration); + sliderRepeat.CirclePiece.FadeOut(repeatFadeOutParameters.duration); break; case DrawableHitCircle circle: - - if (circle is DrawableSliderHead) - { - lastSliderHeadFadeOutDuration = fadeOutDuration; - lastSliderHeadFadeOutStartTime = fadeOutStartTime; - } - Drawable fadeTarget = circle; if (increaseVisibility) @@ -125,6 +114,8 @@ namespace osu.Game.Rulesets.Osu.Mods break; case DrawableSlider slider: + associateNestedSliderCirclesWithHead(slider.HitObject); + using (slider.BeginAbsoluteSequence(fadeOutStartTime, true)) slider.Body.FadeOut(longFadeDuration, Easing.Out); @@ -149,5 +140,24 @@ namespace osu.Game.Rulesets.Osu.Mods break; } } + + private readonly Dictionary correspondingSliderHeadForObject = new Dictionary(); + + private void associateNestedSliderCirclesWithHead(Slider slider) + { + var sliderHead = slider.NestedHitObjects.Single(obj => obj is SliderHeadCircle); + + foreach (var nested in slider.NestedHitObjects) + { + if ((nested is SliderRepeat || nested is SliderEndCircle) && !correspondingSliderHeadForObject.ContainsKey(nested)) + correspondingSliderHeadForObject[nested] = (SliderHeadCircle)sliderHead; + } + } + + private (double startTime, double duration) getFadeOutParametersFromSliderHead(OsuHitObject h) + { + var sliderHead = correspondingSliderHeadForObject[h]; + return (sliderHead.StartTime - sliderHead.TimePreempt + sliderHead.TimeFadeIn, sliderHead.TimePreempt * fade_out_duration_multiplier); + } } } From a8c2b798ad5c71cc2dcdc3d061e54148297218ef Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 15:24:45 +0900 Subject: [PATCH 4582/6909] Add support for nested hitobject pooling --- .../Objects/Drawables/DrawableHitObject.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 244cf831c3..2dba83f2be 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -19,6 +19,7 @@ using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Configuration; +using osu.Game.Rulesets.UI; using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Drawables @@ -126,6 +127,9 @@ namespace osu.Game.Rulesets.Objects.Drawables [CanBeNull] private HitObjectLifetimeEntry lifetimeEntry; + [Resolved(CanBeNull = true)] + private DrawableRuleset drawableRuleset { get; set; } + /// /// Creates a new . /// @@ -195,7 +199,9 @@ 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()}."); + var drawableNested = drawableRuleset?.GetPooledDrawableRepresentation(h) + ?? CreateNestedHitObject(h) + ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); drawableNested.OnNewResult += onNewResult; drawableNested.OnRevertResult += onRevertResult; @@ -203,6 +209,8 @@ namespace osu.Game.Rulesets.Objects.Drawables nestedHitObjects.Value.Add(drawableNested); AddNestedHitObject(drawableNested); + + drawableNested.OnParentReceived(this); } StartTimeBindable.BindTo(HitObject.StartTimeBindable); @@ -291,6 +299,14 @@ namespace osu.Game.Rulesets.Objects.Drawables { } + /// + /// Invoked when this receives a new parenting . + /// + /// The parenting . + protected virtual void OnParentReceived(DrawableHitObject parent) + { + } + /// /// Invoked by the base to populate samples, once on initial load and potentially again on any change to the samples collection. /// From de31c1ea0cd3df825d65fae00dc6b644d3f16a9f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 17:58:39 +0900 Subject: [PATCH 4583/6909] Fix skinfallback test crashing on repeats --- osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 075bf314bc..856bfd7e80 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -125,6 +125,9 @@ namespace osu.Game.Rulesets.Osu.Tests { if (!enabled) return null; + if (component is OsuSkinComponent osuComponent && osuComponent.Component == OsuSkinComponents.SliderBody) + return null; + return new OsuSpriteText { Text = identifier, From 44aed19e4e0e1e4098e2f7d405ac10f252d77185 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 12:37:00 +0900 Subject: [PATCH 4584/6909] Fix mania notelock crashing with overlapping hitwindows --- .../TestSceneHoldNoteInput.cs | 2 +- .../TestSceneOutOfOrderHits.cs | 23 +++++++++++++++++++ .../Objects/Drawables/DrawableHoldNote.cs | 2 +- .../UI/OrderedHitPolicy.cs | 2 +- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 5cb1519196..6c9f184c2c 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests assertHeadJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); - assertNoteJudgement(HitResult.IgnoreHit); + assertNoteJudgement(HitResult.IgnoreMiss); } /// diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index e8c2472c3b..d699921307 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -54,6 +54,29 @@ namespace osu.Game.Rulesets.Mania.Tests } } + [Test] + public void TestMissAfterNextObjectStartTime() + { + var objects = new List + { + new HoldNote + { + StartTime = 1000, + EndTime = 1200, + }, + new HoldNote + { + StartTime = 1220, + EndTime = 1420 + } + }; + + performTest(objects, new List()); + + addJudgementAssert(objects[0], HitResult.IgnoreMiss); + addJudgementAssert(objects[1], HitResult.IgnoreMiss); + } + private void addJudgementAssert(ManiaHitObject hitObject, HitResult result) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judgement is {result}", diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index d9d740c145..3b3f72157a 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); endHold(); } diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs index 0f9cd48dd8..9bc577a81e 100644 --- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mania.UI /// The that was hit. public void HandleHit(DrawableHitObject hitObject) { - if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) + if (hitObject.IsHit && !IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) From ad38867b1d977e9a78abb12c5d685e3b1c9831ab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 12:53:32 +0900 Subject: [PATCH 4585/6909] Completely remove check as it can occur for hits too --- .../TestSceneOutOfOrderHits.cs | 51 +++++++++++++++++-- .../UI/OrderedHitPolicy.cs | 4 -- 2 files changed, 47 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index d699921307..86a142f2f6 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.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; @@ -55,19 +56,19 @@ namespace osu.Game.Rulesets.Mania.Tests } [Test] - public void TestMissAfterNextObjectStartTime() + public void TestHoldNoteMissAfterNextObjectStartTime() { var objects = new List { new HoldNote { StartTime = 1000, - EndTime = 1200, + EndTime = 1010, }, new HoldNote { - StartTime = 1220, - EndTime = 1420 + StartTime = 1020, + EndTime = 1030 } }; @@ -77,12 +78,54 @@ namespace osu.Game.Rulesets.Mania.Tests addJudgementAssert(objects[1], HitResult.IgnoreMiss); } + [Test] + public void TestHoldNoteReleasedHitAfterNextObjectStartTime() + { + var objects = new List + { + new HoldNote + { + StartTime = 1000, + EndTime = 1010, + }, + new HoldNote + { + StartTime = 1020, + EndTime = 1030 + } + }; + + var frames = new List + { + new ManiaReplayFrame(1000, ManiaAction.Key1), + new ManiaReplayFrame(1030), + new ManiaReplayFrame(1040, ManiaAction.Key1), + new ManiaReplayFrame(1050) + }; + + performTest(objects, frames); + + addJudgementAssert(objects[0], HitResult.IgnoreHit); + addJudgementAssert("first head", () => ((HoldNote)objects[0]).Head, HitResult.Perfect); + addJudgementAssert("first tail", () => ((HoldNote)objects[0]).Tail, HitResult.Perfect); + + addJudgementAssert(objects[1], HitResult.IgnoreHit); + addJudgementAssert("second head", () => ((HoldNote)objects[1]).Head, HitResult.Great); + addJudgementAssert("second tail", () => ((HoldNote)objects[1]).Tail, HitResult.Perfect); + } + private void addJudgementAssert(ManiaHitObject 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(ManiaHitObject hitObject, double offset) { AddAssert($"({hitObject.GetType().ReadableName()} @ {hitObject.StartTime}) judged at {offset}", diff --git a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs index 9bc577a81e..961858b62b 100644 --- a/osu.Game.Rulesets.Mania/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Mania/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 System; using System.Collections.Generic; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -44,9 +43,6 @@ namespace osu.Game.Rulesets.Mania.UI /// The that was hit. public void HandleHit(DrawableHitObject hitObject) { - if (hitObject.IsHit && !IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) - throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); - foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { if (obj.Judged) From 60f95e897d9c119e3492a5b9e14d687aaddfa0f4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 12:53:53 +0900 Subject: [PATCH 4586/6909] Revert unnecessary change --- osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs | 2 +- osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs | 4 ++-- osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 6c9f184c2c..5cb1519196 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Mania.Tests assertHeadJudgement(HitResult.Miss); assertTickJudgement(HitResult.LargeTickMiss); assertTailJudgement(HitResult.Miss); - assertNoteJudgement(HitResult.IgnoreMiss); + assertNoteJudgement(HitResult.IgnoreHit); } /// diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index 86a142f2f6..cecac38f70 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -74,8 +74,8 @@ namespace osu.Game.Rulesets.Mania.Tests performTest(objects, new List()); - addJudgementAssert(objects[0], HitResult.IgnoreMiss); - addJudgementAssert(objects[1], HitResult.IgnoreMiss); + addJudgementAssert(objects[0], HitResult.IgnoreHit); + addJudgementAssert(objects[1], HitResult.IgnoreHit); } [Test] diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 3b3f72157a..d9d740c145 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables { if (Tail.AllJudged) { - ApplyResult(r => r.Type = Tail.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = r.Judgement.MaxResult); endHold(); } From d9750fc043d7dc85151f365363909a855bf9cd33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 15:49:45 +0900 Subject: [PATCH 4587/6909] Remove duplicate instantiation of externalLinkOpener --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 6b768cc8fc..1a1ebcc2d4 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -681,7 +681,6 @@ namespace osu.Game loadComponentSingleFile(new AccountCreationOverlay(), topMostOverlayContent.Add, true); loadComponentSingleFile(new DialogOverlay(), topMostOverlayContent.Add, true); - loadComponentSingleFile(externalLinkOpener = new ExternalLinkOpener(), topMostOverlayContent.Add); chatOverlay.State.ValueChanged += state => channelManager.HighPollRate.Value = state.NewValue == Visibility.Visible; From 81c9663e763b4d0268bc41e6bfb09ac91e5b2442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 13:24:32 +0100 Subject: [PATCH 4588/6909] Move {-> Default}KiaiHitExplosion --- .../UI/{KiaiHitExplosion.cs => DefaultKiaiHitExplosion.cs} | 4 ++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) rename osu.Game.Rulesets.Taiko/UI/{KiaiHitExplosion.cs => DefaultKiaiHitExplosion.cs} (92%) diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs similarity index 92% rename from osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs rename to osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs index 067d390894..32c9f3ec4f 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs @@ -13,14 +13,14 @@ using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.UI { - public class KiaiHitExplosion : CircularContainer + public class DefaultKiaiHitExplosion : CircularContainer { public override bool RemoveWhenNotAlive => true; public readonly DrawableHitObject JudgedObject; private readonly HitType type; - public KiaiHitExplosion(DrawableHitObject judgedObject, HitType type) + public DefaultKiaiHitExplosion(DrawableHitObject judgedObject, HitType type) { JudgedObject = judgedObject; this.type = type; diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 120cf264c3..03895dfd68 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI public const float DEFAULT_HEIGHT = 178; private Container hitExplosionContainer; - private Container kiaiExplosionContainer; + private Container kiaiExplosionContainer; private JudgementContainer judgementContainer; private ScrollingHitObjectContainer drumRollHitContainer; internal Drawable HitTarget; @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.UI drumRollHitContainer = new DrumRollHitContainer() } }, - kiaiExplosionContainer = new Container + kiaiExplosionContainer = new Container { Name = "Kiai hit explosions", RelativeSizeAxes = Axes.Both, @@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Taiko.UI { hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) - kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); + kiaiExplosionContainer.Add(new DefaultKiaiHitExplosion(drawableObject, type)); } private class ProxyContainer : LifetimeManagementContainer From 00a486ab516ab6ac65471177c0f2dd3b36758d5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 13:35:49 +0100 Subject: [PATCH 4589/6909] Reintroduce KiaiHitExplosion as skinnable --- .../TaikoSkinComponents.cs | 1 + .../UI/DefaultKiaiHitExplosion.cs | 9 +--- .../UI/KiaiHitExplosion.cs | 47 +++++++++++++++++++ osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 6 +-- 4 files changed, 52 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs diff --git a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs index 132d8f8868..bf48898dd2 100644 --- a/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs +++ b/osu.Game.Rulesets.Taiko/TaikoSkinComponents.cs @@ -18,6 +18,7 @@ namespace osu.Game.Rulesets.Taiko TaikoExplosionMiss, TaikoExplosionOk, TaikoExplosionGreat, + TaikoExplosionKiai, Scroller, Mascot, } diff --git a/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs index 32c9f3ec4f..7ce8b016d5 100644 --- a/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/DefaultKiaiHitExplosion.cs @@ -8,7 +8,6 @@ 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.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.UI @@ -17,19 +16,13 @@ namespace osu.Game.Rulesets.Taiko.UI { public override bool RemoveWhenNotAlive => true; - public readonly DrawableHitObject JudgedObject; private readonly HitType type; - public DefaultKiaiHitExplosion(DrawableHitObject judgedObject, HitType type) + public DefaultKiaiHitExplosion(HitType type) { - JudgedObject = judgedObject; this.type = type; - Anchor = Anchor.CentreLeft; - Origin = Anchor.Centre; - RelativeSizeAxes = Axes.Both; - Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1); Blending = BlendingParameters.Additive; diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs new file mode 100644 index 0000000000..20900a9352 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.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.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.UI +{ + public class KiaiHitExplosion : Container + { + public override bool RemoveWhenNotAlive => true; + + [Cached(typeof(DrawableHitObject))] + public readonly DrawableHitObject JudgedObject; + + private readonly HitType hitType; + + private SkinnableDrawable skinnable; + + public override double LifetimeStart => skinnable.Drawable.LifetimeStart; + + public override double LifetimeEnd => skinnable.Drawable.LifetimeEnd; + + public KiaiHitExplosion(DrawableHitObject judgedObject, HitType hitType) + { + JudgedObject = judgedObject; + this.hitType = hitType; + + Anchor = Anchor.CentreLeft; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.Both; + Size = new Vector2(TaikoHitObject.DEFAULT_SIZE, 1); + } + + [BackgroundDependencyLoader] + private void load() + { + Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoExplosionKiai), _ => new DefaultKiaiHitExplosion(hitType)); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 03895dfd68..120cf264c3 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Taiko.UI public const float DEFAULT_HEIGHT = 178; private Container hitExplosionContainer; - private Container kiaiExplosionContainer; + private Container kiaiExplosionContainer; private JudgementContainer judgementContainer; private ScrollingHitObjectContainer drumRollHitContainer; internal Drawable HitTarget; @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Taiko.UI drumRollHitContainer = new DrumRollHitContainer() } }, - kiaiExplosionContainer = new Container + kiaiExplosionContainer = new Container { Name = "Kiai hit explosions", RelativeSizeAxes = Axes.Both, @@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Taiko.UI { hitExplosionContainer.Add(new HitExplosion(drawableObject, result)); if (drawableObject.HitObject.Kiai) - kiaiExplosionContainer.Add(new DefaultKiaiHitExplosion(drawableObject, type)); + kiaiExplosionContainer.Add(new KiaiHitExplosion(drawableObject, type)); } private class ProxyContainer : LifetimeManagementContainer From 646833a059991206691530888626748ce34e7501 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 14:07:39 +0100 Subject: [PATCH 4590/6909] Add test scene --- .../Skinning/TestSceneKiaiHitExplosion.cs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs new file mode 100644 index 0000000000..b558709592 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneKiaiHitExplosion.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.UI; + +namespace osu.Game.Rulesets.Taiko.Tests.Skinning +{ + [TestFixture] + public class TestSceneKiaiHitExplosion : TaikoSkinnableTestScene + { + [Test] + public void TestKiaiHits() + { + AddStep("rim hit", () => SetContents(() => getContentFor(createHit(HitType.Rim)))); + AddStep("centre hit", () => SetContents(() => getContentFor(createHit(HitType.Centre)))); + } + + private Drawable getContentFor(DrawableTestHit hit) + { + return new Container + { + RelativeSizeAxes = Axes.Both, + Child = new KiaiHitExplosion(hit, hit.HitObject.Type) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + } + + private DrawableTestHit createHit(HitType type) => new DrawableTestHit(new Hit { StartTime = Time.Current, Type = type }); + } +} From 21709ba4bce68efffe1e1a3dffcc6157ee2b2697 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 14:08:27 +0100 Subject: [PATCH 4591/6909] Do not lookup default kiai explosion if skin has own --- .../Skinning/TaikoLegacySkinTransformer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index c88480d18f..ddbf20b827 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -114,6 +114,13 @@ namespace osu.Game.Rulesets.Taiko.Skinning return null; + case TaikoSkinComponents.TaikoExplosionKiai: + // suppress the default kiai explosion if the skin brings its own sprites. + if (hasExplosion.Value) + return Drawable.Empty(); + + return null; + case TaikoSkinComponents.Scroller: if (GetTexture("taiko-slider") != null) return new LegacyTaikoScroller(); From 0d5cac89b3d2f3b6d4de66da1caafa2424935d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 10 Nov 2020 14:50:19 +0100 Subject: [PATCH 4592/6909] Explicitly set lifetime to ensure empty drawables are cleaned up --- .../Skinning/TaikoLegacySkinTransformer.cs | 3 ++- osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index ddbf20b827..880af3fbd8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -116,8 +116,9 @@ namespace osu.Game.Rulesets.Taiko.Skinning case TaikoSkinComponents.TaikoExplosionKiai: // suppress the default kiai explosion if the skin brings its own sprites. + // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. if (hasExplosion.Value) - return Drawable.Empty(); + return KiaiHitExplosion.EmptyExplosion(); return null; diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs index 20900a9352..326cb23897 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -43,5 +43,11 @@ namespace osu.Game.Rulesets.Taiko.UI { Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoExplosionKiai), _ => new DefaultKiaiHitExplosion(hitType)); } + + /// + /// Helper function to use when an explosion is not desired. + /// Lifetime is set to avoid accumulating empty drawables in the parent container. + /// + public static Drawable EmptyExplosion() => Empty().With(d => d.LifetimeEnd = double.MinValue); } } From 1f83769bb270b2a1e33d4baf038fa243a460d864 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 11 Nov 2020 09:11:33 +0100 Subject: [PATCH 4593/6909] Inline empty explosion in legacy transformer --- .../Skinning/TaikoLegacySkinTransformer.cs | 2 +- osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs index 880af3fbd8..96fb065e79 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning // suppress the default kiai explosion if the skin brings its own sprites. // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. if (hasExplosion.Value) - return KiaiHitExplosion.EmptyExplosion(); + return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue); return null; diff --git a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs index 326cb23897..20900a9352 100644 --- a/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/KiaiHitExplosion.cs @@ -43,11 +43,5 @@ namespace osu.Game.Rulesets.Taiko.UI { Child = skinnable = new SkinnableDrawable(new TaikoSkinComponent(TaikoSkinComponents.TaikoExplosionKiai), _ => new DefaultKiaiHitExplosion(hitType)); } - - /// - /// Helper function to use when an explosion is not desired. - /// Lifetime is set to avoid accumulating empty drawables in the parent container. - /// - public static Drawable EmptyExplosion() => Empty().With(d => d.LifetimeEnd = double.MinValue); } } From 8a2addbf3d56bb4aa07b92d3d305e32dd8a037fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 11 Nov 2020 18:03:04 +0900 Subject: [PATCH 4594/6909] 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 bbe8426316..5078fee1cf 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 8f0cc58594..405fb1a6ca 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index f766e0ec03..099ecd8319 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 7177dd5d82f6880255be463b6341fef931d7ad5e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 12 Nov 2020 03:11:29 +0300 Subject: [PATCH 4595/6909] Add counter to most played beatmaps section in user overlay --- .../Historical/PaginatedMostPlayedBeatmapContainer.cs | 4 +++- osu.Game/Users/User.cs | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 8f19cd900c..556f3139dd 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -16,7 +16,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer { public PaginatedMostPlayedBeatmapContainer(Bindable user) - : base(user, "Most Played Beatmaps", "No records. :(") + : base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible) { ItemsPerPage = 5; } @@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical ItemsContainer.Direction = FillDirection.Vertical; } + protected override int GetCount(User user) => user.BeatmapPlaycountsCount; + protected override APIRequest> CreateRequest() => new GetUserMostPlayedBeatmapsRequest(User.Value.Id, VisiblePages++, ItemsPerPage); diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 2a76a963e1..d7e78d5b35 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -144,6 +144,9 @@ namespace osu.Game.Users [JsonProperty(@"scores_first_count")] public int ScoresFirstCount; + [JsonProperty(@"beatmap_playcounts_count")] + public int BeatmapPlaycountsCount; + [JsonProperty] private string[] playstyle { From 7548db7ecc159638e016553844b5e0add7475128 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 11 Nov 2020 16:35:48 +0900 Subject: [PATCH 4596/6909] Fix hitobjects sometimes not fading in completely with HD mod --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 10 ++++++++-- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 +++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 025e202666..cf7faca9b9 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -27,17 +27,23 @@ namespace osu.Game.Rulesets.Osu.Mods public override void ApplyToDrawableHitObjects(IEnumerable drawables) { foreach (var d in drawables) - d.ApplyCustomUpdateState += applyFadeInAdjustment; + { + d.HitObjectApplied += applyFadeInAdjustment; + applyFadeInAdjustment(d); + } base.ApplyToDrawableHitObjects(drawables); } - private void applyFadeInAdjustment(DrawableHitObject hitObject, ArmedState state) + private void applyFadeInAdjustment(DrawableHitObject hitObject) { if (!(hitObject is DrawableOsuHitObject d)) return; d.HitObject.TimeFadeIn = d.HitObject.TimePreempt * fade_in_duration_multiplier; + + foreach (var nested in d.NestedHitObjects) + applyFadeInAdjustment(nested); } private double lastSliderHeadFadeOutStartTime; diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 244cf831c3..6a0e416967 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -26,8 +26,16 @@ namespace osu.Game.Rulesets.Objects.Drawables [Cached(typeof(DrawableHitObject))] public abstract class DrawableHitObject : SkinReloadableDrawable { + /// + /// Invoked after this 's applied has had its defaults applied. + /// public event Action DefaultsApplied; + /// + /// Invoked after a has been applied to this . + /// + public event Action HitObjectApplied; + /// /// The currently represented by this . /// @@ -215,6 +223,7 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject.DefaultsApplied += onDefaultsApplied; OnApply(hitObject); + HitObjectApplied?.Invoke(this); // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. if (IsLoaded) From fe347c8661839179421ff7e4f7ce75d034d81a43 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 18:30:32 +0900 Subject: [PATCH 4597/6909] Add playfield support for keeping hitobjects alive --- osu.Game/Rulesets/UI/Playfield.cs | 35 +++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index d1cb8ecbbd..454880c885 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -187,6 +187,41 @@ namespace osu.Game.Rulesets.UI { } + /// + /// Sets whether to keep a given always alive within this or any nested . + /// + /// The to set. + /// Whether to keep always alive. + public void SetKeepAlive(HitObject hitObject, bool keepAlive) + { + if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) + { + entry.KeepAlive = keepAlive; + return; + } + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.SetKeepAlive(hitObject, keepAlive); + } + + /// + /// Keeps all s alive within this and all nested s. + /// + public void KeepAllAlive() + { + foreach (var (_, entry) in lifetimeEntryMap) + entry.KeepAlive = true; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.KeepAllAlive(); + } + /// /// The cursor currently being used by this . May be null if no cursor is provided. /// From 243e913e4a9129a2f1043d2ca8ba414828b9cedb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 18:32:20 +0900 Subject: [PATCH 4598/6909] Add hitobject usage events --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 20 ++++++++++++++++++++ osu.Game/Rulesets/UI/Playfield.cs | 20 ++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 4164681ffc..1797f0acb8 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -42,6 +42,22 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + public event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + public event Action HitObjectUsageFinished; + private readonly Dictionary startTimeMap = new Dictionary(); private readonly Dictionary drawableMap = new Dictionary(); private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); @@ -88,6 +104,8 @@ namespace osu.Game.Rulesets.UI bindStartTime(drawable); AddInternal(drawableMap[entry] = drawable, false); + + HitObjectUsageBegan?.Invoke(entry.HitObject); } private void removeDrawable(HitObjectLifetimeEntry entry) @@ -103,6 +121,8 @@ namespace osu.Game.Rulesets.UI unbindStartTime(drawable); RemoveInternal(drawable); + + HitObjectUsageFinished?.Invoke(entry.HitObject); } #endregion diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 454880c885..8f2be81c36 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -29,6 +29,22 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + public event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + public event Action HitObjectUsageFinished; + /// /// The contained in this Playfield. /// @@ -88,6 +104,8 @@ namespace osu.Game.Rulesets.UI { h.NewResult += (d, r) => NewResult?.Invoke(d, r); h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); + h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); })); } @@ -247,6 +265,8 @@ namespace osu.Game.Rulesets.UI otherPlayfield.NewResult += (d, r) => NewResult?.Invoke(d, r); otherPlayfield.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + otherPlayfield.HitObjectUsageBegan += h => HitObjectUsageBegan?.Invoke(h); + otherPlayfield.HitObjectUsageFinished += h => HitObjectUsageFinished?.Invoke(h); nestedPlayfields.Value.Add(otherPlayfield); } From 8aaa500431b490adfd898fb722c75f3582f6f179 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 18:34:50 +0900 Subject: [PATCH 4599/6909] Add lifetime extensions --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 12 +++++++- osu.Game/Rulesets/UI/Playfield.cs | 36 ++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 1797f0acb8..bca2466968 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -58,6 +58,16 @@ namespace osu.Game.Rulesets.UI /// public event Action HitObjectUsageFinished; + /// + /// The amount of time prior to the current time within which s should be considered alive. + /// + public double PastLifetimeExtension { get; set; } + + /// + /// The amount of time after the current time within which s should be considered alive. + /// + public double FutureLifetimeExtension { get; set; } + private readonly Dictionary startTimeMap = new Dictionary(); private readonly Dictionary drawableMap = new Dictionary(); private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); @@ -179,7 +189,7 @@ namespace osu.Game.Rulesets.UI protected override bool CheckChildrenLife() { bool aliveChanged = base.CheckChildrenLife(); - aliveChanged |= lifetimeManager.Update(Time.Current, Time.Current); + aliveChanged |= lifetimeManager.Update(Time.Current - PastLifetimeExtension, Time.Current + FutureLifetimeExtension); return aliveChanged; } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 8f2be81c36..5794ff348c 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -240,6 +240,42 @@ namespace osu.Game.Rulesets.UI p.KeepAllAlive(); } + /// + /// The amount of time prior to the current time within which s should be considered alive. + /// + public double PastLifetimeExtension + { + get => HitObjectContainer.PastLifetimeExtension; + set + { + HitObjectContainer.PastLifetimeExtension = value; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var nested in nestedPlayfields.Value) + nested.PastLifetimeExtension = value; + } + } + + /// + /// The amount of time after the current time within which s should be considered alive. + /// + public double FutureLifetimeExtension + { + get => HitObjectContainer.FutureLifetimeExtension; + set + { + HitObjectContainer.FutureLifetimeExtension = value; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var nested in nestedPlayfields.Value) + nested.FutureLifetimeExtension = value; + } + } + /// /// The cursor currently being used by this . May be null if no cursor is provided. /// From 261ddd2b4a687c12245a963653aea3e76ff6e724 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 18:48:25 +0900 Subject: [PATCH 4600/6909] Fix samples not being disposed --- .../Objects/Drawables/DrawableHitObject.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 244cf831c3..6af445939b 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Logging; using osu.Framework.Threading; @@ -126,6 +127,8 @@ namespace osu.Game.Rulesets.Objects.Drawables [CanBeNull] private HitObjectLifetimeEntry lifetimeEntry; + private Container samplesContainer; + /// /// Creates a new . /// @@ -142,6 +145,9 @@ namespace osu.Game.Rulesets.Objects.Drawables private void load(OsuConfigManager config) { config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); + + // Explicit non-virtual function call. + base.AddInternal(samplesContainer = new Container { RelativeSizeAxes = Axes.Both }); } protected override void LoadAsyncComplete() @@ -296,11 +302,8 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual void LoadSamples() { - if (Samples != null) - { - RemoveInternal(Samples); - Samples = null; - } + samplesContainer.Clear(); + Samples = null; var samples = GetSamples().ToArray(); @@ -313,8 +316,7 @@ namespace osu.Game.Rulesets.Objects.Drawables + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } - Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s))); - AddInternal(Samples); + samplesContainer.Add(Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)))); } private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); From 3f0a1271966dd84047f1ddbbbae18b7098d637be Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 18:51:58 +0900 Subject: [PATCH 4601/6909] Fix slider/spinner samples not being disposed --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 6 ++++-- .../Objects/Drawables/DrawableSpinner.cs | 8 +++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 5bbdc5ee7b..04fc755da5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -40,6 +40,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container tailContainer; private Container tickContainer; private Container repeatContainer; + private Container samplesContainer; public DrawableSlider() : this(null) @@ -68,6 +69,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Alpha = 0 }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, + samplesContainer = new Container { RelativeSizeAxes = Axes.Both } }; PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); @@ -105,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.LoadSamples(); - slidingSample?.Expire(); + samplesContainer.Clear(); slidingSample = null; var firstSample = HitObject.Samples.FirstOrDefault(); @@ -115,7 +117,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "sliderslide"; - AddInternal(slidingSample = new PausableSkinnableSound(clone) + samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone) { Looping = true }); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 6b33517c33..824b8806e5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -29,6 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container ticks; private SpinnerBonusDisplay bonusDisplay; + private Container samplesContainer; private Bindable isSpinning; private bool spinnerFrequencyModulate; @@ -75,7 +76,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Anchor = Anchor.Centre, Origin = Anchor.Centre, Y = -120, - } + }, + samplesContainer = new Container { RelativeSizeAxes = Axes.Both } }; PositionBindable.BindValueChanged(pos => Position = pos.NewValue); @@ -97,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.LoadSamples(); - spinningSample?.Expire(); + samplesContainer.Clear(); spinningSample = null; var firstSample = HitObject.Samples.FirstOrDefault(); @@ -107,7 +109,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "spinnerspin"; - AddInternal(spinningSample = new PausableSkinnableSound(clone) + samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone) { Volume = { Value = 0 }, Looping = true, From 1439c0f39289d7d5a3d8848a2fb2f2bd86d78783 Mon Sep 17 00:00:00 2001 From: kamp Date: Thu, 12 Nov 2020 23:19:29 +0100 Subject: [PATCH 4602/6909] Prevent SelectionBox handles from appearing when a stack of circles is selected --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index a72dcff1e9..fd7ea050b9 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -21,12 +21,12 @@ namespace osu.Game.Rulesets.Osu.Edit { base.OnSelectionChanged(); - bool canOperate = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); + Quad quad = selectedMovableObjects.Length > 0 ? getSurroundingQuad(selectedMovableObjects) : new Quad(); - SelectionBox.CanRotate = canOperate; - SelectionBox.CanScaleX = canOperate; - SelectionBox.CanScaleY = canOperate; - SelectionBox.CanReverse = canOperate; + SelectionBox.CanRotate = quad.Width > 0 || quad.Height > 0; + SelectionBox.CanScaleX = quad.Width > 0; + SelectionBox.CanScaleY = quad.Height > 0; + SelectionBox.CanReverse = EditorBeatmap.SelectedHitObjects.Count > 1 || EditorBeatmap.SelectedHitObjects.Any(s => s is Slider); } protected override void OnOperationEnded() From 45ce6cc82d54e16529e4fd892237e4c892d2608e Mon Sep 17 00:00:00 2001 From: kamp Date: Fri, 13 Nov 2020 00:36:47 +0100 Subject: [PATCH 4603/6909] Allow spinners to be reversed --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index a72dcff1e9..ec0a6a2bcc 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -45,12 +45,12 @@ namespace osu.Game.Rulesets.Osu.Edit public override bool HandleReverse() { - var hitObjects = selectedMovableObjects; + var hitObjects = EditorBeatmap.SelectedHitObjects; double endTime = hitObjects.Max(h => h.GetEndTime()); double startTime = hitObjects.Min(h => h.StartTime); - bool moreThanOneObject = hitObjects.Length > 1; + bool moreThanOneObject = hitObjects.Count > 1; foreach (var h in hitObjects) { From 43626573df0308b599d32499feb65458dc883277 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Nov 2020 13:15:54 +0900 Subject: [PATCH 4604/6909] Fix combo break sounds playing when seeking --- osu.Game/Screens/Play/ComboEffects.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index 831b2f593c..f22fd541d0 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -38,12 +38,21 @@ namespace osu.Game.Screens.Play processor.Combo.BindValueChanged(onComboChange); } + [Resolved(canBeNull: true)] + private ISamplePlaybackDisabler samplePlaybackDisabler { get; set; } + private void onComboChange(ValueChangedEvent combo) { if (combo.NewValue == 0 && (combo.OldValue > 20 || (alwaysPlay.Value && firstTime))) { - comboBreakSample?.Play(); firstTime = false; + + // combo break isn't a pausable sound itself as we want to let it play out. + // we still need to disable during seeks, though. + if (samplePlaybackDisabler?.SamplePlaybackDisabled.Value == true) + return; + + comboBreakSample?.Play(); } } } From 4b5743d993c5a8b9a2f279e6c6ef6df02540cb4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Nov 2020 13:35:01 +0900 Subject: [PATCH 4605/6909] Fix combo break sound not playing after rewind --- osu.Game/Screens/Play/ComboEffects.cs | 21 ++++++++++++++++----- osu.Game/Screens/Play/Player.cs | 2 +- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/ComboEffects.cs b/osu.Game/Screens/Play/ComboEffects.cs index f22fd541d0..5041d07e5d 100644 --- a/osu.Game/Screens/Play/ComboEffects.cs +++ b/osu.Game/Screens/Play/ComboEffects.cs @@ -17,8 +17,9 @@ namespace osu.Game.Screens.Play private SkinnableSound comboBreakSample; - private Bindable alwaysPlay; - private bool firstTime = true; + private Bindable alwaysPlayFirst; + + private double? firstBreakTime; public ComboEffects(ScoreProcessor processor) { @@ -29,7 +30,7 @@ namespace osu.Game.Screens.Play private void load(OsuConfigManager config) { InternalChild = comboBreakSample = new SkinnableSound(new SampleInfo("Gameplay/combobreak")); - alwaysPlay = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak); + alwaysPlayFirst = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak); } protected override void LoadComplete() @@ -41,11 +42,21 @@ namespace osu.Game.Screens.Play [Resolved(canBeNull: true)] private ISamplePlaybackDisabler samplePlaybackDisabler { get; set; } + [Resolved] + private GameplayClock gameplayClock { get; set; } + private void onComboChange(ValueChangedEvent combo) { - if (combo.NewValue == 0 && (combo.OldValue > 20 || (alwaysPlay.Value && firstTime))) + // handle the case of rewinding before the first combo break time. + if (gameplayClock.CurrentTime < firstBreakTime) + firstBreakTime = null; + + if (gameplayClock.ElapsedFrameTime < 0) + return; + + if (combo.NewValue == 0 && (combo.OldValue > 20 || (alwaysPlayFirst.Value && firstBreakTime == null))) { - firstTime = false; + firstBreakTime = gameplayClock.CurrentTime; // combo break isn't a pausable sound itself as we want to let it play out. // we still need to disable during seeks, though. diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f9af1818d0..49cc390775 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -302,12 +302,12 @@ namespace osu.Game.Screens.Play { ScoreProcessor, HealthProcessor, + new ComboEffects(ScoreProcessor), breakTracker = new BreakTracker(DrawableRuleset.GameplayStartTime, ScoreProcessor) { Breaks = working.Beatmap.Breaks } }), - new ComboEffects(ScoreProcessor) } }; From a2c81a3a52f8daf0d81aee1626b7e48dbd91c2e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Nov 2020 13:42:00 +0900 Subject: [PATCH 4606/6909] Add back setting to toggle "always play first combo break" --- .../Overlays/Settings/Sections/Gameplay/GeneralSettings.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 9cb02ff3b9..be464fa2b7 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -68,6 +68,11 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Positional hitsounds", Current = config.GetBindable(OsuSetting.PositionalHitSounds) }, + new SettingsCheckbox + { + LabelText = "Always play first combo break sound", + Current = config.GetBindable(OsuSetting.AlwaysPlayFirstComboBreak) + }, new SettingsEnumDropdown { LabelText = "Score meter type", From 0985cb3327421220f0634d567a07f3a2688ed0a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Nov 2020 14:08:08 +0900 Subject: [PATCH 4607/6909] Fix perform from menu not hiding overlays if already on target screen --- .../Navigation/TestScenePerformFromScreen.cs | 19 +++++++++++++++++++ osu.Game/PerformFromMenuRunner.cs | 4 ++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs index a4190e0b84..21d3bdaae3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs +++ b/osu.Game.Tests/Visual/Navigation/TestScenePerformFromScreen.cs @@ -1,9 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Game.Overlays; using osu.Game.Screens; using osu.Game.Screens.Menu; @@ -73,6 +76,22 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("did perform", () => actionPerformed); } + [Test] + public void TestOverlaysAlwaysClosed() + { + ChatOverlay chat = null; + AddUntilStep("is at menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + AddUntilStep("wait for chat load", () => (chat = Game.ChildrenOfType().SingleOrDefault()) != null); + + AddStep("show chat", () => InputManager.Key(Key.F8)); + + AddStep("try to perform", () => Game.PerformFromScreen(_ => actionPerformed = true)); + + AddUntilStep("still at menu", () => Game.ScreenStack.CurrentScreen is MainMenu); + AddAssert("did perform", () => actionPerformed); + AddAssert("chat closed", () => chat.State.Value == Visibility.Hidden); + } + [TestCase(true)] [TestCase(false)] public void TestPerformBlockedByDialog(bool confirmed) diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index 9afe87f74f..5898c116dd 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -76,6 +76,8 @@ namespace osu.Game // a dialog may be blocking the execution for now. if (checkForDialog(current)) return; + game.CloseAllOverlays(false); + // we may already be at the target screen type. if (validScreens.Contains(getCurrentScreen().GetType()) && !beatmap.Disabled) { @@ -83,8 +85,6 @@ namespace osu.Game return; } - game.CloseAllOverlays(false); - while (current != null) { if (validScreens.Contains(current.GetType())) From 35329aa976747372fdfaa05079557b3abcfaecf4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 14:33:23 +0900 Subject: [PATCH 4608/6909] Reduce the number of state updates --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 56b725da0e..01f0e42d92 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -170,7 +170,6 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.LoadComplete(); - StartTimeBindable.BindValueChanged(_ => updateState(State.Value, true)); comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); updateState(ArmedState.Idle, true); @@ -220,6 +219,8 @@ namespace osu.Game.Rulesets.Objects.Drawables } StartTimeBindable.BindTo(HitObject.StartTimeBindable); + StartTimeBindable.BindValueChanged(onStartTimeChanged); + if (HitObject is IHasComboInformation combo) comboIndexBindable.BindTo(combo.ComboIndexBindable); @@ -249,9 +250,11 @@ namespace osu.Game.Rulesets.Objects.Drawables StartTimeBindable.UnbindFrom(HitObject.StartTimeBindable); if (HitObject is IHasComboInformation combo) comboIndexBindable.UnbindFrom(combo.ComboIndexBindable); - samplesBindable.UnbindFrom(HitObject.SamplesBindable); + // Changes in start time trigger state updates. When a new hitobject is applied, OnApply() automatically performs a state update anyway. + StartTimeBindable.ValueChanged -= onStartTimeChanged; + // When a new hitobject is applied, the samples will be cleared before re-populating. // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). samplesBindable.CollectionChanged -= onSamplesChanged; @@ -330,6 +333,8 @@ namespace osu.Game.Rulesets.Objects.Drawables private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); + private void onStartTimeChanged(ValueChangedEvent startTime) => updateState(State.Value, true); + private void onNewResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnNewResult?.Invoke(drawableHitObject, result); private void onRevertResult(DrawableHitObject drawableHitObject, JudgementResult result) => OnRevertResult?.Invoke(drawableHitObject, result); From a07d4a7915aa262bfdfe64e3ce5ba3465a2da523 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 14:42:41 +0900 Subject: [PATCH 4609/6909] Remove unnecessary dictionary for now --- osu.Game/Rulesets/UI/Playfield.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index d1cb8ecbbd..573a57d701 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -136,8 +136,6 @@ namespace osu.Game.Rulesets.UI return false; } - private readonly Dictionary lifetimeEntryMap = new Dictionary(); - /// /// Adds a for a pooled to this . /// @@ -145,7 +143,6 @@ namespace osu.Game.Rulesets.UI public virtual void Add(HitObjectLifetimeEntry entry) { HitObjectContainer.Add(entry); - lifetimeEntryMap[entry.HitObject] = entry; OnHitObjectAdded(entry.HitObject); } @@ -156,9 +153,8 @@ namespace osu.Game.Rulesets.UI /// Whether the was successfully removed. public virtual bool Remove(HitObjectLifetimeEntry entry) { - if (lifetimeEntryMap.Remove(entry.HitObject)) + if (HitObjectContainer.Remove(entry)) { - HitObjectContainer.Remove(entry); OnHitObjectRemoved(entry.HitObject); return true; } From 4236dd826d7a834c7dc9056bfbfaf2dfa0d99e6b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 14:57:09 +0900 Subject: [PATCH 4610/6909] Improve documentation and make abstract again --- .../Visual/Gameplay/TestScenePoolingRuleset.cs | 2 ++ osu.Game/Rulesets/UI/DrawableRuleset.cs | 13 +++++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index c3ae753eae..242eaf7b7d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -142,6 +142,8 @@ namespace osu.Game.Tests.Visual.Gameplay protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); + public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => null; + protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); protected override Playfield CreatePlayfield() => new TestPlayfield(); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 0429936d8e..c912348604 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -324,11 +324,16 @@ namespace osu.Game.Rulesets.UI } /// - /// Creates a DrawableHitObject from a HitObject. + /// Creates a to represent a . /// - /// The HitObject to make drawable. - /// The DrawableHitObject. - public virtual DrawableHitObject CreateDrawableRepresentation(TObject h) => null; + /// + /// If this method returns null, then this will assume the requested type is being pooled, + /// and will instead attempt to retrieve the s at the point they should become alive via pools registered through + /// or . + /// + /// The to represent. + /// The representing . + public abstract DrawableHitObject CreateDrawableRepresentation(TObject h); public void Attach(KeyCounterDisplay keyCounter) => (KeyBindingInputManager as ICanAttachKeyCounter)?.Attach(keyCounter); From beb6bbd2a14509f39bd5235bb8a7fb7583e51271 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 14:58:32 +0900 Subject: [PATCH 4611/6909] Implement now abstract method --- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index c89f138bcd..1d16c47818 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -59,6 +59,8 @@ namespace osu.Game.Rulesets.Osu.UI where TDrawable : DrawableHitObject, new() => new OsuDrawablePool(Playfield.CheckHittable, Playfield.OnHitObjectLoaded, initialSize, maximumSize); + public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) => null; + protected override HitObjectLifetimeEntry CreateLifetimeEntry(OsuHitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor From a9fc7572ed6ae0a9061c4d5d9ab8340829d59e5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 13 Nov 2020 15:33:27 +0900 Subject: [PATCH 4612/6909] 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 5078fee1cf..6e3d5eec1f 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 405fb1a6ca..1850ee3488 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 099ecd8319..2ac23f1503 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 4ef2e9548c01dce805c1ce02d89e40626146531a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 12 Nov 2020 19:52:02 +0900 Subject: [PATCH 4613/6909] Pass HitObjectComposer to BlueprintContainer via ctor --- .../Edit/ManiaBlueprintContainer.cs | 5 +- .../Edit/ManiaHitObjectComposer.cs | 5 +- .../Edit/OsuBlueprintContainer.cs | 5 +- .../Edit/OsuHitObjectComposer.cs | 5 +- .../Edit/TaikoBlueprintContainer.cs | 5 +- .../Edit/TaikoHitObjectComposer.cs | 5 +- .../TestSceneTimelineBlueprintContainer.cs | 2 +- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 7 +- .../Compose/Components/BlueprintContainer.cs | 74 +++++++++++++------ .../Components/ComposeBlueprintContainer.cs | 26 +++---- .../Timeline/TimelineBlueprintContainer.cs | 3 +- .../Screens/Edit/Compose/ComposeScreen.cs | 2 +- 12 files changed, 83 insertions(+), 61 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs index cea27498c3..2fa3f378ff 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaBlueprintContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Mania.Edit.Blueprints; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -12,8 +11,8 @@ namespace osu.Game.Rulesets.Mania.Edit { public class ManiaBlueprintContainer : ComposeBlueprintContainer { - public ManiaBlueprintContainer(IEnumerable drawableHitObjects) - : base(drawableHitObjects) + public ManiaBlueprintContainer(HitObjectComposer composer) + : base(composer) { } diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 7e2469a794..01d572447b 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -13,7 +13,6 @@ 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; @@ -89,8 +88,8 @@ namespace osu.Game.Rulesets.Mania.Edit return drawableRuleset; } - protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new ManiaBlueprintContainer(hitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer() + => new ManiaBlueprintContainer(this); protected override IReadOnlyList CompositionTools => new HitObjectCompositionTool[] { diff --git a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs index 330f34b85c..a68ed34e6b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuBlueprintContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; @@ -14,8 +13,8 @@ namespace osu.Game.Rulesets.Osu.Edit { public class OsuBlueprintContainer : ComposeBlueprintContainer { - public OsuBlueprintContainer(IEnumerable drawableHitObjects) - : base(drawableHitObjects) + public OsuBlueprintContainer(HitObjectComposer composer) + : base(composer) { } diff --git a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index edd684d886..bfa8ab4431 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -16,7 +16,6 @@ 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.Components.TernaryButtons; @@ -80,8 +79,8 @@ namespace osu.Game.Rulesets.Osu.Edit updateDistanceSnapGrid(); } - protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new OsuBlueprintContainer(hitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer() + => new OsuBlueprintContainer(this); private DistanceSnapGrid distanceSnapGrid; private Container distanceSnapGridContainer; diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs index 35227b3c64..8b41448c9d 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoBlueprintContainer.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Edit.Blueprints; @@ -11,8 +10,8 @@ namespace osu.Game.Rulesets.Taiko.Edit { public class TaikoBlueprintContainer : ComposeBlueprintContainer { - public TaikoBlueprintContainer(IEnumerable hitObjects) - : base(hitObjects) + public TaikoBlueprintContainer(HitObjectComposer composer) + : base(composer) { } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs index cdc9672a8e..161799c980 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoHitObjectComposer.cs @@ -4,7 +4,6 @@ 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; @@ -24,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Edit new SwellCompositionTool() }; - protected override ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new TaikoBlueprintContainer(hitObjects); + protected override ComposeBlueprintContainer CreateBlueprintContainer() + => new TaikoBlueprintContainer(this); } } diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index e931be044c..5da63eddf1 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneTimelineBlueprintContainer : TimelineTestScene { - public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(); + public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(null); protected override void LoadComplete() { diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index c9dd061b48..b90aa6863a 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Edit drawableRulesetWrapper, // layers above playfield drawableRulesetWrapper.CreatePlayfieldAdjustmentContainer() - .WithChild(BlueprintContainer = CreateBlueprintContainer(HitObjects)) + .WithChild(BlueprintContainer = CreateBlueprintContainer()) } }, new FillFlowContainer @@ -182,9 +182,8 @@ namespace osu.Game.Rulesets.Edit /// /// Construct a relevant blueprint container. This will manage hitobject selection/placement input handling and display logic. /// - /// A live collection of all s in the editor beatmap. - protected virtual ComposeBlueprintContainer CreateBlueprintContainer(IEnumerable hitObjects) - => new ComposeBlueprintContainer(hitObjects); + protected virtual ComposeBlueprintContainer CreateBlueprintContainer() + => new ComposeBlueprintContainer(this); /// /// Construct a drawable ruleset for the provided ruleset. diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index b67f6a6ba6..3145616cf7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.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.Collections.Specialized; using System.Diagnostics; using System.Linq; @@ -44,12 +45,16 @@ namespace osu.Game.Screens.Edit.Compose.Components protected EditorBeatmap Beatmap { get; private set; } private readonly BindableList selectedHitObjects = new BindableList(); + private readonly HitObjectComposer composer; + private readonly Dictionary blueprintMap = new Dictionary(); [Resolved(canBeNull: true)] private IPositionSnapProvider snapProvider { get; set; } - protected BlueprintContainer() + protected BlueprintContainer(HitObjectComposer composer) { + this.composer = composer; + RelativeSizeAxes = Axes.Both; } @@ -68,8 +73,12 @@ namespace osu.Game.Screens.Edit.Compose.Components DragBox.CreateProxy().With(p => p.Depth = float.MinValue) }); - foreach (var obj in Beatmap.HitObjects) - AddBlueprintFor(obj); + // For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context. + if (composer != null) + { + foreach (var obj in composer.HitObjects) + addBlueprintFor(obj.HitObject); + } selectedHitObjects.BindTo(Beatmap.SelectedHitObjects); selectedHitObjects.CollectionChanged += (selectedObjects, args) => @@ -94,7 +103,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.LoadComplete(); - Beatmap.HitObjectAdded += AddBlueprintFor; + Beatmap.HitObjectAdded += addBlueprintFor; Beatmap.HitObjectRemoved += removeBlueprintFor; } @@ -247,29 +256,17 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Blueprint Addition/Removal - private void removeBlueprintFor(HitObject hitObject) + private void addBlueprintFor(HitObject hitObject) { - var blueprint = SelectionBlueprints.SingleOrDefault(m => m.HitObject == hitObject); - if (blueprint == null) + if (blueprintMap.ContainsKey(hitObject)) return; - blueprint.Deselect(); - - blueprint.Selected -= onBlueprintSelected; - blueprint.Deselected -= onBlueprintDeselected; - - SelectionBlueprints.Remove(blueprint); - - if (movementBlueprint == blueprint) - finishSelectionMovement(); - } - - protected virtual void AddBlueprintFor(HitObject hitObject) - { var blueprint = CreateBlueprintFor(hitObject); if (blueprint == null) return; + blueprintMap[hitObject] = blueprint; + blueprint.Selected += onBlueprintSelected; blueprint.Deselected += onBlueprintDeselected; @@ -277,6 +274,41 @@ namespace osu.Game.Screens.Edit.Compose.Components blueprint.Select(); SelectionBlueprints.Add(blueprint); + + OnBlueprintAdded(hitObject); + } + + private void removeBlueprintFor(HitObject hitObject) + { + if (!blueprintMap.Remove(hitObject, out var blueprint)) + return; + + blueprint.Deselect(); + blueprint.Selected -= onBlueprintSelected; + blueprint.Deselected -= onBlueprintDeselected; + + SelectionBlueprints.Remove(blueprint); + + if (movementBlueprint == blueprint) + finishSelectionMovement(); + + OnBlueprintRemoved(hitObject); + } + + /// + /// Called after a blueprint has been added. + /// + /// The for which the blueprint has been added. + protected virtual void OnBlueprintAdded(HitObject hitObject) + { + } + + /// + /// Called after a blueprint has been removed. + /// + /// The for which the blueprint has been removed. + protected virtual void OnBlueprintRemoved(HitObject hitObject) + { } #endregion @@ -456,7 +488,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (Beatmap != null) { - Beatmap.HitObjectAdded -= AddBlueprintFor; + Beatmap.HitObjectAdded -= addBlueprintFor; Beatmap.HitObjectRemoved -= removeBlueprintFor; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 1527d20f54..27190e9aad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -27,22 +27,18 @@ namespace osu.Game.Screens.Edit.Compose.Components /// public class ComposeBlueprintContainer : BlueprintContainer { - [Resolved] - private HitObjectComposer composer { get; set; } - - private PlacementBlueprint currentPlacement; - - private readonly Container placementBlueprintContainer; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + private readonly HitObjectComposer composer; + private readonly Container placementBlueprintContainer; + + private PlacementBlueprint currentPlacement; private InputManager inputManager; - private readonly IEnumerable drawableHitObjects; - - public ComposeBlueprintContainer(IEnumerable drawableHitObjects) + public ComposeBlueprintContainer(HitObjectComposer composer) + : base(composer) { - this.drawableHitObjects = drawableHitObjects; + this.composer = composer; placementBlueprintContainer = new Container { @@ -186,7 +182,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) { - var drawable = drawableHitObjects.FirstOrDefault(d => d.HitObject == hitObject); + var drawable = composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject); if (drawable == null) return null; @@ -196,11 +192,11 @@ namespace osu.Game.Screens.Edit.Compose.Components public virtual OverlaySelectionBlueprint CreateBlueprintFor(DrawableHitObject hitObject) => null; - protected override void AddBlueprintFor(HitObject hitObject) + protected override void OnBlueprintAdded(HitObject hitObject) { - refreshTool(); + base.OnBlueprintAdded(hitObject); - base.AddBlueprintFor(hitObject); + refreshTool(); // on successful placement, the new combo button should be reset as this is the most common user interaction. if (Beatmap.SelectedHitObjects.Count == 0) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 10913a8bb9..078a158e3d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -31,7 +31,8 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private SelectionBlueprint placementBlueprint; - public TimelineBlueprintContainer() + public TimelineBlueprintContainer(HitObjectComposer composer) + : base(composer) { RelativeSizeAxes = Axes.Both; Anchor = Anchor.Centre; diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 5282b4d998..d9948aa23c 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -53,6 +53,6 @@ namespace osu.Game.Screens.Edit.Compose return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); } - protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(); + protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(composer); } } From 3957697c4840072fc9e0b128d1e7981f91ee2988 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 17:08:20 +0900 Subject: [PATCH 4614/6909] Add pooling support to the editor --- .../Edit/DrawableEditRulesetWrapper.cs | 9 ++---- .../Compose/Components/BlueprintContainer.cs | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs index 8ed7885101..c60d4c7834 100644 --- a/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs +++ b/osu.Game/Rulesets/Edit/DrawableEditRulesetWrapper.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -65,17 +64,13 @@ namespace osu.Game.Rulesets.Edit private void addHitObject(HitObject hitObject) { - var drawableObject = drawableRuleset.CreateDrawableRepresentation((TObject)hitObject); - - drawableRuleset.Playfield.Add(drawableObject); + drawableRuleset.AddHitObject((TObject)hitObject); drawableRuleset.Playfield.PostProcess(); } private void removeHitObject(HitObject hitObject) { - var drawableObject = Playfield.AllHitObjects.Single(d => d.HitObject == hitObject); - - drawableRuleset.Playfield.Remove(drawableObject); + drawableRuleset.RemoveHitObject((TObject)hitObject); drawableRuleset.Playfield.PostProcess(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 3145616cf7..450c42f7fe 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -105,6 +105,16 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectAdded += addBlueprintFor; Beatmap.HitObjectRemoved += removeBlueprintFor; + + if (composer != null) + { + // For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above. + foreach (var obj in composer.HitObjects) + addBlueprintFor(obj.HitObject); + + composer.Playfield.HitObjectUsageBegan += addBlueprintFor; + composer.Playfield.HitObjectUsageFinished += removeBlueprintFor; + } } protected virtual Container CreateSelectionBlueprintContainer() => @@ -381,7 +391,13 @@ namespace osu.Game.Screens.Edit.Compose.Components /// /// Selects all s. /// - private void selectAll() => SelectionBlueprints.ToList().ForEach(m => m.Select()); + private void selectAll() + { + composer.Playfield.KeepAllAlive(); + + // Scheduled to allow the change in lifetime to take place. + Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select())); + } /// /// Deselects all selected s. @@ -392,12 +408,16 @@ namespace osu.Game.Screens.Edit.Compose.Components { SelectionHandler.HandleSelected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 1); + + composer.Playfield.SetKeepAlive(blueprint.HitObject, true); } private void onBlueprintDeselected(SelectionBlueprint blueprint) { SelectionHandler.HandleDeselected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 0); + + composer.Playfield.SetKeepAlive(blueprint.HitObject, false); } #endregion @@ -491,6 +511,12 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectAdded -= addBlueprintFor; Beatmap.HitObjectRemoved -= removeBlueprintFor; } + + if (composer != null) + { + composer.Playfield.HitObjectUsageBegan -= addBlueprintFor; + composer.Playfield.HitObjectUsageFinished -= removeBlueprintFor; + } } } } From 0219aff7bc7cbec5f4e41d533b5fbf17fd3a7c9b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 17:10:29 +0900 Subject: [PATCH 4615/6909] Add timeline visible range adjustment --- .../Compose/Components/BlueprintContainer.cs | 29 ++++++++++--------- .../Components/ComposeBlueprintContainer.cs | 9 ++---- .../Compose/Components/Timeline/Timeline.cs | 5 ++++ .../Timeline/TimelineBlueprintContainer.cs | 8 +++-- 4 files changed, 29 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 450c42f7fe..3229719d5a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -35,6 +35,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected SelectionHandler SelectionHandler { get; private set; } + protected readonly HitObjectComposer Composer; + [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } @@ -45,7 +47,6 @@ namespace osu.Game.Screens.Edit.Compose.Components protected EditorBeatmap Beatmap { get; private set; } private readonly BindableList selectedHitObjects = new BindableList(); - private readonly HitObjectComposer composer; private readonly Dictionary blueprintMap = new Dictionary(); [Resolved(canBeNull: true)] @@ -53,7 +54,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected BlueprintContainer(HitObjectComposer composer) { - this.composer = composer; + Composer = composer; RelativeSizeAxes = Axes.Both; } @@ -74,9 +75,9 @@ namespace osu.Game.Screens.Edit.Compose.Components }); // For non-pooled rulesets, hitobjects are already present in the playfield which allows the blueprints to be loaded in the async context. - if (composer != null) + if (Composer != null) { - foreach (var obj in composer.HitObjects) + foreach (var obj in Composer.HitObjects) addBlueprintFor(obj.HitObject); } @@ -106,14 +107,14 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectAdded += addBlueprintFor; Beatmap.HitObjectRemoved += removeBlueprintFor; - if (composer != null) + if (Composer != null) { // For pooled rulesets, blueprints must be added for hitobjects already "current" as they would've not been "current" during the async load addition process above. - foreach (var obj in composer.HitObjects) + foreach (var obj in Composer.HitObjects) addBlueprintFor(obj.HitObject); - composer.Playfield.HitObjectUsageBegan += addBlueprintFor; - composer.Playfield.HitObjectUsageFinished += removeBlueprintFor; + Composer.Playfield.HitObjectUsageBegan += addBlueprintFor; + Composer.Playfield.HitObjectUsageFinished += removeBlueprintFor; } } @@ -393,7 +394,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// private void selectAll() { - composer.Playfield.KeepAllAlive(); + Composer.Playfield.KeepAllAlive(); // Scheduled to allow the change in lifetime to take place. Schedule(() => SelectionBlueprints.ToList().ForEach(m => m.Select())); @@ -409,7 +410,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionHandler.HandleSelected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 1); - composer.Playfield.SetKeepAlive(blueprint.HitObject, true); + Composer.Playfield.SetKeepAlive(blueprint.HitObject, true); } private void onBlueprintDeselected(SelectionBlueprint blueprint) @@ -417,7 +418,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionHandler.HandleDeselected(blueprint); SelectionBlueprints.ChangeChildDepth(blueprint, 0); - composer.Playfield.SetKeepAlive(blueprint.HitObject, false); + Composer.Playfield.SetKeepAlive(blueprint.HitObject, false); } #endregion @@ -512,10 +513,10 @@ namespace osu.Game.Screens.Edit.Compose.Components Beatmap.HitObjectRemoved -= removeBlueprintFor; } - if (composer != null) + if (Composer != null) { - composer.Playfield.HitObjectUsageBegan -= addBlueprintFor; - composer.Playfield.HitObjectUsageFinished -= removeBlueprintFor; + Composer.Playfield.HitObjectUsageBegan -= addBlueprintFor; + Composer.Playfield.HitObjectUsageFinished -= removeBlueprintFor; } } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 27190e9aad..0d2e2360b1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -29,7 +29,6 @@ namespace osu.Game.Screens.Edit.Compose.Components { public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - private readonly HitObjectComposer composer; private readonly Container placementBlueprintContainer; private PlacementBlueprint currentPlacement; @@ -38,8 +37,6 @@ namespace osu.Game.Screens.Edit.Compose.Components public ComposeBlueprintContainer(HitObjectComposer composer) : base(composer) { - this.composer = composer; - placementBlueprintContainer = new Container { RelativeSizeAxes = Axes.Both @@ -158,7 +155,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private void updatePlacementPosition() { - var snapResult = composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); + var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); currentPlacement.UpdatePosition(snapResult); } @@ -169,7 +166,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { base.Update(); - if (composer.CursorInPlacementArea) + if (Composer.CursorInPlacementArea) createPlacement(); else if (currentPlacement?.PlacementActive == false) removePlacement(); @@ -182,7 +179,7 @@ namespace osu.Game.Screens.Edit.Compose.Components protected sealed override SelectionBlueprint CreateBlueprintFor(HitObject hitObject) { - var drawable = composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject); + var drawable = Composer.HitObjects.FirstOrDefault(d => d.HitObject == hitObject); if (drawable == null) return null; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 7233faa955..f6675902fc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -219,6 +219,11 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private IBeatSnapProvider beatSnapProvider { get; set; } + /// + /// The total amount of time visible on the timeline. + /// + public double VisibleRange => track.Length / Zoom; + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 078a158e3d..0271b2def9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -26,9 +26,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private EditorBeatmap beatmap { get; set; } private DragEvent lastDragEvent; - private Bindable placement; - private SelectionBlueprint placementBlueprint; public TimelineBlueprintContainer(HitObjectComposer composer) @@ -98,6 +96,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (lastDragEvent != null) OnDrag(lastDragEvent); + if (Composer != null) + { + Composer.Playfield.PastLifetimeExtension = timeline.VisibleRange / 2; + Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; + } + base.Update(); } From 688a442fb3fc1bdb895212dce631ef716996a875 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 17:26:46 +0900 Subject: [PATCH 4616/6909] Add missing dictionary --- osu.Game/Rulesets/UI/Playfield.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index e27375bd37..f12db57199 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -154,6 +154,8 @@ namespace osu.Game.Rulesets.UI return false; } + private readonly Dictionary lifetimeEntryMap = new Dictionary(); + /// /// Adds a for a pooled to this . /// @@ -161,6 +163,7 @@ namespace osu.Game.Rulesets.UI public virtual void Add(HitObjectLifetimeEntry entry) { HitObjectContainer.Add(entry); + lifetimeEntryMap[entry.HitObject] = entry; OnHitObjectAdded(entry.HitObject); } @@ -173,6 +176,7 @@ namespace osu.Game.Rulesets.UI { if (HitObjectContainer.Remove(entry)) { + lifetimeEntryMap.Remove(entry.HitObject); OnHitObjectRemoved(entry.HitObject); return true; } From f7f70d41dfb73719b99aadcaa592684621031687 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 17:28:14 +0900 Subject: [PATCH 4617/6909] Add osu! editor pooling support --- .../Edit/DrawableOsuEditRuleset.cs | 51 +-------------- .../UI/OsuEditDrawablePool.cs | 63 +++++++++++++++++++ 2 files changed, 66 insertions(+), 48 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/OsuEditDrawablePool.cs diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 746ff4ac19..05396ebc8b 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -2,13 +2,9 @@ // 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.Graphics.Pooling; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -17,54 +13,13 @@ namespace osu.Game.Rulesets.Osu.Edit { public class DrawableOsuEditRuleset : DrawableOsuRuleset { - /// - /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. - /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. - /// - private const double editor_hit_object_fade_out_extension = 700; - public DrawableOsuEditRuleset(Ruleset ruleset, IBeatmap beatmap, IReadOnlyList mods) : base(ruleset, beatmap, mods) { } - public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) - => base.CreateDrawableRepresentation(h)?.With(d => d.ApplyCustomUpdateState += updateState); - - private void updateState(DrawableHitObject hitObject, ArmedState state) - { - if (state == ArmedState.Idle) - return; - - // adjust the visuals of certain object types to make them stay on screen for longer than usual. - switch (hitObject) - { - default: - // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) - return; - - case DrawableSlider _: - // no specifics to sliders but let them fade slower below. - break; - - case DrawableHitCircle circle: // also handles slider heads - circle.ApproachCircle - .FadeOutFromOne(editor_hit_object_fade_out_extension) - .Expire(); - break; - } - - // Get the existing fade out transform - var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); - - if (existing == null) - return; - - hitObject.RemoveTransform(existing); - - using (hitObject.BeginAbsoluteSequence(existing.StartTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); - } + protected override DrawablePool CreatePool(int initialSize, int? maximumSize = null) + => new OsuEditDrawablePool(Playfield.CheckHittable, Playfield.OnHitObjectLoaded, initialSize, maximumSize); protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor(); diff --git a/osu.Game.Rulesets.Osu/UI/OsuEditDrawablePool.cs b/osu.Game.Rulesets.Osu/UI/OsuEditDrawablePool.cs new file mode 100644 index 0000000000..822ff076f5 --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/OsuEditDrawablePool.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.UI +{ + public class OsuEditDrawablePool : OsuDrawablePool + where T : DrawableHitObject, new() + { + /// + /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. + /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. + /// + private const double editor_hit_object_fade_out_extension = 700; + + public OsuEditDrawablePool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) + : base(checkHittable, onLoaded, initialSize, maximumSize) + { + } + + protected override T CreateNewDrawable() => base.CreateNewDrawable().With(d => d.ApplyCustomUpdateState += updateState); + + private void updateState(DrawableHitObject hitObject, ArmedState state) + { + if (state == ArmedState.Idle) + return; + + // adjust the visuals of certain object types to make them stay on screen for longer than usual. + switch (hitObject) + { + default: + // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) + return; + + case DrawableSlider _: + // no specifics to sliders but let them fade slower below. + break; + + case DrawableHitCircle circle: // also handles slider heads + circle.ApproachCircle + .FadeOutFromOne(editor_hit_object_fade_out_extension) + .Expire(); + break; + } + + // Get the existing fade out transform + var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); + + if (existing == null) + return; + + hitObject.RemoveTransform(existing); + + using (hitObject.BeginAbsoluteSequence(existing.StartTime)) + hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + } + } +} From 864e4006b96e2f1c2948ebc086e18f9c44d4f0ad Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 17:51:01 +0900 Subject: [PATCH 4618/6909] Fix timeline test --- .../TestSceneTimelineBlueprintContainer.cs | 2 +- .../Visual/Editing/TimelineTestScene.cs | 6 ++++- osu.Game.Tests/WaveformTestBeatmap.cs | 25 +++---------------- 3 files changed, 10 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs index 5da63eddf1..6b54bcb4f0 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineBlueprintContainer.cs @@ -10,7 +10,7 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneTimelineBlueprintContainer : TimelineTestScene { - public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(null); + public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(Composer); protected override void LoadComplete() { diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index fdb8781563..63bb018d6e 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -21,21 +21,25 @@ namespace osu.Game.Tests.Visual.Editing { protected TimelineArea TimelineArea { get; private set; } + protected HitObjectComposer Composer { get; private set; } + [BackgroundDependencyLoader] private void load(AudioManager audio) { Beatmap.Value = new WaveformTestBeatmap(audio); var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); - var editorBeatmap = new EditorBeatmap(playable); Dependencies.Cache(editorBeatmap); Dependencies.CacheAs(editorBeatmap); + Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0); + AddRange(new Drawable[] { editorBeatmap, + Composer, new FillFlowContainer { AutoSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/WaveformTestBeatmap.cs b/osu.Game.Tests/WaveformTestBeatmap.cs index f9613d9e25..8c8c827404 100644 --- a/osu.Game.Tests/WaveformTestBeatmap.cs +++ b/osu.Game.Tests/WaveformTestBeatmap.cs @@ -8,10 +8,9 @@ using osu.Framework.Audio; 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.Archives; -using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; @@ -25,8 +24,8 @@ namespace osu.Game.Tests private readonly Beatmap beatmap; private readonly ITrackStore trackStore; - public WaveformTestBeatmap(AudioManager audioManager) - : this(audioManager, new WaveformBeatmap()) + public WaveformTestBeatmap(AudioManager audioManager, RulesetInfo rulesetInfo = null) + : this(audioManager, new TestBeatmap(rulesetInfo ?? new OsuRuleset().RulesetInfo)) { } @@ -63,21 +62,5 @@ namespace osu.Game.Tests return reader.Filenames.First(f => f.EndsWith(".mp3", StringComparison.Ordinal)); } } - - private class WaveformBeatmap : TestBeatmap - { - public WaveformBeatmap() - : base(new CatchRuleset().RulesetInfo) - { - } - - protected override Beatmap CreateBeatmap() - { - using (var reader = getZipReader()) - using (var beatmapStream = reader.GetStream(reader.Filenames.First(f => f.EndsWith(".osu", StringComparison.Ordinal)))) - using (var beatmapReader = new LineBufferedReader(beatmapStream)) - return Decoder.GetDecoder(beatmapReader).Decode(beatmapReader); - } - } } } From 832d52a05678b723aed22832bfc31cb5a77e6c8a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 18:19:07 +0900 Subject: [PATCH 4619/6909] Fix hitobject sample tests --- osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs | 2 -- osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 11 +++++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs index 6b95931b21..64eaafbe75 100644 --- a/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneHitObjectSamples.cs @@ -3,7 +3,6 @@ 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.Beatmaps; @@ -12,7 +11,6 @@ using static osu.Game.Skinning.LegacySkinConfiguration; namespace osu.Game.Tests.Gameplay { - [HeadlessTest] public class TestSceneHitObjectSamples : HitObjectSampleTest { protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index 1e43e5d148..e3557222d5 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -9,11 +9,14 @@ using System.Threading.Tasks; 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.Rulesets.Objects; +using osu.Game.Screens.Ranking; using osu.Game.Skinning; using osu.Game.Storyboards; using osu.Game.Tests.Visual; @@ -21,6 +24,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Beatmaps { + [HeadlessTest] public abstract class HitObjectSampleTest : PlayerTestScene { protected abstract IResourceStore Resources { get; } @@ -44,7 +48,9 @@ namespace osu.Game.Tests.Beatmaps private readonly TestResourceStore beatmapSkinResourceStore = new TestResourceStore(); private SkinSourceDependencyContainer dependencies; private IBeatmap currentTestBeatmap; + protected sealed override bool HasCustomSteps => true; + protected override bool Autoplay => true; protected sealed override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => new DependencyContainer(dependencies = new SkinSourceDependencyContainer(base.CreateChildDependencies(parent))); @@ -54,6 +60,8 @@ namespace osu.Game.Tests.Beatmaps protected sealed override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false); + protected void CreateTestWithBeatmap(string filename) { CreateTest(() => @@ -73,6 +81,9 @@ namespace osu.Game.Tests.Beatmaps currentTestBeatmap.BeatmapInfo.Ruleset = rulesetStore.GetRuleset(currentTestBeatmap.BeatmapInfo.RulesetID); }); }); + + AddStep("seek to completion", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.Objects.Last().GetEndTime())); + AddUntilStep("results displayed", () => Stack.CurrentScreen is ResultsScreen); } protected void SetupSkins(string beatmapFile, string userFile) From feabca860bbbdc603fa391c7f9d19822789d26cc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 18:35:33 +0900 Subject: [PATCH 4620/6909] Fix sample playback test --- .../Visual/Gameplay/TestSceneGameplaySamplePlayback.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index b86cb69eb4..7c6a213fe2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -23,11 +23,13 @@ namespace osu.Game.Tests.Visual.Gameplay DrawableSample[] samples = null; ISamplePlaybackDisabler sampleDisabler = null; - AddStep("get variables", () => + AddUntilStep("get variables", () => { sampleDisabler = Player; - slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); - samples = slider.ChildrenOfType().ToArray(); + slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).FirstOrDefault(); + samples = slider?.ChildrenOfType().ToArray(); + + return slider != null; }); AddUntilStep("wait for slider sliding then seek", () => From 92189e35ccb160bb41d6a4a52a9073f1b072bf0d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 18:52:53 +0900 Subject: [PATCH 4621/6909] Make playfield KeepAlive methods internal --- osu.Game/Rulesets/UI/Playfield.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index f12db57199..fb2a60399b 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -210,7 +210,7 @@ namespace osu.Game.Rulesets.UI /// /// The to set. /// Whether to keep always alive. - public void SetKeepAlive(HitObject hitObject, bool keepAlive) + internal void SetKeepAlive(HitObject hitObject, bool keepAlive) { if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) { @@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.UI /// /// Keeps all s alive within this and all nested s. /// - public void KeepAllAlive() + internal void KeepAllAlive() { foreach (var (_, entry) in lifetimeEntryMap) entry.KeepAlive = true; From 4a4219fd117edb714c9493bd0674ccca78df8c91 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 18:53:37 +0900 Subject: [PATCH 4622/6909] Add region --- osu.Game/Rulesets/UI/Playfield.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index fb2a60399b..e2578e9822 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -154,8 +154,6 @@ namespace osu.Game.Rulesets.UI return false; } - private readonly Dictionary lifetimeEntryMap = new Dictionary(); - /// /// Adds a for a pooled to this . /// @@ -205,6 +203,10 @@ namespace osu.Game.Rulesets.UI { } + #region Editor logic + + private readonly Dictionary lifetimeEntryMap = new Dictionary(); + /// /// Sets whether to keep a given always alive within this or any nested . /// @@ -276,6 +278,8 @@ namespace osu.Game.Rulesets.UI } } + #endregion + /// /// The cursor currently being used by this . May be null if no cursor is provided. /// From d83b479c8122a0819f81819ef4967c80cff1b483 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 18:54:49 +0900 Subject: [PATCH 4623/6909] Internalise lifetime extensions and events too --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 8 +- osu.Game/Rulesets/UI/Playfield.cs | 186 ++++++++++----------- 2 files changed, 97 insertions(+), 97 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index bca2466968..25fb7ab9f3 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this represents the time when the s become alive. /// - public event Action HitObjectUsageBegan; + internal event Action HitObjectUsageBegan; /// /// Invoked when a becomes unused by a . @@ -56,17 +56,17 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this represents the time when the s become dead. /// - public event Action HitObjectUsageFinished; + internal event Action HitObjectUsageFinished; /// /// The amount of time prior to the current time within which s should be considered alive. /// - public double PastLifetimeExtension { get; set; } + internal double PastLifetimeExtension { get; set; } /// /// The amount of time after the current time within which s should be considered alive. /// - public double FutureLifetimeExtension { get; set; } + internal double FutureLifetimeExtension { get; set; } private readonly Dictionary startTimeMap = new Dictionary(); private readonly Dictionary drawableMap = new Dictionary(); diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index e2578e9822..6747145d50 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -29,22 +29,6 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; - /// - /// Invoked when a becomes used by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become alive. - /// - public event Action HitObjectUsageBegan; - - /// - /// Invoked when a becomes unused by a . - /// - /// - /// If this uses pooled objects, this represents the time when the s become dead. - /// - public event Action HitObjectUsageFinished; - /// /// The contained in this Playfield. /// @@ -203,83 +187,6 @@ namespace osu.Game.Rulesets.UI { } - #region Editor logic - - private readonly Dictionary lifetimeEntryMap = new Dictionary(); - - /// - /// Sets whether to keep a given always alive within this or any nested . - /// - /// The to set. - /// Whether to keep always alive. - internal void SetKeepAlive(HitObject hitObject, bool keepAlive) - { - if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) - { - entry.KeepAlive = keepAlive; - return; - } - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var p in nestedPlayfields.Value) - p.SetKeepAlive(hitObject, keepAlive); - } - - /// - /// Keeps all s alive within this and all nested s. - /// - internal void KeepAllAlive() - { - foreach (var (_, entry) in lifetimeEntryMap) - entry.KeepAlive = true; - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var p in nestedPlayfields.Value) - p.KeepAllAlive(); - } - - /// - /// The amount of time prior to the current time within which s should be considered alive. - /// - public double PastLifetimeExtension - { - get => HitObjectContainer.PastLifetimeExtension; - set - { - HitObjectContainer.PastLifetimeExtension = value; - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var nested in nestedPlayfields.Value) - nested.PastLifetimeExtension = value; - } - } - - /// - /// The amount of time after the current time within which s should be considered alive. - /// - public double FutureLifetimeExtension - { - get => HitObjectContainer.FutureLifetimeExtension; - set - { - HitObjectContainer.FutureLifetimeExtension = value; - - if (!nestedPlayfields.IsValueCreated) - return; - - foreach (var nested in nestedPlayfields.Value) - nested.FutureLifetimeExtension = value; - } - } - - #endregion - /// /// The cursor currently being used by this . May be null if no cursor is provided. /// @@ -339,6 +246,99 @@ namespace osu.Game.Rulesets.UI /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); + #region Editor logic + + /// + /// Invoked when a becomes used by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become alive. + /// + internal event Action HitObjectUsageBegan; + + /// + /// Invoked when a becomes unused by a . + /// + /// + /// If this uses pooled objects, this represents the time when the s become dead. + /// + internal event Action HitObjectUsageFinished; + + private readonly Dictionary lifetimeEntryMap = new Dictionary(); + + /// + /// Sets whether to keep a given always alive within this or any nested . + /// + /// The to set. + /// Whether to keep always alive. + internal void SetKeepAlive(HitObject hitObject, bool keepAlive) + { + if (lifetimeEntryMap.TryGetValue(hitObject, out var entry)) + { + entry.KeepAlive = keepAlive; + return; + } + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.SetKeepAlive(hitObject, keepAlive); + } + + /// + /// Keeps all s alive within this and all nested s. + /// + internal void KeepAllAlive() + { + foreach (var (_, entry) in lifetimeEntryMap) + entry.KeepAlive = true; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var p in nestedPlayfields.Value) + p.KeepAllAlive(); + } + + /// + /// The amount of time prior to the current time within which s should be considered alive. + /// + internal double PastLifetimeExtension + { + get => HitObjectContainer.PastLifetimeExtension; + set + { + HitObjectContainer.PastLifetimeExtension = value; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var nested in nestedPlayfields.Value) + nested.PastLifetimeExtension = value; + } + } + + /// + /// The amount of time after the current time within which s should be considered alive. + /// + internal double FutureLifetimeExtension + { + get => HitObjectContainer.FutureLifetimeExtension; + set + { + HitObjectContainer.FutureLifetimeExtension = value; + + if (!nestedPlayfields.IsValueCreated) + return; + + foreach (var nested in nestedPlayfields.Value) + nested.FutureLifetimeExtension = value; + } + } + + #endregion + public class InvisibleCursorContainer : GameplayCursorContainer { protected override Drawable CreateCursor() => new InvisibleCursor(); From 1e05fd48e239e1f8622935122bb2bfb82e2af467 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 21:43:53 +0900 Subject: [PATCH 4624/6909] Fix hidden mod crash --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 84a335750a..7c1dd46c02 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -28,10 +28,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override void ApplyToDrawableHitObjects(IEnumerable drawables) { foreach (var d in drawables) - { d.HitObjectApplied += applyFadeInAdjustment; - applyFadeInAdjustment(d); - } base.ApplyToDrawableHitObjects(drawables); } From 9792d1fc738ffeaad759d3a6826af94de612f763 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 13 Nov 2020 23:03:44 +0900 Subject: [PATCH 4625/6909] Fix slider tests --- .../Mods/TestSceneOsuModSpunOut.cs | 8 ++++++-- .../TestSceneSliderSnaking.cs | 17 ++++++++-------- osu.Game/Rulesets/UI/DrawableRuleset.cs | 20 ++++++++++++++++--- 3 files changed, 32 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index d8064d36ea..7b909d2907 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Mod = new OsuModSpunOut(), Autoplay = false, Beatmap = singleSpinnerBeatmap, - PassCondition = () => Player.ChildrenOfType().Single().Progress >= 1 + PassCondition = () => Player.ChildrenOfType().SingleOrDefault()?.Progress >= 1 }); [TestCase(null)] @@ -45,7 +45,11 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods Mods = mods, Autoplay = false, Beatmap = singleSpinnerBeatmap, - PassCondition = () => Precision.AlmostEquals(Player.ChildrenOfType().Single().SpinsPerMinute, 286, 1) + PassCondition = () => + { + var counter = Player.ChildrenOfType().SingleOrDefault(); + return counter != null && Precision.AlmostEquals(counter.SpinsPerMinute, 286, 1); + } }); } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index 3d100e4b1c..b71400b71d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests config.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); } - private DrawableSlider slider; + private DrawableSlider drawableSlider; [SetUpSteps] public override void SetUpSteps() @@ -68,7 +68,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); double startTime = hitObjects[sliderIndex].StartTime; - retrieveDrawableSlider(sliderIndex); + addSeekStep(startTime); + retrieveDrawableSlider((Slider)hitObjects[sliderIndex]); setSnaking(true); ensureSnakingIn(startTime + fade_in_modifier); @@ -93,7 +94,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddUntilStep("wait for track to start running", () => Beatmap.Value.Track.IsRunning); double startTime = hitObjects[sliderIndex].StartTime; - retrieveDrawableSlider(sliderIndex); + addSeekStep(startTime); + retrieveDrawableSlider((Slider)hitObjects[sliderIndex]); setSnaking(false); ensureNoSnakingIn(startTime + fade_in_modifier); @@ -127,9 +129,8 @@ 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(Slider slider) => AddUntilStep($"retrieve slider @ {slider.StartTime}", () => + (drawableSlider = (DrawableSlider)Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == slider)) != null); private void ensureSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionIncreased); private void ensureNoSnakingIn(double startTime) => checkPositionChange(startTime, sliderEnd, positionRemainsSame); @@ -150,13 +151,13 @@ namespace osu.Game.Rulesets.Osu.Tests 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 List sliderCurve => ((PlaySliderBody)drawableSlider.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 drawable = Player.DrawableRuleset.Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObjects[1]); var repeat = drawable.ChildrenOfType>().First().Children.First(); return repeat.Position; } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index c912348604..e3c81d0f57 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -593,10 +593,24 @@ namespace osu.Game.Rulesets.UI [CanBeNull] public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject) { - if (!pools.TryGetValue(hitObject.GetType(), out var pool)) - return null; + var lookupType = hitObject.GetType(); - return (DrawableHitObject)pool.Get(d => + IDrawablePool pool; + + // Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists. + if (!pools.TryGetValue(lookupType, out pool)) + { + foreach (var (t, p) in pools) + { + if (!t.IsInstanceOfType(hitObject)) + continue; + + pools[lookupType] = pool = p; + break; + } + } + + return (DrawableHitObject)pool?.Get(d => { var dho = (DrawableHitObject)d; From 36f1833f6eecbbacbb69d85c01a0d53d07f23241 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 14 Nov 2020 00:41:18 +0900 Subject: [PATCH 4626/6909] Move hitobject pooling to Playfield --- .../Gameplay/TestScenePoolingRuleset.cs | 19 +-- .../Objects/Drawables/DrawableHitObject.cs | 4 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 69 +---------- osu.Game/Rulesets/UI/HitObjectContainer.cs | 4 +- osu.Game/Rulesets/UI/HitObjectPoolProvider.cs | 111 ++++++++++++++++++ osu.Game/Rulesets/UI/Playfield.cs | 3 +- 6 files changed, 130 insertions(+), 80 deletions(-) create mode 100644 osu.Game/Rulesets/UI/HitObjectPoolProvider.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index 242eaf7b7d..2e1e667d0d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -134,19 +134,13 @@ namespace osu.Game.Tests.Visual.Gameplay { } - [BackgroundDependencyLoader] - private void load() - { - RegisterPool(PoolSize); - } - protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => null; protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); - protected override Playfield CreatePlayfield() => new TestPlayfield(); + protected override Playfield CreatePlayfield() => new TestPlayfield(PoolSize); private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry { @@ -161,11 +155,20 @@ namespace osu.Game.Tests.Visual.Gameplay private class TestPlayfield : Playfield { - public TestPlayfield() + private readonly int poolSize; + + public TestPlayfield(int poolSize) { + this.poolSize = poolSize; AddInternal(HitObjectContainer); } + [BackgroundDependencyLoader] + private void load() + { + RegisterPool(poolSize); + } + protected override GameplayCursorContainer CreateCursor() => null; } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 3e3936b45a..c22257e544 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private HitObjectLifetimeEntry lifetimeEntry; [Resolved(CanBeNull = true)] - private DrawableRuleset drawableRuleset { get; set; } + private HitObjectPoolProvider poolProvider { get; set; } private Container samplesContainer; @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in HitObject.NestedHitObjects) { - var drawableNested = drawableRuleset?.GetPooledDrawableRepresentation(h) + var drawableNested = poolProvider?.GetPooledDrawableRepresentation(h) ?? CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index c912348604..5022b571fd 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -15,10 +15,8 @@ using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.Pooling; using osu.Framework.Input; using osu.Framework.Input.Events; using osu.Game.Configuration; @@ -327,9 +325,8 @@ namespace osu.Game.Rulesets.UI /// Creates a to represent a . /// /// - /// If this method returns null, then this will assume the requested type is being pooled, - /// and will instead attempt to retrieve the s at the point they should become alive via pools registered through - /// or . + /// If this method returns null, then this will assume the requested type is being pooled inside the , + /// and will instead attempt to retrieve the s at the point they should become alive via pools registered in the . /// /// The to represent. /// The representing . @@ -550,68 +547,8 @@ namespace osu.Game.Rulesets.UI /// public abstract void CancelResume(); - private readonly Dictionary pools = new Dictionary(); private readonly Dictionary lifetimeEntries = new Dictionary(); - /// - /// Registers a default pool with this which is to be used whenever - /// representations are requested for the given type (via ). - /// - /// The number of s to be initially stored in the pool. - /// - /// The maximum number of s that can be stored in the pool. - /// If this limit is exceeded, every subsequent will be created anew instead of being retrieved from the pool, - /// until some of the existing s are returned to the pool. - /// - /// The type. - /// The receiver for s. - protected void RegisterPool(int initialSize, int? maximumSize = null) - where TObject : HitObject - where TDrawable : DrawableHitObject, new() - => RegisterPool(new DrawablePool(initialSize, maximumSize)); - - /// - /// Registers a custom pool with this which is to be used whenever - /// representations are requested for the given type (via ). - /// - /// The to register. - /// The type. - /// The receiver for s. - protected void RegisterPool([NotNull] DrawablePool pool) - where TObject : HitObject - where TDrawable : DrawableHitObject, new() - { - pools[typeof(TObject)] = pool; - AddInternal(pool); - } - - /// - /// Attempts to retrieve the poolable representation of a . - /// - /// The to retrieve the representation of. - /// The representing , or null if no poolable representation exists. - [CanBeNull] - public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject) - { - if (!pools.TryGetValue(hitObject.GetType(), out var pool)) - return null; - - return (DrawableHitObject)pool.Get(d => - { - var dho = (DrawableHitObject)d; - - // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. - // This is done before Apply() so that the state is updated once when the hitobject is applied. - if (!dho.IsLoaded) - { - foreach (var m in Mods.OfType()) - m.ApplyToDrawableHitObjects(dho.Yield()); - } - - dho.Apply(hitObject, GetLifetimeEntry(hitObject)); - }); - } - /// /// Creates the for a given . /// @@ -629,7 +566,7 @@ namespace osu.Game.Rulesets.UI /// The to retrieve or create the for. /// The for . [NotNull] - protected HitObjectLifetimeEntry GetLifetimeEntry([NotNull] HitObject hitObject) + internal HitObjectLifetimeEntry GetLifetimeEntry([NotNull] HitObject hitObject) { if (lifetimeEntries.TryGetValue(hitObject, out var entry)) return entry; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 25fb7ab9f3..8de9f41482 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.UI private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); [Resolved(CanBeNull = true)] - private DrawableRuleset drawableRuleset { get; set; } + private HitObjectPoolProvider poolProvider { get; set; } public HitObjectContainer() { @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI { Debug.Assert(!drawableMap.ContainsKey(entry)); - var drawable = drawableRuleset.GetPooledDrawableRepresentation(entry.HitObject); + var drawable = poolProvider.GetPooledDrawableRepresentation(entry.HitObject); if (drawable == null) throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); diff --git a/osu.Game/Rulesets/UI/HitObjectPoolProvider.cs b/osu.Game/Rulesets/UI/HitObjectPoolProvider.cs new file mode 100644 index 0000000000..ee3aee59b4 --- /dev/null +++ b/osu.Game/Rulesets/UI/HitObjectPoolProvider.cs @@ -0,0 +1,111 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.UI +{ + /// + /// A that pools s and allows children to retrieve them via . + /// + [Cached(typeof(HitObjectPoolProvider))] + public class HitObjectPoolProvider : CompositeDrawable + { + [Resolved] + private DrawableRuleset drawableRuleset { get; set; } + + [Resolved] + private IReadOnlyList mods { get; set; } + + [Resolved(CanBeNull = true)] + private HitObjectPoolProvider parentProvider { get; set; } + + private readonly Dictionary pools = new Dictionary(); + + /// + /// Registers a default pool with this which is to be used whenever + /// representations are requested for the given type (via ). + /// + /// The number of s to be initially stored in the pool. + /// + /// The maximum number of s that can be stored in the pool. + /// If this limit is exceeded, every subsequent will be created anew instead of being retrieved from the pool, + /// until some of the existing s are returned to the pool. + /// + /// The type. + /// The receiver for s. + protected void RegisterPool(int initialSize, int? maximumSize = null) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + => RegisterPool(new DrawablePool(initialSize, maximumSize)); + + /// + /// Registers a custom pool with this which is to be used whenever + /// representations are requested for the given type (via ). + /// + /// The to register. + /// The type. + /// The receiver for s. + protected void RegisterPool([NotNull] DrawablePool pool) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + { + pools[typeof(TObject)] = pool; + AddInternal(pool); + } + + /// + /// Attempts to retrieve the poolable representation of a . + /// + /// The to retrieve the representation of. + /// The representing , or null if no poolable representation exists. + [CanBeNull] + public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject) + { + var lookupType = hitObject.GetType(); + + IDrawablePool pool; + + // Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists. + if (!pools.TryGetValue(lookupType, out pool)) + { + foreach (var (t, p) in pools) + { + if (!t.IsInstanceOfType(hitObject)) + continue; + + pools[lookupType] = pool = p; + break; + } + } + + if (pool == null) + return parentProvider?.GetPooledDrawableRepresentation(hitObject); + + return (DrawableHitObject)pool.Get(d => + { + var dho = (DrawableHitObject)d; + + // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. + // This is done before Apply() so that the state is updated once when the hitobject is applied. + if (!dho.IsLoaded) + { + foreach (var m in mods.OfType()) + m.ApplyToDrawableHitObjects(dho.Yield()); + } + + dho.Apply(hitObject, drawableRuleset.GetLifetimeEntry(hitObject)); + }); + } + } +} diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 6747145d50..1d0196d173 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -9,7 +9,6 @@ using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -17,7 +16,7 @@ using osuTK; namespace osu.Game.Rulesets.UI { - public abstract class Playfield : CompositeDrawable + public abstract class Playfield : HitObjectPoolProvider { /// /// Invoked when a is judged. From c71b237c4f8d73ce957a23ae4a2c56478725307a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 14 Nov 2020 00:54:57 +0900 Subject: [PATCH 4627/6909] Merge all pooling support into Playfield --- .../Gameplay/TestScenePoolingRuleset.cs | 24 +-- .../Objects/Drawables/DrawableHitObject.cs | 4 +- osu.Game/Rulesets/UI/DrawableRuleset.cs | 55 +----- osu.Game/Rulesets/UI/HitObjectContainer.cs | 4 +- osu.Game/Rulesets/UI/HitObjectPoolProvider.cs | 111 ------------ .../Rulesets/UI/IPooledHitObjectProvider.cs | 20 +++ osu.Game/Rulesets/UI/Playfield.cs | 167 ++++++++++++++---- 7 files changed, 172 insertions(+), 213 deletions(-) delete mode 100644 osu.Game/Rulesets/UI/HitObjectPoolProvider.cs create mode 100644 osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index 2e1e667d0d..d009d805f0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -134,23 +134,11 @@ namespace osu.Game.Tests.Visual.Gameplay { } - protected override HitObjectLifetimeEntry CreateLifetimeEntry(TestHitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); - public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => null; protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); protected override Playfield CreatePlayfield() => new TestPlayfield(PoolSize); - - private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry - { - public TestHitObjectLifetimeEntry(HitObject hitObject) - : base(hitObject) - { - } - - protected override double InitialLifetimeOffset => 0; - } } private class TestPlayfield : Playfield @@ -169,9 +157,21 @@ namespace osu.Game.Tests.Visual.Gameplay RegisterPool(poolSize); } + protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); + protected override GameplayCursorContainer CreateCursor() => null; } + private class TestHitObjectLifetimeEntry : HitObjectLifetimeEntry + { + public TestHitObjectLifetimeEntry(HitObject hitObject) + : base(hitObject) + { + } + + protected override double InitialLifetimeOffset => 0; + } + private class TestBeatmapConverter : BeatmapConverter { public TestBeatmapConverter(IBeatmap beatmap, Ruleset ruleset) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index c22257e544..b400c532c5 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Objects.Drawables private HitObjectLifetimeEntry lifetimeEntry; [Resolved(CanBeNull = true)] - private HitObjectPoolProvider poolProvider { get; set; } + private IPooledHitObjectProvider pooledObjectProvider { get; set; } private Container samplesContainer; @@ -212,7 +212,7 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in HitObject.NestedHitObjects) { - var drawableNested = poolProvider?.GetPooledDrawableRepresentation(h) + var drawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h) ?? CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 5022b571fd..c1a601eaae 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -15,7 +15,6 @@ using System.Linq; using System.Threading; using JetBrains.Annotations; using osu.Framework.Bindables; -using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics.Cursor; using osu.Framework.Input; using osu.Framework.Input.Events; @@ -246,7 +245,7 @@ namespace osu.Game.Rulesets.UI if (drawableRepresentation != null) Playfield.Add(drawableRepresentation); else - Playfield.Add(GetLifetimeEntry(hitObject)); + Playfield.Add(hitObject); } /// @@ -258,15 +257,10 @@ namespace osu.Game.Rulesets.UI /// The to remove. public bool RemoveHitObject(TObject hitObject) { - var entry = GetLifetimeEntry(hitObject); - - // May have been newly-created by the above call - remove it anyway. - RemoveLifetimeEntry(hitObject); - - if (Playfield.Remove(entry)) + if (Playfield.Remove(hitObject)) return true; - // If the entry was not removed from the playfield, assume the hitobject is not being pooled and attempt a direct removal. + // If the entry was not removed from the playfield, assume the hitobject is not being pooled and attempt a direct drawable removal. var drawableObject = Playfield.AllHitObjects.SingleOrDefault(d => d.HitObject == hitObject); if (drawableObject != null) return Playfield.Remove(drawableObject); @@ -274,16 +268,6 @@ namespace osu.Game.Rulesets.UI return false; } - protected sealed override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) - { - if (!(hitObject is TObject tHitObject)) - throw new InvalidOperationException($"Unexpected hitobject type: {hitObject.GetType().ReadableName()}"); - - return CreateLifetimeEntry(tHitObject); - } - - protected virtual HitObjectLifetimeEntry CreateLifetimeEntry(TObject hitObject) => new HitObjectLifetimeEntry(hitObject); - public override void SetRecordTarget(Replay recordingReplay) { if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager)) @@ -546,39 +530,6 @@ namespace osu.Game.Rulesets.UI /// Invoked when the user requests to pause while the resume overlay is active. /// public abstract void CancelResume(); - - private readonly Dictionary lifetimeEntries = new Dictionary(); - - /// - /// Creates the for a given . - /// - /// - /// This may be overridden to provide custom lifetime control (e.g. via . - /// - /// The to create the entry for. - /// The . - [NotNull] - protected abstract HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject); - - /// - /// Retrieves or creates the for a given . - /// - /// The to retrieve or create the for. - /// The for . - [NotNull] - internal HitObjectLifetimeEntry GetLifetimeEntry([NotNull] HitObject hitObject) - { - if (lifetimeEntries.TryGetValue(hitObject, out var entry)) - return entry; - - return lifetimeEntries[hitObject] = CreateLifetimeEntry(hitObject); - } - - /// - /// Removes the for a . - /// - /// The to remove the for. - internal void RemoveLifetimeEntry([NotNull] HitObject hitObject) => lifetimeEntries.Remove(hitObject); } public class BeatmapInvalidForRulesetException : ArgumentException diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 8de9f41482..1dc029506f 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.UI private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); [Resolved(CanBeNull = true)] - private HitObjectPoolProvider poolProvider { get; set; } + private IPooledHitObjectProvider pooledObjectProvider { get; set; } public HitObjectContainer() { @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI { Debug.Assert(!drawableMap.ContainsKey(entry)); - var drawable = poolProvider.GetPooledDrawableRepresentation(entry.HitObject); + var drawable = pooledObjectProvider.GetPooledDrawableRepresentation(entry.HitObject); if (drawable == null) throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); diff --git a/osu.Game/Rulesets/UI/HitObjectPoolProvider.cs b/osu.Game/Rulesets/UI/HitObjectPoolProvider.cs deleted file mode 100644 index ee3aee59b4..0000000000 --- a/osu.Game/Rulesets/UI/HitObjectPoolProvider.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.Generic; -using System.Linq; -using JetBrains.Annotations; -using osu.Framework.Allocation; -using osu.Framework.Extensions.IEnumerableExtensions; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Pooling; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; - -namespace osu.Game.Rulesets.UI -{ - /// - /// A that pools s and allows children to retrieve them via . - /// - [Cached(typeof(HitObjectPoolProvider))] - public class HitObjectPoolProvider : CompositeDrawable - { - [Resolved] - private DrawableRuleset drawableRuleset { get; set; } - - [Resolved] - private IReadOnlyList mods { get; set; } - - [Resolved(CanBeNull = true)] - private HitObjectPoolProvider parentProvider { get; set; } - - private readonly Dictionary pools = new Dictionary(); - - /// - /// Registers a default pool with this which is to be used whenever - /// representations are requested for the given type (via ). - /// - /// The number of s to be initially stored in the pool. - /// - /// The maximum number of s that can be stored in the pool. - /// If this limit is exceeded, every subsequent will be created anew instead of being retrieved from the pool, - /// until some of the existing s are returned to the pool. - /// - /// The type. - /// The receiver for s. - protected void RegisterPool(int initialSize, int? maximumSize = null) - where TObject : HitObject - where TDrawable : DrawableHitObject, new() - => RegisterPool(new DrawablePool(initialSize, maximumSize)); - - /// - /// Registers a custom pool with this which is to be used whenever - /// representations are requested for the given type (via ). - /// - /// The to register. - /// The type. - /// The receiver for s. - protected void RegisterPool([NotNull] DrawablePool pool) - where TObject : HitObject - where TDrawable : DrawableHitObject, new() - { - pools[typeof(TObject)] = pool; - AddInternal(pool); - } - - /// - /// Attempts to retrieve the poolable representation of a . - /// - /// The to retrieve the representation of. - /// The representing , or null if no poolable representation exists. - [CanBeNull] - public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject) - { - var lookupType = hitObject.GetType(); - - IDrawablePool pool; - - // Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists. - if (!pools.TryGetValue(lookupType, out pool)) - { - foreach (var (t, p) in pools) - { - if (!t.IsInstanceOfType(hitObject)) - continue; - - pools[lookupType] = pool = p; - break; - } - } - - if (pool == null) - return parentProvider?.GetPooledDrawableRepresentation(hitObject); - - return (DrawableHitObject)pool.Get(d => - { - var dho = (DrawableHitObject)d; - - // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. - // This is done before Apply() so that the state is updated once when the hitobject is applied. - if (!dho.IsLoaded) - { - foreach (var m in mods.OfType()) - m.ApplyToDrawableHitObjects(dho.Yield()); - } - - dho.Apply(hitObject, drawableRuleset.GetLifetimeEntry(hitObject)); - }); - } - } -} diff --git a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs new file mode 100644 index 0000000000..d8240d892f --- /dev/null +++ b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.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 JetBrains.Annotations; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.UI +{ + internal interface IPooledHitObjectProvider + { + /// + /// Attempts to retrieve the poolable representation of a . + /// + /// The to retrieve the representation of. + /// The representing , or null if no poolable representation exists. + [CanBeNull] + public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject); + } +} diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 1d0196d173..80e33e0ec5 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -4,11 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; @@ -16,7 +19,8 @@ using osuTK; namespace osu.Game.Rulesets.UI { - public abstract class Playfield : HitObjectPoolProvider + [Cached(typeof(IPooledHitObjectProvider))] + public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider { /// /// Invoked when a is judged. @@ -137,39 +141,6 @@ namespace osu.Game.Rulesets.UI return false; } - /// - /// Adds a for a pooled to this . - /// - /// The controlling the lifetime of the . - public virtual void Add(HitObjectLifetimeEntry entry) - { - HitObjectContainer.Add(entry); - lifetimeEntryMap[entry.HitObject] = entry; - OnHitObjectAdded(entry.HitObject); - } - - /// - /// Removes a for a pooled from this . - /// - /// The controlling the lifetime of the . - /// Whether the was successfully removed. - public virtual bool Remove(HitObjectLifetimeEntry entry) - { - if (HitObjectContainer.Remove(entry)) - { - lifetimeEntryMap.Remove(entry.HitObject); - OnHitObjectRemoved(entry.HitObject); - return true; - } - - bool removedFromNested = false; - - if (nestedPlayfields.IsValueCreated) - removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(entry)); - - return removedFromNested; - } - /// /// Invoked when a is added to this . /// @@ -245,6 +216,134 @@ namespace osu.Game.Rulesets.UI /// protected virtual HitObjectContainer CreateHitObjectContainer() => new HitObjectContainer(); + #region Pooling support + + [Resolved(CanBeNull = true)] + private IPooledHitObjectProvider parentPooledObjectProvider { get; set; } + + private readonly Dictionary pools = new Dictionary(); + + /// + /// Adds a for a pooled to this . + /// + /// + public virtual void Add(HitObject hitObject) + { + var entry = CreateLifetimeEntry(hitObject); + lifetimeEntryMap[entry.HitObject] = entry; + + HitObjectContainer.Add(entry); + OnHitObjectAdded(entry.HitObject); + } + + /// + /// Removes a for a pooled from this . + /// + /// + /// Whether the was successfully removed. + public virtual bool Remove(HitObject hitObject) + { + if (lifetimeEntryMap.Remove(hitObject, out var entry)) + { + HitObjectContainer.Remove(entry); + OnHitObjectRemoved(hitObject); + return true; + } + + bool removedFromNested = false; + + if (nestedPlayfields.IsValueCreated) + removedFromNested = nestedPlayfields.Value.Any(p => p.Remove(hitObject)); + + return removedFromNested; + } + + /// + /// Creates the for a given . + /// + /// + /// This may be overridden to provide custom lifetime control (e.g. via . + /// + /// The to create the entry for. + /// The . + [NotNull] + protected virtual HitObjectLifetimeEntry CreateLifetimeEntry([NotNull] HitObject hitObject) => new HitObjectLifetimeEntry(hitObject); + + /// + /// Registers a default pool with this which is to be used whenever + /// representations are requested for the given type. + /// + /// The number of s to be initially stored in the pool. + /// + /// The maximum number of s that can be stored in the pool. + /// If this limit is exceeded, every subsequent will be created anew instead of being retrieved from the pool, + /// until some of the existing s are returned to the pool. + /// + /// The type. + /// The receiver for s. + protected void RegisterPool(int initialSize, int? maximumSize = null) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + => RegisterPool(new DrawablePool(initialSize, maximumSize)); + + /// + /// Registers a custom pool with this which is to be used whenever + /// representations are requested for the given type. + /// + /// The to register. + /// The type. + /// The receiver for s. + protected void RegisterPool([NotNull] DrawablePool pool) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + { + pools[typeof(TObject)] = pool; + AddInternal(pool); + } + + DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject) + { + var lookupType = hitObject.GetType(); + + IDrawablePool pool; + + // Tests may add derived hitobject instances for which pools don't exist. Try to find any applicable pool and dynamically assign the type if the pool exists. + if (!pools.TryGetValue(lookupType, out pool)) + { + foreach (var (t, p) in pools) + { + if (!t.IsInstanceOfType(hitObject)) + continue; + + pools[lookupType] = pool = p; + break; + } + } + + if (pool == null) + return parentPooledObjectProvider?.GetPooledDrawableRepresentation(hitObject); + + return (DrawableHitObject)pool.Get(d => + { + var dho = (DrawableHitObject)d; + + // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. + // This is done before Apply() so that the state is updated once when the hitobject is applied. + if (!dho.IsLoaded) + { + foreach (var m in mods.OfType()) + m.ApplyToDrawableHitObjects(dho.Yield()); + } + + if (!lifetimeEntryMap.TryGetValue(hitObject, out var entry)) + lifetimeEntryMap[hitObject] = entry = CreateLifetimeEntry(hitObject); + + dho.Apply(hitObject, entry); + }); + } + + #endregion + #region Editor logic /// From 5b60f32c7f96bc1b17fff30bf875c7e8f6acaf6c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 14 Nov 2020 01:03:23 +0900 Subject: [PATCH 4628/6909] Move implementation into OsuPlayfield --- .../Edit/DrawableOsuEditRuleset.cs | 10 ++--- .../UI/DrawableOsuRuleset.cs | 41 ------------------- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 33 +++++++++++++++ 3 files changed, 38 insertions(+), 46 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 05396ebc8b..8af6fd65ce 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -18,16 +18,16 @@ namespace osu.Game.Rulesets.Osu.Edit { } - protected override DrawablePool CreatePool(int initialSize, int? maximumSize = null) - => new OsuEditDrawablePool(Playfield.CheckHittable, Playfield.OnHitObjectLoaded, initialSize, maximumSize); - - protected override Playfield CreatePlayfield() => new OsuPlayfieldNoCursor(); + protected override Playfield CreatePlayfield() => new OsuEditPlayfield(); public override PlayfieldAdjustmentContainer CreatePlayfieldAdjustmentContainer() => new OsuPlayfieldAdjustmentContainer { Size = Vector2.One }; - private class OsuPlayfieldNoCursor : OsuPlayfield + private class OsuEditPlayfield : OsuPlayfield { protected override GameplayCursorContainer CreateCursor() => null; + + protected override DrawablePool CreatePool(int initialSize, int? maximumSize = null) + => new OsuEditDrawablePool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); } } } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 1d16c47818..69179137a6 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -4,18 +4,14 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Pooling; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play; @@ -34,35 +30,8 @@ namespace osu.Game.Rulesets.Osu.UI { } - [BackgroundDependencyLoader] - private void load() - { - registerPool(10, 100); - - registerPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(5, 50); - - registerPool(2, 20); - registerPool(10, 100); - registerPool(10, 100); - } - - private void registerPool(int initialSize, int? maximumSize = null) - where TObject : HitObject - where TDrawable : DrawableHitObject, new() - => RegisterPool(CreatePool(initialSize, maximumSize)); - - protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) - where TDrawable : DrawableHitObject, new() - => new OsuDrawablePool(Playfield.CheckHittable, Playfield.OnHitObjectLoaded, initialSize, maximumSize); - public override DrawableHitObject CreateDrawableRepresentation(OsuHitObject h) => null; - protected override HitObjectLifetimeEntry CreateLifetimeEntry(OsuHitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // always show the gameplay cursor protected override Playfield CreatePlayfield() => new OsuPlayfield(); @@ -87,15 +56,5 @@ namespace osu.Game.Rulesets.Osu.UI return 0; } } - - private class OsuHitObjectLifetimeEntry : HitObjectLifetimeEntry - { - public OsuHitObjectLifetimeEntry(HitObject hitObject) - : base(hitObject) - { - } - - protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt; - } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 5d59a6ff38..e8ff6c410f 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -97,8 +97,31 @@ namespace osu.Game.Rulesets.Osu.UI private void load(OsuRulesetConfigManager config) { config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); + + registerPool(10, 100); + + registerPool(10, 100); + registerPool(10, 100); + registerPool(10, 100); + registerPool(10, 100); + registerPool(5, 50); + + registerPool(2, 20); + registerPool(10, 100); + registerPool(10, 100); } + private void registerPool(int initialSize, int? maximumSize = null) + where TObject : HitObject + where TDrawable : DrawableHitObject, new() + => RegisterPool(CreatePool(initialSize, maximumSize)); + + protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) + where TDrawable : DrawableHitObject, new() + => new OsuDrawablePool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); + + protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); + protected override void OnHitObjectAdded(HitObject hitObject) { base.OnHitObjectAdded(hitObject); @@ -172,5 +195,15 @@ namespace osu.Game.Rulesets.Osu.UI return judgement; } } + + private class OsuHitObjectLifetimeEntry : HitObjectLifetimeEntry + { + public OsuHitObjectLifetimeEntry(HitObject hitObject) + : base(hitObject) + { + } + + protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt; + } } } From 21b015d63afffa9b0acdb0027f37d0b228614698 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 14 Nov 2020 01:06:38 +0900 Subject: [PATCH 4629/6909] Remove explicit public --- osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs index d8240d892f..315926dfc6 100644 --- a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs +++ b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs @@ -15,6 +15,6 @@ namespace osu.Game.Rulesets.UI /// The to retrieve the representation of. /// The representing , or null if no poolable representation exists. [CanBeNull] - public DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject); + DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject); } } From b4d4f5456c762f7867ef5a3db4e814a125348f38 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 14 Nov 2020 01:49:48 +0900 Subject: [PATCH 4630/6909] Fix broken fail judgement test --- .../Visual/Gameplay/TestSceneFailJudgement.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs index d80efb2c6e..745932315c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneFailJudgement.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Tests.Visual.Gameplay @@ -21,8 +22,14 @@ namespace osu.Game.Tests.Visual.Gameplay protected override void AddCheckSteps() { AddUntilStep("wait for fail", () => Player.HasFailed); - AddUntilStep("wait for multiple judged objects", () => ((FailPlayer)Player).DrawableRuleset.Playfield.AllHitObjects.Count(h => h.AllJudged) > 1); - AddAssert("total judgements == 1", () => ((FailPlayer)Player).HealthProcessor.JudgedHits >= 1); + AddUntilStep("wait for multiple judgements", () => ((FailPlayer)Player).ScoreProcessor.JudgedHits > 1); + AddAssert("total number of results == 1", () => + { + var score = new ScoreInfo(); + ((FailPlayer)Player).ScoreProcessor.PopulateScore(score); + + return score.Statistics.Values.Sum() == 1; + }); } private class FailPlayer : TestPlayer From 7a89e58483a998a484a944acd5edf4c61bdcbdcf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 20:49:06 +0100 Subject: [PATCH 4631/6909] Disable pressed/released action logic when rewinding --- .../Objects/Drawables/DrawableHoldNote.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index d9d740c145..59899637f9 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -249,6 +249,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return false; + // do not run any of this logic when rewinding, as it inverts order of presses/releases. + if (Time.Elapsed < 0) + return false; + if (CheckHittable?.Invoke(this, Time.Current) == false) return false; @@ -281,6 +285,10 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables if (action != Action.Value) return; + // do not run any of this logic when rewinding, as it inverts order of presses/releases. + if (Time.Elapsed < 0) + return; + // Make sure a hold was started if (HoldStartTime == null) return; From 2f33aeac9ff14d075957d7c053dd7b384d2caa37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 14:31:07 +0100 Subject: [PATCH 4632/6909] Move drawable instatiation to [SetUp] --- .../Skinning/ManiaHitObjectTestScene.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs index d24c81dac6..96444fd316 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/ManiaHitObjectTestScene.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.Framework.Allocation; +using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Mania.Objects.Drawables; @@ -15,8 +15,8 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning /// public abstract class ManiaHitObjectTestScene : ManiaSkinnableTestScene { - [BackgroundDependencyLoader] - private void load() + [SetUp] + public void SetUp() => Schedule(() => { SetContents(() => new FillFlowContainer { @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning }, } }); - } + }); protected abstract DrawableManiaHitObject CreateHitObject(); } From 2ccc81ccc0ca7406d21e6283c60a77f5849bf681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 14:31:24 +0100 Subject: [PATCH 4633/6909] Add test case for fading hold note --- .../Skinning/TestSceneHoldNote.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs index 9c4c2b3d5b..e88ff8e2ac 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHoldNote.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Bindables; @@ -26,6 +27,18 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning }); } + [Test] + public void TestFadeOnMiss() + { + AddStep("miss tick", () => + { + foreach (var holdNote in holdNotes) + holdNote.ChildrenOfType().First().MissForcefully(); + }); + } + + private IEnumerable holdNotes => CreatedDrawables.SelectMany(d => d.ChildrenOfType()); + protected override DrawableManiaHitObject CreateHitObject() { var note = new HoldNote { Duration = 1000 }; From 55a91dbbe031df2bb677a6518cf035961d4faf1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 15:16:33 +0100 Subject: [PATCH 4634/6909] Add fading on hit state change --- .../Skinning/LegacyBodyPiece.cs | 46 +++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index c0f0fcb4af..a9cd7b592f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Mania.Skinning { public class LegacyBodyPiece : LegacyManiaColumnElement { + private DrawableHoldNote holdNote; + private readonly IBindable direction = new Bindable(); private readonly IBindable isHitting = new Bindable(); @@ -38,6 +40,8 @@ namespace osu.Game.Rulesets.Mania.Skinning [BackgroundDependencyLoader] private void load(ISkinSource skin, IScrollingInfo scrollingInfo, DrawableHitObject drawableObject) { + holdNote = (DrawableHoldNote)drawableObject; + string imageName = GetColumnSkinConfig(skin, LegacyManiaSkinConfigurationLookups.HoldNoteBodyImage)?.Value ?? $"mania-note{FallbackColumnIndex}L"; @@ -92,11 +96,44 @@ namespace osu.Game.Rulesets.Mania.Skinning InternalChild = bodySprite; direction.BindTo(scrollingInfo.Direction); - direction.BindValueChanged(onDirectionChanged, true); - - var holdNote = (DrawableHoldNote)drawableObject; isHitting.BindTo(holdNote.IsHitting); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + direction.BindValueChanged(onDirectionChanged, true); isHitting.BindValueChanged(onIsHittingChanged, true); + + holdNote.ApplyCustomUpdateState += applyCustomUpdateState; + applyCustomUpdateState(holdNote, holdNote.State.Value); + } + + /// + /// Stores the start time of the fade animation that plays when any of the nested + /// hitobjects of the hold note are missed. + /// + private double? missFadeTime; + + private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) + { + switch (state) + { + case ArmedState.Miss: + missFadeTime ??= hitObject.StateUpdateTime; + + using (BeginAbsoluteSequence(missFadeTime.Value)) + { + // colour and duration matches stable + // transforms not applied to entire hold note in order to not affect hit lighting + holdNote.Head.FadeColour(Colour4.DarkGray, 60); + bodySprite?.FadeColour(Colour4.DarkGray, 60); + holdNote.Tail.FadeColour(Colour4.DarkGray, 60); + } + + break; + } } private void onIsHittingChanged(ValueChangedEvent isHitting) @@ -162,6 +199,9 @@ namespace osu.Game.Rulesets.Mania.Skinning { base.Dispose(isDisposing); + if (holdNote != null) + holdNote.ApplyCustomUpdateState -= applyCustomUpdateState; + lightContainer?.Expire(); } } From 4777b1be8108462cad86c5b81e2865888a1972e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 15:43:10 +0100 Subject: [PATCH 4635/6909] Fix fade not applying to tails sometimes --- .../Skinning/LegacyBodyPiece.cs | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index a9cd7b592f..a0cbc47df1 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -118,21 +118,22 @@ namespace osu.Game.Rulesets.Mania.Skinning private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) { - switch (state) + if (state == ArmedState.Miss) + missFadeTime = hitObject.StateUpdateTime; + + if (missFadeTime == null) + return; + + // this state update could come from any nested object of the hold note. + // make sure the transforms are consistent across all affected parts + // even if they're idle. + using (BeginAbsoluteSequence(missFadeTime.Value)) { - case ArmedState.Miss: - missFadeTime ??= hitObject.StateUpdateTime; - - using (BeginAbsoluteSequence(missFadeTime.Value)) - { - // colour and duration matches stable - // transforms not applied to entire hold note in order to not affect hit lighting - holdNote.Head.FadeColour(Colour4.DarkGray, 60); - bodySprite?.FadeColour(Colour4.DarkGray, 60); - holdNote.Tail.FadeColour(Colour4.DarkGray, 60); - } - - break; + // colour and duration matches stable + // transforms not applied to entire hold note in order to not affect hit lighting + holdNote.Head.FadeColour(Colour4.DarkGray, 60); + bodySprite?.FadeColour(Colour4.DarkGray, 60); + holdNote.Tail.FadeColour(Colour4.DarkGray, 60); } } From b62bf5798d40d0df7cd0e8ed63e59215dae6a088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 21:14:34 +0100 Subject: [PATCH 4636/6909] Store time of hold note break --- .../Objects/Drawables/DrawableHoldNote.cs | 8 ++++---- .../Objects/Drawables/DrawableHoldNoteTail.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index 59899637f9..a64cc6dc67 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -51,9 +51,9 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public double? HoldStartTime { get; private set; } /// - /// Whether the hold note has been released too early and shouldn't give full score for the release. + /// Time at which the hold note has been broken, i.e. released too early, resulting in a reduced score. /// - public bool HasBroken { get; private set; } + public double? HoldBrokenTime { get; private set; } /// /// Whether the hold note has been released potentially without having caused a break. @@ -238,7 +238,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables } if (Tail.Judged && !Tail.IsHit) - HasBroken = true; + HoldBrokenTime = Time.Current; } public bool OnPressed(ManiaAction action) @@ -298,7 +298,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables // If the key has been released too early, the user should not receive full score for the release if (!Tail.IsHit) - HasBroken = true; + HoldBrokenTime = Time.Current; releaseTime = Time.Current; } diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index c780c0836e..a4029e7893 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables ApplyResult(r => { // If the head wasn't hit or the hold note was broken, cap the max score to Meh. - if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HasBroken)) + if (result > HitResult.Meh && (!holdNote.Head.IsHit || holdNote.HoldBrokenTime != null)) result = HitResult.Meh; r.Type = result; From 2071cba944050ce4da997b1e8111d96650d12c22 Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 13 Nov 2020 12:32:23 -0800 Subject: [PATCH 4637/6909] Add music bindings to on screen display --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- .../Overlays/Music/MusicKeyBindingHandler.cs | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a4b99bb6e6..89a6ee8b07 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -196,7 +196,7 @@ namespace osu.Game.Configuration public Func LookupSkinName { private get; set; } - public Func LookupKeyBindings { private get; set; } + public Func LookupKeyBindings { get; set; } } public enum OsuSetting diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index e6edfb1e3e..0d6158d46f 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays.OSD; @@ -25,6 +26,9 @@ namespace osu.Game.Overlays.Music [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } + [Resolved] + private OsuConfigManager config { get; set; } + public bool OnPressed(GlobalAction action) { if (beatmap.Disabled) @@ -37,11 +41,11 @@ namespace osu.Game.Overlays.Music bool wasPlaying = musicController.IsPlaying; if (musicController.TogglePause()) - onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track")); + onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track", config.LookupKeyBindings(action))); return true; case GlobalAction.MusicNext: - musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track"))); + musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track", config.LookupKeyBindings(action)))); return true; @@ -51,11 +55,11 @@ namespace osu.Game.Overlays.Music switch (res) { case PreviousTrackResult.Restart: - onScreenDisplay?.Display(new MusicActionToast("Restart track")); + onScreenDisplay?.Display(new MusicActionToast("Restart track", config.LookupKeyBindings(action))); break; case PreviousTrackResult.Previous: - onScreenDisplay?.Display(new MusicActionToast("Previous track")); + onScreenDisplay?.Display(new MusicActionToast("Previous track", config.LookupKeyBindings(action))); break; } }); @@ -72,8 +76,8 @@ namespace osu.Game.Overlays.Music private class MusicActionToast : Toast { - public MusicActionToast(string action) - : base("Music Playback", action, string.Empty) + public MusicActionToast(string action, string shortcut) + : base("Music Playback", action, shortcut) { } } From a199a957cca03746a93b2ebf1e5ffa0a90b43bd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 21:47:55 +0100 Subject: [PATCH 4638/6909] Use stored hold note break time to fade upon it --- .../Skinning/LegacyBodyPiece.cs | 53 +++++++++++-------- 1 file changed, 31 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index a0cbc47df1..9ed25115d8 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -23,6 +23,12 @@ namespace osu.Game.Rulesets.Mania.Skinning private readonly IBindable direction = new Bindable(); private readonly IBindable isHitting = new Bindable(); + /// + /// Stores the start time of the fade animation that plays when any of the nested + /// hitobjects of the hold note are missed. + /// + private readonly Bindable missFadeTime = new Bindable(); + [CanBeNull] private Drawable bodySprite; @@ -105,36 +111,16 @@ namespace osu.Game.Rulesets.Mania.Skinning direction.BindValueChanged(onDirectionChanged, true); isHitting.BindValueChanged(onIsHittingChanged, true); + missFadeTime.BindValueChanged(onMissFadeTimeChanged, true); holdNote.ApplyCustomUpdateState += applyCustomUpdateState; applyCustomUpdateState(holdNote, holdNote.State.Value); } - /// - /// Stores the start time of the fade animation that plays when any of the nested - /// hitobjects of the hold note are missed. - /// - private double? missFadeTime; - private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) { if (state == ArmedState.Miss) - missFadeTime = hitObject.StateUpdateTime; - - if (missFadeTime == null) - return; - - // this state update could come from any nested object of the hold note. - // make sure the transforms are consistent across all affected parts - // even if they're idle. - using (BeginAbsoluteSequence(missFadeTime.Value)) - { - // colour and duration matches stable - // transforms not applied to entire hold note in order to not affect hit lighting - holdNote.Head.FadeColour(Colour4.DarkGray, 60); - bodySprite?.FadeColour(Colour4.DarkGray, 60); - holdNote.Tail.FadeColour(Colour4.DarkGray, 60); - } + missFadeTime.Value ??= hitObject.StateUpdateTime; } private void onIsHittingChanged(ValueChangedEvent isHitting) @@ -196,6 +182,29 @@ namespace osu.Game.Rulesets.Mania.Skinning } } + private void onMissFadeTimeChanged(ValueChangedEvent missFadeTimeChange) + { + if (missFadeTimeChange.NewValue == null) + return; + + // this update could come from any nested object of the hold note (or even from an input). + // make sure the transforms are consistent across all affected parts. + using (BeginAbsoluteSequence(missFadeTimeChange.NewValue.Value)) + { + // colour and duration matches stable + // transforms not applied to entire hold note in order to not affect hit lighting + holdNote.Head.FadeColour(Colour4.DarkGray, 60); + holdNote.Tail.FadeColour(Colour4.DarkGray, 60); + bodySprite?.FadeColour(Colour4.DarkGray, 60); + } + } + + protected override void Update() + { + base.Update(); + missFadeTime.Value ??= holdNote.HoldBrokenTime; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From ba30800bf4d242b30f0909bf97a146cced4e6ccc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 13 Nov 2020 22:21:22 +0100 Subject: [PATCH 4639/6909] Extract constant --- osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 9ed25115d8..f1f72c8618 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -193,9 +193,11 @@ namespace osu.Game.Rulesets.Mania.Skinning { // colour and duration matches stable // transforms not applied to entire hold note in order to not affect hit lighting - holdNote.Head.FadeColour(Colour4.DarkGray, 60); - holdNote.Tail.FadeColour(Colour4.DarkGray, 60); - bodySprite?.FadeColour(Colour4.DarkGray, 60); + const double fade_duration = 60; + + holdNote.Head.FadeColour(Colour4.DarkGray, fade_duration); + holdNote.Tail.FadeColour(Colour4.DarkGray, fade_duration); + bodySprite?.FadeColour(Colour4.DarkGray, fade_duration); } } From 706d7890b4bf93816e8d8b2ebe4d75d15898a7c9 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 5 Nov 2020 06:40:20 -0800 Subject: [PATCH 4640/6909] Make tournaments strings more consistent --- osu.Game.Tournament/Screens/SetupScreen.cs | 8 ++++---- osu.Game.Tournament/TournamentSceneManager.cs | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 98bc292901..e78d3a9e83 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -83,8 +83,8 @@ namespace osu.Game.Tournament.Screens }, new ActionableInfo { - Label = "Current User", - ButtonText = "Change Login", + Label = "Current user", + ButtonText = "Change sign-in", Action = () => { api.Logout(); @@ -102,12 +102,12 @@ namespace osu.Game.Tournament.Screens }, Value = api?.LocalUser.Value.Username, Failing = api?.IsLoggedIn != true, - Description = "In order to access the API and display metadata, a login is required." + Description = "In order to access the API and display metadata, signing in is required." }, new LabelledDropdown { Label = "Ruleset", - Description = "Decides what stats are displayed and which ranks are retrieved for players", + Description = "Decides what stats are displayed and which ranks are retrieved for players.", Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 2c539cdd43..870ea466cc 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -127,10 +127,10 @@ namespace osu.Game.Tournament new ScreenButton(typeof(ScheduleScreen)) { Text = "Schedule", RequestSelection = SetScreen }, new ScreenButton(typeof(LadderScreen)) { Text = "Bracket", RequestSelection = SetScreen }, new Separator(), - new ScreenButton(typeof(TeamIntroScreen)) { Text = "TeamIntro", RequestSelection = SetScreen }, + new ScreenButton(typeof(TeamIntroScreen)) { Text = "Team Intro", RequestSelection = SetScreen }, new ScreenButton(typeof(SeedingScreen)) { Text = "Seeding", RequestSelection = SetScreen }, new Separator(), - new ScreenButton(typeof(MapPoolScreen)) { Text = "MapPool", RequestSelection = SetScreen }, + new ScreenButton(typeof(MapPoolScreen)) { Text = "Map Pool", RequestSelection = SetScreen }, new ScreenButton(typeof(GameplayScreen)) { Text = "Gameplay", RequestSelection = SetScreen }, new Separator(), new ScreenButton(typeof(TeamWinScreen)) { Text = "Win", RequestSelection = SetScreen }, From 9d8e7e895442c29df40a3ebf2fb0df5cf020bbb1 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 04:46:26 +0300 Subject: [PATCH 4641/6909] ProfileLineChart layout implementation --- .../Online/TestSceneProfileLineChart.cs | 39 +++++++++++ .../Sections/Historical/ProfileLineChart.cs | 64 +++++++++++++++++++ 2 files changed, 103 insertions(+) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs create mode 100644 osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs new file mode 100644 index 0000000000..34359baab5 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.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.Overlays.Profile.Sections.Historical; +using osu.Framework.Graphics; +using static osu.Game.Users.User; +using System; +using osu.Game.Overlays; +using osu.Framework.Allocation; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneProfileLineChart : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); + + public TestSceneProfileLineChart() + { + var values = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1000 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 20 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 20000 }, + new UserHistoryCount { Date = new DateTime(2010, 8, 1), Count = 30 }, + new UserHistoryCount { Date = new DateTime(2010, 9, 1), Count = 50 }, + new UserHistoryCount { Date = new DateTime(2010, 10, 1), Count = 2000 }, + new UserHistoryCount { Date = new DateTime(2010, 11, 1), Count = 2100 } + }; + + Add(new ProfileLineChart + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Values = values + }); + } + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs new file mode 100644 index 0000000000..10a03aa012 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -0,0 +1,64 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; +using JetBrains.Annotations; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class ProfileLineChart : CompositeDrawable + { + private UserHistoryCount[] values; + + [CanBeNull] + public UserHistoryCount[] Values + { + get => values; + set + { + values = value; + graph.Values = values; + } + } + + private readonly UserHistoryGraph graph; + + public ProfileLineChart() + { + RelativeSizeAxes = Axes.X; + Height = 250; + InternalChild = new GridContainer + { + RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension() + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + Empty(), + graph = new UserHistoryGraph + { + RelativeSizeAxes = Axes.Both + } + }, + new Drawable[] + { + Empty(), + Empty() + } + } + }; + } + } +} From d98c59f2a4b399da80b5c511f062c60c6d5d536e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 06:38:02 +0300 Subject: [PATCH 4642/6909] Implement horizontal ticks creation --- .../Sections/Historical/ProfileLineChart.cs | 126 +++++++++++++++++- 1 file changed, 123 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 10a03aa012..a02f869f51 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -5,6 +5,13 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using JetBrains.Annotations; using static osu.Game.Users.User; +using System; +using System.Linq; +using osu.Game.Graphics.Sprites; +using osu.Framework.Utils; +using osu.Framework.Allocation; +using osu.Game.Graphics; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Overlays.Profile.Sections.Historical { @@ -20,10 +27,14 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { values = value; graph.Values = values; + + createRowTicks(); } } private readonly UserHistoryGraph graph; + private readonly Container rowTicksContainer; + private readonly Container rowLinesContainer; public ProfileLineChart() { @@ -46,10 +57,25 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { new Drawable[] { - Empty(), - graph = new UserHistoryGraph + rowTicksContainer = new Container { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Y, + AutoSizeAxes = Axes.X + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + rowLinesContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + graph = new UserHistoryGraph + { + RelativeSizeAxes = Axes.Both + } + } } }, new Drawable[] @@ -60,5 +86,99 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } }; } + + private void createRowTicks() + { + rowTicksContainer.Clear(); + rowLinesContainer.Clear(); + + var min = values.Select(v => v.Count).Min(); + var max = values.Select(v => v.Count).Max(); + + var niceRange = niceNumber(max - min, false); + var niceTick = niceNumber(niceRange / (6 - 1), true); + var axisStart = Math.Floor(min / niceTick) * niceTick; + var axisEnd = Math.Ceiling(max / niceTick) * niceTick; + + var rollingRow = axisStart; + + while (rollingRow <= axisEnd) + { + var y = -Interpolation.ValueAt(rollingRow, 0, 1f, min, max); + + rowTicksContainer.Add(new TickText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.CentreRight, + RelativePositionAxes = Axes.Y, + Text = rollingRow.ToString("N0"), + Y = y + }); + + rowLinesContainer.Add(new TickLine + { + Anchor = Anchor.BottomRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + RelativePositionAxes = Axes.Y, + Height = 1, + Y = y + }); + + rollingRow += niceTick; + } + } + + private double niceNumber(double value, bool round) + { + var exponent = (int)Math.Floor(Math.Log10(value)); + var fraction = value / Math.Pow(10, exponent); + + double niceFraction; + + if (round) + { + if (fraction < 1.5) + niceFraction = 1.0; + else if (fraction < 3) + niceFraction = 2.0; + else if (fraction < 7) + niceFraction = 5.0; + else + niceFraction = 10.0; + } + else + { + if (fraction <= 1.0) + niceFraction = 1.0; + else if (fraction <= 2.0) + niceFraction = 2.0; + else if (fraction <= 5.0) + niceFraction = 5.0; + else + niceFraction = 10.0; + } + + return niceFraction * Math.Pow(10, exponent); + } + + private class TickText : OsuSpriteText + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Foreground1; + Font = OsuFont.GetFont(size: 12); + } + } + + private class TickLine : Box + { + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Colour = colourProvider.Background6; + } + } } } From 00e974794076c241779694f6ec988a8ccf204044 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 06:44:29 +0300 Subject: [PATCH 4643/6909] Test scene visual improvements --- .../Online/TestSceneProfileLineChart.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs index 34359baab5..0be835c07d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs @@ -7,6 +7,8 @@ using static osu.Game.Users.User; using System; using osu.Game.Overlays; using osu.Framework.Allocation; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Containers; namespace osu.Game.Tests.Visual.Online { @@ -28,11 +30,25 @@ namespace osu.Game.Tests.Visual.Online new UserHistoryCount { Date = new DateTime(2010, 11, 1), Count = 2100 } }; - Add(new ProfileLineChart + AddRange(new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Values = values + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4 + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Padding = new MarginPadding { Horizontal = 50 }, + Child = new ProfileLineChart + { + Values = values + } + } }); } } From 01f28a35c3269316e5c380d14c8c41c3a65eff6b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 07:28:01 +0300 Subject: [PATCH 4644/6909] Implement vertical ticks creation --- .../Sections/Historical/ProfileLineChart.cs | 70 +++++++++++++++++-- 1 file changed, 65 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index a02f869f51..2908b50a6e 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -29,12 +29,15 @@ namespace osu.Game.Overlays.Profile.Sections.Historical graph.Values = values; createRowTicks(); + createColumnTicks(); } } private readonly UserHistoryGraph graph; private readonly Container rowTicksContainer; + private readonly Container columnTicksContainer; private readonly Container rowLinesContainer; + private readonly Container columnLinesContainer; public ProfileLineChart() { @@ -67,9 +70,20 @@ namespace osu.Game.Overlays.Profile.Sections.Historical RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - rowLinesContainer = new Container + new Container { - RelativeSizeAxes = Axes.Both + RelativeSizeAxes = Axes.Both, + Children = new[] + { + rowLinesContainer = new Container + { + RelativeSizeAxes = Axes.Both + }, + columnLinesContainer = new Container + { + RelativeSizeAxes = Axes.Both + } + } }, graph = new UserHistoryGraph { @@ -81,7 +95,12 @@ namespace osu.Game.Overlays.Profile.Sections.Historical new Drawable[] { Empty(), - Empty() + columnTicksContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Top = 10 } + } } } }; @@ -104,7 +123,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical while (rollingRow <= axisEnd) { - var y = -Interpolation.ValueAt(rollingRow, 0, 1f, min, max); + var y = -Interpolation.ValueAt(rollingRow, 0, 1f, axisStart, axisEnd); rowTicksContainer.Add(new TickText { @@ -129,9 +148,50 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } } + private void createColumnTicks() + { + columnTicksContainer.Clear(); + columnLinesContainer.Clear(); + + var min = values.Select(v => v.Date).Min().Ticks; + var max = values.Select(v => v.Date).Max().Ticks; + + var niceRange = niceNumber(max - min, false); + var niceTick = niceNumber(niceRange / (Math.Min(values.Length, 15) - 1), true); + var axisStart = Math.Floor(min / niceTick) * niceTick; + var axisEnd = Math.Ceiling(max / niceTick) * niceTick; + + var rollingRow = axisStart; + + while (rollingRow <= axisEnd) + { + var x = Interpolation.ValueAt(rollingRow, 0, 1f, axisStart, axisEnd); + + columnTicksContainer.Add(new TickText + { + Origin = Anchor.CentreLeft, + RelativePositionAxes = Axes.X, + Text = new DateTime((long)rollingRow).ToString("MMM yyyy"), + Rotation = 45, + X = x + }); + + columnLinesContainer.Add(new TickLine + { + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + Width = 1, + X = x + }); + + rollingRow += niceTick; + } + } + private double niceNumber(double value, bool round) { - var exponent = (int)Math.Floor(Math.Log10(value)); + var exponent = Math.Floor(Math.Log10(value)); var fraction = value / Math.Pow(10, exponent); double niceFraction; From 90f37ff4ab651281059a5f8bdf2bc41cf6c69a1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Nov 2020 18:04:59 +0900 Subject: [PATCH 4645/6909] Rejig namespaces --- osu.Game.Rulesets.Osu/{UI => Edit}/OsuEditDrawablePool.cs | 2 +- .../{UI => Objects/Drawables}/OsuDrawablePool.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) rename osu.Game.Rulesets.Osu/{UI => Edit}/OsuEditDrawablePool.cs (98%) rename osu.Game.Rulesets.Osu/{UI => Objects/Drawables}/OsuDrawablePool.cs (93%) diff --git a/osu.Game.Rulesets.Osu/UI/OsuEditDrawablePool.cs b/osu.Game.Rulesets.Osu/Edit/OsuEditDrawablePool.cs similarity index 98% rename from osu.Game.Rulesets.Osu/UI/OsuEditDrawablePool.cs rename to osu.Game.Rulesets.Osu/Edit/OsuEditDrawablePool.cs index 822ff076f5..946181d8a4 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuEditDrawablePool.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuEditDrawablePool.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; -namespace osu.Game.Rulesets.Osu.UI +namespace osu.Game.Rulesets.Osu.Edit { public class OsuEditDrawablePool : OsuDrawablePool where T : DrawableHitObject, new() diff --git a/osu.Game.Rulesets.Osu/UI/OsuDrawablePool.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/OsuDrawablePool.cs similarity index 93% rename from osu.Game.Rulesets.Osu/UI/OsuDrawablePool.cs rename to osu.Game.Rulesets.Osu/Objects/Drawables/OsuDrawablePool.cs index 148146f25a..1d9330b962 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuDrawablePool.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/OsuDrawablePool.cs @@ -5,9 +5,8 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables; -namespace osu.Game.Rulesets.Osu.UI +namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class OsuDrawablePool : DrawablePool where T : DrawableHitObject, new() From 7ac4d2c4be80ffe933bd76ff6c1333e6b3d498fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Nov 2020 18:05:30 +0900 Subject: [PATCH 4646/6909] Move "drawable" to first prefix --- .../Edit/{OsuEditDrawablePool.cs => DrawableOsuEditPool.cs} | 4 ++-- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs | 2 +- .../Drawables/{OsuDrawablePool.cs => DrawableOsuPool.cs} | 4 ++-- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) rename osu.Game.Rulesets.Osu/Edit/{OsuEditDrawablePool.cs => DrawableOsuEditPool.cs} (95%) rename osu.Game.Rulesets.Osu/Objects/Drawables/{OsuDrawablePool.cs => DrawableOsuPool.cs} (89%) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuEditDrawablePool.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs similarity index 95% rename from osu.Game.Rulesets.Osu/Edit/OsuEditDrawablePool.cs rename to osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs index 946181d8a4..569031752e 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuEditDrawablePool.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Edit { - public class OsuEditDrawablePool : OsuDrawablePool + public class DrawableOsuEditPool : DrawableOsuPool where T : DrawableHitObject, new() { /// @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Edit /// private const double editor_hit_object_fade_out_extension = 700; - public OsuEditDrawablePool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) + public DrawableOsuEditPool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) : base(checkHittable, onLoaded, initialSize, maximumSize) { } diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 8af6fd65ce..547dff88b5 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Edit protected override GameplayCursorContainer CreateCursor() => null; protected override DrawablePool CreatePool(int initialSize, int? maximumSize = null) - => new OsuEditDrawablePool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); + => new DrawableOsuEditPool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/OsuDrawablePool.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs similarity index 89% rename from osu.Game.Rulesets.Osu/Objects/Drawables/OsuDrawablePool.cs rename to osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs index 1d9330b962..1b5fd50022 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/OsuDrawablePool.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs @@ -8,13 +8,13 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class OsuDrawablePool : DrawablePool + public class DrawableOsuPool : DrawablePool where T : DrawableHitObject, new() { private readonly Func checkHittable; private readonly Action onLoaded; - public OsuDrawablePool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) + public DrawableOsuPool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) : base(initialSize, maximumSize) { this.checkHittable = checkHittable; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index e8ff6c410f..c816502d61 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Osu.UI protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) where TDrawable : DrawableHitObject, new() - => new OsuDrawablePool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); + => new DrawableOsuPool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); From deea75b2e9f53f92eff40109adef3e162d76b68c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 14 Nov 2020 18:05:51 +0900 Subject: [PATCH 4647/6909] Fix typo in comment --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs index 569031752e..776aacd143 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Edit switch (hitObject) { default: - // there are quite a few drawable hit types we don't want to extent (spinners, ticks etc.) + // there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.) return; case DrawableSlider _: From 4d2bc790fd96b68234b39e623d6147e09a444ffb Mon Sep 17 00:00:00 2001 From: kamp Date: Sat, 14 Nov 2020 13:20:16 +0100 Subject: [PATCH 4648/6909] Fix crash on shift+right-click deleting objects --- osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 3229719d5a..c9043ccef3 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -338,7 +338,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. private bool beginClickSelection(MouseButtonEvent e) { - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.ToList()) { if (!blueprint.IsHovered) continue; From 610ed99ae3eba0f9f28cc7ca13b9cd56d94f2cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 14 Nov 2020 14:48:48 +0100 Subject: [PATCH 4649/6909] Add null checks to unguarded resolved-as-null fields --- .../Screens/Ladder/Components/DrawableTournamentMatch.cs | 4 ++-- osu.Game/Online/DownloadTrackingComposite.cs | 7 +++++-- osu.Game/Online/Leaderboards/Leaderboard.cs | 6 ++++-- osu.Game/Overlays/AccountCreation/ScreenWarning.cs | 4 ++-- .../Overlays/Settings/Sections/General/LoginSettings.cs | 2 +- osu.Game/PerformFromMenuRunner.cs | 2 +- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 3 +++ .../Components/Timeline/TimelineBlueprintContainer.cs | 2 +- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 2 +- osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs | 3 +++ 11 files changed, 24 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index 655beb4bdd..f2065e7e88 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -144,9 +144,9 @@ namespace osu.Game.Tournament.Screens.Ladder.Components if (selected) { selectionBox.Show(); - if (editor) + if (editor && editorInfo != null) editorInfo.Selected.Value = Match; - else + else if (ladderInfo != null) ladderInfo.CurrentMatch.Value = Match; } else diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 5d9cf612bb..bed95344c6 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -46,12 +46,15 @@ namespace osu.Game.Online { if (modelInfo.NewValue == null) attachDownload(null); - else if (manager.IsAvailableLocally(modelInfo.NewValue)) + else if (manager?.IsAvailableLocally(modelInfo.NewValue) == true) State.Value = DownloadState.LocallyAvailable; else - attachDownload(manager.GetExistingDownload(modelInfo.NewValue)); + attachDownload(manager?.GetExistingDownload(modelInfo.NewValue)); }, true); + if (manager == null) + return; + managerDownloadBegan = manager.DownloadBegan.GetBoundCopy(); managerDownloadBegan.BindValueChanged(downloadBegan); managerDownloadFailed = manager.DownloadFailed.GetBoundCopy(); diff --git a/osu.Game/Online/Leaderboards/Leaderboard.cs b/osu.Game/Online/Leaderboards/Leaderboard.cs index 3a5c2e181f..d18f189a70 100644 --- a/osu.Game/Online/Leaderboards/Leaderboard.cs +++ b/osu.Game/Online/Leaderboards/Leaderboard.cs @@ -248,7 +248,9 @@ namespace osu.Game.Online.Leaderboards [BackgroundDependencyLoader] private void load() { - apiState.BindTo(api.State); + if (api != null) + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); } @@ -303,7 +305,7 @@ namespace osu.Game.Online.Leaderboards PlaceholderState = PlaceholderState.NetworkFailure; }); - api.Queue(getScoresRequest); + api?.Queue(getScoresRequest); }); } diff --git a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs index 5375476c9e..b2096968fe 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenWarning.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenWarning.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.AccountCreation public override void OnEntering(IScreen last) { - if (string.IsNullOrEmpty(api.ProvidedUsername)) + if (string.IsNullOrEmpty(api?.ProvidedUsername)) { this.FadeOut(); this.Push(new ScreenEntry()); @@ -43,7 +43,7 @@ namespace osu.Game.Overlays.AccountCreation [BackgroundDependencyLoader(true)] private void load(OsuColour colours, OsuGame game, TextureStore textures) { - if (string.IsNullOrEmpty(api.ProvidedUsername)) + if (string.IsNullOrEmpty(api?.ProvidedUsername)) return; InternalChildren = new Drawable[] diff --git a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs index 873272bf12..8f757f7a36 100644 --- a/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/General/LoginSettings.cs @@ -217,7 +217,7 @@ namespace osu.Game.Overlays.Settings.Sections.General private void performLogin() { if (!string.IsNullOrEmpty(username.Text) && !string.IsNullOrEmpty(password.Text)) - api.Login(username.Text, password.Text); + api?.Login(username.Text, password.Text); else shakeSignIn.Shake(); } diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index 5898c116dd..e2d4fc6051 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -76,7 +76,7 @@ namespace osu.Game // a dialog may be blocking the execution for now. if (checkForDialog(current)) return; - game.CloseAllOverlays(false); + game?.CloseAllOverlays(false); // we may already be at the target screen type. if (validScreens.Contains(getCurrentScreen().GetType()) && !beatmap.Disabled) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 1dc029506f..5fbda305c8 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI { Debug.Assert(!drawableMap.ContainsKey(entry)); - var drawable = pooledObjectProvider.GetPooledDrawableRepresentation(entry.HitObject); + var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject); if (drawable == null) throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 3229719d5a..7fa7ec9fa5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -457,6 +457,9 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprint == null) return false; + if (snapProvider == null) + return true; + Debug.Assert(movementBlueprintOriginalPosition != null); HitObject draggedObject = movementBlueprint.HitObject; diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 0271b2def9..eef02e61a6 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -96,7 +96,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline if (lastDragEvent != null) OnDrag(lastDragEvent); - if (Composer != null) + if (Composer != null && timeline != null) { Composer.Playfield.PastLifetimeExtension = timeline.VisibleRange / 2; Composer.Playfield.FutureLifetimeExtension = timeline.VisibleRange / 2; diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index 09d861522a..eab909b798 100644 --- a/osu.Game/Screens/Edit/Timing/TimingScreen.cs +++ b/osu.Game/Screens/Edit/Timing/TimingScreen.cs @@ -128,7 +128,7 @@ namespace osu.Game.Screens.Edit.Timing controlPointGroups.BindCollectionChanged((sender, args) => { table.ControlGroups = controlPointGroups; - changeHandler.SaveState(); + changeHandler?.SaveState(); }, true); } diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index be1083ce8d..3fc1359006 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -59,6 +59,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components { scheduledFilterUpdate?.Cancel(); + if (filter == null) + return; + filter.Value = new FilterCriteria { SearchString = Search.Current.Value ?? string.Empty, From 8a78d408db6ed6d4a218615459c5a66b4802d6dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 14 Nov 2020 15:52:12 +0100 Subject: [PATCH 4650/6909] Add more missed null checks --- osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 21810379cc..adf22a3370 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected virtual void OnOperationBegan() { - ChangeHandler.BeginChange(); + ChangeHandler?.BeginChange(); } /// @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// protected virtual void OnOperationEnded() { - ChangeHandler.EndChange(); + ChangeHandler?.EndChange(); } #region User Input Handling From ae4a2e74faab435156cfebe6f6da131b5b83896b Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 18:21:10 +0300 Subject: [PATCH 4651/6909] Implement ProfileSubsection --- ...cs => TestSceneProfileSubsectionHeader.cs} | 6 +- .../Profile/Sections/PaginatedContainer.cs | 57 +++----------- .../Profile/Sections/ProfileSubsection.cs | 78 +++++++++++++++++++ ...erHeader.cs => ProfileSubsectionHeader.cs} | 4 +- 4 files changed, 95 insertions(+), 50 deletions(-) rename osu.Game.Tests/Visual/UserInterface/{TestScenePaginatedContainerHeader.cs => TestSceneProfileSubsectionHeader.cs} (95%) create mode 100644 osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs rename osu.Game/Overlays/Profile/Sections/{PaginatedContainerHeader.cs => ProfileSubsectionHeader.cs} (95%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs similarity index 95% rename from osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs index 2e9f919cfd..cd226662d7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs @@ -11,12 +11,12 @@ using osu.Framework.Allocation; namespace osu.Game.Tests.Visual.UserInterface { - public class TestScenePaginatedContainerHeader : OsuTestScene + public class TestSceneProfileSubsectionHeader : OsuTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - private PaginatedContainerHeader header; + private ProfileSubsectionHeader header; [Test] public void TestHiddenCounter() @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void createHeader(string text, CounterVisibilityState state, int initialValue = 0) { Clear(); - Add(header = new PaginatedContainerHeader(text, state) + Add(header = new ProfileSubsectionHeader(text, state) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index c1107ce907..7b66c3f51e 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -6,10 +6,7 @@ using osu.Framework.Allocation; 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.Online.API; -using osu.Game.Rulesets; using osu.Game.Users; using System.Collections.Generic; using System.Linq; @@ -18,7 +15,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Profile.Sections { - public abstract class PaginatedContainer : FillFlowContainer + public abstract class PaginatedContainer : ProfileSubsection { [Resolved] private IAPIProvider api { get; set; } @@ -26,42 +23,25 @@ namespace osu.Game.Overlays.Profile.Sections protected int VisiblePages; protected int ItemsPerPage; - protected readonly Bindable User = new Bindable(); protected FillFlowContainer ItemsContainer; - protected RulesetStore Rulesets; private APIRequest> retrievalRequest; private CancellationTokenSource loadCancellation; - private readonly string missingText; private ShowMoreButton moreButton; - private OsuSpriteText missing; - private PaginatedContainerHeader header; - - private readonly string headerText; - private readonly CounterVisibilityState counterVisibilityState; protected PaginatedContainer(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + : base(user, headerText, missingText, counterVisibilityState) { - this.headerText = headerText; - this.missingText = missingText; - this.counterVisibilityState = counterVisibilityState; - User.BindTo(user); } - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) + protected override Drawable CreateContent() => new FillFlowContainer { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Direction = FillDirection.Vertical; - + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new PaginatedContainerHeader(headerText, counterVisibilityState) - { - Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1 - }, ItemsContainer = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -76,21 +56,10 @@ namespace osu.Game.Overlays.Profile.Sections Margin = new MarginPadding { Top = 10 }, Action = showMore, }, - missing = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 15), - Text = missingText, - Alpha = 0, - }, - }; + } + }; - Rulesets = rulesets; - - User.ValueChanged += onUserChanged; - User.TriggerChange(); - } - - private void onUserChanged(ValueChangedEvent e) + protected override void OnUserChanged(ValueChangedEvent e) { loadCancellation?.Cancel(); retrievalRequest?.Cancel(); @@ -124,15 +93,15 @@ namespace osu.Game.Overlays.Profile.Sections moreButton.Hide(); moreButton.IsLoading = false; - if (!string.IsNullOrEmpty(missing.Text)) - missing.Show(); + if (!string.IsNullOrEmpty(Missing.Text)) + Missing.Show(); return; } LoadComponentsAsync(items.Select(CreateDrawableItem).Where(d => d != null), drawables => { - missing.Hide(); + Missing.Hide(); moreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0); moreButton.IsLoading = false; @@ -142,8 +111,6 @@ namespace osu.Game.Overlays.Profile.Sections protected virtual int GetCount(User user) => 0; - protected void SetCount(int value) => header.Current.Value = value; - protected virtual void OnItemsReceived(List items) { } diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs new file mode 100644 index 0000000000..9583759693 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.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 osu.Framework.Allocation; +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.Rulesets; +using osu.Game.Users; +using JetBrains.Annotations; + +namespace osu.Game.Overlays.Profile.Sections +{ + public abstract class ProfileSubsection : FillFlowContainer + { + protected readonly Bindable User = new Bindable(); + + protected RulesetStore Rulesets { get; private set; } + + protected OsuSpriteText Missing { get; private set; } + + private readonly string headerText; + private readonly string missingText; + private readonly CounterVisibilityState counterVisibilityState; + + private ProfileSubsectionHeader header; + + protected ProfileSubsection(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + { + this.headerText = headerText; + this.missingText = missingText; + this.counterVisibilityState = counterVisibilityState; + User.BindTo(user); + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + + Children = new Drawable[] + { + header = new ProfileSubsectionHeader(headerText, counterVisibilityState) + { + Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1 + }, + CreateContent(), + Missing = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 15), + Text = missingText, + Alpha = 0, + }, + }; + + Rulesets = rulesets; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(OnUserChanged, true); + } + + [NotNull] + protected abstract Drawable CreateContent(); + + protected virtual void OnUserChanged(ValueChangedEvent e) + { + } + + protected void SetCount(int value) => header.Current.Value = value; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs similarity index 95% rename from osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs rename to osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs index 8c617e5fbd..5858cebe89 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Profile.Sections { - public class PaginatedContainerHeader : CompositeDrawable, IHasCurrentValue + public class ProfileSubsectionHeader : CompositeDrawable, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Sections private CounterPill counterPill; - public PaginatedContainerHeader(string text, CounterVisibilityState counterState) + public ProfileSubsectionHeader(string text, CounterVisibilityState counterState) { this.text = text; this.counterState = counterState; From af174aa653206d3ae3dbb42368b2c4ff5285a488 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 18:48:47 +0300 Subject: [PATCH 4652/6909] Implement chart subsections --- .../Historical/ChartProfileSubsection.cs | 51 +++++++++++++++++++ .../Historical/PlayHistorySubsection.cs | 19 +++++++ .../Sections/Historical/ProfileLineChart.cs | 4 +- .../Sections/Historical/ReplaysSubsection.cs | 19 +++++++ .../Profile/Sections/HistoricalSection.cs | 2 + 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs create mode 100644 osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs create mode 100644 osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs new file mode 100644 index 0000000000..24083c9a79 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.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.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Users; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public abstract class ChartProfileSubsection : ProfileSubsection + { + private ProfileLineChart chart; + + protected ChartProfileSubsection(Bindable user, string headerText) + : base(user, headerText) + { + + } + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Top = 10, + Left = 20, + Right = 40 + }, + Child = chart = new ProfileLineChart() + }; + + protected override void OnUserChanged(ValueChangedEvent e) + { + var values = GetValues(e.NewValue); + + if (values?.Length > 1) + { + chart.Values = values; + Show(); + return; + } + + Hide(); + } + + protected abstract UserHistoryCount[] GetValues(User user); + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs new file mode 100644 index 0000000000..3e35f80b49 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.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.Bindables; +using osu.Game.Users; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class PlayHistorySubsection : ChartProfileSubsection + { + public PlayHistorySubsection(Bindable user) + : base(user, "Play History") + { + } + + protected override UserHistoryCount[] GetValues(User user) => user.MonthlyPlaycounts; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 2908b50a6e..55fa6c5400 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -130,7 +130,9 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Anchor = Anchor.BottomRight, Origin = Anchor.CentreRight, RelativePositionAxes = Axes.Y, + Margin = new MarginPadding { Right = 3 }, Text = rollingRow.ToString("N0"), + Font = OsuFont.GetFont(size: 12), Y = y }); @@ -172,6 +174,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Origin = Anchor.CentreLeft, RelativePositionAxes = Axes.X, Text = new DateTime((long)rollingRow).ToString("MMM yyyy"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Rotation = 45, X = x }); @@ -228,7 +231,6 @@ namespace osu.Game.Overlays.Profile.Sections.Historical private void load(OverlayColourProvider colourProvider) { Colour = colourProvider.Foreground1; - Font = OsuFont.GetFont(size: 12); } } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs new file mode 100644 index 0000000000..f6abd1c4fc --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.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.Bindables; +using osu.Game.Users; +using static osu.Game.Users.User; + +namespace osu.Game.Overlays.Profile.Sections.Historical +{ + public class ReplaysSubsection : ChartProfileSubsection + { + public ReplaysSubsection(Bindable user) + : base(user, "Replays Watched History") + { + } + + protected override UserHistoryCount[] GetValues(User user) => user.ReplaysWatchedCounts; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs index bfc47bd88c..6e2b9873cf 100644 --- a/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs +++ b/osu.Game/Overlays/Profile/Sections/HistoricalSection.cs @@ -18,8 +18,10 @@ namespace osu.Game.Overlays.Profile.Sections { Children = new Drawable[] { + new PlayHistorySubsection(User), new PaginatedMostPlayedBeatmapContainer(User), new PaginatedScoreContainer(ScoreType.Recent, User, "Recent Plays (24h)", CounterVisibilityState.VisibleWhenZero), + new ReplaysSubsection(User) }; } } From b344a1373472c3eb1b21b4b58f9a017b33b78f05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 14 Nov 2020 17:08:27 +0100 Subject: [PATCH 4653/6909] Add support for previewing tracks on spectator screen --- osu.Game/Screens/Play/Spectator.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 6f51771c12..71ce157296 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Screens; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Graphics; @@ -35,7 +36,8 @@ using osuTK; namespace osu.Game.Screens.Play { - public class Spectator : OsuScreen + [Cached(typeof(IPreviewTrackOwner))] + public class Spectator : OsuScreen, IPreviewTrackOwner { private readonly User targetUser; @@ -62,6 +64,9 @@ namespace osu.Game.Screens.Play [Resolved] private RulesetStore rulesets { get; set; } + [Resolved] + private PreviewTrackManager previewTrackManager { get; set; } + private Score score; private readonly object scoreLock = new object(); @@ -275,6 +280,7 @@ namespace osu.Game.Screens.Play { watchButton.Enabled.Value = false; beatmapPanelContainer.Clear(); + previewTrackManager.StopAnyPlaying(this); } private void attemptStart() @@ -326,7 +332,6 @@ namespace osu.Game.Screens.Play { if (state?.BeatmapID == null) { - beatmapPanelContainer.Clear(); onlineBeatmap = null; return; } @@ -359,6 +364,12 @@ namespace osu.Game.Screens.Play beatmaps.Download(onlineBeatmap); } + public override bool OnExiting(IScreen next) + { + previewTrackManager.StopAnyPlaying(this); + return base.OnExiting(next); + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From 02168c6c2fb7f31907c5d02797fbc1508075dd87 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 19:17:01 +0300 Subject: [PATCH 4654/6909] Implement dates with zero count fill --- .../Historical/ChartProfileSubsection.cs | 28 ++++++++++++++++++- .../Sections/Historical/ProfileLineChart.cs | 2 +- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs index 24083c9a79..38224dd177 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -38,7 +39,32 @@ namespace osu.Game.Overlays.Profile.Sections.Historical if (values?.Length > 1) { - chart.Values = values; + // Fill dates with 0 count + + var newValues = new List { values[0] }; + var newLast = values[0]; + + for (int i = 1; i < values.Length; i++) + { + while (hasMissingDates(newLast, values[i])) + { + newValues.Add(newLast = new UserHistoryCount + { + Count = 0, + Date = newLast.Date.AddMonths(1) + }); + } + + newValues.Add(newLast = values[i]); + } + + static bool hasMissingDates(UserHistoryCount prev, UserHistoryCount current) + { + var possibleCurrent = prev.Date.AddMonths(1); + return possibleCurrent != current.Date; + } + + chart.Values = newValues.ToArray(); Show(); return; } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 55fa6c5400..7cd529a726 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { private UserHistoryCount[] values; - [CanBeNull] + [NotNull] public UserHistoryCount[] Values { get => values; From 5354bf1fa593bcbfb1d6cafac3e4143af035e45e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 20:07:52 +0300 Subject: [PATCH 4655/6909] Ticks distribution improvements --- .../Sections/Historical/ProfileLineChart.cs | 22 ++++++++----------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 7cd529a726..fe4677037b 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -116,14 +116,12 @@ namespace osu.Game.Overlays.Profile.Sections.Historical var niceRange = niceNumber(max - min, false); var niceTick = niceNumber(niceRange / (6 - 1), true); - var axisStart = Math.Floor(min / niceTick) * niceTick; - var axisEnd = Math.Ceiling(max / niceTick) * niceTick; - var rollingRow = axisStart; + double rollingRow = min; - while (rollingRow <= axisEnd) + while (rollingRow <= max) { - var y = -Interpolation.ValueAt(rollingRow, 0, 1f, axisStart, axisEnd); + var y = -Interpolation.ValueAt(rollingRow, 0, 1f, min, max); rowTicksContainer.Add(new TickText { @@ -155,25 +153,23 @@ namespace osu.Game.Overlays.Profile.Sections.Historical columnTicksContainer.Clear(); columnLinesContainer.Clear(); - var min = values.Select(v => v.Date).Min().Ticks; - var max = values.Select(v => v.Date).Max().Ticks; + var min = values.Select(v => v.Date).Min().ToOADate(); + var max = values.Select(v => v.Date).Max().ToOADate(); var niceRange = niceNumber(max - min, false); var niceTick = niceNumber(niceRange / (Math.Min(values.Length, 15) - 1), true); - var axisStart = Math.Floor(min / niceTick) * niceTick; - var axisEnd = Math.Ceiling(max / niceTick) * niceTick; - var rollingRow = axisStart; + double rollingRow = min; - while (rollingRow <= axisEnd) + while (rollingRow <= max) { - var x = Interpolation.ValueAt(rollingRow, 0, 1f, axisStart, axisEnd); + var x = Interpolation.ValueAt(rollingRow, 0, 1f, min, max); columnTicksContainer.Add(new TickText { Origin = Anchor.CentreLeft, RelativePositionAxes = Axes.X, - Text = new DateTime((long)rollingRow).ToString("MMM yyyy"), + Text = DateTime.FromOADate(rollingRow).ToString("MMM yyyy"), Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), Rotation = 45, X = x From a94546f905ca8ca42e9b4386c148bf362bf612a4 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 20:17:32 +0300 Subject: [PATCH 4656/6909] CI fixes --- .../Profile/Sections/Historical/ChartProfileSubsection.cs | 1 - .../Overlays/Profile/Sections/Historical/ProfileLineChart.cs | 2 +- osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs index 38224dd177..4445cdce51 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs @@ -17,7 +17,6 @@ namespace osu.Game.Overlays.Profile.Sections.Historical protected ChartProfileSubsection(Bindable user, string headerText) : base(user, headerText) { - } protected override Drawable CreateContent() => new Container diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index fe4677037b..b7983fd356 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -92,7 +92,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } } }, - new Drawable[] + new[] { Empty(), columnTicksContainer = new Container diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs index 9583759693..751b35e342 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs @@ -42,7 +42,7 @@ namespace osu.Game.Overlays.Profile.Sections AutoSizeAxes = Axes.Y; Direction = FillDirection.Vertical; - Children = new Drawable[] + Children = new[] { header = new ProfileSubsectionHeader(headerText, counterVisibilityState) { From fe9d17fc568cf757991573fb55d12f36875ed908 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 20:31:03 +0300 Subject: [PATCH 4657/6909] Fix CodeFactor issues --- osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs | 2 +- .../Overlays/Profile/Sections/Historical/ProfileLineChart.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs index 0be835c07d..3d342b0d76 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs @@ -3,12 +3,12 @@ using osu.Game.Overlays.Profile.Sections.Historical; using osu.Framework.Graphics; -using static osu.Game.Users.User; using System; using osu.Game.Overlays; using osu.Framework.Allocation; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Containers; +using static osu.Game.Users.User; namespace osu.Game.Tests.Visual.Online { diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index b7983fd356..5a9c42d7e0 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -4,7 +4,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics; using JetBrains.Annotations; -using static osu.Game.Users.User; using System; using System.Linq; using osu.Game.Graphics.Sprites; @@ -12,6 +11,7 @@ using osu.Framework.Utils; using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Framework.Graphics.Shapes; +using static osu.Game.Users.User; namespace osu.Game.Overlays.Profile.Sections.Historical { From a52c98b55cc0be3e190610a9214cca09eb8288a0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 14 Nov 2020 21:20:37 +0300 Subject: [PATCH 4658/6909] Fix broken test scene --- .../Profile/Sections/Historical/PlayHistorySubsection.cs | 2 +- .../Overlays/Profile/Sections/Historical/ReplaysSubsection.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs index 3e35f80b49..2f15886c3a 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PlayHistorySubsection.cs @@ -14,6 +14,6 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { } - protected override UserHistoryCount[] GetValues(User user) => user.MonthlyPlaycounts; + protected override UserHistoryCount[] GetValues(User user) => user?.MonthlyPlaycounts; } } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs index f6abd1c4fc..e594e8d020 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ReplaysSubsection.cs @@ -14,6 +14,6 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { } - protected override UserHistoryCount[] GetValues(User user) => user.ReplaysWatchedCounts; + protected override UserHistoryCount[] GetValues(User user) => user?.ReplaysWatchedCounts; } } From 5ae3d6cc74f13a85be1117f717ce9eff4836438a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 14 Nov 2020 20:12:46 +0100 Subject: [PATCH 4659/6909] Add failing asserts --- .../TestSceneSpinnerApplication.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs index 0558dad30d..05fc352a0b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Osu.Tests public class TestSceneSpinnerApplication : OsuTestScene { [Test] - public void TestApplyNewCircle() + public void TestApplyNewSpinner() { DrawableSpinner dho = null; @@ -23,18 +23,23 @@ namespace osu.Game.Rulesets.Osu.Tests { Position = new Vector2(256, 192), IndexInCurrentCombo = 0, - Duration = 0, + Duration = 500, })) { Clock = new FramedClock(new StopwatchClock()) }); + AddStep("rotate some", () => dho.RotationTracker.AddRotation(180)); + AddAssert("rotation is set", () => dho.RotationTracker.RateAdjustedRotation == 180); + AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner { Position = new Vector2(256, 192), ComboIndex = 1, Duration = 1000, }), null)); + + AddAssert("rotation is reset", () => dho.RotationTracker.RateAdjustedRotation == 0); } private Spinner prepareObject(Spinner circle) From f8cabbdd21bc64dc3484ec664c8ce43fb8861f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 14 Nov 2020 21:09:22 +0100 Subject: [PATCH 4660/6909] Clear result when freeing pooled hitobject --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index b400c532c5..9735426ea1 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -285,6 +285,7 @@ namespace osu.Game.Rulesets.Objects.Drawables OnFree(HitObject); HitObject = null; + Result = null; lifetimeEntry = null; hasHitObjectApplied = false; From af392e39953ac297b2b56287204958636e91f496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 14 Nov 2020 21:10:12 +0100 Subject: [PATCH 4661/6909] Move rate adjusted spinner rotation into judgement result --- .../TestSceneSpinnerApplication.cs | 4 +- .../TestSceneSpinnerRotation.cs | 12 ++--- .../Judgements/OsuSpinnerJudgementResult.cs | 46 +++++++++++++++++++ .../Objects/Drawables/DrawableSpinner.cs | 12 +++-- .../Drawables/Pieces/DefaultSpinnerDisc.cs | 2 +- .../Pieces/SpinnerRotationTracker.cs | 26 +---------- 6 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs index 05fc352a0b..d7fbc7ac48 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerApplication.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Osu.Tests }); AddStep("rotate some", () => dho.RotationTracker.AddRotation(180)); - AddAssert("rotation is set", () => dho.RotationTracker.RateAdjustedRotation == 180); + AddAssert("rotation is set", () => dho.Result.RateAdjustedRotation == 180); AddStep("apply new spinner", () => dho.Apply(prepareObject(new Spinner { @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Tests Duration = 1000, }), null)); - AddAssert("rotation is reset", () => dho.RotationTracker.RateAdjustedRotation == 0); + AddAssert("rotation is reset", () => dho.Result.RateAdjustedRotation == 0); } private Spinner prepareObject(Spinner circle) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs index 53bf1ea566..ac8d5c81bc 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinnerRotation.cs @@ -62,11 +62,11 @@ namespace osu.Game.Rulesets.Osu.Tests trackerRotationTolerance = Math.Abs(drawableSpinner.RotationTracker.Rotation * 0.1f); }); AddAssert("is disc rotation not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, 100)); - AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100)); + AddAssert("is disc rotation absolute not almost 0", () => !Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); addSeekStep(0); AddAssert("is disc rotation almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.Rotation, 0, trackerRotationTolerance)); - AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, 0, 100)); + AddAssert("is disc rotation absolute almost 0", () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, 0, 100)); } [Test] @@ -87,7 +87,7 @@ namespace osu.Game.Rulesets.Osu.Tests finalSpinnerSymbolRotation = spinnerSymbol.Rotation; spinnerSymbolRotationTolerance = Math.Abs(finalSpinnerSymbolRotation * 0.05f); }); - AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.RotationTracker.RateAdjustedRotation); + AddStep("retrieve cumulative disc rotation", () => finalCumulativeTrackerRotation = drawableSpinner.Result.RateAdjustedRotation); addSeekStep(2500); AddAssert("disc rotation rewound", @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Osu.Tests () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation / 2, spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation rewound", // cumulative rotation is not damped, so we're treating it as the "ground truth" and allowing a comparatively smaller margin of error. - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); + () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation / 2, 100)); addSeekStep(5000); AddAssert("is disc rotation almost same", @@ -107,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("is symbol rotation almost same", () => Precision.AlmostEquals(spinnerSymbol.Rotation, finalSpinnerSymbolRotation, spinnerSymbolRotationTolerance)); AddAssert("is cumulative rotation almost same", - () => Precision.AlmostEquals(drawableSpinner.RotationTracker.RateAdjustedRotation, finalCumulativeTrackerRotation, 100)); + () => Precision.AlmostEquals(drawableSpinner.Result.RateAdjustedRotation, finalCumulativeTrackerRotation, 100)); } [Test] @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Osu.Tests { // multipled by 2 to nullify the score multiplier. (autoplay mod selected) var totalScore = ((ScoreExposedPlayer)Player).ScoreProcessor.TotalScore.Value * 2; - return totalScore == (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; + return totalScore == (int)(drawableSpinner.Result.RateAdjustedRotation / 360) * new SpinnerTick().CreateJudgement().MaxNumericResult; }); addSeekStep(0); diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs new file mode 100644 index 0000000000..5ffe4cd004 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class OsuSpinnerJudgementResult : OsuJudgementResult + { + /// + /// The . + /// + public Spinner Spinner => (Spinner)HitObject; + + /// + /// The total rotation performed on the spinner disc, disregarding the spin direction, + /// adjusted for the track's playback rate. + /// + /// + /// + /// 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). + /// + /// + /// The rotation from each frame is multiplied by the clock's current playback rate. + /// The reason this is done is to ensure that spinners give the same score and require the same number of spins + /// regardless of whether speed-modifying mods are applied. + /// + /// + /// + /// Assuming no speed-modifying mods are active, + /// 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). + /// If Double Time is active instead (with a speed multiplier of 1.5x), + /// in the same scenario the property will return 720 * 1.5 = 1080. + /// + public float RateAdjustedRotation; + + public OsuSpinnerJudgementResult(HitObject hitObject, Judgement judgement) + : base(hitObject, judgement) + { + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 824b8806e5..4d11adad09 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -10,8 +10,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Scoring; @@ -24,6 +26,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new Spinner HitObject => (Spinner)base.HitObject; + public new OsuSpinnerJudgementResult Result => (OsuSpinnerJudgementResult)base.Result; + public SpinnerRotationTracker RotationTracker { get; private set; } public SpinnerSpmCounter SpmCounter { get; private set; } @@ -197,10 +201,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // these become implicitly hit. return 1; - return Math.Clamp(RotationTracker.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1); + return Math.Clamp(Result.RateAdjustedRotation / 360 / HitObject.SpinsRequired, 0, 1); } } + protected override JudgementResult CreateResult(Judgement judgement) => new OsuSpinnerJudgementResult(HitObject, judgement); + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (Time.Current < HitObject.StartTime) return; @@ -244,7 +250,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!SpmCounter.IsPresent && RotationTracker.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); - SpmCounter.SetRotation(RotationTracker.RateAdjustedRotation); + SpmCounter.SetRotation(Result.RateAdjustedRotation); updateBonusScore(); } @@ -256,7 +262,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (ticks.Count == 0) return; - int spins = (int)(RotationTracker.RateAdjustedRotation / 360); + int spins = (int)(Result.RateAdjustedRotation / 360); if (spins < wholeSpins) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 731852c221..376aa68020 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { get { - int rotations = (int)(drawableSpinner.RotationTracker.RateAdjustedRotation / 360); + int rotations = (int)(drawableSpinner.Result.RateAdjustedRotation / 360); if (wholeRotationCount == rotations) return false; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index 910899c307..0716218420 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs @@ -32,30 +32,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public readonly BindableBool Complete = new BindableBool(); - /// - /// The total rotation performed on the spinner disc, disregarding the spin direction, - /// adjusted for the track's playback rate. - /// - /// - /// - /// 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). - /// - /// - /// The rotation from each frame is multiplied by the clock's current playback rate. - /// The reason this is done is to ensure that spinners give the same score and require the same number of spins - /// regardless of whether speed-modifying mods are applied. - /// - /// - /// - /// Assuming no speed-modifying mods are active, - /// 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 ). - /// If Double Time is active instead (with a speed multiplier of 1.5x), - /// in the same scenario the property will return 720 * 1.5 = 1080. - /// - public float RateAdjustedRotation { get; private set; } - /// /// Whether the spinning is spinning at a reasonable speed to be considered visually spinning. /// @@ -131,7 +107,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces currentRotation += angle; // rate has to be applied each frame, because it's not guaranteed to be constant throughout playback // (see: ModTimeRamp) - RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate)); + drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate)); } } } From 727a886fb310547ff918e69831121c55754b67f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 14 Nov 2020 22:42:19 +0100 Subject: [PATCH 4662/6909] Move spinner completion info into judgement --- .../Judgements/OsuSpinnerJudgementResult.cs | 6 ++++++ .../Objects/Drawables/DrawableSpinner.cs | 3 ++- .../Objects/Drawables/Pieces/DefaultSpinnerDisc.cs | 8 ++++++-- .../Objects/Drawables/Pieces/SpinnerRotationTracker.cs | 2 -- osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs | 7 ++++++- 5 files changed, 20 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs index 5ffe4cd004..e58aacd86e 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -38,6 +38,12 @@ namespace osu.Game.Rulesets.Osu.Judgements /// public float RateAdjustedRotation; + /// + /// Time instant at which the spinner has been completed (the user has executed all required spins). + /// Will be null if all required spins haven't been completed. + /// + public double? TimeCompleted; + public OsuSpinnerJudgementResult(HitObject hitObject, Judgement judgement) : base(hitObject, judgement) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 4d11adad09..87c7146a64 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -211,7 +211,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { if (Time.Current < HitObject.StartTime) return; - RotationTracker.Complete.Value = Progress >= 1; + if (Progress >= 1) + Result.TimeCompleted ??= Time.Current; if (userTriggered || Time.Current < HitObject.EndTime) return; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs index 376aa68020..14ce3b014d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs @@ -3,6 +3,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; @@ -28,6 +29,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces private SpinnerTicks ticks; private int wholeRotationCount; + private readonly BindableBool complete = new BindableBool(); private SpinnerFill fill; private Container mainContainer; @@ -89,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.LoadComplete(); - drawableSpinner.RotationTracker.Complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); + complete.BindValueChanged(complete => updateComplete(complete.NewValue, 200)); drawableSpinner.ApplyCustomUpdateState += updateStateTransforms; updateStateTransforms(drawableSpinner, drawableSpinner.State.Value); @@ -99,7 +101,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.Update(); - if (drawableSpinner.RotationTracker.Complete.Value) + complete.Value = Time.Current >= drawableSpinner.Result.TimeCompleted; + + if (complete.Value) { if (checkNewRotationCount) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index 0716218420..31ab76cc0a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs @@ -30,8 +30,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public bool Tracking { get; set; } - public readonly BindableBool Complete = new BindableBool(); - /// /// Whether the spinning is spinning at a reasonable speed to be considered visually spinning. /// diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs index eb9fa85fde..5aa136cf7e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs @@ -60,7 +60,6 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - completed.BindTo(DrawableSpinner.RotationTracker.Complete); completed.BindValueChanged(onCompletedChanged, true); DrawableSpinner.ApplyCustomUpdateState += UpdateStateTransforms; @@ -93,6 +92,12 @@ namespace osu.Game.Rulesets.Osu.Skinning } } + protected override void Update() + { + base.Update(); + completed.Value = Time.Current >= DrawableSpinner.Result.TimeCompleted; + } + protected virtual void UpdateStateTransforms(DrawableHitObject drawableHitObject, ArmedState state) { switch (drawableHitObject) From 532680bb367b1a0cb8ea06e04e6ee945151ccc6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 14 Nov 2020 23:23:11 +0100 Subject: [PATCH 4663/6909] Manually reset rest of rotation tracker state on object application --- .../Pieces/SpinnerRotationTracker.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs index 31ab76cc0a..f82003edb8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Screens.Play; using osuTK; @@ -22,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces public SpinnerRotationTracker(DrawableSpinner drawableSpinner) { this.drawableSpinner = drawableSpinner; + drawableSpinner.HitObjectApplied += resetState; RelativeSizeAxes = Axes.Both; } @@ -107,5 +109,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces // (see: ModTimeRamp) drawableSpinner.Result.RateAdjustedRotation += (float)(Math.Abs(angle) * (gameplayClock?.TrueGameplayRate ?? Clock.Rate)); } + + private void resetState(DrawableHitObject obj) + { + Tracking = false; + IsSpinning.Value = false; + mousePosition = default; + lastAngle = currentRotation = Rotation = 0; + rotationTransferred = false; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableSpinner != null) + drawableSpinner.HitObjectApplied -= resetState; + } } } From 199043f67778ed873903ef2e51aa22b05b7129b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 15 Nov 2020 13:21:09 +0900 Subject: [PATCH 4664/6909] Allow preview tracks to exist without an owner (without hard crashing) --- osu.Game/Audio/PreviewTrackManager.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index 862be41c1a..8d02af6574 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -11,6 +11,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.IO.Stores; +using osu.Framework.Logging; using osu.Game.Beatmaps; namespace osu.Game.Audio @@ -76,7 +77,7 @@ namespace osu.Game.Audio /// The which may be the owner of the . public void StopAnyPlaying(IPreviewTrackOwner source) { - if (CurrentTrack == null || CurrentTrack.Owner != source) + if (CurrentTrack == null || (CurrentTrack.Owner != null && CurrentTrack.Owner != source)) return; CurrentTrack.Stop(); @@ -86,11 +87,12 @@ namespace osu.Game.Audio /// /// Creates the . /// - protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => new TrackManagerPreviewTrack(beatmapSetInfo, trackStore); + protected virtual TrackManagerPreviewTrack CreatePreviewTrack(BeatmapSetInfo beatmapSetInfo, ITrackStore trackStore) => + new TrackManagerPreviewTrack(beatmapSetInfo, trackStore); public class TrackManagerPreviewTrack : PreviewTrack { - [Resolved] + [Resolved(canBeNull: true)] public IPreviewTrackOwner Owner { get; private set; } private readonly BeatmapSetInfo beatmapSetInfo; @@ -102,6 +104,12 @@ namespace osu.Game.Audio this.trackManager = trackManager; } + protected override void LoadComplete() + { + base.LoadComplete(); + Logger.Log($"A {nameof(PreviewTrack)} was created without a containing {nameof(IPreviewTrackOwner)}. An owner should be added for correct behaviour."); + } + protected override Track GetTrack() => trackManager.Get($"https://b.ppy.sh/preview/{beatmapSetInfo?.OnlineBeatmapSetID}.mp3"); } From a4b20d211726e0980d55969e2193121cb9b60b75 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sun, 15 Nov 2020 13:00:07 +0800 Subject: [PATCH 4665/6909] Make EZ mod able to fail in Taiko --- osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 36 ++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index c51b47dc6e..7ac01d2aa7 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -1,12 +1,44 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using Humanizer; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Sprites; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Graphics; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModEasy : ModEasy + public class TaikoModEasy : Mod, IApplicableToDifficulty, IApplicableFailOverride { - public override string Description => @"Beats move slower, less accuracy required, and three lives!"; + public override string Name => "Easy"; + public override string Acronym => "EZ"; + public override string Description => @"Beats move slower, less accuracy required"; + public override IconUsage? Icon => OsuIcon.ModEasy; + public override ModType Type => ModType.DifficultyReduction; + public override double ScoreMultiplier => 0.5; + public override bool Ranked => true; + public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) }; + + public void ReadFromDifficulty(BeatmapDifficulty difficulty) + { + } + + public void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + const float ratio = 0.5f; + difficulty.CircleSize *= ratio; + difficulty.ApproachRate *= ratio; + difficulty.DrainRate *= ratio; + difficulty.OverallDifficulty *= ratio; + } + + public bool PerformFail() => true; + + public bool RestartOnFail => false; + } } From cf7ac6d5e38aa036cbea9b14fc618f607dd29de1 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sun, 15 Nov 2020 13:38:21 +0800 Subject: [PATCH 4666/6909] Remove import --- osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index 7ac01d2aa7..383623ca0a 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -2,11 +2,8 @@ // 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; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Rulesets.Mods; From 3406b0d74f3de4e56bcea4ef8be2a737671e6e85 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Sun, 15 Nov 2020 14:54:04 +0800 Subject: [PATCH 4667/6909] Fix checks --- osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index 383623ca0a..c3b833fa29 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -36,6 +36,5 @@ namespace osu.Game.Rulesets.Taiko.Mods public bool PerformFail() => true; public bool RestartOnFail => false; - } } From 9899687bfe27d75a584a582b3a918e2624e7024a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 14:17:41 +0100 Subject: [PATCH 4668/6909] Rename existing method to allow for new one --- osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs | 2 +- osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs | 2 +- osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs | 2 +- osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs | 2 +- osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs index 04e6dea376..4102dff37c 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Tests [TestCase(LegacyMods.SuddenDeath, new[] { typeof(CatchModSuddenDeath) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime), typeof(CatchModPerfect) })] - public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); + public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); protected override Ruleset CreateRuleset() => new CatchRuleset(); } diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index b22687a0a7..221eed3576 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Mania.Tests [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })] [TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })] [TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })] - public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); + public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); protected override Ruleset CreateRuleset() => new ManiaRuleset(); } diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs index 495f2738b5..731150a584 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime), typeof(OsuModPerfect) })] [TestCase(LegacyMods.SpunOut | LegacyMods.Easy, new[] { typeof(OsuModSpunOut), typeof(OsuModEasy) })] - public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); + public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); protected override Ruleset CreateRuleset() => new OsuRuleset(); } diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs index a59544386b..b67dc74ab1 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [TestCase(LegacyMods.SuddenDeath, new[] { typeof(TaikoModSuddenDeath) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime), typeof(TaikoModPerfect) })] - public new void Test(LegacyMods legacyMods, Type[] expectedMods) => base.Test(legacyMods, expectedMods); + public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); protected override Ruleset CreateRuleset() => new TaikoRuleset(); } diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs index e93bf916c7..5327adc428 100644 --- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Beatmaps /// protected abstract Ruleset CreateRuleset(); - protected void Test(LegacyMods legacyMods, Type[] expectedMods) + protected void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) { var ruleset = CreateRuleset(); var mods = ruleset.ConvertFromLegacyMods(legacyMods).ToList(); From 268bbcf77dee56a5ca1f0e4a0fb0b931c4d6f9a5 Mon Sep 17 00:00:00 2001 From: kamp Date: Sun, 15 Nov 2020 14:22:46 +0100 Subject: [PATCH 4669/6909] Clamp selection movement instead of refusing to move on borders --- .../Edit/OsuSelectionHandler.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 24bf79f9ae..ec8c68005f 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -207,11 +207,17 @@ namespace osu.Game.Rulesets.Osu.Edit Quad quad = getSurroundingQuad(hitObjects); - if (quad.TopLeft.X + delta.X < 0 || - quad.TopLeft.Y + delta.Y < 0 || - quad.BottomRight.X + delta.X > DrawWidth || - quad.BottomRight.Y + delta.Y > DrawHeight) - return false; + Vector2 newTopLeft = quad.TopLeft + delta; + if (newTopLeft.X < 0) + delta.X -= newTopLeft.X; + if (newTopLeft.Y < 0) + delta.Y -= newTopLeft.Y; + + Vector2 newBottomRight = quad.BottomRight + delta; + if (newBottomRight.X > DrawWidth) + delta.X -= newBottomRight.X - DrawWidth; + if (newBottomRight.Y > DrawHeight) + delta.Y -= newBottomRight.Y - DrawHeight; foreach (var h in hitObjects) h.Position += delta; From 8b7429856791f31dfab695e5fa01edcd20c937dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 14:25:22 +0100 Subject: [PATCH 4670/6909] Add base method for testing conversion in other direction --- osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs index 5327adc428..76f97db59f 100644 --- a/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyModConversionTest.cs @@ -31,5 +31,15 @@ namespace osu.Game.Tests.Beatmaps Assert.IsNotNull(mods.SingleOrDefault(mod => mod.GetType() == modType)); } } + + protected void TestToLegacy(LegacyMods expectedLegacyMods, Type[] providedModTypes) + { + var ruleset = CreateRuleset(); + var modInstances = ruleset.GetAllMods() + .Where(mod => providedModTypes.Contains(mod.GetType())) + .ToArray(); + var actualLegacyMods = ruleset.ConvertToLegacyMods(modInstances); + Assert.AreEqual(expectedLegacyMods, actualLegacyMods); + } } } From f9fa7c86eaa169657485dbdaabf975abbedcc65c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 14:53:25 +0100 Subject: [PATCH 4671/6909] Cover mapping fully for catch mods --- .../CatchLegacyModConversionTest.cs | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs index 4102dff37c..eae07daa3d 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchLegacyModConversionTest.cs @@ -12,18 +12,33 @@ namespace osu.Game.Rulesets.Catch.Tests [TestFixture] public class CatchLegacyModConversionTest : LegacyModConversionTest { - [TestCase(LegacyMods.Easy, new[] { typeof(CatchModEasy) })] - [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) })] - [TestCase(LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) })] - [TestCase(LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) })] + private static readonly object[][] catch_mod_mapping = + { + new object[] { LegacyMods.NoFail, new[] { typeof(CatchModNoFail) } }, + new object[] { LegacyMods.Easy, new[] { typeof(CatchModEasy) } }, + new object[] { LegacyMods.Hidden, new[] { typeof(CatchModHidden) } }, + new object[] { LegacyMods.HardRock, new[] { typeof(CatchModHardRock) } }, + new object[] { LegacyMods.SuddenDeath, new[] { typeof(CatchModSuddenDeath) } }, + new object[] { LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime) } }, + new object[] { LegacyMods.Relax, new[] { typeof(CatchModRelax) } }, + new object[] { LegacyMods.HalfTime, new[] { typeof(CatchModHalfTime) } }, + new object[] { LegacyMods.Nightcore, new[] { typeof(CatchModNightcore) } }, + new object[] { LegacyMods.Flashlight, new[] { typeof(CatchModFlashlight) } }, + new object[] { LegacyMods.Autoplay, new[] { typeof(CatchModAutoplay) } }, + new object[] { LegacyMods.Perfect, new[] { typeof(CatchModPerfect) } }, + new object[] { LegacyMods.Cinema, new[] { typeof(CatchModCinema) } }, + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(CatchModHardRock), typeof(CatchModDoubleTime) } } + }; + + [TestCaseSource(nameof(catch_mod_mapping))] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(CatchModCinema) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModNightcore) })] - [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(CatchModFlashlight), typeof(CatchModNightcore) })] - [TestCase(LegacyMods.Perfect, new[] { typeof(CatchModPerfect) })] - [TestCase(LegacyMods.SuddenDeath, new[] { typeof(CatchModSuddenDeath) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(CatchModPerfect) })] - [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(CatchModDoubleTime), typeof(CatchModPerfect) })] public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); + [TestCaseSource(nameof(catch_mod_mapping))] + public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); + protected override Ruleset CreateRuleset() => new CatchRuleset(); } } From ee5e70135f42df4bc31fa2747413886ee090a131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 15:09:02 +0100 Subject: [PATCH 4672/6909] Cover mapping fully for mania mods --- .../ManiaLegacyModConversionTest.cs | 45 ++++++++++++++----- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs index 221eed3576..a28c188051 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaLegacyModConversionTest.cs @@ -12,20 +12,45 @@ namespace osu.Game.Rulesets.Mania.Tests [TestFixture] public class ManiaLegacyModConversionTest : LegacyModConversionTest { - [TestCase(LegacyMods.Easy, new[] { typeof(ManiaModEasy) })] - [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) })] - [TestCase(LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) })] - [TestCase(LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) })] + private static readonly object[][] mania_mod_mapping = + { + new object[] { LegacyMods.NoFail, new[] { typeof(ManiaModNoFail) } }, + new object[] { LegacyMods.Easy, new[] { typeof(ManiaModEasy) } }, + new object[] { LegacyMods.Hidden, new[] { typeof(ManiaModHidden) } }, + new object[] { LegacyMods.HardRock, new[] { typeof(ManiaModHardRock) } }, + new object[] { LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) } }, + new object[] { LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime) } }, + new object[] { LegacyMods.HalfTime, new[] { typeof(ManiaModHalfTime) } }, + new object[] { LegacyMods.Nightcore, new[] { typeof(ManiaModNightcore) } }, + new object[] { LegacyMods.Flashlight, new[] { typeof(ManiaModFlashlight) } }, + new object[] { LegacyMods.Autoplay, new[] { typeof(ManiaModAutoplay) } }, + new object[] { LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) } }, + new object[] { LegacyMods.Key4, new[] { typeof(ManiaModKey4) } }, + new object[] { LegacyMods.Key5, new[] { typeof(ManiaModKey5) } }, + new object[] { LegacyMods.Key6, new[] { typeof(ManiaModKey6) } }, + new object[] { LegacyMods.Key7, new[] { typeof(ManiaModKey7) } }, + new object[] { LegacyMods.Key8, new[] { typeof(ManiaModKey8) } }, + new object[] { LegacyMods.FadeIn, new[] { typeof(ManiaModFadeIn) } }, + new object[] { LegacyMods.Random, new[] { typeof(ManiaModRandom) } }, + new object[] { LegacyMods.Cinema, new[] { typeof(ManiaModCinema) } }, + new object[] { LegacyMods.Key9, new[] { typeof(ManiaModKey9) } }, + new object[] { LegacyMods.KeyCoop, new[] { typeof(ManiaModDualStages) } }, + new object[] { LegacyMods.Key1, new[] { typeof(ManiaModKey1) } }, + new object[] { LegacyMods.Key3, new[] { typeof(ManiaModKey3) } }, + new object[] { LegacyMods.Key2, new[] { typeof(ManiaModKey2) } }, + new object[] { LegacyMods.Mirror, new[] { typeof(ManiaModMirror) } }, + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(ManiaModHardRock), typeof(ManiaModDoubleTime) } } + }; + + [TestCaseSource(nameof(mania_mod_mapping))] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(ManiaModCinema) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModNightcore) })] - [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(ManiaModFlashlight), typeof(ManiaModNightcore) })] - [TestCase(LegacyMods.Perfect, new[] { typeof(ManiaModPerfect) })] - [TestCase(LegacyMods.SuddenDeath, new[] { typeof(ManiaModSuddenDeath) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(ManiaModPerfect) })] - [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(ManiaModDoubleTime), typeof(ManiaModPerfect) })] - [TestCase(LegacyMods.Random | LegacyMods.SuddenDeath, new[] { typeof(ManiaModRandom), typeof(ManiaModSuddenDeath) })] - [TestCase(LegacyMods.Flashlight | LegacyMods.Mirror, new[] { typeof(ManiaModFlashlight), typeof(ManiaModMirror) })] public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); + [TestCaseSource(nameof(mania_mod_mapping))] + public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); + protected override Ruleset CreateRuleset() => new ManiaRuleset(); } } From 9f27d4a9f45a438c07463d2d5c41b3a6a1d4f26f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 15:16:17 +0100 Subject: [PATCH 4673/6909] Cover mapping fully for osu! mods --- .../OsuLegacyModConversionTest.cs | 36 ++++++++++++++----- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs index 731150a584..51da5b85cd 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuLegacyModConversionTest.cs @@ -12,19 +12,37 @@ namespace osu.Game.Rulesets.Osu.Tests [TestFixture] public class OsuLegacyModConversionTest : LegacyModConversionTest { - [TestCase(LegacyMods.Easy, new[] { typeof(OsuModEasy) })] - [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) })] - [TestCase(LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) })] - [TestCase(LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) })] + private static readonly object[][] osu_mod_mapping = + { + new object[] { LegacyMods.NoFail, new[] { typeof(OsuModNoFail) } }, + new object[] { LegacyMods.Easy, new[] { typeof(OsuModEasy) } }, + new object[] { LegacyMods.TouchDevice, new[] { typeof(OsuModTouchDevice) } }, + new object[] { LegacyMods.Hidden, new[] { typeof(OsuModHidden) } }, + new object[] { LegacyMods.HardRock, new[] { typeof(OsuModHardRock) } }, + new object[] { LegacyMods.SuddenDeath, new[] { typeof(OsuModSuddenDeath) } }, + new object[] { LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime) } }, + new object[] { LegacyMods.Relax, new[] { typeof(OsuModRelax) } }, + new object[] { LegacyMods.HalfTime, new[] { typeof(OsuModHalfTime) } }, + new object[] { LegacyMods.Nightcore, new[] { typeof(OsuModNightcore) } }, + new object[] { LegacyMods.Flashlight, new[] { typeof(OsuModFlashlight) } }, + new object[] { LegacyMods.Autoplay, new[] { typeof(OsuModAutoplay) } }, + new object[] { LegacyMods.SpunOut, new[] { typeof(OsuModSpunOut) } }, + new object[] { LegacyMods.Autopilot, new[] { typeof(OsuModAutopilot) } }, + new object[] { LegacyMods.Perfect, new[] { typeof(OsuModPerfect) } }, + new object[] { LegacyMods.Cinema, new[] { typeof(OsuModCinema) } }, + new object[] { LegacyMods.Target, new[] { typeof(OsuModTarget) } }, + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(OsuModHardRock), typeof(OsuModDoubleTime) } } + }; + + [TestCaseSource(nameof(osu_mod_mapping))] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(OsuModCinema) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModNightcore) })] - [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(OsuModFlashlight), typeof(OsuModFlashlight) })] - [TestCase(LegacyMods.Perfect, new[] { typeof(OsuModPerfect) })] - [TestCase(LegacyMods.SuddenDeath, new[] { typeof(OsuModSuddenDeath) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(OsuModPerfect) })] - [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(OsuModDoubleTime), typeof(OsuModPerfect) })] - [TestCase(LegacyMods.SpunOut | LegacyMods.Easy, new[] { typeof(OsuModSpunOut), typeof(OsuModEasy) })] public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); + [TestCaseSource(nameof(osu_mod_mapping))] + public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); + protected override Ruleset CreateRuleset() => new OsuRuleset(); } } From e9b5f54128b8ced841062a40406a21946af888ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 15:24:57 +0100 Subject: [PATCH 4674/6909] Cover mapping fully for taiko mods --- .../TaikoLegacyModConversionTest.cs | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs index b67dc74ab1..a039e84106 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoLegacyModConversionTest.cs @@ -12,18 +12,34 @@ namespace osu.Game.Rulesets.Taiko.Tests [TestFixture] public class TaikoLegacyModConversionTest : LegacyModConversionTest { - [TestCase(LegacyMods.Easy, new[] { typeof(TaikoModEasy) })] - [TestCase(LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) })] - [TestCase(LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) })] - [TestCase(LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) })] + private static readonly object[][] taiko_mod_mapping = + { + new object[] { LegacyMods.NoFail, new[] { typeof(TaikoModNoFail) } }, + new object[] { LegacyMods.Easy, new[] { typeof(TaikoModEasy) } }, + new object[] { LegacyMods.Hidden, new[] { typeof(TaikoModHidden) } }, + new object[] { LegacyMods.HardRock, new[] { typeof(TaikoModHardRock) } }, + new object[] { LegacyMods.SuddenDeath, new[] { typeof(TaikoModSuddenDeath) } }, + new object[] { LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime) } }, + new object[] { LegacyMods.Relax, new[] { typeof(TaikoModRelax) } }, + new object[] { LegacyMods.HalfTime, new[] { typeof(TaikoModHalfTime) } }, + new object[] { LegacyMods.Nightcore, new[] { typeof(TaikoModNightcore) } }, + new object[] { LegacyMods.Flashlight, new[] { typeof(TaikoModFlashlight) } }, + new object[] { LegacyMods.Autoplay, new[] { typeof(TaikoModAutoplay) } }, + new object[] { LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) } }, + new object[] { LegacyMods.Random, new[] { typeof(TaikoModRandom) } }, + new object[] { LegacyMods.Cinema, new[] { typeof(TaikoModCinema) } }, + new object[] { LegacyMods.HardRock | LegacyMods.DoubleTime, new[] { typeof(TaikoModHardRock), typeof(TaikoModDoubleTime) } } + }; + + [TestCaseSource(nameof(taiko_mod_mapping))] + [TestCase(LegacyMods.Cinema | LegacyMods.Autoplay, new[] { typeof(TaikoModCinema) })] [TestCase(LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModNightcore) })] - [TestCase(LegacyMods.Flashlight | LegacyMods.Nightcore | LegacyMods.DoubleTime, new[] { typeof(TaikoModFlashlight), typeof(TaikoModNightcore) })] - [TestCase(LegacyMods.Perfect, new[] { typeof(TaikoModPerfect) })] - [TestCase(LegacyMods.SuddenDeath, new[] { typeof(TaikoModSuddenDeath) })] [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath, new[] { typeof(TaikoModPerfect) })] - [TestCase(LegacyMods.Perfect | LegacyMods.SuddenDeath | LegacyMods.DoubleTime, new[] { typeof(TaikoModDoubleTime), typeof(TaikoModPerfect) })] public new void TestFromLegacy(LegacyMods legacyMods, Type[] expectedMods) => base.TestFromLegacy(legacyMods, expectedMods); + [TestCaseSource(nameof(taiko_mod_mapping))] + public new void TestToLegacy(LegacyMods legacyMods, Type[] givenMods) => base.TestToLegacy(legacyMods, givenMods); + protected override Ruleset CreateRuleset() => new TaikoRuleset(); } } From 5ace7abaa8f49372293ca77ffde1ec8613a4f014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 15:29:50 +0100 Subject: [PATCH 4675/6909] Add abstract non-generic ModNightcore to pattern-match over --- osu.Game/Rulesets/Mods/ModNightcore.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModNightcore.cs b/osu.Game/Rulesets/Mods/ModNightcore.cs index e8b051b4d9..a44967c21c 100644 --- a/osu.Game/Rulesets/Mods/ModNightcore.cs +++ b/osu.Game/Rulesets/Mods/ModNightcore.cs @@ -18,14 +18,17 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Mods { - public abstract class ModNightcore : ModDoubleTime, IApplicableToDrawableRuleset - where TObject : HitObject + public abstract class ModNightcore : ModDoubleTime { public override string Name => "Nightcore"; public override string Acronym => "NC"; public override IconUsage? Icon => OsuIcon.ModNightcore; public override string Description => "Uguuuuuuuu..."; + } + public abstract class ModNightcore : ModNightcore, IApplicableToDrawableRuleset + where TObject : HitObject + { private readonly BindableNumber tempoAdjust = new BindableDouble(1); private readonly BindableNumber freqAdjust = new BindableDouble(1); From 5d44286d387bd2fe19994bf9682809599f610607 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 15:35:06 +0100 Subject: [PATCH 4676/6909] Add missing generic mappings to legacy mods --- osu.Game/Rulesets/Ruleset.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index 8caadffd1d..b3b3d11ab3 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -81,10 +81,18 @@ namespace osu.Game.Rulesets value |= LegacyMods.HardRock; break; + case ModPerfect _: + value |= LegacyMods.Perfect; + break; + case ModSuddenDeath _: value |= LegacyMods.SuddenDeath; break; + case ModNightcore _: + value |= LegacyMods.Nightcore; + break; + case ModDoubleTime _: value |= LegacyMods.DoubleTime; break; @@ -100,6 +108,14 @@ namespace osu.Game.Rulesets case ModFlashlight _: value |= LegacyMods.Flashlight; break; + + case ModCinema _: + value |= LegacyMods.Cinema; + break; + + case ModAutoplay _: + value |= LegacyMods.Autoplay; + break; } } From e5d9cca9de2654b8decdf41708ed6ffda2838230 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 15:38:12 +0100 Subject: [PATCH 4677/6909] Fix mania-specific missing cases --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index b92e042686..f70e7b315e 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -119,6 +119,9 @@ namespace osu.Game.Rulesets.Mania if (mods.HasFlag(LegacyMods.Key9)) yield return new ManiaModKey9(); + if (mods.HasFlag(LegacyMods.KeyCoop)) + yield return new ManiaModDualStages(); + if (mods.HasFlag(LegacyMods.NoFail)) yield return new ManiaModNoFail(); @@ -173,13 +176,22 @@ namespace osu.Game.Rulesets.Mania value |= LegacyMods.Key9; break; + case ManiaModDualStages _: + value |= LegacyMods.KeyCoop; + break; + case ManiaModFadeIn _: value |= LegacyMods.FadeIn; + value &= ~LegacyMods.Hidden; // due to inheritance break; case ManiaModMirror _: value |= LegacyMods.Mirror; break; + + case ManiaModRandom _: + value |= LegacyMods.Random; + break; } } From a17990f2eea290477ad9b0bc740a652202520cae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 15:40:31 +0100 Subject: [PATCH 4678/6909] Fix osu!-specific missing cases --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 678fb8aba6..d8180b0e58 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -107,6 +107,35 @@ namespace osu.Game.Rulesets.Osu yield return new OsuModTouchDevice(); } + public override LegacyMods ConvertToLegacyMods(Mod[] mods) + { + var value = base.ConvertToLegacyMods(mods); + + foreach (var mod in mods) + { + switch (mod) + { + case OsuModAutopilot _: + value |= LegacyMods.Autopilot; + break; + + case OsuModSpunOut _: + value |= LegacyMods.SpunOut; + break; + + case OsuModTarget _: + value |= LegacyMods.Target; + break; + + case OsuModTouchDevice _: + value |= LegacyMods.TouchDevice; + break; + } + } + + return value; + } + public override IEnumerable GetModsFor(ModType type) { switch (type) From 7736d1ae85bfcfd7cfc6343a69e742da59c7423a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 15:41:58 +0100 Subject: [PATCH 4679/6909] Fix taiko-specific missing cases --- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 73e9c16d07..2a49dd655c 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -92,6 +92,19 @@ namespace osu.Game.Rulesets.Taiko if (mods.HasFlag(LegacyMods.Relax)) yield return new TaikoModRelax(); + + if (mods.HasFlag(LegacyMods.Random)) + yield return new TaikoModRandom(); + } + + public override LegacyMods ConvertToLegacyMods(Mod[] mods) + { + var value = base.ConvertToLegacyMods(mods); + + if (mods.OfType().Any()) + value |= LegacyMods.Random; + + return value; } public override IEnumerable GetModsFor(ModType type) From 83727a2e85a868d612a80bdc7a65bb0bf7b6befd Mon Sep 17 00:00:00 2001 From: kamp Date: Sun, 15 Nov 2020 16:06:29 +0100 Subject: [PATCH 4680/6909] Add quick-delete tests --- .../Editing/TestSceneEditorQuickDelete.cs | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs new file mode 100644 index 0000000000..cb921fd857 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; +using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; +using osu.Game.Tests.Beatmaps; +using osu.Game.Rulesets.Edit; +using osu.Game.Screens.Edit.Compose; +using osu.Game.Screens.Edit.Compose.Components; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneEditorQuickDelete : EditorTestScene + { + protected override Ruleset CreateEditorRuleset() => new OsuRuleset(); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + + [Test] + public void TestQuickDeleteRemovesObject() + { + var addedObject = new HitCircle { StartTime = 1000 }; + + AddStep("add hitobject", () => EditorBeatmap.Add(addedObject)); + + AddStep("select added object", () => EditorBeatmap.SelectedHitObjects.Add(addedObject)); + + AddStep("move mouse to object", () => + { + var pos = getBlueprintContainer.SelectionBlueprints + .ChildrenOfType().First() + .ChildrenOfType().First().ScreenSpaceDrawQuad.Centre; + InputManager.MoveMouseTo(pos); + }); + AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); + AddStep("rightclick", () => InputManager.Click(MouseButton.Right)); + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + + AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); + } + + [Test] + public void TestQuickDeleteRemovesSliderControlPoint() + { + Slider slider = new Slider { StartTime = 1000 }; + + PathControlPoint[] points = new PathControlPoint[] + { + new PathControlPoint(), + new PathControlPoint(new Vector2(50, 0)), + new PathControlPoint(new Vector2(100, 0)) + }; + + AddStep("add slider", () => + { + slider.Path = new SliderPath(points); + EditorBeatmap.Add(slider); + }); + + AddStep("select added slider", () => EditorBeatmap.SelectedHitObjects.Add(slider)); + + AddStep("move mouse to controlpoint", () => + { + // This doesn't get the HitCirclePiece corresponding to the last control point on consecutive runs, + // causing the slider to translate by 50 every time and go off the screen after about 10 runs. + var pos = getBlueprintContainer.SelectionBlueprints + .ChildrenOfType().First() + .ChildrenOfType().Last().ScreenSpaceDrawQuad.Centre; + InputManager.MoveMouseTo(pos); + }); + AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); + AddStep("rightclick", () => InputManager.Click(MouseButton.Right)); + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + + AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2); + } + + private BlueprintContainer getBlueprintContainer => Editor.ChildrenOfType().First() + .ChildrenOfType().First() + .ChildrenOfType().First(); + } +} From 6b38bb9d76e8da7857ef2e197ba77ce674942285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 16:38:25 +0100 Subject: [PATCH 4681/6909] Add test coverage for new ownerless behaviour --- .../TestScenePreviewTrackManager.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs index d76905dab8..a3db20ce83 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs @@ -167,6 +167,21 @@ namespace osu.Game.Tests.Visual.Components AddAssert("game not muted", () => audio.Tracks.AggregateVolume.Value != 0); } + [Test] + public void TestOwnerNotRegistered() + { + PreviewTrack track = null; + + AddStep("get track", () => Add(new TestTrackOwner(track = getTrack(), registerAsOwner: false))); + AddUntilStep("wait for loaded", () => track.IsLoaded); + + AddStep("start track", () => track.Start()); + AddUntilStep("track is running", () => track.IsRunning); + + AddStep("cancel from anyone", () => trackManager.StopAnyPlaying(this)); + AddAssert("track stopped", () => !track.IsRunning); + } + private TestPreviewTrack getTrack() => (TestPreviewTrack)trackManager.Get(null); private TestPreviewTrack getOwnedTrack() @@ -181,10 +196,12 @@ namespace osu.Game.Tests.Visual.Components private class TestTrackOwner : CompositeDrawable, IPreviewTrackOwner { private readonly PreviewTrack track; + private readonly bool registerAsOwner; - public TestTrackOwner(PreviewTrack track) + public TestTrackOwner(PreviewTrack track, bool registerAsOwner = true) { this.track = track; + this.registerAsOwner = registerAsOwner; } [BackgroundDependencyLoader] @@ -196,7 +213,8 @@ namespace osu.Game.Tests.Visual.Components protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(this); + if (registerAsOwner) + dependencies.CacheAs(this); return dependencies; } } From 1db303b15924cba91b828d553a858508c6840b38 Mon Sep 17 00:00:00 2001 From: kamp Date: Sun, 15 Nov 2020 16:54:48 +0100 Subject: [PATCH 4682/6909] Revert beginClickSelection logic --- .../Edit/Compose/Components/BlueprintContainer.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index c9043ccef3..4390ed4f10 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -338,15 +338,18 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. private bool beginClickSelection(MouseButtonEvent e) { - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.ToList()) + bool selectedPerformed = true; + + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) { if (!blueprint.IsHovered) continue; - if (SelectionHandler.HandleSelectionRequested(blueprint, e)) - return clickSelectionBegan = true; + selectedPerformed &= SelectionHandler.HandleSelectionRequested(blueprint, e); + clickSelectionBegan = true; + break; } - return false; + return selectedPerformed; } /// From c77ec3e905adc7ffd5bf2e74001cea184081887d Mon Sep 17 00:00:00 2001 From: kamp Date: Sun, 15 Nov 2020 17:42:52 +0100 Subject: [PATCH 4683/6909] Fix slider control point quickdelete test --- osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs index cb921fd857..55daf130a8 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; +using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Tests.Beatmaps; using osu.Game.Rulesets.Edit; using osu.Game.Screens.Edit.Compose; @@ -72,11 +73,10 @@ namespace osu.Game.Tests.Visual.Editing AddStep("move mouse to controlpoint", () => { - // This doesn't get the HitCirclePiece corresponding to the last control point on consecutive runs, - // causing the slider to translate by 50 every time and go off the screen after about 10 runs. var pos = getBlueprintContainer.SelectionBlueprints .ChildrenOfType().First() - .ChildrenOfType().Last().ScreenSpaceDrawQuad.Centre; + .ChildrenOfType().First() + .ChildrenOfType().ElementAt(1).ScreenSpaceDrawQuad.Centre; InputManager.MoveMouseTo(pos); }); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); From 42de4437ccf3d18271e6c119a8f2d40fd619b4c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 19:43:42 +0100 Subject: [PATCH 4684/6909] Add failing test case --- .../Gameplay/TestScenePoolingRuleset.cs | 44 ++++++++++++++++++- 1 file changed, 43 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index d009d805f0..3e777119c4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -57,6 +57,43 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("DHO reused", () => this.ChildrenOfType().Single() == firstObject); } + [Test] + public void TestCustomTransformsClearedBetweenReuses() + { + ManualClock clock = null; + + createTest(new Beatmap + { + HitObjects = + { + new HitObject(), + new HitObject { StartTime = 2000 } + } + }, 1, () => new FramedClock(clock = new ManualClock())); + + DrawableTestHitObject firstObject = null; + Vector2 position = default; + + AddUntilStep("first object shown", () => this.ChildrenOfType().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[0]); + AddStep("get DHO", () => firstObject = this.ChildrenOfType().Single()); + AddStep("store position", () => position = firstObject.Position); + AddStep("add custom transform", () => firstObject.ApplyCustomUpdateState += onStateUpdate); + + AddStep("fast forward past first object", () => clock.CurrentTime = 1500); + AddStep("unapply custom transform", () => firstObject.ApplyCustomUpdateState -= onStateUpdate); + + AddStep("fast forward to second object", () => clock.CurrentTime = drawableRuleset.Beatmap.HitObjects[1].StartTime); + AddUntilStep("second object shown", () => this.ChildrenOfType().SingleOrDefault()?.HitObject == drawableRuleset.Beatmap.HitObjects[1]); + AddAssert("DHO reused", () => this.ChildrenOfType().Single() == firstObject); + AddAssert("object in new position", () => firstObject.Position != position); + + void onStateUpdate(DrawableHitObject hitObject, ArmedState state) + { + using (hitObject.BeginAbsoluteSequence(hitObject.StateUpdateTime)) + hitObject.MoveToOffset(new Vector2(-100, 0)); + } + } + [Test] public void TestNotReusedWithHitObjectsSpacedClose() { @@ -210,7 +247,6 @@ namespace osu.Game.Tests.Visual.Gameplay Anchor = Anchor.Centre; Origin = Anchor.Centre; - Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); Size = new Vector2(50, 50); Colour = new Color4(RNG.NextSingle(), RNG.NextSingle(), RNG.NextSingle(), 1f); @@ -225,6 +261,12 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + protected override void OnApply(HitObject hitObject) + { + base.OnApply(hitObject); + Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); + } + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (timeOffset > HitObject.Duration) From 9c0a0031d671712e96061b534732732af1989776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 19:45:49 +0100 Subject: [PATCH 4685/6909] Clear existing transforms on DHO return to pool --- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index b400c532c5..930b1471f3 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -287,6 +287,8 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject = null; lifetimeEntry = null; + clearExistingStateTransforms(); + hasHitObjectApplied = false; } @@ -403,8 +405,7 @@ namespace osu.Game.Rulesets.Objects.Drawables double transformTime = HitObject.StartTime - InitialLifetimeOffset; - base.ApplyTransformsAt(double.MinValue, true); - base.ClearTransformsAfter(double.MinValue, true); + clearExistingStateTransforms(); using (BeginAbsoluteSequence(transformTime, true)) UpdateInitialTransforms(); @@ -432,6 +433,12 @@ namespace osu.Game.Rulesets.Objects.Drawables PlaySamples(); } + private void clearExistingStateTransforms() + { + base.ApplyTransformsAt(double.MinValue, true); + base.ClearTransformsAfter(double.MinValue, true); + } + /// /// Apply (generally fade-in) transforms leading into the start time. /// The local drawable hierarchy is recursively delayed to for convenience. From 4e77800b98aa35758c555d4c690e34d4aa6312aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 20:51:35 +0100 Subject: [PATCH 4686/6909] Rename & simplify property --- .../Visual/Editing/TestSceneEditorQuickDelete.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs index 55daf130a8..5bae6ec5f7 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -28,6 +28,9 @@ namespace osu.Game.Tests.Visual.Editing protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(ruleset, false); + private BlueprintContainer blueprintContainer + => Editor.ChildrenOfType().First(); + [Test] public void TestQuickDeleteRemovesObject() { @@ -39,7 +42,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("move mouse to object", () => { - var pos = getBlueprintContainer.SelectionBlueprints + var pos = blueprintContainer.SelectionBlueprints .ChildrenOfType().First() .ChildrenOfType().First().ScreenSpaceDrawQuad.Centre; InputManager.MoveMouseTo(pos); @@ -73,7 +76,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("move mouse to controlpoint", () => { - var pos = getBlueprintContainer.SelectionBlueprints + var pos = blueprintContainer.SelectionBlueprints .ChildrenOfType().First() .ChildrenOfType().First() .ChildrenOfType().ElementAt(1).ScreenSpaceDrawQuad.Centre; @@ -85,9 +88,5 @@ namespace osu.Game.Tests.Visual.Editing AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2); } - - private BlueprintContainer getBlueprintContainer => Editor.ChildrenOfType().First() - .ChildrenOfType().First() - .ChildrenOfType().First(); } } From 1f0945d4deef4678fcc785a7931bdd6fcf7c9665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 20:52:33 +0100 Subject: [PATCH 4687/6909] Simplify accesses via ChildrenOfType() --- .../Visual/Editing/TestSceneEditorQuickDelete.cs | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs index 5bae6ec5f7..9d35185f7e 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -9,13 +9,9 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles; using osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components; -using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders; using osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components; using osu.Game.Tests.Beatmaps; -using osu.Game.Rulesets.Edit; -using osu.Game.Screens.Edit.Compose; using osu.Game.Screens.Edit.Compose.Components; using osuTK; using osuTK.Input; @@ -42,9 +38,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("move mouse to object", () => { - var pos = blueprintContainer.SelectionBlueprints - .ChildrenOfType().First() - .ChildrenOfType().First().ScreenSpaceDrawQuad.Centre; + var pos = blueprintContainer.ChildrenOfType().First().ScreenSpaceDrawQuad.Centre; InputManager.MoveMouseTo(pos); }); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); @@ -76,10 +70,7 @@ namespace osu.Game.Tests.Visual.Editing AddStep("move mouse to controlpoint", () => { - var pos = blueprintContainer.SelectionBlueprints - .ChildrenOfType().First() - .ChildrenOfType().First() - .ChildrenOfType().ElementAt(1).ScreenSpaceDrawQuad.Centre; + var pos = blueprintContainer.ChildrenOfType().ElementAt(1).ScreenSpaceDrawQuad.Centre; InputManager.MoveMouseTo(pos); }); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); From 337311c3236c848c9ead6c885991a227f288a581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 20:52:58 +0100 Subject: [PATCH 4688/6909] Remove redundant type specification --- osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs index 9d35185f7e..9bcb056f25 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Editing { Slider slider = new Slider { StartTime = 1000 }; - PathControlPoint[] points = new PathControlPoint[] + PathControlPoint[] points = { new PathControlPoint(), new PathControlPoint(new Vector2(50, 0)), From 399a1a16a070d31e36a2076f505dd9111de8f381 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 15 Nov 2020 21:06:47 +0100 Subject: [PATCH 4689/6909] Refactor beginClickSelection in a slightly different way --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 4390ed4f10..8f3c86b98a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -338,18 +338,14 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. private bool beginClickSelection(MouseButtonEvent e) { - bool selectedPerformed = true; - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) { if (!blueprint.IsHovered) continue; - selectedPerformed &= SelectionHandler.HandleSelectionRequested(blueprint, e); - clickSelectionBegan = true; - break; + return clickSelectionBegan = SelectionHandler.HandleSelectionRequested(blueprint, e); } - return selectedPerformed; + return false; } /// From 7169dc91735feb4250636432ee7f5a07114cd94a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Nov 2020 14:06:37 +0900 Subject: [PATCH 4690/6909] Add extra step checking slider deletion on second click --- .../Visual/Editing/TestSceneEditorQuickDelete.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs index 9bcb056f25..9efd299fba 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorQuickDelete.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Editing InputManager.MoveMouseTo(pos); }); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); - AddStep("rightclick", () => InputManager.Click(MouseButton.Right)); + AddStep("right click", () => InputManager.Click(MouseButton.Right)); AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); @@ -74,10 +74,15 @@ namespace osu.Game.Tests.Visual.Editing InputManager.MoveMouseTo(pos); }); AddStep("hold shift", () => InputManager.PressKey(Key.ShiftLeft)); - AddStep("rightclick", () => InputManager.Click(MouseButton.Right)); - AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); + AddStep("right click", () => InputManager.Click(MouseButton.Right)); AddAssert("slider has 2 points", () => slider.Path.ControlPoints.Count == 2); + + // second click should nuke the object completely. + AddStep("right click", () => InputManager.Click(MouseButton.Right)); + AddAssert("no hitobjects in beatmap", () => EditorBeatmap.HitObjects.Count == 0); + + AddStep("release shift", () => InputManager.ReleaseKey(Key.ShiftLeft)); } } } From a4c17906b402781c6041f71585e7bc3a015db0bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Nov 2020 14:52:18 +0900 Subject: [PATCH 4691/6909] Make comment a bit more explicit --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index f70e7b315e..906f7382c5 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Mania case ManiaModFadeIn _: value |= LegacyMods.FadeIn; - value &= ~LegacyMods.Hidden; // due to inheritance + value &= ~LegacyMods.Hidden; // this is toggled on in the base call due to inheritance, but we don't want that. break; case ManiaModMirror _: From b20898a1ac289e552c2514b995534173d2077b99 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 16 Nov 2020 14:57:52 +0900 Subject: [PATCH 4692/6909] Use `dotnet tool` for InspectCode build script --- .config/dotnet-tools.json | 18 ++++++++++++++++++ build/InspectCode.cake | 23 ++++++----------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 6ba6ae82c8..dd53eefd23 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -13,6 +13,24 @@ "commands": [ "dotnet-format" ] + }, + "jetbrains.resharper.globaltools": { + "version": "2020.2.4", + "commands": [ + "jb" + ] + }, + "nvika": { + "version": "2.0.0", + "commands": [ + "nvika" + ] + }, + "codefilesanity": { + "version": "15.0.0", + "commands": [ + "CodeFileSanity" + ] } } } \ No newline at end of file diff --git a/build/InspectCode.cake b/build/InspectCode.cake index c8f4f37c94..6836d9071b 100644 --- a/build/InspectCode.cake +++ b/build/InspectCode.cake @@ -1,7 +1,4 @@ #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(); /////////////////////////////////////////////////////////////////////////////// // ARGUMENTS @@ -18,23 +15,15 @@ var desktopSlnf = rootDirectory.CombineWithFilePath("osu.Desktop.slnf"); // TASKS /////////////////////////////////////////////////////////////////////////////// -// windows only because both inspectcode and nvika depend on net45 Task("InspectCode") - .WithCriteria(IsRunningOnWindows()) .Does(() => { - InspectCode(desktopSlnf, new InspectCodeSettings { - CachesHome = "inspectcode", - OutputFile = "inspectcodereport.xml", - ArgumentCustomization = arg => { - if (AppVeyor.IsRunningOnAppVeyor) // Don't flood CI output - arg.Append("--verbosity:WARN"); - return arg; - }, - }); + var inspectcodereport = "inspectcodereport.xml"; + var cacheDir = "inspectcode"; + var verbosity = AppVeyor.IsRunningOnAppVeyor ? "WARN" : "INFO"; // Don't flood CI output - int returnCode = StartProcess(nVikaToolPath, $@"parsereport ""inspectcodereport.xml"" --treatwarningsaserrors"); - if (returnCode != 0) - throw new Exception($"inspectcode failed with return code {returnCode}"); + DotNetCoreTool(rootDirectory.FullPath, + "jb", $@"inspectcode ""{desktopSlnf}"" --output=""{inspectcodereport}"" --caches-home=""{cacheDir}"" --verbosity={verbosity}"); + DotNetCoreTool(rootDirectory.FullPath, "nvika", $@"parsereport ""{inspectcodereport}"" --treatwarningsaserrors"); }); Task("CodeFileSanity") From 9a7fdb2b7e31e0495c12e5294c89a24042f8254d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Nov 2020 16:43:17 +0900 Subject: [PATCH 4693/6909] Move skin deletion logic to OsuGameBase to promote thread safety `CurrentSkinInfo` is used in multiple places expecting thread safety, while ItemRemoved events are explicitly mentioning they are not thread safe. As SkinManager itself doesn't have the ability to schedule to the update thread, I've just moved the logic to `OsuGameBase`. We may want to move the current skin bindable out of the manager class in the future to match things like `BeatmapManager`. Closes https://github.com/ppy/osu/issues/10837. --- osu.Game/OsuGameBase.cs | 11 +++++++++++ osu.Game/Skinning/SkinManager.cs | 10 ---------- 2 files changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 193f6fe61b..1147f67ad2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -194,6 +194,17 @@ namespace osu.Game dependencies.Cache(SkinManager = new SkinManager(Storage, contextFactory, Host, Audio, new NamespacedResourceStore(Resources, "Skins/Legacy"))); dependencies.CacheAs(SkinManager); + // needs to be done here rather than inside SkinManager to ensure thread safety of CurrentSkinInfo. + SkinManager.ItemRemoved.BindValueChanged(weakRemovedInfo => + { + 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 == SkinManager.CurrentSkinInfo.Value.ID) + Schedule(() => SkinManager.CurrentSkinInfo.Value = SkinInfo.Default); + } + }); + dependencies.CacheAs(API ??= new APIAccess(LocalConfig)); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index bef3e86a4d..9b69a1eecd 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -48,16 +48,6 @@ namespace osu.Game.Skinning this.audio = audio; this.legacyDefaultResources = legacyDefaultResources; - ItemRemoved.BindValueChanged(weakRemovedInfo => - { - 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 709370c69b366718596ccb6409a992c1ffacf957 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Nov 2020 16:49:31 +0900 Subject: [PATCH 4694/6909] Move schedule call outwards --- osu.Game/OsuGameBase.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1147f67ad2..e7b5d3304d 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -199,9 +199,12 @@ namespace osu.Game { 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 == SkinManager.CurrentSkinInfo.Value.ID) - Schedule(() => SkinManager.CurrentSkinInfo.Value = SkinInfo.Default); + Schedule(() => + { + // check the removed skin is not the current user choice. if it is, switch back to default. + if (removedInfo.ID == SkinManager.CurrentSkinInfo.Value.ID) + SkinManager.CurrentSkinInfo.Value = SkinInfo.Default; + }); } }); From dc38aeac4392d44b1b99aa3b4a18e47aa5373f21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 16 Nov 2020 17:23:02 +0900 Subject: [PATCH 4695/6909] Remove unnecessary local definition of colour logic from taiko judgement --- .../UI/DrawableTaikoJudgement.cs | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index cbfc5a8628..b5e35f88b5 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.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 osu.Game.Rulesets.Objects.Drawables; -using osu.Framework.Allocation; -using osu.Game.Graphics; -using osu.Game.Rulesets.Judgements; using osu.Framework.Graphics; -using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.UI { @@ -25,21 +22,6 @@ namespace osu.Game.Rulesets.Taiko.UI { } - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - switch (Result.Type) - { - case HitResult.Ok: - JudgementBody.Colour = colours.GreenLight; - break; - - case HitResult.Great: - JudgementBody.Colour = colours.BlueLight; - break; - } - } - protected override void ApplyHitAnimations() { this.MoveToY(-100, 500); From d7acfd5413cb1844b55f3f858a2b97147d8787cc Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Mon, 16 Nov 2020 18:15:15 +0800 Subject: [PATCH 4696/6909] Remove retires from ModEasy --- osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs | 42 +++++++++++++++++ osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs | 47 +++++++++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs | 49 +++++++++++++++++++- osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 33 ++----------- osu.Game/Rulesets/Mods/ModEasy.cs | 44 ++---------------- 5 files changed, 144 insertions(+), 71 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs index a82d0af102..f58a4051de 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs @@ -1,12 +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.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Mods { public class CatchModEasy : ModEasy { public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; + + [SettingSource("Extra Lives", "Number of extra lives")] + public Bindable Retries { get; } = new BindableInt(2) + { + MinValue = 0, + MaxValue = 10 + }; + public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; + + private int retries; + + private BindableNumber health; + + public override void ReadFromDifficulty(BeatmapDifficulty difficulty) + { + } + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + const float ratio = 0.5f; + difficulty.CircleSize *= ratio; + difficulty.ApproachRate *= ratio; + difficulty.DrainRate *= ratio; + difficulty.OverallDifficulty *= ratio; + + retries = Retries.Value; + } + public bool PerformFail() + { + if (retries == 0) return true; + + health.Value = health.MaxValue; + retries--; + + return false; + } + public bool RestartOnFail => false; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs index ff77df0ae0..52c959395f 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs @@ -1,12 +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 Humanizer; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModEasy : ModEasy { public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!"; + + [SettingSource("Extra Lives", "Number of extra lives")] + public Bindable Retries { get; } = new BindableInt(2) + { + MinValue = 0, + MaxValue = 10 + }; + public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; + + private int retries; + + private BindableNumber health; + + public override void ReadFromDifficulty(BeatmapDifficulty difficulty) + { + } + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + const float ratio = 0.5f; + difficulty.CircleSize *= ratio; + difficulty.ApproachRate *= ratio; + difficulty.DrainRate *= ratio; + difficulty.OverallDifficulty *= ratio; + + retries = Retries.Value; + } + public bool PerformFail() + { + if (retries == 0) return true; + + health.Value = health.MaxValue; + retries--; + + return false; + } + public bool RestartOnFail => false; + + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) + { + health = healthProcessor.Health.GetBoundCopy(); + } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs index f13c7d2ff6..df7f7bbce6 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs @@ -1,12 +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 Humanizer; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModEasy : ModEasy + public class OsuModEasy : ModEasy, IApplicableToDifficulty, IApplicableFailOverride, IApplicableToHealthProcessor { public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; + + [SettingSource("Extra Lives", "Number of extra lives")] + public Bindable Retries { get; } = new BindableInt(2) + { + MinValue = 0, + MaxValue = 10 + }; + public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; + + private int retries; + + private BindableNumber health; + + public override void ReadFromDifficulty(BeatmapDifficulty difficulty) + { + } + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + const float ratio = 0.5f; + difficulty.CircleSize *= ratio; + difficulty.ApproachRate *= ratio; + difficulty.DrainRate *= ratio; + difficulty.OverallDifficulty *= ratio; + + retries = Retries.Value; + } + public bool PerformFail() + { + if (retries == 0) return true; + + health.Value = health.MaxValue; + retries--; + + return false; + } + public bool RestartOnFail => false; + + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) + { + health = healthProcessor.Health.GetBoundCopy(); + } } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index c3b833fa29..9e49c3fa13 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -1,40 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using osu.Framework.Graphics.Sprites; -using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Rulesets.Mods; +using osu.Game.Beatmaps; namespace osu.Game.Rulesets.Taiko.Mods { - public class TaikoModEasy : Mod, IApplicableToDifficulty, IApplicableFailOverride + public class TaikoModEasy : ModEasy { - public override string Name => "Easy"; - public override string Acronym => "EZ"; - public override string Description => @"Beats move slower, less accuracy required"; - public override IconUsage? Icon => OsuIcon.ModEasy; - public override ModType Type => ModType.DifficultyReduction; - public override double ScoreMultiplier => 0.5; - public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModHardRock), typeof(ModDifficultyAdjust) }; - - public void ReadFromDifficulty(BeatmapDifficulty difficulty) - { - } - - public void ApplyToDifficulty(BeatmapDifficulty difficulty) - { - const float ratio = 0.5f; - difficulty.CircleSize *= ratio; - difficulty.ApproachRate *= ratio; - difficulty.DrainRate *= ratio; - difficulty.OverallDifficulty *= ratio; - } - - public bool PerformFail() => true; - - public bool RestartOnFail => false; + public override string Description => @"Beats move slower, less accuracy required!"; } } diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index c6f3930029..cf06091d99 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -2,17 +2,13 @@ // 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; -using osu.Game.Configuration; using osu.Game.Graphics; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModEasy : Mod, IApplicableToDifficulty, IApplicableFailOverride, IApplicableToHealthProcessor + public abstract class ModEasy : Mod, IApplicableToDifficulty { public override string Name => "Easy"; public override string Acronym => "EZ"; @@ -22,49 +18,17 @@ 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")] - public Bindable Retries { get; } = new BindableInt(2) - { - MinValue = 0, - MaxValue = 10 - }; - - public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; - - private int retries; - - private BindableNumber health; - - public void ReadFromDifficulty(BeatmapDifficulty difficulty) + public virtual void ReadFromDifficulty(BeatmapDifficulty difficulty) { } - public void ApplyToDifficulty(BeatmapDifficulty difficulty) + public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty) { const float ratio = 0.5f; difficulty.CircleSize *= ratio; difficulty.ApproachRate *= ratio; difficulty.DrainRate *= ratio; - difficulty.OverallDifficulty *= ratio; - - retries = Retries.Value; - } - - public bool PerformFail() - { - if (retries == 0) return true; - - health.Value = health.MaxValue; - retries--; - - return false; - } - - public bool RestartOnFail => false; - - public void ApplyToHealthProcessor(HealthProcessor healthProcessor) - { - health = healthProcessor.Health.GetBoundCopy(); + difficulty.OverallDifficulty *= ratio;; } } } From f2ef7bee5d48c2946cebcbcd401d9ed1d6d69288 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Mon, 16 Nov 2020 18:17:50 +0800 Subject: [PATCH 4697/6909] Fix checks --- osu.Game/Rulesets/Mods/ModEasy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModEasy.cs b/osu.Game/Rulesets/Mods/ModEasy.cs index cf06091d99..1290e8136c 100644 --- a/osu.Game/Rulesets/Mods/ModEasy.cs +++ b/osu.Game/Rulesets/Mods/ModEasy.cs @@ -28,7 +28,7 @@ namespace osu.Game.Rulesets.Mods difficulty.CircleSize *= ratio; difficulty.ApproachRate *= ratio; difficulty.DrainRate *= ratio; - difficulty.OverallDifficulty *= ratio;; + difficulty.OverallDifficulty *= ratio; } } } From a53b5ef8b95c2d0f7d3c776025e38f7e845cb4ce Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 16 Nov 2020 19:22:08 +0900 Subject: [PATCH 4698/6909] Remove `--no-restore` from VSCode build tasks --- .vscode/tasks.json | 17 ----------------- .../.vscode/tasks.json | 11 ----------- .../.vscode/tasks.json | 11 ----------- osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json | 11 ----------- .../.vscode/tasks.json | 11 ----------- osu.Game.Tournament.Tests/.vscode/tasks.json | 11 ----------- 6 files changed, 72 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index aa77d4f055..a70e5ac3a9 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Desktop", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Desktop", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -40,7 +38,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tests", "-p:GenerateFullPaths=true", "-m", @@ -55,7 +52,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tests", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -71,7 +67,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests", "-p:GenerateFullPaths=true", "-m", @@ -86,7 +81,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -102,7 +96,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Benchmarks", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -111,16 +104,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore (netcoreapp3.1)", - "type": "shell", - "command": "dotnet", - "args": [ - "restore", - "build/Desktop.proj" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json index 2c915a31b7..d8feacc8a7 100644 --- a/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Catch.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Catch.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json index ca03924c70..323110b605 100644 --- a/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Mania.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Mania.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json index 14ffbfb4ae..590bedb8b2 100644 --- a/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Osu.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Osu.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json index 09340f6f9f..63f25c2402 100644 --- a/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json +++ b/osu.Game.Rulesets.Taiko.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Rulesets.Taiko.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file diff --git a/osu.Game.Tournament.Tests/.vscode/tasks.json b/osu.Game.Tournament.Tests/.vscode/tasks.json index c69ac0391a..04ec7275ac 100644 --- a/osu.Game.Tournament.Tests/.vscode/tasks.json +++ b/osu.Game.Tournament.Tests/.vscode/tasks.json @@ -9,7 +9,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests.csproj", "-p:GenerateFullPaths=true", "-m", @@ -24,7 +23,6 @@ "command": "dotnet", "args": [ "build", - "--no-restore", "osu.Game.Tournament.Tests.csproj", "-p:Configuration=Release", "-p:GenerateFullPaths=true", @@ -33,15 +31,6 @@ ], "group": "build", "problemMatcher": "$msCompile" - }, - { - "label": "Restore", - "type": "shell", - "command": "dotnet", - "args": [ - "restore" - ], - "problemMatcher": [] } ] } \ No newline at end of file From 017a6b71531f9d4efe35b2d3c97ddfc1f6274a0c Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Mon, 16 Nov 2020 18:22:17 +0800 Subject: [PATCH 4699/6909] Fix checks --- osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs | 5 +++++ osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs index f58a4051de..6e154c9202 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs @@ -50,5 +50,10 @@ namespace osu.Game.Rulesets.Catch.Mods return false; } public bool RestartOnFail => false; + + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) + { + health = healthProcessor.Health.GetBoundCopy(); + } } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index 9e49c3fa13..0ec24412e9 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Rulesets.Mods; -using osu.Game.Beatmaps; namespace osu.Game.Rulesets.Taiko.Mods { From 16d25c502204c74eca2e6b21b18ec3ca14e9004b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 16 Nov 2020 19:25:36 +0900 Subject: [PATCH 4700/6909] Adjast readme for the removed VSCode restore task --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 86c42dae12..c9443ba063 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,6 @@ git pull Build configurations for the recommended IDEs (listed above) are included. You should use the provided Build/Run functionality of your IDE to get things going. When testing or building new components, it's highly encouraged you use the `VisualTests` project/configuration. More information on this is provided [below](#contributing). - Visual Studio / Rider users should load the project via one of the platform-specific `.slnf` files, rather than the main `.sln.` This will allow access to template run configurations. -- Visual Studio Code users must run the `Restore` task before any build attempt. You can also build and run *osu!* from the command-line with a single command: From 99ee5e3ad74a50bc21855987fcb7640c8ef21d85 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Mon, 16 Nov 2020 18:28:50 +0800 Subject: [PATCH 4701/6909] Correct inheritance --- osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs | 2 +- osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs index 6e154c9202..88d20d15e2 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModEasy : ModEasy + public class CatchModEasy : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor { public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs index 52c959395f..79073b26ba 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModEasy : ModEasy + public class ManiaModEasy : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor { public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!"; diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs index df7f7bbce6..43ceddd6e1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModEasy : ModEasy, IApplicableToDifficulty, IApplicableFailOverride, IApplicableToHealthProcessor + public class OsuModEasy : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor { public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; From 1b1f4c9c09cf8669b957b07a4888cf47dcd67199 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 20:35:22 +0900 Subject: [PATCH 4702/6909] Refactor user request to fix threadsafety issues --- osu.Game/Database/UserLookupCache.cs | 123 ++++++++++----------------- 1 file changed, 47 insertions(+), 76 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index c85ad6d651..05ba9c882b 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; using System.Linq; using System.Threading; @@ -15,103 +14,75 @@ namespace osu.Game.Database { public class UserLookupCache : MemoryCachingComponent { - private readonly HashSet nextTaskIDs = new HashSet(); - [Resolved] private IAPIProvider api { get; set; } - private readonly object taskAssignmentLock = new object(); - - private Task> pendingRequest; - - /// - /// Whether has already grabbed its IDs. - /// - private bool pendingRequestConsumedIDs; - public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token); protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) - { - var users = await getQueryTaskForUser(lookup); - return users.FirstOrDefault(u => u.Id == lookup); - } + => await queryUser(lookup); - /// - /// Return the task responsible for fetching the provided user. - /// This may be part of a larger batch lookup to reduce web requests. - /// - /// The user to lookup. - /// The task responsible for the lookup. - private Task> getQueryTaskForUser(int userId) + private readonly List<(int id, TaskCompletionSource)> pendingUserTasks = new List<(int, TaskCompletionSource)>(); + private Task pendingRequestTask; + private readonly object taskAssignmentLock = new object(); + + private Task queryUser(int userId) { lock (taskAssignmentLock) { - nextTaskIDs.Add(userId); + var tcs = new TaskCompletionSource(); - // if there's a pending request which hasn't been started yet (and is not yet full), we can wait on it. - if (pendingRequest != null && !pendingRequestConsumedIDs && nextTaskIDs.Count < 50) - return pendingRequest; + // Add to the queue. + pendingUserTasks.Add((userId, tcs)); - return queueNextTask(nextLookup); - } + // Create a request task if there's not already one. + if (pendingRequestTask == null) + createNewTask(); - List nextLookup() - { - int[] lookupItems; - - lock (taskAssignmentLock) - { - pendingRequestConsumedIDs = true; - lookupItems = nextTaskIDs.ToArray(); - nextTaskIDs.Clear(); - - if (lookupItems.Length == 0) - { - queueNextTask(null); - return new List(); - } - } - - var request = new GetUsersRequest(lookupItems); - - // rather than queueing, we maintain our own single-threaded request stream. - api.Perform(request); - - return request.Result?.Users; + return tcs.Task; } } - /// - /// Queues new work at the end of the current work tasks. - /// Ensures the provided work is eventually run. - /// - /// The work to run. Can be null to signify the end of available work. - /// The task tracking this work. - private Task> queueNextTask(Func> work) + private void performLookup() { + var userTasks = new List<(int id, TaskCompletionSource task)>(); + + // Grab at most 50 users from the queue. lock (taskAssignmentLock) { - if (work == null) + while (pendingUserTasks.Count > 0 && userTasks.Count < 50) { - pendingRequest = null; - pendingRequestConsumedIDs = false; - } - else if (pendingRequest == null) - { - // special case for the first request ever. - pendingRequest = Task.Run(work); - pendingRequestConsumedIDs = false; - } - else - { - // append the new request on to the last to be executed. - pendingRequest = pendingRequest.ContinueWith(_ => work()); - pendingRequestConsumedIDs = false; - } + (int id, TaskCompletionSource task) next = pendingUserTasks[^1]; - return pendingRequest; + pendingUserTasks.RemoveAt(pendingUserTasks.Count - 1); + + // Perform a secondary check for existence, in case the user was queried in a previous batch. + if (CheckExists(next.id, out var existing)) + next.task.SetResult(existing); + else + userTasks.Add(next); + } } + + // Query the users. + var request = new GetUsersRequest(userTasks.Select(t => t.id).ToArray()); + + // rather than queueing, we maintain our own single-threaded request stream. + api.Perform(request); + + // Create a new request task if there's still more users to query. + lock (taskAssignmentLock) + { + pendingRequestTask = null; + if (pendingUserTasks.Count > 0) + createNewTask(); + } + + // Notify of completion. + foreach (var (id, task) in userTasks) + task.SetResult(request.Result?.Users?.FirstOrDefault(u => u.Id == id)); } + + private void createNewTask() => pendingRequestTask = Task.Run(performLookup); } } From 87bf168718bd918fb90cb04306216ceae8d0a688 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 20:52:51 +0900 Subject: [PATCH 4703/6909] Use queue instead of list --- osu.Game/Database/UserLookupCache.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 05ba9c882b..71413d076e 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -22,7 +22,7 @@ namespace osu.Game.Database protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) => await queryUser(lookup); - private readonly List<(int id, TaskCompletionSource)> pendingUserTasks = new List<(int, TaskCompletionSource)>(); + private readonly Queue<(int id, TaskCompletionSource)> pendingUserTasks = new Queue<(int, TaskCompletionSource)>(); private Task pendingRequestTask; private readonly object taskAssignmentLock = new object(); @@ -33,7 +33,7 @@ namespace osu.Game.Database var tcs = new TaskCompletionSource(); // Add to the queue. - pendingUserTasks.Add((userId, tcs)); + pendingUserTasks.Enqueue((userId, tcs)); // Create a request task if there's not already one. if (pendingRequestTask == null) @@ -52,9 +52,7 @@ namespace osu.Game.Database { while (pendingUserTasks.Count > 0 && userTasks.Count < 50) { - (int id, TaskCompletionSource task) next = pendingUserTasks[^1]; - - pendingUserTasks.RemoveAt(pendingUserTasks.Count - 1); + (int id, TaskCompletionSource task) next = pendingUserTasks.Dequeue(); // Perform a secondary check for existence, in case the user was queried in a previous batch. if (CheckExists(next.id, out var existing)) From 85b0f714670215fc85eec8215f393674ccfd062a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 21:17:43 +0900 Subject: [PATCH 4704/6909] Handle duplicate user IDs within the same batch --- osu.Game/Database/UserLookupCache.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 71413d076e..ba7f2ad98e 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -45,12 +45,15 @@ namespace osu.Game.Database private void performLookup() { + // userTasks may exceed 50 elements, indicating the existence of duplicate user IDs. All duplicated user IDs must be fulfilled. + // userIds contains at most 50 unique user IDs from userTasks, which is used to perform the lookup. var userTasks = new List<(int id, TaskCompletionSource task)>(); + var userIds = new HashSet(); - // Grab at most 50 users from the queue. + // Grab at most 50 unique user IDs from the queue. lock (taskAssignmentLock) { - while (pendingUserTasks.Count > 0 && userTasks.Count < 50) + while (pendingUserTasks.Count > 0 && userIds.Count < 50) { (int id, TaskCompletionSource task) next = pendingUserTasks.Dequeue(); @@ -58,12 +61,15 @@ namespace osu.Game.Database if (CheckExists(next.id, out var existing)) next.task.SetResult(existing); else + { userTasks.Add(next); + userIds.Add(next.id); + } } } // Query the users. - var request = new GetUsersRequest(userTasks.Select(t => t.id).ToArray()); + var request = new GetUsersRequest(userIds.ToArray()); // rather than queueing, we maintain our own single-threaded request stream. api.Perform(request); From cead67d51209a1f3a1869448d15345b9d78577f5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 21:47:33 +0900 Subject: [PATCH 4705/6909] Add back removed InitialLifetimeOffset removal --- 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 3b7c8bcc2a..ca49ed9e75 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -201,6 +201,8 @@ namespace osu.Game.Rulesets.Objects.Drawables // Copy any existing result from the entry (required for rewind / judgement revert). Result = lifetimeEntry.Result; } + else + LifetimeStart = HitObject.StartTime - InitialLifetimeOffset; // Ensure this DHO has a result. Result ??= CreateResult(HitObject.CreateJudgement()) @@ -646,6 +648,10 @@ namespace osu.Game.Rulesets.Objects.Drawables /// This is only used as an optimisation to delay the initial update of this and may be tuned more aggressively if required. /// It is indirectly used to decide the automatic transform offset provided to . /// A more accurate should be set for further optimisation (in , for example). + /// + /// Only has an effect if this is not being pooled. + /// For pooled s, use instead. + /// /// protected virtual double InitialLifetimeOffset => 10000; From 4cf6aca8735c43c36d1a9ed90660b0600a591942 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 22:40:25 +0900 Subject: [PATCH 4706/6909] Fix slider ball tint not working --- .../TestSceneSliderApplication.cs | 45 +++++++++++++++++++ .../Objects/Drawables/DrawableSlider.cs | 11 ++++- 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index fb1ebbb0d0..084af7dafe 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -1,20 +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.Linq; using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; 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.Skinning; using osu.Game.Tests.Visual; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneSliderApplication : OsuTestScene { + [Resolved] + private SkinManager skinManager { get; set; } + [Test] public void TestApplyNewSlider() { @@ -50,6 +60,41 @@ namespace osu.Game.Rulesets.Osu.Tests }), null)); } + [Test] + public void TestBallTintChangedOnAccentChange() + { + DrawableSlider dho = null; + + AddStep("create slider", () => + { + var tintingSkin = skinManager.GetSkin(DefaultLegacySkin.Info); + tintingSkin.Configuration.ConfigDictionary["AllowSliderBallTint"] = "1"; + + Child = new SkinProvidingContainer(tintingSkin) + { + RelativeSizeAxes = Axes.Both, + Child = dho = new DrawableSlider(prepareObject(new Slider + { + Position = new Vector2(256, 192), + IndexInCurrentCombo = 0, + StartTime = Time.Current, + Path = new SliderPath(PathType.Linear, new[] + { + Vector2.Zero, + new Vector2(150, 100), + new Vector2(300, 0), + }) + })) + }; + }); + + AddStep("set accent white", () => dho.AccentColour.Value = Color4.White); + AddAssert("ball is white", () => dho.ChildrenOfType().Single().AccentColour == Color4.White); + + AddStep("set accent red", () => dho.AccentColour.Value = Color4.Red); + AddAssert("ball is red", () => dho.ChildrenOfType().Single().AccentColour == Color4.Red); + } + private Slider prepareObject(Slider slider) { slider.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 04fc755da5..f7b1894058 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -80,6 +80,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { foreach (var drawableHitObject in NestedHitObjects) drawableHitObject.AccentColour.Value = colour.NewValue; + updateBallTint(); }, true); Tracking.BindValueChanged(updateSlidingSample); @@ -244,7 +245,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.ApplySkin(skin, allowFallback); - bool allowBallTint = skin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; + updateBallTint(); + } + + private void updateBallTint() + { + if (CurrentSkin == null) + return; + + bool allowBallTint = CurrentSkin.GetConfig(OsuSkinConfiguration.AllowSliderBallTint)?.Value ?? false; Ball.AccentColour = allowBallTint ? AccentColour.Value : Color4.White; } From 84b42f207dc8beefd61122dad234674a196b20a0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 23:14:20 +0900 Subject: [PATCH 4707/6909] Fix triangles disappearing after a while --- .../Objects/Drawables/Pieces/TrianglesPiece.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs index 6cdb0d3df3..add62192f0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs @@ -7,8 +7,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class TrianglesPiece : Triangles { - protected override bool ExpireOffScreenTriangles => false; - protected override bool CreateNewTriangles => false; protected override float SpawnRatio => 0.5f; public TrianglesPiece(int? seed = null) From e8dbc190f1ce8584980eb25486c04a786d6e218f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 23:30:24 +0900 Subject: [PATCH 4708/6909] Remove ability to pool DHOs in parent playfields --- osu.Game/Rulesets/UI/Playfield.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 80e33e0ec5..82ec653f31 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -218,9 +218,6 @@ namespace osu.Game.Rulesets.UI #region Pooling support - [Resolved(CanBeNull = true)] - private IPooledHitObjectProvider parentPooledObjectProvider { get; set; } - private readonly Dictionary pools = new Dictionary(); /// @@ -320,10 +317,7 @@ namespace osu.Game.Rulesets.UI } } - if (pool == null) - return parentPooledObjectProvider?.GetPooledDrawableRepresentation(hitObject); - - return (DrawableHitObject)pool.Get(d => + return (DrawableHitObject)pool?.Get(d => { var dho = (DrawableHitObject)d; From f5e12b9d7c0828efdd527706216fbee7efa02375 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 16 Nov 2020 23:53:54 +0900 Subject: [PATCH 4709/6909] Adjust TestScenePlayerLoader for safety --- .../Visual/Gameplay/TestScenePlayerLoader.cs | 127 ++++++++---------- 1 file changed, 53 insertions(+), 74 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs index 9b31dd045a..88fbf09ef4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePlayerLoader.cs @@ -23,17 +23,15 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens; using osu.Game.Screens.Play; using osu.Game.Screens.Play.PlayerSettings; using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { - public class TestScenePlayerLoader : OsuManualInputManagerTestScene + public class TestScenePlayerLoader : ScreenTestScene { private TestPlayerLoader loader; - private TestPlayerLoaderContainer container; private TestPlayer player; private bool epilepsyWarning; @@ -44,21 +42,46 @@ namespace osu.Game.Tests.Visual.Gameplay [Resolved] private SessionStatics sessionStatics { get; set; } + [Cached] + private readonly NotificationOverlay notificationOverlay; + + [Cached] + private readonly VolumeOverlay volumeOverlay; + + private readonly ChangelogOverlay changelogOverlay; + + public TestScenePlayerLoader() + { + AddRange(new Drawable[] + { + notificationOverlay = new NotificationOverlay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }, + volumeOverlay = new VolumeOverlay + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, + changelogOverlay = new ChangelogOverlay() + }); + } + + [SetUp] + public void Setup() => Schedule(() => + { + player = null; + audioManager.Volume.SetDefault(); + }); + /// /// Sets the input manager child to a new test player loader container instance. /// /// If the test player should behave like the production one. /// An action to run before player load but after bindable leases are returned. - public void ResetPlayer(bool interactive, Action beforeLoadAction = null) + private void resetPlayer(bool interactive, Action beforeLoadAction = null) { - player = null; - - audioManager.Volume.SetDefault(); - - InputManager.Clear(); - - container = new TestPlayerLoaderContainer(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive))); - beforeLoadAction?.Invoke(); Beatmap.Value = CreateWorkingBeatmap(new OsuRuleset().RulesetInfo); @@ -67,13 +90,13 @@ namespace osu.Game.Tests.Visual.Gameplay foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToTrack(Beatmap.Value.Track); - InputManager.Child = container; + LoadScreen(loader = new TestPlayerLoader(() => player = new TestPlayer(interactive, interactive))); } [Test] public void TestEarlyExitBeforePlayerConstruction() { - AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); + AddStep("load dummy beatmap", () => resetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddStep("exit loader", () => loader.Exit()); AddUntilStep("wait for not current", () => !loader.IsCurrentScreen()); @@ -90,7 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestEarlyExitAfterPlayerConstruction() { - AddStep("load dummy beatmap", () => ResetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); + AddStep("load dummy beatmap", () => resetPlayer(false, () => SelectedMods.Value = new[] { new OsuModNightcore() })); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddAssert("mod rate applied", () => Beatmap.Value.Track.Rate != 1); AddUntilStep("wait for non-null player", () => player != null); @@ -104,7 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBlockLoadViaMouseMovement() { - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); AddUntilStep("wait for load ready", () => @@ -129,20 +152,18 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestBlockLoadViaFocus() { - OsuFocusedOverlayContainer overlay = null; - - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddStep("show focused overlay", () => { container.Add(overlay = new ChangelogOverlay { State = { Value = Visibility.Visible } }); }); - AddUntilStep("overlay visible", () => overlay.IsPresent); + AddStep("show focused overlay", () => changelogOverlay.Show()); + AddUntilStep("overlay visible", () => changelogOverlay.IsPresent); - AddUntilStep("wait for load ready", () => player.LoadState == LoadState.Ready); + AddUntilStep("wait for load ready", () => player?.LoadState == LoadState.Ready); AddRepeatStep("twiddle thumbs", () => { }, 20); AddAssert("loader still active", () => loader.IsCurrentScreen()); - AddStep("hide overlay", () => overlay.Hide()); + AddStep("hide overlay", () => changelogOverlay.Hide()); AddUntilStep("loads after idle", () => !loader.IsCurrentScreen()); } @@ -151,15 +172,9 @@ namespace osu.Game.Tests.Visual.Gameplay { SlowLoadPlayer slowPlayer = null; - AddStep("load dummy beatmap", () => ResetPlayer(false)); - AddUntilStep("wait for current", () => loader.IsCurrentScreen()); - AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); - AddUntilStep("wait for player to be current", () => player.IsCurrentScreen()); AddStep("load slow dummy beatmap", () => { - InputManager.Child = container = new TestPlayerLoaderContainer( - loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false))); - + LoadScreen(loader = new TestPlayerLoader(() => slowPlayer = new SlowLoadPlayer(false, false))); Scheduler.AddDelayed(() => slowPlayer.AllowLoad.Set(), 5000); }); @@ -173,7 +188,7 @@ namespace osu.Game.Tests.Visual.Gameplay TestMod playerMod1 = null; TestMod playerMod2 = null; - AddStep("load player", () => { ResetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); }); + AddStep("load player", () => { resetPlayer(true, () => SelectedMods.Value = new[] { gameMod = new TestMod() }); }); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddStep("mouse in centre", () => InputManager.MoveMouseTo(loader.ScreenSpaceDrawQuad.Centre)); @@ -201,7 +216,7 @@ namespace osu.Game.Tests.Visual.Gameplay { var testMod = new TestMod(); - AddStep("load player", () => ResetPlayer(true)); + AddStep("load player", () => resetPlayer(true)); AddUntilStep("wait for loader to become current", () => loader.IsCurrentScreen()); AddStep("set test mod in loader", () => loader.Mods.Value = new[] { testMod }); @@ -223,7 +238,7 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestMutedNotificationMuteButton() { - addVolumeSteps("mute button", () => container.VolumeOverlay.IsMuted.Value = true, () => !container.VolumeOverlay.IsMuted.Value); + addVolumeSteps("mute button", () => volumeOverlay.IsMuted.Value = true, () => !volumeOverlay.IsMuted.Value); } /// @@ -236,13 +251,13 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("reset notification lock", () => sessionStatics.GetBindable(Static.MutedAudioNotificationShownOnce).Value = false); - AddStep("load player", () => ResetPlayer(false, beforeLoad)); + AddStep("load player", () => resetPlayer(false, beforeLoad)); AddUntilStep("wait for player", () => player?.LoadState == LoadState.Ready); - AddAssert("check for notification", () => container.NotificationOverlay.UnreadCount.Value == 1); + AddAssert("check for notification", () => notificationOverlay.UnreadCount.Value == 1); AddStep("click notification", () => { - var scrollContainer = (OsuScrollContainer)container.NotificationOverlay.Children.Last(); + var scrollContainer = (OsuScrollContainer)notificationOverlay.Children.Last(); var flowContainer = scrollContainer.Children.OfType>().First(); var notification = flowContainer.First(); @@ -260,7 +275,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEpilepsyWarning(bool warning) { AddStep("change epilepsy warning", () => epilepsyWarning = warning); - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); @@ -277,7 +292,7 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestEpilepsyWarningEarlyExit() { AddStep("set epilepsy warning", () => epilepsyWarning = true); - AddStep("load dummy beatmap", () => ResetPlayer(false)); + AddStep("load dummy beatmap", () => resetPlayer(false)); AddUntilStep("wait for current", () => loader.IsCurrentScreen()); @@ -287,42 +302,6 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("sound volume restored", () => Beatmap.Value.Track.AggregateVolume.Value == 1); } - private class TestPlayerLoaderContainer : Container - { - [Cached] - public readonly NotificationOverlay NotificationOverlay; - - [Cached] - public readonly VolumeOverlay VolumeOverlay; - - public TestPlayerLoaderContainer(IScreen screen) - { - RelativeSizeAxes = Axes.Both; - - OsuScreenStack stack; - - InternalChildren = new Drawable[] - { - stack = new OsuScreenStack - { - RelativeSizeAxes = Axes.Both, - }, - NotificationOverlay = new NotificationOverlay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - }, - VolumeOverlay = new VolumeOverlay - { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - } - }; - - stack.Push(screen); - } - } - private class TestPlayerLoader : PlayerLoader { public new VisualSettings VisualSettings => base.VisualSettings; From 8da40ce2dc8904b4d9937f6b179eb77cd988a455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Nov 2020 19:42:08 +0100 Subject: [PATCH 4710/6909] Reduce duplication by extracting ModEasyWithExtraLives --- osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs | 49 +----------------- osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs | 49 +----------------- osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs | 49 +----------------- .../Rulesets/Mods/ModEasyWithExtraLives.cs | 50 +++++++++++++++++++ 4 files changed, 53 insertions(+), 144 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs index 88d20d15e2..16ef56d845 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModEasy.cs @@ -1,59 +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 Humanizer; -using osu.Framework.Bindables; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Catch.Mods { - public class CatchModEasy : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor + public class CatchModEasy : ModEasyWithExtraLives { public override string Description => @"Larger fruits, more forgiving HP drain, less accuracy required, and three lives!"; - - [SettingSource("Extra Lives", "Number of extra lives")] - public Bindable Retries { get; } = new BindableInt(2) - { - MinValue = 0, - MaxValue = 10 - }; - public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; - - private int retries; - - private BindableNumber health; - - public override void ReadFromDifficulty(BeatmapDifficulty difficulty) - { - } - - public override void ApplyToDifficulty(BeatmapDifficulty difficulty) - { - const float ratio = 0.5f; - difficulty.CircleSize *= ratio; - difficulty.ApproachRate *= ratio; - difficulty.DrainRate *= ratio; - difficulty.OverallDifficulty *= ratio; - - retries = Retries.Value; - } - public bool PerformFail() - { - if (retries == 0) return true; - - health.Value = health.MaxValue; - retries--; - - return false; - } - public bool RestartOnFail => false; - - public void ApplyToHealthProcessor(HealthProcessor healthProcessor) - { - health = healthProcessor.Health.GetBoundCopy(); - } } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs index 79073b26ba..4093aeb2a7 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModEasy.cs @@ -1,59 +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 Humanizer; -using osu.Framework.Bindables; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModEasy : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor + public class ManiaModEasy : ModEasyWithExtraLives { public override string Description => @"More forgiving HP drain, less accuracy required, and three lives!"; - - [SettingSource("Extra Lives", "Number of extra lives")] - public Bindable Retries { get; } = new BindableInt(2) - { - MinValue = 0, - MaxValue = 10 - }; - public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; - - private int retries; - - private BindableNumber health; - - public override void ReadFromDifficulty(BeatmapDifficulty difficulty) - { - } - - public override void ApplyToDifficulty(BeatmapDifficulty difficulty) - { - const float ratio = 0.5f; - difficulty.CircleSize *= ratio; - difficulty.ApproachRate *= ratio; - difficulty.DrainRate *= ratio; - difficulty.OverallDifficulty *= ratio; - - retries = Retries.Value; - } - public bool PerformFail() - { - if (retries == 0) return true; - - health.Value = health.MaxValue; - retries--; - - return false; - } - public bool RestartOnFail => false; - - public void ApplyToHealthProcessor(HealthProcessor healthProcessor) - { - health = healthProcessor.Health.GetBoundCopy(); - } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs index 43ceddd6e1..06b5b6cfb8 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModEasy.cs @@ -1,59 +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 Humanizer; -using osu.Framework.Bindables; -using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModEasy : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor + public class OsuModEasy : ModEasyWithExtraLives { public override string Description => @"Larger circles, more forgiving HP drain, less accuracy required, and three lives!"; - - [SettingSource("Extra Lives", "Number of extra lives")] - public Bindable Retries { get; } = new BindableInt(2) - { - MinValue = 0, - MaxValue = 10 - }; - public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; - - private int retries; - - private BindableNumber health; - - public override void ReadFromDifficulty(BeatmapDifficulty difficulty) - { - } - - public override void ApplyToDifficulty(BeatmapDifficulty difficulty) - { - const float ratio = 0.5f; - difficulty.CircleSize *= ratio; - difficulty.ApproachRate *= ratio; - difficulty.DrainRate *= ratio; - difficulty.OverallDifficulty *= ratio; - - retries = Retries.Value; - } - public bool PerformFail() - { - if (retries == 0) return true; - - health.Value = health.MaxValue; - retries--; - - return false; - } - public bool RestartOnFail => false; - - public void ApplyToHealthProcessor(HealthProcessor healthProcessor) - { - health = healthProcessor.Health.GetBoundCopy(); - } } } diff --git a/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.cs new file mode 100644 index 0000000000..2ac0f59d84 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModEasyWithExtraLives.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 Humanizer; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModEasyWithExtraLives : ModEasy, IApplicableFailOverride, IApplicableToHealthProcessor + { + [SettingSource("Extra Lives", "Number of extra lives")] + public Bindable Retries { get; } = new BindableInt(2) + { + MinValue = 0, + MaxValue = 10 + }; + + public override string SettingDescription => Retries.IsDefault ? string.Empty : $"{"lives".ToQuantity(Retries.Value)}"; + + private int retries; + + private BindableNumber health; + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + base.ApplyToDifficulty(difficulty); + retries = Retries.Value; + } + + public bool PerformFail() + { + if (retries == 0) return true; + + health.Value = health.MaxValue; + retries--; + + return false; + } + + public bool RestartOnFail => false; + + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) + { + health = healthProcessor.Health.GetBoundCopy(); + } + } +} From e88920442c7f3a2ce09b17d346737018c54bbb8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Nov 2020 20:01:10 +0100 Subject: [PATCH 4711/6909] Use HitStateUpdateTime instead --- osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index f1f72c8618..328c4e547b 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -120,7 +120,7 @@ namespace osu.Game.Rulesets.Mania.Skinning private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) { if (state == ArmedState.Miss) - missFadeTime.Value ??= hitObject.StateUpdateTime; + missFadeTime.Value ??= hitObject.HitStateUpdateTime; } private void onIsHittingChanged(ValueChangedEvent isHitting) From 80d81c30440594e4aa92d62812504086f82a95da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Nov 2020 20:24:04 +0100 Subject: [PATCH 4712/6909] Reword taiko easy mod description to fit others better Co-authored-by: Joseph Madamba --- osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index 0ec24412e9..d1ad4c9d8d 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -7,6 +7,6 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModEasy : ModEasy { - public override string Description => @"Beats move slower, less accuracy required!"; + public override string Description => @"Beats move slower, and less accuracy required!"; } } From 21f29e28e2a11f33b702c3dfbe92d5da32a92b36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 16 Nov 2020 20:36:56 +0100 Subject: [PATCH 4713/6909] Add clarification comment --- osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs index 328c4e547b..8902d82f33 100644 --- a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs @@ -119,6 +119,7 @@ namespace osu.Game.Rulesets.Mania.Skinning private void applyCustomUpdateState(DrawableHitObject hitObject, ArmedState state) { + // ensure that the hold note is also faded out when the head/tail/any tick is missed. if (state == ArmedState.Miss) missFadeTime.Value ??= hitObject.HitStateUpdateTime; } From c6618f08aad3b47cc69ef73153966a5f384b580c Mon Sep 17 00:00:00 2001 From: kamp Date: Mon, 16 Nov 2020 21:26:08 +0100 Subject: [PATCH 4714/6909] Fix slider control point connections not being updated --- .../PathControlPointConnectionPiece.cs | 12 +++++++++- .../Components/PathControlPointVisualiser.cs | 24 +++++++++++++++++++ 2 files changed, 35 insertions(+), 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 ba1d35c35c..45c4a61ce1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -20,7 +20,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private readonly Path path; private readonly Slider slider; - private readonly int controlPointIndex; + private int controlPointIndex; + + public int ControlPointIndex + { + get => controlPointIndex; + set + { + controlPointIndex = value; + updateConnectingPath(); + } + } private IBindable sliderPosition; private IBindable pathVersion; 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 17541866ec..14ce0e065b 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -66,6 +66,17 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components switch (e.Action) { case NotifyCollectionChangedAction.Add: + // If inserting in the the path (not appending), + // update indices of existing connections after insert location + if (e.NewStartingIndex < Pieces.Count) + { + foreach (var connection in Connections) + { + if (connection.ControlPointIndex >= e.NewStartingIndex) + connection.ControlPointIndex += e.NewItems.Count; + } + } + for (int i = 0; i < e.NewItems.Count; i++) { var point = (PathControlPoint)e.NewItems[i]; @@ -82,12 +93,25 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components break; case NotifyCollectionChangedAction.Remove: + int oldSize = Pieces.Count; + foreach (var point in e.OldItems.Cast()) { Pieces.RemoveAll(p => p.ControlPoint == point); Connections.RemoveAll(c => c.ControlPoint == point); } + // If removing before the end of the path, + // update indices of connections after remove location + if (e.OldStartingIndex + e.OldItems.Count < oldSize) + { + foreach (var connection in Connections) + { + if (connection.ControlPointIndex >= e.OldStartingIndex) + connection.ControlPointIndex -= e.OldItems.Count; + } + } + break; } } From 009d666241fb7adfd1c1f11d7603478dbd9166b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Nov 2020 10:57:11 +0900 Subject: [PATCH 4715/6909] Use dictionary to avoid linq overhead --- osu.Game/Database/UserLookupCache.cs | 36 +++++++++++++++++++--------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index ba7f2ad98e..05d6930992 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -45,15 +45,13 @@ namespace osu.Game.Database private void performLookup() { - // userTasks may exceed 50 elements, indicating the existence of duplicate user IDs. All duplicated user IDs must be fulfilled. - // userIds contains at most 50 unique user IDs from userTasks, which is used to perform the lookup. - var userTasks = new List<(int id, TaskCompletionSource task)>(); - var userIds = new HashSet(); + // contains at most 50 unique user IDs from userTasks, which is used to perform the lookup. + var userTasks = new Dictionary>>(); // Grab at most 50 unique user IDs from the queue. lock (taskAssignmentLock) { - while (pendingUserTasks.Count > 0 && userIds.Count < 50) + while (pendingUserTasks.Count > 0 && userTasks.Count < 50) { (int id, TaskCompletionSource task) next = pendingUserTasks.Dequeue(); @@ -62,14 +60,16 @@ namespace osu.Game.Database next.task.SetResult(existing); else { - userTasks.Add(next); - userIds.Add(next.id); + if (userTasks.TryGetValue(next.id, out var tasks)) + tasks.Add(next.task); + else + userTasks[next.id] = new List> { next.task }; } } } // Query the users. - var request = new GetUsersRequest(userIds.ToArray()); + var request = new GetUsersRequest(userTasks.Keys.ToArray()); // rather than queueing, we maintain our own single-threaded request stream. api.Perform(request); @@ -82,9 +82,23 @@ namespace osu.Game.Database createNewTask(); } - // Notify of completion. - foreach (var (id, task) in userTasks) - task.SetResult(request.Result?.Users?.FirstOrDefault(u => u.Id == id)); + foreach (var user in request.Result.Users) + { + if (userTasks.TryGetValue(user.Id, out var tasks)) + { + foreach (var task in tasks) + task.SetResult(user); + + userTasks.Remove(user.Id); + } + } + + // if any tasks remain which were not satisfied, return null. + foreach (var tasks in userTasks.Values) + { + foreach (var task in tasks) + task.SetResult(null); + } } private void createNewTask() => pendingRequestTask = Task.Run(performLookup); From 33c643e36907d0c5830b1a13118c58d0b816ff11 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 12:23:34 +0900 Subject: [PATCH 4716/6909] Add obsoletion for unused property --- osu.Game/Graphics/Backgrounds/Triangles.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index 5b0fa44444..a81dbbb64a 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -60,6 +60,7 @@ namespace osu.Game.Graphics.Backgrounds /// /// Whether we want to expire triangles as they exit our draw area completely. /// + [Obsolete("Unused.")] // Can be removed 20210518 protected virtual bool ExpireOffScreenTriangles => true; /// From 3bcf9c255a483a9936671b5c4acadc0e7b475d5a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 13:06:30 +0900 Subject: [PATCH 4717/6909] Add Triangles.Reset() --- osu.Game/Graphics/Backgrounds/Triangles.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/Backgrounds/Triangles.cs b/osu.Game/Graphics/Backgrounds/Triangles.cs index a81dbbb64a..0e9382279a 100644 --- a/osu.Game/Graphics/Backgrounds/Triangles.cs +++ b/osu.Game/Graphics/Backgrounds/Triangles.cs @@ -87,12 +87,9 @@ namespace osu.Game.Graphics.Backgrounds /// public float Velocity = 1; - private readonly Random stableRandom; - - private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle()); - private readonly SortedList parts = new SortedList(Comparer.Default); + private Random stableRandom; private IShader shader; private readonly Texture texture; @@ -173,7 +170,20 @@ namespace osu.Game.Graphics.Backgrounds } } - protected int AimCount; + /// + /// Clears and re-initialises triangles according to a given seed. + /// + /// An optional seed to stabilise random positions / attributes. Note that this does not guarantee stable playback when seeking in time. + public void Reset(int? seed = null) + { + if (seed != null) + stableRandom = new Random(seed.Value); + + parts.Clear(); + addTriangles(true); + } + + protected int AimCount { get; private set; } private void addTriangles(bool randomY) { @@ -227,6 +237,8 @@ namespace osu.Game.Graphics.Backgrounds } } + private float nextRandom() => (float)(stableRandom?.NextDouble() ?? RNG.NextSingle()); + protected override DrawNode CreateDrawNode() => new TrianglesDrawNode(this); private class TrianglesDrawNode : DrawNode From c101f32db8a796f08f21d317ffb5aef8310f8d14 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 13:06:52 +0900 Subject: [PATCH 4718/6909] Reset osu! triangle pieces on hitobject application --- .../Objects/Drawables/Pieces/CirclePiece.cs | 28 ++++++++++++++-- .../Objects/Drawables/Pieces/ExplodePiece.cs | 32 ++++++++++++++++++- .../Drawables/Pieces/TrianglesPiece.cs | 1 + 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs index c455c66e8d..d0e1055dce 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs @@ -13,6 +13,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class CirclePiece : CompositeDrawable { + [Resolved] + private DrawableHitObject drawableObject { get; set; } + + private TrianglesPiece triangles; + public CirclePiece() { Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); @@ -26,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } [BackgroundDependencyLoader] - private void load(TextureStore textures, DrawableHitObject drawableHitObject) + private void load(TextureStore textures) { InternalChildren = new Drawable[] { @@ -36,13 +41,32 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Origin = Anchor.Centre, Texture = textures.Get(@"Gameplay/osu/disc"), }, - new TrianglesPiece(drawableHitObject.GetHashCode()) + triangles = new TrianglesPiece { RelativeSizeAxes = Axes.Both, Blending = BlendingParameters.Additive, Alpha = 0.5f, } }; + + drawableObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(drawableObject); + } + + private void onHitObjectApplied(DrawableHitObject obj) + { + if (obj.HitObject == null) + return; + + triangles.Reset((int)obj.HitObject.StartTime); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableObject != null) + drawableObject.HitObjectApplied -= onHitObjectApplied; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs index 6381ddca69..09299a3622 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class ExplodePiece : Container { + [Resolved] + private DrawableHitObject drawableObject { get; set; } + + private TrianglesPiece triangles; + public ExplodePiece() { Size = new Vector2(OsuHitObject.OBJECT_RADIUS * 2); @@ -18,13 +25,36 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces Blending = BlendingParameters.Additive; Alpha = 0; + } - Child = new TrianglesPiece + [BackgroundDependencyLoader] + private void load() + { + Child = triangles = new TrianglesPiece { Blending = BlendingParameters.Additive, RelativeSizeAxes = Axes.Both, Alpha = 0.2f, }; + + drawableObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(drawableObject); + } + + private void onHitObjectApplied(DrawableHitObject obj) + { + if (obj.HitObject == null) + return; + + triangles.Reset((int)obj.HitObject.StartTime); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableObject != null) + drawableObject.HitObjectApplied -= onHitObjectApplied; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs index add62192f0..53dc7ecea3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs @@ -7,6 +7,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { public class TrianglesPiece : Triangles { + protected override bool CreateNewTriangles => false; protected override float SpawnRatio => 0.5f; public TrianglesPiece(int? seed = null) From 77942af3a653aa3f8672d44f5370e56236c03df6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 13:37:58 +0900 Subject: [PATCH 4719/6909] Fix hold note judgements displaying incorrectly --- .../Objects/Drawables/DrawableHoldNoteTick.cs | 2 -- osu.Game.Rulesets.Mania/UI/Column.cs | 2 +- osu.Game.Rulesets.Mania/UI/Stage.cs | 4 ++++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs index f265419aa0..98931dceed 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTick.cs @@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables /// public class DrawableHoldNoteTick : DrawableManiaHitObject { - public override bool DisplayResult => false; - /// /// References the time at which the user started holding the hold note. /// diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index c28a1c13d8..9aabcc6699 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -114,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.UI if (result.IsHit) hitPolicy.HandleHit(judgedObject); - if (!result.IsHit || !DisplayJudgements.Value) + if (!result.IsHit || !judgedObject.DisplayResult || !DisplayJudgements.Value) return; HitObjectArea.Explosions.Add(hitExplosionPool.Get(e => e.Apply(result))); diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index e7a2de266d..3d7960ffe3 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -167,6 +167,10 @@ namespace osu.Game.Rulesets.Mania.UI if (!judgedObject.DisplayResult || !DisplayJudgements.Value) return; + // Tick judgements should not display text. + if (judgedObject is DrawableHoldNoteTick) + return; + judgements.Clear(false); judgements.Add(judgementPool.Get(j => { From 9dfa4249e059f68a54677625fb05227a401de745 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Nov 2020 14:05:13 +0900 Subject: [PATCH 4720/6909] Make Apply non-virtual --- .../Objects/Drawables/DrawableOsuJudgement.cs | 17 ++++++----------- .../Rulesets/Judgements/DrawableJudgement.cs | 2 +- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 98898ce1b4..5bf5f89b26 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -39,23 +39,18 @@ 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(); Lighting.ResetAnimation(); Lighting.SetColourFrom(JudgedObject, Result); + + if (JudgedObject?.HitObject is OsuHitObject osuObject) + { + Position = osuObject.StackedPosition; + Scale = new Vector2(osuObject.Scale); + } } private double fadeOutDelay; diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index d24c81536e..5c617aaa98 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Judgements this.Delay(FadeOutDelay).FadeOut(400); } - public virtual void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) + public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) { Result = result; JudgedObject = judgedObject; From 82c3925a3795345d74d95f48fa4ebd2dd6e23f4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Nov 2020 14:13:50 +0900 Subject: [PATCH 4721/6909] Remove unused DrawableOsuJudgement constructors --- .../Objects/Drawables/DrawableOsuJudgement.cs | 9 --------- 1 file changed, 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 5bf5f89b26..d89a613e0f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -17,15 +17,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables [Resolved] private OsuConfigManager config { get; set; } - public DrawableOsuJudgement(JudgementResult result, DrawableHitObject judgedObject) - : base(result, judgedObject) - { - } - - public DrawableOsuJudgement() - { - } - [BackgroundDependencyLoader] private void load() { From f465dd5a5e6f840169c15129ce9698441c0bd82b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Nov 2020 14:59:34 +0900 Subject: [PATCH 4722/6909] Move all extended animation logic out of DrawableJudgement --- .../UI/DrawableManiaJudgement.cs | 21 +++--- .../Objects/Drawables/DrawableOsuJudgement.cs | 18 ++++- .../Judgements/DefaultJudgementPiece.cs | 65 +++++++++++++++++ .../Rulesets/Judgements/DrawableJudgement.cs | 69 ++++++++++--------- .../Judgements/IAnimatableJudgement.cs | 15 ++++ 5 files changed, 146 insertions(+), 42 deletions(-) create mode 100644 osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs create mode 100644 osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index d99f6cb8d3..c53ab4a717 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -1,10 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mania.UI { @@ -19,13 +19,6 @@ namespace osu.Game.Rulesets.Mania.UI { } - [BackgroundDependencyLoader] - private void load() - { - if (JudgementText != null) - JudgementText.Font = JudgementText.Font.With(size: 25); - } - protected override double FadeInDuration => 50; protected override void ApplyHitAnimations() @@ -36,5 +29,17 @@ namespace osu.Game.Rulesets.Mania.UI JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250); this.Delay(FadeInDuration).FadeOut(200); } + + protected override Drawable CreateDefaultJudgement(HitResult type) + => new ManiaJudgementPiece(); + + private class ManiaJudgementPiece : DefaultJudgementPiece + { + protected override void LoadComplete() + { + base.LoadComplete(); + JudgementText.Font = JudgementText.Font.With(size: 25); + } + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index d89a613e0f..a96ec53e28 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -4,9 +4,9 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; -using osuTK; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -65,8 +65,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay; - JudgementText?.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); base.ApplyHitAnimations(); } + + protected override Drawable CreateDefaultJudgement(HitResult type) => new OsuJudgementPiece(); + + private class OsuJudgementPiece : DefaultJudgementPiece + { + public override void PlayAnimation(HitResult resultType) + { + base.PlayAnimation(resultType); + + if (resultType != HitResult.Miss) + JudgementText.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); + } + } } } diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs new file mode 100644 index 0000000000..051cd755d6 --- /dev/null +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Scoring; +using osuTK; + +namespace osu.Game.Rulesets.Judgements +{ + public class DefaultJudgementPiece : CompositeDrawable, IAnimatableJudgement + { + protected SpriteText JudgementText { get; } + + [Resolved] + private OsuColour colours { get; set; } + + public DefaultJudgementPiece() + { + Origin = Anchor.Centre; + + AutoSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + JudgementText = new OsuSpriteText + { + Font = OsuFont.Numeric.With(size: 20), + Scale = new Vector2(0.85f, 1), + } + }; + } + + public virtual void PlayAnimation(HitResult result) + { + JudgementText.Text = result.GetDescription().ToUpperInvariant(); + JudgementText.Colour = colours.ForHitResult(result); + + this.RotateTo(0); + this.MoveTo(Vector2.Zero); + + switch (result) + { + case HitResult.Miss: + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(40, 800, Easing.InQuint); + break; + + default: + this.ScaleTo(0.9f); + this.ScaleTo(1, 500, Easing.OutElastic); + break; + } + } + } +} diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 5c617aaa98..a73b422ccf 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -3,18 +3,14 @@ using System.Diagnostics; using JetBrains.Annotations; -using osuTK; using osu.Framework.Allocation; -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; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; +using osuTK; namespace osu.Game.Rulesets.Judgements { @@ -25,16 +21,13 @@ namespace osu.Game.Rulesets.Judgements { private const float judgement_size = 128; - [Resolved] - private OsuColour colours { get; set; } - public JudgementResult Result { get; private set; } + public DrawableHitObject JudgedObject { get; private set; } protected Container JudgementBody { get; private set; } - protected SpriteText JudgementText { get; private set; } - private SkinnableDrawable bodyDrawable; + private SkinnableDrawable skinnableJudgement; /// /// Duration of initial fade in. @@ -69,14 +62,34 @@ namespace osu.Game.Rulesets.Judgements prepareDrawables(); } + /// + /// Apply top-level animations to the current judgement when successfully hit. + /// Generally used for fading, defaulting to a simple fade out based on . + /// This will be used to calculate the lifetime of the judgement. + /// + /// + /// For animating the actual "default skin" judgement itself, it is recommended to use . + /// This allows applying animations which don't affect custom skins. + /// protected virtual void ApplyHitAnimations() { - JudgementBody.ScaleTo(0.9f); - JudgementBody.ScaleTo(1, 500, Easing.OutElastic); - this.Delay(FadeOutDelay).FadeOut(400); } + /// + /// Apply top-level animations to the current judgement when missed. + /// Generally used for fading, defaulting to a simple fade out based on . + /// This will be used to calculate the lifetime of the judgement. + /// + /// + /// For animating the actual "default skin" judgement itself, it is recommended to use . + /// This allows applying animations which don't affect custom skins. + /// + protected virtual void ApplyMissAnimations() + { + this.Delay(600).FadeOut(200); + } + public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) { Result = result; @@ -91,12 +104,9 @@ namespace osu.Game.Rulesets.Judgements prepareDrawables(); - bodyDrawable.ResetAnimation(); + skinnableJudgement.ResetAnimation(); this.FadeInFromZero(FadeInDuration, Easing.OutQuint); - JudgementBody.ScaleTo(1); - JudgementBody.RotateTo(0); - JudgementBody.MoveTo(Vector2.Zero); switch (Result.Type) { @@ -104,13 +114,7 @@ namespace osu.Game.Rulesets.Judgements break; case HitResult.Miss: - JudgementBody.ScaleTo(1.6f); - JudgementBody.ScaleTo(1, 100, Easing.In); - - JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - JudgementBody.RotateTo(40, 800, Easing.InQuint); - - this.Delay(600).FadeOut(200); + ApplyMissAnimations(); break; default: @@ -118,6 +122,12 @@ namespace osu.Game.Rulesets.Judgements break; } + if (skinnableJudgement.Drawable is IAnimatableJudgement animatable) + { + using (BeginAbsoluteSequence(Result.TimeAbsolute)) + animatable.PlayAnimation(Result.Type); + } + Expire(true); } @@ -139,16 +149,13 @@ namespace osu.Game.Rulesets.Judgements Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Child = bodyDrawable = 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) + Child = skinnableJudgement = new SkinnableDrawable(new GameplaySkinComponent(type), _ => + CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) }); currentDrawableType = type; } + + protected virtual Drawable CreateDefaultJudgement(HitResult type) => new DefaultJudgementPiece(); } } diff --git a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs new file mode 100644 index 0000000000..3f84e6f83c --- /dev/null +++ b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Judgements +{ + /// + /// A skinnable judgement element which supports playing an animation from the current point in time. + /// + public interface IAnimatableJudgement + { + void PlayAnimation(HitResult result); + } +} From e4f1e52422931ec11939a497b1db2e425be1ebce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Nov 2020 15:03:26 +0900 Subject: [PATCH 4723/6909] Add xmldoc coverage of Apply() --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index a73b422ccf..724b4a4d4e 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -90,6 +90,11 @@ namespace osu.Game.Rulesets.Judgements this.Delay(600).FadeOut(200); } + /// + /// Associate a new result / object with this judgement. Should be called when retrieving a judgement from a pool. + /// + /// The applicable judgement. + /// The drawable object. public void Apply([NotNull] JudgementResult result, [CanBeNull] DrawableHitObject judgedObject) { Result = result; @@ -104,6 +109,7 @@ namespace osu.Game.Rulesets.Judgements prepareDrawables(); + // not sure if this should remain going forward. skinnableJudgement.ResetAnimation(); this.FadeInFromZero(FadeInDuration, Easing.OutQuint); From 8247e6ce917053c1e881ef6ce27ce2fcdf3fc456 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Nov 2020 15:43:54 +0900 Subject: [PATCH 4724/6909] Move result type to ctor --- .../UI/DrawableManiaJudgement.cs | 9 +++++++-- .../Objects/Drawables/DrawableOsuJudgement.cs | 13 ++++++++---- .../Judgements/DefaultJudgementPiece.cs | 20 ++++++++++++------- .../Rulesets/Judgements/DrawableJudgement.cs | 5 +++-- .../Judgements/IAnimatableJudgement.cs | 4 +--- 5 files changed, 33 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index c53ab4a717..ebce40a785 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -30,14 +30,19 @@ namespace osu.Game.Rulesets.Mania.UI this.Delay(FadeInDuration).FadeOut(200); } - protected override Drawable CreateDefaultJudgement(HitResult type) - => new ManiaJudgementPiece(); + protected override Drawable CreateDefaultJudgement(HitResult result) => new ManiaJudgementPiece(result); private class ManiaJudgementPiece : DefaultJudgementPiece { + public ManiaJudgementPiece(HitResult result) + : base(result) + { + } + protected override void LoadComplete() { base.LoadComplete(); + JudgementText.Font = JudgementText.Font.With(size: 25); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index a96ec53e28..47fb53379f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -68,15 +68,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.ApplyHitAnimations(); } - protected override Drawable CreateDefaultJudgement(HitResult type) => new OsuJudgementPiece(); + protected override Drawable CreateDefaultJudgement(HitResult result) => new OsuJudgementPiece(result); private class OsuJudgementPiece : DefaultJudgementPiece { - public override void PlayAnimation(HitResult resultType) + public OsuJudgementPiece(HitResult result) + : base(result) { - base.PlayAnimation(resultType); + } - if (resultType != HitResult.Miss) + public override void PlayAnimation() + { + base.PlayAnimation(); + + if (Result != HitResult.Miss) JudgementText.TransformSpacingTo(Vector2.Zero).Then().TransformSpacingTo(new Vector2(14, 0), 1800, Easing.OutQuint); } } diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index 051cd755d6..3b9e5e948a 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -15,36 +15,42 @@ namespace osu.Game.Rulesets.Judgements { public class DefaultJudgementPiece : CompositeDrawable, IAnimatableJudgement { - protected SpriteText JudgementText { get; } + protected readonly HitResult Result; + + protected SpriteText JudgementText { get; private set; } [Resolved] private OsuColour colours { get; set; } - public DefaultJudgementPiece() + public DefaultJudgementPiece(HitResult result) { + this.Result = result; Origin = Anchor.Centre; + } + [BackgroundDependencyLoader] + private void load() + { AutoSizeAxes = Axes.Both; InternalChildren = new Drawable[] { JudgementText = new OsuSpriteText { + Text = Result.GetDescription().ToUpperInvariant(), + Colour = colours.ForHitResult(Result), Font = OsuFont.Numeric.With(size: 20), Scale = new Vector2(0.85f, 1), } }; } - public virtual void PlayAnimation(HitResult result) + public virtual void PlayAnimation() { - JudgementText.Text = result.GetDescription().ToUpperInvariant(); - JudgementText.Colour = colours.ForHitResult(result); - this.RotateTo(0); this.MoveTo(Vector2.Zero); - switch (result) + switch (Result) { case HitResult.Miss: this.ScaleTo(1.6f); diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 724b4a4d4e..468b3190b0 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Judgements if (skinnableJudgement.Drawable is IAnimatableJudgement animatable) { using (BeginAbsoluteSequence(Result.TimeAbsolute)) - animatable.PlayAnimation(Result.Type); + animatable.PlayAnimation(); } Expire(true); @@ -143,6 +143,7 @@ namespace osu.Game.Rulesets.Judgements { var type = Result?.Type ?? HitResult.Perfect; //TODO: better default type from ruleset + // todo: this should be removed once judgements are always pooled. if (type == currentDrawableType) return; @@ -162,6 +163,6 @@ namespace osu.Game.Rulesets.Judgements currentDrawableType = type; } - protected virtual Drawable CreateDefaultJudgement(HitResult type) => new DefaultJudgementPiece(); + protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result); } } diff --git a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs index 3f84e6f83c..3d5bbe6dad 100644 --- a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Scoring; - namespace osu.Game.Rulesets.Judgements { /// @@ -10,6 +8,6 @@ namespace osu.Game.Rulesets.Judgements /// public interface IAnimatableJudgement { - void PlayAnimation(HitResult result); + void PlayAnimation(); } } From dd4b69feab226c09c7334f407431add26828832f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Nov 2020 15:44:15 +0900 Subject: [PATCH 4725/6909] Add legacy judgement implementation which doesn't transform on animations --- osu.Game/Skinning/LegacyJudgementPiece.cs | 58 +++++++++++++++++++++++ osu.Game/Skinning/LegacySkin.cs | 37 +++++++++------ 2 files changed, 81 insertions(+), 14 deletions(-) create mode 100644 osu.Game/Skinning/LegacyJudgementPiece.cs diff --git a/osu.Game/Skinning/LegacyJudgementPiece.cs b/osu.Game/Skinning/LegacyJudgementPiece.cs new file mode 100644 index 0000000000..6c606c638d --- /dev/null +++ b/osu.Game/Skinning/LegacyJudgementPiece.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 osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osuTK; + +namespace osu.Game.Skinning +{ + public class LegacyJudgementPiece : CompositeDrawable, IAnimatableJudgement + { + private readonly HitResult result; + + public LegacyJudgementPiece(HitResult result, Drawable texture) + { + this.result = result; + + AutoSizeAxes = Axes.Both; + Origin = Anchor.Centre; + + InternalChild = texture; + } + + public virtual void PlayAnimation() + { + var animation = InternalChild as IFramedAnimation; + + animation?.GotoFrame(0); + + this.RotateTo(0); + this.MoveTo(Vector2.Zero); + + // legacy judgements don't play any transforms if they are an animation. + if (animation?.FrameCount > 1) + return; + + switch (result) + { + case HitResult.Miss: + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); + + this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + this.RotateTo(40, 800, Easing.InQuint); + break; + + default: + this.ScaleTo(0.9f); + this.ScaleTo(1, 500, Easing.OutElastic); + break; + } + } + } +} diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index fb020f4e39..ca8bb58023 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -371,20 +371,9 @@ namespace osu.Game.Skinning } case GameplaySkinComponent resultComponent: - switch (resultComponent.Component) - { - case HitResult.Miss: - return this.GetAnimation("hit0", true, false); - - case HitResult.Meh: - return this.GetAnimation("hit50", true, false); - - case HitResult.Ok: - return this.GetAnimation("hit100", true, false); - - case HitResult.Great: - return this.GetAnimation("hit300", true, false); - } + var drawable = getJudgementAnimation(resultComponent.Component); + if (drawable != null) + return new LegacyJudgementPiece(resultComponent.Component, drawable); break; } @@ -392,6 +381,26 @@ namespace osu.Game.Skinning return this.GetAnimation(component.LookupName, false, false); } + private Drawable getJudgementAnimation(HitResult result) + { + switch (result) + { + case HitResult.Miss: + return this.GetAnimation("hit0", true, false); + + case HitResult.Meh: + return this.GetAnimation("hit50", true, false); + + case HitResult.Ok: + return this.GetAnimation("hit100", true, false); + + case HitResult.Great: + return this.GetAnimation("hit300", true, false); + } + + return null; + } + public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) { foreach (var name in getFallbackNames(componentName)) From eebce1f9145813b525400517e170d3931a1f8de9 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 17 Nov 2020 18:13:32 +0900 Subject: [PATCH 4726/6909] Fix TestSceneFruitObjects --- .../TestSceneFruitObjects.cs | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 385d8ed7fa..e9dabd30ed 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.Catch.Tests @@ -36,17 +37,29 @@ namespace osu.Game.Rulesets.Catch.Tests Scale = 1.5f, }; - return new DrawableTinyDroplet(droplet) + return new TestDrawableTinyDroplet(droplet) { Anchor = Anchor.Centre, RelativePositionAxes = Axes.None, Position = Vector2.Zero, - Alpha = 1, - LifetimeStart = double.NegativeInfinity, - LifetimeEnd = double.PositiveInfinity, }; } + private class TestDrawableTinyDroplet : DrawableTinyDroplet + { + public TestDrawableTinyDroplet(TinyDroplet tinyDroplet) + : base(tinyDroplet) + { + } + + protected override void OnApply(HitObject hitObject) + { + base.OnApply(hitObject); + LifetimeStart = double.NegativeInfinity; + LifetimeEnd = double.PositiveInfinity; + } + } + private Drawable createDrawableDroplet(bool hyperdash = false) { var droplet = new TestCatchDroplet @@ -55,17 +68,29 @@ namespace osu.Game.Rulesets.Catch.Tests HyperDashTarget = hyperdash ? new Banana() : null }; - return new DrawableDroplet(droplet) + return new TestDrawableDroplet(droplet) { Anchor = Anchor.Centre, RelativePositionAxes = Axes.None, Position = Vector2.Zero, - Alpha = 1, - LifetimeStart = double.NegativeInfinity, - LifetimeEnd = double.PositiveInfinity, }; } + private class TestDrawableDroplet : DrawableDroplet + { + public TestDrawableDroplet(Droplet droplet) + : base(droplet) + { + } + + protected override void OnApply(HitObject hitObject) + { + base.OnApply(hitObject); + LifetimeStart = double.NegativeInfinity; + LifetimeEnd = double.PositiveInfinity; + } + } + private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false) { Fruit fruit = new TestCatchFruit(rep) @@ -74,17 +99,29 @@ namespace osu.Game.Rulesets.Catch.Tests HyperDashTarget = hyperdash ? new Banana() : null }; - return new DrawableFruit(fruit) + return new TestDrawableFruit(fruit) { Anchor = Anchor.Centre, RelativePositionAxes = Axes.None, Position = Vector2.Zero, - Alpha = 1, - LifetimeStart = double.NegativeInfinity, - LifetimeEnd = double.PositiveInfinity, }; } + private class TestDrawableFruit : DrawableFruit + { + public TestDrawableFruit(Fruit fruit) + : base(fruit) + { + } + + protected override void OnApply(HitObject hitObject) + { + base.OnApply(hitObject); + LifetimeStart = double.NegativeInfinity; + LifetimeEnd = double.PositiveInfinity; + } + } + public class TestCatchFruit : Fruit { public TestCatchFruit(FruitVisualRepresentation rep) From 809338a28002c1bed4223b890560dd50662f0f37 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Tue, 17 Nov 2020 20:19:12 +0800 Subject: [PATCH 4727/6909] Init Import screen --- osu.Game/Screens/Import/FileImportScreen.cs | 318 ++++++++++++++++++++ osu.Game/Screens/Menu/ButtonSystem.cs | 2 + osu.Game/Screens/Menu/MainMenu.cs | 4 +- 3 files changed, 323 insertions(+), 1 deletion(-) create mode 100644 osu.Game/Screens/Import/FileImportScreen.cs diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs new file mode 100644 index 0000000000..669c26101c --- /dev/null +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -0,0 +1,318 @@ +using System.IO; +using System.Threading.Tasks; +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.Platform; +using osu.Framework.Screens; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; +using osu.Game.Overlays.Settings; +using osu.Game.Configuration; +using osu.Game.Overlays; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.Containers; +using osuTK.Graphics; + +namespace osu.Game.Screens.Import +{ + public class FileImportScreen : OsuScreen + { + private Container contentContainer; + private FileSelector fileSelector; + private Container fileSelectContainer; + + public override bool HideOverlaysOnEnter => true; + + private string[] fileExtensions = { ".foo" }; + private string defaultPath; + + private readonly Bindable currentFile = new Bindable(); + private readonly IBindable currentDirectory = new Bindable(); + private readonly Bindable filterType = new Bindable(FileFilterType.All); + private TextFlowContainer currentFileText; + private OsuScrollContainer fileNameScroll; + private readonly OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + [Resolved] + private OsuGameBase gameBase { get; set; } + + [Resolved] + private DialogOverlay dialogOverlay { get; set; } + + [BackgroundDependencyLoader(true)] + private void load(Storage storage) + { + storage.GetStorageForDirectory("imports"); + var originalPath = storage.GetFullPath("imports", true); + + defaultPath = originalPath; + + InternalChild = contentContainer = new Container + { + Masking = true, + CornerRadius = 10, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(0.9f, 0.8f), + Children = new Drawable[] + { + new Box + { + Colour = overlayColourProvider.Background5, + RelativeSizeAxes = Axes.Both, + }, + fileSelectContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Width = 0.65f, + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Width = 0.35f, + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Children = new Drawable[] + { + new Box + { + Colour = overlayColourProvider.Background3, + RelativeSizeAxes = Axes.Both + }, + fileNameScroll = new OsuScrollContainer + { + Masking = false, + RelativeSizeAxes = Axes.Both, + Child = currentFileText = new TextFlowContainer(t => t.Font = OsuFont.Default.With(size: 30)) + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + TextAnchor = Anchor.Centre + }, + }, + } + }, + }, + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + Colour = overlayColourProvider.Background4, + RelativeSizeAxes = Axes.Both + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(10), + Children = new Drawable[] + { + new SettingsEnumDropdown + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + LabelText = "File Type", + Current = filterType, + Margin = new MarginPadding { Bottom = 15 } + }, + new GridContainer + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Top = 15 }, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] + { + new TriangleButton + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Height = 50, + Width = 0.9f, + Text = "Refresh", + Action = refresh + }, + new TriangleButton + { + Text = "Import", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Height = 50, + Width = 0.9f, + Action = () => + { + var d = currentFile.Value?.FullName; + if (d != null) + startImport(d); + else + currentFileText.FlashColour(Color4.Red, 500); + }, + } + }, + } + }, + } + }, + } + } + } + } + }, + } + } + } + }; + + fileNameScroll.ScrollContent.Anchor = Anchor.Centre; + fileNameScroll.ScrollContent.Origin = Anchor.Centre; + + currentFile.BindValueChanged(updateFileSelectionText, true); + currentDirectory.BindValueChanged(_ => + { + currentFile.Value = null; + }); + + filterType.BindValueChanged(onFilterTypeChanged, true); + } + + private void onFilterTypeChanged(ValueChangedEvent v) + { + switch (v.NewValue) + { + case FileFilterType.Beatmap: + fileExtensions = new string[] { ".osz" }; + break; + + case FileFilterType.Skin: + fileExtensions = new string[] { ".osk" }; + break; + + case FileFilterType.Replay: + fileExtensions = new string[] { ".osr" }; + break; + + default: + case FileFilterType.All: + fileExtensions = new string[] { ".osk", ".osr", ".osz" }; + break; + } + + refresh(); + } + + private void refresh() + { + currentFile.UnbindBindings(); + currentDirectory.UnbindBindings(); + + fileSelector?.Expire(); + + var directory = currentDirectory.Value?.FullName ?? defaultPath; + fileSelector = new FileSelector(initialPath: directory, validFileExtensions: fileExtensions) + { + RelativeSizeAxes = Axes.Both + }; + + currentDirectory.BindTo(fileSelector.CurrentPath); + currentFile.BindTo(fileSelector.CurrentFile); + + fileSelectContainer.Add(fileSelector); + } + + private void updateFileSelectionText(ValueChangedEvent v) + { + currentFileText.Text = v.NewValue?.Name ?? "Select a file"; + } + + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + + contentContainer.FadeOut().Then().ScaleTo(0.8f).RotateTo(-15).MoveToX(300) + .Then() + .ScaleTo(1, 1500, Easing.OutElastic) + .FadeIn(500) + .MoveToX(0, 500, Easing.OutQuint) + .RotateTo(0, 500, Easing.OutQuint); + } + + public override bool OnExiting(IScreen next) + { + contentContainer.ScaleTo(0.8f, 500, Easing.OutExpo).RotateTo(-15, 500, Easing.OutExpo).MoveToX(300, 500, Easing.OutQuint).FadeOut(500); + this.FadeOut(500, Easing.OutExpo); + + return base.OnExiting(next); + } + + private void startImport(string path) + { + if (string.IsNullOrEmpty(path)) + return; + + if (!File.Exists(path)) + { + refresh(); + currentFileText.Text = "File not exist"; + currentFileText.FlashColour(Color4.Red, 500); + return; + } + + string[] paths = { path }; + + Task.Factory.StartNew(() => gameBase.Import(paths), TaskCreationOptions.LongRunning); + } + + public enum FileFilterType + { + Skin, + Beatmap, + Replay, + All + } + } +} diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 4becdd58cd..1e0d27ac6a 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -41,6 +41,7 @@ namespace osu.Game.Screens.Menu public Action OnExit; public Action OnBeatmapListing; public Action OnSolo; + public Action OnImportButton; public Action OnSettings; public Action OnMulti; public Action OnChart; @@ -131,6 +132,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), () => OnBeatmapListing?.Invoke(), 0, Key.D)); + buttonsTopLevel.Add(new Button(@"Import File", @"button-generic-select", FontAwesome.Solid.File, new Color4(0, 86, 73, 255), () => OnImportButton?.Invoke())); 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 c3ecd75963..69e4ea487e 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -19,6 +19,7 @@ using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Multi; using osu.Game.Screens.Select; +using osu.Game.Screens.Import; namespace osu.Game.Screens.Menu { @@ -105,6 +106,7 @@ namespace osu.Game.Screens.Menu }, OnSolo = onSolo, OnMulti = delegate { this.Push(new Multiplayer()); }, + OnImportButton = onImport, OnExit = confirmAndExit, } } @@ -144,7 +146,7 @@ namespace osu.Game.Screens.Menu [Resolved(canBeNull: true)] private OsuGame game { get; set; } - + private void onImport() => this.Push(new FileImportScreen()); private void confirmAndExit() { if (exitConfirmed) return; From dcaebd2621a34055c4d8a68e27e21c7a0056c04d Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Tue, 17 Nov 2020 21:39:29 +0800 Subject: [PATCH 4728/6909] Add license text --- osu.Game/Screens/Import/FileImportScreen.cs | 4 +++- osu.Game/Screens/Menu/ButtonSystem.cs | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 669c26101c..bcbc87e8cc 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System.IO; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -11,7 +14,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osuTK; using osu.Game.Overlays.Settings; -using osu.Game.Configuration; using osu.Game.Overlays; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.Containers; diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 1e0d27ac6a..1722329c21 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -132,7 +132,10 @@ 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), () => OnBeatmapListing?.Invoke(), 0, Key.D)); - buttonsTopLevel.Add(new Button(@"Import File", @"button-generic-select", FontAwesome.Solid.File, new Color4(0, 86, 73, 255), () => OnImportButton?.Invoke())); + if(RuntimeInfo.OS == RuntimeInfo.Platform.Android) + { + buttonsTopLevel.Add(new Button(@"Import File", @"button-generic-select", FontAwesome.Solid.File, new Color4(0, 86, 73, 255), () => OnImportButton?.Invoke())); + } if (host.CanExit) buttonsTopLevel.Add(new Button(@"exit", string.Empty, OsuIcon.CrossCircle, new Color4(238, 51, 153, 255), () => OnExit?.Invoke(), 0, Key.Q)); From 58c8184ad7f7b008d001a048cfcbf998f14e1423 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 22:56:21 +0900 Subject: [PATCH 4729/6909] Define blueprint order similarly to hitobjects --- .../Compose/Components/BlueprintContainer.cs | 6 +- .../Components/SelectionBlueprintContainer.cs | 76 +++++++++++++++++++ .../Timeline/TimelineBlueprintContainer.cs | 6 +- 3 files changed, 82 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 53b6e14940..3aaa0c7d89 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -118,8 +118,8 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - protected virtual Container CreateSelectionBlueprintContainer() => - new Container { RelativeSizeAxes = Axes.Both }; + protected virtual SelectionBlueprintContainer CreateSelectionBlueprintContainer() => + new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; /// /// Creates a which outlines s and handles movement of selections. @@ -338,7 +338,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. private bool beginClickSelection(MouseButtonEvent e) { - foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren) + foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse()) { if (!blueprint.IsHovered) continue; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs new file mode 100644 index 0000000000..54932f6252 --- /dev/null +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs @@ -0,0 +1,76 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Edit; + +namespace osu.Game.Screens.Edit.Compose.Components +{ + public class SelectionBlueprintContainer : Container + { + public override void Add(SelectionBlueprint drawable) + { + base.Add(drawable); + + if (Content == this) + bindStartTime(drawable); + } + + public override bool Remove(SelectionBlueprint drawable) + { + if (!base.Remove(drawable)) + return false; + + if (Content == this) + unbindStartTime(drawable); + return true; + } + + public override void Clear(bool disposeChildren) + { + base.Clear(disposeChildren); + unbindAllStartTimes(); + } + + private readonly Dictionary startTimeMap = new Dictionary(); + + private void bindStartTime(SelectionBlueprint blueprint) + { + var bindable = blueprint.HitObject.StartTimeBindable.GetBoundCopy(); + + bindable.BindValueChanged(_ => + { + if (LoadState >= LoadState.Ready) + SortInternal(); + }); + + startTimeMap[blueprint] = bindable; + } + + private void unbindStartTime(SelectionBlueprint blueprint) + { + startTimeMap[blueprint].UnbindAll(); + startTimeMap.Remove(blueprint); + } + + private void unbindAllStartTimes() + { + foreach (var kvp in startTimeMap) + kvp.Value.UnbindAll(); + startTimeMap.Clear(); + } + + protected override int Compare(Drawable x, Drawable y) + { + if (!(x is SelectionBlueprint xObj) || !(y is SelectionBlueprint yObj)) + return base.Compare(x, y); + + // Put earlier blueprints towards the end of the list, so they handle input first + int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); + return i == 0 ? CompareReverseChildID(x, y) : i; + } + } +} diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index eef02e61a6..2bd4ac2f91 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - protected override Container CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; protected override void OnDrag(DragEvent e) { @@ -195,13 +195,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - protected class TimelineSelectionBlueprintContainer : Container + protected class TimelineSelectionBlueprintContainer : SelectionBlueprintContainer { protected override Container Content { get; } public TimelineSelectionBlueprintContainer() { - AddInternal(new TimelinePart(Content = new Container { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); + AddInternal(new TimelinePart(Content = new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); } } } From 04805b78c31718a451737bd78b090e96e39a3859 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 23:19:59 +0900 Subject: [PATCH 4730/6909] Tighten osu! ruleset lifetime expiry for past hitobjects --- .../Objects/Drawables/DrawableHitCircle.cs | 2 ++ .../Objects/Drawables/DrawableOsuHitObject.cs | 5 +++++ .../Objects/Drawables/DrawableSlider.cs | 16 ++++++++-------- .../Objects/Drawables/DrawableSpinner.cs | 2 +- 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 2e63160d36..d1ceca6d8f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -180,6 +180,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.Delay(800).FadeOut(); break; } + + Expire(); } public Drawable ProxiedLayer => ApproachCircle; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index bcaf73d34f..c962d191a6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -11,6 +11,7 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -91,6 +92,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // Manually set to reduce the number of future alive objects to a bare minimum. LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; + + // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts. + // An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry. + LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; } /// diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index f7b1894058..14c494d909 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -193,13 +193,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return base.CreateNestedHitObject(hitObject); } - protected override void UpdateInitialTransforms() - { - base.UpdateInitialTransforms(); - - Body.FadeInFromZero(HitObject.TimeFadeIn); - } - public readonly Bindable Tracking = new Bindable(); protected override void Update() @@ -273,6 +266,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.PlaySamples(); } + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); + + Body.FadeInFromZero(HitObject.TimeFadeIn); + } + protected override void UpdateStartTimeStateTransforms() { base.UpdateStartTimeStateTransforms(); @@ -297,7 +297,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables break; } - this.FadeOut(fade_out_time, Easing.OutQuint); + this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); } public Drawable ProxiedLayer => HeadCircle.ProxiedLayer; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 87c7146a64..2a14a7c975 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.UpdateHitStateTransforms(state); - this.FadeOut(160); + this.FadeOut(160).Expire(); // skin change does a rewind of transforms, which will stop the spinning sound from playing if it's currently in playback. isSpinning?.TriggerChange(); From ce4baf328dba51f3f5006bbc69f0ad58b9d91dd6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 17 Nov 2020 23:35:36 +0900 Subject: [PATCH 4731/6909] Move into OnApply() to resolve one-frame issues --- .../Objects/Drawables/DrawableOsuHitObject.cs | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index c962d191a6..a26db06ede 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -61,6 +61,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables PositionBindable.BindTo(HitObject.PositionBindable); StackHeightBindable.BindTo(HitObject.StackHeightBindable); ScaleBindable.BindTo(HitObject.ScaleBindable); + + // Manually set to reduce the number of future alive objects to a bare minimum. + LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; + + // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts. + // An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry. + LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; } protected override void OnFree(HitObject hitObject) @@ -86,18 +93,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public virtual void Shake(double maximumLength) => shakeContainer.Shake(maximumLength); - protected override void UpdateInitialTransforms() - { - base.UpdateInitialTransforms(); - - // Manually set to reduce the number of future alive objects to a bare minimum. - LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; - - // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts. - // An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry. - LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; - } - /// /// Causes this to get missed, disregarding all conditions in implementations of . /// From c360533e4cce0875dbb5175ef6a27bac869759f1 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 17 Nov 2020 23:40:30 +0900 Subject: [PATCH 4732/6909] Simplify code of TestSceneFruitObjects --- .../TestSceneFruitObjects.cs | 131 ++++-------------- 1 file changed, 24 insertions(+), 107 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index e9dabd30ed..89063319d6 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -6,7 +6,6 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osu.Game.Rulesets.Objects; using osuTK; namespace osu.Game.Rulesets.Catch.Tests @@ -19,107 +18,42 @@ namespace osu.Game.Rulesets.Catch.Tests base.LoadComplete(); foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) - AddStep($"show {rep}", () => SetContents(() => createDrawable(rep))); + AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep))); AddStep("show droplet", () => SetContents(() => createDrawableDroplet())); AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) - AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawable(rep, true))); + AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true))); AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true))); } - private Drawable createDrawableTinyDroplet() + private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) => + setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash); + + private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash); + + private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet())); + + private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false) { - var droplet = new TestCatchTinyDroplet + var hitObject = d.HitObject; + hitObject.StartTime = 1000000000000; + hitObject.Scale = 1.5f; + + if (hyperdash) + hitObject.HyperDashTarget = new Banana(); + + d.Anchor = Anchor.Centre; + d.RelativePositionAxes = Axes.None; + d.Position = Vector2.Zero; + d.HitObjectApplied += _ => { - Scale = 1.5f, + d.LifetimeStart = double.NegativeInfinity; + d.LifetimeEnd = double.PositiveInfinity; }; - - return new TestDrawableTinyDroplet(droplet) - { - Anchor = Anchor.Centre, - RelativePositionAxes = Axes.None, - Position = Vector2.Zero, - }; - } - - private class TestDrawableTinyDroplet : DrawableTinyDroplet - { - public TestDrawableTinyDroplet(TinyDroplet tinyDroplet) - : base(tinyDroplet) - { - } - - protected override void OnApply(HitObject hitObject) - { - base.OnApply(hitObject); - LifetimeStart = double.NegativeInfinity; - LifetimeEnd = double.PositiveInfinity; - } - } - - private Drawable createDrawableDroplet(bool hyperdash = false) - { - var droplet = new TestCatchDroplet - { - Scale = 1.5f, - HyperDashTarget = hyperdash ? new Banana() : null - }; - - return new TestDrawableDroplet(droplet) - { - Anchor = Anchor.Centre, - RelativePositionAxes = Axes.None, - Position = Vector2.Zero, - }; - } - - private class TestDrawableDroplet : DrawableDroplet - { - public TestDrawableDroplet(Droplet droplet) - : base(droplet) - { - } - - protected override void OnApply(HitObject hitObject) - { - base.OnApply(hitObject); - LifetimeStart = double.NegativeInfinity; - LifetimeEnd = double.PositiveInfinity; - } - } - - private Drawable createDrawable(FruitVisualRepresentation rep, bool hyperdash = false) - { - Fruit fruit = new TestCatchFruit(rep) - { - Scale = 1.5f, - HyperDashTarget = hyperdash ? new Banana() : null - }; - - return new TestDrawableFruit(fruit) - { - Anchor = Anchor.Centre, - RelativePositionAxes = Axes.None, - Position = Vector2.Zero, - }; - } - - private class TestDrawableFruit : DrawableFruit - { - public TestDrawableFruit(Fruit fruit) - : base(fruit) - { - } - - protected override void OnApply(HitObject hitObject) - { - base.OnApply(hitObject); - LifetimeStart = double.NegativeInfinity; - LifetimeEnd = double.PositiveInfinity; - } + return d; } public class TestCatchFruit : Fruit @@ -127,26 +61,9 @@ namespace osu.Game.Rulesets.Catch.Tests public TestCatchFruit(FruitVisualRepresentation rep) { VisualRepresentation = rep; - StartTime = 1000000000000; } public override FruitVisualRepresentation VisualRepresentation { get; } } - - public class TestCatchDroplet : Droplet - { - public TestCatchDroplet() - { - StartTime = 1000000000000; - } - } - - public class TestCatchTinyDroplet : TinyDroplet - { - public TestCatchTinyDroplet() - { - StartTime = 1000000000000; - } - } } } From 875d7dec7999f96e03c9454ce256d283ff299286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Nov 2020 20:11:13 +0100 Subject: [PATCH 4733/6909] Remove redundant `this.` qualifier --- osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index 3b9e5e948a..7fe3917893 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -24,7 +24,7 @@ namespace osu.Game.Rulesets.Judgements public DefaultJudgementPiece(HitResult result) { - this.Result = result; + Result = result; Origin = Anchor.Centre; } From 57eaee27aec1110f869c9ca5f25262bede0e0188 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 17 Nov 2020 20:12:04 +0100 Subject: [PATCH 4734/6909] Rename param to match type better --- osu.Game/Skinning/LegacyJudgementPiece.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacyJudgementPiece.cs b/osu.Game/Skinning/LegacyJudgementPiece.cs index 6c606c638d..3def37e22c 100644 --- a/osu.Game/Skinning/LegacyJudgementPiece.cs +++ b/osu.Game/Skinning/LegacyJudgementPiece.cs @@ -14,14 +14,14 @@ namespace osu.Game.Skinning { private readonly HitResult result; - public LegacyJudgementPiece(HitResult result, Drawable texture) + public LegacyJudgementPiece(HitResult result, Drawable drawable) { this.result = result; AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; - InternalChild = texture; + InternalChild = drawable; } public virtual void PlayAnimation() From c8fb49d540d4876fb3b7800c4159da6f5239fe24 Mon Sep 17 00:00:00 2001 From: kamp Date: Tue, 17 Nov 2020 22:23:46 +0100 Subject: [PATCH 4735/6909] Apply suggestions and remove redundant updateConnectingPath call --- .../PathControlPointConnectionPiece.cs | 16 +++------------- .../Components/PathControlPointVisualiser.cs | 6 ++---- 2 files changed, 5 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs index 45c4a61ce1..eb7011e8b0 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointConnectionPiece.cs @@ -20,17 +20,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private readonly Path path; private readonly Slider slider; - private int controlPointIndex; - - public int ControlPointIndex - { - get => controlPointIndex; - set - { - controlPointIndex = value; - updateConnectingPath(); - } - } + public int ControlPointIndex { get; set; } private IBindable sliderPosition; private IBindable pathVersion; @@ -38,7 +28,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components public PathControlPointConnectionPiece(Slider slider, int controlPointIndex) { this.slider = slider; - this.controlPointIndex = controlPointIndex; + ControlPointIndex = controlPointIndex; Origin = Anchor.Centre; AutoSizeAxes = Axes.Both; @@ -74,7 +64,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components path.ClearVertices(); - int nextIndex = controlPointIndex + 1; + int nextIndex = ControlPointIndex + 1; if (nextIndex == 0 || nextIndex >= slider.Path.ControlPoints.Count) return; 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 14ce0e065b..e551be4af3 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components switch (e.Action) { case NotifyCollectionChangedAction.Add: - // If inserting in the the path (not appending), + // If inserting in the path (not appending), // update indices of existing connections after insert location if (e.NewStartingIndex < Pieces.Count) { @@ -93,8 +93,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components break; case NotifyCollectionChangedAction.Remove: - int oldSize = Pieces.Count; - foreach (var point in e.OldItems.Cast()) { Pieces.RemoveAll(p => p.ControlPoint == point); @@ -103,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // If removing before the end of the path, // update indices of connections after remove location - if (e.OldStartingIndex + e.OldItems.Count < oldSize) + if (e.OldStartingIndex + e.OldItems.Count < Pieces.Count + e.OldItems.Count) { foreach (var connection in Connections) { From 2d66423fbdc37cba19f675897297a8efa2131784 Mon Sep 17 00:00:00 2001 From: kamp Date: Tue, 17 Nov 2020 23:04:38 +0100 Subject: [PATCH 4736/6909] Simplify inequality --- .../Blueprints/Sliders/Components/PathControlPointVisualiser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index e551be4af3..7375c0e981 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components // If removing before the end of the path, // update indices of connections after remove location - if (e.OldStartingIndex + e.OldItems.Count < Pieces.Count + e.OldItems.Count) + if (e.OldStartingIndex < Pieces.Count) { foreach (var connection in Connections) { From 783c172b5de26e53b1ff0982f8f49a9bcd7bbad1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Nov 2020 13:33:22 +0900 Subject: [PATCH 4737/6909] Make sealed and cleanup comparator --- .../Edit/Compose/Components/BlueprintContainer.cs | 3 +-- ...ner.cs => HitObjectOrderedSelectionContainer.cs} | 13 +++++-------- .../Timeline/TimelineBlueprintContainer.cs | 6 +++--- 3 files changed, 9 insertions(+), 13 deletions(-) rename osu.Game/Screens/Edit/Compose/Components/{SelectionBlueprintContainer.cs => HitObjectOrderedSelectionContainer.cs} (85%) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 3aaa0c7d89..4b98d42c7c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -118,8 +118,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } - protected virtual SelectionBlueprintContainer CreateSelectionBlueprintContainer() => - new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + protected virtual Container CreateSelectionBlueprintContainer() => new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }; /// /// Creates a which outlines s and handles movement of selections. diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs similarity index 85% rename from osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs rename to osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index 54932f6252..ae50b0fd61 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -9,14 +9,12 @@ using osu.Game.Rulesets.Edit; namespace osu.Game.Screens.Edit.Compose.Components { - public class SelectionBlueprintContainer : Container + public sealed class HitObjectOrderedSelectionContainer : Container { public override void Add(SelectionBlueprint drawable) { base.Add(drawable); - - if (Content == this) - bindStartTime(drawable); + bindStartTime(drawable); } public override bool Remove(SelectionBlueprint drawable) @@ -24,8 +22,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (!base.Remove(drawable)) return false; - if (Content == this) - unbindStartTime(drawable); + unbindStartTime(drawable); return true; } @@ -65,8 +62,8 @@ namespace osu.Game.Screens.Edit.Compose.Components protected override int Compare(Drawable x, Drawable y) { - if (!(x is SelectionBlueprint xObj) || !(y is SelectionBlueprint yObj)) - return base.Compare(x, y); + var xObj = (SelectionBlueprint)x; + var yObj = (SelectionBlueprint)y; // Put earlier blueprints towards the end of the list, so they handle input first int i = yObj.HitObject.StartTime.CompareTo(xObj.HitObject.StartTime); diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 2bd4ac2f91..2f14c607c2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -75,7 +75,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - protected override SelectionBlueprintContainer CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + protected override Container CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; protected override void OnDrag(DragEvent e) { @@ -195,13 +195,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline } } - protected class TimelineSelectionBlueprintContainer : SelectionBlueprintContainer + protected class TimelineSelectionBlueprintContainer : Container { protected override Container Content { get; } public TimelineSelectionBlueprintContainer() { - AddInternal(new TimelinePart(Content = new SelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); + AddInternal(new TimelinePart(Content = new HitObjectOrderedSelectionContainer { RelativeSizeAxes = Axes.Both }) { RelativeSizeAxes = Axes.Both }); } } } From f00c23b4a07367179d67c7291e1305f1ca01ab3d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Nov 2020 13:37:15 +0900 Subject: [PATCH 4738/6909] Add comment + xmldoc --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 1 + .../Compose/Components/HitObjectOrderedSelectionContainer.cs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 4b98d42c7c..df9cadebfc 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -337,6 +337,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a selection was performed. private bool beginClickSelection(MouseButtonEvent e) { + // Iterate from the top of the input stack (blueprints closest to the front of the screen first). foreach (SelectionBlueprint blueprint in SelectionBlueprints.AliveChildren.Reverse()) { if (!blueprint.IsHovered) continue; diff --git a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs index ae50b0fd61..9e95fe4fa1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/HitObjectOrderedSelectionContainer.cs @@ -6,9 +6,13 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Edit; +using osu.Game.Rulesets.Objects; namespace osu.Game.Screens.Edit.Compose.Components { + /// + /// A container for ordered by their start times. + /// public sealed class HitObjectOrderedSelectionContainer : Container { public override void Add(SelectionBlueprint drawable) From efc18887c8182d6414ff2b8efe47c1b4f709525e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 15:51:09 +0900 Subject: [PATCH 4739/6909] Update framework --- osu.Android.props | 2 +- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index 6e3d5eec1f..4657896fac 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index b17611f23f..0feab9a717 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -139,7 +139,7 @@ namespace osu.Desktop // SDL2 DesktopWindow case DesktopWindow desktopWindow: - desktopWindow.CursorState.Value |= CursorState.Hidden; + desktopWindow.CursorState |= CursorState.Hidden; desktopWindow.SetIconFromStream(iconStream); desktopWindow.Title = Name; desktopWindow.DragDrop += f => fileDrop(new[] { f }); diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1850ee3488..704ac5a611 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 2ac23f1503..346bd892b0 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From bb1aacb360758c2c5e21e172bcb9f1985933a2b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 16:18:27 +0900 Subject: [PATCH 4740/6909] Fix SkinnableSprite initialising a drawable even when the texture is not available --- osu.Game/Skinning/SkinnableSprite.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 5352928ec6..1340d1474c 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -24,7 +24,15 @@ namespace osu.Game.Skinning { } - protected override Drawable CreateDefault(ISkinComponent component) => new Sprite { Texture = textures.Get(component.LookupName) }; + protected override Drawable CreateDefault(ISkinComponent component) + { + var texture = textures.Get(component.LookupName); + + if (texture == null) + return null; + + return new Sprite { Texture = texture }; + } private class SpriteComponent : ISkinComponent { From 8be31f4805385708909b4405dd3c2aeb427bda15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 17 Nov 2020 23:55:21 +0900 Subject: [PATCH 4741/6909] Adjust legacy skin judgement transforms to match stable --- osu.Game/Skinning/LegacyJudgementPiece.cs | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacyJudgementPiece.cs b/osu.Game/Skinning/LegacyJudgementPiece.cs index 3def37e22c..44aa5106e2 100644 --- a/osu.Game/Skinning/LegacyJudgementPiece.cs +++ b/osu.Game/Skinning/LegacyJudgementPiece.cs @@ -14,6 +14,8 @@ namespace osu.Game.Skinning { private readonly HitResult result; + private bool hasParticle; + public LegacyJudgementPiece(HitResult result, Drawable drawable) { this.result = result; @@ -37,6 +39,8 @@ namespace osu.Game.Skinning if (animation?.FrameCount > 1) return; + const double animation_length = 500; + switch (result) { case HitResult.Miss: @@ -49,8 +53,19 @@ namespace osu.Game.Skinning break; default: - this.ScaleTo(0.9f); - this.ScaleTo(1, 500, Easing.OutElastic); + if (!hasParticle) + { + this.ScaleTo(0.6f).Then() + .ScaleTo(1.1f, animation_length * 0.8f).Then() + .ScaleTo(0.9f, animation_length * 0.4f).Then() + .ScaleTo(1f, animation_length * 0.2f); + } + else + { + this.ScaleTo(0.9f); + this.ScaleTo(1.05f, animation_length); + } + break; } } From 5bd4ace37f73cffd0550812e455bb7c3335cf623 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 15:38:26 +0900 Subject: [PATCH 4742/6909] Split out new and old style legacy judgement pieces --- osu.Game/Skinning/LegacyJudgementPieceNew.cs | 93 +++++++++++++++++++ ...entPiece.cs => LegacyJudgementPieceOld.cs} | 31 +++---- osu.Game/Skinning/LegacySkin.cs | 30 +++++- 3 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Skinning/LegacyJudgementPieceNew.cs rename osu.Game/Skinning/{LegacyJudgementPiece.cs => LegacyJudgementPieceOld.cs} (61%) diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs new file mode 100644 index 0000000000..de77df9e10 --- /dev/null +++ b/osu.Game/Skinning/LegacyJudgementPieceNew.cs @@ -0,0 +1,93 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; +using osuTK; + +namespace osu.Game.Skinning +{ + public class LegacyJudgementPieceNew : CompositeDrawable, IAnimatableJudgement + { + private readonly HitResult result; + + private readonly LegacyJudgementPieceOld temporaryOldStyle; + + private readonly Drawable mainPiece; + + public LegacyJudgementPieceNew(HitResult result, Func createMainDrawable, Drawable particleDrawable) + { + this.result = result; + + AutoSizeAxes = Axes.Both; + Origin = Anchor.Centre; + + InternalChildren = new[] + { + mainPiece = createMainDrawable().With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + }) + }; + + if (result != HitResult.Miss) + { + //new judgement shows old as a temporary effect + AddInternal(temporaryOldStyle = new LegacyJudgementPieceOld(result, createMainDrawable, 1.05f) + { + Blending = BlendingParameters.Additive, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + } + + public void PlayAnimation() + { + var animation = mainPiece as IFramedAnimation; + + animation?.GotoFrame(0); + + this.RotateTo(0); + this.MoveTo(Vector2.Zero); + + if (temporaryOldStyle != null) + { + temporaryOldStyle.PlayAnimation(); + + temporaryOldStyle.Hide(); + temporaryOldStyle.Delay(-16) + .FadeTo(0.5f, 56, Easing.Out).Then() + .FadeOut(300); + } + + // legacy judgements don't play any transforms if they are an animation. + if (animation?.FrameCount > 1) + return; + + switch (result) + { + case HitResult.Miss: + mainPiece.ScaleTo(1.6f); + mainPiece.ScaleTo(1, 100, Easing.In); + + mainPiece.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + mainPiece.RotateTo(40, 800, Easing.InQuint); + break; + + default: + const double animation_length = 1100; + + mainPiece.ScaleTo(0.9f); + mainPiece.ScaleTo(1.05f, animation_length); + break; + } + } + } +} diff --git a/osu.Game/Skinning/LegacyJudgementPiece.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs similarity index 61% rename from osu.Game/Skinning/LegacyJudgementPiece.cs rename to osu.Game/Skinning/LegacyJudgementPieceOld.cs index 44aa5106e2..63d2c44dd9 100644 --- a/osu.Game/Skinning/LegacyJudgementPiece.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.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.Animations; using osu.Framework.Graphics.Containers; @@ -10,20 +11,21 @@ using osuTK; namespace osu.Game.Skinning { - public class LegacyJudgementPiece : CompositeDrawable, IAnimatableJudgement + public class LegacyJudgementPieceOld : CompositeDrawable, IAnimatableJudgement { private readonly HitResult result; - private bool hasParticle; + private readonly float finalScale; - public LegacyJudgementPiece(HitResult result, Drawable drawable) + public LegacyJudgementPieceOld(HitResult result, Func createMainDrawable, float finalScale = 1f) { this.result = result; + this.finalScale = finalScale; AutoSizeAxes = Axes.Both; Origin = Anchor.Centre; - InternalChild = drawable; + InternalChild = createMainDrawable(); } public virtual void PlayAnimation() @@ -39,8 +41,6 @@ namespace osu.Game.Skinning if (animation?.FrameCount > 1) return; - const double animation_length = 500; - switch (result) { case HitResult.Miss: @@ -53,19 +53,14 @@ namespace osu.Game.Skinning break; default: - if (!hasParticle) - { - this.ScaleTo(0.6f).Then() - .ScaleTo(1.1f, animation_length * 0.8f).Then() - .ScaleTo(0.9f, animation_length * 0.4f).Then() - .ScaleTo(1f, animation_length * 0.2f); - } - else - { - this.ScaleTo(0.9f); - this.ScaleTo(1.05f, animation_length); - } + const double animation_length = 120; + this.ScaleTo(0.6f).Then() + .ScaleTo(1.1f, animation_length * 0.8f).Then() + // this is actually correct to match stable; there were overlapping transforms. + .ScaleTo(0.9f).Delay(animation_length * 0.2f) + .ScaleTo(1.1f).ScaleTo(0.9f, animation_length * 0.2f).Then() + .ScaleTo(0.95f).ScaleTo(finalScale, animation_length * 0.2f); break; } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index ca8bb58023..ce1a736f0a 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -371,9 +371,16 @@ namespace osu.Game.Skinning } case GameplaySkinComponent resultComponent: - var drawable = getJudgementAnimation(resultComponent.Component); - if (drawable != null) - return new LegacyJudgementPiece(resultComponent.Component, drawable); + Func createDrawable = () => getJudgementAnimation(resultComponent.Component); + + if (createDrawable() != null) + { + var particles = getParticleTexture(resultComponent.Component); + if (particles != null) + return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleTexture(resultComponent.Component)); + else + return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable); + } break; } @@ -381,6 +388,23 @@ namespace osu.Game.Skinning return this.GetAnimation(component.LookupName, false, false); } + private Drawable getParticleTexture(HitResult result) + { + switch (result) + { + case HitResult.Meh: + return this.GetAnimation("particle50", false, false); + + case HitResult.Ok: + return this.GetAnimation("particle100", false, false); + + case HitResult.Great: + return this.GetAnimation("particle300", false, false); + } + + return null; + } + private Drawable getJudgementAnimation(HitResult result) { switch (result) From 94886a09b23b2c7d850781eaa20c07b0697a2d3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 15:39:02 +0900 Subject: [PATCH 4743/6909] Remove fades from DrawableJudgement itself --- .../UI/DrawableManiaJudgement.cs | 7 +++---- .../Objects/Drawables/DrawableOsuJudgement.cs | 14 +++----------- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 11 +++++++---- 3 files changed, 13 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index ebce40a785..a341cdd8ec 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -19,15 +19,14 @@ namespace osu.Game.Rulesets.Mania.UI { } - protected override double FadeInDuration => 50; - protected override void ApplyHitAnimations() { JudgementBody.ScaleTo(0.8f); JudgementBody.ScaleTo(1, 250, Easing.OutElastic); - JudgementBody.Delay(FadeInDuration).ScaleTo(0.75f, 250); - this.Delay(FadeInDuration).FadeOut(200); + JudgementBody.Delay(50) + .ScaleTo(0.75f, 250) + .FadeOut(200); } protected override Drawable CreateDefaultJudgement(HitResult result) => new ManiaJudgementPiece(result); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 47fb53379f..90133fb01c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -44,26 +44,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } - private double fadeOutDelay; - protected override double FadeOutDelay => fadeOutDelay; - protected override void ApplyHitAnimations() { bool hitLightingEnabled = config.Get(OsuSetting.HitLighting); if (hitLightingEnabled) { - JudgementBody.FadeIn().Delay(FadeInDuration).FadeOut(400); - Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); - } - else - { - JudgementBody.Alpha = 1; - } - fadeOutDelay = hitLightingEnabled ? 1400 : base.FadeOutDelay; + // extend the lifetime to cover lighting fade + LifetimeEnd = 1400; + } base.ApplyHitAnimations(); } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 468b3190b0..09b8353c63 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.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.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; @@ -32,11 +33,13 @@ namespace osu.Game.Rulesets.Judgements /// /// Duration of initial fade in. /// + [Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")] protected virtual double FadeInDuration => 100; /// /// Duration to wait until fade out begins. Defaults to . /// + [Obsolete("Apply any animations manually via ApplyHitAnimations / ApplyMissAnimations. Defaults were moved inside skinned components.")] protected virtual double FadeOutDelay => FadeInDuration; /// @@ -73,7 +76,6 @@ namespace osu.Game.Rulesets.Judgements /// protected virtual void ApplyHitAnimations() { - this.Delay(FadeOutDelay).FadeOut(400); } /// @@ -112,8 +114,6 @@ namespace osu.Game.Rulesets.Judgements // not sure if this should remain going forward. skinnableJudgement.ResetAnimation(); - this.FadeInFromZero(FadeInDuration, Easing.OutQuint); - switch (Result.Type) { case HitResult.None: @@ -134,7 +134,10 @@ namespace osu.Game.Rulesets.Judgements animatable.PlayAnimation(); } - Expire(true); + JudgementBody.Expire(true); + + LifetimeStart = JudgementBody.LifetimeStart; + LifetimeEnd = JudgementBody.LifetimeEnd; } private HitResult? currentDrawableType; From 25d4511e49d6c7db38eb8e6ab349bbfd2434a56b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 16:09:51 +0900 Subject: [PATCH 4744/6909] Fix judgement test scene always using hitobjects at t=0 --- osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index 646f12f710..7ea2ef3a78 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -89,7 +89,13 @@ namespace osu.Game.Rulesets.Osu.Tests Children = new Drawable[] { pool, - pool.Get(j => j.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null)).With(j => + pool.Get(j => j.Apply(new JudgementResult(new HitObject + { + StartTime = Time.Current + }, new Judgement()) + { + Type = result, + }, null)).With(j => { j.Anchor = Anchor.Centre; j.Origin = Anchor.Centre; From 72a15ef2dc51f877dd1416128a43af38f2b718fd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 15:51:09 +0900 Subject: [PATCH 4745/6909] Handle DrawableJudgement lifetime more flexibly --- .../Objects/Drawables/DrawableOsuJudgement.cs | 2 +- .../Rulesets/Judgements/DrawableJudgement.cs | 51 +++++++++++-------- .../Judgements/IAnimatableJudgement.cs | 4 +- 3 files changed, 34 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 90133fb01c..b06c55102c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); // extend the lifetime to cover lighting fade - LifetimeEnd = 1400; + LifetimeEnd = Lighting.LatestTransformEndTime; } base.ApplyHitAnimations(); diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 09b8353c63..45b3e229f3 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -89,7 +89,6 @@ namespace osu.Game.Rulesets.Judgements /// protected virtual void ApplyMissAnimations() { - this.Delay(600).FadeOut(200); } /// @@ -111,33 +110,43 @@ namespace osu.Game.Rulesets.Judgements prepareDrawables(); - // not sure if this should remain going forward. - skinnableJudgement.ResetAnimation(); + LifetimeStart = Result.TimeAbsolute; - switch (Result.Type) + using (BeginAbsoluteSequence(Result.TimeAbsolute, true)) { - case HitResult.None: - break; + // not sure if this should remain going forward. + skinnableJudgement.ResetAnimation(); - case HitResult.Miss: - ApplyMissAnimations(); - break; + switch (Result.Type) + { + case HitResult.None: + break; - default: - ApplyHitAnimations(); - break; - } + case HitResult.Miss: + ApplyMissAnimations(); + break; + + default: + ApplyHitAnimations(); + break; + } + + if (skinnableJudgement.Drawable is IAnimatableJudgement animatable) + { + var drawableAnimation = (Drawable)animatable; + + drawableAnimation.ClearTransforms(); - if (skinnableJudgement.Drawable is IAnimatableJudgement animatable) - { - using (BeginAbsoluteSequence(Result.TimeAbsolute)) animatable.PlayAnimation(); + + drawableAnimation.Expire(true); + + // a derived version of DrawableJudgement may be adjusting lifetime. + // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime. + if (LifetimeEnd == double.MaxValue || drawableAnimation.LifetimeEnd > LifetimeEnd) + LifetimeEnd = drawableAnimation.LifetimeEnd; + } } - - JudgementBody.Expire(true); - - LifetimeStart = JudgementBody.LifetimeStart; - LifetimeEnd = JudgementBody.LifetimeEnd; } private HitResult? currentDrawableType; diff --git a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs index 3d5bbe6dad..32312f1115 100644 --- a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Graphics; + namespace osu.Game.Rulesets.Judgements { /// /// A skinnable judgement element which supports playing an animation from the current point in time. /// - public interface IAnimatableJudgement + public interface IAnimatableJudgement : IDrawable { void PlayAnimation(); } From 9d0a6de26e6039cf87dfd8c64f1f0ffb7f3643d1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 16:18:27 +0900 Subject: [PATCH 4746/6909] Fix SkinnableSprite initialising a drawable even when the texture is not available --- osu.Game/Skinning/SkinnableSprite.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableSprite.cs b/osu.Game/Skinning/SkinnableSprite.cs index 5352928ec6..1340d1474c 100644 --- a/osu.Game/Skinning/SkinnableSprite.cs +++ b/osu.Game/Skinning/SkinnableSprite.cs @@ -24,7 +24,15 @@ namespace osu.Game.Skinning { } - protected override Drawable CreateDefault(ISkinComponent component) => new Sprite { Texture = textures.Get(component.LookupName) }; + protected override Drawable CreateDefault(ISkinComponent component) + { + var texture = textures.Get(component.LookupName); + + if (texture == null) + return null; + + return new Sprite { Texture = texture }; + } private class SpriteComponent : ISkinComponent { From 9d3de5bca0441ea4b04cab337c9ccdc074b170ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 16:18:58 +0900 Subject: [PATCH 4747/6909] Fix hit lighting dictating lifetime even when not present in skin --- .../Objects/Drawables/DrawableOsuJudgement.cs | 2 +- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index b06c55102c..c81cfd97dd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { bool hitLightingEnabled = config.Get(OsuSetting.HitLighting); - if (hitLightingEnabled) + if (hitLightingEnabled && Lighting.Drawable != null) { Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 45b3e229f3..c0fcb1eb3c 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -139,12 +139,14 @@ namespace osu.Game.Rulesets.Judgements animatable.PlayAnimation(); - drawableAnimation.Expire(true); - // a derived version of DrawableJudgement may be adjusting lifetime. // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime. - if (LifetimeEnd == double.MaxValue || drawableAnimation.LifetimeEnd > LifetimeEnd) - LifetimeEnd = drawableAnimation.LifetimeEnd; + double lastTransformTime = drawableAnimation.LatestTransformEndTime; + + if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd) + { + LifetimeEnd = lastTransformTime; + } } } } From c47e70da9b4c6ac4ac337221b747f09ce3f5ab9c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 17:15:45 +0900 Subject: [PATCH 4748/6909] Update animations for new/old miss style and add fades --- .../Judgements/DefaultJudgementPiece.cs | 8 ++++-- osu.Game/Skinning/LegacyJudgementPieceNew.cs | 28 +++++++++++++------ osu.Game/Skinning/LegacyJudgementPieceOld.cs | 25 ++++++++++------- osu.Game/Skinning/LegacySkin.cs | 8 +++--- 4 files changed, 43 insertions(+), 26 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index 7fe3917893..b89c1f4e4f 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -47,18 +47,18 @@ namespace osu.Game.Rulesets.Judgements public virtual void PlayAnimation() { - this.RotateTo(0); - this.MoveTo(Vector2.Zero); - switch (Result) { case HitResult.Miss: this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); + this.MoveTo(Vector2.Zero); this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + this.RotateTo(0); this.RotateTo(40, 800, Easing.InQuint); + break; default: @@ -66,6 +66,8 @@ namespace osu.Game.Rulesets.Judgements this.ScaleTo(1, 500, Easing.OutElastic); break; } + + this.FadeOutFromOne(800); } } } diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs index de77df9e10..d0a2f1d4eb 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceNew.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceNew.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osuTK; @@ -53,9 +54,14 @@ namespace osu.Game.Skinning animation?.GotoFrame(0); - this.RotateTo(0); - this.MoveTo(Vector2.Zero); + const double fade_in_length = 120; + const double fade_out_delay = 500; + const double fade_out_length = 600; + this.FadeInFromZero(fade_in_length); + this.Delay(fade_out_delay).FadeOut(fade_out_length); + + // new style non-miss judgements show the original style temporarily, with additive colour. if (temporaryOldStyle != null) { temporaryOldStyle.PlayAnimation(); @@ -73,19 +79,23 @@ namespace osu.Game.Skinning switch (result) { case HitResult.Miss: - mainPiece.ScaleTo(1.6f); - mainPiece.ScaleTo(1, 100, Easing.In); + this.ScaleTo(1.6f); + this.ScaleTo(1, 100, Easing.In); - mainPiece.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + //todo: this only applies to osu! ruleset apparently. + this.MoveTo(new Vector2(0, -2)); + this.MoveToOffset(new Vector2(0, 20), fade_out_delay + fade_out_length, Easing.In); - mainPiece.RotateTo(40, 800, Easing.InQuint); + float rotation = RNG.NextSingle(-8.6f, 8.6f); + + this.RotateTo(0); + this.RotateTo(rotation, fade_in_length) + .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In); break; default: - const double animation_length = 1100; - mainPiece.ScaleTo(0.9f); - mainPiece.ScaleTo(1.05f, animation_length); + mainPiece.ScaleTo(1.05f, fade_out_delay + fade_out_length); break; } } diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index 63d2c44dd9..3486dce081 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -5,9 +5,9 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osuTK; namespace osu.Game.Skinning { @@ -34,8 +34,12 @@ namespace osu.Game.Skinning animation?.GotoFrame(0); - this.RotateTo(0); - this.MoveTo(Vector2.Zero); + const double fade_in_length = 120; + const double fade_out_delay = 500; + const double fade_out_length = 600; + + this.FadeInFromZero(fade_in_length); + this.Delay(fade_out_delay).FadeOut(fade_out_length); // legacy judgements don't play any transforms if they are an animation. if (animation?.FrameCount > 1) @@ -47,20 +51,21 @@ namespace osu.Game.Skinning this.ScaleTo(1.6f); this.ScaleTo(1, 100, Easing.In); - this.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + float rotation = RNG.NextSingle(-8.6f, 8.6f); - this.RotateTo(40, 800, Easing.InQuint); + this.RotateTo(0); + this.RotateTo(rotation, fade_in_length) + .Then().RotateTo(rotation * 2, fade_out_delay + fade_out_length - fade_in_length, Easing.In); break; default: - const double animation_length = 120; this.ScaleTo(0.6f).Then() - .ScaleTo(1.1f, animation_length * 0.8f).Then() + .ScaleTo(1.1f, fade_in_length * 0.8f).Then() // this is actually correct to match stable; there were overlapping transforms. - .ScaleTo(0.9f).Delay(animation_length * 0.2f) - .ScaleTo(1.1f).ScaleTo(0.9f, animation_length * 0.2f).Then() - .ScaleTo(0.95f).ScaleTo(finalScale, animation_length * 0.2f); + .ScaleTo(0.9f).Delay(fade_in_length * 0.2f) + .ScaleTo(1.1f).ScaleTo(0.9f, fade_in_length * 0.2f).Then() + .ScaleTo(0.95f).ScaleTo(finalScale, fade_in_length * 0.2f); break; } } diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index ce1a736f0a..9fbf3de043 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -373,11 +373,11 @@ namespace osu.Game.Skinning case GameplaySkinComponent resultComponent: Func createDrawable = () => getJudgementAnimation(resultComponent.Component); + // kind of wasteful that we throw this away, but should do for now. if (createDrawable() != null) { - var particles = getParticleTexture(resultComponent.Component); - if (particles != null) - return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleTexture(resultComponent.Component)); + if (Configuration.LegacyVersion > 1) + return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleDrawable(resultComponent.Component)); else return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable); } @@ -388,7 +388,7 @@ namespace osu.Game.Skinning return this.GetAnimation(component.LookupName, false, false); } - private Drawable getParticleTexture(HitResult result) + private Drawable getParticleDrawable(HitResult result) { switch (result) { From d017e725fb87a1c3b22dfe17809defc002925db7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 17:15:53 +0900 Subject: [PATCH 4749/6909] Add comment for future todo task --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index c81cfd97dd..9699fb72a4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -50,6 +50,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (hitLightingEnabled && Lighting.Drawable != null) { + // todo: this animation changes slightly based on new/old legacy skin versions. Lighting.ScaleTo(0.8f).ScaleTo(1.2f, 600, Easing.Out); Lighting.FadeIn(200).Then().Delay(200).FadeOut(1000); From ee8804b50b86e6d2bb85c18908f715fec0b599f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 17:53:27 +0900 Subject: [PATCH 4750/6909] Fix animation playback not running on skin change --- .../Rulesets/Judgements/DrawableJudgement.cs | 39 ++++++++++++++++--- 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index c0fcb1eb3c..85875e2f2a 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -28,8 +28,13 @@ namespace osu.Game.Rulesets.Judgements protected Container JudgementBody { get; private set; } + public override bool RemoveCompletedTransforms => false; + private SkinnableDrawable skinnableJudgement; + [Resolved] + private ISkinSource skinSource { get; set; } + /// /// Duration of initial fade in. /// @@ -65,6 +70,27 @@ namespace osu.Game.Rulesets.Judgements prepareDrawables(); } + protected override void LoadComplete() + { + base.LoadComplete(); + skinSource.SourceChanged += onSkinChanged; + } + + private void onSkinChanged() + { + // on a skin change, the child component will update but not get correctly triggered to play its animation. + // we need to trigger a reinitialisation to make things right. + currentDrawableType = null; + + PrepareForUse(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + skinSource.SourceChanged -= onSkinChanged; + } + /// /// Apply top-level animations to the current judgement when successfully hit. /// Generally used for fading, defaulting to a simple fade out based on . @@ -110,6 +136,12 @@ namespace osu.Game.Rulesets.Judgements prepareDrawables(); + runAnimation(); + } + + private void runAnimation() + { + ClearTransforms(true); LifetimeStart = Result.TimeAbsolute; using (BeginAbsoluteSequence(Result.TimeAbsolute, true)) @@ -135,18 +167,13 @@ namespace osu.Game.Rulesets.Judgements { var drawableAnimation = (Drawable)animatable; - drawableAnimation.ClearTransforms(); - animatable.PlayAnimation(); - // a derived version of DrawableJudgement may be adjusting lifetime. + // a derived version of DrawableJudgement may be proposing a lifetime. // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime. double lastTransformTime = drawableAnimation.LatestTransformEndTime; - if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd) - { LifetimeEnd = lastTransformTime; - } } } } From 1fd582d33341c396919d7f23920a18e55a00b982 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 19:12:29 +0900 Subject: [PATCH 4751/6909] Update lighting tests to not require lighting (some test skins are missing it) --- osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index 7ea2ef3a78..b0119edc14 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -43,10 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests showResult(HitResult.Great); AddUntilStep("judgements shown", () => this.ChildrenOfType().Any()); - AddAssert("judgement body immediately visible", - () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha == 1)); AddAssert("hit lighting hidden", - () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0)); + () => this.ChildrenOfType().All(judgement => !judgement.Lighting.Transforms.Any())); } [Test] @@ -57,10 +55,8 @@ namespace osu.Game.Rulesets.Osu.Tests showResult(HitResult.Great); AddUntilStep("judgements shown", () => this.ChildrenOfType().Any()); - AddAssert("judgement body not immediately visible", - () => this.ChildrenOfType().All(judgement => judgement.JudgementBody.Alpha > 0 && judgement.JudgementBody.Alpha < 1)); AddAssert("hit lighting shown", - () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha > 0)); + () => this.ChildrenOfType().Any(judgement => judgement.Lighting.Transforms.Any())); } private void showResult(HitResult result) From 9a3dd12f309d2cccb3b50dfc92d2369f62b0f21d Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Wed, 18 Nov 2020 18:15:56 +0800 Subject: [PATCH 4752/6909] Move to debug settings --- .../Overlays/Settings/Sections/Debug/GeneralSettings.cs | 9 ++++++++- osu.Game/Screens/Menu/ButtonSystem.cs | 5 ----- osu.Game/Screens/Menu/MainMenu.cs | 4 +--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs index f05b876d8f..22674f0879 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs @@ -4,6 +4,8 @@ using osu.Framework.Allocation; using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.Screens; +using osu.Game.Screens.Import; namespace osu.Game.Overlays.Settings.Sections.Debug { @@ -12,7 +14,7 @@ namespace osu.Game.Overlays.Settings.Sections.Debug protected override string Header => "General"; [BackgroundDependencyLoader] - private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig) + private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, OsuGame game) { Children = new Drawable[] { @@ -27,6 +29,11 @@ namespace osu.Game.Overlays.Settings.Sections.Debug Current = config.GetBindable(DebugSetting.BypassFrontToBackPass) } }; + Add(new SettingsButton + { + Text = "Import files", + Action = () => game?.PerformFromScreen(menu => menu.Push(new FileImportScreen())) + }); } } } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 1722329c21..4becdd58cd 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -41,7 +41,6 @@ namespace osu.Game.Screens.Menu public Action OnExit; public Action OnBeatmapListing; public Action OnSolo; - public Action OnImportButton; public Action OnSettings; public Action OnMulti; public Action OnChart; @@ -132,10 +131,6 @@ 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), () => OnBeatmapListing?.Invoke(), 0, Key.D)); - if(RuntimeInfo.OS == RuntimeInfo.Platform.Android) - { - buttonsTopLevel.Add(new Button(@"Import File", @"button-generic-select", FontAwesome.Solid.File, new Color4(0, 86, 73, 255), () => OnImportButton?.Invoke())); - } 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 69e4ea487e..c3ecd75963 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -19,7 +19,6 @@ using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Multi; using osu.Game.Screens.Select; -using osu.Game.Screens.Import; namespace osu.Game.Screens.Menu { @@ -106,7 +105,6 @@ namespace osu.Game.Screens.Menu }, OnSolo = onSolo, OnMulti = delegate { this.Push(new Multiplayer()); }, - OnImportButton = onImport, OnExit = confirmAndExit, } } @@ -146,7 +144,7 @@ namespace osu.Game.Screens.Menu [Resolved(canBeNull: true)] private OsuGame game { get; set; } - private void onImport() => this.Push(new FileImportScreen()); + private void confirmAndExit() { if (exitConfirmed) return; From 20bb64c627edd4043f6ff72886a2e91de506536c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 19:34:00 +0900 Subject: [PATCH 4753/6909] Fix mania misses not correctly animating (temporary solution) --- .../Skinning/TestSceneDrawableJudgement.cs | 5 +++- .../UI/DrawableManiaJudgement.cs | 28 +++++++++++++++++-- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index a4d4ec50f8..11536ac8c3 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -24,7 +24,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning if (hitWindows.IsHitResultAllowed(result)) { AddStep("Show " + result.GetDescription(), () => SetContents(() => - new DrawableManiaJudgement(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null) + new DrawableManiaJudgement(new JudgementResult(new HitObject + { + StartTime = Time.Current + }, new Judgement()) { Type = result }, null) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index a341cdd8ec..a3dcd0e57f 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -5,6 +5,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -19,6 +20,27 @@ namespace osu.Game.Rulesets.Mania.UI { } + protected override void ApplyMissAnimations() + { + if (!(JudgementBody.Drawable is DefaultManiaJudgementPiece)) + { + // this is temporary logic until mania's skin transformer returns IAnimatableJudgements + JudgementBody.ScaleTo(1.6f); + JudgementBody.ScaleTo(1, 100, Easing.In); + + JudgementBody.MoveTo(Vector2.Zero); + JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + JudgementBody.RotateTo(0); + JudgementBody.RotateTo(40, 800, Easing.InQuint); + JudgementBody.FadeOutFromOne(800); + + LifetimeEnd = JudgementBody.LatestTransformEndTime; + } + + base.ApplyMissAnimations(); + } + protected override void ApplyHitAnimations() { JudgementBody.ScaleTo(0.8f); @@ -29,11 +51,11 @@ namespace osu.Game.Rulesets.Mania.UI .FadeOut(200); } - protected override Drawable CreateDefaultJudgement(HitResult result) => new ManiaJudgementPiece(result); + protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); - private class ManiaJudgementPiece : DefaultJudgementPiece + private class DefaultManiaJudgementPiece : DefaultJudgementPiece { - public ManiaJudgementPiece(HitResult result) + public DefaultManiaJudgementPiece(HitResult result) : base(result) { } From 8522ddc61e4d9a795feb290a66f1f3be6a948dfb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 19:34:27 +0900 Subject: [PATCH 4754/6909] Reduce nesting of skinned component to reduce exposed surface --- .../TestSceneDrawableJudgement.cs | 2 +- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 15 +++++---------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index b0119edc14..1339d11ab6 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -108,7 +108,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestDrawableOsuJudgement : DrawableOsuJudgement { public new SkinnableSprite Lighting => base.Lighting; - public new Container JudgementBody => base.JudgementBody; + public new SkinnableDrawable JudgementBody => base.JudgementBody; } } } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 85875e2f2a..72ffa3a34b 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -6,7 +6,6 @@ using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; @@ -26,11 +25,9 @@ namespace osu.Game.Rulesets.Judgements public DrawableHitObject JudgedObject { get; private set; } - protected Container JudgementBody { get; private set; } - public override bool RemoveCompletedTransforms => false; - private SkinnableDrawable skinnableJudgement; + protected SkinnableDrawable JudgementBody { get; private set; } [Resolved] private ISkinSource skinSource { get; set; } @@ -147,7 +144,7 @@ namespace osu.Game.Rulesets.Judgements using (BeginAbsoluteSequence(Result.TimeAbsolute, true)) { // not sure if this should remain going forward. - skinnableJudgement.ResetAnimation(); + JudgementBody.ResetAnimation(); switch (Result.Type) { @@ -163,7 +160,7 @@ namespace osu.Game.Rulesets.Judgements break; } - if (skinnableJudgement.Drawable is IAnimatableJudgement animatable) + if (JudgementBody.Drawable is IAnimatableJudgement animatable) { var drawableAnimation = (Drawable)animatable; @@ -192,13 +189,11 @@ namespace osu.Game.Rulesets.Judgements if (JudgementBody != null) RemoveInternal(JudgementBody); - AddInternal(JudgementBody = new Container + AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent(type), _ => + CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Child = skinnableJudgement = new SkinnableDrawable(new GameplaySkinComponent(type), _ => - CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) }); currentDrawableType = type; From b3bec81b79b5fa88edbce4ab7b322b10a9fb0db0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 19:38:38 +0900 Subject: [PATCH 4755/6909] Update xmldoc to match new behaviour --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 72ffa3a34b..6407cb40aa 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -90,8 +90,7 @@ namespace osu.Game.Rulesets.Judgements /// /// Apply top-level animations to the current judgement when successfully hit. - /// Generally used for fading, defaulting to a simple fade out based on . - /// This will be used to calculate the lifetime of the judgement. + /// If displaying components which require lifetime extensions, manually adjusting is required. /// /// /// For animating the actual "default skin" judgement itself, it is recommended to use . @@ -103,8 +102,7 @@ namespace osu.Game.Rulesets.Judgements /// /// Apply top-level animations to the current judgement when missed. - /// Generally used for fading, defaulting to a simple fade out based on . - /// This will be used to calculate the lifetime of the judgement. + /// If displaying components which require lifetime extensions, manually adjusting is required. /// /// /// For animating the actual "default skin" judgement itself, it is recommended to use . From 191b95810ce519e6b9b3990ae105867b113f753a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 18 Nov 2020 19:47:35 +0900 Subject: [PATCH 4756/6909] Fix whitespace issues --- .../Skinning/TestSceneDrawableJudgement.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs index 11536ac8c3..dcb25f21ba 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneDrawableJudgement.cs @@ -24,10 +24,10 @@ namespace osu.Game.Rulesets.Mania.Tests.Skinning if (hitWindows.IsHitResultAllowed(result)) { AddStep("Show " + result.GetDescription(), () => SetContents(() => - new DrawableManiaJudgement(new JudgementResult(new HitObject + new DrawableManiaJudgement(new JudgementResult(new HitObject { StartTime = Time.Current }, new Judgement()) { - StartTime = Time.Current - }, new Judgement()) { Type = result }, null) + Type = result + }, null) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From cb5d1d0d77f1dbf87a01d113074dcdb1f31e7b57 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 18 Nov 2020 21:26:35 +0900 Subject: [PATCH 4757/6909] Remove obsolete method --- osu.Game/Rulesets/Objects/SliderEventGenerator.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs index d8c6da86f9..ba38c7f77d 100644 --- a/osu.Game/Rulesets/Objects/SliderEventGenerator.cs +++ b/osu.Game/Rulesets/Objects/SliderEventGenerator.cs @@ -10,14 +10,6 @@ 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) - { - 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 85c5c68dfac097bc15c9218853de000816b74f58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 13:20:21 +0900 Subject: [PATCH 4758/6909] Provide particle drawable as a function (for future use) --- osu.Game/Skinning/LegacyJudgementPieceNew.cs | 2 +- osu.Game/Skinning/LegacySkin.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs index d0a2f1d4eb..b5e1de337a 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceNew.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceNew.cs @@ -20,7 +20,7 @@ namespace osu.Game.Skinning private readonly Drawable mainPiece; - public LegacyJudgementPieceNew(HitResult result, Func createMainDrawable, Drawable particleDrawable) + public LegacyJudgementPieceNew(HitResult result, Func createMainDrawable, Func createParticleDrawable) { this.result = result; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 9fbf3de043..6faee8c2e7 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -377,7 +377,7 @@ namespace osu.Game.Skinning if (createDrawable() != null) { if (Configuration.LegacyVersion > 1) - return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleDrawable(resultComponent.Component)); + return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, () => getParticleDrawable(resultComponent.Component)); else return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable); } From ba735584faac6f0c5ab081cbd1fed06b3b0ec7ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 14:04:43 +0900 Subject: [PATCH 4759/6909] Add null check for disposal safety --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 6407cb40aa..4aed38e689 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -85,7 +85,9 @@ namespace osu.Game.Rulesets.Judgements protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - skinSource.SourceChanged -= onSkinChanged; + + if (skinSource != null) + skinSource.SourceChanged -= onSkinChanged; } /// From 3024ae6d867b4eae0014aaba8d2ed7dbf0470bc5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 14:10:07 +0900 Subject: [PATCH 4760/6909] Add better test coverage of hit lighting (and ensure reset after animation reapplication) --- osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs | 7 +++---- .../Objects/Drawables/DrawableOsuJudgement.cs | 2 ++ 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs index 1339d11ab6..e4158d8f07 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneDrawableJudgement.cs @@ -43,8 +43,8 @@ namespace osu.Game.Rulesets.Osu.Tests showResult(HitResult.Great); AddUntilStep("judgements shown", () => this.ChildrenOfType().Any()); - AddAssert("hit lighting hidden", - () => this.ChildrenOfType().All(judgement => !judgement.Lighting.Transforms.Any())); + AddAssert("hit lighting has no transforms", () => this.ChildrenOfType().All(judgement => !judgement.Lighting.Transforms.Any())); + AddAssert("hit lighting hidden", () => this.ChildrenOfType().All(judgement => judgement.Lighting.Alpha == 0)); } [Test] @@ -55,8 +55,7 @@ namespace osu.Game.Rulesets.Osu.Tests showResult(HitResult.Great); AddUntilStep("judgements shown", () => this.ChildrenOfType().Any()); - AddAssert("hit lighting shown", - () => this.ChildrenOfType().Any(judgement => judgement.Lighting.Transforms.Any())); + AddUntilStep("hit lighting shown", () => this.ChildrenOfType().Any(judgement => judgement.Lighting.Alpha > 0)); } private void showResult(HitResult result) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs index 9699fb72a4..13f5960bd4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -48,6 +48,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { bool hitLightingEnabled = config.Get(OsuSetting.HitLighting); + Lighting.Alpha = 0; + if (hitLightingEnabled && Lighting.Drawable != null) { // todo: this animation changes slightly based on new/old legacy skin versions. From 9df93e1f186d4fa592b549ba6b1a15a4112847c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 14:54:31 +0900 Subject: [PATCH 4761/6909] Add basic implementation of particle explosion Using drawables still, just to get things in place and setup the structure --- .../Gameplay/TestSceneParticleExplosion.cs | 30 ++++++++++ osu.Game/Graphics/ParticleExplosion.cs | 59 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs create mode 100644 osu.Game/Graphics/ParticleExplosion.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs new file mode 100644 index 0000000000..6df4842608 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.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.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Textures; +using osu.Game.Graphics; +using osuTK; + +namespace osu.Game.Tests.Visual.Gameplay +{ + [TestFixture] + public class TestSceneParticleExplosion : OsuTestScene + { + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + AddStep(@"display", () => + { + Child = new ParticleExplosion(textures.Get("Cursor/cursortrail"), 150, 1200) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200) + }; + }); + } + } +} diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs new file mode 100644 index 0000000000..6a6f947dd5 --- /dev/null +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Framework.Utils; +using osuTK; + +namespace osu.Game.Graphics +{ + public class ParticleExplosion : CompositeDrawable + { + public ParticleExplosion(Texture texture, int particleCount, double duration) + { + for (int i = 0; i < particleCount; i++) + { + double rDuration = RNG.NextDouble(duration / 3, duration); + + AddInternal(new Particle(rDuration, RNG.NextSingle(0, MathF.PI * 2)) + { + Texture = texture + }); + } + } + + private class Particle : Sprite + { + private readonly double duration; + private readonly float direction; + + private Vector2 positionForOffset(float offset) => new Vector2( + (float)(offset * Math.Sin(direction)), + (float)(offset * Math.Cos(direction)) + ); + + public Particle(double duration, float direction) + { + this.duration = duration; + this.direction = direction; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + RelativePositionAxes = Axes.Both; + Position = positionForOffset(0); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + this.MoveTo(positionForOffset(1), duration); + this.FadeOut(duration); + Expire(); + } + } + } +} From 9d04ce75ccba4d263a40e961552eed793a8348ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 15:47:02 +0900 Subject: [PATCH 4762/6909] Make particles additive and consume in judgement explosions --- osu.Game/Graphics/ParticleExplosion.cs | 24 ++++++++++++++++---- osu.Game/Skinning/LegacyJudgementPieceNew.cs | 24 +++++++++++++++++++- osu.Game/Skinning/LegacySkin.cs | 10 ++++---- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index 6a6f947dd5..4daae34d62 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.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.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -26,11 +27,19 @@ namespace osu.Game.Graphics } } + public void Restart() + { + foreach (var p in InternalChildren.OfType()) + p.Play(); + } + private class Particle : Sprite { private readonly double duration; private readonly float direction; + public override bool RemoveWhenNotAlive => false; + private Vector2 positionForOffset(float offset) => new Vector2( (float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction)) @@ -40,18 +49,25 @@ namespace osu.Game.Graphics { this.duration = duration; this.direction = direction; - Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Blending = BlendingParameters.Additive; + RelativePositionAxes = Axes.Both; - Position = positionForOffset(0); } protected override void LoadComplete() { base.LoadComplete(); + Play(); + } - this.MoveTo(positionForOffset(1), duration); - this.FadeOut(duration); + public void Play() + { + this.MoveTo(new Vector2(0.5f)); + this.MoveTo(new Vector2(0.5f) + positionForOffset(0.5f), duration); + + this.FadeOutFromOne(duration); Expire(); } } diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs index b5e1de337a..2a53820872 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceNew.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceNew.cs @@ -5,7 +5,9 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Textures; using osu.Framework.Utils; +using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; using osuTK; @@ -20,7 +22,9 @@ namespace osu.Game.Skinning private readonly Drawable mainPiece; - public LegacyJudgementPieceNew(HitResult result, Func createMainDrawable, Func createParticleDrawable) + private readonly ParticleExplosion particles; + + public LegacyJudgementPieceNew(HitResult result, Func createMainDrawable, Texture particleTexture) { this.result = result; @@ -36,6 +40,17 @@ namespace osu.Game.Skinning }) }; + if (particleTexture != null) + { + AddInternal(particles = new ParticleExplosion(particleTexture, 150, 1600) + { + Size = new Vector2(140), + Depth = float.MaxValue, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }); + } + if (result != HitResult.Miss) { //new judgement shows old as a temporary effect @@ -54,6 +69,13 @@ namespace osu.Game.Skinning animation?.GotoFrame(0); + if (particles != null) + { + // start the particles already some way into their animation to break cluster away from centre. + using (particles.BeginDelayedSequence(-100, true)) + particles.Restart(); + } + const double fade_in_length = 120; const double fade_out_delay = 500; const double fade_out_length = 600; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 6faee8c2e7..63a22eba62 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -377,7 +377,7 @@ namespace osu.Game.Skinning if (createDrawable() != null) { if (Configuration.LegacyVersion > 1) - return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, () => getParticleDrawable(resultComponent.Component)); + return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleTexture(resultComponent.Component)); else return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable); } @@ -388,18 +388,18 @@ namespace osu.Game.Skinning return this.GetAnimation(component.LookupName, false, false); } - private Drawable getParticleDrawable(HitResult result) + private Texture getParticleTexture(HitResult result) { switch (result) { case HitResult.Meh: - return this.GetAnimation("particle50", false, false); + return GetTexture("particle50"); case HitResult.Ok: - return this.GetAnimation("particle100", false, false); + return GetTexture("particle100"); case HitResult.Great: - return this.GetAnimation("particle300", false, false); + return GetTexture("particle300"); } return null; From efd5acb8abb0e73a9e5ba632d592d485dc1f2443 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 15:55:11 +0900 Subject: [PATCH 4763/6909] Randomise direction every animation playback --- osu.Game/Graphics/ParticleExplosion.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index 4daae34d62..200bcc062a 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -20,7 +20,7 @@ namespace osu.Game.Graphics { double rDuration = RNG.NextDouble(duration / 3, duration); - AddInternal(new Particle(rDuration, RNG.NextSingle(0, MathF.PI * 2)) + AddInternal(new Particle(rDuration) { Texture = texture }); @@ -36,19 +36,12 @@ namespace osu.Game.Graphics private class Particle : Sprite { private readonly double duration; - private readonly float direction; public override bool RemoveWhenNotAlive => false; - private Vector2 positionForOffset(float offset) => new Vector2( - (float)(offset * Math.Sin(direction)), - (float)(offset * Math.Cos(direction)) - ); - - public Particle(double duration, float direction) + public Particle(double duration) { this.duration = duration; - this.direction = direction; Origin = Anchor.Centre; Blending = BlendingParameters.Additive; @@ -64,11 +57,18 @@ namespace osu.Game.Graphics public void Play() { + double direction = RNG.NextSingle(0, MathF.PI * 2); + this.MoveTo(new Vector2(0.5f)); this.MoveTo(new Vector2(0.5f) + positionForOffset(0.5f), duration); this.FadeOutFromOne(duration); Expire(); + + Vector2 positionForOffset(float offset) => new Vector2( + (float)(offset * Math.Sin(direction)), + (float)(offset * Math.Cos(direction)) + ); } } } From 83024f1ec524db962d88645c1e6fe5be9623a594 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 16:00:20 +0900 Subject: [PATCH 4764/6909] Add back positional randomness from stable --- osu.Game/Graphics/ParticleExplosion.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index 200bcc062a..8a3be513be 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -60,7 +60,7 @@ namespace osu.Game.Graphics double direction = RNG.NextSingle(0, MathF.PI * 2); this.MoveTo(new Vector2(0.5f)); - this.MoveTo(new Vector2(0.5f) + positionForOffset(0.5f), duration); + this.MoveTo(new Vector2(0.5f) + positionForOffset(RNG.NextSingle(0.5f)), duration); this.FadeOutFromOne(duration); Expire(); From fe025043bd59d8164710cce507c16589ad439c98 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 17:16:29 +0900 Subject: [PATCH 4765/6909] Make test run multiple times --- .../Visual/Gameplay/TestSceneParticleExplosion.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs index 6df4842608..63c8757afd 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs @@ -16,15 +16,15 @@ namespace osu.Game.Tests.Visual.Gameplay [BackgroundDependencyLoader] private void load(TextureStore textures) { - AddStep(@"display", () => + AddRepeatStep(@"display", () => { Child = new ParticleExplosion(textures.Get("Cursor/cursortrail"), 150, 1200) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(200) + Size = new Vector2(400) }; - }); + }, 10); } } } From 476d0256ccc0d7620d84316fc7f3bb2657e4b2d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 17:22:37 +0900 Subject: [PATCH 4766/6909] Replace particle explosion implementation with DrawNode version --- osu.Game/Graphics/ParticleExplosion.cs | 128 ++++++++++++++++++------- 1 file changed, 96 insertions(+), 32 deletions(-) diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index 8a3be513be..ba6d26ec22 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -2,9 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; +using System.Collections.Generic; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.OpenGL.Vertices; +using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Utils; @@ -12,64 +13,127 @@ using osuTK; namespace osu.Game.Graphics { - public class ParticleExplosion : CompositeDrawable + public class ParticleExplosion : Sprite { + private readonly double duration; + private double startTime; + + private readonly List parts = new List(); + public ParticleExplosion(Texture texture, int particleCount, double duration) { - for (int i = 0; i < particleCount; i++) - { - double rDuration = RNG.NextDouble(duration / 3, duration); + Texture = texture; + this.duration = duration; + Blending = BlendingParameters.Additive; - AddInternal(new Particle(rDuration) - { - Texture = texture - }); - } + for (int i = 0; i < particleCount; i++) + parts.Add(new ParticlePart(duration)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Restart(); } public void Restart() { - foreach (var p in InternalChildren.OfType()) - p.Play(); + startTime = TransformStartTime; + + this.FadeOutFromOne(duration); + + foreach (var p in parts) + p.Randomise(); } - private class Particle : Sprite + protected override void Update() { - private readonly double duration; + base.Update(); - public override bool RemoveWhenNotAlive => false; + Invalidate(Invalidation.DrawNode); + } - public Particle(double duration) + protected override DrawNode CreateDrawNode() => new ParticleExplosionDrawNode(this); + + private class ParticleExplosionDrawNode : SpriteDrawNode + { + private List parts = new List(); + + private ParticleExplosion source => (ParticleExplosion)Source; + + private double startTime; + private double currentTime; + private Vector2 sourceSize; + + public ParticleExplosionDrawNode(Sprite source) + : base(source) { - this.duration = duration; - - Origin = Anchor.Centre; - Blending = BlendingParameters.Additive; - - RelativePositionAxes = Axes.Both; } - protected override void LoadComplete() + public override void ApplyState() { - base.LoadComplete(); - Play(); + base.ApplyState(); + + parts = source.parts; + sourceSize = source.Size; + startTime = source.startTime; + currentTime = source.Time.Current; } - public void Play() + protected override void Blit(Action vertexAction) { - double direction = RNG.NextSingle(0, MathF.PI * 2); + foreach (var p in parts) + { + var pos = p.PositionAtTime(currentTime - startTime); - this.MoveTo(new Vector2(0.5f)); - this.MoveTo(new Vector2(0.5f) + positionForOffset(RNG.NextSingle(0.5f)), duration); + // todo: implement per particle. + var rect = new RectangleF(pos.X * sourceSize.X, pos.Y * sourceSize.Y, Texture.DisplayWidth, Texture.DisplayHeight); - this.FadeOutFromOne(duration); - Expire(); + var quad = new Quad( + Vector2Extensions.Transform(rect.TopLeft, DrawInfo.Matrix), + Vector2Extensions.Transform(rect.TopRight, DrawInfo.Matrix), + Vector2Extensions.Transform(rect.BottomLeft, DrawInfo.Matrix), + Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix) + ); + + DrawQuad(Texture, quad, DrawColourInfo.Colour, null, vertexAction, + new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), + null, TextureCoords); + } + } + } + + private class ParticlePart + { + private readonly double totalDuration; + + private double duration; + private double direction; + private float distance; + + public ParticlePart(double totalDuration) + { + this.totalDuration = totalDuration; + + Randomise(); + } + + public Vector2 PositionAtTime(double time) + { + return new Vector2(0.5f) + positionForOffset(distance * (float)(time / duration)); Vector2 positionForOffset(float offset) => new Vector2( (float)(offset * Math.Sin(direction)), (float)(offset * Math.Cos(direction)) ); } + + public void Randomise() + { + distance = RNG.NextSingle(0.5f); + duration = RNG.NextDouble(totalDuration / 3, totalDuration); + direction = RNG.NextSingle(0, MathF.PI * 2); + } } } } From 3a7291c5cf699bfc4e83653a44ced17aa86a3f53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 17:56:11 +0900 Subject: [PATCH 4767/6909] Fix some behavioural regressions --- osu.Game/Graphics/ParticleExplosion.cs | 42 +++++++++++++++++--------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index ba6d26ec22..68dedf70a0 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -39,7 +39,6 @@ namespace osu.Game.Graphics public void Restart() { startTime = TransformStartTime; - this.FadeOutFromOne(duration); foreach (var p in parts) @@ -74,6 +73,8 @@ namespace osu.Game.Graphics { base.ApplyState(); + // this is mostly safe as the parts are immutable. + // the most that can go wrong is the random state be incorrect parts = source.parts; sourceSize = source.Size; startTime = source.startTime; @@ -82,13 +83,20 @@ namespace osu.Game.Graphics protected override void Blit(Action vertexAction) { + var time = currentTime - startTime; + foreach (var p in parts) { - var pos = p.PositionAtTime(currentTime - startTime); + Vector2 pos = p.PositionAtTime(time); + float alpha = p.AlphaAtTime(time); - // todo: implement per particle. - var rect = new RectangleF(pos.X * sourceSize.X, pos.Y * sourceSize.Y, Texture.DisplayWidth, Texture.DisplayHeight); + var rect = new RectangleF( + pos.X * sourceSize.X - Texture.DisplayWidth / 2, + pos.Y * sourceSize.Y - Texture.DisplayHeight / 2, + Texture.DisplayWidth, + Texture.DisplayHeight); + // convert to screen space. var quad = new Quad( Vector2Extensions.Transform(rect.TopLeft, DrawInfo.Matrix), Vector2Extensions.Transform(rect.TopRight, DrawInfo.Matrix), @@ -96,7 +104,7 @@ namespace osu.Game.Graphics Vector2Extensions.Transform(rect.BottomRight, DrawInfo.Matrix) ); - DrawQuad(Texture, quad, DrawColourInfo.Colour, null, vertexAction, + DrawQuad(Texture, quad, DrawColourInfo.Colour.MultiplyAlpha(alpha), null, vertexAction, new Vector2(InflationAmount.X / DrawRectangle.Width, InflationAmount.Y / DrawRectangle.Height), null, TextureCoords); } @@ -105,22 +113,31 @@ namespace osu.Game.Graphics private class ParticlePart { - private readonly double totalDuration; + private readonly double availableDuration; private double duration; private double direction; private float distance; - public ParticlePart(double totalDuration) + public ParticlePart(double availableDuration) { - this.totalDuration = totalDuration; + this.availableDuration = availableDuration; Randomise(); } + public void Randomise() + { + distance = RNG.NextSingle(0.5f); + duration = RNG.NextDouble(availableDuration / 3, availableDuration); + direction = RNG.NextSingle(0, MathF.PI * 2); + } + + public float AlphaAtTime(double time) => 1 - progressAtTime(time); + public Vector2 PositionAtTime(double time) { - return new Vector2(0.5f) + positionForOffset(distance * (float)(time / duration)); + return new Vector2(0.5f) + positionForOffset(distance * progressAtTime(time)); Vector2 positionForOffset(float offset) => new Vector2( (float)(offset * Math.Sin(direction)), @@ -128,12 +145,7 @@ namespace osu.Game.Graphics ); } - public void Randomise() - { - distance = RNG.NextSingle(0.5f); - duration = RNG.NextDouble(totalDuration / 3, totalDuration); - direction = RNG.NextSingle(0, MathF.PI * 2); - } + private float progressAtTime(double time) => (float)Math.Clamp(time / duration, 0, 1); } } } From 84e73e88d57c3359be76e905860e20f31122291d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 18:05:41 +0900 Subject: [PATCH 4768/6909] Use structs for parts for added safety --- osu.Game/Graphics/ParticleExplosion.cs | 51 +++++++++++--------------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index 68dedf70a0..fa55260f53 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -13,8 +13,12 @@ using osuTK; namespace osu.Game.Graphics { + /// + /// An explosion of textured particles based on how osu-stable randomises the explosion pattern. + /// public class ParticleExplosion : Sprite { + private readonly int particleCount; private readonly double duration; private double startTime; @@ -23,11 +27,9 @@ namespace osu.Game.Graphics public ParticleExplosion(Texture texture, int particleCount, double duration) { Texture = texture; + this.particleCount = particleCount; this.duration = duration; Blending = BlendingParameters.Additive; - - for (int i = 0; i < particleCount; i++) - parts.Add(new ParticlePart(duration)); } protected override void LoadComplete() @@ -36,19 +38,23 @@ namespace osu.Game.Graphics Restart(); } + /// + /// Restart the animation from the current point in time. + /// Supports transform time offset chaining. + /// public void Restart() { startTime = TransformStartTime; this.FadeOutFromOne(duration); - foreach (var p in parts) - p.Randomise(); + parts.Clear(); + for (int i = 0; i < particleCount; i++) + parts.Add(new ParticlePart(duration)); } protected override void Update() { base.Update(); - Invalidate(Invalidation.DrawNode); } @@ -56,7 +62,7 @@ namespace osu.Game.Graphics private class ParticleExplosionDrawNode : SpriteDrawNode { - private List parts = new List(); + private readonly List parts = new List(); private ParticleExplosion source => (ParticleExplosion)Source; @@ -73,9 +79,9 @@ namespace osu.Game.Graphics { base.ApplyState(); - // this is mostly safe as the parts are immutable. - // the most that can go wrong is the random state be incorrect - parts = source.parts; + parts.Clear(); + parts.AddRange(source.parts); + sourceSize = source.Size; startTime = source.startTime; currentTime = source.Time.Current; @@ -111,22 +117,13 @@ namespace osu.Game.Graphics } } - private class ParticlePart + private readonly struct ParticlePart { - private readonly double availableDuration; - - private double duration; - private double direction; - private float distance; + private readonly double duration; + private readonly double direction; + private readonly float distance; public ParticlePart(double availableDuration) - { - this.availableDuration = availableDuration; - - Randomise(); - } - - public void Randomise() { distance = RNG.NextSingle(0.5f); duration = RNG.NextDouble(availableDuration / 3, availableDuration); @@ -137,12 +134,8 @@ namespace osu.Game.Graphics public Vector2 PositionAtTime(double time) { - return new Vector2(0.5f) + positionForOffset(distance * progressAtTime(time)); - - Vector2 positionForOffset(float offset) => new Vector2( - (float)(offset * Math.Sin(direction)), - (float)(offset * Math.Cos(direction)) - ); + var travelledDistance = distance * progressAtTime(time); + return new Vector2(0.5f) + travelledDistance * new Vector2((float)Math.Sin(direction), (float)Math.Cos(direction)); } private float progressAtTime(double time) => (float)Math.Clamp(time / duration, 0, 1); From dd5b90cf6cde83e0b59f282f0bdb1aa75e30a6d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 18:07:41 +0900 Subject: [PATCH 4769/6909] Add test coverage of animation restarting --- .../Visual/Gameplay/TestSceneParticleExplosion.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs index 63c8757afd..82095cb809 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneParticleExplosion.cs @@ -13,17 +13,26 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneParticleExplosion : OsuTestScene { + private ParticleExplosion explosion; + [BackgroundDependencyLoader] private void load(TextureStore textures) { - AddRepeatStep(@"display", () => + AddStep("create initial", () => { - Child = new ParticleExplosion(textures.Get("Cursor/cursortrail"), 150, 1200) + Child = explosion = new ParticleExplosion(textures.Get("Cursor/cursortrail"), 150, 1200) { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(400) }; + }); + + AddWaitStep("wait for playback", 5); + + AddRepeatStep(@"restart animation", () => + { + explosion.Restart(); }, 10); } } From 1c7ee2ca5fdaa87f2ec487bbe92466dce16560b9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 19 Nov 2020 18:46:19 +0900 Subject: [PATCH 4770/6909] Simplify math by making direction a float --- osu.Game/Graphics/ParticleExplosion.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/ParticleExplosion.cs b/osu.Game/Graphics/ParticleExplosion.cs index fa55260f53..e0d2b50c55 100644 --- a/osu.Game/Graphics/ParticleExplosion.cs +++ b/osu.Game/Graphics/ParticleExplosion.cs @@ -120,7 +120,7 @@ namespace osu.Game.Graphics private readonly struct ParticlePart { private readonly double duration; - private readonly double direction; + private readonly float direction; private readonly float distance; public ParticlePart(double availableDuration) @@ -135,7 +135,7 @@ namespace osu.Game.Graphics public Vector2 PositionAtTime(double time) { var travelledDistance = distance * progressAtTime(time); - return new Vector2(0.5f) + travelledDistance * new Vector2((float)Math.Sin(direction), (float)Math.Cos(direction)); + return new Vector2(0.5f) + travelledDistance * new Vector2(MathF.Sin(direction), MathF.Cos(direction)); } private float progressAtTime(double time) => (float)Math.Clamp(time / duration, 0, 1); From 730b14b5bb102572c742b769e168c48febd9791c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 19:51:09 +0900 Subject: [PATCH 4771/6909] Add initial hit sample pooling --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 13 +- osu.Game/Audio/HitSampleInfo.cs | 15 +- osu.Game/Audio/SampleInfo.cs | 15 +- .../Objects/Drawables/DrawableHitObject.cs | 12 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 29 ++- osu.Game/Rulesets/UI/Playfield.cs | 38 +++- osu.Game/Skinning/PausableSkinnableSound.cs | 8 +- osu.Game/Skinning/SkinReloadableDrawable.cs | 4 +- osu.Game/Skinning/SkinnableSound.cs | 203 +++++++++++++++--- .../Drawables/DrawableStoryboardSample.cs | 4 +- 10 files changed, 283 insertions(+), 58 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 4ecfb7b16d..d61a88b7df 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Game.Audio; using osu.Game.Rulesets.Catch.Judgements; @@ -26,11 +27,17 @@ namespace osu.Game.Rulesets.Catch.Objects Samples = samples; } - private class BananaHitSampleInfo : HitSampleInfo + private class BananaHitSampleInfo : HitSampleInfo, IEquatable { - private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; + private static readonly string[] lookup_names = { "metronomelow", "catch-banana" }; - public override IEnumerable LookupNames => lookupNames; + public override IEnumerable LookupNames => lookup_names; + + public bool Equals(BananaHitSampleInfo other) => true; + + public override bool Equals(object obj) => obj is BananaHitSampleInfo other && Equals(other); + + public override int GetHashCode() => lookup_names.GetHashCode(); } } } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 8efaeb3795..46f0abd7b7 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; namespace osu.Game.Audio { @@ -10,7 +11,7 @@ namespace osu.Game.Audio /// Describes a gameplay hit sample. /// [Serializable] - public class HitSampleInfo : ISampleInfo + public class HitSampleInfo : ISampleInfo, IEquatable { public const string HIT_WHISTLE = @"hitwhistle"; public const string HIT_FINISH = @"hitfinish"; @@ -57,5 +58,17 @@ namespace osu.Game.Audio } public HitSampleInfo Clone() => (HitSampleInfo)MemberwiseClone(); + + public bool Equals(HitSampleInfo other) + => other != null && Bank == other.Bank && Name == other.Name && Suffix == other.Suffix; + + public override bool Equals(object obj) + => obj is HitSampleInfo other && Equals(other); + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] // This will have to be addressed eventually + public override int GetHashCode() + { + return HashCode.Combine(Bank, Name, Suffix); + } } } diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index 240d70c418..221bc31639 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.cs @@ -1,14 +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; namespace osu.Game.Audio { /// /// Describes a gameplay sample. /// - public class SampleInfo : ISampleInfo + public class SampleInfo : ISampleInfo, IEquatable { private readonly string[] sampleNames; @@ -20,5 +22,16 @@ namespace osu.Game.Audio public IEnumerable LookupNames => sampleNames; public int Volume { get; } = 100; + + public override int GetHashCode() + { + return HashCode.Combine(sampleNames, Volume); + } + + public bool Equals(SampleInfo other) + => other != null && sampleNames.SequenceEqual(other.sampleNames); + + public override bool Equals(object obj) + => obj is SampleInfo other && Equals(other); } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ca49ed9e75..0d97066b35 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -10,7 +10,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; using osu.Framework.Logging; using osu.Framework.Threading; @@ -139,8 +138,6 @@ namespace osu.Game.Rulesets.Objects.Drawables [Resolved(CanBeNull = true)] private IPooledHitObjectProvider pooledObjectProvider { get; set; } - private Container samplesContainer; - /// /// Creates a new . /// @@ -159,7 +156,7 @@ namespace osu.Game.Rulesets.Objects.Drawables config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); // Explicit non-virtual function call. - base.AddInternal(samplesContainer = new Container { RelativeSizeAxes = Axes.Both }); + base.AddInternal(Samples = new PausableSkinnableSound(Array.Empty())); } protected override void LoadAsyncComplete() @@ -269,6 +266,8 @@ namespace osu.Game.Rulesets.Objects.Drawables // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). samplesBindable.CollectionChanged -= onSamplesChanged; + Samples.Samples = Array.Empty(); + if (nestedHitObjects.IsValueCreated) { foreach (var obj in nestedHitObjects.Value) @@ -335,8 +334,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual void LoadSamples() { - samplesContainer.Clear(); - Samples = null; + Samples.Samples = Array.Empty(); var samples = GetSamples().ToArray(); @@ -349,7 +347,7 @@ namespace osu.Game.Rulesets.Objects.Drawables + $" This is an indication that {nameof(HitObject.ApplyDefaults)} has not been invoked on {this}."); } - samplesContainer.Add(Samples = new PausableSkinnableSound(samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)))); + Samples.Samples = samples.Select(s => HitObject.SampleControlPoint.ApplyTo(s)).Cast().ToArray(); } private void onSamplesChanged(object sender, NotifyCollectionChangedEventArgs e) => LoadSamples(); diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 44b22033dc..19d573a55a 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -5,6 +5,7 @@ using osuTK; using osu.Game.Rulesets.Objects.Types; using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.IO; using osu.Game.Beatmaps.Formats; using osu.Game.Audio; @@ -500,7 +501,7 @@ namespace osu.Game.Rulesets.Objects.Legacy public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); } - public class LegacyHitSampleInfo : HitSampleInfo + public class LegacyHitSampleInfo : HitSampleInfo, IEquatable { private int customSampleBank; @@ -524,9 +525,21 @@ namespace osu.Game.Rulesets.Objects.Legacy /// using the skin config option. /// public bool IsLayered { get; set; } + + public bool Equals(LegacyHitSampleInfo other) + => other != null && base.Equals(other) && CustomSampleBank == other.CustomSampleBank; + + public override bool Equals(object obj) + => obj is LegacyHitSampleInfo other && Equals(other); + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] // This will have to be addressed eventually + public override int GetHashCode() + { + return HashCode.Combine(base.GetHashCode(), customSampleBank); + } } - private class FileHitSampleInfo : LegacyHitSampleInfo + private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable { public string Filename; @@ -542,6 +555,18 @@ namespace osu.Game.Rulesets.Objects.Legacy Filename, Path.ChangeExtension(Filename, null) }; + + public bool Equals(FileHitSampleInfo other) + => other != null && Filename == other.Filename; + + public override bool Equals(object obj) + => obj is FileHitSampleInfo other && Equals(other); + + [SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")] // This will have to be addressed eventually + public override int GetHashCode() + { + return HashCode.Combine(Filename); + } } } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 82ec653f31..9f3a4c508f 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -8,19 +8,23 @@ using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; +using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; +using osu.Game.Audio; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.UI { [Cached(typeof(IPooledHitObjectProvider))] - public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider + [Cached(typeof(IPooledSampleProvider))] + public abstract class Playfield : CompositeDrawable, IPooledHitObjectProvider, IPooledSampleProvider { /// /// Invoked when a is judged. @@ -80,6 +84,12 @@ namespace osu.Game.Rulesets.UI /// public readonly BindableBool DisplayJudgements = new BindableBool(true); + [Resolved(CanBeNull = true)] + private IReadOnlyList mods { get; set; } + + [Resolved] + private ISampleStore sampleStore { get; set; } + /// /// Creates a new . /// @@ -96,9 +106,6 @@ namespace osu.Game.Rulesets.UI })); } - [Resolved(CanBeNull = true)] - private IReadOnlyList mods { get; set; } - [BackgroundDependencyLoader] private void load() { @@ -336,6 +343,29 @@ namespace osu.Game.Rulesets.UI }); } + private readonly Dictionary> samplePools = new Dictionary>(); + + public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) + { + if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) + samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 5); + + return existingPool.Get(); + } + + private class DrawableSamplePool : DrawablePool + { + private readonly ISampleInfo sampleInfo; + + public DrawableSamplePool(ISampleInfo sampleInfo, int initialSize, int? maximumSize = null) + : base(initialSize, maximumSize) + { + this.sampleInfo = sampleInfo; + } + + protected override PoolableSkinnableSample CreateNewDrawable() => base.CreateNewDrawable().With(d => d.Apply(sampleInfo)); + } + #endregion #region Editor logic diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 4f09aec0b6..758b784649 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -14,13 +14,13 @@ namespace osu.Game.Skinning { protected bool RequestedPlaying { get; private set; } - public PausableSkinnableSound(ISampleInfo hitSamples) - : base(hitSamples) + public PausableSkinnableSound(ISampleInfo sample) + : base(sample) { } - public PausableSkinnableSound(IEnumerable hitSamples) - : base(hitSamples) + public PausableSkinnableSound(IEnumerable samples) + : base(samples) { } diff --git a/osu.Game/Skinning/SkinReloadableDrawable.cs b/osu.Game/Skinning/SkinReloadableDrawable.cs index cc9cbf7b59..50b4143375 100644 --- a/osu.Game/Skinning/SkinReloadableDrawable.cs +++ b/osu.Game/Skinning/SkinReloadableDrawable.cs @@ -27,7 +27,7 @@ namespace osu.Game.Skinning /// /// Whether fallback to default skin should be allowed if the custom skin is missing this resource. /// - private bool allowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin); + protected bool AllowDefaultFallback => allowFallback == null || allowFallback.Invoke(CurrentSkin); /// /// Create a new @@ -58,7 +58,7 @@ namespace osu.Game.Skinning private void skinChanged() { - SkinChanged(CurrentSkin, allowDefaultFallback); + SkinChanged(CurrentSkin, AllowDefaultFallback); OnSkinChanged?.Invoke(); } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index ffa0a963ce..8410b7eeae 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -1,26 +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.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; 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.Game.Audio; namespace osu.Game.Skinning { - public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent + public interface IPooledSampleProvider { - private readonly ISampleInfo[] hitSamples; + [CanBeNull] + PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); + } + + public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent + { + private ISampleInfo sampleInfo; + private DrawableSample sample; [Resolved] - private ISampleStore samples { get; set; } + private ISampleStore sampleStore { get; set; } + [Cached] + private readonly AudioAdjustments adjustments = new AudioAdjustments(); + + public PoolableSkinnableSample() + { + } + + public PoolableSkinnableSample(ISampleInfo sampleInfo) + { + Apply(sampleInfo); + } + + public void Apply(ISampleInfo sampleInfo) + { + if (this.sampleInfo != null) + throw new InvalidOperationException($"A {nameof(PoolableSkinnableSample)} cannot be applied multiple {nameof(ISampleInfo)}s."); + + this.sampleInfo = sampleInfo; + + if (LoadState >= LoadState.Ready) + updateSample(); + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateSample(); + } + + private void updateSample() + { + ClearInternal(); + + var ch = CurrentSkin.GetSample(sampleInfo); + + if (ch == null && AllowDefaultFallback) + { + foreach (var lookup in sampleInfo.LookupNames) + { + if ((ch = sampleStore.Get(lookup)) != null) + break; + } + } + + if (ch == null) + return; + + AddInternal(sample = new DrawableSample(ch) + { + Looping = Looping, + Volume = { Value = sampleInfo.Volume / 100.0 } + }); + } + + public void Play(bool restart = true) => sample?.Play(restart); + + public void Stop() => sample?.Stop(); + + public bool Playing => sample?.Playing ?? false; + + private bool looping; + + public bool Looping + { + get => looping; + set + { + looping = value; + + if (sample != null) + sample.Looping = value; + } + } + + /// + /// The volume of this component. + /// + public BindableNumber Volume => adjustments.Volume; + + /// + /// The playback balance of this sample (-1 .. 1 where 0 is centered) + /// + public BindableNumber Balance => adjustments.Balance; + + /// + /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. + /// + public BindableNumber Frequency => adjustments.Frequency; + + /// + /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. + /// + public BindableNumber Tempo => adjustments.Tempo; + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => adjustments.AddAdjustment(type, adjustBindable); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => adjustments.RemoveAdjustment(type, adjustBindable); + + public void RemoveAllAdjustments(AdjustableProperty type) => adjustments.RemoveAllAdjustments(type); + + public IBindable AggregateVolume => adjustments.AggregateVolume; + + public IBindable AggregateBalance => adjustments.AggregateBalance; + + public IBindable AggregateFrequency => adjustments.AggregateFrequency; + + public IBindable AggregateTempo => adjustments.AggregateTempo; + } + + public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent + { public override bool RemoveWhenNotAlive => false; public override bool RemoveCompletedTransforms => false; @@ -34,17 +157,44 @@ namespace osu.Game.Skinning /// protected bool PlayWhenZeroVolume => Looping; - protected readonly AudioContainer SamplesContainer; + protected readonly AudioContainer SamplesContainer; - public SkinnableSound(ISampleInfo hitSamples) - : this(new[] { hitSamples }) + [Resolved] + private ISampleStore sampleStore { get; set; } + + [Resolved(CanBeNull = true)] + private IPooledSampleProvider pooledProvider { get; set; } + + public SkinnableSound(ISampleInfo sample) + : this(new[] { sample }) { } - public SkinnableSound(IEnumerable hitSamples) + public SkinnableSound(IEnumerable samples) { - this.hitSamples = hitSamples.ToArray(); - InternalChild = SamplesContainer = new AudioContainer(); + this.samples = samples.ToArray(); + + InternalChild = SamplesContainer = new AudioContainer(); + } + + private ISampleInfo[] samples; + + public ISampleInfo[] Samples + { + get => samples; + set + { + if (value == null) + throw new ArgumentNullException(nameof(value)); + + if (samples == value) + return; + + samples = value; + + if (LoadState >= LoadState.Ready) + updateSamples(); + } } private bool looping; @@ -77,34 +227,23 @@ namespace osu.Game.Skinning } protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + // Start playback internally for the new samples if the previous ones were playing beforehand. + if (IsPlaying) + Play(); + } + + private void updateSamples() { bool wasPlaying = IsPlaying; - var channels = hitSamples.Select(s => - { - var ch = skin.GetSample(s); + // Remove all pooled samples (return them to the pool), and dispose the rest. + SamplesContainer.RemoveAll(s => s.IsInPool); + SamplesContainer.Clear(); - if (ch == null && allowFallback) - { - foreach (var lookup in s.LookupNames) - { - if ((ch = samples.Get(lookup)) != null) - break; - } - } + foreach (var s in samples) + SamplesContainer.Add(pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s)); - if (ch != null) - { - ch.Looping = looping; - ch.Volume.Value = s.Volume / 100.0; - } - - return ch; - }).Where(c => c != null); - - SamplesContainer.ChildrenEnumerable = channels.Select(c => new DrawableSample(c)); - - // Start playback internally for the new samples if the previous ones were playing beforehand. if (wasPlaying) Play(); } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 08811b9b8c..b8d212d3c9 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -37,8 +37,8 @@ namespace osu.Game.Storyboards.Drawables foreach (var mod in mods.Value.OfType()) { - foreach (var sample in SamplesContainer) - mod.ApplyToSample(sample); + // foreach (var sample in SamplesContainer) + // mod.ApplyToSample(sample.Sample); } } From 003fed857c9f35c258f6ad1499d8e5bc4e624827 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 19:52:34 +0900 Subject: [PATCH 4772/6909] Separate files --- osu.Game/Skinning/IPooledSampleProvider.cs | 14 ++ osu.Game/Skinning/PoolableSkinnableSample.cs | 133 +++++++++++++++++++ osu.Game/Skinning/SkinnableSound.cs | 127 ------------------ 3 files changed, 147 insertions(+), 127 deletions(-) create mode 100644 osu.Game/Skinning/IPooledSampleProvider.cs create mode 100644 osu.Game/Skinning/PoolableSkinnableSample.cs diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs new file mode 100644 index 0000000000..3dc0b5375d --- /dev/null +++ b/osu.Game/Skinning/IPooledSampleProvider.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 JetBrains.Annotations; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + public interface IPooledSampleProvider + { + [CanBeNull] + PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); + } +} diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs new file mode 100644 index 0000000000..6ad7345954 --- /dev/null +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Audio; +using osu.Game.Audio; + +namespace osu.Game.Skinning +{ + public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent + { + private ISampleInfo sampleInfo; + private DrawableSample sample; + + [Resolved] + private ISampleStore sampleStore { get; set; } + + [Cached] + private readonly AudioAdjustments adjustments = new AudioAdjustments(); + + public PoolableSkinnableSample() + { + } + + public PoolableSkinnableSample(ISampleInfo sampleInfo) + { + Apply(sampleInfo); + } + + public void Apply(ISampleInfo sampleInfo) + { + if (this.sampleInfo != null) + throw new InvalidOperationException($"A {nameof(PoolableSkinnableSample)} cannot be applied multiple {nameof(ISampleInfo)}s."); + + this.sampleInfo = sampleInfo; + + if (LoadState >= LoadState.Ready) + updateSample(); + } + + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateSample(); + } + + private void updateSample() + { + ClearInternal(); + + var ch = CurrentSkin.GetSample(sampleInfo); + + if (ch == null && AllowDefaultFallback) + { + foreach (var lookup in sampleInfo.LookupNames) + { + if ((ch = sampleStore.Get(lookup)) != null) + break; + } + } + + if (ch == null) + return; + + AddInternal(sample = new DrawableSample(ch) + { + Looping = Looping, + Volume = { Value = sampleInfo.Volume / 100.0 } + }); + } + + public void Play(bool restart = true) => sample?.Play(restart); + + public void Stop() => sample?.Stop(); + + public bool Playing => sample?.Playing ?? false; + + private bool looping; + + public bool Looping + { + get => looping; + set + { + looping = value; + + if (sample != null) + sample.Looping = value; + } + } + + /// + /// The volume of this component. + /// + public BindableNumber Volume => adjustments.Volume; + + /// + /// The playback balance of this sample (-1 .. 1 where 0 is centered) + /// + public BindableNumber Balance => adjustments.Balance; + + /// + /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. + /// + public BindableNumber Frequency => adjustments.Frequency; + + /// + /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. + /// + public BindableNumber Tempo => adjustments.Tempo; + + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => adjustments.AddAdjustment(type, adjustBindable); + + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + => adjustments.RemoveAdjustment(type, adjustBindable); + + public void RemoveAllAdjustments(AdjustableProperty type) => adjustments.RemoveAllAdjustments(type); + + public IBindable AggregateVolume => adjustments.AggregateVolume; + + public IBindable AggregateBalance => adjustments.AggregateBalance; + + public IBindable AggregateFrequency => adjustments.AggregateFrequency; + + public IBindable AggregateTempo => adjustments.AggregateTempo; + } +} diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 8410b7eeae..77ae8f1e16 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -4,144 +4,17 @@ using System; using System.Collections.Generic; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; 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.Game.Audio; namespace osu.Game.Skinning { - public interface IPooledSampleProvider - { - [CanBeNull] - PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); - } - - public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent - { - private ISampleInfo sampleInfo; - private DrawableSample sample; - - [Resolved] - private ISampleStore sampleStore { get; set; } - - [Cached] - private readonly AudioAdjustments adjustments = new AudioAdjustments(); - - public PoolableSkinnableSample() - { - } - - public PoolableSkinnableSample(ISampleInfo sampleInfo) - { - Apply(sampleInfo); - } - - public void Apply(ISampleInfo sampleInfo) - { - if (this.sampleInfo != null) - throw new InvalidOperationException($"A {nameof(PoolableSkinnableSample)} cannot be applied multiple {nameof(ISampleInfo)}s."); - - this.sampleInfo = sampleInfo; - - if (LoadState >= LoadState.Ready) - updateSample(); - } - - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - base.SkinChanged(skin, allowFallback); - updateSample(); - } - - private void updateSample() - { - ClearInternal(); - - var ch = CurrentSkin.GetSample(sampleInfo); - - if (ch == null && AllowDefaultFallback) - { - foreach (var lookup in sampleInfo.LookupNames) - { - if ((ch = sampleStore.Get(lookup)) != null) - break; - } - } - - if (ch == null) - return; - - AddInternal(sample = new DrawableSample(ch) - { - Looping = Looping, - Volume = { Value = sampleInfo.Volume / 100.0 } - }); - } - - public void Play(bool restart = true) => sample?.Play(restart); - - public void Stop() => sample?.Stop(); - - public bool Playing => sample?.Playing ?? false; - - private bool looping; - - public bool Looping - { - get => looping; - set - { - looping = value; - - if (sample != null) - sample.Looping = value; - } - } - - /// - /// The volume of this component. - /// - public BindableNumber Volume => adjustments.Volume; - - /// - /// The playback balance of this sample (-1 .. 1 where 0 is centered) - /// - public BindableNumber Balance => adjustments.Balance; - - /// - /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. - /// - public BindableNumber Frequency => adjustments.Frequency; - - /// - /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. - /// - public BindableNumber Tempo => adjustments.Tempo; - - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => adjustments.AddAdjustment(type, adjustBindable); - - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => adjustments.RemoveAdjustment(type, adjustBindable); - - public void RemoveAllAdjustments(AdjustableProperty type) => adjustments.RemoveAllAdjustments(type); - - public IBindable AggregateVolume => adjustments.AggregateVolume; - - public IBindable AggregateBalance => adjustments.AggregateBalance; - - public IBindable AggregateFrequency => adjustments.AggregateFrequency; - - public IBindable AggregateTempo => adjustments.AggregateTempo; - } - public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent { public override bool RemoveWhenNotAlive => false; From 8920534a255fe7c182a33d3200a8c23dd177e299 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 20:24:03 +0900 Subject: [PATCH 4773/6909] Fix pools not being added to hierarchy --- osu.Game/Rulesets/UI/Playfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 9f3a4c508f..5383c4b2ce 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -348,7 +348,7 @@ namespace osu.Game.Rulesets.UI public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) { if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) - samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 5); + AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 5)); return existingPool.Get(); } From 812d5d59b1ac12757a86cb44bd6447b9f7d60846 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 20:29:09 +0900 Subject: [PATCH 4774/6909] Fix looping not being propagated --- osu.Game/Skinning/SkinnableSound.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 77ae8f1e16..eca1b9b03f 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -115,7 +115,12 @@ namespace osu.Game.Skinning SamplesContainer.Clear(); foreach (var s in samples) - SamplesContainer.Add(pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s)); + { + var sample = pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); + sample.Looping = Looping; + + SamplesContainer.Add(sample); + } if (wasPlaying) Play(); From 70cb1979578e51fda79a41b12f2767777d64cf08 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 20:38:36 +0900 Subject: [PATCH 4775/6909] Cleanups --- .../Objects/Drawables/DrawableHitObject.cs | 7 +++---- osu.Game/Skinning/PausableSkinnableSound.cs | 11 ++++++++--- osu.Game/Skinning/SkinnableSound.cs | 15 ++++++++++----- 3 files changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 0d97066b35..26d2ffe3ce 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -156,7 +156,7 @@ namespace osu.Game.Rulesets.Objects.Drawables config.BindWith(OsuSetting.PositionalHitSounds, userPositionalHitSounds); // Explicit non-virtual function call. - base.AddInternal(Samples = new PausableSkinnableSound(Array.Empty())); + base.AddInternal(Samples = new PausableSkinnableSound()); } protected override void LoadAsyncComplete() @@ -266,7 +266,8 @@ namespace osu.Game.Rulesets.Objects.Drawables // In order to stop this needless update, the event is unbound and re-bound as late as possible in Apply(). samplesBindable.CollectionChanged -= onSamplesChanged; - Samples.Samples = Array.Empty(); + // Release the samples for other hitobjects to use. + Samples.Samples = null; if (nestedHitObjects.IsValueCreated) { @@ -334,8 +335,6 @@ namespace osu.Game.Rulesets.Objects.Drawables /// protected virtual void LoadSamples() { - Samples.Samples = Array.Empty(); - var samples = GetSamples().ToArray(); if (samples.Length <= 0) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 758b784649..be4664356d 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Threading; @@ -14,16 +15,20 @@ namespace osu.Game.Skinning { protected bool RequestedPlaying { get; private set; } - public PausableSkinnableSound(ISampleInfo sample) - : base(sample) + public PausableSkinnableSound() { } - public PausableSkinnableSound(IEnumerable samples) + public PausableSkinnableSound([NotNull] IEnumerable samples) : base(samples) { } + public PausableSkinnableSound([NotNull] ISampleInfo sample) + : base(sample) + { + } + private readonly IBindable samplePlaybackDisabled = new Bindable(); private ScheduledDelegate scheduledStart; diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index eca1b9b03f..67d13118d9 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -38,18 +39,23 @@ namespace osu.Game.Skinning [Resolved(CanBeNull = true)] private IPooledSampleProvider pooledProvider { get; set; } - public SkinnableSound(ISampleInfo sample) - : this(new[] { sample }) + public SkinnableSound() { } - public SkinnableSound(IEnumerable samples) + public SkinnableSound([NotNull] IEnumerable samples) + : this() { this.samples = samples.ToArray(); InternalChild = SamplesContainer = new AudioContainer(); } + public SkinnableSound([NotNull] ISampleInfo sample) + : this(new[] { sample }) + { + } + private ISampleInfo[] samples; public ISampleInfo[] Samples @@ -57,8 +63,7 @@ namespace osu.Game.Skinning get => samples; set { - if (value == null) - throw new ArgumentNullException(nameof(value)); + value ??= Array.Empty(); if (samples == value) return; From f013928fa3dd7c0293cef09c8c2b651ea2e8e314 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 20:40:01 +0900 Subject: [PATCH 4776/6909] Set maximum pool size --- osu.Game/Rulesets/UI/Playfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 5383c4b2ce..6e89f20246 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -348,7 +348,7 @@ namespace osu.Game.Rulesets.UI public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) { if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) - AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 5)); + AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1, 30)); return existingPool.Get(); } From 688a04c2ff8e6a931b67ed4a36337508f5555d22 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 20:40:30 +0900 Subject: [PATCH 4777/6909] Make slider/spinner use pooled samples --- .../Objects/Drawables/DrawableSlider.cs | 17 ++++------- .../Objects/Drawables/DrawableSpinner.cs | 29 +++++++++++-------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 14c494d909..b62c04eed9 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.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; +using osu.Game.Audio; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.UI; @@ -40,7 +41,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container tailContainer; private Container tickContainer; private Container repeatContainer; - private Container samplesContainer; + private PausableSkinnableSound slidingSample; public DrawableSlider() : this(null) @@ -69,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Alpha = 0 }, headContainer = new Container { RelativeSizeAxes = Axes.Both }, - samplesContainer = new Container { RelativeSizeAxes = Axes.Both } + slidingSample = new PausableSkinnableSound { Looping = true } }; PositionBindable.BindValueChanged(_ => Position = HitObject.StackedPosition); @@ -100,17 +101,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnFree(hitObject); PathVersion.UnbindFrom(HitObject.Path.Version); - } - private PausableSkinnableSound slidingSample; + slidingSample.Samples = null; + } protected override void LoadSamples() { base.LoadSamples(); - samplesContainer.Clear(); - slidingSample = null; - var firstSample = HitObject.Samples.FirstOrDefault(); if (firstSample != null) @@ -118,10 +116,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "sliderslide"; - samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone) - { - Looping = true - }); + slidingSample.Samples = new ISampleInfo[] { clone }; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 2a14a7c975..e5fc717504 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -9,6 +9,7 @@ using osu.Framework.Audio; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Audio; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; @@ -33,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private Container ticks; private SpinnerBonusDisplay bonusDisplay; - private Container samplesContainer; + private PausableSkinnableSound spinningSample; private Bindable isSpinning; private bool spinnerFrequencyModulate; @@ -81,7 +82,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Origin = Anchor.Centre, Y = -120, }, - samplesContainer = new Container { RelativeSizeAxes = Axes.Both } + spinningSample = new PausableSkinnableSound + { + Volume = { Value = 0 }, + Looping = true, + Frequency = { Value = spinning_sample_initial_frequency } + } }; PositionBindable.BindValueChanged(pos => Position = pos.NewValue); @@ -95,17 +101,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables isSpinning.BindValueChanged(updateSpinningSample); } - private PausableSkinnableSound spinningSample; private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; + protected override void OnFree(HitObject hitObject) + { + base.OnFree(hitObject); + + spinningSample.Samples = null; + } + protected override void LoadSamples() { base.LoadSamples(); - samplesContainer.Clear(); - spinningSample = null; - var firstSample = HitObject.Samples.FirstOrDefault(); if (firstSample != null) @@ -113,12 +122,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); clone.Name = "spinnerspin"; - samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone) - { - Volume = { Value = 0 }, - Looping = true, - Frequency = { Value = spinning_sample_initial_frequency } - }); + spinningSample.Samples = new ISampleInfo[] { clone }; + spinningSample.Frequency.Value = spinning_sample_initial_frequency; } } From 0287269b2fed663d910f9571042accfe3a11d185 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 21:01:38 +0900 Subject: [PATCH 4778/6909] Fix volume discrepancies --- osu.Game/Skinning/PoolableSkinnableSample.cs | 42 ++++++++++---------- osu.Game/Skinning/SkinnableSound.cs | 4 +- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 6ad7345954..ad799dd32e 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -8,26 +8,28 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Containers; using osu.Game.Audio; namespace osu.Game.Skinning { public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent { + private readonly AudioContainer sampleContainer; + private ISampleInfo sampleInfo; private DrawableSample sample; [Resolved] private ISampleStore sampleStore { get; set; } - [Cached] - private readonly AudioAdjustments adjustments = new AudioAdjustments(); - public PoolableSkinnableSample() { + InternalChild = sampleContainer = new AudioContainer { RelativeSizeAxes = Axes.Both }; } public PoolableSkinnableSample(ISampleInfo sampleInfo) + : this() { Apply(sampleInfo); } @@ -39,6 +41,8 @@ namespace osu.Game.Skinning this.sampleInfo = sampleInfo; + Volume.Value = sampleInfo.Volume / 100.0; + if (LoadState >= LoadState.Ready) updateSample(); } @@ -51,7 +55,7 @@ namespace osu.Game.Skinning private void updateSample() { - ClearInternal(); + sampleContainer.Clear(); var ch = CurrentSkin.GetSample(sampleInfo); @@ -67,11 +71,7 @@ namespace osu.Game.Skinning if (ch == null) return; - AddInternal(sample = new DrawableSample(ch) - { - Looping = Looping, - Volume = { Value = sampleInfo.Volume / 100.0 } - }); + sampleContainer.Add(sample = new DrawableSample(ch) { Looping = Looping }); } public void Play(bool restart = true) => sample?.Play(restart); @@ -97,37 +97,35 @@ namespace osu.Game.Skinning /// /// The volume of this component. /// - public BindableNumber Volume => adjustments.Volume; + public BindableNumber Volume => sampleContainer.Volume; /// /// The playback balance of this sample (-1 .. 1 where 0 is centered) /// - public BindableNumber Balance => adjustments.Balance; + public BindableNumber Balance => sampleContainer.Balance; /// /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. /// - public BindableNumber Frequency => adjustments.Frequency; + public BindableNumber Frequency => sampleContainer.Frequency; /// /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. /// - public BindableNumber Tempo => adjustments.Tempo; + public BindableNumber Tempo => sampleContainer.Tempo; - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => adjustments.AddAdjustment(type, adjustBindable); + public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => adjustments.RemoveAdjustment(type, adjustBindable); + public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable); - public void RemoveAllAdjustments(AdjustableProperty type) => adjustments.RemoveAllAdjustments(type); + public void RemoveAllAdjustments(AdjustableProperty type) => sampleContainer.RemoveAllAdjustments(type); - public IBindable AggregateVolume => adjustments.AggregateVolume; + public IBindable AggregateVolume => sampleContainer.AggregateVolume; - public IBindable AggregateBalance => adjustments.AggregateBalance; + public IBindable AggregateBalance => sampleContainer.AggregateBalance; - public IBindable AggregateFrequency => adjustments.AggregateFrequency; + public IBindable AggregateFrequency => sampleContainer.AggregateFrequency; - public IBindable AggregateTempo => adjustments.AggregateTempo; + public IBindable AggregateTempo => sampleContainer.AggregateTempo; } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 67d13118d9..8dc3337525 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -41,14 +41,13 @@ namespace osu.Game.Skinning public SkinnableSound() { + InternalChild = SamplesContainer = new AudioContainer(); } public SkinnableSound([NotNull] IEnumerable samples) : this() { this.samples = samples.ToArray(); - - InternalChild = SamplesContainer = new AudioContainer(); } public SkinnableSound([NotNull] ISampleInfo sample) @@ -123,6 +122,7 @@ namespace osu.Game.Skinning { var sample = pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); sample.Looping = Looping; + sample.Volume.Value = s.Volume / 100.0; SamplesContainer.Add(sample); } From 7180bfe4ba904507fd944d42bad39296a559dabc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 21:21:57 +0900 Subject: [PATCH 4779/6909] Unlimit number of samples per pool --- osu.Game/Rulesets/UI/Playfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 6e89f20246..460251e595 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -348,7 +348,7 @@ namespace osu.Game.Rulesets.UI public PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo) { if (!samplePools.TryGetValue(sampleInfo, out var existingPool)) - AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1, 30)); + AddInternal(samplePools[sampleInfo] = existingPool = new DrawableSamplePool(sampleInfo, 1)); return existingPool.Get(); } From d388c44428c74f2ce32cb48b0a83f900497f46b7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 22:30:21 +0900 Subject: [PATCH 4780/6909] Cleanup, refactoring, and restart sample on skin change --- osu.Game/Skinning/PoolableSkinnableSample.cs | 72 ++++++++++++++------ osu.Game/Skinning/SkinnableSound.cs | 7 -- 2 files changed, 52 insertions(+), 27 deletions(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index ad799dd32e..e38b309efb 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.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.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Track; @@ -13,27 +14,48 @@ using osu.Game.Audio; namespace osu.Game.Skinning { + /// + /// A sample corresponding to an that supports being pooled and responding to skin changes. + /// public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent { - private readonly AudioContainer sampleContainer; + /// + /// The currently-loaded . + /// + [CanBeNull] + public DrawableSample Sample { get; private set; } + private readonly AudioContainer sampleContainer; private ISampleInfo sampleInfo; - private DrawableSample sample; [Resolved] private ISampleStore sampleStore { get; set; } + /// + /// Creates a new with no applied . + /// An can be applied later via . + /// public PoolableSkinnableSample() { InternalChild = sampleContainer = new AudioContainer { RelativeSizeAxes = Axes.Both }; } + /// + /// Creates a new with an applied . + /// + /// The to attach. public PoolableSkinnableSample(ISampleInfo sampleInfo) : this() { Apply(sampleInfo); } + /// + /// Applies an that describes the sample to retrieve. + /// Only one can ever be applied to a . + /// + /// The to apply. + /// If an has already been applied to this . public void Apply(ISampleInfo sampleInfo) { if (this.sampleInfo != null) @@ -55,6 +77,11 @@ namespace osu.Game.Skinning private void updateSample() { + if (sampleInfo == null) + return; + + bool wasPlaying = Playing; + sampleContainer.Clear(); var ch = CurrentSkin.GetSample(sampleInfo); @@ -71,17 +98,34 @@ namespace osu.Game.Skinning if (ch == null) return; - sampleContainer.Add(sample = new DrawableSample(ch) { Looping = Looping }); + sampleContainer.Add(Sample = new DrawableSample(ch) { Looping = Looping }); + + // Start playback internally for the new sample if the previous one was playing beforehand. + if (wasPlaying) + Play(); } - public void Play(bool restart = true) => sample?.Play(restart); + /// + /// Plays the sample. + /// + /// Whether to play the sample from the beginning. + public void Play(bool restart = true) => Sample?.Play(restart); - public void Stop() => sample?.Stop(); + /// + /// Stops the sample. + /// + public void Stop() => Sample?.Stop(); - public bool Playing => sample?.Playing ?? false; + /// + /// Whether the sample is currently playing. + /// + public bool Playing => Sample?.Playing ?? false; private bool looping; + /// + /// Gets or sets whether the sample should loop on completion. + /// public bool Looping { get => looping; @@ -89,29 +133,17 @@ namespace osu.Game.Skinning { looping = value; - if (sample != null) - sample.Looping = value; + if (Sample != null) + Sample.Looping = value; } } - /// - /// The volume of this component. - /// public BindableNumber Volume => sampleContainer.Volume; - /// - /// The playback balance of this sample (-1 .. 1 where 0 is centered) - /// public BindableNumber Balance => sampleContainer.Balance; - /// - /// Rate at which the component is played back (affects pitch). 1 is 100% playback speed, or default frequency. - /// public BindableNumber Frequency => sampleContainer.Frequency; - /// - /// Rate at which the component is played back (does not affect pitch). 1 is 100% playback speed. - /// public BindableNumber Tempo => sampleContainer.Tempo; public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 8dc3337525..0f39784138 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -103,13 +103,6 @@ namespace osu.Game.Skinning SamplesContainer.ForEach(c => c.Stop()); } - protected override void SkinChanged(ISkinSource skin, bool allowFallback) - { - // Start playback internally for the new samples if the previous ones were playing beforehand. - if (IsPlaying) - Play(); - } - private void updateSamples() { bool wasPlaying = IsPlaying; From a53848ef9bbc09b8c4c75a044c3db6a543f85dc0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 22:30:41 +0900 Subject: [PATCH 4781/6909] Fix storyboard imlpementation --- osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index b8d212d3c9..904af730de 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -37,8 +37,8 @@ namespace osu.Game.Storyboards.Drawables foreach (var mod in mods.Value.OfType()) { - // foreach (var sample in SamplesContainer) - // mod.ApplyToSample(sample.Sample); + foreach (var sample in SamplesContainer) + mod.ApplyToSample(sample.Sample); } } From 8a656f7cee1ac51578420e9e9576c8055a8fde67 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 22:42:44 +0900 Subject: [PATCH 4782/6909] Fix missing SkinChanged event + safety --- osu.Game/Skinning/SkinnableSound.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 0f39784138..bb747a2176 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -55,7 +55,7 @@ namespace osu.Game.Skinning { } - private ISampleInfo[] samples; + private ISampleInfo[] samples = Array.Empty(); public ISampleInfo[] Samples { @@ -103,6 +103,12 @@ namespace osu.Game.Skinning SamplesContainer.ForEach(c => c.Stop()); } + protected override void SkinChanged(ISkinSource skin, bool allowFallback) + { + base.SkinChanged(skin, allowFallback); + updateSamples(); + } + private void updateSamples() { bool wasPlaying = IsPlaying; From 7c83a27002a033a114b42a6b933c998d8858f5fa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 19 Nov 2020 22:47:11 +0900 Subject: [PATCH 4783/6909] Add more xmldocs --- osu.Game/Skinning/PoolableSkinnableSample.cs | 2 +- osu.Game/Skinning/SkinnableSound.cs | 29 ++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index e38b309efb..adc58ee94e 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -124,7 +124,7 @@ namespace osu.Game.Skinning private bool looping; /// - /// Gets or sets whether the sample should loop on completion. + /// Whether the sample should loop on completion. /// public bool Looping { diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index bb747a2176..24dddaf758 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -16,6 +16,9 @@ using osu.Game.Audio; namespace osu.Game.Skinning { + /// + /// A sound consisting of one or more samples to be played. + /// public class SkinnableSound : SkinReloadableDrawable, IAdjustableAudioComponent { public override bool RemoveWhenNotAlive => false; @@ -39,17 +42,28 @@ namespace osu.Game.Skinning [Resolved(CanBeNull = true)] private IPooledSampleProvider pooledProvider { get; set; } + /// + /// Creates a new . + /// public SkinnableSound() { InternalChild = SamplesContainer = new AudioContainer(); } + /// + /// Creates a new with some initial samples. + /// + /// The initial samples. public SkinnableSound([NotNull] IEnumerable samples) : this() { this.samples = samples.ToArray(); } + /// + /// Creates a new with an initial sample. + /// + /// The initial sample. public SkinnableSound([NotNull] ISampleInfo sample) : this(new[] { sample }) { @@ -57,6 +71,9 @@ namespace osu.Game.Skinning private ISampleInfo[] samples = Array.Empty(); + /// + /// The samples that should be played. + /// public ISampleInfo[] Samples { get => samples; @@ -76,6 +93,9 @@ namespace osu.Game.Skinning private bool looping; + /// + /// Whether the samples should loop on completion. + /// public bool Looping { get => looping; @@ -89,6 +109,9 @@ namespace osu.Game.Skinning } } + /// + /// Plays the samples. + /// public virtual void Play() { SamplesContainer.ForEach(c => @@ -98,6 +121,9 @@ namespace osu.Game.Skinning }); } + /// + /// Stops the samples. + /// public virtual void Stop() { SamplesContainer.ForEach(c => c.Stop()); @@ -149,6 +175,9 @@ namespace osu.Game.Skinning public void RemoveAllAdjustments(AdjustableProperty type) => SamplesContainer.RemoveAllAdjustments(type); + /// + /// Whether any samples currently playing. + /// public bool IsPlaying => SamplesContainer.Any(s => s.Playing); #endregion From d467a00eeac6f04e6b192c7513538b22e3d1f0d1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 00:11:31 +0900 Subject: [PATCH 4784/6909] Add initial followpoint pooling implementation --- .../TestSceneFollowPoints.cs | 552 +++++++++--------- .../Drawables/Connections/FollowPoint.cs | 5 +- .../Connections/FollowPointConnection.cs | 134 +---- .../Connections/FollowPointRenderer.cs | 167 ++++-- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 +- 5 files changed, 423 insertions(+), 439 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index 6c077eb214..3ebb747a21 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -1,276 +1,276 @@ -// Copyright (c) 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; -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; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; -using osu.Game.Tests.Visual; -using osuTK; - -namespace osu.Game.Rulesets.Osu.Tests -{ - public class TestSceneFollowPoints : OsuTestScene - { - private Container hitObjectContainer; - private FollowPointRenderer followPointRenderer; - - [SetUp] - public void Setup() => Schedule(() => - { - Children = new Drawable[] - { - hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both }, - followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both } - }; - }); - - [Test] - public void TestAddObject() - { - addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); - - assertGroups(); - } - - [Test] - public void TestRemoveObject() - { - addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); - - removeObjectStep(() => getObject(0)); - - assertGroups(); - } - - [Test] - public void TestAddMultipleObjects() - { - addMultipleObjectsStep(); - - assertGroups(); - } - - [Test] - public void TestRemoveEndObject() - { - addMultipleObjectsStep(); - - removeObjectStep(() => getObject(4)); - - assertGroups(); - } - - [Test] - public void TestRemoveStartObject() - { - addMultipleObjectsStep(); - - removeObjectStep(() => getObject(0)); - - assertGroups(); - } - - [Test] - public void TestRemoveMiddleObject() - { - addMultipleObjectsStep(); - - removeObjectStep(() => getObject(2)); - - assertGroups(); - } - - [Test] - public void TestMoveObject() - { - addMultipleObjectsStep(); - - AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); - - assertGroups(); - } - - [TestCase(0, 0)] // Start -> Start - [TestCase(0, 2)] // Start -> Middle - [TestCase(0, 5)] // Start -> End - [TestCase(2, 0)] // Middle -> Start - [TestCase(1, 3)] // Middle -> Middle (forwards) - [TestCase(3, 1)] // Middle -> Middle (backwards) - [TestCase(4, 0)] // End -> Start - [TestCase(4, 2)] // End -> Middle - [TestCase(4, 4)] // End -> End - public void TestReorderObjects(int startIndex, int endIndex) - { - addMultipleObjectsStep(); - - reorderObjectStep(startIndex, endIndex); - - 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) }, - new HitCircle { Position = new Vector2(200, 200) }, - new HitCircle { Position = new Vector2(300, 300) }, - new HitCircle { Position = new Vector2(400, 400) }, - new HitCircle { Position = new Vector2(500, 500) }, - }); - - private void addObjectsStep(Func ctorFunc) - { - AddStep("add hitobjects", () => - { - var objects = ctorFunc(); - - for (int i = 0; i < objects.Length; i++) - { - objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1); - objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - - DrawableOsuHitObject drawableObject = null; - - switch (objects[i]) - { - case HitCircle circle: - drawableObject = new DrawableHitCircle(circle); - break; - - case Slider slider: - drawableObject = new DrawableSlider(slider); - break; - - case Spinner spinner: - drawableObject = new DrawableSpinner(spinner); - break; - } - - hitObjectContainer.Add(drawableObject); - followPointRenderer.AddFollowPoints(objects[i]); - } - }); - } - - private void removeObjectStep(Func getFunc) - { - AddStep("remove hitobject", () => - { - var drawableObject = getFunc.Invoke(); - - hitObjectContainer.Remove(drawableObject); - followPointRenderer.RemoveFollowPoints(drawableObject.HitObject); - }); - } - - private void reorderObjectStep(int startIndex, int endIndex) - { - AddStep($"move object {startIndex} to {endIndex}", () => - { - DrawableOsuHitObject toReorder = getObject(startIndex); - - double targetTime; - if (endIndex < hitObjectContainer.Count) - targetTime = getObject(endIndex).HitObject.StartTime - 1; - else - targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1; - - hitObjectContainer.Remove(toReorder); - toReorder.HitObject.StartTime = targetTime; - hitObjectContainer.Add(toReorder); - }); - } - - private void assertGroups() - { - AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count); - AddAssert("group endpoints 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 (getGroup(i).Start != expectedStart.HitObject) - throw new AssertionException($"Object {i} expected to be the start of group {i}."); - - if (getGroup(i).End != expectedEnd?.HitObject) - throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}."); - } - - return true; - }); - } - - 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]; - - private class TestHitObjectContainer : Container - { - protected override int Compare(Drawable x, Drawable y) - { - var osuX = (DrawableOsuHitObject)x; - var osuY = (DrawableOsuHitObject)y; - - int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime); - - if (compare == 0) - return base.Compare(x, y); - - return compare; - } - } - } -} +// // Copyright (c) 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; +// 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; +// using osu.Game.Rulesets.Osu.Objects.Drawables; +// using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; +// using osu.Game.Tests.Visual; +// using osuTK; +// +// namespace osu.Game.Rulesets.Osu.Tests +// { +// public class TestSceneFollowPoints : OsuTestScene +// { +// private Container hitObjectContainer; +// private FollowPointRenderer followPointRenderer; +// +// [SetUp] +// public void Setup() => Schedule(() => +// { +// Children = new Drawable[] +// { +// hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both }, +// followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both } +// }; +// }); +// +// [Test] +// public void TestAddObject() +// { +// addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); +// +// assertGroups(); +// } +// +// [Test] +// public void TestRemoveObject() +// { +// addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); +// +// removeObjectStep(() => getObject(0)); +// +// assertGroups(); +// } +// +// [Test] +// public void TestAddMultipleObjects() +// { +// addMultipleObjectsStep(); +// +// assertGroups(); +// } +// +// [Test] +// public void TestRemoveEndObject() +// { +// addMultipleObjectsStep(); +// +// removeObjectStep(() => getObject(4)); +// +// assertGroups(); +// } +// +// [Test] +// public void TestRemoveStartObject() +// { +// addMultipleObjectsStep(); +// +// removeObjectStep(() => getObject(0)); +// +// assertGroups(); +// } +// +// [Test] +// public void TestRemoveMiddleObject() +// { +// addMultipleObjectsStep(); +// +// removeObjectStep(() => getObject(2)); +// +// assertGroups(); +// } +// +// [Test] +// public void TestMoveObject() +// { +// addMultipleObjectsStep(); +// +// AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); +// +// assertGroups(); +// } +// +// [TestCase(0, 0)] // Start -> Start +// [TestCase(0, 2)] // Start -> Middle +// [TestCase(0, 5)] // Start -> End +// [TestCase(2, 0)] // Middle -> Start +// [TestCase(1, 3)] // Middle -> Middle (forwards) +// [TestCase(3, 1)] // Middle -> Middle (backwards) +// [TestCase(4, 0)] // End -> Start +// [TestCase(4, 2)] // End -> Middle +// [TestCase(4, 4)] // End -> End +// public void TestReorderObjects(int startIndex, int endIndex) +// { +// addMultipleObjectsStep(); +// +// reorderObjectStep(startIndex, endIndex); +// +// 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) }, +// new HitCircle { Position = new Vector2(200, 200) }, +// new HitCircle { Position = new Vector2(300, 300) }, +// new HitCircle { Position = new Vector2(400, 400) }, +// new HitCircle { Position = new Vector2(500, 500) }, +// }); +// +// private void addObjectsStep(Func ctorFunc) +// { +// AddStep("add hitobjects", () => +// { +// var objects = ctorFunc(); +// +// for (int i = 0; i < objects.Length; i++) +// { +// objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1); +// objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); +// +// DrawableOsuHitObject drawableObject = null; +// +// switch (objects[i]) +// { +// case HitCircle circle: +// drawableObject = new DrawableHitCircle(circle); +// break; +// +// case Slider slider: +// drawableObject = new DrawableSlider(slider); +// break; +// +// case Spinner spinner: +// drawableObject = new DrawableSpinner(spinner); +// break; +// } +// +// hitObjectContainer.Add(drawableObject); +// followPointRenderer.AddFollowPoints(objects[i]); +// } +// }); +// } +// +// private void removeObjectStep(Func getFunc) +// { +// AddStep("remove hitobject", () => +// { +// var drawableObject = getFunc.Invoke(); +// +// hitObjectContainer.Remove(drawableObject); +// followPointRenderer.RemoveFollowPoints(drawableObject.HitObject); +// }); +// } +// +// private void reorderObjectStep(int startIndex, int endIndex) +// { +// AddStep($"move object {startIndex} to {endIndex}", () => +// { +// DrawableOsuHitObject toReorder = getObject(startIndex); +// +// double targetTime; +// if (endIndex < hitObjectContainer.Count) +// targetTime = getObject(endIndex).HitObject.StartTime - 1; +// else +// targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1; +// +// hitObjectContainer.Remove(toReorder); +// toReorder.HitObject.StartTime = targetTime; +// hitObjectContainer.Add(toReorder); +// }); +// } +// +// private void assertGroups() +// { +// AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count); +// AddAssert("group endpoints 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 (getGroup(i).Start != expectedStart.HitObject) +// throw new AssertionException($"Object {i} expected to be the start of group {i}."); +// +// if (getGroup(i).End != expectedEnd?.HitObject) +// throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}."); +// } +// +// return true; +// }); +// } +// +// 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]; +// +// private class TestHitObjectContainer : Container +// { +// protected override int Compare(Drawable x, Drawable y) +// { +// var osuX = (DrawableOsuHitObject)x; +// var osuY = (DrawableOsuHitObject)y; +// +// int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime); +// +// if (compare == 0) +// return base.Compare(x, y); +// +// return compare; +// } +// } +// } +// } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index a981648444..3e2ab65bb2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -7,6 +7,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Shapes; using osu.Game.Skinning; @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// /// A single follow point positioned between two adjacent s. /// - public class FollowPoint : Container, IAnimationTimeReference + public class FollowPoint : PoolableDrawable, IAnimationTimeReference { private const float width = 8; @@ -25,7 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { Origin = Anchor.Centre; - Child = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer + InternalChild = new SkinnableDrawable(new OsuSkinComponent(OsuSkinComponents.FollowPoint), _ => new CircularContainer { Masking = true, AutoSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 3a9e19b361..1d82e91c0e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Diagnostics; -using JetBrains.Annotations; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects; using osuTK; @@ -15,150 +12,77 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections /// /// Visualises the s between two s. /// - public class FollowPointConnection : CompositeDrawable + public class FollowPointConnection : PoolableDrawable { // Todo: These shouldn't be constants - private const int spacing = 32; - private const double preempt = 800; + public const int SPACING = 32; + public const double PREEMPT = 800; - public override bool RemoveWhenNotAlive => false; + public FollowPointRenderer.FollowPointLifetimeEntry Entry; + public DrawablePool Pool; - /// - /// The start time of . - /// - public readonly Bindable StartTime = new BindableDouble(); - - /// - /// The which s will exit from. - /// - [NotNull] - public readonly OsuHitObject Start; - - /// - /// Creates a new . - /// - /// The which s will exit from. - public FollowPointConnection([NotNull] OsuHitObject start) + protected override void FreeAfterUse() { - Start = start; - - RelativeSizeAxes = Axes.Both; - - StartTime.BindTo(start.StartTimeBindable); + base.FreeAfterUse(); + ClearInternal(false); } - protected override void LoadComplete() + protected override void PrepareForUse() { - base.LoadComplete(); - bindEvents(Start); - } + base.PrepareForUse(); - private OsuHitObject end; + OsuHitObject start = Entry.Start; + OsuHitObject end = Entry.End; - /// - /// The which s will enter. - /// - [CanBeNull] - public OsuHitObject End - { - get => end; - set - { - end = value; + double startTime = start.GetEndTime(); - if (end != null) - bindEvents(end); - - if (IsLoaded) - scheduleRefresh(); - else - refresh(); - } - } - - private void bindEvents(OsuHitObject obj) - { - obj.PositionBindable.BindValueChanged(_ => scheduleRefresh()); - obj.DefaultsApplied += _ => scheduleRefresh(); - } - - private void scheduleRefresh() - { - Scheduler.AddOnce(refresh); - } - - private void refresh() - { - double startTime = Start.GetEndTime(); - - LifetimeStart = startTime; - - if (End == null || End.NewCombo || Start is Spinner || End is Spinner) - { - // ensure we always set a lifetime for full LifetimeManagementContainer benefits - LifetimeEnd = LifetimeStart; + if (end == null || end.NewCombo || start is Spinner || end is Spinner) return; - } - Vector2 startPosition = Start.StackedEndPosition; - Vector2 endPosition = End.StackedPosition; - double endTime = End.StartTime; + Vector2 startPosition = start.StackedEndPosition; + Vector2 endPosition = end.StackedPosition; + double endTime = end.StartTime; Vector2 distanceVector = endPosition - startPosition; int distance = (int)distanceVector.Length; float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI)); double duration = endTime - startTime; - double? firstTransformStartTime = null; double finalTransformEndTime = startTime; - int point = 0; - - ClearInternal(); - - for (int d = (int)(spacing * 1.5); d < distance - spacing; d += spacing) + for (int d = (int)(SPACING * 1.5); d < distance - SPACING; d += SPACING) { float fraction = (float)d / distance; Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector; Vector2 pointEndPosition = startPosition + fraction * distanceVector; double fadeOutTime = startTime + fraction * duration; - double fadeInTime = fadeOutTime - preempt; + double fadeInTime = fadeOutTime - PREEMPT; FollowPoint fp; - AddInternal(fp = new FollowPoint()); - - Debug.Assert(End != null); + AddInternal(fp = Pool.Get()); + fp.ClearTransforms(); fp.Position = pointStartPosition; fp.Rotation = rotation; fp.Alpha = 0; - fp.Scale = new Vector2(1.5f * End.Scale); - - firstTransformStartTime ??= fadeInTime; + fp.Scale = new Vector2(1.5f * end.Scale); fp.AnimationStartTime = fadeInTime; using (fp.BeginAbsoluteSequence(fadeInTime)) { - fp.FadeIn(End.TimeFadeIn); - fp.ScaleTo(End.Scale, End.TimeFadeIn, Easing.Out); - fp.MoveTo(pointEndPosition, End.TimeFadeIn, Easing.Out); - fp.Delay(fadeOutTime - fadeInTime).FadeOut(End.TimeFadeIn); + fp.FadeIn(end.TimeFadeIn); + fp.ScaleTo(end.Scale, end.TimeFadeIn, Easing.Out); + fp.MoveTo(pointEndPosition, end.TimeFadeIn, Easing.Out); + fp.Delay(fadeOutTime - fadeInTime).FadeOut(end.TimeFadeIn); - finalTransformEndTime = fadeOutTime + End.TimeFadeIn; + finalTransformEndTime = fadeOutTime + end.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; + Entry.LifetimeEnd = finalTransformEndTime; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index be1392d7c3..ac7b78a25b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -2,53 +2,57 @@ // 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.Graphics.Performance; +using osu.Framework.Graphics.Pooling; +using osu.Game.Rulesets.Objects; +using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { /// /// Visualises connections between s. /// - public class FollowPointRenderer : LifetimeManagementContainer + public class FollowPointRenderer : CompositeDrawable { - /// - /// All the s contained by this . - /// - internal IReadOnlyList Connections => connections; - - private readonly List connections = new List(); - public override bool RemoveCompletedTransforms => false; - /// - /// Adds the s around an . - /// This includes s leading into , and s exiting . - /// - /// The to add s for. - public void AddFollowPoints(OsuHitObject hitObject) - => addConnection(new FollowPointConnection(hitObject).With(g => g.StartTime.BindValueChanged(_ => onStartTimeChanged(g)))); + private DrawablePool connectionPool; + private DrawablePool pointPool; - /// - /// Removes the s around an . - /// This includes s leading into , and s exiting . - /// - /// The to remove s for. - public void RemoveFollowPoints(OsuHitObject hitObject) => removeGroup(connections.Single(g => g.Start == hitObject)); + private readonly List lifetimeEntries = new List(); + private readonly Dictionary connectionsInUse = new Dictionary(); + private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); - /// - /// Adds a to this . - /// - /// The to add. - /// The index of in . - private void addConnection(FollowPointConnection connection) + public FollowPointRenderer() { - // Groups are sorted by their start time when added such that the index can be used to post-process other surrounding connections - int index = connections.AddInPlace(connection, Comparer.Create((g1, g2) => + lifetimeManager.EntryBecameAlive += onEntryBecameAlive; + lifetimeManager.EntryBecameDead += onEntryBecameDead; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] { - int comp = g1.StartTime.Value.CompareTo(g2.StartTime.Value); + connectionPool = new DrawablePool(1, 200), + pointPool = new DrawablePool(50, 1000) + }; + + MakeChildAlive(connectionPool); + MakeChildAlive(pointPool); + } + + public void AddFollowPoints2(OsuHitObject hitObject) + { + var newEntry = new FollowPointLifetimeEntry(hitObject); + + var index = lifetimeEntries.AddInPlace(newEntry, Comparer.Create((e1, e2) => + { + int comp = e1.Start.StartTime.CompareTo(e2.Start.StartTime); if (comp != 0) return comp; @@ -61,19 +65,19 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections return -1; })); - if (index < connections.Count - 1) + if (index < lifetimeEntries.Count - 1) { // Update the connection's end point to the next connection's start point // h1 -> -> -> h2 // connection nextGroup - FollowPointConnection nextConnection = connections[index + 1]; - connection.End = nextConnection.Start; + FollowPointLifetimeEntry nextEntry = lifetimeEntries[index + 1]; + newEntry.End = nextEntry.Start; } else { // The end point may be non-null during re-ordering - connection.End = null; + newEntry.End = null; } if (index > 0) @@ -82,23 +86,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections // h1 -> -> -> h2 // prevGroup connection - FollowPointConnection previousConnection = connections[index - 1]; - previousConnection.End = connection.Start; + FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1]; + previousEntry.End = newEntry.Start; } - AddInternal(connection); + lifetimeManager.AddEntry(newEntry); } - /// - /// Removes a from this . - /// - /// The to remove. - /// Whether was removed. - private void removeGroup(FollowPointConnection connection) + public void RemoveFollowPoints2(OsuHitObject hitObject) { - RemoveInternal(connection); + int index = lifetimeEntries.FindIndex(e => e.Start == hitObject); + var entry = lifetimeEntries[index]; - int index = connections.IndexOf(connection); + lifetimeEntries.RemoveAt(index); + lifetimeManager.RemoveEntry(entry); if (index > 0) { @@ -106,18 +107,76 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections // h1 -> -> -> h2 -> -> -> h3 // prevGroup connection nextGroup // The current connection's end point is used since there may not be a next connection - FollowPointConnection previousConnection = connections[index - 1]; - previousConnection.End = connection.End; + FollowPointLifetimeEntry previousEntry = lifetimeEntries[index - 1]; + previousEntry.End = entry.End; } - - connections.Remove(connection); } - private void onStartTimeChanged(FollowPointConnection connection) + protected override bool CheckChildrenLife() => lifetimeManager.Update(Time.Current); + + private void onEntryBecameAlive(LifetimeEntry entry) { - // Naive but can be improved if performance becomes an issue - removeGroup(connection); - addConnection(connection); + var connection = connectionPool.Get(c => + { + c.Entry = (FollowPointLifetimeEntry)entry; + c.Pool = pointPool; + }); + + connectionsInUse[entry] = connection; + + AddInternal(connection); + MakeChildAlive(connection); + } + + private void onEntryBecameDead(LifetimeEntry entry) + { + RemoveInternal(connectionsInUse[entry]); + connectionsInUse.Remove(entry); + } + + public class FollowPointLifetimeEntry : LifetimeEntry + { + public readonly OsuHitObject Start; + + public FollowPointLifetimeEntry(OsuHitObject start) + { + Start = start; + + LifetimeStart = LifetimeEnd = Start.StartTime; + } + + private OsuHitObject end; + + public OsuHitObject End + { + get => end; + set + { + end = value; + computeLifetimes(); + } + } + + private void computeLifetimes() + { + if (end == null) + { + LifetimeEnd = LifetimeStart; + return; + } + + Vector2 startPosition = Start.StackedEndPosition; + Vector2 endPosition = End.StackedPosition; + Vector2 distanceVector = endPosition - startPosition; + float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length; + + double duration = End.StartTime - Start.GetEndTime(); + + double fadeOutTime = Start.StartTime + fraction * duration; + double fadeInTime = fadeOutTime - FollowPointConnection.PREEMPT; + + LifetimeStart = fadeInTime; + } } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index c816502d61..a8d9423bf6 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -125,13 +125,13 @@ namespace osu.Game.Rulesets.Osu.UI protected override void OnHitObjectAdded(HitObject hitObject) { base.OnHitObjectAdded(hitObject); - followPoints.AddFollowPoints((OsuHitObject)hitObject); + followPoints.AddFollowPoints2((OsuHitObject)hitObject); } protected override void OnHitObjectRemoved(HitObject hitObject) { base.OnHitObjectRemoved(hitObject); - followPoints.RemoveFollowPoints((OsuHitObject)hitObject); + followPoints.RemoveFollowPoints2((OsuHitObject)hitObject); } public void OnHitObjectLoaded(Drawable drawable) From 6356b2dde93c72b8fe4ee891b7cc649f3cca96ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 19 Nov 2020 22:11:52 +0100 Subject: [PATCH 4785/6909] Prevent editor from crashing for rulesets with no compose screen implementation --- osu.Game/Screens/Edit/Compose/ComposeScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index d9948aa23c..46d5eb40b4 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -32,7 +32,8 @@ namespace osu.Game.Screens.Edit.Compose composer = ruleset?.CreateHitObjectComposer(); // make the composer available to the timeline and other components in this screen. - dependencies.CacheAs(composer); + if (composer != null) + dependencies.CacheAs(composer); return dependencies; } From 1df3f88fc44275ee1ae1bfe3404259557c5d6b60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 12:32:17 +0900 Subject: [PATCH 4786/6909] 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 4657896fac..6dab6edc5e 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 704ac5a611..54f3fcede6 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 346bd892b0..692dac909a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From b547abafb2f4bd702b7448d7556485ba76e536b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 13:46:21 +0900 Subject: [PATCH 4787/6909] Fix slider right click context menus not being shown outside the valid playfield area Closes #10816. --- .../Sliders/Components/PathControlPointVisualiser.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs index 7375c0e981..ce5dc4855e 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointVisualiser.cs @@ -20,12 +20,15 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; +using osuTK; using osuTK.Input; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { public class PathControlPointVisualiser : CompositeDrawable, IKeyBindingHandler, IHasContextMenu { + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; // allow context menu to appear outside of the playfield. + internal readonly Container Pieces; internal readonly Container Connections; From a3145ed96dfbd45ff58fdd744e3490528ce00946 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 13:54:41 +0900 Subject: [PATCH 4788/6909] Fix test compile errors --- .../TestSceneFollowPoints.cs | 561 +++++++++--------- .../Connections/FollowPointRenderer.cs | 2 + 2 files changed, 287 insertions(+), 276 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index 3ebb747a21..ef6275a7e7 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -1,276 +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 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; -// using osu.Game.Rulesets.Osu.Objects.Drawables; -// using osu.Game.Rulesets.Osu.Objects.Drawables.Connections; -// using osu.Game.Tests.Visual; -// using osuTK; -// -// namespace osu.Game.Rulesets.Osu.Tests -// { -// public class TestSceneFollowPoints : OsuTestScene -// { -// private Container hitObjectContainer; -// private FollowPointRenderer followPointRenderer; -// -// [SetUp] -// public void Setup() => Schedule(() => -// { -// Children = new Drawable[] -// { -// hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both }, -// followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both } -// }; -// }); -// -// [Test] -// public void TestAddObject() -// { -// addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); -// -// assertGroups(); -// } -// -// [Test] -// public void TestRemoveObject() -// { -// addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); -// -// removeObjectStep(() => getObject(0)); -// -// assertGroups(); -// } -// -// [Test] -// public void TestAddMultipleObjects() -// { -// addMultipleObjectsStep(); -// -// assertGroups(); -// } -// -// [Test] -// public void TestRemoveEndObject() -// { -// addMultipleObjectsStep(); -// -// removeObjectStep(() => getObject(4)); -// -// assertGroups(); -// } -// -// [Test] -// public void TestRemoveStartObject() -// { -// addMultipleObjectsStep(); -// -// removeObjectStep(() => getObject(0)); -// -// assertGroups(); -// } -// -// [Test] -// public void TestRemoveMiddleObject() -// { -// addMultipleObjectsStep(); -// -// removeObjectStep(() => getObject(2)); -// -// assertGroups(); -// } -// -// [Test] -// public void TestMoveObject() -// { -// addMultipleObjectsStep(); -// -// AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); -// -// assertGroups(); -// } -// -// [TestCase(0, 0)] // Start -> Start -// [TestCase(0, 2)] // Start -> Middle -// [TestCase(0, 5)] // Start -> End -// [TestCase(2, 0)] // Middle -> Start -// [TestCase(1, 3)] // Middle -> Middle (forwards) -// [TestCase(3, 1)] // Middle -> Middle (backwards) -// [TestCase(4, 0)] // End -> Start -// [TestCase(4, 2)] // End -> Middle -// [TestCase(4, 4)] // End -> End -// public void TestReorderObjects(int startIndex, int endIndex) -// { -// addMultipleObjectsStep(); -// -// reorderObjectStep(startIndex, endIndex); -// -// 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) }, -// new HitCircle { Position = new Vector2(200, 200) }, -// new HitCircle { Position = new Vector2(300, 300) }, -// new HitCircle { Position = new Vector2(400, 400) }, -// new HitCircle { Position = new Vector2(500, 500) }, -// }); -// -// private void addObjectsStep(Func ctorFunc) -// { -// AddStep("add hitobjects", () => -// { -// var objects = ctorFunc(); -// -// for (int i = 0; i < objects.Length; i++) -// { -// objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1); -// objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); -// -// DrawableOsuHitObject drawableObject = null; -// -// switch (objects[i]) -// { -// case HitCircle circle: -// drawableObject = new DrawableHitCircle(circle); -// break; -// -// case Slider slider: -// drawableObject = new DrawableSlider(slider); -// break; -// -// case Spinner spinner: -// drawableObject = new DrawableSpinner(spinner); -// break; -// } -// -// hitObjectContainer.Add(drawableObject); -// followPointRenderer.AddFollowPoints(objects[i]); -// } -// }); -// } -// -// private void removeObjectStep(Func getFunc) -// { -// AddStep("remove hitobject", () => -// { -// var drawableObject = getFunc.Invoke(); -// -// hitObjectContainer.Remove(drawableObject); -// followPointRenderer.RemoveFollowPoints(drawableObject.HitObject); -// }); -// } -// -// private void reorderObjectStep(int startIndex, int endIndex) -// { -// AddStep($"move object {startIndex} to {endIndex}", () => -// { -// DrawableOsuHitObject toReorder = getObject(startIndex); -// -// double targetTime; -// if (endIndex < hitObjectContainer.Count) -// targetTime = getObject(endIndex).HitObject.StartTime - 1; -// else -// targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1; -// -// hitObjectContainer.Remove(toReorder); -// toReorder.HitObject.StartTime = targetTime; -// hitObjectContainer.Add(toReorder); -// }); -// } -// -// private void assertGroups() -// { -// AddAssert("has correct group count", () => followPointRenderer.Connections.Count == hitObjectContainer.Count); -// AddAssert("group endpoints 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 (getGroup(i).Start != expectedStart.HitObject) -// throw new AssertionException($"Object {i} expected to be the start of group {i}."); -// -// if (getGroup(i).End != expectedEnd?.HitObject) -// throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}."); -// } -// -// return true; -// }); -// } -// -// 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]; -// -// private class TestHitObjectContainer : Container -// { -// protected override int Compare(Drawable x, Drawable y) -// { -// var osuX = (DrawableOsuHitObject)x; -// var osuY = (DrawableOsuHitObject)y; -// -// int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime); -// -// if (compare == 0) -// return base.Compare(x, y); -// -// return compare; -// } -// } -// } -// } +// Copyright (c) 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; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Framework.Utils; +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.Osu.Objects.Drawables.Connections; +using osu.Game.Tests.Visual; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Tests +{ + public class TestSceneFollowPoints : OsuTestScene + { + private Container hitObjectContainer; + private FollowPointRenderer followPointRenderer; + + [SetUp] + public void Setup() => Schedule(() => + { + Children = new Drawable[] + { + hitObjectContainer = new TestHitObjectContainer { RelativeSizeAxes = Axes.Both }, + followPointRenderer = new FollowPointRenderer { RelativeSizeAxes = Axes.Both } + }; + }); + + [Test] + public void TestAddObject() + { + addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); + + assertGroups(); + } + + [Test] + public void TestRemoveObject() + { + addObjectsStep(() => new OsuHitObject[] { new HitCircle { Position = new Vector2(100, 100) } }); + + removeObjectStep(() => getObject(0)); + + assertGroups(); + } + + [Test] + public void TestAddMultipleObjects() + { + addMultipleObjectsStep(); + + assertGroups(); + } + + [Test] + public void TestRemoveEndObject() + { + addMultipleObjectsStep(); + + removeObjectStep(() => getObject(4)); + + assertGroups(); + } + + [Test] + public void TestRemoveStartObject() + { + addMultipleObjectsStep(); + + removeObjectStep(() => getObject(0)); + + assertGroups(); + } + + [Test] + public void TestRemoveMiddleObject() + { + addMultipleObjectsStep(); + + removeObjectStep(() => getObject(2)); + + assertGroups(); + } + + [Test] + public void TestMoveObject() + { + addMultipleObjectsStep(); + + AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); + + assertGroups(); + } + + [TestCase(0, 0)] // Start -> Start + [TestCase(0, 2)] // Start -> Middle + [TestCase(0, 5)] // Start -> End + [TestCase(2, 0)] // Middle -> Start + [TestCase(1, 3)] // Middle -> Middle (forwards) + [TestCase(3, 1)] // Middle -> Middle (backwards) + [TestCase(4, 0)] // End -> Start + [TestCase(4, 2)] // End -> Middle + [TestCase(4, 4)] // End -> End + public void TestReorderObjects(int startIndex, int endIndex) + { + addMultipleObjectsStep(); + + reorderObjectStep(startIndex, endIndex); + + 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) }, + new HitCircle { Position = new Vector2(200, 200) }, + new HitCircle { Position = new Vector2(300, 300) }, + new HitCircle { Position = new Vector2(400, 400) }, + new HitCircle { Position = new Vector2(500, 500) }, + }); + + private void addObjectsStep(Func ctorFunc) + { + AddStep("add hitobjects", () => + { + var objects = ctorFunc(); + + for (int i = 0; i < objects.Length; i++) + { + objects[i].StartTime = Time.Current + 1000 + 500 * (i + 1); + objects[i].ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + DrawableOsuHitObject drawableObject = null; + + switch (objects[i]) + { + case HitCircle circle: + drawableObject = new DrawableHitCircle(circle); + break; + + case Slider slider: + drawableObject = new DrawableSlider(slider); + break; + + case Spinner spinner: + drawableObject = new DrawableSpinner(spinner); + break; + } + + hitObjectContainer.Add(drawableObject); + followPointRenderer.AddFollowPoints2(objects[i]); + } + }); + } + + private void removeObjectStep(Func getFunc) + { + AddStep("remove hitobject", () => + { + var drawableObject = getFunc.Invoke(); + + hitObjectContainer.Remove(drawableObject); + followPointRenderer.RemoveFollowPoints2(drawableObject.HitObject); + }); + } + + private void reorderObjectStep(int startIndex, int endIndex) + { + AddStep($"move object {startIndex} to {endIndex}", () => + { + DrawableOsuHitObject toReorder = getObject(startIndex); + + double targetTime; + if (endIndex < hitObjectContainer.Count) + targetTime = getObject(endIndex).HitObject.StartTime - 1; + else + targetTime = getObject(hitObjectContainer.Count - 1).HitObject.StartTime + 1; + + hitObjectContainer.Remove(toReorder); + toReorder.HitObject.StartTime = targetTime; + hitObjectContainer.Add(toReorder); + }); + } + + private void assertGroups() + { + AddAssert("has correct group count", () => followPointRenderer.Entries.Count == hitObjectContainer.Count); + AddAssert("group endpoints 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 (getEntry(i).Start != expectedStart.HitObject) + throw new AssertionException($"Object {i} expected to be the start of group {i}."); + + if (getEntry(i).End != expectedEnd?.HitObject) + throw new AssertionException($"Object {(expectedEnd == null ? "null" : i.ToString())} expected to be the end of group {i}."); + } + + return true; + }); + } + + 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 manualClock = new ManualClock(); + followPointRenderer.Clock = new FramedClock(manualClock); + + manualClock.CurrentTime = expectedStart.HitObject.StartTime; + followPointRenderer.UpdateSubTree(); + + 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 FollowPointRenderer.FollowPointLifetimeEntry getEntry(int index) => followPointRenderer.Entries[index]; + + private FollowPointConnection getGroup(int index) => followPointRenderer.ChildrenOfType().Single(c => c.Entry == getEntry(index)); + + private class TestHitObjectContainer : Container + { + protected override int Compare(Drawable x, Drawable y) + { + var osuX = (DrawableOsuHitObject)x; + var osuY = (DrawableOsuHitObject)y; + + int compare = osuX.HitObject.StartTime.CompareTo(osuY.HitObject.StartTime); + + if (compare == 0) + return base.Compare(x, y); + + return compare; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index ac7b78a25b..702983b74a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -20,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { public override bool RemoveCompletedTransforms => false; + public IReadOnlyList Entries => lifetimeEntries; + private DrawablePool connectionPool; private DrawablePool pointPool; From 17ff7fe163499324d044cac56267c4b852c56261 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 13:55:01 +0900 Subject: [PATCH 4789/6909] Fix failing test due to early lifetime end --- .../Objects/Drawables/Connections/FollowPointRenderer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 702983b74a..6751585def 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -143,8 +143,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections public FollowPointLifetimeEntry(OsuHitObject start) { Start = start; - - LifetimeStart = LifetimeEnd = Start.StartTime; + LifetimeStart = Start.StartTime; } private OsuHitObject end; @@ -178,6 +177,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections double fadeInTime = fadeOutTime - FollowPointConnection.PREEMPT; LifetimeStart = fadeInTime; + LifetimeEnd = double.MaxValue; // This will be set by the connection. } } } From 2fc53a278daa7ffd27b8ba8affdb6ed1d71f226d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 14:10:28 +0900 Subject: [PATCH 4790/6909] Add back reorder support --- .../Connections/FollowPointRenderer.cs | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 6751585def..6bb32238bd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -27,6 +28,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections private readonly List lifetimeEntries = new List(); private readonly Dictionary connectionsInUse = new Dictionary(); + private readonly Dictionary startTimeMap = new Dictionary(); private readonly LifetimeEntryManager lifetimeManager = new LifetimeEntryManager(); public FollowPointRenderer() @@ -49,6 +51,23 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections } public void AddFollowPoints2(OsuHitObject hitObject) + { + addEntry(hitObject); + + var startTimeBindable = hitObject.StartTimeBindable.GetBoundCopy(); + startTimeBindable.ValueChanged += _ => onStartTimeChanged(hitObject); + startTimeMap[hitObject] = startTimeBindable; + } + + public void RemoveFollowPoints2(OsuHitObject hitObject) + { + removeEntry(hitObject); + + startTimeMap[hitObject].UnbindAll(); + startTimeMap.Remove(hitObject); + } + + private void addEntry(OsuHitObject hitObject) { var newEntry = new FollowPointLifetimeEntry(hitObject); @@ -95,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections lifetimeManager.AddEntry(newEntry); } - public void RemoveFollowPoints2(OsuHitObject hitObject) + private void removeEntry(OsuHitObject hitObject) { int index = lifetimeEntries.FindIndex(e => e.Start == hitObject); var entry = lifetimeEntries[index]; @@ -136,6 +155,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections connectionsInUse.Remove(entry); } + private void onStartTimeChanged(OsuHitObject hitObject) + { + removeEntry(hitObject); + addEntry(hitObject); + } + public class FollowPointLifetimeEntry : LifetimeEntry { public readonly OsuHitObject Start; From c53a8fafe6d648c1dfdc4be449e35a3d2c11bc88 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 14:25:07 +0900 Subject: [PATCH 4791/6909] Make test fail as expected --- osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index ef6275a7e7..a6a7f54886 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -95,9 +95,19 @@ namespace osu.Game.Rulesets.Osu.Tests { addMultipleObjectsStep(); - AddStep("move hitobject", () => getObject(2).HitObject.Position = new Vector2(300, 100)); + AddStep("move hitobject", () => + { + var manualClock = new ManualClock(); + followPointRenderer.Clock = new FramedClock(manualClock); + + manualClock.CurrentTime = getObject(1).HitObject.StartTime; + followPointRenderer.UpdateSubTree(); + + getObject(2).HitObject.Position = new Vector2(300, 100); + }); assertGroups(); + assertDirections(); } [TestCase(0, 0)] // Start -> Start From 8c32d3f78194b75deb5d28b390a9f14272f557c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 14:32:23 +0900 Subject: [PATCH 4792/6909] Don't play flair animations / sounds when watching autoplay or viewing a result directly I think this is a pretty good place to be for now. The flair will play if you just watched a play (local, replay or spectator) but will not play if you are coming from song select (viewing a replay's result screen from the leaderboard) or in the case of autoplay. Closes #10762. --- osu.Game/Screens/Ranking/ResultsScreen.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index f8bdf0140c..ce3e618889 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -16,6 +16,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Online.API; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; @@ -149,7 +150,12 @@ namespace osu.Game.Screens.Ranking }; if (Score != null) - ScorePanelList.AddScore(Score, true); + { + // only show flair / animation when arriving after watching a play that isn't autoplay. + bool shouldFlair = player != null && !Score.Mods.Any(m => m is ModAutoplay); + + ScorePanelList.AddScore(Score, shouldFlair); + } if (player != null && allowRetry) { From 2db42f8e6780eb2fd67854b54413ce8506a2895a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 14:35:44 +0900 Subject: [PATCH 4793/6909] Remove default allowRetry parameter value from ResultsScreen --- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 2 +- osu.Game/OsuGame.cs | 2 +- osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs | 2 +- osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs | 2 +- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Screens/Play/SpectatorResultsScreen.cs | 2 +- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- osu.Game/Screens/Select/PlaySongSelect.cs | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index ff96a999ec..44a2056732 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -326,7 +326,7 @@ namespace osu.Game.Tests.Visual.Ranking public HotkeyRetryOverlay RetryOverlay; public UnrankedSoloResultsScreen(ScoreInfo score) - : base(score) + : base(score, false) { Score.Beatmap.OnlineBeatmapID = 0; Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending; diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index cb0e2cfa8e..bb638bcf3a 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -420,7 +420,7 @@ namespace osu.Game break; case ScorePresentType.Results: - screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo)); + screen.Push(new SoloResultsScreen(databasedScore.ScoreInfo, false)); break; } }, validScreens: new[] { typeof(PlaySongSelect) }); diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 04da943a10..0efa9c5196 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -92,7 +92,7 @@ namespace osu.Game.Screens.Multi.Play protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(roomId.Value != null); - return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem); + return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true); } protected override ScoreInfo CreateScore() diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 8da6a530a8..3623208fa7 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Ranking [Resolved] private IAPIProvider api { get; set; } - public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry = true) + public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry) : base(score, allowRetry) { this.roomId = roomId; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b94f0a5062..d0a83e3c22 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -545,7 +545,7 @@ namespace osu.Game.Screens.Play protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; - protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score); + protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); #region Fail Logic diff --git a/osu.Game/Screens/Play/SpectatorResultsScreen.cs b/osu.Game/Screens/Play/SpectatorResultsScreen.cs index 56ccfd2253..dabdf0a139 100644 --- a/osu.Game/Screens/Play/SpectatorResultsScreen.cs +++ b/osu.Game/Screens/Play/SpectatorResultsScreen.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Play public class SpectatorResultsScreen : SoloResultsScreen { public SpectatorResultsScreen(ScoreInfo score) - : base(score) + : base(score, false) { } diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 9cf2e6757a..76b549da1a 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -18,7 +18,7 @@ namespace osu.Game.Screens.Ranking [Resolved] private RulesetStore rulesets { get; set; } - public SoloResultsScreen(ScoreInfo score, bool allowRetry = true) + public SoloResultsScreen(ScoreInfo score, bool allowRetry) : base(score, allowRetry) { } diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index ee8825640c..50a61ed4c2 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Select } protected void PresentScore(ScoreInfo score) => - FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score))); + FinaliseSelection(score.Beatmap, score.Ruleset, () => this.Push(new SoloResultsScreen(score, false))); protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); From af67b4a346a7147a6132415955b3f25cc99accf3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 14:57:08 +0900 Subject: [PATCH 4794/6909] Remove no longer necessary code from OsuPlayfield --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index c816502d61..ab77be488e 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -5,7 +5,6 @@ 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.Pooling; @@ -40,8 +39,6 @@ namespace osu.Game.Rulesets.Osu.UI protected override GameplayCursorContainer CreateCursor() => new OsuCursorContainer(); - private readonly Bindable playfieldBorderStyle = new BindableBool(); - private readonly IDictionary> poolDictionary = new Dictionary>(); public OsuPlayfield() @@ -67,12 +64,7 @@ namespace osu.Game.Rulesets.Osu.UI RelativeSizeAxes = Axes.Both, Depth = 1, }, - // Todo: This should not exist, but currently helps to reduce LOH allocations due to unbinding skin source events on judgement disposal - // Todo: Remove when hitobjects are properly pooled - new SkinProvidingContainer(null) - { - Child = HitObjectContainer, - }, + HitObjectContainer, approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both, From 45b1fcf26fcbc3ff9befe6637a538978a3e5d01d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 15:01:37 +0900 Subject: [PATCH 4795/6909] Remove unnecessary using statement --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index ab77be488e..6a59cdf112 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -19,7 +19,6 @@ 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 185653b1d8b65ac4d0ce7b0ab4fe25a91b00d22b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 15:11:24 +0900 Subject: [PATCH 4796/6909] Remove depth specifications --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 29 ++++-------------------- 1 file changed, 5 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 6a59cdf112..0e98a1d439 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -44,31 +44,12 @@ namespace osu.Game.Rulesets.Osu.UI { InternalChildren = new Drawable[] { - playfieldBorder = new PlayfieldBorder - { - RelativeSizeAxes = Axes.Both, - Depth = 3 - }, - spinnerProxies = new ProxyContainer - { - RelativeSizeAxes = Axes.Both - }, - followPoints = new FollowPointRenderer - { - RelativeSizeAxes = Axes.Both, - Depth = 2, - }, - judgementLayer = new JudgementContainer - { - RelativeSizeAxes = Axes.Both, - Depth = 1, - }, + playfieldBorder = new PlayfieldBorder { RelativeSizeAxes = Axes.Both }, + spinnerProxies = new ProxyContainer { RelativeSizeAxes = Axes.Both }, + followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, + judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both }, HitObjectContainer, - approachCircles = new ProxyContainer - { - RelativeSizeAxes = Axes.Both, - Depth = -1, - }, + approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; hitPolicy = new OrderedHitPolicy(HitObjectContainer); From 33eea64cfc584bcd691ca3e200ad4c79f06a5956 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 15:31:04 +0900 Subject: [PATCH 4797/6909] Fix follow points not updating on positional changes --- .../Connections/FollowPointConnection.cs | 29 +++++++-- .../Connections/FollowPointRenderer.cs | 60 ++++++++++++++++++- 2 files changed, 81 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 1d82e91c0e..fe35cd02dc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -21,16 +21,33 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections public FollowPointRenderer.FollowPointLifetimeEntry Entry; public DrawablePool Pool; - protected override void FreeAfterUse() - { - base.FreeAfterUse(); - ClearInternal(false); - } - protected override void PrepareForUse() { base.PrepareForUse(); + Entry.Invalidated += onEntryInvalidated; + + refreshPoints(); + } + + protected override void FreeAfterUse() + { + base.FreeAfterUse(); + + Entry.Invalidated -= onEntryInvalidated; + + // Return points to the pool. + ClearInternal(false); + + Entry = null; + } + + private void onEntryInvalidated() => refreshPoints(); + + private void refreshPoints() + { + ClearInternal(false); + OsuHitObject start = Entry.Start; OsuHitObject end = Entry.End; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 6bb32238bd..205c5c0d05 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -117,7 +118,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections private void removeEntry(OsuHitObject hitObject) { int index = lifetimeEntries.FindIndex(e => e.Start == hitObject); + var entry = lifetimeEntries[index]; + entry.UnbindEvents(); lifetimeEntries.RemoveAt(index); lifetimeManager.RemoveEntry(entry); @@ -161,14 +164,26 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections addEntry(hitObject); } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + foreach (var entry in lifetimeEntries) + entry.UnbindEvents(); + lifetimeEntries.Clear(); + } + public class FollowPointLifetimeEntry : LifetimeEntry { + public event Action Invalidated; public readonly OsuHitObject Start; public FollowPointLifetimeEntry(OsuHitObject start) { Start = start; LifetimeStart = Start.StartTime; + + bindEvents(); } private OsuHitObject end; @@ -178,12 +193,51 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections get => end; set { + UnbindEvents(); + end = value; - computeLifetimes(); + + bindEvents(); + + refreshLifetimes(); } } - private void computeLifetimes() + private void bindEvents() + { + UnbindEvents(); + + // Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects. + Start.DefaultsApplied += onDefaultsApplied; + Start.PositionBindable.ValueChanged += onPositionChanged; + + if (End != null) + { + End.DefaultsApplied += onDefaultsApplied; + End.PositionBindable.ValueChanged += onPositionChanged; + } + } + + public void UnbindEvents() + { + if (Start != null) + { + Start.DefaultsApplied -= onDefaultsApplied; + Start.PositionBindable.ValueChanged -= onPositionChanged; + } + + if (End != null) + { + End.DefaultsApplied -= onDefaultsApplied; + End.PositionBindable.ValueChanged -= onPositionChanged; + } + } + + private void onDefaultsApplied(HitObject obj) => refreshLifetimes(); + + private void onPositionChanged(ValueChangedEvent obj) => refreshLifetimes(); + + private void refreshLifetimes() { if (end == null) { @@ -203,6 +257,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections LifetimeStart = fadeInTime; LifetimeEnd = double.MaxValue; // This will be set by the connection. + + Invalidated?.Invoke(); } } } From 2ed2ddfe8a05f24bcc60f84f0e6eb9b96aaf0bd7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 15:39:25 +0900 Subject: [PATCH 4798/6909] Rename methods --- osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs | 4 ++-- .../Objects/Drawables/Connections/FollowPointRenderer.cs | 4 ++-- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index a6a7f54886..e7c73b941d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -182,7 +182,7 @@ namespace osu.Game.Rulesets.Osu.Tests } hitObjectContainer.Add(drawableObject); - followPointRenderer.AddFollowPoints2(objects[i]); + followPointRenderer.AddFollowPoints(objects[i]); } }); } @@ -194,7 +194,7 @@ namespace osu.Game.Rulesets.Osu.Tests var drawableObject = getFunc.Invoke(); hitObjectContainer.Remove(drawableObject); - followPointRenderer.RemoveFollowPoints2(drawableObject.HitObject); + followPointRenderer.RemoveFollowPoints(drawableObject.HitObject); }); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 205c5c0d05..6dea16d01f 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections MakeChildAlive(pointPool); } - public void AddFollowPoints2(OsuHitObject hitObject) + public void AddFollowPoints(OsuHitObject hitObject) { addEntry(hitObject); @@ -60,7 +60,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections startTimeMap[hitObject] = startTimeBindable; } - public void RemoveFollowPoints2(OsuHitObject hitObject) + public void RemoveFollowPoints(OsuHitObject hitObject) { removeEntry(hitObject); diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index a8d9423bf6..c816502d61 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -125,13 +125,13 @@ namespace osu.Game.Rulesets.Osu.UI protected override void OnHitObjectAdded(HitObject hitObject) { base.OnHitObjectAdded(hitObject); - followPoints.AddFollowPoints2((OsuHitObject)hitObject); + followPoints.AddFollowPoints((OsuHitObject)hitObject); } protected override void OnHitObjectRemoved(HitObject hitObject) { base.OnHitObjectRemoved(hitObject); - followPoints.RemoveFollowPoints2((OsuHitObject)hitObject); + followPoints.RemoveFollowPoints((OsuHitObject)hitObject); } public void OnHitObjectLoaded(Drawable drawable) From 2418f17b0cd11ea01bf59a1fba1eae70e81f4ee0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 16:19:02 +0900 Subject: [PATCH 4799/6909] Fix lifetime not being set correctly in some cases --- .../Objects/Drawables/Connections/FollowPointConnection.cs | 3 --- .../Objects/Drawables/Connections/FollowPointRenderer.cs | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index fe35cd02dc..3f6854786b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -53,9 +53,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections double startTime = start.GetEndTime(); - if (end == null || end.NewCombo || start is Spinner || end is Spinner) - return; - Vector2 startPosition = start.StackedEndPosition; Vector2 endPosition = end.StackedPosition; double endTime = end.StartTime; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 6dea16d01f..7067cf2728 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections private void refreshLifetimes() { - if (end == null) + if (End == null || End.NewCombo || Start is Spinner || End is Spinner) { LifetimeEnd = LifetimeStart; return; From d4054c87d3d86bcc54e1e8024d6182956f21e895 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 16:15:17 +0900 Subject: [PATCH 4800/6909] Refactor TestSceneHitCircle to show judgements --- .../TestSceneHitCircle.cs | 67 ++++++++++++------- 1 file changed, 41 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs index 596bc06c68..1278a0ff2d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneHitCircle.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.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; -using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; -using osuTK; -using osu.Game.Rulesets.Mods; 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.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Scoring; +using osuTK; namespace osu.Game.Rulesets.Osu.Tests { @@ -38,13 +38,37 @@ namespace osu.Game.Rulesets.Osu.Tests } private Drawable testSingle(float circleSize, bool auto = false, double timeOffset = 0, Vector2? positionOffset = null) + { + var drawable = createSingle(circleSize, auto, timeOffset, positionOffset); + + var playfield = new TestOsuPlayfield(); + playfield.Add(drawable); + return playfield; + } + + private Drawable testStream(float circleSize, bool auto = false) + { + var playfield = new TestOsuPlayfield(); + + Vector2 pos = new Vector2(-250, 0); + + for (int i = 0; i <= 1000; i += 100) + { + playfield.Add(createSingle(circleSize, auto, i, pos)); + pos.X += 50; + } + + return playfield; + } + + private TestDrawableHitCircle createSingle(float circleSize, bool auto, double timeOffset, Vector2? positionOffset) { positionOffset ??= Vector2.Zero; var circle = new HitCircle { StartTime = Time.Current + 1000 + timeOffset, - Position = positionOffset.Value, + Position = OsuPlayfield.BASE_SIZE / 4 + positionOffset.Value, }; circle.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); @@ -53,31 +77,14 @@ namespace osu.Game.Rulesets.Osu.Tests foreach (var mod in SelectedMods.Value.OfType()) mod.ApplyToDrawableHitObjects(new[] { drawable }); - return drawable; } protected virtual TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) => new TestDrawableHitCircle(circle, auto) { - Anchor = Anchor.Centre, Depth = depthIndex++ }; - private Drawable testStream(float circleSize, bool auto = false) - { - var container = new Container { RelativeSizeAxes = Axes.Both }; - - Vector2 pos = new Vector2(-250, 0); - - for (int i = 0; i <= 1000; i += 100) - { - container.Add(testSingle(circleSize, auto, i, pos)); - pos.X += 50; - } - - return container; - } - protected class TestDrawableHitCircle : DrawableHitCircle { private readonly bool auto; @@ -101,5 +108,13 @@ namespace osu.Game.Rulesets.Osu.Tests base.CheckForResult(userTriggered, timeOffset); } } + + protected class TestOsuPlayfield : OsuPlayfield + { + public TestOsuPlayfield() + { + RelativeSizeAxes = Axes.Both; + } + } } } From 7fe0923fcfbbabd2befa0eda3fe07693dc7c70be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 16:14:38 +0900 Subject: [PATCH 4801/6909] Show main judgement content above hitobjects --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 16 ++++++++++++++-- .../Judgements/DefaultJudgementPiece.cs | 2 ++ .../Rulesets/Judgements/DrawableJudgement.cs | 19 +++++++++++++++++++ .../Judgements/IAnimatableJudgement.cs | 8 ++++++++ osu.Game/Skinning/LegacyJudgementPieceNew.cs | 4 ++++ osu.Game/Skinning/LegacyJudgementPieceOld.cs | 2 ++ 6 files changed, 49 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 0e98a1d439..3bd150c4d3 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -40,6 +40,8 @@ namespace osu.Game.Rulesets.Osu.UI private readonly IDictionary> poolDictionary = new Dictionary>(); + private readonly Container judgementAboveHitObjectLayer; + public OsuPlayfield() { InternalChildren = new Drawable[] @@ -49,6 +51,7 @@ namespace osu.Game.Rulesets.Osu.UI followPoints = new FollowPointRenderer { RelativeSizeAxes = Axes.Both }, judgementLayer = new JudgementContainer { RelativeSizeAxes = Axes.Both }, HitObjectContainer, + judgementAboveHitObjectLayer = new Container { RelativeSizeAxes = Axes.Both }, approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; @@ -58,13 +61,18 @@ namespace osu.Game.Rulesets.Osu.UI 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)); + poolDictionary.Add(result, new DrawableJudgementPool(result, onJudgmentLoaded)); AddRangeInternal(poolDictionary.Values); NewResult += onNewResult; } + private void onJudgmentLoaded(DrawableOsuJudgement judgement) + { + judgementAboveHitObjectLayer.Add(judgement.GetProxyAboveHitObjectsContent()); + } + [BackgroundDependencyLoader(true)] private void load(OsuRulesetConfigManager config) { @@ -150,11 +158,13 @@ namespace osu.Game.Rulesets.Osu.UI private class DrawableJudgementPool : DrawablePool { private readonly HitResult result; + private readonly Action onLoaded; - public DrawableJudgementPool(HitResult result) + public DrawableJudgementPool(HitResult result, Action onLoaded) : base(10) { this.result = result; + this.onLoaded = onLoaded; } protected override DrawableOsuJudgement CreateNewDrawable() @@ -164,6 +174,8 @@ namespace osu.Game.Rulesets.Osu.UI // just a placeholder to initialise the correct drawable hierarchy for this pool. judgement.Apply(new JudgementResult(new HitObject(), new Judgement()) { Type = result }, null); + onLoaded?.Invoke(judgement); + return judgement; } } diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index b89c1f4e4f..c26dee119d 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -69,5 +69,7 @@ namespace osu.Game.Rulesets.Judgements this.FadeOutFromOne(800); } + + public Drawable GetAboveHitObjectsProxiedContent() => this.CreateProxy(); } } diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 4aed38e689..3063656aaf 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; @@ -29,6 +30,8 @@ namespace osu.Game.Rulesets.Judgements protected SkinnableDrawable JudgementBody { get; private set; } + private readonly Container aboveHitObjectsContent; + [Resolved] private ISkinSource skinSource { get; set; } @@ -59,6 +62,12 @@ namespace osu.Game.Rulesets.Judgements { Size = new Vector2(judgement_size); Origin = Anchor.Centre; + + AddInternal(aboveHitObjectsContent = new Container + { + Depth = float.MinValue, + RelativeSizeAxes = Axes.Both + }); } [BackgroundDependencyLoader] @@ -67,6 +76,8 @@ namespace osu.Game.Rulesets.Judgements prepareDrawables(); } + public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy(); + protected override void LoadComplete() { base.LoadComplete(); @@ -189,6 +200,7 @@ namespace osu.Game.Rulesets.Judgements if (JudgementBody != null) RemoveInternal(JudgementBody); + aboveHitObjectsContent.Clear(); AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent(type), _ => CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) { @@ -196,6 +208,13 @@ namespace osu.Game.Rulesets.Judgements Origin = Anchor.Centre, }); + if (JudgementBody.Drawable is IAnimatableJudgement animatable) + { + var proxiedContent = animatable.GetAboveHitObjectsProxiedContent(); + if (proxiedContent != null) + aboveHitObjectsContent.Add(proxiedContent); + } + currentDrawableType = type; } diff --git a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs index 32312f1115..d37a270a20 100644 --- a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs @@ -10,6 +10,14 @@ namespace osu.Game.Rulesets.Judgements /// public interface IAnimatableJudgement : IDrawable { + /// + /// Start the animation for this judgement from the current point in time. + /// void PlayAnimation(); + + /// + /// Get proxied content which should be displayed above all hitobjects. + /// + Drawable GetAboveHitObjectsProxiedContent(); } } diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs index 2a53820872..290571f46c 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceNew.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceNew.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; @@ -121,5 +122,8 @@ namespace osu.Game.Skinning break; } } + + [CanBeNull] + public Drawable GetAboveHitObjectsProxiedContent() => temporaryOldStyle?.CreateProxy(); // for new style judgements, only the old style temporary display is in front of objects. } } diff --git a/osu.Game/Skinning/LegacyJudgementPieceOld.cs b/osu.Game/Skinning/LegacyJudgementPieceOld.cs index 3486dce081..5d74ab9ae3 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceOld.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceOld.cs @@ -69,5 +69,7 @@ namespace osu.Game.Skinning break; } } + + public Drawable GetAboveHitObjectsProxiedContent() => CreateProxy(); } } From a00e0d72793fc97724d3d94e313a9cfaa6f0438b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 16:30:48 +0900 Subject: [PATCH 4802/6909] Move CanBeNull specification to the interface --- osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs | 2 ++ osu.Game/Skinning/LegacyJudgementPieceNew.cs | 2 -- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs index d37a270a20..b38b83b534 100644 --- a/osu.Game/Rulesets/Judgements/IAnimatableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/IAnimatableJudgement.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.Graphics; namespace osu.Game.Rulesets.Judgements @@ -18,6 +19,7 @@ namespace osu.Game.Rulesets.Judgements /// /// Get proxied content which should be displayed above all hitobjects. /// + [CanBeNull] Drawable GetAboveHitObjectsProxiedContent(); } } diff --git a/osu.Game/Skinning/LegacyJudgementPieceNew.cs b/osu.Game/Skinning/LegacyJudgementPieceNew.cs index 290571f46c..ca25efaa01 100644 --- a/osu.Game/Skinning/LegacyJudgementPieceNew.cs +++ b/osu.Game/Skinning/LegacyJudgementPieceNew.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.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; @@ -123,7 +122,6 @@ namespace osu.Game.Skinning } } - [CanBeNull] public Drawable GetAboveHitObjectsProxiedContent() => temporaryOldStyle?.CreateProxy(); // for new style judgements, only the old style temporary display is in front of objects. } } From 53b6d90ab4822e79b3c0569082580eaf2e5339df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 16:30:58 +0900 Subject: [PATCH 4803/6909] Don't show default judgements in front of objects for now --- osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index c26dee119d..d94346cb72 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -70,6 +70,6 @@ namespace osu.Game.Rulesets.Judgements this.FadeOutFromOne(800); } - public Drawable GetAboveHitObjectsProxiedContent() => this.CreateProxy(); + public Drawable GetAboveHitObjectsProxiedContent() => null; } } From 85eb98a7ec631531e545f896c2233445733db0af Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 16:43:07 +0900 Subject: [PATCH 4804/6909] Clean up some code reuse --- .../Connections/FollowPointConnection.cs | 23 +++++++++++++++---- .../Connections/FollowPointRenderer.cs | 8 +++---- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 3f6854786b..f9a594a3cb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -55,12 +55,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections Vector2 startPosition = start.StackedEndPosition; Vector2 endPosition = end.StackedPosition; - double endTime = end.StartTime; Vector2 distanceVector = endPosition - startPosition; int distance = (int)distanceVector.Length; float rotation = (float)(Math.Atan2(distanceVector.Y, distanceVector.X) * (180 / Math.PI)); - double duration = endTime - startTime; double finalTransformEndTime = startTime; @@ -69,8 +67,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections float fraction = (float)d / distance; Vector2 pointStartPosition = startPosition + (fraction - 0.1f) * distanceVector; Vector2 pointEndPosition = startPosition + fraction * distanceVector; - double fadeOutTime = startTime + fraction * duration; - double fadeInTime = fadeOutTime - PREEMPT; + + GetFadeTimes(start, end, (float)d / distance, out var fadeInTime, out var fadeOutTime); FollowPoint fp; @@ -98,5 +96,22 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections // todo: use Expire() on FollowPoints and take lifetime from them when https://github.com/ppy/osu-framework/issues/3300 is fixed. Entry.LifetimeEnd = finalTransformEndTime; } + + /// + /// Computes the fade time of follow point positioned between two hitobjects. + /// + /// The first , where follow points should originate from. + /// The second , which follow points should target. + /// The fractional distance along and at which the follow point is to be located. + /// The fade-in time of the follow point/ + /// The fade-out time of the follow point. + public static void GetFadeTimes(OsuHitObject start, OsuHitObject end, float fraction, out double fadeInTime, out double fadeOutTime) + { + double startTime = start.GetEndTime(); + double duration = end.StartTime - startTime; + + fadeOutTime = startTime + fraction * duration; + fadeInTime = fadeOutTime - PREEMPT; + } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 7067cf2728..b1914fde85 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -248,12 +248,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections Vector2 startPosition = Start.StackedEndPosition; Vector2 endPosition = End.StackedPosition; Vector2 distanceVector = endPosition - startPosition; + + // The lifetime start will match the fade-in time of the first follow point. float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length; - - double duration = End.StartTime - Start.GetEndTime(); - - double fadeOutTime = Start.StartTime + fraction * duration; - double fadeInTime = fadeOutTime - FollowPointConnection.PREEMPT; + FollowPointConnection.GetFadeTimes(Start, End, fraction, out var fadeInTime, out _); LifetimeStart = fadeInTime; LifetimeEnd = double.MaxValue; // This will be set by the connection. From 3b600f0a7b2dda8fa17ba85ee1563526bad44ece Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Fri, 20 Nov 2020 10:45:19 +0300 Subject: [PATCH 4805/6909] Target net5.0 instead of netcoreapp3 --- global.json | 2 +- osu.Desktop/osu.Desktop.csproj | 2 +- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- .../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 +- 9 files changed, 9 insertions(+), 9 deletions(-) diff --git a/global.json b/global.json index 10b61047ac..2cb4c02970 100644 --- a/global.json +++ b/global.json @@ -2,7 +2,7 @@ "sdk": { "allowPrerelease": false, "rollForward": "minor", - "version": "3.1.100" + "version": "5.0.100" }, "msbuild-sdks": { "Microsoft.Build.Traversal": "2.2.3" diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 62e8f7c518..7f5154f456 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -1,6 +1,6 @@  - netcoreapp3.1 + net5.0 WinExe true A free-to-win rhythm game. Rhythm is just a *click* away! diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index ff26f4afaa..7805bfcefc 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -1,7 +1,7 @@ - netcoreapp3.1 + net5.0 Exe false 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 dfe3bf8af4..a51b9830be 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 @@ -9,7 +9,7 @@ WinExe - netcoreapp3.1 + net5.0 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 892f27d27f..d314671bce 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 @@ -9,7 +9,7 @@ WinExe - netcoreapp3.1 + net5.0 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 3639c3616f..b0799bd3f5 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 @@ -9,7 +9,7 @@ WinExe - netcoreapp3.1 + net5.0 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 b59f3a4344..d3dbba4bfc 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 @@ -9,7 +9,7 @@ WinExe - netcoreapp3.1 + net5.0 diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index c692bcd5e4..34de54411b 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -10,7 +10,7 @@ WinExe - netcoreapp3.1 + net5.0 diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 5d55196dcf..d820794980 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -11,7 +11,7 @@ WinExe - netcoreapp3.1 + net5.0 From ca0e1c8cee1ffd03b2bb5fe773741012b90bb654 Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Fri, 20 Nov 2020 10:47:57 +0300 Subject: [PATCH 4806/6909] Update NuGet packages --- osu.Desktop/osu.Desktop.csproj | 4 ++-- osu.Game.Tournament/osu.Game.Tournament.csproj | 2 +- osu.Game/osu.Game.csproj | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 7f5154f456..2052c4bc25 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,11 +24,11 @@ - + - + diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj index 9cce40c9d3..b049542bb0 100644 --- a/osu.Game.Tournament/osu.Game.Tournament.csproj +++ b/osu.Game.Tournament/osu.Game.Tournament.csproj @@ -9,6 +9,6 @@ - + \ No newline at end of file diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 54f3fcede6..1670bf5b11 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,8 +21,8 @@ - - + + @@ -31,6 +31,6 @@ - + From c1f56cd0ba5d2deb9b7187e9915d8b74b26ee1c1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 16:49:22 +0900 Subject: [PATCH 4807/6909] Remove aliveness hackery --- .../Connections/FollowPointRenderer.cs | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index b1914fde85..43914c4d57 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs @@ -43,12 +43,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { InternalChildren = new Drawable[] { - connectionPool = new DrawablePool(1, 200), - pointPool = new DrawablePool(50, 1000) + connectionPool = new DrawablePoolNoLifetime(1, 200), + pointPool = new DrawablePoolNoLifetime(50, 1000) }; - - MakeChildAlive(connectionPool); - MakeChildAlive(pointPool); } public void AddFollowPoints(OsuHitObject hitObject) @@ -136,7 +133,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections } } - protected override bool CheckChildrenLife() => lifetimeManager.Update(Time.Current); + protected override bool CheckChildrenLife() + { + bool anyAliveChanged = base.CheckChildrenLife(); + anyAliveChanged |= lifetimeManager.Update(Time.Current); + return anyAliveChanged; + } private void onEntryBecameAlive(LifetimeEntry entry) { @@ -149,7 +151,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections connectionsInUse[entry] = connection; AddInternal(connection); - MakeChildAlive(connection); } private void onEntryBecameDead(LifetimeEntry entry) @@ -173,6 +174,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections lifetimeEntries.Clear(); } + private class DrawablePoolNoLifetime : DrawablePool + where T : PoolableDrawable, new() + { + public override bool RemoveWhenNotAlive => false; + + public DrawablePoolNoLifetime(int initialSize, int? maximumSize = null) + : base(initialSize, maximumSize) + { + } + } + public class FollowPointLifetimeEntry : LifetimeEntry { public event Action Invalidated; From 7bd75eca816adada1857bbb1445b0f469eeb1aab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 16:53:08 +0900 Subject: [PATCH 4808/6909] Separate classes --- .../TestSceneFollowPoints.cs | 2 +- .../Connections/FollowPointConnection.cs | 2 +- .../Connections/FollowPointLifetimeEntry.cs | 98 +++++++++++++++++++ .../Connections/FollowPointRenderer.cs | 89 ----------------- 4 files changed, 100 insertions(+), 91 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs index e7c73b941d..fe67b63252 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneFollowPoints.cs @@ -272,7 +272,7 @@ namespace osu.Game.Rulesets.Osu.Tests private DrawableOsuHitObject getObject(int index) => hitObjectContainer[index]; - private FollowPointRenderer.FollowPointLifetimeEntry getEntry(int index) => followPointRenderer.Entries[index]; + private FollowPointLifetimeEntry getEntry(int index) => followPointRenderer.Entries[index]; private FollowPointConnection getGroup(int index) => followPointRenderer.ChildrenOfType().Single(c => c.Entry == getEntry(index)); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index f9a594a3cb..700d96eff3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections public const int SPACING = 32; public const double PREEMPT = 800; - public FollowPointRenderer.FollowPointLifetimeEntry Entry; + public FollowPointLifetimeEntry Entry; public DrawablePool Pool; protected override void PrepareForUse() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs new file mode 100644 index 0000000000..a167cb2f0f --- /dev/null +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointLifetimeEntry.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Performance; +using osu.Game.Rulesets.Objects; +using osuTK; + +namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections +{ + public class FollowPointLifetimeEntry : LifetimeEntry + { + public event Action Invalidated; + public readonly OsuHitObject Start; + + public FollowPointLifetimeEntry(OsuHitObject start) + { + Start = start; + LifetimeStart = Start.StartTime; + + bindEvents(); + } + + private OsuHitObject end; + + public OsuHitObject End + { + get => end; + set + { + UnbindEvents(); + + end = value; + + bindEvents(); + + refreshLifetimes(); + } + } + + private void bindEvents() + { + UnbindEvents(); + + // Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects. + Start.DefaultsApplied += onDefaultsApplied; + Start.PositionBindable.ValueChanged += onPositionChanged; + + if (End != null) + { + End.DefaultsApplied += onDefaultsApplied; + End.PositionBindable.ValueChanged += onPositionChanged; + } + } + + public void UnbindEvents() + { + if (Start != null) + { + Start.DefaultsApplied -= onDefaultsApplied; + Start.PositionBindable.ValueChanged -= onPositionChanged; + } + + if (End != null) + { + End.DefaultsApplied -= onDefaultsApplied; + End.PositionBindable.ValueChanged -= onPositionChanged; + } + } + + private void onDefaultsApplied(HitObject obj) => refreshLifetimes(); + + private void onPositionChanged(ValueChangedEvent obj) => refreshLifetimes(); + + private void refreshLifetimes() + { + if (End == null || End.NewCombo || Start is Spinner || End is Spinner) + { + LifetimeEnd = LifetimeStart; + return; + } + + Vector2 startPosition = Start.StackedEndPosition; + Vector2 endPosition = End.StackedPosition; + Vector2 distanceVector = endPosition - startPosition; + + // The lifetime start will match the fade-in time of the first follow point. + float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length; + FollowPointConnection.GetFadeTimes(Start, End, fraction, out var fadeInTime, out _); + + LifetimeStart = fadeInTime; + LifetimeEnd = double.MaxValue; // This will be set by the connection. + + Invalidated?.Invoke(); + } + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs index 43914c4d57..3e85e528e8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointRenderer.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,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Performance; using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Objects; -using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { @@ -184,92 +182,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections { } } - - public class FollowPointLifetimeEntry : LifetimeEntry - { - public event Action Invalidated; - public readonly OsuHitObject Start; - - public FollowPointLifetimeEntry(OsuHitObject start) - { - Start = start; - LifetimeStart = Start.StartTime; - - bindEvents(); - } - - private OsuHitObject end; - - public OsuHitObject End - { - get => end; - set - { - UnbindEvents(); - - end = value; - - bindEvents(); - - refreshLifetimes(); - } - } - - private void bindEvents() - { - UnbindEvents(); - - // Note: Positions are bound for instantaneous feedback from positional changes from the editor, before ApplyDefaults() is called on hitobjects. - Start.DefaultsApplied += onDefaultsApplied; - Start.PositionBindable.ValueChanged += onPositionChanged; - - if (End != null) - { - End.DefaultsApplied += onDefaultsApplied; - End.PositionBindable.ValueChanged += onPositionChanged; - } - } - - public void UnbindEvents() - { - if (Start != null) - { - Start.DefaultsApplied -= onDefaultsApplied; - Start.PositionBindable.ValueChanged -= onPositionChanged; - } - - if (End != null) - { - End.DefaultsApplied -= onDefaultsApplied; - End.PositionBindable.ValueChanged -= onPositionChanged; - } - } - - private void onDefaultsApplied(HitObject obj) => refreshLifetimes(); - - private void onPositionChanged(ValueChangedEvent obj) => refreshLifetimes(); - - private void refreshLifetimes() - { - if (End == null || End.NewCombo || Start is Spinner || End is Spinner) - { - LifetimeEnd = LifetimeStart; - return; - } - - Vector2 startPosition = Start.StackedEndPosition; - Vector2 endPosition = End.StackedPosition; - Vector2 distanceVector = endPosition - startPosition; - - // The lifetime start will match the fade-in time of the first follow point. - float fraction = (int)(FollowPointConnection.SPACING * 1.5) / distanceVector.Length; - FollowPointConnection.GetFadeTimes(Start, End, fraction, out var fadeInTime, out _); - - LifetimeStart = fadeInTime; - LifetimeEnd = double.MaxValue; // This will be set by the connection. - - Invalidated?.Invoke(); - } - } } } From f562a7ea0df68baee20551108bb4bd9fd13d5747 Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Fri, 20 Nov 2020 11:52:17 +0300 Subject: [PATCH 4809/6909] Fix FileNotFoundException on startup --- osu.Desktop/osu.Desktop.csproj | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 2052c4bc25..53b9cdcf92 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,6 +24,8 @@ + + From 044622036cbc94adae124f68ec5fbff18befb2b4 Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Fri, 20 Nov 2020 11:53:17 +0300 Subject: [PATCH 4810/6909] Disable CA1416 --- osu.Desktop/OsuGameDesktop.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 0feab9a717..e2a06d7877 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -58,8 +58,10 @@ namespace osu.Desktop try { +#pragma warning disable CA1416 // Validate platform compatibility using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString()?.Split('"')[1].Replace("osu!.exe", ""); +#pragma warning restore CA1416 // Validate platform compatibility if (checkExists(stableInstallPath)) return stableInstallPath; From c013cd11c9ac330aa18217ce3480e7994d78c3fe Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 20 Nov 2020 17:24:09 +0900 Subject: [PATCH 4811/6909] Add DrawableHitObjectAdded event --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 17 +++++++++++++++++ osu.Game/Rulesets/UI/Playfield.cs | 6 ++++++ 2 files changed, 23 insertions(+) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 5fbda305c8..5dc653395b 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -42,6 +42,11 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; + /// + /// Invoked when a is added. + /// + public event Action DrawableHitObjectAdded; + /// /// Invoked when a becomes used by a . /// @@ -115,6 +120,8 @@ namespace osu.Game.Rulesets.UI bindStartTime(drawable); AddInternal(drawableMap[entry] = drawable, false); + DrawableHitObjectAdded?.Invoke(drawable); + HitObjectUsageBegan?.Invoke(entry.HitObject); } @@ -147,6 +154,16 @@ namespace osu.Game.Rulesets.UI hitObject.OnRevertResult += onRevertResult; AddInternal(hitObject); + + onDrawableHitObjectAddedRecursive(hitObject); + } + + private void onDrawableHitObjectAddedRecursive(DrawableHitObject hitObject) + { + DrawableHitObjectAdded?.Invoke(hitObject); + + foreach (var nested in hitObject.NestedHitObjects) + onDrawableHitObjectAddedRecursive(nested); } public virtual bool Remove(DrawableHitObject hitObject) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 82ec653f31..245fdd59e5 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -32,6 +32,11 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; + /// + /// Invoked when a is added. + /// + public event Action DrawableHitObjectAdded; + /// /// The contained in this Playfield. /// @@ -91,6 +96,7 @@ namespace osu.Game.Rulesets.UI { h.NewResult += (d, r) => NewResult?.Invoke(d, r); h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); + h.DrawableHitObjectAdded += d => DrawableHitObjectAdded?.Invoke(d); h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); })); From 468b2a97cbcf40f71a8df3e3c067617c7a197cff Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 20 Nov 2020 17:25:57 +0900 Subject: [PATCH 4812/6909] Use events instead of overriding Add (catch) --- .../Objects/Drawables/DrawableBananaShower.cs | 2 +- .../Objects/Drawables/DrawableJuiceStream.cs | 3 +-- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 15 ++++----------- 3 files changed, 6 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs index 4ce80aceb8..b46933d0c2 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables switch (hitObject) { case Banana banana: - return createDrawableRepresentation?.Invoke(banana)?.With(o => ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false); + return createDrawableRepresentation?.Invoke(banana); } return base.CreateNestedHitObject(hitObject); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs index 7bc016d94f..cc8accbb2b 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs @@ -47,8 +47,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables switch (hitObject) { case CatchHitObject catchObject: - return createDrawableRepresentation?.Invoke(catchObject)?.With(o => - ((DrawableCatchHitObject)o).CheckPosition = p => CheckPosition?.Invoke(p) ?? false); + return createDrawableRepresentation?.Invoke(catchObject); } throw new ArgumentException($"{nameof(hitObject)} must be of type {nameof(CatchHitObject)}."); diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 735d7fc300..0b379bbe95 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -55,21 +55,14 @@ namespace osu.Game.Rulesets.Catch.UI HitObjectContainer, CatcherArea, }; + + NewResult += onNewResult; + RevertResult += onRevertResult; + DrawableHitObjectAdded += d => ((DrawableCatchHitObject)d).CheckPosition = CheckIfWeCanCatch; } public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); - public override void Add(DrawableHitObject h) - { - h.OnNewResult += onNewResult; - h.OnRevertResult += onRevertResult; - - base.Add(h); - - var fruit = (DrawableCatchHitObject)h; - fruit.CheckPosition = CheckIfWeCanCatch; - } - private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); From cd16a3fa614443cc150c74d407081447942ea58b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 20 Nov 2020 17:28:06 +0900 Subject: [PATCH 4813/6909] Use event instead of using custom pools (osu) --- .../Edit/DrawableOsuEditPool.cs | 63 ------------------- .../Edit/DrawableOsuEditRuleset.cs | 48 +++++++++++++- .../Objects/Drawables/DrawableOsuPool.cs | 32 ---------- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 38 +++++------ 4 files changed, 64 insertions(+), 117 deletions(-) delete mode 100644 osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs delete mode 100644 osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs deleted file mode 100644 index 776aacd143..0000000000 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditPool.cs +++ /dev/null @@ -1,63 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using System.Linq; -using osu.Framework.Graphics; -using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables; - -namespace osu.Game.Rulesets.Osu.Edit -{ - public class DrawableOsuEditPool : DrawableOsuPool - where T : DrawableHitObject, new() - { - /// - /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. - /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. - /// - private const double editor_hit_object_fade_out_extension = 700; - - public DrawableOsuEditPool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) - : base(checkHittable, onLoaded, initialSize, maximumSize) - { - } - - protected override T CreateNewDrawable() => base.CreateNewDrawable().With(d => d.ApplyCustomUpdateState += updateState); - - private void updateState(DrawableHitObject hitObject, ArmedState state) - { - if (state == ArmedState.Idle) - return; - - // adjust the visuals of certain object types to make them stay on screen for longer than usual. - switch (hitObject) - { - default: - // there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.) - return; - - case DrawableSlider _: - // no specifics to sliders but let them fade slower below. - break; - - case DrawableHitCircle circle: // also handles slider heads - circle.ApproachCircle - .FadeOutFromOne(editor_hit_object_fade_out_extension) - .Expire(); - break; - } - - // Get the existing fade out transform - var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); - - if (existing == null) - return; - - hitObject.RemoveTransform(existing); - - using (hitObject.BeginAbsoluteSequence(existing.StartTime)) - hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); - } - } -} diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 547dff88b5..a03389bfe9 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -2,9 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using osu.Framework.Graphics.Pooling; +using System.Linq; +using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; using osuTK; @@ -26,8 +29,47 @@ namespace osu.Game.Rulesets.Osu.Edit { protected override GameplayCursorContainer CreateCursor() => null; - protected override DrawablePool CreatePool(int initialSize, int? maximumSize = null) - => new DrawableOsuEditPool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); + public OsuEditPlayfield() + { + DrawableHitObjectAdded += d => d.ApplyCustomUpdateState += updateState; + } + + private const double editor_hit_object_fade_out_extension = 700; + + private void updateState(DrawableHitObject hitObject, ArmedState state) + { + if (state == ArmedState.Idle) + return; + + // adjust the visuals of certain object types to make them stay on screen for longer than usual. + switch (hitObject) + { + default: + // there are quite a few drawable hit types we don't want to extend (spinners, ticks etc.) + return; + + case DrawableSlider _: + // no specifics to sliders but let them fade slower below. + break; + + case DrawableHitCircle circle: // also handles slider heads + circle.ApproachCircle + .FadeOutFromOne(editor_hit_object_fade_out_extension) + .Expire(); + break; + } + + // Get the existing fade out transform + var existing = hitObject.Transforms.LastOrDefault(t => t.TargetMember == nameof(Alpha)); + + if (existing == null) + return; + + hitObject.RemoveTransform(existing); + + using (hitObject.BeginAbsoluteSequence(existing.StartTime)) + hitObject.FadeOut(editor_hit_object_fade_out_extension).Expire(); + } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.cs deleted file mode 100644 index 1b5fd50022..0000000000 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuPool.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 System; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Pooling; -using osu.Game.Rulesets.Objects.Drawables; - -namespace osu.Game.Rulesets.Osu.Objects.Drawables -{ - public class DrawableOsuPool : DrawablePool - where T : DrawableHitObject, new() - { - private readonly Func checkHittable; - private readonly Action onLoaded; - - public DrawableOsuPool(Func checkHittable, Action onLoaded, int initialSize, int? maximumSize = null) - : base(initialSize, maximumSize) - { - this.checkHittable = checkHittable; - this.onLoaded = onLoaded; - } - - protected override T CreateNewDrawable() => base.CreateNewDrawable().With(o => - { - var osuObject = (DrawableOsuHitObject)(object)o; - - osuObject.CheckHittable = checkHittable; - osuObject.OnLoadComplete += onLoaded; - }); - } -} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index c816502d61..1b0d50b4f3 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -91,6 +91,15 @@ namespace osu.Game.Rulesets.Osu.UI AddRangeInternal(poolDictionary.Values); NewResult += onNewResult; + DrawableHitObjectAdded += onDrawableHitObjectAdded; + } + + private void onDrawableHitObjectAdded(DrawableHitObject drawable) + { + if (!drawable.IsLoaded) + drawable.OnLoadComplete += onDrawableHitObjectLoaded; + + ((DrawableOsuHitObject)drawable).CheckHittable = CheckHittable; } [BackgroundDependencyLoader(true)] @@ -98,28 +107,19 @@ namespace osu.Game.Rulesets.Osu.UI { config?.BindWith(OsuRulesetSetting.PlayfieldBorderStyle, playfieldBorder.PlayfieldBorderStyle); - registerPool(10, 100); + RegisterPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(10, 100); - registerPool(5, 50); + RegisterPool(10, 100); + RegisterPool(10, 100); + RegisterPool(10, 100); + RegisterPool(10, 100); + RegisterPool(5, 50); - registerPool(2, 20); - registerPool(10, 100); - registerPool(10, 100); + RegisterPool(2, 20); + RegisterPool(10, 100); + RegisterPool(10, 100); } - private void registerPool(int initialSize, int? maximumSize = null) - where TObject : HitObject - where TDrawable : DrawableHitObject, new() - => RegisterPool(CreatePool(initialSize, maximumSize)); - - protected virtual DrawablePool CreatePool(int initialSize, int? maximumSize = null) - where TDrawable : DrawableHitObject, new() - => new DrawableOsuPool(CheckHittable, OnHitObjectLoaded, initialSize, maximumSize); - protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new OsuHitObjectLifetimeEntry(hitObject); protected override void OnHitObjectAdded(HitObject hitObject) @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Osu.UI followPoints.RemoveFollowPoints((OsuHitObject)hitObject); } - public void OnHitObjectLoaded(Drawable drawable) + private void onDrawableHitObjectLoaded(Drawable drawable) { switch (drawable) { From 772f6df668e7d95c14bb5ba9e6e84a69ba693ba3 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 20 Nov 2020 18:00:00 +0900 Subject: [PATCH 4814/6909] Add a remark for DrawableHitObjectAdded --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 3 +++ osu.Game/Rulesets/UI/Playfield.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 5dc653395b..da7f5a0ee5 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -45,6 +45,9 @@ namespace osu.Game.Rulesets.UI /// /// Invoked when a is added. /// + /// + /// This event is also called for nested s. + /// public event Action DrawableHitObjectAdded; /// diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 245fdd59e5..be56be91d5 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -35,6 +35,9 @@ namespace osu.Game.Rulesets.UI /// /// Invoked when a is added. /// + /// + /// This event is also called for nested s. + /// public event Action DrawableHitObjectAdded; /// From 1a676ef0d82c4d4cfd0d7fbac2338da3a763272f Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Fri, 20 Nov 2020 12:06:08 +0300 Subject: [PATCH 4815/6909] Resolve CA1416 properly using new API --- osu.Desktop/OsuGameDesktop.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index e2a06d7877..f9c932b260 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -5,6 +5,7 @@ using System; using System.IO; using System.Linq; using System.Reflection; +using System.Runtime.Versioning; using System.Threading.Tasks; using Microsoft.Win32; using osu.Desktop.Overlays; @@ -56,19 +57,12 @@ namespace osu.Desktop string stableInstallPath; - try + if (OperatingSystem.IsWindows()) { -#pragma warning disable CA1416 // Validate platform compatibility - using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - stableInstallPath = key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString()?.Split('"')[1].Replace("osu!.exe", ""); -#pragma warning restore CA1416 // Validate platform compatibility - + stableInstallPath = getStableInstallPathFromRegistry(); if (checkExists(stableInstallPath)) return stableInstallPath; } - catch - { - } stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); if (checkExists(stableInstallPath)) @@ -81,6 +75,13 @@ namespace osu.Desktop return null; } + [SupportedOSPlatform("windows")] + private string getStableInstallPathFromRegistry() + { + using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) + return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString()?.Split('"')[1].Replace("osu!.exe", ""); + } + protected override UpdateManager CreateUpdateManager() { switch (RuntimeInfo.OS) From 27f5a99726c238effb4311e0324dc589f30a1d08 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 20 Nov 2020 18:42:48 +0900 Subject: [PATCH 4816/6909] Fix more than one proxy is created --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 37 +++++++++--------------- 1 file changed, 13 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 1b0d50b4f3..d453b9cd53 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -96,10 +96,20 @@ namespace osu.Game.Rulesets.Osu.UI private void onDrawableHitObjectAdded(DrawableHitObject drawable) { - if (!drawable.IsLoaded) - drawable.OnLoadComplete += onDrawableHitObjectLoaded; - ((DrawableOsuHitObject)drawable).CheckHittable = CheckHittable; + + switch (drawable) + { + case DrawableSpinner _: + if (!drawable.HasProxy) + spinnerProxies.Add(drawable.CreateProxy()); + break; + + case IDrawableHitObjectWithProxiedApproach approach: + if (!approach.ProxiedLayer.HasProxy) + approachCircles.Add(approach.ProxiedLayer.CreateProxy()); + break; + } } [BackgroundDependencyLoader(true)] @@ -134,27 +144,6 @@ namespace osu.Game.Rulesets.Osu.UI followPoints.RemoveFollowPoints((OsuHitObject)hitObject); } - private void onDrawableHitObjectLoaded(Drawable drawable) - { - switch (drawable) - { - case DrawableSliderHead _: - case DrawableSliderTail _: - case DrawableSliderTick _: - case DrawableSliderRepeat _: - case DrawableSpinnerTick _: - break; - - case DrawableSpinner _: - spinnerProxies.Add(drawable.CreateProxy()); - break; - - case IDrawableHitObjectWithProxiedApproach approach: - approachCircles.Add(approach.ProxiedLayer.CreateProxy()); - break; - } - } - private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) { // Hitobjects that block future hits should miss previous hitobjects if they're hit out-of-order. From a26b0915b4047d61032dc7c0e9fc893502ac70ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 20:08:05 +0900 Subject: [PATCH 4817/6909] Fix scheduled tasks not being cleaned up between test steps --- .../TestSceneShaking.cs | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs index d692be89b2..7e973d0971 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneShaking.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 System.Collections.Generic; using System.Diagnostics; +using osu.Framework.Threading; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Scoring; @@ -10,6 +13,19 @@ namespace osu.Game.Rulesets.Osu.Tests { public class TestSceneShaking : TestSceneHitCircle { + private readonly List scheduledTasks = new List(); + + protected override IBeatmap CreateBeatmapForSkinProvider() + { + // best way to run cleanup before a new step is run + foreach (var task in scheduledTasks) + task.Cancel(); + + scheduledTasks.Clear(); + + return base.CreateBeatmapForSkinProvider(); + } + protected override TestDrawableHitCircle CreateDrawableHitCircle(HitCircle circle, bool auto) { var drawableHitObject = base.CreateDrawableHitCircle(circle, auto); @@ -17,7 +33,7 @@ namespace osu.Game.Rulesets.Osu.Tests Debug.Assert(drawableHitObject.HitObject.HitWindows != null); double delay = drawableHitObject.HitObject.StartTime - (drawableHitObject.HitObject.HitWindows.WindowFor(HitResult.Miss) + RNG.Next(0, 300)) - Time.Current; - Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay); + scheduledTasks.Add(Scheduler.AddDelayed(() => drawableHitObject.TriggerJudgement(), delay)); return drawableHitObject; } From 743541649706d9b9165197421860dfd0b3cf264d Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Fri, 20 Nov 2020 14:13:16 +0300 Subject: [PATCH 4818/6909] Workaround FileNotFoundException in a test projects --- osu.Desktop/osu.Desktop.csproj | 2 -- osu.Game/osu.Game.csproj | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 53b9cdcf92..2052c4bc25 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,8 +24,6 @@ - - diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 1670bf5b11..1c6139b519 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,6 +18,8 @@ + + From 8080fe942cc19c6c2ea0db0cdcc92e7b0a329881 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 20 Nov 2020 20:38:26 +0900 Subject: [PATCH 4819/6909] Fix samples being played more than once on skin change --- osu.Game/Skinning/SkinnableSound.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 24dddaf758..a6347fe05a 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -152,7 +152,7 @@ namespace osu.Game.Skinning SamplesContainer.Add(sample); } - if (wasPlaying) + if (wasPlaying && !Looping) Play(); } From 96abee3fde991e91f1fe14663a9e130c4b4a7005 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 20 Nov 2020 22:43:10 +0900 Subject: [PATCH 4820/6909] Fix silent NRE in slider selection blueprint --- .../Blueprints/Sliders/SliderSelectionBlueprint.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 7ae4f387ca..d592e129d9 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -44,6 +44,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } + private readonly BindableList controlPoints = new BindableList(); + private readonly IBindable pathVersion = new Bindable(); + public SliderSelectionBlueprint(DrawableSlider slider) : base(slider) { @@ -61,13 +64,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders }; } - private IBindable pathVersion; - protected override void LoadComplete() { base.LoadComplete(); - pathVersion = HitObject.Path.Version.GetBoundCopy(); + controlPoints.BindTo(HitObject.Path.ControlPoints); + + pathVersion.BindTo(HitObject.Path.Version); pathVersion.BindValueChanged(_ => updatePath()); BodyPiece.UpdateFrom(HitObject); @@ -164,8 +167,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders } } - private BindableList controlPoints => HitObject.Path.ControlPoints; - private int addControlPoint(Vector2 position) { position -= HitObject.Position; From 82aefa3868030e0fd323710a1153b30e890899ab Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sat, 21 Nov 2020 00:27:19 +0900 Subject: [PATCH 4821/6909] Rework and rename to OnNewDrawableHitObject. The semantics is changed and hopefully more clear. --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 2 +- .../Edit/DrawableOsuEditRuleset.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 18 +++++++++++------ .../Objects/Drawables/DrawableHitObject.cs | 12 ++++++++++- osu.Game/Rulesets/UI/HitObjectContainer.cs | 20 ------------------- osu.Game/Rulesets/UI/Playfield.cs | 14 ++++++++++--- 6 files changed, 36 insertions(+), 32 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 0b379bbe95..cd246e78d5 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -58,7 +58,7 @@ namespace osu.Game.Rulesets.Catch.UI NewResult += onNewResult; RevertResult += onRevertResult; - DrawableHitObjectAdded += d => ((DrawableCatchHitObject)d).CheckPosition = CheckIfWeCanCatch; + OnNewDrawableHitObject += d => ((DrawableCatchHitObject)d).CheckPosition = CheckIfWeCanCatch; } public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index a03389bfe9..1a71a88c71 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.Edit public OsuEditPlayfield() { - DrawableHitObjectAdded += d => d.ApplyCustomUpdateState += updateState; + OnNewDrawableHitObject += d => d.ApplyCustomUpdateState += updateState; } private const double editor_hit_object_fade_out_extension = 700; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index d453b9cd53..d7336050eb 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -91,23 +92,28 @@ namespace osu.Game.Rulesets.Osu.UI AddRangeInternal(poolDictionary.Values); NewResult += onNewResult; - DrawableHitObjectAdded += onDrawableHitObjectAdded; + OnNewDrawableHitObject += onDrawableHitObjectAdded; } private void onDrawableHitObjectAdded(DrawableHitObject drawable) { ((DrawableOsuHitObject)drawable).CheckHittable = CheckHittable; + Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}"); + drawable.OnLoadComplete += onDrawableHitObjectLoaded; + } + + private void onDrawableHitObjectLoaded(Drawable drawable) + { + // note: `Slider`'s `ProxiedLayer` is added when its nested `DrawableHitCircle` is loaded. switch (drawable) { case DrawableSpinner _: - if (!drawable.HasProxy) - spinnerProxies.Add(drawable.CreateProxy()); + spinnerProxies.Add(drawable.CreateProxy()); break; - case IDrawableHitObjectWithProxiedApproach approach: - if (!approach.ProxiedLayer.HasProxy) - approachCircles.Add(approach.ProxiedLayer.CreateProxy()); + case DrawableHitCircle hitCircle: + approachCircles.Add(hitCircle.ProxiedLayer.CreateProxy()); break; } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ca49ed9e75..312fbaa2d1 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -74,6 +74,11 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public event Action OnRevertResult; + /// + /// Invoked when a new nested hit object is created by . + /// + internal event Action OnNestedDrawableCreated; + /// /// Whether a visual indicator should be displayed when a scoring result occurs. /// @@ -214,10 +219,15 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in HitObject.NestedHitObjects) { - var drawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h) + var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h); + var drawableNested = pooledDrawableNested ?? CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); + // Invoke the event only if this nested object is just created by `CreateNestedHitObject`. + if (pooledDrawableNested == null) + OnNestedDrawableCreated?.Invoke(drawableNested); + drawableNested.OnNewResult += onNewResult; drawableNested.OnRevertResult += onRevertResult; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index da7f5a0ee5..5fbda305c8 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -42,14 +42,6 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; - /// - /// Invoked when a is added. - /// - /// - /// This event is also called for nested s. - /// - public event Action DrawableHitObjectAdded; - /// /// Invoked when a becomes used by a . /// @@ -123,8 +115,6 @@ namespace osu.Game.Rulesets.UI bindStartTime(drawable); AddInternal(drawableMap[entry] = drawable, false); - DrawableHitObjectAdded?.Invoke(drawable); - HitObjectUsageBegan?.Invoke(entry.HitObject); } @@ -157,16 +147,6 @@ namespace osu.Game.Rulesets.UI hitObject.OnRevertResult += onRevertResult; AddInternal(hitObject); - - onDrawableHitObjectAddedRecursive(hitObject); - } - - private void onDrawableHitObjectAddedRecursive(DrawableHitObject hitObject) - { - DrawableHitObjectAdded?.Invoke(hitObject); - - foreach (var nested in hitObject.NestedHitObjects) - onDrawableHitObjectAddedRecursive(nested); } public virtual bool Remove(DrawableHitObject hitObject) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index be56be91d5..8723a8c531 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -33,12 +33,14 @@ namespace osu.Game.Rulesets.UI public event Action RevertResult; /// - /// Invoked when a is added. + /// Invoked before a new is added. + /// This event is invoked only once for each + /// even the drawable is pooled and used multiple times for different s. /// /// /// This event is also called for nested s. /// - public event Action DrawableHitObjectAdded; + public event Action OnNewDrawableHitObject; /// /// The contained in this Playfield. @@ -93,13 +95,15 @@ namespace osu.Game.Rulesets.UI /// protected Playfield() { + OnNewDrawableHitObject += d => + d.OnNestedDrawableCreated += nested => OnNewDrawableHitObject?.Invoke(nested); + RelativeSizeAxes = Axes.Both; hitObjectContainerLazy = new Lazy(() => CreateHitObjectContainer().With(h => { h.NewResult += (d, r) => NewResult?.Invoke(d, r); h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); - h.DrawableHitObjectAdded += d => DrawableHitObjectAdded?.Invoke(d); h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); })); @@ -133,6 +137,8 @@ namespace osu.Game.Rulesets.UI /// The DrawableHitObject to add. public virtual void Add(DrawableHitObject h) { + OnNewDrawableHitObject?.Invoke(h); + HitObjectContainer.Add(h); OnHitObjectAdded(h.HitObject); } @@ -334,6 +340,8 @@ namespace osu.Game.Rulesets.UI // This is done before Apply() so that the state is updated once when the hitobject is applied. if (!dho.IsLoaded) { + OnNewDrawableHitObject?.Invoke(dho); + foreach (var m in mods.OfType()) m.ApplyToDrawableHitObjects(dho.Yield()); } From 1feda1152da0b15835af3ce83bed31faf5fbed65 Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Sat, 21 Nov 2020 02:06:20 +0300 Subject: [PATCH 4822/6909] Fix InspectCode warnings --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs | 5 +++++ osu.Game.Tournament/IPC/FileBasedIPC.cs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index f9c932b260..dbbf6d048b 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -79,7 +79,7 @@ namespace osu.Desktop private string getStableInstallPathFromRegistry() { using (RegistryKey key = Registry.ClassesRoot.OpenSubKey("osu")) - return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty).ToString()?.Split('"')[1].Replace("osu!.exe", ""); + return key?.OpenSubKey(@"shell\open\command")?.GetValue(string.Empty)?.ToString()?.Split('"')[1].Replace("osu!.exe", ""); } protected override UpdateManager CreateUpdateManager() diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 35473ee76c..58992366ff 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -73,6 +74,8 @@ namespace osu.Game.Tests.Visual.Gameplay switch (args.Action) { case NotifyCollectionChangedAction.Add: + Debug.Assert(args.NewItems != null, "args.NewItems != null"); + foreach (int user in args.NewItems) { if (user == api.LocalUser.Value.Id) @@ -82,6 +85,8 @@ namespace osu.Game.Tests.Visual.Gameplay break; case NotifyCollectionChangedAction.Remove: + Debug.Assert(args.OldItems != null, "args.OldItems != null"); + foreach (int user in args.OldItems) { if (user == api.LocalUser.Value.Id) diff --git a/osu.Game.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 999ce61ac8..99147951b2 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -243,7 +243,7 @@ namespace osu.Game.Tournament.IPC string stableInstallPath; 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 (ipcFileExistsInDirectory(stableInstallPath)) return stableInstallPath; From c4cb1440ab0a02701f3501b04aa73ef3e8e3cdc3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 21 Nov 2020 02:59:01 +0300 Subject: [PATCH 4823/6909] Rename PaginatedContainerHeader to ProfileSubsectionHeader --- ...ntainerHeader.cs => TestSceneProfileSubsectionHeader.cs} | 6 +++--- osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs | 4 ++-- ...ginatedContainerHeader.cs => ProfileSubsectionHeader.cs} | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game.Tests/Visual/UserInterface/{TestScenePaginatedContainerHeader.cs => TestSceneProfileSubsectionHeader.cs} (95%) rename osu.Game/Overlays/Profile/Sections/{PaginatedContainerHeader.cs => ProfileSubsectionHeader.cs} (95%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs similarity index 95% rename from osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs rename to osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs index 2e9f919cfd..cd226662d7 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestScenePaginatedContainerHeader.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneProfileSubsectionHeader.cs @@ -11,12 +11,12 @@ using osu.Framework.Allocation; namespace osu.Game.Tests.Visual.UserInterface { - public class TestScenePaginatedContainerHeader : OsuTestScene + public class TestSceneProfileSubsectionHeader : OsuTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - private PaginatedContainerHeader header; + private ProfileSubsectionHeader header; [Test] public void TestHiddenCounter() @@ -69,7 +69,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void createHeader(string text, CounterVisibilityState state, int initialValue = 0) { Clear(); - Add(header = new PaginatedContainerHeader(text, state) + Add(header = new ProfileSubsectionHeader(text, state) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs index c1107ce907..5bcd7d34c2 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays.Profile.Sections private readonly string missingText; private ShowMoreButton moreButton; private OsuSpriteText missing; - private PaginatedContainerHeader header; + private ProfileSubsectionHeader header; private readonly string headerText; private readonly CounterVisibilityState counterVisibilityState; @@ -58,7 +58,7 @@ namespace osu.Game.Overlays.Profile.Sections Children = new Drawable[] { - header = new PaginatedContainerHeader(headerText, counterVisibilityState) + header = new ProfileSubsectionHeader(headerText, counterVisibilityState) { Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1 }, diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs similarity index 95% rename from osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs rename to osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs index 8c617e5fbd..5858cebe89 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainerHeader.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsectionHeader.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics; namespace osu.Game.Overlays.Profile.Sections { - public class PaginatedContainerHeader : CompositeDrawable, IHasCurrentValue + public class ProfileSubsectionHeader : CompositeDrawable, IHasCurrentValue { private readonly BindableWithCurrent current = new BindableWithCurrent(); @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Sections private CounterPill counterPill; - public PaginatedContainerHeader(string text, CounterVisibilityState counterState) + public ProfileSubsectionHeader(string text, CounterVisibilityState counterState) { this.text = text; this.counterState = counterState; From 718ba9253bce4351e2dc07aaa495c943b70b9803 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 21 Nov 2020 03:18:24 +0300 Subject: [PATCH 4824/6909] Implement ProfileSubsection component --- .../Beatmaps/PaginatedBeatmapContainer.cs | 2 +- .../PaginatedMostPlayedBeatmapContainer.cs | 2 +- .../Kudosu/PaginatedKudosuHistoryContainer.cs | 2 +- ...ainer.cs => PaginatedProfileSubsection.cs} | 66 ++++------------ .../Profile/Sections/ProfileSubsection.cs | 78 +++++++++++++++++++ .../Sections/Ranks/PaginatedScoreContainer.cs | 2 +- .../PaginatedRecentActivityContainer.cs | 2 +- 7 files changed, 100 insertions(+), 54 deletions(-) rename osu.Game/Overlays/Profile/Sections/{PaginatedContainer.cs => PaginatedProfileSubsection.cs} (61%) create mode 100644 osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs diff --git a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs index 4b7de8de90..780d7ea986 100644 --- a/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Beatmaps/PaginatedBeatmapContainer.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Overlays.Profile.Sections.Beatmaps { - public class PaginatedBeatmapContainer : PaginatedContainer + public class PaginatedBeatmapContainer : PaginatedProfileSubsection { private const float panel_padding = 10f; private readonly BeatmapSetType type; diff --git a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs index 556f3139dd..e5bb1f8008 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/PaginatedMostPlayedBeatmapContainer.cs @@ -13,7 +13,7 @@ using osu.Game.Users; namespace osu.Game.Overlays.Profile.Sections.Historical { - public class PaginatedMostPlayedBeatmapContainer : PaginatedContainer + public class PaginatedMostPlayedBeatmapContainer : PaginatedProfileSubsection { public PaginatedMostPlayedBeatmapContainer(Bindable user) : base(user, "Most Played Beatmaps", "No records. :(", CounterVisibilityState.AlwaysVisible) diff --git a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs index 1b8bd23eb4..008d89d881 100644 --- a/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Kudosu/PaginatedKudosuHistoryContainer.cs @@ -11,7 +11,7 @@ using System.Collections.Generic; namespace osu.Game.Overlays.Profile.Sections.Kudosu { - public class PaginatedKudosuHistoryContainer : PaginatedContainer + public class PaginatedKudosuHistoryContainer : PaginatedProfileSubsection { public PaginatedKudosuHistoryContainer(Bindable user) : base(user, missingText: "This user hasn't received any kudosu!") diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs similarity index 61% rename from osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs rename to osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index 5bcd7d34c2..1f897d704a 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -6,10 +6,7 @@ using osu.Framework.Allocation; 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.Online.API; -using osu.Game.Rulesets; using osu.Game.Users; using System.Collections.Generic; using System.Linq; @@ -18,7 +15,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Profile.Sections { - public abstract class PaginatedContainer : FillFlowContainer + public abstract class PaginatedProfileSubsection : ProfileSubsection { [Resolved] private IAPIProvider api { get; set; } @@ -26,42 +23,25 @@ namespace osu.Game.Overlays.Profile.Sections protected int VisiblePages; protected int ItemsPerPage; - protected readonly Bindable User = new Bindable(); - protected FillFlowContainer ItemsContainer; - protected RulesetStore Rulesets; + protected FillFlowContainer ItemsContainer { get; private set; } private APIRequest> retrievalRequest; private CancellationTokenSource loadCancellation; - private readonly string missingText; private ShowMoreButton moreButton; - private OsuSpriteText missing; - private ProfileSubsectionHeader header; - private readonly string headerText; - private readonly CounterVisibilityState counterVisibilityState; - - protected PaginatedContainer(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + protected PaginatedProfileSubsection(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + : base(user, headerText, missingText, counterVisibilityState) { - this.headerText = headerText; - this.missingText = missingText; - this.counterVisibilityState = counterVisibilityState; - User.BindTo(user); } - [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) + protected override Drawable CreateContent() => new FillFlowContainer { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Direction = FillDirection.Vertical; - + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, Children = new Drawable[] { - header = new ProfileSubsectionHeader(headerText, counterVisibilityState) - { - Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1 - }, ItemsContainer = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -75,22 +55,11 @@ namespace osu.Game.Overlays.Profile.Sections Alpha = 0, Margin = new MarginPadding { Top = 10 }, Action = showMore, - }, - missing = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 15), - Text = missingText, - Alpha = 0, - }, - }; + } + } + }; - Rulesets = rulesets; - - User.ValueChanged += onUserChanged; - User.TriggerChange(); - } - - private void onUserChanged(ValueChangedEvent e) + protected override void OnUserChanged(ValueChangedEvent e) { loadCancellation?.Cancel(); retrievalRequest?.Cancel(); @@ -124,15 +93,15 @@ namespace osu.Game.Overlays.Profile.Sections moreButton.Hide(); moreButton.IsLoading = false; - if (!string.IsNullOrEmpty(missing.Text)) - missing.Show(); + if (!string.IsNullOrEmpty(Missing.Text)) + Missing.Show(); return; } LoadComponentsAsync(items.Select(CreateDrawableItem).Where(d => d != null), drawables => { - missing.Hide(); + Missing.Hide(); moreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0); moreButton.IsLoading = false; @@ -142,8 +111,6 @@ namespace osu.Game.Overlays.Profile.Sections protected virtual int GetCount(User user) => 0; - protected void SetCount(int value) => header.Current.Value = value; - protected virtual void OnItemsReceived(List items) { } @@ -154,8 +121,9 @@ namespace osu.Game.Overlays.Profile.Sections protected override void Dispose(bool isDisposing) { - base.Dispose(isDisposing); retrievalRequest?.Cancel(); + loadCancellation?.Cancel(); + base.Dispose(isDisposing); } } } diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs new file mode 100644 index 0000000000..751b35e342 --- /dev/null +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.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 osu.Framework.Allocation; +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.Rulesets; +using osu.Game.Users; +using JetBrains.Annotations; + +namespace osu.Game.Overlays.Profile.Sections +{ + public abstract class ProfileSubsection : FillFlowContainer + { + protected readonly Bindable User = new Bindable(); + + protected RulesetStore Rulesets { get; private set; } + + protected OsuSpriteText Missing { get; private set; } + + private readonly string headerText; + private readonly string missingText; + private readonly CounterVisibilityState counterVisibilityState; + + private ProfileSubsectionHeader header; + + protected ProfileSubsection(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + { + this.headerText = headerText; + this.missingText = missingText; + this.counterVisibilityState = counterVisibilityState; + User.BindTo(user); + } + + [BackgroundDependencyLoader] + private void load(RulesetStore rulesets) + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + + Children = new[] + { + header = new ProfileSubsectionHeader(headerText, counterVisibilityState) + { + Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1 + }, + CreateContent(), + Missing = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 15), + Text = missingText, + Alpha = 0, + }, + }; + + Rulesets = rulesets; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(OnUserChanged, true); + } + + [NotNull] + protected abstract Drawable CreateContent(); + + protected virtual void OnUserChanged(ValueChangedEvent e) + { + } + + protected void SetCount(int value) => header.Current.Value = value; + } +} diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs index 1ce3079d52..53f6d375ca 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/PaginatedScoreContainer.cs @@ -14,7 +14,7 @@ using osu.Framework.Allocation; namespace osu.Game.Overlays.Profile.Sections.Ranks { - public class PaginatedScoreContainer : PaginatedContainer + public class PaginatedScoreContainer : PaginatedProfileSubsection { private readonly ScoreType type; diff --git a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs index 08f39c6272..d7101a8147 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/PaginatedRecentActivityContainer.cs @@ -13,7 +13,7 @@ using osu.Framework.Allocation; namespace osu.Game.Overlays.Profile.Sections.Recent { - public class PaginatedRecentActivityContainer : PaginatedContainer + public class PaginatedRecentActivityContainer : PaginatedProfileSubsection { public PaginatedRecentActivityContainer(Bindable user) : base(user, missingText: "This user hasn't done anything notable recently!") From 281ed49332c3a4f795eac9619bab786a5c1ee977 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sat, 21 Nov 2020 11:19:52 +0900 Subject: [PATCH 4825/6909] Add `HasInitialized` to DHO As it turned out, `IsLoaded` is not a reliable way. --- .../Objects/Drawables/DrawableHitObject.cs | 5 ++++ osu.Game/Rulesets/UI/Playfield.cs | 27 +++++++++++++------ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 312fbaa2d1..84b2dd7957 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -146,6 +146,11 @@ namespace osu.Game.Rulesets.Objects.Drawables private Container samplesContainer; + /// + /// Whether the initialization logic in has applied. + /// + internal bool HasInitialized; + /// /// Creates a new . /// diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 8723a8c531..f0b63fd347 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -16,6 +16,7 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osuTK; +using System.Diagnostics; namespace osu.Game.Rulesets.UI { @@ -95,9 +96,6 @@ namespace osu.Game.Rulesets.UI /// protected Playfield() { - OnNewDrawableHitObject += d => - d.OnNestedDrawableCreated += nested => OnNewDrawableHitObject?.Invoke(nested); - RelativeSizeAxes = Axes.Both; hitObjectContainerLazy = new Lazy(() => CreateHitObjectContainer().With(h => @@ -126,6 +124,16 @@ namespace osu.Game.Rulesets.UI } } + private void onNewDrawableHitObject(DrawableHitObject d) + { + d.OnNestedDrawableCreated += onNewDrawableHitObject; + + OnNewDrawableHitObject?.Invoke(d); + + Debug.Assert(!d.HasInitialized); + d.HasInitialized = true; + } + /// /// Performs post-processing tasks (if any) after all DrawableHitObjects are loaded into this Playfield. /// @@ -137,7 +145,10 @@ namespace osu.Game.Rulesets.UI /// The DrawableHitObject to add. public virtual void Add(DrawableHitObject h) { - OnNewDrawableHitObject?.Invoke(h); + if (h.HasInitialized) + throw new InvalidOperationException($"{nameof(Playfield.Add)} doesn't support {nameof(DrawableHitObject)} reuse. Use pooling instead."); + + onNewDrawableHitObject(h); HitObjectContainer.Add(h); OnHitObjectAdded(h.HitObject); @@ -336,12 +347,12 @@ namespace osu.Game.Rulesets.UI { var dho = (DrawableHitObject)d; - // If this is the first time this DHO is being used (not loaded), then apply the DHO mods. - // This is done before Apply() so that the state is updated once when the hitobject is applied. - if (!dho.IsLoaded) + if (!dho.HasInitialized) { - OnNewDrawableHitObject?.Invoke(dho); + onNewDrawableHitObject(dho); + // If this is the first time this DHO is being used, then apply the DHO mods. + // This is done before Apply() so that the state is updated once when the hitobject is applied. foreach (var m in mods.OfType()) m.ApplyToDrawableHitObjects(dho.Yield()); } From 4345d8dcb641f5838c707e0556dfbaa49dcd821a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sat, 21 Nov 2020 15:20:33 +0900 Subject: [PATCH 4826/6909] Event -> virtual method --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 6 ++++- .../Edit/DrawableOsuEditRuleset.cs | 4 ++-- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 3 +-- osu.Game/Rulesets/UI/Playfield.cs | 23 ++++++++++--------- 4 files changed, 20 insertions(+), 16 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index cd246e78d5..7d8f18ee0b 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -58,7 +58,11 @@ namespace osu.Game.Rulesets.Catch.UI NewResult += onNewResult; RevertResult += onRevertResult; - OnNewDrawableHitObject += d => ((DrawableCatchHitObject)d).CheckPosition = CheckIfWeCanCatch; + } + + protected override void OnNewDrawableHitObject(DrawableHitObject d) + { + ((DrawableCatchHitObject)d).CheckPosition = CheckIfWeCanCatch; } public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index 1a71a88c71..dafde0b927 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -29,9 +29,9 @@ namespace osu.Game.Rulesets.Osu.Edit { protected override GameplayCursorContainer CreateCursor() => null; - public OsuEditPlayfield() + protected override void OnNewDrawableHitObject(DrawableHitObject d) { - OnNewDrawableHitObject += d => d.ApplyCustomUpdateState += updateState; + d.ApplyCustomUpdateState += updateState; } private const double editor_hit_object_fade_out_extension = 700; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index d7336050eb..f70229fc1b 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -92,10 +92,9 @@ namespace osu.Game.Rulesets.Osu.UI AddRangeInternal(poolDictionary.Values); NewResult += onNewResult; - OnNewDrawableHitObject += onDrawableHitObjectAdded; } - private void onDrawableHitObjectAdded(DrawableHitObject drawable) + protected override void OnNewDrawableHitObject(DrawableHitObject drawable) { ((DrawableOsuHitObject)drawable).CheckHittable = CheckHittable; diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index f0b63fd347..fcdd5ff53d 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -33,16 +33,6 @@ namespace osu.Game.Rulesets.UI /// public event Action RevertResult; - /// - /// Invoked before a new is added. - /// This event is invoked only once for each - /// even the drawable is pooled and used multiple times for different s. - /// - /// - /// This event is also called for nested s. - /// - public event Action OnNewDrawableHitObject; - /// /// The contained in this Playfield. /// @@ -128,7 +118,7 @@ namespace osu.Game.Rulesets.UI { d.OnNestedDrawableCreated += onNewDrawableHitObject; - OnNewDrawableHitObject?.Invoke(d); + OnNewDrawableHitObject(d); Debug.Assert(!d.HasInitialized); d.HasInitialized = true; @@ -183,6 +173,17 @@ namespace osu.Game.Rulesets.UI { } + /// + /// Invoked before a new is added to this . + /// It is invoked only once even the drawable is pooled and used multiple times for different s. + /// + /// + /// This is also invoked for nested s. + /// + protected virtual void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) + { + } + /// /// The cursor currently being used by this . May be null if no cursor is provided. /// From 6c5a6b42e59b53825fa73f29fb8de2a7eda42f08 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 21 Nov 2020 14:09:32 +0200 Subject: [PATCH 4827/6909] Only calculate recommended SR once --- osu.Game/Screens/Select/DifficultyRecommender.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 2baabebdad..0e02b45650 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -69,8 +69,11 @@ namespace osu.Game.Screens.Select return beatmap; } - private void calculateRecommendedDifficulties() + private void calculateRecommendedDifficulties(bool onlyIfNoPreviousValues = false) { + if (recommendedStarDifficulty.Any() && onlyIfNoPreviousValues) + return; + // only query API for built-in rulesets rulesets.AvailableRulesets.Where(ruleset => ruleset.ID <= 3).ForEach(rulesetInfo => { @@ -122,7 +125,7 @@ namespace osu.Game.Screens.Select switch (state.NewValue) { case APIState.Online: - calculateRecommendedDifficulties(); + calculateRecommendedDifficulties(true); break; } }); From 72d9da5fac22583df2f275b1d5af65b1deb6e505 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 21 Nov 2020 14:26:09 +0200 Subject: [PATCH 4828/6909] Apply review suggestions --- osu.Game/OsuGame.cs | 25 +++++++++++-------- .../Screens/Select/DifficultyRecommender.cs | 14 +++-------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index e5a299d4b0..acc42bb660 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -335,11 +335,15 @@ namespace osu.Game /// The user should have already requested this interactively. /// /// The beatmap to select. - /// - /// Optional predicate used to filter which difficulties to select. - /// If omitted, this will try to present a recommended beatmap from the current ruleset. - /// In case of failure the first difficulty of the set will be presented, ignoring the predicate. - /// + /// Optional predicate used to narrow the set of difficulties to select from when presenting. + /// + /// Among items satisfying the predicate, the order of preference is: + /// + /// beatmap with recommended difficulty, as provided by , + /// first beatmap from the current ruleset, + /// first beatmap from any ruleset. + /// + /// public void PresentBeatmap(BeatmapSetInfo beatmap, Predicate difficultyCriteria = null) { var databasedSet = beatmap.OnlineBeatmapSetID != null @@ -373,11 +377,12 @@ namespace osu.Game // Try to select recommended beatmap // This should give us a beatmap from current ruleset if there are any in our matched beatmaps - var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps) ?? ( - // Fallback if a difficulty can't be recommended, maybe we are offline - // First try to find a beatmap in current ruleset, otherwise use first beatmap - beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) ?? beatmaps.First() - ); + var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps); + // Fallback if a difficulty can't be recommended, maybe we are offline + // First try to find a beatmap in current ruleset + selection ??= beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)); + // Otherwise use first beatmap + selection ??= beatmaps.First(); Ruleset.Value = selection.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 0e02b45650..ab64513ecb 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -81,7 +81,6 @@ namespace osu.Game.Screens.Select req.Success += result => { - bestRulesetOrder = null; // 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; }; @@ -90,19 +89,12 @@ namespace osu.Game.Screens.Select }); } - private IEnumerable bestRulesetOrder; - private IEnumerable getBestRulesetOrder() { - bestRulesetOrder ??= recommendedStarDifficulty.OrderByDescending(pair => pair.Value) - .Select(pair => pair.Key) - .ToList(); + IEnumerable bestRulesetOrder = recommendedStarDifficulty.OrderByDescending(pair => pair.Value) + .Select(pair => pair.Key) + .ToList(); - return moveCurrentRulesetToFirst(); - } - - private IEnumerable moveCurrentRulesetToFirst() - { List orderedRulesets; if (bestRulesetOrder.Contains(ruleset.Value)) From 07db977af5a61d0f45f80352f183f8db50f616c4 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 21 Nov 2020 14:36:43 +0200 Subject: [PATCH 4829/6909] Remove no longer necessary force calculation Is no longer necessary because recommender uses bindable state value changes, compared to when the test was written, and runs the state change once on load --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index fc14af3ab5..8a4914a31b 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -46,9 +46,6 @@ namespace osu.Game.Tests.Visual.SongSelect base.SetUpSteps(); - // Force recommender to calculate its star ratings again - AddStep("calculate recommended SRs", () => recommender.APIStateChanged(API, APIState.Online)); - User getUser(int? rulesetID) { return new User From 99a95790c3ac901b83d34c67f3dc485920437b5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Nov 2020 14:36:59 +0100 Subject: [PATCH 4830/6909] Resolve test failure --- 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 44a2056732..192d65f3fe 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -326,7 +326,7 @@ namespace osu.Game.Tests.Visual.Ranking public HotkeyRetryOverlay RetryOverlay; public UnrankedSoloResultsScreen(ScoreInfo score) - : base(score, false) + : base(score, true) { Score.Beatmap.OnlineBeatmapID = 0; Score.Beatmap.Status = BeatmapSetOnlineStatus.Pending; From 875f986979b22832eeff88a96bf0448241325730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Nov 2020 14:38:38 +0100 Subject: [PATCH 4831/6909] Remove default from base ResultsScreen too --- osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs | 2 +- osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs | 2 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 9ef9649f77..5323f58a66 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -318,7 +318,7 @@ namespace osu.Game.Tests.Visual.Background private class FadeAccessibleResults : ResultsScreen { public FadeAccessibleResults(ScoreInfo score) - : base(score) + : base(score, true) { } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs index 192d65f3fe..b2be7cdf88 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneResultsScreen.cs @@ -256,7 +256,7 @@ namespace osu.Game.Tests.Visual.Ranking public HotkeyRetryOverlay RetryOverlay; public TestResultsScreen(ScoreInfo score) - : base(score) + : base(score, true) { } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index f8bdf0140c..626db9baa6 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Ranking private readonly bool allowRetry; - protected ResultsScreen(ScoreInfo score, bool allowRetry = true) + protected ResultsScreen(ScoreInfo score, bool allowRetry) { Score = score; this.allowRetry = allowRetry; From 9f997db9587ec142977b7333c1e0176f4b994a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Nov 2020 17:55:56 +0100 Subject: [PATCH 4832/6909] Rewind judgement transforms before clearing --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 3063656aaf..cd6c001172 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -149,6 +149,7 @@ namespace osu.Game.Rulesets.Judgements private void runAnimation() { + ApplyTransformsAt(double.MinValue, true); ClearTransforms(true); LifetimeStart = Result.TimeAbsolute; From 81d0b42930426bddb6793411ae9dce234fbd9c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Nov 2020 19:51:27 +0100 Subject: [PATCH 4833/6909] Add failing test case --- .../Skinning/LegacySkinAnimationTest.cs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs new file mode 100644 index 0000000000..509e2fe8ca --- /dev/null +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.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.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.Animations; +using osu.Framework.Graphics.OpenGL.Textures; +using osu.Framework.Graphics.Textures; +using osu.Framework.Testing; +using osu.Framework.Timing; +using osu.Game.Audio; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.NonVisual.Skinning +{ + [HeadlessTest] + public class LegacySkinAnimationTest : OsuTestScene + { + private const string animation_name = "animation"; + private const int frame_count = 6; + + [Cached(typeof(IAnimationTimeReference))] + private TestAnimationTimeReference animationTimeReference = new TestAnimationTimeReference(); + + private TextureAnimation animation; + + [Test] + public void TestAnimationTimeReferenceChange() + { + ISkin skin = new TestSkin(); + + AddStep("get animation", () => Add(animation = (TextureAnimation)skin.GetAnimation(animation_name, true, false))); + AddAssert("frame count correct", () => animation.FrameCount == frame_count); + assertPlaybackPosition(0); + + AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime = 1000); + assertPlaybackPosition(-1000); + + AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500); + assertPlaybackPosition(-500); + } + + private void assertPlaybackPosition(double expectedPosition) + => AddAssert($"playback position is {expectedPosition}", () => animation.PlaybackPosition == expectedPosition); + + private class TestSkin : ISkin + { + private static readonly string[] lookup_names = Enumerable.Range(0, frame_count).Select(frame => $"{animation_name}-{frame}").ToArray(); + + public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) + { + return lookup_names.Contains(componentName) ? Texture.WhitePixel : null; + } + + public Drawable GetDrawableComponent(ISkinComponent component) => throw new NotSupportedException(); + public SampleChannel GetSample(ISampleInfo sampleInfo) => throw new NotSupportedException(); + public IBindable GetConfig(TLookup lookup) => throw new NotSupportedException(); + } + + private class TestAnimationTimeReference : IAnimationTimeReference + { + public ManualClock ManualClock { get; } + public IFrameBasedClock Clock { get; } + public double AnimationStartTime { get; set; } + + public TestAnimationTimeReference() + { + ManualClock = new ManualClock(); + Clock = new FramedClock(ManualClock); + } + } + } +} From 240c1b0aef9b24162acb488bece8b5bc988c2fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 21 Nov 2020 20:06:30 +0100 Subject: [PATCH 4834/6909] Add support for changing animation start time after load --- .../Objects/Drawables/Connections/FollowPoint.cs | 4 +++- .../Connections/FollowPointConnection.cs | 2 +- .../NonVisual/Skinning/LegacySkinAnimationTest.cs | 5 +++-- osu.Game/Skinning/IAnimationTimeReference.cs | 3 ++- osu.Game/Skinning/LegacySkinExtensions.cs | 15 ++++++++++++++- 5 files changed, 23 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index 3e2ab65bb2..be274131c0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.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 osuTK; using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; @@ -47,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections }); } - public double AnimationStartTime { get; set; } + public Bindable AnimationStartTime { get; } = new BindableDouble(); + IBindable IAnimationTimeReference.AnimationStartTime => AnimationStartTime; } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 700d96eff3..6e7b1050cb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -80,7 +80,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections fp.Alpha = 0; fp.Scale = new Vector2(1.5f * end.Scale); - fp.AnimationStartTime = fadeInTime; + fp.AnimationStartTime.Value = fadeInTime; using (fp.BeginAbsoluteSequence(fadeInTime)) { diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs index 509e2fe8ca..f05938cb39 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.NonVisual.Skinning AddAssert("frame count correct", () => animation.FrameCount == frame_count); assertPlaybackPosition(0); - AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime = 1000); + AddStep("set start time to 1000", () => animationTimeReference.AnimationStartTime.Value = 1000); assertPlaybackPosition(-1000); AddStep("set current time to 500", () => animationTimeReference.ManualClock.CurrentTime = 500); @@ -67,7 +67,8 @@ namespace osu.Game.Tests.NonVisual.Skinning { public ManualClock ManualClock { get; } public IFrameBasedClock Clock { get; } - public double AnimationStartTime { get; set; } + public Bindable AnimationStartTime { get; } = new BindableDouble(); + IBindable IAnimationTimeReference.AnimationStartTime => AnimationStartTime; public TestAnimationTimeReference() { diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs index 7e52bb8176..c987372f4b 100644 --- a/osu.Game/Skinning/IAnimationTimeReference.cs +++ b/osu.Game/Skinning/IAnimationTimeReference.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.OpenGL.Textures; using osu.Framework.Timing; @@ -25,6 +26,6 @@ namespace osu.Game.Skinning /// /// The time which animations should be started from, relative to . /// - double AnimationStartTime { get; } + IBindable AnimationStartTime { get; } } } diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index 0ee02a2442..b57852847c 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.OpenGL.Textures; @@ -70,6 +71,8 @@ namespace osu.Game.Skinning [Resolved(canBeNull: true)] private IAnimationTimeReference timeReference { get; set; } + private readonly Bindable animationStartTime = new BindableDouble(); + public SkinnableTextureAnimation(bool startAtCurrentTime = true) : base(startAtCurrentTime) { @@ -82,8 +85,18 @@ namespace osu.Game.Skinning if (timeReference != null) { Clock = timeReference.Clock; - PlaybackPosition = timeReference.Clock.CurrentTime - timeReference.AnimationStartTime; + ((IBindable)animationStartTime).BindTo(timeReference.AnimationStartTime); } + + animationStartTime.BindValueChanged(_ => updatePlaybackPosition(), true); + } + + private void updatePlaybackPosition() + { + if (timeReference == null) + return; + + PlaybackPosition = timeReference.Clock.CurrentTime - timeReference.AnimationStartTime.Value; } } From 11c3ccfcaa4a4a2adb130e3bbbcda97a0ead8c78 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 21 Nov 2020 22:49:56 +0300 Subject: [PATCH 4835/6909] Move rulesets property to PaginatedProfileSubsection --- .../Profile/Sections/PaginatedProfileSubsection.cs | 4 ++++ osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs | 7 +------ 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index 1f897d704a..3e6b20bf14 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -12,6 +12,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Game.Graphics.UserInterface; +using osu.Game.Rulesets; namespace osu.Game.Overlays.Profile.Sections { @@ -20,6 +21,9 @@ namespace osu.Game.Overlays.Profile.Sections [Resolved] private IAPIProvider api { get; set; } + [Resolved] + protected RulesetStore Rulesets { get; private set; } + protected int VisiblePages; protected int ItemsPerPage; diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs index 751b35e342..6f68804827 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Rulesets; using osu.Game.Users; using JetBrains.Annotations; @@ -17,8 +16,6 @@ namespace osu.Game.Overlays.Profile.Sections { protected readonly Bindable User = new Bindable(); - protected RulesetStore Rulesets { get; private set; } - protected OsuSpriteText Missing { get; private set; } private readonly string headerText; @@ -36,7 +33,7 @@ namespace osu.Game.Overlays.Profile.Sections } [BackgroundDependencyLoader] - private void load(RulesetStore rulesets) + private void load() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -56,8 +53,6 @@ namespace osu.Game.Overlays.Profile.Sections Alpha = 0, }, }; - - Rulesets = rulesets; } protected override void LoadComplete() From dbfc839df379c65b09279704036347384ce6ef90 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 21 Nov 2020 23:03:54 +0300 Subject: [PATCH 4836/6909] Move missing text to PaginatedProfileSubsection --- .../Sections/PaginatedProfileSubsection.cs | 19 +++++++++++++++---- .../Profile/Sections/ProfileSubsection.cs | 16 ++-------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index 3e6b20bf14..b5ae949105 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -13,6 +13,8 @@ using System.Linq; using System.Threading; using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics; namespace osu.Game.Overlays.Profile.Sections { @@ -33,10 +35,13 @@ namespace osu.Game.Overlays.Profile.Sections private CancellationTokenSource loadCancellation; private ShowMoreButton moreButton; + private OsuSpriteText missing; + private readonly string missingText; protected PaginatedProfileSubsection(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) - : base(user, headerText, missingText, counterVisibilityState) + : base(user, headerText, counterVisibilityState) { + this.missingText = missingText; } protected override Drawable CreateContent() => new FillFlowContainer @@ -59,6 +64,12 @@ namespace osu.Game.Overlays.Profile.Sections Alpha = 0, Margin = new MarginPadding { Top = 10 }, Action = showMore, + }, + missing = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 15), + Text = missingText, + Alpha = 0, } } }; @@ -97,15 +108,15 @@ namespace osu.Game.Overlays.Profile.Sections moreButton.Hide(); moreButton.IsLoading = false; - if (!string.IsNullOrEmpty(Missing.Text)) - Missing.Show(); + if (!string.IsNullOrEmpty(missingText)) + missing.Show(); return; } LoadComponentsAsync(items.Select(CreateDrawableItem).Where(d => d != null), drawables => { - Missing.Hide(); + missing.Hide(); moreButton.FadeTo(items.Count == ItemsPerPage ? 1 : 0); moreButton.IsLoading = false; diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs index 6f68804827..0743823113 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs @@ -5,8 +5,6 @@ using osu.Framework.Allocation; 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.Users; using JetBrains.Annotations; @@ -16,18 +14,14 @@ namespace osu.Game.Overlays.Profile.Sections { protected readonly Bindable User = new Bindable(); - protected OsuSpriteText Missing { get; private set; } - private readonly string headerText; - private readonly string missingText; private readonly CounterVisibilityState counterVisibilityState; private ProfileSubsectionHeader header; - protected ProfileSubsection(Bindable user, string headerText = "", string missingText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) + protected ProfileSubsection(Bindable user, string headerText = "", CounterVisibilityState counterVisibilityState = CounterVisibilityState.AlwaysHidden) { this.headerText = headerText; - this.missingText = missingText; this.counterVisibilityState = counterVisibilityState; User.BindTo(user); } @@ -45,13 +39,7 @@ namespace osu.Game.Overlays.Profile.Sections { Alpha = string.IsNullOrEmpty(headerText) ? 0 : 1 }, - CreateContent(), - Missing = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 15), - Text = missingText, - Alpha = 0, - }, + CreateContent() }; } From fe4c6220418b1f8560f6e6bcdc9c8994e9912efd Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sat, 21 Nov 2020 23:13:46 +0300 Subject: [PATCH 4837/6909] Make OnUserChanged private --- .../Profile/Sections/PaginatedProfileSubsection.cs | 8 +++++++- .../Overlays/Profile/Sections/ProfileSubsection.cs | 10 ---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index b5ae949105..51e5622f68 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -74,7 +74,13 @@ namespace osu.Game.Overlays.Profile.Sections } }; - protected override void OnUserChanged(ValueChangedEvent e) + protected override void LoadComplete() + { + base.LoadComplete(); + User.BindValueChanged(onUserChanged, true); + } + + private void onUserChanged(ValueChangedEvent e) { loadCancellation?.Cancel(); retrievalRequest?.Cancel(); diff --git a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs index 0743823113..3e331f85e9 100644 --- a/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/ProfileSubsection.cs @@ -43,19 +43,9 @@ namespace osu.Game.Overlays.Profile.Sections }; } - protected override void LoadComplete() - { - base.LoadComplete(); - User.BindValueChanged(OnUserChanged, true); - } - [NotNull] protected abstract Drawable CreateContent(); - protected virtual void OnUserChanged(ValueChangedEvent e) - { - } - protected void SetCount(int value) => header.Current.Value = value; } } From d4b56aac8403565e2c5f5629c0abe550f76500ad Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 22 Nov 2020 02:17:54 +0300 Subject: [PATCH 4838/6909] Add missing whitespace --- osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs index 75c3ee8290..51e5622f68 100644 --- a/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/PaginatedProfileSubsection.cs @@ -28,6 +28,7 @@ namespace osu.Game.Overlays.Profile.Sections protected int VisiblePages; protected int ItemsPerPage; + protected FillFlowContainer ItemsContainer { get; private set; } private APIRequest> retrievalRequest; From 3cb1d0466734b0b2c07554f14a4c6f7e59f242c9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 22 Nov 2020 02:25:12 +0300 Subject: [PATCH 4839/6909] Move dates fill into it's own method --- .../Historical/ChartProfileSubsection.cs | 55 ++++++++++--------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs index 9413d241fa..783ecec190 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs @@ -44,32 +44,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical if (values?.Length > 1) { - // Fill dates with 0 count - - var newValues = new List { values[0] }; - var newLast = values[0]; - - for (int i = 1; i < values.Length; i++) - { - while (hasMissingDates(newLast, values[i])) - { - newValues.Add(newLast = new UserHistoryCount - { - Count = 0, - Date = newLast.Date.AddMonths(1) - }); - } - - newValues.Add(newLast = values[i]); - } - - static bool hasMissingDates(UserHistoryCount prev, UserHistoryCount current) - { - var possibleCurrent = prev.Date.AddMonths(1); - return possibleCurrent != current.Date; - } - - chart.Values = newValues.ToArray(); + chart.Values = fillZeroValues(values); Show(); return; } @@ -77,6 +52,34 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Hide(); } + private UserHistoryCount[] fillZeroValues(UserHistoryCount[] values) + { + var newValues = new List { values[0] }; + var newLast = values[0]; + + for (int i = 1; i < values.Length; i++) + { + while (hasMissingDates(newLast, values[i])) + { + newValues.Add(newLast = new UserHistoryCount + { + Count = 0, + Date = newLast.Date.AddMonths(1) + }); + } + + newValues.Add(newLast = values[i]); + } + + return newValues.ToArray(); + + static bool hasMissingDates(UserHistoryCount prev, UserHistoryCount current) + { + var possibleCurrent = prev.Date.AddMonths(1); + return possibleCurrent != current.Date; + } + } + protected abstract UserHistoryCount[] GetValues(User user); } } From 453f0ba675de81682ba13a72225e9ad0211b5e2e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 22 Nov 2020 02:34:29 +0300 Subject: [PATCH 4840/6909] Make tick lines thicker --- .../Profile/Sections/Historical/ProfileLineChart.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 5a9c42d7e0..c658ac1aa7 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -12,6 +12,7 @@ using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Framework.Graphics.Shapes; using static osu.Game.Users.User; +using osuTK; namespace osu.Game.Overlays.Profile.Sections.Historical { @@ -140,7 +141,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.X, RelativePositionAxes = Axes.Y, - Height = 1, + Height = 0.1f, + EdgeSmoothness = Vector2.One, Y = y }); @@ -180,7 +182,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, RelativePositionAxes = Axes.X, - Width = 1, + Width = 0.1f, + EdgeSmoothness = Vector2.One, X = x }); From 6e581902cdd49c05b513518df1b4f84bfc5b71b2 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 22 Nov 2020 03:11:38 +0300 Subject: [PATCH 4841/6909] Simplify column ticks creation --- .../Sections/Historical/ProfileLineChart.cs | 109 ++++++++++-------- 1 file changed, 58 insertions(+), 51 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index c658ac1aa7..31c14d3b19 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -123,28 +123,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical while (rollingRow <= max) { var y = -Interpolation.ValueAt(rollingRow, 0, 1f, min, max); - - rowTicksContainer.Add(new TickText - { - Anchor = Anchor.BottomRight, - Origin = Anchor.CentreRight, - RelativePositionAxes = Axes.Y, - Margin = new MarginPadding { Right = 3 }, - Text = rollingRow.ToString("N0"), - Font = OsuFont.GetFont(size: 12), - Y = y - }); - - rowLinesContainer.Add(new TickLine - { - Anchor = Anchor.BottomRight, - Origin = Anchor.CentreRight, - RelativeSizeAxes = Axes.X, - RelativePositionAxes = Axes.Y, - Height = 0.1f, - EdgeSmoothness = Vector2.One, - Y = y - }); + addRowTick(y, (long)rollingRow); rollingRow += niceTick; } @@ -155,42 +134,70 @@ namespace osu.Game.Overlays.Profile.Sections.Historical columnTicksContainer.Clear(); columnLinesContainer.Clear(); - var min = values.Select(v => v.Date).Min().ToOADate(); - var max = values.Select(v => v.Date).Max().ToOADate(); + var totalMonths = values.Length - 1; - var niceRange = niceNumber(max - min, false); - var niceTick = niceNumber(niceRange / (Math.Min(values.Length, 15) - 1), true); + int monthsPerTick = 1; - double rollingRow = min; + if (totalMonths >= 45) + monthsPerTick = 3; + else if (totalMonths >= 20) + monthsPerTick = 2; - while (rollingRow <= max) + for (int i = 0; i < totalMonths; i += monthsPerTick) { - var x = Interpolation.ValueAt(rollingRow, 0, 1f, min, max); - - columnTicksContainer.Add(new TickText - { - Origin = Anchor.CentreLeft, - RelativePositionAxes = Axes.X, - Text = DateTime.FromOADate(rollingRow).ToString("MMM yyyy"), - Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), - Rotation = 45, - X = x - }); - - columnLinesContainer.Add(new TickLine - { - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.Y, - RelativePositionAxes = Axes.X, - Width = 0.1f, - EdgeSmoothness = Vector2.One, - X = x - }); - - rollingRow += niceTick; + var x = (float)i / totalMonths; + addColumnTick(x, values[i].Date); } } + private void addRowTick(float y, long value) + { + rowTicksContainer.Add(new TickText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.CentreRight, + RelativePositionAxes = Axes.Y, + Margin = new MarginPadding { Right = 3 }, + Text = value.ToString("N0"), + Font = OsuFont.GetFont(size: 12), + Y = y + }); + + rowLinesContainer.Add(new TickLine + { + Anchor = Anchor.BottomRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.X, + RelativePositionAxes = Axes.Y, + Height = 0.1f, + EdgeSmoothness = Vector2.One, + Y = y + }); + } + + private void addColumnTick(float x, DateTime value) + { + columnTicksContainer.Add(new TickText + { + Origin = Anchor.CentreLeft, + RelativePositionAxes = Axes.X, + Text = value.ToString("MMM yyyy"), + Font = OsuFont.GetFont(size: 12, weight: FontWeight.SemiBold), + Rotation = 45, + X = x + }); + + columnLinesContainer.Add(new TickLine + { + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.Y, + RelativePositionAxes = Axes.X, + Width = 0.1f, + EdgeSmoothness = Vector2.One, + X = x + }); + } + private double niceNumber(double value, bool round) { var exponent = Math.Floor(Math.Log10(value)); From e6c116f0ab35b181e1d353a0640ec1cd1bcb23c5 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 22 Nov 2020 03:49:00 +0300 Subject: [PATCH 4842/6909] Rework horizontal ticks creation --- .../Sections/Historical/ProfileLineChart.cs | 73 ++++++++++--------- 1 file changed, 40 insertions(+), 33 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 31c14d3b19..770da21657 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -115,17 +115,19 @@ namespace osu.Game.Overlays.Profile.Sections.Historical var min = values.Select(v => v.Count).Min(); var max = values.Select(v => v.Count).Max(); - var niceRange = niceNumber(max - min, false); - var niceTick = niceNumber(niceRange / (6 - 1), true); + var tick = getTick(getRange(max - min), 6); - double rollingRow = min; + double rollingRow = 0; while (rollingRow <= max) { - var y = -Interpolation.ValueAt(rollingRow, 0, 1f, min, max); - addRowTick(y, (long)rollingRow); + if (rollingRow >= min) + { + var y = -Interpolation.ValueAt(rollingRow, 0, 1f, min, max); + addRowTick(y, (long)rollingRow); + } - rollingRow += niceTick; + rollingRow += tick; } } @@ -138,10 +140,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical int monthsPerTick = 1; - if (totalMonths >= 45) - monthsPerTick = 3; - else if (totalMonths >= 20) - monthsPerTick = 2; + if (totalMonths > 20) + monthsPerTick = totalMonths / 10; for (int i = 0; i < totalMonths; i += monthsPerTick) { @@ -198,37 +198,44 @@ namespace osu.Game.Overlays.Profile.Sections.Historical }); } - private double niceNumber(double value, bool round) + private long getRange(double initialRange) { + var exponent = Math.Floor(Math.Log10(initialRange)); + var fraction = initialRange / Math.Pow(10, exponent); + + double niceFraction; + + if (fraction <= 1.0) + niceFraction = 1.0; + else if (fraction <= 2.0) + niceFraction = 2.0; + else if (fraction <= 5.0) + niceFraction = 5.0; + else + niceFraction = 10.0; + + return (long)(niceFraction * Math.Pow(10, exponent)); + } + + private long getTick(long range, int maxTicksCount) + { + var value = range / (maxTicksCount - 1); + var exponent = Math.Floor(Math.Log10(value)); var fraction = value / Math.Pow(10, exponent); double niceFraction; - if (round) - { - if (fraction < 1.5) - niceFraction = 1.0; - else if (fraction < 3) - niceFraction = 2.0; - else if (fraction < 7) - niceFraction = 5.0; - else - niceFraction = 10.0; - } + if (fraction < 1.5) + niceFraction = 1.0; + else if (fraction < 3) + niceFraction = 2.0; + else if (fraction < 7) + niceFraction = 5.0; else - { - if (fraction <= 1.0) - niceFraction = 1.0; - else if (fraction <= 2.0) - niceFraction = 2.0; - else if (fraction <= 5.0) - niceFraction = 5.0; - else - niceFraction = 10.0; - } + niceFraction = 10.0; - return niceFraction * Math.Pow(10, exponent); + return (long)(niceFraction * Math.Pow(10, exponent)); } private class TickText : OsuSpriteText From f07f8089d69062f44077349945a7680a2ea20c21 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 22 Nov 2020 03:58:56 +0300 Subject: [PATCH 4843/6909] Adjust monthsPerTick value --- .../Profile/Sections/Historical/ProfileLineChart.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 770da21657..932005a52f 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -140,8 +140,12 @@ namespace osu.Game.Overlays.Profile.Sections.Historical int monthsPerTick = 1; - if (totalMonths > 20) - monthsPerTick = totalMonths / 10; + if (totalMonths > 80) + monthsPerTick = 12; + else if (totalMonths >= 45) + monthsPerTick = 3; + else if (totalMonths > 20) + monthsPerTick = 2; for (int i = 0; i < totalMonths; i += monthsPerTick) { From 48871329471daf2a5c531460f5490250492529cb Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 22 Nov 2020 04:28:17 +0300 Subject: [PATCH 4844/6909] Adjustments for edge cases support --- .../Sections/Historical/ProfileLineChart.cs | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 932005a52f..688930d76a 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -117,6 +117,8 @@ namespace osu.Game.Overlays.Profile.Sections.Historical var tick = getTick(getRange(max - min), 6); + bool tickIsDecimal = tick < 1.0; + double rollingRow = 0; while (rollingRow <= max) @@ -124,7 +126,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical if (rollingRow >= min) { var y = -Interpolation.ValueAt(rollingRow, 0, 1f, min, max); - addRowTick(y, (long)rollingRow); + addRowTick(y, rollingRow, tickIsDecimal); } rollingRow += tick; @@ -136,7 +138,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical columnTicksContainer.Clear(); columnLinesContainer.Clear(); - var totalMonths = values.Length - 1; + var totalMonths = values.Length; int monthsPerTick = 1; @@ -149,12 +151,12 @@ namespace osu.Game.Overlays.Profile.Sections.Historical for (int i = 0; i < totalMonths; i += monthsPerTick) { - var x = (float)i / totalMonths; + var x = (float)i / (totalMonths - 1); addColumnTick(x, values[i].Date); } } - private void addRowTick(float y, long value) + private void addRowTick(float y, double value, bool tickIsDecimal) { rowTicksContainer.Add(new TickText { @@ -162,7 +164,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Origin = Anchor.CentreRight, RelativePositionAxes = Axes.Y, Margin = new MarginPadding { Right = 3 }, - Text = value.ToString("N0"), + Text = tickIsDecimal ? value.ToString("F1") : value.ToString("N0"), Font = OsuFont.GetFont(size: 12), Y = y }); @@ -202,7 +204,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical }); } - private long getRange(double initialRange) + private double getRange(double initialRange) { var exponent = Math.Floor(Math.Log10(initialRange)); var fraction = initialRange / Math.Pow(10, exponent); @@ -218,10 +220,10 @@ namespace osu.Game.Overlays.Profile.Sections.Historical else niceFraction = 10.0; - return (long)(niceFraction * Math.Pow(10, exponent)); + return niceFraction * Math.Pow(10, exponent); } - private long getTick(long range, int maxTicksCount) + private double getTick(double range, int maxTicksCount) { var value = range / (maxTicksCount - 1); @@ -239,7 +241,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical else niceFraction = 10.0; - return (long)(niceFraction * Math.Pow(10, exponent)); + return niceFraction * Math.Pow(10, exponent); } private class TickText : OsuSpriteText From b745fb681a8bfba02e01317f684fa1a3a8b699f0 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Sun, 22 Nov 2020 04:40:55 +0300 Subject: [PATCH 4845/6909] Fix incorrect static using placement --- .../Overlays/Profile/Sections/Historical/ProfileLineChart.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 688930d76a..1ce14fa245 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -11,8 +11,8 @@ using osu.Framework.Utils; using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Framework.Graphics.Shapes; -using static osu.Game.Users.User; using osuTK; +using static osu.Game.Users.User; namespace osu.Game.Overlays.Profile.Sections.Historical { From a3b1d14f178620fd9556efad890a7a4874f4230c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 22 Nov 2020 14:44:39 +0900 Subject: [PATCH 4846/6909] Apply similar fix to PoolableSkinnableSound --- osu.Game/Skinning/PoolableSkinnableSample.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index adc58ee94e..65f97ff909 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -101,7 +101,7 @@ namespace osu.Game.Skinning sampleContainer.Add(Sample = new DrawableSample(ch) { Looping = Looping }); // Start playback internally for the new sample if the previous one was playing beforehand. - if (wasPlaying) + if (wasPlaying && !Looping) Play(); } From 5247ebaf53d32a830dc652915d2f6641afe60399 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 22 Nov 2020 18:30:51 +0900 Subject: [PATCH 4847/6909] Restore accidently removed comment --- osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs index dafde0b927..5fdb79cbbd 100644 --- a/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs +++ b/osu.Game.Rulesets.Osu/Edit/DrawableOsuEditRuleset.cs @@ -34,6 +34,10 @@ namespace osu.Game.Rulesets.Osu.Edit d.ApplyCustomUpdateState += updateState; } + /// + /// Hit objects are intentionally made to fade out at a constant slower rate than in gameplay. + /// This allows a mapper to gain better historical context and use recent hitobjects as reference / snap points. + /// private const double editor_hit_object_fade_out_extension = 700; private void updateState(DrawableHitObject hitObject, ArmedState state) From c506b438bf465947fe0cff2f9bb25044f6bd35f9 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 22 Nov 2020 18:36:10 +0900 Subject: [PATCH 4848/6909] Remove more code and make some methods private --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 4 ++-- .../Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableSlider.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 5 +---- .../IDrawableHitObjectWithProxiedApproach.cs | 12 ------------ 5 files changed, 5 insertions(+), 20 deletions(-) delete mode 100644 osu.Game/Rulesets/Objects/Drawables/IDrawableHitObjectWithProxiedApproach.cs diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 7d8f18ee0b..abbdeacd9a 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -62,10 +62,10 @@ namespace osu.Game.Rulesets.Catch.UI protected override void OnNewDrawableHitObject(DrawableHitObject d) { - ((DrawableCatchHitObject)d).CheckPosition = CheckIfWeCanCatch; + ((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch; } - public bool CheckIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); + private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index d1ceca6d8f..abb51ae420 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -19,7 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableHitCircle : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach + public class DrawableHitCircle : DrawableOsuHitObject { public OsuAction? HitAction => HitArea.HitAction; protected virtual OsuSkinComponents CirclePieceComponent => OsuSkinComponents.HitCircle; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 14c494d909..6340367593 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -19,7 +19,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Osu.Objects.Drawables { - public class DrawableSlider : DrawableOsuHitObject, IDrawableHitObjectWithProxiedApproach + public class DrawableSlider : DrawableOsuHitObject { public new Slider HitObject => (Slider)base.HitObject; diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 93302c6046..8ff752952c 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.Osu.UI { public class OsuPlayfield : Playfield { - public readonly Func CheckHittable; - private readonly PlayfieldBorder playfieldBorder; private readonly ProxyContainer approachCircles; private readonly ProxyContainer spinnerProxies; @@ -57,7 +55,6 @@ namespace osu.Game.Rulesets.Osu.UI }; hitPolicy = new OrderedHitPolicy(HitObjectContainer); - CheckHittable = hitPolicy.IsHittable; var hitWindows = new OsuHitWindows(); @@ -71,7 +68,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override void OnNewDrawableHitObject(DrawableHitObject drawable) { - ((DrawableOsuHitObject)drawable).CheckHittable = CheckHittable; + ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable; Debug.Assert(!drawable.IsLoaded, $"Already loaded {nameof(DrawableHitObject)} is added to {nameof(OsuPlayfield)}"); drawable.OnLoadComplete += onDrawableHitObjectLoaded; diff --git a/osu.Game/Rulesets/Objects/Drawables/IDrawableHitObjectWithProxiedApproach.cs b/osu.Game/Rulesets/Objects/Drawables/IDrawableHitObjectWithProxiedApproach.cs deleted file mode 100644 index 8f4c95c634..0000000000 --- a/osu.Game/Rulesets/Objects/Drawables/IDrawableHitObjectWithProxiedApproach.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.Framework.Graphics; - -namespace osu.Game.Rulesets.Objects.Drawables -{ - public interface IDrawableHitObjectWithProxiedApproach - { - Drawable ProxiedLayer { get; } - } -} From 666112cb5a7faa7060c8131f9a20e87a4f24dd07 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Sun, 22 Nov 2020 18:47:35 +0900 Subject: [PATCH 4849/6909] Address @bdach's minor suggestions --- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- osu.Game/Rulesets/UI/Playfield.cs | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 84b2dd7957..6ed6c6412e 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -149,7 +149,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Whether the initialization logic in has applied. /// - internal bool HasInitialized; + internal bool IsInitialized; /// /// Creates a new . diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index fcdd5ff53d..2f589f4ce9 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -120,8 +120,8 @@ namespace osu.Game.Rulesets.UI OnNewDrawableHitObject(d); - Debug.Assert(!d.HasInitialized); - d.HasInitialized = true; + Debug.Assert(!d.IsInitialized); + d.IsInitialized = true; } /// @@ -135,8 +135,8 @@ namespace osu.Game.Rulesets.UI /// The DrawableHitObject to add. public virtual void Add(DrawableHitObject h) { - if (h.HasInitialized) - throw new InvalidOperationException($"{nameof(Playfield.Add)} doesn't support {nameof(DrawableHitObject)} reuse. Use pooling instead."); + if (h.IsInitialized) + throw new InvalidOperationException($"{nameof(Add)} doesn't support {nameof(DrawableHitObject)} reuse. Use pooling instead."); onNewDrawableHitObject(h); @@ -175,7 +175,7 @@ namespace osu.Game.Rulesets.UI /// /// Invoked before a new is added to this . - /// It is invoked only once even the drawable is pooled and used multiple times for different s. + /// It is invoked only once even if the drawable is pooled and used multiple times for different s. /// /// /// This is also invoked for nested s. @@ -348,7 +348,7 @@ namespace osu.Game.Rulesets.UI { var dho = (DrawableHitObject)d; - if (!dho.HasInitialized) + if (!dho.IsInitialized) { onNewDrawableHitObject(dho); From 1c31a4a6b6513a97ad91ae47a094be35aea41f07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 22 Nov 2020 13:11:22 +0100 Subject: [PATCH 4850/6909] Expose animation start time as mutable in interface --- .../Objects/Drawables/Connections/FollowPoint.cs | 1 - osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs | 1 - osu.Game/Skinning/IAnimationTimeReference.cs | 2 +- osu.Game/Skinning/LegacySkinExtensions.cs | 2 +- 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs index be274131c0..b989500066 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPoint.cs @@ -49,6 +49,5 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections } public Bindable AnimationStartTime { get; } = new BindableDouble(); - IBindable IAnimationTimeReference.AnimationStartTime => AnimationStartTime; } } diff --git a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs index f05938cb39..a5c937119e 100644 --- a/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs +++ b/osu.Game.Tests/NonVisual/Skinning/LegacySkinAnimationTest.cs @@ -68,7 +68,6 @@ namespace osu.Game.Tests.NonVisual.Skinning public ManualClock ManualClock { get; } public IFrameBasedClock Clock { get; } public Bindable AnimationStartTime { get; } = new BindableDouble(); - IBindable IAnimationTimeReference.AnimationStartTime => AnimationStartTime; public TestAnimationTimeReference() { diff --git a/osu.Game/Skinning/IAnimationTimeReference.cs b/osu.Game/Skinning/IAnimationTimeReference.cs index c987372f4b..f627379a57 100644 --- a/osu.Game/Skinning/IAnimationTimeReference.cs +++ b/osu.Game/Skinning/IAnimationTimeReference.cs @@ -26,6 +26,6 @@ namespace osu.Game.Skinning /// /// The time which animations should be started from, relative to . /// - IBindable AnimationStartTime { get; } + Bindable AnimationStartTime { get; } } } diff --git a/osu.Game/Skinning/LegacySkinExtensions.cs b/osu.Game/Skinning/LegacySkinExtensions.cs index b57852847c..a7c084998d 100644 --- a/osu.Game/Skinning/LegacySkinExtensions.cs +++ b/osu.Game/Skinning/LegacySkinExtensions.cs @@ -85,7 +85,7 @@ namespace osu.Game.Skinning if (timeReference != null) { Clock = timeReference.Clock; - ((IBindable)animationStartTime).BindTo(timeReference.AnimationStartTime); + animationStartTime.BindTo(timeReference.AnimationStartTime); } animationStartTime.BindValueChanged(_ => updatePlaybackPosition(), true); From cc33b0f2c633634e4b0a21f17560ee1b9cc21138 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 22 Nov 2020 16:53:27 +0100 Subject: [PATCH 4851/6909] Apply MenuGlow to Visualisation Colour instead of AccentColour --- osu.Game/Screens/Menu/LogoVisualisation.cs | 3 ++- osu.Game/Screens/Menu/MenuLogoVisualisation.cs | 6 +++--- osu.Game/Screens/Menu/OsuLogo.cs | 1 - 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index ebbb19636c..1f7ea3df31 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -20,6 +20,7 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Utils; +using osu.Framework.Extensions.Color4Extensions; namespace osu.Game.Screens.Menu { @@ -67,7 +68,7 @@ namespace osu.Game.Screens.Menu private int indexOffset; - public Color4 AccentColour { get; set; } + public Color4 AccentColour { get; set; } = Color4.White.Opacity(.2f); /// /// The relative movement of bars based on input amplification. Defaults to 1. diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index 5eb3f1efa0..bc7c25a26f 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -28,12 +28,12 @@ namespace osu.Game.Screens.Menu private void updateColour() { - Color4 defaultColour = Color4.White.Opacity(0.2f); + Color4 defaultColour = Color4.White; if (user.Value?.IsSupporter ?? false) - AccentColour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? defaultColour; + Colour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? defaultColour; else - AccentColour = defaultColour; + Colour = defaultColour; } } } diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 4515ee8ed0..8fa9d6e933 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -144,7 +144,6 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, - Alpha = 0.5f, Size = new Vector2(0.96f) }, new Container From b468f061f176ce4103f4f11680a91957b0d791e2 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 22 Nov 2020 17:20:00 +0100 Subject: [PATCH 4852/6909] Remove unused using --- osu.Game/Screens/Menu/MenuLogoVisualisation.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index bc7c25a26f..31a3c2dffd 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -7,7 +7,6 @@ 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 { From 1551402a8d2e812b87e270892a2252c21a3bc8ca Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 22 Nov 2020 17:33:25 +0100 Subject: [PATCH 4853/6909] Change IntroWelcome visualiser to use Colour instead of AccentColour --- osu.Game/Screens/Menu/IntroWelcome.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index e81646456f..abb83f894a 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -113,8 +113,7 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Alpha = 0.5f, - AccentColour = Color4.DarkBlue, + Colour = Color4.DarkBlue, Size = new Vector2(0.96f) }, new Circle From 458016d17da024f0165ce977b0472eee7b7edd0b Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 22 Nov 2020 17:34:56 +0100 Subject: [PATCH 4854/6909] Remove AccentColour member variable from LogoVisualisation --- osu.Game/Screens/Menu/LogoVisualisation.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 1f7ea3df31..5eb14a8dc1 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -11,7 +11,6 @@ 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 System.Collections.Generic; using JetBrains.Annotations; @@ -27,7 +26,7 @@ namespace osu.Game.Screens.Menu /// /// A visualiser that reacts to music coming from beatmaps. /// - public class LogoVisualisation : Drawable, IHasAccentColour + public class LogoVisualisation : Drawable { private readonly IBindable beatmap = new Bindable(); @@ -68,8 +67,6 @@ namespace osu.Game.Screens.Menu private int indexOffset; - public Color4 AccentColour { get; set; } = Color4.White.Opacity(.2f); - /// /// The relative movement of bars based on input amplification. Defaults to 1. /// @@ -177,7 +174,7 @@ namespace osu.Game.Screens.Menu // Assuming the logo is a circle, we don't need a second dimension. private float size; - private Color4 colour; + private Color4 colour = Color4.White.Opacity(.2f); private float[] audioData; private readonly QuadBatch vertexBatch = new QuadBatch(100, 10); @@ -194,7 +191,6 @@ namespace osu.Game.Screens.Menu shader = Source.shader; texture = Source.texture; size = Source.DrawSize.X; - colour = Source.AccentColour; audioData = Source.frequencyAmplitudes; } From ba7ce4c93363cd19791527d21f9d7de01129e8f3 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 22 Nov 2020 18:37:49 +0100 Subject: [PATCH 4855/6909] Make colour readonly --- osu.Game/Screens/Menu/LogoVisualisation.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 5eb14a8dc1..1d62305059 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -174,7 +174,8 @@ namespace osu.Game.Screens.Menu // Assuming the logo is a circle, we don't need a second dimension. private float size; - private Color4 colour = Color4.White.Opacity(.2f); + private readonly Color4 colour = Color4.White.Opacity(.2f); + private float[] audioData; private readonly QuadBatch vertexBatch = new QuadBatch(100, 10); From 61078e9ae1649426efb3cd43c2b0a51ef1c10a09 Mon Sep 17 00:00:00 2001 From: Derrick Timmermans Date: Sun, 22 Nov 2020 18:59:16 +0100 Subject: [PATCH 4856/6909] Use 0.2f instead of .2f --- osu.Game/Screens/Menu/LogoVisualisation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index 1d62305059..f6e011509c 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Menu // Assuming the logo is a circle, we don't need a second dimension. private float size; - private readonly Color4 colour = Color4.White.Opacity(.2f); + private readonly Color4 colour = Color4.White.Opacity(0.2f); private float[] audioData; From 2ae5a95d77d822cad544dc654c12751472b37ca6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Nov 2020 13:35:02 +0900 Subject: [PATCH 4857/6909] Change opacity value to match master implementation --- osu.Game/Screens/Menu/LogoVisualisation.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index f6e011509c..c96b08902c 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Menu // Assuming the logo is a circle, we don't need a second dimension. private float size; - private readonly Color4 colour = Color4.White.Opacity(0.2f); + private static readonly Color4 transparent_white = Color4.White.Opacity(0.1f); private float[] audioData; @@ -204,7 +204,7 @@ namespace osu.Game.Screens.Menu Vector2 inflation = DrawInfo.MatrixInverse.ExtractScale().Xy; ColourInfo colourInfo = DrawColourInfo.Colour; - colourInfo.ApplyChild(colour); + colourInfo.ApplyChild(transparent_white); if (audioData != null) { From ae609b9d48c50ae6f7ad291f191ee76309fda123 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Nov 2020 13:35:13 +0900 Subject: [PATCH 4858/6909] Remove unnecessary local variable --- osu.Game/Screens/Menu/MenuLogoVisualisation.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index 31a3c2dffd..92add458f9 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -27,12 +27,10 @@ namespace osu.Game.Screens.Menu private void updateColour() { - Color4 defaultColour = Color4.White; - if (user.Value?.IsSupporter ?? false) - Colour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? defaultColour; + Colour = skin.Value.GetConfig(GlobalSkinColours.MenuGlow)?.Value ?? Color4.White; else - Colour = defaultColour; + Colour = Color4.White; } } } From 3ed78688012d711fd374f5350b742625cda6f192 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Nov 2020 13:49:14 +0900 Subject: [PATCH 4859/6909] Scroll editor setup screen to file selector on display Previously the file selector would potentially display off-screen, making for confusing UX. Closes #10942. --- osu.Game/Graphics/Containers/SectionsContainer.cs | 2 ++ .../Edit/Setup/FileChooserLabelledTextBox.cs | 13 ++++++++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index f32f8e0c67..81968de304 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,6 +15,7 @@ namespace osu.Game.Graphics.Containers /// /// A container that can scroll to each section inside it. /// + [Cached] public class SectionsContainer : Container where T : Drawable { diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index b802b3405a..5de6842b50 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -3,10 +3,12 @@ using System; using System.IO; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -21,6 +23,9 @@ namespace osu.Game.Screens.Edit.Setup private readonly IBindable currentFile = new Bindable(); + [Resolved] + private SectionsContainer sectionsContainer { get; set; } + public FileChooserLabelledTextBox() { currentFile.BindValueChanged(onFileSelected); @@ -47,14 +52,16 @@ namespace osu.Game.Screens.Edit.Setup public void DisplayFileChooser() { - Target.Child = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions) + FileSelector fileSelector; + + Target.Child = fileSelector = new FileSelector(validFileExtensions: ResourcesSection.AudioExtensions) { RelativeSizeAxes = Axes.X, Height = 400, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, CurrentFile = { BindTarget = currentFile } }; + + sectionsContainer?.ScrollTo(fileSelector); } internal class FileChooserOsuTextBox : OsuTextBox From 898e2dae27f3f9badc0407ff745375803cec7345 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Nov 2020 14:27:02 +0900 Subject: [PATCH 4860/6909] Restore kiai time flashing behaviour --- osu.Game/Screens/Menu/LogoVisualisation.cs | 2 +- osu.Game/Screens/Menu/OsuLogo.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/LogoVisualisation.cs b/osu.Game/Screens/Menu/LogoVisualisation.cs index c96b08902c..01b2a98c6e 100644 --- a/osu.Game/Screens/Menu/LogoVisualisation.cs +++ b/osu.Game/Screens/Menu/LogoVisualisation.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Menu // Assuming the logo is a circle, we don't need a second dimension. private float size; - private static readonly Color4 transparent_white = Color4.White.Opacity(0.1f); + private static readonly Color4 transparent_white = Color4.White.Opacity(0.2f); private float[] audioData; diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 8fa9d6e933..68d23e1a32 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -81,6 +81,8 @@ namespace osu.Game.Screens.Menu set => rippleContainer.FadeTo(value ? 1 : 0, transition_length, Easing.OutQuint); } + private const float visualizer_default_alpha = 0.5f; + private readonly Box flashLayer; private readonly Container impactContainer; @@ -144,6 +146,7 @@ namespace osu.Game.Screens.Menu RelativeSizeAxes = Axes.Both, Origin = Anchor.Centre, Anchor = Anchor.Centre, + Alpha = visualizer_default_alpha, Size = new Vector2(0.96f) }, new Container @@ -281,8 +284,7 @@ namespace osu.Game.Screens.Menu this.Delay(early_activation).Schedule(() => sampleBeat.Play()); logoBeatContainer - .ScaleTo(1 - 0.02f * amplitudeAdjust, early_activation, Easing.Out) - .Then() + .ScaleTo(1 - 0.02f * amplitudeAdjust, early_activation, Easing.Out).Then() .ScaleTo(1, beatLength * 2, Easing.OutQuint); ripple.ClearTransforms(); @@ -295,15 +297,13 @@ namespace osu.Game.Screens.Menu { flashLayer.ClearTransforms(); flashLayer - .FadeTo(0.2f * amplitudeAdjust, early_activation, Easing.Out) - .Then() + .FadeTo(0.2f * amplitudeAdjust, early_activation, Easing.Out).Then() .FadeOut(beatLength); visualizer.ClearTransforms(); visualizer - .FadeTo(0.9f * amplitudeAdjust, early_activation, Easing.Out) - .Then() - .FadeTo(0.5f, beatLength); + .FadeTo(visualizer_default_alpha * 1.8f * amplitudeAdjust, early_activation, Easing.Out).Then() + .FadeTo(visualizer_default_alpha, beatLength); } } From 1b33d3003924daed2d2995a4889725728fce4d46 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 23 Nov 2020 08:52:29 +0300 Subject: [PATCH 4861/6909] Simplify horizontal ticks creation --- .../Sections/Historical/ProfileLineChart.cs | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 1ce14fa245..7dbcb9ba16 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -115,9 +115,11 @@ namespace osu.Game.Overlays.Profile.Sections.Historical var min = values.Select(v => v.Count).Min(); var max = values.Select(v => v.Count).Max(); - var tick = getTick(getRange(max - min), 6); + var tick = getTick(max - min, 6); - bool tickIsDecimal = tick < 1.0; + // Prevent infinite loop in case if tick is zero + if (tick == 0) + tick = 1; double rollingRow = 0; @@ -126,7 +128,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical if (rollingRow >= min) { var y = -Interpolation.ValueAt(rollingRow, 0, 1f, min, max); - addRowTick(y, rollingRow, tickIsDecimal); + addRowTick(y, rollingRow); } rollingRow += tick; @@ -156,7 +158,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical } } - private void addRowTick(float y, double value, bool tickIsDecimal) + private void addRowTick(float y, double value) { rowTicksContainer.Add(new TickText { @@ -164,7 +166,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Origin = Anchor.CentreRight, RelativePositionAxes = Axes.Y, Margin = new MarginPadding { Right = 3 }, - Text = tickIsDecimal ? value.ToString("F1") : value.ToString("N0"), + Text = value.ToString("N0"), Font = OsuFont.GetFont(size: 12), Y = y }); @@ -204,28 +206,9 @@ namespace osu.Game.Overlays.Profile.Sections.Historical }); } - private double getRange(double initialRange) + private long getTick(long range, int maxTicksCount) { - var exponent = Math.Floor(Math.Log10(initialRange)); - var fraction = initialRange / Math.Pow(10, exponent); - - double niceFraction; - - if (fraction <= 1.0) - niceFraction = 1.0; - else if (fraction <= 2.0) - niceFraction = 2.0; - else if (fraction <= 5.0) - niceFraction = 5.0; - else - niceFraction = 10.0; - - return niceFraction * Math.Pow(10, exponent); - } - - private double getTick(double range, int maxTicksCount) - { - var value = range / (maxTicksCount - 1); + var value = (float)range / (maxTicksCount - 1); var exponent = Math.Floor(Math.Log10(value)); var fraction = value / Math.Pow(10, exponent); @@ -241,7 +224,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical else niceFraction = 10.0; - return niceFraction * Math.Pow(10, exponent); + return (long)(niceFraction * Math.Pow(10, exponent)); } private class TickText : OsuSpriteText From 19faa2b9bbbcc2835e251a69ffbc2c6b8df6249d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 23 Nov 2020 15:18:54 +0900 Subject: [PATCH 4862/6909] Add comment covering intentional call to ClearTransformsAfter --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 2 ++ osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index cd6c001172..889e748a4a 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -149,8 +149,10 @@ namespace osu.Game.Rulesets.Judgements private void runAnimation() { + // undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state. ApplyTransformsAt(double.MinValue, true); ClearTransforms(true); + LifetimeStart = Result.TimeAbsolute; using (BeginAbsoluteSequence(Result.TimeAbsolute, true)) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index ca49ed9e75..0fa76a733f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -439,6 +439,8 @@ namespace osu.Game.Rulesets.Objects.Drawables private void clearExistingStateTransforms() { base.ApplyTransformsAt(double.MinValue, true); + + // has to call this method directly (not ClearTransforms) to bypass the local ClearTransformsAfter override. base.ClearTransformsAfter(double.MinValue, true); } From 3c0ee7de9b12f17924e1b94e3bdc9d58f9e6850d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 23 Nov 2020 09:51:50 +0300 Subject: [PATCH 4863/6909] Add proper tests --- .../Online/TestSceneChartProfileSubsection.cs | 145 ++++++++++++++++++ .../Online/TestSceneProfileLineChart.cs | 55 ------- 2 files changed, 145 insertions(+), 55 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs delete mode 100644 osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs b/osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs new file mode 100644 index 0000000000..4983dfba12 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs @@ -0,0 +1,145 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Overlays.Profile.Sections.Historical; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Users; +using NUnit.Framework; +using osu.Game.Overlays; +using osu.Framework.Allocation; +using System; +using System.Linq; +using osu.Framework.Testing; +using osu.Framework.Graphics.Shapes; +using static osu.Game.Users.User; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneChartProfileSubsection : OsuTestScene + { + [Cached] + private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red); + + private readonly Bindable user = new Bindable(); + private readonly PlayHistorySubsection section; + + public TestSceneChartProfileSubsection() + { + AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider.Background4 + }, + section = new PlayHistorySubsection(user) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + } + }); + } + + [Test] + public void TestNullValues() + { + AddStep("Load user", () => user.Value = user_with_null_values); + AddAssert("Section is hidden", () => section.Alpha == 0); + } + + [Test] + public void TestEmptyValues() + { + AddStep("Load user", () => user.Value = user_with_empty_values); + AddAssert("Section is hidden", () => section.Alpha == 0); + } + + [Test] + public void TestOveValue() + { + AddStep("Load user", () => user.Value = user_with_one_value); + AddAssert("Section is hidden", () => section.Alpha == 0); + } + + [Test] + public void TestTwoValues() + { + AddStep("Load user", () => user.Value = user_with_two_values); + AddAssert("Section is visible", () => section.Alpha == 1); + } + + [Test] + public void TestFilledValues() + { + AddStep("Load user", () => user.Value = user_with_filled_values); + AddAssert("Section is visible", () => section.Alpha == 1); + AddAssert("Array length is the same", () => user_with_filled_values.MonthlyPlaycounts.Length == getChartValuesLength()); + } + + [Test] + public void TestMissingValues() + { + AddStep("Load user", () => user.Value = user_with_missing_values); + AddAssert("Section is visible", () => section.Alpha == 1); + AddAssert("Array length is 7", () => getChartValuesLength() == 7); + } + + private int getChartValuesLength() => this.ChildrenOfType().Single().Values.Length; + + private static readonly User user_with_null_values = new User + { + Id = 1 + }; + + private static readonly User user_with_empty_values = new User + { + Id = 2, + MonthlyPlaycounts = Array.Empty() + }; + + private static readonly User user_with_one_value = new User + { + Id = 3, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 100 } + } + }; + + private static readonly User user_with_two_values = new User + { + Id = 4, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 2 } + } + }; + + private static readonly User user_with_filled_values = new User + { + Id = 5, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1000 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 20 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 20000 }, + new UserHistoryCount { Date = new DateTime(2010, 8, 1), Count = 30 }, + new UserHistoryCount { Date = new DateTime(2010, 9, 1), Count = 50 }, + new UserHistoryCount { Date = new DateTime(2010, 10, 1), Count = 2000 }, + new UserHistoryCount { Date = new DateTime(2010, 11, 1), Count = 2100 } + } + }; + + private static readonly User user_with_missing_values = new User + { + Id = 6, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2020, 1, 1), Count = 100 }, + new UserHistoryCount { Date = new DateTime(2020, 7, 1), Count = 200 } + } + }; + } +} diff --git a/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs b/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs deleted file mode 100644 index 3d342b0d76..0000000000 --- a/osu.Game.Tests/Visual/Online/TestSceneProfileLineChart.cs +++ /dev/null @@ -1,55 +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.Overlays.Profile.Sections.Historical; -using osu.Framework.Graphics; -using System; -using osu.Game.Overlays; -using osu.Framework.Allocation; -using osu.Framework.Graphics.Shapes; -using osu.Framework.Graphics.Containers; -using static osu.Game.Users.User; - -namespace osu.Game.Tests.Visual.Online -{ - public class TestSceneProfileLineChart : OsuTestScene - { - [Cached] - private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Pink); - - public TestSceneProfileLineChart() - { - var values = new[] - { - new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1000 }, - new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 20 }, - new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 20000 }, - new UserHistoryCount { Date = new DateTime(2010, 8, 1), Count = 30 }, - new UserHistoryCount { Date = new DateTime(2010, 9, 1), Count = 50 }, - new UserHistoryCount { Date = new DateTime(2010, 10, 1), Count = 2000 }, - new UserHistoryCount { Date = new DateTime(2010, 11, 1), Count = 2100 } - }; - - AddRange(new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = colourProvider.Background4 - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Padding = new MarginPadding { Horizontal = 50 }, - Child = new ProfileLineChart - { - Values = values - } - } - }); - } - } -} From 087ea9c9a5366c3848a56ea479259065913929d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Nov 2020 20:51:38 +0100 Subject: [PATCH 4864/6909] Fix typo in test name --- osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs b/osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs index 4983dfba12..0c8ee5e0c5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] - public void TestOveValue() + public void TestOneValue() { AddStep("Load user", () => user.Value = user_with_one_value); AddAssert("Section is hidden", () => section.Alpha == 0); From 20f1775ddb11ff2e88c22475788b5784892f0621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Nov 2020 20:59:04 +0100 Subject: [PATCH 4865/6909] Rename test scene to match tested class --- ...ProfileSubsection.cs => TestScenePlayHistorySubsection.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Tests/Visual/Online/{TestSceneChartProfileSubsection.cs => TestScenePlayHistorySubsection.cs} (97%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs b/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs similarity index 97% rename from osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs rename to osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs index 0c8ee5e0c5..c9dbc9dc24 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChartProfileSubsection.cs +++ b/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs @@ -16,7 +16,7 @@ using static osu.Game.Users.User; namespace osu.Game.Tests.Visual.Online { - public class TestSceneChartProfileSubsection : OsuTestScene + public class TestScenePlayHistorySubsection : OsuTestScene { [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Red); @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Online private readonly Bindable user = new Bindable(); private readonly PlayHistorySubsection section; - public TestSceneChartProfileSubsection() + public TestScenePlayHistorySubsection() { AddRange(new Drawable[] { From e9ffeb8b5d1471e59e91cce116e316e54ffe3db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Nov 2020 21:09:42 +0100 Subject: [PATCH 4866/6909] Make missing date check more robust --- .../Profile/Sections/Historical/ChartProfileSubsection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs index 783ecec190..3fd334005f 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs @@ -76,7 +76,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical static bool hasMissingDates(UserHistoryCount prev, UserHistoryCount current) { var possibleCurrent = prev.Date.AddMonths(1); - return possibleCurrent != current.Date; + return possibleCurrent < current.Date; } } From bb5aa9a9c914d2fa0bd57431dfa7d737b00831ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Nov 2020 21:24:37 +0100 Subject: [PATCH 4867/6909] Guard against empty values early --- .../Profile/Sections/Historical/ProfileLineChart.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 7dbcb9ba16..af2ef5a75c 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -26,8 +26,10 @@ namespace osu.Game.Overlays.Profile.Sections.Historical get => values; set { - values = value; - graph.Values = values; + if (value.Length == 0) + throw new ArgumentException("At least one value expected!", nameof(value)); + + graph.Values = values = value; createRowTicks(); createColumnTicks(); From 7b0d3dfe0cee8696f7251465ba63a7755d161ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Nov 2020 21:38:04 +0100 Subject: [PATCH 4868/6909] Refactor tick calculation code for readability --- .../Sections/Historical/ProfileLineChart.cs | 35 +++++++++++-------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index af2ef5a75c..ecc85fb48d 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -117,7 +117,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical var min = values.Select(v => v.Count).Min(); var max = values.Select(v => v.Count).Max(); - var tick = getTick(max - min, 6); + var tick = getTickInterval(max - min, 6); // Prevent infinite loop in case if tick is zero if (tick == 0) @@ -208,25 +208,32 @@ namespace osu.Game.Overlays.Profile.Sections.Historical }); } - private long getTick(long range, int maxTicksCount) + private long getTickInterval(long range, int maxTicksCount) { - var value = (float)range / (maxTicksCount - 1); + // this interval is what would be achieved if the interval was divided perfectly evenly into maxTicksCount ticks. + // can contain ugly fractional parts. + var exactTickInterval = (float)range / (maxTicksCount - 1); - var exponent = Math.Floor(Math.Log10(value)); - var fraction = value / Math.Pow(10, exponent); + // the ideal ticks start with a 1, 2 or 5, and are multipliers of powers of 10. + // first off, use log10 to calculate the number of digits in the "exact" interval. + var numberOfDigits = Math.Floor(Math.Log10(exactTickInterval)); + var tickBase = Math.Pow(10, numberOfDigits); + // then see how the exact tick relates to the power of 10. + var exactTickMultiplier = exactTickInterval / tickBase; - double niceFraction; + double tickMultiplier; - if (fraction < 1.5) - niceFraction = 1.0; - else if (fraction < 3) - niceFraction = 2.0; - else if (fraction < 7) - niceFraction = 5.0; + // round up the fraction to start with a 1, 2 or 5. closest match wins. + if (exactTickMultiplier < 1.5) + tickMultiplier = 1.0; + else if (exactTickMultiplier < 3) + tickMultiplier = 2.0; + else if (exactTickMultiplier < 7) + tickMultiplier = 5.0; else - niceFraction = 10.0; + tickMultiplier = 10.0; - return (long)(niceFraction * Math.Pow(10, exponent)); + return Math.Max((long)(tickMultiplier * tickBase), 1); } private class TickText : OsuSpriteText From 8347ecf494f6824d1fe0ee36b6094e434cc31196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Nov 2020 21:52:47 +0100 Subject: [PATCH 4869/6909] Simplify row tick creation code --- .../Sections/Historical/ProfileLineChart.cs | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index ecc85fb48d..c1eb0811fb 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -117,23 +117,15 @@ namespace osu.Game.Overlays.Profile.Sections.Historical var min = values.Select(v => v.Count).Min(); var max = values.Select(v => v.Count).Max(); - var tick = getTickInterval(max - min, 6); + var tickInterval = getTickInterval(max - min, 6); - // Prevent infinite loop in case if tick is zero - if (tick == 0) - tick = 1; - - double rollingRow = 0; - - while (rollingRow <= max) + for (long currentTick = 0; currentTick <= max; currentTick += tickInterval) { - if (rollingRow >= min) - { - var y = -Interpolation.ValueAt(rollingRow, 0, 1f, min, max); - addRowTick(y, rollingRow); - } + if (currentTick < min) + continue; - rollingRow += tick; + float y = -Interpolation.ValueAt(currentTick, 0, 1f, min, max); + addRowTick(y, currentTick); } } From 5701b32bae6c6d7dd5e4f7d5cf450415d2d5cc40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 23 Nov 2020 22:12:32 +0100 Subject: [PATCH 4870/6909] Handle constant graphs better --- .../Online/TestScenePlayHistorySubsection.cs | 40 ++++++++++++++++++- osu.Game/Graphics/UserInterface/LineGraph.cs | 6 ++- .../Sections/Historical/ProfileLineChart.cs | 12 +++++- 3 files changed, 53 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs b/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs index c9dbc9dc24..cf5ecf5bf2 100644 --- a/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs +++ b/osu.Game.Tests/Visual/Online/TestScenePlayHistorySubsection.cs @@ -69,6 +69,20 @@ namespace osu.Game.Tests.Visual.Online AddAssert("Section is visible", () => section.Alpha == 1); } + [Test] + public void TestConstantValues() + { + AddStep("Load user", () => user.Value = user_with_constant_values); + AddAssert("Section is visible", () => section.Alpha == 1); + } + + [Test] + public void TestConstantZeroValues() + { + AddStep("Load user", () => user.Value = user_with_zero_values); + AddAssert("Section is visible", () => section.Alpha == 1); + } + [Test] public void TestFilledValues() { @@ -117,10 +131,32 @@ namespace osu.Game.Tests.Visual.Online } }; - private static readonly User user_with_filled_values = new User + private static readonly User user_with_constant_values = new User { Id = 5, MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 5 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 5 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 5 } + } + }; + + private static readonly User user_with_zero_values = new User + { + Id = 6, + MonthlyPlaycounts = new[] + { + new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 0 }, + new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 0 }, + new UserHistoryCount { Date = new DateTime(2010, 7, 1), Count = 0 } + } + }; + + private static readonly User user_with_filled_values = new User + { + Id = 7, + MonthlyPlaycounts = new[] { new UserHistoryCount { Date = new DateTime(2010, 5, 1), Count = 1000 }, new UserHistoryCount { Date = new DateTime(2010, 6, 1), Count = 20 }, @@ -134,7 +170,7 @@ namespace osu.Game.Tests.Visual.Online private static readonly User user_with_missing_values = new User { - Id = 6, + Id = 8, MonthlyPlaycounts = new[] { new UserHistoryCount { Date = new DateTime(2020, 1, 1), Count = 100 }, diff --git a/osu.Game/Graphics/UserInterface/LineGraph.cs b/osu.Game/Graphics/UserInterface/LineGraph.cs index 42b523fc5c..70db26c817 100644 --- a/osu.Game/Graphics/UserInterface/LineGraph.cs +++ b/osu.Game/Graphics/UserInterface/LineGraph.cs @@ -119,7 +119,11 @@ namespace osu.Game.Graphics.UserInterface protected float GetYPosition(float value) { - if (ActualMaxValue == ActualMinValue) return 0; + if (ActualMaxValue == ActualMinValue) + // show line at top if the only value on the graph is positive, + // and at bottom if the only value on the graph is zero or negative. + // just kind of makes most sense intuitively. + return value > 1 ? 0 : 1; return (ActualMaxValue - value) / (ActualMaxValue - ActualMinValue); } diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index c1eb0811fb..989871745d 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -124,8 +124,16 @@ namespace osu.Game.Overlays.Profile.Sections.Historical if (currentTick < min) continue; - float y = -Interpolation.ValueAt(currentTick, 0, 1f, min, max); - addRowTick(y, currentTick); + float y; + // special-case the min == max case to match LineGraph. + // lerp isn't really well-defined over a zero interval anyway. + if (min == max) + y = currentTick > 1 ? 1 : 0; + else + y = Interpolation.ValueAt(currentTick, 0, 1f, min, max); + + // y axis is inverted in graph-like coordinates. + addRowTick(-y, currentTick); } } From 52f5473cc043eb4ed12f3e9e395c51eff9a1a57a Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 23 Nov 2020 15:13:58 -0800 Subject: [PATCH 4871/6909] Set global action as a parameter in toast --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- osu.Game/Input/KeyBindingStore.cs | 2 +- .../Overlays/Music/MusicKeyBindingHandler.cs | 16 ++++----- osu.Game/Overlays/OSD/Toast.cs | 33 +++++++++++++++++-- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 89a6ee8b07..44ac48d83a 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -196,7 +196,7 @@ namespace osu.Game.Configuration public Func LookupSkinName { private get; set; } - public Func LookupKeyBindings { get; set; } + public Func LookupKeyBindings { get; set; } } public enum OsuSetting diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index bc73d74d74..f4b7c873d5 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -37,7 +37,7 @@ namespace osu.Game.Input /// /// The action to lookup. /// A set of display strings for all the user's key configuration for the action. - public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) + public IEnumerable GetReadableKeyCombinationsFor(GlobalAction? globalAction) { foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction)) { diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index 0d6158d46f..fa8180b7c0 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; -using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays.OSD; @@ -26,9 +25,6 @@ namespace osu.Game.Overlays.Music [Resolved(canBeNull: true)] private OnScreenDisplay onScreenDisplay { get; set; } - [Resolved] - private OsuConfigManager config { get; set; } - public bool OnPressed(GlobalAction action) { if (beatmap.Disabled) @@ -41,11 +37,11 @@ namespace osu.Game.Overlays.Music bool wasPlaying = musicController.IsPlaying; if (musicController.TogglePause()) - onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track", config.LookupKeyBindings(action))); + onScreenDisplay?.Display(new MusicActionToast(wasPlaying ? "Pause track" : "Play track", action)); return true; case GlobalAction.MusicNext: - musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track", config.LookupKeyBindings(action)))); + musicController.NextTrack(() => onScreenDisplay?.Display(new MusicActionToast("Next track", action))); return true; @@ -55,11 +51,11 @@ namespace osu.Game.Overlays.Music switch (res) { case PreviousTrackResult.Restart: - onScreenDisplay?.Display(new MusicActionToast("Restart track", config.LookupKeyBindings(action))); + onScreenDisplay?.Display(new MusicActionToast("Restart track", action)); break; case PreviousTrackResult.Previous: - onScreenDisplay?.Display(new MusicActionToast("Previous track", config.LookupKeyBindings(action))); + onScreenDisplay?.Display(new MusicActionToast("Previous track", action)); break; } }); @@ -76,8 +72,8 @@ namespace osu.Game.Overlays.Music private class MusicActionToast : Toast { - public MusicActionToast(string action, string shortcut) - : base("Music Playback", action, shortcut) + public MusicActionToast(string value, GlobalAction action) + : base("Music Playback", value, action: action) { } } diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index 1497ca8fa8..d32d63055f 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; @@ -16,12 +19,22 @@ namespace osu.Game.Overlays.OSD private const int toast_minimum_width = 240; private readonly Container content; + + private readonly OsuSpriteText spriteText; + + private readonly string shortcut; + + private readonly GlobalAction? action; + protected override Container Content => content; protected readonly OsuSpriteText ValueText; - protected Toast(string description, string value, string shortcut) + protected Toast(string description, string value, string shortcut = null, GlobalAction? action = null) { + this.shortcut = shortcut; + this.action = action; + Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -68,7 +81,7 @@ namespace osu.Game.Overlays.OSD Origin = Anchor.Centre, Text = value }, - new OsuSpriteText + spriteText = new OsuSpriteText { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, @@ -76,9 +89,23 @@ namespace osu.Game.Overlays.OSD Alpha = 0.3f, Margin = new MarginPadding { Bottom = 15 }, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), - Text = string.IsNullOrEmpty(shortcut) ? "NO KEY BOUND" : shortcut.ToUpperInvariant() }, }; } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + string text; + + if (action != null) + text = config.LookupKeyBindings(action); + else if (!string.IsNullOrEmpty(shortcut)) + text = shortcut; + else + text = "no key bound"; + + spriteText.Text = text.ToUpperInvariant(); + } } } From 44ca67c534ab40635f632e8b4fb6b2b1e124f4bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 13:10:11 +0900 Subject: [PATCH 4872/6909] Simplify fill logic and add xmldoc --- .../Historical/ChartProfileSubsection.cs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs index 3fd334005f..885b12ca6d 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -52,32 +53,30 @@ namespace osu.Game.Overlays.Profile.Sections.Historical Hide(); } - private UserHistoryCount[] fillZeroValues(UserHistoryCount[] values) + /// + /// Add entries for any missing months (filled with zero values). + /// + private UserHistoryCount[] fillZeroValues(UserHistoryCount[] historyEntries) { - var newValues = new List { values[0] }; - var newLast = values[0]; + var filledHistoryEntries = new List(); - for (int i = 1; i < values.Length; i++) + foreach (var entry in historyEntries) { - while (hasMissingDates(newLast, values[i])) + var lastFilled = filledHistoryEntries.LastOrDefault(); + + while (lastFilled?.Date.AddMonths(1) < entry.Date) { - newValues.Add(newLast = new UserHistoryCount + filledHistoryEntries.Add(lastFilled = new UserHistoryCount { Count = 0, - Date = newLast.Date.AddMonths(1) + Date = lastFilled.Date.AddMonths(1) }); } - newValues.Add(newLast = values[i]); + filledHistoryEntries.Add(entry); } - return newValues.ToArray(); - - static bool hasMissingDates(UserHistoryCount prev, UserHistoryCount current) - { - var possibleCurrent = prev.Date.AddMonths(1); - return possibleCurrent < current.Date; - } + return filledHistoryEntries.ToArray(); } protected abstract UserHistoryCount[] GetValues(User user); From 82640418ba1e140d7d4d4d109a15a29e155fe24f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 13:12:04 +0900 Subject: [PATCH 4873/6909] Invert hide logic for readability --- .../Profile/Sections/Historical/ChartProfileSubsection.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs index 885b12ca6d..b82773155d 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ChartProfileSubsection.cs @@ -43,14 +43,14 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { var values = GetValues(e.NewValue); - if (values?.Length > 1) + if (values == null || values.Length <= 1) { - chart.Values = fillZeroValues(values); - Show(); + Hide(); return; } - Hide(); + chart.Values = fillZeroValues(values); + Show(); } /// From e36b1051c18ba0fc4d2210cea84b269c4547e2b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 13:15:59 +0900 Subject: [PATCH 4874/6909] Add spacing between inline comments --- .../Overlays/Profile/Sections/Historical/ProfileLineChart.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs index 989871745d..f02aa36b6c 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/ProfileLineChart.cs @@ -125,6 +125,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical continue; float y; + // special-case the min == max case to match LineGraph. // lerp isn't really well-defined over a zero interval anyway. if (min == max) @@ -218,6 +219,7 @@ namespace osu.Game.Overlays.Profile.Sections.Historical // first off, use log10 to calculate the number of digits in the "exact" interval. var numberOfDigits = Math.Floor(Math.Log10(exactTickInterval)); var tickBase = Math.Pow(10, numberOfDigits); + // then see how the exact tick relates to the power of 10. var exactTickMultiplier = exactTickInterval / tickBase; From 1fd4b04767bffde0750165ac1064ef13376bfacb Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 23 Nov 2020 20:43:46 -0800 Subject: [PATCH 4875/6909] Just set music shortcut text locally --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- osu.Game/Input/KeyBindingStore.cs | 2 +- .../Overlays/Music/MusicKeyBindingHandler.cs | 12 ++++++- osu.Game/Overlays/OSD/Toast.cs | 34 +++---------------- 4 files changed, 18 insertions(+), 32 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 44ac48d83a..89a6ee8b07 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -196,7 +196,7 @@ namespace osu.Game.Configuration public Func LookupSkinName { private get; set; } - public Func LookupKeyBindings { get; set; } + public Func LookupKeyBindings { get; set; } } public enum OsuSetting diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index f4b7c873d5..bc73d74d74 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -37,7 +37,7 @@ namespace osu.Game.Input /// /// The action to lookup. /// A set of display strings for all the user's key configuration for the action. - public IEnumerable GetReadableKeyCombinationsFor(GlobalAction? globalAction) + public IEnumerable GetReadableKeyCombinationsFor(GlobalAction globalAction) { foreach (var action in Query().Where(b => (GlobalAction)b.Action == globalAction)) { diff --git a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs index fa8180b7c0..f06e02e5e1 100644 --- a/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs +++ b/osu.Game/Overlays/Music/MusicKeyBindingHandler.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays.OSD; @@ -72,9 +73,18 @@ namespace osu.Game.Overlays.Music private class MusicActionToast : Toast { + private readonly GlobalAction action; + public MusicActionToast(string value, GlobalAction action) - : base("Music Playback", value, action: action) + : base("Music Playback", value, string.Empty) { + this.action = action; + } + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + ShortcutText.Text = config.LookupKeyBindings(action).ToUpperInvariant(); } } } diff --git a/osu.Game/Overlays/OSD/Toast.cs b/osu.Game/Overlays/OSD/Toast.cs index d32d63055f..4a6316df3f 100644 --- a/osu.Game/Overlays/OSD/Toast.cs +++ b/osu.Game/Overlays/OSD/Toast.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 osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Configuration; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Input.Bindings; using osuTK; using osuTK.Graphics; @@ -20,21 +17,14 @@ namespace osu.Game.Overlays.OSD private readonly Container content; - private readonly OsuSpriteText spriteText; - - private readonly string shortcut; - - private readonly GlobalAction? action; - protected override Container Content => content; protected readonly OsuSpriteText ValueText; - protected Toast(string description, string value, string shortcut = null, GlobalAction? action = null) - { - this.shortcut = shortcut; - this.action = action; + protected readonly OsuSpriteText ShortcutText; + protected Toast(string description, string value, string shortcut) + { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -81,7 +71,7 @@ namespace osu.Game.Overlays.OSD Origin = Anchor.Centre, Text = value }, - spriteText = new OsuSpriteText + ShortcutText = new OsuSpriteText { Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, @@ -89,23 +79,9 @@ namespace osu.Game.Overlays.OSD Alpha = 0.3f, Margin = new MarginPadding { Bottom = 15 }, Font = OsuFont.GetFont(size: 12, weight: FontWeight.Bold), + Text = string.IsNullOrEmpty(shortcut) ? "NO KEY BOUND" : shortcut.ToUpperInvariant() }, }; } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - string text; - - if (action != null) - text = config.LookupKeyBindings(action); - else if (!string.IsNullOrEmpty(shortcut)) - text = shortcut; - else - text = "no key bound"; - - spriteText.Text = text.ToUpperInvariant(); - } } } From ee33f62809067247049f03d6900eb7e7077bf55f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 15:19:51 +0900 Subject: [PATCH 4876/6909] Fix DrawableJudgement not always animating correctly on skin change If the skin is changed before gameplay has started (at the loading screen) it is possible for a sequence of events to occur which results in the animation not being played: - `SkinReloadableDrawable` runs its BDL load (and calls `OnSkinChanged` once) - User changes skin, triggering `DrawableJudgement`'s skin change handling (binding directly on the `SkinSource` locally) - This will call `PrepareDrawables` and reinitialise the `SkinnableDrawable` child hierarchy, then immediately apply the animations to it. - The new `SkinnableDrawable` will then get the `SkinChanged` event and schedule a handler for it, which will run on its first Update call. - Any added animations will be lost as a result. Fixed by binding directly to the `SkinnableDrawable`'s `OnSkinChanged`. This has the added bonus of not needing to reinitialise the child hierarchy on skin change (which felt a bit weird in the first place). --- .../Rulesets/Judgements/DrawableJudgement.cs | 63 ++++++++----------- 1 file changed, 27 insertions(+), 36 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 889e748a4a..45129c17ea 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -32,9 +32,6 @@ namespace osu.Game.Rulesets.Judgements private readonly Container aboveHitObjectsContent; - [Resolved] - private ISkinSource skinSource { get; set; } - /// /// Duration of initial fade in. /// @@ -78,29 +75,6 @@ namespace osu.Game.Rulesets.Judgements public Drawable GetProxyAboveHitObjectsContent() => aboveHitObjectsContent.CreateProxy(); - protected override void LoadComplete() - { - base.LoadComplete(); - skinSource.SourceChanged += onSkinChanged; - } - - private void onSkinChanged() - { - // on a skin change, the child component will update but not get correctly triggered to play its animation. - // we need to trigger a reinitialisation to make things right. - currentDrawableType = null; - - PrepareForUse(); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (skinSource != null) - skinSource.SourceChanged -= onSkinChanged; - } - /// /// Apply top-level animations to the current judgement when successfully hit. /// If displaying components which require lifetime extensions, manually adjusting is required. @@ -142,16 +116,17 @@ namespace osu.Game.Rulesets.Judgements Debug.Assert(Result != null); - prepareDrawables(); - runAnimation(); } private void runAnimation() { + // is a no-op if the drawables are already in a correct state. + prepareDrawables(); + // undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state. - ApplyTransformsAt(double.MinValue, true); - ClearTransforms(true); + // ApplyTransformsAt(double.MinValue, true); + // ClearTransforms(true); LifetimeStart = Result.TimeAbsolute; @@ -193,6 +168,8 @@ namespace osu.Game.Rulesets.Judgements private void prepareDrawables() { + Logger.Log("prepareDrawables on DrawableJudgement"); + var type = Result?.Type ?? HitResult.Perfect; //TODO: better default type from ruleset // todo: this should be removed once judgements are always pooled. @@ -203,7 +180,6 @@ namespace osu.Game.Rulesets.Judgements if (JudgementBody != null) RemoveInternal(JudgementBody); - aboveHitObjectsContent.Clear(); AddInternal(JudgementBody = new SkinnableDrawable(new GameplaySkinComponent(type), _ => CreateDefaultJudgement(type), confineMode: ConfineMode.NoScaling) { @@ -211,14 +187,29 @@ namespace osu.Game.Rulesets.Judgements Origin = Anchor.Centre, }); - if (JudgementBody.Drawable is IAnimatableJudgement animatable) + JudgementBody.OnSkinChanged += () => { - var proxiedContent = animatable.GetAboveHitObjectsProxiedContent(); - if (proxiedContent != null) - aboveHitObjectsContent.Add(proxiedContent); - } + // on a skin change, the child component will update but not get correctly triggered to play its animation (or proxy the newly created content). + // we need to trigger a reinitialisation to make things right. + proxyContent(); + runAnimation(); + }; + + proxyContent(); currentDrawableType = type; + + void proxyContent() + { + aboveHitObjectsContent.Clear(); + + if (JudgementBody.Drawable is IAnimatableJudgement animatable) + { + var proxiedContent = animatable.GetAboveHitObjectsProxiedContent(); + if (proxiedContent != null) + aboveHitObjectsContent.Add(proxiedContent); + } + } } protected virtual Drawable CreateDefaultJudgement(HitResult result) => new DefaultJudgementPiece(result); From 168226067750b572272903939f59bd4cbf5061cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 15:28:03 +0900 Subject: [PATCH 4877/6909] Remove left over logging line --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 45129c17ea..4ed3b75f2e 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -168,8 +168,6 @@ namespace osu.Game.Rulesets.Judgements private void prepareDrawables() { - Logger.Log("prepareDrawables on DrawableJudgement"); - var type = Result?.Type ?? HitResult.Perfect; //TODO: better default type from ruleset // todo: this should be removed once judgements are always pooled. From 72b8eef36e514c430be84abb82d5eabe1288af20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 15:41:56 +0900 Subject: [PATCH 4878/6909] Add ability to pause/resume replay playback --- .../Input/Bindings/GlobalActionContainer.cs | 6 ++++- osu.Game/Screens/Play/Player.cs | 6 ++++- osu.Game/Screens/Play/ReplayPlayer.cs | 23 ++++++++++++++++++- 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index e5d3a89a88..77f57bd637 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -69,6 +69,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), + new KeyBinding(InputKey.Space, GlobalAction.PauseReplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), }; @@ -175,7 +176,7 @@ namespace osu.Game.Input.Bindings [Description("Toggle notifications")] ToggleNotifications, - [Description("Pause")] + [Description("Pause Gameplay")] PauseGameplay, // Editor @@ -196,5 +197,8 @@ namespace osu.Game.Input.Bindings [Description("Random Skin")] RandomSkin, + + [Description("Pause Replay")] + PauseReplay, } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index d0a83e3c22..7979b635aa 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -339,7 +339,11 @@ namespace osu.Game.Screens.Play AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, IsCounting = false }, - RequestSeek = GameplayClockContainer.Seek, + RequestSeek = time => + { + GameplayClockContainer.Seek(time); + GameplayClockContainer.Start(); + }, Anchor = Anchor.Centre, Origin = Anchor.Centre }, diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 3a4298f22d..2c0b766a17 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -1,12 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; using osu.Game.Scoring; using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Play { - public class ReplayPlayer : Player + public class ReplayPlayer : Player, IKeyBindingHandler { protected readonly Score Score; @@ -35,5 +37,24 @@ namespace osu.Game.Screens.Play return Score.ScoreInfo; } + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.PauseReplay: + if (GameplayClockContainer.IsPaused.Value) + GameplayClockContainer.Start(); + else + GameplayClockContainer.Stop(); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } } } From 1d82557d9f5158006d4c29af41d8ce583e6758f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 15:42:14 +0900 Subject: [PATCH 4879/6909] Avoid blocking global actions when skip overlay is not actually active --- osu.Game/Screens/Play/SkipOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index b123757ded..92b304de91 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -133,6 +133,9 @@ namespace osu.Game.Screens.Play switch (action) { case GlobalAction.SkipCutscene: + if (!button.Enabled.Value) + return false; + button.Click(); return true; } From bd1dad5477f46c041b176d6644714e5f7f126291 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 15:54:27 +0900 Subject: [PATCH 4880/6909] Remove null allowance for now --- osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs index 5de6842b50..6e2737256a 100644 --- a/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs +++ b/osu.Game/Screens/Edit/Setup/FileChooserLabelledTextBox.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Edit.Setup CurrentFile = { BindTarget = currentFile } }; - sectionsContainer?.ScrollTo(fileSelector); + sectionsContainer.ScrollTo(fileSelector); } internal class FileChooserOsuTextBox : OsuTextBox From b9c1f782fa00b52beb0b29772ec9767d45b3e2db Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 17:03:26 +0900 Subject: [PATCH 4881/6909] Remove type parameter from DrawableCatchHitObject --- .../Objects/Drawables/DrawableBananaShower.cs | 2 +- .../Drawables/DrawableCatchHitObject.cs | 19 +++---------------- .../Objects/Drawables/DrawableDroplet.cs | 4 ++-- .../Objects/Drawables/DrawableFruit.cs | 4 ++-- .../Objects/Drawables/DrawableJuiceStream.cs | 2 +- 5 files changed, 9 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs index 4ce80aceb8..2215e1d983 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableBananaShower : DrawableCatchHitObject + public class DrawableBananaShower : DrawableCatchHitObject { private readonly Func> createDrawableRepresentation; private readonly Container bananaContainer; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 7922510a49..07f1f79243 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -13,12 +13,11 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public abstract class PalpableDrawableCatchHitObject : DrawableCatchHitObject - where TObject : PalpableCatchHitObject + public abstract class PalpableDrawableCatchHitObject : DrawableCatchHitObject { protected Container ScaleContainer { get; private set; } - protected PalpableDrawableCatchHitObject(TObject hitObject) + protected PalpableDrawableCatchHitObject(CatchHitObject hitObject) : base(hitObject) { Origin = Anchor.Centre; @@ -46,19 +45,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count]; } - public abstract class DrawableCatchHitObject : DrawableCatchHitObject - where TObject : CatchHitObject - { - public new TObject HitObject; - - protected DrawableCatchHitObject(TObject hitObject) - : base(hitObject) - { - HitObject = hitObject; - Anchor = Anchor.BottomLeft; - } - } - public abstract class DrawableCatchHitObject : DrawableHitObject { protected override double InitialLifetimeOffset => HitObject.TimePreempt; @@ -73,6 +59,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables : base(hitObject) { X = hitObject.X; + Anchor = Anchor.BottomLeft; } public Func CheckPosition; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 688240fd86..9db64eba6e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -8,11 +8,11 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableDroplet : PalpableDrawableCatchHitObject + public class DrawableDroplet : PalpableDrawableCatchHitObject { public override bool StaysOnPlate => false; - public DrawableDroplet(Droplet h) + public DrawableDroplet(CatchHitObject h) : base(h) { } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index c1c34e4157..f87c8866b1 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -8,9 +8,9 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableFruit : PalpableDrawableCatchHitObject + public class DrawableFruit : PalpableDrawableCatchHitObject { - public DrawableFruit(Fruit h) + public DrawableFruit(CatchHitObject h) : base(h) { } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs index 7bc016d94f..af4a269404 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs @@ -10,7 +10,7 @@ using osuTK; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableJuiceStream : DrawableCatchHitObject + public class DrawableJuiceStream : DrawableCatchHitObject { private readonly Func> createDrawableRepresentation; private readonly Container dropletContainer; From c9a41f9dae9ece4d7de354f1137b521d02d8add0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 17:14:39 +0900 Subject: [PATCH 4882/6909] Make all objects in selection candidates for spatial snapping Closes #10898. --- .../Editor/TestSceneManiaBeatSnapGrid.cs | 5 +++ .../Editor/TestSceneOsuDistanceSnapGrid.cs | 3 ++ .../Edit/OsuHitObjectComposer.cs | 11 +++++- .../Editing/TestSceneDistanceSnapGrid.cs | 3 ++ osu.Game/Rulesets/Edit/HitObjectComposer.cs | 3 ++ .../Rulesets/Edit/IPositionSnapProvider.cs | 12 ++++++- .../Compose/Components/BlueprintContainer.cs | 34 ++++++++++++++----- .../Compose/Components/Timeline/Timeline.cs | 3 ++ 8 files changed, 64 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs index 654b752001..538a51db5f 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaBeatSnapGrid.cs @@ -96,6 +96,11 @@ namespace osu.Game.Rulesets.Mania.Tests.Editor throw new System.NotImplementedException(); } + public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) + { + throw new System.NotImplementedException(); + } + public override float GetBeatSnapDistanceAt(double referenceTime) { throw new System.NotImplementedException(); diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs index 1232369a0b..9af2a99470 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneOsuDistanceSnapGrid.cs @@ -174,6 +174,9 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor private class SnapProvider : IPositionSnapProvider { + public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + 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/OsuHitObjectComposer.cs b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs index bfa8ab4431..0490e8b8ce 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuHitObjectComposer.cs @@ -105,11 +105,20 @@ namespace osu.Game.Rulesets.Osu.Edit } } - public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + public override SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) { if (snapToVisibleBlueprints(screenSpacePosition, out var snapResult)) return snapResult; + return new SnapResult(screenSpacePosition, null); + } + + public override SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) + { + var positionSnap = SnapScreenSpacePositionToValidPosition(screenSpacePosition); + if (positionSnap.ScreenSpacePosition != screenSpacePosition) + return positionSnap; + // will be null if distance snap is disabled or not feasible for the current time value. if (distanceSnapGrid == null) return base.SnapScreenSpacePositionToValidTime(screenSpacePosition); diff --git a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs index 8190cf5f89..11830ebe35 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneDistanceSnapGrid.cs @@ -153,6 +153,9 @@ namespace osu.Game.Tests.Visual.Editing private class SnapProvider : IPositionSnapProvider { + public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + 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 b90aa6863a..35852f60ea 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -442,6 +442,9 @@ namespace osu.Game.Rulesets.Edit public abstract SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); + public virtual SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + public abstract float GetBeatSnapDistanceAt(double referenceTime); public abstract float DurationToDistance(double referenceTime, double duration); diff --git a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs index cce631464f..4664f3808c 100644 --- a/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs +++ b/osu.Game/Rulesets/Edit/IPositionSnapProvider.cs @@ -8,12 +8,22 @@ namespace osu.Game.Rulesets.Edit public interface IPositionSnapProvider { /// - /// Given a position, find a valid time snap. + /// Given a position, find a valid time and position snap. /// + /// + /// This call should be equivalent to running with any additional logic that can be performed without the time immutability restriction. + /// /// The screen-space position to be snapped. /// The time and position post-snapping. SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition); + /// + /// Given a position, find a value position snap, restricting time to its input value. + /// + /// The screen-space position to be snapped. + /// The position post-snapping. Time will always be null. + SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition); + /// /// Retrieves the distance between two points within a timing point that are one beat length apart. /// diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index df9cadebfc..e9f5238980 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -424,7 +424,7 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement - private Vector2? movementBlueprintOriginalPosition; + private Vector2[] movementBlueprintOriginalPositions; private SelectionBlueprint movementBlueprint; private bool isDraggingBlueprint; @@ -442,8 +442,9 @@ namespace osu.Game.Screens.Edit.Compose.Components return; // 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.ScreenSpaceSelectionPoint; // todo: unsure if correct + var orderedSelection = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime); + movementBlueprint = orderedSelection.First(); + movementBlueprintOriginalPositions = orderedSelection.Select(m => m.ScreenSpaceSelectionPoint).ToArray(); } /// @@ -459,12 +460,29 @@ namespace osu.Game.Screens.Edit.Compose.Components if (snapProvider == null) return true; - Debug.Assert(movementBlueprintOriginalPosition != null); + Debug.Assert(movementBlueprintOriginalPositions != null); - HitObject draggedObject = movementBlueprint.HitObject; + Vector2 distanceTravelled = e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + + // check for positional snap for every object in selection (for things like object-object snapping) + for (var i = 0; i < movementBlueprintOriginalPositions.Length; i++) + { + var testPosition = movementBlueprintOriginalPositions[i] + distanceTravelled; + + var positionalResult = snapProvider.SnapScreenSpacePositionToValidPosition(testPosition); + + if (positionalResult.ScreenSpacePosition == testPosition) continue; + + // attempt to move the objects, and abort any time based snapping if we can. + if (SelectionHandler.HandleMovement(new MoveSelectionEvent(SelectionHandler.SelectedBlueprints.ElementAt(i), positionalResult.ScreenSpacePosition))) + return true; + } + + // if no positional snapping could be performed, try unrestricted snapping from the earliest + // hitobject in the selection. // The final movement position, relative to movementBlueprintOriginalPosition. - Vector2 movePosition = movementBlueprintOriginalPosition.Value + e.ScreenSpaceMousePosition - e.ScreenSpaceMouseDownPosition; + Vector2 movePosition = movementBlueprintOriginalPositions.First() + distanceTravelled; // Retrieve a snapped position. var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); @@ -476,7 +494,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (result.Time.HasValue) { // Apply the start time at the newly snapped-to position - double offset = result.Time.Value - draggedObject.StartTime; + double offset = result.Time.Value - movementBlueprint.HitObject.StartTime; foreach (HitObject obj in Beatmap.SelectedHitObjects) { @@ -497,7 +515,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (movementBlueprint == null) return false; - movementBlueprintOriginalPosition = null; + movementBlueprintOriginalPositions = null; movementBlueprint = null; 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 f6675902fc..20836c0e68 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -224,6 +224,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline /// public double VisibleRange => track.Length / Zoom; + public SnapResult SnapScreenSpacePositionToValidPosition(Vector2 screenSpacePosition) => + new SnapResult(screenSpacePosition, null); + public SnapResult SnapScreenSpacePositionToValidTime(Vector2 screenSpacePosition) => new SnapResult(screenSpacePosition, beatSnapProvider.SnapTime(getTimeFromPosition(Content.ToLocalSpace(screenSpacePosition)))); From 09f2a85d71bcca9753e6ed622574ac6ff29cd663 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 17:40:00 +0900 Subject: [PATCH 4883/6909] Fix potential test failure due to precision check missing --- osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs index 1ca94df26b..8212a16540 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor var first = (OsuHitObject)objects.First(); var second = (OsuHitObject)objects.Last(); - return first.Position == second.Position; + return Precision.AlmostEquals(first.EndPosition, second.Position); }); } From 4eef6c0d4072ef8a2670b0a40b4fcfd07fad1303 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 24 Nov 2020 17:59:18 +0900 Subject: [PATCH 4884/6909] Add test coverage --- .../Editor/TestSceneObjectObjectSnap.cs | 50 +++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs index 8212a16540..d20be90001 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -86,5 +86,55 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor return Precision.AlmostEquals(first.EndPosition, second.Position); }); } + + [Test] + public void TestSecondCircleInSelectionAlsoSnaps() + { + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); + + AddStep("disable distance snap", () => InputManager.Key(Key.Q)); + + AddStep("enter placement mode", () => InputManager.Key(Key.Number2)); + + AddStep("place first object", () => InputManager.Click(MouseButton.Left)); + + AddStep("move mouse right", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.2f, 0))); + AddStep("place second object", () => InputManager.Click(MouseButton.Left)); + + AddStep("move mouse down", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Width * 0.2f))); + AddStep("place third object", () => InputManager.Click(MouseButton.Left)); + + AddStep("enter selection mode", () => InputManager.Key(Key.Number1)); + + AddStep("select objects 2 and 3", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Skip(1))); + + AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); + + AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.02f, 0))); + + AddAssert("object 3 snapped to 1", () => + { + var objects = EditorBeatmap.HitObjects; + + var first = (OsuHitObject)objects.First(); + var third = (OsuHitObject)objects.Last(); + + return Precision.AlmostEquals(first.EndPosition, third.Position); + }); + + AddStep("move mouse slightly off centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * -0.22f, playfield.ScreenSpaceDrawQuad.Width * 0.21f))); + + AddAssert("object 2 snapped to 1", () => + { + var objects = EditorBeatmap.HitObjects; + + var first = (OsuHitObject)objects.First(); + var second = (OsuHitObject)objects.ElementAt(1); + + return Precision.AlmostEquals(first.EndPosition, second.Position); + }); + + AddStep("end drag", () => InputManager.ReleaseButton(MouseButton.Left)); + } } } From 07ee36a4ef2dde5ae2b524f672a124d3a8942efa Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Tue, 24 Nov 2020 17:18:19 +0800 Subject: [PATCH 4885/6909] Simplify code --- osu.Game/Screens/Import/FileImportScreen.cs | 118 ++++---------------- 1 file changed, 19 insertions(+), 99 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index bcbc87e8cc..2f2548c15a 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -13,7 +13,6 @@ using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osuTK; -using osu.Game.Overlays.Settings; using osu.Game.Overlays; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.Containers; @@ -29,12 +28,11 @@ namespace osu.Game.Screens.Import public override bool HideOverlaysOnEnter => true; - private string[] fileExtensions = { ".foo" }; + private string[] fileExtensions; private string defaultPath; private readonly Bindable currentFile = new Bindable(); private readonly IBindable currentDirectory = new Bindable(); - private readonly Bindable filterType = new Bindable(FileFilterType.All); private TextFlowContainer currentFileText; private OsuScrollContainer fileNameScroll; private readonly OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); @@ -42,15 +40,12 @@ namespace osu.Game.Screens.Import [Resolved] private OsuGameBase gameBase { get; set; } - [Resolved] - private DialogOverlay dialogOverlay { get; set; } - [BackgroundDependencyLoader(true)] private void load(Storage storage) { storage.GetStorageForDirectory("imports"); var originalPath = storage.GetFullPath("imports", true); - + fileExtensions = new string[] { ".osk", ".osr", ".osz" }; defaultPath = originalPath; InternalChild = contentContainer = new Container @@ -131,85 +126,43 @@ namespace osu.Game.Screens.Import { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 15, Top =15 }, Children = new Drawable[] { - new Box - { - Colour = overlayColourProvider.Background4, - RelativeSizeAxes = Axes.Both - }, new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Spacing = new Vector2(10), Children = new Drawable[] { - new SettingsEnumDropdown - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - LabelText = "File Type", - Current = filterType, - Margin = new MarginPadding { Bottom = 15 } - }, - new GridContainer - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Top = 15 }, - RowDimensions = new[] + new TriangleButton { - new Dimension(GridSizeMode.AutoSize) - }, - Content = new[] - { - new Drawable[] + Text = "Import", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Height = 50, + Width = 0.9f, + Action = () => { - new TriangleButton - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Height = 50, - Width = 0.9f, - Text = "Refresh", - Action = refresh - }, - new TriangleButton - { - Text = "Import", - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Height = 50, - Width = 0.9f, - Action = () => - { - var d = currentFile.Value?.FullName; - if (d != null) - startImport(d); - else - currentFileText.FlashColour(Color4.Red, 500); - }, - } - }, + var d = currentFile.Value?.FullName; + if (d != null) + startImport(d); + else + currentFileText.FlashColour(Color4.Red, 500); + } } - }, } - }, + } } } } } - }, + } } } } }; - fileNameScroll.ScrollContent.Anchor = Anchor.Centre; fileNameScroll.ScrollContent.Origin = Anchor.Centre; @@ -218,37 +171,6 @@ namespace osu.Game.Screens.Import { currentFile.Value = null; }); - - filterType.BindValueChanged(onFilterTypeChanged, true); - } - - private void onFilterTypeChanged(ValueChangedEvent v) - { - switch (v.NewValue) - { - case FileFilterType.Beatmap: - fileExtensions = new string[] { ".osz" }; - break; - - case FileFilterType.Skin: - fileExtensions = new string[] { ".osk" }; - break; - - case FileFilterType.Replay: - fileExtensions = new string[] { ".osr" }; - break; - - default: - case FileFilterType.All: - fileExtensions = new string[] { ".osk", ".osr", ".osz" }; - break; - } - - refresh(); - } - - private void refresh() - { currentFile.UnbindBindings(); currentDirectory.UnbindBindings(); @@ -265,7 +187,6 @@ namespace osu.Game.Screens.Import fileSelectContainer.Add(fileSelector); } - private void updateFileSelectionText(ValueChangedEvent v) { currentFileText.Text = v.NewValue?.Name ?? "Select a file"; @@ -298,7 +219,6 @@ namespace osu.Game.Screens.Import if (!File.Exists(path)) { - refresh(); currentFileText.Text = "File not exist"; currentFileText.FlashColour(Color4.Red, 500); return; From 0b735b84cf205bb96f0e4d5308a64abf92ee9e63 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Tue, 24 Nov 2020 17:23:53 +0800 Subject: [PATCH 4886/6909] Remove unused enum --- osu.Game/Screens/Import/FileImportScreen.cs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 2f2548c15a..f53ea70f45 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -228,13 +228,5 @@ namespace osu.Game.Screens.Import Task.Factory.StartNew(() => gameBase.Import(paths), TaskCreationOptions.LongRunning); } - - public enum FileFilterType - { - Skin, - Beatmap, - Replay, - All - } } } From df3844cdbb833efc609355f1104c5983b1d00d7b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 12:50:37 +0900 Subject: [PATCH 4887/6909] Add failing tests for pooling scrolling playfield --- .../TestSceneDrawableScrollingRuleset.cs | 182 ++++++++++++++++-- 1 file changed, 165 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 1a1babb4a8..ff3d152090 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -21,13 +21,13 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Objects.Legacy; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Graphics; +using JetBrains.Annotations; namespace osu.Game.Tests.Visual.Gameplay { @@ -46,6 +46,65 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUp] public void Setup() => Schedule(() => testClock.CurrentTime = 0); + [Test] + public void TestHitObjectPooling() + { + var beatmap = createBeatmap(_ => new TestPooledHitObject()); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); + createTest(beatmap); + + assertPosition(0, 0f); + assertDead(3); + + setTime(3 * time_range); + assertPosition(3, 0f); + assertDead(0); + + setTime(0 * time_range); + assertPosition(0, 0f); + assertDead(3); + } + + [TestCase("pooled")] + [TestCase("non-pooled")] + public void TestNestedHitObject(string pooled) + { + var beatmap = createBeatmap(i => + { + var h = pooled == "pooled" ? new TestPooledParentHitObject() : new TestParentHitObject(); + h.Duration = 300; + h.ChildTimeOffset = i % 3 * 100; + return h; + }); + beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); + createTest(beatmap); + + assertPosition(0, 0f); + assertHeight(0); + assertChildPosition(0); + + setTime(5 * time_range); + assertPosition(5, 0f); + assertHeight(5); + assertChildPosition(5); + } + + private void assertDead(int index) => AddAssert($"hitobject {index} is dead", () => getDrawableHitObject(index) == null); + + private void assertHeight(int index) => AddAssert($"hitobject {index} height", () => + { + var d = getDrawableHitObject(index); + return d != null && Precision.AlmostEquals(d.DrawHeight, yScale * (float)(d.HitObject.Duration / time_range), 0.1f); + }); + + private void assertChildPosition(int index) => AddAssert($"hitobject {index} child position", () => + { + var d = getDrawableHitObject(index); + return d is DrawableTestParentHitObject && Precision.AlmostEquals( + d.NestedHitObjects.First().DrawPosition.Y, + yScale * (float)((TestParentHitObject)d.HitObject).ChildTimeOffset / time_range, 0.1f); + }); + [Test] public void TestRelativeBeatLengthScaleSingleTimingPoint() { @@ -147,8 +206,21 @@ namespace osu.Game.Tests.Visual.Gameplay assertPosition(1, 1); } + /// + /// Get a corresponding to the 'th . + /// When a pooling is used and the hit object is not alive, `null` is returned. + /// + [CanBeNull] + private DrawableTestHitObject getDrawableHitObject(int index) + { + var hitObject = drawableRuleset.Beatmap.HitObjects.ElementAt(index); + return (DrawableTestHitObject)drawableRuleset.Playfield.HitObjectContainer.Objects.FirstOrDefault(obj => obj.HitObject == hitObject); + } + + private float yScale => drawableRuleset.Playfield.HitObjectContainer.DrawHeight; + private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}", - () => Precision.AlmostEquals(drawableRuleset.Playfield.AllHitObjects.ElementAt(index).DrawPosition.Y, drawableRuleset.Playfield.HitObjectContainer.DrawHeight * relativeY)); + () => Precision.AlmostEquals(getDrawableHitObject(index)?.DrawPosition.Y ?? -1, yScale * relativeY)); private void setTime(double time) { @@ -160,12 +232,16 @@ namespace osu.Game.Tests.Visual.Gameplay /// The hitobjects are spaced milliseconds apart. /// /// The . - private IBeatmap createBeatmap() + private IBeatmap createBeatmap(Func createAction = null) { - var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } }; + var beatmap = new Beatmap { BeatmapInfo = { Ruleset = new OsuRuleset().RulesetInfo } }; for (int i = 0; i < 10; i++) - beatmap.HitObjects.Add(new HitObject { StartTime = i * time_range }); + { + var h = createAction?.Invoke(i) ?? new TestHitObject(); + h.StartTime = i * time_range; + beatmap.HitObjects.Add(h); + } return beatmap; } @@ -225,7 +301,14 @@ namespace osu.Game.Tests.Visual.Gameplay TimeRange.Value = time_range; } - public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => new DrawableTestHitObject(h); + public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => + h switch + { + TestPooledHitObject _ => null, + TestPooledParentHitObject _ => null, + TestParentHitObject p => new DrawableTestParentHitObject(p), + _ => new DrawableTestHitObject(h), + }; protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); @@ -265,6 +348,9 @@ namespace osu.Game.Tests.Visual.Gameplay } } }); + + RegisterPool(1); + RegisterPool(1); } } @@ -277,30 +363,46 @@ namespace osu.Game.Tests.Visual.Gameplay public override bool CanConvert() => true; - protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) - { - yield return new TestHitObject - { - StartTime = original.StartTime, - Duration = (original as IHasDuration)?.Duration ?? 100 - }; - } + protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) => + throw new NotImplementedException(); } #endregion #region HitObject - private class TestHitObject : ConvertHitObject, IHasDuration + private class TestHitObject : HitObject, IHasDuration { public double EndTime => StartTime + Duration; - public double Duration { get; set; } + public double Duration { get; set; } = 100; + } + + private class TestPooledHitObject : TestHitObject + { + } + + private class TestParentHitObject : TestHitObject + { + public double ChildTimeOffset; + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + AddNested(new TestHitObject { StartTime = StartTime + ChildTimeOffset }); + } + } + + private class TestPooledParentHitObject : TestParentHitObject + { + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + AddNested(new TestPooledHitObject { StartTime = StartTime + ChildTimeOffset }); + } } private class DrawableTestHitObject : DrawableHitObject { - public DrawableTestHitObject(TestHitObject hitObject) + public DrawableTestHitObject([CanBeNull] TestHitObject hitObject) : base(hitObject) { Anchor = Anchor.TopCentre; @@ -326,6 +428,52 @@ namespace osu.Game.Tests.Visual.Gameplay } } + private class DrawableTestPooledHitObject : DrawableTestHitObject + { + public DrawableTestPooledHitObject() + : base(null) + { + InternalChildren[0].Colour = Color4.LightSkyBlue; + InternalChildren[1].Colour = Color4.Blue; + } + + protected override void Update() => LifetimeEnd = HitObject.EndTime; + } + + private class DrawableTestParentHitObject : DrawableTestHitObject + { + private readonly Container container; + + public DrawableTestParentHitObject([CanBeNull] TestHitObject hitObject) + : base(hitObject) + { + InternalChildren[0].Colour = Color4.LightYellow; + InternalChildren[1].Colour = Color4.Yellow; + + AddInternal(container = new Container + { + RelativeSizeAxes = Axes.Both, + }); + } + + protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) => + new DrawableTestHitObject((TestHitObject)hitObject); + + protected override void AddNestedHitObject(DrawableHitObject hitObject) => container.Add(hitObject); + + protected override void ClearNestedHitObjects() => container.Clear(false); + } + + private class DrawableTestPooledParentHitObject : DrawableTestParentHitObject + { + public DrawableTestPooledParentHitObject() + : base(null) + { + InternalChildren[0].Colour = Color4.LightSeaGreen; + InternalChildren[1].Colour = Color4.Green; + } + } + #endregion } } From 5c743adbae77f5447d35be352aa1c87834f18f93 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 12:57:39 +0900 Subject: [PATCH 4888/6909] Support hit object pooling scrolling playfield --- .../Scrolling/ScrollingHitObjectContainer.cs | 40 ++++--------------- .../UI/Scrolling/ScrollingPlayfield.cs | 6 +++ 2 files changed, 14 insertions(+), 32 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index bf64175468..802204900b 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -48,33 +48,8 @@ namespace osu.Game.Rulesets.UI.Scrolling timeRange.ValueChanged += _ => layoutCache.Invalidate(); } - public override void Add(DrawableHitObject hitObject) - { - combinedObjCache.Invalidate(); - hitObject.DefaultsApplied += onDefaultsApplied; - base.Add(hitObject); - } - - public override bool Remove(DrawableHitObject hitObject) - { - var result = base.Remove(hitObject); - - if (result) - { - combinedObjCache.Invalidate(); - hitObjectInitialStateCache.Remove(hitObject); - - hitObject.DefaultsApplied -= onDefaultsApplied; - } - - return result; - } - public override void Clear(bool disposeChildren = true) { - foreach (var h in Objects) - h.DefaultsApplied -= onDefaultsApplied; - base.Clear(disposeChildren); combinedObjCache.Invalidate(); @@ -173,18 +148,19 @@ namespace osu.Game.Rulesets.UI.Scrolling } } - private void onDefaultsApplied(DrawableHitObject drawableObject) + /// + /// Invalidate the cache of the layout of this hit object. + /// + public void InvalidateDrawableHitObject(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 state)) - { - combinedObjCache.Invalidate(); state.Cache.Invalidate(); - } + + combinedObjCache.Invalidate(); } - private float scrollLength; + // Use a nonzero value to prevent infinite results + private float scrollLength = 1; protected override void Update() { diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 9dac3f4de1..9b21a3f0a9 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -24,6 +24,12 @@ namespace osu.Game.Rulesets.UI.Scrolling Direction.BindTo(ScrollingInfo.Direction); } + protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) + { + drawableHitObject.HitObjectApplied += + ((ScrollingHitObjectContainer)HitObjectContainer).InvalidateDrawableHitObject; + } + /// /// Given a position in screen space, return the time within this column. /// From 8f39b54e58dbd673a124875da57935acd6a7a7c0 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 13:34:09 +0900 Subject: [PATCH 4889/6909] Simplify ScrollingHitObjectContainer logic --- .../TestSceneDrawableScrollingRuleset.cs | 2 +- .../Scrolling/ScrollingHitObjectContainer.cs | 76 +++++-------------- 2 files changed, 22 insertions(+), 56 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index ff3d152090..cebe0394c7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -445,7 +445,7 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly Container container; public DrawableTestParentHitObject([CanBeNull] TestHitObject hitObject) - : base(hitObject) + : base(hitObject) { InternalChildren[0].Colour = Color4.LightYellow; InternalChildren[1].Colour = Color4.Yellow; diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 802204900b..11a7665f0f 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -2,13 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Layout; -using osu.Framework.Threading; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -19,7 +16,6 @@ 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; } @@ -27,9 +23,7 @@ namespace osu.Game.Rulesets.UI.Scrolling // 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. - // When invalidated, one or more (but not necessarily all) hitobject states must be re-validated. - private readonly Cached combinedObjCache = new Cached(); + private readonly HashSet invalidHitObjects = new HashSet(); public ScrollingHitObjectContainer() { @@ -52,8 +46,7 @@ namespace osu.Game.Rulesets.UI.Scrolling { base.Clear(disposeChildren); - combinedObjCache.Invalidate(); - hitObjectInitialStateCache.Clear(); + invalidHitObjects.Clear(); } /// @@ -150,17 +143,18 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// Invalidate the cache of the layout of this hit object. + /// A hit object should be invalidated after all its nested hit objects are invalidated. /// public void InvalidateDrawableHitObject(DrawableHitObject drawableObject) { - if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state)) - state.Cache.Invalidate(); + invalidHitObjects.Add(drawableObject); - combinedObjCache.Invalidate(); + // Remove children as nested hit objects will be recursively updated. + foreach (var nested in drawableObject.NestedHitObjects) + invalidHitObjects.Remove(nested); } - // Use a nonzero value to prevent infinite results - private float scrollLength = 1; + private float scrollLength; protected override void Update() { @@ -168,17 +162,11 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!layoutCache.IsValid) { - foreach (var state in hitObjectInitialStateCache.Values) - state.Cache.Invalidate(); - combinedObjCache.Invalidate(); + foreach (var obj in Objects) + invalidHitObjects.Add(obj); scrollingInfo.Algorithm.Reset(); - layoutCache.Validate(); - } - - if (!combinedObjCache.IsValid) - { switch (direction.Value) { case ScrollingDirection.Up: @@ -191,24 +179,16 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } - foreach (var obj in Objects) - { - if (!hitObjectInitialStateCache.TryGetValue(obj, out var state)) - state = hitObjectInitialStateCache[obj] = new InitialState(new Cached()); - - if (state.Cache.IsValid) - continue; - - state.ScheduledComputation?.Cancel(); - state.ScheduledComputation = computeInitialStateRecursive(obj); - - computeLifetimeStartRecursive(obj); - - state.Cache.Validate(); - } - - combinedObjCache.Validate(); + layoutCache.Validate(); } + + foreach (var obj in invalidHitObjects) + { + computeInitialStateRecursive(obj); + computeLifetimeStartRecursive(obj); + } + + invalidHitObjects.Clear(); } private void computeLifetimeStartRecursive(DrawableHitObject hitObject) @@ -247,7 +227,7 @@ namespace osu.Game.Rulesets.UI.Scrolling return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); } - private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => + private void computeInitialStateRecursive(DrawableHitObject hitObject) { if (hitObject.HitObject is IHasDuration e) { @@ -272,7 +252,7 @@ namespace osu.Game.Rulesets.UI.Scrolling // Nested hitobjects don't need to scroll, but they do need accurate positions updatePosition(obj, hitObject.HitObject.StartTime); } - }); + } protected override void UpdateAfterChildrenLife() { @@ -304,19 +284,5 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } } - - private class InitialState - { - [NotNull] - public readonly Cached Cache; - - [CanBeNull] - public ScheduledDelegate ScheduledComputation; - - public InitialState(Cached cache) - { - Cache = cache; - } - } } } From cabc8aa63b4b3e3cb7f59046054f8b87071b651a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 13:57:20 +0900 Subject: [PATCH 4890/6909] Revert "Simplify ScrollingHitObjectContainer logic" This reverts commit b4cc39149c117e6a0e95ee917a67cec8ba723d06. --- .../TestSceneDrawableScrollingRuleset.cs | 2 +- .../Scrolling/ScrollingHitObjectContainer.cs | 74 ++++++++++++++----- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index cebe0394c7..ff3d152090 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -445,7 +445,7 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly Container container; public DrawableTestParentHitObject([CanBeNull] TestHitObject hitObject) - : base(hitObject) + : base(hitObject) { InternalChildren[0].Colour = Color4.LightYellow; InternalChildren[1].Colour = Color4.Yellow; diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 11a7665f0f..802204900b 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -2,10 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Layout; +using osu.Framework.Threading; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -16,6 +19,7 @@ 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; } @@ -23,7 +27,9 @@ namespace osu.Game.Rulesets.UI.Scrolling // 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); - private readonly HashSet invalidHitObjects = new HashSet(); + // 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() { @@ -46,7 +52,8 @@ namespace osu.Game.Rulesets.UI.Scrolling { base.Clear(disposeChildren); - invalidHitObjects.Clear(); + combinedObjCache.Invalidate(); + hitObjectInitialStateCache.Clear(); } /// @@ -143,18 +150,17 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// Invalidate the cache of the layout of this hit object. - /// A hit object should be invalidated after all its nested hit objects are invalidated. /// public void InvalidateDrawableHitObject(DrawableHitObject drawableObject) { - invalidHitObjects.Add(drawableObject); + if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state)) + state.Cache.Invalidate(); - // Remove children as nested hit objects will be recursively updated. - foreach (var nested in drawableObject.NestedHitObjects) - invalidHitObjects.Remove(nested); + combinedObjCache.Invalidate(); } - private float scrollLength; + // Use a nonzero value to prevent infinite results + private float scrollLength = 1; protected override void Update() { @@ -162,11 +168,17 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!layoutCache.IsValid) { - foreach (var obj in Objects) - invalidHitObjects.Add(obj); + foreach (var state in hitObjectInitialStateCache.Values) + state.Cache.Invalidate(); + combinedObjCache.Invalidate(); scrollingInfo.Algorithm.Reset(); + layoutCache.Validate(); + } + + if (!combinedObjCache.IsValid) + { switch (direction.Value) { case ScrollingDirection.Up: @@ -179,16 +191,24 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } - layoutCache.Validate(); - } + foreach (var obj in Objects) + { + if (!hitObjectInitialStateCache.TryGetValue(obj, out var state)) + state = hitObjectInitialStateCache[obj] = new InitialState(new Cached()); - foreach (var obj in invalidHitObjects) - { - computeInitialStateRecursive(obj); - computeLifetimeStartRecursive(obj); - } + if (state.Cache.IsValid) + continue; - invalidHitObjects.Clear(); + state.ScheduledComputation?.Cancel(); + state.ScheduledComputation = computeInitialStateRecursive(obj); + + computeLifetimeStartRecursive(obj); + + state.Cache.Validate(); + } + + combinedObjCache.Validate(); + } } private void computeLifetimeStartRecursive(DrawableHitObject hitObject) @@ -227,7 +247,7 @@ namespace osu.Game.Rulesets.UI.Scrolling return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); } - private void computeInitialStateRecursive(DrawableHitObject hitObject) + private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => { if (hitObject.HitObject is IHasDuration e) { @@ -252,7 +272,7 @@ namespace osu.Game.Rulesets.UI.Scrolling // Nested hitobjects don't need to scroll, but they do need accurate positions updatePosition(obj, hitObject.HitObject.StartTime); } - } + }); protected override void UpdateAfterChildrenLife() { @@ -284,5 +304,19 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } } + + private class InitialState + { + [NotNull] + public readonly Cached Cache; + + [CanBeNull] + public ScheduledDelegate ScheduledComputation; + + public InitialState(Cached cache) + { + Cache = cache; + } + } } } From ec92545d7a4a2270d90682a3108c1936164d8e45 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 14:13:57 +0900 Subject: [PATCH 4891/6909] fix indent --- .../Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index ff3d152090..cebe0394c7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -445,7 +445,7 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly Container container; public DrawableTestParentHitObject([CanBeNull] TestHitObject hitObject) - : base(hitObject) + : base(hitObject) { InternalChildren[0].Colour = Color4.LightYellow; InternalChildren[1].Colour = Color4.Yellow; From ce57e8ddfb883e16dc99bfea54145c48b22a71a7 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 16:06:01 +0900 Subject: [PATCH 4892/6909] Separate Lifetime computation and layout update --- .../Scrolling/ScrollingHitObjectContainer.cs | 92 ++++++------------- 1 file changed, 30 insertions(+), 62 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 802204900b..1845094c44 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -2,13 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Layout; -using osu.Framework.Threading; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; using osuTK; @@ -19,7 +16,12 @@ namespace osu.Game.Rulesets.UI.Scrolling { private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); - private readonly Dictionary hitObjectInitialStateCache = new Dictionary(); + + // If a hit object is not in this set, the position and the size should be updated when the hit object becomes alive. + private readonly HashSet layoutComputedHitObjects = new HashSet(); + + // Used to recompute all lifetime when `layoutCache` becomes invalid + private readonly HashSet lifetimeComputedHitObjects = new HashSet(); [Resolved] private IScrollingInfo scrollingInfo { get; set; } @@ -27,10 +29,6 @@ namespace osu.Game.Rulesets.UI.Scrolling // 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. - // 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; @@ -52,8 +50,7 @@ namespace osu.Game.Rulesets.UI.Scrolling { base.Clear(disposeChildren); - combinedObjCache.Invalidate(); - hitObjectInitialStateCache.Clear(); + layoutComputedHitObjects.Clear(); } /// @@ -150,13 +147,15 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// Invalidate the cache of the layout of this hit object. + /// A hit object should be invalidated after all its nested hit objects are invalidated. /// - public void InvalidateDrawableHitObject(DrawableHitObject drawableObject) + public void InvalidateDrawableHitObject(DrawableHitObject hitObject) { - if (hitObjectInitialStateCache.TryGetValue(drawableObject, out var state)) - state.Cache.Invalidate(); + // lifetime is computed before update + hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); - combinedObjCache.Invalidate(); + lifetimeComputedHitObjects.Add(hitObject); + layoutComputedHitObjects.Remove(hitObject); } // Use a nonzero value to prevent infinite results @@ -168,17 +167,14 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!layoutCache.IsValid) { - foreach (var state in hitObjectInitialStateCache.Values) - state.Cache.Invalidate(); - combinedObjCache.Invalidate(); + // this.Objects cannot be used as it doesn't contain nested objects + foreach (var hitObject in lifetimeComputedHitObjects) + hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); + + layoutComputedHitObjects.Clear(); scrollingInfo.Algorithm.Reset(); - layoutCache.Validate(); - } - - if (!combinedObjCache.IsValid) - { switch (direction.Value) { case ScrollingDirection.Up: @@ -191,32 +187,18 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } - foreach (var obj in Objects) - { - if (!hitObjectInitialStateCache.TryGetValue(obj, out var state)) - state = hitObjectInitialStateCache[obj] = new InitialState(new Cached()); - - if (state.Cache.IsValid) - continue; - - state.ScheduledComputation?.Cancel(); - state.ScheduledComputation = computeInitialStateRecursive(obj); - - computeLifetimeStartRecursive(obj); - - state.Cache.Validate(); - } - - combinedObjCache.Validate(); + layoutCache.Validate(); } - } - private void computeLifetimeStartRecursive(DrawableHitObject hitObject) - { - hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); + foreach (var obj in AliveObjects) + { + if (layoutComputedHitObjects.Contains(obj)) + continue; - foreach (var obj in hitObject.NestedHitObjects) - computeLifetimeStartRecursive(obj); + updateLayoutRecursive(obj); + + layoutComputedHitObjects.Add(obj); + } } private double computeOriginAdjustedLifetimeStart(DrawableHitObject hitObject) @@ -247,7 +229,7 @@ namespace osu.Game.Rulesets.UI.Scrolling return scrollingInfo.Algorithm.GetDisplayStartTime(hitObject.HitObject.StartTime, originAdjustment, timeRange.Value, scrollLength); } - private ScheduledDelegate computeInitialStateRecursive(DrawableHitObject hitObject) => hitObject.Schedule(() => + private void updateLayoutRecursive(DrawableHitObject hitObject) { if (hitObject.HitObject is IHasDuration e) { @@ -267,12 +249,12 @@ namespace osu.Game.Rulesets.UI.Scrolling foreach (var obj in hitObject.NestedHitObjects) { - computeInitialStateRecursive(obj); + updateLayoutRecursive(obj); // Nested hitobjects don't need to scroll, but they do need accurate positions updatePosition(obj, hitObject.HitObject.StartTime); } - }); + } protected override void UpdateAfterChildrenLife() { @@ -304,19 +286,5 @@ namespace osu.Game.Rulesets.UI.Scrolling break; } } - - private class InitialState - { - [NotNull] - public readonly Cached Cache; - - [CanBeNull] - public ScheduledDelegate ScheduledComputation; - - public InitialState(Cached cache) - { - Cache = cache; - } - } } } From d5f082e5fb2cfa609d4e9f384d29812229c0914b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 16:25:22 +0900 Subject: [PATCH 4893/6909] Comment about lifetime assumption --- .../Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 1845094c44..e750727764 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -147,11 +147,13 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// Invalidate the cache of the layout of this hit object. - /// A hit object should be invalidated after all its nested hit objects are invalidated. /// public void InvalidateDrawableHitObject(DrawableHitObject hitObject) { - // lifetime is computed before update + // Lifetime is computed once early and + // layout (Width/Height if `IHasDuration`, and nested object positions) will be computed when the object becomes alive. + // An assumption is that a hit object layout update (setting `Height` or `Width`) won't affect its lifetime. + // This is satisfied in practice because otherwise the hit object won't be aligned to its `StartTime`. hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); lifetimeComputedHitObjects.Add(hitObject); From 7f6e4d5b217ae563b64893f9e194b7e5c2733d5e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 16:46:10 +0900 Subject: [PATCH 4894/6909] Delay lifetime computation until loaded --- .../Scrolling/ScrollingHitObjectContainer.cs | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index e750727764..150ed16bab 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 private readonly HashSet layoutComputedHitObjects = new HashSet(); // Used to recompute all lifetime when `layoutCache` becomes invalid - private readonly HashSet lifetimeComputedHitObjects = new HashSet(); + private readonly HashSet allHitObjects = new HashSet(); [Resolved] private IScrollingInfo scrollingInfo { get; set; } @@ -150,18 +150,19 @@ namespace osu.Game.Rulesets.UI.Scrolling /// public void InvalidateDrawableHitObject(DrawableHitObject hitObject) { - // Lifetime is computed once early and - // layout (Width/Height if `IHasDuration`, and nested object positions) will be computed when the object becomes alive. - // An assumption is that a hit object layout update (setting `Height` or `Width`) won't affect its lifetime. - // This is satisfied in practice because otherwise the hit object won't be aligned to its `StartTime`. - hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); + // Lifetime computation is delayed to the next update because `scrollLength` may not be valid here. + // Layout computation will be delayed to when the object becomes alive. + // An assumption is that a hit object layout update (setting `Height` or `Width`) won't affect its lifetime but + // this is satisfied in practice because otherwise the hit object won't be aligned to its `StartTime`. + + layoutCache.Invalidate(); + + allHitObjects.Add(hitObject); - lifetimeComputedHitObjects.Add(hitObject); layoutComputedHitObjects.Remove(hitObject); } - // Use a nonzero value to prevent infinite results - private float scrollLength = 1; + private float scrollLength; protected override void Update() { @@ -170,7 +171,7 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!layoutCache.IsValid) { // this.Objects cannot be used as it doesn't contain nested objects - foreach (var hitObject in lifetimeComputedHitObjects) + foreach (var hitObject in allHitObjects) hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); layoutComputedHitObjects.Clear(); From e34a2051044d2901df39328051a4dbdadd094802 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 18:52:15 +0900 Subject: [PATCH 4895/6909] Rewrite hit object management, take three --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 8 +-- osu.Game/Rulesets/UI/Playfield.cs | 4 +- .../Scrolling/ScrollingHitObjectContainer.cs | 63 ++++++++++++------- .../UI/Scrolling/ScrollingPlayfield.cs | 6 -- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 5fbda305c8..b10a6efaf2 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this represents the time when the s become alive. /// - internal event Action HitObjectUsageBegan; + internal event Action HitObjectUsageBegan; /// /// Invoked when a becomes unused by a . @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this represents the time when the s become dead. /// - internal event Action HitObjectUsageFinished; + internal event Action HitObjectUsageFinished; /// /// The amount of time prior to the current time within which s should be considered alive. @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.UI bindStartTime(drawable); AddInternal(drawableMap[entry] = drawable, false); - HitObjectUsageBegan?.Invoke(entry.HitObject); + HitObjectUsageBegan?.Invoke(drawable); } private void removeDrawable(HitObjectLifetimeEntry entry) @@ -132,7 +132,7 @@ namespace osu.Game.Rulesets.UI unbindStartTime(drawable); RemoveInternal(drawable); - HitObjectUsageFinished?.Invoke(entry.HitObject); + HitObjectUsageFinished?.Invoke(drawable); } #endregion diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 82ec653f31..e27ab7fda5 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -91,8 +91,8 @@ namespace osu.Game.Rulesets.UI { h.NewResult += (d, r) => NewResult?.Invoke(d, r); h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); - h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); - h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); + h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o.HitObject); + h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o.HitObject); })); } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 150ed16bab..c8afe76f19 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -17,11 +17,16 @@ namespace osu.Game.Rulesets.UI.Scrolling private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); - // If a hit object is not in this set, the position and the size should be updated when the hit object becomes alive. - private readonly HashSet layoutComputedHitObjects = new HashSet(); + // Tracks all `DrawableHitObject` (nested or not) applied a `HitObject`. + // It dynamically changes based on approximate lifetime when a pooling is used. + private readonly HashSet hitObjectApplied = new HashSet(); - // Used to recompute all lifetime when `layoutCache` becomes invalid - private readonly HashSet allHitObjects = new HashSet(); + // The lifetime of a hit object in this will be computed in next update. + private readonly HashSet toComputeLifetime = new HashSet(); + + // The layout (length if IHasDuration, and nested object positions) of a hit object *not* in this set will be computed in next updated. + // Only objects in `AliveObjects` are considered, to prevent a massive recomputation when scrolling speed or something changes. + private readonly HashSet layoutComputed = new HashSet(); [Resolved] private IScrollingInfo scrollingInfo { get; set; } @@ -34,6 +39,9 @@ namespace osu.Game.Rulesets.UI.Scrolling RelativeSizeAxes = Axes.Both; AddLayout(layoutCache); + + HitObjectUsageBegan += onHitObjectUsageBegin; + HitObjectUsageFinished += onHitObjectUsageFinished; } [BackgroundDependencyLoader] @@ -50,7 +58,9 @@ namespace osu.Game.Rulesets.UI.Scrolling { base.Clear(disposeChildren); - layoutComputedHitObjects.Clear(); + hitObjectApplied.Clear(); + toComputeLifetime.Clear(); + layoutComputed.Clear(); } /// @@ -145,21 +155,20 @@ namespace osu.Game.Rulesets.UI.Scrolling } } - /// - /// Invalidate the cache of the layout of this hit object. - /// - public void InvalidateDrawableHitObject(DrawableHitObject hitObject) + private void onHitObjectUsageBegin(DrawableHitObject hitObject) { - // Lifetime computation is delayed to the next update because `scrollLength` may not be valid here. - // Layout computation will be delayed to when the object becomes alive. - // An assumption is that a hit object layout update (setting `Height` or `Width`) won't affect its lifetime but - // this is satisfied in practice because otherwise the hit object won't be aligned to its `StartTime`. + // Lifetime computation is delayed until next update because + // when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed. + hitObjectApplied.Add(hitObject); + toComputeLifetime.Add(hitObject); + layoutComputed.Remove(hitObject); + } - layoutCache.Invalidate(); - - allHitObjects.Add(hitObject); - - layoutComputedHitObjects.Remove(hitObject); + private void onHitObjectUsageFinished(DrawableHitObject hitObject) + { + hitObjectApplied.Remove(hitObject); + toComputeLifetime.Remove(hitObject); + layoutComputed.Remove(hitObject); } private float scrollLength; @@ -170,11 +179,10 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!layoutCache.IsValid) { - // this.Objects cannot be used as it doesn't contain nested objects - foreach (var hitObject in allHitObjects) - hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); + foreach (var hitObject in hitObjectApplied) + toComputeLifetime.Add(hitObject); - layoutComputedHitObjects.Clear(); + layoutComputed.Clear(); scrollingInfo.Algorithm.Reset(); @@ -193,14 +201,21 @@ namespace osu.Game.Rulesets.UI.Scrolling layoutCache.Validate(); } + foreach (var hitObject in toComputeLifetime) + hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); + + toComputeLifetime.Clear(); + + // An assumption is that this update won't affect lifetime, + // but this is satisfied in practice because otherwise the hit object won't be aligned to its `StartTime`. foreach (var obj in AliveObjects) { - if (layoutComputedHitObjects.Contains(obj)) + if (layoutComputed.Contains(obj)) continue; updateLayoutRecursive(obj); - layoutComputedHitObjects.Add(obj); + layoutComputed.Add(obj); } } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 9b21a3f0a9..9dac3f4de1 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -24,12 +24,6 @@ namespace osu.Game.Rulesets.UI.Scrolling Direction.BindTo(ScrollingInfo.Direction); } - protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) - { - drawableHitObject.HitObjectApplied += - ((ScrollingHitObjectContainer)HitObjectContainer).InvalidateDrawableHitObject; - } - /// /// Given a position in screen space, return the time within this column. /// From 7ab27399bf148eda81aba3e30c7e88d3d0aa7143 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Tue, 24 Nov 2020 18:04:34 +0800 Subject: [PATCH 4896/6909] Fix CI errors --- osu.Game/Screens/Import/FileImportScreen.cs | 37 ++++++++++----------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index f53ea70f45..88dcd6623f 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -28,9 +28,7 @@ namespace osu.Game.Screens.Import public override bool HideOverlaysOnEnter => true; - private string[] fileExtensions; private string defaultPath; - private readonly Bindable currentFile = new Bindable(); private readonly IBindable currentDirectory = new Bindable(); private TextFlowContainer currentFileText; @@ -45,7 +43,7 @@ namespace osu.Game.Screens.Import { storage.GetStorageForDirectory("imports"); var originalPath = storage.GetFullPath("imports", true); - fileExtensions = new string[] { ".osk", ".osr", ".osz" }; + string[] fileExtensions = { ".osk", ".osr", ".osz" }; defaultPath = originalPath; InternalChild = contentContainer = new Container @@ -126,7 +124,7 @@ namespace osu.Game.Screens.Import { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 15, Top =15 }, + Margin = new MarginPadding { Bottom = 15, Top =15 } , Children = new Drawable[] { new FillFlowContainer @@ -135,21 +133,21 @@ namespace osu.Game.Screens.Import AutoSizeAxes = Axes.Y, Children = new Drawable[] { - new TriangleButton - { - Text = "Import", - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Height = 50, - Width = 0.9f, - Action = () => - { - var d = currentFile.Value?.FullName; - if (d != null) - startImport(d); - else - currentFileText.FlashColour(Color4.Red, 500); + new TriangleButton + { + Text = "Import", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Height = 50, + Width = 0.9f, + Action = () => + { + var d = currentFile.Value?.FullName; + if (d != null) + startImport(d); + else + currentFileText.FlashColour(Color4.Red, 500); } } } @@ -187,6 +185,7 @@ namespace osu.Game.Screens.Import fileSelectContainer.Add(fileSelector); } + private void updateFileSelectionText(ValueChangedEvent v) { currentFileText.Text = v.NewValue?.Name ?? "Select a file"; From 739c18a34d20eca3b542bbe2aad8e15d793e05e8 Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Tue, 24 Nov 2020 18:25:38 +0800 Subject: [PATCH 4897/6909] dotnet format --- osu.Game/Screens/Import/FileImportScreen.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 88dcd6623f..39fd67c4e1 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -135,14 +135,14 @@ namespace osu.Game.Screens.Import { new TriangleButton { - Text = "Import", - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Height = 50, - Width = 0.9f, - Action = () => - { + Text = "Import", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Height = 50, + Width = 0.9f, + Action = () => + { var d = currentFile.Value?.FullName; if (d != null) startImport(d); From b8a5cd94f741f98453bdd8118e95dc40369012f4 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 19:46:57 +0900 Subject: [PATCH 4898/6909] Invoke HitObjectUsageFinished before removal --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index b10a6efaf2..fb91108605 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -129,10 +129,10 @@ namespace osu.Game.Rulesets.UI drawableMap.Remove(entry); + HitObjectUsageFinished?.Invoke(drawable); + unbindStartTime(drawable); RemoveInternal(drawable); - - HitObjectUsageFinished?.Invoke(drawable); } #endregion From 916a313f1965ae52872001bd6b3e511854d3368d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 19:13:46 +0900 Subject: [PATCH 4899/6909] Rename PalpableDrawable -> DrawablePalpable --- .../Objects/Drawables/DrawableCatchHitObject.cs | 4 ++-- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs | 2 +- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 07f1f79243..7fbc79e4b5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -13,11 +13,11 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public abstract class PalpableDrawableCatchHitObject : DrawableCatchHitObject + public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject { protected Container ScaleContainer { get; private set; } - protected PalpableDrawableCatchHitObject(CatchHitObject hitObject) + protected DrawablePalpableCatchHitObject(CatchHitObject hitObject) : base(hitObject) { Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 9db64eba6e..37a05b5d76 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -8,7 +8,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableDroplet : PalpableDrawableCatchHitObject + public class DrawableDroplet : DrawablePalpableCatchHitObject { public override bool StaysOnPlate => false; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index f87c8866b1..b000a728c1 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -8,7 +8,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableFruit : PalpableDrawableCatchHitObject + public class DrawableFruit : DrawablePalpableCatchHitObject { public DrawableFruit(CatchHitObject h) : base(h) From 4f7aa7e54187fa5b6371499b7d963862a37fb678 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 19:16:03 +0900 Subject: [PATCH 4900/6909] Move Palpable* to separate files --- .../Objects/CatchHitObject.cs | 8 ---- .../Drawables/DrawableCatchHitObject.cs | 37 ---------------- .../DrawablePalpableCatchHitObject.cs | 44 +++++++++++++++++++ .../Objects/PalpableCatchHitObject.cs | 13 ++++++ 4 files changed, 57 insertions(+), 45 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs create mode 100644 osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 5985ec9b68..21bb026bae 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -105,14 +105,6 @@ namespace osu.Game.Rulesets.Catch.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; } - /// - /// Represents a single object that can be caught by the catcher. - /// - public abstract class PalpableCatchHitObject : CatchHitObject - { - public override bool CanBePlated => true; - } - public enum FruitVisualRepresentation { Pear, diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 7fbc79e4b5..4cbc998447 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -2,49 +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.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject - { - protected Container ScaleContainer { get; private set; } - - protected DrawablePalpableCatchHitObject(CatchHitObject hitObject) - : base(hitObject) - { - Origin = Anchor.Centre; - Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); - Masking = false; - } - - [BackgroundDependencyLoader] - private void load() - { - AddRangeInternal(new Drawable[] - { - ScaleContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - } - }); - - ScaleContainer.Scale = new Vector2(HitObject.Scale); - } - - protected override Color4 GetComboColour(IReadOnlyList comboColours) => - comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count]; - } - public abstract class DrawableCatchHitObject : DrawableHitObject { protected override double InitialLifetimeOffset => HitObject.TimePreempt; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs new file mode 100644 index 0000000000..3e843d60c1 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject + { + protected Container ScaleContainer { get; private set; } + + protected DrawablePalpableCatchHitObject(CatchHitObject hitObject) + : base(hitObject) + { + Origin = Anchor.Centre; + Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); + Masking = false; + } + + [BackgroundDependencyLoader] + private void load() + { + AddRangeInternal(new Drawable[] + { + ScaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + } + }); + + ScaleContainer.Scale = new Vector2(HitObject.Scale); + } + + protected override Color4 GetComboColour(IReadOnlyList comboColours) => + comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count]; + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs new file mode 100644 index 0000000000..3a0cbe3ecd --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.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.Catch.Objects +{ + /// + /// Represents a single object that can be caught by the catcher. + /// + public abstract class PalpableCatchHitObject : CatchHitObject + { + public override bool CanBePlated => true; + } +} From 0009724c1a36914287df4c7b835078101b85520c Mon Sep 17 00:00:00 2001 From: PercyDan54 <50285552+PercyDan54@users.noreply.github.com> Date: Tue, 24 Nov 2020 18:48:32 +0800 Subject: [PATCH 4901/6909] Fix CI --- osu.Game/Screens/Import/FileImportScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 39fd67c4e1..a59688a2a9 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -124,7 +124,7 @@ namespace osu.Game.Screens.Import { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 15, Top =15 } , + Margin = new MarginPadding { Bottom = 15, Top = 15 }, Children = new Drawable[] { new FillFlowContainer @@ -148,8 +148,8 @@ namespace osu.Game.Screens.Import startImport(d); else currentFileText.FlashColour(Color4.Red, 500); - } } + } } } } From ab7251d742ec03ae9a9feb082ae2867209e0b955 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 24 Nov 2020 19:57:37 +0900 Subject: [PATCH 4902/6909] Move members to `PalpableCatchHitObject` --- .../TestSceneFruitObjects.cs | 2 +- .../Beatmaps/CatchBeatmapProcessor.cs | 22 ++++++++-------- .../Preprocessing/CatchDifficultyHitObject.cs | 4 +-- .../Objects/CatchHitObject.cs | 22 ---------------- .../Drawables/DrawableCatchHitObject.cs | 2 -- .../Objects/Drawables/DrawableFruit.cs | 2 ++ .../DrawablePalpableCatchHitObject.cs | 26 +++++++++---------- .../Objects/Drawables/DropletPiece.cs | 2 +- .../Objects/Drawables/FruitPiece.cs | 4 +-- .../Objects/PalpableCatchHitObject.cs | 20 +++++++++++++- .../Replays/CatchAutoGenerator.cs | 17 ++++-------- .../Skinning/LegacyFruitPiece.cs | 3 ++- osu.Game.Rulesets.Catch/UI/Catcher.cs | 6 ++--- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 12 ++++----- 14 files changed, 66 insertions(+), 78 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 89063319d6..e8ecd2ca1b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests hitObject.Scale = 1.5f; if (hyperdash) - hitObject.HyperDashTarget = new Banana(); + ((PalpableCatchHitObject)hitObject).HyperDashTarget = new Banana(); d.Anchor = Anchor.Centre; d.RelativePositionAxes = Axes.None; diff --git a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs index a08c5b6fb1..00ce9ea8c2 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapProcessor.cs @@ -5,11 +5,11 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Catch.MathUtils; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Catch.MathUtils; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Catch.Beatmaps { @@ -192,24 +192,24 @@ namespace osu.Game.Rulesets.Catch.Beatmaps private static void initialiseHyperDash(IBeatmap beatmap) { - List objectWithDroplets = new List(); + List palpableObjects = new List(); foreach (var currentObject in beatmap.HitObjects) { if (currentObject is Fruit fruitObject) - objectWithDroplets.Add(fruitObject); + palpableObjects.Add(fruitObject); if (currentObject is JuiceStream) { - foreach (var currentJuiceElement in currentObject.NestedHitObjects) + foreach (var juice in currentObject.NestedHitObjects) { - if (!(currentJuiceElement is TinyDroplet)) - objectWithDroplets.Add((CatchHitObject)currentJuiceElement); + if (juice is PalpableCatchHitObject palpableObject && !(juice is TinyDroplet)) + palpableObjects.Add(palpableObject); } } } - objectWithDroplets.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); + palpableObjects.Sort((h1, h2) => h1.StartTime.CompareTo(h2.StartTime)); double halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) / 2; @@ -221,10 +221,10 @@ namespace osu.Game.Rulesets.Catch.Beatmaps int lastDirection = 0; double lastExcess = halfCatcherWidth; - for (int i = 0; i < objectWithDroplets.Count - 1; i++) + for (int i = 0; i < palpableObjects.Count - 1; i++) { - CatchHitObject currentObject = objectWithDroplets[i]; - CatchHitObject nextObject = objectWithDroplets[i + 1]; + var currentObject = palpableObjects[i]; + var nextObject = palpableObjects[i + 1]; // Reset variables in-case values have changed (e.g. after applying HR) currentObject.HyperDashTarget = null; diff --git a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs index 3e21b8fbaf..dcd410e08f 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Preprocessing/CatchDifficultyHitObject.cs @@ -12,9 +12,9 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Preprocessing { private const float normalized_hitobject_radius = 41.0f; - public new CatchHitObject BaseObject => (CatchHitObject)base.BaseObject; + public new PalpableCatchHitObject BaseObject => (PalpableCatchHitObject)base.BaseObject; - public new CatchHitObject LastObject => (CatchHitObject)base.LastObject; + public new PalpableCatchHitObject LastObject => (PalpableCatchHitObject)base.LastObject; public readonly float NormalizedPosition; public readonly float LastNormalizedPosition; diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 21bb026bae..ccd2422381 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -27,11 +27,6 @@ namespace osu.Game.Rulesets.Catch.Objects set => x = value; } - /// - /// Whether this object can be placed on the catcher's plate. - /// - public virtual bool CanBePlated => false; - /// /// A random offset applied to , set by the . /// @@ -63,13 +58,6 @@ namespace osu.Game.Rulesets.Catch.Objects set => ComboIndexBindable.Value = value; } - /// - /// Difference between the distance to the next object - /// and the distance that would have triggered a hyper dash. - /// A value close to 0 indicates a difficult jump (for difficulty calculation). - /// - public float DistanceToHyperDash { get; set; } - public Bindable LastInComboBindable { get; } = new Bindable(); /// @@ -83,16 +71,6 @@ namespace osu.Game.Rulesets.Catch.Objects public float Scale { get; set; } = 1; - /// - /// Whether this fruit can initiate a hyperdash. - /// - public bool HyperDash => HyperDashTarget != null; - - /// - /// The target fruit if we are to initiate a hyperdash. - /// - public CatchHitObject HyperDashTarget; - protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 4cbc998447..f9f534f9ab 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -12,8 +12,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { protected override double InitialLifetimeOffset => HitObject.TimePreempt; - public virtual bool StaysOnPlate => HitObject.CanBePlated; - public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index b000a728c1..a2fa79965f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -10,6 +10,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableFruit : DrawablePalpableCatchHitObject { + public override bool StaysOnPlate => true; + public DrawableFruit(CatchHitObject h) : base(h) { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 3e843d60c1..539dbb52d1 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -12,29 +12,27 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject { - protected Container ScaleContainer { get; private set; } + public virtual bool StaysOnPlate => true; - protected DrawablePalpableCatchHitObject(CatchHitObject hitObject) - : base(hitObject) + protected readonly Container ScaleContainer; + + protected DrawablePalpableCatchHitObject(CatchHitObject h) + : base(h) { Origin = Anchor.Centre; Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); - Masking = false; + + AddInternal(ScaleContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + }); } [BackgroundDependencyLoader] private void load() { - AddRangeInternal(new Drawable[] - { - ScaleContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - } - }); - ScaleContainer.Scale = new Vector2(HitObject.Scale); } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs index c2499446fa..61e6187611 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables private void load(DrawableHitObject drawableObject) { DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; - var hitObject = drawableCatchObject.HitObject; + var hitObject = (PalpableCatchHitObject)drawableCatchObject.HitObject; InternalChild = new Pulp { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs index 4bffdab3d8..b14e37d138 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public const float RADIUS_ADJUST = 1.1f; private Circle border; - private CatchHitObject hitObject; + private PalpableCatchHitObject hitObject; public FruitPiece() { @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables private void load(DrawableHitObject drawableObject) { DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; - hitObject = drawableCatchObject.HitObject; + hitObject = (PalpableCatchHitObject)drawableCatchObject.HitObject; AddRangeInternal(new[] { diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs index 3a0cbe3ecd..5e35b9ea12 100644 --- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs @@ -5,9 +5,27 @@ namespace osu.Game.Rulesets.Catch.Objects { /// /// Represents a single object that can be caught by the catcher. + /// This includes normal fruits, droplets, and bananas but excludes objects that acts only as a container of nested hit objects. /// public abstract class PalpableCatchHitObject : CatchHitObject { - public override bool CanBePlated => true; + /// + /// Difference between the distance to the next object + /// and the distance that would have triggered a hyper dash. + /// A value close to 0 indicates a difficult jump (for difficulty calculation). + /// + public float DistanceToHyperDash { get; set; } + + /// + /// Whether this fruit can initiate a hyperdash. + /// + public bool HyperDash => HyperDashTarget != null; + + /// + /// The target fruit if we are to initiate a hyperdash. + /// + public CatchHitObject HyperDashTarget; + + public virtual bool StaysOnPlate => true; } } diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index a4f54bfe82..dfc81ee8d9 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Replays float lastPosition = CatchPlayfield.CENTER_X; double lastTime = 0; - void moveToNext(CatchHitObject h) + void moveToNext(PalpableCatchHitObject h) { float positionChange = Math.Abs(lastPosition - h.X); double timeAvailable = h.StartTime - lastTime; @@ -101,23 +101,16 @@ namespace osu.Game.Rulesets.Catch.Replays foreach (var obj in Beatmap.HitObjects) { - switch (obj) + if (obj is PalpableCatchHitObject palpableObject) { - case Fruit _: - moveToNext(obj); - break; + moveToNext(palpableObject); } foreach (var nestedObj in obj.NestedHitObjects.Cast()) { - switch (nestedObj) + if (nestedObj is PalpableCatchHitObject palpableNestedObject) { - case Banana _: - case TinyDroplet _: - case Droplet _: - case Fruit _: - moveToNext(nestedObj); - break; + moveToNext(palpableNestedObject); } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 381d066750..00ab20152a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.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.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; @@ -51,7 +52,7 @@ namespace osu.Game.Rulesets.Catch.Skinning }, }; - if (drawableCatchObject.HitObject.HyperDash) + if (((PalpableCatchHitObject)drawableCatchObject.HitObject).HyperDash) { var hyperDash = new Sprite { diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index a221ca7966..0f0b9df76e 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -220,11 +220,11 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Let the catcher attempt to catch a fruit. /// - /// The fruit to catch. + /// The fruit to catch. /// Whether the catch is possible. - public bool AttemptCatch(CatchHitObject fruit) + public bool AttemptCatch(CatchHitObject hitObject) { - if (!fruit.CanBePlated) + if (!(hitObject is PalpableCatchHitObject fruit)) return false; var halfCatchWidth = catchWidth * 0.5f; diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 5e794a76aa..70739673a9 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.UI }; } - public void OnNewResult(DrawableCatchHitObject fruit, JudgementResult result) + public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result) { if (!result.Type.IsScorable()) return; @@ -69,15 +69,15 @@ namespace osu.Game.Rulesets.Catch.UI lastPlateableFruit.OnLoadComplete += _ => action(); } - if (result.IsHit && fruit.HitObject.CanBePlated) + if (result.IsHit && hitObject.HitObject is PalpableCatchHitObject fruit) { // create a new (cloned) fruit to stay on the plate. the original is faded out immediately. - var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject); + var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit); if (caughtFruit == null) return; caughtFruit.RelativePositionAxes = Axes.None; - caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(fruit.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0); + caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(hitObject.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0); caughtFruit.IsOnPlate = true; caughtFruit.Anchor = Anchor.TopCentre; @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.UI runAfterLoaded(() => MovableCatcher.Explode(caughtFruit)); } - if (fruit.HitObject.LastInCombo) + if (hitObject.HitObject.LastInCombo) { if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result)) runAfterLoaded(() => MovableCatcher.Explode()); @@ -101,7 +101,7 @@ namespace osu.Game.Rulesets.Catch.UI MovableCatcher.Drop(); } - comboDisplay.OnNewResult(fruit, result); + comboDisplay.OnNewResult(hitObject, result); } public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) From 3c3229ac4b262ec4856d8ae0c43ed561f2b77a6a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 25 Nov 2020 07:59:45 +0900 Subject: [PATCH 4903/6909] Remove redundant `StaysOnPlate` --- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 2 -- osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs | 4 +--- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 4 ++-- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index a2fa79965f..b000a728c1 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -10,8 +10,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableFruit : DrawablePalpableCatchHitObject { - public override bool StaysOnPlate => true; - public DrawableFruit(CatchHitObject h) : base(h) { diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs index 5e35b9ea12..bdc7dcc1fe 100644 --- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.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. namespace osu.Game.Rulesets.Catch.Objects @@ -25,7 +25,5 @@ namespace osu.Game.Rulesets.Catch.Objects /// The target fruit if we are to initiate a hyperdash. /// public CatchHitObject HyperDashTarget; - - public virtual bool StaysOnPlate => true; } } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 70739673a9..ad79a23279 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -69,10 +69,10 @@ namespace osu.Game.Rulesets.Catch.UI lastPlateableFruit.OnLoadComplete += _ => action(); } - if (result.IsHit && hitObject.HitObject is PalpableCatchHitObject fruit) + if (result.IsHit && hitObject is DrawablePalpableCatchHitObject fruit) { // create a new (cloned) fruit to stay on the plate. the original is faded out immediately. - var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit); + var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject); if (caughtFruit == null) return; From 6e55eb2090c82bd8f1efce579aeebeb17eaadfe0 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 25 Nov 2020 08:00:11 +0900 Subject: [PATCH 4904/6909] Fix and add comments --- .../Objects/Drawables/DrawablePalpableCatchHitObject.cs | 3 +++ osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 539dbb52d1..c096ea2814 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -12,6 +12,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject { + /// + /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. + /// public virtual bool StaysOnPlate => true; protected readonly Container ScaleContainer; diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs index bdc7dcc1fe..361b338b2c 100644 --- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs @@ -1,11 +1,11 @@ -// 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. namespace osu.Game.Rulesets.Catch.Objects { /// /// Represents a single object that can be caught by the catcher. - /// This includes normal fruits, droplets, and bananas but excludes objects that acts only as a container of nested hit objects. + /// This includes normal fruits, droplets, and bananas but excludes objects that act only as a container of nested hit objects. /// public abstract class PalpableCatchHitObject : CatchHitObject { From 323533d94574451fb801645b3f2535c89b94ed3d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 25 Nov 2020 08:07:59 +0900 Subject: [PATCH 4905/6909] Add hiding Palpable HitObject property --- .../Objects/Drawables/DrawablePalpableCatchHitObject.cs | 2 ++ osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs | 5 ++--- osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs | 6 +++--- osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs | 5 ++--- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index c096ea2814..935aad914e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject { + public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; + /// /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. /// diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs index 61e6187611..dd0723c744 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs @@ -22,8 +22,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject) { - DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; - var hitObject = (PalpableCatchHitObject)drawableCatchObject.HitObject; + var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; InternalChild = new Pulp { @@ -31,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables AccentColour = { BindTarget = drawableObject.AccentColour } }; - if (hitObject.HyperDash) + if (drawableCatchObject.HitObject.HyperDash) { AddInternal(new Container { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs index b14e37d138..f98050ae91 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs @@ -30,12 +30,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject) { - DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; - hitObject = (PalpableCatchHitObject)drawableCatchObject.HitObject; + var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; + hitObject = drawableCatchObject.HitObject; AddRangeInternal(new[] { - getFruitFor(drawableCatchObject.HitObject.VisualRepresentation), + getFruitFor(hitObject.VisualRepresentation), border = new Circle { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 00ab20152a..1494ef3888 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -6,7 +6,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; @@ -32,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Skinning [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject, ISkinSource skin) { - DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; + var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; accentColour.BindTo(drawableCatchObject.AccentColour); @@ -52,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Skinning }, }; - if (((PalpableCatchHitObject)drawableCatchObject.HitObject).HyperDash) + if (drawableCatchObject.HitObject.HyperDash) { var hyperDash = new Sprite { From c46d655832b6b21bc4c7c706ecbcb49d0ea6bb88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Nov 2020 12:11:44 +0900 Subject: [PATCH 4906/6909] Uncomment incorrectly commented lines --- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index 4ed3b75f2e..da9bb8a09d 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -125,8 +125,8 @@ namespace osu.Game.Rulesets.Judgements prepareDrawables(); // undo any transforms applies in ApplyMissAnimations/ApplyHitAnimations to get a sane initial state. - // ApplyTransformsAt(double.MinValue, true); - // ClearTransforms(true); + ApplyTransformsAt(double.MinValue, true); + ClearTransforms(true); LifetimeStart = Result.TimeAbsolute; From d4c6d6275e4197d81d91531d9f41e26085b8df0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Nov 2020 16:46:19 +0900 Subject: [PATCH 4907/6909] Fix volume not being adjustable in the editor using alt-scroll We do this in other places so I think it's fine to handle like this for now (until we come up with a better global solution). Closes #10958. --- osu.Game/Screens/Edit/Editor.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 85467d3bbb..3d5c0ddad4 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -375,6 +375,9 @@ namespace osu.Game.Screens.Edit protected override bool OnScroll(ScrollEvent e) { + if (e.ControlPressed || e.AltPressed || e.SuperPressed) + return false; + const double precision = 1; double scrollComponent = e.ScrollDelta.X + e.ScrollDelta.Y; From 0ddeff648d2581a64613346543ce1c5dcb202f8c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Nov 2020 17:25:54 +0900 Subject: [PATCH 4908/6909] Fix incorrect index lookup on non-ordered selections --- .../Compose/Components/BlueprintContainer.cs | 23 +++++++++---------- 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index e9f5238980..def5f396f1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -187,7 +187,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (e.Button == MouseButton.Right) return false; - if (movementBlueprint != null) + if (movementBlueprints != null) { isDraggingBlueprint = true; changeHandler?.BeginChange(); @@ -299,7 +299,7 @@ namespace osu.Game.Screens.Edit.Compose.Components SelectionBlueprints.Remove(blueprint); - if (movementBlueprint == blueprint) + if (movementBlueprints?.Contains(blueprint) == true) finishSelectionMovement(); OnBlueprintRemoved(hitObject); @@ -425,7 +425,7 @@ namespace osu.Game.Screens.Edit.Compose.Components #region Selection Movement private Vector2[] movementBlueprintOriginalPositions; - private SelectionBlueprint movementBlueprint; + private SelectionBlueprint[] movementBlueprints; private bool isDraggingBlueprint; /// @@ -442,9 +442,8 @@ namespace osu.Game.Screens.Edit.Compose.Components return; // Movement is tracked from the blueprint of the earliest hitobject, since it only makes sense to distance snap from that hitobject - var orderedSelection = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime); - movementBlueprint = orderedSelection.First(); - movementBlueprintOriginalPositions = orderedSelection.Select(m => m.ScreenSpaceSelectionPoint).ToArray(); + movementBlueprints = SelectionHandler.SelectedBlueprints.OrderBy(b => b.HitObject.StartTime).ToArray(); + movementBlueprintOriginalPositions = movementBlueprints.Select(m => m.ScreenSpaceSelectionPoint).ToArray(); } /// @@ -454,7 +453,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a movement was active. private bool moveCurrentSelection(DragEvent e) { - if (movementBlueprint == null) + if (movementBlueprints == null) return false; if (snapProvider == null) @@ -474,7 +473,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (positionalResult.ScreenSpacePosition == testPosition) continue; // attempt to move the objects, and abort any time based snapping if we can. - if (SelectionHandler.HandleMovement(new MoveSelectionEvent(SelectionHandler.SelectedBlueprints.ElementAt(i), positionalResult.ScreenSpacePosition))) + if (SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints[i], positionalResult.ScreenSpacePosition))) return true; } @@ -488,13 +487,13 @@ namespace osu.Game.Screens.Edit.Compose.Components var result = snapProvider.SnapScreenSpacePositionToValidTime(movePosition); // Move the hitobjects. - if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprint, result.ScreenSpacePosition))) + if (!SelectionHandler.HandleMovement(new MoveSelectionEvent(movementBlueprints.First(), result.ScreenSpacePosition))) return true; if (result.Time.HasValue) { // Apply the start time at the newly snapped-to position - double offset = result.Time.Value - movementBlueprint.HitObject.StartTime; + double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime; foreach (HitObject obj in Beatmap.SelectedHitObjects) { @@ -512,11 +511,11 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Whether a movement was active. private bool finishSelectionMovement() { - if (movementBlueprint == null) + if (movementBlueprints == null) return false; movementBlueprintOriginalPositions = null; - movementBlueprint = null; + movementBlueprints = null; return true; } From 740b9fb3a08bdffc40a74a1b963ac8864f7d6a75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 25 Nov 2020 17:33:21 +0900 Subject: [PATCH 4909/6909] Update test to cover non-ordered selection --- .../Editor/TestSceneObjectObjectSnap.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs index d20be90001..6b532e5014 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -98,15 +98,24 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("place first object", () => InputManager.Click(MouseButton.Left)); + AddStep("increment time", () => EditorClock.SeekForward(true)); + AddStep("move mouse right", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.2f, 0))); AddStep("place second object", () => InputManager.Click(MouseButton.Left)); + AddStep("increment time", () => EditorClock.SeekForward(true)); + AddStep("move mouse down", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(0, playfield.ScreenSpaceDrawQuad.Width * 0.2f))); AddStep("place third object", () => InputManager.Click(MouseButton.Left)); AddStep("enter selection mode", () => InputManager.Key(Key.Number1)); - AddStep("select objects 2 and 3", () => EditorBeatmap.SelectedHitObjects.AddRange(EditorBeatmap.HitObjects.Skip(1))); + AddStep("select objects 2 and 3", () => + { + // add selection backwards to test non-sequential time ordering + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[2]); + EditorBeatmap.SelectedHitObjects.Add(EditorBeatmap.HitObjects[1]); + }); AddStep("begin drag", () => InputManager.PressButton(MouseButton.Left)); From c744db1b571873d05e9411c3a7fd0053fe3e2fa8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Nov 2020 17:54:03 +0900 Subject: [PATCH 4910/6909] Rewind pooled DHOs into better states --- .../Objects/Drawables/Pieces/MainCirclePiece.cs | 10 +++++----- .../Skinning/LegacyMainCirclePiece.cs | 10 +++++----- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 12 +++++++++++- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs index bf2236c945..102166f8dd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs @@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces }; } - private readonly IBindable state = new Bindable(); private readonly IBindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); @@ -50,7 +49,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { var drawableOsuObject = (DrawableOsuHitObject)drawableObject; - state.BindTo(drawableObject.State); accentColour.BindTo(drawableObject.AccentColour); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); } @@ -59,7 +57,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces { base.LoadComplete(); - state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => { explode.Colour = colour.NewValue; @@ -68,15 +65,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces }, true); indexInCurrentCombo.BindValueChanged(index => number.Text = (index.NewValue + 1).ToString(), true); + + drawableObject.ApplyCustomUpdateState += updateState; + updateState(drawableObject, drawableObject.State.Value); } - private void updateState(ValueChangedEvent state) + private void updateState(DrawableHitObject drawableObject, ArmedState state) { using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) { glow.FadeOut(400); - switch (state.NewValue) + switch (state) { case ArmedState.Hit: const double flash_in = 40; diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs index 1551d1c149..21af9a479e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs @@ -38,7 +38,6 @@ namespace osu.Game.Rulesets.Osu.Skinning private SkinnableSpriteText hitCircleText; - private readonly IBindable state = new Bindable(); private readonly Bindable accentColour = new Bindable(); private readonly IBindable indexInCurrentCombo = new Bindable(); @@ -113,7 +112,6 @@ namespace osu.Game.Rulesets.Osu.Skinning if (overlayAboveNumber) AddInternal(hitCircleOverlay.CreateProxy()); - state.BindTo(drawableObject.State); accentColour.BindTo(drawableObject.AccentColour); indexInCurrentCombo.BindTo(drawableOsuObject.IndexInCurrentComboBindable); @@ -137,19 +135,21 @@ namespace osu.Game.Rulesets.Osu.Skinning { base.LoadComplete(); - state.BindValueChanged(updateState, true); accentColour.BindValueChanged(colour => hitCircleSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); if (hasNumber) indexInCurrentCombo.BindValueChanged(index => hitCircleText.Text = (index.NewValue + 1).ToString(), true); + + drawableObject.ApplyCustomUpdateState += updateState; + updateState(drawableObject, drawableObject.State.Value); } - private void updateState(ValueChangedEvent state) + private void updateState(DrawableHitObject drawableObject, ArmedState state) { const double legacy_fade_duration = 240; using (BeginAbsoluteSequence(drawableObject.HitStateUpdateTime, true)) { - switch (state.NewValue) + switch (state) { case ArmedState.Hit: circleSprites.FadeOut(legacy_fade_duration, Easing.Out); diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 537da24e01..4c55938e49 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -259,7 +259,17 @@ namespace osu.Game.Rulesets.Objects.Drawables // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. if (IsLoaded) - Schedule(() => updateState(ArmedState.Idle, true)); + { + Scheduler.Add(() => + { + if (Result.IsHit) + updateState(ArmedState.Hit, true); + else if (Result.HasResult) + updateState(ArmedState.Miss, true); + else + updateState(ArmedState.Idle, true); + }); + } hasHitObjectApplied = true; } From 0414e5c5502effefb56416d58d26645683179805 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 25 Nov 2020 23:38:47 +0900 Subject: [PATCH 4911/6909] Add MaximumJudgementOffset to DrawableHitObject, use in more places --- .../Objects/Drawables/DrawableHoldNoteTail.cs | 2 ++ .../Objects/Drawables/DrawableOsuHitObject.cs | 8 ------- .../Objects/Drawables/DrawableSpinnerTick.cs | 12 ++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 ++ .../Objects/Drawables/DrawableDrumRollTick.cs | 2 ++ .../Objects/Drawables/DrawableHitObject.cs | 24 +++++++++++-------- 6 files changed, 32 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs index a4029e7893..3a00933e4d 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNoteTail.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mania.Objects.Drawables public void UpdateResult() => base.UpdateResult(true); + protected override double MaximumJudgementOffset => base.MaximumJudgementOffset * release_window_lenience; + protected override void CheckForResult(bool userTriggered, double timeOffset) { Debug.Assert(HitObject.HitWindows != null); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index a26db06ede..94bce53b12 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -11,7 +11,6 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI; -using osu.Game.Rulesets.Scoring; using osuTK; namespace osu.Game.Rulesets.Osu.Objects.Drawables @@ -61,13 +60,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables PositionBindable.BindTo(HitObject.PositionBindable); StackHeightBindable.BindTo(HitObject.StackHeightBindable); ScaleBindable.BindTo(HitObject.ScaleBindable); - - // Manually set to reduce the number of future alive objects to a bare minimum. - LifetimeStart = HitObject.StartTime - HitObject.TimePreempt; - - // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts. - // An extra 1000ms is added to always overestimate the true lifetime, and a more exact value is set by hit transforms and the following expiry. - LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; } protected override void OnFree(HitObject hitObject) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index fc9a7c00e6..f37d933e11 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Objects.Drawables; + namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject @@ -17,6 +19,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } + private DrawableSpinner drawableSpinner; + + protected override void OnParentReceived(DrawableHitObject parent) + { + base.OnParentReceived(parent); + drawableSpinner = (DrawableSpinner)parent; + } + + protected override double MaximumJudgementOffset => drawableSpinner.HitObject.Duration; + /// /// Apply a judgement result. /// diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 8ff752952c..243092d067 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -176,6 +176,8 @@ namespace osu.Game.Rulesets.Osu.UI public OsuHitObjectLifetimeEntry(HitObject hitObject) : base(hitObject) { + // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts. + LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; } protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index bf44a80037..be659f6ca5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -28,6 +28,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Filled = HitObject.FirstTick }); + protected override double MaximumJudgementOffset => HitObject.HitWindow; + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (!userTriggered) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 537da24e01..eeaac0d77b 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -699,6 +699,18 @@ namespace osu.Game.Rulesets.Objects.Drawables UpdateResult(false); } + /// + /// The maximum offset from the end time of at which this can be judged. + /// The time offset of will be clamped to this value during . + /// + /// Defaults to the miss window of . + /// + /// + /// + /// This does not affect the time offset provided to invocations of . + /// + protected virtual double MaximumJudgementOffset => HitObject.HitWindows?.WindowFor(HitResult.Miss) ?? 0; + /// /// Applies the of this , notifying responders such as /// the of the . @@ -738,14 +750,7 @@ namespace osu.Game.Rulesets.Objects.Drawables $"{GetType().ReadableName()} applied an invalid hit result (was: {Result.Type}, expected: [{Result.Judgement.MinResult} ... {Result.Judgement.MaxResult}])."); } - // Ensure that the judgement is given a valid time offset, because this may not get set by the caller - var endTime = HitObject.GetEndTime(); - - Result.TimeOffset = Time.Current - endTime; - - double missWindow = HitObject.HitWindows.WindowFor(HitResult.Miss); - if (missWindow > 0) - Result.TimeOffset = Math.Min(Result.TimeOffset, missWindow); + Result.TimeOffset = Math.Min(MaximumJudgementOffset, Time.Current - HitObject.GetEndTime()); if (Result.HasResult) updateState(Result.IsHit ? ArmedState.Hit : ArmedState.Miss); @@ -767,8 +772,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (Judged) return false; - var endTime = HitObject.GetEndTime(); - CheckForResult(userTriggered, Time.Current - endTime); + CheckForResult(userTriggered, Time.Current - HitObject.GetEndTime()); return Judged; } From 0817dae86c14d5b7d158cb9920278e46407118ac Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 26 Nov 2020 12:35:49 +0900 Subject: [PATCH 4912/6909] Add failing test to check non-pooled lifetime --- .../Gameplay/TestSceneDrawableScrollingRuleset.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index cebe0394c7..7425c2b7c4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -46,10 +46,11 @@ namespace osu.Game.Tests.Visual.Gameplay [SetUp] public void Setup() => Schedule(() => testClock.CurrentTime = 0); - [Test] - public void TestHitObjectPooling() + [TestCase("pooled")] + [TestCase("non-pooled")] + public void TestHitObjectLifetime(string pooled) { - var beatmap = createBeatmap(_ => new TestPooledHitObject()); + var beatmap = createBeatmap(_ => pooled == "pooled" ? new TestPooledHitObject() : new TestHitObject()); beatmap.ControlPointInfo.Add(0, new TimingControlPoint { BeatLength = time_range }); createTest(beatmap); @@ -208,13 +209,13 @@ namespace osu.Game.Tests.Visual.Gameplay /// /// Get a corresponding to the 'th . - /// When a pooling is used and the hit object is not alive, `null` is returned. + /// When the hit object is not alive, `null` is returned. /// [CanBeNull] private DrawableTestHitObject getDrawableHitObject(int index) { var hitObject = drawableRuleset.Beatmap.HitObjects.ElementAt(index); - return (DrawableTestHitObject)drawableRuleset.Playfield.HitObjectContainer.Objects.FirstOrDefault(obj => obj.HitObject == hitObject); + return (DrawableTestHitObject)drawableRuleset.Playfield.HitObjectContainer.AliveObjects.FirstOrDefault(obj => obj.HitObject == hitObject); } private float yScale => drawableRuleset.Playfield.HitObjectContainer.DrawHeight; @@ -426,6 +427,7 @@ namespace osu.Game.Tests.Visual.Gameplay } }); } + protected override void Update() => LifetimeEnd = HitObject.EndTime; } private class DrawableTestPooledHitObject : DrawableTestHitObject @@ -436,8 +438,6 @@ namespace osu.Game.Tests.Visual.Gameplay InternalChildren[0].Colour = Color4.LightSkyBlue; InternalChildren[1].Colour = Color4.Blue; } - - protected override void Update() => LifetimeEnd = HitObject.EndTime; } private class DrawableTestParentHitObject : DrawableTestHitObject From 9131546876b33e7a6cf56e18daddea2bbfca089e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 26 Nov 2020 13:04:19 +0900 Subject: [PATCH 4913/6909] Workaround TestSceneCatchModRelax failure --- .../Mods/TestSceneCatchModRelax.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs index 1eb0975010..c01aff0aa0 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -38,17 +38,17 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods new Fruit { X = 0, - StartTime = 250 + StartTime = 1000 }, new Fruit { X = CatchPlayfield.WIDTH, - StartTime = 500 + StartTime = 2000 }, new JuiceStream { X = CatchPlayfield.CENTER_X, - StartTime = 750, + StartTime = 3000, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) } } From f6faf95e339960d83aa227d36b37e3b8fee1170e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 26 Nov 2020 14:01:46 +0900 Subject: [PATCH 4914/6909] Revert changes to HitObjectUsageBegan, not use it. --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 10 ++++---- osu.Game/Rulesets/UI/Playfield.cs | 4 ++-- .../Scrolling/ScrollingHitObjectContainer.cs | 24 +++++-------------- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index fb91108605..5fbda305c8 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -48,7 +48,7 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this represents the time when the s become alive. /// - internal event Action HitObjectUsageBegan; + internal event Action HitObjectUsageBegan; /// /// Invoked when a becomes unused by a . @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.UI /// /// If this uses pooled objects, this represents the time when the s become dead. /// - internal event Action HitObjectUsageFinished; + internal event Action HitObjectUsageFinished; /// /// The amount of time prior to the current time within which s should be considered alive. @@ -115,7 +115,7 @@ namespace osu.Game.Rulesets.UI bindStartTime(drawable); AddInternal(drawableMap[entry] = drawable, false); - HitObjectUsageBegan?.Invoke(drawable); + HitObjectUsageBegan?.Invoke(entry.HitObject); } private void removeDrawable(HitObjectLifetimeEntry entry) @@ -129,10 +129,10 @@ namespace osu.Game.Rulesets.UI drawableMap.Remove(entry); - HitObjectUsageFinished?.Invoke(drawable); - unbindStartTime(drawable); RemoveInternal(drawable); + + HitObjectUsageFinished?.Invoke(entry.HitObject); } #endregion diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 411bda77b8..2f589f4ce9 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -92,8 +92,8 @@ namespace osu.Game.Rulesets.UI { h.NewResult += (d, r) => NewResult?.Invoke(d, r); h.RevertResult += (d, r) => RevertResult?.Invoke(d, r); - h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o.HitObject); - h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o.HitObject); + h.HitObjectUsageBegan += o => HitObjectUsageBegan?.Invoke(o); + h.HitObjectUsageFinished += o => HitObjectUsageFinished?.Invoke(o); })); } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index c8afe76f19..44732f490b 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -17,10 +17,6 @@ namespace osu.Game.Rulesets.UI.Scrolling private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); - // Tracks all `DrawableHitObject` (nested or not) applied a `HitObject`. - // It dynamically changes based on approximate lifetime when a pooling is used. - private readonly HashSet hitObjectApplied = new HashSet(); - // The lifetime of a hit object in this will be computed in next update. private readonly HashSet toComputeLifetime = new HashSet(); @@ -39,9 +35,6 @@ namespace osu.Game.Rulesets.UI.Scrolling RelativeSizeAxes = Axes.Both; AddLayout(layoutCache); - - HitObjectUsageBegan += onHitObjectUsageBegin; - HitObjectUsageFinished += onHitObjectUsageFinished; } [BackgroundDependencyLoader] @@ -58,7 +51,6 @@ namespace osu.Game.Rulesets.UI.Scrolling { base.Clear(disposeChildren); - hitObjectApplied.Clear(); toComputeLifetime.Clear(); layoutComputed.Clear(); } @@ -159,18 +151,10 @@ namespace osu.Game.Rulesets.UI.Scrolling { // Lifetime computation is delayed until next update because // when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed. - hitObjectApplied.Add(hitObject); toComputeLifetime.Add(hitObject); layoutComputed.Remove(hitObject); } - private void onHitObjectUsageFinished(DrawableHitObject hitObject) - { - hitObjectApplied.Remove(hitObject); - toComputeLifetime.Remove(hitObject); - layoutComputed.Remove(hitObject); - } - private float scrollLength; protected override void Update() @@ -179,8 +163,12 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!layoutCache.IsValid) { - foreach (var hitObject in hitObjectApplied) - toComputeLifetime.Add(hitObject); + toComputeLifetime.Clear(); + foreach (var hitObject in Objects) + { + if (hitObject.HitObject != null) + toComputeLifetime.Add(hitObject); + } layoutComputed.Clear(); From e43f9285889ab3f649418f7fe74fb40979c33772 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 26 Nov 2020 14:07:40 +0900 Subject: [PATCH 4915/6909] Use DHO.HitObjectApplied to invalidate computation --- .../UI/Scrolling/ScrollingHitObjectContainer.cs | 5 ++++- .../Rulesets/UI/Scrolling/ScrollingPlayfield.cs | 13 +++++++++---- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 44732f490b..6740fcf03d 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -147,7 +147,10 @@ namespace osu.Game.Rulesets.UI.Scrolling } } - private void onHitObjectUsageBegin(DrawableHitObject hitObject) + /// + /// Make this lifetime and layout computed in next update. + /// + internal void InvalidateHitObject(DrawableHitObject hitObject) { // Lifetime computation is delayed until next update because // when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed. diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 9dac3f4de1..8aba896b34 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -15,6 +15,8 @@ namespace osu.Game.Rulesets.UI.Scrolling { protected readonly IBindable Direction = new Bindable(); + public new ScrollingHitObjectContainer HitObjectContainer => (ScrollingHitObjectContainer)base.HitObjectContainer; + [Resolved] protected IScrollingInfo ScrollingInfo { get; private set; } @@ -24,17 +26,20 @@ namespace osu.Game.Rulesets.UI.Scrolling Direction.BindTo(ScrollingInfo.Direction); } + protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) + { + drawableHitObject.HitObjectApplied += d => HitObjectContainer.InvalidateHitObject(d); + } + /// /// Given a position in screen space, return the time within this column. /// - public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) => - ((ScrollingHitObjectContainer)HitObjectContainer).TimeAtScreenSpacePosition(screenSpacePosition); + public virtual double TimeAtScreenSpacePosition(Vector2 screenSpacePosition) => HitObjectContainer.TimeAtScreenSpacePosition(screenSpacePosition); /// /// Given a time, return the screen space position within this column. /// - public virtual Vector2 ScreenSpacePositionAtTime(double time) - => ((ScrollingHitObjectContainer)HitObjectContainer).ScreenSpacePositionAtTime(time); + public virtual Vector2 ScreenSpacePositionAtTime(double time) => HitObjectContainer.ScreenSpacePositionAtTime(time); protected sealed override HitObjectContainer CreateHitObjectContainer() => new ScrollingHitObjectContainer(); } From eae33fe74a56e7d8d8b8f7dce7b29172e57de8ce Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 26 Nov 2020 14:16:33 +0900 Subject: [PATCH 4916/6909] Fix format --- .../Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs | 1 + osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 7425c2b7c4..257ae10d82 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -427,6 +427,7 @@ namespace osu.Game.Tests.Visual.Gameplay } }); } + protected override void Update() => LifetimeEnd = HitObject.EndTime; } diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 6740fcf03d..02ee39e1b8 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -167,6 +167,7 @@ namespace osu.Game.Rulesets.UI.Scrolling if (!layoutCache.IsValid) { toComputeLifetime.Clear(); + foreach (var hitObject in Objects) { if (hitObject.HitObject != null) From 8a73b335f3b59bf84e2b71c7d82ba673a4e78375 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 26 Nov 2020 14:26:38 +0900 Subject: [PATCH 4917/6909] Move catch piece files --- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs | 1 + osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 1 + .../Objects/Drawables/{ => Pieces}/BananaPiece.cs | 3 +-- .../Objects/Drawables/{ => Pieces}/DropletPiece.cs | 3 +-- .../Objects/Drawables/{ => Pieces}/FruitPiece.cs | 2 +- .../Objects/Drawables/{ => Pieces}/GrapePiece.cs | 3 +-- .../Objects/Drawables/{ => Pieces}/PearPiece.cs | 3 +-- .../Objects/Drawables/{ => Pieces}/PineapplePiece.cs | 3 +-- .../Objects/Drawables/{ => Pieces}/PulpFormation.cs | 2 +- .../Objects/Drawables/{ => Pieces}/RaspberryPiece.cs | 3 +-- 10 files changed, 10 insertions(+), 14 deletions(-) rename osu.Game.Rulesets.Catch/Objects/Drawables/{ => Pieces}/BananaPiece.cs (88%) rename osu.Game.Rulesets.Catch/Objects/Drawables/{ => Pieces}/DropletPiece.cs (95%) rename osu.Game.Rulesets.Catch/Objects/Drawables/{ => Pieces}/FruitPiece.cs (98%) rename osu.Game.Rulesets.Catch/Objects/Drawables/{ => Pieces}/GrapePiece.cs (92%) rename osu.Game.Rulesets.Catch/Objects/Drawables/{ => Pieces}/PearPiece.cs (92%) rename osu.Game.Rulesets.Catch/Objects/Drawables/{ => Pieces}/PineapplePiece.cs (93%) rename osu.Game.Rulesets.Catch/Objects/Drawables/{ => Pieces}/PulpFormation.cs (96%) rename osu.Game.Rulesets.Catch/Objects/Drawables/{ => Pieces}/RaspberryPiece.cs (93%) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 37a05b5d76..74cd240aa3 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index b000a728c1..96e24bf76c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs similarity index 88% rename from osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs index ebb0bf0f2c..fa8837dec5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/BananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class BananaPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs similarity index 95% rename from osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs index dd0723c744..6de3167958 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs @@ -5,12 +5,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class DropletPiece : CompositeDrawable { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs similarity index 98% rename from osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs index f98050ae91..632c8e6f3a 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; using osuTK.Graphics; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { internal class FruitPiece : CompositeDrawable { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs similarity index 92% rename from osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs index 1d1faf893b..15349c18d5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/GrapePiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class GrapePiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs similarity index 92% rename from osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs index 7f14217cda..3372a06996 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/PearPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class PearPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs similarity index 93% rename from osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs index c328ba1837..7f80c58178 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/PineapplePiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class PineapplePiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs similarity index 96% rename from osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs index be70c3400c..1df548e70a 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/PulpFormation.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs @@ -10,7 +10,7 @@ using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public abstract class PulpFormation : CompositeDrawable { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs similarity index 93% rename from osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs index 22ce3ba5b3..288ece95b2 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/RaspberryPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs @@ -2,10 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class RaspberryPiece : PulpFormation { From cafe8cf7fad552ed741afacd751f39147ee2da51 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 26 Nov 2020 14:36:42 +0900 Subject: [PATCH 4918/6909] Refactor border of fruits to classes --- .../Objects/Drawables/Pieces/BorderPiece.cs | 31 +++++++++++++ .../Objects/Drawables/Pieces/DropletPiece.cs | 33 +------------- .../Objects/Drawables/Pieces/FruitPiece.cs | 43 ++----------------- .../Drawables/Pieces/HyperBorderPiece.cs | 22 ++++++++++ .../Pieces/HyperDropletBorderPiece.cs | 14 ++++++ 5 files changed, 71 insertions(+), 72 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs new file mode 100644 index 0000000000..1e7a0b0685 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class BorderPiece : Circle + { + public BorderPiece() + { + Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + BorderColour = Color4.White; + BorderThickness = 6f * FruitPiece.RADIUS_ADJUST; + + // Border is drawn only when there is a child drawable. + Child = new Box + { + AlwaysPresent = true, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }; + } + } +} + diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs index 6de3167958..bcef30fda8 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs @@ -4,8 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; using osuTK; @@ -31,36 +29,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces if (drawableCatchObject.HitObject.HyperDash) { - AddInternal(new Container - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Size = new Vector2(2f), - Depth = 1, - Children = new Drawable[] - { - new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - BorderThickness = 6, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0.3f, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - } - } - } - } - }); + AddInternal(new HyperDropletBorderPiece()); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs index 632c8e6f3a..208c9f8316 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs @@ -5,10 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { @@ -19,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces /// public const float RADIUS_ADJUST = 1.1f; - private Circle border; + private BorderPiece border; private PalpableCatchHitObject hitObject; public FruitPiece() @@ -36,46 +33,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces AddRangeInternal(new[] { getFruitFor(hitObject.VisualRepresentation), - border = new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BorderColour = Color4.White, - BorderThickness = 6f * RADIUS_ADJUST, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0, - RelativeSizeAxes = Axes.Both - } - } - }, + border = new BorderPiece(), }); if (hitObject.HyperDash) { - AddInternal(new Circle - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - BorderThickness = 12f * RADIUS_ADJUST, - Children = new Drawable[] - { - new Box - { - AlwaysPresent = true, - Alpha = 0.3f, - Blending = BlendingParameters.Additive, - RelativeSizeAxes = Axes.Both, - Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR, - } - } - }); + AddInternal(new HyperBorderPiece()); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs new file mode 100644 index 0000000000..60bb07e89d --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs @@ -0,0 +1,22 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.UI; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class HyperBorderPiece : BorderPiece + { + public HyperBorderPiece() + { + BorderColour = Catcher.DEFAULT_HYPER_DASH_COLOUR; + BorderThickness = 12f * FruitPiece.RADIUS_ADJUST; + + Child.Alpha = 0.3f; + Child.Blending = BlendingParameters.Additive; + Child.Colour = Catcher.DEFAULT_HYPER_DASH_COLOUR; + } + } +} + diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs new file mode 100644 index 0000000000..1bd9fd6bb2 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class HyperDropletBorderPiece : HyperBorderPiece + { + public HyperDropletBorderPiece() + { + Size /= 2; + BorderThickness = 6f; + } + } +} From f562854feb72d51a4b198db2b69e5ea5132c6131 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 26 Nov 2020 17:22:22 +0900 Subject: [PATCH 4919/6909] Fix timeline objects sometimes not receiving combo colours --- .../Timeline/TimelineHitObjectBlueprint.cs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 975433d407..d534fb3e13 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -55,6 +55,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline private const float circle_size = 24; + [Resolved(CanBeNull = true)] + private HitObjectComposer composer { get; set; } + public TimelineHitObjectBlueprint(HitObject hitObject) : base(hitObject) { @@ -152,19 +155,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline updateShadows(); } - [BackgroundDependencyLoader(true)] - private void load(HitObjectComposer composer) + protected override void LoadComplete() { + base.LoadComplete(); + if (composer != null) { // best effort to get the drawable representation for grabbing colour and what not. drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject); } - } - - protected override void LoadComplete() - { - base.LoadComplete(); if (HitObject is IHasComboInformation comboInfo) { From f3f5ec766535b8ba201b34ed343bda7e5b8012b3 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 26 Nov 2020 18:08:40 +0900 Subject: [PATCH 4920/6909] Fix `Column` not calling `base.Add` --- osu.Game.Rulesets.Mania/UI/Column.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/UI/Column.cs b/osu.Game.Rulesets.Mania/UI/Column.cs index 9aabcc6699..d2a9b69b60 100644 --- a/osu.Game.Rulesets.Mania/UI/Column.cs +++ b/osu.Game.Rulesets.Mania/UI/Column.cs @@ -97,7 +97,7 @@ namespace osu.Game.Rulesets.Mania.UI DrawableManiaHitObject maniaObject = (DrawableManiaHitObject)hitObject; maniaObject.CheckHittable = hitPolicy.IsHittable; - HitObjectContainer.Add(hitObject); + base.Add(hitObject); } public override bool Remove(DrawableHitObject h) From e53f849aa056ac40f5dc9d745a0ad1bc94541e87 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 26 Nov 2020 18:14:25 +0900 Subject: [PATCH 4921/6909] Completely separate combo colours from DHOs --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 28 ++++++- .../Objects/Drawables/DrawableBanana.cs | 25 ------ .../DrawablePalpableCatchHitObject.cs | 5 -- .../Objects/PalpableCatchHitObject.cs | 8 +- .../Objects/Drawables/DrawableHitObject.cs | 8 +- .../Objects/Types/IHasComboInformation.cs | 11 +++ .../Timeline/TimelineHitObjectBlueprint.cs | 84 ++++++++----------- .../Screens/Edit/Compose/ComposeScreen.cs | 20 ++++- 8 files changed, 103 insertions(+), 86 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 4ecfb7b16d..d1033f7801 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -2,13 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects.Types; +using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects { - public class Banana : Fruit + public class Banana : Fruit, IHasComboInformation { /// /// Index of banana in current shower. @@ -26,6 +29,29 @@ namespace osu.Game.Rulesets.Catch.Objects Samples = samples; } + private Color4? colour; + + Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) + { + // override any external colour changes with banananana + return colour ??= getBananaColour(); + } + + private Color4 getBananaColour() + { + switch (RNG.Next(0, 3)) + { + default: + return new Color4(255, 240, 0, 255); + + case 1: + return new Color4(255, 192, 0, 255); + + case 2: + return new Color4(214, 221, 28, 255); + } + } + private class BananaHitSampleInfo : HitSampleInfo { private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index a865984d45..7748b1c565 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.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.Collections.Generic; using osu.Framework.Graphics; using osu.Framework.Utils; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -15,14 +13,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { } - private Color4? colour; - - protected override Color4 GetComboColour(IReadOnlyList comboColours) - { - // override any external colour changes with banananana - return colour ??= getBananaColour(); - } - protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); @@ -46,20 +36,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables if (Samples != null) Samples.Frequency.Value = 0.77f + ((Banana)HitObject).BananaIndex * 0.006f; } - - private Color4 getBananaColour() - { - switch (RNG.Next(0, 3)) - { - default: - return new Color4(255, 240, 0, 255); - - case 1: - return new Color4(255, 192, 0, 255); - - case 2: - return new Color4(214, 221, 28, 255); - } - } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 935aad914e..9339a1c420 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -1,12 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -40,8 +38,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { ScaleContainer.Scale = new Vector2(HitObject.Scale); } - - protected override Color4 GetComboColour(IReadOnlyList comboColours) => - comboColours[(HitObject.IndexInBeatmap + 1) % comboColours.Count]; } } diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs index 361b338b2c..995f61c386 100644 --- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs @@ -1,13 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using osu.Game.Rulesets.Objects.Types; +using osuTK.Graphics; + namespace osu.Game.Rulesets.Catch.Objects { /// /// Represents a single object that can be caught by the catcher. /// This includes normal fruits, droplets, and bananas but excludes objects that act only as a container of nested hit objects. /// - public abstract class PalpableCatchHitObject : CatchHitObject + public abstract class PalpableCatchHitObject : CatchHitObject, IHasComboInformation { /// /// Difference between the distance to the next object @@ -25,5 +29,7 @@ namespace osu.Game.Rulesets.Catch.Objects /// The target fruit if we are to initiate a hyperdash. /// public CatchHitObject HyperDashTarget; + + Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count]; } } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 537da24e01..571b05cc05 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -529,11 +529,10 @@ namespace osu.Game.Rulesets.Objects.Drawables private void updateComboColour() { - if (!(HitObject is IHasComboInformation)) return; + if (!(HitObject is IHasComboInformation combo)) return; - var comboColours = CurrentSkin.GetConfig>(GlobalSkinColours.ComboColours)?.Value; - - AccentColour.Value = GetComboColour(comboColours); + var comboColours = CurrentSkin.GetConfig>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty(); + AccentColour.Value = combo.GetComboColour(comboColours); } /// @@ -544,6 +543,7 @@ namespace osu.Game.Rulesets.Objects.Drawables /// This will only be called if the implements . /// /// A list of combo colours provided by the beatmap or skin. Can be null if not available. + [Obsolete("Unused. Implement IHasComboInformation and IHasComboInformation.GetComboColour() on the HitObject model instead.")] // Can be removed 20210527 protected virtual Color4 GetComboColour(IReadOnlyList comboColours) { if (!(HitObject is IHasComboInformation combo)) diff --git a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs index 211c077d4f..4f66802079 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasComboInformation.cs @@ -1,7 +1,10 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Bindables; +using osuTK.Graphics; namespace osu.Game.Rulesets.Objects.Types { @@ -35,5 +38,13 @@ namespace osu.Game.Rulesets.Objects.Types /// Whether this is the last object in the current combo. /// bool LastInCombo { get; set; } + + /// + /// Retrieves the colour of the combo described by this object from a set of possible combo colours. + /// Defaults to using to decide the colour. + /// + /// A list of possible combo colours provided by the beatmap or skin. + /// The colour of the combo described by this object. + Color4 GetComboColour([NotNull] IReadOnlyList comboColours) => comboColours.Count > 0 ? comboColours[ComboIndex % comboColours.Count] : Color4.White; } } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index d534fb3e13..657c5834b2 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -19,8 +18,8 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; 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.Skinning; using osuTK; using osuTK.Graphics; @@ -28,35 +27,26 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { public class TimelineHitObjectBlueprint : SelectionBlueprint { - private readonly Circle circle; + private const float thickness = 5; + private const float shadow_radius = 5; + private const float circle_size = 24; + + public Action OnDragHandled; [UsedImplicitly] private readonly Bindable startTime; - public Action OnDragHandled; + private Bindable indexInCurrentComboBindable; + private Bindable comboIndexBindable; + private readonly Circle circle; private readonly DragBar dragBar; - private readonly List shadowComponents = new List(); - - private DrawableHitObject drawableHitObject; - - private Bindable comboColour; - private readonly Container mainComponents; - private readonly OsuSpriteText comboIndexText; - private Bindable comboIndex; - - private const float thickness = 5; - - private const float shadow_radius = 5; - - private const float circle_size = 24; - - [Resolved(CanBeNull = true)] - private HitObjectComposer composer { get; set; } + [Resolved] + private ISkinSource skin { get; set; } public TimelineHitObjectBlueprint(HitObject hitObject) : base(hitObject) @@ -159,38 +149,38 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { base.LoadComplete(); - if (composer != null) - { - // best effort to get the drawable representation for grabbing colour and what not. - drawableHitObject = composer.HitObjects.FirstOrDefault(d => d.HitObject == HitObject); - } - if (HitObject is IHasComboInformation comboInfo) { - comboIndex = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); - comboIndex.BindValueChanged(combo => - { - comboIndexText.Text = (combo.NewValue + 1).ToString(); - }, true); + indexInCurrentComboBindable = comboInfo.IndexInCurrentComboBindable.GetBoundCopy(); + indexInCurrentComboBindable.BindValueChanged(_ => updateComboIndex(), true); + + comboIndexBindable = comboInfo.ComboIndexBindable.GetBoundCopy(); + comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); + + skin.SourceChanged += updateComboColour; } + } - if (drawableHitObject != null) - { - comboColour = drawableHitObject.AccentColour.GetBoundCopy(); - comboColour.BindValueChanged(colour => - { - if (HitObject is IHasDuration) - mainComponents.Colour = ColourInfo.GradientHorizontal(drawableHitObject.AccentColour.Value, Color4.White); - else - mainComponents.Colour = drawableHitObject.AccentColour.Value; + private void updateComboIndex() => comboIndexText.Text = (indexInCurrentComboBindable.Value + 1).ToString(); - var col = mainComponents.Colour.TopLeft.Linear; - float brightness = col.R + col.G + col.B; + private void updateComboColour() + { + if (!(HitObject is IHasComboInformation combo)) + return; - // decide the combo index colour based on brightness? - comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White; - }, true); - } + var comboColours = skin.GetConfig>(GlobalSkinColours.ComboColours)?.Value ?? Array.Empty(); + var comboColour = combo.GetComboColour(comboColours); + + if (HitObject is IHasDuration) + mainComponents.Colour = ColourInfo.GradientHorizontal(comboColour, Color4.White); + else + mainComponents.Colour = comboColour; + + var col = mainComponents.Colour.TopLeft.Linear; + float brightness = col.R + col.G + col.B; + + // decide the combo index colour based on brightness? + comboIndexText.Colour = brightness > 0.5f ? Color4.Black : Color4.White; } protected override void Update() diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index 46d5eb40b4..c297a03dbf 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.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 osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -43,6 +44,21 @@ namespace osu.Game.Screens.Edit.Compose if (ruleset == null || composer == null) return new ScreenWhiteBox.UnderConstructionMessage(ruleset == null ? "This beatmap" : $"{ruleset.Description}'s composer"); + return wrapSkinnableContent(composer); + } + + protected override Drawable CreateTimelineContent() + { + if (ruleset == null || composer == null) + return base.CreateTimelineContent(); + + return wrapSkinnableContent(new TimelineBlueprintContainer(composer)); + } + + private Drawable wrapSkinnableContent(Drawable content) + { + Debug.Assert(ruleset != null); + var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation @@ -51,9 +67,7 @@ namespace osu.Game.Screens.Edit.Compose // 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. - return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(composer)); + return beatmapSkinProvider.WithChild(rulesetSkinProvider.WithChild(content)); } - - protected override Drawable CreateTimelineContent() => composer == null ? base.CreateTimelineContent() : new TimelineBlueprintContainer(composer); } } From 02d5b1352b6c9971ea83a131c0d2637e167b56b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 18:25:56 +0900 Subject: [PATCH 4922/6909] Expose generic version of OsuScrollContainer --- osu.Game/Graphics/Containers/OsuScrollContainer.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/Containers/OsuScrollContainer.cs b/osu.Game/Graphics/Containers/OsuScrollContainer.cs index b9122d254d..aaad72f65c 100644 --- a/osu.Game/Graphics/Containers/OsuScrollContainer.cs +++ b/osu.Game/Graphics/Containers/OsuScrollContainer.cs @@ -12,7 +12,19 @@ using osuTK.Input; namespace osu.Game.Graphics.Containers { - public class OsuScrollContainer : ScrollContainer + public class OsuScrollContainer : OsuScrollContainer + { + public OsuScrollContainer() + { + } + + public OsuScrollContainer(Direction direction) + : base(direction) + { + } + } + + public class OsuScrollContainer : ScrollContainer where T : Drawable { public const float SCROLL_BAR_HEIGHT = 10; public const float SCROLL_BAR_PADDING = 3; From f8db7a990283b813278f69a53d3de5863db29eb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 18:28:52 +0900 Subject: [PATCH 4923/6909] Remove ScrollableContent container from carousel This was causing multiple issues with masking and sizing and really didn't need to exist in the first place. Also not sure why the pool was nested inside the scroll container, but it isn't any more. Probably for the best. --- .../SongSelect/TestSceneBeatmapCarousel.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 48 +++++++++---------- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs index 4699784327..44c9361ff8 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapCarousel.cs @@ -917,7 +917,7 @@ namespace osu.Game.Tests.Visual.SongSelect { get { - foreach (var item in ScrollableContent) + foreach (var item in Scroll.Children) { yield return item; diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 83631fd383..164802fc28 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Select /// public bool BeatmapSetsLoaded { get; private set; } - private readonly CarouselScrollContainer scroll; + protected readonly CarouselScrollContainer Scroll; private IEnumerable beatmapSets => root.Children.OfType(); @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Select if (selectedBeatmapSet != null && !beatmapSets.Contains(selectedBeatmapSet.BeatmapSet)) selectedBeatmapSet = null; - ScrollableContent.Clear(false); + Scroll.Clear(false); itemsCache.Invalidate(); scrollPositionCache.Invalidate(); @@ -132,8 +132,6 @@ namespace osu.Game.Screens.Select private readonly Cached itemsCache = new Cached(); private readonly Cached scrollPositionCache = new Cached(); - protected readonly Container ScrollableContent; - public Bindable RightClickScrollingEnabled = new Bindable(); public Bindable RandomAlgorithm = new Bindable(); @@ -155,17 +153,12 @@ namespace osu.Game.Screens.Select InternalChild = new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, - Child = scroll = new CarouselScrollContainer + Children = new Drawable[] { - Masking = false, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + setPool, + Scroll = new CarouselScrollContainer { - setPool, - ScrollableContent = new Container - { - RelativeSizeAxes = Axes.X, - } + RelativeSizeAxes = Axes.Both, } } }; @@ -180,7 +173,7 @@ namespace osu.Game.Screens.Select config.BindWith(OsuSetting.RandomSelectAlgorithm, RandomAlgorithm); config.BindWith(OsuSetting.SongSelectRightMouseScroll, RightClickScrollingEnabled); - RightClickScrollingEnabled.ValueChanged += enabled => scroll.RightMouseScrollbar = enabled.NewValue; + RightClickScrollingEnabled.ValueChanged += enabled => Scroll.RightMouseScrollbar = enabled.NewValue; RightClickScrollingEnabled.TriggerChange(); itemUpdated = beatmaps.ItemUpdated.GetBoundCopy(); @@ -421,12 +414,12 @@ namespace osu.Game.Screens.Select /// /// The position of the lower visible bound with respect to the current scroll position. /// - private float visibleBottomBound => scroll.Current + DrawHeight + BleedBottom; + 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 - BleedTop; + private float visibleUpperBound => Scroll.Current - BleedTop; public void FlushPendingFilterOperations() { @@ -468,7 +461,7 @@ namespace osu.Game.Screens.Select root.Filter(activeCriteria); itemsCache.Invalidate(); - if (alwaysResetScrollPosition || !scroll.UserScrolling) + if (alwaysResetScrollPosition || !Scroll.UserScrolling) ScrollToSelected(); } } @@ -594,7 +587,7 @@ namespace osu.Game.Screens.Select { var toDisplay = visibleItems.GetRange(displayedRange.first, displayedRange.last - displayedRange.first + 1); - foreach (var panel in ScrollableContent.Children) + foreach (var panel in Scroll.Children) { if (toDisplay.Remove(panel.Item)) { @@ -620,7 +613,7 @@ namespace osu.Game.Screens.Select panel.Depth = item.CarouselYPosition; panel.Y = item.CarouselYPosition; - ScrollableContent.Add(panel); + Scroll.Add(panel); } } } @@ -637,7 +630,7 @@ namespace osu.Game.Screens.Select // Update externally controlled state of currently visible items (e.g. x-offset and opacity). // This is a per-frame update on all drawable panels. - foreach (DrawableCarouselItem item in ScrollableContent.Children) + foreach (DrawableCarouselItem item in Scroll.Children) { updateItem(item); @@ -789,7 +782,8 @@ namespace osu.Game.Screens.Select } currentY += visibleHalfHeight; - ScrollableContent.Height = currentY; + + Scroll.ScrollContent.Height = currentY; if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) { @@ -809,7 +803,7 @@ namespace osu.Game.Screens.Select if (firstScroll) { // reduce movement when first displaying the carousel. - scroll.ScrollTo(scrollTarget.Value - 200, false); + Scroll.ScrollTo(scrollTarget.Value - 200, false); firstScroll = false; } @@ -844,7 +838,7 @@ namespace osu.Game.Screens.Select /// For nested items, the parent of the item to be updated. private void updateItem(DrawableCarouselItem item, DrawableCarouselItem parent = null) { - Vector2 posInScroll = ScrollableContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); + Vector2 posInScroll = Scroll.ScrollContent.ToLocalSpace(item.Header.ScreenSpaceDrawQuad.Centre); float itemDrawY = posInScroll.Y - visibleUpperBound; float dist = Math.Abs(1f - itemDrawY / visibleHalfHeight); @@ -889,7 +883,7 @@ namespace osu.Game.Screens.Select } } - private class CarouselScrollContainer : OsuScrollContainer + protected class CarouselScrollContainer : OsuScrollContainer { private bool rightMouseScrollBlocked; @@ -898,6 +892,12 @@ namespace osu.Game.Screens.Select /// public bool UserScrolling { get; private set; } + public CarouselScrollContainer() + { + // size is determined by the carousel itself, due to not all content necessarily being loaded. + ScrollContent.AutoSizeAxes = Axes.None; + } + // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { From 6058c66edb0a0e2ce4150bf8fc530f36b708e9ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 18:32:43 +0900 Subject: [PATCH 4924/6909] Move drawable carousel set movement logic into panels themselves --- osu.Game/Screens/Select/BeatmapCarousel.cs | 10 ---------- .../Carousel/DrawableCarouselBeatmapSet.cs | 20 +++++++++++++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 164802fc28..b6ed0468dd 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -618,16 +618,6 @@ namespace osu.Game.Screens.Select } } - // Finally, if the filtered items have changed, animate drawables to their new locations. - // This is common if a selected/collapsed state has changed. - if (revalidateItems) - { - foreach (DrawableCarouselItem panel in ScrollableContent.Children) - { - panel.MoveToY(panel.Item.CarouselYPosition, 800, Easing.OutQuint); - } - } - // Update externally controlled state of currently visible items (e.g. x-offset and opacity). // This is a per-frame update on all drawable panels. foreach (DrawableCarouselItem item in Scroll.Children) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 93f95e76cc..e25c6932cf 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Graphics.UserInterface; @@ -60,6 +61,25 @@ namespace osu.Game.Screens.Select.Carousel viewDetails = beatmapOverlay.FetchAndShowBeatmapSet; } + protected override void Update() + { + base.Update(); + + // position updates should not occur if the item is filtered away. + // this avoids panels flying across the screen only to be eventually off-screen or faded out. + if (!Item.Visible) + return; + + float targetY = Item.CarouselYPosition; + + if (Precision.AlmostEquals(targetY, Y)) + Y = targetY; + else + // algorithm for this is taken from ScrollContainer. + // while it doesn't necessarily need to match 1:1, as we are emulating scroll in some cases this feels most correct. + Y = (float)Interpolation.Lerp(targetY, Y, Math.Exp(-0.01 * Time.Elapsed)); + } + protected override void UpdateItem() { base.UpdateItem(); From ad258e2e52ac387dd50b35f93022fd62217e6f30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 18:33:37 +0900 Subject: [PATCH 4925/6909] Update scroll position before applying any panel animations In the case of automatic scroll requirements (ie. scroll to selected) we are delegating the animation logic to the panels themselves. In order to make this work correctly, the scroll operation needs to take effect before any animation updates are run. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index b6ed0468dd..3eddba0532 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -573,6 +573,9 @@ namespace osu.Game.Screens.Select if (revalidateItems) updateYPositions(); + if (!scrollPositionCache.IsValid) + updateScrollPosition(); + // This data is consumed to find the currently displayable range. // This is the range we want to keep drawables for, and should exceed the visible range slightly to avoid drawable churn. var newDisplayRange = getDisplayRange(); @@ -653,14 +656,6 @@ namespace osu.Game.Screens.Select return (firstIndex, lastIndex); } - protected override void UpdateAfterChildren() - { - base.UpdateAfterChildren(); - - if (!scrollPositionCache.IsValid) - updateScrollPosition(); - } - private void beatmapRemoved(ValueChangedEvent> weakItem) { if (weakItem.NewValue.TryGetTarget(out var item)) From 0a48dd8f764bb56cba9d53c907fc83a174f7e1b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 18:42:51 +0900 Subject: [PATCH 4926/6909] Delegate scroll animation to panels themselves --- osu.Game/Screens/Select/BeatmapCarousel.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 3eddba0532..2012d47fd3 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -573,6 +573,9 @@ namespace osu.Game.Screens.Select if (revalidateItems) updateYPositions(); + // if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels. + // due to this, scroll application needs to be run immediately after y position updates. + // if this isn't the case, the on-screen pooling / display logic following will fail briefly. if (!scrollPositionCache.IsValid) updateScrollPosition(); @@ -792,8 +795,17 @@ namespace osu.Game.Screens.Select firstScroll = false; } - scroll.ScrollTo(scrollTarget.Value); + // in order to simplify animation logic, rather than using the animated version of ScrollTo, + // we take the difference in scroll height and apply to all visible panels. + // this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer + // to enter clamp-special-case mode where it animates completely differently to normal. + float scrollChange = scrollTarget.Value - Scroll.Current; + + Scroll.ScrollTo(scrollTarget.Value, false); scrollPositionCache.Validate(); + + foreach (var i in Scroll.Children) + i.Y += scrollChange; } } From 05e245d4454987b27fdca0200b798eb131c73f26 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 26 Nov 2020 19:07:09 +0900 Subject: [PATCH 4927/6909] Allow non-pooled DHO to be reused --- osu.Game/Rulesets/UI/Playfield.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 2f589f4ce9..a2ac234471 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -135,10 +135,8 @@ namespace osu.Game.Rulesets.UI /// The DrawableHitObject to add. public virtual void Add(DrawableHitObject h) { - if (h.IsInitialized) - throw new InvalidOperationException($"{nameof(Add)} doesn't support {nameof(DrawableHitObject)} reuse. Use pooling instead."); - - onNewDrawableHitObject(h); + if (!h.IsInitialized) + onNewDrawableHitObject(h); HitObjectContainer.Add(h); OnHitObjectAdded(h.HitObject); From 9811c46e3573e8299fed6a326422704fd3aa3e53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 19:16:18 +0900 Subject: [PATCH 4928/6909] Rename application method to better describe what it actually does --- .../Edit/Blueprints/HoldNotePlacementBlueprint.cs | 4 ++-- .../Edit/Blueprints/ManiaPlacementBlueprint.cs | 4 ++-- .../Edit/Blueprints/NotePlacementBlueprint.cs | 4 ++-- .../Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs | 4 ++-- .../Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs | 4 ++-- .../Edit/Blueprints/HitPlacementBlueprint.cs | 4 ++-- .../Edit/Blueprints/TaikoSpanPlacementBlueprint.cs | 4 ++-- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 +- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 2 +- osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs | 4 ++-- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/HoldNotePlacementBlueprint.cs index b5ec1e1a2a..1f92929392 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 private double originalStartTime; - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); if (PlacementActive) { diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs index 27a279e044..5e09054667 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/ManiaPlacementBlueprint.cs @@ -48,9 +48,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints return true; } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); if (!PlacementActive) 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 684004b558..3db89c8ae6 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/NotePlacementBlueprint.cs @@ -22,9 +22,9 @@ namespace osu.Game.Rulesets.Mania.Edit.Blueprints InternalChild = piece = new EditNotePiece { Origin = Anchor.Centre }; } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); if (result.Playfield != null) { diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs index e14d6647d2..c45a04053f 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCirclePlacementBlueprint.cs @@ -45,9 +45,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles return base.OnMouseDown(e); } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(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 4b99cc23ed..b71e1914f7 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderPlacementBlueprint.cs @@ -67,9 +67,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders inputManager = GetContainingInputManager(); } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); switch (state) { diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs index c5191ab241..17e7fb81f6 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/HitPlacementBlueprint.cs @@ -43,10 +43,10 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints return false; } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { piece.Position = ToLocalSpace(result.ScreenSpacePosition); - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs index 468d980b23..e53b331f46 100644 --- a/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs +++ b/osu.Game.Rulesets.Taiko/Edit/Blueprints/TaikoSpanPlacementBlueprint.cs @@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Taiko.Edit.Blueprints EndPlacement(true); } - public override void UpdatePosition(SnapResult result) + public override void UpdateTimeAndPosition(SnapResult result) { - base.UpdatePosition(result); + base.UpdateTimeAndPosition(result); if (PlacementActive) { diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index d986b71380..9d7dc7b8f7 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -88,7 +88,7 @@ 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 result) + public virtual void UpdateTimeAndPosition(SnapResult result) { if (!PlacementActive) HitObject.StartTime = result.Time ?? EditorClock?.CurrentTime ?? Time.Current; diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 0d2e2360b1..dddfd763d5 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -157,7 +157,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); - currentPlacement.UpdatePosition(snapResult); + currentPlacement.UpdateTimeAndPosition(snapResult); } #endregion diff --git a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs index c3d74f21aa..78a6bcc3db 100644 --- a/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs +++ b/osu.Game/Tests/Visual/PlacementBlueprintTestScene.cs @@ -72,7 +72,7 @@ namespace osu.Game.Tests.Visual { base.Update(); - currentBlueprint.UpdatePosition(SnapForBlueprint(currentBlueprint)); + currentBlueprint.UpdateTimeAndPosition(SnapForBlueprint(currentBlueprint)); } protected virtual SnapResult SnapForBlueprint(PlacementBlueprint blueprint) => @@ -85,7 +85,7 @@ namespace osu.Game.Tests.Visual if (drawable is PlacementBlueprint blueprint) { blueprint.Show(); - blueprint.UpdatePosition(SnapForBlueprint(blueprint)); + blueprint.UpdateTimeAndPosition(SnapForBlueprint(blueprint)); } } From 91592cf32d387d332610a03f46175105ac78ccd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 19:20:39 +0900 Subject: [PATCH 4929/6909] Expose EditorClock for consumption --- .../Screens/Edit/Compose/Components/BlueprintContainer.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index def5f396f1..5c5f203667 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -41,7 +41,7 @@ namespace osu.Game.Screens.Edit.Compose.Components private IEditorChangeHandler changeHandler { get; set; } [Resolved] - private EditorClock editorClock { get; set; } + protected EditorClock EditorClock { get; private set; } [Resolved] protected EditorBeatmap Beatmap { get; private set; } @@ -170,7 +170,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint) return false; - editorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); + EditorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); return true; } @@ -381,7 +381,7 @@ namespace osu.Game.Screens.Edit.Compose.Components 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()) + if (!EditorClock.IsRunning && !isValidForSelection()) blueprint.Deselect(); break; } From da6bccc8125725a06bc7d82fb1d34ae0886e0456 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 19:20:51 +0900 Subject: [PATCH 4930/6909] Apply beat snap if positional snap doesn't give a time result --- .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index dddfd763d5..1893366d90 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -157,6 +157,9 @@ namespace osu.Game.Screens.Edit.Compose.Components { var snapResult = Composer.SnapScreenSpacePositionToValidTime(inputManager.CurrentState.Mouse.Position); + // if no time was found from positional snapping, we should still quantize to the beat. + snapResult.Time ??= Beatmap.SnapTime(EditorClock.CurrentTime, null); + currentPlacement.UpdateTimeAndPosition(snapResult); } From ab1ad99c88f556afb8c1daeb2ffa849c2597e451 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 19:33:51 +0900 Subject: [PATCH 4931/6909] Fix failing test scene (was previously not snapped properly) --- .../Editor/TestSceneObjectObjectSnap.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs index 6b532e5014..7bdf131e0d 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectObjectSnap.cs @@ -25,6 +25,7 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor { base.SetUpSteps(); AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First()); + AddStep("seek to first control point", () => EditorClock.Seek(Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time)); } [TestCase(true)] @@ -66,13 +67,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor AddStep("start slider placement", () => InputManager.Click(MouseButton.Left)); - AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.185f, 0))); + AddStep("move to place end", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.225f, 0))); AddStep("end slider placement", () => InputManager.Click(MouseButton.Right)); AddStep("enter circle placement mode", () => InputManager.Key(Key.Number2)); - AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.20f, 0))); + AddStep("move mouse slightly", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre + new Vector2(playfield.ScreenSpaceDrawQuad.Width * 0.235f, 0))); AddStep("place second object", () => InputManager.Click(MouseButton.Left)); From 9a08cc8c04d8a45747736bcea22db930e53e3096 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 19:40:10 +0900 Subject: [PATCH 4932/6909] Add test coverage of beat snapping hit circles --- .../Editor/TestSceneBeatSnap.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Editor/TestSceneBeatSnap.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneBeatSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneBeatSnap.cs new file mode 100644 index 0000000000..a652fb32f4 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneBeatSnap.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.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Tests.Beatmaps; +using osuTK.Input; + +namespace osu.Game.Rulesets.Osu.Tests.Editor +{ + [TestFixture] + public class TestSceneObjectBeatSnap : TestSceneOsuEditor + { + private OsuPlayfield playfield; + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TestBeatmap(Ruleset.Value, false); + + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("get playfield", () => playfield = Editor.ChildrenOfType().First()); + } + + [Test] + public void TestBeatSnapHitCircle() + { + double firstTimingPointTime() => Beatmap.Value.Beatmap.ControlPointInfo.TimingPoints.First().Time; + + AddStep("seek some milliseconds forward", () => EditorClock.Seek(firstTimingPointTime() + 10)); + + AddStep("move mouse to centre", () => InputManager.MoveMouseTo(playfield.ScreenSpaceDrawQuad.Centre)); + AddStep("enter placement mode", () => InputManager.Key(Key.Number2)); + AddStep("place first object", () => InputManager.Click(MouseButton.Left)); + + AddAssert("ensure object snapped back to correct time", () => EditorBeatmap.HitObjects.First().StartTime == firstTimingPointTime()); + } + } +} From 203c36f7202f4fb869c548fc1d71b5d89d4e4fbc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 19:46:54 +0900 Subject: [PATCH 4933/6909] Rename file to match test name --- .../Editor/{TestSceneBeatSnap.cs => TestSceneObjectBeatSnap.cs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Osu.Tests/Editor/{TestSceneBeatSnap.cs => TestSceneObjectBeatSnap.cs} (100%) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneBeatSnap.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectBeatSnap.cs similarity index 100% rename from osu.Game.Rulesets.Osu.Tests/Editor/TestSceneBeatSnap.cs rename to osu.Game.Rulesets.Osu.Tests/Editor/TestSceneObjectBeatSnap.cs From 3346c06aca2469288f1aa2a4ed48fc7595c97122 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 20:04:44 +0900 Subject: [PATCH 4934/6909] Rename variable/text to be more verbose as to toggle purpose --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 6 +++--- osu.Game/Screens/Play/ReplayPlayer.cs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 77f57bd637..2736c20da9 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -69,7 +69,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), - new KeyBinding(InputKey.Space, GlobalAction.PauseReplay), + new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), }; @@ -198,7 +198,7 @@ namespace osu.Game.Input.Bindings [Description("Random Skin")] RandomSkin, - [Description("Pause Replay")] - PauseReplay, + [Description("Pause / resume replay")] + TogglePauseReplay, } } diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 2c0b766a17..294d116f51 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Play { switch (action) { - case GlobalAction.PauseReplay: + case GlobalAction.TogglePauseReplay: if (GameplayClockContainer.IsPaused.Value) GameplayClockContainer.Start(); else From 1e79cb498b8454d2a6349bae36d7ff0fe788f484 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 26 Nov 2020 20:07:16 +0900 Subject: [PATCH 4935/6909] Standardise binding description case to sentence casing --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index e5d3a89a88..9af1b49ef9 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -163,10 +163,10 @@ namespace osu.Game.Input.Bindings [Description("Toggle now playing overlay")] ToggleNowPlaying, - [Description("Previous Selection")] + [Description("Previous selection")] SelectPrevious, - [Description("Next Selection")] + [Description("Next selection")] SelectNext, [Description("Home")] @@ -175,26 +175,26 @@ namespace osu.Game.Input.Bindings [Description("Toggle notifications")] ToggleNotifications, - [Description("Pause")] + [Description("Pause gameplay")] PauseGameplay, // Editor - [Description("Setup Mode")] + [Description("Setup mode")] EditorSetupMode, - [Description("Compose Mode")] + [Description("Compose mode")] EditorComposeMode, - [Description("Design Mode")] + [Description("Design mode")] EditorDesignMode, - [Description("Timing Mode")] + [Description("Timing mode")] EditorTimingMode, [Description("Hold for HUD")] HoldForHUD, - [Description("Random Skin")] + [Description("Random skin")] RandomSkin, } } From aa4da2a5f82af9141e1efeee7a547313822165c0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 26 Nov 2020 23:42:05 +0900 Subject: [PATCH 4936/6909] Add xmldoc on State --- 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 4c55938e49..1427453c5f 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -128,6 +128,12 @@ namespace osu.Game.Rulesets.Objects.Drawables private readonly Bindable state = new Bindable(); + /// + /// The state of this . + /// + /// + /// For pooled hitobjects, is recommended to be used instead for better editor/rewinding support. + /// public IBindable State => state; /// From 57454bbb1c50d0af8bf9e1d2a9da5db9993e354f Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 10:13:05 +0900 Subject: [PATCH 4937/6909] Remove hitObject argument from OnApply and OnFree --- .../Objects/Drawables/DrawableOsuHitObject.cs | 8 ++++---- .../Objects/Drawables/DrawableSlider.cs | 8 ++++---- .../Objects/Drawables/DrawableSliderHead.cs | 4 ++-- .../Visual/Gameplay/TestScenePoolingRuleset.cs | 4 ++-- .../Rulesets/Objects/Drawables/DrawableHitObject.cs | 10 ++++------ 5 files changed, 16 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index a26db06ede..4b7f048c1b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -53,9 +53,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }); } - protected override void OnApply(HitObject hitObject) + protected override void OnApply() { - base.OnApply(hitObject); + base.OnApply(); IndexInCurrentComboBindable.BindTo(HitObject.IndexInCurrentComboBindable); PositionBindable.BindTo(HitObject.PositionBindable); @@ -70,9 +70,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; } - protected override void OnFree(HitObject hitObject) + protected override void OnFree() { - base.OnFree(hitObject); + base.OnFree(); IndexInCurrentComboBindable.UnbindFrom(HitObject.IndexInCurrentComboBindable); PositionBindable.UnbindFrom(HitObject.PositionBindable); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 6340367593..dd27ac990e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -86,18 +86,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Tracking.BindValueChanged(updateSlidingSample); } - protected override void OnApply(HitObject hitObject) + protected override void OnApply() { - base.OnApply(hitObject); + base.OnApply(); // Ensure that the version will change after the upcoming BindTo(). pathVersion.Value = int.MaxValue; PathVersion.BindTo(HitObject.Path.Version); } - protected override void OnFree(HitObject hitObject) + protected override void OnFree() { - base.OnFree(hitObject); + base.OnFree(); PathVersion.UnbindFrom(HitObject.Path.Version); } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index fd0f35d20d..b5c33c0924 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -36,9 +36,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables pathVersion.BindValueChanged(_ => updatePosition()); } - protected override void OnFree(HitObject hitObject) + protected override void OnFree() { - base.OnFree(hitObject); + base.OnFree(); pathVersion.UnbindFrom(drawableSlider.PathVersion); } diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index 3e777119c4..cd7d692b0a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -261,9 +261,9 @@ namespace osu.Game.Tests.Visual.Gameplay }); } - protected override void OnApply(HitObject hitObject) + protected override void OnApply() { - base.OnApply(hitObject); + base.OnApply(); Position = new Vector2(RNG.Next(-200, 200), RNG.Next(-200, 200)); } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 2e37e8977a..9c799bcf32 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -260,7 +260,7 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject.DefaultsApplied += onDefaultsApplied; - OnApply(hitObject); + OnApply(); HitObjectApplied?.Invoke(this); // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. @@ -315,7 +315,7 @@ namespace osu.Game.Rulesets.Objects.Drawables HitObject.DefaultsApplied -= onDefaultsApplied; - OnFree(HitObject); + OnFree(); HitObject = null; Result = null; @@ -340,16 +340,14 @@ namespace osu.Game.Rulesets.Objects.Drawables /// /// Invoked for this to take on any values from a newly-applied . /// - /// The being applied. - protected virtual void OnApply(HitObject hitObject) + protected virtual void OnApply() { } /// /// Invoked for this to revert any values previously taken on from the currently-applied . /// - /// The currently-applied . - protected virtual void OnFree(HitObject hitObject) + protected virtual void OnFree() { } From fe85b7d482a0aabb076387f174fbbc9ab7835e77 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 10:18:00 +0900 Subject: [PATCH 4938/6909] Remove unused import --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index b5c33c0924..3a92938d75 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; From c272fda41628b34e5a3d4ec7f4eb80ddf7ee2fa7 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 10:31:18 +0900 Subject: [PATCH 4939/6909] Add bindables to catch hit objects --- .../Objects/CatchHitObject.cs | 45 ++++++++++++++++--- .../Objects/PalpableCatchHitObject.cs | 17 ++++++- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index ccd2422381..05b2a21794 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -16,25 +16,47 @@ namespace osu.Game.Rulesets.Catch.Objects { public const float OBJECT_RADIUS = 64; - private float x; + // This value is after XOffset applied. + public readonly Bindable XBindable = new Bindable(); + + // This value is before XOffset applied. + private float originalX; /// /// The horizontal position of the fruit between 0 and . /// public float X { - get => x + XOffset; - set => x = value; + // TODO: I don't like this asymmetry. + get => XBindable.Value; + // originalX is set by `XBindable.BindValueChanged` + set => XBindable.Value = value + xOffset; } + private float xOffset; + /// /// A random offset applied to , set by the . /// - internal float XOffset { get; set; } + internal float XOffset + { + get => xOffset; + set + { + xOffset = value; + XBindable.Value = originalX + xOffset; + } + } public double TimePreempt = 1000; - public int IndexInBeatmap { get; set; } + public readonly Bindable IndexInBeatmapBindable = new Bindable(); + + public int IndexInBeatmap + { + get => IndexInBeatmapBindable.Value; + set => IndexInBeatmapBindable.Value = value; + } public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4); @@ -69,7 +91,13 @@ namespace osu.Game.Rulesets.Catch.Objects set => LastInComboBindable.Value = value; } - public float Scale { get; set; } = 1; + public readonly Bindable ScaleBindable = new Bindable(1); + + public float Scale + { + get => ScaleBindable.Value; + set => ScaleBindable.Value = value; + } protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { @@ -81,6 +109,11 @@ namespace osu.Game.Rulesets.Catch.Objects } protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + protected CatchHitObject() + { + XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset); + } } public enum FruitVisualRepresentation diff --git a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs index 995f61c386..0cd3af01df 100644 --- a/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/PalpableCatchHitObject.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Types; using osuTK.Graphics; @@ -20,15 +21,27 @@ namespace osu.Game.Rulesets.Catch.Objects /// public float DistanceToHyperDash { get; set; } + public readonly Bindable HyperDashBindable = new Bindable(); + /// /// Whether this fruit can initiate a hyperdash. /// - public bool HyperDash => HyperDashTarget != null; + public bool HyperDash => HyperDashBindable.Value; + + private CatchHitObject hyperDashTarget; /// /// The target fruit if we are to initiate a hyperdash. /// - public CatchHitObject HyperDashTarget; + public CatchHitObject HyperDashTarget + { + get => hyperDashTarget; + set + { + hyperDashTarget = value; + HyperDashBindable.Value = value != null; + } + } Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => comboColours[(IndexInBeatmap + 1) % comboColours.Count]; } From 5e36fb322af95b47e31530e961c9a6fe8879528b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 10:10:05 +0900 Subject: [PATCH 4940/6909] Move fruit visual logic from CHO to DrawableFruit --- .../TestSceneFruitObjects.cs | 17 +++--- osu.Game.Rulesets.Catch/Objects/Banana.cs | 2 - .../Objects/BananaShower.cs | 2 - .../Objects/CatchHitObject.cs | 11 ---- .../Objects/Drawables/DrawableBanana.cs | 2 + .../Objects/Drawables/DrawableFruit.cs | 59 ++++++++++++++++++- .../Objects/Drawables/Pieces/FruitPiece.cs | 26 +++----- 7 files changed, 77 insertions(+), 42 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index e8ecd2ca1b..93ffb947e1 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Tests } private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) => - setProperties(new DrawableFruit(new TestCatchFruit(rep)), hyperdash); + setProperties(new TestDrawableFruit(new Fruit(), rep), hyperdash); private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash); @@ -56,14 +56,17 @@ namespace osu.Game.Rulesets.Catch.Tests return d; } - public class TestCatchFruit : Fruit + public class TestDrawableFruit : DrawableFruit { - public TestCatchFruit(FruitVisualRepresentation rep) - { - VisualRepresentation = rep; - } + private readonly FruitVisualRepresentation visualRepresentation; - public override FruitVisualRepresentation VisualRepresentation { get; } + protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => visualRepresentation; + + public TestDrawableFruit(Fruit fruit, FruitVisualRepresentation rep) + : base(fruit) + { + visualRepresentation = rep; + } } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index d1033f7801..ccb1fff15b 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -18,8 +18,6 @@ namespace osu.Game.Rulesets.Catch.Objects /// public int BananaIndex; - public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; - public override Judgement CreateJudgement() => new CatchBananaJudgement(); private static readonly List samples = new List { new BananaHitSampleInfo() }; diff --git a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs index 89c51459a6..b45f95a8e6 100644 --- a/osu.Game.Rulesets.Catch/Objects/BananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/BananaShower.cs @@ -9,8 +9,6 @@ namespace osu.Game.Rulesets.Catch.Objects { public class BananaShower : CatchHitObject, IHasDuration { - public override FruitVisualRepresentation VisualRepresentation => FruitVisualRepresentation.Banana; - public override bool LastInCombo => true; public override Judgement CreateJudgement() => new IgnoreJudgement(); diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 05b2a21794..a74055bff9 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -58,8 +58,6 @@ namespace osu.Game.Rulesets.Catch.Objects set => IndexInBeatmapBindable.Value = value; } - public virtual FruitVisualRepresentation VisualRepresentation => (FruitVisualRepresentation)(IndexInBeatmap % 4); - public virtual bool NewCombo { get; set; } public int ComboOffset { get; set; } @@ -115,13 +113,4 @@ namespace osu.Game.Rulesets.Catch.Objects XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset); } } - - public enum FruitVisualRepresentation - { - Pear, - Grape, - Pineapple, - Raspberry, - Banana // banananananannaanana - } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 7748b1c565..efb0958a3a 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -8,6 +8,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableBanana : DrawableFruit { + protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana; + public DrawableBanana(Banana h) : base(h) { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 96e24bf76c..f4adabdbcb 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -11,18 +12,61 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableFruit : DrawablePalpableCatchHitObject { + public readonly Bindable IndexInBeatmap = new Bindable(); + + public readonly Bindable VisualRepresentation = new Bindable(); + + protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4); + + private FruitPiece fruitPiece; + public DrawableFruit(CatchHitObject h) : base(h) { + IndexInBeatmap.Value = h.IndexInBeatmap; } [BackgroundDependencyLoader] private void load() { - ScaleContainer.Child = new SkinnableDrawable( - new CatchSkinComponent(getComponent(HitObject.VisualRepresentation)), _ => new FruitPiece()); - ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; + + IndexInBeatmap.BindValueChanged(change => + { + VisualRepresentation.Value = GetVisualRepresentation(change.NewValue); + }, true); + + VisualRepresentation.BindValueChanged(change => + { + ScaleContainer.Child = new SkinnableDrawable( + new CatchSkinComponent(getComponent(change.NewValue)), + _ => fruitPiece = new FruitPiece + { + VisualRepresentation = { BindTarget = VisualRepresentation }, + }); + }, true); + } + + protected override void OnApply() + { + base.OnApply(); + + IndexInBeatmap.BindTo(HitObject.IndexInBeatmapBindable); + } + + protected override void OnFree() + { + IndexInBeatmap.UnbindFrom(HitObject.IndexInBeatmapBindable); + + base.OnFree(); + } + + protected override void Update() + { + base.Update(); + + if (fruitPiece != null) + fruitPiece.Border.Alpha = (float)Math.Clamp((StartTimeBindable.Value - Time.Current) / 500, 0, 1); } private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation) @@ -49,4 +93,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables } } } + + public enum FruitVisualRepresentation + { + Pear, + Grape, + Pineapple, + Raspberry, + Banana // banananananannaanana + } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs index 208c9f8316..a9a8f551ce 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.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 System; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { @@ -16,8 +15,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces /// public const float RADIUS_ADJUST = 1.1f; - private BorderPiece border; - private PalpableCatchHitObject hitObject; + public readonly Bindable VisualRepresentation = new Bindable(); + public readonly Bindable HyperDash = new Bindable(); + + public BorderPiece Border; public FruitPiece() { @@ -25,29 +26,20 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces } [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) + private void load() { - var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; - hitObject = drawableCatchObject.HitObject; - AddRangeInternal(new[] { - getFruitFor(hitObject.VisualRepresentation), - border = new BorderPiece(), + getFruitFor(VisualRepresentation.Value), + Border = new BorderPiece(), }); - if (hitObject.HyperDash) + if (HyperDash.Value) { AddInternal(new HyperBorderPiece()); } } - protected override void Update() - { - base.Update(); - border.Alpha = (float)Math.Clamp((hitObject.StartTime - Time.Current) / 500, 0, 1); - } - private Drawable getFruitFor(FruitVisualRepresentation representation) { switch (representation) From 23109f5bbced8d8725ff9fd9979e0178f3ee15cb Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 10:55:33 +0900 Subject: [PATCH 4941/6909] Add bindable to drawable catch hit obejcts --- .../Drawables/DrawableCatchHitObject.cs | 21 ++++++++++- .../Objects/Drawables/DrawableFruit.cs | 22 +++++++----- .../DrawablePalpableCatchHitObject.cs | 36 ++++++++++++++++++- 3 files changed, 68 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index f9f534f9ab..92b4f7cae2 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.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.Graphics; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects.Drawables; @@ -10,6 +11,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public abstract class DrawableCatchHitObject : DrawableHitObject { + public readonly Bindable XBindable = new Bindable(); + protected override double InitialLifetimeOffset => HitObject.TimePreempt; public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; @@ -19,10 +22,26 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected DrawableCatchHitObject(CatchHitObject hitObject) : base(hitObject) { - X = hitObject.X; + if (hitObject != null) + XBindable.Value = hitObject.X; + Anchor = Anchor.BottomLeft; } + protected override void OnApply() + { + base.OnApply(); + + XBindable.BindTo(HitObject.XBindable); + } + + protected override void OnFree() + { + base.OnFree(); + + XBindable.UnbindFrom(HitObject.XBindable); + } + public Func CheckPosition; public bool IsOnPlate; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index f4adabdbcb..d53bcd3a6f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -36,15 +36,19 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables VisualRepresentation.Value = GetVisualRepresentation(change.NewValue); }, true); - VisualRepresentation.BindValueChanged(change => - { - ScaleContainer.Child = new SkinnableDrawable( - new CatchSkinComponent(getComponent(change.NewValue)), - _ => fruitPiece = new FruitPiece - { - VisualRepresentation = { BindTarget = VisualRepresentation }, - }); - }, true); + VisualRepresentation.BindValueChanged(_ => updatePiece()); + HyperDash.BindValueChanged(_ => updatePiece(), true); + } + + private void updatePiece() + { + ScaleContainer.Child = new SkinnableDrawable( + new CatchSkinComponent(getComponent(VisualRepresentation.Value)), + _ => fruitPiece = new FruitPiece + { + VisualRepresentation = { BindTarget = VisualRepresentation }, + HyperDash = { BindTarget = HyperDash }, + }); } protected override void OnApply() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 9339a1c420..95f90a3eee 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osuTK; @@ -12,6 +13,15 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; + public Bindable HyperDash { get; } = new Bindable(); + + public Bindable ScaleBindable { get; } = new Bindable(1); + + /// + /// The multiplicative factor applied to scale relative to scale. + /// + protected virtual float ScaleFactor => 1; + /// /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. /// @@ -36,7 +46,31 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - ScaleContainer.Scale = new Vector2(HitObject.Scale); + XBindable.BindValueChanged(x => + { + if (!IsOnPlate) X = x.NewValue; + }, true); + + ScaleBindable.BindValueChanged(scale => + { + ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor); + }, true); + } + + protected override void OnApply() + { + base.OnApply(); + + HyperDash.BindTo(HitObject.HyperDashBindable); + ScaleBindable.BindTo(HitObject.ScaleBindable); + } + + protected override void OnFree() + { + HyperDash.UnbindFrom(HitObject.HyperDashBindable); + ScaleBindable.UnbindFrom(HitObject.ScaleBindable); + + base.OnFree(); } } } From dbf67f82c009d9bdea881424869c051783db5e3d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 11:40:38 +0900 Subject: [PATCH 4942/6909] Use bindable for DrawableDroplet HyperDash state --- .../Objects/Drawables/DrawableDroplet.cs | 12 +++++++++++- .../Objects/Drawables/Pieces/DropletPiece.cs | 7 ++++--- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 74cd240aa3..9e76265394 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -21,7 +21,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - ScaleContainer.Child = new SkinnableDrawable(new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece()); + HyperDash.BindValueChanged(_ => updatePiece(), true); + } + + private void updatePiece() + { + ScaleContainer.Child = new SkinnableDrawable( + new CatchSkinComponent(CatchSkinComponents.Droplet), + _ => new DropletPiece + { + HyperDash = { BindTarget = HyperDash } + }); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs index bcef30fda8..c90407ae15 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; @@ -11,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class DropletPiece : CompositeDrawable { + public readonly Bindable HyperDash = new Bindable(); + public DropletPiece() { Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); @@ -19,15 +22,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces [BackgroundDependencyLoader] private void load(DrawableHitObject drawableObject) { - var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; - InternalChild = new Pulp { RelativeSizeAxes = Axes.Both, AccentColour = { BindTarget = drawableObject.AccentColour } }; - if (drawableCatchObject.HitObject.HyperDash) + if (HyperDash.Value) { AddInternal(new HyperDropletBorderPiece()); } From e36bb7631d0bc5abcd4ea82e4b90882afb322327 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 11:41:39 +0900 Subject: [PATCH 4943/6909] Fix colour not updated when index changes --- .../Objects/Drawables/DrawableFruit.cs | 17 ----------------- .../Drawables/DrawablePalpableCatchHitObject.cs | 8 ++++++++ .../Objects/Drawables/DrawableHitObject.cs | 6 +++--- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index d53bcd3a6f..e98f410724 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -12,8 +12,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableFruit : DrawablePalpableCatchHitObject { - public readonly Bindable IndexInBeatmap = new Bindable(); - public readonly Bindable VisualRepresentation = new Bindable(); protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4); @@ -23,7 +21,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public DrawableFruit(CatchHitObject h) : base(h) { - IndexInBeatmap.Value = h.IndexInBeatmap; } [BackgroundDependencyLoader] @@ -51,20 +48,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables }); } - protected override void OnApply() - { - base.OnApply(); - - IndexInBeatmap.BindTo(HitObject.IndexInBeatmapBindable); - } - - protected override void OnFree() - { - IndexInBeatmap.UnbindFrom(HitObject.IndexInBeatmapBindable); - - base.OnFree(); - } - protected override void Update() { base.Update(); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 95f90a3eee..f44e290627 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -17,6 +17,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public Bindable ScaleBindable { get; } = new Bindable(1); + public readonly Bindable IndexInBeatmap = new Bindable(); + /// /// The multiplicative factor applied to scale relative to scale. /// @@ -41,6 +43,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Origin = Anchor.Centre, Anchor = Anchor.Centre, }); + + IndexInBeatmap.Value = h.IndexInBeatmap; } [BackgroundDependencyLoader] @@ -55,6 +59,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor); }, true); + + IndexInBeatmap.BindValueChanged(_ => UpdateComboColour()); } protected override void OnApply() @@ -63,12 +69,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables HyperDash.BindTo(HitObject.HyperDashBindable); ScaleBindable.BindTo(HitObject.ScaleBindable); + IndexInBeatmap.BindTo(HitObject.IndexInBeatmapBindable); } protected override void OnFree() { HyperDash.UnbindFrom(HitObject.HyperDashBindable); ScaleBindable.UnbindFrom(HitObject.ScaleBindable); + IndexInBeatmap.UnbindFrom(HitObject.IndexInBeatmapBindable); base.OnFree(); } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 9c799bcf32..182aa1dbe3 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -190,7 +190,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.LoadComplete(); - comboIndexBindable.BindValueChanged(_ => updateComboColour(), true); + comboIndexBindable.BindValueChanged(_ => UpdateComboColour(), true); updateState(ArmedState.Idle, true); } @@ -533,7 +533,7 @@ namespace osu.Game.Rulesets.Objects.Drawables { base.SkinChanged(skin, allowFallback); - updateComboColour(); + UpdateComboColour(); ApplySkin(skin, allowFallback); @@ -541,7 +541,7 @@ namespace osu.Game.Rulesets.Objects.Drawables updateState(State.Value, true); } - private void updateComboColour() + protected void UpdateComboColour() { if (!(HitObject is IHasComboInformation combo)) return; From de471a7e8459334d4105797b51f2cbcf130e049f Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 11:42:14 +0900 Subject: [PATCH 4944/6909] Add test for dynamically changing catch fruits --- .../TestSceneFruitObjects.cs | 8 ++--- .../TestSceneFruitVisualChange.cs | 32 +++++++++++++++++++ 2 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 93ffb947e1..ee7ab27eed 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -30,13 +30,13 @@ namespace osu.Game.Rulesets.Catch.Tests } private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) => - setProperties(new TestDrawableFruit(new Fruit(), rep), hyperdash); + SetProperties(new TestDrawableFruit(new Fruit(), rep), hyperdash); - private Drawable createDrawableDroplet(bool hyperdash = false) => setProperties(new DrawableDroplet(new Droplet()), hyperdash); + private Drawable createDrawableDroplet(bool hyperdash = false) => SetProperties(new DrawableDroplet(new Droplet()), hyperdash); - private Drawable createDrawableTinyDroplet() => setProperties(new DrawableTinyDroplet(new TinyDroplet())); + private Drawable createDrawableTinyDroplet() => SetProperties(new DrawableTinyDroplet(new TinyDroplet())); - private DrawableCatchHitObject setProperties(DrawableCatchHitObject d, bool hyperdash = false) + protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d, bool hyperdash = false) { var hitObject = d.HitObject; hitObject.StartTime = 1000000000000; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs new file mode 100644 index 0000000000..4448e828e7 --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.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.Bindables; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneFruitVisualChange : TestSceneFruitObjects + { + private readonly Bindable indexInBeatmap = new Bindable(); + private readonly Bindable hyperDash = new Bindable(); + + protected override void LoadComplete() + { + AddStep("fruit changes visual and hyper", () => SetContents(() => SetProperties(new DrawableFruit(new Fruit + { + IndexInBeatmapBindable = { BindTarget = indexInBeatmap }, + HyperDashBindable = { BindTarget = hyperDash }, + })))); + + AddStep("droplet changes hyper", () => SetContents(() => SetProperties(new DrawableDroplet(new Droplet + { + HyperDashBindable = { BindTarget = hyperDash }, + })))); + + Scheduler.AddDelayed(() => indexInBeatmap.Value++, 250, true); + Scheduler.AddDelayed(() => hyperDash.Value = !hyperDash.Value, 1000, true); + } + } +} From 35cd6674f62453170861d6dab2f738fbb66932ba Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 11:56:57 +0900 Subject: [PATCH 4945/6909] Fix tiny droplet scale factor --- .../Objects/Drawables/DrawableTinyDroplet.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs index ae775684d8..8c4d821b4a 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs @@ -1,21 +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.Allocation; - namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableTinyDroplet : DrawableDroplet { + protected override float ScaleFactor => base.ScaleFactor / 2; + public DrawableTinyDroplet(TinyDroplet h) : base(h) { } - - [BackgroundDependencyLoader] - private void load() - { - ScaleContainer.Scale /= 2; - } } } From 7ce752391db81112da5ce10eec6b3c66de2d692b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 12:02:07 +0900 Subject: [PATCH 4946/6909] Make TestSceneFruitObjects show correct color --- .../TestSceneFruitObjects.cs | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index ee7ab27eed..c661977b03 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.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 NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects; @@ -17,33 +16,48 @@ namespace osu.Game.Rulesets.Catch.Tests { base.LoadComplete(); - foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) - AddStep($"show {rep}", () => SetContents(() => createDrawableFruit(rep))); + AddStep("show pear", () => SetContents(() => createDrawableFruit(0))); + AddStep("show grape", () => SetContents(() => createDrawableFruit(1))); + AddStep("show pineapple / apple", () => SetContents(() => createDrawableFruit(2))); + AddStep("show raspberry / orange", () => SetContents(() => createDrawableFruit(3))); + + AddStep("show banana", () => SetContents(createDrawableBanana)); AddStep("show droplet", () => SetContents(() => createDrawableDroplet())); AddStep("show tiny droplet", () => SetContents(createDrawableTinyDroplet)); - foreach (FruitVisualRepresentation rep in Enum.GetValues(typeof(FruitVisualRepresentation))) - AddStep($"show hyperdash {rep}", () => SetContents(() => createDrawableFruit(rep, true))); + AddStep("show hyperdash pear", () => SetContents(() => createDrawableFruit(0, true))); + AddStep("show hyperdash grape", () => SetContents(() => createDrawableFruit(1, true))); + AddStep("show hyperdash pineapple / apple", () => SetContents(() => createDrawableFruit(2, true))); + AddStep("show hyperdash raspberry / orange", () => SetContents(() => createDrawableFruit(3, true))); AddStep("show hyperdash droplet", () => SetContents(() => createDrawableDroplet(true))); } - private Drawable createDrawableFruit(FruitVisualRepresentation rep, bool hyperdash = false) => - SetProperties(new TestDrawableFruit(new Fruit(), rep), hyperdash); + private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) => + SetProperties(new DrawableFruit(new Fruit + { + IndexInBeatmap = indexInBeatmap, + HyperDashBindable = { Value = hyperdash } + })); - private Drawable createDrawableDroplet(bool hyperdash = false) => SetProperties(new DrawableDroplet(new Droplet()), hyperdash); + private Drawable createDrawableBanana() => + SetProperties(new DrawableBanana(new Banana())); + + private Drawable createDrawableDroplet(bool hyperdash = false) => + SetProperties(new DrawableDroplet(new Droplet + { + HyperDashBindable = { Value = hyperdash } + })); private Drawable createDrawableTinyDroplet() => SetProperties(new DrawableTinyDroplet(new TinyDroplet())); - protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d, bool hyperdash = false) + protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d) { var hitObject = d.HitObject; hitObject.StartTime = 1000000000000; hitObject.Scale = 1.5f; - - if (hyperdash) - ((PalpableCatchHitObject)hitObject).HyperDashTarget = new Banana(); + hitObject.Samples.Clear(); // otherwise crash due to samples not loaded d.Anchor = Anchor.Centre; d.RelativePositionAxes = Axes.None; @@ -55,18 +69,5 @@ namespace osu.Game.Rulesets.Catch.Tests }; return d; } - - public class TestDrawableFruit : DrawableFruit - { - private readonly FruitVisualRepresentation visualRepresentation; - - protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => visualRepresentation; - - public TestDrawableFruit(Fruit fruit, FruitVisualRepresentation rep) - : base(fruit) - { - visualRepresentation = rep; - } - } } } From 6e40de58e9a17aa3374ff3722e43543c975f4a9f Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 13:36:40 +0900 Subject: [PATCH 4947/6909] Use new OnAdd and OnRemove to invalidate DHO --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 24 +++++++++++++++++ .../Scrolling/ScrollingHitObjectContainer.cs | 27 ++++++++++++++++++- .../UI/Scrolling/ScrollingPlayfield.cs | 5 ---- 3 files changed, 50 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 5fbda305c8..ac5d281ddc 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -114,6 +114,7 @@ namespace osu.Game.Rulesets.UI bindStartTime(drawable); AddInternal(drawableMap[entry] = drawable, false); + OnAdd(drawable); HitObjectUsageBegan?.Invoke(entry.HitObject); } @@ -129,6 +130,7 @@ namespace osu.Game.Rulesets.UI drawableMap.Remove(entry); + OnRemove(drawable); unbindStartTime(drawable); RemoveInternal(drawable); @@ -147,10 +149,12 @@ namespace osu.Game.Rulesets.UI hitObject.OnRevertResult += onRevertResult; AddInternal(hitObject); + OnAdd(hitObject); } public virtual bool Remove(DrawableHitObject hitObject) { + OnRemove(hitObject); if (!RemoveInternal(hitObject)) return false; @@ -178,6 +182,26 @@ namespace osu.Game.Rulesets.UI #endregion + /// + /// Invoked when a is added to this container. + /// + /// + /// This method is not invoked for nested s. + /// + protected virtual void OnAdd(DrawableHitObject drawableHitObject) + { + } + + /// + /// Invoked when a is removed from this container. + /// + /// + /// This method is not invoked for nested s. + /// + protected virtual void OnRemove(DrawableHitObject drawableHitObject) + { + } + public virtual void Clear(bool disposeChildren = true) { lifetimeManager.ClearEntries(); diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 02ee39e1b8..71f8f95300 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -150,7 +150,7 @@ namespace osu.Game.Rulesets.UI.Scrolling /// /// Make this lifetime and layout computed in next update. /// - internal void InvalidateHitObject(DrawableHitObject hitObject) + private void invalidateHitObject(DrawableHitObject hitObject) { // Lifetime computation is delayed until next update because // when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed. @@ -158,6 +158,31 @@ namespace osu.Game.Rulesets.UI.Scrolling layoutComputed.Remove(hitObject); } + private void onAddRecursive(DrawableHitObject hitObject) + { + invalidateHitObject(hitObject); + + hitObject.DefaultsApplied += invalidateHitObject; + + foreach (var nested in hitObject.NestedHitObjects) + onAddRecursive(nested); + } + + protected override void OnAdd(DrawableHitObject drawableHitObject) => onAddRecursive(drawableHitObject); + + private void onRemoveRecursive(DrawableHitObject hitObject) + { + toComputeLifetime.Remove(hitObject); + layoutComputed.Remove(hitObject); + + hitObject.DefaultsApplied -= invalidateHitObject; + + foreach (var nested in hitObject.NestedHitObjects) + onRemoveRecursive(nested); + } + + protected override void OnRemove(DrawableHitObject drawableHitObject) => onRemoveRecursive(drawableHitObject); + private float scrollLength; protected override void Update() diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 8aba896b34..2b75f93f9e 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -26,11 +26,6 @@ namespace osu.Game.Rulesets.UI.Scrolling Direction.BindTo(ScrollingInfo.Direction); } - protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) - { - drawableHitObject.HitObjectApplied += d => HitObjectContainer.InvalidateHitObject(d); - } - /// /// Given a position in screen space, return the time within this column. /// From 792934f2c4ee5617c8a7ca68e8840d3c14400fb7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Nov 2020 13:54:36 +0900 Subject: [PATCH 4948/6909] Allow scroll type to be specified This brings back the ability for the carousel to scroll in a classic way. It turns out this is generally what we want for "seek" operations like "random", else it's quite hard to get the expected animation. I did experiment with applying the animation after the pooled panels are retrieved, but in a best-case scenario there is still a gap where no panels are displayed during the random seek operation. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 57 +++++++++++++++------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 2012d47fd3..4ce87927a1 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -114,7 +114,7 @@ namespace osu.Game.Screens.Select Scroll.Clear(false); itemsCache.Invalidate(); - scrollPositionCache.Invalidate(); + ScrollToSelected(); // apply any pending filter operation that may have been delayed (see applyActiveCriteria's scheduling behaviour when BeatmapSetsLoaded is false). FlushPendingFilterOperations(); @@ -130,7 +130,7 @@ namespace osu.Game.Screens.Select private readonly List visibleItems = new List(); private readonly Cached itemsCache = new Cached(); - private readonly Cached scrollPositionCache = new Cached(); + private PendingScrollOperation pendingScrollOperation = PendingScrollOperation.None; public Bindable RightClickScrollingEnabled = new Bindable(); @@ -462,7 +462,7 @@ namespace osu.Game.Screens.Select itemsCache.Invalidate(); if (alwaysResetScrollPosition || !Scroll.UserScrolling) - ScrollToSelected(); + ScrollToSelected(true); } } @@ -471,7 +471,12 @@ namespace osu.Game.Screens.Select /// /// Scroll to the current . /// - public void ScrollToSelected() => scrollPositionCache.Invalidate(); + /// + /// Whether the scroll position should immediately be shifted to the target, delegating animation to visible panels. + /// This should be true for operations like filtering - where panels are changing visibility state - to avoid large jumps in animation. + /// + public void ScrollToSelected(bool immediate = false) => + pendingScrollOperation = immediate ? PendingScrollOperation.Immediate : PendingScrollOperation.Standard; #region Key / button selection logic @@ -481,12 +486,12 @@ namespace osu.Game.Screens.Select { case Key.Left: if (!e.Repeat) - beginRepeatSelection(() => SelectNext(-1, true), e.Key); + beginRepeatSelection(() => SelectNext(-1), e.Key); return true; case Key.Right: if (!e.Repeat) - beginRepeatSelection(() => SelectNext(1, true), e.Key); + beginRepeatSelection(() => SelectNext(), e.Key); return true; } @@ -574,9 +579,8 @@ namespace osu.Game.Screens.Select updateYPositions(); // if there is a pending scroll action we apply it without animation and transfer the difference in position to the panels. - // due to this, scroll application needs to be run immediately after y position updates. - // if this isn't the case, the on-screen pooling / display logic following will fail briefly. - if (!scrollPositionCache.IsValid) + // this is intentionally applied before updating the visible range below, to avoid animating new items (sourced from pool) from locations off-screen, as it looks bad. + if (pendingScrollOperation != PendingScrollOperation.None) updateScrollPosition(); // This data is consumed to find the currently displayable range. @@ -795,17 +799,27 @@ namespace osu.Game.Screens.Select firstScroll = false; } - // in order to simplify animation logic, rather than using the animated version of ScrollTo, - // we take the difference in scroll height and apply to all visible panels. - // this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer - // to enter clamp-special-case mode where it animates completely differently to normal. - float scrollChange = scrollTarget.Value - Scroll.Current; + switch (pendingScrollOperation) + { + case PendingScrollOperation.Standard: + Scroll.ScrollTo(scrollTarget.Value); + break; - Scroll.ScrollTo(scrollTarget.Value, false); - scrollPositionCache.Validate(); + case PendingScrollOperation.Immediate: + // in order to simplify animation logic, rather than using the animated version of ScrollTo, + // we take the difference in scroll height and apply to all visible panels. + // this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer + // to enter clamp-special-case mode where it animates completely differently to normal. + float scrollChange = scrollTarget.Value - Scroll.Current; - foreach (var i in Scroll.Children) - i.Y += scrollChange; + Scroll.ScrollTo(scrollTarget.Value, false); + + foreach (var i in Scroll.Children) + i.Y += scrollChange; + break; + } + + pendingScrollOperation = PendingScrollOperation.None; } } @@ -849,6 +863,13 @@ namespace osu.Game.Screens.Select item.SetMultiplicativeAlpha(Math.Clamp(1.75f - 1.5f * dist, 0, 1)); } + private enum PendingScrollOperation + { + None, + Standard, + Immediate, + } + /// /// A carousel item strictly used for binary search purposes. /// From f29aa9c4fca5d157a6b610ddab4811b7734beee3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Nov 2020 14:34:12 +0900 Subject: [PATCH 4949/6909] Move taiko barlines to their own ScrollingHitObjectContainer to avoid being considered as a selectable object --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 35 +++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 120cf264c3..370760f03e 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.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.Graphics; @@ -37,7 +38,7 @@ namespace osu.Game.Rulesets.Taiko.UI private SkinnableDrawable mascot; private ProxyContainer topLevelHitContainer; - private ProxyContainer barlineContainer; + private ScrollingHitObjectContainer barlineContainer; private Container rightArea; private Container leftArea; @@ -83,10 +84,7 @@ namespace osu.Game.Rulesets.Taiko.UI RelativeSizeAxes = Axes.Both, Children = new Drawable[] { - barlineContainer = new ProxyContainer - { - RelativeSizeAxes = Axes.Both, - }, + barlineContainer = new ScrollingHitObjectContainer(), new Container { Name = "Hit objects", @@ -159,18 +157,37 @@ namespace osu.Game.Rulesets.Taiko.UI public override void Add(DrawableHitObject h) { - h.OnNewResult += OnNewResult; - base.Add(h); - switch (h) { case DrawableBarLine barline: - barlineContainer.Add(barline.CreateProxy()); + barlineContainer.Add(barline); break; case DrawableTaikoHitObject taikoObject: + h.OnNewResult += OnNewResult; topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); + base.Add(h); break; + + default: + throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type"); + } + } + + public override bool Remove(DrawableHitObject h) + { + switch (h) + { + case DrawableBarLine barline: + return barlineContainer.Remove(barline); + + case DrawableTaikoHitObject _: + h.OnNewResult -= OnNewResult; + // todo: consider tidying of proxied content if required. + return base.Remove(h); + + default: + throw new ArgumentException($"Unsupported {nameof(DrawableHitObject)} type"); } } From b9b885798801e8737498704e2186bec8fe985d58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Nov 2020 15:11:07 +0900 Subject: [PATCH 4950/6909] 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 6dab6edc5e..0b43fd73f5 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 54f3fcede6..e201383d51 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 692dac909a..e5f7581404 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 1246c8ba5f9b15bef09ff8c6b0a1fd208ddbb8d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Nov 2020 15:22:28 +0900 Subject: [PATCH 4951/6909] Reduce the opacity of the default skin slider ball Previous value was [hitting pure white on some brighter combo colours](https://github.com/ppy/osu/issues/10910#issuecomment-734354812). --- 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 c5bf790377..ca5ca7ac59 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs @@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces } private void trackingChanged(ValueChangedEvent tracking) => - box.FadeTo(tracking.NewValue ? 0.6f : 0.05f, 200, Easing.OutQuint); + box.FadeTo(tracking.NewValue ? 0.3f : 0.05f, 200, Easing.OutQuint); } } } From a7194e1bc347a204661c939517067cf931c997b6 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 27 Nov 2020 15:41:35 +0900 Subject: [PATCH 4952/6909] add stateless RNG --- osu.Game/Utils/StatelessRNG.cs | 44 ++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 osu.Game/Utils/StatelessRNG.cs diff --git a/osu.Game/Utils/StatelessRNG.cs b/osu.Game/Utils/StatelessRNG.cs new file mode 100644 index 0000000000..8d08b26ca4 --- /dev/null +++ b/osu.Game/Utils/StatelessRNG.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Utils +{ + /// Provides a fast stateless function that can be used in randomly-looking visual elements. + public static class StatelessRNG + { + private static ulong mix(ulong x) + { + unchecked + { + x ^= x >> 33; + x *= 0xff51afd7ed558ccd; + x ^= x >> 33; + x *= 0xc4ceb9fe1a85ec53; + x ^= x >> 33; + return x; + } + } + + /// Compute an integer from given seed and series number. + /// + /// The seed value of this random number generator. + /// + /// + /// The series number. + /// Different values are computed for the same seed in different series. + /// + public static ulong Get(int seed, int series = 0) => + unchecked(mix(((ulong)(uint)series << 32) | ((uint)seed ^ 0x12345678))); + + /// Compute a floating point value between 0 and 1 (excluding 1) from given seed and series number. + /// + /// The seed value of this random number generator. + /// + /// + /// The series number. + /// Different values are computed for the same seed in different series. + /// + public static float GetSingle(int seed, int series = 0) => + (float)(Get(seed, series) & ((1 << 24) - 1)) / (1 << 24); // float has 24-bit precision + } +} From 7edbba58f70d8d61ddfcb6e90d57db0424751f2e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Nov 2020 16:28:29 +0900 Subject: [PATCH 4953/6909] Avoid updating hitobjects unnecessarily for start time changes This was firing regardless of whether the start time was changed, such as where beat snap provided the same time the object already has. The case where a change actually occurs is already handled by EditorBeatmap (see `startTimeBindables`), so it turns out this local handling is not required at all. --- osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index def5f396f1..57f9a7f221 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -496,10 +496,7 @@ namespace osu.Game.Screens.Edit.Compose.Components double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime; foreach (HitObject obj in Beatmap.SelectedHitObjects) - { obj.StartTime += offset; - Beatmap.Update(obj); - } } return true; From 18bb0cb45b944dbcb2afee1673ec1601702d6249 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Nov 2020 16:31:59 +0900 Subject: [PATCH 4954/6909] Remove unnecessary schedule logic from Apply's local updateState call There were cases in the editor where rewinding of transforms would leave the `DrawableHitObject` in a non-`IsPresent` state, resulting in this scheduled logic never running. This would in turn cause ghost hitobjects, which disappear under certain circumstances. Reproduction: - Open editor to empty beatmap - Place single hitcircle at current point in time - Drag editor timeline backwards to seek before zero, and wait for return to zero - Select hitcircle in playfield - Drag hitcircle to right in timeline, triggering a start time change --- .../Objects/Drawables/DrawableHitObject.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 9c799bcf32..95bc72edf6 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -263,18 +263,15 @@ namespace osu.Game.Rulesets.Objects.Drawables OnApply(); HitObjectApplied?.Invoke(this); - // If not loaded, the state update happens in LoadComplete(). Otherwise, the update is scheduled to allow for lifetime updates. + // If not loaded, the state update happens in LoadComplete(). if (IsLoaded) { - Scheduler.Add(() => - { - if (Result.IsHit) - updateState(ArmedState.Hit, true); - else if (Result.HasResult) - updateState(ArmedState.Miss, true); - else - updateState(ArmedState.Idle, true); - }); + if (Result.IsHit) + updateState(ArmedState.Hit, true); + else if (Result.HasResult) + updateState(ArmedState.Miss, true); + else + updateState(ArmedState.Idle, true); } hasHitObjectApplied = true; From a9c59eed02edba7a43c5d2bebaf21c8a25f7e3cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Nov 2020 16:56:26 +0900 Subject: [PATCH 4955/6909] Add test coverage of fail scenario --- .../Editing/EditorChangeHandlerTest.cs | 33 +++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index b7a41ffd1c..5064b0fd22 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Edit; @@ -44,6 +43,36 @@ namespace osu.Game.Tests.Editing Assert.That(stateChangedFired, Is.EqualTo(2)); } + [Test] + public void TestApplyThenUndoThenApplySameChange() + { + var (handler, beatmap) = createChangeHandler(); + + Assert.That(handler.CanUndo.Value, Is.False); + Assert.That(handler.CanRedo.Value, Is.False); + + string originalHash = handler.CurrentStateHash; + + addArbitraryChange(beatmap); + handler.SaveState(); + + Assert.That(handler.CanUndo.Value, Is.True); + Assert.That(handler.CanRedo.Value, Is.False); + Assert.That(stateChangedFired, Is.EqualTo(1)); + + string hash = handler.CurrentStateHash; + + // save a save without making any changes + handler.RestoreState(-1); + + Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash)); + Assert.That(stateChangedFired, Is.EqualTo(2)); + + addArbitraryChange(beatmap); + handler.SaveState(); + Assert.That(hash, Is.EqualTo(handler.CurrentStateHash)); + } + [Test] public void TestSaveSameStateDoesNotSave() { @@ -139,7 +168,7 @@ namespace osu.Game.Tests.Editing private void addArbitraryChange(EditorBeatmap beatmap) { - beatmap.Add(new HitCircle { StartTime = RNG.Next(0, 100000) }); + beatmap.Add(new HitCircle { StartTime = 2760 }); } } } From 7e34c5e239e2e9fed537ce99cde4f7b3311a6469 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Nov 2020 16:57:11 +0900 Subject: [PATCH 4956/6909] Fix state application always checking newest state for early abort, rather than current --- osu.Game/Screens/Edit/EditorChangeHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/EditorChangeHandler.cs b/osu.Game/Screens/Edit/EditorChangeHandler.cs index 62187aed24..2dcb416a03 100644 --- a/osu.Game/Screens/Edit/EditorChangeHandler.cs +++ b/osu.Game/Screens/Edit/EditorChangeHandler.cs @@ -76,7 +76,7 @@ namespace osu.Game.Screens.Edit var newState = stream.ToArray(); // if the previous state is binary equal we don't need to push a new one, unless this is the initial state. - if (savedStates.Count > 0 && newState.SequenceEqual(savedStates.Last())) return; + if (savedStates.Count > 0 && newState.SequenceEqual(savedStates[currentState])) return; if (currentState < savedStates.Count - 1) savedStates.RemoveRange(currentState + 1, savedStates.Count - currentState - 1); From 5bc76cac58a1e0690e9df0d6cd165841f9a3e736 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 27 Nov 2020 17:01:07 +0900 Subject: [PATCH 4957/6909] Remove unused using statement --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs index cf9908decd..628d95dff4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuHitObject.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Graphics.Containers; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.UI; using osuTK; From 579e61eab9502a205ed0a85a678e5d980d647bd2 Mon Sep 17 00:00:00 2001 From: PercyDan <50285552+PercyDan54@users.noreply.github.com> Date: Sat, 28 Nov 2020 16:31:24 +0800 Subject: [PATCH 4958/6909] Allow null --- osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs index 22674f0879..4a9c9bd8a2 100644 --- a/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Debug/GeneralSettings.cs @@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Settings.Sections.Debug { protected override string Header => "General"; - [BackgroundDependencyLoader] + [BackgroundDependencyLoader(true)] private void load(FrameworkDebugConfigManager config, FrameworkConfigManager frameworkConfig, OsuGame game) { Children = new Drawable[] From 8ad4cf73f5c17fca110486de442a83cf11074473 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Sat, 28 Nov 2020 17:07:29 +0200 Subject: [PATCH 4959/6909] Scale stars from 0.4 to 1 --- osu.Game/Graphics/UserInterface/StarCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index b13d6485ac..f249156a59 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -147,7 +147,7 @@ namespace osu.Game.Graphics.UserInterface public override void DisplayAt(float scale) { - scale = Math.Clamp(scale, min_star_scale, 1); + scale = Math.Clamp(scale * (1 - min_star_scale) + min_star_scale, min_star_scale, 1); this.FadeTo(scale, fading_duration); Icon.ScaleTo(scale, scaling_duration, scaling_easing); From 8e0f525588a5d0e997b5a15da50045ebbb719434 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 28 Nov 2020 20:29:35 +0100 Subject: [PATCH 4960/6909] Rewrite existing test scene somewhat --- .../Visual/Gameplay/TestSceneStarCounter.cs | 35 ++++++++----------- 1 file changed, 15 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs index 709e71d195..d6a6ef712a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs @@ -3,7 +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; @@ -14,44 +13,40 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneStarCounter : OsuTestScene { + private readonly StarCounter starCounter; + private readonly OsuSpriteText starsLabel; + public TestSceneStarCounter() { - StarCounter stars = new StarCounter + starCounter = new StarCounter { Origin = Anchor.Centre, Anchor = Anchor.Centre, - Current = 5, }; - Add(stars); + Add(starCounter); - SpriteText starsLabel = new OsuSpriteText + 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); + setStars(5); - AddStep(@"Stop animation", delegate - { - stars.StopAnimation(); - }); + AddRepeatStep("random value", () => setStars(RNG.NextSingle() * (starCounter.StarCount + 1)), 10); + AddStep("stop animation", () => starCounter.StopAnimation()); + AddStep("reset", () => setStars(0)); + } - AddStep(@"Reset", delegate - { - stars.Current = 0; - starsLabel.Text = stars.Current.ToString("0.00"); - }); + private void setStars(float stars) + { + starCounter.Current = stars; + starsLabel.Text = starCounter.Current.ToString("0.00"); } } } From 9bf70e4e97ed84c95bb5d19eb4e7cf59391f7e8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 28 Nov 2020 20:32:08 +0100 Subject: [PATCH 4961/6909] Add slider test step for visual inspection purposes --- osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs index d6a6ef712a..717485bcc1 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStarCounter.cs @@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay setStars(5); AddRepeatStep("random value", () => setStars(RNG.NextSingle() * (starCounter.StarCount + 1)), 10); + AddSliderStep("exact value", 0f, 10f, 5f, setStars); AddStep("stop animation", () => starCounter.StopAnimation()); AddStep("reset", () => setStars(0)); } From a3afd88387b0d4cb939adf2ff69cd3031672d06a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 28 Nov 2020 20:35:03 +0100 Subject: [PATCH 4962/6909] Use Interpolation.Lerp --- osu.Game/Graphics/UserInterface/StarCounter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/StarCounter.cs b/osu.Game/Graphics/UserInterface/StarCounter.cs index f249156a59..894a21fcf3 100644 --- a/osu.Game/Graphics/UserInterface/StarCounter.cs +++ b/osu.Game/Graphics/UserInterface/StarCounter.cs @@ -147,7 +147,7 @@ namespace osu.Game.Graphics.UserInterface public override void DisplayAt(float scale) { - scale = Math.Clamp(scale * (1 - min_star_scale) + min_star_scale, min_star_scale, 1); + scale = (float)Interpolation.Lerp(min_star_scale, 1, Math.Clamp(scale, 0, 1)); this.FadeTo(scale, fading_duration); Icon.ScaleTo(scale, scaling_duration, scaling_easing); From a5c4a8d2e9ae59d133547c9f1748c135ec87eb5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 29 Nov 2020 22:00:15 +0100 Subject: [PATCH 4963/6909] Create "User Interface" settings section --- .../Settings/Sections/AudioSection.cs | 1 - .../Settings/Sections/GameplaySection.cs | 1 - .../Settings/Sections/GraphicsSection.cs | 1 - .../GeneralSettings.cs} | 6 ++-- .../MainMenuSettings.cs | 2 +- .../SongSelectSettings.cs | 2 +- .../Settings/Sections/UserInterfaceSection.cs | 29 +++++++++++++++++++ osu.Game/Overlays/SettingsOverlay.cs | 1 + 8 files changed, 35 insertions(+), 8 deletions(-) rename osu.Game/Overlays/Settings/Sections/{Graphics/UserInterfaceSettings.cs => UserInterface/GeneralSettings.cs} (88%) rename osu.Game/Overlays/Settings/Sections/{Audio => UserInterface}/MainMenuSettings.cs (97%) rename osu.Game/Overlays/Settings/Sections/{Gameplay => UserInterface}/SongSelectSettings.cs (97%) create mode 100644 osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs diff --git a/osu.Game/Overlays/Settings/Sections/AudioSection.cs b/osu.Game/Overlays/Settings/Sections/AudioSection.cs index 69538358f1..7072d8e63d 100644 --- a/osu.Game/Overlays/Settings/Sections/AudioSection.cs +++ b/osu.Game/Overlays/Settings/Sections/AudioSection.cs @@ -27,7 +27,6 @@ namespace osu.Game.Overlays.Settings.Sections new AudioDevicesSettings(), new VolumeSettings(), new OffsetSettings(), - new MainMenuSettings() }; } } diff --git a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs index e5cebd28e2..acb94a6a01 100644 --- a/osu.Game/Overlays/Settings/Sections/GameplaySection.cs +++ b/osu.Game/Overlays/Settings/Sections/GameplaySection.cs @@ -26,7 +26,6 @@ namespace osu.Game.Overlays.Settings.Sections Children = new Drawable[] { new GeneralSettings(), - new SongSelectSettings(), new ModsSettings(), }; } diff --git a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs index c1b4b0bbcb..4ade48031f 100644 --- a/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs +++ b/osu.Game/Overlays/Settings/Sections/GraphicsSection.cs @@ -23,7 +23,6 @@ namespace osu.Game.Overlays.Settings.Sections new RendererSettings(), new LayoutSettings(), new DetailSettings(), - new UserInterfaceSettings(), }; } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs similarity index 88% rename from osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs rename to osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 38c30ddd64..419d2e83ad 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/UserInterfaceSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -6,11 +6,11 @@ using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; -namespace osu.Game.Overlays.Settings.Sections.Graphics +namespace osu.Game.Overlays.Settings.Sections.UserInterface { - public class UserInterfaceSettings : SettingsSubsection + public class GeneralSettings : SettingsSubsection { - protected override string Header => "User Interface"; + protected override string Header => "General"; [BackgroundDependencyLoader] private void load(OsuConfigManager config) diff --git a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs similarity index 97% rename from osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs rename to osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs index 7682967d10..598b666642 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs @@ -7,7 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; -namespace osu.Game.Overlays.Settings.Sections.Audio +namespace osu.Game.Overlays.Settings.Sections.UserInterface { public class MainMenuSettings : SettingsSubsection { diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs similarity index 97% rename from osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs rename to osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs index b26876556e..c73a783d37 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/SongSelectSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/SongSelectSettings.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; -namespace osu.Game.Overlays.Settings.Sections.Gameplay +namespace osu.Game.Overlays.Settings.Sections.UserInterface { public class SongSelectSettings : SettingsSubsection { diff --git a/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.cs new file mode 100644 index 0000000000..718fea5f2b --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/UserInterfaceSection.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.Sprites; +using osu.Game.Overlays.Settings.Sections.UserInterface; + +namespace osu.Game.Overlays.Settings.Sections +{ + public class UserInterfaceSection : SettingsSection + { + public override string Header => "User Interface"; + + public override Drawable CreateIcon() => new SpriteIcon + { + Icon = FontAwesome.Solid.LayerGroup + }; + + public UserInterfaceSection() + { + Children = new Drawable[] + { + new GeneralSettings(), + new MainMenuSettings(), + new SongSelectSettings() + }; + } + } +} diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index e1bcdbbaf0..f05d82cb6c 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -23,6 +23,7 @@ namespace osu.Game.Overlays { new GeneralSection(), new GraphicsSection(), + new UserInterfaceSection(), new GameplaySection(), new AudioSection(), new SkinSection(), From e0a84ff1dc174ec206ff12d094639bc670c5b6c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 29 Nov 2020 22:03:56 +0100 Subject: [PATCH 4964/6909] Move hold-to-confirm setting back to gameplay section --- .../Settings/Sections/Gameplay/GeneralSettings.cs | 12 ++++++++++++ .../Sections/UserInterface/GeneralSettings.cs | 12 ------------ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index be464fa2b7..53f1a0b4ba 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -5,6 +5,7 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Scoring; namespace osu.Game.Overlays.Settings.Sections.Gameplay @@ -63,6 +64,12 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Always show key overlay", Current = config.GetBindable(OsuSetting.KeyOverlay) }, + new SettingsSlider + { + LabelText = "Hold-to-confirm activation time", + Current = config.GetBindable(OsuSetting.UIHoldActivationDelay), + KeyboardStep = 50 + }, new SettingsCheckbox { LabelText = "Positional hitsounds", @@ -95,5 +102,10 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }); } } + + private class TimeSlider : OsuSliderBar + { + public override string TooltipText => Current.Value.ToString("N0") + "ms"; + } } } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 419d2e83ad..797e00a147 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections.UserInterface { @@ -27,18 +26,7 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface LabelText = "Parallax", Current = config.GetBindable(OsuSetting.MenuParallax) }, - new SettingsSlider - { - LabelText = "Hold-to-confirm activation time", - Current = config.GetBindable(OsuSetting.UIHoldActivationDelay), - KeyboardStep = 50 - }, }; } - - private class TimeSlider : OsuSliderBar - { - public override string TooltipText => Current.Value.ToString("N0") + "ms"; - } } } From 3994cf082d835f9cb61dc687c2270bd800af2514 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Sun, 29 Nov 2020 20:59:02 -0500 Subject: [PATCH 4965/6909] add keybind for in game overlay --- osu.Game/Configuration/OsuConfigManager.cs | 2 +- .../Input/Bindings/GlobalActionContainer.cs | 4 +++ osu.Game/Screens/Play/HUDOverlay.cs | 31 +++++++++++-------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 89a6ee8b07..a07e446d2e 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -184,7 +184,7 @@ namespace osu.Game.Configuration return new TrackedSettings { new TrackedSetting(OsuSetting.MouseDisableButtons, v => new SettingDescription(!v, "gameplay mouse buttons", v ? "disabled" : "enabled", LookupKeyBindings(GlobalAction.ToggleGameplayMouseButtons))), - new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription(), $"cycle: shift-tab quick view: {LookupKeyBindings(GlobalAction.HoldForHUD)}")), + new TrackedSetting(OsuSetting.HUDVisibilityMode, m => new SettingDescription(m, "HUD Visibility", m.GetDescription(), $"cycle: {LookupKeyBindings(GlobalAction.ToggleInGameInterface)} quick view: {LookupKeyBindings(GlobalAction.HoldForHUD)}")), new TrackedSetting(OsuSetting.Scaling, m => new SettingDescription(m, "scaling", m.GetDescription())), new TrackedSetting(OsuSetting.Skin, m => { diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 74eb2b0126..a59d69e5b5 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -68,6 +68,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit), new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), + new KeyBinding(InputKey.I, GlobalAction.ToggleInGameInterface), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), @@ -145,6 +146,9 @@ namespace osu.Game.Input.Bindings [Description("Decrease scroll speed")] DecreaseScrollSpeed, + [Description("Toggle in-game interface")] + ToggleInGameInterface, + [Description("Select")] Select, diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index e83dded075..964e0d0536 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -282,20 +282,7 @@ namespace osu.Game.Screens.Play switch (e.Key) { case Key.Tab: - switch (configVisibilityMode.Value) - { - case HUDVisibilityMode.Never: - configVisibilityMode.Value = HUDVisibilityMode.HideDuringGameplay; - break; - case HUDVisibilityMode.HideDuringGameplay: - configVisibilityMode.Value = HUDVisibilityMode.Always; - break; - - case HUDVisibilityMode.Always: - configVisibilityMode.Value = HUDVisibilityMode.Never; - break; - } return true; } @@ -377,6 +364,24 @@ namespace osu.Game.Screens.Play holdingForHUD = true; updateVisibility(); return true; + + case GlobalAction.ToggleInGameInterface: + switch (configVisibilityMode.Value) + { + case HUDVisibilityMode.Never: + configVisibilityMode.Value = HUDVisibilityMode.HideDuringGameplay; + break; + + case HUDVisibilityMode.HideDuringGameplay: + configVisibilityMode.Value = HUDVisibilityMode.Always; + break; + + case HUDVisibilityMode.Always: + configVisibilityMode.Value = HUDVisibilityMode.Never; + break; + } + updateVisibility(); + return true; } return false; From a780a8bbd8db3e3e8bd8737480de1503df6f81b8 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Sun, 29 Nov 2020 21:52:58 -0500 Subject: [PATCH 4966/6909] forgot to remove something... sorry --- osu.Game/Screens/Play/HUDOverlay.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 964e0d0536..457706b8f5 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -273,24 +273,6 @@ namespace osu.Game.Screens.Play Progress.BindDrawableRuleset(drawableRuleset); } - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Repeat) return false; - - if (e.ShiftPressed) - { - switch (e.Key) - { - case Key.Tab: - - - return true; - } - } - - return base.OnKeyDown(e); - } - protected virtual SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter(); protected virtual SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter(); From 5d3a5081a0548f98d485c4dfe8d879e70536159d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 12:52:58 +0900 Subject: [PATCH 4967/6909] Remove use of HitObject in DHO constructors. --- .../Objects/Drawables/DrawableCatchHitObject.cs | 6 ++---- .../Objects/Drawables/DrawablePalpableCatchHitObject.cs | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 92b4f7cae2..1faa6a5b0f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.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.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.UI; @@ -19,12 +20,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; - protected DrawableCatchHitObject(CatchHitObject hitObject) + protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject) : base(hitObject) { - if (hitObject != null) - XBindable.Value = hitObject.X; - Anchor = Anchor.BottomLeft; } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index f44e290627..128d81aca4 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.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 osu.Framework.Graphics; @@ -31,7 +32,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected readonly Container ScaleContainer; - protected DrawablePalpableCatchHitObject(CatchHitObject h) + protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) : base(h) { Origin = Anchor.Centre; @@ -43,8 +44,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Origin = Anchor.Centre, Anchor = Anchor.Centre, }); - - IndexInBeatmap.Value = h.IndexInBeatmap; } [BackgroundDependencyLoader] From 7986d7802df3d39f976f9ff71cef2dea6fcedf60 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 12:58:14 +0900 Subject: [PATCH 4968/6909] Use `ApplyDefaults` in `TestSceneFruitObjects`. --- osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index c661977b03..160da75aa9 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -3,6 +3,8 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osuTK; @@ -55,9 +57,9 @@ namespace osu.Game.Rulesets.Catch.Tests protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d) { var hitObject = d.HitObject; + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 0 }); hitObject.StartTime = 1000000000000; hitObject.Scale = 1.5f; - hitObject.Samples.Clear(); // otherwise crash due to samples not loaded d.Anchor = Anchor.Centre; d.RelativePositionAxes = Axes.None; From 09b7ba41d6632da36896b2b79371aa54e601961e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 13:00:01 +0900 Subject: [PATCH 4969/6909] Consistently use readonly field for bindables. --- .../Objects/Drawables/DrawablePalpableCatchHitObject.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 128d81aca4..a3908f94b6 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -14,9 +14,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; - public Bindable HyperDash { get; } = new Bindable(); + public readonly Bindable HyperDash = new Bindable(); - public Bindable ScaleBindable { get; } = new Bindable(1); + public readonly Bindable ScaleBindable = new Bindable(1); public readonly Bindable IndexInBeatmap = new Bindable(); From 5e0e4e9db793af739f78794dfd9cab7268bdd8fc Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 13:06:04 +0900 Subject: [PATCH 4970/6909] Use private access modifier for `Border` field. --- osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs index a9a8f551ce..f7d931ad5b 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces public readonly Bindable VisualRepresentation = new Bindable(); public readonly Bindable HyperDash = new Bindable(); - public BorderPiece Border; + public BorderPiece Border { get; private set; } public FruitPiece() { From 6478bed431ec467b5c3579a3d264b8719a6b1f81 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Sun, 29 Nov 2020 23:14:43 -0500 Subject: [PATCH 4971/6909] Revert "forgot to remove something... sorry" This reverts commit a780a8bbd8db3e3e8bd8737480de1503df6f81b8. --- osu.Game/Screens/Play/HUDOverlay.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 457706b8f5..964e0d0536 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -273,6 +273,24 @@ namespace osu.Game.Screens.Play Progress.BindDrawableRuleset(drawableRuleset); } + protected override bool OnKeyDown(KeyDownEvent e) + { + if (e.Repeat) return false; + + if (e.ShiftPressed) + { + switch (e.Key) + { + case Key.Tab: + + + return true; + } + } + + return base.OnKeyDown(e); + } + protected virtual SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter(); protected virtual SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter(); From 9145557522e94e53aa4a0c74f0cc534d23329847 Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Sun, 29 Nov 2020 23:15:12 -0500 Subject: [PATCH 4972/6909] Revert "Revert "forgot to remove something... sorry"" This reverts commit 6478bed431ec467b5c3579a3d264b8719a6b1f81. --- osu.Game/Screens/Play/HUDOverlay.cs | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 964e0d0536..457706b8f5 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -273,24 +273,6 @@ namespace osu.Game.Screens.Play Progress.BindDrawableRuleset(drawableRuleset); } - protected override bool OnKeyDown(KeyDownEvent e) - { - if (e.Repeat) return false; - - if (e.ShiftPressed) - { - switch (e.Key) - { - case Key.Tab: - - - return true; - } - } - - return base.OnKeyDown(e); - } - protected virtual SkinnableAccuracyCounter CreateAccuracyCounter() => new SkinnableAccuracyCounter(); protected virtual SkinnableScoreCounter CreateScoreCounter() => new SkinnableScoreCounter(); From 6bea78619a9c59aefcbd47d935233fc61496031e Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 30 Nov 2020 13:33:29 +0900 Subject: [PATCH 4973/6909] Update comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Tests/Editing/EditorChangeHandlerTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs index 5064b0fd22..481cb3230e 100644 --- a/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs +++ b/osu.Game.Tests/Editing/EditorChangeHandlerTest.cs @@ -62,7 +62,7 @@ namespace osu.Game.Tests.Editing string hash = handler.CurrentStateHash; - // save a save without making any changes + // undo a change without saving handler.RestoreState(-1); Assert.That(originalHash, Is.EqualTo(handler.CurrentStateHash)); From 4228977c866becfb4710349cf86c654d166a4817 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 13:46:02 +0900 Subject: [PATCH 4974/6909] Store a DHO in `FruitPiece` to animate itself. --- .../Objects/Drawables/DrawableFruit.cs | 12 +------ .../Objects/Drawables/Pieces/FruitPiece.cs | 33 +++++++++++++------ 2 files changed, 24 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index e98f410724..4338d80345 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4); - private FruitPiece fruitPiece; - public DrawableFruit(CatchHitObject h) : base(h) { @@ -41,21 +39,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { ScaleContainer.Child = new SkinnableDrawable( new CatchSkinComponent(getComponent(VisualRepresentation.Value)), - _ => fruitPiece = new FruitPiece + _ => new FruitPiece { VisualRepresentation = { BindTarget = VisualRepresentation }, HyperDash = { BindTarget = HyperDash }, }); } - protected override void Update() - { - base.Update(); - - if (fruitPiece != null) - fruitPiece.Border.Alpha = (float)Math.Clamp((StartTimeBindable.Value - Time.Current) / 500, 0, 1); - } - private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation) { switch (hitObjectVisualRepresentation) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs index f7d931ad5b..25fc53ce21 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.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; +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; namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { @@ -18,26 +21,36 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces public readonly Bindable VisualRepresentation = new Bindable(); public readonly Bindable HyperDash = new Bindable(); - public BorderPiece Border { get; private set; } + [CanBeNull] + private DrawableCatchHitObject drawableHitObject; + + [CanBeNull] + private BorderPiece borderPiece; public FruitPiece() { RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load() + [BackgroundDependencyLoader(permitNulls: true)] + private void load([CanBeNull] DrawableHitObject drawable) { - AddRangeInternal(new[] - { - getFruitFor(VisualRepresentation.Value), - Border = new BorderPiece(), - }); + drawableHitObject = (DrawableCatchHitObject)drawable; + + AddInternal(getFruitFor(VisualRepresentation.Value)); + + // if it is not part of a DHO, the border is always invisible. + if (drawableHitObject != null) + AddInternal(borderPiece = new BorderPiece()); if (HyperDash.Value) - { AddInternal(new HyperBorderPiece()); - } + } + + protected override void Update() + { + if (borderPiece != null && drawableHitObject.HitObject != null) + borderPiece.Alpha = (float)Math.Clamp((drawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1); } private Drawable getFruitFor(FruitVisualRepresentation representation) From 8528b2687f922d6013c720bf8ca6cb4990e95ea8 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 14:24:50 +0900 Subject: [PATCH 4975/6909] Fix possible null reference. --- osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs index 25fc53ce21..31487ee407 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces protected override void Update() { - if (borderPiece != null && drawableHitObject.HitObject != null) + if (borderPiece != null && drawableHitObject?.HitObject != null) borderPiece.Alpha = (float)Math.Clamp((drawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1); } From 73990a6674a4d4037574bf569c3b361b857d9dc7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 15:20:52 +0900 Subject: [PATCH 4976/6909] Fix osu!catch combo counter not showing after 1 combo --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index abbdeacd9a..f2f783a11c 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -55,6 +55,11 @@ namespace osu.Game.Rulesets.Catch.UI HitObjectContainer, CatcherArea, }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); NewResult += onNewResult; RevertResult += onRevertResult; From 9fbfb1aa9fd2fd766c066c0b987ee4089b5c2066 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 15:22:55 +0900 Subject: [PATCH 4977/6909] Add comment explaining requirement --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index f2f783a11c..9df32d8d36 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -61,6 +61,7 @@ namespace osu.Game.Rulesets.Catch.UI { base.LoadComplete(); + // these subscriptions need to be done post constructor to ensure externally bound components have a chance to populate required fields (ScoreProcessor / ComboAtJudgement in this case). NewResult += onNewResult; RevertResult += onRevertResult; } From 809caaa44c72f72f5b64bf67a537ff6303d0a16a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 15:39:08 +0900 Subject: [PATCH 4978/6909] Use standard switch syntax (preferred for now) --- .../TestSceneDrawableScrollingRuleset.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 257ae10d82..8da2b58c1e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -302,14 +302,21 @@ namespace osu.Game.Tests.Visual.Gameplay TimeRange.Value = time_range; } - public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) => - h switch + public override DrawableHitObject CreateDrawableRepresentation(TestHitObject h) + { + switch (h) { - TestPooledHitObject _ => null, - TestPooledParentHitObject _ => null, - TestParentHitObject p => new DrawableTestParentHitObject(p), - _ => new DrawableTestHitObject(h), - }; + case TestPooledHitObject _: + case TestPooledParentHitObject _: + return null; + + case TestParentHitObject p: + return new DrawableTestParentHitObject(p); + + default: + return new DrawableTestHitObject(h); + } + } protected override PassThroughInputManager CreateInputManager() => new PassThroughInputManager(); From 31cfaefdeb57bb967555c225804db5309e979592 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 15:39:43 +0900 Subject: [PATCH 4979/6909] Move private functions in line with others --- .../TestSceneDrawableScrollingRuleset.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs index 8da2b58c1e..9931ee4a45 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneDrawableScrollingRuleset.cs @@ -90,22 +90,6 @@ namespace osu.Game.Tests.Visual.Gameplay assertChildPosition(5); } - private void assertDead(int index) => AddAssert($"hitobject {index} is dead", () => getDrawableHitObject(index) == null); - - private void assertHeight(int index) => AddAssert($"hitobject {index} height", () => - { - var d = getDrawableHitObject(index); - return d != null && Precision.AlmostEquals(d.DrawHeight, yScale * (float)(d.HitObject.Duration / time_range), 0.1f); - }); - - private void assertChildPosition(int index) => AddAssert($"hitobject {index} child position", () => - { - var d = getDrawableHitObject(index); - return d is DrawableTestParentHitObject && Precision.AlmostEquals( - d.NestedHitObjects.First().DrawPosition.Y, - yScale * (float)((TestParentHitObject)d.HitObject).ChildTimeOffset / time_range, 0.1f); - }); - [Test] public void TestRelativeBeatLengthScaleSingleTimingPoint() { @@ -220,6 +204,22 @@ namespace osu.Game.Tests.Visual.Gameplay private float yScale => drawableRuleset.Playfield.HitObjectContainer.DrawHeight; + private void assertDead(int index) => AddAssert($"hitobject {index} is dead", () => getDrawableHitObject(index) == null); + + private void assertHeight(int index) => AddAssert($"hitobject {index} height", () => + { + var d = getDrawableHitObject(index); + return d != null && Precision.AlmostEquals(d.DrawHeight, yScale * (float)(d.HitObject.Duration / time_range), 0.1f); + }); + + private void assertChildPosition(int index) => AddAssert($"hitobject {index} child position", () => + { + var d = getDrawableHitObject(index); + return d is DrawableTestParentHitObject && Precision.AlmostEquals( + d.NestedHitObjects.First().DrawPosition.Y, + yScale * (float)((TestParentHitObject)d.HitObject).ChildTimeOffset / time_range, 0.1f); + }); + private void assertPosition(int index, float relativeY) => AddAssert($"hitobject {index} at {relativeY}", () => Precision.AlmostEquals(getDrawableHitObject(index)?.DrawPosition.Y ?? -1, yScale * relativeY)); From 274565998653284abcdef19fc4be19f8e9dc937b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 15:54:20 +0900 Subject: [PATCH 4980/6909] Reword and xmldoc some comments --- .../UI/Scrolling/ScrollingHitObjectContainer.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 71f8f95300..6a77597916 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -17,11 +17,14 @@ namespace osu.Game.Rulesets.UI.Scrolling private readonly IBindable timeRange = new BindableDouble(); private readonly IBindable direction = new Bindable(); - // The lifetime of a hit object in this will be computed in next update. + /// + /// Hit objects which require lifetime computation in the next update call. + /// private readonly HashSet toComputeLifetime = new HashSet(); - // The layout (length if IHasDuration, and nested object positions) of a hit object *not* in this set will be computed in next updated. - // Only objects in `AliveObjects` are considered, to prevent a massive recomputation when scrolling speed or something changes. + /// + /// A set containing all which have an up-to-date layout. + /// private readonly HashSet layoutComputed = new HashSet(); [Resolved] @@ -223,8 +226,7 @@ namespace osu.Game.Rulesets.UI.Scrolling toComputeLifetime.Clear(); - // An assumption is that this update won't affect lifetime, - // but this is satisfied in practice because otherwise the hit object won't be aligned to its `StartTime`. + // only AliveObjects need to be considered for layout (reduces overhead in the case of scroll speed changes). foreach (var obj in AliveObjects) { if (layoutComputed.Contains(obj)) From e14db45374a2a60424aad8586edd1851cb0a9040 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 16:09:38 +0900 Subject: [PATCH 4981/6909] Reorder settings to (probably) feel more correct --- osu.Game/Overlays/SettingsOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index f05d82cb6c..31d188b545 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -23,11 +23,11 @@ namespace osu.Game.Overlays { new GeneralSection(), new GraphicsSection(), + new AudioSection(), + new InputSection(createSubPanel(new KeyBindingPanel())), new UserInterfaceSection(), new GameplaySection(), - new AudioSection(), new SkinSection(), - new InputSection(createSubPanel(new KeyBindingPanel())), new OnlineSection(), new MaintenanceSection(), new DebugSection(), From 55c8aa5d5f7a3ccd83dcc6f0cf574f82f0f0e6e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 16:14:15 +0900 Subject: [PATCH 4982/6909] Move menu cursor size to UI section --- osu.Game/Overlays/Settings/Sections/SizeSlider.cs | 15 +++++++++++++++ .../Overlays/Settings/Sections/SkinSection.cs | 11 ----------- .../Sections/UserInterface/GeneralSettings.cs | 6 ++++++ 3 files changed, 21 insertions(+), 11 deletions(-) create mode 100644 osu.Game/Overlays/Settings/Sections/SizeSlider.cs diff --git a/osu.Game/Overlays/Settings/Sections/SizeSlider.cs b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs new file mode 100644 index 0000000000..101d8f43f7 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/SizeSlider.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Overlays.Settings.Sections +{ + /// + /// A slider intended to show a "size" multiplier number, where 1x is 1.0. + /// + internal class SizeSlider : OsuSliderBar + { + public override string TooltipText => Current.Value.ToString(@"0.##x"); + } +} diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 3e7068f1ff..5898482e4a 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -54,12 +54,6 @@ namespace osu.Game.Overlays.Settings.Sections skinDropdown = new SkinSettingsDropdown(), new ExportSkinButton(), new SettingsSlider - { - LabelText = "Menu cursor size", - Current = config.GetBindable(OsuSetting.MenuCursorSize), - KeyboardStep = 0.01f - }, - new SettingsSlider { LabelText = "Gameplay cursor size", Current = config.GetBindable(OsuSetting.GameplayCursorSize), @@ -136,11 +130,6 @@ namespace osu.Game.Overlays.Settings.Sections Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray()); } - private class SizeSlider : OsuSliderBar - { - public override string TooltipText => Current.Value.ToString(@"0.##x"); - } - private class SkinSettingsDropdown : SettingsDropdown { protected override OsuDropdown CreateDropdown() => new SkinDropdownControl(); diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index 797e00a147..da50f67d5f 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -21,6 +21,12 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface LabelText = "Rotate cursor when dragging", Current = config.GetBindable(OsuSetting.CursorRotation) }, + new SettingsSlider + { + LabelText = "Menu cursor size", + Current = config.GetBindable(OsuSetting.MenuCursorSize), + KeyboardStep = 0.01f + }, new SettingsCheckbox { LabelText = "Parallax", From 4e1e45f3e71ed3353489d3f7ec5e64619814e225 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 16:15:35 +0900 Subject: [PATCH 4983/6909] Move hold-to-confirm back to UI section --- .../Settings/Sections/Gameplay/GeneralSettings.cs | 12 ------------ .../Sections/UserInterface/GeneralSettings.cs | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs index 53f1a0b4ba..be464fa2b7 100644 --- a/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Gameplay/GeneralSettings.cs @@ -5,7 +5,6 @@ using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; -using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets.Scoring; namespace osu.Game.Overlays.Settings.Sections.Gameplay @@ -64,12 +63,6 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay LabelText = "Always show key overlay", Current = config.GetBindable(OsuSetting.KeyOverlay) }, - new SettingsSlider - { - LabelText = "Hold-to-confirm activation time", - Current = config.GetBindable(OsuSetting.UIHoldActivationDelay), - KeyboardStep = 50 - }, new SettingsCheckbox { LabelText = "Positional hitsounds", @@ -102,10 +95,5 @@ namespace osu.Game.Overlays.Settings.Sections.Gameplay }); } } - - private class TimeSlider : OsuSliderBar - { - public override string TooltipText => Current.Value.ToString("N0") + "ms"; - } } } diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs index da50f67d5f..19adfc5dd9 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/GeneralSettings.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections.UserInterface { @@ -32,7 +33,18 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface LabelText = "Parallax", Current = config.GetBindable(OsuSetting.MenuParallax) }, + new SettingsSlider + { + LabelText = "Hold-to-confirm activation time", + Current = config.GetBindable(OsuSetting.UIHoldActivationDelay), + KeyboardStep = 50 + }, }; } + + private class TimeSlider : OsuSliderBar + { + public override string TooltipText => Current.Value.ToString("N0") + "ms"; + } } } From a4e061cb11eeaca6644abca2e3bfee55eacd2c96 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 17:18:29 +0900 Subject: [PATCH 4984/6909] Remove semi-transparent backgrounds from settings and notifications overlays I tried also updating the colours to the "new" versions from designs but they don't match due to colour profile differences (so I'm not yet sure if they are correct or not) and also don't look great without all the UI elements also being updated. --- osu.Game/Overlays/NotificationOverlay.cs | 4 ++-- osu.Game/Overlays/Settings/Sidebar.cs | 3 ++- osu.Game/Overlays/SettingsOverlay.cs | 2 -- osu.Game/Overlays/SettingsPanel.cs | 7 ++++--- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index b5714fbcae..774b001224 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -13,6 +13,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Threading; +using osu.Game.Graphics; namespace osu.Game.Overlays { @@ -44,8 +45,7 @@ namespace osu.Game.Overlays new Box { RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f + Colour = OsuColour.Gray(0.05f), }, new OsuScrollContainer { diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index 031ecaae46..f548f933e2 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; +using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; using osuTK.Graphics; @@ -32,7 +33,7 @@ namespace osu.Game.Overlays.Settings { new Box { - Colour = Color4.Black, + Colour = OsuColour.Gray(0.02f), RelativeSizeAxes = Axes.Both, }, new SidebarScrollContainer diff --git a/osu.Game/Overlays/SettingsOverlay.cs b/osu.Game/Overlays/SettingsOverlay.cs index e1bcdbbaf0..e49885cdf8 100644 --- a/osu.Game/Overlays/SettingsOverlay.cs +++ b/osu.Game/Overlays/SettingsOverlay.cs @@ -61,7 +61,6 @@ namespace osu.Game.Overlays switch (state.NewValue) { case Visibility.Visible: - Background.FadeTo(0.9f, 300, Easing.OutQuint); Sidebar?.FadeColour(Color4.DarkGray, 300, Easing.OutQuint); SectionsContainer.FadeOut(300, Easing.OutQuint); @@ -69,7 +68,6 @@ namespace osu.Game.Overlays break; case Visibility.Hidden: - Background.FadeTo(0.6f, 500, Easing.OutQuint); Sidebar?.FadeColour(Color4.White, 300, Easing.OutQuint); SectionsContainer.FadeIn(500, Easing.OutQuint); diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 2948231c4b..fea4b0738d 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -12,6 +12,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.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Settings; @@ -72,8 +73,8 @@ namespace osu.Game.Overlays Origin = Anchor.TopRight, Scale = new Vector2(2, 1), // over-extend to the left for transitions RelativeSizeAxes = Axes.Both, - Colour = Color4.Black, - Alpha = 0.6f, + Colour = OsuColour.Gray(0.05f), + Alpha = 1, }, SectionsContainer = new SettingsSectionsContainer { @@ -214,7 +215,7 @@ namespace osu.Game.Overlays base.UpdateAfterChildren(); // no null check because the usage of this class is strict - HeaderBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y * 0.5f; + HeaderBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y * 1; } } } From 7ac2fba1273e8d6d495e76ba9f6e4c3332724c68 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Nov 2020 17:44:58 +0900 Subject: [PATCH 4985/6909] More reordering of public vs private methods --- .../Scrolling/ScrollingHitObjectContainer.cs | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 6a77597916..3a5e3c098f 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -150,16 +150,9 @@ namespace osu.Game.Rulesets.UI.Scrolling } } - /// - /// Make this lifetime and layout computed in next update. - /// - private void invalidateHitObject(DrawableHitObject hitObject) - { - // Lifetime computation is delayed until next update because - // when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed. - toComputeLifetime.Add(hitObject); - layoutComputed.Remove(hitObject); - } + protected override void OnAdd(DrawableHitObject drawableHitObject) => onAddRecursive(drawableHitObject); + + protected override void OnRemove(DrawableHitObject drawableHitObject) => onRemoveRecursive(drawableHitObject); private void onAddRecursive(DrawableHitObject hitObject) { @@ -171,8 +164,6 @@ namespace osu.Game.Rulesets.UI.Scrolling onAddRecursive(nested); } - protected override void OnAdd(DrawableHitObject drawableHitObject) => onAddRecursive(drawableHitObject); - private void onRemoveRecursive(DrawableHitObject hitObject) { toComputeLifetime.Remove(hitObject); @@ -184,7 +175,16 @@ namespace osu.Game.Rulesets.UI.Scrolling onRemoveRecursive(nested); } - protected override void OnRemove(DrawableHitObject drawableHitObject) => onRemoveRecursive(drawableHitObject); + /// + /// Make this lifetime and layout computed in next update. + /// + private void invalidateHitObject(DrawableHitObject hitObject) + { + // Lifetime computation is delayed until next update because + // when the hit object is not pooled this container is not loaded here and `scrollLength` cannot be computed. + toComputeLifetime.Add(hitObject); + layoutComputed.Remove(hitObject); + } private float scrollLength; From bf2c6dc241d561c545dae3dbff0aa35cd8e306ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 17:49:55 +0900 Subject: [PATCH 4986/6909] Remove unused usings rider couldn't see --- osu.Game/Overlays/NotificationOverlay.cs | 1 - osu.Game/Overlays/Settings/Sidebar.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Overlays/NotificationOverlay.cs b/osu.Game/Overlays/NotificationOverlay.cs index 774b001224..d51d964fc4 100644 --- a/osu.Game/Overlays/NotificationOverlay.cs +++ b/osu.Game/Overlays/NotificationOverlay.cs @@ -6,7 +6,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Notifications; -using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.Containers; using System; diff --git a/osu.Game/Overlays/Settings/Sidebar.cs b/osu.Game/Overlays/Settings/Sidebar.cs index f548f933e2..4ca6e2ec42 100644 --- a/osu.Game/Overlays/Settings/Sidebar.cs +++ b/osu.Game/Overlays/Settings/Sidebar.cs @@ -12,7 +12,6 @@ using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays.Settings { From fe48b2279c39dee39de905c63e052bc88c6134d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 17:43:53 +0900 Subject: [PATCH 4987/6909] Adjust various paddings and spacings in settings to make them easier to visually parse --- osu.Game/Overlays/Settings/SettingsSection.cs | 15 +++++++++------ osu.Game/Overlays/Settings/SettingsSubsection.cs | 4 ++-- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 97e4ba9da7..8b4821398f 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.Settings public virtual IEnumerable FilterTerms => new[] { Header }; private const int header_size = 26; - private const int header_margin = 25; + private const int margin = 20; private const int border_size = 2; public bool MatchingFilter @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.Settings protected SettingsSection() { - Margin = new MarginPadding { Top = 20 }; + Margin = new MarginPadding { Top = margin }; AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; @@ -46,10 +46,9 @@ namespace osu.Game.Overlays.Settings { Margin = new MarginPadding { - Top = header_size + header_margin + Top = header_size }, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 30), AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, }; @@ -70,7 +69,7 @@ namespace osu.Game.Overlays.Settings { Padding = new MarginPadding { - Top = 20 + border_size, + Top = margin + border_size, Bottom = 10, }, RelativeSizeAxes = Axes.X, @@ -82,7 +81,11 @@ namespace osu.Game.Overlays.Settings Font = OsuFont.GetFont(size: header_size), Text = Header, Colour = colours.Yellow, - Margin = new MarginPadding { Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS } + Margin = new MarginPadding + { + Left = SettingsPanel.CONTENT_MARGINS, + Right = SettingsPanel.CONTENT_MARGINS + } }, FlowContent } diff --git a/osu.Game/Overlays/Settings/SettingsSubsection.cs b/osu.Game/Overlays/Settings/SettingsSubsection.cs index b096c146a6..1b82d973e9 100644 --- a/osu.Game/Overlays/Settings/SettingsSubsection.cs +++ b/osu.Game/Overlays/Settings/SettingsSubsection.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Settings FlowContent = new FillFlowContainer { Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), + Spacing = new Vector2(0, 8), RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, }; @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.Settings new OsuSpriteText { Text = Header.ToUpperInvariant(), - Margin = new MarginPadding { Bottom = 10, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, + Margin = new MarginPadding { Vertical = 30, Left = SettingsPanel.CONTENT_MARGINS, Right = SettingsPanel.CONTENT_MARGINS }, Font = OsuFont.GetFont(weight: FontWeight.Bold), }, FlowContent From a3ef858f3a03f7c694348d7674a72d963f3dd162 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 30 Nov 2020 17:56:04 +0900 Subject: [PATCH 4988/6909] Remove unnecessary multiplication --- osu.Game/Overlays/SettingsPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index fea4b0738d..7a5a586f67 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -215,7 +215,7 @@ namespace osu.Game.Overlays base.UpdateAfterChildren(); // no null check because the usage of this class is strict - HeaderBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y * 1; + HeaderBackground.Alpha = -ExpandableHeader.Y / ExpandableHeader.LayoutSize.Y; } } } From 965cc1f511568b07231c1b189ebd9066d339568e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 17:57:25 +0900 Subject: [PATCH 4989/6909] Remove unnecessary usings #2 --- osu.Game/Overlays/Settings/SettingsSection.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsSection.cs b/osu.Game/Overlays/Settings/SettingsSection.cs index 8b4821398f..4143605c28 100644 --- a/osu.Game/Overlays/Settings/SettingsSection.cs +++ b/osu.Game/Overlays/Settings/SettingsSection.cs @@ -1,16 +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 osuTK; -using osuTK.Graphics; +using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using System.Collections.Generic; -using System.Linq; +using osuTK.Graphics; namespace osu.Game.Overlays.Settings { From 731e689f2da41dcaa040b5dde209f02b1306754d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 18:07:37 +0900 Subject: [PATCH 4990/6909] Add summary tags to the doc comments --- osu.Game/Utils/StatelessRNG.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Utils/StatelessRNG.cs b/osu.Game/Utils/StatelessRNG.cs index 8d08b26ca4..d78d55cc09 100644 --- a/osu.Game/Utils/StatelessRNG.cs +++ b/osu.Game/Utils/StatelessRNG.cs @@ -3,7 +3,9 @@ namespace osu.Game.Utils { + /// /// Provides a fast stateless function that can be used in randomly-looking visual elements. + /// public static class StatelessRNG { private static ulong mix(ulong x) @@ -19,7 +21,9 @@ namespace osu.Game.Utils } } + /// /// Compute an integer from given seed and series number. + /// /// /// The seed value of this random number generator. /// @@ -30,7 +34,9 @@ namespace osu.Game.Utils public static ulong Get(int seed, int series = 0) => unchecked(mix(((ulong)(uint)series << 32) | ((uint)seed ^ 0x12345678))); + /// /// Compute a floating point value between 0 and 1 (excluding 1) from given seed and series number. + /// /// /// The seed value of this random number generator. /// From fdef6e479c750b253c619e8c8e202bc7ba392a0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Nov 2020 18:28:04 +0900 Subject: [PATCH 4991/6909] Remove 1000ms offset and adjust comment --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 243092d067..975b444699 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -176,8 +176,8 @@ namespace osu.Game.Rulesets.Osu.UI public OsuHitObjectLifetimeEntry(HitObject hitObject) : base(hitObject) { - // Arbitrary lifetime end to prevent past objects in idle states remaining alive in non-frame-stable contexts. - LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss) + 1000; + // Prevent past objects in idles states from remaining alive as their end times are skipped in non-frame-stable contexts. + LifetimeEnd = HitObject.GetEndTime() + HitObject.HitWindows.WindowFor(HitResult.Miss); } protected override double InitialLifetimeOffset => ((OsuHitObject)HitObject).TimePreempt; From 3ad2eeaff551b279e5db2b33ac0d0a0087544d1f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 18:35:30 +0900 Subject: [PATCH 4992/6909] Fix outdated xmldoc --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index 9d7dc7b8f7..c0eb891f5e 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -85,7 +85,7 @@ namespace osu.Game.Rulesets.Edit } /// - /// Updates the position of this to a new screen-space position. + /// Updates the time and position of this based on the provided snap information. /// /// The snap result information. public virtual void UpdateTimeAndPosition(SnapResult result) From afb8eb636ddc4cd589755f796f1b8bad9e5adb30 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Nov 2020 18:40:22 +0900 Subject: [PATCH 4993/6909] Apply simple PR reviews --- osu.Game/Skinning/IPooledSampleProvider.cs | 10 +++++++++- osu.Game/Skinning/PoolableSkinnableSample.cs | 4 ++++ osu.Game/Skinning/SkinnableSound.cs | 6 +++--- 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs index 3dc0b5375d..5dbbadcc8a 100644 --- a/osu.Game/Skinning/IPooledSampleProvider.cs +++ b/osu.Game/Skinning/IPooledSampleProvider.cs @@ -6,8 +6,16 @@ using osu.Game.Audio; namespace osu.Game.Skinning { - public interface IPooledSampleProvider + /// + /// Provides pooled samples to be used by s. + /// + internal interface IPooledSampleProvider { + /// + /// Retrieves a from a pool. + /// + /// The describing the sample to retrieve.. + /// The . [CanBeNull] PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); } diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index adc58ee94e..7e885b20d1 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -138,6 +138,8 @@ namespace osu.Game.Skinning } } + #region Re-expose AudioContainer + public BindableNumber Volume => sampleContainer.Volume; public BindableNumber Balance => sampleContainer.Balance; @@ -159,5 +161,7 @@ namespace osu.Game.Skinning public IBindable AggregateFrequency => sampleContainer.AggregateFrequency; public IBindable AggregateTempo => sampleContainer.AggregateTempo; + + #endregion } } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 24dddaf758..46c2e4b749 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -40,7 +40,7 @@ namespace osu.Game.Skinning private ISampleStore sampleStore { get; set; } [Resolved(CanBeNull = true)] - private IPooledSampleProvider pooledProvider { get; set; } + private IPooledSampleProvider samplePool { get; set; } /// /// Creates a new . @@ -145,7 +145,7 @@ namespace osu.Game.Skinning foreach (var s in samples) { - var sample = pooledProvider?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); + var sample = samplePool?.GetPooledSample(s) ?? new PoolableSkinnableSample(s); sample.Looping = Looping; sample.Volume.Value = s.Volume / 100.0; @@ -176,7 +176,7 @@ namespace osu.Game.Skinning => SamplesContainer.RemoveAllAdjustments(type); /// - /// Whether any samples currently playing. + /// Whether any samples are currently playing. /// public bool IsPlaying => SamplesContainer.Any(s => s.Playing); From 51bddd4a0ff00fce883583852a2d225d5b41117c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 18:46:28 +0900 Subject: [PATCH 4994/6909] Rename functions, and add NextInt. --- osu.Game/Utils/StatelessRNG.cs | 42 +++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/osu.Game/Utils/StatelessRNG.cs b/osu.Game/Utils/StatelessRNG.cs index d78d55cc09..d316f718e3 100644 --- a/osu.Game/Utils/StatelessRNG.cs +++ b/osu.Game/Utils/StatelessRNG.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; + namespace osu.Game.Utils { /// @@ -22,7 +24,7 @@ namespace osu.Game.Utils } /// - /// Compute an integer from given seed and series number. + /// Generate a random 64-bit unsigned integer from given seed. /// /// /// The seed value of this random number generator. @@ -31,11 +33,39 @@ namespace osu.Game.Utils /// The series number. /// Different values are computed for the same seed in different series. /// - public static ulong Get(int seed, int series = 0) => - unchecked(mix(((ulong)(uint)series << 32) | ((uint)seed ^ 0x12345678))); + public static ulong NextUlong(int seed, int series = 0) + { + unchecked + { + // + var combined = ((ulong)(uint)series << 32) | (uint)seed; + // The xor operation is to not map (0, 0) to 0. + return mix(combined ^ 0x12345678); + } + } /// - /// Compute a floating point value between 0 and 1 (excluding 1) from given seed and series number. + /// Generate a random integer in range [0, maxValue) from given seed. + /// + /// + /// The number of possible results. + /// + /// + /// The seed value of this random number generator. + /// + /// + /// The series number. + /// Different values are computed for the same seed in different series. + /// + public static int NextInt(int maxValue, int seed, int series = 0) + { + if (maxValue <= 0) throw new ArgumentOutOfRangeException(nameof(maxValue)); + + return (int)(NextUlong(seed, series) % (ulong)maxValue); + } + + /// + /// Compute a random floating point value between 0 and 1 (excluding 1) from given seed and series number. /// /// /// The seed value of this random number generator. @@ -44,7 +74,7 @@ namespace osu.Game.Utils /// The series number. /// Different values are computed for the same seed in different series. /// - public static float GetSingle(int seed, int series = 0) => - (float)(Get(seed, series) & ((1 << 24) - 1)) / (1 << 24); // float has 24-bit precision + public static float NextSingle(int seed, int series = 0) => + (float)(NextUlong(seed, series) & ((1 << 24) - 1)) / (1 << 24); // float has 24-bit precision } } From 05aaa377e74078c0e4aa3f591066a0e3644e4e51 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 19:02:49 +0900 Subject: [PATCH 4995/6909] Don't use CreateDrawableRepresentation in CatcherArea --- .../TestSceneCatcherArea.cs | 1 - osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 1 - osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 17 +++++++++++++---- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index e055f08dc2..2d46cbdbbd 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -103,7 +103,6 @@ namespace osu.Game.Rulesets.Catch.Tests { Anchor = Anchor.Centre, Origin = Anchor.TopCentre, - CreateDrawableRepresentation = ((DrawableRuleset)catchRuleset.CreateInstance().CreateDrawableRulesetWith(new CatchBeatmap())).CreateDrawableRepresentation }, }); } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 9df32d8d36..bab8356ac4 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -42,7 +42,6 @@ namespace osu.Game.Rulesets.Catch.UI CatcherArea = new CatcherArea(difficulty) { - CreateDrawableRepresentation = createDrawableRepresentation, ExplodingFruitTarget = explodingFruitContainer, Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index ad79a23279..085af79689 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -10,7 +10,6 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osuTK; @@ -21,8 +20,6 @@ namespace osu.Game.Rulesets.Catch.UI { public const float CATCHER_SIZE = 106.75f; - public Func> CreateDrawableRepresentation; - public readonly Catcher MovableCatcher; private readonly CatchComboDisplay comboDisplay; @@ -72,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI if (result.IsHit && hitObject is DrawablePalpableCatchHitObject fruit) { // create a new (cloned) fruit to stay on the plate. the original is faded out immediately. - var caughtFruit = (DrawableCatchHitObject)CreateDrawableRepresentation?.Invoke(fruit.HitObject); + var caughtFruit = createCaughtFruit(fruit); if (caughtFruit == null) return; @@ -127,5 +124,17 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay.X = MovableCatcher.X; } + + private DrawableCatchHitObject createCaughtFruit(DrawablePalpableCatchHitObject hitObject) + { + return hitObject.HitObject switch + { + Banana banana => new DrawableBanana(banana), + Fruit fruit => new DrawableFruit(fruit), + TinyDroplet tiny => new DrawableTinyDroplet(tiny), + Droplet droplet => new DrawableDroplet(droplet), + _ => null + }; + } } } From 94fd607a7c548289147008dad8b43a14b1e3a62c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 19:04:09 +0900 Subject: [PATCH 4996/6909] Use hit object pooling for `Droplet` and `TinyDroplet`. --- .../Objects/Drawables/DrawableDroplet.cs | 8 +++++++- .../Objects/Drawables/DrawableTinyDroplet.cs | 9 ++++++++- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 8 ++++++++ osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 6 ------ 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 9e76265394..06ecd44488 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.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.Graphics; using osu.Framework.Utils; @@ -13,7 +14,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public override bool StaysOnPlate => false; - public DrawableDroplet(CatchHitObject h) + public DrawableDroplet() + : this(null) + { + } + + public DrawableDroplet([CanBeNull] CatchHitObject h) : base(h) { } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs index 8c4d821b4a..8f5a04dfda 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableTinyDroplet.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 JetBrains.Annotations; + namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableTinyDroplet : DrawableDroplet { protected override float ScaleFactor => base.ScaleFactor / 2; - public DrawableTinyDroplet(TinyDroplet h) + public DrawableTinyDroplet() + : this(null) + { + } + + public DrawableTinyDroplet([CanBeNull] TinyDroplet h) : base(h) { } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index bab8356ac4..da909b49c9 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -2,6 +2,7 @@ // 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.Game.Beatmaps; @@ -56,6 +57,13 @@ namespace osu.Game.Rulesets.Catch.UI }; } + [BackgroundDependencyLoader] + private void load() + { + RegisterPool(1); + RegisterPool(1); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index ebe45aa3ab..8534645947 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -55,12 +55,6 @@ namespace osu.Game.Rulesets.Catch.UI case BananaShower shower: return new DrawableBananaShower(shower, CreateDrawableRepresentation); - - case TinyDroplet tiny: - return new DrawableTinyDroplet(tiny); - - case Droplet droplet: - return new DrawableDroplet(droplet); } return null; From b76ae525b27d5cf0173a3714f170fbf0844b98c0 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 19:07:50 +0900 Subject: [PATCH 4997/6909] Use hit object pooling for `Fruit` and `Banana`. --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs | 2 -- .../Objects/Drawables/DrawableBanana.cs | 8 +++++++- .../Objects/Drawables/DrawableFruit.cs | 8 +++++++- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 2 ++ osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 6 ------ 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 2d46cbdbbd..c12f38723b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -10,14 +10,12 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; -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; namespace osu.Game.Rulesets.Catch.Tests { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index efb0958a3a..fb982bbdab 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.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.Graphics; using osu.Framework.Utils; @@ -10,7 +11,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana; - public DrawableBanana(Banana h) + public DrawableBanana() + : this(null) + { + } + + public DrawableBanana([CanBeNull] Banana h) : base(h) { } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 4338d80345..68cb649b66 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.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.Allocation; using osu.Framework.Bindables; using osu.Framework.Utils; @@ -16,7 +17,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4); - public DrawableFruit(CatchHitObject h) + public DrawableFruit() + : this(null) + { + } + + public DrawableFruit([CanBeNull] Fruit h) : base(h) { } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index da909b49c9..7cddec100f 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -62,6 +62,8 @@ namespace osu.Game.Rulesets.Catch.UI { RegisterPool(1); RegisterPool(1); + RegisterPool(1); + RegisterPool(1); } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 8534645947..ecc37549bf 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -44,12 +44,6 @@ namespace osu.Game.Rulesets.Catch.UI { switch (h) { - case Banana banana: - return new DrawableBanana(banana); - - case Fruit fruit: - return new DrawableFruit(fruit); - case JuiceStream stream: return new DrawableJuiceStream(stream, CreateDrawableRepresentation); From 9611aaf09eb0c9653b2c79d65a062fa2dfe572d4 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 19:19:14 +0900 Subject: [PATCH 4998/6909] Use hit object pooling for `JuiceStream`. - Use `Clear(false)` to not dispose pooled children. - Don't set nested DHO `Origin`. - Simplify the layout (remove custom `Origin`). --- .../TestSceneDrawableHitObjects.cs | 2 +- .../Objects/Drawables/DrawableJuiceStream.cs | 29 +++++-------------- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 1 + .../UI/DrawableCatchRuleset.cs | 3 -- 4 files changed, 9 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index d35f828e28..3e4995482d 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -136,7 +136,7 @@ namespace osu.Game.Rulesets.Catch.Tests if (juice.NestedHitObjects.Last() is CatchHitObject tail) tail.LastInCombo = true; // usually the (Catch)BeatmapProcessor would do this for us when necessary - addToPlayfield(new DrawableJuiceStream(juice, drawableRuleset.CreateDrawableRepresentation)); + addToPlayfield(new DrawableJuiceStream(juice)); } private void spawnBananas(bool hit = false) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs index a7a5bfa5ad..a496a35842 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableJuiceStream.cs @@ -1,37 +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 JetBrains.Annotations; 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 { public class DrawableJuiceStream : DrawableCatchHitObject { - private readonly Func> createDrawableRepresentation; private readonly Container dropletContainer; - public override Vector2 OriginPosition => base.OriginPosition - new Vector2(0, CatchHitObject.OBJECT_RADIUS); + public DrawableJuiceStream() + : this(null) + { + } - public DrawableJuiceStream(JuiceStream s, Func> createDrawableRepresentation = null) + public DrawableJuiceStream([CanBeNull] JuiceStream s) : base(s) { - this.createDrawableRepresentation = createDrawableRepresentation; RelativeSizeAxes = Axes.X; Origin = Anchor.BottomLeft; - X = 0; AddInternal(dropletContainer = new Container { RelativeSizeAxes = Axes.Both, }); } protected override void AddNestedHitObject(DrawableHitObject hitObject) { - hitObject.Origin = Anchor.BottomCentre; - base.AddNestedHitObject(hitObject); dropletContainer.Add(hitObject); } @@ -39,18 +35,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - dropletContainer.Clear(); - } - - protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) - { - switch (hitObject) - { - case CatchHitObject catchObject: - return createDrawableRepresentation?.Invoke(catchObject); - } - - throw new ArgumentException($"{nameof(hitObject)} must be of type {nameof(CatchHitObject)}."); + dropletContainer.Clear(false); } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 7cddec100f..b4d97fdd2b 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -64,6 +64,7 @@ namespace osu.Game.Rulesets.Catch.UI RegisterPool(1); RegisterPool(1); RegisterPool(1); + RegisterPool(1); } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index ecc37549bf..beea7980c9 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -44,9 +44,6 @@ namespace osu.Game.Rulesets.Catch.UI { switch (h) { - case JuiceStream stream: - return new DrawableJuiceStream(stream, CreateDrawableRepresentation); - case BananaShower shower: return new DrawableBananaShower(shower, CreateDrawableRepresentation); } From f5e8d1d14d47934976b930df6cca83831ce07a6b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Nov 2020 19:19:24 +0900 Subject: [PATCH 4999/6909] Lose old reference immediately on updateSample() --- osu.Game/Skinning/PoolableSkinnableSample.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 7e885b20d1..19b96d6c60 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -83,6 +83,7 @@ namespace osu.Game.Skinning bool wasPlaying = Playing; sampleContainer.Clear(); + Sample = null; var ch = CurrentSkin.GetSample(sampleInfo); From 70628235e36ec7d22e84a885b5ee475cfb88eb6a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 19:22:40 +0900 Subject: [PATCH 5000/6909] Use hit object pooling for `BananaShower`. --- .../Objects/Drawables/DrawableBananaShower.cs | 26 ++++++------------- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 1 + .../UI/DrawableCatchRuleset.cs | 12 +-------- 3 files changed, 10 insertions(+), 29 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs index bf771f690e..9b2f95e221 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBananaShower.cs @@ -1,26 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableBananaShower : DrawableCatchHitObject { - private readonly Func> createDrawableRepresentation; private readonly Container bananaContainer; - public DrawableBananaShower(BananaShower s, Func> createDrawableRepresentation = null) + public DrawableBananaShower() + : this(null) + { + } + + public DrawableBananaShower([CanBeNull] BananaShower s) : base(s) { - this.createDrawableRepresentation = createDrawableRepresentation; RelativeSizeAxes = Axes.X; Origin = Anchor.BottomLeft; - X = 0; AddInternal(bananaContainer = new Container { RelativeSizeAxes = Axes.Both }); } @@ -34,18 +35,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - bananaContainer.Clear(); - } - - protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) - { - switch (hitObject) - { - case Banana banana: - return createDrawableRepresentation?.Invoke(banana); - } - - return base.CreateNestedHitObject(hitObject); + bananaContainer.Clear(false); } } } diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index b4d97fdd2b..97f33007fe 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -65,6 +65,7 @@ namespace osu.Game.Rulesets.Catch.UI RegisterPool(1); RegisterPool(1); RegisterPool(1); + RegisterPool(1); } protected override void LoadComplete() diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index beea7980c9..46733181e3 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -8,7 +8,6 @@ using osu.Game.Configuration; using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Catch.Objects; -using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Drawables; @@ -40,15 +39,6 @@ namespace osu.Game.Rulesets.Catch.UI protected override PassThroughInputManager CreateInputManager() => new CatchInputManager(Ruleset.RulesetInfo); - public override DrawableHitObject CreateDrawableRepresentation(CatchHitObject h) - { - switch (h) - { - case BananaShower shower: - return new DrawableBananaShower(shower, CreateDrawableRepresentation); - } - - return null; - } + public override DrawableHitObject CreateDrawableRepresentation(CatchHitObject h) => null; } } From f589da43177a1df974bbfc8e59e4596459c22672 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Nov 2020 19:24:38 +0900 Subject: [PATCH 5001/6909] Fix compilation error --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index e5fc717504..1ba17d9e17 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -104,9 +104,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private const float spinning_sample_initial_frequency = 1.0f; private const float spinning_sample_modulated_base_frequency = 0.5f; - protected override void OnFree(HitObject hitObject) + protected override void OnFree() { - base.OnFree(hitObject); + base.OnFree(); spinningSample.Samples = null; } From c29ad8edf8731bbaf0d3189a2917b00b7f7cde33 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 30 Nov 2020 19:26:25 +0900 Subject: [PATCH 5002/6909] Better API for dealing with the contained drawable samples --- osu.Game/Skinning/SkinnableSound.cs | 39 +++++++++++-------- .../Drawables/DrawableStoryboardSample.cs | 4 +- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 46c2e4b749..23159e4fe1 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -11,6 +11,7 @@ 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.Game.Audio; @@ -34,7 +35,13 @@ namespace osu.Game.Skinning /// protected bool PlayWhenZeroVolume => Looping; - protected readonly AudioContainer SamplesContainer; + /// + /// All raw s contained in this . + /// + [NotNull, ItemNotNull] + protected IEnumerable DrawableSamples => samplesContainer.Select(c => c.Sample).Where(s => s != null); + + private readonly AudioContainer samplesContainer; [Resolved] private ISampleStore sampleStore { get; set; } @@ -47,7 +54,7 @@ namespace osu.Game.Skinning /// public SkinnableSound() { - InternalChild = SamplesContainer = new AudioContainer(); + InternalChild = samplesContainer = new AudioContainer(); } /// @@ -105,7 +112,7 @@ namespace osu.Game.Skinning looping = value; - SamplesContainer.ForEach(c => c.Looping = looping); + samplesContainer.ForEach(c => c.Looping = looping); } } @@ -114,7 +121,7 @@ namespace osu.Game.Skinning /// public virtual void Play() { - SamplesContainer.ForEach(c => + samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) c.Play(); @@ -126,7 +133,7 @@ namespace osu.Game.Skinning /// public virtual void Stop() { - SamplesContainer.ForEach(c => c.Stop()); + samplesContainer.ForEach(c => c.Stop()); } protected override void SkinChanged(ISkinSource skin, bool allowFallback) @@ -140,8 +147,8 @@ namespace osu.Game.Skinning bool wasPlaying = IsPlaying; // Remove all pooled samples (return them to the pool), and dispose the rest. - SamplesContainer.RemoveAll(s => s.IsInPool); - SamplesContainer.Clear(); + samplesContainer.RemoveAll(s => s.IsInPool); + samplesContainer.Clear(); foreach (var s in samples) { @@ -149,7 +156,7 @@ namespace osu.Game.Skinning sample.Looping = Looping; sample.Volume.Value = s.Volume / 100.0; - SamplesContainer.Add(sample); + samplesContainer.Add(sample); } if (wasPlaying) @@ -158,27 +165,27 @@ namespace osu.Game.Skinning #region Re-expose AudioContainer - public BindableNumber Volume => SamplesContainer.Volume; + public BindableNumber Volume => samplesContainer.Volume; - public BindableNumber Balance => SamplesContainer.Balance; + public BindableNumber Balance => samplesContainer.Balance; - public BindableNumber Frequency => SamplesContainer.Frequency; + public BindableNumber Frequency => samplesContainer.Frequency; - public BindableNumber Tempo => SamplesContainer.Tempo; + public BindableNumber Tempo => samplesContainer.Tempo; public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => SamplesContainer.AddAdjustment(type, adjustBindable); + => samplesContainer.AddAdjustment(type, adjustBindable); public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) - => SamplesContainer.RemoveAdjustment(type, adjustBindable); + => samplesContainer.RemoveAdjustment(type, adjustBindable); public void RemoveAllAdjustments(AdjustableProperty type) - => SamplesContainer.RemoveAllAdjustments(type); + => samplesContainer.RemoveAllAdjustments(type); /// /// Whether any samples are currently playing. /// - public bool IsPlaying => SamplesContainer.Any(s => s.Playing); + public bool IsPlaying => samplesContainer.Any(s => s.Playing); #endregion } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 904af730de..218f051bf0 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -37,8 +37,8 @@ namespace osu.Game.Storyboards.Drawables foreach (var mod in mods.Value.OfType()) { - foreach (var sample in SamplesContainer) - mod.ApplyToSample(sample.Sample); + foreach (var sample in DrawableSamples) + mod.ApplyToSample(sample); } } From a852a27dfb04540e3c43163684cb296d46424179 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 30 Nov 2020 19:36:30 +0900 Subject: [PATCH 5003/6909] Fix current beatmap temporarily becoming empty during ruleset change When changing the ruleset at song select, there was a scenario where it would be set to default (empty) for one debounce length where this was not actually required. This occurs when the currently selected beatmap set has other difficulties which match the target ruleset, specifically. --- osu.Game/Screens/Select/SongSelect.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index b55c0694ef..f32011a27a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -376,7 +376,7 @@ namespace osu.Game.Screens.Select if (selectionChangedDebounce?.Completed == false) { selectionChangedDebounce.RunTask(); - selectionChangedDebounce.Cancel(); // cancel the already scheduled task. + selectionChangedDebounce?.Cancel(); // cancel the already scheduled task. selectionChangedDebounce = null; } @@ -465,19 +465,30 @@ namespace osu.Game.Screens.Select void run() { + // clear pending task immediately to track any potential nested debounce operation. + selectionChangedDebounce = null; + Logger.Log($"updating selection with beatmap:{beatmap?.ID.ToString() ?? "null"} ruleset:{ruleset?.ID.ToString() ?? "null"}"); if (transferRulesetValue()) { Mods.Value = Array.Empty(); - // transferRulesetValue() may trigger a refilter. If the current selection does not match the new ruleset, we want to switch away from it. + // transferRulesetValue() may trigger a re-filter. 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; } + if (selectionChangedDebounce != null) + { + // a new nested operation was started; switch to it for further selection. + // this avoids having two separate debounces trigger from the same source. + selectionChangedDebounce.RunTask(); + return; + } + // We may be arriving here due to another component changing the bindable Beatmap. // In these cases, the other component has already loaded the beatmap, so we don't need to do so again. if (!EqualityComparer.Default.Equals(beatmap, Beatmap.Value.BeatmapInfo)) From 4cd234ea05bb47e6600be35d69a637571d70f315 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 19:56:12 +0900 Subject: [PATCH 5004/6909] Fix null reference of LegacyFruitPiece --- osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 1494ef3888..b8648f46f0 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -19,7 +19,8 @@ namespace osu.Game.Rulesets.Catch.Skinning { private readonly string lookupName; - private readonly IBindable accentColour = new Bindable(); + private readonly Bindable accentColour = new Bindable(); + private readonly Bindable hyperDash = new Bindable(); private Sprite colouredSprite; public LegacyFruitPiece(string lookupName) @@ -34,6 +35,7 @@ namespace osu.Game.Rulesets.Catch.Skinning var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; accentColour.BindTo(drawableCatchObject.AccentColour); + hyperDash.BindTo(drawableCatchObject.HyperDash); InternalChildren = new Drawable[] { @@ -51,9 +53,9 @@ namespace osu.Game.Rulesets.Catch.Skinning }, }; - if (drawableCatchObject.HitObject.HyperDash) + if (hyperDash.Value) { - var hyperDash = new Sprite + var hyperDashOverlay = new Sprite { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -67,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Skinning Catcher.DEFAULT_HYPER_DASH_COLOUR, }; - AddInternal(hyperDash); + AddInternal(hyperDashOverlay); } } From b5e43144a9be23ee4f2435981ab7f25ea8fcdef0 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 30 Nov 2020 19:56:32 +0900 Subject: [PATCH 5005/6909] Add a Player test scene that uses a legacy skin --- .../TestSceneCatchPlayerLegacySkin.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs new file mode 100644 index 0000000000..47c542374a --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.IO.Stores; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests +{ + [TestFixture] + public class TestSceneCatchPlayerLegacySkin : PlayerTestScene + { + private ISkinSource legacySkinSource; + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(legacySkinSource); + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuGameBase game) + { + var legacySkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), audio); + legacySkinSource = new SkinProvidingContainer(legacySkin); + } + + protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); + + public class SkinProvidingPlayer : TestPlayer + { + private readonly ISkinSource skinSource; + + public SkinProvidingPlayer(ISkinSource skinSource) + { + this.skinSource = skinSource; + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(skinSource); + return dependencies; + } + } + } +} From b401259f8432c144e96f030e098491996f827240 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 01:19:36 +0900 Subject: [PATCH 5006/6909] Add test coverage --- .../SongSelect/TestScenePlaySongSelect.cs | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index aa531ba106..a825e2d6fa 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -643,6 +643,54 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("Selected beatmap correct", () => songSelect.Carousel.SelectedBeatmap == filteredBeatmap); } + [Test] + public void TestChangingRulesetOnMultiRulesetBeatmap() + { + int changeCount = 0; + + AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, false)); + AddStep("bind beatmap changed", () => + { + Beatmap.ValueChanged += onChange; + changeCount = 0; + }); + + changeRuleset(0); + + createSongSelect(); + + AddStep("import multi-ruleset map", () => + { + var usableRulesets = rulesets.AvailableRulesets.Where(r => r.ID != 2).ToArray(); + manager.Import(createTestBeatmapSet(usableRulesets)).Wait(); + }); + + int previousSetID = 0; + + AddUntilStep("wait for selection", () => !Beatmap.IsDefault); + + AddStep("record set ID", () => previousSetID = Beatmap.Value.BeatmapSetInfo.ID); + AddAssert("selection changed once", () => changeCount == 1); + + AddAssert("Check ruleset is osu!", () => Ruleset.Value.ID == 0); + + changeRuleset(3); + + AddUntilStep("Check ruleset changed to mania", () => Ruleset.Value.ID == 3); + + AddUntilStep("selection changed", () => changeCount > 1); + + AddAssert("Selected beatmap still same set", () => Beatmap.Value.BeatmapSetInfo.ID == previousSetID); + AddAssert("Selected beatmap is mania", () => Beatmap.Value.BeatmapInfo.Ruleset.ID == 3); + + AddAssert("selection changed only fired twice", () => changeCount == 2); + + AddStep("unbind beatmap changed", () => Beatmap.ValueChanged -= onChange); + AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true)); + + void onChange(ValueChangedEvent valueChangedEvent) => changeCount++; + } + [Test] public void TestDifficultyIconSelectingForDifferentRuleset() { From 07e14b126786d766d2c0cde8f185888c28e06c45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 01:37:53 +0900 Subject: [PATCH 5007/6909] Disable unnecessary inspection --- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index a825e2d6fa..35c6d62cb7 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -688,6 +688,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("unbind beatmap changed", () => Beatmap.ValueChanged -= onChange); AddStep("change convert setting", () => config.Set(OsuSetting.ShowConvertedBeatmaps, true)); + // ReSharper disable once AccessToModifiedClosure void onChange(ValueChangedEvent valueChangedEvent) => changeCount++; } From 2c57deea2bd2724a0a333146f58c2002c18e0c22 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 30 Nov 2020 19:43:20 +0100 Subject: [PATCH 5008/6909] Trim double full-stop in xmldoc --- osu.Game/Skinning/IPooledSampleProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/IPooledSampleProvider.cs b/osu.Game/Skinning/IPooledSampleProvider.cs index 5dbbadcc8a..40193d1a1a 100644 --- a/osu.Game/Skinning/IPooledSampleProvider.cs +++ b/osu.Game/Skinning/IPooledSampleProvider.cs @@ -14,7 +14,7 @@ namespace osu.Game.Skinning /// /// Retrieves a from a pool. /// - /// The describing the sample to retrieve.. + /// The describing the sample to retrieve. /// The . [CanBeNull] PoolableSkinnableSample GetPooledSample(ISampleInfo sampleInfo); From 588a5c2aff768e7139d522b9e3acfe81d111afcf Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 1 Dec 2020 09:35:28 +0900 Subject: [PATCH 5009/6909] Remove empty comment --- osu.Game/Utils/StatelessRNG.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Utils/StatelessRNG.cs b/osu.Game/Utils/StatelessRNG.cs index d316f718e3..11d079498a 100644 --- a/osu.Game/Utils/StatelessRNG.cs +++ b/osu.Game/Utils/StatelessRNG.cs @@ -37,7 +37,6 @@ namespace osu.Game.Utils { unchecked { - // var combined = ((ulong)(uint)series << 32) | (uint)seed; // The xor operation is to not map (0, 0) to 0. return mix(combined ^ 0x12345678); From 4f17e3520e3dfef916a194d10fb9c46e4abcf9af Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 1 Dec 2020 09:38:19 +0900 Subject: [PATCH 5010/6909] Use Cached attribute --- .../TestSceneCatchPlayerLegacySkin.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs index 47c542374a..f9f51ef54a 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -28,19 +28,13 @@ namespace osu.Game.Rulesets.Catch.Tests public class SkinProvidingPlayer : TestPlayer { + [Cached(typeof(ISkinSource))] private readonly ISkinSource skinSource; public SkinProvidingPlayer(ISkinSource skinSource) { this.skinSource = skinSource; } - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(skinSource); - return dependencies; - } } } } From 604619ba47c41537cac4e49c9aeb67f2dd3320bb Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 1 Dec 2020 09:49:04 +0900 Subject: [PATCH 5011/6909] Generalize legacy skin player test. --- .../TestSceneCatchPlayerLegacySkin.cs | 28 +------------- .../Tests/Visual/LegacySkinPlayerTestScene.cs | 38 +++++++++++++++++++ 2 files changed, 39 insertions(+), 27 deletions(-) create mode 100644 osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs index f9f51ef54a..64695153b5 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchPlayerLegacySkin.cs @@ -2,39 +2,13 @@ // 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.IO.Stores; -using osu.Game.Skinning; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - public class TestSceneCatchPlayerLegacySkin : PlayerTestScene + public class TestSceneCatchPlayerLegacySkin : LegacySkinPlayerTestScene { - private ISkinSource legacySkinSource; - - protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(legacySkinSource); - - [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuGameBase game) - { - var legacySkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), audio); - legacySkinSource = new SkinProvidingContainer(legacySkin); - } - protected override Ruleset CreatePlayerRuleset() => new CatchRuleset(); - - public class SkinProvidingPlayer : TestPlayer - { - [Cached(typeof(ISkinSource))] - private readonly ISkinSource skinSource; - - public SkinProvidingPlayer(ISkinSource skinSource) - { - this.skinSource = skinSource; - } - } } } diff --git a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs new file mode 100644 index 0000000000..054f72400e --- /dev/null +++ b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.IO.Stores; +using osu.Game.Rulesets; +using osu.Game.Skinning; + +namespace osu.Game.Tests.Visual +{ + [TestFixture] + public abstract class LegacySkinPlayerTestScene : PlayerTestScene + { + private ISkinSource legacySkinSource; + + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(legacySkinSource); + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuGameBase game) + { + var legacySkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), audio); + legacySkinSource = new SkinProvidingContainer(legacySkin); + } + + public class SkinProvidingPlayer : TestPlayer + { + [Cached(typeof(ISkinSource))] + private readonly ISkinSource skinSource; + + public SkinProvidingPlayer(ISkinSource skinSource) + { + this.skinSource = skinSource; + } + } + } +} From d1076778fd3749a25b1d1eb2277255bdaee0aab1 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 1 Dec 2020 09:50:52 +0900 Subject: [PATCH 5012/6909] Convert switch expression to switch statement --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 085af79689..26077aeba4 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -127,14 +127,23 @@ namespace osu.Game.Rulesets.Catch.UI private DrawableCatchHitObject createCaughtFruit(DrawablePalpableCatchHitObject hitObject) { - return hitObject.HitObject switch + switch (hitObject.HitObject) { - Banana banana => new DrawableBanana(banana), - Fruit fruit => new DrawableFruit(fruit), - TinyDroplet tiny => new DrawableTinyDroplet(tiny), - Droplet droplet => new DrawableDroplet(droplet), - _ => null - }; + case Banana banana: + return new DrawableBanana(banana); + + case Fruit fruit: + return new DrawableFruit(fruit); + + case TinyDroplet tiny: + return new DrawableTinyDroplet(tiny); + + case Droplet droplet: + return new DrawableDroplet(droplet); + + default: + return null; + } } } } From 08cb84b325e0d11f7cf97f0cb665dbbf6fa3c5bb Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 1 Dec 2020 11:32:20 +0900 Subject: [PATCH 5013/6909] Pool osu!catch hit explosion --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 22 ++-- osu.Game.Rulesets.Catch/UI/HitExplosion.cs | 116 ++++++++++++--------- 2 files changed, 82 insertions(+), 56 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 0f0b9df76e..ae374a7cde 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -107,6 +108,9 @@ namespace osu.Game.Rulesets.Catch.UI private float hyperDashTargetPosition; private Bindable hitLighting; + private DrawablePool hitExplosionPool; + private Container hitExplosionContainer; + public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) { this.trailsTarget = trailsTarget; @@ -127,6 +131,7 @@ namespace osu.Game.Rulesets.Catch.UI InternalChildren = new Drawable[] { + hitExplosionPool = new DrawablePool(10), caughtFruitContainer, catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) { @@ -142,7 +147,12 @@ namespace osu.Game.Rulesets.Catch.UI { Anchor = Anchor.TopCentre, Alpha = 0, - } + }, + hitExplosionContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, }; trails = new CatcherTrailDisplay(this); @@ -209,11 +219,11 @@ namespace osu.Game.Rulesets.Catch.UI if (hitLighting.Value) { - AddInternal(new HitExplosion(fruit) - { - X = fruit.X, - Scale = new Vector2(fruit.HitObject.Scale) - }); + var hitExplosion = hitExplosionPool.Get(); + hitExplosion.X = fruit.X; + hitExplosion.Scale = new Vector2(fruit.HitObject.Scale); + hitExplosion.ObjectColour = fruit.AccentColour.Value; + hitExplosionContainer.Add(hitExplosion); } } diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs index 04a86f83be..9716c1ed09 100644 --- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs @@ -5,35 +5,40 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Pooling; 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 + public class HitExplosion : PoolableDrawable { - private readonly CircularContainer largeFaint; + private Color4 objectColour; - public HitExplosion(DrawableCatchHitObject fruit) + public Color4 ObjectColour + { + get => objectColour; + set + { + if (objectColour == value) return; + + objectColour = value; + onColourChanged(); + } + } + + private readonly CircularContainer largeFaint, smallFaint, directionalGrow1, directionalGrow2; + + public HitExplosion() { 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 @@ -42,33 +47,17 @@ namespace osu.Game.Rulesets.Catch.UI 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 + smallFaint = 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 + directionalGrow1 = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -76,16 +65,8 @@ namespace osu.Game.Rulesets.Catch.UI 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 + directionalGrow2 = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -93,30 +74,65 @@ namespace osu.Game.Rulesets.Catch.UI 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() + protected override void PrepareForUse() { - base.LoadComplete(); + base.PrepareForUse(); const double duration = 400; + // we want our size to be very small so the glow dominates it. + largeFaint.Size = new Vector2(0.8f); largeFaint .ResizeTo(largeFaint.Size * new Vector2(5, 1), duration, Easing.OutQuint) .FadeOut(duration * 2); + const float angle_variangle = 15; // should be less than 45 + directionalGrow1.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); + directionalGrow2.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); + this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); Expire(true); } + + private void onColourChanged() + { + const float roundness = 100; + + largeFaint.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.1f, objectColour, Color4.White, 0, 1).Opacity(0.3f), + Roundness = 160, + Radius = 200, + }; + + smallFaint.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.6f, objectColour, Color4.White, 0, 1), + Roundness = 20, + Radius = 50, + }; + + directionalGrow1.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1), + Roundness = roundness, + Radius = 40, + }; + + directionalGrow2.EdgeEffect = new EdgeEffectParameters + { + Type = EdgeEffectType.Glow, + Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1), + Roundness = roundness, + Radius = 40, + }; + } } } From e102f2e8fa390853210038a5b3dd3552997e9cdb Mon Sep 17 00:00:00 2001 From: Ryan Zmuda Date: Mon, 30 Nov 2020 21:38:16 -0500 Subject: [PATCH 5014/6909] Moved enum to bottom, change defualt bind to Shift-Tab, Fixed Notification --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 8 ++++---- osu.Game/Screens/Play/HUDOverlay.cs | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index a59d69e5b5..f4a4813b94 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -68,7 +68,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Tilde }, GlobalAction.QuickExit), new KeyBinding(new[] { InputKey.Control, InputKey.Plus }, GlobalAction.IncreaseScrollSpeed), new KeyBinding(new[] { InputKey.Control, InputKey.Minus }, GlobalAction.DecreaseScrollSpeed), - new KeyBinding(InputKey.I, GlobalAction.ToggleInGameInterface), + new KeyBinding(new[] { InputKey.Shift, InputKey.Tab }, GlobalAction.ToggleInGameInterface), new KeyBinding(InputKey.MouseMiddle, GlobalAction.PauseGameplay), new KeyBinding(InputKey.Space, GlobalAction.TogglePauseReplay), new KeyBinding(InputKey.Control, GlobalAction.HoldForHUD), @@ -146,9 +146,6 @@ namespace osu.Game.Input.Bindings [Description("Decrease scroll speed")] DecreaseScrollSpeed, - [Description("Toggle in-game interface")] - ToggleInGameInterface, - [Description("Select")] Select, @@ -204,5 +201,8 @@ namespace osu.Game.Input.Bindings [Description("Pause / resume replay")] TogglePauseReplay, + + [Description("Toggle in-game interface")] + ToggleInGameInterface, } } diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 457706b8f5..96d1e211fd 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -181,7 +181,7 @@ namespace osu.Game.Screens.Play notificationOverlay?.Post(new SimpleNotification { - Text = @"The score overlay is currently disabled. You can toggle this by pressing Shift+Tab." + Text = $"The score overlay is currently disabled. You can toggle this by pressing {config.LookupKeyBindings(GlobalAction.ToggleInGameInterface)}." }); } From 5945c088cbacc0c0240c65d63031562d65126d49 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 12:57:37 +0900 Subject: [PATCH 5015/6909] A few code standard cleanups --- osu.Game.Rulesets.Catch/UI/HitExplosion.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs index 9716c1ed09..ce337c45b4 100644 --- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs @@ -28,7 +28,10 @@ namespace osu.Game.Rulesets.Catch.UI } } - private readonly CircularContainer largeFaint, smallFaint, directionalGrow1, directionalGrow2; + private readonly CircularContainer largeFaint; + private readonly CircularContainer smallFaint; + private readonly CircularContainer directionalGlow1; + private readonly CircularContainer directionalGlow2; public HitExplosion() { @@ -57,7 +60,7 @@ namespace osu.Game.Rulesets.Catch.UI Masking = true, Blending = BlendingParameters.Additive, }, - directionalGrow1 = new CircularContainer + directionalGlow1 = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -66,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.UI Size = new Vector2(0.01f, initial_height), Blending = BlendingParameters.Additive, }, - directionalGrow2 = new CircularContainer + directionalGlow2 = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -91,8 +94,8 @@ namespace osu.Game.Rulesets.Catch.UI .FadeOut(duration * 2); const float angle_variangle = 15; // should be less than 45 - directionalGrow1.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); - directionalGrow2.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); + directionalGlow1.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); + directionalGlow2.Rotation = RNG.NextSingle(-angle_variangle, angle_variangle); this.FadeInFromZero(50).Then().FadeOut(duration, Easing.Out); Expire(true); @@ -118,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.UI Radius = 50, }; - directionalGrow1.EdgeEffect = new EdgeEffectParameters + directionalGlow1.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1), @@ -126,7 +129,7 @@ namespace osu.Game.Rulesets.Catch.UI Radius = 40, }; - directionalGrow2.EdgeEffect = new EdgeEffectParameters + directionalGlow2.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1), From c8c1848bb8f554c022ddfd9b039d9848e1bb0ef9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 13:46:30 +0900 Subject: [PATCH 5016/6909] Fix slider control point dragging not correctly accounting for drag deadzone --- .../Blueprints/Sliders/Components/PathControlPointPiece.cs | 5 ++++- 1 file changed, 4 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 c06904c0c2..c302e8fe99 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -143,6 +143,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components protected override bool OnClick(ClickEvent e) => RequestSelection != null; + private Vector2 dragStartPosition; + protected override bool OnDragStart(DragStartEvent e) { if (RequestSelection == null) @@ -150,6 +152,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components if (e.Button == MouseButton.Left) { + dragStartPosition = ControlPoint.Position.Value; changeHandler?.BeginChange(); return true; } @@ -174,7 +177,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components slider.Path.ControlPoints[i].Position.Value -= movementDelta; } else - ControlPoint.Position.Value += e.Delta; + ControlPoint.Position.Value = dragStartPosition + (e.MousePosition - e.MouseDownPosition); } protected override void OnDragEnd(DragEndEvent e) => changeHandler?.EndChange(); From 63ff722963bf9762dc3938228e0832ae0a92ad00 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 14:00:54 +0900 Subject: [PATCH 5017/6909] Fix code formatting --- osu.Game/Screens/Play/HUDOverlay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 96d1e211fd..7ab04cf9f9 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -9,7 +9,6 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; -using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Overlays; @@ -19,7 +18,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Screens.Play.HUD; using osuTK; -using osuTK.Input; namespace osu.Game.Screens.Play { @@ -362,6 +360,7 @@ namespace osu.Game.Screens.Play configVisibilityMode.Value = HUDVisibilityMode.Never; break; } + updateVisibility(); return true; } From a147b7186d28cbf1299a019b3cc37f99714a4b6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 14:01:34 +0900 Subject: [PATCH 5018/6909] Remove unnecessary call to updateVisibility --- osu.Game/Screens/Play/HUDOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 7ab04cf9f9..50195d571c 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -361,7 +361,6 @@ namespace osu.Game.Screens.Play break; } - updateVisibility(); return true; } From b256c546198b159aae054f79ba585146fe1b5e7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 14:17:36 +0900 Subject: [PATCH 5019/6909] Scale slider control point display in line with circle size --- .../Sliders/Components/PathControlPointPiece.cs | 9 +++++++-- 1 file changed, 7 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 c06904c0c2..41d4d10568 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/PathControlPointPiece.cs @@ -44,6 +44,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components private OsuColour colours { get; set; } private IBindable sliderPosition; + private IBindable sliderScale; private IBindable controlPointPosition; public PathControlPointPiece(Slider slider, PathControlPoint controlPoint) @@ -69,13 +70,13 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(10), + Size = new Vector2(20), }, markerRing = new CircularContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(14), + Size = new Vector2(28), Masking = true, BorderThickness = 2, BorderColour = Color4.White, @@ -102,6 +103,9 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components controlPointPosition = ControlPoint.Position.GetBoundCopy(); controlPointPosition.BindValueChanged(_ => updateMarkerDisplay()); + sliderScale = slider.ScaleBindable.GetBoundCopy(); + sliderScale.BindValueChanged(_ => updateMarkerDisplay()); + IsSelected.BindValueChanged(_ => updateMarkerDisplay()); updateMarkerDisplay(); @@ -194,6 +198,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders.Components colour = colour.Lighten(1); marker.Colour = colour; + marker.Scale = new Vector2(slider.Scale); } } } From a16b265090942f7c5fa06234978005776586ff5b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 1 Dec 2020 14:46:04 +0900 Subject: [PATCH 5020/6909] Apply suggested styling changes --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 2 +- osu.Game.Rulesets.Catch/UI/HitExplosion.cs | 10 +--------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index ae374a7cde..11b6916a4c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -219,7 +219,7 @@ namespace osu.Game.Rulesets.Catch.UI if (hitLighting.Value) { - var hitExplosion = hitExplosionPool.Get(); + HitExplosion hitExplosion = hitExplosionPool.Get(); hitExplosion.X = fruit.X; hitExplosion.Scale = new Vector2(fruit.HitObject.Scale); hitExplosion.ObjectColour = fruit.AccentColour.Value; diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs index ce337c45b4..24ca778248 100644 --- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs @@ -121,15 +121,7 @@ namespace osu.Game.Rulesets.Catch.UI Radius = 50, }; - directionalGlow1.EdgeEffect = new EdgeEffectParameters - { - Type = EdgeEffectType.Glow, - Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1), - Roundness = roundness, - Radius = 40, - }; - - directionalGlow2.EdgeEffect = new EdgeEffectParameters + directionalGlow1.EdgeEffect = directionalGlow2.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, Colour = Interpolation.ValueAt(0.4f, objectColour, Color4.White, 0, 1), From e8842eed814cec5c1d889515a2075bbbd7ea23d1 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 1 Dec 2020 14:50:42 +0900 Subject: [PATCH 5021/6909] Use bigger initial capacity for catch hit object pool --- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 97f33007fe..820f08d439 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -60,12 +60,12 @@ namespace osu.Game.Rulesets.Catch.UI [BackgroundDependencyLoader] private void load() { - RegisterPool(1); - RegisterPool(1); - RegisterPool(1); - RegisterPool(1); - RegisterPool(1); - RegisterPool(1); + RegisterPool(50); + RegisterPool(50); + RegisterPool(100); + RegisterPool(100); + RegisterPool(10); + RegisterPool(2); } protected override void LoadComplete() From 8d101efb24f53f2f4f0549bc1fe734dee18b4815 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 14:56:14 +0900 Subject: [PATCH 5022/6909] Remove unused proxy layer --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index dd27ac990e..986deccd51 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -300,8 +300,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables this.FadeOut(fade_out_time, Easing.OutQuint).Expire(); } - public Drawable ProxiedLayer => HeadCircle.ProxiedLayer; - public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => sliderBody?.ReceivePositionalInputAt(screenSpacePos) ?? base.ReceivePositionalInputAt(screenSpacePos); private class DefaultSliderBody : PlaySliderBody From d0852d7f4adcd63a16cce7b7a281f69a305b4f28 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 14:56:41 +0900 Subject: [PATCH 5023/6909] Hide slider body immediately on successful hit when snaking is enabled --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 986deccd51..ec653264d0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -294,6 +294,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { case ArmedState.Hit: Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out); + if (sliderBody?.SnakingOut.Value == true) + Body.FadeOut(); break; } From f8ef822e73960ab5e80e5747f312c459db058d20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 15:21:32 +0900 Subject: [PATCH 5024/6909] Add short fade to better hide transition on default skin --- 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 ec653264d0..14bcefde0a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -295,7 +295,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables case ArmedState.Hit: Ball.ScaleTo(HitObject.Scale * 1.4f, fade_out_time, Easing.Out); if (sliderBody?.SnakingOut.Value == true) - Body.FadeOut(); + Body.FadeOut(40); // short fade to allow for any body colour to smoothly disappear. break; } From dd05c56a08d778d051eadcf4eda7f241336d87c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 15:34:19 +0900 Subject: [PATCH 5025/6909] Fix sliders playing hit animations when completely missed --- 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 dd27ac990e..b65463f438 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -255,7 +255,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < HitObject.EndTime) return; - ApplyResult(r => r.Type = r.Judgement.MaxResult); + ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override void PlaySamples() From 5760e1c1fca35e580b459ad6a05431efc50a975f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 15:37:51 +0900 Subject: [PATCH 5026/6909] Make HitSampleInfo immutable --- .../TestSceneAutoJuiceStream.cs | 2 +- osu.Game.Rulesets.Catch/Objects/Banana.cs | 5 + .../Objects/JuiceStream.cs | 7 +- .../TestSceneSlider.cs | 10 +- .../Objects/Drawables/DrawableSlider.cs | 3 +- .../Objects/Drawables/DrawableSpinner.cs | 3 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 9 +- .../Objects/SpinnerBonusTick.cs | 2 +- .../Objects/Drawables/DrawableHit.cs | 6 +- .../Drawables/DrawableTaikoHitObject.cs | 2 +- .../Editing/LegacyEditorBeatmapPatcherTest.cs | 8 +- osu.Game/Audio/HitSampleInfo.cs | 38 +++++-- .../ControlPoints/SampleControlPoint.cs | 14 +-- .../Beatmaps/Formats/LegacyBeatmapEncoder.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 7 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 101 +++++++----------- .../Components/ComposeBlueprintContainer.cs | 4 +- .../Compose/Components/SelectionHandler.cs | 2 +- osu.Game/Utils/Optional.cs | 45 ++++++++ 19 files changed, 144 insertions(+), 126 deletions(-) create mode 100644 osu.Game/Utils/Optional.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 3c636a5b97..0d57fb7029 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests NewCombo = i % 8 == 0, Samples = new List(new[] { - new HitSampleInfo { Bank = "normal", Name = "hitnormal", Volume = 100 } + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal") }) }); } diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index ccb1fff15b..822db890e2 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -55,6 +55,11 @@ namespace osu.Game.Rulesets.Catch.Objects private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; public override IEnumerable LookupNames => lookupNames; + + public BananaHitSampleInfo() + : base(string.Empty) + { + } } } } diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index e209d012fa..d5819935ad 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -50,12 +50,7 @@ namespace osu.Game.Rulesets.Catch.Objects { base.CreateNestedHitObjects(cancellationToken); - var dropletSamples = Samples.Select(s => new HitSampleInfo - { - Bank = s.Bank, - Name = @"slidertick", - Volume = s.Volume - }).ToList(); + var dropletSamples = Samples.Select(s => s.With(@"slidertick")).ToList(); int nodeIndex = 0; SliderEventDescriptor? lastEvent = null; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs index c400e2f2ea..d40484f5ed 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSlider.cs @@ -108,8 +108,8 @@ namespace osu.Game.Rulesets.Osu.Tests AddStep("change samples", () => slider.HitObject.Samples = new[] { - new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }, - new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE }, + new HitSampleInfo(HitSampleInfo.HIT_CLAP), + new HitSampleInfo(HitSampleInfo.HIT_WHISTLE), }); AddAssert("head samples updated", () => assertSamples(slider.HitObject.HeadCircle)); @@ -136,15 +136,15 @@ namespace osu.Game.Rulesets.Osu.Tests slider = (DrawableSlider)createSlider(repeats: 1); for (int i = 0; i < 2; i++) - slider.HitObject.NodeSamples.Add(new List { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } }); + slider.HitObject.NodeSamples.Add(new List { new HitSampleInfo(HitSampleInfo.HIT_FINISH) }); Add(slider); }); AddStep("change samples", () => slider.HitObject.Samples = new[] { - new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }, - new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE }, + new HitSampleInfo(HitSampleInfo.HIT_CLAP), + new HitSampleInfo(HitSampleInfo.HIT_WHISTLE), }); AddAssert("head samples not updated", () => assertSamples(slider.HitObject.HeadCircle)); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index dd27ac990e..3aa0c9ccb5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -115,8 +115,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (firstSample != null) { - var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); - clone.Name = "sliderslide"; + var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("sliderslide"); samplesContainer.Add(slidingSample = new PausableSkinnableSound(clone) { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 2a14a7c975..5a11265a47 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -110,8 +110,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (firstSample != null) { - var clone = HitObject.SampleControlPoint.ApplyTo(firstSample); - clone.Name = "spinnerspin"; + var clone = HitObject.SampleControlPoint.ApplyTo(firstSample).With("spinnerspin"); samplesContainer.Add(spinningSample = new PausableSkinnableSound(clone) { diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 755ce0866a..1670df24a8 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -221,14 +221,7 @@ namespace osu.Game.Rulesets.Osu.Objects var sampleList = new List(); if (firstSample != null) - { - sampleList.Add(new HitSampleInfo - { - Bank = firstSample.Bank, - Volume = firstSample.Volume, - Name = @"slidertick", - }); - } + sampleList.Add(firstSample.With("slidertick")); foreach (var tick in NestedHitObjects.OfType()) tick.Samples = sampleList; diff --git a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs index 235dc8710a..2c443cb96b 100644 --- a/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SpinnerBonusTick.cs @@ -11,7 +11,7 @@ namespace osu.Game.Rulesets.Osu.Objects { public SpinnerBonusTick() { - Samples.Add(new HitSampleInfo { Name = "spinnerbonus" }); + Samples.Add(new HitSampleInfo("spinnerbonus")); } public override Judgement CreateJudgement() => new OsuSpinnerBonusTickJudgement(); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 4a3759794b..29a96a7a40 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (isRimType != rimSamples.Any()) { if (isRimType) - HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP }); + HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP)); else { foreach (var sample in rimSamples) @@ -125,9 +125,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (s.Name != HitSampleInfo.HIT_FINISH) continue; - var sClone = s.Clone(); - sClone.Name = HitSampleInfo.HIT_WHISTLE; - corrected[i] = sClone; + corrected[i] = s.With(HitSampleInfo.HIT_WHISTLE); } return corrected; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index d8d75a7614..ff5b221273 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (isStrong.Value != strongSamples.Any()) { if (isStrong.Value) - HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH }); + HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); else { foreach (var sample in strongSamples) diff --git a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs index bb56131b04..44a908b756 100644 --- a/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs +++ b/osu.Game.Tests/Editing/LegacyEditorBeatmapPatcherTest.cs @@ -139,7 +139,7 @@ namespace osu.Game.Tests.Editing HitObjects = { (OsuHitObject)current.HitObjects[0], - new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } }, + new HitCircle { StartTime = 2000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_FINISH) } }, (OsuHitObject)current.HitObjects[2], } }; @@ -268,12 +268,12 @@ namespace osu.Game.Tests.Editing HitObjects = { (OsuHitObject)current.HitObjects[0], - new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_FINISH } } }, + new HitCircle { StartTime = 1000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_FINISH) } }, (OsuHitObject)current.HitObjects[2], (OsuHitObject)current.HitObjects[3], - new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_WHISTLE } } }, + new HitCircle { StartTime = 2250, Samples = { new HitSampleInfo(HitSampleInfo.HIT_WHISTLE) } }, (OsuHitObject)current.HitObjects[5], - new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo { Name = HitSampleInfo.HIT_CLAP } } }, + new HitCircle { StartTime = 3000, Samples = { new HitSampleInfo(HitSampleInfo.HIT_CLAP) } }, (OsuHitObject)current.HitObjects[7], } }; diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 8efaeb3795..bf4a83e5d7 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.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. +#nullable enable + using System; using System.Collections.Generic; +using osu.Game.Utils; namespace osu.Game.Audio { @@ -22,25 +25,33 @@ namespace osu.Game.Audio /// public static IEnumerable AllAdditions => new[] { HIT_WHISTLE, HIT_CLAP, HIT_FINISH }; - /// - /// The bank to load the sample from. - /// - public string Bank; - /// /// The name of the sample to load. /// - public string Name; + public readonly string Name; + + /// + /// The bank to load the sample from. + /// + public readonly string? Bank; /// /// An optional suffix to provide priority lookup. Falls back to non-suffixed . /// - public string Suffix; + public readonly string? Suffix; /// /// The sample volume. /// - public int Volume { get; set; } + public int Volume { get; } + + public HitSampleInfo(string name, string? bank = null, string? suffix = null, int volume = 100) + { + Name = name; + Bank = bank; + Suffix = suffix; + Volume = volume; + } /// /// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first). @@ -56,6 +67,15 @@ namespace osu.Game.Audio } } - public HitSampleInfo Clone() => (HitSampleInfo)MemberwiseClone(); + /// + /// Creates a new with overridden values. + /// + /// An optional new sample name. + /// An optional new sample bank. + /// An optional new lookup suffix. + /// An optional new volume. + /// The new . + public virtual HitSampleInfo With(Optional name = default, Optional bank = default, Optional suffix = default, Optional volume = default) + => new HitSampleInfo(name.GetOr(Name), bank.GetOr(Bank), suffix.GetOr(Suffix), volume.GetOr(Volume)); } } diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index f57ecfb9e3..8064da1543 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -58,12 +58,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// /// The name of the same. /// A populated . - public HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL) => new HitSampleInfo - { - Bank = SampleBank, - Name = sampleName, - Volume = SampleVolume, - }; + public HitSampleInfo GetSampleInfo(string sampleName = HitSampleInfo.HIT_NORMAL) => new HitSampleInfo(sampleName, SampleBank, volume: SampleVolume); /// /// Applies and to a if necessary, returning the modified . @@ -71,12 +66,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// The . This will not be modified. /// The modified . This does not share a reference with . public virtual HitSampleInfo ApplyTo(HitSampleInfo hitSampleInfo) - { - var newSampleInfo = hitSampleInfo.Clone(); - newSampleInfo.Bank = hitSampleInfo.Bank ?? SampleBank; - newSampleInfo.Volume = hitSampleInfo.Volume > 0 ? hitSampleInfo.Volume : SampleVolume; - return newSampleInfo; - } + => hitSampleInfo.With(bank: hitSampleInfo.Bank ?? SampleBank, volume: hitSampleInfo.Volume > 0 ? hitSampleInfo.Volume : SampleVolume); public override bool IsRedundant(ControlPoint existing) => existing is SampleControlPoint existingSample diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index 7ddb0b4caa..df940e8c8e 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -192,7 +192,7 @@ namespace osu.Game.Beatmaps.Formats 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()); + HitSampleInfo tempHitSample = samplePoint.ApplyTo(new ConvertHitObjectParser.LegacyHitSampleInfo(string.Empty)); // Convert effect flags to the legacy format LegacyEffectFlags effectFlags = LegacyEffectFlags.None; diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index de4dc8cdc8..9f16180e77 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -182,11 +182,8 @@ namespace osu.Game.Beatmaps.Formats { var baseInfo = base.ApplyTo(hitSampleInfo); - if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy - && legacy.CustomSampleBank == 0) - { - legacy.CustomSampleBank = CustomSampleBank; - } + if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0) + return legacy.With(customSampleBank: CustomSampleBank); return baseInfo; } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 44b22033dc..9b38266400 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -13,6 +13,7 @@ using JetBrains.Annotations; using osu.Framework.Utils; using osu.Game.Beatmaps.Legacy; using osu.Game.Skinning; +using osu.Game.Utils; namespace osu.Game.Rulesets.Objects.Legacy { @@ -427,62 +428,25 @@ namespace osu.Game.Rulesets.Objects.Legacy // Todo: This should return the normal SampleInfos if the specified sample file isn't found, but that's a pretty edge-case scenario if (!string.IsNullOrEmpty(bankInfo.Filename)) { - return new List - { - new FileHitSampleInfo - { - Filename = bankInfo.Filename, - Volume = bankInfo.Volume - } - }; + return new List { new FileHitSampleInfo(bankInfo.Filename, bankInfo.Volume) }; } var soundTypes = new List { - new LegacyHitSampleInfo - { - Bank = bankInfo.Normal, - Name = HitSampleInfo.HIT_NORMAL, - Volume = bankInfo.Volume, - CustomSampleBank = bankInfo.CustomSampleBank, + new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.Normal, bankInfo.Volume, bankInfo.CustomSampleBank, // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds - IsLayered = type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal) - } + type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)) }; if (type.HasFlag(LegacyHitSoundType.Finish)) - { - soundTypes.Add(new LegacyHitSampleInfo - { - Bank = bankInfo.Add, - Name = HitSampleInfo.HIT_FINISH, - Volume = bankInfo.Volume, - CustomSampleBank = bankInfo.CustomSampleBank - }); - } + soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); if (type.HasFlag(LegacyHitSoundType.Whistle)) - { - soundTypes.Add(new LegacyHitSampleInfo - { - Bank = bankInfo.Add, - Name = HitSampleInfo.HIT_WHISTLE, - Volume = bankInfo.Volume, - CustomSampleBank = bankInfo.CustomSampleBank - }); - } + soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); if (type.HasFlag(LegacyHitSoundType.Clap)) - { - soundTypes.Add(new LegacyHitSampleInfo - { - Bank = bankInfo.Add, - Name = HitSampleInfo.HIT_CLAP, - Volume = bankInfo.Volume, - CustomSampleBank = bankInfo.CustomSampleBank - }); - } + soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); return soundTypes; } @@ -500,21 +464,11 @@ namespace osu.Game.Rulesets.Objects.Legacy public SampleBankInfo Clone() => (SampleBankInfo)MemberwiseClone(); } +#nullable enable + public class LegacyHitSampleInfo : HitSampleInfo { - private int customSampleBank; - - public int CustomSampleBank - { - get => customSampleBank; - set - { - customSampleBank = value; - - if (value >= 2) - Suffix = value.ToString(); - } - } + public readonly int CustomSampleBank; /// /// Whether this hit sample is layered. @@ -523,18 +477,33 @@ namespace osu.Game.Rulesets.Objects.Legacy /// 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; } + public readonly bool IsLayered; + + public LegacyHitSampleInfo([NotNull] string name, string? bank = null, int volume = 100, int customSampleBank = 0, bool isLayered = false) + : base(name, bank, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume) + { + CustomSampleBank = customSampleBank; + IsLayered = isLayered; + } + + public override HitSampleInfo With(Optional name = default, Optional bank = default, Optional suffix = default, Optional volume = default) + => With(name, bank, volume); + + public LegacyHitSampleInfo With(Optional name = default, Optional bank = default, Optional volume = default, Optional customSampleBank = default, + Optional isLayered = default) + => new LegacyHitSampleInfo(name.GetOr(Name), bank.GetOr(Bank), volume.GetOr(Volume), customSampleBank.GetOr(CustomSampleBank), isLayered.GetOr(IsLayered)); } private class FileHitSampleInfo : LegacyHitSampleInfo { - public string Filename; + public readonly string Filename; - public FileHitSampleInfo() - { - // Make sure that the LegacyBeatmapSkin does not fall back to the user skin. + public FileHitSampleInfo(string filename, int volume) + // Force CSS=1 to 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; + : base(string.Empty, customSampleBank: 1, volume: volume) + { + Filename = filename; } public override IEnumerable LookupNames => new[] @@ -542,6 +511,14 @@ namespace osu.Game.Rulesets.Objects.Legacy Filename, Path.ChangeExtension(Filename, null) }; + + public override HitSampleInfo With(Optional name = default, Optional bank = default, Optional suffix = default, Optional volume = default) + => With(volume: volume); + + public FileHitSampleInfo With(Optional filename = default, Optional volume = default) + => new FileHitSampleInfo(filename.GetOr(Filename), volume.GetOr(Volume)); } + +#nullable disable } } diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index 1893366d90..c09b935f28 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Edit.Compose.Components case TernaryState.True: if (existingSample == null) - samples.Add(new HitSampleInfo { Name = sampleName }); + samples.Add(new HitSampleInfo(sampleName)); break; } } @@ -212,7 +212,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (blueprint != null) { // doing this post-creations as adding the default hit sample should be the case regardless of the ruleset. - blueprint.HitObject.Samples.Add(new HitSampleInfo { Name = HitSampleInfo.HIT_NORMAL }); + blueprint.HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); placementBlueprintContainer.Child = currentPlacement = blueprint; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index adf22a3370..788b485449 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -328,7 +328,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (h.Samples.Any(s => s.Name == sampleName)) continue; - h.Samples.Add(new HitSampleInfo { Name = sampleName }); + h.Samples.Add(new HitSampleInfo(sampleName)); } EditorBeatmap.EndChange(); diff --git a/osu.Game/Utils/Optional.cs b/osu.Game/Utils/Optional.cs new file mode 100644 index 0000000000..9f8a1c2e62 --- /dev/null +++ b/osu.Game/Utils/Optional.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. + +#nullable enable + +namespace osu.Game.Utils +{ + /// + /// A wrapper over a value and a boolean denoting whether the value is valid. + /// + /// The type of value stored. + public readonly ref struct Optional + { + /// + /// The stored value. + /// + public readonly T Value; + + /// + /// Whether is valid. + /// + /// + /// If is a reference type, null may be valid for . + /// + public readonly bool HasValue; + + private Optional(T value) + { + Value = value; + HasValue = true; + } + + /// + /// Returns if it's valid, or a given fallback value otherwise. + /// + /// + /// Shortcase for: optional.HasValue ? optional.Value : fallback. + /// + /// The fallback value to return if is false. + /// + public T GetOr(T fallback) => HasValue ? Value : fallback; + + public static implicit operator Optional(T value) => new Optional(value); + } +} From 199710b941a396cfed43b989a127c5a355522dfa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 15:44:16 +0900 Subject: [PATCH 5027/6909] Implement equality comparers for HitSampleInfo --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 15 ++++++++++++--- osu.Game/Audio/HitSampleInfo.cs | 10 +++++++++- .../Objects/Legacy/ConvertHitObjectParser.cs | 19 +++++++++++++++++-- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 822db890e2..ec4b67f341 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.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.Utils; using osu.Game.Audio; @@ -50,16 +51,24 @@ namespace osu.Game.Rulesets.Catch.Objects } } - private class BananaHitSampleInfo : HitSampleInfo + private class BananaHitSampleInfo : HitSampleInfo, IEquatable { - private static string[] lookupNames { get; } = { "metronomelow", "catch-banana" }; + private static readonly string[] lookup_names = { "metronomelow", "catch-banana" }; - public override IEnumerable LookupNames => lookupNames; + public override IEnumerable LookupNames => lookup_names; public BananaHitSampleInfo() : base(string.Empty) { } + + public bool Equals(BananaHitSampleInfo other) + => other != null; + + public override bool Equals(object obj) + => Equals((BananaHitSampleInfo)obj); + + public override int GetHashCode() => lookup_names.GetHashCode(); } } } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index bf4a83e5d7..4a0e48749a 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -13,7 +13,7 @@ namespace osu.Game.Audio /// Describes a gameplay hit sample. /// [Serializable] - public class HitSampleInfo : ISampleInfo + public class HitSampleInfo : ISampleInfo, IEquatable { public const string HIT_WHISTLE = @"hitwhistle"; public const string HIT_FINISH = @"hitfinish"; @@ -77,5 +77,13 @@ namespace osu.Game.Audio /// The new . public virtual HitSampleInfo With(Optional name = default, Optional bank = default, Optional suffix = default, Optional volume = default) => new HitSampleInfo(name.GetOr(Name), bank.GetOr(Bank), suffix.GetOr(Suffix), volume.GetOr(Volume)); + + public bool Equals(HitSampleInfo? other) + => other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix; + + public override bool Equals(object? obj) + => Equals((HitSampleInfo?)obj); + + public override int GetHashCode() => HashCode.Combine(Name, Bank, Suffix); } } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 9b38266400..931bdb3db7 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -466,7 +466,7 @@ namespace osu.Game.Rulesets.Objects.Legacy #nullable enable - public class LegacyHitSampleInfo : HitSampleInfo + public class LegacyHitSampleInfo : HitSampleInfo, IEquatable { public readonly int CustomSampleBank; @@ -492,9 +492,16 @@ namespace osu.Game.Rulesets.Objects.Legacy public LegacyHitSampleInfo With(Optional name = default, Optional bank = default, Optional volume = default, Optional customSampleBank = default, Optional isLayered = default) => new LegacyHitSampleInfo(name.GetOr(Name), bank.GetOr(Bank), volume.GetOr(Volume), customSampleBank.GetOr(CustomSampleBank), isLayered.GetOr(IsLayered)); + + public bool Equals(LegacyHitSampleInfo? other) + => base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered; + + public override bool Equals(object? obj) => Equals((LegacyHitSampleInfo?)obj); + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered); } - private class FileHitSampleInfo : LegacyHitSampleInfo + private class FileHitSampleInfo : LegacyHitSampleInfo, IEquatable { public readonly string Filename; @@ -517,6 +524,14 @@ namespace osu.Game.Rulesets.Objects.Legacy public FileHitSampleInfo With(Optional filename = default, Optional volume = default) => new FileHitSampleInfo(filename.GetOr(Filename), volume.GetOr(Volume)); + + public bool Equals(FileHitSampleInfo? other) + => base.Equals(other) && Filename == other.Filename; + + public override bool Equals(object? obj) + => Equals((FileHitSampleInfo?)obj); + + public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Filename); } #nullable disable From 2b268ee012f5b4ae9d151d3a2b1fb64e07d488b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 16:16:26 +0900 Subject: [PATCH 5028/6909] Fix editor beat snapping not working correctly when starting with a new beatmap --- osu.Game/Screens/Edit/Editor.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3d5c0ddad4..8f1cd8e28e 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -109,6 +109,12 @@ namespace osu.Game.Screens.Edit beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor; beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); + if (Beatmap.Value is DummyWorkingBeatmap) + { + isNewBeatmap = true; + Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); + } + // Todo: should probably be done at a DrawableRuleset level to share logic with Player. clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; @@ -122,12 +128,6 @@ namespace osu.Game.Screens.Edit // todo: remove caching of this and consume via editorBeatmap? dependencies.Cache(beatDivisor); - if (Beatmap.Value is DummyWorkingBeatmap) - { - isNewBeatmap = true; - Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); - } - try { playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); From 190c6ef45e3fd0cee9581596d85d8ea531160d2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 16:44:08 +0900 Subject: [PATCH 5029/6909] Fix timeline not updating ticks correctly after arbitrary timing control point changes --- .../Timeline/TimelineTickDisplay.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 724256af8b..71268a1e69 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -25,6 +25,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private BindableBeatDivisor beatDivisor { get; set; } + [Resolved] + private IEditorChangeHandler changeHandler { get; set; } + [Resolved] private OsuColour colours { get; set; } @@ -38,7 +41,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [BackgroundDependencyLoader] private void load() { - beatDivisor.BindValueChanged(_ => tickCache.Invalidate()); + beatDivisor.BindValueChanged(_ => invalidateTicks()); + + // currently this is the best way to handle any kind of timing changes. + changeHandler.OnStateChange += invalidateTicks; + } + + private void invalidateTicks() + { + tickCache.Invalidate(); } /// @@ -165,5 +176,13 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline return point; } } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (changeHandler != null) + changeHandler.OnStateChange -= invalidateTicks; + } } } From 4900589af4d958c88a0df65a94d647bc06222032 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 17:02:45 +0900 Subject: [PATCH 5030/6909] Remove unused method --- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 931bdb3db7..5895059df5 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -520,10 +520,7 @@ namespace osu.Game.Rulesets.Objects.Legacy }; public override HitSampleInfo With(Optional name = default, Optional bank = default, Optional suffix = default, Optional volume = default) - => With(volume: volume); - - public FileHitSampleInfo With(Optional filename = default, Optional volume = default) - => new FileHitSampleInfo(filename.GetOr(Filename), volume.GetOr(Volume)); + => new FileHitSampleInfo(Filename, volume.GetOr(Volume)); public bool Equals(FileHitSampleInfo? other) => base.Equals(other) && Filename == other.Filename; From d5a60ed335c012e6ba7af05c39fd08e65eeb8f20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 17:30:42 +0900 Subject: [PATCH 5031/6909] 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 0b43fd73f5..aa4d9fa4ee 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 e201383d51..88af0e2138 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index e5f7581404..06b0ec8a13 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 285e62be9a86369c6bcea7105387864b9e9e58af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 1 Dec 2020 17:47:07 +0900 Subject: [PATCH 5032/6909] Bring code in line with SDL2 defaults --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Desktop/Program.cs | 4 ++-- .../Overlays/Settings/Sections/Graphics/LayoutSettings.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 0feab9a717..c62d2ee99f 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -138,7 +138,7 @@ namespace osu.Desktop break; // SDL2 DesktopWindow - case DesktopWindow desktopWindow: + case SDL2DesktopWindow desktopWindow: desktopWindow.CursorState |= CursorState.Hidden; desktopWindow.SetIconFromStream(iconStream); desktopWindow.Title = Name; diff --git a/osu.Desktop/Program.cs b/osu.Desktop/Program.cs index 285a813d97..6ca7079654 100644 --- a/osu.Desktop/Program.cs +++ b/osu.Desktop/Program.cs @@ -22,9 +22,9 @@ namespace osu.Desktop { // Back up the cwd before DesktopGameHost changes it var cwd = Environment.CurrentDirectory; - bool useSdl = args.Contains("--sdl"); + bool useOsuTK = args.Contains("--tk"); - using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useSdl: useSdl)) + using (DesktopGameHost host = Host.GetSuitableHost(@"osu", true, useOsuTK: useOsuTK)) { host.ExceptionThrown += handleException; diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 14b8dbfac0..62dc1dc806 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -208,7 +208,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private IReadOnlyList getResolutions() { var resolutions = new List { new Size(9999, 9999) }; - var currentDisplay = game.Window?.CurrentDisplay.Value; + var currentDisplay = game.Window?.CurrentDisplayBindable.Value; if (currentDisplay != null) { From b780fdbe4c24efce455c6ebc1b0d53dcc3826eb3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 18:08:59 +0900 Subject: [PATCH 5033/6909] Ignore lookup types for JSON serialisation --- osu.Game/Audio/HitSampleInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 4a0e48749a..27fb5d2f8e 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -56,6 +56,7 @@ namespace osu.Game.Audio /// /// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first). /// + [JsonIgnore] public virtual IEnumerable LookupNames { get From dda4d76d726592aa3fa7c23639611b3e6694b56d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 18:09:21 +0900 Subject: [PATCH 5034/6909] Fix bad equality comparer implementations --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 2 +- osu.Game/Audio/HitSampleInfo.cs | 2 +- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index ec4b67f341..7734ebed12 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Objects => other != null; public override bool Equals(object obj) - => Equals((BananaHitSampleInfo)obj); + => obj is BananaHitSampleInfo other && Equals(other); public override int GetHashCode() => lookup_names.GetHashCode(); } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 27fb5d2f8e..9bd34c009b 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -83,7 +83,7 @@ namespace osu.Game.Audio => other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix; public override bool Equals(object? obj) - => Equals((HitSampleInfo?)obj); + => obj is HitSampleInfo other && Equals(other); public override int GetHashCode() => HashCode.Combine(Name, Bank, Suffix); } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 64694fb12e..015d6a3dd6 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -496,7 +496,8 @@ namespace osu.Game.Rulesets.Objects.Legacy public bool Equals(LegacyHitSampleInfo? other) => base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered; - public override bool Equals(object? obj) => Equals((LegacyHitSampleInfo?)obj); + public override bool Equals(object? obj) + => obj is LegacyHitSampleInfo other && Equals(other); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered); } @@ -529,7 +530,7 @@ namespace osu.Game.Rulesets.Objects.Legacy => base.Equals(other) && Filename == other.Filename; public override bool Equals(object? obj) - => Equals((FileHitSampleInfo?)obj); + => obj is FileHitSampleInfo other && Equals(other); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Filename); } From 17560aeeea75adb03347d31e02a85f2b930b0954 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 18:09:28 +0900 Subject: [PATCH 5035/6909] Volume should be 0 by default --- osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs | 2 +- osu.Game/Audio/HitSampleInfo.cs | 3 ++- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 0d57fb7029..f552c3c27b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests NewCombo = i % 8 == 0, Samples = new List(new[] { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal") + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 100) }) }); } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 9bd34c009b..58b078db71 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json; using osu.Game.Utils; namespace osu.Game.Audio @@ -45,7 +46,7 @@ namespace osu.Game.Audio /// public int Volume { get; } - public HitSampleInfo(string name, string? bank = null, string? suffix = null, int volume = 100) + public HitSampleInfo(string name, string? bank = null, string? suffix = null, int volume = 0) { Name = name; Bank = bank; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 015d6a3dd6..762edf5a13 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -479,7 +479,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// public readonly bool IsLayered; - public LegacyHitSampleInfo(string name, string? bank = null, int volume = 100, int customSampleBank = 0, bool isLayered = false) + public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, int customSampleBank = 0, bool isLayered = false) : base(name, bank, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume) { CustomSampleBank = customSampleBank; From c61f00525dfe57252b43df938671ab75ba29be96 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 18:08:59 +0900 Subject: [PATCH 5036/6909] Ignore lookup types for JSON serialisation --- osu.Game/Audio/HitSampleInfo.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 4a0e48749a..27fb5d2f8e 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -56,6 +56,7 @@ namespace osu.Game.Audio /// /// Retrieve all possible filenames that can be used as a source, returned in order of preference (highest first). /// + [JsonIgnore] public virtual IEnumerable LookupNames { get From 6b4a6c12c81a8f22a8b1b0cb012b011d1c9f5275 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 18:09:21 +0900 Subject: [PATCH 5037/6909] Fix bad equality comparer implementations --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 2 +- osu.Game/Audio/HitSampleInfo.cs | 2 +- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index ec4b67f341..7734ebed12 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Objects => other != null; public override bool Equals(object obj) - => Equals((BananaHitSampleInfo)obj); + => obj is BananaHitSampleInfo other && Equals(other); public override int GetHashCode() => lookup_names.GetHashCode(); } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 27fb5d2f8e..9bd34c009b 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -83,7 +83,7 @@ namespace osu.Game.Audio => other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix; public override bool Equals(object? obj) - => Equals((HitSampleInfo?)obj); + => obj is HitSampleInfo other && Equals(other); public override int GetHashCode() => HashCode.Combine(Name, Bank, Suffix); } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 5895059df5..a7290401bc 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -496,7 +496,8 @@ namespace osu.Game.Rulesets.Objects.Legacy public bool Equals(LegacyHitSampleInfo? other) => base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered; - public override bool Equals(object? obj) => Equals((LegacyHitSampleInfo?)obj); + public override bool Equals(object? obj) + => obj is LegacyHitSampleInfo other && Equals(other); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), CustomSampleBank, IsLayered); } @@ -526,7 +527,7 @@ namespace osu.Game.Rulesets.Objects.Legacy => base.Equals(other) && Filename == other.Filename; public override bool Equals(object? obj) - => Equals((FileHitSampleInfo?)obj); + => obj is FileHitSampleInfo other && Equals(other); public override int GetHashCode() => HashCode.Combine(base.GetHashCode(), Filename); } From 284040511289654bf38d18567426edc285f393c5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 18:09:28 +0900 Subject: [PATCH 5038/6909] Volume should be 0 by default --- osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs | 2 +- osu.Game/Audio/HitSampleInfo.cs | 3 ++- osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 0d57fb7029..f552c3c27b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -43,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Tests NewCombo = i % 8 == 0, Samples = new List(new[] { - new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal") + new HitSampleInfo(HitSampleInfo.HIT_NORMAL, "normal", volume: 100) }) }); } diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 9bd34c009b..58b078db71 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json; using osu.Game.Utils; namespace osu.Game.Audio @@ -45,7 +46,7 @@ namespace osu.Game.Audio /// public int Volume { get; } - public HitSampleInfo(string name, string? bank = null, string? suffix = null, int volume = 100) + public HitSampleInfo(string name, string? bank = null, string? suffix = null, int volume = 0) { Name = name; Bank = bank; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index a7290401bc..2cb72e9e80 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -479,7 +479,7 @@ namespace osu.Game.Rulesets.Objects.Legacy /// public readonly bool IsLayered; - public LegacyHitSampleInfo([NotNull] string name, string? bank = null, int volume = 100, int customSampleBank = 0, bool isLayered = false) + public LegacyHitSampleInfo(string name, string? bank = null, int volume = 0, int customSampleBank = 0, bool isLayered = false) : base(name, bank, customSampleBank >= 2 ? customSampleBank.ToString() : null, volume) { CustomSampleBank = customSampleBank; From 598572195c0f04acdc5c6785a88668464107f108 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 1 Dec 2020 19:57:25 +0900 Subject: [PATCH 5039/6909] Add playlist length to match settings overlay --- .../Match/Components/MatchSettingsOverlay.cs | 21 +++++++++++++++++++ osu.Game/Tests/Beatmaps/TestBeatmap.cs | 1 + 2 files changed, 22 insertions(+) diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index caefc194b1..8bf66b084c 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -2,7 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Specialized; +using System.Linq; using Humanizer; +using Humanizer.Localisation; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -69,6 +72,7 @@ namespace osu.Game.Screens.Multi.Match.Components private OsuSpriteText typeLabel; private LoadingLayer loadingLayer; private DrawableRoomPlaylist playlist; + private OsuSpriteText playlistLength; [Resolved(CanBeNull = true)] private IRoomManager manager { get; set; } @@ -229,6 +233,15 @@ namespace osu.Game.Screens.Multi.Match.Components playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both } }, new Drawable[] + { + playlistLength = new OsuSpriteText + { + Margin = new MarginPadding { Vertical = 5 }, + Colour = colours.Yellow, + Font = OsuFont.GetFont(size: 12), + } + }, + new Drawable[] { new PurpleTriangleButton { @@ -243,6 +256,7 @@ namespace osu.Game.Screens.Multi.Match.Components { new Dimension(), new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), } } }, @@ -315,6 +329,7 @@ namespace osu.Game.Screens.Multi.Match.Components Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue, true); playlist.Items.BindTo(Playlist); + Playlist.BindCollectionChanged(onPlaylistChanged, true); } protected override void Update() @@ -324,6 +339,12 @@ namespace osu.Game.Screens.Multi.Match.Components ApplyButton.Enabled.Value = hasValidSettings; } + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) + { + double totalLength = Playlist.Select(p => p.Beatmap.Value.Length).Sum(); + playlistLength.Text = $"Length: {totalLength.Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2)}"; + } + private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; private void apply() diff --git a/osu.Game/Tests/Beatmaps/TestBeatmap.cs b/osu.Game/Tests/Beatmaps/TestBeatmap.cs index 87b77f4616..035cb64099 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -31,6 +31,7 @@ namespace osu.Game.Tests.Beatmaps BeatmapInfo.BeatmapSet.Metadata = BeatmapInfo.Metadata; BeatmapInfo.BeatmapSet.Files = new List(); BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo }; + BeatmapInfo.Length = 75000; BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo { Status = BeatmapSetOnlineStatus.Ranked, From b236c75ac829f2cf76dcfd2f41da6c7f22c89f45 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 1 Dec 2020 17:32:24 +0000 Subject: [PATCH 5040/6909] Bump Microsoft.Win32.Registry from 4.7.0 to 5.0.0 Bumps [Microsoft.Win32.Registry](https://github.com/dotnet/runtime) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/commits) Signed-off-by: dependabot-preview[bot] --- osu.Desktop/osu.Desktop.csproj | 2 +- osu.Game.Tournament/osu.Game.Tournament.csproj | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 62e8f7c518..7b80ca64d0 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -28,7 +28,7 @@ - + diff --git a/osu.Game.Tournament/osu.Game.Tournament.csproj b/osu.Game.Tournament/osu.Game.Tournament.csproj index 9cce40c9d3..b049542bb0 100644 --- a/osu.Game.Tournament/osu.Game.Tournament.csproj +++ b/osu.Game.Tournament/osu.Game.Tournament.csproj @@ -9,6 +9,6 @@ - + \ No newline at end of file From 94a8784e04e2bd1157e0bb404fbf616148d82672 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Dec 2020 20:08:31 +0100 Subject: [PATCH 5041/6909] Allow editor change handler to be null --- .../Compose/Components/Timeline/TimelineTickDisplay.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs index 71268a1e69..fb11b859a7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineTickDisplay.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private BindableBeatDivisor beatDivisor { get; set; } - [Resolved] + [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } [Resolved] @@ -43,8 +43,9 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { beatDivisor.BindValueChanged(_ => invalidateTicks()); - // currently this is the best way to handle any kind of timing changes. - changeHandler.OnStateChange += invalidateTicks; + if (changeHandler != null) + // currently this is the best way to handle any kind of timing changes. + changeHandler.OnStateChange += invalidateTicks; } private void invalidateTicks() From 2e5a40eddf839b6ce4cfbca727db92828e504562 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 1 Dec 2020 20:12:09 +0100 Subject: [PATCH 5042/6909] Add an IntentFilter to handle osu! files. --- osu.Android/OsuGameActivity.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 7e250dce0e..b6e5742332 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using Android.App; +using Android.Content; using Android.Content.PM; using Android.OS; using Android.Views; @@ -9,7 +10,9 @@ using osu.Framework.Android; namespace osu.Android { + [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] + [IntentFilter(new[] { Intent.ActionDefault }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable, Intent.CategoryAppFiles }, DataSchemes = new[] { "content" }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] public class OsuGameActivity : AndroidGameActivity { protected override Framework.Game CreateGame() => new OsuGameAndroid(this); From 005fa3a7eed9230828d7337cc200a405984bab6a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 1 Dec 2020 20:28:15 +0100 Subject: [PATCH 5043/6909] Add ability to import files from a stream. --- osu.Game/Database/ArchiveModelManager.cs | 8 ++++++++ osu.Game/Database/ICanAcceptFiles.cs | 9 +++++++++ osu.Game/OsuGameBase.cs | 11 +++++++++++ osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 2 ++ 4 files changed, 30 insertions(+) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 8bdc804311..9a7aa7b039 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -212,6 +212,14 @@ namespace osu.Game.Database return import; } + /// + /// Import one from a . + /// + /// The stream to import files from. + /// The filename of the archive being imported. + public async Task Import(Stream stream, string filename) + => await Import(new ZipArchiveReader(stream, filename)); // we need to keep around the filename as some model managers (namely SkinManager) use the archive name to populate skin info + /// /// Fired when the user requests to view the resulting import. /// diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index e4d92d957c..111701f0a4 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.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.Threading.Tasks; namespace osu.Game.Database @@ -17,6 +18,14 @@ namespace osu.Game.Database /// The files which should be imported. Task Import(params string[] paths); + /// + /// Import the specified stream. + /// + /// The stream to import files from. + /// The filename of the archive being imported. + /// + Task Import(Stream stream, string filename); + /// /// An array of accepted file extensions (in the standard format of ".abc"). /// diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e7b5d3304d..0fc2b8d1d7 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -395,6 +395,17 @@ namespace osu.Game } } + public async Task Import(Stream stream, string filename) + { + var extension = Path.GetExtension(filename)?.ToLowerInvariant(); + + foreach (var importer in fileImporters) + { + if (importer.HandledExtensions.Contains(extension)) + await importer.Import(stream, Path.GetFileNameWithoutExtension(filename)); + } + } + public IEnumerable HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 17ecfdd52e..1527f240cb 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -99,6 +99,8 @@ namespace osu.Game.Screens.Edit.Setup return Task.CompletedTask; } + Task ICanAcceptFiles.Import(Stream stream, string filename) => Task.CompletedTask; + protected override void LoadComplete() { base.LoadComplete(); From 03f5460dd2e269c28ae116df0ebd7317f7df9137 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Dec 2020 21:57:16 +0100 Subject: [PATCH 5044/6909] Mark OsuModTestScene as abstract --- osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs index 7697f46160..d3cb3bcf59 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/OsuModTestScene.cs @@ -5,7 +5,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Osu.Tests.Mods { - public class OsuModTestScene : ModTestScene + public abstract class OsuModTestScene : ModTestScene { protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); } From c9429632f484516b874a91215213cb0fca970a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 1 Dec 2020 22:39:04 +0100 Subject: [PATCH 5045/6909] Resolve new NRE inspections --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Game.Tournament/IPC/FileBasedIPC.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index c62d2ee99f..62d8c17058 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.Tournament/IPC/FileBasedIPC.cs b/osu.Game.Tournament/IPC/FileBasedIPC.cs index 999ce61ac8..71417d1cc6 100644 --- a/osu.Game.Tournament/IPC/FileBasedIPC.cs +++ b/osu.Game.Tournament/IPC/FileBasedIPC.cs @@ -243,7 +243,7 @@ namespace osu.Game.Tournament.IPC string stableInstallPath; 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 (ipcFileExistsInDirectory(stableInstallPath)) return stableInstallPath; From 477de1bab0483a6b3e22b82a7fc30eaf4c9610aa Mon Sep 17 00:00:00 2001 From: Pennek <20502902+Pennek@users.noreply.github.com> Date: Wed, 2 Dec 2020 01:11:24 +0100 Subject: [PATCH 5046/6909] change min/max values --- osu.Game/Screens/Edit/Setup/DifficultySection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index aa1d57db31..897ddc6955 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -37,8 +37,8 @@ namespace osu.Game.Screens.Edit.Setup Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, - MinValue = 2, - MaxValue = 7, + MinValue = 0, + MaxValue = 10, Precision = 0.1f, } }, From a9003466e301abe093880aa36ff97e27d0048e9f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 2 Dec 2020 00:56:05 +0000 Subject: [PATCH 5047/6909] Bump Microsoft.Build.Traversal from 2.2.3 to 3.0.2 Bumps [Microsoft.Build.Traversal](https://github.com/Microsoft/MSBuildSdks) from 2.2.3 to 3.0.2. - [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.2.3...Microsoft.Build.Traversal.3.0.2) 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 10b61047ac..2c93a533e4 100644 --- a/global.json +++ b/global.json @@ -5,6 +5,6 @@ "version": "3.1.100" }, "msbuild-sdks": { - "Microsoft.Build.Traversal": "2.2.3" + "Microsoft.Build.Traversal": "3.0.2" } } \ No newline at end of file From aac20eef441091e6f91000b7c76951d71ced3827 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 2 Dec 2020 00:56:05 +0000 Subject: [PATCH 5048/6909] Bump System.IO.Packaging from 4.7.0 to 5.0.0 Bumps [System.IO.Packaging](https://github.com/dotnet/runtime) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/commits) Signed-off-by: dependabot-preview[bot] --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 7b80ca64d0..47c0d805b5 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,7 +24,7 @@ - + From 64dfa9f9285affbb68d027782ec9cd69dad6068e Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 2 Dec 2020 00:56:06 +0000 Subject: [PATCH 5049/6909] Bump Dapper from 2.0.35 to 2.0.78 Bumps [Dapper](https://github.com/StackExchange/Dapper) from 2.0.35 to 2.0.78. - [Release notes](https://github.com/StackExchange/Dapper/releases) - [Commits](https://github.com/StackExchange/Dapper/commits) 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 88af0e2138..4de711eb8b 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,7 @@ - + From 8a65328a6d5914abe07de7eddf0e2a7f234a5113 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 2 Dec 2020 00:56:08 +0000 Subject: [PATCH 5050/6909] Bump DiscordRichPresence from 1.0.150 to 1.0.166 Bumps [DiscordRichPresence](https://github.com/Lachee/discord-rpc-csharp) from 1.0.150 to 1.0.166. - [Release notes](https://github.com/Lachee/discord-rpc-csharp/releases) - [Commits](https://github.com/Lachee/discord-rpc-csharp/commits) Signed-off-by: dependabot-preview[bot] --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 7b80ca64d0..51300dcef8 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -29,7 +29,7 @@ - + From 5772a0811c69d8b232a03d712b839a1dd1e870bf Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 2 Dec 2020 00:56:09 +0000 Subject: [PATCH 5051/6909] Bump Microsoft.NET.Test.Sdk from 16.7.1 to 16.8.0 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.7.1 to 16.8.0. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.7.1...v16.8.0) 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 dfe3bf8af4..61ecd79e3d 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 892f27d27f..fa7bfd7169 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 3639c3616f..d6a03da807 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 b59f3a4344..a89645d881 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 c692bcd5e4..83d7b4135a 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 5d55196dcf..bc6b994988 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 249793d10a3cf8ab5f3ed5242d295097e637aedd Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 2 Dec 2020 00:56:09 +0000 Subject: [PATCH 5052/6909] Bump Sentry from 2.1.6 to 2.1.8 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 2.1.6 to 2.1.8. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Changelog](https://github.com/getsentry/sentry-dotnet/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/2.1.6...2.1.8) 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 88af0e2138..3fa703cf8f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -28,7 +28,7 @@ - + From 989ddd40b42a0766c773eba11ad8ec7f67ea2c44 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 2 Dec 2020 00:56:13 +0000 Subject: [PATCH 5053/6909] Bump System.ComponentModel.Annotations from 4.7.0 to 5.0.0 Bumps [System.ComponentModel.Annotations](https://github.com/dotnet/runtime) from 4.7.0 to 5.0.0. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/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 88af0e2138..923f8f7866 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -31,6 +31,6 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 06b0ec8a13..b32d3f900a 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -92,7 +92,7 @@ - + From e19ef9627a1b0f75b68d184ea5fe5b8d0576dc4d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Dec 2020 10:54:26 +0900 Subject: [PATCH 5054/6909] Fix potentially incorrect override --- .../Rulesets/Objects/Legacy/ConvertHitObjectParser.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 2cb72e9e80..119c9ccd7c 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -486,11 +486,11 @@ namespace osu.Game.Rulesets.Objects.Legacy IsLayered = isLayered; } - public override HitSampleInfo With(Optional name = default, Optional bank = default, Optional suffix = default, Optional volume = default) + public sealed override HitSampleInfo With(Optional name = default, Optional bank = default, Optional suffix = default, Optional volume = default) => With(name, bank, volume); - public LegacyHitSampleInfo With(Optional name = default, Optional bank = default, Optional volume = default, Optional customSampleBank = default, - Optional isLayered = default) + public virtual LegacyHitSampleInfo With(Optional name = default, Optional bank = default, Optional volume = default, Optional customSampleBank = default, + Optional isLayered = default) => new LegacyHitSampleInfo(name.GetOr(Name), bank.GetOr(Bank), volume.GetOr(Volume), customSampleBank.GetOr(CustomSampleBank), isLayered.GetOr(IsLayered)); public bool Equals(LegacyHitSampleInfo? other) @@ -520,7 +520,8 @@ namespace osu.Game.Rulesets.Objects.Legacy Path.ChangeExtension(Filename, null) }; - public override HitSampleInfo With(Optional name = default, Optional bank = default, Optional suffix = default, Optional volume = default) + public sealed override LegacyHitSampleInfo With(Optional name = default, Optional bank = default, Optional volume = default, Optional customSampleBank = default, + Optional isLayered = default) => new FileHitSampleInfo(Filename, volume.GetOr(Volume)); public bool Equals(FileHitSampleInfo? other) From 2150cf1c52336cc624e7bc13fb4eb78ae18d1bd9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Dec 2020 10:55:48 +0900 Subject: [PATCH 5055/6909] Rename parameters --- osu.Game/Audio/HitSampleInfo.cs | 12 ++++++------ .../Beatmaps/ControlPoints/SampleControlPoint.cs | 2 +- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 2 +- .../Objects/Legacy/ConvertHitObjectParser.cs | 16 ++++++++-------- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game/Audio/HitSampleInfo.cs b/osu.Game/Audio/HitSampleInfo.cs index 58b078db71..3d90dd0189 100644 --- a/osu.Game/Audio/HitSampleInfo.cs +++ b/osu.Game/Audio/HitSampleInfo.cs @@ -72,13 +72,13 @@ namespace osu.Game.Audio /// /// Creates a new with overridden values. /// - /// An optional new sample name. - /// An optional new sample bank. - /// An optional new lookup suffix. - /// An optional new volume. + /// An optional new sample name. + /// An optional new sample bank. + /// An optional new lookup suffix. + /// An optional new volume. /// The new . - public virtual HitSampleInfo With(Optional name = default, Optional bank = default, Optional suffix = default, Optional volume = default) - => new HitSampleInfo(name.GetOr(Name), bank.GetOr(Bank), suffix.GetOr(Suffix), volume.GetOr(Volume)); + public virtual HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) + => new HitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newSuffix.GetOr(Suffix), newVolume.GetOr(Volume)); public bool Equals(HitSampleInfo? other) => other != null && Name == other.Name && Bank == other.Bank && Suffix == other.Suffix; diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index 8064da1543..fd0b496335 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -66,7 +66,7 @@ namespace osu.Game.Beatmaps.ControlPoints /// The . This will not be modified. /// The modified . This does not share a reference with . public virtual HitSampleInfo ApplyTo(HitSampleInfo hitSampleInfo) - => hitSampleInfo.With(bank: hitSampleInfo.Bank ?? SampleBank, volume: hitSampleInfo.Volume > 0 ? hitSampleInfo.Volume : SampleVolume); + => hitSampleInfo.With(newBank: hitSampleInfo.Bank ?? SampleBank, newVolume: hitSampleInfo.Volume > 0 ? hitSampleInfo.Volume : SampleVolume); public override bool IsRedundant(ControlPoint existing) => existing is SampleControlPoint existingSample diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 9f16180e77..c9d139bdd0 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -183,7 +183,7 @@ namespace osu.Game.Beatmaps.Formats var baseInfo = base.ApplyTo(hitSampleInfo); if (baseInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0) - return legacy.With(customSampleBank: CustomSampleBank); + return legacy.With(newCustomSampleBank: CustomSampleBank); return baseInfo; } diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 119c9ccd7c..72025de131 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -486,12 +486,12 @@ namespace osu.Game.Rulesets.Objects.Legacy IsLayered = isLayered; } - public sealed override HitSampleInfo With(Optional name = default, Optional bank = default, Optional suffix = default, Optional volume = default) - => With(name, bank, volume); + public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) + => With(newName, newBank, newVolume); - public virtual LegacyHitSampleInfo With(Optional name = default, Optional bank = default, Optional volume = default, Optional customSampleBank = default, - Optional isLayered = default) - => new LegacyHitSampleInfo(name.GetOr(Name), bank.GetOr(Bank), volume.GetOr(Volume), customSampleBank.GetOr(CustomSampleBank), isLayered.GetOr(IsLayered)); + public virtual LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newCustomSampleBank = default, + Optional newIsLayered = default) + => new LegacyHitSampleInfo(newName.GetOr(Name), newBank.GetOr(Bank), newVolume.GetOr(Volume), newCustomSampleBank.GetOr(CustomSampleBank), newIsLayered.GetOr(IsLayered)); public bool Equals(LegacyHitSampleInfo? other) => base.Equals(other) && CustomSampleBank == other.CustomSampleBank && IsLayered == other.IsLayered; @@ -520,9 +520,9 @@ namespace osu.Game.Rulesets.Objects.Legacy Path.ChangeExtension(Filename, null) }; - public sealed override LegacyHitSampleInfo With(Optional name = default, Optional bank = default, Optional volume = default, Optional customSampleBank = default, - Optional isLayered = default) - => new FileHitSampleInfo(Filename, volume.GetOr(Volume)); + public sealed override LegacyHitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newVolume = default, Optional newCustomSampleBank = default, + Optional newIsLayered = default) + => new FileHitSampleInfo(Filename, newVolume.GetOr(Volume)); public bool Equals(FileHitSampleInfo? other) => base.Equals(other) && Filename == other.Filename; From 2de3e655e07ad95dfdd9b25ee5d0a440567d7cb8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Dec 2020 12:59:45 +0900 Subject: [PATCH 5056/6909] Rename NextUlong -> NextULong --- osu.Game/Utils/StatelessRNG.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Utils/StatelessRNG.cs b/osu.Game/Utils/StatelessRNG.cs index 11d079498a..118b08fe30 100644 --- a/osu.Game/Utils/StatelessRNG.cs +++ b/osu.Game/Utils/StatelessRNG.cs @@ -33,7 +33,7 @@ namespace osu.Game.Utils /// The series number. /// Different values are computed for the same seed in different series. /// - public static ulong NextUlong(int seed, int series = 0) + public static ulong NextULong(int seed, int series = 0) { unchecked { @@ -60,7 +60,7 @@ namespace osu.Game.Utils { if (maxValue <= 0) throw new ArgumentOutOfRangeException(nameof(maxValue)); - return (int)(NextUlong(seed, series) % (ulong)maxValue); + return (int)(NextULong(seed, series) % (ulong)maxValue); } /// @@ -74,6 +74,6 @@ namespace osu.Game.Utils /// Different values are computed for the same seed in different series. /// public static float NextSingle(int seed, int series = 0) => - (float)(NextUlong(seed, series) & ((1 << 24) - 1)) / (1 << 24); // float has 24-bit precision + (float)(NextULong(seed, series) & ((1 << 24) - 1)) / (1 << 24); // float has 24-bit precision } } From e84dab858991fdf0363a93b9269789df4208ce00 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Dec 2020 14:36:52 +0900 Subject: [PATCH 5057/6909] Move new beatmap construction above beat divisor binding --- osu.Game/Screens/Edit/Editor.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 8f1cd8e28e..ca7e5fbf20 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -106,15 +106,15 @@ namespace osu.Game.Screens.Edit [BackgroundDependencyLoader] private void load(OsuColour colours, GameHost host, OsuConfigManager config) { - beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor; - beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); - if (Beatmap.Value is DummyWorkingBeatmap) { isNewBeatmap = true; Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); } + beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor; + beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); + // Todo: should probably be done at a DrawableRuleset level to share logic with Player. clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; From 946613e803f27025103eb8b0232a4562939877b3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Dec 2020 15:22:45 +0900 Subject: [PATCH 5058/6909] Fix bananas not playing sounds --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 7734ebed12..a274f25200 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.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. +#nullable enable + using System; using System.Collections.Generic; using osu.Framework.Utils; @@ -8,6 +10,7 @@ using osu.Game.Audio; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Utils; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects @@ -53,19 +56,22 @@ namespace osu.Game.Rulesets.Catch.Objects private class BananaHitSampleInfo : HitSampleInfo, IEquatable { - private static readonly string[] lookup_names = { "metronomelow", "catch-banana" }; + private static readonly string[] lookup_names = { "Gameplay/metronomelow", "Gameplay/catch-banana" }; public override IEnumerable LookupNames => lookup_names; - public BananaHitSampleInfo() - : base(string.Empty) + public BananaHitSampleInfo(int volume = 0) + : base(string.Empty, volume: volume) { } - public bool Equals(BananaHitSampleInfo other) + public sealed override HitSampleInfo With(Optional newName = default, Optional newBank = default, Optional newSuffix = default, Optional newVolume = default) + => new BananaHitSampleInfo(newVolume.GetOr(Volume)); + + public bool Equals(BananaHitSampleInfo? other) => other != null; - public override bool Equals(object obj) + public override bool Equals(object? obj) => obj is BananaHitSampleInfo other && Equals(other); public override int GetHashCode() => lookup_names.GetHashCode(); From 32188418f446f4624b7ecb90e1144bfa57923ae4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 2 Dec 2020 15:28:39 +0900 Subject: [PATCH 5059/6909] 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 aa4d9fa4ee..9d99218f88 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 53b854caa3..4b931726e0 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 b32d3f900a..3a47b77820 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From e7c0e9834f41f5f52fb3b331a02c4742c84bf342 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 16:53:01 +0900 Subject: [PATCH 5060/6909] Introduce RandomSeed in catch DHO --- .../Drawables/DrawableCatchHitObject.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 1faa6a5b0f..eb5f9451b1 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -3,10 +3,13 @@ using System; using JetBrains.Annotations; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Utils; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -20,12 +23,32 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; + /// + /// The seed value used for visual randomness such as fruit rotation. + /// By default, truncated to an integer is used. + /// + public Bindable RandomSeed = new Bindable(); + protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject) : base(hitObject) { Anchor = Anchor.BottomLeft; } + [BackgroundDependencyLoader] + private void load() + { + StartTimeBindable.BindValueChanged(change => + { + RandomSeed.Value = (int)change.NewValue; + }, true); + } + + /// + /// Get a random number in range [0,1) based on seed . + /// + public float RandomSingle(int series) => StatelessRNG.NextSingle(RandomSeed.Value, series); + protected override void OnApply() { base.OnApply(); From 8b6161a51c27fce87f51a171a2a6f4c2e402e212 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 16:54:18 +0900 Subject: [PATCH 5061/6909] Use deterministic randomness in catch hit object --- .../Objects/Drawables/DrawableBanana.cs | 20 ++++++++++++++----- .../Objects/Drawables/DrawableDroplet.cs | 3 +-- .../Objects/Drawables/DrawableFruit.cs | 8 +++++--- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index fb982bbdab..4e34dd2b90 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -3,7 +3,6 @@ using JetBrains.Annotations; using osu.Framework.Graphics; -using osu.Framework.Utils; namespace osu.Game.Rulesets.Catch.Objects.Drawables { @@ -21,6 +20,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { } + protected override void LoadComplete() + { + base.LoadComplete(); + + RandomSeed.BindValueChanged(_ => + { + UpdateComboColour(); + UpdateInitialTransforms(); + }); + } + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); @@ -28,14 +38,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables const float end_scale = 0.6f; const float random_scale_range = 1.6f; - ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RNG.NextSingle())) + ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3))) .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); - ScaleContainer.RotateTo(getRandomAngle()) + ScaleContainer.RotateTo(getRandomAngle(1)) .Then() - .RotateTo(getRandomAngle(), HitObject.TimePreempt); + .RotateTo(getRandomAngle(2), HitObject.TimePreempt); - float getRandomAngle() => 180 * (RNG.NextSingle() * 2 - 1); + float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1); } public override void PlaySamples() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 06ecd44488..b8acea625b 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -4,7 +4,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -45,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.UpdateInitialTransforms(); // roughly matches osu-stable - float startRotation = RNG.NextSingle() * 20; + float startRotation = RandomSingle(1) * 20; double duration = HitObject.TimePreempt + 2000; 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 68cb649b66..010a3ee08c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -5,7 +5,6 @@ using System; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Utils; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -30,8 +29,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - ScaleContainer.Rotation = (float)(RNG.NextDouble() - 0.5f) * 40; - IndexInBeatmap.BindValueChanged(change => { VisualRepresentation.Value = GetVisualRepresentation(change.NewValue); @@ -39,6 +36,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables VisualRepresentation.BindValueChanged(_ => updatePiece()); HyperDash.BindValueChanged(_ => updatePiece(), true); + + RandomSeed.BindValueChanged(_ => + { + ScaleContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; + }, true); } private void updatePiece() From 8a78c495f22416e533249ae4173bb2e81b6411c4 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 16:55:37 +0900 Subject: [PATCH 5062/6909] Refactor DHO testing logic to the "specimen" class --- .../TestSceneFruitObjects.cs | 34 +++++++++++++------ .../TestSceneFruitVisualChange.cs | 4 +-- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs index 160da75aa9..3a651605d3 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitObjects.cs @@ -3,11 +3,12 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osuTK; namespace osu.Game.Rulesets.Catch.Tests { @@ -37,39 +38,50 @@ namespace osu.Game.Rulesets.Catch.Tests } private Drawable createDrawableFruit(int indexInBeatmap, bool hyperdash = false) => - SetProperties(new DrawableFruit(new Fruit + new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit { IndexInBeatmap = indexInBeatmap, HyperDashBindable = { Value = hyperdash } })); private Drawable createDrawableBanana() => - SetProperties(new DrawableBanana(new Banana())); + new TestDrawableCatchHitObjectSpecimen(new DrawableBanana(new Banana())); private Drawable createDrawableDroplet(bool hyperdash = false) => - SetProperties(new DrawableDroplet(new Droplet + new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet { HyperDashBindable = { Value = hyperdash } })); - private Drawable createDrawableTinyDroplet() => SetProperties(new DrawableTinyDroplet(new TinyDroplet())); + private Drawable createDrawableTinyDroplet() => new TestDrawableCatchHitObjectSpecimen(new DrawableTinyDroplet(new TinyDroplet())); + } - protected virtual DrawableCatchHitObject SetProperties(DrawableCatchHitObject d) + public class TestDrawableCatchHitObjectSpecimen : CompositeDrawable + { + public readonly ManualClock ManualClock; + + public TestDrawableCatchHitObjectSpecimen(DrawableCatchHitObject d) { + AutoSizeAxes = Axes.Both; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + ManualClock = new ManualClock(); + Clock = new FramedClock(ManualClock); + var hitObject = d.HitObject; - hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 0 }); - hitObject.StartTime = 1000000000000; + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); hitObject.Scale = 1.5f; + hitObject.StartTime = 500; d.Anchor = Anchor.Centre; - d.RelativePositionAxes = Axes.None; - d.Position = Vector2.Zero; d.HitObjectApplied += _ => { d.LifetimeStart = double.NegativeInfinity; d.LifetimeEnd = double.PositiveInfinity; }; - return d; + + InternalChild = d; } } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs index 4448e828e7..125e0c674c 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitVisualChange.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Catch.Tests protected override void LoadComplete() { - AddStep("fruit changes visual and hyper", () => SetContents(() => SetProperties(new DrawableFruit(new Fruit + AddStep("fruit changes visual and hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableFruit(new Fruit { IndexInBeatmapBindable = { BindTarget = indexInBeatmap }, HyperDashBindable = { BindTarget = hyperDash }, })))); - AddStep("droplet changes hyper", () => SetContents(() => SetProperties(new DrawableDroplet(new Droplet + AddStep("droplet changes hyper", () => SetContents(() => new TestDrawableCatchHitObjectSpecimen(new DrawableDroplet(new Droplet { HyperDashBindable = { BindTarget = hyperDash }, })))); From beda6961e433b7df28f69b898c975035e8ce24ee Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 16:55:58 +0900 Subject: [PATCH 5063/6909] Add a test for fruit randomness --- .../TestSceneFruitRandomness.cs | 95 +++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs new file mode 100644 index 0000000000..449cfac2db --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.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 NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneFruitRandomness : OsuTestScene + { + [Test] + public void TestFruitRandomness() + { + Bindable randomSeed = new Bindable(); + + TestDrawableFruit drawableFruit; + TestDrawableBanana drawableBanana; + + Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit = new TestDrawableFruit(new Fruit()) + { + RandomSeed = { BindTarget = randomSeed } + }) { X = -200 }); + Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana = new TestDrawableBanana(new Banana()) + { + RandomSeed = { BindTarget = randomSeed } + })); + + float fruitRotation = 0; + float bananaRotation = 0; + float bananaScale = 0; + + AddStep("set random seed to 0", () => + { + drawableFruit.HitObject.StartTime = 500; + randomSeed.Value = 0; + + fruitRotation = drawableFruit.InnerRotation; + bananaRotation = drawableBanana.InnerRotation; + bananaScale = drawableBanana.InnerScale; + }); + + AddStep("change random seed", () => + { + randomSeed.Value = 10; + }); + + AddAssert("fruit rotation is changed", () => drawableFruit.InnerRotation != fruitRotation); + AddAssert("banana rotation is changed", () => drawableBanana.InnerRotation != bananaRotation); + AddAssert("banana scale is changed", () => drawableBanana.InnerScale != bananaScale); + + AddStep("reset random seed", () => + { + randomSeed.Value = 0; + }); + + AddAssert("rotation and scale restored", () => + drawableFruit.InnerRotation == fruitRotation && + drawableBanana.InnerRotation == bananaRotation && + drawableBanana.InnerScale == bananaScale); + + AddStep("change start time", () => + { + drawableFruit.HitObject.StartTime = 1000; + }); + + AddAssert("random seed is changed", () => randomSeed.Value == 1000); + + AddSliderStep("random seed", 0, 100, 0, x => randomSeed.Value = x); + } + + private class TestDrawableFruit : DrawableFruit + { + public float InnerRotation => ScaleContainer.Rotation; + + public TestDrawableFruit(Fruit h) + : base(h) + { + } + } + + private class TestDrawableBanana : DrawableBanana + { + public float InnerRotation => ScaleContainer.Rotation; + public float InnerScale => ScaleContainer.Scale.X; + + public TestDrawableBanana(Banana h) + : base(h) + { + } + } + } +} From 08848e49de025022e9ae8ec0338e067987a53b03 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 17:12:30 +0900 Subject: [PATCH 5064/6909] Set banana combo colour using random seed --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 13 ++++--------- .../Objects/CatchHitObject.cs | 10 ++++++++++ .../Objects/Drawables/DrawableCatchHitObject.cs | 17 ++--------------- 3 files changed, 16 insertions(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 7734ebed12..9fd01b3717 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -3,11 +3,11 @@ using System; using System.Collections.Generic; -using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Utils; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects @@ -28,17 +28,12 @@ namespace osu.Game.Rulesets.Catch.Objects Samples = samples; } - private Color4? colour; - - Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) - { - // override any external colour changes with banananana - return colour ??= getBananaColour(); - } + // override any external colour changes with banananana + Color4 IHasComboInformation.GetComboColour(IReadOnlyList comboColours) => getBananaColour(); private Color4 getBananaColour() { - switch (RNG.Next(0, 3)) + switch (StatelessRNG.NextInt(3, RandomSeed.Value)) { default: return new Color4(255, 240, 0, 255); diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index a74055bff9..b9c5e42777 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -97,6 +97,12 @@ namespace osu.Game.Rulesets.Catch.Objects set => ScaleBindable.Value = value; } + /// + /// The seed value used for visual randomness such as fruit rotation. + /// By default, truncated to an integer is used. + /// + public Bindable RandomSeed = new Bindable(); + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); @@ -111,6 +117,10 @@ namespace osu.Game.Rulesets.Catch.Objects protected CatchHitObject() { XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset); + StartTimeBindable.BindValueChanged(change => + { + RandomSeed.Value = (int)change.NewValue; + }, true); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index eb5f9451b1..510431ff69 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -3,11 +3,9 @@ using System; using JetBrains.Annotations; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Utils; @@ -23,10 +21,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; - /// - /// The seed value used for visual randomness such as fruit rotation. - /// By default, truncated to an integer is used. - /// public Bindable RandomSeed = new Bindable(); protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject) @@ -35,15 +29,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Anchor = Anchor.BottomLeft; } - [BackgroundDependencyLoader] - private void load() - { - StartTimeBindable.BindValueChanged(change => - { - RandomSeed.Value = (int)change.NewValue; - }, true); - } - /// /// Get a random number in range [0,1) based on seed . /// @@ -54,6 +39,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.OnApply(); XBindable.BindTo(HitObject.XBindable); + RandomSeed.BindTo(HitObject.RandomSeed); } protected override void OnFree() @@ -61,6 +47,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.OnFree(); XBindable.UnbindFrom(HitObject.XBindable); + RandomSeed.UnbindFrom(HitObject.RandomSeed); } public Func CheckPosition; From ef741a1471170e62edbd34277a17e9966fcf97cf Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 17:16:36 +0900 Subject: [PATCH 5065/6909] Test banana colour change based on random seed --- osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 449cfac2db..244e2f14f4 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -6,6 +6,7 @@ using osu.Framework.Bindables; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Tests.Visual; +using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Tests { @@ -31,6 +32,7 @@ namespace osu.Game.Rulesets.Catch.Tests float fruitRotation = 0; float bananaRotation = 0; float bananaScale = 0; + Color4 bananaColour = new Color4(); AddStep("set random seed to 0", () => { @@ -40,16 +42,19 @@ namespace osu.Game.Rulesets.Catch.Tests fruitRotation = drawableFruit.InnerRotation; bananaRotation = drawableBanana.InnerRotation; bananaScale = drawableBanana.InnerScale; + bananaColour = drawableBanana.AccentColour.Value; }); AddStep("change random seed", () => { + // Use a seed value such that the banana colour is different (2/3 of the seed values are okay). randomSeed.Value = 10; }); AddAssert("fruit rotation is changed", () => drawableFruit.InnerRotation != fruitRotation); AddAssert("banana rotation is changed", () => drawableBanana.InnerRotation != bananaRotation); AddAssert("banana scale is changed", () => drawableBanana.InnerScale != bananaScale); + AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour); AddStep("reset random seed", () => { @@ -59,7 +64,8 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("rotation and scale restored", () => drawableFruit.InnerRotation == fruitRotation && drawableBanana.InnerRotation == bananaRotation && - drawableBanana.InnerScale == bananaScale); + drawableBanana.InnerScale == bananaScale && + drawableBanana.AccentColour.Value == bananaColour); AddStep("change start time", () => { From 1a6b8e022cba52b12289b2b541effaad186a360e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 17:20:52 +0900 Subject: [PATCH 5066/6909] Fix formatting --- .../TestSceneFruitRandomness.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 244e2f14f4..38efcd85b8 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -17,17 +17,14 @@ namespace osu.Game.Rulesets.Catch.Tests { Bindable randomSeed = new Bindable(); - TestDrawableFruit drawableFruit; - TestDrawableBanana drawableBanana; + var drawableFruit = new TestDrawableFruit(new Fruit()); + var drawableBanana = new TestDrawableBanana(new Banana()); - Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit = new TestDrawableFruit(new Fruit()) - { - RandomSeed = { BindTarget = randomSeed } - }) { X = -200 }); - Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana = new TestDrawableBanana(new Banana()) - { - RandomSeed = { BindTarget = randomSeed } - })); + drawableFruit.RandomSeed.BindTo(randomSeed); + drawableBanana.RandomSeed.BindTo(randomSeed); + + Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 }); + Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana)); float fruitRotation = 0; float bananaRotation = 0; From 5936a8ffb4a1ea87720b7436bfdddb31ce47d49d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 18:06:14 +0900 Subject: [PATCH 5067/6909] Fix drawables are added multiple times in a test scene --- .../TestSceneFruitRandomness.cs | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 38efcd85b8..5f7447323f 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -12,13 +12,14 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneFruitRandomness : OsuTestScene { - [Test] - public void TestFruitRandomness() - { - Bindable randomSeed = new Bindable(); + private readonly Bindable randomSeed = new Bindable(); + private readonly TestDrawableFruit drawableFruit; + private readonly TestDrawableBanana drawableBanana; - var drawableFruit = new TestDrawableFruit(new Fruit()); - var drawableBanana = new TestDrawableBanana(new Banana()); + public TestSceneFruitRandomness() + { + drawableFruit = new TestDrawableFruit(new Fruit()); + drawableBanana = new TestDrawableBanana(new Banana()); drawableFruit.RandomSeed.BindTo(randomSeed); drawableBanana.RandomSeed.BindTo(randomSeed); @@ -26,6 +27,12 @@ namespace osu.Game.Rulesets.Catch.Tests Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 }); Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana)); + AddSliderStep("random seed", 0, 100, 0, x => randomSeed.Value = x); + } + + [Test] + public void TestFruitRandomness() + { float fruitRotation = 0; float bananaRotation = 0; float bananaScale = 0; @@ -70,8 +77,6 @@ namespace osu.Game.Rulesets.Catch.Tests }); AddAssert("random seed is changed", () => randomSeed.Value == 1000); - - AddSliderStep("random seed", 0, 100, 0, x => randomSeed.Value = x); } private class TestDrawableFruit : DrawableFruit From d5dccbc3d783c5cc5cb7389fcc970a175ea133e6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 2 Dec 2020 19:02:49 +0900 Subject: [PATCH 5068/6909] Fix spectator not being thread-safe --- .../Spectator/SpectatorStreamingClient.cs | 33 ++++++++++++++----- 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 9ba81720d8..08b524087a 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -36,6 +36,8 @@ namespace osu.Game.Online.Spectator private readonly List watchingUsers = new List(); + private readonly object userLock = new object(); + public IBindableList PlayingUsers => playingUsers; private readonly BindableList playingUsers = new BindableList(); @@ -144,12 +146,19 @@ namespace osu.Game.Online.Spectator await connection.StartAsync(); Logger.Log("Spectator client connected!", LoggingTarget.Network); + // get all the users that were previously being watched + int[] users; + + lock (userLock) + { + users = watchingUsers.ToArray(); + watchingUsers.Clear(); + } + // success isConnected = true; // resubscribe to watched users - var users = watchingUsers.ToArray(); - watchingUsers.Clear(); foreach (var userId in users) WatchUser(userId); @@ -238,21 +247,29 @@ namespace osu.Game.Online.Spectator public virtual void WatchUser(int userId) { - if (watchingUsers.Contains(userId)) - return; + lock (userLock) + { + if (watchingUsers.Contains(userId)) + return; - watchingUsers.Add(userId); + watchingUsers.Add(userId); - if (!isConnected) return; + if (!isConnected) + return; + } connection.SendAsync(nameof(ISpectatorServer.StartWatchingUser), userId); } public void StopWatchingUser(int userId) { - watchingUsers.Remove(userId); + lock (userLock) + { + watchingUsers.Remove(userId); - if (!isConnected) return; + if (!isConnected) + return; + } connection.SendAsync(nameof(ISpectatorServer.EndWatchingUser), userId); } From fdcfa81e4627c129f7bbcfe32280b09556c96914 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 20:53:47 +0900 Subject: [PATCH 5069/6909] Make RandomSeed a property, not a bindable --- .../TestSceneFruitRandomness.cs | 35 ++++++++----------- osu.Game.Rulesets.Catch/Objects/Banana.cs | 2 +- .../Objects/CatchHitObject.cs | 8 ++--- .../Objects/Drawables/DrawableBanana.cs | 7 ++-- .../Drawables/DrawableCatchHitObject.cs | 6 ++-- .../Objects/Drawables/DrawableFruit.cs | 10 +++--- 6 files changed, 27 insertions(+), 41 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 5f7447323f..2ffebb7de1 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Framework.Bindables; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Tests.Visual; @@ -12,7 +11,6 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneFruitRandomness : OsuTestScene { - private readonly Bindable randomSeed = new Bindable(); private readonly TestDrawableFruit drawableFruit; private readonly TestDrawableBanana drawableBanana; @@ -21,27 +19,30 @@ namespace osu.Game.Rulesets.Catch.Tests drawableFruit = new TestDrawableFruit(new Fruit()); drawableBanana = new TestDrawableBanana(new Banana()); - drawableFruit.RandomSeed.BindTo(randomSeed); - drawableBanana.RandomSeed.BindTo(randomSeed); - Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 }); Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana)); - AddSliderStep("random seed", 0, 100, 0, x => randomSeed.Value = x); + AddSliderStep("start time", 500, 600, 0, x => + { + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = x; + }); } [Test] public void TestFruitRandomness() { + // Use values such that the banana colour changes (2/3 of the integers are okay) + const int initial_start_time = 500; + const int another_start_time = 501; + float fruitRotation = 0; float bananaRotation = 0; float bananaScale = 0; Color4 bananaColour = new Color4(); - AddStep("set random seed to 0", () => + AddStep("Initialize start time", () => { - drawableFruit.HitObject.StartTime = 500; - randomSeed.Value = 0; + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; fruitRotation = drawableFruit.InnerRotation; bananaRotation = drawableBanana.InnerRotation; @@ -49,10 +50,9 @@ namespace osu.Game.Rulesets.Catch.Tests bananaColour = drawableBanana.AccentColour.Value; }); - AddStep("change random seed", () => + AddStep("change start time", () => { - // Use a seed value such that the banana colour is different (2/3 of the seed values are okay). - randomSeed.Value = 10; + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; }); AddAssert("fruit rotation is changed", () => drawableFruit.InnerRotation != fruitRotation); @@ -60,9 +60,9 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("banana scale is changed", () => drawableBanana.InnerScale != bananaScale); AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour); - AddStep("reset random seed", () => + AddStep("reset start time", () => { - randomSeed.Value = 0; + drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; }); AddAssert("rotation and scale restored", () => @@ -70,13 +70,6 @@ namespace osu.Game.Rulesets.Catch.Tests drawableBanana.InnerRotation == bananaRotation && drawableBanana.InnerScale == bananaScale && drawableBanana.AccentColour.Value == bananaColour); - - AddStep("change start time", () => - { - drawableFruit.HitObject.StartTime = 1000; - }); - - AddAssert("random seed is changed", () => randomSeed.Value == 1000); } private class TestDrawableFruit : DrawableFruit diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 9fd01b3717..3a5e08b3c3 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Objects private Color4 getBananaColour() { - switch (StatelessRNG.NextInt(3, RandomSeed.Value)) + switch (StatelessRNG.NextInt(3, RandomSeed)) { default: return new Color4(255, 240, 0, 255); diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index b9c5e42777..b86b3a7496 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -99,9 +99,9 @@ namespace osu.Game.Rulesets.Catch.Objects /// /// The seed value used for visual randomness such as fruit rotation. - /// By default, truncated to an integer is used. + /// The value is truncated to an integer. /// - public Bindable RandomSeed = new Bindable(); + public int RandomSeed => (int)StartTime; protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) { @@ -117,10 +117,6 @@ namespace osu.Game.Rulesets.Catch.Objects protected CatchHitObject() { XBindable.BindValueChanged(x => originalX = x.NewValue - xOffset); - StartTimeBindable.BindValueChanged(change => - { - RandomSeed.Value = (int)change.NewValue; - }, true); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 4e34dd2b90..8e9d80106b 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -24,11 +24,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { base.LoadComplete(); - RandomSeed.BindValueChanged(_ => - { - UpdateComboColour(); - UpdateInitialTransforms(); - }); + // start time affects the random seed which is used to determine the banana colour + StartTimeBindable.BindValueChanged(_ => UpdateComboColour()); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 510431ff69..86c1c7d0cd 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; - public Bindable RandomSeed = new Bindable(); + public int RandomSeed => HitObject?.RandomSeed ?? 0; protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject) : base(hitObject) @@ -32,14 +32,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables /// /// Get a random number in range [0,1) based on seed . /// - public float RandomSingle(int series) => StatelessRNG.NextSingle(RandomSeed.Value, series); + public float RandomSingle(int series) => StatelessRNG.NextSingle(RandomSeed, series); protected override void OnApply() { base.OnApply(); XBindable.BindTo(HitObject.XBindable); - RandomSeed.BindTo(HitObject.RandomSeed); } protected override void OnFree() @@ -47,7 +46,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.OnFree(); XBindable.UnbindFrom(HitObject.XBindable); - RandomSeed.UnbindFrom(HitObject.RandomSeed); } public Func CheckPosition; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 010a3ee08c..56b9b56372 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -36,11 +36,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables VisualRepresentation.BindValueChanged(_ => updatePiece()); HyperDash.BindValueChanged(_ => updatePiece(), true); + } - RandomSeed.BindValueChanged(_ => - { - ScaleContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; - }, true); + protected override void UpdateInitialTransforms() + { + base.UpdateInitialTransforms(); + + ScaleContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; } private void updatePiece() From 827e957568d6efc53564193ae9196bbb13cc29bc Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 2 Dec 2020 18:03:49 +0100 Subject: [PATCH 5070/6909] Allow importing osz files / osk files from Downloads directory. --- osu.Android/OsuGameActivity.cs | 23 +++++++++++++++++++++-- osu.Game/OsuGame.cs | 16 ++++++++-------- 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index b6e5742332..c41323b97f 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.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.Linq; +using System.Threading.Tasks; using Android.App; using Android.Content; using Android.Content.PM; @@ -12,10 +14,12 @@ namespace osu.Android { [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] - [IntentFilter(new[] { Intent.ActionDefault }, Categories = new[] { Intent.CategoryDefault, Intent.CategoryBrowsable, Intent.CategoryAppFiles }, DataSchemes = new[] { "content" }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] + [IntentFilter(new[] { Intent.ActionDefault }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] public class OsuGameActivity : AndroidGameActivity { - protected override Framework.Game CreateGame() => new OsuGameAndroid(this); + private OsuGameAndroid game; + + protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); protected override void OnCreate(Bundle savedInstanceState) { @@ -26,8 +30,23 @@ namespace osu.Android base.OnCreate(savedInstanceState); + OnNewIntent(Intent); + Window.AddFlags(WindowManagerFlags.Fullscreen); Window.AddFlags(WindowManagerFlags.KeepScreenOn); } + + protected override void OnNewIntent(Intent intent) + { + if (intent.Action == Intent.ActionView) + { + var filename = intent.Data.Path.Split('/').Last(); + var stream = ContentResolver.OpenInputStream(intent.Data); + if (stream != null) + // intent handler may run before the game has even loaded so we need to wait for the file importers to load before launching import + game.WaitForReady(() => game, _ => Task.Run(() => game.Import(stream, filename))); + } + base.OnNewIntent(intent); + } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index bb638bcf3a..5b6b90fba5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -267,7 +267,7 @@ namespace osu.Game case LinkAction.OpenEditorTimestamp: case LinkAction.JoinMultiplayerMatch: case LinkAction.Spectate: - waitForReady(() => notifications, _ => notifications.Post(new SimpleNotification + WaitForReady(() => notifications, _ => notifications.Post(new SimpleNotification { Text = @"This link type is not yet supported!", Icon = FontAwesome.Solid.LifeRing, @@ -288,7 +288,7 @@ namespace osu.Game } }); - public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ => + public void OpenUrlExternally(string url) => WaitForReady(() => externalLinkOpener, _ => { if (url.StartsWith('/')) url = $"{API.Endpoint}{url}"; @@ -300,7 +300,7 @@ namespace osu.Game /// Open a specific channel in chat. /// /// The channel to display. - public void ShowChannel(string channel) => waitForReady(() => channelManager, _ => + public void ShowChannel(string channel) => WaitForReady(() => channelManager, _ => { try { @@ -316,19 +316,19 @@ namespace osu.Game /// Show a beatmap set as an overlay. /// /// The set to display. - public void ShowBeatmapSet(int setId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmapSet(setId)); + public void ShowBeatmapSet(int setId) => WaitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmapSet(setId)); /// /// Show a user's profile as an overlay. /// /// The user to display. - public void ShowUser(int userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId)); + public void ShowUser(int userId) => WaitForReady(() => userProfile, _ => userProfile.ShowUser(userId)); /// /// Show a beatmap's set as an overlay, displaying the given beatmap. /// /// The beatmap to show. - public void ShowBeatmap(int beatmapId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId)); + public void ShowBeatmap(int beatmapId) => WaitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId)); /// /// Present a beatmap at song select immediately. @@ -483,13 +483,13 @@ namespace osu.Game /// A function to retrieve a (potentially not-yet-constructed) target instance. /// The action to perform on the instance when load is confirmed. /// The type of the target instance. - private void waitForReady(Func retrieveInstance, Action action) + public void WaitForReady(Func retrieveInstance, Action action) where T : Drawable { var instance = retrieveInstance(); if (ScreenStack == null || ScreenStack.CurrentScreen is StartupScreen || instance?.IsLoaded != true) - Schedule(() => waitForReady(retrieveInstance, action)); + Schedule(() => WaitForReady(retrieveInstance, action)); else action(instance); } From bbde1f6b9c755df552ec02c8194866b51f17dbf2 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Tue, 1 Dec 2020 22:26:17 +0100 Subject: [PATCH 5071/6909] Fix MouseHandler not being ignored when raw input is enabled --- .../Settings/Sections/Input/MouseSettings.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index f0d51a0d37..b9e9de1200 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Graphics; +using osu.Framework.Input.Handlers.Mouse; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Input; @@ -18,7 +19,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private readonly BindableBool rawInputToggle = new BindableBool(); private Bindable sensitivityBindable = new BindableDouble(); - private Bindable ignoredInputHandler; + private Bindable ignoredInputHandlers; [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig, FrameworkConfigManager config) @@ -75,20 +76,21 @@ namespace osu.Game.Overlays.Settings.Sections.Input { // this is temporary until we support per-handler settings. const string raw_mouse_handler = @"OsuTKRawMouseHandler"; - const string standard_mouse_handler = @"OsuTKMouseHandler"; + const string osutk_standard_mouse_handler = @"OsuTKMouseHandler"; + string standardMouseHandlers = $"{osutk_standard_mouse_handler} {nameof(MouseHandler)}"; - ignoredInputHandler.Value = enabled.NewValue ? standard_mouse_handler : raw_mouse_handler; + ignoredInputHandlers.Value = enabled.NewValue ? standardMouseHandlers : raw_mouse_handler; }; - ignoredInputHandler = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); - ignoredInputHandler.ValueChanged += handler => + ignoredInputHandlers = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); + ignoredInputHandlers.ValueChanged += handler => { bool raw = !handler.NewValue.Contains("Raw"); rawInputToggle.Value = raw; sensitivityBindable.Disabled = !raw; }; - ignoredInputHandler.TriggerChange(); + ignoredInputHandlers.TriggerChange(); } } From a2a10d4e131e3ecbedcec977f86e4ac8a426db79 Mon Sep 17 00:00:00 2001 From: Leon Gebler Date: Wed, 2 Dec 2020 19:45:59 +0100 Subject: [PATCH 5072/6909] Don't use nameof(MouseHandler) --- osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index b9e9de1200..b5d7a1ac0e 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -76,10 +76,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input { // this is temporary until we support per-handler settings. const string raw_mouse_handler = @"OsuTKRawMouseHandler"; - const string osutk_standard_mouse_handler = @"OsuTKMouseHandler"; - string standardMouseHandlers = $"{osutk_standard_mouse_handler} {nameof(MouseHandler)}"; + const string standard_mouse_handlers = @"OsuTKMouseHandler MouseHandler"; - ignoredInputHandlers.Value = enabled.NewValue ? standardMouseHandlers : raw_mouse_handler; + ignoredInputHandlers.Value = enabled.NewValue ? standard_mouse_handlers : raw_mouse_handler; }; ignoredInputHandlers = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); From 7fd385efe60cb117f881be9afa7d4377c2f13aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 2 Dec 2020 20:01:56 +0100 Subject: [PATCH 5073/6909] Remove unused using directive --- osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index b5d7a1ac0e..b54ad9a641 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Graphics; -using osu.Framework.Input.Handlers.Mouse; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; using osu.Game.Input; From 2e8195e059d8cd2f17d5477460797c4279eef86c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 3 Dec 2020 12:13:14 +0900 Subject: [PATCH 5074/6909] Use transformation to set fruit rotation --- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 56b9b56372..ef9df02a68 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -5,6 +5,7 @@ using System; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; using osu.Game.Skinning; @@ -42,7 +43,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { base.UpdateInitialTransforms(); - ScaleContainer.Rotation = (RandomSingle(1) - 0.5f) * 40; + ScaleContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); } private void updatePiece() From 6c46046c2426ef38fc6589f737e180adecea7829 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 3 Dec 2020 12:32:49 +0900 Subject: [PATCH 5075/6909] Fix DHO expires while hit sound is playing --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 2 +- osu.Game/Skinning/PausableSkinnableSound.cs | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index a922da0aa9..ad96b1caef 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -453,7 +453,7 @@ namespace osu.Game.Rulesets.Objects.Drawables state.Value = newState; if (LifetimeEnd == double.MaxValue && (state.Value != ArmedState.Idle || HitObject.HitWindows == null)) - Expire(); + LifetimeEnd = Math.Max(LatestTransformEndTime, HitStateUpdateTime + (Samples?.Length ?? 0)); // apply any custom state overrides ApplyCustomUpdateState?.Invoke(this, newState); diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 4f09aec0b6..7056b2b2ad 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Threading; @@ -12,6 +13,8 @@ namespace osu.Game.Skinning { public class PausableSkinnableSound : SkinnableSound { + public double Length => SamplesContainer.Children.Count == 0 ? 0 : SamplesContainer.Children.Max(sample => sample.Length); + protected bool RequestedPlaying { get; private set; } public PausableSkinnableSound(ISampleInfo hitSamples) From 897f593b379b10c2765b54ff22618b821c3ad77e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Dec 2020 13:26:28 +0900 Subject: [PATCH 5076/6909] Fix beatmap carousel panels getting masked away when out of scroll bounds Regressed in https://github.com/ppy/osu/pull/10973 due to removed masking specification. Closes #11067. --- 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 4ce87927a1..d76f0abb9e 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -914,6 +914,9 @@ namespace osu.Game.Screens.Select { // size is determined by the carousel itself, due to not all content necessarily being loaded. ScrollContent.AutoSizeAxes = Axes.None; + + // the scroll container may get pushed off-screen by global screen changes, but we still want panels to display outside of the bounds. + Masking = false; } // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) From e3bbc2b1284ab2b7bdeff55843146ab86b4321b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 14:28:37 +0900 Subject: [PATCH 5077/6909] Rework osu! hidden mod to avoid storing hitobjects --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 134 +++++++++--------- .../Objects/Drawables/DrawableSliderHead.cs | 17 ++- .../Objects/Drawables/DrawableSliderRepeat.cs | 4 + .../Objects/Drawables/DrawableSliderTail.cs | 12 ++ 4 files changed, 97 insertions(+), 70 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 7c1dd46c02..78e759f0e0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -2,9 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; +using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; +using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -25,23 +26,19 @@ namespace osu.Game.Rulesets.Osu.Mods protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner); - public override void ApplyToDrawableHitObjects(IEnumerable drawables) + public override void ApplyToBeatmap(IBeatmap beatmap) { - foreach (var d in drawables) - d.HitObjectApplied += applyFadeInAdjustment; + base.ApplyToBeatmap(beatmap); - base.ApplyToDrawableHitObjects(drawables); - } + foreach (var obj in beatmap.HitObjects.OfType()) + applyFadeInAdjustment(obj); - private void applyFadeInAdjustment(DrawableHitObject hitObject) - { - if (!(hitObject is DrawableOsuHitObject d)) - return; - - d.HitObject.TimeFadeIn = d.HitObject.TimePreempt * fade_in_duration_multiplier; - - foreach (var nested in d.NestedHitObjects) - applyFadeInAdjustment(nested); + static void applyFadeInAdjustment(OsuHitObject osuObject) + { + osuObject.TimeFadeIn = osuObject.TimePreempt * fade_in_duration_multiplier; + foreach (var nested in osuObject.NestedHitObjects.OfType()) + applyFadeInAdjustment(nested); + } } protected override void ApplyIncreasedVisibilityState(DrawableHitObject hitObject, ArmedState state) @@ -56,37 +53,27 @@ namespace osu.Game.Rulesets.Osu.Mods applyState(hitObject, false); } - private void applyState(DrawableHitObject drawable, bool increaseVisibility) + private void applyState(DrawableHitObject drawableObject, bool increaseVisibility) { - if (!(drawable is DrawableOsuHitObject d)) + if (!(drawableObject is DrawableOsuHitObject drawableOsuObject)) return; - var h = d.HitObject; + OsuHitObject hitObject = drawableOsuObject.HitObject; - var fadeOutStartTime = h.StartTime - h.TimePreempt + h.TimeFadeIn; - var fadeOutDuration = h.TimePreempt * fade_out_duration_multiplier; + (double startTime, double duration) fadeOut = getFadeOutParameters(drawableOsuObject); - // new duration from completed fade in to end (before fading out) - var longFadeDuration = h.GetEndTime() - fadeOutStartTime; - - switch (drawable) + switch (drawableObject) { - case DrawableSliderTail sliderTail: - // use stored values from head circle to achieve same fade sequence. - var tailFadeOutParameters = getFadeOutParametersFromSliderHead(h); - - using (drawable.BeginAbsoluteSequence(tailFadeOutParameters.startTime, true)) - sliderTail.FadeOut(tailFadeOutParameters.duration); + case DrawableSliderTail _: + using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true)) + drawableObject.FadeOut(fadeOut.duration); break; case DrawableSliderRepeat sliderRepeat: - // use stored values from head circle to achieve same fade sequence. - var repeatFadeOutParameters = getFadeOutParametersFromSliderHead(h); - - using (drawable.BeginAbsoluteSequence(repeatFadeOutParameters.startTime, true)) + using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true)) // only apply to circle piece – reverse arrow is not affected by hidden. - sliderRepeat.CirclePiece.FadeOut(repeatFadeOutParameters.duration); + sliderRepeat.CirclePiece.FadeOut(fadeOut.duration); break; @@ -101,29 +88,23 @@ namespace osu.Game.Rulesets.Osu.Mods else { // we don't want to see the approach circle - using (circle.BeginAbsoluteSequence(h.StartTime - h.TimePreempt, true)) + using (circle.BeginAbsoluteSequence(hitObject.StartTime - hitObject.TimePreempt, true)) circle.ApproachCircle.Hide(); } - // fade out immediately after fade in. - using (drawable.BeginAbsoluteSequence(fadeOutStartTime, true)) - fadeTarget.FadeOut(fadeOutDuration); + using (drawableObject.BeginAbsoluteSequence(fadeOut.startTime, true)) + fadeTarget.FadeOut(fadeOut.duration); break; case DrawableSlider slider: - associateNestedSliderCirclesWithHead(slider.HitObject); - - using (slider.BeginAbsoluteSequence(fadeOutStartTime, true)) - slider.Body.FadeOut(longFadeDuration, Easing.Out); + using (slider.BeginAbsoluteSequence(fadeOut.startTime, true)) + slider.Body.FadeOut(fadeOut.duration, Easing.Out); break; case DrawableSliderTick sliderTick: - // slider ticks fade out over up to one second - var tickFadeOutDuration = Math.Min(sliderTick.HitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000); - - using (sliderTick.BeginAbsoluteSequence(sliderTick.HitObject.StartTime - tickFadeOutDuration, true)) - sliderTick.FadeOut(tickFadeOutDuration); + using (sliderTick.BeginAbsoluteSequence(fadeOut.startTime, true)) + sliderTick.FadeOut(fadeOut.duration); break; @@ -131,30 +112,55 @@ namespace osu.Game.Rulesets.Osu.Mods // hide elements we don't care about. // todo: hide background - using (spinner.BeginAbsoluteSequence(fadeOutStartTime + longFadeDuration, true)) - spinner.FadeOut(fadeOutDuration); + using (spinner.BeginAbsoluteSequence(fadeOut.startTime, true)) + spinner.FadeOut(fadeOut.duration); break; } } - private readonly Dictionary correspondingSliderHeadForObject = new Dictionary(); - - private void associateNestedSliderCirclesWithHead(Slider slider) + private (double startTime, double duration) getFadeOutParameters(DrawableOsuHitObject drawableObject) { - var sliderHead = slider.NestedHitObjects.Single(obj => obj is SliderHeadCircle); - - foreach (var nested in slider.NestedHitObjects) + switch (drawableObject) { - if ((nested is SliderRepeat || nested is SliderEndCircle) && !correspondingSliderHeadForObject.ContainsKey(nested)) - correspondingSliderHeadForObject[nested] = (SliderHeadCircle)sliderHead; - } - } + case DrawableSliderTail tail: + // Use the same fade sequence as the slider head. + Debug.Assert(tail.Slider != null); + return getParameters(tail.Slider.HeadCircle); - private (double startTime, double duration) getFadeOutParametersFromSliderHead(OsuHitObject h) - { - var sliderHead = correspondingSliderHeadForObject[h]; - return (sliderHead.StartTime - sliderHead.TimePreempt + sliderHead.TimeFadeIn, sliderHead.TimePreempt * fade_out_duration_multiplier); + case DrawableSliderRepeat repeat: + // Use the same fade sequence as the slider head. + Debug.Assert(repeat.Slider != null); + return getParameters(repeat.Slider.HeadCircle); + + default: + return getParameters(drawableObject.HitObject); + } + + static (double startTime, double duration) getParameters(OsuHitObject hitObject) + { + var fadeOutStartTime = hitObject.StartTime - hitObject.TimePreempt + hitObject.TimeFadeIn; + var fadeOutDuration = hitObject.TimePreempt * fade_out_duration_multiplier; + + // new duration from completed fade in to end (before fading out) + var longFadeDuration = hitObject.GetEndTime() - fadeOutStartTime; + + switch (hitObject) + { + case Slider _: + return (fadeOutStartTime, longFadeDuration); + + case SliderTick _: + var tickFadeOutDuration = Math.Min(hitObject.TimePreempt - DrawableSliderTick.ANIM_DURATION, 1000); + return (hitObject.StartTime - tickFadeOutDuration, tickFadeOutDuration); + + case Spinner _: + return (fadeOutStartTime + longFadeDuration, fadeOutDuration); + + default: + return (fadeOutStartTime, fadeOutDuration); + } + } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 3a92938d75..e878d61eec 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -2,6 +2,8 @@ // 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; using osu.Game.Rulesets.Objects.Drawables; @@ -11,14 +13,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderHead : DrawableHitCircle { + [CanBeNull] + public Slider Slider => drawableSlider?.HitObject; + private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; private DrawableSlider drawableSlider; - private Slider slider => drawableSlider?.HitObject; - public DrawableSliderHead() { } @@ -58,11 +61,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.Update(); - double completionProgress = Math.Clamp((Time.Current - slider.StartTime) / slider.Duration, 0, 1); + Debug.Assert(Slider != null); + + double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1); //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. if (!IsHit) - Position = slider.CurvePositionAt(completionProgress); + Position = Slider.CurvePositionAt(completionProgress); } public Action OnShake; @@ -71,8 +76,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables private void updatePosition() { - if (slider != null) - Position = HitObject.Position - slider.Position; + if (Slider != null) + Position = HitObject.Position - Slider.Position; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 0735d48ae1..ba503cca6a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -18,6 +19,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new SliderRepeat HitObject => (SliderRepeat)base.HitObject; + [CanBeNull] + public Slider Slider => drawableSlider?.HitObject; + private double animDuration; public Drawable CirclePiece { get; private set; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs index eff72168ee..3deff55538 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTail.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Diagnostics; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -15,6 +16,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new SliderTailCircle HitObject => (SliderTailCircle)base.HitObject; + [CanBeNull] + public Slider Slider => drawableSlider?.HitObject; + /// /// The judgement text is provided by the . /// @@ -22,6 +26,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public bool Tracking { get; set; } + private DrawableSlider drawableSlider; private SkinnableDrawable circlePiece; private Container scaleContainer; @@ -59,6 +64,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } + protected override void OnParentReceived(DrawableHitObject parent) + { + base.OnParentReceived(parent); + + drawableSlider = (DrawableSlider)parent; + } + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); From db7e82c5603b8df224a0c933ced3ae255aeac340 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 14:54:02 +0900 Subject: [PATCH 5078/6909] Add test --- .../Mods/TestSceneOsuModHidden.cs | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index 40f1c4a52f..ff308f389f 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -92,6 +92,30 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = checkSomeHit }); + [Test] + public void TestWithSliderReuse() => CreateModTest(new ModTestData + { + Mod = new OsuModHidden(), + Autoplay = true, + Beatmap = new Beatmap + { + HitObjects = new List + { + new Slider + { + StartTime = 1000, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + }, + new Slider + { + StartTime = 4000, + Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(100, 0), }) + }, + } + }, + PassCondition = checkSomeHit + }); + private bool checkSomeHit() { return Player.ScoreProcessor.JudgedHits >= 4; From 31f7f7072deb281f1bcf4fc0e3cf6208e8e27d24 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 15:13:20 +0900 Subject: [PATCH 5079/6909] Fix song select panels not loading if partially offscreen --- .../Select/Carousel/DrawableCarouselBeatmapSet.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index e25c6932cf..b3c5d458d6 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -100,8 +100,14 @@ namespace osu.Game.Screens.Select.Carousel background = new DelayedLoadWrapper(() => new SetPanelBackground(manager.GetWorkingBeatmap(beatmapSet.Beatmaps.FirstOrDefault())) { RelativeSizeAxes = Axes.Both, - }, 300), - mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100), + }, 300) + { + RelativeSizeAxes = Axes.Both + }, + mainFlow = new DelayedLoadWrapper(() => new SetPanelContent((CarouselBeatmapSet)Item), 100) + { + RelativeSizeAxes = Axes.Both + }, }; background.DelayedLoadComplete += fadeContentIn; From 3550e5b30f93d965265bbf4ab611a588753a23b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Dec 2020 16:42:06 +0900 Subject: [PATCH 5080/6909] Add length display to room screen as well --- .../Online/API/Requests/Responses/APIBeatmap.cs | 1 + .../Online/Multiplayer/PlaylistExtensions.cs | 16 ++++++++++++++++ .../Match/Components/MatchSettingsOverlay.cs | 9 ++------- osu.Game/Screens/Multi/Match/MatchSubScreen.cs | 5 ++++- 4 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Online/Multiplayer/PlaylistExtensions.cs diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs index ae65ac09b2..7343870dbc 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmap.cs @@ -75,6 +75,7 @@ namespace osu.Game.Online.API.Requests.Responses StarDifficulty = starDifficulty, OnlineBeatmapID = OnlineBeatmapID, Version = version, + // this is actually an incorrect mapping (Length is calculated as drain length in lazer's import process, see BeatmapManager.calculateLength). Length = TimeSpan.FromSeconds(length).TotalMilliseconds, Status = Status, BeatmapSet = set, diff --git a/osu.Game/Online/Multiplayer/PlaylistExtensions.cs b/osu.Game/Online/Multiplayer/PlaylistExtensions.cs new file mode 100644 index 0000000000..fe3d96e295 --- /dev/null +++ b/osu.Game/Online/Multiplayer/PlaylistExtensions.cs @@ -0,0 +1,16 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using Humanizer; +using Humanizer.Localisation; +using osu.Framework.Bindables; + +namespace osu.Game.Online.Multiplayer +{ + public static class PlaylistExtensions + { + public static string GetTotalDuration(this BindableList playlist) => + playlist.Select(p => p.Beatmap.Value.Length).Sum().Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2); + } +} diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index 8bf66b084c..668a373d80 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -3,9 +3,7 @@ using System; using System.Collections.Specialized; -using System.Linq; using Humanizer; -using Humanizer.Localisation; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -339,11 +337,8 @@ namespace osu.Game.Screens.Multi.Match.Components ApplyButton.Enabled.Value = hasValidSettings; } - private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) - { - double totalLength = Playlist.Select(p => p.Beatmap.Value.Length).Sum(); - playlistLength.Text = $"Length: {totalLength.Milliseconds().Humanize(minUnit: TimeUnit.Second, maxUnit: TimeUnit.Hour, precision: 2)}"; - } + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => + playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 2cbe215a39..13a5d89a12 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -57,6 +57,7 @@ namespace osu.Game.Screens.Multi.Match private IBindable> managerUpdated; private OverlinedHeader participantsHeader; + private OverlinedHeader playlistHeader; public MatchSubScreen(Room room) { @@ -135,7 +136,7 @@ namespace osu.Game.Screens.Multi.Match RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] { new OverlinedHeader("Playlist"), }, + new Drawable[] { playlistHeader = new OverlinedHeader("Playlist"), }, new Drawable[] { new DrawableRoomPlaylistWithResults @@ -243,6 +244,8 @@ namespace osu.Game.Screens.Multi.Match managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); + + playlist.BindCollectionChanged((_, __) => playlistHeader.Details.Value = playlist.GetTotalDuration(), true); } public override bool OnExiting(IScreen next) From e5c8e06c4bd44ace592b8a0549d524bc582e41d2 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 19:28:47 +0900 Subject: [PATCH 5081/6909] Create children in the constructor --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 33 +++++++++++++-------------- 1 file changed, 16 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 11b6916a4c..d164d2e0ca 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -49,11 +49,7 @@ namespace osu.Game.Rulesets.Catch.UI public Container ExplodingFruitTarget; - private Container caughtFruitContainer { get; } = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.BottomCentre, - }; + private readonly Container caughtFruitContainer; [NotNull] private readonly Container trailsTarget; @@ -92,9 +88,9 @@ namespace osu.Game.Rulesets.Catch.UI /// private readonly float catchWidth; - private CatcherSprite catcherIdle; - private CatcherSprite catcherKiai; - private CatcherSprite catcherFail; + private readonly CatcherSprite catcherIdle; + private readonly CatcherSprite catcherKiai; + private readonly CatcherSprite catcherFail; private CatcherSprite currentCatcher; @@ -108,8 +104,8 @@ namespace osu.Game.Rulesets.Catch.UI private float hyperDashTargetPosition; private Bindable hitLighting; - private DrawablePool hitExplosionPool; - private Container hitExplosionContainer; + private readonly DrawablePool hitExplosionPool; + private readonly Container hitExplosionContainer; public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) { @@ -122,17 +118,15 @@ namespace osu.Game.Rulesets.Catch.UI Scale = calculateScale(difficulty); catchWidth = CalculateCatchWidth(Scale); - } - - [BackgroundDependencyLoader] - private void load(OsuConfigManager config) - { - hitLighting = config.GetBindable(OsuSetting.HitLighting); InternalChildren = new Drawable[] { hitExplosionPool = new DrawablePool(10), - caughtFruitContainer, + caughtFruitContainer = new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.BottomCentre, + }, catcherIdle = new CatcherSprite(CatcherAnimationState.Idle) { Anchor = Anchor.TopCentre, @@ -154,7 +148,12 @@ namespace osu.Game.Rulesets.Catch.UI Origin = Anchor.BottomCentre, }, }; + } + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + hitLighting = config.GetBindable(OsuSetting.HitLighting); trails = new CatcherTrailDisplay(this); updateCatcher(); From af45e8d97b2ff17b05917e6230a3e5353a43e6b4 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 19:45:34 +0900 Subject: [PATCH 5082/6909] Don't delay caught fruit loading It is not needed anymore because some code in DCHO is moved from `load` to constructor. --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 26077aeba4..467dc4283d 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; @@ -28,8 +27,6 @@ namespace osu.Game.Rulesets.Catch.UI set => MovableCatcher.ExplodingFruitTarget = value; } - private DrawableCatchHitObject lastPlateableFruit; - public CatcherArea(BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); @@ -53,19 +50,6 @@ namespace osu.Game.Rulesets.Catch.UI if (!result.Type.IsScorable()) return; - void runAfterLoaded(Action action) - { - if (lastPlateableFruit == null) - return; - - // this is required to make this run after the last caught fruit runs updateState() at least once. - // TODO: find a better alternative - if (lastPlateableFruit.IsLoaded) - action(); - else - lastPlateableFruit.OnLoadComplete += _ => action(); - } - if (result.IsHit && hitObject is DrawablePalpableCatchHitObject fruit) { // create a new (cloned) fruit to stay on the plate. the original is faded out immediately. @@ -84,16 +68,15 @@ namespace osu.Game.Rulesets.Catch.UI caughtFruit.LifetimeEnd = double.MaxValue; MovableCatcher.PlaceOnPlate(caughtFruit); - lastPlateableFruit = caughtFruit; if (!fruit.StaysOnPlate) - runAfterLoaded(() => MovableCatcher.Explode(caughtFruit)); + MovableCatcher.Explode(caughtFruit); } if (hitObject.HitObject.LastInCombo) { if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result)) - runAfterLoaded(() => MovableCatcher.Explode()); + MovableCatcher.Explode(); else MovableCatcher.Drop(); } From a231a4aa6d127c2ebad93b562c45370458837834 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 19:46:30 +0900 Subject: [PATCH 5083/6909] Remove unused method --- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 467dc4283d..a2a4bd5304 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -87,10 +87,6 @@ namespace osu.Game.Rulesets.Catch.UI public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) => comboDisplay.OnRevertResult(fruit, result); - public void OnReleased(CatchAction action) - { - } - public bool AttemptCatch(CatchHitObject obj) { return MovableCatcher.AttemptCatch(obj); From 8d32cca5d69d7d12ebbbce28b6d9cfc1e8259d04 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 20:03:13 +0900 Subject: [PATCH 5084/6909] Use more specific type for caught object --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 12 ++++++------ osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index d164d2e0ca..da71145004 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.UI public Container ExplodingFruitTarget; - private readonly Container caughtFruitContainer; + private readonly Container caughtFruitContainer; [NotNull] private readonly Container trailsTarget; @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Catch.UI InternalChildren = new Drawable[] { hitExplosionPool = new DrawablePool(10), - caughtFruitContainer = new Container + caughtFruitContainer = new Container { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, @@ -196,7 +196,7 @@ namespace osu.Game.Rulesets.Catch.UI /// Add a caught fruit to the catcher's stack. /// /// The fruit that was caught. - public void PlaceOnPlate(DrawableCatchHitObject fruit) + public void PlaceOnPlate(DrawablePalpableCatchHitObject fruit) { var ourRadius = fruit.DisplayRadius; float theirRadius = 0; @@ -385,7 +385,7 @@ namespace osu.Game.Rulesets.Catch.UI Explode(f); } - public void Drop(DrawableHitObject fruit) + public void Drop(DrawablePalpableCatchHitObject fruit) { removeFromPlateWithTransform(fruit, f => { @@ -394,7 +394,7 @@ namespace osu.Game.Rulesets.Catch.UI }); } - public void Explode(DrawableHitObject fruit) + public void Explode(DrawablePalpableCatchHitObject fruit) { var originalX = fruit.X * Scale.X; @@ -478,7 +478,7 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } - private void removeFromPlateWithTransform(DrawableHitObject fruit, Action action) + private void removeFromPlateWithTransform(DrawablePalpableCatchHitObject fruit, Action action) { if (ExplodingFruitTarget != null) { diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index a2a4bd5304..bb5eaaa438 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay.X = MovableCatcher.X; } - private DrawableCatchHitObject createCaughtFruit(DrawablePalpableCatchHitObject hitObject) + private DrawablePalpableCatchHitObject createCaughtFruit(DrawablePalpableCatchHitObject hitObject) { switch (hitObject.HitObject) { From 873f2363c1f757e662845c33b015bd096f5de68c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 20:05:01 +0900 Subject: [PATCH 5085/6909] Simplify the fruit stacking code It is now more clear that the expression of distance checking is probably unintended (a bug) --- .../Objects/Drawables/DrawableCatchHitObject.cs | 2 -- .../Objects/Drawables/DrawablePalpableCatchHitObject.cs | 2 ++ osu.Game.Rulesets.Catch/UI/Catcher.cs | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 1faa6a5b0f..c3dbfc325f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -16,8 +16,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override double InitialLifetimeOffset => HitObject.TimePreempt; - public float DisplayRadius => DrawSize.X / 2 * Scale.X * HitObject.Scale; - protected override float SamplePlaybackPosition => HitObject.X / CatchPlayfield.WIDTH; protected DrawableCatchHitObject([CanBeNull] CatchHitObject hitObject) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index a3908f94b6..0877b5e248 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables /// public virtual bool StaysOnPlate => true; + public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor; + protected readonly Container ScaleContainer; protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index da71145004..94383516bd 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -204,8 +204,7 @@ namespace osu.Game.Rulesets.Catch.UI const float allowance = 10; while (caughtFruitContainer.Any(f => - f.LifetimeEnd == double.MaxValue && - Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = f.DrawSize.X / 2 * f.Scale.X)) / (allowance / 2))) + Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = CatchHitObject.OBJECT_RADIUS / 2)) / (allowance / 2))) { var diff = (ourRadius + theirRadius) / allowance; fruit.X += (RNG.NextSingle() - 0.5f) * diff * 2; From 2eb2c934ccbd3360c393403dcdbb2cb87c8a0dcb Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 2 Dec 2020 21:23:34 +0900 Subject: [PATCH 5086/6909] Refactor fruit dropping code - The repeated `Remove` call was quadratic complexity. Now it is linear time. --- .../TestSceneCatcher.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 1 - osu.Game.Rulesets.Catch/UI/Catcher.cs | 119 +++++++++--------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 7 +- 4 files changed, 65 insertions(+), 64 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 6eeda2c731..cb4aaefa46 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Tests [BackgroundDependencyLoader] private void load() { - SetContents(() => new Catcher(new Container()) + SetContents(() => new Catcher(new Container(), new Container()) { RelativePositionAxes = Axes.None, Anchor = Anchor.Centre, diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 820f08d439..6934dcc1f9 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -43,7 +43,6 @@ namespace osu.Game.Rulesets.Catch.UI CatcherArea = new CatcherArea(difficulty) { - ExplodingFruitTarget = explodingFruitContainer, Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, }; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 94383516bd..f8ed51bd6f 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -17,7 +17,6 @@ using osu.Game.Configuration; 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; @@ -47,15 +46,15 @@ namespace osu.Game.Rulesets.Catch.UI /// public const double BASE_SPEED = 1.0; - public Container ExplodingFruitTarget; - - private readonly Container caughtFruitContainer; - [NotNull] private readonly Container trailsTarget; private CatcherTrailDisplay trails; + private readonly Container droppedObjectTarget; + + private readonly Container caughtFruitContainer; + public CatcherAnimationState CurrentState { get; private set; } /// @@ -107,9 +106,10 @@ namespace osu.Game.Rulesets.Catch.UI private readonly DrawablePool hitExplosionPool; private readonly Container hitExplosionContainer; - public Catcher([NotNull] Container trailsTarget, BeatmapDifficulty difficulty = null) + public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null) { this.trailsTarget = trailsTarget; + this.droppedObjectTarget = droppedObjectTarget; Origin = Anchor.TopCentre; @@ -369,41 +369,14 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Drop any fruit off the plate. /// - public void Drop() - { - foreach (var f in caughtFruitContainer.ToArray()) - Drop(f); - } + public void Drop() => clearPlate(DroppedObjectAnimation.Drop); /// /// Explode any fruit off the plate. /// - public void Explode() - { - foreach (var f in caughtFruitContainer.ToArray()) - Explode(f); - } + public void Explode() => clearPlate(DroppedObjectAnimation.Explode); - public void Drop(DrawablePalpableCatchHitObject fruit) - { - removeFromPlateWithTransform(fruit, f => - { - f.MoveToY(f.Y + 75, 750, Easing.InSine); - f.FadeOut(750); - }); - } - - public void Explode(DrawablePalpableCatchHitObject 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); - }); - } + public void Explode(DrawablePalpableCatchHitObject caughtObject) => removeFromPlate(caughtObject, DroppedObjectAnimation.Explode); protected override void SkinChanged(ISkinSource skin, bool allowFallback) { @@ -477,33 +450,67 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } - private void removeFromPlateWithTransform(DrawablePalpableCatchHitObject fruit, Action action) + private void clearPlate(DroppedObjectAnimation animation) { - if (ExplodingFruitTarget != null) + var caughtObjects = caughtFruitContainer.Children.ToArray(); + caughtFruitContainer.Clear(false); + + droppedObjectTarget.AddRange(caughtObjects); + + foreach (var caughtObject in caughtObjects) + drop(caughtObject, animation); + } + + private void removeFromPlate(DrawablePalpableCatchHitObject caughtObject, DroppedObjectAnimation animation) + { + if (!caughtFruitContainer.Remove(caughtObject)) + throw new InvalidOperationException("Can only drop a caught object on the plate"); + + droppedObjectTarget.Add(caughtObject); + + drop(caughtObject, animation); + } + + private void drop(Drawable d, DroppedObjectAnimation animation) + { + var originalX = d.X * Scale.X; + + d.Anchor = Anchor.TopLeft; + d.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(d.DrawPosition, droppedObjectTarget); + + animate(d, animation, originalX); + } + + private void animate(Drawable d, DroppedObjectAnimation animation, float originalX) + { + // temporary hack to make sure transforms are not cleared by DHO state update + if (!d.IsLoaded) { - fruit.Anchor = Anchor.TopLeft; - fruit.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(fruit.DrawPosition, ExplodingFruitTarget); - - if (!caughtFruitContainer.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); + d.OnLoadComplete += _ => animate(d, animation, originalX); + return; } - var actionTime = Clock.CurrentTime; - - fruit.ApplyCustomUpdateState += onFruitOnApplyCustomUpdateState; - onFruitOnApplyCustomUpdateState(fruit, fruit.State.Value); - - void onFruitOnApplyCustomUpdateState(DrawableHitObject o, ArmedState state) + switch (animation) { - using (fruit.BeginAbsoluteSequence(actionTime)) - action(fruit); + case DroppedObjectAnimation.Drop: + d.MoveToY(d.Y + 75, 750, Easing.InSine); + d.FadeOut(750); + break; - fruit.Expire(); + case DroppedObjectAnimation.Explode: + d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine); + d.MoveToX(d.X + originalX * 6, 1000); + d.FadeOut(750); + break; } + + d.Expire(); } } + + public enum DroppedObjectAnimation + { + Drop, + Explode + } } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index bb5eaaa438..077137a3cb 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -22,11 +22,6 @@ namespace osu.Game.Rulesets.Catch.UI public readonly Catcher MovableCatcher; private readonly CatchComboDisplay comboDisplay; - public Container ExplodingFruitTarget - { - set => MovableCatcher.ExplodingFruitTarget = value; - } - public CatcherArea(BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); @@ -41,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI Margin = new MarginPadding { Bottom = 350f }, X = CatchPlayfield.CENTER_X }, - MovableCatcher = new Catcher(this, difficulty) { X = CatchPlayfield.CENTER_X }, + MovableCatcher = new Catcher(this, this, difficulty) { X = CatchPlayfield.CENTER_X }, }; } From 5a5c956cedfda99b3280c9d7bb7933f267fea821 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 3 Dec 2020 14:44:35 +0900 Subject: [PATCH 5087/6909] Move more logic to Catcher from CatcherArea --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 74 ++++++++++++++++++----- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 44 -------------- 2 files changed, 60 insertions(+), 58 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index f8ed51bd6f..6d5d25243d 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -245,25 +245,29 @@ namespace osu.Game.Rulesets.Catch.UI catchObjectPosition >= catcherPosition - halfCatchWidth && catchObjectPosition <= catcherPosition + halfCatchWidth; - // only update hyperdash state if we are not catching a tiny droplet. - if (fruit is TinyDroplet) return validCatch; - - if (validCatch && fruit.HyperDash) + // droplet doesn't affect the catcher state + if (!(fruit is TinyDroplet)) { - var target = fruit.HyperDashTarget; - var timeDifference = target.StartTime - fruit.StartTime; - double positionDifference = target.X - catcherPosition; - var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); + if (validCatch && fruit.HyperDash) + { + var target = fruit.HyperDashTarget; + var timeDifference = target.StartTime - fruit.StartTime; + double positionDifference = target.X - catcherPosition; + var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); - SetHyperDashState(Math.Abs(velocity), target.X); + 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); } - else - SetHyperDashState(); if (validCatch) - updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); - else if (!(fruit is Banana)) - updateState(CatcherAnimationState.Fail); + placeCaughtObject(fruit); return validCatch; } @@ -450,6 +454,48 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } + private void placeCaughtObject(PalpableCatchHitObject source) + { + var caughtObject = createCaughtObject(source); + if (caughtObject == null) return; + + caughtObject.RelativePositionAxes = Axes.None; + caughtObject.X = source.X - X; + caughtObject.IsOnPlate = true; + + caughtObject.Anchor = Anchor.TopCentre; + caughtObject.Origin = Anchor.Centre; + caughtObject.Scale *= 0.5f; + caughtObject.LifetimeStart = source.StartTime; + caughtObject.LifetimeEnd = double.MaxValue; + + PlaceOnPlate(caughtObject); + + if (!caughtObject.StaysOnPlate) + Explode(caughtObject); + } + + private DrawablePalpableCatchHitObject createCaughtObject(PalpableCatchHitObject source) + { + switch (source) + { + case Banana banana: + return new DrawableBanana(banana); + + case Fruit fruit: + return new DrawableFruit(fruit); + + case TinyDroplet tiny: + return new DrawableTinyDroplet(tiny); + + case Droplet droplet: + return new DrawableDroplet(droplet); + + default: + return null; + } + } + private void clearPlate(DroppedObjectAnimation animation) { var caughtObjects = caughtFruitContainer.Children.ToArray(); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 077137a3cb..9cd0785b85 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -45,29 +45,6 @@ namespace osu.Game.Rulesets.Catch.UI if (!result.Type.IsScorable()) return; - if (result.IsHit && hitObject is DrawablePalpableCatchHitObject fruit) - { - // create a new (cloned) fruit to stay on the plate. the original is faded out immediately. - var caughtFruit = createCaughtFruit(fruit); - - if (caughtFruit == null) return; - - caughtFruit.RelativePositionAxes = Axes.None; - caughtFruit.Position = new Vector2(MovableCatcher.ToLocalSpace(hitObject.ScreenSpaceDrawQuad.Centre).X - MovableCatcher.DrawSize.X / 2, 0); - caughtFruit.IsOnPlate = true; - - caughtFruit.Anchor = Anchor.TopCentre; - caughtFruit.Origin = Anchor.Centre; - caughtFruit.Scale *= 0.5f; - caughtFruit.LifetimeStart = caughtFruit.HitObject.StartTime; - caughtFruit.LifetimeEnd = double.MaxValue; - - MovableCatcher.PlaceOnPlate(caughtFruit); - - if (!fruit.StaysOnPlate) - MovableCatcher.Explode(caughtFruit); - } - if (hitObject.HitObject.LastInCombo) { if (result.Judgement is CatchJudgement catchJudgement && catchJudgement.ShouldExplodeFor(result)) @@ -98,26 +75,5 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay.X = MovableCatcher.X; } - - private DrawablePalpableCatchHitObject createCaughtFruit(DrawablePalpableCatchHitObject hitObject) - { - switch (hitObject.HitObject) - { - case Banana banana: - return new DrawableBanana(banana); - - case Fruit fruit: - return new DrawableFruit(fruit); - - case TinyDroplet tiny: - return new DrawableTinyDroplet(tiny); - - case Droplet droplet: - return new DrawableDroplet(droplet); - - default: - return null; - } - } } } From 1d669cf65ed5c9ee5e398a3cac7e5bc386b5f49a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 3 Dec 2020 16:40:14 +0900 Subject: [PATCH 5088/6909] Add more TestSceneCatcher tests Some tests are moved from TestSceneCatcherArea --- .../TestSceneCatcher.cs | 178 +++++++++++++++++- .../TestSceneCatcherArea.cs | 81 +++----- 2 files changed, 195 insertions(+), 64 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index cb4aaefa46..f41a16026a 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -1,26 +1,192 @@ // 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.Rulesets.Catch.UI; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests { [TestFixture] - public class TestSceneCatcher : CatchSkinnableTestScene + public class TestSceneCatcher : OsuTestScene { - [BackgroundDependencyLoader] - private void load() + [Resolved] + private OsuConfigManager config { get; set; } + + private Container droppedObjectContainer; + + private TestCatcher catcher; + + [SetUp] + public void SetUp() => Schedule(() => { - SetContents(() => new Catcher(new Container(), new Container()) + var difficulty = new BeatmapDifficulty + { + CircleSize = 0, + }; + + var trailContainer = new Container(); + droppedObjectContainer = new Container(); + catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty); + + Child = new Container { - RelativePositionAxes = Axes.None, Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Children = new Drawable[] + { + trailContainer, + droppedObjectContainer, + catcher + } + }; + }); + + [Test] + public void TestCatcherCatchWidth() + { + var halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2; + AddStep("catch fruit", () => + { + attemptCatch(new Fruit { X = -halfWidth + 1 }); + attemptCatch(new Fruit { X = halfWidth - 1 }); }); + checkPlate(2); + AddStep("miss fruit", () => + { + attemptCatch(new Fruit { X = -halfWidth - 1 }); + attemptCatch(new Fruit { X = halfWidth + 1 }); + }); + checkPlate(2); + } + + [Test] + public void TestCatcherStateFruit() + { + AddStep("miss fruit", () => attemptCatch(new Fruit { X = 100 })); + checkState(CatcherAnimationState.Fail); + AddStep("catch fruit", () => attemptCatch(new Fruit())); + checkState(CatcherAnimationState.Idle); + AddStep("catch kiai fruit", () => attemptCatch(new TestKiaiFruit())); + checkState(CatcherAnimationState.Kiai); + } + + [Test] + public void TestCatcherStateTinyDroplet() + { + AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit + { + HyperDashTarget = new Fruit { X = 100 } + })); + AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet())); + AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 })); + checkState(CatcherAnimationState.Kiai); + checkHyperDash(true); + } + + [Test] + public void TestCatcherStateBanana() + { + AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit + { + HyperDashTarget = new Fruit { X = 100 } + })); + AddStep("miss banana", () => attemptCatch(new Banana())); + checkState(CatcherAnimationState.Idle); + checkHyperDash(false); + } + + [Test] + public void TestCatcherStacking() + { + AddStep("catch fruit", () => attemptCatch(new Fruit())); + checkPlate(1); + AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9)); + checkPlate(10); + AddAssert("caught objects are stacked", () => + catcher.CaughtObjects.All(obj => obj.Y <= 0) && + catcher.CaughtObjects.Any(obj => obj.Y == 0) && + catcher.CaughtObjects.Any(obj => obj.Y < -20)); + } + + [Test] + public void TestCatcherExplosionAndDropping() + { + AddStep("catch fruit", () => attemptCatch(new Fruit())); + AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet())); + AddAssert("tiny droplet is exploded", () => catcher.CaughtObjects.Count() == 1 && droppedObjectContainer.Count == 1); + AddUntilStep("wait explosion", () => !droppedObjectContainer.Any()); + AddStep("catch more fruits", () => attemptCatch(new Fruit(), 9)); + AddStep("explode", () => catcher.Explode()); + AddAssert("fruits are exploded", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10); + AddUntilStep("wait explosion", () => !droppedObjectContainer.Any()); + AddStep("catch fruits", () => attemptCatch(new Fruit(), 10)); + AddStep("drop", () => catcher.Drop()); + AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10); + } + + [Test] + public void TestHyperFruitHyperDash() + { + AddStep("catch hyper fruit", () => attemptCatch(new Fruit + { + HyperDashTarget = new Fruit { X = 100 } + })); + checkHyperDash(true); + AddStep("catch normal fruit", () => attemptCatch(new Fruit())); + checkHyperDash(false); + } + + [TestCase(true)] + [TestCase(false)] + public void TestHitLighting(bool enabled) + { + AddStep($"{(enabled ? "enable" : "disable")} hit lighting", () => config.Set(OsuSetting.HitLighting, enabled)); + AddStep("catch fruit", () => attemptCatch(new Fruit())); + AddAssert("check hit lighting", () => catcher.ChildrenOfType().Any() == enabled); + } + + private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count); + + private void checkState(CatcherAnimationState state) => AddAssert($"catcher state is {state}", () => catcher.CurrentState == state); + + private void checkHyperDash(bool state) => AddAssert($"catcher is {(state ? "" : "not ")}hyper dashing", () => catcher.HyperDashing == state); + + private void attemptCatch(CatchHitObject hitObject, int count = 1) + { + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + + for (var i = 0; i < count; i++) + catcher.AttemptCatch(hitObject); + } + + public class TestCatcher : Catcher + { + public IEnumerable CaughtObjects => this.ChildrenOfType(); + + public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty) + : base(trailsTarget, droppedObjectTarget, difficulty) + { + } + } + + public class TestKiaiFruit : Fruit + { + protected override void ApplyDefaultsToSelf(ControlPointInfo controlPointInfo, BeatmapDifficulty difficulty) + { + controlPointInfo.Add(0, new EffectControlPoint { KiaiMode = true }); + base.ApplyDefaultsToSelf(controlPointInfo, difficulty); + } } } } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index c12f38723b..7be6fc92ac 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -27,73 +27,51 @@ namespace osu.Game.Rulesets.Catch.Tests [Resolved] private OsuConfigManager config { get; set; } - private Catcher catcher => this.ChildrenOfType().First().MovableCatcher; + private Catcher catcher => this.ChildrenOfType().First(); + + private float circleSize; public TestSceneCatcherArea() { - AddSliderStep("CircleSize", 0, 8, 5, createCatcher); - AddToggleStep("Hyperdash", t => - CreatedDrawables.OfType().Select(i => i.Child) - .OfType().ForEach(c => c.ToggleHyperDash(t))); + AddSliderStep("circle size", 0, 8, 5, createCatcher); + AddToggleStep("hyper dash", t => this.ChildrenOfType().ForEach(area => area.ToggleHyperDash(t))); - AddRepeatStep("catch fruit", () => catchFruit(new TestFruit(false) - { - X = catcher.X - }), 20); - AddRepeatStep("catch fruit last in combo", () => catchFruit(new TestFruit(false) - { - X = catcher.X, - LastInCombo = true, - }), 20); - AddRepeatStep("catch kiai fruit", () => catchFruit(new TestFruit(true) - { - X = catcher.X - }), 20); - AddRepeatStep("miss fruit", () => catchFruit(new Fruit - { - X = catcher.X + 100, - LastInCombo = true, - }, true), 20); + AddStep("catch fruit", () => attemptCatch(new Fruit())); + AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true })); + AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit())); + AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true })); } - [TestCase(true)] - [TestCase(false)] - public void TestHitLighting(bool enable) + private void attemptCatch(Fruit fruit) { - AddStep("create catcher", () => createCatcher(5)); - - AddStep("toggle hit lighting", () => config.Set(OsuSetting.HitLighting, enable)); - AddStep("catch fruit", () => catchFruit(new TestFruit(false) + fruit.X += catcher.X; + fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { - X = catcher.X - })); - AddStep("catch fruit last in combo", () => catchFruit(new TestFruit(false) - { - X = catcher.X, - LastInCombo = true - })); - AddAssert("check hit explosion", () => catcher.ChildrenOfType().Any() == enable); - } + CircleSize = circleSize + }); - private void catchFruit(Fruit fruit, bool miss = false) - { - this.ChildrenOfType().ForEach(area => + foreach (var area in this.ChildrenOfType()) { DrawableFruit drawable = new DrawableFruit(fruit); area.Add(drawable); Schedule(() => { - area.AttemptCatch(fruit); - area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { Type = miss ? HitResult.Miss : HitResult.Great }); + bool caught = area.AttemptCatch(fruit); + area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) + { + Type = caught ? HitResult.Great : HitResult.Miss + }); drawable.Expire(); }); - }); + } } private void createCatcher(float size) { + circleSize = size; + SetContents(() => new CatchInputManager(catchRuleset) { RelativeSizeAxes = Axes.Both, @@ -111,17 +89,6 @@ 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) @@ -129,8 +96,6 @@ namespace osu.Game.Rulesets.Catch.Tests { } - public new Catcher MovableCatcher => base.MovableCatcher; - public void ToggleHyperDash(bool status) => MovableCatcher.SetHyperDashState(status ? 2 : 1); } } From c64343c7d719ed8c5c3dc1bfa35934145ab2ffad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Dec 2020 17:42:41 +0900 Subject: [PATCH 5089/6909] 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 9d99218f88..9a3d42d6b7 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 4b931726e0..9d37ceee6c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 3a47b77820..ab03393836 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 78c43641d1c6022e1143280843543fec65b678f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 3 Dec 2020 17:43:09 +0900 Subject: [PATCH 5090/6909] Update imagesharp namespaces (and consume System.Drawing types instead) --- osu.Game.Tournament/Models/TournamentMatch.cs | 2 +- osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs | 2 +- .../Screens/Ladder/Components/DrawableTournamentMatch.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Models/TournamentMatch.cs b/osu.Game.Tournament/Models/TournamentMatch.cs index 8ebcbf4e15..bdfb1728f3 100644 --- a/osu.Game.Tournament/Models/TournamentMatch.cs +++ b/osu.Game.Tournament/Models/TournamentMatch.cs @@ -4,10 +4,10 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Drawing; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Tournament.Screens.Ladder.Components; -using SixLabors.Primitives; namespace osu.Game.Tournament.Models { diff --git a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs index efec4cffdd..ca46c3b050 100644 --- a/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/LadderEditorScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Drawing; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -16,7 +17,6 @@ using osu.Game.Tournament.Screens.Ladder; using osu.Game.Tournament.Screens.Ladder.Components; using osuTK; using osuTK.Graphics; -using SixLabors.Primitives; namespace osu.Game.Tournament.Screens.Editors { diff --git a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs index f2065e7e88..1c805bb42e 100644 --- a/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs +++ b/osu.Game.Tournament/Screens/Ladder/Components/DrawableTournamentMatch.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Drawing; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -13,7 +14,6 @@ using osu.Game.Tournament.Models; using osuTK; using osuTK.Graphics; using osuTK.Input; -using SixLabors.Primitives; namespace osu.Game.Tournament.Screens.Ladder.Components { From 3e62da119eca2d34c3e24b1bf61ec68a6eeaddf9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 17:59:39 +0900 Subject: [PATCH 5091/6909] Add to inspector also --- .../Components/OverlinedPlaylistHeader.cs | 22 +++++++++++++++++++ .../Multi/Lounge/Components/RoomInspector.cs | 2 +- .../Screens/Multi/Match/MatchSubScreen.cs | 5 +---- 3 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs diff --git a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs new file mode 100644 index 0000000000..02dbe501b1 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.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.Game.Online.Multiplayer; + +namespace osu.Game.Screens.Multi.Components +{ + public class OverlinedPlaylistHeader : OverlinedHeader + { + public OverlinedPlaylistHeader() + : base("Playlist") + { + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playlist.BindCollectionChanged((_, __) => Details.Value = Playlist.GetTotalDuration()); + } + } +} diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs index 77fbd606f4..dfee278e87 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components } } }, - new Drawable[] { new OverlinedHeader("Playlist"), }, + new Drawable[] { new OverlinedPlaylistHeader(), }, new Drawable[] { new DrawableRoomPlaylist(false, false) diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs index 13a5d89a12..2f8aad4e65 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/MatchSubScreen.cs @@ -57,7 +57,6 @@ namespace osu.Game.Screens.Multi.Match private IBindable> managerUpdated; private OverlinedHeader participantsHeader; - private OverlinedHeader playlistHeader; public MatchSubScreen(Room room) { @@ -136,7 +135,7 @@ namespace osu.Game.Screens.Multi.Match RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] { playlistHeader = new OverlinedHeader("Playlist"), }, + new Drawable[] { new OverlinedPlaylistHeader(), }, new Drawable[] { new DrawableRoomPlaylistWithResults @@ -244,8 +243,6 @@ namespace osu.Game.Screens.Multi.Match managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); - - playlist.BindCollectionChanged((_, __) => playlistHeader.Details.Value = playlist.GetTotalDuration(), true); } public override bool OnExiting(IScreen next) From 62b1e37f73701a71992d23b4d987f6f5854a7fae Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 18:04:53 +0900 Subject: [PATCH 5092/6909] Use async overloads --- osu.Game/Graphics/ScreenshotManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index d1f6fd445e..53ee711626 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -116,13 +116,13 @@ namespace osu.Game.Graphics switch (screenshotFormat.Value) { case ScreenshotFormat.Png: - image.SaveAsPng(stream); + await image.SaveAsPngAsync(stream); break; case ScreenshotFormat.Jpg: const int jpeg_quality = 92; - image.SaveAsJpeg(stream, new JpegEncoder { Quality = jpeg_quality }); + await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }); break; default: From 8245bb85dc93970e584d8b821769fb8eda7b7c5f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 18:06:55 +0900 Subject: [PATCH 5093/6909] Invoke on initial bind --- osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs index 02dbe501b1..5552c1cb72 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs +++ b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs @@ -16,7 +16,7 @@ namespace osu.Game.Screens.Multi.Components { base.LoadComplete(); - Playlist.BindCollectionChanged((_, __) => Details.Value = Playlist.GetTotalDuration()); + Playlist.BindCollectionChanged((_, __) => Details.Value = Playlist.GetTotalDuration(), true); } } } From be456f9c6be31ea98077263f63d84589009429d0 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 3 Dec 2020 18:45:38 +0900 Subject: [PATCH 5094/6909] Make DroppedObjectAnimation private --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 6d5d25243d..de2782fa35 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -552,11 +552,11 @@ namespace osu.Game.Rulesets.Catch.UI d.Expire(); } - } - public enum DroppedObjectAnimation - { - Drop, - Explode + private enum DroppedObjectAnimation + { + Drop, + Explode + } } } From 7e66714c2fb50c7dd7527d55e5396fe496809139 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 3 Dec 2020 18:45:10 +0900 Subject: [PATCH 5095/6909] Use ApplyCustomUpdateState for dropping transformation We cannot just apply the transforms because DHO clears transforms when state is updated --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 46 +++++++++++++-------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index de2782fa35..1101e5b6b4 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -517,40 +517,40 @@ namespace osu.Game.Rulesets.Catch.UI drop(caughtObject, animation); } - private void drop(Drawable d, DroppedObjectAnimation animation) + private void drop(DrawablePalpableCatchHitObject d, DroppedObjectAnimation animation) { var originalX = d.X * Scale.X; + var startTime = Clock.CurrentTime; d.Anchor = Anchor.TopLeft; d.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(d.DrawPosition, droppedObjectTarget); - animate(d, animation, originalX); + // we cannot just apply the transforms because DHO clears transforms when state is updated + d.ApplyCustomUpdateState += (o, state) => animate(o, animation, originalX, startTime); + if (d.IsLoaded) + animate(d, animation, originalX, startTime); } - private void animate(Drawable d, DroppedObjectAnimation animation, float originalX) + private void animate(Drawable d, DroppedObjectAnimation animation, float originalX, double startTime) { - // temporary hack to make sure transforms are not cleared by DHO state update - if (!d.IsLoaded) + using (d.BeginAbsoluteSequence(startTime)) { - d.OnLoadComplete += _ => animate(d, animation, originalX); - return; + switch (animation) + { + case DroppedObjectAnimation.Drop: + d.MoveToY(d.Y + 75, 750, Easing.InSine); + d.FadeOut(750); + break; + + case DroppedObjectAnimation.Explode: + d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine); + d.MoveToX(d.X + originalX * 6, 1000); + d.FadeOut(750); + break; + } + + d.Expire(); } - - switch (animation) - { - case DroppedObjectAnimation.Drop: - d.MoveToY(d.Y + 75, 750, Easing.InSine); - d.FadeOut(750); - break; - - case DroppedObjectAnimation.Explode: - d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine); - d.MoveToX(d.X + originalX * 6, 1000); - d.FadeOut(750); - break; - } - - d.Expire(); } private enum DroppedObjectAnimation From 73e99718bc23f52854bbc0eb467dcb63bae10a97 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 19:46:42 +0900 Subject: [PATCH 5096/6909] Change order of OnParentReceived() --- .../Objects/Drawables/DrawableSliderHead.cs | 5 +++++ .../Objects/Drawables/DrawableSliderRepeat.cs | 5 +++++ .../Objects/Drawables/DrawableSliderTick.cs | 10 +++++++++- .../Objects/Drawables/DrawableSpinnerTick.cs | 1 + .../Objects/Drawables/DrawableHitObject.cs | 16 ++++++++++++---- osu.Game/Rulesets/UI/HitObjectContainer.cs | 2 +- osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs | 3 ++- osu.Game/Rulesets/UI/Playfield.cs | 4 +++- 8 files changed, 38 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 3a92938d75..d1928bd4bb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -47,6 +47,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnParentReceived(parent); drawableSlider = (DrawableSlider)parent; + } + + protected override void OnApply() + { + base.OnApply(); pathVersion.BindTo(drawableSlider.PathVersion); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index 0735d48ae1..f368615e77 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -65,6 +65,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnParentReceived(parent); drawableSlider = (DrawableSlider)parent; + } + + protected override void OnApply() + { + base.OnApply(); Position = HitObject.Position - drawableSlider.Position; } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index faccf5d4d1..d40b6aea6e 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; private SkinnableDrawable scaleContainer; + private DrawableSlider drawableSlider; public DrawableSliderTick() : base(null) @@ -66,7 +67,14 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.OnParentReceived(parent); - Position = HitObject.Position - ((DrawableSlider)parent).HitObject.Position; + drawableSlider = (DrawableSlider)parent; + } + + protected override void OnApply() + { + base.OnApply(); + + Position = HitObject.Position - drawableSlider.HitObject.Position; } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index f37d933e11..d10c4f7511 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -24,6 +24,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected override void OnParentReceived(DrawableHitObject parent) { base.OnParentReceived(parent); + drawableSpinner = (DrawableSpinner)parent; } diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index a922da0aa9..37c36ace7b 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -230,12 +230,12 @@ namespace osu.Game.Rulesets.Objects.Drawables foreach (var h in HitObject.NestedHitObjects) { - var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h); + var pooledDrawableNested = pooledObjectProvider?.GetPooledDrawableRepresentation(h, this); var drawableNested = pooledDrawableNested ?? CreateNestedHitObject(h) ?? throw new InvalidOperationException($"{nameof(CreateNestedHitObject)} returned null for {h.GetType().ReadableName()}."); - // Invoke the event only if this nested object is just created by `CreateNestedHitObject`. + // Only invoke the event for non-pooled DHOs, otherwise the event will be fired by the playfield. if (pooledDrawableNested == null) OnNestedDrawableCreated?.Invoke(drawableNested); @@ -243,10 +243,12 @@ namespace osu.Game.Rulesets.Objects.Drawables drawableNested.OnRevertResult += onRevertResult; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; + // ApplyParent() should occur before Apply() in all cases, so it's invoked before the nested DHO is added to the hierarchy below, but after its events are initialised. + if (pooledDrawableNested == null) + drawableNested.ApplyParent(this); + nestedHitObjects.Value.Add(drawableNested); AddNestedHitObject(drawableNested); - - drawableNested.OnParentReceived(this); } StartTimeBindable.BindTo(HitObject.StartTimeBindable); @@ -348,6 +350,12 @@ namespace osu.Game.Rulesets.Objects.Drawables { } + /// + /// Applies a parenting to this . + /// + /// The parenting . + public void ApplyParent(DrawableHitObject parent) => OnParentReceived(parent); + /// /// Invoked when this receives a new parenting . /// diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index ac5d281ddc..12e39d4fbf 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.UI { Debug.Assert(!drawableMap.ContainsKey(entry)); - var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject); + var drawable = pooledObjectProvider?.GetPooledDrawableRepresentation(entry.HitObject, null); if (drawable == null) throw new InvalidOperationException($"A drawable representation could not be retrieved for hitobject type: {entry.HitObject.GetType().ReadableName()}."); diff --git a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs index 315926dfc6..2d700076d6 100644 --- a/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs +++ b/osu.Game/Rulesets/UI/IPooledHitObjectProvider.cs @@ -13,8 +13,9 @@ namespace osu.Game.Rulesets.UI /// Attempts to retrieve the poolable representation of a . /// /// The to retrieve the representation of. + /// The parenting , if any. /// The representing , or null if no poolable representation exists. [CanBeNull] - DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject); + DrawableHitObject GetPooledDrawableRepresentation([NotNull] HitObject hitObject, [CanBeNull] DrawableHitObject parent); } } diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index a2ac234471..01b25c9717 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -323,7 +323,7 @@ namespace osu.Game.Rulesets.UI AddInternal(pool); } - DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject) + DrawableHitObject IPooledHitObjectProvider.GetPooledDrawableRepresentation(HitObject hitObject, DrawableHitObject parent) { var lookupType = hitObject.GetType(); @@ -359,6 +359,8 @@ namespace osu.Game.Rulesets.UI if (!lifetimeEntryMap.TryGetValue(hitObject, out var entry)) lifetimeEntryMap[hitObject] = entry = CreateLifetimeEntry(hitObject); + if (parent != null) + dho.ApplyParent(parent); dho.Apply(hitObject, entry); }); } From 0bdf99b97a960cd9a5c58e72b4bf3bb0af984ea3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 3 Dec 2020 20:03:39 +0900 Subject: [PATCH 5097/6909] Remove OnParentReceived() --- .../Objects/Drawables/DrawableSliderHead.cs | 22 +++++---------- .../Objects/Drawables/DrawableSliderRepeat.cs | 17 ++++-------- .../Objects/Drawables/DrawableSliderTick.cs | 12 +++------ .../Objects/Drawables/DrawableSpinnerTick.cs | 15 +++-------- .../Objects/Drawables/DrawableHitObject.cs | 27 +++++++------------ osu.Game/Rulesets/UI/Playfield.cs | 3 +-- 6 files changed, 29 insertions(+), 67 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index d1928bd4bb..f584c9c2d3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -4,20 +4,19 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderHead : DrawableHitCircle { + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; - private DrawableSlider drawableSlider; - - private Slider slider => drawableSlider?.HitObject; + private Slider slider => DrawableSlider?.HitObject; public DrawableSliderHead() { @@ -39,24 +38,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.OnFree(); - pathVersion.UnbindFrom(drawableSlider.PathVersion); - } - - protected override void OnParentReceived(DrawableHitObject parent) - { - base.OnParentReceived(parent); - - drawableSlider = (DrawableSlider)parent; + pathVersion.UnbindFrom(DrawableSlider.PathVersion); } protected override void OnApply() { base.OnApply(); - pathVersion.BindTo(drawableSlider.PathVersion); + pathVersion.BindTo(DrawableSlider.PathVersion); - OnShake = drawableSlider.Shake; - CheckHittable = (d, t) => drawableSlider.CheckHittable?.Invoke(d, t) ?? true; + OnShake = DrawableSlider.Shake; + CheckHittable = (d, t) => DrawableSlider.CheckHittable?.Invoke(d, t) ?? true; } protected override void Update() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index f368615e77..2fd9af894d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -18,6 +18,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public new SliderRepeat HitObject => (SliderRepeat)base.HitObject; + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + private double animDuration; public Drawable CirclePiece { get; private set; } @@ -26,8 +28,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; - private DrawableSlider drawableSlider; - public DrawableSliderRepeat() : base(null) { @@ -60,24 +60,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } - protected override void OnParentReceived(DrawableHitObject parent) - { - base.OnParentReceived(parent); - - drawableSlider = (DrawableSlider)parent; - } - protected override void OnApply() { base.OnApply(); - Position = HitObject.Position - drawableSlider.Position; + Position = HitObject.Position - DrawableSlider.Position; } protected override void CheckForResult(bool userTriggered, double timeOffset) { if (HitObject.StartTime <= Time.Current) - ApplyResult(r => r.Type = drawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = DrawableSlider.Tracking.Value ? r.Judgement.MaxResult : r.Judgement.MinResult); } protected override void UpdateInitialTransforms() @@ -119,7 +112,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (IsHit) return; bool isRepeatAtEnd = HitObject.RepeatIndex % 2 == 0; - List curve = ((PlaySliderBody)drawableSlider.Body.Drawable).CurrentCurve; + List curve = ((PlaySliderBody)DrawableSlider.Body.Drawable).CurrentCurve; Position = isRepeatAtEnd ? end : start; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs index d40b6aea6e..c7bfdb02fb 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderTick.cs @@ -22,8 +22,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => false; + protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + private SkinnableDrawable scaleContainer; - private DrawableSlider drawableSlider; public DrawableSliderTick() : base(null) @@ -63,18 +64,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables ScaleBindable.BindValueChanged(scale => scaleContainer.Scale = new Vector2(scale.NewValue)); } - protected override void OnParentReceived(DrawableHitObject parent) - { - base.OnParentReceived(parent); - - drawableSlider = (DrawableSlider)parent; - } - protected override void OnApply() { base.OnApply(); - Position = HitObject.Position - drawableSlider.HitObject.Position; + Position = HitObject.Position - DrawableSlider.HitObject.Position; } protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs index d10c4f7511..726fbd3ea6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinnerTick.cs @@ -1,14 +1,14 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Game.Rulesets.Objects.Drawables; - namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSpinnerTick : DrawableOsuHitObject { public override bool DisplayResult => false; + protected DrawableSpinner DrawableSpinner => (DrawableSpinner)ParentHitObject; + public DrawableSpinnerTick() : base(null) { @@ -19,16 +19,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { } - private DrawableSpinner drawableSpinner; - - protected override void OnParentReceived(DrawableHitObject parent) - { - base.OnParentReceived(parent); - - drawableSpinner = (DrawableSpinner)parent; - } - - protected override double MaximumJudgementOffset => drawableSpinner.HitObject.Duration; + protected override double MaximumJudgementOffset => DrawableSpinner.HitObject.Duration; /// /// Apply a judgement result. diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 37c36ace7b..94d63e4e68 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -43,6 +43,12 @@ namespace osu.Game.Rulesets.Objects.Drawables /// public HitObject HitObject { get; private set; } + /// + /// The parenting , if any. + /// + [CanBeNull] + protected internal DrawableHitObject ParentHitObject { get; internal set; } + /// /// The colour used for various elements of this DrawableHitObject. /// @@ -243,9 +249,9 @@ namespace osu.Game.Rulesets.Objects.Drawables drawableNested.OnRevertResult += onRevertResult; drawableNested.ApplyCustomUpdateState += onApplyCustomUpdateState; - // ApplyParent() should occur before Apply() in all cases, so it's invoked before the nested DHO is added to the hierarchy below, but after its events are initialised. - if (pooledDrawableNested == null) - drawableNested.ApplyParent(this); + // This is only necessary for non-pooled DHOs. For pooled DHOs, this is handled inside GetPooledDrawableRepresentation(). + // Must be done before the nested DHO is added to occur before the nested Apply()! + drawableNested.ParentHitObject = this; nestedHitObjects.Value.Add(drawableNested); AddNestedHitObject(drawableNested); @@ -317,6 +323,7 @@ namespace osu.Game.Rulesets.Objects.Drawables OnFree(); HitObject = null; + ParentHitObject = null; Result = null; lifetimeEntry = null; @@ -350,20 +357,6 @@ namespace osu.Game.Rulesets.Objects.Drawables { } - /// - /// Applies a parenting to this . - /// - /// The parenting . - public void ApplyParent(DrawableHitObject parent) => OnParentReceived(parent); - - /// - /// Invoked when this receives a new parenting . - /// - /// The parenting . - protected virtual void OnParentReceived(DrawableHitObject parent) - { - } - /// /// Invoked by the base to populate samples, once on initial load and potentially again on any change to the samples collection. /// diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index 01b25c9717..cbf3362ea7 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -359,8 +359,7 @@ namespace osu.Game.Rulesets.UI if (!lifetimeEntryMap.TryGetValue(hitObject, out var entry)) lifetimeEntryMap[hitObject] = entry = CreateLifetimeEntry(hitObject); - if (parent != null) - dho.ApplyParent(parent); + dho.ParentHitObject = parent; dho.Apply(hitObject, entry); }); } From 48dad61a46494a588da1078e39295648b3c84dc0 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 3 Dec 2020 19:38:45 +0200 Subject: [PATCH 5098/6909] Apply review suggestions --- .../SongSelect/TestSceneBeatmapRecommendations.cs | 2 -- osu.Game/Rulesets/ILegacyRuleset.cs | 2 ++ osu.Game/Screens/Select/DifficultyRecommender.cs | 12 ++++++++---- 3 files changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 8a4914a31b..6c19206802 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -23,8 +23,6 @@ namespace osu.Game.Tests.Visual.SongSelect { public class TestSceneBeatmapRecommendations : OsuGameTestScene { - protected override bool UseOnlineAPI => false; - [Resolved] private DifficultyRecommender recommender { get; set; } diff --git a/osu.Game/Rulesets/ILegacyRuleset.cs b/osu.Game/Rulesets/ILegacyRuleset.cs index 06a85b5261..f4b03baccd 100644 --- a/osu.Game/Rulesets/ILegacyRuleset.cs +++ b/osu.Game/Rulesets/ILegacyRuleset.cs @@ -5,6 +5,8 @@ namespace osu.Game.Rulesets { public interface ILegacyRuleset { + const int MAX_LEGACY_RULESET_ID = 3; + /// /// Identifies the server-side ID of a legacy ruleset. /// diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index ab64513ecb..f8aaf5c0fd 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.Select [Resolved] private Bindable ruleset { get; set; } + private int storedUserId; + private readonly Dictionary recommendedStarDifficulty = new Dictionary(); private readonly IBindable apiState = new Bindable(); @@ -69,13 +71,15 @@ namespace osu.Game.Screens.Select return beatmap; } - private void calculateRecommendedDifficulties(bool onlyIfNoPreviousValues = false) + private void calculateRecommendedDifficulties() { - if (recommendedStarDifficulty.Any() && onlyIfNoPreviousValues) + if (recommendedStarDifficulty.Any() && api.LocalUser.Value.Id == storedUserId) return; + storedUserId = api.LocalUser.Value.Id; + // only query API for built-in rulesets - rulesets.AvailableRulesets.Where(ruleset => ruleset.ID <= 3).ForEach(rulesetInfo => + rulesets.AvailableRulesets.Where(ruleset => ruleset.ID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID).ForEach(rulesetInfo => { var req = new GetUserRequest(api.LocalUser.Value.Id, rulesetInfo); @@ -117,7 +121,7 @@ namespace osu.Game.Screens.Select switch (state.NewValue) { case APIState.Online: - calculateRecommendedDifficulties(true); + calculateRecommendedDifficulties(); break; } }); From 4cd2e207ac8b8353fd46019d0f446e8e258faf54 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 3 Dec 2020 19:53:06 +0200 Subject: [PATCH 5099/6909] Document getBestRulesetOrder --- osu.Game/Screens/Select/DifficultyRecommender.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index f8aaf5c0fd..21e6629add 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -93,6 +93,9 @@ namespace osu.Game.Screens.Select }); } + /// + /// Rulesets ordered by highest recommended star difficulty, except currently selected ruleset first + /// private IEnumerable getBestRulesetOrder() { IEnumerable bestRulesetOrder = recommendedStarDifficulty.OrderByDescending(pair => pair.Value) From 49be4978bd400fc4ffb1036eb4f9cc23cfd79a07 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 3 Dec 2020 19:53:41 +0200 Subject: [PATCH 5100/6909] Avoid calling ToList twice --- osu.Game/Screens/Select/DifficultyRecommender.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 21e6629add..fa48316e9d 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -99,8 +99,7 @@ namespace osu.Game.Screens.Select private IEnumerable getBestRulesetOrder() { IEnumerable bestRulesetOrder = recommendedStarDifficulty.OrderByDescending(pair => pair.Value) - .Select(pair => pair.Key) - .ToList(); + .Select(pair => pair.Key); List orderedRulesets; From e792f070840bc3857467a3a3a74b00e0e7a075d3 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 3 Dec 2020 20:07:42 +0200 Subject: [PATCH 5101/6909] Add test for recommending current ruleset --- .../TestSceneBeatmapRecommendations.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 6c19206802..75a33af247 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -100,6 +100,24 @@ namespace osu.Game.Tests.Visual.SongSelect presentAndConfirm(() => beatmapSets[3], 2); } + [Test] + public void TestCurrentRulesetIsRecommended() + { + BeatmapSetInfo catchSet = null, mixedSet = null; + + AddStep("create catch beatmapset", () => catchSet = importBeatmapSet(0, new[] { new CatchRuleset().RulesetInfo })); + AddStep("create mixed beatmapset", () => mixedSet = importBeatmapSet(1, + new[] { new TaikoRuleset().RulesetInfo, new CatchRuleset().RulesetInfo, new ManiaRuleset().RulesetInfo })); + + AddAssert("all sets imported", () => ensureAllBeatmapSetsImported(new[] { catchSet, mixedSet })); + + // Switch to catch + presentAndConfirm(() => catchSet, 1); + + // Present mixed difficulty set, expect current ruleset to be selected + presentAndConfirm(() => mixedSet, 2); + } + [Test] public void TestBestRulesetIsRecommended() { From abe5a67bc5329702dd7b0176655b8796c1419c0b Mon Sep 17 00:00:00 2001 From: MATRIX-feather Date: Fri, 4 Dec 2020 03:14:07 +0800 Subject: [PATCH 5102/6909] Simplify implementation --- osu.Game/Screens/Import/FileImportScreen.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index a59688a2a9..211605e5c9 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -45,6 +45,7 @@ namespace osu.Game.Screens.Import var originalPath = storage.GetFullPath("imports", true); string[] fileExtensions = { ".osk", ".osr", ".osz" }; defaultPath = originalPath; + var directory = currentDirectory.Value?.FullName ?? defaultPath; InternalChild = contentContainer = new Container { @@ -67,6 +68,10 @@ namespace osu.Game.Screens.Import Width = 0.65f, Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, + Child = fileSelector = new FileSelector(initialPath: directory, validFileExtensions: fileExtensions) + { + RelativeSizeAxes = Axes.Both + } }, new Container { @@ -169,21 +174,9 @@ namespace osu.Game.Screens.Import { currentFile.Value = null; }); - currentFile.UnbindBindings(); - currentDirectory.UnbindBindings(); - - fileSelector?.Expire(); - - var directory = currentDirectory.Value?.FullName ?? defaultPath; - fileSelector = new FileSelector(initialPath: directory, validFileExtensions: fileExtensions) - { - RelativeSizeAxes = Axes.Both - }; currentDirectory.BindTo(fileSelector.CurrentPath); currentFile.BindTo(fileSelector.CurrentFile); - - fileSelectContainer.Add(fileSelector); } private void updateFileSelectionText(ValueChangedEvent v) From 85de1a1d20cc5299a3c9443ff422da320d92d384 Mon Sep 17 00:00:00 2001 From: MATRIX-feather Date: Fri, 4 Dec 2020 03:19:20 +0800 Subject: [PATCH 5103/6909] Animation changes --- osu.Game/Screens/Import/FileImportScreen.cs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 211605e5c9..9fb8e22977 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -188,17 +188,16 @@ namespace osu.Game.Screens.Import { base.OnEntering(last); - contentContainer.FadeOut().Then().ScaleTo(0.8f).RotateTo(-15).MoveToX(300) + contentContainer.FadeOut().Then().ScaleTo(0.95f) .Then() - .ScaleTo(1, 1500, Easing.OutElastic) - .FadeIn(500) - .MoveToX(0, 500, Easing.OutQuint) - .RotateTo(0, 500, Easing.OutQuint); + .ScaleTo(1, 300, Easing.OutQuint) + .FadeIn(300); } public override bool OnExiting(IScreen next) { - contentContainer.ScaleTo(0.8f, 500, Easing.OutExpo).RotateTo(-15, 500, Easing.OutExpo).MoveToX(300, 500, Easing.OutQuint).FadeOut(500); + contentContainer.ScaleTo(0.95f, 300, Easing.OutQuint) + .FadeOut(300); this.FadeOut(500, Easing.OutExpo); return base.OnExiting(next); From fb080284d2a03c3861d8c2430436c8a30c48d9a2 Mon Sep 17 00:00:00 2001 From: MATRIX-feather Date: Fri, 4 Dec 2020 04:10:08 +0800 Subject: [PATCH 5104/6909] Simplify UI implementation --- osu.Game/Screens/Import/FileImportScreen.cs | 153 +++++++------------- 1 file changed, 54 insertions(+), 99 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 9fb8e22977..9af87887b2 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -13,7 +13,6 @@ using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Graphics.UserInterfaceV2; using osuTK; -using osu.Game.Overlays; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.Containers; using osuTK.Graphics; @@ -22,30 +21,32 @@ namespace osu.Game.Screens.Import { public class FileImportScreen : OsuScreen { - private Container contentContainer; - private FileSelector fileSelector; - private Container fileSelectContainer; - public override bool HideOverlaysOnEnter => true; - private string defaultPath; private readonly Bindable currentFile = new Bindable(); private readonly IBindable currentDirectory = new Bindable(); + + private FileSelector fileSelector; + private Container contentContainer; private TextFlowContainer currentFileText; private OsuScrollContainer fileNameScroll; - private readonly OverlayColourProvider overlayColourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + + private const float duration = 300; + private const float button_height = 50; + private const float button_vertical_margin = 15; [Resolved] private OsuGameBase gameBase { get; set; } + [Resolved] + private OsuColour colours { get; set; } + [BackgroundDependencyLoader(true)] private void load(Storage storage) { storage.GetStorageForDirectory("imports"); var originalPath = storage.GetFullPath("imports", true); string[] fileExtensions = { ".osk", ".osr", ".osz" }; - defaultPath = originalPath; - var directory = currentDirectory.Value?.FullName ?? defaultPath; InternalChild = contentContainer = new Container { @@ -59,19 +60,13 @@ namespace osu.Game.Screens.Import { new Box { - Colour = overlayColourProvider.Background5, + Colour = colours.GreySeafoamDark, RelativeSizeAxes = Axes.Both, }, - fileSelectContainer = new Container + fileSelector = new FileSelector(initialPath: originalPath, validFileExtensions: fileExtensions) { RelativeSizeAxes = Axes.Both, - Width = 0.65f, - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Child = fileSelector = new FileSelector(initialPath: directory, validFileExtensions: fileExtensions) - { - RelativeSizeAxes = Axes.Both - } + Width = 0.65f }, new Container { @@ -79,87 +74,48 @@ namespace osu.Game.Screens.Import Width = 0.35f, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Masking = true, - CornerRadius = 10, Children = new Drawable[] { - new GridContainer + new Box + { + Colour = colours.GreySeafoamDarker, + RelativeSizeAxes = Axes.Both + }, + new Container { RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + Padding = new MarginPadding { Bottom = button_height + button_vertical_margin * 2 }, + Child = fileNameScroll = new OsuScrollContainer { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Child = currentFileText = new TextFlowContainer(t => t.Font = OsuFont.Default.With(size: 30)) { - new Container - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Children = new Drawable[] - { - new Box - { - Colour = overlayColourProvider.Background3, - RelativeSizeAxes = Axes.Both - }, - fileNameScroll = new OsuScrollContainer - { - Masking = false, - RelativeSizeAxes = Axes.Both, - Child = currentFileText = new TextFlowContainer(t => t.Font = OsuFont.Default.With(size: 30)) - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - TextAnchor = Anchor.Centre - }, - }, - } - }, + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + TextAnchor = Anchor.Centre }, - new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Bottom = 15, Top = 15 }, - Children = new Drawable[] - { - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new TriangleButton - { - Text = "Import", - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - RelativeSizeAxes = Axes.X, - Height = 50, - Width = 0.9f, - Action = () => - { - var d = currentFile.Value?.FullName; - if (d != null) - startImport(d); - else - currentFileText.FlashColour(Color4.Red, 500); - } - } - } - } - } - } - } + }, + }, + new TriangleButton + { + Text = "Import", + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + RelativeSizeAxes = Axes.X, + Height = button_height, + Width = 0.9f, + Margin = new MarginPadding { Vertical = button_vertical_margin }, + Action = () => + { + var d = currentFile.Value?.FullName; + if (d != null) + startImport(d); + else + currentFileText.FlashColour(Color4.Red, 500); } } } @@ -190,15 +146,14 @@ namespace osu.Game.Screens.Import contentContainer.FadeOut().Then().ScaleTo(0.95f) .Then() - .ScaleTo(1, 300, Easing.OutQuint) - .FadeIn(300); + .ScaleTo(1, duration, Easing.OutQuint) + .FadeIn(duration); } public override bool OnExiting(IScreen next) { - contentContainer.ScaleTo(0.95f, 300, Easing.OutQuint) - .FadeOut(300); - this.FadeOut(500, Easing.OutExpo); + contentContainer.ScaleTo(0.95f, duration, Easing.OutQuint); + this.FadeOut(duration, Easing.OutQuint); return base.OnExiting(next); } @@ -210,8 +165,8 @@ namespace osu.Game.Screens.Import if (!File.Exists(path)) { - currentFileText.Text = "File not exist"; - currentFileText.FlashColour(Color4.Red, 500); + currentFileText.Text = "No such file"; + currentFileText.FlashColour(colours.Red, duration); return; } From c25e2c3dd577e7d5dc1ce3765ac0d6bec40aca21 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Thu, 3 Dec 2020 23:13:14 +0200 Subject: [PATCH 5105/6909] Select recommended beatmap if last selection is filtered --- osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs index 7935debac7..bf045ed612 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmapSet.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Select.Carousel protected override CarouselItem GetNextToSelect() { - if (LastSelected == null) + if (LastSelected == null || LastSelected.Filtered.Value) { if (GetRecommendedBeatmap?.Invoke(Children.OfType().Where(b => !b.Filtered.Value).Select(b => b.Beatmap)) is BeatmapInfo recommended) return Children.OfType().First(b => b.Beatmap == recommended); From 0bc591fef2dc5ee21680b0f361bb3388af35a825 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Dec 2020 22:38:51 +0100 Subject: [PATCH 5106/6909] Add failing assertions `GameplayBeatmap` has to be used instead of the normal bindable `Beatmap`, beecause the former uses osu!-specific hitobjects, while the latter returns convert objects (i.e. `ConvertSlider`s). Similarly, the mod has to be fetched from the player instead of the global bindable, as `Player` has its own cloned instance of the mod, to which the beatmap is applied. The global bindable instance does not have `FirstObject` set. --- .../Mods/TestSceneOsuModHidden.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs index ff308f389f..1ac3ad9194 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModHidden.cs @@ -2,12 +2,15 @@ // 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.Testing; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Screens.Play; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -17,15 +20,15 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods [Test] public void TestDefaultBeatmapTest() => CreateModTest(new ModTestData { - Mod = new OsuModHidden(), + Mod = new TestOsuModHidden(), Autoplay = true, - PassCondition = checkSomeHit + PassCondition = () => checkSomeHit() && objectWithIncreasedVisibilityHasIndex(0) }); [Test] public void FirstCircleAfterTwoSpinners() => CreateModTest(new ModTestData { - Mod = new OsuModHidden(), + Mod = new TestOsuModHidden(), Autoplay = true, Beatmap = new Beatmap { @@ -54,13 +57,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } } }, - PassCondition = checkSomeHit + PassCondition = () => checkSomeHit() && objectWithIncreasedVisibilityHasIndex(2) }); [Test] public void FirstSliderAfterTwoSpinners() => CreateModTest(new ModTestData { - Mod = new OsuModHidden(), + Mod = new TestOsuModHidden(), Autoplay = true, Beatmap = new Beatmap { @@ -89,13 +92,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods } } }, - PassCondition = checkSomeHit + PassCondition = () => checkSomeHit() && objectWithIncreasedVisibilityHasIndex(2) }); [Test] public void TestWithSliderReuse() => CreateModTest(new ModTestData { - Mod = new OsuModHidden(), + Mod = new TestOsuModHidden(), Autoplay = true, Beatmap = new Beatmap { @@ -116,9 +119,14 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods PassCondition = checkSomeHit }); - private bool checkSomeHit() + private bool checkSomeHit() => Player.ScoreProcessor.JudgedHits >= 4; + + private bool objectWithIncreasedVisibilityHasIndex(int index) + => Player.Mods.Value.OfType().Single().FirstObject == Player.ChildrenOfType().Single().HitObjects[index]; + + private class TestOsuModHidden : OsuModHidden { - return Player.ScoreProcessor.JudgedHits >= 4; + public new HitObject FirstObject => base.FirstObject; } } } From 4d739f11a8352ed16cd7dbb2f499cee9ad6d6240 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Dec 2020 22:40:30 +0100 Subject: [PATCH 5107/6909] Fix spinner ticks getting increased visibility state Regressed in #10696. The old `IsFirstHideableObject()` method did not consider nested hitobjects, while its replacement - `IsFirstAdjustableObject()` - did. Therefore, spinner ticks could be considered first adjustable objects, breaking the old logic. There is no need to match over `SpinnerBonusTick`, as it inherits from `SpinnerTick`. --- osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs index 78e759f0e0..45f314af7b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModHidden.cs @@ -24,7 +24,7 @@ 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 IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner); + protected override bool IsFirstAdjustableObject(HitObject hitObject) => !(hitObject is Spinner || hitObject is SpinnerTick); public override void ApplyToBeatmap(IBeatmap beatmap) { From 71fa0da7f4dc1b57700e68277e4fed251b22df71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Dec 2020 23:13:48 +0100 Subject: [PATCH 5108/6909] Add failing test cases --- .../Audio/SampleInfoEqualityTest.cs | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 osu.Game.Tests/Audio/SampleInfoEqualityTest.cs diff --git a/osu.Game.Tests/Audio/SampleInfoEqualityTest.cs b/osu.Game.Tests/Audio/SampleInfoEqualityTest.cs new file mode 100644 index 0000000000..149096608f --- /dev/null +++ b/osu.Game.Tests/Audio/SampleInfoEqualityTest.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 NUnit.Framework; +using osu.Game.Audio; + +namespace osu.Game.Tests.Audio +{ + [TestFixture] + public class SampleInfoEqualityTest + { + [Test] + public void TestSameSingleSamplesAreEqual() + { + var first = new SampleInfo("sample"); + var second = new SampleInfo("sample"); + + assertEquality(first, second); + } + + [Test] + public void TestDifferentSingleSamplesAreNotEqual() + { + var first = new SampleInfo("first"); + var second = new SampleInfo("second"); + + assertNonEquality(first, second); + } + + [Test] + public void TestDifferentCountSampleSetsAreNotEqual() + { + var first = new SampleInfo("sample", "extra"); + var second = new SampleInfo("sample"); + + assertNonEquality(first, second); + } + + [Test] + public void TestDifferentSampleSetsOfSameCountAreNotEqual() + { + var first = new SampleInfo("first", "common"); + var second = new SampleInfo("common", "second"); + + assertNonEquality(first, second); + } + + [Test] + public void TestSameOrderSameSampleSetsAreEqual() + { + var first = new SampleInfo("first", "second"); + var second = new SampleInfo("first", "second"); + + assertEquality(first, second); + } + + [Test] + public void TestDifferentOrderSameSampleSetsAreEqual() + { + var first = new SampleInfo("first", "second"); + var second = new SampleInfo("second", "first"); + + assertEquality(first, second); + } + + private void assertEquality(SampleInfo first, SampleInfo second) + { + Assert.That(first.Equals(second), Is.True); + Assert.That(first.GetHashCode(), Is.EqualTo(second.GetHashCode())); + } + + private void assertNonEquality(SampleInfo first, SampleInfo second) + { + Assert.That(first.Equals(second), Is.False); + Assert.That(first.GetHashCode(), Is.Not.EqualTo(second.GetHashCode())); + } + } +} From 15d9147eddb9a39a4569d53042fcc05f1d5cf62b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 3 Dec 2020 23:19:26 +0100 Subject: [PATCH 5109/6909] Ensure equality member consistency for SampleInfo The previous implementation of `SampleInfo`'s equality members was not completely correct in its treatment of the `sampleNames` array. While `Equals()` compared the values of `sampleNames` using `SequenceEqual()`, therefore performing a structural check that inspects the contents of both arrays, `GetHashCode()` used `HashCode.Combine()` directly on the arrays, therefore operating on reference equality. This could cause the pooling mechanism of samples to fail, as pointed out in #11079. To resolve, change the `GetHashCode()` implementation such that it also considers the contents of the array rather than just the reference to the array itself. This is achieved by leveraging `StructuralEqualityComparer`. Additionally, as a bonus, an array sort was added to the constructor of `SampleInfo`. This is intended to be a "canonicalisation" processing step for the array of sample names. Thanks to that sort, two instances of `SampleInfo` that have the same sample names but permutated will also turn out to be equal and have the same hash codes, given the implementation of both equality members. This gives `SampleInfo` set-like semantics. --- osu.Game/Audio/SampleInfo.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Audio/SampleInfo.cs b/osu.Game/Audio/SampleInfo.cs index 221bc31639..5d8240204e 100644 --- a/osu.Game/Audio/SampleInfo.cs +++ b/osu.Game/Audio/SampleInfo.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.Linq; @@ -17,6 +18,7 @@ namespace osu.Game.Audio public SampleInfo(params string[] sampleNames) { this.sampleNames = sampleNames; + Array.Sort(sampleNames); } public IEnumerable LookupNames => sampleNames; @@ -25,7 +27,9 @@ namespace osu.Game.Audio public override int GetHashCode() { - return HashCode.Combine(sampleNames, Volume); + return HashCode.Combine( + StructuralComparisons.StructuralEqualityComparer.GetHashCode(sampleNames), + Volume); } public bool Equals(SampleInfo other) From 3de46d0a3be4309fb45b58fa1b9d734d8d6a0b55 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 4 Dec 2020 10:09:07 +0900 Subject: [PATCH 5110/6909] Fix & clarify catcher tests --- .../TestSceneCatcher.cs | 36 ++++++++++--------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index f41a16026a..194a12a9b7 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Catch.Tests } [Test] - public void TestCatcherStateFruit() + public void TestFruitChangesCatcherState() { AddStep("miss fruit", () => attemptCatch(new Fruit { X = 100 })); checkState(CatcherAnimationState.Fail); @@ -82,7 +82,19 @@ namespace osu.Game.Rulesets.Catch.Tests } [Test] - public void TestCatcherStateTinyDroplet() + public void TestNormalFruitResetsHyperDashState() + { + AddStep("catch hyper fruit", () => attemptCatch(new Fruit + { + HyperDashTarget = new Fruit { X = 100 } + })); + checkHyperDash(true); + AddStep("catch normal fruit", () => attemptCatch(new Fruit())); + checkHyperDash(false); + } + + [Test] + public void TestTinyDropletMissPreservesCatcherState() { AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit { @@ -90,19 +102,21 @@ namespace osu.Game.Rulesets.Catch.Tests })); AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet())); AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 })); + // catcher state and hyper dash state is preserved checkState(CatcherAnimationState.Kiai); checkHyperDash(true); } [Test] - public void TestCatcherStateBanana() + public void TestBananaMissPreservesCatcherState() { AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit { HyperDashTarget = new Fruit { X = 100 } })); - AddStep("miss banana", () => attemptCatch(new Banana())); - checkState(CatcherAnimationState.Idle); + AddStep("miss banana", () => attemptCatch(new Banana { X = 100 })); + // catcher state is preserved but hyper dash state is reset + checkState(CatcherAnimationState.Kiai); checkHyperDash(false); } @@ -135,18 +149,6 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10); } - [Test] - public void TestHyperFruitHyperDash() - { - AddStep("catch hyper fruit", () => attemptCatch(new Fruit - { - HyperDashTarget = new Fruit { X = 100 } - })); - checkHyperDash(true); - AddStep("catch normal fruit", () => attemptCatch(new Fruit())); - checkHyperDash(false); - } - [TestCase(true)] [TestCase(false)] public void TestHitLighting(bool enabled) From e82ca66d3ee144b5569e646d25a22e246458aac8 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 4 Dec 2020 10:21:54 +0900 Subject: [PATCH 5111/6909] Fix depth of dropped objects --- .../TestSceneCatcherArea.cs | 26 +++++++++++++------ .../TestSceneHyperDashColouring.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 6 ++--- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 4 +-- 4 files changed, 24 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 7be6fc92ac..281ddc7eaa 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -72,14 +73,23 @@ namespace osu.Game.Rulesets.Catch.Tests { circleSize = size; - SetContents(() => new CatchInputManager(catchRuleset) + SetContents(() => { - RelativeSizeAxes = Axes.Both, - Child = new TestCatcherArea(new BeatmapDifficulty { CircleSize = size }) + var droppedObjectContainer = new Container(); + + return new CatchInputManager(catchRuleset) { - Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, - }, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + droppedObjectContainer, + new TestCatcherArea(droppedObjectContainer, new BeatmapDifficulty { CircleSize = size }) + { + Anchor = Anchor.Centre, + Origin = Anchor.TopCentre, + } + } + }; }); } @@ -91,8 +101,8 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestCatcherArea : CatcherArea { - public TestCatcherArea(BeatmapDifficulty beatmapDifficulty) - : base(beatmapDifficulty) + public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty) + : base(droppedObjectContainer, beatmapDifficulty) { } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 1b8368794c..07cb73e5ff 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -117,7 +117,7 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("create hyper-dashing catcher", () => { - Child = setupSkinHierarchy(catcherArea = new CatcherArea + Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index 6934dcc1f9..df87359ed6 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -36,12 +36,12 @@ namespace osu.Game.Rulesets.Catch.UI public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation) { - var explodingFruitContainer = new Container + var droppedObjectContainer = new Container { RelativeSizeAxes = Axes.Both, }; - CatcherArea = new CatcherArea(difficulty) + CatcherArea = new CatcherArea(droppedObjectContainer, difficulty) { Anchor = Anchor.BottomLeft, Origin = Anchor.TopLeft, @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Catch.UI InternalChildren = new[] { - explodingFruitContainer, + droppedObjectContainer, CatcherArea.MovableCatcher.CreateProxiedContent(), HitObjectContainer, CatcherArea, diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 9cd0785b85..539776354c 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.UI public readonly Catcher MovableCatcher; private readonly CatchComboDisplay comboDisplay; - public CatcherArea(BeatmapDifficulty difficulty = null) + public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); Children = new Drawable[] @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI Margin = new MarginPadding { Bottom = 350f }, X = CatchPlayfield.CENTER_X }, - MovableCatcher = new Catcher(this, this, difficulty) { X = CatchPlayfield.CENTER_X }, + MovableCatcher = new Catcher(this, droppedObjectContainer, difficulty) { X = CatchPlayfield.CENTER_X }, }; } From 23af70dd328c9216dde5540fa8f6c9e6e30c6f8a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 4 Dec 2020 10:24:25 +0900 Subject: [PATCH 5112/6909] Invert `if` --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 41 +++++++++++++-------------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 1101e5b6b4..1037678734 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -245,30 +245,29 @@ namespace osu.Game.Rulesets.Catch.UI catchObjectPosition >= catcherPosition - halfCatchWidth && catchObjectPosition <= catcherPosition + halfCatchWidth; - // droplet doesn't affect the catcher state - if (!(fruit is TinyDroplet)) - { - if (validCatch && fruit.HyperDash) - { - var target = fruit.HyperDashTarget; - var timeDifference = target.StartTime - fruit.StartTime; - double positionDifference = target.X - 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); - } - if (validCatch) placeCaughtObject(fruit); + // droplet doesn't affect the catcher state + if (fruit is TinyDroplet) return validCatch; + + if (validCatch && fruit.HyperDash) + { + var target = fruit.HyperDashTarget; + var timeDifference = target.StartTime - fruit.StartTime; + double positionDifference = target.X - 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; } From 898802340787ec59b212d8d0e7aa955ed797da88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Dec 2020 14:35:56 +0900 Subject: [PATCH 5113/6909] Tidy up code formatting and remove unnecessarily publicly exposed methods --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 84 +++++++++++++-------------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 1037678734..2b88f24348 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -175,55 +175,19 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Calculates the scale of the catcher based off the provided beatmap difficulty. /// - private static Vector2 calculateScale(BeatmapDifficulty difficulty) - => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); + private static Vector2 calculateScale(BeatmapDifficulty difficulty) => new Vector2(1.0f - 0.7f * (difficulty.CircleSize - 5) / 5); /// /// 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; + 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)); - - /// - /// Add a caught fruit to the catcher's stack. - /// - /// The fruit that was caught. - public void PlaceOnPlate(DrawablePalpableCatchHitObject fruit) - { - var ourRadius = fruit.DisplayRadius; - float theirRadius = 0; - - const float allowance = 10; - - while (caughtFruitContainer.Any(f => - Vector2Extensions.Distance(f.Position, fruit.Position) < (ourRadius + (theirRadius = CatchHitObject.OBJECT_RADIUS / 2)) / (allowance / 2))) - { - var diff = (ourRadius + theirRadius) / allowance; - fruit.X += (RNG.NextSingle() - 0.5f) * diff * 2; - fruit.Y -= RNG.NextSingle() * diff; - } - - fruit.X = Math.Clamp(fruit.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); - - caughtFruitContainer.Add(fruit); - - if (hitLighting.Value) - { - HitExplosion hitExplosion = hitExplosionPool.Get(); - hitExplosion.X = fruit.X; - hitExplosion.Scale = new Vector2(fruit.HitObject.Scale); - hitExplosion.ObjectColour = fruit.AccentColour.Value; - hitExplosionContainer.Add(hitExplosion); - } - } + internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty)); /// /// Let the catcher attempt to catch a fruit. @@ -375,12 +339,10 @@ namespace osu.Game.Rulesets.Catch.UI public void Drop() => clearPlate(DroppedObjectAnimation.Drop); /// - /// Explode any fruit off the plate. + /// Explode all fruit off the plate. /// public void Explode() => clearPlate(DroppedObjectAnimation.Explode); - public void Explode(DrawablePalpableCatchHitObject caughtObject) => removeFromPlate(caughtObject, DroppedObjectAnimation.Explode); - protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); @@ -456,6 +418,7 @@ namespace osu.Game.Rulesets.Catch.UI private void placeCaughtObject(PalpableCatchHitObject source) { var caughtObject = createCaughtObject(source); + if (caughtObject == null) return; caughtObject.RelativePositionAxes = Axes.None; @@ -468,10 +431,43 @@ namespace osu.Game.Rulesets.Catch.UI caughtObject.LifetimeStart = source.StartTime; caughtObject.LifetimeEnd = double.MaxValue; - PlaceOnPlate(caughtObject); + adjustPositionInStack(caughtObject); + + caughtFruitContainer.Add(caughtObject); + + addLighting(caughtObject); if (!caughtObject.StaysOnPlate) - Explode(caughtObject); + removeFromPlate(caughtObject, DroppedObjectAnimation.Explode); + } + + private void adjustPositionInStack(DrawablePalpableCatchHitObject caughtObject) + { + const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2; + const float allowance = 10; + + float caughtObjectRadius = caughtObject.DisplayRadius; + + while (caughtFruitContainer.Any(f => Vector2Extensions.Distance(f.Position, caughtObject.Position) < (caughtObjectRadius + radius_div_2) / (allowance / 2))) + { + float diff = (caughtObjectRadius + radius_div_2) / allowance; + + caughtObject.X += (RNG.NextSingle() - 0.5f) * diff * 2; + caughtObject.Y -= RNG.NextSingle() * diff; + } + + caughtObject.X = Math.Clamp(caughtObject.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); + } + + private void addLighting(DrawablePalpableCatchHitObject caughtObject) + { + if (!hitLighting.Value) return; + + HitExplosion hitExplosion = hitExplosionPool.Get(); + hitExplosion.X = caughtObject.X; + hitExplosion.Scale = new Vector2(caughtObject.HitObject.Scale); + hitExplosion.ObjectColour = caughtObject.AccentColour.Value; + hitExplosionContainer.Add(hitExplosion); } private DrawablePalpableCatchHitObject createCaughtObject(PalpableCatchHitObject source) From d3a17b65d52f1eabb6351a9036e983d1970ac5bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Dec 2020 14:36:40 +0900 Subject: [PATCH 5114/6909] Move public methods upwards --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 50 +++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 2b88f24348..2a3447c80a 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -266,24 +266,17 @@ namespace osu.Game.Rulesets.Catch.UI } } - private void runHyperDashStateTransition(bool hyperDashing) + public void UpdatePosition(float position) { - updateTrailVisibility(); + position = Math.Clamp(position, 0, CatchPlayfield.WIDTH); - if (hyperDashing) - { - this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); - this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); - } - else - { - this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); - this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); - } + if (position == X) + return; + + Scale = new Vector2(Math.Abs(Scale.X) * (position > X ? 1 : -1), Scale.Y); + X = position; } - private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing; - public bool OnPressed(CatchAction action) { switch (action) @@ -322,17 +315,6 @@ namespace osu.Game.Rulesets.Catch.UI } } - public void UpdatePosition(float position) - { - position = Math.Clamp(position, 0, CatchPlayfield.WIDTH); - - 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. /// @@ -343,6 +325,24 @@ namespace osu.Game.Rulesets.Catch.UI /// public void Explode() => clearPlate(DroppedObjectAnimation.Explode); + private void runHyperDashStateTransition(bool hyperDashing) + { + updateTrailVisibility(); + + if (hyperDashing) + { + this.FadeColour(hyperDashColour, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + this.FadeTo(0.2f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + } + else + { + this.FadeColour(Color4.White, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + this.FadeTo(1f, HYPER_DASH_TRANSITION_DURATION, Easing.OutQuint); + } + } + + private void updateTrailVisibility() => trails.DisplayTrail = Dashing || HyperDashing; + protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); From b8c284b34fcfd754451382ab66612c3de5c5f37e Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 4 Dec 2020 00:51:46 -0800 Subject: [PATCH 5115/6909] Fix one more key binding string not being sentence cased --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index f4a4813b94..1270df5374 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -149,7 +149,7 @@ namespace osu.Game.Input.Bindings [Description("Select")] Select, - [Description("Quick exit (Hold)")] + [Description("Quick exit (hold)")] QuickExit, // Game-wide beatmap music controller keybindings From a12b04915447d11f43598a81df468e3eba9e8aaa Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 4 Dec 2020 20:11:35 +0900 Subject: [PATCH 5116/6909] Move piece files in Osu ruleset --- .../Drawables/Pieces => Skinning/Default}/ApproachCircle.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/CirclePiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/DefaultSpinnerDisc.cs | 0 .../Drawables/Pieces => Skinning/Default}/DrawableSliderPath.cs | 0 .../Drawables/Pieces => Skinning/Default}/ExplodePiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/FlashPiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/GlowPiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/MainCirclePiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/ManualSliderBody.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/NumberPiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/PlaySliderBody.cs | 0 .../Drawables/Pieces => Skinning/Default}/ReverseArrowPiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/RingPiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/SliderBall.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/SliderBody.cs | 0 .../Drawables/Pieces => Skinning/Default}/SnakingSliderBody.cs | 0 .../Drawables/Pieces => Skinning/Default}/SpinnerBonusDisplay.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/SpinnerFill.cs | 0 .../Pieces => Skinning/Default}/SpinnerRotationTracker.cs | 0 .../Drawables/Pieces => Skinning/Default}/SpinnerSpmCounter.cs | 0 .../Drawables/Pieces => Skinning/Default}/SpinnerTicks.cs | 0 .../Drawables/Pieces => Skinning/Default}/TrianglesPiece.cs | 0 osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacyCursor.cs | 0 osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacyCursorTrail.cs | 0 .../Skinning/{ => Legacy}/LegacyMainCirclePiece.cs | 0 .../Skinning/{ => Legacy}/LegacyNewStyleSpinner.cs | 0 .../Skinning/{ => Legacy}/LegacyOldStyleSpinner.cs | 0 osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacySliderBall.cs | 0 osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacySliderBody.cs | 0 osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacySpinner.cs | 0 30 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/ApproachCircle.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/CirclePiece.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/DefaultSpinnerDisc.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/DrawableSliderPath.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/ExplodePiece.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/FlashPiece.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/GlowPiece.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/MainCirclePiece.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/ManualSliderBody.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/NumberPiece.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/PlaySliderBody.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/ReverseArrowPiece.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/RingPiece.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/SliderBall.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/SliderBody.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/SnakingSliderBody.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/SpinnerBonusDisplay.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/SpinnerFill.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/SpinnerRotationTracker.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/SpinnerSpmCounter.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/SpinnerTicks.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables/Pieces => Skinning/Default}/TrianglesPiece.cs (100%) rename osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacyCursor.cs (100%) rename osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacyCursorTrail.cs (100%) rename osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacyMainCirclePiece.cs (100%) rename osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacyNewStyleSpinner.cs (100%) rename osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacyOldStyleSpinner.cs (100%) rename osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacySliderBall.cs (100%) rename osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacySliderBody.cs (100%) rename osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/LegacySpinner.cs (100%) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ApproachCircle.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ApproachCircle.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/ApproachCircle.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/CirclePiece.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DefaultSpinnerDisc.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DrawableSliderPath.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/DrawableSliderPath.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ExplodePiece.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/FlashPiece.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/GlowPiece.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/GlowPiece.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/GlowPiece.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/MainCirclePiece.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ManualSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ManualSliderBody.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/NumberPiece.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/PlaySliderBody.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/ReverseArrowPiece.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/RingPiece.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBall.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBody.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SliderBody.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SliderBody.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SnakingSliderBody.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerBonusDisplay.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerFill.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerFill.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerFill.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerRotationTracker.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerSpmCounter.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerTicks.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/SpinnerTicks.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerTicks.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/Pieces/TrianglesPiece.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Skinning/LegacyCursor.cs rename to osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Skinning/LegacyCursorTrail.cs rename to osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Skinning/LegacyMainCirclePiece.cs rename to osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Skinning/LegacyNewStyleSpinner.cs rename to osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Skinning/LegacyOldStyleSpinner.cs rename to osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Skinning/LegacySliderBall.cs rename to osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Skinning/LegacySliderBody.cs rename to osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs diff --git a/osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Skinning/LegacySpinner.cs rename to osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs From 245be2c5ed0274a25f331b8027f35001171be7b7 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 4 Dec 2020 20:21:53 +0900 Subject: [PATCH 5117/6909] Adjust namespace --- .../Mods/TestSceneOsuModSpunOut.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs | 2 +- .../TestSceneSliderApplication.cs | 2 +- osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs | 2 +- .../Blueprints/HitCircles/Components/HitCirclePiece.cs | 2 +- .../Blueprints/Sliders/Components/SliderBodyPiece.cs | 2 +- .../Edit/Blueprints/Spinners/Components/SpinnerPiece.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 2 +- .../Objects/Drawables/DrawableHitCircle.cs | 2 +- .../Objects/Drawables/DrawableSlider.cs | 2 +- .../Objects/Drawables/DrawableSliderRepeat.cs | 2 +- .../Objects/Drawables/DrawableSpinner.cs | 2 +- .../Objects/Drawables/SpinnerBackgroundLayer.cs | 2 +- .../Objects/Drawables/SpinnerCentreLayer.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/Default/ApproachCircle.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs | 3 ++- .../Skinning/Default/DefaultSpinnerDisc.cs | 4 +++- .../Skinning/Default/DrawableSliderPath.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs | 3 ++- osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs | 5 +++-- osu.Game.Rulesets.Osu/Skinning/Default/GlowPiece.cs | 2 +- .../Skinning/Default/MainCirclePiece.cs | 4 +++- .../Skinning/Default/ManualSliderBody.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs | 6 +++--- osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs | 5 +++-- .../Skinning/Default/ReverseArrowPiece.cs | 9 +++++---- osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs | 5 +++-- osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs | 9 +++++---- osu.Game.Rulesets.Osu/Skinning/Default/SliderBody.cs | 2 +- .../Skinning/Default/SnakingSliderBody.cs | 4 +++- .../Skinning/Default/SpinnerBonusDisplay.cs | 3 ++- osu.Game.Rulesets.Osu/Skinning/Default/SpinnerFill.cs | 2 +- .../Skinning/Default/SpinnerRotationTracker.cs | 3 ++- .../Skinning/Default/SpinnerSpmCounter.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/Default/SpinnerTicks.cs | 6 +++--- osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs | 4 ++-- .../Skinning/Legacy/LegacyCursorTrail.cs | 2 +- .../Skinning/Legacy/LegacyMainCirclePiece.cs | 2 +- .../Skinning/Legacy/LegacyNewStyleSpinner.cs | 2 +- .../Skinning/Legacy/LegacyOldStyleSpinner.cs | 2 +- .../Skinning/Legacy/LegacySliderBall.cs | 2 +- .../Skinning/Legacy/LegacySliderBody.cs | 4 ++-- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 2 +- .../Skinning/OsuLegacySkinTransformer.cs | 1 + 45 files changed, 75 insertions(+), 59 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs index 7b909d2907..7df5ca0f7c 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModSpunOut.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Objects; 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.Rulesets.Osu.Skinning.Default; using osuTK; namespace osu.Game.Rulesets.Osu.Tests.Mods diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs index dde02e873b..fefe983f97 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneCursorTrail.cs @@ -12,7 +12,7 @@ using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.Testing.Input; using osu.Game.Audio; -using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs index 084af7dafe..aac6db60fe 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderApplication.cs @@ -12,7 +12,7 @@ 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.Skinning.Default; using osu.Game.Skinning; using osu.Game.Tests.Visual; using osuTK; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs index b71400b71d..e111bb1054 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderSnaking.cs @@ -19,7 +19,7 @@ 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.Rulesets.Osu.Skinning.Default; using osu.Game.Storyboards; using osuTK; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs index 2868ddeaa4..0cfc67cedb 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/Components/HitCirclePiece.cs @@ -5,7 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Osu.Skinning.Default; using osuTK; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles.Components diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs index 5581ce4bfd..1c3d270c95 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/Components/SliderBodyPiece.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Graphics; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Osu.Skinning.Default; using osuTK; using osuTK.Graphics; diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs index 2347d8a34c..92961b40bc 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Spinners/Components/SpinnerPiece.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Osu.Skinning.Default; using osuTK; namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Spinners.Components diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index b7e60295cb..df0a41455f 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -7,7 +7,7 @@ using osu.Framework.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.Osu.Skinning.Default; namespace osu.Game.Rulesets.Osu.Mods { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index abb51ae420..3c0260f5f5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -12,7 +12,7 @@ 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.Osu.Skinning.Default; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osuTK; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index af5b609ec8..511cbc2347 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -7,13 +7,13 @@ using JetBrains.Annotations; using osuTK; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.UI; using osuTK.Graphics; using osu.Game.Skinning; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs index a684df98cb..76490e0de1 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderRepeat.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Skinning; using osuTK; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index aea37acf6f..1f3bcece0c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -15,8 +15,8 @@ using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Scoring; using osu.Game.Screens.Ranking; using osu.Game.Skinning; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs index 3cd2454706..10a7d33073 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs @@ -5,7 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Osu.Skinning.Default; namespace osu.Game.Rulesets.Osu.Objects.Drawables { diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs index b62ce822f0..0c38c3a855 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Osu.Skinning.Default; using osuTK; using osuTK.Graphics; diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ApproachCircle.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ApproachCircle.cs index 1b474f265c..62f00a2b49 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ApproachCircle.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ApproachCircle.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class ApproachCircle : Container { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs index d0e1055dce..ba41ebd445 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/CirclePiece.cs @@ -7,9 +7,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osuTK; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class CirclePiece : CompositeDrawable { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs index 14ce3b014d..667fee1495 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DefaultSpinnerDisc.cs @@ -10,10 +10,12 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class DefaultSpinnerDisc : CompositeDrawable { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs b/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs index c31d6beb01..db077f009d 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/DrawableSliderPath.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics.Lines; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public abstract class DrawableSliderPath : SmoothPath { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs index 09299a3622..510ed225a8 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ExplodePiece.cs @@ -5,9 +5,10 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; using osuTK; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class ExplodePiece : Container { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs index 038a2299e9..06ee64d8b3 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/FlashPiece.cs @@ -3,10 +3,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osuTK; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu.Objects; +using osuTK; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class FlashPiece : Container { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/GlowPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/GlowPiece.cs index 30937313fd..f5e01b802e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/GlowPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/GlowPiece.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class GlowPiece : Container { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs index 102166f8dd..fcbe4c1b28 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/MainCirclePiece.cs @@ -6,10 +6,12 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class MainCirclePiece : CompositeDrawable { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs index d69df1d5c2..d73c94eb9b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ManualSliderBody.cs @@ -4,7 +4,7 @@ using System.Collections.Generic; using osuTK; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { /// /// A with the ability to set the drawn vertices manually. diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs index 7c94568835..bea6186501 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs @@ -5,12 +5,12 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; -using osu.Game.Graphics.Sprites; -using osuTK.Graphics; using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; using osu.Game.Skinning; +using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class NumberPiece : Container { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index 29dff53f54..e77c93c721 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -5,11 +5,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Configuration; -using osu.Game.Rulesets.Osu.Skinning; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public abstract class PlaySliderBody : SnakingSliderBody { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs index ae43006e76..0009ffc586 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/ReverseArrowPiece.cs @@ -1,17 +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.Audio.Track; using osu.Framework.Graphics; -using osuTK; 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; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Skinning; +using osuTK; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class ReverseArrowPiece : BeatSyncedContainer { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs index 619fea73bc..7f10a7bf56 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/RingPiece.cs @@ -3,11 +3,12 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Osu.Objects; using osuTK; using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class RingPiece : CircularContainer { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs index ca5ca7ac59..a96beb66d4 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs @@ -10,15 +10,16 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input; using osu.Framework.Input.Events; +using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; -using osu.Game.Rulesets.Osu.Skinning; -using osuTK.Graphics; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; -using osu.Game.Graphics; +using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class SliderBall : CircularContainer, ISliderProgress, IRequireHighFrequencyMousePosition, IHasAccentColour { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBody.cs index 8758a4a066..7e6df759f8 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBody.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics.Lines; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public abstract class SliderBody : CompositeDrawable { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs index 8835a0d84a..ed4e04184b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SnakingSliderBody.cs @@ -8,9 +8,11 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osuTK; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { /// /// A which changes its curve depending on the snaking progress. diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs index f483bb1b26..c0db6228ef 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBonusDisplay.cs @@ -5,8 +5,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Osu.Objects; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { /// /// Shows incremental bonus score achieved for a spinner. diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerFill.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerFill.cs index 043bc5618c..f574ae589e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerFill.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerFill.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class SpinnerFill : CircularContainer, IHasAccentColour { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs index f82003edb8..9393a589eb 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerRotationTracker.cs @@ -9,10 +9,11 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Screens.Play; using osuTK; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class SpinnerRotationTracker : CircularContainer { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs index 80ab03c45c..e5952ecf97 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs @@ -10,7 +10,7 @@ using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class SpinnerSpmCounter : Container { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerTicks.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerTicks.cs index ba7e8eae6f..e518ae1da8 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerTicks.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerTicks.cs @@ -7,12 +7,12 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; -using osuTK; -using osuTK.Graphics; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; +using osuTK; +using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class SpinnerTicks : Container, IHasAccentColour { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs index 53dc7ecea3..fa23c60d57 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/TrianglesPiece.cs @@ -3,7 +3,7 @@ using osu.Game.Graphics.Backgrounds; -namespace osu.Game.Rulesets.Osu.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class TrianglesPiece : Triangles { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs index e96bd29ad5..314139d02a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursor.cs @@ -3,11 +3,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Skinning; using osu.Game.Rulesets.Osu.UI.Cursor; +using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Osu.Skinning +namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public class LegacyCursor : OsuCursorSprite { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index e6cd7bc59d..f18d3191ca 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -10,7 +10,7 @@ using osu.Game.Configuration; using osu.Game.Rulesets.Osu.UI.Cursor; using osu.Game.Skinning; -namespace osu.Game.Rulesets.Osu.Skinning +namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public class LegacyCursorTrail : CursorTrail { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs index 21af9a479e..545e80a709 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyMainCirclePiece.cs @@ -17,7 +17,7 @@ using osuTK; using osuTK.Graphics; using static osu.Game.Skinning.LegacySkinConfiguration; -namespace osu.Game.Rulesets.Osu.Skinning +namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public class LegacyMainCirclePiece : CompositeDrawable { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index 05f4c8e307..efeca53969 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -13,7 +13,7 @@ using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Skinning +namespace osu.Game.Rulesets.Osu.Skinning.Legacy { /// /// Legacy skinned spinner with two main spinning layers, one fixed overlay and one final spinning overlay. diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs index fba802f085..4e07cb60b3 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Osu.Skinning +namespace osu.Game.Rulesets.Osu.Skinning.Legacy { /// /// Legacy skinned spinner with one main spinning layer and a background layer. diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs index 836069013d..1a8c5ada1b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBall.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Skinning; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Skinning +namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public class LegacySliderBall : CompositeDrawable { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs index aad8b189d9..744ded37c9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySliderBody.cs @@ -5,10 +5,10 @@ using System; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Utils; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Osu.Skinning.Default; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Skinning +namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public class LegacySliderBody : PlaySliderBody { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 5aa136cf7e..ec7ecb0d28 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Osu.Skinning +namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public abstract class LegacySpinner : CompositeDrawable { diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs index 78bc26eff7..70abfa1ac9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Skinning; using osuTK; From 626956febdeb3505b91b4aab168f013c1f15060e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 4 Dec 2020 20:25:12 +0900 Subject: [PATCH 5118/6909] Move some files from Drawables to Skinning.Default namespace --- .../Drawables => Skinning/Default}/SpinnerBackgroundLayer.cs | 0 .../{Objects/Drawables => Skinning/Default}/SpinnerCentreLayer.cs | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Osu/{Objects/Drawables => Skinning/Default}/SpinnerBackgroundLayer.cs (100%) rename osu.Game.Rulesets.Osu/{Objects/Drawables => Skinning/Default}/SpinnerCentreLayer.cs (100%) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBackgroundLayer.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerBackgroundLayer.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBackgroundLayer.cs diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerCentreLayer.cs similarity index 100% rename from osu.Game.Rulesets.Osu/Objects/Drawables/SpinnerCentreLayer.cs rename to osu.Game.Rulesets.Osu/Skinning/Default/SpinnerCentreLayer.cs From 0ed9989a8203bde71733773bb43e79399b28cd03 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Fri, 4 Dec 2020 20:25:49 +0900 Subject: [PATCH 5119/6909] Adjust namespace --- .../Skinning/Default/SpinnerBackgroundLayer.cs | 3 +-- osu.Game.Rulesets.Osu/Skinning/Default/SpinnerCentreLayer.cs | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBackgroundLayer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBackgroundLayer.cs index 10a7d33073..f8a6e1d3c9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBackgroundLayer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerBackgroundLayer.cs @@ -5,9 +5,8 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Skinning.Default; -namespace osu.Game.Rulesets.Osu.Objects.Drawables +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class SpinnerBackgroundLayer : SpinnerFill { diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerCentreLayer.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerCentreLayer.cs index 0c38c3a855..67b5ed5410 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerCentreLayer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerCentreLayer.cs @@ -9,11 +9,11 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Osu.Objects.Drawables +namespace osu.Game.Rulesets.Osu.Skinning.Default { public class SpinnerCentreLayer : CompositeDrawable, IHasAccentColour { From 7c0edb796ee352cbc96950e152d7308dade7e1f5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Dec 2020 20:49:18 +0900 Subject: [PATCH 5120/6909] Always confine mouse to screen when running fullscreen --- osu.Game/Input/ConfineMouseTracker.cs | 15 +++++++++++---- osu.Game/Input/OsuConfineMouseMode.cs | 5 ----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index 3dadae6317..75d9c8debb 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -18,6 +18,8 @@ namespace osu.Game.Input public class ConfineMouseTracker : Component { private Bindable frameworkConfineMode; + private Bindable frameworkWindowMode; + private Bindable osuConfineMode; private IBindable localUserPlaying; @@ -25,6 +27,9 @@ namespace osu.Game.Input private void load(OsuGame game, FrameworkConfigManager frameworkConfigManager, OsuConfigManager osuConfigManager) { frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); + frameworkWindowMode = frameworkConfigManager.GetBindable(FrameworkSetting.WindowMode); + frameworkWindowMode.BindValueChanged(_ => updateConfineMode()); + osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); @@ -38,16 +43,18 @@ namespace osu.Game.Input if (frameworkConfineMode.Disabled) return; + if (frameworkWindowMode.Value == WindowMode.Fullscreen) + { + frameworkConfineMode.Value = ConfineMouseMode.Fullscreen; + return; + } + switch (osuConfineMode.Value) { case OsuConfineMouseMode.Never: frameworkConfineMode.Value = ConfineMouseMode.Never; break; - case OsuConfineMouseMode.Fullscreen: - frameworkConfineMode.Value = ConfineMouseMode.Fullscreen; - break; - case OsuConfineMouseMode.DuringGameplay: frameworkConfineMode.Value = localUserPlaying.Value ? ConfineMouseMode.Always : ConfineMouseMode.Never; break; diff --git a/osu.Game/Input/OsuConfineMouseMode.cs b/osu.Game/Input/OsuConfineMouseMode.cs index 32b456395c..a4a1c9eb46 100644 --- a/osu.Game/Input/OsuConfineMouseMode.cs +++ b/osu.Game/Input/OsuConfineMouseMode.cs @@ -17,11 +17,6 @@ namespace osu.Game.Input /// Never, - /// - /// The mouse cursor will be locked to the window bounds while in fullscreen mode. - /// - Fullscreen, - /// /// The mouse cursor will be locked to the window bounds during gameplay, /// but may otherwise move freely. From 7104230ae3645392e051e1b998fb6c4bb809aabb Mon Sep 17 00:00:00 2001 From: MATRIX-feather Date: Fri, 4 Dec 2020 19:52:25 +0800 Subject: [PATCH 5121/6909] Add "onCurrentDirectoryChanged" --- osu.Game/Screens/Import/FileImportScreen.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 9af87887b2..e2e2b699f5 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -126,15 +126,17 @@ namespace osu.Game.Screens.Import fileNameScroll.ScrollContent.Origin = Anchor.Centre; currentFile.BindValueChanged(updateFileSelectionText, true); - currentDirectory.BindValueChanged(_ => - { - currentFile.Value = null; - }); + currentDirectory.BindValueChanged(onCurrentDirectoryChanged); currentDirectory.BindTo(fileSelector.CurrentPath); currentFile.BindTo(fileSelector.CurrentFile); } + private void onCurrentDirectoryChanged(ValueChangedEvent v) + { + currentFile.Value = null; + } + private void updateFileSelectionText(ValueChangedEvent v) { currentFileText.Text = v.NewValue?.Name ?? "Select a file"; From 825120fed3b3111b3dcd836a7ef18e4716160515 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 4 Dec 2020 18:49:01 +0100 Subject: [PATCH 5122/6909] Display import state in a notification. --- osu.Game/Database/ArchiveModelManager.cs | 37 +++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 9a7aa7b039..7b59726cd0 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -218,7 +218,42 @@ namespace osu.Game.Database /// The stream to import files from. /// The filename of the archive being imported. public async Task Import(Stream stream, string filename) - => await Import(new ZipArchiveReader(stream, filename)); // we need to keep around the filename as some model managers (namely SkinManager) use the archive name to populate skin info + { + var notification = new ProgressNotification + { + Progress = 0, + State = ProgressNotificationState.Active, + Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...", + }; + + PostNotification.Invoke(notification); + + try + { + // we need to keep around the filename as some model managers (namely SkinManager) use the archive name to populate skin info + var imported = await Import(new ZipArchiveReader(stream, filename), notification.CancellationToken); + + notification.CompletionText = $"Imported {imported}! Click to view."; + notification.CompletionClickAction += () => + { + PresentImport?.Invoke(new[] { imported }); + return true; + }; + notification.State = ProgressNotificationState.Completed; + } + catch (TaskCanceledException) + { + throw; + } + catch (Exception e) + { + notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed!"; + notification.State = ProgressNotificationState.Cancelled; + Logger.Error(e, $@"Could not import ({filename})", LoggingTarget.Database); + } + + return; + } /// /// Fired when the user requests to view the resulting import. From dd21de0cd526fb81c4d80beea034e343744a740a Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 4 Dec 2020 22:07:45 +0100 Subject: [PATCH 5123/6909] Fix code inspections. --- osu.Android/OsuGameActivity.cs | 4 ++-- osu.Game/Database/ArchiveModelManager.cs | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index c41323b97f..2b25c37d58 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -12,7 +12,6 @@ using osu.Framework.Android; namespace osu.Android { - [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] [IntentFilter(new[] { Intent.ActionDefault }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] public class OsuGameActivity : AndroidGameActivity @@ -44,8 +43,9 @@ namespace osu.Android var stream = ContentResolver.OpenInputStream(intent.Data); if (stream != null) // intent handler may run before the game has even loaded so we need to wait for the file importers to load before launching import - game.WaitForReady(() => game, _ => Task.Run(() => game.Import(stream, filename))); + game.WaitForReady(() => game, _ => Task.Run(() => game.Import(stream, filename))); } + base.OnNewIntent(intent); } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 7b59726cd0..f4208671d7 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -251,8 +251,6 @@ namespace osu.Game.Database notification.State = ProgressNotificationState.Cancelled; Logger.Error(e, $@"Could not import ({filename})", LoggingTarget.Database); } - - return; } /// From 280abdc473e8bd5e512c1456b906d757e98d6345 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 5 Dec 2020 11:50:08 +0100 Subject: [PATCH 5124/6909] Use ContentResolver for getting filename. --- osu.Android/OsuGameActivity.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 2b25c37d58..917a39148d 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.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.Linq; using System.Threading.Tasks; using Android.App; using Android.Content; using Android.Content.PM; using Android.OS; +using Android.Provider; using Android.Views; using osu.Framework.Android; @@ -39,11 +39,14 @@ namespace osu.Android { if (intent.Action == Intent.ActionView) { - var filename = intent.Data.Path.Split('/').Last(); + var cursor = ContentResolver.Query(intent.Data, null, null, null); + var filename_column = cursor.GetColumnIndex(OpenableColumns.DisplayName); + cursor.MoveToFirst(); + var stream = ContentResolver.OpenInputStream(intent.Data); if (stream != null) // intent handler may run before the game has even loaded so we need to wait for the file importers to load before launching import - game.WaitForReady(() => game, _ => Task.Run(() => game.Import(stream, filename))); + game.WaitForReady(() => game, _ => Task.Run(() => game.Import(stream, cursor.GetString(filename_column)))); } base.OnNewIntent(intent); From 0a745144e398c897d31293e12b0e0cc1befd9545 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 5 Dec 2020 13:11:52 +0100 Subject: [PATCH 5125/6909] Disable confine mode dropdown when full-screen After hard-locking the mouse confine mode to `Always` in full-screen to prevent confine issues from popping up, the confine mode dropdown in settings had confusing UX due to seemingly having no effect when full-screen. --- osu.Game/Input/ConfineMouseTracker.cs | 31 ++++++++++++++++++--------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index 75d9c8debb..739bfaadab 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -28,27 +28,38 @@ namespace osu.Game.Input { frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); frameworkWindowMode = frameworkConfigManager.GetBindable(FrameworkSetting.WindowMode); - frameworkWindowMode.BindValueChanged(_ => updateConfineMode()); + frameworkWindowMode.BindValueChanged(_ => updateGameConfineMode()); osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); - osuConfineMode.ValueChanged += _ => updateConfineMode(); - localUserPlaying.BindValueChanged(_ => updateConfineMode(), true); + osuConfineMode.ValueChanged += _ => updateFrameworkConfineMode(); + localUserPlaying.BindValueChanged(_ => updateFrameworkConfineMode(), true); } - private void updateConfineMode() + private LeasedBindable leasedOsuConfineMode; + + private void updateGameConfineMode() + { + if (frameworkWindowMode.Value == WindowMode.Fullscreen && leasedOsuConfineMode == null) + { + leasedOsuConfineMode = osuConfineMode.BeginLease(true); + leasedOsuConfineMode.Value = OsuConfineMouseMode.Always; + } + + if (frameworkWindowMode.Value != WindowMode.Fullscreen && leasedOsuConfineMode != null) + { + leasedOsuConfineMode.Return(); + leasedOsuConfineMode = null; + } + } + + private void updateFrameworkConfineMode() { // confine mode is unavailable on some platforms if (frameworkConfineMode.Disabled) return; - if (frameworkWindowMode.Value == WindowMode.Fullscreen) - { - frameworkConfineMode.Value = ConfineMouseMode.Fullscreen; - return; - } - switch (osuConfineMode.Value) { case OsuConfineMouseMode.Never: From 15ce7bacf1fcc319916f9365643f5ef1e73f26d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 5 Dec 2020 14:12:15 +0100 Subject: [PATCH 5126/6909] Add test coverage for confine functionality Due to growing levels of complexity around confine logic. --- .../Input/ConfineMouseTrackerTest.cs | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 osu.Game.Tests/Input/ConfineMouseTrackerTest.cs diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs new file mode 100644 index 0000000000..42f5209643 --- /dev/null +++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Configuration; +using osu.Framework.Input; +using osu.Framework.Testing; +using osu.Game.Configuration; +using osu.Game.Input; +using osu.Game.Tests.Visual.Navigation; + +namespace osu.Game.Tests.Input +{ + [HeadlessTest] + public class ConfineMouseTrackerTest : OsuGameTestScene + { + [Resolved] + private FrameworkConfigManager frameworkConfigManager { get; set; } + + [Resolved] + private OsuConfigManager osuConfigManager { get; set; } + + [TestCase(WindowMode.Windowed)] + [TestCase(WindowMode.Borderless)] + public void TestDisableConfining(WindowMode windowMode) + { + setWindowModeTo(windowMode); + setGameSideModeTo(OsuConfineMouseMode.Never); + + gameSideConfineModeDisabled(false); + + setLocalUserPlayingTo(false); + frameworkSideModeIs(ConfineMouseMode.Never); + + setLocalUserPlayingTo(true); + frameworkSideModeIs(ConfineMouseMode.Never); + } + + [TestCase(WindowMode.Windowed)] + [TestCase(WindowMode.Borderless)] + public void TestConfiningDuringGameplay(WindowMode windowMode) + { + setWindowModeTo(windowMode); + setGameSideModeTo(OsuConfineMouseMode.DuringGameplay); + + gameSideConfineModeDisabled(false); + + setLocalUserPlayingTo(false); + frameworkSideModeIs(ConfineMouseMode.Never); + + setLocalUserPlayingTo(true); + frameworkSideModeIs(ConfineMouseMode.Always); + } + + [TestCase(WindowMode.Windowed)] + [TestCase(WindowMode.Borderless)] + public void TestConfineAlwaysUserSetting(WindowMode windowMode) + { + setWindowModeTo(windowMode); + setGameSideModeTo(OsuConfineMouseMode.Always); + + gameSideConfineModeDisabled(false); + + setLocalUserPlayingTo(false); + frameworkSideModeIs(ConfineMouseMode.Always); + + setLocalUserPlayingTo(true); + frameworkSideModeIs(ConfineMouseMode.Always); + } + + [Test] + public void TestConfineAlwaysInFullscreen() + { + setGameSideModeTo(OsuConfineMouseMode.Never); + + setWindowModeTo(WindowMode.Fullscreen); + gameSideConfineModeDisabled(true); + + setLocalUserPlayingTo(false); + frameworkSideModeIs(ConfineMouseMode.Always); + + setLocalUserPlayingTo(true); + frameworkSideModeIs(ConfineMouseMode.Always); + + setWindowModeTo(WindowMode.Windowed); + + // old state is restored + gameSideModeIs(OsuConfineMouseMode.Never); + frameworkSideModeIs(ConfineMouseMode.Never); + gameSideConfineModeDisabled(false); + } + + private void setWindowModeTo(WindowMode mode) + // needs to go through .GetBindable().Value instead of .Set() due to default overrides + => AddStep($"make window {mode}", () => frameworkConfigManager.GetBindable(FrameworkSetting.WindowMode).Value = mode); + + private void setGameSideModeTo(OsuConfineMouseMode mode) + => AddStep($"set {mode} game-side", () => Game.LocalConfig.Set(OsuSetting.ConfineMouseMode, mode)); + + private void setLocalUserPlayingTo(bool playing) + => AddStep($"local user {(playing ? "playing" : "not playing")}", () => Game.LocalUserPlaying.Value = playing); + + private void gameSideModeIs(OsuConfineMouseMode mode) + => AddAssert($"mode is {mode} game-side", () => Game.LocalConfig.Get(OsuSetting.ConfineMouseMode) == mode); + + private void frameworkSideModeIs(ConfineMouseMode mode) + => AddAssert($"mode is {mode} framework-side", () => frameworkConfigManager.Get(FrameworkSetting.ConfineMouseMode) == mode); + + private void gameSideConfineModeDisabled(bool disabled) + => AddAssert($"game-side confine mode {(disabled ? "disabled" : "enabled")}", + () => Game.LocalConfig.GetBindable(OsuSetting.ConfineMouseMode).Disabled == disabled); + } +} From 0266410368a9ed8c62265ac67a21f8ef7e7d6593 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 5 Dec 2020 18:30:40 +0100 Subject: [PATCH 5127/6909] Allow importing files through the android share sheet. --- osu.Android/OsuGameActivity.cs | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 917a39148d..b331f3d734 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -5,6 +5,7 @@ using System.Threading.Tasks; using Android.App; using Android.Content; using Android.Content.PM; +using Android.Net; using Android.OS; using Android.Provider; using Android.Views; @@ -13,7 +14,7 @@ using osu.Framework.Android; namespace osu.Android { [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] - [IntentFilter(new[] { Intent.ActionDefault }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] + [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] public class OsuGameActivity : AndroidGameActivity { private OsuGameAndroid game; @@ -37,19 +38,29 @@ namespace osu.Android protected override void OnNewIntent(Intent intent) { - if (intent.Action == Intent.ActionView) + if (intent.Action == Intent.ActionDefault) { - var cursor = ContentResolver.Query(intent.Data, null, null, null); - var filename_column = cursor.GetColumnIndex(OpenableColumns.DisplayName); - cursor.MoveToFirst(); - - var stream = ContentResolver.OpenInputStream(intent.Data); - if (stream != null) - // intent handler may run before the game has even loaded so we need to wait for the file importers to load before launching import - game.WaitForReady(() => game, _ => Task.Run(() => game.Import(stream, cursor.GetString(filename_column)))); + if (intent.Scheme == ContentResolver.SchemeContent) + handleImportFromUri(intent.Data); } - base.OnNewIntent(intent); + if (intent.Action == Intent.ActionSend) + { + var content = intent.ClipData.GetItemAt(0); + handleImportFromUri(content.Uri); + } + } + + private void handleImportFromUri(Uri uri) + { + var cursor = ContentResolver.Query(uri, null, null, null); + var filename_column = cursor.GetColumnIndex(OpenableColumns.DisplayName); + cursor.MoveToFirst(); + + var stream = ContentResolver.OpenInputStream(uri); + if (stream != null) + // intent handler may run before the game has even loaded so we need to wait for the file importers to load before launching import + game.WaitForReady(() => game, _ => Task.Run(() => game.Import(stream, cursor.GetString(filename_column)))); } } } From 08f23cc4249694f0a86aecf6eac545b5027e1796 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 5 Dec 2020 22:07:39 +0100 Subject: [PATCH 5128/6909] Revert leasing logic --- .../Input/ConfineMouseTrackerTest.cs | 16 ++-------- osu.Game/Input/ConfineMouseTracker.cs | 31 ++++++------------- 2 files changed, 12 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs index 42f5209643..b90382488f 100644 --- a/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs +++ b/osu.Game.Tests/Input/ConfineMouseTrackerTest.cs @@ -28,8 +28,6 @@ namespace osu.Game.Tests.Input setWindowModeTo(windowMode); setGameSideModeTo(OsuConfineMouseMode.Never); - gameSideConfineModeDisabled(false); - setLocalUserPlayingTo(false); frameworkSideModeIs(ConfineMouseMode.Never); @@ -44,8 +42,6 @@ namespace osu.Game.Tests.Input setWindowModeTo(windowMode); setGameSideModeTo(OsuConfineMouseMode.DuringGameplay); - gameSideConfineModeDisabled(false); - setLocalUserPlayingTo(false); frameworkSideModeIs(ConfineMouseMode.Never); @@ -60,8 +56,6 @@ namespace osu.Game.Tests.Input setWindowModeTo(windowMode); setGameSideModeTo(OsuConfineMouseMode.Always); - gameSideConfineModeDisabled(false); - setLocalUserPlayingTo(false); frameworkSideModeIs(ConfineMouseMode.Always); @@ -75,20 +69,18 @@ namespace osu.Game.Tests.Input setGameSideModeTo(OsuConfineMouseMode.Never); setWindowModeTo(WindowMode.Fullscreen); - gameSideConfineModeDisabled(true); setLocalUserPlayingTo(false); - frameworkSideModeIs(ConfineMouseMode.Always); + frameworkSideModeIs(ConfineMouseMode.Fullscreen); setLocalUserPlayingTo(true); - frameworkSideModeIs(ConfineMouseMode.Always); + frameworkSideModeIs(ConfineMouseMode.Fullscreen); setWindowModeTo(WindowMode.Windowed); // old state is restored gameSideModeIs(OsuConfineMouseMode.Never); frameworkSideModeIs(ConfineMouseMode.Never); - gameSideConfineModeDisabled(false); } private void setWindowModeTo(WindowMode mode) @@ -106,9 +98,5 @@ namespace osu.Game.Tests.Input private void frameworkSideModeIs(ConfineMouseMode mode) => AddAssert($"mode is {mode} framework-side", () => frameworkConfigManager.Get(FrameworkSetting.ConfineMouseMode) == mode); - - private void gameSideConfineModeDisabled(bool disabled) - => AddAssert($"game-side confine mode {(disabled ? "disabled" : "enabled")}", - () => Game.LocalConfig.GetBindable(OsuSetting.ConfineMouseMode).Disabled == disabled); } } diff --git a/osu.Game/Input/ConfineMouseTracker.cs b/osu.Game/Input/ConfineMouseTracker.cs index 739bfaadab..75d9c8debb 100644 --- a/osu.Game/Input/ConfineMouseTracker.cs +++ b/osu.Game/Input/ConfineMouseTracker.cs @@ -28,38 +28,27 @@ namespace osu.Game.Input { frameworkConfineMode = frameworkConfigManager.GetBindable(FrameworkSetting.ConfineMouseMode); frameworkWindowMode = frameworkConfigManager.GetBindable(FrameworkSetting.WindowMode); - frameworkWindowMode.BindValueChanged(_ => updateGameConfineMode()); + frameworkWindowMode.BindValueChanged(_ => updateConfineMode()); osuConfineMode = osuConfigManager.GetBindable(OsuSetting.ConfineMouseMode); localUserPlaying = game.LocalUserPlaying.GetBoundCopy(); - osuConfineMode.ValueChanged += _ => updateFrameworkConfineMode(); - localUserPlaying.BindValueChanged(_ => updateFrameworkConfineMode(), true); + osuConfineMode.ValueChanged += _ => updateConfineMode(); + localUserPlaying.BindValueChanged(_ => updateConfineMode(), true); } - private LeasedBindable leasedOsuConfineMode; - - private void updateGameConfineMode() - { - if (frameworkWindowMode.Value == WindowMode.Fullscreen && leasedOsuConfineMode == null) - { - leasedOsuConfineMode = osuConfineMode.BeginLease(true); - leasedOsuConfineMode.Value = OsuConfineMouseMode.Always; - } - - if (frameworkWindowMode.Value != WindowMode.Fullscreen && leasedOsuConfineMode != null) - { - leasedOsuConfineMode.Return(); - leasedOsuConfineMode = null; - } - } - - private void updateFrameworkConfineMode() + private void updateConfineMode() { // confine mode is unavailable on some platforms if (frameworkConfineMode.Disabled) return; + if (frameworkWindowMode.Value == WindowMode.Fullscreen) + { + frameworkConfineMode.Value = ConfineMouseMode.Fullscreen; + return; + } + switch (osuConfineMode.Value) { case OsuConfineMouseMode.Never: From 71edada623d017be357d397f495d87ca6babd933 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 5 Dec 2020 22:13:51 +0100 Subject: [PATCH 5129/6909] Hide confine setting entirely in fullscreen --- .../Overlays/Settings/Sections/Input/MouseSettings.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index b54ad9a641..ec0ce08004 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -20,6 +20,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input private Bindable sensitivityBindable = new BindableDouble(); private Bindable ignoredInputHandlers; + private Bindable windowMode; + private SettingsEnumDropdown confineMouseModeSetting; + [BackgroundDependencyLoader] private void load(OsuConfigManager osuConfig, FrameworkConfigManager config) { @@ -30,6 +33,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input configSensitivity.BindValueChanged(val => sensitivityBindable.Value = val.NewValue); sensitivityBindable.BindValueChanged(val => configSensitivity.Value = val.NewValue); + windowMode = config.GetBindable(FrameworkSetting.WindowMode); + windowMode.BindValueChanged(mode => confineMouseModeSetting.Alpha = mode.NewValue == WindowMode.Fullscreen ? 0 : 1); + Children = new Drawable[] { new SettingsCheckbox @@ -47,7 +53,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input LabelText = "Map absolute input to window", Current = config.GetBindable(FrameworkSetting.MapAbsoluteInputToWindow) }, - new SettingsEnumDropdown + confineMouseModeSetting = new SettingsEnumDropdown { LabelText = "Confine mouse cursor to window", Current = osuConfig.GetBindable(OsuSetting.ConfineMouseMode) From 2ea8b105d51a42491b7663c2f5b6c01d6a467843 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 5 Dec 2020 20:42:07 +0100 Subject: [PATCH 5130/6909] Apply review suggestions --- osu.Android/OsuGameActivity.cs | 11 ++++------- osu.Android/OsuGameAndroid.cs | 9 +++++++++ osu.Game/OsuGame.cs | 2 +- 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index b331f3d734..a56206c969 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Threading.Tasks; using Android.App; using Android.Content; using Android.Content.PM; @@ -38,10 +37,9 @@ namespace osu.Android protected override void OnNewIntent(Intent intent) { - if (intent.Action == Intent.ActionDefault) + if (intent.Action == Intent.ActionDefault && intent.Scheme == ContentResolver.SchemeContent) { - if (intent.Scheme == ContentResolver.SchemeContent) - handleImportFromUri(intent.Data); + handleImportFromUri(intent.Data); } if (intent.Action == Intent.ActionSend) @@ -53,14 +51,13 @@ namespace osu.Android private void handleImportFromUri(Uri uri) { - var cursor = ContentResolver.Query(uri, null, null, null); + var cursor = ContentResolver.Query(uri, new[] { OpenableColumns.DisplayName }, null, null); var filename_column = cursor.GetColumnIndex(OpenableColumns.DisplayName); cursor.MoveToFirst(); var stream = ContentResolver.OpenInputStream(uri); if (stream != null) - // intent handler may run before the game has even loaded so we need to wait for the file importers to load before launching import - game.WaitForReady(() => game, _ => Task.Run(() => game.Import(stream, cursor.GetString(filename_column)))); + game.ScheduleImport(stream, cursor.GetString(filename_column)); } } } diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 21d6336b2c..81945ee083 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.IO; +using System.Threading.Tasks; using Android.App; using Android.OS; using osu.Framework.Allocation; @@ -65,6 +67,13 @@ namespace osu.Android } } + /// + /// Schedules a file to be imported once the game is loaded. + /// + /// A stream to the file to import. + /// The filename of the file to import. + public void ScheduleImport(Stream stream, string filename) => WaitForReady(() => this, _ => Task.Run(() => Import(stream, filename))); + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5b6b90fba5..2c1db67d24 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -483,7 +483,7 @@ namespace osu.Game /// A function to retrieve a (potentially not-yet-constructed) target instance. /// The action to perform on the instance when load is confirmed. /// The type of the target instance. - public void WaitForReady(Func retrieveInstance, Action action) + protected void WaitForReady(Func retrieveInstance, Action action) where T : Drawable { var instance = retrieveInstance(); From f6d15b975704c8358fa4eda4c8cbec19597aa252 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 6 Dec 2020 18:59:38 +0100 Subject: [PATCH 5131/6909] Invert back-to-front logic --- osu.Game/Skinning/PoolableSkinnableSample.cs | 2 +- osu.Game/Skinning/SkinnableSound.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index a59fcfea27..cc6b85a13e 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -102,7 +102,7 @@ namespace osu.Game.Skinning sampleContainer.Add(Sample = new DrawableSample(ch) { Looping = Looping }); // Start playback internally for the new sample if the previous one was playing beforehand. - if (wasPlaying && !Looping) + if (wasPlaying && Looping) Play(); } diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 166f005473..645c08cd00 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -159,7 +159,7 @@ namespace osu.Game.Skinning samplesContainer.Add(sample); } - if (wasPlaying && !Looping) + if (wasPlaying && Looping) Play(); } From 0f9b38da081bc95eb7bdcc393df928f19457f036 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 6 Dec 2020 11:35:14 -0800 Subject: [PATCH 5132/6909] Add fade in/out animations to mod settings container --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- osu.Game/Overlays/Mods/ModSettingsContainer.cs | 14 +++++++++++++- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 31adf47456..12da718ab2 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Mods { Width = 180, Text = "Customisation", - Action = () => ModSettingsContainer.Alpha = ModSettingsContainer.Alpha == 1 ? 0 : 1, + Action = () => ModSettingsContainer.ToggleVisibility(), Enabled = { Value = false }, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, @@ -430,7 +430,7 @@ namespace osu.Game.Overlays.Mods DeselectTypes(selectedMod.IncompatibleMods, true); - if (selectedMod.RequiresConfiguration) ModSettingsContainer.Alpha = 1; + if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show(); } else { diff --git a/osu.Game/Overlays/Mods/ModSettingsContainer.cs b/osu.Game/Overlays/Mods/ModSettingsContainer.cs index b185b56ecd..a1d00f91b4 100644 --- a/osu.Game/Overlays/Mods/ModSettingsContainer.cs +++ b/osu.Game/Overlays/Mods/ModSettingsContainer.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays.Mods { - public class ModSettingsContainer : Container + public class ModSettingsContainer : VisibilityContainer { public readonly IBindable> SelectedMods = new Bindable>(Array.Empty()); @@ -27,6 +27,8 @@ namespace osu.Game.Overlays.Mods private readonly FillFlowContainer modSettingsContent; + private const double transition_duration = 200; + public ModSettingsContainer() { Children = new Drawable[] @@ -80,5 +82,15 @@ namespace osu.Game.Overlays.Mods protected override bool OnMouseDown(MouseDownEvent e) => true; protected override bool OnHover(HoverEvent e) => true; + + protected override void PopIn() + { + this.FadeIn(transition_duration, Easing.OutQuint); + } + + protected override void PopOut() + { + this.FadeOut(transition_duration, Easing.OutQuint); + } } } From 15e0ea332b85ddd23823909f1fba32d5e21829a6 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 6 Dec 2020 13:18:19 -0800 Subject: [PATCH 5133/6909] Check state instead of alpha in tests --- .../Visual/UserInterface/TestSceneModSettings.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index 645b83758c..8614700b15 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("button enabled", () => modSelect.CustomiseButton.Enabled.Value); AddStep("open Customisation", () => modSelect.CustomiseButton.Click()); AddStep("deselect mod", () => modSelect.SelectMod(testCustomisableMod)); - AddAssert("controls hidden", () => modSelect.ModSettingsContainer.Alpha == 0); + AddAssert("controls hidden", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden); } [Test] @@ -72,11 +72,11 @@ namespace osu.Game.Tests.Visual.UserInterface createModSelect(); openModSelect(); - AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.Alpha == 0); + AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden); AddStep("select mod", () => modSelect.SelectMod(testCustomisableAutoOpenMod)); - AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.Alpha == 1); + AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.State.Value == Visibility.Visible); AddStep("deselect mod", () => modSelect.SelectMod(testCustomisableAutoOpenMod)); - AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.Alpha == 0); + AddAssert("Customisation closed", () => modSelect.ModSettingsContainer.State.Value == Visibility.Hidden); } [Test] @@ -123,7 +123,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("change mod settings menu width to full screen", () => modSelect.SetModSettingsWidth(1.0f)); AddStep("select cm2", () => modSelect.SelectMod(testCustomisableAutoOpenMod)); - AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.Alpha == 1); + AddAssert("Customisation opened", () => modSelect.ModSettingsContainer.State.Value == Visibility.Visible); AddStep("hover over mod behind settings menu", () => InputManager.MoveMouseTo(modSelect.GetModButton(testCustomisableMod))); AddAssert("Mod is not considered hovered over", () => !modSelect.GetModButton(testCustomisableMod).IsHovered); AddStep("left click mod", () => InputManager.Click(MouseButton.Left)); @@ -153,7 +153,7 @@ namespace osu.Game.Tests.Visual.UserInterface private class TestModSelectOverlay : ModSelectOverlay { - public new Container ModSettingsContainer => base.ModSettingsContainer; + public new VisibilityContainer ModSettingsContainer => base.ModSettingsContainer; public new TriangleButton CustomiseButton => base.CustomiseButton; public bool ButtonsLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded); From cf3fbe0b0b832ef2476e46eb600f086b385005b9 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Sun, 6 Dec 2020 18:06:36 -0600 Subject: [PATCH 5134/6909] osuDifficulty ar11 nerf --- .../Difficulty/OsuPerformanceCalculator.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 063cde8747..3f0b0dcc71 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -99,16 +99,13 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (Attributes.MaxCombo > 0) aimValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); - double approachRateFactor = 1.0; - + double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) - approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33); + approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33); else if (Attributes.ApproachRate < 8.0) - { - approachRateFactor += 0.01 * (8.0 - Attributes.ApproachRate); - } + approachRateFactor += 0.1 * (8.0 - Attributes.ApproachRate); - aimValue *= approachRateFactor; + aimValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0)); // We want to give more reward for lower AR when it comes to aim and HD. This nerfs high AR and buffs lower AR. if (mods.Any(h => h is OsuModHidden)) @@ -137,8 +134,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty double speedValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.SpeedStrain / 0.0675) - 4.0, 3.0) / 100000.0; // Longer maps are worth more - speedValue *= 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + - (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); + double lengthBonus = 0.95 + 0.4 * Math.Min(1.0, totalHits / 2000.0) + + (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); + speedValue *= lengthBonus; // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available speedValue *= Math.Pow(0.97, countMiss); @@ -147,11 +145,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (Attributes.MaxCombo > 0) speedValue *= Math.Min(Math.Pow(scoreMaxCombo, 0.8) / Math.Pow(Attributes.MaxCombo, 0.8), 1.0); - double approachRateFactor = 1.0; + double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) - approachRateFactor += 0.3 * (Attributes.ApproachRate - 10.33); + approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33); - speedValue *= approachRateFactor; + speedValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0)); if (mods.Any(m => m is OsuModHidden)) speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); From c905df8a770a72294401952b5120680ec004b92c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 12:26:58 +0900 Subject: [PATCH 5135/6909] Move LegacySkinTransformer --- .../Skinning/{ => Legacy}/OsuLegacySkinTransformer.cs | 1 - 1 file changed, 1 deletion(-) rename osu.Game.Rulesets.Osu/Skinning/{ => Legacy}/OsuLegacySkinTransformer.cs (99%) diff --git a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs similarity index 99% rename from osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs rename to osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 70abfa1ac9..78bc26eff7 100644 --- a/osu.Game.Rulesets.Osu/Skinning/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -4,7 +4,6 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Skinning; using osuTK; From 0d88ff340489ef14e1ecc8dc6c52dd0c92848770 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 12:27:12 +0900 Subject: [PATCH 5136/6909] Adjust namespace --- osu.Game.Rulesets.Osu/OsuRuleset.cs | 2 +- .../Skinning/Legacy/OsuLegacySkinTransformer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index d8180b0e58..cba0c5be14 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -24,13 +24,13 @@ using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Difficulty; using osu.Game.Rulesets.Osu.Scoring; -using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Skinning; using System; using System.Linq; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Statistics; using osu.Game.Screens.Ranking.Statistics; diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs index 78bc26eff7..d74f885573 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/OsuLegacySkinTransformer.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Osu.Skinning +namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public class OsuLegacySkinTransformer : LegacySkinTransformer { From f78bd7c7399435d12d47b672ec444979b3c376e4 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 12:29:14 +0900 Subject: [PATCH 5137/6909] Move piece files of Taiko ruleset --- .../Drawables/Pieces => Skinning/Default}/CentreHitCirclePiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/CirclePiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/ElongatedCirclePiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/RimHitCirclePiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/SwellSymbolPiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/TickPiece.cs | 0 osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyBarLine.cs | 0 .../Skinning/{ => Legacy}/LegacyCirclePiece.cs | 0 osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyDrumRoll.cs | 0 osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyHit.cs | 0 .../Skinning/{ => Legacy}/LegacyHitExplosion.cs | 0 osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyInputDrum.cs | 0 .../Skinning/{ => Legacy}/LegacyTaikoScroller.cs | 0 .../Skinning/{ => Legacy}/TaikoLegacyHitTarget.cs | 0 .../Skinning/{ => Legacy}/TaikoLegacyPlayfieldBackgroundRight.cs | 0 .../Skinning/{ => Legacy}/TaikoLegacySkinTransformer.cs | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Taiko/{Objects/Drawables/Pieces => Skinning/Default}/CentreHitCirclePiece.cs (100%) rename osu.Game.Rulesets.Taiko/{Objects/Drawables/Pieces => Skinning/Default}/CirclePiece.cs (100%) rename osu.Game.Rulesets.Taiko/{Objects/Drawables/Pieces => Skinning/Default}/ElongatedCirclePiece.cs (100%) rename osu.Game.Rulesets.Taiko/{Objects/Drawables/Pieces => Skinning/Default}/RimHitCirclePiece.cs (100%) rename osu.Game.Rulesets.Taiko/{Objects/Drawables/Pieces => Skinning/Default}/SwellSymbolPiece.cs (100%) rename osu.Game.Rulesets.Taiko/{Objects/Drawables/Pieces => Skinning/Default}/TickPiece.cs (100%) rename osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyBarLine.cs (100%) rename osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyCirclePiece.cs (100%) rename osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyDrumRoll.cs (100%) rename osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyHit.cs (100%) rename osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyHitExplosion.cs (100%) rename osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyInputDrum.cs (100%) rename osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/LegacyTaikoScroller.cs (100%) rename osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/TaikoLegacyHitTarget.cs (100%) rename osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/TaikoLegacyPlayfieldBackgroundRight.cs (100%) rename osu.Game.Rulesets.Taiko/Skinning/{ => Legacy}/TaikoLegacySkinTransformer.cs (100%) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CentreHitCirclePiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/CirclePiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/ElongatedCirclePiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/RimHitCirclePiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/SwellSymbolPiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Objects/Drawables/Pieces/TickPiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyBarLine.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/LegacyBarLine.cs rename to osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyBarLine.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/LegacyCirclePiece.cs rename to osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/LegacyDrumRoll.cs rename to osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/LegacyHit.cs rename to osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/LegacyHitExplosion.cs rename to osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/LegacyInputDrum.cs rename to osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/LegacyTaikoScroller.cs rename to osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyHitTarget.cs rename to osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/TaikoLegacyPlayfieldBackgroundRight.cs rename to osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs diff --git a/osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs similarity index 100% rename from osu.Game.Rulesets.Taiko/Skinning/TaikoLegacySkinTransformer.cs rename to osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs From c70b13ecc2f0a0438d26853a09b138b0dc34e76c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 12:30:25 +0900 Subject: [PATCH 5138/6909] Adjust namespace --- .../Skinning/TestSceneTaikoScroller.cs | 2 +- .../Objects/Drawables/DrawableDrumRoll.cs | 2 +- .../Objects/Drawables/DrawableDrumRollTick.cs | 2 +- .../Objects/Drawables/DrawableHit.cs | 2 +- .../Objects/Drawables/DrawableSwell.cs | 2 +- .../Objects/Drawables/DrawableSwellTick.cs | 2 +- .../Skinning/Default/CentreHitCirclePiece.cs | 4 ++-- .../Skinning/Default/CirclePiece.cs | 10 +++++----- .../Skinning/Default/ElongatedCirclePiece.cs | 2 +- .../Skinning/Default/RimHitCirclePiece.cs | 2 +- .../Skinning/Default/SwellSymbolPiece.cs | 4 ++-- osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs | 4 ++-- .../Skinning/Legacy/LegacyBarLine.cs | 2 +- .../Skinning/Legacy/LegacyCirclePiece.cs | 2 +- .../Skinning/Legacy/LegacyDrumRoll.cs | 2 +- osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs | 2 +- .../Skinning/Legacy/LegacyHitExplosion.cs | 2 +- .../Skinning/Legacy/LegacyInputDrum.cs | 2 +- .../Skinning/Legacy/LegacyTaikoScroller.cs | 2 +- .../Skinning/Legacy/TaikoLegacyHitTarget.cs | 2 +- .../Legacy/TaikoLegacyPlayfieldBackgroundRight.cs | 2 +- .../Skinning/Legacy/TaikoLegacySkinTransformer.cs | 2 +- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 2 +- 23 files changed, 30 insertions(+), 30 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs index 114038b81c..4ae3cbd418 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneTaikoScroller.cs @@ -6,7 +6,7 @@ using osu.Framework.Testing; using osu.Framework.Timing; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Rulesets.Taiko.Skinning; +using osu.Game.Rulesets.Taiko.Skinning.Legacy; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Tests.Skinning diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index c596fa2c7c..4ead4982a1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -8,12 +8,12 @@ using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osuTK.Graphics; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; +using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; using osuTK; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index be659f6ca5..e68e40ae1c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -4,7 +4,7 @@ using System; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 29a96a7a40..d1751d8a75 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -11,7 +11,7 @@ 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; +using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index ff0a27023d..5c6278ed08 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -13,7 +13,7 @@ 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; +using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 6202583494..14c86d151f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs index 0509841ba8..f65bb54726 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CentreHitCirclePiece.cs @@ -4,11 +4,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osuTK; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; +using osuTK; -namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Taiko.Skinning.Default { public class CentreHitCirclePiece : CirclePiece { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs index f515a35c18..8ca996159b 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/CirclePiece.cs @@ -5,15 +5,15 @@ using osu.Framework.Audio.Track; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.Backgrounds; -using osuTK.Graphics; -using osu.Game.Beatmaps.ControlPoints; using osu.Framework.Graphics.Effects; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; +using osuTK.Graphics; -namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Taiko.Skinning.Default { /// /// A circle piece which is used uniformly through osu!taiko to visualise hitobjects. diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs index 034ab6dd21..210841bca0 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/ElongatedCirclePiece.cs @@ -5,7 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; -namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Taiko.Skinning.Default { public class ElongatedCirclePiece : CirclePiece { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs index 3273ab7fa7..ca2ab301be 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/RimHitCirclePiece.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Taiko.Skinning.Default { public class RimHitCirclePiece : CirclePiece { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs index a8f9f0b94d..2f59cac3ff 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/SwellSymbolPiece.cs @@ -2,13 +2,13 @@ // 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; +using osuTK; -namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Taiko.Skinning.Default { public class SwellCirclePiece : CirclePiece { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs index 0648bcebcd..09c8243aac 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Default/TickPiece.cs @@ -3,11 +3,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osuTK; using osuTK.Graphics; -using osu.Framework.Graphics.Shapes; -namespace osu.Game.Rulesets.Taiko.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Taiko.Skinning.Default { public class TickPiece : CompositeDrawable { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyBarLine.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyBarLine.cs index 7d08a21ab1..2b528ae8ce 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyBarLine.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Taiko.Skinning +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class LegacyBarLine : Sprite { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs index 9b73ccd248..821ddc3c04 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs @@ -12,7 +12,7 @@ using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Taiko.Skinning +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class LegacyCirclePiece : CompositeDrawable, IHasAccentColour { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs index 5ab8e3a8c8..ea6f813be8 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyDrumRoll.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics; using osu.Game.Skinning; using osuTK.Graphics; -namespace osu.Game.Rulesets.Taiko.Skinning +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class LegacyDrumRoll : CompositeDrawable, IHasAccentColour { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs index b11b64c22c..d93317f0e2 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHit.cs @@ -5,7 +5,7 @@ using osu.Framework.Allocation; using osu.Game.Skinning; using osuTK.Graphics; -namespace osu.Game.Rulesets.Taiko.Skinning +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class LegacyHit : LegacyCirclePiece { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs index 19493271be..651cdd6438 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyHitExplosion.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects.Drawables; -namespace osu.Game.Rulesets.Taiko.Skinning +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class LegacyHitExplosion : CompositeDrawable { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs index b7b55b9ae0..795885d4b9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyInputDrum.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Taiko.Audio; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Taiko.Skinning +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { /// /// A component of the playfield that captures input and displays input as a drum. diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs index e029040ef3..eb92097204 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyTaikoScroller.cs @@ -13,7 +13,7 @@ using osu.Game.Screens.Play; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Taiko.Skinning +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class LegacyTaikoScroller : CompositeDrawable { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs index e522fb7c10..9feb2054da 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyHitTarget.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Taiko.UI; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Taiko.Skinning +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class TaikoLegacyHitTarget : CompositeDrawable { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs index 4bbb6be6b1..02756d57a4 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacyPlayfieldBackgroundRight.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Taiko.Skinning +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class TaikoLegacyPlayfieldBackgroundRight : BeatSyncedContainer { diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 96fb065e79..d8e3100048 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.UI; using osu.Game.Skinning; -namespace osu.Game.Rulesets.Taiko.Skinning +namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { public class TaikoLegacySkinTransformer : LegacySkinTransformer { diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index 2a49dd655c..f2b5d195b4 100644 --- a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs @@ -25,7 +25,7 @@ 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.Rulesets.Taiko.Skinning.Legacy; using osu.Game.Screens.Ranking.Statistics; using osu.Game.Skinning; From 40da799103b049070a18c5dccaa9f9f58da53ab1 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 12:31:56 +0900 Subject: [PATCH 5139/6909] Move piece files of Mania ruleset --- .../Drawables/Pieces => Skinning/Default}/DefaultBodyPiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/DefaultNotePiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/IHoldNoteBody.cs | 0 .../Skinning/{ => Legacy}/HitTargetInsetContainer.cs | 0 osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyBodyPiece.cs | 0 .../Skinning/{ => Legacy}/LegacyColumnBackground.cs | 0 .../Skinning/{ => Legacy}/LegacyHitExplosion.cs | 0 osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyHitTarget.cs | 0 .../Skinning/{ => Legacy}/LegacyHoldNoteHeadPiece.cs | 0 .../Skinning/{ => Legacy}/LegacyHoldNoteTailPiece.cs | 0 osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyKeyArea.cs | 0 .../Skinning/{ => Legacy}/LegacyManiaColumnElement.cs | 0 osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyNotePiece.cs | 0 .../Skinning/{ => Legacy}/LegacyStageBackground.cs | 0 .../Skinning/{ => Legacy}/LegacyStageForeground.cs | 0 .../Skinning/{ => Legacy}/ManiaLegacySkinTransformer.cs | 0 16 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Mania/{Objects/Drawables/Pieces => Skinning/Default}/DefaultBodyPiece.cs (100%) rename osu.Game.Rulesets.Mania/{Objects/Drawables/Pieces => Skinning/Default}/DefaultNotePiece.cs (100%) rename osu.Game.Rulesets.Mania/{Objects/Drawables/Pieces => Skinning/Default}/IHoldNoteBody.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/HitTargetInsetContainer.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyBodyPiece.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyColumnBackground.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyHitExplosion.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyHitTarget.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyHoldNoteHeadPiece.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyHoldNoteTailPiece.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyKeyArea.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyManiaColumnElement.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyNotePiece.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyStageBackground.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/LegacyStageForeground.cs (100%) rename osu.Game.Rulesets.Mania/Skinning/{ => Legacy}/ManiaLegacySkinTransformer.cs (100%) diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBodyPiece.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultBodyPiece.cs rename to osu.Game.Rulesets.Mania/Skinning/Default/DefaultBodyPiece.cs diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultNotePiece.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/DefaultNotePiece.cs rename to osu.Game.Rulesets.Mania/Skinning/Default/DefaultNotePiece.cs diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Skinning/Default/IHoldNoteBody.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Objects/Drawables/Pieces/IHoldNoteBody.cs rename to osu.Game.Rulesets.Mania/Skinning/Default/IHoldNoteBody.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/HitTargetInsetContainer.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/HitTargetInsetContainer.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/HitTargetInsetContainer.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyBodyPiece.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyColumnBackground.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyHitExplosion.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitTarget.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyHitTarget.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitTarget.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteHeadPiece.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteHeadPiece.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteHeadPiece.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteTailPiece.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyHoldNoteTailPiece.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteTailPiece.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyKeyArea.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyManiaColumnElement.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyNotePiece.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyStageBackground.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/LegacyStageForeground.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs similarity index 100% rename from osu.Game.Rulesets.Mania/Skinning/ManiaLegacySkinTransformer.cs rename to osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs From e3c035fe9c651ac1eec750dd8197bbc037e9e994 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 12:32:52 +0900 Subject: [PATCH 5140/6909] Adjust namespace --- .../Editor/TestSceneManiaHitObjectComposer.cs | 2 +- .../Skinning/TestSceneHitExplosion.cs | 2 +- .../Edit/Blueprints/Components/EditBodyPiece.cs | 2 +- .../Edit/Blueprints/Components/EditNotePiece.cs | 2 +- .../Edit/ManiaHitObjectComposer.cs | 2 +- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 2 +- .../Objects/Drawables/DrawableHoldNote.cs | 2 +- .../Objects/Drawables/DrawableNote.cs | 2 +- .../Skinning/Default/DefaultBodyPiece.cs | 5 +++-- .../Skinning/Default/DefaultNotePiece.cs | 4 ++-- .../Skinning/Default/IHoldNoteBody.cs | 2 +- .../Skinning/Legacy/HitTargetInsetContainer.cs | 2 +- .../Skinning/Legacy/LegacyBodyPiece.cs | 2 +- .../Skinning/Legacy/LegacyColumnBackground.cs | 2 +- .../Skinning/Legacy/LegacyHitExplosion.cs | 2 +- .../Skinning/Legacy/LegacyHitTarget.cs | 2 +- .../Skinning/Legacy/LegacyHoldNoteHeadPiece.cs | 2 +- .../Skinning/Legacy/LegacyHoldNoteTailPiece.cs | 2 +- .../Skinning/Legacy/LegacyKeyArea.cs | 2 +- .../Skinning/Legacy/LegacyManiaColumnElement.cs | 2 +- .../Skinning/Legacy/LegacyNotePiece.cs | 2 +- .../Skinning/Legacy/LegacyStageBackground.cs | 2 +- .../Skinning/Legacy/LegacyStageForeground.cs | 2 +- .../Skinning/Legacy/ManiaLegacySkinTransformer.cs | 14 +++++++------- .../UI/Components/DefaultHitTarget.cs | 2 +- osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs | 2 +- 26 files changed, 35 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs index c9551ee79e..aaf96c63a6 100644 --- a/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania.Tests/Editor/TestSceneManiaHitObjectComposer.cs @@ -15,7 +15,7 @@ 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.Mania.Skinning.Default; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Screens.Edit; diff --git a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs index 0c56f7bcf4..4dc6700786 100644 --- a/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs +++ b/osu.Game.Rulesets.Mania.Tests/Skinning/TestSceneHitExplosion.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Objects; using osuTK; diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs index 5fa687298a..f5067ea366 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditBodyPiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Mania.Skinning.Default; namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components { diff --git a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs index 8773a39939..9c9273de3a 100644 --- a/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Edit/Blueprints/Components/EditNotePiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Mania.Skinning.Default; namespace osu.Game.Rulesets.Mania.Edit.Blueprints.Components { diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs index 01d572447b..324670c4b2 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaHitObjectComposer.cs @@ -9,7 +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.Skinning.Default; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 906f7382c5..59c766fd84 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -26,7 +26,7 @@ using osu.Game.Rulesets.Mania.Configuration; using osu.Game.Rulesets.Mania.Difficulty; using osu.Game.Rulesets.Mania.Edit; using osu.Game.Rulesets.Mania.Scoring; -using osu.Game.Rulesets.Mania.Skinning; +using osu.Game.Rulesets.Mania.Skinning.Legacy; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; using osu.Game.Scoring; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs index a64cc6dc67..4f062753a6 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableHoldNote.cs @@ -4,9 +4,9 @@ using System; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; +using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; diff --git a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs index b3402d13e4..b512986ccb 100644 --- a/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs +++ b/osu.Game.Rulesets.Mania/Objects/Drawables/DrawableNote.cs @@ -5,7 +5,7 @@ using System.Diagnostics; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Input.Bindings; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBodyPiece.cs index 9999983af5..db1ac6da88 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultBodyPiece.cs @@ -5,16 +5,17 @@ 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; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Layout; +using osu.Game.Rulesets.Mania.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables; +using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Mania.Skinning.Default { /// /// Represents length-wise portion of a hold note. diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultNotePiece.cs index 29f5217fd8..c9c3cff799 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Default/DefaultNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Default/DefaultNotePiece.cs @@ -4,7 +4,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -12,8 +11,9 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.UI.Scrolling; +using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Mania.Skinning.Default { /// /// Represents the static hit markers of notes. diff --git a/osu.Game.Rulesets.Mania/Skinning/Default/IHoldNoteBody.cs b/osu.Game.Rulesets.Mania/Skinning/Default/IHoldNoteBody.cs index ac3792c01d..1f290f1f1c 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Default/IHoldNoteBody.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Default/IHoldNoteBody.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. -namespace osu.Game.Rulesets.Mania.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Mania.Skinning.Default { /// /// Interface for mania hold note bodies. diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/HitTargetInsetContainer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/HitTargetInsetContainer.cs index c8b05ed2f8..3c89e2c04a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/HitTargetInsetContainer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/HitTargetInsetContainer.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class HitTargetInsetContainer : Container { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs index 8902d82f33..31db08ce2f 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyBodyPiece.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyBodyPiece : LegacyManiaColumnElement { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs index 3bf51b3073..661e7f66f4 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyColumnBackground.cs @@ -12,7 +12,7 @@ using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyColumnBackground : LegacyManiaColumnElement, IKeyBindingHandler { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs index 7c5d41efcf..73aece1ed4 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitExplosion.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyHitExplosion : LegacyManiaColumnElement, IHitExplosion { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitTarget.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitTarget.cs index 6eced571d2..490a03d11a 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitTarget.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHitTarget.cs @@ -12,7 +12,7 @@ using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyHitTarget : CompositeDrawable { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteHeadPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteHeadPiece.cs index c5aa062d0f..21e5bdd5d6 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteHeadPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteHeadPiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Skinning; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyHoldNoteHeadPiece : LegacyNotePiece { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteTailPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteTailPiece.cs index 2e8259f10a..232b47ae27 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteTailPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyHoldNoteTailPiece.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics.Textures; using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyHoldNoteTailPiece : LegacyNotePiece { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs index b269ea25d4..78ccb83a8c 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyKeyArea.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyKeyArea : LegacyManiaColumnElement, IKeyBindingHandler { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs index 3c0c632c14..eb5514ba43 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaColumnElement.cs @@ -8,7 +8,7 @@ using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.UI; using osu.Game.Skinning; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { /// /// A which is placed somewhere within a . diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs index 283b04373b..31279796ce 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyNotePiece.cs @@ -12,7 +12,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyNotePiece : LegacyManiaColumnElement { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs index b0bab8e760..fec3e9493e 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageBackground.cs @@ -12,7 +12,7 @@ using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyStageBackground : CompositeDrawable { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs index 4609fcc849..4e1952a670 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyStageForeground.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets.UI.Scrolling; using osu.Game.Skinning; using osuTK; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class LegacyStageForeground : CompositeDrawable { diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 3724269f4d..89f639e2fe 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -2,19 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Graphics; -using osu.Framework.Bindables; -using osu.Game.Rulesets.Scoring; -using osu.Game.Beatmaps; -using osu.Game.Rulesets.Mania.Beatmaps; -using osu.Game.Skinning; using System.Collections.Generic; using System.Diagnostics; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; -namespace osu.Game.Rulesets.Mania.Skinning +namespace osu.Game.Rulesets.Mania.Skinning.Legacy { public class ManiaLegacySkinTransformer : LegacySkinTransformer { diff --git a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs index e0b099ab9b..ec6c377a2e 100644 --- a/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs +++ b/osu.Game.Rulesets.Mania/UI/Components/DefaultHitTarget.cs @@ -8,7 +8,7 @@ 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.Mania.Skinning.Default; using osu.Game.Rulesets.UI.Scrolling; using osuTK.Graphics; diff --git a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs index 225269cf48..69b81d6d5c 100644 --- a/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs +++ b/osu.Game.Rulesets.Mania/UI/DefaultHitExplosion.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.Judgements; -using osu.Game.Rulesets.Mania.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Mania.Skinning.Default; using osu.Game.Rulesets.UI.Scrolling; using osuTK; using osuTK.Graphics; From b4b9312e0f9ad08ae3c9fda7f8d55450d3cbffdf Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 12:34:38 +0900 Subject: [PATCH 5141/6909] Move piece files of Catch ruleset --- .../{Objects/Drawables/Pieces => Skinning/Default}/BananaPiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/BorderPiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/DropletPiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/FruitPiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/GrapePiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/HyperBorderPiece.cs | 0 .../Pieces => Skinning/Default}/HyperDropletBorderPiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/PearPiece.cs | 0 .../Drawables/Pieces => Skinning/Default}/PineapplePiece.cs | 0 .../{Objects/Drawables/Pieces => Skinning/Default}/Pulp.cs | 0 .../Drawables/Pieces => Skinning/Default}/PulpFormation.cs | 0 .../Drawables/Pieces => Skinning/Default}/RaspberryPiece.cs | 0 .../Skinning/{ => Legacy}/CatchLegacySkinTransformer.cs | 0 .../Skinning/{ => Legacy}/LegacyCatchComboCounter.cs | 0 osu.Game.Rulesets.Catch/Skinning/{ => Legacy}/LegacyFruitPiece.cs | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/BananaPiece.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/BorderPiece.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/DropletPiece.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/FruitPiece.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/GrapePiece.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/HyperBorderPiece.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/HyperDropletBorderPiece.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/PearPiece.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/PineapplePiece.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/Pulp.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/PulpFormation.cs (100%) rename osu.Game.Rulesets.Catch/{Objects/Drawables/Pieces => Skinning/Default}/RaspberryPiece.cs (100%) rename osu.Game.Rulesets.Catch/Skinning/{ => Legacy}/CatchLegacySkinTransformer.cs (100%) rename osu.Game.Rulesets.Catch/Skinning/{ => Legacy}/LegacyCatchComboCounter.cs (100%) rename osu.Game.Rulesets.Catch/Skinning/{ => Legacy}/LegacyFruitPiece.cs (100%) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BorderPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/GrapePiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/GrapePiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperBorderPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/HyperDropletBorderPiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/HyperDropletBorderPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/HyperDropletBorderPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/PearPiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/PearPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/PineapplePiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/PineapplePiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Skinning/Default/Pulp.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/Pulp.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs b/osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/RaspberryPiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Default/RaspberryPiece.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs rename to osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Skinning/LegacyCatchComboCounter.cs rename to osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs similarity index 100% rename from osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs rename to osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs From d18397acad71b87231ee5b15e5751426428f81cb Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 12:35:24 +0900 Subject: [PATCH 5142/6909] Adjust namespace --- osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs | 1 + osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs | 1 + osu.Game.Rulesets.Catch/CatchRuleset.cs | 2 +- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs | 2 +- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 2 +- osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs | 2 +- osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs | 3 ++- osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs | 3 ++- osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs | 3 ++- osu.Game.Rulesets.Catch/Skinning/Default/GrapePiece.cs | 2 +- osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs | 2 +- .../Skinning/Default/HyperDropletBorderPiece.cs | 2 +- osu.Game.Rulesets.Catch/Skinning/Default/PearPiece.cs | 2 +- osu.Game.Rulesets.Catch/Skinning/Default/PineapplePiece.cs | 2 +- osu.Game.Rulesets.Catch/Skinning/Default/Pulp.cs | 2 +- osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs | 3 ++- osu.Game.Rulesets.Catch/Skinning/Default/RaspberryPiece.cs | 2 +- .../Skinning/Legacy/CatchLegacySkinTransformer.cs | 2 +- .../Skinning/Legacy/LegacyCatchComboCounter.cs | 2 +- osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs | 2 +- 20 files changed, 24 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs index b570f090ca..e70def7f8b 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchSkinColourDecodingTest.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Framework.IO.Stores; using osu.Game.Rulesets.Catch.Skinning; +using osu.Game.Rulesets.Catch.Skinning.Legacy; using osu.Game.Skinning; using osuTK.Graphics; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index 07cb73e5ff..d78dc2d2b5 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -13,6 +13,7 @@ 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.Skinning.Legacy; using osu.Game.Rulesets.Catch.UI; using osu.Game.Skinning; using osu.Game.Tests.Visual; diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index ad584d3f48..0a817eca0d 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -21,7 +21,7 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using System; -using osu.Game.Rulesets.Catch.Skinning; +using osu.Game.Rulesets.Catch.Skinning.Legacy; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index b8acea625b..e87d862fa5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -4,7 +4,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index ef9df02a68..da2529615e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -6,7 +6,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; +using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs index fa8837dec5..815855dbcf 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public class BananaPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs index 1e7a0b0685..7308d6b499 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs @@ -3,10 +3,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Shapes; +using osu.Game.Rulesets.Catch.Objects; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public class BorderPiece : Circle { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs index c90407ae15..0d305dbca5 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs @@ -5,10 +5,11 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Objects.Drawables; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public class DropletPiece : CompositeDrawable { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs index 31487ee407..81667315a0 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs @@ -7,9 +7,10 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { internal class FruitPiece : CompositeDrawable { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/GrapePiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/GrapePiece.cs index 15349c18d5..bb6a787c21 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/GrapePiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/GrapePiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public class GrapePiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs index 60bb07e89d..d160956a6e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Catch.UI; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public class HyperBorderPiece : BorderPiece { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/HyperDropletBorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/HyperDropletBorderPiece.cs index 1bd9fd6bb2..53a487b97f 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/HyperDropletBorderPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/HyperDropletBorderPiece.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. -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public class HyperDropletBorderPiece : HyperBorderPiece { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/PearPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/PearPiece.cs index 3372a06996..a99a4034ed 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/PearPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/PearPiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public class PearPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/PineapplePiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/PineapplePiece.cs index 7f80c58178..8d7dce6fc4 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/PineapplePiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/PineapplePiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public class PineapplePiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/Pulp.cs b/osu.Game.Rulesets.Catch/Skinning/Default/Pulp.cs index d3e4945611..37a8334068 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/Pulp.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/Pulp.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osuTK.Graphics; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public class Pulp : Circle { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs b/osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs index 1df548e70a..837bc74540 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs @@ -6,11 +6,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public abstract class PulpFormation : CompositeDrawable { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/RaspberryPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/RaspberryPiece.cs index 288ece95b2..f35d64f593 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/RaspberryPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/RaspberryPiece.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osuTK; -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +namespace osu.Game.Rulesets.Catch.Skinning.Default { public class RaspberryPiece : PulpFormation { diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs index 22db147e32..d597d439c2 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/CatchLegacySkinTransformer.cs @@ -9,7 +9,7 @@ using osuTK; using osuTK.Graphics; using static osu.Game.Skinning.LegacySkinConfiguration; -namespace osu.Game.Rulesets.Catch.Skinning +namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public class CatchLegacySkinTransformer : LegacySkinTransformer { diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs index 34608b07ff..f797ae75c2 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyCatchComboCounter.cs @@ -9,7 +9,7 @@ using osuTK; using osuTK.Graphics; using static osu.Game.Skinning.LegacySkinConfiguration; -namespace osu.Game.Rulesets.Catch.Skinning +namespace osu.Game.Rulesets.Catch.Skinning.Legacy { /// /// A combo counter implementation that visually behaves almost similar to stable's osu!catch combo counter. diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs index b8648f46f0..2db95a11a1 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs @@ -13,7 +13,7 @@ using osu.Game.Skinning; using osuTK; using osuTK.Graphics; -namespace osu.Game.Rulesets.Catch.Skinning +namespace osu.Game.Rulesets.Catch.Skinning.Legacy { internal class LegacyFruitPiece : CompositeDrawable { From 87189452d1a7d53c1167eb94fc291a74b27ded16 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 12:57:24 +0900 Subject: [PATCH 5143/6909] Refactor legacy skin piece to allow texture update --- .../Skinning/LegacyCatchHitObjectPiece.cs | 100 ++++++++++++++++++ .../Skinning/LegacyDropletPiece.cs | 20 ++++ .../Skinning/LegacyFruitPiece.cs | 84 ++++----------- 3 files changed, 140 insertions(+), 64 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs create mode 100644 osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs new file mode 100644 index 0000000000..4bcb92b9be --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs @@ -0,0 +1,100 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Pooling; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +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; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Skinning +{ + public abstract class LegacyCatchHitObjectPiece : PoolableDrawable + { + public readonly Bindable AccentColour = new Bindable(); + public readonly Bindable HyperDash = new Bindable(); + + private readonly Sprite colouredSprite; + private readonly Sprite overlaySprite; + private readonly Sprite hyperSprite; + + [Resolved] + private ISkinSource skin { get; set; } + + protected ISkinSource Skin => skin; + + [Resolved(canBeNull: true)] + private DrawableHitObject drawableHitObject { get; set; } + + [CanBeNull] + protected DrawablePalpableCatchHitObject DrawableHitObject => (DrawablePalpableCatchHitObject)drawableHitObject; + + protected LegacyCatchHitObjectPiece() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + colouredSprite = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + overlaySprite = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + hyperSprite = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Blending = BlendingParameters.Additive, + Depth = 1, + Alpha = 0, + Scale = new Vector2(1.2f), + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (DrawableHitObject != null) + { + AccentColour.BindTo(DrawableHitObject.AccentColour); + HyperDash.BindTo(DrawableHitObject.HyperDash); + } + + hyperSprite.Colour = Skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ?? + Skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? + Catcher.DEFAULT_HYPER_DASH_COLOUR; + + AccentColour.BindValueChanged(colour => + { + colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue); + }, true); + + HyperDash.BindValueChanged(hyper => + { + hyperSprite.Alpha = hyper.NewValue ? 0.7f : 0; + }, true); + } + + protected void SetTexture(Texture texture, Texture overlayTexture) + { + colouredSprite.Texture = texture; + overlaySprite.Texture = overlayTexture; + hyperSprite.Texture = texture; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs new file mode 100644 index 0000000000..cc8886f631 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.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.Textures; + +namespace osu.Game.Rulesets.Catch.Skinning +{ + public class LegacyDropletPiece : LegacyCatchHitObjectPiece + { + protected override void LoadComplete() + { + base.LoadComplete(); + + Texture texture = Skin.GetTexture("fruit-drop"); + Texture overlayTexture = Skin.GetTexture("fruit-drop-overlay"); + + SetTexture(texture, overlayTexture); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index b8648f46f0..7008234e99 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -1,83 +1,39 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; 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; -using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning { - internal class LegacyFruitPiece : CompositeDrawable + internal class LegacyFruitPiece : LegacyCatchHitObjectPiece { - private readonly string lookupName; + public readonly Bindable VisualRepresentation = new Bindable(); - private readonly Bindable accentColour = new Bindable(); - private readonly Bindable hyperDash = new Bindable(); - private Sprite colouredSprite; - - public LegacyFruitPiece(string lookupName) + private readonly string[] lookupNames = { - this.lookupName = lookupName; - RelativeSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject, ISkinSource skin) - { - var drawableCatchObject = (DrawablePalpableCatchHitObject)drawableObject; - - accentColour.BindTo(drawableCatchObject.AccentColour); - hyperDash.BindTo(drawableCatchObject.HyperDash); - - InternalChildren = new Drawable[] - { - colouredSprite = new Sprite - { - Texture = skin.GetTexture(lookupName), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - new Sprite - { - Texture = skin.GetTexture($"{lookupName}-overlay"), - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }, - }; - - if (hyperDash.Value) - { - var hyperDashOverlay = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Blending = BlendingParameters.Additive, - Depth = 1, - Alpha = 0.7f, - 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(hyperDashOverlay); - } - } + "fruit-pear", "fruit-grapes", "fruit-apple", "fruit-orange", "fruit-bananas" + }; protected override void LoadComplete() { base.LoadComplete(); - accentColour.BindValueChanged(colour => colouredSprite.Colour = LegacyColourCompatibility.DisallowZeroAlpha(colour.NewValue), true); + var fruit = (DrawableFruit)DrawableHitObject; + + if (fruit != null) + VisualRepresentation.BindTo(fruit.VisualRepresentation); + + VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true); + } + + private void setTexture(FruitVisualRepresentation visualRepresentation) + { + Texture texture = Skin.GetTexture(lookupNames[(int)visualRepresentation]); + Texture overlayTexture = Skin.GetTexture(lookupNames[(int)visualRepresentation] + "-overlay"); + + SetTexture(texture, overlayTexture); } } } From d51d2c533155d292c454780a9597bfc866f83556 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 12:59:03 +0900 Subject: [PATCH 5144/6909] Don't recreate pieces when catch DHO is reused --- .../CatchSkinComponents.cs | 7 +--- .../Objects/Drawables/DrawableDroplet.cs | 7 +--- .../Objects/Drawables/DrawableFruit.cs | 39 ++----------------- .../Skinning/CatchLegacySkinTransformer.cs | 21 +++++----- 4 files changed, 17 insertions(+), 57 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs index 23d8428fec..668f7197be 100644 --- a/osu.Game.Rulesets.Catch/CatchSkinComponents.cs +++ b/osu.Game.Rulesets.Catch/CatchSkinComponents.cs @@ -5,11 +5,8 @@ namespace osu.Game.Rulesets.Catch { public enum CatchSkinComponents { - FruitBananas, - FruitApple, - FruitGrapes, - FruitOrange, - FruitPear, + Fruit, + Banana, Droplet, CatcherIdle, CatcherFail, diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index b8acea625b..4c49bfe6c9 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -25,17 +25,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() - { - HyperDash.BindValueChanged(_ => updatePiece(), true); - } - - private void updatePiece() { ScaleContainer.Child = new SkinnableDrawable( new CatchSkinComponent(CatchSkinComponents.Droplet), _ => new DropletPiece { - HyperDash = { BindTarget = HyperDash } + HyperDash = { BindTarget = HyperDash }, }); } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index ef9df02a68..2998d2cc6f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -35,21 +34,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables VisualRepresentation.Value = GetVisualRepresentation(change.NewValue); }, true); - VisualRepresentation.BindValueChanged(_ => updatePiece()); - HyperDash.BindValueChanged(_ => updatePiece(), true); - } - - protected override void UpdateInitialTransforms() - { - base.UpdateInitialTransforms(); - - ScaleContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); - } - - private void updatePiece() - { ScaleContainer.Child = new SkinnableDrawable( - new CatchSkinComponent(getComponent(VisualRepresentation.Value)), + new CatchSkinComponent(this is DrawableBanana ? CatchSkinComponents.Banana : CatchSkinComponents.Fruit), _ => new FruitPiece { VisualRepresentation = { BindTarget = VisualRepresentation }, @@ -57,28 +43,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables }); } - private CatchSkinComponents getComponent(FruitVisualRepresentation hitObjectVisualRepresentation) + protected override void UpdateInitialTransforms() { - switch (hitObjectVisualRepresentation) - { - case FruitVisualRepresentation.Pear: - return CatchSkinComponents.FruitPear; + base.UpdateInitialTransforms(); - case FruitVisualRepresentation.Grape: - return CatchSkinComponents.FruitGrapes; - - case FruitVisualRepresentation.Pineapple: - return CatchSkinComponents.FruitApple; - - case FruitVisualRepresentation.Raspberry: - return CatchSkinComponents.FruitOrange; - - case FruitVisualRepresentation.Banana: - return CatchSkinComponents.FruitBananas; - - default: - throw new ArgumentOutOfRangeException(nameof(hitObjectVisualRepresentation), hitObjectVisualRepresentation, null); - } + ScaleContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); } } diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 22db147e32..1889b230a7 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -1,11 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using Humanizer; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Skinning; -using osuTK; using osuTK.Graphics; using static osu.Game.Skinning.LegacySkinConfiguration; @@ -40,20 +38,21 @@ namespace osu.Game.Rulesets.Catch.Skinning switch (catchSkinComponent.Component) { - case CatchSkinComponents.FruitApple: - case CatchSkinComponents.FruitBananas: - case CatchSkinComponents.FruitOrange: - case CatchSkinComponents.FruitGrapes: - case CatchSkinComponents.FruitPear: - var lookupName = catchSkinComponent.Component.ToString().Kebaberize(); - if (GetTexture(lookupName) != null) - return new LegacyFruitPiece(lookupName); + case CatchSkinComponents.Fruit: + if (GetTexture("fruit-pear") != null) + return new LegacyFruitPiece(); + + break; + + case CatchSkinComponents.Banana: + if (GetTexture("fruit-bananas") != null) + return new LegacyFruitPiece(); break; case CatchSkinComponents.Droplet: if (GetTexture("fruit-drop") != null) - return new LegacyFruitPiece("fruit-drop") { Scale = new Vector2(0.8f) }; + return new LegacyDropletPiece(); break; From 42b810d0c84ebb1ed7f5701b0d5f1fb99a57c139 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Dec 2020 13:03:34 +0900 Subject: [PATCH 5145/6909] Update DiscordRichPresence with fix for startup crashes --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index adf9c452f6..8b8ad9f8af 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -29,7 +29,7 @@ - + From 0d73bf84888b655ef1b233464bf1a9239c70b9f3 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 13:14:00 +0900 Subject: [PATCH 5146/6909] Refactor catch default piece to allow reuse But Fruit in-place update is still incomplete, as child drawables are recreated when reused. --- .../Objects/Drawables/DrawableDroplet.cs | 5 +- .../Objects/Drawables/DrawableFruit.cs | 6 +-- .../Drawables/Pieces/CatchHitObjectPiece.cs | 54 +++++++++++++++++++ .../Objects/Drawables/Pieces/DropletPiece.cs | 27 +++------- .../Objects/Drawables/Pieces/FruitPiece.cs | 48 +++++++---------- 5 files changed, 83 insertions(+), 57 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/CatchHitObjectPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 4c49bfe6c9..dea19a2446 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -28,10 +28,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { ScaleContainer.Child = new SkinnableDrawable( new CatchSkinComponent(CatchSkinComponents.Droplet), - _ => new DropletPiece - { - HyperDash = { BindTarget = HyperDash }, - }); + _ => new DropletPiece()); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 2998d2cc6f..8db8f5ec82 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -36,11 +36,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables ScaleContainer.Child = new SkinnableDrawable( new CatchSkinComponent(this is DrawableBanana ? CatchSkinComponents.Banana : CatchSkinComponents.Fruit), - _ => new FruitPiece - { - VisualRepresentation = { BindTarget = VisualRepresentation }, - HyperDash = { BindTarget = HyperDash }, - }); + _ => new FruitPiece()); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/CatchHitObjectPiece.cs new file mode 100644 index 0000000000..ec5f66a945 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/CatchHitObjectPiece.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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class CatchHitObjectPiece : CompositeDrawable + { + public readonly Bindable AccentColour = new Bindable(); + public readonly Bindable HyperDash = new Bindable(); + + [Resolved(canBeNull: true)] + private DrawableHitObject drawableHitObject { get; set; } + + [CanBeNull] + protected DrawablePalpableCatchHitObject DrawableHitObject => (DrawablePalpableCatchHitObject)drawableHitObject; + + [CanBeNull] + protected BorderPiece BorderPiece; + + [CanBeNull] + protected HyperBorderPiece HyperBorderPiece; + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (DrawableHitObject != null) + { + AccentColour.BindTo(DrawableHitObject.AccentColour); + HyperDash.BindTo(DrawableHitObject.HyperDash); + } + + HyperDash.BindValueChanged(hyper => + { + if (HyperBorderPiece != null) + HyperBorderPiece.Alpha = hyper.NewValue ? 1 : 0; + }, true); + } + + protected override void Update() + { + if (BorderPiece != null && DrawableHitObject?.HitObject != null) + BorderPiece.Alpha = (float)Math.Clamp((DrawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs index c90407ae15..f92c92160a 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/DropletPiece.cs @@ -1,37 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Objects.Drawables; using osuTK; namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { - public class DropletPiece : CompositeDrawable + public class DropletPiece : CatchHitObjectPiece { - public readonly Bindable HyperDash = new Bindable(); - public DropletPiece() { Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); - } - [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) - { - InternalChild = new Pulp + InternalChildren = new Drawable[] { - RelativeSizeAxes = Axes.Both, - AccentColour = { BindTarget = drawableObject.AccentColour } + new Pulp + { + RelativeSizeAxes = Axes.Both, + AccentColour = { BindTarget = AccentColour } + }, + HyperBorderPiece = new HyperDropletBorderPiece() }; - - if (HyperDash.Value) - { - AddInternal(new HyperDropletBorderPiece()); - } } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs index 31487ee407..9b5c00b2bf 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.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 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; namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { - internal class FruitPiece : CompositeDrawable + internal class FruitPiece : CatchHitObjectPiece { /// /// Because we're adding a border around the fruit, we need to scale down some. @@ -19,38 +14,36 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces public const float RADIUS_ADJUST = 1.1f; public readonly Bindable VisualRepresentation = new Bindable(); - public readonly Bindable HyperDash = new Bindable(); - - [CanBeNull] - private DrawableCatchHitObject drawableHitObject; - - [CanBeNull] - private BorderPiece borderPiece; public FruitPiece() { RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader(permitNulls: true)] - private void load([CanBeNull] DrawableHitObject drawable) + protected override void LoadComplete() { - drawableHitObject = (DrawableCatchHitObject)drawable; + base.LoadComplete(); + + if (DrawableHitObject != null) + { + var fruit = (DrawableFruit)DrawableHitObject; + VisualRepresentation.BindTo(fruit.VisualRepresentation); + } + + VisualRepresentation.BindValueChanged(_ => recreateChildren(), true); + } + + private void recreateChildren() + { + ClearInternal(); AddInternal(getFruitFor(VisualRepresentation.Value)); - // if it is not part of a DHO, the border is always invisible. - if (drawableHitObject != null) - AddInternal(borderPiece = new BorderPiece()); + if (DrawableHitObject != null) + AddInternal(BorderPiece = new BorderPiece()); if (HyperDash.Value) - AddInternal(new HyperBorderPiece()); - } - - protected override void Update() - { - if (borderPiece != null && drawableHitObject?.HitObject != null) - borderPiece.Alpha = (float)Math.Clamp((drawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1); + AddInternal(HyperBorderPiece = new HyperBorderPiece()); } private Drawable getFruitFor(FruitVisualRepresentation representation) @@ -66,9 +59,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces case FruitVisualRepresentation.Pineapple: return new PineapplePiece(); - case FruitVisualRepresentation.Banana: - return new BananaPiece(); - case FruitVisualRepresentation.Raspberry: return new RaspberryPiece(); } From b8f1c499a45f6dd71b925b2963ba7f5f7858d05c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 13:56:22 +0900 Subject: [PATCH 5147/6909] Allow PulpFormation to update formation dynamically Pulps are lazily allocated but never deallocated for a DrawableFruit --- .../Objects/Drawables/Pieces/BananaPiece.cs | 30 --------- .../Objects/Drawables/Pieces/FruitPiece.cs | 46 ++++---------- .../Drawables/Pieces/FruitPulpFormation.cs | 63 +++++++++++++++++++ .../Objects/Drawables/Pieces/GrapePiece.cs | 42 ------------- .../Objects/Drawables/Pieces/PearPiece.cs | 42 ------------- .../Drawables/Pieces/PineapplePiece.cs | 48 -------------- .../Objects/Drawables/Pieces/Pulp.cs | 4 +- .../Objects/Drawables/Pieces/PulpFormation.cs | 26 +++++--- .../Drawables/Pieces/RaspberryPiece.cs | 48 -------------- 9 files changed, 95 insertions(+), 254 deletions(-) delete mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPulpFormation.cs delete mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs delete mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs delete mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs delete mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs deleted file mode 100644 index fa8837dec5..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.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 osuTK; - -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces -{ - public class BananaPiece : PulpFormation - { - public BananaPiece() - { - InternalChildren = new Drawable[] - { - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(SMALL_PULP), - Y = -0.3f - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f), - Y = 0.05f, - }, - }; - } - } -} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs index 9b5c00b2bf..e915253ff7 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPiece.cs @@ -18,6 +18,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces public FruitPiece() { RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new FruitPulpFormation + { + AccentColour = { BindTarget = AccentColour }, + VisualRepresentation = { BindTarget = VisualRepresentation } + }, + BorderPiece = new BorderPiece(), + HyperBorderPiece = new HyperBorderPiece() + }; } protected override void LoadComplete() @@ -29,41 +40,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces var fruit = (DrawableFruit)DrawableHitObject; VisualRepresentation.BindTo(fruit.VisualRepresentation); } - - VisualRepresentation.BindValueChanged(_ => recreateChildren(), true); - } - - private void recreateChildren() - { - ClearInternal(); - - AddInternal(getFruitFor(VisualRepresentation.Value)); - - if (DrawableHitObject != null) - AddInternal(BorderPiece = new BorderPiece()); - - if (HyperDash.Value) - AddInternal(HyperBorderPiece = new HyperBorderPiece()); - } - - private Drawable getFruitFor(FruitVisualRepresentation representation) - { - switch (representation) - { - case FruitVisualRepresentation.Pear: - return new PearPiece(); - - case FruitVisualRepresentation.Grape: - return new GrapePiece(); - - case FruitVisualRepresentation.Pineapple: - return new PineapplePiece(); - - case FruitVisualRepresentation.Raspberry: - return new RaspberryPiece(); - } - - return Empty(); } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPulpFormation.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPulpFormation.cs new file mode 100644 index 0000000000..cd870b1f98 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPulpFormation.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; +using osuTK; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class FruitPulpFormation : PulpFormation + { + public readonly Bindable VisualRepresentation = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + VisualRepresentation.BindValueChanged(setFormation, true); + } + + private void setFormation(ValueChangedEvent visualRepresentation) + { + Clear(); + + switch (visualRepresentation.NewValue) + { + case FruitVisualRepresentation.Pear: + Add(new Vector2(0, -0.33f), new Vector2(SMALL_PULP)); + Add(PositionAt(60, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + Add(PositionAt(180, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + Add(PositionAt(300, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + break; + + case FruitVisualRepresentation.Grape: + Add(new Vector2(0, -0.25f), new Vector2(SMALL_PULP)); + Add(PositionAt(0, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + Add(PositionAt(120, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + Add(PositionAt(240, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + break; + + case FruitVisualRepresentation.Pineapple: + Add(new Vector2(0, -0.3f), new Vector2(SMALL_PULP)); + Add(PositionAt(45, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + Add(PositionAt(135, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + Add(PositionAt(225, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + Add(PositionAt(315, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + break; + + case FruitVisualRepresentation.Raspberry: + Add(new Vector2(0, -0.34f), new Vector2(SMALL_PULP)); + Add(PositionAt(0, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + Add(PositionAt(90, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + Add(PositionAt(180, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + Add(PositionAt(270, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + break; + + case FruitVisualRepresentation.Banana: + Add(new Vector2(0, -0.3f), new Vector2(SMALL_PULP)); + Add(new Vector2(0, 0.05f), new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f)); + break; + } + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs deleted file mode 100644 index 15349c18d5..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/GrapePiece.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osuTK; - -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces -{ - public class GrapePiece : PulpFormation - { - public GrapePiece() - { - InternalChildren = new Drawable[] - { - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(SMALL_PULP), - Y = -0.25f, - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_3), - Position = PositionAt(0, DISTANCE_FROM_CENTRE_3), - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_3), - Position = PositionAt(120, DISTANCE_FROM_CENTRE_3), - }, - new Pulp - { - Size = new Vector2(LARGE_PULP_3), - AccentColour = { BindTarget = AccentColour }, - Position = PositionAt(240, DISTANCE_FROM_CENTRE_3), - }, - }; - } - } -} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs deleted file mode 100644 index 3372a06996..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PearPiece.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Graphics; -using osuTK; - -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces -{ - public class PearPiece : PulpFormation - { - public PearPiece() - { - InternalChildren = new Drawable[] - { - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(SMALL_PULP), - Y = -0.33f, - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_3), - Position = PositionAt(60, DISTANCE_FROM_CENTRE_3), - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_3), - Position = PositionAt(180, DISTANCE_FROM_CENTRE_3), - }, - new Pulp - { - Size = new Vector2(LARGE_PULP_3), - AccentColour = { BindTarget = AccentColour }, - Position = PositionAt(300, DISTANCE_FROM_CENTRE_3), - }, - }; - } - } -} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.cs deleted file mode 100644 index 7f80c58178..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PineapplePiece.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 osu.Framework.Graphics; -using osuTK; - -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces -{ - public class PineapplePiece : PulpFormation - { - public PineapplePiece() - { - InternalChildren = new Drawable[] - { - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(SMALL_PULP), - Y = -0.3f, - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_4), - Position = PositionAt(45, DISTANCE_FROM_CENTRE_4), - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_4), - Position = PositionAt(135, DISTANCE_FROM_CENTRE_4), - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_4), - Position = PositionAt(225, DISTANCE_FROM_CENTRE_4), - }, - new Pulp - { - Size = new Vector2(LARGE_PULP_4), - AccentColour = { BindTarget = AccentColour }, - Position = PositionAt(315, DISTANCE_FROM_CENTRE_4), - }, - }; - } - } -} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs index d3e4945611..3113cf0ceb 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/Pulp.cs @@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public class Pulp : Circle { + public readonly Bindable AccentColour = new Bindable(); + public Pulp() { RelativePositionAxes = Axes.Both; @@ -22,8 +24,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces Colour = Color4.White.Opacity(0.9f); } - public readonly Bindable AccentColour = new Bindable(); - protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs index 1df548e70a..412750019f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/PulpFormation.cs @@ -2,11 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Objects.Drawables; using osuTK; using osuTK.Graphics; @@ -14,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces { public abstract class PulpFormation : CompositeDrawable { - protected readonly IBindable AccentColour = new Bindable(); + public readonly Bindable AccentColour = new Bindable(); protected const float LARGE_PULP_3 = 16f * FruitPiece.RADIUS_ADJUST; protected const float DISTANCE_FROM_CENTRE_3 = 0.15f; @@ -24,6 +22,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces protected const float SMALL_PULP = LARGE_PULP_3 / 2; + private int numPulps; + protected PulpFormation() { RelativeSizeAxes = Axes.Both; @@ -33,11 +33,23 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces distance * MathF.Sin(angle * MathF.PI / 180), distance * MathF.Cos(angle * MathF.PI / 180)); - [BackgroundDependencyLoader] - private void load(DrawableHitObject drawableObject) + protected void Clear() { - DrawableCatchHitObject drawableCatchObject = (DrawableCatchHitObject)drawableObject; - AccentColour.BindTo(drawableCatchObject.AccentColour); + for (; numPulps > 0; numPulps--) + InternalChildren[numPulps - 1].Alpha = 0; + } + + protected void Add(Vector2 position, Vector2 size) + { + if (numPulps == InternalChildren.Count) + AddInternal(new Pulp { AccentColour = { BindTarget = AccentColour } }); + + var pulp = InternalChildren[numPulps]; + pulp.Position = position; + pulp.Size = size; + pulp.Alpha = 1; + + numPulps++; } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.cs deleted file mode 100644 index 288ece95b2..0000000000 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/RaspberryPiece.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 osu.Framework.Graphics; -using osuTK; - -namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces -{ - public class RaspberryPiece : PulpFormation - { - public RaspberryPiece() - { - InternalChildren = new Drawable[] - { - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(SMALL_PULP), - Y = -0.34f, - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_4), - Position = PositionAt(0, DISTANCE_FROM_CENTRE_4), - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_4), - Position = PositionAt(90, DISTANCE_FROM_CENTRE_4), - }, - new Pulp - { - AccentColour = { BindTarget = AccentColour }, - Size = new Vector2(LARGE_PULP_4), - Position = PositionAt(180, DISTANCE_FROM_CENTRE_4), - }, - new Pulp - { - Size = new Vector2(LARGE_PULP_4), - AccentColour = { BindTarget = AccentColour }, - Position = PositionAt(270, DISTANCE_FROM_CENTRE_4), - }, - }; - } - } -} From c0f39514b9aba1295ba168ad29b541ba92d4b1a7 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 14:00:22 +0900 Subject: [PATCH 5148/6909] Fix legacy droplet scale --- osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs index cc8886f631..8f4331d2a3 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyDropletPiece.cs @@ -2,11 +2,17 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics.Textures; +using osuTK; namespace osu.Game.Rulesets.Catch.Skinning { public class LegacyDropletPiece : LegacyCatchHitObjectPiece { + public LegacyDropletPiece() + { + Scale = new Vector2(0.8f); + } + protected override void LoadComplete() { base.LoadComplete(); From 7f1ad1040d3fef6def87abcd4144014b3952148b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 14:08:50 +0900 Subject: [PATCH 5149/6909] Don't inherit DrawableFruit from DrawableBanana - A banana cannot be hyper --- .../Objects/Drawables/DrawableBanana.cs | 15 +++++++++--- .../Objects/Drawables/DrawableFruit.cs | 7 ++---- .../Objects/Drawables/Pieces/BananaPiece.cs | 24 +++++++++++++++++++ .../Drawables/Pieces/BananaPulpFormation.cs | 16 +++++++++++++ .../Drawables/Pieces/FruitPulpFormation.cs | 5 ---- .../Skinning/CatchLegacySkinTransformer.cs | 2 +- .../Skinning/LegacyBananaPiece.cs | 20 ++++++++++++++++ .../Skinning/LegacyFruitPiece.cs | 2 +- 8 files changed, 76 insertions(+), 15 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPulpFormation.cs create mode 100644 osu.Game.Rulesets.Catch/Skinning/LegacyBananaPiece.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 8e9d80106b..bdf257a13f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -2,14 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using JetBrains.Annotations; +using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.Objects.Drawables.Pieces; +using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableBanana : DrawableFruit + public class DrawableBanana : DrawablePalpableCatchHitObject { - protected override FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => FruitVisualRepresentation.Banana; - public DrawableBanana() : this(null) { @@ -20,6 +21,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { } + [BackgroundDependencyLoader] + private void load() + { + ScaleContainer.Child = new SkinnableDrawable( + new CatchSkinComponent(CatchSkinComponents.Banana), + _ => new BananaPiece()); + } + protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 8db8f5ec82..63b48ea99f 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -14,8 +14,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public readonly Bindable VisualRepresentation = new Bindable(); - protected virtual FruitVisualRepresentation GetVisualRepresentation(int indexInBeatmap) => (FruitVisualRepresentation)(indexInBeatmap % 4); - public DrawableFruit() : this(null) { @@ -31,11 +29,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { IndexInBeatmap.BindValueChanged(change => { - VisualRepresentation.Value = GetVisualRepresentation(change.NewValue); + VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4); }, true); ScaleContainer.Child = new SkinnableDrawable( - new CatchSkinComponent(this is DrawableBanana ? CatchSkinComponents.Banana : CatchSkinComponents.Fruit), + new CatchSkinComponent(CatchSkinComponents.Fruit), _ => new FruitPiece()); } @@ -53,6 +51,5 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Grape, Pineapple, Raspberry, - Banana // banananananannaanana } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.cs new file mode 100644 index 0000000000..9f5d3f36df --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPiece.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; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class BananaPiece : CatchHitObjectPiece + { + public BananaPiece() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = new Drawable[] + { + new BananaPulpFormation + { + AccentColour = { BindTarget = AccentColour }, + }, + BorderPiece = new BorderPiece(), + }; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPulpFormation.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPulpFormation.cs new file mode 100644 index 0000000000..b22d7fb413 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/BananaPulpFormation.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 osuTK; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces +{ + public class BananaPulpFormation : PulpFormation + { + public BananaPulpFormation() + { + Add(new Vector2(0, -0.3f), new Vector2(SMALL_PULP)); + Add(new Vector2(0, 0.05f), new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f)); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPulpFormation.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPulpFormation.cs index cd870b1f98..c0b1f588f5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPulpFormation.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/Pieces/FruitPulpFormation.cs @@ -52,11 +52,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables.Pieces Add(PositionAt(180, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); Add(PositionAt(270, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); break; - - case FruitVisualRepresentation.Banana: - Add(new Vector2(0, -0.3f), new Vector2(SMALL_PULP)); - Add(new Vector2(0, 0.05f), new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f)); - break; } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs index 1889b230a7..b4bee8adc6 100644 --- a/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Catch/Skinning/CatchLegacySkinTransformer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Rulesets.Catch.Skinning case CatchSkinComponents.Banana: if (GetTexture("fruit-bananas") != null) - return new LegacyFruitPiece(); + return new LegacyBananaPiece(); break; diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyBananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyBananaPiece.cs new file mode 100644 index 0000000000..f80e50c8c0 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyBananaPiece.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.Textures; + +namespace osu.Game.Rulesets.Catch.Skinning +{ + public class LegacyBananaPiece : LegacyCatchHitObjectPiece + { + protected override void LoadComplete() + { + base.LoadComplete(); + + Texture texture = Skin.GetTexture("fruit-bananas"); + Texture overlayTexture = Skin.GetTexture("fruit-bananas-overlay"); + + SetTexture(texture, overlayTexture); + } + } +} diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs index 7008234e99..9b3003f1d6 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyFruitPiece.cs @@ -13,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Skinning private readonly string[] lookupNames = { - "fruit-pear", "fruit-grapes", "fruit-apple", "fruit-orange", "fruit-bananas" + "fruit-pear", "fruit-grapes", "fruit-apple", "fruit-orange" }; protected override void LoadComplete() From c1d39b64010652f194df448b74f9ccedc04fea0a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 14:10:17 +0900 Subject: [PATCH 5150/6909] Don't inherit Fruit from Banana --- osu.Game.Rulesets.Catch/Objects/Banana.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Banana.cs b/osu.Game.Rulesets.Catch/Objects/Banana.cs index 3f71da713e..178306b3bc 100644 --- a/osu.Game.Rulesets.Catch/Objects/Banana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Banana.cs @@ -14,7 +14,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects { - public class Banana : Fruit, IHasComboInformation + public class Banana : PalpableCatchHitObject, IHasComboInformation { /// /// Index of banana in current shower. From 2469608c1004d73dcf6a71704acdf8fabe96f53d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 15:19:18 +0900 Subject: [PATCH 5151/6909] Fix possible null reference due to bindable change before variable is initialized --- osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index ec0ce08004..ebdf23e5bd 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -33,9 +33,6 @@ namespace osu.Game.Overlays.Settings.Sections.Input configSensitivity.BindValueChanged(val => sensitivityBindable.Value = val.NewValue); sensitivityBindable.BindValueChanged(val => configSensitivity.Value = val.NewValue); - windowMode = config.GetBindable(FrameworkSetting.WindowMode); - windowMode.BindValueChanged(mode => confineMouseModeSetting.Alpha = mode.NewValue == WindowMode.Fullscreen ? 0 : 1); - Children = new Drawable[] { new SettingsCheckbox @@ -70,6 +67,9 @@ namespace osu.Game.Overlays.Settings.Sections.Input }, }; + windowMode = config.GetBindable(FrameworkSetting.WindowMode); + windowMode.BindValueChanged(mode => confineMouseModeSetting.Alpha = mode.NewValue == WindowMode.Fullscreen ? 0 : 1); + if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) { rawInputToggle.Disabled = true; From f4eb17d398b5c98e711986fe02a1c4c30ca1b0a5 Mon Sep 17 00:00:00 2001 From: ekrctb <32995012+ekrctb@users.noreply.github.com> Date: Mon, 7 Dec 2020 16:09:14 +0900 Subject: [PATCH 5152/6909] Update osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs Set mouse confine mode at initialization Co-authored-by: Dean Herbert --- 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 ebdf23e5bd..455e13711d 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -68,7 +68,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input }; windowMode = config.GetBindable(FrameworkSetting.WindowMode); - windowMode.BindValueChanged(mode => confineMouseModeSetting.Alpha = mode.NewValue == WindowMode.Fullscreen ? 0 : 1); + windowMode.BindValueChanged(mode => confineMouseModeSetting.Alpha = mode.NewValue == WindowMode.Fullscreen ? 0 : 1, true); if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) { From 7253866e1790c7009304a1ebfb2f1ecb5cca1258 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Dec 2020 16:42:55 +0900 Subject: [PATCH 5153/6909] Move customisation panel to be in same area as main content --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 84 ++++++++++++---------- 1 file changed, 47 insertions(+), 37 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 12da718ab2..34f5c70adb 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -158,37 +158,57 @@ namespace osu.Game.Overlays.Mods }, new Drawable[] { - // Body - new OsuScrollContainer + new Container { - ScrollbarVisible = false, - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding + Children = new Drawable[] { - Vertical = 10, - Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING - }, - Child = ModSectionsContainer = new FillFlowContainer - { - Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 10f), - Width = content_width, - LayoutDuration = 200, - LayoutEasing = Easing.OutQuint, - Children = new ModSection[] + // Body + new OsuScrollContainer { - new DifficultyReductionSection { Action = modButtonPressed }, - new DifficultyIncreaseSection { Action = modButtonPressed }, - new AutomationSection { Action = modButtonPressed }, - new ConversionSection { Action = modButtonPressed }, - new FunSection { Action = modButtonPressed }, - } - }, + ScrollbarVisible = false, + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Vertical = 10, + Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING + }, + Children = new Drawable[] + { + ModSectionsContainer = new FillFlowContainer + { + Origin = Anchor.TopCentre, + Anchor = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Width = content_width, + LayoutDuration = 200, + LayoutEasing = Easing.OutQuint, + Children = new ModSection[] + { + new DifficultyReductionSection { Action = modButtonPressed }, + new DifficultyIncreaseSection { Action = modButtonPressed }, + new AutomationSection { Action = modButtonPressed }, + new ConversionSection { Action = modButtonPressed }, + new FunSection { Action = modButtonPressed }, + } + }, + } + }, + ModSettingsContainer = new ModSettingsContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Width = 0.3f, + Alpha = 0, + Padding = new MarginPadding(30), + SelectedMods = { BindTarget = SelectedMods }, + }, + } }, }, new Drawable[] @@ -281,16 +301,6 @@ namespace osu.Game.Overlays.Mods }, }, }, - ModSettingsContainer = new ModSettingsContainer - { - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Width = 0.25f, - Alpha = 0, - X = -100, - SelectedMods = { BindTarget = SelectedMods }, - } }; ((IBindable)CustomiseButton.Enabled).BindTo(ModSettingsContainer.HasSettingsForSelection); From 454e94574c5c8a7a3c4989c4a1b79b1c47f5967a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Dec 2020 16:43:07 +0900 Subject: [PATCH 5154/6909] Add corner rounding and positional transform --- .../Overlays/Mods/ModSettingsContainer.cs | 46 ++++++++++++------- 1 file changed, 30 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSettingsContainer.cs b/osu.Game/Overlays/Mods/ModSettingsContainer.cs index a1d00f91b4..1c57ff54ad 100644 --- a/osu.Game/Overlays/Mods/ModSettingsContainer.cs +++ b/osu.Game/Overlays/Mods/ModSettingsContainer.cs @@ -27,28 +27,40 @@ namespace osu.Game.Overlays.Mods private readonly FillFlowContainer modSettingsContent; - private const double transition_duration = 200; + private readonly Container content; + + private const double transition_duration = 400; public ModSettingsContainer() { - Children = new Drawable[] + RelativeSizeAxes = Axes.Both; + + Child = content = new Container { - new Box + Masking = true, + CornerRadius = 10, + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Both, + X = 1, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = new Color4(0, 0, 0, 192) - }, - new OsuScrollContainer - { - RelativeSizeAxes = Axes.Both, - Child = modSettingsContent = new FillFlowContainer + new Box { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Spacing = new Vector2(0f, 10f), - Padding = new MarginPadding(20), + RelativeSizeAxes = Axes.Both, + Colour = new Color4(0, 0, 0, 192) + }, + new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = modSettingsContent = new FillFlowContainer + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Spacing = new Vector2(0f, 10f), + Padding = new MarginPadding(20), + } } } }; @@ -86,11 +98,13 @@ namespace osu.Game.Overlays.Mods protected override void PopIn() { this.FadeIn(transition_duration, Easing.OutQuint); + content.MoveToX(0, transition_duration, Easing.OutQuint); } protected override void PopOut() { this.FadeOut(transition_duration, Easing.OutQuint); + content.MoveToX(1, transition_duration, Easing.OutQuint); } } } From a548269c341296145d5381ca6d1365640be50991 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 17:26:12 +0900 Subject: [PATCH 5155/6909] Fix scrolling hit object displayed in wrong position for one frame --- .../Scrolling/ScrollingHitObjectContainer.cs | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs index 3a5e3c098f..289578f3d8 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingHitObjectContainer.cs @@ -225,10 +225,19 @@ namespace osu.Game.Rulesets.UI.Scrolling hitObject.LifetimeStart = computeOriginAdjustedLifetimeStart(hitObject); toComputeLifetime.Clear(); + } - // only AliveObjects need to be considered for layout (reduces overhead in the case of scroll speed changes). + protected override void UpdateAfterChildrenLife() + { + base.UpdateAfterChildrenLife(); + + // We need to calculate hit object positions (including nested hit objects) as soon as possible after lifetimes + // to prevent hit objects displayed in a wrong position for one frame. + // Only AliveObjects need to be considered for layout (reduces overhead in the case of scroll speed changes). foreach (var obj in AliveObjects) { + updatePosition(obj, Time.Current); + if (layoutComputed.Contains(obj)) continue; @@ -293,15 +302,6 @@ namespace osu.Game.Rulesets.UI.Scrolling } } - protected override void UpdateAfterChildrenLife() - { - base.UpdateAfterChildrenLife(); - - // We need to calculate hitobject positions as soon as possible after lifetimes so that hitobjects get the final say in their positions - foreach (var obj in AliveObjects) - updatePosition(obj, Time.Current); - } - private void updatePosition(DrawableHitObject hitObject, double currentTime) { switch (direction.Value) From eb38bc4b4c755f0e7612cf9b9e10063341c4f318 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Dec 2020 18:00:45 +0900 Subject: [PATCH 5156/6909] Add the ability to import into ArchiveModelManagers from a stream --- .../Beatmaps/IO/ImportBeatmapTest.cs | 15 ++-- .../Menus/TestSceneMusicActionHandling.cs | 3 +- .../Online/TestSceneDirectDownloadButton.cs | 2 +- .../TestSceneDeleteLocalScore.cs | 3 +- osu.Game/Database/ArchiveModelManager.cs | 55 ++++++-------- .../DownloadableArchiveModelManager.cs | 2 +- osu.Game/Database/ICanAcceptFiles.cs | 8 ++ osu.Game/Database/ImportTask.cs | 73 +++++++++++++++++++ osu.Game/OsuGameBase.cs | 11 +++ .../Screens/Edit/Setup/ResourcesSection.cs | 3 + 10 files changed, 132 insertions(+), 43 deletions(-) create mode 100644 osu.Game/Database/ImportTask.cs diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index b941313103..6e16058360 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -14,6 +14,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions; using osu.Framework.Logging; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.IO; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Objects; @@ -127,7 +128,7 @@ namespace osu.Game.Tests.Beatmaps.IO // zip files differ because different compression or encoder. Assert.AreNotEqual(hashBefore, hashFile(temp)); - var importedSecondTime = await osu.Dependencies.Get().Import(temp); + var importedSecondTime = await osu.Dependencies.Get().Import(new ImportTask(temp)); ensureLoaded(osu); @@ -184,7 +185,7 @@ namespace osu.Game.Tests.Beatmaps.IO zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); } - var importedSecondTime = await osu.Dependencies.Get().Import(temp); + var importedSecondTime = await osu.Dependencies.Get().Import(new ImportTask(temp)); ensureLoaded(osu); @@ -235,7 +236,7 @@ namespace osu.Game.Tests.Beatmaps.IO zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); } - var importedSecondTime = await osu.Dependencies.Get().Import(temp); + var importedSecondTime = await osu.Dependencies.Get().Import(new ImportTask(temp)); ensureLoaded(osu); @@ -351,7 +352,7 @@ namespace osu.Game.Tests.Beatmaps.IO // this will trigger purging of the existing beatmap (online set id match) but should rollback due to broken osu. try { - await manager.Import(breakTemp); + await manager.Import(new ImportTask(breakTemp)); } catch { @@ -614,7 +615,7 @@ namespace osu.Game.Tests.Beatmaps.IO zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); } - var imported = await osu.Dependencies.Get().Import(temp); + var imported = await osu.Dependencies.Get().Import(new ImportTask(temp)); ensureLoaded(osu); @@ -667,7 +668,7 @@ namespace osu.Game.Tests.Beatmaps.IO zip.SaveTo(temp, new ZipWriterOptions(CompressionType.Deflate)); } - var imported = await osu.Dependencies.Get().Import(temp); + var imported = await osu.Dependencies.Get().Import(new ImportTask(temp)); ensureLoaded(osu); @@ -821,7 +822,7 @@ namespace osu.Game.Tests.Beatmaps.IO var manager = osu.Dependencies.Get(); - var importedSet = await manager.Import(temp); + var importedSet = await manager.Import(new ImportTask(temp)); ensureLoaded(osu); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs index b34e027e9c..aaf3323432 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneMusicActionHandling.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Input.Bindings; using osu.Game.Overlays; using osu.Game.Tests.Resources; @@ -52,7 +53,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("import beatmap with track", () => { - var setWithTrack = Game.BeatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result; + var setWithTrack = Game.BeatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).Result; Beatmap.Value = Game.BeatmapManager.GetWorkingBeatmap(setWithTrack.Beatmaps.First()); }); diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs index 684ce10820..63bda08c88 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online ensureSoleilyRemoved(); createButtonWithBeatmap(createSoleily()); AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded); - AddStep("import soleily", () => beatmaps.Import(new[] { TestResources.GetTestBeatmapForImport() })); + AddStep("import soleily", () => beatmaps.Import(TestResources.GetTestBeatmapForImport())); AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineBeatmapSetID == 241526)); createButtonWithBeatmap(createSoleily()); AddAssert("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index e54292f7cc..81862448a8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -12,6 +12,7 @@ using osu.Framework.Platform; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Database; using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Leaderboards; @@ -83,7 +84,7 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); - beatmap = beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Result.Beatmaps[0]; + beatmap = beatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).Result.Beatmaps[0]; for (int i = 0; i < 50; i++) { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 8bdc804311..e18dc7f7eb 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -21,9 +21,7 @@ using osu.Game.IO; 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; namespace osu.Game.Database @@ -114,10 +112,19 @@ namespace osu.Game.Database PostNotification?.Invoke(notification); - return Import(notification, paths); + return Import(notification, paths.Select(p => new ImportTask(p)).ToArray()); } - protected async Task> Import(ProgressNotification notification, params string[] paths) + public Task Import(Stream stream, string filename) + { + var notification = new ProgressNotification { State = ProgressNotificationState.Active }; + + PostNotification?.Invoke(notification); + + return Import(notification, new ImportTask(stream, filename)); + } + + protected async Task> Import(ProgressNotification notification, params ImportTask[] tasks) { notification.Progress = 0; notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising..."; @@ -126,13 +133,13 @@ namespace osu.Game.Database var imported = new List(); - await Task.WhenAll(paths.Select(async path => + await Task.WhenAll(tasks.Select(async task => { notification.CancellationToken.ThrowIfCancellationRequested(); try { - var model = await Import(path, notification.CancellationToken); + var model = await Import(task, notification.CancellationToken); lock (imported) { @@ -140,8 +147,8 @@ namespace osu.Game.Database imported.Add(model); current++; - notification.Text = $"Imported {current} of {paths.Length} {HumanisedModelName}s"; - notification.Progress = (float)current / paths.Length; + notification.Text = $"Imported {current} of {tasks.Length} {HumanisedModelName}s"; + notification.Progress = (float)current / tasks.Length; } } catch (TaskCanceledException) @@ -150,7 +157,7 @@ namespace osu.Game.Database } catch (Exception e) { - Logger.Error(e, $@"Could not import ({Path.GetFileName(path)})", LoggingTarget.Database); + Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database); } })); @@ -183,16 +190,17 @@ namespace osu.Game.Database /// /// Import one from the filesystem and delete the file on success. + /// Note that this bypasses the UI flow and should only be used for special cases or testing. /// - /// The archive location on disk. + /// The archive location on disk. /// An optional cancellation token. /// The imported model, if successful. - public async Task Import(string path, CancellationToken cancellationToken = default) + internal async Task Import(ImportTask task, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); TModel import; - using (ArchiveReader reader = getReaderFrom(path)) + using (ArchiveReader reader = task.GetReader()) import = await Import(reader, cancellationToken); // We may or may not want to delete the file depending on where it is stored. @@ -201,12 +209,12 @@ namespace osu.Game.Database // TODO: Add a check to prevent files from storage to be deleted. try { - if (import != null && File.Exists(path) && ShouldDeleteArchive(path)) - File.Delete(path); + if (import != null && File.Exists(task.Path) && ShouldDeleteArchive(task.Path)) + File.Delete(task.Path); } catch (Exception e) { - LogForModel(import, $@"Could not delete original file after import ({Path.GetFileName(path)})", e); + LogForModel(import, $@"Could not delete original file after import ({task})", e); } return import; @@ -727,23 +735,6 @@ namespace osu.Game.Database protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}"; - /// - /// Creates an from a valid storage path. - /// - /// A file or folder path resolving the archive content. - /// A reader giving access to the archive's content. - private ArchiveReader getReaderFrom(string path) - { - if (ZipUtils.IsZipArchive(path)) - return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), Path.GetFileName(path)); - if (Directory.Exists(path)) - return new LegacyDirectoryArchiveReader(path); - if (File.Exists(path)) - return new LegacyFileArchiveReader(path); - - throw new InvalidFormatException($"{path} is not a valid archive"); - } - #region Event handling / delaying private readonly List queuedEvents = new List(); diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs index 8f469ca590..50b022f9ff 100644 --- a/osu.Game/Database/DownloadableArchiveModelManager.cs +++ b/osu.Game/Database/DownloadableArchiveModelManager.cs @@ -82,7 +82,7 @@ namespace osu.Game.Database Task.Factory.StartNew(async () => { // This gets scheduled back to the update thread, but we want the import to run in the background. - var imported = await Import(notification, filename); + var imported = await Import(notification, new ImportTask(filename)); // for now a failed import will be marked as a failed download for simplicity. if (!imported.Any()) diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index e4d92d957c..276c284c9f 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.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.Threading.Tasks; namespace osu.Game.Database @@ -17,6 +18,13 @@ namespace osu.Game.Database /// The files which should be imported. Task Import(params string[] paths); + /// + /// Import the provided stream as a simple item. + /// + /// The stream to import files from. Should be in a supported archive format. + /// The filename of the archive being imported. + Task Import(Stream stream, string filename); + /// /// An array of accepted file extensions (in the standard format of ".abc"). /// diff --git a/osu.Game/Database/ImportTask.cs b/osu.Game/Database/ImportTask.cs new file mode 100644 index 0000000000..89eb347df0 --- /dev/null +++ b/osu.Game/Database/ImportTask.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.IO; +using osu.Game.IO.Archives; +using osu.Game.Utils; +using SharpCompress.Common; + +namespace osu.Game.Database +{ + /// + /// An encapsulated import task to be imported to an . + /// + public class ImportTask + { + /// + /// The path to the file (or filename in the case a stream is provided). + /// + public string Path { get; } + + /// + /// An optional stream which provides the file content. + /// + public Stream Stream { get; } + + /// + /// Construct a new import task from a path (on a local filesystem). + /// + public ImportTask(string path) + { + Path = path; + } + + /// + /// Construct a new import task from a stream. + /// + public ImportTask(Stream stream, string filename) + { + Path = filename; + Stream = stream; + } + + /// + /// Retrieve an archive reader from this task. + /// + public ArchiveReader GetReader() + { + if (Stream != null) + return new ZipArchiveReader(Stream, Path); + + return getReaderFrom(Path); + } + + /// + /// Creates an from a valid storage path. + /// + /// A file or folder path resolving the archive content. + /// A reader giving access to the archive's content. + private ArchiveReader getReaderFrom(string path) + { + if (ZipUtils.IsZipArchive(path)) + return new ZipArchiveReader(File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read), System.IO.Path.GetFileName(path)); + if (Directory.Exists(path)) + return new LegacyDirectoryArchiveReader(path); + if (File.Exists(path)) + return new LegacyFileArchiveReader(path); + + throw new InvalidFormatException($"{path} is not a valid archive"); + } + + public override string ToString() => System.IO.Path.GetFileName(Path); + } +} diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e7b5d3304d..0fc2b8d1d7 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -395,6 +395,17 @@ namespace osu.Game } } + public async Task Import(Stream stream, string filename) + { + var extension = Path.GetExtension(filename)?.ToLowerInvariant(); + + foreach (var importer in fileImporters) + { + if (importer.HandledExtensions.Contains(extension)) + await importer.Import(stream, Path.GetFileNameWithoutExtension(filename)); + } + } + public IEnumerable HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 17ecfdd52e..0c957b80af 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.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.Linq; @@ -99,6 +100,8 @@ namespace osu.Game.Screens.Edit.Setup return Task.CompletedTask; } + Task ICanAcceptFiles.Import(Stream stream, string filename) => throw new NotImplementedException(); + protected override void LoadComplete() { base.LoadComplete(); From 12c6b3c1fb8df3431cc7b64db5aeda623c0a2687 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 7 Dec 2020 18:12:55 +0900 Subject: [PATCH 5157/6909] Pool catcher trail sprite --- .../UI/CatcherTrailDisplay.cs | 22 +++++++++++-------- .../UI/CatcherTrailSprite.cs | 18 ++++++++++++--- 2 files changed, 28 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs index f7e9fd19a7..fa65190032 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailDisplay.cs @@ -6,6 +6,7 @@ using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Framework.Graphics.Sprites; using osuTK; using osuTK.Graphics; @@ -20,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.UI { private readonly Catcher catcher; + private readonly DrawablePool trailPool; + private readonly Container dashTrails; private readonly Container hyperDashTrails; private readonly Container endGlowSprites; @@ -80,8 +83,9 @@ namespace osu.Game.Rulesets.Catch.UI RelativeSizeAxes = Axes.Both; - InternalChildren = new[] + InternalChildren = new Drawable[] { + trailPool = new DrawablePool(30), 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 }, @@ -118,14 +122,14 @@ namespace osu.Game.Rulesets.Catch.UI { 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 - }; + CatcherTrailSprite sprite = trailPool.Get(); + + sprite.Texture = texture; + sprite.Anchor = catcher.Anchor; + sprite.Scale = catcher.Scale; + sprite.Blending = BlendingParameters.Additive; + sprite.RelativePositionAxes = catcher.RelativePositionAxes; + sprite.Position = catcher.Position; target.Add(sprite); diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs index 56cb7dbfda..b3be18d46b 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs @@ -1,17 +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.Pooling; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osuTK; namespace osu.Game.Rulesets.Catch.UI { - public class CatcherTrailSprite : Sprite + public class CatcherTrailSprite : PoolableDrawable { - public CatcherTrailSprite(Texture texture) + public Texture Texture { - Texture = texture; + set => sprite.Texture = value; + } + + private readonly Sprite sprite; + + public CatcherTrailSprite() + { + InternalChild = sprite = new Sprite + { + RelativeSizeAxes = Axes.Both + }; Size = new Vector2(CatcherArea.CATCHER_SIZE); From 8eb1076fd7e276a74415ff98d9011703dcb6e0f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Dec 2020 18:18:14 +0900 Subject: [PATCH 5158/6909] Add test coverage --- .../Beatmaps/IO/ImportBeatmapTest.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 6e16058360..c32e359de6 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -69,6 +69,42 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public async Task TestImportThenDeleteFromStream() + { + // unfortunately for the time being we need to reference osu.Framework.Desktop for a game host here. + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) + { + try + { + var osu = LoadOsuIntoHost(host); + + var tempPath = TestResources.GetTestBeatmapForImport(); + + var manager = osu.Dependencies.Get(); + + BeatmapSetInfo importedSet; + + using (var stream = File.OpenRead(tempPath)) + { + importedSet = await manager.Import(new ImportTask(stream, Path.GetFileName(tempPath))); + ensureLoaded(osu); + } + + Assert.IsTrue(File.Exists(tempPath), "Stream source file somehow went missing"); + File.Delete(tempPath); + + var imported = manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + + deleteBeatmapSet(imported, osu); + } + finally + { + host.Exit(); + } + } + } + [Test] public async Task TestImportThenImport() { From fa658747631ab6c4f7176da1a2b5ea1f2cff125a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Dec 2020 21:09:38 +0900 Subject: [PATCH 5159/6909] Limit room name to 100 characters --- osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index 668a373d80..b8003b9774 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -135,6 +135,7 @@ namespace osu.Game.Screens.Multi.Match.Components { RelativeSizeAxes = Axes.X, TabbableContentContainer = this, + LengthLimit = 100 }, }, new Section("Duration") From eda6e1fbddb46b9eec3a33a1417581154645ff8a Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 7 Dec 2020 13:11:13 +0100 Subject: [PATCH 5160/6909] Add tournament switching in the UI --- osu.Game.Tournament/IO/TournamentStorage.cs | 18 ++++++++++++++++++ osu.Game.Tournament/Screens/SetupScreen.cs | 13 +++++++++++++ 2 files changed, 31 insertions(+) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 2e8a6ce667..66e27ddbb5 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.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 osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Platform; using osu.Game.IO; using System.IO; +using System.Collections.Generic; using osu.Game.Tournament.Configuration; namespace osu.Game.Tournament.IO @@ -13,12 +15,15 @@ namespace osu.Game.Tournament.IO { private const string default_tournament = "default"; private readonly Storage storage; + private readonly Storage allTournaments; private readonly TournamentStorageManager storageConfig; + public readonly Bindable CurrentTournament; public TournamentStorage(Storage storage) : base(storage.GetStorageForDirectory("tournaments"), string.Empty) { this.storage = storage; + allTournaments = UnderlyingStorage; storageConfig = new TournamentStorageManager(storage); @@ -29,9 +34,22 @@ namespace osu.Game.Tournament.IO else Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament)); + CurrentTournament = new Bindable(storageConfig.Get(StorageConfig.CurrentTournament)); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); + + CurrentTournament.BindValueChanged(updateTournament, false); } + private void updateTournament(ValueChangedEvent newTournament) + { + ChangeTargetStorage(allTournaments.GetStorageForDirectory(newTournament.NewValue)); + Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty)); + storageConfig.Set(StorageConfig.CurrentTournament, newTournament.NewValue); + storageConfig.Save(); + } + + public IEnumerable ListTournaments() => allTournaments.GetDirectories(string.Empty); + public override void Migrate(Storage newStorage) { // this migration only happens once on moving to the per-tournament storage system. diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index e78d3a9e83..9f34c36e64 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -9,8 +9,10 @@ using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Platform; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; +using osu.Game.Tournament.IO; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -40,6 +42,9 @@ namespace osu.Game.Tournament.Screens [Resolved] private RulesetStore rulesets { get; set; } + [Resolved] + private Storage storage { get; set; } + [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } @@ -70,6 +75,7 @@ namespace osu.Game.Tournament.Screens private void reload() { var fileBasedIpc = ipc as FileBasedIPC; + var tourneyStorage = storage as TournamentStorage; fillFlow.Children = new Drawable[] { new ActionableInfo @@ -111,6 +117,13 @@ namespace osu.Game.Tournament.Screens Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, + new LabelledDropdown + { + Label = "Current tournament", + Description = "Changes the background videos and bracket to match the selected tournament. This requires a restart after selecting to apply changes.", + Items = tourneyStorage?.ListTournaments(), + Current = tourneyStorage?.CurrentTournament, + }, resolution = new ResolutionSelector { Label = "Stream area resolution", From 191f863a494dcbdab5aabb92c0686ddded29fabf Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 7 Dec 2020 13:14:59 +0100 Subject: [PATCH 5161/6909] Remove unncessary words from the description --- 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 9f34c36e64..f22cd13cea 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tournament.Screens new LabelledDropdown { Label = "Current tournament", - Description = "Changes the background videos and bracket to match the selected tournament. This requires a restart after selecting to apply changes.", + Description = "Changes the background videos and bracket to match the selected tournament. This requires a restart to apply changes.", Items = tourneyStorage?.ListTournaments(), Current = tourneyStorage?.CurrentTournament, }, From 5be0672fe91eec0f766f8fd88f1434f41d22b7a3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Dec 2020 21:54:42 +0900 Subject: [PATCH 5162/6909] Split out enums --- .../Multi/Lounge/Components/FilterControl.cs | 18 ------------------ .../Lounge/Components/RoomCategoryFilter.cs | 12 ++++++++++++ .../Lounge/Components/RoomStatusFilter.cs | 17 +++++++++++++++++ 3 files changed, 29 insertions(+), 18 deletions(-) create mode 100644 osu.Game/Screens/Multi/Lounge/Components/RoomCategoryFilter.cs create mode 100644 osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index 3fc1359006..2afadc6536 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.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.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -71,21 +70,4 @@ namespace osu.Game.Screens.Multi.Lounge.Components }; } } - - public enum RoomStatusFilter - { - Open, - - [Description("Recently Ended")] - Ended, - Participated, - Owned, - } - - public enum RoomCategoryFilter - { - Any, - Normal, - Spotlight - } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomCategoryFilter.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomCategoryFilter.cs new file mode 100644 index 0000000000..c076905174 --- /dev/null +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomCategoryFilter.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.Screens.Multi.Lounge.Components +{ + public enum RoomCategoryFilter + { + Any, + Normal, + Spotlight + } +} diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.cs new file mode 100644 index 0000000000..9da938ac8b --- /dev/null +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.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.ComponentModel; + +namespace osu.Game.Screens.Multi.Lounge.Components +{ + public enum RoomStatusFilter + { + Open, + + [Description("Recently Ended")] + Ended, + Participated, + Owned, + } +} From 1b3836aeef6c531f59035a3ff98d5bf1ad9273aa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Dec 2020 21:59:26 +0900 Subject: [PATCH 5163/6909] Re-implement multiplayer FilterControl --- .../Multi/Lounge/Components/FilterControl.cs | 110 ++++++++++++++---- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 8 +- 2 files changed, 89 insertions(+), 29 deletions(-) diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index 2afadc6536..032c50aa73 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -3,21 +3,21 @@ 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.Threading; -using osu.Game.Overlays.SearchableList; +using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osuTK.Graphics; namespace osu.Game.Screens.Multi.Lounge.Components { - public class FilterControl : SearchableListFilterControl + public abstract class FilterControl : CompositeDrawable { - protected override Color4 BackgroundColour => Color4.Black.Opacity(0.5f); - protected override RoomStatusFilter DefaultTab => RoomStatusFilter.Open; - protected override RoomCategoryFilter DefaultCategory => RoomCategoryFilter.Any; - - protected override float ContentHorizontalPadding => base.ContentHorizontalPadding + OsuScreen.HORIZONTAL_OVERFLOW_PADDING; + protected const float VERTICAL_PADDING = 10; + protected const float HORIZONTAL_PADDING = 80; [Resolved(CanBeNull = true)] private Bindable filter { get; set; } @@ -25,49 +25,109 @@ namespace osu.Game.Screens.Multi.Lounge.Components [Resolved] private IBindable ruleset { get; set; } + private readonly Box tabStrip; + private readonly SearchTextBox search; + private readonly PageTabControl tabs; + public FilterControl() { - DisplayStyleControl.Hide(); + InternalChildren = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4.Black, + Alpha = 0.25f, + }, + tabStrip = new Box + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = 1, + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Top = VERTICAL_PADDING, + Horizontal = HORIZONTAL_PADDING + }, + Children = new Drawable[] + { + search = new FilterSearchTextBox + { + RelativeSizeAxes = Axes.X, + }, + tabs = new PageTabControl + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + }, + } + } + }; + + tabs.Current.Value = RoomStatusFilter.Open; + tabs.Current.TriggerChange(); } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { filter ??= new Bindable(); + tabStrip.Colour = colours.Yellow; } protected override void LoadComplete() { base.LoadComplete(); - ruleset.BindValueChanged(_ => updateFilter()); - Search.Current.BindValueChanged(_ => scheduleUpdateFilter()); - Dropdown.Current.BindValueChanged(_ => updateFilter()); - Tabs.Current.BindValueChanged(_ => updateFilter(), true); + search.Current.BindValueChanged(_ => updateFilterDebounced()); + ruleset.BindValueChanged(_ => UpdateFilter()); + tabs.Current.BindValueChanged(_ => UpdateFilter(), true); } private ScheduledDelegate scheduledFilterUpdate; - private void scheduleUpdateFilter() + private void updateFilterDebounced() { scheduledFilterUpdate?.Cancel(); - scheduledFilterUpdate = Scheduler.AddDelayed(updateFilter, 200); + scheduledFilterUpdate = Scheduler.AddDelayed(UpdateFilter, 200); } - private void updateFilter() + protected void UpdateFilter() { scheduledFilterUpdate?.Cancel(); - if (filter == null) - return; + var criteria = CreateCriteria(); + criteria.SearchString = search.Current.Value; + criteria.Status = tabs.Current.Value; + criteria.Ruleset = ruleset.Value; - filter.Value = new FilterCriteria + filter.Value = criteria; + } + + protected virtual FilterCriteria CreateCriteria() => new FilterCriteria(); + + public bool HoldFocus + { + get => search.HoldFocus; + set => search.HoldFocus = value; + } + + public void TakeFocus() => search.TakeFocus(); + + private class FilterSearchTextBox : SearchTextBox + { + [BackgroundDependencyLoader] + private void load() { - SearchString = Search.Current.Value ?? string.Empty, - StatusFilter = Tabs.Current.Value, - RoomCategoryFilter = Dropdown.Current.Value, - Ruleset = ruleset.Value - }; + BackgroundUnfocused = OsuColour.Gray(0.06f); + BackgroundFocused = OsuColour.Gray(0.12f); + } } } } diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index 4dc9ba549b..b33a79772e 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Multi.Lounge protected override void OnFocus(FocusEvent e) { - Filter.Search.TakeFocus(); + Filter.TakeFocus(); } public override void OnEntering(IScreen last) @@ -136,19 +136,19 @@ namespace osu.Game.Screens.Multi.Lounge private void onReturning() { - Filter.Search.HoldFocus = true; + Filter.HoldFocus = true; } public override bool OnExiting(IScreen next) { - Filter.Search.HoldFocus = false; + Filter.HoldFocus = false; return base.OnExiting(next); } public override void OnSuspending(IScreen next) { base.OnSuspending(next); - Filter.Search.HoldFocus = false; + Filter.HoldFocus = false; } private void joinRequested(Room room) From f599427080ed04c6968134a2e78c6239c1d86010 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Dec 2020 21:59:38 +0900 Subject: [PATCH 5164/6909] Implement TimeshiftFilterControl --- ....cs => TestSceneTimeshiftFilterControl.cs} | 11 ++-- .../Online/Multiplayer/GetRoomsRequest.cs | 18 +++--- .../Multi/Lounge/Components/FilterCriteria.cs | 4 +- .../Lounge/Components/RoomCategoryFilter.cs | 12 ---- .../Components/TimeshiftFilterControl.cs | 59 +++++++++++++++++++ .../Screens/Multi/Lounge/LoungeSubScreen.cs | 6 +- osu.Game/Screens/Multi/RoomManager.cs | 2 +- 7 files changed, 83 insertions(+), 29 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneLoungeFilterControl.cs => TestSceneTimeshiftFilterControl.cs} (55%) delete mode 100644 osu.Game/Screens/Multi/Lounge/Components/RoomCategoryFilter.cs create mode 100644 osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeFilterControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftFilterControl.cs similarity index 55% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeFilterControl.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftFilterControl.cs index 7c0c2797f5..f635a28b5c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeFilterControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftFilterControl.cs @@ -6,14 +6,17 @@ using osu.Game.Screens.Multi.Lounge.Components; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneLoungeFilterControl : OsuTestScene + public class TestSceneTimeshiftFilterControl : OsuTestScene { - public TestSceneLoungeFilterControl() + public TestSceneTimeshiftFilterControl() { - Child = new FilterControl + Child = new TimeshiftFilterControl { Anchor = Anchor.Centre, - Origin = Anchor.Centre + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + Width = 0.7f, + Height = 80, }; } } diff --git a/osu.Game/Online/Multiplayer/GetRoomsRequest.cs b/osu.Game/Online/Multiplayer/GetRoomsRequest.cs index 64e0386f77..a0609f77dd 100644 --- a/osu.Game/Online/Multiplayer/GetRoomsRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomsRequest.cs @@ -11,24 +11,24 @@ namespace osu.Game.Online.Multiplayer { public class GetRoomsRequest : APIRequest> { - private readonly RoomStatusFilter statusFilter; - private readonly RoomCategoryFilter categoryFilter; + private readonly RoomStatusFilter status; + private readonly string category; - public GetRoomsRequest(RoomStatusFilter statusFilter, RoomCategoryFilter categoryFilter) + public GetRoomsRequest(RoomStatusFilter status, string category) { - this.statusFilter = statusFilter; - this.categoryFilter = categoryFilter; + this.status = status; + this.category = category; } protected override WebRequest CreateWebRequest() { var req = base.CreateWebRequest(); - if (statusFilter != RoomStatusFilter.Open) - req.AddParameter("mode", statusFilter.ToString().Underscore().ToLowerInvariant()); + if (status != RoomStatusFilter.Open) + req.AddParameter("mode", status.ToString().Underscore().ToLowerInvariant()); - if (categoryFilter != RoomCategoryFilter.Any) - req.AddParameter("category", categoryFilter.ToString().Underscore().ToLowerInvariant()); + if (!string.IsNullOrEmpty(category)) + req.AddParameter("category", category); return req; } diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs index 6d70225eec..7b04be86b1 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 RoomStatusFilter StatusFilter; - public RoomCategoryFilter RoomCategoryFilter; + public RoomStatusFilter Status; + public string Category; public RulesetInfo Ruleset; } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomCategoryFilter.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomCategoryFilter.cs deleted file mode 100644 index c076905174..0000000000 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomCategoryFilter.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. - -namespace osu.Game.Screens.Multi.Lounge.Components -{ - public enum RoomCategoryFilter - { - Any, - Normal, - Spotlight - } -} diff --git a/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs new file mode 100644 index 0000000000..a2ea104c74 --- /dev/null +++ b/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs @@ -0,0 +1,59 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Overlays.SearchableList; + +namespace osu.Game.Screens.Multi.Lounge.Components +{ + public class TimeshiftFilterControl : FilterControl + { + private readonly Dropdown dropdown; + + public TimeshiftFilterControl() + { + AddInternal(dropdown = new SlimEnumDropdown + { + Anchor = Anchor.BottomRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.None, + Width = 160, + X = -HORIZONTAL_PADDING, + Y = -30 + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + dropdown.Current.BindValueChanged(_ => UpdateFilter()); + } + + protected override FilterCriteria CreateCriteria() + { + var criteria = base.CreateCriteria(); + + switch (dropdown.Current.Value) + { + case TimeshiftCategory.Normal: + criteria.Category = "normal"; + break; + + case TimeshiftCategory.Spotlight: + criteria.Category = "spotlight"; + break; + } + + return criteria; + } + + private enum TimeshiftCategory + { + Any, + Normal, + Spotlight + } + } +} diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index b33a79772e..a26a64d86d 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -48,7 +48,6 @@ namespace osu.Game.Screens.Multi.Lounge InternalChildren = new Drawable[] { - Filter = new FilterControl { Depth = -1 }, content = new Container { RelativeSizeAxes = Axes.Both, @@ -79,6 +78,11 @@ namespace osu.Game.Screens.Multi.Lounge }, }, }, + Filter = new TimeshiftFilterControl + { + RelativeSizeAxes = Axes.X, + Height = 80, + }, }; // scroll selected room into view on selection. diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs index 2a96fa536d..fb0cf73bb9 100644 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ b/osu.Game/Screens/Multi/RoomManager.cs @@ -317,7 +317,7 @@ namespace osu.Game.Screens.Multi var tcs = new TaskCompletionSource(); pollReq?.Cancel(); - pollReq = new GetRoomsRequest(currentFilter.Value.StatusFilter, currentFilter.Value.RoomCategoryFilter); + pollReq = new GetRoomsRequest(currentFilter.Value.Status, currentFilter.Value.Category); pollReq.Success += result => { From 34f36727509e6b108564c06714f2252505a5bf3a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Dec 2020 22:04:41 +0900 Subject: [PATCH 5165/6909] Make ctor protected --- osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index 032c50aa73..896c215c42 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components private readonly SearchTextBox search; private readonly PageTabControl tabs; - public FilterControl() + protected FilterControl() { InternalChildren = new Drawable[] { From 10c1823534cd9e873997965f81fbb72e5dc1b6fa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 7 Dec 2020 22:07:11 +0900 Subject: [PATCH 5166/6909] Remove now unused files --- .../UserInterface}/SlimEnumDropdown.cs | 5 +- .../SearchableList/DisplayStyleControl.cs | 84 --------- .../SearchableList/HeaderTabControl.cs | 29 --- .../SearchableListFilterControl.cs | 165 ------------------ .../Components/TimeshiftFilterControl.cs | 2 +- 5 files changed, 3 insertions(+), 282 deletions(-) rename osu.Game/{Overlays/SearchableList => Graphics/UserInterface}/SlimEnumDropdown.cs (94%) delete mode 100644 osu.Game/Overlays/SearchableList/DisplayStyleControl.cs delete mode 100644 osu.Game/Overlays/SearchableList/HeaderTabControl.cs delete mode 100644 osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs diff --git a/osu.Game/Overlays/SearchableList/SlimEnumDropdown.cs b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs similarity index 94% rename from osu.Game/Overlays/SearchableList/SlimEnumDropdown.cs rename to osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs index 9e7ff1205f..965734792c 100644 --- a/osu.Game/Overlays/SearchableList/SlimEnumDropdown.cs +++ b/osu.Game/Graphics/UserInterface/SlimEnumDropdown.cs @@ -2,14 +2,13 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.UserInterface; using osuTK; +using osuTK.Graphics; -namespace osu.Game.Overlays.SearchableList +namespace osu.Game.Graphics.UserInterface { public class SlimEnumDropdown : OsuEnumDropdown where T : struct, Enum diff --git a/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs b/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs deleted file mode 100644 index ffbc1c9586..0000000000 --- a/osu.Game/Overlays/SearchableList/DisplayStyleControl.cs +++ /dev/null @@ -1,84 +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 osuTK; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics.Containers; - -namespace osu.Game.Overlays.SearchableList -{ - public class DisplayStyleControl : CompositeDrawable - { - public readonly Bindable DisplayStyle = new Bindable(); - - public DisplayStyleControl() - { - AutoSizeAxes = Axes.Both; - - InternalChild = 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), - }, - }; - - DisplayStyle.Value = PanelDisplayStyle.Grid; - } - - private class DisplayStyleToggleButton : OsuClickableContainer - { - private readonly SpriteIcon icon; - private readonly PanelDisplayStyle style; - private readonly Bindable bindable; - - public DisplayStyleToggleButton(IconUsage icon, PanelDisplayStyle style, Bindable bindable) - { - this.bindable = bindable; - this.style = style; - Size = new Vector2(25f); - - Children = new Drawable[] - { - this.icon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Icon = icon, - Size = new Vector2(18), - Alpha = 0.5f, - }, - }; - - bindable.ValueChanged += Bindable_ValueChanged; - Bindable_ValueChanged(new ValueChangedEvent(bindable.Value, bindable.Value)); - Action = () => bindable.Value = this.style; - } - - private void Bindable_ValueChanged(ValueChangedEvent e) - { - icon.FadeTo(e.NewValue == style ? 1.0f : 0.5f, 100); - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - bindable.ValueChanged -= Bindable_ValueChanged; - } - } - } - - public enum PanelDisplayStyle - { - Grid, - List, - } -} diff --git a/osu.Game/Overlays/SearchableList/HeaderTabControl.cs b/osu.Game/Overlays/SearchableList/HeaderTabControl.cs deleted file mode 100644 index 2087a72c54..0000000000 --- a/osu.Game/Overlays/SearchableList/HeaderTabControl.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 osuTK.Graphics; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Graphics.UserInterface; - -namespace osu.Game.Overlays.SearchableList -{ - public class HeaderTabControl : OsuTabControl - { - protected override TabItem CreateTabItem(T value) => new HeaderTabItem(value); - - public HeaderTabControl() - { - Height = 26; - AccentColour = Color4.White; - } - - private class HeaderTabItem : OsuTabItem - { - public HeaderTabItem(T value) - : base(value) - { - Text.Font = Text.Font.With(size: 16); - } - } - } -} diff --git a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs b/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs deleted file mode 100644 index 1990674aa9..0000000000 --- a/osu.Game/Overlays/SearchableList/SearchableListFilterControl.cs +++ /dev/null @@ -1,165 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osuTK.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.UserInterface; -using osu.Framework.Graphics.Shapes; - -namespace osu.Game.Overlays.SearchableList -{ - public abstract class SearchableListFilterControl : Container - where TTab : struct, Enum - where TCategory : struct, Enum - { - private const float padding = 10; - - private readonly Drawable filterContainer; - private readonly Drawable rightFilterContainer; - private readonly Box tabStrip; - - public readonly SearchTextBox Search; - public readonly PageTabControl Tabs; - public readonly SlimEnumDropdown Dropdown; - public readonly DisplayStyleControl DisplayStyleControl; - - protected abstract Color4 BackgroundColour { get; } - protected abstract TTab DefaultTab { get; } - protected abstract TCategory DefaultCategory { get; } - protected virtual Drawable CreateSupplementaryControls() => null; - - /// - /// The amount of padding added to content (does not affect background or tab control strip). - /// - protected virtual float ContentHorizontalPadding => WaveOverlayContainer.WIDTH_PADDING; - - protected SearchableListFilterControl() - { - RelativeSizeAxes = Axes.X; - - var controls = CreateSupplementaryControls(); - Container controlsContainer; - Children = new[] - { - filterContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = BackgroundColour, - Alpha = 0.9f, - }, - tabStrip = new Box - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = 1, - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Top = padding, - Horizontal = ContentHorizontalPadding - }, - Children = new Drawable[] - { - Search = new FilterSearchTextBox - { - RelativeSizeAxes = Axes.X, - }, - controlsContainer = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Margin = new MarginPadding { Top = controls != null ? padding : 0 }, - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Right = 225 }, - Child = Tabs = new PageTabControl - { - RelativeSizeAxes = Axes.X, - }, - }, - new Box // keep the tab strip part of autosize, but don't put it in the flow container - { - RelativeSizeAxes = Axes.X, - Height = 1, - Colour = Color4.White.Opacity(0), - }, - }, - }, - }, - }, - 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 }; - - Tabs.Current.Value = DefaultTab; - Tabs.Current.TriggerChange(); - - Dropdown.Current.Value = DefaultCategory; - Dropdown.Current.TriggerChange(); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - tabStrip.Colour = colours.Yellow; - } - - protected override void Update() - { - base.Update(); - - Height = filterContainer.Height; - rightFilterContainer.Margin = new MarginPadding { Top = filterContainer.Height - 30, Right = ContentHorizontalPadding }; - } - - private class FilterSearchTextBox : SearchTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = OsuColour.Gray(0.06f); - BackgroundFocused = OsuColour.Gray(0.12f); - } - } - } -} diff --git a/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs index a2ea104c74..68cab283a0 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs @@ -3,7 +3,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; -using osu.Game.Overlays.SearchableList; +using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Multi.Lounge.Components { From c778646f101d9f73709c5b76ae339b4ffeaef7c2 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 7 Dec 2020 18:01:57 +0100 Subject: [PATCH 5167/6909] Add support for importing replay files. --- 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 a56206c969..541455277d 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -13,7 +13,7 @@ using osu.Framework.Android; namespace osu.Android { [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] - [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] + [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk", ".*\\.osr" }, DataMimeType = "application/*")] public class OsuGameActivity : AndroidGameActivity { private OsuGameAndroid game; From f980f413244c3252890868df1f32c2f1c88d3f81 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 7 Dec 2020 21:38:15 +0100 Subject: [PATCH 5168/6909] Address review --- osu.Game.Tournament/IO/TournamentStorage.cs | 6 ++---- osu.Game.Tournament/Screens/SetupScreen.cs | 12 +++++------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index 66e27ddbb5..cf54c1f5ef 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -34,18 +34,16 @@ namespace osu.Game.Tournament.IO else Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament)); - CurrentTournament = new Bindable(storageConfig.Get(StorageConfig.CurrentTournament)); + CurrentTournament = storageConfig.GetBindable(StorageConfig.CurrentTournament); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); - CurrentTournament.BindValueChanged(updateTournament, false); + CurrentTournament.BindValueChanged(updateTournament); } private void updateTournament(ValueChangedEvent newTournament) { ChangeTargetStorage(allTournaments.GetStorageForDirectory(newTournament.NewValue)); Logger.Log("Changing tournament storage: " + GetFullPath(string.Empty)); - storageConfig.Set(StorageConfig.CurrentTournament, newTournament.NewValue); - storageConfig.Save(); } public IEnumerable ListTournaments() => allTournaments.GetDirectories(string.Empty); diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index f22cd13cea..3c9d3c949b 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -42,16 +42,14 @@ namespace osu.Game.Tournament.Screens [Resolved] private RulesetStore rulesets { get; set; } - [Resolved] - private Storage storage { get; set; } - [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } private Bindable windowSize; + private TournamentStorage storage; [BackgroundDependencyLoader] - private void load(FrameworkConfigManager frameworkConfig) + private void load(FrameworkConfigManager frameworkConfig, Storage storage) { windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); @@ -66,6 +64,7 @@ namespace osu.Game.Tournament.Screens api.LocalUser.BindValueChanged(_ => Schedule(reload)); stableInfo.OnStableInfoSaved += () => Schedule(reload); + this.storage = (TournamentStorage)storage; reload(); } @@ -75,7 +74,6 @@ namespace osu.Game.Tournament.Screens private void reload() { var fileBasedIpc = ipc as FileBasedIPC; - var tourneyStorage = storage as TournamentStorage; fillFlow.Children = new Drawable[] { new ActionableInfo @@ -121,8 +119,8 @@ namespace osu.Game.Tournament.Screens { Label = "Current tournament", Description = "Changes the background videos and bracket to match the selected tournament. This requires a restart to apply changes.", - Items = tourneyStorage?.ListTournaments(), - Current = tourneyStorage?.CurrentTournament, + Items = storage.ListTournaments(), + Current = storage.CurrentTournament, }, resolution = new ResolutionSelector { From 6002014f952ee71992f54c267ed4c9967e5d5639 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 7 Dec 2020 22:07:54 +0100 Subject: [PATCH 5169/6909] Change underlyingstorage to alltournaments for clarity --- osu.Game.Tournament/IO/TournamentStorage.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/IO/TournamentStorage.cs b/osu.Game.Tournament/IO/TournamentStorage.cs index cf54c1f5ef..2ba1b6be8f 100644 --- a/osu.Game.Tournament/IO/TournamentStorage.cs +++ b/osu.Game.Tournament/IO/TournamentStorage.cs @@ -29,10 +29,10 @@ namespace osu.Game.Tournament.IO if (storage.Exists("tournament.ini")) { - ChangeTargetStorage(UnderlyingStorage.GetStorageForDirectory(storageConfig.Get(StorageConfig.CurrentTournament))); + ChangeTargetStorage(allTournaments.GetStorageForDirectory(storageConfig.Get(StorageConfig.CurrentTournament))); } else - Migrate(UnderlyingStorage.GetStorageForDirectory(default_tournament)); + Migrate(allTournaments.GetStorageForDirectory(default_tournament)); CurrentTournament = storageConfig.GetBindable(StorageConfig.CurrentTournament); Logger.Log("Using tournament storage: " + GetFullPath(string.Empty)); From 3cbdaf5960a992e21b4fff30ad8660987edf005a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 10:30:23 +0900 Subject: [PATCH 5170/6909] Make resolved properties protected --- .../Skinning/Default/CatchHitObjectPiece.cs | 12 ++++++------ .../Skinning/Default/FruitPiece.cs | 7 +++---- .../Skinning/LegacyCatchHitObjectPiece.cs | 16 +++++++--------- 3 files changed, 16 insertions(+), 19 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs index 3c3cb5b0ee..be6cf0fc05 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs @@ -18,10 +18,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default public readonly Bindable HyperDash = new Bindable(); [Resolved(canBeNull: true)] - private DrawableHitObject drawableHitObject { get; set; } - [CanBeNull] - protected DrawablePalpableCatchHitObject DrawableHitObject => (DrawablePalpableCatchHitObject)drawableHitObject; + protected DrawableHitObject DrawableHitObject { get; private set; } [CanBeNull] protected BorderPiece BorderPiece; @@ -33,10 +31,12 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default { base.LoadComplete(); - if (DrawableHitObject != null) + var hitObject = (DrawablePalpableCatchHitObject)DrawableHitObject; + + if (hitObject != null) { - AccentColour.BindTo(DrawableHitObject.AccentColour); - HyperDash.BindTo(DrawableHitObject.HyperDash); + AccentColour.BindTo(hitObject.AccentColour); + HyperDash.BindTo(hitObject.HyperDash); } HyperDash.BindValueChanged(hyper => diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs index 05b924eb75..45d688c4e5 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs @@ -36,11 +36,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default { base.LoadComplete(); - if (DrawableHitObject != null) - { - var fruit = (DrawableFruit)DrawableHitObject; + var fruit = (DrawableFruit)DrawableHitObject; + + if (fruit != null) VisualRepresentation.BindTo(fruit.VisualRepresentation); - } } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs index 4bcb92b9be..1e68439402 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs @@ -27,15 +27,11 @@ namespace osu.Game.Rulesets.Catch.Skinning private readonly Sprite hyperSprite; [Resolved] - private ISkinSource skin { get; set; } - - protected ISkinSource Skin => skin; + protected ISkinSource Skin { get; private set; } [Resolved(canBeNull: true)] - private DrawableHitObject drawableHitObject { get; set; } - [CanBeNull] - protected DrawablePalpableCatchHitObject DrawableHitObject => (DrawablePalpableCatchHitObject)drawableHitObject; + protected DrawableHitObject DrawableHitObject { get; private set; } protected LegacyCatchHitObjectPiece() { @@ -69,10 +65,12 @@ namespace osu.Game.Rulesets.Catch.Skinning { base.LoadComplete(); - if (DrawableHitObject != null) + var hitObject = (DrawablePalpableCatchHitObject)DrawableHitObject; + + if (hitObject != null) { - AccentColour.BindTo(DrawableHitObject.AccentColour); - HyperDash.BindTo(DrawableHitObject.HyperDash); + AccentColour.BindTo(hitObject.AccentColour); + HyperDash.BindTo(hitObject.HyperDash); } hyperSprite.Colour = Skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ?? From 4da6717d0e3ececba5dc73a36326a1bb92e4ea26 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 10:32:46 +0900 Subject: [PATCH 5171/6909] Rename things in PulpFormation --- .../Skinning/Default/BananaPulpFormation.cs | 4 +-- .../Skinning/Default/FruitPulpFormation.cs | 36 +++++++++---------- .../Skinning/Default/PulpFormation.cs | 15 ++++---- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/BananaPulpFormation.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPulpFormation.cs index cabea46083..ee1cc68f7d 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/BananaPulpFormation.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPulpFormation.cs @@ -9,8 +9,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default { public BananaPulpFormation() { - Add(new Vector2(0, -0.3f), new Vector2(SMALL_PULP)); - Add(new Vector2(0, 0.05f), new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f)); + AddPulp(new Vector2(0, -0.3f), new Vector2(SMALL_PULP)); + AddPulp(new Vector2(0, 0.05f), new Vector2(LARGE_PULP_4 * 0.8f, LARGE_PULP_4 * 2.5f)); } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs index 8696854f23..88e0b5133a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPulpFormation.cs @@ -25,33 +25,33 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default switch (visualRepresentation.NewValue) { case FruitVisualRepresentation.Pear: - Add(new Vector2(0, -0.33f), new Vector2(SMALL_PULP)); - Add(PositionAt(60, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); - Add(PositionAt(180, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); - Add(PositionAt(300, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + AddPulp(new Vector2(0, -0.33f), new Vector2(SMALL_PULP)); + AddPulp(PositionAt(60, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + AddPulp(PositionAt(180, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + AddPulp(PositionAt(300, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); break; case FruitVisualRepresentation.Grape: - Add(new Vector2(0, -0.25f), new Vector2(SMALL_PULP)); - Add(PositionAt(0, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); - Add(PositionAt(120, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); - Add(PositionAt(240, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + AddPulp(new Vector2(0, -0.25f), new Vector2(SMALL_PULP)); + AddPulp(PositionAt(0, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + AddPulp(PositionAt(120, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); + AddPulp(PositionAt(240, DISTANCE_FROM_CENTRE_3), new Vector2(LARGE_PULP_3)); break; case FruitVisualRepresentation.Pineapple: - Add(new Vector2(0, -0.3f), new Vector2(SMALL_PULP)); - Add(PositionAt(45, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); - Add(PositionAt(135, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); - Add(PositionAt(225, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); - Add(PositionAt(315, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + AddPulp(new Vector2(0, -0.3f), new Vector2(SMALL_PULP)); + AddPulp(PositionAt(45, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + AddPulp(PositionAt(135, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + AddPulp(PositionAt(225, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + AddPulp(PositionAt(315, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); break; case FruitVisualRepresentation.Raspberry: - Add(new Vector2(0, -0.34f), new Vector2(SMALL_PULP)); - Add(PositionAt(0, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); - Add(PositionAt(90, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); - Add(PositionAt(180, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); - Add(PositionAt(270, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + AddPulp(new Vector2(0, -0.34f), new Vector2(SMALL_PULP)); + AddPulp(PositionAt(0, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + AddPulp(PositionAt(90, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + AddPulp(PositionAt(180, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); + AddPulp(PositionAt(270, DISTANCE_FROM_CENTRE_4), new Vector2(LARGE_PULP_4)); break; } } diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs b/osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs index c0e3d0e724..8753aa4077 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/PulpFormation.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default protected const float SMALL_PULP = LARGE_PULP_3 / 2; - private int numPulps; + private int pulpsInUse; protected PulpFormation() { @@ -35,21 +35,22 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default protected void Clear() { - for (; numPulps > 0; numPulps--) - InternalChildren[numPulps - 1].Alpha = 0; + for (int i = 0; i < pulpsInUse; i++) + InternalChildren[i].Alpha = 0; + pulpsInUse = 0; } - protected void Add(Vector2 position, Vector2 size) + protected void AddPulp(Vector2 position, Vector2 size) { - if (numPulps == InternalChildren.Count) + if (pulpsInUse == InternalChildren.Count) AddInternal(new Pulp { AccentColour = { BindTarget = AccentColour } }); - var pulp = InternalChildren[numPulps]; + var pulp = InternalChildren[pulpsInUse]; pulp.Position = position; pulp.Size = size; pulp.Alpha = 1; - numPulps++; + pulpsInUse++; } } } From 57b7ef88e2f85e7e9e5073a7ece9acc043b7c246 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Dec 2020 12:12:53 +0900 Subject: [PATCH 5172/6909] Fix spacing --- 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 3f0b0dcc71..0ebe0ddc2d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -101,9 +101,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty double approachRateFactor = 0.0; if (Attributes.ApproachRate > 10.33) - approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33); + approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33); else if (Attributes.ApproachRate < 8.0) - approachRateFactor += 0.1 * (8.0 - Attributes.ApproachRate); + approachRateFactor += 0.1 * (8.0 - Attributes.ApproachRate); aimValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0)); From 77279a7e56154ae3796444403b561e4023a3183c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 12:48:59 +0900 Subject: [PATCH 5173/6909] Update stale xmldoc on import method MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Database/ArchiveModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index e18dc7f7eb..36cc4cce39 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -192,7 +192,7 @@ namespace osu.Game.Database /// Import one from the filesystem and delete the file on success. /// Note that this bypasses the UI flow and should only be used for special cases or testing. /// - /// The archive location on disk. + /// The containing data about the to import. /// An optional cancellation token. /// The imported model, if successful. internal async Task Import(ImportTask task, CancellationToken cancellationToken = default) From 58d7e4197809c986c62389e52b90e3465bf8b225 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 12:52:34 +0900 Subject: [PATCH 5174/6909] Enable nullable on ImportTask --- osu.Game/Database/ImportTask.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/ImportTask.cs b/osu.Game/Database/ImportTask.cs index 89eb347df0..1433a567a9 100644 --- a/osu.Game/Database/ImportTask.cs +++ b/osu.Game/Database/ImportTask.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. +#nullable enable + using System.IO; using osu.Game.IO.Archives; using osu.Game.Utils; @@ -21,7 +23,7 @@ namespace osu.Game.Database /// /// An optional stream which provides the file content. /// - public Stream Stream { get; } + public Stream? Stream { get; } /// /// Construct a new import task from a path (on a local filesystem). From 0213f77b4b4c43e09c5c5e7e4229f0dd4af500f5 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 14:28:30 +0900 Subject: [PATCH 5175/6909] Move catcher state changing logic to OnNewResult method --- .../TestSceneCatcher.cs | 31 +++++++++++++- .../TestSceneCatcherArea.cs | 3 +- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 41 +++++++++++-------- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 11 +++-- 5 files changed, 60 insertions(+), 28 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 194a12a9b7..d97c56164e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.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; @@ -12,8 +13,11 @@ using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; +using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests @@ -169,7 +173,32 @@ namespace osu.Game.Rulesets.Catch.Tests hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); for (var i = 0; i < count; i++) - catcher.AttemptCatch(hitObject); + { + var drawableObject = createDrawableObject(hitObject); + var result = new JudgementResult(hitObject, new CatchJudgement()) + { + Type = catcher.CanCatch(hitObject) ? HitResult.Great : HitResult.Miss + }; + catcher.OnNewResult(drawableObject, result); + } + } + + private DrawableCatchHitObject createDrawableObject(CatchHitObject hitObject) + { + switch (hitObject) + { + case Banana banana: + return new DrawableBanana(banana); + + case Droplet droplet: + return new DrawableDroplet(droplet); + + case Fruit fruit: + return new DrawableFruit(fruit); + + default: + throw new ArgumentOutOfRangeException(nameof(hitObject)); + } } public class TestCatcher : Catcher diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 281ddc7eaa..c8826aa174 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -58,10 +58,9 @@ namespace osu.Game.Rulesets.Catch.Tests Schedule(() => { - bool caught = area.AttemptCatch(fruit); area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) { - Type = caught ? HitResult.Great : HitResult.Miss + Type = area.MovableCatcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss }); drawable.Expire(); diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index df87359ed6..fdc12bf088 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -81,7 +81,7 @@ namespace osu.Game.Rulesets.Catch.UI ((DrawableCatchHitObject)d).CheckPosition = checkIfWeCanCatch; } - private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.AttemptCatch(obj); + private bool checkIfWeCanCatch(CatchHitObject obj) => CatcherArea.MovableCatcher.CanCatch(obj); private void onNewResult(DrawableHitObject judgedObject, JudgementResult result) => CatcherArea.OnNewResult((DrawableCatchHitObject)judgedObject, result); diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 2a3447c80a..8998dbf488 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -17,6 +17,7 @@ using osu.Game.Configuration; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Skinning; +using osu.Game.Rulesets.Judgements; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -190,11 +191,9 @@ namespace osu.Game.Rulesets.Catch.UI internal static float CalculateCatchWidth(BeatmapDifficulty difficulty) => CalculateCatchWidth(calculateScale(difficulty)); /// - /// Let the catcher attempt to catch a fruit. + /// Determine if this catcher can catch a in the current position. /// - /// The fruit to catch. - /// Whether the catch is possible. - public bool AttemptCatch(CatchHitObject hitObject) + public bool CanCatch(CatchHitObject hitObject) { if (!(hitObject is PalpableCatchHitObject fruit)) return false; @@ -205,21 +204,25 @@ namespace osu.Game.Rulesets.Catch.UI var catchObjectPosition = fruit.X; var catcherPosition = Position.X; - var validCatch = - catchObjectPosition >= catcherPosition - halfCatchWidth && - catchObjectPosition <= catcherPosition + halfCatchWidth; + return catchObjectPosition >= catcherPosition - halfCatchWidth && + catchObjectPosition <= catcherPosition + halfCatchWidth; + } - if (validCatch) - placeCaughtObject(fruit); + public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result) + { + if (!(drawableObject.HitObject is PalpableCatchHitObject hitObject)) return; + + if (result.IsHit) + placeCaughtObject(hitObject); // droplet doesn't affect the catcher state - if (fruit is TinyDroplet) return validCatch; + if (hitObject is TinyDroplet) return; - if (validCatch && fruit.HyperDash) + if (result.IsHit && hitObject.HyperDash) { - var target = fruit.HyperDashTarget; - var timeDifference = target.StartTime - fruit.StartTime; - double positionDifference = target.X - catcherPosition; + var target = hitObject.HyperDashTarget; + var timeDifference = target.StartTime - hitObject.StartTime; + double positionDifference = target.X - X; var velocity = positionDifference / Math.Max(1.0, timeDifference - 1000.0 / 60.0); SetHyperDashState(Math.Abs(velocity), target.X); @@ -227,12 +230,14 @@ namespace osu.Game.Rulesets.Catch.UI else SetHyperDashState(); - if (validCatch) - updateState(fruit.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); - else if (!(fruit is Banana)) + if (result.IsHit) + updateState(hitObject.Kiai ? CatcherAnimationState.Kiai : CatcherAnimationState.Idle); + else if (!(hitObject is Banana)) updateState(CatcherAnimationState.Fail); + } - return validCatch; + public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) + { } /// diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 539776354c..857d9141c9 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.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.Replays; using osu.Game.Rulesets.Judgements; @@ -42,6 +41,8 @@ namespace osu.Game.Rulesets.Catch.UI public void OnNewResult(DrawableCatchHitObject hitObject, JudgementResult result) { + MovableCatcher.OnNewResult(hitObject, result); + if (!result.Type.IsScorable()) return; @@ -56,12 +57,10 @@ namespace osu.Game.Rulesets.Catch.UI comboDisplay.OnNewResult(hitObject, result); } - public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) - => comboDisplay.OnRevertResult(fruit, result); - - public bool AttemptCatch(CatchHitObject obj) + public void OnRevertResult(DrawableCatchHitObject hitObject, JudgementResult result) { - return MovableCatcher.AttemptCatch(obj); + comboDisplay.OnRevertResult(hitObject, result); + MovableCatcher.OnRevertResult(hitObject, result); } protected override void UpdateAfterChildren() From cb76a2d7b549072aed1ad4ac3beae16fbcbb6c35 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 15:02:55 +0900 Subject: [PATCH 5176/6909] Restore catcher state on revert judgement result --- .../TestSceneCatcher.cs | 38 ++++++++++++++----- .../Judgements/CatchJudgementResult.cs | 23 +++++++++++ .../Drawables/DrawableCatchHitObject.cs | 4 ++ osu.Game.Rulesets.Catch/UI/Catcher.cs | 6 +++ 4 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index d97c56164e..62149620d2 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -56,6 +56,24 @@ namespace osu.Game.Rulesets.Catch.Tests }; }); + [Test] + public void TestCatcherStateRevert() + { + DrawableCatchHitObject drawableObject = null; + JudgementResult result = null; + AddStep("catch kiai fruit", () => + { + drawableObject = createDrawableObject(new TestKiaiFruit()); + result = attemptCatch(drawableObject); + }); + checkState(CatcherAnimationState.Kiai); + AddStep("revert result", () => + { + catcher.OnRevertResult(drawableObject, result); + }); + checkState(CatcherAnimationState.Idle); + } + [Test] public void TestCatcherCatchWidth() { @@ -170,17 +188,19 @@ namespace osu.Game.Rulesets.Catch.Tests private void attemptCatch(CatchHitObject hitObject, int count = 1) { - hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - for (var i = 0; i < count; i++) + attemptCatch(createDrawableObject(hitObject)); + } + + private JudgementResult attemptCatch(DrawableCatchHitObject drawableObject) + { + drawableObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + var result = new CatchJudgementResult(drawableObject.HitObject, drawableObject.HitObject.CreateJudgement()) { - var drawableObject = createDrawableObject(hitObject); - var result = new JudgementResult(hitObject, new CatchJudgement()) - { - Type = catcher.CanCatch(hitObject) ? HitResult.Great : HitResult.Miss - }; - catcher.OnNewResult(drawableObject, result); - } + Type = catcher.CanCatch(drawableObject.HitObject) ? HitResult.Great : HitResult.Miss + }; + catcher.OnNewResult(drawableObject, result); + return result; } private DrawableCatchHitObject createDrawableObject(CatchHitObject hitObject) diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs new file mode 100644 index 0000000000..4b375e641b --- /dev/null +++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.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.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Catch.Judgements +{ + public class CatchJudgementResult : JudgementResult + { + /// + /// The catcher animation state prior to this judgement. + /// + public CatcherAnimationState CatcherAnimationState; + + public CatchJudgementResult([NotNull] HitObject hitObject, [NotNull] Judgement judgement) + : base(hitObject, judgement) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 6aa8ff439e..70efe9cf29 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -5,7 +5,9 @@ using System; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Utils; @@ -52,6 +54,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public override bool RemoveWhenNotAlive => IsOnPlate; + protected override JudgementResult CreateResult(Judgement judgement) => new CatchJudgementResult(HitObject, judgement); + protected override void CheckForResult(bool userTriggered, double timeOffset) { if (CheckPosition == null) return; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 8998dbf488..33068f95eb 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -14,6 +14,7 @@ using osu.Framework.Input.Bindings; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Configuration; +using osu.Game.Rulesets.Catch.Judgements; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Catch.Skinning; @@ -210,6 +211,9 @@ namespace osu.Game.Rulesets.Catch.UI public void OnNewResult(DrawableCatchHitObject drawableObject, JudgementResult result) { + var catchResult = (CatchJudgementResult)result; + catchResult.CatcherAnimationState = CurrentState; + if (!(drawableObject.HitObject is PalpableCatchHitObject hitObject)) return; if (result.IsHit) @@ -238,6 +242,8 @@ namespace osu.Game.Rulesets.Catch.UI public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) { + var catchResult = (CatchJudgementResult)result; + updateState(catchResult.CatcherAnimationState); } /// From 100b365c98977011702d0c6ffaa8a343250067bb Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 15:21:47 +0900 Subject: [PATCH 5177/6909] Restore hyper dash state on revert judgement result --- .../TestSceneCatcher.cs | 31 ++++++++++++++++++- .../Judgements/CatchJudgementResult.cs | 5 +++ osu.Game.Rulesets.Catch/UI/Catcher.cs | 13 +++++++- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 62149620d2..cf6011d721 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -57,7 +57,36 @@ namespace osu.Game.Rulesets.Catch.Tests }); [Test] - public void TestCatcherStateRevert() + public void TestCatcherHyperStateReverted() + { + DrawableCatchHitObject drawableObject1 = null; + DrawableCatchHitObject drawableObject2 = null; + JudgementResult result1 = null; + JudgementResult result2 = null; + AddStep("catch hyper fruit", () => + { + drawableObject1 = createDrawableObject(new Fruit { HyperDashTarget = new Fruit { X = 100 } }); + result1 = attemptCatch(drawableObject1); + }); + AddStep("catch normal fruit", () => + { + drawableObject2 = createDrawableObject(new Fruit()); + result2 = attemptCatch(drawableObject2); + }); + AddStep("revert second result", () => + { + catcher.OnRevertResult(drawableObject2, result2); + }); + checkHyperDash(true); + AddStep("revert first result", () => + { + catcher.OnRevertResult(drawableObject1, result1); + }); + checkHyperDash(false); + } + + [Test] + public void TestCatcherAnimationStateReverted() { DrawableCatchHitObject drawableObject = null; JudgementResult result = null; diff --git a/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs b/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs index 4b375e641b..c09355d59c 100644 --- a/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs +++ b/osu.Game.Rulesets.Catch/Judgements/CatchJudgementResult.cs @@ -15,6 +15,11 @@ namespace osu.Game.Rulesets.Catch.Judgements /// public CatcherAnimationState CatcherAnimationState; + /// + /// Whether the catcher was hyper dashing prior to this judgement. + /// + public bool CatcherHyperDash; + public CatchJudgementResult([NotNull] HitObject hitObject, [NotNull] Judgement judgement) : base(hitObject, judgement) { diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 33068f95eb..da80fa2bd5 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -213,6 +213,7 @@ namespace osu.Game.Rulesets.Catch.UI { var catchResult = (CatchJudgementResult)result; catchResult.CatcherAnimationState = CurrentState; + catchResult.CatcherHyperDash = HyperDashing; if (!(drawableObject.HitObject is PalpableCatchHitObject hitObject)) return; @@ -243,7 +244,17 @@ namespace osu.Game.Rulesets.Catch.UI public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) { var catchResult = (CatchJudgementResult)result; - updateState(catchResult.CatcherAnimationState); + + if (CurrentState != catchResult.CatcherAnimationState) + updateState(catchResult.CatcherAnimationState); + + if (HyperDashing != catchResult.CatcherHyperDash) + { + if (catchResult.CatcherHyperDash) + SetHyperDashState(2); + else + SetHyperDashState(); + } } /// From 1a66d8f2bc7ae8ae423b2da010965ad22dc4ff60 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 15:24:39 +0900 Subject: [PATCH 5178/6909] Remove caught objects on revert result --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index da80fa2bd5..2bf085312f 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -241,7 +241,7 @@ namespace osu.Game.Rulesets.Catch.UI updateState(CatcherAnimationState.Fail); } - public void OnRevertResult(DrawableCatchHitObject fruit, JudgementResult result) + public void OnRevertResult(DrawableCatchHitObject drawableObject, JudgementResult result) { var catchResult = (CatchJudgementResult)result; @@ -255,6 +255,9 @@ namespace osu.Game.Rulesets.Catch.UI else SetHyperDashState(); } + + caughtFruitContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); + droppedObjectTarget.RemoveAll(d => (d as DrawableCatchHitObject)?.HitObject == drawableObject.HitObject); } /// From 02571ec7ae3f45df77db3d162d1bd3cc4afe640c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 15:43:17 +0900 Subject: [PATCH 5179/6909] Remove hit explosion on revert result --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 2 ++ osu.Game.Rulesets.Catch/UI/HitExplosion.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 2bf085312f..a806e623af 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -258,6 +258,7 @@ namespace osu.Game.Rulesets.Catch.UI caughtFruitContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); droppedObjectTarget.RemoveAll(d => (d as DrawableCatchHitObject)?.HitObject == drawableObject.HitObject); + hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); } /// @@ -489,6 +490,7 @@ namespace osu.Game.Rulesets.Catch.UI if (!hitLighting.Value) return; HitExplosion hitExplosion = hitExplosionPool.Get(); + hitExplosion.HitObject = caughtObject.HitObject; hitExplosion.X = caughtObject.X; hitExplosion.Scale = new Vector2(caughtObject.HitObject.Scale); hitExplosion.ObjectColour = caughtObject.AccentColour.Value; diff --git a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs index 24ca778248..26627422e1 100644 --- a/osu.Game.Rulesets.Catch/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Catch/UI/HitExplosion.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Pooling; using osu.Framework.Utils; +using osu.Game.Rulesets.Catch.Objects; using osuTK; using osuTK.Graphics; @@ -15,6 +16,7 @@ namespace osu.Game.Rulesets.Catch.UI public class HitExplosion : PoolableDrawable { private Color4 objectColour; + public CatchHitObject HitObject; public Color4 ObjectColour { From 17d48c82f6f4d8ed22981b2ed4e749012709db55 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 16:59:06 +0900 Subject: [PATCH 5180/6909] Use switch statement instead of an array --- .../Skinning/Legacy/LegacyFruitPiece.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs index e45f00c6aa..6f93e68594 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs @@ -2,7 +2,6 @@ // 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.Catch.Objects.Drawables; namespace osu.Game.Rulesets.Catch.Skinning.Legacy @@ -11,11 +10,6 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy { public readonly Bindable VisualRepresentation = new Bindable(); - private readonly string[] lookupNames = - { - "fruit-pear", "fruit-grapes", "fruit-apple", "fruit-orange" - }; - protected override void LoadComplete() { base.LoadComplete(); @@ -30,10 +24,24 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy private void setTexture(FruitVisualRepresentation visualRepresentation) { - Texture texture = Skin.GetTexture(lookupNames[(int)visualRepresentation]); - Texture overlayTexture = Skin.GetTexture(lookupNames[(int)visualRepresentation] + "-overlay"); + switch (visualRepresentation) + { + case FruitVisualRepresentation.Pear: + SetTexture(Skin.GetTexture("fruit-pear"), Skin.GetTexture("fruit-pear-overlay")); + break; - SetTexture(texture, overlayTexture); + case FruitVisualRepresentation.Grape: + SetTexture(Skin.GetTexture("fruit-grapes"), Skin.GetTexture("fruit-grapes-overlay")); + break; + + case FruitVisualRepresentation.Pineapple: + SetTexture(Skin.GetTexture("fruit-apple"), Skin.GetTexture("fruit-apple-overlay")); + break; + + case FruitVisualRepresentation.Raspberry: + SetTexture(Skin.GetTexture("fruit-orange"), Skin.GetTexture("fruit-orange-overlay")); + break; + } } } } From 603cecb2ebbf2241f605ed86daf18d1139013140 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 17:02:57 +0900 Subject: [PATCH 5181/6909] Make CatchHitObjectPiece abstract class --- osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs index be6cf0fc05..0d4a4e8e78 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs @@ -12,7 +12,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning.Default { - public class CatchHitObjectPiece : CompositeDrawable + public abstract class CatchHitObjectPiece : CompositeDrawable { public readonly Bindable AccentColour = new Bindable(); public readonly Bindable HyperDash = new Bindable(); From 9d926de9443143f7a7b75860c4b05ac973e0d9ee Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Dec 2020 17:04:26 +0900 Subject: [PATCH 5182/6909] Fix test failure --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index c8826aa174..8602c7aad1 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -15,7 +15,6 @@ 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; namespace osu.Game.Rulesets.Catch.Tests @@ -58,7 +57,7 @@ namespace osu.Game.Rulesets.Catch.Tests Schedule(() => { - area.OnNewResult(drawable, new JudgementResult(fruit, new CatchJudgement()) + area.OnNewResult(drawable, new CatchJudgementResult(fruit, new CatchJudgement()) { Type = area.MovableCatcher.CanCatch(fruit) ? HitResult.Great : HitResult.Miss }); From 4d5c242d35e0603282bee8d157a92fb4239c9e22 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 17:15:40 +0900 Subject: [PATCH 5183/6909] Use virtual property instead of a field for optional pieces --- .../Skinning/Default/BananaPiece.cs | 2 ++ .../Skinning/Default/CatchHitObjectPiece.cs | 10 ++++++++-- .../Skinning/Default/DropletPiece.cs | 2 ++ osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs | 3 +++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs index f81c1063b9..8da18a668a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/BananaPiece.cs @@ -7,6 +7,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default { public class BananaPiece : CatchHitObjectPiece { + protected override BorderPiece BorderPiece { get; } + public BananaPiece() { RelativeSizeAxes = Axes.Both; diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs index 0d4a4e8e78..d59b6cc0de 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs @@ -21,11 +21,17 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default [CanBeNull] protected DrawableHitObject DrawableHitObject { get; private set; } + /// + /// A part of this piece that will be faded out while falling in the playfield. + /// [CanBeNull] - protected BorderPiece BorderPiece; + protected virtual BorderPiece BorderPiece => null; + /// + /// A part of this piece that will be only visible when is true. + /// [CanBeNull] - protected HyperBorderPiece HyperBorderPiece; + protected virtual HyperBorderPiece HyperBorderPiece => null; protected override void LoadComplete() { diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs index c149f7769f..8b1052dfe2 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/DropletPiece.cs @@ -9,6 +9,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default { public class DropletPiece : CatchHitObjectPiece { + protected override HyperBorderPiece HyperBorderPiece { get; } + public DropletPiece() { Size = new Vector2(CatchHitObject.OBJECT_RADIUS / 2); diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs index 45d688c4e5..2e3803a31a 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs @@ -16,6 +16,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default public readonly Bindable VisualRepresentation = new Bindable(); + protected override BorderPiece BorderPiece { get; } + protected override HyperBorderPiece HyperBorderPiece { get; } + public FruitPiece() { RelativeSizeAxes = Axes.Both; From 22a5df6309aad3ab7f075c38e7617eee64c4c12c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 17:31:00 +0900 Subject: [PATCH 5184/6909] Clear all transforms of catcher trail sprite before returned to pool --- osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs index b3be18d46b..0e3e409fac 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherTrailSprite.cs @@ -30,5 +30,11 @@ namespace osu.Game.Rulesets.Catch.UI // 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; } + + protected override void FreeAfterUse() + { + ClearTransforms(); + base.FreeAfterUse(); + } } } From 56721a6fa9aec7d9ae55091931e474186862d6b7 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 12:48:13 +0900 Subject: [PATCH 5185/6909] Compute object position in stack via a pure function --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index a806e623af..76c5afeda7 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -447,8 +447,10 @@ namespace osu.Game.Rulesets.Catch.UI if (caughtObject == null) return; + var positionInStack = computePositionInStack(new Vector2(source.X - X, 0), caughtObject.DisplayRadius); + caughtObject.RelativePositionAxes = Axes.None; - caughtObject.X = source.X - X; + caughtObject.Position = positionInStack; caughtObject.IsOnPlate = true; caughtObject.Anchor = Anchor.TopCentre; @@ -457,8 +459,6 @@ namespace osu.Game.Rulesets.Catch.UI caughtObject.LifetimeStart = source.StartTime; caughtObject.LifetimeEnd = double.MaxValue; - adjustPositionInStack(caughtObject); - caughtFruitContainer.Add(caughtObject); addLighting(caughtObject); @@ -467,22 +467,22 @@ namespace osu.Game.Rulesets.Catch.UI removeFromPlate(caughtObject, DroppedObjectAnimation.Explode); } - private void adjustPositionInStack(DrawablePalpableCatchHitObject caughtObject) + private Vector2 computePositionInStack(Vector2 position, float displayRadius) { const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2; const float allowance = 10; - float caughtObjectRadius = caughtObject.DisplayRadius; - - while (caughtFruitContainer.Any(f => Vector2Extensions.Distance(f.Position, caughtObject.Position) < (caughtObjectRadius + radius_div_2) / (allowance / 2))) + while (caughtFruitContainer.Any(f => Vector2Extensions.Distance(f.Position, position) < (displayRadius + radius_div_2) / (allowance / 2))) { - float diff = (caughtObjectRadius + radius_div_2) / allowance; + float diff = (displayRadius + radius_div_2) / allowance; - caughtObject.X += (RNG.NextSingle() - 0.5f) * diff * 2; - caughtObject.Y -= RNG.NextSingle() * diff; + position.X += (RNG.NextSingle() - 0.5f) * diff * 2; + position.Y -= RNG.NextSingle() * diff; } - caughtObject.X = Math.Clamp(caughtObject.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); + position.X = Math.Clamp(position.X, -CatcherArea.CATCHER_SIZE / 2, CatcherArea.CATCHER_SIZE / 2); + + return position; } private void addLighting(DrawablePalpableCatchHitObject caughtObject) From 004c705aa9da1024078af02f8ef6c87c8468d804 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 20:05:18 +0900 Subject: [PATCH 5186/6909] Remove ScaleContainer and flatten the Drawable tree of catch DHO --- .../TestSceneFruitRandomness.cs | 47 +++++-------------- .../Objects/Drawables/DrawableBanana.cs | 14 +++--- .../Objects/Drawables/DrawableDroplet.cs | 6 +-- .../Objects/Drawables/DrawableFruit.cs | 6 +-- .../DrawablePalpableCatchHitObject.cs | 14 +----- 5 files changed, 28 insertions(+), 59 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index 2ffebb7de1..e1d817b314 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -11,13 +11,13 @@ namespace osu.Game.Rulesets.Catch.Tests { public class TestSceneFruitRandomness : OsuTestScene { - private readonly TestDrawableFruit drawableFruit; - private readonly TestDrawableBanana drawableBanana; + private readonly DrawableFruit drawableFruit; + private readonly DrawableBanana drawableBanana; public TestSceneFruitRandomness() { - drawableFruit = new TestDrawableFruit(new Fruit()); - drawableBanana = new TestDrawableBanana(new Banana()); + drawableFruit = new DrawableFruit(new Fruit()); + drawableBanana = new DrawableBanana(new Banana()); Add(new TestDrawableCatchHitObjectSpecimen(drawableFruit) { X = -200 }); Add(new TestDrawableCatchHitObjectSpecimen(drawableBanana)); @@ -44,9 +44,9 @@ namespace osu.Game.Rulesets.Catch.Tests { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; - fruitRotation = drawableFruit.InnerRotation; - bananaRotation = drawableBanana.InnerRotation; - bananaScale = drawableBanana.InnerScale; + fruitRotation = drawableFruit.Rotation; + bananaRotation = drawableBanana.Rotation; + bananaScale = drawableBanana.Scale.X; bananaColour = drawableBanana.AccentColour.Value; }); @@ -55,9 +55,9 @@ namespace osu.Game.Rulesets.Catch.Tests drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; }); - AddAssert("fruit rotation is changed", () => drawableFruit.InnerRotation != fruitRotation); - AddAssert("banana rotation is changed", () => drawableBanana.InnerRotation != bananaRotation); - AddAssert("banana scale is changed", () => drawableBanana.InnerScale != bananaScale); + AddAssert("fruit rotation is changed", () => drawableFruit.Rotation != fruitRotation); + AddAssert("banana rotation is changed", () => drawableBanana.Rotation != bananaRotation); + AddAssert("banana scale is changed", () => drawableBanana.Scale.X != bananaScale); AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour); AddStep("reset start time", () => @@ -66,31 +66,10 @@ namespace osu.Game.Rulesets.Catch.Tests }); AddAssert("rotation and scale restored", () => - drawableFruit.InnerRotation == fruitRotation && - drawableBanana.InnerRotation == bananaRotation && - drawableBanana.InnerScale == bananaScale && + drawableFruit.Rotation == fruitRotation && + drawableBanana.Rotation == bananaRotation && + drawableBanana.Scale.X == bananaScale && drawableBanana.AccentColour.Value == bananaColour); } - - private class TestDrawableFruit : DrawableFruit - { - public float InnerRotation => ScaleContainer.Rotation; - - public TestDrawableFruit(Fruit h) - : base(h) - { - } - } - - private class TestDrawableBanana : DrawableBanana - { - public float InnerRotation => ScaleContainer.Rotation; - public float InnerScale => ScaleContainer.Scale.X; - - public TestDrawableBanana(Banana h) - : base(h) - { - } - } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 2a543a0e04..4bc5da2396 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -24,9 +24,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - ScaleContainer.Child = new SkinnableDrawable( + AddInternal(new SkinnableDrawable( new CatchSkinComponent(CatchSkinComponents.Banana), - _ => new BananaPiece()); + _ => new BananaPiece())); } protected override void LoadComplete() @@ -44,12 +44,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables const float end_scale = 0.6f; const float random_scale_range = 1.6f; - ScaleContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3))) - .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); + this.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3))) + .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); - ScaleContainer.RotateTo(getRandomAngle(1)) - .Then() - .RotateTo(getRandomAngle(2), HitObject.TimePreempt); + this.RotateTo(getRandomAngle(1)) + .Then() + .RotateTo(getRandomAngle(2), HitObject.TimePreempt); float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1); } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index 81c8de2e59..d96b1c8902 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -26,9 +26,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - ScaleContainer.Child = new SkinnableDrawable( + AddInternal(new SkinnableDrawable( new CatchSkinComponent(CatchSkinComponents.Droplet), - _ => new DropletPiece()); + _ => new DropletPiece())); } protected override void UpdateInitialTransforms() @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables float startRotation = RandomSingle(1) * 20; double duration = HitObject.TimePreempt + 2000; - ScaleContainer.RotateTo(startRotation).RotateTo(startRotation + 720, duration); + this.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 0fcd319a93..8135a31b43 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -32,16 +32,16 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4); }, true); - ScaleContainer.Child = new SkinnableDrawable( + AddInternal(new SkinnableDrawable( new CatchSkinComponent(CatchSkinComponents.Fruit), - _ => new FruitPiece()); + _ => new FruitPiece())); } protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); - ScaleContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); + this.RotateTo((RandomSingle(1) - 0.5f) * 40); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 0877b5e248..b99710ae8e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -5,7 +5,6 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osuTK; namespace osu.Game.Rulesets.Catch.Objects.Drawables @@ -21,7 +20,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public readonly Bindable IndexInBeatmap = new Bindable(); /// - /// The multiplicative factor applied to scale relative to scale. + /// The multiplicative factor applied to relative to scale. /// protected virtual float ScaleFactor => 1; @@ -32,20 +31,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor; - protected readonly Container ScaleContainer; - protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) : base(h) { Origin = Anchor.Centre; Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); - - AddInternal(ScaleContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Origin = Anchor.Centre, - Anchor = Anchor.Centre, - }); } [BackgroundDependencyLoader] @@ -58,7 +48,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables ScaleBindable.BindValueChanged(scale => { - ScaleContainer.Scale = new Vector2(scale.NewValue * ScaleFactor); + Scale = new Vector2(scale.NewValue * ScaleFactor); }, true); IndexInBeatmap.BindValueChanged(_ => UpdateComboColour()); From 94a59ac3b240c75a00e440f2062ddbd7d9cec6e5 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 20:41:26 +0900 Subject: [PATCH 5187/6909] Make catch hit lighting logic not dependent on caught object --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 33 +++++++++++++++------------ 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 76c5afeda7..ac0f25e21b 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -215,10 +215,19 @@ namespace osu.Game.Rulesets.Catch.UI catchResult.CatcherAnimationState = CurrentState; catchResult.CatcherHyperDash = HyperDashing; - if (!(drawableObject.HitObject is PalpableCatchHitObject hitObject)) return; + if (!(drawableObject is DrawablePalpableCatchHitObject palpableObject)) return; + + var hitObject = palpableObject.HitObject; if (result.IsHit) - placeCaughtObject(hitObject); + { + var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplayRadius); + + placeCaughtObject(hitObject, positionInStack); + + if (hitLighting.Value) + addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value); + } // droplet doesn't affect the catcher state if (hitObject is TinyDroplet) return; @@ -441,16 +450,14 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } - private void placeCaughtObject(PalpableCatchHitObject source) + private void placeCaughtObject(PalpableCatchHitObject source, Vector2 position) { var caughtObject = createCaughtObject(source); if (caughtObject == null) return; - var positionInStack = computePositionInStack(new Vector2(source.X - X, 0), caughtObject.DisplayRadius); - caughtObject.RelativePositionAxes = Axes.None; - caughtObject.Position = positionInStack; + caughtObject.Position = position; caughtObject.IsOnPlate = true; caughtObject.Anchor = Anchor.TopCentre; @@ -461,8 +468,6 @@ namespace osu.Game.Rulesets.Catch.UI caughtFruitContainer.Add(caughtObject); - addLighting(caughtObject); - if (!caughtObject.StaysOnPlate) removeFromPlate(caughtObject, DroppedObjectAnimation.Explode); } @@ -485,15 +490,13 @@ namespace osu.Game.Rulesets.Catch.UI return position; } - private void addLighting(DrawablePalpableCatchHitObject caughtObject) + private void addLighting(CatchHitObject hitObject, float x, Color4 colour) { - if (!hitLighting.Value) return; - HitExplosion hitExplosion = hitExplosionPool.Get(); - hitExplosion.HitObject = caughtObject.HitObject; - hitExplosion.X = caughtObject.X; - hitExplosion.Scale = new Vector2(caughtObject.HitObject.Scale); - hitExplosion.ObjectColour = caughtObject.AccentColour.Value; + hitExplosion.HitObject = hitObject; + hitExplosion.X = x; + hitExplosion.Scale = new Vector2(hitObject.Scale); + hitExplosion.ObjectColour = colour; hitExplosionContainer.Add(hitExplosion); } From be4a668e0b83025c6c7d87f5964ea4887e62d606 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Dec 2020 15:34:31 +0900 Subject: [PATCH 5188/6909] Add basic structure for multiplayer state and model components --- .../RealtimeMultiplayer/ISpectatorClient.cs | 19 +++++++++++++++++++ .../RealtimeMultiplayer/ISpectatorServer.cs | 13 +++++++++++++ .../RealtimeMultiplayer/MultiplayerRoom.cs | 15 +++++++++++++++ .../MultiplayerRoomState.cs | 17 +++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs create mode 100644 osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs create mode 100644 osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs create mode 100644 osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs b/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs new file mode 100644 index 0000000000..8054c489c5 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.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.Threading.Tasks; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + /// + /// An interface defining a spectator client instance. + /// + public interface IMultiplayerClient + { + /// + /// Signals that the room has changed state. + /// + /// The state of the room. + Task RoomStateChanged(MultiplayerRoomState state); + } +} diff --git a/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs b/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs new file mode 100644 index 0000000000..3d00eaaf6a --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.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.Online.RealtimeMultiplayer +{ + /// + /// An interface defining the spectator server instance. + /// + public interface IMultiplayerServer + { + // TODO: implement + } +} diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs new file mode 100644 index 0000000000..ee1f42f7bf --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.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; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + [Serializable] + public class MultiplayerRoom + { + public long RoomID { get; set; } + + public MultiplayerRoomState State { get; set; } + } +} diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs new file mode 100644 index 0000000000..60e47e9027 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.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.Online.RealtimeMultiplayer +{ + /// + /// The current overall state of a realtime multiplayer room. + /// + public enum MultiplayerRoomState + { + Open, + WaitingForLoad, + Playing, + WaitingForResults, + Closed + } +} From daed27460c8f2b29349401d59ac4026f2fcf8e48 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Dec 2020 17:23:57 +0900 Subject: [PATCH 5189/6909] Add simple user state class --- .../RealtimeMultiplayer/MultiplayerClientState.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs new file mode 100644 index 0000000000..ac76f8999a --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs @@ -0,0 +1,15 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public class MultiplayerClientState + { + public MultiplayerClientState(in long roomId) + { + CurrentRoomID = roomId; + } + + public long CurrentRoomID { get; } + } +} From f4ccbbd09261ac4ba63d4633d3d884841b4139c9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 4 Dec 2020 18:18:41 +0900 Subject: [PATCH 5190/6909] Add basic server implementation --- .../RealtimeMultiplayer/ISpectatorServer.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs b/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs index 3d00eaaf6a..ba7d7d3832 100644 --- a/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.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.Threading.Tasks; + namespace osu.Game.Online.RealtimeMultiplayer { /// @@ -8,6 +10,17 @@ namespace osu.Game.Online.RealtimeMultiplayer /// public interface IMultiplayerServer { - // TODO: implement + /// + /// Request to join a multiplayer room. + /// + /// The databased room ID. + /// Whether the room could be joined. + Task JoinRoom(long roomId); + + /// + /// Request to leave the currently joined room. + /// + /// The databased room ID. + Task LeaveRoom(long roomId); } } From 8ebdb5723b19827960f08a60e3e0c252fb188208 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 7 Dec 2020 18:50:02 +0900 Subject: [PATCH 5191/6909] Add models for users and rooms --- .../RealtimeMultiplayer/MultiplayerRoom.cs | 9 ++++++++ .../MultiplayerRoomUser.cs | 21 +++++++++++++++++++ .../MultiplayerUserState.cs | 13 ++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs create mode 100644 osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index ee1f42f7bf..f42f8d7f5f 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; namespace osu.Game.Online.RealtimeMultiplayer { @@ -11,5 +12,13 @@ namespace osu.Game.Online.RealtimeMultiplayer public long RoomID { get; set; } public MultiplayerRoomState State { get; set; } + + public IReadOnlyList Users => users; + + private List users = new List(); + + public void Join(int user) => users.Add(new MultiplayerRoomUser(user)); + + public void Leave(int user) => users.RemoveAll(u => u.UserID == user); } } diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs new file mode 100644 index 0000000000..c5fed1629c --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.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.Users; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public class MultiplayerRoomUser + { + public MultiplayerRoomUser(in int userId) + { + UserID = userId; + } + + public long UserID { get; set; } + + public MultiplayerUserState State { get; set; } + + public User User { get; set; } + } +} diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs new file mode 100644 index 0000000000..6d6baa8017 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.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.Online.RealtimeMultiplayer +{ + public enum MultiplayerUserState + { + Watching, + Ready, + Playing, + Results, + } +} From fdf025942b526ad34ce1a8cd25bd55f0acb6980c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 01:24:38 +0900 Subject: [PATCH 5192/6909] Ensure room is locked when mutating users --- .../RealtimeMultiplayer/MultiplayerRoom.cs | 25 ++++++++++++++++--- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index f42f8d7f5f..1a02dab5e0 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -9,16 +9,33 @@ namespace osu.Game.Online.RealtimeMultiplayer [Serializable] public class MultiplayerRoom { + private object writeLock = new object(); + public long RoomID { get; set; } public MultiplayerRoomState State { get; set; } - public IReadOnlyList Users => users; - private List users = new List(); - public void Join(int user) => users.Add(new MultiplayerRoomUser(user)); + public IReadOnlyList Users + { + get + { + lock (writeLock) + return users.ToArray(); + } + } - public void Leave(int user) => users.RemoveAll(u => u.UserID == user); + public void Join(int user) + { + lock (writeLock) + users.Add(new MultiplayerRoomUser(user)); + } + + public void Leave(int user) + { + lock (writeLock) + users.RemoveAll(u => u.UserID == user); + } } } From ca86524c922012b32e7a1420c54b49d96a6df3d6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 01:35:29 +0900 Subject: [PATCH 5193/6909] Add locking on join/leave operations --- .../RealtimeMultiplayer/MultiplayerRoom.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index 1a02dab5e0..db3993255a 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -26,16 +26,25 @@ namespace osu.Game.Online.RealtimeMultiplayer } } - public void Join(int user) + public MultiplayerRoomUser Join(int userId) { - lock (writeLock) - users.Add(new MultiplayerRoomUser(user)); + var user = new MultiplayerRoomUser(userId); + lock (writeLock) users.Add(user); + return user; } - public void Leave(int user) + public MultiplayerRoomUser Leave(int userId) { lock (writeLock) - users.RemoveAll(u => u.UserID == user); + { + var user = users.Find(u => u.UserID == userId); + + if (user == null) + return null; + + users.Remove(user); + return user; + } } } } From ff52a5ddc6bc5cbb4d5dfdf52fed460b6e660826 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 01:35:42 +0900 Subject: [PATCH 5194/6909] Add callbacks for join/leave events to notify other room occupants --- .../Online/RealtimeMultiplayer/ISpectatorClient.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs b/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs index 8054c489c5..d5f6baad43 100644 --- a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs @@ -15,5 +15,17 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The state of the room. Task RoomStateChanged(MultiplayerRoomState state); + + /// + /// Signals that a user has joined the room. + /// + /// The user. + Task UserJoined(MultiplayerRoomUser user); + + /// + /// Signals that a user has left the room. + /// + /// The user. + Task UserLeft(MultiplayerRoomUser user); } } From 6e5846d91b7389575a91691851f6311de8a5cddc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 14:33:01 +0900 Subject: [PATCH 5195/6909] Fix serialization failure due to missing set --- osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs index ac76f8999a..17c209ba26 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs @@ -5,11 +5,11 @@ namespace osu.Game.Online.RealtimeMultiplayer { public class MultiplayerClientState { + public long CurrentRoomID { get; set; } + public MultiplayerClientState(in long roomId) { CurrentRoomID = roomId; } - - public long CurrentRoomID { get; } } } From baf16cfbc3a30082996acb9bde002b58ff815105 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 14:33:38 +0900 Subject: [PATCH 5196/6909] Add room settings related model and event flow --- .../RealtimeMultiplayer/ISpectatorClient.cs | 6 +++++ .../RealtimeMultiplayer/MultiplayerRoom.cs | 2 ++ .../MultiplayerRoomSettings.cs | 25 +++++++++++++++++++ 3 files changed, 33 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs b/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs index d5f6baad43..1e6832e728 100644 --- a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs @@ -27,5 +27,11 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The user. Task UserLeft(MultiplayerRoomUser user); + + /// + /// Signals that the settings for this room have changed. + /// + /// The updated room settings. + Task SettingsChanged(MultiplayerRoomSettings newSettings); } } diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index db3993255a..61320b9d73 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -15,6 +15,8 @@ namespace osu.Game.Online.RealtimeMultiplayer public MultiplayerRoomState State { get; set; } + public MultiplayerRoomSettings Settings { get; set; } + private List users = new List(); public IReadOnlyList Users diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs new file mode 100644 index 0000000000..4c2c014cd9 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.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 System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using osu.Game.Online.API; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public class MultiplayerRoomSettings : IEquatable + { + public int? BeatmapID { get; set; } + + public int? RulesetID { get; set; } + + [NotNull] + public IEnumerable Mods { get; set; } = Enumerable.Empty(); + + public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods) && RulesetID == other?.RulesetID; + + public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; + } +} From 882ace6efea12be861efc400d5b61e0a8dd48a0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 14:33:47 +0900 Subject: [PATCH 5197/6909] Make MultiplayerRoomUser equatable --- .../MultiplayerRoomUser.cs | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs index c5fed1629c..3daebe3dda 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs @@ -1,21 +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 osu.Game.Users; namespace osu.Game.Online.RealtimeMultiplayer { - public class MultiplayerRoomUser + public class MultiplayerRoomUser : IEquatable { public MultiplayerRoomUser(in int userId) { UserID = userId; } - public long UserID { get; set; } + public long UserID { get; } public MultiplayerUserState State { get; set; } public User User { get; set; } + + public bool Equals(MultiplayerRoomUser other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return UserID == other.UserID; + } + + public override bool Equals(object obj) + { + if (ReferenceEquals(null, obj)) return false; + if (ReferenceEquals(this, obj)) return true; + if (obj.GetType() != this.GetType()) return false; + + return Equals((MultiplayerRoomUser)obj); + } + + public override int GetHashCode() => UserID.GetHashCode(); } } From e193f8214df514f770d3bcad9ef41ce053255ed8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 14:39:26 +0900 Subject: [PATCH 5198/6909] Remove unnecessary room id from leave room request --- osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs b/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs index ba7d7d3832..053ffa0462 100644 --- a/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs @@ -20,7 +20,6 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// Request to leave the currently joined room. /// - /// The databased room ID. - Task LeaveRoom(long roomId); + Task LeaveRoom(); } } From b3bdaaa7b59dc6a58f4448a7a236e0b8f2756f6f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 14:49:36 +0900 Subject: [PATCH 5199/6909] Move exceptions to common code --- .../RealtimeMultiplayer/InvalidStateException.cs | 15 +++++++++++++++ .../RealtimeMultiplayer/NotJoinedRoomException.cs | 12 ++++++++++++ .../UserAlreadyInMultiplayerRoom.cs | 15 +++++++++++++++ 3 files changed, 42 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs create mode 100644 osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs create mode 100644 osu.Game/Online/RealtimeMultiplayer/UserAlreadyInMultiplayerRoom.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs b/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs new file mode 100644 index 0000000000..8393e7e925 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.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; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public class InvalidStateException : Exception + { + public InvalidStateException(string message) + : base(message) + { + } + } +} diff --git a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs b/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs new file mode 100644 index 0000000000..1b264d1ac5 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs @@ -0,0 +1,12 @@ +using System; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public class NotJoinedRoomException : Exception + { + public NotJoinedRoomException() + : base("This user has not yet joined a multiplayer room.") + { + } + } +} \ No newline at end of file diff --git a/osu.Game/Online/RealtimeMultiplayer/UserAlreadyInMultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/UserAlreadyInMultiplayerRoom.cs new file mode 100644 index 0000000000..9a2090c710 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/UserAlreadyInMultiplayerRoom.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; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public class UserAlreadyInMultiplayerRoom : Exception + { + public UserAlreadyInMultiplayerRoom() + : base("This user is already in a room.") + { + } + } +} From 327799c263bd923f4b7b51b8e675914aabb1d74f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 14:51:48 +0900 Subject: [PATCH 5200/6909] Rename multiplayer server file to match class --- .../{ISpectatorServer.cs => IMultiplayerServer.cs} | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) rename osu.Game/Online/RealtimeMultiplayer/{ISpectatorServer.cs => IMultiplayerServer.cs} (66%) diff --git a/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs similarity index 66% rename from osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs rename to osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index 053ffa0462..f6eb3b8ff5 100644 --- a/osu.Game/Online/RealtimeMultiplayer/ISpectatorServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs @@ -1,6 +1,3 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - using System.Threading.Tasks; namespace osu.Game.Online.RealtimeMultiplayer @@ -14,12 +11,12 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Request to join a multiplayer room. /// /// The databased room ID. - /// Whether the room could be joined. - Task JoinRoom(long roomId); + /// If the user is already in the requested (or another) room. + Task JoinRoom(long roomId); /// /// Request to leave the currently joined room. /// Task LeaveRoom(); } -} +} \ No newline at end of file From 5a231cef1507289a2da1c4a719e26b195edccf01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 15:46:11 +0900 Subject: [PATCH 5201/6909] Add thread safety for external operations on MultiplayerRoom --- .../RealtimeMultiplayer/MultiplayerRoom.cs | 30 ++++++++++++------- 1 file changed, 20 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index 61320b9d73..7c3724915f 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -9,8 +9,6 @@ namespace osu.Game.Online.RealtimeMultiplayer [Serializable] public class MultiplayerRoom { - private object writeLock = new object(); - public long RoomID { get; set; } public MultiplayerRoomState State { get; set; } @@ -31,22 +29,34 @@ namespace osu.Game.Online.RealtimeMultiplayer public MultiplayerRoomUser Join(int userId) { var user = new MultiplayerRoomUser(userId); - lock (writeLock) users.Add(user); + PerformUpdate(_ => users.Add(user)); return user; } public MultiplayerRoomUser Leave(int userId) { - lock (writeLock) + MultiplayerRoomUser user = null; + + PerformUpdate(_ => { - var user = users.Find(u => u.UserID == userId); + user = users.Find(u => u.UserID == userId); - if (user == null) - return null; + if (user != null) + users.Remove(user); + }); - users.Remove(user); - return user; - } + return user; + } + + private object writeLock = new object(); + + /// + /// Perform an update on this room in a thread-safe manner. + /// + /// The action to perform. + public void PerformUpdate(Action action) + { + lock (writeLock) action(this); } } } From 4f449ba8217fa7ce930d2f608f55464d9da90ff0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 15:49:08 +0900 Subject: [PATCH 5202/6909] Rename idle state --- osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs index 6d6baa8017..47a1226e29 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs @@ -5,7 +5,7 @@ namespace osu.Game.Online.RealtimeMultiplayer { public enum MultiplayerUserState { - Watching, + Idle, Ready, Playing, Results, From 42b1e9d6a4185cea488227a159de1d256456bb4b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 15:54:52 +0900 Subject: [PATCH 5203/6909] Add xmldoc coverage of MultiplayerRoom --- .../RealtimeMultiplayer/MultiplayerRoom.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index 7c3724915f..4bd8411344 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -9,14 +9,28 @@ namespace osu.Game.Online.RealtimeMultiplayer [Serializable] public class MultiplayerRoom { + /// + /// The ID of the room, used for database persistence. + /// public long RoomID { get; set; } + /// + /// The current state of the room (ie. whether it is in progress or otherwise). + /// public MultiplayerRoomState State { get; set; } + /// + /// All currently enforced game settings for this room. + /// public MultiplayerRoomSettings Settings { get; set; } private List users = new List(); + private object writeLock = new object(); + + /// + /// All users which are currently in this room, in any state. + /// public IReadOnlyList Users { get @@ -26,6 +40,9 @@ namespace osu.Game.Online.RealtimeMultiplayer } } + /// + /// Join a new user to this room. + /// public MultiplayerRoomUser Join(int userId) { var user = new MultiplayerRoomUser(userId); @@ -33,6 +50,9 @@ namespace osu.Game.Online.RealtimeMultiplayer return user; } + /// + /// Remove a user from this room. + /// public MultiplayerRoomUser Leave(int userId) { MultiplayerRoomUser user = null; @@ -48,8 +68,6 @@ namespace osu.Game.Online.RealtimeMultiplayer return user; } - private object writeLock = new object(); - /// /// Perform an update on this room in a thread-safe manner. /// From 2365d656104a66233ffe983907db64c5515b108c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 16:02:35 +0900 Subject: [PATCH 5204/6909] Move business logic out of MultiplayerRoom --- .../RealtimeMultiplayer/MultiplayerRoom.cs | 48 +++---------------- 1 file changed, 7 insertions(+), 41 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index 4bd8411344..dc7ced7a9a 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -6,6 +6,9 @@ using System.Collections.Generic; namespace osu.Game.Online.RealtimeMultiplayer { + /// + /// A multiplayer room. + /// [Serializable] public class MultiplayerRoom { @@ -24,50 +27,13 @@ namespace osu.Game.Online.RealtimeMultiplayer /// public MultiplayerRoomSettings Settings { get; set; } - private List users = new List(); + /// + /// All users currently in this room. + /// + public List Users { get; set; } = new List(); private object writeLock = new object(); - /// - /// All users which are currently in this room, in any state. - /// - public IReadOnlyList Users - { - get - { - lock (writeLock) - return users.ToArray(); - } - } - - /// - /// Join a new user to this room. - /// - public MultiplayerRoomUser Join(int userId) - { - var user = new MultiplayerRoomUser(userId); - PerformUpdate(_ => users.Add(user)); - return user; - } - - /// - /// Remove a user from this room. - /// - public MultiplayerRoomUser Leave(int userId) - { - MultiplayerRoomUser user = null; - - PerformUpdate(_ => - { - user = users.Find(u => u.UserID == userId); - - if (user != null) - users.Remove(user); - }); - - return user; - } - /// /// Perform an update on this room in a thread-safe manner. /// From 5d2ca7fc39be724fb6098b2c36076131220406c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 16:15:51 +0900 Subject: [PATCH 5205/6909] Make remaining model classes nullable and serializable --- .../MultiplayerClientState.cs | 2 ++ .../RealtimeMultiplayer/MultiplayerRoom.cs | 9 ++++++++- .../MultiplayerRoomSettings.cs | 7 ++++++- .../MultiplayerRoomState.cs | 2 ++ .../MultiplayerRoomUser.cs | 19 ++++++++++--------- 5 files changed, 28 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs index 17c209ba26..8c440abba3 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.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. +#nullable enable + namespace osu.Game.Online.RealtimeMultiplayer { public class MultiplayerClientState diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index dc7ced7a9a..e70e12c89e 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.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. +#nullable enable + using System; using System.Collections.Generic; @@ -25,13 +27,18 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// All currently enforced game settings for this room. /// - public MultiplayerRoomSettings Settings { get; set; } + public MultiplayerRoomSettings Settings { get; set; } = MultiplayerRoomSettings.Empty(); /// /// All users currently in this room. /// public List Users { get; set; } = new List(); + /// + /// The host of this room, in control of changing room settings. + /// + public MultiplayerRoomUser? Host { get; set; } + private object writeLock = new object(); /// diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs index 4c2c014cd9..3137afa8a4 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.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. +#nullable enable + using System; using System.Collections.Generic; using System.Linq; @@ -9,6 +11,7 @@ using osu.Game.Online.API; namespace osu.Game.Online.RealtimeMultiplayer { + [Serializable] public class MultiplayerRoomSettings : IEquatable { public int? BeatmapID { get; set; } @@ -18,8 +21,10 @@ namespace osu.Game.Online.RealtimeMultiplayer [NotNull] public IEnumerable Mods { get; set; } = Enumerable.Empty(); - public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods) && RulesetID == other?.RulesetID; + public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID; public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; + + public static MultiplayerRoomSettings Empty() => new MultiplayerRoomSettings(); } } diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs index 60e47e9027..9f76cd945e 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.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. +#nullable enable + namespace osu.Game.Online.RealtimeMultiplayer { /// diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs index 3daebe3dda..17aca4458a 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs @@ -1,27 +1,29 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using osu.Game.Users; namespace osu.Game.Online.RealtimeMultiplayer { + [Serializable] public class MultiplayerRoomUser : IEquatable { + public readonly long UserID; + + public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle; + + public User? User { get; set; } + public MultiplayerRoomUser(in int userId) { UserID = userId; } - public long UserID { get; } - - public MultiplayerUserState State { get; set; } - - public User User { get; set; } - public bool Equals(MultiplayerRoomUser other) { - if (ReferenceEquals(null, other)) return false; if (ReferenceEquals(this, other)) return true; return UserID == other.UserID; @@ -29,9 +31,8 @@ namespace osu.Game.Online.RealtimeMultiplayer public override bool Equals(object obj) { - if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; - if (obj.GetType() != this.GetType()) return false; + if (obj.GetType() != GetType()) return false; return Equals((MultiplayerRoomUser)obj); } From 5f5c0d55336a2228becd3021e2ee348c56c55d58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 16:15:58 +0900 Subject: [PATCH 5206/6909] Return room model when joining --- osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index f6eb3b8ff5..a05a4c1e11 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs @@ -12,7 +12,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The databased room ID. /// If the user is already in the requested (or another) room. - Task JoinRoom(long roomId); + Task JoinRoom(long roomId); /// /// Request to leave the currently joined room. From 71de7ce0a3abc0af75d210d54ceafca9c1f0f5b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 16:32:45 +0900 Subject: [PATCH 5207/6909] Add missing methods to server interface --- .../RealtimeMultiplayer/IMultiplayerServer.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index a05a4c1e11..44d7ecc2e9 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs @@ -18,5 +18,17 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Request to leave the currently joined room. /// Task LeaveRoom(); + + /// + /// Transfer the host of the currently joined room to another user in the room. + /// + /// The new user which is to become host. + Task TransferHost(long userId); + + /// + /// As the host, update the settings of the currently joined room. + /// + /// The new settings to apply. + Task ChangeSettings(MultiplayerRoomSettings settings); } -} \ No newline at end of file +} From a4ca8d2998dc9fa222aceeb09ae90bf013bdf542 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 16:44:47 +0900 Subject: [PATCH 5208/6909] Ensure multiplayer rooms are instantiated with a room ID --- osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index e70e12c89e..8a88d8fc90 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -17,7 +17,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The ID of the room, used for database persistence. /// - public long RoomID { get; set; } + public readonly long RoomID; /// /// The current state of the room (ie. whether it is in progress or otherwise). @@ -41,6 +41,11 @@ namespace osu.Game.Online.RealtimeMultiplayer private object writeLock = new object(); + public MultiplayerRoom(in long roomId) + { + RoomID = roomId; + } + /// /// Perform an update on this room in a thread-safe manner. /// From dbe048cdc6b6d893e92109d50fd3fa5732fa294c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 17:03:44 +0900 Subject: [PATCH 5209/6909] Add client method for notifying about host changes --- osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs b/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs index 1e6832e728..f3dff0de86 100644 --- a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs @@ -28,6 +28,12 @@ namespace osu.Game.Online.RealtimeMultiplayer /// The user. Task UserLeft(MultiplayerRoomUser user); + /// + /// Signal that the host of the room has changed. + /// + /// The user ID of the new host. + Task HostChanged(long userId); + /// /// Signals that the settings for this room have changed. /// From 11a7057289b20a195bd1be91e1a11681d2f29eb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 17:41:56 +0900 Subject: [PATCH 5210/6909] Add notification flow for user state changes in room --- .../RealtimeMultiplayer/IMultiplayerServer.cs | 7 +++++++ .../RealtimeMultiplayer/ISpectatorClient.cs | 7 +++++++ .../RealtimeMultiplayer/InvalidStateChange.cs | 15 +++++++++++++++ 3 files changed, 29 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/InvalidStateChange.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index 44d7ecc2e9..ad4aa5d2c2 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs @@ -30,5 +30,12 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The new settings to apply. Task ChangeSettings(MultiplayerRoomSettings settings); + + /// + /// Change the local user state in the currently joined room. + /// + /// The proposed new state. + /// If the state change requested is not valid, given the previous state or room state. + Task ChangeState(MultiplayerUserState newState); } } diff --git a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs b/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs index f3dff0de86..fa06652474 100644 --- a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs @@ -39,5 +39,12 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The updated room settings. Task SettingsChanged(MultiplayerRoomSettings newSettings); + + /// + /// Signals that a user in this room changed their state. + /// + /// The ID of the user performing a state change. + /// The new state of the user. + Task UserStateChanged(long userId, MultiplayerUserState state); } } diff --git a/osu.Game/Online/RealtimeMultiplayer/InvalidStateChange.cs b/osu.Game/Online/RealtimeMultiplayer/InvalidStateChange.cs new file mode 100644 index 0000000000..d1016e95cb --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/InvalidStateChange.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; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public class InvalidStateChange : Exception + { + public InvalidStateChange(MultiplayerUserState oldState, MultiplayerUserState newState) + : base($"Cannot change from {oldState} to {newState}") + { + } + } +} From 345352be6757ab9225df004307e07a48de2523dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 17:42:08 +0900 Subject: [PATCH 5211/6909] Mark PerformUpdate as an instant handle method (doesn't really help with anything) --- osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index 8a88d8fc90..3fbc01a2f5 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; namespace osu.Game.Online.RealtimeMultiplayer { @@ -50,7 +51,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Perform an update on this room in a thread-safe manner. /// /// The action to perform. - public void PerformUpdate(Action action) + public void PerformUpdate([InstantHandle] Action action) { lock (writeLock) action(this); } From 8eccfa476cbbff5a3e6ceb1e0cef9b2c0e74a683 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 18:02:14 +0900 Subject: [PATCH 5212/6909] Add loading states --- osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs index 47a1226e29..948ed50cca 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs @@ -7,6 +7,8 @@ namespace osu.Game.Online.RealtimeMultiplayer { Idle, Ready, + WaitingForLoad, + Loaded, Playing, Results, } From 60550b73f77d0ec625c070fcd6eee395e8397149 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 18:18:20 +0900 Subject: [PATCH 5213/6909] Add missing states and xmldoc for all states' purposes --- .../MultiplayerUserState.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs index 948ed50cca..ed799f5252 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs @@ -5,11 +5,51 @@ namespace osu.Game.Online.RealtimeMultiplayer { public enum MultiplayerUserState { + /// + /// The user is idle and waiting for something to happen (or watching the match but not participating). + /// Idle, + + /// + /// The user has marked themselves as ready to participate and should be considered for the next game start. + /// Ready, + + /// + /// The server is waiting for this user to finish loading. This is a reserved state, and is set by the server. + /// + /// + /// All users in state when the game start will be transitioned to this state. + /// All users in this state need to transition to before the game can start. + /// WaitingForLoad, + + /// + /// The user's client has marked itself as loaded and ready to begin gameplay. + /// Loaded, + + /// + /// The user is currently playing in a game. This is a reserved state, and is set by the server. + /// + /// + /// Once there are no remaining users, all users in state will be transitioned to this state. + /// At this point the game will start for all users. + /// Playing, + + /// + /// The user has finished playing and is ready to view results. + /// + /// + /// Once all users transition from to this state, the game will end and results will be distributed. + /// All users will be transitioned to the state. + /// + FinishedPlay, + + /// + /// The user is currently viewing results. This is a reserved state, and is set by the server. + /// Results, } } From 147db0abe22642c8f449b3f121cc8040c98f17f4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 18:28:31 +0900 Subject: [PATCH 5214/6909] Fix client naming and xmldoc --- .../{ISpectatorClient.cs => IMultiplayerClient.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game/Online/RealtimeMultiplayer/{ISpectatorClient.cs => IMultiplayerClient.cs} (96%) diff --git a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs similarity index 96% rename from osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs rename to osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs index fa06652474..e772ef39f9 100644 --- a/osu.Game/Online/RealtimeMultiplayer/ISpectatorClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs @@ -6,7 +6,7 @@ using System.Threading.Tasks; namespace osu.Game.Online.RealtimeMultiplayer { /// - /// An interface defining a spectator client instance. + /// An interface defining a multiplayer client instance. /// public interface IMultiplayerClient { From 2aedd82e27a490a09c5efe526e2e45a7120b12e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 18:30:58 +0900 Subject: [PATCH 5215/6909] Document room states and remove unnecessary WaitingForResults state --- .../RealtimeMultiplayer/MultiplayerRoomState.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs index 9f76cd945e..69c04b09a8 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs @@ -10,10 +10,24 @@ namespace osu.Game.Online.RealtimeMultiplayer /// public enum MultiplayerRoomState { + /// + /// The room is open and accepting new players. + /// Open, + + /// + /// A game start has been triggered but players have not finished loading. + /// WaitingForLoad, + + /// + /// A game is currently ongoing. + /// Playing, - WaitingForResults, + + /// + /// The room has been disbanded and closed. + /// Closed } } From 2433838d5887e84089496713857dbd8f12f797aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 18:42:08 +0900 Subject: [PATCH 5216/6909] Add methods covering match start / end --- .../RealtimeMultiplayer/IMultiplayerClient.cs | 15 +++++++++++++++ .../RealtimeMultiplayer/IMultiplayerServer.cs | 5 +++++ 2 files changed, 20 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs index e772ef39f9..1e59b62a9d 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs @@ -46,5 +46,20 @@ namespace osu.Game.Online.RealtimeMultiplayer /// The ID of the user performing a state change. /// The new state of the user. Task UserStateChanged(long userId, MultiplayerUserState state); + + /// + /// Signals that a match is to be started. Users in the state should begin loading gameplay at this point. + /// + Task LoadRequested(); + + /// + /// Signals that a match has started. All loaded users' clients should now start gameplay as soon as possible. + /// + Task MatchStarted(); + + /// + /// Signals that the match has ended, all players have finished and results are ready to be displayed. + /// + Task ResultsReady(); } } diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index ad4aa5d2c2..eb9eddbdf5 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs @@ -37,5 +37,10 @@ namespace osu.Game.Online.RealtimeMultiplayer /// The proposed new state. /// If the state change requested is not valid, given the previous state or room state. Task ChangeState(MultiplayerUserState newState); + + /// + /// As the host of a room, start the match. + /// + Task StartMatch(); } } From df908f90b2fe4ed8eab7804dbd32c0191d16c4ac Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 19:06:08 +0900 Subject: [PATCH 5217/6909] Add exception to be thrown when an operation is requested requiring host when not host --- .../RealtimeMultiplayer/IMultiplayerServer.cs | 2 ++ .../RealtimeMultiplayer/NotHostException.cs | 15 +++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/NotHostException.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index eb9eddbdf5..565c07d0ec 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs @@ -23,6 +23,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Transfer the host of the currently joined room to another user in the room. /// /// The new user which is to become host. + /// A user other than the current host is attempting to transfer host. Task TransferHost(long userId); /// @@ -41,6 +42,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// As the host of a room, start the match. /// + /// A user other than the current host is attempting to start the game. Task StartMatch(); } } diff --git a/osu.Game/Online/RealtimeMultiplayer/NotHostException.cs b/osu.Game/Online/RealtimeMultiplayer/NotHostException.cs new file mode 100644 index 0000000000..e421c6ae28 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/NotHostException.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; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public class NotHostException : Exception + { + public NotHostException() + : base("User is attempting to perform a host level operation while not the host") + { + } + } +} From 021a11609326c10de152d53963c04bebc2a77e21 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 19:06:27 +0900 Subject: [PATCH 5218/6909] Add extra xmldoc covering the fact that MatchStarted is received by all users --- osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs index 1e59b62a9d..c89d99dd1c 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs @@ -53,7 +53,7 @@ namespace osu.Game.Online.RealtimeMultiplayer Task LoadRequested(); /// - /// Signals that a match has started. All loaded users' clients should now start gameplay as soon as possible. + /// Signals that a match has started. All user in the state should begin gameplay as soon as possible. /// Task MatchStarted(); From d76fabedf90c4c45eda7e0ead815170f3b3012e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 19:44:22 +0900 Subject: [PATCH 5219/6909] Add note about LoadRequested only being sent to a subset of room users --- osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs | 2 +- osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs index c89d99dd1c..e75b0da207 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs @@ -48,7 +48,7 @@ namespace osu.Game.Online.RealtimeMultiplayer Task UserStateChanged(long userId, MultiplayerUserState state); /// - /// Signals that a match is to be started. Users in the state should begin loading gameplay at this point. + /// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point. /// Task LoadRequested(); diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs index ed799f5252..ed9acd146e 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs @@ -13,6 +13,10 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The user has marked themselves as ready to participate and should be considered for the next game start. /// + /// + /// Clients in this state will receive gameplay channel messages. + /// As a client the only thing to look for in this state is a call. + /// Ready, /// From aa68ae4ff27713f25ad42545baa905d83e29de4f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 20:10:23 +0900 Subject: [PATCH 5220/6909] Change locking mechanism for multiplayer rooms to use using-disposal pattern Was required to lock over `await` calls server-side. --- .../RealtimeMultiplayer/LockUntilDisposal.cs | 24 +++++++++++++++++++ .../RealtimeMultiplayer/MultiplayerRoom.cs | 9 ++----- 2 files changed, 26 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Online/RealtimeMultiplayer/LockUntilDisposal.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/LockUntilDisposal.cs b/osu.Game/Online/RealtimeMultiplayer/LockUntilDisposal.cs new file mode 100644 index 0000000000..cd16fce2ee --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/LockUntilDisposal.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Threading; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public readonly struct LockUntilDisposal : IDisposable + { + private readonly object lockTarget; + + public LockUntilDisposal(object lockTarget) + { + this.lockTarget = lockTarget; + Monitor.Enter(lockTarget); + } + + public void Dispose() + { + Monitor.Exit(lockTarget); + } + } +} diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index 3fbc01a2f5..0f18ab6c89 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; -using JetBrains.Annotations; namespace osu.Game.Online.RealtimeMultiplayer { @@ -48,12 +47,8 @@ namespace osu.Game.Online.RealtimeMultiplayer } /// - /// Perform an update on this room in a thread-safe manner. + /// Request a lock on this room to perform a thread-safe update. /// - /// The action to perform. - public void PerformUpdate([InstantHandle] Action action) - { - lock (writeLock) action(this); - } + public LockUntilDisposal LockForUpdate() => new LockUntilDisposal(writeLock); } } From ed50fd445e81589df6375b284fbb003617bd129a Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 21:07:12 +0900 Subject: [PATCH 5221/6909] Fix hit lighting colour not applied in TestSceneCatcher --- .../TestSceneCatcher.cs | 59 +++++++++++++------ 1 file changed, 41 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index cf6011d721..94cd0ec8a7 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; using osu.Game.Tests.Visual; namespace osu.Game.Rulesets.Catch.Tests @@ -65,13 +66,11 @@ namespace osu.Game.Rulesets.Catch.Tests JudgementResult result2 = null; AddStep("catch hyper fruit", () => { - drawableObject1 = createDrawableObject(new Fruit { HyperDashTarget = new Fruit { X = 100 } }); - result1 = attemptCatch(drawableObject1); + attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1); }); AddStep("catch normal fruit", () => { - drawableObject2 = createDrawableObject(new Fruit()); - result2 = attemptCatch(drawableObject2); + attemptCatch(new Fruit(), out drawableObject2, out result2); }); AddStep("revert second result", () => { @@ -92,8 +91,7 @@ namespace osu.Game.Rulesets.Catch.Tests JudgementResult result = null; AddStep("catch kiai fruit", () => { - drawableObject = createDrawableObject(new TestKiaiFruit()); - result = attemptCatch(drawableObject); + attemptCatch(new TestKiaiFruit(), out drawableObject, out result); }); checkState(CatcherAnimationState.Kiai); AddStep("revert result", () => @@ -200,13 +198,22 @@ namespace osu.Game.Rulesets.Catch.Tests AddAssert("fruits are dropped", () => !catcher.CaughtObjects.Any() && droppedObjectContainer.Count == 10); } - [TestCase(true)] - [TestCase(false)] - public void TestHitLighting(bool enabled) + [Test] + public void TestHitLightingColour() { - AddStep($"{(enabled ? "enable" : "disable")} hit lighting", () => config.Set(OsuSetting.HitLighting, enabled)); + var fruitColour = SkinConfiguration.DefaultComboColours[1]; + AddStep("enable hit lighting", () => config.Set(OsuSetting.HitLighting, true)); AddStep("catch fruit", () => attemptCatch(new Fruit())); - AddAssert("check hit lighting", () => catcher.ChildrenOfType().Any() == enabled); + AddAssert("correct hit lighting colour", () => + catcher.ChildrenOfType().First()?.ObjectColour == fruitColour); + } + + [Test] + public void TestHitLightingDisabled() + { + AddStep("disable hit lighting", () => config.Set(OsuSetting.HitLighting, false)); + AddStep("catch fruit", () => attemptCatch(new Fruit())); + AddAssert("no hit lighting", () => !catcher.ChildrenOfType().Any()); } private void checkPlate(int count) => AddAssert($"{count} objects on the plate", () => catcher.CaughtObjects.Count() == count); @@ -218,18 +225,34 @@ namespace osu.Game.Rulesets.Catch.Tests private void attemptCatch(CatchHitObject hitObject, int count = 1) { for (var i = 0; i < count; i++) - attemptCatch(createDrawableObject(hitObject)); + attemptCatch(hitObject, out _, out _); } - private JudgementResult attemptCatch(DrawableCatchHitObject drawableObject) + private void attemptCatch(CatchHitObject hitObject, out DrawableCatchHitObject drawableObject, out JudgementResult result) { - drawableObject.HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); - var result = new CatchJudgementResult(drawableObject.HitObject, drawableObject.HitObject.CreateJudgement()) + hitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + drawableObject = createDrawableObject(hitObject); + result = createResult(hitObject); + applyResult(drawableObject, result); + } + + private void applyResult(DrawableCatchHitObject drawableObject, JudgementResult result) + { + // Load DHO to set colour of hit explosion correctly + Add(drawableObject); + drawableObject.OnLoadComplete += _ => { - Type = catcher.CanCatch(drawableObject.HitObject) ? HitResult.Great : HitResult.Miss + catcher.OnNewResult(drawableObject, result); + drawableObject.Expire(); + }; + } + + private JudgementResult createResult(CatchHitObject hitObject) + { + return new CatchJudgementResult(hitObject, hitObject.CreateJudgement()) + { + Type = catcher.CanCatch(hitObject) ? HitResult.Great : HitResult.Miss }; - catcher.OnNewResult(drawableObject, result); - return result; } private DrawableCatchHitObject createDrawableObject(CatchHitObject hitObject) From c301223d8cf12897e6d0327b11c505b59798be3d Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 20:34:08 +0900 Subject: [PATCH 5222/6909] Make object on the catcher plate separate CaughtObject class --- .../TestSceneCatcher.cs | 2 +- .../Objects/Drawables/CaughtObject.cs | 83 +++++++++++++++++++ .../Drawables/DrawableCatchHitObject.cs | 4 - .../Objects/Drawables/DrawableDroplet.cs | 2 - .../DrawablePalpableCatchHitObject.cs | 7 +- .../Skinning/Default/CatchHitObjectPiece.cs | 16 +++- .../Skinning/Default/FruitPiece.cs | 4 + .../Skinning/Legacy/LegacyFruitPiece.cs | 4 + .../Skinning/LegacyCatchHitObjectPiece.cs | 7 ++ osu.Game.Rulesets.Catch/UI/Catcher.cs | 76 ++++++----------- 10 files changed, 141 insertions(+), 64 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 94cd0ec8a7..2747524503 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -275,7 +275,7 @@ namespace osu.Game.Rulesets.Catch.Tests public class TestCatcher : Catcher { - public IEnumerable CaughtObjects => this.ChildrenOfType(); + public IEnumerable CaughtObjects => this.ChildrenOfType(); public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty) : base(trailsTarget, droppedObjectTarget, difficulty) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs new file mode 100644 index 0000000000..0f3a143553 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -0,0 +1,83 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Rulesets.Catch.Skinning.Default; +using osu.Game.Skinning; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + [Cached(typeof(CaughtObject))] + public abstract class CaughtObject : SkinnableDrawable + { + public readonly Bindable AccentColour = new Bindable(); + + public CatchHitObject HitObject { get; private set; } + + /// + /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. + /// + public virtual bool StaysOnPlate => true; + + public override bool RemoveWhenNotAlive => true; + + protected CaughtObject(CatchSkinComponents skinComponent, Func defaultImplementation) + : base(new CatchSkinComponent(skinComponent), defaultImplementation) + { + Anchor = Anchor.TopCentre; + Origin = Anchor.Centre; + + RelativeSizeAxes = Axes.None; + Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); + } + + public virtual void CopyFrom(DrawablePalpableCatchHitObject drawableObject) + { + HitObject = drawableObject.HitObject; + Scale = drawableObject.Scale / 2; + Rotation = drawableObject.Rotation; + AccentColour.Value = drawableObject.AccentColour.Value; + } + } + + public class CaughtFruit : CaughtObject + { + public readonly Bindable VisualRepresentation = new Bindable(); + + public CaughtFruit() + : base(CatchSkinComponents.Fruit, _ => new FruitPiece()) + { + } + + public override void CopyFrom(DrawablePalpableCatchHitObject drawableObject) + { + base.CopyFrom(drawableObject); + + var drawableFruit = (DrawableFruit)drawableObject; + VisualRepresentation.Value = drawableFruit.VisualRepresentation.Value; + } + } + + public class CaughtBanana : CaughtObject + { + public CaughtBanana() + : base(CatchSkinComponents.Banana, _ => new BananaPiece()) + { + } + } + + public class CaughtDroplet : CaughtObject + { + public override bool StaysOnPlate => false; + + public CaughtDroplet() + : base(CatchSkinComponents.Droplet, _ => new DropletPiece()) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index 70efe9cf29..bfd124c691 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -50,10 +50,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public Func CheckPosition; - public bool IsOnPlate; - - public override bool RemoveWhenNotAlive => IsOnPlate; - protected override JudgementResult CreateResult(Judgement judgement) => new CatchJudgementResult(HitObject, judgement); protected override void CheckForResult(bool userTriggered, double timeOffset) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index d96b1c8902..c5f0bb8b18 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -11,8 +11,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public class DrawableDroplet : DrawablePalpableCatchHitObject { - public override bool StaysOnPlate => false; - public DrawableDroplet() : this(null) { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index b99710ae8e..34d9cf820e 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -24,11 +24,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables /// protected virtual float ScaleFactor => 1; - /// - /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. - /// - public virtual bool StaysOnPlate => true; - public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor; protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) @@ -43,7 +38,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { XBindable.BindValueChanged(x => { - if (!IsOnPlate) X = x.NewValue; + X = x.NewValue; }, true); ScaleBindable.BindValueChanged(scale => diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs index d59b6cc0de..cb326b77e2 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs @@ -21,6 +21,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default [CanBeNull] protected DrawableHitObject DrawableHitObject { get; private set; } + [Resolved(canBeNull: true)] + [CanBeNull] + protected CaughtObject CaughtObject { get; private set; } + /// /// A part of this piece that will be faded out while falling in the playfield. /// @@ -45,6 +49,9 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default HyperDash.BindTo(hitObject.HyperDash); } + if (CaughtObject != null) + AccentColour.BindTo(CaughtObject.AccentColour); + HyperDash.BindValueChanged(hyper => { if (HyperBorderPiece != null) @@ -54,8 +61,13 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default protected override void Update() { - if (BorderPiece != null && DrawableHitObject?.HitObject != null) - BorderPiece.Alpha = (float)Math.Clamp((DrawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1); + if (BorderPiece != null) + { + if (DrawableHitObject?.HitObject != null) + BorderPiece.Alpha = (float)Math.Clamp((DrawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1); + else + BorderPiece.Alpha = 0; + } } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs index 2e3803a31a..8c705cec4f 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs @@ -43,6 +43,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default if (fruit != null) VisualRepresentation.BindTo(fruit.VisualRepresentation); + + var caughtFruit = (CaughtFruit)CaughtObject; + if (caughtFruit != null) + VisualRepresentation.BindTo(caughtFruit.VisualRepresentation); } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs index 6f93e68594..5140c62d3e 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs @@ -19,6 +19,10 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy if (fruit != null) VisualRepresentation.BindTo(fruit.VisualRepresentation); + var caughtFruit = (CaughtFruit)CaughtObject; + if (caughtFruit != null) + VisualRepresentation.BindTo(caughtFruit.VisualRepresentation); + VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true); } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs index 1e68439402..9527f15ff1 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs @@ -33,6 +33,10 @@ namespace osu.Game.Rulesets.Catch.Skinning [CanBeNull] protected DrawableHitObject DrawableHitObject { get; private set; } + [Resolved(canBeNull: true)] + [CanBeNull] + protected CaughtObject CaughtObject { get; private set; } + protected LegacyCatchHitObjectPiece() { RelativeSizeAxes = Axes.Both; @@ -73,6 +77,9 @@ namespace osu.Game.Rulesets.Catch.Skinning HyperDash.BindTo(hitObject.HyperDash); } + if (CaughtObject != null) + AccentColour.BindTo(CaughtObject.AccentColour); + hyperSprite.Colour = Skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ?? Skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? Catcher.DEFAULT_HYPER_DASH_COLOUR; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index ac0f25e21b..756582e3f2 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -55,7 +55,7 @@ namespace osu.Game.Rulesets.Catch.UI private readonly Container droppedObjectTarget; - private readonly Container caughtFruitContainer; + private readonly Container caughtFruitContainer; public CatcherAnimationState CurrentState { get; private set; } @@ -124,7 +124,7 @@ namespace osu.Game.Rulesets.Catch.UI InternalChildren = new Drawable[] { hitExplosionPool = new DrawablePool(10), - caughtFruitContainer = new Container + caughtFruitContainer = new Container { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Catch.UI { var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplayRadius); - placeCaughtObject(hitObject, positionInStack); + placeCaughtObject(palpableObject, positionInStack); if (hitLighting.Value) addLighting(hitObject, positionInStack.X, drawableObject.AccentColour.Value); @@ -450,21 +450,14 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } - private void placeCaughtObject(PalpableCatchHitObject source, Vector2 position) + private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position) { - var caughtObject = createCaughtObject(source); + var caughtObject = createCaughtObject(drawableObject.HitObject); if (caughtObject == null) return; - caughtObject.RelativePositionAxes = Axes.None; + caughtObject.CopyFrom(drawableObject); caughtObject.Position = position; - caughtObject.IsOnPlate = true; - - caughtObject.Anchor = Anchor.TopCentre; - caughtObject.Origin = Anchor.Centre; - caughtObject.Scale *= 0.5f; - caughtObject.LifetimeStart = source.StartTime; - caughtObject.LifetimeEnd = double.MaxValue; caughtFruitContainer.Add(caughtObject); @@ -500,21 +493,18 @@ namespace osu.Game.Rulesets.Catch.UI hitExplosionContainer.Add(hitExplosion); } - private DrawablePalpableCatchHitObject createCaughtObject(PalpableCatchHitObject source) + private CaughtObject createCaughtObject(PalpableCatchHitObject source) { switch (source) { - case Banana banana: - return new DrawableBanana(banana); + case Fruit _: + return new CaughtFruit(); - case Fruit fruit: - return new DrawableFruit(fruit); + case Banana _: + return new CaughtBanana(); - case TinyDroplet tiny: - return new DrawableTinyDroplet(tiny); - - case Droplet droplet: - return new DrawableDroplet(droplet); + case Droplet _: + return new CaughtDroplet(); default: return null; @@ -532,7 +522,7 @@ namespace osu.Game.Rulesets.Catch.UI drop(caughtObject, animation); } - private void removeFromPlate(DrawablePalpableCatchHitObject caughtObject, DroppedObjectAnimation animation) + private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation) { if (!caughtFruitContainer.Remove(caughtObject)) throw new InvalidOperationException("Can only drop a caught object on the plate"); @@ -542,40 +532,28 @@ namespace osu.Game.Rulesets.Catch.UI drop(caughtObject, animation); } - private void drop(DrawablePalpableCatchHitObject d, DroppedObjectAnimation animation) + private void drop(CaughtObject d, DroppedObjectAnimation animation) { var originalX = d.X * Scale.X; - var startTime = Clock.CurrentTime; d.Anchor = Anchor.TopLeft; d.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(d.DrawPosition, droppedObjectTarget); - // we cannot just apply the transforms because DHO clears transforms when state is updated - d.ApplyCustomUpdateState += (o, state) => animate(o, animation, originalX, startTime); - if (d.IsLoaded) - animate(d, animation, originalX, startTime); - } - - private void animate(Drawable d, DroppedObjectAnimation animation, float originalX, double startTime) - { - using (d.BeginAbsoluteSequence(startTime)) + switch (animation) { - switch (animation) - { - case DroppedObjectAnimation.Drop: - d.MoveToY(d.Y + 75, 750, Easing.InSine); - d.FadeOut(750); - break; + case DroppedObjectAnimation.Drop: + d.MoveToY(d.Y + 75, 750, Easing.InSine); + d.FadeOut(750); + break; - case DroppedObjectAnimation.Explode: - d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine); - d.MoveToX(d.X + originalX * 6, 1000); - d.FadeOut(750); - break; - } - - d.Expire(); + case DroppedObjectAnimation.Explode: + d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine); + d.MoveToX(d.X + originalX * 6, 1000); + d.FadeOut(750); + break; } + + d.Expire(); } private enum DroppedObjectAnimation From 02f5fda330306b6ae9b3a67dad6f50b2373562c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 8 Dec 2020 21:15:10 +0900 Subject: [PATCH 5223/6909] Add missing final newline in file --- osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs b/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs index 1b264d1ac5..ca4a54f6d7 100644 --- a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs +++ b/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs @@ -9,4 +9,4 @@ namespace osu.Game.Online.RealtimeMultiplayer { } } -} \ No newline at end of file +} From a32dac00ddf388f2359dd97ebd23fc7faa68d687 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 21:29:03 +0900 Subject: [PATCH 5224/6909] Introduce IHasCatchObjectState implemented by DHO and CaughtObject --- .../Objects/Drawables/CaughtObject.cs | 31 ++++++++++--------- .../Objects/Drawables/DrawableBanana.cs | 2 +- .../Objects/Drawables/DrawableDroplet.cs | 2 +- .../Objects/Drawables/DrawableFruit.cs | 4 +-- ...s => DrawablePalpableHasCatchHitObject.cs} | 14 ++++++--- .../Objects/Drawables/IHasCatchObjectState.cs | 24 ++++++++++++++ .../Skinning/Default/CatchHitObjectPiece.cs | 31 ++++--------------- .../Skinning/Default/FruitPiece.cs | 10 ++---- .../Skinning/Legacy/LegacyFruitPiece.cs | 10 ++---- .../Skinning/LegacyCatchHitObjectPiece.cs | 23 +++----------- osu.Game.Rulesets.Catch/UI/Catcher.cs | 5 +-- 11 files changed, 70 insertions(+), 86 deletions(-) rename osu.Game.Rulesets.Catch/Objects/Drawables/{DrawablePalpableCatchHitObject.cs => DrawablePalpableHasCatchHitObject.cs} (78%) create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs index 0f3a143553..d3555ea771 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -12,12 +12,12 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - [Cached(typeof(CaughtObject))] - public abstract class CaughtObject : SkinnableDrawable + [Cached(typeof(IHasCatchObjectState))] + public abstract class CaughtObject : SkinnableDrawable, IHasCatchObjectState { - public readonly Bindable AccentColour = new Bindable(); - public CatchHitObject HitObject { get; private set; } + public Bindable AccentColour { get; } = new Bindable(); + public Bindable HyperDash { get; } = new Bindable(); /// /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. @@ -36,30 +36,31 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); } - public virtual void CopyFrom(DrawablePalpableCatchHitObject drawableObject) + public virtual void CopyFrom(IHasCatchObjectState objectState) { - HitObject = drawableObject.HitObject; - Scale = drawableObject.Scale / 2; - Rotation = drawableObject.Rotation; - AccentColour.Value = drawableObject.AccentColour.Value; + HitObject = objectState.HitObject; + Scale = objectState.Scale; + Rotation = objectState.Rotation; + AccentColour.Value = objectState.AccentColour.Value; + HyperDash.Value = objectState.HyperDash.Value; } } - public class CaughtFruit : CaughtObject + public class CaughtFruit : CaughtObject, IHasFruitState { - public readonly Bindable VisualRepresentation = new Bindable(); + public Bindable VisualRepresentation { get; } = new Bindable(); public CaughtFruit() : base(CatchSkinComponents.Fruit, _ => new FruitPiece()) { } - public override void CopyFrom(DrawablePalpableCatchHitObject drawableObject) + public override void CopyFrom(IHasCatchObjectState objectState) { - base.CopyFrom(drawableObject); + base.CopyFrom(objectState); - var drawableFruit = (DrawableFruit)drawableObject; - VisualRepresentation.Value = drawableFruit.VisualRepresentation.Value; + var fruitState = (IHasFruitState)objectState; + VisualRepresentation.Value = fruitState.VisualRepresentation.Value; } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 4bc5da2396..34d89eb188 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableBanana : DrawablePalpableCatchHitObject + public class DrawableBanana : DrawablePalpableHasCatchHitObject { public DrawableBanana() : this(null) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index c5f0bb8b18..acdb3bb38c 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableDroplet : DrawablePalpableCatchHitObject + public class DrawableDroplet : DrawablePalpableHasCatchHitObject { public DrawableDroplet() : this(null) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 8135a31b43..6ec46f6535 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -10,9 +10,9 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableFruit : DrawablePalpableCatchHitObject + public class DrawableFruit : DrawablePalpableHasCatchHitObject, IHasFruitState { - public readonly Bindable VisualRepresentation = new Bindable(); + public Bindable VisualRepresentation { get; } = new Bindable(); public DrawableFruit() : this(null) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableHasCatchHitObject.cs similarity index 78% rename from osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableHasCatchHitObject.cs index 34d9cf820e..ee893f7880 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableHasCatchHitObject.cs @@ -6,18 +6,22 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osuTK; +using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject + [Cached(typeof(IHasCatchObjectState))] + public abstract class DrawablePalpableHasCatchHitObject : DrawableCatchHitObject, IHasCatchObjectState { public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; - public readonly Bindable HyperDash = new Bindable(); + Bindable IHasCatchObjectState.AccentColour => AccentColour; - public readonly Bindable ScaleBindable = new Bindable(1); + public Bindable HyperDash { get; } = new Bindable(); - public readonly Bindable IndexInBeatmap = new Bindable(); + public Bindable ScaleBindable { get; } = new Bindable(1); + + public Bindable IndexInBeatmap { get; } = new Bindable(); /// /// The multiplicative factor applied to relative to scale. @@ -26,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor; - protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) + protected DrawablePalpableHasCatchHitObject([CanBeNull] CatchHitObject h) : base(h) { Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs new file mode 100644 index 0000000000..01d833b61a --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.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.Bindables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + public interface IHasCatchObjectState + { + CatchHitObject HitObject { get; } + Bindable AccentColour { get; } + Bindable HyperDash { get; } + + float Rotation { get; } + Vector2 Scale { get; } + } + + public interface IHasFruitState : IHasCatchObjectState + { + Bindable VisualRepresentation { get; } + } +} diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs index cb326b77e2..b105fb4034 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Rulesets.Catch.Objects.Drawables; -using osu.Game.Rulesets.Objects.Drawables; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Skinning.Default @@ -17,13 +16,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default public readonly Bindable AccentColour = new Bindable(); public readonly Bindable HyperDash = new Bindable(); - [Resolved(canBeNull: true)] - [CanBeNull] - protected DrawableHitObject DrawableHitObject { get; private set; } - - [Resolved(canBeNull: true)] - [CanBeNull] - protected CaughtObject CaughtObject { get; private set; } + [Resolved] + protected IHasCatchObjectState ObjectState { get; private set; } /// /// A part of this piece that will be faded out while falling in the playfield. @@ -41,16 +35,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default { base.LoadComplete(); - var hitObject = (DrawablePalpableCatchHitObject)DrawableHitObject; - - if (hitObject != null) - { - AccentColour.BindTo(hitObject.AccentColour); - HyperDash.BindTo(hitObject.HyperDash); - } - - if (CaughtObject != null) - AccentColour.BindTo(CaughtObject.AccentColour); + AccentColour.BindTo(ObjectState.AccentColour); + HyperDash.BindTo(ObjectState.HyperDash); HyperDash.BindValueChanged(hyper => { @@ -61,13 +47,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default protected override void Update() { - if (BorderPiece != null) - { - if (DrawableHitObject?.HitObject != null) - BorderPiece.Alpha = (float)Math.Clamp((DrawableHitObject.HitObject.StartTime - Time.Current) / 500, 0, 1); - else - BorderPiece.Alpha = 0; - } + if (BorderPiece != null && ObjectState?.HitObject != null) + BorderPiece.Alpha = (float)Math.Clamp((ObjectState.HitObject.StartTime - Time.Current) / 500, 0, 1); } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs index 8c705cec4f..49f128c960 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/FruitPiece.cs @@ -39,14 +39,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default { base.LoadComplete(); - var fruit = (DrawableFruit)DrawableHitObject; - - if (fruit != null) - VisualRepresentation.BindTo(fruit.VisualRepresentation); - - var caughtFruit = (CaughtFruit)CaughtObject; - if (caughtFruit != null) - VisualRepresentation.BindTo(caughtFruit.VisualRepresentation); + var fruitState = (IHasFruitState)ObjectState; + VisualRepresentation.BindTo(fruitState.VisualRepresentation); } } } diff --git a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs index 5140c62d3e..969cc38e5b 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Legacy/LegacyFruitPiece.cs @@ -14,14 +14,8 @@ namespace osu.Game.Rulesets.Catch.Skinning.Legacy { base.LoadComplete(); - var fruit = (DrawableFruit)DrawableHitObject; - - if (fruit != null) - VisualRepresentation.BindTo(fruit.VisualRepresentation); - - var caughtFruit = (CaughtFruit)CaughtObject; - if (caughtFruit != null) - VisualRepresentation.BindTo(caughtFruit.VisualRepresentation); + var fruitState = (IHasFruitState)ObjectState; + VisualRepresentation.BindTo(fruitState.VisualRepresentation); VisualRepresentation.BindValueChanged(visual => setTexture(visual.NewValue), true); } diff --git a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs index 9527f15ff1..4b1f5a4724 100644 --- a/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/LegacyCatchHitObjectPiece.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 JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -10,7 +9,6 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; 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; using osuTK.Graphics; @@ -29,13 +27,8 @@ namespace osu.Game.Rulesets.Catch.Skinning [Resolved] protected ISkinSource Skin { get; private set; } - [Resolved(canBeNull: true)] - [CanBeNull] - protected DrawableHitObject DrawableHitObject { get; private set; } - - [Resolved(canBeNull: true)] - [CanBeNull] - protected CaughtObject CaughtObject { get; private set; } + [Resolved] + protected IHasCatchObjectState ObjectState { get; private set; } protected LegacyCatchHitObjectPiece() { @@ -69,16 +62,8 @@ namespace osu.Game.Rulesets.Catch.Skinning { base.LoadComplete(); - var hitObject = (DrawablePalpableCatchHitObject)DrawableHitObject; - - if (hitObject != null) - { - AccentColour.BindTo(hitObject.AccentColour); - HyperDash.BindTo(hitObject.HyperDash); - } - - if (CaughtObject != null) - AccentColour.BindTo(CaughtObject.AccentColour); + AccentColour.BindTo(ObjectState.AccentColour); + HyperDash.BindTo(ObjectState.HyperDash); hyperSprite.Colour = Skin.GetConfig(CatchSkinColour.HyperDashFruit)?.Value ?? Skin.GetConfig(CatchSkinColour.HyperDash)?.Value ?? diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 756582e3f2..4d540bce03 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -215,7 +215,7 @@ namespace osu.Game.Rulesets.Catch.UI catchResult.CatcherAnimationState = CurrentState; catchResult.CatcherHyperDash = HyperDashing; - if (!(drawableObject is DrawablePalpableCatchHitObject palpableObject)) return; + if (!(drawableObject is DrawablePalpableHasCatchHitObject palpableObject)) return; var hitObject = palpableObject.HitObject; @@ -450,7 +450,7 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } - private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position) + private void placeCaughtObject(DrawablePalpableHasCatchHitObject drawableObject, Vector2 position) { var caughtObject = createCaughtObject(drawableObject.HitObject); @@ -458,6 +458,7 @@ namespace osu.Game.Rulesets.Catch.UI caughtObject.CopyFrom(drawableObject); caughtObject.Position = position; + caughtObject.Scale /= 2; caughtFruitContainer.Add(caughtObject); From 168ba625006f86ca7fd758adcec2e24af1169f40 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 8 Dec 2020 22:09:48 +0900 Subject: [PATCH 5225/6909] Port StanR's dynamic SO pp changes --- osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs | 1 + osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs | 2 ++ osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs index fff033357d..e8ac60bc5e 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyAttributes.cs @@ -12,5 +12,6 @@ namespace osu.Game.Rulesets.Osu.Difficulty public double ApproachRate; public double OverallDifficulty; public int HitCircleCount; + public int SpinnerCount; } } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 6027635b75..6a7d76151c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -48,6 +48,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty maxCombo += beatmap.HitObjects.OfType().Sum(s => s.NestedHitObjects.Count - 1); int hitCirclesCount = beatmap.HitObjects.Count(h => h is HitCircle); + int spinnerCount = beatmap.HitObjects.Count(h => h is Spinner); return new OsuDifficultyAttributes { @@ -59,6 +60,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty OverallDifficulty = (80 - hitWindowGreat) / 6, MaxCombo = maxCombo, HitCircleCount = hitCirclesCount, + SpinnerCount = spinnerCount, Skills = skills }; } diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 0ebe0ddc2d..030b0cf6d1 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -52,7 +52,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty multiplier *= 0.90; if (mods.Any(m => m is OsuModSpunOut)) - multiplier *= 0.95; + multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); double aimValue = computeAimValue(); double speedValue = computeSpeedValue(); From 749d5380ca5a1c5bd118307e4f9459cd7df00130 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 22:38:10 +0900 Subject: [PATCH 5226/6909] Pool caught objects and dropped objects --- .../TestSceneCatcherArea.cs | 5 +- .../Objects/Drawables/CaughtObject.cs | 14 ++++- .../Objects/Drawables/IHasCatchObjectState.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 52 +++++++++++++------ 4 files changed, 53 insertions(+), 20 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 8602c7aad1..3d5e44476e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -73,7 +73,10 @@ namespace osu.Game.Rulesets.Catch.Tests SetContents(() => { - var droppedObjectContainer = new Container(); + var droppedObjectContainer = new Container + { + RelativeSizeAxes = Axes.Both + }; return new CatchInputManager(catchRuleset) { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs index d3555ea771..f36d287126 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -15,7 +15,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [Cached(typeof(IHasCatchObjectState))] public abstract class CaughtObject : SkinnableDrawable, IHasCatchObjectState { - public CatchHitObject HitObject { get; private set; } + public PalpableCatchHitObject HitObject { get; private set; } public Bindable AccentColour { get; } = new Bindable(); public Bindable HyperDash { get; } = new Bindable(); @@ -29,7 +29,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected CaughtObject(CatchSkinComponents skinComponent, Func defaultImplementation) : base(new CatchSkinComponent(skinComponent), defaultImplementation) { - Anchor = Anchor.TopCentre; Origin = Anchor.Centre; RelativeSizeAxes = Axes.None; @@ -44,6 +43,17 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables AccentColour.Value = objectState.AccentColour.Value; HyperDash.Value = objectState.HyperDash.Value; } + + protected override void FreeAfterUse() + { + ClearTransforms(); + + Alpha = 1; + LifetimeStart = double.MinValue; + LifetimeEnd = double.MaxValue; + + base.FreeAfterUse(); + } } public class CaughtFruit : CaughtObject, IHasFruitState diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs index 01d833b61a..a282bc5da0 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public interface IHasCatchObjectState { - CatchHitObject HitObject { get; } + PalpableCatchHitObject HitObject { get; } Bindable AccentColour { get; } Bindable HyperDash { get; } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 4d540bce03..7aecd95efd 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -108,6 +108,10 @@ namespace osu.Game.Rulesets.Catch.UI private readonly DrawablePool hitExplosionPool; private readonly Container hitExplosionContainer; + private readonly DrawablePool caughtFruitPool; + private readonly DrawablePool caughtBananaPool; + private readonly DrawablePool caughtDropletPool; + public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null) { this.trailsTarget = trailsTarget; @@ -124,6 +128,10 @@ namespace osu.Game.Rulesets.Catch.UI InternalChildren = new Drawable[] { hitExplosionPool = new DrawablePool(10), + caughtFruitPool = new DrawablePool(50), + caughtBananaPool = new DrawablePool(100), + // less capacity is needed compared to fruit because droplet is not stacked + caughtDropletPool = new DrawablePool(25), caughtFruitContainer = new Container { Anchor = Anchor.TopCentre, @@ -452,11 +460,12 @@ namespace osu.Game.Rulesets.Catch.UI private void placeCaughtObject(DrawablePalpableHasCatchHitObject drawableObject, Vector2 position) { - var caughtObject = createCaughtObject(drawableObject.HitObject); + var caughtObject = getCaughtObject(drawableObject.HitObject); if (caughtObject == null) return; caughtObject.CopyFrom(drawableObject); + caughtObject.Anchor = Anchor.TopCentre; caughtObject.Position = position; caughtObject.Scale /= 2; @@ -494,52 +503,62 @@ namespace osu.Game.Rulesets.Catch.UI hitExplosionContainer.Add(hitExplosion); } - private CaughtObject createCaughtObject(PalpableCatchHitObject source) + private CaughtObject getCaughtObject(PalpableCatchHitObject source) { switch (source) { case Fruit _: - return new CaughtFruit(); + return caughtFruitPool.Get(); case Banana _: - return new CaughtBanana(); + return caughtBananaPool.Get(); case Droplet _: - return new CaughtDroplet(); + return caughtDropletPool.Get(); default: return null; } } + private CaughtObject getDroppedObject(CaughtObject caughtObject) + { + var droppedObject = getCaughtObject(caughtObject.HitObject); + + droppedObject.CopyFrom(caughtObject); + droppedObject.Anchor = Anchor.TopLeft; + droppedObject.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget); + + return droppedObject; + } + private void clearPlate(DroppedObjectAnimation animation) { var caughtObjects = caughtFruitContainer.Children.ToArray(); + var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray(); + caughtFruitContainer.Clear(false); - droppedObjectTarget.AddRange(caughtObjects); + droppedObjectTarget.AddRange(droppedObjects); - foreach (var caughtObject in caughtObjects) - drop(caughtObject, animation); + foreach (var droppedObject in droppedObjects) + applyDropAnimation(droppedObject, animation); } private void removeFromPlate(CaughtObject caughtObject, DroppedObjectAnimation animation) { + var droppedObject = getDroppedObject(caughtObject); + if (!caughtFruitContainer.Remove(caughtObject)) throw new InvalidOperationException("Can only drop a caught object on the plate"); - droppedObjectTarget.Add(caughtObject); + droppedObjectTarget.Add(droppedObject); - drop(caughtObject, animation); + applyDropAnimation(droppedObject, animation); } - private void drop(CaughtObject d, DroppedObjectAnimation animation) + private void applyDropAnimation(Drawable d, DroppedObjectAnimation animation) { - var originalX = d.X * Scale.X; - - d.Anchor = Anchor.TopLeft; - d.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(d.DrawPosition, droppedObjectTarget); - switch (animation) { case DroppedObjectAnimation.Drop: @@ -548,6 +567,7 @@ namespace osu.Game.Rulesets.Catch.UI break; case DroppedObjectAnimation.Explode: + var originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtFruitContainer).X * Scale.X; d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine); d.MoveToX(d.X + originalX * 6, 1000); d.FadeOut(750); From 1f36bbecd180049026e62f592201434c3cf2c283 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 23:07:30 +0900 Subject: [PATCH 5227/6909] Fix dropped objects not removed on revert result --- 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 7aecd95efd..d2930af1ca 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -274,7 +274,7 @@ namespace osu.Game.Rulesets.Catch.UI } caughtFruitContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); - droppedObjectTarget.RemoveAll(d => (d as DrawableCatchHitObject)?.HitObject == drawableObject.HitObject); + droppedObjectTarget.RemoveAll(d => (d as CaughtObject)?.HitObject == drawableObject.HitObject); hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); } From 5ca98b00334946cf1f0e0a876cd268374f3a7267 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 23:11:22 +0900 Subject: [PATCH 5228/6909] Add doc comments a bit --- osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs | 3 +++ .../Objects/Drawables/IHasCatchObjectState.cs | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs index f36d287126..01bf943e1a 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -12,6 +12,9 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { + /// + /// Represents a caught by the catcher. + /// [Cached(typeof(IHasCatchObjectState))] public abstract class CaughtObject : SkinnableDrawable, IHasCatchObjectState { diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs index a282bc5da0..0a75fb2224 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs @@ -7,6 +7,9 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { + /// + /// Provides a visual state of a . + /// public interface IHasCatchObjectState { PalpableCatchHitObject HitObject { get; } @@ -17,6 +20,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Vector2 Scale { get; } } + /// + /// Provides a visual state of a . + /// public interface IHasFruitState : IHasCatchObjectState { Bindable VisualRepresentation { get; } From 1212ffd24f7c6aae776b796fdab47dec3673d2e8 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Tue, 8 Dec 2020 23:35:24 +0900 Subject: [PATCH 5229/6909] Rename to CopyStateFrom, and add comment --- .../Objects/Drawables/CaughtObject.cs | 9 ++++++--- osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs index 01bf943e1a..87319a4498 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -38,7 +38,10 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); } - public virtual void CopyFrom(IHasCatchObjectState objectState) + /// + /// Copies the hit object visual state from another object. + /// + public virtual void CopyStateFrom(IHasCatchObjectState objectState) { HitObject = objectState.HitObject; Scale = objectState.Scale; @@ -68,9 +71,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { } - public override void CopyFrom(IHasCatchObjectState objectState) + public override void CopyStateFrom(IHasCatchObjectState objectState) { - base.CopyFrom(objectState); + base.CopyStateFrom(objectState); var fruitState = (IHasFruitState)objectState; VisualRepresentation.Value = fruitState.VisualRepresentation.Value; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index d2930af1ca..c9580ca5f8 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -464,7 +464,7 @@ namespace osu.Game.Rulesets.Catch.UI if (caughtObject == null) return; - caughtObject.CopyFrom(drawableObject); + caughtObject.CopyStateFrom(drawableObject); caughtObject.Anchor = Anchor.TopCentre; caughtObject.Position = position; caughtObject.Scale /= 2; @@ -525,7 +525,7 @@ namespace osu.Game.Rulesets.Catch.UI { var droppedObject = getCaughtObject(caughtObject.HitObject); - droppedObject.CopyFrom(caughtObject); + droppedObject.CopyStateFrom(caughtObject); droppedObject.Anchor = Anchor.TopLeft; droppedObject.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget); From d8838ddbfbda709e1b92e10e6914d8f492a23623 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 8 Dec 2020 18:48:50 +0100 Subject: [PATCH 5230/6909] Remove duplicated overload. --- osu.Game/Database/ArchiveModelManager.cs | 43 +----------------------- 1 file changed, 1 insertion(+), 42 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 3ec3b96579..6766ca2b77 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -218,48 +218,7 @@ namespace osu.Game.Database } return import; - } - - /// - /// Import one from a . - /// - /// The stream to import files from. - /// The filename of the archive being imported. - public async Task Import(Stream stream, string filename) - { - var notification = new ProgressNotification - { - Progress = 0, - State = ProgressNotificationState.Active, - Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising...", - }; - - PostNotification.Invoke(notification); - - try - { - // we need to keep around the filename as some model managers (namely SkinManager) use the archive name to populate skin info - var imported = await Import(new ZipArchiveReader(stream, filename), notification.CancellationToken); - - notification.CompletionText = $"Imported {imported}! Click to view."; - notification.CompletionClickAction += () => - { - PresentImport?.Invoke(new[] { imported }); - return true; - }; - notification.State = ProgressNotificationState.Completed; - } - catch (TaskCanceledException) - { - throw; - } - catch (Exception e) - { - notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import failed!"; - notification.State = ProgressNotificationState.Cancelled; - Logger.Error(e, $@"Could not import ({filename})", LoggingTarget.Database); - } - } + } /// /// Fired when the user requests to view the resulting import. From aa7d22460d2b7e77bae25f7edd030fbef3cb71ba Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 8 Dec 2020 19:46:55 +0100 Subject: [PATCH 5231/6909] Override Import() instead. --- osu.Android/OsuGameActivity.cs | 3 ++- osu.Android/OsuGameAndroid.cs | 11 +++++------ osu.Game/Database/ArchiveModelManager.cs | 2 +- osu.Game/OsuGameBase.cs | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 541455277d..eb9df24bf7 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.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 Android.App; using Android.Content; using Android.Content.PM; @@ -57,7 +58,7 @@ namespace osu.Android var stream = ContentResolver.OpenInputStream(uri); if (stream != null) - game.ScheduleImport(stream, cursor.GetString(filename_column)); + Task.Factory.StartNew(() => game.Import(stream, cursor.GetString(filename_column)), TaskCreationOptions.LongRunning); } } } diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 81945ee083..c9b27a16d6 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -67,12 +67,11 @@ namespace osu.Android } } - /// - /// Schedules a file to be imported once the game is loaded. - /// - /// A stream to the file to import. - /// The filename of the file to import. - public void ScheduleImport(Stream stream, string filename) => WaitForReady(() => this, _ => Task.Run(() => Import(stream, filename))); + public override Task Import(Stream stream, string filename) + { + WaitForReady(() => this, _ => Task.Run(() => base.Import(stream, filename))); + return Task.CompletedTask; + } protected override void LoadComplete() { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 6766ca2b77..36cc4cce39 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -218,7 +218,7 @@ namespace osu.Game.Database } return import; - } + } /// /// Fired when the user requests to view the resulting import. diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0fc2b8d1d7..150569f1dd 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -395,7 +395,7 @@ namespace osu.Game } } - public async Task Import(Stream stream, string filename) + public virtual async Task Import(Stream stream, string filename) { var extension = Path.GetExtension(filename)?.ToLowerInvariant(); From 748035e80a4550305cc2b294c2ced3d254829c6e Mon Sep 17 00:00:00 2001 From: Xexxar Date: Tue, 8 Dec 2020 16:53:52 -0600 Subject: [PATCH 5232/6909] changes to acc scaling curve for speed and aim pp --- .../Difficulty/OsuPerformanceCalculator.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 0ebe0ddc2d..f239289d7f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -121,10 +121,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty : 0.0); } - // Scale the aim value with accuracy _slightly_ - aimValue *= 0.5 + accuracy / 2.0; - // It is important to also consider accuracy difficulty when doing that - aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; + // Scale the speed value with accuracy _alot_ + if (accuracy > .8) + aimValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; + else + aimValue *= accuracy * (.5 / .8); return aimValue; } @@ -154,8 +155,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(m => m is OsuModHidden)) speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); - // Scale the speed value with accuracy _slightly_ - speedValue *= 0.02 + accuracy; + // Scale the speed value with accuracy _alot_ + if (accuracy > .8) + speedValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; + else + speedValue *= accuracy * (.5 / .8); // It is important to also consider accuracy difficulty when doing that speedValue *= 0.96 + Math.Pow(Attributes.OverallDifficulty, 2) / 1600; From b80204642e52a31163805731db903d1327419a49 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 9 Dec 2020 10:25:35 +0900 Subject: [PATCH 5233/6909] Revert rename error --- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs | 2 +- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs | 2 +- osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs | 2 +- ...HasCatchHitObject.cs => DrawablePalpableCatchHitObject.cs} | 4 ++-- osu.Game.Rulesets.Catch/UI/Catcher.cs | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game.Rulesets.Catch/Objects/Drawables/{DrawablePalpableHasCatchHitObject.cs => DrawablePalpableCatchHitObject.cs} (92%) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 34d89eb188..4bc5da2396 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableBanana : DrawablePalpableHasCatchHitObject + public class DrawableBanana : DrawablePalpableCatchHitObject { public DrawableBanana() : this(null) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index acdb3bb38c..c5f0bb8b18 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableDroplet : DrawablePalpableHasCatchHitObject + public class DrawableDroplet : DrawablePalpableCatchHitObject { public DrawableDroplet() : this(null) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs index 6ec46f6535..57f27d01b8 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -10,7 +10,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Catch.Objects.Drawables { - public class DrawableFruit : DrawablePalpableHasCatchHitObject, IHasFruitState + public class DrawableFruit : DrawablePalpableCatchHitObject, IHasFruitState { public Bindable VisualRepresentation { get; } = new Bindable(); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableHasCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs similarity index 92% rename from osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableHasCatchHitObject.cs rename to osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index ee893f7880..aea9d2c082 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableHasCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -11,7 +11,7 @@ using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables { [Cached(typeof(IHasCatchObjectState))] - public abstract class DrawablePalpableHasCatchHitObject : DrawableCatchHitObject, IHasCatchObjectState + public abstract class DrawablePalpableCatchHitObject : DrawableCatchHitObject, IHasCatchObjectState { public new PalpableCatchHitObject HitObject => (PalpableCatchHitObject)base.HitObject; @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor; - protected DrawablePalpableHasCatchHitObject([CanBeNull] CatchHitObject h) + protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) : base(h) { Origin = Anchor.Centre; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index c9580ca5f8..30a5832c4f 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -223,7 +223,7 @@ namespace osu.Game.Rulesets.Catch.UI catchResult.CatcherAnimationState = CurrentState; catchResult.CatcherHyperDash = HyperDashing; - if (!(drawableObject is DrawablePalpableHasCatchHitObject palpableObject)) return; + if (!(drawableObject is DrawablePalpableCatchHitObject palpableObject)) return; var hitObject = palpableObject.HitObject; @@ -458,7 +458,7 @@ namespace osu.Game.Rulesets.Catch.UI updateCatcher(); } - private void placeCaughtObject(DrawablePalpableHasCatchHitObject drawableObject, Vector2 position) + private void placeCaughtObject(DrawablePalpableCatchHitObject drawableObject, Vector2 position) { var caughtObject = getCaughtObject(drawableObject.HitObject); From df9de7a8ddee1986df712deb138c7d4238f41bed Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 9 Dec 2020 10:28:42 +0900 Subject: [PATCH 5234/6909] Remove null check that is not required anymore --- osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs index b105fb4034..51c06c8e37 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/CatchHitObjectPiece.cs @@ -47,7 +47,7 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default protected override void Update() { - if (BorderPiece != null && ObjectState?.HitObject != null) + if (BorderPiece != null) BorderPiece.Alpha = (float)Math.Clamp((ObjectState.HitObject.StartTime - Time.Current) / 500, 0, 1); } } From ccca7e0b25a97f52d9ef6d754d2f39fe6132a65b Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 9 Dec 2020 10:35:01 +0900 Subject: [PATCH 5235/6909] more specific type droppedObjectContainer --- osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs | 6 +++--- .../TestSceneCatcherArea.cs | 4 ++-- .../TestSceneHyperDashColouring.cs | 2 +- osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs | 2 +- osu.Game.Rulesets.Catch/UI/Catcher.cs | 14 ++++++++++---- osu.Game.Rulesets.Catch/UI/CatcherArea.cs | 2 +- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index 2747524503..e8bb57cdf3 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -29,7 +29,7 @@ namespace osu.Game.Rulesets.Catch.Tests [Resolved] private OsuConfigManager config { get; set; } - private Container droppedObjectContainer; + private Container droppedObjectContainer; private TestCatcher catcher; @@ -42,7 +42,7 @@ namespace osu.Game.Rulesets.Catch.Tests }; var trailContainer = new Container(); - droppedObjectContainer = new Container(); + droppedObjectContainer = new Container(); catcher = new TestCatcher(trailContainer, droppedObjectContainer, difficulty); Child = new Container @@ -277,7 +277,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public IEnumerable CaughtObjects => this.ChildrenOfType(); - public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty) + public TestCatcher(Container trailsTarget, Container droppedObjectTarget, BeatmapDifficulty difficulty) : base(trailsTarget, droppedObjectTarget, difficulty) { } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 3d5e44476e..31c285ef22 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Catch.Tests SetContents(() => { - var droppedObjectContainer = new Container + var droppedObjectContainer = new Container { RelativeSizeAxes = Axes.Both }; @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Catch.Tests private class TestCatcherArea : CatcherArea { - public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty) + public TestCatcherArea(Container droppedObjectContainer, BeatmapDifficulty beatmapDifficulty) : base(droppedObjectContainer, beatmapDifficulty) { } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs index d78dc2d2b5..683a776dcc 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDashColouring.cs @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("create hyper-dashing catcher", () => { - Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container()) + Child = setupSkinHierarchy(catcherArea = new CatcherArea(new Container()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs index fdc12bf088..73420a9eda 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchPlayfield.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.UI public CatchPlayfield(BeatmapDifficulty difficulty, Func> createDrawableRepresentation) { - var droppedObjectContainer = new Container + var droppedObjectContainer = new Container { RelativeSizeAxes = Axes.Both, }; diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 30a5832c4f..2893fcee92 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -53,10 +53,16 @@ namespace osu.Game.Rulesets.Catch.UI private CatcherTrailDisplay trails; - private readonly Container droppedObjectTarget; - + /// + /// Contains caught objects on the plate. + /// private readonly Container caughtFruitContainer; + /// + /// Contains objects dropped from the plate. + /// + private readonly Container droppedObjectTarget; + public CatcherAnimationState CurrentState { get; private set; } /// @@ -112,7 +118,7 @@ namespace osu.Game.Rulesets.Catch.UI private readonly DrawablePool caughtBananaPool; private readonly DrawablePool caughtDropletPool; - public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null) + public Catcher([NotNull] Container trailsTarget, [NotNull] Container droppedObjectTarget, BeatmapDifficulty difficulty = null) { this.trailsTarget = trailsTarget; this.droppedObjectTarget = droppedObjectTarget; @@ -274,7 +280,7 @@ namespace osu.Game.Rulesets.Catch.UI } caughtFruitContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); - droppedObjectTarget.RemoveAll(d => (d as CaughtObject)?.HitObject == drawableObject.HitObject); + droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject); hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); } diff --git a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs index 857d9141c9..44adbd5512 100644 --- a/osu.Game.Rulesets.Catch/UI/CatcherArea.cs +++ b/osu.Game.Rulesets.Catch/UI/CatcherArea.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Catch.UI public readonly Catcher MovableCatcher; private readonly CatchComboDisplay comboDisplay; - public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null) + public CatcherArea(Container droppedObjectContainer, BeatmapDifficulty difficulty = null) { Size = new Vector2(CatchPlayfield.WIDTH, CATCHER_SIZE); Children = new Drawable[] From c8b0934573c029e7568d63927533dcbddcde9099 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 9 Dec 2020 10:35:36 +0900 Subject: [PATCH 5236/6909] Rename caughtFruitContainer -> caughtObjectContainer --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 2893fcee92..475fca7f1c 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Contains caught objects on the plate. /// - private readonly Container caughtFruitContainer; + private readonly Container caughtObjectContainer; /// /// Contains objects dropped from the plate. @@ -138,7 +138,7 @@ namespace osu.Game.Rulesets.Catch.UI caughtBananaPool = new DrawablePool(100), // less capacity is needed compared to fruit because droplet is not stacked caughtDropletPool = new DrawablePool(25), - caughtFruitContainer = new Container + caughtObjectContainer = new Container { Anchor = Anchor.TopCentre, Origin = Anchor.BottomCentre, @@ -186,7 +186,7 @@ namespace osu.Game.Rulesets.Catch.UI /// /// Creates proxied content to be displayed beneath hitobjects. /// - public Drawable CreateProxiedContent() => caughtFruitContainer.CreateProxy(); + public Drawable CreateProxiedContent() => caughtObjectContainer.CreateProxy(); /// /// Calculates the scale of the catcher based off the provided beatmap difficulty. @@ -279,7 +279,7 @@ namespace osu.Game.Rulesets.Catch.UI SetHyperDashState(); } - caughtFruitContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); + caughtObjectContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); droppedObjectTarget.RemoveAll(d => d.HitObject == drawableObject.HitObject); hitExplosionContainer.RemoveAll(d => d.HitObject == drawableObject.HitObject); } @@ -475,7 +475,7 @@ namespace osu.Game.Rulesets.Catch.UI caughtObject.Position = position; caughtObject.Scale /= 2; - caughtFruitContainer.Add(caughtObject); + caughtObjectContainer.Add(caughtObject); if (!caughtObject.StaysOnPlate) removeFromPlate(caughtObject, DroppedObjectAnimation.Explode); @@ -486,7 +486,7 @@ namespace osu.Game.Rulesets.Catch.UI const float radius_div_2 = CatchHitObject.OBJECT_RADIUS / 2; const float allowance = 10; - while (caughtFruitContainer.Any(f => Vector2Extensions.Distance(f.Position, position) < (displayRadius + radius_div_2) / (allowance / 2))) + while (caughtObjectContainer.Any(f => Vector2Extensions.Distance(f.Position, position) < (displayRadius + radius_div_2) / (allowance / 2))) { float diff = (displayRadius + radius_div_2) / allowance; @@ -533,17 +533,17 @@ namespace osu.Game.Rulesets.Catch.UI droppedObject.CopyStateFrom(caughtObject); droppedObject.Anchor = Anchor.TopLeft; - droppedObject.Position = caughtFruitContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget); + droppedObject.Position = caughtObjectContainer.ToSpaceOfOtherDrawable(caughtObject.DrawPosition, droppedObjectTarget); return droppedObject; } private void clearPlate(DroppedObjectAnimation animation) { - var caughtObjects = caughtFruitContainer.Children.ToArray(); + var caughtObjects = caughtObjectContainer.Children.ToArray(); var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray(); - caughtFruitContainer.Clear(false); + caughtObjectContainer.Clear(false); droppedObjectTarget.AddRange(droppedObjects); @@ -555,7 +555,7 @@ namespace osu.Game.Rulesets.Catch.UI { var droppedObject = getDroppedObject(caughtObject); - if (!caughtFruitContainer.Remove(caughtObject)) + if (!caughtObjectContainer.Remove(caughtObject)) throw new InvalidOperationException("Can only drop a caught object on the plate"); droppedObjectTarget.Add(droppedObject); @@ -573,7 +573,7 @@ namespace osu.Game.Rulesets.Catch.UI break; case DroppedObjectAnimation.Explode: - var originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtFruitContainer).X * Scale.X; + var originalX = droppedObjectTarget.ToSpaceOfOtherDrawable(d.DrawPosition, caughtObjectContainer).X * Scale.X; d.MoveToY(d.Y - 50, 250, Easing.OutSine).Then().MoveToY(d.Y + 50, 500, Easing.InSine); d.MoveToX(d.X + originalX * 6, 1000); d.FadeOut(750); From 86445e7c2304eb54193c2278ac306e938b0a525e Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 9 Dec 2020 10:36:54 +0900 Subject: [PATCH 5237/6909] Remove unnecessary copy --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 475fca7f1c..e885881c42 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -540,8 +540,7 @@ namespace osu.Game.Rulesets.Catch.UI private void clearPlate(DroppedObjectAnimation animation) { - var caughtObjects = caughtObjectContainer.Children.ToArray(); - var droppedObjects = caughtObjects.Select(getDroppedObject).ToArray(); + var droppedObjects = caughtObjectContainer.Children.Select(getDroppedObject).ToArray(); caughtObjectContainer.Clear(false); From b52e279702a879eebd333efa734aae9b3b1a2820 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 9 Dec 2020 10:38:11 +0900 Subject: [PATCH 5238/6909] Reword exception message --- 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 e885881c42..125f735a60 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -555,7 +555,7 @@ namespace osu.Game.Rulesets.Catch.UI var droppedObject = getDroppedObject(caughtObject); if (!caughtObjectContainer.Remove(caughtObject)) - throw new InvalidOperationException("Can only drop a caught object on the plate"); + throw new InvalidOperationException("Can only drop objects that were previously caught on the plate"); droppedObjectTarget.Add(droppedObject); From da2f3d4473a0583b53cfc76671b48e209f21f321 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 9 Dec 2020 10:40:42 +0900 Subject: [PATCH 5239/6909] Move classes to separate files --- .../Objects/Drawables/CaughtBanana.cs | 18 +++++++++ .../Objects/Drawables/CaughtDroplet.cs | 20 ++++++++++ .../Objects/Drawables/CaughtFruit.cs | 29 +++++++++++++++ .../Objects/Drawables/CaughtObject.cs | 37 ------------------- .../Objects/Drawables/IHasCatchObjectState.cs | 8 ---- .../Objects/Drawables/IHasFruitState.cs | 15 ++++++++ 6 files changed, 82 insertions(+), 45 deletions(-) create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.cs create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.cs create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs create mode 100644 osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.cs new file mode 100644 index 0000000000..8a91f82437 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtBanana.cs @@ -0,0 +1,18 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Catch.Skinning.Default; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + /// + /// Represents a caught by the catcher. + /// + public class CaughtBanana : CaughtObject + { + public CaughtBanana() + : base(CatchSkinComponents.Banana, _ => new BananaPiece()) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.cs new file mode 100644 index 0000000000..4a3397feff --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtDroplet.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.Catch.Skinning.Default; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + /// + /// Represents a caught by the catcher. + /// + public class CaughtDroplet : CaughtObject + { + public override bool StaysOnPlate => false; + + public CaughtDroplet() + : base(CatchSkinComponents.Droplet, _ => new DropletPiece()) + { + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.cs new file mode 100644 index 0000000000..140b411c88 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtFruit.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.Bindables; +using osu.Game.Rulesets.Catch.Skinning.Default; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + /// + /// Represents a caught by the catcher. + /// + public class CaughtFruit : CaughtObject, IHasFruitState + { + public Bindable VisualRepresentation { get; } = new Bindable(); + + public CaughtFruit() + : base(CatchSkinComponents.Fruit, _ => new FruitPiece()) + { + } + + public override void CopyStateFrom(IHasCatchObjectState objectState) + { + base.CopyStateFrom(objectState); + + var fruitState = (IHasFruitState)objectState; + VisualRepresentation.Value = fruitState.VisualRepresentation.Value; + } + } +} diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs index 87319a4498..e597d1a4cd 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -5,7 +5,6 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Rulesets.Catch.Skinning.Default; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -61,40 +60,4 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables base.FreeAfterUse(); } } - - public class CaughtFruit : CaughtObject, IHasFruitState - { - public Bindable VisualRepresentation { get; } = new Bindable(); - - public CaughtFruit() - : base(CatchSkinComponents.Fruit, _ => new FruitPiece()) - { - } - - public override void CopyStateFrom(IHasCatchObjectState objectState) - { - base.CopyStateFrom(objectState); - - var fruitState = (IHasFruitState)objectState; - VisualRepresentation.Value = fruitState.VisualRepresentation.Value; - } - } - - public class CaughtBanana : CaughtObject - { - public CaughtBanana() - : base(CatchSkinComponents.Banana, _ => new BananaPiece()) - { - } - } - - public class CaughtDroplet : CaughtObject - { - public override bool StaysOnPlate => false; - - public CaughtDroplet() - : base(CatchSkinComponents.Droplet, _ => new DropletPiece()) - { - } - } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs index 0a75fb2224..55ca502877 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs @@ -19,12 +19,4 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables float Rotation { get; } Vector2 Scale { get; } } - - /// - /// Provides a visual state of a . - /// - public interface IHasFruitState : IHasCatchObjectState - { - Bindable VisualRepresentation { get; } - } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.cs new file mode 100644 index 0000000000..2d4de543c3 --- /dev/null +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasFruitState.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.Bindables; + +namespace osu.Game.Rulesets.Catch.Objects.Drawables +{ + /// + /// Provides a visual state of a . + /// + public interface IHasFruitState : IHasCatchObjectState + { + Bindable VisualRepresentation { get; } + } +} From 775c4bad973c3f9afba809851b0b63add97929a1 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 9 Dec 2020 10:47:04 +0900 Subject: [PATCH 5240/6909] Remove unneeded lifetime assignment --- osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs index e597d1a4cd..fb83b73212 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -52,10 +52,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables protected override void FreeAfterUse() { ClearTransforms(); - Alpha = 1; - LifetimeStart = double.MinValue; - LifetimeEnd = double.MaxValue; base.FreeAfterUse(); } From a8e2f35b622a5797837115eeef7f2b96fd4a9169 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Wed, 9 Dec 2020 10:50:35 +0900 Subject: [PATCH 5241/6909] Remove unneeded check of caught object removal The logic was public but now it is private so the condition is ensured by the caller --- osu.Game.Rulesets.Catch/UI/Catcher.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 125f735a60..64dacd09e6 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -554,8 +554,7 @@ namespace osu.Game.Rulesets.Catch.UI { var droppedObject = getDroppedObject(caughtObject); - if (!caughtObjectContainer.Remove(caughtObject)) - throw new InvalidOperationException("Can only drop objects that were previously caught on the plate"); + caughtObjectContainer.Remove(caughtObject); droppedObjectTarget.Add(droppedObject); From ff5150a14d152242026cdd6be462088f2a15ad03 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 12:03:52 +0900 Subject: [PATCH 5242/6909] Fix typo in IMultiplayerClient xmldoc --- osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs index e75b0da207..c162f066d4 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs @@ -53,7 +53,7 @@ namespace osu.Game.Online.RealtimeMultiplayer Task LoadRequested(); /// - /// Signals that a match has started. All user in the state should begin gameplay as soon as possible. + /// Signals that a match has started. All users in the state should begin gameplay as soon as possible. /// Task MatchStarted(); From 2046cbe2d987fa12b46f6acfcd40a91a5cb283f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 12:05:50 +0900 Subject: [PATCH 5243/6909] Add missing exceptions to server xmldoc --- osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index 565c07d0ec..3c92f70a01 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs @@ -17,6 +17,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// Request to leave the currently joined room. /// + /// If the user is not in a room. Task LeaveRoom(); /// @@ -24,12 +25,15 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The new user which is to become host. /// A user other than the current host is attempting to transfer host. + /// If the user is not in a room. Task TransferHost(long userId); /// /// As the host, update the settings of the currently joined room. /// /// The new settings to apply. + /// A user other than the current host is attempting to transfer host. + /// If the user is not in a room. Task ChangeSettings(MultiplayerRoomSettings settings); /// @@ -37,12 +41,14 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The proposed new state. /// If the state change requested is not valid, given the previous state or room state. + /// If the user is not in a room. Task ChangeState(MultiplayerUserState newState); /// /// As the host of a room, start the match. /// /// A user other than the current host is attempting to start the game. + /// If the user is not in a room. Task StartMatch(); } } From fd4fa963ac6e86265ad9db5e5335040ae1de5291 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 12:07:19 +0900 Subject: [PATCH 5244/6909] Standardise exception naming --- ...lreadyInMultiplayerRoom.cs => AlreadyInRoomException.cs} | 6 +++--- osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs | 4 ++-- ...InvalidStateChange.cs => InvalidStateChangeException.cs} | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) rename osu.Game/Online/RealtimeMultiplayer/{UserAlreadyInMultiplayerRoom.cs => AlreadyInRoomException.cs} (60%) rename osu.Game/Online/RealtimeMultiplayer/{InvalidStateChange.cs => InvalidStateChangeException.cs} (65%) diff --git a/osu.Game/Online/RealtimeMultiplayer/UserAlreadyInMultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.cs similarity index 60% rename from osu.Game/Online/RealtimeMultiplayer/UserAlreadyInMultiplayerRoom.cs rename to osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.cs index 9a2090c710..f99bea651c 100644 --- a/osu.Game/Online/RealtimeMultiplayer/UserAlreadyInMultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.cs @@ -5,10 +5,10 @@ using System; namespace osu.Game.Online.RealtimeMultiplayer { - public class UserAlreadyInMultiplayerRoom : Exception + public class AlreadyInRoomException : Exception { - public UserAlreadyInMultiplayerRoom() - : base("This user is already in a room.") + public AlreadyInRoomException() + : base("This user is already in a multiplayer room.") { } } diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index 3c92f70a01..1bb6331c0a 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs @@ -11,7 +11,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Request to join a multiplayer room. /// /// The databased room ID. - /// If the user is already in the requested (or another) room. + /// If the user is already in the requested (or another) room. Task JoinRoom(long roomId); /// @@ -40,7 +40,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Change the local user state in the currently joined room. /// /// The proposed new state. - /// If the state change requested is not valid, given the previous state or room state. + /// If the state change requested is not valid, given the previous state or room state. /// If the user is not in a room. Task ChangeState(MultiplayerUserState newState); diff --git a/osu.Game/Online/RealtimeMultiplayer/InvalidStateChange.cs b/osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs similarity index 65% rename from osu.Game/Online/RealtimeMultiplayer/InvalidStateChange.cs rename to osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs index d1016e95cb..1e33f55491 100644 --- a/osu.Game/Online/RealtimeMultiplayer/InvalidStateChange.cs +++ b/osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs @@ -5,9 +5,9 @@ using System; namespace osu.Game.Online.RealtimeMultiplayer { - public class InvalidStateChange : Exception + public class InvalidStateChangeException : Exception { - public InvalidStateChange(MultiplayerUserState oldState, MultiplayerUserState newState) + public InvalidStateChangeException(MultiplayerUserState oldState, MultiplayerUserState newState) : base($"Cannot change from {oldState} to {newState}") { } From 1013749a83365c54d7585e1a4289c54113754e52 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 12:10:47 +0900 Subject: [PATCH 5245/6909] Change user id type to int --- osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs index 17aca4458a..60f1c9e42e 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs @@ -11,7 +11,7 @@ namespace osu.Game.Online.RealtimeMultiplayer [Serializable] public class MultiplayerRoomUser : IEquatable { - public readonly long UserID; + public readonly int UserID; public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle; From 0eb5b16454647d0f1fa7de484b2f5eb15d6d37fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 12:12:03 +0900 Subject: [PATCH 5246/6909] Remove Empty() implementation for RoomSettings until otherwise necessary --- osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs | 2 +- osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index 0f18ab6c89..24c20bded4 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -27,7 +27,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// All currently enforced game settings for this room. /// - public MultiplayerRoomSettings Settings { get; set; } = MultiplayerRoomSettings.Empty(); + public MultiplayerRoomSettings Settings { get; set; } = new MultiplayerRoomSettings(); /// /// All users currently in this room. diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs index 3137afa8a4..8f0f4febd1 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs @@ -24,7 +24,5 @@ namespace osu.Game.Online.RealtimeMultiplayer public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID; public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; - - public static MultiplayerRoomSettings Empty() => new MultiplayerRoomSettings(); } } From 427d41bab5a08b998f6b8c3349e402f0c4315890 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 12:17:37 +0900 Subject: [PATCH 5247/6909] Add missing licence headers --- osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs | 3 +++ osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs | 3 +++ 2 files changed, 6 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index 1bb6331c0a..836260a26d 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.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.Threading.Tasks; namespace osu.Game.Online.RealtimeMultiplayer diff --git a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs b/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs index ca4a54f6d7..d71200a086 100644 --- a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs +++ b/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.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; namespace osu.Game.Online.RealtimeMultiplayer From 48129c52d676d8e33d63852031e530fe4cef6b32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 12:38:24 +0900 Subject: [PATCH 5248/6909] Change get-only property for now --- .../Online/RealtimeMultiplayer/MultiplayerClientState.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs index 8c440abba3..27fa0e9ac9 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs @@ -1,13 +1,16 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; + #nullable enable namespace osu.Game.Online.RealtimeMultiplayer { + [Serializable] public class MultiplayerClientState { - public long CurrentRoomID { get; set; } + public readonly long CurrentRoomID; public MultiplayerClientState(in long roomId) { From c92c2cbfc01716b553369e376b9754d9f70c5262 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 14:46:43 +0900 Subject: [PATCH 5249/6909] Change exceptions which should be returned to the user to HubException type See https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.signalr.hubexception?view=aspnetcore-5.0. --- .../RealtimeMultiplayer/AlreadyInRoomException.cs | 10 +++++++++- .../RealtimeMultiplayer/InvalidStateChangeException.cs | 10 +++++++++- .../Online/RealtimeMultiplayer/NotHostException.cs | 10 +++++++++- .../RealtimeMultiplayer/NotJoinedRoomException.cs | 10 +++++++++- 4 files changed, 36 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.cs b/osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.cs index f99bea651c..7f3c2b339a 100644 --- a/osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.cs +++ b/osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.cs @@ -2,14 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.RealtimeMultiplayer { - public class AlreadyInRoomException : Exception + [Serializable] + public class AlreadyInRoomException : HubException { public AlreadyInRoomException() : base("This user is already in a multiplayer room.") { } + + protected AlreadyInRoomException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs b/osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs index 1e33f55491..d9a276fc19 100644 --- a/osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs +++ b/osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs @@ -2,14 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.RealtimeMultiplayer { - public class InvalidStateChangeException : Exception + [Serializable] + public class InvalidStateChangeException : HubException { public InvalidStateChangeException(MultiplayerUserState oldState, MultiplayerUserState newState) : base($"Cannot change from {oldState} to {newState}") { } + + protected InvalidStateChangeException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/osu.Game/Online/RealtimeMultiplayer/NotHostException.cs b/osu.Game/Online/RealtimeMultiplayer/NotHostException.cs index e421c6ae28..56095043f0 100644 --- a/osu.Game/Online/RealtimeMultiplayer/NotHostException.cs +++ b/osu.Game/Online/RealtimeMultiplayer/NotHostException.cs @@ -2,14 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.RealtimeMultiplayer { - public class NotHostException : Exception + [Serializable] + public class NotHostException : HubException { public NotHostException() : base("User is attempting to perform a host level operation while not the host") { } + + protected NotHostException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } diff --git a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs b/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs index d71200a086..7a6e089d0b 100644 --- a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs +++ b/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs @@ -2,14 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.RealtimeMultiplayer { - public class NotJoinedRoomException : Exception + [Serializable] + public class NotJoinedRoomException : HubException { public NotJoinedRoomException() : base("This user has not yet joined a multiplayer room.") { } + + protected NotJoinedRoomException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } From e09715d71efe23bdedf5a6b7718e6bdbe4ef2258 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 14:47:26 +0900 Subject: [PATCH 5250/6909] Add ToString implementation to MultiplayerRoom for easier debug --- osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index 24c20bded4..1ba8d0c487 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -50,5 +50,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Request a lock on this room to perform a thread-safe update. /// public LockUntilDisposal LockForUpdate() => new LockUntilDisposal(writeLock); + + public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]"; } } From bef52af1da4da012d37eb78dc2ac62fa138a371e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 15:05:32 +0900 Subject: [PATCH 5251/6909] Move client state class to server implementation Never used by client. --- .../MultiplayerClientState.cs | 20 ------------------- 1 file changed, 20 deletions(-) delete mode 100644 osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.cs deleted file mode 100644 index 27fa0e9ac9..0000000000 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerClientState.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 System; - -#nullable enable - -namespace osu.Game.Online.RealtimeMultiplayer -{ - [Serializable] - public class MultiplayerClientState - { - public readonly long CurrentRoomID; - - public MultiplayerClientState(in long roomId) - { - CurrentRoomID = roomId; - } - } -} From ab00a15555a31394e23b9dd6277fc02aa25f9239 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 15:05:57 +0900 Subject: [PATCH 5252/6909] Add JsonConstructor specs to allow for correct deserialization of readonly fields --- osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs | 2 ++ osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index 1ba8d0c487..5704ddd675 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json; namespace osu.Game.Online.RealtimeMultiplayer { @@ -41,6 +42,7 @@ namespace osu.Game.Online.RealtimeMultiplayer private object writeLock = new object(); + [JsonConstructor] public MultiplayerRoom(in long roomId) { RoomID = roomId; diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs index 60f1c9e42e..caf1a70197 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using Newtonsoft.Json; using osu.Game.Users; namespace osu.Game.Online.RealtimeMultiplayer @@ -17,6 +18,7 @@ namespace osu.Game.Online.RealtimeMultiplayer public User? User { get; set; } + [JsonConstructor] public MultiplayerRoomUser(in int userId) { UserID = userId; From 578e5cb92e66a62bd81b08d2cb0c944cedca5030 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 15:59:13 +0900 Subject: [PATCH 5253/6909] Also make InvalidStateException serializable --- .../RealtimeMultiplayer/InvalidStateException.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs b/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs index 8393e7e925..7791bfc69f 100644 --- a/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs +++ b/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs @@ -2,14 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Runtime.Serialization; +using Microsoft.AspNetCore.SignalR; namespace osu.Game.Online.RealtimeMultiplayer { - public class InvalidStateException : Exception + [Serializable] + public class InvalidStateException : HubException { public InvalidStateException(string message) : base(message) { } + + protected InvalidStateException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } } } From 1e08b298b865cac8f05e13c4941a23a7b0131fa6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 16:01:09 +0900 Subject: [PATCH 5254/6909] Remove unnecessary exception type --- .../AlreadyInRoomException.cs | 23 ------------------- 1 file changed, 23 deletions(-) delete mode 100644 osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.cs b/osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.cs deleted file mode 100644 index 7f3c2b339a..0000000000 --- a/osu.Game/Online/RealtimeMultiplayer/AlreadyInRoomException.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 System; -using System.Runtime.Serialization; -using Microsoft.AspNetCore.SignalR; - -namespace osu.Game.Online.RealtimeMultiplayer -{ - [Serializable] - public class AlreadyInRoomException : HubException - { - public AlreadyInRoomException() - : base("This user is already in a multiplayer room.") - { - } - - protected AlreadyInRoomException(SerializationInfo info, StreamingContext context) - : base(info, context) - { - } - } -} From bb97eae8b1b46df5162a6502a282b0915527dd6b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 16:02:37 +0900 Subject: [PATCH 5255/6909] Update outdated exception references in xmldoc --- osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index 836260a26d..e2e7b6b991 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs @@ -14,7 +14,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Request to join a multiplayer room. /// /// The databased room ID. - /// If the user is already in the requested (or another) room. + /// If the user is already in the requested (or another) room. Task JoinRoom(long roomId); /// @@ -52,6 +52,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// A user other than the current host is attempting to start the game. /// If the user is not in a room. + /// If an attempt to start the game occurs when the game's (or users') state disallows it. Task StartMatch(); } } From c8e3c7e77bb9b00356cd6b020ac212941d1b5fa5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 9 Dec 2020 17:45:07 +0900 Subject: [PATCH 5256/6909] Add stateful client interface --- .../IStatefulMultiplayerClient.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/IStatefulMultiplayerClient.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/IStatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/IStatefulMultiplayerClient.cs new file mode 100644 index 0000000000..578092662a --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/IStatefulMultiplayerClient.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. + +#nullable enable + +namespace osu.Game.Online.RealtimeMultiplayer +{ + /// + /// A multiplayer client which maintains local room and user state. Also provides a proxy to access the . + /// + public interface IStatefulMultiplayerClient : IMultiplayerClient, IMultiplayerServer + { + MultiplayerUserState State { get; } + + MultiplayerRoom? Room { get; } + } +} From 6da854e37c3eb3b8f426904895f6ece374bea498 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 9 Dec 2020 13:32:59 +0100 Subject: [PATCH 5257/6909] Move scheduled import logic to OsuGame. --- osu.Android/OsuGameAndroid.cs | 6 ------ osu.Game/OsuGame.cs | 7 +++++++ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index c9b27a16d6..57512012f9 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -67,12 +67,6 @@ namespace osu.Android } } - public override Task Import(Stream stream, string filename) - { - WaitForReady(() => this, _ => Task.Run(() => base.Import(stream, filename))); - return Task.CompletedTask; - } - protected override void LoadComplete() { base.LoadComplete(); diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 2c1db67d24..0ad58b2438 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -52,6 +52,7 @@ using osu.Game.Updater; using osu.Game.Utils; using LogLevel = osu.Framework.Logging.LogLevel; using osu.Game.Users; +using System.IO; namespace osu.Game { @@ -426,6 +427,12 @@ namespace osu.Game }, validScreens: new[] { typeof(PlaySongSelect) }); } + public override Task Import(Stream stream, string filename) + { + WaitForReady(() => this, _ => Task.Run(() => base.Import(stream, filename))); + return Task.CompletedTask; + } + protected virtual Loader CreateLoader() => new Loader(); protected virtual UpdateManager CreateUpdateManager() => new UpdateManager(); From ac91f0e2707bf57eacacf079c4de09b1c108926d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 10 Dec 2020 00:25:46 +0900 Subject: [PATCH 5258/6909] Add extended limits to difficulty adjustment mod --- .../Mods/CatchModDifficultyAdjust.cs | 12 ++- .../Mods/OsuModDifficultyAdjust.cs | 12 ++- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 79 ++++++++++++++++++- 3 files changed, 97 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index acdd0a420c..859dfb7647 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Catch.Mods public class CatchModDifficultyAdjust : ModDifficultyAdjust { [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] - public BindableNumber CircleSize { get; } = new BindableFloat + public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 1, @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Catch.Mods }; [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] - public BindableNumber ApproachRate { get; } = new BindableFloat + public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 1, @@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Catch.Mods Value = 5, }; + protected override void ApplyLimits(bool extended) + { + base.ApplyLimits(extended); + + CircleSize.MaxValue = extended ? 11 : 10; + ApproachRate.MaxValue = extended ? 11 : 10; + } + public override string SettingDescription { get diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index ff995e38ce..a6ad2e75f1 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -12,7 +12,7 @@ namespace osu.Game.Rulesets.Osu.Mods public class OsuModDifficultyAdjust : ModDifficultyAdjust { [SettingSource("Circle Size", "Override a beatmap's set CS.", FIRST_SETTING_ORDER - 1)] - public BindableNumber CircleSize { get; } = new BindableFloat + public BindableNumber CircleSize { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 0, @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods }; [SettingSource("Approach Rate", "Override a beatmap's set AR.", LAST_SETTING_ORDER + 1)] - public BindableNumber ApproachRate { get; } = new BindableFloat + public BindableNumber ApproachRate { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 0, @@ -31,6 +31,14 @@ namespace osu.Game.Rulesets.Osu.Mods Value = 5, }; + protected override void ApplyLimits(bool extended) + { + base.ApplyLimits(extended); + + CircleSize.MaxValue = extended ? 11 : 10; + ApproachRate.MaxValue = extended ? 11 : 10; + } + public override string SettingDescription { get diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 165644edbe..7df663ad3a 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -34,7 +34,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)] - public BindableNumber DrainRate { get; } = new BindableFloat + public BindableNumber DrainRate { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 0, @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Mods }; [SettingSource("Accuracy", "Override a beatmap's set OD.", LAST_SETTING_ORDER)] - public BindableNumber OverallDifficulty { get; } = new BindableFloat + public BindableNumber OverallDifficulty { get; } = new BindableFloatWithLimitExtension { Precision = 0.1f, MinValue = 0, @@ -53,6 +53,24 @@ namespace osu.Game.Rulesets.Mods Value = 5, }; + [SettingSource("Extended Limits", "Adjust difficulty beyond sane limits.")] + public BindableBool ExtendedLimits { get; } = new BindableBool(); + + protected ModDifficultyAdjust() + { + ExtendedLimits.BindValueChanged(extend => ApplyLimits(extend.NewValue)); + } + + /// + /// Changes the difficulty adjustment limits. Occurs when the value of is changed. + /// + /// Whether limits should extend beyond sane ranges. + protected virtual void ApplyLimits(bool extended) + { + DrainRate.MaxValue = extended ? 11 : 10; + OverallDifficulty.MaxValue = extended ? 11 : 10; + } + public override string SettingDescription { get @@ -123,5 +141,62 @@ namespace osu.Game.Rulesets.Mods difficulty.DrainRate = DrainRate.Value; difficulty.OverallDifficulty = OverallDifficulty.Value; } + + /// + /// A that extends its min/max values to support any assigned value. + /// + protected class BindableDoubleWithLimitExtension : BindableDouble + { + public override double Value + { + get => base.Value; + set + { + if (value < MinValue) + MinValue = value; + if (value > MaxValue) + MaxValue = value; + base.Value = value; + } + } + } + + /// + /// A that extends its min/max values to support any assigned value. + /// + protected class BindableFloatWithLimitExtension : BindableFloat + { + public override float Value + { + get => base.Value; + set + { + if (value < MinValue) + MinValue = value; + if (value > MaxValue) + MaxValue = value; + base.Value = value; + } + } + } + + /// + /// A that extends its min/max values to support any assigned value. + /// + protected class BindableIntWithLimitExtension : BindableInt + { + public override int Value + { + get => base.Value; + set + { + if (value < MinValue) + MinValue = value; + if (value > MaxValue) + MaxValue = value; + base.Value = value; + } + } + } } } From 47a93d8614eff32b5fd7bb9db8b81e8d97a47f34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 10 Dec 2020 00:26:35 +0900 Subject: [PATCH 5259/6909] Adjust osu! hitobject fade-ins to support AR>10 --- .../Objects/Drawables/Connections/FollowPointConnection.cs | 5 ++++- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 6e7b1050cb..40154ca84c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -110,8 +110,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections double startTime = start.GetEndTime(); double duration = end.StartTime - startTime; + // For now, adjust the pre-empt for approach rates > 10. + double preempt = PREEMPT * Math.Min(1, start.TimePreempt / 450); + fadeOutTime = startTime + fraction * duration; - fadeInTime = fadeOutTime - PREEMPT; + fadeInTime = fadeOutTime - preempt; } } } diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 15af141c99..6d28a576a4 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.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.Bindables; using osu.Game.Beatmaps; @@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Osu.Objects base.ApplyDefaultsToSelf(controlPointInfo, difficulty); TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); - TimeFadeIn = 400; // as per osu-stable + TimeFadeIn = 400 * Math.Min(1, TimePreempt / 450); Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } From 9835245ea29e3dcc053feb818b65f2e959cb5d06 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 10 Dec 2020 00:32:31 +0900 Subject: [PATCH 5260/6909] Add test --- .../Online/TestAPIModSerialization.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/osu.Game.Tests/Online/TestAPIModSerialization.cs b/osu.Game.Tests/Online/TestAPIModSerialization.cs index 5948582d77..84862ebb07 100644 --- a/osu.Game.Tests/Online/TestAPIModSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModSerialization.cs @@ -68,12 +68,29 @@ namespace osu.Game.Tests.Online Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25)); } + [Test] + public void TestDeserialiseDifficultyAdjustModWithExtendedLimits() + { + var apiMod = new APIMod(new TestModDifficultyAdjust + { + OverallDifficulty = { Value = 11 }, + ExtendedLimits = { Value = true } + }); + + var deserialised = JsonConvert.DeserializeObject(JsonConvert.SerializeObject(apiMod)); + var converted = (TestModDifficultyAdjust)deserialised.ToMod(new TestRuleset()); + + Assert.That(converted.ExtendedLimits.Value, Is.True); + Assert.That(converted.OverallDifficulty.Value, Is.EqualTo(11)); + } + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => new Mod[] { new TestMod(), new TestModTimeRamp(), + new TestModDifficultyAdjust() }; public override DrawableRuleset CreateDrawableRulesetWith(IBeatmap beatmap, IReadOnlyList mods = null) => throw new System.NotImplementedException(); @@ -135,5 +152,9 @@ namespace osu.Game.Tests.Online Value = true }; } + + private class TestModDifficultyAdjust : ModDifficultyAdjust + { + } } } From 7e3fcfe43721ef34a2619084a9007f67de79ad92 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Wed, 9 Dec 2020 10:35:48 -0600 Subject: [PATCH 5261/6909] fixed issue with comment --- 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 f239289d7f..c35b739f81 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty : 0.0); } - // Scale the speed value with accuracy _alot_ + // Scale the aim value with accuracy _alot_ if (accuracy > .8) aimValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; else From cfc34a63bd478b914ebf98f47eef25a08879c3d6 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Wed, 9 Dec 2020 11:21:03 -0600 Subject: [PATCH 5262/6909] realized i accidently deleted the OD scaling --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index c35b739f81..306c491210 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -126,6 +126,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; else aimValue *= accuracy * (.5 / .8); + // It is important to also consider accuracy difficulty when doing that + aimValue *= 0.975 + Math.Pow(Attributes.OverallDifficulty, 2) / 2000; return aimValue; } @@ -161,7 +163,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty else speedValue *= accuracy * (.5 / .8); // It is important to also consider accuracy difficulty when doing that - speedValue *= 0.96 + Math.Pow(Attributes.OverallDifficulty, 2) / 1600; + speedValue *= 0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 1000; return speedValue; } From 146e6a61935bb71a7dec3dac37b1c4791c88a061 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 9 Dec 2020 18:30:52 +0100 Subject: [PATCH 5263/6909] Fix formatting issues --- .../Difficulty/OsuPerformanceCalculator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 306c491210..0306ef3ca0 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -123,9 +123,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the aim value with accuracy _alot_ if (accuracy > .8) - aimValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; + aimValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; else - aimValue *= accuracy * (.5 / .8); + aimValue *= accuracy * (.5 / .8); // It is important to also consider accuracy difficulty when doing that aimValue *= 0.975 + Math.Pow(Attributes.OverallDifficulty, 2) / 2000; @@ -159,9 +159,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the speed value with accuracy _alot_ if (accuracy > .8) - speedValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; + speedValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; else - speedValue *= accuracy * (.5 / .8); + speedValue *= accuracy * (.5 / .8); // It is important to also consider accuracy difficulty when doing that speedValue *= 0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 1000; From 051afe5a7a6838b40be5cea60d2d8d0af777ef43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 9 Dec 2020 18:31:51 +0100 Subject: [PATCH 5264/6909] Fix typos in comments --- 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 0306ef3ca0..477028b02b 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty : 0.0); } - // Scale the aim value with accuracy _alot_ + // Scale the aim value with accuracy _a lot_ if (accuracy > .8) aimValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; else @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(m => m is OsuModHidden)) speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); - // Scale the speed value with accuracy _alot_ + // Scale the speed value with accuracy _a lot_ if (accuracy > .8) speedValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; else From 05ad9aae8d221e0caf59ce282771d6996fb50abb Mon Sep 17 00:00:00 2001 From: Xexxar Date: Wed, 9 Dec 2020 11:57:01 -0600 Subject: [PATCH 5265/6909] changed curve to linear OD + acc based curve --- .../Difficulty/OsuPerformanceCalculator.cs | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 306c491210..87a6fbb23a 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -121,13 +121,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty : 0.0); } - // Scale the aim value with accuracy _alot_ - if (accuracy > .8) - aimValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; - else - aimValue *= accuracy * (.5 / .8); - // It is important to also consider accuracy difficulty when doing that - aimValue *= 0.975 + Math.Pow(Attributes.OverallDifficulty, 2) / 2000; + // Scale the aim value with accuracy and OD + aimValue *= .975 + Math.Pow(Attributes.OverallDifficulty, 2) / 1500 + 200 * (accuracy - 1) * Math.Pow(1 / Attributes.OverallDifficulty, 2); return aimValue; } @@ -157,13 +152,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (mods.Any(m => m is OsuModHidden)) speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); - // Scale the speed value with accuracy _alot_ - if (accuracy > .8) - speedValue *= 0.5 + Math.Pow(Math.Sin(2.5 * (accuracy - .8) * Math.PI), 2) / 2; - else - speedValue *= accuracy * (.5 / .8); - // It is important to also consider accuracy difficulty when doing that - speedValue *= 0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 1000; + // Scale the speed value with accuracy and OD + speedValue *= .95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750 + 200 * (accuracy - 1) * Math.Pow(1 / Attributes.OverallDifficulty, 2); return speedValue; } From d604c51cbd50ef2b72e831d23937373df55b6a0f Mon Sep 17 00:00:00 2001 From: Xexxar Date: Wed, 9 Dec 2020 13:04:14 -0600 Subject: [PATCH 5266/6909] capped scaling at OD 8 to prevent overscaling --- 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 87a6fbb23a..fce17071e4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty } // Scale the aim value with accuracy and OD - aimValue *= .975 + Math.Pow(Attributes.OverallDifficulty, 2) / 1500 + 200 * (accuracy - 1) * Math.Pow(1 / Attributes.OverallDifficulty, 2); + aimValue *= Math.Max(0, .975 + Math.Pow(Attributes.OverallDifficulty, 2) / 1500 + 200 * (accuracy - 1) * Math.Pow(1 / Math.Max(8, Attributes.OverallDifficulty), 2)); return aimValue; } @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); // Scale the speed value with accuracy and OD - speedValue *= .95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750 + 200 * (accuracy - 1) * Math.Pow(1 / Attributes.OverallDifficulty, 2); + speedValue *= Math.Max(0, .95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750 + 200 * (accuracy - 1) * Math.Pow(1 / Math.Max(8, Attributes.OverallDifficulty), 2)); return speedValue; } From 54abc3bd4dd121fab5986b4a6c0f35bec5e0c037 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Wed, 9 Dec 2020 20:07:52 -0600 Subject: [PATCH 5267/6909] revert aim curve and add new 50s nerf --- .../Difficulty/OsuPerformanceCalculator.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index ddb5cb506d..2b9c817094 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -121,9 +121,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty : 0.0); } - // Scale the aim value with accuracy and OD - aimValue *= Math.Max(0, .975 + Math.Pow(Attributes.OverallDifficulty, 2) / 1500 + 200 * (accuracy - 1) * Math.Pow(1 / Math.Max(8, Attributes.OverallDifficulty), 2)); - + // Scale the aim value with accuracy _slightly_ + aimValue *= 0.5 + accuracy / 2.0; + // It is important to also consider accuracy difficulty when doing that + aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; + return aimValue; } @@ -153,7 +155,9 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); // Scale the speed value with accuracy and OD - speedValue *= Math.Max(0, .95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750 + 200 * (accuracy - 1) * Math.Pow(1 / Math.Max(8, Attributes.OverallDifficulty), 2)); + speedValue *= (.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); + // Scale the speed value with # of 50s to punish doubletapping. + speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0.5 * countMeh : countMeh - totalHits / 500.0 * 0.5); return speedValue; } From cc996ec7fcbe360b451ec786c81f6a243703af7d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Dec 2020 16:32:14 +0900 Subject: [PATCH 5268/6909] Ensure player is consumed at the point of scheduled push running the first time --- osu.Game/Screens/Play/PlayerLoader.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 42074ac241..c26195b09c 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -274,6 +274,13 @@ namespace osu.Game.Screens.Play } } + private Player consumePlayer() + { + var consumed = player; + player = null; + return consumed; + } + private void prepareNewPlayer() { if (!this.IsCurrentScreen()) @@ -331,6 +338,8 @@ namespace osu.Game.Screens.Play scheduledPushPlayer = Scheduler.AddDelayed(() => { contentOut(); + // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared). + var consumedPlayer = consumePlayer(); TransformSequence pushSequence = this.Delay(250); @@ -362,8 +371,6 @@ namespace osu.Game.Screens.Play // Note that this may change if the player we load requested a re-run. ValidForResume = false; - if (player.LoadedBeatmapSuccessfully) - this.Push(player); else this.Exit(); }); @@ -373,6 +380,8 @@ namespace osu.Game.Screens.Play { Schedule(pushWhenLoaded); } + if (consumedPlayer.LoadedBeatmapSuccessfully) + this.Push(consumedPlayer); } private void cancelLoad() From 491ab7405942009e17012d326aa90ae88fea46ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Dec 2020 16:33:28 +0900 Subject: [PATCH 5269/6909] Schedule pushWhenLoaded once ever Previously it was being scheduled another time each OnResume, resulting in more and more calls as a user retries the same beatmap multiple times. To simplify things I've decided to just schedule once ever. This means that on resuming there's no 400ms delay any more, but in testing this isn't really an issue (load time is still high enough that it will never really be below that anyway). Even if gameplay was to load faster, the animation should gracefully proceed. --- osu.Game/Screens/Play/PlayerLoader.cs | 95 +++++++++++++-------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index c26195b09c..d9c7afdc3c 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -179,7 +179,10 @@ namespace osu.Game.Screens.Play contentIn(); MetadataInfo.Delay(750).FadeIn(500); - this.Delay(1800).Schedule(pushWhenLoaded); + + // after an initial delay, start the debounced load check. + // this will continue to execute even after resuming back on restart. + Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, 1800, 0)); showMuteWarningIfNeeded(); } @@ -189,8 +192,6 @@ namespace osu.Game.Screens.Play base.OnResuming(last); contentIn(); - - this.Delay(400).Schedule(pushWhenLoaded); } public override void OnSuspending(IScreen next) @@ -322,66 +323,62 @@ namespace osu.Game.Screens.Play { if (!this.IsCurrentScreen()) return; - try + if (!readyForPush) { - if (!readyForPush) - { - // as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce - // if we become unready for push during the delay. - cancelLoad(); - return; - } + // as the pushDebounce below has a delay, we need to keep checking and cancel a future debounce + // if we become unready for push during the delay. + cancelLoad(); + return; + } - if (scheduledPushPlayer != null) - return; + // if a push has already been scheduled, no further action is required. + // this value is reset via cancelLoad() to allow a second usage of the same PlayerLoader screen. + if (scheduledPushPlayer != null) + return; - scheduledPushPlayer = Scheduler.AddDelayed(() => - { - contentOut(); + scheduledPushPlayer = Scheduler.AddDelayed(() => + { // ensure that once we have reached this "point of no return", readyForPush will be false for all future checks (until a new player instance is prepared). var consumedPlayer = consumePlayer(); - TransformSequence pushSequence = this.Delay(250); + contentOut(); - // only show if the warning was created (i.e. the beatmap needs it) - // and this is not a restart of the map (the warning expires after first load). - if (epilepsyWarning?.IsAlive == true) - { - const double epilepsy_display_length = 3000; + TransformSequence pushSequence = this.Delay(250); - pushSequence - .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) - .TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint) - .Delay(epilepsy_display_length) - .Schedule(() => - { - epilepsyWarning.Hide(); - epilepsyWarning.Expire(); - }) - .Delay(EpilepsyWarning.FADE_DURATION); - } + // only show if the warning was created (i.e. the beatmap needs it) + // and this is not a restart of the map (the warning expires after first load). + if (epilepsyWarning?.IsAlive == true) + { + const double epilepsy_display_length = 3000; - pushSequence.Schedule(() => - { - if (!this.IsCurrentScreen()) return; + pushSequence + .Schedule(() => epilepsyWarning.State.Value = Visibility.Visible) + .TransformBindableTo(volumeAdjustment, 0.25, EpilepsyWarning.FADE_DURATION, Easing.OutQuint) + .Delay(epilepsy_display_length) + .Schedule(() => + { + epilepsyWarning.Hide(); + epilepsyWarning.Expire(); + }) + .Delay(EpilepsyWarning.FADE_DURATION); + } - LoadTask = null; + pushSequence.Schedule(() => + { + if (!this.IsCurrentScreen()) return; - // 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; + 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. + ValidForResume = false; - else - this.Exit(); - }); - }, 500); - } - finally - { - Schedule(pushWhenLoaded); - } if (consumedPlayer.LoadedBeatmapSuccessfully) this.Push(consumedPlayer); + else + this.Exit(); + }); + }, 500); } private void cancelLoad() From 67dd7be71a549822b47562be9a4cfc106b338a12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Dec 2020 16:34:58 +0900 Subject: [PATCH 5270/6909] Move cancelLoad call to OnResuming This has no real effect; it just feels more readable to me. --- osu.Game/Screens/Play/PlayerLoader.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index d9c7afdc3c..f3575b01b4 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -191,6 +191,7 @@ namespace osu.Game.Screens.Play { base.OnResuming(last); + cancelLoad(); contentIn(); } @@ -198,8 +199,6 @@ namespace osu.Game.Screens.Play { base.OnSuspending(next); - cancelLoad(); - BackgroundBrightnessReduction = false; // we're moving to player, so a period of silence is upcoming. From 437c0506cec8ee2b2ba1ee3ed71bb57ea50dd04b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Dec 2020 16:56:56 +0900 Subject: [PATCH 5271/6909] Refactor to allow for special disposal handling to still work --- osu.Game/Screens/Play/PlayerLoader.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index f3575b01b4..729119fa36 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.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 System.Threading.Tasks; using JetBrains.Annotations; @@ -71,8 +72,9 @@ namespace osu.Game.Screens.Play } private bool readyForPush => + !playerConsumed // don't push unless the player is completely loaded - player?.LoadState == LoadState.Ready + && player?.LoadState == LoadState.Ready // don't push if the user is hovering one of the panes, unless they are idle. && (IsHovered || idleTracker.IsIdle.Value) // don't push if the user is dragging a slider or otherwise. @@ -84,6 +86,11 @@ namespace osu.Game.Screens.Play private Player player; + /// + /// Whether the curent player instance has been consumed via . + /// + private bool playerConsumed; + private LogoTrackingContainer content; private bool hideOverlays; @@ -191,7 +198,11 @@ namespace osu.Game.Screens.Play { base.OnResuming(last); + // prepare for a retry. + player = null; + playerConsumed = false; cancelLoad(); + contentIn(); } @@ -276,9 +287,10 @@ namespace osu.Game.Screens.Play private Player consumePlayer() { - var consumed = player; - player = null; - return consumed; + Debug.Assert(!playerConsumed); + + playerConsumed = true; + return player; } private void prepareNewPlayer() @@ -395,7 +407,7 @@ namespace osu.Game.Screens.Play if (isDisposing) { // if the player never got pushed, we should explicitly dispose it. - DisposalTask = LoadTask?.ContinueWith(_ => player.Dispose()); + DisposalTask = LoadTask?.ContinueWith(_ => player?.Dispose()); } } From 7c2f506b7902ec2c77048ccd058ec205fedd1570 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 10 Dec 2020 17:10:29 +0900 Subject: [PATCH 5272/6909] Port StanR's NF multiplier changes --- 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 030b0cf6d1..12192e36d4 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty double multiplier = 1.12; // This is being adjusted to keep the final pp value scaled around what it used to be when changing things if (mods.Any(m => m is OsuModNoFail)) - multiplier *= 0.90; + multiplier *= Math.Max(0.90, 1.0 - 0.02 * countMiss); if (mods.Any(m => m is OsuModSpunOut)) multiplier *= 1.0 - Math.Pow((double)Attributes.SpinnerCount / totalHits, 0.85); From 679a550d833c9d6e12160c4a77ac81fa2662cca5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Dec 2020 17:42:28 +0900 Subject: [PATCH 5273/6909] Fix single threaded seeking not working due to unnecessary seek call --- osu.Game/Screens/Play/GameplayClockContainer.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 2c83161614..5d11cdf21d 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -154,13 +154,16 @@ namespace osu.Game.Screens.Play public void Start() { - // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time - // This accounts for the audio clock source potentially taking time to enter a completely stopped state - Seek(GameplayClock.CurrentTime); - adjustableClock.Start(); - IsPaused.Value = false; + if (!adjustableClock.IsRunning) + { + // Seeking the decoupled clock to its current time ensures that its source clock will be seeked to the same time + // This accounts for the audio clock source potentially taking time to enter a completely stopped state + Seek(GameplayClock.CurrentTime); this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); + adjustableClock.Start(); + } + IsPaused.Value = false; } /// From 01bd765384e9f9d2b61118fcab7e75132a7c3cf0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 10 Dec 2020 17:42:47 +0900 Subject: [PATCH 5274/6909] Simplify pause handling by moving transform logic to bindable change event --- osu.Game/Screens/Play/GameplayClockContainer.cs | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 5d11cdf21d..0bbdc980db 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -95,6 +95,16 @@ namespace osu.Game.Screens.Play localGameplayClock = new LocalGameplayClock(userOffsetClock); GameplayClock.IsPaused.BindTo(IsPaused); + + IsPaused.BindValueChanged(onPaused); + } + + private void onPaused(ValueChangedEvent isPaused) + { + if (isPaused.NewValue) + this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop()); + else + this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); } private double totalOffset => userOffsetClock.Offset + platformOffsetClock.Offset; @@ -160,9 +170,9 @@ namespace osu.Game.Screens.Play // This accounts for the audio clock source potentially taking time to enter a completely stopped state Seek(GameplayClock.CurrentTime); - this.TransformBindableTo(pauseFreqAdjust, 1, 200, Easing.In); adjustableClock.Start(); } + IsPaused.Value = false; } @@ -202,8 +212,6 @@ namespace osu.Game.Screens.Play public void Stop() { - this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop()); - IsPaused.Value = true; } From e097b6e61c2796f0c5071704d90b7119134fca0c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 10 Dec 2020 19:42:01 +0900 Subject: [PATCH 5275/6909] Add ScalingContainer back Don't want to set DHO.Scale or DHO.Rotation because because DHO may be transformed by mods. DHO.Size is also assigned for drawable visualizer --- .../Objects/Drawables/CaughtObject.cs | 4 +++- .../Objects/Drawables/DrawableBanana.cs | 14 ++++++------- .../Objects/Drawables/DrawableDroplet.cs | 6 +++--- .../Objects/Drawables/DrawableFruit.cs | 6 +++--- .../DrawablePalpableCatchHitObject.cs | 20 ++++++++++++++++++- .../Objects/Drawables/IHasCatchObjectState.cs | 3 +-- 6 files changed, 36 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs index fb83b73212..22fb338229 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public Bindable AccentColour { get; } = new Bindable(); public Bindable HyperDash { get; } = new Bindable(); + float IHasCatchObjectState.Scale => Scale.X; + /// /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. /// @@ -43,7 +45,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public virtual void CopyStateFrom(IHasCatchObjectState objectState) { HitObject = objectState.HitObject; - Scale = objectState.Scale; + Scale = new Vector2(objectState.Scale); Rotation = objectState.Rotation; AccentColour.Value = objectState.AccentColour.Value; HyperDash.Value = objectState.HyperDash.Value; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs index 4bc5da2396..c1b41a7afc 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableBanana.cs @@ -24,9 +24,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - AddInternal(new SkinnableDrawable( + ScalingContainer.Child = new SkinnableDrawable( new CatchSkinComponent(CatchSkinComponents.Banana), - _ => new BananaPiece())); + _ => new BananaPiece()); } protected override void LoadComplete() @@ -44,12 +44,12 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables const float end_scale = 0.6f; const float random_scale_range = 1.6f; - this.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3))) - .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); + ScalingContainer.ScaleTo(HitObject.Scale * (end_scale + random_scale_range * RandomSingle(3))) + .Then().ScaleTo(HitObject.Scale * end_scale, HitObject.TimePreempt); - this.RotateTo(getRandomAngle(1)) - .Then() - .RotateTo(getRandomAngle(2), HitObject.TimePreempt); + ScalingContainer.RotateTo(getRandomAngle(1)) + .Then() + .RotateTo(getRandomAngle(2), HitObject.TimePreempt); float getRandomAngle(int series) => 180 * (RandomSingle(series) * 2 - 1); } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs index c5f0bb8b18..2dce9507a5 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableDroplet.cs @@ -24,9 +24,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - AddInternal(new SkinnableDrawable( + ScalingContainer.Child = new SkinnableDrawable( new CatchSkinComponent(CatchSkinComponents.Droplet), - _ => new DropletPiece())); + _ => new DropletPiece()); } protected override void UpdateInitialTransforms() @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables float startRotation = RandomSingle(1) * 20; double duration = HitObject.TimePreempt + 2000; - this.RotateTo(startRotation).RotateTo(startRotation + 720, duration); + ScalingContainer.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 57f27d01b8..0b89c46480 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableFruit.cs @@ -32,16 +32,16 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables VisualRepresentation.Value = (FruitVisualRepresentation)(change.NewValue % 4); }, true); - AddInternal(new SkinnableDrawable( + ScalingContainer.Child = new SkinnableDrawable( new CatchSkinComponent(CatchSkinComponents.Fruit), - _ => new FruitPiece())); + _ => new FruitPiece()); } protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); - this.RotateTo((RandomSingle(1) - 0.5f) * 40); + ScalingContainer.RotateTo((RandomSingle(1) - 0.5f) * 40); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index aea9d2c082..9ddf56a9cd 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -5,6 +5,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osuTK; using osuTK.Graphics; @@ -30,11 +31,27 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor; + /// + /// The container internal transforms (such as scaling based on the circle size) are applied to. + /// + protected readonly Container ScalingContainer; + + float IHasCatchObjectState.Scale => HitObject.Scale * ScaleFactor; + + float IHasCatchObjectState.Rotation => ScalingContainer.Rotation; + protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) : base(h) { Origin = Anchor.Centre; Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2); + + AddInternal(ScalingContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(CatchHitObject.OBJECT_RADIUS * 2) + }); } [BackgroundDependencyLoader] @@ -47,7 +64,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables ScaleBindable.BindValueChanged(scale => { - Scale = new Vector2(scale.NewValue * ScaleFactor); + ScalingContainer.Scale = new Vector2(scale.NewValue * ScaleFactor); + Size = ScalingContainer.Size * ScalingContainer.Scale; }, true); IndexInBeatmap.BindValueChanged(_ => UpdateComboColour()); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs index 55ca502877..fac0249985 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables @@ -17,6 +16,6 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Bindable HyperDash { get; } float Rotation { get; } - Vector2 Scale { get; } + float Scale { get; } } } From 2634c6b8d9dc18f34ae7eb81dd114d36a71adb21 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Thu, 10 Dec 2020 20:43:01 +0900 Subject: [PATCH 5276/6909] Combine DisplayRadius and Scale to DisplaySize --- .../TestSceneFruitRandomness.cs | 23 ++++++++++--------- .../Objects/Drawables/CaughtObject.cs | 8 ++++--- .../DrawablePalpableCatchHitObject.cs | 8 +++---- .../Objects/Drawables/IHasCatchObjectState.cs | 5 ++-- osu.Game.Rulesets.Catch/UI/Catcher.cs | 2 +- 5 files changed, 24 insertions(+), 22 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs index e1d817b314..c888dc0a65 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneFruitRandomness.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Objects.Drawables; using osu.Game.Tests.Visual; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Tests @@ -37,16 +38,16 @@ namespace osu.Game.Rulesets.Catch.Tests float fruitRotation = 0; float bananaRotation = 0; - float bananaScale = 0; + Vector2 bananaSize = new Vector2(); Color4 bananaColour = new Color4(); AddStep("Initialize start time", () => { drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; - fruitRotation = drawableFruit.Rotation; - bananaRotation = drawableBanana.Rotation; - bananaScale = drawableBanana.Scale.X; + fruitRotation = drawableFruit.DisplayRotation; + bananaRotation = drawableBanana.DisplayRotation; + bananaSize = drawableBanana.DisplaySize; bananaColour = drawableBanana.AccentColour.Value; }); @@ -55,9 +56,9 @@ namespace osu.Game.Rulesets.Catch.Tests drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = another_start_time; }); - AddAssert("fruit rotation is changed", () => drawableFruit.Rotation != fruitRotation); - AddAssert("banana rotation is changed", () => drawableBanana.Rotation != bananaRotation); - AddAssert("banana scale is changed", () => drawableBanana.Scale.X != bananaScale); + AddAssert("fruit rotation is changed", () => drawableFruit.DisplayRotation != fruitRotation); + AddAssert("banana rotation is changed", () => drawableBanana.DisplayRotation != bananaRotation); + AddAssert("banana size is changed", () => drawableBanana.DisplaySize != bananaSize); AddAssert("banana colour is changed", () => drawableBanana.AccentColour.Value != bananaColour); AddStep("reset start time", () => @@ -65,10 +66,10 @@ namespace osu.Game.Rulesets.Catch.Tests drawableFruit.HitObject.StartTime = drawableBanana.HitObject.StartTime = initial_start_time; }); - AddAssert("rotation and scale restored", () => - drawableFruit.Rotation == fruitRotation && - drawableBanana.Rotation == bananaRotation && - drawableBanana.Scale.X == bananaScale && + AddAssert("rotation and size restored", () => + drawableFruit.DisplayRotation == fruitRotation && + drawableBanana.DisplayRotation == bananaRotation && + drawableBanana.DisplaySize == bananaSize && drawableBanana.AccentColour.Value == bananaColour); } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs index 22fb338229..524505d588 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/CaughtObject.cs @@ -21,7 +21,9 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public Bindable AccentColour { get; } = new Bindable(); public Bindable HyperDash { get; } = new Bindable(); - float IHasCatchObjectState.Scale => Scale.X; + public Vector2 DisplaySize => Size * Scale; + + public float DisplayRotation => Rotation; /// /// Whether this hit object should stay on the catcher plate when the object is caught by the catcher. @@ -45,8 +47,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public virtual void CopyStateFrom(IHasCatchObjectState objectState) { HitObject = objectState.HitObject; - Scale = new Vector2(objectState.Scale); - Rotation = objectState.Rotation; + Scale = Vector2.Divide(objectState.DisplaySize, Size); + Rotation = objectState.DisplayRotation; AccentColour.Value = objectState.AccentColour.Value; HyperDash.Value = objectState.HyperDash.Value; } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 9ddf56a9cd..7df06bd92d 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -29,16 +29,14 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables /// protected virtual float ScaleFactor => 1; - public float DisplayRadius => CatchHitObject.OBJECT_RADIUS * HitObject.Scale * ScaleFactor; - /// /// The container internal transforms (such as scaling based on the circle size) are applied to. /// protected readonly Container ScalingContainer; - float IHasCatchObjectState.Scale => HitObject.Scale * ScaleFactor; + public Vector2 DisplaySize => ScalingContainer.Size * ScalingContainer.Scale; - float IHasCatchObjectState.Rotation => ScalingContainer.Rotation; + public float DisplayRotation => ScalingContainer.Rotation; protected DrawablePalpableCatchHitObject([CanBeNull] CatchHitObject h) : base(h) @@ -65,7 +63,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables ScaleBindable.BindValueChanged(scale => { ScalingContainer.Scale = new Vector2(scale.NewValue * ScaleFactor); - Size = ScalingContainer.Size * ScalingContainer.Scale; + Size = DisplaySize; }, true); IndexInBeatmap.BindValueChanged(_ => UpdateComboColour()); diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs index fac0249985..d68e32aca9 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Bindables; +using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Objects.Drawables @@ -15,7 +16,7 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables Bindable AccentColour { get; } Bindable HyperDash { get; } - float Rotation { get; } - float Scale { get; } + Vector2 DisplaySize { get; } + float DisplayRotation { get; } } } diff --git a/osu.Game.Rulesets.Catch/UI/Catcher.cs b/osu.Game.Rulesets.Catch/UI/Catcher.cs index 64dacd09e6..f164c2655a 100644 --- a/osu.Game.Rulesets.Catch/UI/Catcher.cs +++ b/osu.Game.Rulesets.Catch/UI/Catcher.cs @@ -235,7 +235,7 @@ namespace osu.Game.Rulesets.Catch.UI if (result.IsHit) { - var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplayRadius); + var positionInStack = computePositionInStack(new Vector2(palpableObject.X - X, 0), palpableObject.DisplaySize.X / 2); placeCaughtObject(palpableObject, positionInStack); From b3d834731586f16cff8ea513ab1a361283b38078 Mon Sep 17 00:00:00 2001 From: Firmatorenio Date: Thu, 10 Dec 2020 20:11:08 +0600 Subject: [PATCH 5277/6909] add support for ScorePosition into LegacyManiaSkin --- osu.Game/Skinning/LegacyManiaSkinConfiguration.cs | 1 + osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs | 1 + osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 4 ++++ 3 files changed, 6 insertions(+) diff --git a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs index 35a6140cbc..a8225a5bea 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfiguration.cs @@ -35,6 +35,7 @@ namespace osu.Game.Skinning public float HitPosition = (480 - 402) * POSITION_SCALE_FACTOR; public float LightPosition = (480 - 413) * POSITION_SCALE_FACTOR; + public float ScorePosition = (480 - 402 + 150) * POSITION_SCALE_FACTOR; public bool ShowJudgementLine = true; public bool KeysUnderNotes; diff --git a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs index a99710ea96..9db6c8bf66 100644 --- a/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs +++ b/osu.Game/Skinning/LegacyManiaSkinConfigurationLookup.cs @@ -25,6 +25,7 @@ namespace osu.Game.Skinning LeftLineWidth, RightLineWidth, HitPosition, + ScorePosition, LightPosition, HitTargetImage, ShowJudgementLine, diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 3dbec23194..49feb9c3f0 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -94,6 +94,10 @@ namespace osu.Game.Skinning currentConfig.LightPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; + case "ScorePosition": + currentConfig.ScorePosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + break; + case "JudgementLine": currentConfig.ShowJudgementLine = pair.Value == "1"; break; From cc5639d2b42cfd9b05272fb659a72c879cd1e3c5 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Thu, 10 Dec 2020 09:48:40 -0600 Subject: [PATCH 5278/6909] added unneeded whitespace --- 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 2b9c817094..5a827eef96 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -125,7 +125,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= 0.5 + accuracy / 2.0; // It is important to also consider accuracy difficulty when doing that aimValue *= 0.98 + Math.Pow(Attributes.OverallDifficulty, 2) / 2500; - + return aimValue; } From 1f2946d64c988362c528b2401091252416a99cc2 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Thu, 10 Dec 2020 14:21:06 -0600 Subject: [PATCH 5279/6909] changed miss penalty curve to scale with totalhits --- .../Difficulty/OsuPerformanceCalculator.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 0ebe0ddc2d..8a641992d3 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -93,7 +93,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available - aimValue *= Math.Pow(0.97, countMiss); + if (countMiss > 0) + aimValue *= .97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, .775), Math.Pow(countMiss, .75)); // Combo scaling if (Attributes.MaxCombo > 0) @@ -139,7 +140,8 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= lengthBonus; // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available - speedValue *= Math.Pow(0.97, countMiss); + if (countMiss > 0) + speedValue *= .97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, .775), Math.Pow(countMiss, 1)); // Combo scaling if (Attributes.MaxCombo > 0) From fd0d793c6966beb9d93cffbbef9cc809e76e9d73 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Thu, 10 Dec 2020 18:51:54 -0600 Subject: [PATCH 5280/6909] changed the comment to reflect the change --- 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 8a641992d3..66ebc63243 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty aimValue *= lengthBonus; - // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available + // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) aimValue *= .97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, .775), Math.Pow(countMiss, .75)); @@ -139,7 +139,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty (totalHits > 2000 ? Math.Log10(totalHits / 2000.0) * 0.5 : 0.0); speedValue *= lengthBonus; - // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available + // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) speedValue *= .97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, .775), Math.Pow(countMiss, 1)); From 3fb41a20b55aa7e2d5270195e1a182dec18ed80b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Dec 2020 13:27:52 +0900 Subject: [PATCH 5281/6909] Add room name to settings --- .../Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs index 8f0f4febd1..a5dcafefcd 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs @@ -18,11 +18,13 @@ namespace osu.Game.Online.RealtimeMultiplayer public int? RulesetID { get; set; } + public string Name { get; set; } = "Unnamed room"; + [NotNull] public IEnumerable Mods { get; set; } = Enumerable.Empty(); - public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID; + public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID && Name.Equals(other.Name, StringComparison.Ordinal); - public override string ToString() => $"Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; + public override string ToString() => $"Name:{Name} Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; } } From c1c0b9a9db1c9a35114fd0535fc49ed3b5671e9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Dec 2020 14:10:45 +0900 Subject: [PATCH 5282/6909] Add realtime to room categories --- osu.Game/Online/Multiplayer/RoomCategory.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/RoomCategory.cs b/osu.Game/Online/Multiplayer/RoomCategory.cs index 636a73a3e9..d6786a72fe 100644 --- a/osu.Game/Online/Multiplayer/RoomCategory.cs +++ b/osu.Game/Online/Multiplayer/RoomCategory.cs @@ -6,6 +6,7 @@ namespace osu.Game.Online.Multiplayer public enum RoomCategory { Normal, - Spotlight + Spotlight, + Realtime, } } From 719b08b22fc0f078194b3fa54c64c78f88786666 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Dec 2020 14:11:42 +0900 Subject: [PATCH 5283/6909] Make room setting's BeatmapID non-nullable --- osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs index a5dcafefcd..3934b8c1d7 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs @@ -14,7 +14,7 @@ namespace osu.Game.Online.RealtimeMultiplayer [Serializable] public class MultiplayerRoomSettings : IEquatable { - public int? BeatmapID { get; set; } + public int BeatmapID { get; set; } public int? RulesetID { get; set; } From d3b2e2b36eebcb6913244df225f4007bd014708b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Dec 2020 14:43:17 +0900 Subject: [PATCH 5284/6909] Change locking method to better allow cross-thread locking --- .../RealtimeMultiplayer/LockUntilDisposal.cs | 24 ------------------- .../RealtimeMultiplayer/MultiplayerRoom.cs | 20 +++++++++++++++- 2 files changed, 19 insertions(+), 25 deletions(-) delete mode 100644 osu.Game/Online/RealtimeMultiplayer/LockUntilDisposal.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/LockUntilDisposal.cs b/osu.Game/Online/RealtimeMultiplayer/LockUntilDisposal.cs deleted file mode 100644 index cd16fce2ee..0000000000 --- a/osu.Game/Online/RealtimeMultiplayer/LockUntilDisposal.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; -using System.Threading; - -namespace osu.Game.Online.RealtimeMultiplayer -{ - public readonly struct LockUntilDisposal : IDisposable - { - private readonly object lockTarget; - - public LockUntilDisposal(object lockTarget) - { - this.lockTarget = lockTarget; - Monitor.Enter(lockTarget); - } - - public void Dispose() - { - Monitor.Exit(lockTarget); - } - } -} diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs index 5704ddd675..e009a34707 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs @@ -5,7 +5,9 @@ using System; using System.Collections.Generic; +using System.Threading; using Newtonsoft.Json; +using osu.Framework.Allocation; namespace osu.Game.Online.RealtimeMultiplayer { @@ -48,10 +50,26 @@ namespace osu.Game.Online.RealtimeMultiplayer RoomID = roomId; } + private object updateLock = new object(); + + private ManualResetEventSlim freeForWrite = new ManualResetEventSlim(true); + /// /// Request a lock on this room to perform a thread-safe update. /// - public LockUntilDisposal LockForUpdate() => new LockUntilDisposal(writeLock); + public IDisposable LockForUpdate() + { + // ReSharper disable once InconsistentlySynchronizedField + freeForWrite.Wait(); + + lock (updateLock) + { + freeForWrite.Wait(); + freeForWrite.Reset(); + + return new ValueInvokeOnDisposal(this, r => freeForWrite.Set()); + } + } public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]"; } From 2dd591125673b4b4d6b130204995bfe41cf54de9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Dec 2020 14:44:01 +0900 Subject: [PATCH 5285/6909] Rename method to better match purpose --- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 0bbdc980db..0248432917 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -96,10 +96,10 @@ namespace osu.Game.Screens.Play GameplayClock.IsPaused.BindTo(IsPaused); - IsPaused.BindValueChanged(onPaused); + IsPaused.BindValueChanged(onPauseChanged); } - private void onPaused(ValueChangedEvent isPaused) + private void onPauseChanged(ValueChangedEvent isPaused) { if (isPaused.NewValue) this.TransformBindableTo(pauseFreqAdjust, 0, 200, Easing.Out).OnComplete(_ => adjustableClock.Stop()); From c0d20d8ce430a2ee7257c7ac8cf34eeefd54489f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Dec 2020 16:43:00 +0900 Subject: [PATCH 5286/6909] Add some spacing to interface class --- .../Objects/Drawables/IHasCatchObjectState.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs index d68e32aca9..81b61f0959 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/IHasCatchObjectState.cs @@ -13,10 +13,13 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables public interface IHasCatchObjectState { PalpableCatchHitObject HitObject { get; } + Bindable AccentColour { get; } + Bindable HyperDash { get; } Vector2 DisplaySize { get; } + float DisplayRotation { get; } } } From a35060ea7a3c7c825b826b4ad7a65a898249d8bb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Dec 2020 17:56:00 +0900 Subject: [PATCH 5287/6909] Add a simple cache-busting query string to online.db retrieval As we are finally pushing updates for this database, this adds a minimum level of guarantee that a client will request a new version (without having to worry about multiple levels of server-side caching). --- osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index c4563d5844..e90ccbb805 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -104,7 +104,7 @@ namespace osu.Game.Beatmaps 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 = new FileWebRequest(compressedCacheFilePath, $"https://assets.ppy.sh/client-resources/{cache_database_name}.bz2?{DateTimeOffset.UtcNow:yyyyMMdd}"); cacheDownloadRequest.Failed += ex => { From b5f6baf3412128bc142d966affffc433abfbe490 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Dec 2020 18:03:48 +0900 Subject: [PATCH 5288/6909] 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 9a3d42d6b7..e0bf1867b9 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 9d37ceee6c..bd8a7c73b2 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index ab03393836..c5e7c45eda 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From aedb18b9f21e0e98af5f3720369314658565a2aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 11 Dec 2020 18:14:33 +0900 Subject: [PATCH 5289/6909] Make RulesetID non-nullable --- osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs index 3934b8c1d7..d2f64235c3 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs @@ -16,7 +16,7 @@ namespace osu.Game.Online.RealtimeMultiplayer { public int BeatmapID { get; set; } - public int? RulesetID { get; set; } + public int RulesetID { get; set; } public string Name { get; set; } = "Unnamed room"; From 544160798b6448df40d9c2a9d85ca210f14fa3d4 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Fri, 11 Dec 2020 08:01:45 -0600 Subject: [PATCH 5290/6909] cleaned up mistakes and made quality changes --- 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 66ebc63243..2c41959d3f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) - aimValue *= .97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, .775), Math.Pow(countMiss, .75)); + aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, 0.775), countMiss); // Combo scaling if (Attributes.MaxCombo > 0) @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) - speedValue *= .97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, .775), Math.Pow(countMiss, 1)); + speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, 0.775), countMiss); // Combo scaling if (Attributes.MaxCombo > 0) From abc8a2982c68163e29a80736c5227d51d01d0417 Mon Sep 17 00:00:00 2001 From: Xexxar Date: Fri, 11 Dec 2020 08:20:56 -0600 Subject: [PATCH 5291/6909] swapped ^.75 buff onto speed instead of aim --- 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 2c41959d3f..5d3552798c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) - aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, 0.775), countMiss); + aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss); // Combo scaling if (Attributes.MaxCombo > 0) @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) - speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, 0.775), countMiss); + speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .75)); // Combo scaling if (Attributes.MaxCombo > 0) From 3acaa63373f1465249b537a956b0b9901936cedd Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Fri, 11 Dec 2020 23:24:38 +0900 Subject: [PATCH 5292/6909] CI fixes --- 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 2c41959d3f..ebcebeae7c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -94,7 +94,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) - aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, 0.775), countMiss); + aimValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss); // Combo scaling if (Attributes.MaxCombo > 0) @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) - speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / (double)totalHits, 0.775), countMiss); + speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), countMiss); // Combo scaling if (Attributes.MaxCombo > 0) From b7872a54b885a2572ec439e8e2a8c0cb2f47dccf Mon Sep 17 00:00:00 2001 From: Xexxar Date: Fri, 11 Dec 2020 10:48:53 -0600 Subject: [PATCH 5293/6909] small factor rebalance --- 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 5d3552798c..a43c78f93f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Penalize misses by assessing # of misses relative to the total # of objects. Default a 3% reduction for any # of misses. if (countMiss > 0) - speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .75)); + speedValue *= 0.97 * Math.Pow(1 - Math.Pow((double)countMiss / totalHits, 0.775), Math.Pow(countMiss, .875)); // Combo scaling if (Attributes.MaxCombo > 0) From f20c5a2bda5f3d4a199af516e22b8caf1e97526a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 12 Dec 2020 15:29:21 +0900 Subject: [PATCH 5294/6909] Update framework (again) --- 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 e0bf1867b9..eaedcb7bc3 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 bd8a7c73b2..b4c7dca12f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index c5e7c45eda..7542aded86 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 08b79bb92126925a538eb8ff62a7c7d058f6c574 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 12 Dec 2020 17:12:15 +0100 Subject: [PATCH 5295/6909] Store and return unstarted task for consumers to await on. --- osu.Game/OsuGame.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 0ad58b2438..6c32e2e94c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -429,8 +429,9 @@ namespace osu.Game public override Task Import(Stream stream, string filename) { - WaitForReady(() => this, _ => Task.Run(() => base.Import(stream, filename))); - return Task.CompletedTask; + var importTask = new Task(async () => await base.Import(stream, filename)); + WaitForReady(() => this, _ => importTask.Start()); + return importTask; } protected virtual Loader CreateLoader() => new Loader(); From 8292c746ea82e35412f717fe8d6db6921661e54e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Dec 2020 22:42:40 +0100 Subject: [PATCH 5296/6909] Leverage hitobject model for strong hit instead of creating own --- osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index e4c0766844..c1b422452e 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Utils; @@ -9,7 +10,6 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; -using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.Taiko.Judgements; using osu.Game.Rulesets.Taiko.Objects; @@ -126,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Tests var cpi = new ControlPointInfo(); cpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); - Hit hit = new Hit(); + Hit hit = new Hit { IsStrong = true }; hit.ApplyDefaults(cpi, new BeatmapDifficulty()); var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) }; @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Taiko.Tests DrawableRuleset.Playfield.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 }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h.NestedHitObjects.Single(), new JudgementResult(new HitObject(), new TaikoStrongJudgement()) { Type = HitResult.Great }); } private void addMissJudgement() @@ -210,15 +210,5 @@ namespace osu.Game.Rulesets.Taiko.Tests DrawableRuleset.Playfield.Add(new DrawableHit(h)); } - - private class TestStrongNestedHit : DrawableStrongNestedHit - { - public TestStrongNestedHit(DrawableHitObject mainObject) - : base(new StrongHitObject { StartTime = mainObject.HitObject.StartTime }, mainObject) - { - } - - public override bool OnPressed(TaikoAction action) => false; - } } } From 34e7a36b382ba88d4e2ae74fceb8ad7833133090 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Dec 2020 22:48:22 +0100 Subject: [PATCH 5297/6909] Fix kiai hit steps not working correctly --- osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs | 7 +++++-- osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs | 14 ++------------ 2 files changed, 7 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs index fb0917341e..f048cad18c 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTestHit.cs @@ -13,12 +13,15 @@ namespace osu.Game.Rulesets.Taiko.Tests { public readonly HitResult Type; - public DrawableTestHit(Hit hit, HitResult type = HitResult.Great) + public DrawableTestHit(Hit hit, HitResult type = HitResult.Great, bool kiai = false) : base(hit) { Type = type; - HitObject.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); + var controlPoints = new ControlPointInfo(); + controlPoints.Add(0, new EffectControlPoint { KiaiMode = kiai }); + + HitObject.ApplyDefaults(controlPoints, new BeatmapDifficulty()); } protected override void UpdateInitialTransforms() diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index c1b422452e..14a127ac82 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -106,13 +106,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great; - var cpi = new ControlPointInfo(); - cpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); - Hit hit = new Hit(); - hit.ApplyDefaults(cpi, new BeatmapDifficulty()); - - var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) }; + var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) }; DrawableRuleset.Playfield.Add(h); @@ -123,13 +118,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great; - var cpi = new ControlPointInfo(); - cpi.Add(0, new EffectControlPoint { KiaiMode = kiai }); - Hit hit = new Hit { IsStrong = true }; - hit.ApplyDefaults(cpi, new BeatmapDifficulty()); - - var h = new DrawableTestHit(hit) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) }; + var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) }; DrawableRuleset.Playfield.Add(h); From 76193e221754455ac9638f7974437c2259b9fc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Dec 2020 22:52:01 +0100 Subject: [PATCH 5298/6909] Fix miss step not working --- osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 14a127ac82..d257712553 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -130,8 +130,11 @@ namespace osu.Game.Rulesets.Taiko.Tests private void addMissJudgement() { DrawableTestHit h; - DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit(), HitResult.Miss)); - ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(new HitObject(), new TaikoJudgement()) { Type = HitResult.Miss }); + DrawableRuleset.Playfield.Add(h = new DrawableTestHit(new Hit { StartTime = DrawableRuleset.Playfield.Time.Current }, HitResult.Miss) + { + Alpha = 0 + }); + ((TaikoPlayfield)DrawableRuleset.Playfield).OnNewResult(h, new JudgementResult(h.HitObject, new TaikoJudgement()) { Type = HitResult.Miss }); } private void addBarLine(bool major, double delay = scroll_time) From 43c0e2191deaad07687704619d69b2c10056c0a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 12 Dec 2020 23:50:12 +0100 Subject: [PATCH 5299/6909] Apply local fix for strong/colour not being applied correctly --- .../TestSceneHits.cs | 29 +++++++++++++++++-- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index d257712553..1d51020a6f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -2,10 +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.Graphics; using osu.Framework.Utils; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Judgements; @@ -118,7 +120,11 @@ namespace osu.Game.Rulesets.Taiko.Tests { HitResult hitResult = RNG.Next(2) == 0 ? HitResult.Ok : HitResult.Great; - Hit hit = new Hit { IsStrong = true }; + Hit hit = new Hit + { + IsStrong = true, + Samples = createSamples(strong: true) + }; var h = new DrawableTestHit(hit, kiai: kiai) { X = RNG.NextSingle(hitResult == HitResult.Ok ? -0.1f : -0.05f, hitResult == HitResult.Ok ? 0.1f : 0.05f) }; DrawableRuleset.Playfield.Add(h); @@ -166,6 +172,7 @@ namespace osu.Game.Rulesets.Taiko.Tests { StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, IsStrong = strong, + Samples = createSamples(strong: strong), Duration = duration, TickRate = 8, }; @@ -183,7 +190,8 @@ namespace osu.Game.Rulesets.Taiko.Tests Hit h = new Hit { StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, - IsStrong = strong + IsStrong = strong, + Samples = createSamples(HitType.Centre, strong) }; h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); @@ -196,12 +204,27 @@ namespace osu.Game.Rulesets.Taiko.Tests Hit h = new Hit { StartTime = DrawableRuleset.Playfield.Time.Current + scroll_time, - IsStrong = strong + IsStrong = strong, + Samples = createSamples(HitType.Rim, strong) }; h.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty()); DrawableRuleset.Playfield.Add(new DrawableHit(h)); } + + // TODO: can be removed if a better way of handling colour/strong type and samples is developed + private IList createSamples(HitType? hitType = null, bool strong = false) + { + var samples = new List(); + + if (hitType == HitType.Rim) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_CLAP)); + + if (strong) + samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); + + return samples; + } } } From 3a3b32186ee62acc40a66f9047a777d4d2b72b2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Dec 2020 00:00:37 +0100 Subject: [PATCH 5300/6909] Make height test steps work better --- .../DrawableTaikoRulesetTestScene.cs | 8 +++++--- osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs index d1c4a1c56d..783636a62d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/DrawableTaikoRulesetTestScene.cs @@ -16,6 +16,8 @@ namespace osu.Game.Rulesets.Taiko.Tests { public abstract class DrawableTaikoRulesetTestScene : OsuTestScene { + protected const int DEFAULT_PLAYFIELD_CONTAINER_HEIGHT = 768; + protected DrawableTaikoRuleset DrawableRuleset { get; private set; } protected Container PlayfieldContainer { get; private set; } @@ -44,10 +46,10 @@ namespace osu.Game.Rulesets.Taiko.Tests Add(PlayfieldContainer = new Container { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.X, - Height = 768, + Height = DEFAULT_PLAYFIELD_CONTAINER_HEIGHT, Children = new[] { DrawableRuleset = new DrawableTaikoRuleset(new TaikoRuleset(), beatmap.GetPlayableBeatmap(new TaikoRuleset().RulesetInfo)) } }); } diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs index 1d51020a6f..c3fa03d404 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHits.cs @@ -99,7 +99,7 @@ namespace osu.Game.Rulesets.Taiko.Tests break; case 6: - PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, TaikoPlayfield.DEFAULT_HEIGHT), 500); + PlayfieldContainer.Delay(delay).ResizeTo(new Vector2(1, DEFAULT_PLAYFIELD_CONTAINER_HEIGHT), 500); break; } } From 1ddc896b765a72c5135e2bd10e8759293dcce73b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Dec 2020 12:18:29 +0100 Subject: [PATCH 5301/6909] Rename Strong{-> Nested}HitObject --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 2 +- .../Objects/Drawables/DrawableDrumRoll.cs | 6 +++--- .../Objects/Drawables/DrawableDrumRollTick.cs | 6 +++--- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 6 +++--- .../Objects/Drawables/DrawableStrongNestedHit.cs | 4 ++-- .../Objects/Drawables/DrawableTaikoHitObject.cs | 6 +++--- .../{StrongHitObject.cs => StrongNestedHitObject.cs} | 2 +- osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs | 2 +- 8 files changed, 17 insertions(+), 17 deletions(-) rename osu.Game.Rulesets.Taiko/Objects/{StrongHitObject.cs => StrongNestedHitObject.cs} (89%) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index 99e103da3b..e62841b2c4 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning createDrawableRuleset(); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); - assertStateAfterResult(new JudgementResult(new StrongHitObject(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); + assertStateAfterResult(new JudgementResult(new StrongNestedHitObject(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); } [Test] diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 4ead4982a1..4330e426c1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Content.X = DrawHeight / 2; } - protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); private void updateColour() { @@ -164,8 +164,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private class StrongNestedHit : DrawableStrongNestedHit { - public StrongNestedHit(StrongHitObject strong, DrawableDrumRoll drumRoll) - : base(strong, drumRoll) + public StrongNestedHit(StrongNestedHitObject nestedHit, DrawableDrumRoll drumRoll) + : base(nestedHit, drumRoll) { } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index e68e40ae1c..c458a620bd 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -61,12 +61,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return UpdateResult(true); } - protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); private class StrongNestedHit : DrawableStrongNestedHit { - public StrongNestedHit(StrongHitObject strong, DrawableDrumRollTick tick) - : base(strong, tick) + public StrongNestedHit(StrongNestedHitObject nestedHit, DrawableDrumRollTick tick) + : base(nestedHit, tick) { } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index d1751d8a75..5837a391b4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); private class StrongNestedHit : DrawableStrongNestedHit { @@ -240,8 +240,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public new DrawableHit MainObject => (DrawableHit)base.MainObject; - public StrongNestedHit(StrongHitObject strong, DrawableHit hit) - : base(strong, hit) + public StrongNestedHit(StrongNestedHitObject nestedHit, DrawableHit hit) + : base(nestedHit, hit) { } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index 108e42eea5..e1da357c47 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -13,8 +13,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public readonly DrawableHitObject MainObject; - protected DrawableStrongNestedHit(StrongHitObject strong, DrawableHitObject mainObject) - : base(strong) + protected DrawableStrongNestedHit(StrongNestedHitObject nestedHit, DrawableHitObject mainObject) + : base(nestedHit) { MainObject = mainObject; } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index ff5b221273..42f24f1f52 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -208,7 +208,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { switch (hitObject) { - case StrongHitObject strong: + case StrongNestedHitObject strong: return CreateStrongHit(strong); } @@ -221,11 +221,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected abstract SkinnableDrawable CreateMainPiece(); /// - /// Creates the handler for this 's . + /// Creates the handler for this 's . /// This is only invoked if is true for . /// /// The strong hitobject. /// The strong hitobject handler. - protected virtual DrawableStrongNestedHit CreateStrongHit(StrongHitObject hitObject) => null; + protected virtual DrawableStrongNestedHit CreateStrongHit(StrongNestedHitObject hitObject) => null; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs similarity index 89% rename from osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs rename to osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs index 72a04698be..b2def4c6e5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/StrongHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects { - public class StrongHitObject : TaikoHitObject + public class StrongNestedHitObject : TaikoHitObject { public override Judgement CreateJudgement() => new TaikoStrongJudgement(); diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index d2c37d965c..2c1e3f32f3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Taiko.Objects base.CreateNestedHitObjects(cancellationToken); if (IsStrong) - AddNested(new StrongHitObject { StartTime = this.GetEndTime() }); + AddNested(new StrongNestedHitObject { StartTime = this.GetEndTime() }); } public override Judgement CreateJudgement() => new TaikoJudgement(); From f74567e8eb393d351039c0208700cc703bfc3e3f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Dec 2020 12:36:39 +0100 Subject: [PATCH 5302/6909] Introduce base class for hitobjects that can be strong --- .../TaikoBeatmapConversionTest.cs | 2 +- .../Beatmaps/TaikoBeatmapConverter.cs | 4 +- .../Edit/TaikoSelectionHandler.cs | 2 +- .../Drawables/DrawableTaikoHitObject.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- .../Objects/DrumRollTick.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Hit.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Swell.cs | 2 - .../Objects/TaikoHitObject.cs | 44 ------------------- .../Objects/TaikoStrongHitObject.cs | 42 ++++++++++++++++++ .../Replays/TaikoAutoGenerator.cs | 4 +- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 2 +- osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs | 6 +-- 13 files changed, 56 insertions(+), 60 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index 5e550a5d03..126af1a6ad 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Tests IsCentre = (hitObject as Hit)?.Type == HitType.Centre, IsDrumRoll = hitObject is DrumRoll, IsSwell = hitObject is Swell, - IsStrong = ((TaikoHitObject)hitObject).IsStrong + IsStrong = (hitObject as TaikoStrongHitObject)?.IsStrong == true }; } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 607eaf5dbd..78a0d1fdff 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -65,8 +65,8 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => { TaikoHitObject first = x.First(); - if (x.Skip(1).Any() && first.CanBeStrong) - first.IsStrong = true; + if (x.Skip(1).Any() && first is TaikoStrongHitObject strong) + strong.IsStrong = true; return first; }).ToList(); } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index a05de1f217..7de920ca8c 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Edit base.UpdateTernaryStates(); selectionRimState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType(), h => h.Type == HitType.Rim); - selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType(), h => h.IsStrong); + selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType(), h => h.IsStrong); } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 42f24f1f52..1e39e7e97e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -158,7 +158,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.LoadSamples(); - if (HitObject.CanBeStrong) + if (HitObject is TaikoStrongHitObject) isStrong.Value = getStrongSamples().Any(); } diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 5f52160be1..471e1a7b2f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRoll : TaikoHitObject, IHasPath + public class DrumRoll : TaikoStrongHitObject, IHasPath { /// /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 8a8be3e38d..5c36a3203c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRollTick : TaikoHitObject + public class DrumRollTick : TaikoStrongHitObject { /// /// Whether this is the first (initial) tick of the slider. diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index 68cc8d0ead..d6385c810d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -5,7 +5,7 @@ using osu.Framework.Bindables; namespace osu.Game.Rulesets.Taiko.Objects { - public class Hit : TaikoHitObject + public class Hit : TaikoStrongHitObject { public readonly Bindable TypeBindable = new Bindable(); diff --git a/osu.Game.Rulesets.Taiko/Objects/Swell.cs b/osu.Game.Rulesets.Taiko/Objects/Swell.cs index bf8b7bc178..eeae6e79f8 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Swell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Swell.cs @@ -17,8 +17,6 @@ namespace osu.Game.Rulesets.Taiko.Objects set => Duration = value - StartTime; } - public override bool CanBeStrong => false; - public double Duration { get; set; } /// diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs index 2c1e3f32f3..f047c03f4b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoHitObject.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.Threading; -using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Scoring; @@ -19,47 +16,6 @@ namespace osu.Game.Rulesets.Taiko.Objects /// public const float DEFAULT_SIZE = 0.45f; - /// - /// Scale multiplier for a strong drawable taiko hit object. - /// - public const float STRONG_SCALE = 1.4f; - - /// - /// Default size of a strong drawable taiko hit object. - /// - public const float DEFAULT_STRONG_SIZE = DEFAULT_SIZE * STRONG_SCALE; - - public readonly Bindable IsStrongBindable = new BindableBool(); - - /// - /// Whether this can be made a "strong" (large) hit. - /// - public virtual bool CanBeStrong => true; - - /// - /// Whether this HitObject is a "strong" type. - /// Strong hit objects give more points for hitting the hit object with both keys. - /// - public bool IsStrong - { - get => IsStrongBindable.Value; - set - { - if (value && !CanBeStrong) - throw new InvalidOperationException($"Object of type {GetType()} cannot be strong"); - - IsStrongBindable.Value = value; - } - } - - protected override void CreateNestedHitObjects(CancellationToken cancellationToken) - { - base.CreateNestedHitObjects(cancellationToken); - - if (IsStrong) - AddNested(new StrongNestedHitObject { StartTime = this.GetEndTime() }); - } - public override Judgement CreateJudgement() => new TaikoJudgement(); protected override HitWindows CreateHitWindows() => new TaikoHitWindows(); diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs new file mode 100644 index 0000000000..a8d00b7681 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs @@ -0,0 +1,42 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using osu.Framework.Bindables; +using osu.Game.Rulesets.Objects; + +namespace osu.Game.Rulesets.Taiko.Objects +{ + public abstract class TaikoStrongHitObject : TaikoHitObject + { + /// + /// Scale multiplier for a strong drawable taiko hit object. + /// + public const float STRONG_SCALE = 1.4f; + + /// + /// Default size of a strong drawable taiko hit object. + /// + 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 bool IsStrong + { + get => IsStrongBindable.Value; + set => IsStrongBindable.Value = value; + } + + protected override void CreateNestedHitObjects(CancellationToken cancellationToken) + { + base.CreateNestedHitObjects(cancellationToken); + + if (IsStrong) + AddNested(new StrongNestedHitObject { StartTime = this.GetEndTime() }); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs index db2e5948f5..a3dbe672a4 100644 --- a/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs +++ b/osu.Game.Rulesets.Taiko/Replays/TaikoAutoGenerator.cs @@ -102,13 +102,13 @@ namespace osu.Game.Rulesets.Taiko.Replays if (hit.Type == HitType.Centre) { - actions = h.IsStrong + actions = hit.IsStrong ? new[] { TaikoAction.LeftCentre, TaikoAction.RightCentre } : new[] { hitButton ? TaikoAction.LeftCentre : TaikoAction.RightCentre }; } else { - actions = h.IsStrong + actions = hit.IsStrong ? new[] { TaikoAction.LeftRim, TaikoAction.RightRim } : new[] { hitButton ? TaikoAction.LeftRim : TaikoAction.RightRim }; } diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index 247c0dde73..05408e1049 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.UI /// public void VisualiseSecondHit() { - this.ResizeTo(new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), 50); + this.ResizeTo(new Vector2(TaikoStrongHitObject.DEFAULT_STRONG_SIZE), 50); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs index caddc8b122..5d8145391d 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, - Size = new Vector2(border_thickness, (1 - TaikoHitObject.DEFAULT_STRONG_SIZE) / 2f), + Size = new Vector2(border_thickness, (1 - TaikoStrongHitObject.DEFAULT_STRONG_SIZE) / 2f), Alpha = 0.1f }, new CircularContainer @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Size = new Vector2(TaikoHitObject.DEFAULT_STRONG_SIZE), + Size = new Vector2(TaikoStrongHitObject.DEFAULT_STRONG_SIZE), Masking = true, BorderColour = Color4.White, BorderThickness = border_thickness, @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.Y, - Size = new Vector2(border_thickness, (1 - TaikoHitObject.DEFAULT_STRONG_SIZE) / 2f), + Size = new Vector2(border_thickness, (1 - TaikoStrongHitObject.DEFAULT_STRONG_SIZE) / 2f), Alpha = 0.1f }, }; From b1635ecd166456bfa0667c80af75f1ea642f603c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Dec 2020 12:51:10 +0100 Subject: [PATCH 5303/6909] Reflect can-be-strong split in DHO structure --- .../Objects/Drawables/DrawableDrumRoll.cs | 2 +- .../Objects/Drawables/DrawableDrumRollTick.cs | 2 +- .../Objects/Drawables/DrawableHit.cs | 2 +- .../Drawables/DrawableTaikoHitObject.cs | 82 +------------ .../Drawables/DrawableTaikoStrongHitObject.cs | 110 ++++++++++++++++++ .../Skinning/Legacy/LegacyCirclePiece.cs | 4 +- 6 files changed, 116 insertions(+), 86 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 4330e426c1..3f8970e506 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -19,7 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public class DrawableDrumRoll : DrawableTaikoHitObject + public class DrawableDrumRoll : DrawableTaikoStrongHitObject { /// /// Number of rolling hits required to reach the dark/final colour. diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index c458a620bd..c21b7983a0 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public class DrawableDrumRollTick : DrawableTaikoHitObject + public class DrawableDrumRollTick : DrawableTaikoStrongHitObject { /// /// The hit type corresponding to the that the user pressed to hit this . diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 5837a391b4..c82ac8498e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -16,7 +16,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public class DrawableHit : DrawableTaikoHitObject + public class DrawableHit : DrawableTaikoStrongHitObject { /// /// A list of keys which can result in hits for this HitObject. diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index 1e39e7e97e..cf3aa69b6f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -4,13 +4,11 @@ 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.Primitives; using osu.Framework.Input.Bindings; using osu.Game.Audio; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Skinning; using osuTK; @@ -120,112 +118,34 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected Vector2 BaseSize; protected SkinnableDrawable MainPiece; - private readonly Bindable isStrong; - - private readonly Container strongHitContainer; - protected DrawableTaikoHitObject(TObject hitObject) : base(hitObject) { HitObject = hitObject; - isStrong = HitObject.IsStrongBindable.GetBoundCopy(); Anchor = Anchor.CentreLeft; Origin = Anchor.Custom; RelativeSizeAxes = Axes.Both; - - AddInternal(strongHitContainer = new Container()); } [BackgroundDependencyLoader] private void load() { - isStrong.BindValueChanged(_ => - { - // will overwrite samples, should only be called on change. - updateSamplesFromStrong(); - - RecreatePieces(); - }); - RecreatePieces(); } - private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); - - protected override void LoadSamples() - { - base.LoadSamples(); - - if (HitObject is TaikoStrongHitObject) - isStrong.Value = getStrongSamples().Any(); - } - - private void updateSamplesFromStrong() - { - var strongSamples = getStrongSamples(); - - if (isStrong.Value != strongSamples.Any()) - { - if (isStrong.Value) - HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); - else - { - foreach (var sample in strongSamples) - HitObject.Samples.Remove(sample); - } - } - } - protected virtual void RecreatePieces() { - Size = BaseSize = new Vector2(HitObject.IsStrong ? TaikoHitObject.DEFAULT_STRONG_SIZE : TaikoHitObject.DEFAULT_SIZE); + Size = BaseSize = new Vector2(TaikoHitObject.DEFAULT_SIZE); MainPiece?.Expire(); Content.Add(MainPiece = CreateMainPiece()); } - protected override void AddNestedHitObject(DrawableHitObject hitObject) - { - base.AddNestedHitObject(hitObject); - - switch (hitObject) - { - case DrawableStrongNestedHit strong: - strongHitContainer.Add(strong); - break; - } - } - - protected override void ClearNestedHitObjects() - { - base.ClearNestedHitObjects(); - strongHitContainer.Clear(); - } - - protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) - { - switch (hitObject) - { - case StrongNestedHitObject strong: - return CreateStrongHit(strong); - } - - return base.CreateNestedHitObject(hitObject); - } - // Most osu!taiko hitsounds are managed by the drum (see DrumSampleMapping). public override IEnumerable GetSamples() => Enumerable.Empty(); protected abstract SkinnableDrawable CreateMainPiece(); - - /// - /// Creates the handler for this 's . - /// This is only invoked if is true for . - /// - /// The strong hitobject. - /// The strong hitobject handler. - protected virtual DrawableStrongNestedHit CreateStrongHit(StrongNestedHitObject hitObject) => null; } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs new file mode 100644 index 0000000000..875612c8a1 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Game.Audio; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osuTK; + +namespace osu.Game.Rulesets.Taiko.Objects.Drawables +{ + public abstract class DrawableTaikoStrongHitObject : DrawableTaikoHitObject + where TObject : TaikoStrongHitObject + { + private readonly Bindable isStrong; + + private readonly Container strongHitContainer; + + protected DrawableTaikoStrongHitObject(TObject hitObject) + : base(hitObject) + { + isStrong = HitObject.IsStrongBindable.GetBoundCopy(); + + AddInternal(strongHitContainer = new Container()); + } + + [BackgroundDependencyLoader] + private void load() + { + isStrong.BindValueChanged(_ => + { + // will overwrite samples, should only be called on change. + updateSamplesFromStrong(); + + RecreatePieces(); + }); + } + + private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); + + protected override void LoadSamples() + { + base.LoadSamples(); + isStrong.Value = getStrongSamples().Any(); + } + + private void updateSamplesFromStrong() + { + var strongSamples = getStrongSamples(); + + if (isStrong.Value != strongSamples.Any()) + { + if (isStrong.Value) + HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_FINISH)); + else + { + foreach (var sample in strongSamples) + HitObject.Samples.Remove(sample); + } + } + } + + protected override void RecreatePieces() + { + base.RecreatePieces(); + if (HitObject.IsStrong) + Size = BaseSize = new Vector2(TaikoStrongHitObject.DEFAULT_STRONG_SIZE); + } + + protected override void AddNestedHitObject(DrawableHitObject hitObject) + { + base.AddNestedHitObject(hitObject); + + switch (hitObject) + { + case DrawableStrongNestedHit strong: + strongHitContainer.Add(strong); + break; + } + } + + protected override void ClearNestedHitObjects() + { + base.ClearNestedHitObjects(); + strongHitContainer.Clear(); + } + + protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) + { + switch (hitObject) + { + case StrongNestedHitObject strong: + return CreateStrongHit(strong); + } + + return base.CreateNestedHitObject(hitObject); + } + + /// + /// Creates the handler for this 's . + /// This is only invoked if is true for . + /// + /// The strong hitobject. + /// The strong hitobject handler. + protected abstract DrawableStrongNestedHit CreateStrongHit(StrongNestedHitObject hitObject); + } +} diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs index 821ddc3c04..52c9080633 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs @@ -7,7 +7,7 @@ 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.Rulesets.Taiko.Objects; using osu.Game.Skinning; using osuTK; using osuTK.Graphics; @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const string normal_hit = "taikohit"; const string big_hit = "taikobig"; - string prefix = ((drawableHitObject as DrawableTaikoHitObject)?.HitObject.IsStrong ?? false) ? big_hit : normal_hit; + string prefix = ((drawableHitObject.HitObject as TaikoStrongHitObject)?.IsStrong ?? false) ? big_hit : normal_hit; return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? // fallback to regular size if "big" version doesn't exist. From 4d444df6b3979852403e46e37c5a1c29237cfb21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Dec 2020 12:53:42 +0100 Subject: [PATCH 5304/6909] Rename DHO CreateStrong{-> Nested}Hit --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 2 +- .../Objects/Drawables/DrawableDrumRollTick.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 2 +- .../Objects/Drawables/DrawableTaikoStrongHitObject.cs | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 3f8970e506..3a7b644158 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Content.X = DrawHeight / 2; } - protected override DrawableStrongNestedHit CreateStrongHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); private void updateColour() { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index c21b7983a0..bc83fc538c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -61,7 +61,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return UpdateResult(true); } - protected override DrawableStrongNestedHit CreateStrongHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); private class StrongNestedHit : DrawableStrongNestedHit { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index c82ac8498e..3928c4bb5e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override DrawableStrongNestedHit CreateStrongHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); private class StrongNestedHit : DrawableStrongNestedHit { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs index 875612c8a1..0aa8be2da5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables switch (hitObject) { case StrongNestedHitObject strong: - return CreateStrongHit(strong); + return CreateStrongNestedHit(strong); } return base.CreateNestedHitObject(hitObject); @@ -105,6 +105,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// The strong hitobject. /// The strong hitobject handler. - protected abstract DrawableStrongNestedHit CreateStrongHit(StrongNestedHitObject hitObject); + protected abstract DrawableStrongNestedHit CreateStrongNestedHit(StrongNestedHitObject hitObject); } } From 61c488cd5eb33314611e87035073112b6a5444b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Dec 2020 12:59:46 +0100 Subject: [PATCH 5305/6909] Create HO-specific nested hit types --- .../Skinning/TestSceneDrawableTaikoMascot.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 6 ++++++ osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs | 6 ++++++ osu.Game.Rulesets.Taiko/Objects/Hit.cs | 6 ++++++ osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs | 9 ++++++++- 6 files changed, 28 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs index e62841b2c4..8d1aafdcc2 100644 --- a/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs +++ b/osu.Game.Rulesets.Taiko.Tests/Skinning/TestSceneDrawableTaikoMascot.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.Taiko.Tests.Skinning createDrawableRuleset(); assertStateAfterResult(new JudgementResult(new Hit(), new TaikoJudgement()) { Type = HitResult.Great }, TaikoMascotAnimationState.Idle); - assertStateAfterResult(new JudgementResult(new StrongNestedHitObject(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); + assertStateAfterResult(new JudgementResult(new Hit.StrongNestedHit(), new TaikoStrongJudgement()) { Type = HitResult.IgnoreMiss }, TaikoMascotAnimationState.Idle); } [Test] diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 471e1a7b2f..93f95d6446 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -109,6 +109,12 @@ namespace osu.Game.Rulesets.Taiko.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; + protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; + + public class StrongNestedHit : StrongNestedHitObject + { + } + #region LegacyBeatmapEncoder double IHasDistance.Distance => Duration * Velocity; diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 5c36a3203c..6b6ffa8668 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -28,5 +28,11 @@ namespace osu.Game.Rulesets.Taiko.Objects public override Judgement CreateJudgement() => new TaikoDrumRollTickJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; + + protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; + + public class StrongNestedHit : StrongNestedHitObject + { + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index d6385c810d..6bdde376f6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -17,5 +17,11 @@ namespace osu.Game.Rulesets.Taiko.Objects get => TypeBindable.Value; set => TypeBindable.Value = value; } + + protected override StrongNestedHitObject CreateStrongNestedHit(double startTime) => new StrongNestedHit { StartTime = startTime }; + + public class StrongNestedHit : StrongNestedHitObject + { + } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs index b2def4c6e5..a0b1d3ef9b 100644 --- a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects { - public class StrongNestedHitObject : TaikoHitObject + public abstract class StrongNestedHitObject : TaikoHitObject { public override Judgement CreateJudgement() => new TaikoStrongJudgement(); diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs index a8d00b7681..6a8e33e718 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs @@ -36,7 +36,14 @@ namespace osu.Game.Rulesets.Taiko.Objects base.CreateNestedHitObjects(cancellationToken); if (IsStrong) - AddNested(new StrongNestedHitObject { StartTime = this.GetEndTime() }); + AddNested(CreateStrongNestedHit(this.GetEndTime())); } + + /// + /// Creates a representing a second hit on this object. + /// This is only called if is true. + /// + /// The start time of the nested hit. + protected abstract StrongNestedHitObject CreateStrongNestedHit(double startTime); } } From 60379b09dbc22a1b8229c45a5a74a8f384a2bb39 Mon Sep 17 00:00:00 2001 From: Firmatorenio Date: Sun, 13 Dec 2020 18:14:41 +0600 Subject: [PATCH 5306/6909] added a container for the judgements to move up or down --- ...awableManiaJudgementAdjustmentContainer.cs | 23 +++++++++++++++++++ osu.Game.Rulesets.Mania/UI/Stage.cs | 8 +------ 2 files changed, 24 insertions(+), 7 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/UI/DrawableManiaJudgementAdjustmentContainer.cs diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgementAdjustmentContainer.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgementAdjustmentContainer.cs new file mode 100644 index 0000000000..b5abd9fb10 --- /dev/null +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgementAdjustmentContainer.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using osu.Game.Rulesets.UI; +using osu.Framework.Graphics; + +namespace osu.Game.Rulesets.Mania.UI +{ + public class DrawableManiaJudgementAdjustmentContainer : JudgementContainer + { + private float scorePosition => 0; + public DrawableManiaJudgementAdjustmentContainer(float hitTargetPosition) + { + Anchor = Anchor.TopCentre; + Origin = Anchor.Centre; + RelativeSizeAxes = Axes.Both; + Y = hitTargetPosition + 150; + } + + public DrawableManiaJudgementAdjustmentContainer() + : this(110) { } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index 3d7960ffe3..73af205a37 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -101,13 +101,7 @@ namespace osu.Game.Rulesets.Mania.UI { RelativeSizeAxes = Axes.Both }, - judgements = new JudgementContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Both, - Y = HIT_TARGET_POSITION + 150, - }, + judgements = new DrawableManiaJudgementAdjustmentContainer(HIT_TARGET_POSITION), topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } } } From 7d2b77cdbdf2f09b5c4938d3c87fe5b181682ab8 Mon Sep 17 00:00:00 2001 From: Graham Johnson Date: Sun, 13 Dec 2020 07:58:58 -0500 Subject: [PATCH 5307/6909] improve selection box rotation UX --- .../Screens/Edit/Compose/Components/SelectionBox.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 742d433760..347d9e3ba7 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -191,7 +191,7 @@ namespace osu.Game.Screens.Edit.Compose.Components { Anchor = Anchor.TopCentre, Y = -separation, - HandleDrag = e => OnRotation?.Invoke(e.Delta.X), + HandleDrag = e => OnRotation?.Invoke(convertDragEventToAngleOfRotation(e)), OperationStarted = operationStarted, OperationEnded = operationEnded } @@ -242,6 +242,15 @@ namespace osu.Game.Screens.Edit.Compose.Components private int activeOperations; + private float convertDragEventToAngleOfRotation(DragEvent e) + { + // Adjust coordinate system to the center of SelectionBox + float startAngle = MathF.Atan2(e.LastMousePosition.Y - DrawHeight / 2, e.LastMousePosition.X - DrawWidth / 2); + float endAngle = MathF.Atan2(e.MousePosition.Y - DrawHeight / 2, e.MousePosition.X - DrawWidth / 2); + + return (endAngle - startAngle) * 180 / MathF.PI; + } + private void operationEnded() { if (--activeOperations == 0) From 091b08b5070e4abac6999cf7e7a9c2ef4c71d560 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Dec 2020 14:38:16 +0100 Subject: [PATCH 5308/6909] Scope drawable nested hits more closely to models --- .../Objects/Drawables/DrawableDrumRoll.cs | 6 +++--- .../Objects/Drawables/DrawableDrumRollTick.cs | 6 +++--- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 6 +++--- .../Objects/Drawables/DrawableTaikoStrongHitObject.cs | 7 ++++--- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 3a7b644158..0c6d28472d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -19,7 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public class DrawableDrumRoll : DrawableTaikoStrongHitObject + public class DrawableDrumRoll : DrawableTaikoStrongHitObject { /// /// Number of rolling hits required to reach the dark/final colour. @@ -154,7 +154,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Content.X = DrawHeight / 2; } - protected override DrawableStrongNestedHit CreateStrongNestedHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); private void updateColour() { @@ -164,7 +164,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private class StrongNestedHit : DrawableStrongNestedHit { - public StrongNestedHit(StrongNestedHitObject nestedHit, DrawableDrumRoll drumRoll) + public StrongNestedHit(DrumRoll.StrongNestedHit nestedHit, DrawableDrumRoll drumRoll) : base(nestedHit, drumRoll) { } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index bc83fc538c..c26e20f92c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public class DrawableDrumRollTick : DrawableTaikoStrongHitObject + public class DrawableDrumRollTick : DrawableTaikoStrongHitObject { /// /// The hit type corresponding to the that the user pressed to hit this . @@ -61,11 +61,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return UpdateResult(true); } - protected override DrawableStrongNestedHit CreateStrongNestedHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); private class StrongNestedHit : DrawableStrongNestedHit { - public StrongNestedHit(StrongNestedHitObject nestedHit, DrawableDrumRollTick tick) + public StrongNestedHit(DrumRollTick.StrongNestedHit nestedHit, DrawableDrumRollTick tick) : base(nestedHit, tick) { } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 3928c4bb5e..31cb3d764d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -16,7 +16,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public class DrawableHit : DrawableTaikoStrongHitObject + public class DrawableHit : DrawableTaikoStrongHitObject { /// /// A list of keys which can result in hits for this HitObject. @@ -228,7 +228,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override DrawableStrongNestedHit CreateStrongNestedHit(StrongNestedHitObject hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); private class StrongNestedHit : DrawableStrongNestedHit { @@ -240,7 +240,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables public new DrawableHit MainObject => (DrawableHit)base.MainObject; - public StrongNestedHit(StrongNestedHitObject nestedHit, DrawableHit hit) + public StrongNestedHit(Hit.StrongNestedHit nestedHit, DrawableHit hit) : base(nestedHit, hit) { } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs index 0aa8be2da5..04e584e15d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs @@ -12,8 +12,9 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public abstract class DrawableTaikoStrongHitObject : DrawableTaikoHitObject + public abstract class DrawableTaikoStrongHitObject : DrawableTaikoHitObject where TObject : TaikoStrongHitObject + where TStrongNestedObject : StrongNestedHitObject { private readonly Bindable isStrong; @@ -92,7 +93,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { switch (hitObject) { - case StrongNestedHitObject strong: + case TStrongNestedObject strong: return CreateStrongNestedHit(strong); } @@ -105,6 +106,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// The strong hitobject. /// The strong hitobject handler. - protected abstract DrawableStrongNestedHit CreateStrongNestedHit(StrongNestedHitObject hitObject); + protected abstract DrawableStrongNestedHit CreateStrongNestedHit(TStrongNestedObject hitObject); } } From 080f7a3e32d9d19bca0b25ebb184c8402d155d1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Dec 2020 13:04:44 +0100 Subject: [PATCH 5309/6909] Add/fix up xmldocs with clarifications --- .../Objects/Drawables/DrawableStrongNestedHit.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs | 4 ++++ osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index e1da357c47..d735d258b3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { /// - /// Used as a nested hitobject to provide s for s. + /// Used as a nested hitobject to provide s for s. /// public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject { diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs index a0b1d3ef9b..2e8989bc79 100644 --- a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs @@ -7,6 +7,10 @@ using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects { + /// + /// Base type for nested strong hits. + /// Used by s to represent their strong bonus scoring portions. + /// public abstract class StrongNestedHitObject : TaikoHitObject { public override Judgement CreateJudgement() => new TaikoStrongJudgement(); diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs index 6a8e33e718..861de95bae 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs @@ -7,6 +7,9 @@ using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Taiko.Objects { + /// + /// Base class for taiko hitobjects that can become strong (large). + /// public abstract class TaikoStrongHitObject : TaikoHitObject { /// From acd07017d1cf3b9c3497b73915363ed3be109725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 13 Dec 2020 16:36:09 +0100 Subject: [PATCH 5310/6909] Replace now-default SDL run config with legacy osuTK config --- .../{osu_SDL.xml => osu___legacy_osuTK_.xml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename .idea/.idea.osu.Desktop/.idea/runConfigurations/{osu_SDL.xml => osu___legacy_osuTK_.xml} (80%) diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml similarity index 80% rename from .idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml rename to .idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml index 31f1fda09d..811fd9cc6d 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu_SDL.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/osu___legacy_osuTK_.xml @@ -1,7 +1,7 @@ - + /// /// This value is the original value specified in the beatmap, not affected by beatmap processing. + /// It should be used instead of when working on a beatmap, not a gameplay. /// public float OriginalX { @@ -32,6 +33,9 @@ namespace osu.Game.Rulesets.Catch.Objects float IHasXPosition.X => OriginalX; + /// + /// An alias of setter. + /// public float X { set => OriginalX = value; From 7cbbd74df247626ae917f766bcc6f03cd5e37a27 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Dec 2020 11:38:07 +0900 Subject: [PATCH 5331/6909] Remove X setter from CatchHitObject --- .../Mods/TestSceneCatchModRelax.cs | 8 +++---- .../TestSceneAutoJuiceStream.cs | 2 +- .../TestSceneCatchModHidden.cs | 2 +- .../TestSceneCatchStacker.cs | 2 +- .../TestSceneCatcher.cs | 22 +++++++++---------- .../TestSceneCatcherArea.cs | 2 +- .../TestSceneDrawableHitObjects.cs | 6 ++--- .../TestSceneHyperDash.cs | 14 ++++++------ .../TestSceneJuiceStream.cs | 4 ++-- .../Beatmaps/CatchBeatmapConverter.cs | 4 ++-- .../Objects/CatchHitObject.cs | 8 ------- .../Objects/JuiceStream.cs | 6 ++--- 12 files changed, 36 insertions(+), 44 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs index c01aff0aa0..da4834aa73 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -32,22 +32,22 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods { new Fruit { - X = CatchPlayfield.CENTER_X, + OriginalX = CatchPlayfield.CENTER_X, StartTime = 0 }, new Fruit { - X = 0, + OriginalX = 0, StartTime = 1000 }, new Fruit { - X = CatchPlayfield.WIDTH, + OriginalX = CatchPlayfield.WIDTH, StartTime = 2000 }, new JuiceStream { - X = CatchPlayfield.CENTER_X, + OriginalX = CatchPlayfield.CENTER_X, StartTime = 3000, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index f552c3c27b..45cf5095f6 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests beatmap.HitObjects.Add(new JuiceStream { - X = CatchPlayfield.CENTER_X - width / 2, + OriginalX = CatchPlayfield.CENTER_X - width / 2, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs index f15da29993..6af9c88088 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Tests { StartTime = 1000, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }), - X = CatchPlayfield.WIDTH / 2 + OriginalX = CatchPlayfield.WIDTH / 2 } } }, diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs index 1ff31697b8..d7835bd8c4 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests { beatmap.HitObjects.Add(new Fruit { - X = (0.5f + i / 2048f * (i % 10 - 5)) * CatchPlayfield.WIDTH, + OriginalX = (0.5f + i / 2048f * (i % 10 - 5)) * CatchPlayfield.WIDTH, StartTime = i * 100, NewCombo = i % 8 == 0 }); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index e8bb57cdf3..d57e8e027e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Tests JudgementResult result2 = null; AddStep("catch hyper fruit", () => { - attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1); + attemptCatch(new Fruit { HyperDashTarget = new Fruit { OriginalX = 100 } }, out drawableObject1, out result1); }); AddStep("catch normal fruit", () => { @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Tests var halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2; AddStep("catch fruit", () => { - attemptCatch(new Fruit { X = -halfWidth + 1 }); - attemptCatch(new Fruit { X = halfWidth - 1 }); + attemptCatch(new Fruit { OriginalX = -halfWidth + 1 }); + attemptCatch(new Fruit { OriginalX = halfWidth - 1 }); }); checkPlate(2); AddStep("miss fruit", () => { - attemptCatch(new Fruit { X = -halfWidth - 1 }); - attemptCatch(new Fruit { X = halfWidth + 1 }); + attemptCatch(new Fruit { OriginalX = -halfWidth - 1 }); + attemptCatch(new Fruit { OriginalX = halfWidth + 1 }); }); checkPlate(2); } @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Catch.Tests [Test] public void TestFruitChangesCatcherState() { - AddStep("miss fruit", () => attemptCatch(new Fruit { X = 100 })); + AddStep("miss fruit", () => attemptCatch(new Fruit { OriginalX = 100 })); checkState(CatcherAnimationState.Fail); AddStep("catch fruit", () => attemptCatch(new Fruit())); checkState(CatcherAnimationState.Idle); @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Catch.Tests { AddStep("catch hyper fruit", () => attemptCatch(new Fruit { - HyperDashTarget = new Fruit { X = 100 } + HyperDashTarget = new Fruit { OriginalX = 100 } })); checkHyperDash(true); AddStep("catch normal fruit", () => attemptCatch(new Fruit())); @@ -147,10 +147,10 @@ namespace osu.Game.Rulesets.Catch.Tests { AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit { - HyperDashTarget = new Fruit { X = 100 } + HyperDashTarget = new Fruit { OriginalX = 100 } })); AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet())); - AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 })); + AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { OriginalX = 100 })); // catcher state and hyper dash state is preserved checkState(CatcherAnimationState.Kiai); checkHyperDash(true); @@ -161,9 +161,9 @@ namespace osu.Game.Rulesets.Catch.Tests { AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit { - HyperDashTarget = new Fruit { X = 100 } + HyperDashTarget = new Fruit { OriginalX = 100 } })); - AddStep("miss banana", () => attemptCatch(new Banana { X = 100 })); + AddStep("miss banana", () => attemptCatch(new Banana { OriginalX = 100 })); // catcher state is preserved but hyper dash state is reset checkState(CatcherAnimationState.Kiai); checkHyperDash(false); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 5079e57e5e..423c3b7a13 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("catch fruit", () => attemptCatch(new Fruit())); AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true })); AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit())); - AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true })); + AddStep("miss last in combo", () => attemptCatch(new Fruit { OriginalX = 100, LastInCombo = true })); } private void attemptCatch(Fruit fruit) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 3e4995482d..2db534e8c9 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Catch.Tests { var fruit = new Fruit { - X = getXCoords(hit), + OriginalX = getXCoords(hit), LastInCombo = i % 4 == 0, StartTime = playfieldTime + 800 + (200 * i) }; @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Catch.Tests var juice = new JuiceStream { - X = xCoords, + OriginalX = xCoords, StartTime = playfieldTime + 1000, Path = new SliderPath(PathType.Linear, new[] { @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Catch.Tests { var banana = new Banana { - X = getXCoords(hit), + OriginalX = getXCoords(hit), LastInCombo = i % 4 == 0, StartTime = playfieldTime + 800 + (200 * i) }; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index db09b2bc6b..67af3c4420 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -70,20 +70,20 @@ namespace osu.Game.Rulesets.Catch.Tests beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); // Should produce a hyper-dash (edge case test) - beatmap.HitObjects.Add(new Fruit { StartTime = 1816, X = 56, NewCombo = true }); - beatmap.HitObjects.Add(new Fruit { StartTime = 2008, X = 308, NewCombo = true }); + beatmap.HitObjects.Add(new Fruit { StartTime = 1816, OriginalX = 56, NewCombo = true }); + beatmap.HitObjects.Add(new Fruit { StartTime = 2008, OriginalX = 308, NewCombo = true }); double startTime = 3000; const float left_x = 0.02f * CatchPlayfield.WIDTH; const float right_x = 0.98f * CatchPlayfield.WIDTH; - createObjects(() => new Fruit { X = left_x }); + createObjects(() => new Fruit { OriginalX = 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 Fruit { OriginalX = right_x }); + createObjects(() => new Fruit { OriginalX = left_x }); + createObjects(() => new Fruit { OriginalX = right_x }); createObjects(() => new TestJuiceStream(left_x), 1); beatmap.ControlPointInfo.Add(startTime, new TimingControlPoint @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public TestJuiceStream(float x) { - X = x; + OriginalX = x; Path = new SliderPath(new[] { diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs index 269e783899..dbcf382d62 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests { new JuiceStream { - X = CatchPlayfield.CENTER_X, + OriginalX = CatchPlayfield.CENTER_X, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, new Banana { - X = CatchPlayfield.CENTER_X, + OriginalX = 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 34964fc4ae..55e86a7be2 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Path = curveData.Path, NodeSamples = curveData.NodeSamples, RepeatCount = curveData.RepeatCount, - X = positionData?.X ?? 0, + OriginalX = positionData?.X ?? 0, NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0 @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Samples = obj.Samples, NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, - X = positionData?.X ?? 0 + OriginalX = positionData?.X ?? 0 }.Yield(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index e689d6a178..ebbbd44960 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -33,14 +33,6 @@ namespace osu.Game.Rulesets.Catch.Objects float IHasXPosition.X => OriginalX; - /// - /// An alias of setter. - /// - public float X - { - set => OriginalX = value; - } - public readonly Bindable EffectiveXBindable = new Bindable(); /// diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index 35fd58826e..bda0457c83 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new TinyDroplet { StartTime = t + lastEvent.Value.Time, - X = OriginalX + Path.PositionAt( + OriginalX = OriginalX + Path.PositionAt( lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X, }); } @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.Objects { Samples = dropletSamples, StartTime = e.Time, - X = OriginalX + Path.PositionAt(e.PathProgress).X, + OriginalX = OriginalX + Path.PositionAt(e.PathProgress).X, }); break; @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Catch.Objects { Samples = this.GetNodeSamples(nodeIndex++), StartTime = e.Time, - X = OriginalX + Path.PositionAt(e.PathProgress).X, + OriginalX = OriginalX + Path.PositionAt(e.PathProgress).X, }); break; } From 1794bfeddba4b8533d10f9854f06a0f5e9731cc7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 14 Dec 2020 13:07:55 +0900 Subject: [PATCH 5332/6909] Move offset into legacy mania judgement --- .../Legacy/LegacyManiaJudgementPiece.cs | 84 +++++++++++++++++++ .../Legacy/ManiaLegacySkinTransformer.cs | 3 +- .../UI/DrawableManiaJudgement.cs | 53 +++++------- osu.Game.Rulesets.Mania/UI/Stage.cs | 8 +- .../UI/Scrolling/ScrollingPlayfield.cs | 9 -- 5 files changed, 108 insertions(+), 49 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs new file mode 100644 index 0000000000..464d754205 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -0,0 +1,84 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Animations; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Scoring; +using osu.Game.Skinning; +using osuTK; + +namespace osu.Game.Rulesets.Mania.Skinning.Legacy +{ + public class LegacyManiaJudgementPiece : CompositeDrawable, IAnimatableJudgement + { + private readonly HitResult result; + private readonly Drawable animation; + + public LegacyManiaJudgementPiece(HitResult result, Drawable animation) + { + this.result = result; + this.animation = animation; + + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(ISkinSource skin) + { + float? scorePosition = skin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition)?.Value; + + if (scorePosition != null) + scorePosition -= Stage.HIT_TARGET_POSITION + 150; + + Y = scorePosition ?? 0; + + if (animation != null) + InternalChild = animation; + } + + public void PlayAnimation() + { + if (animation == null) + return; + + (animation as IFramedAnimation)?.GotoFrame(0); + + switch (result) + { + case HitResult.None: + break; + + case HitResult.Miss: + animation.ScaleTo(1.6f); + animation.ScaleTo(1, 100, Easing.In); + + animation.MoveTo(Vector2.Zero); + animation.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + + animation.RotateTo(0); + animation.RotateTo(40, 800, Easing.InQuint); + + this.FadeOutFromOne(800); + break; + + default: + animation.ScaleTo(0.8f); + animation.ScaleTo(1, 250, Easing.OutElastic); + + animation.Delay(50).ScaleTo(0.75f, 250); + + this.Delay(50).FadeOut(200); + break; + } + } + + public Drawable GetAboveHitObjectsProxiedContent() => null; + } +} diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs index 89f639e2fe..7e2a8823b6 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/ManiaLegacySkinTransformer.cs @@ -136,7 +136,8 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy string filename = this.GetManiaSkinConfig(hitresult_mapping[result])?.Value ?? default_hitresult_skin_filenames[result]; - return this.GetAnimation(filename, true, true); + var animation = this.GetAnimation(filename, true, true); + return animation == null ? null : new LegacyManiaJudgementPiece(result, animation); } public override SampleChannel GetSample(ISampleInfo sampleInfo) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs index a3dcd0e57f..34d972e60f 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaJudgement.cs @@ -5,7 +5,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Scoring; -using osuTK; namespace osu.Game.Rulesets.Mania.UI { @@ -20,37 +19,6 @@ namespace osu.Game.Rulesets.Mania.UI { } - protected override void ApplyMissAnimations() - { - if (!(JudgementBody.Drawable is DefaultManiaJudgementPiece)) - { - // this is temporary logic until mania's skin transformer returns IAnimatableJudgements - JudgementBody.ScaleTo(1.6f); - JudgementBody.ScaleTo(1, 100, Easing.In); - - JudgementBody.MoveTo(Vector2.Zero); - JudgementBody.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); - - JudgementBody.RotateTo(0); - JudgementBody.RotateTo(40, 800, Easing.InQuint); - JudgementBody.FadeOutFromOne(800); - - LifetimeEnd = JudgementBody.LatestTransformEndTime; - } - - base.ApplyMissAnimations(); - } - - protected override void ApplyHitAnimations() - { - JudgementBody.ScaleTo(0.8f); - JudgementBody.ScaleTo(1, 250, Easing.OutElastic); - - JudgementBody.Delay(50) - .ScaleTo(0.75f, 250) - .FadeOut(200); - } - protected override Drawable CreateDefaultJudgement(HitResult result) => new DefaultManiaJudgementPiece(result); private class DefaultManiaJudgementPiece : DefaultJudgementPiece @@ -66,6 +34,27 @@ namespace osu.Game.Rulesets.Mania.UI JudgementText.Font = JudgementText.Font.With(size: 25); } + + public override void PlayAnimation() + { + base.PlayAnimation(); + + switch (Result) + { + case HitResult.None: + case HitResult.Miss: + break; + + default: + this.ScaleTo(0.8f); + this.ScaleTo(1, 250, Easing.OutElastic); + + this.Delay(50) + .ScaleTo(0.75f, 250) + .FadeOut(200); + break; + } + } } } } diff --git a/osu.Game.Rulesets.Mania/UI/Stage.cs b/osu.Game.Rulesets.Mania/UI/Stage.cs index d2f5e6902a..dc34bffab1 100644 --- a/osu.Game.Rulesets.Mania/UI/Stage.cs +++ b/osu.Game.Rulesets.Mania/UI/Stage.cs @@ -10,7 +10,6 @@ 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; @@ -107,6 +106,7 @@ namespace osu.Game.Rulesets.Mania.UI Anchor = Anchor.TopCentre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, + Y = HIT_TARGET_POSITION + 150 }, topLevelContainer = new Container { RelativeSizeAxes = Axes.Both } } @@ -181,12 +181,6 @@ namespace osu.Game.Rulesets.Mania.UI })); } - protected override void OnSkinChanged() - { - judgements.Y = CurrentSkin.GetManiaSkinConfig(LegacyManiaSkinConfigurationLookups.ScorePosition) - ?.Value ?? HIT_TARGET_POSITION + 150; - } - protected override void Update() { // Due to masking differences, it is not possible to get the width of the columns container automatically diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 4f76198b9f..844a249769 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -20,20 +20,11 @@ namespace osu.Game.Rulesets.UI.Scrolling [Resolved] protected IScrollingInfo ScrollingInfo { get; private set; } - protected ISkinSource CurrentSkin { get; private set; } [BackgroundDependencyLoader] private void load(ISkinSource skin) { Direction.BindTo(ScrollingInfo.Direction); - CurrentSkin = skin; - - skin.SourceChanged += OnSkinChanged; - OnSkinChanged(); - } - - protected virtual void OnSkinChanged() - { } /// From 028909353c8dc5b027a05ae5fb995ee278db9ede Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 14 Dec 2020 13:15:52 +0900 Subject: [PATCH 5333/6909] Revert one more change --- osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 844a249769..475300c483 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.UI.Scrolling protected IScrollingInfo ScrollingInfo { get; private set; } [BackgroundDependencyLoader] - private void load(ISkinSource skin) + private void load() { Direction.BindTo(ScrollingInfo.Direction); } From d96399ea42130f3727d0188aa1ec8a0855c0b4b7 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Dec 2020 13:18:14 +0900 Subject: [PATCH 5334/6909] Revert "Remove X setter from CatchHitObject" This reverts commit 7cbbd74d --- .../Mods/TestSceneCatchModRelax.cs | 8 +++---- .../TestSceneAutoJuiceStream.cs | 2 +- .../TestSceneCatchModHidden.cs | 2 +- .../TestSceneCatchStacker.cs | 2 +- .../TestSceneCatcher.cs | 22 +++++++++---------- .../TestSceneCatcherArea.cs | 2 +- .../TestSceneDrawableHitObjects.cs | 6 ++--- .../TestSceneHyperDash.cs | 14 ++++++------ .../TestSceneJuiceStream.cs | 4 ++-- .../Beatmaps/CatchBeatmapConverter.cs | 4 ++-- .../Objects/CatchHitObject.cs | 8 +++++++ .../Objects/JuiceStream.cs | 6 ++--- 12 files changed, 44 insertions(+), 36 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs index da4834aa73..c01aff0aa0 100644 --- a/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs +++ b/osu.Game.Rulesets.Catch.Tests/Mods/TestSceneCatchModRelax.cs @@ -32,22 +32,22 @@ namespace osu.Game.Rulesets.Catch.Tests.Mods { new Fruit { - OriginalX = CatchPlayfield.CENTER_X, + X = CatchPlayfield.CENTER_X, StartTime = 0 }, new Fruit { - OriginalX = 0, + X = 0, StartTime = 1000 }, new Fruit { - OriginalX = CatchPlayfield.WIDTH, + X = CatchPlayfield.WIDTH, StartTime = 2000 }, new JuiceStream { - OriginalX = CatchPlayfield.CENTER_X, + X = CatchPlayfield.CENTER_X, StartTime = 3000, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, Vector2.UnitY * 200 }) } diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs index 45cf5095f6..f552c3c27b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneAutoJuiceStream.cs @@ -33,7 +33,7 @@ namespace osu.Game.Rulesets.Catch.Tests beatmap.HitObjects.Add(new JuiceStream { - OriginalX = CatchPlayfield.CENTER_X - width / 2, + X = CatchPlayfield.CENTER_X - width / 2, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs index 6af9c88088..f15da29993 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchModHidden.cs @@ -40,7 +40,7 @@ namespace osu.Game.Rulesets.Catch.Tests { StartTime = 1000, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, new Vector2(0, -192) }), - OriginalX = CatchPlayfield.WIDTH / 2 + X = CatchPlayfield.WIDTH / 2 } } }, diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs index d7835bd8c4..1ff31697b8 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatchStacker.cs @@ -26,7 +26,7 @@ namespace osu.Game.Rulesets.Catch.Tests { beatmap.HitObjects.Add(new Fruit { - OriginalX = (0.5f + i / 2048f * (i % 10 - 5)) * CatchPlayfield.WIDTH, + X = (0.5f + i / 2048f * (i % 10 - 5)) * CatchPlayfield.WIDTH, StartTime = i * 100, NewCombo = i % 8 == 0 }); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs index d57e8e027e..e8bb57cdf3 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcher.cs @@ -66,7 +66,7 @@ namespace osu.Game.Rulesets.Catch.Tests JudgementResult result2 = null; AddStep("catch hyper fruit", () => { - attemptCatch(new Fruit { HyperDashTarget = new Fruit { OriginalX = 100 } }, out drawableObject1, out result1); + attemptCatch(new Fruit { HyperDashTarget = new Fruit { X = 100 } }, out drawableObject1, out result1); }); AddStep("catch normal fruit", () => { @@ -107,14 +107,14 @@ namespace osu.Game.Rulesets.Catch.Tests var halfWidth = Catcher.CalculateCatchWidth(new BeatmapDifficulty { CircleSize = 0 }) / 2; AddStep("catch fruit", () => { - attemptCatch(new Fruit { OriginalX = -halfWidth + 1 }); - attemptCatch(new Fruit { OriginalX = halfWidth - 1 }); + attemptCatch(new Fruit { X = -halfWidth + 1 }); + attemptCatch(new Fruit { X = halfWidth - 1 }); }); checkPlate(2); AddStep("miss fruit", () => { - attemptCatch(new Fruit { OriginalX = -halfWidth - 1 }); - attemptCatch(new Fruit { OriginalX = halfWidth + 1 }); + attemptCatch(new Fruit { X = -halfWidth - 1 }); + attemptCatch(new Fruit { X = halfWidth + 1 }); }); checkPlate(2); } @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Catch.Tests [Test] public void TestFruitChangesCatcherState() { - AddStep("miss fruit", () => attemptCatch(new Fruit { OriginalX = 100 })); + AddStep("miss fruit", () => attemptCatch(new Fruit { X = 100 })); checkState(CatcherAnimationState.Fail); AddStep("catch fruit", () => attemptCatch(new Fruit())); checkState(CatcherAnimationState.Idle); @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Catch.Tests { AddStep("catch hyper fruit", () => attemptCatch(new Fruit { - HyperDashTarget = new Fruit { OriginalX = 100 } + HyperDashTarget = new Fruit { X = 100 } })); checkHyperDash(true); AddStep("catch normal fruit", () => attemptCatch(new Fruit())); @@ -147,10 +147,10 @@ namespace osu.Game.Rulesets.Catch.Tests { AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit { - HyperDashTarget = new Fruit { OriginalX = 100 } + HyperDashTarget = new Fruit { X = 100 } })); AddStep("catch tiny droplet", () => attemptCatch(new TinyDroplet())); - AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { OriginalX = 100 })); + AddStep("miss tiny droplet", () => attemptCatch(new TinyDroplet { X = 100 })); // catcher state and hyper dash state is preserved checkState(CatcherAnimationState.Kiai); checkHyperDash(true); @@ -161,9 +161,9 @@ namespace osu.Game.Rulesets.Catch.Tests { AddStep("catch hyper kiai fruit", () => attemptCatch(new TestKiaiFruit { - HyperDashTarget = new Fruit { OriginalX = 100 } + HyperDashTarget = new Fruit { X = 100 } })); - AddStep("miss banana", () => attemptCatch(new Banana { OriginalX = 100 })); + AddStep("miss banana", () => attemptCatch(new Banana { X = 100 })); // catcher state is preserved but hyper dash state is reset checkState(CatcherAnimationState.Kiai); checkHyperDash(false); diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 423c3b7a13..5079e57e5e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Catch.Tests AddStep("catch fruit", () => attemptCatch(new Fruit())); AddStep("catch fruit last in combo", () => attemptCatch(new Fruit { LastInCombo = true })); AddStep("catch kiai fruit", () => attemptCatch(new TestSceneCatcher.TestKiaiFruit())); - AddStep("miss last in combo", () => attemptCatch(new Fruit { OriginalX = 100, LastInCombo = true })); + AddStep("miss last in combo", () => attemptCatch(new Fruit { X = 100, LastInCombo = true })); } private void attemptCatch(Fruit fruit) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs index 2db534e8c9..3e4995482d 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneDrawableHitObjects.cs @@ -105,7 +105,7 @@ namespace osu.Game.Rulesets.Catch.Tests { var fruit = new Fruit { - OriginalX = getXCoords(hit), + X = getXCoords(hit), LastInCombo = i % 4 == 0, StartTime = playfieldTime + 800 + (200 * i) }; @@ -122,7 +122,7 @@ namespace osu.Game.Rulesets.Catch.Tests var juice = new JuiceStream { - OriginalX = xCoords, + X = xCoords, StartTime = playfieldTime + 1000, Path = new SliderPath(PathType.Linear, new[] { @@ -145,7 +145,7 @@ namespace osu.Game.Rulesets.Catch.Tests { var banana = new Banana { - OriginalX = getXCoords(hit), + X = getXCoords(hit), LastInCombo = i % 4 == 0, StartTime = playfieldTime + 800 + (200 * i) }; diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs index 67af3c4420..db09b2bc6b 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneHyperDash.cs @@ -70,20 +70,20 @@ namespace osu.Game.Rulesets.Catch.Tests beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); // Should produce a hyper-dash (edge case test) - beatmap.HitObjects.Add(new Fruit { StartTime = 1816, OriginalX = 56, NewCombo = true }); - beatmap.HitObjects.Add(new Fruit { StartTime = 2008, OriginalX = 308, 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 * CatchPlayfield.WIDTH; const float right_x = 0.98f * CatchPlayfield.WIDTH; - createObjects(() => new Fruit { OriginalX = left_x }); + createObjects(() => new Fruit { X = left_x }); createObjects(() => new TestJuiceStream(right_x), 1); createObjects(() => new TestJuiceStream(left_x), 1); - createObjects(() => new Fruit { OriginalX = right_x }); - createObjects(() => new Fruit { OriginalX = left_x }); - createObjects(() => new Fruit { OriginalX = right_x }); + createObjects(() => new Fruit { X = right_x }); + createObjects(() => new Fruit { X = left_x }); + createObjects(() => new Fruit { X = right_x }); createObjects(() => new TestJuiceStream(left_x), 1); beatmap.ControlPointInfo.Add(startTime, new TimingControlPoint @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Catch.Tests { public TestJuiceStream(float x) { - OriginalX = x; + X = x; Path = new SliderPath(new[] { diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs index dbcf382d62..269e783899 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneJuiceStream.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Catch.Tests { new JuiceStream { - OriginalX = CatchPlayfield.CENTER_X, + X = CatchPlayfield.CENTER_X, Path = new SliderPath(PathType.Linear, new[] { Vector2.Zero, @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Catch.Tests }, new Banana { - OriginalX = CatchPlayfield.CENTER_X, + 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 55e86a7be2..34964fc4ae 100644 --- a/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs +++ b/osu.Game.Rulesets.Catch/Beatmaps/CatchBeatmapConverter.cs @@ -36,7 +36,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Path = curveData.Path, NodeSamples = curveData.NodeSamples, RepeatCount = curveData.RepeatCount, - OriginalX = positionData?.X ?? 0, + X = positionData?.X ?? 0, NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, LegacyLastTickOffset = (obj as IHasLegacyLastTickOffset)?.LegacyLastTickOffset ?? 0 @@ -59,7 +59,7 @@ namespace osu.Game.Rulesets.Catch.Beatmaps Samples = obj.Samples, NewCombo = comboData?.NewCombo ?? false, ComboOffset = comboData?.ComboOffset ?? 0, - OriginalX = positionData?.X ?? 0 + X = positionData?.X ?? 0 }.Yield(); } } diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index ebbbd44960..e689d6a178 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -33,6 +33,14 @@ namespace osu.Game.Rulesets.Catch.Objects float IHasXPosition.X => OriginalX; + /// + /// An alias of setter. + /// + public float X + { + set => OriginalX = value; + } + public readonly Bindable EffectiveXBindable = new Bindable(); /// diff --git a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs index bda0457c83..35fd58826e 100644 --- a/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs +++ b/osu.Game.Rulesets.Catch/Objects/JuiceStream.cs @@ -75,7 +75,7 @@ namespace osu.Game.Rulesets.Catch.Objects AddNested(new TinyDroplet { StartTime = t + lastEvent.Value.Time, - OriginalX = OriginalX + Path.PositionAt( + X = OriginalX + Path.PositionAt( lastEvent.Value.PathProgress + (t / sinceLastTick) * (e.PathProgress - lastEvent.Value.PathProgress)).X, }); } @@ -93,7 +93,7 @@ namespace osu.Game.Rulesets.Catch.Objects { Samples = dropletSamples, StartTime = e.Time, - OriginalX = OriginalX + Path.PositionAt(e.PathProgress).X, + X = OriginalX + Path.PositionAt(e.PathProgress).X, }); break; @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Catch.Objects { Samples = this.GetNodeSamples(nodeIndex++), StartTime = e.Time, - OriginalX = OriginalX + Path.PositionAt(e.PathProgress).X, + X = OriginalX + Path.PositionAt(e.PathProgress).X, }); break; } From 0ad256a7626c6aa28c97f64ecc9bc9a1940c2f87 Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Dec 2020 13:18:32 +0900 Subject: [PATCH 5335/6909] Fix comment --- osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index e689d6a178..6267eca7de 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -51,7 +51,7 @@ namespace osu.Game.Rulesets.Catch.Objects private float xOffset; /// - /// A random offset applied to the horizontal value, set by the . + /// A random offset applied to the horizontal position, set by the . /// internal float XOffset { From 2e88e283d84c0547fd2f8272be87de6ccd8375d2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 14 Dec 2020 13:20:51 +0900 Subject: [PATCH 5336/6909] Remove unused using --- osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs index 475300c483..2b75f93f9e 100644 --- a/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs +++ b/osu.Game/Rulesets/UI/Scrolling/ScrollingPlayfield.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Drawables; -using osu.Game.Skinning; using osuTK; namespace osu.Game.Rulesets.UI.Scrolling From 5b5e883904298074b80671e87e0b88eca883127c Mon Sep 17 00:00:00 2001 From: ekrctb Date: Mon, 14 Dec 2020 13:39:07 +0900 Subject: [PATCH 5337/6909] Remove EffectiveXBindable (setting Value was not handled) And use orthogonal `OriginalXBindable` and `XOffsetBindable`. --- .../TestSceneCatcherArea.cs | 2 +- .../Objects/CatchHitObject.cs | 52 +++++++------------ .../Drawables/DrawableCatchHitObject.cs | 9 ++-- .../DrawablePalpableCatchHitObject.cs | 6 +-- 4 files changed, 29 insertions(+), 40 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs index 5079e57e5e..1cbfa6338e 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneCatcherArea.cs @@ -44,7 +44,7 @@ namespace osu.Game.Rulesets.Catch.Tests private void attemptCatch(Fruit fruit) { - fruit.OriginalX += catcher.X; + fruit.X = fruit.OriginalX + catcher.X; fruit.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize diff --git a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs index 6267eca7de..ae45182960 100644 --- a/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/CatchHitObject.cs @@ -4,7 +4,6 @@ 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; @@ -21,46 +20,40 @@ namespace osu.Game.Rulesets.Catch.Objects /// /// The horizontal position of the hit object between 0 and . /// - /// - /// This value is the original value specified in the beatmap, not affected by beatmap processing. - /// It should be used instead of when working on a beatmap, not a gameplay. - /// - public float OriginalX + public float X { - get => OriginalXBindable.Value; set => OriginalXBindable.Value = value; } - float IHasXPosition.X => OriginalX; + float IHasXPosition.X => OriginalXBindable.Value; + + public readonly Bindable XOffsetBindable = new Bindable(); /// - /// An alias of setter. + /// A random offset applied to the horizontal position, set by the beatmap processing. /// - public float X + public float XOffset { - set => OriginalX = value; + set => XOffsetBindable.Value = value; } - public readonly Bindable EffectiveXBindable = new Bindable(); + /// + /// The horizontal position of the hit object between 0 and . + /// + /// + /// This value is the original value specified in the beatmap, not affected by the beatmap processing. + /// Use for a gameplay. + /// + public float OriginalX => OriginalXBindable.Value; /// /// The effective horizontal position of the hit object between 0 and . /// - public float EffectiveX => EffectiveXBindable.Value; - - private float xOffset; - - /// - /// A random offset applied to the horizontal position, set by the . - /// - internal float XOffset - { - set - { - xOffset = value; - EffectiveXBindable.Value = OriginalX + xOffset; - } - } + /// + /// This value is the original value plus the offset applied by the beatmap processing. + /// Use if a value not affected by the offset is desired. + /// + public float EffectiveX => OriginalXBindable.Value + XOffsetBindable.Value; public double TimePreempt = 1000; @@ -127,10 +120,5 @@ namespace osu.Game.Rulesets.Catch.Objects } protected override HitWindows CreateHitWindows() => HitWindows.Empty; - - protected CatchHitObject() - { - OriginalXBindable.BindValueChanged(change => EffectiveXBindable.Value = change.NewValue + xOffset); - } } } diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs index edd607a443..0c065948ef 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawableCatchHitObject.cs @@ -15,7 +15,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { public abstract class DrawableCatchHitObject : DrawableHitObject { - public readonly Bindable EffectiveXBindable = new Bindable(); + public readonly Bindable OriginalXBindable = new Bindable(); + public readonly Bindable XOffsetBindable = new Bindable(); protected override double InitialLifetimeOffset => HitObject.TimePreempt; @@ -38,14 +39,16 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables { base.OnApply(); - EffectiveXBindable.BindTo(HitObject.EffectiveXBindable); + OriginalXBindable.BindTo(HitObject.OriginalXBindable); + XOffsetBindable.BindTo(HitObject.XOffsetBindable); } protected override void OnFree() { base.OnFree(); - EffectiveXBindable.UnbindFrom(HitObject.EffectiveXBindable); + OriginalXBindable.UnbindFrom(HitObject.OriginalXBindable); + XOffsetBindable.UnbindFrom(HitObject.XOffsetBindable); } public Func CheckPosition; diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index d3fa43c6b6..84af7922f9 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -55,10 +55,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - EffectiveXBindable.BindValueChanged(x => - { - X = x.NewValue; - }, true); + OriginalXBindable.BindValueChanged(_ => X = OriginalXBindable.Value + XOffsetBindable.Value); + XOffsetBindable.BindValueChanged(_ => X = OriginalXBindable.Value + XOffsetBindable.Value, true); ScaleBindable.BindValueChanged(scale => { From b81dbfc1921dabe9b846d752c7b4ca2a2a347c6d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 13:56:46 +0900 Subject: [PATCH 5338/6909] Move shared implementation to a named function --- .../Objects/Drawables/DrawablePalpableCatchHitObject.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs index 84af7922f9..27cd7ed2bc 100644 --- a/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs +++ b/osu.Game.Rulesets.Catch/Objects/Drawables/DrawablePalpableCatchHitObject.cs @@ -55,8 +55,8 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables [BackgroundDependencyLoader] private void load() { - OriginalXBindable.BindValueChanged(_ => X = OriginalXBindable.Value + XOffsetBindable.Value); - XOffsetBindable.BindValueChanged(_ => X = OriginalXBindable.Value + XOffsetBindable.Value, true); + OriginalXBindable.BindValueChanged(updateXPosition); + XOffsetBindable.BindValueChanged(updateXPosition, true); ScaleBindable.BindValueChanged(scale => { @@ -67,6 +67,11 @@ namespace osu.Game.Rulesets.Catch.Objects.Drawables IndexInBeatmap.BindValueChanged(_ => UpdateComboColour()); } + private void updateXPosition(ValueChangedEvent _) + { + X = OriginalXBindable.Value + XOffsetBindable.Value; + } + protected override void OnApply() { base.OnApply(); From a835ca9612e29fa71d66c852a0e4f5401bd3a465 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 14 Dec 2020 14:20:43 +0900 Subject: [PATCH 5339/6909] Fix anchors/origins for legacy pieces --- .../Skinning/Legacy/LegacyManiaJudgementPiece.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index 464d754205..9684cbb167 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -40,7 +40,13 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy Y = scorePosition ?? 0; if (animation != null) - InternalChild = animation; + { + InternalChild = animation.With(d => + { + d.Anchor = Anchor.Centre; + d.Origin = Anchor.Centre; + }); + } } public void PlayAnimation() From 0d7f53b0b9b8d83835ac16aaf32d032c15b68dc9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 14:21:21 +0900 Subject: [PATCH 5340/6909] Fix gameplay loading too fast the first time entering a beatmap --- osu.Game/Screens/Play/PlayerLoader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 729119fa36..f59b36bc42 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -189,7 +189,7 @@ namespace osu.Game.Screens.Play // after an initial delay, start the debounced load check. // this will continue to execute even after resuming back on restart. - Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, 1800, 0)); + Scheduler.Add(new ScheduledDelegate(pushWhenLoaded, Clock.CurrentTime + 1800, 0)); showMuteWarningIfNeeded(); } From 704150306324135bbb6ba4957627619fa294548c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 15:34:32 +0900 Subject: [PATCH 5341/6909] Avoid intermediary delegate --- .../Objects/Drawables/DrawableBarLine.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs index e7dd9a18c2..9e50faabc1 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs @@ -111,13 +111,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void LoadComplete() { base.LoadComplete(); - major.BindValueChanged(majorChanged => updateMajor(majorChanged.NewValue), true); + major.BindValueChanged(updateMajor); } - private void updateMajor(bool major) + private void updateMajor(ValueChangedEvent major) { - line.Alpha = major ? 1f : 0.75f; - triangleContainer.Alpha = major ? 1 : 0; + line.Alpha = major.NewValue ? 1f : 0.75f; + triangleContainer.Alpha = major.NewValue ? 1 : 0; } protected override void OnApply() From 51e8a05f181de7f34ef7b2f4290a3693069119e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 16:44:29 +0900 Subject: [PATCH 5342/6909] Seal SetRecordTarget method to simplify modification --- 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 c1a601eaae..b66a09aef1 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -268,7 +268,7 @@ namespace osu.Game.Rulesets.UI return false; } - public override void SetRecordTarget(Replay recordingReplay) + public sealed override void SetRecordTarget(Replay recordingReplay) { if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager)) throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available"); From 1793385e96270a9b07050845b37b14fde04fe5c5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 16:52:14 +0900 Subject: [PATCH 5343/6909] Pass a score to the replay recorder to allow reading more general scoring data --- osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs | 4 ++-- osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs | 3 ++- osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs | 3 ++- osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs | 6 +++--- osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs | 3 ++- osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs | 6 +++--- osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs | 3 ++- osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs | 6 +++--- .../Visual/Gameplay/TestSceneReplayRecorder.cs | 5 +++-- .../Visual/Gameplay/TestSceneReplayRecording.cs | 5 +++-- .../Visual/Gameplay/TestSceneSpectatorPlayback.cs | 3 ++- osu.Game/Rulesets/UI/DrawableRuleset.cs | 10 +++++----- osu.Game/Rulesets/UI/ReplayRecorder.cs | 10 +++++----- osu.Game/Screens/Play/Player.cs | 8 ++++---- 14 files changed, 41 insertions(+), 34 deletions(-) diff --git a/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs b/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs index 9a4d1f9585..1ddb5ac630 100644 --- a/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs +++ b/osu.Game.Rulesets.Catch/UI/CatchReplayRecorder.cs @@ -2,10 +2,10 @@ // 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 osu.Game.Scoring; using osuTK; namespace osu.Game.Rulesets.Catch.UI @@ -14,7 +14,7 @@ namespace osu.Game.Rulesets.Catch.UI { private readonly CatchPlayfield playfield; - public CatchReplayRecorder(Replay target, CatchPlayfield playfield) + public CatchReplayRecorder(Score target, CatchPlayfield playfield) : base(target) { this.playfield = playfield; diff --git a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs index 46733181e3..9389fa803b 100644 --- a/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/UI/DrawableCatchRuleset.cs @@ -13,6 +13,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.Scoring; namespace osu.Game.Rulesets.Catch.UI { @@ -31,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, (CatchPlayfield)Playfield); + protected override ReplayRecorder CreateReplayRecorder(Score score) => new CatchReplayRecorder(score, (CatchPlayfield)Playfield); 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 7f5b9a6ee0..941ac9816c 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -23,6 +23,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.Scoring; namespace osu.Game.Rulesets.Mania.UI { @@ -132,6 +133,6 @@ namespace osu.Game.Rulesets.Mania.UI protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new ManiaFramedReplayInputHandler(replay); - protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new ManiaReplayRecorder(replay); + protected override ReplayRecorder CreateReplayRecorder(Score score) => new ManiaReplayRecorder(score); } } diff --git a/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs index 18275000a2..b502d1f9e5 100644 --- a/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs +++ b/osu.Game.Rulesets.Mania/UI/ManiaReplayRecorder.cs @@ -2,18 +2,18 @@ // 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 osu.Game.Scoring; using osuTK; namespace osu.Game.Rulesets.Mania.UI { public class ManiaReplayRecorder : ReplayRecorder { - public ManiaReplayRecorder(Replay replay) - : base(replay) + public ManiaReplayRecorder(Score score) + : base(score) { } diff --git a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs index 69179137a6..df3f7c64e4 100644 --- a/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/UI/DrawableOsuRuleset.cs @@ -14,6 +14,7 @@ using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Play; using osuTK; @@ -44,7 +45,7 @@ namespace osu.Game.Rulesets.Osu.UI protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new OsuFramedReplayInputHandler(replay); - protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new OsuReplayRecorder(replay); + protected override ReplayRecorder CreateReplayRecorder(Score score) => new OsuReplayRecorder(score); public override double GameplayStartTime { diff --git a/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs b/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs index b68ea136d5..1304dfe416 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuReplayRecorder.cs @@ -2,18 +2,18 @@ // 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 osu.Game.Scoring; using osuTK; namespace osu.Game.Rulesets.Osu.UI { public class OsuReplayRecorder : ReplayRecorder { - public OsuReplayRecorder(Replay replay) - : base(replay) + public OsuReplayRecorder(Score score) + : base(score) { } diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index bbf8cb8de0..9cf931ee0a 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -17,6 +17,7 @@ using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Scoring; using osu.Game.Skinning; using osuTK; @@ -82,6 +83,6 @@ namespace osu.Game.Rulesets.Taiko.UI protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); - protected override ReplayRecorder CreateReplayRecorder(Replay replay) => new TaikoReplayRecorder(replay); + protected override ReplayRecorder CreateReplayRecorder(Score score) => new TaikoReplayRecorder(score); } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs b/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs index 1859dabf03..e6391d1386 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoReplayRecorder.cs @@ -2,18 +2,18 @@ // 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 osu.Game.Scoring; using osuTK; namespace osu.Game.Rulesets.Taiko.UI { public class TaikoReplayRecorder : ReplayRecorder { - public TaikoReplayRecorder(Replay replay) - : base(replay) + public TaikoReplayRecorder(Score score) + : base(score) { } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index b72960931f..b2ad7ca5b4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -19,6 +19,7 @@ using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Visual.UserInterface; using osuTK; @@ -53,7 +54,7 @@ namespace osu.Game.Tests.Visual.Gameplay { recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Recorder = recorder = new TestReplayRecorder(replay) + Recorder = recorder = new TestReplayRecorder(new Score { Replay = replay }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos), }, @@ -271,7 +272,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { - public TestReplayRecorder(Replay target) + public TestReplayRecorder(Score target) : base(target) { } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index 6872b6a669..40c4214749 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -15,6 +15,7 @@ using osu.Game.Replays; using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Visual.UserInterface; using osuTK; @@ -44,7 +45,7 @@ namespace osu.Game.Tests.Visual.Gameplay { recordingManager = new TestRulesetInputManager(new TestSceneModSettings.TestRulesetInfo(), 0, SimultaneousBindingMode.Unique) { - Recorder = new TestReplayRecorder(replay) + Recorder = new TestReplayRecorder(new Score { Replay = replay }) { ScreenSpaceToGamefield = pos => recordingManager.ToLocalSpace(pos) }, @@ -206,7 +207,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { - public TestReplayRecorder(Replay target) + public TestReplayRecorder(Score target) : base(target) { } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 35473ee76c..e148fa381c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -27,6 +27,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Visual.UserInterface; using osuTK; @@ -348,7 +349,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestReplayRecorder : ReplayRecorder { public TestReplayRecorder() - : base(new Replay()) + : base(new Score()) { } diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index b66a09aef1..6940e43e5b 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -268,12 +268,12 @@ namespace osu.Game.Rulesets.UI return false; } - public sealed override void SetRecordTarget(Replay recordingReplay) + public sealed override void SetRecordTarget(Score score) { if (!(KeyBindingInputManager is IHasRecordingHandler recordingInputManager)) throw new InvalidOperationException($"A {nameof(KeyBindingInputManager)} which supports recording is not available"); - var recorder = CreateReplayRecorder(recordingReplay); + var recorder = CreateReplayRecorder(score); if (recorder == null) return; @@ -327,7 +327,7 @@ namespace osu.Game.Rulesets.UI protected virtual ReplayInputHandler CreateReplayInputHandler(Replay replay) => null; - protected virtual ReplayRecorder CreateReplayRecorder(Replay replay) => null; + protected virtual ReplayRecorder CreateReplayRecorder(Score score) => null; /// /// Creates a Playfield. @@ -516,8 +516,8 @@ namespace osu.Game.Rulesets.UI /// /// Sets a replay to be used to record gameplay. /// - /// The target to be recorded to. - public abstract void SetRecordTarget(Replay recordingReplay); + /// The target to be recorded to. + public abstract void SetRecordTarget(Score score); /// /// Invoked when the interactive user requests resuming from a paused state. diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 1438ebd37a..2918a3b445 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -10,8 +10,8 @@ using osu.Framework.Input; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; using osu.Game.Online.Spectator; -using osu.Game.Replays; using osu.Game.Rulesets.Replays; +using osu.Game.Scoring; using osu.Game.Screens.Play; using osuTK; @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.UI public abstract class ReplayRecorder : ReplayRecorder, IKeyBindingHandler where T : struct { - private readonly Replay target; + private readonly Score target; private readonly List pressedActions = new List(); @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.UI [Resolved] private GameplayBeatmap gameplayBeatmap { get; set; } - protected ReplayRecorder(Replay target) + protected ReplayRecorder(Score target) { this.target = target; @@ -79,7 +79,7 @@ namespace osu.Game.Rulesets.UI private void recordFrame(bool important) { - var last = target.Frames.LastOrDefault(); + var last = target.Replay.Frames.LastOrDefault(); if (!important && last != null && Time.Current - last.Time < (1000d / RecordFrameRate)) return; @@ -90,7 +90,7 @@ namespace osu.Game.Rulesets.UI if (frame != null) { - target.Frames.Add(frame); + target.Replay.Frames.Add(frame); spectatorStreaming?.HandleFrame(frame); } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 7979b635aa..f7491ddfba 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -157,14 +157,14 @@ namespace osu.Game.Screens.Play PrepareReplay(); } - private Replay recordingReplay; + private Score recordingScore; /// /// Run any recording / playback setup for replays. /// protected virtual void PrepareReplay() { - DrawableRuleset.SetRecordTarget(recordingReplay = new Replay()); + DrawableRuleset.SetRecordTarget(recordingScore = new Score { Replay = new Replay() }); } [BackgroundDependencyLoader(true)] @@ -758,9 +758,9 @@ namespace osu.Game.Screens.Play var score = new Score { ScoreInfo = CreateScore() }; - if (recordingReplay?.Frames.Count > 0) + if (recordingScore?.Replay.Frames.Count > 0) { - score.Replay = recordingReplay; + score.Replay = recordingScore.Replay; using (var stream = new MemoryStream()) { From 64a2526678c07acd9b568ae11ff682f197306717 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 17:33:23 +0900 Subject: [PATCH 5344/6909] Add header class and basic flow for propagating data updates --- .../Visual/Gameplay/TestSceneSpectator.cs | 3 +- osu.Game/Online/Spectator/FrameDataBundle.cs | 8 ++++- osu.Game/Online/Spectator/FrameHeader.cs | 35 +++++++++++++++++++ .../Spectator/SpectatorStreamingClient.cs | 12 +++++-- osu.Game/Rulesets/UI/ReplayRecorder.cs | 2 +- 5 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Online/Spectator/FrameHeader.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 72c6fd8d44..3e5b561a6f 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -17,6 +17,7 @@ using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Replays; using osu.Game.Rulesets.UI; +using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Tests.Beatmaps.IO; using osu.Game.Users; @@ -272,7 +273,7 @@ namespace osu.Game.Tests.Visual.Gameplay frames.Add(new LegacyReplayFrame(i * 100, RNG.Next(0, 512), RNG.Next(0, 512), buttonState)); } - var bundle = new FrameDataBundle(frames); + var bundle = new FrameDataBundle(new ScoreInfo(), frames); ((ISpectatorClient)this).UserSentFrames(StreamingUser.Id, bundle); if (!sentState) diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs index 5281e61f9c..fecf88de22 100644 --- a/osu.Game/Online/Spectator/FrameDataBundle.cs +++ b/osu.Game/Online/Spectator/FrameDataBundle.cs @@ -1,20 +1,26 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + using System; using System.Collections.Generic; using osu.Game.Replays.Legacy; +using osu.Game.Scoring; namespace osu.Game.Online.Spectator { [Serializable] public class FrameDataBundle { + public FrameHeader Header { get; set; } + public IEnumerable Frames { get; set; } - public FrameDataBundle(IEnumerable frames) + public FrameDataBundle(ScoreInfo score, IEnumerable frames) { Frames = frames; + Header = new FrameHeader(score); } } } diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs new file mode 100644 index 0000000000..0940eefa40 --- /dev/null +++ b/osu.Game/Online/Spectator/FrameHeader.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. + +#nullable enable + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; + +namespace osu.Game.Online.Spectator +{ + [Serializable] + public class FrameHeader + { + public int Combo { get; set; } + + public int MaxCombo { get; set; } + + public Dictionary Statistics = new Dictionary(); + + /// + /// Construct header summary information from a point-in-time reference to a score which is actively being played. + /// + /// The score for reference. + public FrameHeader(ScoreInfo score) + { + Combo = score.Combo; + MaxCombo = score.MaxCombo; + + foreach (var kvp in score.Statistics) + Statistics[kvp.Key] = kvp.Value; + } + } +} diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 08b524087a..0167a5d025 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; +using osu.Game.Scoring; using osu.Game.Screens.Play; namespace osu.Game.Online.Spectator @@ -52,6 +53,9 @@ namespace osu.Game.Online.Spectator [CanBeNull] private IBeatmap currentBeatmap; + [CanBeNull] + private Score currentScore; + [Resolved] private IBindable currentRuleset { get; set; } @@ -203,7 +207,7 @@ namespace osu.Game.Online.Spectator return Task.CompletedTask; } - public void BeginPlaying(GameplayBeatmap beatmap) + public void BeginPlaying(GameplayBeatmap beatmap, Score score) { if (isPlaying) throw new InvalidOperationException($"Cannot invoke {nameof(BeginPlaying)} when already playing"); @@ -216,6 +220,8 @@ namespace osu.Game.Online.Spectator currentState.Mods = currentMods.Value.Select(m => new APIMod(m)); currentBeatmap = beatmap.PlayableBeatmap; + currentScore = score; + beginPlaying(); } @@ -308,7 +314,9 @@ namespace osu.Game.Online.Spectator pendingFrames.Clear(); - SendFrames(new FrameDataBundle(frames)); + Debug.Assert(currentScore != null); + + SendFrames(new FrameDataBundle(currentScore.ScoreInfo, frames)); lastSendTime = Time.Current; } diff --git a/osu.Game/Rulesets/UI/ReplayRecorder.cs b/osu.Game/Rulesets/UI/ReplayRecorder.cs index 2918a3b445..a4d46e3888 100644 --- a/osu.Game/Rulesets/UI/ReplayRecorder.cs +++ b/osu.Game/Rulesets/UI/ReplayRecorder.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.UI inputManager = GetContainingInputManager(); - spectatorStreaming?.BeginPlaying(gameplayBeatmap); + spectatorStreaming?.BeginPlaying(gameplayBeatmap, target); } protected override void Dispose(bool isDisposing) From ae22f75406970fb5521ac70ad756ae378be59d92 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 17:33:33 +0900 Subject: [PATCH 5345/6909] Bind replay recording score to judgement changes --- osu.Game/Screens/Play/Player.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f7491ddfba..f40a7ccda8 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -165,6 +165,8 @@ namespace osu.Game.Screens.Play protected virtual void PrepareReplay() { DrawableRuleset.SetRecordTarget(recordingScore = new Score { Replay = new Replay() }); + + ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(recordingScore.ScoreInfo); } [BackgroundDependencyLoader(true)] From 79768f0aa412f00fa6d8e45101038cd8417e2c78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 17:52:38 +0900 Subject: [PATCH 5346/6909] 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 eaedcb7bc3..2a08cb7867 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 b4c7dca12f..960959f367 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 7542aded86..a5bcb91c74 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From f3e6c586f7765c2ade6bf446e0605f792ef49bf9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 17:59:04 +0900 Subject: [PATCH 5347/6909] Change waitForReady back to private implementation --- osu.Game/OsuGame.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 6c32e2e94c..888fd8c803 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -268,7 +268,7 @@ namespace osu.Game case LinkAction.OpenEditorTimestamp: case LinkAction.JoinMultiplayerMatch: case LinkAction.Spectate: - WaitForReady(() => notifications, _ => notifications.Post(new SimpleNotification + waitForReady(() => notifications, _ => notifications.Post(new SimpleNotification { Text = @"This link type is not yet supported!", Icon = FontAwesome.Solid.LifeRing, @@ -289,7 +289,7 @@ namespace osu.Game } }); - public void OpenUrlExternally(string url) => WaitForReady(() => externalLinkOpener, _ => + public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ => { if (url.StartsWith('/')) url = $"{API.Endpoint}{url}"; @@ -301,7 +301,7 @@ namespace osu.Game /// Open a specific channel in chat. /// /// The channel to display. - public void ShowChannel(string channel) => WaitForReady(() => channelManager, _ => + public void ShowChannel(string channel) => waitForReady(() => channelManager, _ => { try { @@ -317,19 +317,19 @@ namespace osu.Game /// Show a beatmap set as an overlay. /// /// The set to display. - public void ShowBeatmapSet(int setId) => WaitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmapSet(setId)); + public void ShowBeatmapSet(int setId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmapSet(setId)); /// /// Show a user's profile as an overlay. /// /// The user to display. - public void ShowUser(int userId) => WaitForReady(() => userProfile, _ => userProfile.ShowUser(userId)); + public void ShowUser(int userId) => waitForReady(() => userProfile, _ => userProfile.ShowUser(userId)); /// /// Show a beatmap's set as an overlay, displaying the given beatmap. /// /// The beatmap to show. - public void ShowBeatmap(int beatmapId) => WaitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId)); + public void ShowBeatmap(int beatmapId) => waitForReady(() => beatmapSetOverlay, _ => beatmapSetOverlay.FetchAndShowBeatmap(beatmapId)); /// /// Present a beatmap at song select immediately. @@ -430,7 +430,7 @@ namespace osu.Game public override Task Import(Stream stream, string filename) { var importTask = new Task(async () => await base.Import(stream, filename)); - WaitForReady(() => this, _ => importTask.Start()); + waitForReady(() => this, _ => importTask.Start()); return importTask; } @@ -491,13 +491,13 @@ namespace osu.Game /// A function to retrieve a (potentially not-yet-constructed) target instance. /// The action to perform on the instance when load is confirmed. /// The type of the target instance. - protected void WaitForReady(Func retrieveInstance, Action action) + private void waitForReady(Func retrieveInstance, Action action) where T : Drawable { var instance = retrieveInstance(); if (ScreenStack == null || ScreenStack.CurrentScreen is StartupScreen || instance?.IsLoaded != true) - Schedule(() => WaitForReady(retrieveInstance, action)); + Schedule(() => waitForReady(retrieveInstance, action)); else action(instance); } From c5112edd08b5fea1688bbc4e339536b725f8cd7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 18:03:01 +0900 Subject: [PATCH 5348/6909] Add comment regarding the reasoning for encapsulating the task in another --- osu.Game/OsuGame.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 888fd8c803..d67d790ce2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -429,8 +429,11 @@ namespace osu.Game public override Task Import(Stream stream, string filename) { + // encapsulate task as we don't want to begin the import process until in a ready state. var importTask = new Task(async () => await base.Import(stream, filename)); + waitForReady(() => this, _ => importTask.Start()); + return importTask; } From 7bf04808485e78ea96c85176dcfdf8e3360cc89b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 18:12:23 +0900 Subject: [PATCH 5349/6909] Tidy up android-side code quality --- osu.Android/OsuGameActivity.cs | 31 +++++++++++++++++++++---------- 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index eb9df24bf7..9798d669d6 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -38,27 +38,38 @@ namespace osu.Android protected override void OnNewIntent(Intent intent) { - if (intent.Action == Intent.ActionDefault && intent.Scheme == ContentResolver.SchemeContent) + switch (intent.Action) { - handleImportFromUri(intent.Data); - } + case Intent.ActionDefault: + if (intent.Scheme == ContentResolver.SchemeContent) + handleImportFromUri(intent.Data); + break; - if (intent.Action == Intent.ActionSend) - { - var content = intent.ClipData.GetItemAt(0); - handleImportFromUri(content.Uri); + case Intent.ActionSend: + { + var content = intent.ClipData?.GetItemAt(0); + if (content != null) + handleImportFromUri(content.Uri); + break; + } } } private void handleImportFromUri(Uri uri) { - var cursor = ContentResolver.Query(uri, new[] { OpenableColumns.DisplayName }, null, null); - var filename_column = cursor.GetColumnIndex(OpenableColumns.DisplayName); + var cursor = ContentResolver?.Query(uri, new[] { OpenableColumns.DisplayName }, null, null); + + if (cursor == null) + return; + cursor.MoveToFirst(); + var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); + var stream = ContentResolver.OpenInputStream(uri); + if (stream != null) - Task.Factory.StartNew(() => game.Import(stream, cursor.GetString(filename_column)), TaskCreationOptions.LongRunning); + Task.Factory.StartNew(() => game.Import(stream, cursor.GetString(filenameColumn)), TaskCreationOptions.LongRunning); } } } From 38e0b4e64dd3fbf26b3e39364ef0d968f66c193f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 18:13:32 +0900 Subject: [PATCH 5350/6909] Remove unused using statements --- osu.Android/OsuGameAndroid.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Android/OsuGameAndroid.cs b/osu.Android/OsuGameAndroid.cs index 57512012f9..21d6336b2c 100644 --- a/osu.Android/OsuGameAndroid.cs +++ b/osu.Android/OsuGameAndroid.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.IO; -using System.Threading.Tasks; using Android.App; using Android.OS; using osu.Framework.Allocation; From 0d9c1cb5d338fc77cbd13b1f1f27efe0450102c6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 14 Dec 2020 18:41:24 +0900 Subject: [PATCH 5351/6909] Fix issues with data serialisation --- osu.Game/Online/Spectator/FrameDataBundle.cs | 8 +++++++ osu.Game/Online/Spectator/FrameHeader.cs | 17 ++++++++++----- osu.Game/Online/Spectator/StatisticPair.cs | 23 ++++++++++++++++++++ 3 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Online/Spectator/StatisticPair.cs diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs index fecf88de22..a8d0434324 100644 --- a/osu.Game/Online/Spectator/FrameDataBundle.cs +++ b/osu.Game/Online/Spectator/FrameDataBundle.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using Newtonsoft.Json; using osu.Game.Replays.Legacy; using osu.Game.Scoring; @@ -22,5 +23,12 @@ namespace osu.Game.Online.Spectator Frames = frames; Header = new FrameHeader(score); } + + [JsonConstructor] + public FrameDataBundle(FrameHeader header, IEnumerable frames) + { + Header = header; + Frames = frames; + } } } diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs index 0940eefa40..9b6cc615a4 100644 --- a/osu.Game/Online/Spectator/FrameHeader.cs +++ b/osu.Game/Online/Spectator/FrameHeader.cs @@ -4,8 +4,8 @@ #nullable enable using System; -using System.Collections.Generic; -using osu.Game.Rulesets.Scoring; +using System.Linq; +using Newtonsoft.Json; using osu.Game.Scoring; namespace osu.Game.Online.Spectator @@ -17,7 +17,7 @@ namespace osu.Game.Online.Spectator public int MaxCombo { get; set; } - public Dictionary Statistics = new Dictionary(); + public StatisticPair[] Statistics { get; set; } /// /// Construct header summary information from a point-in-time reference to a score which is actively being played. @@ -28,8 +28,15 @@ namespace osu.Game.Online.Spectator Combo = score.Combo; MaxCombo = score.MaxCombo; - foreach (var kvp in score.Statistics) - Statistics[kvp.Key] = kvp.Value; + Statistics = score.Statistics.Select(kvp => new StatisticPair(kvp.Key, kvp.Value)).ToArray(); + } + + [JsonConstructor] + public FrameHeader(int combo, int maxCombo, StatisticPair[] statistics) + { + Combo = combo; + MaxCombo = maxCombo; + Statistics = statistics; } } } diff --git a/osu.Game/Online/Spectator/StatisticPair.cs b/osu.Game/Online/Spectator/StatisticPair.cs new file mode 100644 index 0000000000..793143a64c --- /dev/null +++ b/osu.Game/Online/Spectator/StatisticPair.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 osu.Game.Rulesets.Scoring; + +namespace osu.Game.Online.Spectator +{ + [Serializable] + public struct StatisticPair + { + public HitResult Result; + public int Count; + + public StatisticPair(HitResult result, int count) + { + Result = result; + Count = count; + } + + public override string ToString() => $"{Result}=>{Count}"; + } +} From 54827d4e96c82beec3d8eccd650b074623ee775d Mon Sep 17 00:00:00 2001 From: Xexxar Date: Mon, 14 Dec 2020 12:41:24 -0600 Subject: [PATCH 5352/6909] fixed low 50s count still penalizing high obj count maps --- osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs index 60df173223..af7786c3fd 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -159,7 +159,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty // Scale the speed value with accuracy and OD speedValue *= (.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); // Scale the speed value with # of 50s to punish doubletapping. - speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0.5 * countMeh : countMeh - totalHits / 500.0 * 0.5); + speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); return speedValue; } From a323c5ce580bee6ed909ee3bb84adc5baf5a9efe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 20:01:28 +0100 Subject: [PATCH 5353/6909] Use most backwards-compatible overload for query --- osu.Android/OsuGameActivity.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 9798d669d6..fe9b292389 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -57,7 +57,9 @@ namespace osu.Android private void handleImportFromUri(Uri uri) { - var cursor = ContentResolver?.Query(uri, new[] { OpenableColumns.DisplayName }, null, null); + // there are more performant overloads of this method, but this one is the most backwards-compatible + // (dates back to API 1). + var cursor = ContentResolver?.Query(uri, null, null, null, null); if (cursor == null) return; From 1f6e5f4d329934d06b5423481806e1fc8c78ddb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 20:11:53 +0100 Subject: [PATCH 5354/6909] Copy archive contents to memory stream --- osu.Android/OsuGameActivity.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index fe9b292389..f531b79d92 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.IO; using System.Threading.Tasks; using Android.App; using Android.Content; @@ -69,9 +70,21 @@ namespace osu.Android var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); var stream = ContentResolver.OpenInputStream(uri); + string filename = cursor.GetString(filenameColumn); if (stream != null) - Task.Factory.StartNew(() => game.Import(stream, cursor.GetString(filenameColumn)), TaskCreationOptions.LongRunning); + Task.Factory.StartNew(() => runImport(stream, filename), TaskCreationOptions.LongRunning); + } + + private Task runImport(Stream stream, string filename) + { + // SharpCompress requires archive streams to be seekable, which the stream opened by + // OpenInputStream() seems to not necessarily be. + // copy to an arbitrary-access memory stream to be able to proceed with the import. + var copy = new MemoryStream(); + stream.CopyTo(copy); + + return game.Import(copy, filename); } } } From 3e3be56e468d5bdf8d6a3042d7d21159d7b7e538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 20:23:49 +0100 Subject: [PATCH 5355/6909] Touch up and clarify intent handling --- osu.Android/OsuGameActivity.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index f531b79d92..28c9433095 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.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.IO; using System.Threading.Tasks; using Android.App; @@ -31,13 +32,18 @@ namespace osu.Android base.OnCreate(savedInstanceState); - OnNewIntent(Intent); + // OnNewIntent() only fires for an activity if it's *re-launched* while it's on top of the activity stack. + // on first launch we still have to fire manually. + // reference: https://developer.android.com/reference/android/app/Activity#onNewIntent(android.content.Intent) + handleIntent(Intent); Window.AddFlags(WindowManagerFlags.Fullscreen); Window.AddFlags(WindowManagerFlags.KeepScreenOn); } - protected override void OnNewIntent(Intent intent) + protected override void OnNewIntent(Intent intent) => handleIntent(intent); + + private void handleIntent(Intent intent) { switch (intent.Action) { From f9d7945a6f96b9b7bf19c301a251372e6c3bb681 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 21:03:49 +0100 Subject: [PATCH 5356/6909] Remove non-functional replay import for now --- 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 28c9433095..cf0179e2ac 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -16,7 +16,7 @@ using osu.Framework.Android; namespace osu.Android { [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] - [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk", ".*\\.osr" }, DataMimeType = "application/*")] + [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] public class OsuGameActivity : AndroidGameActivity { private OsuGameAndroid game; From 5af1ac1b537bfce3b993692dbdff1e01a4de7278 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 21:46:02 +0100 Subject: [PATCH 5357/6909] Rename TaikoStrong{-> able}HitObject --- osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs | 2 +- osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs | 2 +- osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs | 2 +- .../Objects/Drawables/DrawableTaikoStrongHitObject.cs | 6 +++--- osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Hit.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs | 2 +- ...{TaikoStrongHitObject.cs => TaikoStrongableHitObject.cs} | 2 +- .../Skinning/Legacy/LegacyCirclePiece.cs | 2 +- osu.Game.Rulesets.Taiko/UI/HitExplosion.cs | 2 +- osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs | 6 +++--- 12 files changed, 16 insertions(+), 16 deletions(-) rename osu.Game.Rulesets.Taiko/Objects/{TaikoStrongHitObject.cs => TaikoStrongableHitObject.cs} (96%) diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index 126af1a6ad..3d77fb05db 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Tests IsCentre = (hitObject as Hit)?.Type == HitType.Centre, IsDrumRoll = hitObject is DrumRoll, IsSwell = hitObject is Swell, - IsStrong = (hitObject as TaikoStrongHitObject)?.IsStrong == true + IsStrong = (hitObject as TaikoStrongableHitObject)?.IsStrong == true }; } diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 78a0d1fdff..1214c594aa 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -65,7 +65,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps converted.HitObjects = converted.HitObjects.GroupBy(t => t.StartTime).Select(x => { TaikoHitObject first = x.First(); - if (x.Skip(1).Any() && first is TaikoStrongHitObject strong) + if (x.Skip(1).Any() && first is TaikoStrongableHitObject strong) strong.IsStrong = true; return first; }).ToList(); diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index 7de920ca8c..3fbcee44af 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Edit base.UpdateTernaryStates(); selectionRimState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType(), h => h.Type == HitType.Rim); - selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType(), h => h.IsStrong); + selectionStrongState.Value = GetStateFromSelection(EditorBeatmap.SelectedHitObjects.OfType(), h => h.IsStrong); } } } diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs index 04e584e15d..614c12165e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs @@ -13,7 +13,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public abstract class DrawableTaikoStrongHitObject : DrawableTaikoHitObject - where TObject : TaikoStrongHitObject + where TObject : TaikoStrongableHitObject where TStrongNestedObject : StrongNestedHitObject { private readonly Bindable isStrong; @@ -68,7 +68,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { base.RecreatePieces(); if (HitObject.IsStrong) - Size = BaseSize = new Vector2(TaikoStrongHitObject.DEFAULT_STRONG_SIZE); + Size = BaseSize = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE); } protected override void AddNestedHitObject(DrawableHitObject hitObject) @@ -102,7 +102,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables /// /// Creates the handler for this 's . - /// This is only invoked if is true for . + /// This is only invoked if is true for . /// /// The strong hitobject. /// The strong hitobject handler. diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs index 93f95d6446..c0377c67a5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRoll.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRoll : TaikoStrongHitObject, IHasPath + public class DrumRoll : TaikoStrongableHitObject, IHasPath { /// /// Drum roll distance that results in a duration of 1 speed-adjusted beat length. diff --git a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs index 6b6ffa8668..9d0336441e 100644 --- a/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/DrumRollTick.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects { - public class DrumRollTick : TaikoStrongHitObject + public class DrumRollTick : TaikoStrongableHitObject { /// /// Whether this is the first (initial) tick of the slider. diff --git a/osu.Game.Rulesets.Taiko/Objects/Hit.cs b/osu.Game.Rulesets.Taiko/Objects/Hit.cs index 6bdde376f6..1b51288605 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Hit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Hit.cs @@ -5,7 +5,7 @@ using osu.Framework.Bindables; namespace osu.Game.Rulesets.Taiko.Objects { - public class Hit : TaikoStrongHitObject + public class Hit : TaikoStrongableHitObject { public readonly Bindable TypeBindable = new Bindable(); diff --git a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs index 2e8989bc79..3b427e48c5 100644 --- a/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/StrongNestedHitObject.cs @@ -9,7 +9,7 @@ namespace osu.Game.Rulesets.Taiko.Objects { /// /// Base type for nested strong hits. - /// Used by s to represent their strong bonus scoring portions. + /// Used by s to represent their strong bonus scoring portions. /// public abstract class StrongNestedHitObject : TaikoHitObject { diff --git a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs similarity index 96% rename from osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs rename to osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs index 861de95bae..fcd055bcec 100644 --- a/osu.Game.Rulesets.Taiko/Objects/TaikoStrongHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/TaikoStrongableHitObject.cs @@ -10,7 +10,7 @@ namespace osu.Game.Rulesets.Taiko.Objects /// /// Base class for taiko hitobjects that can become strong (large). /// - public abstract class TaikoStrongHitObject : TaikoHitObject + public abstract class TaikoStrongableHitObject : TaikoHitObject { /// /// Scale multiplier for a strong drawable taiko hit object. diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs index 52c9080633..2b6c14ca63 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/LegacyCirclePiece.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy const string normal_hit = "taikohit"; const string big_hit = "taikobig"; - string prefix = ((drawableHitObject.HitObject as TaikoStrongHitObject)?.IsStrong ?? false) ? big_hit : normal_hit; + string prefix = ((drawableHitObject.HitObject as TaikoStrongableHitObject)?.IsStrong ?? false) ? big_hit : normal_hit; return skin.GetAnimation($"{prefix}{lookup}", true, false) ?? // fallback to regular size if "big" version doesn't exist. diff --git a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs index 05408e1049..d1fb3348b9 100644 --- a/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs +++ b/osu.Game.Rulesets.Taiko/UI/HitExplosion.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Taiko.UI /// public void VisualiseSecondHit() { - this.ResizeTo(new Vector2(TaikoStrongHitObject.DEFAULT_STRONG_SIZE), 50); + this.ResizeTo(new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), 50); } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs index 5d8145391d..6401c6d09f 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoHitTarget.cs @@ -32,7 +32,7 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, RelativeSizeAxes = Axes.Y, - Size = new Vector2(border_thickness, (1 - TaikoStrongHitObject.DEFAULT_STRONG_SIZE) / 2f), + Size = new Vector2(border_thickness, (1 - TaikoStrongableHitObject.DEFAULT_STRONG_SIZE) / 2f), Alpha = 0.1f }, new CircularContainer @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, - Size = new Vector2(TaikoStrongHitObject.DEFAULT_STRONG_SIZE), + Size = new Vector2(TaikoStrongableHitObject.DEFAULT_STRONG_SIZE), Masking = true, BorderColour = Color4.White, BorderThickness = border_thickness, @@ -83,7 +83,7 @@ namespace osu.Game.Rulesets.Taiko.UI Anchor = Anchor.BottomCentre, Origin = Anchor.BottomCentre, RelativeSizeAxes = Axes.Y, - Size = new Vector2(border_thickness, (1 - TaikoStrongHitObject.DEFAULT_STRONG_SIZE) / 2f), + Size = new Vector2(border_thickness, (1 - TaikoStrongableHitObject.DEFAULT_STRONG_SIZE) / 2f), Alpha = 0.1f }, }; From c5a218f7c96afe0551d4f083075e9edb924a9405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 21:46:28 +0100 Subject: [PATCH 5358/6909] Add "strongable" to user dictionary --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 3ef419c572..22ea73858e 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -928,5 +928,6 @@ private void load() True True True + True True True From 512549b4ea796e4b8f7e026cc1d2ad58db43ef46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 21:47:31 +0100 Subject: [PATCH 5359/6909] Rename DrawableTaikoStrong{-> able}HitObject --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 2 +- .../Objects/Drawables/DrawableDrumRollTick.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 2 +- .../Objects/Drawables/DrawableStrongNestedHit.cs | 2 +- ...StrongHitObject.cs => DrawableTaikoStrongableHitObject.cs} | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename osu.Game.Rulesets.Taiko/Objects/Drawables/{DrawableTaikoStrongHitObject.cs => DrawableTaikoStrongableHitObject.cs} (94%) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 0c6d28472d..4925b6fdfc 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -19,7 +19,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public class DrawableDrumRoll : DrawableTaikoStrongHitObject + public class DrawableDrumRoll : DrawableTaikoStrongableHitObject { /// /// Number of rolling hits required to reach the dark/final colour. diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index c26e20f92c..c6761de5e3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -9,7 +9,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public class DrawableDrumRollTick : DrawableTaikoStrongHitObject + public class DrawableDrumRollTick : DrawableTaikoStrongableHitObject { /// /// The hit type corresponding to the that the user pressed to hit this . diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 31cb3d764d..431f2980ec 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -16,7 +16,7 @@ using osu.Game.Skinning; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public class DrawableHit : DrawableTaikoStrongHitObject + public class DrawableHit : DrawableTaikoStrongableHitObject { /// /// A list of keys which can result in hits for this HitObject. diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index d735d258b3..d2e8888197 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs @@ -7,7 +7,7 @@ using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { /// - /// Used as a nested hitobject to provide s for s. + /// Used as a nested hitobject to provide s for s. /// public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject { diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs similarity index 94% rename from osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs rename to osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs index 614c12165e..af3e94d9c6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs @@ -12,7 +12,7 @@ using osuTK; namespace osu.Game.Rulesets.Taiko.Objects.Drawables { - public abstract class DrawableTaikoStrongHitObject : DrawableTaikoHitObject + public abstract class DrawableTaikoStrongableHitObject : DrawableTaikoHitObject where TObject : TaikoStrongableHitObject where TStrongNestedObject : StrongNestedHitObject { @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly Container strongHitContainer; - protected DrawableTaikoStrongHitObject(TObject hitObject) + protected DrawableTaikoStrongableHitObject(TObject hitObject) : base(hitObject) { isStrong = HitObject.IsStrongBindable.GetBoundCopy(); From 2051f49f78c98e79214cee4d3f5ae8f185545bc0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 21:58:13 +0100 Subject: [PATCH 5360/6909] Ensure correct initial state of taiko bar lines --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs index 9e50faabc1..d653f01db6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableBarLine.cs @@ -111,7 +111,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void LoadComplete() { base.LoadComplete(); - major.BindValueChanged(updateMajor); + major.BindValueChanged(updateMajor, true); } private void updateMajor(ValueChangedEvent major) From 523e8034407435b3d4b1beeaea4a24a457bc9918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Dec 2020 00:28:00 +0100 Subject: [PATCH 5361/6909] Fix swells crashing on rapid seeks in editor --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 5c6278ed08..229d581d0c 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -168,7 +168,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables foreach (var t in ticks) { - if (!t.IsHit) + if (!t.Result.HasResult) { nextTick = t; break; @@ -208,7 +208,8 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables continue; } - tick.TriggerResult(false); + if (!tick.Result.HasResult) + tick.TriggerResult(false); } ApplyResult(r => r.Type = numHits > HitObject.RequiredHits / 2 ? HitResult.Ok : r.Judgement.MinResult); From cba4657021286be23f5c0d90a89f5dca0da7de80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 12:37:41 +0900 Subject: [PATCH 5362/6909] Get handled file extensions from game itself, rather than duplicating locally --- osu.Game/Screens/Import/FileImportScreen.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index e2e2b699f5..24a498e8cd 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; +using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -36,7 +37,7 @@ namespace osu.Game.Screens.Import private const float button_vertical_margin = 15; [Resolved] - private OsuGameBase gameBase { get; set; } + private OsuGameBase game { get; set; } [Resolved] private OsuColour colours { get; set; } @@ -46,7 +47,6 @@ namespace osu.Game.Screens.Import { storage.GetStorageForDirectory("imports"); var originalPath = storage.GetFullPath("imports", true); - string[] fileExtensions = { ".osk", ".osr", ".osz" }; InternalChild = contentContainer = new Container { @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Import Colour = colours.GreySeafoamDark, RelativeSizeAxes = Axes.Both, }, - fileSelector = new FileSelector(initialPath: originalPath, validFileExtensions: fileExtensions) + fileSelector = new FileSelector(originalPath, game.HandledExtensions.ToArray()) { RelativeSizeAxes = Axes.Both, Width = 0.65f @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Import string[] paths = { path }; - Task.Factory.StartNew(() => gameBase.Import(paths), TaskCreationOptions.LongRunning); + Task.Factory.StartNew(() => game.Import(paths), TaskCreationOptions.LongRunning); } } } From 33f77f81f2d3af9dec7c1d6f9f94442e8211924c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 12:43:27 +0900 Subject: [PATCH 5363/6909] Disable the import button when no file is selected, rather than weird flash logic --- osu.Game/Screens/Import/FileImportScreen.cs | 24 +++++++++------------ 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 24a498e8cd..da6d3f6622 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -32,6 +32,8 @@ namespace osu.Game.Screens.Import private TextFlowContainer currentFileText; private OsuScrollContainer fileNameScroll; + private TriangleButton importButton; + private const float duration = 300; private const float button_height = 50; private const float button_vertical_margin = 15; @@ -100,7 +102,7 @@ namespace osu.Game.Screens.Import }, }, }, - new TriangleButton + importButton = new TriangleButton { Text = "Import", Anchor = Anchor.BottomCentre, @@ -109,14 +111,7 @@ namespace osu.Game.Screens.Import Height = button_height, Width = 0.9f, Margin = new MarginPadding { Vertical = button_vertical_margin }, - Action = () => - { - var d = currentFile.Value?.FullName; - if (d != null) - startImport(d); - else - currentFileText.FlashColour(Color4.Red, 500); - } + Action = () => startImport(currentFile.Value?.FullName) } } } @@ -125,21 +120,22 @@ namespace osu.Game.Screens.Import fileNameScroll.ScrollContent.Anchor = Anchor.Centre; fileNameScroll.ScrollContent.Origin = Anchor.Centre; - currentFile.BindValueChanged(updateFileSelectionText, true); - currentDirectory.BindValueChanged(onCurrentDirectoryChanged); + currentFile.BindValueChanged(fileChanged, true); + currentDirectory.BindValueChanged(directoryChanged); currentDirectory.BindTo(fileSelector.CurrentPath); currentFile.BindTo(fileSelector.CurrentFile); } - private void onCurrentDirectoryChanged(ValueChangedEvent v) + private void directoryChanged(ValueChangedEvent v) { currentFile.Value = null; } - private void updateFileSelectionText(ValueChangedEvent v) + private void fileChanged(ValueChangedEvent selectedFile) { - currentFileText.Text = v.NewValue?.Name ?? "Select a file"; + importButton.Enabled.Value = selectedFile.NewValue != null; + currentFileText.Text = selectedFile.NewValue?.Name ?? "Select a file"; } public override void OnEntering(IScreen last) From cafe81ab97cdf5565a168fe9c757b833bec64b35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 12:52:38 +0900 Subject: [PATCH 5364/6909] Further refactoring and bindable simplification --- osu.Game/Screens/Import/FileImportScreen.cs | 37 +++++++++------------ 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index da6d3f6622..72032c82b8 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -12,11 +12,10 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Platform; using osu.Framework.Screens; using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; using osuTK; -using osu.Game.Graphics.UserInterface; -using osu.Game.Graphics.Containers; -using osuTK.Graphics; namespace osu.Game.Screens.Import { @@ -24,13 +23,9 @@ namespace osu.Game.Screens.Import { public override bool HideOverlaysOnEnter => true; - private readonly Bindable currentFile = new Bindable(); - private readonly IBindable currentDirectory = new Bindable(); - private FileSelector fileSelector; private Container contentContainer; private TextFlowContainer currentFileText; - private OsuScrollContainer fileNameScroll; private TriangleButton importButton; @@ -47,9 +42,6 @@ namespace osu.Game.Screens.Import [BackgroundDependencyLoader(true)] private void load(Storage storage) { - storage.GetStorageForDirectory("imports"); - var originalPath = storage.GetFullPath("imports", true); - InternalChild = contentContainer = new Container { Masking = true, @@ -65,7 +57,7 @@ namespace osu.Game.Screens.Import Colour = colours.GreySeafoamDark, RelativeSizeAxes = Axes.Both, }, - fileSelector = new FileSelector(originalPath, game.HandledExtensions.ToArray()) + fileSelector = new FileSelector(validFileExtensions: game.HandledExtensions.ToArray()) { RelativeSizeAxes = Axes.Both, Width = 0.65f @@ -87,7 +79,7 @@ namespace osu.Game.Screens.Import { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Bottom = button_height + button_vertical_margin * 2 }, - Child = fileNameScroll = new OsuScrollContainer + Child = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, Anchor = Anchor.TopCentre, @@ -100,6 +92,11 @@ namespace osu.Game.Screens.Import Origin = Anchor.Centre, TextAnchor = Anchor.Centre }, + ScrollContent = + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } }, }, importButton = new TriangleButton @@ -111,25 +108,21 @@ namespace osu.Game.Screens.Import Height = button_height, Width = 0.9f, Margin = new MarginPadding { Vertical = button_vertical_margin }, - Action = () => startImport(currentFile.Value?.FullName) + Action = () => startImport(fileSelector.CurrentFile.Value?.FullName) } } } } }; - fileNameScroll.ScrollContent.Anchor = Anchor.Centre; - fileNameScroll.ScrollContent.Origin = Anchor.Centre; - currentFile.BindValueChanged(fileChanged, true); - currentDirectory.BindValueChanged(directoryChanged); - - currentDirectory.BindTo(fileSelector.CurrentPath); - currentFile.BindTo(fileSelector.CurrentFile); + fileSelector.CurrentFile.BindValueChanged(fileChanged, true); + fileSelector.CurrentPath.BindValueChanged(directoryChanged); } - private void directoryChanged(ValueChangedEvent v) + private void directoryChanged(ValueChangedEvent _) { - currentFile.Value = null; + // this should probably be done by the selector itself, but let's do it here for now. + fileSelector.CurrentFile.Value = null; } private void fileChanged(ValueChangedEvent selectedFile) From db4f2d5ffbbcf3ba90a4aa5c563314c40d5bacb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 12:52:45 +0900 Subject: [PATCH 5365/6909] Refresh view after import succeeds --- osu.Game/Screens/Import/FileImportScreen.cs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 72032c82b8..3646dc819c 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -154,16 +154,17 @@ namespace osu.Game.Screens.Import if (string.IsNullOrEmpty(path)) return; - if (!File.Exists(path)) - { - currentFileText.Text = "No such file"; - currentFileText.FlashColour(colours.Red, duration); - return; - } + Task.Factory.StartNew(async () => await game.Import(path), TaskCreationOptions.LongRunning) + .ContinueWith(_ => + { + // some files will be deleted after successful import, so we want to refresh the view. - string[] paths = { path }; - - Task.Factory.StartNew(() => game.Import(paths), TaskCreationOptions.LongRunning); + Schedule(() => + { + // should probably be exposed as a refresh method. + fileSelector.CurrentPath.TriggerChange(); + }); + }); } } } From 0d4ac2f748b968cf972c8270b1dc2138857ced96 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 12:57:28 +0900 Subject: [PATCH 5366/6909] Refresh view after import completes --- osu.Game/Screens/Import/FileImportScreen.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 3646dc819c..5f7888d743 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -154,17 +154,17 @@ namespace osu.Game.Screens.Import if (string.IsNullOrEmpty(path)) return; - Task.Factory.StartNew(async () => await game.Import(path), TaskCreationOptions.LongRunning) - .ContinueWith(_ => - { - // some files will be deleted after successful import, so we want to refresh the view. + Task.Factory.StartNew(async () => + { + await game.Import(path); - Schedule(() => - { - // should probably be exposed as a refresh method. - fileSelector.CurrentPath.TriggerChange(); - }); + // some files will be deleted after successful import, so we want to refresh the view. + Schedule(() => + { + // should probably be exposed as a refresh method. + fileSelector.CurrentPath.TriggerChange(); }); + }, TaskCreationOptions.LongRunning); } } } From 10ee8ed8e86c392015a29ffa81c8798101066fd0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 13:02:38 +0900 Subject: [PATCH 5367/6909] Reorder functions and simplify transform logic --- osu.Game/Screens/Import/FileImportScreen.cs | 34 ++++++++++----------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 5f7888d743..329623e03a 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -119,6 +119,22 @@ namespace osu.Game.Screens.Import fileSelector.CurrentPath.BindValueChanged(directoryChanged); } + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + + contentContainer.ScaleTo(0.95f).ScaleTo(1, duration, Easing.OutQuint); + this.FadeInFromZero(duration); + } + + public override bool OnExiting(IScreen next) + { + contentContainer.ScaleTo(0.95f, duration, Easing.OutQuint); + this.FadeOut(duration, Easing.OutQuint); + + return base.OnExiting(next); + } + private void directoryChanged(ValueChangedEvent _) { // this should probably be done by the selector itself, but let's do it here for now. @@ -131,24 +147,6 @@ namespace osu.Game.Screens.Import currentFileText.Text = selectedFile.NewValue?.Name ?? "Select a file"; } - public override void OnEntering(IScreen last) - { - base.OnEntering(last); - - contentContainer.FadeOut().Then().ScaleTo(0.95f) - .Then() - .ScaleTo(1, duration, Easing.OutQuint) - .FadeIn(duration); - } - - public override bool OnExiting(IScreen next) - { - contentContainer.ScaleTo(0.95f, duration, Easing.OutQuint); - this.FadeOut(duration, Easing.OutQuint); - - return base.OnExiting(next); - } - private void startImport(string path) { if (string.IsNullOrEmpty(path)) From f0e6b6eaf81c540382f70dc39dcdad249acaad42 Mon Sep 17 00:00:00 2001 From: Gagah Pangeran Date: Tue, 15 Dec 2020 11:09:09 +0700 Subject: [PATCH 5368/6909] sort by ruleset id first then star diff --- osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs index 88c15776cd..1567e18caa 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs @@ -152,7 +152,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels } else { - foreach (var b in SetInfo.Beatmaps.OrderBy(beatmap => beatmap.StarDifficulty)) + foreach (var b in SetInfo.Beatmaps.OrderBy(beatmap => beatmap.Ruleset.ID).ThenBy(beatmap => beatmap.StarDifficulty)) icons.Add(new DifficultyIcon(b)); } From 8bdef0ff5573276ea4f2a7f6a833bd81098b84d9 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 15 Dec 2020 13:18:41 +0900 Subject: [PATCH 5369/6909] Code quality fix Co-authored-by: Dean Herbert --- 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 af7786c3fd..b235b59609 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -157,7 +157,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty speedValue *= 1.0 + 0.04 * (12.0 - Attributes.ApproachRate); // Scale the speed value with accuracy and OD - speedValue *= (.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); + speedValue *= (0.95 + Math.Pow(Attributes.OverallDifficulty, 2) / 750) * Math.Pow(accuracy, (14.5 - Math.Max(Attributes.OverallDifficulty, 8)) / 2); // Scale the speed value with # of 50s to punish doubletapping. speedValue *= Math.Pow(0.98, countMeh < totalHits / 500.0 ? 0 : countMeh - totalHits / 500.0); From 8362ad37e34c10d2ada7bd4468d851d6debdb45a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 15:22:14 +0900 Subject: [PATCH 5370/6909] Bring up-to-date with code changes --- .../Gameplay/TestSceneInGameLeaderboard.cs | 26 ++++++++----------- osu.Game/Screens/Play/InGameScoreContainer.cs | 3 ++- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs index 0019212dfa..4944a6477d 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.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 NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Screens.Play; using osu.Game.Users; using osuTK; @@ -16,12 +15,6 @@ namespace osu.Game.Tests.Visual.Gameplay [TestFixture] public class TestSceneInGameLeaderboard : OsuTestScene { - public override IReadOnlyList RequiredTypes => new[] - { - typeof(InGameLeaderboard), - typeof(InGameScoreContainer), - }; - private readonly TestInGameLeaderboard leaderboard; private readonly BindableDouble playerScore; @@ -35,18 +28,21 @@ namespace osu.Game.Tests.Visual.Gameplay RelativeSizeAxes = Axes.X, PlayerCurrentScore = { BindTarget = playerScore = new BindableDouble(1222333) } }); + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("reset leaderboard", () => + { + leaderboard.ClearScores(); + playerScore.Value = 1222333; + }); AddStep("add player user", () => leaderboard.PlayerUser = new User { Username = "You" }); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } - [SetUp] - public void SetUp() - { - leaderboard.ClearScores(); - playerScore.Value = 1222333; - } - [Test] public void TestPlayerScore() { diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/InGameScoreContainer.cs index f548b3de3f..d3ec132351 100644 --- a/osu.Game/Screens/Play/InGameScoreContainer.cs +++ b/osu.Game/Screens/Play/InGameScoreContainer.cs @@ -6,6 +6,7 @@ using System.Linq; 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.Game.Graphics; @@ -210,7 +211,7 @@ namespace osu.Game.Screens.Play }, scoreText = new GlowingSpriteText { - GlowColour = OsuColour.FromHex(@"83ccfa"), + GlowColour = Color4Extensions.FromHex(@"83ccfa"), Font = OsuFont.Numeric.With(size: 14), } } From dd5572b20a9336562e30bcf14d4bc444d8d0f3be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 15:26:20 +0900 Subject: [PATCH 5371/6909] Remove unnecessary methods and event --- osu.Game/Screens/Play/InGameScoreContainer.cs | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/InGameScoreContainer.cs index d3ec132351..693020fc4c 100644 --- a/osu.Game/Screens/Play/InGameScoreContainer.cs +++ b/osu.Game/Screens/Play/InGameScoreContainer.cs @@ -19,12 +19,6 @@ namespace osu.Game.Screens.Play { public class InGameScoreContainer : FillFlowContainer { - /// - /// Called once an item's score has changed. - /// Useful for doing calculations on what score to show or hide next. (scrolling system) - /// - public Action OnScoreChange; - /// /// Whether to declare a new position for un-positioned players. /// Must be disabled for online leaderboards with top 50 scores only. @@ -101,9 +95,16 @@ namespace osu.Game.Screens.Play private void updateScores() { - reorderPositions(); + var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); + var orderedPositions = this.Select(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition).OrderByDescending(p => p.HasValue).ThenBy(p => p).ToList(); - OnScoreChange?.Invoke(); + for (int i = 0; i < Count; i++) + { + int newPosition = orderedPositions[i] ?? maxPosition + 1; + + SetLayoutPosition(orderedByScore[i], newPosition); + orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; + } } } From 8b68ccc0ff96f2bd0ae9b3076cdc19e2bff3ac26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 15:27:26 +0900 Subject: [PATCH 5372/6909] Rename class and move inside HUD namespace --- .../Gameplay/TestSceneInGameLeaderboard.cs | 34 +++--- .../Screens/Play/HUD/GameplayLeaderboard.cs | 104 ++++++++++++++++++ .../GameplayLeaderboardScore.cs} | 100 +---------------- osu.Game/Screens/Play/InGameLeaderboard.cs | 42 ------- 4 files changed, 122 insertions(+), 158 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs rename osu.Game/Screens/Play/{InGameScoreContainer.cs => HUD/GameplayLeaderboardScore.cs} (52%) delete mode 100644 osu.Game/Screens/Play/InGameLeaderboard.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs index 4944a6477d..657eb7e8ba 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs @@ -6,27 +6,27 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; -using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Gameplay { [TestFixture] - public class TestSceneInGameLeaderboard : OsuTestScene + public class TestSceneGameplayLeaderboard : OsuTestScene { - private readonly TestInGameLeaderboard leaderboard; - private readonly BindableDouble playerScore; + private readonly TestGameplayLeaderboard leaderboard; - public TestSceneInGameLeaderboard() + private readonly BindableDouble playerScore = new BindableDouble(); + + public TestSceneGameplayLeaderboard() { - Add(leaderboard = new TestInGameLeaderboard + Add(leaderboard = new TestGameplayLeaderboard { Anchor = Anchor.Centre, Origin = Anchor.Centre, Scale = new Vector2(2), RelativeSizeAxes = Axes.X, - PlayerCurrentScore = { BindTarget = playerScore = new BindableDouble(1222333) } }); } @@ -35,11 +35,11 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("reset leaderboard", () => { - leaderboard.ClearScores(); + leaderboard.Clear(); playerScore.Value = 1222333; }); - AddStep("add player user", () => leaderboard.PlayerUser = new User { Username = "You" }); + AddStep("add player user", () => leaderboard.AddRealTimePlayer(playerScore, new User { Username = "You" })); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } @@ -49,8 +49,8 @@ namespace osu.Game.Tests.Visual.Gameplay var player2Score = new BindableDouble(1234567); var player3Score = new BindableDouble(1111111); - AddStep("add player 2", () => leaderboard.AddDummyPlayer(player2Score, "Player 2")); - AddStep("add player 3", () => leaderboard.AddDummyPlayer(player3Score, "Player 3")); + AddStep("add player 2", () => leaderboard.AddRealTimePlayer(player2Score, new User { Username = "Player 2" })); + AddStep("add player 3", () => leaderboard.AddRealTimePlayer(player3Score, new User { Username = "Player 3" })); AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); @@ -67,18 +67,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); } - private class TestInGameLeaderboard : InGameLeaderboard + private class TestGameplayLeaderboard : GameplayLeaderboard { - public void ClearScores() => ScoresContainer.RemoveAll(s => s.User.Username != PlayerUser.Username); - - public bool CheckPositionByUsername(string username, int? estimatedPosition) + public bool CheckPositionByUsername(string username, int? expectedPosition) { - var scoreItem = ScoresContainer.FirstOrDefault(i => i.User.Username == username); + var scoreItem = this.FirstOrDefault(i => i.User.Username == username); - return scoreItem != null && scoreItem.ScorePosition == estimatedPosition; + return scoreItem != null && scoreItem.ScorePosition == expectedPosition; } - - public void AddDummyPlayer(BindableDouble currentScore, string username) => ScoresContainer.AddRealTimePlayer(currentScore, new User { Username = username }); } } } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs new file mode 100644 index 0000000000..9e1b9a3958 --- /dev/null +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.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 System.Linq; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Scoring; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Screens.Play.HUD +{ + public class GameplayLeaderboard : FillFlowContainer + { + /// + /// Whether to declare a new position for un-positioned players. + /// Must be disabled for online leaderboards with top 50 scores only. + /// + public bool DeclareNewPosition = true; + + public GameplayLeaderboard() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + Spacing = new Vector2(2.5f); + LayoutDuration = 500; + LayoutEasing = Easing.OutQuint; + } + + /// + /// Adds a real-time player score item whose score is updated via a . + /// + /// The bindable current score of the player. + /// The player user. + /// Returns the drawable score item of that player. + public GameplayLeaderboardScore AddRealTimePlayer(BindableDouble currentScore, User user = null) + { + if (currentScore == null) + return null; + + var scoreItem = addScore(currentScore.Value, user); + currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue; + + return scoreItem; + } + + /// + /// Adds a score item based off a with an initial position. + /// + /// The score info to use for this item. + /// The initial position of this item. + /// Returns the drawable score item of that player. + public GameplayLeaderboardScore AddScore(ScoreInfo score, int? initialPosition = null) => score != null ? addScore(score.TotalScore, score.User, initialPosition) : null; + + private int maxPosition => this.Max(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition) ?? 0; + + private GameplayLeaderboardScore addScore(double totalScore, User user = null, int? position = null) + { + var scoreItem = new GameplayLeaderboardScore(position) + { + User = user, + TotalScore = totalScore, + OnScoreChange = updateScores, + }; + + Add(scoreItem); + SetLayoutPosition(scoreItem, position ?? maxPosition + 1); + + reorderPositions(); + + return scoreItem; + } + + private void reorderPositions() + { + var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); + var orderedPositions = this.Select(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition).OrderByDescending(p => p.HasValue).ThenBy(p => p).ToList(); + + for (int i = 0; i < Count; i++) + { + int newPosition = orderedPositions[i] ?? maxPosition + 1; + + SetLayoutPosition(orderedByScore[i], newPosition); + orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; + } + } + + private void updateScores() + { + var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); + var orderedPositions = this.Select(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition).OrderByDescending(p => p.HasValue).ThenBy(p => p).ToList(); + + for (int i = 0; i < Count; i++) + { + int newPosition = orderedPositions[i] ?? maxPosition + 1; + + SetLayoutPosition(orderedByScore[i], newPosition); + orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; + } + } + } +} diff --git a/osu.Game/Screens/Play/InGameScoreContainer.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs similarity index 52% rename from osu.Game/Screens/Play/InGameScoreContainer.cs rename to osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 693020fc4c..3af7345ae1 100644 --- a/osu.Game/Screens/Play/InGameScoreContainer.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -2,113 +2,19 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; 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.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Scoring; using osu.Game.Users; using osuTK; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { - public class InGameScoreContainer : FillFlowContainer - { - /// - /// Whether to declare a new position for un-positioned players. - /// Must be disabled for online leaderboards with top 50 scores only. - /// - public bool DeclareNewPosition = true; - - public InGameScoreContainer() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Direction = FillDirection.Vertical; - Spacing = new Vector2(2.5f); - LayoutDuration = 500; - LayoutEasing = Easing.OutQuint; - } - - /// - /// Adds a real-time player score item whose score is updated via a . - /// - /// The bindable current score of the player. - /// The player user. - /// Returns the drawable score item of that player. - public InGameScoreItem AddRealTimePlayer(BindableDouble currentScore, User user = null) - { - if (currentScore == null) - return null; - - var scoreItem = addScore(currentScore.Value, user); - currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue; - - return scoreItem; - } - - /// - /// Adds a score item based off a with an initial position. - /// - /// The score info to use for this item. - /// The initial position of this item. - /// Returns the drawable score item of that player. - public InGameScoreItem AddScore(ScoreInfo score, int? initialPosition = null) => score != null ? addScore(score.TotalScore, score.User, initialPosition) : null; - - private int maxPosition => this.Max(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition) ?? 0; - - private InGameScoreItem addScore(double totalScore, User user = null, int? position = null) - { - var scoreItem = new InGameScoreItem(position) - { - User = user, - TotalScore = totalScore, - OnScoreChange = updateScores, - }; - - Add(scoreItem); - SetLayoutPosition(scoreItem, position ?? maxPosition + 1); - - reorderPositions(); - - return scoreItem; - } - - private void reorderPositions() - { - var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); - var orderedPositions = this.Select(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition).OrderByDescending(p => p.HasValue).ThenBy(p => p).ToList(); - - for (int i = 0; i < Count; i++) - { - int newPosition = orderedPositions[i] ?? maxPosition + 1; - - SetLayoutPosition(orderedByScore[i], newPosition); - orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; - } - } - - private void updateScores() - { - var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); - var orderedPositions = this.Select(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition).OrderByDescending(p => p.HasValue).ThenBy(p => p).ToList(); - - for (int i = 0; i < Count; i++) - { - int newPosition = orderedPositions[i] ?? maxPosition + 1; - - SetLayoutPosition(orderedByScore[i], newPosition); - orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; - } - } - } - - public class InGameScoreItem : CompositeDrawable + public class GameplayLeaderboardScore : CompositeDrawable { private readonly OsuSpriteText positionText, positionSymbol, userString; private readonly GlowingSpriteText scoreText; @@ -159,7 +65,7 @@ namespace osu.Game.Screens.Play } } - public InGameScoreItem(int? initialPosition) + public GameplayLeaderboardScore(int? initialPosition) { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; diff --git a/osu.Game/Screens/Play/InGameLeaderboard.cs b/osu.Game/Screens/Play/InGameLeaderboard.cs deleted file mode 100644 index c8f5cf5fd7..0000000000 --- a/osu.Game/Screens/Play/InGameLeaderboard.cs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Users; - -namespace osu.Game.Screens.Play -{ - public class InGameLeaderboard : CompositeDrawable - { - protected readonly InGameScoreContainer ScoresContainer; - - public readonly BindableDouble PlayerCurrentScore = new BindableDouble(); - - private bool playerItemCreated; - private User playerUser; - - public User PlayerUser - { - get => playerUser; - set - { - playerUser = value; - - if (playerItemCreated) - return; - - ScoresContainer.AddRealTimePlayer(PlayerCurrentScore, playerUser); - playerItemCreated = true; - } - } - - public InGameLeaderboard() - { - AutoSizeAxes = Axes.Y; - - InternalChild = ScoresContainer = new InGameScoreContainer(); - } - } -} From b5ab400ad78ac968b4b5a629bb5aacab39dd63eb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 15:44:43 +0900 Subject: [PATCH 5373/6909] Fix test filename not matching updated class name --- ...InGameLeaderboard.cs => TestSceneGameplayLeaderboard.cs} | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) rename osu.Game.Tests/Visual/Gameplay/{TestSceneInGameLeaderboard.cs => TestSceneGameplayLeaderboard.cs} (89%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs similarity index 89% rename from osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 657eb7e8ba..df970c1c46 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneInGameLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay playerScore.Value = 1222333; }); - AddStep("add player user", () => leaderboard.AddRealTimePlayer(playerScore, new User { Username = "You" })); + AddStep("add player user", () => leaderboard.AddPlayer(playerScore, new User { Username = "You" })); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } @@ -49,8 +49,8 @@ namespace osu.Game.Tests.Visual.Gameplay var player2Score = new BindableDouble(1234567); var player3Score = new BindableDouble(1111111); - AddStep("add player 2", () => leaderboard.AddRealTimePlayer(player2Score, new User { Username = "Player 2" })); - AddStep("add player 3", () => leaderboard.AddRealTimePlayer(player3Score, new User { Username = "Player 3" })); + AddStep("add player 2", () => leaderboard.AddPlayer(player2Score, new User { Username = "Player 2" })); + AddStep("add player 3", () => leaderboard.AddPlayer(player3Score, new User { Username = "Player 3" })); AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); From e37089af5e188e7dbb769ba4d891a25b48420412 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 15:44:56 +0900 Subject: [PATCH 5374/6909] Further code cleanup --- .../Screens/Play/HUD/GameplayLeaderboard.cs | 65 ++++--------------- .../Play/HUD/GameplayLeaderboardScore.cs | 5 +- 2 files changed, 15 insertions(+), 55 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 9e1b9a3958..6ee654fb9e 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -1,11 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Scoring; using osu.Game.Users; using osuTK; @@ -13,52 +14,33 @@ namespace osu.Game.Screens.Play.HUD { public class GameplayLeaderboard : FillFlowContainer { - /// - /// Whether to declare a new position for un-positioned players. - /// Must be disabled for online leaderboards with top 50 scores only. - /// - public bool DeclareNewPosition = true; - public GameplayLeaderboard() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + Spacing = new Vector2(2.5f); - LayoutDuration = 500; + + LayoutDuration = 250; LayoutEasing = Easing.OutQuint; } /// - /// Adds a real-time player score item whose score is updated via a . + /// Adds a player to the leaderboard. /// /// The bindable current score of the player. - /// The player user. - /// Returns the drawable score item of that player. - public GameplayLeaderboardScore AddRealTimePlayer(BindableDouble currentScore, User user = null) + /// The player. + public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user) { - if (currentScore == null) - return null; - var scoreItem = addScore(currentScore.Value, user); currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue; - - return scoreItem; } - /// - /// Adds a score item based off a with an initial position. - /// - /// The score info to use for this item. - /// The initial position of this item. - /// Returns the drawable score item of that player. - public GameplayLeaderboardScore AddScore(ScoreInfo score, int? initialPosition = null) => score != null ? addScore(score.TotalScore, score.User, initialPosition) : null; - - private int maxPosition => this.Max(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition) ?? 0; - - private GameplayLeaderboardScore addScore(double totalScore, User user = null, int? position = null) + private GameplayLeaderboardScore addScore(double totalScore, User user) { - var scoreItem = new GameplayLeaderboardScore(position) + var scoreItem = new GameplayLeaderboardScore { User = user, TotalScore = totalScore, @@ -66,38 +48,19 @@ namespace osu.Game.Screens.Play.HUD }; Add(scoreItem); - SetLayoutPosition(scoreItem, position ?? maxPosition + 1); - - reorderPositions(); + updateScores(); return scoreItem; } - private void reorderPositions() - { - var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); - var orderedPositions = this.Select(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition).OrderByDescending(p => p.HasValue).ThenBy(p => p).ToList(); - - for (int i = 0; i < Count; i++) - { - int newPosition = orderedPositions[i] ?? maxPosition + 1; - - SetLayoutPosition(orderedByScore[i], newPosition); - orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; - } - } - private void updateScores() { var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); - var orderedPositions = this.Select(i => this.Any(item => item.InitialPosition.HasValue) ? i.InitialPosition : i.ScorePosition).OrderByDescending(p => p.HasValue).ThenBy(p => p).ToList(); for (int i = 0; i < Count; i++) { - int newPosition = orderedPositions[i] ?? maxPosition + 1; - - SetLayoutPosition(orderedByScore[i], newPosition); - orderedByScore[i].ScorePosition = DeclareNewPosition ? newPosition : orderedPositions[i]; + SetLayoutPosition(orderedByScore[i], i); + orderedByScore[i].ScorePosition = i + 1; } } } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 3af7345ae1..4c75f422c9 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -22,7 +22,6 @@ namespace osu.Game.Screens.Play.HUD public Action OnScoreChange; private int? scorePosition; - public int? InitialPosition; public int? ScorePosition { @@ -65,7 +64,7 @@ namespace osu.Game.Screens.Play.HUD } } - public GameplayLeaderboardScore(int? initialPosition) + public GameplayLeaderboardScore() { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; @@ -125,8 +124,6 @@ namespace osu.Game.Screens.Play.HUD }, }, }; - - InitialPosition = ScorePosition = initialPosition; } [BackgroundDependencyLoader] From ea6c196f81e72ac967da65218c69f8bf37ce2f9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 15 Dec 2020 16:03:18 +0900 Subject: [PATCH 5375/6909] Remove unused using statement --- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 6ee654fb9e..e53c56b390 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.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 JetBrains.Annotations; using osu.Framework.Bindables; From d0668192aad1d44fea6516ce0d108146177a7737 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 15 Dec 2020 21:25:53 +0100 Subject: [PATCH 5376/6909] Ensure stream disposal & move entire operation inside task --- osu.Android/OsuGameActivity.cs | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index cf0179e2ac..e801c2ca6e 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; using System.IO; using System.Threading.Tasks; using Android.App; @@ -62,7 +61,7 @@ namespace osu.Android } } - private void handleImportFromUri(Uri uri) + private void handleImportFromUri(Uri uri) => Task.Factory.StartNew(async () => { // there are more performant overloads of this method, but this one is the most backwards-compatible // (dates back to API 1). @@ -74,23 +73,16 @@ namespace osu.Android cursor.MoveToFirst(); var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); - - var stream = ContentResolver.OpenInputStream(uri); string filename = cursor.GetString(filenameColumn); - if (stream != null) - Task.Factory.StartNew(() => runImport(stream, filename), TaskCreationOptions.LongRunning); - } - - private Task runImport(Stream stream, string filename) - { // SharpCompress requires archive streams to be seekable, which the stream opened by // OpenInputStream() seems to not necessarily be. // copy to an arbitrary-access memory stream to be able to proceed with the import. var copy = new MemoryStream(); - stream.CopyTo(copy); + using (var stream = ContentResolver.OpenInputStream(uri)) + await stream.CopyToAsync(copy); - return game.Import(copy, filename); - } + await game.Import(copy, filename); + }, TaskCreationOptions.LongRunning); } } From 4cd290af11eb9918848929b717e38d681bac7a3d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Dec 2020 12:31:05 +0900 Subject: [PATCH 5377/6909] Split server interfaces --- .../IMultiplayerLoungeServer.cs | 20 ++++++++ .../IMultiplayerRoomServer.cs | 51 +++++++++++++++++++ .../RealtimeMultiplayer/IMultiplayerServer.cs | 50 +----------------- 3 files changed, 73 insertions(+), 48 deletions(-) create mode 100644 osu.Game/Online/RealtimeMultiplayer/IMultiplayerLoungeServer.cs create mode 100644 osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerLoungeServer.cs new file mode 100644 index 0000000000..eecb61bcb0 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerLoungeServer.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.Threading.Tasks; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + /// + /// Interface for an out-of-room multiplayer server. + /// + public interface IMultiplayerLoungeServer + { + /// + /// Request to join a multiplayer room. + /// + /// The databased room ID. + /// If the user is already in the requested (or another) room. + Task JoinRoom(long roomId); + } +} diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs new file mode 100644 index 0000000000..f1b3daf7d3 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs @@ -0,0 +1,51 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading.Tasks; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + /// + /// Interface for an in-room multiplayer server. + /// + public interface IMultiplayerRoomServer + { + /// + /// Request to leave the currently joined room. + /// + /// If the user is not in a room. + Task LeaveRoom(); + + /// + /// Transfer the host of the currently joined room to another user in the room. + /// + /// The new user which is to become host. + /// A user other than the current host is attempting to transfer host. + /// If the user is not in a room. + Task TransferHost(long userId); + + /// + /// As the host, update the settings of the currently joined room. + /// + /// The new settings to apply. + /// A user other than the current host is attempting to transfer host. + /// If the user is not in a room. + Task ChangeSettings(MultiplayerRoomSettings settings); + + /// + /// Change the local user state in the currently joined room. + /// + /// The proposed new state. + /// If the state change requested is not valid, given the previous state or room state. + /// If the user is not in a room. + Task ChangeState(MultiplayerUserState newState); + + /// + /// As the host of a room, start the match. + /// + /// A user other than the current host is attempting to start the game. + /// If the user is not in a room. + /// If an attempt to start the game occurs when the game's (or users') state disallows it. + Task StartMatch(); + } +} diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs index e2e7b6b991..1d093af743 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs @@ -1,58 +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.Tasks; - namespace osu.Game.Online.RealtimeMultiplayer { /// - /// An interface defining the spectator server instance. + /// An interface defining the multiplayer server instance. /// - public interface IMultiplayerServer + public interface IMultiplayerServer : IMultiplayerRoomServer, IMultiplayerLoungeServer { - /// - /// Request to join a multiplayer room. - /// - /// The databased room ID. - /// If the user is already in the requested (or another) room. - Task JoinRoom(long roomId); - - /// - /// Request to leave the currently joined room. - /// - /// If the user is not in a room. - Task LeaveRoom(); - - /// - /// Transfer the host of the currently joined room to another user in the room. - /// - /// The new user which is to become host. - /// A user other than the current host is attempting to transfer host. - /// If the user is not in a room. - Task TransferHost(long userId); - - /// - /// As the host, update the settings of the currently joined room. - /// - /// The new settings to apply. - /// A user other than the current host is attempting to transfer host. - /// If the user is not in a room. - Task ChangeSettings(MultiplayerRoomSettings settings); - - /// - /// Change the local user state in the currently joined room. - /// - /// The proposed new state. - /// If the state change requested is not valid, given the previous state or room state. - /// If the user is not in a room. - Task ChangeState(MultiplayerUserState newState); - - /// - /// As the host of a room, start the match. - /// - /// A user other than the current host is attempting to start the game. - /// If the user is not in a room. - /// If an attempt to start the game occurs when the game's (or users') state disallows it. - Task StartMatch(); } } From 31fe28b8b326f70180c31c94c5e46a36a95b99b2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Dec 2020 12:32:53 +0900 Subject: [PATCH 5378/6909] Remove IStatefulMultiplayerClient --- .../IStatefulMultiplayerClient.cs | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 osu.Game/Online/RealtimeMultiplayer/IStatefulMultiplayerClient.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/IStatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/IStatefulMultiplayerClient.cs deleted file mode 100644 index 578092662a..0000000000 --- a/osu.Game/Online/RealtimeMultiplayer/IStatefulMultiplayerClient.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. - -#nullable enable - -namespace osu.Game.Online.RealtimeMultiplayer -{ - /// - /// A multiplayer client which maintains local room and user state. Also provides a proxy to access the . - /// - public interface IStatefulMultiplayerClient : IMultiplayerClient, IMultiplayerServer - { - MultiplayerUserState State { get; } - - MultiplayerRoom? Room { get; } - } -} From 84a077078967aeb6bf8a5f526bdb7cd1288c876d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 15:35:07 +0900 Subject: [PATCH 5379/6909] Change frame header to use dictionary for compatibility --- osu.Game/Online/Spectator/FrameHeader.cs | 10 ++++++---- osu.Game/Online/Spectator/StatisticPair.cs | 23 ---------------------- 2 files changed, 6 insertions(+), 27 deletions(-) delete mode 100644 osu.Game/Online/Spectator/StatisticPair.cs diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs index 9b6cc615a4..f2dd30a002 100644 --- a/osu.Game/Online/Spectator/FrameHeader.cs +++ b/osu.Game/Online/Spectator/FrameHeader.cs @@ -4,8 +4,9 @@ #nullable enable using System; -using System.Linq; +using System.Collections.Generic; using Newtonsoft.Json; +using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; namespace osu.Game.Online.Spectator @@ -17,7 +18,7 @@ namespace osu.Game.Online.Spectator public int MaxCombo { get; set; } - public StatisticPair[] Statistics { get; set; } + public Dictionary Statistics { get; set; } /// /// Construct header summary information from a point-in-time reference to a score which is actively being played. @@ -28,11 +29,12 @@ namespace osu.Game.Online.Spectator Combo = score.Combo; MaxCombo = score.MaxCombo; - Statistics = score.Statistics.Select(kvp => new StatisticPair(kvp.Key, kvp.Value)).ToArray(); + // copy for safety + Statistics = new Dictionary(score.Statistics); } [JsonConstructor] - public FrameHeader(int combo, int maxCombo, StatisticPair[] statistics) + public FrameHeader(int combo, int maxCombo, Dictionary statistics) { Combo = combo; MaxCombo = maxCombo; diff --git a/osu.Game/Online/Spectator/StatisticPair.cs b/osu.Game/Online/Spectator/StatisticPair.cs deleted file mode 100644 index 793143a64c..0000000000 --- a/osu.Game/Online/Spectator/StatisticPair.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 System; -using osu.Game.Rulesets.Scoring; - -namespace osu.Game.Online.Spectator -{ - [Serializable] - public struct StatisticPair - { - public HitResult Result; - public int Count; - - public StatisticPair(HitResult result, int count) - { - Result = result; - Count = count; - } - - public override string ToString() => $"{Result}=>{Count}"; - } -} From 72d296f4128066787bf0c098d52422aefe363622 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 16:19:53 +0900 Subject: [PATCH 5380/6909] Add received timestamp and basic xmldoc for header class --- osu.Game/Online/Spectator/FrameHeader.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs index f2dd30a002..b4988fecf9 100644 --- a/osu.Game/Online/Spectator/FrameHeader.cs +++ b/osu.Game/Online/Spectator/FrameHeader.cs @@ -14,12 +14,26 @@ namespace osu.Game.Online.Spectator [Serializable] public class FrameHeader { + /// + /// The current combo of the score. + /// public int Combo { get; set; } + /// + /// The maximum combo achieved up to the current point in time. + /// public int MaxCombo { get; set; } + /// + /// Cumulative hit statistics. + /// public Dictionary Statistics { get; set; } + /// + /// The time at which this frame was received by the server. + /// + public DateTimeOffset ReceivedTime { get; set; } + /// /// Construct header summary information from a point-in-time reference to a score which is actively being played. /// @@ -34,11 +48,12 @@ namespace osu.Game.Online.Spectator } [JsonConstructor] - public FrameHeader(int combo, int maxCombo, Dictionary statistics) + public FrameHeader(int combo, int maxCombo, Dictionary statistics, DateTimeOffset receivedTime) { Combo = combo; MaxCombo = maxCombo; Statistics = statistics; + ReceivedTime = receivedTime; } } } From fb795f6bfdbb0638c21ae33e2f02e025ca38d780 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 13:00:19 +0900 Subject: [PATCH 5381/6909] Add initial hook-up to spectator backend --- .../Visual/Gameplay/TestSceneSpectator.cs | 2 +- .../TestSceneSpectatorDrivenLeaderboard.cs | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 3e5b561a6f..1fdff99da6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -232,7 +232,7 @@ namespace osu.Game.Tests.Visual.Gameplay public class TestSpectatorStreamingClient : SpectatorStreamingClient { - public readonly User StreamingUser = new User { Id = 1234, Username = "Test user" }; + public readonly User StreamingUser = new User { Id = 55, Username = "Test user" }; public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs new file mode 100644 index 0000000000..7211755ba6 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.Spectator; +using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Screens.Play.HUD; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneSpectatorDrivenLeaderboard : OsuTestScene + { + [Cached(typeof(SpectatorStreamingClient))] + private TestSceneSpectator.TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSceneSpectator.TestSpectatorStreamingClient(); + + // used just to show beatmap card for the time being. + protected override bool UseOnlineAPI => true; + + [SetUp] + public void SetUp() => Schedule(() => + { + OsuScoreProcessor scoreProcessor; + + testSpectatorStreamingClient.StartPlay(55); + + Children = new Drawable[] + { + scoreProcessor = new OsuScoreProcessor(), + new MultiplayerGameplayLeaderboard(scoreProcessor) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + } + }; + }); + } + + public class MultiplayerGameplayLeaderboard : GameplayLeaderboard + { + private readonly OsuScoreProcessor scoreProcessor; + + public MultiplayerGameplayLeaderboard(OsuScoreProcessor scoreProcessor) + { + this.scoreProcessor = scoreProcessor; + + AddPlayer(new BindableDouble(), new GuestUser()); + } + + [Resolved] + private SpectatorStreamingClient streamingClient { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + + [BackgroundDependencyLoader] + private void load() + { + Console.WriteLine("got here"); + + foreach (var user in streamingClient.PlayingUsers) + { + streamingClient.WatchUser(user); + var resolvedUser = userLookupCache.GetUserAsync(user).Result; + AddPlayer(new BindableDouble(), resolvedUser); + } + } + } +} From 2954218897e90eb54a692063c0e4af5cf882d28c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 15:25:20 +0900 Subject: [PATCH 5382/6909] Add method to ScoreProcessor to calculate score and accuracy from statistics --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 39 +++++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 499673619f..b4f29d7a6e 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -68,7 +68,12 @@ namespace osu.Game.Rulesets.Scoring private readonly double comboPortion; private int maxAchievableCombo; - private double maxBaseScore; + + /// + /// The maximum achievable base score. + /// + public double MaxBaseScore { get; private set; } + private double rollingMaxBaseScore; private double baseScore; @@ -196,7 +201,7 @@ namespace osu.Game.Rulesets.Scoring private double getScore(ScoringMode mode) { return GetScore(mode, maxAchievableCombo, - maxBaseScore > 0 ? baseScore / maxBaseScore : 0, + MaxBaseScore > 0 ? baseScore / MaxBaseScore : 0, maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1, scoreResultCounts); } @@ -227,6 +232,34 @@ namespace osu.Game.Rulesets.Scoring } } + /// + /// Given a minimal set of inputs, return the computed score and accuracy for the tracked beatmap / mods combination. + /// + /// The to compute the total score in. + /// The maximum combo achievable in the beatmap. + /// Statistics to be used for calculating accuracy, bonus score, etc. + /// The computed score and accuracy for provided inputs. + public (double score, double accuracy) GetScoreAndAccuracy(ScoringMode mode, int maxCombo, Dictionary statistics) + { + // calculate base score from statistics pairs + int computedBaseScore = 0; + + foreach (var pair in statistics) + { + if (!pair.Key.AffectsAccuracy()) + continue; + + computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; + } + + double accuracy = MaxBaseScore > 0 ? computedBaseScore / MaxBaseScore : 0; + double comboRatio = maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1; + + double score = GetScore(mode, maxAchievableCombo, accuracy, comboRatio, scoreResultCounts); + + return (score, accuracy); + } + private double getBonusScore(Dictionary statistics) => statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE + statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE; @@ -266,7 +299,7 @@ namespace osu.Game.Rulesets.Scoring if (storeResults) { maxAchievableCombo = HighestCombo.Value; - maxBaseScore = baseScore; + MaxBaseScore = baseScore; } baseScore = 0; From d009a0be51defe7890002ca501d666c40e29fbd9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 15:25:27 +0900 Subject: [PATCH 5383/6909] Move class to final location --- .../HUD/MultiplayerGameplayLeaderboard.cs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs new file mode 100644 index 0000000000..1d9fdd9ded --- /dev/null +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -0,0 +1,98 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Configuration; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.Spectator; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Screens.Play.HUD +{ + public class MultiplayerGameplayLeaderboard : GameplayLeaderboard + { + private readonly ScoreProcessor scoreProcessor; + + /// + /// Construct a new leaderboard. + /// + /// A score processor instance to handle score calculation for scores of users in the match. + public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor) + { + this.scoreProcessor = scoreProcessor; + + AddPlayer(new BindableDouble(), new GuestUser()); + } + + [Resolved] + private SpectatorStreamingClient streamingClient { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + + private readonly Dictionary userScores = new Dictionary(); + + private Bindable scoringMode; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + streamingClient.OnNewFrames += handleIncomingFrames; + + foreach (var user in streamingClient.PlayingUsers) + { + streamingClient.WatchUser(user); + var resolvedUser = userLookupCache.GetUserAsync(user).Result; + + var trackedUser = new TrackedUserData(); + + userScores[user] = trackedUser; + AddPlayer(trackedUser.Score, resolvedUser); + } + + scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); + scoringMode.BindValueChanged(updateAllScores, true); + } + + private void updateAllScores(ValueChangedEvent mode) + { + foreach (var trackedData in userScores.Values) + trackedData.UpdateScore(scoreProcessor, mode.NewValue); + } + + private void handleIncomingFrames(int userId, FrameDataBundle bundle) + { + if (userScores.TryGetValue(userId, out var trackedData)) + { + trackedData.LastHeader = bundle.Header; + trackedData.UpdateScore(scoreProcessor, scoringMode.Value); + } + } + + private class TrackedUserData + { + public readonly BindableDouble Score = new BindableDouble(); + + public readonly BindableDouble Accuracy = new BindableDouble(); + + public readonly BindableInt CurrentCombo = new BindableInt(); + + [CanBeNull] + public FrameHeader LastHeader; + + public void UpdateScore(ScoreProcessor processor, ScoringMode mode) + { + if (LastHeader == null) + return; + + (Score.Value, Accuracy.Value) = processor.GetScoreAndAccuracy(mode, LastHeader.MaxCombo, LastHeader.Statistics.ToDictionary(s => s.Result, s => s.Count)); + CurrentCombo.Value = LastHeader.Combo; + } + } + } +} From 09d0ceb7668e115170437c1d2063ed883897ed02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 15:26:03 +0900 Subject: [PATCH 5384/6909] Add testing setup to get a better visual idea of how scoreboard will work fixup! Add method to ScoreProcessor to calculate score and accuracy from statistics --- .../TestSceneSpectatorDrivenLeaderboard.cs | 109 +++++++++++++----- .../HUD/MultiplayerGameplayLeaderboard.cs | 4 +- 2 files changed, 85 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs index 7211755ba6..e7ebf6e92c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs @@ -2,14 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; +using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Database; -using osu.Game.Online.API; +using osu.Framework.Utils; using osu.Game.Online.Spectator; +using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu.Scoring; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.Gameplay @@ -17,7 +21,7 @@ namespace osu.Game.Tests.Visual.Gameplay public class TestSceneSpectatorDrivenLeaderboard : OsuTestScene { [Cached(typeof(SpectatorStreamingClient))] - private TestSceneSpectator.TestSpectatorStreamingClient testSpectatorStreamingClient = new TestSceneSpectator.TestSpectatorStreamingClient(); + private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16); // used just to show beatmap card for the time being. protected override bool UseOnlineAPI => true; @@ -27,7 +31,7 @@ namespace osu.Game.Tests.Visual.Gameplay { OsuScoreProcessor scoreProcessor; - testSpectatorStreamingClient.StartPlay(55); + streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); Children = new Drawable[] { @@ -38,37 +42,90 @@ namespace osu.Game.Tests.Visual.Gameplay Origin = Anchor.Centre, } }; + + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + + var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + + scoreProcessor.ApplyBeatmap(playable); }); - } - public class MultiplayerGameplayLeaderboard : GameplayLeaderboard - { - private readonly OsuScoreProcessor scoreProcessor; - - public MultiplayerGameplayLeaderboard(OsuScoreProcessor scoreProcessor) + [Test] + public void TestScoreUpdates() { - this.scoreProcessor = scoreProcessor; - - AddPlayer(new BindableDouble(), new GuestUser()); + AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100); } - [Resolved] - private SpectatorStreamingClient streamingClient { get; set; } - - [Resolved] - private UserLookupCache userLookupCache { get; set; } - - [BackgroundDependencyLoader] - private void load() + public class TestMultiplayerStreaming : SpectatorStreamingClient { - Console.WriteLine("got here"); + public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; - foreach (var user in streamingClient.PlayingUsers) + private readonly int totalUsers; + + public TestMultiplayerStreaming(int totalUsers) { - streamingClient.WatchUser(user); - var resolvedUser = userLookupCache.GetUserAsync(user).Result; - AddPlayer(new BindableDouble(), resolvedUser); + this.totalUsers = totalUsers; } + + public void Start(int beatmapId) + { + for (int i = 0; i < totalUsers; i++) + { + ((ISpectatorClient)this).UserBeganPlaying(i, new SpectatorState + { + BeatmapID = beatmapId, + RulesetID = 0, + }); + } + } + + private readonly Dictionary lastHeaders = new Dictionary(); + + public void RandomlyUpdateState() + { + foreach (var userId in PlayingUsers) + { + if (RNG.Next(0, 1) == 1) + continue; + + if (!lastHeaders.TryGetValue(userId, out var header)) + { + lastHeaders[userId] = header = new FrameHeader(new ScoreInfo + { + Statistics = new Dictionary(new[] + { + new KeyValuePair(HitResult.Miss, 0), + new KeyValuePair(HitResult.Meh, 0), + new KeyValuePair(HitResult.Great, 0) + }) + }); + } + + switch (RNG.Next(0, 3)) + { + case 0: + header.Combo = 0; + header.Statistics[HitResult.Miss]++; + break; + + case 1: + header.Combo++; + header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); + header.Statistics[HitResult.Meh]++; + break; + + default: + header.Combo++; + header.MaxCombo = Math.Max(header.MaxCombo, header.Combo); + header.Statistics[HitResult.Great]++; + break; + } + + ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty())); + } + } + + protected override Task Connect() => Task.CompletedTask; } } } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 1d9fdd9ded..b3621f42c2 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -90,7 +89,8 @@ namespace osu.Game.Screens.Play.HUD if (LastHeader == null) return; - (Score.Value, Accuracy.Value) = processor.GetScoreAndAccuracy(mode, LastHeader.MaxCombo, LastHeader.Statistics.ToDictionary(s => s.Result, s => s.Count)); + (Score.Value, Accuracy.Value) = processor.GetScoreAndAccuracy(mode, LastHeader.MaxCombo, LastHeader.Statistics); + CurrentCombo.Value = LastHeader.Combo; } } From c1ba0f46425e756177e6c383d7f2e42b9d176c80 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 15:53:05 +0900 Subject: [PATCH 5385/6909] Use a local lookup cache for better usernames --- .../TestSceneSpectatorDrivenLeaderboard.cs | 7 +++++ .../TestSceneCurrentlyPlayingDisplay.cs | 28 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs index e7ebf6e92c..647d57b5fe 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs @@ -9,12 +9,14 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Utils; +using osu.Game.Database; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.Online; namespace osu.Game.Tests.Visual.Gameplay { @@ -23,6 +25,9 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(SpectatorStreamingClient))] private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16); + [Cached(typeof(UserLookupCache))] + private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); + // used just to show beatmap card for the time being. protected override bool UseOnlineAPI => true; @@ -35,6 +40,8 @@ namespace osu.Game.Tests.Visual.Gameplay Children = new Drawable[] { + streamingClient, + lookupCache, scoreProcessor = new OsuScoreProcessor(), new MultiplayerGameplayLeaderboard(scoreProcessor) { diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 7eba64f418..4f0ca67e64 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -69,8 +69,34 @@ namespace osu.Game.Tests.Visual.Online internal class TestUserLookupCache : UserLookupCache { + private static readonly string[] usernames = + { + "fieryrage", + "Kerensa", + "MillhioreF", + "Player01", + "smoogipoo", + "Ephemeral", + "BTMC", + "Cilvery", + "m980", + "HappyStick", + "LittleEndu", + "frenzibyte", + "Zallius", + "BanchoBot", + "rocketminer210", + "pishifat" + }; + + private int id; + protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) - => Task.FromResult(new User { Username = "peppy", Id = 2 }); + => Task.FromResult(new User + { + Id = id++, + Username = usernames[id % usernames.Length], + }); } } } From 6e2131c164ad2a2d45ec7bab53d58deae3279de7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 15:53:15 +0900 Subject: [PATCH 5386/6909] Don't track local user score in any special way --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index b3621f42c2..f8fce0825f 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; @@ -24,8 +23,6 @@ namespace osu.Game.Screens.Play.HUD public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor) { this.scoreProcessor = scoreProcessor; - - AddPlayer(new BindableDouble(), new GuestUser()); } [Resolved] From 6bce587b599c04194589c8f3bd2716a44f886ea3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 16:05:46 +0900 Subject: [PATCH 5387/6909] Pass users in via constructor and correctly unbind on disposal --- .../TestSceneSpectatorDrivenLeaderboard.cs | 3 +- .../HUD/MultiplayerGameplayLeaderboard.cs | 32 ++++++++++++++++--- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs index 647d57b5fe..ffd02d247a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; @@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Gameplay streamingClient, lookupCache, scoreProcessor = new OsuScoreProcessor(), - new MultiplayerGameplayLeaderboard(scoreProcessor) + new MultiplayerGameplayLeaderboard(scoreProcessor, streamingClient.PlayingUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index f8fce0825f..93f258c507 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -16,13 +16,22 @@ namespace osu.Game.Screens.Play.HUD { private readonly ScoreProcessor scoreProcessor; + private readonly int[] userIds; + + private readonly Dictionary userScores = new Dictionary(); + /// /// Construct a new leaderboard. /// /// A score processor instance to handle score calculation for scores of users in the match. - public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor) + /// IDs of all users in this match. + public MultiplayerGameplayLeaderboard(ScoreProcessor scoreProcessor, int[] userIds) { + // todo: this will eventually need to be created per user to support different mod combinations. this.scoreProcessor = scoreProcessor; + + // todo: this will likely be passed in as User instances. + this.userIds = userIds; } [Resolved] @@ -31,8 +40,6 @@ namespace osu.Game.Screens.Play.HUD [Resolved] private UserLookupCache userLookupCache { get; set; } - private readonly Dictionary userScores = new Dictionary(); - private Bindable scoringMode; [BackgroundDependencyLoader] @@ -40,9 +47,11 @@ namespace osu.Game.Screens.Play.HUD { streamingClient.OnNewFrames += handleIncomingFrames; - foreach (var user in streamingClient.PlayingUsers) + foreach (var user in userIds) { streamingClient.WatchUser(user); + + // probably won't be required in the final implementation. var resolvedUser = userLookupCache.GetUserAsync(user).Result; var trackedUser = new TrackedUserData(); @@ -70,6 +79,21 @@ namespace osu.Game.Screens.Play.HUD } } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (streamingClient != null) + { + foreach (var user in userIds) + { + streamingClient.StopWatchingUser(user); + } + + streamingClient.OnNewFrames -= handleIncomingFrames; + } + } + private class TrackedUserData { public readonly BindableDouble Score = new BindableDouble(); From a01bb3d5a3a7ee33650de9b305addf61a1daa16f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 16:08:44 +0900 Subject: [PATCH 5388/6909] Better limit bindable exposure of data class --- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 2 +- .../Play/HUD/MultiplayerGameplayLeaderboard.cs | 16 +++++++++++----- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index e53c56b390..3934a99221 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Play.HUD /// /// The bindable current score of the player. /// The player. - public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user) + public void AddPlayer([NotNull] IBindableNumber currentScore, [NotNull] User user) { var scoreItem = addScore(currentScore.Value, user); currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue; diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 93f258c507..33bcf06aa7 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -96,11 +96,17 @@ namespace osu.Game.Screens.Play.HUD private class TrackedUserData { - public readonly BindableDouble Score = new BindableDouble(); + public IBindableNumber Score => score; - public readonly BindableDouble Accuracy = new BindableDouble(); + private readonly BindableDouble score = new BindableDouble(); - public readonly BindableInt CurrentCombo = new BindableInt(); + public IBindableNumber Accuracy => accuracy; + + private readonly BindableDouble accuracy = new BindableDouble(); + + public IBindableNumber CurrentCombo => currentCombo; + + private readonly BindableInt currentCombo = new BindableInt(); [CanBeNull] public FrameHeader LastHeader; @@ -110,9 +116,9 @@ namespace osu.Game.Screens.Play.HUD if (LastHeader == null) return; - (Score.Value, Accuracy.Value) = processor.GetScoreAndAccuracy(mode, LastHeader.MaxCombo, LastHeader.Statistics); + (score.Value, accuracy.Value) = processor.GetScoreAndAccuracy(mode, LastHeader.MaxCombo, LastHeader.Statistics); - CurrentCombo.Value = LastHeader.Combo; + currentCombo.Value = LastHeader.Combo; } } } From cda3bd2017180391786a46ef771c4b3e04dc4cb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 16:22:47 +0900 Subject: [PATCH 5389/6909] Rename test scene to match tested class name --- ...erboard.cs => TestSceneMultiplayerGameplayLeaderboard.cs} | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) rename osu.Game.Tests/Visual/Gameplay/{TestSceneSpectatorDrivenLeaderboard.cs => TestSceneMultiplayerGameplayLeaderboard.cs} (96%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs similarity index 96% rename from osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs rename to osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs index ffd02d247a..78f9db57be 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorDrivenLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -21,7 +21,7 @@ using osu.Game.Tests.Visual.Online; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneSpectatorDrivenLeaderboard : OsuTestScene + public class TestSceneMultiplayerGameplayLeaderboard : OsuTestScene { [Cached(typeof(SpectatorStreamingClient))] private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16); @@ -29,9 +29,6 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); - // used just to show beatmap card for the time being. - protected override bool UseOnlineAPI => true; - [SetUp] public void SetUp() => Schedule(() => { From c0021847686aa1c3022abef0dabaa858b2cc8464 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 16 Dec 2020 18:08:16 +0900 Subject: [PATCH 5390/6909] Clamp osu!mania's HitPosition offset to match osu-stable implementation Closes #11184. --- osu.Game/Skinning/LegacyManiaSkinDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs index 6a04a95040..0a1de461ea 100644 --- a/osu.Game/Skinning/LegacyManiaSkinDecoder.cs +++ b/osu.Game/Skinning/LegacyManiaSkinDecoder.cs @@ -87,7 +87,7 @@ namespace osu.Game.Skinning break; case "HitPosition": - currentConfig.HitPosition = (480 - float.Parse(pair.Value, CultureInfo.InvariantCulture)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; + currentConfig.HitPosition = (480 - Math.Clamp(float.Parse(pair.Value, CultureInfo.InvariantCulture), 240, 480)) * LegacyManiaSkinConfiguration.POSITION_SCALE_FACTOR; break; case "LightPosition": From 95711578415c95d6fcc52498e88c55da0712d40f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 16 Dec 2020 20:04:28 +0900 Subject: [PATCH 5391/6909] Use ints for userid parameters --- osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs | 4 ++-- osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs index c162f066d4..9af0047137 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs @@ -32,7 +32,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Signal that the host of the room has changed. /// /// The user ID of the new host. - Task HostChanged(long userId); + Task HostChanged(int userId); /// /// Signals that the settings for this room have changed. @@ -45,7 +45,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The ID of the user performing a state change. /// The new state of the user. - Task UserStateChanged(long userId, MultiplayerUserState state); + Task UserStateChanged(int userId, MultiplayerUserState state); /// /// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point. diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs index f1b3daf7d3..12dfe481c4 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// The new user which is to become host. /// A user other than the current host is attempting to transfer host. /// If the user is not in a room. - Task TransferHost(long userId); + Task TransferHost(int userId); /// /// As the host, update the settings of the currently joined room. From 5d7294451f1a5a24a7eabdf7a738e6fc07278b17 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 16 Dec 2020 14:28:16 +0100 Subject: [PATCH 5392/6909] Refactor Import() overload to take a list of import tasks instead. --- osu.Android/OsuGameActivity.cs | 3 ++- osu.Game/Database/ArchiveModelManager.cs | 4 ++-- osu.Game/Database/ICanAcceptFiles.cs | 7 ++----- osu.Game/OsuGame.cs | 6 +++--- osu.Game/OsuGameBase.cs | 6 +++--- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 2 +- 6 files changed, 13 insertions(+), 15 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index e801c2ca6e..bd5523f0e2 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -11,6 +11,7 @@ using Android.OS; using Android.Provider; using Android.Views; using osu.Framework.Android; +using osu.Game.Database; namespace osu.Android { @@ -82,7 +83,7 @@ namespace osu.Android using (var stream = ContentResolver.OpenInputStream(uri)) await stream.CopyToAsync(copy); - await game.Import(copy, filename); + await game.Import(new ImportTask(copy, filename)); }, TaskCreationOptions.LongRunning); } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 36cc4cce39..9f69ad035f 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -115,13 +115,13 @@ namespace osu.Game.Database return Import(notification, paths.Select(p => new ImportTask(p)).ToArray()); } - public Task Import(Stream stream, string filename) + public Task Import(params ImportTask[] tasks) { var notification = new ProgressNotification { State = ProgressNotificationState.Active }; PostNotification?.Invoke(notification); - return Import(notification, new ImportTask(stream, filename)); + return Import(notification, tasks); } protected async Task> Import(ProgressNotification notification, params ImportTask[] tasks) diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index 276c284c9f..5ec187975a 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; -using System.IO; using System.Threading.Tasks; namespace osu.Game.Database @@ -19,11 +18,9 @@ namespace osu.Game.Database Task Import(params string[] paths); /// - /// Import the provided stream as a simple item. + /// Import the specified files from the given import tasks. /// - /// The stream to import files from. Should be in a supported archive format. - /// The filename of the archive being imported. - Task Import(Stream stream, string filename); + Task Import(params ImportTask[] tasks); /// /// An array of accepted file extensions (in the standard format of ".abc"). diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d67d790ce2..1f5b991758 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -52,7 +52,7 @@ using osu.Game.Updater; using osu.Game.Utils; using LogLevel = osu.Framework.Logging.LogLevel; using osu.Game.Users; -using System.IO; +using osu.Game.Database; namespace osu.Game { @@ -427,10 +427,10 @@ namespace osu.Game }, validScreens: new[] { typeof(PlaySongSelect) }); } - public override Task Import(Stream stream, string filename) + public override Task Import(params ImportTask[] imports) { // encapsulate task as we don't want to begin the import process until in a ready state. - var importTask = new Task(async () => await base.Import(stream, filename)); + var importTask = new Task(async () => await base.Import(imports)); waitForReady(() => this, _ => importTask.Start()); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 150569f1dd..0a579cc347 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -395,14 +395,14 @@ namespace osu.Game } } - public virtual async Task Import(Stream stream, string filename) + public virtual async Task Import(params ImportTask[] tasks) { - var extension = Path.GetExtension(filename)?.ToLowerInvariant(); + var extension = Path.GetExtension(tasks.First().Path)?.ToLowerInvariant(); foreach (var importer in fileImporters) { if (importer.HandledExtensions.Contains(extension)) - await importer.Import(stream, Path.GetFileNameWithoutExtension(filename)); + await importer.Import(tasks); } } diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 0c957b80af..fe9b10667a 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -100,7 +100,7 @@ namespace osu.Game.Screens.Edit.Setup return Task.CompletedTask; } - Task ICanAcceptFiles.Import(Stream stream, string filename) => throw new NotImplementedException(); + Task ICanAcceptFiles.Import(params ImportTask[] tasks) => throw new NotImplementedException(); protected override void LoadComplete() { From 9d8906924580cc3886759de27dbbb05fb5460b00 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 16 Dec 2020 20:33:29 +0100 Subject: [PATCH 5393/6909] Add ability to import multiple files at once on android. --- osu.Android/OsuGameActivity.cs | 63 ++++++++++++++++++++++++---------- 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index bd5523f0e2..bf73f33b74 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.IO; +using System.Linq; using System.Threading.Tasks; using Android.App; using Android.Content; @@ -16,7 +18,7 @@ using osu.Game.Database; namespace osu.Android { [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] - [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] + [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] public class OsuGameActivity : AndroidGameActivity { private OsuGameAndroid game; @@ -49,41 +51,64 @@ namespace osu.Android { case Intent.ActionDefault: if (intent.Scheme == ContentResolver.SchemeContent) - handleImportFromUri(intent.Data); + handleImportFromUris(intent.Data); break; case Intent.ActionSend: { var content = intent.ClipData?.GetItemAt(0); if (content != null) - handleImportFromUri(content.Uri); + handleImportFromUris(content.Uri); + break; + } + + case Intent.ActionSendMultiple: + { + var uris = new List(); + for (int i = 0; i < intent.ClipData?.ItemCount; i++) + { + var content = intent.ClipData?.GetItemAt(i); + if (content != null) + uris.Add(content.Uri); + } + handleImportFromUris(uris.ToArray()); break; } } } - private void handleImportFromUri(Uri uri) => Task.Factory.StartNew(async () => + private void handleImportFromUris(params Uri[] uris) => Task.Factory.StartNew(async () => { - // there are more performant overloads of this method, but this one is the most backwards-compatible - // (dates back to API 1). - var cursor = ContentResolver?.Query(uri, null, null, null, null); + var tasks = new List(); - if (cursor == null) - return; + await Task.WhenAll(uris.Select(async uri => + { + // there are more performant overloads of this method, but this one is the most backwards-compatible + // (dates back to API 1). + var cursor = ContentResolver?.Query(uri, null, null, null, null); - cursor.MoveToFirst(); + if (cursor == null) + return; - var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); - string filename = cursor.GetString(filenameColumn); + cursor.MoveToFirst(); - // SharpCompress requires archive streams to be seekable, which the stream opened by - // OpenInputStream() seems to not necessarily be. - // copy to an arbitrary-access memory stream to be able to proceed with the import. - var copy = new MemoryStream(); - using (var stream = ContentResolver.OpenInputStream(uri)) - await stream.CopyToAsync(copy); + var filenameColumn = cursor.GetColumnIndex(OpenableColumns.DisplayName); + string filename = cursor.GetString(filenameColumn); - await game.Import(new ImportTask(copy, filename)); + // SharpCompress requires archive streams to be seekable, which the stream opened by + // OpenInputStream() seems to not necessarily be. + // copy to an arbitrary-access memory stream to be able to proceed with the import. + var copy = new MemoryStream(); + using (var stream = ContentResolver.OpenInputStream(uri)) + await stream.CopyToAsync(copy); + + lock (tasks) + { + tasks.Add(new ImportTask(copy, filename)); + } + })); + + await game.Import(tasks.ToArray()); }, TaskCreationOptions.LongRunning); } } From cc0442a9a17880bd6f427638f831f97f6f50cb50 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 16 Dec 2020 20:42:30 +0100 Subject: [PATCH 5394/6909] Fix CI inspections. --- osu.Game/OsuGameBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 0a579cc347..521778a9cd 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -397,7 +397,7 @@ namespace osu.Game public virtual async Task Import(params ImportTask[] tasks) { - var extension = Path.GetExtension(tasks.First().Path)?.ToLowerInvariant(); + var extension = Path.GetExtension(tasks.First().Path).ToLowerInvariant(); foreach (var importer in fileImporters) { From 41d8b84bd7c7aff099593a034c8dd71ee1d0ae13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Dec 2020 15:47:20 +0900 Subject: [PATCH 5395/6909] Revert MaxBaseScore to being a private field (no longe required to be public) --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index b4f29d7a6e..f4850d3325 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -72,7 +72,7 @@ namespace osu.Game.Rulesets.Scoring /// /// The maximum achievable base score. /// - public double MaxBaseScore { get; private set; } + private double maxBaseScore; private double rollingMaxBaseScore; private double baseScore; @@ -201,7 +201,7 @@ namespace osu.Game.Rulesets.Scoring private double getScore(ScoringMode mode) { return GetScore(mode, maxAchievableCombo, - MaxBaseScore > 0 ? baseScore / MaxBaseScore : 0, + maxBaseScore > 0 ? baseScore / maxBaseScore : 0, maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1, scoreResultCounts); } @@ -252,7 +252,7 @@ namespace osu.Game.Rulesets.Scoring computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; } - double accuracy = MaxBaseScore > 0 ? computedBaseScore / MaxBaseScore : 0; + double accuracy = maxBaseScore > 0 ? computedBaseScore / maxBaseScore : 0; double comboRatio = maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1; double score = GetScore(mode, maxAchievableCombo, accuracy, comboRatio, scoreResultCounts); @@ -299,7 +299,7 @@ namespace osu.Game.Rulesets.Scoring if (storeResults) { maxAchievableCombo = HighestCombo.Value; - MaxBaseScore = baseScore; + maxBaseScore = baseScore; } baseScore = 0; From de9c21e7d19e265fbf776430d2708f860ed16bbf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Dec 2020 15:48:53 +0900 Subject: [PATCH 5396/6909] Tenatively mark leaderboard class as LongRunningLoad until final integration --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 33bcf06aa7..b2dc47ce61 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -12,6 +12,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Screens.Play.HUD { + [LongRunningLoad] public class MultiplayerGameplayLeaderboard : GameplayLeaderboard { private readonly ScoreProcessor scoreProcessor; From cc3dddf59fa432bad0fc2d36fe8810853223f48f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Dec 2020 16:02:06 +0900 Subject: [PATCH 5397/6909] Fix test scene crashing on second run of SetUp Also correctly support LongRunningLoad --- ...TestSceneMultiplayerGameplayLeaderboard.cs | 53 +++++++++++++------ 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs index 78f9db57be..0c8d8ca165 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -9,6 +9,8 @@ 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.Framework.Utils; using osu.Game.Database; using osu.Game.Online.Spectator; @@ -29,31 +31,48 @@ namespace osu.Game.Tests.Visual.Gameplay [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); - [SetUp] - public void SetUp() => Schedule(() => + private MultiplayerGameplayLeaderboard leaderboard; + + protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + + public TestSceneMultiplayerGameplayLeaderboard() { - OsuScoreProcessor scoreProcessor; - - streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); - - Children = new Drawable[] + base.Content.Children = new Drawable[] { streamingClient, lookupCache, - scoreProcessor = new OsuScoreProcessor(), - new MultiplayerGameplayLeaderboard(scoreProcessor, streamingClient.PlayingUsers.ToArray()) + Content + }; + } + + [SetUpSteps] + public void SetUpSteps() + { + AddStep("create leaderboard", () => + { + OsuScoreProcessor scoreProcessor; + Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); + + var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); + + streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + + Children = new Drawable[] + { + scoreProcessor = new OsuScoreProcessor(), + }; + + scoreProcessor.ApplyBeatmap(playable); + + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(scoreProcessor, streamingClient.PlayingUsers.ToArray()) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - } - }; + }, Add); + }); - Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); - - var playable = Beatmap.Value.GetPlayableBeatmap(Ruleset.Value); - - scoreProcessor.ApplyBeatmap(playable); - }); + AddUntilStep("wait for load", () => leaderboard.IsLoaded); + } [Test] public void TestScoreUpdates() From f13683dc908e2f42b19d2a1fb6b05f808eb01b36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Dec 2020 16:05:41 +0900 Subject: [PATCH 5398/6909] Correctly account for max combo of the input, rather than the global --- 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 f4850d3325..f5fb918ba3 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -253,7 +253,7 @@ namespace osu.Game.Rulesets.Scoring } double accuracy = maxBaseScore > 0 ? computedBaseScore / maxBaseScore : 0; - double comboRatio = maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1; + double comboRatio = maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1; double score = GetScore(mode, maxAchievableCombo, accuracy, comboRatio, scoreResultCounts); From 81b0db040179821af0928ae8389a85f5666f3f4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Dec 2020 16:14:41 +0900 Subject: [PATCH 5399/6909] Remove double construction of empty replay object --- osu.Game/Screens/Play/Player.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f40a7ccda8..18950a9d0a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -21,7 +21,6 @@ using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; 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; @@ -164,7 +163,7 @@ namespace osu.Game.Screens.Play /// protected virtual void PrepareReplay() { - DrawableRuleset.SetRecordTarget(recordingScore = new Score { Replay = new Replay() }); + DrawableRuleset.SetRecordTarget(recordingScore = new Score()); ScoreProcessor.NewJudgement += result => ScoreProcessor.PopulateScore(recordingScore.ScoreInfo); } From 3ff70d331adf2e87451b3bcb8306c39140d9a7e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 17 Dec 2020 16:17:13 +0900 Subject: [PATCH 5400/6909] Mark recordingScore as nullable --- osu.Game/Screens/Play/Player.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 18950a9d0a..a54f9fc047 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.IO; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -156,6 +157,7 @@ namespace osu.Game.Screens.Play PrepareReplay(); } + [CanBeNull] private Score recordingScore; /// From 78ce6f1cd21e81858f7f923dab907e52dde020e9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Dec 2020 13:30:55 +0300 Subject: [PATCH 5401/6909] Add friends list to API providers --- osu.Game/Online/API/APIAccess.cs | 14 ++++++++++++-- osu.Game/Online/API/DummyAPIAccess.cs | 7 +++++++ osu.Game/Online/API/IAPIProvider.cs | 6 ++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index b916339a53..f084f6f2c7 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -41,6 +41,8 @@ namespace osu.Game.Online.API public Bindable LocalUser { get; } = new Bindable(createGuestUser()); + public BindableList Friends { get; } = new BindableList(); + public Bindable Activity { get; } = new Bindable(); protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); @@ -143,6 +145,10 @@ namespace osu.Game.Online.API failureCount = 0; + var friendsReq = new GetFriendsRequest(); + friendsReq.Success += f => Friends.AddRange(f); + handleRequest(friendsReq); + //we're connected! state.Value = APIState.Online; }; @@ -352,8 +358,12 @@ namespace osu.Game.Online.API password = null; authentication.Clear(); - // Scheduled prior to state change such that the state changed event is invoked with the correct user present - Schedule(() => LocalUser.Value = createGuestUser()); + // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present + Schedule(() => + { + LocalUser.Value = createGuestUser(); + Friends.Clear(); + }); state.Value = APIState.Offline; } diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index e275676cea..7e5a6378ec 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -5,6 +5,7 @@ using System; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; +using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Users; @@ -18,6 +19,12 @@ namespace osu.Game.Online.API Id = 1001, }); + public BindableList Friends { get; } = new BindableList(new User + { + Username = @"Dummy's friend", + Id = 2002, + }.Yield()); + public Bindable Activity { get; } = new Bindable(); public string AccessToken => "token"; diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index cadc806f4f..3f62b37a48 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -15,6 +15,12 @@ namespace osu.Game.Online.API /// Bindable LocalUser { get; } + /// + /// The user's friends. + /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. + /// + BindableList Friends { get; } + /// /// The current user's activity. /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. From 449b9a21aefe912280728fd621fe11c782fbdb28 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Dec 2020 13:31:57 +0300 Subject: [PATCH 5402/6909] Allow OverlayView fetching with no API requests required --- osu.Game/Overlays/OverlayView.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index c254cdf290..f5ef183bd5 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -42,25 +42,29 @@ namespace osu.Game.Overlays /// /// Create the API request for fetching data. /// - protected abstract APIRequest CreateRequest(); + protected virtual APIRequest CreateRequest() => null; /// /// Fired when results arrive from the main API request. /// /// - protected abstract void OnSuccess(T response); + protected virtual void OnSuccess(T response) + { + } /// /// Force a re-request for data from the API. /// - protected void PerformFetch() + protected virtual void PerformFetch() { request?.Cancel(); - request = CreateRequest(); - request.Success += response => Schedule(() => OnSuccess(response)); - API.Queue(request); + if (request != null) + { + request.Success += response => Schedule(() => OnSuccess(response)); + API.Queue(request); + } } private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => From 94175d053289d944765c24d3e486f402929692e0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Dec 2020 13:33:17 +0300 Subject: [PATCH 5403/6909] Use global friends list instead of always fetching --- osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 41b25ee1a5..8a8ac36e14 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -9,8 +9,6 @@ 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.Users; using osuTK; @@ -143,11 +141,11 @@ namespace osu.Game.Overlays.Dashboard.Friends userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); } - protected override APIRequest> CreateRequest() => new GetFriendsRequest(); - - protected override void OnSuccess(List response) + protected override void PerformFetch() { - Users = response; + base.PerformFetch(); + + Users = API.Friends.ToList(); } private void recreatePanels() From 904a4daa985b8f5e37573520a9147ad9adb80167 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Dec 2020 13:33:49 +0300 Subject: [PATCH 5404/6909] Add todo comment reminding of updating friends list along --- osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs b/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs index 6e1b6e2c7d..6c2b2dc16a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs @@ -50,6 +50,8 @@ namespace osu.Game.Overlays.Profile.Header.Components } }; + // todo: when friending/unfriending is implemented, the APIAccess.Friends list should be updated accordingly. + User.BindValueChanged(user => updateFollowers(user.NewValue), true); } From 5d180753fa3cbc40971c867e5d1b36cdf1484306 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Dec 2020 13:44:30 +0300 Subject: [PATCH 5405/6909] Complete connection once friends list is succesfully fetched --- osu.Game/Online/API/APIAccess.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index f084f6f2c7..806a42a38b 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -146,11 +146,15 @@ namespace osu.Game.Online.API failureCount = 0; var friendsReq = new GetFriendsRequest(); - friendsReq.Success += f => Friends.AddRange(f); - handleRequest(friendsReq); + friendsReq.Success += f => + { + Friends.AddRange(f); - //we're connected! - state.Value = APIState.Online; + //we're connected! + state.Value = APIState.Online; + }; + + handleRequest(friendsReq); }; if (!handleRequest(userReq)) From 0faf3fdfd3fc4f96d137d1e8824398383e441ace Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 17 Dec 2020 15:12:32 +0300 Subject: [PATCH 5406/6909] Update gameplay leaderboard scores with the new design --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 11 +- .../Screens/Play/HUD/GameplayLeaderboard.cs | 47 +-- .../Play/HUD/GameplayLeaderboardScore.cs | 268 ++++++++++++------ 3 files changed, 215 insertions(+), 111 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index df970c1c46..d0fdb3dd9c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -24,9 +24,8 @@ namespace osu.Game.Tests.Visual.Gameplay Add(leaderboard = new TestGameplayLeaderboard { Anchor = Anchor.Centre, - Origin = Anchor.Centre, + Origin = Anchor.TopCentre, Scale = new Vector2(2), - RelativeSizeAxes = Axes.X, }); } @@ -39,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay playerScore.Value = 1222333; }); - AddStep("add player user", () => leaderboard.AddPlayer(playerScore, new User { Username = "You" })); + AddStep("add player user", () => leaderboard.AddLocalUser(playerScore, new User { Username = "You" })); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } @@ -75,6 +74,12 @@ namespace osu.Game.Tests.Visual.Gameplay return scoreItem != null && scoreItem.ScorePosition == expectedPosition; } + + public void AddPlayer(BindableDouble totalScore, User user) => + base.AddPlayer(totalScore, new BindableDouble(1f), new BindableInt(1), user, false); + + public void AddLocalUser(BindableDouble totalScore, User user) => + base.AddPlayer(totalScore, new BindableDouble(1f), new BindableInt(1), user, true); } } } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index e53c56b390..a2673e3097 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -15,8 +15,7 @@ namespace osu.Game.Screens.Play.HUD { public GameplayLeaderboard() { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; + AutoSizeAxes = Axes.Both; Direction = FillDirection.Vertical; @@ -29,32 +28,44 @@ namespace osu.Game.Screens.Play.HUD /// /// Adds a player to the leaderboard. /// - /// The bindable current score of the player. + /// A bindable of the player's total score. + /// A bindable of the player's accuracy. + /// A bindable of the player's current combo. /// The player. - public void AddPlayer([NotNull] BindableDouble currentScore, [NotNull] User user) + public void AddPlayer([NotNull] IBindableNumber totalScore, + [NotNull] IBindableNumber accuracy, + [NotNull] IBindableNumber combo, + [NotNull] User user) { - var scoreItem = addScore(currentScore.Value, user); - currentScore.ValueChanged += s => scoreItem.TotalScore = s.NewValue; + AddPlayer(totalScore, accuracy, combo, user, false); } - private GameplayLeaderboardScore addScore(double totalScore, User user) + /// + /// Adds a player to the leaderboard. + /// + /// A bindable of the player's total score. + /// A bindable of the player's accuracy. + /// A bindable of the player's current combo. + /// The player. + /// Whether the provided is the local user. + protected void AddPlayer([NotNull] IBindableNumber totalScore, + [NotNull] IBindableNumber accuracy, + [NotNull] IBindableNumber combo, + [NotNull] User user, bool localUser) => Schedule(() => { - var scoreItem = new GameplayLeaderboardScore + Add(new GameplayLeaderboardScore(user, localUser) { - User = user, - TotalScore = totalScore, - OnScoreChange = updateScores, - }; + TotalScore = { BindTarget = totalScore }, + Accuracy = { BindTarget = accuracy }, + Combo = { BindTarget = combo }, + }); - Add(scoreItem); - updateScores(); - - return scoreItem; - } + totalScore.BindValueChanged(_ => updateScores(), true); + }); private void updateScores() { - var orderedByScore = this.OrderByDescending(i => i.TotalScore).ToList(); + var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList(); for (int i = 0; i < Count; i++) { diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 4c75f422c9..62ea5782e8 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -1,25 +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.Linq; 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.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; using osu.Game.Users; +using osu.Game.Utils; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { public class GameplayLeaderboardScore : CompositeDrawable { - private readonly OsuSpriteText positionText, positionSymbol, userString; - private readonly GlowingSpriteText scoreText; + private const float regular_width = 215f; + private const float extended_width = 235f; - public Action OnScoreChange; + private const float panel_height = 35f; + + private OsuSpriteText positionText, scoreText, accuracyText, comboText; + public readonly BindableDouble TotalScore = new BindableDouble(); + public readonly BindableDouble Accuracy = new BindableDouble(); + public readonly BindableInt Combo = new BindableInt(); private int? scorePosition; @@ -34,103 +44,181 @@ namespace osu.Game.Screens.Play.HUD positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}"; positionText.FadeTo(scorePosition.HasValue ? 1 : 0); - positionSymbol.FadeTo(scorePosition.HasValue ? 1 : 0); } } - private double totalScore; + public User User { get; } - public double TotalScore + private readonly bool localUser; + + public GameplayLeaderboardScore(User user, bool localUser) { - get => totalScore; - set - { - totalScore = value; - scoreText.Text = totalScore.ToString("N0"); + User = user; + this.localUser = localUser; - OnScoreChange?.Invoke(); - } - } + AutoSizeAxes = Axes.Both; - private User user; - - public User User - { - get => user; - set - { - user = value; - userString.Text = user?.Username; - } - } - - public GameplayLeaderboardScore() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - - InternalChild = new Container - { - Masking = true, - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Right = 2.5f }, - Spacing = new Vector2(2.5f), - Children = new[] - { - positionText = new OsuSpriteText - { - Alpha = 0, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold), - }, - positionSymbol = new OsuSpriteText - { - Alpha = 0, - Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold), - Text = ">", - }, - } - }, - new FillFlowContainer - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Left = 2.5f }, - Spacing = new Vector2(2.5f), - Children = new Drawable[] - { - userString = new OsuSpriteText - { - Size = new Vector2(80, 16), - Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold, italics: true), - }, - scoreText = new GlowingSpriteText - { - GlowColour = Color4Extensions.FromHex(@"83ccfa"), - Font = OsuFont.Numeric.With(size: 14), - } - } - }, - }, - }; + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; } [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(IAPIProvider api) { - positionText.Colour = colours.YellowLight; - positionSymbol.Colour = colours.Yellow; + const float panel_shear = 0.15f; + const float shear_width = panel_height * panel_shear; + + Color4 panelColour, textColour; + float panelWidth; + + if (localUser) + { + panelWidth = extended_width; + panelColour = Color4Extensions.FromHex("7fcc33"); + textColour = Color4.White; + } + else if (api.Friends.Any(f => User.Equals(f))) + { + panelWidth = extended_width; + panelColour = Color4Extensions.FromHex("ffd966"); + textColour = Color4Extensions.FromHex("2e576b"); + } + else + { + panelWidth = regular_width; + panelColour = Color4Extensions.FromHex("3399cc"); + textColour = Color4.White; + } + + InternalChildren = new Drawable[] + { + new Container + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Masking = true, + CornerRadius = 5f, + Shear = new Vector2(panel_shear, 0f), + Size = new Vector2(panelWidth, panel_height), + Child = new Box + { + Alpha = 0.5f, + RelativeSizeAxes = Axes.Both, + Colour = panelColour, + } + }, + new GridContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Size = new Vector2(regular_width, panel_height), + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, 35f), + new Dimension(), + new Dimension(GridSizeMode.Absolute, 85f), + }, + Content = new[] + { + new Drawable[] + { + positionText = new OsuSpriteText + { + Padding = new MarginPadding { Right = shear_width / 2 }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = textColour, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold), + Shadow = false, + }, + new Container + { + Padding = new MarginPadding { Horizontal = shear_width / 3 }, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = 5f, + Shear = new Vector2(panel_shear, 0f), + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Box + { + Alpha = 0.5f, + RelativeSizeAxes = Axes.Both, + Colour = panelColour, + }, + } + }, + new OsuSpriteText + { + Padding = new MarginPadding { Left = shear_width }, + RelativeSizeAxes = Axes.X, + Width = 0.8f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = textColour, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), + Text = User.Username, + Truncate = true, + Shadow = false, + } + } + }, + new Container + { + Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f }, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = textColour, + Children = new Drawable[] + { + scoreText = new OsuSpriteText + { + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Spacing = new Vector2(0.5f, 0f), + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), + Shadow = false, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Children = new Drawable[] + { + accuracyText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + Shadow = false, + }, + comboText = new OsuSpriteText + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), + Shadow = false, + }, + } + } + }, + } + } + } + } + }; + + TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); + Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); + Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); } } } From c15bb6b928a4130d1515e8db07dd2e9f071dccfb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 17 Dec 2020 21:47:57 +0900 Subject: [PATCH 5407/6909] Add beatmap hash to MultiplayerRoomSettings --- .../RealtimeMultiplayer/MultiplayerRoomSettings.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs index d2f64235c3..60e0d1292e 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs @@ -18,13 +18,20 @@ namespace osu.Game.Online.RealtimeMultiplayer public int RulesetID { get; set; } + public string BeatmapChecksum { get; set; } = string.Empty; + public string Name { get; set; } = "Unnamed room"; [NotNull] public IEnumerable Mods { get; set; } = Enumerable.Empty(); - public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && Mods.SequenceEqual(other.Mods) && RulesetID == other.RulesetID && Name.Equals(other.Name, StringComparison.Ordinal); + public bool Equals(MultiplayerRoomSettings other) + => BeatmapID == other.BeatmapID + && BeatmapChecksum == other.BeatmapChecksum + && Mods.SequenceEqual(other.Mods) + && RulesetID == other.RulesetID + && Name.Equals(other.Name, StringComparison.Ordinal); - public override string ToString() => $"Name:{Name} Beatmap:{BeatmapID} Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; + public override string ToString() => $"Name:{Name} Beatmap:{BeatmapID} ({BeatmapChecksum}) Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; } } From 5e4f667cffac3ce6f4bfb108b29697e8fea9ba40 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Dec 2020 01:27:58 +0300 Subject: [PATCH 5408/6909] Revert "Allow OverlayView fetching with no API requests required" This reverts commit 449b9a21aefe912280728fd621fe11c782fbdb28. --- osu.Game/Overlays/OverlayView.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/OverlayView.cs b/osu.Game/Overlays/OverlayView.cs index f5ef183bd5..c254cdf290 100644 --- a/osu.Game/Overlays/OverlayView.cs +++ b/osu.Game/Overlays/OverlayView.cs @@ -42,29 +42,25 @@ namespace osu.Game.Overlays /// /// Create the API request for fetching data. /// - protected virtual APIRequest CreateRequest() => null; + protected abstract APIRequest CreateRequest(); /// /// Fired when results arrive from the main API request. /// /// - protected virtual void OnSuccess(T response) - { - } + protected abstract void OnSuccess(T response); /// /// Force a re-request for data from the API. /// - protected virtual void PerformFetch() + protected void PerformFetch() { request?.Cancel(); - request = CreateRequest(); - if (request != null) - { - request.Success += response => Schedule(() => OnSuccess(response)); - API.Queue(request); - } + request = CreateRequest(); + request.Success += response => Schedule(() => OnSuccess(response)); + + API.Queue(request); } private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => From 9c22753f3fb23f94a19b3870d7cc1e46638e616e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Dec 2020 02:51:19 +0300 Subject: [PATCH 5409/6909] Remove unnecessary inheritance to OverlayView --- .../Visual/Online/TestSceneFriendDisplay.cs | 11 +++----- .../Dashboard/Friends/FriendDisplay.cs | 26 ++++++++++++------- 2 files changed, 19 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 0cc6e9f358..9bece39ca0 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Purple); - private TestFriendDisplay display; + private FriendDisplay display; [SetUp] public void Setup() => Schedule(() => @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.Online Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, - Child = display = new TestFriendDisplay() + Child = display = new FriendDisplay() }; }); @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOnline() { - AddStep("Fetch online", () => display?.Fetch()); + // No need to do anything, fetch is performed automatically. } private List getUsers() => new List @@ -76,10 +76,5 @@ namespace osu.Game.Tests.Visual.Online LastVisit = DateTimeOffset.Now } }; - - private class TestFriendDisplay : FriendDisplay - { - public void Fetch() => PerformFetch(); - } } } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index 8a8ac36e14..cc26a11da1 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -5,16 +5,18 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Users; using osuTK; namespace osu.Game.Overlays.Dashboard.Friends { - public class FriendDisplay : OverlayView> + public class FriendDisplay : CompositeDrawable { private List users = new List(); @@ -39,8 +41,16 @@ namespace osu.Game.Overlays.Dashboard.Friends private Container itemsPlaceholder; private LoadingLayer loading; - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private readonly IBindableList apiFriends = new BindableList(); + + public FriendDisplay() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader(true)] + private void load(OverlayColourProvider colourProvider, IAPIProvider api) { InternalChild = new FillFlowContainer { @@ -130,6 +140,9 @@ namespace osu.Game.Overlays.Dashboard.Friends background.Colour = colourProvider.Background4; controlBackground.Colour = colourProvider.Background5; + + apiFriends.BindTo(api.Friends); + apiFriends.BindCollectionChanged((_, __) => Schedule(() => Users = apiFriends.ToList()), true); } protected override void LoadComplete() @@ -141,13 +154,6 @@ namespace osu.Game.Overlays.Dashboard.Friends userListToolbar.SortCriteria.BindValueChanged(_ => recreatePanels()); } - protected override void PerformFetch() - { - base.PerformFetch(); - - Users = API.Friends.ToList(); - } - private void recreatePanels() { if (!users.Any()) From 8a01e567a146d71405b8feeb7bfd7d532569b000 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Dec 2020 03:06:28 +0300 Subject: [PATCH 5410/6909] Fix API potentially getting stuck in connecting state --- osu.Game/Online/API/APIAccess.cs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 806a42a38b..b42c78e9cd 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -145,16 +145,14 @@ namespace osu.Game.Online.API failureCount = 0; - var friendsReq = new GetFriendsRequest(); - friendsReq.Success += f => + fetchFriends(() => { - Friends.AddRange(f); - //we're connected! state.Value = APIState.Online; - }; - - handleRequest(friendsReq); + }, () => + { + state.Value = APIState.Failing; + }); }; if (!handleRequest(userReq)) @@ -255,6 +253,19 @@ namespace osu.Game.Online.API return null; } + private void fetchFriends(Action onSuccess, Action onFail) + { + var friendsReq = new GetFriendsRequest(); + friendsReq.Success += res => + { + Friends.AddRange(res); + onSuccess?.Invoke(); + }; + + if (!handleRequest(friendsReq)) + onFail?.Invoke(); + } + /// /// Handle a single API request. /// Ensures all exceptions are caught and dealt with correctly. From a01ed1827a481fb8ac87aef1a1fda89ed5b33f4e Mon Sep 17 00:00:00 2001 From: Graham Johnson Date: Thu, 17 Dec 2020 19:34:16 -0500 Subject: [PATCH 5411/6909] Align the drag circles on the selction box in the editor to be on the center of the border --- .../Edit/Compose/Components/SelectionBox.cs | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 347d9e3ba7..e4feceb987 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -235,6 +235,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle { Anchor = anchor, + Y = getAdjustmentToCenterCircleOnBorder(anchor).Y, + X = getAdjustmentToCenterCircleOnBorder(anchor).X, HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), OperationStarted = operationStarted, OperationEnded = operationEnded @@ -251,6 +253,45 @@ namespace osu.Game.Screens.Edit.Compose.Components return (endAngle - startAngle) * 180 / MathF.PI; } + /// + /// Adjust Drag circle to be centered on the center of the border instead of on the edge. + /// + /// The part of the rectangle to be adjusted. + private Vector2 getAdjustmentToCenterCircleOnBorder(Anchor anchor) + { + Vector2 adjustment = Vector2.Zero; + + switch (anchor) + { + case Anchor.TopLeft: + case Anchor.CentreLeft: + case Anchor.BottomLeft: + adjustment.X = BORDER_RADIUS / 2; + break; + case Anchor.TopRight: + case Anchor.CentreRight: + case Anchor.BottomRight: + adjustment.X = -BORDER_RADIUS / 2; + break; + } + + switch (anchor) + { + case Anchor.TopLeft: + case Anchor.TopCentre: + case Anchor.TopRight: + adjustment.Y = BORDER_RADIUS / 2; + break; + case Anchor.BottomLeft: + case Anchor.BottomCentre: + case Anchor.BottomRight: + adjustment.Y = -BORDER_RADIUS / 2; + break; + } + + return adjustment; + } + private void operationEnded() { if (--activeOperations == 0) From a8abefcd665adf869b97e45d89c0d85a6d979a7b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Dec 2020 03:34:33 +0300 Subject: [PATCH 5412/6909] Make GameplayLeaderboardScore a model class --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 20 +++++---- .../Screens/Play/HUD/GameplayLeaderboard.cs | 41 ++----------------- .../Play/HUD/GameplayLeaderboardScore.cs | 5 ++- 3 files changed, 17 insertions(+), 49 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index d0fdb3dd9c..12bc918d45 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Gameplay playerScore.Value = 1222333; }); - AddStep("add player user", () => leaderboard.AddLocalUser(playerScore, new User { Username = "You" })); + AddStep("add local player", () => leaderboard.Add(createLeaderboardScore(playerScore, "You", true))); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } @@ -48,8 +48,8 @@ namespace osu.Game.Tests.Visual.Gameplay var player2Score = new BindableDouble(1234567); var player3Score = new BindableDouble(1111111); - AddStep("add player 2", () => leaderboard.AddPlayer(player2Score, new User { Username = "Player 2" })); - AddStep("add player 3", () => leaderboard.AddPlayer(player3Score, new User { Username = "Player 3" })); + AddStep("add player 2", () => leaderboard.Add(createLeaderboardScore(player2Score, "Player 2"))); + AddStep("add player 3", () => leaderboard.Add(createLeaderboardScore(player3Score, "Player 3"))); AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); @@ -66,6 +66,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); } + private static GameplayLeaderboardScore createLeaderboardScore(BindableDouble score, string username, bool localOrReplayPlayer = false) + { + return new GameplayLeaderboardScore(new User { Username = username }, localOrReplayPlayer) + { + TotalScore = { BindTarget = score }, + }; + } + private class TestGameplayLeaderboard : GameplayLeaderboard { public bool CheckPositionByUsername(string username, int? expectedPosition) @@ -74,12 +82,6 @@ namespace osu.Game.Tests.Visual.Gameplay return scoreItem != null && scoreItem.ScorePosition == expectedPosition; } - - public void AddPlayer(BindableDouble totalScore, User user) => - base.AddPlayer(totalScore, new BindableDouble(1f), new BindableInt(1), user, false); - - public void AddLocalUser(BindableDouble totalScore, User user) => - base.AddPlayer(totalScore, new BindableDouble(1f), new BindableInt(1), user, true); } } } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index a2673e3097..573bf54b14 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -2,11 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using JetBrains.Annotations; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Users; using osuTK; namespace osu.Game.Screens.Play.HUD @@ -25,44 +22,12 @@ namespace osu.Game.Screens.Play.HUD LayoutEasing = Easing.OutQuint; } - /// - /// Adds a player to the leaderboard. - /// - /// A bindable of the player's total score. - /// A bindable of the player's accuracy. - /// A bindable of the player's current combo. - /// The player. - public void AddPlayer([NotNull] IBindableNumber totalScore, - [NotNull] IBindableNumber accuracy, - [NotNull] IBindableNumber combo, - [NotNull] User user) + public override void Add(GameplayLeaderboardScore drawable) { - AddPlayer(totalScore, accuracy, combo, user, false); + base.Add(drawable); + drawable?.TotalScore.BindValueChanged(_ => updateScores(), true); } - /// - /// Adds a player to the leaderboard. - /// - /// A bindable of the player's total score. - /// A bindable of the player's accuracy. - /// A bindable of the player's current combo. - /// The player. - /// Whether the provided is the local user. - protected void AddPlayer([NotNull] IBindableNumber totalScore, - [NotNull] IBindableNumber accuracy, - [NotNull] IBindableNumber combo, - [NotNull] User user, bool localUser) => Schedule(() => - { - Add(new GameplayLeaderboardScore(user, localUser) - { - TotalScore = { BindTarget = totalScore }, - Accuracy = { BindTarget = accuracy }, - Combo = { BindTarget = combo }, - }); - - totalScore.BindValueChanged(_ => updateScores(), true); - }); - private void updateScores() { var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList(); diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 62ea5782e8..f94d6bd01f 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -27,8 +27,9 @@ namespace osu.Game.Screens.Play.HUD private const float panel_height = 35f; private OsuSpriteText positionText, scoreText, accuracyText, comboText; - public readonly BindableDouble TotalScore = new BindableDouble(); - public readonly BindableDouble Accuracy = new BindableDouble(); + + public readonly BindableDouble TotalScore = new BindableDouble(1000000); + public readonly BindableDouble Accuracy = new BindableDouble(1); public readonly BindableInt Combo = new BindableInt(); private int? scorePosition; From 92bf74ba29b3bc6c8ec645fe54d9c218d76ff6cf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Dec 2020 03:37:24 +0300 Subject: [PATCH 5413/6909] localUser -> localOrReplayPlayer --- .../Screens/Play/HUD/GameplayLeaderboardScore.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index f94d6bd01f..285ed93341 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -50,12 +50,17 @@ namespace osu.Game.Screens.Play.HUD public User User { get; } - private readonly bool localUser; + private readonly bool localOrReplayPlayer; - public GameplayLeaderboardScore(User user, bool localUser) + /// + /// Creates a new . + /// + /// The score's player. + /// Whether the player is the local user or a replay player. + public GameplayLeaderboardScore(User user, bool localOrReplayPlayer) { User = user; - this.localUser = localUser; + this.localOrReplayPlayer = localOrReplayPlayer; AutoSizeAxes = Axes.Both; @@ -72,7 +77,7 @@ namespace osu.Game.Screens.Play.HUD Color4 panelColour, textColour; float panelWidth; - if (localUser) + if (localOrReplayPlayer) { panelWidth = extended_width; panelColour = Color4Extensions.FromHex("7fcc33"); From a0235a06e642319ce5108c21a3643e09bdc2031f Mon Sep 17 00:00:00 2001 From: Graham Johnson Date: Thu, 17 Dec 2020 19:40:21 -0500 Subject: [PATCH 5414/6909] update comment --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index e4feceb987..4a0a925962 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -257,6 +257,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Adjust Drag circle to be centered on the center of the border instead of on the edge. /// /// The part of the rectangle to be adjusted. + /// A 2d vector on how much to adjust the drag circle private Vector2 getAdjustmentToCenterCircleOnBorder(Anchor anchor) { Vector2 adjustment = Vector2.Zero; From 44f4ed4fd3001587378bae5ff90d4e4edfd5011a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 11:19:40 +0900 Subject: [PATCH 5415/6909] Fix spacing --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 4a0a925962..f50e599457 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -269,6 +269,7 @@ namespace osu.Game.Screens.Edit.Compose.Components case Anchor.BottomLeft: adjustment.X = BORDER_RADIUS / 2; break; + case Anchor.TopRight: case Anchor.CentreRight: case Anchor.BottomRight: @@ -283,6 +284,7 @@ namespace osu.Game.Screens.Edit.Compose.Components case Anchor.TopRight: adjustment.Y = BORDER_RADIUS / 2; break; + case Anchor.BottomLeft: case Anchor.BottomCentre: case Anchor.BottomRight: From 9079d33412207d9f3e18940d3d61c8d8f1a86137 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 11:20:21 +0900 Subject: [PATCH 5416/6909] X before Y for sanity --- osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index f50e599457..8ccd4f71e9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -235,8 +235,8 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle { Anchor = anchor, - Y = getAdjustmentToCenterCircleOnBorder(anchor).Y, X = getAdjustmentToCenterCircleOnBorder(anchor).X, + Y = getAdjustmentToCenterCircleOnBorder(anchor).Y, HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), OperationStarted = operationStarted, OperationEnded = operationEnded From 208a9e596ed91f212f37745f493c3416601c3ada Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 14:58:58 +0900 Subject: [PATCH 5417/6909] Set new room for each test iteration --- .../Multiplayer/TestSceneLoungeRoomInfo.cs | 5 +--- .../TestSceneMatchBeatmapDetailArea.cs | 4 +--- .../Multiplayer/TestSceneMatchHeader.cs | 12 ++++++---- .../Multiplayer/TestSceneMatchLeaderboard.cs | 10 +++++--- .../TestSceneMatchSettingsOverlay.cs | 4 +--- .../Multiplayer/TestSceneMatchSongSelect.cs | 7 ------ .../Multiplayer/TestSceneMatchSubScreen.cs | 6 ----- .../TestSceneOverlinedParticipants.cs | 5 ++-- .../Multiplayer/TestSceneOverlinedPlaylist.cs | 23 +++++++++++-------- .../Multiplayer/TestSceneParticipantsList.cs | 13 +++++------ osu.Game/Tests/Visual/MultiplayerTestScene.cs | 15 +++++++----- 11 files changed, 49 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs index cdad37a9ad..9baaa42c83 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs @@ -4,7 +4,6 @@ using System; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Users; @@ -14,10 +13,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneLoungeRoomInfo : MultiplayerTestScene { [SetUp] - public void Setup() => Schedule(() => + public new void Setup() => Schedule(() => { - Room = new Room(); - Child = new RoomInfo { Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 01cd26fbe5..6b1d90e06e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -24,10 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private RulesetStore rulesetStore { get; set; } [SetUp] - public void Setup() => Schedule(() => + public new void Setup() => Schedule(() => { - Room = new Room(); - Child = new MatchBeatmapDetailArea { Anchor = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index e5943105b7..ec5292e51e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.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 NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Osu; @@ -14,7 +15,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { public TestSceneMatchHeader() { - Room = new Room(); + Child = new Header(); + } + + [SetUp] + public new void Setup() => Schedule(() => + { Room.Playlist.Add(new PlaylistItem { Beatmap = @@ -41,8 +47,6 @@ namespace osu.Game.Tests.Visual.Multiplayer Room.Name.Value = "A very awesome room"; Room.Host.Value = new User { Id = 2, Username = "peppy" }; - - Child = new Header(); - } + }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index c24c6c4ba3..a72f71d79c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -3,10 +3,10 @@ using System.Collections.Generic; using Newtonsoft.Json; +using NUnit.Framework; 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; @@ -19,8 +19,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestSceneMatchLeaderboard() { - Room = new Room { RoomID = { Value = 3 } }; - Add(new MatchLeaderboard { Origin = Anchor.Centre, @@ -40,6 +38,12 @@ namespace osu.Game.Tests.Visual.Multiplayer api.Queue(req); } + [SetUp] + public new void Setup() => Schedule(() => + { + Room.RoomID.Value = 3; + }); + private class GetRoomScoresRequest : APIRequest> { protected override string Target => "rooms/3/leaderboard"; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index 07ff56b5c3..cbe8cc6137 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -23,10 +23,8 @@ namespace osu.Game.Tests.Visual.Multiplayer private TestRoomSettings settings; [SetUp] - public void Setup() => Schedule(() => + public new void Setup() => Schedule(() => { - Room = new Room(); - settings = new TestRoomSettings { RelativeSizeAxes = Axes.Both, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index 55b8902d7b..4742fd0d84 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -14,7 +14,6 @@ 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.Mods; using osu.Game.Rulesets.Osu; @@ -94,12 +93,6 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for present", () => songSelect.IsCurrentScreen()); } - [SetUp] - public void Setup() => Schedule(() => - { - Room = new Room(); - }); - [Test] public void TestItemAddedIfEmptyOnStart() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index 2e22317539..65e9893851 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -45,12 +45,6 @@ namespace osu.Game.Tests.Visual.Multiplayer manager.Import(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo.BeatmapSet).Wait(); } - [SetUp] - public void Setup() => Schedule(() => - { - Room = new Room(); - }); - [SetUpSteps] public void SetupSteps() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs index b6bfa7c93a..481541a1af 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; using osuTK; @@ -14,9 +13,9 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override bool UseOnlineAPI => true; [SetUp] - public void Setup() => Schedule(() => + public new void Setup() => Schedule(() => { - Room = new Room { RoomID = { Value = 7 } }; + Room.RoomID.Value = 7; }); [Test] diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs index 14984988cb..3dbc990f7a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.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 NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Osu; @@ -16,7 +17,18 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestSceneOverlinedPlaylist() { - Room = new Room { RoomID = { Value = 7 } }; + Add(new DrawableRoomPlaylist(false, false) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(500), + }); + } + + [SetUp] + public new void Setup() => Schedule(() => + { + Room.RoomID.Value = 7; for (int i = 0; i < 10; i++) { @@ -27,13 +39,6 @@ namespace osu.Game.Tests.Visual.Multiplayer Ruleset = { Value = new OsuRuleset().RulesetInfo } }); } - - Add(new DrawableRoomPlaylist(false, false) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(500), - }); - } + }); } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs index f71c5fc5d2..1e5647f7f4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; namespace osu.Game.Tests.Visual.Multiplayer @@ -12,15 +11,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { protected override bool UseOnlineAPI => true; - [SetUp] - public void Setup() => Schedule(() => - { - Room = new Room { RoomID = { Value = 7 } }; - }); - public TestSceneParticipantsList() { Add(new ParticipantsList { RelativeSizeAxes = Axes.Both }); } + + [SetUp] + public new void Setup() => Schedule(() => + { + Room.RoomID.Value = 7; + }); } } diff --git a/osu.Game/Tests/Visual/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/MultiplayerTestScene.cs index 4d073f16f4..dc614896c5 100644 --- a/osu.Game/Tests/Visual/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/MultiplayerTestScene.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Multiplayer; @@ -10,13 +11,9 @@ namespace osu.Game.Tests.Visual public abstract class MultiplayerTestScene : ScreenTestScene { [Cached] - private readonly Bindable currentRoom = new Bindable(); + private readonly Bindable currentRoom = new Bindable(new Room()); - protected Room Room - { - get => currentRoom.Value; - set => currentRoom.Value = value; - } + protected Room Room => currentRoom.Value; private CachedModelDependencyContainer dependencies; @@ -26,5 +23,11 @@ namespace osu.Game.Tests.Visual dependencies.Model.BindTo(currentRoom); return dependencies; } + + [SetUp] + public void Setup() => Schedule(() => + { + Room.CopyFrom(new Room()); + }); } } From a4f7eb83c7baa4b847600fcca112b219abe71287 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 15:07:39 +0900 Subject: [PATCH 5418/6909] Fix overlined participants test scene not working --- .../TestSceneOverlinedParticipants.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs index 481541a1af..88ecdb0aad 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs @@ -4,18 +4,25 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Multi.Components; -using osuTK; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneOverlinedParticipants : MultiplayerTestScene { - protected override bool UseOnlineAPI => true; - [SetUp] public new void Setup() => Schedule(() => { Room.RoomID.Value = 7; + + for (int i = 0; i < 50; i++) + { + Room.RecentParticipants.Add(new User + { + Username = "peppy", + Id = 2 + }); + } }); [Test] @@ -27,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Width = 500, + Width = 0.2f, }; }); } @@ -41,7 +48,8 @@ namespace osu.Game.Tests.Visual.Multiplayer { Anchor = Anchor.Centre, Origin = Anchor.Centre, - Size = new Vector2(500) + Width = 0.2f, + Height = 0.2f, }; }); } From f0e91ba43188c7cc3f7a0e660c13477551082f71 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 15:09:11 +0900 Subject: [PATCH 5419/6909] Fix overlined playlist test scene not working --- osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs index 3dbc990f7a..f8af67aa7f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs @@ -22,6 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(500), + Items = { BindTarget = Room.Playlist } }); } From 8c5e25b990af1ea231617428af012ba516db95ee Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 15:11:11 +0900 Subject: [PATCH 5420/6909] Remove overlined test scenes --- .../TestSceneOverlinedParticipants.cs | 57 ------------------- .../Multiplayer/TestSceneOverlinedPlaylist.cs | 45 --------------- .../Multiplayer/TestSceneParticipantsList.cs | 46 ++++++++++++--- 3 files changed, 39 insertions(+), 109 deletions(-) delete mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs delete mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs deleted file mode 100644 index 88ecdb0aad..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedParticipants.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using NUnit.Framework; -using osu.Framework.Graphics; -using osu.Game.Screens.Multi.Components; -using osu.Game.Users; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestSceneOverlinedParticipants : MultiplayerTestScene - { - [SetUp] - public new void Setup() => Schedule(() => - { - Room.RoomID.Value = 7; - - for (int i = 0; i < 50; i++) - { - Room.RecentParticipants.Add(new User - { - Username = "peppy", - Id = 2 - }); - } - }); - - [Test] - public void TestHorizontalLayout() - { - AddStep("create component", () => - { - Child = new ParticipantsDisplay(Direction.Horizontal) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.2f, - }; - }); - } - - [Test] - public void TestVerticalLayout() - { - AddStep("create component", () => - { - Child = new ParticipantsDisplay(Direction.Vertical) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.2f, - Height = 0.2f, - }; - }); - } - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs deleted file mode 100644 index f8af67aa7f..0000000000 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneOverlinedPlaylist.cs +++ /dev/null @@ -1,45 +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.Graphics; -using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Multi; -using osu.Game.Tests.Beatmaps; -using osuTK; - -namespace osu.Game.Tests.Visual.Multiplayer -{ - public class TestSceneOverlinedPlaylist : MultiplayerTestScene - { - protected override bool UseOnlineAPI => true; - - public TestSceneOverlinedPlaylist() - { - Add(new DrawableRoomPlaylist(false, false) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(500), - Items = { BindTarget = Room.Playlist } - }); - } - - [SetUp] - public new void Setup() => Schedule(() => - { - Room.RoomID.Value = 7; - - for (int i = 0; i < 10; i++) - { - Room.Playlist.Add(new PlaylistItem - { - ID = i, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, - Ruleset = { Value = new OsuRuleset().RulesetInfo } - }); - } - }); - } -} diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs index 1e5647f7f4..7bbec7d30e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs @@ -4,22 +4,54 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Screens.Multi.Components; +using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneParticipantsList : MultiplayerTestScene { - protected override bool UseOnlineAPI => true; - - public TestSceneParticipantsList() - { - Add(new ParticipantsList { RelativeSizeAxes = Axes.Both }); - } - [SetUp] public new void Setup() => Schedule(() => { Room.RoomID.Value = 7; + + for (int i = 0; i < 50; i++) + { + Room.RecentParticipants.Add(new User + { + Username = "peppy", + Id = 2 + }); + } }); + + [Test] + public void TestHorizontalLayout() + { + AddStep("create component", () => + { + Child = new ParticipantsDisplay(Direction.Horizontal) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.2f, + }; + }); + } + + [Test] + public void TestVerticalLayout() + { + AddStep("create component", () => + { + Child = new ParticipantsDisplay(Direction.Vertical) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Width = 0.2f, + Height = 0.2f, + }; + }); + } } } From 206bf3713ed2af58b97b55489783848728934dde Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 15:16:36 +0900 Subject: [PATCH 5421/6909] Make IAPIProvider read-only bindables into IBindables --- osu.Desktop/DiscordRichPresence.cs | 2 +- .../Visual/Menus/TestSceneDisclaimer.cs | 3 ++- .../Online/TestSceneAccountCreationOverlay.cs | 2 +- .../Online/TestSceneNowPlayingCommand.cs | 11 +++++--- osu.Game/Online/API/APIAccess.cs | 26 +++++++++++-------- osu.Game/Online/API/DummyAPIAccess.cs | 4 +++ osu.Game/Online/API/IAPIProvider.cs | 6 ++--- osu.Game/OsuGame.cs | 3 +-- .../BeatmapSet/Buttons/FavouriteButton.cs | 2 +- .../BeatmapSet/Scores/ScoresContainer.cs | 2 +- .../Overlays/Comments/CommentsContainer.cs | 2 +- .../Backgrounds/BackgroundScreenDefault.cs | 2 +- osu.Game/Screens/Menu/Disclaimer.cs | 2 +- .../Screens/Menu/MenuLogoVisualisation.cs | 2 +- osu.Game/Screens/Menu/MenuSideFlashes.cs | 2 +- 15 files changed, 41 insertions(+), 30 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 26d7402a5b..f1878d967d 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -26,7 +26,7 @@ namespace osu.Desktop [Resolved] private IBindable ruleset { get; set; } - private Bindable user; + private IBindable user; private readonly IBindable status = new Bindable(); private readonly IBindable activity = new Bindable(); diff --git a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs index 49fab08ded..9cbdee3632 100644 --- a/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs +++ b/osu.Game.Tests/Visual/Menus/TestSceneDisclaimer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Game.Online.API; using osu.Game.Screens.Menu; using osu.Game.Users; @@ -16,7 +17,7 @@ namespace osu.Game.Tests.Visual.Menus AddStep("toggle support", () => { - API.LocalUser.Value = new User + ((DummyAPIAccess)API).LocalUser.Value = new User { Username = API.LocalUser.Value.Username, Id = API.LocalUser.Value.Id + 1, diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs index 6c8ec917ba..dcfe0432a8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs @@ -14,7 +14,7 @@ namespace osu.Game.Tests.Visual.Online { private readonly Container userPanelArea; - private Bindable localUser; + private IBindable localUser; public TestSceneAccountCreationOverlay() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 8e151a987a..0324da6cf5 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -6,6 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Online.API; using osu.Game.Online.Chat; using osu.Game.Rulesets; using osu.Game.Users; @@ -18,6 +19,8 @@ namespace osu.Game.Tests.Visual.Online [Cached(typeof(IChannelPostTarget))] private PostTarget postTarget { get; set; } + private DummyAPIAccess api => (DummyAPIAccess)API; + public TestSceneNowPlayingCommand() { Add(postTarget = new PostTarget()); @@ -26,7 +29,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestGenericActivity() { - AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby(null)); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(null)); AddStep("Run command", () => Add(new NowPlayingCommand())); @@ -36,7 +39,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestEditActivity() { - AddStep("Set activity", () => API.Activity.Value = new UserActivity.Editing(new BeatmapInfo())); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.Editing(new BeatmapInfo())); AddStep("Run command", () => Add(new NowPlayingCommand())); @@ -46,7 +49,7 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestPlayActivity() { - AddStep("Set activity", () => API.Activity.Value = new UserActivity.SoloGame(new BeatmapInfo(), new RulesetInfo())); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.SoloGame(new BeatmapInfo(), new RulesetInfo())); AddStep("Run command", () => Add(new NowPlayingCommand())); @@ -57,7 +60,7 @@ namespace osu.Game.Tests.Visual.Online [TestCase(false)] public void TestLinkPresence(bool hasOnlineId) { - AddStep("Set activity", () => API.Activity.Value = new UserActivity.InLobby(null)); + AddStep("Set activity", () => api.Activity.Value = new UserActivity.InLobby(null)); AddStep("Set beatmap", () => Beatmap.Value = new DummyWorkingBeatmap(Audio, null) { diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index b42c78e9cd..b96475d5bf 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -39,11 +39,15 @@ namespace osu.Game.Online.API private string password; - public Bindable LocalUser { get; } = new Bindable(createGuestUser()); + public IBindable LocalUser => localUser; + public IBindableList Friends => friends; + public IBindable Activity => activity; - public BindableList Friends { get; } = new BindableList(); + private Bindable localUser { get; } = new Bindable(createGuestUser()); - public Bindable Activity { get; } = new Bindable(); + private BindableList friends { get; } = new BindableList(); + + private Bindable activity { get; } = new Bindable(); protected bool HasLogin => authentication.Token.Value != null || (!string.IsNullOrEmpty(ProvidedUsername) && !string.IsNullOrEmpty(password)); @@ -63,10 +67,10 @@ namespace osu.Game.Online.API authentication.TokenString = config.Get(OsuSetting.Token); authentication.Token.ValueChanged += onTokenChanged; - LocalUser.BindValueChanged(u => + localUser.BindValueChanged(u => { - u.OldValue?.Activity.UnbindFrom(Activity); - u.NewValue.Activity.BindTo(Activity); + u.OldValue?.Activity.UnbindFrom(activity); + u.NewValue.Activity.BindTo(activity); }, true); var thread = new Thread(run) @@ -138,10 +142,10 @@ namespace osu.Game.Online.API var userReq = new GetUserRequest(); userReq.Success += u => { - LocalUser.Value = u; + localUser.Value = u; // todo: save/pull from settings - LocalUser.Value.Status.Value = new UserStatusOnline(); + localUser.Value.Status.Value = new UserStatusOnline(); failureCount = 0; @@ -343,7 +347,7 @@ namespace osu.Game.Online.API return true; } - public bool IsLoggedIn => LocalUser.Value.Id > 1; + public bool IsLoggedIn => localUser.Value.Id > 1; public void Queue(APIRequest request) { @@ -376,8 +380,8 @@ namespace osu.Game.Online.API // Scheduled prior to state change such that the state changed event is invoked with the correct user and their friends present Schedule(() => { - LocalUser.Value = createGuestUser(); - Friends.Clear(); + localUser.Value = createGuestUser(); + friends.Clear(); }); state.Value = APIState.Offline; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 7e5a6378ec..a0bc8dd9d6 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -93,5 +93,9 @@ namespace osu.Game.Online.API } public void SetState(APIState newState) => state.Value = newState; + + IBindable IAPIProvider.LocalUser => LocalUser; + IBindableList IAPIProvider.Friends => Friends; + IBindable IAPIProvider.Activity => Activity; } } diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 3f62b37a48..3a444460f2 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -13,19 +13,19 @@ namespace osu.Game.Online.API /// The local user. /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// - Bindable LocalUser { get; } + IBindable LocalUser { get; } /// /// The user's friends. /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// - BindableList Friends { get; } + IBindableList Friends { get; } /// /// The current user's activity. /// This is not thread-safe and should be scheduled locally if consumed from a drawable component. /// - Bindable Activity { get; } + IBindable Activity { get; } /// /// Retrieve the OAuth access token. diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d67d790ce2..1e342fa464 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -51,7 +51,6 @@ using osu.Game.Screens.Select; using osu.Game.Updater; using osu.Game.Utils; using LogLevel = osu.Framework.Logging.LogLevel; -using osu.Game.Users; using System.IO; namespace osu.Game @@ -976,7 +975,7 @@ namespace osu.Game if (newScreen is IOsuScreen newOsuScreen) { OverlayActivationMode.BindTo(newOsuScreen.OverlayActivationMode); - ((IBindable)API.Activity).BindTo(newOsuScreen.Activity); + API.Activity.BindTo(newOsuScreen.Activity); MusicController.AllowRateAdjustments = newOsuScreen.AllowRateAdjustments; diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs index 742b1055b2..c983b337b5 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private PostBeatmapFavouriteRequest request; private LoadingLayer loading; - private readonly Bindable localUser = new Bindable(); + private readonly IBindable localUser = new Bindable(); public string TooltipText { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index a58d662de7..9a2dcd014a 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -26,7 +26,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores public readonly Bindable Beatmap = new Bindable(); private readonly Bindable ruleset = new Bindable(); private readonly Bindable scope = new Bindable(BeatmapLeaderboardScope.Global); - private readonly Bindable user = new Bindable(); + private readonly IBindable user = new Bindable(); private readonly Box background; private readonly ScoreTable scoreTable; diff --git a/osu.Game/Overlays/Comments/CommentsContainer.cs b/osu.Game/Overlays/Comments/CommentsContainer.cs index 2a78748be6..513fabf52a 100644 --- a/osu.Game/Overlays/Comments/CommentsContainer.cs +++ b/osu.Game/Overlays/Comments/CommentsContainer.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Comments public readonly Bindable Sort = new Bindable(); public readonly BindableBool ShowDeleted = new BindableBool(); - protected readonly Bindable User = new Bindable(); + protected readonly IBindable User = new Bindable(); [Resolved] private IAPIProvider api { get; set; } diff --git a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs index 8beb955824..bd4577fd57 100644 --- a/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs +++ b/osu.Game/Screens/Backgrounds/BackgroundScreenDefault.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Backgrounds private int currentDisplay; private const int background_count = 7; - private Bindable user; + private IBindable user; private Bindable skin; private Bindable mode; private Bindable introSequence; diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 8368047d5a..ceec12c967 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Menu if (nextScreen != null) LoadComponentAsync(nextScreen); - currentUser.BindTo(api.LocalUser); + ((IBindable)currentUser).BindTo(api.LocalUser); } public override void OnEntering(IScreen last) diff --git a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs index 92add458f9..c44beeffa5 100644 --- a/osu.Game/Screens/Menu/MenuLogoVisualisation.cs +++ b/osu.Game/Screens/Menu/MenuLogoVisualisation.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.Menu { internal class MenuLogoVisualisation : LogoVisualisation { - private Bindable user; + private IBindable user; private Bindable skin; [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Menu/MenuSideFlashes.cs b/osu.Game/Screens/Menu/MenuSideFlashes.cs index 2ff8132d47..a1ae4555ed 100644 --- a/osu.Game/Screens/Menu/MenuSideFlashes.cs +++ b/osu.Game/Screens/Menu/MenuSideFlashes.cs @@ -35,7 +35,7 @@ namespace osu.Game.Screens.Menu private const double box_fade_in_time = 65; private const int box_width = 200; - private Bindable user; + private IBindable user; private Bindable skin; [Resolved] From d36169f697ce53809bbec26b44411b28c66f6b0a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 15:16:48 +0900 Subject: [PATCH 5422/6909] Move friend request to a more understandable place in connection flow --- osu.Game/Online/API/APIAccess.cs | 36 +++++++++++++------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index b96475d5bf..93d913dfad 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -140,6 +140,7 @@ namespace osu.Game.Online.API } var userReq = new GetUserRequest(); + userReq.Success += u => { localUser.Value = u; @@ -148,15 +149,6 @@ namespace osu.Game.Online.API localUser.Value.Status.Value = new UserStatusOnline(); failureCount = 0; - - fetchFriends(() => - { - //we're connected! - state.Value = APIState.Online; - }, () => - { - state.Value = APIState.Failing; - }); }; if (!handleRequest(userReq)) @@ -166,6 +158,19 @@ namespace osu.Game.Online.API continue; } + // getting user's friends is considered part of the connection process. + var friendsReq = new GetFriendsRequest(); + + friendsReq.Success += res => + { + friends.AddRange(res); + + //we're connected! + state.Value = APIState.Online; + }; + + if (!handleRequest(friendsReq)) + state.Value = APIState.Failing; // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests // before actually going online. @@ -257,19 +262,6 @@ namespace osu.Game.Online.API return null; } - private void fetchFriends(Action onSuccess, Action onFail) - { - var friendsReq = new GetFriendsRequest(); - friendsReq.Success += res => - { - Friends.AddRange(res); - onSuccess?.Invoke(); - }; - - if (!handleRequest(friendsReq)) - onFail?.Invoke(); - } - /// /// Handle a single API request. /// Ensures all exceptions are caught and dealt with correctly. From bdfeb55dec5614b535737fb22df80c59627973a4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 15:18:06 +0900 Subject: [PATCH 5423/6909] Fix room status test scene not working --- .../Visual/Multiplayer/TestSceneRoomStatus.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs index 1925e0ef4f..c1dfb94464 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.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.Containers; using osu.Game.Online.Multiplayer; @@ -22,18 +23,21 @@ namespace osu.Game.Tests.Visual.Multiplayer new DrawableRoom(new Room { Name = { Value = "Room 1" }, - Status = { Value = new RoomStatusOpen() } - }), + Status = { Value = new RoomStatusOpen() }, + EndDate = { Value = DateTimeOffset.Now.AddDays(1) } + }) { MatchingFilter = true }, new DrawableRoom(new Room { Name = { Value = "Room 2" }, - Status = { Value = new RoomStatusPlaying() } - }), + Status = { Value = new RoomStatusPlaying() }, + EndDate = { Value = DateTimeOffset.Now.AddDays(1) } + }) { MatchingFilter = true }, new DrawableRoom(new Room { Name = { Value = "Room 3" }, - Status = { Value = new RoomStatusEnded() } - }), + Status = { Value = new RoomStatusEnded() }, + EndDate = { Value = DateTimeOffset.Now } + }) { MatchingFilter = true }, } }; } From 57c5d45c022844e729e11c4236d7fa0b8cc93ed9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 15:19:38 +0900 Subject: [PATCH 5424/6909] Standardise and extract common connection failure handling logic --- osu.Game/Online/API/APIAccess.cs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 93d913dfad..fe500b9548 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -153,8 +153,7 @@ namespace osu.Game.Online.API if (!handleRequest(userReq)) { - if (State.Value == APIState.Connecting) - state.Value = APIState.Failing; + failConnectionProcess(); continue; } @@ -170,7 +169,11 @@ namespace osu.Game.Online.API }; if (!handleRequest(friendsReq)) - state.Value = APIState.Failing; + { + failConnectionProcess(); + continue; + } + // The Success callback event is fired on the main thread, so we should wait for that to run before proceeding. // Without this, we will end up circulating this Connecting loop multiple times and queueing up many web requests // before actually going online. @@ -203,6 +206,13 @@ namespace osu.Game.Online.API Thread.Sleep(50); } + + void failConnectionProcess() + { + // if something went wrong during the connection process, we want to reset the state (but only if still connecting). + if (State.Value == APIState.Connecting) + state.Value = APIState.Failing; + } } public void Perform(APIRequest request) From 99b670627a847e42ca245923b6e89f70f3a1ea04 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 15:25:12 +0900 Subject: [PATCH 5425/6909] Remove unused placeholder friend in DummyAPI implementation --- osu.Game/Online/API/DummyAPIAccess.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index a0bc8dd9d6..4e1bc21df3 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -19,11 +19,7 @@ namespace osu.Game.Online.API Id = 1001, }); - public BindableList Friends { get; } = new BindableList(new User - { - Username = @"Dummy's friend", - Id = 2002, - }.Yield()); + public BindableList Friends { get; } = new BindableList(); public Bindable Activity { get; } = new Bindable(); From 4af508235ee855cd0ac9be9d5aed56d416651f95 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 15:35:18 +0900 Subject: [PATCH 5426/6909] Rename long variable --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 285ed93341..1d548e8809 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -50,17 +50,17 @@ namespace osu.Game.Screens.Play.HUD public User User { get; } - private readonly bool localOrReplayPlayer; + private readonly bool trackedPlayer; /// /// Creates a new . /// /// The score's player. - /// Whether the player is the local user or a replay player. - public GameplayLeaderboardScore(User user, bool localOrReplayPlayer) + /// Whether the player is the local user or a replay player. + public GameplayLeaderboardScore(User user, bool trackedPlayer) { User = user; - this.localOrReplayPlayer = localOrReplayPlayer; + this.trackedPlayer = trackedPlayer; AutoSizeAxes = Axes.Both; @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Play.HUD Color4 panelColour, textColour; float panelWidth; - if (localOrReplayPlayer) + if (trackedPlayer) { panelWidth = extended_width; panelColour = Color4Extensions.FromHex("7fcc33"); From c80ecec0b418eedfa54931ef34d496984655cec1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 15:36:24 +0900 Subject: [PATCH 5427/6909] Reorder methods --- osu.Game/Screens/Play/Player.cs | 42 ++++++++++++++++----------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a54f9fc047..92c76ec2d2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -531,29 +531,8 @@ namespace osu.Game.Screens.Play completionProgressDelegate = Schedule(GotoRanking); } - protected virtual ScoreInfo CreateScore() - { - var score = new ScoreInfo - { - Beatmap = Beatmap.Value.BeatmapInfo, - Ruleset = rulesetInfo, - Mods = Mods.Value.ToArray(), - }; - - if (DrawableRuleset.ReplayScore != null) - score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); - else - score.User = api.LocalUser.Value; - - ScoreProcessor.PopulateScore(score); - - return score; - } - protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; - protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); - #region Fail Logic protected FailOverlay FailOverlay { get; private set; } @@ -748,6 +727,25 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } + protected virtual ScoreInfo CreateScore() + { + var score = new ScoreInfo + { + Beatmap = Beatmap.Value.BeatmapInfo, + Ruleset = rulesetInfo, + Mods = Mods.Value.ToArray(), + }; + + if (DrawableRuleset.ReplayScore != null) + score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); + else + score.User = api.LocalUser.Value; + + ScoreProcessor.PopulateScore(score); + + return score; + } + protected virtual void GotoRanking() { if (DrawableRuleset.ReplayScore != null) @@ -781,6 +779,8 @@ namespace osu.Game.Screens.Play })); } + protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); + private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; From a749dca20b14a247cb8828cc0171b99be5e6f141 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 15:43:15 +0900 Subject: [PATCH 5428/6909] Remove left over using statement --- osu.Game/Online/API/DummyAPIAccess.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 4e1bc21df3..265298270c 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -5,7 +5,6 @@ using System; using System.Threading; using System.Threading.Tasks; using osu.Framework.Bindables; -using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Game.Users; From ceb2e4762d7c63efc3f9e044ae326447543c8528 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 16:20:42 +0900 Subject: [PATCH 5429/6909] Add test covering a more consistent spread of player scores --- .../Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 12bc918d45..d596def98a 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osuTK; @@ -66,6 +67,13 @@ namespace osu.Game.Tests.Visual.Gameplay AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); } + [Test] + public void TestRandomScores() + { + int playerNumber = 1; + AddRepeatStep("add player with random score", () => leaderboard.Add(createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), $"Player {playerNumber++}")), 10); + } + private static GameplayLeaderboardScore createLeaderboardScore(BindableDouble score, string username, bool localOrReplayPlayer = false) { return new GameplayLeaderboardScore(new User { Username = username }, localOrReplayPlayer) From c84807ed5c0f09e47993666920d5374d561c5f9b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 16:20:54 +0900 Subject: [PATCH 5430/6909] Refactor implementation --- .../Play/HUD/GameplayLeaderboardScore.cs | 123 +++++++++++------- 1 file changed, 73 insertions(+), 50 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 1d548e8809..439210c944 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.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.Bindables; @@ -11,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.API; using osu.Game.Users; using osu.Game.Utils; using osuTK; @@ -26,7 +24,7 @@ namespace osu.Game.Screens.Play.HUD private const float panel_height = 35f; - private OsuSpriteText positionText, scoreText, accuracyText, comboText; + private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; public readonly BindableDouble TotalScore = new BindableDouble(1000000); public readonly BindableDouble Accuracy = new BindableDouble(1); @@ -45,6 +43,7 @@ namespace osu.Game.Screens.Play.HUD positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}"; positionText.FadeTo(scorePosition.HasValue ? 1 : 0); + updateColour(); } } @@ -52,6 +51,9 @@ namespace osu.Game.Screens.Play.HUD private readonly bool trackedPlayer; + private Container mainFillContainer; + private Box centralFill; + /// /// Creates a new . /// @@ -62,62 +64,93 @@ namespace osu.Game.Screens.Play.HUD User = user; this.trackedPlayer = trackedPlayer; - AutoSizeAxes = Axes.Both; + AutoSizeAxes = Axes.X; + Height = panel_height; Anchor = Anchor.TopRight; Origin = Anchor.TopRight; } - [BackgroundDependencyLoader] - private void load(IAPIProvider api) + protected override void LoadComplete() { - const float panel_shear = 0.15f; - const float shear_width = panel_height * panel_shear; + base.LoadComplete(); - Color4 panelColour, textColour; - float panelWidth; + updateColour(); + FinishTransforms(true); + } - if (trackedPlayer) + private void updateColour() + { + if (scorePosition == 1) { - panelWidth = extended_width; + mainFillContainer.ResizeWidthTo(extended_width, 200, Easing.OutQuint); panelColour = Color4Extensions.FromHex("7fcc33"); textColour = Color4.White; } - else if (api.Friends.Any(f => User.Equals(f))) + else if (trackedPlayer) { - panelWidth = extended_width; + mainFillContainer.ResizeWidthTo(extended_width, 200, Easing.OutQuint); panelColour = Color4Extensions.FromHex("ffd966"); textColour = Color4Extensions.FromHex("2e576b"); } else { - panelWidth = regular_width; + mainFillContainer.ResizeWidthTo(regular_width, 200, Easing.OutQuint); panelColour = Color4Extensions.FromHex("3399cc"); textColour = Color4.White; } + } + + private Color4 panelColour + { + set + { + mainFillContainer.FadeColour(value, 200, Easing.OutQuint); + centralFill.FadeColour(value, 200, Easing.OutQuint); + } + } + + private Color4 textColour + { + set + { + scoreText.FadeColour(value, 200, Easing.OutQuint); + accuracyText.FadeColour(value, 200, Easing.OutQuint); + comboText.FadeColour(value, 200, Easing.OutQuint); + usernameText.FadeColour(value, 200, Easing.OutQuint); + positionText.FadeColour(value, 200, Easing.OutQuint); + } + } + + [BackgroundDependencyLoader] + private void load() + { + const float panel_shear = 0.15f; + const float shear_width = panel_height * panel_shear; InternalChildren = new Drawable[] { - new Container + mainFillContainer = new Container { + Width = regular_width, + RelativeSizeAxes = Axes.Y, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, Masking = true, CornerRadius = 5f, Shear = new Vector2(panel_shear, 0f), - Size = new Vector2(panelWidth, panel_height), Child = new Box { Alpha = 0.5f, RelativeSizeAxes = Axes.Both, - Colour = panelColour, } }, new GridContainer { + Width = regular_width, + RelativeSizeAxes = Axes.Y, Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Size = new Vector2(regular_width, panel_height), ColumnDimensions = new[] { new Dimension(GridSizeMode.Absolute, 35f), @@ -133,7 +166,7 @@ namespace osu.Game.Screens.Play.HUD Padding = new MarginPadding { Right = shear_width / 2 }, Anchor = Anchor.Centre, Origin = Anchor.Centre, - Colour = textColour, + Colour = Color4.White, Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold), Shadow = false, }, @@ -151,22 +184,22 @@ namespace osu.Game.Screens.Play.HUD RelativeSizeAxes = Axes.Both, Children = new[] { - new Box + centralFill = new Box { Alpha = 0.5f, RelativeSizeAxes = Axes.Both, - Colour = panelColour, + Colour = Color4Extensions.FromHex("3399cc"), }, } }, - new OsuSpriteText + usernameText = new OsuSpriteText { Padding = new MarginPadding { Left = shear_width }, RelativeSizeAxes = Axes.X, Width = 0.8f, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Colour = textColour, + Colour = Color4.White, Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), Text = User.Username, Truncate = true, @@ -180,41 +213,31 @@ namespace osu.Game.Screens.Play.HUD RelativeSizeAxes = Axes.Both, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Colour = textColour, + Colour = Color4.White, Children = new Drawable[] { scoreText = new OsuSpriteText { - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Spacing = new Vector2(0.5f, 0f), - Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold), + Spacing = new Vector2(-1f, 0f), + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true), Shadow = false, }, - new Container + accuracyText = new OsuSpriteText { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Children = new Drawable[] - { - accuracyText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), - Shadow = false, - }, - comboText = new OsuSpriteText - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold), - Shadow = false, - }, - } - } + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-1f, 0f), + Shadow = false, + }, + comboText = new OsuSpriteText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Spacing = new Vector2(-1f, 0f), + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), + Shadow = false, + }, }, } } From e6a38ffbce738d3c464647349b8e5b81b55b373f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 16:33:55 +0900 Subject: [PATCH 5431/6909] Fix test failure due to polluted bindable value from previous test --- osu.Game/Tests/Visual/MultiplayerTestScene.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/MultiplayerTestScene.cs index dc614896c5..6f24e00a92 100644 --- a/osu.Game/Tests/Visual/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/MultiplayerTestScene.cs @@ -11,7 +11,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 => currentRoom.Value; @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual [SetUp] public void Setup() => Schedule(() => { - Room.CopyFrom(new Room()); + currentRoom.Value = new Room(); }); } } From 07a8ffa4aaefeeaa2da30aea60a94381826f7c13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 16:50:25 +0900 Subject: [PATCH 5432/6909] Fix failing tests due to ignoring the lookup ID --- .../Visual/Online/TestSceneCurrentlyPlayingDisplay.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 4f0ca67e64..eaa881b02c 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -89,13 +89,11 @@ namespace osu.Game.Tests.Visual.Online "pishifat" }; - private int id; - protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User { - Id = id++, - Username = usernames[id % usernames.Length], + Id = lookup++, + Username = usernames[lookup % usernames.Length], }); } } From 2db7433c0b11f5a90be103621af529a2c35f50e7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 16:51:59 +0900 Subject: [PATCH 5433/6909] Refactor player score creation and submission process --- .../TestSceneCompletionCancellation.cs | 3 +- .../Screens/Multi/Play/TimeshiftPlayer.cs | 17 ++-- osu.Game/Screens/Play/Player.cs | 79 ++++++++++--------- osu.Game/Screens/Play/ReplayPlayer.cs | 13 +-- 4 files changed, 65 insertions(+), 47 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index 6fd5511e5a..6e3b394057 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -10,6 +10,7 @@ using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Scoring; using osu.Game.Storyboards; using osuTK; @@ -117,7 +118,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - protected override void GotoRanking() + protected override void GotoRanking(ScoreInfo score) { GotoRankingInvoked = true; } diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 0efa9c5196..76e4a328e0 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; @@ -95,19 +96,25 @@ namespace osu.Game.Screens.Multi.Play return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true); } - protected override ScoreInfo CreateScore() + protected override Score CreateScore() { var score = base.CreateScore(); - score.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); + score.ScoreInfo.TotalScore = (int)Math.Round(ScoreProcessor.GetStandardisedScore()); + return score; + } + + protected override async Task SubmitScore(Score score) + { + await base.SubmitScore(score); Debug.Assert(token != null); - var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score); - request.Success += s => score.OnlineScoreID = s.ID; + var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo); + request.Success += s => score.ScoreInfo.OnlineScoreID = s.ID; request.Failure += e => Logger.Error(e, "Failed to submit score"); api.Queue(request); - return score; + return score.ScoreInfo; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 92c76ec2d2..3fb680b9c9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -5,6 +5,7 @@ using System; 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; @@ -527,8 +528,18 @@ namespace osu.Game.Screens.Play if (!showResults) return; - using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) - completionProgressDelegate = Schedule(GotoRanking); + SubmitScore(CreateScore()).ContinueWith(t => Schedule(() => + { + using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) + { + completionProgressDelegate = Schedule(() => + { + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + GotoRanking(t.Result); + }); + } + })); } protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; @@ -727,60 +738,56 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } - protected virtual ScoreInfo CreateScore() + protected virtual Score CreateScore() { - var score = new ScoreInfo + var score = new Score { - Beatmap = Beatmap.Value.BeatmapInfo, - Ruleset = rulesetInfo, - Mods = Mods.Value.ToArray(), + ScoreInfo = new ScoreInfo + { + Beatmap = Beatmap.Value.BeatmapInfo, + Ruleset = rulesetInfo, + Mods = Mods.Value.ToArray(), + } }; if (DrawableRuleset.ReplayScore != null) - score.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); + { + score.ScoreInfo.User = DrawableRuleset.ReplayScore.ScoreInfo?.User ?? new GuestUser(); + score.Replay = DrawableRuleset.ReplayScore.Replay; + } else - score.User = api.LocalUser.Value; + { + score.ScoreInfo.User = api.LocalUser.Value; + if (recordingScore?.Replay.Frames.Count > 0) + score.Replay = recordingScore.Replay; + } - ScoreProcessor.PopulateScore(score); + ScoreProcessor.PopulateScore(score.ScoreInfo); return score; } - protected virtual void GotoRanking() + protected virtual async Task SubmitScore(Score score) { + // Replays are already populated and present in the game's database, so should not be re-imported. if (DrawableRuleset.ReplayScore != null) + return score.ScoreInfo; + + LegacyByteArrayReader replayReader; + + using (var stream = new MemoryStream()) { - // if a replay is present, we likely don't want to import into the local database. - this.Push(CreateResults(CreateScore())); - return; + new LegacyScoreEncoder(score, gameplayBeatmap.PlayableBeatmap).Encode(stream); + replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } - LegacyByteArrayReader replayReader = null; - - var score = new Score { ScoreInfo = CreateScore() }; - - if (recordingScore?.Replay.Frames.Count > 0) - { - score.Replay = recordingScore.Replay; - - 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)); - })); + return await scoreManager.Import(score.ScoreInfo, replayReader); } protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); + protected virtual void GotoRanking(ScoreInfo score) => this.Push(CreateResults(score)); + private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 294d116f51..390d1d1959 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 System.Threading.Tasks; using osu.Framework.Input.Bindings; using osu.Game.Input.Bindings; using osu.Game.Scoring; @@ -26,18 +27,20 @@ namespace osu.Game.Screens.Play DrawableRuleset?.SetReplayScore(Score); } - protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); - - protected override ScoreInfo CreateScore() + protected override Score CreateScore() { var baseScore = base.CreateScore(); // Since the replay score doesn't contain statistics, we'll pass them through here. - Score.ScoreInfo.HitEvents = baseScore.HitEvents; + Score.ScoreInfo.HitEvents = baseScore.ScoreInfo.HitEvents; - return Score.ScoreInfo; + return Score; } + protected override Task SubmitScore(Score score) => Task.FromResult(score.ScoreInfo); + + protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); + public bool OnPressed(GlobalAction action) { switch (action) From 70cda680c0e868f41f27bd902cd2d0712d7ee4b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 16:55:55 +0900 Subject: [PATCH 5434/6909] Update to match new implementation --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index b2dc47ce61..4d39d1a6b3 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; @@ -44,7 +45,7 @@ namespace osu.Game.Screens.Play.HUD private Bindable scoringMode; [BackgroundDependencyLoader] - private void load(OsuConfigManager config) + private void load(OsuConfigManager config, IAPIProvider api) { streamingClient.OnNewFrames += handleIncomingFrames; @@ -58,7 +59,7 @@ namespace osu.Game.Screens.Play.HUD var trackedUser = new TrackedUserData(); userScores[user] = trackedUser; - AddPlayer(trackedUser.Score, resolvedUser); + Add(new GameplayLeaderboardScore(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id)); //trackedUser.Score, resolvedUser); } scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); From 97ff500b0daa28063188e699411b99017e2241bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 16:56:22 +0900 Subject: [PATCH 5435/6909] Make timeshift wait on score submission --- .../Screens/Multi/Play/TimeshiftPlayer.cs | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 76e4a328e0..e106dc3a1c 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -105,16 +105,29 @@ namespace osu.Game.Screens.Multi.Play protected override async Task SubmitScore(Score score) { - await base.SubmitScore(score); - Debug.Assert(token != null); + bool completed = false; var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo); - request.Success += s => score.ScoreInfo.OnlineScoreID = s.ID; - request.Failure += e => Logger.Error(e, "Failed to submit score"); + + request.Success += s => + { + score.ScoreInfo.OnlineScoreID = s.ID; + completed = true; + }; + + request.Failure += e => + { + Logger.Error(e, "Failed to submit score"); + completed = true; + }; + api.Queue(request); - return score.ScoreInfo; + while (!completed) + await Task.Delay(100); + + return await base.SubmitScore(score); } protected override void Dispose(bool isDisposing) From 157a72ec5deb0f5176fd171c57b9e61d30daa958 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 17:07:38 +0900 Subject: [PATCH 5436/6909] Revert previous player add flow via interface --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 16 +++++++--------- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 16 +++++++++++++--- .../Screens/Play/HUD/GameplayLeaderboardScore.cs | 8 ++++---- osu.Game/Screens/Play/HUD/ILeaderboardScore.cs | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/ILeaderboardScore.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index d596def98a..6e9375f69c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay playerScore.Value = 1222333; }); - AddStep("add local player", () => leaderboard.Add(createLeaderboardScore(playerScore, "You", true))); + AddStep("add local player", () => createLeaderboardScore(playerScore, "You", true)); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } @@ -49,8 +49,8 @@ namespace osu.Game.Tests.Visual.Gameplay var player2Score = new BindableDouble(1234567); var player3Score = new BindableDouble(1111111); - AddStep("add player 2", () => leaderboard.Add(createLeaderboardScore(player2Score, "Player 2"))); - AddStep("add player 3", () => leaderboard.Add(createLeaderboardScore(player3Score, "Player 3"))); + AddStep("add player 2", () => createLeaderboardScore(player2Score, "Player 2")); + AddStep("add player 3", () => createLeaderboardScore(player3Score, "Player 3")); AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); @@ -71,15 +71,13 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestRandomScores() { int playerNumber = 1; - AddRepeatStep("add player with random score", () => leaderboard.Add(createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), $"Player {playerNumber++}")), 10); + AddRepeatStep("add player with random score", () => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), $"Player {playerNumber++}"), 10); } - private static GameplayLeaderboardScore createLeaderboardScore(BindableDouble score, string username, bool localOrReplayPlayer = false) + private void createLeaderboardScore(BindableDouble score, string username, bool isTracked = false) { - return new GameplayLeaderboardScore(new User { Username = username }, localOrReplayPlayer) - { - TotalScore = { BindTarget = score }, - }; + var leaderboardScore = leaderboard.AddPlayer(new User { Username = username }, isTracked); + leaderboardScore.TotalScore.BindTo(score); } private class TestGameplayLeaderboard : GameplayLeaderboard diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 573bf54b14..e738477a80 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.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.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Users; using osuTK; namespace osu.Game.Screens.Play.HUD @@ -22,13 +24,21 @@ namespace osu.Game.Screens.Play.HUD LayoutEasing = Easing.OutQuint; } - public override void Add(GameplayLeaderboardScore drawable) + public ILeaderboardScore AddPlayer(User user, bool isTracked) { + var drawable = new GameplayLeaderboardScore(user, isTracked); base.Add(drawable); - drawable?.TotalScore.BindValueChanged(_ => updateScores(), true); + drawable.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(sort), true); + + return drawable; } - private void updateScores() + public override void Add(GameplayLeaderboardScore drawable) + { + throw new InvalidOperationException($"Use {nameof(AddPlayer)} instead."); + } + + private void sort() { var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList(); diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 439210c944..d4d7c69f6b 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public class GameplayLeaderboardScore : CompositeDrawable + public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore { private const float regular_width = 215f; private const float extended_width = 235f; @@ -26,9 +26,9 @@ namespace osu.Game.Screens.Play.HUD private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; - public readonly BindableDouble TotalScore = new BindableDouble(1000000); - public readonly BindableDouble Accuracy = new BindableDouble(1); - public readonly BindableInt Combo = new BindableInt(); + public BindableDouble TotalScore { get; } = new BindableDouble(); + public BindableDouble Accuracy { get; } = new BindableDouble(1); + public BindableInt Combo { get; } = new BindableInt(); private int? scorePosition; diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs new file mode 100644 index 0000000000..bc1a03c5aa --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; + +namespace osu.Game.Screens.Play.HUD +{ + public interface ILeaderboardScore + { + BindableDouble TotalScore { get; } + BindableDouble Accuracy { get; } + BindableInt Combo { get; } + } +} From cb3f89d0a5c73baa360413c9ef9cf08c8ea39617 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 17:13:51 +0900 Subject: [PATCH 5437/6909] Hook up with new leaderboard design --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 4d39d1a6b3..c96f496cd0 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -59,7 +59,11 @@ namespace osu.Game.Screens.Play.HUD var trackedUser = new TrackedUserData(); userScores[user] = trackedUser; - Add(new GameplayLeaderboardScore(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id)); //trackedUser.Score, resolvedUser); + var leaderboardScore = AddPlayer(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id); + + ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy); + ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score); + ((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo); } scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); From bca4d83af794a587eb02eba41357ce6e841381c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 17:07:38 +0900 Subject: [PATCH 5438/6909] Revert previous player add flow via interface --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 16 +++++++--------- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 16 +++++++++++++--- .../Screens/Play/HUD/GameplayLeaderboardScore.cs | 8 ++++---- osu.Game/Screens/Play/HUD/ILeaderboardScore.cs | 14 ++++++++++++++ 4 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 osu.Game/Screens/Play/HUD/ILeaderboardScore.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index d596def98a..6e9375f69c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay playerScore.Value = 1222333; }); - AddStep("add local player", () => leaderboard.Add(createLeaderboardScore(playerScore, "You", true))); + AddStep("add local player", () => createLeaderboardScore(playerScore, "You", true)); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } @@ -49,8 +49,8 @@ namespace osu.Game.Tests.Visual.Gameplay var player2Score = new BindableDouble(1234567); var player3Score = new BindableDouble(1111111); - AddStep("add player 2", () => leaderboard.Add(createLeaderboardScore(player2Score, "Player 2"))); - AddStep("add player 3", () => leaderboard.Add(createLeaderboardScore(player3Score, "Player 3"))); + AddStep("add player 2", () => createLeaderboardScore(player2Score, "Player 2")); + AddStep("add player 3", () => createLeaderboardScore(player3Score, "Player 3")); AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); @@ -71,15 +71,13 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestRandomScores() { int playerNumber = 1; - AddRepeatStep("add player with random score", () => leaderboard.Add(createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), $"Player {playerNumber++}")), 10); + AddRepeatStep("add player with random score", () => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), $"Player {playerNumber++}"), 10); } - private static GameplayLeaderboardScore createLeaderboardScore(BindableDouble score, string username, bool localOrReplayPlayer = false) + private void createLeaderboardScore(BindableDouble score, string username, bool isTracked = false) { - return new GameplayLeaderboardScore(new User { Username = username }, localOrReplayPlayer) - { - TotalScore = { BindTarget = score }, - }; + var leaderboardScore = leaderboard.AddPlayer(new User { Username = username }, isTracked); + leaderboardScore.TotalScore.BindTo(score); } private class TestGameplayLeaderboard : GameplayLeaderboard diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 573bf54b14..e738477a80 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.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.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Game.Users; using osuTK; namespace osu.Game.Screens.Play.HUD @@ -22,13 +24,21 @@ namespace osu.Game.Screens.Play.HUD LayoutEasing = Easing.OutQuint; } - public override void Add(GameplayLeaderboardScore drawable) + public ILeaderboardScore AddPlayer(User user, bool isTracked) { + var drawable = new GameplayLeaderboardScore(user, isTracked); base.Add(drawable); - drawable?.TotalScore.BindValueChanged(_ => updateScores(), true); + drawable.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(sort), true); + + return drawable; } - private void updateScores() + public override void Add(GameplayLeaderboardScore drawable) + { + throw new InvalidOperationException($"Use {nameof(AddPlayer)} instead."); + } + + private void sort() { var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList(); diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 439210c944..d4d7c69f6b 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD { - public class GameplayLeaderboardScore : CompositeDrawable + public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore { private const float regular_width = 215f; private const float extended_width = 235f; @@ -26,9 +26,9 @@ namespace osu.Game.Screens.Play.HUD private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; - public readonly BindableDouble TotalScore = new BindableDouble(1000000); - public readonly BindableDouble Accuracy = new BindableDouble(1); - public readonly BindableInt Combo = new BindableInt(); + public BindableDouble TotalScore { get; } = new BindableDouble(); + public BindableDouble Accuracy { get; } = new BindableDouble(1); + public BindableInt Combo { get; } = new BindableInt(); private int? scorePosition; diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs new file mode 100644 index 0000000000..bc1a03c5aa --- /dev/null +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -0,0 +1,14 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Bindables; + +namespace osu.Game.Screens.Play.HUD +{ + public interface ILeaderboardScore + { + BindableDouble TotalScore { get; } + BindableDouble Accuracy { get; } + BindableInt Combo { get; } + } +} From 4cf013c0058555776eea78a956203cfbbfe8d70a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 17:19:00 +0900 Subject: [PATCH 5439/6909] Fix animation replacing itself even when score position hasn't changed --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index d4d7c69f6b..086ac956e8 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -37,6 +37,9 @@ namespace osu.Game.Screens.Play.HUD get => scorePosition; set { + if (value == scorePosition) + return; + scorePosition = value; if (scorePosition.HasValue) From e82986b76328983a9c648ba274df7689115941af Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 17:19:36 +0900 Subject: [PATCH 5440/6909] Fix panel x positions getting weird duration relayouts Also adjust the transitions a bit to feel better. --- .../Play/HUD/GameplayLeaderboardScore.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 086ac956e8..77e6d320ae 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -67,11 +67,7 @@ namespace osu.Game.Screens.Play.HUD User = user; this.trackedPlayer = trackedPlayer; - AutoSizeAxes = Axes.X; - Height = panel_height; - - Anchor = Anchor.TopRight; - Origin = Anchor.TopRight; + Size = new Vector2(extended_width, panel_height); } protected override void LoadComplete() @@ -82,23 +78,25 @@ namespace osu.Game.Screens.Play.HUD FinishTransforms(true); } + private const double transition_duration = 500; + private void updateColour() { if (scorePosition == 1) { - mainFillContainer.ResizeWidthTo(extended_width, 200, Easing.OutQuint); + mainFillContainer.ResizeWidthTo(extended_width, transition_duration, Easing.OutElastic); panelColour = Color4Extensions.FromHex("7fcc33"); textColour = Color4.White; } else if (trackedPlayer) { - mainFillContainer.ResizeWidthTo(extended_width, 200, Easing.OutQuint); + mainFillContainer.ResizeWidthTo(extended_width, transition_duration, Easing.OutElastic); panelColour = Color4Extensions.FromHex("ffd966"); textColour = Color4Extensions.FromHex("2e576b"); } else { - mainFillContainer.ResizeWidthTo(regular_width, 200, Easing.OutQuint); + mainFillContainer.ResizeWidthTo(regular_width, transition_duration, Easing.OutElastic); panelColour = Color4Extensions.FromHex("3399cc"); textColour = Color4.White; } @@ -108,8 +106,8 @@ namespace osu.Game.Screens.Play.HUD { set { - mainFillContainer.FadeColour(value, 200, Easing.OutQuint); - centralFill.FadeColour(value, 200, Easing.OutQuint); + mainFillContainer.FadeColour(value, transition_duration, Easing.OutQuint); + centralFill.FadeColour(value, transition_duration, Easing.OutQuint); } } From 668536ce562eb30088b66602f294672b5be58d58 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 17:25:48 +0900 Subject: [PATCH 5441/6909] Fix vertical size potentially changing during relayout --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 2 +- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 4 +++- .../Screens/Play/HUD/GameplayLeaderboardScore.cs | 15 ++++++++------- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index 6e9375f69c..ff15e1d2dc 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Gameplay Add(leaderboard = new TestGameplayLeaderboard { Anchor = Anchor.Centre, - Origin = Anchor.TopCentre, + Origin = Anchor.Centre, Scale = new Vector2(2), }); } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index e738477a80..99319c0008 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play.HUD { public GameplayLeaderboard() { - AutoSizeAxes = Axes.Both; + Width = GameplayLeaderboardScore.EXTENDED_WIDTH; Direction = FillDirection.Vertical; @@ -30,6 +30,8 @@ namespace osu.Game.Screens.Play.HUD base.Add(drawable); drawable.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(sort), true); + Height = Count * (GameplayLeaderboardScore.PANEL_HEIGHT + Spacing.Y); + return drawable; } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 77e6d320ae..242e7ff6b9 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -19,10 +19,11 @@ namespace osu.Game.Screens.Play.HUD { public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore { - private const float regular_width = 215f; - private const float extended_width = 235f; + public const float EXTENDED_WIDTH = 235f; - private const float panel_height = 35f; + private const float regular_width = 215f; + + public const float PANEL_HEIGHT = 35f; private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; @@ -67,7 +68,7 @@ namespace osu.Game.Screens.Play.HUD User = user; this.trackedPlayer = trackedPlayer; - Size = new Vector2(extended_width, panel_height); + Size = new Vector2(EXTENDED_WIDTH, PANEL_HEIGHT); } protected override void LoadComplete() @@ -84,13 +85,13 @@ namespace osu.Game.Screens.Play.HUD { if (scorePosition == 1) { - mainFillContainer.ResizeWidthTo(extended_width, transition_duration, Easing.OutElastic); + mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, transition_duration, Easing.OutElastic); panelColour = Color4Extensions.FromHex("7fcc33"); textColour = Color4.White; } else if (trackedPlayer) { - mainFillContainer.ResizeWidthTo(extended_width, transition_duration, Easing.OutElastic); + mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, transition_duration, Easing.OutElastic); panelColour = Color4Extensions.FromHex("ffd966"); textColour = Color4Extensions.FromHex("2e576b"); } @@ -127,7 +128,7 @@ namespace osu.Game.Screens.Play.HUD private void load() { const float panel_shear = 0.15f; - const float shear_width = panel_height * panel_shear; + const float shear_width = PANEL_HEIGHT * panel_shear; InternalChildren = new Drawable[] { From 615352c1e41a13c9dc189003aeb4ee2b8a3f95df Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 17:30:11 +0900 Subject: [PATCH 5442/6909] Fix shear offset not being included in GameplayLeaderboard's own size --- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 9 +++++++-- .../Screens/Play/HUD/GameplayLeaderboardScore.cs | 14 ++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 99319c0008..aa668cc4c8 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -14,7 +14,7 @@ namespace osu.Game.Screens.Play.HUD { public GameplayLeaderboard() { - Width = GameplayLeaderboardScore.EXTENDED_WIDTH; + Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH; Direction = FillDirection.Vertical; @@ -26,7 +26,12 @@ namespace osu.Game.Screens.Play.HUD public ILeaderboardScore AddPlayer(User user, bool isTracked) { - var drawable = new GameplayLeaderboardScore(user, isTracked); + var drawable = new GameplayLeaderboardScore(user, isTracked) + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + }; + base.Add(drawable); drawable.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(sort), true); diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 242e7ff6b9..5c4b4406cc 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -16,6 +16,7 @@ using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD + { public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore { @@ -25,6 +26,10 @@ namespace osu.Game.Screens.Play.HUD public const float PANEL_HEIGHT = 35f; + public const float SHEAR_WIDTH = PANEL_HEIGHT * panel_shear; + + private const float panel_shear = 0.15f; + private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; public BindableDouble TotalScore { get; } = new BindableDouble(); @@ -127,9 +132,6 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load() { - const float panel_shear = 0.15f; - const float shear_width = PANEL_HEIGHT * panel_shear; - InternalChildren = new Drawable[] { mainFillContainer = new Container @@ -165,7 +167,7 @@ namespace osu.Game.Screens.Play.HUD { positionText = new OsuSpriteText { - Padding = new MarginPadding { Right = shear_width / 2 }, + Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 }, Anchor = Anchor.Centre, Origin = Anchor.Centre, Colour = Color4.White, @@ -174,7 +176,7 @@ namespace osu.Game.Screens.Play.HUD }, new Container { - Padding = new MarginPadding { Horizontal = shear_width / 3 }, + Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 }, RelativeSizeAxes = Axes.Both, Children = new Drawable[] { @@ -196,7 +198,7 @@ namespace osu.Game.Screens.Play.HUD }, usernameText = new OsuSpriteText { - Padding = new MarginPadding { Left = shear_width }, + Padding = new MarginPadding { Left = SHEAR_WIDTH }, RelativeSizeAxes = Axes.X, Width = 0.8f, Anchor = Anchor.CentreLeft, From fdad5e86d3baa4adb991717e8408afae2da02bb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 18 Dec 2020 17:33:18 +0900 Subject: [PATCH 5443/6909] Remove stray newline --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 5c4b4406cc..0b58cf76a2 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -16,7 +16,6 @@ using osuTK; using osuTK.Graphics; namespace osu.Game.Screens.Play.HUD - { public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore { From 2958cab239027632f0b9b5c61109bbbd90bfa8b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 17:47:33 +0900 Subject: [PATCH 5444/6909] Remove GotoRanking --- .../TestSceneCompletionCancellation.cs | 13 ++++++++----- osu.Game/Screens/Play/Player.cs | 18 +++++++++++++++--- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs index 6e3b394057..4ee48fd853 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneCompletionCancellation.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Scoring; +using osu.Game.Screens.Ranking; using osu.Game.Storyboards; using osuTK; @@ -51,7 +52,7 @@ namespace osu.Game.Tests.Visual.Gameplay cancel(); complete(); - AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).GotoRankingInvoked); + AddUntilStep("attempted to push ranking", () => ((FakeRankingPushPlayer)Player).ResultsCreated); } /// @@ -85,7 +86,7 @@ namespace osu.Game.Tests.Visual.Gameplay { // wait to ensure there was no attempt of pushing the results screen. AddWaitStep("wait", resultsDisplayWaitCount); - AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).GotoRankingInvoked); + AddAssert("no attempt to push ranking", () => !((FakeRankingPushPlayer)Player).ResultsCreated); } protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) @@ -111,16 +112,18 @@ namespace osu.Game.Tests.Visual.Gameplay public class FakeRankingPushPlayer : TestPlayer { - public bool GotoRankingInvoked; + public bool ResultsCreated { get; private set; } public FakeRankingPushPlayer() : base(true, true) { } - protected override void GotoRanking(ScoreInfo score) + protected override ResultsScreen CreateResults(ScoreInfo score) { - GotoRankingInvoked = true; + var results = base.CreateResults(score); + ResultsCreated = true; + return results; } } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 3fb680b9c9..cc5f32d300 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -536,7 +536,7 @@ namespace osu.Game.Screens.Play { // screen may be in the exiting transition phase. if (this.IsCurrentScreen()) - GotoRanking(t.Result); + this.Push(CreateResults(t.Result)); }); } })); @@ -738,6 +738,10 @@ namespace osu.Game.Screens.Play return base.OnExiting(next); } + /// + /// Creates the player's . + /// + /// The . protected virtual Score CreateScore() { var score = new Score @@ -767,6 +771,11 @@ namespace osu.Game.Screens.Play return score; } + /// + /// Submits the player's . + /// + /// The to submit. + /// The submitted score. protected virtual async Task SubmitScore(Score score) { // Replays are already populated and present in the game's database, so should not be re-imported. @@ -784,10 +793,13 @@ namespace osu.Game.Screens.Play return await scoreManager.Import(score.ScoreInfo, replayReader); } + /// + /// Creates the for a . + /// + /// The to be displayed in the results screen. + /// The . protected virtual ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, true); - protected virtual void GotoRanking(ScoreInfo score) => this.Push(CreateResults(score)); - private void fadeOut(bool instant = false) { float fadeOutDuration = instant ? 0 : 250; From 1369b75a86b93cac21c22c0d4a9305b9864f9258 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 17:48:42 +0900 Subject: [PATCH 5445/6909] Fix potential multiple submission --- osu.Game/Screens/Play/Player.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index cc5f32d300..94c595908e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -502,6 +502,7 @@ namespace osu.Game.Screens.Play } private ScheduledDelegate completionProgressDelegate; + private Task scoreSubmissionTask; private void updateCompletionState(ValueChangedEvent completionState) { @@ -528,7 +529,8 @@ namespace osu.Game.Screens.Play if (!showResults) return; - SubmitScore(CreateScore()).ContinueWith(t => Schedule(() => + scoreSubmissionTask ??= SubmitScore(CreateScore()); + scoreSubmissionTask.ContinueWith(t => Schedule(() => { using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) { From 8826d0155908750dfcc99df83bed41e548bb3391 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 18:20:36 +0900 Subject: [PATCH 5446/6909] Create completion progress delegate immediately --- osu.Game/Screens/Play/Player.cs | 37 ++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 94c595908e..b79b8eeae8 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -529,21 +529,38 @@ namespace osu.Game.Screens.Play if (!showResults) return; - scoreSubmissionTask ??= SubmitScore(CreateScore()); - scoreSubmissionTask.ContinueWith(t => Schedule(() => + scoreSubmissionTask ??= Task.Run(async () => { - using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) + var score = CreateScore(); + + try { - completionProgressDelegate = Schedule(() => - { - // screen may be in the exiting transition phase. - if (this.IsCurrentScreen()) - this.Push(CreateResults(t.Result)); - }); + return await SubmitScore(score); } - })); + catch (Exception ex) + { + Logger.Error(ex, "Score submission failed!"); + return score.ScoreInfo; + } + }); + + using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) + scheduleCompletion(); } + private void scheduleCompletion() => completionProgressDelegate = Schedule(() => + { + if (!scoreSubmissionTask.IsCompleted) + { + scheduleCompletion(); + return; + } + + // screen may be in the exiting transition phase. + if (this.IsCurrentScreen()) + this.Push(CreateResults(scoreSubmissionTask.Result)); + }); + protected override bool OnScroll(ScrollEvent e) => mouseWheelDisabled.Value && !GameplayClockContainer.IsPaused.Value; #region Fail Logic From eccfc8ccd2b420c99cc2b30f868ad74e1214881a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 18:31:49 +0900 Subject: [PATCH 5447/6909] Fix potential cross-reference access --- osu.Game/Screens/Play/Player.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b79b8eeae8..c8f1980ab1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -23,8 +23,10 @@ using osu.Game.Graphics.Containers; using osu.Game.IO.Archives; 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.Replays; using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; @@ -781,8 +783,7 @@ namespace osu.Game.Screens.Play else { score.ScoreInfo.User = api.LocalUser.Value; - if (recordingScore?.Replay.Frames.Count > 0) - score.Replay = recordingScore.Replay; + score.Replay = new Replay { Frames = recordingScore?.Replay.Frames.ToList() ?? new List() }; } ScoreProcessor.PopulateScore(score.ScoreInfo); From c9e75e790830816cd4b90525a71a625319e43bae Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Dec 2020 13:09:05 +0300 Subject: [PATCH 5448/6909] Add user avatar to leaderboard scores --- .../Play/HUD/GameplayLeaderboardScore.cs | 53 +++++++++++++++---- 1 file changed, 43 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 0b58cf76a2..9c444a6f1f 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Users; +using osu.Game.Users.Drawables; using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -129,7 +130,7 @@ namespace osu.Game.Screens.Play.HUD } [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { InternalChildren = new Drawable[] { @@ -195,19 +196,51 @@ namespace osu.Game.Screens.Play.HUD }, } }, - usernameText = new OsuSpriteText + new FillFlowContainer { Padding = new MarginPadding { Left = SHEAR_WIDTH }, - RelativeSizeAxes = Axes.X, - Width = 0.8f, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Colour = Color4.White, - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), - Text = User.Username, - Truncate = true, - Shadow = false, - } + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4f, 0f), + Children = new Drawable[] + { + new CircularContainer + { + Masking = true, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(25f), + Children = new Drawable[] + { + new Box + { + Name = "Placeholder while avatar loads", + Alpha = 0.3f, + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray4, + }, + new UpdateableAvatar(User) + { + RelativeSizeAxes = Axes.Both, + }, + } + }, + usernameText = new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Width = 0.8f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.White, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), + Text = User.Username, + Truncate = true, + Shadow = false, + } + } + }, } }, new Container From 030dce55599422cc4017b5b754eab9a66cee623a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Dec 2020 13:09:50 +0300 Subject: [PATCH 5449/6909] Increase leaderboard score width a bit --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 9c444a6f1f..af3cb640bb 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -20,9 +20,9 @@ namespace osu.Game.Screens.Play.HUD { public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore { - public const float EXTENDED_WIDTH = 235f; + public const float EXTENDED_WIDTH = 255f; - private const float regular_width = 215f; + private const float regular_width = 235f; public const float PANEL_HEIGHT = 35f; From 228acf25a7d1b483841f4c6b1bbc4ab46c15aad6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 18 Dec 2020 13:13:31 +0300 Subject: [PATCH 5450/6909] Add test case creating leaderboard scores with existing users --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index ff15e1d2dc..ca61672ef9 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Gameplay playerScore.Value = 1222333; }); - AddStep("add local player", () => createLeaderboardScore(playerScore, "You", true)); + AddStep("add local player", () => createLeaderboardScore(playerScore, new User { Username = "You", Id = 3 }, true)); AddSliderStep("set player score", 50, 5000000, 1222333, v => playerScore.Value = v); } @@ -49,8 +49,8 @@ namespace osu.Game.Tests.Visual.Gameplay var player2Score = new BindableDouble(1234567); var player3Score = new BindableDouble(1111111); - AddStep("add player 2", () => createLeaderboardScore(player2Score, "Player 2")); - AddStep("add player 3", () => createLeaderboardScore(player3Score, "Player 3")); + AddStep("add player 2", () => createLeaderboardScore(player2Score, new User { Username = "Player 2" })); + AddStep("add player 3", () => createLeaderboardScore(player3Score, new User { Username = "Player 3" })); AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); @@ -71,12 +71,23 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestRandomScores() { int playerNumber = 1; - AddRepeatStep("add player with random score", () => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), $"Player {playerNumber++}"), 10); + AddRepeatStep("add player with random score", () => createRandomScore(new User { Username = $"Player {playerNumber++}" }), 10); } - private void createLeaderboardScore(BindableDouble score, string username, bool isTracked = false) + [Test] + public void TestExistingUsers() { - var leaderboardScore = leaderboard.AddPlayer(new User { Username = username }, isTracked); + AddStep("add peppy", () => createRandomScore(new User { Username = "peppy", Id = 2 })); + AddStep("add smoogipoo", () => createRandomScore(new User { Username = "smoogipoo", Id = 1040328 })); + AddStep("add flyte", () => createRandomScore(new User { Username = "flyte", Id = 3103765 })); + AddStep("add frenzibyte", () => createRandomScore(new User { Username = "frenzibyte", Id = 14210502 })); + } + + private void createRandomScore(User user) => createLeaderboardScore(new BindableDouble(RNG.Next(0, 5_000_000)), user); + + private void createLeaderboardScore(BindableDouble score, User user, bool isTracked = false) + { + var leaderboardScore = leaderboard.AddPlayer(user, isTracked); leaderboardScore.TotalScore.BindTo(score); } From 122250f454059d9adc231e982bd976e3795e1beb Mon Sep 17 00:00:00 2001 From: Graham Johnson Date: Fri, 18 Dec 2020 10:45:23 -0500 Subject: [PATCH 5451/6909] replace drag cirle function with dictionary --- .../Edit/Compose/Components/SelectionBox.cs | 61 ++++++------------- 1 file changed, 17 insertions(+), 44 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 8ccd4f71e9..85e86499be 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -235,13 +236,27 @@ namespace osu.Game.Screens.Edit.Compose.Components private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle { Anchor = anchor, - X = getAdjustmentToCenterCircleOnBorder(anchor).X, - Y = getAdjustmentToCenterCircleOnBorder(anchor).Y, + X = dragCircleAdjustments[anchor].X, + Y = dragCircleAdjustments[anchor].Y, HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), OperationStarted = operationStarted, OperationEnded = operationEnded }); + /// + /// Adjust Drag circle to be centered on the center of the border instead of on the edge. + /// + private Dictionary dragCircleAdjustments = new Dictionary(){ + {Anchor.TopLeft, new Vector2(BORDER_RADIUS / 2)}, + {Anchor.CentreLeft, new Vector2(BORDER_RADIUS / 2, 0)}, + {Anchor.BottomLeft, new Vector2(BORDER_RADIUS / 2, -BORDER_RADIUS / 2)}, + {Anchor.TopCentre, new Vector2(0, BORDER_RADIUS / 2)}, + {Anchor.BottomCentre, new Vector2(0, -BORDER_RADIUS / 2)}, + {Anchor.TopRight, new Vector2(-BORDER_RADIUS / 2, BORDER_RADIUS / 2)}, + {Anchor.CentreRight, new Vector2(-BORDER_RADIUS / 2, 0)}, + {Anchor.BottomRight, new Vector2(-BORDER_RADIUS / 2)} + }; + private int activeOperations; private float convertDragEventToAngleOfRotation(DragEvent e) @@ -253,48 +268,6 @@ namespace osu.Game.Screens.Edit.Compose.Components return (endAngle - startAngle) * 180 / MathF.PI; } - /// - /// Adjust Drag circle to be centered on the center of the border instead of on the edge. - /// - /// The part of the rectangle to be adjusted. - /// A 2d vector on how much to adjust the drag circle - private Vector2 getAdjustmentToCenterCircleOnBorder(Anchor anchor) - { - Vector2 adjustment = Vector2.Zero; - - switch (anchor) - { - case Anchor.TopLeft: - case Anchor.CentreLeft: - case Anchor.BottomLeft: - adjustment.X = BORDER_RADIUS / 2; - break; - - case Anchor.TopRight: - case Anchor.CentreRight: - case Anchor.BottomRight: - adjustment.X = -BORDER_RADIUS / 2; - break; - } - - switch (anchor) - { - case Anchor.TopLeft: - case Anchor.TopCentre: - case Anchor.TopRight: - adjustment.Y = BORDER_RADIUS / 2; - break; - - case Anchor.BottomLeft: - case Anchor.BottomCentre: - case Anchor.BottomRight: - adjustment.Y = -BORDER_RADIUS / 2; - break; - } - - return adjustment; - } - private void operationEnded() { if (--activeOperations == 0) From 4494bb1eb5a587157f180f3ae14d078a0604194d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 00:15:41 +0900 Subject: [PATCH 5452/6909] Abstract RoomManager and Multiplayer --- .../Multiplayer/TestSceneMultiScreen.cs | 3 +- .../Navigation/TestSceneScreenNavigation.cs | 5 +- osu.Game/Screens/Menu/MainMenu.cs | 4 +- .../Components/ListingPollingComponent.cs | 68 ++++ .../Screens/Multi/Components/RoomManager.cs | 188 ++++++++++ .../Multi/Components/RoomPollingComponent.cs | 41 +++ .../Components/SelectionPollingComponent.cs | 69 ++++ osu.Game/Screens/Multi/IRoomManager.cs | 2 + osu.Game/Screens/Multi/Multiplayer.cs | 60 +--- osu.Game/Screens/Multi/RoomManager.cs | 337 ------------------ .../Multi/Timeshift/TimeshiftMultiplayer.cs | 48 +++ .../Multi/Timeshift/TimeshiftRoomManager.cs | 20 ++ 12 files changed, 460 insertions(+), 385 deletions(-) create mode 100644 osu.Game/Screens/Multi/Components/ListingPollingComponent.cs create mode 100644 osu.Game/Screens/Multi/Components/RoomManager.cs create mode 100644 osu.Game/Screens/Multi/Components/RoomPollingComponent.cs create mode 100644 osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs delete mode 100644 osu.Game/Screens/Multi/RoomManager.cs create mode 100644 osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs create mode 100644 osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs index 3924b0333f..0390b995e1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Overlays; +using osu.Game.Screens.Multi.Timeshift; namespace osu.Game.Tests.Visual.Multiplayer { @@ -17,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestSceneMultiScreen() { - Screens.Multi.Multiplayer multi = new Screens.Multi.Multiplayer(); + var multi = new TimeshiftMultiplayer(); AddStep("show", () => LoadScreen(multi)); AddUntilStep("wait for loaded", () => multi.IsLoaded); diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index d87854a7ea..43f97d8ace 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; +using osu.Game.Screens.Multi.Timeshift; using osu.Game.Screens.Play; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Options; @@ -107,14 +108,14 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitMultiWithEscape() { - PushAndConfirm(() => new Screens.Multi.Multiplayer()); + PushAndConfirm(() => new TimeshiftMultiplayer()); exitViaEscapeAndConfirm(); } [Test] public void TestExitMultiWithBackButton() { - PushAndConfirm(() => new Screens.Multi.Multiplayer()); + PushAndConfirm(() => new TimeshiftMultiplayer()); exitViaBackButtonAndConfirm(); } diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index c3ecd75963..b781c347f0 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -17,7 +17,7 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; -using osu.Game.Screens.Multi; +using osu.Game.Screens.Multi.Timeshift; using osu.Game.Screens.Select; namespace osu.Game.Screens.Menu @@ -104,7 +104,7 @@ namespace osu.Game.Screens.Menu this.Push(new Editor()); }, OnSolo = onSolo, - OnMulti = delegate { this.Push(new Multiplayer()); }, + OnMulti = delegate { this.Push(new TimeshiftMultiplayer()); }, OnExit = confirmAndExit, } } diff --git a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs new file mode 100644 index 0000000000..e22f09779e --- /dev/null +++ b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs @@ -0,0 +1,68 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Screens.Multi.Components +{ + /// + /// A that polls for the lounge listing. + /// + public class ListingPollingComponent : RoomPollingComponent + { + [Resolved] + private Bindable currentFilter { get; set; } + + [Resolved] + private Bindable selectedRoom { 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.Status, currentFilter.Value.Category); + + pollReq.Success += result => + { + for (int i = 0; i < result.Count; i++) + { + if (result[i].RoomID.Value == selectedRoom.Value?.RoomID.Value) + { + // The listing request always has less information than the opened room, so don't include it. + result[i] = selectedRoom.Value; + break; + } + } + + NotifyRoomsReceived(result); + tcs.SetResult(true); + }; + + pollReq.Failure += _ => tcs.SetResult(false); + + API.Queue(pollReq); + + return tcs.Task; + } + } +} diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs new file mode 100644 index 0000000000..46941dc58e --- /dev/null +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -0,0 +1,188 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; + +namespace osu.Game.Screens.Multi.Components +{ + public abstract class RoomManager : CompositeDrawable, IRoomManager + { + public event Action RoomsUpdated; + + private readonly BindableList rooms = new BindableList(); + + public Bindable InitialRoomsReceived { get; } = new Bindable(); + + public IBindableList Rooms => rooms; + + [Resolved] + private RulesetStore rulesets { get; set; } + + [Resolved] + private BeatmapManager beatmaps { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + private Room joinedRoom; + + protected RoomManager() + { + RelativeSizeAxes = Axes.Both; + + InternalChildren = CreatePollingComponents().Select(p => + { + p.InitialRoomsReceived.BindTo(InitialRoomsReceived); + p.RoomsReceived = onRoomsReceived; + return p; + }).ToList(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + PartRoom(); + } + + public virtual void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + { + room.Host.Value = api.LocalUser.Value; + + var req = new CreateRoomRequest(room); + + req.Success += result => + { + joinedRoom = room; + + update(room, result); + addRoom(room); + + RoomsUpdated?.Invoke(); + onSuccess?.Invoke(room); + }; + + req.Failure += exception => + { + if (req.Result != null) + onError?.Invoke(req.Result.Error); + else + Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important); + }; + + api.Queue(req); + } + + private JoinRoomRequest currentJoinRoomRequest; + + public virtual void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + { + currentJoinRoomRequest?.Cancel(); + currentJoinRoomRequest = new JoinRoomRequest(room); + + currentJoinRoomRequest.Success += () => + { + joinedRoom = room; + onSuccess?.Invoke(room); + }; + + currentJoinRoomRequest.Failure += exception => + { + if (!(exception is OperationCanceledException)) + Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important); + onError?.Invoke(exception.ToString()); + }; + + api.Queue(currentJoinRoomRequest); + } + + public void PartRoom() + { + currentJoinRoomRequest?.Cancel(); + + if (joinedRoom == null) + return; + + api.Queue(new PartRoomRequest(joinedRoom)); + joinedRoom = null; + } + + private readonly HashSet ignoredRooms = new HashSet(); + + private void onRoomsReceived(List received) + { + // Remove past matches + foreach (var r in rooms.ToList()) + { + if (received.All(e => e.RoomID.Value != r.RoomID.Value)) + rooms.Remove(r); + } + + for (int i = 0; i < received.Count; i++) + { + var room = received[i]; + + Debug.Assert(room.RoomID.Value != null); + + if (ignoredRooms.Contains(room.RoomID.Value.Value)) + continue; + + room.Position.Value = i; + + try + { + update(room, room); + addRoom(room); + } + catch (Exception ex) + { + Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); + + ignoredRooms.Add(room.RoomID.Value.Value); + rooms.Remove(room); + } + } + + RoomsUpdated?.Invoke(); + } + + /// + /// Updates a local with a remote copy. + /// + /// The local to update. + /// The remote to update with. + private void update(Room local, Room remote) + { + foreach (var pi in remote.Playlist) + pi.MapObjects(beatmaps, rulesets); + + local.CopyFrom(remote); + } + + /// + /// Adds a to the list of available rooms. + /// + /// The to add. + private void addRoom(Room room) + { + var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); + if (existing == null) + rooms.Add(room); + else + existing.CopyFrom(room); + } + + protected abstract RoomPollingComponent[] CreatePollingComponents(); + } +} diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs new file mode 100644 index 0000000000..5430d54644 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/RoomPollingComponent.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 System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.Multi.Components +{ + public abstract class RoomPollingComponent : PollingComponent + { + public Action> RoomsReceived; + + /// + /// The time in milliseconds to wait between polls. + /// Setting to zero stops all polling. + /// + public new readonly Bindable TimeBetweenPolls = new Bindable(); + + public IBindable InitialRoomsReceived => initialRoomsReceived; + private readonly Bindable initialRoomsReceived = new Bindable(); + + [Resolved] + protected IAPIProvider API { get; private set; } + + protected RoomPollingComponent() + { + TimeBetweenPolls.BindValueChanged(time => base.TimeBetweenPolls = time.NewValue); + } + + protected void NotifyRoomsReceived(List rooms) + { + initialRoomsReceived.Value = true; + RoomsReceived?.Invoke(rooms); + } + } +} diff --git a/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs b/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs new file mode 100644 index 0000000000..544d5b2388 --- /dev/null +++ b/osu.Game/Screens/Multi/Components/SelectionPollingComponent.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.Collections.Generic; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Screens.Multi.Components +{ + /// + /// A that polls for the currently-selected room. + /// + public class SelectionPollingComponent : RoomPollingComponent + { + [Resolved] + private Bindable selectedRoom { get; set; } + + [Resolved] + private IRoomManager roomManager { 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 => + { + var rooms = new List(roomManager.Rooms); + + int index = rooms.FindIndex(r => r.RoomID == result.RoomID); + if (index < 0) + return; + + rooms[index] = result; + + NotifyRoomsReceived(rooms); + tcs.SetResult(true); + }; + + pollReq.Failure += _ => tcs.SetResult(false); + + API.Queue(pollReq); + + return tcs.Task; + } + } +} diff --git a/osu.Game/Screens/Multi/IRoomManager.cs b/osu.Game/Screens/Multi/IRoomManager.cs index bf75843c3e..3d18edcd71 100644 --- a/osu.Game/Screens/Multi/IRoomManager.cs +++ b/osu.Game/Screens/Multi/IRoomManager.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.Bindables; using osu.Game.Online.Multiplayer; namespace osu.Game.Screens.Multi { + [Cached(typeof(IRoomManager))] public interface IRoomManager { /// diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index a323faeea1..5f61c5e635 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; @@ -30,7 +29,7 @@ using osuTK; namespace osu.Game.Screens.Multi { [Cached] - public class Multiplayer : OsuScreen + public abstract class Multiplayer : OsuScreen { public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true; @@ -46,6 +45,9 @@ namespace osu.Game.Screens.Multi private readonly IBindable isIdle = new BindableBool(); + [Cached(Type = typeof(IRoomManager))] + protected IRoomManager RoomManager { get; private set; } + [Cached] private readonly Bindable selectedRoom = new Bindable(); @@ -55,9 +57,6 @@ namespace osu.Game.Screens.Multi [Resolved(CanBeNull = true)] private MusicController music { get; set; } - [Cached(Type = typeof(IRoomManager))] - private RoomManager roomManager; - [Resolved] private OsuGameBase game { get; set; } @@ -70,7 +69,7 @@ namespace osu.Game.Screens.Multi private readonly Drawable header; private readonly Drawable headerBackground; - public Multiplayer() + protected Multiplayer() { Anchor = Anchor.Centre; Origin = Anchor.Centre; @@ -82,7 +81,7 @@ namespace osu.Game.Screens.Multi InternalChild = waves = new MultiplayerWaveContainer { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Children = new[] { new Box { @@ -137,7 +136,7 @@ namespace osu.Game.Screens.Multi Origin = Anchor.TopRight, Action = () => CreateRoom() }, - roomManager = new RoomManager() + (Drawable)(RoomManager = CreateRoomManager()) } }; @@ -168,7 +167,7 @@ namespace osu.Game.Screens.Multi protected override void LoadComplete() { base.LoadComplete(); - isIdle.BindValueChanged(idle => updatePollingRate(idle.NewValue), true); + isIdle.BindValueChanged(idle => UpdatePollingRate(idle.NewValue), true); } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) @@ -178,36 +177,7 @@ namespace osu.Game.Screens.Multi return dependencies; } - private void updatePollingRate(bool idle) - { - if (!this.IsCurrentScreen()) - { - roomManager.TimeBetweenListingPolls = 0; - roomManager.TimeBetweenSelectionPolls = 0; - } - else - { - switch (screenStack.CurrentScreen) - { - case LoungeSubScreen _: - roomManager.TimeBetweenListingPolls = idle ? 120000 : 15000; - roomManager.TimeBetweenSelectionPolls = idle ? 120000 : 15000; - 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})"); - } + protected abstract void UpdatePollingRate(bool isIdle); private void forcefullyExit() { @@ -241,7 +211,7 @@ namespace osu.Game.Screens.Multi beginHandlingTrack(); - updatePollingRate(isIdle.Value); + UpdatePollingRate(isIdle.Value); } public override void OnSuspending(IScreen next) @@ -251,12 +221,12 @@ namespace osu.Game.Screens.Multi endHandlingTrack(); - updatePollingRate(isIdle.Value); + UpdatePollingRate(isIdle.Value); } public override bool OnExiting(IScreen next) { - roomManager.PartRoom(); + RoomManager.PartRoom(); waves.Hide(); @@ -344,12 +314,14 @@ namespace osu.Game.Screens.Multi if (newScreen is IOsuScreen newOsuScreen) ((IBindable)Activity).BindTo(newOsuScreen.Activity); - updatePollingRate(isIdle.Value); + UpdatePollingRate(isIdle.Value); createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200); updateTrack(); } + protected IScreen CurrentSubScreen => screenStack.CurrentScreen; + private void updateTrack(ValueChangedEvent _ = null) { if (screenStack.CurrentScreen is MatchSubScreen) @@ -381,6 +353,8 @@ namespace osu.Game.Screens.Multi } } + protected abstract IRoomManager CreateRoomManager(); + private class MultiplayerWaveContainer : WaveContainer { protected override bool StartHidden => true; diff --git a/osu.Game/Screens/Multi/RoomManager.cs b/osu.Game/Screens/Multi/RoomManager.cs deleted file mode 100644 index fb0cf73bb9..0000000000 --- a/osu.Game/Screens/Multi/RoomManager.cs +++ /dev/null @@ -1,337 +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.Diagnostics; -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; -using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; -using osu.Game.Rulesets; -using osu.Game.Screens.Multi.Lounge.Components; - -namespace osu.Game.Screens.Multi -{ - public class RoomManager : CompositeDrawable, IRoomManager - { - public event Action RoomsUpdated; - - private readonly BindableList rooms = new BindableList(); - - public Bindable InitialRoomsReceived { get; } = new Bindable(); - - public IBindableList Rooms => rooms; - - public double TimeBetweenListingPolls - { - get => listingPollingComponent.TimeBetweenPolls; - set => listingPollingComponent.TimeBetweenPolls = value; - } - - public double TimeBetweenSelectionPolls - { - get => selectionPollingComponent.TimeBetweenPolls; - set => selectionPollingComponent.TimeBetweenPolls = value; - } - - [Resolved] - private RulesetStore rulesets { get; set; } - - [Resolved] - private BeatmapManager beatmaps { get; set; } - - [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() - { - RelativeSizeAxes = Axes.Both; - - InternalChildren = new Drawable[] - { - listingPollingComponent = new ListingPollingComponent - { - InitialRoomsReceived = { BindTarget = InitialRoomsReceived }, - RoomsReceived = onListingReceived - }, - selectionPollingComponent = new SelectionPollingComponent { RoomReceived = onSelectedRoomReceived } - }; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - PartRoom(); - } - - public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) - { - room.Host.Value = api.LocalUser.Value; - - var req = new CreateRoomRequest(room); - - req.Success += result => - { - joinedRoom = room; - - update(room, result); - addRoom(room); - - RoomsUpdated?.Invoke(); - onSuccess?.Invoke(room); - }; - - req.Failure += exception => - { - if (req.Result != null) - onError?.Invoke(req.Result.Error); - else - Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important); - }; - - api.Queue(req); - } - - private JoinRoomRequest currentJoinRoomRequest; - - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) - { - currentJoinRoomRequest?.Cancel(); - currentJoinRoomRequest = new JoinRoomRequest(room); - - currentJoinRoomRequest.Success += () => - { - joinedRoom = room; - onSuccess?.Invoke(room); - }; - - currentJoinRoomRequest.Failure += exception => - { - if (!(exception is OperationCanceledException)) - Logger.Log($"Failed to join room: {exception}", level: LogLevel.Important); - onError?.Invoke(exception.ToString()); - }; - - api.Queue(currentJoinRoomRequest); - } - - public void PartRoom() - { - currentJoinRoomRequest?.Cancel(); - - if (joinedRoom == null) - return; - - api.Queue(new PartRoomRequest(joinedRoom)); - joinedRoom = null; - } - - private readonly HashSet ignoredRooms = new HashSet(); - - /// - /// Invoked when the listing of all s is received from the server. - /// - /// The listing. - private void onListingReceived(List listing) - { - // 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) - { - // 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; - } - - var room = listing[i]; - - Debug.Assert(room.RoomID.Value != null); - - if (ignoredRooms.Contains(room.RoomID.Value.Value)) - continue; - - room.Position.Value = i; - - try - { - update(room, room); - addRoom(room); - } - catch (Exception ex) - { - Logger.Error(ex, $"Failed to update room: {room.Name.Value}."); - - ignoredRooms.Add(room.RoomID.Value.Value); - rooms.Remove(room); - } - } - - RoomsUpdated?.Invoke(); - } - - /// - /// Invoked when a is received from the server. - /// - /// The received . - private void onSelectedRoomReceived(Room toUpdate) - { - foreach (var room in rooms) - { - if (room.RoomID.Value == toUpdate.RoomID.Value) - { - toUpdate.Position.Value = room.Position.Value; - update(room, toUpdate); - break; - } - } - } - - /// - /// Updates a local with a remote copy. - /// - /// The local to update. - /// The remote to update with. - private void update(Room local, Room remote) - { - foreach (var pi in remote.Playlist) - pi.MapObjects(beatmaps, rulesets); - - local.CopyFrom(remote); - } - - /// - /// Adds a to the list of available rooms. - /// - /// The to add. - private void addRoom(Room room) - { - var existing = rooms.FirstOrDefault(e => e.RoomID.Value == room.RoomID.Value); - if (existing == null) - rooms.Add(room); - 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; - - public readonly Bindable InitialRoomsReceived = new Bindable(); - - [Resolved] - private IAPIProvider api { get; set; } - - [Resolved] - private Bindable currentFilter { get; set; } - - [BackgroundDependencyLoader] - private void load() - { - currentFilter.BindValueChanged(_ => - { - InitialRoomsReceived.Value = false; - - 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.Status, currentFilter.Value.Category); - - pollReq.Success += result => - { - InitialRoomsReceived.Value = true; - RoomsReceived?.Invoke(result); - tcs.SetResult(true); - }; - - pollReq.Failure += _ => tcs.SetResult(false); - - api.Queue(pollReq); - - return tcs.Task; - } - } - } -} diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs new file mode 100644 index 0000000000..1ff9c670a8 --- /dev/null +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.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.Logging; +using osu.Framework.Screens; +using osu.Game.Screens.Multi.Lounge; +using osu.Game.Screens.Multi.Match; + +namespace osu.Game.Screens.Multi.Timeshift +{ + public class TimeshiftMultiplayer : Multiplayer + { + protected override void UpdatePollingRate(bool isIdle) + { + var timeshiftManager = (TimeshiftRoomManager)RoomManager; + + if (!this.IsCurrentScreen()) + { + timeshiftManager.TimeBetweenListingPolls.Value = 0; + timeshiftManager.TimeBetweenSelectionPolls.Value = 0; + } + else + { + switch (CurrentSubScreen) + { + case LoungeSubScreen _: + timeshiftManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; + timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; + break; + + case MatchSubScreen _: + timeshiftManager.TimeBetweenListingPolls.Value = 0; + timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000; + break; + + default: + timeshiftManager.TimeBetweenListingPolls.Value = 0; + timeshiftManager.TimeBetweenSelectionPolls.Value = 0; + break; + } + } + + Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})"); + } + + protected override IRoomManager CreateRoomManager() => new TimeshiftRoomManager(); + } +} diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs new file mode 100644 index 0000000000..ba96721afc --- /dev/null +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.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.Bindables; +using osu.Game.Screens.Multi.Components; + +namespace osu.Game.Screens.Multi.Timeshift +{ + public class TimeshiftRoomManager : RoomManager + { + public readonly Bindable TimeBetweenListingPolls = new Bindable(); + public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); + + protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] + { + new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } }, + new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } } + }; + } +} From f4e9703deb20a7abdd303781a17c63e736951136 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 00:52:32 +0900 Subject: [PATCH 5453/6909] Fix incorrect comparison --- osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs b/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs index 544d5b2388..37a190b5e0 100644 --- a/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Multi.Components { var rooms = new List(roomManager.Rooms); - int index = rooms.FindIndex(r => r.RoomID == result.RoomID); + int index = rooms.FindIndex(r => r.RoomID.Value == result.RoomID.Value); if (index < 0) return; From ab9158c306c5b19c6f314967216ab4fe4915fd23 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 18 Dec 2020 20:50:54 +0900 Subject: [PATCH 5454/6909] Add a stateful multiplayer client --- .../StatefulMultiplayerClient.cs | 389 ++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs new file mode 100644 index 0000000000..3e2f435524 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -0,0 +1,389 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.RoomStatuses; +using osu.Game.Rulesets; +using osu.Game.Users; +using osu.Game.Utils; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer + { + /// + /// Invoked when any change occurs to the multiplayer room. + /// + public event Action? RoomChanged; + + /// + /// Invoked when the multiplayer server requests the current beatmap to be loaded into play. + /// + public event Action? LoadRequested; + + /// + /// Invoked when the multiplayer server requests gameplay to be started. + /// + public event Action? MatchStarted; + + /// + /// Invoked when the multiplayer server has finished collating results. + /// + public event Action? ResultsReady; + + /// + /// Whether the is currently connected. + /// + public abstract IBindable IsConnected { get; } + + /// + /// The joined . + /// + public MultiplayerRoom? Room { get; private set; } + + /// + /// The users currently in gameplay. + /// + public readonly BindableList PlayingUsers = new BindableList(); + + [Resolved] + private UserLookupCache userLookupCache { get; set; } = null!; + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + [Resolved] + private RulesetStore rulesets { get; set; } = null!; + + private Room? apiRoom; + private int playlistItemId; // Todo: THIS IS SUPER TEMPORARY!! + + /// + /// Joins the for a given API . + /// + /// The API . + public async Task JoinRoom(Room room) + { + Debug.Assert(Room == null); + Debug.Assert(room.RoomID.Value != null); + + apiRoom = room; + playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; + + Room = await JoinRoom(room.RoomID.Value.Value); + + Debug.Assert(Room != null); + + foreach (var user in Room.Users) + await PopulateUser(user); + + updateLocalRoomSettings(Room.Settings); + } + + /// + /// Joins the with a given ID. + /// + /// The room ID. + /// The joined . + protected abstract Task JoinRoom(long roomId); + + public virtual Task LeaveRoom() + { + if (Room == null) + return Task.CompletedTask; + + apiRoom = null; + Room = null; + + Schedule(() => RoomChanged?.Invoke()); + + return Task.CompletedTask; + } + + /// + /// Change the current settings. + /// + /// + /// A room must have been joined via for this to have any effect. + /// + /// The new room name, if any. + /// The new room playlist item, if any. + public void ChangeSettings(Optional name = default, Optional item = default) + { + if (Room == null) + return; + + // A dummy playlist item filled with the current room settings (except mods). + var existingPlaylistItem = new PlaylistItem + { + Beatmap = + { + Value = new BeatmapInfo + { + OnlineBeatmapID = Room.Settings.BeatmapID, + MD5Hash = Room.Settings.BeatmapChecksum + } + }, + RulesetID = Room.Settings.RulesetID + }; + + var newSettings = new MultiplayerRoomSettings + { + Name = name.GetOr(Room.Settings.Name), + BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, + BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, + RulesetID = item.GetOr(existingPlaylistItem).RulesetID, + Mods = item.HasValue ? item.Value!.RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods + }; + + // Make sure there would be a meaningful change in settings. + if (newSettings.Equals(Room.Settings)) + return; + + ChangeSettings(newSettings); + } + + public abstract Task TransferHost(int userId); + + public abstract Task ChangeSettings(MultiplayerRoomSettings settings); + + public abstract Task ChangeState(MultiplayerUserState newState); + + public abstract Task StartMatch(); + + Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) + { + Schedule(() => + { + if (Room == null) + return; + + Debug.Assert(apiRoom != null); + + Room.State = state; + + switch (state) + { + case MultiplayerRoomState.Open: + apiRoom.Status.Value = new RoomStatusOpen(); + break; + + case MultiplayerRoomState.Playing: + apiRoom.Status.Value = new RoomStatusPlaying(); + break; + + case MultiplayerRoomState.Closed: + apiRoom.Status.Value = new RoomStatusEnded(); + break; + } + + RoomChanged?.Invoke(); + }); + + return Task.CompletedTask; + } + + async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user) + { + await PopulateUser(user); + + Schedule(() => + { + if (Room == null) + return; + + Room.Users.Add(user); + + RoomChanged?.Invoke(); + }); + } + + Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) + { + Schedule(() => + { + if (Room == null) + return; + + Room.Users.Remove(user); + PlayingUsers.Remove(user.UserID); + + RoomChanged?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.HostChanged(int userId) + { + Schedule(() => + { + if (Room == null) + return; + + Debug.Assert(apiRoom != null); + + var user = Room.Users.FirstOrDefault(u => u.UserID == userId); + + Room.Host = user; + apiRoom.Host.Value = user?.User; + + RoomChanged?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.SettingsChanged(MultiplayerRoomSettings newSettings) + { + updateLocalRoomSettings(newSettings); + return Task.CompletedTask; + } + + Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state) + { + Schedule(() => + { + if (Room == null) + return; + + Room.Users.Single(u => u.UserID == userId).State = state; + + if (state != MultiplayerUserState.Playing) + PlayingUsers.Remove(userId); + + RoomChanged?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.LoadRequested() + { + Schedule(() => + { + if (Room == null) + return; + + LoadRequested?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.MatchStarted() + { + Debug.Assert(Room != null); + var players = Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID).ToList(); + + Schedule(() => + { + if (Room == null) + return; + + PlayingUsers.AddRange(players); + + MatchStarted?.Invoke(); + }); + + return Task.CompletedTask; + } + + Task IMultiplayerClient.ResultsReady() + { + Schedule(() => + { + if (Room == null) + return; + + ResultsReady?.Invoke(); + }); + + return Task.CompletedTask; + } + + /// + /// Populates the for a given . + /// + /// The to populate. + protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID); + + /// + /// Updates the local room settings with the given . + /// + /// + /// This updates both the joined and the respective API . + /// + /// The new to update from. + private void updateLocalRoomSettings(MultiplayerRoomSettings settings) + { + if (Room == null) + return; + + // Update a few instantaneously properties of the room. + Schedule(() => + { + if (Room == null) + return; + + Debug.Assert(apiRoom != null); + + Room.Settings = settings; + apiRoom.Name.Value = Room.Settings.Name; + + // The playlist update is delayed until an online beatmap lookup (below) succeeds. + // In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here. + apiRoom.Playlist.Clear(); + + RoomChanged?.Invoke(); + }); + + var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); + req.Success += res => + { + var beatmapSet = res.ToBeatmapSet(rulesets); + + var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); + beatmap.MD5Hash = settings.BeatmapChecksum; + + var ruleset = rulesets.GetRuleset(settings.RulesetID); + var mods = settings.Mods.Select(m => m.ToMod(ruleset.CreateInstance())); + + PlaylistItem playlistItem = new PlaylistItem + { + ID = playlistItemId, + Beatmap = { Value = beatmap }, + Ruleset = { Value = ruleset }, + }; + + playlistItem.RequiredMods.AddRange(mods); + + Schedule(() => + { + if (Room == null || !Room.Settings.Equals(settings)) + return; + + Debug.Assert(apiRoom != null); + + apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. + apiRoom.Playlist.Add(playlistItem); + }); + }; + + api.Queue(req); + } + } +} From 9ceb090f04d682f583fdc2d06fe1462f12a9c8ec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 00:17:50 +0900 Subject: [PATCH 5455/6909] Fix ambiguous reference --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 3e2f435524..60960f4929 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -117,7 +117,7 @@ namespace osu.Game.Online.RealtimeMultiplayer /// Change the current settings. /// /// - /// A room must have been joined via for this to have any effect. + /// A room must be joined for this to have any effect. /// /// The new room name, if any. /// The new room playlist item, if any. From cf2340cafb8b7ce964935a410f3bd3af49041458 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 00:18:41 +0900 Subject: [PATCH 5456/6909] Add a realtime room manager --- .../RealtimeRoomManager.cs | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs new file mode 100644 index 0000000000..a86e924c85 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Logging; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Components; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class RealtimeRoomManager : RoomManager + { + [Resolved] + private StatefulMultiplayerClient multiplayerClient { get; set; } + + public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) + => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError); + + public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) + => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError); + + private void joinMultiplayerRoom(Room room, Action onSuccess = null) + { + Debug.Assert(room.RoomID.Value != null); + + var joinTask = multiplayerClient.JoinRoom(room); + joinTask.ContinueWith(_ => onSuccess?.Invoke(room)); + joinTask.ContinueWith(t => + { + PartRoom(); + if (t.Exception != null) + Logger.Error(t.Exception, "Failed to join multiplayer room."); + }, TaskContinuationOptions.NotOnRanToCompletion); + } + + protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] + { + new ListingPollingComponent() + }; + } +} From a6520d3d446230cb3c0ef49aa15cdf45ba8b2232 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:01:09 +0900 Subject: [PATCH 5457/6909] Clear rooms and poll only when connected to multiplayer server --- .../Screens/Multi/Components/RoomManager.cs | 6 ++ .../RealtimeRoomManager.cs | 64 ++++++++++++++++++- 2 files changed, 69 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 46941dc58e..d92427680e 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -157,6 +157,12 @@ namespace osu.Game.Screens.Multi.Components RoomsUpdated?.Invoke(); } + protected void ClearRooms() + { + rooms.Clear(); + InitialRoomsReceived.Value = false; + } + /// /// Updates a local with a remote copy. /// diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index a86e924c85..62ea5d5512 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; @@ -17,6 +18,22 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer [Resolved] private StatefulMultiplayerClient multiplayerClient { get; set; } + public readonly Bindable TimeBetweenListingPolls = new Bindable(); + public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); + private readonly IBindable isConnected = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + isConnected.BindTo(multiplayerClient.IsConnected); + isConnected.BindValueChanged(connected => Schedule(() => + { + if (!connected.NewValue) + ClearRooms(); + })); + } + public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError); @@ -39,7 +56,52 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] { - new ListingPollingComponent() + new RealtimeListingPollingComponent + { + TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, + AllowPolling = { BindTarget = isConnected } + }, + new RealtimeSelectionPollingComponent + { + TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls }, + AllowPolling = { BindTarget = isConnected } + } }; + + private class RealtimeListingPollingComponent : ListingPollingComponent + { + public readonly IBindable AllowPolling = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + AllowPolling.BindValueChanged(_ => + { + if (IsLoaded) + PollImmediately(); + }); + } + + protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); + } + + private class RealtimeSelectionPollingComponent : SelectionPollingComponent + { + public readonly IBindable AllowPolling = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + AllowPolling.BindValueChanged(_ => + { + if (IsLoaded) + PollImmediately(); + }); + } + + protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); + } } } From 1e2163f55e0378604ebe2f083a84b1271a8e7f4d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:14:50 +0900 Subject: [PATCH 5458/6909] Add a testable realtime multiplayer client --- .../TestRealtimeMultiplayerClient.cs | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs new file mode 100644 index 0000000000..2a90f1e744 --- /dev/null +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.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. + +#nullable enable + +using System.Diagnostics; +using System.Linq; +using System.Threading.Tasks; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.API; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Users; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestRealtimeMultiplayerClient : StatefulMultiplayerClient + { + public override IBindable IsConnected { get; } = new Bindable(true); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user }); + + public void RemoveUser(User user) + { + Debug.Assert(Room != null); + ((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.User == user)); + + Schedule(() => + { + if (Room.Users.Any()) + TransferHost(Room.Users.First().UserID); + }); + } + + public void ChangeUserState(int userId, MultiplayerUserState newState) + { + Debug.Assert(Room != null); + + ((IMultiplayerClient)this).UserStateChanged(userId, newState); + + Schedule(() => + { + switch (newState) + { + case MultiplayerUserState.Loaded: + if (Room.Users.All(u => u.State != MultiplayerUserState.WaitingForLoad)) + { + foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.Loaded)) + ChangeUserState(u.UserID, MultiplayerUserState.Playing); + + ((IMultiplayerClient)this).MatchStarted(); + } + + break; + + case MultiplayerUserState.FinishedPlay: + if (Room.Users.All(u => u.State != MultiplayerUserState.Playing)) + { + foreach (var u in Room.Users.Where(u => u.State == MultiplayerUserState.FinishedPlay)) + ChangeUserState(u.UserID, MultiplayerUserState.Results); + + ((IMultiplayerClient)this).ResultsReady(); + } + + break; + } + }); + } + + protected override Task JoinRoom(long roomId) + { + var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value }; + + var room = new MultiplayerRoom(roomId); + room.Users.Add(user); + + if (room.Users.Count == 1) + room.Host = user; + + return Task.FromResult(room); + } + + public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); + + public override async Task ChangeSettings(MultiplayerRoomSettings settings) + { + Debug.Assert(Room != null); + + await ((IMultiplayerClient)this).SettingsChanged(settings); + + foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) + ChangeUserState(user.UserID, MultiplayerUserState.Idle); + } + + public override Task ChangeState(MultiplayerUserState newState) + { + ChangeUserState(api.LocalUser.Value.Id, newState); + return Task.CompletedTask; + } + + public override async Task StartMatch() + { + Debug.Assert(Room != null); + + foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) + ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); + + await ((IMultiplayerClient)this).LoadRequested(); + } + } +} From 50a35c0f63222f767678d4b661b93f19e04a2b67 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:16:00 +0900 Subject: [PATCH 5459/6909] Add connection/disconnection capability --- .../RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs index 2a90f1e744..bfa8362c7e 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs @@ -16,11 +16,16 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer { public class TestRealtimeMultiplayerClient : StatefulMultiplayerClient { - public override IBindable IsConnected { get; } = new Bindable(true); + public override IBindable IsConnected => isConnected; + private readonly Bindable isConnected = new Bindable(true); [Resolved] private IAPIProvider api { get; set; } = null!; + public void Connect() => isConnected.Value = true; + + public void Disconnect() => isConnected.Value = false; + public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user }); public void RemoveUser(User user) From c6555c53cc315b6df93cab9d7c56dc2c018ad2ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:17:24 +0900 Subject: [PATCH 5460/6909] Add a testable realtime room manager --- .../API/Requests/GetBeatmapSetRequest.cs | 10 +- .../Online/Multiplayer/CreateRoomRequest.cs | 6 +- .../TestRealtimeRoomManager.cs | 91 +++++++++++++++++++ 3 files changed, 99 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs diff --git a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs index 8e6deeb3c6..158ae03b8d 100644 --- a/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/GetBeatmapSetRequest.cs @@ -7,16 +7,16 @@ namespace osu.Game.Online.API.Requests { public class GetBeatmapSetRequest : APIRequest { - private readonly int id; - private readonly BeatmapSetLookupType type; + public readonly int ID; + public readonly BeatmapSetLookupType Type; public GetBeatmapSetRequest(int id, BeatmapSetLookupType type = BeatmapSetLookupType.SetId) { - this.id = id; - this.type = type; + ID = id; + Type = type; } - protected override string Target => type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{id}" : $@"beatmapsets/lookup?beatmap_id={id}"; + protected override string Target => Type == BeatmapSetLookupType.SetId ? $@"beatmapsets/{ID}" : $@"beatmapsets/lookup?beatmap_id={ID}"; } public enum BeatmapSetLookupType diff --git a/osu.Game/Online/Multiplayer/CreateRoomRequest.cs b/osu.Game/Online/Multiplayer/CreateRoomRequest.cs index dcb4ed51ea..5be99e9442 100644 --- a/osu.Game/Online/Multiplayer/CreateRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/CreateRoomRequest.cs @@ -10,11 +10,11 @@ namespace osu.Game.Online.Multiplayer { public class CreateRoomRequest : APIRequest { - private readonly Room room; + public readonly Room Room; public CreateRoomRequest(Room room) { - this.room = room; + Room = room; } protected override WebRequest CreateWebRequest() @@ -24,7 +24,7 @@ namespace osu.Game.Online.Multiplayer req.ContentType = "application/json"; req.Method = HttpMethod.Post; - req.AddRaw(JsonConvert.SerializeObject(room)); + req.AddRaw(JsonConvert.SerializeObject(Room)); return req; } diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs new file mode 100644 index 0000000000..773b72da88 --- /dev/null +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.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 osu.Framework.Allocation; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets.Scoring; +using osu.Game.Scoring; +using osu.Game.Screens.Multi.RealtimeMultiplayer; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestRealtimeRoomManager : RealtimeRoomManager + { + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private OsuGameBase game { get; set; } + + private readonly List rooms = new List(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + int currentScoreId = 0; + + ((DummyAPIAccess)api).HandleRequest = req => + { + switch (req) + { + case CreateRoomRequest createRoomRequest: + var createdRoom = new APICreatedRoom(); + + createdRoom.CopyFrom(createRoomRequest.Room); + createdRoom.RoomID.Value = 1; + + rooms.Add(createdRoom); + createRoomRequest.TriggerSuccess(createdRoom); + break; + + case JoinRoomRequest joinRoomRequest: + joinRoomRequest.TriggerSuccess(); + break; + + case PartRoomRequest partRoomRequest: + partRoomRequest.TriggerSuccess(); + break; + + case GetRoomsRequest getRoomsRequest: + getRoomsRequest.TriggerSuccess(rooms); + break; + + case GetBeatmapSetRequest getBeatmapSetRequest: + var onlineReq = new GetBeatmapSetRequest(getBeatmapSetRequest.ID, getBeatmapSetRequest.Type); + onlineReq.Success += res => getBeatmapSetRequest.TriggerSuccess(res); + onlineReq.Failure += e => getBeatmapSetRequest.TriggerFailure(e); + + // Get the online API from the game's dependencies. + game.Dependencies.Get().Queue(onlineReq); + break; + + case CreateRoomScoreRequest createRoomScoreRequest: + createRoomScoreRequest.TriggerSuccess(new APIScoreToken { ID = 1 }); + break; + + case SubmitRoomScoreRequest submitRoomScoreRequest: + submitRoomScoreRequest.TriggerSuccess(new MultiplayerScore + { + ID = currentScoreId++, + Accuracy = 1, + EndedAt = DateTimeOffset.Now, + Passed = true, + Rank = ScoreRank.S, + MaxCombo = 1000, + TotalScore = 1000000, + User = api.LocalUser.Value, + Statistics = new Dictionary() + }); + break; + } + }; + } + + public new void Schedule(Action action) => base.Schedule(action); + } +} From c6da680c803a0d9cf055fbedbd57e7992eed823e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:19:08 +0900 Subject: [PATCH 5461/6909] Add a container for testing purposes --- .../TestRealtimeRoomContainer.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs new file mode 100644 index 0000000000..aa75968cca --- /dev/null +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.Multi.RealtimeMultiplayer; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestRealtimeRoomContainer : Container + { + protected override Container Content => content; + private readonly Container content; + + [Cached(typeof(StatefulMultiplayerClient))] + public readonly TestRealtimeMultiplayerClient Client; + + [Cached(typeof(RealtimeRoomManager))] + public readonly TestRealtimeRoomManager RoomManager; + + [Cached] + public readonly Bindable Filter = new Bindable(new FilterCriteria()); + + public TestRealtimeRoomContainer() + { + RelativeSizeAxes = Axes.Both; + + AddRangeInternal(new Drawable[] + { + Client = new TestRealtimeMultiplayerClient(), + RoomManager = new TestRealtimeRoomManager(), + content = new Container { RelativeSizeAxes = Axes.Both } + }); + } + } +} From 2fc5561b7ec7a4a5c53235203cd65b1c8d6869b8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:22:52 +0900 Subject: [PATCH 5462/6909] Add handling for GetRoomRequest() --- osu.Game/Online/Multiplayer/GetRoomRequest.cs | 6 +++--- .../TestRealtimeRoomManager.cs | 19 ++++++++++++++++++- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Multiplayer/GetRoomRequest.cs b/osu.Game/Online/Multiplayer/GetRoomRequest.cs index 2907b49f1d..449c2c8e31 100644 --- a/osu.Game/Online/Multiplayer/GetRoomRequest.cs +++ b/osu.Game/Online/Multiplayer/GetRoomRequest.cs @@ -7,13 +7,13 @@ namespace osu.Game.Online.Multiplayer { public class GetRoomRequest : APIRequest { - private readonly int roomId; + public readonly int RoomId; public GetRoomRequest(int roomId) { - this.roomId = roomId; + RoomId = roomId; } - protected override string Target => $"rooms/{roomId}"; + protected override string Target => $"rooms/{RoomId}"; } } diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs index 773b72da88..cee1c706ae 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -52,7 +53,23 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer break; case GetRoomsRequest getRoomsRequest: - getRoomsRequest.TriggerSuccess(rooms); + var roomsWithoutParticipants = new List(); + + foreach (var r in rooms) + { + var newRoom = new Room(); + + newRoom.CopyFrom(r); + newRoom.RecentParticipants.Clear(); + + roomsWithoutParticipants.Add(newRoom); + } + + getRoomsRequest.TriggerSuccess(roomsWithoutParticipants); + break; + + case GetRoomRequest getRoomRequest: + getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId)); break; case GetBeatmapSetRequest getBeatmapSetRequest: From 9b0ca8fc3b2b380578202e5e75145d1b88ed9743 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:57:30 +0900 Subject: [PATCH 5463/6909] Make real time room manager not poll while inside a room --- .../Screens/Multi/Components/RoomManager.cs | 14 ++++++------ .../RealtimeRoomManager.cs | 22 +++++++++++++------ 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index d92427680e..6e27515849 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -27,6 +27,8 @@ namespace osu.Game.Screens.Multi.Components public IBindableList Rooms => rooms; + protected Room JoinedRoom { get; private set; } + [Resolved] private RulesetStore rulesets { get; set; } @@ -36,8 +38,6 @@ namespace osu.Game.Screens.Multi.Components [Resolved] private IAPIProvider api { get; set; } - private Room joinedRoom; - protected RoomManager() { RelativeSizeAxes = Axes.Both; @@ -64,7 +64,7 @@ namespace osu.Game.Screens.Multi.Components req.Success += result => { - joinedRoom = room; + JoinedRoom = room; update(room, result); addRoom(room); @@ -93,7 +93,7 @@ namespace osu.Game.Screens.Multi.Components currentJoinRoomRequest.Success += () => { - joinedRoom = room; + JoinedRoom = room; onSuccess?.Invoke(room); }; @@ -111,11 +111,11 @@ namespace osu.Game.Screens.Multi.Components { currentJoinRoomRequest?.Cancel(); - if (joinedRoom == null) + if (JoinedRoom == null) return; - api.Queue(new PartRoomRequest(joinedRoom)); - joinedRoom = null; + api.Queue(new PartRoomRequest(JoinedRoom)); + JoinedRoom = null; } private readonly HashSet ignoredRooms = new HashSet(); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 62ea5d5512..69addde2a6 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -21,17 +21,16 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public readonly Bindable TimeBetweenListingPolls = new Bindable(); public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); private readonly IBindable isConnected = new Bindable(); + private readonly Bindable allowPolling = new Bindable(); protected override void LoadComplete() { base.LoadComplete(); isConnected.BindTo(multiplayerClient.IsConnected); - isConnected.BindValueChanged(connected => Schedule(() => - { - if (!connected.NewValue) - ClearRooms(); - })); + isConnected.BindValueChanged(_ => Schedule(updatePolling), true); + + updatePolling(); } public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) @@ -54,17 +53,26 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer }, TaskContinuationOptions.NotOnRanToCompletion); } + private void updatePolling() + { + if (!isConnected.Value) + ClearRooms(); + + // Don't poll when not connected or when a room has been joined. + allowPolling.Value = isConnected.Value && JoinedRoom == null; + } + protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] { new RealtimeListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, - AllowPolling = { BindTarget = isConnected } + AllowPolling = { BindTarget = allowPolling } }, new RealtimeSelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls }, - AllowPolling = { BindTarget = isConnected } + AllowPolling = { BindTarget = allowPolling } } }; From 7d1fe7955e633e332e2d43fafc1dd24f313e4ee5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 01:57:40 +0900 Subject: [PATCH 5464/6909] Small improvements to testable room manager --- .../RealtimeMultiplayer/TestRealtimeRoomManager.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs index cee1c706ae..0d1314fb51 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs @@ -5,11 +5,13 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.Multiplayer; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; +using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.RealtimeMultiplayer; namespace osu.Game.Tests.Visual.RealtimeMultiplayer @@ -22,6 +24,9 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer [Resolved] private OsuGameBase game { get; set; } + [Cached] + public readonly Bindable Filter = new Bindable(new FilterCriteria()); + private readonly List rooms = new List(); protected override void LoadComplete() @@ -29,6 +34,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer base.LoadComplete(); int currentScoreId = 0; + int currentRoomId = 0; ((DummyAPIAccess)api).HandleRequest = req => { @@ -38,7 +44,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer var createdRoom = new APICreatedRoom(); createdRoom.CopyFrom(createRoomRequest.Room); - createdRoom.RoomID.Value = 1; + createdRoom.RoomID.Value ??= currentRoomId++; rooms.Add(createdRoom); createRoomRequest.TriggerSuccess(createdRoom); @@ -103,6 +109,8 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer }; } + public new void ClearRooms() => base.ClearRooms(); + public new void Schedule(Action action) => base.Schedule(action); } } From 0fb8615f95b29dbff7ba1c839c3591a1b72aa5b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:02:04 +0900 Subject: [PATCH 5465/6909] Implement room parting --- .../Screens/Multi/Components/RoomManager.cs | 4 +++- .../RealtimeRoomManager.cs | 20 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 6e27515849..21bff70b8b 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -107,7 +107,7 @@ namespace osu.Game.Screens.Multi.Components api.Queue(currentJoinRoomRequest); } - public void PartRoom() + public virtual void PartRoom() { currentJoinRoomRequest?.Cancel(); @@ -157,6 +157,8 @@ namespace osu.Game.Screens.Multi.Components RoomsUpdated?.Invoke(); } + protected void RemoveRoom(Room room) => rooms.Remove(room); + protected void ClearRooms() { rooms.Clear(); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 69addde2a6..4f73ee3865 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -23,6 +23,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private readonly IBindable isConnected = new Bindable(); private readonly Bindable allowPolling = new Bindable(); + private ListingPollingComponent listingPollingComponent; + protected override void LoadComplete() { base.LoadComplete(); @@ -39,6 +41,22 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError); + public override void PartRoom() + { + if (JoinedRoom == null) + return; + + var joinedRoom = JoinedRoom; + + base.PartRoom(); + multiplayerClient.LeaveRoom().Wait(); + + // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. + RemoveRoom(joinedRoom); + // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. + Schedule(() => listingPollingComponent.PollImmediately()); + } + private void joinMultiplayerRoom(Room room, Action onSuccess = null) { Debug.Assert(room.RoomID.Value != null); @@ -64,7 +82,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] { - new RealtimeListingPollingComponent + listingPollingComponent = new RealtimeListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, AllowPolling = { BindTarget = allowPolling } From a593f588db95a8ac6b8bb2594c1a94909ade9c5f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:02:47 +0900 Subject: [PATCH 5466/6909] Add a test for the realtime room manager --- .../TestSceneRealtimeRoomManager.cs | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs new file mode 100644 index 0000000000..6bd8c410a4 --- /dev/null +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.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 NUnit.Framework; +using osu.Framework.Graphics; +using osu.Game.Online.Multiplayer; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestSceneRealtimeRoomManager : MultiplayerTestScene + { + private TestRealtimeRoomContainer roomContainer; + private TestRealtimeRoomManager roomManager => roomContainer.RoomManager; + + [Test] + public void TestPollsInitially() + { + AddStep("create room manager with a few rooms", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room { Name = { Value = "1" } }); + roomManager.PartRoom(); + roomManager.CreateRoom(new Room { Name = { Value = "2" } }); + roomManager.PartRoom(); + roomManager.ClearRooms(); + }); + }); + + AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); + } + + [Test] + public void TestRoomsClearedOnDisconnection() + { + AddStep("create room manager with a few rooms", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + }); + }); + + AddStep("disconnect", () => roomContainer.Client.Disconnect()); + + AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0); + AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); + } + + [Test] + public void TestRoomsPolledOnReconnect() + { + AddStep("create room manager with a few rooms", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + }); + }); + + AddStep("disconnect", () => roomContainer.Client.Disconnect()); + AddStep("connect", () => roomContainer.Client.Connect()); + + AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); + } + + [Test] + public void TestRoomsNotPolledWhenJoined() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.ClearRooms(); + }); + }); + + AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0); + AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); + } + + private TestRealtimeRoomManager createRoomManager() + { + Child = roomContainer = new TestRealtimeRoomContainer + { + RoomManager = + { + TimeBetweenListingPolls = { Value = 1 }, + TimeBetweenSelectionPolls = { Value = 1 } + } + }; + + return roomManager; + } + } +} From e84ce80d6cb0187b67527d5289ee3480e0746484 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:17:07 +0900 Subject: [PATCH 5467/6909] Make test headless --- .../Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs index 6bd8c410a4..9a4b748de1 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs @@ -3,10 +3,12 @@ using NUnit.Framework; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Online.Multiplayer; namespace osu.Game.Tests.Visual.RealtimeMultiplayer { + [HeadlessTest] public class TestSceneRealtimeRoomManager : MultiplayerTestScene { private TestRealtimeRoomContainer roomContainer; From 109e6b4283e0a49ac13be9a59572ab1f6f599e0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:20:02 +0900 Subject: [PATCH 5468/6909] Add tests for creating/joining/parting multiplayer rooms --- .../TestSceneRealtimeRoomManager.cs | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs index 9a4b748de1..598641682b 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs @@ -90,6 +90,52 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); } + [Test] + public void TestMultiplayerRoomJoinedWhenCreated() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + }); + }); + + AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null); + } + + [Test] + public void TestMultiplayerRoomPartedWhenAPIRoomParted() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + roomManager.CreateRoom(new Room()); + roomManager.PartRoom(); + }); + }); + + AddAssert("multiplayer room parted", () => roomContainer.Client.Room == null); + } + + [Test] + public void TestMultiplayerRoomPartedWhenAPIRoomJoined() + { + AddStep("create room manager with a room", () => + { + createRoomManager().With(d => d.OnLoadComplete += _ => + { + var r = new Room(); + roomManager.CreateRoom(r); + roomManager.PartRoom(); + roomManager.JoinRoom(r); + }); + }); + + AddAssert("multiplayer room parted", () => roomContainer.Client.Room != null); + } + private TestRealtimeRoomManager createRoomManager() { Child = roomContainer = new TestRealtimeRoomContainer From 3f4a66c4ae6036b98cb3512ba2440d75596c056e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:23:42 +0900 Subject: [PATCH 5469/6909] Add realtime multiplayer test scene abstract class --- .../RealtimeMultiplayerTestScene.cs | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs new file mode 100644 index 0000000000..e41076a4fd --- /dev/null +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs @@ -0,0 +1,53 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.Multi.RealtimeMultiplayer; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class RealtimeMultiplayerTestScene : MultiplayerTestScene + { + [Cached(typeof(StatefulMultiplayerClient))] + public TestRealtimeMultiplayerClient Client { get; } + + [Cached(typeof(RealtimeRoomManager))] + public TestRealtimeRoomManager RoomManager { get; } + + [Cached] + public Bindable Filter { get; } + + protected override Container Content => content; + private readonly TestRealtimeRoomContainer content; + + private readonly bool joinRoom; + + public RealtimeMultiplayerTestScene(bool joinRoom = true) + { + this.joinRoom = joinRoom; + base.Content.Add(content = new TestRealtimeRoomContainer { RelativeSizeAxes = Axes.Both }); + + Client = content.Client; + RoomManager = content.RoomManager; + Filter = content.Filter; + } + + [SetUp] + public new void Setup() => Schedule(() => + { + RoomManager.Schedule(() => RoomManager.PartRoom()); + + if (joinRoom) + { + Room.RoomID.Value = 1; + RoomManager.Schedule(() => RoomManager.JoinRoom(Room, null, null)); + } + }); + } +} From 9b08f573baeb360e0f5135f98f5205b7145757e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:41:04 +0900 Subject: [PATCH 5470/6909] Fix room not created before being joined --- .../RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs index e41076a4fd..b52106551e 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs @@ -44,10 +44,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer RoomManager.Schedule(() => RoomManager.PartRoom()); if (joinRoom) - { - Room.RoomID.Value = 1; - RoomManager.Schedule(() => RoomManager.JoinRoom(Room, null, null)); - } + RoomManager.Schedule(() => RoomManager.CreateRoom(Room)); }); } } From 4d051818a152573834a0e859994f25c83bda1b7a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:30:53 +0900 Subject: [PATCH 5471/6909] Add base class for all realtime multiplayer classes --- .../RealtimeRoomComposite.cs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs new file mode 100644 index 0000000000..e6d1274316 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.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 JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Game.Online.RealtimeMultiplayer; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public abstract class RealtimeRoomComposite : MultiplayerComposite + { + [CanBeNull] + protected MultiplayerRoom Room => Client.Room; + + [Resolved] + protected StatefulMultiplayerClient Client { get; private set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Client.RoomChanged += OnRoomChanged; + OnRoomChanged(); + } + + protected virtual void OnRoomChanged() + { + } + + protected override void Dispose(bool isDisposing) + { + if (Client != null) + Client.RoomChanged -= OnRoomChanged; + + base.Dispose(isDisposing); + } + } +} From 1e5c32410ad572883295e0f8e47e90704ef4593e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:37:47 +0900 Subject: [PATCH 5472/6909] Add the realtime multiplayer participants list --- .../TestSceneParticipantsList.cs | 96 +++++++++ .../Participants/ParticipantPanel.cs | 187 ++++++++++++++++++ .../Participants/ParticipantsList.cs | 55 ++++++ .../Participants/ReadyMark.cs | 51 +++++ 4 files changed, 389 insertions(+) create mode 100644 osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs new file mode 100644 index 0000000000..ee6bbc4ecd --- /dev/null +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs @@ -0,0 +1,96 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Testing; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; +using osu.Game.Users; +using osuTK; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestSceneParticipantsList : RealtimeMultiplayerTestScene + { + [SetUp] + public new void Setup() => Schedule(() => + { + Child = new ParticipantsList + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.Y, + Size = new Vector2(380, 0.7f) + }; + }); + + [Test] + public void TestAddUser() + { + AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); + + AddStep("add user", () => Client.AddUser(new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddAssert("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); + } + + [Test] + public void TestRemoveUser() + { + User secondUser = null; + + AddStep("add a user", () => + { + Client.AddUser(secondUser = new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + }); + + AddStep("remove host", () => Client.RemoveUser(API.LocalUser.Value)); + + AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.User == secondUser); + } + + [Test] + public void TestToggleReadyState() + { + AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); + + AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready)); + AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); + + AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle)); + AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); + } + + [Test] + public void TestCrownChangesStateWhenHostTransferred() + { + AddStep("add user", () => Client.AddUser(new User + { + Id = 3, + Username = "Second", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + })); + + AddUntilStep("first user crown visible", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 1); + AddUntilStep("second user crown hidden", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 0); + + AddStep("make second user host", () => Client.TransferHost(3)); + + AddUntilStep("first user crown hidden", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 0); + AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs new file mode 100644 index 0000000000..306a54bfdc --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs @@ -0,0 +1,187 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Users; +using osu.Game.Users.Drawables; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class ParticipantPanel : RealtimeRoomComposite, IHasContextMenu + { + public readonly MultiplayerRoomUser User; + + [Resolved] + private IAPIProvider api { get; set; } + + private ReadyMark readyMark; + private SpriteIcon crown; + + public ParticipantPanel(MultiplayerRoomUser user) + { + User = user; + + RelativeSizeAxes = Axes.X; + Height = 40; + } + + [BackgroundDependencyLoader] + private void load() + { + Debug.Assert(User.User != null); + + var backgroundColour = Color4Extensions.FromHex("#33413C"); + + InternalChildren = new Drawable[] + { + crown = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.Crown, + Size = new Vector2(14), + Colour = Color4Extensions.FromHex("#F7E65D"), + Alpha = 0 + }, + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 24 }, + Child = new Container + { + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 5, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = backgroundColour + }, + new UserCoverBackground + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + RelativeSizeAxes = Axes.Both, + Width = 0.75f, + User = User.User, + Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f)) + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Spacing = new Vector2(10), + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new UpdateableAvatar + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + FillMode = FillMode.Fit, + User = User.User + }, + new UpdateableFlag + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(30, 20), + Country = User.User.Country + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), + Text = User.User.Username + }, + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(size: 14), + Text = User.User.CurrentModeRank != null ? $"#{User.User.CurrentModeRank}" : string.Empty + } + } + }, + readyMark = new ReadyMark + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10 }, + Alpha = 0 + } + } + } + } + }; + } + + protected override void OnRoomChanged() + { + base.OnRoomChanged(); + + if (Room == null) + return; + + if (User.State == MultiplayerUserState.Ready) + readyMark.FadeIn(50); + else + readyMark.FadeOut(50); + + if (Room.Host?.Equals(User) == true) + crown.FadeIn(50); + else + crown.FadeOut(50); + } + + public MenuItem[] ContextMenuItems + { + get + { + if (Room == null) + return null; + + // If the local user is targetted. + if (User.UserID == api.LocalUser.Value.Id) + return null; + + // If the local user is not the host of the room. + if (Room.Host?.UserID != api.LocalUser.Value.Id) + return null; + + int targetUser = User.UserID; + + return new MenuItem[] + { + new OsuMenuItem("Give host", MenuItemType.Standard, () => + { + // Ensure the local user is still host. + if (Room.Host?.UserID != api.LocalUser.Value.Id) + return; + + Client.TransferHost(targetUser); + }) + }; + } + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs new file mode 100644 index 0000000000..d4c32d9189 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.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.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Cursor; +using osuTK; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class ParticipantsList : RealtimeRoomComposite + { + private FillFlowContainer panels; + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new OsuScrollContainer + { + RelativeSizeAxes = Axes.Both, + Child = panels = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 2) + } + } + }; + } + + protected override void OnRoomChanged() + { + base.OnRoomChanged(); + + if (Room == null) + panels.Clear(); + else + { + // Remove panels for users no longer in the room. + panels.RemoveAll(p => !Room.Users.Contains(p.User)); + + // Add panels for all users new to the room. + foreach (var user in Room.Users.Except(panels.Select(p => p.User))) + panels.Add(new ParticipantPanel(user)); + } + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs new file mode 100644 index 0000000000..df49d9342e --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.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.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class ReadyMark : CompositeDrawable + { + public ReadyMark() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12), + Text = "ready", + Colour = Color4Extensions.FromHex("#DDFFFF") + }, + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.CheckCircle, + Size = new Vector2(12), + Colour = Color4Extensions.FromHex("#AADD00") + } + } + }; + } + } +} From 11a903a206a820191af25de9e4f953223869c44a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:46:16 +0900 Subject: [PATCH 5473/6909] Add test for many users and disable scrollbar --- .../TestSceneParticipantsList.cs | 20 +++++++++++++++++++ .../Participants/ParticipantsList.cs | 1 + 2 files changed, 21 insertions(+) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs index ee6bbc4ecd..8c997e9e32 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs @@ -92,5 +92,25 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer AddUntilStep("first user crown hidden", () => this.ChildrenOfType().ElementAt(0).ChildrenOfType().First().Alpha == 0); AddUntilStep("second user crown visible", () => this.ChildrenOfType().ElementAt(1).ChildrenOfType().First().Alpha == 1); } + + [Test] + public void TestManyUsers() + { + AddStep("add many users", () => + { + for (int i = 0; i < 20; i++) + { + Client.AddUser(new User + { + Id = i, + Username = $"User {i}", + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + if (i % 2 == 0) + Client.ChangeUserState(i, MultiplayerUserState.Ready); + } + }); + } } } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs index d4c32d9189..218c2cabb7 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs @@ -24,6 +24,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants Child = new OsuScrollContainer { RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, Child = panels = new FillFlowContainer { RelativeSizeAxes = Axes.X, From e4a54dc6cdccdf17b1e950fe1abf0424e9c28485 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:52:51 +0900 Subject: [PATCH 5474/6909] Renamespace ready button --- osu.Game/Screens/Multi/{Match => }/Components/ReadyButton.cs | 2 +- osu.Game/Screens/Multi/Match/Components/Footer.cs | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) rename osu.Game/Screens/Multi/{Match => }/Components/ReadyButton.cs (98%) diff --git a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Components/ReadyButton.cs similarity index 98% rename from osu.Game/Screens/Multi/Match/Components/ReadyButton.cs rename to osu.Game/Screens/Multi/Components/ReadyButton.cs index a64f24dd7e..6d12111d8f 100644 --- a/osu.Game/Screens/Multi/Match/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Components/ReadyButton.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.Multi.Components { public class ReadyButton : TriangleButton { diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/Multi/Match/Components/Footer.cs index be4ee873fa..4ec8628d2b 100644 --- a/osu.Game/Screens/Multi/Match/Components/Footer.cs +++ b/osu.Game/Screens/Multi/Match/Components/Footer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Components; using osuTK; namespace osu.Game.Screens.Multi.Match.Components From 4e0113afbf51e5d9be43e73b1fee714a21c04ee1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:55:48 +0900 Subject: [PATCH 5475/6909] Abstractify ready button and add a timeshift implementation --- .../Screens/Multi/Components/ReadyButton.cs | 24 +++--------- .../Screens/Multi/Match/Components/Footer.cs | 4 +- .../Multi/Timeshift/TimeshiftReadyButton.cs | 38 +++++++++++++++++++ 3 files changed, 46 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs diff --git a/osu.Game/Screens/Multi/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Components/ReadyButton.cs index 6d12111d8f..0bb4ed8617 100644 --- a/osu.Game/Screens/Multi/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Components/ReadyButton.cs @@ -13,26 +13,20 @@ using osu.Game.Online.Multiplayer; namespace osu.Game.Screens.Multi.Components { - public class ReadyButton : TriangleButton + public abstract class ReadyButton : TriangleButton { public readonly Bindable SelectedItem = new Bindable(); - [Resolved(typeof(Room), nameof(Room.EndDate))] - private Bindable endDate { get; set; } + public new readonly BindableBool Enabled = new BindableBool(); [Resolved] - private IBindable gameBeatmap { get; set; } + protected IBindable GameBeatmap { get; private set; } [Resolved] private BeatmapManager beatmaps { get; set; } private bool hasBeatmap; - public ReadyButton() - { - Text = "Start"; - } - private IBindable> managerUpdated; private IBindable> managerRemoved; @@ -45,10 +39,6 @@ namespace osu.Game.Screens.Multi.Components managerRemoved.BindValueChanged(beatmapRemoved); SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true); - - BackgroundColour = colours.Green; - Triangles.ColourDark = colours.Green; - Triangles.ColourLight = colours.GreenLight; } private void updateSelectedItem(PlaylistItem item) @@ -94,15 +84,13 @@ namespace osu.Game.Screens.Multi.Components private void updateEnabledState() { - if (gameBeatmap.Value == null || SelectedItem.Value == null) + if (GameBeatmap.Value == null || SelectedItem.Value == null) { - Enabled.Value = false; + base.Enabled.Value = false; return; } - bool hasEnoughTime = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value; - - Enabled.Value = hasBeatmap && hasEnoughTime; + base.Enabled.Value = hasBeatmap && Enabled.Value; } } } diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/Multi/Match/Components/Footer.cs index 4ec8628d2b..d6a7e380bf 100644 --- a/osu.Game/Screens/Multi/Match/Components/Footer.cs +++ b/osu.Game/Screens/Multi/Match/Components/Footer.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.Multi.Timeshift; using osuTK; namespace osu.Game.Screens.Multi.Match.Components @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Match.Components InternalChildren = new[] { background = new Box { RelativeSizeAxes = Axes.Both }, - new ReadyButton + new TimeshiftReadyButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs new file mode 100644 index 0000000000..b6698b195c --- /dev/null +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs @@ -0,0 +1,38 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Game.Graphics; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Components; + +namespace osu.Game.Screens.Multi.Timeshift +{ + public class TimeshiftReadyButton : ReadyButton + { + [Resolved(typeof(Room), nameof(Room.EndDate))] + private Bindable endDate { get; set; } + + public TimeshiftReadyButton() + { + Text = "Start"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Green; + Triangles.ColourDark = colours.Green; + Triangles.ColourLight = colours.GreenLight; + } + + protected override void Update() + { + base.Update(); + + Enabled.Value = endDate.Value == null || DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; + } + } +} From 6efe24695b2cf930f554fd72cd9c5c51214abf6f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 02:59:11 +0900 Subject: [PATCH 5476/6909] Add the realtime multiplayer ready button --- .../TestSceneRealtimeReadyButton.cs | 129 ++++++++++++++++++ .../RealtimeReadyButton.cs | 122 +++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs new file mode 100644 index 0000000000..889c0c0be3 --- /dev/null +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs @@ -0,0 +1,129 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +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.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Rulesets; +using osu.Game.Screens.Multi.RealtimeMultiplayer; +using osu.Game.Tests.Resources; +using osu.Game.Users; +using osuTK; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestSceneRealtimeReadyButton : RealtimeMultiplayerTestScene + { + private RealtimeReadyButton button; + + private BeatmapManager beatmaps; + private RulesetStore rulesets; + + [BackgroundDependencyLoader] + private void load(GameHost host, AudioManager audio) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); + beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait(); + } + + [SetUp] + public new void Setup() => Schedule(() => + { + var beatmap = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First().Beatmaps.First(); + + Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); + + Child = button = new RealtimeReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(200, 50), + SelectedItem = + { + Value = new PlaylistItem + { + Beatmap = { Value = beatmap }, + Ruleset = { Value = beatmap.Ruleset } + } + } + }; + + Client.AddUser(API.LocalUser.Value); + }); + + [Test] + public void TestToggleStateWhenNotHost() + { + AddStep("add second user as host", () => + { + Client.AddUser(new User { Id = 2, Username = "Another user" }); + Client.TransferHost(2); + }); + + addClickButtonStep(); + AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + + addClickButtonStep(); + AddAssert("user is idle", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + } + + [TestCase(true)] + [TestCase(false)] + public void TestToggleStateWhenHost(bool allReady) + { + AddStep("setup", () => + { + Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + + if (!allReady) + Client.AddUser(new User { Id = 2, Username = "Another user" }); + }); + + addClickButtonStep(); + AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); + + addClickButtonStep(); + AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + } + + [Test] + public void TestBecomeHostWhileReady() + { + addClickButtonStep(); + AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); + + addClickButtonStep(); + AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + } + + [Test] + public void TestLoseHostWhileReady() + { + AddStep("setup", () => + { + Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + Client.AddUser(new User { Id = 2, Username = "Another user" }); + }); + + addClickButtonStep(); + AddStep("transfer host", () => Client.TransferHost(Client.Room?.Users[1].UserID ?? 0)); + + addClickButtonStep(); + AddAssert("match not started", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); + } + + private void addClickButtonStep() => AddStep("click button", () => + { + InputManager.MoveMouseTo(button); + InputManager.Click(MouseButton.Left); + }); + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs new file mode 100644 index 0000000000..d52df258ad --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs @@ -0,0 +1,122 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Diagnostics; +using System.Linq; +using JetBrains.Annotations; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Game.Graphics; +using osu.Game.Graphics.Backgrounds; +using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Components; +using osuTK; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class RealtimeReadyButton : RealtimeRoomComposite + { + public Bindable SelectedItem => button.SelectedItem; + + [Resolved] + private IAPIProvider api { get; set; } + + [CanBeNull] + private MultiplayerRoomUser localUser; + + [Resolved] + private OsuColour colours { get; set; } + + private readonly ButtonWithTrianglesExposed button; + + public RealtimeReadyButton() + { + InternalChild = button = new ButtonWithTrianglesExposed + { + RelativeSizeAxes = Axes.Both, + Size = Vector2.One, + Enabled = { Value = true }, + Action = onClick + }; + } + + protected override void OnRoomChanged() + { + base.OnRoomChanged(); + + localUser = Room?.Users.Single(u => u.User?.Id == api.LocalUser.Value.Id); + button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open; + updateState(); + } + + private void updateState() + { + if (localUser == null) + return; + + Debug.Assert(Room != null); + + switch (localUser.State) + { + case MultiplayerUserState.Idle: + button.Text = "Ready"; + updateButtonColour(true); + break; + + case MultiplayerUserState.Ready: + if (Room?.Host?.Equals(localUser) == true) + { + button.Text = "Let's go!"; + updateButtonColour(Room.Users.All(u => u.State == MultiplayerUserState.Ready)); + } + else + { + button.Text = "Waiting for host..."; + updateButtonColour(false); + } + + break; + } + } + + private void updateButtonColour(bool green) + { + if (green) + { + button.BackgroundColour = colours.Green; + button.Triangles.ColourDark = colours.Green; + button.Triangles.ColourLight = colours.GreenLight; + } + else + { + button.BackgroundColour = colours.YellowDark; + button.Triangles.ColourDark = colours.YellowDark; + button.Triangles.ColourLight = colours.Yellow; + } + } + + private void onClick() + { + if (localUser == null) + return; + + if (localUser.State == MultiplayerUserState.Idle) + Client.ChangeState(MultiplayerUserState.Ready); + else + { + if (Room?.Host?.Equals(localUser) == true) + Client.StartMatch(); + else + Client.ChangeState(MultiplayerUserState.Idle); + } + } + + private class ButtonWithTrianglesExposed : ReadyButton + { + public new Triangles Triangles => base.Triangles; + } + } +} From cc22efaa6babc4592a06a34f6affb226caea56cb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 03:17:04 +0900 Subject: [PATCH 5477/6909] Use tcs instead of delay-wait --- osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index e106dc3a1c..2b7ca189ee 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -107,25 +107,23 @@ namespace osu.Game.Screens.Multi.Play { Debug.Assert(token != null); - bool completed = false; + var tcs = new TaskCompletionSource(); var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo); request.Success += s => { score.ScoreInfo.OnlineScoreID = s.ID; - completed = true; + tcs.SetResult(true); }; request.Failure += e => { Logger.Error(e, "Failed to submit score"); - completed = true; + tcs.SetResult(false); }; api.Queue(request); - - while (!completed) - await Task.Delay(100); + await tcs.Task; return await base.SubmitScore(score); } From 772dd0287e4932e232d4f6c033b7246f740d3541 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sat, 19 Dec 2020 03:32:05 +0900 Subject: [PATCH 5478/6909] Split submission and import into two methods --- .../Screens/Multi/Play/TimeshiftPlayer.cs | 6 ++-- osu.Game/Screens/Play/Player.cs | 33 ++++++++++++++----- osu.Game/Screens/Play/ReplayPlayer.cs | 3 +- 3 files changed, 30 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 2b7ca189ee..41dcf61740 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -103,8 +103,10 @@ namespace osu.Game.Screens.Multi.Play return score; } - protected override async Task SubmitScore(Score score) + protected override async Task SubmitScore(Score score) { + await base.SubmitScore(score); + Debug.Assert(token != null); var tcs = new TaskCompletionSource(); @@ -124,8 +126,6 @@ namespace osu.Game.Screens.Multi.Play api.Queue(request); await tcs.Task; - - return await base.SubmitScore(score); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c8f1980ab1..a83f0e1b33 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -537,13 +537,23 @@ namespace osu.Game.Screens.Play try { - return await SubmitScore(score); + await SubmitScore(score); } catch (Exception ex) { Logger.Error(ex, "Score submission failed!"); - return score.ScoreInfo; } + + try + { + await ImportScore(score); + } + catch (Exception ex) + { + Logger.Error(ex, "Score import failed!"); + } + + return score.ScoreInfo; }); using (BeginDelayedSequence(RESULTS_DISPLAY_DELAY)) @@ -792,15 +802,15 @@ namespace osu.Game.Screens.Play } /// - /// Submits the player's . + /// Imports the player's to the local database. /// - /// The to submit. - /// The submitted score. - protected virtual async Task SubmitScore(Score score) + /// The to import. + /// The imported score. + protected virtual async Task ImportScore(Score score) { // Replays are already populated and present in the game's database, so should not be re-imported. if (DrawableRuleset.ReplayScore != null) - return score.ScoreInfo; + return; LegacyByteArrayReader replayReader; @@ -810,9 +820,16 @@ namespace osu.Game.Screens.Play replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } - return await scoreManager.Import(score.ScoreInfo, replayReader); + await scoreManager.Import(score.ScoreInfo, replayReader); } + /// + /// Submits the player's . + /// + /// The to submit. + /// The submitted score. + protected virtual Task SubmitScore(Score score) => Task.CompletedTask; + /// /// Creates the for a . /// diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index 390d1d1959..a07213cb33 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -37,7 +37,8 @@ namespace osu.Game.Screens.Play return Score; } - protected override Task SubmitScore(Score score) => Task.FromResult(score.ScoreInfo); + // Don't re-import replay scores as they're already present in the database. + protected override Task ImportScore(Score score) => Task.CompletedTask; protected override ResultsScreen CreateResults(ScoreInfo score) => new SoloResultsScreen(score, false); From f1878eff639bce6d336008cc85e4a4b2c8260bf9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 18 Dec 2020 23:45:42 +0100 Subject: [PATCH 5479/6909] Use yet another solution leveraging padding --- .../Edit/Compose/Components/SelectionBox.cs | 26 ++++++------------- 1 file changed, 8 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs index 85e86499be..2f4721f63e 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionBox.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionBox.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.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -93,6 +92,7 @@ namespace osu.Game.Screens.Edit.Compose.Components } } + private Container dragHandles; private FillFlowContainer buttons; public const float BORDER_RADIUS = 3; @@ -152,6 +152,12 @@ namespace osu.Game.Screens.Edit.Compose.Components }, } }, + dragHandles = new Container + { + RelativeSizeAxes = Axes.Both, + // ensures that the centres of all drag handles line up with the middle of the selection box border. + Padding = new MarginPadding(BORDER_RADIUS / 2) + }, buttons = new FillFlowContainer { Y = 20, @@ -233,30 +239,14 @@ namespace osu.Game.Screens.Edit.Compose.Components }); } - private void addDragHandle(Anchor anchor) => AddInternal(new SelectionBoxDragHandle + private void addDragHandle(Anchor anchor) => dragHandles.Add(new SelectionBoxDragHandle { Anchor = anchor, - X = dragCircleAdjustments[anchor].X, - Y = dragCircleAdjustments[anchor].Y, HandleDrag = e => OnScale?.Invoke(e.Delta, anchor), OperationStarted = operationStarted, OperationEnded = operationEnded }); - /// - /// Adjust Drag circle to be centered on the center of the border instead of on the edge. - /// - private Dictionary dragCircleAdjustments = new Dictionary(){ - {Anchor.TopLeft, new Vector2(BORDER_RADIUS / 2)}, - {Anchor.CentreLeft, new Vector2(BORDER_RADIUS / 2, 0)}, - {Anchor.BottomLeft, new Vector2(BORDER_RADIUS / 2, -BORDER_RADIUS / 2)}, - {Anchor.TopCentre, new Vector2(0, BORDER_RADIUS / 2)}, - {Anchor.BottomCentre, new Vector2(0, -BORDER_RADIUS / 2)}, - {Anchor.TopRight, new Vector2(-BORDER_RADIUS / 2, BORDER_RADIUS / 2)}, - {Anchor.CentreRight, new Vector2(-BORDER_RADIUS / 2, 0)}, - {Anchor.BottomRight, new Vector2(-BORDER_RADIUS / 2)} - }; - private int activeOperations; private float convertDragEventToAngleOfRotation(DragEvent e) From beaced321153867f0ae9c1d39af5a82848e2ced0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 19 Dec 2020 13:58:56 +0900 Subject: [PATCH 5480/6909] Remove unnecessary async state machine --- 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 a83f0e1b33..c539dff5d9 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -806,11 +806,11 @@ namespace osu.Game.Screens.Play /// /// The to import. /// The imported score. - protected virtual async Task ImportScore(Score score) + protected virtual Task ImportScore(Score score) { // Replays are already populated and present in the game's database, so should not be re-imported. if (DrawableRuleset.ReplayScore != null) - return; + return Task.CompletedTask; LegacyByteArrayReader replayReader; @@ -820,7 +820,7 @@ namespace osu.Game.Screens.Play replayReader = new LegacyByteArrayReader(stream.ToArray(), "replay.osr"); } - await scoreManager.Import(score.ScoreInfo, replayReader); + return scoreManager.Import(score.ScoreInfo, replayReader); } /// From 926281831b85353c1ddf878167abfbbad965a56f Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 19 Dec 2020 10:36:27 +0100 Subject: [PATCH 5481/6909] Fix missing XMLDoc bit. --- osu.Game/Database/ICanAcceptFiles.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/ICanAcceptFiles.cs b/osu.Game/Database/ICanAcceptFiles.cs index 5ec187975a..74fd6fcc36 100644 --- a/osu.Game/Database/ICanAcceptFiles.cs +++ b/osu.Game/Database/ICanAcceptFiles.cs @@ -20,6 +20,7 @@ namespace osu.Game.Database /// /// Import the specified files from the given import tasks. /// + /// The import tasks from which the files should be imported. Task Import(params ImportTask[] tasks); /// From 4fba0c8e6a3279216294a5a75fa2f79687d0a0c1 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 19 Dec 2020 10:55:39 +0100 Subject: [PATCH 5482/6909] Remove not used using statement. --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 4ebf7fbe63..e382ff5d48 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -51,7 +51,6 @@ using osu.Game.Screens.Select; using osu.Game.Updater; using osu.Game.Utils; using LogLevel = osu.Framework.Logging.LogLevel; -using osu.Game.Users; using osu.Game.Database; namespace osu.Game From 28ca21b432627ffb9d8058475bf969143dd71163 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 14:50:09 +0100 Subject: [PATCH 5483/6909] Seal banned method & throw better exception --- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index aa668cc4c8..d2d5594edd 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -40,9 +40,9 @@ namespace osu.Game.Screens.Play.HUD return drawable; } - public override void Add(GameplayLeaderboardScore drawable) + public sealed override void Add(GameplayLeaderboardScore drawable) { - throw new InvalidOperationException($"Use {nameof(AddPlayer)} instead."); + throw new NotSupportedException($"Use {nameof(AddPlayer)} instead."); } private void sort() From 22a2c3efdf2f15944e7714da36ea11962345055d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 14:55:58 +0100 Subject: [PATCH 5484/6909] Add back xmldoc of AddPlayer --- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index d2d5594edd..cab1cbd3f1 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -24,6 +24,14 @@ namespace osu.Game.Screens.Play.HUD LayoutEasing = Easing.OutQuint; } + /// + /// Adds a player to the leaderboard. + /// + /// The player. + /// + /// Whether the player should be tracked on the leaderboard. + /// Set to true for the local player or a player whose replay is currently being played. + /// public ILeaderboardScore AddPlayer(User user, bool isTracked) { var drawable = new GameplayLeaderboardScore(user, isTracked) From d392e0f27e2444dfa8a21758a835ddbaef1842e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 15:02:56 +0100 Subject: [PATCH 5485/6909] Extract shared rank-formatting helper --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 4 ++-- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 3 +-- osu.Game/Utils/FormatUtils.cs | 8 ++++++++ 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index dcd0cb435a..d8207aa8f4 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -24,8 +24,8 @@ using osu.Game.Scoring; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; -using Humanizer; using osu.Game.Online.API; +using osu.Game.Utils; namespace osu.Game.Online.Leaderboards { @@ -358,7 +358,7 @@ namespace osu.Game.Online.Leaderboards Anchor = Anchor.Centre, Origin = Anchor.Centre, Font = OsuFont.GetFont(size: 20, italics: true), - Text = rank == null ? "-" : rank.Value.ToMetric(decimals: rank < 100000 ? 1 : 0), + Text = rank == null ? "-" : rank.Value.FormatRank() }; } diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index af3cb640bb..9684ae016c 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.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 Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -49,7 +48,7 @@ namespace osu.Game.Screens.Play.HUD scorePosition = value; if (scorePosition.HasValue) - positionText.Text = $"#{scorePosition.Value.ToMetric(decimals: scorePosition < 100000 ? 1 : 0)}"; + positionText.Text = $"#{scorePosition.Value.FormatRank()}"; positionText.FadeTo(scorePosition.HasValue ? 1 : 0); updateColour(); diff --git a/osu.Game/Utils/FormatUtils.cs b/osu.Game/Utils/FormatUtils.cs index f2ab99f4b7..2578d8d835 100644 --- a/osu.Game/Utils/FormatUtils.cs +++ b/osu.Game/Utils/FormatUtils.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 Humanizer; + namespace osu.Game.Utils { public static class FormatUtils @@ -18,5 +20,11 @@ namespace osu.Game.Utils /// The accuracy to be formatted /// formatted accuracy in percentage public static string FormatAccuracy(this decimal accuracy) => $"{accuracy:0.00}%"; + + /// + /// Formats the supplied rank/leaderboard position in a consistent, simplified way. + /// + /// The rank/position to be formatted. + public static string FormatRank(this int rank) => rank.ToMetric(decimals: rank < 100_000 ? 1 : 0); } } From e2cc401c124b631c8ade6bde8120e71c44c3e2ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 15:05:59 +0100 Subject: [PATCH 5486/6909] Move BDL above LoadComplete() --- .../Play/HUD/GameplayLeaderboardScore.cs | 106 +++++++++--------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 9684ae016c..d510ea5f8b 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -75,59 +75,6 @@ namespace osu.Game.Screens.Play.HUD Size = new Vector2(EXTENDED_WIDTH, PANEL_HEIGHT); } - protected override void LoadComplete() - { - base.LoadComplete(); - - updateColour(); - FinishTransforms(true); - } - - private const double transition_duration = 500; - - private void updateColour() - { - if (scorePosition == 1) - { - mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, transition_duration, Easing.OutElastic); - panelColour = Color4Extensions.FromHex("7fcc33"); - textColour = Color4.White; - } - else if (trackedPlayer) - { - mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, transition_duration, Easing.OutElastic); - panelColour = Color4Extensions.FromHex("ffd966"); - textColour = Color4Extensions.FromHex("2e576b"); - } - else - { - mainFillContainer.ResizeWidthTo(regular_width, transition_duration, Easing.OutElastic); - panelColour = Color4Extensions.FromHex("3399cc"); - textColour = Color4.White; - } - } - - private Color4 panelColour - { - set - { - mainFillContainer.FadeColour(value, transition_duration, Easing.OutQuint); - centralFill.FadeColour(value, transition_duration, Easing.OutQuint); - } - } - - private Color4 textColour - { - set - { - scoreText.FadeColour(value, 200, Easing.OutQuint); - accuracyText.FadeColour(value, 200, Easing.OutQuint); - comboText.FadeColour(value, 200, Easing.OutQuint); - usernameText.FadeColour(value, 200, Easing.OutQuint); - positionText.FadeColour(value, 200, Easing.OutQuint); - } - } - [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -284,5 +231,58 @@ namespace osu.Game.Screens.Play.HUD Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); } + + protected override void LoadComplete() + { + base.LoadComplete(); + + updateColour(); + FinishTransforms(true); + } + + private const double transition_duration = 500; + + private void updateColour() + { + if (scorePosition == 1) + { + mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, transition_duration, Easing.OutElastic); + panelColour = Color4Extensions.FromHex("7fcc33"); + textColour = Color4.White; + } + else if (trackedPlayer) + { + mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, transition_duration, Easing.OutElastic); + panelColour = Color4Extensions.FromHex("ffd966"); + textColour = Color4Extensions.FromHex("2e576b"); + } + else + { + mainFillContainer.ResizeWidthTo(regular_width, transition_duration, Easing.OutElastic); + panelColour = Color4Extensions.FromHex("3399cc"); + textColour = Color4.White; + } + } + + private Color4 panelColour + { + set + { + mainFillContainer.FadeColour(value, transition_duration, Easing.OutQuint); + centralFill.FadeColour(value, transition_duration, Easing.OutQuint); + } + } + + private Color4 textColour + { + set + { + scoreText.FadeColour(value, 200, Easing.OutQuint); + accuracyText.FadeColour(value, 200, Easing.OutQuint); + comboText.FadeColour(value, 200, Easing.OutQuint); + usernameText.FadeColour(value, 200, Easing.OutQuint); + positionText.FadeColour(value, 200, Easing.OutQuint); + } + } } } From 315a957a0ca3ad242671e49ec73a27e05e1432b6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 15:17:31 +0100 Subject: [PATCH 5487/6909] Extract constant for text transition duration --- .../Screens/Play/HUD/GameplayLeaderboardScore.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index d510ea5f8b..cff3a1fc66 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -273,15 +273,17 @@ namespace osu.Game.Screens.Play.HUD } } + private const double text_transition_duration = 200; + private Color4 textColour { set { - scoreText.FadeColour(value, 200, Easing.OutQuint); - accuracyText.FadeColour(value, 200, Easing.OutQuint); - comboText.FadeColour(value, 200, Easing.OutQuint); - usernameText.FadeColour(value, 200, Easing.OutQuint); - positionText.FadeColour(value, 200, Easing.OutQuint); + scoreText.FadeColour(value, text_transition_duration, Easing.OutQuint); + accuracyText.FadeColour(value, text_transition_duration, Easing.OutQuint); + comboText.FadeColour(value, text_transition_duration, Easing.OutQuint); + usernameText.FadeColour(value, text_transition_duration, Easing.OutQuint); + positionText.FadeColour(value, text_transition_duration, Easing.OutQuint); } } } From 06a17a9d8cb9c94dd886e2b1e80992436521f42d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 15:18:05 +0100 Subject: [PATCH 5488/6909] Rename other constant to be distinguishable --- .../Screens/Play/HUD/GameplayLeaderboardScore.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index cff3a1fc66..b93db09f71 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -240,25 +240,25 @@ namespace osu.Game.Screens.Play.HUD FinishTransforms(true); } - private const double transition_duration = 500; + private const double panel_transition_duration = 500; private void updateColour() { if (scorePosition == 1) { - mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, transition_duration, Easing.OutElastic); + mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); panelColour = Color4Extensions.FromHex("7fcc33"); textColour = Color4.White; } else if (trackedPlayer) { - mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, transition_duration, Easing.OutElastic); + mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); panelColour = Color4Extensions.FromHex("ffd966"); textColour = Color4Extensions.FromHex("2e576b"); } else { - mainFillContainer.ResizeWidthTo(regular_width, transition_duration, Easing.OutElastic); + mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic); panelColour = Color4Extensions.FromHex("3399cc"); textColour = Color4.White; } @@ -268,8 +268,8 @@ namespace osu.Game.Screens.Play.HUD { set { - mainFillContainer.FadeColour(value, transition_duration, Easing.OutQuint); - centralFill.FadeColour(value, transition_duration, Easing.OutQuint); + mainFillContainer.FadeColour(value, panel_transition_duration, Easing.OutQuint); + centralFill.FadeColour(value, panel_transition_duration, Easing.OutQuint); } } From c738a57b399b5f701fa314fb2dc01de43659c092 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 18:48:17 +0100 Subject: [PATCH 5489/6909] Fix username overflow in new leaderboard design --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index b93db09f71..58281debf1 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -176,7 +176,7 @@ namespace osu.Game.Screens.Play.HUD usernameText = new OsuSpriteText { RelativeSizeAxes = Axes.X, - Width = 0.8f, + Width = 0.6f, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Colour = Color4.White, From afa6a869545692f4abed538f7d5e20e70a37a3c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 19:00:05 +0100 Subject: [PATCH 5490/6909] Remove unnecessary lookup incrementing --- .../Visual/Online/TestSceneCurrentlyPlayingDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index eaa881b02c..1666c9cde4 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Online protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) => Task.FromResult(new User { - Id = lookup++, + Id = lookup, Username = usernames[lookup % usernames.Length], }); } From ee33c0be93c16b61762edc6d85165fb5ec05fd93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 19:08:29 +0100 Subject: [PATCH 5491/6909] Extract combo & accuracy ratio calculation helpers --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index f5fb918ba3..10d0cc2865 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -201,8 +201,8 @@ namespace osu.Game.Rulesets.Scoring private double getScore(ScoringMode mode) { return GetScore(mode, maxAchievableCombo, - maxBaseScore > 0 ? baseScore / maxBaseScore : 0, - maxAchievableCombo > 0 ? (double)HighestCombo.Value / maxAchievableCombo : 1, + calculateAccuracyRatio(baseScore), + calculateComboRatio(HighestCombo.Value), scoreResultCounts); } @@ -252,14 +252,17 @@ namespace osu.Game.Rulesets.Scoring computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; } - double accuracy = maxBaseScore > 0 ? computedBaseScore / maxBaseScore : 0; - double comboRatio = maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1; + double accuracy = calculateAccuracyRatio(computedBaseScore); + double comboRatio = calculateComboRatio(maxCombo); double score = GetScore(mode, maxAchievableCombo, accuracy, comboRatio, scoreResultCounts); return (score, accuracy); } + private double calculateAccuracyRatio(double baseScore) => maxBaseScore > 0 ? baseScore / maxBaseScore : 0; + private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1; + private double getBonusScore(Dictionary statistics) => statistics.GetOrDefault(HitResult.SmallBonus) * Judgement.SMALL_BONUS_SCORE + statistics.GetOrDefault(HitResult.LargeBonus) * Judgement.LARGE_BONUS_SCORE; From 631a0cea41e7e21b1ab592bc80fa748c733a22d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 21:25:04 +0100 Subject: [PATCH 5492/6909] Fix intended random factor not being random in test --- .../Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs index 0c8d8ca165..0ca1d9008b 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.Gameplay { foreach (var userId in PlayingUsers) { - if (RNG.Next(0, 1) == 1) + if (RNG.NextBool()) continue; if (!lastHeaders.TryGetValue(userId, out var header)) From f827b6c030b3cbde588effdd908b3138eff827fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 21:26:40 +0100 Subject: [PATCH 5493/6909] Use terser dictionary initialiser syntax --- .../TestSceneMultiplayerGameplayLeaderboard.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs index 0ca1d9008b..e42ddeb35e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -116,12 +116,12 @@ namespace osu.Game.Tests.Visual.Gameplay { lastHeaders[userId] = header = new FrameHeader(new ScoreInfo { - Statistics = new Dictionary(new[] + Statistics = new Dictionary { - new KeyValuePair(HitResult.Miss, 0), - new KeyValuePair(HitResult.Meh, 0), - new KeyValuePair(HitResult.Great, 0) - }) + [HitResult.Miss] = 0, + [HitResult.Meh] = 0, + [HitResult.Great] = 0 + } }); } From 4e5064c4f611de5704982e8dbbe670a247814f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 19 Dec 2020 21:31:17 +0100 Subject: [PATCH 5494/6909] Start accuracy at 1 --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index c96f496cd0..12321de442 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -108,7 +108,7 @@ namespace osu.Game.Screens.Play.HUD public IBindableNumber Accuracy => accuracy; - private readonly BindableDouble accuracy = new BindableDouble(); + private readonly BindableDouble accuracy = new BindableDouble(1); public IBindableNumber CurrentCombo => currentCombo; From b87f89986a5b2bb5fd73a43ba04459df84f92417 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 19 Dec 2020 16:57:25 -0800 Subject: [PATCH 5495/6909] Fix selected item not being highlighted on some setting dropdowns --- .../Settings/Sections/Graphics/LayoutSettings.cs | 3 +-- .../Sections/UserInterface/MainMenuSettings.cs | 11 +++-------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 62dc1dc806..3df2a78552 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -59,11 +59,10 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics Children = new Drawable[] { - windowModeDropdown = new SettingsDropdown + windowModeDropdown = new SettingsEnumDropdown { LabelText = "Screen mode", Current = config.GetBindable(FrameworkSetting.WindowMode), - ItemSource = windowModes, }, resolutionSettingsContainer = new Container { diff --git a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs index 598b666642..95e2e9da30 100644 --- a/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/UserInterface/MainMenuSettings.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Configuration; @@ -28,23 +26,20 @@ namespace osu.Game.Overlays.Settings.Sections.UserInterface LabelText = "osu! music theme", Current = config.GetBindable(OsuSetting.MenuMusic) }, - new SettingsDropdown + new SettingsEnumDropdown { LabelText = "Intro sequence", Current = config.GetBindable(OsuSetting.IntroSequence), - Items = Enum.GetValues(typeof(IntroSequence)).Cast() }, - new SettingsDropdown + new SettingsEnumDropdown { LabelText = "Background source", Current = config.GetBindable(OsuSetting.MenuBackgroundSource), - Items = Enum.GetValues(typeof(BackgroundSource)).Cast() }, - new SettingsDropdown + new SettingsEnumDropdown { LabelText = "Seasonal backgrounds", Current = config.GetBindable(OsuSetting.SeasonalBackgroundMode), - Items = Enum.GetValues(typeof(SeasonalBackgroundMode)).Cast() } }; } From 5b8e35c98cec02b359eb2cf598713638538d349a Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 19 Dec 2020 16:57:42 -0800 Subject: [PATCH 5496/6909] Make settings dropdown abstract --- osu.Game/Overlays/Settings/SettingsDropdown.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs index 1175ddaab8..e1c6333aa0 100644 --- a/osu.Game/Overlays/Settings/SettingsDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { - public class SettingsDropdown : SettingsItem + public abstract class SettingsDropdown : SettingsItem { protected new OsuDropdown Control => (OsuDropdown)base.Control; From d20eb368f5f86a22bf91a9f8f9838ef862effbc5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 17:36:23 +0900 Subject: [PATCH 5497/6909] Make return into IEnumerable --- osu.Game/Screens/Multi/Components/RoomManager.cs | 2 +- osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 46941dc58e..ffc5e94106 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -183,6 +183,6 @@ namespace osu.Game.Screens.Multi.Components existing.CopyFrom(room); } - protected abstract RoomPollingComponent[] CreatePollingComponents(); + protected abstract IEnumerable CreatePollingComponents(); } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs index ba96721afc..d21f844e04 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Bindables; using osu.Game.Screens.Multi.Components; @@ -11,7 +12,7 @@ namespace osu.Game.Screens.Multi.Timeshift public readonly Bindable TimeBetweenListingPolls = new Bindable(); public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); - protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] + protected override IEnumerable CreatePollingComponents() => new RoomPollingComponent[] { new ListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls } }, new SelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls } } From 7c7f15089a6ee06b3e244fc03db8e7114e165cca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 17:42:23 +0900 Subject: [PATCH 5498/6909] Make CreateRoomManager return the drawable version --- osu.Game/Screens/Multi/Multiplayer.cs | 8 ++++---- osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 5f61c5e635..837ccdf2e9 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Multi private readonly IBindable isIdle = new BindableBool(); [Cached(Type = typeof(IRoomManager))] - protected IRoomManager RoomManager { get; private set; } + protected RoomManager RoomManager { get; private set; } [Cached] private readonly Bindable selectedRoom = new Bindable(); @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi InternalChild = waves = new MultiplayerWaveContainer { RelativeSizeAxes = Axes.Both, - Children = new[] + Children = new Drawable[] { new Box { @@ -136,7 +136,7 @@ namespace osu.Game.Screens.Multi Origin = Anchor.TopRight, Action = () => CreateRoom() }, - (Drawable)(RoomManager = CreateRoomManager()) + RoomManager = CreateRoomManager() } }; @@ -353,7 +353,7 @@ namespace osu.Game.Screens.Multi } } - protected abstract IRoomManager CreateRoomManager(); + protected abstract RoomManager CreateRoomManager(); private class MultiplayerWaveContainer : WaveContainer { diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs index 1ff9c670a8..d2d6a35a2e 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs @@ -3,6 +3,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Match; @@ -43,6 +44,6 @@ namespace osu.Game.Screens.Multi.Timeshift Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})"); } - protected override IRoomManager CreateRoomManager() => new TimeshiftRoomManager(); + protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager(); } } From d74485704ae9c4a424bfb5fc5780b03e14f3f8be Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 17:46:45 +0900 Subject: [PATCH 5499/6909] Reset intial rooms received on filter change --- osu.Game/Screens/Multi/Components/ListingPollingComponent.cs | 2 ++ osu.Game/Screens/Multi/Components/RoomPollingComponent.cs | 5 ++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs index e22f09779e..ebb3403950 100644 --- a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs @@ -25,6 +25,8 @@ namespace osu.Game.Screens.Multi.Components { currentFilter.BindValueChanged(_ => { + InitialRoomsReceived.Value = false; + if (IsLoaded) PollImmediately(); }); diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs index 5430d54644..a81a9540c3 100644 --- a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs @@ -21,8 +21,7 @@ namespace osu.Game.Screens.Multi.Components /// public new readonly Bindable TimeBetweenPolls = new Bindable(); - public IBindable InitialRoomsReceived => initialRoomsReceived; - private readonly Bindable initialRoomsReceived = new Bindable(); + public readonly Bindable InitialRoomsReceived = new Bindable(); [Resolved] protected IAPIProvider API { get; private set; } @@ -34,7 +33,7 @@ namespace osu.Game.Screens.Multi.Components protected void NotifyRoomsReceived(List rooms) { - initialRoomsReceived.Value = true; + InitialRoomsReceived.Value = true; RoomsReceived?.Invoke(rooms); } } From 812a1d2b4f43cfb1eade5f22d9290bbe26d0fb27 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:03:30 +0900 Subject: [PATCH 5500/6909] Fix onSuccess callback potentially being called on failure --- .../Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 4f73ee3865..1a6e976d15 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer Debug.Assert(room.RoomID.Value != null); var joinTask = multiplayerClient.JoinRoom(room); - joinTask.ContinueWith(_ => onSuccess?.Invoke(room)); + joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion); joinTask.ContinueWith(t => { PartRoom(); From fb61cdfd417701b4cacfe1aefd4bb374e344c2e5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:05:43 +0900 Subject: [PATCH 5501/6909] Remove unnecessary first-frame polling + address concerns --- .../Screens/Multi/Components/RoomManager.cs | 11 ++++++----- .../RealtimeMultiplayer/RealtimeRoomManager.cs | 17 ++++++++++++----- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 21bff70b8b..7b419c9efe 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -27,7 +27,8 @@ namespace osu.Game.Screens.Multi.Components public IBindableList Rooms => rooms; - protected Room JoinedRoom { get; private set; } + protected IBindable JoinedRoom => joinedRoom; + private readonly Bindable joinedRoom = new Bindable(); [Resolved] private RulesetStore rulesets { get; set; } @@ -64,7 +65,7 @@ namespace osu.Game.Screens.Multi.Components req.Success += result => { - JoinedRoom = room; + joinedRoom.Value = room; update(room, result); addRoom(room); @@ -93,7 +94,7 @@ namespace osu.Game.Screens.Multi.Components currentJoinRoomRequest.Success += () => { - JoinedRoom = room; + joinedRoom.Value = room; onSuccess?.Invoke(room); }; @@ -114,8 +115,8 @@ namespace osu.Game.Screens.Multi.Components if (JoinedRoom == null) return; - api.Queue(new PartRoomRequest(JoinedRoom)); - JoinedRoom = null; + api.Queue(new PartRoomRequest(joinedRoom.Value)); + joinedRoom.Value = null; } private readonly HashSet ignoredRooms = new HashSet(); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 1a6e976d15..9e3c921d9e 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -30,7 +30,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer base.LoadComplete(); isConnected.BindTo(multiplayerClient.IsConnected); - isConnected.BindValueChanged(_ => Schedule(updatePolling), true); + isConnected.BindValueChanged(_ => Schedule(updatePolling)); + JoinedRoom.BindValueChanged(_ => updatePolling()); updatePolling(); } @@ -46,7 +47,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer if (JoinedRoom == null) return; - var joinedRoom = JoinedRoom; + var joinedRoom = JoinedRoom.Value; base.PartRoom(); multiplayerClient.LeaveRoom().Wait(); @@ -77,7 +78,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer ClearRooms(); // Don't poll when not connected or when a room has been joined. - allowPolling.Value = isConnected.Value && JoinedRoom == null; + allowPolling.Value = isConnected.Value && JoinedRoom.Value == null; } protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] @@ -102,8 +103,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { base.LoadComplete(); - AllowPolling.BindValueChanged(_ => + AllowPolling.BindValueChanged(allowPolling => { + if (!allowPolling.NewValue) + return; + if (IsLoaded) PollImmediately(); }); @@ -120,8 +124,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { base.LoadComplete(); - AllowPolling.BindValueChanged(_ => + AllowPolling.BindValueChanged(allowPolling => { + if (!allowPolling.NewValue) + return; + if (IsLoaded) PollImmediately(); }); From 724e4b83fe266896bef4c10f9a51840cc36a24ae Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:19:41 +0900 Subject: [PATCH 5502/6909] Fix nullability and remove early check --- .../StatefulMultiplayerClient.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 60960f4929..fe2f4c88f0 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -9,6 +9,7 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Database; @@ -140,20 +141,14 @@ namespace osu.Game.Online.RealtimeMultiplayer RulesetID = Room.Settings.RulesetID }; - var newSettings = new MultiplayerRoomSettings + ChangeSettings(new MultiplayerRoomSettings { Name = name.GetOr(Room.Settings.Name), BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, RulesetID = item.GetOr(existingPlaylistItem).RulesetID, - Mods = item.HasValue ? item.Value!.RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods - }; - - // Make sure there would be a meaningful change in settings. - if (newSettings.Equals(Room.Settings)) - return; - - ChangeSettings(newSettings); + Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods + }); } public abstract Task TransferHost(int userId); From ba4307a74c9bf82503b2e5dad391e3b28442959a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:24:13 +0900 Subject: [PATCH 5503/6909] Directly return task --- .../RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs index bfa8362c7e..de52633c88 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs @@ -106,14 +106,14 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer return Task.CompletedTask; } - public override async Task StartMatch() + public override Task StartMatch() { Debug.Assert(Room != null); foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) ChangeUserState(user.UserID, MultiplayerUserState.WaitingForLoad); - await ((IMultiplayerClient)this).LoadRequested(); + return ((IMultiplayerClient)this).LoadRequested(); } } } From 1e2b425f3f0b787a747896e485a80a066b1a7429 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:24:15 +0900 Subject: [PATCH 5504/6909] Fix incorrect test name + assertion --- .../RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs index 598641682b..925a83a863 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer } [Test] - public void TestMultiplayerRoomPartedWhenAPIRoomJoined() + public void TestMultiplayerRoomJoinedWhenAPIRoomJoined() { AddStep("create room manager with a room", () => { @@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer }); }); - AddAssert("multiplayer room parted", () => roomContainer.Client.Room != null); + AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null); } private TestRealtimeRoomManager createRoomManager() From 8b1f5ff4927fa83700a4b22e213fe8ff0914d8d8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:25:23 +0900 Subject: [PATCH 5505/6909] Only instantiate ruleset once --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index fe2f4c88f0..77dbc16786 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -354,14 +354,14 @@ namespace osu.Game.Online.RealtimeMultiplayer var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); beatmap.MD5Hash = settings.BeatmapChecksum; - var ruleset = rulesets.GetRuleset(settings.RulesetID); - var mods = settings.Mods.Select(m => m.ToMod(ruleset.CreateInstance())); + var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); + var mods = settings.Mods.Select(m => m.ToMod(ruleset)); PlaylistItem playlistItem = new PlaylistItem { ID = playlistItemId, Beatmap = { Value = beatmap }, - Ruleset = { Value = ruleset }, + Ruleset = { Value = ruleset.RulesetInfo }, }; playlistItem.RequiredMods.AddRange(mods); From 508f73d94961b8baae97956a3c328b41277ed434 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:25:54 +0900 Subject: [PATCH 5506/6909] Fix up comment --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 77dbc16786..35c8a3397e 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -328,7 +328,7 @@ namespace osu.Game.Online.RealtimeMultiplayer if (Room == null) return; - // Update a few instantaneously properties of the room. + // Update a few properties of the room instantaneously. Schedule(() => { if (Room == null) From 0cf078562dada06982d9af80136c02e145d42efa Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:30:00 +0900 Subject: [PATCH 5507/6909] Split method up and remove nested scheduling --- .../StatefulMultiplayerClient.cs | 59 +++++++++---------- 1 file changed, 29 insertions(+), 30 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 35c8a3397e..8d3b161804 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -15,6 +15,7 @@ using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Rulesets; @@ -347,38 +348,36 @@ namespace osu.Game.Online.RealtimeMultiplayer }); var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => - { - var beatmapSet = res.ToBeatmapSet(rulesets); - - var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); - beatmap.MD5Hash = settings.BeatmapChecksum; - - var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); - var mods = settings.Mods.Select(m => m.ToMod(ruleset)); - - PlaylistItem playlistItem = new PlaylistItem - { - ID = playlistItemId, - Beatmap = { Value = beatmap }, - Ruleset = { Value = ruleset.RulesetInfo }, - }; - - playlistItem.RequiredMods.AddRange(mods); - - Schedule(() => - { - if (Room == null || !Room.Settings.Equals(settings)) - return; - - Debug.Assert(apiRoom != null); - - apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. - apiRoom.Playlist.Add(playlistItem); - }); - }; + req.Success += res => updatePlaylist(settings, res); api.Queue(req); } + + private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) + { + if (Room == null || !Room.Settings.Equals(settings)) + return; + + Debug.Assert(apiRoom != null); + + var beatmapSet = onlineSet.ToBeatmapSet(rulesets); + var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); + beatmap.MD5Hash = settings.BeatmapChecksum; + + var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); + var mods = settings.Mods.Select(m => m.ToMod(ruleset)); + + PlaylistItem playlistItem = new PlaylistItem + { + ID = playlistItemId, + Beatmap = { Value = beatmap }, + Ruleset = { Value = ruleset.RulesetInfo }, + }; + + playlistItem.RequiredMods.AddRange(mods); + + apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. + apiRoom.Playlist.Add(playlistItem); + } } } From 45107280a00e310402a368a6f9b15050f1bcfd0a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:34:54 +0900 Subject: [PATCH 5508/6909] Make TimeBetweenPolls into a bindable --- .../Components/TestScenePollingComponent.cs | 10 +++---- osu.Game/Online/Chat/ChannelManager.cs | 2 +- osu.Game/Online/PollingComponent.cs | 30 ++++++++----------- .../Multi/Components/RoomPollingComponent.cs | 11 ------- 4 files changed, 19 insertions(+), 34 deletions(-) diff --git a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs index fb10015ef4..2236f85b92 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePollingComponent.cs @@ -61,12 +61,12 @@ namespace osu.Game.Tests.Visual.Components { createPoller(true); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust); checkCount(1); checkCount(2); checkCount(3); - AddStep("set poll interval to 5", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5); + AddStep("set poll interval to 5", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust * 5); checkCount(4); checkCount(4); checkCount(4); @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Components checkCount(5); checkCount(5); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust); checkCount(6); checkCount(7); } @@ -87,7 +87,7 @@ namespace osu.Game.Tests.Visual.Components { createPoller(false); - AddStep("set poll interval to 1", () => poller.TimeBetweenPolls = TimePerAction * safety_adjust * 5); + AddStep("set poll interval to 1", () => poller.TimeBetweenPolls.Value = TimePerAction * safety_adjust * 5); checkCount(0); skip(); checkCount(0); @@ -141,7 +141,7 @@ namespace osu.Game.Tests.Visual.Components public class TestSlowPoller : TestPoller { - protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls / 2f / Clock.Rate)).ContinueWith(_ => base.Poll()); + protected override Task Poll() => Task.Delay((int)(TimeBetweenPolls.Value / 2f / Clock.Rate)).ContinueWith(_ => base.Poll()); } } } diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 16f46581c5..62ae507419 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -57,7 +57,7 @@ namespace osu.Game.Online.Chat { CurrentChannel.ValueChanged += currentChannelChanged; - HighPollRate.BindValueChanged(enabled => TimeBetweenPolls = enabled.NewValue ? 1000 : 6000, true); + HighPollRate.BindValueChanged(enabled => TimeBetweenPolls.Value = enabled.NewValue ? 1000 : 6000, true); } /// diff --git a/osu.Game/Online/PollingComponent.cs b/osu.Game/Online/PollingComponent.cs index 228f147835..3d19f2ab09 100644 --- a/osu.Game/Online/PollingComponent.cs +++ b/osu.Game/Online/PollingComponent.cs @@ -3,6 +3,7 @@ using System; using System.Threading.Tasks; +using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Framework.Threading; @@ -19,22 +20,11 @@ namespace osu.Game.Online private bool pollingActive; - private double timeBetweenPolls; - /// /// The time in milliseconds to wait between polls. /// Setting to zero stops all polling. /// - public double TimeBetweenPolls - { - get => timeBetweenPolls; - set - { - timeBetweenPolls = value; - scheduledPoll?.Cancel(); - pollIfNecessary(); - } - } + public readonly Bindable TimeBetweenPolls = new Bindable(); /// /// @@ -42,7 +32,13 @@ namespace osu.Game.Online /// The initial time in milliseconds to wait between polls. Setting to zero stops all polling. protected PollingComponent(double timeBetweenPolls = 0) { - TimeBetweenPolls = timeBetweenPolls; + TimeBetweenPolls.BindValueChanged(_ => + { + scheduledPoll?.Cancel(); + pollIfNecessary(); + }); + + TimeBetweenPolls.Value = timeBetweenPolls; } protected override void LoadComplete() @@ -60,7 +56,7 @@ namespace osu.Game.Online if (pollingActive) return false; // don't try polling if the time between polls hasn't been set. - if (timeBetweenPolls == 0) return false; + if (TimeBetweenPolls.Value == 0) return false; if (!lastTimePolled.HasValue) { @@ -68,7 +64,7 @@ namespace osu.Game.Online return true; } - if (Time.Current - lastTimePolled.Value > timeBetweenPolls) + if (Time.Current - lastTimePolled.Value > TimeBetweenPolls.Value) { doPoll(); return true; @@ -99,7 +95,7 @@ namespace osu.Game.Online /// public void PollImmediately() { - lastTimePolled = Time.Current - timeBetweenPolls; + lastTimePolled = Time.Current - TimeBetweenPolls.Value; scheduleNextPoll(); } @@ -121,7 +117,7 @@ namespace osu.Game.Online double lastPollDuration = lastTimePolled.HasValue ? Time.Current - lastTimePolled.Value : 0; - scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, timeBetweenPolls - lastPollDuration)); + scheduledPoll = Scheduler.AddDelayed(doPoll, Math.Max(0, TimeBetweenPolls.Value - lastPollDuration)); } } } diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs index a81a9540c3..ad0720db7b 100644 --- a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs @@ -15,22 +15,11 @@ namespace osu.Game.Screens.Multi.Components { public Action> RoomsReceived; - /// - /// The time in milliseconds to wait between polls. - /// Setting to zero stops all polling. - /// - public new readonly Bindable TimeBetweenPolls = new Bindable(); - public readonly Bindable InitialRoomsReceived = new Bindable(); [Resolved] protected IAPIProvider API { get; private set; } - protected RoomPollingComponent() - { - TimeBetweenPolls.BindValueChanged(time => base.TimeBetweenPolls = time.NewValue); - } - protected void NotifyRoomsReceived(List rooms) { InitialRoomsReceived.Value = true; From ce2560b545deea1076c5f6cfd781e9089a360061 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:36:31 +0900 Subject: [PATCH 5509/6909] Extract value into const --- .../Participants/ParticipantPanel.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs index 306a54bfdc..002849a275 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs @@ -142,15 +142,17 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants if (Room == null) return; + const double fade_time = 50; + if (User.State == MultiplayerUserState.Ready) - readyMark.FadeIn(50); + readyMark.FadeIn(fade_time); else - readyMark.FadeOut(50); + readyMark.FadeOut(fade_time); if (Room.Host?.Equals(User) == true) - crown.FadeIn(50); + crown.FadeIn(fade_time); else - crown.FadeOut(50); + crown.FadeOut(fade_time); } public MenuItem[] ContextMenuItems From 19db35501e156ceaaf45f65bb4360dc4d859a68f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:44:36 +0900 Subject: [PATCH 5510/6909] Fix incorrect end date usage in timeshift ready button --- osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs index b6698b195c..ba639c29f4 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Multi.Timeshift public class TimeshiftReadyButton : ReadyButton { [Resolved(typeof(Room), nameof(Room.EndDate))] - private Bindable endDate { get; set; } + private Bindable endDate { get; set; } public TimeshiftReadyButton() { @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Timeshift { base.Update(); - Enabled.Value = endDate.Value == null || DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; + Enabled.Value = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; } } } From a07a36793a5d90781178b0f94743580359c17973 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:44:41 +0900 Subject: [PATCH 5511/6909] Fix test not working --- .../RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs index 889c0c0be3..1f863028af 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs @@ -97,6 +97,12 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer [Test] public void TestBecomeHostWhileReady() { + AddStep("add host", () => + { + Client.AddUser(new User { Id = 2, Username = "Another user" }); + Client.TransferHost(2); + }); + addClickButtonStep(); AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); From b002c466660535e32e036d322e97e68335c6c497 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 18:49:39 +0900 Subject: [PATCH 5512/6909] Add number of ready users to button --- .../Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs index d52df258ad..ea8fb04994 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs @@ -69,8 +69,9 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer case MultiplayerUserState.Ready: if (Room?.Host?.Equals(localUser) == true) { - button.Text = "Let's go!"; - updateButtonColour(Room.Users.All(u => u.State == MultiplayerUserState.Ready)); + int countReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); + button.Text = $"Start match ({countReady} / {Room.Users.Count} ready)"; + updateButtonColour(true); } else { From f1aefcdf86a9ef784644614915bd11c1476c9816 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 19 Dec 2020 22:48:39 +0100 Subject: [PATCH 5513/6909] Handle multiple extensions in the import files. --- osu.Game/OsuGameBase.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 521778a9cd..ca87772209 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -397,13 +397,18 @@ namespace osu.Game public virtual async Task Import(params ImportTask[] tasks) { - var extension = Path.GetExtension(tasks.First().Path).ToLowerInvariant(); + var importTasks = new List(); - foreach (var importer in fileImporters) + foreach (var extension in tasks.Select(t => Path.GetExtension(t.Path)).Distinct()) { - if (importer.HandledExtensions.Contains(extension)) - await importer.Import(tasks); + var importList = tasks.Where(t => t.Path.EndsWith(extension, StringComparison.OrdinalIgnoreCase)); + var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(extension)); + + if (importer != null) + importTasks.Add(importer.Import(importList.ToArray())); } + + await Task.WhenAll(importTasks); } public IEnumerable HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); From f876a329b1b4e8b2cbef4effd6ef242e1be1c233 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 22:51:33 +0900 Subject: [PATCH 5514/6909] Fire-and-forget leave-room request --- .../StatefulMultiplayerClient.cs | 29 +++++++++++++++++-- .../RealtimeRoomManager.cs | 2 +- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 60960f4929..b846d6732f 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -77,7 +77,9 @@ namespace osu.Game.Online.RealtimeMultiplayer /// The API . public async Task JoinRoom(Room room) { - Debug.Assert(Room == null); + if (Room != null) + throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + Debug.Assert(room.RoomID.Value != null); apiRoom = room; @@ -166,6 +168,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) @@ -198,6 +203,9 @@ namespace osu.Game.Online.RealtimeMultiplayer async Task IMultiplayerClient.UserJoined(MultiplayerRoomUser user) { + if (Room == null) + return; + await PopulateUser(user); Schedule(() => @@ -213,6 +221,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) @@ -229,6 +240,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.HostChanged(int userId) { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) @@ -255,6 +269,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.UserStateChanged(int userId, MultiplayerUserState state) { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) @@ -273,6 +290,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.LoadRequested() { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) @@ -286,7 +306,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.MatchStarted() { - Debug.Assert(Room != null); + if (Room == null) + return Task.CompletedTask; + var players = Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID).ToList(); Schedule(() => @@ -304,6 +326,9 @@ namespace osu.Game.Online.RealtimeMultiplayer Task IMultiplayerClient.ResultsReady() { + if (Room == null) + return Task.CompletedTask; + Schedule(() => { if (Room == null) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 4f73ee3865..d2a03da714 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer var joinedRoom = JoinedRoom; base.PartRoom(); - multiplayerClient.LeaveRoom().Wait(); + multiplayerClient.LeaveRoom(); // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. RemoveRoom(joinedRoom); From 9d13a5b06a9f90e9cd9a8e489594f03b3d3ec4f0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 22:53:07 +0900 Subject: [PATCH 5515/6909] Fix potential cross-thread list access --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index b846d6732f..bf58849e35 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -309,14 +309,12 @@ namespace osu.Game.Online.RealtimeMultiplayer if (Room == null) return Task.CompletedTask; - var players = Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID).ToList(); - Schedule(() => { if (Room == null) return; - PlayingUsers.AddRange(players); + PlayingUsers.AddRange(Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID)); MatchStarted?.Invoke(); }); From c33e693b8e3331af505931950940dc7684b04f41 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 23:05:17 +0900 Subject: [PATCH 5516/6909] Refactor InitialRoomsReceived to avoid extra bindables --- .../Visual/Multiplayer/TestRoomManager.cs | 2 +- .../Multiplayer/TestSceneMatchSettingsOverlay.cs | 2 +- .../Multiplayer/TestSceneMatchSubScreen.cs | 2 +- .../Multi/Components/ListingPollingComponent.cs | 3 +-- osu.Game/Screens/Multi/Components/RoomManager.cs | 12 ++++++++++-- .../Multi/Components/RoomPollingComponent.cs | 16 ++++++++-------- osu.Game/Screens/Multi/IRoomManager.cs | 2 +- osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs | 2 +- 8 files changed, 24 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs index 67a53307fc..9dd4aea4bd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public readonly BindableList Rooms = new BindableList(); - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); IBindableList IRoomManager.Rooms => Rooms; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index cbe8cc6137..234374ee2b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -131,7 +131,7 @@ namespace osu.Game.Tests.Visual.Multiplayer remove { } } - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); public IBindableList Rooms => null; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs index 65e9893851..bceb6efac1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.Multiplayer remove => throw new NotImplementedException(); } - public Bindable InitialRoomsReceived { get; } = new Bindable(true); + public IBindable InitialRoomsReceived { get; } = new Bindable(true); public IBindableList Rooms { get; } = new BindableList(); diff --git a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs index ebb3403950..dff6c50bf2 100644 --- a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs @@ -25,8 +25,7 @@ namespace osu.Game.Screens.Multi.Components { currentFilter.BindValueChanged(_ => { - InitialRoomsReceived.Value = false; - + NotifyRoomsReceived(null); if (IsLoaded) PollImmediately(); }); diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index ffc5e94106..382ce52723 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -23,7 +23,8 @@ namespace osu.Game.Screens.Multi.Components private readonly BindableList rooms = new BindableList(); - public Bindable InitialRoomsReceived { get; } = new Bindable(); + public IBindable InitialRoomsReceived => initialRoomsReceived; + private readonly Bindable initialRoomsReceived = new Bindable(); public IBindableList Rooms => rooms; @@ -44,7 +45,6 @@ namespace osu.Game.Screens.Multi.Components InternalChildren = CreatePollingComponents().Select(p => { - p.InitialRoomsReceived.BindTo(InitialRoomsReceived); p.RoomsReceived = onRoomsReceived; return p; }).ToList(); @@ -122,6 +122,13 @@ namespace osu.Game.Screens.Multi.Components private void onRoomsReceived(List received) { + if (received == null) + { + rooms.Clear(); + initialRoomsReceived.Value = false; + return; + } + // Remove past matches foreach (var r in rooms.ToList()) { @@ -155,6 +162,7 @@ namespace osu.Game.Screens.Multi.Components } RoomsUpdated?.Invoke(); + initialRoomsReceived.Value = true; } /// diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs index ad0720db7b..fbaf9dd930 100644 --- a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; @@ -13,17 +12,18 @@ namespace osu.Game.Screens.Multi.Components { public abstract class RoomPollingComponent : PollingComponent { + /// + /// Invoked when any s have been received from the API. + /// + /// Any s present locally but not returned by this event are to be removed from display. + /// If null, the display of local rooms is reset to an initial state. + /// + /// public Action> RoomsReceived; - public readonly Bindable InitialRoomsReceived = new Bindable(); - [Resolved] protected IAPIProvider API { get; private set; } - protected void NotifyRoomsReceived(List rooms) - { - InitialRoomsReceived.Value = true; - RoomsReceived?.Invoke(rooms); - } + protected void NotifyRoomsReceived(List rooms) => RoomsReceived?.Invoke(rooms); } } diff --git a/osu.Game/Screens/Multi/IRoomManager.cs b/osu.Game/Screens/Multi/IRoomManager.cs index 3d18edcd71..630e3af91c 100644 --- a/osu.Game/Screens/Multi/IRoomManager.cs +++ b/osu.Game/Screens/Multi/IRoomManager.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.Multi /// /// Whether an initial listing of rooms has been received. /// - Bindable InitialRoomsReceived { get; } + IBindable 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 a26a64d86d..165a2b201c 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -27,7 +27,7 @@ namespace osu.Game.Screens.Multi.Lounge protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); - private readonly Bindable initialRoomsReceived = new Bindable(); + private readonly IBindable initialRoomsReceived = new Bindable(); private Container content; private LoadingLayer loadingLayer; From 594db76cf3191ae55861633c1efa187794a018c5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 23:10:45 +0900 Subject: [PATCH 5517/6909] Fix compilation errors --- osu.Game/Screens/Multi/Components/RoomManager.cs | 5 ++--- .../Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 3 ++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 0bcf3a90c3..276a5a6148 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -125,8 +125,7 @@ namespace osu.Game.Screens.Multi.Components { if (received == null) { - rooms.Clear(); - initialRoomsReceived.Value = false; + ClearRooms(); return; } @@ -171,7 +170,7 @@ namespace osu.Game.Screens.Multi.Components protected void ClearRooms() { rooms.Clear(); - InitialRoomsReceived.Value = false; + initialRoomsReceived.Value = false; } /// diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 9e3c921d9e..a50628a5fa 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.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.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -81,7 +82,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer allowPolling.Value = isConnected.Value && JoinedRoom.Value == null; } - protected override RoomPollingComponent[] CreatePollingComponents() => new RoomPollingComponent[] + protected override IEnumerable CreatePollingComponents() => new RoomPollingComponent[] { listingPollingComponent = new RealtimeListingPollingComponent { From 1d7d8bd6fccba84c32f004caddaf3fb0a3bdfe33 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 23:26:31 +0900 Subject: [PATCH 5518/6909] Hook up a realtime multiplayer client --- .../RealtimeMultiplayerClient.cs | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs diff --git a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs new file mode 100644 index 0000000000..6f18e1c922 --- /dev/null +++ b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs @@ -0,0 +1,171 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Logging; +using osu.Game.Online.API; + +namespace osu.Game.Online.RealtimeMultiplayer +{ + public class RealtimeMultiplayerClient : StatefulMultiplayerClient + { + private const string endpoint = "https://spectator.ppy.sh/multiplayer"; + + public override IBindable IsConnected => isConnected; + + private readonly Bindable isConnected = new Bindable(); + private readonly IBindable apiState = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private HubConnection? connection; + + [BackgroundDependencyLoader] + private void load() + { + apiState.BindTo(api.State); + apiState.BindValueChanged(apiStateChanged, true); + } + + private void apiStateChanged(ValueChangedEvent state) + { + switch (state.NewValue) + { + case APIState.Failing: + case APIState.Offline: + connection?.StopAsync(); + connection = null; + break; + + case APIState.Online: + Task.Run(Connect); + break; + } + } + + protected virtual async Task Connect() + { + if (connection != null) + return; + + connection = new HubConnectionBuilder() + .WithUrl(endpoint, options => + { + options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); + }) + .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }) + .Build(); + + // this is kind of SILLY + // https://github.com/dotnet/aspnetcore/issues/15198 + connection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); + connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); + connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); + connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); + connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); + connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); + connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); + connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); + connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); + + connection.Closed += async ex => + { + isConnected.Value = false; + + if (ex != null) + { + Logger.Log($"Multiplayer client lost connection: {ex}", LoggingTarget.Network); + await tryUntilConnected(); + } + }; + + await tryUntilConnected(); + + async Task tryUntilConnected() + { + Logger.Log("Multiplayer client connecting...", LoggingTarget.Network); + + while (api.State.Value == APIState.Online) + { + try + { + // reconnect on any failure + await connection.StartAsync(); + Logger.Log("Multiplayer client connected!", LoggingTarget.Network); + + // Success. + isConnected.Value = true; + break; + } + catch (Exception e) + { + Logger.Log($"Multiplayer client connection error: {e}", LoggingTarget.Network); + await Task.Delay(5000); + } + } + } + } + + protected override Task JoinRoom(long roomId) + { + if (!isConnected.Value) + return Task.FromCanceled(CancellationToken.None); + + return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoom), roomId); + } + + public override async Task LeaveRoom() + { + if (!isConnected.Value) + return; + + if (Room == null) + return; + + await base.LeaveRoom(); + await connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); + } + + public override Task TransferHost(int userId) + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId); + } + + public override Task ChangeSettings(MultiplayerRoomSettings settings) + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings); + } + + public override Task ChangeState(MultiplayerUserState newState) + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState); + } + + public override Task StartMatch() + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); + } + } +} From 455a84c73fb616e89ec76c8af8cb553ed61d3688 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 23:32:57 +0900 Subject: [PATCH 5519/6909] Add realtime multiplayer screen --- .../Multi/Lounge/Components/DrawableRoom.cs | 2 +- osu.Game/Screens/Multi/Multiplayer.cs | 12 +++- .../RealtimeMultiplayer.cs | 65 +++++++++++++++++++ 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index 01a85382e4..56116b219a 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -242,7 +242,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - multiplayer?.CreateRoom(Room.CreateCopy()); + multiplayer?.OpenNewRoom(Room.CreateCopy()); }) }; } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 837ccdf2e9..027fedaad6 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -134,7 +134,7 @@ namespace osu.Game.Screens.Multi { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Action = () => CreateRoom() + Action = () => OpenNewRoom() }, RoomManager = CreateRoomManager() } @@ -264,10 +264,16 @@ namespace osu.Game.Screens.Multi } /// - /// Create a new room. + /// Creates and opens the newly-created room. /// /// An optional template to use when creating the room. - public void CreateRoom(Room room = null) => loungeSubScreen.Open(room ?? new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } }); + public void OpenNewRoom(Room room = null) => loungeSubScreen.Open(room ?? CreateNewRoom()); + + /// + /// Creates a new room. + /// + /// The created . + protected virtual Room CreateNewRoom() => new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } }; private void beginHandlingTrack() { diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs new file mode 100644 index 0000000000..075c552437 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Logging; +using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.Multi.Lounge; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class RealtimeMultiplayer : Multiplayer + { + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + + if (client.Room != null) + client.ChangeState(MultiplayerUserState.Idle); + } + + protected override void UpdatePollingRate(bool isIdle) + { + var timeshiftManager = (RealtimeRoomManager)RoomManager; + + if (!this.IsCurrentScreen()) + { + timeshiftManager.TimeBetweenListingPolls.Value = 0; + timeshiftManager.TimeBetweenSelectionPolls.Value = 0; + } + else + { + switch (CurrentSubScreen) + { + case LoungeSubScreen _: + timeshiftManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; + timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; + break; + + // Don't poll inside the match or anywhere else. + default: + timeshiftManager.TimeBetweenListingPolls.Value = 0; + timeshiftManager.TimeBetweenSelectionPolls.Value = 0; + break; + } + } + + Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})"); + } + + protected override Room CreateNewRoom() + { + var room = base.CreateNewRoom(); + room.Category.Value = RoomCategory.Realtime; + return room; + } + + protected override RoomManager CreateRoomManager() => new RealtimeRoomManager(); + } +} From b9e4a7196e454743a96736c597b6749975e197eb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 23:36:56 +0900 Subject: [PATCH 5520/6909] Add realtime lounge subscreen --- ...s => TestSceneTimeshiftLoungeSubScreen.cs} | 5 ++-- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 25 ++++++++++--------- osu.Game/Screens/Multi/Multiplayer.cs | 4 ++- .../RealtimeFilterControl.cs | 17 +++++++++++++ .../RealtimeLoungeSubScreen.cs | 13 ++++++++++ .../RealtimeMultiplayer.cs | 2 ++ .../Timeshift/TimeshiftLoungeSubScreen.cs | 13 ++++++++++ .../Multi/Timeshift/TimeshiftMultiplayer.cs | 2 ++ 8 files changed, 66 insertions(+), 15 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneLoungeSubScreen.cs => TestSceneTimeshiftLoungeSubScreen.cs} (92%) create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeFilterControl.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs create mode 100644 osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftLoungeSubScreen.cs similarity index 92% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftLoungeSubScreen.cs index 68987127d2..73afd65d6d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftLoungeSubScreen.cs @@ -10,10 +10,11 @@ using osu.Framework.Testing; using osu.Game.Graphics.Containers; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.Multi.Timeshift; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneLoungeSubScreen : RoomManagerTestScene + public class TestSceneTimeshiftLoungeSubScreen : RoomManagerTestScene { private LoungeSubScreen loungeScreen; @@ -26,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - AddStep("push screen", () => LoadScreen(loungeScreen = new LoungeSubScreen + AddStep("push screen", () => LoadScreen(loungeScreen = new TimeshiftLoungeSubScreen { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index 165a2b201c..26e351fc2b 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -19,16 +19,15 @@ using osu.Game.Users; namespace osu.Game.Screens.Multi.Lounge { [Cached] - public class LoungeSubScreen : MultiplayerSubScreen + public abstract class LoungeSubScreen : MultiplayerSubScreen { public override string Title => "Lounge"; - protected FilterControl Filter; - protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); private readonly IBindable initialRoomsReceived = new Bindable(); + private FilterControl filter; private Container content; private LoadingLayer loadingLayer; @@ -78,11 +77,11 @@ namespace osu.Game.Screens.Multi.Lounge }, }, }, - Filter = new TimeshiftFilterControl + filter = CreateFilterControl().With(d => { - RelativeSizeAxes = Axes.X, - Height = 80, - }, + d.RelativeSizeAxes = Axes.X; + d.Height = 80; + }) }; // scroll selected room into view on selection. @@ -108,7 +107,7 @@ namespace osu.Game.Screens.Multi.Lounge content.Padding = new MarginPadding { - Top = Filter.DrawHeight, + Top = filter.DrawHeight, Left = WaveOverlayContainer.WIDTH_PADDING - DrawableRoom.SELECTION_BORDER_WIDTH + HORIZONTAL_OVERFLOW_PADDING, Right = WaveOverlayContainer.WIDTH_PADDING + HORIZONTAL_OVERFLOW_PADDING, }; @@ -116,7 +115,7 @@ namespace osu.Game.Screens.Multi.Lounge protected override void OnFocus(FocusEvent e) { - Filter.TakeFocus(); + filter.TakeFocus(); } public override void OnEntering(IScreen last) @@ -140,19 +139,19 @@ namespace osu.Game.Screens.Multi.Lounge private void onReturning() { - Filter.HoldFocus = true; + filter.HoldFocus = true; } public override bool OnExiting(IScreen next) { - Filter.HoldFocus = false; + filter.HoldFocus = false; return base.OnExiting(next); } public override void OnSuspending(IScreen next) { base.OnSuspending(next); - Filter.HoldFocus = false; + filter.HoldFocus = false; } private void joinRequested(Room room) @@ -195,5 +194,7 @@ namespace osu.Game.Screens.Multi.Lounge this.Push(new MatchSubScreen(room)); } + + protected abstract FilterControl CreateFilterControl(); } } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 027fedaad6..b37ee45b84 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.Multi screenStack.ScreenPushed += screenPushed; screenStack.ScreenExited += screenExited; - screenStack.Push(loungeSubScreen = new LoungeSubScreen()); + screenStack.Push(loungeSubScreen = CreateLounge()); } private readonly IBindable apiState = new Bindable(); @@ -361,6 +361,8 @@ namespace osu.Game.Screens.Multi protected abstract RoomManager CreateRoomManager(); + protected abstract LoungeSubScreen CreateLounge(); + private class MultiplayerWaveContainer : WaveContainer { protected override bool StartHidden => true; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeFilterControl.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeFilterControl.cs new file mode 100644 index 0000000000..acd9a057e3 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeFilterControl.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class RealtimeFilterControl : FilterControl + { + protected override FilterCriteria CreateCriteria() + { + var criteria = base.CreateCriteria(); + criteria.Category = "realtime"; + return criteria; + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs new file mode 100644 index 0000000000..ed187e436f --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Screens.Multi.Lounge; +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class RealtimeLoungeSubScreen : LoungeSubScreen + { + protected override FilterControl CreateFilterControl() => new RealtimeFilterControl(); + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs index 075c552437..6455701d31 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs @@ -61,5 +61,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer } protected override RoomManager CreateRoomManager() => new RealtimeRoomManager(); + + protected override LoungeSubScreen CreateLounge() => new RealtimeLoungeSubScreen(); } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs new file mode 100644 index 0000000000..70fb1aef1d --- /dev/null +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs @@ -0,0 +1,13 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Screens.Multi.Lounge; +using osu.Game.Screens.Multi.Lounge.Components; + +namespace osu.Game.Screens.Multi.Timeshift +{ + public class TimeshiftLoungeSubScreen : LoungeSubScreen + { + protected override FilterControl CreateFilterControl() => new TimeshiftFilterControl(); + } +} diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs index d2d6a35a2e..a38b2a931e 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs @@ -45,5 +45,7 @@ namespace osu.Game.Screens.Multi.Timeshift } protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager(); + + protected override LoungeSubScreen CreateLounge() => new TimeshiftLoungeSubScreen(); } } From a1ba4b6979f0a0029855bfb30b3215499d6905a1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 23:40:19 +0900 Subject: [PATCH 5521/6909] Split MatchSubScreen into abstract component + timeshift implementation --- ....cs => TestSceneTimeshiftRoomSubScreen.cs} | 12 +-- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 4 +- osu.Game/Screens/Multi/Match/RoomSubScreen.cs | 74 ++++++++++++++++ osu.Game/Screens/Multi/Multiplayer.cs | 4 +- .../Multi/Timeshift/TimeshiftMultiplayer.cs | 2 +- .../TimeshiftRoomSubScreen.cs} | 88 +++---------------- 6 files changed, 97 insertions(+), 87 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneMatchSubScreen.cs => TestSceneTimeshiftRoomSubScreen.cs} (94%) create mode 100644 osu.Game/Screens/Multi/Match/RoomSubScreen.cs rename osu.Game/Screens/Multi/{Match/MatchSubScreen.cs => Timeshift/TimeshiftRoomSubScreen.cs} (78%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs similarity index 94% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs index bceb6efac1..f56f78ce89 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs @@ -16,15 +16,15 @@ using osu.Game.Online.Multiplayer; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Match; using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.Multi.Timeshift; using osu.Game.Tests.Beatmaps; using osu.Game.Users; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchSubScreen : MultiplayerTestScene + public class TestSceneTimeshiftRoomSubScreen : MultiplayerTestScene { protected override bool UseOnlineAPI => true; @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager manager; private RulesetStore rulesets; - private TestMatchSubScreen match; + private TestTimeshiftRoomSubScreen match; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public void SetupSteps() { - AddStep("load match", () => LoadScreen(match = new TestMatchSubScreen(Room))); + AddStep("load match", () => LoadScreen(match = new TestTimeshiftRoomSubScreen(Room))); AddUntilStep("wait for load", () => match.IsCurrentScreen()); } @@ -131,13 +131,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize != 1); } - private class TestMatchSubScreen : MatchSubScreen + private class TestTimeshiftRoomSubScreen : TimeshiftRoomSubScreen { public new Bindable SelectedItem => base.SelectedItem; public new Bindable Beatmap => base.Beatmap; - public TestMatchSubScreen(Room room) + public TestTimeshiftRoomSubScreen(Room room) : base(room) { } diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index 26e351fc2b..f4591d089e 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -13,7 +13,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.Match; +using osu.Game.Screens.Multi.Timeshift; using osu.Game.Users; namespace osu.Game.Screens.Multi.Lounge @@ -192,7 +192,7 @@ namespace osu.Game.Screens.Multi.Lounge selectedRoom.Value = room; - this.Push(new MatchSubScreen(room)); + this.Push(new TimeshiftRoomSubScreen(room)); } protected abstract FilterControl CreateFilterControl(); diff --git a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs new file mode 100644 index 0000000000..0cc9a4354e --- /dev/null +++ b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Screens; +using osu.Game.Audio; +using osu.Game.Beatmaps; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.Multi.Match +{ + [Cached(typeof(IPreviewTrackOwner))] + public abstract class RoomSubScreen : MultiplayerSubScreen, IPreviewTrackOwner + { + protected readonly Bindable SelectedItem = new Bindable(); + + public override bool DisallowExternalBeatmapRulesetChanges => true; + + [Resolved(typeof(Room), nameof(Room.Playlist))] + protected BindableList Playlist { get; private set; } + + [Resolved] + private BeatmapManager beatmapManager { get; set; } + + private IBindable> managerUpdated; + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); + SelectedItem.Value = Playlist.FirstOrDefault(); + + managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); + managerUpdated.BindValueChanged(beatmapUpdated); + } + + private void selectedItemChanged() + { + updateWorkingBeatmap(); + + var item = SelectedItem.Value; + + Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty(); + + if (item?.Ruleset != null) + Ruleset.Value = item.Ruleset.Value; + } + + private void beatmapUpdated(ValueChangedEvent> weakSet) => Schedule(updateWorkingBeatmap); + + private void updateWorkingBeatmap() + { + var beatmap = SelectedItem.Value?.Beatmap.Value; + + // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info + var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID); + + Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + } + + public override bool OnExiting(IScreen next) + { + RoomManager?.PartRoom(); + Mods.Value = Array.Empty(); + + return base.OnExiting(next); + } + } +} diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index b37ee45b84..eae779421d 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -308,7 +308,7 @@ namespace osu.Game.Screens.Multi headerBackground.MoveToX(0, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint); break; - case MatchSubScreen _: + case RoomSubScreen _: header.ResizeHeightTo(135, MultiplayerSubScreen.APPEAR_DURATION, Easing.OutQuint); headerBackground.MoveToX(-MultiplayerSubScreen.X_SHIFT, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint); break; @@ -330,7 +330,7 @@ namespace osu.Game.Screens.Multi private void updateTrack(ValueChangedEvent _ = null) { - if (screenStack.CurrentScreen is MatchSubScreen) + if (screenStack.CurrentScreen is RoomSubScreen) { var track = Beatmap.Value?.Track; diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs index a38b2a931e..2ea4857799 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Multi.Timeshift timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; break; - case MatchSubScreen _: + case RoomSubScreen _: timeshiftManager.TimeBetweenListingPolls.Value = 0; timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000; break; diff --git a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs similarity index 78% rename from osu.Game/Screens/Multi/Match/MatchSubScreen.cs rename to osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs index 2f8aad4e65..433a980d60 100644 --- a/osu.Game/Screens/Multi/Match/MatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs @@ -1,7 +1,6 @@ -// 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.Diagnostics; using System.Linq; using osu.Framework.Allocation; @@ -9,28 +8,21 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; 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.Multiplayer; -using osu.Game.Online.Multiplayer.GameTypes; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.Multi.Match; 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 osu.Game.Users; -using Footer = osu.Game.Screens.Multi.Match.Components.Footer; -namespace osu.Game.Screens.Multi.Match +namespace osu.Game.Screens.Multi.Timeshift { - [Cached(typeof(IPreviewTrackOwner))] - public class MatchSubScreen : MultiplayerSubScreen, IPreviewTrackOwner + public class TimeshiftRoomSubScreen : RoomSubScreen { - public override bool DisallowExternalBeatmapRulesetChanges => true; - public override string Title { get; } public override string ShortTitle => "room"; @@ -38,27 +30,15 @@ namespace osu.Game.Screens.Multi.Match [Resolved(typeof(Room), nameof(Room.RoomID))] private Bindable roomId { get; set; } - [Resolved(typeof(Room), nameof(Room.Type))] - private Bindable type { get; set; } - - [Resolved(typeof(Room), nameof(Room.Playlist))] - private BindableList playlist { get; set; } - - [Resolved] - private BeatmapManager beatmapManager { get; set; } - [Resolved(canBeNull: true)] private Multiplayer multiplayer { get; set; } - protected readonly Bindable SelectedItem = new Bindable(); - private MatchSettingsOverlay settingsOverlay; private MatchLeaderboard leaderboard; - private IBindable> managerUpdated; private OverlinedHeader participantsHeader; - public MatchSubScreen(Room room) + public TimeshiftRoomSubScreen(Room room) { Title = room.RoomID.Value == null ? "New room" : room.Name.Value; Activity.Value = new UserActivity.InLobby(room); @@ -96,7 +76,7 @@ namespace osu.Game.Screens.Multi.Match }, Content = new[] { - new Drawable[] { new Components.Header() }, + new Drawable[] { new Match.Components.Header() }, new Drawable[] { participantsHeader = new OverlinedHeader("Participants") @@ -141,7 +121,7 @@ namespace osu.Game.Screens.Multi.Match new DrawableRoomPlaylistWithResults { RelativeSizeAxes = Axes.Both, - Items = { BindTarget = playlist }, + Items = { BindTarget = Playlist }, SelectedItem = { BindTarget = SelectedItem }, RequestShowResults = item => { @@ -195,7 +175,7 @@ namespace osu.Game.Screens.Multi.Match }, new Drawable[] { - new Footer + new Match.Components.Footer { OnStart = onStart, SelectedItem = { BindTarget = SelectedItem } @@ -234,61 +214,17 @@ namespace osu.Game.Screens.Multi.Match // Set the first playlist item. // This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()). - Schedule(() => SelectedItem.Value = playlist.FirstOrDefault()); + Schedule(() => SelectedItem.Value = Playlist.FirstOrDefault()); } }, true); - - SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); - SelectedItem.Value = playlist.FirstOrDefault(); - - managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); - managerUpdated.BindValueChanged(beatmapUpdated); - } - - public override bool OnExiting(IScreen next) - { - RoomManager?.PartRoom(); - Mods.Value = Array.Empty(); - - return base.OnExiting(next); - } - - private void selectedItemChanged() - { - updateWorkingBeatmap(); - - var item = SelectedItem.Value; - - Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty(); - - if (item?.Ruleset != null) - Ruleset.Value = item.Ruleset.Value; - } - - private void beatmapUpdated(ValueChangedEvent> weakSet) => Schedule(updateWorkingBeatmap); - - private void updateWorkingBeatmap() - { - var beatmap = SelectedItem.Value?.Beatmap.Value; - - // Retrieve the corresponding local beatmap, since we can't directly use the playlist's beatmap info - var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID); - - Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } private void onStart() { - switch (type.Value) + multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value) { - default: - case GameTypeTimeshift _: - multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value) - { - Exited = () => leaderboard.RefreshScores() - })); - break; - } + Exited = () => leaderboard.RefreshScores() + })); } } } From a25cd910f83fe6ed012fdf63e1f2a20245ac625d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 22:20:35 +0100 Subject: [PATCH 5522/6909] Prepare base DHO for HO application --- .../Objects/Drawables/DrawableTaikoHitObject.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs index cf3aa69b6f..6041eccb51 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoHitObject.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; -using osu.Framework.Allocation; +using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Primitives; @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly Container nonProxiedContent; - protected DrawableTaikoHitObject(TaikoHitObject hitObject) + protected DrawableTaikoHitObject([CanBeNull] TaikoHitObject hitObject) : base(hitObject) { AddRangeInternal(new[] @@ -113,25 +113,23 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public override Vector2 OriginPosition => new Vector2(DrawHeight / 2); - public new TObject HitObject; + public new TObject HitObject => (TObject)base.HitObject; protected Vector2 BaseSize; protected SkinnableDrawable MainPiece; - protected DrawableTaikoHitObject(TObject hitObject) + protected DrawableTaikoHitObject([CanBeNull] TObject hitObject) : base(hitObject) { - HitObject = hitObject; - Anchor = Anchor.CentreLeft; Origin = Anchor.Custom; RelativeSizeAxes = Axes.Both; } - [BackgroundDependencyLoader] - private void load() + protected override void OnApply() { + base.OnApply(); RecreatePieces(); } From 7b350fc8e5f61644d63700c236fe2c447eb39efd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 22:27:45 +0100 Subject: [PATCH 5523/6909] Prepare strongable DHO for HO application --- .../DrawableTaikoStrongableHitObject.cs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs index af3e94d9c6..62f338ca91 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using osu.Framework.Allocation; +using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; using osu.Game.Audio; @@ -16,28 +16,38 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables where TObject : TaikoStrongableHitObject where TStrongNestedObject : StrongNestedHitObject { - private readonly Bindable isStrong; + private readonly Bindable isStrong = new BindableBool(); private readonly Container strongHitContainer; - protected DrawableTaikoStrongableHitObject(TObject hitObject) + protected DrawableTaikoStrongableHitObject([CanBeNull] TObject hitObject) : base(hitObject) { - isStrong = HitObject.IsStrongBindable.GetBoundCopy(); - AddInternal(strongHitContainer = new Container()); } - [BackgroundDependencyLoader] - private void load() + protected override void OnApply() { + isStrong.BindTo(HitObject.IsStrongBindable); isStrong.BindValueChanged(_ => { - // will overwrite samples, should only be called on change. + // will overwrite samples, should only be called on subsequent changes + // after the initial application. updateSamplesFromStrong(); RecreatePieces(); }); + + base.OnApply(); + } + + protected override void OnFree() + { + base.OnFree(); + + isStrong.UnbindFrom(HitObject.IsStrongBindable); + // ensure the next application does not accidentally overwrite samples. + isStrong.UnbindEvents(); } private HitSampleInfo[] getStrongSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_FINISH).ToArray(); From a31e8d137f3c0bf2926e603a9ef294eb0f8f47c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 22:53:00 +0100 Subject: [PATCH 5524/6909] Add guard when clearing samples --- osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs index 644c67ea59..da6da0ea97 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -303,7 +303,8 @@ namespace osu.Game.Rulesets.Objects.Drawables samplesBindable.CollectionChanged -= onSamplesChanged; // Release the samples for other hitobjects to use. - Samples.Samples = null; + if (Samples != null) + Samples.Samples = null; if (nestedHitObjects.IsValueCreated) { From 55cdff5be770414190ed13b7d0effea094c848bb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Sun, 20 Dec 2020 23:49:06 +0900 Subject: [PATCH 5525/6909] Renamespace ready button --- .../Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs | 2 +- .../RealtimeMultiplayer/{ => Match}/RealtimeReadyButton.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game/Screens/Multi/RealtimeMultiplayer/{ => Match}/RealtimeReadyButton.cs (98%) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs index 1f863028af..b7cd81fb32 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs @@ -11,7 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Rulesets; -using osu.Game.Screens.Multi.RealtimeMultiplayer; +using osu.Game.Screens.Multi.RealtimeMultiplayer.Match; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs similarity index 98% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs rename to osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs index ea8fb04994..09487e9831 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeReadyButton.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs @@ -15,7 +15,7 @@ using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Multi.Components; using osuTK; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match { public class RealtimeReadyButton : RealtimeRoomComposite { From 232c0205b4d7927c3b84dac00a719ec2fa4be7ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 22:51:35 +0100 Subject: [PATCH 5526/6909] Refactor hit object application scene to work reliably --- .../HitObjectApplicationTestScene.cs | 14 ++++++++++---- .../TestSceneBarLineApplication.cs | 7 ++++--- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs index 07c7b4d1db..a1d000386f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs +++ b/osu.Game.Rulesets.Taiko.Tests/HitObjectApplicationTestScene.cs @@ -25,16 +25,22 @@ namespace osu.Game.Rulesets.Taiko.Tests private ScrollingHitObjectContainer hitObjectContainer; - [SetUpSteps] - public void SetUp() - => AddStep("create SHOC", () => Child = hitObjectContainer = new ScrollingHitObjectContainer + [BackgroundDependencyLoader] + private void load() + { + Child = hitObjectContainer = new ScrollingHitObjectContainer { RelativeSizeAxes = Axes.X, Height = 200, Anchor = Anchor.Centre, Origin = Anchor.Centre, Clock = new FramedClock(new StopwatchClock()) - }); + }; + } + + [SetUpSteps] + public void SetUp() + => AddStep("clear SHOC", () => hitObjectContainer.Clear(false)); protected void AddHitObject(DrawableHitObject hitObject) => AddStep("add to SHOC", () => hitObjectContainer.Add(hitObject)); diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs index 65230a07bc..a970965141 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneBarLineApplication.cs @@ -12,12 +12,13 @@ namespace osu.Game.Rulesets.Taiko.Tests [Test] public void TestApplyNewBarLine() { - DrawableBarLine barLine = new DrawableBarLine(PrepareObject(new BarLine + DrawableBarLine barLine = new DrawableBarLine(); + + AddStep("apply new bar line", () => barLine.Apply(PrepareObject(new BarLine { StartTime = 400, Major = true - })); - + }), null)); AddHitObject(barLine); RemoveHitObject(barLine); From 536df074a9a5505a9b14dddd29f616d42e243c93 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:02:49 +0900 Subject: [PATCH 5527/6909] Don't attempt to re-map existing beatmap/ruleset (for testing) --- osu.Game/Online/Multiplayer/PlaylistItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/PlaylistItem.cs b/osu.Game/Online/Multiplayer/PlaylistItem.cs index 416091a1aa..4c4c071fc9 100644 --- a/osu.Game/Online/Multiplayer/PlaylistItem.cs +++ b/osu.Game/Online/Multiplayer/PlaylistItem.cs @@ -64,8 +64,8 @@ namespace osu.Game.Online.Multiplayer public void MapObjects(BeatmapManager beatmaps, RulesetStore rulesets) { - Beatmap.Value = apiBeatmap.ToBeatmap(rulesets); - Ruleset.Value = rulesets.GetRuleset(RulesetID); + Beatmap.Value ??= apiBeatmap.ToBeatmap(rulesets); + Ruleset.Value ??= rulesets.GetRuleset(RulesetID); Ruleset rulesetInstance = Ruleset.Value.CreateInstance(); From 1fdc19ee0fe9855a44eb95e94a8555be2103b0b7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:04:06 +0900 Subject: [PATCH 5528/6909] Add realtime match subscreen and related components --- .../TestSceneRealtimeMatchSubScreen.cs | 77 +++ .../Match/BeatmapSelectionControl.cs | 81 ++++ .../Match/RealtimeMatchFooter.cs | 48 ++ .../Match/RealtimeMatchHeader.cs | 106 +++++ .../Match/RealtimeMatchSettingsOverlay.cs | 441 ++++++++++++++++++ .../Participants/ParticipantsListHeader.cs | 31 ++ .../RealtimeMatchSongSelect.cs | 20 + .../RealtimeMatchSubScreen.cs | 201 ++++++++ .../RealtimeMultiplayer/RealtimePlayer.cs | 16 + 9 files changed, 1021 insertions(+) create mode 100644 osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Match/BeatmapSelectionControl.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchFooter.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchHeader.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsListHeader.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs new file mode 100644 index 0000000000..d31364d62e --- /dev/null +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs @@ -0,0 +1,77 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Screens; +using osu.Framework.Testing; +using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets.Osu; +using osu.Game.Screens.Multi.RealtimeMultiplayer; +using osu.Game.Screens.Multi.RealtimeMultiplayer.Match; +using osu.Game.Tests.Beatmaps; +using osuTK.Input; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestSceneRealtimeMatchSubScreen : RealtimeMultiplayerTestScene + { + private RealtimeMatchSubScreen screen; + + public TestSceneRealtimeMatchSubScreen() + : base(false) + { + } + + [SetUp] + public new void Setup() => Schedule(() => + { + Room.Name.Value = "Test Room"; + }); + + [SetUpSteps] + public void SetupSteps() + { + AddStep("load match", () => LoadScreen(screen = new RealtimeMatchSubScreen(Room))); + AddUntilStep("wait for load", () => screen.IsCurrentScreen()); + } + + [Test] + public void TestSettingValidity() + { + AddAssert("create button not enabled", () => !this.ChildrenOfType().Single().Enabled.Value); + + AddStep("set playlist", () => + { + Room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + }); + }); + + AddAssert("create button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + } + + [Test] + public void TestCreatedRoom() + { + AddStep("set playlist", () => + { + Room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Ruleset = { Value = new OsuRuleset().RulesetInfo }, + }); + }); + + AddStep("click create button", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddWaitStep("wait", 500); + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/BeatmapSelectionControl.cs new file mode 100644 index 0000000000..1939744916 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/BeatmapSelectionControl.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.Specialized; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Framework.Screens; +using osu.Game.Online.API; +using osu.Game.Screens.Multi.Match.Components; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match +{ + public class BeatmapSelectionControl : MultiplayerComposite + { + [Resolved] + private RealtimeMatchSubScreen matchSubScreen { get; set; } + + [Resolved] + private IAPIProvider api { get; set; } + + private Container beatmapPanelContainer; + private Button selectButton; + + public BeatmapSelectionControl() + { + AutoSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + beatmapPanelContainer = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }, + selectButton = new PurpleTriangleButton + { + RelativeSizeAxes = Axes.X, + Height = 40, + Text = "Select beatmap", + Action = () => matchSubScreen.Push(new RealtimeMatchSongSelect()), + Alpha = 0 + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playlist.BindCollectionChanged(onPlaylistChanged, true); + Host.BindValueChanged(host => + { + if (RoomID.Value == null || host.NewValue?.Equals(api.LocalUser.Value) == true) + selectButton.Show(); + else + selectButton.Hide(); + }, true); + } + + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) + { + if (Playlist.Any()) + beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(Playlist.Single(), false, false); + else + beatmapPanelContainer.Clear(); + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchFooter.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchFooter.cs new file mode 100644 index 0000000000..31871729f6 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchFooter.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Bindables; +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.Online.Multiplayer; +using osuTK; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match +{ + public class RealtimeMatchFooter : CompositeDrawable + { + public const float HEIGHT = 50; + + public readonly Bindable SelectedItem = new Bindable(); + + private readonly Drawable background; + + public RealtimeMatchFooter() + { + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + + InternalChildren = new[] + { + background = new Box { RelativeSizeAxes = Axes.Both }, + new RealtimeReadyButton + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(600, 50), + SelectedItem = { BindTarget = SelectedItem } + } + }; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + background.Colour = Color4Extensions.FromHex(@"28242d"); + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchHeader.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchHeader.cs new file mode 100644 index 0000000000..a9a10d1510 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchHeader.cs @@ -0,0 +1,106 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.API; +using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Users.Drawables; +using osuTK; +using FontWeight = osu.Game.Graphics.FontWeight; +using OsuColour = osu.Game.Graphics.OsuColour; +using OsuFont = osu.Game.Graphics.OsuFont; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match +{ + public class RealtimeMatchHeader : MultiplayerComposite + { + public const float HEIGHT = 50; + + public Action OpenSettings; + + private UpdateableAvatar avatar; + private LinkFlowContainer hostText; + private Button openSettingsButton; + + [Resolved] + private IAPIProvider api { get; set; } + + public RealtimeMatchHeader() + { + RelativeSizeAxes = Axes.X; + Height = HEIGHT; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + InternalChildren = new Drawable[] + { + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + avatar = new UpdateableAvatar + { + Size = new Vector2(50), + Masking = true, + CornerRadius = 10, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(size: 30), + Current = { BindTarget = RoomName } + }, + hostText = new LinkFlowContainer(s => s.Font = OsuFont.GetFont(size: 20)) + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + } + } + } + } + }, + openSettingsButton = new PurpleTriangleButton + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Size = new Vector2(150, HEIGHT), + Text = "Open settings", + Action = () => OpenSettings?.Invoke(), + Alpha = 0 + } + }; + + Host.BindValueChanged(host => + { + avatar.User = host.NewValue; + + hostText.Clear(); + + if (host.NewValue != null) + { + hostText.AddText("hosted by "); + hostText.AddUserLink(host.NewValue, s => s.Font = s.Font.With(weight: FontWeight.SemiBold)); + } + + openSettingsButton.Alpha = host.NewValue?.Equals(api.LocalUser.Value) == true ? 1 : 0; + }, true); + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs new file mode 100644 index 0000000000..3b9603e1bd --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs @@ -0,0 +1,441 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Overlays; +using osu.Game.Rulesets; +using osu.Game.Screens.Multi.Match.Components; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match +{ + public class RealtimeMatchSettingsOverlay : FocusedOverlayContainer + { + private const float transition_duration = 350; + private const float field_padding = 45; + + protected MatchSettings Settings { get; private set; } + + [BackgroundDependencyLoader] + private void load() + { + Masking = true; + + Child = Settings = new MatchSettings + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Y, + SettingsApplied = Hide + }; + } + + protected override void PopIn() + { + Settings.MoveToY(0, transition_duration, Easing.OutQuint); + } + + protected override void PopOut() + { + Settings.MoveToY(-1, transition_duration, Easing.InSine); + } + + protected class MatchSettings : MultiplayerComposite + { + private const float disabled_alpha = 0.2f; + + public Action SettingsApplied; + + public OsuTextBox NameField, MaxParticipantsField; + public RoomAvailabilityPicker AvailabilityPicker; + public GameTypePicker TypePicker; + public TriangleButton ApplyButton; + + public OsuSpriteText ErrorText; + + private OsuSpriteText typeLabel; + private LoadingLayer loadingLayer; + private BeatmapSelectionControl initialBeatmapControl; + + [Resolved] + private RealtimeRoomManager manager { get; set; } + + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + [Resolved] + private Bindable currentRoom { get; set; } + + [Resolved] + private Bindable beatmap { get; set; } + + [Resolved] + private Bindable ruleset { get; set; } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Container dimContent; + + InternalChildren = new Drawable[] + { + dimContent = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d"), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Distributed), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new OsuScrollContainer + { + Padding = new MarginPadding + { + Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING, + Vertical = 10 + }, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] + { + new Container + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new SectionContainer + { + Padding = new MarginPadding { Right = field_padding / 2 }, + Children = new[] + { + new Section("Room name") + { + Child = NameField = new SettingsTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + }, + }, + new Section("Room visibility") + { + Alpha = disabled_alpha, + Child = AvailabilityPicker = new RoomAvailabilityPicker + { + Enabled = { Value = false } + }, + }, + new Section("Game type") + { + Alpha = disabled_alpha, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(7), + Children = new Drawable[] + { + TypePicker = new GameTypePicker + { + RelativeSizeAxes = Axes.X, + Enabled = { Value = false } + }, + typeLabel = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14), + Colour = colours.Yellow + }, + }, + }, + }, + }, + }, + new SectionContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Padding = new MarginPadding { Left = field_padding / 2 }, + Children = new[] + { + new Section("Max participants") + { + Alpha = disabled_alpha, + Child = MaxParticipantsField = new SettingsNumberTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + ReadOnly = true, + }, + }, + new Section("Password (optional)") + { + Alpha = disabled_alpha, + Child = new SettingsPasswordTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + ReadOnly = true, + }, + }, + } + } + }, + }, + initialBeatmapControl = new BeatmapSelectionControl + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Width = 0.5f + } + } + } + }, + }, + }, + new Drawable[] + { + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Y = 2, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding { Vertical = 20 }, + Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, + Children = new Drawable[] + { + ApplyButton = new CreateOrUpdateButton + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(230, 55), + Enabled = { Value = false }, + Action = apply, + }, + ErrorText = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Alpha = 0, + Depth = 1, + Colour = colours.RedDark + } + } + } + } + } + } + } + }, + } + }, + loadingLayer = new LoadingLayer(dimContent) + }; + + TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true); + RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true); + Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true); + Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); + MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); + RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true); + } + + protected override void Update() + { + base.Update(); + + ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0; + } + + private void apply() + { + if (!ApplyButton.Enabled.Value) + return; + + hideError(); + loadingLayer.Show(); + + // If the client is already in a room, update via the client. + // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation. + if (client.Room != null) + { + client.ChangeSettings(name: NameField.Text); + onSuccess(currentRoom.Value); + } + else + { + currentRoom.Value.Name.Value = NameField.Text; + currentRoom.Value.Availability.Value = AvailabilityPicker.Current.Value; + currentRoom.Value.Type.Value = TypePicker.Current.Value; + + if (int.TryParse(MaxParticipantsField.Text, out int max)) + currentRoom.Value.MaxParticipants.Value = max; + else + currentRoom.Value.MaxParticipants.Value = null; + + manager?.CreateRoom(currentRoom.Value, onSuccess, onError); + } + } + + private void hideError() => ErrorText.FadeOut(50); + + private void onSuccess(Room room) + { + loadingLayer.Hide(); + SettingsApplied?.Invoke(); + } + + private void onError(string text) + { + ErrorText.Text = text; + ErrorText.FadeIn(50); + + loadingLayer.Hide(); + } + } + + private class SettingsTextBox : OsuTextBox + { + [BackgroundDependencyLoader] + private void load() + { + BackgroundUnfocused = Color4.Black; + BackgroundFocused = Color4.Black; + } + } + + private class SettingsNumberTextBox : SettingsTextBox + { + protected override bool CanAddCharacter(char character) => char.IsNumber(character); + } + + private class SettingsPasswordTextBox : OsuPasswordTextBox + { + [BackgroundDependencyLoader] + private void load() + { + BackgroundUnfocused = Color4.Black; + BackgroundFocused = Color4.Black; + } + } + + private class SectionContainer : FillFlowContainer
    + { + public SectionContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Width = 0.5f; + Direction = FillDirection.Vertical; + Spacing = new Vector2(field_padding); + } + } + + private class Section : Container + { + private readonly Container content; + + protected override Container Content => content; + + public Section(string title) + { + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), + Text = title.ToUpper(), + }, + content = new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }, + }, + }; + } + } + + public class CreateOrUpdateButton : TriangleButton + { + [Resolved(typeof(Room), nameof(Room.RoomID))] + private Bindable roomId { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + roomId.BindValueChanged(id => Text = id.NewValue == null ? "Create" : "Update", true); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Yellow; + Triangles.ColourLight = colours.YellowLight; + Triangles.ColourDark = colours.YellowDark; + } + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsListHeader.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsListHeader.cs new file mode 100644 index 0000000000..0ca7d34005 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsListHeader.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Components; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class ParticipantsListHeader : OverlinedHeader + { + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + public ParticipantsListHeader() + : base("Participants") + { + } + + protected override void Update() + { + base.Update(); + + var room = client.Room; + if (room == null) + return; + + Details.Value = room.Users.Count.ToString(); + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs new file mode 100644 index 0000000000..9bcc303065 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.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.Screens.Select; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class RealtimeMatchSongSelect : SongSelect + { + protected override BeatmapDetailArea CreateBeatmapDetailArea() + { + throw new System.NotImplementedException(); + } + + protected override bool OnStart() + { + throw new System.NotImplementedException(); + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs new file mode 100644 index 0000000000..d8b42e065b --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs @@ -0,0 +1,201 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Specialized; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.Multi.Match; +using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.Multi.RealtimeMultiplayer.Match; +using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; +using osu.Game.Screens.Play; +using osu.Game.Users; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + [Cached] + public class RealtimeMatchSubScreen : RoomSubScreen + { + public override string Title { get; } + + public override string ShortTitle => "match"; + + [Resolved(canBeNull: true)] + private Multiplayer multiplayer { get; set; } + + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + private RealtimeMatchSettingsOverlay settingsOverlay; + + public RealtimeMatchSubScreen(Room room) + { + Title = room.RoomID.Value == null ? "New match" : room.Name.Value; + Activity.Value = new UserActivity.InLobby(room); + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChildren = new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding + { + Horizontal = 105, + Vertical = 20 + }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(), + }, + Content = new[] + { + new Drawable[] + { + new RealtimeMatchHeader + { + OpenSettings = () => settingsOverlay.Show() + } + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + Content = new[] + { + new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, + Child = new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] { new ParticipantsListHeader() }, + new Drawable[] + { + new Participants.ParticipantsList + { + RelativeSizeAxes = Axes.Both + }, + } + } + } + }, + new FillFlowContainer + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding { Horizontal = 5 }, + Children = new Drawable[] + { + new OverlinedHeader("Beatmap"), + new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } + } + } + } + } + } + }, + new Drawable[] + { + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize) + }, + Content = new[] + { + new Drawable[] { new OverlinedHeader("Chat") }, + new Drawable[] { new MatchChatDisplay { RelativeSizeAxes = Axes.Both } } + } + } + } + }, + } + } + }, + new Drawable[] + { + new Footer { SelectedItem = { BindTarget = SelectedItem } } + } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + } + }, + settingsOverlay = new RealtimeMatchSettingsOverlay + { + RelativeSizeAxes = Axes.Both, + State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Playlist.BindCollectionChanged(onPlaylistChanged, true); + + client.LoadRequested += onLoadRequested; + } + + public override bool OnBackButton() + { + if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible) + { + settingsOverlay.Hide(); + return true; + } + + return base.OnBackButton(); + } + + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); + + private void onLoadRequested() => multiplayer?.Push(new PlayerLoader(() => new RealtimePlayer(SelectedItem.Value))); + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client != null) + client.LoadRequested -= onLoadRequested; + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs new file mode 100644 index 0000000000..d629027246 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.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.Online.Multiplayer; +using osu.Game.Screens.Multi.Play; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class RealtimePlayer : TimeshiftPlayer + { + public RealtimePlayer(PlaylistItem playlistItem) + : base(playlistItem) + { + } + } +} From 945ba59c8e3cb9c6df69fdba8ced4fc8322f9085 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:06:44 +0900 Subject: [PATCH 5529/6909] Make timeshift player able to not allow pause --- osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 41dcf61740..f86d8ff267 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -35,7 +35,8 @@ namespace osu.Game.Screens.Multi.Play [Resolved] private IBindable ruleset { get; set; } - public TimeshiftPlayer(PlaylistItem playlistItem) + public TimeshiftPlayer(PlaylistItem playlistItem, bool allowPause = true) + : base(allowPause) { this.playlistItem = playlistItem; } From 07077b8f4e52e2aa3902d73a4f936fc2086e7e84 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:13:05 +0900 Subject: [PATCH 5530/6909] Add realtime player --- .../Screens/Multi/Play/TimeshiftPlayer.cs | 32 ++++----- .../RealtimeMultiplayer/RealtimePlayer.cs | 67 ++++++++++++++++++- .../RealtimeResultsScreen.cs | 17 +++++ 3 files changed, 99 insertions(+), 17 deletions(-) create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index f86d8ff267..e8462088f1 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -25,9 +25,11 @@ namespace osu.Game.Screens.Multi.Play public Action Exited; [Resolved(typeof(Room), nameof(Room.RoomID))] - private Bindable roomId { get; set; } + protected Bindable RoomId { get; private set; } - private readonly PlaylistItem playlistItem; + protected readonly PlaylistItem PlaylistItem; + + protected int? Token { get; private set; } [Resolved] private IAPIProvider api { get; set; } @@ -38,30 +40,28 @@ namespace osu.Game.Screens.Multi.Play public TimeshiftPlayer(PlaylistItem playlistItem, bool allowPause = true) : base(allowPause) { - this.playlistItem = playlistItem; + PlaylistItem = playlistItem; } - private int? token; - [BackgroundDependencyLoader] private void load() { - token = null; + Token = null; bool failed = false; // Sanity checks to ensure that TimeshiftPlayer matches the settings for the current PlaylistItem - if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != playlistItem.Beatmap.Value.OnlineBeatmapID) + if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != PlaylistItem.Beatmap.Value.OnlineBeatmapID) throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap"); - if (ruleset.Value.ID != playlistItem.Ruleset.Value.ID) + if (ruleset.Value.ID != PlaylistItem.Ruleset.Value.ID) throw new InvalidOperationException("Current Ruleset does not match PlaylistItem's Ruleset"); - if (!playlistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals))) + if (!PlaylistItem.RequiredMods.All(m => Mods.Value.Any(m.Equals))) throw new InvalidOperationException("Current Mods do not match PlaylistItem's RequiredMods"); - var req = new CreateRoomScoreRequest(roomId.Value ?? 0, playlistItem.ID, Game.VersionHash); - req.Success += r => token = r.ID; + var req = new CreateRoomScoreRequest(RoomId.Value ?? 0, PlaylistItem.ID, Game.VersionHash); + req.Success += r => Token = r.ID; req.Failure += e => { failed = true; @@ -77,7 +77,7 @@ namespace osu.Game.Screens.Multi.Play api.Queue(req); - while (!failed && !token.HasValue) + while (!failed && !Token.HasValue) Thread.Sleep(1000); } @@ -93,8 +93,8 @@ namespace osu.Game.Screens.Multi.Play protected override ResultsScreen CreateResults(ScoreInfo score) { - Debug.Assert(roomId.Value != null); - return new TimeshiftResultsScreen(score, roomId.Value.Value, playlistItem, true); + Debug.Assert(RoomId.Value != null); + return new TimeshiftResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); } protected override Score CreateScore() @@ -108,10 +108,10 @@ namespace osu.Game.Screens.Multi.Play { await base.SubmitScore(score); - Debug.Assert(token != null); + Debug.Assert(Token != null); var tcs = new TaskCompletionSource(); - var request = new SubmitRoomScoreRequest(token.Value, roomId.Value ?? 0, playlistItem.ID, score.ScoreInfo); + var request = new SubmitRoomScoreRequest(Token.Value, RoomId.Value ?? 0, PlaylistItem.ID, score.ScoreInfo); request.Success += s => { diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index d629027246..d654496f40 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -1,16 +1,81 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Diagnostics; +using System.Threading; +using System.Threading.Tasks; +using osu.Framework.Allocation; using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Scoring; using osu.Game.Screens.Multi.Play; +using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Multi.RealtimeMultiplayer { public class RealtimePlayer : TimeshiftPlayer { + protected override bool PauseOnFocusLost => false; + + // Disallow fails in multiplayer for now. + protected override bool CheckModsAllowFailure() => false; + + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); + private bool started; + public RealtimePlayer(PlaylistItem playlistItem) - : base(playlistItem) + : base(playlistItem, false) { } + + [BackgroundDependencyLoader] + private void load() + { + if (Token == null) + return; // Todo: Somehow handle token retrieval failure. + + client.MatchStarted += onMatchStarted; + client.ResultsReady += onResultsReady; + client.ChangeState(MultiplayerUserState.Loaded); + + while (!started) + Thread.Sleep(100); + } + + private void onMatchStarted() => started = true; + + private void onResultsReady() => resultsReady.SetResult(true); + + protected override async Task SubmitScore(Score score) + { + await base.SubmitScore(score); + + await client.ChangeState(MultiplayerUserState.FinishedPlay); + + // Await up to 30 seconds for results to become available (3 api request timeouts). + // This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur. + await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(30))); + } + + protected override ResultsScreen CreateResults(ScoreInfo score) + { + Debug.Assert(RoomId.Value != null); + return new RealtimeResultsScreen(score, RoomId.Value.Value, PlaylistItem); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (client != null) + { + client.MatchStarted -= onMatchStarted; + client.ResultsReady -= onResultsReady; + } + } } } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs new file mode 100644 index 0000000000..1598c8fbb5 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs @@ -0,0 +1,17 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Online.Multiplayer; +using osu.Game.Scoring; +using osu.Game.Screens.Multi.Ranking; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class RealtimeResultsScreen : TimeshiftResultsScreen + { + public RealtimeResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem) + : base(score, roomId, playlistItem, false) + { + } + } +} From 5b4197a9efb7194e955d2d6eb891f8ec67638f91 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:14:54 +0900 Subject: [PATCH 5531/6909] Disable watching replays from realtime results screen --- .../Multi/Ranking/TimeshiftResultsScreen.cs | 4 ++-- .../RealtimeResultsScreen.cs | 2 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 23 +++++++++++-------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index 3623208fa7..d3f1c19c7c 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -32,8 +32,8 @@ namespace osu.Game.Screens.Multi.Ranking [Resolved] private IAPIProvider api { get; set; } - public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry) - : base(score, allowRetry) + public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true) + : base(score, allowRetry, allowWatchingReplay) { this.roomId = roomId; this.playlistItem = playlistItem; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs index 1598c8fbb5..3964a87eb6 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs @@ -10,7 +10,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public class RealtimeResultsScreen : TimeshiftResultsScreen { public RealtimeResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem) - : base(score, roomId, playlistItem, false) + : base(score, roomId, playlistItem, false, false) { } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 887e7ec8a9..528a1842af 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -57,11 +57,13 @@ namespace osu.Game.Screens.Ranking private APIRequest nextPageRequest; private readonly bool allowRetry; + private readonly bool allowWatchingReplay; - protected ResultsScreen(ScoreInfo score, bool allowRetry) + protected ResultsScreen(ScoreInfo score, bool allowRetry, bool allowWatchingReplay = true) { Score = score; this.allowRetry = allowRetry; + this.allowWatchingReplay = allowWatchingReplay; SelectedScore.Value = score; } @@ -128,15 +130,7 @@ namespace osu.Game.Screens.Ranking Origin = Anchor.Centre, AutoSizeAxes = Axes.Both, Spacing = new Vector2(5), - Direction = FillDirection.Horizontal, - Children = new Drawable[] - { - new ReplayDownloadButton(null) - { - Score = { BindTarget = SelectedScore }, - Width = 300 - }, - } + Direction = FillDirection.Horizontal } } } @@ -157,6 +151,15 @@ namespace osu.Game.Screens.Ranking ScorePanelList.AddScore(Score, shouldFlair); } + if (allowWatchingReplay) + { + buttons.Add(new ReplayDownloadButton(null) + { + Score = { BindTarget = SelectedScore }, + Width = 300 + }); + } + if (player != null && allowRetry) { buttons.Add(new RetryButton { Width = 300 }); From 15480c006b6353620b64dce0affcb935d935e3a7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:21:30 +0900 Subject: [PATCH 5532/6909] Create the correct room subscreen --- osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs | 6 ++++-- .../Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs | 4 ++++ .../Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index f4591d089e..44c893363b 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -13,7 +13,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.Timeshift; +using osu.Game.Screens.Multi.Match; using osu.Game.Users; namespace osu.Game.Screens.Multi.Lounge @@ -192,9 +192,11 @@ namespace osu.Game.Screens.Multi.Lounge selectedRoom.Value = room; - this.Push(new TimeshiftRoomSubScreen(room)); + this.Push(CreateRoomSubScreen(room)); } protected abstract FilterControl CreateFilterControl(); + + protected abstract RoomSubScreen CreateRoomSubScreen(Room room); } } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs index ed187e436f..9fbf0c4654 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs @@ -1,13 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.Multi.Match; namespace osu.Game.Screens.Multi.RealtimeMultiplayer { public class RealtimeLoungeSubScreen : LoungeSubScreen { protected override FilterControl CreateFilterControl() => new RealtimeFilterControl(); + + protected override RoomSubScreen CreateRoomSubScreen(Room room) => new RealtimeMatchSubScreen(room); } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs index 70fb1aef1d..8e426ffbcc 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs @@ -1,13 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.Multi.Match; namespace osu.Game.Screens.Multi.Timeshift { public class TimeshiftLoungeSubScreen : LoungeSubScreen { protected override FilterControl CreateFilterControl() => new TimeshiftFilterControl(); + + protected override RoomSubScreen CreateRoomSubScreen(Room room) => new TimeshiftRoomSubScreen(room); } } From 959959dbedf50b76d470d5f0457c260988e7c15b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:21:41 +0900 Subject: [PATCH 5533/6909] Add multiplayer client to OsuGameBase --- osu.Game/OsuGameBase.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 150569f1dd..eb27821d82 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -30,6 +30,7 @@ using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; +using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Resources; @@ -78,6 +79,7 @@ namespace osu.Game protected IAPIProvider API; private SpectatorStreamingClient spectatorStreaming; + private StatefulMultiplayerClient multiplayerClient; protected MenuCursorContainer MenuCursorContainer; @@ -211,6 +213,7 @@ namespace osu.Game dependencies.CacheAs(API ??= new APIAccess(LocalConfig)); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); + dependencies.CacheAs(multiplayerClient = new RealtimeMultiplayerClient()); var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); @@ -277,6 +280,7 @@ namespace osu.Game if (API is APIAccess apiAccess) AddInternal(apiAccess); AddInternal(spectatorStreaming); + AddInternal(multiplayerClient); AddInternal(RulesetConfigCache); From 275efd12b84bf8d775313bcf93d75840c48ad370 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:21:48 +0900 Subject: [PATCH 5534/6909] Fix room manager reference --- .../RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs index 3b9603e1bd..924c736472 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match private BeatmapSelectionControl initialBeatmapControl; [Resolved] - private RealtimeRoomManager manager { get; set; } + private IRoomManager manager { get; set; } [Resolved] private StatefulMultiplayerClient client { get; set; } From e32b1c34cac0436c39a5087896f386af30239d06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 22:53:46 +0100 Subject: [PATCH 5535/6909] Implement hit application --- .../TestSceneHitApplication.cs | 37 ++++++++++++++++ .../Objects/Drawables/DrawableHit.cs | 44 ++++++++++++++----- 2 files changed, 70 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs new file mode 100644 index 0000000000..52fd440857 --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneHitApplication.cs @@ -0,0 +1,37 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneHitApplication : HitObjectApplicationTestScene + { + [Test] + public void TestApplyNewHit() + { + var hit = new DrawableHit(); + + AddStep("apply new hit", () => hit.Apply(PrepareObject(new Hit + { + Type = HitType.Rim, + IsStrong = false, + StartTime = 300 + }), null)); + + AddHitObject(hit); + RemoveHitObject(hit); + + AddStep("apply new hit", () => hit.Apply(PrepareObject(new Hit + { + Type = HitType.Centre, + IsStrong = true, + StartTime = 500 + }), null)); + + AddHitObject(hit); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 431f2980ec..5a479e1f53 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using osu.Framework.Allocation; +using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Audio; @@ -36,29 +36,51 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private bool pressHandledThisFrame; - private readonly Bindable type; + private readonly Bindable type = new Bindable(); - public DrawableHit(Hit hit) - : base(hit) + public DrawableHit() + : this(null) { - type = HitObject.TypeBindable.GetBoundCopy(); - FillMode = FillMode.Fit; - - updateActionsFromType(); } - [BackgroundDependencyLoader] - private void load() + public DrawableHit([CanBeNull] Hit hit) + : base(hit) { + FillMode = FillMode.Fit; + } + + protected override void OnApply() + { + type.BindTo(HitObject.TypeBindable); type.BindValueChanged(_ => { updateActionsFromType(); - // will overwrite samples, should only be called on change. + // will overwrite samples, should only be called on subsequent changes + // after the initial application. updateSamplesFromTypeChange(); RecreatePieces(); }); + + // action update also has to happen immediately on application. + updateActionsFromType(); + + base.OnApply(); + } + + protected override void OnFree() + { + base.OnFree(); + + type.UnbindFrom(HitObject.TypeBindable); + type.UnbindEvents(); + + UnproxyContent(); + + HitActions = null; + HitAction = null; + validActionPressed = pressHandledThisFrame = false; } private HitSampleInfo[] getRimSamples() => HitObject.Samples.Where(s => s.Name == HitSampleInfo.HIT_CLAP || s.Name == HitSampleInfo.HIT_WHISTLE).ToArray(); From 8b6bc09b8f0c0dd48879fe05adb5f614aeb4dc29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 23:02:33 +0100 Subject: [PATCH 5536/6909] Implement drum roll application --- .../TestSceneDrumRollApplication.cs | 39 +++++++++++++++++++ .../Objects/Drawables/DrawableDrumRoll.cs | 34 ++++++++++------ 2 files changed, 62 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.cs new file mode 100644 index 0000000000..54450e27db --- /dev/null +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneDrumRollApplication.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 NUnit.Framework; +using osu.Game.Rulesets.Taiko.Objects; +using osu.Game.Rulesets.Taiko.Objects.Drawables; + +namespace osu.Game.Rulesets.Taiko.Tests +{ + public class TestSceneDrumRollApplication : HitObjectApplicationTestScene + { + [Test] + public void TestApplyNewDrumRoll() + { + var drumRoll = new DrawableDrumRoll(); + + AddStep("apply new drum roll", () => drumRoll.Apply(PrepareObject(new DrumRoll + { + StartTime = 300, + Duration = 500, + IsStrong = false, + TickRate = 2 + }), null)); + + AddHitObject(drumRoll); + RemoveHitObject(drumRoll); + + AddStep("apply new drum roll", () => drumRoll.Apply(PrepareObject(new DrumRoll + { + StartTime = 150, + Duration = 400, + IsStrong = true, + TickRate = 16 + }), null)); + + AddHitObject(drumRoll); + } + } +} diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 4925b6fdfc..ede7453804 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Utils; using osu.Game.Graphics; @@ -31,15 +32,26 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ///
    private int rollingHits; - private Container tickContainer; + private readonly Container tickContainer; private Color4 colourIdle; private Color4 colourEngaged; - public DrawableDrumRoll(DrumRoll drumRoll) + public DrawableDrumRoll() + : this(null) + { + } + + public DrawableDrumRoll([CanBeNull] DrumRoll drumRoll) : base(drumRoll) { RelativeSizeAxes = Axes.Y; + + Content.Add(tickContainer = new Container + { + RelativeSizeAxes = Axes.Both, + Depth = float.MinValue + }); } [BackgroundDependencyLoader] @@ -47,12 +59,6 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { colourIdle = colours.YellowDark; colourEngaged = colours.YellowDarker; - - Content.Add(tickContainer = new Container - { - RelativeSizeAxes = Axes.Both, - Depth = float.MinValue - }); } protected override void LoadComplete() @@ -68,6 +74,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables updateColour(); } + protected override void OnFree() + { + base.OnFree(); + rollingHits = 0; + } + protected override void AddNestedHitObject(DrawableHitObject hitObject) { base.AddNestedHitObject(hitObject); @@ -114,7 +126,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables rollingHits = Math.Clamp(rollingHits, 0, rolling_hits_for_engaged_colour); - updateColour(); + updateColour(100); } protected override void CheckForResult(bool userTriggered, double timeOffset) @@ -156,10 +168,10 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); - private void updateColour() + private void updateColour(double fadeDuration = 0) { Color4 newColour = Interpolation.ValueAt((float)rollingHits / rolling_hits_for_engaged_colour, colourIdle, colourEngaged, 0, 1); - (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, 100); + (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration); } private class StrongNestedHit : DrawableStrongNestedHit From e3b6eaa39059ba8afc2e71a0d034bc3b227fe49f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 23:06:22 +0100 Subject: [PATCH 5537/6909] Implement swell application Also removes a weird sizing application that seems to have no effect (introduced in 27e63eb; compare removals for other taiko DHO types in 9d00e5b and 58bf288). --- .../Objects/Drawables/DrawableSwell.cs | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 229d581d0c..9798a79450 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -35,7 +36,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private readonly CircularContainer targetRing; private readonly CircularContainer expandingRing; - public DrawableSwell(Swell swell) + public DrawableSwell() + : this(null) + { + } + + public DrawableSwell([CanBeNull] Swell swell) : base(swell) { FillMode = FillMode.Fit; @@ -123,12 +129,13 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Origin = Anchor.Centre, }); - protected override void LoadComplete() + protected override void OnFree() { - base.LoadComplete(); + base.OnFree(); - // We need to set this here because RelativeSizeAxes won't/can't set our size by default with a different RelativeChildSize - Width *= Parent.RelativeChildSize.X; + UnproxyContent(); + + lastWasCentre = null; } protected override void AddNestedHitObject(DrawableHitObject hitObject) From 3bd42795899eda793b9b0d346abdae7902585ca9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 23:10:19 +0100 Subject: [PATCH 5538/6909] Implement drum roll tick application --- .../Objects/Drawables/DrawableDrumRollTick.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index c6761de5e3..e7d8ef1e12 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.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.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Skinning.Default; @@ -16,7 +17,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ///
    public HitType JudgementType; - public DrawableDrumRollTick(DrumRollTick tick) + public DrawableDrumRollTick() + : this(null) + { + } + + public DrawableDrumRollTick([CanBeNull] DrumRollTick tick) : base(tick) { FillMode = FillMode.Fit; From d823c77a63e315207bbe7f282ef07ef3eeb8bf1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 23:11:51 +0100 Subject: [PATCH 5539/6909] Implement swell tick application --- .../Objects/Drawables/DrawableSwellTick.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs index 14c86d151f..47fc7e5ab3 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwellTick.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.Graphics; using osu.Game.Rulesets.Taiko.Skinning.Default; using osu.Game.Skinning; @@ -11,7 +12,12 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables { public override bool DisplayResult => false; - public DrawableSwellTick(SwellTick hitObject) + public DrawableSwellTick() + : this(null) + { + } + + public DrawableSwellTick([CanBeNull] SwellTick hitObject) : base(hitObject) { } From ae6dedacaf1edb784c9ca7bdfb4a871f6e0821f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 14 Dec 2020 23:21:19 +0100 Subject: [PATCH 5540/6909] Implement nested strong hit application --- .../Objects/Drawables/DrawableDrumRoll.cs | 17 ++++++++---- .../Objects/Drawables/DrawableDrumRollTick.cs | 17 ++++++++---- .../Objects/Drawables/DrawableHit.cs | 27 +++++++++++-------- .../Drawables/DrawableStrongNestedHit.cs | 7 +++-- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 2 +- 5 files changed, 44 insertions(+), 26 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index ede7453804..01336ea2e4 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -166,7 +166,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables Content.X = DrawHeight / 2; } - protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRoll.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); private void updateColour(double fadeDuration = 0) { @@ -176,17 +176,24 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables private class StrongNestedHit : DrawableStrongNestedHit { - public StrongNestedHit(DrumRoll.StrongNestedHit nestedHit, DrawableDrumRoll drumRoll) - : base(nestedHit, drumRoll) + public new DrawableDrumRoll ParentHitObject => (DrawableDrumRoll)base.ParentHitObject; + + public StrongNestedHit() + : this(null) + { + } + + public StrongNestedHit([CanBeNull] DrumRoll.StrongNestedHit nestedHit) + : base(nestedHit) { } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!MainObject.Judged) + if (!ParentHitObject.Judged) return; - ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index e7d8ef1e12..1e625d91d6 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -67,21 +67,28 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables return UpdateResult(true); } - protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); private class StrongNestedHit : DrawableStrongNestedHit { - public StrongNestedHit(DrumRollTick.StrongNestedHit nestedHit, DrawableDrumRollTick tick) - : base(nestedHit, tick) + public new DrawableDrumRollTick ParentHitObject => (DrawableDrumRollTick)base.ParentHitObject; + + public StrongNestedHit() + : this(null) + { + } + + public StrongNestedHit([CanBeNull] DrumRollTick.StrongNestedHit nestedHit) + : base(nestedHit) { } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!MainObject.Judged) + if (!ParentHitObject.Judged) return; - ApplyResult(r => r.Type = MainObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); + ApplyResult(r => r.Type = ParentHitObject.IsHit ? r.Judgement.MaxResult : r.Judgement.MinResult); } public override bool OnPressed(TaikoAction action) => false; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 5a479e1f53..73ebd7c117 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -250,32 +250,37 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables } } - protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject, this); + protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); private class StrongNestedHit : DrawableStrongNestedHit { + public new DrawableHit ParentHitObject => (DrawableHit)base.ParentHitObject; + /// /// The lenience for the second key press. /// This does not adjust by map difficulty in ScoreV2 yet. /// private const double second_hit_window = 30; - public new DrawableHit MainObject => (DrawableHit)base.MainObject; + public StrongNestedHit() + : this(null) + { + } - public StrongNestedHit(Hit.StrongNestedHit nestedHit, DrawableHit hit) - : base(nestedHit, hit) + public StrongNestedHit([CanBeNull] Hit.StrongNestedHit nestedHit) + : base(nestedHit) { } protected override void CheckForResult(bool userTriggered, double timeOffset) { - if (!MainObject.Result.HasResult) + if (!ParentHitObject.Result.HasResult) { base.CheckForResult(userTriggered, timeOffset); return; } - if (!MainObject.Result.IsHit) + if (!ParentHitObject.Result.IsHit) { ApplyResult(r => r.Type = r.Judgement.MinResult); return; @@ -283,27 +288,27 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables if (!userTriggered) { - if (timeOffset - MainObject.Result.TimeOffset > second_hit_window) + if (timeOffset - ParentHitObject.Result.TimeOffset > second_hit_window) ApplyResult(r => r.Type = r.Judgement.MinResult); return; } - if (Math.Abs(timeOffset - MainObject.Result.TimeOffset) <= second_hit_window) + if (Math.Abs(timeOffset - ParentHitObject.Result.TimeOffset) <= second_hit_window) ApplyResult(r => r.Type = r.Judgement.MaxResult); } public override bool OnPressed(TaikoAction action) { // Don't process actions until the main hitobject is hit - if (!MainObject.IsHit) + if (!ParentHitObject.IsHit) return false; // Don't process actions if the pressed button was released - if (MainObject.HitAction == null) + if (ParentHitObject.HitAction == null) return false; // Don't handle invalid hit action presses - if (!MainObject.HitActions.Contains(action)) + if (!ParentHitObject.HitActions.Contains(action)) return false; return UpdateResult(true); diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs index d2e8888197..9c22e34387 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableStrongNestedHit.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.Rulesets.Objects.Drawables; +using JetBrains.Annotations; using osu.Game.Rulesets.Taiko.Judgements; namespace osu.Game.Rulesets.Taiko.Objects.Drawables @@ -11,12 +11,11 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables ///
    public abstract class DrawableStrongNestedHit : DrawableTaikoHitObject { - public readonly DrawableHitObject MainObject; + public new DrawableTaikoHitObject ParentHitObject => (DrawableTaikoHitObject)base.ParentHitObject; - protected DrawableStrongNestedHit(StrongNestedHitObject nestedHit, DrawableHitObject mainObject) + protected DrawableStrongNestedHit([CanBeNull] StrongNestedHitObject nestedHit) : base(nestedHit) { - MainObject = mainObject; } } } diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index d20b190c86..8682495b41 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Taiko.UI { case TaikoStrongJudgement _: if (result.IsHit) - hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).MainObject)?.VisualiseSecondHit(); + hitExplosionContainer.Children.FirstOrDefault(e => e.JudgedObject == ((DrawableStrongNestedHit)judgedObject).ParentHitObject)?.VisualiseSecondHit(); break; case TaikoDrumRollTickJudgement _: From 3af702453f66663cef225445e11f5cfc5d2cf4b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:37:13 +0900 Subject: [PATCH 5541/6909] Implement realtime match song select --- .../RealtimeMatchSongSelect.cs | 45 ++++++++++++++++--- 1 file changed, 39 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs index 9bcc303065..157216e9bb 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs @@ -1,20 +1,53 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Screens; +using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Select; namespace osu.Game.Screens.Multi.RealtimeMultiplayer { - public class RealtimeMatchSongSelect : SongSelect + public class RealtimeMatchSongSelect : SongSelect, IMultiplayerSubScreen { - protected override BeatmapDetailArea CreateBeatmapDetailArea() - { - throw new System.NotImplementedException(); - } + public string ShortTitle => "song selection"; + + public override string Title => ShortTitle.Humanize(); + + [Resolved(typeof(Room), nameof(Room.Playlist))] + private BindableList playlist { get; set; } + + [Resolved] + private StatefulMultiplayerClient client { get; set; } protected override bool OnStart() { - throw new System.NotImplementedException(); + var item = new PlaylistItem(); + + item.Beatmap.Value = Beatmap.Value.BeatmapInfo; + item.Ruleset.Value = Ruleset.Value; + + item.RequiredMods.Clear(); + item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); + + // If the client is already in a room, update via the client. + // Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation. + if (client.Room != null) + client.ChangeSettings(item: item); + else + { + playlist.Clear(); + playlist.Add(item); + } + + this.Exit(); + return true; } + + protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); } } From d127494c2dedfd133012e574f7fe68d15049073d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:39:31 +0900 Subject: [PATCH 5542/6909] Fix thread-unsafe room removal --- .../Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index a50628a5fa..734d00b9aa 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -54,9 +54,12 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer multiplayerClient.LeaveRoom().Wait(); // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. - RemoveRoom(joinedRoom); // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. - Schedule(() => listingPollingComponent.PollImmediately()); + Schedule(() => + { + RemoveRoom(joinedRoom); + listingPollingComponent.PollImmediately(); + }); } private void joinMultiplayerRoom(Room room, Action onSuccess = null) From a893360c0e07d3c2b31206ace178ad33cf85a47f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 00:41:14 +0900 Subject: [PATCH 5543/6909] Reword comment --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 8d3b161804..0065b425ec 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -71,7 +71,9 @@ namespace osu.Game.Online.RealtimeMultiplayer private RulesetStore rulesets { get; set; } = null!; private Room? apiRoom; - private int playlistItemId; // Todo: THIS IS SUPER TEMPORARY!! + + // Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise. + private int playlistItemId; /// /// Joins the for a given API . From 0c5333bd586dfea1f40e3f6e6d4144bd887713a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 17:57:19 +0100 Subject: [PATCH 5544/6909] Adjust top-level hitobjects to support nested pooling --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs | 2 +- .../Objects/Drawables/DrawableTaikoStrongableHitObject.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index 01336ea2e4..d085b95f35 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -95,7 +95,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - tickContainer.Clear(); + tickContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs index 9798a79450..60f9521996 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableSwell.cs @@ -153,7 +153,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - ticks.Clear(); + ticks.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs index 62f338ca91..4f1523eb3f 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableTaikoStrongableHitObject.cs @@ -96,7 +96,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override void ClearNestedHitObjects() { base.ClearNestedHitObjects(); - strongHitContainer.Clear(); + strongHitContainer.Clear(false); } protected override DrawableHitObject CreateNestedHitObject(HitObject hitObject) From 370f56eadbb97aa1ed05d7bc1952e3b35924430f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 18:02:31 +0100 Subject: [PATCH 5545/6909] Make strong hit DHOs public for pool registration --- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs | 2 +- .../Objects/Drawables/DrawableDrumRollTick.cs | 2 +- osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs index d085b95f35..d066abf767 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRoll.cs @@ -174,7 +174,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables (MainPiece.Drawable as IHasAccentColour)?.FadeAccent(newColour, fadeDuration); } - private class StrongNestedHit : DrawableStrongNestedHit + public class StrongNestedHit : DrawableStrongNestedHit { public new DrawableDrumRoll ParentHitObject => (DrawableDrumRoll)base.ParentHitObject; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs index 1e625d91d6..0df45c424d 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableDrumRollTick.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override DrawableStrongNestedHit CreateStrongNestedHit(DrumRollTick.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); - private class StrongNestedHit : DrawableStrongNestedHit + public class StrongNestedHit : DrawableStrongNestedHit { public new DrawableDrumRollTick ParentHitObject => (DrawableDrumRollTick)base.ParentHitObject; diff --git a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs index 73ebd7c117..38cda69a46 100644 --- a/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs +++ b/osu.Game.Rulesets.Taiko/Objects/Drawables/DrawableHit.cs @@ -252,7 +252,7 @@ namespace osu.Game.Rulesets.Taiko.Objects.Drawables protected override DrawableStrongNestedHit CreateStrongNestedHit(Hit.StrongNestedHit hitObject) => new StrongNestedHit(hitObject); - private class StrongNestedHit : DrawableStrongNestedHit + public class StrongNestedHit : DrawableStrongNestedHit { public new DrawableHit ParentHitObject => (DrawableHit)base.ParentHitObject; From 62da4eff37db8dfc1cfeaeac7d35190eab54356a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 18:07:59 +0100 Subject: [PATCH 5546/6909] Route new result callback via playfield Follows route taken by osu! and catch (and required for proper pooling support). --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 8682495b41..6b001d6c70 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -149,6 +149,12 @@ namespace osu.Game.Rulesets.Taiko.UI }; } + protected override void LoadComplete() + { + base.LoadComplete(); + NewResult += OnNewResult; + } + protected override void Update() { base.Update(); @@ -208,7 +214,6 @@ namespace osu.Game.Rulesets.Taiko.UI break; case DrawableTaikoHitObject taikoObject: - h.OnNewResult += OnNewResult; topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); base.Add(h); break; @@ -226,7 +231,6 @@ namespace osu.Game.Rulesets.Taiko.UI return barLinePlayfield.Remove(barLine); case DrawableTaikoHitObject _: - h.OnNewResult -= OnNewResult; // todo: consider tidying of proxied content if required. return base.Remove(h); From 5d575d2a9b8dd95d8c06e9ef9f1b91214b7794d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 18:11:29 +0100 Subject: [PATCH 5547/6909] Accept proxied content via OnNewDrawableHitObject In the non-pooled case, `OnNewDrawableHitObject()` will be called automatically on each new DHO via `Playfield.Add(DrawableHitObject)`. In the pooled case, it will be called via `Playfield`'s implementation of `GetPooledDrawableRepresentation(HitObject, DrawableHitObject)`. --- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 6b001d6c70..b3bf0974c3 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -155,6 +155,14 @@ namespace osu.Game.Rulesets.Taiko.UI NewResult += OnNewResult; } + protected override void OnNewDrawableHitObject(DrawableHitObject drawableHitObject) + { + base.OnNewDrawableHitObject(drawableHitObject); + + var taikoObject = (DrawableTaikoHitObject)drawableHitObject; + topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); + } + protected override void Update() { base.Update(); @@ -213,8 +221,7 @@ namespace osu.Game.Rulesets.Taiko.UI barLinePlayfield.Add(barLine); break; - case DrawableTaikoHitObject taikoObject: - topLevelHitContainer.Add(taikoObject.CreateProxiedContent()); + case DrawableTaikoHitObject _: base.Add(h); break; @@ -231,7 +238,6 @@ namespace osu.Game.Rulesets.Taiko.UI return barLinePlayfield.Remove(barLine); case DrawableTaikoHitObject _: - // todo: consider tidying of proxied content if required. return base.Remove(h); default: From b24fc1922e11b103243ef24e361609376c353b51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 18:16:05 +0100 Subject: [PATCH 5548/6909] Enable pooling for taiko DHOs --- .../UI/DrawableTaikoRuleset.cs | 18 +----------------- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs index 9cf931ee0a..ed8e6859a2 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoRuleset.cs @@ -7,7 +7,6 @@ using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Taiko.Objects; -using osu.Game.Rulesets.Taiko.Objects.Drawables; using osu.Game.Rulesets.UI; using osu.Game.Rulesets.Taiko.Replays; using osu.Framework.Input; @@ -64,22 +63,7 @@ namespace osu.Game.Rulesets.Taiko.UI protected override Playfield CreatePlayfield() => new TaikoPlayfield(Beatmap.ControlPointInfo); - public override DrawableHitObject CreateDrawableRepresentation(TaikoHitObject h) - { - switch (h) - { - case Hit hit: - return new DrawableHit(hit); - - case DrumRoll drumRoll: - return new DrawableDrumRoll(drumRoll); - - case Swell swell: - return new DrawableSwell(swell); - } - - return null; - } + public override DrawableHitObject CreateDrawableRepresentation(TaikoHitObject h) => null; protected override ReplayInputHandler CreateReplayInputHandler(Replay replay) => new TaikoFramedReplayInputHandler(replay); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index b3bf0974c3..148ec7755e 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -147,6 +147,18 @@ namespace osu.Game.Rulesets.Taiko.UI }, drumRollHitContainer.CreateProxy(), }; + + RegisterPool(50); + RegisterPool(50); + + RegisterPool(5); + RegisterPool(5); + + RegisterPool(100); + RegisterPool(100); + + RegisterPool(5); + RegisterPool(100); } protected override void LoadComplete() From 6e21806873976561322499602e516ee0a6c29d36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 20 Dec 2020 18:25:49 +0100 Subject: [PATCH 5549/6909] Adjust sample test to pass with pooling --- .../TestSceneSampleOutput.cs | 38 ++++++++++++------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs index 4ba9c447fb..296468d98d 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TestSceneSampleOutput.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using osu.Framework.Testing; using osu.Game.Audio; @@ -18,24 +19,33 @@ namespace osu.Game.Rulesets.Taiko.Tests public override void SetUpSteps() { base.SetUpSteps(); - AddAssert("has correct samples", () => + + var expectedSampleNames = new[] { - var names = Player.DrawableRuleset.Playfield.AllHitObjects.OfType().Select(h => string.Join(',', h.GetSamples().Select(s => s.Name))); + string.Empty, + string.Empty, + string.Empty, + string.Empty, + HitSampleInfo.HIT_FINISH, + HitSampleInfo.HIT_WHISTLE, + HitSampleInfo.HIT_WHISTLE, + HitSampleInfo.HIT_WHISTLE, + }; + var actualSampleNames = new List(); - var expected = new[] - { - string.Empty, - string.Empty, - string.Empty, - string.Empty, - HitSampleInfo.HIT_FINISH, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, - HitSampleInfo.HIT_WHISTLE, - }; + // due to pooling we can't access all samples right away due to object re-use, + // so we need to collect as we go. + AddStep("collect sample names", () => Player.DrawableRuleset.Playfield.NewResult += (dho, _) => + { + if (!(dho is DrawableHit h)) + return; - return names.SequenceEqual(expected); + actualSampleNames.Add(string.Join(',', h.GetSamples().Select(s => s.Name))); }); + + AddUntilStep("all samples collected", () => actualSampleNames.Count == expectedSampleNames.Length); + + AddAssert("samples are correct", () => actualSampleNames.SequenceEqual(expectedSampleNames)); } protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) => new TaikoBeatmapConversionTest().GetBeatmap("sample-to-type-conversions"); From a8569fe15cdf663685d960e69356205b11944f8a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 13:35:46 +0900 Subject: [PATCH 5550/6909] Fix a couple of simple cases of incorrect TextureLoaderStore initialisation --- osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs | 3 ++- osu.Game/Storyboards/Drawables/DrawableStoryboard.cs | 5 +++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index a9b2a15b35..b13b20dae2 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -13,6 +13,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Game.Rulesets.Configuration; namespace osu.Game.Rulesets.UI @@ -46,7 +47,7 @@ namespace osu.Game.Rulesets.UI if (resources != null) { - TextureStore = new TextureStore(new TextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); + TextureStore = new TextureStore(parent.Get().CreateTextureLoaderStore(new NamespacedResourceStore(resources, @"Textures"))); CacheAs(TextureStore = new FallbackTextureStore(TextureStore, parent.Get())); SampleStore = parent.Get().GetSampleStore(new NamespacedResourceStore(resources, @"Samples")); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs index 4bc28e6cef..af4615c895 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboard.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; +using osu.Framework.Platform; using osu.Game.IO; using osu.Game.Screens.Play; @@ -59,12 +60,12 @@ namespace osu.Game.Storyboards.Drawables } [BackgroundDependencyLoader(true)] - private void load(FileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken) + private void load(FileStore fileStore, GameplayClock clock, CancellationToken? cancellationToken, GameHost host) { if (clock != null) Clock = clock; - dependencies.Cache(new TextureStore(new TextureLoaderStore(fileStore.Store), false, scaleAdjust: 1)); + dependencies.Cache(new TextureStore(host.CreateTextureLoaderStore(fileStore.Store), false, scaleAdjust: 1)); foreach (var layer in Storyboard.Layers) { From 7c804be4d3c0dd28ba9cbe3c8eabb771bcc32670 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 14:06:33 +0900 Subject: [PATCH 5551/6909] Rename textureStore to make its purpose more clear --- osu.Game/Beatmaps/BeatmapManager.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 33e024fa28..c9d507d068 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -68,7 +68,7 @@ namespace osu.Game.Beatmaps private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; private readonly AudioManager audioManager; - private readonly TextureStore textureStore; + private readonly LargeTextureStore largeTextureStore; private readonly ITrackStore trackStore; [CanBeNull] @@ -92,7 +92,7 @@ namespace osu.Game.Beatmaps if (performOnlineLookups) onlineLookupQueue = new BeatmapOnlineLookupQueue(api, storage); - textureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); + largeTextureStore = new LargeTextureStore(host?.CreateTextureLoaderStore(Files.Store)); trackStore = audioManager.GetTrackStore(Files.Store); } @@ -302,7 +302,7 @@ namespace osu.Game.Beatmaps beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; - workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, textureStore, trackStore, beatmapInfo, audioManager)); + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, largeTextureStore, trackStore, beatmapInfo, audioManager)); return working; } From 0ffbe12fcc2c13af237d71aa06d1e0ccc170651b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 14:06:50 +0900 Subject: [PATCH 5552/6909] Expose resources to beatmaps in a saner way --- osu.Game/Beatmaps/BeatmapManager.cs | 16 ++++++-- .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 28 +++++-------- osu.Game/Beatmaps/IBeatmapResourceProvider.cs | 40 +++++++++++++++++++ 3 files changed, 64 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Beatmaps/IBeatmapResourceProvider.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index c9d507d068..4b334952ef 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -16,6 +16,7 @@ using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; @@ -28,8 +29,8 @@ using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; -using osu.Game.Users; using osu.Game.Skinning; +using osu.Game.Users; using Decoder = osu.Game.Beatmaps.Formats.Decoder; namespace osu.Game.Beatmaps @@ -38,7 +39,7 @@ namespace osu.Game.Beatmaps /// Handles the storage and retrieval of Beatmaps/WorkingBeatmaps. /// [ExcludeFromDynamicCompile] - public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable + public partial class BeatmapManager : DownloadableArchiveModelManager, IDisposable, IBeatmapResourceProvider { /// /// Fired when a single difficulty has been hidden. @@ -71,6 +72,8 @@ namespace osu.Game.Beatmaps private readonly LargeTextureStore largeTextureStore; private readonly ITrackStore trackStore; + private readonly GameHost host; + [CanBeNull] private readonly BeatmapOnlineLookupQueue onlineLookupQueue; @@ -80,6 +83,7 @@ namespace osu.Game.Beatmaps { this.rulesets = rulesets; this.audioManager = audioManager; + this.host = host; DefaultBeatmap = defaultBeatmap; @@ -302,7 +306,7 @@ namespace osu.Game.Beatmaps beatmapInfo.Metadata ??= beatmapInfo.BeatmapSet.Metadata; - workingCache.Add(working = new BeatmapManagerWorkingBeatmap(Files.Store, largeTextureStore, trackStore, beatmapInfo, audioManager)); + workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); return working; } @@ -492,6 +496,12 @@ namespace osu.Game.Beatmaps onlineLookupQueue?.Dispose(); } + TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; + ITrackStore IBeatmapResourceProvider.Tracks => trackStore; + AudioManager IBeatmapResourceProvider.AudioManager => audioManager; + IResourceStore IBeatmapResourceProvider.Files => Files.Store; + IResourceStore IBeatmapResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + /// /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index f5c0d97c1f..1d9a496e36 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -3,10 +3,8 @@ using System; using System.Linq; -using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; using osu.Framework.Logging; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; @@ -21,16 +19,12 @@ namespace osu.Game.Beatmaps [ExcludeFromDynamicCompile] private class BeatmapManagerWorkingBeatmap : WorkingBeatmap { - private readonly IResourceStore store; - private readonly TextureStore textureStore; - private readonly ITrackStore trackStore; + private readonly IBeatmapResourceProvider resources; - public BeatmapManagerWorkingBeatmap(IResourceStore store, TextureStore textureStore, ITrackStore trackStore, BeatmapInfo beatmapInfo, AudioManager audioManager) - : base(beatmapInfo, audioManager) + public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, IBeatmapResourceProvider resources) + : base(beatmapInfo, resources?.AudioManager) { - this.store = store; - this.textureStore = textureStore; - this.trackStore = trackStore; + this.resources = resources; } protected override IBeatmap GetBeatmap() @@ -40,7 +34,7 @@ namespace osu.Game.Beatmaps try { - using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) + using (var stream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapInfo.Path)))) return Decoder.GetDecoder(stream).Decode(stream); } catch (Exception e) @@ -61,7 +55,7 @@ namespace osu.Game.Beatmaps try { - return textureStore.Get(getPathForFile(Metadata.BackgroundFile)); + return resources.LargeTextureStore.Get(getPathForFile(Metadata.BackgroundFile)); } catch (Exception e) { @@ -77,7 +71,7 @@ namespace osu.Game.Beatmaps try { - return trackStore.Get(getPathForFile(Metadata.AudioFile)); + return resources.Tracks.Get(getPathForFile(Metadata.AudioFile)); } catch (Exception e) { @@ -93,7 +87,7 @@ namespace osu.Game.Beatmaps try { - var trackData = store.GetStream(getPathForFile(Metadata.AudioFile)); + var trackData = resources.Files.GetStream(getPathForFile(Metadata.AudioFile)); return trackData == null ? null : new Waveform(trackData); } catch (Exception e) @@ -109,7 +103,7 @@ namespace osu.Game.Beatmaps try { - using (var stream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapInfo.Path)))) + using (var stream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapInfo.Path)))) { var decoder = Decoder.GetDecoder(stream); @@ -118,7 +112,7 @@ namespace osu.Game.Beatmaps storyboard = decoder.Decode(stream); else { - using (var secondaryStream = new LineBufferedReader(store.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) + using (var secondaryStream = new LineBufferedReader(resources.Files.GetStream(getPathForFile(BeatmapSetInfo.StoryboardFile)))) storyboard = decoder.Decode(stream, secondaryStream); } } @@ -138,7 +132,7 @@ namespace osu.Game.Beatmaps { try { - return new LegacyBeatmapSkin(BeatmapInfo, store, AudioManager); + return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, AudioManager); } catch (Exception e) { diff --git a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs new file mode 100644 index 0000000000..43d1a60784 --- /dev/null +++ b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs @@ -0,0 +1,40 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Audio; +using osu.Framework.Audio.Track; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; + +namespace osu.Game.Beatmaps +{ + public interface IBeatmapResourceProvider + { + /// + /// Retrieve a global large texture store, used for loading beatmap backgrounds. + /// + TextureStore LargeTextureStore { get; } + + /// + /// Access a global track store for retrieving beatmap tracks from. + /// + ITrackStore Tracks { get; } + + /// + /// Retrieve the game-wide audio manager. + /// + AudioManager AudioManager { get; } + + /// + /// Access game-wide user files. + /// + IResourceStore Files { get; } + + /// + /// Create a texture loader store based on an underlying data store. + /// + /// The underlying provider of texture data (in arbitrary image formats). + /// A texture loader store. + IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore); + } +} From a5bcf1dc20b31f6fb318dd3bdc7f7be40ae078f8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 15:14:32 +0900 Subject: [PATCH 5553/6909] Expose resources to skin via interface (and share common pieces with beatmap) --- .../Gameplay/TestSceneStoryboardSamples.cs | 28 +++++++++++------- osu.Game/Beatmaps/BeatmapManager.cs | 6 ++-- .../Beatmaps/BeatmapManager_WorkingBeatmap.cs | 2 +- osu.Game/Beatmaps/IBeatmapResourceProvider.cs | 22 ++------------ osu.Game/IO/IStorageResourceProvider.cs | 29 +++++++++++++++++++ osu.Game/Skinning/DefaultLegacySkin.cs | 6 ++-- osu.Game/Skinning/LegacyBeatmapSkin.cs | 6 ++-- osu.Game/Skinning/LegacySkin.cs | 11 ++++--- osu.Game/Skinning/SkinManager.cs | 19 ++++++++---- .../Tests/Beatmaps/HitObjectSampleTest.cs | 20 +++++++++---- .../Tests/Visual/LegacySkinPlayerTestScene.cs | 5 ++-- osu.Game/Tests/Visual/SkinnableTestScene.cs | 23 ++++++++++----- 12 files changed, 109 insertions(+), 68 deletions(-) create mode 100644 osu.Game/IO/IStorageResourceProvider.cs diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index d46769a7c0..36f99c5599 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -10,9 +10,11 @@ using NUnit.Framework; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics.Audio; +using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Audio; +using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; @@ -27,7 +29,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Gameplay { [HeadlessTest] - public class TestSceneStoryboardSamples : OsuTestScene + public class TestSceneStoryboardSamples : OsuTestScene, IStorageResourceProvider { [Test] public void TestRetrieveTopLevelSample() @@ -35,7 +37,7 @@ namespace osu.Game.Tests.Gameplay ISkin skin = null; SampleChannel channel = null; - AddStep("create skin", () => skin = new TestSkin("test-sample", Audio)); + AddStep("create skin", () => skin = new TestSkin("test-sample", this)); AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("test-sample"))); AddAssert("sample is non-null", () => channel != null); @@ -47,7 +49,7 @@ namespace osu.Game.Tests.Gameplay ISkin skin = null; SampleChannel channel = null; - AddStep("create skin", () => skin = new TestSkin("folder/test-sample", Audio)); + AddStep("create skin", () => skin = new TestSkin("folder/test-sample", this)); AddStep("retrieve sample", () => channel = skin.GetSample(new SampleInfo("folder/test-sample"))); AddAssert("sample is non-null", () => channel != null); @@ -105,7 +107,7 @@ namespace osu.Game.Tests.Gameplay AddStep("setup storyboard sample", () => { - Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, Audio); + Beatmap.Value = new TestCustomSkinWorkingBeatmap(new OsuRuleset().RulesetInfo, this); SelectedMods.Value = new[] { testedMod }; var beatmapSkinSourceContainer = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); @@ -128,8 +130,8 @@ namespace osu.Game.Tests.Gameplay private class TestSkin : LegacySkin { - public TestSkin(string resourceName, AudioManager audioManager) - : base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), audioManager, "skin.ini") + public TestSkin(string resourceName, IStorageResourceProvider resources) + : base(DefaultLegacySkin.Info, new TestResourceStore(resourceName), resources, "skin.ini") { } } @@ -158,15 +160,15 @@ namespace osu.Game.Tests.Gameplay private class TestCustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap { - private readonly AudioManager audio; + private readonly IStorageResourceProvider resources; - public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, AudioManager audio) - : base(ruleset, null, audio) + public TestCustomSkinWorkingBeatmap(RulesetInfo ruleset, IStorageResourceProvider resources) + : base(ruleset, null, resources.AudioManager) { - this.audio = audio; + this.resources = resources; } - protected override ISkin GetSkin() => new TestSkin("test-sample", audio); + protected override ISkin GetSkin() => new TestSkin("test-sample", resources); } private class TestDrawableStoryboardSample : DrawableStoryboardSample @@ -176,5 +178,9 @@ namespace osu.Game.Tests.Gameplay { } } + + public AudioManager AudioManager => Audio; + public IResourceStore Files => null; + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4b334952ef..3cf691da9a 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -498,9 +498,9 @@ namespace osu.Game.Beatmaps TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore; - AudioManager IBeatmapResourceProvider.AudioManager => audioManager; - IResourceStore IBeatmapResourceProvider.Files => Files.Store; - IResourceStore IBeatmapResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + AudioManager IStorageResourceProvider.AudioManager => audioManager; + IResourceStore IStorageResourceProvider.Files => Files.Store; + IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); /// /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 1d9a496e36..5d298dbccc 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -132,7 +132,7 @@ namespace osu.Game.Beatmaps { try { - return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, AudioManager); + return new LegacyBeatmapSkin(BeatmapInfo, resources.Files, resources); } catch (Exception e) { diff --git a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs index 43d1a60784..dfea0c7a30 100644 --- a/osu.Game/Beatmaps/IBeatmapResourceProvider.cs +++ b/osu.Game/Beatmaps/IBeatmapResourceProvider.cs @@ -1,14 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; -using osu.Framework.IO.Stores; +using osu.Game.IO; namespace osu.Game.Beatmaps { - public interface IBeatmapResourceProvider + public interface IBeatmapResourceProvider : IStorageResourceProvider { /// /// Retrieve a global large texture store, used for loading beatmap backgrounds. @@ -19,22 +18,5 @@ namespace osu.Game.Beatmaps /// Access a global track store for retrieving beatmap tracks from. /// ITrackStore Tracks { get; } - - /// - /// Retrieve the game-wide audio manager. - /// - AudioManager AudioManager { get; } - - /// - /// Access game-wide user files. - /// - IResourceStore Files { get; } - - /// - /// Create a texture loader store based on an underlying data store. - /// - /// The underlying provider of texture data (in arbitrary image formats). - /// A texture loader store. - IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore); } } diff --git a/osu.Game/IO/IStorageResourceProvider.cs b/osu.Game/IO/IStorageResourceProvider.cs new file mode 100644 index 0000000000..cbd1039807 --- /dev/null +++ b/osu.Game/IO/IStorageResourceProvider.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.Audio; +using osu.Framework.Graphics.Textures; +using osu.Framework.IO.Stores; + +namespace osu.Game.IO +{ + public interface IStorageResourceProvider + { + /// + /// Retrieve the game-wide audio manager. + /// + AudioManager AudioManager { get; } + + /// + /// Access game-wide user files. + /// + IResourceStore Files { get; } + + /// + /// Create a texture loader store based on an underlying data store. + /// + /// The underlying provider of texture data (in arbitrary image formats). + /// A texture loader store. + IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore); + } +} diff --git a/osu.Game/Skinning/DefaultLegacySkin.cs b/osu.Game/Skinning/DefaultLegacySkin.cs index 2758a4cbba..4027cc650d 100644 --- a/osu.Game/Skinning/DefaultLegacySkin.cs +++ b/osu.Game/Skinning/DefaultLegacySkin.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.Audio; using osu.Framework.IO.Stores; +using osu.Game.IO; using osuTK.Graphics; namespace osu.Game.Skinning { public class DefaultLegacySkin : LegacySkin { - public DefaultLegacySkin(IResourceStore storage, AudioManager audioManager) - : base(Info, storage, audioManager, string.Empty) + public DefaultLegacySkin(IResourceStore storage, IStorageResourceProvider resources) + : base(Info, storage, resources, string.Empty) { Configuration.CustomColours["SliderBall"] = new Color4(2, 170, 255, 255); Configuration.AddComboColours( diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index d647bc4a2d..fdcb81b574 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -1,12 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.IO.Stores; using osu.Game.Audio; using osu.Game.Beatmaps; +using osu.Game.IO; using osu.Game.Rulesets.Objects.Legacy; namespace osu.Game.Skinning @@ -16,8 +16,8 @@ namespace osu.Game.Skinning protected override bool AllowManiaSkin => false; protected override bool UseCustomSampleBanks => true; - public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, AudioManager audioManager) - : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), audioManager, beatmap.Path) + public LegacyBeatmapSkin(BeatmapInfo beatmap, IResourceStore storage, IStorageResourceProvider resources) + : base(createSkinInfo(beatmap), new LegacySkinResourceStore(beatmap.BeatmapSet, storage), resources, beatmap.Path) { // Disallow default colours fallback on beatmap skins to allow using parent skin combo colours. (via SkinProvidingContainer) Configuration.AllowDefaultComboColoursFallback = false; diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 80b2fef35c..5a7ad095f6 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -7,7 +7,6 @@ using System.Diagnostics; using System.IO; using System.Linq; using JetBrains.Annotations; -using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -54,12 +53,12 @@ namespace osu.Game.Skinning private readonly Dictionary maniaConfigurations = new Dictionary(); - public LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager) - : this(skin, new LegacySkinResourceStore(skin, storage), audioManager, "skin.ini") + public LegacySkin(SkinInfo skin, IStorageResourceProvider resources) + : this(skin, new LegacySkinResourceStore(skin, resources.Files), resources, "skin.ini") { } - protected LegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, string filename) + protected LegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources, string filename) : base(skin) { using (var stream = storage?.GetStream(filename)) @@ -85,12 +84,12 @@ namespace osu.Game.Skinning if (storage != null) { - var samples = audioManager?.GetSampleStore(storage); + var samples = resources?.AudioManager?.GetSampleStore(storage); if (samples != null) samples.PlaybackConcurrency = OsuGameBase.SAMPLE_CONCURRENCY; Samples = samples; - Textures = new TextureStore(new TextureLoaderStore(storage)); + Textures = new TextureStore(resources?.CreateTextureLoaderStore(storage)); (storage as ResourceStore)?.AddExtension("ogg"); } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 9b69a1eecd..2ee940c02d 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -22,15 +22,18 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.Database; +using osu.Game.IO; using osu.Game.IO.Archives; namespace osu.Game.Skinning { [ExcludeFromDynamicCompile] - public class SkinManager : ArchiveModelManager, ISkinSource + public class SkinManager : ArchiveModelManager, ISkinSource, IStorageResourceProvider { private readonly AudioManager audio; + private readonly GameHost host; + private readonly IResourceStore legacyDefaultResources; public readonly Bindable CurrentSkin = new Bindable(new DefaultSkin()); @@ -42,10 +45,12 @@ namespace osu.Game.Skinning protected override string ImportFromStablePath => "Skins"; - public SkinManager(Storage storage, DatabaseContextFactory contextFactory, IIpcHost importHost, AudioManager audio, IResourceStore legacyDefaultResources) - : base(storage, contextFactory, new SkinStore(contextFactory, storage), importHost) + public SkinManager(Storage storage, DatabaseContextFactory contextFactory, GameHost host, AudioManager audio, IResourceStore legacyDefaultResources) + : base(storage, contextFactory, new SkinStore(contextFactory, storage), host) { this.audio = audio; + this.host = host; + this.legacyDefaultResources = legacyDefaultResources; CurrentSkinInfo.ValueChanged += skin => CurrentSkin.Value = GetSkin(skin.NewValue); @@ -148,9 +153,9 @@ namespace osu.Game.Skinning return new DefaultSkin(); if (skinInfo == DefaultLegacySkin.Info) - return new DefaultLegacySkin(legacyDefaultResources, audio); + return new DefaultLegacySkin(legacyDefaultResources, this); - return new LegacySkin(skinInfo, Files.Store, audio); + return new LegacySkin(skinInfo, this); } /// @@ -169,5 +174,9 @@ namespace osu.Game.Skinning public SampleChannel GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo); public IBindable GetConfig(TLookup lookup) => CurrentSkin.Value.GetConfig(lookup); + + AudioManager IStorageResourceProvider.AudioManager => audio; + IResourceStore IStorageResourceProvider.Files => Files.Store; + IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); } } diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index e3557222d5..dd1979a7e3 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Framework.Timing; @@ -25,7 +26,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Beatmaps { [HeadlessTest] - public abstract class HitObjectSampleTest : PlayerTestScene + public abstract class HitObjectSampleTest : PlayerTestScene, IStorageResourceProvider { protected abstract IResourceStore Resources { get; } protected LegacySkin Skin { get; private set; } @@ -58,7 +59,7 @@ namespace osu.Game.Tests.Beatmaps protected sealed override IBeatmap CreateBeatmap(RulesetInfo ruleset) => currentTestBeatmap; protected sealed override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) - => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, Audio); + => new TestWorkingBeatmap(beatmapInfo, beatmapSkinResourceStore, beatmap, storyboard, Clock, this); protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(false); @@ -109,7 +110,7 @@ namespace osu.Game.Tests.Beatmaps }; // Need to refresh the cached skin source to refresh the skin resource store. - dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, userSkinResourceStore, Audio)); + dependencies.SkinSource = new SkinProvidingContainer(Skin = new LegacySkin(userSkinInfo, this)); }); } @@ -122,6 +123,10 @@ namespace osu.Game.Tests.Beatmaps protected void AssertNoLookup(string name) => AddAssert($"\"{name}\" not looked up", () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && !userSkinResourceStore.PerformedLookups.Contains(name)); + public AudioManager AudioManager => Audio; + public IResourceStore Files => userSkinResourceStore; + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; + private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer { public ISkinSource SkinSource; @@ -191,14 +196,17 @@ namespace osu.Game.Tests.Beatmaps private readonly BeatmapInfo skinBeatmapInfo; private readonly IResourceStore resourceStore; - public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, AudioManager audio) - : base(beatmap, storyboard, referenceClock, audio) + private readonly IStorageResourceProvider resources; + + public TestWorkingBeatmap(BeatmapInfo skinBeatmapInfo, IResourceStore resourceStore, IBeatmap beatmap, Storyboard storyboard, IFrameBasedClock referenceClock, IStorageResourceProvider resources) + : base(beatmap, storyboard, referenceClock, resources.AudioManager) { this.skinBeatmapInfo = skinBeatmapInfo; this.resourceStore = resourceStore; + this.resources = resources; } - protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, AudioManager); + protected override ISkin GetSkin() => new LegacyBeatmapSkin(skinBeatmapInfo, resourceStore, resources); } } } diff --git a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs index 054f72400e..c186525757 100644 --- a/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs +++ b/osu.Game/Tests/Visual/LegacySkinPlayerTestScene.cs @@ -3,7 +3,6 @@ using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio; using osu.Framework.IO.Stores; using osu.Game.Rulesets; using osu.Game.Skinning; @@ -18,9 +17,9 @@ namespace osu.Game.Tests.Visual protected override TestPlayer CreatePlayer(Ruleset ruleset) => new SkinProvidingPlayer(legacySkinSource); [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuGameBase game) + private void load(OsuGameBase game, SkinManager skins) { - var legacySkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), audio); + var legacySkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), skins); legacySkinSource = new SkinProvidingContainer(legacySkin); } diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 68098f9d3b..3a9c33788a 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -13,8 +13,10 @@ using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; +using osu.Game.IO; using osu.Game.Rulesets; using osu.Game.Skinning; using osuTK; @@ -22,13 +24,16 @@ using osuTK.Graphics; namespace osu.Game.Tests.Visual { - public abstract class SkinnableTestScene : OsuGridTestScene + public abstract class SkinnableTestScene : OsuGridTestScene, IStorageResourceProvider { private Skin metricsSkin; private Skin defaultSkin; private Skin specialSkin; private Skin oldSkin; + [Resolved] + private GameHost host { get; set; } + protected SkinnableTestScene() : base(2, 3) { @@ -39,10 +44,10 @@ namespace osu.Game.Tests.Visual { var dllStore = new DllResourceStore(DynamicCompilationOriginal.GetType().Assembly); - metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), audio, true); - defaultSkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), audio); - specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore(dllStore, "Resources/special_skin"), audio, true); - oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore(dllStore, "Resources/old_skin"), audio, true); + metricsSkin = new TestLegacySkin(new SkinInfo { Name = "metrics-skin" }, new NamespacedResourceStore(dllStore, "Resources/metrics_skin"), this, true); + defaultSkin = new DefaultLegacySkin(new NamespacedResourceStore(game.Resources, "Skins/Legacy"), this); + specialSkin = new TestLegacySkin(new SkinInfo { Name = "special-skin" }, new NamespacedResourceStore(dllStore, "Resources/special_skin"), this, true); + oldSkin = new TestLegacySkin(new SkinInfo { Name = "old-skin" }, new NamespacedResourceStore(dllStore, "Resources/old_skin"), this, true); } private readonly List createdDrawables = new List(); @@ -147,6 +152,10 @@ namespace osu.Game.Tests.Visual protected virtual IBeatmap CreateBeatmapForSkinProvider() => CreateWorkingBeatmap(Ruleset.Value).GetPlayableBeatmap(Ruleset.Value); + public AudioManager AudioManager => Audio; + public IResourceStore Files => null; + public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + private class OutlineBox : CompositeDrawable { public OutlineBox() @@ -170,8 +179,8 @@ namespace osu.Game.Tests.Visual { private readonly bool extrapolateAnimations; - public TestLegacySkin(SkinInfo skin, IResourceStore storage, AudioManager audioManager, bool extrapolateAnimations) - : base(skin, storage, audioManager, "skin.ini") + public TestLegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources, bool extrapolateAnimations) + : base(skin, storage, resources, "skin.ini") { this.extrapolateAnimations = extrapolateAnimations; } From 82cf58353ce15e45770bea510181769abeaee846 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 15:38:20 +0900 Subject: [PATCH 5554/6909] Fix incorrect joinedroom null checks --- osu.Game/Screens/Multi/Components/RoomManager.cs | 2 +- .../Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 276a5a6148..482ee5492c 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -112,7 +112,7 @@ namespace osu.Game.Screens.Multi.Components { currentJoinRoomRequest?.Cancel(); - if (JoinedRoom == null) + if (JoinedRoom.Value == null) return; api.Queue(new PartRoomRequest(joinedRoom.Value)); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index b81ca7aade..e0fca3ce4c 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public override void PartRoom() { - if (JoinedRoom == null) + if (JoinedRoom.Value == null) return; var joinedRoom = JoinedRoom.Value; From a59124dd938a8c98a276c85299cbbb144f87b190 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:18:39 +0900 Subject: [PATCH 5555/6909] Make room duration/endsat nullable --- .../Visual/Multiplayer/TestSceneRoomStatus.cs | 12 +++++++++--- osu.Game/Online/Multiplayer/Room.cs | 16 +++++++++++----- .../Screens/Multi/Components/RoomStatusInfo.cs | 11 +++++++++-- .../Match/Components/MatchSettingsOverlay.cs | 2 +- osu.Game/Screens/Multi/MultiplayerComposite.cs | 4 ++-- .../Multi/Timeshift/TimeshiftReadyButton.cs | 4 ++-- 6 files changed, 34 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs index c1dfb94464..a6dd1437f7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs @@ -22,22 +22,28 @@ namespace osu.Game.Tests.Visual.Multiplayer { new DrawableRoom(new Room { - Name = { Value = "Room 1" }, + Name = { Value = "Open - ending in 1 day" }, Status = { Value = new RoomStatusOpen() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) } }) { MatchingFilter = true }, new DrawableRoom(new Room { - Name = { Value = "Room 2" }, + Name = { Value = "Playing - ending in 1 day" }, Status = { Value = new RoomStatusPlaying() }, EndDate = { Value = DateTimeOffset.Now.AddDays(1) } }) { MatchingFilter = true }, new DrawableRoom(new Room { - Name = { Value = "Room 3" }, + Name = { Value = "Ended" }, Status = { Value = new RoomStatusEnded() }, EndDate = { Value = DateTimeOffset.Now } }) { MatchingFilter = true }, + new DrawableRoom(new Room + { + Name = { Value = "Open (realtime)" }, + Status = { Value = new RoomStatusOpen() }, + Category = { Value = RoomCategory.Realtime } + }) { MatchingFilter = true }, } }; } diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 9a21543b2e..ee8992e399 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online.Multiplayer [Cached] [JsonIgnore] - public readonly Bindable Duration = new Bindable(TimeSpan.FromMinutes(30)); + public readonly Bindable Duration = new Bindable(); [Cached] [JsonIgnore] @@ -78,16 +78,22 @@ namespace osu.Game.Online.Multiplayer } [JsonProperty("duration")] - private int duration + private int? duration { - get => (int)Duration.Value.TotalMinutes; - set => Duration.Value = TimeSpan.FromMinutes(value); + get => (int?)Duration.Value?.TotalMinutes; + set + { + if (value == null) + Duration.Value = null; + else + Duration.Value = TimeSpan.FromMinutes(value.Value); + } } // Only supports retrieval for now [Cached] [JsonProperty("ends_at")] - public readonly Bindable EndDate = 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)] diff --git a/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs b/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs index d799f846c2..b5676692a4 100644 --- a/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs +++ b/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs @@ -48,16 +48,23 @@ namespace osu.Game.Screens.Multi.Components private class EndDatePart : DrawableDate { - public readonly IBindable EndDate = new Bindable(); + public readonly IBindable EndDate = new Bindable(); public EndDatePart() : base(DateTimeOffset.UtcNow) { - EndDate.BindValueChanged(date => Date = date.NewValue); + EndDate.BindValueChanged(date => + { + // If null, set a very large future date to prevent unnecessary schedules. + Date = date.NewValue ?? DateTimeOffset.Now.AddYears(1); + }, true); } protected override string Format() { + if (EndDate.Value == null) + return string.Empty; + var diffToNow = Date.Subtract(DateTimeOffset.Now); if (diffToNow.TotalSeconds < -5) diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index b8003b9774..1859e8db8a 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -325,7 +325,7 @@ namespace osu.Game.Screens.Multi.Match.Components Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true); Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); - Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue, true); + Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); playlist.Items.BindTo(Playlist); Playlist.BindCollectionChanged(onPlaylistChanged, true); diff --git a/osu.Game/Screens/Multi/MultiplayerComposite.cs b/osu.Game/Screens/Multi/MultiplayerComposite.cs index e612e77748..6e0c69d712 100644 --- a/osu.Game/Screens/Multi/MultiplayerComposite.cs +++ b/osu.Game/Screens/Multi/MultiplayerComposite.cs @@ -40,12 +40,12 @@ namespace osu.Game.Screens.Multi protected Bindable MaxParticipants { get; private set; } [Resolved(typeof(Room))] - protected Bindable EndDate { get; private set; } + protected Bindable EndDate { get; private set; } [Resolved(typeof(Room))] protected Bindable Availability { get; private set; } [Resolved(typeof(Room))] - protected Bindable Duration { get; private set; } + protected Bindable Duration { get; private set; } } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs index ba639c29f4..c878451eee 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Multi.Timeshift public class TimeshiftReadyButton : ReadyButton { [Resolved(typeof(Room), nameof(Room.EndDate))] - private Bindable endDate { get; set; } + private Bindable endDate { get; set; } public TimeshiftReadyButton() { @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Timeshift { base.Update(); - Enabled.Value = DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; + Enabled.Value = endDate.Value != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; } } } From c3d1eaf36dec71ff703f623b1f274192e6f1275c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:21:05 +0900 Subject: [PATCH 5556/6909] Make RealtimeMultiplayerTestScene abstract --- .../RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs index b52106551e..aec70d8be4 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs @@ -12,7 +12,7 @@ using osu.Game.Screens.Multi.RealtimeMultiplayer; namespace osu.Game.Tests.Visual.RealtimeMultiplayer { - public class RealtimeMultiplayerTestScene : MultiplayerTestScene + public abstract class RealtimeMultiplayerTestScene : MultiplayerTestScene { [Cached(typeof(StatefulMultiplayerClient))] public TestRealtimeMultiplayerClient Client { get; } @@ -28,7 +28,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer private readonly bool joinRoom; - public RealtimeMultiplayerTestScene(bool joinRoom = true) + protected RealtimeMultiplayerTestScene(bool joinRoom = true) { this.joinRoom = joinRoom; base.Content.Add(content = new TestRealtimeRoomContainer { RelativeSizeAxes = Axes.Both }); From 64a32723f3b544fec8f02b120f00986ffcd3e65e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:23:42 +0900 Subject: [PATCH 5557/6909] One more case --- osu.Game/Online/Multiplayer/Room.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index ee8992e399..e3444d6b7e 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -139,7 +139,7 @@ namespace osu.Game.Online.Multiplayer ParticipantCount.Value = other.ParticipantCount.Value; EndDate.Value = other.EndDate.Value; - if (DateTimeOffset.Now >= EndDate.Value) + if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); if (!Playlist.SequenceEqual(other.Playlist)) From 5d73359bd7c0e95a9e18c202319d0ecf77071e18 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:35:19 +0900 Subject: [PATCH 5558/6909] Make participant count non-nullable --- osu.Game/Online/Multiplayer/Room.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 9a21543b2e..5930a624bc 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -67,15 +67,8 @@ namespace osu.Game.Online.Multiplayer public readonly BindableList RecentParticipants = new BindableList(); [Cached] - public readonly Bindable ParticipantCount = new Bindable(); - - // todo: TEMPORARY [JsonProperty("participant_count")] - private int? participantCount - { - get => ParticipantCount.Value; - set => ParticipantCount.Value = value ?? 0; - } + public readonly Bindable ParticipantCount = new Bindable(); [JsonProperty("duration")] private int duration From d096f2f8f6445eb3524df9612e8d7a891528bebe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 16:39:46 +0900 Subject: [PATCH 5559/6909] Fix potential cross-thread operation during chat channel load The callbacks are scheduled to the API thread, but hooked up in BDL load. This causes a potential case of cross-thread collection enumeration. I've tested and it seems like the schedule logic should be fine for short term. Longer term, we probably want to re-think how this works so background operations aren't performed on the `DrawableChannel` in the first place (chat shouldn't have an overhead like this when not visible). Closes #11231. --- osu.Game/Overlays/Chat/DrawableChannel.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index d63faebae4..5926d11c03 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -103,7 +103,7 @@ namespace osu.Game.Overlays.Chat Colour = colours.ChatBlue.Lighten(0.7f), }; - private void newMessagesArrived(IEnumerable newMessages) + private void newMessagesArrived(IEnumerable newMessages) => Schedule(() => { if (newMessages.Min(m => m.Id) < chatLines.Max(c => c.Message.Id)) { @@ -155,9 +155,9 @@ namespace osu.Game.Overlays.Chat if (shouldScrollToEnd) scrollToEnd(); - } + }); - private void pendingMessageResolved(Message existing, Message updated) + private void pendingMessageResolved(Message existing, Message updated) => Schedule(() => { var found = chatLines.LastOrDefault(c => c.Message == existing); @@ -169,12 +169,12 @@ namespace osu.Game.Overlays.Chat found.Message = updated; ChatLineFlow.Add(found); } - } + }); - private void messageRemoved(Message removed) + private void messageRemoved(Message removed) => Schedule(() => { chatLines.FirstOrDefault(c => c.Message == removed)?.FadeColour(Color4.Red, 400).FadeOut(600).Expire(); - } + }); private IEnumerable chatLines => ChatLineFlow.Children.OfType(); From a021aaf54673acf78c5272d077e757b1ded87bc8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:42:21 +0900 Subject: [PATCH 5560/6909] Fix room category being serialised as ints --- osu.Game/Online/Multiplayer/Room.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 9a21543b2e..53ae142ad4 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -35,9 +35,22 @@ namespace osu.Game.Online.Multiplayer public readonly Bindable ChannelId = new Bindable(); [Cached] - [JsonProperty("category")] + [JsonIgnore] public readonly Bindable Category = new Bindable(); + // Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106) + [JsonProperty("category")] + private string categoryString + { + get => Category.Value.ToString().ToLower(); + set + { + if (!Enum.TryParse(value, true, out var enumValue)) + enumValue = RoomCategory.Normal; + Category.Value = enumValue; + } + } + [Cached] [JsonIgnore] public readonly Bindable Duration = new Bindable(TimeSpan.FromMinutes(30)); From e23d81bfc64594c80d21ea1043ec085a47161f25 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 16:56:45 +0900 Subject: [PATCH 5561/6909] Use enum property --- .../Converters/SnakeCaseStringEnumConverter.cs | 16 ++++++++++++++++ osu.Game/Online/Multiplayer/Room.cs | 13 +++++-------- 2 files changed, 21 insertions(+), 8 deletions(-) create mode 100644 osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs diff --git a/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs b/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.cs new file mode 100644 index 0000000000..1d82a5bc87 --- /dev/null +++ b/osu.Game/IO/Serialization/Converters/SnakeCaseStringEnumConverter.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 Newtonsoft.Json.Converters; +using Newtonsoft.Json.Serialization; + +namespace osu.Game.IO.Serialization.Converters +{ + public class SnakeCaseStringEnumConverter : StringEnumConverter + { + public SnakeCaseStringEnumConverter() + { + NamingStrategy = new SnakeCaseNamingStrategy(); + } + } +} diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 53ae142ad4..66e3e8975a 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -6,6 +6,7 @@ using System.Linq; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.IO.Serialization.Converters; using osu.Game.Online.Multiplayer.GameTypes; using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Users; @@ -40,15 +41,11 @@ namespace osu.Game.Online.Multiplayer // Todo: osu-framework bug (https://github.com/ppy/osu-framework/issues/4106) [JsonProperty("category")] - private string categoryString + [JsonConverter(typeof(SnakeCaseStringEnumConverter))] + private RoomCategory category { - get => Category.Value.ToString().ToLower(); - set - { - if (!Enum.TryParse(value, true, out var enumValue)) - enumValue = RoomCategory.Normal; - Category.Value = enumValue; - } + get => Category.Value; + set => Category.Value = value; } [Cached] From eb46c9ce9be47341a18e08fedeeb65408ac5847c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 17:11:30 +0900 Subject: [PATCH 5562/6909] Fix metadata lost in beatmapset deserialisation --- .../Online/API/Requests/Responses/APIBeatmapSet.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 6d0160fbc4..720d6bfff4 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -80,7 +80,7 @@ namespace osu.Game.Online.API.Requests.Responses public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) { - return new BeatmapSetInfo + var beatmapSet = new BeatmapSetInfo { OnlineBeatmapSetID = OnlineBeatmapSetID, Metadata = this, @@ -104,8 +104,17 @@ namespace osu.Game.Online.API.Requests.Responses Genre = genre, Language = language }, - Beatmaps = beatmaps?.Select(b => b.ToBeatmap(rulesets)).ToList(), }; + + beatmapSet.Beatmaps = beatmaps?.Select(b => + { + var beatmap = b.ToBeatmap(rulesets); + beatmap.BeatmapSet = beatmapSet; + beatmap.Metadata = beatmapSet.Metadata; + return beatmap; + }).ToList(); + + return beatmapSet; } } } From e1b2de27a6395f051e4f9f71aa52dfe6398cc4fc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 17:19:29 +0900 Subject: [PATCH 5563/6909] Update osu!mania legacy skin's judgement animation to match stable --- .../Legacy/LegacyManiaJudgementPiece.cs | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index 9684cbb167..e275e1c0ca 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -5,11 +5,11 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Containers; +using osu.Framework.Utils; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mania.UI; using osu.Game.Rulesets.Scoring; using osu.Game.Skinning; -using osuTK; namespace osu.Game.Rulesets.Mania.Skinning.Legacy { @@ -56,31 +56,30 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy (animation as IFramedAnimation)?.GotoFrame(0); + this.FadeInFromZero(20, Easing.Out) + .Then().Delay(160) + .FadeOutFromOne(40, Easing.In); + switch (result) { case HitResult.None: break; case HitResult.Miss: - animation.ScaleTo(1.6f); - animation.ScaleTo(1, 100, Easing.In); - - animation.MoveTo(Vector2.Zero); - animation.MoveToOffset(new Vector2(0, 100), 800, Easing.InQuint); + animation.ScaleTo(1.2f).Then().ScaleTo(1, 100, Easing.Out); animation.RotateTo(0); - animation.RotateTo(40, 800, Easing.InQuint); - - this.FadeOutFromOne(800); + animation.RotateTo(RNG.NextSingle(-18, 18), 100, Easing.Out); break; default: - animation.ScaleTo(0.8f); - animation.ScaleTo(1, 250, Easing.OutElastic); - - animation.Delay(50).ScaleTo(0.75f, 250); - - this.Delay(50).FadeOut(200); + animation.ScaleTo(0.8f) + .Then().ScaleTo(1, 40) + // this is actually correct to match stable; there were overlapping transforms. + .Then().ScaleTo(0.85f) + .Then().ScaleTo(0.7f, 40) + .Then().Delay(100) + .Then().ScaleTo(0.4f, 40, Easing.In); break; } } From 9fa1f60b7d2f0fcae3537148374a5e9214ac907b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 17:31:15 +0900 Subject: [PATCH 5564/6909] Fix incorrect footer being used --- .../Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs index d8b42e065b..cdab1435c0 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs @@ -149,7 +149,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer }, new Drawable[] { - new Footer { SelectedItem = { BindTarget = SelectedItem } } + new RealtimeMatchFooter { SelectedItem = { BindTarget = SelectedItem } } } }, RowDimensions = new[] From 83f1350d7d218ba69082bffe0fc7f495b428cf34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 17:49:10 +0900 Subject: [PATCH 5565/6909] Fix editor background not being correctly cleaned up on forced exit Closes #11214. Should be pretty obvious why. --- osu.Game/Screens/Edit/Editor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index ca7e5fbf20..223c678fba 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -461,7 +461,7 @@ namespace osu.Game.Screens.Edit if (dialogOverlay == null || dialogOverlay.CurrentDialog is PromptForSaveDialog) { confirmExit(); - return false; + return base.OnExiting(next); } if (isNewBeatmap || HasUnsavedChanges) From d11d754715220e5e84c991d497e91d561126444a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 18:09:37 +0900 Subject: [PATCH 5566/6909] Increase size of circle display on timeline --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 657c5834b2..ae2a82fa10 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline { private const float thickness = 5; private const float shadow_radius = 5; - private const float circle_size = 24; + private const float circle_size = 34; public Action OnDragHandled; From d1be7c23d96ec9efe7d4edaaa87fd43c155af259 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 18:09:56 +0900 Subject: [PATCH 5567/6909] Increase height of timeline drag area --- .../Compose/Components/Timeline/TimelineBlueprintContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index 2f14c607c2..ead1aa5c62 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Anchor = Anchor.Centre; Origin = Anchor.Centre; - Height = 0.4f; + Height = 0.6f; AddInternal(new Box { From 423c6158e16bda8c5382de4f99eeabcd776cfb97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 21 Dec 2020 18:10:11 +0900 Subject: [PATCH 5568/6909] Highlight timeline drag area when hovered for better visibility --- .../Timeline/TimelineBlueprintContainer.cs | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs index ead1aa5c62..1fc529910b 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineBlueprintContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; +using osu.Game.Graphics; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Objects; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -25,10 +26,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline [Resolved] private EditorBeatmap beatmap { get; set; } + [Resolved] + private OsuColour colours { get; set; } + private DragEvent lastDragEvent; private Bindable placement; private SelectionBlueprint placementBlueprint; + private readonly Box backgroundBox; + public TimelineBlueprintContainer(HitObjectComposer composer) : base(composer) { @@ -38,7 +44,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline Height = 0.6f; - AddInternal(new Box + AddInternal(backgroundBox = new Box { Colour = Color4.Black, RelativeSizeAxes = Axes.Both, @@ -77,6 +83,18 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline protected override Container CreateSelectionBlueprintContainer() => new TimelineSelectionBlueprintContainer { RelativeSizeAxes = Axes.Both }; + protected override bool OnHover(HoverEvent e) + { + backgroundBox.FadeColour(colours.BlueLighter, 120, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + backgroundBox.FadeColour(Color4.Black, 600, Easing.OutQuint); + base.OnHoverLost(e); + } + protected override void OnDrag(DragEvent e) { handleScrollViaDrag(e); From 0566ed1a9b15a105530ab800865077bc68ab5a9e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 18:38:44 +0900 Subject: [PATCH 5569/6909] Add button to main menu --- osu.Game/Screens/Menu/ButtonSystem.cs | 11 +++++------ osu.Game/Screens/Menu/MainMenu.cs | 5 +++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 4becdd58cd..0014f6768f 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -42,8 +42,7 @@ namespace osu.Game.Screens.Menu public Action OnBeatmapListing; public Action OnSolo; public Action OnSettings; - public Action OnMulti; - public Action OnChart; + public Action OnMulti; public const float BUTTON_WIDTH = 140f; public const float WEDGE_WIDTH = 20; @@ -124,8 +123,8 @@ namespace osu.Game.Screens.Menu private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) { buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMulti, 0, Key.M)); - buttonsPlay.Add(new Button(@"chart", @"button-generic-select", OsuIcon.Charts, new Color4(80, 53, 160, 255), () => OnChart?.Invoke())); + buttonsPlay.Add(new Button(@"multi", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), () => onMulti(true), 0, Key.M)); + buttonsPlay.Add(new Button(@"timeshift", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), () => onMulti(false), 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); @@ -154,7 +153,7 @@ namespace osu.Game.Screens.Menu sampleBack = audio.Samples.Get(@"Menu/button-back-select"); } - private void onMulti() + private void onMulti(bool realtime) { if (!api.IsLoggedIn) { @@ -172,7 +171,7 @@ namespace osu.Game.Screens.Menu return; } - OnMulti?.Invoke(); + OnMulti?.Invoke(realtime); } private void updateIdleState(bool isIdle) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index b781c347f0..391bbf8429 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -17,6 +17,8 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; +using osu.Game.Screens.Multi; +using osu.Game.Screens.Multi.RealtimeMultiplayer; using osu.Game.Screens.Multi.Timeshift; using osu.Game.Screens.Select; @@ -104,7 +106,7 @@ namespace osu.Game.Screens.Menu this.Push(new Editor()); }, OnSolo = onSolo, - OnMulti = delegate { this.Push(new TimeshiftMultiplayer()); }, + OnMulti = realtime => this.Push(realtime ? (Multiplayer)new RealtimeMultiplayer() : new TimeshiftMultiplayer()), OnExit = confirmAndExit, } } @@ -136,7 +138,6 @@ namespace osu.Game.Screens.Menu buttons.OnSettings = () => settings?.ToggleVisibility(); buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility(); - buttons.OnChart = () => rankings?.ShowSpotlights(); LoadComponentAsync(background = new BackgroundScreenDefault()); preloadSongSelect(); From 8427ee1b8e6425b2d6e2bc837a737dba2f7e6e85 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 18:42:23 +0900 Subject: [PATCH 5570/6909] Fix incorrect cached type --- .../RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs | 4 ++-- .../Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs index aec70d8be4..30bd3ebc32 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs @@ -7,8 +7,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.RealtimeMultiplayer; namespace osu.Game.Tests.Visual.RealtimeMultiplayer { @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer [Cached(typeof(StatefulMultiplayerClient))] public TestRealtimeMultiplayerClient Client { get; } - [Cached(typeof(RealtimeRoomManager))] + [Cached(typeof(IRoomManager))] public TestRealtimeRoomManager RoomManager { get; } [Cached] diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs index aa75968cca..3565d6ac5d 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs @@ -6,8 +6,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.RealtimeMultiplayer; namespace osu.Game.Tests.Visual.RealtimeMultiplayer { @@ -19,7 +19,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer [Cached(typeof(StatefulMultiplayerClient))] public readonly TestRealtimeMultiplayerClient Client; - [Cached(typeof(RealtimeRoomManager))] + [Cached(typeof(IRoomManager))] public readonly TestRealtimeRoomManager RoomManager; [Cached] From 56bd3d8a82230fd626fad39c23a40a3fd0729e1a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 18:42:31 +0900 Subject: [PATCH 5571/6909] Add realtime multiplayer test scene --- .../TestSceneRealtimeMultiplayer.cs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs new file mode 100644 index 0000000000..80955ca380 --- /dev/null +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.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.Screens.Multi.Components; + +namespace osu.Game.Tests.Visual.RealtimeMultiplayer +{ + public class TestSceneRealtimeMultiplayer : RealtimeMultiplayerTestScene + { + public TestSceneRealtimeMultiplayer() + { + var multi = new TestRealtimeMultiplayer(); + + AddStep("show", () => LoadScreen(multi)); + AddUntilStep("wait for loaded", () => multi.IsLoaded); + } + + private class TestRealtimeMultiplayer : Screens.Multi.RealtimeMultiplayer.RealtimeMultiplayer + { + protected override RoomManager CreateRoomManager() => new TestRealtimeRoomManager(); + } + } +} From f21b4a269fb7620ef6d612f7709c88f35093c3ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 21 Dec 2020 18:42:55 +0900 Subject: [PATCH 5572/6909] Reduce wait length --- .../RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs index d31364d62e..a059bb1cc0 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs @@ -71,7 +71,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer InputManager.Click(MouseButton.Left); }); - AddWaitStep("wait", 500); + AddWaitStep("wait", 10); } } } From 10c2745682db8b3be8f35f81b8ef42345a4f3e44 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 12:01:09 +0900 Subject: [PATCH 5573/6909] Add region specifications around implicit interface implementations --- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 4 ++++ osu.Game/Beatmaps/BeatmapManager.cs | 4 ++++ osu.Game/Skinning/SkinManager.cs | 4 ++++ osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs | 4 ++++ osu.Game/Tests/Visual/SkinnableTestScene.cs | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 36f99c5599..38cb6729c3 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -179,8 +179,12 @@ namespace osu.Game.Tests.Gameplay } } + #region IResourceStorageProvider + public AudioManager AudioManager => Audio; public IResourceStore Files => null; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; + + #endregion } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 3cf691da9a..8a2991af90 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -496,12 +496,16 @@ namespace osu.Game.Beatmaps onlineLookupQueue?.Dispose(); } + #region IResourceStorageProvider + TextureStore IBeatmapResourceProvider.LargeTextureStore => largeTextureStore; ITrackStore IBeatmapResourceProvider.Tracks => trackStore; AudioManager IStorageResourceProvider.AudioManager => audioManager; IResourceStore IStorageResourceProvider.Files => Files.Store; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + #endregion + /// /// A dummy WorkingBeatmap for the purpose of retrieving a beatmap for star difficulty calculation. /// diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 2ee940c02d..99c64b13a4 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -175,8 +175,12 @@ namespace osu.Game.Skinning public IBindable GetConfig(TLookup lookup) => CurrentSkin.Value.GetConfig(lookup); + #region IResourceStorageProvider + AudioManager IStorageResourceProvider.AudioManager => audio; IResourceStore IStorageResourceProvider.Files => Files.Store; IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + + #endregion } } diff --git a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs index dd1979a7e3..62814d4ed4 100644 --- a/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs +++ b/osu.Game/Tests/Beatmaps/HitObjectSampleTest.cs @@ -123,10 +123,14 @@ namespace osu.Game.Tests.Beatmaps protected void AssertNoLookup(string name) => AddAssert($"\"{name}\" not looked up", () => !beatmapSkinResourceStore.PerformedLookups.Contains(name) && !userSkinResourceStore.PerformedLookups.Contains(name)); + #region IResourceStorageProvider + public AudioManager AudioManager => Audio; public IResourceStore Files => userSkinResourceStore; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => null; + #endregion + private class SkinSourceDependencyContainer : IReadOnlyDependencyContainer { public ISkinSource SkinSource; diff --git a/osu.Game/Tests/Visual/SkinnableTestScene.cs b/osu.Game/Tests/Visual/SkinnableTestScene.cs index 3a9c33788a..3d2c68c2ad 100644 --- a/osu.Game/Tests/Visual/SkinnableTestScene.cs +++ b/osu.Game/Tests/Visual/SkinnableTestScene.cs @@ -152,10 +152,14 @@ namespace osu.Game.Tests.Visual protected virtual IBeatmap CreateBeatmapForSkinProvider() => CreateWorkingBeatmap(Ruleset.Value).GetPlayableBeatmap(Ruleset.Value); + #region IResourceStorageProvider + public AudioManager AudioManager => Audio; public IResourceStore Files => null; public IResourceStore CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + #endregion + private class OutlineBox : CompositeDrawable { public OutlineBox() From a97a2b2a6637b2ec49ff2430dd7dce1cbd935099 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 12:03:25 +0900 Subject: [PATCH 5574/6909] Add nullability to BeatmapManager's GameHost reference --- osu.Game/Beatmaps/BeatmapManager.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 8a2991af90..42418e532b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -72,6 +72,7 @@ namespace osu.Game.Beatmaps private readonly LargeTextureStore largeTextureStore; private readonly ITrackStore trackStore; + [CanBeNull] private readonly GameHost host; [CanBeNull] @@ -502,7 +503,7 @@ namespace osu.Game.Beatmaps ITrackStore IBeatmapResourceProvider.Tracks => trackStore; AudioManager IStorageResourceProvider.AudioManager => audioManager; IResourceStore IStorageResourceProvider.Files => Files.Store; - IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host.CreateTextureLoaderStore(underlyingStore); + IResourceStore IStorageResourceProvider.CreateTextureLoaderStore(IResourceStore underlyingStore) => host?.CreateTextureLoaderStore(underlyingStore); #endregion From 85518b4d9919973a685d2467530064c0518fc605 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 12:06:10 +0900 Subject: [PATCH 5575/6909] Enforce non-null for BeatmapManager WorkingBeatmap resources --- osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs index 5d298dbccc..62cf29dc03 100644 --- a/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/BeatmapManager_WorkingBeatmap.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics.CodeAnalysis; using System.Linq; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; @@ -19,10 +20,11 @@ namespace osu.Game.Beatmaps [ExcludeFromDynamicCompile] private class BeatmapManagerWorkingBeatmap : WorkingBeatmap { + [NotNull] private readonly IBeatmapResourceProvider resources; - public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, IBeatmapResourceProvider resources) - : base(beatmapInfo, resources?.AudioManager) + public BeatmapManagerWorkingBeatmap(BeatmapInfo beatmapInfo, [NotNull] IBeatmapResourceProvider resources) + : base(beatmapInfo, resources.AudioManager) { this.resources = resources; } From 13ef097a53933e2e84608911aa6fd5976a73c945 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 12:08:40 +0900 Subject: [PATCH 5576/6909] Annotate potentially null parameters in protected ctor of LegacySkin --- 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 5a7ad095f6..e4e5bf2f75 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -58,7 +58,7 @@ namespace osu.Game.Skinning { } - protected LegacySkin(SkinInfo skin, IResourceStore storage, IStorageResourceProvider resources, string filename) + protected LegacySkin(SkinInfo skin, [CanBeNull] IResourceStore storage, [CanBeNull] IStorageResourceProvider resources, string filename) : base(skin) { using (var stream = storage?.GetStream(filename)) From bf39aa5980819bc4c0171251173111e47573fb4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 12:18:42 +0900 Subject: [PATCH 5577/6909] Fix incorrectly converted rotation values --- .../Skinning/Legacy/LegacyManiaJudgementPiece.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs index e275e1c0ca..5d662c18d3 100644 --- a/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs +++ b/osu.Game.Rulesets.Mania/Skinning/Legacy/LegacyManiaJudgementPiece.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Mania.Skinning.Legacy animation.ScaleTo(1.2f).Then().ScaleTo(1, 100, Easing.Out); animation.RotateTo(0); - animation.RotateTo(RNG.NextSingle(-18, 18), 100, Easing.Out); + animation.RotateTo(RNG.NextSingle(-5.73f, 5.73f), 100, Easing.Out); break; default: From 87176edca142a2fbbfe4aea1076902449fda1c79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 12:52:30 +0900 Subject: [PATCH 5578/6909] Fix crash when attempting to scale two hitobjects on the same axis --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index ec8c68005f..660e1844aa 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -157,10 +157,16 @@ namespace osu.Game.Rulesets.Osu.Edit foreach (var h in hitObjects) { - h.Position = new Vector2( - quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X), - quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y) - ); + var newPosition = h.Position; + + // guard against no-ops and NaN. + if (scale.X != 0 && quad.Width > 0) + newPosition.X = quad.TopLeft.X + (h.X - quad.TopLeft.X) / quad.Width * (quad.Width + scale.X); + + if (scale.Y != 0 && quad.Height > 0) + newPosition.Y = quad.TopLeft.Y + (h.Y - quad.TopLeft.Y) / quad.Height * (quad.Height + scale.Y); + + h.Position = newPosition; } } From 7c5964fad8471635aa98a4fa1f81ef43ee9d5351 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 13:04:39 +0900 Subject: [PATCH 5579/6909] Revert window modes to previous code to correctly apply framework restrictions --- osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs | 3 ++- osu.Game/Overlays/Settings/SettingsDropdown.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 3df2a78552..3ef60c8fcd 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -59,9 +59,10 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics Children = new Drawable[] { - windowModeDropdown = new SettingsEnumDropdown + windowModeDropdown = new SettingsDropdown { LabelText = "Screen mode", + ItemSource = windowModes, Current = config.GetBindable(FrameworkSetting.WindowMode), }, resolutionSettingsContainer = new Container diff --git a/osu.Game/Overlays/Settings/SettingsDropdown.cs b/osu.Game/Overlays/Settings/SettingsDropdown.cs index e1c6333aa0..1175ddaab8 100644 --- a/osu.Game/Overlays/Settings/SettingsDropdown.cs +++ b/osu.Game/Overlays/Settings/SettingsDropdown.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { - public abstract class SettingsDropdown : SettingsItem + public class SettingsDropdown : SettingsItem { protected new OsuDropdown Control => (OsuDropdown)base.Control; From dab5924a63c869dc0ce74b3c22e8c797a6485169 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Dec 2020 08:02:15 +0300 Subject: [PATCH 5580/6909] Fix resolution dropdown not respecting current display changes --- .../Sections/Graphics/LayoutSettings.cs | 103 ++++++++---------- 1 file changed, 45 insertions(+), 58 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 62dc1dc806..ff3b580c7e 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.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.Drawing; using System.Linq; using osu.Framework.Allocation; @@ -25,9 +24,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private FillFlowContainer> scalingSettings; + private readonly IBindable currentDisplay = new Bindable(); + private readonly IBindableList windowModes = new BindableList(); + private Bindable scalingMode; private Bindable sizeFullscreen; - private readonly IBindableList windowModes = new BindableList(); + + private readonly BindableList resolutions = new BindableList(new[] { new Size(9999, 9999) }); [Resolved] private OsuGameBase game { get; set; } @@ -53,9 +56,10 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingPositionY = osuConfig.GetBindable(OsuSetting.ScalingPositionY); if (host.Window != null) + { + currentDisplay.BindTo(host.Window.CurrentDisplayBindable); windowModes.BindTo(host.Window.SupportedWindowModes); - - Container resolutionSettingsContainer; + } Children = new Drawable[] { @@ -65,10 +69,12 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics Current = config.GetBindable(FrameworkSetting.WindowMode), ItemSource = windowModes, }, - resolutionSettingsContainer = new Container + resolutionDropdown = new ResolutionSettingsDropdown { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y + LabelText = "Resolution", + ShowsDefaultIndicator = false, + ItemSource = resolutions, + Current = sizeFullscreen }, new SettingsSlider { @@ -126,31 +132,34 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, }; - scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); - - var resolutions = getResolutions(); - - if (resolutions.Count > 1) + windowModes.BindCollectionChanged((sender, args) => { - resolutionSettingsContainer.Child = resolutionDropdown = new ResolutionSettingsDropdown - { - LabelText = "Resolution", - ShowsDefaultIndicator = false, - Items = resolutions, - Current = sizeFullscreen - }; + if (windowModes.Count > 1) + windowModeDropdown.Show(); + else + windowModeDropdown.Hide(); + }, true); - windowModeDropdown.Current.BindValueChanged(mode => + windowModeDropdown.Current.ValueChanged += v => updateResolutionDropdown(); + + currentDisplay.BindValueChanged(v => Schedule(() => + { + resolutions.RemoveRange(1, resolutions.Count - 1); + + if (v.NewValue != null) { - if (mode.NewValue == WindowMode.Fullscreen) - { - resolutionDropdown.Show(); - sizeFullscreen.TriggerChange(); - } - else - resolutionDropdown.Hide(); - }, true); - } + resolutions.AddRange(v.NewValue.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()); + } + + updateResolutionDropdown(); + }), true); + + scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); scalingMode.BindValueChanged(mode => { @@ -163,17 +172,13 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics scalingSettings.ForEach(s => s.TransferValueOnCommit = mode.NewValue == ScalingMode.Everything); }, true); - windowModes.CollectionChanged += (sender, args) => windowModesChanged(); - - windowModesChanged(); - } - - private void windowModesChanged() - { - if (windowModes.Count > 1) - windowModeDropdown.Show(); - else - windowModeDropdown.Hide(); + void updateResolutionDropdown() + { + if (resolutions.Count > 1 && windowModeDropdown.Current.Value == WindowMode.Fullscreen) + resolutionDropdown.Show(); + else + resolutionDropdown.Hide(); + } } /// @@ -205,24 +210,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics preview.Expire(); } - private IReadOnlyList getResolutions() - { - var resolutions = new List { new Size(9999, 9999) }; - var currentDisplay = game.Window?.CurrentDisplayBindable.Value; - - if (currentDisplay != null) - { - 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; - } - private class ScalingPreview : ScalingContainer { public ScalingPreview() From dff865f335c8c4c70ed8b4ec7bf1975fc2f52440 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 14:12:02 +0900 Subject: [PATCH 5581/6909] Tidy up comments, code, and multiple linq enumeration --- osu.Game/OsuGame.cs | 18 ++++++------------ .../Screens/Select/DifficultyRecommender.cs | 2 ++ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d2c27b766c..d76bd163e1 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -364,25 +364,19 @@ namespace osu.Game // we might even already be at the song if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true)) - { return; - } // Find beatmaps that match our predicate. - var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true); + var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList(); // Use all beatmaps if predicate matched nothing - if (!beatmaps.Any()) + if (beatmaps.Count == 0) beatmaps = databasedSet.Beatmaps; - // Try to select recommended beatmap - // This should give us a beatmap from current ruleset if there are any in our matched beatmaps - var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps); - // Fallback if a difficulty can't be recommended, maybe we are offline - // First try to find a beatmap in current ruleset - selection ??= beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)); - // Otherwise use first beatmap - selection ??= beatmaps.First(); + // Prefer recommended beatmap if recommendations are available, else fallback to a sane selection. + var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps) + ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) + ?? beatmaps.First(); Ruleset.Value = selection.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index fa48316e9d..3b3fc812e6 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; @@ -47,6 +48,7 @@ namespace osu.Game.Screens.Select /// /// A collection of beatmaps to select a difficulty from. /// The recommended difficulty, or null if a recommendation could not be provided. + [CanBeNull] public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps) { if (!recommendedStarDifficulty.Any()) From 626b7615ad86ca6b93b68cf0dd32e5f23b30b03d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 14:23:33 +0900 Subject: [PATCH 5582/6909] Move and rename some fields for better readability --- osu.Game/OsuGameBase.cs | 6 +++--- osu.Game/Screens/Select/DifficultyRecommender.cs | 13 ++++++++----- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 5fcd6882a6..f6ec541bcb 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -99,6 +99,9 @@ namespace osu.Game [Cached(typeof(IBindable>))] protected readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); + [Cached] + protected readonly DifficultyRecommender DifficultyRecommender = new DifficultyRecommender(); + /// /// Mods available for the current . /// @@ -117,9 +120,6 @@ namespace osu.Game public bool IsDeployedBuild => AssemblyVersion.Major > 0; - [Cached] - protected readonly DifficultyRecommender DifficultyRecommender = new DifficultyRecommender(); - public virtual string Version { get diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Screens/Select/DifficultyRecommender.cs index 3b3fc812e6..5c031b5d6e 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Screens/Select/DifficultyRecommender.cs @@ -27,7 +27,10 @@ namespace osu.Game.Screens.Select [Resolved] private Bindable ruleset { get; set; } - private int storedUserId; + /// + /// The user for which the last requests were run. + /// + private int? requestedUserId; private readonly Dictionary recommendedStarDifficulty = new Dictionary(); @@ -73,12 +76,12 @@ namespace osu.Game.Screens.Select return beatmap; } - private void calculateRecommendedDifficulties() + private void fetchRecommendedValues() { - if (recommendedStarDifficulty.Any() && api.LocalUser.Value.Id == storedUserId) + if (recommendedStarDifficulty.Count > 0 && api.LocalUser.Value.Id == requestedUserId) return; - storedUserId = api.LocalUser.Value.Id; + requestedUserId = api.LocalUser.Value.Id; // only query API for built-in rulesets rulesets.AvailableRulesets.Where(ruleset => ruleset.ID <= ILegacyRuleset.MAX_LEGACY_RULESET_ID).ForEach(rulesetInfo => @@ -125,7 +128,7 @@ namespace osu.Game.Screens.Select switch (state.NewValue) { case APIState.Online: - calculateRecommendedDifficulties(); + fetchRecommendedValues(); break; } }); From 8cc2ed3faeb362e8c78c8a924b4f8c5889690660 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 14:28:26 +0900 Subject: [PATCH 5583/6909] Move from OsuGameBase to OsuGame Also moves to a more suitable namespace. --- .../{Screens/Select => Beatmaps}/DifficultyRecommender.cs | 7 +++++-- osu.Game/OsuGame.cs | 7 ++++++- osu.Game/OsuGameBase.cs | 6 ------ 3 files changed, 11 insertions(+), 9 deletions(-) rename osu.Game/{Screens/Select => Beatmaps}/DifficultyRecommender.cs (94%) diff --git a/osu.Game/Screens/Select/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs similarity index 94% rename from osu.Game/Screens/Select/DifficultyRecommender.cs rename to osu.Game/Beatmaps/DifficultyRecommender.cs index 5c031b5d6e..f870e8f89f 100644 --- a/osu.Game/Screens/Select/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -9,13 +9,16 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; 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 +namespace osu.Game.Beatmaps { + /// + /// A class which will recommend the most suitable difficulty for the local user from a beatmap set. + /// This requires the user to be logged in, as it sources from the user's online profile. + /// public class DifficultyRecommender : Component { [Resolved] diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index d76bd163e1..bb51c55551 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -80,6 +80,9 @@ namespace osu.Game private BeatmapSetOverlay beatmapSetOverlay; + [Cached] + private readonly DifficultyRecommender difficultyRecommender = new DifficultyRecommender(); + [Cached] private readonly ScreenshotManager screenshotManager = new ScreenshotManager(); @@ -374,7 +377,7 @@ namespace osu.Game beatmaps = databasedSet.Beatmaps; // Prefer recommended beatmap if recommendations are available, else fallback to a sane selection. - var selection = DifficultyRecommender.GetRecommendedBeatmap(beatmaps) + var selection = difficultyRecommender.GetRecommendedBeatmap(beatmaps) ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) ?? beatmaps.First(); @@ -639,6 +642,8 @@ namespace osu.Game GetStableStorage = GetStorageForStableInstall }, Add, true); + loadComponentSingleFile(difficultyRecommender, Add); + loadComponentSingleFile(screenshotManager, Add); // dependency on notification overlay, dependent by settings overlay diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index f6ec541bcb..150569f1dd 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -36,7 +36,6 @@ using osu.Game.Resources; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens.Select; using osu.Game.Skinning; using osuTK.Input; using RuntimeInfo = osu.Framework.RuntimeInfo; @@ -99,9 +98,6 @@ namespace osu.Game [Cached(typeof(IBindable>))] protected readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); - [Cached] - protected readonly DifficultyRecommender DifficultyRecommender = new DifficultyRecommender(); - /// /// Mods available for the current . /// @@ -306,8 +302,6 @@ namespace osu.Game AddInternal(MusicController = new MusicController()); dependencies.CacheAs(MusicController); - Add(DifficultyRecommender); - Ruleset.BindValueChanged(onRulesetChanged); } From df5e1d83bd24fb0311868aa93034f790e7b47758 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 14:36:52 +0900 Subject: [PATCH 5584/6909] Allow recommender to potentially be null --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 2847b31e98..a5252fdc96 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -118,7 +118,7 @@ namespace osu.Game.Screens.Select BleedBottom = Footer.HEIGHT, SelectionChanged = updateSelectedBeatmap, BeatmapSetsChanged = carouselBeatmapsLoaded, - GetRecommendedBeatmap = recommender.GetRecommendedBeatmap, + GetRecommendedBeatmap = s => recommender?.GetRecommendedBeatmap(s), }, c => carouselContainer.Child = c); AddRangeInternal(new Drawable[] From c6be969e336e153d7e59fd5e23a5d3eeb8547f5a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 14:42:12 +0900 Subject: [PATCH 5585/6909] Remove unnecessary resolved recommender in test --- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 75a33af247..53a956c77c 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Online.API; @@ -15,7 +14,6 @@ using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Mania; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Taiko; -using osu.Game.Screens.Select; using osu.Game.Tests.Visual.Navigation; using osu.Game.Users; @@ -23,9 +21,6 @@ namespace osu.Game.Tests.Visual.SongSelect { public class TestSceneBeatmapRecommendations : OsuGameTestScene { - [Resolved] - private DifficultyRecommender recommender { get; set; } - [SetUpSteps] public override void SetUpSteps() { From 3bf670510ad61874b516255562c940da0f10c8b4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 22 Dec 2020 14:55:25 +0900 Subject: [PATCH 5586/6909] Split into two actions --- osu.Game/Screens/Menu/ButtonSystem.cs | 32 ++++++++++++++++++++++----- osu.Game/Screens/Menu/MainMenu.cs | 4 ++-- 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 0014f6768f..b52ab2cb2f 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -42,7 +42,8 @@ namespace osu.Game.Screens.Menu public Action OnBeatmapListing; public Action OnSolo; public Action OnSettings; - public Action OnMulti; + public Action OnMultiplayer; + public Action OnTimeshift; public const float BUTTON_WIDTH = 140f; public const float WEDGE_WIDTH = 20; @@ -123,8 +124,8 @@ namespace osu.Game.Screens.Menu private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) { buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new Button(@"multi", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), () => onMulti(true), 0, Key.M)); - buttonsPlay.Add(new Button(@"timeshift", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), () => onMulti(false), 0, Key.L)); + buttonsPlay.Add(new Button(@"multi", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); + buttonsPlay.Add(new Button(@"timeshift", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onTimeshift, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); @@ -153,7 +154,7 @@ namespace osu.Game.Screens.Menu sampleBack = audio.Samples.Get(@"Menu/button-back-select"); } - private void onMulti(bool realtime) + private void onMultiplayer() { if (!api.IsLoggedIn) { @@ -171,7 +172,28 @@ namespace osu.Game.Screens.Menu return; } - OnMulti?.Invoke(realtime); + OnMultiplayer?.Invoke(); + } + + private void onTimeshift() + { + if (!api.IsLoggedIn) + { + notifications?.Post(new SimpleNotification + { + Text = "You gotta be logged in to multi 'yo!", + Icon = FontAwesome.Solid.Globe, + Activated = () => + { + loginOverlay?.Show(); + return true; + } + }); + + return; + } + + OnTimeshift?.Invoke(); } private void updateIdleState(bool isIdle) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 391bbf8429..fa96ac9c51 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -17,7 +17,6 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; -using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.RealtimeMultiplayer; using osu.Game.Screens.Multi.Timeshift; using osu.Game.Screens.Select; @@ -106,7 +105,8 @@ namespace osu.Game.Screens.Menu this.Push(new Editor()); }, OnSolo = onSolo, - OnMulti = realtime => this.Push(realtime ? (Multiplayer)new RealtimeMultiplayer() : new TimeshiftMultiplayer()), + OnMultiplayer = () => this.Push(new RealtimeMultiplayer()), + OnTimeshift = () => this.Push(new TimeshiftMultiplayer()), OnExit = confirmAndExit, } } From 807c1ecd1f47386554d4adc160d426e675413711 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 14:57:32 +0900 Subject: [PATCH 5587/6909] Refactor recommendation iteration code to read better --- osu.Game/Beatmaps/DifficultyRecommender.cs | 52 +++++++--------------- 1 file changed, 16 insertions(+), 36 deletions(-) diff --git a/osu.Game/Beatmaps/DifficultyRecommender.cs b/osu.Game/Beatmaps/DifficultyRecommender.cs index f870e8f89f..340c47d89b 100644 --- a/osu.Game/Beatmaps/DifficultyRecommender.cs +++ b/osu.Game/Beatmaps/DifficultyRecommender.cs @@ -35,7 +35,7 @@ namespace osu.Game.Beatmaps /// private int? requestedUserId; - private readonly Dictionary recommendedStarDifficulty = new Dictionary(); + private readonly Dictionary recommendedDifficultyMapping = new Dictionary(); private readonly IBindable apiState = new Bindable(); @@ -57,31 +57,27 @@ namespace osu.Game.Beatmaps [CanBeNull] public BeatmapInfo GetRecommendedBeatmap(IEnumerable beatmaps) { - if (!recommendedStarDifficulty.Any()) - return null; - - BeatmapInfo beatmap = null; - - foreach (var r in getBestRulesetOrder()) + foreach (var r in orderedRulesets) { - recommendedStarDifficulty.TryGetValue(r, out var stars); + if (!recommendedDifficultyMapping.TryGetValue(r, out var recommendation)) + continue; - beatmap = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => + BeatmapInfo beatmap = beatmaps.Where(b => b.Ruleset.Equals(r)).OrderBy(b => { - var difference = b.StarDifficulty - stars; + var difference = b.StarDifficulty - recommendation; return difference >= 0 ? difference * 2 : difference * -1; // prefer easier over harder }).FirstOrDefault(); if (beatmap != null) - break; + return beatmap; } - return beatmap; + return null; } private void fetchRecommendedValues() { - if (recommendedStarDifficulty.Count > 0 && api.LocalUser.Value.Id == requestedUserId) + if (recommendedDifficultyMapping.Count > 0 && api.LocalUser.Value.Id == requestedUserId) return; requestedUserId = api.LocalUser.Value.Id; @@ -94,7 +90,7 @@ namespace osu.Game.Beatmaps 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; + recommendedDifficultyMapping[rulesetInfo] = Math.Pow((double)(result.Statistics.PP ?? 0), 0.4) * 0.195; }; api.Queue(req); @@ -102,29 +98,13 @@ namespace osu.Game.Beatmaps } /// - /// Rulesets ordered by highest recommended star difficulty, except currently selected ruleset first + /// Rulesets ordered descending by their respective recommended difficulties. + /// The currently selected ruleset will always be first. /// - private IEnumerable getBestRulesetOrder() - { - IEnumerable bestRulesetOrder = recommendedStarDifficulty.OrderByDescending(pair => pair.Value) - .Select(pair => pair.Key); - - List orderedRulesets; - - if (bestRulesetOrder.Contains(ruleset.Value)) - { - orderedRulesets = bestRulesetOrder.ToList(); - orderedRulesets.Remove(ruleset.Value); - orderedRulesets.Insert(0, ruleset.Value); - } - else - { - orderedRulesets = new List { ruleset.Value }; - orderedRulesets.AddRange(bestRulesetOrder); - } - - return orderedRulesets; - } + private IEnumerable orderedRulesets => + recommendedDifficultyMapping + .OrderByDescending(pair => pair.Value).Select(pair => pair.Key).Where(r => !r.Equals(ruleset.Value)) + .Prepend(ruleset.Value); private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { From dece41d050d2f063ed3002269475167484b2270c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 22 Dec 2020 14:58:47 +0900 Subject: [PATCH 5588/6909] Add todo --- osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index d654496f40..f4e84510bf 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -14,6 +14,7 @@ using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Multi.RealtimeMultiplayer { + // Todo: The "room" part of TimeshiftPlayer should be split out into an abstract player class to be inherited instead. public class RealtimePlayer : TimeshiftPlayer { protected override bool PauseOnFocusLost => false; From 81e2edc73ff95d47d272c711d1c7034829a68870 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 22 Dec 2020 14:59:11 +0900 Subject: [PATCH 5589/6909] Use MRE with timeout to wait on match start --- .../RealtimeMultiplayer/RealtimePlayer.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index f4e84510bf..c6d44686b5 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -6,6 +6,8 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Logging; +using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Scoring; @@ -26,7 +28,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private StatefulMultiplayerClient client { get; set; } private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); - private bool started; + private readonly ManualResetEventSlim startedEvent = new ManualResetEventSlim(); public RealtimePlayer(PlaylistItem playlistItem) : base(playlistItem, false) @@ -43,11 +45,19 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer client.ResultsReady += onResultsReady; client.ChangeState(MultiplayerUserState.Loaded); - while (!started) - Thread.Sleep(100); + if (!startedEvent.Wait(TimeSpan.FromSeconds(30))) + { + Logger.Log("Failed to start the multiplayer match in time.", LoggingTarget.Runtime, LogLevel.Important); + + Schedule(() => + { + ValidForResume = false; + this.Exit(); + }); + } } - private void onMatchStarted() => started = true; + private void onMatchStarted() => startedEvent.Set(); private void onResultsReady() => resultsReady.SetResult(true); From 3f966386ed563bd43c241068cf3bdd8ec10f8541 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 15:15:32 +0900 Subject: [PATCH 5590/6909] Fix compile time failure due to potentially null connection --- .../Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs index 6f18e1c922..75bb578a29 100644 --- a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -99,6 +100,8 @@ namespace osu.Game.Online.RealtimeMultiplayer { try { + Debug.Assert(connection != null); + // reconnect on any failure await connection.StartAsync(); Logger.Log("Multiplayer client connected!", LoggingTarget.Network); From 85e93c5ddec913aa48515814867e89688c92e518 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 15:22:27 +0900 Subject: [PATCH 5591/6909] Fix main menu multiplayer icons being back to front --- osu.Game/Screens/Menu/ButtonSystem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index b52ab2cb2f..badfa3f693 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -124,8 +124,8 @@ namespace osu.Game.Screens.Menu private void load(AudioManager audio, IdleTracker idleTracker, GameHost host) { buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); - buttonsPlay.Add(new Button(@"multi", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); - buttonsPlay.Add(new Button(@"timeshift", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onTimeshift, 0, Key.L)); + buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); + buttonsPlay.Add(new Button(@"timeshift", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onTimeshift, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); From ab90db7c8df2ab778d3b84d964eae62823e41443 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 22 Dec 2020 15:27:49 +0900 Subject: [PATCH 5592/6909] Fix stuck lounge on join failure --- .../Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index e0fca3ce4c..0c9b58a0c4 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -38,10 +38,10 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer } public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) - => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError); + => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) - => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess), onError); + => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); public override void PartRoom() { @@ -62,7 +62,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer }); } - private void joinMultiplayerRoom(Room room, Action onSuccess = null) + private void joinMultiplayerRoom(Room room, Action onSuccess = null, Action onError = null) { Debug.Assert(room.RoomID.Value != null); @@ -73,6 +73,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer PartRoom(); if (t.Exception != null) Logger.Error(t.Exception, "Failed to join multiplayer room."); + onError?.Invoke(t.Exception?.ToString() ?? string.Empty); }, TaskContinuationOptions.NotOnRanToCompletion); } From 27e64bdb342aa33aa74da56fa155960662fc3a1e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 22 Dec 2020 15:31:07 +0900 Subject: [PATCH 5593/6909] Schedule callback continuations --- .../Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 0c9b58a0c4..7ce031e0e9 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -67,13 +67,13 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer Debug.Assert(room.RoomID.Value != null); var joinTask = multiplayerClient.JoinRoom(room); - joinTask.ContinueWith(_ => onSuccess?.Invoke(room), TaskContinuationOptions.OnlyOnRanToCompletion); + joinTask.ContinueWith(_ => Schedule(() => onSuccess?.Invoke(room)), TaskContinuationOptions.OnlyOnRanToCompletion); joinTask.ContinueWith(t => { PartRoom(); if (t.Exception != null) Logger.Error(t.Exception, "Failed to join multiplayer room."); - onError?.Invoke(t.Exception?.ToString() ?? string.Empty); + Schedule(() => onError?.Invoke(t.Exception?.ToString() ?? string.Empty)); }, TaskContinuationOptions.NotOnRanToCompletion); } From 8201fa8e3463024ab3155738df934bc1cf967822 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 15:51:24 +0900 Subject: [PATCH 5594/6909] Split out common implementation and private classes in MatchSettingsOverlay --- .../TestSceneMatchSettingsOverlay.cs | 10 +- .../TestSceneTimeshiftRoomSubScreen.cs | 2 +- .../Match/Components/MatchSettingsOverlay.cs | 185 +++++++++--------- .../Match/RealtimeMatchSettingsOverlay.cs | 95 +-------- .../Multi/Timeshift/TimeshiftRoomSubScreen.cs | 5 +- 5 files changed, 108 insertions(+), 189 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index 234374ee2b..90eef26b20 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -109,14 +109,14 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("error not displayed", () => !settings.ErrorText.IsPresent); } - private class TestRoomSettings : MatchSettingsOverlay + private class TestRoomSettings : TimeshiftMatchSettingsOverlay { - public TriangleButton ApplyButton => Settings.ApplyButton; + public TriangleButton ApplyButton => ((MatchSettings)Settings).ApplyButton; - public OsuTextBox NameField => Settings.NameField; - public OsuDropdown DurationField => Settings.DurationField; + public OsuTextBox NameField => ((MatchSettings)Settings).NameField; + public OsuDropdown DurationField => ((MatchSettings)Settings).DurationField; - public OsuSpriteText ErrorText => Settings.ErrorText; + public OsuSpriteText ErrorText => ((MatchSettings)Settings).ErrorText; } private class TestRoomManager : IRoomManager diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs index f56f78ce89..bbd7d84081 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create room", () => { - InputManager.MoveMouseTo(match.ChildrenOfType().Single()); + InputManager.MoveMouseTo(match.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index 1859e8db8a..d837ceaccf 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs @@ -21,20 +21,108 @@ using osuTK.Graphics; namespace osu.Game.Screens.Multi.Match.Components { - public class MatchSettingsOverlay : FocusedOverlayContainer + public abstract class MatchSettingsOverlay : FocusedOverlayContainer { - private const float transition_duration = 350; - private const float field_padding = 45; + protected const float TRANSITION_DURATION = 350; + protected const float FIELD_PADDING = 45; - public Action EditPlaylist; - - protected MatchSettings Settings { get; private set; } + protected MultiplayerComposite Settings { get; set; } [BackgroundDependencyLoader] private void load() { Masking = true; + } + protected override void PopIn() + { + Settings.MoveToY(0, TRANSITION_DURATION, Easing.OutQuint); + } + + protected override void PopOut() + { + Settings.MoveToY(-1, TRANSITION_DURATION, Easing.InSine); + } + + protected class SettingsTextBox : OsuTextBox + { + [BackgroundDependencyLoader] + private void load() + { + BackgroundUnfocused = Color4.Black; + BackgroundFocused = Color4.Black; + } + } + + protected class SettingsNumberTextBox : SettingsTextBox + { + protected override bool CanAddCharacter(char character) => char.IsNumber(character); + } + + protected class SettingsPasswordTextBox : OsuPasswordTextBox + { + [BackgroundDependencyLoader] + private void load() + { + BackgroundUnfocused = Color4.Black; + BackgroundFocused = Color4.Black; + } + } + + protected class SectionContainer : FillFlowContainer
    + { + public SectionContainer() + { + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Width = 0.5f; + Direction = FillDirection.Vertical; + Spacing = new Vector2(FIELD_PADDING); + } + } + + protected class Section : Container + { + private readonly Container content; + + protected override Container Content => content; + + public Section(string title) + { + AutoSizeAxes = Axes.Y; + RelativeSizeAxes = Axes.X; + + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(5), + Children = new Drawable[] + { + new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), + Text = title.ToUpper(), + }, + content = new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }, + }, + }; + } + } + } + + public class TimeshiftMatchSettingsOverlay : MatchSettingsOverlay + { + public Action EditPlaylist; + + [BackgroundDependencyLoader] + private void load() + { Child = Settings = new MatchSettings { RelativeSizeAxes = Axes.Both, @@ -43,16 +131,6 @@ namespace osu.Game.Screens.Multi.Match.Components }; } - protected override void PopIn() - { - Settings.MoveToY(0, transition_duration, Easing.OutQuint); - } - - protected override void PopOut() - { - Settings.MoveToY(-1, transition_duration, Easing.InSine); - } - protected class MatchSettings : MultiplayerComposite { private const float disabled_alpha = 0.2f; @@ -126,7 +204,7 @@ namespace osu.Game.Screens.Multi.Match.Components { new SectionContainer { - Padding = new MarginPadding { Right = field_padding / 2 }, + Padding = new MarginPadding { Right = FIELD_PADDING / 2 }, Children = new[] { new Section("Room name") @@ -216,7 +294,7 @@ namespace osu.Game.Screens.Multi.Match.Components { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Padding = new MarginPadding { Left = field_padding / 2 }, + Padding = new MarginPadding { Left = FIELD_PADDING / 2 }, Children = new[] { new Section("Playlist") @@ -379,77 +457,6 @@ namespace osu.Game.Screens.Multi.Match.Components } } - private class SettingsTextBox : OsuTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = Color4.Black; - BackgroundFocused = Color4.Black; - } - } - - private class SettingsNumberTextBox : SettingsTextBox - { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); - } - - private class SettingsPasswordTextBox : OsuPasswordTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = Color4.Black; - BackgroundFocused = Color4.Black; - } - } - - private class SectionContainer : FillFlowContainer
    - { - public SectionContainer() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Width = 0.5f; - Direction = FillDirection.Vertical; - Spacing = new Vector2(field_padding); - } - } - - private class Section : Container - { - private readonly Container content; - - protected override Container Content => content; - - public Section(string title) - { - AutoSizeAxes = Axes.Y; - RelativeSizeAxes = Axes.X; - - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5), - Children = new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), - Text = title.ToUpper(), - }, - content = new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - }, - }, - }; - } - } - public class CreateRoomButton : TriangleButton { public CreateRoomButton() diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs index 924c736472..5bd388cceb 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs @@ -19,22 +19,14 @@ using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Screens.Multi.Match.Components; using osuTK; -using osuTK.Graphics; namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match { - public class RealtimeMatchSettingsOverlay : FocusedOverlayContainer + public class RealtimeMatchSettingsOverlay : MatchSettingsOverlay { - private const float transition_duration = 350; - private const float field_padding = 45; - - protected MatchSettings Settings { get; private set; } - [BackgroundDependencyLoader] private void load() { - Masking = true; - Child = Settings = new MatchSettings { RelativeSizeAxes = Axes.Both, @@ -43,16 +35,6 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match }; } - protected override void PopIn() - { - Settings.MoveToY(0, transition_duration, Easing.OutQuint); - } - - protected override void PopOut() - { - Settings.MoveToY(-1, transition_duration, Easing.InSine); - } - protected class MatchSettings : MultiplayerComposite { private const float disabled_alpha = 0.2f; @@ -143,7 +125,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match { new SectionContainer { - Padding = new MarginPadding { Right = field_padding / 2 }, + Padding = new MarginPadding { Right = FIELD_PADDING / 2 }, Children = new[] { new Section("Room name") @@ -192,7 +174,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, - Padding = new MarginPadding { Left = field_padding / 2 }, + Padding = new MarginPadding { Left = FIELD_PADDING / 2 }, Children = new[] { new Section("Max participants") @@ -347,77 +329,6 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match } } - private class SettingsTextBox : OsuTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = Color4.Black; - BackgroundFocused = Color4.Black; - } - } - - private class SettingsNumberTextBox : SettingsTextBox - { - protected override bool CanAddCharacter(char character) => char.IsNumber(character); - } - - private class SettingsPasswordTextBox : OsuPasswordTextBox - { - [BackgroundDependencyLoader] - private void load() - { - BackgroundUnfocused = Color4.Black; - BackgroundFocused = Color4.Black; - } - } - - private class SectionContainer : FillFlowContainer
    - { - public SectionContainer() - { - RelativeSizeAxes = Axes.X; - AutoSizeAxes = Axes.Y; - Width = 0.5f; - Direction = FillDirection.Vertical; - Spacing = new Vector2(field_padding); - } - } - - private class Section : Container - { - private readonly Container content; - - protected override Container Content => content; - - public Section(string title) - { - AutoSizeAxes = Axes.Y; - RelativeSizeAxes = Axes.X; - - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Spacing = new Vector2(5), - Children = new Drawable[] - { - new OsuSpriteText - { - Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 12), - Text = title.ToUpper(), - }, - content = new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - }, - }, - }; - } - } - public class CreateOrUpdateButton : TriangleButton { [Resolved(typeof(Room), nameof(Room.RoomID))] diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs index 433a980d60..fa901179e9 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs @@ -18,6 +18,7 @@ using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Play; using osu.Game.Screens.Select; using osu.Game.Users; +using Footer = osu.Game.Screens.Multi.Match.Components.Footer; namespace osu.Game.Screens.Multi.Timeshift { @@ -175,7 +176,7 @@ namespace osu.Game.Screens.Multi.Timeshift }, new Drawable[] { - new Match.Components.Footer + new Footer { OnStart = onStart, SelectedItem = { BindTarget = SelectedItem } @@ -188,7 +189,7 @@ namespace osu.Game.Screens.Multi.Timeshift new Dimension(GridSizeMode.AutoSize), } }, - settingsOverlay = new MatchSettingsOverlay + settingsOverlay = new TimeshiftMatchSettingsOverlay { RelativeSizeAxes = Axes.Both, EditPlaylist = () => this.Push(new MatchSongSelect()), From 17d924c7554d561d028ed761681c6e4703286267 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 15:52:47 +0900 Subject: [PATCH 5595/6909] Move timeshift settings overlay to correct namespace --- .../TestSceneMatchSettingsOverlay.cs | 2 +- .../Match/Components/MatchSettingsOverlay.cs | 377 ----------------- .../TimeshiftMatchSettingsOverlay.cs | 391 ++++++++++++++++++ 3 files changed, 392 insertions(+), 378 deletions(-) create mode 100644 osu.Game/Screens/Multi/Timeshift/TimeshiftMatchSettingsOverlay.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index 90eef26b20..1fcae9c709 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.Multi.Timeshift; namespace osu.Game.Tests.Visual.Multiplayer { diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs index d837ceaccf..0bb56d0cdf 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.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.Specialized; -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.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; -using osu.Game.Overlays; using osuTK; using osuTK.Graphics; @@ -115,372 +106,4 @@ namespace osu.Game.Screens.Multi.Match.Components } } } - - public class TimeshiftMatchSettingsOverlay : MatchSettingsOverlay - { - public Action EditPlaylist; - - [BackgroundDependencyLoader] - private void load() - { - Child = Settings = new MatchSettings - { - RelativeSizeAxes = Axes.Both, - RelativePositionAxes = Axes.Y, - EditPlaylist = () => EditPlaylist?.Invoke() - }; - } - - protected class MatchSettings : MultiplayerComposite - { - private const float disabled_alpha = 0.2f; - - public Action EditPlaylist; - - public OsuTextBox NameField, MaxParticipantsField; - public OsuDropdown DurationField; - public RoomAvailabilityPicker AvailabilityPicker; - public GameTypePicker TypePicker; - public TriangleButton ApplyButton; - - public OsuSpriteText ErrorText; - - private OsuSpriteText typeLabel; - private LoadingLayer loadingLayer; - private DrawableRoomPlaylist playlist; - private OsuSpriteText playlistLength; - - [Resolved(CanBeNull = true)] - private IRoomManager manager { get; set; } - - [Resolved] - private Bindable currentRoom { get; set; } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - Container dimContent; - - InternalChildren = new Drawable[] - { - dimContent = new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d"), - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] - { - new Dimension(GridSizeMode.Distributed), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] - { - new OsuScrollContainer - { - Padding = new MarginPadding - { - Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING, - Vertical = 10 - }, - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new Container - { - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new SectionContainer - { - Padding = new MarginPadding { Right = FIELD_PADDING / 2 }, - Children = new[] - { - new Section("Room name") - { - Child = NameField = new SettingsTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - LengthLimit = 100 - }, - }, - new Section("Duration") - { - Child = DurationField = new DurationDropdown - { - RelativeSizeAxes = Axes.X, - Items = new[] - { - TimeSpan.FromMinutes(30), - TimeSpan.FromHours(1), - TimeSpan.FromHours(2), - TimeSpan.FromHours(4), - TimeSpan.FromHours(8), - TimeSpan.FromHours(12), - //TimeSpan.FromHours(16), - TimeSpan.FromHours(24), - TimeSpan.FromDays(3), - TimeSpan.FromDays(7) - } - } - }, - new Section("Room visibility") - { - Alpha = disabled_alpha, - Child = AvailabilityPicker = new RoomAvailabilityPicker - { - Enabled = { Value = false } - }, - }, - new Section("Game type") - { - Alpha = disabled_alpha, - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Spacing = new Vector2(7), - Children = new Drawable[] - { - TypePicker = new GameTypePicker - { - RelativeSizeAxes = Axes.X, - Enabled = { Value = false } - }, - typeLabel = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 14), - Colour = colours.Yellow - }, - }, - }, - }, - new Section("Max participants") - { - Alpha = disabled_alpha, - Child = MaxParticipantsField = new SettingsNumberTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - ReadOnly = true, - }, - }, - new Section("Password (optional)") - { - Alpha = disabled_alpha, - Child = new SettingsPasswordTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - ReadOnly = true, - }, - }, - }, - }, - new SectionContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Padding = new MarginPadding { Left = FIELD_PADDING / 2 }, - Children = new[] - { - new Section("Playlist") - { - Child = new GridContainer - { - RelativeSizeAxes = Axes.X, - Height = 300, - Content = new[] - { - new Drawable[] - { - playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both } - }, - new Drawable[] - { - playlistLength = new OsuSpriteText - { - Margin = new MarginPadding { Vertical = 5 }, - Colour = colours.Yellow, - Font = OsuFont.GetFont(size: 12), - } - }, - new Drawable[] - { - new PurpleTriangleButton - { - RelativeSizeAxes = Axes.X, - Height = 40, - Text = "Edit playlist", - Action = () => EditPlaylist?.Invoke() - } - } - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - } - } - }, - }, - }, - }, - } - }, - }, - }, - new Drawable[] - { - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Y = 2, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Margin = new MarginPadding { Vertical = 20 }, - Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, - Children = new Drawable[] - { - ApplyButton = new CreateRoomButton - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Size = new Vector2(230, 55), - Enabled = { Value = false }, - Action = apply, - }, - ErrorText = new OsuSpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Alpha = 0, - Depth = 1, - Colour = colours.RedDark - } - } - } - } - } - } - } - }, - } - }, - loadingLayer = new LoadingLayer(dimContent) - }; - - TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true); - RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true); - Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true); - Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); - MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); - Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); - - playlist.Items.BindTo(Playlist); - Playlist.BindCollectionChanged(onPlaylistChanged, true); - } - - protected override void Update() - { - base.Update(); - - ApplyButton.Enabled.Value = hasValidSettings; - } - - private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => - playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; - - private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; - - private void apply() - { - if (!ApplyButton.Enabled.Value) - return; - - hideError(); - - RoomName.Value = NameField.Text; - Availability.Value = AvailabilityPicker.Current.Value; - Type.Value = TypePicker.Current.Value; - - if (int.TryParse(MaxParticipantsField.Text, out int max)) - MaxParticipants.Value = max; - else - MaxParticipants.Value = null; - - Duration.Value = DurationField.Current.Value; - - manager?.CreateRoom(currentRoom.Value, onSuccess, onError); - - loadingLayer.Show(); - } - - private void hideError() => ErrorText.FadeOut(50); - - private void onSuccess(Room room) => loadingLayer.Hide(); - - private void onError(string text) - { - ErrorText.Text = text; - ErrorText.FadeIn(50); - - loadingLayer.Hide(); - } - } - - public class CreateRoomButton : TriangleButton - { - public CreateRoomButton() - { - Text = "Create"; - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - BackgroundColour = colours.Yellow; - Triangles.ColourLight = colours.YellowLight; - Triangles.ColourDark = colours.YellowDark; - } - } - - private class DurationDropdown : OsuDropdown - { - public DurationDropdown() - { - Menu.MaxHeight = 100; - } - - protected override string GenerateItemText(TimeSpan item) => item.Humanize(); - } - } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMatchSettingsOverlay.cs new file mode 100644 index 0000000000..7e1e9894d8 --- /dev/null +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMatchSettingsOverlay.cs @@ -0,0 +1,391 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Specialized; +using 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.Graphics.Shapes; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; +using osu.Game.Overlays; +using osu.Game.Screens.Multi.Match.Components; +using osuTK; + +namespace osu.Game.Screens.Multi.Timeshift +{ + public class TimeshiftMatchSettingsOverlay : MatchSettingsOverlay + { + public Action EditPlaylist; + + [BackgroundDependencyLoader] + private void load() + { + Child = Settings = new MatchSettings + { + RelativeSizeAxes = Axes.Both, + RelativePositionAxes = Axes.Y, + EditPlaylist = () => EditPlaylist?.Invoke() + }; + } + + protected class MatchSettings : MultiplayerComposite + { + private const float disabled_alpha = 0.2f; + + public Action EditPlaylist; + + public OsuTextBox NameField, MaxParticipantsField; + public OsuDropdown DurationField; + public RoomAvailabilityPicker AvailabilityPicker; + public GameTypePicker TypePicker; + public TriangleButton ApplyButton; + + public OsuSpriteText ErrorText; + + private OsuSpriteText typeLabel; + private LoadingLayer loadingLayer; + private DrawableRoomPlaylist playlist; + private OsuSpriteText playlistLength; + + [Resolved(CanBeNull = true)] + private IRoomManager manager { get; set; } + + [Resolved] + private Bindable currentRoom { get; set; } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + Container dimContent; + + InternalChildren = new Drawable[] + { + dimContent = new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d"), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.Distributed), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new OsuScrollContainer + { + Padding = new MarginPadding + { + Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING, + Vertical = 10 + }, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new Container + { + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new SectionContainer + { + Padding = new MarginPadding { Right = FIELD_PADDING / 2 }, + Children = new[] + { + new Section("Room name") + { + Child = NameField = new SettingsTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + LengthLimit = 100 + }, + }, + new Section("Duration") + { + Child = DurationField = new DurationDropdown + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + TimeSpan.FromMinutes(30), + TimeSpan.FromHours(1), + TimeSpan.FromHours(2), + TimeSpan.FromHours(4), + TimeSpan.FromHours(8), + TimeSpan.FromHours(12), + //TimeSpan.FromHours(16), + TimeSpan.FromHours(24), + TimeSpan.FromDays(3), + TimeSpan.FromDays(7) + } + } + }, + new Section("Room visibility") + { + Alpha = disabled_alpha, + Child = AvailabilityPicker = new RoomAvailabilityPicker + { + Enabled = { Value = false } + }, + }, + new Section("Game type") + { + Alpha = disabled_alpha, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(7), + Children = new Drawable[] + { + TypePicker = new GameTypePicker + { + RelativeSizeAxes = Axes.X, + Enabled = { Value = false } + }, + typeLabel = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14), + Colour = colours.Yellow + }, + }, + }, + }, + new Section("Max participants") + { + Alpha = disabled_alpha, + Child = MaxParticipantsField = new SettingsNumberTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + ReadOnly = true, + }, + }, + new Section("Password (optional)") + { + Alpha = disabled_alpha, + Child = new SettingsPasswordTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + ReadOnly = true, + }, + }, + }, + }, + new SectionContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Padding = new MarginPadding { Left = FIELD_PADDING / 2 }, + Children = new[] + { + new Section("Playlist") + { + Child = new GridContainer + { + RelativeSizeAxes = Axes.X, + Height = 300, + Content = new[] + { + new Drawable[] + { + playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both } + }, + new Drawable[] + { + playlistLength = new OsuSpriteText + { + Margin = new MarginPadding { Vertical = 5 }, + Colour = colours.Yellow, + Font = OsuFont.GetFont(size: 12), + } + }, + new Drawable[] + { + new PurpleTriangleButton + { + RelativeSizeAxes = Axes.X, + Height = 40, + Text = "Edit playlist", + Action = () => EditPlaylist?.Invoke() + } + } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + } + } + }, + }, + }, + }, + } + }, + }, + }, + new Drawable[] + { + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Y = 2, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding { Vertical = 20 }, + Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, + Children = new Drawable[] + { + ApplyButton = new CreateRoomButton + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(230, 55), + Enabled = { Value = false }, + Action = apply, + }, + ErrorText = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Alpha = 0, + Depth = 1, + Colour = colours.RedDark + } + } + } + } + } + } + } + }, + } + }, + loadingLayer = new LoadingLayer(dimContent) + }; + + TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true); + RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true); + Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true); + Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); + MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); + Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); + + playlist.Items.BindTo(Playlist); + Playlist.BindCollectionChanged(onPlaylistChanged, true); + } + + protected override void Update() + { + base.Update(); + + ApplyButton.Enabled.Value = hasValidSettings; + } + + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => + playlistLength.Text = $"Length: {Playlist.GetTotalDuration()}"; + + private bool hasValidSettings => RoomID.Value == null && NameField.Text.Length > 0 && Playlist.Count > 0; + + private void apply() + { + if (!ApplyButton.Enabled.Value) + return; + + hideError(); + + RoomName.Value = NameField.Text; + Availability.Value = AvailabilityPicker.Current.Value; + Type.Value = TypePicker.Current.Value; + + if (int.TryParse(MaxParticipantsField.Text, out int max)) + MaxParticipants.Value = max; + else + MaxParticipants.Value = null; + + Duration.Value = DurationField.Current.Value; + + manager?.CreateRoom(currentRoom.Value, onSuccess, onError); + + loadingLayer.Show(); + } + + private void hideError() => ErrorText.FadeOut(50); + + private void onSuccess(Room room) => loadingLayer.Hide(); + + private void onError(string text) + { + ErrorText.Text = text; + ErrorText.FadeIn(50); + + loadingLayer.Hide(); + } + } + + public class CreateRoomButton : TriangleButton + { + public CreateRoomButton() + { + Text = "Create"; + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + BackgroundColour = colours.Yellow; + Triangles.ColourLight = colours.YellowLight; + Triangles.ColourDark = colours.YellowDark; + } + } + + private class DurationDropdown : OsuDropdown + { + public DurationDropdown() + { + Menu.MaxHeight = 100; + } + + protected override string GenerateItemText(TimeSpan item) => item.Humanize(); + } + } +} From 34421c92327c00fb3574141271f6d6094f5f94da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 15:58:57 +0900 Subject: [PATCH 5596/6909] 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 2a08cb7867..fc01f9bf1d 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 960959f367..cbf9f6f1bd 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index a5bcb91c74..adbcc0ef1c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 3cf889b7c5c4d8f4e8382ab5245c30a551b1f7e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 16:19:19 +0900 Subject: [PATCH 5597/6909] Fix some errors being completely ignored --- osu.Game/Screens/Multi/Components/RoomManager.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 482ee5492c..f78d0d979e 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -76,10 +76,7 @@ namespace osu.Game.Screens.Multi.Components req.Failure += exception => { - if (req.Result != null) - onError?.Invoke(req.Result.Error); - else - Logger.Log($"Failed to create the room: {exception}", level: LogLevel.Important); + onError?.Invoke(req.Result?.Error ?? exception.Message); }; api.Queue(req); From 2d7174d99cc6e24b30f3266a32cd86fe9c735c34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 22 Dec 2020 16:23:06 +0900 Subject: [PATCH 5598/6909] Add padding to song select --- .../Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs index 157216e9bb..b2ae5402a1 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs @@ -5,6 +5,7 @@ using System.Linq; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; @@ -24,6 +25,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer [Resolved] private StatefulMultiplayerClient client { get; set; } + public RealtimeMatchSongSelect() + { + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; + } + protected override bool OnStart() { var item = new PlaylistItem(); From 12876d7fb642500d7c5db31827a718a0af7b1798 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 16:50:30 +0900 Subject: [PATCH 5599/6909] Add very basic error handling on ChangeSettings calls --- .../StatefulMultiplayerClient.cs | 6 +++--- .../Match/RealtimeMatchSettingsOverlay.cs | 9 +++++++-- .../RealtimeMultiplayer/RealtimeMatchSongSelect.cs | 13 +++++++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index f56499f040..4ebd648689 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -127,10 +127,10 @@ namespace osu.Game.Online.RealtimeMultiplayer /// /// The new room name, if any. /// The new room playlist item, if any. - public void ChangeSettings(Optional name = default, Optional item = default) + public Task ChangeSettings(Optional name = default, Optional item = default) { if (Room == null) - return; + throw new InvalidOperationException("Must be joined to a match to change settings."); // A dummy playlist item filled with the current room settings (except mods). var existingPlaylistItem = new PlaylistItem @@ -146,7 +146,7 @@ namespace osu.Game.Online.RealtimeMultiplayer RulesetID = Room.Settings.RulesetID }; - ChangeSettings(new MultiplayerRoomSettings + return ChangeSettings(new MultiplayerRoomSettings { Name = name.GetOr(Room.Settings.Name), BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs index 5bd388cceb..3e495b490f 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs @@ -294,8 +294,13 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation. if (client.Room != null) { - client.ChangeSettings(name: NameField.Text); - onSuccess(currentRoom.Value); + client.ChangeSettings(name: NameField.Text).ContinueWith(t => Schedule(() => + { + if (t.IsCompletedSuccessfully) + onSuccess(currentRoom.Value); + else + onError(t.Exception?.Message ?? "Error changing settings."); + })); } else { diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs index b2ae5402a1..0feeed6fe5 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs @@ -6,6 +6,7 @@ using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; @@ -43,14 +44,22 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer // If the client is already in a room, update via the client. // Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation. if (client.Room != null) - client.ChangeSettings(item: item); + { + client.ChangeSettings(item: item).ContinueWith(t => Schedule(() => + { + if (t.IsCompletedSuccessfully) + this.Exit(); + else + Logger.Log($"Could not use current beatmap ({t.Exception?.Message})", level: LogLevel.Important); + })); + } else { playlist.Clear(); playlist.Add(item); + this.Exit(); } - this.Exit(); return true; } From 30357a9447a077e09b7e0a11006fd48a156890a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 17:08:02 +0900 Subject: [PATCH 5600/6909] Add loading layer to multi song select to show during settings confirmation --- .../RealtimeMatchSongSelect.cs | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs index 0feeed6fe5..d4308d361c 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs @@ -2,12 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using System.Threading; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Select; @@ -26,11 +28,19 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer [Resolved] private StatefulMultiplayerClient client { get; set; } + private LoadingLayer loadingLayer; + public RealtimeMatchSongSelect() { Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; } + [BackgroundDependencyLoader] + private void load() + { + AddInternal(loadingLayer = new LoadingLayer(Carousel)); + } + protected override bool OnStart() { var item = new PlaylistItem(); @@ -45,13 +55,20 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer // Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation. if (client.Room != null) { - client.ChangeSettings(item: item).ContinueWith(t => Schedule(() => + loadingLayer.Show(); + + client.ChangeSettings(item: item).ContinueWith(t => { - if (t.IsCompletedSuccessfully) - this.Exit(); - else - Logger.Log($"Could not use current beatmap ({t.Exception?.Message})", level: LogLevel.Important); - })); + return Schedule(() => + { + loadingLayer.Hide(); + + if (t.IsCompletedSuccessfully) + this.Exit(); + else + Logger.Log($"Could not use current beatmap ({t.Exception?.Message})", level: LogLevel.Important); + }); + }); } else { From 59734229ff94bd907fd640374ce081c62581cb61 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 17:21:53 +0900 Subject: [PATCH 5601/6909] Remove unused using --- .../Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs index d4308d361c..f3dab93089 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; -using System.Threading; using Humanizer; using osu.Framework.Allocation; using osu.Framework.Bindables; From 3d5783a0eab3e1360dad85f9b6b5183c56467be9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 17:34:51 +0900 Subject: [PATCH 5602/6909] Improve variable names --- .../Sections/Graphics/LayoutSettings.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 6ec5d1f03a..b722fd4137 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -140,20 +140,20 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics windowModeDropdown.Hide(); }, true); - windowModeDropdown.Current.ValueChanged += v => updateResolutionDropdown(); + windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown(); - currentDisplay.BindValueChanged(v => Schedule(() => + currentDisplay.BindValueChanged(display => Schedule(() => { resolutions.RemoveRange(1, resolutions.Count - 1); - if (v.NewValue != null) + if (display.NewValue != null) { - resolutions.AddRange(v.NewValue.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()); + resolutions.AddRange(display.NewValue.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()); } updateResolutionDropdown(); From 4f02928601b75ed162b6527b728cce8d971182cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 17:36:56 +0900 Subject: [PATCH 5603/6909] Change sorting to better handle portrait screens --- .../Overlays/Settings/Sections/Graphics/LayoutSettings.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index b722fd4137..3d3b543d70 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.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.Drawing; using System.Linq; using osu.Framework.Allocation; @@ -150,8 +151,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics { resolutions.AddRange(display.NewValue.DisplayModes .Where(m => m.Size.Width >= 800 && m.Size.Height >= 600) - .OrderByDescending(m => m.Size.Width) - .ThenByDescending(m => m.Size.Height) + .OrderByDescending(m => Math.Max(m.Size.Height, m.Size.Width)) .Select(m => m.Size) .Distinct()); } From ce806dd880f58b03e18fdf80d76ce9f5c67c86e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 18:25:45 +0900 Subject: [PATCH 5604/6909] Replace the ready mark display with a state display, showing all participant states --- .../Multiplayer/TestSceneParticipantsList.cs | 1 + .../TestSceneParticipantsList.cs | 11 +- .../Participants/ParticipantPanel.cs | 10 +- .../Participants/ReadyMark.cs | 51 ------- .../Participants/StateDisplay.cs | 128 ++++++++++++++++++ 5 files changed, 138 insertions(+), 63 deletions(-) delete mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs index 7bbec7d30e..360aa22af9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs @@ -20,6 +20,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Room.RecentParticipants.Add(new User { Username = "peppy", + CurrentModeRank = 1234, Id = 2 }); } diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs index 8c997e9e32..35ea0d3813 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; using osu.Game.Users; @@ -65,13 +66,13 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer [Test] public void TestToggleReadyState() { - AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); + AddAssert("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Ready)); - AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); + AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); AddStep("make user idle", () => Client.ChangeState(MultiplayerUserState.Idle)); - AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); + AddUntilStep("ready mark invisible", () => !this.ChildrenOfType().Single().IsPresent); } [Test] @@ -104,11 +105,11 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer { Id = i, Username = $"User {i}", + CurrentModeRank = RNG.Next(1, 100000), CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); - if (i % 2 == 0) - Client.ChangeUserState(i, MultiplayerUserState.Ready); + Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results)); } }); } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs index 002849a275..a4ff2ce346 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs @@ -30,7 +30,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants [Resolved] private IAPIProvider api { get; set; } - private ReadyMark readyMark; + private StateDisplay userStateDisplay; private SpriteIcon crown; public ParticipantPanel(MultiplayerRoomUser user) @@ -122,12 +122,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants } } }, - readyMark = new ReadyMark + userStateDisplay = new StateDisplay { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding { Right = 10 }, - Alpha = 0 } } } @@ -144,10 +143,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants const double fade_time = 50; - if (User.State == MultiplayerUserState.Ready) - readyMark.FadeIn(fade_time); - else - readyMark.FadeOut(fade_time); + userStateDisplay.Status = User.State; if (Room.Host?.Equals(User) == true) crown.FadeIn(fade_time); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs deleted file mode 100644 index df49d9342e..0000000000 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ReadyMark.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Framework.Allocation; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osuTK; - -namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants -{ - public class ReadyMark : CompositeDrawable - { - public ReadyMark() - { - AutoSizeAxes = Axes.Both; - } - - [BackgroundDependencyLoader] - private void load() - { - InternalChild = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - Children = new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12), - Text = "ready", - Colour = Color4Extensions.FromHex("#DDFFFF") - }, - new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Icon = FontAwesome.Solid.CheckCircle, - Size = new Vector2(12), - Colour = Color4Extensions.FromHex("#AADD00") - } - } - }; - } - } -} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs new file mode 100644 index 0000000000..db93525217 --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.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 osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Online.RealtimeMultiplayer; +using osuTK; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +{ + public class StateDisplay : CompositeDrawable + { + public StateDisplay() + { + AutoSizeAxes = Axes.Both; + } + + private MultiplayerUserState status; + + private OsuSpriteText text; + private SpriteIcon icon; + + private const double fade_time = 50; + + public MultiplayerUserState Status + { + set + { + if (value == status) + return; + + status = value; + + if (IsLoaded) + updateStatus(); + } + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + Children = new Drawable[] + { + text = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12), + Colour = Color4Extensions.FromHex("#DDFFFF") + }, + icon = new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = FontAwesome.Solid.CheckCircle, + Size = new Vector2(12), + } + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + updateStatus(); + } + + [Resolved] + private OsuColour colours { get; set; } + + private void updateStatus() + { + switch (status) + { + default: + this.FadeOut(fade_time); + return; + + case MultiplayerUserState.Ready: + text.Text = "ready"; + icon.Icon = FontAwesome.Solid.CheckCircle; + icon.Colour = Color4Extensions.FromHex("#AADD00"); + break; + + case MultiplayerUserState.WaitingForLoad: + text.Text = "loading"; + icon.Icon = FontAwesome.Solid.PauseCircle; + icon.Colour = colours.Yellow; + break; + + case MultiplayerUserState.Loaded: + text.Text = "loaded"; + icon.Icon = FontAwesome.Solid.DotCircle; + icon.Colour = colours.YellowLight; + break; + + case MultiplayerUserState.Playing: + text.Text = "playing"; + icon.Icon = FontAwesome.Solid.PlayCircle; + icon.Colour = colours.BlueLight; + break; + + case MultiplayerUserState.FinishedPlay: + text.Text = "results pending"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; + icon.Colour = colours.BlueLighter; + break; + + case MultiplayerUserState.Results: + text.Text = "results"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; + icon.Colour = colours.BlueLighter; + break; + } + + this.FadeIn(fade_time); + } + } +} From 23bf9c372c71d15049595955c22bda7df3939dfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 18:26:39 +0900 Subject: [PATCH 5605/6909] Fix naming conflict with test scenes --- ...articipantsList.cs => TestSceneTimeshiftParticipantsList.cs} | 2 +- ...sList.cs => TestSceneRealtimeMultiplayerParticipantsList.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneParticipantsList.cs => TestSceneTimeshiftParticipantsList.cs} (95%) rename osu.Game.Tests/Visual/RealtimeMultiplayer/{TestSceneParticipantsList.cs => TestSceneRealtimeMultiplayerParticipantsList.cs} (97%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftParticipantsList.cs similarity index 95% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftParticipantsList.cs index 360aa22af9..efc3be032c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftParticipantsList.cs @@ -8,7 +8,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneParticipantsList : MultiplayerTestScene + public class TestSceneTimeshiftParticipantsList : MultiplayerTestScene { [SetUp] public new void Setup() => Schedule(() => diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs similarity index 97% rename from osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs rename to osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs index 35ea0d3813..7fd31906f7 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneParticipantsList.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Tests.Visual.RealtimeMultiplayer { - public class TestSceneParticipantsList : RealtimeMultiplayerTestScene + public class TestSceneRealtimeMultiplayerParticipantsList : RealtimeMultiplayerTestScene { [SetUp] public new void Setup() => Schedule(() => From 6517acc510885301d7bc0c3cf82d7638f7fc63cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 22 Dec 2020 19:09:59 +0900 Subject: [PATCH 5606/6909] Add leaderboard display to realtime player --- .../RealtimeMultiplayer/RealtimePlayer.cs | 23 +++++++++++++++++++ osu.Game/Screens/Play/HUDOverlay.cs | 7 +++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index c6d44686b5..453e7e6140 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -3,6 +3,7 @@ using System; using System.Diagnostics; +using System.Linq; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -12,7 +13,9 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Scoring; using osu.Game.Screens.Multi.Play; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; +using osuTK; namespace osu.Game.Screens.Multi.RealtimeMultiplayer { @@ -30,6 +33,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); private readonly ManualResetEventSlim startedEvent = new ManualResetEventSlim(); + private MultiplayerGameplayLeaderboard leaderboard; + public RealtimePlayer(PlaylistItem playlistItem) : base(playlistItem, false) { @@ -55,6 +60,24 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer this.Exit(); }); } + + Debug.Assert(client.Room != null); + + int[] userIds = client.Room.Users.Where(u => u.State >= MultiplayerUserState.WaitingForLoad).Select(u => u.UserID).ToArray(); + + // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); + } + + protected override void Update() + { + base.Update(); + + const float padding = 44; // enough margin to avoid the hit error display. + + leaderboard.Position = new Vector2( + padding, + padding + HUDOverlay.TopScoringElementsHeight); } private void onMatchStarted() => startedEvent.Set(); diff --git a/osu.Game/Screens/Play/HUDOverlay.cs b/osu.Game/Screens/Play/HUDOverlay.cs index 50195d571c..3dffab8102 100644 --- a/osu.Game/Screens/Play/HUDOverlay.cs +++ b/osu.Game/Screens/Play/HUDOverlay.cs @@ -28,6 +28,11 @@ namespace osu.Game.Screens.Play public const Easing FADE_EASING = Easing.Out; + /// + /// The total height of all the top of screen scoring elements. + /// + public float TopScoringElementsHeight { get; private set; } + public readonly KeyCounterDisplay KeyCounter; public readonly SkinnableComboCounter ComboCounter; public readonly SkinnableScoreCounter ScoreCounter; @@ -209,7 +214,7 @@ namespace osu.Game.Screens.Play // HACK: for now align with the accuracy counter. // this is done for the sake of hacky legacy skins which extend the health bar to take up the full screen area. // it only works with the default skin due to padding offsetting it *just enough* to coexist. - topRightElements.Y = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y; + topRightElements.Y = TopScoringElementsHeight = ToLocalSpace(AccuracyCounter.Drawable.ScreenSpaceDrawQuad.BottomRight).Y; bottomRightElements.Y = -Progress.Height; } From e3483147e2f682000121bd1534f494efbd42e6c8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Dec 2020 13:53:01 +0300 Subject: [PATCH 5607/6909] Move track looping logic into subscreens --- osu.Game/Screens/Multi/Match/RoomSubScreen.cs | 52 ++++++++++++++++++ osu.Game/Screens/Multi/Multiplayer.cs | 53 ------------------- 2 files changed, 52 insertions(+), 53 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs index 0cc9a4354e..21316a98ce 100644 --- a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; +using osu.Game.Overlays; using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.Multi.Match @@ -23,6 +24,9 @@ namespace osu.Game.Screens.Multi.Match [Resolved(typeof(Room), nameof(Room.Playlist))] protected BindableList Playlist { get; private set; } + [Resolved] + private MusicController music { get; set; } + [Resolved] private BeatmapManager beatmapManager { get; set; } @@ -61,6 +65,30 @@ namespace osu.Game.Screens.Multi.Match var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); + if (this.IsCurrentScreen()) + applyTrackLooping(); + } + + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + + music?.EnsurePlayingSomething(); + applyTrackLooping(); + } + + public override void OnSuspending(IScreen next) + { + cancelTrackLooping(); + base.OnSuspending(next); + } + + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + + music?.EnsurePlayingSomething(); + applyTrackLooping(); } public override bool OnExiting(IScreen next) @@ -68,7 +96,31 @@ namespace osu.Game.Screens.Multi.Match RoomManager?.PartRoom(); Mods.Value = Array.Empty(); + cancelTrackLooping(); + return base.OnExiting(next); } + + private void applyTrackLooping() + { + var track = Beatmap.Value?.Track; + + if (track != null) + { + track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; + track.Looping = true; + } + } + + private void cancelTrackLooping() + { + var track = Beatmap?.Value?.Track; + + if (track != null) + { + track.Looping = false; + track.RestartPoint = 0; + } + } } } diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index eae779421d..de2e0d58c9 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; @@ -198,8 +197,6 @@ namespace osu.Game.Screens.Multi { this.FadeIn(); waves.Show(); - - beginHandlingTrack(); } public override void OnResuming(IScreen last) @@ -209,8 +206,6 @@ namespace osu.Game.Screens.Multi base.OnResuming(last); - beginHandlingTrack(); - UpdatePollingRate(isIdle.Value); } @@ -219,8 +214,6 @@ namespace osu.Game.Screens.Multi this.ScaleTo(1.1f, 250, Easing.InSine); this.FadeOut(250); - endHandlingTrack(); - UpdatePollingRate(isIdle.Value); } @@ -235,8 +228,6 @@ namespace osu.Game.Screens.Multi if (screenStack.CurrentScreen != null) loungeSubScreen.MakeCurrent(); - endHandlingTrack(); - base.OnExiting(next); return false; } @@ -275,17 +266,6 @@ namespace osu.Game.Screens.Multi /// The created . protected virtual Room CreateNewRoom() => new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } }; - private void beginHandlingTrack() - { - Beatmap.BindValueChanged(updateTrack, true); - } - - private void endHandlingTrack() - { - cancelLooping(); - Beatmap.ValueChanged -= updateTrack; - } - private void screenPushed(IScreen lastScreen, IScreen newScreen) { subScreenChanged(lastScreen, newScreen); @@ -322,43 +302,10 @@ namespace osu.Game.Screens.Multi UpdatePollingRate(isIdle.Value); createButton.FadeTo(newScreen is LoungeSubScreen ? 1 : 0, 200); - - updateTrack(); } protected IScreen CurrentSubScreen => screenStack.CurrentScreen; - private void updateTrack(ValueChangedEvent _ = null) - { - if (screenStack.CurrentScreen is RoomSubScreen) - { - var track = Beatmap.Value?.Track; - - if (track != null) - { - track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - track.Looping = true; - - music?.EnsurePlayingSomething(); - } - } - else - { - cancelLooping(); - } - } - - private void cancelLooping() - { - var track = Beatmap?.Value?.Track; - - if (track != null) - { - track.Looping = false; - track.RestartPoint = 0; - } - } - protected abstract RoomManager CreateRoomManager(); protected abstract LoungeSubScreen CreateLounge(); From 91d5c53643a7489066a23b36e6865d67c621ddf2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Dec 2020 16:36:17 +0300 Subject: [PATCH 5608/6909] Add method for checking room joinability --- osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs | 2 ++ .../Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs | 2 ++ .../Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs | 2 ++ osu.Game/Screens/Multi/Components/RoomManager.cs | 2 ++ osu.Game/Screens/Multi/IRoomManager.cs | 6 ++++++ .../Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 3 +++ 6 files changed, 17 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs index 9dd4aea4bd..ea8fb4ef49 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs @@ -24,6 +24,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room); + public bool CanJoinRoom(Room room) => true; + public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) { } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index 1fcae9c709..55dbd1a7c6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -146,6 +146,8 @@ namespace osu.Game.Tests.Visual.Multiplayer onSuccess?.Invoke(room); } + public bool CanJoinRoom(Room room) => true; + public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => throw new NotImplementedException(); public void PartRoom() => throw new NotImplementedException(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs index bbd7d84081..8fdb5d4093 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs @@ -161,6 +161,8 @@ namespace osu.Game.Tests.Visual.Multiplayer onSuccess?.Invoke(room); } + public bool CanJoinRoom(Room room) => true; + public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => onSuccess?.Invoke(room); public void PartRoom() diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index f78d0d979e..3fcfe1df25 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -82,6 +82,8 @@ namespace osu.Game.Screens.Multi.Components api.Queue(req); } + public virtual bool CanJoinRoom(Room room) => true; + private JoinRoomRequest currentJoinRoomRequest; public virtual void JoinRoom(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 630e3af91c..a1cff129a9 100644 --- a/osu.Game/Screens/Multi/IRoomManager.cs +++ b/osu.Game/Screens/Multi/IRoomManager.cs @@ -34,6 +34,12 @@ namespace osu.Game.Screens.Multi /// An action to be invoked if an error occurred. void CreateRoom(Room room, Action onSuccess = null, Action onError = null); + /// + /// Whether the provided can be joined. + /// + /// The to check for. + bool CanJoinRoom(Room room); + /// /// Joins a . /// diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 7ce031e0e9..d8dc5f127a 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Multi.Components; @@ -40,6 +41,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); + public override bool CanJoinRoom(Room room) => !(room.Status.Value is RoomStatusEnded); + public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); From a64ffcd2949ca3f47c0bc3019de9596dc28e79d5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 22 Dec 2020 16:38:10 +0300 Subject: [PATCH 5609/6909] Refrain from joining room if not allowed --- osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index c7c37cbc0d..c1f69b779c 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -115,7 +115,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components { if (room == selectedRoom.Value) { - joinSelected(); + if (roomManager.CanJoinRoom(room)) + joinSelected(); + return; } From 668f89d8b230103d8daee7fd639fde069baa4d99 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 22 Dec 2020 17:33:11 +0200 Subject: [PATCH 5610/6909] Copy test from #11019 --- .../TestSceneSectionsContainer.cs | 122 ++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs new file mode 100644 index 0000000000..5c2e6e457d --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneSectionsContainer.cs @@ -0,0 +1,122 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Colour; +using osu.Framework.Graphics.Shapes; +using osu.Game.Graphics.Containers; +using osuTK.Graphics; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneSectionsContainer : OsuManualInputManagerTestScene + { + private readonly SectionsContainer container; + private float custom; + private const float header_height = 100; + + public TestSceneSectionsContainer() + { + container = new SectionsContainer + { + RelativeSizeAxes = Axes.Y, + Width = 300, + Origin = Anchor.Centre, + Anchor = Anchor.Centre, + FixedHeader = new Box + { + Alpha = 0.5f, + Width = 300, + Height = header_height, + Colour = Color4.Red + } + }; + container.SelectedSection.ValueChanged += section => + { + if (section.OldValue != null) + section.OldValue.Selected = false; + if (section.NewValue != null) + section.NewValue.Selected = true; + }; + Add(container); + } + + [Test] + public void TestSelection() + { + AddStep("clear", () => container.Clear()); + AddStep("add 1/8th", () => append(1 / 8.0f)); + AddStep("add third", () => append(1 / 3.0f)); + AddStep("add half", () => append(1 / 2.0f)); + AddStep("add full", () => append(1)); + AddSliderStep("set custom", 0.1f, 1.1f, 0.5f, i => custom = i); + AddStep("add custom", () => append(custom)); + AddStep("scroll to previous", () => container.ScrollTo( + container.Children.Reverse().SkipWhile(s => s != container.SelectedSection.Value).Skip(1).FirstOrDefault() ?? container.Children.First() + )); + AddStep("scroll to next", () => container.ScrollTo( + container.Children.SkipWhile(s => s != container.SelectedSection.Value).Skip(1).FirstOrDefault() ?? container.Children.Last() + )); + AddStep("scroll up", () => triggerUserScroll(1)); + AddStep("scroll down", () => triggerUserScroll(-1)); + } + + [Test] + public void TestCorrectSectionSelected() + { + const int sections_count = 11; + float[] alternating = { 0.07f, 0.33f, 0.16f, 0.33f }; + AddStep("clear", () => container.Clear()); + AddStep("fill with sections", () => + { + for (int i = 0; i < sections_count; i++) + append(alternating[i % alternating.Length]); + }); + + void step(int scrollIndex) + { + AddStep($"scroll to section {scrollIndex + 1}", () => container.ScrollTo(container.Children[scrollIndex])); + AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[scrollIndex]); + } + + for (int i = 1; i < sections_count; i++) + step(i); + for (int i = sections_count - 2; i >= 0; i--) + step(i); + + AddStep("scroll almost to end", () => container.ScrollTo(container.Children[sections_count - 2])); + AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[sections_count - 2]); + AddStep("scroll down", () => triggerUserScroll(-1)); + AddUntilStep("correct section selected", () => container.SelectedSection.Value == container.Children[sections_count - 1]); + } + + private static readonly ColourInfo selected_colour = ColourInfo.GradientVertical(Color4.Yellow, Color4.Gold); + private static readonly ColourInfo default_colour = ColourInfo.GradientVertical(Color4.White, Color4.DarkGray); + + private void append(float multiplier) + { + container.Add(new TestSection + { + Width = 300, + Height = (container.ChildSize.Y - header_height) * multiplier, + Colour = default_colour + }); + } + + private void triggerUserScroll(float direction) + { + InputManager.MoveMouseTo(container); + InputManager.ScrollVerticalBy(direction); + } + + private class TestSection : Box + { + public bool Selected + { + set => Colour = value ? selected_colour : default_colour; + } + } + } +} From 78c14fd69693f688f40595699eba849486f49463 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 22 Dec 2020 17:36:44 +0200 Subject: [PATCH 5611/6909] Refactor code into UserTrackingScrollContainer --- .../Graphics/Containers/SectionsContainer.cs | 4 +- .../Containers/UserTrackingScrollContainer.cs | 49 +++++++++++++++++++ osu.Game/Overlays/OverlayScrollContainer.cs | 4 +- osu.Game/Overlays/UserProfileOverlay.cs | 2 +- osu.Game/Screens/Select/BeatmapCarousel.cs | 20 +------- 5 files changed, 55 insertions(+), 24 deletions(-) create mode 100644 osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 81968de304..6e9520ef8f 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -95,7 +95,7 @@ namespace osu.Game.Graphics.Containers protected override Container Content => scrollContentContainer; - private readonly OsuScrollContainer scrollContainer; + private readonly UserTrackingScrollContainer scrollContainer; private readonly Container headerBackgroundContainer; private readonly MarginPadding originalSectionsMargin; private Drawable expandableHeader, fixedHeader, footer, headerBackground; @@ -139,7 +139,7 @@ namespace osu.Game.Graphics.Containers public void ScrollToTop() => scrollContainer.ScrollTo(0); [NotNull] - protected virtual OsuScrollContainer CreateScrollContainer() => new OsuScrollContainer(); + protected virtual UserTrackingScrollContainer CreateScrollContainer() => new UserTrackingScrollContainer(); [NotNull] protected virtual FlowContainer CreateScrollContentContainer() => diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs new file mode 100644 index 0000000000..b8ce34b204 --- /dev/null +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.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.Graphics; + +namespace osu.Game.Graphics.Containers +{ + public class UserTrackingScrollContainer : UserTrackingScrollContainer + { + public UserTrackingScrollContainer() + { + } + + public UserTrackingScrollContainer(Direction direction) + : base(direction) + { + } + } + + public class UserTrackingScrollContainer : OsuScrollContainer + where T : Drawable + { + /// + /// Whether the last scroll event was user triggered, directly on the scroll container. + /// + public bool UserScrolling { get; private set; } + + public UserTrackingScrollContainer() + { + } + + public UserTrackingScrollContainer(Direction direction) + : base(direction) + { + } + + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) + { + 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); + } + } +} diff --git a/osu.Game/Overlays/OverlayScrollContainer.cs b/osu.Game/Overlays/OverlayScrollContainer.cs index b67d5db1a4..0004719b87 100644 --- a/osu.Game/Overlays/OverlayScrollContainer.cs +++ b/osu.Game/Overlays/OverlayScrollContainer.cs @@ -17,9 +17,9 @@ using osuTK.Graphics; namespace osu.Game.Overlays { /// - /// which provides . Mostly used in . + /// which provides . Mostly used in . /// - public class OverlayScrollContainer : OsuScrollContainer + public class OverlayScrollContainer : UserTrackingScrollContainer { /// /// Scroll position at which the will be shown. diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 81027667fa..7f29545c2e 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -202,7 +202,7 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.Both; } - protected override OsuScrollContainer CreateScrollContainer() => new OverlayScrollContainer(); + protected override UserTrackingScrollContainer CreateScrollContainer() => new OverlayScrollContainer(); protected override FlowContainer CreateScrollContentContainer() => new FillFlowContainer { diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index d76f0abb9e..e9a8d28316 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -901,15 +901,10 @@ namespace osu.Game.Screens.Select } } - protected class CarouselScrollContainer : OsuScrollContainer + protected class CarouselScrollContainer : UserTrackingScrollContainer { private bool rightMouseScrollBlocked; - /// - /// Whether the last scroll event was user triggered, directly on the scroll container. - /// - public bool UserScrolling { get; private set; } - public CarouselScrollContainer() { // size is determined by the carousel itself, due to not all content necessarily being loaded. @@ -919,19 +914,6 @@ namespace osu.Game.Screens.Select Masking = false; } - // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) - protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) - { - 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 2cf76ebc75c6e0dc4bfbe428a9ee099c21a5eeb9 Mon Sep 17 00:00:00 2001 From: Endrik Tombak Date: Tue, 22 Dec 2020 17:51:12 +0200 Subject: [PATCH 5612/6909] Scroll to 20% and select section intersecting below there --- .../Graphics/Containers/SectionsContainer.cs | 43 +++++++++++++++---- 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 6e9520ef8f..b5f81c516a 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Layout; +using osu.Framework.Utils; namespace osu.Game.Graphics.Containers { @@ -20,6 +21,8 @@ namespace osu.Game.Graphics.Containers where T : Drawable { public Bindable SelectedSection { get; } = new Bindable(); + private Drawable lastClickedSection; + private T smallestSection; public Drawable ExpandableHeader { @@ -131,10 +134,21 @@ namespace osu.Game.Graphics.Containers lastKnownScroll = float.NaN; headerHeight = float.NaN; footerHeight = float.NaN; + + if (drawable == null) + return; + + if (smallestSection == null || smallestSection.Height > drawable.Height) + smallestSection = drawable; } - public void ScrollTo(Drawable section) => - scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - (FixedHeader?.BoundingBox.Height ?? 0)); + private const float scroll_target_multiplier = 0.2f; + + public void ScrollTo(Drawable section) + { + lastClickedSection = section; + scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - scrollContainer.DisplayableContent * scroll_target_multiplier - (FixedHeader?.BoundingBox.Height ?? 0)); + } public void ScrollToTop() => scrollContainer.ScrollTo(0); @@ -183,6 +197,10 @@ namespace osu.Game.Graphics.Containers { lastKnownScroll = currentScroll; + // reset last clicked section because user started scrolling themselves + if (scrollContainer.UserScrolling) + lastClickedSection = null; + if (ExpandableHeader != null && FixedHeader != null) { float offset = Math.Min(ExpandableHeader.LayoutSize.Y, currentScroll); @@ -194,18 +212,27 @@ namespace osu.Game.Graphics.Containers headerBackgroundContainer.Height = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0; - float scrollOffset = FixedHeader?.LayoutSize.Y ?? 0; + // scroll offset is our fixed header height if we have it plus 20% of content height + // plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards + // but the 5% can't be bigger than our smallest section height, otherwise it won't get selected correctly + float sectionOrContent = Math.Min(smallestSection?.Height / 2.0f ?? 0, scrollContainer.DisplayableContent * 0.05f); + float scrollOffset = (FixedHeader?.LayoutSize.Y ?? 0) + scrollContainer.DisplayableContent * scroll_target_multiplier + sectionOrContent; Func diff = section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset; - if (scrollContainer.IsScrolledToEnd()) + if (Precision.AlmostBigger(0, scrollContainer.Current)) { - SelectedSection.Value = Children.LastOrDefault(); + SelectedSection.Value = lastClickedSection as T ?? Children.FirstOrDefault(); + return; } - else + + if (Precision.AlmostBigger(scrollContainer.Current, scrollContainer.ScrollableExtent)) { - SelectedSection.Value = Children.TakeWhile(section => diff(section) <= 0).LastOrDefault() - ?? Children.FirstOrDefault(); + SelectedSection.Value = lastClickedSection as T ?? Children.LastOrDefault(); + return; } + + SelectedSection.Value = Children.TakeWhile(section => diff(section) <= 0).LastOrDefault() + ?? Children.FirstOrDefault(); } } From 5efc3b94961cf918fa96b54818382e89b2cd9faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Dec 2020 21:12:28 +0100 Subject: [PATCH 5613/6909] Start state display as hidden Would otherwise flicker for a few frames when a new user was added to the list of participants. --- .../Multi/RealtimeMultiplayer/Participants/StateDisplay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs index db93525217..844f239363 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs @@ -18,6 +18,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants public StateDisplay() { AutoSizeAxes = Axes.Both; + Alpha = 0; } private MultiplayerUserState status; From 4a677ecc190f0ec7b354db5f2436349353cd9f2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Dec 2020 21:16:45 +0100 Subject: [PATCH 5614/6909] Make random state choice in test more robust `RNG.Next(int, int)` is max-exclusive, so the random state choice would actually never pick `MultiplayerUserState.Results` on its own. The only reason why that state ever did show up was by a freak accident of sorts (the logic in `TestRealtimeMultiplayerClient` would automatically convert every `FinishedPlay` state to `Results`, up until seeing the first player that was in the `Playing` state). --- .../TestSceneRealtimeMultiplayerParticipantsList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs index 7fd31906f7..4221821496 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); - Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results)); + Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1)); } }); } From 32728047044a426d2c076cc143d836412d53b7a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 22 Dec 2020 22:31:40 +0100 Subject: [PATCH 5615/6909] Fix potential crash when no submission token Can happen because `TimeshiftPlayer` will schedule a screen exit on token retrieval failure, and `RealtimePlayer`'s BDL won't even attempt to create a leaderboard in that case. --- .../Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index 453e7e6140..7824b414f2 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Screens; @@ -33,6 +34,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); private readonly ManualResetEventSlim startedEvent = new ManualResetEventSlim(); + [CanBeNull] private MultiplayerGameplayLeaderboard leaderboard; public RealtimePlayer(PlaylistItem playlistItem) @@ -72,6 +74,13 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override void Update() { base.Update(); + adjustLeaderboardPosition(); + } + + private void adjustLeaderboardPosition() + { + if (leaderboard == null) + return; const float padding = 44; // enough margin to avoid the hit error display. From 7751ef4f3eadf4fa4583e3815bc6f3a95c5c354b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 23 Dec 2020 05:49:18 +0300 Subject: [PATCH 5616/6909] Revert previous logic of join guarding --- osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs | 2 -- .../Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs | 2 -- .../Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs | 2 -- osu.Game/Screens/Multi/Components/RoomManager.cs | 2 -- osu.Game/Screens/Multi/IRoomManager.cs | 6 ------ osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs | 4 +--- .../Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 3 --- 7 files changed, 1 insertion(+), 20 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs index ea8fb4ef49..9dd4aea4bd 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs @@ -24,8 +24,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => Rooms.Add(room); - public bool CanJoinRoom(Room room) => true; - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) { } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index 55dbd1a7c6..1fcae9c709 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -146,8 +146,6 @@ namespace osu.Game.Tests.Visual.Multiplayer onSuccess?.Invoke(room); } - public bool CanJoinRoom(Room room) => true; - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => throw new NotImplementedException(); public void PartRoom() => throw new NotImplementedException(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs index 8fdb5d4093..bbd7d84081 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs @@ -161,8 +161,6 @@ namespace osu.Game.Tests.Visual.Multiplayer onSuccess?.Invoke(room); } - public bool CanJoinRoom(Room room) => true; - public void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => onSuccess?.Invoke(room); public void PartRoom() diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index 3fcfe1df25..f78d0d979e 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -82,8 +82,6 @@ namespace osu.Game.Screens.Multi.Components api.Queue(req); } - public virtual bool CanJoinRoom(Room room) => true; - private JoinRoomRequest currentJoinRoomRequest; public virtual void JoinRoom(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 a1cff129a9..630e3af91c 100644 --- a/osu.Game/Screens/Multi/IRoomManager.cs +++ b/osu.Game/Screens/Multi/IRoomManager.cs @@ -34,12 +34,6 @@ namespace osu.Game.Screens.Multi /// An action to be invoked if an error occurred. void CreateRoom(Room room, Action onSuccess = null, Action onError = null); - /// - /// Whether the provided can be joined. - /// - /// The to check for. - bool CanJoinRoom(Room room); - /// /// Joins a . /// diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index c1f69b779c..c7c37cbc0d 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -115,9 +115,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components { if (room == selectedRoom.Value) { - if (roomManager.CanJoinRoom(room)) - joinSelected(); - + joinSelected(); return; } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index d8dc5f127a..7ce031e0e9 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Multi.Components; @@ -41,8 +40,6 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); - public override bool CanJoinRoom(Room room) => !(room.Status.Value is RoomStatusEnded); - public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); From 3aa2b228380ccc8a2ce3e9043c5dcd43a80a6b97 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 23 Dec 2020 05:52:10 +0300 Subject: [PATCH 5617/6909] Add early check for room status before joining --- .../Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 7ce031e0e9..484d5cce0b 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Multi.Components; @@ -41,7 +42,15 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer => base.CreateRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) - => base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); + { + if (room.Status.Value is RoomStatusEnded) + { + onError?.Invoke("Cannot join an ended room."); + return; + } + + base.JoinRoom(room, r => joinMultiplayerRoom(r, onSuccess, onError), onError); + } public override void PartRoom() { From 45dcd3242db7cc84753577d4de75ba571e75deb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 13:57:48 +0900 Subject: [PATCH 5618/6909] Add comment explaining why things are done where they are --- .../Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 484d5cce0b..f982574eb3 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -43,6 +43,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) { + // this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join. + // should probably be done at a higher level, but due to the current structure of things this is the easiest place for now. if (room.Status.Value is RoomStatusEnded) { onError?.Invoke("Cannot join an ended room."); From be427a4ec0fd7f30189f3b37941844483ad95d62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 14:20:35 +0900 Subject: [PATCH 5619/6909] Fix realtime leaderboard showing accuracy based on final base score, not rolling --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 23 ++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 10d0cc2865..4b2e2bf715 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -233,7 +233,7 @@ namespace osu.Game.Rulesets.Scoring } /// - /// Given a minimal set of inputs, return the computed score and accuracy for the tracked beatmap / mods combination. + /// Given a minimal set of inputs, return the computed score and accuracy for the tracked beatmap / mods combination, at the current point in time. /// /// The to compute the total score in. /// The maximum combo achievable in the beatmap. @@ -252,15 +252,28 @@ namespace osu.Game.Rulesets.Scoring computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; } - double accuracy = calculateAccuracyRatio(computedBaseScore); + double pointInTimeAccuracy = calculateAccuracyRatio(computedBaseScore, true); double comboRatio = calculateComboRatio(maxCombo); - double score = GetScore(mode, maxAchievableCombo, accuracy, comboRatio, scoreResultCounts); + double score = GetScore(mode, maxAchievableCombo, calculateAccuracyRatio(computedBaseScore), comboRatio, scoreResultCounts); - return (score, accuracy); + return (score, pointInTimeAccuracy); + } + + /// + /// Get the accuracy fraction for the provided base score. + /// + /// The score to be used for accuracy calculation. + /// Whether the rolling base score should be used (ie. for the current point in time based on Apply/Reverted results). + /// The computed accuracy. + private double calculateAccuracyRatio(double baseScore, bool preferRolling = false) + { + if (preferRolling && rollingMaxBaseScore != 0) + return baseScore / rollingMaxBaseScore; + + return maxBaseScore > 0 ? baseScore / maxBaseScore : 0; } - private double calculateAccuracyRatio(double baseScore) => maxBaseScore > 0 ? baseScore / maxBaseScore : 0; private double calculateComboRatio(int maxCombo) => maxAchievableCombo > 0 ? (double)maxCombo / maxAchievableCombo : 1; private double getBonusScore(Dictionary statistics) From dec997c0f49687800b9c90641299d38b4c9e060d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 14:44:20 +0900 Subject: [PATCH 5620/6909] Fix flashlight not updating its position during replay rewinding Closes #11260 --- osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index ac20407ed2..3f770cfb5e 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Mods var destination = e.MousePosition; FlashlightPosition = Interpolation.ValueAt( - Math.Clamp(Clock.ElapsedFrameTime, 0, follow_delay), position, destination, 0, follow_delay, Easing.Out); + Math.Clamp(Math.Abs(Clock.ElapsedFrameTime), 0, follow_delay), position, destination, 0, follow_delay, Easing.Out); return base.OnMouseMove(e); } From 286884421d74d8cd565533bdd723448b88416dc3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 23 Dec 2020 08:47:34 +0300 Subject: [PATCH 5621/6909] Apply track looping and play on track change --- osu.Game/Screens/Multi/Match/RoomSubScreen.cs | 72 ++++++++++--------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs index 21316a98ce..4f5d2a5b3e 100644 --- a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs @@ -41,6 +41,37 @@ namespace osu.Game.Screens.Multi.Match managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); + + if (music != null) + music.TrackChanged += applyToTrack; + } + + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + applyToTrack(); + } + + public override void OnSuspending(IScreen next) + { + resetTrack(); + base.OnSuspending(next); + } + + public override void OnResuming(IScreen last) + { + base.OnResuming(last); + applyToTrack(); + } + + public override bool OnExiting(IScreen next) + { + RoomManager?.PartRoom(); + Mods.Value = Array.Empty(); + + resetTrack(); + + return base.OnExiting(next); } private void selectedItemChanged() @@ -65,54 +96,25 @@ namespace osu.Game.Screens.Multi.Match var localBeatmap = beatmap == null ? null : beatmapManager.QueryBeatmap(b => b.OnlineBeatmapID == beatmap.OnlineBeatmapID); Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); - if (this.IsCurrentScreen()) - applyTrackLooping(); } - public override void OnEntering(IScreen last) + private void applyToTrack(WorkingBeatmap _ = default, TrackChangeDirection __ = default) { - base.OnEntering(last); + if (!this.IsCurrentScreen()) + return; - music?.EnsurePlayingSomething(); - applyTrackLooping(); - } - - public override void OnSuspending(IScreen next) - { - cancelTrackLooping(); - base.OnSuspending(next); - } - - public override void OnResuming(IScreen last) - { - base.OnResuming(last); - - music?.EnsurePlayingSomething(); - applyTrackLooping(); - } - - public override bool OnExiting(IScreen next) - { - RoomManager?.PartRoom(); - Mods.Value = Array.Empty(); - - cancelTrackLooping(); - - return base.OnExiting(next); - } - - private void applyTrackLooping() - { var track = Beatmap.Value?.Track; if (track != null) { track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; track.Looping = true; + + music?.EnsurePlayingSomething(); } } - private void cancelTrackLooping() + private void resetTrack() { var track = Beatmap?.Value?.Track; From 00d50150de6e39cbdff44e7f5a98c41a3c57b6a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 15:49:22 +0900 Subject: [PATCH 5622/6909] Ensure the current room is left at a mutliplayer client level on client disconnection --- .../RealtimeMultiplayer/RealtimeMultiplayerClient.cs | 4 ++++ .../RealtimeMultiplayer/StatefulMultiplayerClient.cs | 10 ++++++++++ 2 files changed, 14 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs index 75bb578a29..5cbf3be8ca 100644 --- a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs @@ -130,7 +130,11 @@ namespace osu.Game.Online.RealtimeMultiplayer public override async Task LeaveRoom() { if (!isConnected.Value) + { + // even if not connected, make sure the local room state can be cleaned up. + await base.LeaveRoom(); return; + } if (Room == null) return; diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 4ebd648689..9680387fcc 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -75,6 +75,16 @@ namespace osu.Game.Online.RealtimeMultiplayer // Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise. private int playlistItemId; + protected StatefulMultiplayerClient() + { + IsConnected.BindValueChanged(connected => + { + // clean up local room state on server disconnect. + if (!connected.NewValue) + LeaveRoom(); + }); + } + /// /// Joins the for a given API . /// From 12df3056e6d27d2da5d29a172708e849162eaa3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 15:58:50 +0900 Subject: [PATCH 5623/6909] Ensure appropriate screens handle exiting when the server gets disconnected I would have liked for this to be handled via the `OnRoomChanged` event flow, but this isn't present in RealtimeMatchSubScreen due to inheritence woes. --- .../RealtimeMultiplayer/RealtimeMatchSubScreen.cs | 14 ++++++++++++++ .../Multi/RealtimeMultiplayer/RealtimePlayer.cs | 12 ++++++++++++ 2 files changed, 26 insertions(+) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs index cdab1435c0..8405fc196b 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs @@ -4,8 +4,10 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; @@ -34,6 +36,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private RealtimeMatchSettingsOverlay settingsOverlay; + private IBindable isConnected; + public RealtimeMatchSubScreen(Room room) { Title = room.RoomID.Value == null ? "New match" : room.Name.Value; @@ -173,6 +177,16 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer Playlist.BindCollectionChanged(onPlaylistChanged, true); client.LoadRequested += onLoadRequested; + + isConnected = client.IsConnected.GetBoundCopy(); + isConnected.BindValueChanged(connected => + { + if (!connected.NewValue) + { + Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important); + Schedule(this.Exit); + } + }, true); } public override bool OnBackButton() diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index c6d44686b5..d74ccdd32f 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; @@ -30,6 +31,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); private readonly ManualResetEventSlim startedEvent = new ManualResetEventSlim(); + private IBindable isConnected; + public RealtimePlayer(PlaylistItem playlistItem) : base(playlistItem, false) { @@ -43,6 +46,15 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer client.MatchStarted += onMatchStarted; client.ResultsReady += onResultsReady; + + isConnected = client.IsConnected.GetBoundCopy(); + isConnected.BindValueChanged(connected => + { + if (!connected.NewValue) + // messaging to the user about this disconnect will be provided by the RealtimeMatchSubScreen. + Schedule(this.Exit); + }, true); + client.ChangeState(MultiplayerUserState.Loaded); if (!startedEvent.Wait(TimeSpan.FromSeconds(30))) From a1d42dc4a061a8fb2f50064c485cbd3cf07186be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 16:17:55 +0900 Subject: [PATCH 5624/6909] Don't allow creating or joining a room when not connected to server --- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 2 +- .../RealtimeLoungeSubScreen.cs | 17 +++++++++++++++++ .../RealtimeMultiplayer/RealtimeRoomManager.cs | 6 ++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index 44c893363b..6b08745dd7 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -184,7 +184,7 @@ namespace osu.Game.Screens.Multi.Lounge /// /// Push a room as a new subscreen. /// - public void Open(Room room) + public virtual void Open(Room room) { // Handles the case where a room is clicked 3 times in quick succession if (!this.IsCurrentScreen()) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs index 9fbf0c4654..b53ec94519 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.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 osu.Framework.Allocation; +using osu.Framework.Logging; using osu.Game.Online.Multiplayer; +using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Match; @@ -13,5 +16,19 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override FilterControl CreateFilterControl() => new RealtimeFilterControl(); protected override RoomSubScreen CreateRoomSubScreen(Room room) => new RealtimeMatchSubScreen(room); + + [Resolved] + private StatefulMultiplayerClient client { get; set; } + + public override void Open(Room room) + { + if (!client.IsConnected.Value) + { + Logger.Log("Not currently connected to the multiplayer server.", LoggingTarget.Runtime, LogLevel.Important); + return; + } + + base.Open(room); + } } } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index f982574eb3..2f60f504de 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -43,6 +43,12 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public override void JoinRoom(Room room, Action onSuccess = null, Action onError = null) { + if (!multiplayerClient.IsConnected.Value) + { + onError?.Invoke("Not currently connected to the multiplayer server."); + return; + } + // this is done here as a pre-check to avoid clicking on already closed rooms in the lounge from triggering a server join. // should probably be done at a higher level, but due to the current structure of things this is the easiest place for now. if (room.Status.Value is RoomStatusEnded) From 569c4092efe42a55caabc5a08c7f7bc227f3b1e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 16:19:03 +0900 Subject: [PATCH 5625/6909] Move notification to stateful client so it is only shown to the user from one location --- .../RealtimeMultiplayer/StatefulMultiplayerClient.cs | 4 ++++ .../Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs | 7 ++----- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 9680387fcc..79d82a8d02 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -11,6 +11,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; using osu.Framework.Graphics; +using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; @@ -81,7 +82,10 @@ namespace osu.Game.Online.RealtimeMultiplayer { // clean up local room state on server disconnect. if (!connected.NewValue) + { + Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important); LeaveRoom(); + } }); } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs index 8405fc196b..807ea74404 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; @@ -18,6 +17,7 @@ using osu.Game.Screens.Multi.RealtimeMultiplayer.Match; using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; using osu.Game.Screens.Play; using osu.Game.Users; +using ParticipantsList = osu.Game.Screens.Multi.RealtimeMultiplayer.Participants.ParticipantsList; namespace osu.Game.Screens.Multi.RealtimeMultiplayer { @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer new Drawable[] { new ParticipantsListHeader() }, new Drawable[] { - new Participants.ParticipantsList + new ParticipantsList { RelativeSizeAxes = Axes.Both }, @@ -182,10 +182,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer isConnected.BindValueChanged(connected => { if (!connected.NewValue) - { - Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important); Schedule(this.Exit); - } }, true); } From f5d27b40a8f5c9dd8b3e849360425ed540b79c39 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 16:32:58 +0900 Subject: [PATCH 5626/6909] Standardise flow for aborting realtime player exit to avoid double-exit call --- .../Multi/RealtimeMultiplayer/RealtimePlayer.cs | 12 ++++++------ osu.Game/Screens/Play/Player.cs | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index d74ccdd32f..edec40890e 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -51,8 +51,12 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer isConnected.BindValueChanged(connected => { if (!connected.NewValue) + { + startedEvent.Set(); + // messaging to the user about this disconnect will be provided by the RealtimeMatchSubScreen. - Schedule(this.Exit); + Schedule(PerformImmediateExit); + } }, true); client.ChangeState(MultiplayerUserState.Loaded); @@ -61,11 +65,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { Logger.Log("Failed to start the multiplayer match in time.", LoggingTarget.Runtime, LogLevel.Important); - Schedule(() => - { - ValidForResume = false; - this.Exit(); - }); + Schedule(PerformImmediateExit); } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c539dff5d9..c6265c48d2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -386,7 +386,7 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; fadeOut(true); - performImmediateExit(); + PerformImmediateExit(); }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, @@ -458,7 +458,7 @@ namespace osu.Game.Screens.Play return playable; } - private void performImmediateExit() + protected void PerformImmediateExit() { // if a restart has been requested, cancel any pending completion (user has shown intent to restart). completionProgressDelegate?.Cancel(); @@ -498,7 +498,7 @@ namespace osu.Game.Screens.Play RestartRequested?.Invoke(); if (this.IsCurrentScreen()) - performImmediateExit(); + PerformImmediateExit(); else this.MakeCurrent(); } From 91021eb8c45b0826bf0503cc9d5d4fac822fe005 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 16:49:17 +0900 Subject: [PATCH 5627/6909] Remove unused using --- osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index edec40890e..9e2ba9b04a 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -8,7 +8,6 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; -using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Scoring; From d27b83d678bb4c9f0525b685880a5e9dd136ee90 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 16:51:11 +0900 Subject: [PATCH 5628/6909] More correctly handle fire-and-forget async call --- .../RealtimeMultiplayer/RealtimePlayer.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index 9e2ba9b04a..0f8c0f247c 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -51,21 +51,25 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { if (!connected.NewValue) { - startedEvent.Set(); - // messaging to the user about this disconnect will be provided by the RealtimeMatchSubScreen. - Schedule(PerformImmediateExit); + failAndBail(); } }, true); - client.ChangeState(MultiplayerUserState.Loaded); + client.ChangeState(MultiplayerUserState.Loaded).ContinueWith(task => + failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); if (!startedEvent.Wait(TimeSpan.FromSeconds(30))) - { - Logger.Log("Failed to start the multiplayer match in time.", LoggingTarget.Runtime, LogLevel.Important); + failAndBail("Failed to start the multiplayer match in time."); + } - Schedule(PerformImmediateExit); - } + private void failAndBail(string message = null) + { + if (!string.IsNullOrEmpty(message)) + Logger.Log(message, LoggingTarget.Runtime, LogLevel.Important); + + startedEvent.Set(); + Schedule(PerformImmediateExit); } private void onMatchStarted() => startedEvent.Set(); From c3c3364d399b915f9d0d3044dc3cd045830bd3dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 16:56:51 +0900 Subject: [PATCH 5629/6909] Simplify error handling of JoinRoom call --- .../RealtimeRoomManager.cs | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 2f60f504de..8bdf2bdc1a 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -83,15 +83,19 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { Debug.Assert(room.RoomID.Value != null); - var joinTask = multiplayerClient.JoinRoom(room); - joinTask.ContinueWith(_ => Schedule(() => onSuccess?.Invoke(room)), TaskContinuationOptions.OnlyOnRanToCompletion); - joinTask.ContinueWith(t => + multiplayerClient.JoinRoom(room).ContinueWith(t => { - PartRoom(); - if (t.Exception != null) - Logger.Error(t.Exception, "Failed to join multiplayer room."); - Schedule(() => onError?.Invoke(t.Exception?.ToString() ?? string.Empty)); - }, TaskContinuationOptions.NotOnRanToCompletion); + if (t.IsCompletedSuccessfully) + Schedule(() => onSuccess?.Invoke(room)); + else + { + if (t.Exception != null) + Logger.Error(t.Exception, "Failed to join multiplayer room."); + + PartRoom(); + Schedule(() => onError?.Invoke(t.Exception?.ToString() ?? string.Empty)); + } + }); } private void updatePolling() From 1864da00e69806eefb389fd0a5742c0477eb419d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 17:10:02 +0900 Subject: [PATCH 5630/6909] Add extension method to handle cases of fire-and-forget async usage --- osu.Game/Extensions/TaskExtensions.cs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 osu.Game/Extensions/TaskExtensions.cs diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs new file mode 100644 index 0000000000..913a622d9b --- /dev/null +++ b/osu.Game/Extensions/TaskExtensions.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.Threading.Tasks; +using osu.Framework.Logging; + +namespace osu.Game.Extensions +{ + public static class TaskExtensions + { + /// + /// Denote a task which is to be run without local error handling logic, where failure is not catastrophic. + /// Avoids unobserved exceptions from being fired. + /// + /// The task. + /// Whether errors should be logged as important, or silently ignored. + public static void FireAndForget(this Task task, bool logOnError = false) + { + task.ContinueWith(t => + { + if (logOnError) + Logger.Log($"Error running task: {t.Exception?.Message ?? "unknown"}", LoggingTarget.Runtime, LogLevel.Important); + }, TaskContinuationOptions.NotOnRanToCompletion); + } + } +} From 7cc38f03d10a44c30c5edb59e28f2bdc46e377dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 17:10:34 +0900 Subject: [PATCH 5631/6909] Use extension method in all call sites of fire-and-forget async usage --- .../RealtimeMultiplayer/StatefulMultiplayerClient.cs | 5 +++-- .../Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs | 7 ++++--- .../RealtimeMultiplayer/Participants/ParticipantPanel.cs | 3 ++- .../Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs | 2 +- .../Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs | 3 ++- .../Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs | 4 ++-- .../Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 4 +++- 7 files changed, 17 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 79d82a8d02..3196f10f6f 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. + // See the LICENCE file in the repository root for full licence text. #nullable enable @@ -14,6 +14,7 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -84,7 +85,7 @@ namespace osu.Game.Online.RealtimeMultiplayer if (!connected.NewValue) { Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important); - LeaveRoom(); + LeaveRoom().FireAndForget(); } }); } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs index 09487e9831..59f9d5e1ec 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.API; @@ -105,13 +106,13 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match return; if (localUser.State == MultiplayerUserState.Idle) - Client.ChangeState(MultiplayerUserState.Ready); + Client.ChangeState(MultiplayerUserState.Ready).FireAndForget(true); else { if (Room?.Host?.Equals(localUser) == true) - Client.StartMatch(); + Client.StartMatch().FireAndForget(true); else - Client.ChangeState(MultiplayerUserState.Idle); + Client.ChangeState(MultiplayerUserState.Idle).FireAndForget(true); } } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs index a4ff2ce346..fd16754045 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -176,7 +177,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants if (Room.Host?.UserID != api.LocalUser.Value.Id) return; - Client.TransferHost(targetUser); + Client.TransferHost(targetUser).FireAndForget(true); }) }; } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs index f3dab93089..4a8e398008 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer client.ChangeSettings(item: item).ContinueWith(t => { - return Schedule(() => + Schedule(() => { loadingLayer.Hide(); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs index 6455701d31..d5b891f2cc 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Multi.Components; @@ -21,7 +22,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer base.OnResuming(last); if (client.Room != null) - client.ChangeState(MultiplayerUserState.Idle); + client.ChangeState(MultiplayerUserState.Idle).FireAndForget(true); } protected override void UpdatePollingRate(bool isIdle) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index 0f8c0f247c..7c6b33cddd 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -56,8 +56,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer } }, true); - client.ChangeState(MultiplayerUserState.Loaded).ContinueWith(task => - failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); + client.ChangeState(MultiplayerUserState.Loaded) + .ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); if (!startedEvent.Wait(TimeSpan.FromSeconds(30))) failAndBail("Failed to start the multiplayer match in time."); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 8bdf2bdc1a..98a0e5b694 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -8,6 +8,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; +using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.Multiplayer.RoomStatuses; using osu.Game.Online.RealtimeMultiplayer; @@ -68,7 +69,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer var joinedRoom = JoinedRoom.Value; base.PartRoom(); - multiplayerClient.LeaveRoom(); + + multiplayerClient.LeaveRoom().FireAndForget(); // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. From 0ddcab574f05416953361e920a1af22856a16828 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 17:14:58 +0900 Subject: [PATCH 5632/6909] Rename method to avoid weird code analysis rule --- osu.Game/Extensions/TaskExtensions.cs | 2 +- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 2 +- .../Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs | 6 +++--- .../RealtimeMultiplayer/Participants/ParticipantPanel.cs | 2 +- .../Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs | 2 +- .../Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs index 913a622d9b..a1215d786b 100644 --- a/osu.Game/Extensions/TaskExtensions.cs +++ b/osu.Game/Extensions/TaskExtensions.cs @@ -14,7 +14,7 @@ namespace osu.Game.Extensions /// /// The task. /// Whether errors should be logged as important, or silently ignored. - public static void FireAndForget(this Task task, bool logOnError = false) + public static void CatchUnobservedExceptions(this Task task, bool logOnError = false) { task.ContinueWith(t => { diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 3196f10f6f..7b375ca475 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -85,7 +85,7 @@ namespace osu.Game.Online.RealtimeMultiplayer if (!connected.NewValue) { Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important); - LeaveRoom().FireAndForget(); + LeaveRoom().CatchUnobservedExceptions(); } }); } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs index 59f9d5e1ec..5bead2b271 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs @@ -106,13 +106,13 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match return; if (localUser.State == MultiplayerUserState.Idle) - Client.ChangeState(MultiplayerUserState.Ready).FireAndForget(true); + Client.ChangeState(MultiplayerUserState.Ready).CatchUnobservedExceptions(true); else { if (Room?.Host?.Equals(localUser) == true) - Client.StartMatch().FireAndForget(true); + Client.StartMatch().CatchUnobservedExceptions(true); else - Client.ChangeState(MultiplayerUserState.Idle).FireAndForget(true); + Client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); } } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs index fd16754045..85393d1bae 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs @@ -177,7 +177,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants if (Room.Host?.UserID != api.LocalUser.Value.Id) return; - Client.TransferHost(targetUser).FireAndForget(true); + Client.TransferHost(targetUser).CatchUnobservedExceptions(true); }) }; } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs index d5b891f2cc..6685cf52d6 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer base.OnResuming(last); if (client.Room != null) - client.ChangeState(MultiplayerUserState.Idle).FireAndForget(true); + client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); } protected override void UpdatePollingRate(bool isIdle) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index 98a0e5b694..cd337bbb55 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer base.PartRoom(); - multiplayerClient.LeaveRoom().FireAndForget(); + multiplayerClient.LeaveRoom().CatchUnobservedExceptions(); // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. From 3c8f871b2815ca8f4618dbf2e51870e31324cae8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 17:39:08 +0900 Subject: [PATCH 5633/6909] Move player constructor configuration to dedicated class; add AllowRestart parameter --- .../TestSceneHoldNoteInput.cs | 6 +- .../TestSceneOutOfOrderHits.cs | 6 +- .../TestSceneLegacyBeatmapSkin.cs | 6 +- .../TestSceneOutOfOrderHits.cs | 6 +- .../TestSceneSliderInput.cs | 6 +- .../Screens/Multi/Play/TimeshiftPlayer.cs | 4 +- .../RealtimeMultiplayer/RealtimePlayer.cs | 7 +- osu.Game/Screens/Play/Player.cs | 140 +++++++++--------- osu.Game/Screens/Play/PlayerConfiguration.cs | 23 +++ osu.Game/Screens/Play/ReplayPlayer.cs | 4 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 15 +- osu.Game/Tests/Visual/TestPlayer.cs | 6 +- 12 files changed, 146 insertions(+), 83 deletions(-) create mode 100644 osu.Game/Screens/Play/PlayerConfiguration.cs diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs index 5cb1519196..596430f9e5 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneHoldNoteInput.cs @@ -355,7 +355,11 @@ namespace osu.Game.Rulesets.Mania.Tests protected override bool PauseOnFocusLost => false; public ScoreAccessibleReplayPlayer(Score score) - : base(score, false, false) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { } } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs index cecac38f70..18891f8c58 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneOutOfOrderHits.cs @@ -176,7 +176,11 @@ namespace osu.Game.Rulesets.Mania.Tests protected override bool PauseOnFocusLost => false; public ScoreAccessibleReplayPlayer(Score score) - : base(score, false, false) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index 3ff37c4147..a768626005 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -74,7 +74,11 @@ namespace osu.Game.Rulesets.Osu.Tests private readonly bool userHasCustomColours; public ExposedPlayer(bool userHasCustomColours) - : base(false, false) + : base(new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { this.userHasCustomColours = userHasCustomColours; } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs index 32a36ab317..296b421a11 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs @@ -439,7 +439,11 @@ namespace osu.Game.Rulesets.Osu.Tests protected override bool PauseOnFocusLost => false; public ScoreAccessibleReplayPlayer(Score score) - : base(score, false, false) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs index 0164fb8bf4..2cc031405e 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSliderInput.cs @@ -378,7 +378,11 @@ namespace osu.Game.Rulesets.Osu.Tests protected override bool PauseOnFocusLost => false; public ScoreAccessibleReplayPlayer(Score score) - : base(score, false, false) + : base(score, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) { } } diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index e8462088f1..f07f1c2fb0 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -37,8 +37,8 @@ namespace osu.Game.Screens.Multi.Play [Resolved] private IBindable ruleset { get; set; } - public TimeshiftPlayer(PlaylistItem playlistItem, bool allowPause = true) - : base(allowPause) + public TimeshiftPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null) + : base(configuration) { PlaylistItem = playlistItem; } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index c6d44686b5..4b878f43d0 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -12,6 +12,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Scoring; using osu.Game.Screens.Multi.Play; +using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Multi.RealtimeMultiplayer @@ -31,7 +32,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private readonly ManualResetEventSlim startedEvent = new ManualResetEventSlim(); public RealtimePlayer(PlaylistItem playlistItem) - : base(playlistItem, false) + : base(playlistItem, new PlayerConfiguration + { + AllowPause = false, + AllowRestart = false, + }) { } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c539dff5d9..a1c91ab26b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -128,18 +128,14 @@ namespace osu.Game.Screens.Play ///
    protected virtual bool CheckModsAllowFailure() => Mods.Value.OfType().All(m => m.PerformFail()); - private readonly bool allowPause; - private readonly bool showResults; + public readonly PlayerConfiguration Configuration; /// /// Create a new player instance. /// - /// Whether pausing should be allowed. If not allowed, attempting to pause will quit. - /// Whether results screen should be pushed on completion. - public Player(bool allowPause = true, bool showResults = true) + public Player(PlayerConfiguration configuration = null) { - this.allowPause = allowPause; - this.showResults = showResults; + this.Configuration = configuration ??= new PlayerConfiguration(); } private GameplayBeatmap gameplayBeatmap; @@ -317,59 +313,77 @@ namespace osu.Game.Screens.Play } }; - private Drawable createOverlayComponents(WorkingBeatmap working) => new Container + private Drawable createOverlayComponents(WorkingBeatmap working) { - RelativeSizeAxes = Axes.Both, - Children = new[] + var container = new Container { - DimmableStoryboard.OverlayLayerContainer.CreateProxy(), - BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) + RelativeSizeAxes = Axes.Both, + Children = new[] { - 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(), - HUDOverlay = new HUDOverlay(ScoreProcessor, HealthProcessor, DrawableRuleset, Mods.Value) - { - HoldToQuit = + DimmableStoryboard.OverlayLayerContainer.CreateProxy(), + BreakOverlay = new BreakOverlay(working.Beatmap.BeatmapInfo.LetterboxInBreaks, ScoreProcessor) { - Action = performUserRequestedExit, - IsPaused = { BindTarget = GameplayClockContainer.IsPaused } + Clock = DrawableRuleset.FrameStableClock, + ProcessCustomClock = false, + Breaks = working.Beatmap.Breaks }, - PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, - KeyCounter = + // display the cursor above some HUD elements. + DrawableRuleset.Cursor?.CreateProxy() ?? new Container(), + DrawableRuleset.ResumeOverlay?.CreateProxy() ?? new Container(), + HUDOverlay = new HUDOverlay(ScoreProcessor, HealthProcessor, DrawableRuleset, Mods.Value) { - AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, - IsCounting = false + HoldToQuit = + { + Action = performUserRequestedExit, + IsPaused = { BindTarget = GameplayClockContainer.IsPaused } + }, + PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, + KeyCounter = + { + AlwaysVisible = { BindTarget = DrawableRuleset.HasReplayLoaded }, + IsCounting = false + }, + RequestSeek = time => + { + GameplayClockContainer.Seek(time); + GameplayClockContainer.Start(); + }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre }, - RequestSeek = time => + skipOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) { - GameplayClockContainer.Seek(time); - GameplayClockContainer.Start(); + RequestSkip = GameplayClockContainer.Skip }, - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, - skipOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) - { - RequestSkip = GameplayClockContainer.Skip - }, - FailOverlay = new FailOverlay - { - OnRetry = Restart, - OnQuit = performUserRequestedExit, - }, - PauseOverlay = new PauseOverlay - { - OnResume = Resume, - Retries = RestartCount, - OnRetry = Restart, - OnQuit = performUserRequestedExit, - }, - new HotkeyRetryOverlay + FailOverlay = new FailOverlay + { + OnRetry = Restart, + OnQuit = performUserRequestedExit, + }, + PauseOverlay = new PauseOverlay + { + OnResume = Resume, + Retries = RestartCount, + OnRetry = Restart, + OnQuit = performUserRequestedExit, + }, + new HotkeyExitOverlay + { + Action = () => + { + if (!this.IsCurrentScreen()) return; + + fadeOut(true); + performImmediateExit(); + }, + }, + failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, + } + }; + + if (Configuration.AllowRestart) + { + container.Add(new HotkeyRetryOverlay { Action = () => { @@ -378,20 +392,11 @@ namespace osu.Game.Screens.Play fadeOut(true); Restart(); }, - }, - new HotkeyExitOverlay - { - Action = () => - { - if (!this.IsCurrentScreen()) return; - - fadeOut(true); - performImmediateExit(); - }, - }, - failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, + }); } - }; + + return container; + } private void onBreakTimeChanged(ValueChangedEvent isBreakTime) { @@ -490,6 +495,9 @@ namespace osu.Game.Screens.Play ///
    public void Restart() { + if (!Configuration.AllowRestart) + return; + // at the point of restarting the track should either already be paused or the volume should be zero. // stopping here is to ensure music doesn't become audible after exiting back to PlayerLoader. musicController.Stop(); @@ -529,7 +537,7 @@ namespace osu.Game.Screens.Play ValidForResume = false; - if (!showResults) return; + if (!Configuration.ShowResults) return; scoreSubmissionTask ??= Task.Run(async () => { @@ -628,7 +636,7 @@ namespace osu.Game.Screens.Play private bool canPause => // must pass basic screen conditions (beatmap loaded, instance allows pause) - LoadedBeatmapSuccessfully && allowPause && ValidForResume + LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value // cannot pause if we are already in a fail state diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs new file mode 100644 index 0000000000..475a234679 --- /dev/null +++ b/osu.Game/Screens/Play/PlayerConfiguration.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.Screens.Play +{ + public class PlayerConfiguration + { + /// + /// Whether pausing should be allowed. If not allowed, attempting to pause will quit. + /// + public bool AllowPause { get; set; } = true; + + /// + /// Whether results screen should be pushed on completion. + /// + public bool ShowResults { get; set; } = true; + + /// + /// Whether the player should be allowed to trigger a restart. + /// + public bool AllowRestart { get; set; } = true; + } +} diff --git a/osu.Game/Screens/Play/ReplayPlayer.cs b/osu.Game/Screens/Play/ReplayPlayer.cs index a07213cb33..e23cc22929 100644 --- a/osu.Game/Screens/Play/ReplayPlayer.cs +++ b/osu.Game/Screens/Play/ReplayPlayer.cs @@ -16,8 +16,8 @@ namespace osu.Game.Screens.Play // Disallow replays from failing. (see https://github.com/ppy/osu/issues/6108) protected override bool CheckModsAllowFailure() => false; - public ReplayPlayer(Score score, bool allowPause = true, bool showResults = true) - : base(allowPause, showResults) + public ReplayPlayer(Score score, PlayerConfiguration configuration = null) + : base(configuration) { Score = score; } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 528a1842af..8a12427798 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -164,15 +164,18 @@ namespace osu.Game.Screens.Ranking { buttons.Add(new RetryButton { Width = 300 }); - AddInternal(new HotkeyRetryOverlay + if (player?.Configuration.AllowRestart == true) { - Action = () => + AddInternal(new HotkeyRetryOverlay { - if (!this.IsCurrentScreen()) return; + Action = () => + { + if (!this.IsCurrentScreen()) return; - player?.Restart(); - }, - }); + player?.Restart(); + }, + }); + } } } diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index f016d29f38..f47391ce6a 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -37,7 +37,11 @@ namespace osu.Game.Tests.Visual public readonly List Results = new List(); public TestPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) - : base(allowPause, showResults) + : base(new PlayerConfiguration + { + AllowPause = allowPause, + ShowResults = showResults + }) { PauseOnFocusLost = pauseOnFocusLost; } From f9fd909187c3839101e3c52aa476e2460b3e0778 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 18:07:38 +0900 Subject: [PATCH 5634/6909] Fix missed inspections --- 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 a1c91ab26b..f1fb27e154 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -135,7 +135,7 @@ namespace osu.Game.Screens.Play ///
    public Player(PlayerConfiguration configuration = null) { - this.Configuration = configuration ??= new PlayerConfiguration(); + Configuration = configuration ?? new PlayerConfiguration(); } private GameplayBeatmap gameplayBeatmap; From 94e4928c4b5feb2bb404173e27fadedbb5d5468d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 11:27:15 +0100 Subject: [PATCH 5635/6909] Bring back accidentally-removed license header --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 7b375ca475..6331d324a6 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -1,4 +1,4 @@ - +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. #nullable enable From 6a80e1303d540187093c794302f5982fa171ae0e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 23 Dec 2020 12:56:04 +0100 Subject: [PATCH 5636/6909] LINQ-ify Import() logic and ignore case of file extensions. --- osu.Game/OsuGameBase.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index ca87772209..7def93255b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -397,18 +397,13 @@ namespace osu.Game public virtual async Task Import(params ImportTask[] tasks) { - var importTasks = new List(); - - foreach (var extension in tasks.Select(t => Path.GetExtension(t.Path)).Distinct()) + var extensions = tasks.Select(t => Path.GetExtension(t.Path).ToLowerInvariant()).Distinct(); + await Task.WhenAll(extensions.Select(ext => { - var importList = tasks.Where(t => t.Path.EndsWith(extension, StringComparison.OrdinalIgnoreCase)); - var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(extension)); + var imports = tasks.Where(t => t.Path.EndsWith(ext, StringComparison.OrdinalIgnoreCase)); - if (importer != null) - importTasks.Add(importer.Import(importList.ToArray())); - } - - await Task.WhenAll(importTasks); + return fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(ext))?.Import(imports.ToArray()) ?? Task.CompletedTask; + })); } public IEnumerable HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); From ea5da53597e2d1a57b76b6f4d80429645ccc6657 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 23 Dec 2020 13:31:27 +0100 Subject: [PATCH 5637/6909] Handle URL links with the osu scheme. --- osu.Android/OsuGameActivity.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index e801c2ca6e..55484c439a 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -16,8 +16,11 @@ namespace osu.Android { [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] + [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataScheme = osu_url_scheme)] public class OsuGameActivity : AndroidGameActivity { + private const string osu_url_scheme = "osu"; + private OsuGameAndroid game; protected override Framework.Game CreateGame() => game = new OsuGameAndroid(this); @@ -49,6 +52,8 @@ namespace osu.Android case Intent.ActionDefault: if (intent.Scheme == ContentResolver.SchemeContent) handleImportFromUri(intent.Data); + else if (intent.Scheme == osu_url_scheme) + Task.Run(() => game.HandleLink(intent.DataString)); break; case Intent.ActionSend: From 78b8c60f1949aef5c5a63febcf46968038dd9287 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Wed, 23 Dec 2020 13:38:33 +0100 Subject: [PATCH 5638/6909] Opt for SingleInstance launch mode --- 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 55484c439a..dfdecdca2e 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -14,7 +14,7 @@ using osu.Framework.Android; namespace osu.Android { - [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false)] + [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataScheme = osu_url_scheme)] public class OsuGameActivity : AndroidGameActivity From 582b0d2a7467f54f4ae125f79fe6c5611fb88f66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 13:47:28 +0100 Subject: [PATCH 5639/6909] Revert logic to be closer to original Note the reversal of the order of operations in `endHandlingTrack()` (done for extra safety, to ensure no more value changed events can be fired at the point of cancelling looping). --- osu.Game/Screens/Multi/Match/RoomSubScreen.cs | 26 ++++++++++++------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs index 4f5d2a5b3e..b9d7408946 100644 --- a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs @@ -41,27 +41,24 @@ namespace osu.Game.Screens.Multi.Match managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); - - if (music != null) - music.TrackChanged += applyToTrack; } public override void OnEntering(IScreen last) { base.OnEntering(last); - applyToTrack(); + beginHandlingTrack(); } public override void OnSuspending(IScreen next) { - resetTrack(); + endHandlingTrack(); base.OnSuspending(next); } public override void OnResuming(IScreen last) { base.OnResuming(last); - applyToTrack(); + beginHandlingTrack(); } public override bool OnExiting(IScreen next) @@ -69,7 +66,7 @@ namespace osu.Game.Screens.Multi.Match RoomManager?.PartRoom(); Mods.Value = Array.Empty(); - resetTrack(); + endHandlingTrack(); return base.OnExiting(next); } @@ -98,7 +95,18 @@ namespace osu.Game.Screens.Multi.Match Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } - private void applyToTrack(WorkingBeatmap _ = default, TrackChangeDirection __ = default) + private void beginHandlingTrack() + { + Beatmap.BindValueChanged(applyLoopingToTrack, true); + } + + private void endHandlingTrack() + { + Beatmap.ValueChanged -= applyLoopingToTrack; + cancelTrackLooping(); + } + + private void applyLoopingToTrack(ValueChangedEvent _ = null) { if (!this.IsCurrentScreen()) return; @@ -114,7 +122,7 @@ namespace osu.Game.Screens.Multi.Match } } - private void resetTrack() + private void cancelTrackLooping() { var track = Beatmap?.Value?.Track; From c5692a5d6aa51c51513e8a34c5ac023cb9326847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 14:18:24 +0100 Subject: [PATCH 5640/6909] Re-enable carousel selection after error --- .../Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs index 4a8e398008..8f317800e3 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs @@ -65,7 +65,10 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer if (t.IsCompletedSuccessfully) this.Exit(); else + { Logger.Log($"Could not use current beatmap ({t.Exception?.Message})", level: LogLevel.Important); + Carousel.AllowSelection = true; + } }); }); } From 4296f61d6cbe0a8d25a94a9ae22274139c8b1b1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 23 Dec 2020 22:39:14 +0900 Subject: [PATCH 5641/6909] Tidy up event flow of change settings call --- .../RealtimeMultiplayer/StatefulMultiplayerClient.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 6331d324a6..dc999ee2be 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -369,7 +369,6 @@ namespace osu.Game.Online.RealtimeMultiplayer if (Room == null) return; - // Update a few properties of the room instantaneously. Schedule(() => { if (Room == null) @@ -377,6 +376,7 @@ namespace osu.Game.Online.RealtimeMultiplayer Debug.Assert(apiRoom != null); + // Update a few properties of the room instantaneously. Room.Settings = settings; apiRoom.Name.Value = Room.Settings.Name; @@ -385,12 +385,12 @@ namespace osu.Game.Online.RealtimeMultiplayer apiRoom.Playlist.Clear(); RoomChanged?.Invoke(); + + var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); + req.Success += res => updatePlaylist(settings, res); + + api.Queue(req); }); - - var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => updatePlaylist(settings, res); - - api.Queue(req); } private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) From 980e85ce25f087f28306c530e15b3d7916b2b5bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 15:51:26 +0100 Subject: [PATCH 5642/6909] Refactor player exit logic to convey intention better --- .../RealtimeMultiplayer/RealtimePlayer.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 22 ++++++++++++++----- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index 0d8e636450..e467e5fcf8 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -61,7 +61,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer startedEvent.Set(); // messaging to the user about this disconnect will be provided by the RealtimeMatchSubScreen. - Schedule(PerformImmediateExit); + Schedule(() => PerformExit(false)); } }, true); @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { Logger.Log("Failed to start the multiplayer match in time.", LoggingTarget.Runtime, LogLevel.Important); - Schedule(PerformImmediateExit); + Schedule(() => PerformExit(false)); } Debug.Assert(client.Room != null); diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c6265c48d2..2bc84ce5d5 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -386,7 +386,7 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; fadeOut(true); - PerformImmediateExit(); + PerformExit(true); }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, @@ -458,20 +458,30 @@ namespace osu.Game.Screens.Play return playable; } - protected void PerformImmediateExit() + /// + /// Exits the . + /// + /// + /// Whether the exit is requested by the user, or a higher-level game component. + /// Pausing is allowed only in the former case. + /// + protected void PerformExit(bool userRequested) { // if a restart has been requested, cancel any pending completion (user has shown intent to restart). completionProgressDelegate?.Cancel(); ValidForResume = false; - performUserRequestedExit(); + if (!this.IsCurrentScreen()) return; + + if (userRequested) + performUserRequestedExit(); + else + this.Exit(); } private void performUserRequestedExit() { - if (!this.IsCurrentScreen()) return; - if (ValidForResume && HasFailed && !FailOverlay.IsPresent) { failAnimation.FinishTransforms(true); @@ -498,7 +508,7 @@ namespace osu.Game.Screens.Play RestartRequested?.Invoke(); if (this.IsCurrentScreen()) - PerformImmediateExit(); + PerformExit(true); else this.MakeCurrent(); } From 3b0bf1136642c40fd189cbbd1940d3121f286e0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 17:01:01 +0100 Subject: [PATCH 5643/6909] Fix JoinRoom failing to return canceled token As it turns out, `Task.FromCanceled` expects to receive an already cancelled `CancellationToken`, which `CancellationToken.None` is not. --- .../Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs index 5cbf3be8ca..026b7176d1 100644 --- a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs @@ -122,7 +122,7 @@ namespace osu.Game.Online.RealtimeMultiplayer protected override Task JoinRoom(long roomId) { if (!isConnected.Value) - return Task.FromCanceled(CancellationToken.None); + return Task.FromCanceled(new CancellationToken(true)); return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoom), roomId); } From e4959489b70d61f76c6f25e0c7125fd24d96becc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 17:08:28 +0100 Subject: [PATCH 5644/6909] Improve user-facing error messages in room settings --- .../Match/RealtimeMatchSettingsOverlay.cs | 3 ++- .../Multi/RealtimeMultiplayer/RealtimeRoomManager.cs | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs index 3e495b490f..a93b1b09d1 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs @@ -5,6 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -299,7 +300,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match if (t.IsCompletedSuccessfully) onSuccess(currentRoom.Value); else - onError(t.Exception?.Message ?? "Error changing settings."); + onError(t.Exception?.AsSingular().Message ?? "Error changing settings."); })); } else diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs index cd337bbb55..eb6e5fad07 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs @@ -7,6 +7,7 @@ using System.Diagnostics; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; using osu.Game.Extensions; using osu.Game.Online.Multiplayer; @@ -91,11 +92,13 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer Schedule(() => onSuccess?.Invoke(room)); else { + const string message = "Failed to join multiplayer room."; + if (t.Exception != null) - Logger.Error(t.Exception, "Failed to join multiplayer room."); + Logger.Error(t.Exception, message); PartRoom(); - Schedule(() => onError?.Invoke(t.Exception?.ToString() ?? string.Empty)); + Schedule(() => onError?.Invoke(t.Exception?.AsSingular().Message ?? message)); } }); } From e89583d732e5c9996534bdb89a8ee42470cbe530 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 01:33:19 +0900 Subject: [PATCH 5645/6909] Prefer connecting to dev server when running in DEBUG --- osu.Game/Online/API/APIAccess.cs | 8 +++++++- .../RealtimeMultiplayer/RealtimeMultiplayerClient.cs | 4 ++++ osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 4 ++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index fe500b9548..ca457ccf71 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -26,9 +26,15 @@ namespace osu.Game.Online.API private readonly OAuth authentication; +#if DEBUG + public string Endpoint => @"https://dev.ppy.sh"; + private const string client_secret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT"; +#else public string Endpoint => @"https://osu.ppy.sh"; - private const string client_id = @"5"; private const string client_secret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; +#endif + + private const string client_id = @"5"; private readonly Queue queue = new Queue(); diff --git a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs index 75bb578a29..4ec5b9af40 100644 --- a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs @@ -19,7 +19,11 @@ namespace osu.Game.Online.RealtimeMultiplayer { public class RealtimeMultiplayerClient : StatefulMultiplayerClient { +#if DEBUG + private const string endpoint = "https://dev.ppy.sh/multiplayer"; +#else private const string endpoint = "https://spectator.ppy.sh/multiplayer"; +#endif public override IBindable IsConnected => isConnected; diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 0167a5d025..c9203b595e 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -104,7 +104,11 @@ namespace osu.Game.Online.Spectator } } +#if DEBUG + private const string endpoint = "https://dev.ppy.sh/spectator"; +#else private const string endpoint = "https://spectator.ppy.sh/spectator"; +#endif protected virtual async Task Connect() { From 9843da59f4866918391902de30125577b60c1464 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 20:29:17 +0100 Subject: [PATCH 5646/6909] Fix intermittent test fail due to duplicate user `TestSceneRealtimeReadyButton` was manually adding `API.LocalUser`, which wasn't actually needed. The base `RealtimeMultiplayerTestScene` by default creates a new room as `API.LocalUser`, therefore automatically adding that user to the room - and as such there is no need to add them manually unless the `joinRoom` ctor param is specified as `false`. --- .../Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs index b7cd81fb32..e9d3ddb32d 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs @@ -55,8 +55,6 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer } } }; - - Client.AddUser(API.LocalUser.Value); }); [Test] From 47020c888731c2370d6f81fbfe9f11d6e7fcfdc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 21:00:47 +0100 Subject: [PATCH 5647/6909] Add failing test cases --- .../TestSceneRealtimeMultiplayer.cs | 24 +++++++++++++++++++ .../TestRealtimeMultiplayerClient.cs | 2 +- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs index 80955ca380..5cf80df6aa 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.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.Game.Screens.Multi.Components; +using osu.Game.Users; namespace osu.Game.Tests.Visual.RealtimeMultiplayer { @@ -15,6 +17,28 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer AddUntilStep("wait for loaded", () => multi.IsLoaded); } + [Test] + public void TestOneUserJoinedMultipleTimes() + { + var user = new User { Id = 33 }; + + AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); + + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + } + + [Test] + public void TestOneUserLeftMultipleTimes() + { + var user = new User { Id = 44 }; + + AddStep("add user", () => Client.AddUser(user)); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + + AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); + AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + } + private class TestRealtimeMultiplayer : Screens.Multi.RealtimeMultiplayer.RealtimeMultiplayer { protected override RoomManager CreateRoomManager() => new TestRealtimeRoomManager(); diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs index de52633c88..52047016e2 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs @@ -31,7 +31,7 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer public void RemoveUser(User user) { Debug.Assert(Room != null); - ((IMultiplayerClient)this).UserLeft(Room.Users.Single(u => u.User == user)); + ((IMultiplayerClient)this).UserLeft(new MultiplayerRoomUser(user.Id)); Schedule(() => { From a71496bc4e9f0386d13b295eb92496f33144e5b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 20:34:19 +0100 Subject: [PATCH 5648/6909] Sanity check received user joined messages While test failures fixed in 9843da5 were a shortcoming of the test, they exposed a potential vulnerable point of the multiplayer client logic. In case of unreliable message delivery it is not unreasonable that duplicate messages might arrive, in which case the same scenario that failed in the tests could crash the game. To ensure that is not the case, explicitly screen each new joined user against the room user list, to ensure that duplicates do not show up. `UserLeft` is already tolerant in that respect (if a user is requested to be removed twice by the server, the second removal just won't do anything). --- .../Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs index 6331d324a6..e8dbeda9cb 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs @@ -226,6 +226,10 @@ namespace osu.Game.Online.RealtimeMultiplayer if (Room == null) return; + // for sanity, ensure that there can be no duplicate users in the room user list. + if (Room.Users.Any(existing => existing.UserID == user.UserID)) + return; + Room.Users.Add(user); RoomChanged?.Invoke(); From 05d9f2376224b42d9ee56f8ee094374a15dc2801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 16:38:18 +0100 Subject: [PATCH 5649/6909] Move out create room button to separate class --- osu.Game/Screens/Multi/CreateRoomButton.cs | 31 ++++++++++++++++++++++ osu.Game/Screens/Multi/Multiplayer.cs | 23 ---------------- 2 files changed, 31 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Screens/Multi/CreateRoomButton.cs diff --git a/osu.Game/Screens/Multi/CreateRoomButton.cs b/osu.Game/Screens/Multi/CreateRoomButton.cs new file mode 100644 index 0000000000..b501de46ef --- /dev/null +++ b/osu.Game/Screens/Multi/CreateRoomButton.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Screens.Multi.Match.Components; +using osuTK; + +namespace osu.Game.Screens.Multi +{ + public class CreateRoomButton : PurpleTriangleButton + { + public CreateRoomButton() + { + Size = new Vector2(150, Header.HEIGHT - 20); + Margin = new MarginPadding + { + Top = 10, + Right = 10 + OsuScreen.HORIZONTAL_OVERFLOW_PADDING, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + Triangles.TriangleScale = 1.5f; + + Text = "Create room"; + } + } +} diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index de2e0d58c9..a957eb6824 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -21,9 +21,7 @@ using osu.Game.Screens.Multi.Components; 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.Users; -using osuTK; namespace osu.Game.Screens.Multi { @@ -332,26 +330,5 @@ namespace osu.Game.Screens.Multi protected override double TransformDuration => 200; } } - - public class CreateRoomButton : PurpleTriangleButton - { - public CreateRoomButton() - { - Size = new Vector2(150, Header.HEIGHT - 20); - Margin = new MarginPadding - { - Top = 10, - Right = 10 + HORIZONTAL_OVERFLOW_PADDING, - }; - } - - [BackgroundDependencyLoader] - private void load() - { - Triangles.TriangleScale = 1.5f; - - Text = "Create room"; - } - } } } From c13acb609aed79df56e883586697be71b72e74c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 21:50:29 +0100 Subject: [PATCH 5650/6909] Move out sizing logic to multiplayer screen --- osu.Game/Screens/Multi/CreateRoomButton.cs | 12 ------------ osu.Game/Screens/Multi/Multiplayer.cs | 7 +++++++ 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Multi/CreateRoomButton.cs b/osu.Game/Screens/Multi/CreateRoomButton.cs index b501de46ef..9e53904510 100644 --- a/osu.Game/Screens/Multi/CreateRoomButton.cs +++ b/osu.Game/Screens/Multi/CreateRoomButton.cs @@ -2,24 +2,12 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Framework.Graphics; using osu.Game.Screens.Multi.Match.Components; -using osuTK; namespace osu.Game.Screens.Multi { public class CreateRoomButton : PurpleTriangleButton { - public CreateRoomButton() - { - Size = new Vector2(150, Header.HEIGHT - 20); - Margin = new MarginPadding - { - Top = 10, - Right = 10 + OsuScreen.HORIZONTAL_OVERFLOW_PADDING, - }; - } - [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index a957eb6824..c820eae51f 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -22,6 +22,7 @@ using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Match; using osu.Game.Users; +using osuTK; namespace osu.Game.Screens.Multi { @@ -131,6 +132,12 @@ namespace osu.Game.Screens.Multi { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + Size = new Vector2(150, Header.HEIGHT - 20), + Margin = new MarginPadding + { + Top = 10, + Right = 10 + HORIZONTAL_OVERFLOW_PADDING, + }, Action = () => OpenNewRoom() }, RoomManager = CreateRoomManager() From 414f886b02d21c24ddaddc273ac747dc1d347b63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 23 Dec 2020 22:02:37 +0100 Subject: [PATCH 5651/6909] Split timeshift & multiplayer "create" buttons Multiplayer button gets new, different "Create match" text, and disable logic in case of a dropped connection to the multiplayer server. --- osu.Game/Screens/Multi/Multiplayer.cs | 18 ++++++++------- .../CreateRealtimeMatchButton.cs | 23 +++++++++++++++++++ .../RealtimeMultiplayer.cs | 3 +++ .../CreateTimeshiftRoomButton.cs} | 4 ++-- .../Multi/Timeshift/TimeshiftMultiplayer.cs | 3 +++ 5 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs rename osu.Game/Screens/Multi/{CreateRoomButton.cs => Timeshift/CreateTimeshiftRoomButton.cs} (78%) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index c820eae51f..a7d40a89d3 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -128,18 +128,18 @@ namespace osu.Game.Screens.Multi } }, new Header(screenStack), - createButton = new CreateRoomButton + createButton = CreateNewMultiplayerGameButton().With(button => { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Size = new Vector2(150, Header.HEIGHT - 20), - Margin = new MarginPadding + button.Anchor = Anchor.TopRight; + button.Origin = Anchor.TopRight; + button.Size = new Vector2(150, Header.HEIGHT - 20); + button.Margin = new MarginPadding { Top = 10, Right = 10 + HORIZONTAL_OVERFLOW_PADDING, - }, - Action = () => OpenNewRoom() - }, + }; + button.Action = () => OpenNewRoom(); + }), RoomManager = CreateRoomManager() } }; @@ -315,6 +315,8 @@ namespace osu.Game.Screens.Multi protected abstract LoungeSubScreen CreateLounge(); + protected abstract OsuButton CreateNewMultiplayerGameButton(); + private class MultiplayerWaveContainer : WaveContainer { protected override bool StartHidden => true; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs new file mode 100644 index 0000000000..eda907f8cb --- /dev/null +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.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.Allocation; +using osu.Framework.Bindables; +using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Match.Components; + +namespace osu.Game.Screens.Multi.RealtimeMultiplayer +{ + public class CreateRealtimeMatchButton : PurpleTriangleButton + { + [BackgroundDependencyLoader] + private void load(StatefulMultiplayerClient multiplayerClient) + { + Triangles.TriangleScale = 1.5f; + + Text = "Create match"; + + ((IBindable)Enabled).BindTo(multiplayerClient.IsConnected); + } + } +} diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs index 6685cf52d6..6739a51fe8 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Extensions; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Multi.Components; @@ -64,5 +65,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override RoomManager CreateRoomManager() => new RealtimeRoomManager(); protected override LoungeSubScreen CreateLounge() => new RealtimeLoungeSubScreen(); + + protected override OsuButton CreateNewMultiplayerGameButton() => new CreateRealtimeMatchButton(); } } diff --git a/osu.Game/Screens/Multi/CreateRoomButton.cs b/osu.Game/Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs similarity index 78% rename from osu.Game/Screens/Multi/CreateRoomButton.cs rename to osu.Game/Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs index 9e53904510..bd9d667630 100644 --- a/osu.Game/Screens/Multi/CreateRoomButton.cs +++ b/osu.Game/Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs @@ -4,9 +4,9 @@ using osu.Framework.Allocation; using osu.Game.Screens.Multi.Match.Components; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.Multi.Timeshift { - public class CreateRoomButton : PurpleTriangleButton + public class CreateTimeshiftRoomButton : PurpleTriangleButton { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs index 2ea4857799..d525a3800d 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs @@ -3,6 +3,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Graphics.UserInterface; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Match; @@ -47,5 +48,7 @@ namespace osu.Game.Screens.Multi.Timeshift protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager(); protected override LoungeSubScreen CreateLounge() => new TimeshiftLoungeSubScreen(); + + protected override OsuButton CreateNewMultiplayerGameButton() => new CreateTimeshiftRoomButton(); } } From 06bc3cd54d448d5597a193d7024c036f25b9e081 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Thu, 24 Dec 2020 00:17:26 +0100 Subject: [PATCH 5652/6909] Apply review suggestions. --- osu.Android/OsuGameActivity.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index dfdecdca2e..953c06f4e2 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.IO; +using System.Linq; using System.Threading.Tasks; using Android.App; using Android.Content; @@ -16,10 +17,10 @@ namespace osu.Android { [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] - [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataScheme = osu_url_scheme)] + [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { - private const string osu_url_scheme = "osu"; + private static readonly string[] osu_url_schemes = { "osu", "osump" }; private OsuGameAndroid game; @@ -52,8 +53,8 @@ namespace osu.Android case Intent.ActionDefault: if (intent.Scheme == ContentResolver.SchemeContent) handleImportFromUri(intent.Data); - else if (intent.Scheme == osu_url_scheme) - Task.Run(() => game.HandleLink(intent.DataString)); + else if (osu_url_schemes.Contains(intent.Scheme)) + game.HandleLink(intent.DataString); break; case Intent.ActionSend: From d6dadd12faa7c277befcdcefd0eecdc25196cf0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 10:38:53 +0900 Subject: [PATCH 5653/6909] Send multiplayer user IDs via ctor for better thread safety --- .../RealtimeMultiplayer/RealtimeMatchSubScreen.cs | 10 +++++++++- .../Multi/RealtimeMultiplayer/RealtimePlayer.cs | 13 +++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs index cdab1435c0..468908f83d 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; @@ -188,7 +189,14 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); - private void onLoadRequested() => multiplayer?.Push(new PlayerLoader(() => new RealtimePlayer(SelectedItem.Value))); + private void onLoadRequested() + { + Debug.Assert(client.Room != null); + + int[] userIds = client.Room.Users.Where(u => u.State >= MultiplayerUserState.WaitingForLoad).Select(u => u.UserID).ToArray(); + + multiplayer?.Push(new PlayerLoader(() => new RealtimePlayer(SelectedItem.Value, userIds))); + } protected override void Dispose(bool isDisposing) { diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index 7824b414f2..25543d3d6d 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Linq; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; @@ -37,9 +36,17 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer [CanBeNull] private MultiplayerGameplayLeaderboard leaderboard; - public RealtimePlayer(PlaylistItem playlistItem) + private readonly int[] userIds; + + /// + /// Construct a multiplayer player. + /// + /// The playlist item to be played. + /// The users which are participating in this game. + public RealtimePlayer(PlaylistItem playlistItem, int[] userIds) : base(playlistItem, false) { + this.userIds = userIds; } [BackgroundDependencyLoader] @@ -65,8 +72,6 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer Debug.Assert(client.Room != null); - int[] userIds = client.Room.Users.Where(u => u.State >= MultiplayerUserState.WaitingForLoad).Select(u => u.UserID).ToArray(); - // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); } From a411b26a09530bed75c404afe49d5b447bd5a863 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 10:51:24 +0900 Subject: [PATCH 5654/6909] Remove unnecessary clamp Co-authored-by: Joseph Madamba --- osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs index 3f770cfb5e..20c0818d03 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModFlashlight.cs @@ -56,7 +56,7 @@ namespace osu.Game.Rulesets.Osu.Mods var destination = e.MousePosition; FlashlightPosition = Interpolation.ValueAt( - Math.Clamp(Math.Abs(Clock.ElapsedFrameTime), 0, follow_delay), position, destination, 0, follow_delay, Easing.Out); + Math.Min(Math.Abs(Clock.ElapsedFrameTime), follow_delay), position, destination, 0, follow_delay, Easing.Out); return base.OnMouseMove(e); } From 61a5d3ef4ae8d7724eb02686a01a33e85cbc4e0c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 13:32:35 +0900 Subject: [PATCH 5655/6909] Remove double handling of restart allowance on results screen (already handled locally) --- osu.Game/Screens/Ranking/ResultsScreen.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 8a12427798..528a1842af 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -164,18 +164,15 @@ namespace osu.Game.Screens.Ranking { buttons.Add(new RetryButton { Width = 300 }); - if (player?.Configuration.AllowRestart == true) + AddInternal(new HotkeyRetryOverlay { - AddInternal(new HotkeyRetryOverlay + Action = () => { - Action = () => - { - if (!this.IsCurrentScreen()) return; + if (!this.IsCurrentScreen()) return; - player?.Restart(); - }, - }); - } + player?.Restart(); + }, + }); } } From 1f80f01b53f4861a26ee8564bb7b81684cfad488 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 14:46:52 +0900 Subject: [PATCH 5656/6909] Add accuracy to frame bundle header --- osu.Game/Online/Spectator/FrameHeader.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs index b4988fecf9..135b356eda 100644 --- a/osu.Game/Online/Spectator/FrameHeader.cs +++ b/osu.Game/Online/Spectator/FrameHeader.cs @@ -14,6 +14,11 @@ namespace osu.Game.Online.Spectator [Serializable] public class FrameHeader { + /// + /// The current accuracy of the score. + /// + public double Accuracy { get; set; } + /// /// The current combo of the score. /// @@ -42,16 +47,18 @@ namespace osu.Game.Online.Spectator { Combo = score.Combo; MaxCombo = score.MaxCombo; + Accuracy = score.Accuracy; // copy for safety Statistics = new Dictionary(score.Statistics); } [JsonConstructor] - public FrameHeader(int combo, int maxCombo, Dictionary statistics, DateTimeOffset receivedTime) + public FrameHeader(int combo, int maxCombo, double accuracy, Dictionary statistics, DateTimeOffset receivedTime) { Combo = combo; MaxCombo = maxCombo; + Accuracy = accuracy; Statistics = statistics; ReceivedTime = receivedTime; } From d66e2183185bbedc26b2d7350499e71d3aebc0fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 14:57:23 +0900 Subject: [PATCH 5657/6909] Source display accuracy from header and remove from ScoreProcessor function --- osu.Game/Rulesets/Scoring/ScoreProcessor.cs | 15 +++++---------- .../Play/HUD/MultiplayerGameplayLeaderboard.cs | 4 ++-- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs index 4b2e2bf715..2024290460 100644 --- a/osu.Game/Rulesets/Scoring/ScoreProcessor.cs +++ b/osu.Game/Rulesets/Scoring/ScoreProcessor.cs @@ -193,7 +193,7 @@ namespace osu.Game.Rulesets.Scoring private void updateScore() { if (rollingMaxBaseScore != 0) - Accuracy.Value = baseScore / rollingMaxBaseScore; + Accuracy.Value = calculateAccuracyRatio(baseScore, true); TotalScore.Value = getScore(Mode.Value); } @@ -233,13 +233,13 @@ namespace osu.Game.Rulesets.Scoring } /// - /// Given a minimal set of inputs, return the computed score and accuracy for the tracked beatmap / mods combination, at the current point in time. + /// Given a minimal set of inputs, return the computed score for the tracked beatmap / mods combination, at the current point in time. /// /// The to compute the total score in. /// The maximum combo achievable in the beatmap. /// Statistics to be used for calculating accuracy, bonus score, etc. - /// The computed score and accuracy for provided inputs. - public (double score, double accuracy) GetScoreAndAccuracy(ScoringMode mode, int maxCombo, Dictionary statistics) + /// The computed score for provided inputs. + public double GetImmediateScore(ScoringMode mode, int maxCombo, Dictionary statistics) { // calculate base score from statistics pairs int computedBaseScore = 0; @@ -252,12 +252,7 @@ namespace osu.Game.Rulesets.Scoring computedBaseScore += Judgement.ToNumericResult(pair.Key) * pair.Value; } - double pointInTimeAccuracy = calculateAccuracyRatio(computedBaseScore, true); - double comboRatio = calculateComboRatio(maxCombo); - - double score = GetScore(mode, maxAchievableCombo, calculateAccuracyRatio(computedBaseScore), comboRatio, scoreResultCounts); - - return (score, pointInTimeAccuracy); + return GetScore(mode, maxAchievableCombo, calculateAccuracyRatio(computedBaseScore), calculateComboRatio(maxCombo), scoreResultCounts); } /// diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 12321de442..c10ec9e004 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -122,8 +122,8 @@ namespace osu.Game.Screens.Play.HUD if (LastHeader == null) return; - (score.Value, accuracy.Value) = processor.GetScoreAndAccuracy(mode, LastHeader.MaxCombo, LastHeader.Statistics); - + score.Value = processor.GetImmediateScore(mode, LastHeader.MaxCombo, LastHeader.Statistics); + accuracy.Value = LastHeader.Accuracy; currentCombo.Value = LastHeader.Combo; } } From e86e9bfae625351e5323c9393e011190dd58c7b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 15:32:55 +0900 Subject: [PATCH 5658/6909] Don't begin gameplay until all users are in a completely prepared state --- .../RealtimeMultiplayer/RealtimePlayer.cs | 39 ++++++++++++------- osu.Game/Screens/Play/Player.cs | 18 +++++++-- 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index 085c52cb85..20b184bed3 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -3,12 +3,12 @@ using System; using System.Diagnostics; -using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Scoring; @@ -33,13 +33,14 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private IBindable isConnected; private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); - private readonly ManualResetEventSlim startedEvent = new ManualResetEventSlim(); [CanBeNull] private MultiplayerGameplayLeaderboard leaderboard; private readonly int[] userIds; + private LoadingLayer loadingDisplay; + /// /// Construct a multiplayer player. /// @@ -60,6 +61,12 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer client.MatchStarted += onMatchStarted; client.ResultsReady += onResultsReady; + ScoreProcessor.HasCompleted.BindValueChanged(completed => + { + // wait for server to tell us that results are ready (see SubmitScore implementation) + loadingDisplay.Show(); + }); + isConnected = client.IsConnected.GetBoundCopy(); isConnected.BindValueChanged(connected => { @@ -70,19 +77,20 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer } }, true); - client.ChangeState(MultiplayerUserState.Loaded) - .ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); - - if (!startedEvent.Wait(TimeSpan.FromSeconds(30))) - { - failAndBail("Failed to start the multiplayer match in time."); - return; - } - Debug.Assert(client.Room != null); // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); + + HUDOverlay.Add(loadingDisplay = new LoadingLayer(DrawableRuleset) { Depth = float.MaxValue }); + } + + protected override void StartGameplay() + { + // block base call, but let the server know we are ready to start. + loadingDisplay.Show(); + + client.ChangeState(MultiplayerUserState.Loaded).ContinueWith(task => failAndBail(task.Exception?.Message ?? "Server error"), TaskContinuationOptions.NotOnRanToCompletion); } private void failAndBail(string message = null) @@ -90,7 +98,6 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer if (!string.IsNullOrEmpty(message)) Logger.Log(message, LoggingTarget.Runtime, LogLevel.Important); - startedEvent.Set(); Schedule(() => PerformExit(false)); } @@ -112,7 +119,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer padding + HUDOverlay.TopScoringElementsHeight); } - private void onMatchStarted() => startedEvent.Set(); + private void onMatchStarted() => Scheduler.Add(() => + { + loadingDisplay.Hide(); + base.StartGameplay(); + }); private void onResultsReady() => resultsReady.SetResult(true); @@ -124,7 +135,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer // Await up to 30 seconds for results to become available (3 api request timeouts). // This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur. - await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(30))); + await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))); } protected override ResultsScreen CreateResults(ScoreInfo score) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2bc84ce5d5..3f761d9e11 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -723,9 +723,6 @@ namespace osu.Game.Screens.Play storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; - GameplayClockContainer.Restart(); - GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint); - foreach (var mod in Mods.Value.OfType()) mod.ApplyToPlayer(this); @@ -740,6 +737,21 @@ namespace osu.Game.Screens.Play mod.ApplyToTrack(musicController.CurrentTrack); updateGameplayState(); + + GameplayClockContainer.FadeInFromZero(750, Easing.OutQuint); + StartGameplay(); + } + + /// + /// Called to trigger the starting of the gameplay clock and underlying gameplay. + /// This will be called on entering the player screen once. A derived class may block the first call to this to delay the start of gameplay. + /// + protected virtual void StartGameplay() + { + if (GameplayClockContainer.GameplayClock.IsRunning) + throw new InvalidOperationException($"{nameof(StartGameplay)} should not be called when the gameplay clock is already running"); + + GameplayClockContainer.Restart(); } public override void OnSuspending(IScreen next) From 5457e4598b1a21405a183790cdd299218f789849 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 16:20:38 +0900 Subject: [PATCH 5659/6909] Schedule UpdateFilter calls to avoid operations occuring while at a sub screen --- osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs index 896c215c42..3712cbe33e 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs @@ -98,7 +98,9 @@ namespace osu.Game.Screens.Multi.Lounge.Components scheduledFilterUpdate = Scheduler.AddDelayed(UpdateFilter, 200); } - protected void UpdateFilter() + protected void UpdateFilter() => Scheduler.AddOnce(updateFilter); + + private void updateFilter() { scheduledFilterUpdate?.Cancel(); From 6bd6888a938667f2050ef55c24ff4f9872a418ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 16:29:51 +0900 Subject: [PATCH 5660/6909] Disallow skipping in multiplayer for now --- osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs | 1 + osu.Game/Screens/Play/Player.cs | 3 +++ osu.Game/Screens/Play/PlayerConfiguration.cs | 5 +++++ 3 files changed, 9 insertions(+) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index b1615cc7f6..5da712bfcc 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -51,6 +51,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { AllowPause = false, AllowRestart = false, + AllowSkippingIntro = false, }) { this.userIds = userIds; diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 4bd6340751..90600b0cf1 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -381,6 +381,9 @@ namespace osu.Game.Screens.Play } }; + if (!Configuration.AllowSkippingIntro) + skipOverlay.Expire(); + if (Configuration.AllowRestart) { container.Add(new HotkeyRetryOverlay diff --git a/osu.Game/Screens/Play/PlayerConfiguration.cs b/osu.Game/Screens/Play/PlayerConfiguration.cs index 475a234679..cd30ead638 100644 --- a/osu.Game/Screens/Play/PlayerConfiguration.cs +++ b/osu.Game/Screens/Play/PlayerConfiguration.cs @@ -19,5 +19,10 @@ namespace osu.Game.Screens.Play /// Whether the player should be allowed to trigger a restart. /// public bool AllowRestart { get; set; } = true; + + /// + /// Whether the player should be allowed to skip the intro, advancing to the start of gameplay. + /// + public bool AllowSkippingIntro { get; set; } = true; } } From 3148c04fb89493c65159d177e208fc2f03b6187e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 16:53:25 +0900 Subject: [PATCH 5661/6909] Play a sound when starting a timeshift or multiplayer room --- osu.Game/Screens/Multi/Match/RoomSubScreen.cs | 20 +++++++++++++++++++ .../RealtimeMatchSubScreen.cs | 6 +----- .../Multi/Timeshift/TimeshiftRoomSubScreen.cs | 10 +++------- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs index b9d7408946..0598524e81 100644 --- a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs @@ -4,6 +4,8 @@ using System; using System.Linq; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Audio; @@ -11,6 +13,7 @@ using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play; namespace osu.Game.Screens.Multi.Match { @@ -21,6 +24,8 @@ namespace osu.Game.Screens.Multi.Match public override bool DisallowExternalBeatmapRulesetChanges => true; + private SampleChannel sampleStart; + [Resolved(typeof(Room), nameof(Room.Playlist))] protected BindableList Playlist { get; private set; } @@ -30,8 +35,17 @@ namespace osu.Game.Screens.Multi.Match [Resolved] private BeatmapManager beatmapManager { get; set; } + [Resolved(canBeNull: true)] + protected Multiplayer Multiplayer { get; private set; } + private IBindable> managerUpdated; + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); + } + protected override void LoadComplete() { base.LoadComplete(); @@ -71,6 +85,12 @@ namespace osu.Game.Screens.Multi.Match return base.OnExiting(next); } + protected void StartPlay(Func player) + { + sampleStart?.Play(); + Multiplayer?.Push(new PlayerLoader(player)); + } + private void selectedItemChanged() { updateWorkingBeatmap(); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs index 45ac7e25d3..15d997605c 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs @@ -16,7 +16,6 @@ using osu.Game.Screens.Multi.Match; using osu.Game.Screens.Multi.Match.Components; using osu.Game.Screens.Multi.RealtimeMultiplayer.Match; using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; -using osu.Game.Screens.Play; using osu.Game.Users; using ParticipantsList = osu.Game.Screens.Multi.RealtimeMultiplayer.Participants.ParticipantsList; @@ -29,9 +28,6 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public override string ShortTitle => "match"; - [Resolved(canBeNull: true)] - private Multiplayer multiplayer { get; set; } - [Resolved] private StatefulMultiplayerClient client { get; set; } @@ -206,7 +202,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer int[] userIds = client.Room.Users.Where(u => u.State >= MultiplayerUserState.WaitingForLoad).Select(u => u.UserID).ToArray(); - multiplayer?.Push(new PlayerLoader(() => new RealtimePlayer(SelectedItem.Value, userIds))); + StartPlay(() => new RealtimePlayer(SelectedItem.Value, userIds)); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs index fa901179e9..730ad795b2 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs @@ -15,7 +15,6 @@ using osu.Game.Screens.Multi.Match; 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 osu.Game.Users; using Footer = osu.Game.Screens.Multi.Match.Components.Footer; @@ -220,12 +219,9 @@ namespace osu.Game.Screens.Multi.Timeshift }, true); } - private void onStart() + private void onStart() => StartPlay(() => new TimeshiftPlayer(SelectedItem.Value) { - multiplayer?.Push(new PlayerLoader(() => new TimeshiftPlayer(SelectedItem.Value) - { - Exited = () => leaderboard.RefreshScores() - })); - } + Exited = () => leaderboard.RefreshScores() + }); } } From c35454081c931b2d1e836698d4f11186d1c7f56e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 17:17:45 +0900 Subject: [PATCH 5662/6909] Add sound when players change ready state --- .../TestSceneRealtimeReadyButton.cs | 31 ++++++++++++++++ .../Match/RealtimeReadyButton.cs | 36 +++++++++++++++++-- 2 files changed, 64 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs index e9d3ddb32d..825470846c 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs +++ b/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Platform; +using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Online.RealtimeMultiplayer; @@ -124,6 +125,36 @@ namespace osu.Game.Tests.Visual.RealtimeMultiplayer AddAssert("match not started", () => Client.Room?.Users[0].State == MultiplayerUserState.Idle); } + [TestCase(true)] + [TestCase(false)] + public void TestManyUsersChangingState(bool isHost) + { + const int users = 10; + AddStep("setup", () => + { + Client.TransferHost(Client.Room?.Users[0].UserID ?? 0); + for (int i = 0; i < users; i++) + Client.AddUser(new User { Id = i, Username = "Another user" }); + }); + + if (!isHost) + AddStep("transfer host", () => Client.TransferHost(2)); + + addClickButtonStep(); + + AddRepeatStep("change user ready state", () => + { + Client.ChangeUserState(RNG.Next(0, users), RNG.NextBool() ? MultiplayerUserState.Ready : MultiplayerUserState.Idle); + }, 20); + + AddRepeatStep("ready all users", () => + { + var nextUnready = Client.Room?.Users.FirstOrDefault(c => c.State == MultiplayerUserState.Idle); + if (nextUnready != null) + Client.ChangeUserState(nextUnready.UserID, MultiplayerUserState.Ready); + }, users); + } + private void addClickButtonStep() => AddStep("click button", () => { InputManager.MoveMouseTo(button); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs index 5bead2b271..962c6bbead 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs @@ -5,6 +5,8 @@ using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Extensions; @@ -31,8 +33,12 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match [Resolved] private OsuColour colours { get; set; } + private SampleChannel sampleReadyCount; + private readonly ButtonWithTrianglesExposed button; + private int countReady; + public RealtimeReadyButton() { InternalChild = button = new ButtonWithTrianglesExposed @@ -44,6 +50,12 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match }; } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleReadyCount = audio.Samples.Get(@"SongSelect/select-difficulty"); + } + protected override void OnRoomChanged() { base.OnRoomChanged(); @@ -60,6 +72,10 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match Debug.Assert(Room != null); + int newCountReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); + + string countText = $"({newCountReady} / {Room.Users.Count} ready)"; + switch (localUser.State) { case MultiplayerUserState.Idle: @@ -70,18 +86,32 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match case MultiplayerUserState.Ready: if (Room?.Host?.Equals(localUser) == true) { - int countReady = Room.Users.Count(u => u.State == MultiplayerUserState.Ready); - button.Text = $"Start match ({countReady} / {Room.Users.Count} ready)"; + button.Text = $"Start match {countText}"; updateButtonColour(true); } else { - button.Text = "Waiting for host..."; + button.Text = $"Waiting for host... {countText}"; updateButtonColour(false); } break; } + + if (newCountReady != countReady) + { + countReady = newCountReady; + Scheduler.AddOnce(playSound); + } + } + + private void playSound() + { + if (sampleReadyCount != null) + { + sampleReadyCount.Frequency.Value = 0.77f + countReady * 0.06f; + sampleReadyCount.Play(); + } } private void updateButtonColour(bool green) From eb795a212730b87f947f35eb25586a802563b543 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 17:58:38 +0900 Subject: [PATCH 5663/6909] Move all endpoint information to a configuration class --- ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 ++ .../Visual/Gameplay/TestSceneSpectator.cs | 6 ++++ .../DevelopmentOsuConfigManager.cs | 19 ++++++++++++ osu.Game/Online/API/APIAccess.cs | 18 ++++------- .../DevelopmentEndpointConfiguration.cs | 17 +++++++++++ osu.Game/Online/EndpointConfiguration.cs | 30 +++++++++++++++++++ .../Online/ProductionEndpointConfiguration.cs | 17 +++++++++++ .../RealtimeMultiplayerClient.cs | 13 ++++---- .../Spectator/SpectatorStreamingClient.cs | 13 ++++---- osu.Game/OsuGameBase.cs | 18 +++++++---- 10 files changed, 124 insertions(+), 29 deletions(-) create mode 100644 osu.Game/Configuration/DevelopmentOsuConfigManager.cs create mode 100644 osu.Game/Online/DevelopmentEndpointConfiguration.cs create mode 100644 osu.Game/Online/EndpointConfiguration.cs create mode 100644 osu.Game/Online/ProductionEndpointConfiguration.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs index e42ddeb35e..8078c7b994 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Database; +using osu.Game.Online; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu.Scoring; @@ -87,6 +88,7 @@ namespace osu.Game.Tests.Visual.Gameplay private readonly int totalUsers; public TestMultiplayerStreaming(int totalUsers) + : base(new DevelopmentEndpointConfiguration()) { this.totalUsers = totalUsers; } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 1fdff99da6..26524f07da 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -12,6 +12,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Online; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu; @@ -238,6 +239,11 @@ namespace osu.Game.Tests.Visual.Gameplay private int beatmapId; + public TestSpectatorStreamingClient() + : base(new DevelopmentEndpointConfiguration()) + { + } + protected override Task Connect() { return Task.CompletedTask; diff --git a/osu.Game/Configuration/DevelopmentOsuConfigManager.cs b/osu.Game/Configuration/DevelopmentOsuConfigManager.cs new file mode 100644 index 0000000000..ff19dd874c --- /dev/null +++ b/osu.Game/Configuration/DevelopmentOsuConfigManager.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.Platform; +using osu.Framework.Testing; + +namespace osu.Game.Configuration +{ + [ExcludeFromDynamicCompile] + public class DevelopmentOsuConfigManager : OsuConfigManager + { + protected override string Filename => base.Filename.Replace(".ini", ".dev.ini"); + + public DevelopmentOsuConfigManager(Storage storage) + : base(storage) + { + } + } +} diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index ca457ccf71..49c815b2d3 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -26,18 +26,10 @@ namespace osu.Game.Online.API private readonly OAuth authentication; -#if DEBUG - public string Endpoint => @"https://dev.ppy.sh"; - private const string client_secret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT"; -#else - public string Endpoint => @"https://osu.ppy.sh"; - private const string client_secret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; -#endif - - private const string client_id = @"5"; - private readonly Queue queue = new Queue(); + public string Endpoint { get; } + /// /// The username/email provided by the user when initiating a login. /// @@ -61,11 +53,13 @@ namespace osu.Game.Online.API private readonly Logger log; - public APIAccess(OsuConfigManager config) + public APIAccess(OsuConfigManager config, EndpointConfiguration endpointConfiguration) { this.config = config; - authentication = new OAuth(client_id, client_secret, Endpoint); + Endpoint = endpointConfiguration.APIEndpoint; + + authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, Endpoint); log = Logger.GetLogger(LoggingTarget.Network); ProvidedUsername = config.Get(OsuSetting.Username); diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs new file mode 100644 index 0000000000..5e4105f5fd --- /dev/null +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.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.Online +{ + public class DevelopmentEndpointConfiguration : EndpointConfiguration + { + public DevelopmentEndpointConfiguration() + { + APIEndpoint = @"https://dev.ppy.sh"; + APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT"; + APIClientID = "5"; + SpectatorEndpoint = $"{APIEndpoint}/spectator"; + MultiplayerEndpoint = $"{APIEndpoint}/multiplayer"; + } + } +} diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs new file mode 100644 index 0000000000..a8b1a84e62 --- /dev/null +++ b/osu.Game/Online/EndpointConfiguration.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. + +namespace osu.Game.Online +{ + /// + /// Holds configuration for API endpoints. + /// + public class EndpointConfiguration + { + /// + /// The endpoint for the main (osu-web) API. + /// + public string APIEndpoint { get; set; } + + /// + /// The OAuth client secret. + /// + public string APIClientSecret { get; set; } + + /// + /// The OAuth client ID. + /// + public string APIClientID { get; set; } + + public string SpectatorEndpoint { get; set; } + + public string MultiplayerEndpoint { get; set; } + } +} diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs new file mode 100644 index 0000000000..f5c71ef737 --- /dev/null +++ b/osu.Game/Online/ProductionEndpointConfiguration.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.Online +{ + public class ProductionEndpointConfiguration : EndpointConfiguration + { + public ProductionEndpointConfiguration() + { + APIEndpoint = @"https://osu.ppy.sh"; + APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; + APIClientID = "5"; + SpectatorEndpoint = "https://spectator.ppy.sh/spectator"; + MultiplayerEndpoint = "https://spectator.ppy.sh/multiplayer"; + } + } +} diff --git a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs index 4ec5b9af40..0e2b4855da 100644 --- a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs @@ -19,12 +19,6 @@ namespace osu.Game.Online.RealtimeMultiplayer { public class RealtimeMultiplayerClient : StatefulMultiplayerClient { -#if DEBUG - private const string endpoint = "https://dev.ppy.sh/multiplayer"; -#else - private const string endpoint = "https://spectator.ppy.sh/multiplayer"; -#endif - public override IBindable IsConnected => isConnected; private readonly Bindable isConnected = new Bindable(); @@ -35,6 +29,13 @@ namespace osu.Game.Online.RealtimeMultiplayer private HubConnection? connection; + private readonly string endpoint; + + public RealtimeMultiplayerClient(EndpointConfiguration endpoints) + { + endpoint = endpoints.MultiplayerEndpoint; + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index c9203b595e..1432fd1c98 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -81,6 +81,13 @@ namespace osu.Game.Online.Spectator ///
    public event Action OnUserFinishedPlaying; + private readonly string endpoint; + + public SpectatorStreamingClient(EndpointConfiguration endpoints) + { + endpoint = endpoints.SpectatorEndpoint; + } + [BackgroundDependencyLoader] private void load() { @@ -104,12 +111,6 @@ namespace osu.Game.Online.Spectator } } -#if DEBUG - private const string endpoint = "https://dev.ppy.sh/spectator"; -#else - private const string endpoint = "https://spectator.ppy.sh/spectator"; -#endif - protected virtual async Task Connect() { if (connection != null) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index eb27821d82..bdc9e5eb7b 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -30,6 +30,7 @@ using osu.Game.Database; using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; +using osu.Game.Online; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Online.Spectator; using osu.Game.Overlays; @@ -54,6 +55,8 @@ namespace osu.Game public const int SAMPLE_CONCURRENCY = 6; + public bool UseDevelopmentServer { get; } + protected OsuConfigManager LocalConfig; protected BeatmapManager BeatmapManager; @@ -132,6 +135,7 @@ namespace osu.Game public OsuGameBase() { + UseDevelopmentServer = DebugUtils.IsDebugBuild; Name = @"osu!lazer"; } @@ -170,7 +174,7 @@ namespace osu.Game dependencies.Cache(largeStore); dependencies.CacheAs(this); - dependencies.Cache(LocalConfig); + dependencies.CacheAs(LocalConfig); AddFont(Resources, @"Fonts/osuFont"); @@ -210,10 +214,12 @@ namespace osu.Game } }); - dependencies.CacheAs(API ??= new APIAccess(LocalConfig)); + EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); - dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient()); - dependencies.CacheAs(multiplayerClient = new RealtimeMultiplayerClient()); + dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints)); + + dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient(endpoints)); + dependencies.CacheAs(multiplayerClient = new RealtimeMultiplayerClient(endpoints)); var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); @@ -369,7 +375,9 @@ namespace osu.Game // may be non-null for certain tests Storage ??= host.Storage; - LocalConfig ??= new OsuConfigManager(Storage); + LocalConfig ??= UseDevelopmentServer + ? new DevelopmentOsuConfigManager(Storage) + : new OsuConfigManager(Storage); } protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage); From 323da82477304456af0c9a4bd04e089061c6ea3e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 18:11:40 +0900 Subject: [PATCH 5664/6909] Add website root URL and update most links to use it For what it's worth, I intentionally didn't include news / changelog / supporter, because these should never change. --- osu.Game/Online/API/APIAccess.cs | 11 +++++++---- osu.Game/Online/API/APIRequest.cs | 2 +- osu.Game/Online/API/DummyAPIAccess.cs | 4 +++- osu.Game/Online/API/IAPIProvider.cs | 7 ++++++- osu.Game/Online/Chat/NowPlayingCommand.cs | 2 +- .../Online/DevelopmentEndpointConfiguration.cs | 6 +++--- osu.Game/Online/EndpointConfiguration.cs | 17 ++++++++++++++--- .../Online/ProductionEndpointConfiguration.cs | 6 +++--- .../RealtimeMultiplayerClient.cs | 2 +- .../Spectator/SpectatorStreamingClient.cs | 2 +- osu.Game/OsuGame.cs | 2 +- osu.Game/Overlays/BeatmapSet/Header.cs | 6 +++++- .../Profile/Header/BottomHeaderContainer.cs | 6 +++++- .../Profile/Header/TopHeaderContainer.cs | 6 +++++- .../Sections/Recent/DrawableRecentActivity.cs | 2 +- 15 files changed, 57 insertions(+), 24 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 49c815b2d3..133ba22406 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -28,7 +28,9 @@ namespace osu.Game.Online.API private readonly Queue queue = new Queue(); - public string Endpoint { get; } + public string APIEndpointUrl { get; } + + public string WebsiteRootUrl { get; } /// /// The username/email provided by the user when initiating a login. @@ -57,9 +59,10 @@ namespace osu.Game.Online.API { this.config = config; - Endpoint = endpointConfiguration.APIEndpoint; + APIEndpointUrl = endpointConfiguration.APIEndpointUrl; + WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; - authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, Endpoint); + authentication = new OAuth(endpointConfiguration.APIClientID, endpointConfiguration.APIClientSecret, APIEndpointUrl); log = Logger.GetLogger(LoggingTarget.Network); ProvidedUsername = config.Get(OsuSetting.Username); @@ -245,7 +248,7 @@ namespace osu.Game.Online.API var req = new RegistrationRequest { - Url = $@"{Endpoint}/users", + Url = $@"{APIEndpointUrl}/users", Method = HttpMethod.Post, Username = username, Email = email, diff --git a/osu.Game/Online/API/APIRequest.cs b/osu.Game/Online/API/APIRequest.cs index 6912d9b629..a7174324d8 100644 --- a/osu.Game/Online/API/APIRequest.cs +++ b/osu.Game/Online/API/APIRequest.cs @@ -57,7 +57,7 @@ namespace osu.Game.Online.API protected virtual WebRequest CreateWebRequest() => new OsuWebRequest(Uri); - protected virtual string Uri => $@"{API.Endpoint}/api/v2/{Target}"; + protected virtual string Uri => $@"{API.APIEndpointUrl}/api/v2/{Target}"; protected APIAccess API; protected WebRequest WebRequest; diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 265298270c..3e996ac97f 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -28,7 +28,9 @@ namespace osu.Game.Online.API public string ProvidedUsername => LocalUser.Value.Username; - public string Endpoint => "http://localhost"; + public string APIEndpointUrl => "http://localhost"; + + public string WebsiteRootUrl => "http://localhost"; /// /// Provide handling logic for an arbitrary API request. diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 3a444460f2..4407f1f55e 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -46,7 +46,12 @@ namespace osu.Game.Online.API /// /// The URL endpoint for this API. Does not include a trailing slash. /// - string Endpoint { get; } + string APIEndpointUrl { get; } + + /// + /// The root URL of of the website, excluding the trailing slash. + /// + public string WebsiteRootUrl { get; } /// /// The current connection state of the API. diff --git a/osu.Game/Online/Chat/NowPlayingCommand.cs b/osu.Game/Online/Chat/NowPlayingCommand.cs index c0b54812b6..926709694b 100644 --- a/osu.Game/Online/Chat/NowPlayingCommand.cs +++ b/osu.Game/Online/Chat/NowPlayingCommand.cs @@ -46,7 +46,7 @@ namespace osu.Game.Online.Chat break; } - var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[https://osu.ppy.sh/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString(); + var beatmapString = beatmap.OnlineBeatmapID.HasValue ? $"[{api.WebsiteRootUrl}/b/{beatmap.OnlineBeatmapID} {beatmap}]" : beatmap.ToString(); channelManager.PostMessage($"is {verb} {beatmapString}", true); Expire(); diff --git a/osu.Game/Online/DevelopmentEndpointConfiguration.cs b/osu.Game/Online/DevelopmentEndpointConfiguration.cs index 5e4105f5fd..69531dbe1b 100644 --- a/osu.Game/Online/DevelopmentEndpointConfiguration.cs +++ b/osu.Game/Online/DevelopmentEndpointConfiguration.cs @@ -7,11 +7,11 @@ namespace osu.Game.Online { public DevelopmentEndpointConfiguration() { - APIEndpoint = @"https://dev.ppy.sh"; + WebsiteRootUrl = APIEndpointUrl = @"https://dev.ppy.sh"; APIClientSecret = @"3LP2mhUrV89xxzD1YKNndXHEhWWCRLPNKioZ9ymT"; APIClientID = "5"; - SpectatorEndpoint = $"{APIEndpoint}/spectator"; - MultiplayerEndpoint = $"{APIEndpoint}/multiplayer"; + SpectatorEndpointUrl = $"{APIEndpointUrl}/spectator"; + MultiplayerEndpointUrl = $"{APIEndpointUrl}/multiplayer"; } } } diff --git a/osu.Game/Online/EndpointConfiguration.cs b/osu.Game/Online/EndpointConfiguration.cs index a8b1a84e62..e347d3c653 100644 --- a/osu.Game/Online/EndpointConfiguration.cs +++ b/osu.Game/Online/EndpointConfiguration.cs @@ -8,10 +8,15 @@ namespace osu.Game.Online /// public class EndpointConfiguration { + /// + /// The base URL for the website. + /// + public string WebsiteRootUrl { get; set; } + /// /// The endpoint for the main (osu-web) API. /// - public string APIEndpoint { get; set; } + public string APIEndpointUrl { get; set; } /// /// The OAuth client secret. @@ -23,8 +28,14 @@ namespace osu.Game.Online /// public string APIClientID { get; set; } - public string SpectatorEndpoint { get; set; } + /// + /// The endpoint for the SignalR spectator server. + /// + public string SpectatorEndpointUrl { get; set; } - public string MultiplayerEndpoint { get; set; } + /// + /// The endpoint for the SignalR multiplayer server. + /// + public string MultiplayerEndpointUrl { get; set; } } } diff --git a/osu.Game/Online/ProductionEndpointConfiguration.cs b/osu.Game/Online/ProductionEndpointConfiguration.cs index f5c71ef737..c6ddc03564 100644 --- a/osu.Game/Online/ProductionEndpointConfiguration.cs +++ b/osu.Game/Online/ProductionEndpointConfiguration.cs @@ -7,11 +7,11 @@ namespace osu.Game.Online { public ProductionEndpointConfiguration() { - APIEndpoint = @"https://osu.ppy.sh"; + WebsiteRootUrl = APIEndpointUrl = @"https://osu.ppy.sh"; APIClientSecret = @"FGc9GAtyHzeQDshWP5Ah7dega8hJACAJpQtw6OXk"; APIClientID = "5"; - SpectatorEndpoint = "https://spectator.ppy.sh/spectator"; - MultiplayerEndpoint = "https://spectator.ppy.sh/multiplayer"; + SpectatorEndpointUrl = "https://spectator.ppy.sh/spectator"; + MultiplayerEndpointUrl = "https://spectator.ppy.sh/multiplayer"; } } } diff --git a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs index 0e2b4855da..bfc89df483 100644 --- a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs +++ b/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs @@ -33,7 +33,7 @@ namespace osu.Game.Online.RealtimeMultiplayer public RealtimeMultiplayerClient(EndpointConfiguration endpoints) { - endpoint = endpoints.MultiplayerEndpoint; + endpoint = endpoints.MultiplayerEndpointUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 1432fd1c98..344b73f3d9 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -85,7 +85,7 @@ namespace osu.Game.Online.Spectator public SpectatorStreamingClient(EndpointConfiguration endpoints) { - endpoint = endpoints.SpectatorEndpoint; + endpoint = endpoints.SpectatorEndpointUrl; } [BackgroundDependencyLoader] diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index bb51c55551..17831ed26b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -294,7 +294,7 @@ namespace osu.Game public void OpenUrlExternally(string url) => waitForReady(() => externalLinkOpener, _ => { if (url.StartsWith('/')) - url = $"{API.Endpoint}{url}"; + url = $"{API.APIEndpointUrl}{url}"; externalLinkOpener.OpenUrlExternally(url); }); diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 06e31277dd..321e496511 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -15,6 +15,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Rulesets; @@ -40,6 +41,9 @@ namespace osu.Game.Overlays.BeatmapSet public bool DownloadButtonsVisible => downloadButtonsContainer.Any(); + [Resolved] + private IAPIProvider api { get; set; } + public BeatmapRulesetSelector RulesetSelector => beatmapSetHeader.RulesetSelector; public readonly BeatmapPicker Picker; @@ -213,7 +217,7 @@ namespace osu.Game.Overlays.BeatmapSet Picker.Beatmap.ValueChanged += b => { Details.Beatmap = b.NewValue; - externalLink.Link = $@"https://osu.ppy.sh/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; + externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; }; } diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index ebee377a51..2925107766 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; +using osu.Game.Online.API; using osu.Game.Users; using osuTK; using osuTK.Graphics; @@ -27,6 +28,9 @@ namespace osu.Game.Overlays.Profile.Header private Color4 iconColour; + [Resolved] + private IAPIProvider api { get; set; } + public BottomHeaderContainer() { AutoSizeAxes = Axes.Y; @@ -109,7 +113,7 @@ namespace osu.Game.Overlays.Profile.Header } topLinkContainer.AddText("Contributed "); - topLinkContainer.AddLink($@"{user.PostCount:#,##0} forum posts", $"https://osu.ppy.sh/users/{user.Id}/posts", creationParameters: embolden); + topLinkContainer.AddLink($@"{user.PostCount:#,##0} forum posts", $"{api.WebsiteRootUrl}/users/{user.Id}/posts", creationParameters: embolden); string websiteWithoutProtocol = user.Website; diff --git a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs index 2cc1f6533f..e0642d650c 100644 --- a/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/TopHeaderContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.API; using osu.Game.Overlays.Profile.Header.Components; using osu.Game.Users; using osu.Game.Users.Drawables; @@ -23,6 +24,9 @@ namespace osu.Game.Overlays.Profile.Header public readonly Bindable User = new Bindable(); + [Resolved] + private IAPIProvider api { get; set; } + private SupporterIcon supporterTag; private UpdateableAvatar avatar; private OsuSpriteText usernameText; @@ -166,7 +170,7 @@ namespace osu.Game.Overlays.Profile.Header { avatar.User = user; usernameText.Text = user?.Username ?? string.Empty; - openUserExternally.Link = $@"https://osu.ppy.sh/users/{user?.Id ?? 0}"; + openUserExternally.Link = $@"{api.WebsiteRootUrl}/users/{user?.Id ?? 0}"; userFlag.Country = user?.Country; userCountryText.Text = user?.Country?.FullName ?? "Alien"; supporterTag.SupportLevel = user?.SupportLevel ?? 0; diff --git a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs index 8782e82642..49b46f7e7a 100644 --- a/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs +++ b/osu.Game/Overlays/Profile/Sections/Recent/DrawableRecentActivity.cs @@ -216,7 +216,7 @@ namespace osu.Game.Overlays.Profile.Sections.Recent private void addBeatmapsetLink() => content.AddLink(activity.Beatmapset?.Title, LinkAction.OpenBeatmapSet, getLinkArgument(activity.Beatmapset?.Url), creationParameters: t => t.Font = getLinkFont()); - private string getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.Endpoint}{url}").Argument; + private string getLinkArgument(string url) => MessageFormatter.GetLinkDetails($"{api.APIEndpointUrl}{url}").Argument; private FontUsage getLinkFont(FontWeight fontWeight = FontWeight.Regular) => OsuFont.GetFont(size: font_size, weight: fontWeight, italics: true); From 261c250b46dae0db37151601a2d789c0e789b5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Dec 2020 11:33:49 +0100 Subject: [PATCH 5665/6909] Update outdated comment --- osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index 20b184bed3..b1179ea7cd 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -133,7 +133,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer await client.ChangeState(MultiplayerUserState.FinishedPlay); - // Await up to 30 seconds for results to become available (3 api request timeouts). + // Await up to 60 seconds for results to become available (6 api request timeouts). // This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur. await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))); } From 40b9d1bc5ef7b3f0c63766a7478f278aaa565cc6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Dec 2020 12:45:01 +0100 Subject: [PATCH 5666/6909] Invert if & early-return to reduce nesting --- .../RealtimeMultiplayer/Match/RealtimeReadyButton.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs index 962c6bbead..be405feef1 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs @@ -107,11 +107,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match private void playSound() { - if (sampleReadyCount != null) - { - sampleReadyCount.Frequency.Value = 0.77f + countReady * 0.06f; - sampleReadyCount.Play(); - } + if (sampleReadyCount == null) + return; + + sampleReadyCount.Frequency.Value = 0.77f + countReady * 0.06f; + sampleReadyCount.Play(); } private void updateButtonColour(bool green) From 66a23c22e5580f6866db87aebfe503d195a2a8dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 21:28:21 +0900 Subject: [PATCH 5667/6909] Fix various tests failing due to dependence on specific online data --- .../Online/TestSceneChangelogOverlay.cs | 5 +- .../Online/TestSceneNowPlayingCommand.cs | 2 +- .../Requests/Responses/APIChangelogEntry.cs | 3 +- osu.Game/Overlays/Changelog/ChangelogBuild.cs | 49 ++++++++++--------- osu.Game/Overlays/ChangelogOverlay.cs | 1 + 5 files changed, 33 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 02f6de2269..998e42b478 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -41,6 +41,7 @@ namespace osu.Game.Tests.Visual.Online } [Test] + [Ignore("needs to be updated to not be so server dependent")] public void ShowWithBuild() { AddStep(@"Show with Lazer 2018.712.0", () => @@ -49,7 +50,7 @@ namespace osu.Game.Tests.Visual.Online { Version = "2018.712.0", DisplayVersion = "2018.712.0", - UpdateStream = new APIUpdateStream { Id = 7, Name = OsuGameBase.CLIENT_STREAM_NAME }, + UpdateStream = new APIUpdateStream { Id = 5, Name = OsuGameBase.CLIENT_STREAM_NAME }, ChangelogEntries = new List { new APIChangelogEntry @@ -64,7 +65,7 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep(@"wait for streams", () => changelog.Streams?.Count > 0); AddAssert(@"correct build displayed", () => changelog.Current.Value.Version == "2018.712.0"); - AddAssert(@"correct stream selected", () => changelog.Header.Streams.Current.Value.Id == 7); + AddAssert(@"correct stream selected", () => changelog.Header.Streams.Current.Value.Id == 5); } [Test] diff --git a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs index 0324da6cf5..64e80e9f02 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneNowPlayingCommand.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("Run command", () => Add(new NowPlayingCommand())); if (hasOnlineId) - AddAssert("Check link presence", () => postTarget.LastMessage.Contains("https://osu.ppy.sh/b/1234")); + AddAssert("Check link presence", () => postTarget.LastMessage.Contains("/b/1234")); else AddAssert("Check link not present", () => !postTarget.LastMessage.Contains("https://")); } diff --git a/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs b/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs index f949ab5da5..1ff7523ba6 100644 --- a/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs +++ b/osu.Game/Online/API/Requests/Responses/APIChangelogEntry.cs @@ -48,6 +48,7 @@ namespace osu.Game.Online.API.Requests.Responses public enum ChangelogEntryType { Add, - Fix + Fix, + Misc } } diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs index 48bf6c2ddd..65ff0fef92 100644 --- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -131,33 +131,36 @@ namespace osu.Game.Overlays.Changelog t.Padding = new MarginPadding { Left = 10 }; }); - if (entry.GithubUser.UserId != null) + if (entry.GithubUser != null) { - title.AddUserLink(new User + if (entry.GithubUser.UserId != null) { - Username = entry.GithubUser.OsuUsername, - Id = entry.GithubUser.UserId.Value - }, t => + title.AddUserLink(new User + { + Username = entry.GithubUser.OsuUsername, + Id = entry.GithubUser.UserId.Value + }, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } + else if (entry.GithubUser.GithubUrl != null) { - t.Font = fontMedium; - t.Colour = entryColour; - }); - } - else if (entry.GithubUser.GithubUrl != null) - { - title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => + title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } + else { - t.Font = fontMedium; - t.Colour = entryColour; - }); - } - else - { - title.AddText(entry.GithubUser.DisplayName, t => - { - t.Font = fontMedium; - t.Colour = entryColour; - }); + title.AddText(entry.GithubUser.DisplayName, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } } ChangelogEntries.Add(titleContainer); diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index c7e9a86fa4..f591b1d427 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -86,6 +86,7 @@ namespace osu.Game.Overlays } public void ShowListing() + { Current.Value = null; Show(); From 4270c29f6083cea4abc28f9d82aef3bc6c5c222a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Dec 2020 13:42:08 +0100 Subject: [PATCH 5668/6909] Trim stray newline --- osu.Game/Overlays/ChangelogOverlay.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index f591b1d427..c7e9a86fa4 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -86,7 +86,6 @@ namespace osu.Game.Overlays } public void ShowListing() - { Current.Value = null; Show(); From d5c348b568096b1a10fc2d2a04a0b2dd922c20e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Dec 2020 13:44:46 +0100 Subject: [PATCH 5669/6909] Remove explicit public access modifier from interface --- osu.Game/Online/API/IAPIProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 4407f1f55e..1951dfaf40 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -51,7 +51,7 @@ namespace osu.Game.Online.API /// /// The root URL of of the website, excluding the trailing slash. /// - public string WebsiteRootUrl { get; } + string WebsiteRootUrl { get; } /// /// The current connection state of the API. From f991448a3e6f359efa277f4491c472421c311ca9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 21:49:38 +0900 Subject: [PATCH 5670/6909] Re-sort the leaderboard order a maximum of once a second --- .../Screens/Play/HUD/GameplayLeaderboard.cs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index cab1cbd3f1..e33cc05e64 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Users; @@ -12,6 +13,8 @@ namespace osu.Game.Screens.Play.HUD { public class GameplayLeaderboard : FillFlowContainer { + private readonly Cached sorting = new Cached(); + public GameplayLeaderboard() { Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH; @@ -24,6 +27,13 @@ namespace osu.Game.Screens.Play.HUD LayoutEasing = Easing.OutQuint; } + protected override void LoadComplete() + { + base.LoadComplete(); + + Scheduler.AddDelayed(sort, 1000, true); + } + /// /// Adds a player to the leaderboard. /// @@ -41,7 +51,7 @@ namespace osu.Game.Screens.Play.HUD }; base.Add(drawable); - drawable.TotalScore.BindValueChanged(_ => Scheduler.AddOnce(sort), true); + drawable.TotalScore.BindValueChanged(_ => sorting.Invalidate(), true); Height = Count * (GameplayLeaderboardScore.PANEL_HEIGHT + Spacing.Y); @@ -55,6 +65,9 @@ namespace osu.Game.Screens.Play.HUD private void sort() { + if (sorting.IsValid) + return; + var orderedByScore = this.OrderByDescending(i => i.TotalScore.Value).ToList(); for (int i = 0; i < Count; i++) @@ -62,6 +75,8 @@ namespace osu.Game.Screens.Play.HUD SetLayoutPosition(orderedByScore[i], i); orderedByScore[i].ScorePosition = i + 1; } + + sorting.Validate(); } } } From aec25e2d73079043d810fbc97da19441c1b1c581 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 21:53:18 +0900 Subject: [PATCH 5671/6909] Rename "timeshift" to "playlists" This only covers the user-facing instances. Code and class name changes will happen once things have calmed down. --- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- osu.Game/Screens/Menu/Disclaimer.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index badfa3f693..5af6517f49 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.Menu { buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); - buttonsPlay.Add(new Button(@"timeshift", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onTimeshift, 0, Key.L)); + buttonsPlay.Add(new Button(@"playlists", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onTimeshift, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); diff --git a/osu.Game/Screens/Menu/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index ceec12c967..9c7206d259 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -208,7 +208,7 @@ namespace osu.Game.Screens.Menu "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 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!", + "Check out the \"playlist\" system, which lets users create their own custom and permanent leaderboards!", "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 3a46e210d48d903bc631021979518f1fcab59293 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 21:59:10 +0900 Subject: [PATCH 5672/6909] Change low-hanging references of "room" to "playlist" --- osu.Game/Screens/Multi/Multiplayer.cs | 6 +++--- .../Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs | 2 +- .../Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs | 2 +- osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs | 6 ++++++ 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index a7d40a89d3..585ac71189 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -59,7 +59,7 @@ namespace osu.Game.Screens.Multi private OsuGameBase game { get; set; } [Resolved] - private IAPIProvider api { get; set; } + protected IAPIProvider API { get; private set; } [Resolved(CanBeNull = true)] private OsuLogo logo { get; set; } @@ -155,7 +155,7 @@ namespace osu.Game.Screens.Multi [BackgroundDependencyLoader(true)] private void load(IdleTracker idleTracker) { - apiState.BindTo(api.State); + apiState.BindTo(API.State); apiState.BindValueChanged(onlineStateChanged, true); if (idleTracker != null) @@ -269,7 +269,7 @@ namespace osu.Game.Screens.Multi /// Creates a new room. /// /// The created . - protected virtual Room CreateNewRoom() => new Room { Name = { Value = $"{api.LocalUser}'s awesome room" } }; + protected abstract Room CreateNewRoom(); private void screenPushed(IScreen lastScreen, IScreen newScreen) { diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs index 6739a51fe8..e15a6bd408 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override Room CreateNewRoom() { - var room = base.CreateNewRoom(); + var room = new Room { Name = { Value = $"{API.LocalUser}'s awesome room" } }; room.Category.Value = RoomCategory.Realtime; return room; } diff --git a/osu.Game/Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs b/osu.Game/Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs index bd9d667630..0424493472 100644 --- a/osu.Game/Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs +++ b/osu.Game/Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.Multi.Timeshift { Triangles.TriangleScale = 1.5f; - Text = "Create room"; + Text = "Create playlist"; } } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs index d525a3800d..e1b94f8455 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs @@ -4,6 +4,7 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Match; @@ -45,6 +46,11 @@ namespace osu.Game.Screens.Multi.Timeshift Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})"); } + protected override Room CreateNewRoom() + { + return new Room { Name = { Value = $"{API.LocalUser}'s awesome playlist" } }; + } + protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager(); protected override LoungeSubScreen CreateLounge() => new TimeshiftLoungeSubScreen(); From d0e834796818beedbfbb9ecb3ab70dfd8bb18b5a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 24 Dec 2020 22:28:25 +0900 Subject: [PATCH 5673/6909] Change asserts into until steps --- .../Gameplay/TestSceneGameplayLeaderboard.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index ca61672ef9..c0a021436e 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -52,19 +52,19 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("add player 2", () => createLeaderboardScore(player2Score, new User { Username = "Player 2" })); AddStep("add player 3", () => createLeaderboardScore(player3Score, new User { Username = "Player 3" })); - AddAssert("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); - AddAssert("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); - AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); + AddUntilStep("is player 2 position #1", () => leaderboard.CheckPositionByUsername("Player 2", 1)); + AddUntilStep("is player position #2", () => leaderboard.CheckPositionByUsername("You", 2)); + AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); AddStep("set score above player 3", () => player2Score.Value = playerScore.Value - 500); - AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); - AddAssert("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2)); - AddAssert("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); + AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); + AddUntilStep("is player 2 position #2", () => leaderboard.CheckPositionByUsername("Player 2", 2)); + AddUntilStep("is player 3 position #3", () => leaderboard.CheckPositionByUsername("Player 3", 3)); AddStep("set score below players", () => player2Score.Value = playerScore.Value - 123456); - AddAssert("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); - AddAssert("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2)); - AddAssert("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); + AddUntilStep("is player position #1", () => leaderboard.CheckPositionByUsername("You", 1)); + AddUntilStep("is player 3 position #2", () => leaderboard.CheckPositionByUsername("Player 3", 2)); + AddUntilStep("is player 2 position #3", () => leaderboard.CheckPositionByUsername("Player 2", 3)); } [Test] From 76a7aabfe86b73d5619ff4b3e0d6d23e74907f00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Dec 2020 14:32:30 +0100 Subject: [PATCH 5674/6909] Always create realtime-specific player elements regardless of token --- .../Multi/RealtimeMultiplayer/RealtimePlayer.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index 07d21995ce..8eb6a11228 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics; using System.Threading.Tasks; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; @@ -35,7 +34,6 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private readonly TaskCompletionSource resultsReady = new TaskCompletionSource(); - [CanBeNull] private MultiplayerGameplayLeaderboard leaderboard; private readonly int[] userIds; @@ -61,6 +59,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer [BackgroundDependencyLoader] private void load() { + // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. + LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); + + HUDOverlay.Add(loadingDisplay = new LoadingLayer(DrawableRuleset) { Depth = float.MaxValue }); + if (Token == null) return; // Todo: Somehow handle token retrieval failure. @@ -84,11 +87,6 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer }, true); Debug.Assert(client.Room != null); - - // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. - LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); - - HUDOverlay.Add(loadingDisplay = new LoadingLayer(DrawableRuleset) { Depth = float.MaxValue }); } protected override void StartGameplay() @@ -115,9 +113,6 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private void adjustLeaderboardPosition() { - if (leaderboard == null) - return; - const float padding = 44; // enough margin to avoid the hit error display. leaderboard.Position = new Vector2( From a97681a5daa18b6e4374a901b65e836a1bef82eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Dec 2020 15:07:03 +0100 Subject: [PATCH 5675/6909] Proxy screen transition events to subscreens in multiplayer --- osu.Game/Screens/Multi/Multiplayer.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index a7d40a89d3..c4259400fa 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -202,6 +202,11 @@ namespace osu.Game.Screens.Multi { this.FadeIn(); waves.Show(); + + if (loungeSubScreen.IsCurrentScreen()) + loungeSubScreen.OnEntering(last); + else + loungeSubScreen.MakeCurrent(); } public override void OnResuming(IScreen last) @@ -209,6 +214,7 @@ namespace osu.Game.Screens.Multi this.FadeIn(250); this.ScaleTo(1, 250, Easing.OutSine); + screenStack.CurrentScreen?.OnResuming(last); base.OnResuming(last); UpdatePollingRate(isIdle.Value); @@ -219,6 +225,8 @@ namespace osu.Game.Screens.Multi this.ScaleTo(1.1f, 250, Easing.InSine); this.FadeOut(250); + screenStack.CurrentScreen?.OnSuspending(next); + UpdatePollingRate(isIdle.Value); } @@ -230,9 +238,7 @@ namespace osu.Game.Screens.Multi this.Delay(WaveContainer.DISAPPEAR_DURATION).FadeOut(); - if (screenStack.CurrentScreen != null) - loungeSubScreen.MakeCurrent(); - + screenStack.CurrentScreen?.OnExiting(next); base.OnExiting(next); return false; } From 7f0f6d86b0f931401e4fb8b347485ff6656ee354 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Dec 2020 16:08:45 +0100 Subject: [PATCH 5676/6909] Rename {room -> playlist} on playlist room screen --- osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs index 730ad795b2..4a66f09a22 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.Multi.Timeshift { public override string Title { get; } - public override string ShortTitle => "room"; + public override string ShortTitle => "playlist"; [Resolved(typeof(Room), nameof(Room.RoomID))] private Bindable roomId { get; set; } @@ -40,7 +40,7 @@ namespace osu.Game.Screens.Multi.Timeshift public TimeshiftRoomSubScreen(Room room) { - Title = room.RoomID.Value == null ? "New room" : room.Name.Value; + Title = room.RoomID.Value == null ? "New playlist" : room.Name.Value; Activity.Value = new UserActivity.InLobby(room); } From db1c11073f430edd13aa8a480b0e30233de4697e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Dec 2020 16:10:29 +0100 Subject: [PATCH 5677/6909] Rename back to "room" for "realtime" multiplayer --- .../Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs | 2 +- .../Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs index eda907f8cb..cdaeb6faec 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { Triangles.TriangleScale = 1.5f; - Text = "Create match"; + Text = "Create room"; ((IBindable)Enabled).BindTo(multiplayerClient.IsConnected); } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs index 15d997605c..1778bc272c 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs @@ -26,7 +26,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { public override string Title { get; } - public override string ShortTitle => "match"; + public override string ShortTitle => "room"; [Resolved] private StatefulMultiplayerClient client { get; set; } @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer public RealtimeMatchSubScreen(Room room) { - Title = room.RoomID.Value == null ? "New match" : room.Name.Value; + Title = room.RoomID.Value == null ? "New room" : room.Name.Value; Activity.Value = new UserActivity.InLobby(room); } From 6ec045f2353e80534efc1961990ac3d04adf58f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 24 Dec 2020 16:18:35 +0100 Subject: [PATCH 5678/6909] Distinguish primary multi screen titles in header --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs | 2 +- osu.Game/Screens/Multi/Header.cs | 8 ++++---- osu.Game/Screens/Multi/Multiplayer.cs | 4 +++- .../Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs | 2 ++ osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs | 2 ++ 5 files changed, 12 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs index 76ab402b72..0ccd882d95 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs @@ -23,7 +23,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Children = new Drawable[] { screenStack, - new Header(screenStack) + new Header("Multiplayer", screenStack) }; AddStep("push multi screen", () => screenStack.CurrentScreen.Push(new TestMultiplayerSubScreen(++index))); diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/Multi/Header.cs index cd8695286b..637d8bb52b 100644 --- a/osu.Game/Screens/Multi/Header.cs +++ b/osu.Game/Screens/Multi/Header.cs @@ -22,7 +22,7 @@ namespace osu.Game.Screens.Multi { public const float HEIGHT = 80; - public Header(ScreenStack stack) + public Header(string mainTitle, ScreenStack stack) { RelativeSizeAxes = Axes.X; Height = HEIGHT; @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Multi Padding = new MarginPadding { Left = WaveOverlayContainer.WIDTH_PADDING + OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, Children = new Drawable[] { - title = new MultiHeaderTitle + title = new MultiHeaderTitle(mainTitle) { Anchor = Anchor.CentreLeft, Origin = Anchor.BottomLeft, @@ -80,7 +80,7 @@ namespace osu.Game.Screens.Multi set => pageTitle.Text = value.ShortTitle.Titleize(); } - public MultiHeaderTitle() + public MultiHeaderTitle(string mainTitle) { AutoSizeAxes = Axes.Both; @@ -98,7 +98,7 @@ namespace osu.Game.Screens.Multi Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 24), - Text = "Multiplayer" + Text = mainTitle }, dot = new OsuSpriteText { diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer.cs index 585ac71189..56c1c6cb37 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer.cs @@ -127,7 +127,7 @@ namespace osu.Game.Screens.Multi screenStack = new MultiplayerSubScreenStack { RelativeSizeAxes = Axes.Both } } }, - new Header(screenStack), + new Header(ScreenTitle, screenStack), createButton = CreateNewMultiplayerGameButton().With(button => { button.Anchor = Anchor.TopRight; @@ -311,6 +311,8 @@ namespace osu.Game.Screens.Multi protected IScreen CurrentSubScreen => screenStack.CurrentScreen; + protected abstract string ScreenTitle { get; } + protected abstract RoomManager CreateRoomManager(); protected abstract LoungeSubScreen CreateLounge(); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs index e15a6bd408..70308844b0 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs @@ -62,6 +62,8 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer return room; } + protected override string ScreenTitle => "Multiplayer"; + protected override RoomManager CreateRoomManager() => new RealtimeRoomManager(); protected override LoungeSubScreen CreateLounge() => new RealtimeLoungeSubScreen(); diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs index e1b94f8455..60b01ee431 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs +++ b/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs @@ -51,6 +51,8 @@ namespace osu.Game.Screens.Multi.Timeshift return new Room { Name = { Value = $"{API.LocalUser}'s awesome playlist" } }; } + protected override string ScreenTitle => "Playlists"; + protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager(); protected override LoungeSubScreen CreateLounge() => new TimeshiftLoungeSubScreen(); From 60c7c8b63badedf7c0bef7ec7b5c365d1877e486 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Dec 2020 00:44:42 +0900 Subject: [PATCH 5679/6909] Pluralise playlists in tip --- 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 9c7206d259..46fddabb26 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -208,7 +208,7 @@ namespace osu.Game.Screens.Menu "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 temporary until exiting. Restore accidentally deleted content from the maintenance settings!", - "Check out the \"playlist\" system, which lets users create their own custom and permanent leaderboards!", + "Check out the \"playlists\" system, which lets users create their own custom and permanent leaderboards!", "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 a1384942b1e6b2720988cb4618f9eab7bf8058cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Dec 2020 13:11:21 +0900 Subject: [PATCH 5680/6909] Timeshift -> Playlists at a code level --- .../TestSceneMatchSettingsOverlay.cs | 4 +-- .../Multiplayer/TestSceneMultiScreen.cs | 4 +-- ....cs => TestScenePlaylistsFilterControl.cs} | 6 ++-- ...s => TestScenePlaylistsLoungeSubScreen.cs} | 6 ++-- ... => TestScenePlaylistsParticipantsList.cs} | 2 +- ....cs => TestScenePlaylistsResultsScreen.cs} | 4 +-- ....cs => TestScenePlaylistsRoomSubScreen.cs} | 14 ++++----- .../Navigation/TestSceneScreenNavigation.cs | 6 ++-- .../GameTypes/GameTypeTimeshift.cs | 4 +-- osu.Game/Online/Multiplayer/Room.cs | 2 +- osu.Game/Screens/Menu/ButtonSystem.cs | 8 ++--- osu.Game/Screens/Menu/MainMenu.cs | 4 +-- .../Components/TimeshiftFilterControl.cs | 14 ++++----- .../Screens/Multi/Match/Components/Footer.cs | 4 +-- .../Multi/Match/Components/GameTypePicker.cs | 2 +- .../Screens/Multi/Play/TimeshiftPlayer.cs | 8 ++--- .../CreatePlaylistsRoomButton.cs} | 4 +-- .../PlaylistsLoungeSubScreen.cs} | 8 ++--- .../PlaylistsMatchSettingsOverlay.cs} | 4 +-- .../PlaylistsMultiplayer.cs} | 30 +++++++++---------- .../PlaylistsReadyButton.cs} | 6 ++-- .../PlaylistsRoomManager.cs} | 4 +-- .../PlaylistsRoomSubScreen.cs} | 12 ++++---- .../Multi/Ranking/TimeshiftResultsScreen.cs | 4 +-- .../RealtimeMultiplayer.cs | 16 +++++----- .../RealtimeMultiplayer/RealtimePlayer.cs | 4 +-- .../RealtimeResultsScreen.cs | 2 +- 27 files changed, 93 insertions(+), 93 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneTimeshiftFilterControl.cs => TestScenePlaylistsFilterControl.cs} (76%) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneTimeshiftLoungeSubScreen.cs => TestScenePlaylistsLoungeSubScreen.cs} (93%) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneTimeshiftParticipantsList.cs => TestScenePlaylistsParticipantsList.cs} (95%) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneTimeshiftResultsScreen.cs => TestScenePlaylistsResultsScreen.cs} (99%) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneTimeshiftRoomSubScreen.cs => TestScenePlaylistsRoomSubScreen.cs} (93%) rename osu.Game/Screens/Multi/{Timeshift/CreateTimeshiftRoomButton.cs => Playlists/CreatePlaylistsRoomButton.cs} (79%) rename osu.Game/Screens/Multi/{Timeshift/TimeshiftLoungeSubScreen.cs => Playlists/PlaylistsLoungeSubScreen.cs} (71%) rename osu.Game/Screens/Multi/{Timeshift/TimeshiftMatchSettingsOverlay.cs => Playlists/PlaylistsMatchSettingsOverlay.cs} (99%) rename osu.Game/Screens/Multi/{Timeshift/TimeshiftMultiplayer.cs => Playlists/PlaylistsMultiplayer.cs} (62%) rename osu.Game/Screens/Multi/{Timeshift/TimeshiftReadyButton.cs => Playlists/PlaylistsReadyButton.cs} (88%) rename osu.Game/Screens/Multi/{Timeshift/TimeshiftRoomManager.cs => Playlists/PlaylistsRoomManager.cs} (89%) rename osu.Game/Screens/Multi/{Timeshift/TimeshiftRoomSubScreen.cs => Playlists/PlaylistsRoomSubScreen.cs} (97%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs index 1fcae9c709..90abecd26d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Timeshift; +using osu.Game.Screens.Multi.Playlists; namespace osu.Game.Tests.Visual.Multiplayer { @@ -109,7 +109,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("error not displayed", () => !settings.ErrorText.IsPresent); } - private class TestRoomSettings : TimeshiftMatchSettingsOverlay + private class TestRoomSettings : PlaylistsMatchSettingsOverlay { public TriangleButton ApplyButton => ((MatchSettings)Settings).ApplyButton; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs index 0390b995e1..9ac1eb8013 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Overlays; -using osu.Game.Screens.Multi.Timeshift; +using osu.Game.Screens.Multi.Playlists; namespace osu.Game.Tests.Visual.Multiplayer { @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestSceneMultiScreen() { - var multi = new TimeshiftMultiplayer(); + var multi = new PlaylistsMultiplayer(); AddStep("show", () => LoadScreen(multi)); AddUntilStep("wait for loaded", () => multi.IsLoaded); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftFilterControl.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsFilterControl.cs similarity index 76% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftFilterControl.cs rename to osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsFilterControl.cs index f635a28b5c..427a69552d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftFilterControl.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsFilterControl.cs @@ -6,11 +6,11 @@ using osu.Game.Screens.Multi.Lounge.Components; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneTimeshiftFilterControl : OsuTestScene + public class TestScenePlaylistsFilterControl : OsuTestScene { - public TestSceneTimeshiftFilterControl() + public TestScenePlaylistsFilterControl() { - Child = new TimeshiftFilterControl + Child = new PlaylistsFilterControl { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftLoungeSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsLoungeSubScreen.cs similarity index 93% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftLoungeSubScreen.cs rename to osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsLoungeSubScreen.cs index 73afd65d6d..f8788f0c36 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsLoungeSubScreen.cs @@ -10,11 +10,11 @@ using osu.Framework.Testing; using osu.Game.Graphics.Containers; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.Timeshift; +using osu.Game.Screens.Multi.Playlists; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneTimeshiftLoungeSubScreen : RoomManagerTestScene + public class TestScenePlaylistsLoungeSubScreen : RoomManagerTestScene { private LoungeSubScreen loungeScreen; @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { base.SetUpSteps(); - AddStep("push screen", () => LoadScreen(loungeScreen = new TimeshiftLoungeSubScreen + AddStep("push screen", () => LoadScreen(loungeScreen = new PlaylistsLoungeSubScreen { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsParticipantsList.cs similarity index 95% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftParticipantsList.cs rename to osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsParticipantsList.cs index efc3be032c..d71fdc42e3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsParticipantsList.cs @@ -8,7 +8,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneTimeshiftParticipantsList : MultiplayerTestScene + public class TestScenePlaylistsParticipantsList : MultiplayerTestScene { [SetUp] public new void Setup() => Schedule(() => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsResultsScreen.cs similarity index 99% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs rename to osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsResultsScreen.cs index 03fd2b968c..99fc6597ee 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftResultsScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsResultsScreen.cs @@ -26,7 +26,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneTimeshiftResultsScreen : ScreenTestScene + public class TestScenePlaylistsResultsScreen : ScreenTestScene { private const int scores_per_result = 10; @@ -360,7 +360,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }; } - private class TestResultsScreen : TimeshiftResultsScreen + private class TestResultsScreen : PlaylistsResultsScreen { public new LoadingSpinner LeftSpinner => base.LeftSpinner; public new LoadingSpinner CentreSpinner => base.CentreSpinner; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSubScreen.cs similarity index 93% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs rename to osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSubScreen.cs index bbd7d84081..02cc03eca4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneTimeshiftRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSubScreen.cs @@ -17,14 +17,14 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Multi.Timeshift; +using osu.Game.Screens.Multi.Playlists; using osu.Game.Tests.Beatmaps; using osu.Game.Users; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneTimeshiftRoomSubScreen : MultiplayerTestScene + public class TestScenePlaylistsRoomSubScreen : MultiplayerTestScene { protected override bool UseOnlineAPI => true; @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager manager; private RulesetStore rulesets; - private TestTimeshiftRoomSubScreen match; + private TestPlaylistsRoomSubScreen match; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public void SetupSteps() { - AddStep("load match", () => LoadScreen(match = new TestTimeshiftRoomSubScreen(Room))); + AddStep("load match", () => LoadScreen(match = new TestPlaylistsRoomSubScreen(Room))); AddUntilStep("wait for load", () => match.IsCurrentScreen()); } @@ -120,7 +120,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("create room", () => { - InputManager.MoveMouseTo(match.ChildrenOfType().Single()); + InputManager.MoveMouseTo(match.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); @@ -131,13 +131,13 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("match has original beatmap", () => match.Beatmap.Value.Beatmap.BeatmapInfo.BaseDifficulty.CircleSize != 1); } - private class TestTimeshiftRoomSubScreen : TimeshiftRoomSubScreen + private class TestPlaylistsRoomSubScreen : PlaylistsRoomSubScreen { public new Bindable SelectedItem => base.SelectedItem; public new Bindable Beatmap => base.Beatmap; - public TestTimeshiftRoomSubScreen(Room room) + public TestPlaylistsRoomSubScreen(Room room) : base(room) { } diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 43f97d8ace..381bbca74f 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -11,7 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; -using osu.Game.Screens.Multi.Timeshift; +using osu.Game.Screens.Multi.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Options; @@ -108,14 +108,14 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitMultiWithEscape() { - PushAndConfirm(() => new TimeshiftMultiplayer()); + PushAndConfirm(() => new PlaylistsMultiplayer()); exitViaEscapeAndConfirm(); } [Test] public void TestExitMultiWithBackButton() { - PushAndConfirm(() => new TimeshiftMultiplayer()); + PushAndConfirm(() => new PlaylistsMultiplayer()); exitViaBackButtonAndConfirm(); } diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs b/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs index 1a3d2837ce..5840ccb803 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs +++ b/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs @@ -8,9 +8,9 @@ using osuTK; namespace osu.Game.Online.Multiplayer.GameTypes { - public class GameTypeTimeshift : GameType + public class GameTypePlaylists : GameType { - public override string Name => "Timeshift"; + public override string Name => "Playlists"; public override Drawable GetIcon(OsuColour colours, float size) => new SpriteIcon { diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Multiplayer/Room.cs index 11efe281d1..d13d7dc774 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Multiplayer/Room.cs @@ -66,7 +66,7 @@ namespace osu.Game.Online.Multiplayer [Cached] [JsonIgnore] - public readonly Bindable Type = new Bindable(new GameTypeTimeshift()); + public readonly Bindable Type = new Bindable(new GameTypePlaylists()); [Cached] [JsonIgnore] diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 5af6517f49..474cbde192 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Menu public Action OnSolo; public Action OnSettings; public Action OnMultiplayer; - public Action OnTimeshift; + public Action OnPlaylists; public const float BUTTON_WIDTH = 140f; public const float WEDGE_WIDTH = 20; @@ -125,7 +125,7 @@ namespace osu.Game.Screens.Menu { buttonsPlay.Add(new Button(@"solo", @"button-solo-select", FontAwesome.Solid.User, new Color4(102, 68, 204, 255), () => OnSolo?.Invoke(), WEDGE_WIDTH, Key.P)); buttonsPlay.Add(new Button(@"multi", @"button-generic-select", FontAwesome.Solid.Users, new Color4(94, 63, 186, 255), onMultiplayer, 0, Key.M)); - buttonsPlay.Add(new Button(@"playlists", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onTimeshift, 0, Key.L)); + buttonsPlay.Add(new Button(@"playlists", @"button-generic-select", OsuIcon.Charts, new Color4(94, 63, 186, 255), onPlaylists, 0, Key.L)); buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); buttonsTopLevel.Add(new Button(@"play", @"button-play-select", OsuIcon.Logo, new Color4(102, 68, 204, 255), () => State = ButtonSystemState.Play, WEDGE_WIDTH, Key.P)); @@ -175,7 +175,7 @@ namespace osu.Game.Screens.Menu OnMultiplayer?.Invoke(); } - private void onTimeshift() + private void onPlaylists() { if (!api.IsLoggedIn) { @@ -193,7 +193,7 @@ namespace osu.Game.Screens.Menu return; } - OnTimeshift?.Invoke(); + OnPlaylists?.Invoke(); } private void updateIdleState(bool isIdle) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index fa96ac9c51..5650b22f25 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -18,7 +18,7 @@ using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Multi.RealtimeMultiplayer; -using osu.Game.Screens.Multi.Timeshift; +using osu.Game.Screens.Multi.Playlists; using osu.Game.Screens.Select; namespace osu.Game.Screens.Menu @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Menu }, OnSolo = onSolo, OnMultiplayer = () => this.Push(new RealtimeMultiplayer()), - OnTimeshift = () => this.Push(new TimeshiftMultiplayer()), + OnPlaylists = () => this.Push(new PlaylistsMultiplayer()), OnExit = confirmAndExit, } } diff --git a/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs index 68cab283a0..3c55c3c43f 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs @@ -7,13 +7,13 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Screens.Multi.Lounge.Components { - public class TimeshiftFilterControl : FilterControl + public class PlaylistsFilterControl : FilterControl { - private readonly Dropdown dropdown; + private readonly Dropdown dropdown; - public TimeshiftFilterControl() + public PlaylistsFilterControl() { - AddInternal(dropdown = new SlimEnumDropdown + AddInternal(dropdown = new SlimEnumDropdown { Anchor = Anchor.BottomRight, Origin = Anchor.TopRight, @@ -37,11 +37,11 @@ namespace osu.Game.Screens.Multi.Lounge.Components switch (dropdown.Current.Value) { - case TimeshiftCategory.Normal: + case PlaylistsCategory.Normal: criteria.Category = "normal"; break; - case TimeshiftCategory.Spotlight: + case PlaylistsCategory.Spotlight: criteria.Category = "spotlight"; break; } @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components return criteria; } - private enum TimeshiftCategory + private enum PlaylistsCategory { Any, Normal, diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/Multi/Match/Components/Footer.cs index d6a7e380bf..fdf1d0dbeb 100644 --- a/osu.Game/Screens/Multi/Match/Components/Footer.cs +++ b/osu.Game/Screens/Multi/Match/Components/Footer.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi.Timeshift; +using osu.Game.Screens.Multi.Playlists; using osuTK; namespace osu.Game.Screens.Multi.Match.Components @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Match.Components InternalChildren = new[] { background = new Box { RelativeSizeAxes = Axes.Both }, - new TimeshiftReadyButton + new PlaylistsReadyButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs b/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs index b69cb9705d..c7fc329a1a 100644 --- a/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs +++ b/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.Multi.Match.Components AddItem(new GameTypeVersus()); AddItem(new GameTypeTagTeam()); AddItem(new GameTypeTeamVersus()); - AddItem(new GameTypeTimeshift()); + AddItem(new GameTypePlaylists()); } private class GameTypePickerItem : DisableableTabItem diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index f07f1c2fb0..65b0091505 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -20,7 +20,7 @@ using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Multi.Play { - public class TimeshiftPlayer : Player + public class PlaylistsPlayer : Player { public Action Exited; @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Multi.Play [Resolved] private IBindable ruleset { get; set; } - public TimeshiftPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null) + public PlaylistsPlayer(PlaylistItem playlistItem, PlayerConfiguration configuration = null) : base(configuration) { PlaylistItem = playlistItem; @@ -50,7 +50,7 @@ namespace osu.Game.Screens.Multi.Play bool failed = false; - // Sanity checks to ensure that TimeshiftPlayer matches the settings for the current PlaylistItem + // Sanity checks to ensure that PlaylistsPlayer matches the settings for the current PlaylistItem if (Beatmap.Value.BeatmapInfo.OnlineBeatmapID != PlaylistItem.Beatmap.Value.OnlineBeatmapID) throw new InvalidOperationException("Current Beatmap does not match PlaylistItem's Beatmap"); @@ -94,7 +94,7 @@ namespace osu.Game.Screens.Multi.Play protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(RoomId.Value != null); - return new TimeshiftResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); + return new PlaylistsResultsScreen(score, RoomId.Value.Value, PlaylistItem, true); } protected override Score CreateScore() diff --git a/osu.Game/Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs b/osu.Game/Screens/Multi/Playlists/CreatePlaylistsRoomButton.cs similarity index 79% rename from osu.Game/Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs rename to osu.Game/Screens/Multi/Playlists/CreatePlaylistsRoomButton.cs index 0424493472..acee063115 100644 --- a/osu.Game/Screens/Multi/Timeshift/CreateTimeshiftRoomButton.cs +++ b/osu.Game/Screens/Multi/Playlists/CreatePlaylistsRoomButton.cs @@ -4,9 +4,9 @@ using osu.Framework.Allocation; using osu.Game.Screens.Multi.Match.Components; -namespace osu.Game.Screens.Multi.Timeshift +namespace osu.Game.Screens.Multi.Playlists { - public class CreateTimeshiftRoomButton : PurpleTriangleButton + public class CreatePlaylistsRoomButton : PurpleTriangleButton { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsLoungeSubScreen.cs similarity index 71% rename from osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs rename to osu.Game/Screens/Multi/Playlists/PlaylistsLoungeSubScreen.cs index 8e426ffbcc..b40c543b68 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftLoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsLoungeSubScreen.cs @@ -6,12 +6,12 @@ using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Match; -namespace osu.Game.Screens.Multi.Timeshift +namespace osu.Game.Screens.Multi.Playlists { - public class TimeshiftLoungeSubScreen : LoungeSubScreen + public class PlaylistsLoungeSubScreen : LoungeSubScreen { - protected override FilterControl CreateFilterControl() => new TimeshiftFilterControl(); + protected override FilterControl CreateFilterControl() => new PlaylistsFilterControl(); - protected override RoomSubScreen CreateRoomSubScreen(Room room) => new TimeshiftRoomSubScreen(room); + protected override RoomSubScreen CreateRoomSubScreen(Room room) => new PlaylistsRoomSubScreen(room); } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsMatchSettingsOverlay.cs similarity index 99% rename from osu.Game/Screens/Multi/Timeshift/TimeshiftMatchSettingsOverlay.cs rename to osu.Game/Screens/Multi/Playlists/PlaylistsMatchSettingsOverlay.cs index 7e1e9894d8..af29e8d34d 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -19,9 +19,9 @@ using osu.Game.Overlays; using osu.Game.Screens.Multi.Match.Components; using osuTK; -namespace osu.Game.Screens.Multi.Timeshift +namespace osu.Game.Screens.Multi.Playlists { - public class TimeshiftMatchSettingsOverlay : MatchSettingsOverlay + public class PlaylistsMatchSettingsOverlay : MatchSettingsOverlay { public Action EditPlaylist; diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsMultiplayer.cs similarity index 62% rename from osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs rename to osu.Game/Screens/Multi/Playlists/PlaylistsMultiplayer.cs index 60b01ee431..fce24da966 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftMultiplayer.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsMultiplayer.cs @@ -9,41 +9,41 @@ using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Match; -namespace osu.Game.Screens.Multi.Timeshift +namespace osu.Game.Screens.Multi.Playlists { - public class TimeshiftMultiplayer : Multiplayer + public class PlaylistsMultiplayer : Multiplayer { protected override void UpdatePollingRate(bool isIdle) { - var timeshiftManager = (TimeshiftRoomManager)RoomManager; + var playlistsManager = (PlaylistsRoomManager)RoomManager; if (!this.IsCurrentScreen()) { - timeshiftManager.TimeBetweenListingPolls.Value = 0; - timeshiftManager.TimeBetweenSelectionPolls.Value = 0; + playlistsManager.TimeBetweenListingPolls.Value = 0; + playlistsManager.TimeBetweenSelectionPolls.Value = 0; } else { switch (CurrentSubScreen) { case LoungeSubScreen _: - timeshiftManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; - timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; + playlistsManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; + playlistsManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; break; case RoomSubScreen _: - timeshiftManager.TimeBetweenListingPolls.Value = 0; - timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000; + playlistsManager.TimeBetweenListingPolls.Value = 0; + playlistsManager.TimeBetweenSelectionPolls.Value = isIdle ? 30000 : 5000; break; default: - timeshiftManager.TimeBetweenListingPolls.Value = 0; - timeshiftManager.TimeBetweenSelectionPolls.Value = 0; + playlistsManager.TimeBetweenListingPolls.Value = 0; + playlistsManager.TimeBetweenSelectionPolls.Value = 0; break; } } - Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {playlistsManager.TimeBetweenListingPolls.Value}, selection: {playlistsManager.TimeBetweenSelectionPolls.Value})"); } protected override Room CreateNewRoom() @@ -53,10 +53,10 @@ namespace osu.Game.Screens.Multi.Timeshift protected override string ScreenTitle => "Playlists"; - protected override RoomManager CreateRoomManager() => new TimeshiftRoomManager(); + protected override RoomManager CreateRoomManager() => new PlaylistsRoomManager(); - protected override LoungeSubScreen CreateLounge() => new TimeshiftLoungeSubScreen(); + protected override LoungeSubScreen CreateLounge() => new PlaylistsLoungeSubScreen(); - protected override OsuButton CreateNewMultiplayerGameButton() => new CreateTimeshiftRoomButton(); + protected override OsuButton CreateNewMultiplayerGameButton() => new CreatePlaylistsRoomButton(); } } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsReadyButton.cs similarity index 88% rename from osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs rename to osu.Game/Screens/Multi/Playlists/PlaylistsReadyButton.cs index c878451eee..f5adf899e3 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftReadyButton.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsReadyButton.cs @@ -8,14 +8,14 @@ using osu.Game.Graphics; using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; -namespace osu.Game.Screens.Multi.Timeshift +namespace osu.Game.Screens.Multi.Playlists { - public class TimeshiftReadyButton : ReadyButton + public class PlaylistsReadyButton : ReadyButton { [Resolved(typeof(Room), nameof(Room.EndDate))] private Bindable endDate { get; set; } - public TimeshiftReadyButton() + public PlaylistsReadyButton() { Text = "Start"; } diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsRoomManager.cs similarity index 89% rename from osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs rename to osu.Game/Screens/Multi/Playlists/PlaylistsRoomManager.cs index d21f844e04..ae57eeddcc 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomManager.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsRoomManager.cs @@ -5,9 +5,9 @@ using System.Collections.Generic; using osu.Framework.Bindables; using osu.Game.Screens.Multi.Components; -namespace osu.Game.Screens.Multi.Timeshift +namespace osu.Game.Screens.Multi.Playlists { - public class TimeshiftRoomManager : RoomManager + public class PlaylistsRoomManager : RoomManager { public readonly Bindable TimeBetweenListingPolls = new Bindable(); public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); diff --git a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs similarity index 97% rename from osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs rename to osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs index 4a66f09a22..b2bcba1724 100644 --- a/osu.Game/Screens/Multi/Timeshift/TimeshiftRoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs @@ -19,9 +19,9 @@ using osu.Game.Screens.Select; using osu.Game.Users; using Footer = osu.Game.Screens.Multi.Match.Components.Footer; -namespace osu.Game.Screens.Multi.Timeshift +namespace osu.Game.Screens.Multi.Playlists { - public class TimeshiftRoomSubScreen : RoomSubScreen + public class PlaylistsRoomSubScreen : RoomSubScreen { public override string Title { get; } @@ -38,7 +38,7 @@ namespace osu.Game.Screens.Multi.Timeshift private OverlinedHeader participantsHeader; - public TimeshiftRoomSubScreen(Room room) + public PlaylistsRoomSubScreen(Room room) { Title = room.RoomID.Value == null ? "New playlist" : room.Name.Value; Activity.Value = new UserActivity.InLobby(room); @@ -126,7 +126,7 @@ namespace osu.Game.Screens.Multi.Timeshift RequestShowResults = item => { Debug.Assert(roomId.Value != null); - multiplayer?.Push(new TimeshiftResultsScreen(null, roomId.Value.Value, item, false)); + multiplayer?.Push(new PlaylistsResultsScreen(null, roomId.Value.Value, item, false)); } } }, @@ -188,7 +188,7 @@ namespace osu.Game.Screens.Multi.Timeshift new Dimension(GridSizeMode.AutoSize), } }, - settingsOverlay = new TimeshiftMatchSettingsOverlay + settingsOverlay = new PlaylistsMatchSettingsOverlay { RelativeSizeAxes = Axes.Both, EditPlaylist = () => this.Push(new MatchSongSelect()), @@ -219,7 +219,7 @@ namespace osu.Game.Screens.Multi.Timeshift }, true); } - private void onStart() => StartPlay(() => new TimeshiftPlayer(SelectedItem.Value) + private void onStart() => StartPlay(() => new PlaylistsPlayer(SelectedItem.Value) { Exited = () => leaderboard.RefreshScores() }); diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index d3f1c19c7c..c757433e2f 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -17,7 +17,7 @@ using osu.Game.Screens.Ranking; namespace osu.Game.Screens.Multi.Ranking { - public class TimeshiftResultsScreen : ResultsScreen + public class PlaylistsResultsScreen : ResultsScreen { private readonly int roomId; private readonly PlaylistItem playlistItem; @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.Ranking [Resolved] private IAPIProvider api { get; set; } - public TimeshiftResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true) + public PlaylistsResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true) : base(score, allowRetry, allowWatchingReplay) { this.roomId = roomId; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs index 70308844b0..87c838f29f 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs @@ -28,31 +28,31 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override void UpdatePollingRate(bool isIdle) { - var timeshiftManager = (RealtimeRoomManager)RoomManager; + var playlistsManager = (RealtimeRoomManager)RoomManager; if (!this.IsCurrentScreen()) { - timeshiftManager.TimeBetweenListingPolls.Value = 0; - timeshiftManager.TimeBetweenSelectionPolls.Value = 0; + playlistsManager.TimeBetweenListingPolls.Value = 0; + playlistsManager.TimeBetweenSelectionPolls.Value = 0; } else { switch (CurrentSubScreen) { case LoungeSubScreen _: - timeshiftManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; - timeshiftManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; + playlistsManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; + playlistsManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; break; // Don't poll inside the match or anywhere else. default: - timeshiftManager.TimeBetweenListingPolls.Value = 0; - timeshiftManager.TimeBetweenSelectionPolls.Value = 0; + playlistsManager.TimeBetweenListingPolls.Value = 0; + playlistsManager.TimeBetweenSelectionPolls.Value = 0; break; } } - Logger.Log($"Polling adjusted (listing: {timeshiftManager.TimeBetweenListingPolls.Value}, selection: {timeshiftManager.TimeBetweenSelectionPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {playlistsManager.TimeBetweenListingPolls.Value}, selection: {playlistsManager.TimeBetweenSelectionPolls.Value})"); } protected override Room CreateNewRoom() diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs index 8eb6a11228..033e4756eb 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs @@ -19,8 +19,8 @@ using osuTK; namespace osu.Game.Screens.Multi.RealtimeMultiplayer { - // Todo: The "room" part of TimeshiftPlayer should be split out into an abstract player class to be inherited instead. - public class RealtimePlayer : TimeshiftPlayer + // Todo: The "room" part of PlaylistsPlayer should be split out into an abstract player class to be inherited instead. + public class RealtimePlayer : PlaylistsPlayer { protected override bool PauseOnFocusLost => false; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs index 3964a87eb6..6bec06cbba 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs +++ b/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs @@ -7,7 +7,7 @@ using osu.Game.Screens.Multi.Ranking; namespace osu.Game.Screens.Multi.RealtimeMultiplayer { - public class RealtimeResultsScreen : TimeshiftResultsScreen + public class RealtimeResultsScreen : PlaylistsResultsScreen { public RealtimeResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem) : base(score, roomId, playlistItem, false, false) From 12e4bbdc5b00de3a1c81da809fad4c69787269a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Dec 2020 13:20:37 +0900 Subject: [PATCH 5681/6909] Reorganise test scenes into playlists specific namespace --- .../TestSceneRealtimeMatchSubScreen.cs | 3 ++- .../TestSceneRealtimeMultiplayer.cs | 3 ++- .../TestSceneRealtimeMultiplayerParticipantsList.cs | 3 ++- .../TestSceneRealtimeReadyButton.cs | 3 ++- .../TestSceneRealtimeRoomManager.cs | 3 ++- .../TestScenePlaylistsFilterControl.cs | 2 +- .../TestScenePlaylistsLoungeSubScreen.cs | 3 ++- .../TestScenePlaylistsMatchSettingsOverlay.cs} | 4 ++-- .../TestScenePlaylistsParticipantsList.cs | 2 +- .../TestScenePlaylistsResultsScreen.cs | 2 +- .../TestScenePlaylistsRoomSubScreen.cs | 2 +- .../TestScenePlaylistsScreen.cs} | 6 +++--- 12 files changed, 21 insertions(+), 15 deletions(-) rename osu.Game.Tests/Visual/{RealtimeMultiplayer => Multiplayer}/TestSceneRealtimeMatchSubScreen.cs (96%) rename osu.Game.Tests/Visual/{RealtimeMultiplayer => Multiplayer}/TestSceneRealtimeMultiplayer.cs (94%) rename osu.Game.Tests/Visual/{RealtimeMultiplayer => Multiplayer}/TestSceneRealtimeMultiplayerParticipantsList.cs (97%) rename osu.Game.Tests/Visual/{RealtimeMultiplayer => Multiplayer}/TestSceneRealtimeReadyButton.cs (98%) rename osu.Game.Tests/Visual/{RealtimeMultiplayer => Multiplayer}/TestSceneRealtimeRoomManager.cs (98%) rename osu.Game.Tests/Visual/{Multiplayer => Playlists}/TestScenePlaylistsFilterControl.cs (93%) rename osu.Game.Tests/Visual/{Multiplayer => Playlists}/TestScenePlaylistsLoungeSubScreen.cs (95%) rename osu.Game.Tests/Visual/{Multiplayer/TestSceneMatchSettingsOverlay.cs => Playlists/TestScenePlaylistsMatchSettingsOverlay.cs} (97%) rename osu.Game.Tests/Visual/{Multiplayer => Playlists}/TestScenePlaylistsParticipantsList.cs (97%) rename osu.Game.Tests/Visual/{Multiplayer => Playlists}/TestScenePlaylistsResultsScreen.cs (99%) rename osu.Game.Tests/Visual/{Multiplayer => Playlists}/TestScenePlaylistsRoomSubScreen.cs (99%) rename osu.Game.Tests/Visual/{Multiplayer/TestSceneMultiScreen.cs => Playlists/TestScenePlaylistsScreen.cs} (82%) diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMatchSubScreen.cs similarity index 96% rename from osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMatchSubScreen.cs index a059bb1cc0..dff375d1bb 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMatchSubScreen.cs @@ -10,9 +10,10 @@ using osu.Game.Rulesets.Osu; using osu.Game.Screens.Multi.RealtimeMultiplayer; using osu.Game.Screens.Multi.RealtimeMultiplayer.Match; using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Visual.RealtimeMultiplayer; using osuTK.Input; -namespace osu.Game.Tests.Visual.RealtimeMultiplayer +namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneRealtimeMatchSubScreen : RealtimeMultiplayerTestScene { diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayer.cs similarity index 94% rename from osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayer.cs index 5cf80df6aa..cc07bb8e79 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayer.cs @@ -3,9 +3,10 @@ using NUnit.Framework; using osu.Game.Screens.Multi.Components; +using osu.Game.Tests.Visual.RealtimeMultiplayer; using osu.Game.Users; -namespace osu.Game.Tests.Visual.RealtimeMultiplayer +namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneRealtimeMultiplayer : RealtimeMultiplayerTestScene { diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs similarity index 97% rename from osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs index 4221821496..7c26918927 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs @@ -9,10 +9,11 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; +using osu.Game.Tests.Visual.RealtimeMultiplayer; using osu.Game.Users; using osuTK; -namespace osu.Game.Tests.Visual.RealtimeMultiplayer +namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneRealtimeMultiplayerParticipantsList : RealtimeMultiplayerTestScene { diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeReadyButton.cs similarity index 98% rename from osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeReadyButton.cs index 825470846c..3c92276629 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeReadyButton.cs @@ -14,11 +14,12 @@ using osu.Game.Online.RealtimeMultiplayer; using osu.Game.Rulesets; using osu.Game.Screens.Multi.RealtimeMultiplayer.Match; using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual.RealtimeMultiplayer; using osu.Game.Users; using osuTK; using osuTK.Input; -namespace osu.Game.Tests.Visual.RealtimeMultiplayer +namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneRealtimeReadyButton : RealtimeMultiplayerTestScene { diff --git a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeRoomManager.cs similarity index 98% rename from osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeRoomManager.cs index 925a83a863..ba90e0840b 100644 --- a/osu.Game.Tests/Visual/RealtimeMultiplayer/TestSceneRealtimeRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeRoomManager.cs @@ -5,8 +5,9 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Online.Multiplayer; +using osu.Game.Tests.Visual.RealtimeMultiplayer; -namespace osu.Game.Tests.Visual.RealtimeMultiplayer +namespace osu.Game.Tests.Visual.Multiplayer { [HeadlessTest] public class TestSceneRealtimeRoomManager : MultiplayerTestScene diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsFilterControl.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs similarity index 93% rename from osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsFilterControl.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs index 427a69552d..66992b27a2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsFilterControl.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Screens.Multi.Lounge.Components; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { public class TestScenePlaylistsFilterControl : OsuTestScene { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs similarity index 95% rename from osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsLoungeSubScreen.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index f8788f0c36..04555857f5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -11,8 +11,9 @@ using osu.Game.Graphics.Containers; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Playlists; +using osu.Game.Tests.Visual.Multiplayer; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { public class TestScenePlaylistsLoungeSubScreen : RoomManagerTestScene { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs similarity index 97% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 90abecd26d..2a110c0386 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -13,9 +13,9 @@ using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.Playlists; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { - public class TestSceneMatchSettingsOverlay : MultiplayerTestScene + public class TestScenePlaylistsMatchSettingsOverlay : MultiplayerTestScene { [Cached(Type = typeof(IRoomManager))] private TestRoomManager roomManager = new TestRoomManager(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs similarity index 97% rename from osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsParticipantsList.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index d71fdc42e3..d5553e4527 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; using osu.Game.Screens.Multi.Components; using osu.Game.Users; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { public class TestScenePlaylistsParticipantsList : MultiplayerTestScene { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs similarity index 99% rename from osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsResultsScreen.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 99fc6597ee..10ae44351b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -24,7 +24,7 @@ using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; using osu.Game.Users; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { public class TestScenePlaylistsResultsScreen : ScreenTestScene { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs similarity index 99% rename from osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSubScreen.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 02cc03eca4..aebff14c80 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -22,7 +22,7 @@ using osu.Game.Tests.Beatmaps; using osu.Game.Users; using osuTK.Input; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { public class TestScenePlaylistsRoomSubScreen : MultiplayerTestScene { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs similarity index 82% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs rename to osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs index 9ac1eb8013..8203ca4845 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs @@ -6,17 +6,17 @@ using osu.Framework.Allocation; using osu.Game.Overlays; using osu.Game.Screens.Multi.Playlists; -namespace osu.Game.Tests.Visual.Multiplayer +namespace osu.Game.Tests.Visual.Playlists { [TestFixture] - public class TestSceneMultiScreen : ScreenTestScene + public class TestScenePlaylistsScreen : ScreenTestScene { protected override bool UseOnlineAPI => true; [Cached] private MusicController musicController { get; set; } = new MusicController(); - public TestSceneMultiScreen() + public TestScenePlaylistsScreen() { var multi = new PlaylistsMultiplayer(); From 5d4b73baa5f655c1c7f77a69228c06e895c40b6d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Dec 2020 13:38:11 +0900 Subject: [PATCH 5682/6909] RealtimeMultiplayer -> Multiplayer --- .../Multiplayer/RoomManagerTestScene.cs | 4 ++-- .../Visual/Multiplayer/TestRoomManager.cs | 2 +- .../TestSceneDrawableRoomPlaylist.cs | 2 +- .../Multiplayer/TestSceneLoungeRoomInfo.cs | 2 +- .../TestSceneLoungeRoomsContainer.cs | 2 +- .../TestSceneMatchBeatmapDetailArea.cs | 2 +- .../Multiplayer/TestSceneMatchHeader.cs | 2 +- ...Multiplayer.cs => TestSceneMultiplayer.cs} | 11 ++++----- ... => TestSceneMultiplayerMatchSubScreen.cs} | 21 ++++++++-------- ...> TestSceneMultiplayerParticipantsList.cs} | 7 +++--- ....cs => TestSceneMultiplayerReadyButton.cs} | 11 ++++----- ....cs => TestSceneMultiplayerRoomManager.cs} | 13 +++++----- .../Visual/Multiplayer/TestSceneRoomStatus.cs | 8 +++---- .../TestScenePlaylistsMatchSettingsOverlay.cs | 4 ++-- .../TestScenePlaylistsParticipantsList.cs | 2 +- .../TestScenePlaylistsResultsScreen.cs | 2 +- .../TestScenePlaylistsRoomSubScreen.cs | 4 ++-- .../IMultiplayerClient.cs | 2 +- .../IMultiplayerLoungeServer.cs | 2 +- .../IMultiplayerRoomServer.cs | 2 +- .../IMultiplayerServer.cs | 2 +- .../InvalidStateChangeException.cs | 2 +- .../InvalidStateException.cs | 2 +- .../MultiplayerClient.cs} | 6 ++--- .../MultiplayerRoom.cs | 2 +- .../MultiplayerRoomSettings.cs | 2 +- .../MultiplayerRoomState.cs | 4 ++-- .../MultiplayerRoomUser.cs | 2 +- .../MultiplayerUserState.cs | 2 +- .../NotHostException.cs | 2 +- .../NotJoinedRoomException.cs | 2 +- .../StatefulMultiplayerClient.cs | 6 ++--- .../{Multiplayer => Rooms}/APICreatedRoom.cs | 2 +- .../{Multiplayer => Rooms}/APILeaderboard.cs | 2 +- .../APIPlaylistBeatmap.cs | 2 +- .../{Multiplayer => Rooms}/APIScoreToken.cs | 2 +- .../CreateRoomRequest.cs | 2 +- .../CreateRoomScoreRequest.cs | 2 +- .../Online/{Multiplayer => Rooms}/GameType.cs | 2 +- .../GameTypes/GameTypeTag.cs | 2 +- .../GameTypes/GameTypeTagTeam.cs | 2 +- .../GameTypes/GameTypeTeamVersus.cs | 2 +- .../GameTypes/GameTypeTimeshift.cs | 2 +- .../GameTypes/GameTypeVersus.cs | 2 +- .../GameTypes/VersusRow.cs | 2 +- .../GetRoomLeaderboardRequest.cs | 2 +- .../{Multiplayer => Rooms}/GetRoomRequest.cs | 2 +- .../{Multiplayer => Rooms}/GetRoomsRequest.cs | 2 +- .../IndexPlaylistScoresRequest.cs | 2 +- .../IndexScoresParams.cs | 2 +- .../IndexedMultiplayerScores.cs | 2 +- .../{Multiplayer => Rooms}/JoinRoomRequest.cs | 2 +- .../MultiplayerScore.cs | 2 +- .../MultiplayerScores.cs | 2 +- .../MultiplayerScoresAround.cs | 2 +- .../{Multiplayer => Rooms}/PartRoomRequest.cs | 2 +- .../PlaylistExtensions.cs | 2 +- .../{Multiplayer => Rooms}/PlaylistItem.cs | 2 +- .../Online/{Multiplayer => Rooms}/Room.cs | 6 ++--- .../RoomAvailability.cs | 2 +- .../{Multiplayer => Rooms}/RoomCategory.cs | 4 ++-- .../{Multiplayer => Rooms}/RoomStatus.cs | 4 ++-- .../RoomStatuses/RoomStatusEnded.cs | 2 +- .../RoomStatuses/RoomStatusOpen.cs | 2 +- .../RoomStatuses/RoomStatusPlaying.cs | 2 +- .../ShowPlaylistUserScoreRequest.cs | 2 +- .../SubmitRoomScoreRequest.cs | 2 +- osu.Game/OsuGameBase.cs | 4 ++-- osu.Game/Screens/Menu/MainMenu.cs | 4 ++-- .../Multi/Components/DrawableGameType.cs | 2 +- .../Components/ListingPollingComponent.cs | 2 +- .../Components/MatchBeatmapDetailArea.cs | 2 +- .../Components/OverlinedPlaylistHeader.cs | 2 +- .../Screens/Multi/Components/ReadyButton.cs | 2 +- .../Screens/Multi/Components/RoomManager.cs | 2 +- .../Multi/Components/RoomPollingComponent.cs | 2 +- .../Multi/Components/RoomStatusInfo.cs | 4 ++-- .../Components/SelectionPollingComponent.cs | 2 +- .../Components/StatusColouredContainer.cs | 2 +- .../Screens/Multi/DrawableRoomPlaylist.cs | 2 +- .../Screens/Multi/DrawableRoomPlaylistItem.cs | 2 +- .../Multi/DrawableRoomPlaylistWithResults.cs | 2 +- osu.Game/Screens/Multi/IRoomManager.cs | 2 +- .../Multi/Lounge/Components/DrawableRoom.cs | 6 ++--- .../Multi/Lounge/Components/RoomsContainer.cs | 2 +- .../Screens/Multi/Lounge/LoungeSubScreen.cs | 2 +- .../Screens/Multi/Match/Components/Footer.cs | 2 +- .../Multi/Match/Components/GameTypePicker.cs | 4 ++-- .../Match/Components/MatchChatDisplay.cs | 2 +- .../Match/Components/MatchLeaderboard.cs | 2 +- .../Components/RoomAvailabilityPicker.cs | 2 +- osu.Game/Screens/Multi/Match/RoomSubScreen.cs | 4 ++-- .../CreateMultiplayerMatchButton.cs} | 6 ++--- .../Match/BeatmapSelectionControl.cs | 6 ++--- .../Match/MultiplayerMatchFooter.cs} | 10 ++++---- .../Match/MultiplayerMatchHeader.cs} | 6 ++--- .../Match/MultiplayerMatchSettingsOverlay.cs} | 6 ++--- .../Match/MultiplayerReadyButton.cs} | 8 +++---- .../Multiplayer.cs} | 16 ++++++------- .../MultiplayerFilterControl.cs} | 4 ++-- .../MultiplayerLoungeSubScreen.cs} | 10 ++++---- .../MultiplayerMatchSongSelect.cs} | 8 +++---- .../MultiplayerMatchSubScreen.cs} | 24 +++++++++---------- .../MultiplayerPlayer.cs} | 12 +++++----- .../MultiplayerResultsScreen.cs} | 8 +++---- .../MultiplayerRoomComposite.cs} | 6 ++--- .../MultiplayerRoomManager.cs} | 16 ++++++------- .../Participants/ParticipantPanel.cs | 6 ++--- .../Participants/ParticipantsList.cs | 4 ++-- .../Participants/ParticipantsListHeader.cs | 4 ++-- .../Participants/StateDisplay.cs | 4 ++-- .../Screens/Multi/MultiplayerComposite.cs | 2 +- .../{Multiplayer.cs => MultiplayerScreen.cs} | 6 ++--- .../Screens/Multi/Play/TimeshiftPlayer.cs | 2 +- .../Playlists/PlaylistsLoungeSubScreen.cs | 2 +- .../PlaylistsMatchSettingsOverlay.cs | 2 +- .../Multi/Playlists/PlaylistsMultiplayer.cs | 4 ++-- .../Multi/Playlists/PlaylistsReadyButton.cs | 2 +- .../Multi/Playlists/PlaylistsRoomSubScreen.cs | 4 ++-- .../Multi/Ranking/TimeshiftResultsScreen.cs | 2 +- osu.Game/Screens/Select/MatchSongSelect.cs | 2 +- .../MultiplayerTestScene.cs} | 16 ++++++------- .../TestMultiplayerClient.cs} | 6 ++--- .../TestMultiplayerRoomContainer.cs} | 16 ++++++------- .../TestMultiplayerRoomManager.cs} | 8 +++---- ...ltiplayerTestScene.cs => RoomTestScene.cs} | 4 ++-- osu.Game/Users/UserActivity.cs | 2 +- 127 files changed, 260 insertions(+), 265 deletions(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneRealtimeMultiplayer.cs => TestSceneMultiplayer.cs} (77%) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneRealtimeMatchSubScreen.cs => TestSceneMultiplayerMatchSubScreen.cs} (73%) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneRealtimeMultiplayerParticipantsList.cs => TestSceneMultiplayerParticipantsList.cs} (94%) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneRealtimeReadyButton.cs => TestSceneMultiplayerReadyButton.cs} (94%) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneRealtimeRoomManager.cs => TestSceneMultiplayerRoomManager.cs} (92%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/IMultiplayerClient.cs (98%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/IMultiplayerLoungeServer.cs (93%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/IMultiplayerRoomServer.cs (98%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/IMultiplayerServer.cs (88%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/InvalidStateChangeException.cs (93%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/InvalidStateException.cs (92%) rename osu.Game/Online/{RealtimeMultiplayer/RealtimeMultiplayerClient.cs => Multiplayer/MultiplayerClient.cs} (97%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/MultiplayerRoom.cs (98%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/MultiplayerRoomSettings.cs (96%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/MultiplayerRoomState.cs (86%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/MultiplayerRoomUser.cs (96%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/MultiplayerUserState.cs (98%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/NotHostException.cs (92%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/NotJoinedRoomException.cs (92%) rename osu.Game/Online/{RealtimeMultiplayer => Multiplayer}/StatefulMultiplayerClient.cs (99%) rename osu.Game/Online/{Multiplayer => Rooms}/APICreatedRoom.cs (88%) rename osu.Game/Online/{Multiplayer => Rooms}/APILeaderboard.cs (92%) rename osu.Game/Online/{Multiplayer => Rooms}/APIPlaylistBeatmap.cs (94%) rename osu.Game/Online/{Multiplayer => Rooms}/APIScoreToken.cs (88%) rename osu.Game/Online/{Multiplayer => Rooms}/CreateRoomRequest.cs (95%) rename osu.Game/Online/{Multiplayer => Rooms}/CreateRoomScoreRequest.cs (96%) rename osu.Game/Online/{Multiplayer => Rooms}/GameType.cs (93%) rename osu.Game/Online/{Multiplayer => Rooms}/GameTypes/GameTypeTag.cs (94%) rename osu.Game/Online/{Multiplayer => Rooms}/GameTypes/GameTypeTagTeam.cs (96%) rename osu.Game/Online/{Multiplayer => Rooms}/GameTypes/GameTypeTeamVersus.cs (95%) rename osu.Game/Online/{Multiplayer => Rooms}/GameTypes/GameTypeTimeshift.cs (93%) rename osu.Game/Online/{Multiplayer => Rooms}/GameTypes/GameTypeVersus.cs (92%) rename osu.Game/Online/{Multiplayer => Rooms}/GameTypes/VersusRow.cs (97%) rename osu.Game/Online/{Multiplayer => Rooms}/GetRoomLeaderboardRequest.cs (92%) rename osu.Game/Online/{Multiplayer => Rooms}/GetRoomRequest.cs (92%) rename osu.Game/Online/{Multiplayer => Rooms}/GetRoomsRequest.cs (96%) rename osu.Game/Online/{Multiplayer => Rooms}/IndexPlaylistScoresRequest.cs (97%) rename osu.Game/Online/{Multiplayer => Rooms}/IndexScoresParams.cs (94%) rename osu.Game/Online/{Multiplayer => Rooms}/IndexedMultiplayerScores.cs (95%) rename osu.Game/Online/{Multiplayer => Rooms}/JoinRoomRequest.cs (94%) rename osu.Game/Online/{Multiplayer => Rooms}/MultiplayerScore.cs (98%) rename osu.Game/Online/{Multiplayer => Rooms}/MultiplayerScores.cs (95%) rename osu.Game/Online/{Multiplayer => Rooms}/MultiplayerScoresAround.cs (95%) rename osu.Game/Online/{Multiplayer => Rooms}/PartRoomRequest.cs (94%) rename osu.Game/Online/{Multiplayer => Rooms}/PlaylistExtensions.cs (93%) rename osu.Game/Online/{Multiplayer => Rooms}/PlaylistItem.cs (98%) rename osu.Game/Online/{Multiplayer => Rooms}/Room.cs (97%) rename osu.Game/Online/{Multiplayer => Rooms}/RoomAvailability.cs (90%) rename osu.Game/Online/{Multiplayer => Rooms}/RoomCategory.cs (80%) rename osu.Game/Online/{Multiplayer => Rooms}/RoomStatus.cs (93%) rename osu.Game/Online/{Multiplayer => Rooms}/RoomStatuses/RoomStatusEnded.cs (88%) rename osu.Game/Online/{Multiplayer => Rooms}/RoomStatuses/RoomStatusOpen.cs (89%) rename osu.Game/Online/{Multiplayer => Rooms}/RoomStatuses/RoomStatusPlaying.cs (88%) rename osu.Game/Online/{Multiplayer => Rooms}/ShowPlaylistUserScoreRequest.cs (95%) rename osu.Game/Online/{Multiplayer => Rooms}/SubmitRoomScoreRequest.cs (97%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/CreateRealtimeMatchButton.cs => Multiplayer/CreateMultiplayerMatchButton.cs} (77%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer => Multiplayer}/Match/BeatmapSelectionControl.cs (91%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/Match/RealtimeMatchFooter.cs => Multiplayer/Match/MultiplayerMatchFooter.cs} (84%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/Match/RealtimeMatchHeader.cs => Multiplayer/Match/MultiplayerMatchHeader.cs} (95%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs => Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs} (99%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/Match/RealtimeReadyButton.cs => Multiplayer/Match/MultiplayerReadyButton.cs} (95%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/RealtimeMultiplayer.cs => Multiplayer/Multiplayer.cs} (86%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/RealtimeFilterControl.cs => Multiplayer/MultiplayerFilterControl.cs} (79%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/RealtimeLoungeSubScreen.cs => Multiplayer/MultiplayerLoungeSubScreen.cs} (79%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/RealtimeMatchSongSelect.cs => Multiplayer/MultiplayerMatchSongSelect.cs} (92%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/RealtimeMatchSubScreen.cs => Multiplayer/MultiplayerMatchSubScreen.cs} (92%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/RealtimePlayer.cs => Multiplayer/MultiplayerPlayer.cs} (93%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/RealtimeResultsScreen.cs => Multiplayer/MultiplayerResultsScreen.cs} (55%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/RealtimeRoomComposite.cs => Multiplayer/MultiplayerRoomComposite.cs} (83%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer/RealtimeRoomManager.cs => Multiplayer/MultiplayerRoomManager.cs} (92%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer => Multiplayer}/Participants/ParticipantPanel.cs (97%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer => Multiplayer}/Participants/ParticipantsList.cs (93%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer => Multiplayer}/Participants/ParticipantsListHeader.cs (86%) rename osu.Game/Screens/Multi/{RealtimeMultiplayer => Multiplayer}/Participants/StateDisplay.cs (97%) rename osu.Game/Screens/Multi/{Multiplayer.cs => MultiplayerScreen.cs} (99%) rename osu.Game/Tests/Visual/{RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs => Multiplayer/MultiplayerTestScene.cs} (67%) rename osu.Game/Tests/Visual/{RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs => Multiplayer/TestMultiplayerClient.cs} (95%) rename osu.Game/Tests/Visual/{RealtimeMultiplayer/TestRealtimeRoomContainer.cs => Multiplayer/TestMultiplayerRoomContainer.cs} (67%) rename osu.Game/Tests/Visual/{RealtimeMultiplayer/TestRealtimeRoomManager.cs => Multiplayer/TestMultiplayerRoomManager.cs} (95%) rename osu.Game/Tests/Visual/{MultiplayerTestScene.cs => RoomTestScene.cs} (90%) diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs index 8b7e0fd9da..a2c496a504 100644 --- a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs @@ -4,14 +4,14 @@ using System; using osu.Framework.Allocation; using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Screens.Multi; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public abstract class RoomManagerTestScene : MultiplayerTestScene + public abstract class RoomManagerTestScene : RoomTestScene { [Cached(Type = typeof(IRoomManager))] protected TestRoomManager RoomManager { get; } = new TestRoomManager(); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs index 9dd4aea4bd..7d9d4a6542 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs @@ -3,7 +3,7 @@ using System; using osu.Framework.Bindables; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi; namespace osu.Game.Tests.Visual.Multiplayer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 55b026eff6..722e1a50ef 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -13,7 +13,7 @@ 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.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs index 9baaa42c83..1359274512 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs @@ -4,7 +4,7 @@ using System; using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Online.Multiplayer.RoomStatuses; +using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Users; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index e33d15cfff..9b6a6de7c2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -6,7 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Multi.Lounge.Components; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 6b1d90e06e..f4e0cc415c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -5,7 +5,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index ec5292e51e..e162009771 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Multi.Match.Components; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs similarity index 77% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayer.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index cc07bb8e79..5c07b9d0ea 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -3,16 +3,15 @@ using NUnit.Framework; using osu.Game.Screens.Multi.Components; -using osu.Game.Tests.Visual.RealtimeMultiplayer; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneRealtimeMultiplayer : RealtimeMultiplayerTestScene + public class TestSceneMultiplayer : MultiplayerTestScene { - public TestSceneRealtimeMultiplayer() + public TestSceneMultiplayer() { - var multi = new TestRealtimeMultiplayer(); + var multi = new TestMultiplayer(); AddStep("show", () => LoadScreen(multi)); AddUntilStep("wait for loaded", () => multi.IsLoaded); @@ -40,9 +39,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); } - private class TestRealtimeMultiplayer : Screens.Multi.RealtimeMultiplayer.RealtimeMultiplayer + private class TestMultiplayer : Screens.Multi.Multiplayer.Multiplayer { - protected override RoomManager CreateRoomManager() => new TestRealtimeRoomManager(); + protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs similarity index 73% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMatchSubScreen.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index dff375d1bb..6dc26dae57 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -5,21 +5,20 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Screens; using osu.Framework.Testing; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Multi.RealtimeMultiplayer; -using osu.Game.Screens.Multi.RealtimeMultiplayer.Match; +using osu.Game.Screens.Multi.Multiplayer; +using osu.Game.Screens.Multi.Multiplayer.Match; using osu.Game.Tests.Beatmaps; -using osu.Game.Tests.Visual.RealtimeMultiplayer; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneRealtimeMatchSubScreen : RealtimeMultiplayerTestScene + public class TestSceneMultiplayerMatchSubScreen : MultiplayerTestScene { - private RealtimeMatchSubScreen screen; + private MultiplayerMatchSubScreen screen; - public TestSceneRealtimeMatchSubScreen() + public TestSceneMultiplayerMatchSubScreen() : base(false) { } @@ -33,14 +32,14 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public void SetupSteps() { - AddStep("load match", () => LoadScreen(screen = new RealtimeMatchSubScreen(Room))); + AddStep("load match", () => LoadScreen(screen = new MultiplayerMatchSubScreen(Room))); AddUntilStep("wait for load", () => screen.IsCurrentScreen()); } [Test] public void TestSettingValidity() { - AddAssert("create button not enabled", () => !this.ChildrenOfType().Single().Enabled.Value); + AddAssert("create button not enabled", () => !this.ChildrenOfType().Single().Enabled.Value); AddStep("set playlist", () => { @@ -51,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); }); - AddAssert("create button enabled", () => this.ChildrenOfType().Single().Enabled.Value); + AddAssert("create button enabled", () => this.ChildrenOfType().Single().Enabled.Value); } [Test] @@ -68,7 +67,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("click create button", () => { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs similarity index 94% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 7c26918927..ee2fa4ef5a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -7,15 +7,14 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; -using osu.Game.Online.RealtimeMultiplayer; -using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; -using osu.Game.Tests.Visual.RealtimeMultiplayer; +using osu.Game.Online.Multiplayer; +using osu.Game.Screens.Multi.Multiplayer.Participants; using osu.Game.Users; using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneRealtimeMultiplayerParticipantsList : RealtimeMultiplayerTestScene + public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene { [SetUp] public new void Setup() => Schedule(() => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs similarity index 94% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeReadyButton.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 3c92276629..c8ebdb1b76 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -10,20 +10,19 @@ using osu.Framework.Platform; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Screens.Multi.RealtimeMultiplayer.Match; +using osu.Game.Screens.Multi.Multiplayer.Match; using osu.Game.Tests.Resources; -using osu.Game.Tests.Visual.RealtimeMultiplayer; using osu.Game.Users; using osuTK; using osuTK.Input; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneRealtimeReadyButton : RealtimeMultiplayerTestScene + public class TestSceneMultiplayerReadyButton : MultiplayerTestScene { - private RealtimeReadyButton button; + private MultiplayerReadyButton button; private BeatmapManager beatmaps; private RulesetStore rulesets; @@ -43,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); - Child = button = new RealtimeReadyButton + Child = button = new MultiplayerReadyButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs similarity index 92% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeRoomManager.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index ba90e0840b..292c4846ab 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRealtimeRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -4,16 +4,15 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; -using osu.Game.Online.Multiplayer; -using osu.Game.Tests.Visual.RealtimeMultiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Tests.Visual.Multiplayer { [HeadlessTest] - public class TestSceneRealtimeRoomManager : MultiplayerTestScene + public class TestSceneMultiplayerRoomManager : MultiplayerTestScene { - private TestRealtimeRoomContainer roomContainer; - private TestRealtimeRoomManager roomManager => roomContainer.RoomManager; + private TestMultiplayerRoomContainer roomContainer; + private TestMultiplayerRoomManager roomManager => roomContainer.RoomManager; [Test] public void TestPollsInitially() @@ -137,9 +136,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null); } - private TestRealtimeRoomManager createRoomManager() + private TestMultiplayerRoomManager createRoomManager() { - Child = roomContainer = new TestRealtimeRoomContainer + Child = roomContainer = new TestMultiplayerRoomContainer { RoomManager = { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs index a6dd1437f7..788255501b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs @@ -4,8 +4,8 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.RoomStatuses; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.Multi.Lounge.Components; namespace osu.Game.Tests.Visual.Multiplayer @@ -40,9 +40,9 @@ namespace osu.Game.Tests.Visual.Multiplayer }) { MatchingFilter = true }, new DrawableRoom(new Room { - Name = { Value = "Open (realtime)" }, + Name = { Value = "Open" }, Status = { Value = new RoomStatusOpen() }, - Category = { Value = RoomCategory.Realtime } + Category = { Value = RoomCategory.Multiplayer } }) { MatchingFilter = true }, } }; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 2a110c0386..4e75619f13 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -9,13 +9,13 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.Playlists; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsMatchSettingsOverlay : MultiplayerTestScene + public class TestScenePlaylistsMatchSettingsOverlay : RoomTestScene { [Cached(Type = typeof(IRoomManager))] private TestRoomManager roomManager = new TestRoomManager(); diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index d5553e4527..5a0f196df9 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -8,7 +8,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsParticipantsList : MultiplayerTestScene + public class TestScenePlaylistsParticipantsList : RoomTestScene { [SetUp] public new void Setup() => Schedule(() => diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 10ae44351b..b42f2e1d2d 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -15,7 +15,7 @@ using osu.Game.Graphics.Containers; 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.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index aebff14c80..96ff93a145 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -12,7 +12,7 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.Multi; @@ -24,7 +24,7 @@ using osuTK.Input; namespace osu.Game.Tests.Visual.Playlists { - public class TestScenePlaylistsRoomSubScreen : MultiplayerTestScene + public class TestScenePlaylistsRoomSubScreen : RoomTestScene { protected override bool UseOnlineAPI => true; diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs similarity index 98% rename from osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs rename to osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 9af0047137..b97fcc9ae7 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// /// An interface defining a multiplayer client instance. diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerLoungeServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs similarity index 93% rename from osu.Game/Online/RealtimeMultiplayer/IMultiplayerLoungeServer.cs rename to osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs index eecb61bcb0..4640640c5f 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerLoungeServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerLoungeServer.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// /// Interface for an out-of-room multiplayer server. diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs similarity index 98% rename from osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs rename to osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 12dfe481c4..481e3fb1de 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -3,7 +3,7 @@ using System.Threading.Tasks; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// /// Interface for an in-room multiplayer server. diff --git a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerServer.cs similarity index 88% rename from osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs rename to osu.Game/Online/Multiplayer/IMultiplayerServer.cs index 1d093af743..d3a070af6d 100644 --- a/osu.Game/Online/RealtimeMultiplayer/IMultiplayerServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerServer.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. -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// /// An interface defining the multiplayer server instance. diff --git a/osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs similarity index 93% rename from osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs rename to osu.Game/Online/Multiplayer/InvalidStateChangeException.cs index d9a276fc19..69b6d4bc13 100644 --- a/osu.Game/Online/RealtimeMultiplayer/InvalidStateChangeException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateChangeException.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class InvalidStateChangeException : HubException diff --git a/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs b/osu.Game/Online/Multiplayer/InvalidStateException.cs similarity index 92% rename from osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs rename to osu.Game/Online/Multiplayer/InvalidStateException.cs index 7791bfc69f..77a3533dd3 100644 --- a/osu.Game/Online/RealtimeMultiplayer/InvalidStateException.cs +++ b/osu.Game/Online/Multiplayer/InvalidStateException.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class InvalidStateException : HubException diff --git a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs similarity index 97% rename from osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs rename to osu.Game/Online/Multiplayer/MultiplayerClient.cs index cb5c21a8c9..24ea6abc4a 100644 --- a/osu.Game/Online/RealtimeMultiplayer/RealtimeMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -15,9 +15,9 @@ using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Online.API; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { - public class RealtimeMultiplayerClient : StatefulMultiplayerClient + public class MultiplayerClient : StatefulMultiplayerClient { public override IBindable IsConnected => isConnected; @@ -31,7 +31,7 @@ namespace osu.Game.Online.RealtimeMultiplayer private readonly string endpoint; - public RealtimeMultiplayerClient(EndpointConfiguration endpoints) + public MultiplayerClient(EndpointConfiguration endpoints) { endpoint = endpoints.MultiplayerEndpointUrl; } diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs similarity index 98% rename from osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs rename to osu.Game/Online/Multiplayer/MultiplayerRoom.cs index e009a34707..2134e50d72 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -9,7 +9,7 @@ using System.Threading; using Newtonsoft.Json; using osu.Framework.Allocation; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// /// A multiplayer room. diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs similarity index 96% rename from osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs rename to osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 60e0d1292e..857b38ea60 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -9,7 +9,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Game.Online.API; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class MultiplayerRoomSettings : IEquatable diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomState.cs similarity index 86% rename from osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs rename to osu.Game/Online/Multiplayer/MultiplayerRoomState.cs index 69c04b09a8..48f25d7ca2 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomState.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomState.cs @@ -3,10 +3,10 @@ #nullable enable -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { /// - /// The current overall state of a realtime multiplayer room. + /// The current overall state of a multiplayer room. /// public enum MultiplayerRoomState { diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs similarity index 96% rename from osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs rename to osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index caf1a70197..99624dc3e7 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -7,7 +7,7 @@ using System; using Newtonsoft.Json; using osu.Game.Users; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class MultiplayerRoomUser : IEquatable diff --git a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs b/osu.Game/Online/Multiplayer/MultiplayerUserState.cs similarity index 98% rename from osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs rename to osu.Game/Online/Multiplayer/MultiplayerUserState.cs index ed9acd146e..e54c71cd85 100644 --- a/osu.Game/Online/RealtimeMultiplayer/MultiplayerUserState.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerUserState.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. -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { public enum MultiplayerUserState { diff --git a/osu.Game/Online/RealtimeMultiplayer/NotHostException.cs b/osu.Game/Online/Multiplayer/NotHostException.cs similarity index 92% rename from osu.Game/Online/RealtimeMultiplayer/NotHostException.cs rename to osu.Game/Online/Multiplayer/NotHostException.cs index 56095043f0..051cde45a0 100644 --- a/osu.Game/Online/RealtimeMultiplayer/NotHostException.cs +++ b/osu.Game/Online/Multiplayer/NotHostException.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class NotHostException : HubException diff --git a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs similarity index 92% rename from osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs rename to osu.Game/Online/Multiplayer/NotJoinedRoomException.cs index 7a6e089d0b..0e9902f002 100644 --- a/osu.Game/Online/RealtimeMultiplayer/NotJoinedRoomException.cs +++ b/osu.Game/Online/Multiplayer/NotJoinedRoomException.cs @@ -5,7 +5,7 @@ using System; using System.Runtime.Serialization; using Microsoft.AspNetCore.SignalR; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { [Serializable] public class NotJoinedRoomException : HubException diff --git a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs similarity index 99% rename from osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs rename to osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 9149bdcba3..e422e982ae 100644 --- a/osu.Game/Online/RealtimeMultiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -18,13 +18,13 @@ using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.RoomStatuses; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Rulesets; using osu.Game.Users; using osu.Game.Utils; -namespace osu.Game.Online.RealtimeMultiplayer +namespace osu.Game.Online.Multiplayer { public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer { diff --git a/osu.Game/Online/Multiplayer/APICreatedRoom.cs b/osu.Game/Online/Rooms/APICreatedRoom.cs similarity index 88% rename from osu.Game/Online/Multiplayer/APICreatedRoom.cs rename to osu.Game/Online/Rooms/APICreatedRoom.cs index 2a3bb39647..d1062b2306 100644 --- a/osu.Game/Online/Multiplayer/APICreatedRoom.cs +++ b/osu.Game/Online/Rooms/APICreatedRoom.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class APICreatedRoom : Room { diff --git a/osu.Game/Online/Multiplayer/APILeaderboard.cs b/osu.Game/Online/Rooms/APILeaderboard.cs similarity index 92% rename from osu.Game/Online/Multiplayer/APILeaderboard.cs rename to osu.Game/Online/Rooms/APILeaderboard.cs index 65863d6e0e..c487123906 100644 --- a/osu.Game/Online/Multiplayer/APILeaderboard.cs +++ b/osu.Game/Online/Rooms/APILeaderboard.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Online.API.Requests.Responses; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class APILeaderboard { diff --git a/osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs similarity index 94% rename from osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs rename to osu.Game/Online/Rooms/APIPlaylistBeatmap.cs index 98972ef36d..973dccd528 100644 --- a/osu.Game/Online/Multiplayer/APIPlaylistBeatmap.cs +++ b/osu.Game/Online/Rooms/APIPlaylistBeatmap.cs @@ -6,7 +6,7 @@ using osu.Game.Beatmaps; using osu.Game.Online.API.Requests.Responses; using osu.Game.Rulesets; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class APIPlaylistBeatmap : APIBeatmap { diff --git a/osu.Game/Online/Multiplayer/APIScoreToken.cs b/osu.Game/Online/Rooms/APIScoreToken.cs similarity index 88% rename from osu.Game/Online/Multiplayer/APIScoreToken.cs rename to osu.Game/Online/Rooms/APIScoreToken.cs index 1f0063d94e..f652c1720d 100644 --- a/osu.Game/Online/Multiplayer/APIScoreToken.cs +++ b/osu.Game/Online/Rooms/APIScoreToken.cs @@ -3,7 +3,7 @@ using Newtonsoft.Json; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class APIScoreToken { diff --git a/osu.Game/Online/Multiplayer/CreateRoomRequest.cs b/osu.Game/Online/Rooms/CreateRoomRequest.cs similarity index 95% rename from osu.Game/Online/Multiplayer/CreateRoomRequest.cs rename to osu.Game/Online/Rooms/CreateRoomRequest.cs index 5be99e9442..f058eb9ba8 100644 --- a/osu.Game/Online/Multiplayer/CreateRoomRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomRequest.cs @@ -6,7 +6,7 @@ using Newtonsoft.Json; using osu.Framework.IO.Network; using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class CreateRoomRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs similarity index 96% rename from osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs rename to osu.Game/Online/Rooms/CreateRoomScoreRequest.cs index 2d99b12519..afd0dadc7e 100644 --- a/osu.Game/Online/Multiplayer/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs @@ -5,7 +5,7 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class CreateRoomScoreRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/GameType.cs b/osu.Game/Online/Rooms/GameType.cs similarity index 93% rename from osu.Game/Online/Multiplayer/GameType.cs rename to osu.Game/Online/Rooms/GameType.cs index 10381d93bb..caa352d812 100644 --- a/osu.Game/Online/Multiplayer/GameType.cs +++ b/osu.Game/Online/Rooms/GameType.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public abstract class GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs b/osu.Game/Online/Rooms/GameTypes/GameTypeTag.cs similarity index 94% rename from osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypeTag.cs index 5ba5f1a415..e468612738 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTag.cs +++ b/osu.Game/Online/Rooms/GameTypes/GameTypeTag.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osuTK; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class GameTypeTag : GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs b/osu.Game/Online/Rooms/GameTypes/GameTypeTagTeam.cs similarity index 96% rename from osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypeTagTeam.cs index ef0a00a9f0..b82f203fac 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTagTeam.cs +++ b/osu.Game/Online/Rooms/GameTypes/GameTypeTagTeam.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osuTK; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class GameTypeTagTeam : GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs b/osu.Game/Online/Rooms/GameTypes/GameTypeTeamVersus.cs similarity index 95% rename from osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypeTeamVersus.cs index c25bce1c71..5ad4033dc9 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTeamVersus.cs +++ b/osu.Game/Online/Rooms/GameTypes/GameTypeTeamVersus.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osuTK; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class GameTypeTeamVersus : GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs b/osu.Game/Online/Rooms/GameTypes/GameTypeTimeshift.cs similarity index 93% rename from osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypeTimeshift.cs index 5840ccb803..3425c6c5cd 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeTimeshift.cs +++ b/osu.Game/Online/Rooms/GameTypes/GameTypeTimeshift.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osuTK; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class GameTypePlaylists : GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs b/osu.Game/Online/Rooms/GameTypes/GameTypeVersus.cs similarity index 92% rename from osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypeVersus.cs index 4640c7b361..3783cc67b0 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/GameTypeVersus.cs +++ b/osu.Game/Online/Rooms/GameTypes/GameTypeVersus.cs @@ -4,7 +4,7 @@ using osu.Framework.Graphics; using osu.Game.Graphics; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class GameTypeVersus : GameType { diff --git a/osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs b/osu.Game/Online/Rooms/GameTypes/VersusRow.cs similarity index 97% rename from osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs rename to osu.Game/Online/Rooms/GameTypes/VersusRow.cs index b6e8e4458f..0bd09a23ac 100644 --- a/osu.Game/Online/Multiplayer/GameTypes/VersusRow.cs +++ b/osu.Game/Online/Rooms/GameTypes/VersusRow.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics.Shapes; using osuTK; using osuTK.Graphics; -namespace osu.Game.Online.Multiplayer.GameTypes +namespace osu.Game.Online.Rooms.GameTypes { public class VersusRow : FillFlowContainer { diff --git a/osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs similarity index 92% rename from osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs rename to osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs index 37c21457bc..15f1221a00 100644 --- a/osu.Game/Online/Multiplayer/GetRoomLeaderboardRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs @@ -3,7 +3,7 @@ using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class GetRoomLeaderboardRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/GetRoomRequest.cs b/osu.Game/Online/Rooms/GetRoomRequest.cs similarity index 92% rename from osu.Game/Online/Multiplayer/GetRoomRequest.cs rename to osu.Game/Online/Rooms/GetRoomRequest.cs index 449c2c8e31..ce117075c7 100644 --- a/osu.Game/Online/Multiplayer/GetRoomRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomRequest.cs @@ -3,7 +3,7 @@ using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class GetRoomRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs similarity index 96% rename from osu.Game/Online/Multiplayer/GetRoomsRequest.cs rename to osu.Game/Online/Rooms/GetRoomsRequest.cs index a0609f77dd..5084b8627f 100644 --- a/osu.Game/Online/Multiplayer/GetRoomsRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs @@ -7,7 +7,7 @@ using osu.Framework.IO.Network; using osu.Game.Online.API; using osu.Game.Screens.Multi.Lounge.Components; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class GetRoomsRequest : APIRequest> { diff --git a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs similarity index 97% rename from osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs rename to osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs index 684d0aecd8..43f80a2dc4 100644 --- a/osu.Game/Online/Multiplayer/IndexPlaylistScoresRequest.cs +++ b/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs @@ -8,7 +8,7 @@ using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { /// /// Returns a list of scores for the specified playlist item. diff --git a/osu.Game/Online/Multiplayer/IndexScoresParams.cs b/osu.Game/Online/Rooms/IndexScoresParams.cs similarity index 94% rename from osu.Game/Online/Multiplayer/IndexScoresParams.cs rename to osu.Game/Online/Rooms/IndexScoresParams.cs index a511e9a780..3df8c8e753 100644 --- a/osu.Game/Online/Multiplayer/IndexScoresParams.cs +++ b/osu.Game/Online/Rooms/IndexScoresParams.cs @@ -6,7 +6,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { /// /// A collection of parameters which should be passed to the index endpoint to fetch the next page. diff --git a/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs b/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs similarity index 95% rename from osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs rename to osu.Game/Online/Rooms/IndexedMultiplayerScores.cs index e237b7e3fb..2008d1aa52 100644 --- a/osu.Game/Online/Multiplayer/IndexedMultiplayerScores.cs +++ b/osu.Game/Online/Rooms/IndexedMultiplayerScores.cs @@ -4,7 +4,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { /// /// A object returned via a . diff --git a/osu.Game/Online/Multiplayer/JoinRoomRequest.cs b/osu.Game/Online/Rooms/JoinRoomRequest.cs similarity index 94% rename from osu.Game/Online/Multiplayer/JoinRoomRequest.cs rename to osu.Game/Online/Rooms/JoinRoomRequest.cs index 74375af856..faa20a3e6c 100644 --- a/osu.Game/Online/Multiplayer/JoinRoomRequest.cs +++ b/osu.Game/Online/Rooms/JoinRoomRequest.cs @@ -5,7 +5,7 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class JoinRoomRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs similarity index 98% rename from osu.Game/Online/Multiplayer/MultiplayerScore.cs rename to osu.Game/Online/Rooms/MultiplayerScore.cs index 8191003aad..677a3d3026 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -13,7 +13,7 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Users; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class MultiplayerScore { diff --git a/osu.Game/Online/Multiplayer/MultiplayerScores.cs b/osu.Game/Online/Rooms/MultiplayerScores.cs similarity index 95% rename from osu.Game/Online/Multiplayer/MultiplayerScores.cs rename to osu.Game/Online/Rooms/MultiplayerScores.cs index 7b9dcff828..3f970b2f8e 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScores.cs +++ b/osu.Game/Online/Rooms/MultiplayerScores.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using Newtonsoft.Json; using osu.Game.Online.API.Requests; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { /// /// An object which contains scores and related data for fetching next pages. diff --git a/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs b/osu.Game/Online/Rooms/MultiplayerScoresAround.cs similarity index 95% rename from osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs rename to osu.Game/Online/Rooms/MultiplayerScoresAround.cs index 2ac62d0300..a99439312a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerScoresAround.cs +++ b/osu.Game/Online/Rooms/MultiplayerScoresAround.cs @@ -4,7 +4,7 @@ using JetBrains.Annotations; using Newtonsoft.Json; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { /// /// An object which stores scores higher and lower than the user's score. diff --git a/osu.Game/Online/Multiplayer/PartRoomRequest.cs b/osu.Game/Online/Rooms/PartRoomRequest.cs similarity index 94% rename from osu.Game/Online/Multiplayer/PartRoomRequest.cs rename to osu.Game/Online/Rooms/PartRoomRequest.cs index 54bb005d96..2f036abc8c 100644 --- a/osu.Game/Online/Multiplayer/PartRoomRequest.cs +++ b/osu.Game/Online/Rooms/PartRoomRequest.cs @@ -5,7 +5,7 @@ using System.Net.Http; using osu.Framework.IO.Network; using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class PartRoomRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/PlaylistExtensions.cs b/osu.Game/Online/Rooms/PlaylistExtensions.cs similarity index 93% rename from osu.Game/Online/Multiplayer/PlaylistExtensions.cs rename to osu.Game/Online/Rooms/PlaylistExtensions.cs index fe3d96e295..992011da3c 100644 --- a/osu.Game/Online/Multiplayer/PlaylistExtensions.cs +++ b/osu.Game/Online/Rooms/PlaylistExtensions.cs @@ -6,7 +6,7 @@ using Humanizer; using Humanizer.Localisation; using osu.Framework.Bindables; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public static class PlaylistExtensions { diff --git a/osu.Game/Online/Multiplayer/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs similarity index 98% rename from osu.Game/Online/Multiplayer/PlaylistItem.cs rename to osu.Game/Online/Rooms/PlaylistItem.cs index 4c4c071fc9..ada2140ca6 100644 --- a/osu.Game/Online/Multiplayer/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -10,7 +10,7 @@ using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class PlaylistItem : IEquatable { diff --git a/osu.Game/Online/Multiplayer/Room.cs b/osu.Game/Online/Rooms/Room.cs similarity index 97% rename from osu.Game/Online/Multiplayer/Room.cs rename to osu.Game/Online/Rooms/Room.cs index d13d7dc774..67f874cdc4 100644 --- a/osu.Game/Online/Multiplayer/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -7,11 +7,11 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.IO.Serialization.Converters; -using osu.Game.Online.Multiplayer.GameTypes; -using osu.Game.Online.Multiplayer.RoomStatuses; +using osu.Game.Online.Rooms.GameTypes; +using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Users; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class Room { diff --git a/osu.Game/Online/Multiplayer/RoomAvailability.cs b/osu.Game/Online/Rooms/RoomAvailability.cs similarity index 90% rename from osu.Game/Online/Multiplayer/RoomAvailability.cs rename to osu.Game/Online/Rooms/RoomAvailability.cs index 08fa853562..3aea0e5948 100644 --- a/osu.Game/Online/Multiplayer/RoomAvailability.cs +++ b/osu.Game/Online/Rooms/RoomAvailability.cs @@ -3,7 +3,7 @@ using System.ComponentModel; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public enum RoomAvailability { diff --git a/osu.Game/Online/Multiplayer/RoomCategory.cs b/osu.Game/Online/Rooms/RoomCategory.cs similarity index 80% rename from osu.Game/Online/Multiplayer/RoomCategory.cs rename to osu.Game/Online/Rooms/RoomCategory.cs index d6786a72fe..f485e65ba9 100644 --- a/osu.Game/Online/Multiplayer/RoomCategory.cs +++ b/osu.Game/Online/Rooms/RoomCategory.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. -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public enum RoomCategory { Normal, Spotlight, - Realtime, + Multiplayer, } } diff --git a/osu.Game/Online/Multiplayer/RoomStatus.cs b/osu.Game/Online/Rooms/RoomStatus.cs similarity index 93% rename from osu.Game/Online/Multiplayer/RoomStatus.cs rename to osu.Game/Online/Rooms/RoomStatus.cs index 3ff2770ab4..87c5aa3fda 100644 --- a/osu.Game/Online/Multiplayer/RoomStatus.cs +++ b/osu.Game/Online/Rooms/RoomStatus.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 osuTK.Graphics; using osu.Game.Graphics; +using osuTK.Graphics; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public abstract class RoomStatus { diff --git a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs similarity index 88% rename from osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs rename to osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs index 4177d28a99..c852f86f6b 100644 --- a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusEnded.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusEnded.cs @@ -4,7 +4,7 @@ using osu.Game.Graphics; using osuTK.Graphics; -namespace osu.Game.Online.Multiplayer.RoomStatuses +namespace osu.Game.Online.Rooms.RoomStatuses { public class RoomStatusEnded : RoomStatus { diff --git a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs similarity index 89% rename from osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs rename to osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs index 45a1cb1909..4f7f0d6f5d 100644 --- a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusOpen.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusOpen.cs @@ -4,7 +4,7 @@ using osu.Game.Graphics; using osuTK.Graphics; -namespace osu.Game.Online.Multiplayer.RoomStatuses +namespace osu.Game.Online.Rooms.RoomStatuses { public class RoomStatusOpen : RoomStatus { diff --git a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusPlaying.cs b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs similarity index 88% rename from osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusPlaying.cs rename to osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs index b2cb5c4510..f04f1b23af 100644 --- a/osu.Game/Online/Multiplayer/RoomStatuses/RoomStatusPlaying.cs +++ b/osu.Game/Online/Rooms/RoomStatuses/RoomStatusPlaying.cs @@ -4,7 +4,7 @@ using osu.Game.Graphics; using osuTK.Graphics; -namespace osu.Game.Online.Multiplayer.RoomStatuses +namespace osu.Game.Online.Rooms.RoomStatuses { public class RoomStatusPlaying : RoomStatus { diff --git a/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs similarity index 95% rename from osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs rename to osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs index 936b8bbe89..3f728a5417 100644 --- a/osu.Game/Online/Multiplayer/ShowPlaylistUserScoreRequest.cs +++ b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs @@ -3,7 +3,7 @@ using osu.Game.Online.API; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class ShowPlaylistUserScoreRequest : APIRequest { diff --git a/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs similarity index 97% rename from osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs rename to osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index d31aef2ea5..5a78b9fabd 100644 --- a/osu.Game/Online/Multiplayer/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -7,7 +7,7 @@ using osu.Framework.IO.Network; using osu.Game.Online.API; using osu.Game.Scoring; -namespace osu.Game.Online.Multiplayer +namespace osu.Game.Online.Rooms { public class SubmitRoomScoreRequest : APIRequest { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index bdc9e5eb7b..0b5abc4e31 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -31,7 +31,7 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Overlays; using osu.Game.Resources; @@ -219,7 +219,7 @@ namespace osu.Game dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints)); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient(endpoints)); - dependencies.CacheAs(multiplayerClient = new RealtimeMultiplayerClient(endpoints)); + dependencies.CacheAs(multiplayerClient = new MultiplayerClient(endpoints)); var defaultBeatmap = new DummyWorkingBeatmap(Audio, Textures); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 5650b22f25..15ac2237d3 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -17,7 +17,7 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; -using osu.Game.Screens.Multi.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Multiplayer; using osu.Game.Screens.Multi.Playlists; using osu.Game.Screens.Select; @@ -105,7 +105,7 @@ namespace osu.Game.Screens.Menu this.Push(new Editor()); }, OnSolo = onSolo, - OnMultiplayer = () => this.Push(new RealtimeMultiplayer()), + OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new PlaylistsMultiplayer()), OnExit = confirmAndExit, } diff --git a/osu.Game/Screens/Multi/Components/DrawableGameType.cs b/osu.Game/Screens/Multi/Components/DrawableGameType.cs index 28240f0796..38af6d065e 100644 --- a/osu.Game/Screens/Multi/Components/DrawableGameType.cs +++ b/osu.Game/Screens/Multi/Components/DrawableGameType.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi.Components { diff --git a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs index dff6c50bf2..edce5400d1 100644 --- a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs @@ -4,7 +4,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Lounge.Components; namespace osu.Game.Screens.Multi.Components diff --git a/osu.Game/Screens/Multi/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/Multi/Components/MatchBeatmapDetailArea.cs index 2c5fd2d397..6997840c92 100644 --- a/osu.Game/Screens/Multi/Components/MatchBeatmapDetailArea.cs +++ b/osu.Game/Screens/Multi/Components/MatchBeatmapDetailArea.cs @@ -8,7 +8,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Select; using osuTK; diff --git a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs index 5552c1cb72..ebebe8b660 100644 --- a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs +++ b/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.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.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi.Components { diff --git a/osu.Game/Screens/Multi/Components/ReadyButton.cs b/osu.Game/Screens/Multi/Components/ReadyButton.cs index 0bb4ed8617..68df30965d 100644 --- a/osu.Game/Screens/Multi/Components/ReadyButton.cs +++ b/osu.Game/Screens/Multi/Components/ReadyButton.cs @@ -9,7 +9,7 @@ using osu.Framework.Bindables; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi.Components { diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/Multi/Components/RoomManager.cs index f78d0d979e..7e0c8c4ec5 100644 --- a/osu.Game/Screens/Multi/Components/RoomManager.cs +++ b/osu.Game/Screens/Multi/Components/RoomManager.cs @@ -12,7 +12,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; namespace osu.Game.Screens.Multi.Components diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs index fbaf9dd930..1d10277d1c 100644 --- a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs @@ -6,7 +6,7 @@ using System.Collections.Generic; using osu.Framework.Allocation; using osu.Game.Online; using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi.Components { diff --git a/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs b/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs index b5676692a4..89021691f3 100644 --- a/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs +++ b/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs @@ -8,8 +8,8 @@ using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.RoomStatuses; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; namespace osu.Game.Screens.Multi.Components { diff --git a/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs b/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs index 37a190b5e0..3050765931 100644 --- a/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi.Components { diff --git a/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs b/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs index a115f06e7b..68c14eeb15 100644 --- a/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs +++ b/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs @@ -6,7 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi.Components { diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs index 89c335183b..956d38a90e 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs @@ -7,7 +7,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osuTK; namespace osu.Game.Screens.Multi diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs index bda00b65b5..a2aeae154a 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs @@ -21,7 +21,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.Chat; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays.BeatmapListing.Panels; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs b/osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs index 439aaaa275..fa241a3c42 100644 --- a/osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs +++ b/osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs @@ -11,7 +11,7 @@ using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi { diff --git a/osu.Game/Screens/Multi/IRoomManager.cs b/osu.Game/Screens/Multi/IRoomManager.cs index 630e3af91c..eee2a223a1 100644 --- a/osu.Game/Screens/Multi/IRoomManager.cs +++ b/osu.Game/Screens/Multi/IRoomManager.cs @@ -4,7 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi { diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs index 56116b219a..6e4d8b46ed 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs @@ -17,12 +17,12 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; using osuTK; using osuTK.Graphics; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.UserInterface; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi.Lounge.Components { @@ -42,7 +42,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components private CachedModelDependencyContainer dependencies; [Resolved(canBeNull: true)] - private Multiplayer multiplayer { get; set; } + private MultiplayerScreen multiplayer { get; set; } [Resolved] private BeatmapManager beatmaps { get; set; } @@ -228,7 +228,7 @@ namespace osu.Game.Screens.Multi.Lounge.Components private class RoomName : OsuSpriteText { - [Resolved(typeof(Room), nameof(Online.Multiplayer.Room.Name))] + [Resolved(typeof(Room), nameof(Online.Rooms.Room.Name))] private Bindable name { get; set; } [BackgroundDependencyLoader] diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs index c7c37cbc0d..fbd5f44a30 100644 --- a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs @@ -15,9 +15,9 @@ 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; using osu.Game.Graphics.Cursor; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi.Lounge.Components { diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs index 6b08745dd7..cbab79e2ab 100644 --- a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs @@ -10,7 +10,7 @@ using osu.Framework.Input.Events; using osu.Framework.Screens; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Match; diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/Multi/Match/Components/Footer.cs index fdf1d0dbeb..7074ceca38 100644 --- a/osu.Game/Screens/Multi/Match/Components/Footer.cs +++ b/osu.Game/Screens/Multi/Match/Components/Footer.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Playlists; using osuTK; diff --git a/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs b/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs index c7fc329a1a..23a3da6e38 100644 --- a/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs +++ b/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs @@ -8,8 +8,8 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.GameTypes; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.GameTypes; using osu.Game.Screens.Multi.Components; using osuTK; diff --git a/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs index f8b64a54ef..b790ad9be5 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs @@ -4,7 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Chat; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi.Match.Components { diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs index f2409d64e7..8cc7b62f98 100644 --- a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs @@ -8,7 +8,7 @@ using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Screens.Multi.Match.Components { diff --git a/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs b/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs index 7ef39c2a74..2292826d55 100644 --- a/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs +++ b/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Components; using osuTK; using osuTK.Graphics; diff --git a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs index 0598524e81..b626156852 100644 --- a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Match/RoomSubScreen.cs @@ -10,7 +10,7 @@ using osu.Framework.Bindables; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; @@ -36,7 +36,7 @@ namespace osu.Game.Screens.Multi.Match private BeatmapManager beatmapManager { get; set; } [Resolved(canBeNull: true)] - protected Multiplayer Multiplayer { get; private set; } + protected MultiplayerScreen Multiplayer { get; private set; } private IBindable> managerUpdated; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs b/osu.Game/Screens/Multi/Multiplayer/CreateMultiplayerMatchButton.cs similarity index 77% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs rename to osu.Game/Screens/Multi/Multiplayer/CreateMultiplayerMatchButton.cs index cdaeb6faec..b8b3f15fca 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/CreateRealtimeMatchButton.cs +++ b/osu.Game/Screens/Multi/Multiplayer/CreateMultiplayerMatchButton.cs @@ -3,12 +3,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Match.Components; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.Multiplayer { - public class CreateRealtimeMatchButton : PurpleTriangleButton + public class CreateMultiplayerMatchButton : PurpleTriangleButton { [BackgroundDependencyLoader] private void load(StatefulMultiplayerClient multiplayerClient) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/Multi/Multiplayer/Match/BeatmapSelectionControl.cs similarity index 91% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/Match/BeatmapSelectionControl.cs rename to osu.Game/Screens/Multi/Multiplayer/Match/BeatmapSelectionControl.cs index 1939744916..dfb6feeaf9 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Match/BeatmapSelectionControl.cs @@ -11,12 +11,12 @@ using osu.Framework.Screens; using osu.Game.Online.API; using osu.Game.Screens.Multi.Match.Components; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match +namespace osu.Game.Screens.Multi.Multiplayer.Match { public class BeatmapSelectionControl : MultiplayerComposite { [Resolved] - private RealtimeMatchSubScreen matchSubScreen { get; set; } + private MultiplayerMatchSubScreen matchSubScreen { get; set; } [Resolved] private IAPIProvider api { get; set; } @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match RelativeSizeAxes = Axes.X, Height = 40, Text = "Select beatmap", - Action = () => matchSubScreen.Push(new RealtimeMatchSongSelect()), + Action = () => matchSubScreen.Push(new MultiplayerMatchSongSelect()), Alpha = 0 } } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchFooter.cs b/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchFooter.cs similarity index 84% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchFooter.cs rename to osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchFooter.cs index 31871729f6..145ae18817 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchFooter.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -8,12 +8,12 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osuTK; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match +namespace osu.Game.Screens.Multi.Multiplayer.Match { - public class RealtimeMatchFooter : CompositeDrawable + public class MultiplayerMatchFooter : CompositeDrawable { public const float HEIGHT = 50; @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match private readonly Drawable background; - public RealtimeMatchFooter() + public MultiplayerMatchFooter() { RelativeSizeAxes = Axes.X; Height = HEIGHT; @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match InternalChildren = new[] { background = new Box { RelativeSizeAxes = Axes.Both }, - new RealtimeReadyButton + new MultiplayerReadyButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchHeader.cs b/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchHeader.cs similarity index 95% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchHeader.cs rename to osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchHeader.cs index a9a10d1510..0c0e580a3e 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchHeader.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchHeader.cs @@ -17,9 +17,9 @@ using FontWeight = osu.Game.Graphics.FontWeight; using OsuColour = osu.Game.Graphics.OsuColour; using OsuFont = osu.Game.Graphics.OsuFont; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match +namespace osu.Game.Screens.Multi.Multiplayer.Match { - public class RealtimeMatchHeader : MultiplayerComposite + public class MultiplayerMatchHeader : MultiplayerComposite { public const float HEIGHT = 50; @@ -32,7 +32,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match [Resolved] private IAPIProvider api { get; set; } - public RealtimeMatchHeader() + public MultiplayerMatchHeader() { RelativeSizeAxes = Axes.X; Height = HEIGHT; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs similarity index 99% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs rename to osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index a93b1b09d1..4a5b5fd181 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeMatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -15,15 +15,15 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Screens.Multi.Match.Components; using osuTK; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match +namespace osu.Game.Screens.Multi.Multiplayer.Match { - public class RealtimeMatchSettingsOverlay : MatchSettingsOverlay + public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay { [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs b/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerReadyButton.cs similarity index 95% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs rename to osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerReadyButton.cs index be405feef1..cea1eeecbb 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Match/RealtimeReadyButton.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerReadyButton.cs @@ -14,13 +14,13 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Components; using osuTK; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match +namespace osu.Game.Screens.Multi.Multiplayer.Match { - public class RealtimeReadyButton : RealtimeRoomComposite + public class MultiplayerReadyButton : MultiplayerRoomComposite { public Bindable SelectedItem => button.SelectedItem; @@ -39,7 +39,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Match private int countReady; - public RealtimeReadyButton() + public MultiplayerReadyButton() { InternalChild = button = new ButtonWithTrianglesExposed { diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs b/osu.Game/Screens/Multi/Multiplayer/Multiplayer.cs similarity index 86% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs rename to osu.Game/Screens/Multi/Multiplayer/Multiplayer.cs index 87c838f29f..97fa4cfa6e 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMultiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Multiplayer.cs @@ -7,13 +7,13 @@ using osu.Framework.Screens; using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Lounge; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.Multiplayer { - public class RealtimeMultiplayer : Multiplayer + public class Multiplayer : MultiplayerScreen { [Resolved] private StatefulMultiplayerClient client { get; set; } @@ -28,7 +28,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override void UpdatePollingRate(bool isIdle) { - var playlistsManager = (RealtimeRoomManager)RoomManager; + var playlistsManager = (MultiplayerRoomManager)RoomManager; if (!this.IsCurrentScreen()) { @@ -58,16 +58,16 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override Room CreateNewRoom() { var room = new Room { Name = { Value = $"{API.LocalUser}'s awesome room" } }; - room.Category.Value = RoomCategory.Realtime; + room.Category.Value = RoomCategory.Multiplayer; return room; } protected override string ScreenTitle => "Multiplayer"; - protected override RoomManager CreateRoomManager() => new RealtimeRoomManager(); + protected override RoomManager CreateRoomManager() => new MultiplayerRoomManager(); - protected override LoungeSubScreen CreateLounge() => new RealtimeLoungeSubScreen(); + protected override LoungeSubScreen CreateLounge() => new MultiplayerLoungeSubScreen(); - protected override OsuButton CreateNewMultiplayerGameButton() => new CreateRealtimeMatchButton(); + protected override OsuButton CreateNewMultiplayerGameButton() => new CreateMultiplayerMatchButton(); } } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeFilterControl.cs b/osu.Game/Screens/Multi/Multiplayer/MultiplayerFilterControl.cs similarity index 79% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeFilterControl.cs rename to osu.Game/Screens/Multi/Multiplayer/MultiplayerFilterControl.cs index acd9a057e3..bebad1944e 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeFilterControl.cs +++ b/osu.Game/Screens/Multi/Multiplayer/MultiplayerFilterControl.cs @@ -3,9 +3,9 @@ using osu.Game.Screens.Multi.Lounge.Components; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.Multiplayer { - public class RealtimeFilterControl : FilterControl + public class MultiplayerFilterControl : FilterControl { protected override FilterCriteria CreateCriteria() { diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs b/osu.Game/Screens/Multi/Multiplayer/MultiplayerLoungeSubScreen.cs similarity index 79% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs rename to osu.Game/Screens/Multi/Multiplayer/MultiplayerLoungeSubScreen.cs index b53ec94519..ffc81efe3c 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeLoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -4,18 +4,18 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Game.Online.Multiplayer; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Match; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.Multiplayer { - public class RealtimeLoungeSubScreen : LoungeSubScreen + public class MultiplayerLoungeSubScreen : LoungeSubScreen { - protected override FilterControl CreateFilterControl() => new RealtimeFilterControl(); + protected override FilterControl CreateFilterControl() => new MultiplayerFilterControl(); - protected override RoomSubScreen CreateRoomSubScreen(Room room) => new RealtimeMatchSubScreen(room); + protected override RoomSubScreen CreateRoomSubScreen(Room room) => new MultiplayerMatchSubScreen(room); [Resolved] private StatefulMultiplayerClient client { get; set; } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs b/osu.Game/Screens/Multi/Multiplayer/MultiplayerMatchSongSelect.cs similarity index 92% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs rename to osu.Game/Screens/Multi/Multiplayer/MultiplayerMatchSongSelect.cs index 8f317800e3..ed1321d4e2 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSongSelect.cs +++ b/osu.Game/Screens/Multi/Multiplayer/MultiplayerMatchSongSelect.cs @@ -10,12 +10,12 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Select; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.Multiplayer { - public class RealtimeMatchSongSelect : SongSelect, IMultiplayerSubScreen + public class MultiplayerMatchSongSelect : SongSelect, IMultiplayerSubScreen { public string ShortTitle => "song selection"; @@ -29,7 +29,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer private LoadingLayer loadingLayer; - public RealtimeMatchSongSelect() + public MultiplayerMatchSongSelect() { Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs b/osu.Game/Screens/Multi/Multiplayer/MultiplayerMatchSubScreen.cs similarity index 92% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs rename to osu.Game/Screens/Multi/Multiplayer/MultiplayerMatchSubScreen.cs index 1778bc272c..4e371d4ed4 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeMatchSubScreen.cs +++ b/osu.Game/Screens/Multi/Multiplayer/MultiplayerMatchSubScreen.cs @@ -10,19 +10,19 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Match; using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Multi.RealtimeMultiplayer.Match; -using osu.Game.Screens.Multi.RealtimeMultiplayer.Participants; +using osu.Game.Screens.Multi.Multiplayer.Match; +using osu.Game.Screens.Multi.Multiplayer.Participants; using osu.Game.Users; -using ParticipantsList = osu.Game.Screens.Multi.RealtimeMultiplayer.Participants.ParticipantsList; +using ParticipantsList = osu.Game.Screens.Multi.Multiplayer.Participants.ParticipantsList; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.Multiplayer { [Cached] - public class RealtimeMatchSubScreen : RoomSubScreen + public class MultiplayerMatchSubScreen : RoomSubScreen { public override string Title { get; } @@ -31,11 +31,11 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer [Resolved] private StatefulMultiplayerClient client { get; set; } - private RealtimeMatchSettingsOverlay settingsOverlay; + private MultiplayerMatchSettingsOverlay settingsOverlay; private IBindable isConnected; - public RealtimeMatchSubScreen(Room room) + public MultiplayerMatchSubScreen(Room room) { Title = room.RoomID.Value == null ? "New room" : room.Name.Value; Activity.Value = new UserActivity.InLobby(room); @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { new Drawable[] { - new RealtimeMatchHeader + new MultiplayerMatchHeader { OpenSettings = () => settingsOverlay.Show() } @@ -150,7 +150,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer }, new Drawable[] { - new RealtimeMatchFooter { SelectedItem = { BindTarget = SelectedItem } } + new MultiplayerMatchFooter { SelectedItem = { BindTarget = SelectedItem } } } }, RowDimensions = new[] @@ -159,7 +159,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer new Dimension(GridSizeMode.AutoSize), } }, - settingsOverlay = new RealtimeMatchSettingsOverlay + settingsOverlay = new MultiplayerMatchSettingsOverlay { RelativeSizeAxes = Axes.Both, State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden } @@ -202,7 +202,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer int[] userIds = client.Room.Users.Where(u => u.State >= MultiplayerUserState.WaitingForLoad).Select(u => u.UserID).ToArray(); - StartPlay(() => new RealtimePlayer(SelectedItem.Value, userIds)); + StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs b/osu.Game/Screens/Multi/Multiplayer/MultiplayerPlayer.cs similarity index 93% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs rename to osu.Game/Screens/Multi/Multiplayer/MultiplayerPlayer.cs index 033e4756eb..3255bcc642 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimePlayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer/MultiplayerPlayer.cs @@ -9,7 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.Multi.Play; using osu.Game.Screens.Play; @@ -17,10 +17,10 @@ using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osuTK; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.Multiplayer { // Todo: The "room" part of PlaylistsPlayer should be split out into an abstract player class to be inherited instead. - public class RealtimePlayer : PlaylistsPlayer + public class MultiplayerPlayer : PlaylistsPlayer { protected override bool PauseOnFocusLost => false; @@ -45,7 +45,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer /// /// The playlist item to be played. /// The users which are participating in this game. - public RealtimePlayer(PlaylistItem playlistItem, int[] userIds) + public MultiplayerPlayer(PlaylistItem playlistItem, int[] userIds) : base(playlistItem, new PlayerConfiguration { AllowPause = false, @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer { if (!connected.NewValue) { - // messaging to the user about this disconnect will be provided by the RealtimeMatchSubScreen. + // messaging to the user about this disconnect will be provided by the MultiplayerMatchSubScreen. failAndBail(); } }, true); @@ -142,7 +142,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override ResultsScreen CreateResults(ScoreInfo score) { Debug.Assert(RoomId.Value != null); - return new RealtimeResultsScreen(score, RoomId.Value.Value, PlaylistItem); + return new MultiplayerResultsScreen(score, RoomId.Value.Value, PlaylistItem); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs b/osu.Game/Screens/Multi/Multiplayer/MultiplayerResultsScreen.cs similarity index 55% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs rename to osu.Game/Screens/Multi/Multiplayer/MultiplayerResultsScreen.cs index 6bec06cbba..a01930c5ef 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeResultsScreen.cs +++ b/osu.Game/Screens/Multi/Multiplayer/MultiplayerResultsScreen.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 osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.Multi.Ranking; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.Multiplayer { - public class RealtimeResultsScreen : PlaylistsResultsScreen + public class MultiplayerResultsScreen : PlaylistsResultsScreen { - public RealtimeResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem) + public MultiplayerResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem) : base(score, roomId, playlistItem, false, false) { } diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs b/osu.Game/Screens/Multi/Multiplayer/MultiplayerRoomComposite.cs similarity index 83% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs rename to osu.Game/Screens/Multi/Multiplayer/MultiplayerRoomComposite.cs index e6d1274316..6fe7c10bfb 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomComposite.cs +++ b/osu.Game/Screens/Multi/Multiplayer/MultiplayerRoomComposite.cs @@ -3,11 +3,11 @@ using JetBrains.Annotations; using osu.Framework.Allocation; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Multiplayer; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.Multiplayer { - public abstract class RealtimeRoomComposite : MultiplayerComposite + public abstract class MultiplayerRoomComposite : MultiplayerComposite { [CanBeNull] protected MultiplayerRoom Room => Client.Room; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs b/osu.Game/Screens/Multi/Multiplayer/MultiplayerRoomManager.cs similarity index 92% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs rename to osu.Game/Screens/Multi/Multiplayer/MultiplayerRoomManager.cs index eb6e5fad07..fdd46cdc2a 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/RealtimeRoomManager.cs +++ b/osu.Game/Screens/Multi/Multiplayer/MultiplayerRoomManager.cs @@ -11,13 +11,13 @@ using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; using osu.Game.Extensions; using osu.Game.Online.Multiplayer; -using osu.Game.Online.Multiplayer.RoomStatuses; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Rooms; +using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Screens.Multi.Components; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer +namespace osu.Game.Screens.Multi.Multiplayer { - public class RealtimeRoomManager : RoomManager + public class MultiplayerRoomManager : RoomManager { [Resolved] private StatefulMultiplayerClient multiplayerClient { get; set; } @@ -114,19 +114,19 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override IEnumerable CreatePollingComponents() => new RoomPollingComponent[] { - listingPollingComponent = new RealtimeListingPollingComponent + listingPollingComponent = new MultiplayerListingPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, AllowPolling = { BindTarget = allowPolling } }, - new RealtimeSelectionPollingComponent + new MultiplayerSelectionPollingComponent { TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls }, AllowPolling = { BindTarget = allowPolling } } }; - private class RealtimeListingPollingComponent : ListingPollingComponent + private class MultiplayerListingPollingComponent : ListingPollingComponent { public readonly IBindable AllowPolling = new Bindable(); @@ -147,7 +147,7 @@ namespace osu.Game.Screens.Multi.RealtimeMultiplayer protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); } - private class RealtimeSelectionPollingComponent : SelectionPollingComponent + private class MultiplayerSelectionPollingComponent : SelectionPollingComponent { public readonly IBindable AllowPolling = new Bindable(); diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantPanel.cs similarity index 97% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs rename to osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantPanel.cs index 85393d1bae..93f8a9de8b 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantPanel.cs @@ -16,15 +16,15 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Multiplayer; using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +namespace osu.Game.Screens.Multi.Multiplayer.Participants { - public class ParticipantPanel : RealtimeRoomComposite, IHasContextMenu + public class ParticipantPanel : MultiplayerRoomComposite, IHasContextMenu { public readonly MultiplayerRoomUser User; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantsList.cs similarity index 93% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs rename to osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantsList.cs index 218c2cabb7..3f37274d4c 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantsList.cs @@ -9,9 +9,9 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osuTK; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +namespace osu.Game.Screens.Multi.Multiplayer.Participants { - public class ParticipantsList : RealtimeRoomComposite + public class ParticipantsList : MultiplayerRoomComposite { private FillFlowContainer panels; diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsListHeader.cs b/osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantsListHeader.cs similarity index 86% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsListHeader.cs rename to osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantsListHeader.cs index 0ca7d34005..dc3ceedfd7 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/ParticipantsListHeader.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantsListHeader.cs @@ -2,10 +2,10 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi.Components; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +namespace osu.Game.Screens.Multi.Multiplayer.Participants { public class ParticipantsListHeader : OverlinedHeader { diff --git a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/Multi/Multiplayer/Participants/StateDisplay.cs similarity index 97% rename from osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs rename to osu.Game/Screens/Multi/Multiplayer/Participants/StateDisplay.cs index 844f239363..61faa0d85d 100644 --- a/osu.Game/Screens/Multi/RealtimeMultiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Participants/StateDisplay.cs @@ -8,10 +8,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Multiplayer; using osuTK; -namespace osu.Game.Screens.Multi.RealtimeMultiplayer.Participants +namespace osu.Game.Screens.Multi.Multiplayer.Participants { public class StateDisplay : CompositeDrawable { diff --git a/osu.Game/Screens/Multi/MultiplayerComposite.cs b/osu.Game/Screens/Multi/MultiplayerComposite.cs index 6e0c69d712..fe4ca759b0 100644 --- a/osu.Game/Screens/Multi/MultiplayerComposite.cs +++ b/osu.Game/Screens/Multi/MultiplayerComposite.cs @@ -5,7 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Users; namespace osu.Game.Screens.Multi diff --git a/osu.Game/Screens/Multi/Multiplayer.cs b/osu.Game/Screens/Multi/MultiplayerScreen.cs similarity index 99% rename from osu.Game/Screens/Multi/Multiplayer.cs rename to osu.Game/Screens/Multi/MultiplayerScreen.cs index 34b7139f2d..983c7aeac9 100644 --- a/osu.Game/Screens/Multi/Multiplayer.cs +++ b/osu.Game/Screens/Multi/MultiplayerScreen.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Input; using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.Menu; using osu.Game.Screens.Multi.Components; @@ -27,7 +27,7 @@ using osuTK; namespace osu.Game.Screens.Multi { [Cached] - public abstract class Multiplayer : OsuScreen + public abstract class MultiplayerScreen : OsuScreen { public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true; @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Multi private readonly Drawable header; private readonly Drawable headerBackground; - protected Multiplayer() + protected MultiplayerScreen() { Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs index 65b0091505..f05f732494 100644 --- a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs +++ b/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs @@ -11,7 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; using osu.Game.Screens.Multi.Ranking; diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsLoungeSubScreen.cs index b40c543b68..513854515b 100644 --- a/osu.Game/Screens/Multi/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsLoungeSubScreen.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.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Lounge.Components; using osu.Game.Screens.Multi.Match; diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsMatchSettingsOverlay.cs index af29e8d34d..f3109a33e4 100644 --- a/osu.Game/Screens/Multi/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.Multi.Match.Components; using osuTK; diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsMultiplayer.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsMultiplayer.cs index fce24da966..92b0160247 100644 --- a/osu.Game/Screens/Multi/Playlists/PlaylistsMultiplayer.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsMultiplayer.cs @@ -4,14 +4,14 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Lounge; using osu.Game.Screens.Multi.Match; namespace osu.Game.Screens.Multi.Playlists { - public class PlaylistsMultiplayer : Multiplayer + public class PlaylistsMultiplayer : MultiplayerScreen { protected override void UpdatePollingRate(bool isIdle) { diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsReadyButton.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsReadyButton.cs index f5adf899e3..3c35e2a6f3 100644 --- a/osu.Game/Screens/Multi/Playlists/PlaylistsReadyButton.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsReadyButton.cs @@ -5,7 +5,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Components; namespace osu.Game.Screens.Multi.Playlists diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs index b2bcba1724..67242003e6 100644 --- a/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs @@ -9,7 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Match; using osu.Game.Screens.Multi.Match.Components; @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Multi.Playlists private Bindable roomId { get; set; } [Resolved(canBeNull: true)] - private Multiplayer multiplayer { get; set; } + private MultiplayerScreen multiplayer { get; set; } private MatchSettingsOverlay settingsOverlay; private MatchLeaderboard leaderboard; diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs index c757433e2f..7b1ab2c5b7 100644 --- a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs +++ b/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs @@ -11,7 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.Ranking; diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 8692833a21..80fd5c2067 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -9,7 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Beatmaps; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.Components; diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs similarity index 67% rename from osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs rename to osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 30bd3ebc32..0e23f4d8c6 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/RealtimeMultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -6,32 +6,32 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.Lounge.Components; -namespace osu.Game.Tests.Visual.RealtimeMultiplayer +namespace osu.Game.Tests.Visual.Multiplayer { - public abstract class RealtimeMultiplayerTestScene : MultiplayerTestScene + public abstract class MultiplayerTestScene : RoomTestScene { [Cached(typeof(StatefulMultiplayerClient))] - public TestRealtimeMultiplayerClient Client { get; } + public TestMultiplayerClient Client { get; } [Cached(typeof(IRoomManager))] - public TestRealtimeRoomManager RoomManager { get; } + public TestMultiplayerRoomManager RoomManager { get; } [Cached] public Bindable Filter { get; } protected override Container Content => content; - private readonly TestRealtimeRoomContainer content; + private readonly TestMultiplayerRoomContainer content; private readonly bool joinRoom; - protected RealtimeMultiplayerTestScene(bool joinRoom = true) + protected MultiplayerTestScene(bool joinRoom = true) { this.joinRoom = joinRoom; - base.Content.Add(content = new TestRealtimeRoomContainer { RelativeSizeAxes = Axes.Both }); + base.Content.Add(content = new TestMultiplayerRoomContainer { RelativeSizeAxes = Axes.Both }); Client = content.Client; RoomManager = content.RoomManager; diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs similarity index 95% rename from osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs rename to osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 52047016e2..9a839c8d22 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -9,12 +9,12 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Multiplayer; using osu.Game.Users; -namespace osu.Game.Tests.Visual.RealtimeMultiplayer +namespace osu.Game.Tests.Visual.Multiplayer { - public class TestRealtimeMultiplayerClient : StatefulMultiplayerClient + public class TestMultiplayerClient : StatefulMultiplayerClient { public override IBindable IsConnected => isConnected; private readonly Bindable isConnected = new Bindable(true); diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs similarity index 67% rename from osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs rename to osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs index 3565d6ac5d..0df397d98e 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomContainer.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs @@ -5,34 +5,34 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Online.RealtimeMultiplayer; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.Multi; using osu.Game.Screens.Multi.Lounge.Components; -namespace osu.Game.Tests.Visual.RealtimeMultiplayer +namespace osu.Game.Tests.Visual.Multiplayer { - public class TestRealtimeRoomContainer : Container + public class TestMultiplayerRoomContainer : Container { protected override Container Content => content; private readonly Container content; [Cached(typeof(StatefulMultiplayerClient))] - public readonly TestRealtimeMultiplayerClient Client; + public readonly TestMultiplayerClient Client; [Cached(typeof(IRoomManager))] - public readonly TestRealtimeRoomManager RoomManager; + public readonly TestMultiplayerRoomManager RoomManager; [Cached] public readonly Bindable Filter = new Bindable(new FilterCriteria()); - public TestRealtimeRoomContainer() + public TestMultiplayerRoomContainer() { RelativeSizeAxes = Axes.Both; AddRangeInternal(new Drawable[] { - Client = new TestRealtimeMultiplayerClient(), - RoomManager = new TestRealtimeRoomManager(), + Client = new TestMultiplayerClient(), + RoomManager = new TestMultiplayerRoomManager(), content = new Container { RelativeSizeAxes = Axes.Both } }); } diff --git a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs similarity index 95% rename from osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs rename to osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index 0d1314fb51..abfefd363a 100644 --- a/osu.Game/Tests/Visual/RealtimeMultiplayer/TestRealtimeRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -8,15 +8,15 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.API.Requests; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.RealtimeMultiplayer; +using osu.Game.Screens.Multi.Multiplayer; -namespace osu.Game.Tests.Visual.RealtimeMultiplayer +namespace osu.Game.Tests.Visual.Multiplayer { - public class TestRealtimeRoomManager : RealtimeRoomManager + public class TestMultiplayerRoomManager : MultiplayerRoomManager { [Resolved] private IAPIProvider api { get; set; } diff --git a/osu.Game/Tests/Visual/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/RoomTestScene.cs similarity index 90% rename from osu.Game/Tests/Visual/MultiplayerTestScene.cs rename to osu.Game/Tests/Visual/RoomTestScene.cs index 6f24e00a92..aaf5c7624f 100644 --- a/osu.Game/Tests/Visual/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/RoomTestScene.cs @@ -4,11 +4,11 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; namespace osu.Game.Tests.Visual { - public abstract class MultiplayerTestScene : ScreenTestScene + public abstract class RoomTestScene : ScreenTestScene { [Cached] private readonly Bindable currentRoom = new Bindable(); diff --git a/osu.Game/Users/UserActivity.cs b/osu.Game/Users/UserActivity.cs index 0b4fa94942..f633773d11 100644 --- a/osu.Game/Users/UserActivity.cs +++ b/osu.Game/Users/UserActivity.cs @@ -3,7 +3,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osuTK.Graphics; From e49dce2c866f804b2e1a93fa49b025c112fc8f06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Dec 2020 15:34:13 +0900 Subject: [PATCH 5683/6909] Fix some missed renames --- .../GameTypes/{GameTypeTimeshift.cs => GameTypePlaylists.cs} | 0 .../{TimeshiftFilterControl.cs => PlaylistsFilterControl.cs} | 0 .../Screens/Multi/Play/{TimeshiftPlayer.cs => PlaylistsPlayer.cs} | 0 .../{TimeshiftResultsScreen.cs => PlaylistsResultsScreen.cs} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game/Online/Rooms/GameTypes/{GameTypeTimeshift.cs => GameTypePlaylists.cs} (100%) rename osu.Game/Screens/Multi/Lounge/Components/{TimeshiftFilterControl.cs => PlaylistsFilterControl.cs} (100%) rename osu.Game/Screens/Multi/Play/{TimeshiftPlayer.cs => PlaylistsPlayer.cs} (100%) rename osu.Game/Screens/Multi/Ranking/{TimeshiftResultsScreen.cs => PlaylistsResultsScreen.cs} (100%) diff --git a/osu.Game/Online/Rooms/GameTypes/GameTypeTimeshift.cs b/osu.Game/Online/Rooms/GameTypes/GameTypePlaylists.cs similarity index 100% rename from osu.Game/Online/Rooms/GameTypes/GameTypeTimeshift.cs rename to osu.Game/Online/Rooms/GameTypes/GameTypePlaylists.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs b/osu.Game/Screens/Multi/Lounge/Components/PlaylistsFilterControl.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/Components/TimeshiftFilterControl.cs rename to osu.Game/Screens/Multi/Lounge/Components/PlaylistsFilterControl.cs diff --git a/osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs b/osu.Game/Screens/Multi/Play/PlaylistsPlayer.cs similarity index 100% rename from osu.Game/Screens/Multi/Play/TimeshiftPlayer.cs rename to osu.Game/Screens/Multi/Play/PlaylistsPlayer.cs diff --git a/osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs b/osu.Game/Screens/Multi/Ranking/PlaylistsResultsScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/Ranking/TimeshiftResultsScreen.cs rename to osu.Game/Screens/Multi/Ranking/PlaylistsResultsScreen.cs From 13c38c9b5556d988ab343beb70fbe2659a5237db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 14:18:21 +0100 Subject: [PATCH 5684/6909] Fix tests failing due to wrong inheritance --- osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index a72f71d79c..71b8ad05ee 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -13,7 +13,7 @@ using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchLeaderboard : MultiplayerTestScene + public class TestSceneMatchLeaderboard : RoomTestScene { protected override bool UseOnlineAPI => true; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index 292c4846ab..7a3845cbf3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -9,7 +9,7 @@ using osu.Game.Online.Rooms; namespace osu.Game.Tests.Visual.Multiplayer { [HeadlessTest] - public class TestSceneMultiplayerRoomManager : MultiplayerTestScene + public class TestSceneMultiplayerRoomManager : RoomTestScene { private TestMultiplayerRoomContainer roomContainer; private TestMultiplayerRoomManager roomManager => roomContainer.RoomManager; From da8365f9d0047301b63a10103f0e7630fc8b3a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 14:34:47 +0100 Subject: [PATCH 5685/6909] Fix other missed cases of changing inheritance --- osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs | 2 +- .../Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs | 2 +- osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs | 2 +- osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs index 1359274512..f58e1114b8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs @@ -10,7 +10,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneLoungeRoomInfo : MultiplayerTestScene + public class TestSceneLoungeRoomInfo : RoomTestScene { [SetUp] public new void Setup() => Schedule(() => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index f4e0cc415c..571330f50e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -15,7 +15,7 @@ using osuTK; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchBeatmapDetailArea : MultiplayerTestScene + public class TestSceneMatchBeatmapDetailArea : RoomTestScene { [Resolved] private BeatmapManager beatmapManager { get; set; } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index e162009771..61968dce46 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs @@ -11,7 +11,7 @@ using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchHeader : MultiplayerTestScene + public class TestSceneMatchHeader : RoomTestScene { public TestSceneMatchHeader() { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index 4742fd0d84..157597e800 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -23,7 +23,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchSongSelect : MultiplayerTestScene + public class TestSceneMatchSongSelect : RoomTestScene { [Resolved] private BeatmapManager beatmapManager { get; set; } From 3a6a3a067b4816b81bf1851b75640a8e73349221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 14:54:21 +0100 Subject: [PATCH 5686/6909] Rewrite test to cover failure case --- .../Online/TestSceneAccountCreationOverlay.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs index dcfe0432a8..3d65e7e4ba 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneAccountCreationOverlay.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -13,13 +14,12 @@ namespace osu.Game.Tests.Visual.Online public class TestSceneAccountCreationOverlay : OsuTestScene { private readonly Container userPanelArea; + private readonly AccountCreationOverlay accountCreation; private IBindable localUser; public TestSceneAccountCreationOverlay() { - AccountCreationOverlay accountCreation; - Children = new Drawable[] { accountCreation = new AccountCreationOverlay(), @@ -31,8 +31,6 @@ namespace osu.Game.Tests.Visual.Online Origin = Anchor.TopRight, }, }; - - AddStep("show", () => accountCreation.Show()); } [BackgroundDependencyLoader] @@ -42,8 +40,19 @@ namespace osu.Game.Tests.Visual.Online localUser = API.LocalUser.GetBoundCopy(); localUser.BindValueChanged(user => { userPanelArea.Child = new UserGridPanel(user.NewValue) { Width = 200 }; }, true); + } - AddStep("logout", API.Logout); + [Test] + public void TestOverlayVisibility() + { + AddStep("start hidden", () => accountCreation.Hide()); + AddStep("log out", API.Logout); + + AddStep("show manually", () => accountCreation.Show()); + AddUntilStep("overlay is visible", () => accountCreation.State.Value == Visibility.Visible); + + AddStep("log back in", () => API.Login("dummy", "password")); + AddUntilStep("overlay is hidden", () => accountCreation.State.Value == Visibility.Hidden); } } } From 2d7f9bf29045e9096ad8cb9dc672a05ad6ab7c97 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Dec 2020 23:34:29 +0900 Subject: [PATCH 5687/6909] Revert RoomCategory naming change to avoid json deserialization failures --- osu.Game/Online/Rooms/RoomCategory.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/RoomCategory.cs b/osu.Game/Online/Rooms/RoomCategory.cs index f485e65ba9..bb9f1298d3 100644 --- a/osu.Game/Online/Rooms/RoomCategory.cs +++ b/osu.Game/Online/Rooms/RoomCategory.cs @@ -5,8 +5,9 @@ namespace osu.Game.Online.Rooms { public enum RoomCategory { + // used for osu-web deserialization so names shouldn't be changed. Normal, Spotlight, - Multiplayer, + Realtime, } } From e421b6d34e50642cae45794fdb28e64805c6efd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Dec 2020 23:36:09 +0900 Subject: [PATCH 5688/6909] Update some missed variables --- .../Visual/Multiplayer/TestSceneRoomStatus.cs | 2 +- .../Screens/Multi/Multiplayer/Multiplayer.cs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs index 788255501b..7140050bd5 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Name = { Value = "Open" }, Status = { Value = new RoomStatusOpen() }, - Category = { Value = RoomCategory.Multiplayer } + Category = { Value = RoomCategory.Realtime } }) { MatchingFilter = true }, } }; diff --git a/osu.Game/Screens/Multi/Multiplayer/Multiplayer.cs b/osu.Game/Screens/Multi/Multiplayer/Multiplayer.cs index 97fa4cfa6e..76aa6b9f8f 100644 --- a/osu.Game/Screens/Multi/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer/Multiplayer.cs @@ -28,37 +28,37 @@ namespace osu.Game.Screens.Multi.Multiplayer protected override void UpdatePollingRate(bool isIdle) { - var playlistsManager = (MultiplayerRoomManager)RoomManager; + var multiplayerRoomManager = (MultiplayerRoomManager)RoomManager; if (!this.IsCurrentScreen()) { - playlistsManager.TimeBetweenListingPolls.Value = 0; - playlistsManager.TimeBetweenSelectionPolls.Value = 0; + multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; } else { switch (CurrentSubScreen) { case LoungeSubScreen _: - playlistsManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; - playlistsManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; + multiplayerRoomManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; break; // Don't poll inside the match or anywhere else. default: - playlistsManager.TimeBetweenListingPolls.Value = 0; - playlistsManager.TimeBetweenSelectionPolls.Value = 0; + multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; break; } } - Logger.Log($"Polling adjusted (listing: {playlistsManager.TimeBetweenListingPolls.Value}, selection: {playlistsManager.TimeBetweenSelectionPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})"); } protected override Room CreateNewRoom() { var room = new Room { Name = { Value = $"{API.LocalUser}'s awesome room" } }; - room.Category.Value = RoomCategory.Multiplayer; + room.Category.Value = RoomCategory.Realtime; return room; } From 0d8fb83d0a0a56359947ac8e52ff9e10a953db66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 15:37:14 +0100 Subject: [PATCH 5689/6909] Ensure account creation overlay is shown after logout Scheduling the entire API state change callback caused the scheduled hide to fire the first time the user attempted to display the account creation overlay after a logout, because the drawable wasn't present before that (so its scheduler wasn't running). It is not theoretically safe to run `Hide()` unscheduled at its present call site (as the value change callbacks are fired on the background API thread). This could also be fixed by setting `AlwaysPresent = true`, but that's a pretty ugly and unperformant change to make in general. --- osu.Game/Overlays/AccountCreationOverlay.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/AccountCreationOverlay.cs b/osu.Game/Overlays/AccountCreationOverlay.cs index 58ede5502a..3084c7475a 100644 --- a/osu.Game/Overlays/AccountCreationOverlay.cs +++ b/osu.Game/Overlays/AccountCreationOverlay.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Screens; +using osu.Framework.Threading; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Online.API; @@ -93,6 +94,11 @@ namespace osu.Game.Overlays if (welcomeScreen.GetChildScreen() != null) welcomeScreen.MakeCurrent(); + + // there might be a stale scheduled hide from a previous API state change. + // cancel it here so that the overlay is not hidden again after one frame. + scheduledHide?.Cancel(); + scheduledHide = null; } protected override void PopOut() @@ -101,7 +107,9 @@ namespace osu.Game.Overlays this.FadeOut(100); } - private void apiStateChanged(ValueChangedEvent state) => Schedule(() => + private ScheduledDelegate scheduledHide; + + private void apiStateChanged(ValueChangedEvent state) { switch (state.NewValue) { @@ -113,9 +121,10 @@ namespace osu.Game.Overlays break; case APIState.Online: - Hide(); + scheduledHide?.Cancel(); + scheduledHide = Schedule(Hide); break; } - }); + } } } From 8a36eab060264b8b903142000657caf5c7d3056c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Dec 2020 23:42:02 +0900 Subject: [PATCH 5690/6909] Move missed file from Play namespace --- osu.Game/Screens/Multi/Multiplayer/MultiplayerPlayer.cs | 2 +- osu.Game/Screens/Multi/{Play => Playlists}/PlaylistsPlayer.cs | 2 +- osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) rename osu.Game/Screens/Multi/{Play => Playlists}/PlaylistsPlayer.cs (99%) diff --git a/osu.Game/Screens/Multi/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/Multi/Multiplayer/MultiplayerPlayer.cs index 3255bcc642..32d95aa11f 100644 --- a/osu.Game/Screens/Multi/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/Multi/Multiplayer/MultiplayerPlayer.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Play; +using osu.Game.Screens.Multi.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; diff --git a/osu.Game/Screens/Multi/Play/PlaylistsPlayer.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsPlayer.cs similarity index 99% rename from osu.Game/Screens/Multi/Play/PlaylistsPlayer.cs rename to osu.Game/Screens/Multi/Playlists/PlaylistsPlayer.cs index f05f732494..40198d82cd 100644 --- a/osu.Game/Screens/Multi/Play/PlaylistsPlayer.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsPlayer.cs @@ -18,7 +18,7 @@ using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; -namespace osu.Game.Screens.Multi.Play +namespace osu.Game.Screens.Multi.Playlists { public class PlaylistsPlayer : Player { diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs index 67242003e6..f6d084b112 100644 --- a/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs @@ -13,7 +13,6 @@ using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Match; 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 osu.Game.Users; From 836d1491d0d1576ee444179953eb863bb8c1d740 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Dec 2020 23:45:44 +0900 Subject: [PATCH 5691/6909] PlaylistsMultiplayer -> Playlists --- .../Visual/Navigation/TestSceneScreenNavigation.cs | 5 ++--- osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs | 3 +-- osu.Game/Screens/Menu/MainMenu.cs | 2 +- .../Playlists/{PlaylistsMultiplayer.cs => Playlists.cs} | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) rename osu.Game/Screens/Multi/Playlists/{PlaylistsMultiplayer.cs => Playlists.cs} (97%) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 381bbca74f..ac63c89183 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -11,7 +11,6 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; -using osu.Game.Screens.Multi.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Options; @@ -108,14 +107,14 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitMultiWithEscape() { - PushAndConfirm(() => new PlaylistsMultiplayer()); + PushAndConfirm(() => new Screens.Multi.Playlists.Playlists()); exitViaEscapeAndConfirm(); } [Test] public void TestExitMultiWithBackButton() { - PushAndConfirm(() => new PlaylistsMultiplayer()); + PushAndConfirm(() => new Screens.Multi.Playlists.Playlists()); exitViaBackButtonAndConfirm(); } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs index 8203ca4845..b780eb5347 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs @@ -4,7 +4,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Game.Overlays; -using osu.Game.Screens.Multi.Playlists; namespace osu.Game.Tests.Visual.Playlists { @@ -18,7 +17,7 @@ namespace osu.Game.Tests.Visual.Playlists public TestScenePlaylistsScreen() { - var multi = new PlaylistsMultiplayer(); + var multi = new Screens.Multi.Playlists.Playlists(); AddStep("show", () => LoadScreen(multi)); AddUntilStep("wait for loaded", () => multi.IsLoaded); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 15ac2237d3..aeb58a75fc 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -106,7 +106,7 @@ namespace osu.Game.Screens.Menu }, OnSolo = onSolo, OnMultiplayer = () => this.Push(new Multiplayer()), - OnPlaylists = () => this.Push(new PlaylistsMultiplayer()), + OnPlaylists = () => this.Push(new Playlists()), OnExit = confirmAndExit, } } diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsMultiplayer.cs b/osu.Game/Screens/Multi/Playlists/Playlists.cs similarity index 97% rename from osu.Game/Screens/Multi/Playlists/PlaylistsMultiplayer.cs rename to osu.Game/Screens/Multi/Playlists/Playlists.cs index 92b0160247..da6aade942 100644 --- a/osu.Game/Screens/Multi/Playlists/PlaylistsMultiplayer.cs +++ b/osu.Game/Screens/Multi/Playlists/Playlists.cs @@ -11,7 +11,7 @@ using osu.Game.Screens.Multi.Match; namespace osu.Game.Screens.Multi.Playlists { - public class PlaylistsMultiplayer : MultiplayerScreen + public class Playlists : MultiplayerScreen { protected override void UpdatePollingRate(bool isIdle) { From 9de1a67e0389017d5a233fa7b38478f26287b305 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 25 Dec 2020 23:47:32 +0900 Subject: [PATCH 5692/6909] Move PlaylistsResultsScreen to correct namespace --- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 2 +- osu.Game/Screens/Multi/Multiplayer/MultiplayerResultsScreen.cs | 2 +- osu.Game/Screens/Multi/Playlists/PlaylistsPlayer.cs | 1 - .../Multi/{Ranking => Playlists}/PlaylistsResultsScreen.cs | 2 +- osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs | 1 - 5 files changed, 3 insertions(+), 5 deletions(-) rename osu.Game/Screens/Multi/{Ranking => Playlists}/PlaylistsResultsScreen.cs (99%) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index b42f2e1d2d..97bb48c7b5 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -19,7 +19,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Ranking; +using osu.Game.Screens.Multi.Playlists; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; using osu.Game.Users; diff --git a/osu.Game/Screens/Multi/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/Multi/Multiplayer/MultiplayerResultsScreen.cs index a01930c5ef..3e39473954 100644 --- a/osu.Game/Screens/Multi/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/Multi/Multiplayer/MultiplayerResultsScreen.cs @@ -3,7 +3,7 @@ using osu.Game.Online.Rooms; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Ranking; +using osu.Game.Screens.Multi.Playlists; namespace osu.Game.Screens.Multi.Multiplayer { diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsPlayer.cs index 40198d82cd..25a3340ead 100644 --- a/osu.Game/Screens/Multi/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsPlayer.cs @@ -14,7 +14,6 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; diff --git a/osu.Game/Screens/Multi/Ranking/PlaylistsResultsScreen.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsResultsScreen.cs similarity index 99% rename from osu.Game/Screens/Multi/Ranking/PlaylistsResultsScreen.cs rename to osu.Game/Screens/Multi/Playlists/PlaylistsResultsScreen.cs index 7b1ab2c5b7..5d1d3a2724 100644 --- a/osu.Game/Screens/Multi/Ranking/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsResultsScreen.cs @@ -15,7 +15,7 @@ using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -namespace osu.Game.Screens.Multi.Ranking +namespace osu.Game.Screens.Multi.Playlists { public class PlaylistsResultsScreen : ResultsScreen { diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs index f6d084b112..4867268fe1 100644 --- a/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs @@ -13,7 +13,6 @@ using osu.Game.Online.Rooms; using osu.Game.Screens.Multi.Components; using osu.Game.Screens.Multi.Match; using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Multi.Ranking; using osu.Game.Screens.Select; using osu.Game.Users; using Footer = osu.Game.Screens.Multi.Match.Components.Footer; From e797e5ce7a73441cc2c65e4fd5909c789b3fc3fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 16:48:03 +0100 Subject: [PATCH 5693/6909] Rename Multi directory to OnlinePlay --- .../Components/BeatmapDetailAreaPlaylistTabItem.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Components/BeatmapTitle.cs | 0 .../Screens/{Multi => OnlinePlay}/Components/BeatmapTypeInfo.cs | 0 .../{Multi => OnlinePlay}/Components/DisableableTabControl.cs | 0 .../Screens/{Multi => OnlinePlay}/Components/DrawableGameType.cs | 0 .../{Multi => OnlinePlay}/Components/ListingPollingComponent.cs | 0 .../{Multi => OnlinePlay}/Components/MatchBeatmapDetailArea.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Components/ModeTypeInfo.cs | 0 .../Components/MultiplayerBackgroundSprite.cs | 0 .../Screens/{Multi => OnlinePlay}/Components/OverlinedHeader.cs | 0 .../{Multi => OnlinePlay}/Components/OverlinedPlaylistHeader.cs | 0 .../{Multi => OnlinePlay}/Components/ParticipantCountDisplay.cs | 0 .../{Multi => OnlinePlay}/Components/ParticipantsDisplay.cs | 0 .../Screens/{Multi => OnlinePlay}/Components/ParticipantsList.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Components/ReadyButton.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Components/RoomManager.cs | 0 .../{Multi => OnlinePlay}/Components/RoomPollingComponent.cs | 0 .../Screens/{Multi => OnlinePlay}/Components/RoomStatusInfo.cs | 0 .../{Multi => OnlinePlay}/Components/SelectionPollingComponent.cs | 0 .../{Multi => OnlinePlay}/Components/StatusColouredContainer.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/DrawableRoomPlaylist.cs | 0 .../Screens/{Multi => OnlinePlay}/DrawableRoomPlaylistItem.cs | 0 .../{Multi => OnlinePlay}/DrawableRoomPlaylistWithResults.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Header.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/IMultiplayerSubScreen.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/IRoomManager.cs | 0 .../{Multi => OnlinePlay}/Lounge/Components/DrawableRoom.cs | 0 .../{Multi => OnlinePlay}/Lounge/Components/FilterControl.cs | 0 .../{Multi => OnlinePlay}/Lounge/Components/FilterCriteria.cs | 0 .../{Multi => OnlinePlay}/Lounge/Components/ParticipantInfo.cs | 0 .../Lounge/Components/PlaylistsFilterControl.cs | 0 .../Screens/{Multi => OnlinePlay}/Lounge/Components/RoomInfo.cs | 0 .../{Multi => OnlinePlay}/Lounge/Components/RoomInspector.cs | 0 .../{Multi => OnlinePlay}/Lounge/Components/RoomStatusFilter.cs | 0 .../{Multi => OnlinePlay}/Lounge/Components/RoomsContainer.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Lounge/LoungeSubScreen.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/Footer.cs | 0 .../{Multi => OnlinePlay}/Match/Components/GameTypePicker.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/Header.cs | 0 .../{Multi => OnlinePlay}/Match/Components/MatchChatDisplay.cs | 0 .../{Multi => OnlinePlay}/Match/Components/MatchLeaderboard.cs | 0 .../Match/Components/MatchLeaderboardScore.cs | 0 .../Match/Components/MatchSettingsOverlay.cs | 0 .../Match/Components/PurpleTriangleButton.cs | 0 .../Match/Components/RoomAvailabilityPicker.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Match/RoomSubScreen.cs | 0 .../Multiplayer/CreateMultiplayerMatchButton.cs | 0 .../Multiplayer/Match/BeatmapSelectionControl.cs | 0 .../Multiplayer/Match/MultiplayerMatchFooter.cs | 0 .../Multiplayer/Match/MultiplayerMatchHeader.cs | 0 .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 0 .../Multiplayer/Match/MultiplayerReadyButton.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Multiplayer.cs | 0 .../{Multi => OnlinePlay}/Multiplayer/MultiplayerFilterControl.cs | 0 .../Multiplayer/MultiplayerLoungeSubScreen.cs | 0 .../Multiplayer/MultiplayerMatchSongSelect.cs | 0 .../Multiplayer/MultiplayerMatchSubScreen.cs | 0 .../{Multi => OnlinePlay}/Multiplayer/MultiplayerPlayer.cs | 0 .../{Multi => OnlinePlay}/Multiplayer/MultiplayerResultsScreen.cs | 0 .../{Multi => OnlinePlay}/Multiplayer/MultiplayerRoomComposite.cs | 0 .../{Multi => OnlinePlay}/Multiplayer/MultiplayerRoomManager.cs | 0 .../Multiplayer/Participants/ParticipantPanel.cs | 0 .../Multiplayer/Participants/ParticipantsList.cs | 0 .../Multiplayer/Participants/ParticipantsListHeader.cs | 0 .../Multiplayer/Participants/StateDisplay.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/MultiplayerComposite.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/MultiplayerScreen.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/MultiplayerSubScreen.cs | 0 .../Screens/{Multi => OnlinePlay}/MultiplayerSubScreenStack.cs | 0 .../{Multi => OnlinePlay}/Playlists/CreatePlaylistsRoomButton.cs | 0 osu.Game/Screens/{Multi => OnlinePlay}/Playlists/Playlists.cs | 0 .../{Multi => OnlinePlay}/Playlists/PlaylistsLoungeSubScreen.cs | 0 .../Playlists/PlaylistsMatchSettingsOverlay.cs | 0 .../Screens/{Multi => OnlinePlay}/Playlists/PlaylistsPlayer.cs | 0 .../{Multi => OnlinePlay}/Playlists/PlaylistsReadyButton.cs | 0 .../{Multi => OnlinePlay}/Playlists/PlaylistsResultsScreen.cs | 0 .../{Multi => OnlinePlay}/Playlists/PlaylistsRoomManager.cs | 0 .../{Multi => OnlinePlay}/Playlists/PlaylistsRoomSubScreen.cs | 0 78 files changed, 0 insertions(+), 0 deletions(-) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/BeatmapDetailAreaPlaylistTabItem.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/BeatmapTitle.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/BeatmapTypeInfo.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/DisableableTabControl.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/DrawableGameType.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/ListingPollingComponent.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/MatchBeatmapDetailArea.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/ModeTypeInfo.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/MultiplayerBackgroundSprite.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/OverlinedHeader.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/OverlinedPlaylistHeader.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/ParticipantCountDisplay.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/ParticipantsDisplay.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/ParticipantsList.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/ReadyButton.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/RoomManager.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/RoomPollingComponent.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/RoomStatusInfo.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/SelectionPollingComponent.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Components/StatusColouredContainer.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/DrawableRoomPlaylist.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/DrawableRoomPlaylistItem.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/DrawableRoomPlaylistWithResults.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Header.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/IMultiplayerSubScreen.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/IRoomManager.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Lounge/Components/DrawableRoom.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Lounge/Components/FilterControl.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Lounge/Components/FilterCriteria.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Lounge/Components/ParticipantInfo.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Lounge/Components/PlaylistsFilterControl.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Lounge/Components/RoomInfo.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Lounge/Components/RoomInspector.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Lounge/Components/RoomStatusFilter.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Lounge/Components/RoomsContainer.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Lounge/LoungeSubScreen.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/Footer.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/GameTypePicker.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/Header.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/MatchChatDisplay.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/MatchLeaderboard.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/MatchLeaderboardScore.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/MatchSettingsOverlay.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/PurpleTriangleButton.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Match/Components/RoomAvailabilityPicker.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Match/RoomSubScreen.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/CreateMultiplayerMatchButton.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Match/BeatmapSelectionControl.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Match/MultiplayerMatchFooter.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Match/MultiplayerMatchHeader.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Match/MultiplayerReadyButton.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Multiplayer.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/MultiplayerFilterControl.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/MultiplayerLoungeSubScreen.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/MultiplayerMatchSongSelect.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/MultiplayerMatchSubScreen.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/MultiplayerPlayer.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/MultiplayerResultsScreen.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/MultiplayerRoomComposite.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/MultiplayerRoomManager.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Participants/ParticipantPanel.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Participants/ParticipantsList.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Participants/ParticipantsListHeader.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Multiplayer/Participants/StateDisplay.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/MultiplayerComposite.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/MultiplayerScreen.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/MultiplayerSubScreen.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/MultiplayerSubScreenStack.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Playlists/CreatePlaylistsRoomButton.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Playlists/Playlists.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Playlists/PlaylistsLoungeSubScreen.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Playlists/PlaylistsMatchSettingsOverlay.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Playlists/PlaylistsPlayer.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Playlists/PlaylistsReadyButton.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Playlists/PlaylistsResultsScreen.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Playlists/PlaylistsRoomManager.cs (100%) rename osu.Game/Screens/{Multi => OnlinePlay}/Playlists/PlaylistsRoomSubScreen.cs (100%) diff --git a/osu.Game/Screens/Multi/Components/BeatmapDetailAreaPlaylistTabItem.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/BeatmapDetailAreaPlaylistTabItem.cs rename to osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs diff --git a/osu.Game/Screens/Multi/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/BeatmapTitle.cs rename to osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs diff --git a/osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/BeatmapTypeInfo.cs rename to osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs diff --git a/osu.Game/Screens/Multi/Components/DisableableTabControl.cs b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/DisableableTabControl.cs rename to osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs diff --git a/osu.Game/Screens/Multi/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/DrawableGameType.cs rename to osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs diff --git a/osu.Game/Screens/Multi/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/ListingPollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs diff --git a/osu.Game/Screens/Multi/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/MatchBeatmapDetailArea.cs rename to osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs diff --git a/osu.Game/Screens/Multi/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/ModeTypeInfo.cs rename to osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs diff --git a/osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/MultiplayerBackgroundSprite.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/MultiplayerBackgroundSprite.cs rename to osu.Game/Screens/OnlinePlay/Components/MultiplayerBackgroundSprite.cs diff --git a/osu.Game/Screens/Multi/Components/OverlinedHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/OverlinedHeader.cs rename to osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs diff --git a/osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/OverlinedPlaylistHeader.cs rename to osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs diff --git a/osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/ParticipantCountDisplay.cs rename to osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs diff --git a/osu.Game/Screens/Multi/Components/ParticipantsDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/ParticipantsDisplay.cs rename to osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs diff --git a/osu.Game/Screens/Multi/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/ParticipantsList.cs rename to osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs diff --git a/osu.Game/Screens/Multi/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/ReadyButton.cs rename to osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs diff --git a/osu.Game/Screens/Multi/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/RoomManager.cs rename to osu.Game/Screens/OnlinePlay/Components/RoomManager.cs diff --git a/osu.Game/Screens/Multi/Components/RoomPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/RoomPollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs diff --git a/osu.Game/Screens/Multi/Components/RoomStatusInfo.cs b/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/RoomStatusInfo.cs rename to osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs diff --git a/osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/SelectionPollingComponent.cs rename to osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs diff --git a/osu.Game/Screens/Multi/Components/StatusColouredContainer.cs b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs similarity index 100% rename from osu.Game/Screens/Multi/Components/StatusColouredContainer.cs rename to osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs similarity index 100% rename from osu.Game/Screens/Multi/DrawableRoomPlaylist.cs rename to osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs similarity index 100% rename from osu.Game/Screens/Multi/DrawableRoomPlaylistItem.cs rename to osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs diff --git a/osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs similarity index 100% rename from osu.Game/Screens/Multi/DrawableRoomPlaylistWithResults.cs rename to osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs diff --git a/osu.Game/Screens/Multi/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs similarity index 100% rename from osu.Game/Screens/Multi/Header.cs rename to osu.Game/Screens/OnlinePlay/Header.cs diff --git a/osu.Game/Screens/Multi/IMultiplayerSubScreen.cs b/osu.Game/Screens/OnlinePlay/IMultiplayerSubScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/IMultiplayerSubScreen.cs rename to osu.Game/Screens/OnlinePlay/IMultiplayerSubScreen.cs diff --git a/osu.Game/Screens/Multi/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs similarity index 100% rename from osu.Game/Screens/Multi/IRoomManager.cs rename to osu.Game/Screens/OnlinePlay/IRoomManager.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/Components/DrawableRoom.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/Components/FilterControl.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/Components/FilterCriteria.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/Components/ParticipantInfo.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/PlaylistsFilterControl.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/Components/PlaylistsFilterControl.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/Components/RoomInfo.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/Components/RoomInspector.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/Components/RoomStatusFilter.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs diff --git a/osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/Components/RoomsContainer.cs rename to osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs diff --git a/osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/Lounge/LoungeSubScreen.cs rename to osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs diff --git a/osu.Game/Screens/Multi/Match/Components/Footer.cs b/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs similarity index 100% rename from osu.Game/Screens/Multi/Match/Components/Footer.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs diff --git a/osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs b/osu.Game/Screens/OnlinePlay/Match/Components/GameTypePicker.cs similarity index 100% rename from osu.Game/Screens/Multi/Match/Components/GameTypePicker.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/GameTypePicker.cs diff --git a/osu.Game/Screens/Multi/Match/Components/Header.cs b/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs similarity index 100% rename from osu.Game/Screens/Multi/Match/Components/Header.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/Header.cs diff --git a/osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs similarity index 100% rename from osu.Game/Screens/Multi/Match/Components/MatchChatDisplay.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs similarity index 100% rename from osu.Game/Screens/Multi/Match/Components/MatchLeaderboard.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs diff --git a/osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs similarity index 100% rename from osu.Game/Screens/Multi/Match/Components/MatchLeaderboardScore.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs diff --git a/osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs similarity index 100% rename from osu.Game/Screens/Multi/Match/Components/MatchSettingsOverlay.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs diff --git a/osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs b/osu.Game/Screens/OnlinePlay/Match/Components/PurpleTriangleButton.cs similarity index 100% rename from osu.Game/Screens/Multi/Match/Components/PurpleTriangleButton.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/PurpleTriangleButton.cs diff --git a/osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomAvailabilityPicker.cs similarity index 100% rename from osu.Game/Screens/Multi/Match/Components/RoomAvailabilityPicker.cs rename to osu.Game/Screens/OnlinePlay/Match/Components/RoomAvailabilityPicker.cs diff --git a/osu.Game/Screens/Multi/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/Match/RoomSubScreen.cs rename to osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/CreateMultiplayerMatchButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/CreateMultiplayerMatchButton.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/Match/BeatmapSelectionControl.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchFooter.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchHeader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchHeader.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/Match/MultiplayerReadyButton.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/Multiplayer.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/MultiplayerFilterControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/MultiplayerFilterControl.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/MultiplayerLoungeSubScreen.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/MultiplayerMatchSongSelect.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/MultiplayerMatchSubScreen.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/MultiplayerPlayer.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/MultiplayerResultsScreen.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/MultiplayerRoomComposite.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/MultiplayerRoomManager.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantPanel.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantsList.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantsListHeader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/Participants/ParticipantsListHeader.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs diff --git a/osu.Game/Screens/Multi/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs similarity index 100% rename from osu.Game/Screens/Multi/Multiplayer/Participants/StateDisplay.cs rename to osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs diff --git a/osu.Game/Screens/Multi/MultiplayerComposite.cs b/osu.Game/Screens/OnlinePlay/MultiplayerComposite.cs similarity index 100% rename from osu.Game/Screens/Multi/MultiplayerComposite.cs rename to osu.Game/Screens/OnlinePlay/MultiplayerComposite.cs diff --git a/osu.Game/Screens/Multi/MultiplayerScreen.cs b/osu.Game/Screens/OnlinePlay/MultiplayerScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/MultiplayerScreen.cs rename to osu.Game/Screens/OnlinePlay/MultiplayerScreen.cs diff --git a/osu.Game/Screens/Multi/MultiplayerSubScreen.cs b/osu.Game/Screens/OnlinePlay/MultiplayerSubScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/MultiplayerSubScreen.cs rename to osu.Game/Screens/OnlinePlay/MultiplayerSubScreen.cs diff --git a/osu.Game/Screens/Multi/MultiplayerSubScreenStack.cs b/osu.Game/Screens/OnlinePlay/MultiplayerSubScreenStack.cs similarity index 100% rename from osu.Game/Screens/Multi/MultiplayerSubScreenStack.cs rename to osu.Game/Screens/OnlinePlay/MultiplayerSubScreenStack.cs diff --git a/osu.Game/Screens/Multi/Playlists/CreatePlaylistsRoomButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs similarity index 100% rename from osu.Game/Screens/Multi/Playlists/CreatePlaylistsRoomButton.cs rename to osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs diff --git a/osu.Game/Screens/Multi/Playlists/Playlists.cs b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs similarity index 100% rename from osu.Game/Screens/Multi/Playlists/Playlists.cs rename to osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/Playlists/PlaylistsLoungeSubScreen.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs similarity index 100% rename from osu.Game/Screens/Multi/Playlists/PlaylistsMatchSettingsOverlay.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs similarity index 100% rename from osu.Game/Screens/Multi/Playlists/PlaylistsPlayer.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsReadyButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs similarity index 100% rename from osu.Game/Screens/Multi/Playlists/PlaylistsReadyButton.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/Playlists/PlaylistsResultsScreen.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsRoomManager.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs similarity index 100% rename from osu.Game/Screens/Multi/Playlists/PlaylistsRoomManager.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs diff --git a/osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs similarity index 100% rename from osu.Game/Screens/Multi/Playlists/PlaylistsRoomSubScreen.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs From 83fb7c7a1ae3a0aac1e42ec8160c90d1aef5009e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 16:50:00 +0100 Subject: [PATCH 5694/6909] Re-namespace all files in OnlinePlay directory --- .../Visual/Multiplayer/RoomManagerTestScene.cs | 2 +- .../Visual/Multiplayer/TestRoomManager.cs | 2 +- .../Multiplayer/TestSceneDrawableRoomPlaylist.cs | 2 +- .../Visual/Multiplayer/TestSceneLoungeRoomInfo.cs | 2 +- .../Multiplayer/TestSceneLoungeRoomsContainer.cs | 2 +- .../Multiplayer/TestSceneMatchBeatmapDetailArea.cs | 2 +- .../Visual/Multiplayer/TestSceneMatchHeader.cs | 2 +- .../Multiplayer/TestSceneMatchLeaderboard.cs | 2 +- .../Visual/Multiplayer/TestSceneMatchSongSelect.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiHeader.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 4 ++-- .../TestSceneMultiplayerMatchSubScreen.cs | 4 ++-- .../TestSceneMultiplayerParticipantsList.cs | 2 +- .../Multiplayer/TestSceneMultiplayerReadyButton.cs | 2 +- .../Visual/Multiplayer/TestSceneRoomStatus.cs | 2 +- .../Visual/Navigation/TestSceneScreenNavigation.cs | 4 ++-- .../Playlists/TestScenePlaylistsFilterControl.cs | 2 +- .../Playlists/TestScenePlaylistsLoungeSubScreen.cs | 6 +++--- .../TestScenePlaylistsMatchSettingsOverlay.cs | 4 ++-- .../TestScenePlaylistsParticipantsList.cs | 2 +- .../Playlists/TestScenePlaylistsResultsScreen.cs | 2 +- .../Playlists/TestScenePlaylistsRoomSubScreen.cs | 6 +++--- .../Visual/Playlists/TestScenePlaylistsScreen.cs | 2 +- osu.Game/Online/Rooms/GetRoomsRequest.cs | 2 +- .../Overlays/Dashboard/CurrentlyPlayingDisplay.cs | 2 +- osu.Game/Screens/Menu/MainMenu.cs | 4 ++-- .../Components/BeatmapDetailAreaPlaylistTabItem.cs | 2 +- .../Screens/OnlinePlay/Components/BeatmapTitle.cs | 2 +- .../OnlinePlay/Components/BeatmapTypeInfo.cs | 2 +- .../OnlinePlay/Components/DisableableTabControl.cs | 2 +- .../OnlinePlay/Components/DrawableGameType.cs | 2 +- .../Components/ListingPollingComponent.cs | 4 ++-- .../Components/MatchBeatmapDetailArea.cs | 2 +- .../Screens/OnlinePlay/Components/ModeTypeInfo.cs | 2 +- .../Components/MultiplayerBackgroundSprite.cs | 2 +- .../OnlinePlay/Components/OverlinedHeader.cs | 2 +- .../Components/OverlinedPlaylistHeader.cs | 2 +- .../Components/ParticipantCountDisplay.cs | 2 +- .../OnlinePlay/Components/ParticipantsDisplay.cs | 2 +- .../OnlinePlay/Components/ParticipantsList.cs | 2 +- .../Screens/OnlinePlay/Components/ReadyButton.cs | 2 +- .../Screens/OnlinePlay/Components/RoomManager.cs | 2 +- .../OnlinePlay/Components/RoomPollingComponent.cs | 2 +- .../OnlinePlay/Components/RoomStatusInfo.cs | 2 +- .../Components/SelectionPollingComponent.cs | 2 +- .../Components/StatusColouredContainer.cs | 2 +- .../Screens/OnlinePlay/DrawableRoomPlaylist.cs | 2 +- .../Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- .../OnlinePlay/DrawableRoomPlaylistWithResults.cs | 2 +- osu.Game/Screens/OnlinePlay/Header.cs | 4 ++-- .../Screens/OnlinePlay/IMultiplayerSubScreen.cs | 2 +- osu.Game/Screens/OnlinePlay/IRoomManager.cs | 2 +- .../OnlinePlay/Lounge/Components/DrawableRoom.cs | 10 +++++----- .../OnlinePlay/Lounge/Components/FilterControl.cs | 2 +- .../OnlinePlay/Lounge/Components/FilterCriteria.cs | 2 +- .../Lounge/Components/ParticipantInfo.cs | 2 +- .../Lounge/Components/PlaylistsFilterControl.cs | 2 +- .../OnlinePlay/Lounge/Components/RoomInfo.cs | 4 ++-- .../OnlinePlay/Lounge/Components/RoomInspector.cs | 4 ++-- .../Lounge/Components/RoomStatusFilter.cs | 2 +- .../OnlinePlay/Lounge/Components/RoomsContainer.cs | 6 +++--- .../Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 6 +++--- .../Screens/OnlinePlay/Match/Components/Footer.cs | 4 ++-- .../OnlinePlay/Match/Components/GameTypePicker.cs | 4 ++-- .../Screens/OnlinePlay/Match/Components/Header.cs | 2 +- .../Match/Components/MatchChatDisplay.cs | 2 +- .../Match/Components/MatchLeaderboard.cs | 2 +- .../Match/Components/MatchLeaderboardScore.cs | 2 +- .../Match/Components/MatchSettingsOverlay.cs | 2 +- .../Match/Components/PurpleTriangleButton.cs | 2 +- .../Match/Components/RoomAvailabilityPicker.cs | 4 ++-- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- .../Multiplayer/CreateMultiplayerMatchButton.cs | 4 ++-- .../Multiplayer/Match/BeatmapSelectionControl.cs | 4 ++-- .../Multiplayer/Match/MultiplayerMatchFooter.cs | 2 +- .../Multiplayer/Match/MultiplayerMatchHeader.cs | 4 ++-- .../Match/MultiplayerMatchSettingsOverlay.cs | 4 ++-- .../Multiplayer/Match/MultiplayerReadyButton.cs | 4 ++-- .../Screens/OnlinePlay/Multiplayer/Multiplayer.cs | 6 +++--- .../Multiplayer/MultiplayerFilterControl.cs | 4 ++-- .../Multiplayer/MultiplayerLoungeSubScreen.cs | 8 ++++---- .../Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 14 +++++++------- .../OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 ++-- .../Multiplayer/MultiplayerResultsScreen.cs | 4 ++-- .../Multiplayer/MultiplayerRoomComposite.cs | 2 +- .../Multiplayer/MultiplayerRoomManager.cs | 4 ++-- .../Multiplayer/Participants/ParticipantPanel.cs | 2 +- .../Multiplayer/Participants/ParticipantsList.cs | 2 +- .../Participants/ParticipantsListHeader.cs | 4 ++-- .../Multiplayer/Participants/StateDisplay.cs | 2 +- .../Screens/OnlinePlay/MultiplayerComposite.cs | 2 +- osu.Game/Screens/OnlinePlay/MultiplayerScreen.cs | 10 +++++----- .../Screens/OnlinePlay/MultiplayerSubScreen.cs | 2 +- .../OnlinePlay/MultiplayerSubScreenStack.cs | 2 +- .../Playlists/CreatePlaylistsRoomButton.cs | 4 ++-- osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs | 8 ++++---- .../Playlists/PlaylistsLoungeSubScreen.cs | 8 ++++---- .../Playlists/PlaylistsMatchSettingsOverlay.cs | 4 ++-- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- .../OnlinePlay/Playlists/PlaylistsReadyButton.cs | 4 ++-- .../OnlinePlay/Playlists/PlaylistsResultsScreen.cs | 2 +- .../OnlinePlay/Playlists/PlaylistsRoomManager.cs | 4 ++-- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 10 +++++----- osu.Game/Screens/Play/Spectator.cs | 2 +- osu.Game/Screens/Select/MatchSongSelect.cs | 4 ++-- .../Visual/Multiplayer/MultiplayerTestScene.cs | 4 ++-- .../Multiplayer/TestMultiplayerRoomContainer.cs | 4 ++-- .../Multiplayer/TestMultiplayerRoomManager.cs | 4 ++-- 109 files changed, 176 insertions(+), 176 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs index a2c496a504..c665a57452 100644 --- a/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs +++ b/osu.Game.Tests/Visual/Multiplayer/RoomManagerTestScene.cs @@ -6,7 +6,7 @@ using osu.Framework.Allocation; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Screens.Multi; +using osu.Game.Screens.OnlinePlay; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs index 7d9d4a6542..1785c99784 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestRoomManager.cs @@ -4,7 +4,7 @@ using System; using osu.Framework.Bindables; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi; +using osu.Game.Screens.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 722e1a50ef..65c0cfd328 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -18,7 +18,7 @@ using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Multi; +using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Input; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs index f58e1114b8..9f24347ae9 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomInfo.cs @@ -5,7 +5,7 @@ using System; using NUnit.Framework; using osu.Framework.Graphics; using osu.Game.Online.Rooms.RoomStatuses; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 9b6a6de7c2..279dcfa584 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Catch; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; using osuTK.Graphics; using osuTK.Input; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs index 571330f50e..9ad9f2c883 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchBeatmapDetailArea.cs @@ -9,7 +9,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Tests.Beatmaps; using osuTK; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs index 61968dce46..7cdc6b1a7d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchHeader.cs @@ -6,7 +6,7 @@ using osu.Game.Beatmaps; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs index 71b8ad05ee..64eaf0556b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchLeaderboard.cs @@ -7,7 +7,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Online.API; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Users; using osuTK; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index 157597e800..e0fd7d9874 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -18,7 +18,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.Select; namespace osu.Game.Tests.Visual.Multiplayer diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs index 0ccd882d95..089de223fc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs @@ -5,7 +5,7 @@ using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Screens; -using osu.Game.Screens.Multi; +using osu.Game.Screens.OnlinePlay; namespace osu.Game.Tests.Visual.Multiplayer { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 5c07b9d0ea..2e39471dc0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using NUnit.Framework; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer @@ -39,7 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); } - private class TestMultiplayer : Screens.Multi.Multiplayer.Multiplayer + private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer { protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 6dc26dae57..8869718fd1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -7,8 +7,8 @@ using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Multi.Multiplayer; -using osu.Game.Screens.Multi.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Beatmaps; using osuTK.Input; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index ee2fa4ef5a..9181170bee 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi.Multiplayer.Participants; +using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Users; using osuTK; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index c8ebdb1b76..6b11613f1c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -12,7 +12,7 @@ using osu.Game.Beatmaps; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Screens.Multi.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Resources; using osu.Game.Users; using osuTK; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs index 7140050bd5..cec40635f3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneRoomStatus.cs @@ -6,7 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Tests.Visual.Multiplayer { diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index ac63c89183..8480e6eaaa 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -107,14 +107,14 @@ namespace osu.Game.Tests.Visual.Navigation [Test] public void TestExitMultiWithEscape() { - PushAndConfirm(() => new Screens.Multi.Playlists.Playlists()); + PushAndConfirm(() => new Screens.OnlinePlay.Playlists.Playlists()); exitViaEscapeAndConfirm(); } [Test] public void TestExitMultiWithBackButton() { - PushAndConfirm(() => new Screens.Multi.Playlists.Playlists()); + PushAndConfirm(() => new Screens.OnlinePlay.Playlists.Playlists()); exitViaBackButtonAndConfirm(); } diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs index 66992b27a2..40e191dd7e 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsFilterControl.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Tests.Visual.Playlists { diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 04555857f5..008c862cc3 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -8,9 +8,9 @@ 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; -using osu.Game.Screens.Multi.Playlists; +using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Visual.Multiplayer; namespace osu.Game.Tests.Visual.Playlists diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs index 4e75619f13..44a79b6598 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsMatchSettingsOverlay.cs @@ -10,8 +10,8 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Playlists; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Playlists; namespace osu.Game.Tests.Visual.Playlists { diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index 5a0f196df9..8dd81e02e2 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -3,7 +3,7 @@ using NUnit.Framework; using osu.Framework.Graphics; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Users; namespace osu.Game.Tests.Visual.Playlists diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index 97bb48c7b5..cdcded8f61 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -19,7 +19,7 @@ using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Playlists; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Ranking; using osu.Game.Tests.Beatmaps; using osu.Game.Users; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index 96ff93a145..a4c87d3ace 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -15,9 +15,9 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Multi.Playlists; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Beatmaps; using osu.Game.Users; using osuTK.Input; diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs index b780eb5347..e52f823f0b 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsScreen.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tests.Visual.Playlists public TestScenePlaylistsScreen() { - var multi = new Screens.Multi.Playlists.Playlists(); + var multi = new Screens.OnlinePlay.Playlists.Playlists(); AddStep("show", () => LoadScreen(multi)); AddUntilStep("wait for loaded", () => multi.IsLoaded); diff --git a/osu.Game/Online/Rooms/GetRoomsRequest.cs b/osu.Game/Online/Rooms/GetRoomsRequest.cs index 5084b8627f..e45365797a 100644 --- a/osu.Game/Online/Rooms/GetRoomsRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomsRequest.cs @@ -5,7 +5,7 @@ using System.Collections.Generic; using Humanizer; using osu.Framework.IO.Network; using osu.Game.Online.API; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Online.Rooms { diff --git a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs index d39a81f5e8..c89699f2ee 100644 --- a/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs +++ b/osu.Game/Overlays/Dashboard/CurrentlyPlayingDisplay.cs @@ -11,7 +11,7 @@ using osu.Framework.Screens; using osu.Game.Database; using osu.Game.Online.API; using osu.Game.Online.Spectator; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Play; using osu.Game.Users; using osuTK; diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index aeb58a75fc..9d5720ff34 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -17,8 +17,8 @@ using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; -using osu.Game.Screens.Multi.Multiplayer; -using osu.Game.Screens.Multi.Playlists; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Select; namespace osu.Game.Screens.Menu diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs index 3f2ab28f1a..fb927411e6 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapDetailAreaPlaylistTabItem.cs @@ -3,7 +3,7 @@ using osu.Game.Screens.Select; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class BeatmapDetailAreaPlaylistTabItem : BeatmapDetailAreaTabItem { diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index 9e7a59d7d2..bc355d18a9 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class BeatmapTitle : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs index ce3b612262..434d7b75ed 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osuTK; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class BeatmapTypeInfo : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs index 27b5aec4d3..bbc407e926 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DisableableTabControl.cs @@ -5,7 +5,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public abstract class DisableableTabControl : TabControl { diff --git a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs index 38af6d065e..c4dc2a2b8f 100644 --- a/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs +++ b/osu.Game/Screens/OnlinePlay/Components/DrawableGameType.cs @@ -10,7 +10,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class DrawableGameType : CircularContainer, IHasTooltip { diff --git a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs index edce5400d1..e50784fcbe 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ListingPollingComponent.cs @@ -5,9 +5,9 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { /// /// A that polls for the lounge listing. diff --git a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs index 6997840c92..b013cbafd8 100644 --- a/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs +++ b/osu.Game/Screens/OnlinePlay/Components/MatchBeatmapDetailArea.cs @@ -12,7 +12,7 @@ using osu.Game.Online.Rooms; using osu.Game.Screens.Select; using osuTK; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class MatchBeatmapDetailArea : BeatmapDetailArea { diff --git a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs index f07bd8c3b2..719afcdd33 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps.Drawables; using osuTK; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class ModeTypeInfo : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Components/MultiplayerBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/MultiplayerBackgroundSprite.cs index 2240e55e2f..45e2c553e7 100644 --- a/osu.Game/Screens/OnlinePlay/Components/MultiplayerBackgroundSprite.cs +++ b/osu.Game/Screens/OnlinePlay/Components/MultiplayerBackgroundSprite.cs @@ -6,7 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Beatmaps.Drawables; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class MultiplayerBackgroundSprite : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs index 7ec20c8cae..c78dfef592 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { /// /// A header used in the multiplayer interface which shows text / details beneath a line. diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs index ebebe8b660..45b822d20a 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedPlaylistHeader.cs @@ -3,7 +3,7 @@ using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class OverlinedPlaylistHeader : OverlinedHeader { diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs index 498eeb09b3..357974adfc 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class ParticipantCountDisplay : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs index 6ea4283379..5184f873f3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs @@ -6,7 +6,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Graphics.Containers; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class ParticipantsDisplay : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs index 7978b4eaab..b5019b4cdc 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs @@ -12,7 +12,7 @@ using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class ParticipantsList : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 68df30965d..08f89d8ed8 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public abstract class ReadyButton : TriangleButton { diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 7e0c8c4ec5..2ed259e2b8 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -15,7 +15,7 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public abstract class RoomManager : CompositeDrawable, IRoomManager { diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs index 1d10277d1c..b2ea3a05d6 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomPollingComponent.cs @@ -8,7 +8,7 @@ using osu.Game.Online; using osu.Game.Online.API; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public abstract class RoomPollingComponent : PollingComponent { diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs b/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs index 89021691f3..58cb25f30e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class RoomStatusInfo : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index 3050765931..0eec155060 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs @@ -7,7 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { /// /// A that polls for the currently-selected room. diff --git a/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs index 68c14eeb15..760de354dc 100644 --- a/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Components/StatusColouredContainer.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Components +namespace osu.Game.Screens.OnlinePlay.Components { public class StatusColouredContainer : Container { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs index 956d38a90e..a08d9edb34 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylist.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Online.Rooms; using osuTK; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public class DrawableRoomPlaylist : OsuRearrangeableListContainer { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index a2aeae154a..e3bce4029f 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -29,7 +29,7 @@ using osu.Game.Screens.Play.HUD; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public class DrawableRoomPlaylistItem : OsuRearrangeableListItem { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs index fa241a3c42..575f336e58 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistWithResults.cs @@ -13,7 +13,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public class DrawableRoomPlaylistWithResults : DrawableRoomPlaylist { diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs index 637d8bb52b..bffd744fdc 100644 --- a/osu.Game/Screens/OnlinePlay/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -10,13 +10,13 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Graphics; -using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; using osu.Game.Overlays; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public class Header : Container { diff --git a/osu.Game/Screens/OnlinePlay/IMultiplayerSubScreen.cs b/osu.Game/Screens/OnlinePlay/IMultiplayerSubScreen.cs index 31ee123f83..fc149cd2b2 100644 --- a/osu.Game/Screens/OnlinePlay/IMultiplayerSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/IMultiplayerSubScreen.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. -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public interface IMultiplayerSubScreen : IOsuScreen { diff --git a/osu.Game/Screens/OnlinePlay/IRoomManager.cs b/osu.Game/Screens/OnlinePlay/IRoomManager.cs index eee2a223a1..8ff02536f3 100644 --- a/osu.Game/Screens/OnlinePlay/IRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/IRoomManager.cs @@ -6,7 +6,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { [Cached(typeof(IRoomManager))] public interface IRoomManager diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 6e4d8b46ed..6d37a483a9 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -9,22 +9,22 @@ using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.UserInterface; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; -using osu.Game.Screens.Multi.Components; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; using osuTK; using osuTK.Graphics; -using osu.Framework.Graphics.Cursor; -using osu.Framework.Graphics.UserInterface; -using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class DrawableRoom : OsuClickableContainer, IStateful, IFilterable, IHasContextMenu { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs index 3712cbe33e..7fc1c670ca 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterControl.cs @@ -12,7 +12,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Rulesets; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public abstract class FilterControl : CompositeDrawable { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs index 7b04be86b1..488af5d4de 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/FilterCriteria.cs @@ -3,7 +3,7 @@ using osu.Game.Rulesets; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class FilterCriteria { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs index 4152a9a3b2..895c0e3eda 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Users.Drawables; using osuTK; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class ParticipantInfo : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs index 3c55c3c43f..a463742097 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/PlaylistsFilterControl.cs @@ -5,7 +5,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.UserInterface; using osu.Game.Graphics.UserInterface; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class PlaylistsFilterControl : FilterControl { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs index e6f6ce5ed2..8552d425aa 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs @@ -6,10 +6,10 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Containers; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osuTK; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class RoomInfo : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs index dfee278e87..4b1ec9ae89 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs @@ -7,10 +7,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Beatmaps; using osu.Game.Graphics; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class RoomInspector : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs index 9da938ac8b..0c8dc8832b 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomStatusFilter.cs @@ -3,7 +3,7 @@ using System.ComponentModel; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public enum RoomStatusFilter { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index fbd5f44a30..f70c33babe 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -13,13 +13,13 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; using osu.Framework.Threading; using osu.Game.Extensions; +using osu.Game.Graphics.Cursor; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; -using osuTK; -using osu.Game.Graphics.Cursor; using osu.Game.Online.Rooms; +using osuTK; -namespace osu.Game.Screens.Multi.Lounge.Components +namespace osu.Game.Screens.OnlinePlay.Lounge.Components { public class RoomsContainer : CompositeDrawable, IKeyBindingHandler { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index cbab79e2ab..cc56c11d32 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -12,11 +12,11 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Overlays; -using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.Match; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; -namespace osu.Game.Screens.Multi.Lounge +namespace osu.Game.Screens.OnlinePlay.Lounge { [Cached] public abstract class LoungeSubScreen : MultiplayerSubScreen diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs b/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs index 7074ceca38..5c27d78d50 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs @@ -10,10 +10,10 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Playlists; +using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class Footer : CompositeDrawable { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/GameTypePicker.cs b/osu.Game/Screens/OnlinePlay/Match/Components/GameTypePicker.cs index 23a3da6e38..cca1f84bbb 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/GameTypePicker.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/GameTypePicker.cs @@ -10,10 +10,10 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.GameTypes; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osuTK; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class GameTypePicker : DisableableTabControl { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs b/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs index 134a0b3f2e..df0dfc6ec1 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Users.Drawables; using osuTK; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class Header : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs index b790ad9be5..8800215c2e 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs @@ -6,7 +6,7 @@ using osu.Framework.Bindables; using osu.Game.Online.Chat; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class MatchChatDisplay : StandAloneChatDisplay { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index 8cc7b62f98..50869f42ff 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs @@ -10,7 +10,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Online.Rooms; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class MatchLeaderboard : Leaderboard { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs index 1fabdbb86a..e8f5b1e826 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboardScore.cs @@ -8,7 +8,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Leaderboards; using osu.Game.Scoring; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class MatchLeaderboardScore : LeaderboardScore { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs index 0bb56d0cdf..998ab889d6 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public abstract class MatchSettingsOverlay : FocusedOverlayContainer { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/PurpleTriangleButton.cs b/osu.Game/Screens/OnlinePlay/Match/Components/PurpleTriangleButton.cs index 1d93116d07..28e8961a9a 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/PurpleTriangleButton.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/PurpleTriangleButton.cs @@ -5,7 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Game.Graphics.UserInterface; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class PurpleTriangleButton : TriangleButton { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/RoomAvailabilityPicker.cs b/osu.Game/Screens/OnlinePlay/Match/Components/RoomAvailabilityPicker.cs index 2292826d55..677a5be0d9 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/RoomAvailabilityPicker.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/RoomAvailabilityPicker.cs @@ -11,11 +11,11 @@ using osu.Framework.Input.Events; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Match.Components +namespace osu.Game.Screens.OnlinePlay.Match.Components { public class RoomAvailabilityPicker : DisableableTabControl { diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index b626156852..2b0035c8bc 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -15,7 +15,7 @@ using osu.Game.Overlays; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; -namespace osu.Game.Screens.Multi.Match +namespace osu.Game.Screens.OnlinePlay.Match { [Cached(typeof(IPreviewTrackOwner))] public abstract class RoomSubScreen : MultiplayerSubScreen, IPreviewTrackOwner diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs index b8b3f15fca..163efd9c20 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs @@ -4,9 +4,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; -namespace osu.Game.Screens.Multi.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class CreateMultiplayerMatchButton : PurpleTriangleButton { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index dfb6feeaf9..1718ebd83a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -9,9 +9,9 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Online.API; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; -namespace osu.Game.Screens.Multi.Multiplayer.Match +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class BeatmapSelectionControl : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index 145ae18817..a52f62fe00 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics; using osu.Game.Online.Rooms; using osuTK; -namespace osu.Game.Screens.Multi.Multiplayer.Match +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerMatchFooter : CompositeDrawable { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs index 0c0e580a3e..42e34c4be3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs @@ -10,14 +10,14 @@ using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Online.API; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Users.Drawables; using osuTK; using FontWeight = osu.Game.Graphics.FontWeight; using OsuColour = osu.Game.Graphics.OsuColour; using OsuFont = osu.Game.Graphics.OsuFont; -namespace osu.Game.Screens.Multi.Multiplayer.Match +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerMatchHeader : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 4a5b5fd181..8741b0323d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -18,10 +18,10 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; -namespace osu.Game.Screens.Multi.Multiplayer.Match +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerMatchSettingsOverlay : MatchSettingsOverlay { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index cea1eeecbb..15d6ef8aff 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -15,10 +15,10 @@ using osu.Game.Graphics.Backgrounds; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; using osuTK; -namespace osu.Game.Screens.Multi.Multiplayer.Match +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerReadyButton : MultiplayerRoomComposite { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 76aa6b9f8f..ce4918dae1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -8,10 +8,10 @@ using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Components; -using osu.Game.Screens.Multi.Lounge; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Lounge; -namespace osu.Game.Screens.Multi.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class Multiplayer : MultiplayerScreen { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs index bebad1944e..37e0fd109a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerFilterControl.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.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Lounge.Components; -namespace osu.Game.Screens.Multi.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerFilterControl : FilterControl { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs index ffc81efe3c..0a9a3f680f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerLoungeSubScreen.cs @@ -5,11 +5,11 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Lounge; -using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.Match; +using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match; -namespace osu.Game.Screens.Multi.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerLoungeSubScreen : LoungeSubScreen { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index ed1321d4e2..76869300e8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -13,7 +13,7 @@ using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Screens.Select; -namespace osu.Game.Screens.Multi.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerMatchSongSelect : SongSelect, IMultiplayerSubScreen { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4e371d4ed4..58314c3774 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -11,15 +11,15 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Components; -using osu.Game.Screens.Multi.Match; -using osu.Game.Screens.Multi.Match.Components; -using osu.Game.Screens.Multi.Multiplayer.Match; -using osu.Game.Screens.Multi.Multiplayer.Participants; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Match; +using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer.Match; +using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Users; -using ParticipantsList = osu.Game.Screens.Multi.Multiplayer.Participants.ParticipantsList; +using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.ParticipantsList; -namespace osu.Game.Screens.Multi.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Multiplayer { [Cached] public class MultiplayerMatchSubScreen : RoomSubScreen diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 32d95aa11f..4247e954bd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -11,13 +11,13 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Playlists; +using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Screens.Play; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Ranking; using osuTK; -namespace osu.Game.Screens.Multi.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Multiplayer { // Todo: The "room" part of PlaylistsPlayer should be split out into an abstract player class to be inherited instead. public class MultiplayerPlayer : PlaylistsPlayer diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs index 3e39473954..e3b47b3254 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -3,9 +3,9 @@ using osu.Game.Online.Rooms; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Playlists; +using osu.Game.Screens.OnlinePlay.Playlists; -namespace osu.Game.Screens.Multi.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerResultsScreen : PlaylistsResultsScreen { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs index 6fe7c10bfb..654dafe9aa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs @@ -5,7 +5,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Online.Multiplayer; -namespace osu.Game.Screens.Multi.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Multiplayer { public abstract class MultiplayerRoomComposite : MultiplayerComposite { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index fdd46cdc2a..3cb263298f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -13,9 +13,9 @@ using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; -namespace osu.Game.Screens.Multi.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerRoomManager : RoomManager { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 93f8a9de8b..044afa7445 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -22,7 +22,7 @@ using osu.Game.Users.Drawables; using osuTK; using osuTK.Graphics; -namespace osu.Game.Screens.Multi.Multiplayer.Participants +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { public class ParticipantPanel : MultiplayerRoomComposite, IHasContextMenu { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs index 3f37274d4c..b9ac096c4a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Cursor; using osuTK; -namespace osu.Game.Screens.Multi.Multiplayer.Participants +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { public class ParticipantsList : MultiplayerRoomComposite { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs index dc3ceedfd7..6c1a55a0eb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsListHeader.cs @@ -3,9 +3,9 @@ using osu.Framework.Allocation; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; -namespace osu.Game.Screens.Multi.Multiplayer.Participants +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { public class ParticipantsListHeader : OverlinedHeader { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index 61faa0d85d..8d2879fc93 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs @@ -11,7 +11,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Online.Multiplayer; using osuTK; -namespace osu.Game.Screens.Multi.Multiplayer.Participants +namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { public class StateDisplay : CompositeDrawable { diff --git a/osu.Game/Screens/OnlinePlay/MultiplayerComposite.cs b/osu.Game/Screens/OnlinePlay/MultiplayerComposite.cs index fe4ca759b0..ab54178ab4 100644 --- a/osu.Game/Screens/OnlinePlay/MultiplayerComposite.cs +++ b/osu.Game/Screens/OnlinePlay/MultiplayerComposite.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Game.Online.Rooms; using osu.Game.Users; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public class MultiplayerComposite : CompositeDrawable { diff --git a/osu.Game/Screens/OnlinePlay/MultiplayerScreen.cs b/osu.Game/Screens/OnlinePlay/MultiplayerScreen.cs index 983c7aeac9..3693e897ae 100644 --- a/osu.Game/Screens/OnlinePlay/MultiplayerScreen.cs +++ b/osu.Game/Screens/OnlinePlay/MultiplayerScreen.cs @@ -17,14 +17,14 @@ using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Screens.Menu; -using osu.Game.Screens.Multi.Components; -using osu.Game.Screens.Multi.Lounge; -using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.Match; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; using osuTK; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { [Cached] public abstract class MultiplayerScreen : OsuScreen diff --git a/osu.Game/Screens/OnlinePlay/MultiplayerSubScreen.cs b/osu.Game/Screens/OnlinePlay/MultiplayerSubScreen.cs index 8e46de1a95..d7fca98ebe 100644 --- a/osu.Game/Screens/OnlinePlay/MultiplayerSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/MultiplayerSubScreen.cs @@ -5,7 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Screens; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public abstract class MultiplayerSubScreen : OsuScreen, IMultiplayerSubScreen { diff --git a/osu.Game/Screens/OnlinePlay/MultiplayerSubScreenStack.cs b/osu.Game/Screens/OnlinePlay/MultiplayerSubScreenStack.cs index 3b0ed0dba1..335da86a65 100644 --- a/osu.Game/Screens/OnlinePlay/MultiplayerSubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/MultiplayerSubScreenStack.cs @@ -3,7 +3,7 @@ using osu.Framework.Screens; -namespace osu.Game.Screens.Multi +namespace osu.Game.Screens.OnlinePlay { public class MultiplayerSubScreenStack : OsuScreenStack { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs index acee063115..fcb773f8be 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/CreatePlaylistsRoomButton.cs @@ -2,9 +2,9 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; -namespace osu.Game.Screens.Multi.Playlists +namespace osu.Game.Screens.OnlinePlay.Playlists { public class CreatePlaylistsRoomButton : PurpleTriangleButton { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs index da6aade942..a7fb391fbc 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs @@ -5,11 +5,11 @@ using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Components; -using osu.Game.Screens.Multi.Lounge; -using osu.Game.Screens.Multi.Match; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Match; -namespace osu.Game.Screens.Multi.Playlists +namespace osu.Game.Screens.OnlinePlay.Playlists { public class Playlists : MultiplayerScreen { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs index 513854515b..bfbff4240c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsLoungeSubScreen.cs @@ -2,11 +2,11 @@ // See the LICENCE file in the repository root for full licence text. using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Lounge; -using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.Match; +using osu.Game.Screens.OnlinePlay.Lounge; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Match; -namespace osu.Game.Screens.Multi.Playlists +namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsLoungeSubScreen : LoungeSubScreen { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index f3109a33e4..557f1df657 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -16,10 +16,10 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Overlays; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osuTK; -namespace osu.Game.Screens.Multi.Playlists +namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsMatchSettingsOverlay : MatchSettingsOverlay { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 25a3340ead..2c3e7a12e2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -17,7 +17,7 @@ using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; -namespace osu.Game.Screens.Multi.Playlists +namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsPlayer : Player { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs index 3c35e2a6f3..edee8e571a 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs @@ -6,9 +6,9 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Graphics; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; -namespace osu.Game.Screens.Multi.Playlists +namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsReadyButton : ReadyButton { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs index 5d1d3a2724..e13c8a9f82 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs @@ -15,7 +15,7 @@ using osu.Game.Online.Rooms; using osu.Game.Scoring; using osu.Game.Screens.Ranking; -namespace osu.Game.Screens.Multi.Playlists +namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsResultsScreen : ResultsScreen { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs index ae57eeddcc..c55d1c3e94 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomManager.cs @@ -3,9 +3,9 @@ using System.Collections.Generic; using osu.Framework.Bindables; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay.Components; -namespace osu.Game.Screens.Multi.Playlists +namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsRoomManager : RoomManager { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 4867268fe1..51a9ae569e 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -10,14 +10,14 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Online.API; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi.Components; -using osu.Game.Screens.Multi.Match; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.OnlinePlay.Match; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.Select; using osu.Game.Users; -using Footer = osu.Game.Screens.Multi.Match.Components.Footer; +using Footer = osu.Game.Screens.OnlinePlay.Match.Components.Footer; -namespace osu.Game.Screens.Multi.Playlists +namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsRoomSubScreen : RoomSubScreen { diff --git a/osu.Game/Screens/Play/Spectator.cs b/osu.Game/Screens/Play/Spectator.cs index 71ce157296..28311f5113 100644 --- a/osu.Game/Screens/Play/Spectator.cs +++ b/osu.Game/Screens/Play/Spectator.cs @@ -30,7 +30,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Replays; using osu.Game.Rulesets.Replays.Types; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Match.Components; +using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Users; using osuTK; diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 80fd5c2067..1b89a58b40 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -10,8 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; -using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Components; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Components; namespace osu.Game.Screens.Select { diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 0e23f4d8c6..da0e39d965 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -7,8 +7,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Tests.Visual.Multiplayer { diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs index 0df397d98e..ad3e2f7105 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs @@ -6,8 +6,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; -using osu.Game.Screens.Multi; -using osu.Game.Screens.Multi.Lounge.Components; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Lounge.Components; namespace osu.Game.Tests.Visual.Multiplayer { diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index abfefd363a..5e12156f3c 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -11,8 +11,8 @@ using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; -using osu.Game.Screens.Multi.Lounge.Components; -using osu.Game.Screens.Multi.Multiplayer; +using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Screens.OnlinePlay.Multiplayer; namespace osu.Game.Tests.Visual.Multiplayer { From 4caf75850b4faf18d62827fcb4e9b1b50f4f1b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 17:00:00 +0100 Subject: [PATCH 5695/6909] Rename {Multiplayer -> OnlinePlay}Screen --- .../Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 4 ++-- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 ++-- osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs | 2 +- .../OnlinePlay/{MultiplayerScreen.cs => OnlinePlayScreen.cs} | 4 ++-- osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs | 2 +- .../Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 5 +---- 6 files changed, 9 insertions(+), 12 deletions(-) rename osu.Game/Screens/OnlinePlay/{MultiplayerScreen.cs => OnlinePlayScreen.cs} (99%) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index 6d37a483a9..f4d167a193 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components private CachedModelDependencyContainer dependencies; [Resolved(canBeNull: true)] - private MultiplayerScreen multiplayer { get; set; } + private OnlinePlayScreen parentScreen { get; set; } [Resolved] private BeatmapManager beatmaps { get; set; } @@ -242,7 +242,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { new OsuMenuItem("Create copy", MenuItemType.Standard, () => { - multiplayer?.OpenNewRoom(Room.CreateCopy()); + parentScreen?.OpenNewRoom(Room.CreateCopy()); }) }; } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 2b0035c8bc..3a5af90824 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Match private BeatmapManager beatmapManager { get; set; } [Resolved(canBeNull: true)] - protected MultiplayerScreen Multiplayer { get; private set; } + protected OnlinePlayScreen ParentScreen { get; private set; } private IBindable> managerUpdated; @@ -88,7 +88,7 @@ namespace osu.Game.Screens.OnlinePlay.Match protected void StartPlay(Func player) { sampleStart?.Play(); - Multiplayer?.Push(new PlayerLoader(player)); + ParentScreen?.Push(new PlayerLoader(player)); } private void selectedItemChanged() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index ce4918dae1..76f5c74433 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -13,7 +13,7 @@ using osu.Game.Screens.OnlinePlay.Lounge; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public class Multiplayer : MultiplayerScreen + public class Multiplayer : OnlinePlayScreen { [Resolved] private StatefulMultiplayerClient client { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/MultiplayerScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs similarity index 99% rename from osu.Game/Screens/OnlinePlay/MultiplayerScreen.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 3693e897ae..08741ef4fe 100644 --- a/osu.Game/Screens/OnlinePlay/MultiplayerScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -27,7 +27,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay { [Cached] - public abstract class MultiplayerScreen : OsuScreen + public abstract class OnlinePlayScreen : OsuScreen { public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true; @@ -67,7 +67,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly Drawable header; private readonly Drawable headerBackground; - protected MultiplayerScreen() + protected OnlinePlayScreen() { Anchor = Anchor.Centre; Origin = Anchor.Centre; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs index a7fb391fbc..5b132c97fd 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/Playlists.cs @@ -11,7 +11,7 @@ using osu.Game.Screens.OnlinePlay.Match; namespace osu.Game.Screens.OnlinePlay.Playlists { - public class Playlists : MultiplayerScreen + public class Playlists : OnlinePlayScreen { protected override void UpdatePollingRate(bool isIdle) { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 51a9ae569e..e76ca995bf 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -28,9 +28,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved(typeof(Room), nameof(Room.RoomID))] private Bindable roomId { get; set; } - [Resolved(canBeNull: true)] - private MultiplayerScreen multiplayer { get; set; } - private MatchSettingsOverlay settingsOverlay; private MatchLeaderboard leaderboard; @@ -124,7 +121,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RequestShowResults = item => { Debug.Assert(roomId.Value != null); - multiplayer?.Push(new PlaylistsResultsScreen(null, roomId.Value.Value, item, false)); + ParentScreen?.Push(new PlaylistsResultsScreen(null, roomId.Value.Value, item, false)); } } }, From eb0f125fefb5691e82c31a1730db1da323a0a095 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 17:00:31 +0100 Subject: [PATCH 5696/6909] Rename {Multiplayer -> OnlinePlay}SubScreenStack --- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 2 +- ...MultiplayerSubScreenStack.cs => OnlinePlaySubScreenStack.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game/Screens/OnlinePlay/{MultiplayerSubScreenStack.cs => OnlinePlaySubScreenStack.cs} (93%) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 08741ef4fe..66309bd47e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -124,7 +124,7 @@ namespace osu.Game.Screens.OnlinePlay } } }, - screenStack = new MultiplayerSubScreenStack { RelativeSizeAxes = Axes.Both } + screenStack = new OnlinePlaySubScreenStack { RelativeSizeAxes = Axes.Both } } }, new Header(ScreenTitle, screenStack), diff --git a/osu.Game/Screens/OnlinePlay/MultiplayerSubScreenStack.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs similarity index 93% rename from osu.Game/Screens/OnlinePlay/MultiplayerSubScreenStack.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs index 335da86a65..7f2a0980c1 100644 --- a/osu.Game/Screens/OnlinePlay/MultiplayerSubScreenStack.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreenStack.cs @@ -5,7 +5,7 @@ using osu.Framework.Screens; namespace osu.Game.Screens.OnlinePlay { - public class MultiplayerSubScreenStack : OsuScreenStack + public class OnlinePlaySubScreenStack : OsuScreenStack { protected override void ScreenChanged(IScreen prev, IScreen next) { From e5064ee930c58a01fcf79807c14aaf83a7e420d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 17:02:35 +0100 Subject: [PATCH 5697/6909] Rename {Multiplayer -> OnlinePlay}SubScreen --- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 2 +- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 8 ++++---- .../{MultiplayerSubScreen.cs => OnlinePlaySubScreen.cs} | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game/Screens/OnlinePlay/{MultiplayerSubScreen.cs => OnlinePlaySubScreen.cs} (94%) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index cc56c11d32..79f5dfdee1 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -19,7 +19,7 @@ using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay.Lounge { [Cached] - public abstract class LoungeSubScreen : MultiplayerSubScreen + public abstract class LoungeSubScreen : OnlinePlaySubScreen { public override string Title => "Lounge"; diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 3a5af90824..2449563c73 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -18,7 +18,7 @@ using osu.Game.Screens.Play; namespace osu.Game.Screens.OnlinePlay.Match { [Cached(typeof(IPreviewTrackOwner))] - public abstract class RoomSubScreen : MultiplayerSubScreen, IPreviewTrackOwner + public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner { protected readonly Bindable SelectedItem = new Bindable(); diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 66309bd47e..60897e8b4c 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -295,13 +295,13 @@ namespace osu.Game.Screens.OnlinePlay switch (newScreen) { case LoungeSubScreen _: - header.Delay(MultiplayerSubScreen.RESUME_TRANSITION_DELAY).ResizeHeightTo(400, MultiplayerSubScreen.APPEAR_DURATION, Easing.OutQuint); - headerBackground.MoveToX(0, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint); + header.Delay(OnlinePlaySubScreen.RESUME_TRANSITION_DELAY).ResizeHeightTo(400, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint); + headerBackground.MoveToX(0, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint); break; case RoomSubScreen _: - header.ResizeHeightTo(135, MultiplayerSubScreen.APPEAR_DURATION, Easing.OutQuint); - headerBackground.MoveToX(-MultiplayerSubScreen.X_SHIFT, MultiplayerSubScreen.X_MOVE_DURATION, Easing.OutQuint); + header.ResizeHeightTo(135, OnlinePlaySubScreen.APPEAR_DURATION, Easing.OutQuint); + headerBackground.MoveToX(-OnlinePlaySubScreen.X_SHIFT, OnlinePlaySubScreen.X_MOVE_DURATION, Easing.OutQuint); break; } diff --git a/osu.Game/Screens/OnlinePlay/MultiplayerSubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs similarity index 94% rename from osu.Game/Screens/OnlinePlay/MultiplayerSubScreen.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index d7fca98ebe..b6dcfe6dab 100644 --- a/osu.Game/Screens/OnlinePlay/MultiplayerSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -7,7 +7,7 @@ using osu.Framework.Screens; namespace osu.Game.Screens.OnlinePlay { - public abstract class MultiplayerSubScreen : OsuScreen, IMultiplayerSubScreen + public abstract class OnlinePlaySubScreen : OsuScreen, IMultiplayerSubScreen { public override bool DisallowExternalBeatmapRulesetChanges => false; @@ -16,7 +16,7 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(CanBeNull = true)] protected IRoomManager RoomManager { get; private set; } - protected MultiplayerSubScreen() + protected OnlinePlaySubScreen() { Anchor = Anchor.Centre; Origin = Anchor.Centre; From 4c43a67b68a9b0d1c574f33a4aa4e739e80397e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 17:05:29 +0100 Subject: [PATCH 5698/6909] Rename I{Multiplayer -> OnlinePlay}SubScreen --- osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs | 8 ++++---- osu.Game/Screens/OnlinePlay/Header.cs | 6 +++--- .../{IMultiplayerSubScreen.cs => IOnlinePlaySubScreen.cs} | 2 +- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 4 ++-- osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs | 2 +- osu.Game/Screens/Select/MatchSongSelect.cs | 2 +- 7 files changed, 13 insertions(+), 13 deletions(-) rename osu.Game/Screens/OnlinePlay/{IMultiplayerSubScreen.cs => IOnlinePlaySubScreen.cs} (82%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs index 089de223fc..2244dcfc56 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiHeader.cs @@ -18,7 +18,7 @@ namespace osu.Game.Tests.Visual.Multiplayer OsuScreenStack screenStack = new OsuScreenStack { RelativeSizeAxes = Axes.Both }; - screenStack.Push(new TestMultiplayerSubScreen(index)); + screenStack.Push(new TestOnlinePlaySubScreen(index)); Children = new Drawable[] { @@ -26,16 +26,16 @@ namespace osu.Game.Tests.Visual.Multiplayer new Header("Multiplayer", screenStack) }; - AddStep("push multi screen", () => screenStack.CurrentScreen.Push(new TestMultiplayerSubScreen(++index))); + AddStep("push multi screen", () => screenStack.CurrentScreen.Push(new TestOnlinePlaySubScreen(++index))); } - private class TestMultiplayerSubScreen : OsuScreen, IMultiplayerSubScreen + private class TestOnlinePlaySubScreen : OsuScreen, IOnlinePlaySubScreen { private readonly int index; public string ShortTitle => $"Screen {index}"; - public TestMultiplayerSubScreen(int index) + public TestOnlinePlaySubScreen(int index) { this.index = index; } diff --git a/osu.Game/Screens/OnlinePlay/Header.cs b/osu.Game/Screens/OnlinePlay/Header.cs index bffd744fdc..bf0a53cbb6 100644 --- a/osu.Game/Screens/OnlinePlay/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Header.cs @@ -61,8 +61,8 @@ namespace osu.Game.Screens.OnlinePlay breadcrumbs.Current.ValueChanged += screen => { - if (screen.NewValue is IMultiplayerSubScreen multiScreen) - title.Screen = multiScreen; + if (screen.NewValue is IOnlinePlaySubScreen onlineSubScreen) + title.Screen = onlineSubScreen; }; breadcrumbs.Current.TriggerChange(); @@ -75,7 +75,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly OsuSpriteText dot; private readonly OsuSpriteText pageTitle; - public IMultiplayerSubScreen Screen + public IOnlinePlaySubScreen Screen { set => pageTitle.Text = value.ShortTitle.Titleize(); } diff --git a/osu.Game/Screens/OnlinePlay/IMultiplayerSubScreen.cs b/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs similarity index 82% rename from osu.Game/Screens/OnlinePlay/IMultiplayerSubScreen.cs rename to osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs index fc149cd2b2..a4762292a9 100644 --- a/osu.Game/Screens/OnlinePlay/IMultiplayerSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/IOnlinePlaySubScreen.cs @@ -3,7 +3,7 @@ namespace osu.Game.Screens.OnlinePlay { - public interface IMultiplayerSubScreen : IOsuScreen + public interface IOnlinePlaySubScreen : IOsuScreen { string Title { get; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 76869300e8..0842574f54 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -15,7 +15,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public class MultiplayerMatchSongSelect : SongSelect, IMultiplayerSubScreen + public class MultiplayerMatchSongSelect : SongSelect, IOnlinePlaySubScreen { public string ShortTitle => "song selection"; diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 60897e8b4c..9ba2e41d12 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay [Cached] public abstract class OnlinePlayScreen : OsuScreen { - public override bool CursorVisible => (screenStack.CurrentScreen as IMultiplayerSubScreen)?.CursorVisible ?? true; + public override bool CursorVisible => (screenStack.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; // this is required due to PlayerLoader eventually being pushed to the main stack // while leases may be taken out by a subscreen. @@ -245,7 +245,7 @@ namespace osu.Game.Screens.OnlinePlay public override bool OnBackButton() { - if ((screenStack.CurrentScreen as IMultiplayerSubScreen)?.OnBackButton() == true) + if ((screenStack.CurrentScreen as IOnlinePlaySubScreen)?.OnBackButton() == true) return true; if (screenStack.CurrentScreen != null && !(screenStack.CurrentScreen is LoungeSubScreen)) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs index b6dcfe6dab..e1bd889088 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySubScreen.cs @@ -7,7 +7,7 @@ using osu.Framework.Screens; namespace osu.Game.Screens.OnlinePlay { - public abstract class OnlinePlaySubScreen : OsuScreen, IMultiplayerSubScreen + public abstract class OnlinePlaySubScreen : OsuScreen, IOnlinePlaySubScreen { public override bool DisallowExternalBeatmapRulesetChanges => false; diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 1b89a58b40..0948a4d19a 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -15,7 +15,7 @@ using osu.Game.Screens.OnlinePlay.Components; namespace osu.Game.Screens.Select { - public class MatchSongSelect : SongSelect, IMultiplayerSubScreen + public class MatchSongSelect : SongSelect, IOnlinePlaySubScreen { public Action Selected; From ed4b8482b60d1133753296aebf78afd4fec95c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 17:12:33 +0100 Subject: [PATCH 5699/6909] Rename {Multiplayer -> OnlinePlay}Composite --- osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs | 2 +- osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs | 2 +- osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs | 2 +- osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs | 2 +- .../Screens/OnlinePlay/Components/ParticipantCountDisplay.cs | 2 +- osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs | 2 +- osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs | 2 +- osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs | 2 +- osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs | 2 +- .../Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs | 2 +- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs | 2 +- osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs | 2 +- osu.Game/Screens/OnlinePlay/Match/Components/Header.cs | 2 +- .../Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs | 2 +- .../OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs | 2 +- .../OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs | 2 +- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 2 +- .../Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs | 2 +- .../{MultiplayerComposite.cs => OnlinePlayComposite.cs} | 2 +- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 2 +- .../OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs | 2 +- 21 files changed, 21 insertions(+), 21 deletions(-) rename osu.Game/Screens/OnlinePlay/{MultiplayerComposite.cs => OnlinePlayComposite.cs} (96%) diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index bc355d18a9..acb82360b3 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -12,7 +12,7 @@ using osu.Game.Online.Chat; namespace osu.Game.Screens.OnlinePlay.Components { - public class BeatmapTitle : MultiplayerComposite + public class BeatmapTitle : OnlinePlayComposite { private readonly LinkFlowContainer textFlow; diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs index 434d7b75ed..3aa13458a4 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTypeInfo.cs @@ -11,7 +11,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Components { - public class BeatmapTypeInfo : MultiplayerComposite + public class BeatmapTypeInfo : OnlinePlayComposite { private LinkFlowContainer beatmapAuthor; diff --git a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs index 719afcdd33..03b27b605c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs @@ -10,7 +10,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Components { - public class ModeTypeInfo : MultiplayerComposite + public class ModeTypeInfo : OnlinePlayComposite { private const float height = 30; private const float transition_duration = 100; diff --git a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs index c78dfef592..08a0a3405e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OverlinedHeader.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.OnlinePlay.Components /// /// A header used in the multiplayer interface which shows text / details beneath a line. /// - public class OverlinedHeader : MultiplayerComposite + public class OverlinedHeader : OnlinePlayComposite { private bool showLine = true; diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs index 357974adfc..53821da8fd 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantCountDisplay.cs @@ -9,7 +9,7 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Screens.OnlinePlay.Components { - public class ParticipantCountDisplay : MultiplayerComposite + public class ParticipantCountDisplay : OnlinePlayComposite { private const float text_size = 30; private const float transition_duration = 100; diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs index 5184f873f3..c36d1a2e76 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsDisplay.cs @@ -8,7 +8,7 @@ using osu.Game.Graphics.Containers; namespace osu.Game.Screens.OnlinePlay.Components { - public class ParticipantsDisplay : MultiplayerComposite + public class ParticipantsDisplay : OnlinePlayComposite { public Bindable Details = new Bindable(); diff --git a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs index b5019b4cdc..9aceb39a27 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ParticipantsList.cs @@ -14,7 +14,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Components { - public class ParticipantsList : MultiplayerComposite + public class ParticipantsList : OnlinePlayComposite { public const float TILE_SIZE = 35; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs b/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs index 58cb25f30e..bcc256bcff 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomStatusInfo.cs @@ -13,7 +13,7 @@ using osu.Game.Online.Rooms.RoomStatuses; namespace osu.Game.Screens.OnlinePlay.Components { - public class RoomStatusInfo : MultiplayerComposite + public class RoomStatusInfo : OnlinePlayComposite { public RoomStatusInfo() { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs index f4d167a193..0a7198a7fa 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/DrawableRoom.cs @@ -155,7 +155,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components Width = cover_width, Masking = true, Margin = new MarginPadding { Left = stripWidth }, - Child = new MultiplayerBackgroundSprite(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both } + Child = new OnlinePlayBackgroundSprite(BeatmapSetCoverType.List) { RelativeSizeAxes = Axes.Both } }, new Container { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs index 895c0e3eda..0d5ce65d5a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs @@ -13,7 +13,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class ParticipantInfo : MultiplayerComposite + public class ParticipantInfo : OnlinePlayComposite { public ParticipantInfo() { diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs index 8552d425aa..0a17702f2a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs @@ -11,7 +11,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class RoomInfo : MultiplayerComposite + public class RoomInfo : OnlinePlayComposite { private readonly List statusElements = new List(); private readonly OsuTextFlowContainer roomName; diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs index 4b1ec9ae89..c28354c753 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInspector.cs @@ -12,7 +12,7 @@ using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Lounge.Components { - public class RoomInspector : MultiplayerComposite + public class RoomInspector : OnlinePlayComposite { private const float transition_duration = 100; diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs b/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs index df0dfc6ec1..a2d11c54c1 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/Header.cs @@ -12,7 +12,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Match.Components { - public class Header : MultiplayerComposite + public class Header : OnlinePlayComposite { public const float HEIGHT = 50; diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs index 998ab889d6..ea3951fc3b 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchSettingsOverlay.cs @@ -17,7 +17,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components protected const float TRANSITION_DURATION = 350; protected const float FIELD_PADDING = 45; - protected MultiplayerComposite Settings { get; set; } + protected OnlinePlayComposite Settings { get; set; } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index 1718ebd83a..f17e04d4d4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -13,7 +13,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class BeatmapSelectionControl : MultiplayerComposite + public class BeatmapSelectionControl : OnlinePlayComposite { [Resolved] private MultiplayerMatchSubScreen matchSubScreen { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs index 42e34c4be3..bb351d06d3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchHeader.cs @@ -19,7 +19,7 @@ using OsuFont = osu.Game.Graphics.OsuFont; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class MultiplayerMatchHeader : MultiplayerComposite + public class MultiplayerMatchHeader : OnlinePlayComposite { public const float HEIGHT = 50; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index 8741b0323d..ae03d384f6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }; } - protected class MatchSettings : MultiplayerComposite + protected class MatchSettings : OnlinePlayComposite { private const float disabled_alpha = 0.2f; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs index 654dafe9aa..ac608a13d4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs @@ -7,7 +7,7 @@ using osu.Game.Online.Multiplayer; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public abstract class MultiplayerRoomComposite : MultiplayerComposite + public abstract class MultiplayerRoomComposite : OnlinePlayComposite { [CanBeNull] protected MultiplayerRoom Room => Client.Room; diff --git a/osu.Game/Screens/OnlinePlay/MultiplayerComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs similarity index 96% rename from osu.Game/Screens/OnlinePlay/MultiplayerComposite.cs rename to osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index ab54178ab4..64792a32f3 100644 --- a/osu.Game/Screens/OnlinePlay/MultiplayerComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -10,7 +10,7 @@ using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay { - public class MultiplayerComposite : CompositeDrawable + public class OnlinePlayComposite : CompositeDrawable { [Resolved(typeof(Room))] protected Bindable RoomID { get; private set; } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 9ba2e41d12..4074dd1573 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -338,7 +338,7 @@ namespace osu.Game.Screens.OnlinePlay } } - private class HeaderBackgroundSprite : MultiplayerBackgroundSprite + private class HeaderBackgroundSprite : OnlinePlayBackgroundSprite { protected override UpdateableBeatmapBackgroundSprite CreateBackgroundSprite() => new BackgroundSprite { RelativeSizeAxes = Axes.Both }; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index 557f1df657..6b92526f35 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }; } - protected class MatchSettings : MultiplayerComposite + protected class MatchSettings : OnlinePlayComposite { private const float disabled_alpha = 0.2f; From 2e4b1b95c283418818bd72f40a0885778112728f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 17:12:46 +0100 Subject: [PATCH 5700/6909] Rename {Multiplayer -> OnlinePlay}BackgroundSprite --- ...layerBackgroundSprite.cs => OnlinePlayBackgroundSprite.cs} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename osu.Game/Screens/OnlinePlay/Components/{MultiplayerBackgroundSprite.cs => OnlinePlayBackgroundSprite.cs} (85%) diff --git a/osu.Game/Screens/OnlinePlay/Components/MultiplayerBackgroundSprite.cs b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs similarity index 85% rename from osu.Game/Screens/OnlinePlay/Components/MultiplayerBackgroundSprite.cs rename to osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs index 45e2c553e7..d8dfac496d 100644 --- a/osu.Game/Screens/OnlinePlay/Components/MultiplayerBackgroundSprite.cs +++ b/osu.Game/Screens/OnlinePlay/Components/OnlinePlayBackgroundSprite.cs @@ -8,12 +8,12 @@ using osu.Game.Beatmaps.Drawables; namespace osu.Game.Screens.OnlinePlay.Components { - public class MultiplayerBackgroundSprite : MultiplayerComposite + public class OnlinePlayBackgroundSprite : OnlinePlayComposite { private readonly BeatmapSetCoverType beatmapSetCoverType; private UpdateableBeatmapBackgroundSprite sprite; - public MultiplayerBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover) + public OnlinePlayBackgroundSprite(BeatmapSetCoverType beatmapSetCoverType = BeatmapSetCoverType.Cover) { this.beatmapSetCoverType = beatmapSetCoverType; } From 0bd9f68cbd6758faa79fbb8a3feac064edb2bdda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 18:39:55 +0100 Subject: [PATCH 5701/6909] Refactor update stream colour mapping code --- .../API/Requests/Responses/APIUpdateStream.cs | 37 +++++-------------- 1 file changed, 10 insertions(+), 27 deletions(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs index d9e48373bb..5af7d6a01c 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUpdateStream.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using Newtonsoft.Json; using osu.Framework.Graphics.Colour; using osuTK.Graphics; @@ -27,34 +28,16 @@ namespace osu.Game.Online.API.Requests.Responses public bool Equals(APIUpdateStream other) => Id == other?.Id; - public ColourInfo Colour + internal static readonly Dictionary KNOWN_STREAMS = new Dictionary { - get - { - switch (Name) - { - case "stable40": - return new Color4(102, 204, 255, 255); + ["stable40"] = new Color4(102, 204, 255, 255), + ["stable"] = new Color4(34, 153, 187, 255), + ["beta40"] = new Color4(255, 221, 85, 255), + ["cuttingedge"] = new Color4(238, 170, 0, 255), + [OsuGameBase.CLIENT_STREAM_NAME] = new Color4(237, 18, 33, 255), + ["web"] = new Color4(136, 102, 238, 255) + }; - case "stable": - return new Color4(34, 153, 187, 255); - - case "beta40": - return new Color4(255, 221, 85, 255); - - case "cuttingedge": - return new Color4(238, 170, 0, 255); - - case OsuGameBase.CLIENT_STREAM_NAME: - return new Color4(237, 18, 33, 255); - - case "web": - return new Color4(136, 102, 238, 255); - - default: - return new Color4(0, 0, 0, 255); - } - } - } + public ColourInfo Colour => KNOWN_STREAMS.TryGetValue(Name, out var colour) ? colour : new Color4(0, 0, 0, 255); } } From dacf6d5a34134b2d10070a0fc74232492e37e237 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 19:28:33 +0100 Subject: [PATCH 5702/6909] Decouple changelog test scene from web --- .../Online/TestSceneChangelogOverlay.cs | 132 ++++++++++++------ 1 file changed, 92 insertions(+), 40 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 998e42b478..9f617d49da 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -1,8 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Collections.Generic; +using System.Linq; +using Humanizer; using NUnit.Framework; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays; using osu.Game.Overlays.Changelog; @@ -12,15 +17,63 @@ namespace osu.Game.Tests.Visual.Online [TestFixture] public class TestSceneChangelogOverlay : OsuTestScene { + private DummyAPIAccess dummyAPI => (DummyAPIAccess)API; + + private readonly Dictionary streams; + private readonly Dictionary builds; + + private APIChangelogBuild requestedBuild; private TestChangelogOverlay changelog; - protected override bool UseOnlineAPI => true; + public TestSceneChangelogOverlay() + { + streams = APIUpdateStream.KNOWN_STREAMS.Keys.Select((stream, id) => new APIUpdateStream + { + Id = id + 1, + Name = stream, + DisplayName = stream.Humanize(), // not quite there, but good enough. + }).ToDictionary(stream => stream.Name); + + string version = DateTimeOffset.Now.ToString("yyyy.Mdd.0"); + builds = APIUpdateStream.KNOWN_STREAMS.Keys.Select(stream => new APIChangelogBuild + { + Version = version, + DisplayVersion = version, + UpdateStream = streams[stream], + ChangelogEntries = new List() + }).ToDictionary(build => build.UpdateStream.Name); + + foreach (var stream in streams.Values) + stream.LatestBuild = builds[stream.Name]; + } [SetUp] - public void SetUp() => Schedule(() => + public void SetUp() { - Child = changelog = new TestChangelogOverlay(); - }); + requestedBuild = null; + + dummyAPI.HandleRequest = request => + { + switch (request) + { + case GetChangelogRequest changelogRequest: + var changelogResponse = new APIChangelogIndex + { + Streams = streams.Values.ToList(), + Builds = builds.Values.ToList() + }; + changelogRequest.TriggerSuccess(changelogResponse); + break; + + case GetChangelogBuildRequest buildRequest: + if (requestedBuild != null) + buildRequest.TriggerSuccess(requestedBuild); + break; + } + }; + + Schedule(() => Child = changelog = new TestChangelogOverlay()); + } [Test] public void ShowWithNoFetch() @@ -41,26 +94,22 @@ namespace osu.Game.Tests.Visual.Online } [Test] - [Ignore("needs to be updated to not be so server dependent")] public void ShowWithBuild() { - AddStep(@"Show with Lazer 2018.712.0", () => + showBuild(() => new APIChangelogBuild { - changelog.ShowBuild(new APIChangelogBuild + Version = "2018.712.0", + DisplayVersion = "2018.712.0", + UpdateStream = streams[OsuGameBase.CLIENT_STREAM_NAME], + ChangelogEntries = new List { - Version = "2018.712.0", - DisplayVersion = "2018.712.0", - UpdateStream = new APIUpdateStream { Id = 5, Name = OsuGameBase.CLIENT_STREAM_NAME }, - ChangelogEntries = new List + new APIChangelogEntry { - new APIChangelogEntry - { - Category = "Test", - Title = "Title", - MessageHtml = "Message", - } + Category = "Test", + Title = "Title", + MessageHtml = "Message", } - }); + } }); AddUntilStep(@"wait for streams", () => changelog.Streams?.Count > 0); @@ -71,35 +120,38 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestHTMLUnescaping() { - AddStep(@"Ensure HTML string unescaping", () => + showBuild(() => new APIChangelogBuild { - changelog.ShowBuild(new APIChangelogBuild + Version = "2019.920.0", + DisplayVersion = "2019.920.0", + UpdateStream = new APIUpdateStream { - Version = "2019.920.0", - DisplayVersion = "2019.920.0", - UpdateStream = new APIUpdateStream + Name = "Test", + DisplayName = "Test" + }, + ChangelogEntries = new List + { + new APIChangelogEntry { - Name = "Test", - DisplayName = "Test" - }, - ChangelogEntries = new List - { - new APIChangelogEntry + Category = "Testing HTML strings unescaping", + Title = "Ensuring HTML strings are being unescaped", + MessageHtml = """"This text should appear triple-quoted""" >_<", + GithubUser = new APIChangelogUser { - Category = "Testing HTML strings unescaping", - Title = "Ensuring HTML strings are being unescaped", - MessageHtml = """"This text should appear triple-quoted""" >_<", - GithubUser = new APIChangelogUser - { - DisplayName = "Dummy", - OsuUsername = "Dummy", - } - }, - } - }); + DisplayName = "Dummy", + OsuUsername = "Dummy", + } + }, + } }); } + private void showBuild(Func build) + { + AddStep("set up build", () => requestedBuild = build.Invoke()); + AddStep("show build", () => changelog.ShowBuild(requestedBuild)); + } + private class TestChangelogOverlay : ChangelogOverlay { public new List Streams => base.Streams; From 5f43299d3779ffbde32d3bd2f735592f30fad820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 19:49:41 +0100 Subject: [PATCH 5703/6909] Fix tests failing due to base logic firing It turns out that the changelog code was semi-intentionally relying on the request to get release streams to be slow to initially show the listing of all streams. Locally suppress the base tab control logic to fix this. --- osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs index 509a6dabae..6bbff045b5 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs @@ -8,5 +8,11 @@ namespace osu.Game.Overlays.Changelog public class ChangelogUpdateStreamControl : OverlayStreamControl { protected override OverlayStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value); + + protected override void LoadComplete() + { + // suppress base logic of immediately selecting first item if one exists + // (we always want to start with no stream selected). + } } } From 3ac618778f11010de62bedec719141319b37cc40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 20:08:57 +0100 Subject: [PATCH 5704/6909] Handle all changelog entry types correctly --- .../Online/TestSceneChangelogOverlay.cs | 20 +++++++++++++++--- osu.Game/Overlays/Changelog/ChangelogBuild.cs | 21 ++++++++++++++++++- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 9f617d49da..42f822664f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -105,9 +105,23 @@ namespace osu.Game.Tests.Visual.Online { new APIChangelogEntry { - Category = "Test", - Title = "Title", - MessageHtml = "Message", + Type = ChangelogEntryType.Fix, + Category = "osu!", + Title = "Fix thing", + MessageHtml = "Additional info goes here.", + }, + new APIChangelogEntry + { + Type = ChangelogEntryType.Add, + Category = "osu!", + Title = "Add thing", + Major = true + }, + new APIChangelogEntry + { + Type = ChangelogEntryType.Misc, + Category = "Code quality", + Title = "Clean up thing" } } }); diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs index 65ff0fef92..6ec3f08a2b 100644 --- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -84,7 +84,7 @@ namespace osu.Game.Overlays.Changelog Anchor = Anchor.CentreLeft, Origin = Anchor.CentreRight, Size = new Vector2(10), - Icon = entry.Type == ChangelogEntryType.Fix ? FontAwesome.Solid.Check : FontAwesome.Solid.Plus, + Icon = getIconForChangelogEntry(entry.Type), Colour = entryColour.Opacity(0.5f), Margin = new MarginPadding { Right = 5 }, }, @@ -186,6 +186,25 @@ namespace osu.Game.Overlays.Changelog } } + private static IconUsage getIconForChangelogEntry(ChangelogEntryType entryType) + { + // compare: https://github.com/ppy/osu-web/blob/master/resources/assets/coffee/react/_components/changelog-entry.coffee#L8-L11 + switch (entryType) + { + case ChangelogEntryType.Add: + return FontAwesome.Solid.Plus; + + case ChangelogEntryType.Fix: + return FontAwesome.Solid.Check; + + case ChangelogEntryType.Misc: + return FontAwesome.Regular.Circle; + + default: + throw new ArgumentOutOfRangeException(nameof(entryType), $"Unrecognised entry type {entryType}"); + } + } + protected virtual FillFlowContainer CreateHeader() => new FillFlowContainer { Anchor = Anchor.TopCentre, From 0aedc720f2c61a9f0d8d4223d3896259181ad071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 20:31:12 +0100 Subject: [PATCH 5705/6909] Extract changelog entry component --- osu.Game/Overlays/Changelog/ChangelogBuild.cs | 146 +------------ osu.Game/Overlays/Changelog/ChangelogEntry.cs | 202 ++++++++++++++++++ 2 files changed, 203 insertions(+), 145 deletions(-) create mode 100644 osu.Game/Overlays/Changelog/ChangelogEntry.cs diff --git a/osu.Game/Overlays/Changelog/ChangelogBuild.cs b/osu.Game/Overlays/Changelog/ChangelogBuild.cs index 6ec3f08a2b..2d071b7345 100644 --- a/osu.Game/Overlays/Changelog/ChangelogBuild.cs +++ b/osu.Game/Overlays/Changelog/ChangelogBuild.cs @@ -9,14 +9,8 @@ using osu.Game.Graphics.Containers; using osu.Game.Online.API.Requests.Responses; using System; using System.Linq; -using System.Text.RegularExpressions; using osu.Game.Graphics.Sprites; -using osu.Game.Users; -using osuTK.Graphics; using osu.Framework.Allocation; -using System.Net; -using osuTK; -using osu.Framework.Extensions.Color4Extensions; namespace osu.Game.Overlays.Changelog { @@ -63,145 +57,7 @@ namespace osu.Game.Overlays.Changelog Margin = new MarginPadding { Top = 35, Bottom = 15 }, }); - var fontLarge = OsuFont.GetFont(size: 16); - var fontMedium = OsuFont.GetFont(size: 12); - - foreach (var entry in categoryEntries) - { - var entryColour = entry.Major ? colours.YellowLight : Color4.White; - - LinkFlowContainer title; - - var titleContainer = new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Vertical = 5 }, - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreRight, - Size = new Vector2(10), - Icon = getIconForChangelogEntry(entry.Type), - Colour = entryColour.Opacity(0.5f), - Margin = new MarginPadding { Right = 5 }, - }, - title = new LinkFlowContainer - { - Direction = FillDirection.Full, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - TextAnchor = Anchor.BottomLeft, - } - } - }; - - title.AddText(entry.Title, t => - { - t.Font = fontLarge; - t.Colour = entryColour; - }); - - if (!string.IsNullOrEmpty(entry.Repository)) - { - title.AddText(" (", t => - { - t.Font = fontLarge; - t.Colour = entryColour; - }); - title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, - creationParameters: t => - { - t.Font = fontLarge; - t.Colour = entryColour; - }); - title.AddText(")", t => - { - t.Font = fontLarge; - t.Colour = entryColour; - }); - } - - title.AddText("by ", t => - { - t.Font = fontMedium; - t.Colour = entryColour; - t.Padding = new MarginPadding { Left = 10 }; - }); - - if (entry.GithubUser != null) - { - if (entry.GithubUser.UserId != null) - { - title.AddUserLink(new User - { - Username = entry.GithubUser.OsuUsername, - Id = entry.GithubUser.UserId.Value - }, t => - { - t.Font = fontMedium; - t.Colour = entryColour; - }); - } - else if (entry.GithubUser.GithubUrl != null) - { - title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => - { - t.Font = fontMedium; - t.Colour = entryColour; - }); - } - else - { - title.AddText(entry.GithubUser.DisplayName, t => - { - t.Font = fontMedium; - t.Colour = entryColour; - }); - } - } - - ChangelogEntries.Add(titleContainer); - - if (!string.IsNullOrEmpty(entry.MessageHtml)) - { - var message = new TextFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - }; - - // todo: use markdown parsing once API returns markdown - message.AddText(WebUtility.HtmlDecode(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty)), t => - { - t.Font = fontMedium; - t.Colour = colourProvider.Foreground1; - }); - - ChangelogEntries.Add(message); - } - } - } - } - - private static IconUsage getIconForChangelogEntry(ChangelogEntryType entryType) - { - // compare: https://github.com/ppy/osu-web/blob/master/resources/assets/coffee/react/_components/changelog-entry.coffee#L8-L11 - switch (entryType) - { - case ChangelogEntryType.Add: - return FontAwesome.Solid.Plus; - - case ChangelogEntryType.Fix: - return FontAwesome.Solid.Check; - - case ChangelogEntryType.Misc: - return FontAwesome.Regular.Circle; - - default: - throw new ArgumentOutOfRangeException(nameof(entryType), $"Unrecognised entry type {entryType}"); + ChangelogEntries.AddRange(categoryEntries.Select(entry => new ChangelogEntry(entry))); } } diff --git a/osu.Game/Overlays/Changelog/ChangelogEntry.cs b/osu.Game/Overlays/Changelog/ChangelogEntry.cs new file mode 100644 index 0000000000..55edb40283 --- /dev/null +++ b/osu.Game/Overlays/Changelog/ChangelogEntry.cs @@ -0,0 +1,202 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Net; +using System.Text.RegularExpressions; +using osu.Framework.Allocation; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Containers; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Users; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.Changelog +{ + public class ChangelogEntry : FillFlowContainer + { + private readonly APIChangelogEntry entry; + + [Resolved] + private OsuColour colours { get; set; } + + [Resolved] + private OverlayColourProvider colourProvider { get; set; } + + private FontUsage fontLarge; + private FontUsage fontMedium; + + public ChangelogEntry(APIChangelogEntry entry) + { + this.entry = entry; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + Direction = FillDirection.Vertical; + } + + [BackgroundDependencyLoader] + private void load() + { + fontLarge = OsuFont.GetFont(size: 16); + fontMedium = OsuFont.GetFont(size: 12); + + Children = new[] + { + createTitle(), + createMessage() + }; + } + + private Drawable createTitle() + { + var entryColour = entry.Major ? colours.YellowLight : Color4.White; + + LinkFlowContainer title; + + var titleContainer = new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Margin = new MarginPadding { Vertical = 5 }, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreRight, + Size = new Vector2(10), + Icon = getIconForChangelogEntry(entry.Type), + Colour = entryColour.Opacity(0.5f), + Margin = new MarginPadding { Right = 5 }, + }, + title = new LinkFlowContainer + { + Direction = FillDirection.Full, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + TextAnchor = Anchor.BottomLeft, + } + } + }; + + title.AddText(entry.Title, t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + + if (!string.IsNullOrEmpty(entry.Repository)) + addRepositoryReference(title, entryColour); + + if (entry.GithubUser != null) + addGithubAuthorReference(title, entryColour); + + return titleContainer; + } + + private void addRepositoryReference(LinkFlowContainer title, Color4 entryColour) + { + title.AddText(" (", t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + title.AddLink($"{entry.Repository.Replace("ppy/", "")}#{entry.GithubPullRequestId}", entry.GithubUrl, + t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + title.AddText(")", t => + { + t.Font = fontLarge; + t.Colour = entryColour; + }); + } + + private void addGithubAuthorReference(LinkFlowContainer title, Color4 entryColour) + { + title.AddText("by ", t => + { + t.Font = fontMedium; + t.Colour = entryColour; + t.Padding = new MarginPadding { Left = 10 }; + }); + + if (entry.GithubUser.UserId != null) + { + title.AddUserLink(new User + { + Username = entry.GithubUser.OsuUsername, + Id = entry.GithubUser.UserId.Value + }, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } + else if (entry.GithubUser.GithubUrl != null) + { + title.AddLink(entry.GithubUser.DisplayName, entry.GithubUser.GithubUrl, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } + else + { + title.AddText(entry.GithubUser.DisplayName, t => + { + t.Font = fontMedium; + t.Colour = entryColour; + }); + } + } + + private Drawable createMessage() + { + if (string.IsNullOrEmpty(entry.MessageHtml)) + return Empty(); + + var message = new TextFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + }; + + // todo: use markdown parsing once API returns markdown + message.AddText(WebUtility.HtmlDecode(Regex.Replace(entry.MessageHtml, @"<(.|\n)*?>", string.Empty)), t => + { + t.Font = fontMedium; + t.Colour = colourProvider.Foreground1; + }); + + return message; + } + + private static IconUsage getIconForChangelogEntry(ChangelogEntryType entryType) + { + // compare: https://github.com/ppy/osu-web/blob/master/resources/assets/coffee/react/_components/changelog-entry.coffee#L8-L11 + switch (entryType) + { + case ChangelogEntryType.Add: + return FontAwesome.Solid.Plus; + + case ChangelogEntryType.Fix: + return FontAwesome.Solid.Check; + + case ChangelogEntryType.Misc: + return FontAwesome.Regular.Circle; + + default: + throw new ArgumentOutOfRangeException(nameof(entryType), $"Unrecognised entry type {entryType}"); + } + } + } +} From c32fc05f69435a8b5adc6b7c715ce57b75cb404e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 20:41:35 +0100 Subject: [PATCH 5706/6909] Improve test scene coverage of corner cases --- .../Online/TestSceneChangelogOverlay.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index 42f822664f..eef2892290 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -109,19 +109,43 @@ namespace osu.Game.Tests.Visual.Online Category = "osu!", Title = "Fix thing", MessageHtml = "Additional info goes here.", + Repository = "osu", + GithubPullRequestId = 11100, + GithubUser = new APIChangelogUser + { + OsuUsername = "smoogipoo", + UserId = 1040328 + } }, new APIChangelogEntry { Type = ChangelogEntryType.Add, Category = "osu!", Title = "Add thing", - Major = true + Major = true, + Repository = "ppy/osu-framework", + GithubPullRequestId = 4444, + GithubUser = new APIChangelogUser + { + DisplayName = "frenzibyte", + GithubUrl = "https://github.com/frenzibyte" + } }, new APIChangelogEntry { Type = ChangelogEntryType.Misc, Category = "Code quality", - Title = "Clean up thing" + Title = "Clean up thing", + GithubUser = new APIChangelogUser + { + DisplayName = "some dude" + } + }, + new APIChangelogEntry + { + Type = ChangelogEntryType.Misc, + Category = "Code quality", + Title = "Clean up another thing" } } }); From 09b0a57290fe224f9a45bd759ae4c85856c3f518 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 25 Dec 2020 20:44:19 +0100 Subject: [PATCH 5707/6909] Schedule all of setup to avoid headless test fail --- osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs index eef2892290..cd2c4e9346 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChangelogOverlay.cs @@ -48,7 +48,7 @@ namespace osu.Game.Tests.Visual.Online } [SetUp] - public void SetUp() + public void SetUp() => Schedule(() => { requestedBuild = null; @@ -72,8 +72,8 @@ namespace osu.Game.Tests.Visual.Online } }; - Schedule(() => Child = changelog = new TestChangelogOverlay()); - } + Child = changelog = new TestChangelogOverlay(); + }); [Test] public void ShowWithNoFetch() From e0198c36aeebfddea645a9386a66a7439568d3d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Dec 2020 09:48:13 +0900 Subject: [PATCH 5708/6909] Fix user population happening in single file --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index e422e982ae..6d253d5992 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -108,8 +108,7 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(Room != null); - foreach (var user in Room.Users) - await PopulateUser(user); + await Task.WhenAll(Room.Users.Select(PopulateUser)); updateLocalRoomSettings(Room.Settings); } From 5ce5b6cec06403b2bbbeea6864f97cf0ebca0bc0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Dec 2020 10:25:16 +0900 Subject: [PATCH 5709/6909] Fix non-safe thread access to room users on room join --- .../Multiplayer/StatefulMultiplayerClient.cs | 44 ++++++++++++++++--- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 6d253d5992..ec0967df75 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -4,8 +4,10 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -108,7 +110,9 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(Room != null); - await Task.WhenAll(Room.Users.Select(PopulateUser)); + var users = getRoomUsers(); + + await Task.WhenAll(users.Select(PopulateUser)); updateLocalRoomSettings(Room.Settings); } @@ -122,13 +126,16 @@ namespace osu.Game.Online.Multiplayer public virtual Task LeaveRoom() { - if (Room == null) - return Task.CompletedTask; + Schedule(() => + { + if (Room == null) + return; - apiRoom = null; - Room = null; + apiRoom = null; + Room = null; - Schedule(() => RoomChanged?.Invoke()); + RoomChanged?.Invoke(); + }); return Task.CompletedTask; } @@ -360,6 +367,31 @@ namespace osu.Game.Online.Multiplayer /// The to populate. protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID); + /// + /// Retrieve a copy of users currently in the joined in a thread-safe manner. + /// This should be used whenever accessing users from outside of an Update thread context (ie. when not calling ). + /// + /// A copy of users in the current room, or null if unavailable. + private List? getRoomUsers() + { + List? users = null; + + ManualResetEventSlim resetEvent = new ManualResetEventSlim(); + + // at some point we probably want to replace all these schedule calls with Room.LockForUpdate. + // for now, as this would require quite some consideration due to the number of accesses to the room instance, + // let's just to a schedule for the non-scheduled usages instead. + Schedule(() => + { + users = Room?.Users.ToList(); + resetEvent.Set(); + }); + + resetEvent.Wait(100); + + return users; + } + /// /// Updates the local room settings with the given . /// From f9900720d58e9b5e61f48f3a83643b8f2c11b88c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Dec 2020 10:48:55 +0900 Subject: [PATCH 5710/6909] Rename OnRoomChanged to OnRoomUpdated to avoid confusion --- .../Multiplayer/StatefulMultiplayerClient.cs | 16 ++++++++-------- .../Multiplayer/Match/MultiplayerReadyButton.cs | 4 ++-- .../Multiplayer/MultiplayerRoomComposite.cs | 11 +++++++---- .../Multiplayer/Participants/ParticipantPanel.cs | 4 ++-- .../Multiplayer/Participants/ParticipantsList.cs | 4 ++-- 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index ec0967df75..8839b79c13 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -33,7 +33,7 @@ namespace osu.Game.Online.Multiplayer /// /// Invoked when any change occurs to the multiplayer room. /// - public event Action? RoomChanged; + public event Action? RoomUpdated; /// /// Invoked when the multiplayer server requests the current beatmap to be loaded into play. @@ -134,7 +134,7 @@ namespace osu.Game.Online.Multiplayer apiRoom = null; Room = null; - RoomChanged?.Invoke(); + RoomUpdated?.Invoke(); }); return Task.CompletedTask; @@ -214,7 +214,7 @@ namespace osu.Game.Online.Multiplayer break; } - RoomChanged?.Invoke(); + RoomUpdated?.Invoke(); }); return Task.CompletedTask; @@ -238,7 +238,7 @@ namespace osu.Game.Online.Multiplayer Room.Users.Add(user); - RoomChanged?.Invoke(); + RoomUpdated?.Invoke(); }); } @@ -255,7 +255,7 @@ namespace osu.Game.Online.Multiplayer Room.Users.Remove(user); PlayingUsers.Remove(user.UserID); - RoomChanged?.Invoke(); + RoomUpdated?.Invoke(); }); return Task.CompletedTask; @@ -278,7 +278,7 @@ namespace osu.Game.Online.Multiplayer Room.Host = user; apiRoom.Host.Value = user?.User; - RoomChanged?.Invoke(); + RoomUpdated?.Invoke(); }); return Task.CompletedTask; @@ -305,7 +305,7 @@ namespace osu.Game.Online.Multiplayer if (state != MultiplayerUserState.Playing) PlayingUsers.Remove(userId); - RoomChanged?.Invoke(); + RoomUpdated?.Invoke(); }); return Task.CompletedTask; @@ -419,7 +419,7 @@ namespace osu.Game.Online.Multiplayer // In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here. apiRoom.Playlist.Clear(); - RoomChanged?.Invoke(); + RoomUpdated?.Invoke(); var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); req.Success += res => updatePlaylist(settings, res); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 15d6ef8aff..975a2cf023 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -56,9 +56,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match sampleReadyCount = audio.Samples.Get(@"SongSelect/select-difficulty"); } - protected override void OnRoomChanged() + protected override void OnRoomUpdated() { - base.OnRoomChanged(); + base.OnRoomUpdated(); localUser = Room?.Users.Single(u => u.User?.Id == api.LocalUser.Value.Id); button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs index ac608a13d4..8030107ad8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomComposite.cs @@ -19,18 +19,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - Client.RoomChanged += OnRoomChanged; - OnRoomChanged(); + Client.RoomUpdated += OnRoomUpdated; + OnRoomUpdated(); } - protected virtual void OnRoomChanged() + /// + /// Invoked when any change occurs to the multiplayer room. + /// + protected virtual void OnRoomUpdated() { } protected override void Dispose(bool isDisposing) { if (Client != null) - Client.RoomChanged -= OnRoomChanged; + Client.RoomUpdated -= OnRoomUpdated; base.Dispose(isDisposing); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 044afa7445..de3069b2f6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -135,9 +135,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants }; } - protected override void OnRoomChanged() + protected override void OnRoomUpdated() { - base.OnRoomChanged(); + base.OnRoomUpdated(); if (Room == null) return; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs index b9ac096c4a..3759e45f18 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantsList.cs @@ -36,9 +36,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants }; } - protected override void OnRoomChanged() + protected override void OnRoomUpdated() { - base.OnRoomChanged(); + base.OnRoomUpdated(); if (Room == null) panels.Clear(); From fe1bbb1cac671b65dca154784955314db434adbc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Dec 2020 10:49:22 +0900 Subject: [PATCH 5711/6909] Don't fail if the local user is not present in room users when updating ready button state --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 975a2cf023..281e92404c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -60,7 +60,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.OnRoomUpdated(); - localUser = Room?.Users.Single(u => u.User?.Id == api.LocalUser.Value.Id); + // this method is called on leaving the room, so the local user may not exist in the room any more. + localUser = Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); + button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open; updateState(); } From e7339d6959ba8c94fd76ec2e5f2d0e9da0f5074d Mon Sep 17 00:00:00 2001 From: Neuheit <38368299+Neuheit@users.noreply.github.com> Date: Fri, 25 Dec 2020 21:07:33 -0500 Subject: [PATCH 5712/6909] fix(osu.Game): Ensure Category property is copied in Room. --- osu.Game/Online/Rooms/Room.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 67f874cdc4..bb21971afa 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -130,6 +130,7 @@ namespace osu.Game.Online.Rooms { RoomID.Value = other.RoomID.Value; Name.Value = other.Name.Value; + Category.Value = other.Category.Value; if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) Host.Value = other.Host.Value; From ff57562956515a4895c6f50c0ec49746a2ad4963 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Dec 2020 11:34:05 +0900 Subject: [PATCH 5713/6909] Fix multiplayer leaderboard not unsubscribing from quit users --- ...TestSceneMultiplayerGameplayLeaderboard.cs | 5 +- .../HUD/MultiplayerGameplayLeaderboard.cs | 61 ++++++++++++++----- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs index 8078c7b994..98a3ce9b47 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -20,11 +20,12 @@ using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; +using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.Online; namespace osu.Game.Tests.Visual.Gameplay { - public class TestSceneMultiplayerGameplayLeaderboard : OsuTestScene + public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene { [Cached(typeof(SpectatorStreamingClient))] private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16); @@ -47,7 +48,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [SetUpSteps] - public void SetUpSteps() + public override void SetUpSteps() { AddStep("create leaderboard", () => { diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index c10ec9e004..ce6e19aea0 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -2,12 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System.Collections.Generic; +using System.Collections.Specialized; +using System.Diagnostics; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Configuration; using osu.Game.Database; using osu.Game.Online.API; +using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Rulesets.Scoring; @@ -18,10 +22,21 @@ namespace osu.Game.Screens.Play.HUD { private readonly ScoreProcessor scoreProcessor; - private readonly int[] userIds; - private readonly Dictionary userScores = new Dictionary(); + [Resolved] + private SpectatorStreamingClient streamingClient { get; set; } + + [Resolved] + private StatefulMultiplayerClient multiplayerClient { get; set; } + + [Resolved] + private UserLookupCache userLookupCache { get; set; } + + private Bindable scoringMode; + + private readonly BindableList playingUsers; + /// /// Construct a new leaderboard. /// @@ -33,32 +48,24 @@ namespace osu.Game.Screens.Play.HUD this.scoreProcessor = scoreProcessor; // todo: this will likely be passed in as User instances. - this.userIds = userIds; + playingUsers = new BindableList(userIds); } - [Resolved] - private SpectatorStreamingClient streamingClient { get; set; } - - [Resolved] - private UserLookupCache userLookupCache { get; set; } - - private Bindable scoringMode; - [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { streamingClient.OnNewFrames += handleIncomingFrames; - foreach (var user in userIds) + foreach (var userId in playingUsers) { - streamingClient.WatchUser(user); + streamingClient.WatchUser(userId); // probably won't be required in the final implementation. - var resolvedUser = userLookupCache.GetUserAsync(user).Result; + var resolvedUser = userLookupCache.GetUserAsync(userId).Result; var trackedUser = new TrackedUserData(); - userScores[user] = trackedUser; + userScores[userId] = trackedUser; var leaderboardScore = AddPlayer(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id); ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy); @@ -70,6 +77,28 @@ namespace osu.Game.Screens.Play.HUD scoringMode.BindValueChanged(updateAllScores, true); } + protected override void LoadComplete() + { + base.LoadComplete(); + + playingUsers.BindCollectionChanged(usersChanged); + playingUsers.BindTo(multiplayerClient.PlayingUsers); + } + + private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Remove: + foreach (var userId in e.OldItems.OfType()) + { + streamingClient.StopWatchingUser(userId); + } + + break; + } + } + private void updateAllScores(ValueChangedEvent mode) { foreach (var trackedData in userScores.Values) @@ -91,7 +120,7 @@ namespace osu.Game.Screens.Play.HUD if (streamingClient != null) { - foreach (var user in userIds) + foreach (var user in playingUsers) { streamingClient.StopWatchingUser(user); } From 116acc2b5e073dc6578ad1169ce109a09bc76d24 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Dec 2020 11:35:51 +0900 Subject: [PATCH 5714/6909] Add flow for marking user as quit for further handling --- .../Play/HUD/MultiplayerGameplayLeaderboard.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index ce6e19aea0..a71c4685d9 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -93,6 +93,9 @@ namespace osu.Game.Screens.Play.HUD foreach (var userId in e.OldItems.OfType()) { streamingClient.StopWatchingUser(userId); + + if (userScores.TryGetValue(userId, out var trackedData)) + trackedData.MarkUserQuit(); } break; @@ -143,11 +146,19 @@ namespace osu.Game.Screens.Play.HUD private readonly BindableInt currentCombo = new BindableInt(); + public IBindable UserQuit => userQuit; + + private readonly BindableBool userQuit = new BindableBool(); + [CanBeNull] public FrameHeader LastHeader; + public void MarkUserQuit() => userQuit.Value = true; + public void UpdateScore(ScoreProcessor processor, ScoringMode mode) { + Debug.Assert(UserQuit.Value); + if (LastHeader == null) return; From 71dcbeaf7ce57eb0aae0cd72be4f3a74a2e505bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Dec 2020 11:43:10 +0900 Subject: [PATCH 5715/6909] Mark user as quit visually on the leaderboard --- .../Screens/Play/HUD/GameplayLeaderboardScore.cs | 13 +++++++++++++ osu.Game/Screens/Play/HUD/ILeaderboardScore.cs | 2 ++ .../Play/HUD/MultiplayerGameplayLeaderboard.cs | 12 ++++++++++-- 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 58281debf1..ed86f3241d 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -34,6 +34,7 @@ namespace osu.Game.Screens.Play.HUD public BindableDouble TotalScore { get; } = new BindableDouble(); public BindableDouble Accuracy { get; } = new BindableDouble(1); public BindableInt Combo { get; } = new BindableInt(); + public BindableBool HasQuit { get; } = new BindableBool(); private int? scorePosition; @@ -230,6 +231,15 @@ namespace osu.Game.Screens.Play.HUD TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); + HasQuit.BindValueChanged(v => + { + if (v.NewValue) + { + // we will probably want to display this in a better way once we have a design. + // and also show states other than quit. + panelColour = Color4.Gray; + } + }, true); } protected override void LoadComplete() @@ -244,6 +254,9 @@ namespace osu.Game.Screens.Play.HUD private void updateColour() { + if (HasQuit.Value) + return; + if (scorePosition == 1) { mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); diff --git a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs index bc1a03c5aa..83b6f6621b 100644 --- a/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/ILeaderboardScore.cs @@ -10,5 +10,7 @@ namespace osu.Game.Screens.Play.HUD BindableDouble TotalScore { get; } BindableDouble Accuracy { get; } BindableInt Combo { get; } + + BindableBool HasQuit { get; } } } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index a71c4685d9..6b0ca4d74c 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -71,6 +71,7 @@ namespace osu.Game.Screens.Play.HUD ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy); ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score); ((IBindable)leaderboardScore.Combo).BindTo(trackedUser.CurrentCombo); + ((IBindable)leaderboardScore.HasQuit).BindTo(trackedUser.UserQuit); } scoringMode = config.GetBindable(OsuSetting.ScoreDisplayMode); @@ -81,8 +82,15 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - playingUsers.BindCollectionChanged(usersChanged); + // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. + foreach (int userId in playingUsers) + { + if (!multiplayerClient.PlayingUsers.Contains(userId)) + usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId })); + } + playingUsers.BindTo(multiplayerClient.PlayingUsers); + playingUsers.BindCollectionChanged(usersChanged); } private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) @@ -157,7 +165,7 @@ namespace osu.Game.Screens.Play.HUD public void UpdateScore(ScoreProcessor processor, ScoringMode mode) { - Debug.Assert(UserQuit.Value); + Debug.Assert(!UserQuit.Value); if (LastHeader == null) return; From 2599e95335a95ac8ae6b4a9685ac074432089a60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Dec 2020 12:11:19 +0900 Subject: [PATCH 5716/6909] Add test coverage --- .../TestSceneMultiplayerGameplayLeaderboard.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs index 98a3ce9b47..c214a34fe3 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -27,8 +27,10 @@ namespace osu.Game.Tests.Visual.Gameplay { public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene { + private const int users = 16; + [Cached(typeof(SpectatorStreamingClient))] - private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(16); + private TestMultiplayerStreaming streamingClient = new TestMultiplayerStreaming(users); [Cached(typeof(UserLookupCache))] private UserLookupCache lookupCache = new TestSceneCurrentlyPlayingDisplay.TestUserLookupCache(); @@ -59,6 +61,9 @@ namespace osu.Game.Tests.Visual.Gameplay streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); + Client.PlayingUsers.Clear(); + Client.PlayingUsers.AddRange(streamingClient.PlayingUsers); + Children = new Drawable[] { scoreProcessor = new OsuScoreProcessor(), @@ -82,6 +87,12 @@ namespace osu.Game.Tests.Visual.Gameplay AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100); } + [Test] + public void TestUserQuit() + { + AddRepeatStep("mark user quit", () => Client.PlayingUsers.RemoveAt(0), users); + } + public class TestMultiplayerStreaming : SpectatorStreamingClient { public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; From 966a2151e3c2af58e9cdb506ffe8637dc5ce9fe3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Dec 2020 17:55:24 +0900 Subject: [PATCH 5717/6909] Ensure the previous leaderboard is removed --- .../Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs index c214a34fe3..975c54c3f6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs @@ -54,6 +54,8 @@ namespace osu.Game.Tests.Visual.Gameplay { AddStep("create leaderboard", () => { + leaderboard?.Expire(); + OsuScoreProcessor scoreProcessor; Beatmap.Value = CreateWorkingBeatmap(Ruleset.Value); From dae27fefe433cefe70b1eb69be65e62b6c1f689b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Dec 2020 10:59:14 +0100 Subject: [PATCH 5718/6909] Run user list copy inline if possible `getRoomUsers()` was not safe to call from the update thread, as evidenced by the test failures. This was due to the fact that the added reset event could never actually be set from within the method, as the wait was blocking the scheduled set from ever proceeding. Resolve by allowing the scheduled copy & set to run inline if on the update thread already. --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index ec0967df75..06ed4b4a6c 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -380,12 +380,12 @@ namespace osu.Game.Online.Multiplayer // at some point we probably want to replace all these schedule calls with Room.LockForUpdate. // for now, as this would require quite some consideration due to the number of accesses to the room instance, - // let's just to a schedule for the non-scheduled usages instead. - Schedule(() => + // let's just add a manual schedule for the non-scheduled usages instead. + Scheduler.Add(() => { users = Room?.Users.ToList(); resetEvent.Set(); - }); + }, false); resetEvent.Wait(100); From 04d54c40dbdc65f08975f1ca065ddd4ca1b67d9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Dec 2020 11:58:25 +0100 Subject: [PATCH 5719/6909] Allow all StatefulMultiplayerClient schedules to run inline Fixes test failures due to not allowing to do so, therefore inverting execution order in some cases - for example, calling JoinRoom(room); LeaveRoom(); on the update thread would invert execution order due to the first being unscheduled but the second being scheduled. --- .../Multiplayer/StatefulMultiplayerClient.cs | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 06ed4b4a6c..98b53e723c 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -126,7 +126,7 @@ namespace osu.Game.Online.Multiplayer public virtual Task LeaveRoom() { - Schedule(() => + Scheduler.Add(() => { if (Room == null) return; @@ -135,7 +135,7 @@ namespace osu.Game.Online.Multiplayer Room = null; RoomChanged?.Invoke(); - }); + }, false); return Task.CompletedTask; } @@ -190,7 +190,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return Task.CompletedTask; - Schedule(() => + Scheduler.Add(() => { if (Room == null) return; @@ -215,7 +215,7 @@ namespace osu.Game.Online.Multiplayer } RoomChanged?.Invoke(); - }); + }, false); return Task.CompletedTask; } @@ -227,7 +227,7 @@ namespace osu.Game.Online.Multiplayer await PopulateUser(user); - Schedule(() => + Scheduler.Add(() => { if (Room == null) return; @@ -239,7 +239,7 @@ namespace osu.Game.Online.Multiplayer Room.Users.Add(user); RoomChanged?.Invoke(); - }); + }, false); } Task IMultiplayerClient.UserLeft(MultiplayerRoomUser user) @@ -247,7 +247,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return Task.CompletedTask; - Schedule(() => + Scheduler.Add(() => { if (Room == null) return; @@ -256,7 +256,7 @@ namespace osu.Game.Online.Multiplayer PlayingUsers.Remove(user.UserID); RoomChanged?.Invoke(); - }); + }, false); return Task.CompletedTask; } @@ -266,7 +266,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return Task.CompletedTask; - Schedule(() => + Scheduler.Add(() => { if (Room == null) return; @@ -279,7 +279,7 @@ namespace osu.Game.Online.Multiplayer apiRoom.Host.Value = user?.User; RoomChanged?.Invoke(); - }); + }, false); return Task.CompletedTask; } @@ -295,7 +295,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return Task.CompletedTask; - Schedule(() => + Scheduler.Add(() => { if (Room == null) return; @@ -306,7 +306,7 @@ namespace osu.Game.Online.Multiplayer PlayingUsers.Remove(userId); RoomChanged?.Invoke(); - }); + }, false); return Task.CompletedTask; } @@ -316,13 +316,13 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return Task.CompletedTask; - Schedule(() => + Scheduler.Add(() => { if (Room == null) return; LoadRequested?.Invoke(); - }); + }, false); return Task.CompletedTask; } @@ -332,7 +332,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return Task.CompletedTask; - Schedule(() => + Scheduler.Add(() => { if (Room == null) return; @@ -340,7 +340,7 @@ namespace osu.Game.Online.Multiplayer PlayingUsers.AddRange(Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID)); MatchStarted?.Invoke(); - }); + }, false); return Task.CompletedTask; } @@ -350,13 +350,13 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return Task.CompletedTask; - Schedule(() => + Scheduler.Add(() => { if (Room == null) return; ResultsReady?.Invoke(); - }); + }, false); return Task.CompletedTask; } @@ -404,7 +404,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return; - Schedule(() => + Scheduler.Add(() => { if (Room == null) return; @@ -425,7 +425,7 @@ namespace osu.Game.Online.Multiplayer req.Success += res => updatePlaylist(settings, res); api.Queue(req); - }); + }, false); } private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) From b9d725ab4928c0385b63da2d5ed5496b0faee21a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 26 Dec 2020 20:13:28 +0900 Subject: [PATCH 5720/6909] Don't copy spotlight category --- osu.Game/Online/Rooms/Room.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index bb21971afa..763ba25d52 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -130,7 +130,9 @@ namespace osu.Game.Online.Rooms { RoomID.Value = other.RoomID.Value; Name.Value = other.Name.Value; - Category.Value = other.Category.Value; + + if (other.Category.Value != RoomCategory.Spotlight) + Category.Value = other.Category.Value; if (other.Host.Value != null && Host.Value?.Id != other.Host.Value.Id) Host.Value = other.Host.Value; From 0b42b4b95598dcb067a4d06b4b734a0e468204c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Dec 2020 13:54:10 +0100 Subject: [PATCH 5721/6909] Rename {Drawable -> Clickable}Avatar --- osu.Game/Online/Leaderboards/LeaderboardScore.cs | 4 ++-- osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs | 4 ++-- .../Drawables/{DrawableAvatar.cs => ClickableAvatar.cs} | 7 ++++--- osu.Game/Users/Drawables/UpdateableAvatar.cs | 2 +- 4 files changed, 9 insertions(+), 8 deletions(-) rename osu.Game/Users/Drawables/{DrawableAvatar.cs => ClickableAvatar.cs} (90%) diff --git a/osu.Game/Online/Leaderboards/LeaderboardScore.cs b/osu.Game/Online/Leaderboards/LeaderboardScore.cs index d8207aa8f4..5608002513 100644 --- a/osu.Game/Online/Leaderboards/LeaderboardScore.cs +++ b/osu.Game/Online/Leaderboards/LeaderboardScore.cs @@ -78,7 +78,7 @@ namespace osu.Game.Online.Leaderboards statisticsLabels = GetStatistics(score).Select(s => new ScoreComponentLabel(s)).ToList(); - DrawableAvatar innerAvatar; + ClickableAvatar innerAvatar; Children = new Drawable[] { @@ -115,7 +115,7 @@ namespace osu.Game.Online.Leaderboards Children = new[] { avatar = new DelayedLoadWrapper( - innerAvatar = new DrawableAvatar(user) + innerAvatar = new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, CornerRadius = corner_radius, diff --git a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs index 5b428a3825..00f46b0035 100644 --- a/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs +++ b/osu.Game/Overlays/Chat/Tabs/PrivateChannelTabItem.cs @@ -25,7 +25,7 @@ namespace osu.Game.Overlays.Chat.Tabs if (value.Type != ChannelType.PM) throw new ArgumentException("Argument value needs to have the targettype user!"); - DrawableAvatar avatar; + ClickableAvatar avatar; AddRange(new Drawable[] { @@ -48,7 +48,7 @@ namespace osu.Game.Overlays.Chat.Tabs Anchor = Anchor.Centre, Origin = Anchor.Centre, Masking = true, - Child = new DelayedLoadWrapper(avatar = new DrawableAvatar(value.Users.First()) + Child = new DelayedLoadWrapper(avatar = new ClickableAvatar(value.Users.First()) { RelativeSizeAxes = Axes.Both, OpenOnClick = { Value = false }, diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs similarity index 90% rename from osu.Game/Users/Drawables/DrawableAvatar.cs rename to osu.Game/Users/Drawables/ClickableAvatar.cs index 42d2dbb1c6..61af2d8e27 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -14,7 +14,7 @@ using osu.Game.Graphics.Containers; namespace osu.Game.Users.Drawables { [LongRunningLoad] - public class DrawableAvatar : Container + public class ClickableAvatar : Container { /// /// Whether to open the user's profile when clicked. @@ -27,10 +27,11 @@ namespace osu.Game.Users.Drawables private OsuGame game { get; set; } /// - /// An avatar for specified user. + /// A clickable avatar for specified user, with UI sounds included. + /// If is true, clicking will open the user's profile. /// /// The user. A null value will get a placeholder avatar. - public DrawableAvatar(User user = null) + public ClickableAvatar(User user = null) { this.user = user; } diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 171462f3fc..4772207edf 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -65,7 +65,7 @@ namespace osu.Game.Users.Drawables if (user == null && !ShowGuestOnNull) return null; - var avatar = new DrawableAvatar(user) + var avatar = new ClickableAvatar(user) { RelativeSizeAxes = Axes.Both, }; From e8f96b24013827b5c173159b960a0443d18ee87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Dec 2020 14:02:53 +0100 Subject: [PATCH 5722/6909] Bring back DrawableAvatar as a simple sprite --- osu.Game/Users/Drawables/ClickableAvatar.cs | 22 ++---------- osu.Game/Users/Drawables/DrawableAvatar.cs | 39 +++++++++++++++++++++ 2 files changed, 42 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Users/Drawables/DrawableAvatar.cs diff --git a/osu.Game/Users/Drawables/ClickableAvatar.cs b/osu.Game/Users/Drawables/ClickableAvatar.cs index 61af2d8e27..0fca9c7c9b 100644 --- a/osu.Game/Users/Drawables/ClickableAvatar.cs +++ b/osu.Game/Users/Drawables/ClickableAvatar.cs @@ -1,19 +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.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.Framework.Input.Events; using osu.Game.Graphics.Containers; namespace osu.Game.Users.Drawables { - [LongRunningLoad] public class ClickableAvatar : Container { /// @@ -27,7 +24,7 @@ namespace osu.Game.Users.Drawables private OsuGame game { get; set; } /// - /// A clickable avatar for specified user, with UI sounds included. + /// A clickable avatar for the specified user, with UI sounds included. /// If is true, clicking will open the user's profile. /// /// The user. A null value will get a placeholder avatar. @@ -39,28 +36,15 @@ namespace osu.Game.Users.Drawables [BackgroundDependencyLoader] private void load(LargeTextureStore textures) { - if (textures == null) - throw new ArgumentNullException(nameof(textures)); - - Texture texture = null; - if (user != null && user.Id > 1) texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); - texture ??= textures.Get(@"Online/avatar-guest"); - ClickableArea clickableArea; Add(clickableArea = new ClickableArea { RelativeSizeAxes = Axes.Both, - Child = new Sprite - { - RelativeSizeAxes = Axes.Both, - Texture = texture, - FillMode = FillMode.Fit, - Anchor = Anchor.Centre, - Origin = Anchor.Centre - }, Action = openProfile }); + LoadComponentAsync(new DrawableAvatar(user), clickableArea.Add); + clickableArea.Enabled.BindTo(OpenOnClick); } diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs new file mode 100644 index 0000000000..81f6ad52d9 --- /dev/null +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; + +namespace osu.Game.Users.Drawables +{ + [LongRunningLoad] + public class DrawableAvatar : Sprite + { + private readonly User user; + + /// + /// A simple, non-interactable avatar sprite for the specified user. + /// + /// The user. A null value will get a placeholder avatar. + public DrawableAvatar(User user = null) + { + this.user = user; + + RelativeSizeAxes = Axes.Both; + FillMode = FillMode.Fit; + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + } + + [BackgroundDependencyLoader] + private void load(LargeTextureStore textures) + { + if (user != null && user.Id > 1) + Texture = textures.Get($@"https://a.ppy.sh/{user.Id}"); + + Texture ??= textures.Get(@"Online/avatar-guest"); + } + } +} From 8ec7970b6ab10a028d4066571255587e95baf29f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Dec 2020 14:06:09 +0100 Subject: [PATCH 5723/6909] Move load-complete fade specification inside --- osu.Game/Users/Drawables/DrawableAvatar.cs | 6 ++++++ osu.Game/Users/Drawables/UpdateableAvatar.cs | 1 - 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Users/Drawables/DrawableAvatar.cs b/osu.Game/Users/Drawables/DrawableAvatar.cs index 81f6ad52d9..3dae3afe3f 100644 --- a/osu.Game/Users/Drawables/DrawableAvatar.cs +++ b/osu.Game/Users/Drawables/DrawableAvatar.cs @@ -35,5 +35,11 @@ namespace osu.Game.Users.Drawables Texture ??= textures.Get(@"Online/avatar-guest"); } + + protected override void LoadComplete() + { + base.LoadComplete(); + this.FadeInFromZero(300, Easing.OutQuint); + } } } diff --git a/osu.Game/Users/Drawables/UpdateableAvatar.cs b/osu.Game/Users/Drawables/UpdateableAvatar.cs index 4772207edf..927e48cb56 100644 --- a/osu.Game/Users/Drawables/UpdateableAvatar.cs +++ b/osu.Game/Users/Drawables/UpdateableAvatar.cs @@ -70,7 +70,6 @@ namespace osu.Game.Users.Drawables RelativeSizeAxes = Axes.Both, }; - avatar.OnLoadComplete += d => d.FadeInFromZero(300, Easing.OutQuint); avatar.OpenOnClick.BindTo(OpenOnClick); return avatar; From 15948de2f0cbe1c64abf0b6a4a9fba813c047a2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 26 Dec 2020 14:06:23 +0100 Subject: [PATCH 5724/6909] Fix gameplay leaderboard avatars being clickable --- .../Screens/Play/HUD/GameplayLeaderboardScore.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 58281debf1..51b19a8d45 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -78,6 +78,8 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(OsuColour colours) { + Container avatarContainer; + InternalChildren = new Drawable[] { mainFillContainer = new Container @@ -152,7 +154,7 @@ namespace osu.Game.Screens.Play.HUD Spacing = new Vector2(4f, 0f), Children = new Drawable[] { - new CircularContainer + avatarContainer = new CircularContainer { Masking = true, Anchor = Anchor.CentreLeft, @@ -166,11 +168,7 @@ namespace osu.Game.Screens.Play.HUD Alpha = 0.3f, RelativeSizeAxes = Axes.Both, Colour = colours.Gray4, - }, - new UpdateableAvatar(User) - { - RelativeSizeAxes = Axes.Both, - }, + } } }, usernameText = new OsuSpriteText @@ -227,6 +225,8 @@ namespace osu.Game.Screens.Play.HUD } }; + LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add); + TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); From 9e15dccc56bfcdde0f36a6c15db413faff07b06b Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 26 Dec 2020 15:36:21 +0100 Subject: [PATCH 5725/6909] Move graceful exit to OsuGameBase --- osu.Game/OsuGame.cs | 12 ------------ osu.Game/OsuGameBase.cs | 12 ++++++++++++ 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index bb638bcf3a..710dfd7031 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -918,18 +918,6 @@ namespace osu.Game return base.OnExiting(); } - /// - /// Use to programatically exit the game as if the user was triggering via alt-f4. - /// Will keep persisting until an exit occurs (exit may be blocked multiple times). - /// - public void GracefullyExit() - { - if (!OnExiting()) - Exit(); - else - Scheduler.AddDelayed(GracefullyExit, 2000); - } - protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index e7b5d3304d..91ab2bdc1e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -368,6 +368,18 @@ namespace osu.Game LocalConfig ??= new OsuConfigManager(Storage); } + /// + /// Use to programatically exit the game as if the user was triggering via alt-f4. + /// Will keep persisting until an exit occurs (exit may be blocked multiple times). + /// + public void GracefullyExit() + { + if (!OnExiting()) + Exit(); + else + Scheduler.AddDelayed(GracefullyExit, 2000); + } + protected override Storage CreateStorage(GameHost host, Storage defaultStorage) => new OsuStorage(host, defaultStorage); private readonly List fileImporters = new List(); From 8e428353ee0ae94e7efd7995ddbd01317842b7f7 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sat, 26 Dec 2020 15:44:59 +0100 Subject: [PATCH 5726/6909] Revise TournamentSwitcher to include a close button --- osu.Game.Tournament/Screens/SetupScreen.cs | 44 ++++++++++++++++++---- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 3c9d3c949b..de3321397e 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -42,6 +42,9 @@ namespace osu.Game.Tournament.Screens [Resolved] private RulesetStore rulesets { get; set; } + [Resolved] + private TournamentGameBase game { get; set; } + [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } @@ -68,9 +71,6 @@ namespace osu.Game.Tournament.Screens reload(); } - [Resolved] - private Framework.Game game { get; set; } - private void reload() { var fileBasedIpc = ipc as FileBasedIPC; @@ -115,12 +115,14 @@ namespace osu.Game.Tournament.Screens Items = rulesets.AvailableRulesets, Current = LadderInfo.Ruleset, }, - new LabelledDropdown + new TournamentSwitcher { Label = "Current tournament", Description = "Changes the background videos and bracket to match the selected tournament. This requires a restart to apply changes.", Items = storage.ListTournaments(), Current = storage.CurrentTournament, + ButtonText = "Close osu!", + Action = () => game.GracefullyExit() }, resolution = new ResolutionSelector { @@ -165,7 +167,7 @@ namespace osu.Game.Tournament.Screens private class ActionableInfo : LabelledDrawable { - private OsuButton button; + protected OsuButton Button; public ActionableInfo() : base(true) @@ -174,7 +176,7 @@ namespace osu.Game.Tournament.Screens public string ButtonText { - set => button.Text = value; + set => Button.Text = value; } public string Value @@ -211,7 +213,7 @@ namespace osu.Game.Tournament.Screens Spacing = new Vector2(10, 0), Children = new Drawable[] { - button = new TriangleButton + Button = new TriangleButton { Size = new Vector2(100, 40), Action = () => Action?.Invoke() @@ -222,6 +224,34 @@ namespace osu.Game.Tournament.Screens }; } + private class TournamentSwitcher : ActionableInfo + { + private OsuDropdown dropdown; + + public IEnumerable Items + { + get => dropdown.Items; + set => dropdown.Items = value; + } + + public Bindable Current + { + get => dropdown.Current; + set => dropdown.Current = value; + } + + protected override Drawable CreateComponent() + { + var drawable = base.CreateComponent(); + FlowContainer.Insert(-1, dropdown = new OsuDropdown + { + Width = 510 + }); + + return drawable; + } + } + private class ResolutionSelector : ActionableInfo { private const int minimum_window_height = 480; From fa0576f47f05faa4631e7a00a5a50b4c50d38eb1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Dec 2020 13:40:02 +0900 Subject: [PATCH 5727/6909] Move quit colour change implementation to updateColour for better coverage --- .../Play/HUD/GameplayLeaderboardScore.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index ed86f3241d..4aeb65bb01 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -231,15 +231,7 @@ namespace osu.Game.Screens.Play.HUD TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); - HasQuit.BindValueChanged(v => - { - if (v.NewValue) - { - // we will probably want to display this in a better way once we have a design. - // and also show states other than quit. - panelColour = Color4.Gray; - } - }, true); + HasQuit.BindValueChanged(v => updateColour()); } protected override void LoadComplete() @@ -255,7 +247,14 @@ namespace osu.Game.Screens.Play.HUD private void updateColour() { if (HasQuit.Value) + { + // we will probably want to display this in a better way once we have a design. + // and also show states other than quit. + mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic); + panelColour = Color4.Gray; + textColour = Color4.White; return; + } if (scorePosition == 1) { From d14a8d24b5c3cb7669a3b7537ba69820aa1e0fa4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Dec 2020 16:42:20 +0900 Subject: [PATCH 5728/6909] Remove assert for now --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 6b0ca4d74c..42df0cfe8c 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -165,8 +165,6 @@ namespace osu.Game.Screens.Play.HUD public void UpdateScore(ScoreProcessor processor, ScoringMode mode) { - Debug.Assert(!UserQuit.Value); - if (LastHeader == null) return; From 1b34f2115f6ef323aa66bf07d3e0b7ac05e76606 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 27 Dec 2020 16:57:23 +0900 Subject: [PATCH 5729/6909] Remove dignostics using --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 42df0cfe8c..00e2b8bfa7 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Specialized; -using System.Diagnostics; using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; From 6b6b1514e2b7b2249db579297cfe94aa44a8b52d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Dec 2020 12:58:37 +0100 Subject: [PATCH 5730/6909] Rename method to be less misleading As it doesn't only change colour, but also width. --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 4aeb65bb01..1bd279922c 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -52,7 +52,7 @@ namespace osu.Game.Screens.Play.HUD positionText.Text = $"#{scorePosition.Value.FormatRank()}"; positionText.FadeTo(scorePosition.HasValue ? 1 : 0); - updateColour(); + updateState(); } } @@ -231,20 +231,20 @@ namespace osu.Game.Screens.Play.HUD TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); - HasQuit.BindValueChanged(v => updateColour()); + HasQuit.BindValueChanged(v => updateState()); } protected override void LoadComplete() { base.LoadComplete(); - updateColour(); + updateState(); FinishTransforms(true); } private const double panel_transition_duration = 500; - private void updateColour() + private void updateState() { if (HasQuit.Value) { From f75dccc9e4872477c66a484ddc13ef943511e43f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 27 Dec 2020 13:00:27 +0100 Subject: [PATCH 5731/6909] Explicitly use discard in value changed callback --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 1bd279922c..43e259695e 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -231,7 +231,7 @@ namespace osu.Game.Screens.Play.HUD TotalScore.BindValueChanged(v => scoreText.Text = v.NewValue.ToString("N0"), true); Accuracy.BindValueChanged(v => accuracyText.Text = v.NewValue.FormatAccuracy(), true); Combo.BindValueChanged(v => comboText.Text = $"{v.NewValue}x", true); - HasQuit.BindValueChanged(v => updateState()); + HasQuit.BindValueChanged(_ => updateState()); } protected override void LoadComplete() From e9e0e18dc53a83c18c6aff7a2aac27b92445c1cb Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 27 Dec 2020 14:11:30 +0100 Subject: [PATCH 5732/6909] Fix missed change in merge conflict... --- 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 c397608bc6..7f2ef82d12 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -54,7 +54,7 @@ namespace osu.Android { case Intent.ActionDefault: if (intent.Scheme == ContentResolver.SchemeContent) - handleImportFromUri(intent.Data); + handleImportFromUris(intent.Data); else if (osu_url_schemes.Contains(intent.Scheme)) game.HandleLink(intent.DataString); break; From 4d61c143db058bcc47bac98490dbaa246e873872 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Dec 2020 14:56:29 +0900 Subject: [PATCH 5733/6909] Fix lookup cache throwing a null reference if no matches were successful --- osu.Game/Database/UserLookupCache.cs | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 05d6930992..e7ddf4c567 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -72,6 +72,7 @@ namespace osu.Game.Database var request = new GetUsersRequest(userTasks.Keys.ToArray()); // rather than queueing, we maintain our own single-threaded request stream. + // todo: we probably want retry logic here. api.Perform(request); // Create a new request task if there's still more users to query. @@ -82,14 +83,19 @@ namespace osu.Game.Database createNewTask(); } - foreach (var user in request.Result.Users) - { - if (userTasks.TryGetValue(user.Id, out var tasks)) - { - foreach (var task in tasks) - task.SetResult(user); + List foundUsers = request.Result?.Users; - userTasks.Remove(user.Id); + if (foundUsers != null) + { + foreach (var user in foundUsers) + { + if (userTasks.TryGetValue(user.Id, out var tasks)) + { + foreach (var task in tasks) + task.SetResult(user); + + userTasks.Remove(user.Id); + } } } From 046a76cb1d1597414ae08e9e47a2dfa1dcac8e09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Dec 2020 14:56:53 +0900 Subject: [PATCH 5734/6909] Allow null users to still be displayed in the participant list The fix here is correcting the access of `user.Country`. The deicision to have null users display is because this is the best we can do (if osu-web could not resolve the user). We still want the users in the lobby to be aware of this user's presence, rather than hiding them from view. osu-stable does a similar thing, showing these users as `[Loading]`. I decided to go with blank names instead because having *any* text there causes confusion. We can iterate on this in future design updates. --- .../TestSceneMultiplayerParticipantsList.cs | 10 ++++++++++ .../Multiplayer/Participants/ParticipantPanel.cs | 13 ++++++------- .../Visual/Multiplayer/TestMultiplayerClient.cs | 2 ++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 9181170bee..968a869532 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -43,6 +43,16 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); } + [Test] + public void TestAddNullUser() + { + AddAssert("one unique panel", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 1); + + AddStep("add non-resolvable user", () => Client.AddNullUser(-3)); + + AddUntilStep("two unique panels", () => this.ChildrenOfType().Select(p => p.User).Distinct().Count() == 2); + } + [Test] public void TestRemoveUser() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index de3069b2f6..f99655e305 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -45,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants [BackgroundDependencyLoader] private void load() { - Debug.Assert(User.User != null); + var user = User.User; var backgroundColour = Color4Extensions.FromHex("#33413C"); @@ -82,7 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Origin = Anchor.CentreRight, RelativeSizeAxes = Axes.Both, Width = 0.75f, - User = User.User, + User = user, Colour = ColourInfo.GradientHorizontal(Color4.White.Opacity(0), Color4.White.Opacity(0.25f)) }, new FillFlowContainer @@ -98,28 +97,28 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.Both, FillMode = FillMode.Fit, - User = User.User + User = user }, new UpdateableFlag { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Size = new Vector2(30, 20), - Country = User.User.Country + Country = user?.Country }, new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), - Text = User.User.Username + Text = user?.Username }, new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 14), - Text = User.User.CurrentModeRank != null ? $"#{User.User.CurrentModeRank}" : string.Empty + Text = user?.CurrentModeRank != null ? $"#{user.CurrentModeRank}" : string.Empty } } }, diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 9a839c8d22..2ce5211757 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -28,6 +28,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public void AddUser(User user) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(user.Id) { User = user }); + public void AddNullUser(int userId) => ((IMultiplayerClient)this).UserJoined(new MultiplayerRoomUser(userId)); + public void RemoveUser(User user) { Debug.Assert(Room != null); From bdbc210f6d6cbea01a5d8106b31c34be5c43955c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Dec 2020 16:51:45 +0900 Subject: [PATCH 5735/6909] Update fastlane and dependencies --- Gemfile.lock | 56 +++++++++++++++++++++++++++------------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a4b49af7e4..8ac863c9a8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,27 +1,27 @@ GEM remote: https://rubygems.org/ specs: - CFPropertyList (3.0.2) + CFPropertyList (3.0.3) addressable (2.7.0) public_suffix (>= 2.0.2, < 5.0) atomos (0.1.3) aws-eventstream (1.1.0) - aws-partitions (1.354.0) - aws-sdk-core (3.104.3) + aws-partitions (1.413.0) + aws-sdk-core (3.110.0) aws-eventstream (~> 1, >= 1.0.2) aws-partitions (~> 1, >= 1.239.0) aws-sigv4 (~> 1.1) jmespath (~> 1.0) - aws-sdk-kms (1.36.0) - aws-sdk-core (~> 3, >= 3.99.0) + aws-sdk-kms (1.40.0) + aws-sdk-core (~> 3, >= 3.109.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.78.0) - aws-sdk-core (~> 3, >= 3.104.3) + aws-sdk-s3 (1.87.0) + aws-sdk-core (~> 3, >= 3.109.0) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.1) - aws-sigv4 (1.2.1) + aws-sigv4 (1.2.2) aws-eventstream (~> 1, >= 1.0.2) - babosa (1.0.3) + babosa (1.0.4) claide (1.0.3) colored (1.2) colored2 (3.1.2) @@ -29,22 +29,23 @@ GEM highline (~> 1.7.2) declarative (0.0.20) declarative-option (0.1.0) - digest-crc (0.6.1) - rake (~> 13.0) + digest-crc (0.6.3) + rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.7.6) - emoji_regex (3.0.0) - excon (0.76.0) - faraday (1.0.1) + emoji_regex (3.2.1) + excon (0.78.1) + faraday (1.2.0) multipart-post (>= 1.2, < 3) - faraday-cookie_jar (0.0.6) - faraday (>= 0.7.4) + ruby2_keywords + faraday-cookie_jar (0.0.7) + faraday (>= 0.8.0) http-cookie (~> 1.0.0) faraday_middleware (1.0.0) faraday (~> 1.0) - fastimage (2.2.0) - fastlane (2.156.0) + fastimage (2.2.1) + fastlane (2.170.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) aws-sdk-s3 (~> 1.0) @@ -96,17 +97,17 @@ GEM google-cloud-core (1.5.0) google-cloud-env (~> 1.0) google-cloud-errors (~> 1.0) - google-cloud-env (1.3.3) + google-cloud-env (1.4.0) faraday (>= 0.17.3, < 2.0) google-cloud-errors (1.0.1) - google-cloud-storage (1.27.0) + google-cloud-storage (1.29.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.13.1) + googleauth (0.14.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) memoist (~> 0.16) @@ -118,10 +119,10 @@ GEM domain_name (~> 0.5) httpclient (2.8.3) jmespath (1.4.0) - json (2.3.1) - jwt (2.2.1) + json (2.5.1) + jwt (2.2.2) memoist (0.16.2) - mini_magick (4.10.1) + mini_magick (4.11.0) mini_mime (1.0.2) mini_portile2 (2.4.0) multi_json (1.15.0) @@ -132,14 +133,15 @@ GEM mini_portile2 (~> 2.4.0) os (1.1.1) plist (3.5.0) - public_suffix (4.0.5) - rake (13.0.1) + public_suffix (4.0.6) + rake (13.0.3) representable (3.0.4) declarative (< 0.1.0) declarative-option (< 0.2.0) uber (< 0.2.0) retriable (3.1.2) rouge (2.0.7) + ruby2_keywords (0.0.2) rubyzip (2.3.0) security (0.1.3) signet (0.14.0) @@ -168,7 +170,7 @@ GEM unf_ext (0.0.7.7) unicode-display_width (1.7.0) word_wrap (1.0.0) - xcodeproj (1.18.0) + xcodeproj (1.19.0) CFPropertyList (>= 2.3.3, < 4.0) atomos (~> 0.1.3) claide (>= 1.0.2, < 2.0) From 2e3537e9664d24dbb6f5fec80e0c9a3e42cb1664 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Dec 2020 16:52:54 +0900 Subject: [PATCH 5736/6909] 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 fc01f9bf1d..cd2ce58c55 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 cbf9f6f1bd..3e1b56c29c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index adbcc0ef1c..85ba0590ea 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 5ca9a6a98018544d67f61ef1912596084a70999a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Dec 2020 20:05:48 +0900 Subject: [PATCH 5737/6909] Add xmldoc on UserLookupCache's lookup method --- osu.Game/Database/UserLookupCache.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 05d6930992..8a99e27708 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -17,6 +17,12 @@ namespace osu.Game.Database [Resolved] private IAPIProvider api { get; set; } + /// + /// Perform an API lookup on the specified user, populating a model. + /// + /// The user to lookup. + /// An optional cancellation token. + /// The populated user, or null if the user does not exist or the request could not be satisfied. public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token); protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) From 545dcac4ec0950f2ade486c55a88f9f39b1802ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Dec 2020 20:13:24 +0900 Subject: [PATCH 5738/6909] Add null hinting on UserLookupCache query method --- osu.Game/Database/UserLookupCache.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 8a99e27708..49ea82a4fe 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -23,6 +24,7 @@ namespace osu.Game.Database /// The user to lookup. /// An optional cancellation token. /// The populated user, or null if the user does not exist or the request could not be satisfied. + [ItemCanBeNull] public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token); protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) From 447a55ce11cf144eddc85dea8e37d99c2dbb46e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Dec 2020 20:16:53 +0900 Subject: [PATCH 5739/6909] Fix incorrect null handling in GameplayLeaderboard --- osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 6 ++++-- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index 83b70911c6..cb20deb272 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.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 osu.Framework.Extensions.Color4Extensions; @@ -56,6 +57,7 @@ namespace osu.Game.Screens.Play.HUD } } + [CanBeNull] public User User { get; } private readonly bool trackedPlayer; @@ -68,7 +70,7 @@ namespace osu.Game.Screens.Play.HUD /// /// The score's player. /// Whether the player is the local user or a replay player. - public GameplayLeaderboardScore(User user, bool trackedPlayer) + public GameplayLeaderboardScore([CanBeNull] User user, bool trackedPlayer) { User = user; this.trackedPlayer = trackedPlayer; @@ -180,7 +182,7 @@ namespace osu.Game.Screens.Play.HUD Origin = Anchor.CentreLeft, Colour = Color4.White, Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), - Text = User.Username, + Text = User?.Username, Truncate = true, Shadow = false, } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index 00e2b8bfa7..e7e5459f76 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.Play.HUD var trackedUser = new TrackedUserData(); userScores[userId] = trackedUser; - var leaderboardScore = AddPlayer(resolvedUser, resolvedUser.Id == api.LocalUser.Value.Id); + var leaderboardScore = AddPlayer(resolvedUser, resolvedUser?.Id == api.LocalUser.Value.Id); ((IBindable)leaderboardScore.Accuracy).BindTo(trackedUser.Accuracy); ((IBindable)leaderboardScore.TotalScore).BindTo(trackedUser.Score); From 8f0413472cfd57b480d0feedd365a5ce22aae3ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Dec 2020 20:30:08 +0900 Subject: [PATCH 5740/6909] Add test coverage of null users in scoreboard --- .../Visual/Online/TestSceneCurrentlyPlayingDisplay.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs index 1666c9cde4..1baa07f208 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneCurrentlyPlayingDisplay.cs @@ -90,11 +90,17 @@ namespace osu.Game.Tests.Visual.Online }; protected override Task ComputeValueAsync(int lookup, CancellationToken token = default) - => Task.FromResult(new User + { + // tests against failed lookups + if (lookup == 13) + return Task.FromResult(null); + + return Task.FromResult(new User { Id = lookup, Username = usernames[lookup % usernames.Length], }); + } } } } From 6254907ef984032f158bb0a3164862cc13a5d435 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Dec 2020 20:31:08 +0900 Subject: [PATCH 5741/6909] Move multiplayer leaderboard test to correct namespace --- .../TestSceneMultiplayerGameplayLeaderboard.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) rename osu.Game.Tests/Visual/{Gameplay => Multiplayer}/TestSceneMultiplayerGameplayLeaderboard.cs (98%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs similarity index 98% rename from osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs rename to osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 975c54c3f6..d0b1e77549 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -20,10 +20,9 @@ using osu.Game.Rulesets.Osu.Scoring; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using osu.Game.Screens.Play.HUD; -using osu.Game.Tests.Visual.Multiplayer; using osu.Game.Tests.Visual.Online; -namespace osu.Game.Tests.Visual.Gameplay +namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayerGameplayLeaderboard : MultiplayerTestScene { From fb21b7c0167bed184df6c71dcfcd7a1f29c3ecfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 12:09:32 +0100 Subject: [PATCH 5742/6909] Add failing test cases --- .../TestSceneMultiplayerMatchSongSelect.cs | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs new file mode 100644 index 0000000000..95c333e9f4 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -0,0 +1,145 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Extensions; +using osu.Framework.Platform; +using osu.Framework.Screens; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Taiko; +using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Select; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneMultiplayerMatchSongSelect : RoomTestScene + { + private BeatmapManager manager; + private RulesetStore rulesets; + + private List beatmaps; + + private TestMultiplayerMatchSongSelect songSelect; + + [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)); + + beatmaps = new List(); + + for (int i = 0; i < 8; ++i) + { + int beatmapId = 10 * 10 + i; + + int length = RNG.Next(30000, 200000); + double bpm = RNG.NextSingle(80, 200); + + beatmaps.Add(new BeatmapInfo + { + Ruleset = rulesets.GetRuleset(i % 4), + OnlineBeatmapID = beatmapId, + Length = length, + BPM = bpm, + BaseDifficulty = new BeatmapDifficulty() + }); + } + + manager.Import(new BeatmapSetInfo + { + OnlineBeatmapSetID = 10, + Hash = Guid.NewGuid().ToString().ComputeMD5Hash(), + Metadata = new BeatmapMetadata + { + Artist = "Some Artist", + Title = "Some Beatmap", + AuthorString = "Some Author" + }, + Beatmaps = beatmaps, + DateAdded = DateTimeOffset.UtcNow + }).Wait(); + } + + public override void SetUpSteps() + { + base.SetUpSteps(); + + AddStep("reset", () => + { + Ruleset.Value = new OsuRuleset().RulesetInfo; + Beatmap.SetDefault(); + SelectedMods.SetDefault(); + }); + + AddStep("create song select", () => LoadScreen(songSelect = new TestMultiplayerMatchSongSelect())); + AddUntilStep("wait for present", () => songSelect.IsCurrentScreen()); + } + + [Test] + public void TestBeatmapRevertedOnExitIfNoSelection() + { + BeatmapInfo selectedBeatmap = null; + + AddStep("select beatmap", + () => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.Where(beatmap => beatmap.RulesetID == new OsuRuleset().LegacyID).ElementAt(1))); + AddUntilStep("wait for selection", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap)); + + AddStep("exit song select", () => songSelect.Exit()); + AddAssert("beatmap reverted", () => Beatmap.IsDefault); + } + + [Test] + public void TestModsRevertedOnExitIfNoSelection() + { + AddStep("change mods", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); + + AddStep("exit song select", () => songSelect.Exit()); + AddAssert("mods reverted", () => SelectedMods.Value.Count == 0); + } + + [Test] + public void TestRulesetRevertedOnExitIfNoSelection() + { + AddStep("change ruleset", () => Ruleset.Value = new CatchRuleset().RulesetInfo); + + AddStep("exit song select", () => songSelect.Exit()); + AddAssert("ruleset reverted", () => Ruleset.Value.Equals(new OsuRuleset().RulesetInfo)); + } + + [Test] + public void TestBeatmapConfirmed() + { + BeatmapInfo selectedBeatmap = null; + + AddStep("change ruleset", () => Ruleset.Value = new TaikoRuleset().RulesetInfo); + AddStep("select beatmap", + () => songSelect.Carousel.SelectBeatmap(selectedBeatmap = beatmaps.First(beatmap => beatmap.RulesetID == new TaikoRuleset().LegacyID))); + AddUntilStep("wait for selection", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap)); + AddStep("set mods", () => SelectedMods.Value = new[] { new TaikoModDoubleTime() }); + + AddStep("confirm selection", () => songSelect.FinaliseSelection()); + AddStep("exit song select", () => songSelect.Exit()); + + AddAssert("beatmap not changed", () => Beatmap.Value.BeatmapInfo.Equals(selectedBeatmap)); + AddAssert("ruleset not changed", () => Ruleset.Value.Equals(new TaikoRuleset().RulesetInfo)); + AddAssert("mods not changed", () => SelectedMods.Value.Single() is TaikoModDoubleTime); + } + + private class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelect + { + public new BeatmapCarousel Carousel => base.Carousel; + } + } +} From f16b516e5880d8d27a439ab0a6615535879eee34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 12:32:06 +0100 Subject: [PATCH 5743/6909] Revert user changes if no selection was made --- .../Multiplayer/MultiplayerMatchSongSelect.cs | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 0842574f54..72539a2e3a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.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 Humanizer; using osu.Framework.Allocation; @@ -8,9 +9,12 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -29,6 +33,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private LoadingLayer loadingLayer; + private WorkingBeatmap initialBeatmap; + private RulesetInfo initialRuleset; + private IReadOnlyList initialMods; + + private bool itemSelected; + public MultiplayerMatchSongSelect() { Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; @@ -38,10 +48,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void load() { AddInternal(loadingLayer = new LoadingLayer(Carousel)); + initialBeatmap = Beatmap.Value; + initialRuleset = Ruleset.Value; + initialMods = Mods.Value.ToList(); } protected override bool OnStart() { + itemSelected = true; var item = new PlaylistItem(); item.Beatmap.Value = Beatmap.Value.BeatmapInfo; @@ -82,6 +96,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return true; } + public override bool OnExiting(IScreen next) + { + if (!itemSelected) + { + Beatmap.Value = initialBeatmap; + Ruleset.Value = initialRuleset; + Mods.Value = initialMods; + } + + return base.OnExiting(next); + } + protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); } } From 92d74a9343df3fd7378205fe26ff1753ebf52134 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 28 Dec 2020 20:48:14 +0900 Subject: [PATCH 5744/6909] Fix potential nullref in test scene --- osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs index c0a021436e..17fe09f2c6 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplayLeaderboard.cs @@ -95,7 +95,7 @@ namespace osu.Game.Tests.Visual.Gameplay { public bool CheckPositionByUsername(string username, int? expectedPosition) { - var scoreItem = this.FirstOrDefault(i => i.User.Username == username); + var scoreItem = this.FirstOrDefault(i => i.User?.Username == username); return scoreItem != null && scoreItem.ScorePosition == expectedPosition; } From a9822800fc368986a152723b883d9842b920f54c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 13:00:05 +0100 Subject: [PATCH 5745/6909] Add more null hinting in GameplayLeaderboard --- osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index e33cc05e64..7b94bf19ec 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -42,7 +43,7 @@ namespace osu.Game.Screens.Play.HUD /// Whether the player should be tracked on the leaderboard. /// Set to true for the local player or a player whose replay is currently being played. /// - public ILeaderboardScore AddPlayer(User user, bool isTracked) + public ILeaderboardScore AddPlayer([CanBeNull] User user, bool isTracked) { var drawable = new GameplayLeaderboardScore(user, isTracked) { From b352c1503ff2eff470dd573d08ad8e2015c07906 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Mon, 28 Dec 2020 15:13:33 +0100 Subject: [PATCH 5746/6909] Fix IntentFilter capturing all file types Removed string arrays and split the IntentFilter into multiple. Also added DataHost and DataMimeType --- osu.Android/OsuGameActivity.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 953c06f4e2..7abaff3cdb 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -16,7 +16,10 @@ using osu.Framework.Android; namespace osu.Android { [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] - [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataPathPatterns = new[] { ".*\\.osz", ".*\\.osk" }, DataMimeType = "application/*")] + [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] + [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] + [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "file", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] + [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "file", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { From d971aa5295b7d20a471d8be62fdc4c6e263dd139 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Mon, 28 Dec 2020 15:54:21 +0100 Subject: [PATCH 5747/6909] Remove file intents and add Send intent Removed IntentFilters with DataScheme = "file" Added Intent.ActionSend with application/octet-stream and application/zip --- osu.Android/OsuGameActivity.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 7abaff3cdb..cdf033e685 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -16,10 +16,10 @@ using osu.Framework.Android; namespace osu.Android { [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] - [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] - [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] - [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "file", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] - [IntentFilter(new[] { Intent.ActionDefault, Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "file", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] + [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] + [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] + [IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "application/octet-stream")] + [IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "application/zip")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { From d2301068b6c2a04961986570075ef5075c2cf168 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 16:35:33 +0100 Subject: [PATCH 5748/6909] Fix changelog header staying dimmed after build show --- .../Overlays/Changelog/ChangelogUpdateStreamControl.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs index 6bbff045b5..aa36a5c8fd 100644 --- a/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs +++ b/osu.Game/Overlays/Changelog/ChangelogUpdateStreamControl.cs @@ -7,12 +7,11 @@ namespace osu.Game.Overlays.Changelog { public class ChangelogUpdateStreamControl : OverlayStreamControl { - protected override OverlayStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value); - - protected override void LoadComplete() + public ChangelogUpdateStreamControl() { - // suppress base logic of immediately selecting first item if one exists - // (we always want to start with no stream selected). + SelectFirstTabByDefault = false; } + + protected override OverlayStreamItem CreateStreamItem(APIUpdateStream value) => new ChangelogUpdateStreamItem(value); } } From 1d311a66805aad4c784ff0a51bc523e69019ccde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 19:11:44 +0100 Subject: [PATCH 5749/6909] Change PlayingUsers population logic to match expectations --- .../Multiplayer/StatefulMultiplayerClient.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index fcb0977f53..c15401e99d 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -133,6 +133,7 @@ namespace osu.Game.Online.Multiplayer apiRoom = null; Room = null; + PlayingUsers.Clear(); RoomUpdated?.Invoke(); }, false); @@ -302,8 +303,7 @@ namespace osu.Game.Online.Multiplayer Room.Users.Single(u => u.UserID == userId).State = state; - if (state != MultiplayerUserState.Playing) - PlayingUsers.Remove(userId); + updatePlayingUsers(userId, state); RoomUpdated?.Invoke(); }, false); @@ -337,8 +337,6 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return; - PlayingUsers.AddRange(Room.Users.Where(u => u.State == MultiplayerUserState.Playing).Select(u => u.UserID)); - MatchStarted?.Invoke(); }, false); @@ -454,5 +452,17 @@ namespace osu.Game.Online.Multiplayer apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. apiRoom.Playlist.Add(playlistItem); } + + private void updatePlayingUsers(int userId, MultiplayerUserState state) + { + bool isPlaying = state >= MultiplayerUserState.WaitingForLoad && state <= MultiplayerUserState.FinishedPlay; + bool wasPlaying = PlayingUsers.Contains(userId); + + if (!wasPlaying && isPlaying) + PlayingUsers.Add(userId); + + if (wasPlaying && !isPlaying) + PlayingUsers.Remove(userId); + } } } From a014d0ec18953854650c33263489273171d828f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 19:12:58 +0100 Subject: [PATCH 5750/6909] Use PlayingUsers when constructing player directly --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 58314c3774..ba0ed16cf4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -200,7 +200,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { Debug.Assert(client.Room != null); - int[] userIds = client.Room.Users.Where(u => u.State >= MultiplayerUserState.WaitingForLoad).Select(u => u.UserID).ToArray(); + int[] userIds = client.PlayingUsers.ToArray(); StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); } From f7407347f78445a141c196fbcda5282f1ba14250 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 20:01:17 +0100 Subject: [PATCH 5751/6909] Add test coverage of PlayingUsers tracking --- .../StatefulMultiplayerClientTest.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs new file mode 100644 index 0000000000..8d543e7485 --- /dev/null +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.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 Humanizer; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Online.Multiplayer; +using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Tests.NonVisual.Multiplayer +{ + [HeadlessTest] + public class StatefulMultiplayerClientTest : MultiplayerTestScene + { + [Test] + public void TestPlayingUserTracking() + { + int id = 2000; + + AddRepeatStep("add some users", () => Client.AddUser(new User { Id = id++ }), 5); + checkPlayingUserCount(0); + + changeState(3, MultiplayerUserState.WaitingForLoad); + checkPlayingUserCount(3); + + changeState(3, MultiplayerUserState.Playing); + checkPlayingUserCount(3); + + changeState(3, MultiplayerUserState.Results); + checkPlayingUserCount(0); + + changeState(6, MultiplayerUserState.WaitingForLoad); + checkPlayingUserCount(6); + + AddStep("another user left", () => Client.RemoveUser(Client.Room?.Users.Last().User)); + checkPlayingUserCount(5); + + AddStep("leave room", () => Client.LeaveRoom()); + checkPlayingUserCount(0); + } + + private void checkPlayingUserCount(int expectedCount) + => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => Client.PlayingUsers.Count == expectedCount); + + private void changeState(int userCount, MultiplayerUserState state) + => AddStep($"{"user".ToQuantity(userCount)} in {state}", () => + { + for (int i = 0; i < userCount; ++i) + { + var userId = Client.Room?.Users[i].UserID ?? throw new AssertionException("Room cannot be null!"); + Client.ChangeUserState(userId, state); + } + }); + } +} From 770a5a85dff5cff621287732639eb33534ea95ad Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Mon, 28 Dec 2020 20:57:08 +0100 Subject: [PATCH 5752/6909] Merge Intent.ActionSend into one IntentFilter Co-authored-by: Lucas A. --- 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 cdf033e685..da69b516dd 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -19,7 +19,7 @@ namespace osu.Android [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "application/octet-stream")] - [IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "application/zip")] + [IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { From 9e6994166c0eef43cb63b6d1d34e70e0cb324634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 20:59:12 +0100 Subject: [PATCH 5753/6909] Add helper to track ongoing operations in UI --- .../OnlinePlay/OngoingOperationTracker.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs new file mode 100644 index 0000000000..f2d943e14f --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.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.Bindables; + +namespace osu.Game.Screens.OnlinePlay +{ + /// + /// Utility class to track ongoing online operations' progress. + /// Can be used to disable interactivity while waiting for a response from online sources. + /// + public class OngoingOperationTracker + { + /// + /// Whether there is an online operation in progress. + /// + public IBindable InProgress => inProgress; + + private readonly Bindable inProgress = new BindableBool(); + + private LeasedBindable leasedInProgress; + + /// + /// Begins tracking a new online operation. + /// + /// An operation has already been started. + public void BeginOperation() + { + if (leasedInProgress != null) + throw new InvalidOperationException("Cannot begin operation while another is in progress."); + + leasedInProgress = inProgress.BeginLease(true); + leasedInProgress.Value = true; + } + + /// + /// Ends tracking an online operation. + /// Does nothing if an operation has not been begun yet. + /// + public void EndOperation() + { + leasedInProgress?.Return(); + leasedInProgress = null; + } + } +} From 47ab7c9fd67270079d39e8ad9f45fb3e986c76df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 20:59:38 +0100 Subject: [PATCH 5754/6909] Disable ready button after host click --- .../TestSceneMultiplayerReadyButton.cs | 22 +++++++++++++++---- .../Match/MultiplayerReadyButton.cs | 13 ++++++++++- .../Multiplayer/MultiplayerMatchSubScreen.cs | 4 ++++ 3 files changed, 34 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 6b11613f1c..958c6d218b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -7,11 +7,14 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Resources; using osu.Game.Users; @@ -27,6 +30,9 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps; private RulesetStore rulesets; + [Cached] + private OngoingOperationTracker gameplayStartTracker = new OngoingOperationTracker(); + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -89,8 +95,7 @@ namespace osu.Game.Tests.Visual.Multiplayer addClickButtonStep(); AddAssert("user is ready", () => Client.Room?.Users[0].State == MultiplayerUserState.Ready); - addClickButtonStep(); - AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + verifyGameplayStartFlow(); } [Test] @@ -105,8 +110,7 @@ namespace osu.Game.Tests.Visual.Multiplayer addClickButtonStep(); AddStep("make user host", () => Client.TransferHost(Client.Room?.Users[0].UserID ?? 0)); - addClickButtonStep(); - AddAssert("match started", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + verifyGameplayStartFlow(); } [Test] @@ -160,5 +164,15 @@ namespace osu.Game.Tests.Visual.Multiplayer InputManager.MoveMouseTo(button); InputManager.Click(MouseButton.Left); }); + + private void verifyGameplayStartFlow() + { + addClickButtonStep(); + AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); + AddAssert("ready button disabled", () => !button.ChildrenOfType().Single().Enabled.Value); + + AddStep("transitioned to gameplay", () => gameplayStartTracker.EndOperation()); + AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 281e92404c..5009b435f4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -33,11 +33,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private OsuColour colours { get; set; } + [Resolved] + private OngoingOperationTracker gameplayStartTracker { get; set; } + private SampleChannel sampleReadyCount; private readonly ButtonWithTrianglesExposed button; private int countReady; + private IBindable gameplayStartInProgress; public MultiplayerReadyButton() { @@ -54,6 +58,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void load(AudioManager audio) { sampleReadyCount = audio.Samples.Get(@"SongSelect/select-difficulty"); + + gameplayStartInProgress = gameplayStartTracker.InProgress.GetBoundCopy(); + gameplayStartInProgress.BindValueChanged(_ => updateState()); } protected override void OnRoomUpdated() @@ -63,7 +70,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match // this method is called on leaving the room, so the local user may not exist in the room any more. localUser = Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); - button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open; updateState(); } @@ -100,6 +106,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match break; } + button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !gameplayStartInProgress.Value; + if (newCountReady != countReady) { countReady = newCountReady; @@ -142,7 +150,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match else { if (Room?.Host?.Equals(localUser) == true) + { + gameplayStartTracker.BeginOperation(); Client.StartMatch().CatchUnobservedExceptions(true); + } else Client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 58314c3774..93db913ce0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -31,6 +31,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private StatefulMultiplayerClient client { get; set; } + [Cached] + private OngoingOperationTracker gameplayStartTracker = new OngoingOperationTracker(); + private MultiplayerMatchSettingsOverlay settingsOverlay; private IBindable isConnected; @@ -203,6 +206,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer int[] userIds = client.Room.Users.Where(u => u.State >= MultiplayerUserState.WaitingForLoad).Select(u => u.UserID).ToArray(); StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); + gameplayStartTracker.EndOperation(); } protected override void Dispose(bool isDisposing) From af66e4531196853215e8625cf858d4a0d221aaec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 21:39:11 +0100 Subject: [PATCH 5755/6909] Disable create room button after triggering join --- .../TestSceneCreateMultiplayerMatchButton.cs | 52 +++++++++++++++++++ .../OnlinePlay/Lounge/LoungeSubScreen.cs | 22 ++++---- .../CreateMultiplayerMatchButton.cs | 19 ++++++- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 3 ++ 4 files changed, 83 insertions(+), 13 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs new file mode 100644 index 0000000000..2a549e5262 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs @@ -0,0 +1,52 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Screens.OnlinePlay.Multiplayer; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneCreateMultiplayerMatchButton : MultiplayerTestScene + { + [Cached] + private OngoingOperationTracker joiningRoomTracker = new OngoingOperationTracker(); + + private CreateMultiplayerMatchButton button; + + public override void SetUpSteps() + { + base.SetUpSteps(); + AddStep("create button", () => Child = button = new CreateMultiplayerMatchButton + { + Width = 200, + Height = 100, + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); + } + + [Test] + public void TestButtonEnableStateChanges() + { + assertButtonEnableState(true); + + AddStep("begin joining room", () => joiningRoomTracker.BeginOperation()); + assertButtonEnableState(false); + + AddStep("end joining room", () => joiningRoomTracker.EndOperation()); + assertButtonEnableState(true); + + AddStep("disconnect client", () => Client.Disconnect()); + assertButtonEnableState(false); + + AddStep("re-connect client", () => Client.Connect()); + assertButtonEnableState(true); + } + + private void assertButtonEnableState(bool enabled) + => AddAssert($"button {(enabled ? "enabled" : "disabled")}", () => button.Enabled.Value == enabled); + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 79f5dfdee1..2730693f73 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -26,6 +26,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); private readonly IBindable initialRoomsReceived = new Bindable(); + private readonly IBindable joiningRoom = new Bindable(); private FilterControl filter; private Container content; @@ -37,7 +38,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] private MusicController music { get; set; } - private bool joiningRoom; + [Resolved] + private OngoingOperationTracker joiningRoomTracker { get; set; } [BackgroundDependencyLoader] private void load() @@ -98,7 +100,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge base.LoadComplete(); initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived); - initialRoomsReceived.BindValueChanged(onInitialRoomsReceivedChanged, true); + initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer()); + + joiningRoom.BindTo(joiningRoomTracker.InProgress); + joiningRoom.BindValueChanged(_ => updateLoadingLayer(), true); } protected override void UpdateAfterChildren() @@ -156,26 +161,21 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void joinRequested(Room room) { - joiningRoom = true; - updateLoadingLayer(); + joiningRoomTracker.BeginOperation(); RoomManager?.JoinRoom(room, r => { Open(room); - joiningRoom = false; - updateLoadingLayer(); + joiningRoomTracker.EndOperation(); }, _ => { - joiningRoom = false; - updateLoadingLayer(); + joiningRoomTracker.EndOperation(); }); } - private void onInitialRoomsReceivedChanged(ValueChangedEvent received) => updateLoadingLayer(); - private void updateLoadingLayer() { - if (joiningRoom || !initialRoomsReceived.Value) + if (joiningRoom.Value || !initialRoomsReceived.Value) loadingLayer.Show(); else loadingLayer.Hide(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs index 163efd9c20..7518a4e71b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs @@ -10,14 +10,29 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class CreateMultiplayerMatchButton : PurpleTriangleButton { + private IBindable isConnected; + private IBindable joiningRoom; + + [Resolved] + private StatefulMultiplayerClient multiplayerClient { get; set; } + + [Resolved] + private OngoingOperationTracker joiningRoomTracker { get; set; } + [BackgroundDependencyLoader] - private void load(StatefulMultiplayerClient multiplayerClient) + private void load() { Triangles.TriangleScale = 1.5f; Text = "Create room"; - ((IBindable)Enabled).BindTo(multiplayerClient.IsConnected); + isConnected = multiplayerClient.IsConnected.GetBoundCopy(); + isConnected.BindValueChanged(_ => updateState()); + + joiningRoom = joiningRoomTracker.InProgress.GetBoundCopy(); + joiningRoom.BindValueChanged(_ => updateState(), true); } + + private void updateState() => Enabled.Value = isConnected.Value && !joiningRoom.Value; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 4074dd1573..92665c498b 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -52,6 +52,9 @@ namespace osu.Game.Screens.OnlinePlay [Cached] private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); + [Cached] + private readonly OngoingOperationTracker joiningRoomTracker = new OngoingOperationTracker(); + [Resolved(CanBeNull = true)] private MusicController music { get; set; } From 6dc0f6af50a3befd707a7f9d4a5fe10cd0862413 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 21:44:58 +0100 Subject: [PATCH 5756/6909] Disable setting apply button for duration of operation --- .../Match/MultiplayerMatchSettingsOverlay.cs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index ae03d384f6..de25cff546 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -68,6 +68,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private Bindable ruleset { get; set; } + private readonly OngoingOperationTracker applyingSettingsTracker = new OngoingOperationTracker(); + [BackgroundDependencyLoader] private void load(OsuColour colours) { @@ -274,13 +276,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true); + + applyingSettingsTracker.InProgress.BindValueChanged(v => + { + if (v.NewValue) + loadingLayer.Show(); + else + loadingLayer.Hide(); + }); } protected override void Update() { base.Update(); - ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0; + ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0 && !applyingSettingsTracker.InProgress.Value; } private void apply() @@ -289,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match return; hideError(); - loadingLayer.Show(); + applyingSettingsTracker.BeginOperation(); // If the client is already in a room, update via the client. // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation. @@ -322,7 +332,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void onSuccess(Room room) { - loadingLayer.Hide(); + applyingSettingsTracker.EndOperation(); SettingsApplied?.Invoke(); } @@ -330,8 +340,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { ErrorText.Text = text; ErrorText.FadeIn(50); - - loadingLayer.Hide(); + applyingSettingsTracker.EndOperation(); } } From 540dec2e7ccbb96342683545d744ffa74b4011d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 28 Dec 2020 22:54:52 +0100 Subject: [PATCH 5757/6909] Allow null tracker in lounge screen for tests --- .../Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 2730693f73..f25835f56a 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -38,7 +38,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge [Resolved] private MusicController music { get; set; } - [Resolved] + [Resolved(CanBeNull = true)] private OngoingOperationTracker joiningRoomTracker { get; set; } [BackgroundDependencyLoader] @@ -102,8 +102,11 @@ namespace osu.Game.Screens.OnlinePlay.Lounge initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived); initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer()); - joiningRoom.BindTo(joiningRoomTracker.InProgress); - joiningRoom.BindValueChanged(_ => updateLoadingLayer(), true); + if (joiningRoomTracker != null) + { + joiningRoom.BindTo(joiningRoomTracker.InProgress); + joiningRoom.BindValueChanged(_ => updateLoadingLayer(), true); + } } protected override void UpdateAfterChildren() @@ -161,15 +164,15 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void joinRequested(Room room) { - joiningRoomTracker.BeginOperation(); + joiningRoomTracker?.BeginOperation(); RoomManager?.JoinRoom(room, r => { Open(room); - joiningRoomTracker.EndOperation(); + joiningRoomTracker?.EndOperation(); }, _ => { - joiningRoomTracker.EndOperation(); + joiningRoomTracker?.EndOperation(); }); } From 355ecc4499bf1357612c6c6607b65a46e84ea432 Mon Sep 17 00:00:00 2001 From: TheOmyNomy Date: Tue, 29 Dec 2020 12:37:57 +1100 Subject: [PATCH 5758/6909] Change cursor trail blending mode to match stable --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs index f18d3191ca..af9ea99232 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyCursorTrail.cs @@ -20,17 +20,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy private double lastTrailTime; private IBindable cursorSize; - public LegacyCursorTrail() - { - Blending = BlendingParameters.Additive; - } - [BackgroundDependencyLoader] private void load(ISkinSource skin, OsuConfigManager config) { Texture = skin.GetTexture("cursortrail"); disjointTrail = skin.GetTexture("cursormiddle") == null; + Blending = !disjointTrail ? BlendingParameters.Additive : BlendingParameters.Inherit; + if (Texture != null) { // stable "magic ratio". see OsuPlayfieldAdjustmentContainer for full explanation. From 6aeb7ece660ef5600f8b8da00ac38fd05a5def41 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Dec 2020 14:25:08 +0900 Subject: [PATCH 5759/6909] Tidy up update state code, naming, xmldoc --- .../Multiplayer/StatefulMultiplayerClient.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index c15401e99d..bf62c450c9 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -303,7 +303,7 @@ namespace osu.Game.Online.Multiplayer Room.Users.Single(u => u.UserID == userId).State = state; - updatePlayingUsers(userId, state); + updateUserPlayingState(userId, state); RoomUpdated?.Invoke(); }, false); @@ -453,15 +453,22 @@ namespace osu.Game.Online.Multiplayer apiRoom.Playlist.Add(playlistItem); } - private void updatePlayingUsers(int userId, MultiplayerUserState state) + /// + /// For the provided user ID, update whether the user is included in . + /// + /// The user's ID. + /// The new state of the user. + private void updateUserPlayingState(int userId, MultiplayerUserState state) { - bool isPlaying = state >= MultiplayerUserState.WaitingForLoad && state <= MultiplayerUserState.FinishedPlay; bool wasPlaying = PlayingUsers.Contains(userId); + bool isPlaying = state >= MultiplayerUserState.WaitingForLoad && state <= MultiplayerUserState.FinishedPlay; - if (!wasPlaying && isPlaying) + if (isPlaying == wasPlaying) + return; + + if (isPlaying) PlayingUsers.Add(userId); - - if (wasPlaying && !isPlaying) + else PlayingUsers.Remove(userId); } } From e3a41f61186525d18d01bd8306e1d2a6f7a2baa3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Dec 2020 14:27:33 +0900 Subject: [PATCH 5760/6909] Rename variable to make more sense It needs to be explicitly stated that the users in this list are related to the *joined* room. Especially since it's sharing its variable name with `SpectatorStreamingClient` where it has the opposite meaning (is a list of *globally* playing players). --- .../Multiplayer/StatefulMultiplayerClientTest.cs | 2 +- .../TestSceneMultiplayerGameplayLeaderboard.cs | 6 +++--- .../Multiplayer/StatefulMultiplayerClient.cs | 16 ++++++++-------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- .../Play/HUD/MultiplayerGameplayLeaderboard.cs | 4 ++-- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs index 8d543e7485..a2ad37cf4a 100644 --- a/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs +++ b/osu.Game.Tests/NonVisual/Multiplayer/StatefulMultiplayerClientTest.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.NonVisual.Multiplayer } private void checkPlayingUserCount(int expectedCount) - => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => Client.PlayingUsers.Count == expectedCount); + => AddAssert($"{"user".ToQuantity(expectedCount)} playing", () => Client.CurrentMatchPlayingUserIds.Count == expectedCount); private void changeState(int userCount, MultiplayerUserState state) => AddStep($"{"user".ToQuantity(userCount)} in {state}", () => diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index d0b1e77549..d016accc25 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -62,8 +62,8 @@ namespace osu.Game.Tests.Visual.Multiplayer streamingClient.Start(Beatmap.Value.BeatmapInfo.OnlineBeatmapID ?? 0); - Client.PlayingUsers.Clear(); - Client.PlayingUsers.AddRange(streamingClient.PlayingUsers); + Client.CurrentMatchPlayingUserIds.Clear(); + Client.CurrentMatchPlayingUserIds.AddRange(streamingClient.PlayingUsers); Children = new Drawable[] { @@ -91,7 +91,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestUserQuit() { - AddRepeatStep("mark user quit", () => Client.PlayingUsers.RemoveAt(0), users); + AddRepeatStep("mark user quit", () => Client.CurrentMatchPlayingUserIds.RemoveAt(0), users); } public class TestMultiplayerStreaming : SpectatorStreamingClient diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index bf62c450c9..bb690d786a 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -61,9 +61,9 @@ namespace osu.Game.Online.Multiplayer public MultiplayerRoom? Room { get; private set; } /// - /// The users currently in gameplay. + /// The users in the joined which are currently in gameplay. /// - public readonly BindableList PlayingUsers = new BindableList(); + public readonly BindableList CurrentMatchPlayingUserIds = new BindableList(); [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; @@ -133,7 +133,7 @@ namespace osu.Game.Online.Multiplayer apiRoom = null; Room = null; - PlayingUsers.Clear(); + CurrentMatchPlayingUserIds.Clear(); RoomUpdated?.Invoke(); }, false); @@ -254,7 +254,7 @@ namespace osu.Game.Online.Multiplayer return; Room.Users.Remove(user); - PlayingUsers.Remove(user.UserID); + CurrentMatchPlayingUserIds.Remove(user.UserID); RoomUpdated?.Invoke(); }, false); @@ -454,22 +454,22 @@ namespace osu.Game.Online.Multiplayer } /// - /// For the provided user ID, update whether the user is included in . + /// For the provided user ID, update whether the user is included in . /// /// The user's ID. /// The new state of the user. private void updateUserPlayingState(int userId, MultiplayerUserState state) { - bool wasPlaying = PlayingUsers.Contains(userId); + bool wasPlaying = CurrentMatchPlayingUserIds.Contains(userId); bool isPlaying = state >= MultiplayerUserState.WaitingForLoad && state <= MultiplayerUserState.FinishedPlay; if (isPlaying == wasPlaying) return; if (isPlaying) - PlayingUsers.Add(userId); + CurrentMatchPlayingUserIds.Add(userId); else - PlayingUsers.Remove(userId); + CurrentMatchPlayingUserIds.Remove(userId); } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index ba0ed16cf4..ffa36ecfdb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -200,7 +200,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { Debug.Assert(client.Room != null); - int[] userIds = client.PlayingUsers.ToArray(); + int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); } diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index e7e5459f76..d4ce542a67 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -84,11 +84,11 @@ namespace osu.Game.Screens.Play.HUD // BindableList handles binding in a really bad way (Clear then AddRange) so we need to do this manually.. foreach (int userId in playingUsers) { - if (!multiplayerClient.PlayingUsers.Contains(userId)) + if (!multiplayerClient.CurrentMatchPlayingUserIds.Contains(userId)) usersChanged(this, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, new[] { userId })); } - playingUsers.BindTo(multiplayerClient.PlayingUsers); + playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); playingUsers.BindCollectionChanged(usersChanged); } From f31a0e455a7834e839643a711df9dcd96409c315 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Dec 2020 14:29:40 +0900 Subject: [PATCH 5761/6909] Minor xmldoc rewording --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index bb690d786a..39d119b2a4 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -61,7 +61,7 @@ namespace osu.Game.Online.Multiplayer public MultiplayerRoom? Room { get; private set; } /// - /// The users in the joined which are currently in gameplay. + /// The users in the joined which are participating in the current gameplay loop. /// public readonly BindableList CurrentMatchPlayingUserIds = new BindableList(); From 45c578b85780f81051035dee3094efeef995fff9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Dec 2020 15:10:08 +0900 Subject: [PATCH 5762/6909] Remove selection polling from multiplayer Looks like this was just copy-paste without any thought into whether it should exist. It really shouldn't exist. This is a thing for the playlists system because the *whole system* there relies on polling the web API to get updated information. In the case of mutliplayer, we hand off all communications to the realtime server at the point of joining the rooms. The argument that this was there to do faster polling on the selection isn't valid since the polling times were the same for both cases. Closes #11348. --- .../OnlinePlay/Multiplayer/Multiplayer.cs | 5 +--- .../Multiplayer/MultiplayerRoomManager.cs | 28 +------------------ 2 files changed, 2 insertions(+), 31 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 76f5c74433..310617a0bc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -33,7 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen()) { multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; - multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; } else { @@ -41,18 +40,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { case LoungeSubScreen _: multiplayerRoomManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; - multiplayerRoomManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; break; // Don't poll inside the match or anywhere else. default: multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; - multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; break; } } - Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value})"); } protected override Room CreateNewRoom() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index 3cb263298f..5c327266a3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private StatefulMultiplayerClient multiplayerClient { get; set; } public readonly Bindable TimeBetweenListingPolls = new Bindable(); - public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); + private readonly IBindable isConnected = new Bindable(); private readonly Bindable allowPolling = new Bindable(); @@ -119,11 +119,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, AllowPolling = { BindTarget = allowPolling } }, - new MultiplayerSelectionPollingComponent - { - TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls }, - AllowPolling = { BindTarget = allowPolling } - } }; private class MultiplayerListingPollingComponent : ListingPollingComponent @@ -146,26 +141,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); } - - private class MultiplayerSelectionPollingComponent : SelectionPollingComponent - { - public readonly IBindable AllowPolling = new Bindable(); - - protected override void LoadComplete() - { - base.LoadComplete(); - - AllowPolling.BindValueChanged(allowPolling => - { - if (!allowPolling.NewValue) - return; - - if (IsLoaded) - PollImmediately(); - }); - } - - protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); - } } } From 2cb84c5111b9e244d98492e4b7fe43e3ef553492 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Dec 2020 15:19:52 +0900 Subject: [PATCH 5763/6909] Fix error message being shown to user on multiplayer disconnection when not in room --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 39d119b2a4..dc80488d39 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -84,7 +84,7 @@ namespace osu.Game.Online.Multiplayer IsConnected.BindValueChanged(connected => { // clean up local room state on server disconnect. - if (!connected.NewValue) + if (!connected.NewValue && Room != null) { Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important); LeaveRoom().CatchUnobservedExceptions(); From 903dca875e7aa364277dc336e52c9c350a6f363a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 07:46:22 +0100 Subject: [PATCH 5764/6909] Make localUser a client property --- .../Online/Multiplayer/StatefulMultiplayerClient.cs | 5 +++++ .../Multiplayer/Match/MultiplayerReadyButton.cs | 10 ++++------ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 39d119b2a4..0f44085134 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -65,6 +65,11 @@ namespace osu.Game.Online.Multiplayer /// public readonly BindableList CurrentMatchPlayingUserIds = new BindableList(); + /// + /// The corresponding to the local player, if available. + /// + public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); + [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 5009b435f4..9b406ff52c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -27,9 +27,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private IAPIProvider api { get; set; } - [CanBeNull] - private MultiplayerRoomUser localUser; - [Resolved] private OsuColour colours { get; set; } @@ -67,14 +64,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.OnRoomUpdated(); - // this method is called on leaving the room, so the local user may not exist in the room any more. - localUser = Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); - updateState(); } private void updateState() { + var localUser = Client.LocalUser; + if (localUser == null) return; @@ -142,6 +138,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void onClick() { + var localUser = Client.LocalUser; + if (localUser == null) return; From 9ff214023297f4300a439e5c2427281a0cb37bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 07:51:46 +0100 Subject: [PATCH 5765/6909] Move ready-up logic to match sub-screen --- .../Match/MultiplayerMatchFooter.cs | 9 +++++- .../Match/MultiplayerReadyButton.cs | 30 ++++--------------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 28 ++++++++++++++++- 3 files changed, 41 insertions(+), 26 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index a52f62fe00..bd1ca1377e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -19,7 +20,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public readonly Bindable SelectedItem = new Bindable(); + public Action OnReady + { + set => readyButton.OnReady = value; + } + private readonly Drawable background; + private readonly MultiplayerReadyButton readyButton; public MultiplayerMatchFooter() { @@ -29,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match InternalChildren = new[] { background = new Box { RelativeSizeAxes = Axes.Both }, - new MultiplayerReadyButton + readyButton = new MultiplayerReadyButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 9b406ff52c..96470cf070 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -1,15 +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.Diagnostics; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.API; @@ -24,6 +23,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public Bindable SelectedItem => button.SelectedItem; + public Action OnReady + { + set => button.Action = value; + } + [Resolved] private IAPIProvider api { get; set; } @@ -47,7 +51,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match RelativeSizeAxes = Axes.Both, Size = Vector2.One, Enabled = { Value = true }, - Action = onClick }; } @@ -136,27 +139,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match } } - private void onClick() - { - var localUser = Client.LocalUser; - - if (localUser == null) - return; - - if (localUser.State == MultiplayerUserState.Idle) - Client.ChangeState(MultiplayerUserState.Ready).CatchUnobservedExceptions(true); - else - { - if (Room?.Host?.Equals(localUser) == true) - { - gameplayStartTracker.BeginOperation(); - Client.StartMatch().CatchUnobservedExceptions(true); - } - else - Client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); - } - } - private class ButtonWithTrianglesExposed : ReadyButton { public new Triangles Triangles => base.Triangles; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 8eafd7c7df..a87471c8af 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -9,6 +9,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; @@ -153,7 +154,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, new Drawable[] { - new MultiplayerMatchFooter { SelectedItem = { BindTarget = SelectedItem } } + new MultiplayerMatchFooter + { + SelectedItem = { BindTarget = SelectedItem }, + OnReady = onReady + } } }, RowDimensions = new[] @@ -199,6 +204,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); + private void onReady() + { + var localUser = client.LocalUser; + + if (localUser == null) + return; + + if (localUser.State == MultiplayerUserState.Idle) + client.ChangeState(MultiplayerUserState.Ready).CatchUnobservedExceptions(true); + else + { + if (client.Room?.Host?.Equals(localUser) == true) + { + gameplayStartTracker.BeginOperation(); + client.StartMatch().CatchUnobservedExceptions(true); + } + else + client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); + } + } + private void onLoadRequested() { Debug.Assert(client.Room != null); From f59ba799d30390f030fcac0e12ac99a1893f78f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 07:54:27 +0100 Subject: [PATCH 5766/6909] Adjust operation tracker implementation --- osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs index f2d943e14f..6a340d7954 100644 --- a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using osu.Framework.Allocation; using osu.Framework.Bindables; namespace osu.Game.Screens.OnlinePlay @@ -24,21 +25,26 @@ namespace osu.Game.Screens.OnlinePlay /// /// Begins tracking a new online operation. /// + /// + /// An that will automatically mark the operation as ended on disposal. + /// /// An operation has already been started. - public void BeginOperation() + public IDisposable BeginOperation() { if (leasedInProgress != null) throw new InvalidOperationException("Cannot begin operation while another is in progress."); leasedInProgress = inProgress.BeginLease(true); leasedInProgress.Value = true; + + return new InvokeOnDisposal(endOperation); } /// /// Ends tracking an online operation. /// Does nothing if an operation has not been begun yet. /// - public void EndOperation() + private void endOperation() { leasedInProgress?.Return(); leasedInProgress = null; From db52255bbec7a284b07baac967280e2cf5b3d9a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 08:20:43 +0100 Subject: [PATCH 5767/6909] Adjust tracker usages to match new API --- .../TestSceneCreateMultiplayerMatchButton.cs | 9 ++++-- .../TestSceneMultiplayerReadyButton.cs | 9 ++++-- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 21 +++++++++---- .../CreateMultiplayerMatchButton.cs | 10 +++--- .../Match/MultiplayerMatchSettingsOverlay.cs | 31 +++++++++++++++---- .../Match/MultiplayerReadyButton.cs | 10 +++--- .../Multiplayer/MultiplayerMatchSubScreen.cs | 19 +++++++++--- .../Screens/OnlinePlay/OnlinePlayScreen.cs | 2 +- 8 files changed, 79 insertions(+), 32 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs index 2a549e5262..381270c5aa 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.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.Framework.Allocation; using osu.Framework.Graphics; @@ -12,7 +13,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneCreateMultiplayerMatchButton : MultiplayerTestScene { [Cached] - private OngoingOperationTracker joiningRoomTracker = new OngoingOperationTracker(); + private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); private CreateMultiplayerMatchButton button; @@ -31,12 +32,14 @@ namespace osu.Game.Tests.Visual.Multiplayer [Test] public void TestButtonEnableStateChanges() { + IDisposable joiningRoomOperation = null; + assertButtonEnableState(true); - AddStep("begin joining room", () => joiningRoomTracker.BeginOperation()); + AddStep("begin joining room", () => joiningRoomOperation = ongoingOperationTracker.BeginOperation()); assertButtonEnableState(false); - AddStep("end joining room", () => joiningRoomTracker.EndOperation()); + AddStep("end joining room", () => joiningRoomOperation.Dispose()); assertButtonEnableState(true); AddStep("disconnect client", () => Client.Disconnect()); diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 958c6d218b..de02c6c844 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; @@ -31,7 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private RulesetStore rulesets; [Cached] - private OngoingOperationTracker gameplayStartTracker = new OngoingOperationTracker(); + private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -167,11 +168,15 @@ namespace osu.Game.Tests.Visual.Multiplayer private void verifyGameplayStartFlow() { + IDisposable gameplayStartOperation = null; + + AddStep("hook up tracker", () => button.OnReady = () => gameplayStartOperation = ongoingOperationTracker.BeginOperation()); + addClickButtonStep(); AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddAssert("ready button disabled", () => !button.ChildrenOfType().Single().Enabled.Value); - AddStep("transitioned to gameplay", () => gameplayStartTracker.EndOperation()); + AddStep("transitioned to gameplay", () => gameplayStartOperation.Dispose()); AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); } } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index f25835f56a..2a16a62714 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.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; +using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -39,7 +42,10 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private MusicController music { get; set; } [Resolved(CanBeNull = true)] - private OngoingOperationTracker joiningRoomTracker { get; set; } + private OngoingOperationTracker ongoingOperationTracker { get; set; } + + [CanBeNull] + private IDisposable joiningRoomOperation { get; set; } [BackgroundDependencyLoader] private void load() @@ -102,9 +108,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge initialRoomsReceived.BindTo(RoomManager.InitialRoomsReceived); initialRoomsReceived.BindValueChanged(_ => updateLoadingLayer()); - if (joiningRoomTracker != null) + if (ongoingOperationTracker != null) { - joiningRoom.BindTo(joiningRoomTracker.InProgress); + joiningRoom.BindTo(ongoingOperationTracker.InProgress); joiningRoom.BindValueChanged(_ => updateLoadingLayer(), true); } } @@ -164,15 +170,18 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void joinRequested(Room room) { - joiningRoomTracker?.BeginOperation(); + Debug.Assert(joiningRoomOperation == null); + joiningRoomOperation = ongoingOperationTracker?.BeginOperation(); RoomManager?.JoinRoom(room, r => { Open(room); - joiningRoomTracker?.EndOperation(); + joiningRoomOperation?.Dispose(); + joiningRoomOperation = null; }, _ => { - joiningRoomTracker?.EndOperation(); + joiningRoomOperation?.Dispose(); + joiningRoomOperation = null; }); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs index 7518a4e71b..3785dfc29f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs @@ -11,13 +11,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public class CreateMultiplayerMatchButton : PurpleTriangleButton { private IBindable isConnected; - private IBindable joiningRoom; + private IBindable operationInProgress; [Resolved] private StatefulMultiplayerClient multiplayerClient { get; set; } [Resolved] - private OngoingOperationTracker joiningRoomTracker { get; set; } + private OngoingOperationTracker ongoingOperationTracker { get; set; } [BackgroundDependencyLoader] private void load() @@ -29,10 +29,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer isConnected = multiplayerClient.IsConnected.GetBoundCopy(); isConnected.BindValueChanged(_ => updateState()); - joiningRoom = joiningRoomTracker.InProgress.GetBoundCopy(); - joiningRoom.BindValueChanged(_ => updateState(), true); + operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); + operationInProgress.BindValueChanged(_ => updateState(), true); } - private void updateState() => Enabled.Value = isConnected.Value && !joiningRoom.Value; + private void updateState() => Enabled.Value = isConnected.Value && !operationInProgress.Value; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index de25cff546..59b138123d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -2,6 +2,8 @@ // 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; using osu.Framework.Extensions.Color4Extensions; @@ -68,7 +70,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private Bindable ruleset { get; set; } - private readonly OngoingOperationTracker applyingSettingsTracker = new OngoingOperationTracker(); + [Resolved] + private OngoingOperationTracker ongoingOperationTracker { get; set; } + + private readonly IBindable operationInProgress = new BindableBool(); + + [CanBeNull] + private IDisposable applyingSettingsOperation; [BackgroundDependencyLoader] private void load(OsuColour colours) @@ -277,7 +285,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); RoomID.BindValueChanged(roomId => initialBeatmapControl.Alpha = roomId.NewValue == null ? 1 : 0, true); - applyingSettingsTracker.InProgress.BindValueChanged(v => + operationInProgress.BindTo(ongoingOperationTracker.InProgress); + operationInProgress.BindValueChanged(v => { if (v.NewValue) loadingLayer.Show(); @@ -290,7 +299,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.Update(); - ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0 && !applyingSettingsTracker.InProgress.Value; + ApplyButton.Enabled.Value = Playlist.Count > 0 && NameField.Text.Length > 0 && !operationInProgress.Value; } private void apply() @@ -299,7 +308,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match return; hideError(); - applyingSettingsTracker.BeginOperation(); + + Debug.Assert(applyingSettingsOperation == null); + applyingSettingsOperation = ongoingOperationTracker.BeginOperation(); // If the client is already in a room, update via the client. // Otherwise, update the room directly in preparation for it to be submitted to the API on match creation. @@ -332,15 +343,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void onSuccess(Room room) { - applyingSettingsTracker.EndOperation(); + Debug.Assert(applyingSettingsOperation != null); + SettingsApplied?.Invoke(); + + applyingSettingsOperation.Dispose(); + applyingSettingsOperation = null; } private void onError(string text) { + Debug.Assert(applyingSettingsOperation != null); + ErrorText.Text = text; ErrorText.FadeIn(50); - applyingSettingsTracker.EndOperation(); + + applyingSettingsOperation.Dispose(); + applyingSettingsOperation = null; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 96470cf070..b145cd46c1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -35,14 +35,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private OsuColour colours { get; set; } [Resolved] - private OngoingOperationTracker gameplayStartTracker { get; set; } + private OngoingOperationTracker ongoingOperationTracker { get; set; } private SampleChannel sampleReadyCount; private readonly ButtonWithTrianglesExposed button; private int countReady; - private IBindable gameplayStartInProgress; + private IBindable operationInProgress; public MultiplayerReadyButton() { @@ -59,8 +59,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { sampleReadyCount = audio.Samples.Get(@"SongSelect/select-difficulty"); - gameplayStartInProgress = gameplayStartTracker.InProgress.GetBoundCopy(); - gameplayStartInProgress.BindValueChanged(_ => updateState()); + operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); + operationInProgress.BindValueChanged(_ => updateState()); } protected override void OnRoomUpdated() @@ -105,7 +105,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match break; } - button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !gameplayStartInProgress.Value; + button.Enabled.Value = Client.Room?.State == MultiplayerRoomState.Open && !operationInProgress.Value; if (newCountReady != countReady) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a87471c8af..b82bf508ce 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.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.Specialized; using System.Diagnostics; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -32,13 +34,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private StatefulMultiplayerClient client { get; set; } - [Cached] - private OngoingOperationTracker gameplayStartTracker = new OngoingOperationTracker(); + [Resolved] + private OngoingOperationTracker ongoingOperationTracker { get; set; } private MultiplayerMatchSettingsOverlay settingsOverlay; private IBindable isConnected; + [CanBeNull] + private IDisposable gameplayStartOperation; + public MultiplayerMatchSubScreen(Room room) { Title = room.RoomID.Value == null ? "New room" : room.Name.Value; @@ -217,7 +222,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { if (client.Room?.Host?.Equals(localUser) == true) { - gameplayStartTracker.BeginOperation(); + Debug.Assert(gameplayStartOperation == null); + gameplayStartOperation = ongoingOperationTracker.BeginOperation(); + client.StartMatch().CatchUnobservedExceptions(true); } else @@ -232,7 +239,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer int[] userIds = client.CurrentMatchPlayingUserIds.ToArray(); StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); - gameplayStartTracker.EndOperation(); + + Debug.Assert(gameplayStartOperation != null); + + gameplayStartOperation.Dispose(); + gameplayStartOperation = null; } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 92665c498b..13ebc1912e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); [Cached] - private readonly OngoingOperationTracker joiningRoomTracker = new OngoingOperationTracker(); + private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); [Resolved(CanBeNull = true)] private MusicController music { get; set; } From 03b78d1c4b2de871626068a454b163448f216e09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Dec 2020 15:27:22 +0900 Subject: [PATCH 5768/6909] Handle SocketExceptions and HttpRequestExceptions more silently These can occur when a network connection is completely unavailable (ie. host resolution failures are occurring). Currently these would appear as important errors which spammed the notification overlay every retry forever, while no network connection is available. I also took this opportunity to remove a lot of `bool` passing which was no longer in use (previously the fail count / retry process was different to what we have today). --- osu.Game/Online/API/APIAccess.cs | 46 ++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 133ba22406..2aaea22155 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Net; using System.Net.Http; +using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; @@ -293,8 +294,21 @@ namespace osu.Game.Online.API failureCount = 0; return true; } + catch (HttpRequestException re) + { + log.Add($"{nameof(HttpRequestException)} while performing request {req}: {re.Message}"); + handleFailure(); + return false; + } + catch (SocketException se) + { + log.Add($"{nameof(SocketException)} while performing request {req}: {se.Message}"); + handleFailure(); + return false; + } catch (WebException we) { + log.Add($"{nameof(WebException)} while performing request {req}: {we.Message}"); handleWebException(we); return false; } @@ -312,7 +326,7 @@ namespace osu.Game.Online.API /// public IBindable State => state; - private bool handleWebException(WebException we) + private void handleWebException(WebException we) { HttpStatusCode statusCode = (we.Response as HttpWebResponse)?.StatusCode ?? (we.Status == WebExceptionStatus.UnknownError ? HttpStatusCode.NotAcceptable : HttpStatusCode.RequestTimeout); @@ -330,26 +344,24 @@ namespace osu.Game.Online.API { case HttpStatusCode.Unauthorized: Logout(); - return true; + break; case HttpStatusCode.RequestTimeout: - failureCount++; - log.Add($@"API failure count is now {failureCount}"); - - if (failureCount < 3) - // we might try again at an api level. - return false; - - if (State.Value == APIState.Online) - { - state.Value = APIState.Failing; - flushQueue(); - } - - return true; + handleFailure(); + break; } + } - return true; + private void handleFailure() + { + failureCount++; + log.Add($@"API failure count is now {failureCount}"); + + if (failureCount >= 3 && State.Value == APIState.Online) + { + state.Value = APIState.Failing; + flushQueue(); + } } public bool IsLoggedIn => localUser.Value.Id > 1; From 4d04e0dee7c73eaab08ecb4a47cee7822221972a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Dec 2020 16:25:51 +0900 Subject: [PATCH 5769/6909] Disallow entering the playlists/multiplayer screens if API is failing --- osu.Game/Screens/Menu/ButtonSystem.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 474cbde192..65a16f2fcb 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -156,7 +156,7 @@ namespace osu.Game.Screens.Menu private void onMultiplayer() { - if (!api.IsLoggedIn) + if (!api.IsLoggedIn || api.State.Value != APIState.Online) { notifications?.Post(new SimpleNotification { @@ -177,11 +177,11 @@ namespace osu.Game.Screens.Menu private void onPlaylists() { - if (!api.IsLoggedIn) + if (!api.IsLoggedIn || api.State.Value != APIState.Online) { notifications?.Post(new SimpleNotification { - Text = "You gotta be logged in to multi 'yo!", + Text = "You gotta be logged in to view playlists 'yo!", Icon = FontAwesome.Solid.Globe, Activated = () => { From 906a9b79b5b62a6d25c304549a6308451127079b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Dec 2020 16:47:36 +0900 Subject: [PATCH 5770/6909] Show an error when forcefully exiting online play due to API failure --- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 4074dd1573..75612516a9 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; @@ -165,7 +166,10 @@ namespace osu.Game.Screens.OnlinePlay private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => { if (state.NewValue != APIState.Online) + { + Logger.Log("API connection was lost, can't continue with online play", LoggingTarget.Network, LogLevel.Important); Schedule(forcefullyExit); + } }); protected override void LoadComplete() From e9b0652359e9bcffed8c4e84f95a35f3397eef45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 09:09:47 +0100 Subject: [PATCH 5771/6909] Move ready-up operation logic again to client To salvage ready up button tests. --- .../TestSceneMultiplayerReadyButton.cs | 16 ++++++--- .../Multiplayer/StatefulMultiplayerClient.cs | 33 +++++++++++++++++++ .../Match/MultiplayerMatchFooter.cs | 4 +-- .../Match/MultiplayerReadyButton.cs | 2 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 33 ++++++++----------- 5 files changed, 61 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index de02c6c844..86398b205d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached] private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); + private IDisposable toggleReadyOperation; + [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) { @@ -61,6 +63,14 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap = { Value = beatmap }, Ruleset = { Value = beatmap.Ruleset } } + }, + OnToggleReady = async () => + { + toggleReadyOperation = ongoingOperationTracker.BeginOperation(); + + bool gameplayStarted = await Client.ToggleReady(); + if (!gameplayStarted) + toggleReadyOperation.Dispose(); } }; }); @@ -168,15 +178,11 @@ namespace osu.Game.Tests.Visual.Multiplayer private void verifyGameplayStartFlow() { - IDisposable gameplayStartOperation = null; - - AddStep("hook up tracker", () => button.OnReady = () => gameplayStartOperation = ongoingOperationTracker.BeginOperation()); - addClickButtonStep(); AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddAssert("ready button disabled", () => !button.ChildrenOfType().Single().Enabled.Value); - AddStep("transitioned to gameplay", () => gameplayStartOperation.Dispose()); + AddStep("transitioned to gameplay", () => toggleReadyOperation.Dispose()); AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); } } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 0f44085134..ee42e56d75 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -183,6 +183,39 @@ namespace osu.Game.Online.Multiplayer }); } + /// + /// Toggles the 's ready state. + /// + /// true if this toggle triggered a gameplay start; false otherwise. + /// If a toggle of ready state is not valid at this time. + public async Task ToggleReady() + { + var localUser = LocalUser; + + if (localUser == null) + return false; + + switch (localUser.State) + { + case MultiplayerUserState.Idle: + await ChangeState(MultiplayerUserState.Ready); + return false; + + case MultiplayerUserState.Ready: + if (Room?.Host?.Equals(localUser) == true) + { + await StartMatch(); + return true; + } + + await ChangeState(MultiplayerUserState.Idle); + return false; + + default: + throw new InvalidOperationException($"Cannot toggle ready when in {localUser.State}"); + } + } + public abstract Task TransferHost(int userId); public abstract Task ChangeSettings(MultiplayerRoomSettings settings); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index bd1ca1377e..a5e7650e31 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -20,9 +20,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public readonly Bindable SelectedItem = new Bindable(); - public Action OnReady + public Action OnToggleReady { - set => readyButton.OnReady = value; + set => readyButton.OnToggleReady = value; } private readonly Drawable background; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index b145cd46c1..251b5b30ae 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public Bindable SelectedItem => button.SelectedItem; - public Action OnReady + public Action OnToggleReady { set => button.Action = value; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b82bf508ce..88b4c8a1bf 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -162,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new MultiplayerMatchFooter { SelectedItem = { BindTarget = SelectedItem }, - OnReady = onReady + OnToggleReady = onToggleReady } } }, @@ -209,27 +209,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); - private void onReady() + private void onToggleReady() { - var localUser = client.LocalUser; + Debug.Assert(gameplayStartOperation == null); + gameplayStartOperation = ongoingOperationTracker.BeginOperation(); - if (localUser == null) - return; + client.ToggleReady() + .ContinueWith(t => + { + // if gameplay was started, the button will be unblocked on load requested. + if (t.Result) return; - if (localUser.State == MultiplayerUserState.Idle) - client.ChangeState(MultiplayerUserState.Ready).CatchUnobservedExceptions(true); - else - { - if (client.Room?.Host?.Equals(localUser) == true) - { - Debug.Assert(gameplayStartOperation == null); - gameplayStartOperation = ongoingOperationTracker.BeginOperation(); - - client.StartMatch().CatchUnobservedExceptions(true); - } - else - client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); - } + // gameplay was not started; unblock button. + gameplayStartOperation?.Dispose(); + gameplayStartOperation = null; + }) + .CatchUnobservedExceptions(); } private void onLoadRequested() From 274730de34336b646ca57027c6820922cd9fc0ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 09:15:33 +0100 Subject: [PATCH 5772/6909] Cache tracker in test scene to resolve test fails --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 8869718fd1..2344ebea0e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -3,10 +3,12 @@ using System.Linq; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Beatmaps; @@ -18,6 +20,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerMatchSubScreen screen; + [Cached] + private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); + public TestSceneMultiplayerMatchSubScreen() : base(false) { From 6bbd0ecfac9fc77ae680064ec61b29f2409380e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 29 Dec 2020 17:39:00 +0900 Subject: [PATCH 5773/6909] Remove unused lock object --- osu.Game/Online/Multiplayer/MultiplayerRoom.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index 2134e50d72..99f0eae2b3 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -42,8 +42,6 @@ namespace osu.Game.Online.Multiplayer /// public MultiplayerRoomUser? Host { get; set; } - private object writeLock = new object(); - [JsonConstructor] public MultiplayerRoom(in long roomId) { From cafa241ef3e7d73952956fda2ec83b04ef519da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 09:41:32 +0100 Subject: [PATCH 5774/6909] Fix ready-up button getting stuck if server operation fails --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 88b4c8a1bf..5676bb22b4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; -using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; @@ -218,13 +217,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer .ContinueWith(t => { // if gameplay was started, the button will be unblocked on load requested. - if (t.Result) return; + // accessing Exception here also silences any potential errors from the antecedent task + // (we still want to unblock the button if the ready-up fails). + if (t.Exception == null && t.Result) return; // gameplay was not started; unblock button. gameplayStartOperation?.Dispose(); gameplayStartOperation = null; - }) - .CatchUnobservedExceptions(); + }); } private void onLoadRequested() From 5d2319923324b48fa749474c11686db3f82b41d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 10:56:29 +0100 Subject: [PATCH 5775/6909] Trim redundant IsLoggedIn checks --- osu.Game/Screens/Menu/ButtonSystem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 65a16f2fcb..3ac9bb6c24 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -156,7 +156,7 @@ namespace osu.Game.Screens.Menu private void onMultiplayer() { - if (!api.IsLoggedIn || api.State.Value != APIState.Online) + if (api.State.Value != APIState.Online) { notifications?.Post(new SimpleNotification { @@ -177,7 +177,7 @@ namespace osu.Game.Screens.Menu private void onPlaylists() { - if (!api.IsLoggedIn || api.State.Value != APIState.Online) + if (api.State.Value != APIState.Online) { notifications?.Post(new SimpleNotification { From 361d215ab4def9965856c4e998a6373c504bfc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 10:56:59 +0100 Subject: [PATCH 5776/6909] Reword notification messages to match new logic --- osu.Game/Screens/Menu/ButtonSystem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 3ac9bb6c24..fd4d74dc6b 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -160,7 +160,7 @@ namespace osu.Game.Screens.Menu { notifications?.Post(new SimpleNotification { - Text = "You gotta be logged in to multi 'yo!", + Text = "You gotta be online to multi 'yo!", Icon = FontAwesome.Solid.Globe, Activated = () => { @@ -181,7 +181,7 @@ namespace osu.Game.Screens.Menu { notifications?.Post(new SimpleNotification { - Text = "You gotta be logged in to view playlists 'yo!", + Text = "You gotta be online to view playlists 'yo!", Icon = FontAwesome.Solid.Globe, Activated = () => { From f2163a471a83eee0c0d41486f256a9b607113e27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 29 Dec 2020 11:53:42 +0100 Subject: [PATCH 5777/6909] Trim missed reference to deleted member --- .../Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index 7a3845cbf3..80d1acd145 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -143,7 +143,6 @@ namespace osu.Game.Tests.Visual.Multiplayer RoomManager = { TimeBetweenListingPolls = { Value = 1 }, - TimeBetweenSelectionPolls = { Value = 1 } } }; From 013b9b62a1ff7dfd22db139d0571259bcd89de43 Mon Sep 17 00:00:00 2001 From: Firmatorenio Date: Tue, 29 Dec 2020 20:22:56 +0600 Subject: [PATCH 5778/6909] add SV multipliers to taiko difficulty mods --- osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 9 +++++++++ osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs | 9 +++++++++ osu.Game/Rulesets/Mods/ModHardRock.cs | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index d1ad4c9d8d..5ff91eec9f 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.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.Beatmaps; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Taiko.Mods @@ -8,5 +9,13 @@ namespace osu.Game.Rulesets.Taiko.Mods public class TaikoModEasy : ModEasy { public override string Description => @"Beats move slower, and less accuracy required!"; + + private const double slider_multiplier = 0.8; + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + base.ApplyToDifficulty(difficulty); + difficulty.SliderMultiplier *= slider_multiplier; + } } } diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs index 49d225cdb5..37c8dab2de 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.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.Beatmaps; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Taiko.Mods @@ -9,5 +10,13 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override double ScoreMultiplier => 1.06; public override bool Ranked => true; + + private const double slider_multiplier = 1.87; + + public override void ApplyToDifficulty(BeatmapDifficulty difficulty) + { + base.ApplyToDifficulty(difficulty); + difficulty.SliderMultiplier *= slider_multiplier; + } } } diff --git a/osu.Game/Rulesets/Mods/ModHardRock.cs b/osu.Game/Rulesets/Mods/ModHardRock.cs index 0e589735c1..4edcb0b074 100644 --- a/osu.Game/Rulesets/Mods/ModHardRock.cs +++ b/osu.Game/Rulesets/Mods/ModHardRock.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods { } - public void ApplyToDifficulty(BeatmapDifficulty difficulty) + public virtual void ApplyToDifficulty(BeatmapDifficulty difficulty) { const float ratio = 1.4f; difficulty.CircleSize = Math.Min(difficulty.CircleSize * 1.3f, 10.0f); // CS uses a custom 1.3 ratio. From 3552034ffe0044dc5edff9195e6e609504336c84 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Dec 2020 00:55:27 +0900 Subject: [PATCH 5779/6909] 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 cd2ce58c55..611f0d05f4 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 3e1b56c29c..93aa2bc701 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 85ba0590ea..5445adb3fb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 497d644a19f04ad19d990bbe7a042f1a556265f1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 30 Dec 2020 20:24:50 +0900 Subject: [PATCH 5780/6909] Move thread safety / locking logic from MultiplayerRoom --- .../Online/Multiplayer/MultiplayerRoom.cs | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index 99f0eae2b3..12fcf25ace 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -5,9 +5,7 @@ using System; using System.Collections.Generic; -using System.Threading; using Newtonsoft.Json; -using osu.Framework.Allocation; namespace osu.Game.Online.Multiplayer { @@ -48,27 +46,6 @@ namespace osu.Game.Online.Multiplayer RoomID = roomId; } - private object updateLock = new object(); - - private ManualResetEventSlim freeForWrite = new ManualResetEventSlim(true); - - /// - /// Request a lock on this room to perform a thread-safe update. - /// - public IDisposable LockForUpdate() - { - // ReSharper disable once InconsistentlySynchronizedField - freeForWrite.Wait(); - - lock (updateLock) - { - freeForWrite.Wait(); - freeForWrite.Reset(); - - return new ValueInvokeOnDisposal(this, r => freeForWrite.Set()); - } - } - public override string ToString() => $"RoomID:{RoomID} Host:{Host?.UserID} Users:{Users.Count} State:{State} Settings: [{Settings}]"; } } From 669c42a38d7f3ac2d015754166b7af7aacbd13c9 Mon Sep 17 00:00:00 2001 From: Firmatorenio Date: Wed, 30 Dec 2020 20:57:41 +0600 Subject: [PATCH 5781/6909] add remarks explaining HR SV multiplier --- osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs | 3 +++ osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs | 10 +++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs index 5ff91eec9f..ad6fdf59e2 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModEasy.cs @@ -10,6 +10,9 @@ namespace osu.Game.Rulesets.Taiko.Mods { public override string Description => @"Beats move slower, and less accuracy required!"; + /// + /// Multiplier factor added to the scrolling speed. + /// private const double slider_multiplier = 0.8; public override void ApplyToDifficulty(BeatmapDifficulty difficulty) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs index 37c8dab2de..a5a8b75f80 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModHardRock.cs @@ -11,7 +11,15 @@ namespace osu.Game.Rulesets.Taiko.Mods public override double ScoreMultiplier => 1.06; public override bool Ranked => true; - private const double slider_multiplier = 1.87; + /// + /// Multiplier factor added to the scrolling speed. + /// + /// + /// This factor is made up of two parts: the base part (1.4) and the aspect ratio adjustment (4/3). + /// Stable applies the latter by dividing the width of the user's display by the width of a display with the same height, but 4:3 aspect ratio. + /// TODO: Revisit if taiko playfield ever changes away from a hard-coded 16:9 (see https://github.com/ppy/osu/issues/5685). + /// + private const double slider_multiplier = 1.4 * 4 / 3; public override void ApplyToDifficulty(BeatmapDifficulty difficulty) { From 59f2017a13672be09b527873470d528717d4ae70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 30 Dec 2020 16:22:11 +0100 Subject: [PATCH 5782/6909] Move BindValueChanged subscriptions to LoadComplete --- .../Multiplayer/CreateMultiplayerMatchButton.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs index 3785dfc29f..87b0e49b5b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs @@ -27,9 +27,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Text = "Create room"; isConnected = multiplayerClient.IsConnected.GetBoundCopy(); - isConnected.BindValueChanged(_ => updateState()); - operationInProgress = ongoingOperationTracker.InProgress.GetBoundCopy(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + isConnected.BindValueChanged(_ => updateState()); operationInProgress.BindValueChanged(_ => updateState(), true); } From dd87478690096232e4833dc69116006b1bf1e57e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 30 Dec 2020 16:29:19 +0100 Subject: [PATCH 5783/6909] Add helper IsHost property to Client --- .../Online/Multiplayer/StatefulMultiplayerClient.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index ee42e56d75..290765dc35 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -70,6 +70,18 @@ namespace osu.Game.Online.Multiplayer /// public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); + /// + /// Whether the is the host in . + /// + public bool IsHost + { + get + { + var localUser = LocalUser; + return localUser != null && Room?.Host != null && localUser.Equals(Room.Host); + } + } + [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; From d34609b98ed925ccb181f2fc6d47101e3c1623a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 30 Dec 2020 16:29:36 +0100 Subject: [PATCH 5784/6909] Rename On{ToggleReady -> ReadyClick} --- .../Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs | 2 +- .../OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs | 4 ++-- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 2 +- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 86398b205d..1d82b29a91 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -64,7 +64,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Ruleset = { Value = beatmap.Ruleset } } }, - OnToggleReady = async () => + OnReadyClick = async () => { toggleReadyOperation = ongoingOperationTracker.BeginOperation(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index a5e7650e31..bbf861fac3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -20,9 +20,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public readonly Bindable SelectedItem = new Bindable(); - public Action OnToggleReady + public Action OnReadyClick { - set => readyButton.OnToggleReady = value; + set => readyButton.OnReadyClick = value; } private readonly Drawable background; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 251b5b30ae..c6018a79e9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public Bindable SelectedItem => button.SelectedItem; - public Action OnToggleReady + public Action OnReadyClick { set => button.Action = value; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 5676bb22b4..b00ab065fb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -161,7 +161,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new MultiplayerMatchFooter { SelectedItem = { BindTarget = SelectedItem }, - OnToggleReady = onToggleReady + OnReadyClick = onToggleReady } } }, From f800448c87f163346a2ce4284ace782646816823 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 30 Dec 2020 18:00:57 +0100 Subject: [PATCH 5785/6909] Move game start logic to a higher level --- .../TestSceneMultiplayerReadyButton.cs | 17 ++++--- .../Multiplayer/StatefulMultiplayerClient.cs | 15 ++---- .../Multiplayer/MultiplayerMatchSubScreen.cs | 51 ++++++++++++------- 3 files changed, 48 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 1d82b29a91..a6037dcbf2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached] private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - private IDisposable toggleReadyOperation; + private IDisposable readyClickOperation; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -66,11 +66,16 @@ namespace osu.Game.Tests.Visual.Multiplayer }, OnReadyClick = async () => { - toggleReadyOperation = ongoingOperationTracker.BeginOperation(); + readyClickOperation = ongoingOperationTracker.BeginOperation(); - bool gameplayStarted = await Client.ToggleReady(); - if (!gameplayStarted) - toggleReadyOperation.Dispose(); + if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) + { + await Client.StartMatch(); + return; + } + + await Client.ToggleReady(); + readyClickOperation.Dispose(); } }; }); @@ -182,7 +187,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("user waiting for load", () => Client.Room?.Users[0].State == MultiplayerUserState.WaitingForLoad); AddAssert("ready button disabled", () => !button.ChildrenOfType().Single().Enabled.Value); - AddStep("transitioned to gameplay", () => toggleReadyOperation.Dispose()); + AddStep("transitioned to gameplay", () => readyClickOperation.Dispose()); AddAssert("ready button enabled", () => button.ChildrenOfType().Single().Enabled.Value); } } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 290765dc35..5c6baa4fea 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -198,30 +198,23 @@ namespace osu.Game.Online.Multiplayer /// /// Toggles the 's ready state. /// - /// true if this toggle triggered a gameplay start; false otherwise. /// If a toggle of ready state is not valid at this time. - public async Task ToggleReady() + public async Task ToggleReady() { var localUser = LocalUser; if (localUser == null) - return false; + return; switch (localUser.State) { case MultiplayerUserState.Idle: await ChangeState(MultiplayerUserState.Ready); - return false; + return; case MultiplayerUserState.Ready: - if (Room?.Host?.Equals(localUser) == true) - { - await StartMatch(); - return true; - } - await ChangeState(MultiplayerUserState.Idle); - return false; + return; default: throw new InvalidOperationException($"Cannot toggle ready when in {localUser.State}"); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b00ab065fb..1bef8841f5 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; @@ -41,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private IBindable isConnected; [CanBeNull] - private IDisposable gameplayStartOperation; + private IDisposable readyClickOperation; public MultiplayerMatchSubScreen(Room room) { @@ -161,7 +162,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new MultiplayerMatchFooter { SelectedItem = { BindTarget = SelectedItem }, - OnReadyClick = onToggleReady + OnReadyClick = onReadyClick } } }, @@ -208,23 +209,37 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); - private void onToggleReady() + private void onReadyClick() { - Debug.Assert(gameplayStartOperation == null); - gameplayStartOperation = ongoingOperationTracker.BeginOperation(); + Debug.Assert(readyClickOperation == null); + readyClickOperation = ongoingOperationTracker.BeginOperation(); + + if (client.IsHost && client.LocalUser?.State == MultiplayerUserState.Ready) + { + client.StartMatch() + .ContinueWith(t => + { + // accessing Exception here silences any potential errors from the antecedent task + if (t.Exception != null) + { + // gameplay was not started due to an exception; unblock button. + endOperation(); + } + + // gameplay is starting, the button will be unblocked on load requested. + }); + return; + } client.ToggleReady() - .ContinueWith(t => - { - // if gameplay was started, the button will be unblocked on load requested. - // accessing Exception here also silences any potential errors from the antecedent task - // (we still want to unblock the button if the ready-up fails). - if (t.Exception == null && t.Result) return; + .ContinueWith(_ => endOperation()) + .CatchUnobservedExceptions(); - // gameplay was not started; unblock button. - gameplayStartOperation?.Dispose(); - gameplayStartOperation = null; - }); + void endOperation() + { + readyClickOperation?.Dispose(); + readyClickOperation = null; + } } private void onLoadRequested() @@ -235,10 +250,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); - Debug.Assert(gameplayStartOperation != null); + Debug.Assert(readyClickOperation != null); - gameplayStartOperation.Dispose(); - gameplayStartOperation = null; + readyClickOperation.Dispose(); + readyClickOperation = null; } protected override void Dispose(bool isDisposing) From eb64e6bf4da427df3b3b0f1bfab8dbab42f4819f Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Wed, 30 Dec 2020 23:35:07 +0100 Subject: [PATCH 5786/6909] Remove duplicate application/octet-stream --- osu.Android/OsuGameActivity.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index da69b516dd..9d28ad7c5b 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -18,7 +18,6 @@ namespace osu.Android [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] - [IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeType = "application/octet-stream")] [IntentFilter(new[] { Intent.ActionSend }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity From f9196ae9767653c75dc2d9ff6a0a273671a4466c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 31 Dec 2020 16:36:20 +0900 Subject: [PATCH 5787/6909] Fix PerformFromMenuRunner failing if CurrentScreen is null --- osu.Game/PerformFromMenuRunner.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index e2d4fc6051..9222a64023 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -73,13 +73,16 @@ namespace osu.Game // find closest valid target IScreen current = getCurrentScreen(); + if (current == null) + return; + // a dialog may be blocking the execution for now. if (checkForDialog(current)) return; game?.CloseAllOverlays(false); // we may already be at the target screen type. - if (validScreens.Contains(getCurrentScreen().GetType()) && !beatmap.Disabled) + if (validScreens.Contains(current.GetType()) && !beatmap.Disabled) { complete(); return; From 00c6703c51df3cd05788d1c41d84b64d88de1ff6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 31 Dec 2020 10:27:42 +0100 Subject: [PATCH 5788/6909] Inline complete method as well For better guarantees that `finalAction` is actually called on the same screen that `checkCanComplete()` was (uses result of one `getCurrentScreen()` call throughout instead of calling multiple times). --- osu.Game/PerformFromMenuRunner.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index 9222a64023..7999023998 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -84,7 +84,8 @@ namespace osu.Game // we may already be at the target screen type. if (validScreens.Contains(current.GetType()) && !beatmap.Disabled) { - complete(); + finalAction(current); + Cancel(); return; } @@ -138,11 +139,5 @@ namespace osu.Game lastEncounteredDialogScreen = current; return true; } - - private void complete() - { - finalAction(getCurrentScreen()); - Cancel(); - } } } From 2d279350ad5f6a4e544601c7be7bca768bef1cbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 31 Dec 2020 11:29:02 +0100 Subject: [PATCH 5789/6909] Catch multiplayer client-related unobserved exceptions better Silencing an exception from a task continuation requires accessing `task.Exception` in any way, which was not done previously if `logOnError` was false. To resolve without having to worry whether the compiler will optimise away a useless access or now, just always log, but switch the logging level. The unimportant errors will be logged as debug and therefore essentially silenced on release builds (but could still be potentially useful in debugging). --- osu.Game/Extensions/TaskExtensions.cs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs index a1215d786b..4138c2757a 100644 --- a/osu.Game/Extensions/TaskExtensions.cs +++ b/osu.Game/Extensions/TaskExtensions.cs @@ -1,7 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + +using System; using System.Threading.Tasks; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; namespace osu.Game.Extensions @@ -13,13 +17,19 @@ namespace osu.Game.Extensions /// Avoids unobserved exceptions from being fired. /// /// The task. - /// Whether errors should be logged as important, or silently ignored. - public static void CatchUnobservedExceptions(this Task task, bool logOnError = false) + /// + /// Whether errors should be logged as errors visible to users, or as debug messages. + /// Logging as debug will essentially silence the errors on non-release builds. + /// + public static void CatchUnobservedExceptions(this Task task, bool logAsError = false) { task.ContinueWith(t => { - if (logOnError) - Logger.Log($"Error running task: {t.Exception?.Message ?? "unknown"}", LoggingTarget.Runtime, LogLevel.Important); + Exception? exception = t.Exception?.AsSingular(); + if (logAsError) + Logger.Error(exception, $"Error running task: {exception?.Message ?? "(unknown)"}", LoggingTarget.Runtime, true); + else + Logger.Log($"Error running task: {exception}", LoggingTarget.Runtime, LogLevel.Debug); }, TaskContinuationOptions.NotOnRanToCompletion); } } From 7d9a61fbc1b7afad59df89d3cf13cea4cce8be8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 31 Dec 2020 11:50:59 +0100 Subject: [PATCH 5790/6909] Handle unobserved exceptions from ready button properly --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 1bef8841f5..39323d9db9 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -222,6 +222,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // accessing Exception here silences any potential errors from the antecedent task if (t.Exception != null) { + t.CatchUnobservedExceptions(true); // will run immediately. // gameplay was not started due to an exception; unblock button. endOperation(); } @@ -232,8 +233,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } client.ToggleReady() - .ContinueWith(_ => endOperation()) - .CatchUnobservedExceptions(); + .ContinueWith(t => + { + t.CatchUnobservedExceptions(true); // will run immediately. + endOperation(); + }); void endOperation() { From 17abe90c27ff837d39e97ec3678dad33bad3e6c9 Mon Sep 17 00:00:00 2001 From: mcendu Date: Thu, 31 Dec 2020 20:23:13 +0800 Subject: [PATCH 5791/6909] move SkinnableHealthDisplay Similar components are in osu.Game.Screens.Play.HUD while this is not --- .../Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs | 2 +- osu.Game/Screens/Play/{ => HUD}/SkinnableHealthDisplay.cs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) rename osu.Game/Screens/Play/{ => HUD}/SkinnableHealthDisplay.cs (95%) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index e1b0820662..5bac8582d7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.Gameplay { diff --git a/osu.Game/Screens/Play/SkinnableHealthDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs similarity index 95% rename from osu.Game/Screens/Play/SkinnableHealthDisplay.cs rename to osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs index d35d15d665..1f91f5e50f 100644 --- a/osu.Game/Screens/Play/SkinnableHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs @@ -5,10 +5,9 @@ using System; using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { public class SkinnableHealthDisplay : SkinnableDrawable, IHealthDisplay { From b4df2d6d43f96659c6687badc4c739770957ebfa Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 1 Jan 2021 03:46:09 +0300 Subject: [PATCH 5792/6909] Add method for copying properties from another mod --- osu.Game/Rulesets/Mods/Mod.cs | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b8dc7a2661..236bf9ff00 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -131,22 +131,31 @@ namespace osu.Game.Rulesets.Mods /// public virtual Mod CreateCopy() { - var copy = (Mod)Activator.CreateInstance(GetType()); + var result = (Mod)Activator.CreateInstance(GetType(), true); + result.CopyFrom(this); + return result; + } + + /// + /// Copies properties of given mod into here, through changing value. + /// + /// The mod to copy properties from. + public void CopyFrom(Mod them) + { + if (them.GetType() != GetType()) + throw new ArgumentException($"Expected mod of type {GetType()}, got {them.GetType()}.", nameof(them)); - // Copy bindable values across foreach (var (_, prop) in this.GetSettingsSourceProperties()) { - var origBindable = prop.GetValue(this); - var copyBindable = prop.GetValue(copy); + var ourBindable = prop.GetValue(this); + var theirBindable = prop.GetValue(them); // The bindables themselves are readonly, so the value must be transferred through the Bindable.Value property. - var valueProperty = origBindable.GetType().GetProperty(nameof(Bindable.Value), BindingFlags.Public | BindingFlags.Instance); + var valueProperty = theirBindable.GetType().GetProperty(nameof(Bindable.Value), BindingFlags.Public | BindingFlags.Instance); Debug.Assert(valueProperty != null); - valueProperty.SetValue(copyBindable, valueProperty.GetValue(origBindable)); + valueProperty.SetValue(ourBindable, valueProperty.GetValue(theirBindable)); } - - return copy; } public bool Equals(IMod other) => GetType() == other?.GetType(); From 2ce9599957d50f5c9b8220cf7d3105372191a6d4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 1 Jan 2021 03:47:13 +0300 Subject: [PATCH 5793/6909] Copy selected mods properties into overlay's buttons --- osu.Game/Overlays/Mods/ModSection.cs | 24 +++++++++++++++------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 2 +- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 0107f94dcf..86da179064 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -127,18 +127,28 @@ namespace osu.Game.Overlays.Mods } /// - /// Select one or more mods in this section and deselects all other ones. + /// Updates all buttons with the given list of selected mods. /// - /// The types of s which should be selected. - public void SelectTypes(IEnumerable modTypes) + /// The types of s to select. + public void UpdateSelectedMods(IReadOnlyList newSelectedMods) { foreach (var button in buttons) { - int i = Array.FindIndex(button.Mods, m => modTypes.Any(t => t == m.GetType())); + int index = -1; - if (i >= 0) - button.SelectAt(i); - else + foreach (var mod in newSelectedMods) + { + index = Array.FindIndex(button.Mods, m1 => mod.GetType() == m1.GetType()); + if (index < 0) + continue; + + var buttonMod = button.Mods[index]; + buttonMod.CopyFrom(mod); + button.SelectAt(index); + break; + } + + if (index < 0) button.Deselect(); } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 34f5c70adb..c1622548f5 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -409,7 +409,7 @@ namespace osu.Game.Overlays.Mods private void selectedModsChanged(ValueChangedEvent> mods) { foreach (var section in ModSectionsContainer.Children) - section.SelectTypes(mods.NewValue.Select(m => m.GetType()).ToList()); + section.UpdateSelectedMods(mods.NewValue); updateMods(); } From ee664ad57117fbdd01dd38a3025a621e69bf7eca Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 1 Jan 2021 03:47:18 +0300 Subject: [PATCH 5794/6909] Add test coverage --- .../UserInterface/TestSceneModSelectOverlay.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 6f083f4ab6..0d0acbb8f4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -131,6 +131,18 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("ensure mods not selected", () => modDisplay.Current.Value.Count == 0); } + [Test] + public void TestExternallySetCustomizedMod() + { + AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }); + + AddAssert("ensure button is selected and customized accordingly", () => + { + var button = modSelect.GetModButton(SelectedMods.Value.Single()); + return ((OsuModDoubleTime)button.SelectedMod).SpeedChange.Value == 1.01; + }); + } + private void testSingleMod(Mod mod) { selectNext(mod); From a031c8e0b62f3933e23e049dc75633f6c4603aef Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 1 Jan 2021 15:34:09 +0300 Subject: [PATCH 5795/6909] Apply documentation suggestions 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/ModSection.cs | 2 +- osu.Game/Rulesets/Mods/Mod.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 86da179064..e29ed4f5c4 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -129,7 +129,7 @@ namespace osu.Game.Overlays.Mods /// /// Updates all buttons with the given list of selected mods. /// - /// The types of s to select. + /// The new list of selected mods to select. public void UpdateSelectedMods(IReadOnlyList newSelectedMods) { foreach (var button in buttons) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 236bf9ff00..6e83299d63 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -137,7 +137,7 @@ namespace osu.Game.Rulesets.Mods } /// - /// Copies properties of given mod into here, through changing value. + /// Copies mod setting values from into this instance. /// /// The mod to copy properties from. public void CopyFrom(Mod them) From c1a1e3acc5aa199442ce7a96058e1eaae2bc7dd9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 1 Jan 2021 15:40:40 +0300 Subject: [PATCH 5796/6909] Revert drive-by changes --- 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 6e83299d63..3f1f16e561 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -131,7 +131,7 @@ namespace osu.Game.Rulesets.Mods /// public virtual Mod CreateCopy() { - var result = (Mod)Activator.CreateInstance(GetType(), true); + var result = (Mod)Activator.CreateInstance(GetType()); result.CopyFrom(this); return result; } @@ -151,7 +151,7 @@ namespace osu.Game.Rulesets.Mods var theirBindable = prop.GetValue(them); // The bindables themselves are readonly, so the value must be transferred through the Bindable.Value property. - var valueProperty = theirBindable.GetType().GetProperty(nameof(Bindable.Value), BindingFlags.Public | BindingFlags.Instance); + var valueProperty = ourBindable.GetType().GetProperty(nameof(Bindable.Value), BindingFlags.Public | BindingFlags.Instance); Debug.Assert(valueProperty != null); valueProperty.SetValue(ourBindable, valueProperty.GetValue(theirBindable)); From 988f9b98a153d95b5f6de42dc3782d08912c125f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 1 Jan 2021 16:16:00 +0300 Subject: [PATCH 5797/6909] Split button mods updating to private method --- osu.Game/Overlays/Mods/ModSection.cs | 30 ++++++++++++++-------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index e29ed4f5c4..573d1e5355 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -133,24 +133,24 @@ namespace osu.Game.Overlays.Mods public void UpdateSelectedMods(IReadOnlyList newSelectedMods) { foreach (var button in buttons) + updateButtonMods(button, newSelectedMods); + } + + private void updateButtonMods(ModButton button, IReadOnlyList newSelectedMods) + { + foreach (var mod in newSelectedMods) { - int index = -1; - - foreach (var mod in newSelectedMods) - { - index = Array.FindIndex(button.Mods, m1 => mod.GetType() == m1.GetType()); - if (index < 0) - continue; - - var buttonMod = button.Mods[index]; - buttonMod.CopyFrom(mod); - button.SelectAt(index); - break; - } - + var index = Array.FindIndex(button.Mods, m1 => mod.GetType() == m1.GetType()); if (index < 0) - button.Deselect(); + continue; + + var buttonMod = button.Mods[index]; + buttonMod.CopyFrom(mod); + button.SelectAt(index); + return; } + + button.Deselect(); } protected ModSection() From 7441cfd94ee93e764102eead7839bb2c862ce541 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 1 Jan 2021 17:53:29 +0000 Subject: [PATCH 5798/6909] Bump Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson Bumps [Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson](https://github.com/aspnet/AspNetCore) from 3.1.9 to 3.1.10. - [Release notes](https://github.com/aspnet/AspNetCore/releases) - [Commits](https://github.com/aspnet/AspNetCore/compare/v3.1.9...v3.1.10) 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 93aa2bc701..4c57eb6d00 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,7 +22,7 @@ - + From 0fd2e368c1c73e0d2e4932a620f2f4c6021633c7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 1 Jan 2021 17:53:41 +0000 Subject: [PATCH 5799/6909] Bump Microsoft.NET.Test.Sdk from 16.8.0 to 16.8.3 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.8.0 to 16.8.3. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.8.0...v16.8.3) 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 61ecd79e3d..51d2032795 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 fa7bfd7169..3261f632f2 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 d6a03da807..32243e0bc3 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 a89645d881..210f81d111 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 83d7b4135a..9049b67f90 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 bc6b994988..dc4f22788d 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 652b0ccd8fe5c47dbde90e41f15d8d11baea76f7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 1 Jan 2021 17:54:11 +0000 Subject: [PATCH 5800/6909] Bump Microsoft.AspNetCore.SignalR.Client from 3.1.9 to 3.1.10 Bumps [Microsoft.AspNetCore.SignalR.Client](https://github.com/aspnet/AspNetCore) from 3.1.9 to 3.1.10. - [Release notes](https://github.com/aspnet/AspNetCore/releases) - [Commits](https://github.com/aspnet/AspNetCore/compare/v3.1.9...v3.1.10) 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 93aa2bc701..3c8df17ee5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,7 +21,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 5445adb3fb..f5c133263b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -78,7 +78,7 @@ $(NoWarn);NU1605 - + From 6cd838fd4b8e510739891e64019c192122e08f2f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Fri, 1 Jan 2021 17:55:04 +0000 Subject: [PATCH 5801/6909] Bump Microsoft.CodeAnalysis.BannedApiAnalyzers from 3.3.1 to 3.3.2 Bumps [Microsoft.CodeAnalysis.BannedApiAnalyzers](https://github.com/dotnet/roslyn-analyzers) from 3.3.1 to 3.3.2. - [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/v3.3.1...v3.3.2) 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 551cb75077..9ec442aafa 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,7 +16,7 @@ - + From fa73d0172e01492f6f22c8c457751c0ea37fb659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 2 Jan 2021 00:11:21 +0100 Subject: [PATCH 5802/6909] Keep SignalR at last working version on iOS --- osu.iOS.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.iOS.props b/osu.iOS.props index f5c133263b..5445adb3fb 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -78,7 +78,7 @@ $(NoWarn);NU1605 - + From 72a6ca77559c409bc0a5671663510d1fbcbaffb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Jan 2021 16:47:00 +0900 Subject: [PATCH 5803/6909] Allow signalr to retry connecting when connection is closed without an exception --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 24ea6abc4a..7cd1ef78f7 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -88,11 +88,12 @@ namespace osu.Game.Online.Multiplayer { isConnected.Value = false; - if (ex != null) - { - Logger.Log($"Multiplayer client lost connection: {ex}", LoggingTarget.Network); + Logger.Log(ex != null + ? $"Multiplayer client lost connection: {ex}" + : "Multiplayer client disconnected", LoggingTarget.Network); + + if (connection != null) await tryUntilConnected(); - } }; await tryUntilConnected(); From 66bd847b4507cb575549c01496dbabf819c6164b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 2 Jan 2021 11:53:06 +0100 Subject: [PATCH 5804/6909] Bump InspectCode tool to 2020.3.2 --- .config/dotnet-tools.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index dd53eefd23..58c24181d3 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,7 +15,7 @@ ] }, "jetbrains.resharper.globaltools": { - "version": "2020.2.4", + "version": "2020.3.2", "commands": [ "jb" ] From 18ac97ca563e9a02d56c469e5c084ccfa4b1e978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 2 Jan 2021 12:21:53 +0100 Subject: [PATCH 5805/6909] Disable "merge sequential patterns" suggestions As they were considered to be detrimental to code readability. --- osu.sln.DotSettings | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 22ea73858e..aa8f8739c1 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -106,6 +106,7 @@ HINT WARNING WARNING + DO_NOT_SHOW WARNING WARNING WARNING From 924af58f5b1ba2b5fcdaa00a79db21ce6c19bceb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 2 Jan 2021 12:25:16 +0100 Subject: [PATCH 5806/6909] Replace using static with explicit nested reference This seems to be an inspectcode bug, as the code is correct and compiles, but let's just work around it for now. --- .../Visual/Components/TestScenePreviewTrackManager.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs index a3db20ce83..9a999a4931 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs @@ -8,7 +8,6 @@ using osu.Framework.Audio.Track; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Beatmaps; -using static osu.Game.Tests.Visual.Components.TestScenePreviewTrackManager.TestPreviewTrackManager; namespace osu.Game.Tests.Visual.Components { @@ -100,7 +99,7 @@ namespace osu.Game.Tests.Visual.Components [Test] public void TestNonPresentTrack() { - TestPreviewTrack track = null; + TestPreviewTrackManager.TestPreviewTrack track = null; AddStep("get non-present track", () => { @@ -182,9 +181,9 @@ namespace osu.Game.Tests.Visual.Components AddAssert("track stopped", () => !track.IsRunning); } - private TestPreviewTrack getTrack() => (TestPreviewTrack)trackManager.Get(null); + private TestPreviewTrackManager.TestPreviewTrack getTrack() => (TestPreviewTrackManager.TestPreviewTrack)trackManager.Get(null); - private TestPreviewTrack getOwnedTrack() + private TestPreviewTrackManager.TestPreviewTrack getOwnedTrack() { var track = getTrack(); From 6c3ccaddbfec4a148ad6080e2ab6e958d54cd2e0 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Sat, 2 Jan 2021 12:51:41 +0000 Subject: [PATCH 5807/6909] Bump Microsoft.CodeAnalysis.FxCopAnalyzers from 3.3.1 to 3.3.2 Bumps [Microsoft.CodeAnalysis.FxCopAnalyzers](https://github.com/dotnet/roslyn-analyzers) from 3.3.1 to 3.3.2. - [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/v3.3.1...v3.3.2) 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 9ec442aafa..a74b204436 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ - + $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset From e2de5bb8f940e71fcf9cfefa8fe504966c5e00bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 2 Jan 2021 22:05:41 +0900 Subject: [PATCH 5808/6909] Fix the beatmap carousel not returning to centre correctly after resizing window --- osu.Game/Screens/Select/BeatmapCarousel.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index d76f0abb9e..c83c89bb7f 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Pooling; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Layout; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Beatmaps; @@ -567,6 +568,15 @@ namespace osu.Game.Screens.Select #endregion + protected override bool OnInvalidate(Invalidation invalidation, InvalidationSource source) + { + // handles the vertical size of the carousel changing (ie. on window resize when aspect ratio has changed). + if ((invalidation & Invalidation.Layout) > 0) + itemsCache.Invalidate(); + + return base.OnInvalidate(invalidation, source); + } + protected override void Update() { base.Update(); From 26b2a065bc365fc75e04315dfe9cbc71a3b0bb57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 2 Jan 2021 14:25:15 +0100 Subject: [PATCH 5809/6909] Add deprecation warning ignore with explanation --- Directory.Build.props | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index a74b204436..b55eff9df9 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -28,9 +28,17 @@ $(NoWarn);CS1591 - - $(NoWarn);NU1701 + + $(NoWarn);NU1701;CA9998 false @@ -43,4 +51,4 @@ Copyright (c) 2020 ppy Pty Ltd osu game - \ No newline at end of file + From 4266c67bb66dc58fcdbc4406afe38a7d47c392d9 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sat, 2 Jan 2021 19:19:55 +0100 Subject: [PATCH 5810/6909] Remove duplicated code path in switch. --- osu.Android/OsuGameActivity.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 42d9ad33ab..788e5f82be 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -62,13 +62,6 @@ namespace osu.Android break; case Intent.ActionSend: - { - var content = intent.ClipData?.GetItemAt(0); - if (content != null) - handleImportFromUris(content.Uri); - break; - } - case Intent.ActionSendMultiple: { var uris = new List(); From 0e0cb94ed5804fc9ef1b2316ec3e2663a5341fe8 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Sun, 3 Jan 2021 03:20:25 +0100 Subject: [PATCH 5811/6909] testing (#2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Catch multiplayer client-related unobserved exceptions better Silencing an exception from a task continuation requires accessing `task.Exception` in any way, which was not done previously if `logOnError` was false. To resolve without having to worry whether the compiler will optimise away a useless access or now, just always log, but switch the logging level. The unimportant errors will be logged as debug and therefore essentially silenced on release builds (but could still be potentially useful in debugging). * move SkinnableHealthDisplay Similar components are in osu.Game.Screens.Play.HUD while this is not * Bump Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson Bumps [Microsoft.AspNetCore.SignalR.Protocols.NewtonsoftJson](https://github.com/aspnet/AspNetCore) from 3.1.9 to 3.1.10. - [Release notes](https://github.com/aspnet/AspNetCore/releases) - [Commits](https://github.com/aspnet/AspNetCore/compare/v3.1.9...v3.1.10) Signed-off-by: dependabot-preview[bot] * Bump Microsoft.NET.Test.Sdk from 16.8.0 to 16.8.3 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.8.0 to 16.8.3. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.8.0...v16.8.3) Signed-off-by: dependabot-preview[bot] * Bump Microsoft.AspNetCore.SignalR.Client from 3.1.9 to 3.1.10 Bumps [Microsoft.AspNetCore.SignalR.Client](https://github.com/aspnet/AspNetCore) from 3.1.9 to 3.1.10. - [Release notes](https://github.com/aspnet/AspNetCore/releases) - [Commits](https://github.com/aspnet/AspNetCore/compare/v3.1.9...v3.1.10) Signed-off-by: dependabot-preview[bot] * Bump Microsoft.CodeAnalysis.BannedApiAnalyzers from 3.3.1 to 3.3.2 Bumps [Microsoft.CodeAnalysis.BannedApiAnalyzers](https://github.com/dotnet/roslyn-analyzers) from 3.3.1 to 3.3.2. - [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/v3.3.1...v3.3.2) Signed-off-by: dependabot-preview[bot] * Keep SignalR at last working version on iOS * Allow signalr to retry connecting when connection is closed without an exception * Bump InspectCode tool to 2020.3.2 * Disable "merge sequential patterns" suggestions As they were considered to be detrimental to code readability. * Replace using static with explicit nested reference This seems to be an inspectcode bug, as the code is correct and compiles, but let's just work around it for now. Co-authored-by: Bartłomiej Dach Co-authored-by: mcendu Co-authored-by: Dean Herbert Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- .config/dotnet-tools.json | 2 +- Directory.Build.props | 2 +- .../osu.Game.Rulesets.Catch.Tests.csproj | 2 +- .../osu.Game.Rulesets.Mania.Tests.csproj | 2 +- .../osu.Game.Rulesets.Osu.Tests.csproj | 2 +- .../osu.Game.Rulesets.Taiko.Tests.csproj | 2 +- .../Components/TestScenePreviewTrackManager.cs | 7 +++---- .../TestSceneSkinnableHealthDisplay.cs | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- .../osu.Game.Tournament.Tests.csproj | 2 +- osu.Game/Extensions/TaskExtensions.cs | 18 ++++++++++++++---- .../Online/Multiplayer/MultiplayerClient.cs | 9 +++++---- .../Play/{ => HUD}/SkinnableHealthDisplay.cs | 3 +-- osu.Game/osu.Game.csproj | 4 ++-- osu.sln.DotSettings | 1 + 15 files changed, 35 insertions(+), 25 deletions(-) rename osu.Game/Screens/Play/{ => HUD}/SkinnableHealthDisplay.cs (95%) diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index dd53eefd23..58c24181d3 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -15,7 +15,7 @@ ] }, "jetbrains.resharper.globaltools": { - "version": "2020.2.4", + "version": "2020.3.2", "commands": [ "jb" ] diff --git a/Directory.Build.props b/Directory.Build.props index 551cb75077..9ec442aafa 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,7 +16,7 @@ - + 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 61ecd79e3d..51d2032795 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 fa7bfd7169..3261f632f2 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 d6a03da807..32243e0bc3 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 a89645d881..210f81d111 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/Visual/Components/TestScenePreviewTrackManager.cs b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs index a3db20ce83..9a999a4931 100644 --- a/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs +++ b/osu.Game.Tests/Visual/Components/TestScenePreviewTrackManager.cs @@ -8,7 +8,6 @@ using osu.Framework.Audio.Track; using osu.Framework.Graphics.Containers; using osu.Game.Audio; using osu.Game.Beatmaps; -using static osu.Game.Tests.Visual.Components.TestScenePreviewTrackManager.TestPreviewTrackManager; namespace osu.Game.Tests.Visual.Components { @@ -100,7 +99,7 @@ namespace osu.Game.Tests.Visual.Components [Test] public void TestNonPresentTrack() { - TestPreviewTrack track = null; + TestPreviewTrackManager.TestPreviewTrack track = null; AddStep("get non-present track", () => { @@ -182,9 +181,9 @@ namespace osu.Game.Tests.Visual.Components AddAssert("track stopped", () => !track.IsRunning); } - private TestPreviewTrack getTrack() => (TestPreviewTrack)trackManager.Get(null); + private TestPreviewTrackManager.TestPreviewTrack getTrack() => (TestPreviewTrackManager.TestPreviewTrack)trackManager.Get(null); - private TestPreviewTrack getOwnedTrack() + private TestPreviewTrackManager.TestPreviewTrack getOwnedTrack() { var track = getTrack(); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs index e1b0820662..5bac8582d7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableHealthDisplay.cs @@ -9,7 +9,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Judgements; -using osu.Game.Screens.Play; +using osu.Game.Screens.Play.HUD; namespace osu.Game.Tests.Visual.Gameplay { diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 83d7b4135a..9049b67f90 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 bc6b994988..dc4f22788d 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 @@ - + diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs index a1215d786b..4138c2757a 100644 --- a/osu.Game/Extensions/TaskExtensions.cs +++ b/osu.Game/Extensions/TaskExtensions.cs @@ -1,7 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +#nullable enable + +using System; using System.Threading.Tasks; +using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; namespace osu.Game.Extensions @@ -13,13 +17,19 @@ namespace osu.Game.Extensions /// Avoids unobserved exceptions from being fired. /// /// The task. - /// Whether errors should be logged as important, or silently ignored. - public static void CatchUnobservedExceptions(this Task task, bool logOnError = false) + /// + /// Whether errors should be logged as errors visible to users, or as debug messages. + /// Logging as debug will essentially silence the errors on non-release builds. + /// + public static void CatchUnobservedExceptions(this Task task, bool logAsError = false) { task.ContinueWith(t => { - if (logOnError) - Logger.Log($"Error running task: {t.Exception?.Message ?? "unknown"}", LoggingTarget.Runtime, LogLevel.Important); + Exception? exception = t.Exception?.AsSingular(); + if (logAsError) + Logger.Error(exception, $"Error running task: {exception?.Message ?? "(unknown)"}", LoggingTarget.Runtime, true); + else + Logger.Log($"Error running task: {exception}", LoggingTarget.Runtime, LogLevel.Debug); }, TaskContinuationOptions.NotOnRanToCompletion); } } diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 24ea6abc4a..7cd1ef78f7 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -88,11 +88,12 @@ namespace osu.Game.Online.Multiplayer { isConnected.Value = false; - if (ex != null) - { - Logger.Log($"Multiplayer client lost connection: {ex}", LoggingTarget.Network); + Logger.Log(ex != null + ? $"Multiplayer client lost connection: {ex}" + : "Multiplayer client disconnected", LoggingTarget.Network); + + if (connection != null) await tryUntilConnected(); - } }; await tryUntilConnected(); diff --git a/osu.Game/Screens/Play/SkinnableHealthDisplay.cs b/osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs similarity index 95% rename from osu.Game/Screens/Play/SkinnableHealthDisplay.cs rename to osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs index d35d15d665..1f91f5e50f 100644 --- a/osu.Game/Screens/Play/SkinnableHealthDisplay.cs +++ b/osu.Game/Screens/Play/HUD/SkinnableHealthDisplay.cs @@ -5,10 +5,9 @@ using System; using osu.Framework.Bindables; using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Play.HUD; using osu.Game.Skinning; -namespace osu.Game.Screens.Play +namespace osu.Game.Screens.Play.HUD { public class SkinnableHealthDisplay : SkinnableDrawable, IHealthDisplay { diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 93aa2bc701..6c220a5c21 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -21,8 +21,8 @@ - - + + diff --git a/osu.sln.DotSettings b/osu.sln.DotSettings index 22ea73858e..aa8f8739c1 100644 --- a/osu.sln.DotSettings +++ b/osu.sln.DotSettings @@ -106,6 +106,7 @@ HINT WARNING WARNING + DO_NOT_SHOW WARNING WARNING WARNING From 8bb84570df904c9c55cc781e18cc13bfb13e834e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Jan 2021 04:21:50 +0300 Subject: [PATCH 5812/6909] Introduce beatmap availability structure --- osu.Game/Online/Rooms/BeatmapAvailability.cs | 56 ++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 osu.Game/Online/Rooms/BeatmapAvailability.cs diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs new file mode 100644 index 0000000000..ca53cb2295 --- /dev/null +++ b/osu.Game/Online/Rooms/BeatmapAvailability.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 System; + +namespace osu.Game.Online.Rooms +{ + /// + /// The local availability information about a certain beatmap for the client. + /// + public readonly struct BeatmapAvailability : IEquatable + { + /// + /// The beatmap's availability state. + /// + public readonly DownloadState State; + + /// + /// The beatmap's downloading progress, null when not in state. + /// + public readonly double? DownloadProgress; + + /// + /// Constructs a new non- beatmap availability state. + /// + /// The beatmap availability state. + /// Throws if was specified in this constructor, as it has its own constructor (see . + public BeatmapAvailability(DownloadState state) + { + if (state == DownloadState.Downloading) + throw new ArgumentException($"{nameof(DownloadState.Downloading)} state has its own constructor, use it instead."); + + State = state; + DownloadProgress = null; + } + + /// + /// Constructs a new -specific beatmap availability state. + /// + /// The beatmap availability state (always ). + /// The beatmap's downloading current progress. + /// Throws if non- was specified in this constructor, as they have their own constructor (see . + public BeatmapAvailability(DownloadState state, double downloadProgress) + { + if (state != DownloadState.Downloading) + throw new ArgumentException($"This is a constructor specific for {DownloadState.Downloading} state, use the regular one instead."); + + State = DownloadState.Downloading; + DownloadProgress = downloadProgress; + } + + public bool Equals(BeatmapAvailability other) => State == other.State && DownloadProgress == other.DownloadProgress; + + public override string ToString() => $"{string.Join(", ", State, $"{DownloadProgress:0.00%}")}"; + } +} From 09e5e2629ab2ffa0ad052683bcb91000e18a87fe Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Jan 2021 04:25:06 +0300 Subject: [PATCH 5813/6909] Add user beatmap availability property --- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 99624dc3e7..92c92f438d 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -5,6 +5,7 @@ using System; using Newtonsoft.Json; +using osu.Game.Online.Rooms; using osu.Game.Users; namespace osu.Game.Online.Multiplayer @@ -16,6 +17,11 @@ namespace osu.Game.Online.Multiplayer public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle; + /// + /// The availability state of the beatmap, set to by default. + /// + public BeatmapAvailability BeatmapAvailability { get; set; } = new BeatmapAvailability(DownloadState.LocallyAvailable); + public User? User { get; set; } [JsonConstructor] From dfa8be9173839c3f5dd7aaabb14f0a60692580f1 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Jan 2021 04:32:50 +0300 Subject: [PATCH 5814/6909] Add beatmap availability change state & event methods --- .../Online/Multiplayer/IMultiplayerClient.cs | 8 ++++++++ .../Multiplayer/IMultiplayerRoomServer.cs | 8 ++++++++ .../Online/Multiplayer/MultiplayerClient.cs | 9 +++++++++ .../Multiplayer/StatefulMultiplayerClient.cs | 20 +++++++++++++++++++ .../Multiplayer/TestMultiplayerClient.cs | 15 ++++++++++++++ 5 files changed, 60 insertions(+) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index b97fcc9ae7..5410fbc030 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; +using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer { @@ -47,6 +48,13 @@ namespace osu.Game.Online.Multiplayer /// The new state of the user. Task UserStateChanged(int userId, MultiplayerUserState state); + /// + /// Signals that a user in this room has their beatmap availability state changed. + /// + /// The ID of the user whose beatmap availability state has changed. + /// The new beatmap availability state of the user. + Task UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability); + /// /// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point. /// diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 481e3fb1de..7fda526faf 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System.Threading.Tasks; +using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer { @@ -40,6 +41,13 @@ namespace osu.Game.Online.Multiplayer /// If the user is not in a room. Task ChangeState(MultiplayerUserState newState); + /// + /// Change the user's local availability state of the beatmap set in joined room. + /// This will also force user state back to . + /// + /// The proposed new beatmap availability state. + Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + /// /// As the host of a room, start the match. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 7cd1ef78f7..50dc8f661c 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -14,6 +14,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; using osu.Game.Online.API; +using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer { @@ -173,6 +174,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState); } + public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability) + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability); + } + public override Task StartMatch() { if (!isConnected.Value) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index dc80488d39..c1818de87a 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -184,6 +184,8 @@ namespace osu.Game.Online.Multiplayer public abstract Task ChangeState(MultiplayerUserState newState); + public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + public abstract Task StartMatch(); Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) @@ -311,6 +313,24 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } + Task IMultiplayerClient.UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability) + { + if (Room == null) + return Task.CompletedTask; + + Scheduler.Add(() => + { + if (Room == null) + return; + + Room.Users.Single(u => u.UserID == userId).BeatmapAvailability = beatmapAvailability; + + RoomUpdated?.Invoke(); + }, false); + + return Task.CompletedTask; + } + Task IMultiplayerClient.LoadRequested() { if (Room == null) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 2ce5211757..c155447f8c 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -10,6 +10,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer @@ -77,6 +78,14 @@ namespace osu.Game.Tests.Visual.Multiplayer }); } + public void ChangeUserBeatmapAvailability(int userId, BeatmapAvailability newBeatmapAvailability) + { + Debug.Assert(Room != null); + + ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(userId, newBeatmapAvailability); + ChangeUserState(userId, MultiplayerUserState.Idle); + } + protected override Task JoinRoom(long roomId) { var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value }; @@ -108,6 +117,12 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability) + { + ChangeUserBeatmapAvailability(api.LocalUser.Value.Id, newBeatmapAvailability); + return Task.CompletedTask; + } + public override Task StartMatch() { Debug.Assert(Room != null); From caa5109e3a0e1e526c789024d2ecbc9d57767e76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Jan 2021 12:18:35 +0900 Subject: [PATCH 5815/6909] Add precautionary null checks to update methods in SongSelect --- 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 a5252fdc96..8ad1ace36a 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -428,7 +428,7 @@ namespace osu.Game.Screens.Select private void updateSelectedBeatmap(BeatmapInfo beatmap) { - if (beatmap?.Equals(beatmapNoDebounce) == true) + if (beatmap == null || beatmap.Equals(beatmapNoDebounce)) return; beatmapNoDebounce = beatmap; @@ -438,7 +438,7 @@ namespace osu.Game.Screens.Select private void updateSelectedRuleset(RulesetInfo ruleset) { - if (ruleset?.Equals(rulesetNoDebounce) == true) + if (ruleset == null || ruleset.Equals(rulesetNoDebounce)) return; rulesetNoDebounce = ruleset; From 2e5c67be3f415bc0841c4ae2cd122237ff49c380 Mon Sep 17 00:00:00 2001 From: LavaDesu Date: Wed, 30 Dec 2020 12:29:51 +0700 Subject: [PATCH 5816/6909] Add ability to toggle discord rich presence There are 3 modes: enabled, limited, and disabled. The limited mode hides identifiable information such as username, rank, and (if participating in one) multiplayer lobby name. --- osu.Desktop/DiscordRichPresence.cs | 15 ++++++++--- .../Configuration/DiscordRichPresenceMode.cs | 17 ++++++++++++ osu.Game/Configuration/OsuConfigManager.cs | 5 +++- .../Sections/Online/IntegrationSettings.cs | 27 +++++++++++++++++++ .../Settings/Sections/OnlineSection.cs | 3 ++- 5 files changed, 61 insertions(+), 6 deletions(-) create mode 100644 osu.Game/Configuration/DiscordRichPresenceMode.cs create mode 100644 osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index f1878d967d..6b331d4952 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -9,6 +9,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Logging; +using osu.Game.Configuration; using osu.Game.Online.API; using osu.Game.Rulesets; using osu.Game.Users; @@ -30,6 +31,7 @@ namespace osu.Desktop private readonly IBindable status = new Bindable(); private readonly IBindable activity = new Bindable(); + private readonly Bindable mode = new Bindable(); private readonly RichPresence presence = new RichPresence { @@ -37,7 +39,7 @@ namespace osu.Desktop }; [BackgroundDependencyLoader] - private void load(IAPIProvider provider) + private void load(IAPIProvider provider, OsuConfigManager config) { client = new DiscordRpcClient(client_id) { @@ -51,6 +53,7 @@ namespace osu.Desktop client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); + config.BindWith(OsuSetting.DiscordRichPresence, mode); (user = provider.LocalUser.GetBoundCopy()).BindValueChanged(u => { status.UnbindBindings(); @@ -63,6 +66,7 @@ namespace osu.Desktop ruleset.BindValueChanged(_ => updateStatus()); status.BindValueChanged(_ => updateStatus()); activity.BindValueChanged(_ => updateStatus()); + mode.BindValueChanged(_ => updateStatus()); client.Initialize(); } @@ -78,7 +82,7 @@ namespace osu.Desktop if (!client.IsInitialized) return; - if (status.Value is UserStatusOffline) + if (status.Value is UserStatusOffline || mode.Value == DiscordRichPresenceMode.Disabled) { client.ClearPresence(); return; @@ -96,7 +100,10 @@ namespace osu.Desktop } // update user information - presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty); + if (mode.Value == DiscordRichPresenceMode.Limited) + presence.Assets.LargeImageText = string.Empty; + else + presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty); // update ruleset presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom"; @@ -137,7 +144,7 @@ namespace osu.Desktop return edit.Beatmap.ToString(); case UserActivity.InLobby lobby: - return lobby.Room.Name.Value; + return mode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; } return string.Empty; diff --git a/osu.Game/Configuration/DiscordRichPresenceMode.cs b/osu.Game/Configuration/DiscordRichPresenceMode.cs new file mode 100644 index 0000000000..bd39faff4d --- /dev/null +++ b/osu.Game/Configuration/DiscordRichPresenceMode.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.ComponentModel; + +namespace osu.Game.Configuration +{ + public enum DiscordRichPresenceMode + { + Disabled, + + [Description("Hide identifiable information")] + Limited, + + Enabled + } +} diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index a07e446d2e..c733fe2fb4 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.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; @@ -138,6 +138,8 @@ namespace osu.Game.Configuration Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes); + Set(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Enabled); + Set(OsuSetting.EditorWaveformOpacity, 1f); } @@ -266,6 +268,7 @@ namespace osu.Game.Configuration GameplayDisableWinKey, SeasonalBackgroundMode, EditorWaveformOpacity, + DiscordRichPresence, AutomaticallyDownloadWhenSpectating, } } diff --git a/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.cs new file mode 100644 index 0000000000..d2867962c0 --- /dev/null +++ b/osu.Game/Overlays/Settings/Sections/Online/IntegrationSettings.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.Game.Configuration; + +namespace osu.Game.Overlays.Settings.Sections.Online +{ + public class IntegrationSettings : SettingsSubsection + { + protected override string Header => "Integrations"; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + Children = new Drawable[] + { + new SettingsEnumDropdown + { + LabelText = "Discord Rich Presence", + Current = config.GetBindable(OsuSetting.DiscordRichPresence) + } + }; + } + } +} diff --git a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs index 150cddb388..7aa4eff29a 100644 --- a/osu.Game/Overlays/Settings/Sections/OnlineSection.cs +++ b/osu.Game/Overlays/Settings/Sections/OnlineSection.cs @@ -20,7 +20,8 @@ namespace osu.Game.Overlays.Settings.Sections { Children = new Drawable[] { - new WebSettings() + new WebSettings(), + new IntegrationSettings() }; } } From a6d49929978a2617ef2f180d3e3e868513e8737e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Jan 2021 12:53:25 +0900 Subject: [PATCH 5817/6909] Ensure SelectionChanged events are only sent once when selection is null --- osu.Game/Screens/Select/BeatmapCarousel.cs | 31 ++++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index c83c89bb7f..37213c6003 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -125,6 +125,8 @@ namespace osu.Game.Screens.Select { BeatmapSetsChanged?.Invoke(); BeatmapSetsLoaded = true; + + itemsCache.Invalidate(); }); } @@ -731,6 +733,12 @@ namespace osu.Game.Screens.Select private const float panel_padding = 5; + /// + /// After loading, we want to invoke a selection changed event at least once. + /// This handles the case where this event is potentially sending a null selection. + /// + private bool sentInitialSelectionEvent; + /// /// Computes the target Y positions for every item in the carousel. /// @@ -787,13 +795,21 @@ namespace osu.Game.Screens.Select Scroll.ScrollContent.Height = currentY; - if (BeatmapSetsLoaded && (selectedBeatmapSet == null || selectedBeatmap == null || selectedBeatmapSet.State.Value != CarouselItemState.Selected)) - { - selectedBeatmapSet = null; - SelectionChanged?.Invoke(null); - } - itemsCache.Validate(); + + // update and let external consumers know about selection loss. + if (BeatmapSetsLoaded) + { + bool selectionLost = selectedBeatmapSet != null && selectedBeatmapSet.State.Value != CarouselItemState.Selected; + + if (selectionLost || !sentInitialSelectionEvent) + { + selectedBeatmapSet = null; + SelectionChanged?.Invoke(null); + + sentInitialSelectionEvent = true; + } + } } private bool firstScroll = true; @@ -816,14 +832,13 @@ namespace osu.Game.Screens.Select break; case PendingScrollOperation.Immediate: + // in order to simplify animation logic, rather than using the animated version of ScrollTo, // we take the difference in scroll height and apply to all visible panels. // this avoids edge cases like when the visible panels is reduced suddenly, causing ScrollContainer // to enter clamp-special-case mode where it animates completely differently to normal. float scrollChange = scrollTarget.Value - Scroll.Current; - Scroll.ScrollTo(scrollTarget.Value, false); - foreach (var i in Scroll.Children) i.Y += scrollChange; break; From 1a443381247492b9233ae9c01ebf1a44510359ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Jan 2021 15:38:28 +0900 Subject: [PATCH 5818/6909] Use SingleOrDefault for added safety when looking up mod acronyms --- osu.Game/Online/API/APIMod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 780e5daa16..6c988e2e00 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -36,7 +36,7 @@ namespace osu.Game.Online.API public Mod ToMod(Ruleset ruleset) { - Mod resultMod = ruleset.GetAllMods().FirstOrDefault(m => m.Acronym == Acronym); + Mod resultMod = ruleset.GetAllMods().SingleOrDefault(m => m.Acronym == Acronym); if (resultMod == null) throw new InvalidOperationException($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}."); From 23e216fa0b374e23751c97124dc069877004a60d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Jan 2021 15:47:15 +0900 Subject: [PATCH 5819/6909] Simplify some default value checks (we are sure the return is an IBindable) --- osu.Game/Online/API/APIMod.cs | 7 ++++++- osu.Game/Rulesets/Mods/Mod.cs | 8 +++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 6c988e2e00..a7e5f0d6f9 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -31,7 +31,12 @@ namespace osu.Game.Online.API Acronym = mod.Acronym; foreach (var (_, property) in mod.GetSettingsSourceProperties()) - Settings.Add(property.Name.Underscore(), property.GetValue(mod)); + { + var bindable = (IBindable)property.GetValue(mod); + + if (!bindable.IsDefault) + Settings.Add(property.Name.Underscore(), property.GetValue(mod)); + } } public Mod ToMod(Ruleset ruleset) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b8dc7a2661..92b548a9cc 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -84,12 +84,10 @@ namespace osu.Game.Rulesets.Mods foreach ((SettingSourceAttribute attr, PropertyInfo property) in this.GetOrderedSettingsSourceProperties()) { - object bindableObj = property.GetValue(this); + var bindable = (IBindable)property.GetValue(this); - if ((bindableObj as IHasDefaultValue)?.IsDefault == true) - continue; - - tooltipTexts.Add($"{attr.Label} {bindableObj}"); + if (!bindable.IsDefault) + tooltipTexts.Add($"{attr.Label} {bindable}"); } return string.Join(", ", tooltipTexts.Where(s => !string.IsNullOrEmpty(s))); From 29dbb1cc0d94d26b3fb077e924bd2eb6d6b16dfd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Jan 2021 15:48:28 +0900 Subject: [PATCH 5820/6909] Add internal pathway for ensuring correct application of bindable mods --- osu.Game/Online/API/APIMod.cs | 2 +- osu.Game/Rulesets/Mods/Mod.cs | 21 ++++++++++++------- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 6 ++++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index a7e5f0d6f9..6e3bdac61a 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -51,7 +51,7 @@ namespace osu.Game.Online.API if (!Settings.TryGetValue(property.Name.Underscore(), out object settingValue)) continue; - ((IBindable)property.GetValue(resultMod)).Parse(settingValue); + resultMod.CopyAdjustedSetting((IBindable)property.GetValue(resultMod), settingValue); } return resultMod; diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 92b548a9cc..487cdedd13 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; using System.Reflection; using Newtonsoft.Json; @@ -134,19 +133,25 @@ namespace osu.Game.Rulesets.Mods // Copy bindable values across foreach (var (_, prop) in this.GetSettingsSourceProperties()) { - var origBindable = prop.GetValue(this); - var copyBindable = prop.GetValue(copy); + var origBindable = (IBindable)prop.GetValue(this); + var copyBindable = (IBindable)prop.GetValue(copy); - // The bindables themselves are readonly, so the value must be transferred through the Bindable.Value property. - var valueProperty = origBindable.GetType().GetProperty(nameof(Bindable.Value), BindingFlags.Public | BindingFlags.Instance); - Debug.Assert(valueProperty != null); - - valueProperty.SetValue(copyBindable, valueProperty.GetValue(origBindable)); + // we only care about changes that have been made away from defaults. + if (!origBindable.IsDefault) + copy.CopyAdjustedSetting(copyBindable, origBindable); } return copy; } + /// + /// When creating copies or clones of a Mod, this method will be called to copy explicitly adjusted user settings. + /// The base implementation will transfer the value via and should be called unless replaced with custom logic. + /// + /// The target bindable to apply the adjustment. + /// The adjustment to apply. + internal virtual void CopyAdjustedSetting(IBindable bindable, object value) => bindable.Parse(value); + public bool Equals(IMod other) => GetType() == other?.GetType(); } } diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 165644edbe..58af96a8df 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -114,6 +114,12 @@ namespace osu.Game.Rulesets.Mods bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault; } + internal override void CopyAdjustedSetting(IBindable bindable, object value) + { + userChangedSettings[bindable] = true; + base.CopyAdjustedSetting(bindable, value); + } + /// /// Apply all custom settings to the provided beatmap. /// From 99fa0e25dcb542e98e6bdf3c71de45a891634806 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Jan 2021 16:46:24 +0900 Subject: [PATCH 5821/6909] Switch back to FirstOrDefault to allow for weird testing logic to pass --- osu.Game/Online/API/APIMod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 6e3bdac61a..3ceafc5160 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -41,7 +41,7 @@ namespace osu.Game.Online.API public Mod ToMod(Ruleset ruleset) { - Mod resultMod = ruleset.GetAllMods().SingleOrDefault(m => m.Acronym == Acronym); + Mod resultMod = ruleset.GetAllMods().FirstOrDefault(m => m.Acronym == Acronym); if (resultMod == null) throw new InvalidOperationException($"There is no mod in the ruleset ({ruleset.ShortName}) matching the acronym {Acronym}."); From 6ad1b7767e6ded5cd992838a7395c7205378b90c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Jan 2021 17:04:16 +0900 Subject: [PATCH 5822/6909] Update osu.Game/Online/API/APIMod.cs Co-authored-by: Salman Ahmed --- osu.Game/Online/API/APIMod.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 3ceafc5160..c8b76b9685 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -35,7 +35,7 @@ namespace osu.Game.Online.API var bindable = (IBindable)property.GetValue(mod); if (!bindable.IsDefault) - Settings.Add(property.Name.Underscore(), property.GetValue(mod)); + Settings.Add(property.Name.Underscore(), bindable); } } From 7c9f345cd27ff36ea5e638a53fb1c12f74712361 Mon Sep 17 00:00:00 2001 From: LavaDesu Date: Sun, 3 Jan 2021 16:46:25 +0700 Subject: [PATCH 5823/6909] Use better naming for DiscordRichPresenceMode --- osu.Desktop/DiscordRichPresence.cs | 2 +- osu.Game/Configuration/DiscordRichPresenceMode.cs | 4 ++-- osu.Game/Configuration/OsuConfigManager.cs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 6b331d4952..172db324cb 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -82,7 +82,7 @@ namespace osu.Desktop if (!client.IsInitialized) return; - if (status.Value is UserStatusOffline || mode.Value == DiscordRichPresenceMode.Disabled) + if (status.Value is UserStatusOffline || mode.Value == DiscordRichPresenceMode.Off) { client.ClearPresence(); return; diff --git a/osu.Game/Configuration/DiscordRichPresenceMode.cs b/osu.Game/Configuration/DiscordRichPresenceMode.cs index bd39faff4d..2e58e3554b 100644 --- a/osu.Game/Configuration/DiscordRichPresenceMode.cs +++ b/osu.Game/Configuration/DiscordRichPresenceMode.cs @@ -7,11 +7,11 @@ namespace osu.Game.Configuration { public enum DiscordRichPresenceMode { - Disabled, + Off, [Description("Hide identifiable information")] Limited, - Enabled + Full } } diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index c733fe2fb4..eb34a0885d 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -138,7 +138,7 @@ namespace osu.Game.Configuration Set(OsuSetting.MenuBackgroundSource, BackgroundSource.Skin); Set(OsuSetting.SeasonalBackgroundMode, SeasonalBackgroundMode.Sometimes); - Set(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Enabled); + Set(OsuSetting.DiscordRichPresence, DiscordRichPresenceMode.Full); Set(OsuSetting.EditorWaveformOpacity, 1f); } From 2501707d7d86c9927fda5f3ece0d75c14e769bb0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Jan 2021 20:44:57 +0900 Subject: [PATCH 5824/6909] Copy values using Bind to also copy defaults --- osu.Game/Rulesets/Mods/Mod.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 487cdedd13..d2747d98c7 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -150,7 +150,17 @@ namespace osu.Game.Rulesets.Mods /// /// The target bindable to apply the adjustment. /// The adjustment to apply. - internal virtual void CopyAdjustedSetting(IBindable bindable, object value) => bindable.Parse(value); + internal virtual void CopyAdjustedSetting(IBindable bindable, object value) + { + if (value is IBindable incoming) + { + //copy including transfer of default values. + bindable.BindTo(incoming); + bindable.UnbindFrom(incoming); + } + else + bindable.Parse(value); + } public bool Equals(IMod other) => GetType() == other?.GetType(); } From a3e29b9154c996efbe051da95e9c8bbaac8c2096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Jan 2021 13:25:44 +0100 Subject: [PATCH 5825/6909] Rename parameters for readability --- osu.Game/Rulesets/Mods/Mod.cs | 14 +++++++------- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index d2747d98c7..b7432ec966 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -148,18 +148,18 @@ namespace osu.Game.Rulesets.Mods /// When creating copies or clones of a Mod, this method will be called to copy explicitly adjusted user settings. /// The base implementation will transfer the value via and should be called unless replaced with custom logic. /// - /// The target bindable to apply the adjustment. - /// The adjustment to apply. - internal virtual void CopyAdjustedSetting(IBindable bindable, object value) + /// The target bindable to apply the adjustment. + /// The adjustment to apply. + internal virtual void CopyAdjustedSetting(IBindable target, object source) { - if (value is IBindable incoming) + if (source is IBindable sourceBindable) { //copy including transfer of default values. - bindable.BindTo(incoming); - bindable.UnbindFrom(incoming); + target.BindTo(sourceBindable); + target.UnbindFrom(sourceBindable); } else - bindable.Parse(value); + target.Parse(source); } public bool Equals(IMod other) => GetType() == other?.GetType(); diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 58af96a8df..72a4bb297f 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -114,10 +114,10 @@ namespace osu.Game.Rulesets.Mods bindable.ValueChanged += _ => userChangedSettings[bindable] = !bindable.IsDefault; } - internal override void CopyAdjustedSetting(IBindable bindable, object value) + internal override void CopyAdjustedSetting(IBindable target, object source) { - userChangedSettings[bindable] = true; - base.CopyAdjustedSetting(bindable, value); + userChangedSettings[target] = true; + base.CopyAdjustedSetting(target, source); } /// From 9e4a925ab1f071fced203cc081f2c6f0b7c2ca96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 3 Jan 2021 13:30:21 +0100 Subject: [PATCH 5826/6909] Clarify & cleanup comments some --- osu.Game/Rulesets/Mods/Mod.cs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index b7432ec966..24d184e531 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -145,16 +145,19 @@ namespace osu.Game.Rulesets.Mods } /// - /// When creating copies or clones of a Mod, this method will be called to copy explicitly adjusted user settings. - /// The base implementation will transfer the value via and should be called unless replaced with custom logic. + /// When creating copies or clones of a Mod, this method will be called + /// to copy explicitly adjusted user settings from . + /// The base implementation will transfer the value via + /// or by binding and unbinding (if is an ) + /// and should be called unless replaced with custom logic. /// - /// The target bindable to apply the adjustment. + /// The target bindable to apply the adjustment to. /// The adjustment to apply. internal virtual void CopyAdjustedSetting(IBindable target, object source) { if (source is IBindable sourceBindable) { - //copy including transfer of default values. + // copy including transfer of default values. target.BindTo(sourceBindable); target.UnbindFrom(sourceBindable); } From efb71713efdde039ab6a3d0fce58476e6ef6d575 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Jan 2021 22:43:02 +0900 Subject: [PATCH 5827/6909] Fix null condition inhibiting deselection events --- osu.Game/Screens/Select/SongSelect.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 8ad1ace36a..e3036c662b 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -428,7 +428,10 @@ namespace osu.Game.Screens.Select private void updateSelectedBeatmap(BeatmapInfo beatmap) { - if (beatmap == null || beatmap.Equals(beatmapNoDebounce)) + if (beatmap == null && beatmapNoDebounce == null) + return; + + if (beatmap?.Equals(beatmapNoDebounce) == true) return; beatmapNoDebounce = beatmap; From 53e6a349bbed8492c62b66ff8b47333da8101230 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 3 Jan 2021 22:44:30 +0900 Subject: [PATCH 5828/6909] Fix incorrect initial conditional Turns out this wasn't actually required. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 37213c6003..36f8fbedb3 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -733,12 +733,6 @@ namespace osu.Game.Screens.Select private const float panel_padding = 5; - /// - /// After loading, we want to invoke a selection changed event at least once. - /// This handles the case where this event is potentially sending a null selection. - /// - private bool sentInitialSelectionEvent; - /// /// Computes the target Y positions for every item in the carousel. /// @@ -802,12 +796,10 @@ namespace osu.Game.Screens.Select { bool selectionLost = selectedBeatmapSet != null && selectedBeatmapSet.State.Value != CarouselItemState.Selected; - if (selectionLost || !sentInitialSelectionEvent) + if (selectionLost) { selectedBeatmapSet = null; SelectionChanged?.Invoke(null); - - sentInitialSelectionEvent = true; } } } From 152e9ecccf0f6150236d70a0d90f6d3a721e87aa Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Jan 2021 18:34:11 +0300 Subject: [PATCH 5829/6909] Make `BeatmapAvailability` class in-line with other online data structures --- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 2 +- osu.Game/Online/Rooms/BeatmapAvailability.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 92c92f438d..f515b574df 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -20,7 +20,7 @@ namespace osu.Game.Online.Multiplayer /// /// The availability state of the beatmap, set to by default. /// - public BeatmapAvailability BeatmapAvailability { get; set; } = new BeatmapAvailability(DownloadState.LocallyAvailable); + public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable(); public User? User { get; set; } diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index ca53cb2295..04dcd2a84a 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -8,7 +8,7 @@ namespace osu.Game.Online.Rooms /// /// The local availability information about a certain beatmap for the client. /// - public readonly struct BeatmapAvailability : IEquatable + public class BeatmapAvailability : IEquatable { /// /// The beatmap's availability state. @@ -49,7 +49,7 @@ namespace osu.Game.Online.Rooms DownloadProgress = downloadProgress; } - public bool Equals(BeatmapAvailability other) => State == other.State && DownloadProgress == other.DownloadProgress; + public bool Equals(BeatmapAvailability other) => other != null && State == other.State && DownloadProgress == other.DownloadProgress; public override string ToString() => $"{string.Join(", ", State, $"{DownloadProgress:0.00%}")}"; } From c8423d1c4600d89bf4979715e7da540ca118633c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Jan 2021 18:34:58 +0300 Subject: [PATCH 5830/6909] Make constructors design more pleasent to eyes --- osu.Game/Online/Rooms/BeatmapAvailability.cs | 30 ++++---------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index 04dcd2a84a..1fd099fcc7 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -20,35 +20,17 @@ namespace osu.Game.Online.Rooms /// public readonly double? DownloadProgress; - /// - /// Constructs a new non- beatmap availability state. - /// - /// The beatmap availability state. - /// Throws if was specified in this constructor, as it has its own constructor (see . - public BeatmapAvailability(DownloadState state) + private BeatmapAvailability(DownloadState state, double? downloadProgress = null) { - if (state == DownloadState.Downloading) - throw new ArgumentException($"{nameof(DownloadState.Downloading)} state has its own constructor, use it instead."); - State = state; - DownloadProgress = null; - } - - /// - /// Constructs a new -specific beatmap availability state. - /// - /// The beatmap availability state (always ). - /// The beatmap's downloading current progress. - /// Throws if non- was specified in this constructor, as they have their own constructor (see . - public BeatmapAvailability(DownloadState state, double downloadProgress) - { - if (state != DownloadState.Downloading) - throw new ArgumentException($"This is a constructor specific for {DownloadState.Downloading} state, use the regular one instead."); - - State = DownloadState.Downloading; DownloadProgress = downloadProgress; } + public static BeatmapAvailability NotDownload() => new BeatmapAvailability(DownloadState.NotDownloaded); + public static BeatmapAvailability Downloading(double progress) => new BeatmapAvailability(DownloadState.Downloading, progress); + public static BeatmapAvailability Downloaded() => new BeatmapAvailability(DownloadState.Downloaded); + public static BeatmapAvailability LocallyAvailable() => new BeatmapAvailability(DownloadState.LocallyAvailable); + public bool Equals(BeatmapAvailability other) => other != null && State == other.State && DownloadProgress == other.DownloadProgress; public override string ToString() => $"{string.Join(", ", State, $"{DownloadProgress:0.00%}")}"; From 839f5a75705f92cb77dfb75392ecf748a7f955d9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 3 Jan 2021 18:36:37 +0300 Subject: [PATCH 5831/6909] Ensure clients don't blow up when given user isn't in room --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index c1818de87a..799b66020c 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -320,10 +320,13 @@ namespace osu.Game.Online.Multiplayer Scheduler.Add(() => { - if (Room == null) + var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + + // we don't care whether the room doesn't exist or user isn't in joined room, just return in that point. + if (user == null) return; - Room.Users.Single(u => u.UserID == userId).BeatmapAvailability = beatmapAvailability; + user.BeatmapAvailability = beatmapAvailability; RoomUpdated?.Invoke(); }, false); From df04dd21de6a16da70031ee4fac020ae0f923211 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 4 Jan 2021 07:45:29 +0300 Subject: [PATCH 5832/6909] Add failing test case --- .../Beatmaps/IO/ImportBeatmapTest.cs | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index c32e359de6..041df3e6ff 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -852,6 +852,39 @@ namespace osu.Game.Tests.Beatmaps.IO } } + [Test] + public async Task TestItemRemovedShouldPassConsumableBeatmapSet() + { + using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) + { + try + { + var osu = LoadOsuIntoHost(host); + var manager = osu.Dependencies.Get(); + + var removedQueue = new Queue(); + manager.ItemRemoved.BindValueChanged(evt => + { + if (evt.NewValue.TryGetTarget(out var target)) + removedQueue.Enqueue(target); + }); + + var imported = await LoadOszIntoOsu(osu); + deleteBeatmapSet(imported, osu); + + Assert.That(removedQueue.Count, Is.EqualTo(1)); + + var removedItem = removedQueue.Single(); + Assert.That(removedItem.Metadata, Is.EqualTo(imported.Metadata)); + Assert.That(removedItem.Beatmaps, Is.EquivalentTo(imported.Beatmaps)); + } + finally + { + host.Exit(); + } + } + } + public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) { var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); From 738c94d1938f99ff5fb638528cff02bf80f297df Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 4 Jan 2021 07:46:51 +0300 Subject: [PATCH 5833/6909] Update soft-deletion logic to use model store's consumable items instead --- osu.Game/Database/ArchiveModelManager.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 36cc4cce39..61b2f7668e 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -76,7 +76,7 @@ namespace osu.Game.Database protected readonly IDatabaseContextFactory ContextFactory; - protected readonly MutableDatabaseBackedStore ModelStore; + protected readonly MutableDatabaseBackedStoreWithFileIncludes ModelStore; // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ArchiveImportIPCChannel ipc; @@ -492,7 +492,7 @@ namespace osu.Game.Database using (ContextFactory.GetForWrite()) { // re-fetch the model on the import context. - var foundModel = queryModel().Include(s => s.Files).ThenInclude(f => f.FileInfo).FirstOrDefault(s => s.ID == item.ID); + var foundModel = ModelStore.ConsumableItems.SingleOrDefault(i => i.ID == item.ID); if (foundModel == null || foundModel.DeletePending) return false; @@ -731,8 +731,6 @@ namespace osu.Game.Database yield return f.Filename; } - private DbSet queryModel() => ContextFactory.Get().Set(); - protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}"; #region Event handling / delaying From 1463ff288629232333880a0f18888c4f1a65e5d6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 4 Jan 2021 08:12:31 +0300 Subject: [PATCH 5834/6909] Remove unnecessary using directive --- osu.Game/Database/ArchiveModelManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 61b2f7668e..b9f805ae31 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Humanizer; using JetBrains.Annotations; -using Microsoft.EntityFrameworkCore; using osu.Framework; using osu.Framework.Bindables; using osu.Framework.Extensions; From ca5f2bcd4ca8395698a5267d6a8fb2a6cb214e45 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 4 Jan 2021 08:49:03 +0300 Subject: [PATCH 5835/6909] Revert database-side changes --- .../Beatmaps/IO/ImportBeatmapTest.cs | 33 ------------------- osu.Game/Database/ArchiveModelManager.cs | 6 ++-- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index 041df3e6ff..c32e359de6 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -852,39 +852,6 @@ namespace osu.Game.Tests.Beatmaps.IO } } - [Test] - public async Task TestItemRemovedShouldPassConsumableBeatmapSet() - { - using (HeadlessGameHost host = new CleanRunHeadlessGameHost(nameof(ImportBeatmapTest))) - { - try - { - var osu = LoadOsuIntoHost(host); - var manager = osu.Dependencies.Get(); - - var removedQueue = new Queue(); - manager.ItemRemoved.BindValueChanged(evt => - { - if (evt.NewValue.TryGetTarget(out var target)) - removedQueue.Enqueue(target); - }); - - var imported = await LoadOszIntoOsu(osu); - deleteBeatmapSet(imported, osu); - - Assert.That(removedQueue.Count, Is.EqualTo(1)); - - var removedItem = removedQueue.Single(); - Assert.That(removedItem.Metadata, Is.EqualTo(imported.Metadata)); - Assert.That(removedItem.Beatmaps, Is.EquivalentTo(imported.Beatmaps)); - } - finally - { - host.Exit(); - } - } - } - public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) { var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index b9f805ae31..81d8668196 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -75,7 +75,7 @@ namespace osu.Game.Database protected readonly IDatabaseContextFactory ContextFactory; - protected readonly MutableDatabaseBackedStoreWithFileIncludes ModelStore; + protected readonly MutableDatabaseBackedStore ModelStore; // ReSharper disable once NotAccessedField.Local (we should keep a reference to this so it is not finalised) private ArchiveImportIPCChannel ipc; @@ -491,7 +491,7 @@ namespace osu.Game.Database using (ContextFactory.GetForWrite()) { // re-fetch the model on the import context. - var foundModel = ModelStore.ConsumableItems.SingleOrDefault(i => i.ID == item.ID); + var foundModel = queryModel().Include(s => s.Files).ThenInclude(f => f.FileInfo).FirstOrDefault(s => s.ID == item.ID); if (foundModel == null || foundModel.DeletePending) return false; @@ -730,6 +730,8 @@ namespace osu.Game.Database yield return f.Filename; } + private DbSet queryModel() => ContextFactory.Get().Set(); + protected virtual string HumanisedModelName => $"{typeof(TModel).Name.Replace("Info", "").ToLower()}"; #region Event handling / delaying From 445a4bd01c70428f33bc36b603a52e80e92d0aeb Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 4 Jan 2021 08:51:37 +0300 Subject: [PATCH 5836/6909] Re-query beatmap info on database changes --- osu.Game/Database/ArchiveModelManager.cs | 1 + .../OnlinePlay/Components/ReadyButton.cs | 34 ++++--------------- 2 files changed, 8 insertions(+), 27 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 81d8668196..36cc4cce39 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -9,6 +9,7 @@ using System.Threading; using System.Threading.Tasks; using Humanizer; using JetBrains.Annotations; +using Microsoft.EntityFrameworkCore; using osu.Framework; using osu.Framework.Bindables; using osu.Framework.Extensions; diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 08f89d8ed8..144535ed4c 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -2,8 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Linq; -using System.Linq.Expressions; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Beatmaps; @@ -41,38 +39,20 @@ namespace osu.Game.Screens.OnlinePlay.Components SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true); } - private void updateSelectedItem(PlaylistItem item) - { - hasBeatmap = findBeatmap(expr => beatmaps.QueryBeatmap(expr)); - } + private void updateSelectedItem(PlaylistItem _) => updateBeatmapState(); + private void beatmapUpdated(ValueChangedEvent> _) => updateBeatmapState(); + private void beatmapRemoved(ValueChangedEvent> _) => updateBeatmapState(); - private void beatmapUpdated(ValueChangedEvent> weakSet) - { - if (weakSet.NewValue.TryGetTarget(out var set)) - { - if (findBeatmap(expr => set.Beatmaps.AsQueryable().FirstOrDefault(expr))) - Schedule(() => hasBeatmap = true); - } - } - - private void beatmapRemoved(ValueChangedEvent> weakSet) - { - if (weakSet.NewValue.TryGetTarget(out var set)) - { - if (findBeatmap(expr => set.Beatmaps.AsQueryable().FirstOrDefault(expr))) - Schedule(() => hasBeatmap = false); - } - } - - private bool findBeatmap(Func>, BeatmapInfo> expression) + private void updateBeatmapState() { int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID; string checksum = SelectedItem.Value?.Beatmap.Value?.MD5Hash; if (beatmapId == null || checksum == null) - return false; + return; - return expression(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum) != null; + var databasedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); + hasBeatmap = databasedBeatmap != null && !databasedBeatmap.BeatmapSet.DeletePending; } protected override void Update() From cb7df0fe1172935581909b938cab6fe987fcc5ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 15:14:39 +0900 Subject: [PATCH 5837/6909] Add failing test for storyboard start time ordering --- .../Formats/LegacyStoryboardDecoderTest.cs | 20 +++++++++++++++++++ .../Resources/out-of-order-starttimes.osb | 6 ++++++ 2 files changed, 26 insertions(+) create mode 100644 osu.Game.Tests/Resources/out-of-order-starttimes.osb diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 9ebedb3c80..b36597a949 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -95,6 +95,26 @@ namespace osu.Game.Tests.Beatmaps.Formats } } + [Test] + public void TestOutOfOrderStartTimes() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("out-of-order-starttimes.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer background = storyboard.Layers.Single(l => l.Depth == 3); + Assert.AreEqual(2, background.Elements.Count); + + Assert.AreEqual(1500, background.Elements[0].StartTime); + Assert.AreEqual(1000, background.Elements[1].StartTime); + + Assert.AreEqual(1000, storyboard.FirstEventTime); + } + } + [Test] public void TestDecodeVariableWithSuffix() { diff --git a/osu.Game.Tests/Resources/out-of-order-starttimes.osb b/osu.Game.Tests/Resources/out-of-order-starttimes.osb new file mode 100644 index 0000000000..09988ff64e --- /dev/null +++ b/osu.Game.Tests/Resources/out-of-order-starttimes.osb @@ -0,0 +1,6 @@ +[Events] +//Storyboard Layer 0 (Background) +Sprite,Background,TopCentre,"img.jpg",320,240 + F,0,1500,1600,0,1 +Sprite,Background,TopCentre,"img.jpg",320,240 + F,0,1000,1100,0,1 From 20d04d69332f18b7f14eedc44fed5bdf0ae9e9a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 15:16:01 +0900 Subject: [PATCH 5838/6909] Fix Storyboard's FirstEventTime not finding the true earliest event --- .../Beatmaps/Formats/LegacyStoryboardDecoderTest.cs | 2 +- osu.Game/Screens/Play/GameplayClockContainer.cs | 4 +++- osu.Game/Storyboards/Storyboard.cs | 10 +++++++++- 3 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index b36597a949..7bee580863 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -111,7 +111,7 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(1500, background.Elements[0].StartTime); Assert.AreEqual(1000, background.Elements[1].StartTime); - Assert.AreEqual(1000, storyboard.FirstEventTime); + Assert.AreEqual(1000, storyboard.EarliestEventTime); } } diff --git a/osu.Game/Screens/Play/GameplayClockContainer.cs b/osu.Game/Screens/Play/GameplayClockContainer.cs index 0248432917..ddbb087962 100644 --- a/osu.Game/Screens/Play/GameplayClockContainer.cs +++ b/osu.Game/Screens/Play/GameplayClockContainer.cs @@ -131,7 +131,9 @@ namespace osu.Game.Screens.Play // if a storyboard is present, it may dictate the appropriate start time by having events in negative time space. // this is commonly used to display an intro before the audio track start. - startTime = Math.Min(startTime, beatmap.Storyboard.FirstEventTime); + double? firstStoryboardEvent = beatmap.Storyboard.EarliestEventTime; + if (firstStoryboardEvent != null) + startTime = Math.Min(startTime, firstStoryboardEvent.Value); // some beatmaps specify a current lead-in time which should be used instead of the ruleset-provided value when available. // this is not available as an option in the live editor but can still be applied via .osu editing. diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index e0d18eab00..d4ba18d394 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using Microsoft.EntityFrameworkCore.Internal; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; @@ -27,7 +28,14 @@ namespace osu.Game.Storyboards public bool HasDrawable => Layers.Any(l => l.Elements.Any(e => e.IsDrawable)); - public double FirstEventTime => Layers.Min(l => l.Elements.FirstOrDefault()?.StartTime ?? 0); + /// + /// Across all layers, find the earliest point in time that a storyboard element exists at. + /// Will return null if there are no elements. + /// + /// + /// This iterates all elements and as such should be used sparingly or stored locally. + /// + public double? EarliestEventTime => Layers.SelectMany(l => l.Elements).OrderBy(e => e.StartTime).FirstOrDefault()?.StartTime; /// /// Depth of the currently front-most storyboard layer, excluding the overlay layer. From 9e0c490141e4e85ecde7c96d7a6b3f37f0b3b4fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 15:40:22 +0900 Subject: [PATCH 5839/6909] Remove unused using --- osu.Game/Storyboards/Storyboard.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Storyboards/Storyboard.cs b/osu.Game/Storyboards/Storyboard.cs index d4ba18d394..1ba25cc11e 100644 --- a/osu.Game/Storyboards/Storyboard.cs +++ b/osu.Game/Storyboards/Storyboard.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using Microsoft.EntityFrameworkCore.Internal; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; From ea38b00b29adabf0ec91b0b085f2af0b11829b23 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 4 Jan 2021 10:27:08 +0300 Subject: [PATCH 5840/6909] Schedule all calls to `updateBeatmapState()` --- osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 144535ed4c..6f86f2b879 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -39,9 +39,9 @@ namespace osu.Game.Screens.OnlinePlay.Components SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true); } - private void updateSelectedItem(PlaylistItem _) => updateBeatmapState(); - private void beatmapUpdated(ValueChangedEvent> _) => updateBeatmapState(); - private void beatmapRemoved(ValueChangedEvent> _) => updateBeatmapState(); + private void updateSelectedItem(PlaylistItem _) => Scheduler.AddOnce(updateBeatmapState); + private void beatmapUpdated(ValueChangedEvent> _) => Scheduler.AddOnce(updateBeatmapState); + private void beatmapRemoved(ValueChangedEvent> _) => Scheduler.AddOnce(updateBeatmapState); private void updateBeatmapState() { From 485a57776b205581528925011a1d2ec77d8fc26d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 4 Jan 2021 10:28:41 +0300 Subject: [PATCH 5841/6909] Fix `hasBeatmap` potentially checking on outdated `DeletePending` value --- osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 6f86f2b879..b782df75df 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -52,7 +52,15 @@ namespace osu.Game.Screens.OnlinePlay.Components return; var databasedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); - hasBeatmap = databasedBeatmap != null && !databasedBeatmap.BeatmapSet.DeletePending; + + if (databasedBeatmap == null) + hasBeatmap = false; + else + { + // DeletePending isn't updated in the beatmap info query above, need to directly query the beatmap set from database as well. + var databasedSet = beatmaps.QueryBeatmapSet(s => s.Equals(databasedBeatmap.BeatmapSet)); + hasBeatmap = databasedSet?.DeletePending == false; + } } protected override void Update() From ba4e41142262d683e25185af98e13a07157214ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 16:37:07 +0900 Subject: [PATCH 5842/6909] Clone and copy ControlPointInfo when retrieving a playable beatmap --- osu.Game/Beatmaps/ControlPoints/ControlPoint.cs | 17 +++++++++++++++++ .../Beatmaps/ControlPoints/ControlPointInfo.cs | 12 +++++++++++- .../ControlPoints/DifficultyControlPoint.cs | 7 +++++++ .../ControlPoints/EffectControlPoint.cs | 8 ++++++++ .../ControlPoints/SampleControlPoint.cs | 8 ++++++++ .../ControlPoints/TimingControlPoint.cs | 8 ++++++++ osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 15 +++++++++++++-- osu.Game/Beatmaps/IBeatmap.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 3 +++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 +++++- osu.Game/Screens/Play/GameplayBeatmap.cs | 6 +++++- 11 files changed, 86 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index c6649f6af1..090675473d 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -28,5 +28,22 @@ namespace osu.Game.Beatmaps.ControlPoints /// An existing control point to compare with. /// Whether this is redundant when placed alongside . public abstract bool IsRedundant(ControlPoint existing); + + /// + /// Create a copy of this room without online information. + /// Should be used to create a local copy of a room for submitting in the future. + /// + public ControlPoint CreateCopy() + { + var copy = (ControlPoint)Activator.CreateInstance(GetType()); + + copy.CopyFrom(this); + + return copy; + } + + public virtual void CopyFrom(ControlPoint other) + { + } } } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index b843aad950..b56f9a106b 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -11,7 +11,7 @@ using osu.Framework.Lists; namespace osu.Game.Beatmaps.ControlPoints { [Serializable] - public class ControlPointInfo + public class ControlPointInfo : ICloneable { /// /// All control points grouped by time. @@ -297,5 +297,15 @@ namespace osu.Game.Beatmaps.ControlPoints break; } } + + public object Clone() + { + var controlPointInfo = new ControlPointInfo(); + + foreach (var point in AllControlPoints) + controlPointInfo.Add(point.Time, point.CreateCopy()); + + return controlPointInfo; + } } } diff --git a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs index 283bf76572..0bc5605051 100644 --- a/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/DifficultyControlPoint.cs @@ -39,5 +39,12 @@ namespace osu.Game.Beatmaps.ControlPoints public override bool IsRedundant(ControlPoint existing) => existing is DifficultyControlPoint existingDifficulty && SpeedMultiplier == existingDifficulty.SpeedMultiplier; + + public override void CopyFrom(ControlPoint other) + { + SpeedMultiplier = ((DifficultyControlPoint)other).SpeedMultiplier; + + base.CopyFrom(other); + } } } diff --git a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs index ea28fca170..79bc88e773 100644 --- a/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/EffectControlPoint.cs @@ -50,5 +50,13 @@ namespace osu.Game.Beatmaps.ControlPoints && existing is EffectControlPoint existingEffect && KiaiMode == existingEffect.KiaiMode && OmitFirstBarLine == existingEffect.OmitFirstBarLine; + + public override void CopyFrom(ControlPoint other) + { + KiaiMode = ((EffectControlPoint)other).KiaiMode; + OmitFirstBarLine = ((EffectControlPoint)other).OmitFirstBarLine; + + base.CopyFrom(other); + } } } diff --git a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs index fd0b496335..4aa6a3d6e9 100644 --- a/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/SampleControlPoint.cs @@ -72,5 +72,13 @@ namespace osu.Game.Beatmaps.ControlPoints => existing is SampleControlPoint existingSample && SampleBank == existingSample.SampleBank && SampleVolume == existingSample.SampleVolume; + + public override void CopyFrom(ControlPoint other) + { + SampleVolume = ((SampleControlPoint)other).SampleVolume; + SampleBank = ((SampleControlPoint)other).SampleBank; + + base.CopyFrom(other); + } } } diff --git a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs index d9378bca4a..580642f593 100644 --- a/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/TimingControlPoint.cs @@ -69,5 +69,13 @@ namespace osu.Game.Beatmaps.ControlPoints // Timing points are never redundant as they can change the time signature. public override bool IsRedundant(ControlPoint existing) => false; + + public override void CopyFrom(ControlPoint other) + { + TimeSignature = ((TimingControlPoint)other).TimeSignature; + BeatLength = ((TimingControlPoint)other).BeatLength; + + base.CopyFrom(other); + } } } diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index c9d139bdd0..06ff677aed 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -164,13 +164,24 @@ namespace osu.Game.Beatmaps.Formats /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. /// DO NOT USE THIS UNLESS 100% SURE. /// - public readonly float BpmMultiplier; + public float BpmMultiplier { get; set; } public LegacyDifficultyControlPoint(double beatLength) + : this() + { + BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100f : 1; + } + + public LegacyDifficultyControlPoint() { SpeedMultiplierBindable.Precision = double.Epsilon; + } - BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100f : 1; + public override void CopyFrom(ControlPoint other) + { + base.CopyFrom(other); + + BpmMultiplier = ((LegacyDifficultyControlPoint)other).BpmMultiplier; } } diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 8f27e0b0e9..7dd85e1232 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps /// /// The control points in this beatmap. /// - ControlPointInfo ControlPointInfo { get; } + ControlPointInfo ControlPointInfo { get; set; } /// /// The breaks in this beatmap. diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 30382c444f..06b5913b18 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -14,6 +14,7 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Framework.Statistics; using osu.Framework.Testing; +using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Types; @@ -111,6 +112,8 @@ namespace osu.Game.Beatmaps // Convert IBeatmap converted = converter.Convert(cancellationSource.Token); + converted.ControlPointInfo = (ControlPointInfo)converted.ControlPointInfo.Clone(); + // Apply conversion mods to the result foreach (var mod in mods.OfType()) { diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 165d2ba278..a54a95f59d 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -74,7 +74,11 @@ namespace osu.Game.Screens.Edit public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; - public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo; + public ControlPointInfo ControlPointInfo + { + get => PlayableBeatmap.ControlPointInfo; + set => PlayableBeatmap.ControlPointInfo = value; + } public List Breaks => PlayableBeatmap.Breaks; diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index 64894544f4..565595656f 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -29,7 +29,11 @@ namespace osu.Game.Screens.Play public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; - public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo; + public ControlPointInfo ControlPointInfo + { + get => PlayableBeatmap.ControlPointInfo; + set => PlayableBeatmap.ControlPointInfo = value; + } public List Breaks => PlayableBeatmap.Breaks; From b4a779108e7c87994198690cba57ca05e1a0e503 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 16:37:49 +0900 Subject: [PATCH 5843/6909] Ensure working beatmap is reloaded on exiting the editor --- osu.Game/Screens/Edit/Editor.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 223c678fba..a9fe7ba38d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -483,6 +483,8 @@ namespace osu.Game.Screens.Edit Background.FadeColour(Color4.White, 500); resetTrack(); + Beatmap.Value = beatmapManager.GetWorkingBeatmap(Beatmap.Value.BeatmapInfo); + return base.OnExiting(next); } From 7fdf876b4c8fb19020033d080fca3ee08dd44adc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 16:38:15 +0900 Subject: [PATCH 5844/6909] Fix editor timing screen mutating the WorkingBeatmap instead of EditorBeatmap --- osu.Game/Screens/Edit/Timing/DifficultySection.cs | 2 +- osu.Game/Screens/Edit/Timing/EffectSection.cs | 2 +- osu.Game/Screens/Edit/Timing/GroupSection.cs | 9 ++++----- osu.Game/Screens/Edit/Timing/SampleSection.cs | 2 +- osu.Game/Screens/Edit/Timing/Section.cs | 3 +-- osu.Game/Screens/Edit/Timing/TimingScreen.cs | 11 +++++------ osu.Game/Screens/Edit/Timing/TimingSection.cs | 2 +- 7 files changed, 14 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/DifficultySection.cs b/osu.Game/Screens/Edit/Timing/DifficultySection.cs index b55d74e3b4..b87b8961f8 100644 --- a/osu.Game/Screens/Edit/Timing/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Timing/DifficultySection.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Timing protected override DifficultyControlPoint CreatePoint() { - var reference = Beatmap.Value.Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time); + var reference = Beatmap.ControlPointInfo.DifficultyPointAt(SelectedGroup.Value.Time); return new DifficultyControlPoint { diff --git a/osu.Game/Screens/Edit/Timing/EffectSection.cs b/osu.Game/Screens/Edit/Timing/EffectSection.cs index 2f143108a9..6d23b52c05 100644 --- a/osu.Game/Screens/Edit/Timing/EffectSection.cs +++ b/osu.Game/Screens/Edit/Timing/EffectSection.cs @@ -37,7 +37,7 @@ namespace osu.Game.Screens.Edit.Timing protected override EffectControlPoint CreatePoint() { - var reference = Beatmap.Value.Beatmap.ControlPointInfo.EffectPointAt(SelectedGroup.Value.Time); + var reference = Beatmap.ControlPointInfo.EffectPointAt(SelectedGroup.Value.Time); return new EffectControlPoint { diff --git a/osu.Game/Screens/Edit/Timing/GroupSection.cs b/osu.Game/Screens/Edit/Timing/GroupSection.cs index 2605ea8b75..2e2c380d4a 100644 --- a/osu.Game/Screens/Edit/Timing/GroupSection.cs +++ b/osu.Game/Screens/Edit/Timing/GroupSection.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; @@ -24,7 +23,7 @@ namespace osu.Game.Screens.Edit.Timing protected Bindable SelectedGroup { get; private set; } [Resolved] - protected IBindable Beatmap { get; private set; } + protected EditorBeatmap Beatmap { get; private set; } [Resolved] private EditorClock clock { get; set; } @@ -107,13 +106,13 @@ namespace osu.Game.Screens.Edit.Timing var currentGroupItems = SelectedGroup.Value.ControlPoints.ToArray(); - Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value); + Beatmap.ControlPointInfo.RemoveGroup(SelectedGroup.Value); foreach (var cp in currentGroupItems) - Beatmap.Value.Beatmap.ControlPointInfo.Add(time, cp); + Beatmap.ControlPointInfo.Add(time, cp); // the control point might not necessarily exist yet, if currentGroupItems was empty. - SelectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(time, true); + SelectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(time, true); changeHandler?.EndChange(); } diff --git a/osu.Game/Screens/Edit/Timing/SampleSection.cs b/osu.Game/Screens/Edit/Timing/SampleSection.cs index 280e19c99a..cc73af6349 100644 --- a/osu.Game/Screens/Edit/Timing/SampleSection.cs +++ b/osu.Game/Screens/Edit/Timing/SampleSection.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Edit.Timing protected override SampleControlPoint CreatePoint() { - var reference = Beatmap.Value.Beatmap.ControlPointInfo.SamplePointAt(SelectedGroup.Value.Time); + var reference = Beatmap.ControlPointInfo.SamplePointAt(SelectedGroup.Value.Time); return new SampleControlPoint { diff --git a/osu.Game/Screens/Edit/Timing/Section.cs b/osu.Game/Screens/Edit/Timing/Section.cs index 7a81eeb1a4..5269fa9774 100644 --- a/osu.Game/Screens/Edit/Timing/Section.cs +++ b/osu.Game/Screens/Edit/Timing/Section.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.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; @@ -27,7 +26,7 @@ namespace osu.Game.Screens.Edit.Timing private const float header_height = 20; [Resolved] - protected IBindable Beatmap { get; private set; } + protected EditorBeatmap Beatmap { get; private set; } [Resolved] protected Bindable SelectedGroup { get; private set; } diff --git a/osu.Game/Screens/Edit/Timing/TimingScreen.cs b/osu.Game/Screens/Edit/Timing/TimingScreen.cs index eab909b798..c5d2dd756a 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.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -62,7 +61,7 @@ namespace osu.Game.Screens.Edit.Timing private EditorClock clock { get; set; } [Resolved] - protected IBindable Beatmap { get; private set; } + protected EditorBeatmap Beatmap { get; private set; } [Resolved] private Bindable selectedGroup { get; set; } @@ -124,7 +123,7 @@ namespace osu.Game.Screens.Edit.Timing selectedGroup.BindValueChanged(selected => { deleteButton.Enabled.Value = selected.NewValue != null; }, true); - controlPointGroups.BindTo(Beatmap.Value.Beatmap.ControlPointInfo.Groups); + controlPointGroups.BindTo(Beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((sender, args) => { table.ControlGroups = controlPointGroups; @@ -137,14 +136,14 @@ namespace osu.Game.Screens.Edit.Timing if (selectedGroup.Value == null) return; - Beatmap.Value.Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value); + Beatmap.ControlPointInfo.RemoveGroup(selectedGroup.Value); - selectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime); + selectedGroup.Value = Beatmap.ControlPointInfo.Groups.FirstOrDefault(g => g.Time >= clock.CurrentTime); } private void addNew() { - selectedGroup.Value = Beatmap.Value.Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true); + selectedGroup.Value = Beatmap.ControlPointInfo.GroupAt(clock.CurrentTime, true); } } } diff --git a/osu.Game/Screens/Edit/Timing/TimingSection.cs b/osu.Game/Screens/Edit/Timing/TimingSection.cs index 1ae2a86885..a0bb9ac506 100644 --- a/osu.Game/Screens/Edit/Timing/TimingSection.cs +++ b/osu.Game/Screens/Edit/Timing/TimingSection.cs @@ -49,7 +49,7 @@ namespace osu.Game.Screens.Edit.Timing protected override TimingControlPoint CreatePoint() { - var reference = Beatmap.Value.Beatmap.ControlPointInfo.TimingPointAt(SelectedGroup.Value.Time); + var reference = Beatmap.ControlPointInfo.TimingPointAt(SelectedGroup.Value.Time); return new TimingControlPoint { From b7dd54847fc97e98ac5d0eef7d06b5b4d12b509b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 16:47:08 +0900 Subject: [PATCH 5845/6909] Move resolved usage of WorkingBeatmap in editor components as local as possible to avoid misuse --- .../Screens/Edit/Compose/ComposeScreen.cs | 5 ++++- osu.Game/Screens/Edit/EditorScreen.cs | 5 ----- .../Screens/Edit/Setup/DifficultySection.cs | 16 ++++++++-------- .../Screens/Edit/Setup/MetadataSection.cs | 16 ++++++++-------- .../Screens/Edit/Setup/ResourcesSection.cs | 19 +++++++++++-------- osu.Game/Screens/Edit/Setup/SetupSection.cs | 4 +--- 6 files changed, 32 insertions(+), 33 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs index c297a03dbf..81b1195a40 100644 --- a/osu.Game/Screens/Edit/Compose/ComposeScreen.cs +++ b/osu.Game/Screens/Edit/Compose/ComposeScreen.cs @@ -16,6 +16,9 @@ namespace osu.Game.Screens.Edit.Compose { public class ComposeScreen : EditorScreenWithTimeline { + [Resolved] + private IBindable beatmap { get; set; } + private HitObjectComposer composer; public ComposeScreen() @@ -59,7 +62,7 @@ namespace osu.Game.Screens.Edit.Compose { Debug.Assert(ruleset != null); - var beatmapSkinProvider = new BeatmapSkinProvidingContainer(Beatmap.Value.Skin); + var beatmapSkinProvider = new BeatmapSkinProvidingContainer(beatmap.Value.Skin); // the beatmapSkinProvider is used as the fallback source here to allow the ruleset-specific skin implementation // full access to all skin sources. diff --git a/osu.Game/Screens/Edit/EditorScreen.cs b/osu.Game/Screens/Edit/EditorScreen.cs index 4d62a7d3cd..7fbb6a8ca0 100644 --- a/osu.Game/Screens/Edit/EditorScreen.cs +++ b/osu.Game/Screens/Edit/EditorScreen.cs @@ -2,10 +2,8 @@ // 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; namespace osu.Game.Screens.Edit { @@ -14,9 +12,6 @@ namespace osu.Game.Screens.Edit /// public abstract class EditorScreen : Container { - [Resolved] - protected IBindable Beatmap { get; private set; } - [Resolved] protected EditorBeatmap EditorBeatmap { get; private set; } diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index 897ddc6955..f180d7e63e 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = "Object Size", Description = "The size of all hit objects", - Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize) + Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.CircleSize) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -46,7 +46,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = "Health Drain", Description = "The rate of passive health drain throughout playable time", - Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate) + Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.DrainRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -58,7 +58,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = "Approach Rate", Description = "The speed at which objects are presented to the player", - Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate) + Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -70,7 +70,7 @@ namespace osu.Game.Screens.Edit.Setup { Label = "Overall Difficulty", Description = "The harshness of hit windows and difficulty of special objects (ie. spinners)", - Current = new BindableFloat(Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty) + Current = new BindableFloat(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty) { Default = BeatmapDifficulty.DEFAULT_DIFFICULTY, MinValue = 0, @@ -88,10 +88,10 @@ namespace osu.Game.Screens.Edit.Setup { // for now, update these on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. - Beatmap.Value.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value; - Beatmap.Value.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value; - Beatmap.Value.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value; - Beatmap.Value.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value; + Beatmap.BeatmapInfo.BaseDifficulty.CircleSize = circleSizeSlider.Current.Value; + Beatmap.BeatmapInfo.BaseDifficulty.DrainRate = healthDrainSlider.Current.Value; + Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value; + Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value; editorBeatmap.UpdateAllHitObjects(); } diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 4ddee2acc6..e812c042fb 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -29,25 +29,25 @@ namespace osu.Game.Screens.Edit.Setup artistTextBox = new LabelledTextBox { Label = "Artist", - Current = { Value = Beatmap.Value.Metadata.Artist }, + Current = { Value = Beatmap.Metadata.Artist }, TabbableContentContainer = this }, titleTextBox = new LabelledTextBox { Label = "Title", - Current = { Value = Beatmap.Value.Metadata.Title }, + Current = { Value = Beatmap.Metadata.Title }, TabbableContentContainer = this }, creatorTextBox = new LabelledTextBox { Label = "Creator", - Current = { Value = Beatmap.Value.Metadata.AuthorString }, + Current = { Value = Beatmap.Metadata.AuthorString }, TabbableContentContainer = this }, difficultyTextBox = new LabelledTextBox { Label = "Difficulty Name", - Current = { Value = Beatmap.Value.BeatmapInfo.Version }, + Current = { Value = Beatmap.BeatmapInfo.Version }, TabbableContentContainer = this }, }; @@ -62,10 +62,10 @@ namespace osu.Game.Screens.Edit.Setup // for now, update these on commit rather than making BeatmapMetadata bindables. // after switching database engines we can reconsider if switching to bindables is a good direction. - Beatmap.Value.Metadata.Artist = artistTextBox.Current.Value; - Beatmap.Value.Metadata.Title = titleTextBox.Current.Value; - Beatmap.Value.Metadata.AuthorString = creatorTextBox.Current.Value; - Beatmap.Value.BeatmapInfo.Version = difficultyTextBox.Current.Value; + Beatmap.Metadata.Artist = artistTextBox.Current.Value; + Beatmap.Metadata.Title = titleTextBox.Current.Value; + Beatmap.Metadata.AuthorString = creatorTextBox.Current.Value; + Beatmap.BeatmapInfo.Version = difficultyTextBox.Current.Value; } } } diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index 0c957b80af..b3bceceea3 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -42,6 +42,9 @@ namespace osu.Game.Screens.Edit.Setup [Resolved] private BeatmapManager beatmaps { get; set; } + [Resolved] + private IBindable working { get; set; } + [Resolved(canBeNull: true)] private Editor editor { get; set; } @@ -70,7 +73,7 @@ namespace osu.Game.Screens.Edit.Setup audioTrackTextBox = new FileChooserLabelledTextBox { Label = "Audio Track", - Current = { Value = Beatmap.Value.Metadata.AudioFile ?? "Click to select a track" }, + Current = { Value = Beatmap.Metadata.AudioFile ?? "Click to select a track" }, Target = audioTrackFileChooserContainer, TabbableContentContainer = this }, @@ -115,11 +118,11 @@ namespace osu.Game.Screens.Edit.Setup if (!info.Exists) return false; - var set = Beatmap.Value.BeatmapSetInfo; + var set = working.Value.BeatmapSetInfo; // remove the previous background for now. // in the future we probably want to check if this is being used elsewhere (other difficulties?) - var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.BackgroundFile); + var oldFile = set.Files.FirstOrDefault(f => f.Filename == working.Value.Metadata.BackgroundFile); using (var stream = info.OpenRead()) { @@ -129,7 +132,7 @@ namespace osu.Game.Screens.Edit.Setup beatmaps.AddFile(set, stream, info.Name); } - Beatmap.Value.Metadata.BackgroundFile = info.Name; + working.Value.Metadata.BackgroundFile = info.Name; updateBackgroundSprite(); return true; @@ -148,11 +151,11 @@ namespace osu.Game.Screens.Edit.Setup if (!info.Exists) return false; - var set = Beatmap.Value.BeatmapSetInfo; + var set = working.Value.BeatmapSetInfo; // remove the previous audio track for now. // in the future we probably want to check if this is being used elsewhere (other difficulties?) - var oldFile = set.Files.FirstOrDefault(f => f.Filename == Beatmap.Value.Metadata.AudioFile); + var oldFile = set.Files.FirstOrDefault(f => f.Filename == working.Value.Metadata.AudioFile); using (var stream = info.OpenRead()) { @@ -162,7 +165,7 @@ namespace osu.Game.Screens.Edit.Setup beatmaps.AddFile(set, stream, info.Name); } - Beatmap.Value.Metadata.AudioFile = info.Name; + working.Value.Metadata.AudioFile = info.Name; music.ReloadCurrentTrack(); @@ -178,7 +181,7 @@ namespace osu.Game.Screens.Edit.Setup private void updateBackgroundSprite() { - LoadComponentAsync(new BeatmapBackgroundSprite(Beatmap.Value) + LoadComponentAsync(new BeatmapBackgroundSprite(working.Value) { RelativeSizeAxes = Axes.Both, Anchor = Anchor.Centre, diff --git a/osu.Game/Screens/Edit/Setup/SetupSection.cs b/osu.Game/Screens/Edit/Setup/SetupSection.cs index cdf17d355e..88521a8fb0 100644 --- a/osu.Game/Screens/Edit/Setup/SetupSection.cs +++ b/osu.Game/Screens/Edit/Setup/SetupSection.cs @@ -2,10 +2,8 @@ // 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.Graphics; using osuTK; @@ -19,7 +17,7 @@ namespace osu.Game.Screens.Edit.Setup protected OsuColour Colours { get; private set; } [Resolved] - protected IBindable Beatmap { get; private set; } + protected EditorBeatmap Beatmap { get; private set; } protected override Container Content => flow; From 3b08faa0ea134d99f6cd9dcd02096df1ffac115a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 17:49:11 +0900 Subject: [PATCH 5846/6909] Fix RemoveBlockingOverlay causing transform mutation from disposal threads --- osu.Game/OsuGame.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 17831ed26b..36e3078653 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -151,11 +151,11 @@ namespace osu.Game updateBlockingOverlayFade(); } - public void RemoveBlockingOverlay(OverlayContainer overlay) + public void RemoveBlockingOverlay(OverlayContainer overlay) => Schedule(() => { visibleBlockingOverlays.Remove(overlay); updateBlockingOverlayFade(); - } + }); /// /// Close all game-wide overlays. From 2d1b52be0d8e02ffc4c4fbbd6a9f8f621d54f211 Mon Sep 17 00:00:00 2001 From: KyeKiller Date: Mon, 4 Jan 2021 17:21:31 +0000 Subject: [PATCH 5847/6909] Moved "ToolbarSocialButton" This will remove it from coming off the screen. --- osu.Game/Overlays/Toolbar/Toolbar.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 393e349bd0..88a2518f84 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -69,12 +69,13 @@ namespace osu.Game.Overlays.Toolbar AutoSizeAxes = Axes.X, Children = new Drawable[] { + + new ToolbarSocialButton(), new ToolbarNewsButton(), new ToolbarChangelogButton(), new ToolbarRankingsButton(), new ToolbarBeatmapListingButton(), new ToolbarChatButton(), - new ToolbarSocialButton(), new ToolbarMusicButton(), //new ToolbarButton //{ From 2e2b3ab5d4f66cd0295cd939a209c51485d59b41 Mon Sep 17 00:00:00 2001 From: KyeKiller Date: Mon, 4 Jan 2021 17:26:42 +0000 Subject: [PATCH 5848/6909] Should remove codeFactor error --- osu.Game/Overlays/Toolbar/Toolbar.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 88a2518f84..c95d02cea4 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -69,7 +69,6 @@ namespace osu.Game.Overlays.Toolbar AutoSizeAxes = Axes.X, Children = new Drawable[] { - new ToolbarSocialButton(), new ToolbarNewsButton(), new ToolbarChangelogButton(), From 73f5e5aaf9fb94726b6906f98b6493202fedb680 Mon Sep 17 00:00:00 2001 From: KyeKiller Date: Mon, 4 Jan 2021 21:03:51 +0000 Subject: [PATCH 5849/6909] Moved "ToolbarSocialButton" back --- 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 c95d02cea4..393e349bd0 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -69,12 +69,12 @@ namespace osu.Game.Overlays.Toolbar AutoSizeAxes = Axes.X, Children = new Drawable[] { - new ToolbarSocialButton(), new ToolbarNewsButton(), new ToolbarChangelogButton(), new ToolbarRankingsButton(), new ToolbarBeatmapListingButton(), new ToolbarChatButton(), + new ToolbarSocialButton(), new ToolbarMusicButton(), //new ToolbarButton //{ From 3468df840b37291dfdae94af3fb0d29a8e7493fa Mon Sep 17 00:00:00 2001 From: KyeKiller Date: Mon, 4 Jan 2021 21:04:30 +0000 Subject: [PATCH 5850/6909] Moved tooltip to the left to stop the overflow --- osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index e62c7bc807..ca334702ce 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -2,12 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { public class ToolbarSocialButton : ToolbarOverlayToggleButton { + + protected override Anchor TooltipAnchor => Anchor.TopRight; + public ToolbarSocialButton() { Hotkey = GlobalAction.ToggleSocial; From 0e42d415c18888192358e4f7e2a41074ebd0530f Mon Sep 17 00:00:00 2001 From: KyeKiller Date: Mon, 4 Jan 2021 21:05:28 +0000 Subject: [PATCH 5851/6909] Hit another oopie --- osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs index ca334702ce..1e00afc5fd 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarSocialButton.cs @@ -9,7 +9,6 @@ namespace osu.Game.Overlays.Toolbar { public class ToolbarSocialButton : ToolbarOverlayToggleButton { - protected override Anchor TooltipAnchor => Anchor.TopRight; public ToolbarSocialButton() From 1234d0fa0409c786c4e4ec4e2a63b5a5da406292 Mon Sep 17 00:00:00 2001 From: KyeKiller Date: Mon, 4 Jan 2021 22:01:12 +0000 Subject: [PATCH 5852/6909] Applied all tooltips to the right --- osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs | 3 +++ osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs | 5 ++++- osu.Game/Overlays/Toolbar/ToolbarChatButton.cs | 3 +++ osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs | 3 +++ osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs | 3 +++ 5 files changed, 16 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs index 0363873326..c495d673ce 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs @@ -2,12 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { public class ToolbarBeatmapListingButton : ToolbarOverlayToggleButton { + protected override Anchor TooltipAnchor => Anchor.TopRight; + public ToolbarBeatmapListingButton() { Hotkey = GlobalAction.ToggleDirect; diff --git a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs index 23f8b141b2..28112d178f 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs @@ -2,11 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; namespace osu.Game.Overlays.Toolbar { public class ToolbarChangelogButton : ToolbarOverlayToggleButton - { + { + protected override Anchor TooltipAnchor => Anchor.TopRight; + [BackgroundDependencyLoader(true)] private void load(ChangelogOverlay changelog) { diff --git a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs index f9a66ae7bb..2d3b33e9bc 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChatButton.cs @@ -2,12 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { public class ToolbarChatButton : ToolbarOverlayToggleButton { + protected override Anchor TooltipAnchor => Anchor.TopRight; + public ToolbarChatButton() { Hotkey = GlobalAction.ToggleChat; diff --git a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs index 0ba2935c80..9b2573ad07 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarNewsButton.cs @@ -2,11 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; namespace osu.Game.Overlays.Toolbar { public class ToolbarNewsButton : ToolbarOverlayToggleButton { + protected override Anchor TooltipAnchor => Anchor.TopRight; + [BackgroundDependencyLoader(true)] private void load(NewsOverlay news) { diff --git a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs index 22a01bcdb5..312fc41aab 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarRankingsButton.cs @@ -2,11 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; +using osu.Framework.Graphics; namespace osu.Game.Overlays.Toolbar { public class ToolbarRankingsButton : ToolbarOverlayToggleButton { + protected override Anchor TooltipAnchor => Anchor.TopRight; + [BackgroundDependencyLoader(true)] private void load(RankingsOverlay rankings) { From 77e660e42677eee4d2407b3b59a649f836b35f34 Mon Sep 17 00:00:00 2001 From: KyeKiller Date: Mon, 4 Jan 2021 22:11:52 +0000 Subject: [PATCH 5853/6909] Should pass all checks again now. --- osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs index 28112d178f..86bc73361a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarChangelogButton.cs @@ -7,7 +7,7 @@ using osu.Framework.Graphics; namespace osu.Game.Overlays.Toolbar { public class ToolbarChangelogButton : ToolbarOverlayToggleButton - { + { protected override Anchor TooltipAnchor => Anchor.TopRight; [BackgroundDependencyLoader(true)] From 81355652fa8601d4537cb6998afe6208da1b925d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 5 Jan 2021 06:00:15 +0300 Subject: [PATCH 5854/6909] Add simple test coverage --- .../TestSceneMultiplayerReadyButton.cs | 29 +++++++++++++++---- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 6b11613f1c..03ba73d35b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -7,8 +7,10 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Graphics; using osu.Framework.Platform; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; @@ -23,6 +25,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneMultiplayerReadyButton : MultiplayerTestScene { private MultiplayerReadyButton button; + private BeatmapSetInfo importedSet; private BeatmapManager beatmaps; private RulesetStore rulesets; @@ -38,9 +41,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUp] public new void Setup() => Schedule(() => { - var beatmap = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First().Beatmaps.First(); - - Beatmap.Value = beatmaps.GetWorkingBeatmap(beatmap); + importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); + Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); Child = button = new MultiplayerReadyButton { @@ -51,13 +53,30 @@ namespace osu.Game.Tests.Visual.Multiplayer { Value = new PlaylistItem { - Beatmap = { Value = beatmap }, - Ruleset = { Value = beatmap.Ruleset } + Beatmap = { Value = Beatmap.Value.BeatmapInfo }, + Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset } } } }; }); + [Test] + public void TestDeletedBeatmapDisableReady() + { + OsuButton readyButton = null; + + AddAssert("ensure ready button enabled", () => + { + readyButton = button.ChildrenOfType().Single(); + return readyButton.Enabled.Value; + }); + + AddStep("delete beatmap", () => beatmaps.Delete(importedSet)); + AddAssert("ready button disabled", () => !readyButton.Enabled.Value); + AddStep("undelete beatmap", () => beatmaps.Undelete(importedSet)); + AddAssert("ready button enabled back", () => readyButton.Enabled.Value); + } + [Test] public void TestToggleStateWhenNotHost() { From caa88c6100cb17d3307336ab43e00f16fbe98b01 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 13:13:51 +0900 Subject: [PATCH 5855/6909] Use CreateCopy instead of Clone interface I was going for conformity by using the IClonable interface, but it doesn't look like we use it anywhere else in the project. --- osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs | 4 ++-- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index b56f9a106b..e8a91e4001 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -11,7 +11,7 @@ using osu.Framework.Lists; namespace osu.Game.Beatmaps.ControlPoints { [Serializable] - public class ControlPointInfo : ICloneable + public class ControlPointInfo { /// /// All control points grouped by time. @@ -298,7 +298,7 @@ namespace osu.Game.Beatmaps.ControlPoints } } - public object Clone() + public ControlPointInfo CreateCopy() { var controlPointInfo = new ControlPointInfo(); diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 06b5913b18..8688017887 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -112,7 +112,7 @@ namespace osu.Game.Beatmaps // Convert IBeatmap converted = converter.Convert(cancellationSource.Token); - converted.ControlPointInfo = (ControlPointInfo)converted.ControlPointInfo.Clone(); + converted.ControlPointInfo = converted.ControlPointInfo.CreateCopy(); // Apply conversion mods to the result foreach (var mod in mods.OfType()) From 385c9cd2e23e9f66b3b316b11b594c5377d27b77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 13:14:16 +0900 Subject: [PATCH 5856/6909] Add test coverage --- .../NonVisual/ControlPointInfoTest.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs index 90a487c0ac..b27c257795 100644 --- a/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs +++ b/osu.Game.Tests/NonVisual/ControlPointInfoTest.cs @@ -246,5 +246,32 @@ namespace osu.Game.Tests.NonVisual Assert.That(cpi.DifficultyPoints.Count, Is.EqualTo(0)); Assert.That(cpi.AllControlPoints.Count, Is.EqualTo(0)); } + + [Test] + public void TestCreateCopyIsDeepClone() + { + var cpi = new ControlPointInfo(); + + cpi.Add(1000, new TimingControlPoint { BeatLength = 500 }); + + var cpiCopy = cpi.CreateCopy(); + + cpiCopy.Add(2000, new TimingControlPoint { BeatLength = 500 }); + + Assert.That(cpi.Groups.Count, Is.EqualTo(1)); + Assert.That(cpiCopy.Groups.Count, Is.EqualTo(2)); + + Assert.That(cpi.TimingPoints.Count, Is.EqualTo(1)); + Assert.That(cpiCopy.TimingPoints.Count, Is.EqualTo(2)); + + Assert.That(cpi.TimingPoints[0], Is.Not.SameAs(cpiCopy.TimingPoints[0])); + Assert.That(cpi.TimingPoints[0].BeatLengthBindable, Is.Not.SameAs(cpiCopy.TimingPoints[0].BeatLengthBindable)); + + Assert.That(cpi.TimingPoints[0].BeatLength, Is.EqualTo(cpiCopy.TimingPoints[0].BeatLength)); + + cpi.TimingPoints[0].BeatLength = 800; + + Assert.That(cpi.TimingPoints[0].BeatLength, Is.Not.EqualTo(cpiCopy.TimingPoints[0].BeatLength)); + } } } From 6b8e1913eee030038ed4af72be5168d0c2ba7fab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 13:27:45 +0900 Subject: [PATCH 5857/6909] Fix dependency not always available due to nested LoadComponentAsync call --- osu.Game/Screens/Edit/EditorScreenWithTimeline.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs index b9457f422a..2d623a200c 100644 --- a/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs +++ b/osu.Game/Screens/Edit/EditorScreenWithTimeline.cs @@ -30,16 +30,16 @@ namespace osu.Game.Screens.Edit { } + private Container mainContent; + + private LoadingSpinner spinner; + [BackgroundDependencyLoader(true)] private void load([CanBeNull] BindableBeatDivisor beatDivisor) { if (beatDivisor != null) this.beatDivisor.BindTo(beatDivisor); - Container mainContent; - - LoadingSpinner spinner; - Children = new Drawable[] { mainContent = new Container @@ -99,6 +99,11 @@ namespace osu.Game.Screens.Edit } }, }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); LoadComponentAsync(CreateMainContent(), content => { From afab35a31ad71e8f25e0d9084dc4edbb8e0183b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 13:41:31 +0900 Subject: [PATCH 5858/6909] Fix missing copy implementation in LegacySampleControlPiont --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 06ff677aed..a06ad35b89 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -203,6 +203,13 @@ namespace osu.Game.Beatmaps.Formats => base.IsRedundant(existing) && existing is LegacySampleControlPoint existingSample && CustomSampleBank == existingSample.CustomSampleBank; + + public override void CopyFrom(ControlPoint other) + { + base.CopyFrom(other); + + CustomSampleBank = ((LegacySampleControlPoint)other).CustomSampleBank; + } } } } From 31a6e9b860ceb9ee1ece9441beb41b8aca042f5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 14:24:49 +0900 Subject: [PATCH 5859/6909] Remove unused using --- osu.Game/Beatmaps/WorkingBeatmap.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 8688017887..d25adca92b 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -14,7 +14,6 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Logging; using osu.Framework.Statistics; using osu.Framework.Testing; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects.Types; From ed6ffe2ef1b55d957851391d4635d0e93585dcf2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 14:54:59 +0900 Subject: [PATCH 5860/6909] Remove hacky code --- osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index b782df75df..64ddba669d 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -53,14 +53,7 @@ namespace osu.Game.Screens.OnlinePlay.Components var databasedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); - if (databasedBeatmap == null) - hasBeatmap = false; - else - { - // DeletePending isn't updated in the beatmap info query above, need to directly query the beatmap set from database as well. - var databasedSet = beatmaps.QueryBeatmapSet(s => s.Equals(databasedBeatmap.BeatmapSet)); - hasBeatmap = databasedSet?.DeletePending == false; - } + hasBeatmap = databasedBeatmap?.BeatmapSet?.DeletePending == false; } protected override void Update() From 962c95dc0158bfddf43ad8128dc4e1db2a131cb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 19:06:00 +0900 Subject: [PATCH 5861/6909] Fix ModSelection making unsafe advances of ModSection --- osu.Game/Overlays/Mods/ModSection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 0107f94dcf..d00a365dde 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -130,7 +130,7 @@ namespace osu.Game.Overlays.Mods /// Select one or more mods in this section and deselects all other ones. /// /// The types of s which should be selected. - public void SelectTypes(IEnumerable modTypes) + public void SelectTypes(IEnumerable modTypes) => Schedule(() => { foreach (var button in buttons) { @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Mods else button.Deselect(); } - } + }); protected ModSection() { From a3e4e2f6c394544efae9df90228690fbdd3660c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 18:24:21 +0900 Subject: [PATCH 5862/6909] Switch ResultsScreen and SongSelect inheritance and remove local implementation --- osu.Game/Screens/Ranking/ResultsScreen.cs | 4 +--- osu.Game/Screens/Select/SongSelect.cs | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 528a1842af..98794627fe 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -25,7 +25,7 @@ using osuTK; namespace osu.Game.Screens.Ranking { - public abstract class ResultsScreen : OsuScreen, IKeyBindingHandler + public abstract class ResultsScreen : ScreenWithBeatmapBackground, IKeyBindingHandler { protected const float BACKGROUND_BLUR = 20; private static readonly float screen_height = 768 - TwoLayerButton.SIZE_EXTENDED.Y; @@ -35,8 +35,6 @@ namespace osu.Game.Screens.Ranking // 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); - public readonly Bindable SelectedScore = new Bindable(); public readonly ScoreInfo Score; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index a5252fdc96..50a1dd6edf 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -41,7 +41,7 @@ using System.Diagnostics; namespace osu.Game.Screens.Select { - public abstract class SongSelect : OsuScreen, IKeyBindingHandler + public abstract class SongSelect : ScreenWithBeatmapBackground, IKeyBindingHandler { public static readonly float WEDGE_HEIGHT = 245; @@ -76,8 +76,6 @@ namespace osu.Game.Screens.Select [Resolved] private Bindable> selectedMods { get; set; } - protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); - protected BeatmapCarousel Carousel { get; private set; } private BeatmapInfoWedge beatmapInfoWedge; From b3f08b29ca60b99f6c3aa2f3a5d3dec679452a06 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 18:32:23 +0900 Subject: [PATCH 5863/6909] Ensure that all changes to screen backgrounds are on the correct thread --- .../Background/TestSceneUserDimBackgrounds.cs | 55 ++++++++++++------- osu.Game/Rulesets/Mods/ModCinema.cs | 2 +- osu.Game/Screens/BackgroundScreen.cs | 2 + osu.Game/Screens/Edit/Editor.cs | 13 +++-- osu.Game/Screens/Menu/MainMenu.cs | 6 +- osu.Game/Screens/OsuScreen.cs | 21 +++++-- osu.Game/Screens/Play/EpilepsyWarning.cs | 21 ++++++- osu.Game/Screens/Play/Player.cs | 17 ++++-- osu.Game/Screens/Play/PlayerLoader.cs | 32 +++++++---- .../Play/ScreenWithBeatmapBackground.cs | 3 +- osu.Game/Screens/Ranking/ResultsScreen.cs | 14 +++-- osu.Game/Screens/Select/SongSelect.cs | 6 +- 12 files changed, 132 insertions(+), 60 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 5323f58a66..b5df3285f0 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -82,7 +82,7 @@ namespace osu.Game.Tests.Visual.Background }); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddStep("Stop background preview", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); - AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect()); + AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.CheckBackgroundBlur(playerLoader.ExpectedBackgroundBlur)); } /// @@ -106,6 +106,7 @@ namespace osu.Game.Tests.Visual.Background public void TestStoryboardBackgroundVisibility() { performFullSetup(); + AddAssert("Background retained from song select", () => songSelect.IsBackgroundCurrent()); createFakeStoryboard(); AddStep("Enable Storyboard", () => { @@ -198,8 +199,9 @@ namespace osu.Game.Tests.Visual.Background }))); AddUntilStep("Wait for results is current", () => results.IsCurrentScreen()); + AddUntilStep("Screen is undimmed, original background retained", () => - songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && results.IsBlurCorrect()); + songSelect.IsBackgroundUndimmed() && songSelect.IsBackgroundCurrent() && songSelect.CheckBackgroundBlur(results.ExpectedBackgroundBlur)); } /// @@ -224,7 +226,7 @@ namespace osu.Game.Tests.Visual.Background AddStep("Resume PlayerLoader", () => player.Restart()); AddUntilStep("Screen is dimmed and blur applied", () => songSelect.IsBackgroundDimmed() && songSelect.IsUserBlurApplied()); AddStep("Move mouse to center of screen", () => InputManager.MoveMouseTo(playerLoader.ScreenPos)); - AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && playerLoader.IsBlurCorrect()); + AddUntilStep("Screen is undimmed and user blur removed", () => songSelect.IsBackgroundUndimmed() && songSelect.CheckBackgroundBlur(playerLoader.ExpectedBackgroundBlur)); } private void createFakeStoryboard() => AddStep("Create storyboard", () => @@ -274,9 +276,11 @@ namespace osu.Game.Tests.Visual.Background private class DummySongSelect : PlaySongSelect { + private FadeAccessibleBackground background; + protected override BackgroundScreen CreateBackground() { - FadeAccessibleBackground background = new FadeAccessibleBackground(Beatmap.Value); + background = new FadeAccessibleBackground(Beatmap.Value); DimEnabled.BindTo(background.EnableUserDim); return background; } @@ -294,42 +298,54 @@ namespace osu.Game.Tests.Visual.Background config.BindWith(OsuSetting.BlurLevel, BlurLevel); } - public bool IsBackgroundDimmed() => ((FadeAccessibleBackground)Background).CurrentColour == OsuColour.Gray(1f - ((FadeAccessibleBackground)Background).CurrentDim); + public bool IsBackgroundDimmed() => background.CurrentColour == OsuColour.Gray(1f - background.CurrentDim); - public bool IsBackgroundUndimmed() => ((FadeAccessibleBackground)Background).CurrentColour == Color4.White; + public bool IsBackgroundUndimmed() => background.CurrentColour == Color4.White; - public bool IsUserBlurApplied() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2((float)BlurLevel.Value * BackgroundScreenBeatmap.USER_BLUR_FACTOR); + public bool IsUserBlurApplied() => background.CurrentBlur == new Vector2((float)BlurLevel.Value * BackgroundScreenBeatmap.USER_BLUR_FACTOR); - public bool IsUserBlurDisabled() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(0); + public bool IsUserBlurDisabled() => background.CurrentBlur == new Vector2(0); - public bool IsBackgroundInvisible() => ((FadeAccessibleBackground)Background).CurrentAlpha == 0; + public bool IsBackgroundInvisible() => background.CurrentAlpha == 0; - public bool IsBackgroundVisible() => ((FadeAccessibleBackground)Background).CurrentAlpha == 1; + public bool IsBackgroundVisible() => background.CurrentAlpha == 1; - public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR); + public bool IsBlurCorrect() => background.CurrentBlur == new Vector2(BACKGROUND_BLUR); + + public bool CheckBackgroundBlur(Vector2 expected) => background.CurrentBlur == expected; /// /// Make sure every time a screen gets pushed, the background doesn't get replaced /// /// Whether or not the original background (The one created in DummySongSelect) is still the current background - public bool IsBackgroundCurrent() => ((FadeAccessibleBackground)Background).IsCurrentScreen(); + public bool IsBackgroundCurrent() => background?.IsCurrentScreen() == true; } private class FadeAccessibleResults : ResultsScreen { + private FadeAccessibleBackground background; + public FadeAccessibleResults(ScoreInfo score) : base(score, true) { } - protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value); + protected override BackgroundScreen CreateBackground() => background = new FadeAccessibleBackground(Beatmap.Value); - public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR); + public Vector2 ExpectedBackgroundBlur => new Vector2(BACKGROUND_BLUR); } private class LoadBlockingTestPlayer : TestPlayer { - protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value); + protected override BackgroundScreen CreateBackground() => + new FadeAccessibleBackground(Beatmap.Value); + + public override void OnEntering(IScreen last) + { + base.OnEntering(last); + + ApplyToBackground(b => ReplacesBackground.BindTo(b.StoryboardReplacesBackground)); + } public new DimmableStoryboard DimmableStoryboard => base.DimmableStoryboard; @@ -354,15 +370,16 @@ namespace osu.Game.Tests.Visual.Background Thread.Sleep(1); StoryboardEnabled = config.GetBindable(OsuSetting.ShowStoryboard); - ReplacesBackground.BindTo(Background.StoryboardReplacesBackground); DrawableRuleset.IsPaused.BindTo(IsPaused); } } private class TestPlayerLoader : PlayerLoader { + private FadeAccessibleBackground background; + public VisualSettings VisualSettingsPos => VisualSettings; - public BackgroundScreen ScreenPos => Background; + public BackgroundScreen ScreenPos => background; public TestPlayerLoader(Player player) : base(() => player) @@ -371,9 +388,9 @@ namespace osu.Game.Tests.Visual.Background public void TriggerOnHover() => OnHover(new HoverEvent(new InputState())); - public bool IsBlurCorrect() => ((FadeAccessibleBackground)Background).CurrentBlur == new Vector2(BACKGROUND_BLUR); + public Vector2 ExpectedBackgroundBlur => new Vector2(BACKGROUND_BLUR); - protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value); + protected override BackgroundScreen CreateBackground() => background = new FadeAccessibleBackground(Beatmap.Value); } private class FadeAccessibleBackground : BackgroundScreenBeatmap diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index cf8128301c..bee9e56edd 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -37,7 +37,7 @@ namespace osu.Game.Rulesets.Mods public void ApplyToPlayer(Player player) { - player.Background.EnableUserDim.Value = false; + player.ApplyToBackground(b => b.EnableUserDim.Value = false); player.DimmableStoryboard.IgnoreUserSettings.Value = true; diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index 0f3615b7a9..ea220c2f82 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -34,6 +34,8 @@ namespace osu.Game.Screens return false; } + public void ApplyToBackground(Action action) => Schedule(() => action.Invoke(this)); + protected override void Update() { base.Update(); diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 223c678fba..8c34cb2e08 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -444,11 +444,14 @@ namespace osu.Game.Screens.Edit { base.OnEntering(last); - // todo: temporary. we want to be applying dim using the UserDimContainer eventually. - Background.FadeColour(Color4.DarkGray, 500); + ApplyToBackground(b => + { + // todo: temporary. we want to be applying dim using the UserDimContainer eventually. + b.FadeColour(Color4.DarkGray, 500); - Background.EnableUserDim.Value = false; - Background.BlurAmount.Value = 0; + b.EnableUserDim.Value = false; + b.BlurAmount.Value = 0; + }); resetTrack(true); } @@ -480,7 +483,7 @@ namespace osu.Game.Screens.Edit } } - Background.FadeColour(Color4.White, 500); + ApplyToBackground(b => b.FadeColour(Color4.White, 500)); resetTrack(); return base.OnExiting(next); diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 9d5720ff34..97fd58318b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -127,11 +127,11 @@ namespace osu.Game.Screens.Menu { case ButtonSystemState.Initial: case ButtonSystemState.Exit: - Background.FadeColour(Color4.White, 500, Easing.OutSine); + ApplyToBackground(b => b.FadeColour(Color4.White, 500, Easing.OutSine)); break; default: - Background.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(0.8f), 500, Easing.OutSine)); break; } }; @@ -256,7 +256,7 @@ namespace osu.Game.Screens.Menu { base.OnResuming(last); - (Background as BackgroundScreenDefault)?.Next(); + ApplyToBackground(b => (b as BackgroundScreenDefault)?.Next()); // we may have consumed our preloaded instance, so let's make another. preloadSongSelect(); diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 851aedd84f..c97c0aef2b 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -114,9 +114,14 @@ namespace osu.Game.Screens Mods = screenDependencies.Mods; } - protected BackgroundScreen Background => backgroundStack?.CurrentScreen as BackgroundScreen; + public void ApplyToBackground(Action action) => background.ApplyToBackground(action); - private BackgroundScreen localBackground; + /// + /// The background created and owned by this screen. May be null if the background didn't change. + /// + private BackgroundScreen ownedBackground; + + private BackgroundScreen background; [Resolved(canBeNull: true)] private BackgroundScreenStack backgroundStack { get; set; } @@ -160,7 +165,15 @@ namespace osu.Game.Screens { applyArrivingDefaults(false); - backgroundStack?.Push(localBackground = CreateBackground()); + backgroundStack?.Push(ownedBackground = CreateBackground()); + + background = backgroundStack?.CurrentScreen as BackgroundScreen; + + if (background != ownedBackground) + { + // background may have not been replaced, at which point we don't want to track the background lifetime. + ownedBackground = null; + } base.OnEntering(last); } @@ -173,7 +186,7 @@ namespace osu.Game.Screens if (base.OnExiting(next)) return true; - if (localBackground != null && backgroundStack?.CurrentScreen == localBackground) + if (ownedBackground != null && backgroundStack?.CurrentScreen == ownedBackground) backgroundStack?.Exit(); return false; diff --git a/osu.Game/Screens/Play/EpilepsyWarning.cs b/osu.Game/Screens/Play/EpilepsyWarning.cs index dc42427fbf..89e25d849f 100644 --- a/osu.Game/Screens/Play/EpilepsyWarning.cs +++ b/osu.Game/Screens/Play/EpilepsyWarning.cs @@ -24,7 +24,19 @@ namespace osu.Game.Screens.Play Alpha = 0f; } - public BackgroundScreenBeatmap DimmableBackground { get; set; } + private BackgroundScreenBeatmap dimmableBackground; + + public BackgroundScreenBeatmap DimmableBackground + { + get => dimmableBackground; + set + { + dimmableBackground = value; + + if (IsLoaded) + updateBackgroundFade(); + } + } [BackgroundDependencyLoader] private void load(OsuColour colours, IBindable beatmap) @@ -75,11 +87,16 @@ namespace osu.Game.Screens.Play protected override void PopIn() { - DimmableBackground?.FadeColour(OsuColour.Gray(0.5f), FADE_DURATION, Easing.OutQuint); + updateBackgroundFade(); this.FadeIn(FADE_DURATION, Easing.OutQuint); } + private void updateBackgroundFade() + { + DimmableBackground?.FadeColour(OsuColour.Gray(0.5f), FADE_DURATION, Easing.OutQuint); + } + protected override void PopOut() => this.FadeOut(FADE_DURATION); } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bf2e6f5379..1fcbed7ef7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -721,15 +721,20 @@ namespace osu.Game.Screens.Play .Delay(250) .FadeIn(250); - Background.EnableUserDim.Value = true; - Background.BlurAmount.Value = 0; + ApplyToBackground(b => + { + b.EnableUserDim.Value = true; + b.BlurAmount.Value = 0; + + // bind component bindables. + b.IsBreakTime.BindTo(breakTracker.IsBreakTime); + + b.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); + }); - // bind component bindables. - Background.IsBreakTime.BindTo(breakTracker.IsBreakTime); HUDOverlay.IsBreakTime.BindTo(breakTracker.IsBreakTime); DimmableStoryboard.IsBreakTime.BindTo(breakTracker.IsBreakTime); - Background.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); DimmableStoryboard.StoryboardReplacesBackground.BindTo(storyboardReplacesBackground); storyboardReplacesBackground.Value = Beatmap.Value.Storyboard.ReplacesBackground && Beatmap.Value.Storyboard.HasDrawable; @@ -875,7 +880,7 @@ namespace osu.Game.Screens.Play float fadeOutDuration = instant ? 0 : 250; this.FadeOut(fadeOutDuration); - Background.EnableUserDim.Value = false; + ApplyToBackground(b => b.EnableUserDim.Value = false); storyboardReplacesBackground.Value = false; } diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index f59b36bc42..5b4bd11216 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Play backgroundBrightnessReduction = value; - Background.FadeColour(OsuColour.Gray(backgroundBrightnessReduction ? 0.8f : 1), 200); + ApplyToBackground(b => b.FadeColour(OsuColour.Gray(backgroundBrightnessReduction ? 0.8f : 1), 200)); } } @@ -176,12 +176,17 @@ namespace osu.Game.Screens.Play { base.OnEntering(last); - if (epilepsyWarning != null) - epilepsyWarning.DimmableBackground = Background; + ApplyToBackground(b => + { + if (epilepsyWarning != null) + epilepsyWarning.DimmableBackground = b; + + b?.FadeColour(Color4.White, 800, Easing.OutQuint); + }); + Beatmap.Value.Track.AddAdjustment(AdjustableProperty.Volume, volumeAdjustment); content.ScaleTo(0.7f); - Background?.FadeColour(Color4.White, 800, Easing.OutQuint); contentIn(); @@ -225,7 +230,8 @@ namespace osu.Game.Screens.Play content.ScaleTo(0.7f, 150, Easing.InQuint); this.FadeOut(150); - Background.EnableUserDim.Value = false; + ApplyToBackground(b => b.EnableUserDim.Value = false); + BackgroundBrightnessReduction = false; Beatmap.Value.Track.RemoveAdjustment(AdjustableProperty.Volume, volumeAdjustment); @@ -270,16 +276,22 @@ namespace osu.Game.Screens.Play if (inputManager.HoveredDrawables.Contains(VisualSettings)) { // Preview user-defined background dim and blur when hovered on the visual settings panel. - Background.EnableUserDim.Value = true; - Background.BlurAmount.Value = 0; + ApplyToBackground(b => + { + b.EnableUserDim.Value = true; + b.BlurAmount.Value = 0; + }); BackgroundBrightnessReduction = false; } else { - // Returns background dim and blur to the values specified by PlayerLoader. - Background.EnableUserDim.Value = false; - Background.BlurAmount.Value = BACKGROUND_BLUR; + ApplyToBackground(b => + { + // Returns background dim and blur to the values specified by PlayerLoader. + b.EnableUserDim.Value = false; + b.BlurAmount.Value = BACKGROUND_BLUR; + }); BackgroundBrightnessReduction = true; } diff --git a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs index 8eb253608b..88dab88d42 100644 --- a/osu.Game/Screens/Play/ScreenWithBeatmapBackground.cs +++ b/osu.Game/Screens/Play/ScreenWithBeatmapBackground.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.Screens.Backgrounds; namespace osu.Game.Screens.Play @@ -9,6 +10,6 @@ namespace osu.Game.Screens.Play { protected override BackgroundScreen CreateBackground() => new BackgroundScreenBeatmap(Beatmap.Value); - public new BackgroundScreenBeatmap Background => (BackgroundScreenBeatmap)base.Background; + public void ApplyToBackground(Action action) => base.ApplyToBackground(b => action.Invoke((BackgroundScreenBeatmap)b)); } } diff --git a/osu.Game/Screens/Ranking/ResultsScreen.cs b/osu.Game/Screens/Ranking/ResultsScreen.cs index 98794627fe..c1f5d92d17 100644 --- a/osu.Game/Screens/Ranking/ResultsScreen.cs +++ b/osu.Game/Screens/Ranking/ResultsScreen.cs @@ -18,7 +18,6 @@ using osu.Game.Input.Bindings; using osu.Game.Online.API; using osu.Game.Rulesets.Mods; using osu.Game.Scoring; -using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking.Statistics; using osuTK; @@ -235,15 +234,18 @@ namespace osu.Game.Screens.Ranking { base.OnEntering(last); - ((BackgroundScreenBeatmap)Background).BlurAmount.Value = BACKGROUND_BLUR; + ApplyToBackground(b => + { + b.BlurAmount.Value = BACKGROUND_BLUR; + b.FadeTo(0.5f, 250); + }); - Background.FadeTo(0.5f, 250); bottomPanel.FadeTo(1, 250); } public override bool OnExiting(IScreen next) { - Background.FadeTo(1, 250); + ApplyToBackground(b => b.FadeTo(1, 250)); return base.OnExiting(next); } @@ -293,7 +295,7 @@ namespace osu.Game.Screens.Ranking ScorePanelList.HandleInput = false; // Dim background. - Background.FadeTo(0.1f, 150); + ApplyToBackground(b => b.FadeTo(0.1f, 150)); detachedPanel = expandedPanel; } @@ -317,7 +319,7 @@ namespace osu.Game.Screens.Ranking ScorePanelList.HandleInput = true; // Un-dim background. - Background.FadeTo(0.5f, 150); + ApplyToBackground(b => b.FadeTo(0.5f, 150)); detachedPanel = null; } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 50a1dd6edf..3c255011c9 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -19,7 +19,6 @@ using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.Menu; using osu.Game.Screens.Select.Options; @@ -38,6 +37,7 @@ using osu.Game.Collections; using osu.Game.Graphics.UserInterface; using osu.Game.Scoring; using System.Diagnostics; +using osu.Game.Screens.Play; namespace osu.Game.Screens.Select { @@ -682,12 +682,12 @@ namespace osu.Game.Screens.Select /// The working beatmap. private void updateComponentFromBeatmap(WorkingBeatmap beatmap) { - if (Background is BackgroundScreenBeatmap backgroundModeBeatmap) + ApplyToBackground(backgroundModeBeatmap => { backgroundModeBeatmap.Beatmap = beatmap; backgroundModeBeatmap.BlurAmount.Value = BACKGROUND_BLUR; backgroundModeBeatmap.FadeColour(Color4.White, 250); - } + }); beatmapInfoWedge.Beatmap = beatmap; From 5904e426ebf666ce8191ec5902389adc9f25b1b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 16:00:25 +0900 Subject: [PATCH 5864/6909] Remove unused variable --- .../Visual/Background/TestSceneUserDimBackgrounds.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index b5df3285f0..7ade7725d9 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -323,14 +323,12 @@ namespace osu.Game.Tests.Visual.Background private class FadeAccessibleResults : ResultsScreen { - private FadeAccessibleBackground background; - public FadeAccessibleResults(ScoreInfo score) : base(score, true) { } - protected override BackgroundScreen CreateBackground() => background = new FadeAccessibleBackground(Beatmap.Value); + protected override BackgroundScreen CreateBackground() => new FadeAccessibleBackground(Beatmap.Value); public Vector2 ExpectedBackgroundBlur => new Vector2(BACKGROUND_BLUR); } From 57a8cd74615765e54d60a425f904a4034045b1a0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 16:17:55 +0900 Subject: [PATCH 5865/6909] Schedule deselection operations for safety --- osu.Game/Overlays/Mods/ModSection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index d00a365dde..47b101c2b0 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -104,7 +104,7 @@ namespace osu.Game.Overlays.Mods /// /// The types of s which should be deselected. /// Set to true to bypass animations and update selections immediately. - public void DeselectTypes(IEnumerable modTypes, bool immediate = false) + public void DeselectTypes(IEnumerable modTypes, bool immediate = false) => Schedule(() => { int delay = 0; @@ -124,7 +124,7 @@ namespace osu.Game.Overlays.Mods } } } - } + }); /// /// Select one or more mods in this section and deselects all other ones. From 9bac791a576a0b16860f3addb3ef3a451d166d1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 16:17:42 +0900 Subject: [PATCH 5866/6909] Fix deselection of autoplay mod failing --- osu.Game/Screens/Select/PlaySongSelect.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/PlaySongSelect.cs b/osu.Game/Screens/Select/PlaySongSelect.cs index 50a61ed4c2..e61d5cce85 100644 --- a/osu.Game/Screens/Select/PlaySongSelect.cs +++ b/osu.Game/Screens/Select/PlaySongSelect.cs @@ -9,6 +9,7 @@ using osu.Framework.Screens; using osu.Game.Graphics; using osu.Game.Overlays; using osu.Game.Overlays.Notifications; +using osu.Game.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens.Play; using osu.Game.Screens.Ranking; @@ -42,6 +43,8 @@ namespace osu.Game.Screens.Select protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + private ModAutoplay getAutoplayMod() => Ruleset.Value.CreateInstance().GetAutoplayMod(); + public override void OnResuming(IScreen last) { base.OnResuming(last); @@ -50,10 +53,10 @@ namespace osu.Game.Screens.Select if (removeAutoModOnResume) { - var autoType = Ruleset.Value.CreateInstance().GetAutoplayMod()?.GetType(); + var autoType = getAutoplayMod()?.GetType(); if (autoType != null) - ModSelect.DeselectTypes(new[] { autoType }, true); + Mods.Value = Mods.Value.Where(m => m.GetType() != autoType).ToArray(); removeAutoModOnResume = false; } @@ -81,12 +84,9 @@ namespace osu.Game.Screens.Select // Ctrl+Enter should start map with autoplay enabled. if (GetContainingInputManager().CurrentState?.Keyboard.ControlPressed == true) { - var auto = Ruleset.Value.CreateInstance().GetAutoplayMod(); - var autoType = auto?.GetType(); + var autoplayMod = getAutoplayMod(); - var mods = Mods.Value; - - if (autoType == null) + if (autoplayMod == null) { notifications?.Post(new SimpleNotification { @@ -95,9 +95,11 @@ namespace osu.Game.Screens.Select return false; } - if (mods.All(m => m.GetType() != autoType)) + var mods = Mods.Value; + + if (mods.All(m => m.GetType() != autoplayMod.GetType())) { - Mods.Value = mods.Append(auto).ToArray(); + Mods.Value = mods.Append(autoplayMod).ToArray(); removeAutoModOnResume = true; } } From 4d6c13f169cc7c09c4db61bbfdfd780c5b44fef5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 16:18:13 +0900 Subject: [PATCH 5867/6909] Privatise ModSelectOverlay methods that may be unsafe to be called externally --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 34f5c70adb..491052fa2c 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -249,7 +249,7 @@ namespace osu.Game.Overlays.Mods { Width = 180, Text = "Deselect All", - Action = DeselectAll, + Action = deselectAll, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, @@ -318,7 +318,7 @@ namespace osu.Game.Overlays.Mods sampleOff = audio.Samples.Get(@"UI/check-off"); } - public void DeselectAll() + private void deselectAll() { foreach (var section in ModSectionsContainer.Children) section.DeselectAll(); @@ -331,7 +331,7 @@ namespace osu.Game.Overlays.Mods /// /// The types of s which should be deselected. /// Set to true to bypass animations and update selections immediately. - public void DeselectTypes(Type[] modTypes, bool immediate = false) + private void deselectTypes(Type[] modTypes, bool immediate = false) { if (modTypes.Length == 0) return; @@ -438,7 +438,7 @@ namespace osu.Game.Overlays.Mods { if (State.Value == Visibility.Visible) sampleOn?.Play(); - DeselectTypes(selectedMod.IncompatibleMods, true); + deselectTypes(selectedMod.IncompatibleMods, true); if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show(); } From 5d8c153c1e21949156a0dbf54b9cf8d71bca4c4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 16:41:03 +0900 Subject: [PATCH 5868/6909] Move schedule logic to buttons rather than section It turns out there's some quite convoluted scheduling / order of execution requirements of ModSelectOverlay and ModSection. Applying scheduling causes a runaway condition ending in zero frames after many mod button changes. I wanted to avoid rewriting the whole component, so have just moved the schedule to guard against the part where drawables are actually changed. --- osu.Game/Overlays/Mods/ModButton.cs | 62 +++++++++++++++------------- osu.Game/Overlays/Mods/ModSection.cs | 8 ++-- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index e574828cd2..51c8bcf9a9 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -64,41 +64,45 @@ namespace osu.Game.Overlays.Mods if (newIndex >= 0 && !Mods[newIndex].HasImplementation) return false; - selectedIndex = newIndex; - Mod modAfter = SelectedMod ?? Mods[0]; - - if (beforeSelected != Selected) + Schedule(() => { - iconsContainer.RotateTo(Selected ? 5f : 0f, 300, Easing.OutElastic); - iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, Easing.OutElastic); - } + selectedIndex = newIndex; + Mod modAfter = SelectedMod ?? Mods[0]; - if (modBefore != modAfter) - { - const float rotate_angle = 16; - - foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing); - backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing); - - backgroundIcon.Mod = modAfter; - - using (BeginDelayedSequence(mod_switch_duration, true)) + if (beforeSelected != Selected) { - foregroundIcon - .RotateTo(-rotate_angle * direction) - .RotateTo(0f, mod_switch_duration, mod_switch_easing); - - backgroundIcon - .RotateTo(rotate_angle * direction) - .RotateTo(0f, mod_switch_duration, mod_switch_easing); - - Schedule(() => displayMod(modAfter)); + iconsContainer.RotateTo(Selected ? 5f : 0f, 300, Easing.OutElastic); + iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, Easing.OutElastic); } - } - foregroundIcon.Selected.Value = Selected; + if (modBefore != modAfter) + { + const float rotate_angle = 16; + + foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing); + backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing); + + backgroundIcon.Mod = modAfter; + + using (BeginDelayedSequence(mod_switch_duration, true)) + { + foregroundIcon + .RotateTo(-rotate_angle * direction) + .RotateTo(0f, mod_switch_duration, mod_switch_easing); + + backgroundIcon + .RotateTo(rotate_angle * direction) + .RotateTo(0f, mod_switch_duration, mod_switch_easing); + + Schedule(() => displayMod(modAfter)); + } + } + + foregroundIcon.Selected.Value = Selected; + + SelectionChanged?.Invoke(SelectedMod); + }); - SelectionChanged?.Invoke(SelectedMod); return true; } diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 47b101c2b0..0107f94dcf 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -104,7 +104,7 @@ namespace osu.Game.Overlays.Mods /// /// The types of s which should be deselected. /// Set to true to bypass animations and update selections immediately. - public void DeselectTypes(IEnumerable modTypes, bool immediate = false) => Schedule(() => + public void DeselectTypes(IEnumerable modTypes, bool immediate = false) { int delay = 0; @@ -124,13 +124,13 @@ namespace osu.Game.Overlays.Mods } } } - }); + } /// /// Select one or more mods in this section and deselects all other ones. /// /// The types of s which should be selected. - public void SelectTypes(IEnumerable modTypes) => Schedule(() => + public void SelectTypes(IEnumerable modTypes) { foreach (var button in buttons) { @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Mods else button.Deselect(); } - }); + } protected ModSection() { From 54982dcdd7e46ee8081f176ebc07b9037e0bfd5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 4 Jan 2021 22:42:39 +0900 Subject: [PATCH 5869/6909] Refactor LoadingLayer to avoid applying effects to external drawables In theory this seemed like a good idea (and an optimisation in some cases, due to lower fill rate), but in practice this leads to weird edge cases. This aims to do away with the operations on external drawables by applying a dim to the area behind the `LoadingLayer` when required. I went over each usage and ensured they look as good or better than previously. The specific bad usage here was the restoration of the colour on dispose (if the `LoadingLayer` was disposed in a still-visible state). I'm aware that the `BeatmapListingOverlay` will now dim completely during load. I think this is fine for the time being. --- .../UserInterface/TestSceneLoadingLayer.cs | 10 +- .../Graphics/UserInterface/LoadingLayer.cs | 29 +- .../Overlays/AccountCreation/ScreenEntry.cs | 2 +- osu.Game/Overlays/BeatmapListingOverlay.cs | 8 +- .../BeatmapSet/Buttons/FavouriteButton.cs | 2 +- .../BeatmapSet/Scores/ScoresContainer.cs | 10 +- .../Dashboard/Friends/FriendDisplay.cs | 2 +- osu.Game/Overlays/DashboardOverlay.cs | 2 +- osu.Game/Overlays/NewsOverlay.cs | 2 +- .../Overlays/Rankings/SpotlightsLayout.cs | 3 +- osu.Game/Overlays/RankingsOverlay.cs | 6 +- .../OnlinePlay/Lounge/LoungeSubScreen.cs | 2 +- .../Match/MultiplayerMatchSettingsOverlay.cs | 279 ++++++------ .../Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- .../Multiplayer/MultiplayerPlayer.cs | 2 +- .../PlaylistsMatchSettingsOverlay.cs | 409 +++++++++--------- .../Screens/Play/BeatmapMetadataDisplay.cs | 2 +- osu.Game/Screens/Select/BeatmapDetails.cs | 6 +- 18 files changed, 389 insertions(+), 389 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs index 1be191fc29..a694595115 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -14,11 +15,12 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneLoadingLayer : OsuTestScene { - private Drawable dimContent; private LoadingLayer overlay; private Container content; + private Drawable dimContent => overlay.Children.OfType().First(); + [SetUp] public void SetUp() => Schedule(() => { @@ -29,14 +31,14 @@ namespace osu.Game.Tests.Visual.UserInterface Size = new Vector2(300), Anchor = Anchor.Centre, Origin = Anchor.Centre, - Children = new[] + Children = new Drawable[] { new Box { Colour = Color4.SlateGray, RelativeSizeAxes = Axes.Both, }, - dimContent = new FillFlowContainer + new FillFlowContainer { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -51,7 +53,7 @@ namespace osu.Game.Tests.Visual.UserInterface new TriangleButton { Text = "puush me", Width = 200, Action = () => { } }, } }, - overlay = new LoadingLayer(dimContent), + overlay = new LoadingLayer(true), } }, }; diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index c8c4424bee..6aca30328a 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osuTK; using osuTK.Graphics; @@ -17,22 +18,31 @@ namespace osu.Game.Graphics.UserInterface /// public class LoadingLayer : LoadingSpinner { - private readonly Drawable dimTarget; + private readonly Box backgroundDimLayer; /// - /// Constuct a new loading spinner. + /// Construct a new loading spinner. /// - /// An optional target to dim when displayed. + /// Whether the full background area should be dimmed while loading. /// Whether the spinner should have a surrounding black box for visibility. - public LoadingLayer(Drawable dimTarget = null, bool withBox = true) + public LoadingLayer(bool dimBackground = false, bool withBox = true) : base(withBox) { RelativeSizeAxes = Axes.Both; Size = new Vector2(1); - this.dimTarget = dimTarget; - MainContents.RelativeSizeAxes = Axes.None; + + if (dimBackground) + { + AddInternal(backgroundDimLayer = new Box + { + Depth = float.MaxValue, + Colour = Color4.Black, + Alpha = 0, + RelativeSizeAxes = Axes.Both, + }); + } } public override bool HandleNonPositionalInput => false; @@ -56,19 +66,20 @@ namespace osu.Game.Graphics.UserInterface protected override void PopIn() { - dimTarget?.FadeColour(OsuColour.Gray(0.5f), TRANSITION_DURATION, Easing.OutQuint); + backgroundDimLayer?.FadeTo(0.5f, TRANSITION_DURATION * 2, Easing.OutQuint); base.PopIn(); } protected override void PopOut() { - dimTarget?.FadeColour(Color4.White, TRANSITION_DURATION, Easing.OutQuint); + backgroundDimLayer?.FadeOut(TRANSITION_DURATION, Easing.OutQuint); base.PopOut(); } protected override void Update() { base.Update(); + MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 30, 100)); } @@ -79,7 +90,7 @@ namespace osu.Game.Graphics.UserInterface if (State.Value == Visibility.Visible) { // ensure we don't leave the target in a bad state. - dimTarget?.FadeColour(Color4.White, TRANSITION_DURATION, Easing.OutQuint); + // dimTarget?.FadeColour(Color4.White, TRANSITION_DURATION, Easing.OutQuint); } } } diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index a0b1b27ebf..0d5538155a 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -124,7 +124,7 @@ namespace osu.Game.Overlays.AccountCreation }, }, }, - loadingLayer = new LoadingLayer(mainContent) + loadingLayer = new LoadingLayer(true) }; textboxes = new[] { usernameTextBox, emailTextBox, passwordTextBox }; diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 1e29e713af..0c9c995dd6 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -92,14 +92,14 @@ namespace osu.Game.Overlays { foundContent = new FillFlowContainer(), notFoundContent = new NotFoundDrawable(), - loadingLayer = new LoadingLayer(panelTarget) } } - } + }, }, } - } - } + }, + }, + loadingLayer = new LoadingLayer(true) }; } diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs index c983b337b5..7ad6906cea 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/FavouriteButton.cs @@ -53,7 +53,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons Size = new Vector2(18), Shadow = false, }, - loading = new LoadingLayer(icon, false), + loading = new LoadingLayer(true, false), }); Action = () => diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs index 9a2dcd014a..b598b7d97f 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/ScoresContainer.cs @@ -157,11 +157,11 @@ namespace osu.Game.Overlays.BeatmapSet.Scores } } }, - loading = new LoadingLayer() } } - } - } + }, + }, + loading = new LoadingLayer() }); } @@ -228,7 +228,9 @@ namespace osu.Game.Overlays.BeatmapSet.Scores { Scores = null; notSupporterPlaceholder.Show(); + loading.Hide(); + loading.FinishTransforms(); return; } @@ -241,6 +243,8 @@ namespace osu.Game.Overlays.BeatmapSet.Scores getScoresRequest.Success += scores => { loading.Hide(); + loading.FinishTransforms(); + Scores = scores; if (!scores.Scores.Any()) diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index cc26a11da1..e6fe6ac749 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -128,7 +128,7 @@ namespace osu.Game.Overlays.Dashboard.Friends AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 50 } }, - loading = new LoadingLayer(itemsPlaceholder) + loading = new LoadingLayer(true) } } } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 04defce636..03c320debe 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -68,7 +68,7 @@ namespace osu.Game.Overlays } } }, - loading = new LoadingLayer(content), + loading = new LoadingLayer(true), }; } diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index c8c1db012f..5820d405d4 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -59,7 +59,7 @@ namespace osu.Game.Overlays }, }, }, - loading = new LoadingLayer(content), + loading = new LoadingLayer(true), }; } diff --git a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs index 61339df76f..b16e0a4908 100644 --- a/osu.Game/Overlays/Rankings/SpotlightsLayout.cs +++ b/osu.Game/Overlays/Rankings/SpotlightsLayout.cs @@ -45,6 +45,7 @@ namespace osu.Game.Overlays.Rankings { RelativeSizeAxes = Axes.X; AutoSizeAxes = Axes.Y; + InternalChild = new ReverseChildIDFillFlowContainer { RelativeSizeAxes = Axes.X, @@ -68,7 +69,7 @@ namespace osu.Game.Overlays.Rankings AutoSizeAxes = Axes.Y, Margin = new MarginPadding { Vertical = 10 } }, - loading = new LoadingLayer(content) + loading = new LoadingLayer(true) } } } diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index ae6d49960a..25350e310a 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -42,6 +42,8 @@ namespace osu.Game.Overlays Depth = -float.MaxValue }) { + loading = new LoadingLayer(true); + Children = new Drawable[] { background = new Box @@ -74,12 +76,12 @@ namespace osu.Game.Overlays RelativeSizeAxes = Axes.X, Margin = new MarginPadding { Bottom = 10 } }, - loading = new LoadingLayer(contentContainer), } } } } - } + }, + loading }; } diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 79f5dfdee1..0f06188dc2 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge Padding = new MarginPadding(10), Child = roomsContainer = new RoomsContainer { JoinRequested = joinRequested } }, - loadingLayer = new LoadingLayer(roomsContainer), + loadingLayer = new LoadingLayer(true), } }, new RoomInspector diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index ae03d384f6..67c6aa7add 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -71,201 +71,192 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [BackgroundDependencyLoader] private void load(OsuColour colours) { - Container dimContent; - InternalChildren = new Drawable[] { - dimContent = new Container + new Box { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Colour = Color4Extensions.FromHex(@"28242d"), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new Box + new Dimension(GridSizeMode.Distributed), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d"), - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + new OsuScrollContainer { - new Dimension(GridSizeMode.Distributed), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] + Padding = new MarginPadding { - new OsuScrollContainer + Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING, + Vertical = 10 + }, + RelativeSizeAxes = Axes.Both, + Children = new[] + { + new FillFlowContainer { - Padding = new MarginPadding + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 10), + Children = new Drawable[] { - Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING, - Vertical = 10 - }, - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new FillFlowContainer + new Container { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 10), Children = new Drawable[] { - new Container + new SectionContainer { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + Padding = new MarginPadding { Right = FIELD_PADDING / 2 }, + Children = new[] { - new SectionContainer + new Section("Room name") { - Padding = new MarginPadding { Right = FIELD_PADDING / 2 }, - Children = new[] + Child = NameField = new SettingsTextBox { - new Section("Room name") + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + }, + }, + new Section("Room visibility") + { + Alpha = disabled_alpha, + Child = AvailabilityPicker = new RoomAvailabilityPicker + { + Enabled = { Value = false } + }, + }, + new Section("Game type") + { + Alpha = disabled_alpha, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(7), + Children = new Drawable[] { - Child = NameField = new SettingsTextBox + TypePicker = new GameTypePicker { RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - }, - }, - new Section("Room visibility") - { - Alpha = disabled_alpha, - Child = AvailabilityPicker = new RoomAvailabilityPicker - { Enabled = { Value = false } }, - }, - new Section("Game type") - { - Alpha = disabled_alpha, - Child = new FillFlowContainer + typeLabel = new OsuSpriteText { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Spacing = new Vector2(7), - Children = new Drawable[] - { - TypePicker = new GameTypePicker - { - RelativeSizeAxes = Axes.X, - Enabled = { Value = false } - }, - typeLabel = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 14), - Colour = colours.Yellow - }, - }, + Font = OsuFont.GetFont(size: 14), + Colour = colours.Yellow }, }, }, }, - new SectionContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Padding = new MarginPadding { Left = FIELD_PADDING / 2 }, - Children = new[] - { - new Section("Max participants") - { - Alpha = disabled_alpha, - Child = MaxParticipantsField = new SettingsNumberTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - ReadOnly = true, - }, - }, - new Section("Password (optional)") - { - Alpha = disabled_alpha, - Child = new SettingsPasswordTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - ReadOnly = true, - }, - }, - } - } }, }, - initialBeatmapControl = new BeatmapSelectionControl + new SectionContainer { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - RelativeSizeAxes = Axes.X, - Width = 0.5f + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Padding = new MarginPadding { Left = FIELD_PADDING / 2 }, + Children = new[] + { + new Section("Max participants") + { + Alpha = disabled_alpha, + Child = MaxParticipantsField = new SettingsNumberTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + ReadOnly = true, + }, + }, + new Section("Password (optional)") + { + Alpha = disabled_alpha, + Child = new SettingsPasswordTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + ReadOnly = true, + }, + }, + } } - } + }, + }, + initialBeatmapControl = new BeatmapSelectionControl + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + RelativeSizeAxes = Axes.X, + Width = 0.5f } - }, - }, + } + } }, - new Drawable[] + }, + }, + new Drawable[] + { + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Y = 2, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] { - new Container + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + }, + new FillFlowContainer { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Y = 2, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding { Vertical = 20 }, + Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, Children = new Drawable[] { - new Box + ApplyButton = new CreateOrUpdateButton { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(230, 55), + Enabled = { Value = false }, + Action = apply, }, - new FillFlowContainer + ErrorText = new OsuSpriteText { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Margin = new MarginPadding { Vertical = 20 }, - Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, - Children = new Drawable[] - { - ApplyButton = new CreateOrUpdateButton - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Size = new Vector2(230, 55), - Enabled = { Value = false }, - Action = apply, - }, - ErrorText = new OsuSpriteText - { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Alpha = 0, - Depth = 1, - Colour = colours.RedDark - } - } + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Alpha = 0, + Depth = 1, + Colour = colours.RedDark } } } } } - }, + } } }, - loadingLayer = new LoadingLayer(dimContent) + loadingLayer = new LoadingLayer(true) }; TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 72539a2e3a..36dbb9e792 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -47,7 +47,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [BackgroundDependencyLoader] private void load() { - AddInternal(loadingLayer = new LoadingLayer(Carousel)); + AddInternal(loadingLayer = new LoadingLayer(true)); initialBeatmap = Beatmap.Value; initialRuleset = Ruleset.Value; initialMods = Mods.Value.ToList(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 4247e954bd..4bee502e2e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -62,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // todo: this should be implemented via a custom HUD implementation, and correctly masked to the main content area. LoadComponentAsync(leaderboard = new MultiplayerGameplayLeaderboard(ScoreProcessor, userIds), HUDOverlay.Add); - HUDOverlay.Add(loadingDisplay = new LoadingLayer(DrawableRuleset) { Depth = float.MaxValue }); + HUDOverlay.Add(loadingDisplay = new LoadingLayer(true) { Depth = float.MaxValue }); if (Token == null) return; // Todo: Somehow handle token retrieval failure. diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index 6b92526f35..01f9920609 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -64,243 +64,234 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [BackgroundDependencyLoader] private void load(OsuColour colours) { - Container dimContent; - InternalChildren = new Drawable[] { - dimContent = new Container + new Box { RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + Colour = Color4Extensions.FromHex(@"28242d"), + }, + new GridContainer + { + RelativeSizeAxes = Axes.Both, + RowDimensions = new[] { - new Box + new Dimension(GridSizeMode.Distributed), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d"), - }, - new GridContainer - { - RelativeSizeAxes = Axes.Both, - RowDimensions = new[] + new OsuScrollContainer { - new Dimension(GridSizeMode.Distributed), - new Dimension(GridSizeMode.AutoSize), - }, - Content = new[] - { - new Drawable[] + Padding = new MarginPadding { - new OsuScrollContainer - { - Padding = new MarginPadding - { - Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING, - Vertical = 10 - }, - RelativeSizeAxes = Axes.Both, - Children = new[] - { - new Container - { - Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new SectionContainer - { - Padding = new MarginPadding { Right = FIELD_PADDING / 2 }, - Children = new[] - { - new Section("Room name") - { - Child = NameField = new SettingsTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - LengthLimit = 100 - }, - }, - new Section("Duration") - { - Child = DurationField = new DurationDropdown - { - RelativeSizeAxes = Axes.X, - Items = new[] - { - TimeSpan.FromMinutes(30), - TimeSpan.FromHours(1), - TimeSpan.FromHours(2), - TimeSpan.FromHours(4), - TimeSpan.FromHours(8), - TimeSpan.FromHours(12), - //TimeSpan.FromHours(16), - TimeSpan.FromHours(24), - TimeSpan.FromDays(3), - TimeSpan.FromDays(7) - } - } - }, - new Section("Room visibility") - { - Alpha = disabled_alpha, - Child = AvailabilityPicker = new RoomAvailabilityPicker - { - Enabled = { Value = false } - }, - }, - new Section("Game type") - { - Alpha = disabled_alpha, - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Spacing = new Vector2(7), - Children = new Drawable[] - { - TypePicker = new GameTypePicker - { - RelativeSizeAxes = Axes.X, - Enabled = { Value = false } - }, - typeLabel = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 14), - Colour = colours.Yellow - }, - }, - }, - }, - new Section("Max participants") - { - Alpha = disabled_alpha, - Child = MaxParticipantsField = new SettingsNumberTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - ReadOnly = true, - }, - }, - new Section("Password (optional)") - { - Alpha = disabled_alpha, - Child = new SettingsPasswordTextBox - { - RelativeSizeAxes = Axes.X, - TabbableContentContainer = this, - ReadOnly = true, - }, - }, - }, - }, - new SectionContainer - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Padding = new MarginPadding { Left = FIELD_PADDING / 2 }, - Children = new[] - { - new Section("Playlist") - { - Child = new GridContainer - { - RelativeSizeAxes = Axes.X, - Height = 300, - Content = new[] - { - new Drawable[] - { - playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both } - }, - new Drawable[] - { - playlistLength = new OsuSpriteText - { - Margin = new MarginPadding { Vertical = 5 }, - Colour = colours.Yellow, - Font = OsuFont.GetFont(size: 12), - } - }, - new Drawable[] - { - new PurpleTriangleButton - { - RelativeSizeAxes = Axes.X, - Height = 40, - Text = "Edit playlist", - Action = () => EditPlaylist?.Invoke() - } - } - }, - RowDimensions = new[] - { - new Dimension(), - new Dimension(GridSizeMode.AutoSize), - new Dimension(GridSizeMode.AutoSize), - } - } - }, - }, - }, - }, - } - }, - }, + Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING, + Vertical = 10 }, - new Drawable[] + RelativeSizeAxes = Axes.Both, + Children = new[] { new Container { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Y = 2, + Padding = new MarginPadding { Horizontal = WaveOverlayContainer.WIDTH_PADDING }, RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Children = new Drawable[] { - new Box + new SectionContainer { - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Margin = new MarginPadding { Vertical = 20 }, - Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, - Children = new Drawable[] + Padding = new MarginPadding { Right = FIELD_PADDING / 2 }, + Children = new[] { - ApplyButton = new CreateRoomButton + new Section("Room name") { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Size = new Vector2(230, 55), - Enabled = { Value = false }, - Action = apply, + Child = NameField = new SettingsTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + LengthLimit = 100 + }, }, - ErrorText = new OsuSpriteText + new Section("Duration") { - Anchor = Anchor.BottomCentre, - Origin = Anchor.BottomCentre, - Alpha = 0, - Depth = 1, - Colour = colours.RedDark - } - } + Child = DurationField = new DurationDropdown + { + RelativeSizeAxes = Axes.X, + Items = new[] + { + TimeSpan.FromMinutes(30), + TimeSpan.FromHours(1), + TimeSpan.FromHours(2), + TimeSpan.FromHours(4), + TimeSpan.FromHours(8), + TimeSpan.FromHours(12), + //TimeSpan.FromHours(16), + TimeSpan.FromHours(24), + TimeSpan.FromDays(3), + TimeSpan.FromDays(7) + } + } + }, + new Section("Room visibility") + { + Alpha = disabled_alpha, + Child = AvailabilityPicker = new RoomAvailabilityPicker + { + Enabled = { Value = false } + }, + }, + new Section("Game type") + { + Alpha = disabled_alpha, + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Spacing = new Vector2(7), + Children = new Drawable[] + { + TypePicker = new GameTypePicker + { + RelativeSizeAxes = Axes.X, + Enabled = { Value = false } + }, + typeLabel = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 14), + Colour = colours.Yellow + }, + }, + }, + }, + new Section("Max participants") + { + Alpha = disabled_alpha, + Child = MaxParticipantsField = new SettingsNumberTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + ReadOnly = true, + }, + }, + new Section("Password (optional)") + { + Alpha = disabled_alpha, + Child = new SettingsPasswordTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + ReadOnly = true, + }, + }, + }, + }, + new SectionContainer + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + Padding = new MarginPadding { Left = FIELD_PADDING / 2 }, + Children = new[] + { + new Section("Playlist") + { + Child = new GridContainer + { + RelativeSizeAxes = Axes.X, + Height = 300, + Content = new[] + { + new Drawable[] + { + playlist = new DrawableRoomPlaylist(true, true) { RelativeSizeAxes = Axes.Both } + }, + new Drawable[] + { + playlistLength = new OsuSpriteText + { + Margin = new MarginPadding { Vertical = 5 }, + Colour = colours.Yellow, + Font = OsuFont.GetFont(size: 12), + } + }, + new Drawable[] + { + new PurpleTriangleButton + { + RelativeSizeAxes = Axes.X, + Height = 40, + Text = "Edit playlist", + Action = () => EditPlaylist?.Invoke() + } + } + }, + RowDimensions = new[] + { + new Dimension(), + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + } + } + }, + }, + }, + }, + } + }, + }, + }, + new Drawable[] + { + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Y = 2, + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex(@"28242d").Darken(0.5f).Opacity(1f), + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Margin = new MarginPadding { Vertical = 20 }, + Padding = new MarginPadding { Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, + Children = new Drawable[] + { + ApplyButton = new CreateRoomButton + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Size = new Vector2(230, 55), + Enabled = { Value = false }, + Action = apply, + }, + ErrorText = new OsuSpriteText + { + Anchor = Anchor.BottomCentre, + Origin = Anchor.BottomCentre, + Alpha = 0, + Depth = 1, + Colour = colours.RedDark } } } } } - }, + } } }, - loadingLayer = new LoadingLayer(dimContent) + loadingLayer = new LoadingLayer(true) }; TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true); diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index 5530b4beac..00176e9127 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -131,7 +131,7 @@ namespace osu.Game.Screens.Play Anchor = Anchor.Centre, FillMode = FillMode.Fill, }, - loading = new LoadingLayer(backgroundSprite) + loading = new LoadingLayer(true) } }, new OsuSpriteText diff --git a/osu.Game/Screens/Select/BeatmapDetails.cs b/osu.Game/Screens/Select/BeatmapDetails.cs index 71f78c5c95..8a1c291fca 100644 --- a/osu.Game/Screens/Select/BeatmapDetails.cs +++ b/osu.Game/Screens/Select/BeatmapDetails.cs @@ -63,8 +63,6 @@ namespace osu.Game.Screens.Select public BeatmapDetails() { - Container content; - Children = new Drawable[] { new Box @@ -72,7 +70,7 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.Both, Colour = Color4.Black.Opacity(0.5f), }, - content = new Container + new Container { RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { Horizontal = spacing }, @@ -159,7 +157,7 @@ namespace osu.Game.Screens.Select }, }, }, - loading = new LoadingLayer(content), + loading = new LoadingLayer(true), }; } From 0b1ee2e267f3daea22a437d445edf4336bd048f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 17:42:19 +0900 Subject: [PATCH 5870/6909] Remove unused dispose logic --- osu.Game/Graphics/UserInterface/LoadingLayer.cs | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index 6aca30328a..a4905156a9 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -3,7 +3,6 @@ using System; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osuTK; @@ -82,16 +81,5 @@ namespace osu.Game.Graphics.UserInterface MainContents.Size = new Vector2(Math.Clamp(Math.Min(DrawWidth, DrawHeight) * 0.25f, 30, 100)); } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - if (State.Value == Visibility.Visible) - { - // ensure we don't leave the target in a bad state. - // dimTarget?.FadeColour(Color4.White, TRANSITION_DURATION, Easing.OutQuint); - } - } } } From 0639429a23bb706d84b56dea98b7f23d3f4563b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 18:10:39 +0900 Subject: [PATCH 5871/6909] Fix test (and remove no longer valid test) --- .../UserInterface/TestSceneLoadingLayer.cs | 36 ++++++++----------- .../Graphics/UserInterface/LoadingLayer.cs | 8 ++--- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs index a694595115..d426723f0b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneLoadingLayer.cs @@ -1,11 +1,11 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Utils; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osuTK; @@ -15,12 +15,10 @@ namespace osu.Game.Tests.Visual.UserInterface { public class TestSceneLoadingLayer : OsuTestScene { - private LoadingLayer overlay; + private TestLoadingLayer overlay; private Container content; - private Drawable dimContent => overlay.Children.OfType().First(); - [SetUp] public void SetUp() => Schedule(() => { @@ -53,7 +51,7 @@ namespace osu.Game.Tests.Visual.UserInterface new TriangleButton { Text = "puush me", Width = 200, Action = () => { } }, } }, - overlay = new LoadingLayer(true), + overlay = new TestLoadingLayer(true), } }, }; @@ -66,25 +64,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("show", () => overlay.Show()); - AddUntilStep("wait for content dim", () => dimContent.Colour != Color4.White); + AddUntilStep("wait for content dim", () => overlay.BackgroundDimLayer.Alpha > 0); AddStep("hide", () => overlay.Hide()); - AddUntilStep("wait for content restore", () => dimContent.Colour == Color4.White); - } - - [Test] - public void TestContentRestoreOnDispose() - { - AddAssert("not visible", () => !overlay.IsPresent); - - AddStep("show", () => overlay.Show()); - - AddUntilStep("wait for content dim", () => dimContent.Colour != Color4.White); - - AddStep("expire", () => overlay.Expire()); - - AddUntilStep("wait for content restore", () => dimContent.Colour == Color4.White); + AddUntilStep("wait for content restore", () => Precision.AlmostEquals(overlay.BackgroundDimLayer.Alpha, 0)); } [Test] @@ -100,5 +84,15 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("hide", () => overlay.Hide()); } + + private class TestLoadingLayer : LoadingLayer + { + public new Box BackgroundDimLayer => base.BackgroundDimLayer; + + public TestLoadingLayer(bool dimBackground = false, bool withBox = true) + : base(dimBackground, withBox) + { + } + } } } diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index a4905156a9..3e3fd32d65 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -17,7 +17,7 @@ namespace osu.Game.Graphics.UserInterface /// public class LoadingLayer : LoadingSpinner { - private readonly Box backgroundDimLayer; + protected Box BackgroundDimLayer { get; private set; } /// /// Construct a new loading spinner. @@ -34,7 +34,7 @@ namespace osu.Game.Graphics.UserInterface if (dimBackground) { - AddInternal(backgroundDimLayer = new Box + AddInternal(BackgroundDimLayer = new Box { Depth = float.MaxValue, Colour = Color4.Black, @@ -65,13 +65,13 @@ namespace osu.Game.Graphics.UserInterface protected override void PopIn() { - backgroundDimLayer?.FadeTo(0.5f, TRANSITION_DURATION * 2, Easing.OutQuint); + BackgroundDimLayer?.FadeTo(0.5f, TRANSITION_DURATION * 2, Easing.OutQuint); base.PopIn(); } protected override void PopOut() { - backgroundDimLayer?.FadeOut(TRANSITION_DURATION, Easing.OutQuint); + BackgroundDimLayer?.FadeOut(TRANSITION_DURATION, Easing.OutQuint); base.PopOut(); } From d0d2e41b28df281507e16d5de19f4ec0c5e8a8b7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 18:19:28 +0900 Subject: [PATCH 5872/6909] Fix display settings binding to configuration bindables in async load --- .../Settings/Sections/Graphics/LayoutSettings.cs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 3d3b543d70..e815e2f68b 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -133,6 +133,15 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics }, }; + scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown(); + windowModes.BindCollectionChanged((sender, args) => { if (windowModes.Count > 1) @@ -141,8 +150,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics windowModeDropdown.Hide(); }, true); - windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown(); - currentDisplay.BindValueChanged(display => Schedule(() => { resolutions.RemoveRange(1, resolutions.Count - 1); @@ -159,8 +166,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics updateResolutionDropdown(); }), true); - scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); - scalingMode.BindValueChanged(mode => { scalingSettings.ClearTransforms(); From 83dbba3cbfb3a02297ae8bc801a11bf2028f3cb6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 18:41:45 +0900 Subject: [PATCH 5873/6909] Fix carousel beatmap set panels applying transforms to difficulties while they are loading --- .../Carousel/DrawableCarouselBeatmapSet.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index b3c5d458d6..17fa66447d 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -40,6 +41,8 @@ namespace osu.Game.Screens.Select.Carousel private BeatmapSetInfo beatmapSet; + private Task beatmapsLoadTask; + [Resolved] private BeatmapManager manager { get; set; } @@ -85,7 +88,9 @@ namespace osu.Game.Screens.Select.Carousel base.UpdateItem(); Content.Clear(); + beatmapContainer = null; + beatmapsLoadTask = null; if (Item == null) return; @@ -122,11 +127,7 @@ namespace osu.Game.Screens.Select.Carousel MovementContainer.MoveToX(0, 500, Easing.OutExpo); - if (beatmapContainer != null) - { - foreach (var beatmap in beatmapContainer) - beatmap.MoveToY(0, 800, Easing.OutQuint); - } + updateBeatmapYPositions(); } protected override void Selected() @@ -163,7 +164,7 @@ namespace osu.Game.Screens.Select.Carousel ChildrenEnumerable = visibleBeatmaps.Select(c => c.CreateDrawableRepresentation()) }; - LoadComponentAsync(beatmapContainer, loaded => + beatmapsLoadTask = LoadComponentAsync(beatmapContainer, loaded => { // make sure the pooled target hasn't changed. if (beatmapContainer != loaded) @@ -173,16 +174,26 @@ namespace osu.Game.Screens.Select.Carousel updateBeatmapYPositions(); }); } + } - void updateBeatmapYPositions() + private void updateBeatmapYPositions() + { + if (beatmapsLoadTask == null || !beatmapsLoadTask.IsCompleted) + return; + + float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; + + bool isSelected = Item.State.Value == CarouselItemState.Selected; + + foreach (var panel in beatmapContainer.Children) { - float yPos = DrawableCarouselBeatmap.CAROUSEL_BEATMAP_SPACING; - - foreach (var panel in beatmapContainer.Children) + if (isSelected) { panel.MoveToY(yPos, 800, Easing.OutQuint); yPos += panel.Item.TotalHeight; } + else + panel.MoveToY(0, 800, Easing.OutQuint); } } From 4b539b01c1862bfb0a29ad69dc6b400f3719e99b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 5 Jan 2021 20:38:58 +0900 Subject: [PATCH 5874/6909] Match code between updateSelectedBeatmap/Ruleset --- osu.Game/Screens/Select/SongSelect.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index e3036c662b..7e217ca7a4 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -435,13 +435,15 @@ namespace osu.Game.Screens.Select return; beatmapNoDebounce = beatmap; - performUpdateSelected(); } private void updateSelectedRuleset(RulesetInfo ruleset) { - if (ruleset == null || ruleset.Equals(rulesetNoDebounce)) + if (ruleset == null && rulesetNoDebounce == null) + return; + + if (ruleset?.Equals(rulesetNoDebounce) == true) return; rulesetNoDebounce = ruleset; From 2b253f6d01426901994e95b03ead4233f7cb67dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Jan 2021 22:39:59 +0100 Subject: [PATCH 5875/6909] Remove now-unused fields & locals --- osu.Game/Overlays/AccountCreation/ScreenEntry.cs | 4 +--- osu.Game/Screens/Play/BeatmapMetadataDisplay.cs | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs index 0d5538155a..bcb3d4b635 100644 --- a/osu.Game/Overlays/AccountCreation/ScreenEntry.cs +++ b/osu.Game/Overlays/AccountCreation/ScreenEntry.cs @@ -48,11 +48,9 @@ namespace osu.Game.Overlays.AccountCreation [BackgroundDependencyLoader] private void load(OsuColour colours) { - FillFlowContainer mainContent; - InternalChildren = new Drawable[] { - mainContent = new FillFlowContainer + new FillFlowContainer { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Vertical, diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index 00176e9127..b53141e8fb 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -53,7 +53,6 @@ namespace osu.Game.Screens.Play private readonly Bindable> mods; private readonly Drawable facade; private LoadingSpinner loading; - private Sprite backgroundSprite; public IBindable> Mods => mods; @@ -123,7 +122,7 @@ namespace osu.Game.Screens.Play Masking = true, Children = new Drawable[] { - backgroundSprite = new Sprite + new Sprite { RelativeSizeAxes = Axes.Both, Texture = beatmap?.Background, From ac1d6d444430296d2ab08ac3f5e89f5a965655ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Jan 2021 22:40:16 +0100 Subject: [PATCH 5876/6909] Make auto-property get-only --- osu.Game/Graphics/UserInterface/LoadingLayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index 3e3fd32d65..d86ea9e4ea 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.cs @@ -17,7 +17,7 @@ namespace osu.Game.Graphics.UserInterface /// public class LoadingLayer : LoadingSpinner { - protected Box BackgroundDimLayer { get; private set; } + protected Box BackgroundDimLayer { get; } /// /// Construct a new loading spinner. From 0880e76da8d58d02ffe4ad95c173d9373e3d50b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 5 Jan 2021 22:47:57 +0100 Subject: [PATCH 5877/6909] Mark background dim layer as possibly-null --- osu.Game/Graphics/UserInterface/LoadingLayer.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/LoadingLayer.cs b/osu.Game/Graphics/UserInterface/LoadingLayer.cs index d86ea9e4ea..47ba5fce4d 100644 --- a/osu.Game/Graphics/UserInterface/LoadingLayer.cs +++ b/osu.Game/Graphics/UserInterface/LoadingLayer.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.Shapes; using osu.Framework.Input.Events; @@ -17,6 +18,7 @@ namespace osu.Game.Graphics.UserInterface /// public class LoadingLayer : LoadingSpinner { + [CanBeNull] protected Box BackgroundDimLayer { get; } /// From 15dd7a87a62d73d0ba6e85fea034f1d7cb9656c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 15:19:12 +0900 Subject: [PATCH 5878/6909] Move gameplay preview event binding to LoadComplete --- .../Settings/Sections/Graphics/LayoutSettings.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index e815e2f68b..7acbf038d8 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -132,14 +132,14 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics } }, }; - - scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); } protected override void LoadComplete() { base.LoadComplete(); + scalingSettings.ForEach(s => bindPreviewEvent(s.Current)); + windowModeDropdown.Current.ValueChanged += _ => updateResolutionDropdown(); windowModes.BindCollectionChanged((sender, args) => @@ -186,11 +186,6 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics } } - /// - /// Create a delayed bindable which only updates when a condition is met. - /// - /// The config bindable. - /// A bindable which will propagate updates with a delay. private void bindPreviewEvent(Bindable bindable) { bindable.ValueChanged += _ => From 11a0c637bc07812e01125df31251791cf9a24694 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 15:25:53 +0900 Subject: [PATCH 5879/6909] Mark background properties as nullable --- osu.Game/Screens/OsuScreen.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index c97c0aef2b..d1cdb9f1de 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -119,8 +120,10 @@ namespace osu.Game.Screens /// /// The background created and owned by this screen. May be null if the background didn't change. /// + [CanBeNull] private BackgroundScreen ownedBackground; + [CanBeNull] private BackgroundScreen background; [Resolved(canBeNull: true)] From e9d4e4d1d5bb5c6df13474c867c81b4d516cd61d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 15:26:13 +0900 Subject: [PATCH 5880/6909] Add xmldoc and throw a local exception on null background --- osu.Game/Screens/BackgroundScreen.cs | 4 ++++ osu.Game/Screens/OsuScreen.cs | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index ea220c2f82..c81362eebe 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -34,6 +34,10 @@ namespace osu.Game.Screens return false; } + /// + /// Apply arbitrary changes to this background in a thread safe manner. + /// + /// The operation to perform. public void ApplyToBackground(Action action) => Schedule(() => action.Invoke(this)); protected override void Update() diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index d1cdb9f1de..11467db6c8 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -115,8 +115,6 @@ namespace osu.Game.Screens Mods = screenDependencies.Mods; } - public void ApplyToBackground(Action action) => background.ApplyToBackground(action); - /// /// The background created and owned by this screen. May be null if the background didn't change. /// @@ -148,6 +146,18 @@ namespace osu.Game.Screens Activity.Value ??= InitialActivity; } + /// + /// Apply arbitrary changes to the current background screen in a thread safe manner. + /// + /// The operation to perform. + public void ApplyToBackground(Action action) + { + if (background == null) + throw new InvalidOperationException("Attempted to apply to background before screen is pushed"); + + background.ApplyToBackground(action); + } + public override void OnResuming(IScreen last) { if (PlayResumeSound) From 550ef3f13307f3d8592a0bc1ecd103f67af046e1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 15:28:01 +0900 Subject: [PATCH 5881/6909] Aggressively dispose ownedBackground if it was not used, because we can --- osu.Game/Screens/OsuScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index 11467db6c8..fb26739a44 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -185,6 +185,7 @@ namespace osu.Game.Screens if (background != ownedBackground) { // background may have not been replaced, at which point we don't want to track the background lifetime. + ownedBackground?.Dispose(); ownedBackground = null; } From 07cff7038728545a5ce399c3b727b1d03c68813c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 18:19:03 +0900 Subject: [PATCH 5882/6909] Add specific messaging for when there's no background stack available --- osu.Game/Screens/OsuScreen.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index fb26739a44..e1a29946f4 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -125,6 +125,7 @@ namespace osu.Game.Screens private BackgroundScreen background; [Resolved(canBeNull: true)] + [CanBeNull] private BackgroundScreenStack backgroundStack { get; set; } [Resolved(canBeNull: true)] @@ -152,8 +153,11 @@ namespace osu.Game.Screens /// The operation to perform. public void ApplyToBackground(Action action) { + if (backgroundStack == null) + throw new InvalidOperationException("Attempted to apply to background without a background stack being available."); + if (background == null) - throw new InvalidOperationException("Attempted to apply to background before screen is pushed"); + throw new InvalidOperationException("Attempted to apply to background before screen is pushed."); background.ApplyToBackground(action); } From 99701a6d9b22e2fbc4864556a14f9f44311b5236 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 21:06:33 +0900 Subject: [PATCH 5883/6909] Add null check on beatmapContainer for safety --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index 17fa66447d..c0415384c8 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -178,6 +178,9 @@ namespace osu.Game.Screens.Select.Carousel private void updateBeatmapYPositions() { + if (beatmapContainer == null) + return; + if (beatmapsLoadTask == null || !beatmapsLoadTask.IsCompleted) return; From 43b9fde45731fdbd5da77f119eaedc5e0afa0f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Jan 2021 13:15:15 +0100 Subject: [PATCH 5884/6909] Add some nullability annotations for good measure --- osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs index c0415384c8..d7e901b71e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmapSet.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -37,10 +38,12 @@ namespace osu.Game.Screens.Select.Carousel public IEnumerable DrawableBeatmaps => beatmapContainer?.Children ?? Enumerable.Empty(); + [CanBeNull] private Container beatmapContainer; private BeatmapSetInfo beatmapSet; + [CanBeNull] private Task beatmapsLoadTask; [Resolved] From 32accc8eab227aebcc40cc026897324119c0e589 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 22:56:10 +0900 Subject: [PATCH 5885/6909] Remove "osu!direct" button --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- osu.Game/Screens/Menu/ButtonSystem.cs | 1 - osu.Game/Screens/Menu/Disclaimer.cs | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index 1270df5374..fa3f70735a 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -112,7 +112,7 @@ namespace osu.Game.Input.Bindings [Description("Toggle settings")] ToggleSettings, - [Description("Toggle osu!direct")] + [Description("Toggle beatmap listing")] ToggleDirect, [Description("Increase volume")] diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index fd4d74dc6b..0c47ef3b1e 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -130,7 +130,6 @@ 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), () => 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/Disclaimer.cs b/osu.Game/Screens/Menu/Disclaimer.cs index 46fddabb26..72eb9c7c0c 100644 --- a/osu.Game/Screens/Menu/Disclaimer.cs +++ b/osu.Game/Screens/Menu/Disclaimer.cs @@ -201,7 +201,7 @@ namespace osu.Game.Screens.Menu "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!", + "For now, what used to be \"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!", From 59025e9d50a6866b71f2571919460c94a155222c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 23:09:58 +0900 Subject: [PATCH 5886/6909] Remove related events --- osu.Game/Screens/Menu/ButtonSystem.cs | 1 - osu.Game/Screens/Menu/MainMenu.cs | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 0c47ef3b1e..8417858878 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -39,7 +39,6 @@ namespace osu.Game.Screens.Menu public Action OnEdit; public Action OnExit; - public Action OnBeatmapListing; public Action OnSolo; public Action OnSettings; public Action OnMultiplayer; diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 97fd58318b..ef45a656fa 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Menu private SongTicker songTicker; [BackgroundDependencyLoader(true)] - private void load(BeatmapListingOverlay beatmapListing, SettingsOverlay settings, RankingsOverlay rankings, OsuConfigManager config, SessionStatics statics) + private void load(SettingsOverlay settings, OsuConfigManager config, SessionStatics statics) { holdDelay = config.GetBindable(OsuSetting.UIHoldActivationDelay); loginDisplayed = statics.GetBindable(Static.LoginOverlayDisplayed); @@ -137,7 +137,6 @@ namespace osu.Game.Screens.Menu }; buttons.OnSettings = () => settings?.ToggleVisibility(); - buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility(); LoadComponentAsync(background = new BackgroundScreenDefault()); preloadSongSelect(); From 283c69a68f3f4a1a29ea6d676fdaabdad67000aa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 23:12:56 +0900 Subject: [PATCH 5887/6909] Update enum name in line with changes --- osu.Game/Input/Bindings/GlobalActionContainer.cs | 4 ++-- osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index fa3f70735a..b8c2fa201f 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -34,7 +34,7 @@ namespace osu.Game.Input.Bindings new KeyBinding(new[] { InputKey.Control, InputKey.Alt, InputKey.R }, GlobalAction.ResetInputSettings), new KeyBinding(new[] { InputKey.Control, InputKey.T }, GlobalAction.ToggleToolbar), new KeyBinding(new[] { InputKey.Control, InputKey.O }, GlobalAction.ToggleSettings), - new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleDirect), + new KeyBinding(new[] { InputKey.Control, InputKey.D }, GlobalAction.ToggleBeatmapListing), new KeyBinding(new[] { InputKey.Control, InputKey.N }, GlobalAction.ToggleNotifications), new KeyBinding(InputKey.Escape, GlobalAction.Back), @@ -113,7 +113,7 @@ namespace osu.Game.Input.Bindings ToggleSettings, [Description("Toggle beatmap listing")] - ToggleDirect, + ToggleBeatmapListing, [Description("Increase volume")] IncreaseVolume, diff --git a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs index c495d673ce..bfe36a6a0f 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarBeatmapListingButton.cs @@ -13,7 +13,7 @@ namespace osu.Game.Overlays.Toolbar public ToolbarBeatmapListingButton() { - Hotkey = GlobalAction.ToggleDirect; + Hotkey = GlobalAction.ToggleBeatmapListing; } [BackgroundDependencyLoader(true)] From cf3043fc08fa723dde4f6c4a7ea4a060a4e72c50 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 23:20:26 +0900 Subject: [PATCH 5888/6909] Only show "development build" footer on debug releases --- osu.Desktop/Overlays/VersionManager.cs | 39 ++++++++++++++++---------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs index 8c759f8487..59d0f9635d 100644 --- a/osu.Desktop/Overlays/VersionManager.cs +++ b/osu.Desktop/Overlays/VersionManager.cs @@ -26,9 +26,11 @@ namespace osu.Desktop.Overlays Alpha = 0; + FillFlowContainer mainFill; + Children = new Drawable[] { - new FillFlowContainer + mainFill = new FillFlowContainer { AutoSizeAxes = Axes.Both, Direction = FillDirection.Vertical, @@ -55,23 +57,30 @@ namespace osu.Desktop.Overlays }, } }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Font = OsuFont.Numeric.With(size: 12), - Colour = colours.Yellow, - Text = @"Development Build" - }, - new Sprite - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Texture = textures.Get(@"Menu/dev-build-footer"), - }, } } }; + + if (DebugUtils.IsDebugBuild) + { + mainFill.AddRange(new Drawable[] + { + new OsuSpriteText + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Font = OsuFont.Numeric.With(size: 12), + Colour = colours.Yellow, + Text = @"Development Build" + }, + new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.TopCentre, + Texture = textures.Get(@"Menu/dev-build-footer"), + }, + }); + } } protected override void PopIn() From cfbfb8d58bd2768bf83be25c2e2da1cd26a1d4ce Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 23:21:46 +0900 Subject: [PATCH 5889/6909] Revert "Remove related events" This reverts commit 59025e9d50a6866b71f2571919460c94a155222c. --- osu.Game/Screens/Menu/ButtonSystem.cs | 1 + osu.Game/Screens/Menu/MainMenu.cs | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 8417858878..0c47ef3b1e 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -39,6 +39,7 @@ namespace osu.Game.Screens.Menu public Action OnEdit; public Action OnExit; + public Action OnBeatmapListing; public Action OnSolo; public Action OnSettings; public Action OnMultiplayer; diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index ef45a656fa..97fd58318b 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -71,7 +71,7 @@ namespace osu.Game.Screens.Menu private SongTicker songTicker; [BackgroundDependencyLoader(true)] - private void load(SettingsOverlay settings, 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); @@ -137,6 +137,7 @@ namespace osu.Game.Screens.Menu }; buttons.OnSettings = () => settings?.ToggleVisibility(); + buttons.OnBeatmapListing = () => beatmapListing?.ToggleVisibility(); LoadComponentAsync(background = new BackgroundScreenDefault()); preloadSongSelect(); From 35be7ec0e1477c9d904538c4dd726e50b6f2e827 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 23:28:14 +0900 Subject: [PATCH 5890/6909] Add back button but rename to "browse" --- osu.Game/Screens/Menu/ButtonSystem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 0c47ef3b1e..f400b2114b 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -129,7 +129,8 @@ namespace osu.Game.Screens.Menu buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); 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(@"edit", @"button-generic-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); + buttonsTopLevel.Add(new Button(@"browse", @"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)); From d056e6575e1cd711890ffee0592b173b6eccb960 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 6 Jan 2021 23:30:54 +0900 Subject: [PATCH 5891/6909] Use IsDeployedBuild instead of IsDebugBuild for footer display conditional --- osu.Desktop/Overlays/VersionManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/Overlays/VersionManager.cs b/osu.Desktop/Overlays/VersionManager.cs index 59d0f9635d..e4a3451651 100644 --- a/osu.Desktop/Overlays/VersionManager.cs +++ b/osu.Desktop/Overlays/VersionManager.cs @@ -61,7 +61,7 @@ namespace osu.Desktop.Overlays } }; - if (DebugUtils.IsDebugBuild) + if (!game.IsDeployedBuild) { mainFill.AddRange(new Drawable[] { From a8530fde9d3dea6f4a0064e8371c2debbcdd2471 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 00:05:12 +0900 Subject: [PATCH 5892/6909] Tidy up variables and spacing --- osu.Desktop/DiscordRichPresence.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 172db324cb..63b12fb84b 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -31,7 +31,8 @@ namespace osu.Desktop private readonly IBindable status = new Bindable(); private readonly IBindable activity = new Bindable(); - private readonly Bindable mode = new Bindable(); + + private readonly Bindable privacyMode = new Bindable(); private readonly RichPresence presence = new RichPresence { @@ -53,7 +54,8 @@ namespace osu.Desktop client.OnError += (_, e) => Logger.Log($"An error occurred with Discord RPC Client: {e.Code} {e.Message}", LoggingTarget.Network); - config.BindWith(OsuSetting.DiscordRichPresence, mode); + config.BindWith(OsuSetting.DiscordRichPresence, privacyMode); + (user = provider.LocalUser.GetBoundCopy()).BindValueChanged(u => { status.UnbindBindings(); @@ -66,7 +68,7 @@ namespace osu.Desktop ruleset.BindValueChanged(_ => updateStatus()); status.BindValueChanged(_ => updateStatus()); activity.BindValueChanged(_ => updateStatus()); - mode.BindValueChanged(_ => updateStatus()); + privacyMode.BindValueChanged(_ => updateStatus()); client.Initialize(); } @@ -82,7 +84,7 @@ namespace osu.Desktop if (!client.IsInitialized) return; - if (status.Value is UserStatusOffline || mode.Value == DiscordRichPresenceMode.Off) + if (status.Value is UserStatusOffline || privacyMode.Value == DiscordRichPresenceMode.Off) { client.ClearPresence(); return; @@ -100,7 +102,7 @@ namespace osu.Desktop } // update user information - if (mode.Value == DiscordRichPresenceMode.Limited) + if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; else presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty); @@ -144,7 +146,7 @@ namespace osu.Desktop return edit.Beatmap.ToString(); case UserActivity.InLobby lobby: - return mode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; + return privacyMode.Value == DiscordRichPresenceMode.Limited ? string.Empty : lobby.Room.Name.Value; } return string.Empty; From fb057857e707349ead9bf7aa539d5e61c3a42cbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Jan 2021 17:40:29 +0100 Subject: [PATCH 5893/6909] Update references to current year --- Directory.Build.props | 4 ++-- LICENCE | 2 +- osu.Desktop/osu.nuspec | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Directory.Build.props b/Directory.Build.props index 9ec442aafa..049db816a8 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -40,7 +40,7 @@ https://github.com/ppy/osu Automated release. ppy Pty Ltd - Copyright (c) 2020 ppy Pty Ltd + Copyright (c) 2021 ppy Pty Ltd osu game - \ No newline at end of file + diff --git a/LICENCE b/LICENCE index 2435c23545..b5962ad3b2 100644 --- a/LICENCE +++ b/LICENCE @@ -1,4 +1,4 @@ -Copyright (c) 2020 ppy Pty Ltd . +Copyright (c) 2021 ppy Pty Ltd . Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/osu.Desktop/osu.nuspec b/osu.Desktop/osu.nuspec index 2fc6009183..fa182f8e70 100644 --- a/osu.Desktop/osu.nuspec +++ b/osu.Desktop/osu.nuspec @@ -11,7 +11,7 @@ false A free-to-win rhythm game. Rhythm is just a *click* away! testing - Copyright (c) 2020 ppy Pty Ltd + Copyright (c) 2021 ppy Pty Ltd en-AU From 09742998cdeaeaae7fda4c77309e45cd14b69b18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Jan 2021 17:40:46 +0100 Subject: [PATCH 5894/6909] Fix mistaken obsoletion notice It was added in c9f38f7bb6af116fa7ec813ceb3f100dcad30adb, which specified 2021 in another place (and was committed in October of 2020 anyway). Update the year so that it doesn't get culled prematurely. --- 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 da6da0ea97..e5eaf5db88 100644 --- a/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs +++ b/osu.Game/Rulesets/Objects/Drawables/DrawableHitObject.cs @@ -750,7 +750,7 @@ namespace osu.Game.Rulesets.Objects.Drawables if (Result.Type != originalType) { Logger.Log($"{GetType().ReadableName()} applied an invalid hit result ({originalType}) when {nameof(HitResult.IgnoreMiss)} or {nameof(HitResult.IgnoreHit)} is expected.\n" - + $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2020-03-28 onwards.", level: LogLevel.Important); + + $"This has been automatically adjusted to {Result.Type}, and support will be removed from 2021-03-28 onwards.", level: LogLevel.Important); } } From 539785e422accca0ccd360b4b02969ccf7a70a24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Jan 2021 17:46:44 +0100 Subject: [PATCH 5895/6909] Remove obsoleted IHasCurve --- .../Rulesets/Objects/Legacy/ConvertSlider.cs | 5 +- osu.Game/Rulesets/Objects/Types/IHasCurve.cs | 55 ------------------- 2 files changed, 1 insertion(+), 59 deletions(-) delete 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 36b421586e..df569b91c1 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertSlider.cs @@ -10,10 +10,7 @@ using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Rulesets.Objects.Legacy { - internal abstract class ConvertSlider : ConvertHitObject, IHasPathWithRepeats, IHasLegacyLastTickOffset, -#pragma warning disable 618 - IHasCurve -#pragma warning restore 618 + 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/IHasCurve.cs b/osu.Game/Rulesets/Objects/Types/IHasCurve.cs deleted file mode 100644 index 26f50ffa31..0000000000 --- a/osu.Game/Rulesets/Objects/Types/IHasCurve.cs +++ /dev/null @@ -1,55 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System; -using osuTK; - -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 -} From 9cc63e8dce8e65feba008ffb02195928f8ea8a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Jan 2021 17:48:46 +0100 Subject: [PATCH 5896/6909] Remove obsoleted IHasEndTime --- .../Rulesets/Objects/Types/IHasDuration.cs | 16 +++--------- .../Rulesets/Objects/Types/IHasEndTime.cs | 26 ------------------- 2 files changed, 3 insertions(+), 39 deletions(-) delete mode 100644 osu.Game/Rulesets/Objects/Types/IHasEndTime.cs diff --git a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs index b558273650..ca734da5ad 100644 --- a/osu.Game/Rulesets/Objects/Types/IHasDuration.cs +++ b/osu.Game/Rulesets/Objects/Types/IHasDuration.cs @@ -6,26 +6,16 @@ namespace osu.Game.Rulesets.Objects.Types /// /// A HitObject that ends at a different time than its start time. /// -#pragma warning disable 618 - public interface IHasDuration : IHasEndTime -#pragma warning restore 618 + public interface IHasDuration { - double IHasEndTime.EndTime - { - get => EndTime; - set => Duration = (Duration - EndTime) + value; - } - - double IHasEndTime.Duration => Duration; - /// /// The time at which the HitObject ends. /// - new double EndTime { get; } + double EndTime { get; } /// /// The duration of the HitObject. /// - new double Duration { get; set; } + double Duration { get; set; } } } diff --git a/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs b/osu.Game/Rulesets/Objects/Types/IHasEndTime.cs deleted file mode 100644 index c3769c5909..0000000000 --- a/osu.Game/Rulesets/Objects/Types/IHasEndTime.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; -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 - { - /// - /// The time at which the HitObject ends. - /// - [JsonIgnore] - double EndTime { get; set; } - - /// - /// The duration of the HitObject. - /// - double Duration { get; } - } -} From 68352782db59f2db881ba5f443bf352cf8a9c03b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Jan 2021 18:11:47 +0100 Subject: [PATCH 5897/6909] Change .StartsWith() to .Equals() In line with planned-but-delayed breaking change. --- osu.Game/Rulesets/RulesetStore.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/RulesetStore.cs b/osu.Game/Rulesets/RulesetStore.cs index d422bca087..deabea57ef 100644 --- a/osu.Game/Rulesets/RulesetStore.cs +++ b/osu.Game/Rulesets/RulesetStore.cs @@ -100,9 +100,7 @@ namespace osu.Game.Rulesets foreach (var r in instances.Where(r => !(r is ILegacyRuleset))) { - // 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 (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.StartsWith(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) + if (existingRulesets.FirstOrDefault(ri => ri.InstantiationInfo.Equals(r.RulesetInfo.InstantiationInfo, StringComparison.Ordinal)) == null) context.RulesetInfo.Add(r.RulesetInfo); } From 4998aaaa987b75de4d017f09067f44dd05caeec9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Jan 2021 18:13:30 +0100 Subject: [PATCH 5898/6909] Remove outdated warning disable Does not trigger any more on Rider 2020.3.2. --- osu.Game/Screens/Select/BeatmapCarousel.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapCarousel.cs b/osu.Game/Screens/Select/BeatmapCarousel.cs index 36f8fbedb3..7ba6e400bf 100644 --- a/osu.Game/Screens/Select/BeatmapCarousel.cs +++ b/osu.Game/Screens/Select/BeatmapCarousel.cs @@ -936,7 +936,6 @@ namespace osu.Game.Screens.Select Masking = false; } - // ReSharper disable once OptionalParameterHierarchyMismatch 2020.3 EAP4 bug. (https://youtrack.jetbrains.com/issue/RSRP-481535?p=RIDER-51910) protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) { UserScrolling = true; From 9984c80c87c428316d622523f805a4d7a4a4f196 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Jan 2021 20:40:47 +0100 Subject: [PATCH 5899/6909] Make useless existing test actually fail --- .../Mods/TestSceneOsuModDifficultyAdjust.cs | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs index 49c1fe8540..db8546c71b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModDifficultyAdjust.cs @@ -1,13 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; +using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Tests.Mods @@ -18,8 +22,23 @@ namespace osu.Game.Rulesets.Osu.Tests.Mods public void TestNoAdjustment() => CreateModTest(new ModTestData { Mod = new OsuModDifficultyAdjust(), + Beatmap = new Beatmap + { + BeatmapInfo = new BeatmapInfo + { + BaseDifficulty = new BeatmapDifficulty + { + CircleSize = 8 + } + }, + HitObjects = new List + { + new HitCircle { StartTime = 1000 }, + new HitCircle { StartTime = 2000 } + } + }, Autoplay = true, - PassCondition = checkSomeHit + PassCondition = () => checkSomeHit() && checkObjectsScale(0.29f) }); [Test] From 303cc62ee7c8eec32cb9cd4522202bb3cbdf512d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 6 Jan 2021 21:57:54 +0100 Subject: [PATCH 5900/6909] Transfer flags indicating if settings were changed --- .../Mods/CatchModDifficultyAdjust.cs | 4 ++-- .../Mods/OsuModDifficultyAdjust.cs | 4 ++-- osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 18 +++++++++++++++--- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs index acdd0a420c..438d17dbc5 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModDifficultyAdjust.cs @@ -59,8 +59,8 @@ namespace osu.Game.Rulesets.Catch.Mods { base.ApplySettings(difficulty); - difficulty.CircleSize = CircleSize.Value; - difficulty.ApproachRate = ApproachRate.Value; + ApplySetting(CircleSize, cs => difficulty.CircleSize = cs); + ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar); } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs index ff995e38ce..a638234dbd 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModDifficultyAdjust.cs @@ -59,8 +59,8 @@ namespace osu.Game.Rulesets.Osu.Mods { base.ApplySettings(difficulty); - difficulty.CircleSize = CircleSize.Value; - difficulty.ApproachRate = ApproachRate.Value; + ApplySetting(CircleSize, cs => difficulty.CircleSize = cs); + ApplySetting(ApproachRate, ar => difficulty.ApproachRate = ar); } } } diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index 72a4bb297f..a531e885db 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -116,18 +116,30 @@ namespace osu.Game.Rulesets.Mods internal override void CopyAdjustedSetting(IBindable target, object source) { - userChangedSettings[target] = true; + // if the value is non-bindable, it's presumably coming from an external source (like the API) - therefore presume it is not default. + // if the value is bindable, defer to the source's IsDefault to be able to tell. + userChangedSettings[target] = !(source is IBindable bindableSource) || !bindableSource.IsDefault; base.CopyAdjustedSetting(target, source); } + /// + /// Applies a setting from a configuration bindable using , if it has been changed by the user. + /// + protected void ApplySetting(BindableNumber setting, Action applyFunc) + where T : struct, IComparable, IConvertible, IEquatable + { + if (userChangedSettings.TryGetValue(setting, out bool userChangedSetting) && userChangedSetting) + applyFunc.Invoke(setting.Value); + } + /// /// Apply all custom settings to the provided beatmap. /// /// The beatmap to have settings applied. protected virtual void ApplySettings(BeatmapDifficulty difficulty) { - difficulty.DrainRate = DrainRate.Value; - difficulty.OverallDifficulty = OverallDifficulty.Value; + ApplySetting(DrainRate, dr => difficulty.DrainRate = dr); + ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od); } } } From 6620eadec3f1c7fe5fac7b98d52683d1a05aba4a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 18:47:03 +0900 Subject: [PATCH 5901/6909] Reduce default hover sound debounce interval --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index 40899e7e95..22d59e70f7 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -24,7 +24,7 @@ namespace osu.Game.Graphics.UserInterface /// /// Length of debounce for hover sound playback, in milliseconds. Default is 50ms. /// - public double HoverDebounceTime { get; } = 50; + public double HoverDebounceTime { get; } = 20; protected readonly HoverSampleSet SampleSet; From 8f52a83b297d277bf07bbda9e9e2efd2d0684092 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 18:47:20 +0900 Subject: [PATCH 5902/6909] Share hover sound debounce across all instances via SessionStatics --- osu.Game/Configuration/SessionStatics.cs | 9 ++++++ .../Graphics/UserInterface/HoverSounds.cs | 29 +++++++++++-------- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 03bc434aac..f28ee707b9 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.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.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; namespace osu.Game.Configuration { @@ -14,6 +16,7 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); + Set(Static.LastHoverSoundPlaybackTime, 0.0); Set(Static.SeasonalBackgrounds, null); } } @@ -28,5 +31,11 @@ namespace osu.Game.Configuration /// Value under this lookup can be null if there are no backgrounds available (or API is not reachable). /// SeasonalBackgrounds, + + /// + /// The last playback time in milliseconds of a hover sample (from ). + /// Used to debounce hover sounds game-wide to avoid volume saturation, especially in scrolling views with many UI controls like . + /// + LastHoverSoundPlaybackTime } } diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index 22d59e70f7..efd55c892c 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -5,11 +5,12 @@ using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; +using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; -using osu.Framework.Threading; +using osu.Game.Configuration; namespace osu.Game.Graphics.UserInterface { @@ -28,31 +29,35 @@ namespace osu.Game.Graphics.UserInterface protected readonly HoverSampleSet SampleSet; + private Bindable lastPlaybackTime; + public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal) { SampleSet = sampleSet; RelativeSizeAxes = Axes.Both; } - private ScheduledDelegate playDelegate; + [BackgroundDependencyLoader] + private void load(AudioManager audio, SessionStatics statics) + { + lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); + + sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}"); + } protected override bool OnHover(HoverEvent e) { - playDelegate?.Cancel(); + bool requiresDebounce = HoverDebounceTime <= 0; + bool enoughTimePassedSinceLastPlayback = lastPlaybackTime.Value == 0 || Time.Current - lastPlaybackTime.Value > HoverDebounceTime; - if (HoverDebounceTime <= 0) + if (!requiresDebounce || enoughTimePassedSinceLastPlayback) + { sampleHover?.Play(); - else - playDelegate = Scheduler.AddDelayed(() => sampleHover?.Play(), HoverDebounceTime); + lastPlaybackTime.Value = Time.Current; + } return base.OnHover(e); } - - [BackgroundDependencyLoader] - private void load(AudioManager audio) - { - sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}"); - } } public enum HoverSampleSet From 69ac22dd7fdd477943ce041cb42072de318c15e3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 19:06:10 +0900 Subject: [PATCH 5903/6909] Fix incorrectly copy pasted xmldoc --- osu.Game/Beatmaps/ControlPoints/ControlPoint.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs index 090675473d..e8dc623ddb 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPoint.cs @@ -30,8 +30,7 @@ namespace osu.Game.Beatmaps.ControlPoints public abstract bool IsRedundant(ControlPoint existing); /// - /// Create a copy of this room without online information. - /// Should be used to create a local copy of a room for submitting in the future. + /// Create an unbound copy of this control point. /// public ControlPoint CreateCopy() { From 00dc98e3ab0fef1b568969a5086d9ede48ec8003 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 19:06:52 +0900 Subject: [PATCH 5904/6909] Make legacy control point's BpmMultiplier setter private again --- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index a06ad35b89..069a25b83d 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -164,7 +164,7 @@ namespace osu.Game.Beatmaps.Formats /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. /// DO NOT USE THIS UNLESS 100% SURE. /// - public float BpmMultiplier { get; set; } + public float BpmMultiplier { get; private set; } public LegacyDifficultyControlPoint(double beatLength) : this() From 42643fbaf69e321f8ae1d52e70ddbc17ac985a42 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 19:10:19 +0900 Subject: [PATCH 5905/6909] Use already resolved EditorBeatmap rather than resolving a second time locally --- osu.Game/Screens/Edit/Setup/DifficultySection.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/DifficultySection.cs b/osu.Game/Screens/Edit/Setup/DifficultySection.cs index f180d7e63e..36fb0191b0 100644 --- a/osu.Game/Screens/Edit/Setup/DifficultySection.cs +++ b/osu.Game/Screens/Edit/Setup/DifficultySection.cs @@ -13,9 +13,6 @@ namespace osu.Game.Screens.Edit.Setup { internal class DifficultySection : SetupSection { - [Resolved] - private EditorBeatmap editorBeatmap { get; set; } - private LabelledSliderBar circleSizeSlider; private LabelledSliderBar healthDrainSlider; private LabelledSliderBar approachRateSlider; @@ -93,7 +90,7 @@ namespace osu.Game.Screens.Edit.Setup Beatmap.BeatmapInfo.BaseDifficulty.ApproachRate = approachRateSlider.Current.Value; Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty = overallDifficultySlider.Current.Value; - editorBeatmap.UpdateAllHitObjects(); + Beatmap.UpdateAllHitObjects(); } } } From 77b55212a3c9870a0e42cd90e77e3ae4a552e273 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 19:11:51 +0900 Subject: [PATCH 5906/6909] Change access of beatmap to use working for consistency in file --- osu.Game/Screens/Edit/Setup/ResourcesSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs index b3bceceea3..010d7c2797 100644 --- a/osu.Game/Screens/Edit/Setup/ResourcesSection.cs +++ b/osu.Game/Screens/Edit/Setup/ResourcesSection.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Edit.Setup audioTrackTextBox = new FileChooserLabelledTextBox { Label = "Audio Track", - Current = { Value = Beatmap.Metadata.AudioFile ?? "Click to select a track" }, + Current = { Value = working.Value.Metadata.AudioFile ?? "Click to select a track" }, Target = audioTrackFileChooserContainer, TabbableContentContainer = this }, From 3c3e860dbc34d37855b79786a1abb754af1667e8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 7 Jan 2021 23:52:04 +0900 Subject: [PATCH 5907/6909] Move ControlPointInfo copying to base Beatmap.Clone method (and remove setter) --- osu.Game/Beatmaps/Beatmap.cs | 10 +++++++++- osu.Game/Beatmaps/IBeatmap.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 -- osu.Game/Screens/Edit/EditorBeatmap.cs | 6 +----- osu.Game/Screens/Play/GameplayBeatmap.cs | 6 +----- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 5435e86dfd..be2006e67a 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -50,7 +50,15 @@ namespace osu.Game.Beatmaps IBeatmap IBeatmap.Clone() => Clone(); - public Beatmap Clone() => (Beatmap)MemberwiseClone(); + public Beatmap Clone() + { + var clone = (Beatmap)MemberwiseClone(); + + clone.ControlPointInfo = ControlPointInfo.CreateCopy(); + // todo: deep clone other elements as required. + + return clone; + } } public class Beatmap : Beatmap diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 7dd85e1232..8f27e0b0e9 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps /// /// The control points in this beatmap. /// - ControlPointInfo ControlPointInfo { get; set; } + ControlPointInfo ControlPointInfo { get; } /// /// The breaks in this beatmap. diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index d25adca92b..30382c444f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -111,8 +111,6 @@ namespace osu.Game.Beatmaps // Convert IBeatmap converted = converter.Convert(cancellationSource.Token); - converted.ControlPointInfo = converted.ControlPointInfo.CreateCopy(); - // Apply conversion mods to the result foreach (var mod in mods.OfType()) { diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index a54a95f59d..165d2ba278 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -74,11 +74,7 @@ namespace osu.Game.Screens.Edit public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; - public ControlPointInfo ControlPointInfo - { - get => PlayableBeatmap.ControlPointInfo; - set => PlayableBeatmap.ControlPointInfo = value; - } + public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo; public List Breaks => PlayableBeatmap.Breaks; diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index 565595656f..64894544f4 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -29,11 +29,7 @@ namespace osu.Game.Screens.Play public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; - public ControlPointInfo ControlPointInfo - { - get => PlayableBeatmap.ControlPointInfo; - set => PlayableBeatmap.ControlPointInfo = value; - } + public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo; public List Breaks => PlayableBeatmap.Breaks; From 11801d61c1b3701356b253c9feb3a23eaa0d1dcf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 14:05:22 +0900 Subject: [PATCH 5908/6909] Use nullable doubule to better represent initial playback case --- osu.Game/Configuration/SessionStatics.cs | 2 +- osu.Game/Graphics/UserInterface/HoverSounds.cs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index f28ee707b9..382eab751b 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -16,7 +16,7 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); - Set(Static.LastHoverSoundPlaybackTime, 0.0); + Set(Static.LastHoverSoundPlaybackTime, (double?)0.0); Set(Static.SeasonalBackgrounds, null); } } diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index efd55c892c..29b4bf5704 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -29,7 +29,7 @@ namespace osu.Game.Graphics.UserInterface protected readonly HoverSampleSet SampleSet; - private Bindable lastPlaybackTime; + private Bindable lastPlaybackTime; public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal) { @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio, SessionStatics statics) { - lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); + lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}"); } @@ -48,7 +48,7 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { bool requiresDebounce = HoverDebounceTime <= 0; - bool enoughTimePassedSinceLastPlayback = lastPlaybackTime.Value == 0 || Time.Current - lastPlaybackTime.Value > HoverDebounceTime; + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; if (!requiresDebounce || enoughTimePassedSinceLastPlayback) { From e156bcdcae175f46a5c78fb9eb7c4ed5d0dbf5d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 14:05:34 +0900 Subject: [PATCH 5909/6909] Remove unnecessary (and broken) requiresDebounce check --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index 29b4bf5704..4d30d61ff0 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -47,10 +47,9 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { - bool requiresDebounce = HoverDebounceTime <= 0; bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; - if (!requiresDebounce || enoughTimePassedSinceLastPlayback) + if (enoughTimePassedSinceLastPlayback) { sampleHover?.Play(); lastPlaybackTime.Value = Time.Current; From c208800150494eceb06472e45310eecc8859b29b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 14:17:14 +0900 Subject: [PATCH 5910/6909] Fix auto selection scenario regressing due to scheduling too much --- osu.Game/Overlays/Mods/ModButton.cs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 51c8bcf9a9..b58e70cae6 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -52,9 +52,10 @@ namespace osu.Game.Overlays.Mods if (newIndex == selectedIndex) return false; int direction = newIndex < selectedIndex ? -1 : 1; + bool beforeSelected = Selected; - Mod modBefore = SelectedMod ?? Mods[0]; + Mod previousSelection = SelectedMod ?? Mods[0]; if (newIndex >= Mods.Length) newIndex = -1; @@ -64,25 +65,26 @@ namespace osu.Game.Overlays.Mods if (newIndex >= 0 && !Mods[newIndex].HasImplementation) return false; + selectedIndex = newIndex; + + Mod newSelection = SelectedMod ?? Mods[0]; + Schedule(() => { - selectedIndex = newIndex; - Mod modAfter = SelectedMod ?? Mods[0]; - if (beforeSelected != Selected) { iconsContainer.RotateTo(Selected ? 5f : 0f, 300, Easing.OutElastic); iconsContainer.ScaleTo(Selected ? 1.1f : 1f, 300, Easing.OutElastic); } - if (modBefore != modAfter) + if (previousSelection != newSelection) { const float rotate_angle = 16; foregroundIcon.RotateTo(rotate_angle * direction, mod_switch_duration, mod_switch_easing); backgroundIcon.RotateTo(-rotate_angle * direction, mod_switch_duration, mod_switch_easing); - backgroundIcon.Mod = modAfter; + backgroundIcon.Mod = newSelection; using (BeginDelayedSequence(mod_switch_duration, true)) { @@ -94,15 +96,15 @@ namespace osu.Game.Overlays.Mods .RotateTo(rotate_angle * direction) .RotateTo(0f, mod_switch_duration, mod_switch_easing); - Schedule(() => displayMod(modAfter)); + Schedule(() => displayMod(newSelection)); } } foregroundIcon.Selected.Value = Selected; - - SelectionChanged?.Invoke(SelectedMod); }); + SelectionChanged?.Invoke(newSelection); + return true; } From a6766e64de95cdd23dfa17e029275be3e7ea3c15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 17:08:01 +0900 Subject: [PATCH 5911/6909] Add custom handling of Point serialization to fix startup crashes of tournament client SixLabors moved their data types around in a recent update (see https://github.com/ppy/osu-framework/pull/4025) and it was deemed that we should prefer `System.Drawing` primitives where possible. This was applied to the tournament client via https://github.com/ppy/osu/pull/11072 without correct consideration given to the fact that we serialize these types. `System.Drawing.Point` serializes into a comma separated string, which seems to be less correct than what we had, so I've switched back to the old format for the time being. We can reasses this in the future; the main goal here is to restore usability to the tournament client. Closes #11443. --- osu.Game.Tournament/JsonPointConverter.cs | 67 +++++++++++++++++++++++ osu.Game.Tournament/TournamentGameBase.cs | 7 ++- 2 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 osu.Game.Tournament/JsonPointConverter.cs diff --git a/osu.Game.Tournament/JsonPointConverter.cs b/osu.Game.Tournament/JsonPointConverter.cs new file mode 100644 index 0000000000..57b91958d8 --- /dev/null +++ b/osu.Game.Tournament/JsonPointConverter.cs @@ -0,0 +1,67 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Diagnostics; +using System.Drawing; +using Newtonsoft.Json; + +namespace osu.Game.Tournament +{ + /// + /// We made a change from using SixLabors.ImageSharp.Point to System.Drawing.Point at some stage. + /// This handles converting to a standardised format on json serialize/deserialize operations. + /// + internal class JsonPointConverter : JsonConverter + { + public override void WriteJson(JsonWriter writer, Point value, JsonSerializer serializer) + { + // use the format of LaborSharp's Point since it is nicer. + serializer.Serialize(writer, new SixLabors.ImageSharp.Point(value.X, value.Y)); + } + + public override Point ReadJson(JsonReader reader, Type objectType, Point existingValue, bool hasExistingValue, JsonSerializer serializer) + { + if (reader.TokenType != JsonToken.StartObject) + { + // if there's no object present then this is using string representation (System.Drawing.Point serializes to "x,y") + string str = (string)reader.Value; + + Debug.Assert(str != null); + + var split = str.Split(','); + + return new Point(int.Parse(split[0]), int.Parse(split[1])); + } + + var point = new Point(); + + while (reader.Read()) + { + if (reader.TokenType == JsonToken.EndObject) break; + + if (reader.TokenType == JsonToken.PropertyName) + { + var name = reader.Value?.ToString(); + int? val = reader.ReadAsInt32(); + + if (val == null) + continue; + + switch (name) + { + case "X": + point.X = val.Value; + break; + + case "Y": + point.Y = val.Value; + break; + } + } + } + + return point; + } + } +} diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index dbda6aa023..bc36f27e5b 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -8,12 +8,12 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; using osu.Framework.Input; -using osu.Framework.Platform; using osu.Framework.IO.Stores; +using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Online.API.Requests; -using osu.Game.Tournament.IPC; using osu.Game.Tournament.IO; +using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osu.Game.Users; using osuTK.Input; @@ -60,7 +60,7 @@ namespace osu.Game.Tournament { using (Stream stream = storage.GetStream(bracket_filename, FileAccess.Read, FileMode.Open)) using (var sr = new StreamReader(stream)) - ladder = JsonConvert.DeserializeObject(sr.ReadToEnd()); + ladder = JsonConvert.DeserializeObject(sr.ReadToEnd(), new JsonPointConverter()); } ladder ??= new LadderInfo(); @@ -251,6 +251,7 @@ namespace osu.Game.Tournament Formatting = Formatting.Indented, NullValueHandling = NullValueHandling.Ignore, DefaultValueHandling = DefaultValueHandling.Ignore, + Converters = new JsonConverter[] { new JsonPointConverter() } })); } } From edd328c8fecdb2566c8f68f087f2c8089219bb77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 8 Jan 2021 17:24:55 +0900 Subject: [PATCH 5912/6909] Move bindable closer to source class --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index c6018a79e9..04030cdbfd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -37,12 +37,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } + private IBindable operationInProgress; + private SampleChannel sampleReadyCount; private readonly ButtonWithTrianglesExposed button; private int countReady; - private IBindable operationInProgress; public MultiplayerReadyButton() { From 924f91ed9b91445d4cba0bd819441a0959021faa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Jan 2021 15:56:35 +0100 Subject: [PATCH 5913/6909] Fix song select test doing the completely wrong thing --- .../Visual/SongSelect/TestSceneAdvancedStats.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs index 3d3517ada4..40b2f66d74 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneAdvancedStats.cs @@ -11,6 +11,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Select.Details; using osuTK.Graphics; @@ -141,16 +142,12 @@ namespace osu.Game.Tests.Visual.SongSelect AddStep("select changed Difficulty Adjust mod", () => { var ruleset = advancedStats.Beatmap.Ruleset.CreateInstance(); - var difficultyAdjustMod = ruleset.GetAllMods().OfType().Single(); + var difficultyAdjustMod = ruleset.GetAllMods().OfType().Single(); var originalDifficulty = advancedStats.Beatmap.BaseDifficulty; - var adjustedDifficulty = new BeatmapDifficulty - { - CircleSize = originalDifficulty.CircleSize, - DrainRate = originalDifficulty.DrainRate - 0.5f, - OverallDifficulty = originalDifficulty.OverallDifficulty, - ApproachRate = originalDifficulty.ApproachRate + 2.2f, - }; - difficultyAdjustMod.ReadFromDifficulty(adjustedDifficulty); + + difficultyAdjustMod.ReadFromDifficulty(originalDifficulty); + difficultyAdjustMod.DrainRate.Value = originalDifficulty.DrainRate - 0.5f; + difficultyAdjustMod.ApproachRate.Value = originalDifficulty.ApproachRate + 2.2f; SelectedMods.Value = new[] { difficultyAdjustMod }; }); From 9182f5dafb224baf6e6534edc7c727026231a1b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 9 Jan 2021 00:38:38 +0900 Subject: [PATCH 5914/6909] Switch to using an anonymous type for serialisation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Tournament/JsonPointConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/JsonPointConverter.cs b/osu.Game.Tournament/JsonPointConverter.cs index 57b91958d8..7ad972f3e7 100644 --- a/osu.Game.Tournament/JsonPointConverter.cs +++ b/osu.Game.Tournament/JsonPointConverter.cs @@ -17,7 +17,7 @@ namespace osu.Game.Tournament public override void WriteJson(JsonWriter writer, Point value, JsonSerializer serializer) { // use the format of LaborSharp's Point since it is nicer. - serializer.Serialize(writer, new SixLabors.ImageSharp.Point(value.X, value.Y)); + serializer.Serialize(writer, new { value.X, value.Y }); } public override Point ReadJson(JsonReader reader, Type objectType, Point existingValue, bool hasExistingValue, JsonSerializer serializer) From 82725b59c030d27abd3159ee3397a4b77e834b1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 9 Jan 2021 00:56:54 +0900 Subject: [PATCH 5915/6909] Use PointConverter --- osu.Game.Tournament/JsonPointConverter.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tournament/JsonPointConverter.cs b/osu.Game.Tournament/JsonPointConverter.cs index 7ad972f3e7..9c82f8ac06 100644 --- a/osu.Game.Tournament/JsonPointConverter.cs +++ b/osu.Game.Tournament/JsonPointConverter.cs @@ -29,9 +29,7 @@ namespace osu.Game.Tournament Debug.Assert(str != null); - var split = str.Split(','); - - return new Point(int.Parse(split[0]), int.Parse(split[1])); + return new PointConverter().ConvertFromString(str) as Point? ?? new Point(); } var point = new Point(); From 0cf5be3ef432a718d23bab4fc951f04182375ebb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Jan 2021 17:02:57 +0100 Subject: [PATCH 5916/6909] Fix selection change event being invoked with wrong mod --- osu.Game/Overlays/Mods/ModButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index b58e70cae6..ab8efdabcc 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -103,7 +103,7 @@ namespace osu.Game.Overlays.Mods foregroundIcon.Selected.Value = Selected; }); - SelectionChanged?.Invoke(newSelection); + SelectionChanged?.Invoke(SelectedMod); return true; } From 8feaf3fb6ad084885f8b69b997caea7faf92bb50 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 9 Jan 2021 01:24:18 +0900 Subject: [PATCH 5917/6909] 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 611f0d05f4..492c88c7e4 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 6c220a5c21..f28a55e016 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 5445adb3fb..93be3645ee 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From d507a08951b33618a00fcce8e3c61ab289ee9172 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Jan 2021 18:16:03 +0100 Subject: [PATCH 5918/6909] Start with null last hover playback time --- osu.Game/Configuration/SessionStatics.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Configuration/SessionStatics.cs b/osu.Game/Configuration/SessionStatics.cs index 382eab751b..fd401119ff 100644 --- a/osu.Game/Configuration/SessionStatics.cs +++ b/osu.Game/Configuration/SessionStatics.cs @@ -16,7 +16,7 @@ namespace osu.Game.Configuration { Set(Static.LoginOverlayDisplayed, false); Set(Static.MutedAudioNotificationShownOnce, false); - Set(Static.LastHoverSoundPlaybackTime, (double?)0.0); + Set(Static.LastHoverSoundPlaybackTime, (double?)null); Set(Static.SeasonalBackgrounds, null); } } From 49c6abcb5c1a8eaa3baa8d0c3b94b2799b3e82cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Jan 2021 18:25:51 +0100 Subject: [PATCH 5919/6909] Remove mention of default value in xmldoc Just bound to get outdated with every change anyway. Look at the actual default value declaration to see what the default is. --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index 4d30d61ff0..a1d06711db 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -23,7 +23,7 @@ namespace osu.Game.Graphics.UserInterface private SampleChannel sampleHover; /// - /// Length of debounce for hover sound playback, in milliseconds. Default is 50ms. + /// Length of debounce for hover sound playback, in milliseconds. /// public double HoverDebounceTime { get; } = 20; From 52789118a363bf0f648936d9d629d2afa554c76b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Jan 2021 19:59:13 +0100 Subject: [PATCH 5920/6909] Schedule play button state update Revealed by the framework-side transform thread safety checks. `Stopped` is even annotated as not being thread-safe (but was annotated as such long after the class's nascence). --- osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs index e95fdeecf4..04df27ce52 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs @@ -142,7 +142,9 @@ namespace osu.Game.Overlays.BeatmapListing.Panels AddInternal(preview); loading = false; - preview.Stopped += () => Playing.Value = false; + // make sure that the update of value of Playing (and the ensuing value change callbacks) + // are marshaled back to the update thread. + preview.Stopped += () => Schedule(() => Playing.Value = false); // user may have changed their mind. if (Playing.Value) From 274a045d8dbd03640280b422674bd5c7b0eebf44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Jan 2021 20:01:47 +0100 Subject: [PATCH 5921/6909] Remove Dispose() override Culls another non-thread-safe mutation of the `Playing` bindable. It seems to be a weird vestige from an earlier revision of the old "direct" panel, which relied on `DisposeOnDeathRemoval` to finish track playback (and then was removed in 6c150c9ed793799fd6672cc2107c97c2e3844a09). The play button is no longer responsible for managing preview track lifetime anyway; `PreviewTrackManager`'s method are intended for that. --- osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs | 6 ------ 1 file changed, 6 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs index 04df27ce52..eb409785e0 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs @@ -163,11 +163,5 @@ namespace osu.Game.Overlays.BeatmapListing.Panels if (Preview?.Start() != true) Playing.Value = false; } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - Playing.Value = false; - } } } From 284d30d336d0d7b851481a084c3067c45ff3cfdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Jan 2021 21:13:36 +0100 Subject: [PATCH 5922/6909] Move screen activity update to LoadComplete() Fixes a potential crash when moving from main menu to editor after having previously opened the login settings overlay. Setting the activity in BDL as done before is unsafe, as that set can trigger value change callbacks, which in turn can trigger adding transforms, which should always be done on the update thread. Semantically it also makes sense, as the user activity should change once the screen they're moving to has actually loaded and displayed to the user. --- osu.Game/Screens/OsuScreen.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OsuScreen.cs b/osu.Game/Screens/OsuScreen.cs index e1a29946f4..9b716b323d 100644 --- a/osu.Game/Screens/OsuScreen.cs +++ b/osu.Game/Screens/OsuScreen.cs @@ -143,7 +143,11 @@ namespace osu.Game.Screens private void load(OsuGame osu, AudioManager audio) { sampleExit = audio.Samples.Get(@"UI/screen-back"); + } + protected override void LoadComplete() + { + base.LoadComplete(); Activity.Value ??= InitialActivity; } From dad5dd36676de0c965c645b2564e496adb334908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Jan 2021 22:17:37 +0100 Subject: [PATCH 5923/6909] Remove unnecessary permissiveness wrt null --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 3 ++- osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs | 9 ++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 39323d9db9..e539b315e4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -241,7 +241,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer void endOperation() { - readyClickOperation?.Dispose(); + Debug.Assert(readyClickOperation != null); + readyClickOperation.Dispose(); readyClickOperation = null; } } diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs index 6a340d7954..c34d39136e 100644 --- a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -40,13 +40,12 @@ namespace osu.Game.Screens.OnlinePlay return new InvokeOnDisposal(endOperation); } - /// - /// Ends tracking an online operation. - /// Does nothing if an operation has not been begun yet. - /// private void endOperation() { - leasedInProgress?.Return(); + if (leasedInProgress == null) + throw new InvalidOperationException("Cannot end operation multiple times."); + + leasedInProgress.Return(); leasedInProgress = null; } } From c2eeb822b84d007beec6190cda094ca3ea1ba062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Jan 2021 22:23:38 +0100 Subject: [PATCH 5924/6909] Rename {joiningRoom -> operationInProgress} --- osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs index 2a16a62714..9b4e78543c 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/LoungeSubScreen.cs @@ -29,7 +29,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge protected override UserActivity InitialActivity => new UserActivity.SearchingForLobby(); private readonly IBindable initialRoomsReceived = new Bindable(); - private readonly IBindable joiningRoom = new Bindable(); + private readonly IBindable operationInProgress = new Bindable(); private FilterControl filter; private Container content; @@ -110,8 +110,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge if (ongoingOperationTracker != null) { - joiningRoom.BindTo(ongoingOperationTracker.InProgress); - joiningRoom.BindValueChanged(_ => updateLoadingLayer(), true); + operationInProgress.BindTo(ongoingOperationTracker.InProgress); + operationInProgress.BindValueChanged(_ => updateLoadingLayer(), true); } } @@ -187,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge private void updateLoadingLayer() { - if (joiningRoom.Value || !initialRoomsReceived.Value) + if (operationInProgress.Value || !initialRoomsReceived.Value) loadingLayer.Show(); else loadingLayer.Hide(); From ff60d652ed6094054125990375dc9edc8ea05313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 8 Jan 2021 22:28:21 +0100 Subject: [PATCH 5925/6909] Move out test ongoing operation tracker to higher level --- .../Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs | 6 +----- osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 3 +++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index a6037dcbf2..8b9bffcee1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -15,7 +15,6 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Resources; using osu.Game.Users; @@ -31,9 +30,6 @@ namespace osu.Game.Tests.Visual.Multiplayer private BeatmapManager beatmaps; private RulesetStore rulesets; - [Cached] - private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - private IDisposable readyClickOperation; [BackgroundDependencyLoader] @@ -66,7 +62,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }, OnReadyClick = async () => { - readyClickOperation = ongoingOperationTracker.BeginOperation(); + readyClickOperation = OngoingOperationTracker.BeginOperation(); if (Client.IsHost && Client.LocalUser?.State == MultiplayerUserState.Ready) { diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index da0e39d965..75ddb34685 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -23,6 +23,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached] public Bindable Filter { get; } + [Cached] + public OngoingOperationTracker OngoingOperationTracker { get; } = new OngoingOperationTracker(); + protected override Container Content => content; private readonly TestMultiplayerRoomContainer content; From 0aad0c7c6c5dd363bd5bb75d2eb4cc5c35a3804f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 9 Jan 2021 00:30:12 +0300 Subject: [PATCH 5926/6909] Target logic at `this` and adjust variables --- osu.Game/Rulesets/Mods/Mod.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 7fe9a06597..e72e9a004f 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -144,12 +144,12 @@ namespace osu.Game.Rulesets.Mods foreach (var (_, prop) in this.GetSettingsSourceProperties()) { - var origBindable = (IBindable)prop.GetValue(this); - var copyBindable = (IBindable)prop.GetValue(copy); + var targetBindable = (IBindable)prop.GetValue(this); + var sourceBindable = (IBindable)prop.GetValue(them); // we only care about changes that have been made away from defaults. - if (!origBindable.IsDefault) - copy.CopyAdjustedSetting(copyBindable, origBindable); + if (!sourceBindable.IsDefault) + CopyAdjustedSetting(targetBindable, sourceBindable); } } From 8c3955d34136026b2387864223b065c7f57b185e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 9 Jan 2021 21:38:20 +0100 Subject: [PATCH 5927/6909] Improve safety of ongoing operation tracker Finishing an operation started via `OngoingOperationTracker.BeginOperation()` was risky in cases where the operation ended at a callback on another thread (which, in the case of multiplayer, is *most* cases). In particular, if any consumer registered a callback that mutates transforms when the operation ends, it would result in crashes after the framework-side safety checks. Rework `OngoingOperationTracker` into an always-present component residing in the drawable hierarchy, and ensure that the `operationInProgress` bindable is always updated on the update thread. This way consumers don't have to add local schedules in multiple places. --- .../TestSceneCreateMultiplayerMatchButton.cs | 7 +------ .../Screens/OnlinePlay/OngoingOperationTracker.cs | 11 +++++++++-- osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs | 5 +++-- .../Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 3 ++- .../Multiplayer/TestMultiplayerRoomContainer.cs | 4 ++++ 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs index 381270c5aa..2f0398c6ef 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneCreateMultiplayerMatchButton.cs @@ -3,18 +3,13 @@ using System; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneCreateMultiplayerMatchButton : MultiplayerTestScene { - [Cached] - private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - private CreateMultiplayerMatchButton button; public override void SetUpSteps() @@ -36,7 +31,7 @@ namespace osu.Game.Tests.Visual.Multiplayer assertButtonEnableState(true); - AddStep("begin joining room", () => joiningRoomOperation = ongoingOperationTracker.BeginOperation()); + AddStep("begin joining room", () => joiningRoomOperation = OngoingOperationTracker.BeginOperation()); assertButtonEnableState(false); AddStep("end joining room", () => joiningRoomOperation.Dispose()); diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs index c34d39136e..5c9e9ce90b 100644 --- a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; namespace osu.Game.Screens.OnlinePlay { @@ -11,7 +12,7 @@ namespace osu.Game.Screens.OnlinePlay /// Utility class to track ongoing online operations' progress. /// Can be used to disable interactivity while waiting for a response from online sources. /// - public class OngoingOperationTracker + public class OngoingOperationTracker : Component { /// /// Whether there is an online operation in progress. @@ -22,6 +23,11 @@ namespace osu.Game.Screens.OnlinePlay private LeasedBindable leasedInProgress; + public OngoingOperationTracker() + { + AlwaysPresent = true; + } + /// /// Begins tracking a new online operation. /// @@ -37,7 +43,8 @@ namespace osu.Game.Screens.OnlinePlay leasedInProgress = inProgress.BeginLease(true); leasedInProgress.Value = true; - return new InvokeOnDisposal(endOperation); + // for extra safety, marshal the end of operation back to the update thread if necessary. + return new InvokeOnDisposal(() => Scheduler.Add(endOperation, false)); } private void endOperation() diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index b73e0a7c52..71fd0d5c76 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.OnlinePlay private readonly Bindable currentFilter = new Bindable(new FilterCriteria()); [Cached] - private readonly OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); + private OngoingOperationTracker ongoingOperationTracker { get; set; } [Resolved(CanBeNull = true)] private MusicController music { get; set; } @@ -144,7 +144,8 @@ namespace osu.Game.Screens.OnlinePlay }; button.Action = () => OpenNewRoom(); }), - RoomManager = CreateRoomManager() + RoomManager = CreateRoomManager(), + ongoingOperationTracker = new OngoingOperationTracker() } }; diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 75ddb34685..a87b22affe 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public Bindable Filter { get; } [Cached] - public OngoingOperationTracker OngoingOperationTracker { get; } = new OngoingOperationTracker(); + public OngoingOperationTracker OngoingOperationTracker { get; } protected override Container Content => content; private readonly TestMultiplayerRoomContainer content; @@ -39,6 +39,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Client = content.Client; RoomManager = content.RoomManager; Filter = content.Filter; + OngoingOperationTracker = content.OngoingOperationTracker; } [SetUp] diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs index ad3e2f7105..860caef071 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs @@ -25,6 +25,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached] public readonly Bindable Filter = new Bindable(new FilterCriteria()); + [Cached] + public readonly OngoingOperationTracker OngoingOperationTracker; + public TestMultiplayerRoomContainer() { RelativeSizeAxes = Axes.Both; @@ -33,6 +36,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Client = new TestMultiplayerClient(), RoomManager = new TestMultiplayerRoomManager(), + OngoingOperationTracker = new OngoingOperationTracker(), content = new Container { RelativeSizeAxes = Axes.Both } }); } From 4b4adc927cb49a166f9f13c093c318afbbe18f2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 10 Jan 2021 15:35:53 +0100 Subject: [PATCH 5928/6909] Rename param to match method body --- osu.Game/Rulesets/Mods/Mod.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index e72e9a004f..3a8717e678 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -134,18 +134,18 @@ namespace osu.Game.Rulesets.Mods } /// - /// Copies mod setting values from into this instance. + /// Copies mod setting values from into this instance. /// - /// The mod to copy properties from. - public void CopyFrom(Mod them) + /// The mod to copy properties from. + public void CopyFrom(Mod source) { - if (them.GetType() != GetType()) - throw new ArgumentException($"Expected mod of type {GetType()}, got {them.GetType()}.", nameof(them)); + if (source.GetType() != GetType()) + throw new ArgumentException($"Expected mod of type {GetType()}, got {source.GetType()}.", nameof(source)); foreach (var (_, prop) in this.GetSettingsSourceProperties()) { var targetBindable = (IBindable)prop.GetValue(this); - var sourceBindable = (IBindable)prop.GetValue(them); + var sourceBindable = (IBindable)prop.GetValue(source); // we only care about changes that have been made away from defaults. if (!sourceBindable.IsDefault) From f466791b69d3341b05625157ae52301a4a035be6 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 10 Jan 2021 17:34:20 +0100 Subject: [PATCH 5929/6909] Move assignments to the TournamentSwitcher component This also adds conditional checks for displaying the "Close osu!" button --- osu.Game.Tournament/Screens/SetupScreen.cs | 41 ++++++++++++++++------ 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index de3321397e..7ae0375b9a 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -42,17 +42,13 @@ namespace osu.Game.Tournament.Screens [Resolved] private RulesetStore rulesets { get; set; } - [Resolved] - private TournamentGameBase game { get; set; } - [Resolved(canBeNull: true)] private TournamentSceneManager sceneManager { get; set; } private Bindable windowSize; - private TournamentStorage storage; [BackgroundDependencyLoader] - private void load(FrameworkConfigManager frameworkConfig, Storage storage) + private void load(FrameworkConfigManager frameworkConfig) { windowSize = frameworkConfig.GetBindable(FrameworkSetting.WindowedSize); @@ -67,7 +63,6 @@ namespace osu.Game.Tournament.Screens api.LocalUser.BindValueChanged(_ => Schedule(reload)); stableInfo.OnStableInfoSaved += () => Schedule(reload); - this.storage = (TournamentStorage)storage; reload(); } @@ -119,10 +114,6 @@ namespace osu.Game.Tournament.Screens { Label = "Current tournament", Description = "Changes the background videos and bracket to match the selected tournament. This requires a restart to apply changes.", - Items = storage.ListTournaments(), - Current = storage.CurrentTournament, - ButtonText = "Close osu!", - Action = () => game.GracefullyExit() }, resolution = new ResolutionSelector { @@ -240,14 +231,44 @@ namespace osu.Game.Tournament.Screens set => dropdown.Current = value; } + private string originalTournament; + + private TournamentStorage storage; + + [Resolved] + private TournamentGameBase game { get; set; } + + [BackgroundDependencyLoader] + private void load(Storage storage) + { + this.storage = (TournamentStorage)storage; + Current = this.storage.CurrentTournament; + originalTournament = this.storage.CurrentTournament.Value; + Items = this.storage.ListTournaments(); + Action = () => game.GracefullyExit(); + ButtonText = "Close osu!"; + } + protected override Drawable CreateComponent() { var drawable = base.CreateComponent(); + FlowContainer.Insert(-1, dropdown = new OsuDropdown { Width = 510 }); + Current.BindValueChanged(v => + { + if (v.NewValue == originalTournament) + { + Button.Hide(); + return; + } + + Button.Show(); + }); + return drawable; } } From bd377237884c071617df95c797650fd1af2774bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 01:47:04 +0900 Subject: [PATCH 5930/6909] Expose as IBindable for added safety --- .../BeatmapListing/Panels/BeatmapPanel.cs | 2 +- .../BeatmapListing/Panels/PlayButton.cs | 21 +++++++++++-------- .../BeatmapSet/Buttons/PreviewButton.cs | 3 ++- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs index 1567e18caa..afb5eeda36 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanel.cs @@ -38,7 +38,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels private Container content; public PreviewTrack Preview => PlayButton.Preview; - public Bindable PreviewPlaying => PlayButton?.Playing; + public IBindable PreviewPlaying => PlayButton?.Playing; protected abstract PlayButton PlayButton { get; } protected abstract Box PreviewBar { get; } diff --git a/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs index eb409785e0..4bbc3569fe 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/PlayButton.cs @@ -18,7 +18,10 @@ namespace osu.Game.Overlays.BeatmapListing.Panels { public class PlayButton : Container { - public readonly BindableBool Playing = new BindableBool(); + public IBindable Playing => playing; + + private readonly BindableBool playing = new BindableBool(); + public PreviewTrack Preview { get; private set; } private BeatmapSetInfo beatmapSet; @@ -36,7 +39,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels Preview?.Expire(); Preview = null; - Playing.Value = false; + playing.Value = false; } } @@ -82,7 +85,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels }, }); - Playing.ValueChanged += playingStateChanged; + playing.ValueChanged += playingStateChanged; } [Resolved] @@ -96,7 +99,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels protected override bool OnClick(ClickEvent e) { - Playing.Toggle(); + playing.Toggle(); return true; } @@ -108,7 +111,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels protected override void OnHoverLost(HoverLostEvent e) { - if (!Playing.Value) + if (!playing.Value) icon.FadeColour(Color4.White, 120, Easing.InOutQuint); base.OnHoverLost(e); } @@ -122,7 +125,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels { if (BeatmapSet == null) { - Playing.Value = false; + playing.Value = false; return; } @@ -144,10 +147,10 @@ namespace osu.Game.Overlays.BeatmapListing.Panels loading = false; // make sure that the update of value of Playing (and the ensuing value change callbacks) // are marshaled back to the update thread. - preview.Stopped += () => Schedule(() => Playing.Value = false); + preview.Stopped += () => Schedule(() => playing.Value = false); // user may have changed their mind. - if (Playing.Value) + if (playing.Value) attemptStart(); }); } @@ -161,7 +164,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels private void attemptStart() { if (Preview?.Start() != true) - Playing.Value = false; + playing.Value = false; } } } diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs index 6accce7d77..56d60a97b2 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs @@ -24,7 +24,8 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons private readonly PlayButton playButton; private PreviewTrack preview => playButton.Preview; - public Bindable Playing => playButton.Playing; + + public IBindable Playing => playButton.Playing; public BeatmapSetInfo BeatmapSet { From d2ca6da0fdae2f14b7430aa0d183e31087f5c3f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 01:56:09 +0900 Subject: [PATCH 5931/6909] Remove unused constant --- osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs index 56d60a97b2..a5e5f664c9 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/PreviewButton.cs @@ -18,8 +18,6 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons { public class PreviewButton : OsuClickableContainer { - private const float transition_duration = 500; - private readonly Box background, progress; private readonly PlayButton playButton; From e99310b59cfddbad40882e187a79668cdd865bb4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 11 Jan 2021 08:02:57 +0300 Subject: [PATCH 5932/6909] Add JsonConstructor attribute --- osu.Game/Online/Rooms/BeatmapAvailability.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index 1fd099fcc7..4796e26d14 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using Newtonsoft.Json; namespace osu.Game.Online.Rooms { @@ -20,6 +21,7 @@ namespace osu.Game.Online.Rooms /// public readonly double? DownloadProgress; + [JsonConstructor] private BeatmapAvailability(DownloadState state, double? downloadProgress = null) { State = state; From a8dfa5e2a9b09e07893c83fb410b0c669e6868a5 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 11 Jan 2021 08:04:00 +0300 Subject: [PATCH 5933/6909] Rename typo'd method --- osu.Game/Online/Rooms/BeatmapAvailability.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index 4796e26d14..794e79ce55 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -28,7 +28,7 @@ namespace osu.Game.Online.Rooms DownloadProgress = downloadProgress; } - public static BeatmapAvailability NotDownload() => new BeatmapAvailability(DownloadState.NotDownloaded); + public static BeatmapAvailability NotDownloaded() => new BeatmapAvailability(DownloadState.NotDownloaded); public static BeatmapAvailability Downloading(double progress) => new BeatmapAvailability(DownloadState.Downloading, progress); public static BeatmapAvailability Downloaded() => new BeatmapAvailability(DownloadState.Downloaded); public static BeatmapAvailability LocallyAvailable() => new BeatmapAvailability(DownloadState.LocallyAvailable); From 2286e3679f75adb10b9ba7f32316db4e116cea71 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 11 Jan 2021 08:21:07 +0300 Subject: [PATCH 5934/6909] Downloaded -> Importing --- osu.Game/Online/Rooms/BeatmapAvailability.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index 794e79ce55..b6b9c632fe 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -30,7 +30,7 @@ namespace osu.Game.Online.Rooms public static BeatmapAvailability NotDownloaded() => new BeatmapAvailability(DownloadState.NotDownloaded); public static BeatmapAvailability Downloading(double progress) => new BeatmapAvailability(DownloadState.Downloading, progress); - public static BeatmapAvailability Downloaded() => new BeatmapAvailability(DownloadState.Downloaded); + public static BeatmapAvailability Importing() => new BeatmapAvailability(DownloadState.Downloaded); public static BeatmapAvailability LocallyAvailable() => new BeatmapAvailability(DownloadState.LocallyAvailable); public bool Equals(BeatmapAvailability other) => other != null && State == other.State && DownloadProgress == other.DownloadProgress; From 49057e8cbcca3921affcefca625365c5d9ccf23c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 14:35:42 +0900 Subject: [PATCH 5935/6909] Cache TournamentStorage explicitly for better safety --- osu.Game.Tournament/TournamentGameBase.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index bc36f27e5b..97c950261b 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -40,6 +40,8 @@ namespace osu.Game.Tournament Resources.AddStore(new DllResourceStore(typeof(TournamentGameBase).Assembly)); dependencies.CacheAs(storage = new TournamentStorage(baseStorage)); + dependencies.CacheAs(storage); + dependencies.Cache(new TournamentVideoResourceStore(storage)); Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage))); From ba3a7a0501aad64940217bd104d4d74f96ed5e1d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 14:35:47 +0900 Subject: [PATCH 5936/6909] Clean up code --- osu.Game.Tournament/Screens/SetupScreen.cs | 40 +++++----------------- 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index 7ae0375b9a..cefad148a0 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -219,33 +219,22 @@ namespace osu.Game.Tournament.Screens { private OsuDropdown dropdown; - public IEnumerable Items - { - get => dropdown.Items; - set => dropdown.Items = value; - } - - public Bindable Current - { - get => dropdown.Current; - set => dropdown.Current = value; - } - - private string originalTournament; - - private TournamentStorage storage; + private string startupTournament; [Resolved] private TournamentGameBase game { get; set; } [BackgroundDependencyLoader] - private void load(Storage storage) + private void load(TournamentStorage storage) { - this.storage = (TournamentStorage)storage; - Current = this.storage.CurrentTournament; - originalTournament = this.storage.CurrentTournament.Value; - Items = this.storage.ListTournaments(); + dropdown.Current = storage.CurrentTournament; + dropdown.Items = storage.ListTournaments(); + dropdown.Current.BindValueChanged(v => Button.FadeTo(v.NewValue == startupTournament ? 0 : 1)); + + startupTournament = storage.CurrentTournament.Value; + Action = () => game.GracefullyExit(); + ButtonText = "Close osu!"; } @@ -258,17 +247,6 @@ namespace osu.Game.Tournament.Screens Width = 510 }); - Current.BindValueChanged(v => - { - if (v.NewValue == originalTournament) - { - Button.Hide(); - return; - } - - Button.Show(); - }); - return drawable; } } From bd627534b76edaf9bb71329eb50597ac3b17f9cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 14:38:51 +0900 Subject: [PATCH 5937/6909] Use disabled state instead of hiding button --- osu.Game.Tournament/Screens/SetupScreen.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/SetupScreen.cs index cefad148a0..c7b299e07f 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/SetupScreen.cs @@ -227,11 +227,11 @@ namespace osu.Game.Tournament.Screens [BackgroundDependencyLoader] private void load(TournamentStorage storage) { + startupTournament = storage.CurrentTournament.Value; + dropdown.Current = storage.CurrentTournament; dropdown.Items = storage.ListTournaments(); - dropdown.Current.BindValueChanged(v => Button.FadeTo(v.NewValue == startupTournament ? 0 : 1)); - - startupTournament = storage.CurrentTournament.Value; + dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true); Action = () => game.GracefullyExit(); From 7a7c583ded7de7e6a354f49d83f131b7471c89f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 14:44:07 +0900 Subject: [PATCH 5938/6909] Move setup screen classes out of single file and into their own namespace --- .../Screens/TestSceneSetupScreen.cs | 2 +- .../TestSceneStablePathSelectScreen.cs | 2 +- .../Screens/Setup/ActionableInfo.cs | 69 +++++++++ .../Screens/Setup/ResolutionSelector.cs | 50 ++++++ .../Screens/{ => Setup}/SetupScreen.cs | 145 +----------------- .../{ => Setup}/StablePathSelectScreen.cs | 4 +- .../Screens/Setup/TournamentSwitcher.cs | 43 ++++++ osu.Game.Tournament/TournamentSceneManager.cs | 1 + 8 files changed, 168 insertions(+), 148 deletions(-) create mode 100644 osu.Game.Tournament/Screens/Setup/ActionableInfo.cs create mode 100644 osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs rename osu.Game.Tournament/Screens/{ => Setup}/SetupScreen.cs (54%) rename osu.Game.Tournament/Screens/{ => Setup}/StablePathSelectScreen.cs (99%) create mode 100644 osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs index 650b4c5412..70b260c84c 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneSetupScreen.cs @@ -2,7 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Tournament.Screens; +using osu.Game.Tournament.Screens.Setup; namespace osu.Game.Tournament.Tests.Screens { diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs index 6e63b2d799..b422227788 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneStablePathSelectScreen.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.Tournament.Screens; +using osu.Game.Tournament.Screens.Setup; namespace osu.Game.Tournament.Tests.Screens { diff --git a/osu.Game.Tournament/Screens/Setup/ActionableInfo.cs b/osu.Game.Tournament/Screens/Setup/ActionableInfo.cs new file mode 100644 index 0000000000..f7d52a294e --- /dev/null +++ b/osu.Game.Tournament/Screens/Setup/ActionableInfo.cs @@ -0,0 +1,69 @@ +using System; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Graphics.UserInterfaceV2; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Tournament.Screens.Setup +{ + internal class ActionableInfo : LabelledDrawable + { + protected OsuButton Button; + + public ActionableInfo() + : base(true) + { + } + + public string ButtonText + { + set => Button.Text = value; + } + + public string Value + { + set => valueText.Text = value; + } + + public bool Failing + { + set => valueText.Colour = value ? Color4.Red : Color4.White; + } + + public Action Action; + + private TournamentSpriteText valueText; + protected FillFlowContainer FlowContainer; + + protected override Drawable CreateComponent() => new Container + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Children = new Drawable[] + { + valueText = new TournamentSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + }, + FlowContainer = new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + Button = new TriangleButton + { + Size = new Vector2(100, 40), + Action = () => Action?.Invoke() + } + } + } + } + }; + } +} diff --git a/osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs b/osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs new file mode 100644 index 0000000000..47472c386c --- /dev/null +++ b/osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs @@ -0,0 +1,50 @@ +using System; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; + +namespace osu.Game.Tournament.Screens.Setup +{ + internal class ResolutionSelector : ActionableInfo + { + private const int minimum_window_height = 480; + private const int maximum_window_height = 2160; + + public new Action Action; + + private OsuNumberBox numberBox; + + protected override Drawable CreateComponent() + { + var drawable = base.CreateComponent(); + FlowContainer.Insert(-1, numberBox = new OsuNumberBox + { + Text = "1080", + Width = 100 + }); + + base.Action = () => + { + if (string.IsNullOrEmpty(numberBox.Text)) + return; + + // 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 = maximum_window_height; + } + else + { + number = Math.Clamp(number, minimum_window_height, maximum_window_height); + } + + // in case number got clamped, reset number in numberBox + numberBox.Text = number.ToString(); + + Action?.Invoke(number); + }; + return drawable; + } + } +} diff --git a/osu.Game.Tournament/Screens/SetupScreen.cs b/osu.Game.Tournament/Screens/Setup/SetupScreen.cs similarity index 54% rename from osu.Game.Tournament/Screens/SetupScreen.cs rename to osu.Game.Tournament/Screens/Setup/SetupScreen.cs index c7b299e07f..5d8f0405ca 100644 --- a/osu.Game.Tournament/Screens/SetupScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/SetupScreen.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.Drawing; using osu.Framework.Allocation; @@ -9,19 +8,16 @@ using osu.Framework.Bindables; using osu.Framework.Configuration; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Platform; using osu.Game.Graphics.UserInterface; using osu.Game.Graphics.UserInterfaceV2; -using osu.Game.Tournament.IO; using osu.Game.Online.API; using osu.Game.Overlays; using osu.Game.Rulesets; using osu.Game.Tournament.IPC; using osu.Game.Tournament.Models; using osuTK; -using osuTK.Graphics; -namespace osu.Game.Tournament.Screens +namespace osu.Game.Tournament.Screens.Setup { public class SetupScreen : TournamentScreen, IProvideVideo { @@ -155,144 +151,5 @@ namespace osu.Game.Tournament.Screens Width = 0.5f, }; } - - private class ActionableInfo : LabelledDrawable - { - protected OsuButton Button; - - public ActionableInfo() - : base(true) - { - } - - public string ButtonText - { - set => Button.Text = value; - } - - public string Value - { - set => valueText.Text = value; - } - - public bool Failing - { - set => valueText.Colour = value ? Color4.Red : Color4.White; - } - - public Action Action; - - private TournamentSpriteText valueText; - protected FillFlowContainer FlowContainer; - - protected override Drawable CreateComponent() => new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Children = new Drawable[] - { - valueText = new TournamentSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - }, - FlowContainer = new FillFlowContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(10, 0), - Children = new Drawable[] - { - Button = new TriangleButton - { - Size = new Vector2(100, 40), - Action = () => Action?.Invoke() - } - } - } - } - }; - } - - private class TournamentSwitcher : ActionableInfo - { - private OsuDropdown dropdown; - - private string startupTournament; - - [Resolved] - private TournamentGameBase game { get; set; } - - [BackgroundDependencyLoader] - private void load(TournamentStorage storage) - { - startupTournament = storage.CurrentTournament.Value; - - dropdown.Current = storage.CurrentTournament; - dropdown.Items = storage.ListTournaments(); - dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true); - - Action = () => game.GracefullyExit(); - - ButtonText = "Close osu!"; - } - - protected override Drawable CreateComponent() - { - var drawable = base.CreateComponent(); - - FlowContainer.Insert(-1, dropdown = new OsuDropdown - { - Width = 510 - }); - - return drawable; - } - } - - private class ResolutionSelector : ActionableInfo - { - private const int minimum_window_height = 480; - private const int maximum_window_height = 2160; - - public new Action Action; - - private OsuNumberBox numberBox; - - protected override Drawable CreateComponent() - { - var drawable = base.CreateComponent(); - FlowContainer.Insert(-1, numberBox = new OsuNumberBox - { - Text = "1080", - Width = 100 - }); - - base.Action = () => - { - if (string.IsNullOrEmpty(numberBox.Text)) - return; - - // 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 = maximum_window_height; - } - else - { - number = Math.Clamp(number, minimum_window_height, maximum_window_height); - } - - // in case number got clamped, reset number in numberBox - numberBox.Text = number.ToString(); - - Action?.Invoke(number); - }; - return drawable; - } - } } } diff --git a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs similarity index 99% rename from osu.Game.Tournament/Screens/StablePathSelectScreen.cs rename to osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs index 717b43f704..03f79b644f 100644 --- a/osu.Game.Tournament/Screens/StablePathSelectScreen.cs +++ b/osu.Game.Tournament/Screens/Setup/StablePathSelectScreen.cs @@ -13,11 +13,11 @@ 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 osu.Game.Tournament.IPC; using osuTK; -namespace osu.Game.Tournament.Screens +namespace osu.Game.Tournament.Screens.Setup { public class StablePathSelectScreen : TournamentScreen { diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs new file mode 100644 index 0000000000..a993a0594a --- /dev/null +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -0,0 +1,43 @@ +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Game.Graphics.UserInterface; +using osu.Game.Tournament.IO; + +namespace osu.Game.Tournament.Screens.Setup +{ + internal class TournamentSwitcher : ActionableInfo + { + private OsuDropdown dropdown; + + private string startupTournament; + + [Resolved] + private TournamentGameBase game { get; set; } + + [BackgroundDependencyLoader] + private void load(TournamentStorage storage) + { + startupTournament = storage.CurrentTournament.Value; + + dropdown.Current = storage.CurrentTournament; + dropdown.Items = storage.ListTournaments(); + dropdown.Current.BindValueChanged(v => Button.Enabled.Value = v.NewValue != startupTournament, true); + + Action = () => game.GracefullyExit(); + + ButtonText = "Close osu!"; + } + + protected override Drawable CreateComponent() + { + var drawable = base.CreateComponent(); + + FlowContainer.Insert(-1, dropdown = new OsuDropdown + { + Width = 510 + }); + + return drawable; + } + } +} \ No newline at end of file diff --git a/osu.Game.Tournament/TournamentSceneManager.cs b/osu.Game.Tournament/TournamentSceneManager.cs index 870ea466cc..ced1a8ec72 100644 --- a/osu.Game.Tournament/TournamentSceneManager.cs +++ b/osu.Game.Tournament/TournamentSceneManager.cs @@ -19,6 +19,7 @@ using osu.Game.Tournament.Screens.Gameplay; using osu.Game.Tournament.Screens.Ladder; using osu.Game.Tournament.Screens.MapPool; using osu.Game.Tournament.Screens.Schedule; +using osu.Game.Tournament.Screens.Setup; using osu.Game.Tournament.Screens.Showcase; using osu.Game.Tournament.Screens.TeamIntro; using osu.Game.Tournament.Screens.TeamWin; From c9466426b743471e36e4126f3637d44c00fc819c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 14:45:01 +0900 Subject: [PATCH 5939/6909] Change field to local variable --- osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index a993a0594a..e38a374e1c 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -9,15 +9,13 @@ namespace osu.Game.Tournament.Screens.Setup { private OsuDropdown dropdown; - private string startupTournament; - [Resolved] private TournamentGameBase game { get; set; } [BackgroundDependencyLoader] private void load(TournamentStorage storage) { - startupTournament = storage.CurrentTournament.Value; + string startupTournament = storage.CurrentTournament.Value; dropdown.Current = storage.CurrentTournament; dropdown.Items = storage.ListTournaments(); @@ -40,4 +38,4 @@ namespace osu.Game.Tournament.Screens.Setup return drawable; } } -} \ No newline at end of file +} From f65042cf4452997589ba678b8c4670a16eafdd65 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 Jan 2021 15:47:27 +0900 Subject: [PATCH 5940/6909] Add missing licence headers --- osu.Game.Tournament/Screens/Setup/ActionableInfo.cs | 3 +++ osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs | 3 +++ osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs | 3 +++ 3 files changed, 9 insertions(+) diff --git a/osu.Game.Tournament/Screens/Setup/ActionableInfo.cs b/osu.Game.Tournament/Screens/Setup/ActionableInfo.cs index f7d52a294e..cfdf9c99ae 100644 --- a/osu.Game.Tournament/Screens/Setup/ActionableInfo.cs +++ b/osu.Game.Tournament/Screens/Setup/ActionableInfo.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.Graphics.Containers; diff --git a/osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs b/osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs index 47472c386c..4b518ea7c7 100644 --- a/osu.Game.Tournament/Screens/Setup/ResolutionSelector.cs +++ b/osu.Game.Tournament/Screens/Setup/ResolutionSelector.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.Game.Graphics.UserInterface; diff --git a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs index e38a374e1c..74c872646c 100644 --- a/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs +++ b/osu.Game.Tournament/Screens/Setup/TournamentSwitcher.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Graphics.UserInterface; From 90fb67b377e2022f3bfa60dfca4f31a7d3670228 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 11 Jan 2021 20:52:02 +0300 Subject: [PATCH 5941/6909] Update code in-line with decided direction --- osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs | 1 - osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 7fda526faf..f324a1a216 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -43,7 +43,6 @@ namespace osu.Game.Online.Multiplayer /// /// Change the user's local availability state of the beatmap set in joined room. - /// This will also force user state back to . /// /// The proposed new beatmap availability state. Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index c155447f8c..7fbc770351 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -83,7 +83,6 @@ namespace osu.Game.Tests.Visual.Multiplayer Debug.Assert(Room != null); ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged(userId, newBeatmapAvailability); - ChangeUserState(userId, MultiplayerUserState.Idle); } protected override Task JoinRoom(long roomId) From 0d5fbb15ac06c321a0f43d9a2e6ff5d9a61e8cc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 11 Jan 2021 20:28:24 +0100 Subject: [PATCH 5942/6909] Fix up code comments Default value restated in xmldoc was snipped because it's made redundant by the initialiser and possibly bound to be outdated at some point. --- osu.Game/Online/Multiplayer/IMultiplayerClient.cs | 2 +- osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs | 2 +- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 2 +- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 5410fbc030..19dd473230 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -49,7 +49,7 @@ namespace osu.Game.Online.Multiplayer Task UserStateChanged(int userId, MultiplayerUserState state); /// - /// Signals that a user in this room has their beatmap availability state changed. + /// Signals that a user in this room changed their beatmap availability state. /// /// The ID of the user whose beatmap availability state has changed. /// The new beatmap availability state of the user. diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index f324a1a216..09816974a7 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -42,7 +42,7 @@ namespace osu.Game.Online.Multiplayer Task ChangeState(MultiplayerUserState newState); /// - /// Change the user's local availability state of the beatmap set in joined room. + /// Change the local user's availability state of the current beatmap set in joined room. /// /// The proposed new beatmap availability state. Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index f515b574df..2590acbc81 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -18,7 +18,7 @@ namespace osu.Game.Online.Multiplayer public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle; /// - /// The availability state of the beatmap, set to by default. + /// The availability state of the current beatmap. /// public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable(); diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 799b66020c..99aa8fe015 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -322,7 +322,7 @@ namespace osu.Game.Online.Multiplayer { var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); - // we don't care whether the room doesn't exist or user isn't in joined room, just return in that point. + // errors here are not critical - beatmap availability state is mostly for display. if (user == null) return; From 249be461d511b10f541ef331b3b1aebe666d74be Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Jan 2021 11:09:55 +0300 Subject: [PATCH 5943/6909] Add "explicit maps" search filter control --- .../Online/API/Requests/SearchBeatmapSetsRequest.cs | 8 +++++++- .../BeatmapListing/BeatmapListingFilterControl.cs | 4 +++- .../BeatmapListing/BeatmapListingSearchControl.cs | 6 +++++- osu.Game/Overlays/BeatmapListing/SearchExplicit.cs | 11 +++++++++++ 4 files changed, 26 insertions(+), 3 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapListing/SearchExplicit.cs diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index bbaa7e745f..939d3c6cb4 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -30,6 +30,8 @@ namespace osu.Game.Online.API.Requests public SearchPlayed Played { get; } + public SearchExplicit Explicit { get; } + [CanBeNull] public IReadOnlyCollection Ranks { get; } @@ -50,7 +52,8 @@ namespace osu.Game.Online.API.Requests SearchLanguage language = SearchLanguage.Any, IReadOnlyCollection extra = null, IReadOnlyCollection ranks = null, - SearchPlayed played = SearchPlayed.Any) + SearchPlayed played = SearchPlayed.Any, + SearchExplicit explicitMaps = SearchExplicit.Hide) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; @@ -64,6 +67,7 @@ namespace osu.Game.Online.API.Requests Extra = extra; Ranks = ranks; Played = played; + Explicit = explicitMaps; } protected override WebRequest CreateWebRequest() @@ -93,6 +97,8 @@ namespace osu.Game.Online.API.Requests if (Played != SearchPlayed.Any) req.AddParameter("played", Played.ToString().ToLowerInvariant()); + req.AddParameter("nsfw", Explicit == SearchExplicit.Show ? "true" : "false"); + req.AddCursor(cursor); return req; diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index d991dcfcfb..650adcb4a9 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -141,6 +141,7 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Extra.CollectionChanged += (_, __) => queueUpdateSearch(); searchControl.Ranks.CollectionChanged += (_, __) => queueUpdateSearch(); searchControl.Played.BindValueChanged(_ => queueUpdateSearch()); + searchControl.Explicit.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch()); @@ -193,7 +194,8 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Language.Value, searchControl.Extra, searchControl.Ranks, - searchControl.Played.Value); + searchControl.Played.Value, + searchControl.Explicit.Value); getSetsRequest.Success += response => { diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index e232bf045f..c14d693f7d 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -42,6 +42,8 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable Played => playedFilter.Current; + public Bindable Explicit => explicitFilter.Current; + public BeatmapSetInfo BeatmapSet { set @@ -65,6 +67,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchMultipleSelectionFilterRow extraFilter; private readonly BeatmapSearchScoreFilterRow ranksFilter; private readonly BeatmapSearchFilterRow playedFilter; + private readonly BeatmapSearchFilterRow explicitFilter; private readonly Box background; private readonly UpdateableBeatmapSetCover beatmapCover; @@ -125,7 +128,8 @@ namespace osu.Game.Overlays.BeatmapListing languageFilter = new BeatmapSearchFilterRow(@"Language"), extraFilter = new BeatmapSearchMultipleSelectionFilterRow(@"Extra"), ranksFilter = new BeatmapSearchScoreFilterRow(), - playedFilter = new BeatmapSearchFilterRow(@"Played") + playedFilter = new BeatmapSearchFilterRow(@"Played"), + explicitFilter = new BeatmapSearchFilterRow(@"Explicit Maps"), } } } diff --git a/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs b/osu.Game/Overlays/BeatmapListing/SearchExplicit.cs new file mode 100644 index 0000000000..3e57cdd48c --- /dev/null +++ b/osu.Game/Overlays/BeatmapListing/SearchExplicit.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.BeatmapListing +{ + public enum SearchExplicit + { + Hide, + Show + } +} From 24c18397397648894f0b943ec98e2f63735c71cf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Jan 2021 11:10:25 +0300 Subject: [PATCH 5944/6909] Add global web setting for allowing explicit content --- osu.Game/Configuration/OsuConfigManager.cs | 3 +++ .../BeatmapListing/BeatmapListingSearchControl.cs | 12 +++++++++++- .../Overlays/Settings/Sections/Online/WebSettings.cs | 6 ++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index eb34a0885d..5e7a843baf 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -60,6 +60,8 @@ namespace osu.Game.Configuration Set(OsuSetting.ExternalLinkWarning, true); Set(OsuSetting.PreferNoVideo, false); + Set(OsuSetting.AllowExplicitContent, false); + // Audio Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); @@ -270,5 +272,6 @@ namespace osu.Game.Configuration EditorWaveformOpacity, DiscordRichPresence, AutomaticallyDownloadWhenSpectating, + AllowExplicitContent, } } diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index c14d693f7d..3761aee312 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -11,6 +11,7 @@ using osu.Framework.Bindables; using osu.Framework.Input.Events; using osu.Game.Beatmaps.Drawables; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics.Containers; using osu.Game.Graphics.UserInterface; using osuTK.Graphics; @@ -140,10 +141,19 @@ namespace osu.Game.Overlays.BeatmapListing categoryFilter.Current.Value = SearchCategory.Leaderboard; } + private IBindable allowExplicitContent; + [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + private void load(OverlayColourProvider colourProvider, OsuConfigManager config) { background.Colour = colourProvider.Dark6; + + allowExplicitContent = config.GetBindable(OsuSetting.AllowExplicitContent); + allowExplicitContent.BindValueChanged(allow => + { + // Update search control if global "explicit allowed" setting changed. + Explicit.Value = allow.NewValue ? SearchExplicit.Show : SearchExplicit.Hide; + }, true); } public void TakeFocus() => textBox.TakeFocus(); diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index 8134c350a6..da7ef46f65 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -33,6 +33,12 @@ namespace osu.Game.Overlays.Settings.Sections.Online Keywords = new[] { "spectator" }, Current = config.GetBindable(OsuSetting.AutomaticallyDownloadWhenSpectating), }, + new SettingsCheckbox + { + LabelText = "Hide warnings for explicit content in beatmaps", + Keywords = new[] { "nsfw", "18+", "offensive" }, + Current = config.GetBindable(OsuSetting.AllowExplicitContent), + } }; } } From 80fa2cf69330ff805c1de0f4464682a8f79b5156 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Jan 2021 11:14:05 +0300 Subject: [PATCH 5945/6909] Add test coverage --- .../TestSceneBeatmapListingSearchControl.cs | 71 +++++++++++++------ 1 file changed, 48 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index 3f757031f8..cb86047dea 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osu.Game.Overlays.BeatmapListing; @@ -19,9 +20,18 @@ namespace osu.Game.Tests.Visual.UserInterface [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private readonly BeatmapListingSearchControl control; + private BeatmapListingSearchControl control; - public TestSceneBeatmapListingSearchControl() + private OsuConfigManager localConfig; + + [BackgroundDependencyLoader] + private void load() + { + Dependencies.Cache(localConfig = new OsuConfigManager(LocalStorage)); + } + + [SetUp] + public void SetUp() => Schedule(() => { OsuSpriteText query; OsuSpriteText ruleset; @@ -31,30 +41,34 @@ namespace osu.Game.Tests.Visual.UserInterface OsuSpriteText extra; OsuSpriteText ranks; OsuSpriteText played; + OsuSpriteText explicitMap; - Add(control = new BeatmapListingSearchControl + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - }); - - Add(new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 5), - Children = new Drawable[] + control = new BeatmapListingSearchControl { - query = new OsuSpriteText(), - ruleset = new OsuSpriteText(), - category = new OsuSpriteText(), - genre = new OsuSpriteText(), - language = new OsuSpriteText(), - extra = new OsuSpriteText(), - ranks = new OsuSpriteText(), - played = new OsuSpriteText() + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 5), + Children = new Drawable[] + { + query = new OsuSpriteText(), + ruleset = new OsuSpriteText(), + category = new OsuSpriteText(), + genre = new OsuSpriteText(), + language = new OsuSpriteText(), + extra = new OsuSpriteText(), + ranks = new OsuSpriteText(), + played = new OsuSpriteText(), + explicitMap = new OsuSpriteText(), + } } - }); + }; control.Query.BindValueChanged(q => query.Text = $"Query: {q.NewValue}", true); control.Ruleset.BindValueChanged(r => ruleset.Text = $"Ruleset: {r.NewValue}", true); @@ -64,7 +78,8 @@ namespace osu.Game.Tests.Visual.UserInterface control.Extra.BindCollectionChanged((u, v) => extra.Text = $"Extra: {(control.Extra.Any() ? string.Join('.', control.Extra.Select(i => i.ToString().ToLowerInvariant())) : "")}", true); control.Ranks.BindCollectionChanged((u, v) => ranks.Text = $"Ranks: {(control.Ranks.Any() ? string.Join('.', control.Ranks.Select(i => i.ToString())) : "")}", true); control.Played.BindValueChanged(p => played.Text = $"Played: {p.NewValue}", true); - } + control.Explicit.BindValueChanged(e => explicitMap.Text = $"Explicit Maps: {e.NewValue}", true); + }); [Test] public void TestCovers() @@ -74,6 +89,16 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("Set null beatmap", () => control.BeatmapSet = null); } + [Test] + public void TestExplicitConfig() + { + AddStep("configure explicit content to allowed", () => localConfig.Set(OsuSetting.AllowExplicitContent, true)); + AddAssert("explicit control set to show", () => control.Explicit.Value == SearchExplicit.Show); + + AddStep("configure explicit content to disallowed", () => localConfig.Set(OsuSetting.AllowExplicitContent, false)); + AddAssert("explicit control set to hide", () => control.Explicit.Value == SearchExplicit.Hide); + } + private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo { OnlineInfo = new BeatmapSetOnlineInfo From 422260797b68cc6e3710d327c4e3accc66d4797c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 16:03:12 +0900 Subject: [PATCH 5946/6909] Revert polling changes to fix participant list display It turns out this polling was necessary to get extra data that isn't included in the main listing request. It was removed deemed useless, and in order to fix the order of rooms changing when selecting a room. Weirdly, I can't reproduce this happening any more, and on close inspection of the code can't see how it could happen in the first place. For now, let's revert this change and iterate from there, if/when the same issue arises again. I've discussed avoiding this second poll by potentially including more data (just `user_id`s?) in the main listing request, but not 100% sure on this - even if the returned data is minimal it's an extra join server-side, which could cause performance issues for large numbers of rooms. --- .../TestSceneMultiplayerRoomManager.cs | 1 + .../OnlinePlay/Multiplayer/Multiplayer.cs | 5 +++- .../Multiplayer/MultiplayerRoomManager.cs | 28 ++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index 80d1acd145..7a3845cbf3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -143,6 +143,7 @@ namespace osu.Game.Tests.Visual.Multiplayer RoomManager = { TimeBetweenListingPolls = { Value = 1 }, + TimeBetweenSelectionPolls = { Value = 1 } } }; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 310617a0bc..76f5c74433 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -33,6 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen()) { multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; } else { @@ -40,16 +41,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { case LoungeSubScreen _: multiplayerRoomManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; break; // Don't poll inside the match or anywhere else. default: multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; break; } } - Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})"); } protected override Room CreateNewRoom() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index 5c327266a3..3cb263298f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private StatefulMultiplayerClient multiplayerClient { get; set; } public readonly Bindable TimeBetweenListingPolls = new Bindable(); - + public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); private readonly IBindable isConnected = new Bindable(); private readonly Bindable allowPolling = new Bindable(); @@ -119,6 +119,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, AllowPolling = { BindTarget = allowPolling } }, + new MultiplayerSelectionPollingComponent + { + TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls }, + AllowPolling = { BindTarget = allowPolling } + } }; private class MultiplayerListingPollingComponent : ListingPollingComponent @@ -141,5 +146,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); } + + private class MultiplayerSelectionPollingComponent : SelectionPollingComponent + { + public readonly IBindable AllowPolling = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + AllowPolling.BindValueChanged(allowPolling => + { + if (!allowPolling.NewValue) + return; + + if (IsLoaded) + PollImmediately(); + }); + } + + protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); + } } } From 22a0f99f35496d22b5b610626b057c36d1bea82d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 12 Jan 2021 17:49:18 +0900 Subject: [PATCH 5947/6909] Add failing test --- .../TaikoBeatmapConversionTest.cs | 1 + ...rating-drumroll-2-expected-conversion.json | 18 ++++++++++++++++++ .../Beatmaps/slider-generating-drumroll-2.osu | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-2-expected-conversion.json create mode 100644 osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-2.osu diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs index 3d77fb05db..b6db333dc9 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoBeatmapConversionTest.cs @@ -23,6 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Tests [TestCase("sample-to-type-conversions")] [TestCase("slider-conversion-v6")] [TestCase("slider-conversion-v14")] + [TestCase("slider-generating-drumroll-2")] public void Test(string name) => base.Test(name); protected override IEnumerable CreateConvertValue(HitObject hitObject) diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-2-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-2-expected-conversion.json new file mode 100644 index 0000000000..b4ee98c86a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-2-expected-conversion.json @@ -0,0 +1,18 @@ +{ + "Mappings": [ + { + "StartTime": 51532, + "Objects": [ + { + "StartTime": 51532, + "EndTime": 52301, + "IsRim": false, + "IsCentre": false, + "IsDrumRoll": true, + "IsSwell": false, + "IsStrong": false + } + ] + } + ] +} \ No newline at end of file diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-2.osu b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-2.osu new file mode 100644 index 0000000000..d81b09ee26 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-generating-drumroll-2.osu @@ -0,0 +1,19 @@ +osu file format v14 + +[General] +Mode: 0 + +[Difficulty] +HPDrainRate:2 +CircleSize:3.2 +OverallDifficulty:2 +ApproachRate:3 +SliderMultiplier:0.999999999999999 +SliderTickRate:1 + +[TimingPoints] +763,384.615384615385,4,2,0,70,1,0 +49993,-90.9090909090909,4,2,0,75,0,1 + +[HitObjects] +51,245,51532,2,0,P|18:150|17:122,2,110.000003356934,0|8|0,0:0|0:0|0:0,0:0:0:0: From 9a22df2b88029de3f22b51938ad6b163322d95ec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 12 Jan 2021 17:50:22 +0900 Subject: [PATCH 5948/6909] Fix BPM multiplier not working in all cases --- .../Beatmaps/TaikoBeatmapConverter.cs | 6 +- ...er-conversion-v14-expected-conversion.json | 307 +----------------- osu.Game/Beatmaps/Formats/LegacyDecoder.cs | 5 +- 3 files changed, 18 insertions(+), 300 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs index 1214c594aa..b51f096d7d 100644 --- a/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs +++ b/osu.Game.Rulesets.Taiko/Beatmaps/TaikoBeatmapConverter.cs @@ -160,7 +160,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps } } - private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out double taikoDuration, out double tickSpacing) + private bool shouldConvertSliderToHits(HitObject obj, IBeatmap beatmap, IHasDistance distanceData, out int taikoDuration, out double tickSpacing) { // DO NOT CHANGE OR REFACTOR ANYTHING IN HERE WITHOUT TESTING AGAINST _ALL_ BEATMAPS. // Some of these calculations look redundant, but they are not - extremely small floating point errors are introduced to maintain 1:1 compatibility with stable. @@ -185,7 +185,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps // The velocity and duration of the taiko hit object - calculated as the velocity of a drum roll. double taikoVelocity = sliderScoringPointDistance * beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate; - taikoDuration = distance / taikoVelocity * beatLength; + taikoDuration = (int)(distance / taikoVelocity * beatLength); if (isForCurrentRuleset) { @@ -200,7 +200,7 @@ namespace osu.Game.Rulesets.Taiko.Beatmaps beatLength = timingPoint.BeatLength; // If the drum roll is to be split into hit circles, assume the ticks are 1/8 spaced within the duration of one beat - tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, taikoDuration / spans); + tickSpacing = Math.Min(beatLength / beatmap.BeatmapInfo.BaseDifficulty.SliderTickRate, (double)taikoDuration / spans); return tickSpacing > 0 && distance / osuVelocity * 1000 < 2 * beatLength; diff --git a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json index 6a6063cb74..b7ad128cab 100644 --- a/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json +++ b/osu.Game.Rulesets.Taiko/Resources/Testing/Beatmaps/slider-conversion-v14-expected-conversion.json @@ -1,7 +1,9 @@ { - "Mappings": [{ + "Mappings": [ + { "StartTime": 2000, - "Objects": [{ + "Objects": [ + { "StartTime": 2000, "EndTime": 2000, "IsRim": false, @@ -23,7 +25,8 @@ }, { "StartTime": 4000, - "Objects": [{ + "Objects": [ + { "StartTime": 4000, "EndTime": 4000, "IsRim": false, @@ -45,7 +48,8 @@ }, { "StartTime": 6000, - "Objects": [{ + "Objects": [ + { "StartTime": 6000, "EndTime": 6000, "IsRim": true, @@ -76,300 +80,13 @@ }, { "StartTime": 8000, - "Objects": [{ + "Objects": [ + { "StartTime": 8000, - "EndTime": 8000, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8026, - "EndTime": 8026, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8053, - "EndTime": 8053, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8080, - "EndTime": 8080, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8107, - "EndTime": 8107, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8133, - "EndTime": 8133, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8160, - "EndTime": 8160, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8187, - "EndTime": 8187, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8214, - "EndTime": 8214, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8241, - "EndTime": 8241, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8267, - "EndTime": 8267, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8294, - "EndTime": 8294, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8321, - "EndTime": 8321, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8348, - "EndTime": 8348, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8374, - "EndTime": 8374, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8401, - "EndTime": 8401, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8428, - "EndTime": 8428, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8455, - "EndTime": 8455, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8482, - "EndTime": 8482, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8508, - "EndTime": 8508, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8535, - "EndTime": 8535, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8562, - "EndTime": 8562, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8589, - "EndTime": 8589, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8615, - "EndTime": 8615, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8642, - "EndTime": 8642, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8669, - "EndTime": 8669, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8696, - "EndTime": 8696, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8723, - "EndTime": 8723, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8749, - "EndTime": 8749, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8776, - "EndTime": 8776, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8803, - "EndTime": 8803, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8830, - "EndTime": 8830, - "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, - "IsSwell": false, - "IsStrong": false - }, - { - "StartTime": 8857, "EndTime": 8857, "IsRim": false, - "IsCentre": true, - "IsDrumRoll": false, + "IsCentre": false, + "IsDrumRoll": true, "IsSwell": false, "IsStrong": false } diff --git a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs index 069a25b83d..2fb24c24e0 100644 --- a/osu.Game/Beatmaps/Formats/LegacyDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyDecoder.cs @@ -164,12 +164,13 @@ namespace osu.Game.Beatmaps.Formats /// Legacy BPM multiplier that introduces floating-point errors for rulesets that depend on it. /// DO NOT USE THIS UNLESS 100% SURE. /// - public float BpmMultiplier { get; private set; } + public double BpmMultiplier { get; private set; } public LegacyDifficultyControlPoint(double beatLength) : this() { - BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100f : 1; + // Note: In stable, the division occurs on floats, but with compiler optimisations turned on actually seems to occur on doubles via some .NET black magic (possibly inlining?). + BpmMultiplier = beatLength < 0 ? Math.Clamp((float)-beatLength, 10, 10000) / 100.0 : 1; } public LegacyDifficultyControlPoint() From b51b07c3a90c0dc1635984f9346175746c5e3da7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 18:05:29 +0900 Subject: [PATCH 5949/6909] Fix unstable multiplayer room ordering when selection is made --- .../OnlinePlay/Components/SelectionPollingComponent.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index 0eec155060..dcf3c94b76 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.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 System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -47,9 +48,11 @@ namespace osu.Game.Screens.OnlinePlay.Components pollReq.Success += result => { - var rooms = new List(roomManager.Rooms); + // existing rooms need to be ordered by their position because the received of NotifyRoomsReceives expects to be able to sort them based on this order. + var rooms = new List(roomManager.Rooms.OrderBy(r => r.Position.Value)); int index = rooms.FindIndex(r => r.RoomID.Value == result.RoomID.Value); + if (index < 0) return; From 7298adc9d9b8ef878f4be5b8799be222aa0dd90b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 19:04:16 +0900 Subject: [PATCH 5950/6909] Fix non-threadsafe usage of MultiplayerClient.IsConnected --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 1 + .../OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs | 4 ++-- .../Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 4 ++-- .../OnlinePlay/Multiplayer/MultiplayerRoomManager.cs | 6 ++---- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 34cba09e8c..770039d79d 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -52,6 +52,7 @@ namespace osu.Game.Online.Multiplayer /// /// Whether the is currently connected. + /// This is NOT thread safe and usage should be scheduled. /// public abstract IBindable IsConnected { get; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs index 87b0e49b5b..a13d2cf540 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/CreateMultiplayerMatchButton.cs @@ -34,8 +34,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - isConnected.BindValueChanged(_ => updateState()); - operationInProgress.BindValueChanged(_ => updateState(), true); + isConnected.BindValueChanged(_ => Scheduler.AddOnce(updateState)); + operationInProgress.BindValueChanged(_ => Scheduler.AddOnce(updateState), true); } private void updateState() => Enabled.Value = isConnected.Value && !operationInProgress.Value; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 4bee502e2e..04d9e0a72a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -77,14 +77,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }); isConnected = client.IsConnected.GetBoundCopy(); - isConnected.BindValueChanged(connected => + isConnected.BindValueChanged(connected => Schedule(() => { if (!connected.NewValue) { // messaging to the user about this disconnect will be provided by the MultiplayerMatchSubScreen. failAndBail(); } - }, true); + }), true); Debug.Assert(client.Room != null); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index 5c327266a3..bcae2e5cbb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -34,10 +34,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); isConnected.BindTo(multiplayerClient.IsConnected); - isConnected.BindValueChanged(_ => Schedule(updatePolling)); - JoinedRoom.BindValueChanged(_ => updatePolling()); - - updatePolling(); + isConnected.BindValueChanged(_ => Scheduler.AddOnce(updatePolling)); + JoinedRoom.BindValueChanged(_ => Scheduler.AddOnce(updatePolling), true); } public override void CreateRoom(Room room, Action onSuccess = null, Action onError = null) From 2d3cacca11bc1d8de6c78cf8b6f1154f1e058050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Jan 2021 00:58:53 +0100 Subject: [PATCH 5951/6909] Fix non-hosts crashing on load requested `onLoadRequested()` always released the `readyClickOperation` ongoing operation, without checking whether it actually needs to/should (it should only do so if the action initiating the operation was starting the game by the host). This would crash all other consumers, who already released the operation when their ready-up operation completed server side. To resolve, relax the constraint such that the operation can be ended multiple times in any order. At the end of the day the thing that matters is that the operation is done and the ready button is unblocked. --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index e539b315e4..80991569dc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -241,8 +241,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer void endOperation() { - Debug.Assert(readyClickOperation != null); - readyClickOperation.Dispose(); + readyClickOperation?.Dispose(); readyClickOperation = null; } } @@ -255,9 +254,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer StartPlay(() => new MultiplayerPlayer(SelectedItem.Value, userIds)); - Debug.Assert(readyClickOperation != null); - - readyClickOperation.Dispose(); + readyClickOperation?.Dispose(); readyClickOperation = null; } From 5f10bcce02411a22f783ba3a5bbb9d7ab66f43af Mon Sep 17 00:00:00 2001 From: Mysfit Date: Wed, 13 Jan 2021 00:09:22 -0500 Subject: [PATCH 5952/6909] Added beatmap colour settings checkbox and associated tests. --- .../TestSceneLegacyBeatmapSkin.cs | 78 +++++++++++++++++-- .../TestSceneSkinFallbacks.cs | 25 ++++++ osu.Game/Configuration/OsuConfigManager.cs | 2 + .../Overlays/Settings/Sections/SkinSection.cs | 5 ++ .../Play/PlayerSettings/VisualSettings.cs | 3 + .../Skinning/BeatmapSkinProvidingContainer.cs | 7 +- 6 files changed, 110 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index a768626005..83b1e28476 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -7,9 +7,12 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Play; using osu.Game.Skinning; @@ -24,34 +27,81 @@ namespace osu.Game.Rulesets.Osu.Tests [Resolved] private AudioManager audio { get; set; } - [TestCase(true)] - [TestCase(false)] - public void TestBeatmapComboColours(bool customSkinColoursPresent) + private readonly Bindable beatmapSkins = new Bindable(); + private readonly Bindable beatmapColours = new Bindable(); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); + config.BindWith(OsuSetting.BeatmapColours, beatmapColours); + } + + [TestCase(true, true, true)] + [TestCase(true, false, true)] + [TestCase(false, true, true)] + [TestCase(false, false, true)] + public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin, bool useBeatmapColour) { ExposedPlayer player = null; - AddStep("load coloured beatmap", () => player = loadBeatmap(customSkinColoursPresent, true)); + configureSettings(useBeatmapSkin, useBeatmapColour); + AddStep("load coloured beatmap", () => player = loadBeatmap(userHasCustomColours, true)); AddUntilStep("wait for player", () => player.IsLoaded); AddAssert("is beatmap skin colours", () => player.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours)); } - [Test] - public void TestBeatmapNoComboColours() + [TestCase(true, false)] + [TestCase(false, false)] + public void TestBeatmapComboColoursOverride(bool useBeatmapSkin, bool useBeatmapColour) { ExposedPlayer player = null; + configureSettings(useBeatmapSkin, useBeatmapColour); + AddStep("load coloured beatmap", () => player = loadBeatmap(true, true)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is user custom skin colours", () => player.UsableComboColours.SequenceEqual(TestSkin.Colours)); + } + + [TestCase(true, false)] + [TestCase(false, false)] + public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin, bool useBeatmapColour) + { + ExposedPlayer player = null; + + configureSettings(useBeatmapSkin, useBeatmapColour); + AddStep("load coloured beatmap", () => player = loadBeatmap(false, true)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is default user skin colours", () => player.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); + } + + [TestCase(true, true)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(false, false)] + public void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour) + { + ExposedPlayer player = null; + + configureSettings(useBeatmapSkin, useBeatmapColour); AddStep("load no-colour beatmap", () => player = loadBeatmap(false, false)); AddUntilStep("wait for player", () => player.IsLoaded); AddAssert("is default user skin colours", () => player.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); } - [Test] - public void TestBeatmapNoComboColoursSkinOverride() + [TestCase(true, true)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(false, false)] + public void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour) { ExposedPlayer player = null; + configureSettings(useBeatmapSkin, useBeatmapColour); AddStep("load custom-skin colour", () => player = loadBeatmap(true, false)); AddUntilStep("wait for player", () => player.IsLoaded); @@ -69,6 +119,18 @@ namespace osu.Game.Rulesets.Osu.Tests return player; } + private void configureSettings(bool beatmapSkins, bool beatmapColours) + { + AddStep($"{(beatmapSkins ? "enable" : "disable")} beatmap skins", () => + { + this.beatmapSkins.Value = beatmapSkins; + }); + AddStep($"{(beatmapColours ? "enable" : "disable")} beatmap colours", () => + { + this.beatmapColours.Value = beatmapColours; + }); + } + private class ExposedPlayer : Player { private readonly bool userHasCustomColours; diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs index 856bfd7e80..10baca438d 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSkinFallbacks.cs @@ -52,6 +52,31 @@ namespace osu.Game.Rulesets.Osu.Tests checkNextHitObject(null); } + [Test] + public void TestBeatmapColourDefault() + { + AddStep("enable user provider", () => testUserSkin.Enabled = true); + + AddStep("enable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, true)); + AddStep("enable beatmap colours", () => LocalConfig.Set(OsuSetting.BeatmapColours, true)); + checkNextHitObject("beatmap"); + + AddStep("enable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, true)); + AddStep("disable beatmap colours", () => LocalConfig.Set(OsuSetting.BeatmapColours, false)); + checkNextHitObject("beatmap"); + + AddStep("disable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, false)); + AddStep("enable beatmap colours", () => LocalConfig.Set(OsuSetting.BeatmapColours, true)); + checkNextHitObject("user"); + + AddStep("disable beatmap skin", () => LocalConfig.Set(OsuSetting.BeatmapSkins, false)); + AddStep("disable beatmap colours", () => LocalConfig.Set(OsuSetting.BeatmapColours, false)); + checkNextHitObject("user"); + + AddStep("disable user provider", () => testUserSkin.Enabled = false); + checkNextHitObject(null); + } + private void checkNextHitObject(string skin) => AddUntilStep($"check skin from {skin}", () => { diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index eb34a0885d..5a8f7b4477 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -82,6 +82,7 @@ namespace osu.Game.Configuration Set(OsuSetting.ShowStoryboard, true); Set(OsuSetting.BeatmapSkins, true); + Set(OsuSetting.BeatmapColours, true); Set(OsuSetting.BeatmapHitsounds, true); Set(OsuSetting.CursorRotation, true); @@ -250,6 +251,7 @@ namespace osu.Game.Configuration ScreenshotCaptureMenuCursor, SongSelectRightMouseScroll, BeatmapSkins, + BeatmapColours, BeatmapHitsounds, IncreaseFirstObjectVisibility, ScoreDisplayMode, diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5898482e4a..e29f97c33e 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -70,6 +70,11 @@ namespace osu.Game.Overlays.Settings.Sections Current = config.GetBindable(OsuSetting.BeatmapSkins) }, new SettingsCheckbox + { + LabelText = "Beatmap colours", + Current = config.GetBindable(OsuSetting.BeatmapColours) + }, + new SettingsCheckbox { LabelText = "Beatmap hitsounds", Current = config.GetBindable(OsuSetting.BeatmapHitsounds) diff --git a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs index 8f29fe7893..a97078c461 100644 --- a/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs +++ b/osu.Game/Screens/Play/PlayerSettings/VisualSettings.cs @@ -14,6 +14,7 @@ namespace osu.Game.Screens.Play.PlayerSettings private readonly PlayerSliderBar blurSliderBar; private readonly PlayerCheckbox showStoryboardToggle; private readonly PlayerCheckbox beatmapSkinsToggle; + private readonly PlayerCheckbox beatmapColorsToggle; private readonly PlayerCheckbox beatmapHitsoundsToggle; public VisualSettings() @@ -43,6 +44,7 @@ namespace osu.Game.Screens.Play.PlayerSettings }, showStoryboardToggle = new PlayerCheckbox { LabelText = "Storyboard / Video" }, beatmapSkinsToggle = new PlayerCheckbox { LabelText = "Beatmap skins" }, + beatmapColorsToggle = new PlayerCheckbox { LabelText = "Beatmap colours" }, beatmapHitsoundsToggle = new PlayerCheckbox { LabelText = "Beatmap hitsounds" } }; } @@ -54,6 +56,7 @@ namespace osu.Game.Screens.Play.PlayerSettings blurSliderBar.Current = config.GetBindable(OsuSetting.BlurLevel); showStoryboardToggle.Current = config.GetBindable(OsuSetting.ShowStoryboard); beatmapSkinsToggle.Current = config.GetBindable(OsuSetting.BeatmapSkins); + beatmapColorsToggle.Current = config.GetBindable(OsuSetting.BeatmapColours); beatmapHitsoundsToggle.Current = config.GetBindable(OsuSetting.BeatmapHitsounds); } } diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs index fc01f0bd31..ffc35b3ab6 100644 --- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs +++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs @@ -15,6 +15,7 @@ namespace osu.Game.Skinning public class BeatmapSkinProvidingContainer : SkinProvidingContainer { private Bindable beatmapSkins; + private Bindable beatmapColours; private Bindable beatmapHitsounds; protected override bool AllowConfigurationLookup @@ -24,10 +25,10 @@ namespace osu.Game.Skinning if (beatmapSkins == null) throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed."); - return beatmapSkins.Value; + return beatmapColours.Value; } } - + protected override bool AllowDrawableLookup(ISkinComponent component) { if (beatmapSkins == null) @@ -62,6 +63,7 @@ namespace osu.Game.Skinning var config = parent.Get(); beatmapSkins = config.GetBindable(OsuSetting.BeatmapSkins); + beatmapColours = config.GetBindable(OsuSetting.BeatmapColours); beatmapHitsounds = config.GetBindable(OsuSetting.BeatmapHitsounds); return base.CreateChildDependencies(parent); @@ -71,6 +73,7 @@ namespace osu.Game.Skinning private void load() { beatmapSkins.BindValueChanged(_ => TriggerSourceChanged()); + beatmapColours.BindValueChanged(_ => TriggerSourceChanged()); beatmapHitsounds.BindValueChanged(_ => TriggerSourceChanged()); } } From 7bfb5954a8e7553dbd429f5901865525045d809a Mon Sep 17 00:00:00 2001 From: Mysfit Date: Wed, 13 Jan 2021 00:25:54 -0500 Subject: [PATCH 5953/6909] Fix whitespace formatting. --- osu.Game/Skinning/BeatmapSkinProvidingContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs index ffc35b3ab6..c16547589d 100644 --- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs +++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs @@ -28,7 +28,7 @@ namespace osu.Game.Skinning return beatmapColours.Value; } } - + protected override bool AllowDrawableLookup(ISkinComponent component) { if (beatmapSkins == null) From 80bcd78a4802131d02576bebcf87c16f8297f2af Mon Sep 17 00:00:00 2001 From: Mysfit Date: Wed, 13 Jan 2021 02:04:59 -0500 Subject: [PATCH 5954/6909] Removed unnecessary using. --- osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index 83b1e28476..138182c7c4 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -12,7 +12,6 @@ using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; -using osu.Game.Rulesets.Osu.Configuration; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Screens.Play; using osu.Game.Skinning; From e8daea91d22e87e95d2deaf8caf70817c6277ebc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Jan 2021 18:13:05 +0300 Subject: [PATCH 5955/6909] Add online beatmap "explicit content" property --- osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs | 5 +++++ osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs index 06dee4d3f5..48f1f0ce68 100644 --- a/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs +++ b/osu.Game/Beatmaps/BeatmapSetOnlineInfo.cs @@ -31,6 +31,11 @@ namespace osu.Game.Beatmaps /// public BeatmapSetOnlineStatus Status { get; set; } + /// + /// Whether or not this beatmap set has explicit content. + /// + public bool HasExplicitContent { get; set; } + /// /// Whether or not this beatmap set has a background video. /// diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index 720d6bfff4..bd1800e9f7 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -42,6 +42,9 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"bpm")] private double bpm { get; set; } + [JsonProperty(@"nsfw")] + private bool hasExplicitContent { get; set; } + [JsonProperty(@"video")] private bool hasVideo { get; set; } @@ -94,6 +97,7 @@ namespace osu.Game.Online.API.Requests.Responses FavouriteCount = favouriteCount, BPM = bpm, Status = Status, + HasExplicitContent = hasExplicitContent, HasVideo = hasVideo, HasStoryboard = hasStoryboard, Submitted = submitted, From ee6baeb57e0d51e8d65a3c8c70e25701cc58ada8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 12 Jan 2021 18:15:00 +0300 Subject: [PATCH 5956/6909] Add "explicit" marker pill --- .../BeatmapSet/ExplicitBeatmapPill.cs | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs diff --git a/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs b/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs new file mode 100644 index 0000000000..77528c65c3 --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.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 osu.Game.Graphics.Sprites; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Overlays.BeatmapSet +{ + public class ExplicitBeatmapPill : CompositeDrawable + { + public ExplicitBeatmapPill() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader(true)] + private void load(OsuColour colours, OverlayColourProvider colourProvider) + { + InternalChild = new CircularContainer + { + Masking = true, + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colourProvider?.Background5 ?? colours.Gray2, + }, + new OsuSpriteText + { + Margin = new MarginPadding { Horizontal = 10f, Vertical = 2f }, + Text = "EXPLICIT", + Font = OsuFont.GetFont(size: 10, weight: FontWeight.Bold), + // todo: this is --hsl-orange-2 from the new palette in https://github.com/ppy/osu-web/blob/8ceb46f/resources/assets/less/colors.less#L128-L151, + // should probably take the whole palette from there onto OsuColour for a nicer look in code. + Colour = Color4.FromHsl(new Vector4(45f / 360, 0.8f, 0.6f, 1f)), + } + } + }; + } + } +} From f6637eec36a460c5551e81d5b8776c12fad0caa9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 11:29:30 +0300 Subject: [PATCH 5957/6909] Add explicit pill to beatmap panels --- .../BeatmapListing/Panels/GridBeatmapPanel.cs | 29 +++++++++++++++---- .../BeatmapListing/Panels/ListBeatmapPanel.cs | 27 ++++++++++++++--- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs index 28c36e6c56..b7002a96e5 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays.BeatmapSet; using osuTK; using osuTK.Graphics; @@ -24,7 +25,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels private const float horizontal_padding = 10; private const float vertical_padding = 5; - private FillFlowContainer bottomPanel, statusContainer; + private FillFlowContainer bottomPanel, statusContainer, titleContainer; private PlayButton playButton; private Box progressBar; @@ -73,12 +74,20 @@ namespace osu.Game.Overlays.BeatmapListing.Panels AutoSizeAxes = Axes.Both, Padding = new MarginPadding { Left = horizontal_padding, Right = horizontal_padding }, Direction = FillDirection.Vertical, - Children = new[] + Children = new Drawable[] { - new OsuSpriteText + titleContainer = new FillFlowContainer { - Text = new LocalisedString((SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title)), - Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true) + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new Drawable[] + { + new OsuSpriteText + { + Text = new LocalisedString((SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title)), + Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true) + }, + } }, new OsuSpriteText { @@ -194,6 +203,16 @@ namespace osu.Game.Overlays.BeatmapListing.Panels }, }); + if (SetInfo.OnlineInfo?.HasExplicitContent ?? false) + { + titleContainer.Add(new ExplicitBeatmapPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 10f, Top = 2f }, + }); + } + if (SetInfo.OnlineInfo?.HasVideo ?? false) { statusContainer.Add(new IconPill(FontAwesome.Solid.Film)); diff --git a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs index 433ea37f06..69671ab75b 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs @@ -14,6 +14,7 @@ using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; +using osu.Game.Overlays.BeatmapSet; using osuTK; using osuTK.Graphics; @@ -26,7 +27,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels private const float vertical_padding = 5; private const float height = 70; - private FillFlowContainer statusContainer; + private FillFlowContainer statusContainer, titleContainer; protected BeatmapPanelDownloadButton DownloadButton; private PlayButton playButton; private Box progressBar; @@ -98,10 +99,18 @@ namespace osu.Game.Overlays.BeatmapListing.Panels Direction = FillDirection.Vertical, Children = new Drawable[] { - new OsuSpriteText + titleContainer = new FillFlowContainer { - Text = new LocalisedString((SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title)), - Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true) + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Children = new[] + { + new OsuSpriteText + { + Text = new LocalisedString((SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title)), + Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true) + }, + } }, new OsuSpriteText { @@ -208,6 +217,16 @@ namespace osu.Game.Overlays.BeatmapListing.Panels }, }); + if (SetInfo.OnlineInfo?.HasExplicitContent ?? false) + { + titleContainer.Add(new ExplicitBeatmapPill + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 10f, Top = 2f }, + }); + } + if (SetInfo.OnlineInfo?.HasVideo ?? false) { statusContainer.Add(new IconPill(FontAwesome.Solid.Film) { IconSize = new Vector2(20) }); From 78631323ba35174543cbb09d2c8fd4f545888077 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 11:32:36 +0300 Subject: [PATCH 5958/6909] Add explicit pill to beatmap overlay --- osu.Game/Overlays/BeatmapSet/Header.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 321e496511..fbdee91d42 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -34,6 +34,7 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Box coverGradient; private readonly OsuSpriteText title, artist; private readonly AuthorInfo author; + private readonly ExplicitBeatmapPill explicitPill; private readonly FillFlowContainer downloadButtonsContainer; private readonly BeatmapAvailability beatmapAvailability; private readonly BeatmapSetOnlineStatusPill onlineStatusPill; @@ -146,6 +147,13 @@ namespace osu.Game.Overlays.BeatmapSet Origin = Anchor.BottomLeft, Margin = new MarginPadding { Left = 3, Bottom = 4 }, // To better lineup with the font }, + explicitPill = new ExplicitBeatmapPill + { + Alpha = 0f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Left = 10f, Top = 4 }, + } } }, artist = new OsuSpriteText @@ -253,6 +261,8 @@ namespace osu.Game.Overlays.BeatmapSet title.Text = setInfo.NewValue.Metadata.Title ?? string.Empty; artist.Text = setInfo.NewValue.Metadata.Artist ?? string.Empty; + explicitPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; + onlineStatusPill.FadeIn(500, Easing.OutQuint); onlineStatusPill.Status = setInfo.NewValue.OnlineInfo.Status; From 1502b07ea86d7673dbbffa2fc2ec83612ca8d81c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 11:33:00 +0300 Subject: [PATCH 5959/6909] Add explicit pill to playlist items --- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index e3bce4029f..7987d715e3 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -23,6 +23,7 @@ using osu.Game.Online; using osu.Game.Online.Chat; using osu.Game.Online.Rooms; using osu.Game.Overlays.BeatmapListing.Panels; +using osu.Game.Overlays.BeatmapSet; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; @@ -41,6 +42,7 @@ namespace osu.Game.Screens.OnlinePlay private Container difficultyIconContainer; private LinkFlowContainer beatmapText; private LinkFlowContainer authorText; + private ExplicitBeatmapPill explicitPill; private ModDisplay modDisplay; private readonly Bindable beatmap = new Bindable(); @@ -116,6 +118,9 @@ namespace osu.Game.Screens.OnlinePlay authorText.AddUserLink(Item.Beatmap.Value?.Metadata.Author); } + bool hasExplicitContent = Item.Beatmap.Value.BeatmapSet.OnlineInfo?.HasExplicitContent == true; + explicitPill.Alpha = hasExplicitContent ? 1 : 0; + modDisplay.Current.Value = requiredMods.ToArray(); } @@ -165,18 +170,37 @@ namespace osu.Game.Screens.OnlinePlay { AutoSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Spacing = new Vector2(15, 0), + Spacing = new Vector2(10f, 0), Children = new Drawable[] { - authorText = new LinkFlowContainer { AutoSizeAxes = Axes.Both }, - modDisplay = new ModDisplay + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10f, 0), + Children = new Drawable[] + { + authorText = new LinkFlowContainer { AutoSizeAxes = Axes.Both }, + explicitPill = new ExplicitBeatmapPill + { + Alpha = 0f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Top = 3f }, + } + }, + }, + new Container { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, AutoSizeAxes = Axes.Both, - Scale = new Vector2(0.4f), - DisplayUnrankedText = false, - ExpansionMode = ExpansionMode.AlwaysExpanded + Child = modDisplay = new ModDisplay + { + Scale = new Vector2(0.4f), + DisplayUnrankedText = false, + ExpansionMode = ExpansionMode.AlwaysExpanded + } } } } From 7fd55efc434fb5522de44429cc27e27c32cd6969 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 11:57:29 +0300 Subject: [PATCH 5960/6909] Add test cases for displaying explicit beatmaps --- .../Multiplayer/TestSceneDrawableRoomPlaylist.cs | 9 +++++++++ .../Visual/Online/TestSceneBeatmapSetOverlay.cs | 11 +++++++++++ osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs | 7 ++++++- 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 65c0cfd328..17d85d1120 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -222,6 +222,15 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("download buttons shown", () => playlist.ChildrenOfType().All(d => d.IsPresent)); } + [Test] + public void TestExplicitBeatmapItem() + { + var beatmap = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo; + beatmap.BeatmapSet.OnlineInfo.HasExplicitContent = true; + + createPlaylist(beatmap); + } + private void moveToItem(int index, Vector2? offset = null) => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType>().ElementAt(index), offset)); diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index c5d1fd6887..689321698a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -235,6 +235,17 @@ namespace osu.Game.Tests.Visual.Online AddAssert("left-most beatmap selected", () => overlay.Header.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected); } + [Test] + public void TestExplicitBeatmap() + { + AddStep("show explicit map", () => + { + var beatmapSet = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; + beatmapSet.OnlineInfo.HasExplicitContent = true; + overlay.ShowBeatmapSet(beatmapSet); + }); + } + [Test] public void TestHide() { diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs index 74ece5da05..fd5f306e07 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectPanel.cs @@ -99,13 +99,16 @@ namespace osu.Game.Tests.Visual.Online [BackgroundDependencyLoader] private void load(RulesetStore rulesets) { - var normal = CreateWorkingBeatmap(Ruleset.Value).BeatmapSetInfo; + var normal = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; normal.OnlineInfo.HasVideo = true; normal.OnlineInfo.HasStoryboard = true; var undownloadable = getUndownloadableBeatmapSet(); var manyDifficulties = getManyDifficultiesBeatmapSet(rulesets); + var explicitMap = CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet; + explicitMap.OnlineInfo.HasExplicitContent = true; + Child = new BasicScrollContainer { RelativeSizeAxes = Axes.Both, @@ -121,9 +124,11 @@ namespace osu.Game.Tests.Visual.Online new GridBeatmapPanel(normal), new GridBeatmapPanel(undownloadable), new GridBeatmapPanel(manyDifficulties), + new GridBeatmapPanel(explicitMap), new ListBeatmapPanel(normal), new ListBeatmapPanel(undownloadable), new ListBeatmapPanel(manyDifficulties), + new ListBeatmapPanel(explicitMap) }, }, }; From f827c620813468f2c670f393c424b84e8b11f634 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 11:58:55 +0300 Subject: [PATCH 5961/6909] Add empty online info to test beatmap This is for `BeatmapSetOverlay` to not eat a null reference trying to access `Beatmap.OnlineInfo`. --- 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 035cb64099..fa6dc5647d 100644 --- a/osu.Game/Tests/Beatmaps/TestBeatmap.cs +++ b/osu.Game/Tests/Beatmaps/TestBeatmap.cs @@ -32,6 +32,7 @@ namespace osu.Game.Tests.Beatmaps BeatmapInfo.BeatmapSet.Files = new List(); BeatmapInfo.BeatmapSet.Beatmaps = new List { BeatmapInfo }; BeatmapInfo.Length = 75000; + BeatmapInfo.OnlineInfo = new BeatmapOnlineInfo(); BeatmapInfo.BeatmapSet.OnlineInfo = new BeatmapSetOnlineInfo { Status = BeatmapSetOnlineStatus.Ranked, From 268f9d661f16c55aa1c8ebadb4f44e05baa7373a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 13:00:14 +0300 Subject: [PATCH 5962/6909] Dispose of local config on disposal --- .../UserInterface/TestSceneBeatmapListingSearchControl.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index cb86047dea..dc46da6293 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -99,6 +99,12 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("explicit control set to hide", () => control.Explicit.Value == SearchExplicit.Hide); } + protected override void Dispose(bool isDisposing) + { + localConfig?.Dispose(); + base.Dispose(isDisposing); + } + private static readonly BeatmapSetInfo beatmap_set = new BeatmapSetInfo { OnlineInfo = new BeatmapSetOnlineInfo From 9d59d784f8674b1319a175c1edb74d079de76dfe Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 17:06:44 +0300 Subject: [PATCH 5963/6909] Add Colour{1-4} properties to `OverlayColourProvider` --- osu.Game/Overlays/OverlayColourProvider.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index 9816f313ad..f33223d7aa 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.cs @@ -16,6 +16,11 @@ namespace osu.Game.Overlays this.colourScheme = colourScheme; } + public Color4 Colour1 => getColour(1, 0.7f); + public Color4 Colour2 => getColour(0.8f, 0.6f); + public Color4 Colour3 => getColour(0.6f, 0.5f); + public Color4 Colour4 => getColour(0.4f, 0.3f); + public Color4 Highlight1 => getColour(1, 0.7f); public Color4 Content1 => getColour(0.4f, 1); public Color4 Content2 => getColour(0.4f, 0.9f); From e275dd02e08ab8d9e890d2d0a2b1f822e1c53f6c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 17:07:11 +0300 Subject: [PATCH 5964/6909] Create static colour properties for now --- osu.Game/Overlays/OverlayColourProvider.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Overlays/OverlayColourProvider.cs b/osu.Game/Overlays/OverlayColourProvider.cs index f33223d7aa..abd1e43f25 100644 --- a/osu.Game/Overlays/OverlayColourProvider.cs +++ b/osu.Game/Overlays/OverlayColourProvider.cs @@ -11,6 +11,13 @@ namespace osu.Game.Overlays { private readonly OverlayColourScheme colourScheme; + public static OverlayColourProvider Red { get; } = new OverlayColourProvider(OverlayColourScheme.Red); + public static OverlayColourProvider Pink { get; } = new OverlayColourProvider(OverlayColourScheme.Pink); + public static OverlayColourProvider Orange { get; } = new OverlayColourProvider(OverlayColourScheme.Orange); + public static OverlayColourProvider Green { get; } = new OverlayColourProvider(OverlayColourScheme.Green); + public static OverlayColourProvider Purple { get; } = new OverlayColourProvider(OverlayColourScheme.Purple); + public static OverlayColourProvider Blue { get; } = new OverlayColourProvider(OverlayColourScheme.Blue); + public OverlayColourProvider(OverlayColourScheme colourScheme) { this.colourScheme = colourScheme; From 43daa7c7c09463acac11095bc93f304e1c20ae83 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 17:07:42 +0300 Subject: [PATCH 5965/6909] Use `Colour2` of orange theme for explicit pill --- osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs b/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs index 77528c65c3..ad19dffccb 100644 --- a/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs +++ b/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs @@ -7,8 +7,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; -using osuTK; -using osuTK.Graphics; namespace osu.Game.Overlays.BeatmapSet { @@ -38,9 +36,7 @@ namespace osu.Game.Overlays.BeatmapSet Margin = new MarginPadding { Horizontal = 10f, Vertical = 2f }, Text = "EXPLICIT", Font = OsuFont.GetFont(size: 10, weight: FontWeight.Bold), - // todo: this is --hsl-orange-2 from the new palette in https://github.com/ppy/osu-web/blob/8ceb46f/resources/assets/less/colors.less#L128-L151, - // should probably take the whole palette from there onto OsuColour for a nicer look in code. - Colour = Color4.FromHsl(new Vector4(45f / 360, 0.8f, 0.6f, 1f)), + Colour = OverlayColourProvider.Orange.Colour2, } } }; From 1f12b2bd091e7968c60e6c100cc9369edd5c3973 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 18:04:29 +0300 Subject: [PATCH 5966/6909] Rename download state `Downloaded` to `Importing` --- osu.Game/Graphics/UserInterface/DownloadButton.cs | 2 +- osu.Game/Online/DownloadState.cs | 2 +- osu.Game/Online/DownloadTrackingComposite.cs | 4 ++-- .../BeatmapListing/Panels/BeatmapPanelDownloadButton.cs | 2 +- .../Overlays/BeatmapListing/Panels/DownloadProgressBar.cs | 2 +- osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs | 2 +- osu.Game/Overlays/BeatmapSet/Header.cs | 2 +- osu.Game/Screens/Ranking/ReplayDownloadButton.cs | 2 +- 8 files changed, 9 insertions(+), 9 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/DownloadButton.cs b/osu.Game/Graphics/UserInterface/DownloadButton.cs index da6c95299e..5168ff646b 100644 --- a/osu.Game/Graphics/UserInterface/DownloadButton.cs +++ b/osu.Game/Graphics/UserInterface/DownloadButton.cs @@ -73,7 +73,7 @@ namespace osu.Game.Graphics.UserInterface TooltipText = "Downloading..."; break; - case DownloadState.Downloaded: + case DownloadState.Importing: background.FadeColour(colours.Yellow, 500, Easing.InOutExpo); TooltipText = "Importing"; break; diff --git a/osu.Game/Online/DownloadState.cs b/osu.Game/Online/DownloadState.cs index 72efbc286e..a58c40d16a 100644 --- a/osu.Game/Online/DownloadState.cs +++ b/osu.Game/Online/DownloadState.cs @@ -7,7 +7,7 @@ namespace osu.Game.Online { NotDownloaded, Downloading, - Downloaded, + Importing, LocallyAvailable } } diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index bed95344c6..7a64c9002d 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -106,7 +106,7 @@ namespace osu.Game.Online { if (attachedRequest.Progress == 1) { - State.Value = DownloadState.Downloaded; + State.Value = DownloadState.Importing; Progress.Value = 1; } else @@ -125,7 +125,7 @@ namespace osu.Game.Online } } - private void onRequestSuccess(string _) => Schedule(() => State.Value = DownloadState.Downloaded); + private void onRequestSuccess(string _) => Schedule(() => State.Value = DownloadState.Importing); private void onRequestProgress(float progress) => Schedule(() => Progress.Value = progress); diff --git a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs index 001ca801d9..cec1a5ac12 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/BeatmapPanelDownloadButton.cs @@ -57,7 +57,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels switch (State.Value) { case DownloadState.Downloading: - case DownloadState.Downloaded: + case DownloadState.Importing: shakeContainer.Shake(); break; diff --git a/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs index 93cf8799b5..6a2f2e4569 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs @@ -50,7 +50,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels progressBar.ResizeHeightTo(4, 400, Easing.OutQuint); break; - case DownloadState.Downloaded: + case DownloadState.Importing: progressBar.FadeIn(400, Easing.OutQuint); progressBar.ResizeHeightTo(4, 400, Easing.OutQuint); diff --git a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs index 56c0052bfe..cffff86a64 100644 --- a/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs +++ b/osu.Game/Overlays/BeatmapSet/Buttons/HeaderDownloadButton.cs @@ -126,7 +126,7 @@ namespace osu.Game.Overlays.BeatmapSet.Buttons }; break; - case DownloadState.Downloaded: + case DownloadState.Importing: textSprites.Children = new Drawable[] { new OsuSpriteText diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 321e496511..c17901bb3f 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -287,7 +287,7 @@ namespace osu.Game.Overlays.BeatmapSet break; case DownloadState.Downloading: - case DownloadState.Downloaded: + case DownloadState.Importing: // temporary to avoid showing two buttons for maps with novideo. will be fixed in new beatmap overlay design. downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value); break; diff --git a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs index b76842f405..18b8649a59 100644 --- a/osu.Game/Screens/Ranking/ReplayDownloadButton.cs +++ b/osu.Game/Screens/Ranking/ReplayDownloadButton.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Ranking scores.Download(Model.Value); break; - case DownloadState.Downloaded: + case DownloadState.Importing: case DownloadState.Downloading: shakeContainer.Shake(); break; From 1ba586a683b3321cf7806d9dabf068983d0bd27b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 13 Jan 2021 17:53:01 +0100 Subject: [PATCH 5967/6909] Revert overlooked AR<8 speed buff Pull request #11107 introduced changes in osu! performance calculation, related to a scaling coefficient applied to the speed and aim skills. The coefficient in question was dependent on the approach rate of a map. During a post-merge review of that PR, it was spotted that the scaling coefficient for speed also had a 10x buff applied for AR<8, which could reach magnitudes as large as 80% on AR0, which seems quite exorbitant. This change was not discussed or mentioned anywhere in the review process. Revert back to the old multiplier of 0.01 rather than 0.1 for AR<8. The negative slope through AR0 to 8 is retained in its previous form. --- 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 a44c97c3cf..44a9dd2f1f 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuPerformanceCalculator.cs @@ -104,7 +104,7 @@ namespace osu.Game.Rulesets.Osu.Difficulty if (Attributes.ApproachRate > 10.33) approachRateFactor += 0.4 * (Attributes.ApproachRate - 10.33); else if (Attributes.ApproachRate < 8.0) - approachRateFactor += 0.1 * (8.0 - Attributes.ApproachRate); + approachRateFactor += 0.01 * (8.0 - Attributes.ApproachRate); aimValue *= 1.0 + Math.Min(approachRateFactor, approachRateFactor * (totalHits / 1000.0)); From 1248d39d7e5fa6a7a30271a1346059594e0e021d Mon Sep 17 00:00:00 2001 From: Mysfit Date: Wed, 13 Jan 2021 13:07:07 -0500 Subject: [PATCH 5968/6909] Reverted change to AllowConfigurationLookup and added a separate AllowColourLookup bool with config case based on lookup type in SkinProvidingContainer GetConfig call. --- .../Skinning/BeatmapSkinProvidingContainer.cs | 11 ++++++ osu.Game/Skinning/SkinProvidingContainer.cs | 34 ++++++++++++++++--- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs index c16547589d..57c08a903f 100644 --- a/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs +++ b/osu.Game/Skinning/BeatmapSkinProvidingContainer.cs @@ -25,6 +25,17 @@ namespace osu.Game.Skinning if (beatmapSkins == null) throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed."); + return beatmapSkins.Value; + } + } + + protected override bool AllowColourLookup + { + get + { + if (beatmapColours == null) + throw new InvalidOperationException($"{nameof(BeatmapSkinProvidingContainer)} needs to be loaded before being consumed."); + return beatmapColours.Value; } } diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index adf62ed452..3232a30110 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -32,6 +32,8 @@ namespace osu.Game.Skinning protected virtual bool AllowConfigurationLookup => true; + protected virtual bool AllowColourLookup => true; + public SkinProvidingContainer(ISkin skin) { this.skin = skin; @@ -68,11 +70,35 @@ namespace osu.Game.Skinning public IBindable GetConfig(TLookup lookup) { - if (AllowConfigurationLookup && skin != null) + if (skin != null) { - var bindable = skin.GetConfig(lookup); - if (bindable != null) - return bindable; + switch (lookup) + { + // todo: the GlobalSkinColours switch is pulled from LegacySkin and should not exist. + // will likely change based on how databased storage of skin configuration goes. + case GlobalSkinColours global: + switch (global) + { + case GlobalSkinColours.ComboColours: + var bindable = skin.GetConfig(lookup); + if (bindable != null && AllowColourLookup) + return bindable; + else + return fallbackSource?.GetConfig(lookup); + } + + break; + + default: + if (AllowConfigurationLookup) + { + var bindable = skin.GetConfig(lookup); + if (bindable != null) + return bindable; + } + + break; + } } return fallbackSource?.GetConfig(lookup); From 95acc457aad5f08df2b7a7a4bc37af4a241ffc2d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 13 Jan 2021 22:34:48 +0300 Subject: [PATCH 5969/6909] Fix stupid mistake fuck. --- osu.Game/Online/Rooms/BeatmapAvailability.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index b6b9c632fe..e7dbc5f436 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -30,7 +30,7 @@ namespace osu.Game.Online.Rooms public static BeatmapAvailability NotDownloaded() => new BeatmapAvailability(DownloadState.NotDownloaded); public static BeatmapAvailability Downloading(double progress) => new BeatmapAvailability(DownloadState.Downloading, progress); - public static BeatmapAvailability Importing() => new BeatmapAvailability(DownloadState.Downloaded); + public static BeatmapAvailability Importing() => new BeatmapAvailability(DownloadState.Importing); public static BeatmapAvailability LocallyAvailable() => new BeatmapAvailability(DownloadState.LocallyAvailable); public bool Equals(BeatmapAvailability other) => other != null && State == other.State && DownloadProgress == other.DownloadProgress; From 8b95817f7abf8f5674ffa11d37c1eb37ce259806 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Wed, 13 Jan 2021 16:05:46 -0500 Subject: [PATCH 5970/6909] Moved SkinProvidingContainer bindable fetching to common method. Replaced redundant test boolean declarations with inline values. --- .../TestSceneLegacyBeatmapSkin.cs | 28 +++++++++---------- osu.Game/Skinning/SkinProvidingContainer.cs | 24 ++++++++-------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index 138182c7c4..22b028906f 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -36,41 +36,41 @@ namespace osu.Game.Rulesets.Osu.Tests config.BindWith(OsuSetting.BeatmapColours, beatmapColours); } - [TestCase(true, true, true)] - [TestCase(true, false, true)] - [TestCase(false, true, true)] - [TestCase(false, false, true)] - public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin, bool useBeatmapColour) + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin) { ExposedPlayer player = null; - configureSettings(useBeatmapSkin, useBeatmapColour); + configureSettings(useBeatmapSkin, true); AddStep("load coloured beatmap", () => player = loadBeatmap(userHasCustomColours, true)); AddUntilStep("wait for player", () => player.IsLoaded); AddAssert("is beatmap skin colours", () => player.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours)); } - [TestCase(true, false)] - [TestCase(false, false)] - public void TestBeatmapComboColoursOverride(bool useBeatmapSkin, bool useBeatmapColour) + [TestCase(true)] + [TestCase(false)] + public void TestBeatmapComboColoursOverride(bool useBeatmapSkin) { ExposedPlayer player = null; - configureSettings(useBeatmapSkin, useBeatmapColour); + configureSettings(useBeatmapSkin, false); AddStep("load coloured beatmap", () => player = loadBeatmap(true, true)); AddUntilStep("wait for player", () => player.IsLoaded); AddAssert("is user custom skin colours", () => player.UsableComboColours.SequenceEqual(TestSkin.Colours)); } - [TestCase(true, false)] - [TestCase(false, false)] - public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin, bool useBeatmapColour) + [TestCase(true)] + [TestCase(false)] + public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin) { ExposedPlayer player = null; - configureSettings(useBeatmapSkin, useBeatmapColour); + configureSettings(useBeatmapSkin, false); AddStep("load coloured beatmap", () => player = loadBeatmap(false, true)); AddUntilStep("wait for player", () => player.IsLoaded); diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 3232a30110..ae9a84932a 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -80,30 +80,28 @@ namespace osu.Game.Skinning switch (global) { case GlobalSkinColours.ComboColours: - var bindable = skin.GetConfig(lookup); - if (bindable != null && AllowColourLookup) - return bindable; - else - return fallbackSource?.GetConfig(lookup); + return getBindable(lookup, AllowColourLookup); } break; default: - if (AllowConfigurationLookup) - { - var bindable = skin.GetConfig(lookup); - if (bindable != null) - return bindable; - } - - break; + return getBindable(lookup, AllowConfigurationLookup); } } return fallbackSource?.GetConfig(lookup); } + private IBindable getBindable(TLookup lookup, bool bindableReturnCheck) + { + var bindable = skin.GetConfig(lookup); + if (bindable != null && bindableReturnCheck) + return bindable; + else + return fallbackSource?.GetConfig(lookup); + } + protected virtual void TriggerSourceChanged() => SourceChanged?.Invoke(); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) From 562634dfd2de90736dfb812c9d8834c0c97bcb4c Mon Sep 17 00:00:00 2001 From: Jesse Myers Date: Wed, 13 Jan 2021 16:49:14 -0500 Subject: [PATCH 5971/6909] Improve naming around the config lookup with fallback private method. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Skinning/SkinProvidingContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index ae9a84932a..2dc19667e4 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -93,10 +93,10 @@ namespace osu.Game.Skinning return fallbackSource?.GetConfig(lookup); } - private IBindable getBindable(TLookup lookup, bool bindableReturnCheck) + private IBindable lookupWithFallback(TLookup lookup, bool canUseSkinLookup) { var bindable = skin.GetConfig(lookup); - if (bindable != null && bindableReturnCheck) + if (bindable != null && canUseSkinLookup) return bindable; else return fallbackSource?.GetConfig(lookup); From abf718242b11519825228a2f6098dff745231608 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 14 Jan 2021 05:40:43 +0300 Subject: [PATCH 5972/6909] Make explicit marker font semi-bold --- osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs b/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs index ad19dffccb..aefb3299a5 100644 --- a/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs +++ b/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.BeatmapSet { Margin = new MarginPadding { Horizontal = 10f, Vertical = 2f }, Text = "EXPLICIT", - Font = OsuFont.GetFont(size: 10, weight: FontWeight.Bold), + Font = OsuFont.GetFont(size: 10, weight: FontWeight.SemiBold), Colour = OverlayColourProvider.Orange.Colour2, } } From 6281c1086ac377503c89a5648608e47ab6b71e89 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 14 Jan 2021 05:41:09 +0300 Subject: [PATCH 5973/6909] Space out explicit marker in beatmap overlay --- osu.Game/Overlays/BeatmapSet/Header.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index fbdee91d42..9f2b40dfa4 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -152,7 +152,7 @@ namespace osu.Game.Overlays.BeatmapSet Alpha = 0f, Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = 10f, Top = 4 }, + Margin = new MarginPadding { Left = 15f, Top = 4 }, } } }, From d5878db615ef55e87b9e39e22b480610f12cdf6c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 14 Jan 2021 12:33:33 +0900 Subject: [PATCH 5974/6909] Fix default judgement text mispositioned for one frame --- osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs index d94346cb72..21ac017685 100644 --- a/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs +++ b/osu.Game/Rulesets/Judgements/DefaultJudgementPiece.cs @@ -37,6 +37,8 @@ namespace osu.Game.Rulesets.Judgements { JudgementText = new OsuSpriteText { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Text = Result.GetDescription().ToUpperInvariant(), Colour = colours.ForHitResult(Result), Font = OsuFont.Numeric.With(size: 20), From 8a0b975d71d08f6a845b10b46aec50d714d00c68 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 18:25:32 +0900 Subject: [PATCH 5975/6909] Fix deadlock scenario when calculating fallback difficulty The previous code would run a calcaulation for the beatmap's own ruleset if the current one failed. While this does make sense, with the current way we use this component (and the implementation flow) it is quite unsafe. The to the call on `.Result` in the `catch` block, this would 100% deadlock due to the thread concurrency of the `ThreadedTaskScheduler` being 1. Even if the nested run could be run inline (it should be), the task scheduler won't even get to the point of checking whether this is feasible due to it being saturated by the already running task. I'm not sure if we still need this fallback lookup logic. After removing it, it's feasible that 0 stars will be returned during the scenario that previously caused a deadlock, but I don't necessarily think this is incorrect. There may be another reason for this needing to exist which I'm not aware of (diffcalc?) but if that's the case we may want to move the try-catch handling to the point of usage. To reproduce the deadlock scenario with 100% success (the repro instructions in the linked issue aren't that simple and require some patience and good timing), the main portion of the lookup can be changed to randomly trigger a nested lookup: ``` if (RNG.NextSingle() > 0.5f) return GetAsync(new DifficultyCacheLookup(key.Beatmap, key.Beatmap.Ruleset, key.OrderedMods)).Result; else return new StarDifficulty(attributes); ``` After switching beatmap once or twice, pausing debug and viewing the state of threads should show exactly what is going on. --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 3b58062add..37d262abe5 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -260,17 +260,10 @@ namespace osu.Game.Beatmaps } catch (BeatmapInvalidForRulesetException e) { - // Conversion has failed for the given ruleset, so return the difficulty in the beatmap's default ruleset. - - // Ensure the beatmap's default ruleset isn't the one already being converted to. - // This shouldn't happen as it means something went seriously wrong, but if it does an endless loop should be avoided. if (rulesetInfo.Equals(beatmapInfo.Ruleset)) - { Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset})."); - return new StarDifficulty(); - } - return GetAsync(new DifficultyCacheLookup(key.Beatmap, key.Beatmap.Ruleset, key.OrderedMods)).Result; + return new StarDifficulty(); } catch { From 99e43c77c23709c90100cea34210c5a2d054a206 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Thu, 14 Jan 2021 16:53:55 -0500 Subject: [PATCH 5976/6909] Simplified colour config checks in SkinProvidingContainer.cs --- osu.Game/Skinning/SkinProvidingContainer.cs | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 2dc19667e4..d2645eff68 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; +using osuTK.Graphics; namespace osu.Game.Skinning { @@ -72,22 +73,12 @@ namespace osu.Game.Skinning { if (skin != null) { - switch (lookup) - { - // todo: the GlobalSkinColours switch is pulled from LegacySkin and should not exist. - // will likely change based on how databased storage of skin configuration goes. - case GlobalSkinColours global: - switch (global) - { - case GlobalSkinColours.ComboColours: - return getBindable(lookup, AllowColourLookup); - } + TValue tValueTypeCheck = default; - break; - - default: - return getBindable(lookup, AllowConfigurationLookup); - } + if (lookup is GlobalSkinColours || tValueTypeCheck is Color4) + return lookupWithFallback(lookup, AllowColourLookup); + else + return lookupWithFallback(lookup, AllowConfigurationLookup); } return fallbackSource?.GetConfig(lookup); From dc8e38cf4d8dfcdb0a33a6d4c73147ce43fe4c05 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 15 Jan 2021 07:20:13 +0300 Subject: [PATCH 5977/6909] Remove pointless inline comment --- osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 3761aee312..5c6267b726 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -151,7 +151,6 @@ namespace osu.Game.Overlays.BeatmapListing allowExplicitContent = config.GetBindable(OsuSetting.AllowExplicitContent); allowExplicitContent.BindValueChanged(allow => { - // Update search control if global "explicit allowed" setting changed. Explicit.Value = allow.NewValue ? SearchExplicit.Show : SearchExplicit.Hide; }, true); } From 4cccde9007cdb1646f041da71d77184c51f490d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 13:20:46 +0900 Subject: [PATCH 5978/6909] 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 492c88c7e4..919d83f8db 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 ff016199d3..ac014f2964 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -28,7 +28,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 93be3645ee..799042626b 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 0c01a3a685c9ba1ca5c29f5e124043780c9f1d61 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Thu, 14 Jan 2021 23:30:24 -0500 Subject: [PATCH 5979/6909] Found a better solution than TValue type checking for additional beatmap colour settings. Added unit tests for Catch Beatmap Skin settings. --- .../TestSceneLegacyBeatmapSkin.cs | 360 ++++++++++++++++++ osu.Game/Skinning/SkinProvidingContainer.cs | 5 +- 2 files changed, 361 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs new file mode 100644 index 0000000000..12ceaaa6fa --- /dev/null +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs @@ -0,0 +1,360 @@ +// Copyright (c) ppy Pty Ltd . 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.Audio; +using osu.Framework.Bindables; +using osu.Framework.IO.Stores; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; +using osu.Game.Rulesets.Catch.Objects; +using osu.Game.Rulesets.Catch.Skinning; +using osu.Game.Rulesets.Catch.UI; +using osu.Game.Rulesets.Objects; +using osu.Game.Screens.Play; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; +using osuTK; +using osuTK.Graphics; + +namespace osu.Game.Rulesets.Catch.Tests +{ + public class TestSceneLegacyBeatmapSkin : ScreenTestScene + { + [Resolved] + private AudioManager audio { get; set; } + + private readonly Bindable beatmapSkins = new Bindable(); + private readonly Bindable beatmapColours = new Bindable(); + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); + config.BindWith(OsuSetting.BeatmapColours, beatmapColours); + } + + [TestCase(true, true)] + [TestCase(true, false)] + [TestCase(false, true)] + [TestCase(false, false)] + public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin) + { + ExposedPlayer player = null; + + configureSettings(useBeatmapSkin, true); + AddStep("load coloured beatmap", () => player = loadBeatmap(userHasCustomColours, true)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is beatmap skin colours", () => player.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours)); + } + + [TestCase(true)] + [TestCase(false)] + public void TestBeatmapComboColoursOverride(bool useBeatmapSkin) + { + ExposedPlayer player = null; + + configureSettings(useBeatmapSkin, false); + AddStep("load coloured beatmap", () => player = loadBeatmap(true, true)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is user custom skin colours", () => player.UsableComboColours.SequenceEqual(TestSkin.Colours)); + } + + [TestCase(true)] + [TestCase(false)] + public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin) + { + ExposedPlayer player = null; + + configureSettings(useBeatmapSkin, false); + AddStep("load coloured beatmap", () => player = loadBeatmap(false, true)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is default user skin colours", () => player.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); + } + + [TestCase(true, true)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(false, false)] + public void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour) + { + ExposedPlayer player = null; + + configureSettings(useBeatmapSkin, useBeatmapColour); + AddStep("load no-colour beatmap", () => player = loadBeatmap(false, false)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is default user skin colours", () => player.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); + } + + [TestCase(true, true)] + [TestCase(false, true)] + [TestCase(true, false)] + [TestCase(false, false)] + public void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour) + { + ExposedPlayer player = null; + + configureSettings(useBeatmapSkin, useBeatmapColour); + AddStep("load custom-skin colour", () => player = loadBeatmap(true, false)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is custom user skin colours", () => player.UsableComboColours.SequenceEqual(TestSkin.Colours)); + } + + [TestCase(true)] + [TestCase(false)] + public void TestBeatmapHyperDashColours(bool useBeatmapSkin) + { + ExposedPlayer player = null; + + configureSettings(useBeatmapSkin, true); + AddStep("load custom-skin colour", () => player = loadBeatmap(true, true)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is custom hyper dash colours", () => player.UsableHyperDashColour == TestBeatmapSkin.HYPER_DASH_COLOUR); + AddAssert("is custom hyper dash after image colours", () => player.UsableHyperDashAfterImageColour == TestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); + AddAssert("is custom hyper dash fruit colours", () => player.UsableHyperDashFruitColour == TestBeatmapSkin.HYPER_DASH_FRUIT_COLOUR); + } + + [TestCase(true)] + [TestCase(false)] + public void TestBeatmapHyperDashColoursOverride(bool useBeatmapSkin) + { + ExposedPlayer player = null; + + configureSettings(useBeatmapSkin, false); + AddStep("load custom-skin colour", () => player = loadBeatmap(true, true)); + AddUntilStep("wait for player", () => player.IsLoaded); + + AddAssert("is custom hyper dash colours", () => player.UsableHyperDashColour == TestSkin.HYPER_DASH_COLOUR); + AddAssert("is custom hyper dash after image colours", () => player.UsableHyperDashAfterImageColour == TestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); + AddAssert("is custom hyper dash fruit colours", () => player.UsableHyperDashFruitColour == TestSkin.HYPER_DASH_FRUIT_COLOUR); + } + + private ExposedPlayer loadBeatmap(bool userHasCustomColours, bool beatmapHasColours) + { + ExposedPlayer player; + + Beatmap.Value = new CustomSkinWorkingBeatmap(audio, beatmapHasColours); + + LoadScreen(player = new ExposedPlayer(userHasCustomColours)); + + return player; + } + + private void configureSettings(bool beatmapSkins, bool beatmapColours) + { + AddStep($"{(beatmapSkins ? "enable" : "disable")} beatmap skins", () => + { + this.beatmapSkins.Value = beatmapSkins; + }); + AddStep($"{(beatmapColours ? "enable" : "disable")} beatmap colours", () => + { + this.beatmapColours.Value = beatmapColours; + }); + } + + private class ExposedPlayer : Player + { + private readonly bool userHasCustomColours; + + public ExposedPlayer(bool userHasCustomColours) + : base(new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + this.userHasCustomColours = userHasCustomColours; + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(new TestSkin(userHasCustomColours)); + return dependencies; + } + + public IReadOnlyList UsableComboColours => + GameplayClockContainer.ChildrenOfType() + .First() + .GetConfig>(GlobalSkinColours.ComboColours)?.Value; + + public Color4 UsableHyperDashColour => + GameplayClockContainer.ChildrenOfType() + .First() + .GetConfig(new SkinCustomColourLookup(CatchSkinColour.HyperDash))? + .Value ?? Color4.Red; + + public Color4 UsableHyperDashAfterImageColour => + GameplayClockContainer.ChildrenOfType() + .First() + .GetConfig(new SkinCustomColourLookup(CatchSkinColour.HyperDashAfterImage))? + .Value ?? Color4.Red; + + public Color4 UsableHyperDashFruitColour => + GameplayClockContainer.ChildrenOfType() + .First() + .GetConfig(new SkinCustomColourLookup(CatchSkinColour.HyperDashFruit))? + .Value ?? Color4.Red; + } + + private class TestJuiceStream : JuiceStream + { + public TestJuiceStream(float x) + { + X = x; + + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero), + new PathControlPoint(new Vector2(30, 0)), + }); + } + } + + private class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap + { + private readonly bool hasColours; + + public CustomSkinWorkingBeatmap(AudioManager audio, bool hasColours) + : base(createBeatmap(new CatchRuleset().RulesetInfo), null, null, audio) + { + this.hasColours = hasColours; + } + + protected override ISkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours); + + private static IBeatmap createBeatmap(RulesetInfo ruleset) + { + var beatmap = new Beatmap + { + BeatmapInfo = + { + Ruleset = ruleset, + BaseDifficulty = new BeatmapDifficulty { CircleSize = 3.6f } + } + }; + + beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); + + // Should produce a hyper-dash (edge case test) + 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 * CatchPlayfield.WIDTH; + const float right_x = 0.98f * CatchPlayfield.WIDTH; + + 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); + + beatmap.ControlPointInfo.Add(startTime, new TimingControlPoint + { + BeatLength = 50 + }); + + createObjects(() => new TestJuiceStream(left_x) + { + Path = new SliderPath(new[] + { + new PathControlPoint(Vector2.Zero), + new PathControlPoint(new Vector2(512, 0)) + }) + }, 1); + + return beatmap; + + void createObjects(Func createObject, int count = 3) + { + const float spacing = 140; + + for (int i = 0; i < count; i++) + { + var hitObject = createObject(); + hitObject.StartTime = startTime + i * spacing; + beatmap.HitObjects.Add(hitObject); + } + + startTime += 700; + } + } + } + + private class TestBeatmapSkin : LegacyBeatmapSkin + { + public static Color4[] Colours { get; } = + { + new Color4(50, 100, 150, 255), + new Color4(40, 80, 120, 255), + }; + + public static readonly Color4 HYPER_DASH_COLOUR = Color4.DarkBlue; + + public static readonly Color4 HYPER_DASH_AFTER_IMAGE_COLOUR = Color4.DarkCyan; + + public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; + + public TestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) + : base(beatmap, new ResourceStore(), null) + { + if (hasColours) + { + Configuration.AddComboColours(Colours); + Configuration.CustomColours.Add(CatchSkinColour.HyperDash.ToString(), HYPER_DASH_COLOUR); + Configuration.CustomColours.Add(CatchSkinColour.HyperDashAfterImage.ToString(), HYPER_DASH_AFTER_IMAGE_COLOUR); + Configuration.CustomColours.Add(CatchSkinColour.HyperDashFruit.ToString(), HYPER_DASH_FRUIT_COLOUR); + } + } + } + + private class TestSkin : LegacySkin, ISkinSource + { + public static Color4[] Colours { get; } = + { + new Color4(150, 100, 50, 255), + new Color4(20, 20, 20, 255), + }; + + public static readonly Color4 HYPER_DASH_COLOUR = Color4.LightBlue; + + public static readonly Color4 HYPER_DASH_AFTER_IMAGE_COLOUR = Color4.LightCoral; + + public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan; + + public TestSkin(bool hasCustomColours) + : base(new SkinInfo(), new ResourceStore(), null, string.Empty) + { + if (hasCustomColours) + { + Configuration.AddComboColours(Colours); + Configuration.CustomColours.Add(CatchSkinColour.HyperDash.ToString(), HYPER_DASH_COLOUR); + Configuration.CustomColours.Add(CatchSkinColour.HyperDashAfterImage.ToString(), HYPER_DASH_AFTER_IMAGE_COLOUR); + Configuration.CustomColours.Add(CatchSkinColour.HyperDashFruit.ToString(), HYPER_DASH_FRUIT_COLOUR); + } + } + + public event Action SourceChanged + { + add { } + remove { } + } + } + } +} diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index d2645eff68..e97822b86e 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.OpenGL.Textures; using osu.Framework.Graphics.Textures; using osu.Game.Audio; -using osuTK.Graphics; namespace osu.Game.Skinning { @@ -73,9 +72,7 @@ namespace osu.Game.Skinning { if (skin != null) { - TValue tValueTypeCheck = default; - - if (lookup is GlobalSkinColours || tValueTypeCheck is Color4) + if (lookup is GlobalSkinColours || lookup is SkinCustomColourLookup) return lookupWithFallback(lookup, AllowColourLookup); else return lookupWithFallback(lookup, AllowConfigurationLookup); From 0a65ae8f1ef72f883e9f1e324bf3cd622bd321d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 14:07:24 +0900 Subject: [PATCH 5980/6909] Fix the beatmap carousel playing the difficulty change sample on beatmap change --- osu.Game/Screens/Select/SongSelect.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 40db04ae71..6c0bd3a228 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -500,7 +500,7 @@ namespace osu.Game.Screens.Select if (beatmap != null) { - if (beatmap.BeatmapSetInfoID == beatmapNoDebounce?.BeatmapSetInfoID) + if (beatmap.BeatmapSetInfoID == previous?.BeatmapInfo.BeatmapSetInfoID) sampleChangeDifficulty.Play(); else sampleChangeBeatmap.Play(); From c6e9a6cd5a3d990b2c72fab3f82148cd65dfb6fe Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 Jan 2021 14:28:49 +0900 Subject: [PATCH 5981/6909] Make most common BPM more accurate --- osu.Game/Beatmaps/Beatmap.cs | 38 +++++++++++++++++++ osu.Game/Beatmaps/BeatmapManager.cs | 2 +- .../ControlPoints/ControlPointInfo.cs | 7 ---- osu.Game/Beatmaps/IBeatmap.cs | 5 +++ osu.Game/Screens/Edit/EditorBeatmap.cs | 2 + osu.Game/Screens/Play/GameplayBeatmap.cs | 2 + osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- 7 files changed, 49 insertions(+), 9 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index be2006e67a..410fd5e92e 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -48,6 +48,44 @@ namespace osu.Game.Beatmaps public virtual IEnumerable GetStatistics() => Enumerable.Empty(); + public double GetMostCommonBeatLength() + { + // The last playable time in the beatmap - the last timing point extends to this time. + // Note: This is more accurate and may present different results because osu-stable didn't have the ability to calculate slider durations in this context. + double lastTime = HitObjects.LastOrDefault()?.GetEndTime() ?? ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0; + + var beatLengthsAndDurations = + // Construct a set of (beatLength, duration) tuples for each individual timing point. + ControlPointInfo.TimingPoints.Select((t, i) => + { + if (t.Time > lastTime) + return (beatLength: t.BeatLength, 0); + + var nextTime = i == ControlPointInfo.TimingPoints.Count - 1 ? lastTime : ControlPointInfo.TimingPoints[i + 1].Time; + return (beatLength: t.BeatLength, duration: nextTime - t.Time); + }) + // Aggregate durations into a set of (beatLength, duration) tuples for each beat length + .GroupBy(t => t.beatLength) + .Select(g => (beatLength: g.Key, duration: g.Sum(t => t.duration))) + // And if there are no timing points, use a default. + .DefaultIfEmpty((TimingControlPoint.DEFAULT_BEAT_LENGTH, 0)); + + // Find the single beat length with the maximum aggregate duration. + double maxDurationBeatLength = double.NegativeInfinity; + double maxDuration = double.NegativeInfinity; + + foreach (var (beatLength, duration) in beatLengthsAndDurations) + { + if (duration > maxDuration) + { + maxDuration = duration; + maxDurationBeatLength = beatLength; + } + } + + return 60000 / maxDurationBeatLength; + } + IBeatmap IBeatmap.Clone() => Clone(); public Beatmap Clone() diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 42418e532b..0d4cc38ac3 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -451,7 +451,7 @@ namespace osu.Game.Beatmaps // TODO: this should be done in a better place once we actually need to dynamically update it. beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; beatmap.BeatmapInfo.Length = calculateLength(beatmap); - beatmap.BeatmapInfo.BPM = beatmap.ControlPointInfo.BPMMode; + beatmap.BeatmapInfo.BPM = beatmap.GetMostCommonBeatLength(); beatmapInfos.Add(beatmap.BeatmapInfo); } diff --git a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs index e8a91e4001..5cc60a5758 100644 --- a/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs +++ b/osu.Game/Beatmaps/ControlPoints/ControlPointInfo.cs @@ -101,13 +101,6 @@ namespace osu.Game.Beatmaps.ControlPoints public double BPMMinimum => 60000 / (TimingPoints.OrderByDescending(c => c.BeatLength).FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength; - /// - /// Finds the mode BPM (most common BPM) represented by the control points. - /// - [JsonIgnore] - public double BPMMode => - 60000 / (TimingPoints.GroupBy(c => c.BeatLength).OrderByDescending(grp => grp.Count()).FirstOrDefault()?.FirstOrDefault() ?? TimingControlPoint.DEFAULT).BeatLength; - /// /// Remove all s and return to a pristine state. /// diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 8f27e0b0e9..aaca8f7658 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -47,6 +47,11 @@ namespace osu.Game.Beatmaps /// IEnumerable GetStatistics(); + /// + /// Finds the most common beat length represented by the control points in this beatmap. + /// + double GetMostCommonBeatLength(); + /// /// Creates a shallow-clone of this beatmap and returns it. /// diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 165d2ba278..0e9008ba68 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -84,6 +84,8 @@ namespace osu.Game.Screens.Edit public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); + public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); + public IBeatmap Clone() => (EditorBeatmap)MemberwiseClone(); private IList mutableHitObjects => (IList)PlayableBeatmap.HitObjects; diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index 64894544f4..53c1360bfa 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.Play public IEnumerable GetStatistics() => PlayableBeatmap.GetStatistics(); + public double GetMostCommonBeatLength() => PlayableBeatmap.GetMostCommonBeatLength(); + public IBeatmap Clone() => PlayableBeatmap.Clone(); private readonly Bindable lastJudgementResult = new Bindable(); diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 04c1f6efe4..abcd697d85 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -391,7 +391,7 @@ namespace osu.Game.Screens.Select if (Precision.AlmostEquals(bpmMin, bpmMax)) return $"{bpmMin:0}"; - return $"{bpmMin:0}-{bpmMax:0} (mostly {beatmap.ControlPointInfo.BPMMode:0})"; + return $"{bpmMin:0}-{bpmMax:0} (mostly {beatmap.GetMostCommonBeatLength():0})"; } private OsuSpriteText[] getMapper(BeatmapMetadata metadata) From 24e991a5ef9c1797e4bebddf1d0a2f623845a40f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 Jan 2021 14:32:06 +0900 Subject: [PATCH 5982/6909] Actually return beat length and not BPM --- osu.Game/Beatmaps/Beatmap.cs | 2 +- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 410fd5e92e..4e2e9eb96d 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -83,7 +83,7 @@ namespace osu.Game.Beatmaps } } - return 60000 / maxDurationBeatLength; + return maxDurationBeatLength; } IBeatmap IBeatmap.Clone() => Clone(); diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 0d4cc38ac3..b934ac556d 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -451,7 +451,7 @@ namespace osu.Game.Beatmaps // TODO: this should be done in a better place once we actually need to dynamically update it. beatmap.BeatmapInfo.StarDifficulty = ruleset?.CreateInstance().CreateDifficultyCalculator(new DummyConversionBeatmap(beatmap)).Calculate().StarRating ?? 0; beatmap.BeatmapInfo.Length = calculateLength(beatmap); - beatmap.BeatmapInfo.BPM = beatmap.GetMostCommonBeatLength(); + beatmap.BeatmapInfo.BPM = 60000 / beatmap.GetMostCommonBeatLength(); beatmapInfos.Add(beatmap.BeatmapInfo); } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index abcd697d85..86cb561bc7 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -391,7 +391,7 @@ namespace osu.Game.Screens.Select if (Precision.AlmostEquals(bpmMin, bpmMax)) return $"{bpmMin:0}"; - return $"{bpmMin:0}-{bpmMax:0} (mostly {beatmap.GetMostCommonBeatLength():0})"; + return $"{bpmMin:0}-{bpmMax:0} (mostly {60000 / beatmap.GetMostCommonBeatLength():0})"; } private OsuSpriteText[] getMapper(BeatmapMetadata metadata) From ebbc32adfa9c4652cadff32dc5c429af9867ff09 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 14:51:26 +0900 Subject: [PATCH 5983/6909] Change conditional used to decide legacy judgement animation to match stable In stable, the type of legacy judgement to show is based on the presence of particle textures in the skin. We were using the skin version instead, which turns out to be incorrect and not what some user skins expect. Closes #11078. --- osu.Game/Skinning/LegacySkin.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index e4e5bf2f75..7397e3d08b 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -378,8 +378,12 @@ namespace osu.Game.Skinning // kind of wasteful that we throw this away, but should do for now. if (createDrawable() != null) { - if (Configuration.LegacyVersion > 1) - return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, getParticleTexture(resultComponent.Component)); + var particle = getParticleTexture(resultComponent.Component); + + if (particle != null) + { + return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, particle); + } else return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable); } From 86f66727de47175b8045523c56f719a8051041a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 13:29:37 +0900 Subject: [PATCH 5984/6909] Update KeyBinding usages in line with interface changes --- osu.Game/Database/OsuDbContext.cs | 2 ++ osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs | 2 +- osu.Game/Input/Bindings/GlobalActionContainer.cs | 2 +- osu.Game/Input/KeyBindingStore.cs | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 2ae07b3cf8..2aae62edea 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -135,6 +135,8 @@ namespace osu.Game.Database modelBuilder.Entity().HasIndex(b => new { b.RulesetID, b.Variant }); modelBuilder.Entity().HasIndex(b => b.IntAction); + modelBuilder.Entity().Ignore(b => b.KeyCombination); + modelBuilder.Entity().Ignore(b => b.Action); modelBuilder.Entity().HasIndex(b => new { b.RulesetID, b.Variant }); diff --git a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs index 94edc33099..d12eaa10f6 100644 --- a/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs +++ b/osu.Game/Input/Bindings/DatabasedKeyBindingContainer.cs @@ -23,7 +23,7 @@ namespace osu.Game.Input.Bindings private KeyBindingStore store; - public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); + public override IEnumerable DefaultKeyBindings => ruleset.CreateInstance().GetDefaultKeyBindings(variant ?? 0); /// /// Create a new instance. diff --git a/osu.Game/Input/Bindings/GlobalActionContainer.cs b/osu.Game/Input/Bindings/GlobalActionContainer.cs index b8c2fa201f..8ccdb9249e 100644 --- a/osu.Game/Input/Bindings/GlobalActionContainer.cs +++ b/osu.Game/Input/Bindings/GlobalActionContainer.cs @@ -21,7 +21,7 @@ namespace osu.Game.Input.Bindings handler = game; } - public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings); + public override IEnumerable DefaultKeyBindings => GlobalKeyBindings.Concat(InGameKeyBindings).Concat(AudioControlKeyBindings).Concat(EditorKeyBindings); public IEnumerable GlobalKeyBindings => new[] { diff --git a/osu.Game/Input/KeyBindingStore.cs b/osu.Game/Input/KeyBindingStore.cs index bc73d74d74..b25b00eb84 100644 --- a/osu.Game/Input/KeyBindingStore.cs +++ b/osu.Game/Input/KeyBindingStore.cs @@ -49,7 +49,7 @@ namespace osu.Game.Input } } - private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) + private void insertDefaults(IEnumerable defaults, int? rulesetId = null, int? variant = null) { using (var usage = ContextFactory.GetForWrite()) { From 51255033e24c2afbbac85573ef5e7edf610eb920 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 13:41:35 +0900 Subject: [PATCH 5985/6909] Update some missed usages of KeyBindingContainer in tests --- osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs | 2 +- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs index b2ad7ca5b4..802dbf2021 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecorder.cs @@ -244,7 +244,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestKeyBindingContainer : KeyBindingContainer { - public override IEnumerable DefaultKeyBindings => new[] + public override IEnumerable DefaultKeyBindings => new[] { new KeyBinding(InputKey.MouseLeft, TestAction.Down), }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs index 40c4214749..6e338b7202 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneReplayRecording.cs @@ -179,7 +179,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestKeyBindingContainer : KeyBindingContainer { - public override IEnumerable DefaultKeyBindings => new[] + public override IEnumerable DefaultKeyBindings => new[] { new KeyBinding(InputKey.MouseLeft, TestAction.Down), }; diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index b9ff95cb29..8278ff9adf 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -303,7 +303,7 @@ namespace osu.Game.Tests.Visual.Gameplay internal class TestKeyBindingContainer : KeyBindingContainer { - public override IEnumerable DefaultKeyBindings => new[] + public override IEnumerable DefaultKeyBindings => new[] { new KeyBinding(InputKey.MouseLeft, TestAction.Down), }; From f42a6270bbd750e2539ddad3ff97c0adb29ee521 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 14:53:55 +0900 Subject: [PATCH 5986/6909] Update framework (again) for native libs fix --- 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 919d83f8db..db5c933c41 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 ac014f2964..5e9e90c78f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -28,7 +28,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 799042626b..225cf981f2 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From e0a4a666c8e5fa8224fae065d3597b49fa747103 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 15:01:16 +0900 Subject: [PATCH 5987/6909] Remove unnecessary workaround (mentioned package is pinned by SignalR to a working version) --- osu.Game/osu.Game.csproj | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 5e9e90c78f..301ee39a61 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,8 +18,6 @@ - - From 7c612ec5561225b2a618bf11915aa693e20e7cf6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 15:11:03 +0900 Subject: [PATCH 5988/6909] Remove global.json --- global.json | 10 ---------- osu.sln | 1 - 2 files changed, 11 deletions(-) delete mode 100644 global.json diff --git a/global.json b/global.json deleted file mode 100644 index f5aaffcd3d..0000000000 --- a/global.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sdk": { - "allowPrerelease": false, - "rollForward": "minor", - "version": "5.0.100" - }, - "msbuild-sdks": { - "Microsoft.Build.Traversal": "3.0.2" - } -} \ No newline at end of file diff --git a/osu.sln b/osu.sln index 1d64f6ff10..c9453359b1 100644 --- a/osu.sln +++ b/osu.sln @@ -57,7 +57,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig Directory.Build.props = Directory.Build.props - global.json = global.json osu.Android.props = osu.Android.props osu.iOS.props = osu.iOS.props CodeAnalysis\osu.ruleset = CodeAnalysis\osu.ruleset From 3f8834030416aeb80f5746a37999a83c16064d86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 15:17:38 +0900 Subject: [PATCH 5989/6909] Restore previous exception handling flow for stable path lookup --- osu.Desktop/OsuGameDesktop.cs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 55e42b160e..d1515acafa 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -59,10 +59,14 @@ namespace osu.Desktop if (OperatingSystem.IsWindows()) { - stableInstallPath = getStableInstallPathFromRegistry(); + try + { + stableInstallPath = getStableInstallPathFromRegistry(); - if (checkExists(stableInstallPath)) - return stableInstallPath; + if (!string.IsNullOrEmpty(stableInstallPath) && checkExists(stableInstallPath)) + return stableInstallPath; + } + catch { } } stableInstallPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), @"osu!"); From d023ad8ad1a03b500f13b90ca5d1d24a75e182ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 15:18:29 +0900 Subject: [PATCH 5990/6909] Remove assert messages --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs index 8278ff9adf..35b3bfc1f8 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectatorPlayback.cs @@ -75,7 +75,7 @@ namespace osu.Game.Tests.Visual.Gameplay switch (args.Action) { case NotifyCollectionChangedAction.Add: - Debug.Assert(args.NewItems != null, "args.NewItems != null"); + Debug.Assert(args.NewItems != null); foreach (int user in args.NewItems) { @@ -86,7 +86,7 @@ namespace osu.Game.Tests.Visual.Gameplay break; case NotifyCollectionChangedAction.Remove: - Debug.Assert(args.OldItems != null, "args.OldItems != null"); + Debug.Assert(args.OldItems != null); foreach (int user in args.OldItems) { From b8c85ef017184b8672adb80401375bd3c8c8e224 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 16:03:12 +0900 Subject: [PATCH 5991/6909] Revert polling changes to fix participant list display It turns out this polling was necessary to get extra data that isn't included in the main listing request. It was removed deemed useless, and in order to fix the order of rooms changing when selecting a room. Weirdly, I can't reproduce this happening any more, and on close inspection of the code can't see how it could happen in the first place. For now, let's revert this change and iterate from there, if/when the same issue arises again. I've discussed avoiding this second poll by potentially including more data (just `user_id`s?) in the main listing request, but not 100% sure on this - even if the returned data is minimal it's an extra join server-side, which could cause performance issues for large numbers of rooms. --- .../TestSceneMultiplayerRoomManager.cs | 1 + .../OnlinePlay/Multiplayer/Multiplayer.cs | 5 +++- .../Multiplayer/MultiplayerRoomManager.cs | 28 ++++++++++++++++++- 3 files changed, 32 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index 80d1acd145..7a3845cbf3 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -143,6 +143,7 @@ namespace osu.Game.Tests.Visual.Multiplayer RoomManager = { TimeBetweenListingPolls = { Value = 1 }, + TimeBetweenSelectionPolls = { Value = 1 } } }; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 310617a0bc..76f5c74433 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -33,6 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen()) { multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; } else { @@ -40,16 +41,18 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { case LoungeSubScreen _: multiplayerRoomManager.TimeBetweenListingPolls.Value = isIdle ? 120000 : 15000; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = isIdle ? 120000 : 15000; break; // Don't poll inside the match or anywhere else. default: multiplayerRoomManager.TimeBetweenListingPolls.Value = 0; + multiplayerRoomManager.TimeBetweenSelectionPolls.Value = 0; break; } } - Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value})"); + Logger.Log($"Polling adjusted (listing: {multiplayerRoomManager.TimeBetweenListingPolls.Value}, selection: {multiplayerRoomManager.TimeBetweenSelectionPolls.Value})"); } protected override Room CreateNewRoom() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index 5c327266a3..3cb263298f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -23,7 +23,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private StatefulMultiplayerClient multiplayerClient { get; set; } public readonly Bindable TimeBetweenListingPolls = new Bindable(); - + public readonly Bindable TimeBetweenSelectionPolls = new Bindable(); private readonly IBindable isConnected = new Bindable(); private readonly Bindable allowPolling = new Bindable(); @@ -119,6 +119,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer TimeBetweenPolls = { BindTarget = TimeBetweenListingPolls }, AllowPolling = { BindTarget = allowPolling } }, + new MultiplayerSelectionPollingComponent + { + TimeBetweenPolls = { BindTarget = TimeBetweenSelectionPolls }, + AllowPolling = { BindTarget = allowPolling } + } }; private class MultiplayerListingPollingComponent : ListingPollingComponent @@ -141,5 +146,26 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); } + + private class MultiplayerSelectionPollingComponent : SelectionPollingComponent + { + public readonly IBindable AllowPolling = new Bindable(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + AllowPolling.BindValueChanged(allowPolling => + { + if (!allowPolling.NewValue) + return; + + if (IsLoaded) + PollImmediately(); + }); + } + + protected override Task Poll() => !AllowPolling.Value ? Task.CompletedTask : base.Poll(); + } } } From ede5abdba4eb0f88806c38790c7a622546f70af9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 12 Jan 2021 18:05:29 +0900 Subject: [PATCH 5992/6909] Fix unstable multiplayer room ordering when selection is made --- .../OnlinePlay/Components/SelectionPollingComponent.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs index 0eec155060..dcf3c94b76 100644 --- a/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.cs +++ b/osu.Game/Screens/OnlinePlay/Components/SelectionPollingComponent.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 System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -47,9 +48,11 @@ namespace osu.Game.Screens.OnlinePlay.Components pollReq.Success += result => { - var rooms = new List(roomManager.Rooms); + // existing rooms need to be ordered by their position because the received of NotifyRoomsReceives expects to be able to sort them based on this order. + var rooms = new List(roomManager.Rooms.OrderBy(r => r.Position.Value)); int index = rooms.FindIndex(r => r.RoomID.Value == result.RoomID.Value); + if (index < 0) return; From 2b578e97e532fc0bb215566b8bd34df25d1d4625 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 14 Jan 2021 18:25:32 +0900 Subject: [PATCH 5993/6909] Fix deadlock scenario when calculating fallback difficulty The previous code would run a calcaulation for the beatmap's own ruleset if the current one failed. While this does make sense, with the current way we use this component (and the implementation flow) it is quite unsafe. The to the call on `.Result` in the `catch` block, this would 100% deadlock due to the thread concurrency of the `ThreadedTaskScheduler` being 1. Even if the nested run could be run inline (it should be), the task scheduler won't even get to the point of checking whether this is feasible due to it being saturated by the already running task. I'm not sure if we still need this fallback lookup logic. After removing it, it's feasible that 0 stars will be returned during the scenario that previously caused a deadlock, but I don't necessarily think this is incorrect. There may be another reason for this needing to exist which I'm not aware of (diffcalc?) but if that's the case we may want to move the try-catch handling to the point of usage. To reproduce the deadlock scenario with 100% success (the repro instructions in the linked issue aren't that simple and require some patience and good timing), the main portion of the lookup can be changed to randomly trigger a nested lookup: ``` if (RNG.NextSingle() > 0.5f) return GetAsync(new DifficultyCacheLookup(key.Beatmap, key.Beatmap.Ruleset, key.OrderedMods)).Result; else return new StarDifficulty(attributes); ``` After switching beatmap once or twice, pausing debug and viewing the state of threads should show exactly what is going on. --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 3b58062add..37d262abe5 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -260,17 +260,10 @@ namespace osu.Game.Beatmaps } catch (BeatmapInvalidForRulesetException e) { - // Conversion has failed for the given ruleset, so return the difficulty in the beatmap's default ruleset. - - // Ensure the beatmap's default ruleset isn't the one already being converted to. - // This shouldn't happen as it means something went seriously wrong, but if it does an endless loop should be avoided. if (rulesetInfo.Equals(beatmapInfo.Ruleset)) - { Logger.Error(e, $"Failed to convert {beatmapInfo.OnlineBeatmapID} to the beatmap's default ruleset ({beatmapInfo.Ruleset})."); - return new StarDifficulty(); - } - return GetAsync(new DifficultyCacheLookup(key.Beatmap, key.Beatmap.Ruleset, key.OrderedMods)).Result; + return new StarDifficulty(); } catch { From ed78be825f9a988df3920fdbda730120bebfe125 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 15:47:41 +0900 Subject: [PATCH 5994/6909] Fix editor timeline not snapping on non-precise wheel scroll For wheel input with precision, we still prefer exact tracking for now. May change this in the future based on feedback from mappers, but it makes little sense to do non-snapped scrolling when input is coming from a non-precise source. --- .../Screens/Edit/Compose/Components/Timeline/Timeline.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 20836c0e68..12f7625bf9 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -138,6 +138,15 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline scrollToTrackTime(); } + protected override bool OnScroll(ScrollEvent e) + { + // if this is not a precision scroll event, let the editor handle the seek itself (for snapping support) + if (!e.AltPressed && !e.IsPrecise) + return false; + + return base.OnScroll(e); + } + protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); From 04fa32bc34852ba79a249529f627b9a6f8aa6dd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 16:14:21 +0900 Subject: [PATCH 5995/6909] Rename and add xmldoc for smooth seeking method --- osu.Game/Rulesets/Edit/HitObjectComposer.cs | 2 +- .../Timelines/Summary/Parts/MarkerPart.cs | 2 +- .../Compose/Components/BlueprintContainer.cs | 2 +- osu.Game/Screens/Edit/EditorClock.cs | 33 +++++++++++-------- .../Screens/Edit/Timing/ControlPointTable.cs | 2 +- 5 files changed, 24 insertions(+), 17 deletions(-) diff --git a/osu.Game/Rulesets/Edit/HitObjectComposer.cs b/osu.Game/Rulesets/Edit/HitObjectComposer.cs index 35852f60ea..e927951d0a 100644 --- a/osu.Game/Rulesets/Edit/HitObjectComposer.cs +++ b/osu.Game/Rulesets/Edit/HitObjectComposer.cs @@ -332,7 +332,7 @@ namespace osu.Game.Rulesets.Edit EditorBeatmap.Add(hitObject); if (EditorClock.CurrentTime < hitObject.StartTime) - EditorClock.SeekTo(hitObject.StartTime); + EditorClock.SeekSmoothlyTo(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 9e9ac93d23..5a2214509c 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.SeekTo(markerPos / DrawWidth * editorClock.TrackLength); + editorClock.SeekSmoothlyTo(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 0b45bd5597..5371beac60 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.Edit.Compose.Components if (clickedBlueprint == null || SelectionHandler.SelectedBlueprints.FirstOrDefault(b => b.IsHovered) != clickedBlueprint) return false; - EditorClock?.SeekTo(clickedBlueprint.HitObject.StartTime); + EditorClock?.SeekSmoothlyTo(clickedBlueprint.HitObject.StartTime); return true; } diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index 148eef6c93..c651d6a7c4 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.Edit if (!snapped || ControlPointInfo.TimingPoints.Count == 0) { - SeekTo(seekTime); + SeekSmoothlyTo(seekTime); return; } @@ -145,11 +145,11 @@ namespace osu.Game.Screens.Edit // Ensure the sought point is within the boundaries seekTime = Math.Clamp(seekTime, 0, TrackLength); - SeekTo(seekTime); + SeekSmoothlyTo(seekTime); } /// - /// The current time of this clock, include any active transform seeks performed via . + /// The current time of this clock, include any active transform seeks performed via . /// public double CurrentTimeAccurate => Transforms.OfType().FirstOrDefault()?.EndValue ?? CurrentTime; @@ -182,6 +182,23 @@ namespace osu.Game.Screens.Edit return underlyingClock.Seek(position); } + /// + /// Seek smoothly to the provided destination. + /// Use to perform an immediate seek. + /// + /// + public void SeekSmoothlyTo(double seekDestination) + { + seekingOrStopped.Value = true; + + if (IsRunning) + Seek(seekDestination); + else + { + transformSeekTo(seekDestination, transform_time, Easing.OutQuint); + } + } + public void ResetSpeedAdjustments() => underlyingClock.ResetSpeedAdjustments(); double IAdjustableClock.Rate @@ -243,16 +260,6 @@ namespace osu.Game.Screens.Edit } } - public void SeekTo(double seekDestination) - { - seekingOrStopped.Value = true; - - 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)); diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 89d3c36250..e4b9150df1 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -206,7 +206,7 @@ namespace osu.Game.Screens.Edit.Timing Action = () => { selectedGroup.Value = controlGroup; - clock.SeekTo(controlGroup.Time); + clock.SeekSmoothlyTo(controlGroup.Time); }; } From 831c06a3c7c02549fe9bab75ff83afaaf30fa1ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 16:14:38 +0900 Subject: [PATCH 5996/6909] Expose and consume boolean covering whether an ongoing smooth seek is running --- .../Edit/Compose/Components/Timeline/Timeline.cs | 10 ++++++---- osu.Game/Screens/Edit/EditorClock.cs | 11 +++++++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs index 20836c0e68..7df4f1ae7d 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -146,12 +146,14 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline seekTrackToCurrent(); 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 track isn't running. There are three cases we have to be wary of: + // 1) The user flick-drags on this timeline and we are applying an interpolated seek on the clock, until interrupted by 2 or 3. + // 2) The user changes the track time through some other means (scrolling in the editor or overview timeline; clicking a hitobject etc.). We want the timeline to track the clock's time. + // 3) An ongoing seek transform is running from an external seek. We want the timeline to track the clock's 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 && editorClock.CurrentTime == lastTrackTime) + // Checking IsSeeking covers the third case, where the transform may not have been applied yet. + if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime && !editorClock.IsSeeking) seekTrackToCurrent(); else scrollToTrackTime(); diff --git a/osu.Game/Screens/Edit/EditorClock.cs b/osu.Game/Screens/Edit/EditorClock.cs index c651d6a7c4..ec0f5d7154 100644 --- a/osu.Game/Screens/Edit/EditorClock.cs +++ b/osu.Game/Screens/Edit/EditorClock.cs @@ -35,6 +35,11 @@ namespace osu.Game.Screens.Edit private readonly Bindable seekingOrStopped = new Bindable(true); + /// + /// Whether a seek is currently in progress. True for the duration of a seek performed via . + /// + public bool IsSeeking { get; private set; } + public EditorClock(WorkingBeatmap beatmap, BindableBeatDivisor beatDivisor) : this(beatmap.Beatmap.ControlPointInfo, beatmap.Track.Length, beatDivisor) { @@ -176,7 +181,7 @@ namespace osu.Game.Screens.Edit public bool Seek(double position) { - seekingOrStopped.Value = true; + seekingOrStopped.Value = IsSeeking = true; ClearTransforms(); return underlyingClock.Seek(position); @@ -246,6 +251,8 @@ namespace osu.Game.Screens.Edit { if (seekingOrStopped.Value) { + IsSeeking &= Transforms.Any(); + if (track.Value?.IsRunning != true) { // seeking in the editor can happen while the track isn't running. @@ -256,7 +263,7 @@ namespace osu.Game.Screens.Edit // we are either running a seek tween or doing an immediate seek. // in the case of an immediate seek the seeking bool will be set to false after one update. // this allows for silencing hit sounds and the likes. - seekingOrStopped.Value = Transforms.Any(); + seekingOrStopped.Value = IsSeeking; } } From b5e784ed427a47c93eae96a2a459b6a4b9dc224c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 16:34:28 +0900 Subject: [PATCH 5997/6909] Fix possibility of crash when selecting a random skin during skin import --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5898482e4a..123cecb0cd 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -91,7 +91,7 @@ namespace osu.Game.Overlays.Settings.Sections if (skinDropdown.Items.All(s => s.ID != configBindable.Value)) configBindable.Value = 0; - configBindable.BindValueChanged(id => dropdownBindable.Value = skinDropdown.Items.Single(s => s.ID == id.NewValue), true); + configBindable.BindValueChanged(id => Scheduler.AddOnce(updateSelectedSkinFromConfig), true); dropdownBindable.BindValueChanged(skin => { if (skin.NewValue == random_skin_info) @@ -104,6 +104,8 @@ namespace osu.Game.Overlays.Settings.Sections }); } + private void updateSelectedSkinFromConfig() => dropdownBindable.Value = skinDropdown.Items.Single(s => s.ID == configBindable.Value); + private void updateItems() { skinItems = skins.GetAllUsableSkins(); From 88a27124c095f5a3cff7eaddaaab92cfe1832d55 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 17:13:27 +0900 Subject: [PATCH 5998/6909] Make long spinner test longer and fix step name --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index 496b1b3559..c22b1dc407 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu.Tests [TestCase(true)] public void TestLongSpinner(bool autoplay) { - AddStep("Very short spinner", () => SetContents(() => testSingle(5, autoplay, 2000))); + AddStep("Very long spinner", () => SetContents(() => testSingle(5, autoplay, 4000))); AddUntilStep("Wait for completion", () => drawableSpinner.Result.HasResult); AddUntilStep("Check correct progress", () => drawableSpinner.Progress == (autoplay ? 1 : 0)); } From 6adb6b6700b4b19336955b397d5939679919d70b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 17:13:52 +0900 Subject: [PATCH 5999/6909] Fix spinner tests not playing spinning sound due to empty hitsamples --- osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs index c22b1dc407..f697a77d94 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneSpinner.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.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Mods; @@ -55,7 +57,11 @@ namespace osu.Game.Rulesets.Osu.Tests var spinner = new Spinner { StartTime = Time.Current + delay, - EndTime = Time.Current + delay + length + EndTime = Time.Current + delay + length, + Samples = new List + { + new HitSampleInfo("hitnormal") + } }; spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = circleSize }); From 8a6857f151373dc2bb6edeb5239efef9932e447f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 17:16:12 +0900 Subject: [PATCH 6000/6909] Add support for playing a SkinnableSample without restarting it --- osu.Game/Skinning/PausableSkinnableSound.cs | 4 ++-- osu.Game/Skinning/SkinnableSound.cs | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 4b6099e85f..cb5234c847 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -67,7 +67,7 @@ namespace osu.Game.Skinning } } - public override void Play() + public override void Play(bool restart = true) { cancelPendingStart(); RequestedPlaying = true; @@ -75,7 +75,7 @@ namespace osu.Game.Skinning if (samplePlaybackDisabled.Value) return; - base.Play(); + base.Play(restart); } public override void Stop() diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 645c08cd00..b841f99598 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -119,12 +119,13 @@ namespace osu.Game.Skinning /// /// Plays the samples. /// - public virtual void Play() + /// Whether to play the sample from the beginning. + public virtual void Play(bool restart = true) { samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) - c.Play(); + c.Play(restart); }); } From 767c76921faa69b9f648e14fec3ab95f466abce1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 17:17:10 +0900 Subject: [PATCH 6001/6909] Adjust transition time of spinner sound --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 1f3bcece0c..4f61ab4a9a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -131,11 +131,11 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (tracking.NewValue) { spinningSample?.Play(); - spinningSample?.VolumeTo(1, 200); + spinningSample?.VolumeTo(1, 300); } else { - spinningSample?.VolumeTo(0, 200).Finally(_ => spinningSample.Stop()); + spinningSample?.VolumeTo(0, 300).Finally(_ => spinningSample.Stop()); } } From 311f8b70178cb5ebf0fdc23824fdae53b8336a7f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 17:17:51 +0900 Subject: [PATCH 6002/6909] Only restart spinning sample if it was not already playing --- 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 4f61ab4a9a..c5ae195274 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -130,7 +130,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { if (tracking.NewValue) { - spinningSample?.Play(); + spinningSample?.Play(!spinningSample.IsPlaying); spinningSample?.VolumeTo(1, 300); } else From 14b33236828438a72b5c6b4acc4edbba9e26d985 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 17:18:15 +0900 Subject: [PATCH 6003/6909] Use OnComplete instead of Finally to avoid potentially stopping on aborted transforms --- 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 c5ae195274..56aedebed3 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -135,7 +135,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } else { - spinningSample?.VolumeTo(0, 300).Finally(_ => spinningSample.Stop()); + spinningSample?.VolumeTo(0, 300).OnComplete(_ => spinningSample.Stop()); } } From d6e6b4bbeea9ec532b7d927fa3d38c7e1efbf49f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 15 Jan 2021 17:34:01 +0900 Subject: [PATCH 6004/6909] Revert forced cloning of ControlPointInfo This reverts commit 3c3e860dbc34d37855b79786a1abb754af1667e8. Closes https://github.com/ppy/osu/issues/11491. --- osu.Game/Beatmaps/Beatmap.cs | 10 +--------- osu.Game/Beatmaps/IBeatmap.cs | 2 +- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 ++ osu.Game/Screens/Edit/EditorBeatmap.cs | 6 +++++- osu.Game/Screens/Play/GameplayBeatmap.cs | 6 +++++- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index be2006e67a..5435e86dfd 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -50,15 +50,7 @@ namespace osu.Game.Beatmaps IBeatmap IBeatmap.Clone() => Clone(); - public Beatmap Clone() - { - var clone = (Beatmap)MemberwiseClone(); - - clone.ControlPointInfo = ControlPointInfo.CreateCopy(); - // todo: deep clone other elements as required. - - return clone; - } + public Beatmap Clone() => (Beatmap)MemberwiseClone(); } public class Beatmap : Beatmap diff --git a/osu.Game/Beatmaps/IBeatmap.cs b/osu.Game/Beatmaps/IBeatmap.cs index 8f27e0b0e9..7dd85e1232 100644 --- a/osu.Game/Beatmaps/IBeatmap.cs +++ b/osu.Game/Beatmaps/IBeatmap.cs @@ -24,7 +24,7 @@ namespace osu.Game.Beatmaps /// /// The control points in this beatmap. /// - ControlPointInfo ControlPointInfo { get; } + ControlPointInfo ControlPointInfo { get; set; } /// /// The breaks in this beatmap. diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 30382c444f..d25adca92b 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -111,6 +111,8 @@ namespace osu.Game.Beatmaps // Convert IBeatmap converted = converter.Convert(cancellationSource.Token); + converted.ControlPointInfo = converted.ControlPointInfo.CreateCopy(); + // Apply conversion mods to the result foreach (var mod in mods.OfType()) { diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 165d2ba278..a54a95f59d 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -74,7 +74,11 @@ namespace osu.Game.Screens.Edit public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; - public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo; + public ControlPointInfo ControlPointInfo + { + get => PlayableBeatmap.ControlPointInfo; + set => PlayableBeatmap.ControlPointInfo = value; + } public List Breaks => PlayableBeatmap.Breaks; diff --git a/osu.Game/Screens/Play/GameplayBeatmap.cs b/osu.Game/Screens/Play/GameplayBeatmap.cs index 64894544f4..565595656f 100644 --- a/osu.Game/Screens/Play/GameplayBeatmap.cs +++ b/osu.Game/Screens/Play/GameplayBeatmap.cs @@ -29,7 +29,11 @@ namespace osu.Game.Screens.Play public BeatmapMetadata Metadata => PlayableBeatmap.Metadata; - public ControlPointInfo ControlPointInfo => PlayableBeatmap.ControlPointInfo; + public ControlPointInfo ControlPointInfo + { + get => PlayableBeatmap.ControlPointInfo; + set => PlayableBeatmap.ControlPointInfo = value; + } public List Breaks => PlayableBeatmap.Breaks; From 3c1a86d11deefc694a00ebad3369c563a07168de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 15 Jan 2021 22:04:45 +0100 Subject: [PATCH 6005/6909] Trim braces for consistency --- osu.Game/Skinning/LegacySkin.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 7397e3d08b..090ffaebd7 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -381,9 +381,7 @@ namespace osu.Game.Skinning var particle = getParticleTexture(resultComponent.Component); if (particle != null) - { return new LegacyJudgementPieceNew(resultComponent.Component, createDrawable, particle); - } else return new LegacyJudgementPieceOld(resultComponent.Component, createDrawable); } From 112967c1e8d0fd75c24aed6276afd95140687e79 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Fri, 15 Jan 2021 23:46:46 -0500 Subject: [PATCH 6006/6909] Created base class for testing beatmap colours. --- .../TestSceneLegacyBeatmapSkin.cs | 191 +++++------------- .../TestSceneLegacyBeatmapSkin.cs | 178 +++++----------- .../Beatmaps/LegacyBeatmapSkinColourTest.cs | 147 ++++++++++++++ 3 files changed, 245 insertions(+), 271 deletions(-) create mode 100644 osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs index 12ceaaa6fa..89298242e5 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.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.Allocation; using osu.Framework.Audio; -using osu.Framework.Bindables; -using osu.Framework.IO.Stores; using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; @@ -17,179 +14,114 @@ using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Skinning; using osu.Game.Rulesets.Catch.UI; using osu.Game.Rulesets.Objects; -using osu.Game.Screens.Play; using osu.Game.Skinning; -using osu.Game.Tests.Visual; +using osu.Game.Tests.Beatmaps; using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Tests { - public class TestSceneLegacyBeatmapSkin : ScreenTestScene + public class TestSceneLegacyBeatmapSkin : LegacyBeatmapSkinColourTest { [Resolved] private AudioManager audio { get; set; } - private readonly Bindable beatmapSkins = new Bindable(); - private readonly Bindable beatmapColours = new Bindable(); - [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); - config.BindWith(OsuSetting.BeatmapColours, beatmapColours); + config.BindWith(OsuSetting.BeatmapSkins, BeatmapSkins); + config.BindWith(OsuSetting.BeatmapColours, BeatmapColours); } [TestCase(true, true)] [TestCase(true, false)] [TestCase(false, true)] [TestCase(false, false)] - public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin) + public override void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, true); - AddStep("load coloured beatmap", () => player = loadBeatmap(userHasCustomColours, true)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is beatmap skin colours", () => player.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours)); + TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); + base.TestBeatmapComboColours(userHasCustomColours, useBeatmapSkin); + AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours)); } [TestCase(true)] [TestCase(false)] - public void TestBeatmapComboColoursOverride(bool useBeatmapSkin) + public override void TestBeatmapComboColoursOverride(bool useBeatmapSkin) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, false); - AddStep("load coloured beatmap", () => player = loadBeatmap(true, true)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is user custom skin colours", () => player.UsableComboColours.SequenceEqual(TestSkin.Colours)); + TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); + base.TestBeatmapComboColoursOverride(useBeatmapSkin); + AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours)); } [TestCase(true)] [TestCase(false)] - public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin) + public override void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, false); - AddStep("load coloured beatmap", () => player = loadBeatmap(false, true)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is default user skin colours", () => player.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); + TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); + base.TestBeatmapComboColoursOverrideWithDefaultColours(useBeatmapSkin); + AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); } [TestCase(true, true)] [TestCase(false, true)] [TestCase(true, false)] [TestCase(false, false)] - public void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour) + public override void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, useBeatmapColour); - AddStep("load no-colour beatmap", () => player = loadBeatmap(false, false)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is default user skin colours", () => player.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); + TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false); + base.TestBeatmapNoComboColours(useBeatmapSkin, useBeatmapColour); + AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); } [TestCase(true, true)] [TestCase(false, true)] [TestCase(true, false)] [TestCase(false, false)] - public void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour) + public override void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, useBeatmapColour); - AddStep("load custom-skin colour", () => player = loadBeatmap(true, false)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is custom user skin colours", () => player.UsableComboColours.SequenceEqual(TestSkin.Colours)); + TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, false); + base.TestBeatmapNoComboColoursSkinOverride(useBeatmapSkin, useBeatmapColour); + AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours)); } [TestCase(true)] [TestCase(false)] public void TestBeatmapHyperDashColours(bool useBeatmapSkin) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, true); - AddStep("load custom-skin colour", () => player = loadBeatmap(true, true)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is custom hyper dash colours", () => player.UsableHyperDashColour == TestBeatmapSkin.HYPER_DASH_COLOUR); - AddAssert("is custom hyper dash after image colours", () => player.UsableHyperDashAfterImageColour == TestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); - AddAssert("is custom hyper dash fruit colours", () => player.UsableHyperDashFruitColour == TestBeatmapSkin.HYPER_DASH_FRUIT_COLOUR); + TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); + ConfigureTest(useBeatmapSkin, true, true); + AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == CatchTestBeatmapSkin.HYPER_DASH_COLOUR); + AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == CatchTestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); + AddAssert("is custom hyper dash fruit colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashFruitColour == CatchTestBeatmapSkin.HYPER_DASH_FRUIT_COLOUR); } [TestCase(true)] [TestCase(false)] public void TestBeatmapHyperDashColoursOverride(bool useBeatmapSkin) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, false); - AddStep("load custom-skin colour", () => player = loadBeatmap(true, true)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is custom hyper dash colours", () => player.UsableHyperDashColour == TestSkin.HYPER_DASH_COLOUR); - AddAssert("is custom hyper dash after image colours", () => player.UsableHyperDashAfterImageColour == TestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); - AddAssert("is custom hyper dash fruit colours", () => player.UsableHyperDashFruitColour == TestSkin.HYPER_DASH_FRUIT_COLOUR); + TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); + ConfigureTest(useBeatmapSkin, false, true); + AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == CatchTestSkin.HYPER_DASH_COLOUR); + AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == CatchTestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); + AddAssert("is custom hyper dash fruit colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashFruitColour == CatchTestSkin.HYPER_DASH_FRUIT_COLOUR); } - private ExposedPlayer loadBeatmap(bool userHasCustomColours, bool beatmapHasColours) + protected override ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new CatchExposedPlayer(userHasCustomColours); + + private class CatchExposedPlayer : ExposedPlayer { - ExposedPlayer player; - - Beatmap.Value = new CustomSkinWorkingBeatmap(audio, beatmapHasColours); - - LoadScreen(player = new ExposedPlayer(userHasCustomColours)); - - return player; - } - - private void configureSettings(bool beatmapSkins, bool beatmapColours) - { - AddStep($"{(beatmapSkins ? "enable" : "disable")} beatmap skins", () => + public CatchExposedPlayer(bool userHasCustomColours) + : base(userHasCustomColours) { - this.beatmapSkins.Value = beatmapSkins; - }); - AddStep($"{(beatmapColours ? "enable" : "disable")} beatmap colours", () => - { - this.beatmapColours.Value = beatmapColours; - }); - } - - private class ExposedPlayer : Player - { - private readonly bool userHasCustomColours; - - public ExposedPlayer(bool userHasCustomColours) - : base(new PlayerConfiguration - { - AllowPause = false, - ShowResults = false, - }) - { - this.userHasCustomColours = userHasCustomColours; } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new TestSkin(userHasCustomColours)); + dependencies.CacheAs(new CatchTestSkin(UserHasCustomColours)); return dependencies; } - public IReadOnlyList UsableComboColours => - GameplayClockContainer.ChildrenOfType() - .First() - .GetConfig>(GlobalSkinColours.ComboColours)?.Value; - public Color4 UsableHyperDashColour => GameplayClockContainer.ChildrenOfType() .First() @@ -223,17 +155,14 @@ namespace osu.Game.Rulesets.Catch.Tests } } - private class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap + private class CatchCustomSkinWorkingBeatmap : CustomSkinWorkingBeatmap { - private readonly bool hasColours; - - public CustomSkinWorkingBeatmap(AudioManager audio, bool hasColours) - : base(createBeatmap(new CatchRuleset().RulesetInfo), null, null, audio) + public CatchCustomSkinWorkingBeatmap(AudioManager audio, bool hasColours) + : base(createBeatmap(new CatchRuleset().RulesetInfo), audio, hasColours) { - this.hasColours = hasColours; } - protected override ISkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours); + protected override ISkin GetSkin() => new CatchTestBeatmapSkin(BeatmapInfo, HasColours); private static IBeatmap createBeatmap(RulesetInfo ruleset) { @@ -297,26 +226,19 @@ namespace osu.Game.Rulesets.Catch.Tests } } - private class TestBeatmapSkin : LegacyBeatmapSkin + private class CatchTestBeatmapSkin : TestBeatmapSkin { - public static Color4[] Colours { get; } = - { - new Color4(50, 100, 150, 255), - new Color4(40, 80, 120, 255), - }; - public static readonly Color4 HYPER_DASH_COLOUR = Color4.DarkBlue; public static readonly Color4 HYPER_DASH_AFTER_IMAGE_COLOUR = Color4.DarkCyan; public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; - public TestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) - : base(beatmap, new ResourceStore(), null) + public CatchTestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) + : base(beatmap, hasColours) { if (hasColours) { - Configuration.AddComboColours(Colours); Configuration.CustomColours.Add(CatchSkinColour.HyperDash.ToString(), HYPER_DASH_COLOUR); Configuration.CustomColours.Add(CatchSkinColour.HyperDashAfterImage.ToString(), HYPER_DASH_AFTER_IMAGE_COLOUR); Configuration.CustomColours.Add(CatchSkinColour.HyperDashFruit.ToString(), HYPER_DASH_FRUIT_COLOUR); @@ -324,37 +246,24 @@ namespace osu.Game.Rulesets.Catch.Tests } } - private class TestSkin : LegacySkin, ISkinSource + private class CatchTestSkin : TestSkin { - public static Color4[] Colours { get; } = - { - new Color4(150, 100, 50, 255), - new Color4(20, 20, 20, 255), - }; - public static readonly Color4 HYPER_DASH_COLOUR = Color4.LightBlue; public static readonly Color4 HYPER_DASH_AFTER_IMAGE_COLOUR = Color4.LightCoral; public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan; - public TestSkin(bool hasCustomColours) - : base(new SkinInfo(), new ResourceStore(), null, string.Empty) + public CatchTestSkin(bool hasCustomColours) + : base(hasCustomColours) { if (hasCustomColours) { - Configuration.AddComboColours(Colours); Configuration.CustomColours.Add(CatchSkinColour.HyperDash.ToString(), HYPER_DASH_COLOUR); Configuration.CustomColours.Add(CatchSkinColour.HyperDashAfterImage.ToString(), HYPER_DASH_AFTER_IMAGE_COLOUR); Configuration.CustomColours.Add(CatchSkinColour.HyperDashFruit.ToString(), HYPER_DASH_FRUIT_COLOUR); } } - - public event Action SourceChanged - { - add { } - remove { } - } } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index 22b028906f..095ce63ec5 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -1,168 +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.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Bindables; -using osu.Framework.IO.Stores; -using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Screens.Play; using osu.Game.Skinning; -using osu.Game.Tests.Visual; +using osu.Game.Tests.Beatmaps; using osuTK; -using osuTK.Graphics; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneLegacyBeatmapSkin : ScreenTestScene + public class TestSceneLegacyBeatmapSkin : LegacyBeatmapSkinColourTest { [Resolved] private AudioManager audio { get; set; } - private readonly Bindable beatmapSkins = new Bindable(); - private readonly Bindable beatmapColours = new Bindable(); - [BackgroundDependencyLoader] private void load(OsuConfigManager config) { - config.BindWith(OsuSetting.BeatmapSkins, beatmapSkins); - config.BindWith(OsuSetting.BeatmapColours, beatmapColours); + config.BindWith(OsuSetting.BeatmapSkins, BeatmapSkins); + config.BindWith(OsuSetting.BeatmapColours, BeatmapColours); } [TestCase(true, true)] [TestCase(true, false)] [TestCase(false, true)] [TestCase(false, false)] - public void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin) + public override void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, true); - AddStep("load coloured beatmap", () => player = loadBeatmap(userHasCustomColours, true)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is beatmap skin colours", () => player.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours)); + TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, true); + base.TestBeatmapComboColours(userHasCustomColours, useBeatmapSkin); + AddAssert("is beatmap skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestBeatmapSkin.Colours)); } [TestCase(true)] [TestCase(false)] - public void TestBeatmapComboColoursOverride(bool useBeatmapSkin) + public override void TestBeatmapComboColoursOverride(bool useBeatmapSkin) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, false); - AddStep("load coloured beatmap", () => player = loadBeatmap(true, true)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is user custom skin colours", () => player.UsableComboColours.SequenceEqual(TestSkin.Colours)); + TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, true); + base.TestBeatmapComboColoursOverride(useBeatmapSkin); + AddAssert("is user custom skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours)); } [TestCase(true)] [TestCase(false)] - public void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin) + public override void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, false); - AddStep("load coloured beatmap", () => player = loadBeatmap(false, true)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is default user skin colours", () => player.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); + TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, true); + base.TestBeatmapComboColoursOverrideWithDefaultColours(useBeatmapSkin); + AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); } [TestCase(true, true)] [TestCase(false, true)] [TestCase(true, false)] [TestCase(false, false)] - public void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour) + public override void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, useBeatmapColour); - AddStep("load no-colour beatmap", () => player = loadBeatmap(false, false)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is default user skin colours", () => player.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); + TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, false); + base.TestBeatmapNoComboColours(useBeatmapSkin, useBeatmapColour); + AddAssert("is default user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(SkinConfiguration.DefaultComboColours)); } [TestCase(true, true)] [TestCase(false, true)] [TestCase(true, false)] [TestCase(false, false)] - public void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour) + public override void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour) { - ExposedPlayer player = null; - - configureSettings(useBeatmapSkin, useBeatmapColour); - AddStep("load custom-skin colour", () => player = loadBeatmap(true, false)); - AddUntilStep("wait for player", () => player.IsLoaded); - - AddAssert("is custom user skin colours", () => player.UsableComboColours.SequenceEqual(TestSkin.Colours)); + TestBeatmap = new OsuCustomSkinWorkingBeatmap(audio, false); + base.TestBeatmapNoComboColoursSkinOverride(useBeatmapSkin, useBeatmapColour); + AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours)); } - private ExposedPlayer loadBeatmap(bool userHasCustomColours, bool beatmapHasColours) + protected override ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new OsuExposedPlayer(userHasCustomColours); + + private class OsuExposedPlayer : ExposedPlayer { - ExposedPlayer player; - - Beatmap.Value = new CustomSkinWorkingBeatmap(audio, beatmapHasColours); - - LoadScreen(player = new ExposedPlayer(userHasCustomColours)); - - return player; - } - - private void configureSettings(bool beatmapSkins, bool beatmapColours) - { - AddStep($"{(beatmapSkins ? "enable" : "disable")} beatmap skins", () => + public OsuExposedPlayer(bool userHasCustomColours) + : base(userHasCustomColours) { - this.beatmapSkins.Value = beatmapSkins; - }); - AddStep($"{(beatmapColours ? "enable" : "disable")} beatmap colours", () => - { - this.beatmapColours.Value = beatmapColours; - }); - } - - private class ExposedPlayer : Player - { - private readonly bool userHasCustomColours; - - public ExposedPlayer(bool userHasCustomColours) - : base(new PlayerConfiguration - { - AllowPause = false, - ShowResults = false, - }) - { - this.userHasCustomColours = userHasCustomColours; } protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new TestSkin(userHasCustomColours)); + dependencies.CacheAs(new OsuTestSkin(UserHasCustomColours)); return dependencies; } - - public IReadOnlyList UsableComboColours => - GameplayClockContainer.ChildrenOfType() - .First() - .GetConfig>(GlobalSkinColours.ComboColours)?.Value; } - private class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap + private class OsuCustomSkinWorkingBeatmap : CustomSkinWorkingBeatmap { private readonly bool hasColours; - public CustomSkinWorkingBeatmap(AudioManager audio, bool hasColours) - : base(new Beatmap + public OsuCustomSkinWorkingBeatmap(AudioManager audio, bool hasColours) + : base(createBeatmap(), audio, hasColours) + { + this.hasColours = hasColours; + } + + protected override ISkin GetSkin() => new OsuTestBeatmapSkin(BeatmapInfo, hasColours); + + private static IBeatmap createBeatmap() => + new Beatmap { BeatmapInfo = { @@ -170,49 +115,22 @@ namespace osu.Game.Rulesets.Osu.Tests Ruleset = new OsuRuleset().RulesetInfo, }, HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } - }, null, null, audio) - { - this.hasColours = hasColours; - } - - protected override ISkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, hasColours); + }; } - private class TestBeatmapSkin : LegacyBeatmapSkin + private class OsuTestBeatmapSkin : TestBeatmapSkin { - public static Color4[] Colours { get; } = + public OsuTestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) + : base(beatmap, hasColours) { - new Color4(50, 100, 150, 255), - new Color4(40, 80, 120, 255), - }; - - public TestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) - : base(beatmap, new ResourceStore(), null) - { - if (hasColours) - Configuration.AddComboColours(Colours); } } - private class TestSkin : LegacySkin, ISkinSource + private class OsuTestSkin : TestSkin { - public static Color4[] Colours { get; } = + public OsuTestSkin(bool hasCustomColours) + : base(hasCustomColours) { - new Color4(150, 100, 50, 255), - new Color4(20, 20, 20, 255), - }; - - public TestSkin(bool hasCustomColours) - : base(new SkinInfo(), null, null, string.Empty) - { - if (hasCustomColours) - Configuration.AddComboColours(Colours); - } - - public event Action SourceChanged - { - add { } - remove { } } } } diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs new file mode 100644 index 0000000000..b42c3ea70d --- /dev/null +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -0,0 +1,147 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.IO.Stores; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Screens.Play; +using osu.Game.Skinning; +using osu.Game.Tests.Visual; +using osuTK.Graphics; + +namespace osu.Game.Tests.Beatmaps +{ + public class LegacyBeatmapSkinColourTest : ScreenTestScene + { + protected readonly Bindable BeatmapSkins = new Bindable(); + protected readonly Bindable BeatmapColours = new Bindable(); + protected ExposedPlayer TestPlayer; + protected WorkingBeatmap TestBeatmap; + + public virtual void TestBeatmapComboColours(bool userHasCustomColours, bool useBeatmapSkin) => ConfigureTest(useBeatmapSkin, true, userHasCustomColours); + + public virtual void TestBeatmapComboColoursOverride(bool useBeatmapSkin) => ConfigureTest(useBeatmapSkin, false, true); + + public virtual void TestBeatmapComboColoursOverrideWithDefaultColours(bool useBeatmapSkin) => ConfigureTest(useBeatmapSkin, false, false); + + public virtual void TestBeatmapNoComboColours(bool useBeatmapSkin, bool useBeatmapColour) => ConfigureTest(useBeatmapSkin, useBeatmapColour, false); + + public virtual void TestBeatmapNoComboColoursSkinOverride(bool useBeatmapSkin, bool useBeatmapColour) => ConfigureTest(useBeatmapSkin, useBeatmapColour, true); + + protected virtual void ConfigureTest(bool useBeatmapSkin, bool useBeatmapColours, bool userHasCustomColours) + { + configureSettings(useBeatmapSkin, useBeatmapColours); + AddStep($"load {(((CustomSkinWorkingBeatmap)TestBeatmap).HasColours ? "coloured " : "")} beatmap", () => TestPlayer = LoadBeatmap(userHasCustomColours)); + AddUntilStep("wait for player load", () => TestPlayer.IsLoaded); + } + + private void configureSettings(bool beatmapSkins, bool beatmapColours) + { + AddStep($"{(beatmapSkins ? "enable" : "disable")} beatmap skins", () => + { + BeatmapSkins.Value = beatmapSkins; + }); + AddStep($"{(beatmapColours ? "enable" : "disable")} beatmap colours", () => + { + BeatmapColours.Value = beatmapColours; + }); + } + + protected virtual ExposedPlayer LoadBeatmap(bool userHasCustomColours) + { + ExposedPlayer player; + + Beatmap.Value = TestBeatmap; + + LoadScreen(player = CreateTestPlayer(userHasCustomColours)); + + return player; + } + + protected virtual ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new ExposedPlayer(userHasCustomColours); + + protected class ExposedPlayer : Player + { + protected readonly bool UserHasCustomColours; + + public ExposedPlayer(bool userHasCustomColours) + : base(new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + UserHasCustomColours = userHasCustomColours; + } + + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) + { + var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + dependencies.CacheAs(new TestSkin(UserHasCustomColours)); + return dependencies; + } + + public IReadOnlyList UsableComboColours => + GameplayClockContainer.ChildrenOfType() + .First() + .GetConfig>(GlobalSkinColours.ComboColours)?.Value; + } + + protected class CustomSkinWorkingBeatmap : ClockBackedTestWorkingBeatmap + { + public readonly bool HasColours; + + public CustomSkinWorkingBeatmap(IBeatmap beatmap, AudioManager audio, bool hasColours) + : base(beatmap, null, null, audio) + { + HasColours = hasColours; + } + + protected override ISkin GetSkin() => new TestBeatmapSkin(BeatmapInfo, HasColours); + } + + protected class TestBeatmapSkin : LegacyBeatmapSkin + { + public static Color4[] Colours { get; } = + { + new Color4(50, 100, 150, 255), + new Color4(40, 80, 120, 255), + }; + + public TestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) + : base(beatmap, new ResourceStore(), null) + { + if (hasColours) + Configuration.AddComboColours(Colours); + } + } + + protected class TestSkin : LegacySkin, ISkinSource + { + public static Color4[] Colours { get; } = + { + new Color4(150, 100, 50, 255), + new Color4(20, 20, 20, 255), + }; + + public TestSkin(bool hasCustomColours) + : base(new SkinInfo(), new ResourceStore(), null, string.Empty) + { + if (hasCustomColours) + Configuration.AddComboColours(Colours); + } + + public event Action SourceChanged + { + add { } + remove { } + } + } + } +} From a3535f4b79a3634e31fe9fd935545b653558787f Mon Sep 17 00:00:00 2001 From: Mysfit Date: Sat, 16 Jan 2021 02:09:35 -0500 Subject: [PATCH 6007/6909] Further simplified beatmap colouring tests. --- .../TestSceneLegacyBeatmapSkin.cs | 144 ++---------------- .../TestSceneLegacyBeatmapSkin.cs | 38 ----- .../Beatmaps/LegacyBeatmapSkinColourTest.cs | 22 +++ 3 files changed, 35 insertions(+), 169 deletions(-) diff --git a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs index 89298242e5..eea83ef7c1 100644 --- a/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Catch.Tests/TestSceneLegacyBeatmapSkin.cs @@ -1,22 +1,17 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Beatmaps.ControlPoints; using osu.Game.Configuration; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Skinning; -using osu.Game.Rulesets.Catch.UI; -using osu.Game.Rulesets.Objects; using osu.Game.Skinning; using osu.Game.Tests.Beatmaps; -using osuTK; using osuTK.Graphics; namespace osu.Game.Rulesets.Catch.Tests @@ -90,9 +85,9 @@ namespace osu.Game.Rulesets.Catch.Tests { TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); ConfigureTest(useBeatmapSkin, true, true); - AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == CatchTestBeatmapSkin.HYPER_DASH_COLOUR); - AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == CatchTestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); - AddAssert("is custom hyper dash fruit colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashFruitColour == CatchTestBeatmapSkin.HYPER_DASH_FRUIT_COLOUR); + AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestBeatmapSkin.HYPER_DASH_COLOUR); + AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestBeatmapSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); + AddAssert("is custom hyper dash fruit colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashFruitColour == TestBeatmapSkin.HYPER_DASH_FRUIT_COLOUR); } [TestCase(true)] @@ -101,9 +96,9 @@ namespace osu.Game.Rulesets.Catch.Tests { TestBeatmap = new CatchCustomSkinWorkingBeatmap(audio, true); ConfigureTest(useBeatmapSkin, false, true); - AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == CatchTestSkin.HYPER_DASH_COLOUR); - AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == CatchTestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); - AddAssert("is custom hyper dash fruit colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashFruitColour == CatchTestSkin.HYPER_DASH_FRUIT_COLOUR); + AddAssert("is custom hyper dash colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashColour == TestSkin.HYPER_DASH_COLOUR); + AddAssert("is custom hyper dash after image colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashAfterImageColour == TestSkin.HYPER_DASH_AFTER_IMAGE_COLOUR); + AddAssert("is custom hyper dash fruit colours", () => ((CatchExposedPlayer)TestPlayer).UsableHyperDashFruitColour == TestSkin.HYPER_DASH_FRUIT_COLOUR); } protected override ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new CatchExposedPlayer(userHasCustomColours); @@ -115,13 +110,6 @@ namespace osu.Game.Rulesets.Catch.Tests { } - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new CatchTestSkin(UserHasCustomColours)); - return dependencies; - } - public Color4 UsableHyperDashColour => GameplayClockContainer.ChildrenOfType() .First() @@ -141,129 +129,23 @@ namespace osu.Game.Rulesets.Catch.Tests .Value ?? Color4.Red; } - private class TestJuiceStream : JuiceStream - { - public TestJuiceStream(float x) - { - X = x; - - Path = new SliderPath(new[] - { - new PathControlPoint(Vector2.Zero), - new PathControlPoint(new Vector2(30, 0)), - }); - } - } - private class CatchCustomSkinWorkingBeatmap : CustomSkinWorkingBeatmap { public CatchCustomSkinWorkingBeatmap(AudioManager audio, bool hasColours) - : base(createBeatmap(new CatchRuleset().RulesetInfo), audio, hasColours) + : base(createBeatmap(), audio, hasColours) { } - protected override ISkin GetSkin() => new CatchTestBeatmapSkin(BeatmapInfo, HasColours); - - private static IBeatmap createBeatmap(RulesetInfo ruleset) - { - var beatmap = new Beatmap + private static IBeatmap createBeatmap() => + new Beatmap { BeatmapInfo = { - Ruleset = ruleset, - BaseDifficulty = new BeatmapDifficulty { CircleSize = 3.6f } - } + BeatmapSet = new BeatmapSetInfo(), + Ruleset = new CatchRuleset().RulesetInfo + }, + HitObjects = { new Fruit { StartTime = 1816, X = 56, NewCombo = true } } }; - - beatmap.ControlPointInfo.Add(0, new TimingControlPoint()); - - // Should produce a hyper-dash (edge case test) - 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 * CatchPlayfield.WIDTH; - const float right_x = 0.98f * CatchPlayfield.WIDTH; - - 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); - - beatmap.ControlPointInfo.Add(startTime, new TimingControlPoint - { - BeatLength = 50 - }); - - createObjects(() => new TestJuiceStream(left_x) - { - Path = new SliderPath(new[] - { - new PathControlPoint(Vector2.Zero), - new PathControlPoint(new Vector2(512, 0)) - }) - }, 1); - - return beatmap; - - void createObjects(Func createObject, int count = 3) - { - const float spacing = 140; - - for (int i = 0; i < count; i++) - { - var hitObject = createObject(); - hitObject.StartTime = startTime + i * spacing; - beatmap.HitObjects.Add(hitObject); - } - - startTime += 700; - } - } - } - - private class CatchTestBeatmapSkin : TestBeatmapSkin - { - public static readonly Color4 HYPER_DASH_COLOUR = Color4.DarkBlue; - - public static readonly Color4 HYPER_DASH_AFTER_IMAGE_COLOUR = Color4.DarkCyan; - - public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; - - public CatchTestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) - : base(beatmap, hasColours) - { - if (hasColours) - { - Configuration.CustomColours.Add(CatchSkinColour.HyperDash.ToString(), HYPER_DASH_COLOUR); - Configuration.CustomColours.Add(CatchSkinColour.HyperDashAfterImage.ToString(), HYPER_DASH_AFTER_IMAGE_COLOUR); - Configuration.CustomColours.Add(CatchSkinColour.HyperDashFruit.ToString(), HYPER_DASH_FRUIT_COLOUR); - } - } - } - - private class CatchTestSkin : TestSkin - { - public static readonly Color4 HYPER_DASH_COLOUR = Color4.LightBlue; - - public static readonly Color4 HYPER_DASH_AFTER_IMAGE_COLOUR = Color4.LightCoral; - - public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan; - - public CatchTestSkin(bool hasCustomColours) - : base(hasCustomColours) - { - if (hasCustomColours) - { - Configuration.CustomColours.Add(CatchSkinColour.HyperDash.ToString(), HYPER_DASH_COLOUR); - Configuration.CustomColours.Add(CatchSkinColour.HyperDashAfterImage.ToString(), HYPER_DASH_AFTER_IMAGE_COLOUR); - Configuration.CustomColours.Add(CatchSkinColour.HyperDashFruit.ToString(), HYPER_DASH_FRUIT_COLOUR); - } - } } } } diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs index 095ce63ec5..c26419b0e8 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneLegacyBeatmapSkin.cs @@ -77,35 +77,13 @@ namespace osu.Game.Rulesets.Osu.Tests AddAssert("is custom user skin colours", () => TestPlayer.UsableComboColours.SequenceEqual(TestSkin.Colours)); } - protected override ExposedPlayer CreateTestPlayer(bool userHasCustomColours) => new OsuExposedPlayer(userHasCustomColours); - - private class OsuExposedPlayer : ExposedPlayer - { - public OsuExposedPlayer(bool userHasCustomColours) - : base(userHasCustomColours) - { - } - - protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) - { - var dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - dependencies.CacheAs(new OsuTestSkin(UserHasCustomColours)); - return dependencies; - } - } - private class OsuCustomSkinWorkingBeatmap : CustomSkinWorkingBeatmap { - private readonly bool hasColours; - public OsuCustomSkinWorkingBeatmap(AudioManager audio, bool hasColours) : base(createBeatmap(), audio, hasColours) { - this.hasColours = hasColours; } - protected override ISkin GetSkin() => new OsuTestBeatmapSkin(BeatmapInfo, hasColours); - private static IBeatmap createBeatmap() => new Beatmap { @@ -117,21 +95,5 @@ namespace osu.Game.Rulesets.Osu.Tests HitObjects = { new HitCircle { Position = new Vector2(256, 192) } } }; } - - private class OsuTestBeatmapSkin : TestBeatmapSkin - { - public OsuTestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) - : base(beatmap, hasColours) - { - } - } - - private class OsuTestSkin : TestSkin - { - public OsuTestSkin(bool hasCustomColours) - : base(hasCustomColours) - { - } - } } } diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index b42c3ea70d..fb3432fbae 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -114,11 +114,22 @@ namespace osu.Game.Tests.Beatmaps new Color4(40, 80, 120, 255), }; + public static readonly Color4 HYPER_DASH_COLOUR = Color4.DarkBlue; + + public static readonly Color4 HYPER_DASH_AFTER_IMAGE_COLOUR = Color4.DarkCyan; + + public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.DarkGoldenrod; + public TestBeatmapSkin(BeatmapInfo beatmap, bool hasColours) : base(beatmap, new ResourceStore(), null) { if (hasColours) + { Configuration.AddComboColours(Colours); + Configuration.CustomColours.Add("HyperDash", HYPER_DASH_COLOUR); + Configuration.CustomColours.Add("HyperDashAfterImage", HYPER_DASH_AFTER_IMAGE_COLOUR); + Configuration.CustomColours.Add("HyperDashFruit", HYPER_DASH_FRUIT_COLOUR); + } } } @@ -130,11 +141,22 @@ namespace osu.Game.Tests.Beatmaps new Color4(20, 20, 20, 255), }; + public static readonly Color4 HYPER_DASH_COLOUR = Color4.LightBlue; + + public static readonly Color4 HYPER_DASH_AFTER_IMAGE_COLOUR = Color4.LightCoral; + + public static readonly Color4 HYPER_DASH_FRUIT_COLOUR = Color4.LightCyan; + public TestSkin(bool hasCustomColours) : base(new SkinInfo(), new ResourceStore(), null, string.Empty) { if (hasCustomColours) + { Configuration.AddComboColours(Colours); + Configuration.CustomColours.Add("HyperDash", HYPER_DASH_COLOUR); + Configuration.CustomColours.Add("HyperDashAfterImage", HYPER_DASH_AFTER_IMAGE_COLOUR); + Configuration.CustomColours.Add("HyperDashFruit", HYPER_DASH_FRUIT_COLOUR); + } } public event Action SourceChanged From d9034eab26e8abe8698a4de5feacd1be0ae8ecff Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 22:54:54 +0300 Subject: [PATCH 6008/6909] Make model manager in `DownloadTrackingComposite` protected --- osu.Game/Online/DownloadTrackingComposite.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 7a64c9002d..4e52761813 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -20,7 +20,7 @@ namespace osu.Game.Online protected readonly Bindable Model = new Bindable(); [Resolved(CanBeNull = true)] - private TModelManager manager { get; set; } + protected TModelManager Manager { get; private set; } /// /// Holds the current download state of the , whether is has already been downloaded, is in progress, or is not downloaded. @@ -49,19 +49,19 @@ namespace osu.Game.Online else if (manager?.IsAvailableLocally(modelInfo.NewValue) == true) State.Value = DownloadState.LocallyAvailable; else - attachDownload(manager?.GetExistingDownload(modelInfo.NewValue)); + attachDownload(Manager?.GetExistingDownload(modelInfo.NewValue)); }, true); - if (manager == null) + if (Manager == null) return; - managerDownloadBegan = manager.DownloadBegan.GetBoundCopy(); + managerDownloadBegan = Manager.DownloadBegan.GetBoundCopy(); managerDownloadBegan.BindValueChanged(downloadBegan); - managerDownloadFailed = manager.DownloadFailed.GetBoundCopy(); + managerDownloadFailed = Manager.DownloadFailed.GetBoundCopy(); managerDownloadFailed.BindValueChanged(downloadFailed); - managedUpdated = manager.ItemUpdated.GetBoundCopy(); + managedUpdated = Manager.ItemUpdated.GetBoundCopy(); managedUpdated.BindValueChanged(itemUpdated); - managerRemoved = manager.ItemRemoved.GetBoundCopy(); + managerRemoved = Manager.ItemRemoved.GetBoundCopy(); managerRemoved.BindValueChanged(itemRemoved); } From 04d17aadfa4685f54e670b934e17fc3be3cf2b3e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 22:57:55 +0300 Subject: [PATCH 6009/6909] Add overridable method for verifying models in database --- osu.Game/Online/DownloadTrackingComposite.cs | 42 +++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 4e52761813..9dd8258e78 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.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.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; @@ -65,6 +66,15 @@ namespace osu.Game.Online managerRemoved.BindValueChanged(itemRemoved); } + /// + /// Verifies that the given databased model is in a correct state to be considered available. + /// + /// + /// In the case of multiplayer/playlists, this has to verify that the databased beatmap set with the selected beatmap matches what's online. + /// + /// The model in database. + protected virtual bool VerifyDatabasedModel([NotNull] TModel databasedModel) => true; + private void downloadBegan(ValueChangedEvent>> weakRequest) { if (weakRequest.NewValue.TryGetTarget(out var request)) @@ -134,23 +144,35 @@ namespace osu.Game.Online private void itemUpdated(ValueChangedEvent> weakItem) { if (weakItem.NewValue.TryGetTarget(out var item)) - setDownloadStateFromManager(item, DownloadState.LocallyAvailable); + { + Schedule(() => + { + if (!item.Equals(Model.Value)) + return; + + if (!VerifyDatabasedModel(item)) + { + State.Value = DownloadState.NotDownloaded; + return; + } + + State.Value = DownloadState.LocallyAvailable; + }); + } } private void itemRemoved(ValueChangedEvent> weakItem) { if (weakItem.NewValue.TryGetTarget(out var item)) - setDownloadStateFromManager(item, DownloadState.NotDownloaded); + { + Schedule(() => + { + if (item.Equals(Model.Value)) + State.Value = DownloadState.NotDownloaded; + }); + } } - private void setDownloadStateFromManager(TModel s, DownloadState state) => Schedule(() => - { - if (!s.Equals(Model.Value)) - return; - - State.Value = state; - }); - #region Disposal protected override void Dispose(bool isDisposing) From 7ad8b167ccb2f73247f2506cace66b22ec333665 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 22:58:29 +0300 Subject: [PATCH 6010/6909] Add overridable method for checking local availability of current model --- osu.Game/Online/DownloadTrackingComposite.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 9dd8258e78..1631d9790b 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -47,7 +47,7 @@ namespace osu.Game.Online { if (modelInfo.NewValue == null) attachDownload(null); - else if (manager?.IsAvailableLocally(modelInfo.NewValue) == true) + else if (IsModelAvailableLocally()) State.Value = DownloadState.LocallyAvailable; else attachDownload(Manager?.GetExistingDownload(modelInfo.NewValue)); @@ -75,6 +75,13 @@ namespace osu.Game.Online /// The model in database. protected virtual bool VerifyDatabasedModel([NotNull] TModel databasedModel) => true; + /// + /// Whether the given model is available in the database. + /// By default, this calls , + /// but can be overriden to add additional checks for verifying the model in database. + /// + protected virtual bool IsModelAvailableLocally() => Manager.IsAvailableLocally(Model.Value); + private void downloadBegan(ValueChangedEvent>> weakRequest) { if (weakRequest.NewValue.TryGetTarget(out var request)) From da9c23f3478356b9183fecbe82d0d416c039a26d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 23:00:56 +0300 Subject: [PATCH 6011/6909] Add beatmap availability tracker component for multiplayer --- .../Online/Rooms/MultiplayerBeatmapTracker.cs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs diff --git a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs new file mode 100644 index 0000000000..b22d17f3ef --- /dev/null +++ b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs @@ -0,0 +1,89 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; + +namespace osu.Game.Online.Rooms +{ + public class MultiplayerBeatmapTracker : DownloadTrackingComposite + { + public readonly IBindable SelectedItem = new Bindable(); + + /// + /// The availability state of the currently selected playlist item. + /// + public IBindable Availability => availability; + + private readonly Bindable availability = new Bindable(); + + public MultiplayerBeatmapTracker() + { + State.BindValueChanged(_ => updateAvailability()); + Progress.BindValueChanged(_ => updateAvailability()); + updateAvailability(); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + SelectedItem.BindValueChanged(item => Model.Value = item.NewValue?.Beatmap.Value.BeatmapSet, true); + } + + protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet) + { + int? beatmapId = SelectedItem.Value.Beatmap.Value.OnlineBeatmapID; + string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; + + BeatmapInfo matchingBeatmap; + + if (databasedSet.Beatmaps == null) + { + // The given databased beatmap set is not passed in a usable state to check with. + // Perform a full query instead, as per https://github.com/ppy/osu/pull/11415. + matchingBeatmap = Manager.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); + return matchingBeatmap != null; + } + + matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); + return matchingBeatmap != null; + } + + protected override bool IsModelAvailableLocally() + { + int? beatmapId = SelectedItem.Value.Beatmap.Value.OnlineBeatmapID; + string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; + + var beatmap = Manager.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); + return beatmap?.BeatmapSet.DeletePending == false; + } + + private void updateAvailability() + { + switch (State.Value) + { + case DownloadState.NotDownloaded: + availability.Value = BeatmapAvailability.NotDownloaded(); + break; + + case DownloadState.Downloading: + availability.Value = BeatmapAvailability.Downloading(Progress.Value); + break; + + case DownloadState.Importing: + availability.Value = BeatmapAvailability.Importing(); + break; + + case DownloadState.LocallyAvailable: + availability.Value = BeatmapAvailability.LocallyAvailable(); + break; + + default: + throw new ArgumentOutOfRangeException(nameof(State)); + } + } + } +} From cf2378103656863879a08b9e2e2704bdffdebd03 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 23:02:30 +0300 Subject: [PATCH 6012/6909] Cache beatmap tracker and bind to selected item in `RoomSubScreen` --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 2449563c73..c049d4be20 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -40,6 +40,17 @@ namespace osu.Game.Screens.OnlinePlay.Match private IBindable> managerUpdated; + [Cached] + protected readonly MultiplayerBeatmapTracker BeatmapTracker; + + protected RoomSubScreen() + { + InternalChild = BeatmapTracker = new MultiplayerBeatmapTracker + { + SelectedItem = { BindTarget = SelectedItem }, + }; + } + [BackgroundDependencyLoader] private void load(AudioManager audio) { From 96feaa027ded707f31ef18482aa0b632dad5627c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 23:06:54 +0300 Subject: [PATCH 6013/6909] Make `ArchiveModelManager` import method overridable (for testing purposes) --- osu.Game/Database/ArchiveModelManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 36cc4cce39..8502ab5965 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -308,7 +308,7 @@ namespace osu.Game.Database /// The model to be imported. /// An optional archive to use for model population. /// An optional cancellation token. - public async Task Import(TModel item, ArchiveReader archive = null, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => + public virtual async Task Import(TModel item, ArchiveReader archive = null, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => { cancellationToken.ThrowIfCancellationRequested(); From 4778686dc4b712e7c93a30661718fd383674fee2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 23:07:46 +0300 Subject: [PATCH 6014/6909] Expose method for triggering filename-backed success in `APIDownloadRequest` Exactly like in `APIRequest` --- osu.Game/Online/API/APIDownloadRequest.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/APIDownloadRequest.cs b/osu.Game/Online/API/APIDownloadRequest.cs index 940b9b4803..02c589403c 100644 --- a/osu.Game/Online/API/APIDownloadRequest.cs +++ b/osu.Game/Online/API/APIDownloadRequest.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 osu.Framework.IO.Network; @@ -28,13 +29,19 @@ namespace osu.Game.Online.API private void request_Progress(long current, long total) => API.Schedule(() => Progressed?.Invoke(current, total)); - protected APIDownloadRequest() + internal void TriggerSuccess(string filename) { - base.Success += onSuccess; + if (this.filename != null) + throw new InvalidOperationException("Attempted to trigger success more than once"); + + this.filename = filename; + + TriggerSuccess(); } - private void onSuccess() + internal override void TriggerSuccess() { + base.TriggerSuccess(); Success?.Invoke(filename); } From 23c7afa573b6d9b8b0f4af49283224e2231394b7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 23:17:05 +0300 Subject: [PATCH 6015/6909] Expose method for setting progress of archive download request --- osu.Game/Online/API/ArchiveDownloadRequest.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/ArchiveDownloadRequest.cs b/osu.Game/Online/API/ArchiveDownloadRequest.cs index f1966aeb2b..bb57a7a5f8 100644 --- a/osu.Game/Online/API/ArchiveDownloadRequest.cs +++ b/osu.Game/Online/API/ArchiveDownloadRequest.cs @@ -18,7 +18,13 @@ namespace osu.Game.Online.API { Model = model; - Progressed += (current, total) => DownloadProgressed?.Invoke(Progress = (float)current / total); + Progressed += (current, total) => SetProgress((float)current / total); + } + + protected void SetProgress(float progress) + { + Progress = progress; + DownloadProgressed?.Invoke(progress); } } } From adb2605d5d7ddb30fea80a5f289e28e3c357a8db Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 23:17:47 +0300 Subject: [PATCH 6016/6909] Enforce `double` type in the download progress path Wasn't sure where to exactly put this, or whether to split it, but it's very small change to worry about, so I guess it's fine being here --- osu.Game/Online/API/ArchiveDownloadRequest.cs | 8 ++++---- osu.Game/Online/DownloadTrackingComposite.cs | 2 +- .../Overlays/Notifications/ProgressNotification.cs | 10 +++++----- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/API/ArchiveDownloadRequest.cs b/osu.Game/Online/API/ArchiveDownloadRequest.cs index bb57a7a5f8..fdb2a984dc 100644 --- a/osu.Game/Online/API/ArchiveDownloadRequest.cs +++ b/osu.Game/Online/API/ArchiveDownloadRequest.cs @@ -10,18 +10,18 @@ namespace osu.Game.Online.API { public readonly TModel Model; - public float Progress; + public double Progress { get; private set; } - public event Action DownloadProgressed; + public event Action DownloadProgressed; protected ArchiveDownloadRequest(TModel model) { Model = model; - Progressed += (current, total) => SetProgress((float)current / total); + Progressed += (current, total) => SetProgress((double)current / total); } - protected void SetProgress(float progress) + protected void SetProgress(double progress) { Progress = progress; DownloadProgressed?.Invoke(progress); diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 1631d9790b..b72cf38369 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -144,7 +144,7 @@ namespace osu.Game.Online private void onRequestSuccess(string _) => Schedule(() => State.Value = DownloadState.Importing); - private void onRequestProgress(float progress) => Schedule(() => Progress.Value = progress); + private void onRequestProgress(double progress) => Schedule(() => Progress.Value = progress); private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null)); diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 3105ecd742..e18bab8e83 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -23,9 +23,9 @@ namespace osu.Game.Overlays.Notifications public string CompletionText { get; set; } = "Task has completed!"; - private float progress; + private double progress; - public float Progress + public double Progress { get => progress; set @@ -185,9 +185,9 @@ namespace osu.Game.Overlays.Notifications private Color4 colourActive; private Color4 colourInactive; - private float progress; + private double progress; - public float Progress + public double Progress { get => progress; set @@ -195,7 +195,7 @@ namespace osu.Game.Overlays.Notifications if (progress == value) return; progress = value; - box.ResizeTo(new Vector2(progress, 1), 100, Easing.OutQuad); + box.ResizeTo(new Vector2((float)progress, 1), 100, Easing.OutQuad); } } From f0602243bfa792f3880e9d083f1ddc67431a29cf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 23:30:01 +0300 Subject: [PATCH 6017/6909] Add beatmapset file containing the eaxct beatmap in `TestBeatmap` solely --- .../Resources/Archives/test-beatmap.osz | Bin 0 -> 7286 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 osu.Game.Tests/Resources/Archives/test-beatmap.osz diff --git a/osu.Game.Tests/Resources/Archives/test-beatmap.osz b/osu.Game.Tests/Resources/Archives/test-beatmap.osz new file mode 100644 index 0000000000000000000000000000000000000000..f98fd2f8ffef0968270a1f2c7d75162de80417c9 GIT binary patch literal 7286 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 BJ!=2} literal 0 HcmV?d00001 From 80f7db8db364dc4903bf2ec4888b1269e82cde3a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 23:31:15 +0300 Subject: [PATCH 6018/6909] Add test coverage for the new multiplayer beatmap tracker --- .../TestSceneMultiplayerBeatmapTracker.cs | 178 ++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs diff --git a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs new file mode 100644 index 0000000000..839d47afc2 --- /dev/null +++ b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs @@ -0,0 +1,178 @@ +// Copyright (c) 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 JetBrains.Annotations; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Platform; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Database; +using osu.Game.IO.Archives; +using osu.Game.Online.API; +using osu.Game.Online.Rooms; +using osu.Game.Rulesets; +using osu.Game.Tests.Beatmaps; +using osu.Game.Tests.Resources; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.Online +{ + [HeadlessTest] + public class TestSceneMultiplayerBeatmapTracker : OsuTestScene + { + private RulesetStore rulesets; + private TestBeatmapManager beatmaps; + + private string testBeatmapFile; + private BeatmapInfo testBeatmapInfo; + private BeatmapSetInfo testBeatmapSet; + + private readonly Bindable selectedItem = new Bindable(); + private MultiplayerBeatmapTracker tracker; + + [BackgroundDependencyLoader] + private void load(AudioManager audio, GameHost host) + { + Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); + Dependencies.CacheAs(beatmaps = new TestBeatmapManager(LocalStorage, ContextFactory, rulesets, API, audio, host, Beatmap.Default)); + } + + [SetUp] + public void SetUp() => Schedule(() => + { + testBeatmapFile = getTestBeatmapOsz(); + + testBeatmapInfo = new TestBeatmap(Ruleset.Value).BeatmapInfo; + testBeatmapSet = testBeatmapInfo.BeatmapSet; + + var existing = beatmaps.QueryBeatmapSet(s => s.OnlineBeatmapSetID == testBeatmapSet.OnlineBeatmapSetID); + if (existing != null) + beatmaps.Delete(existing); + + selectedItem.Value = new PlaylistItem + { + Beatmap = { Value = testBeatmapInfo }, + Ruleset = { Value = testBeatmapInfo.Ruleset }, + }; + + Child = tracker = new MultiplayerBeatmapTracker + { + SelectedItem = { BindTarget = selectedItem, } + }; + }); + + [Test] + public void TestBeatmapDownloadingFlow() + { + AddAssert("ensure beatmap unavailable", () => !beatmaps.IsAvailableLocally(testBeatmapSet)); + addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded); + + AddStep("start downloading", () => beatmaps.Download(testBeatmapSet)); + addAvailabilityCheckStep("state downloading 0%", () => BeatmapAvailability.Downloading(0.0)); + + AddStep("set progress 40%", () => ((TestDownloadRequest)beatmaps.GetExistingDownload(testBeatmapSet)).SetProgress(0.4)); + addAvailabilityCheckStep("state downloading 40%", () => BeatmapAvailability.Downloading(0.4)); + + AddStep("finish download", () => ((TestDownloadRequest)beatmaps.GetExistingDownload(testBeatmapSet)).TriggerSuccess(testBeatmapFile)); + addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing); + + AddStep("allow importing", () => beatmaps.AllowImport.Set()); + AddUntilStep("wait for import", () => beatmaps.IsAvailableLocally(testBeatmapSet)); + addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); + } + + [Test] + public void TestTrackerRespectsSoftDeleting() + { + AddStep("allow importing", () => beatmaps.AllowImport.Set()); + AddStep("import beatmap", () => beatmaps.Import(testBeatmapSet).Wait()); + addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); + + AddStep("delete beatmap", () => beatmaps.Delete(testBeatmapSet)); + addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded); + + AddStep("undelete beatmap", () => beatmaps.Undelete(testBeatmapSet)); + addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); + } + + [Test] + public void TestTrackerRespectsChecksum() + { + AddStep("allow importing", () => beatmaps.AllowImport.Set()); + + BeatmapInfo wrongBeatmap = null; + + AddStep("import wrong checksum beatmap", () => + { + wrongBeatmap = new TestBeatmap(Ruleset.Value).BeatmapInfo; + wrongBeatmap.MD5Hash = "1337"; + + beatmaps.Import(wrongBeatmap.BeatmapSet).Wait(); + }); + AddAssert("wrong beatmap available", () => beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == wrongBeatmap.OnlineBeatmapID) != null); + addAvailabilityCheckStep("state still not downloaded", BeatmapAvailability.NotDownloaded); + + AddStep("recreate tracker", () => Child = tracker = new MultiplayerBeatmapTracker + { + SelectedItem = { BindTarget = selectedItem } + }); + addAvailabilityCheckStep("state not downloaded as well", BeatmapAvailability.NotDownloaded); + } + + private void addAvailabilityCheckStep(string description, Func expected) + { + AddAssert(description, () => tracker.Availability.Value.Equals(expected.Invoke())); + } + + private string getTestBeatmapOsz() + { + var filename = Path.GetTempFileName() + ".osz"; + + using (var stream = TestResources.OpenResource("Archives/test-beatmap.osz")) + using (var file = File.Create(filename)) + stream.CopyTo(file); + + return filename; + } + + private class TestBeatmapManager : BeatmapManager + { + public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim(); + + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) + => new TestDownloadRequest(set); + + public TestBeatmapManager(Storage storage, IDatabaseContextFactory contextFactory, RulesetStore rulesets, IAPIProvider api, [NotNull] AudioManager audioManager, GameHost host = null, WorkingBeatmap defaultBeatmap = null, bool performOnlineLookups = false) + : base(storage, contextFactory, rulesets, api, audioManager, host, defaultBeatmap, performOnlineLookups) + { + } + + public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, CancellationToken cancellationToken = default) + { + while (!AllowImport.IsSet) + await Task.Delay(10, cancellationToken); + + return await base.Import(item, archive, cancellationToken); + } + } + + private class TestDownloadRequest : ArchiveDownloadRequest + { + public new void SetProgress(double progress) => base.SetProgress(progress); + + public TestDownloadRequest(BeatmapSetInfo model) + : base(model) + { + } + + protected override string Target => null; + } + } +} From 59ae50b0e58d1ff813498093b39c60c73b3e790f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 23:02:30 +0300 Subject: [PATCH 6019/6909] Clean up ready button logic into using `MultiplayerBeatmapTracker` --- .../OnlinePlay/Components/ReadyButton.cs | 62 +++---------------- .../OnlinePlay/Match/Components/Footer.cs | 4 -- .../Match/MultiplayerMatchFooter.cs | 5 -- .../Match/MultiplayerReadyButton.cs | 3 - .../Multiplayer/MultiplayerMatchSubScreen.cs | 5 +- .../Playlists/PlaylistsReadyButton.cs | 6 +- .../Playlists/PlaylistsRoomSubScreen.cs | 5 +- 7 files changed, 16 insertions(+), 74 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs index 64ddba669d..d4e34b5331 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ReadyButton.cs @@ -1,77 +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 osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Game.Beatmaps; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; +using osu.Game.Online; using osu.Game.Online.Rooms; namespace osu.Game.Screens.OnlinePlay.Components { public abstract class ReadyButton : TriangleButton { - public readonly Bindable SelectedItem = new Bindable(); - public new readonly BindableBool Enabled = new BindableBool(); - [Resolved] - protected IBindable GameBeatmap { get; private set; } - - [Resolved] - private BeatmapManager beatmaps { get; set; } - - private bool hasBeatmap; - - private IBindable> managerUpdated; - private IBindable> managerRemoved; + private IBindable availability; [BackgroundDependencyLoader] - private void load(OsuColour colours) + private void load(MultiplayerBeatmapTracker beatmapTracker) { - managerUpdated = beatmaps.ItemUpdated.GetBoundCopy(); - managerUpdated.BindValueChanged(beatmapUpdated); - managerRemoved = beatmaps.ItemRemoved.GetBoundCopy(); - managerRemoved.BindValueChanged(beatmapRemoved); + availability = beatmapTracker.Availability.GetBoundCopy(); - SelectedItem.BindValueChanged(item => updateSelectedItem(item.NewValue), true); + availability.BindValueChanged(_ => updateState()); + Enabled.BindValueChanged(_ => updateState(), true); } - private void updateSelectedItem(PlaylistItem _) => Scheduler.AddOnce(updateBeatmapState); - private void beatmapUpdated(ValueChangedEvent> _) => Scheduler.AddOnce(updateBeatmapState); - private void beatmapRemoved(ValueChangedEvent> _) => Scheduler.AddOnce(updateBeatmapState); - - private void updateBeatmapState() - { - int? beatmapId = SelectedItem.Value?.Beatmap.Value?.OnlineBeatmapID; - string checksum = SelectedItem.Value?.Beatmap.Value?.MD5Hash; - - if (beatmapId == null || checksum == null) - return; - - var databasedBeatmap = beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); - - hasBeatmap = databasedBeatmap?.BeatmapSet?.DeletePending == false; - } - - protected override void Update() - { - base.Update(); - - updateEnabledState(); - } - - private void updateEnabledState() - { - if (GameBeatmap.Value == null || SelectedItem.Value == null) - { - base.Enabled.Value = false; - return; - } - - base.Enabled.Value = hasBeatmap && Enabled.Value; - } + private void updateState() => base.Enabled.Value = availability.Value.State == DownloadState.LocallyAvailable && Enabled.Value; } } diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs b/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs index 5c27d78d50..e91c46beed 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/Footer.cs @@ -3,13 +3,11 @@ 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; using osu.Game.Graphics; -using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Playlists; using osuTK; @@ -20,7 +18,6 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components public const float HEIGHT = 50; public Action OnStart; - public readonly Bindable SelectedItem = new Bindable(); private readonly Drawable background; @@ -37,7 +34,6 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(600, 50), - SelectedItem = { BindTarget = SelectedItem }, Action = () => OnStart?.Invoke() } }; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs index bbf861fac3..fdc1ae9d3c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchFooter.cs @@ -3,13 +3,11 @@ 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; using osu.Game.Graphics; -using osu.Game.Online.Rooms; using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match @@ -18,8 +16,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public const float HEIGHT = 50; - public readonly Bindable SelectedItem = new Bindable(); - public Action OnReadyClick { set => readyButton.OnReadyClick = value; @@ -41,7 +37,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(600, 50), - SelectedItem = { BindTarget = SelectedItem } } }; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 04030cdbfd..389a2380fa 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -13,7 +13,6 @@ using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; -using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osuTK; @@ -21,8 +20,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { public class MultiplayerReadyButton : MultiplayerRoomComposite { - public Bindable SelectedItem => button.SelectedItem; - public Action OnReadyClick { set => button.Action = value; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 80991569dc..a641935b9a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + AddRangeInternal(new Drawable[] { new GridContainer { @@ -161,7 +161,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { new MultiplayerMatchFooter { - SelectedItem = { BindTarget = SelectedItem }, OnReadyClick = onReadyClick } } @@ -177,7 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RelativeSizeAxes = Axes.Both, State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden } } - }; + }); } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs index edee8e571a..9ac1fe1722 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsReadyButton.cs @@ -4,6 +4,7 @@ using System; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; @@ -15,6 +16,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved(typeof(Room), nameof(Room.EndDate))] private Bindable endDate { get; set; } + [Resolved] + private IBindable gameBeatmap { get; set; } + public PlaylistsReadyButton() { Text = "Start"; @@ -32,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { base.Update(); - Enabled.Value = endDate.Value != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(GameBeatmap.Value.Track.Length) < endDate.Value; + Enabled.Value = endDate.Value != null && DateTimeOffset.UtcNow.AddSeconds(30).AddMilliseconds(gameBeatmap.Value.Track.Length) < endDate.Value; } } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index e76ca995bf..7b3cdf16db 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + AddRangeInternal(new Drawable[] { new GridContainer { @@ -173,7 +173,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new Footer { OnStart = onStart, - SelectedItem = { BindTarget = SelectedItem } } } }, @@ -189,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists EditPlaylist = () => this.Push(new MatchSongSelect()), State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden } } - }; + }); } [Resolved] From 63c0dc9bd9cf9d7c7f7251aa91d778c23a021071 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 16 Jan 2021 23:04:28 +0300 Subject: [PATCH 6020/6909] Update ready button test scene with new logic --- .../TestSceneMultiplayerReadyButton.cs | 31 +++++++++++++------ 1 file changed, 21 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 878776bf51..1357adb39a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -6,6 +6,7 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Testing; @@ -26,8 +27,11 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneMultiplayerReadyButton : MultiplayerTestScene { private MultiplayerReadyButton button; + private MultiplayerBeatmapTracker beatmapTracker; private BeatmapSetInfo importedSet; + private readonly Bindable selectedItem = new Bindable(); + private BeatmapManager beatmaps; private RulesetStore rulesets; @@ -39,6 +43,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait(); + + Add(beatmapTracker = new MultiplayerBeatmapTracker + { + SelectedItem = { BindTarget = selectedItem } + }); + + Dependencies.Cache(beatmapTracker); } [SetUp] @@ -46,20 +57,20 @@ namespace osu.Game.Tests.Visual.Multiplayer { importedSet = beatmaps.GetAllUsableBeatmapSetsEnumerable(IncludedDetails.All).First(); Beatmap.Value = beatmaps.GetWorkingBeatmap(importedSet.Beatmaps.First()); + selectedItem.Value = new PlaylistItem + { + Beatmap = { Value = Beatmap.Value.BeatmapInfo }, + Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset }, + }; - Child = button = new MultiplayerReadyButton + if (button != null) + Remove(button); + + Add(button = new MultiplayerReadyButton { Anchor = Anchor.Centre, Origin = Anchor.Centre, Size = new Vector2(200, 50), - SelectedItem = - { - Value = new PlaylistItem - { - Beatmap = { Value = Beatmap.Value.BeatmapInfo }, - Ruleset = { Value = Beatmap.Value.BeatmapInfo.Ruleset } - } - }, OnReadyClick = async () => { readyClickOperation = OngoingOperationTracker.BeginOperation(); @@ -73,7 +84,7 @@ namespace osu.Game.Tests.Visual.Multiplayer await Client.ToggleReady(); readyClickOperation.Dispose(); } - }; + }); }); [Test] From bb0d2899931bbefedf3229d2b963a5d368fc6177 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Jan 2021 23:24:28 +0100 Subject: [PATCH 6021/6909] Split variable for readability --- 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 d3273ba170..716796a585 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -426,7 +426,8 @@ namespace osu.Game { var imports = tasks.Where(t => t.Path.EndsWith(ext, StringComparison.OrdinalIgnoreCase)); - return fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(ext))?.Import(imports.ToArray()) ?? Task.CompletedTask; + var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(ext)); + return importer?.Import(imports.ToArray()) ?? Task.CompletedTask; })); } From dee46d7ba201e70c463f68f5b4cf38c43a87d085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 16 Jan 2021 23:42:28 +0100 Subject: [PATCH 6022/6909] Use GroupBy() instead --- osu.Game/OsuGameBase.cs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 716796a585..1f8ae54e55 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -421,13 +421,11 @@ namespace osu.Game public virtual async Task Import(params ImportTask[] tasks) { - var extensions = tasks.Select(t => Path.GetExtension(t.Path).ToLowerInvariant()).Distinct(); - await Task.WhenAll(extensions.Select(ext => + var tasksPerExtension = tasks.GroupBy(t => Path.GetExtension(t.Path).ToLowerInvariant()); + await Task.WhenAll(tasksPerExtension.Select(taskGroup => { - var imports = tasks.Where(t => t.Path.EndsWith(ext, StringComparison.OrdinalIgnoreCase)); - - var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(ext)); - return importer?.Import(imports.ToArray()) ?? Task.CompletedTask; + var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key)); + return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask; })); } From 816cc7a59b146cff51773d9f3ec862e0d61af7ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Jan 2021 00:35:23 +0100 Subject: [PATCH 6023/6909] Adjust explicit label spacing on beatmap set overlay --- osu.Game/Overlays/BeatmapSet/Header.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 9f2b40dfa4..a84ed5ac29 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -145,14 +145,14 @@ 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 = 5, Bottom = 4 }, // To better lineup with the font }, explicitPill = new ExplicitBeatmapPill { Alpha = 0f, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Left = 15f, Top = 4 }, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Left = 10, Bottom = 4 }, } } }, From eb53e32792dcb36c1aac9d612dc24f09733487dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Jan 2021 14:40:22 +0100 Subject: [PATCH 6024/6909] Use task completion source for room join flow On Android, users were unable to join or create multiplayer rooms. The root cause of that was that the both the wait and set of the `ManualResetEvent` in `getRoomUsers` occurred on the same thread, which created a chicken-and-egg situation - the set could not proceed until the wait had actually completed. Resolve by substituting the `ManualResetEvent` for a `TaskCompletionSource` to achieve a promise-style task, which the previous code was a crude approximation of anyway. Closes #11385. --- .../Multiplayer/StatefulMultiplayerClient.cs | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index fbdfb6a8c5..f0e11b2b8b 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -7,7 +7,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -128,7 +127,8 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(Room != null); - var users = getRoomUsers(); + var users = await getRoomUsers(); + Debug.Assert(users != null); await Task.WhenAll(users.Select(PopulateUser)); @@ -437,24 +437,20 @@ namespace osu.Game.Online.Multiplayer /// This should be used whenever accessing users from outside of an Update thread context (ie. when not calling ). /// /// A copy of users in the current room, or null if unavailable. - private List? getRoomUsers() + private Task?> getRoomUsers() { - List? users = null; - - ManualResetEventSlim resetEvent = new ManualResetEventSlim(); + var tcs = new TaskCompletionSource?>(); // at some point we probably want to replace all these schedule calls with Room.LockForUpdate. // for now, as this would require quite some consideration due to the number of accesses to the room instance, // let's just add a manual schedule for the non-scheduled usages instead. Scheduler.Add(() => { - users = Room?.Users.ToList(); - resetEvent.Set(); + var users = Room?.Users.ToList(); + tcs.SetResult(users); }, false); - resetEvent.Wait(100); - - return users; + return tcs.Task; } /// From 5fd644fc576de33cab2f4b0daeb323b262b3b98e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 17 Jan 2021 22:40:24 +0900 Subject: [PATCH 6025/6909] Unify variable names --- .../TestSceneBeatmapListingSearchControl.cs | 10 +++++----- osu.Game/Configuration/OsuConfigManager.cs | 4 ++-- .../Online/API/Requests/SearchBeatmapSetsRequest.cs | 8 ++++---- .../BeatmapListing/BeatmapListingFilterControl.cs | 4 ++-- .../BeatmapListing/BeatmapListingSearchControl.cs | 10 +++++----- .../Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs | 2 +- .../Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs | 2 +- ...citBeatmapPill.cs => ExplicitContentBeatmapPill.cs} | 4 ++-- osu.Game/Overlays/BeatmapSet/Header.cs | 6 +++--- .../Overlays/Settings/Sections/Online/WebSettings.cs | 2 +- .../Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 6 +++--- 11 files changed, 29 insertions(+), 29 deletions(-) rename osu.Game/Overlays/BeatmapSet/{ExplicitBeatmapPill.cs => ExplicitContentBeatmapPill.cs} (92%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs index dc46da6293..a9747e73f9 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneBeatmapListingSearchControl.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.UserInterface control.Extra.BindCollectionChanged((u, v) => extra.Text = $"Extra: {(control.Extra.Any() ? string.Join('.', control.Extra.Select(i => i.ToString().ToLowerInvariant())) : "")}", true); control.Ranks.BindCollectionChanged((u, v) => ranks.Text = $"Ranks: {(control.Ranks.Any() ? string.Join('.', control.Ranks.Select(i => i.ToString())) : "")}", true); control.Played.BindValueChanged(p => played.Text = $"Played: {p.NewValue}", true); - control.Explicit.BindValueChanged(e => explicitMap.Text = $"Explicit Maps: {e.NewValue}", true); + control.ExplicitContent.BindValueChanged(e => explicitMap.Text = $"Explicit Maps: {e.NewValue}", true); }); [Test] @@ -92,11 +92,11 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestExplicitConfig() { - AddStep("configure explicit content to allowed", () => localConfig.Set(OsuSetting.AllowExplicitContent, true)); - AddAssert("explicit control set to show", () => control.Explicit.Value == SearchExplicit.Show); + AddStep("configure explicit content to allowed", () => localConfig.Set(OsuSetting.ShowOnlineExplicitContent, true)); + AddAssert("explicit control set to show", () => control.ExplicitContent.Value == SearchExplicit.Show); - AddStep("configure explicit content to disallowed", () => localConfig.Set(OsuSetting.AllowExplicitContent, false)); - AddAssert("explicit control set to hide", () => control.Explicit.Value == SearchExplicit.Hide); + AddStep("configure explicit content to disallowed", () => localConfig.Set(OsuSetting.ShowOnlineExplicitContent, false)); + AddAssert("explicit control set to hide", () => control.ExplicitContent.Value == SearchExplicit.Hide); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Configuration/OsuConfigManager.cs b/osu.Game/Configuration/OsuConfigManager.cs index 5e7a843baf..6b48501dac 100644 --- a/osu.Game/Configuration/OsuConfigManager.cs +++ b/osu.Game/Configuration/OsuConfigManager.cs @@ -60,7 +60,7 @@ namespace osu.Game.Configuration Set(OsuSetting.ExternalLinkWarning, true); Set(OsuSetting.PreferNoVideo, false); - Set(OsuSetting.AllowExplicitContent, false); + Set(OsuSetting.ShowOnlineExplicitContent, false); // Audio Set(OsuSetting.VolumeInactive, 0.25, 0, 1, 0.01); @@ -272,6 +272,6 @@ namespace osu.Game.Configuration EditorWaveformOpacity, DiscordRichPresence, AutomaticallyDownloadWhenSpectating, - AllowExplicitContent, + ShowOnlineExplicitContent, } } diff --git a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs index 939d3c6cb4..5360d36f3d 100644 --- a/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs +++ b/osu.Game/Online/API/Requests/SearchBeatmapSetsRequest.cs @@ -30,7 +30,7 @@ namespace osu.Game.Online.API.Requests public SearchPlayed Played { get; } - public SearchExplicit Explicit { get; } + public SearchExplicit ExplicitContent { get; } [CanBeNull] public IReadOnlyCollection Ranks { get; } @@ -53,7 +53,7 @@ namespace osu.Game.Online.API.Requests IReadOnlyCollection extra = null, IReadOnlyCollection ranks = null, SearchPlayed played = SearchPlayed.Any, - SearchExplicit explicitMaps = SearchExplicit.Hide) + SearchExplicit explicitContent = SearchExplicit.Hide) { this.query = string.IsNullOrEmpty(query) ? string.Empty : System.Uri.EscapeDataString(query); this.ruleset = ruleset; @@ -67,7 +67,7 @@ namespace osu.Game.Online.API.Requests Extra = extra; Ranks = ranks; Played = played; - Explicit = explicitMaps; + ExplicitContent = explicitContent; } protected override WebRequest CreateWebRequest() @@ -97,7 +97,7 @@ namespace osu.Game.Online.API.Requests if (Played != SearchPlayed.Any) req.AddParameter("played", Played.ToString().ToLowerInvariant()); - req.AddParameter("nsfw", Explicit == SearchExplicit.Show ? "true" : "false"); + req.AddParameter("nsfw", ExplicitContent == SearchExplicit.Show ? "true" : "false"); req.AddCursor(cursor); diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs index 650adcb4a9..bcc5a91677 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingFilterControl.cs @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Extra.CollectionChanged += (_, __) => queueUpdateSearch(); searchControl.Ranks.CollectionChanged += (_, __) => queueUpdateSearch(); searchControl.Played.BindValueChanged(_ => queueUpdateSearch()); - searchControl.Explicit.BindValueChanged(_ => queueUpdateSearch()); + searchControl.ExplicitContent.BindValueChanged(_ => queueUpdateSearch()); sortCriteria.BindValueChanged(_ => queueUpdateSearch()); sortDirection.BindValueChanged(_ => queueUpdateSearch()); @@ -195,7 +195,7 @@ namespace osu.Game.Overlays.BeatmapListing searchControl.Extra, searchControl.Ranks, searchControl.Played.Value, - searchControl.Explicit.Value); + searchControl.ExplicitContent.Value); getSetsRequest.Success += response => { diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs index 5c6267b726..b138a5ac52 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapListingSearchControl.cs @@ -43,7 +43,7 @@ namespace osu.Game.Overlays.BeatmapListing public Bindable Played => playedFilter.Current; - public Bindable Explicit => explicitFilter.Current; + public Bindable ExplicitContent => explicitContentFilter.Current; public BeatmapSetInfo BeatmapSet { @@ -68,7 +68,7 @@ namespace osu.Game.Overlays.BeatmapListing private readonly BeatmapSearchMultipleSelectionFilterRow extraFilter; private readonly BeatmapSearchScoreFilterRow ranksFilter; private readonly BeatmapSearchFilterRow playedFilter; - private readonly BeatmapSearchFilterRow explicitFilter; + private readonly BeatmapSearchFilterRow explicitContentFilter; private readonly Box background; private readonly UpdateableBeatmapSetCover beatmapCover; @@ -130,7 +130,7 @@ namespace osu.Game.Overlays.BeatmapListing extraFilter = new BeatmapSearchMultipleSelectionFilterRow(@"Extra"), ranksFilter = new BeatmapSearchScoreFilterRow(), playedFilter = new BeatmapSearchFilterRow(@"Played"), - explicitFilter = new BeatmapSearchFilterRow(@"Explicit Maps"), + explicitContentFilter = new BeatmapSearchFilterRow(@"Explicit Content"), } } } @@ -148,10 +148,10 @@ namespace osu.Game.Overlays.BeatmapListing { background.Colour = colourProvider.Dark6; - allowExplicitContent = config.GetBindable(OsuSetting.AllowExplicitContent); + allowExplicitContent = config.GetBindable(OsuSetting.ShowOnlineExplicitContent); allowExplicitContent.BindValueChanged(allow => { - Explicit.Value = allow.NewValue ? SearchExplicit.Show : SearchExplicit.Hide; + ExplicitContent.Value = allow.NewValue ? SearchExplicit.Show : SearchExplicit.Hide; }, true); } diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs index b7002a96e5..c1d366bb82 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs @@ -205,7 +205,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels if (SetInfo.OnlineInfo?.HasExplicitContent ?? false) { - titleContainer.Add(new ExplicitBeatmapPill + titleContainer.Add(new ExplicitContentBeatmapPill { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs index 69671ab75b..76a30d1c11 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs @@ -219,7 +219,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels if (SetInfo.OnlineInfo?.HasExplicitContent ?? false) { - titleContainer.Add(new ExplicitBeatmapPill + titleContainer.Add(new ExplicitContentBeatmapPill { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, diff --git a/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs b/osu.Game/Overlays/BeatmapSet/ExplicitContentBeatmapPill.cs similarity index 92% rename from osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs rename to osu.Game/Overlays/BeatmapSet/ExplicitContentBeatmapPill.cs index aefb3299a5..329f8ee0a2 100644 --- a/osu.Game/Overlays/BeatmapSet/ExplicitBeatmapPill.cs +++ b/osu.Game/Overlays/BeatmapSet/ExplicitContentBeatmapPill.cs @@ -10,9 +10,9 @@ using osu.Game.Graphics.Sprites; namespace osu.Game.Overlays.BeatmapSet { - public class ExplicitBeatmapPill : CompositeDrawable + public class ExplicitContentBeatmapPill : CompositeDrawable { - public ExplicitBeatmapPill() + public ExplicitContentBeatmapPill() { AutoSizeAxes = Axes.Both; } diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs index 876a7e8917..916c21c010 100644 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ b/osu.Game/Overlays/BeatmapSet/Header.cs @@ -34,7 +34,7 @@ namespace osu.Game.Overlays.BeatmapSet private readonly Box coverGradient; private readonly OsuSpriteText title, artist; private readonly AuthorInfo author; - private readonly ExplicitBeatmapPill explicitPill; + private readonly ExplicitContentBeatmapPill explicitContentPill; private readonly FillFlowContainer downloadButtonsContainer; private readonly BeatmapAvailability beatmapAvailability; private readonly BeatmapSetOnlineStatusPill onlineStatusPill; @@ -147,7 +147,7 @@ namespace osu.Game.Overlays.BeatmapSet Origin = Anchor.BottomLeft, Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font }, - explicitPill = new ExplicitBeatmapPill + explicitContentPill = new ExplicitContentBeatmapPill { Alpha = 0f, Anchor = Anchor.BottomLeft, @@ -261,7 +261,7 @@ namespace osu.Game.Overlays.BeatmapSet title.Text = setInfo.NewValue.Metadata.Title ?? string.Empty; artist.Text = setInfo.NewValue.Metadata.Artist ?? string.Empty; - explicitPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; + explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; onlineStatusPill.FadeIn(500, Easing.OutQuint); onlineStatusPill.Status = setInfo.NewValue.OnlineInfo.Status; diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index da7ef46f65..3e1cc1d91e 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -37,7 +37,7 @@ namespace osu.Game.Overlays.Settings.Sections.Online { LabelText = "Hide warnings for explicit content in beatmaps", Keywords = new[] { "nsfw", "18+", "offensive" }, - Current = config.GetBindable(OsuSetting.AllowExplicitContent), + Current = config.GetBindable(OsuSetting.ShowOnlineExplicitContent), } }; } diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 7987d715e3..b16f82fce9 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay private Container difficultyIconContainer; private LinkFlowContainer beatmapText; private LinkFlowContainer authorText; - private ExplicitBeatmapPill explicitPill; + private ExplicitContentBeatmapPill explicitContentPill; private ModDisplay modDisplay; private readonly Bindable beatmap = new Bindable(); @@ -119,7 +119,7 @@ namespace osu.Game.Screens.OnlinePlay } bool hasExplicitContent = Item.Beatmap.Value.BeatmapSet.OnlineInfo?.HasExplicitContent == true; - explicitPill.Alpha = hasExplicitContent ? 1 : 0; + explicitContentPill.Alpha = hasExplicitContent ? 1 : 0; modDisplay.Current.Value = requiredMods.ToArray(); } @@ -181,7 +181,7 @@ namespace osu.Game.Screens.OnlinePlay Children = new Drawable[] { authorText = new LinkFlowContainer { AutoSizeAxes = Axes.Both }, - explicitPill = new ExplicitBeatmapPill + explicitContentPill = new ExplicitContentBeatmapPill { Alpha = 0f, Anchor = Anchor.CentreLeft, From 5278cad393aeb1d76f7603cda445fb67b2d46234 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 17 Jan 2021 22:40:58 +0900 Subject: [PATCH 6026/6909] Reword setting to make more sense --- osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs index 3e1cc1d91e..59bcbe4d89 100644 --- a/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Online/WebSettings.cs @@ -35,7 +35,7 @@ namespace osu.Game.Overlays.Settings.Sections.Online }, new SettingsCheckbox { - LabelText = "Hide warnings for explicit content in beatmaps", + LabelText = "Show explicit content in search results", Keywords = new[] { "nsfw", "18+", "offensive" }, Current = config.GetBindable(OsuSetting.ShowOnlineExplicitContent), } From 39746cb3de8ad11221ff1766822f720918d49858 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 00:11:35 +0900 Subject: [PATCH 6027/6909] Test removing nuget restore from iOS/Android build scripts --- fastlane/Fastfile | 8 -------- 1 file changed, 8 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8c278604aa..18b5907e82 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -48,10 +48,6 @@ desc 'Deploy to play store' desc 'Compile the project' lane :build do |options| - nuget_restore( - project_path: 'osu.sln' - ) - souyuz( build_configuration: 'Release', solution_path: 'osu.sln', @@ -107,10 +103,6 @@ platform :ios do desc 'Compile the project' lane :build do - nuget_restore( - project_path: 'osu.sln' - ) - souyuz( platform: "ios", plist_path: "osu.iOS/Info.plist" From 585aa87c5389b16dc9c8d4c17c38365cb0100174 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 17 Jan 2021 19:17:14 +0300 Subject: [PATCH 6028/6909] Fix playlist item download button never shown back after hiding --- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 8 +++++++- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 17d85d1120..c6cfd3c64a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -201,11 +201,17 @@ namespace osu.Game.Tests.Visual.Multiplayer } [Test] - public void TestDownloadButtonHiddenInitiallyWhenBeatmapExists() + public void TestDownloadButtonHiddenWhenBeatmapExists() { createPlaylist(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo); AddAssert("download button hidden", () => !playlist.ChildrenOfType().Single().IsPresent); + + AddStep("delete beatmap set", () => manager.Delete(manager.QueryBeatmapSets(_ => true).Single())); + AddUntilStep("download button shown", () => playlist.ChildrenOfType().Single().IsPresent); + + AddStep("undelete beatmap set", () => manager.Undelete(manager.QueryBeatmapSets(_ => true).Single())); + AddUntilStep("download button hidden", () => !playlist.ChildrenOfType().Single().IsPresent); } [Test] diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index b16f82fce9..f8982582d5 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -249,6 +249,8 @@ namespace osu.Game.Screens.OnlinePlay [Resolved] private BeatmapManager beatmapManager { get; set; } + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + public PlaylistDownloadButton(PlaylistItem playlistItem) : base(playlistItem.Beatmap.Value.BeatmapSet) { From 2b23c8eabd02a1c3743d7538bbecb9ae3d224333 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 17 Jan 2021 18:08:58 +0100 Subject: [PATCH 6029/6909] Use alpha directly for checking visibility in test `IsPresent` is no longer synonymous with being visible, after applying the fix to the issue in question. --- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index c6cfd3c64a..874c1694eb 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -205,13 +205,16 @@ namespace osu.Game.Tests.Visual.Multiplayer { createPlaylist(new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo); - AddAssert("download button hidden", () => !playlist.ChildrenOfType().Single().IsPresent); + assertDownloadButtonVisible(false); AddStep("delete beatmap set", () => manager.Delete(manager.QueryBeatmapSets(_ => true).Single())); - AddUntilStep("download button shown", () => playlist.ChildrenOfType().Single().IsPresent); + assertDownloadButtonVisible(true); AddStep("undelete beatmap set", () => manager.Undelete(manager.QueryBeatmapSets(_ => true).Single())); - AddUntilStep("download button hidden", () => !playlist.ChildrenOfType().Single().IsPresent); + assertDownloadButtonVisible(false); + + void assertDownloadButtonVisible(bool visible) => AddUntilStep($"download button {(visible ? "shown" : "hidden")}", + () => playlist.ChildrenOfType().Single().Alpha == (visible ? 1 : 0)); } [Test] From 172552d55173791a5a5293bcab1d2240b8b02783 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 17 Jan 2021 21:02:33 +0300 Subject: [PATCH 6030/6909] Use `TaskCompletionSource` for better awaiting --- .../Online/TestSceneMultiplayerBeatmapTracker.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs index 839d47afc2..05e8f8b2e4 100644 --- a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs +++ b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs @@ -47,6 +47,8 @@ namespace osu.Game.Tests.Online [SetUp] public void SetUp() => Schedule(() => { + beatmaps.AllowImport = new TaskCompletionSource(); + testBeatmapFile = getTestBeatmapOsz(); testBeatmapInfo = new TestBeatmap(Ruleset.Value).BeatmapInfo; @@ -83,7 +85,7 @@ namespace osu.Game.Tests.Online AddStep("finish download", () => ((TestDownloadRequest)beatmaps.GetExistingDownload(testBeatmapSet)).TriggerSuccess(testBeatmapFile)); addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing); - AddStep("allow importing", () => beatmaps.AllowImport.Set()); + AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddUntilStep("wait for import", () => beatmaps.IsAvailableLocally(testBeatmapSet)); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); } @@ -91,7 +93,7 @@ namespace osu.Game.Tests.Online [Test] public void TestTrackerRespectsSoftDeleting() { - AddStep("allow importing", () => beatmaps.AllowImport.Set()); + AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); AddStep("import beatmap", () => beatmaps.Import(testBeatmapSet).Wait()); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); @@ -105,7 +107,7 @@ namespace osu.Game.Tests.Online [Test] public void TestTrackerRespectsChecksum() { - AddStep("allow importing", () => beatmaps.AllowImport.Set()); + AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); BeatmapInfo wrongBeatmap = null; @@ -144,7 +146,7 @@ namespace osu.Game.Tests.Online private class TestBeatmapManager : BeatmapManager { - public readonly ManualResetEventSlim AllowImport = new ManualResetEventSlim(); + public TaskCompletionSource AllowImport = new TaskCompletionSource(); protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => new TestDownloadRequest(set); @@ -156,9 +158,7 @@ namespace osu.Game.Tests.Online public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, CancellationToken cancellationToken = default) { - while (!AllowImport.IsSet) - await Task.Delay(10, cancellationToken); - + await AllowImport.Task; return await base.Import(item, archive, cancellationToken); } } From d93a853dfd531e9d8d83473b04e3c22b5d290023 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 17 Jan 2021 21:16:45 +0300 Subject: [PATCH 6031/6909] Enforce `float` type in the download progress path instead --- .../Online/TestSceneMultiplayerBeatmapTracker.cs | 8 ++++---- osu.Game/Online/API/ArchiveDownloadRequest.cs | 6 +++--- osu.Game/Online/DownloadTrackingComposite.cs | 2 +- osu.Game/Online/Rooms/BeatmapAvailability.cs | 6 +++--- osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs | 2 +- .../Overlays/Notifications/ProgressNotification.cs | 10 +++++----- 6 files changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs index 05e8f8b2e4..60a4508b3d 100644 --- a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs +++ b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs @@ -77,10 +77,10 @@ namespace osu.Game.Tests.Online addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded); AddStep("start downloading", () => beatmaps.Download(testBeatmapSet)); - addAvailabilityCheckStep("state downloading 0%", () => BeatmapAvailability.Downloading(0.0)); + addAvailabilityCheckStep("state downloading 0%", () => BeatmapAvailability.Downloading(0.0f)); - AddStep("set progress 40%", () => ((TestDownloadRequest)beatmaps.GetExistingDownload(testBeatmapSet)).SetProgress(0.4)); - addAvailabilityCheckStep("state downloading 40%", () => BeatmapAvailability.Downloading(0.4)); + AddStep("set progress 40%", () => ((TestDownloadRequest)beatmaps.GetExistingDownload(testBeatmapSet)).SetProgress(0.4f)); + addAvailabilityCheckStep("state downloading 40%", () => BeatmapAvailability.Downloading(0.4f)); AddStep("finish download", () => ((TestDownloadRequest)beatmaps.GetExistingDownload(testBeatmapSet)).TriggerSuccess(testBeatmapFile)); addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing); @@ -165,7 +165,7 @@ namespace osu.Game.Tests.Online private class TestDownloadRequest : ArchiveDownloadRequest { - public new void SetProgress(double progress) => base.SetProgress(progress); + public new void SetProgress(float progress) => base.SetProgress(progress); public TestDownloadRequest(BeatmapSetInfo model) : base(model) diff --git a/osu.Game/Online/API/ArchiveDownloadRequest.cs b/osu.Game/Online/API/ArchiveDownloadRequest.cs index fdb2a984dc..ccb4e9c119 100644 --- a/osu.Game/Online/API/ArchiveDownloadRequest.cs +++ b/osu.Game/Online/API/ArchiveDownloadRequest.cs @@ -12,16 +12,16 @@ namespace osu.Game.Online.API public double Progress { get; private set; } - public event Action DownloadProgressed; + public event Action DownloadProgressed; protected ArchiveDownloadRequest(TModel model) { Model = model; - Progressed += (current, total) => SetProgress((double)current / total); + Progressed += (current, total) => SetProgress((float)current / total); } - protected void SetProgress(double progress) + protected void SetProgress(float progress) { Progress = progress; DownloadProgressed?.Invoke(progress); diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index b72cf38369..1631d9790b 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -144,7 +144,7 @@ namespace osu.Game.Online private void onRequestSuccess(string _) => Schedule(() => State.Value = DownloadState.Importing); - private void onRequestProgress(double progress) => Schedule(() => Progress.Value = progress); + private void onRequestProgress(float progress) => Schedule(() => Progress.Value = progress); private void onRequestFailure(Exception e) => Schedule(() => attachDownload(null)); diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index e7dbc5f436..170009a85b 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -19,17 +19,17 @@ namespace osu.Game.Online.Rooms /// /// The beatmap's downloading progress, null when not in state. /// - public readonly double? DownloadProgress; + public readonly float? DownloadProgress; [JsonConstructor] - private BeatmapAvailability(DownloadState state, double? downloadProgress = null) + private BeatmapAvailability(DownloadState state, float? downloadProgress = null) { State = state; DownloadProgress = downloadProgress; } public static BeatmapAvailability NotDownloaded() => new BeatmapAvailability(DownloadState.NotDownloaded); - public static BeatmapAvailability Downloading(double progress) => new BeatmapAvailability(DownloadState.Downloading, progress); + public static BeatmapAvailability Downloading(float progress) => new BeatmapAvailability(DownloadState.Downloading, progress); public static BeatmapAvailability Importing() => new BeatmapAvailability(DownloadState.Importing); public static BeatmapAvailability LocallyAvailable() => new BeatmapAvailability(DownloadState.LocallyAvailable); diff --git a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs index b22d17f3ef..b15399ad62 100644 --- a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs +++ b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Rooms break; case DownloadState.Downloading: - availability.Value = BeatmapAvailability.Downloading(Progress.Value); + availability.Value = BeatmapAvailability.Downloading((float)Progress.Value); break; case DownloadState.Importing: diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index e18bab8e83..3105ecd742 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -23,9 +23,9 @@ namespace osu.Game.Overlays.Notifications public string CompletionText { get; set; } = "Task has completed!"; - private double progress; + private float progress; - public double Progress + public float Progress { get => progress; set @@ -185,9 +185,9 @@ namespace osu.Game.Overlays.Notifications private Color4 colourActive; private Color4 colourInactive; - private double progress; + private float progress; - public double Progress + public float Progress { get => progress; set @@ -195,7 +195,7 @@ namespace osu.Game.Overlays.Notifications if (progress == value) return; progress = value; - box.ResizeTo(new Vector2((float)progress, 1), 100, Easing.OutQuad); + box.ResizeTo(new Vector2(progress, 1), 100, Easing.OutQuad); } } From 0425a659a8c0bae3117e77dd512e899ca6a5c3da Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 17 Jan 2021 21:19:55 +0300 Subject: [PATCH 6032/6909] Add null-permissive operator to manager back --- osu.Game/Online/DownloadTrackingComposite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 1631d9790b..188cb9be7a 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -80,7 +80,7 @@ namespace osu.Game.Online /// By default, this calls , /// but can be overriden to add additional checks for verifying the model in database. /// - protected virtual bool IsModelAvailableLocally() => Manager.IsAvailableLocally(Model.Value); + protected virtual bool IsModelAvailableLocally() => Manager?.IsAvailableLocally(Model.Value) == true; private void downloadBegan(ValueChangedEvent>> weakRequest) { From ccef50e2a2e16767565c75bdc70c52faba7c57e4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 17 Jan 2021 22:04:12 +0300 Subject: [PATCH 6033/6909] Log important message if user imported bad-checksum beatmap --- osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs index b15399ad62..659f52f00f 100644 --- a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs +++ b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using osu.Framework.Bindables; +using osu.Framework.Logging; using osu.Game.Beatmaps; namespace osu.Game.Online.Rooms @@ -34,6 +35,15 @@ namespace osu.Game.Online.Rooms } protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet) + { + var verified = verifyDatabasedModel(databasedSet); + if (!verified) + Logger.Log("The imported beatmapset does not match the online version.", LoggingTarget.Runtime, LogLevel.Important); + + return verified; + } + + private bool verifyDatabasedModel(BeatmapSetInfo databasedSet) { int? beatmapId = SelectedItem.Value.Beatmap.Value.OnlineBeatmapID; string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; From b6a37c1c15fe2a95b7efb94b61e37de132d9864e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 17 Jan 2021 22:08:28 +0300 Subject: [PATCH 6034/6909] Make `TriggerSuccess(filename)` protected and expose in test instead --- osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs | 1 + osu.Game/Online/API/APIDownloadRequest.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs index 60a4508b3d..4b8992052e 100644 --- a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs +++ b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs @@ -166,6 +166,7 @@ namespace osu.Game.Tests.Online private class TestDownloadRequest : ArchiveDownloadRequest { public new void SetProgress(float progress) => base.SetProgress(progress); + public new void TriggerSuccess(string filename) => base.TriggerSuccess(filename); public TestDownloadRequest(BeatmapSetInfo model) : base(model) diff --git a/osu.Game/Online/API/APIDownloadRequest.cs b/osu.Game/Online/API/APIDownloadRequest.cs index 02c589403c..62e22d8f88 100644 --- a/osu.Game/Online/API/APIDownloadRequest.cs +++ b/osu.Game/Online/API/APIDownloadRequest.cs @@ -29,7 +29,7 @@ namespace osu.Game.Online.API private void request_Progress(long current, long total) => API.Schedule(() => Progressed?.Invoke(current, total)); - internal void TriggerSuccess(string filename) + protected void TriggerSuccess(string filename) { if (this.filename != null) throw new InvalidOperationException("Attempted to trigger success more than once"); From ec00aaef90a9bfe8d768f01148f99a78d043401e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 15 Jan 2021 19:13:55 +0900 Subject: [PATCH 6035/6909] Add nuget deploys for all rulesets --- appveyor_deploy.yml | 73 +++++++++++++++---- .../osu.Game.Rulesets.Catch.csproj | 7 ++ .../osu.Game.Rulesets.Mania.csproj | 7 ++ .../osu.Game.Rulesets.Osu.csproj | 7 ++ .../osu.Game.Rulesets.Taiko.csproj | 7 ++ 5 files changed, 88 insertions(+), 13 deletions(-) diff --git a/appveyor_deploy.yml b/appveyor_deploy.yml index bb4482f501..737e5c43ab 100644 --- a/appveyor_deploy.yml +++ b/appveyor_deploy.yml @@ -1,21 +1,68 @@ clone_depth: 1 version: '{build}' image: Visual Studio 2019 -dotnet_csproj: - patch: true - file: 'osu.Game\osu.Game.csproj' # Use wildcard when it's able to exclude Xamarin projects - version: $(APPVEYOR_REPO_TAG_NAME) -before_build: - - ps: dotnet --info # Useful when version mismatch between CI and local - - ps: nuget restore -verbosity quiet # Only nuget.exe knows both new (.NET Core) and old (Xamarin) projects test: off skip_non_tags: true configuration: Release -build: - project: build\Desktop.proj # Skipping Xamarin Release that's slow and covered by fastlane - parallel: true - verbosity: minimal - publish_nuget: true + +environment: + matrix: + - job_name: osu-game + - job_name: osu-ruleset + job_depends_on: osu-game + - job_name: taiko-ruleset + job_depends_on: osu-game + - job_name: catch-ruleset + job_depends_on: osu-game + - job_name: mania-ruleset + job_depends_on: osu-game + +nuget: + project_feed: true + +for: + - + matrix: + only: + - job_name: osu-game + build_script: + - cmd: dotnet pack osu.Game\osu.Game.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% + - + matrix: + only: + - job_name: osu-ruleset + build_script: + - cmd: dotnet remove osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj reference osu.Game\osu.Game.csproj + - cmd: dotnet add osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% + - cmd: dotnet pack osu.Game.Rulesets.Osu\osu.Game.Rulesets.Osu.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% + - + matrix: + only: + - job_name: taiko-ruleset + build_script: + - cmd: dotnet remove osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj reference osu.Game\osu.Game.csproj + - cmd: dotnet add osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% + - cmd: dotnet pack osu.Game.Rulesets.Taiko\osu.Game.Rulesets.Taiko.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% + - + matrix: + only: + - job_name: catch-ruleset + build_script: + - cmd: dotnet remove osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj reference osu.Game\osu.Game.csproj + - cmd: dotnet add osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% + - cmd: dotnet pack osu.Game.Rulesets.Catch\osu.Game.Rulesets.Catch.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% + - + matrix: + only: + - job_name: mania-ruleset + build_script: + - cmd: dotnet remove osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj reference osu.Game\osu.Game.csproj + - cmd: dotnet add osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj package ppy.osu.Game -v %APPVEYOR_REPO_TAG_NAME% + - cmd: dotnet pack osu.Game.Rulesets.Mania\osu.Game.Rulesets.Mania.csproj /p:Version=%APPVEYOR_REPO_TAG_NAME% + +artifacts: + - path: '**\*.nupkg' + deploy: - provider: Environment - name: nuget + name: nuget \ No newline at end of file diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj index b19affbf9f..5bdf39824c 100644 --- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj +++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj @@ -5,6 +5,13 @@ true catch the fruit. to the beat. + + + osu!catch + ppy.osu.Game.Rulesets.Catch + true + + diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index 07ef1022ae..c30fe8494f 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -5,6 +5,13 @@ true smash the keys. to the beat. + + + osu!mania + ppy.osu.Game.Rulesets.Mania + true + + diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index bffeaabb55..36ee804259 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -5,6 +5,13 @@ true click the circles. to the beat. + + + osu!standard + ppy.osu.Game.Rulesets.Osu + true + + diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index ebed8c6d7c..00deb719e1 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -5,6 +5,13 @@ true bash the drum. to the beat. + + + osu!taiko + ppy.osu.Game.Rulesets.Taiko + true + + From 1b166d809eb5bb5f04bdbcfabfba3916cd66a114 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 18 Jan 2021 11:07:54 +0900 Subject: [PATCH 6036/6909] Adjust package titles --- osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj | 2 +- osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj | 2 +- osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj | 2 +- osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj index 5bdf39824c..e2f95ca177 100644 --- a/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj +++ b/osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj @@ -7,7 +7,7 @@ - osu!catch + osu!catch (ruleset) ppy.osu.Game.Rulesets.Catch true diff --git a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj index c30fe8494f..4f6840f9ca 100644 --- a/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj +++ b/osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj @@ -7,7 +7,7 @@ - osu!mania + osu!mania (ruleset) ppy.osu.Game.Rulesets.Mania true diff --git a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj index 36ee804259..98f1e69bd1 100644 --- a/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj +++ b/osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj @@ -7,7 +7,7 @@ - osu!standard + osu! (ruleset) ppy.osu.Game.Rulesets.Osu true diff --git a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj index 00deb719e1..b752c13d18 100644 --- a/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj +++ b/osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj @@ -7,7 +7,7 @@ - osu!taiko + osu!taiko (ruleset) ppy.osu.Game.Rulesets.Taiko true From 94fee8c31d123b940ddf636cf6982152479cc7ed Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 16:13:58 +0900 Subject: [PATCH 6037/6909] Avoid doing a config lookup if initial conditional fails --- osu.Game/Skinning/SkinProvidingContainer.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index e97822b86e..27cf0c697a 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -74,8 +74,8 @@ namespace osu.Game.Skinning { if (lookup is GlobalSkinColours || lookup is SkinCustomColourLookup) return lookupWithFallback(lookup, AllowColourLookup); - else - return lookupWithFallback(lookup, AllowConfigurationLookup); + + return lookupWithFallback(lookup, AllowConfigurationLookup); } return fallbackSource?.GetConfig(lookup); @@ -83,11 +83,14 @@ namespace osu.Game.Skinning private IBindable lookupWithFallback(TLookup lookup, bool canUseSkinLookup) { - var bindable = skin.GetConfig(lookup); - if (bindable != null && canUseSkinLookup) - return bindable; - else - return fallbackSource?.GetConfig(lookup); + if (canUseSkinLookup) + { + var bindable = skin.GetConfig(lookup); + if (bindable != null) + return bindable; + } + + return fallbackSource?.GetConfig(lookup); } protected virtual void TriggerSourceChanged() => SourceChanged?.Invoke(); From 27ffc9844520036e81a652d9d8d7b45b7b5845be Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 18 Jan 2021 10:48:12 +0300 Subject: [PATCH 6038/6909] Implement WebOverlay component --- .../Online/TestSceneFullscreenOverlay.cs | 4 +- osu.Game/Overlays/BeatmapListingOverlay.cs | 86 ++++++++----------- osu.Game/Overlays/BeatmapSetOverlay.cs | 74 ++++++---------- osu.Game/Overlays/ChangelogOverlay.cs | 56 +++--------- osu.Game/Overlays/DashboardOverlay.cs | 56 ++---------- osu.Game/Overlays/FullscreenOverlay.cs | 26 +++++- osu.Game/Overlays/NewsOverlay.cs | 62 +++---------- osu.Game/Overlays/RankingsOverlay.cs | 81 +++-------------- osu.Game/Overlays/UserProfileOverlay.cs | 13 ++- osu.Game/Overlays/WebOverlay.cs | 50 +++++++++++ 10 files changed, 193 insertions(+), 315 deletions(-) create mode 100644 osu.Game/Overlays/WebOverlay.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs index 8f20bcdcc1..f8b059e471 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs @@ -41,7 +41,7 @@ namespace osu.Game.Tests.Visual.Online private class TestFullscreenOverlay : FullscreenOverlay { public TestFullscreenOverlay() - : base(OverlayColourScheme.Pink, null) + : base(OverlayColourScheme.Pink) { Children = new Drawable[] { @@ -52,6 +52,8 @@ namespace osu.Game.Tests.Visual.Online }, }; } + + protected override OverlayHeader CreateHeader() => null; } } } diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 0c9c995dd6..ae1667d403 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -15,98 +15,82 @@ using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; 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.Overlays.BeatmapListing; using osu.Game.Overlays.BeatmapListing.Panels; using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays { - public class BeatmapListingOverlay : FullscreenOverlay + public class BeatmapListingOverlay : WebOverlay { [Resolved] private PreviewTrackManager previewTrackManager { get; set; } private Drawable currentContent; - private LoadingLayer loadingLayer; private Container panelTarget; private FillFlowContainer foundContent; private NotFoundDrawable notFoundContent; - - private OverlayScrollContainer resultScrollContainer; + private BeatmapListingFilterControl filterControl; public BeatmapListingOverlay() - : base(OverlayColourScheme.Blue, new BeatmapListingHeader()) + : base(OverlayColourScheme.Blue) { } - private BeatmapListingFilterControl filterControl; - [BackgroundDependencyLoader] private void load() { - Children = new Drawable[] + Child = new FillFlowContainer { - new Box + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background6 - }, - resultScrollContainer = new OverlayScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new ReverseChildIDFillFlowContainer + filterControl = new BeatmapListingFilterControl + { + TypingStarted = onTypingStarted, + SearchStarted = onSearchStarted, + SearchFinished = onSearchFinished, + }, + new Container { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, Children = new Drawable[] { - Header, - filterControl = new BeatmapListingFilterControl + new Box { - TypingStarted = onTypingStarted, - SearchStarted = onSearchStarted, - SearchFinished = onSearchFinished, + RelativeSizeAxes = Axes.Both, + Colour = ColourProvider.Background4, }, - new Container + panelTarget = new Container { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, + Padding = new MarginPadding { Horizontal = 20 }, Children = new Drawable[] { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background4, - }, - panelTarget = new Container - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Padding = new MarginPadding { Horizontal = 20 }, - Children = new Drawable[] - { - foundContent = new FillFlowContainer(), - notFoundContent = new NotFoundDrawable(), - } - } - }, - }, - } + foundContent = new FillFlowContainer(), + notFoundContent = new NotFoundDrawable(), + } + } + }, }, - }, - loadingLayer = new LoadingLayer(true) + } }; } + protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader(); + + protected override Color4 GetBackgroundColour() => ColourProvider.Background6; + private void onTypingStarted() { // temporary until the textbox/header is updated to always stay on screen. - resultScrollContainer.ScrollToStart(); + ScrollFlow.ScrollToStart(); } protected override void OnFocus(FocusEvent e) @@ -125,7 +109,7 @@ namespace osu.Game.Overlays previewTrackManager.StopAnyPlaying(this); if (panelTarget.Any()) - loadingLayer.Show(); + Loading.Show(); } private Task panelLoadDelegate; @@ -173,7 +157,7 @@ namespace osu.Game.Overlays private void addContentToPlaceholder(Drawable content) { - loadingLayer.Hide(); + Loading.Hide(); lastFetchDisplayedTime = Time.Current; var lastContent = currentContent; @@ -256,7 +240,7 @@ namespace osu.Game.Overlays bool shouldShowMore = panelLoadDelegate?.IsCompleted != false && Time.Current - lastFetchDisplayedTime > time_between_fetches - && (resultScrollContainer.ScrollableExtent > 0 && resultScrollContainer.IsScrolledToEnd(pagination_scroll_distance)); + && (ScrollFlow.ScrollableExtent > 0 && ScrollFlow.IsScrolledToEnd(pagination_scroll_distance)); if (shouldShowMore) filterControl.FetchNextPage(); diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index bbec62a85a..10953415ed 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Game.Beatmaps; using osu.Game.Graphics.Containers; @@ -16,10 +15,11 @@ using osu.Game.Overlays.BeatmapSet.Scores; using osu.Game.Overlays.Comments; using osu.Game.Rulesets; using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays { - public class BeatmapSetOverlay : FullscreenOverlay // we don't provide a standard header for now. + public class BeatmapSetOverlay : WebOverlay // we don't provide a standard header for now. { public const float X_PADDING = 40; public const float Y_PADDING = 25; @@ -36,55 +36,40 @@ namespace osu.Game.Overlays // receive input outside our bounds so we can trigger a close event on ourselves. public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; - private readonly Box background; - public BeatmapSetOverlay() - : base(OverlayColourScheme.Blue, null) + : base(OverlayColourScheme.Blue) { - OverlayScrollContainer scroll; Info info; CommentsSection comments; - Children = new Drawable[] + Child = new FillFlowContainer { - background = new Box + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Spacing = new Vector2(0, 20), + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both - }, - scroll = new OverlayScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new ReverseChildIDFillFlowContainer + new BeatmapSetLayoutSection { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 20), - Children = new[] + Child = new ReverseChildIDFillFlowContainer { - new BeatmapSetLayoutSection + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - Child = new ReverseChildIDFillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - Header = new Header(), - info = new Info() - } - }, - }, - new ScoresContainer - { - Beatmap = { BindTarget = Header.Picker.Beatmap } - }, - comments = new CommentsSection() + Header = new Header(), + info = new Info() + } }, }, - }, + new ScoresContainer + { + Beatmap = { BindTarget = Header.Picker.Beatmap } + }, + comments = new CommentsSection() + } }; Header.BeatmapSet.BindTo(beatmapSet); @@ -94,16 +79,13 @@ namespace osu.Game.Overlays Header.Picker.Beatmap.ValueChanged += b => { info.Beatmap = b.NewValue; - - scroll.ScrollToStart(); + ScrollFlow.ScrollToStart(); }; } - [BackgroundDependencyLoader] - private void load() - { - background.Colour = ColourProvider.Background6; - } + protected override OverlayHeader CreateHeader() => null; + + protected override Color4 GetBackgroundColour() => ColourProvider.Background6; protected override void PopOutComplete() { diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index c7e9a86fa4..5d99887053 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -11,22 +11,18 @@ using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Graphics.Shapes; -using osu.Game.Graphics.Containers; using osu.Game.Input.Bindings; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; using osu.Game.Overlays.Changelog; +using osuTK.Graphics; namespace osu.Game.Overlays { - public class ChangelogOverlay : FullscreenOverlay + public class ChangelogOverlay : WebOverlay { public readonly Bindable Current = new Bindable(); - private Container content; - private SampleChannel sampleBack; private List builds; @@ -34,45 +30,14 @@ namespace osu.Game.Overlays protected List Streams; public ChangelogOverlay() - : base(OverlayColourScheme.Purple, new ChangelogHeader()) + : base(OverlayColourScheme.Purple) { } [BackgroundDependencyLoader] private void load(AudioManager audio) { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background4, - }, - new OverlayScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new ReverseChildIDFillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - Header.With(h => - { - h.ListingSelected = ShowListing; - h.Build.BindTarget = Current; - }), - content = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - }, - }, - }, - }; + Header.Build.BindTarget = Current; sampleBack = audio.Samples.Get(@"UI/generic-select-soft"); @@ -85,6 +50,13 @@ namespace osu.Game.Overlays }); } + protected override ChangelogHeader CreateHeader() => new ChangelogHeader + { + ListingSelected = ShowListing, + }; + + protected override Color4 GetBackgroundColour() => ColourProvider.Background4; + public void ShowListing() { Current.Value = null; @@ -198,16 +170,16 @@ namespace osu.Game.Overlays private void loadContent(ChangelogContent newContent) { - content.FadeTo(0.2f, 300, Easing.OutQuint); + Content.FadeTo(0.2f, 300, Easing.OutQuint); loadContentCancellation?.Cancel(); LoadComponentAsync(newContent, c => { - content.FadeIn(300, Easing.OutQuint); + Content.FadeIn(300, Easing.OutQuint); c.BuildSelected = ShowBuild; - content.Child = c; + Child = c; }, (loadContentCancellation = new CancellationTokenSource()).Token); } } diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 03c320debe..3722d36388 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -7,29 +7,18 @@ 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 + public class DashboardOverlay : WebOverlay { private CancellationTokenSource cancellationToken; - private Container content; - private LoadingLayer loading; - private OverlayScrollContainer scrollFlow; - public DashboardOverlay() - : base(OverlayColourScheme.Purple, new DashboardOverlayHeader - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Depth = -float.MaxValue - }) + : base(OverlayColourScheme.Purple) { } @@ -40,45 +29,16 @@ namespace osu.Game.Overlays { apiState.BindTo(api.State); apiState.BindValueChanged(onlineStateChanged, true); - - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background5 - }, - scrollFlow = new OverlayScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - Header, - content = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - } - } - } - }, - loading = new LoadingLayer(true), - }; } protected override void LoadComplete() { base.LoadComplete(); - Header.Current.BindValueChanged(onTabChanged); } + protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader(); + private bool displayUpdateRequired = true; protected override void PopIn() @@ -102,21 +62,21 @@ namespace osu.Game.Overlays private void loadDisplay(Drawable display) { - scrollFlow.ScrollToStart(); + ScrollFlow.ScrollToStart(); LoadComponentAsync(display, loaded => { if (API.IsLoggedIn) - loading.Hide(); + Loading.Hide(); - content.Child = loaded; + Child = loaded; }, (cancellationToken = new CancellationTokenSource()).Token); } private void onTabChanged(ValueChangedEvent tab) { cancellationToken?.Cancel(); - loading.Show(); + Loading.Show(); if (!API.IsLoggedIn) { diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index 6f56d95929..d65213f573 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -6,6 +6,7 @@ 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.Containers; using osu.Game.Online.API; using osuTK.Graphics; @@ -27,9 +28,13 @@ namespace osu.Game.Overlays [Cached] protected readonly OverlayColourProvider ColourProvider; - protected FullscreenOverlay(OverlayColourScheme colourScheme, T header) + protected override Container Content => content; + + private readonly Container content; + + protected FullscreenOverlay(OverlayColourScheme colourScheme) { - Header = header; + Header = CreateHeader(); ColourProvider = new OverlayColourProvider(colourScheme); @@ -47,6 +52,19 @@ namespace osu.Game.Overlays Type = EdgeEffectType.Shadow, Radius = 10 }; + + base.Content.AddRange(new Drawable[] + { + new Box + { + RelativeSizeAxes = Axes.Both, + Colour = GetBackgroundColour() + }, + content = new Container + { + RelativeSizeAxes = Axes.Both + } + }); } [BackgroundDependencyLoader] @@ -58,6 +76,10 @@ namespace osu.Game.Overlays Waves.FourthWaveColour = ColourProvider.Dark3; } + protected abstract T CreateHeader(); + + protected virtual Color4 GetBackgroundColour() => ColourProvider.Background5; + public override void Show() { if (State.Value == Visibility.Visible) diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 5820d405d4..268d2bb6a2 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -2,67 +2,22 @@ // 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.Framework.Graphics.Shapes; -using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.News; using osu.Game.Overlays.News.Displays; namespace osu.Game.Overlays { - public class NewsOverlay : FullscreenOverlay + public class NewsOverlay : WebOverlay { private readonly Bindable article = new Bindable(null); - private Container content; - private LoadingLayer loading; - private OverlayScrollContainer scrollFlow; - public NewsOverlay() - : base(OverlayColourScheme.Purple, new NewsHeader()) + : base(OverlayColourScheme.Purple) { } - [BackgroundDependencyLoader] - private void load() - { - Children = new Drawable[] - { - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background5, - }, - scrollFlow = new OverlayScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - Header.With(h => - { - h.ShowFrontPage = ShowFrontPage; - }), - content = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - } - }, - }, - }, - loading = new LoadingLayer(true), - }; - } - protected override void LoadComplete() { base.LoadComplete(); @@ -71,6 +26,11 @@ namespace osu.Game.Overlays article.BindValueChanged(onArticleChanged); } + protected override NewsHeader CreateHeader() => new NewsHeader + { + ShowFrontPage = ShowFrontPage + }; + private bool displayUpdateRequired = true; protected override void PopIn() @@ -107,7 +67,7 @@ namespace osu.Game.Overlays private void onArticleChanged(ValueChangedEvent e) { cancellationToken?.Cancel(); - loading.Show(); + Loading.Show(); if (e.NewValue == null) { @@ -122,11 +82,11 @@ namespace osu.Game.Overlays protected void LoadDisplay(Drawable display) { - scrollFlow.ScrollToStart(); + ScrollFlow.ScrollToStart(); LoadComponentAsync(display, loaded => { - content.Child = loaded; - loading.Hide(); + Child = loaded; + Loading.Hide(); }, (cancellationToken = new CancellationTokenSource()).Token); } diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index 25350e310a..89853e9044 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -4,96 +4,41 @@ 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.Overlays.Rankings; using osu.Game.Users; using osu.Game.Rulesets; using osu.Game.Online.API; using System.Threading; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Rankings.Tables; namespace osu.Game.Overlays { - public class RankingsOverlay : FullscreenOverlay + public class RankingsOverlay : WebOverlay { protected Bindable Country => Header.Country; protected Bindable Scope => Header.Current; - private readonly OverlayScrollContainer scrollFlow; - private readonly Container contentContainer; - private readonly LoadingLayer loading; - private readonly Box background; - private APIRequest lastRequest; private CancellationTokenSource cancellationToken; [Resolved] private IAPIProvider api { get; set; } - public RankingsOverlay() - : base(OverlayColourScheme.Green, new RankingsOverlayHeader - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - Depth = -float.MaxValue - }) - { - loading = new LoadingLayer(true); + [Resolved] + private Bindable ruleset { get; set; } - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both - }, - scrollFlow = new OverlayScrollContainer - { - RelativeSizeAxes = Axes.Both, - ScrollbarVisible = false, - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - Header, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - contentContainer = new Container - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopCentre, - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Margin = new MarginPadding { Bottom = 10 } - }, - } - } - } - } - }, - loading - }; + public RankingsOverlay() + : base(OverlayColourScheme.Green) + { } [BackgroundDependencyLoader] private void load() { - background.Colour = ColourProvider.Background5; } - [Resolved] - private Bindable ruleset { get; set; } - protected override void LoadComplete() { base.LoadComplete(); @@ -129,6 +74,8 @@ namespace osu.Game.Overlays Scheduler.AddOnce(loadNewContent); } + protected override RankingsOverlayHeader CreateHeader() => new RankingsOverlayHeader(); + public void ShowCountry(Country requested) { if (requested == null) @@ -147,7 +94,7 @@ namespace osu.Game.Overlays private void loadNewContent() { - loading.Show(); + Loading.Show(); cancellationToken?.Cancel(); lastRequest?.Cancel(); @@ -218,19 +165,19 @@ namespace osu.Game.Overlays private void loadContent(Drawable content) { - scrollFlow.ScrollToStart(); + ScrollFlow.ScrollToStart(); if (content == null) { - contentContainer.Clear(); - loading.Hide(); + Clear(); + Loading.Hide(); return; } LoadComponentAsync(content, loaded => { - loading.Hide(); - contentContainer.Child = loaded; + Loading.Hide(); + Child = loaded; }, (cancellationToken = new CancellationTokenSource()).Token); } diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index 81027667fa..c29df72501 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -15,6 +15,7 @@ using osu.Game.Overlays.Profile; using osu.Game.Overlays.Profile.Sections; using osu.Game.Users; using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays { @@ -29,10 +30,14 @@ namespace osu.Game.Overlays public const float CONTENT_X_MARGIN = 70; public UserProfileOverlay() - : base(OverlayColourScheme.Pink, new ProfileHeader()) + : base(OverlayColourScheme.Pink) { } + protected override ProfileHeader CreateHeader() => new ProfileHeader(); + + protected override Color4 GetBackgroundColour() => ColourProvider.Background6; + public void ShowUser(int userId) => ShowUser(new User { Id = userId }); public void ShowUser(User user, bool fetchOnline = true) @@ -72,12 +77,6 @@ namespace osu.Game.Overlays Origin = Anchor.TopCentre, }; - Add(new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourProvider.Background6 - }); - Add(sectionsContainer = new ProfileSectionsContainer { ExpandableHeader = Header, diff --git a/osu.Game/Overlays/WebOverlay.cs b/osu.Game/Overlays/WebOverlay.cs new file mode 100644 index 0000000000..aca767e32f --- /dev/null +++ b/osu.Game/Overlays/WebOverlay.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.UserInterface; + +namespace osu.Game.Overlays +{ + public abstract class WebOverlay : FullscreenOverlay + where T : OverlayHeader + { + protected override Container Content => content; + + protected readonly OverlayScrollContainer ScrollFlow; + protected readonly LoadingLayer Loading; + private readonly Container content; + + protected WebOverlay(OverlayColourScheme colourScheme) + : base(colourScheme) + { + FillFlowContainer flow = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical + }; + + if (Header != null) + flow.Add(Header.With(h => h.Depth = -float.MaxValue)); + + flow.Add(content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + }); + + base.Content.AddRange(new Drawable[] + { + ScrollFlow = new OverlayScrollContainer + { + RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, + Child = flow + }, + Loading = new LoadingLayer(true) + }); + } + } +} From 88abee705b37d0a6c9db00aa7b8a3f5323703590 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 18 Jan 2021 10:48:53 +0300 Subject: [PATCH 6039/6909] Add missing event mapping for user beatmap availability change --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 50dc8f661c..6f789b1ef5 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -81,6 +81,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); + connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged); connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); From 4e6c1a3906ed40be5752b13994499f73b4c10142 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 18 Jan 2021 10:49:38 +0300 Subject: [PATCH 6040/6909] Update client beatmap availability in-line with tracker --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 ++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index c049d4be20..7569da00f2 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -43,6 +43,8 @@ namespace osu.Game.Screens.OnlinePlay.Match [Cached] protected readonly MultiplayerBeatmapTracker BeatmapTracker; + protected IBindable BeatmapAvailability => BeatmapTracker.Availability; + protected RoomSubScreen() { InternalChild = BeatmapTracker = new MultiplayerBeatmapTracker diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a641935b9a..94288673fb 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -184,7 +184,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); Playlist.BindCollectionChanged(onPlaylistChanged, true); + BeatmapAvailability.BindValueChanged(updateClientAvailability, true); + client.RoomUpdated += onRoomUpdated; client.LoadRequested += onLoadRequested; isConnected = client.IsConnected.GetBoundCopy(); @@ -208,6 +210,21 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); + private void updateClientAvailability(ValueChangedEvent _ = null) + { + if (client.Room != null) + client.ChangeBeatmapAvailability(BeatmapAvailability.Value).CatchUnobservedExceptions(true); + } + + private void onRoomUpdated() + { + if (client.Room == null) + return; + + if (client.LocalUser?.BeatmapAvailability.Equals(BeatmapAvailability.Value) == false) + updateClientAvailability(); + } + private void onReadyClick() { Debug.Assert(readyClickOperation == null); @@ -262,7 +279,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.Dispose(isDisposing); if (client != null) + { client.LoadRequested -= onLoadRequested; + client.RoomUpdated -= onRoomUpdated; + } } } } From bd44bf8c0b9348f9443df2dfc95470d86bd80ecf Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 18 Jan 2021 10:46:48 +0300 Subject: [PATCH 6041/6909] Extract disabling progress bar user-interactivity --- osu.Game/Graphics/UserInterface/ProgressBar.cs | 8 +++++++- .../Overlays/BeatmapListing/Panels/DownloadProgressBar.cs | 8 +------- osu.Game/Overlays/NowPlayingOverlay.cs | 5 +++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ProgressBar.cs b/osu.Game/Graphics/UserInterface/ProgressBar.cs index d271cd121c..4ee1c73bf5 100644 --- a/osu.Game/Graphics/UserInterface/ProgressBar.cs +++ b/osu.Game/Graphics/UserInterface/ProgressBar.cs @@ -40,8 +40,14 @@ namespace osu.Game.Graphics.UserInterface set => CurrentNumber.Value = value; } - public ProgressBar() + private readonly bool userInteractive; + public override bool HandlePositionalInput => userInteractive; + public override bool HandleNonPositionalInput => userInteractive; + + public ProgressBar(bool userInteractive) { + this.userInteractive = userInteractive; + CurrentNumber.MinValue = 0; CurrentNumber.MaxValue = 1; RelativeSizeAxes = Axes.X; diff --git a/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs index 6a2f2e4569..ca94078401 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/DownloadProgressBar.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.BeatmapListing.Panels public DownloadProgressBar(BeatmapSetInfo beatmapSet) : base(beatmapSet) { - AddInternal(progressBar = new InteractionDisabledProgressBar + AddInternal(progressBar = new ProgressBar(false) { Height = 0, Alpha = 0, @@ -64,11 +64,5 @@ namespace osu.Game.Overlays.BeatmapListing.Panels } }, true); } - - private class InteractionDisabledProgressBar : ProgressBar - { - public override bool HandlePositionalInput => false; - public override bool HandleNonPositionalInput => false; - } } } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 9beb859f28..d64c61044d 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -411,6 +411,11 @@ namespace osu.Game.Overlays private class HoverableProgressBar : ProgressBar { + public HoverableProgressBar() + : base(true) + { + } + protected override bool OnHover(HoverEvent e) { this.ResizeHeightTo(progress_height, 500, Easing.OutQuint); From 02d2b2742b66d1bf6b9899430d90580f69872db1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 16:57:36 +0900 Subject: [PATCH 6042/6909] Fix selection box not updating with hitcircles/sliders far in the future or past --- .../Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs | 2 +- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs index 093bae854e..abbb54e3c1 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/HitCircles/HitCircleSelectionBlueprint.cs @@ -30,6 +30,6 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.HitCircles public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => DrawableObject.HitArea.ReceivePositionalInputAt(screenSpacePos); - public override Quad SelectionQuad => DrawableObject.HitArea.ScreenSpaceDrawQuad; + public override Quad SelectionQuad => CirclePiece.ScreenSpaceDrawQuad; } } diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index d592e129d9..3d3dff653a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -44,6 +44,8 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders [Resolved(CanBeNull = true)] private IEditorChangeHandler changeHandler { get; set; } + public override Quad SelectionQuad => BodyPiece.ScreenSpaceDrawQuad; + private readonly BindableList controlPoints = new BindableList(); private readonly IBindable pathVersion = new Bindable(); From c79ab63743e14b4205e40f33d5ae4519dfbeb58e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 16:56:10 +0900 Subject: [PATCH 6043/6909] Fix sliders with an even number of repeats not allowing rotation/scale transforms --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 660e1844aa..91bb665ee2 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -236,7 +236,18 @@ namespace osu.Game.Rulesets.Osu.Edit /// /// The hit objects to calculate a quad for. private Quad getSurroundingQuad(OsuHitObject[] hitObjects) => - getSurroundingQuad(hitObjects.SelectMany(h => new[] { h.Position, h.EndPosition })); + getSurroundingQuad(hitObjects.SelectMany(h => + { + if (h is IHasPath path) + return new[] + { + h.Position, + // can't use EndPosition for reverse slider cases. + h.Position + path.Path.PositionAt(1) + }; + + return new[] { h.Position }; + })); /// /// Returns a gamefield-space quad surrounding the provided points. From 6deb10e0750423c60175975edf62e383535dc346 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 18 Jan 2021 11:09:45 +0300 Subject: [PATCH 6044/6909] Add UI state display for each client's beatmap availability --- .../Participants/ParticipantPanel.cs | 2 +- .../Multiplayer/Participants/StateDisplay.cs | 193 +++++++++++------- 2 files changed, 119 insertions(+), 76 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index f99655e305..17bf3a58ec 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -143,7 +143,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; - userStateDisplay.Status = User.State; + userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); if (Room.Host?.Equals(User) == true) crown.FadeIn(fade_time); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index 8d2879fc93..4245628a59 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.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 osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -8,119 +9,161 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osuTK; +using osuTK.Graphics; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants { public class StateDisplay : CompositeDrawable { + private const double fade_time = 50; + + private SpriteIcon icon; + private OsuSpriteText text; + private ProgressBar progressBar; + public StateDisplay() { AutoSizeAxes = Axes.Both; Alpha = 0; } - private MultiplayerUserState status; - - private OsuSpriteText text; - private SpriteIcon icon; - - private const double fade_time = 50; - - public MultiplayerUserState Status - { - set - { - if (value == status) - return; - - status = value; - - if (IsLoaded) - updateStatus(); - } - } - [BackgroundDependencyLoader] - private void load() + private void load(OsuColour colours) { + this.colours = colours; + InternalChild = new FillFlowContainer { AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, Spacing = new Vector2(5), Children = new Drawable[] { - text = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12), - Colour = Color4Extensions.FromHex("#DDFFFF") - }, icon = new SpriteIcon { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, Icon = FontAwesome.Solid.CheckCircle, Size = new Vector2(12), - } + }, + new CircularContainer + { + Masking = true, + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Children = new Drawable[] + { + progressBar = new ProgressBar(false) + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + BackgroundColour = Color4.Black.Opacity(0.4f), + FillColour = colours.Blue, + Alpha = 0f, + }, + text = new OsuSpriteText + { + Padding = new MarginPadding { Horizontal = 5f, Vertical = 1f }, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Font = OsuFont.GetFont(weight: FontWeight.Regular, size: 12), + Colour = Color4Extensions.FromHex("#DDFFFF") + }, + } + }, } }; } - protected override void LoadComplete() - { - base.LoadComplete(); - updateStatus(); - } + private OsuColour colours; - [Resolved] - private OsuColour colours { get; set; } - - private void updateStatus() + public void UpdateStatus(MultiplayerUserState state, BeatmapAvailability availability) { - switch (status) + if (availability.State != DownloadState.LocallyAvailable) { - default: - this.FadeOut(fade_time); - return; + switch (availability.State) + { + case DownloadState.NotDownloaded: + progressBar.FadeOut(fade_time); + text.Text = "no map"; + icon.Icon = FontAwesome.Solid.MinusCircle; + icon.Colour = colours.RedLight; + break; - case MultiplayerUserState.Ready: - text.Text = "ready"; - icon.Icon = FontAwesome.Solid.CheckCircle; - icon.Colour = Color4Extensions.FromHex("#AADD00"); - break; + case DownloadState.Downloading: + Debug.Assert(availability.DownloadProgress != null); - case MultiplayerUserState.WaitingForLoad: - text.Text = "loading"; - icon.Icon = FontAwesome.Solid.PauseCircle; - icon.Colour = colours.Yellow; - break; + var progress = availability.DownloadProgress.Value; + progressBar.FadeIn(fade_time); + progressBar.CurrentTime = progress; - case MultiplayerUserState.Loaded: - text.Text = "loaded"; - icon.Icon = FontAwesome.Solid.DotCircle; - icon.Colour = colours.YellowLight; - break; + text.Text = "downloading map"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleDown; + icon.Colour = colours.Blue; + break; - case MultiplayerUserState.Playing: - text.Text = "playing"; - icon.Icon = FontAwesome.Solid.PlayCircle; - icon.Colour = colours.BlueLight; - break; + case DownloadState.Importing: + progressBar.FadeOut(fade_time); + text.Text = "importing map"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleDown; + icon.Colour = colours.Yellow; + break; + } + } + else + { + progressBar.FadeOut(fade_time); - case MultiplayerUserState.FinishedPlay: - text.Text = "results pending"; - icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; - icon.Colour = colours.BlueLighter; - break; + switch (state) + { + default: + this.FadeOut(fade_time); + return; - case MultiplayerUserState.Results: - text.Text = "results"; - icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; - icon.Colour = colours.BlueLighter; - break; + case MultiplayerUserState.Ready: + text.Text = "ready"; + icon.Icon = FontAwesome.Solid.CheckCircle; + icon.Colour = Color4Extensions.FromHex("#AADD00"); + break; + + case MultiplayerUserState.WaitingForLoad: + text.Text = "loading"; + icon.Icon = FontAwesome.Solid.PauseCircle; + icon.Colour = colours.Yellow; + break; + + case MultiplayerUserState.Loaded: + text.Text = "loaded"; + icon.Icon = FontAwesome.Solid.DotCircle; + icon.Colour = colours.YellowLight; + break; + + case MultiplayerUserState.Playing: + text.Text = "playing"; + icon.Icon = FontAwesome.Solid.PlayCircle; + icon.Colour = colours.BlueLight; + break; + + case MultiplayerUserState.FinishedPlay: + text.Text = "results pending"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; + icon.Colour = colours.BlueLighter; + break; + + case MultiplayerUserState.Results: + text.Text = "results"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; + icon.Colour = colours.BlueLighter; + break; + } } this.FadeIn(fade_time); From 6e34ab5d152f37d19702ff305c2a53323e89ef75 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 18 Jan 2021 11:13:38 +0300 Subject: [PATCH 6045/6909] Rename WebOverlay to OnlineOverlay --- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- osu.Game/Overlays/BeatmapSetOverlay.cs | 2 +- osu.Game/Overlays/ChangelogOverlay.cs | 2 +- osu.Game/Overlays/DashboardOverlay.cs | 2 +- osu.Game/Overlays/NewsOverlay.cs | 2 +- osu.Game/Overlays/{WebOverlay.cs => OnlineOverlay.cs} | 4 ++-- osu.Game/Overlays/RankingsOverlay.cs | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game/Overlays/{WebOverlay.cs => OnlineOverlay.cs} (91%) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index ae1667d403..eafb7e95d5 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -23,7 +23,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public class BeatmapListingOverlay : WebOverlay + public class BeatmapListingOverlay : OnlineOverlay { [Resolved] private PreviewTrackManager previewTrackManager { get; set; } diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 10953415ed..872621801a 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -19,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public class BeatmapSetOverlay : WebOverlay // we don't provide a standard header for now. + public class BeatmapSetOverlay : OnlineOverlay // we don't provide a standard header for now. { public const float X_PADDING = 40; public const float Y_PADDING = 25; diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 5d99887053..5200b567ff 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -19,7 +19,7 @@ using osuTK.Graphics; namespace osu.Game.Overlays { - public class ChangelogOverlay : WebOverlay + public class ChangelogOverlay : OnlineOverlay { public readonly Bindable Current = new Bindable(); diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 3722d36388..39a23fe3d4 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -13,7 +13,7 @@ using osu.Game.Overlays.Dashboard.Friends; namespace osu.Game.Overlays { - public class DashboardOverlay : WebOverlay + public class DashboardOverlay : OnlineOverlay { private CancellationTokenSource cancellationToken; diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 268d2bb6a2..08e8331dd3 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -9,7 +9,7 @@ using osu.Game.Overlays.News.Displays; namespace osu.Game.Overlays { - public class NewsOverlay : WebOverlay + public class NewsOverlay : OnlineOverlay { private readonly Bindable article = new Bindable(null); diff --git a/osu.Game/Overlays/WebOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs similarity index 91% rename from osu.Game/Overlays/WebOverlay.cs rename to osu.Game/Overlays/OnlineOverlay.cs index aca767e32f..b44ccc32c9 100644 --- a/osu.Game/Overlays/WebOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -7,7 +7,7 @@ using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays { - public abstract class WebOverlay : FullscreenOverlay + public abstract class OnlineOverlay : FullscreenOverlay where T : OverlayHeader { protected override Container Content => content; @@ -16,7 +16,7 @@ namespace osu.Game.Overlays protected readonly LoadingLayer Loading; private readonly Container content; - protected WebOverlay(OverlayColourScheme colourScheme) + protected OnlineOverlay(OverlayColourScheme colourScheme) : base(colourScheme) { FillFlowContainer flow = new FillFlowContainer diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index 89853e9044..f6bbac4407 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -14,7 +14,7 @@ using osu.Game.Overlays.Rankings.Tables; namespace osu.Game.Overlays { - public class RankingsOverlay : WebOverlay + public class RankingsOverlay : OnlineOverlay { protected Bindable Country => Header.Country; From 2f1d4bf51b0964aded4fa5d31be592c7da3eb482 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 17:13:47 +0900 Subject: [PATCH 6046/6909] Add missing braces --- osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs index 91bb665ee2..871339ae7b 100644 --- a/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs +++ b/osu.Game.Rulesets.Osu/Edit/OsuSelectionHandler.cs @@ -239,12 +239,14 @@ namespace osu.Game.Rulesets.Osu.Edit getSurroundingQuad(hitObjects.SelectMany(h => { if (h is IHasPath path) + { return new[] { h.Position, // can't use EndPosition for reverse slider cases. h.Position + path.Path.PositionAt(1) }; + } return new[] { h.Position }; })); From 5f2e9c5485d854c3a5a85dee64613226fd77cd38 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 18 Jan 2021 11:10:38 +0300 Subject: [PATCH 6047/6909] Add visual test case for displaying beatmap availability states --- .../TestSceneMultiplayerParticipantsList.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 968a869532..e2f1a13593 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -7,7 +7,9 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Online; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Users; using osuTK; @@ -73,6 +75,20 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.User == secondUser); } + [Test] + public void TestBeatmapDownloadingStates() + { + AddStep("set to no map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); + AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + AddRepeatStep("increment progress", () => + { + var progress = this.ChildrenOfType().Single().User.BeatmapAvailability.DownloadProgress ?? 0; + Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f))); + }, 25); + AddStep("set to importing map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Importing())); + AddStep("set to available", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); + } + [Test] public void TestToggleReadyState() { @@ -120,6 +136,26 @@ namespace osu.Game.Tests.Visual.Multiplayer }); Client.ChangeUserState(i, (MultiplayerUserState)RNG.Next(0, (int)MultiplayerUserState.Results + 1)); + + if (RNG.NextBool()) + { + var beatmapState = (DownloadState)RNG.Next(0, (int)DownloadState.LocallyAvailable + 1); + + switch (beatmapState) + { + case DownloadState.NotDownloaded: + Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.NotDownloaded()); + break; + + case DownloadState.Downloading: + Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Downloading(RNG.NextSingle())); + break; + + case DownloadState.Importing: + Client.ChangeUserBeatmapAvailability(i, BeatmapAvailability.Importing()); + break; + } + } } }); } From 0b165dce4bbbb0647ccbdae48b8d2621173655f6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 17:50:32 +0900 Subject: [PATCH 6048/6909] Fix multiplayer mod select showing autoplay as a choice --- osu.Game/OsuGameBase.cs | 1 + osu.Game/Overlays/Mods/ModSelectOverlay.cs | 7 +++++-- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 5 +++++ osu.Game/Screens/Select/SongSelect.cs | 8 +++----- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 1f8ae54e55..20d88d33f2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -327,6 +327,7 @@ namespace osu.Game if (!SelectedMods.Disabled) SelectedMods.Value = Array.Empty(); + AvailableMods.Value = dict; } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 0c8245bebe..b93602116b 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -30,6 +30,7 @@ namespace osu.Game.Overlays.Mods { public class ModSelectOverlay : WaveOverlayContainer { + private readonly Func isValidMod; public const float HEIGHT = 510; protected readonly TriangleButton DeselectAllButton; @@ -60,8 +61,10 @@ namespace osu.Game.Overlays.Mods private SampleChannel sampleOn, sampleOff; - public ModSelectOverlay() + public ModSelectOverlay(Func isValidMod = null) { + this.isValidMod = isValidMod ?? (m => true); + Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2"); Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2"); Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774"); @@ -403,7 +406,7 @@ namespace osu.Game.Overlays.Mods if (mods.NewValue == null) return; foreach (var section in ModSectionsContainer.Children) - section.Mods = mods.NewValue[section.ModType]; + section.Mods = mods.NewValue[section.ModType].Where(isValidMod); } private void selectedModsChanged(ValueChangedEvent> mods) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 36dbb9e792..ebc06d2445 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; @@ -109,5 +110,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + + protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(isValidMod); + + private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 6c0bd3a228..4fca77a176 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -251,11 +251,7 @@ namespace osu.Game.Screens.Select Children = new Drawable[] { BeatmapOptions = new BeatmapOptionsOverlay(), - ModSelect = new ModSelectOverlay - { - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, - } + ModSelect = CreateModSelectOverlay() } } } @@ -305,6 +301,8 @@ namespace osu.Game.Screens.Select } } + protected virtual ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(); + protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) { // if not the current screen, we want to get carousel in a good presentation state before displaying (resume or enter). From 1359153382232fc303dd1ded30048545c4aa0638 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 17:54:22 +0900 Subject: [PATCH 6049/6909] Revert "Test removing nuget restore from iOS/Android build scripts" This reverts commit 39746cb3de8ad11221ff1766822f720918d49858. --- fastlane/Fastfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 18b5907e82..8c278604aa 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -48,6 +48,10 @@ desc 'Deploy to play store' desc 'Compile the project' lane :build do |options| + nuget_restore( + project_path: 'osu.sln' + ) + souyuz( build_configuration: 'Release', solution_path: 'osu.sln', @@ -103,6 +107,10 @@ platform :ios do desc 'Compile the project' lane :build do + nuget_restore( + project_path: 'osu.sln' + ) + souyuz( platform: "ios", plist_path: "osu.iOS/Info.plist" From 46681322d0489d0829114fc87071b7e5c8f0c525 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 17:56:00 +0900 Subject: [PATCH 6050/6909] Restore nuget packages per project to avoid toolchain incompatibilities with net50 --- fastlane/Fastfile | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8c278604aa..1823b9e924 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -48,9 +48,8 @@ desc 'Deploy to play store' desc 'Compile the project' lane :build do |options| - nuget_restore( - project_path: 'osu.sln' - ) + nuget_restore(project_path: 'osu.Android/osu.Android.csproj') + nuget_restore(project_path: 'osu.Game/osu.Game.csproj') souyuz( build_configuration: 'Release', @@ -107,9 +106,8 @@ platform :ios do desc 'Compile the project' lane :build do - nuget_restore( - project_path: 'osu.sln' - ) + nuget_restore(project_path: 'osu.iOS/osu.iOS.csproj') + nuget_restore(project_path: 'osu.Game/osu.Game.csproj') souyuz( platform: "ios", From 0560eb41201d0094d8321927bc3498502065c994 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 18:22:21 +0900 Subject: [PATCH 6051/6909] Reduce final fill alpha of main menu confirm-to-exit --- osu.Game/Overlays/HoldToConfirmOverlay.cs | 13 +++++++++++-- osu.Game/Screens/Menu/ExitConfirmOverlay.cs | 5 +++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/HoldToConfirmOverlay.cs b/osu.Game/Overlays/HoldToConfirmOverlay.cs index eb325d8dd3..0542f66b5b 100644 --- a/osu.Game/Overlays/HoldToConfirmOverlay.cs +++ b/osu.Game/Overlays/HoldToConfirmOverlay.cs @@ -24,6 +24,13 @@ namespace osu.Game.Overlays [Resolved] private AudioManager audio { get; set; } + private readonly float finalFillAlpha; + + protected HoldToConfirmOverlay(float finalFillAlpha = 1) + { + this.finalFillAlpha = finalFillAlpha; + } + [BackgroundDependencyLoader] private void load() { @@ -42,8 +49,10 @@ namespace osu.Game.Overlays Progress.ValueChanged += p => { - audioVolume.Value = 1 - p.NewValue; - overlay.Alpha = (float)p.NewValue; + var target = p.NewValue * finalFillAlpha; + + audioVolume.Value = 1 - target; + overlay.Alpha = (float)target; }; audio.Tracks.AddAdjustment(AdjustableProperty.Volume, audioVolume); diff --git a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs index db2faeb60a..a491283e5f 100644 --- a/osu.Game/Screens/Menu/ExitConfirmOverlay.cs +++ b/osu.Game/Screens/Menu/ExitConfirmOverlay.cs @@ -13,6 +13,11 @@ namespace osu.Game.Screens.Menu public void Abort() => AbortConfirm(); + public ExitConfirmOverlay() + : base(0.7f) + { + } + public bool OnPressed(GlobalAction action) { if (action == GlobalAction.Back) From 12443e39ae70060657f1b097bc7b9c6e35345e08 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 19:16:32 +0900 Subject: [PATCH 6052/6909] 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 db5c933c41..9ad5946311 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 301ee39a61..2b8f81532d 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 225cf981f2..4732620085 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From ced7a36788d7f61ce1163f043d872eb97ad44a02 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 21:24:10 +0900 Subject: [PATCH 6053/6909] Update namespaces --- osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs | 1 - osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs | 2 +- osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs | 1 - osu.Game/Rulesets/UI/Playfield.cs | 2 +- osu.Game/Skinning/PoolableSkinnableSample.cs | 2 +- osu.Game/Skinning/SkinnableSound.cs | 2 +- 6 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs index 33e3c7cb8c..987a5812db 100644 --- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs +++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs @@ -9,7 +9,6 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; diff --git a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs index 80f1b02794..97087e31ab 100644 --- a/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs +++ b/osu.Game.Tests/Testing/TestSceneRulesetDependencies.cs @@ -5,7 +5,7 @@ using System; using System.Collections.Generic; using NUnit.Framework; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; +using osu.Framework.Audio.Sample; using osu.Framework.Configuration.Tracking; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index b13b20dae2..81ec73a6c5 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -8,7 +8,6 @@ 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.OpenGL.Textures; using osu.Framework.Graphics.Textures; diff --git a/osu.Game/Rulesets/UI/Playfield.cs b/osu.Game/Rulesets/UI/Playfield.cs index b4e0025351..c40ab4bd94 100644 --- a/osu.Game/Rulesets/UI/Playfield.cs +++ b/osu.Game/Rulesets/UI/Playfield.cs @@ -8,7 +8,6 @@ using JetBrains.Annotations; using osu.Framework.Graphics; using osu.Game.Rulesets.Objects.Drawables; using osu.Framework.Allocation; -using osu.Framework.Audio.Track; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics.Containers; @@ -20,6 +19,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Skinning; using osuTK; using System.Diagnostics; +using osu.Framework.Audio.Sample; namespace osu.Game.Rulesets.UI { diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index cc6b85a13e..2a0f480b48 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -5,7 +5,7 @@ using System; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Track; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Audio; diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index b841f99598..a874e9a0db 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -7,7 +7,7 @@ using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; -using osu.Framework.Audio.Track; +using osu.Framework.Audio.Sample; using osu.Framework.Bindables; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; From e6ceaad73233c8322f93554ebcbf136da5c47285 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 18 Jan 2021 17:23:51 +0300 Subject: [PATCH 6054/6909] Revert user state back to idle upon availability change --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 94288673fb..826859c598 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -184,7 +184,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); Playlist.BindCollectionChanged(onPlaylistChanged, true); - BeatmapAvailability.BindValueChanged(updateClientAvailability, true); + BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); client.RoomUpdated += onRoomUpdated; client.LoadRequested += onLoadRequested; @@ -210,10 +210,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); - private void updateClientAvailability(ValueChangedEvent _ = null) + private void updateBeatmapAvailability(ValueChangedEvent _ = null) { - if (client.Room != null) - client.ChangeBeatmapAvailability(BeatmapAvailability.Value).CatchUnobservedExceptions(true); + if (client.Room == null) + return; + + client.ChangeBeatmapAvailability(BeatmapAvailability.Value).CatchUnobservedExceptions(true); + + if (client.LocalUser?.State == MultiplayerUserState.Ready) + client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); } private void onRoomUpdated() @@ -222,7 +227,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; if (client.LocalUser?.BeatmapAvailability.Equals(BeatmapAvailability.Value) == false) - updateClientAvailability(); + updateBeatmapAvailability(); } private void onReadyClick() From e74ecebfd61440a47c7423b0802491ccc1930b7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 18 Jan 2021 23:45:57 +0900 Subject: [PATCH 6055/6909] nuget restore other rulesets --- fastlane/Fastfile | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 1823b9e924..cc5abf5b03 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -50,6 +50,10 @@ desc 'Deploy to play store' lane :build do |options| nuget_restore(project_path: 'osu.Android/osu.Android.csproj') nuget_restore(project_path: 'osu.Game/osu.Game.csproj') + nuget_restore(project_path: 'osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj') + nuget_restore(project_path: 'osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj') + nuget_restore(project_path: 'osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj') + nuget_restore(project_path: 'osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj') souyuz( build_configuration: 'Release', @@ -108,6 +112,10 @@ platform :ios do lane :build do nuget_restore(project_path: 'osu.iOS/osu.iOS.csproj') nuget_restore(project_path: 'osu.Game/osu.Game.csproj') + nuget_restore(project_path: 'osu.Game.Rulesets.Osu/osu.Game.Rulesets.Osu.csproj') + nuget_restore(project_path: 'osu.Game.Rulesets.Taiko/osu.Game.Rulesets.Taiko.csproj') + nuget_restore(project_path: 'osu.Game.Rulesets.Catch/osu.Game.Rulesets.Catch.csproj') + nuget_restore(project_path: 'osu.Game.Rulesets.Mania/osu.Game.Rulesets.Mania.csproj') souyuz( platform: "ios", From ddcfd854bd1c66b673d73e5459526ded76ebcb41 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 18 Jan 2021 19:20:19 +0300 Subject: [PATCH 6056/6909] Wait for scheduled state changes before asserting --- osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs index 4b8992052e..d692e6f9a3 100644 --- a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs +++ b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs @@ -130,6 +130,8 @@ namespace osu.Game.Tests.Online private void addAvailabilityCheckStep(string description, Func expected) { + // In DownloadTrackingComposite, state changes are scheduled one frame later, wait one step. + AddWaitStep("wait for potential change", 1); AddAssert(description, () => tracker.Availability.Value.Equals(expected.Invoke())); } From c6b0f3c247aa54a64d9e61c47643a09ea91ec177 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Mon, 18 Jan 2021 21:22:50 +0300 Subject: [PATCH 6057/6909] Implement TabbableOnlineOverlay component --- osu.Game/Overlays/DashboardOverlay.cs | 92 ++----------------- osu.Game/Overlays/RankingsOverlay.cs | 46 ++-------- osu.Game/Overlays/TabbableOnlineOverlay.cs | 101 +++++++++++++++++++++ 3 files changed, 116 insertions(+), 123 deletions(-) create mode 100644 osu.Game/Overlays/TabbableOnlineOverlay.cs diff --git a/osu.Game/Overlays/DashboardOverlay.cs b/osu.Game/Overlays/DashboardOverlay.cs index 39a23fe3d4..83ad8faf1c 100644 --- a/osu.Game/Overlays/DashboardOverlay.cs +++ b/osu.Game/Overlays/DashboardOverlay.cs @@ -2,115 +2,35 @@ // 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.Game.Online.API; using osu.Game.Overlays.Dashboard; using osu.Game.Overlays.Dashboard.Friends; namespace osu.Game.Overlays { - public class DashboardOverlay : OnlineOverlay + public class DashboardOverlay : TabbableOnlineOverlay { - private CancellationTokenSource cancellationToken; - public DashboardOverlay() : base(OverlayColourScheme.Purple) { } - private readonly IBindable apiState = new Bindable(); - - [BackgroundDependencyLoader] - private void load(IAPIProvider api) - { - apiState.BindTo(api.State); - apiState.BindValueChanged(onlineStateChanged, true); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - Header.Current.BindValueChanged(onTabChanged); - } - protected override DashboardOverlayHeader CreateHeader() => new DashboardOverlayHeader(); - private bool displayUpdateRequired = true; - - protected override void PopIn() + protected override void CreateDisplayToLoad(DashboardOverlayTabs tab) { - base.PopIn(); - - // We don't want to create a 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 => - { - if (API.IsLoggedIn) - Loading.Hide(); - - Child = loaded; - }, (cancellationToken = new CancellationTokenSource()).Token); - } - - private void onTabChanged(ValueChangedEvent tab) - { - cancellationToken?.Cancel(); - Loading.Show(); - - if (!API.IsLoggedIn) - { - loadDisplay(Empty()); - return; - } - - switch (tab.NewValue) + switch (tab) { case DashboardOverlayTabs.Friends: - loadDisplay(new FriendDisplay()); + LoadDisplay(new FriendDisplay()); break; case DashboardOverlayTabs.CurrentlyPlaying: - loadDisplay(new CurrentlyPlayingDisplay()); + LoadDisplay(new CurrentlyPlayingDisplay()); break; default: - throw new NotImplementedException($"Display for {tab.NewValue} tab is not implemented"); + throw new NotImplementedException($"Display for {tab} tab is not implemented"); } } - - private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => - { - if (State.Value == Visibility.Hidden) - return; - - Header.Current.TriggerChange(); - }); - - protected override void Dispose(bool isDisposing) - { - cancellationToken?.Cancel(); - base.Dispose(isDisposing); - } } } diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index f6bbac4407..6cd72d6e2c 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -8,20 +8,18 @@ using osu.Game.Overlays.Rankings; using osu.Game.Users; using osu.Game.Rulesets; using osu.Game.Online.API; -using System.Threading; using osu.Game.Online.API.Requests; using osu.Game.Overlays.Rankings.Tables; namespace osu.Game.Overlays { - public class RankingsOverlay : OnlineOverlay + public class RankingsOverlay : TabbableOnlineOverlay { protected Bindable Country => Header.Country; protected Bindable Scope => Header.Current; private APIRequest lastRequest; - private CancellationTokenSource cancellationToken; [Resolved] private IAPIProvider api { get; set; } @@ -34,11 +32,6 @@ namespace osu.Game.Overlays { } - [BackgroundDependencyLoader] - private void load() - { - } - protected override void LoadComplete() { base.LoadComplete(); @@ -54,6 +47,8 @@ namespace osu.Game.Overlays Scheduler.AddOnce(loadNewContent); }); + // Unbind events from scope so base class event will not be called + Scope.UnbindEvents(); Scope.BindValueChanged(_ => { // country filtering is only valid for performance scope. @@ -70,8 +65,6 @@ namespace osu.Game.Overlays Scheduler.AddOnce(loadNewContent); }); - - Scheduler.AddOnce(loadNewContent); } protected override RankingsOverlayHeader CreateHeader() => new RankingsOverlayHeader(); @@ -92,16 +85,13 @@ namespace osu.Game.Overlays Show(); } - private void loadNewContent() + protected override void CreateDisplayToLoad(RankingsScope tab) { - Loading.Show(); - - cancellationToken?.Cancel(); lastRequest?.Cancel(); if (Scope.Value == RankingsScope.Spotlights) { - loadContent(new SpotlightsLayout + LoadDisplay(new SpotlightsLayout { Ruleset = { BindTarget = ruleset } }); @@ -113,12 +103,12 @@ namespace osu.Game.Overlays if (request == null) { - loadContent(null); + LoadDisplay(Empty()); return; } - request.Success += () => Schedule(() => loadContent(createTableFromResponse(request))); - request.Failure += _ => Schedule(() => loadContent(null)); + request.Success += () => Schedule(() => LoadDisplay(createTableFromResponse(request))); + request.Failure += _ => Schedule(() => LoadDisplay(Empty())); api.Queue(request); } @@ -163,29 +153,11 @@ namespace osu.Game.Overlays return null; } - private void loadContent(Drawable content) - { - ScrollFlow.ScrollToStart(); - - if (content == null) - { - Clear(); - Loading.Hide(); - return; - } - - LoadComponentAsync(content, loaded => - { - Loading.Hide(); - Child = loaded; - }, (cancellationToken = new CancellationTokenSource()).Token); - } + private void loadNewContent() => OnTabChanged(Scope.Value); protected override void Dispose(bool isDisposing) { lastRequest?.Cancel(); - cancellationToken?.Cancel(); - base.Dispose(isDisposing); } } diff --git a/osu.Game/Overlays/TabbableOnlineOverlay.cs b/osu.Game/Overlays/TabbableOnlineOverlay.cs new file mode 100644 index 0000000000..cbcf3cd96e --- /dev/null +++ b/osu.Game/Overlays/TabbableOnlineOverlay.cs @@ -0,0 +1,101 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Threading; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Online.API; + +namespace osu.Game.Overlays +{ + public abstract class TabbableOnlineOverlay : OnlineOverlay + where THeader : TabControlOverlayHeader + { + private readonly IBindable apiState = new Bindable(); + + private CancellationTokenSource cancellationToken; + private bool displayUpdateRequired = true; + + protected TabbableOnlineOverlay(OverlayColourScheme colourScheme) + : base(colourScheme) + { + } + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + apiState.BindTo(api.State); + apiState.BindValueChanged(onlineStateChanged, true); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Header.Current.BindValueChanged(tab => OnTabChanged(tab.NewValue)); + } + + protected override void PopIn() + { + base.PopIn(); + + // We don't want to create a 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; + } + + protected void LoadDisplay(Drawable display) + { + ScrollFlow.ScrollToStart(); + + LoadComponentAsync(display, loaded => + { + if (API.IsLoggedIn) + Loading.Hide(); + + Child = loaded; + }, (cancellationToken = new CancellationTokenSource()).Token); + } + + protected void OnTabChanged(TEnum tab) + { + cancellationToken?.Cancel(); + Loading.Show(); + + if (!API.IsLoggedIn) + { + LoadDisplay(Empty()); + return; + } + + CreateDisplayToLoad(tab); + } + + protected abstract void CreateDisplayToLoad(TEnum tab); + + private void onlineStateChanged(ValueChangedEvent state) => Schedule(() => + { + if (State.Value == Visibility.Hidden) + return; + + Header.Current.TriggerChange(); + }); + + protected override void Dispose(bool isDisposing) + { + cancellationToken?.Cancel(); + base.Dispose(isDisposing); + } + } +} From 25f511fd5b2b6b2bfbd9e1787182a8a87f086de0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 18 Jan 2021 21:34:24 +0300 Subject: [PATCH 6058/6909] Remove unnecessary full querying --- osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs index 659f52f00f..c2c800badc 100644 --- a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs +++ b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs @@ -48,17 +48,7 @@ namespace osu.Game.Online.Rooms int? beatmapId = SelectedItem.Value.Beatmap.Value.OnlineBeatmapID; string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; - BeatmapInfo matchingBeatmap; - - if (databasedSet.Beatmaps == null) - { - // The given databased beatmap set is not passed in a usable state to check with. - // Perform a full query instead, as per https://github.com/ppy/osu/pull/11415. - matchingBeatmap = Manager.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); - return matchingBeatmap != null; - } - - matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); + var matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); return matchingBeatmap != null; } From 5e476fa189d7637db79c228721a601a005f2d355 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 18 Jan 2021 22:07:25 +0300 Subject: [PATCH 6059/6909] Enforce one missed property back to single-floating type --- osu.Game/Online/API/ArchiveDownloadRequest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/ArchiveDownloadRequest.cs b/osu.Game/Online/API/ArchiveDownloadRequest.cs index ccb4e9c119..0bf238109e 100644 --- a/osu.Game/Online/API/ArchiveDownloadRequest.cs +++ b/osu.Game/Online/API/ArchiveDownloadRequest.cs @@ -10,7 +10,7 @@ namespace osu.Game.Online.API { public readonly TModel Model; - public double Progress { get; private set; } + public float Progress { get; private set; } public event Action DownloadProgressed; From 7476cb3047b400489d39af6494c782de531313ed Mon Sep 17 00:00:00 2001 From: rednir Date: Mon, 18 Jan 2021 19:51:42 +0000 Subject: [PATCH 6060/6909] Sort SkinSection in alphabetical order --- .../Overlays/Settings/Sections/SkinSection.cs | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 5898482e4a..0bfa0ba4f0 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -107,21 +107,20 @@ namespace osu.Game.Overlays.Settings.Sections private void updateItems() { skinItems = skins.GetAllUsableSkins(); - - // insert after lazer built-in skins - int firstNonDefault = skinItems.FindIndex(s => s.ID > 0); - if (firstNonDefault < 0) - firstNonDefault = skinItems.Count; - - skinItems.Insert(firstNonDefault, random_skin_info); - + skinItems = sortList(skinItems); + skinDropdown.Items = skinItems; } private void itemUpdated(ValueChangedEvent> weakItem) { if (weakItem.NewValue.TryGetTarget(out var item)) - Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToArray()); + { + List newDropdownItems = skinDropdown.Items.ToList(); + newDropdownItems.Add(item); + newDropdownItems = sortList(newDropdownItems); + Schedule(() => skinDropdown.Items = newDropdownItems.ToArray()); + } } private void itemRemoved(ValueChangedEvent> weakItem) @@ -130,6 +129,24 @@ namespace osu.Game.Overlays.Settings.Sections Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray()); } + private List sortList(List skinsList) + { + skinsList.Sort((a, b) => String.Compare(a.Name, b.Name, StringComparison.Ordinal)); + for (int i = 0; i < skinsList.Count; i++) + { + // insert lazer built-in skins before user skins + if (skinsList[i].ID <= 0) { + var itemToMove = skinsList[i]; + skinsList.RemoveAt(i); + skinsList.Insert(0, itemToMove); + } + } + skinsList.RemoveAll(s => s.ID == SkinInfo.RANDOM_SKIN); + skinsList.Insert(0, random_skin_info); + + return skinsList; + } + private class SkinSettingsDropdown : SettingsDropdown { protected override OsuDropdown CreateDropdown() => new SkinDropdownControl(); From 63ca9de7e4a85990bd162d25cd9be01f8c91c239 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 18 Jan 2021 23:00:07 +0300 Subject: [PATCH 6061/6909] Rewerite beatmap term properly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs index c2c800badc..32a6c39ea8 100644 --- a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs +++ b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs @@ -38,7 +38,7 @@ namespace osu.Game.Online.Rooms { var verified = verifyDatabasedModel(databasedSet); if (!verified) - Logger.Log("The imported beatmapset does not match the online version.", LoggingTarget.Runtime, LogLevel.Important); + Logger.Log("The imported beatmap set does not match the online version.", LoggingTarget.Runtime, LogLevel.Important); return verified; } From 0b65c0cd25a727fbfa121395e515fe9e8bd295b1 Mon Sep 17 00:00:00 2001 From: rednir Date: Mon, 18 Jan 2021 20:17:42 +0000 Subject: [PATCH 6062/6909] Remove whitespace --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 8e78940ac2..20953b7a9b 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -113,7 +113,6 @@ namespace osu.Game.Overlays.Settings.Sections { skinItems = skins.GetAllUsableSkins(); skinItems = sortList(skinItems); - skinDropdown.Items = skinItems; } From 5233a0449a63b3db599d11535b3484c8ebc50f47 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Mon, 18 Jan 2021 16:08:06 -0500 Subject: [PATCH 6063/6909] Hide main room subscreen on initial mp room creation. Toggle mp room subscreen visibility based on settings overlay visibility before room is created. --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 80991569dc..df61a0ad21 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -39,6 +39,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private MultiplayerMatchSettingsOverlay settingsOverlay; + private readonly Bindable settingsOverlayVisibility = new Bindable(); + + private GridContainer subScreenContainer; + private IBindable isConnected; [CanBeNull] @@ -55,7 +59,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { InternalChildren = new Drawable[] { - new GridContainer + subScreenContainer = new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] @@ -178,6 +182,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden } } }; + + subScreenContainer.Hide(); + settingsOverlayVisibility.BindTo(settingsOverlay.State); + settingsOverlayVisibility.ValueChanged += settingsOverlayVisibilityChanged; } protected override void LoadComplete() @@ -258,6 +266,22 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer readyClickOperation = null; } + private void settingsOverlayVisibilityChanged(ValueChangedEvent settingsOverlayVisibilityChangedEvent) + { + if (client.Room != null) + { + subScreenContainer.Show(); + settingsOverlayVisibility.ValueChanged -= settingsOverlayVisibilityChanged; + } + else + { + if (settingsOverlayVisibilityChangedEvent.NewValue == Visibility.Visible) + subScreenContainer.Hide(); + else + subScreenContainer.Show(); + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); From cbfb999c2845074d799d1126296584898d9be52b Mon Sep 17 00:00:00 2001 From: Mysfit Date: Mon, 18 Jan 2021 17:13:24 -0500 Subject: [PATCH 6064/6909] Use the client.RoomUpdated action instead of binding the value of the settings overlay visibility and creating an event from it based on its ValueChanged action. --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index df61a0ad21..38c2ca7e0c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -39,8 +39,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private MultiplayerMatchSettingsOverlay settingsOverlay; - private readonly Bindable settingsOverlayVisibility = new Bindable(); - private GridContainer subScreenContainer; private IBindable isConnected; @@ -184,8 +182,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }; subScreenContainer.Hide(); - settingsOverlayVisibility.BindTo(settingsOverlay.State); - settingsOverlayVisibility.ValueChanged += settingsOverlayVisibilityChanged; + client.RoomUpdated += roomUpdated; } protected override void LoadComplete() @@ -266,19 +263,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer readyClickOperation = null; } - private void settingsOverlayVisibilityChanged(ValueChangedEvent settingsOverlayVisibilityChangedEvent) + private void roomUpdated() { if (client.Room != null) { + // If the room is updated and is not null, show the room sub screen container and unsubscribe. subScreenContainer.Show(); - settingsOverlayVisibility.ValueChanged -= settingsOverlayVisibilityChanged; - } - else - { - if (settingsOverlayVisibilityChangedEvent.NewValue == Visibility.Visible) - subScreenContainer.Hide(); - else - subScreenContainer.Show(); + client.RoomUpdated -= roomUpdated; } } From f0add0a7cff7b6fe63ca0ccbf0de4caa130fd30d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 19 Jan 2021 01:34:37 +0300 Subject: [PATCH 6065/6909] Make BeatmapSetOverlay use OverlayHeader --- .../Online/TestSceneBeatmapSetOverlay.cs | 2 +- .../Overlays/BeatmapSet/BeatmapSetHeader.cs | 300 ++++++++++++++++- osu.Game/Overlays/BeatmapSet/Header.cs | 313 ------------------ osu.Game/Overlays/BeatmapSetOverlay.cs | 9 +- 4 files changed, 302 insertions(+), 322 deletions(-) delete mode 100644 osu.Game/Overlays/BeatmapSet/Header.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 689321698a..7ff978c7ca 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -315,7 +315,7 @@ namespace osu.Game.Tests.Visual.Online private class TestBeatmapSetOverlay : BeatmapSetOverlay { - public new Header Header => base.Header; + public new BeatmapSetHeader Header => base.Header; } } } diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs index 6511b15fc8..bc9008d1f5 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs @@ -1,23 +1,319 @@ // 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.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.Beatmaps; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Overlays.BeatmapListing.Panels; +using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Rulesets; +using osuTK; +using osuTK.Graphics; namespace osu.Game.Overlays.BeatmapSet { public class BeatmapSetHeader : OverlayHeader { - public readonly Bindable Ruleset = new Bindable(); + private const float transition_duration = 200; + private const float buttons_height = 45; + private const float buttons_spacing = 5; + + public readonly Bindable BeatmapSet = new Bindable(); + + public bool DownloadButtonsVisible => downloadButtonsContainer.Any(); + + public BeatmapPicker Picker { get; private set; } public BeatmapRulesetSelector RulesetSelector { get; private set; } + private IBindable state => downloadTracker.State; + + [Cached(typeof(IBindable))] + private readonly Bindable ruleset = new Bindable(); + + [Resolved] + private IAPIProvider api { get; set; } + + private readonly DownloadTracker downloadTracker; + private OsuSpriteText title, artist; + private AuthorInfo author; + private ExplicitContentBeatmapPill explicitContentPill; + private FillFlowContainer downloadButtonsContainer; + private BeatmapAvailability beatmapAvailability; + private BeatmapSetOnlineStatusPill onlineStatusPill; + private ExternalLinkButton externalLink; + private UpdateableBeatmapSetCover cover; + private Box coverGradient; + private FillFlowContainer fadeContent; + private FavouriteButton favouriteButton; + private LoadingSpinner loading; + private Details details; + + public BeatmapSetHeader() + { + Masking = true; + + EdgeEffect = new EdgeEffectParameters + { + Colour = Color4.Black.Opacity(0.25f), + Type = EdgeEffectType.Shadow, + Radius = 3, + Offset = new Vector2(0f, 1f), + }; + + AddInternal(downloadTracker = new DownloadTracker + { + BeatmapSet = { BindTarget = BeatmapSet } + }); + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + Picker.Beatmap.ValueChanged += b => + { + details.Beatmap = b.NewValue; + externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; + }; + + coverGradient.Colour = ColourInfo.GradientVertical(colourProvider.Background6.Opacity(0.3f), colourProvider.Background6.Opacity(0.8f)); + onlineStatusPill.BackgroundColour = colourProvider.Background6; + + state.BindValueChanged(_ => updateDownloadButtons()); + + BeatmapSet.BindValueChanged(setInfo => + { + Picker.BeatmapSet = RulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = details.BeatmapSet = setInfo.NewValue; + cover.BeatmapSet = setInfo.NewValue; + + if (setInfo.NewValue == null) + { + onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint); + fadeContent.Hide(); + + loading.Show(); + + downloadButtonsContainer.FadeOut(transition_duration); + favouriteButton.FadeOut(transition_duration); + } + else + { + fadeContent.FadeIn(500, Easing.OutQuint); + + loading.Hide(); + + title.Text = setInfo.NewValue.Metadata.Title ?? string.Empty; + artist.Text = setInfo.NewValue.Metadata.Artist ?? string.Empty; + + explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; + + onlineStatusPill.FadeIn(500, Easing.OutQuint); + onlineStatusPill.Status = setInfo.NewValue.OnlineInfo.Status; + + downloadButtonsContainer.FadeIn(transition_duration); + favouriteButton.FadeIn(transition_duration); + + updateDownloadButtons(); + } + }, true); + } + + protected override Drawable CreateContent() => new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + cover = new UpdateableBeatmapSetCover + { + RelativeSizeAxes = Axes.Both, + Masking = true, + }, + coverGradient = new Box + { + RelativeSizeAxes = Axes.Both + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Vertical = BeatmapSetOverlay.Y_PADDING, + Left = BeatmapSetOverlay.X_PADDING, + Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH, + }, + Children = new Drawable[] + { + fadeContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = Picker = new BeatmapPicker(), + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 15 }, + Children = new Drawable[] + { + title = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true) + }, + externalLink = new ExternalLinkButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font + }, + explicitContentPill = new ExplicitContentBeatmapPill + { + Alpha = 0f, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Left = 10, Bottom = 4 }, + } + } + }, + artist = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true), + Margin = new MarginPadding { Bottom = 20 } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = author = new AuthorInfo(), + }, + beatmapAvailability = new BeatmapAvailability(), + new Container + { + RelativeSizeAxes = Axes.X, + Height = buttons_height, + Margin = new MarginPadding { Top = 10 }, + Children = new Drawable[] + { + favouriteButton = new FavouriteButton + { + BeatmapSet = { BindTarget = BeatmapSet } + }, + downloadButtonsContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = buttons_height + buttons_spacing }, + Spacing = new Vector2(buttons_spacing), + }, + }, + }, + }, + }, + } + }, + loading = new LoadingSpinner + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.5f), + }, + new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = BeatmapSetOverlay.X_PADDING }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + onlineStatusPill = new BeatmapSetOnlineStatusPill + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + TextSize = 14, + TextPadding = new MarginPadding { Horizontal = 35, Vertical = 10 } + }, + details = new Details(), + }, + }, + } + }; + + private void updateDownloadButtons() + { + if (BeatmapSet.Value == null) return; + + if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && state.Value != DownloadState.LocallyAvailable) + { + downloadButtonsContainer.Clear(); + return; + } + + switch (state.Value) + { + case DownloadState.LocallyAvailable: + // temporary for UX until new design is implemented. + downloadButtonsContainer.Child = new BeatmapPanelDownloadButton(BeatmapSet.Value) + { + Width = 50, + RelativeSizeAxes = Axes.Y, + SelectedBeatmap = { BindTarget = Picker.Beatmap } + }; + break; + + case DownloadState.Downloading: + case DownloadState.Importing: + // temporary to avoid showing two buttons for maps with novideo. will be fixed in new beatmap overlay design. + downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value); + break; + + default: + downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value); + if (BeatmapSet.Value.OnlineInfo.HasVideo) + downloadButtonsContainer.Add(new HeaderDownloadButton(BeatmapSet.Value, true)); + break; + } + } + + private class DownloadTracker : BeatmapDownloadTrackingComposite + { + public new Bindable State => base.State; + } + protected override OverlayTitle CreateTitle() => new BeatmapHeaderTitle(); protected override Drawable CreateTitleContent() => RulesetSelector = new BeatmapRulesetSelector { - Current = Ruleset + Current = ruleset }; private class BeatmapHeaderTitle : OverlayTitle diff --git a/osu.Game/Overlays/BeatmapSet/Header.cs b/osu.Game/Overlays/BeatmapSet/Header.cs deleted file mode 100644 index 916c21c010..0000000000 --- a/osu.Game/Overlays/BeatmapSet/Header.cs +++ /dev/null @@ -1,313 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Linq; -using osu.Framework.Allocation; -using osu.Framework.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.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online; -using osu.Game.Online.API; -using osu.Game.Overlays.BeatmapListing.Panels; -using osu.Game.Overlays.BeatmapSet.Buttons; -using osu.Game.Rulesets; -using osuTK; -using osuTK.Graphics; - -namespace osu.Game.Overlays.BeatmapSet -{ - public class Header : BeatmapDownloadTrackingComposite - { - private const float transition_duration = 200; - private const float buttons_height = 45; - private const float buttons_spacing = 5; - - private readonly UpdateableBeatmapSetCover cover; - private readonly Box coverGradient; - private readonly OsuSpriteText title, artist; - private readonly AuthorInfo author; - private readonly ExplicitContentBeatmapPill explicitContentPill; - private readonly FillFlowContainer downloadButtonsContainer; - private readonly BeatmapAvailability beatmapAvailability; - private readonly BeatmapSetOnlineStatusPill onlineStatusPill; - public Details Details; - - public bool DownloadButtonsVisible => downloadButtonsContainer.Any(); - - [Resolved] - private IAPIProvider api { get; set; } - - public BeatmapRulesetSelector RulesetSelector => beatmapSetHeader.RulesetSelector; - public readonly BeatmapPicker Picker; - - private readonly FavouriteButton favouriteButton; - private readonly FillFlowContainer fadeContent; - private readonly LoadingSpinner loading; - private readonly BeatmapSetHeader beatmapSetHeader; - - [Cached(typeof(IBindable))] - private readonly Bindable ruleset = new Bindable(); - - public Header() - { - ExternalLinkButton externalLink; - - 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), - }; - - InternalChild = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - beatmapSetHeader = new BeatmapSetHeader - { - Ruleset = { BindTarget = ruleset }, - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - cover = new UpdateableBeatmapSetCover - { - RelativeSizeAxes = Axes.Both, - Masking = true, - }, - coverGradient = new Box - { - RelativeSizeAxes = Axes.Both - }, - }, - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Vertical = BeatmapSetOverlay.Y_PADDING, - Left = BeatmapSetOverlay.X_PADDING, - Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH, - }, - Children = new Drawable[] - { - fadeContent = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = Picker = new BeatmapPicker(), - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 15 }, - Children = new Drawable[] - { - title = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true) - }, - externalLink = new ExternalLinkButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font - }, - explicitContentPill = new ExplicitContentBeatmapPill - { - Alpha = 0f, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Left = 10, Bottom = 4 }, - } - } - }, - artist = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true), - Margin = new MarginPadding { Bottom = 20 } - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = author = new AuthorInfo(), - }, - beatmapAvailability = new BeatmapAvailability(), - new Container - { - RelativeSizeAxes = Axes.X, - Height = buttons_height, - Margin = new MarginPadding { Top = 10 }, - Children = new Drawable[] - { - favouriteButton = new FavouriteButton - { - BeatmapSet = { BindTarget = BeatmapSet } - }, - downloadButtonsContainer = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = buttons_height + buttons_spacing }, - Spacing = new Vector2(buttons_spacing), - }, - }, - }, - }, - }, - } - }, - loading = new LoadingSpinner - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.5f), - }, - new FillFlowContainer - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = BeatmapSetOverlay.X_PADDING }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), - Children = new Drawable[] - { - onlineStatusPill = new BeatmapSetOnlineStatusPill - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - TextSize = 14, - TextPadding = new MarginPadding { Horizontal = 35, Vertical = 10 } - }, - Details = new Details(), - }, - }, - }, - }, - } - }; - - Picker.Beatmap.ValueChanged += b => - { - Details.Beatmap = b.NewValue; - externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; - }; - } - - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) - { - coverGradient.Colour = ColourInfo.GradientVertical(colourProvider.Background6.Opacity(0.3f), colourProvider.Background6.Opacity(0.8f)); - onlineStatusPill.BackgroundColour = colourProvider.Background6; - - State.BindValueChanged(_ => updateDownloadButtons()); - - BeatmapSet.BindValueChanged(setInfo => - { - Picker.BeatmapSet = RulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue; - cover.BeatmapSet = setInfo.NewValue; - - if (setInfo.NewValue == null) - { - onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint); - fadeContent.Hide(); - - loading.Show(); - - downloadButtonsContainer.FadeOut(transition_duration); - favouriteButton.FadeOut(transition_duration); - } - else - { - fadeContent.FadeIn(500, Easing.OutQuint); - - loading.Hide(); - - title.Text = setInfo.NewValue.Metadata.Title ?? string.Empty; - artist.Text = setInfo.NewValue.Metadata.Artist ?? string.Empty; - - explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; - - onlineStatusPill.FadeIn(500, Easing.OutQuint); - onlineStatusPill.Status = setInfo.NewValue.OnlineInfo.Status; - - downloadButtonsContainer.FadeIn(transition_duration); - favouriteButton.FadeIn(transition_duration); - - updateDownloadButtons(); - } - }, true); - } - - private void updateDownloadButtons() - { - if (BeatmapSet.Value == null) return; - - if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable) - { - downloadButtonsContainer.Clear(); - return; - } - - switch (State.Value) - { - case DownloadState.LocallyAvailable: - // temporary for UX until new design is implemented. - downloadButtonsContainer.Child = new BeatmapPanelDownloadButton(BeatmapSet.Value) - { - Width = 50, - RelativeSizeAxes = Axes.Y, - SelectedBeatmap = { BindTarget = Picker.Beatmap } - }; - break; - - case DownloadState.Downloading: - case DownloadState.Importing: - // temporary to avoid showing two buttons for maps with novideo. will be fixed in new beatmap overlay design. - downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value); - break; - - default: - downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value); - if (BeatmapSet.Value.OnlineInfo.HasVideo) - downloadButtonsContainer.Add(new HeaderDownloadButton(BeatmapSet.Value, true)); - break; - } - } - } -} diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index bbec62a85a..86f0f4f614 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -19,15 +19,12 @@ using osuTK; namespace osu.Game.Overlays { - public class BeatmapSetOverlay : FullscreenOverlay // we don't provide a standard header for now. + public class BeatmapSetOverlay : FullscreenOverlay { public const float X_PADDING = 40; public const float Y_PADDING = 25; public const float RIGHT_WIDTH = 275; - //todo: should be an OverlayHeader? or maybe not? - protected new readonly Header Header; - [Resolved] private RulesetStore rulesets { get; set; } @@ -39,7 +36,7 @@ namespace osu.Game.Overlays private readonly Box background; public BeatmapSetOverlay() - : base(OverlayColourScheme.Blue, null) + : base(OverlayColourScheme.Blue, new BeatmapSetHeader()) { OverlayScrollContainer scroll; Info info; @@ -72,7 +69,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - Header = new Header(), + Header, info = new Info() } }, From 1e99357a9790884a7a84e4c052a6ab65921586a7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 19 Jan 2021 12:13:27 +0900 Subject: [PATCH 6066/6909] Update build configurations to net5.0 paths --- .../.idea/runConfigurations/Benchmarks.xml | 6 +++--- .../runConfigurations/CatchRuleset__Tests_.xml | 6 +++--- .../runConfigurations/ManiaRuleset__Tests_.xml | 6 +++--- .../runConfigurations/OsuRuleset__Tests_.xml | 6 +++--- .../runConfigurations/TaikoRuleset__Tests_.xml | 6 +++--- .../.idea/runConfigurations/Tournament.xml | 6 +++--- .../runConfigurations/Tournament__Tests_.xml | 6 +++--- .../.idea/runConfigurations/osu_.xml | 6 +++--- .../.idea/runConfigurations/osu___Tests_.xml | 6 +++--- .../runConfigurations/osu___legacy_osuTK_.xml | 6 +++--- .vscode/launch.json | 18 +++++++++--------- .../.vscode/launch.json | 4 ++-- .../.vscode/launch.json | 4 ++-- .../.vscode/launch.json | 4 ++-- .../.vscode/launch.json | 4 ++-- osu.Game.Tournament.Tests/.vscode/launch.json | 4 ++-- 16 files changed, 49 insertions(+), 49 deletions(-) diff --git a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml index 1815c271b4..8fa7608b8e 100644 --- a/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml +++ b/.idea/.idea.osu.Desktop/.idea/runConfigurations/Benchmarks.xml @@ -1,8 +1,8 @@ - public class HoverClickSounds : HoverSounds { - private SampleChannel sampleClick; + private Sample sampleClick; private readonly MouseButton[] buttons; /// diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index a1d06711db..a91e2ffcab 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -20,7 +20,7 @@ namespace osu.Game.Graphics.UserInterface /// public class HoverSounds : CompositeDrawable { - private SampleChannel sampleHover; + private Sample sampleHover; /// /// Length of debounce for hover sound playback, in milliseconds. diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 6593531099..c075fbb328 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -40,8 +40,8 @@ namespace osu.Game.Graphics.UserInterface protected readonly Nub Nub; private readonly OsuTextFlowContainer labelText; - private SampleChannel sampleChecked; - private SampleChannel sampleUnchecked; + private Sample sampleChecked; + private Sample sampleUnchecked; public OsuCheckbox() { diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index d0356e77c7..bcf5220380 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -25,7 +25,7 @@ namespace osu.Game.Graphics.UserInterface /// private const int max_decimal_digits = 5; - private SampleChannel sample; + private Sample sample; private double lastSampleTime; private T lastSampleValue; @@ -157,14 +157,14 @@ namespace osu.Game.Graphics.UserInterface lastSampleValue = value; lastSampleTime = Clock.CurrentTime; - sample.Frequency.Value = 1 + NormalizedValue * 0.2f; + var channel = sample.Play(); + + channel.Frequency.Value = 1 + NormalizedValue * 0.2f; if (NormalizedValue == 0) - sample.Frequency.Value -= 0.4f; + channel.Frequency.Value -= 0.4f; else if (NormalizedValue == 1) - sample.Frequency.Value += 0.4f; - - sample.Play(); + channel.Frequency.Value += 0.4f; } private void updateTooltipText(T value) diff --git a/osu.Game/Graphics/UserInterface/OsuTextBox.cs b/osu.Game/Graphics/UserInterface/OsuTextBox.cs index 1ec4dfc91a..75af9efc38 100644 --- a/osu.Game/Graphics/UserInterface/OsuTextBox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTextBox.cs @@ -23,11 +23,11 @@ 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; + private readonly Sample[] textAddedSamples = new Sample[4]; + private Sample capsTextAddedSample; + private Sample textRemovedSample; + private Sample textCommittedSample; + private Sample caretMovedSample; /// /// Whether to allow playing a different samples based on the type of character. diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index c7e9a86fa4..a4f46517d5 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -27,7 +27,7 @@ namespace osu.Game.Overlays private Container content; - private SampleChannel sampleBack; + private Sample sampleBack; private List builds; diff --git a/osu.Game/Overlays/MedalOverlay.cs b/osu.Game/Overlays/MedalOverlay.cs index 4425c2f168..0feae16b68 100644 --- a/osu.Game/Overlays/MedalOverlay.cs +++ b/osu.Game/Overlays/MedalOverlay.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays private readonly Sprite innerSpin, outerSpin; private DrawableMedal drawableMedal; - private SampleChannel getSample; + private Sample getSample; private readonly Container content; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 0c8245bebe..7bbffc6172 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -58,7 +58,7 @@ namespace osu.Game.Overlays.Mods private readonly FillFlowContainer footerContainer; - private SampleChannel sampleOn, sampleOff; + private Sample sampleOn, sampleOff; public ModSelectOverlay() { diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index 81ec73a6c5..deec948d14 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -102,9 +102,9 @@ namespace osu.Game.Rulesets.UI this.fallback = fallback; } - public SampleChannel Get(string name) => primary.Get(name) ?? fallback.Get(name); + public Sample Get(string name) => primary.Get(name) ?? fallback.Get(name); - public Task GetAsync(string name) => primary.GetAsync(name) ?? fallback.GetAsync(name); + public Task GetAsync(string name) => primary.GetAsync(name) ?? fallback.GetAsync(name); public Stream GetStream(string name) => primary.GetStream(name) ?? fallback.GetStream(name); diff --git a/osu.Game/Screens/Menu/Button.cs b/osu.Game/Screens/Menu/Button.cs index be6ed9700c..d956394ebb 100644 --- a/osu.Game/Screens/Menu/Button.cs +++ b/osu.Game/Screens/Menu/Button.cs @@ -45,8 +45,8 @@ namespace osu.Game.Screens.Menu public ButtonSystemState VisibleState = ButtonSystemState.TopLevel; private readonly Action clickAction; - private SampleChannel sampleClick; - private SampleChannel sampleHover; + private Sample sampleClick; + private Sample sampleHover; public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => box.ReceivePositionalInputAt(screenSpacePos); diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index f400b2114b..00061d6ea6 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Menu private readonly List public virtual bool DisallowExternalBeatmapRulesetChanges => false; - private SampleChannel sampleExit; + private Sample sampleExit; protected virtual bool PlayResumeSound => true; diff --git a/osu.Game/Screens/Play/FailAnimation.cs b/osu.Game/Screens/Play/FailAnimation.cs index 608f20affd..71bea2a145 100644 --- a/osu.Game/Screens/Play/FailAnimation.cs +++ b/osu.Game/Screens/Play/FailAnimation.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.Play private const float duration = 2500; - private SampleChannel failSample; + private Sample failSample; public FailAnimation(DrawableRuleset drawableRuleset) { diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 1fcbed7ef7..7dda5973a0 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -91,7 +91,7 @@ namespace osu.Game.Screens.Play [Resolved] private MusicController musicController { get; set; } - private SampleChannel sampleRestart; + private Sample sampleRestart; public BreakOverlay BreakOverlay; diff --git a/osu.Game/Screens/Play/SkipOverlay.cs b/osu.Game/Screens/Play/SkipOverlay.cs index 92b304de91..3f214e49d9 100644 --- a/osu.Game/Screens/Play/SkipOverlay.cs +++ b/osu.Game/Screens/Play/SkipOverlay.cs @@ -230,7 +230,7 @@ namespace osu.Game.Screens.Play private Box background; private AspectContainer aspect; - private SampleChannel sampleConfirm; + private Sample sampleConfirm; public Button() { diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index f1120f55a6..c5c1e2eac7 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -20,7 +20,7 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselHeader : Container { - private SampleChannel sampleHover; + private Sample sampleHover; private readonly Box hoverLayer; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 6c0bd3a228..a91dc49069 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -86,10 +86,10 @@ namespace osu.Game.Screens.Select protected ModSelectOverlay ModSelect { get; private set; } - protected SampleChannel SampleConfirm { get; private set; } + protected Sample SampleConfirm { get; private set; } - private SampleChannel sampleChangeDifficulty; - private SampleChannel sampleChangeBeatmap; + private Sample sampleChangeDifficulty; + private Sample sampleChangeBeatmap; private Container carouselContainer; diff --git a/osu.Game/Skinning/DefaultSkin.cs b/osu.Game/Skinning/DefaultSkin.cs index 61d0112c89..346c7b3c65 100644 --- a/osu.Game/Skinning/DefaultSkin.cs +++ b/osu.Game/Skinning/DefaultSkin.cs @@ -24,7 +24,7 @@ namespace osu.Game.Skinning public override Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => null; - public override SampleChannel GetSample(ISampleInfo sampleInfo) => null; + public override Sample GetSample(ISampleInfo sampleInfo) => null; public override IBindable GetConfig(TLookup lookup) { diff --git a/osu.Game/Skinning/ISkin.cs b/osu.Game/Skinning/ISkin.cs index 5abd963773..ef8de01042 100644 --- a/osu.Game/Skinning/ISkin.cs +++ b/osu.Game/Skinning/ISkin.cs @@ -48,7 +48,7 @@ namespace osu.Game.Skinning /// The requested sample. /// A matching sample channel, or null if unavailable. [CanBeNull] - SampleChannel GetSample(ISampleInfo sampleInfo); + Sample GetSample(ISampleInfo sampleInfo); /// /// Retrieve a configuration value. diff --git a/osu.Game/Skinning/LegacyBeatmapSkin.cs b/osu.Game/Skinning/LegacyBeatmapSkin.cs index fdcb81b574..fb4207b647 100644 --- a/osu.Game/Skinning/LegacyBeatmapSkin.cs +++ b/osu.Game/Skinning/LegacyBeatmapSkin.cs @@ -39,7 +39,7 @@ namespace osu.Game.Skinning return base.GetConfig(lookup); } - public override SampleChannel GetSample(ISampleInfo sampleInfo) + public override Sample GetSample(ISampleInfo sampleInfo) { if (sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy && legacy.CustomSampleBank == 0) { diff --git a/osu.Game/Skinning/LegacySkin.cs b/osu.Game/Skinning/LegacySkin.cs index 090ffaebd7..e5d0217671 100644 --- a/osu.Game/Skinning/LegacySkin.cs +++ b/osu.Game/Skinning/LegacySkin.cs @@ -29,7 +29,7 @@ namespace osu.Game.Skinning protected TextureStore Textures; [CanBeNull] - protected IResourceStore Samples; + protected ISampleStore Samples; /// /// Whether texture for the keys exists. @@ -452,7 +452,7 @@ namespace osu.Game.Skinning return null; } - public override SampleChannel GetSample(ISampleInfo sampleInfo) + public override Sample GetSample(ISampleInfo sampleInfo) { IEnumerable lookupNames; diff --git a/osu.Game/Skinning/LegacySkinTransformer.cs b/osu.Game/Skinning/LegacySkinTransformer.cs index ebc4757e75..e2f4a82a54 100644 --- a/osu.Game/Skinning/LegacySkinTransformer.cs +++ b/osu.Game/Skinning/LegacySkinTransformer.cs @@ -34,14 +34,14 @@ namespace osu.Game.Skinning public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => Source.GetTexture(componentName, wrapModeS, wrapModeT); - public virtual SampleChannel GetSample(ISampleInfo sampleInfo) + public virtual Sample GetSample(ISampleInfo sampleInfo) { if (!(sampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacySample)) return Source.GetSample(sampleInfo); var playLayeredHitSounds = GetConfig(LegacySetting.LayeredHitSounds); if (legacySample.IsLayered && playLayeredHitSounds?.Value == false) - return new SampleChannelVirtual(); + return new SampleVirtual(); return Source.GetSample(sampleInfo); } diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index cb5234c847..4b6099e85f 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -67,7 +67,7 @@ namespace osu.Game.Skinning } } - public override void Play(bool restart = true) + public override void Play() { cancelPendingStart(); RequestedPlaying = true; @@ -75,7 +75,7 @@ namespace osu.Game.Skinning if (samplePlaybackDisabled.Value) return; - base.Play(restart); + base.Play(); } public override void Stop() diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 2a0f480b48..2c83023fdc 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -27,6 +27,7 @@ namespace osu.Game.Skinning private readonly AudioContainer sampleContainer; private ISampleInfo sampleInfo; + private SampleChannel activeChannel; [Resolved] private ISampleStore sampleStore { get; set; } @@ -99,7 +100,7 @@ namespace osu.Game.Skinning if (ch == null) return; - sampleContainer.Add(Sample = new DrawableSample(ch) { Looping = Looping }); + sampleContainer.Add(Sample = new DrawableSample(ch)); // Start playback internally for the new sample if the previous one was playing beforehand. if (wasPlaying && Looping) @@ -109,18 +110,26 @@ namespace osu.Game.Skinning /// /// Plays the sample. /// - /// Whether to play the sample from the beginning. - public void Play(bool restart = true) => Sample?.Play(restart); + public void Play() + { + if (Sample == null) + return; + + activeChannel = Sample.Play(); + activeChannel.Looping = Looping; + } /// /// Stops the sample. /// - public void Stop() => Sample?.Stop(); + public void Stop() => activeChannel?.Stop(); /// /// Whether the sample is currently playing. /// - public bool Playing => Sample?.Playing ?? false; + public bool Playing => activeChannel?.Playing ?? false; + + public bool Played => activeChannel?.Played ?? false; private bool looping; @@ -134,8 +143,8 @@ namespace osu.Game.Skinning { looping = value; - if (Sample != null) - Sample.Looping = value; + if (activeChannel != null) + activeChannel.Looping = value; } } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index 4b0cf02c0a..e8d84b49f9 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -19,7 +19,7 @@ namespace osu.Game.Skinning public abstract Drawable GetDrawableComponent(ISkinComponent componentName); - public abstract SampleChannel GetSample(ISampleInfo sampleInfo); + public abstract Sample GetSample(ISampleInfo sampleInfo); public Texture GetTexture(string componentName) => GetTexture(componentName, default, default); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 99c64b13a4..2826c826a5 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -171,7 +171,7 @@ namespace osu.Game.Skinning public Texture GetTexture(string componentName, WrapMode wrapModeS, WrapMode wrapModeT) => CurrentSkin.Value.GetTexture(componentName, wrapModeS, wrapModeT); - public SampleChannel GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo); + public Sample GetSample(ISampleInfo sampleInfo) => CurrentSkin.Value.GetSample(sampleInfo); public IBindable GetConfig(TLookup lookup) => CurrentSkin.Value.GetConfig(lookup); diff --git a/osu.Game/Skinning/SkinProvidingContainer.cs b/osu.Game/Skinning/SkinProvidingContainer.cs index 27cf0c697a..ba67d0a678 100644 --- a/osu.Game/Skinning/SkinProvidingContainer.cs +++ b/osu.Game/Skinning/SkinProvidingContainer.cs @@ -59,9 +59,9 @@ namespace osu.Game.Skinning return fallbackSource?.GetTexture(componentName, wrapModeS, wrapModeT); } - public SampleChannel GetSample(ISampleInfo sampleInfo) + public Sample GetSample(ISampleInfo sampleInfo) { - SampleChannel sourceChannel; + Sample sourceChannel; if (AllowSampleLookup(sampleInfo) && (sourceChannel = skin?.GetSample(sampleInfo)) != null) return sourceChannel; diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index a874e9a0db..06c694dc7a 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -119,13 +119,12 @@ namespace osu.Game.Skinning /// /// Plays the samples. /// - /// Whether to play the sample from the beginning. - public virtual void Play(bool restart = true) + public virtual void Play() { samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) - c.Play(restart); + c.Play(); }); } @@ -188,6 +187,8 @@ namespace osu.Game.Skinning /// public bool IsPlaying => samplesContainer.Any(s => s.Playing); + public bool IsPlayed => samplesContainer.Any(s => s.Played); + #endregion } } From 5a64abee648f7c49f92305f6ba988c19199e9f64 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Jan 2021 11:51:31 +0300 Subject: [PATCH 6070/6909] Inline with above method --- .../Online/Rooms/MultiplayerBeatmapTracker.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs index 32a6c39ea8..59dfdcf464 100644 --- a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs +++ b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs @@ -35,21 +35,19 @@ namespace osu.Game.Online.Rooms } protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet) - { - var verified = verifyDatabasedModel(databasedSet); - if (!verified) - Logger.Log("The imported beatmap set does not match the online version.", LoggingTarget.Runtime, LogLevel.Important); - - return verified; - } - - private bool verifyDatabasedModel(BeatmapSetInfo databasedSet) { int? beatmapId = SelectedItem.Value.Beatmap.Value.OnlineBeatmapID; string checksum = SelectedItem.Value.Beatmap.Value.MD5Hash; var matchingBeatmap = databasedSet.Beatmaps.FirstOrDefault(b => b.OnlineBeatmapID == beatmapId && b.MD5Hash == checksum); - return matchingBeatmap != null; + + if (matchingBeatmap == null) + { + Logger.Log("The imported beatmap set does not match the online version.", LoggingTarget.Runtime, LogLevel.Important); + return false; + } + + return true; } protected override bool IsModelAvailableLocally() From 63b4c529a6fe1f3a996f6074bfb736144e2e9c76 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Jan 2021 11:57:40 +0300 Subject: [PATCH 6071/6909] Add xmldoc explaining what the multiplayer beatmap tracker is for --- osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs index 59dfdcf464..28e4872ad3 100644 --- a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs +++ b/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs @@ -9,6 +9,12 @@ using osu.Game.Beatmaps; namespace osu.Game.Online.Rooms { + /// + /// Represent a checksum-verifying beatmap availability tracker usable for online play screens. + /// + /// This differs from a regular download tracking composite as this accounts for the + /// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap. + /// public class MultiplayerBeatmapTracker : DownloadTrackingComposite { public readonly IBindable SelectedItem = new Bindable(); From f1894a8bacb83c8e35c62422447d2d7cd78a3aa9 Mon Sep 17 00:00:00 2001 From: rednir Date: Tue, 19 Jan 2021 12:17:56 +0000 Subject: [PATCH 6072/6909] fixed itemUpdated() --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 0bfa0ba4f0..ba92ed5b04 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -116,8 +116,7 @@ namespace osu.Game.Overlays.Settings.Sections { if (weakItem.NewValue.TryGetTarget(out var item)) { - List newDropdownItems = skinDropdown.Items.ToList(); - newDropdownItems.Add(item); + List newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList(); newDropdownItems = sortList(newDropdownItems); Schedule(() => skinDropdown.Items = newDropdownItems.ToArray()); } From 9b7187e3c855fa50dfc8331f43730a113ed95059 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Tue, 19 Jan 2021 08:23:31 -0500 Subject: [PATCH 6073/6909] Revert "Use fades instead of event listening. Fixed same issue in the playlist room creation." This reverts commit 3a7608275d5b0188c3ab70df2ae9482fad252392. --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 45 +++++-------------- .../Playlists/PlaylistsRoomSubScreen.cs | 38 +--------------- 2 files changed, 13 insertions(+), 70 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 03a76c66f2..38c2ca7e0c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -180,40 +180,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden } } }; - } - public override void OnEntering(IScreen last) - { - base.OnEntering(last); - - subScreenContainer.FadeOut().Delay(1000).FadeIn(500); - } - - public override bool OnExiting(IScreen next) - { - if (base.OnExiting(next)) - return true; - - subScreenContainer.FadeOut(); - - return false; - } - - public override void OnResuming(IScreen last) - { - base.OnResuming(last); - - if (client.Room == null) - subScreenContainer.FadeOut().Delay(1000).FadeIn(500); - else - subScreenContainer.FadeInFromZero(); - } - - public override void OnSuspending(IScreen next) - { - subScreenContainer.FadeOut(); - - base.OnSuspending(next); + subScreenContainer.Hide(); + client.RoomUpdated += roomUpdated; } protected override void LoadComplete() @@ -294,6 +263,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer readyClickOperation = null; } + private void roomUpdated() + { + if (client.Room != null) + { + // If the room is updated and is not null, show the room sub screen container and unsubscribe. + subScreenContainer.Show(); + client.RoomUpdated -= roomUpdated; + } + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 583956b3f1..e76ca995bf 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -33,8 +33,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private OverlinedHeader participantsHeader; - private GridContainer subScreenContainer; - public PlaylistsRoomSubScreen(Room room) { Title = room.RoomID.Value == null ? "New playlist" : room.Name.Value; @@ -46,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { InternalChildren = new Drawable[] { - subScreenContainer = new GridContainer + new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] @@ -197,40 +195,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] private IAPIProvider api { get; set; } - public override void OnEntering(IScreen last) - { - base.OnEntering(last); - - subScreenContainer.FadeOut().Delay(1000).FadeIn(500); - } - - public override bool OnExiting(IScreen next) - { - if (base.OnExiting(next)) - return true; - - subScreenContainer.FadeOut(); - - return false; - } - - public override void OnResuming(IScreen last) - { - base.OnResuming(last); - - if (roomId.Value == null) - subScreenContainer.FadeOut().Delay(1000).FadeIn(500); - else - subScreenContainer.FadeInFromZero(); - } - - public override void OnSuspending(IScreen next) - { - subScreenContainer.FadeOut(); - - base.OnSuspending(next); - } - protected override void LoadComplete() { base.LoadComplete(); From 6d1d488831f1992957b6817eaa590114e6db60b5 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Tue, 19 Jan 2021 08:24:14 -0500 Subject: [PATCH 6074/6909] Revert "Use the client.RoomUpdated action instead of binding the value of the settings overlay visibility and creating an event from it based on its ValueChanged action." This reverts commit cbfb999c2845074d799d1126296584898d9be52b. --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 38c2ca7e0c..df61a0ad21 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private MultiplayerMatchSettingsOverlay settingsOverlay; + private readonly Bindable settingsOverlayVisibility = new Bindable(); + private GridContainer subScreenContainer; private IBindable isConnected; @@ -182,7 +184,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }; subScreenContainer.Hide(); - client.RoomUpdated += roomUpdated; + settingsOverlayVisibility.BindTo(settingsOverlay.State); + settingsOverlayVisibility.ValueChanged += settingsOverlayVisibilityChanged; } protected override void LoadComplete() @@ -263,13 +266,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer readyClickOperation = null; } - private void roomUpdated() + private void settingsOverlayVisibilityChanged(ValueChangedEvent settingsOverlayVisibilityChangedEvent) { if (client.Room != null) { - // If the room is updated and is not null, show the room sub screen container and unsubscribe. subScreenContainer.Show(); - client.RoomUpdated -= roomUpdated; + settingsOverlayVisibility.ValueChanged -= settingsOverlayVisibilityChanged; + } + else + { + if (settingsOverlayVisibilityChangedEvent.NewValue == Visibility.Visible) + subScreenContainer.Hide(); + else + subScreenContainer.Show(); } } From 33677f57702f2b17dd6dd9e385a4553aa66bdd9a Mon Sep 17 00:00:00 2001 From: Mysfit Date: Tue, 19 Jan 2021 08:52:43 -0500 Subject: [PATCH 6075/6909] Use BindValueChanged to show main content for new multiplayer and playlist rooms when the settings overlay is hidden. --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 37 +++++++------------ .../Playlists/PlaylistsRoomSubScreen.cs | 15 +++++++- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index df61a0ad21..0ec43c2b10 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -39,15 +39,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private MultiplayerMatchSettingsOverlay settingsOverlay; - private readonly Bindable settingsOverlayVisibility = new Bindable(); - - private GridContainer subScreenContainer; - private IBindable isConnected; [CanBeNull] private IDisposable readyClickOperation; + private GridContainer mainContent; + public MultiplayerMatchSubScreen(Room room) { Title = room.RoomID.Value == null ? "New room" : room.Name.Value; @@ -59,7 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { InternalChildren = new Drawable[] { - subScreenContainer = new GridContainer + mainContent = new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] @@ -183,9 +181,16 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } }; - subScreenContainer.Hide(); - settingsOverlayVisibility.BindTo(settingsOverlay.State); - settingsOverlayVisibility.ValueChanged += settingsOverlayVisibilityChanged; + if (client.Room == null) + { + mainContent.Hide(); + + settingsOverlay.State.BindValueChanged(visibility => + { + if (visibility.NewValue == Visibility.Hidden) + mainContent.Show(); + }, true); + } } protected override void LoadComplete() @@ -266,22 +271,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer readyClickOperation = null; } - private void settingsOverlayVisibilityChanged(ValueChangedEvent settingsOverlayVisibilityChangedEvent) - { - if (client.Room != null) - { - subScreenContainer.Show(); - settingsOverlayVisibility.ValueChanged -= settingsOverlayVisibilityChanged; - } - else - { - if (settingsOverlayVisibilityChangedEvent.NewValue == Visibility.Visible) - subScreenContainer.Hide(); - else - subScreenContainer.Show(); - } - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index e76ca995bf..b8d9d258f5 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -33,6 +33,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists private OverlinedHeader participantsHeader; + private GridContainer mainContent; + public PlaylistsRoomSubScreen(Room room) { Title = room.RoomID.Value == null ? "New playlist" : room.Name.Value; @@ -44,7 +46,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { InternalChildren = new Drawable[] { - new GridContainer + mainContent = new GridContainer { RelativeSizeAxes = Axes.Both, Content = new[] @@ -190,6 +192,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden } } }; + + if (roomId.Value == null) + { + mainContent.Hide(); + + settingsOverlay.State.BindValueChanged(visibility => + { + if (visibility.NewValue == Visibility.Hidden) + mainContent.Show(); + }, true); + } } [Resolved] From 31e61326e1ea4d4f3b3e1e8450c8fdc385939d0d Mon Sep 17 00:00:00 2001 From: rednir Date: Tue, 19 Jan 2021 14:00:17 +0000 Subject: [PATCH 6076/6909] Only user skins are sorted --- .../Overlays/Settings/Sections/SkinSection.cs | 25 ++++++++----------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index ba92ed5b04..1dcc5d824d 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -38,6 +38,8 @@ namespace osu.Game.Overlays.Settings.Sections private List skinItems; + private int firstNonDefault; + [Resolved] private SkinManager skins { get; set; } @@ -107,8 +109,10 @@ namespace osu.Game.Overlays.Settings.Sections private void updateItems() { skinItems = skins.GetAllUsableSkins(); + firstNonDefault = skinItems.FindIndex(s => s.ID > 0); + + skinItems.Insert(firstNonDefault, random_skin_info); skinItems = sortList(skinItems); - skinDropdown.Items = skinItems; } @@ -130,19 +134,12 @@ namespace osu.Game.Overlays.Settings.Sections private List sortList(List skinsList) { - skinsList.Sort((a, b) => String.Compare(a.Name, b.Name, StringComparison.Ordinal)); - for (int i = 0; i < skinsList.Count; i++) - { - // insert lazer built-in skins before user skins - if (skinsList[i].ID <= 0) { - var itemToMove = skinsList[i]; - skinsList.RemoveAt(i); - skinsList.Insert(0, itemToMove); - } - } - skinsList.RemoveAll(s => s.ID == SkinInfo.RANDOM_SKIN); - skinsList.Insert(0, random_skin_info); - + // Sort user skins seperate from built-in skins + List userSkinsList = skinsList.GetRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); + skinsList.RemoveRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); + userSkinsList.Sort((a, b) => String.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + + skinsList.AddRange(userSkinsList); return skinsList; } From c5c5fdca454cd9f1a036eba686d33673d074c038 Mon Sep 17 00:00:00 2001 From: rednir Date: Tue, 19 Jan 2021 14:10:08 +0000 Subject: [PATCH 6077/6909] Remove another whitespace --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index b42091ce30..9ca8ec9051 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -143,7 +143,6 @@ namespace osu.Game.Overlays.Settings.Sections List userSkinsList = skinsList.GetRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); skinsList.RemoveRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); userSkinsList.Sort((a, b) => String.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); - skinsList.AddRange(userSkinsList); return skinsList; } From b265d2dab499f4cdf75fadbc1acc20acb2f21ad3 Mon Sep 17 00:00:00 2001 From: rednir Date: Tue, 19 Jan 2021 14:16:22 +0000 Subject: [PATCH 6078/6909] Remove another whitespace --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index b42091ce30..9ca8ec9051 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -143,7 +143,6 @@ namespace osu.Game.Overlays.Settings.Sections List userSkinsList = skinsList.GetRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); skinsList.RemoveRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); userSkinsList.Sort((a, b) => String.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); - skinsList.AddRange(userSkinsList); return skinsList; } From 052e9eef02661abf004152ac29c015a329ad20d3 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Tue, 19 Jan 2021 09:16:39 -0500 Subject: [PATCH 6079/6909] Added inline comments --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 ++ osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 ++ 2 files changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 0ec43c2b10..7c4b6d18ec 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -183,6 +183,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.Room == null) { + // A new room is being created. + // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed. mainContent.Hide(); settingsOverlay.State.BindValueChanged(visibility => diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index b8d9d258f5..22580f0537 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -195,6 +195,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists if (roomId.Value == null) { + // A new room is being created. + // The main content should be hidden until the settings overlay is hidden, signaling the room is ready to be displayed. mainContent.Hide(); settingsOverlay.State.BindValueChanged(visibility => From ed3dece9f8f7c0c0e0d35ac3fa330f2359b84cbc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Jan 2021 18:36:05 +0300 Subject: [PATCH 6080/6909] Fix wrong importing of test beatmaps Importing via `testBeatmapSet` causes the beatmapset hash to not be calculated due to no files existing in the importing process, which leads into not reusing existing test beatmaps due to no hash. --- osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs index d692e6f9a3..a6275f14e6 100644 --- a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs +++ b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs @@ -94,13 +94,13 @@ namespace osu.Game.Tests.Online public void TestTrackerRespectsSoftDeleting() { AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); - AddStep("import beatmap", () => beatmaps.Import(testBeatmapSet).Wait()); + AddStep("import beatmap", () => beatmaps.Import(testBeatmapFile).Wait()); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); - AddStep("delete beatmap", () => beatmaps.Delete(testBeatmapSet)); + AddStep("delete beatmap", () => beatmaps.Delete(beatmaps.QueryBeatmapSet(b => b.OnlineBeatmapSetID == testBeatmapSet.OnlineBeatmapSetID))); addAvailabilityCheckStep("state not downloaded", BeatmapAvailability.NotDownloaded); - AddStep("undelete beatmap", () => beatmaps.Undelete(testBeatmapSet)); + AddStep("undelete beatmap", () => beatmaps.Undelete(beatmaps.QueryBeatmapSet(b => b.OnlineBeatmapSetID == testBeatmapSet.OnlineBeatmapSetID))); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); } From 14ff2af00ef868bd1ff65dd22f7eed242d7c2e8b Mon Sep 17 00:00:00 2001 From: rednir Date: Tue, 19 Jan 2021 15:37:59 +0000 Subject: [PATCH 6081/6909] Satisfy AppVeyor --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 9ca8ec9051..7023702d0e 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -142,7 +142,7 @@ namespace osu.Game.Overlays.Settings.Sections // Sort user skins seperate from built-in skins List userSkinsList = skinsList.GetRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); skinsList.RemoveRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); - userSkinsList.Sort((a, b) => String.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + userSkinsList.Sort((a, b) => System.String.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); skinsList.AddRange(userSkinsList); return skinsList; } From dba01cf2b1ea7cf3445f14e30855ce9e95e6bd76 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Jan 2021 18:43:16 +0300 Subject: [PATCH 6082/6909] Use beatmap "soleily" and remove no longer needed archive --- .../TestSceneMultiplayerBeatmapTracker.cs | 41 ++++++++++-------- .../Resources/Archives/test-beatmap.osz | Bin 7286 -> 0 bytes 2 files changed, 24 insertions(+), 17 deletions(-) delete mode 100644 osu.Game.Tests/Resources/Archives/test-beatmap.osz diff --git a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs index a6275f14e6..9caecc198a 100644 --- a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs +++ b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.IO; using System.Threading; using System.Threading.Tasks; @@ -10,15 +11,17 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Bindables; +using osu.Framework.Extensions; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Formats; using osu.Game.Database; +using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Rulesets; -using osu.Game.Tests.Beatmaps; using osu.Game.Tests.Resources; using osu.Game.Tests.Visual; @@ -49,9 +52,9 @@ namespace osu.Game.Tests.Online { beatmaps.AllowImport = new TaskCompletionSource(); - testBeatmapFile = getTestBeatmapOsz(); + testBeatmapFile = TestResources.GetTestBeatmapForImport(); - testBeatmapInfo = new TestBeatmap(Ruleset.Value).BeatmapInfo; + testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile); testBeatmapSet = testBeatmapInfo.BeatmapSet; var existing = beatmaps.QueryBeatmapSet(s => s.OnlineBeatmapSetID == testBeatmapSet.OnlineBeatmapSetID); @@ -109,16 +112,10 @@ namespace osu.Game.Tests.Online { AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); - BeatmapInfo wrongBeatmap = null; - - AddStep("import wrong checksum beatmap", () => + AddStep("import altered beatmap", () => { - wrongBeatmap = new TestBeatmap(Ruleset.Value).BeatmapInfo; - wrongBeatmap.MD5Hash = "1337"; - - beatmaps.Import(wrongBeatmap.BeatmapSet).Wait(); + beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait(); }); - AddAssert("wrong beatmap available", () => beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == wrongBeatmap.OnlineBeatmapID) != null); addAvailabilityCheckStep("state still not downloaded", BeatmapAvailability.NotDownloaded); AddStep("recreate tracker", () => Child = tracker = new MultiplayerBeatmapTracker @@ -135,15 +132,25 @@ namespace osu.Game.Tests.Online AddAssert(description, () => tracker.Availability.Value.Equals(expected.Invoke())); } - private string getTestBeatmapOsz() + private static BeatmapInfo getTestBeatmapInfo(string archiveFile) { - var filename = Path.GetTempFileName() + ".osz"; + BeatmapInfo info; - using (var stream = TestResources.OpenResource("Archives/test-beatmap.osz")) - using (var file = File.Create(filename)) - stream.CopyTo(file); + using (var archive = new ZipArchiveReader(File.OpenRead(archiveFile))) + using (var stream = archive.GetStream("Soleily - Renatus (Gamu) [Insane].osu")) + using (var reader = new LineBufferedReader(stream)) + { + var decoder = Decoder.GetDecoder(reader); + var beatmap = decoder.Decode(reader); - return filename; + info = beatmap.BeatmapInfo; + info.BeatmapSet.Beatmaps = new List { info }; + info.BeatmapSet.Metadata = info.Metadata; + info.MD5Hash = stream.ComputeMD5Hash(); + info.Hash = stream.ComputeSHA2Hash(); + } + + return info; } private class TestBeatmapManager : BeatmapManager diff --git a/osu.Game.Tests/Resources/Archives/test-beatmap.osz b/osu.Game.Tests/Resources/Archives/test-beatmap.osz deleted file mode 100644 index f98fd2f8ffef0968270a1f2c7d75162de80417c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7286 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 BJ!=2} From a880b8d21dae103d8e3340678384356b9218b4f2 Mon Sep 17 00:00:00 2001 From: rednir Date: Tue, 19 Jan 2021 16:11:16 +0000 Subject: [PATCH 6083/6909] Satisfy AppVeyor --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 9ca8ec9051..c39f95d93a 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -142,7 +142,7 @@ namespace osu.Game.Overlays.Settings.Sections // Sort user skins seperate from built-in skins List userSkinsList = skinsList.GetRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); skinsList.RemoveRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); - userSkinsList.Sort((a, b) => String.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); + userSkinsList.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); skinsList.AddRange(userSkinsList); return skinsList; } From 206a0b8bace7733f6ac6f12909adcaecb6472d08 Mon Sep 17 00:00:00 2001 From: rednir Date: Tue, 19 Jan 2021 16:55:50 +0000 Subject: [PATCH 6084/6909] Fix firstNonDefault staying as -1 --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index c39f95d93a..a3e472a9f9 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -115,7 +115,8 @@ namespace osu.Game.Overlays.Settings.Sections { skinItems = skins.GetAllUsableSkins(); firstNonDefault = skinItems.FindIndex(s => s.ID > 0); - + if (firstNonDefault < 0) + firstNonDefault = skinItems.Count; skinItems.Insert(firstNonDefault, random_skin_info); skinItems = sortList(skinItems); skinDropdown.Items = skinItems; From 34612ae233fbf664c7db40567b09505be918d3f2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 19 Jan 2021 19:03:29 +0300 Subject: [PATCH 6085/6909] Forward internal management to a container alongside tracker --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 17 ++++++++++++++--- .../Multiplayer/MultiplayerMatchSubScreen.cs | 4 ++-- .../Playlists/PlaylistsRoomSubScreen.cs | 4 ++-- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index c049d4be20..4b89a0c278 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -7,6 +7,8 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -43,14 +45,23 @@ namespace osu.Game.Screens.OnlinePlay.Match [Cached] protected readonly MultiplayerBeatmapTracker BeatmapTracker; + private readonly Container content = new Container { RelativeSizeAxes = Axes.Both }; + protected RoomSubScreen() { - InternalChild = BeatmapTracker = new MultiplayerBeatmapTracker + base.AddInternal(BeatmapTracker = new MultiplayerBeatmapTracker { - SelectedItem = { BindTarget = SelectedItem }, - }; + SelectedItem = { BindTarget = SelectedItem } + }); + + base.AddInternal(content); } + // This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690) + protected override void AddInternal(Drawable drawable) => content.Add(drawable); + protected override bool RemoveInternal(Drawable drawable) => content.Remove(drawable); + protected override void ClearInternal(bool disposeChildren = true) => content.Clear(disposeChildren); + [BackgroundDependencyLoader] private void load(AudioManager audio) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a641935b9a..fa4b972f98 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -53,7 +53,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [BackgroundDependencyLoader] private void load() { - AddRangeInternal(new Drawable[] + InternalChildren = new Drawable[] { new GridContainer { @@ -176,7 +176,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RelativeSizeAxes = Axes.Both, State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden } } - }); + }; } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 7b3cdf16db..781c455eb4 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -42,7 +42,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [BackgroundDependencyLoader] private void load() { - AddRangeInternal(new Drawable[] + InternalChildren = new Drawable[] { new GridContainer { @@ -188,7 +188,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists EditPlaylist = () => this.Push(new MatchSongSelect()), State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden } } - }); + }; } [Resolved] From b00c6a1d60723622101512b13168214ff61696aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Jan 2021 18:29:55 +0100 Subject: [PATCH 6086/6909] Make first non-default skin index a property The previous code was very brittle - it was not always updating properly, and seems to have worked either by a carefully crafted set of circumstances, or just plain coincidence. Having this be a get-only property avoids potential error in the future caused by not updating the index properly, at the expense of an added linear lookup. --- .../Overlays/Settings/Sections/SkinSection.cs | 21 ++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index a3e472a9f9..bbb4c50f6b 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -38,7 +38,17 @@ namespace osu.Game.Overlays.Settings.Sections private List skinItems; - private int firstNonDefault; + private int firstNonDefaultSkinIndex + { + get + { + var index = skinItems.FindIndex(s => s.ID > 0); + if (index < 0) + index = skinItems.Count; + + return index; + } + } [Resolved] private SkinManager skins { get; set; } @@ -114,10 +124,7 @@ namespace osu.Game.Overlays.Settings.Sections private void updateItems() { skinItems = skins.GetAllUsableSkins(); - firstNonDefault = skinItems.FindIndex(s => s.ID > 0); - if (firstNonDefault < 0) - firstNonDefault = skinItems.Count; - skinItems.Insert(firstNonDefault, random_skin_info); + skinItems.Insert(firstNonDefaultSkinIndex, random_skin_info); skinItems = sortList(skinItems); skinDropdown.Items = skinItems; } @@ -141,8 +148,8 @@ namespace osu.Game.Overlays.Settings.Sections private List sortList(List skinsList) { // Sort user skins seperate from built-in skins - List userSkinsList = skinsList.GetRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); - skinsList.RemoveRange(firstNonDefault + 1, skinsList.Count - (firstNonDefault + 1)); + List userSkinsList = skinsList.GetRange(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex); + skinsList.RemoveRange(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex); userSkinsList.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); skinsList.AddRange(userSkinsList); return skinsList; From 78e590d25da4b570447d8fb3b8462f56fa52f50c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Jan 2021 18:36:42 +0100 Subject: [PATCH 6087/6909] Refactor skin sorting method * Rename to `sortUserSkins` to convey meaning better. * Sort in-place instead of slicing the list. * Change to `void` to avoid misleading users that the method returns a new list instance. * Fix typo in comment. --- .../Overlays/Settings/Sections/SkinSection.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index bbb4c50f6b..75c0324408 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -125,7 +125,7 @@ namespace osu.Game.Overlays.Settings.Sections { skinItems = skins.GetAllUsableSkins(); skinItems.Insert(firstNonDefaultSkinIndex, random_skin_info); - skinItems = sortList(skinItems); + sortUserSkins(skinItems); skinDropdown.Items = skinItems; } @@ -134,7 +134,7 @@ namespace osu.Game.Overlays.Settings.Sections if (weakItem.NewValue.TryGetTarget(out var item)) { List newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList(); - newDropdownItems = sortList(newDropdownItems); + sortUserSkins(newDropdownItems); Schedule(() => skinDropdown.Items = newDropdownItems.ToArray()); } } @@ -145,14 +145,11 @@ namespace osu.Game.Overlays.Settings.Sections Schedule(() => skinDropdown.Items = skinDropdown.Items.Where(i => i.ID != item.ID).ToArray()); } - private List sortList(List skinsList) + private void sortUserSkins(List skinsList) { - // Sort user skins seperate from built-in skins - List userSkinsList = skinsList.GetRange(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex); - skinsList.RemoveRange(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex); - userSkinsList.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase)); - skinsList.AddRange(userSkinsList); - return skinsList; + // Sort user skins separately from built-in skins + skinsList.Sort(firstNonDefaultSkinIndex, skinsList.Count - firstNonDefaultSkinIndex, + Comparer.Create((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase))); } private class SkinSettingsDropdown : SettingsDropdown From 3b49b7461ef628031ba3440ea795cf5a42073a2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Jan 2021 18:46:21 +0100 Subject: [PATCH 6088/6909] Schedule entire operation for safety Also removes a redundant list copy. --- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 75c0324408..4cfd801caf 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -133,9 +133,12 @@ namespace osu.Game.Overlays.Settings.Sections { if (weakItem.NewValue.TryGetTarget(out var item)) { - List newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList(); - sortUserSkins(newDropdownItems); - Schedule(() => skinDropdown.Items = newDropdownItems.ToArray()); + Schedule(() => + { + List newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList(); + sortUserSkins(newDropdownItems); + skinDropdown.Items = newDropdownItems; + }); } } From 82e5a5bf6fbcc997e692e2af269a69f990343ddc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 19 Jan 2021 23:10:15 +0100 Subject: [PATCH 6089/6909] Mark legacy beatmap skin colour test as abstract --- osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs index fb3432fbae..051ede30b7 100644 --- a/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs +++ b/osu.Game/Tests/Beatmaps/LegacyBeatmapSkinColourTest.cs @@ -17,7 +17,7 @@ using osuTK.Graphics; namespace osu.Game.Tests.Beatmaps { - public class LegacyBeatmapSkinColourTest : ScreenTestScene + public abstract class LegacyBeatmapSkinColourTest : ScreenTestScene { protected readonly Bindable BeatmapSkins = new Bindable(); protected readonly Bindable BeatmapColours = new Bindable(); From 2ca3ccad0647c28f20b9aedaf0d560baf98b9de9 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Wed, 20 Jan 2021 04:56:46 +0300 Subject: [PATCH 6090/6909] Move all the content to BeatmapSetHeaderContent drawable --- .../Online/TestSceneBeatmapSetOverlay.cs | 6 +- .../Overlays/BeatmapSet/BeatmapSetHeader.cs | 278 +---------------- .../BeatmapSet/BeatmapSetHeaderContent.cs | 283 ++++++++++++++++++ osu.Game/Overlays/BeatmapSetOverlay.cs | 6 +- 4 files changed, 295 insertions(+), 278 deletions(-) create mode 100644 osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs index 7ff978c7ca..edc1696456 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapSetOverlay.cs @@ -231,8 +231,8 @@ namespace osu.Game.Tests.Visual.Online }); }); - AddAssert("shown beatmaps of current ruleset", () => overlay.Header.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value))); - AddAssert("left-most beatmap selected", () => overlay.Header.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected); + AddAssert("shown beatmaps of current ruleset", () => overlay.Header.HeaderContent.Picker.Difficulties.All(b => b.Beatmap.Ruleset.Equals(overlay.Header.RulesetSelector.Current.Value))); + AddAssert("left-most beatmap selected", () => overlay.Header.HeaderContent.Picker.Difficulties.First().State == BeatmapPicker.DifficultySelectorState.Selected); } [Test] @@ -310,7 +310,7 @@ namespace osu.Game.Tests.Visual.Online private void downloadAssert(bool shown) { - AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.DownloadButtonsVisible == shown); + AddAssert($"is download button {(shown ? "shown" : "hidden")}", () => overlay.Header.HeaderContent.DownloadButtonsVisible == shown); } private class TestBeatmapSetOverlay : BeatmapSetOverlay diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs index bc9008d1f5..4b26b02a8e 100644 --- a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeader.cs @@ -1,24 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Linq; using osu.Framework.Allocation; using osu.Framework.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.Game.Beatmaps; -using osu.Game.Beatmaps.Drawables; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; -using osu.Game.Graphics.UserInterface; -using osu.Game.Online; -using osu.Game.Online.API; -using osu.Game.Overlays.BeatmapListing.Panels; -using osu.Game.Overlays.BeatmapSet.Buttons; using osu.Game.Rulesets; using osuTK; using osuTK.Graphics; @@ -27,41 +15,16 @@ namespace osu.Game.Overlays.BeatmapSet { public class BeatmapSetHeader : OverlayHeader { - private const float transition_duration = 200; - private const float buttons_height = 45; - private const float buttons_spacing = 5; - public readonly Bindable BeatmapSet = new Bindable(); - public bool DownloadButtonsVisible => downloadButtonsContainer.Any(); - - public BeatmapPicker Picker { get; private set; } + public BeatmapSetHeaderContent HeaderContent { get; private set; } + [Cached] public BeatmapRulesetSelector RulesetSelector { get; private set; } - private IBindable state => downloadTracker.State; - [Cached(typeof(IBindable))] private readonly Bindable ruleset = new Bindable(); - [Resolved] - private IAPIProvider api { get; set; } - - private readonly DownloadTracker downloadTracker; - private OsuSpriteText title, artist; - private AuthorInfo author; - private ExplicitContentBeatmapPill explicitContentPill; - private FillFlowContainer downloadButtonsContainer; - private BeatmapAvailability beatmapAvailability; - private BeatmapSetOnlineStatusPill onlineStatusPill; - private ExternalLinkButton externalLink; - private UpdateableBeatmapSetCover cover; - private Box coverGradient; - private FillFlowContainer fadeContent; - private FavouriteButton favouriteButton; - private LoadingSpinner loading; - private Details details; - public BeatmapSetHeader() { Masking = true; @@ -73,249 +36,20 @@ namespace osu.Game.Overlays.BeatmapSet Radius = 3, Offset = new Vector2(0f, 1f), }; - - AddInternal(downloadTracker = new DownloadTracker - { - BeatmapSet = { BindTarget = BeatmapSet } - }); } - [BackgroundDependencyLoader] - private void load(OverlayColourProvider colourProvider) + protected override Drawable CreateContent() => HeaderContent = new BeatmapSetHeaderContent { - Picker.Beatmap.ValueChanged += b => - { - details.Beatmap = b.NewValue; - externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; - }; - - coverGradient.Colour = ColourInfo.GradientVertical(colourProvider.Background6.Opacity(0.3f), colourProvider.Background6.Opacity(0.8f)); - onlineStatusPill.BackgroundColour = colourProvider.Background6; - - state.BindValueChanged(_ => updateDownloadButtons()); - - BeatmapSet.BindValueChanged(setInfo => - { - Picker.BeatmapSet = RulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = details.BeatmapSet = setInfo.NewValue; - cover.BeatmapSet = setInfo.NewValue; - - if (setInfo.NewValue == null) - { - onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint); - fadeContent.Hide(); - - loading.Show(); - - downloadButtonsContainer.FadeOut(transition_duration); - favouriteButton.FadeOut(transition_duration); - } - else - { - fadeContent.FadeIn(500, Easing.OutQuint); - - loading.Hide(); - - title.Text = setInfo.NewValue.Metadata.Title ?? string.Empty; - artist.Text = setInfo.NewValue.Metadata.Artist ?? string.Empty; - - explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; - - onlineStatusPill.FadeIn(500, Easing.OutQuint); - onlineStatusPill.Status = setInfo.NewValue.OnlineInfo.Status; - - downloadButtonsContainer.FadeIn(transition_duration); - favouriteButton.FadeIn(transition_duration); - - updateDownloadButtons(); - } - }, true); - } - - protected override Drawable CreateContent() => new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] - { - cover = new UpdateableBeatmapSetCover - { - RelativeSizeAxes = Axes.Both, - Masking = true, - }, - coverGradient = new Box - { - RelativeSizeAxes = Axes.Both - }, - }, - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding - { - Vertical = BeatmapSetOverlay.Y_PADDING, - Left = BeatmapSetOverlay.X_PADDING, - Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH, - }, - Children = new Drawable[] - { - fadeContent = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = Picker = new BeatmapPicker(), - }, - new FillFlowContainer - { - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = 15 }, - Children = new Drawable[] - { - title = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true) - }, - externalLink = new ExternalLinkButton - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font - }, - explicitContentPill = new ExplicitContentBeatmapPill - { - Alpha = 0f, - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Margin = new MarginPadding { Left = 10, Bottom = 4 }, - } - } - }, - artist = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true), - Margin = new MarginPadding { Bottom = 20 } - }, - new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Child = author = new AuthorInfo(), - }, - beatmapAvailability = new BeatmapAvailability(), - new Container - { - RelativeSizeAxes = Axes.X, - Height = buttons_height, - Margin = new MarginPadding { Top = 10 }, - Children = new Drawable[] - { - favouriteButton = new FavouriteButton - { - BeatmapSet = { BindTarget = BeatmapSet } - }, - downloadButtonsContainer = new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = buttons_height + buttons_spacing }, - Spacing = new Vector2(buttons_spacing), - }, - }, - }, - }, - }, - } - }, - loading = new LoadingSpinner - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(1.5f), - }, - new FillFlowContainer - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - AutoSizeAxes = Axes.Both, - Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = BeatmapSetOverlay.X_PADDING }, - Direction = FillDirection.Vertical, - Spacing = new Vector2(10), - Children = new Drawable[] - { - onlineStatusPill = new BeatmapSetOnlineStatusPill - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - TextSize = 14, - TextPadding = new MarginPadding { Horizontal = 35, Vertical = 10 } - }, - details = new Details(), - }, - }, - } + BeatmapSet = { BindTarget = BeatmapSet } }; - private void updateDownloadButtons() - { - if (BeatmapSet.Value == null) return; - - if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && state.Value != DownloadState.LocallyAvailable) - { - downloadButtonsContainer.Clear(); - return; - } - - switch (state.Value) - { - case DownloadState.LocallyAvailable: - // temporary for UX until new design is implemented. - downloadButtonsContainer.Child = new BeatmapPanelDownloadButton(BeatmapSet.Value) - { - Width = 50, - RelativeSizeAxes = Axes.Y, - SelectedBeatmap = { BindTarget = Picker.Beatmap } - }; - break; - - case DownloadState.Downloading: - case DownloadState.Importing: - // temporary to avoid showing two buttons for maps with novideo. will be fixed in new beatmap overlay design. - downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value); - break; - - default: - downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value); - if (BeatmapSet.Value.OnlineInfo.HasVideo) - downloadButtonsContainer.Add(new HeaderDownloadButton(BeatmapSet.Value, true)); - break; - } - } - - private class DownloadTracker : BeatmapDownloadTrackingComposite - { - public new Bindable State => base.State; - } - - protected override OverlayTitle CreateTitle() => new BeatmapHeaderTitle(); - protected override Drawable CreateTitleContent() => RulesetSelector = new BeatmapRulesetSelector { Current = ruleset }; + protected override OverlayTitle CreateTitle() => new BeatmapHeaderTitle(); + private class BeatmapHeaderTitle : OverlayTitle { public BeatmapHeaderTitle() diff --git a/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs new file mode 100644 index 0000000000..153aa41582 --- /dev/null +++ b/osu.Game/Overlays/BeatmapSet/BeatmapSetHeaderContent.cs @@ -0,0 +1,283 @@ +// 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.Colour; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Shapes; +using osu.Game.Beatmaps.Drawables; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osu.Game.Graphics.UserInterface; +using osu.Game.Online; +using osu.Game.Online.API; +using osu.Game.Overlays.BeatmapListing.Panels; +using osu.Game.Overlays.BeatmapSet.Buttons; +using osuTK; + +namespace osu.Game.Overlays.BeatmapSet +{ + public class BeatmapSetHeaderContent : BeatmapDownloadTrackingComposite + { + private const float transition_duration = 200; + private const float buttons_height = 45; + private const float buttons_spacing = 5; + + public bool DownloadButtonsVisible => downloadButtonsContainer.Any(); + + public readonly Details Details; + public readonly BeatmapPicker Picker; + + private readonly UpdateableBeatmapSetCover cover; + private readonly Box coverGradient; + private readonly OsuSpriteText title, artist; + private readonly AuthorInfo author; + private readonly ExplicitContentBeatmapPill explicitContentPill; + private readonly FillFlowContainer downloadButtonsContainer; + private readonly BeatmapAvailability beatmapAvailability; + private readonly BeatmapSetOnlineStatusPill onlineStatusPill; + private readonly FavouriteButton favouriteButton; + private readonly FillFlowContainer fadeContent; + private readonly LoadingSpinner loading; + + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private BeatmapRulesetSelector rulesetSelector { get; set; } + + public BeatmapSetHeaderContent() + { + ExternalLinkButton externalLink; + + RelativeSizeAxes = Axes.X; + AutoSizeAxes = Axes.Y; + InternalChild = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + cover = new UpdateableBeatmapSetCover + { + RelativeSizeAxes = Axes.Both, + Masking = true, + }, + coverGradient = new Box + { + RelativeSizeAxes = Axes.Both + }, + }, + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Padding = new MarginPadding + { + Vertical = BeatmapSetOverlay.Y_PADDING, + Left = BeatmapSetOverlay.X_PADDING, + Right = BeatmapSetOverlay.X_PADDING + BeatmapSetOverlay.RIGHT_WIDTH, + }, + Children = new Drawable[] + { + fadeContent = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = Picker = new BeatmapPicker(), + }, + new FillFlowContainer + { + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = 15 }, + Children = new Drawable[] + { + title = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 30, weight: FontWeight.SemiBold, italics: true) + }, + externalLink = new ExternalLinkButton + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Left = 5, Bottom = 4 }, // To better lineup with the font + }, + explicitContentPill = new ExplicitContentBeatmapPill + { + Alpha = 0f, + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Margin = new MarginPadding { Left = 10, Bottom = 4 }, + } + } + }, + artist = new OsuSpriteText + { + Font = OsuFont.GetFont(size: 20, weight: FontWeight.Medium, italics: true), + Margin = new MarginPadding { Bottom = 20 } + }, + new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Child = author = new AuthorInfo(), + }, + beatmapAvailability = new BeatmapAvailability(), + new Container + { + RelativeSizeAxes = Axes.X, + Height = buttons_height, + Margin = new MarginPadding { Top = 10 }, + Children = new Drawable[] + { + favouriteButton = new FavouriteButton + { + BeatmapSet = { BindTarget = BeatmapSet } + }, + downloadButtonsContainer = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = buttons_height + buttons_spacing }, + Spacing = new Vector2(buttons_spacing), + }, + }, + }, + }, + }, + } + }, + loading = new LoadingSpinner + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(1.5f), + }, + new FillFlowContainer + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + AutoSizeAxes = Axes.Both, + Margin = new MarginPadding { Top = BeatmapSetOverlay.Y_PADDING, Right = BeatmapSetOverlay.X_PADDING }, + Direction = FillDirection.Vertical, + Spacing = new Vector2(10), + Children = new Drawable[] + { + onlineStatusPill = new BeatmapSetOnlineStatusPill + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + TextSize = 14, + TextPadding = new MarginPadding { Horizontal = 35, Vertical = 10 } + }, + Details = new Details(), + }, + }, + } + }; + + Picker.Beatmap.ValueChanged += b => + { + Details.Beatmap = b.NewValue; + externalLink.Link = $@"{api.WebsiteRootUrl}/beatmapsets/{BeatmapSet.Value?.OnlineBeatmapSetID}#{b.NewValue?.Ruleset.ShortName}/{b.NewValue?.OnlineBeatmapID}"; + }; + } + + [BackgroundDependencyLoader] + private void load(OverlayColourProvider colourProvider) + { + coverGradient.Colour = ColourInfo.GradientVertical(colourProvider.Background6.Opacity(0.3f), colourProvider.Background6.Opacity(0.8f)); + onlineStatusPill.BackgroundColour = colourProvider.Background6; + + State.BindValueChanged(_ => updateDownloadButtons()); + + BeatmapSet.BindValueChanged(setInfo => + { + Picker.BeatmapSet = rulesetSelector.BeatmapSet = author.BeatmapSet = beatmapAvailability.BeatmapSet = Details.BeatmapSet = setInfo.NewValue; + cover.BeatmapSet = setInfo.NewValue; + + if (setInfo.NewValue == null) + { + onlineStatusPill.FadeTo(0.5f, 500, Easing.OutQuint); + fadeContent.Hide(); + + loading.Show(); + + downloadButtonsContainer.FadeOut(transition_duration); + favouriteButton.FadeOut(transition_duration); + } + else + { + fadeContent.FadeIn(500, Easing.OutQuint); + + loading.Hide(); + + title.Text = setInfo.NewValue.Metadata.Title ?? string.Empty; + artist.Text = setInfo.NewValue.Metadata.Artist ?? string.Empty; + + explicitContentPill.Alpha = setInfo.NewValue.OnlineInfo.HasExplicitContent ? 1 : 0; + + onlineStatusPill.FadeIn(500, Easing.OutQuint); + onlineStatusPill.Status = setInfo.NewValue.OnlineInfo.Status; + + downloadButtonsContainer.FadeIn(transition_duration); + favouriteButton.FadeIn(transition_duration); + + updateDownloadButtons(); + } + }, true); + } + + private void updateDownloadButtons() + { + if (BeatmapSet.Value == null) return; + + if ((BeatmapSet.Value.OnlineInfo.Availability?.DownloadDisabled ?? false) && State.Value != DownloadState.LocallyAvailable) + { + downloadButtonsContainer.Clear(); + return; + } + + switch (State.Value) + { + case DownloadState.LocallyAvailable: + // temporary for UX until new design is implemented. + downloadButtonsContainer.Child = new BeatmapPanelDownloadButton(BeatmapSet.Value) + { + Width = 50, + RelativeSizeAxes = Axes.Y, + SelectedBeatmap = { BindTarget = Picker.Beatmap } + }; + break; + + case DownloadState.Downloading: + case DownloadState.Importing: + // temporary to avoid showing two buttons for maps with novideo. will be fixed in new beatmap overlay design. + downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value); + break; + + default: + downloadButtonsContainer.Child = new HeaderDownloadButton(BeatmapSet.Value); + if (BeatmapSet.Value.OnlineInfo.HasVideo) + downloadButtonsContainer.Add(new HeaderDownloadButton(BeatmapSet.Value, true)); + break; + } + } + } +} diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 86f0f4f614..c16ec339bb 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -76,7 +76,7 @@ namespace osu.Game.Overlays }, new ScoresContainer { - Beatmap = { BindTarget = Header.Picker.Beatmap } + Beatmap = { BindTarget = Header.HeaderContent.Picker.Beatmap } }, comments = new CommentsSection() }, @@ -88,7 +88,7 @@ namespace osu.Game.Overlays info.BeatmapSet.BindTo(beatmapSet); comments.BeatmapSet.BindTo(beatmapSet); - Header.Picker.Beatmap.ValueChanged += b => + Header.HeaderContent.Picker.Beatmap.ValueChanged += b => { info.Beatmap = b.NewValue; @@ -122,7 +122,7 @@ namespace osu.Game.Overlays req.Success += res => { beatmapSet.Value = res.ToBeatmapSet(rulesets); - Header.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineBeatmapID == beatmapId); + Header.HeaderContent.Picker.Beatmap.Value = Header.BeatmapSet.Value.Beatmaps.First(b => b.OnlineBeatmapID == beatmapId); }; API.Queue(req); From 58269f931491a2e2b6367a68b9c3a178415aa3c1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Jan 2021 13:35:43 +0900 Subject: [PATCH 6091/6909] Update with framework changes --- osu.Game/Skinning/PoolableSkinnableSample.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 2c83023fdc..0157af002e 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -129,7 +129,7 @@ namespace osu.Game.Skinning /// public bool Playing => activeChannel?.Playing ?? false; - public bool Played => activeChannel?.Played ?? false; + public bool Played => !activeChannel?.Playing ?? false; private bool looping; From bdb9d4f7d0ac8c0b20adffeb85823e60ee0d5ceb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Jan 2021 13:59:30 +0900 Subject: [PATCH 6092/6909] Restart sound on play --- osu.Game/Skinning/SkinnableSound.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index 06c694dc7a..b3db2d6558 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -124,7 +124,10 @@ namespace osu.Game.Skinning samplesContainer.ForEach(c => { if (PlayWhenZeroVolume || c.AggregateVolume.Value > 0) + { + c.Stop(); c.Play(); + } }); } From 8ffbcc9860e48e1940774540281511f32387f2e6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Jan 2021 14:05:35 +0900 Subject: [PATCH 6093/6909] Fix test failures and general discrepancies --- .../Editing/TestSceneEditorSamplePlayback.cs | 14 +++++++------- .../Gameplay/TestSceneGameplaySamplePlayback.cs | 6 +++--- .../Visual/Gameplay/TestSceneSkinnableSound.cs | 12 ++++++++++-- osu.Game/Graphics/UserInterface/OsuSliderBar.cs | 1 - osu.Game/Skinning/PoolableSkinnableSample.cs | 10 ++++++++-- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs index 876c1308b4..2abc8a8dec 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSamplePlayback.cs @@ -19,14 +19,14 @@ namespace osu.Game.Tests.Visual.Editing public void TestSlidingSampleStopsOnSeek() { DrawableSlider slider = null; - SkinnableSound[] loopingSamples = null; - SkinnableSound[] onceOffSamples = null; + PoolableSkinnableSample[] loopingSamples = null; + PoolableSkinnableSample[] onceOffSamples = null; AddStep("get first slider", () => { slider = Editor.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).First(); - onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray(); - loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray(); + onceOffSamples = slider.ChildrenOfType().Where(s => !s.Looping).ToArray(); + loopingSamples = slider.ChildrenOfType().Where(s => s.Looping).ToArray(); }); AddStep("start playback", () => EditorClock.Start()); @@ -36,15 +36,15 @@ namespace osu.Game.Tests.Visual.Editing if (!slider.Tracking.Value) return false; - if (!loopingSamples.Any(s => s.IsPlaying)) + if (!loopingSamples.Any(s => s.Playing)) return false; EditorClock.Seek(20000); return true; }); - AddAssert("non-looping samples are playing", () => onceOffSamples.Length == 4 && loopingSamples.All(s => s.IsPlayed || s.IsPlaying)); - AddAssert("looping samples are not playing", () => loopingSamples.Length == 1 && loopingSamples.All(s => s.IsPlayed && !s.IsPlaying)); + AddAssert("non-looping samples are playing", () => onceOffSamples.Length == 4 && loopingSamples.All(s => s.Played || s.Playing)); + AddAssert("looping samples are not playing", () => loopingSamples.Length == 1 && loopingSamples.All(s => s.Played && !s.Playing)); } } } diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs index b13acdcb95..6b3fc304e0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneGameplaySamplePlayback.cs @@ -19,14 +19,14 @@ namespace osu.Game.Tests.Visual.Gameplay public void TestAllSamplesStopDuringSeek() { DrawableSlider slider = null; - SkinnableSound[] samples = null; + PoolableSkinnableSample[] samples = null; ISamplePlaybackDisabler sampleDisabler = null; AddUntilStep("get variables", () => { sampleDisabler = Player; slider = Player.ChildrenOfType().OrderBy(s => s.HitObject.StartTime).FirstOrDefault(); - samples = slider?.ChildrenOfType().ToArray(); + samples = slider?.ChildrenOfType().ToArray(); return slider != null; }); @@ -36,7 +36,7 @@ namespace osu.Game.Tests.Visual.Gameplay if (!slider.Tracking.Value) return false; - if (!samples.Any(s => s.IsPlaying)) + if (!samples.Any(s => s.Playing)) return false; Player.ChildrenOfType().First().Seek(40000); diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs index 28c266f7d8..d688e9cb21 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSkinnableSound.cs @@ -43,7 +43,11 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestStoppedSoundDoesntResumeAfterPause() { - AddStep("start sample with looping", () => skinnableSound.Looping = true); + AddStep("start sample with looping", () => + { + skinnableSound.Looping = true; + skinnableSound.Play(); + }); AddUntilStep("wait for sample to start playing", () => skinnableSound.IsPlaying); @@ -62,7 +66,11 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestLoopingSoundResumesAfterPause() { - AddStep("start sample with looping", () => skinnableSound.Looping = true); + AddStep("start sample with looping", () => + { + skinnableSound.Looping = true; + skinnableSound.Play(); + }); AddUntilStep("wait for sample to start playing", () => skinnableSound.IsPlaying); diff --git a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs index bcf5220380..f58962f8e1 100644 --- a/osu.Game/Graphics/UserInterface/OsuSliderBar.cs +++ b/osu.Game/Graphics/UserInterface/OsuSliderBar.cs @@ -155,7 +155,6 @@ namespace osu.Game.Graphics.UserInterface return; lastSampleValue = value; - lastSampleTime = Clock.CurrentTime; var channel = sample.Play(); diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 0157af002e..cff793e8d4 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -117,19 +117,25 @@ namespace osu.Game.Skinning activeChannel = Sample.Play(); activeChannel.Looping = Looping; + + Played = true; } /// /// Stops the sample. /// - public void Stop() => activeChannel?.Stop(); + public void Stop() + { + activeChannel?.Stop(); + activeChannel = null; + } /// /// Whether the sample is currently playing. /// public bool Playing => activeChannel?.Playing ?? false; - public bool Played => !activeChannel?.Playing ?? false; + public bool Played { get; private set; } private bool looping; From 5261c0184919c79973c08a7bc43466adf4f0d3ba Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Jan 2021 19:43:42 +0900 Subject: [PATCH 6094/6909] Tie JoinRoom() and PartRoom() together --- .../Multiplayer/StatefulMultiplayerClient.cs | 154 +++++++++++------- 1 file changed, 92 insertions(+), 62 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index f0e11b2b8b..18a63171c4 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -4,7 +4,6 @@ #nullable enable using System; -using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -109,30 +108,54 @@ namespace osu.Game.Online.Multiplayer }); } + private readonly object joinOrLeaveTaskLock = new object(); + private Task? joinOrLeaveTask; + /// /// Joins the for a given API . /// /// The API . public async Task JoinRoom(Room room) { - if (Room != null) - throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + Task? lastTask; + Task newTask; - Debug.Assert(room.RoomID.Value != null); + lock (joinOrLeaveTaskLock) + { + lastTask = joinOrLeaveTask; + joinOrLeaveTask = newTask = Task.Run(async () => + { + if (lastTask != null) + await lastTask; - apiRoom = room; - playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; + // Should be thread-safe since joinOrLeaveTask is locked on in both JoinRoom() and LeaveRoom(). + if (Room != null) + throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); - Room = await JoinRoom(room.RoomID.Value.Value); + Debug.Assert(room.RoomID.Value != null); - Debug.Assert(Room != null); + // Join the server-side room. + var joinedRoom = await JoinRoom(room.RoomID.Value.Value); + Debug.Assert(joinedRoom != null); - var users = await getRoomUsers(); - Debug.Assert(users != null); + // Populate users. + Debug.Assert(joinedRoom.Users != null); + await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)); - await Task.WhenAll(users.Select(PopulateUser)); + // Update the stored room (must be done on update thread for thread-safety). + await scheduleAsync(() => + { + Room = joinedRoom; + apiRoom = room; + playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; + }); - updateLocalRoomSettings(Room.Settings); + // Update room settings. + await updateLocalRoomSettings(joinedRoom.Settings); + }); + } + + await newTask; } /// @@ -142,21 +165,35 @@ namespace osu.Game.Online.Multiplayer /// The joined . protected abstract Task JoinRoom(long roomId); - public virtual Task LeaveRoom() + public virtual async Task LeaveRoom() { - Scheduler.Add(() => + Task? lastTask; + Task newTask; + + lock (joinOrLeaveTaskLock) { - if (Room == null) - return; + lastTask = joinOrLeaveTask; + joinOrLeaveTask = newTask = Task.Run(async () => + { + if (lastTask != null) + await lastTask; - apiRoom = null; - Room = null; - CurrentMatchPlayingUserIds.Clear(); + // Should be thread-safe since joinOrLeaveTask is locked on in both JoinRoom() and LeaveRoom(). + if (Room == null) + return; - RoomUpdated?.Invoke(); - }, false); + await scheduleAsync(() => + { + apiRoom = null; + Room = null; + CurrentMatchPlayingUserIds.Clear(); - return Task.CompletedTask; + RoomUpdated?.Invoke(); + }); + }); + } + + await newTask; } /// @@ -432,27 +469,6 @@ namespace osu.Game.Online.Multiplayer /// The to populate. protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID); - /// - /// Retrieve a copy of users currently in the joined in a thread-safe manner. - /// This should be used whenever accessing users from outside of an Update thread context (ie. when not calling ). - /// - /// A copy of users in the current room, or null if unavailable. - private Task?> getRoomUsers() - { - var tcs = new TaskCompletionSource?>(); - - // at some point we probably want to replace all these schedule calls with Room.LockForUpdate. - // for now, as this would require quite some consideration due to the number of accesses to the room instance, - // let's just add a manual schedule for the non-scheduled usages instead. - Scheduler.Add(() => - { - var users = Room?.Users.ToList(); - tcs.SetResult(users); - }, false); - - return tcs.Task; - } - /// /// Updates the local room settings with the given . /// @@ -460,34 +476,28 @@ namespace osu.Game.Online.Multiplayer /// This updates both the joined and the respective API . /// /// The new to update from. - private void updateLocalRoomSettings(MultiplayerRoomSettings settings) + private Task updateLocalRoomSettings(MultiplayerRoomSettings settings) => scheduleAsync(() => { if (Room == null) return; - Scheduler.Add(() => - { - if (Room == null) - return; + Debug.Assert(apiRoom != null); - Debug.Assert(apiRoom != null); + // Update a few properties of the room instantaneously. + Room.Settings = settings; + apiRoom.Name.Value = Room.Settings.Name; - // Update a few properties of the room instantaneously. - Room.Settings = settings; - apiRoom.Name.Value = Room.Settings.Name; + // The playlist update is delayed until an online beatmap lookup (below) succeeds. + // In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here. + apiRoom.Playlist.Clear(); - // The playlist update is delayed until an online beatmap lookup (below) succeeds. - // In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here. - apiRoom.Playlist.Clear(); + RoomUpdated?.Invoke(); - RoomUpdated?.Invoke(); + var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); + req.Success += res => updatePlaylist(settings, res); - var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => updatePlaylist(settings, res); - - api.Queue(req); - }, false); - } + api.Queue(req); + }); private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) { @@ -534,5 +544,25 @@ namespace osu.Game.Online.Multiplayer else CurrentMatchPlayingUserIds.Remove(userId); } + + private Task scheduleAsync(Action action) + { + var tcs = new TaskCompletionSource(); + + Scheduler.Add(() => + { + try + { + action(); + tcs.SetResult(true); + } + catch (Exception ex) + { + tcs.SetException(ex); + } + }); + + return tcs.Task; + } } } From 5ff76be052a73b95e4aa2b562dfb88055939018b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Jan 2021 19:43:51 +0900 Subject: [PATCH 6095/6909] Fix potential test failures due to timing --- .../Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index 7a3845cbf3..6de5704410 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); }); - AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null); + AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null); } [Test] @@ -133,7 +133,7 @@ namespace osu.Game.Tests.Visual.Multiplayer }); }); - AddAssert("multiplayer room joined", () => roomContainer.Client.Room != null); + AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null); } private TestMultiplayerRoomManager createRoomManager() From e005a1cc9fc72413fe5a6c4592eb867cde860c14 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Jan 2021 20:16:34 +0900 Subject: [PATCH 6096/6909] Remove unnecessary condition blocking the part --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 50dc8f661c..5d18521eac 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -143,9 +143,6 @@ namespace osu.Game.Online.Multiplayer return; } - if (Room == null) - return; - await base.LeaveRoom(); await connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); } From 6b139d4cf312cb75970a1555dab546d65f8ba898 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Jan 2021 20:21:07 +0900 Subject: [PATCH 6097/6909] Reset task post-execution --- .../Multiplayer/StatefulMultiplayerClient.cs | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 18a63171c4..80162eae59 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -155,7 +155,19 @@ namespace osu.Game.Online.Multiplayer }); } - await newTask; + try + { + await newTask; + } + finally + { + // The task will be awaited in the future, so reset it so that the user doesn't get into a permanently faulted state if anything fails. + lock (joinOrLeaveTask) + { + if (joinOrLeaveTask == newTask) + joinOrLeaveTask = null; + } + } } /// @@ -193,7 +205,19 @@ namespace osu.Game.Online.Multiplayer }); } - await newTask; + try + { + await newTask; + } + finally + { + // The task will be awaited in the future, so reset it so that the user doesn't get into a permanently faulted state if anything fails. + lock (joinOrLeaveTask) + { + if (joinOrLeaveTask == newTask) + joinOrLeaveTask = null; + } + } } /// From eb85efcea2d79eb8b1750b9d703692e6fb305280 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Jan 2021 20:59:28 +0900 Subject: [PATCH 6098/6909] Add check to playlists too --- osu.Game/Screens/Select/MatchSongSelect.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 0948a4d19a..ed47b5d5ac 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -10,6 +10,8 @@ using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Components; @@ -78,5 +80,9 @@ namespace osu.Game.Screens.Select item.RequiredMods.Clear(); item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); } + + protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(isValidMod); + + private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; } } From bba182a02d10f283d85a876c89530c55247359b6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 20 Jan 2021 21:27:16 +0900 Subject: [PATCH 6099/6909] Fix test failure --- .../Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs index a4c87d3ace..319c2bc6fd 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsRoomSubScreen.cs @@ -11,12 +11,10 @@ using osu.Framework.Platform; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Beatmaps; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Rulesets; using osu.Game.Rulesets.Osu; using osu.Game.Screens.OnlinePlay; -using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Playlists; using osu.Game.Tests.Beatmaps; using osu.Game.Users; @@ -85,8 +83,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("move mouse to create button", () => { - var footer = match.ChildrenOfType
    ().Single(); - InputManager.MoveMouseTo(footer.ChildrenOfType().Single()); + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); }); AddStep("click", () => InputManager.Click(MouseButton.Left)); From ce3c2f07dc8f36d6cfea1af94943cc74093833d7 Mon Sep 17 00:00:00 2001 From: vmaggioli Date: Tue, 19 Jan 2021 20:13:21 -0500 Subject: [PATCH 6100/6909] Fix zero length spinners and sliders --- .../TestSceneSliderSelectionBlueprint.cs | 21 +++++++++++++++++++ .../Sliders/SliderSelectionBlueprint.cs | 5 ++++- .../Timeline/TimelineHitObjectBlueprint.cs | 2 +- 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index f6e1be693b..55b3707c38 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -161,6 +161,27 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor checkControlPointSelected(1, false); } + [Test] + public void TestZeroLengthSliderNotAllowed() + { + moveMouseToControlPoint(1); + AddStep("drag control point 1 to control point 0", () => + { + InputManager.PressButton(MouseButton.Left); + moveMouseToControlPoint(0); + InputManager.ReleaseButton(MouseButton.Left); + }); + moveMouseToControlPoint(2); + AddStep("drag control point 2 to control point 0", () => + { + InputManager.PressButton(MouseButton.Left); + moveMouseToControlPoint(0); + InputManager.ReleaseButton(MouseButton.Left); + }); + checkPositions(); + + } + private void moveHitObject() { AddStep("move hitobject", () => diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 3d3dff653a..99edcd2149 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -226,7 +226,10 @@ 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; + float expectedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; + if (expectedDistance < 1) + return; + HitObject.Path.ExpectedDistance.Value = expectedDistance; editorBeatmap?.Update(HitObject); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index ae2a82fa10..1dc37510ad 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -387,7 +387,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline case IHasDuration endTimeHitObject: var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); - if (endTimeHitObject.EndTime == snappedTime) + if (endTimeHitObject.EndTime == snappedTime || (snappedTime - hitObject.StartTime) < 1) return; endTimeHitObject.Duration = snappedTime - hitObject.StartTime; From d42773ebb2b731e93b03feab519fa951cbc60421 Mon Sep 17 00:00:00 2001 From: vmaggioli Date: Wed, 20 Jan 2021 12:36:31 -0500 Subject: [PATCH 6101/6909] Fix preceeding space --- .../Editor/TestSceneSliderSelectionBlueprint.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index 55b3707c38..ce1c13dac5 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -179,7 +179,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor InputManager.ReleaseButton(MouseButton.Left); }); checkPositions(); - } private void moveHitObject() From 5ee3a5f230435a166c85fdb8d542f4dc34be1f91 Mon Sep 17 00:00:00 2001 From: vmaggioli Date: Wed, 20 Jan 2021 13:00:25 -0500 Subject: [PATCH 6102/6909] Use AlmostEquals --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 1dc37510ad..301543b3c1 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Primitives; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -387,7 +388,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline case IHasDuration endTimeHitObject: var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); - if (endTimeHitObject.EndTime == snappedTime || (snappedTime - hitObject.StartTime) < 1) + if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, 1)) return; endTimeHitObject.Duration = snappedTime - hitObject.StartTime; From 7abe33ad0e2df62efe586788e8963b0fe10f869d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 20 Jan 2021 23:23:12 +0100 Subject: [PATCH 6103/6909] Add failing test case --- .../Gameplay/TestScenePoolingRuleset.cs | 75 +++++++++++++++++-- 1 file changed, 69 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs index cd7d692b0a..17a009a2ce 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePoolingRuleset.cs @@ -17,10 +17,12 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Judgements; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Objects.Legacy; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.UI; using osuTK; using osuTK.Graphics; @@ -129,6 +131,31 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("no DHOs shown", () => !this.ChildrenOfType().Any()); } + [Test] + public void TestApplyHitResultOnKilled() + { + ManualClock clock = null; + bool anyJudged = false; + + void onNewResult(JudgementResult _) => anyJudged = true; + + var beatmap = new Beatmap(); + beatmap.HitObjects.Add(new TestKilledHitObject { Duration = 20 }); + + createTest(beatmap, 10, () => new FramedClock(clock = new ManualClock())); + + AddStep("subscribe to new result", () => + { + anyJudged = false; + drawableRuleset.NewResult += onNewResult; + }); + AddStep("skip past object", () => clock.CurrentTime = beatmap.HitObjects[0].GetEndTime() + 1000); + + AddAssert("object judged", () => anyJudged); + + AddStep("clean up", () => drawableRuleset.NewResult -= onNewResult); + } + private void createTest(IBeatmap beatmap, int poolSize, Func createClock = null) => AddStep("create test", () => { var ruleset = new TestPoolingRuleset(); @@ -192,6 +219,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void load() { RegisterPool(poolSize); + RegisterPool(poolSize); } protected override HitObjectLifetimeEntry CreateLifetimeEntry(HitObject hitObject) => new TestHitObjectLifetimeEntry(hitObject); @@ -220,19 +248,30 @@ namespace osu.Game.Tests.Visual.Gameplay protected override IEnumerable ConvertHitObject(HitObject original, IBeatmap beatmap, CancellationToken cancellationToken) { - yield return new TestHitObject + switch (original) { - StartTime = original.StartTime, - Duration = 250 - }; + case TestKilledHitObject h: + yield return h; + + break; + + default: + yield return new TestHitObject + { + StartTime = original.StartTime, + Duration = 250 + }; + + break; + } } } #endregion - #region HitObject + #region HitObjects - private class TestHitObject : ConvertHitObject + private class TestHitObject : ConvertHitObject, IHasDuration { public double EndTime => StartTime + Duration; @@ -287,6 +326,30 @@ namespace osu.Game.Tests.Visual.Gameplay } } + private class TestKilledHitObject : TestHitObject + { + } + + private class DrawableTestKilledHitObject : DrawableHitObject + { + public DrawableTestKilledHitObject() + : base(null) + { + } + + protected override void UpdateHitStateTransforms(ArmedState state) + { + base.UpdateHitStateTransforms(state); + Expire(); + } + + public override void OnKilled() + { + base.OnKilled(); + ApplyResult(r => r.Type = r.Judgement.MinResult); + } + } + #endregion } } From 1d9aaac2c221a4e507df4019076f080e74f1a0b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 20 Jan 2021 23:55:54 +0100 Subject: [PATCH 6104/6909] Fix HOC not propagating DHO results applied on kill `DrawableHitObject.OnKilled()` calls `UpdateResult()` to clean up a hitobject's state definitively with regards to the judgement result before returning the DHO back to the pool. As it turns out, if a consumer was relying on this code path (as taiko was in the case of nested strong hit objects), it would not work properly with pooling, due to `HitObjectContainer` unsubscribing from `On{New,Revert}Result` *before* calling the DHO's `OnKilled()`. This in turn would lead to users potentially getting stuck in gameplay, due to `ScoreProcessor` not receiving all results via that event path. To resolve, change the call ordering to allow hit result changes applied in `OnKilled()` to propagate normally. --- osu.Game/Rulesets/UI/HitObjectContainer.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 12e39d4fbf..1972043ccb 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -124,9 +124,11 @@ namespace osu.Game.Rulesets.UI Debug.Assert(drawableMap.ContainsKey(entry)); var drawable = drawableMap[entry]; + + // OnKilled can potentially change the hitobject's result, so it needs to run first before unbinding. + drawable.OnKilled(); drawable.OnNewResult -= onNewResult; drawable.OnRevertResult -= onRevertResult; - drawable.OnKilled(); drawableMap.Remove(entry); From 76e1f6e57bbd27297a320b64cbb27dcbdad6c85a Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 21 Jan 2021 12:45:44 +0900 Subject: [PATCH 6105/6909] Fix locking on incorrect object MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 80162eae59..f2b5a44fcf 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -162,7 +162,7 @@ namespace osu.Game.Online.Multiplayer finally { // The task will be awaited in the future, so reset it so that the user doesn't get into a permanently faulted state if anything fails. - lock (joinOrLeaveTask) + lock (joinOrLeaveTaskLock) { if (joinOrLeaveTask == newTask) joinOrLeaveTask = null; @@ -212,7 +212,7 @@ namespace osu.Game.Online.Multiplayer finally { // The task will be awaited in the future, so reset it so that the user doesn't get into a permanently faulted state if anything fails. - lock (joinOrLeaveTask) + lock (joinOrLeaveTaskLock) { if (joinOrLeaveTask == newTask) joinOrLeaveTask = null; From 163a937e415b21b4f92d84675aae2a27f57e70c9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 Jan 2021 13:30:53 +0900 Subject: [PATCH 6106/6909] Fix mod test failing intermittently --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 0d0acbb8f4..bb72226750 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -134,6 +134,8 @@ namespace osu.Game.Tests.Visual.UserInterface [Test] public void TestExternallySetCustomizedMod() { + changeRuleset(0); + AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }); AddAssert("ensure button is selected and customized accordingly", () => From 65ece1aa722b74e6590b4b3d208a10141238164d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 21 Jan 2021 07:50:41 +0300 Subject: [PATCH 6107/6909] Mark OverlayHeader as NotNull in FullscreenOverlay --- osu.Game/Overlays/FullscreenOverlay.cs | 8 ++++--- osu.Game/Overlays/OnlineOverlay.cs | 32 ++++++++++++-------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index d65213f573..d0a0c994aa 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -16,9 +17,9 @@ namespace osu.Game.Overlays public abstract class FullscreenOverlay : WaveOverlayContainer, INamedOverlayComponent where T : OverlayHeader { - public virtual string IconTexture => Header?.Title.IconTexture ?? string.Empty; - public virtual string Title => Header?.Title.Title ?? string.Empty; - public virtual string Description => Header?.Title.Description ?? string.Empty; + public virtual string IconTexture => Header.Title.IconTexture ?? string.Empty; + public virtual string Title => Header.Title.Title ?? string.Empty; + public virtual string Description => Header.Title.Description ?? string.Empty; public T Header { get; } @@ -76,6 +77,7 @@ namespace osu.Game.Overlays Waves.FourthWaveColour = ColourProvider.Dark3; } + [NotNull] protected abstract T CreateHeader(); protected virtual Color4 GetBackgroundColour() => ColourProvider.Background5; diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index b44ccc32c9..4a7318d065 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -19,29 +19,27 @@ namespace osu.Game.Overlays protected OnlineOverlay(OverlayColourScheme colourScheme) : base(colourScheme) { - FillFlowContainer flow = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical - }; - - if (Header != null) - flow.Add(Header.With(h => h.Depth = -float.MaxValue)); - - flow.Add(content = new Container - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y - }); - base.Content.AddRange(new Drawable[] { ScrollFlow = new OverlayScrollContainer { RelativeSizeAxes = Axes.Both, ScrollbarVisible = false, - Child = flow + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Y, + RelativeSizeAxes = Axes.X, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + Header.With(h => h.Depth = -float.MaxValue), + content = new Container + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y + } + } + } }, Loading = new LoadingLayer(true) }); From 54dbb43f79a758e73e4782c1364e2405384a82c4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 21 Jan 2021 14:03:35 +0900 Subject: [PATCH 6108/6909] Fix more potential failures --- osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index bb72226750..bd4010a7f3 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -40,6 +40,7 @@ namespace osu.Game.Tests.Visual.UserInterface [SetUp] public void SetUp() => Schedule(() => { + SelectedMods.Value = Array.Empty(); Children = new Drawable[] { modSelect = new TestModSelectOverlay From 0fcf61d352197ef5f4d8e10d8bf1d19d55c266b0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 14:07:02 +0900 Subject: [PATCH 6109/6909] Replace null check with assert --- osu.Game/Graphics/Containers/SectionsContainer.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index b5f81c516a..d378b67acf 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.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 JetBrains.Annotations; using osu.Framework.Allocation; @@ -131,13 +132,13 @@ namespace osu.Game.Graphics.Containers public override void Add(T drawable) { base.Add(drawable); + + Debug.Assert(drawable != null); + lastKnownScroll = float.NaN; headerHeight = float.NaN; footerHeight = float.NaN; - if (drawable == null) - return; - if (smallestSection == null || smallestSection.Height > drawable.Height) smallestSection = drawable; } From 8f9089d1aefa26c90a883e226dcce91b9cabfd8e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 14:30:22 +0900 Subject: [PATCH 6110/6909] Move constant to a better place --- osu.Game/Graphics/Containers/SectionsContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index d378b67acf..fd8b98e767 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -109,6 +109,8 @@ namespace osu.Game.Graphics.Containers private float lastKnownScroll; + private const float scroll_target_multiplier = 0.2f; + public SectionsContainer() { AddRangeInternal(new Drawable[] @@ -143,8 +145,6 @@ namespace osu.Game.Graphics.Containers smallestSection = drawable; } - private const float scroll_target_multiplier = 0.2f; - public void ScrollTo(Drawable section) { lastClickedSection = section; From 555abcdc3695c437efe3277fc738e54b637fe235 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 14:31:31 +0900 Subject: [PATCH 6111/6909] Replace nan usage with nullable float --- .../Graphics/Containers/SectionsContainer.cs | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index fd8b98e767..2140e251f8 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -40,7 +40,7 @@ namespace osu.Game.Graphics.Containers if (value == null) return; AddInternal(expandableHeader); - lastKnownScroll = float.NaN; + lastKnownScroll = null; } } @@ -56,7 +56,7 @@ namespace osu.Game.Graphics.Containers if (value == null) return; AddInternal(fixedHeader); - lastKnownScroll = float.NaN; + lastKnownScroll = null; } } @@ -75,7 +75,7 @@ namespace osu.Game.Graphics.Containers footer.Anchor |= Anchor.y2; footer.Origin |= Anchor.y2; scrollContainer.Add(footer); - lastKnownScroll = float.NaN; + lastKnownScroll = null; } } @@ -93,7 +93,7 @@ namespace osu.Game.Graphics.Containers headerBackgroundContainer.Add(headerBackground); - lastKnownScroll = float.NaN; + lastKnownScroll = null; } } @@ -105,9 +105,9 @@ namespace osu.Game.Graphics.Containers private Drawable expandableHeader, fixedHeader, footer, headerBackground; private FlowContainer scrollContentContainer; - private float headerHeight, footerHeight; + private float? headerHeight, footerHeight; - private float lastKnownScroll; + private float? lastKnownScroll; private const float scroll_target_multiplier = 0.2f; @@ -137,9 +137,9 @@ namespace osu.Game.Graphics.Containers Debug.Assert(drawable != null); - lastKnownScroll = float.NaN; - headerHeight = float.NaN; - footerHeight = float.NaN; + lastKnownScroll = null; + headerHeight = null; + footerHeight = null; if (smallestSection == null || smallestSection.Height > drawable.Height) smallestSection = drawable; @@ -171,7 +171,7 @@ namespace osu.Game.Graphics.Containers if (source == InvalidationSource.Child && (invalidation & Invalidation.DrawSize) != 0) { - lastKnownScroll = -1; + lastKnownScroll = null; result = true; } @@ -242,8 +242,9 @@ namespace osu.Game.Graphics.Containers if (!Children.Any()) return; var newMargin = originalSectionsMargin; - newMargin.Top += headerHeight; - newMargin.Bottom += footerHeight; + + newMargin.Top += (headerHeight ?? 0); + newMargin.Bottom += (footerHeight ?? 0); scrollContentContainer.Margin = newMargin; } From 6d167b7865688b4f73cb94106fbbe233e739ce40 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 14:40:55 +0900 Subject: [PATCH 6112/6909] Remove the need to store the smallest section --- osu.Game/Graphics/Containers/SectionsContainer.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 2140e251f8..6607193cc6 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -23,7 +23,6 @@ namespace osu.Game.Graphics.Containers { public Bindable SelectedSection { get; } = new Bindable(); private Drawable lastClickedSection; - private T smallestSection; public Drawable ExpandableHeader { @@ -140,9 +139,6 @@ namespace osu.Game.Graphics.Containers lastKnownScroll = null; headerHeight = null; footerHeight = null; - - if (smallestSection == null || smallestSection.Height > drawable.Height) - smallestSection = drawable; } public void ScrollTo(Drawable section) @@ -213,10 +209,12 @@ namespace osu.Game.Graphics.Containers headerBackgroundContainer.Height = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0; + var smallestSectionHeight = Children.Count > 0 ? Children.Min(d => d.Height) : 0; + // scroll offset is our fixed header height if we have it plus 20% of content height // plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards // but the 5% can't be bigger than our smallest section height, otherwise it won't get selected correctly - float sectionOrContent = Math.Min(smallestSection?.Height / 2.0f ?? 0, scrollContainer.DisplayableContent * 0.05f); + float sectionOrContent = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f); float scrollOffset = (FixedHeader?.LayoutSize.Y ?? 0) + scrollContainer.DisplayableContent * scroll_target_multiplier + sectionOrContent; Func diff = section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset; From e5eec27e9574037e13287b11438c044d8cc1c0da Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 14:44:47 +0900 Subject: [PATCH 6113/6909] Simplify selected section resolution --- osu.Game/Graphics/Containers/SectionsContainer.cs | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 6607193cc6..cae2dd7e37 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -215,23 +215,16 @@ namespace osu.Game.Graphics.Containers // plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards // but the 5% can't be bigger than our smallest section height, otherwise it won't get selected correctly float sectionOrContent = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f); + float scrollOffset = (FixedHeader?.LayoutSize.Y ?? 0) + scrollContainer.DisplayableContent * scroll_target_multiplier + sectionOrContent; Func diff = section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset; if (Precision.AlmostBigger(0, scrollContainer.Current)) - { SelectedSection.Value = lastClickedSection as T ?? Children.FirstOrDefault(); - return; - } - - if (Precision.AlmostBigger(scrollContainer.Current, scrollContainer.ScrollableExtent)) - { + else if (Precision.AlmostBigger(scrollContainer.Current, scrollContainer.ScrollableExtent)) SelectedSection.Value = lastClickedSection as T ?? Children.LastOrDefault(); - return; - } - - SelectedSection.Value = Children.TakeWhile(section => diff(section) <= 0).LastOrDefault() - ?? Children.FirstOrDefault(); + else + SelectedSection.Value = Children.TakeWhile(section => diff(section) <= 0).LastOrDefault() ?? Children.FirstOrDefault(); } } From a85f952a381ca32869e06a16a23c149955a88dbc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 14:46:35 +0900 Subject: [PATCH 6114/6909] Inline single use function --- osu.Game/Graphics/Containers/SectionsContainer.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index cae2dd7e37..cf361abadc 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -217,14 +217,17 @@ namespace osu.Game.Graphics.Containers float sectionOrContent = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f); float scrollOffset = (FixedHeader?.LayoutSize.Y ?? 0) + scrollContainer.DisplayableContent * scroll_target_multiplier + sectionOrContent; - Func diff = section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset; if (Precision.AlmostBigger(0, scrollContainer.Current)) SelectedSection.Value = lastClickedSection as T ?? Children.FirstOrDefault(); else if (Precision.AlmostBigger(scrollContainer.Current, scrollContainer.ScrollableExtent)) SelectedSection.Value = lastClickedSection as T ?? Children.LastOrDefault(); else - SelectedSection.Value = Children.TakeWhile(section => diff(section) <= 0).LastOrDefault() ?? Children.FirstOrDefault(); + { + SelectedSection.Value = Children + .TakeWhile(section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset <= 0) + .LastOrDefault() ?? Children.FirstOrDefault(); + } } } From 9daf29fedcb0b4e74a7a3d17579557c68fd4723a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 14:52:41 +0900 Subject: [PATCH 6115/6909] Extract out commonly used variables --- 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 cf361abadc..5afe74db18 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -178,7 +178,10 @@ namespace osu.Game.Graphics.Containers { base.UpdateAfterChildren(); - float headerH = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); + float fixedHeaderSize = (FixedHeader?.LayoutSize.Y ?? 0); + float expandableHeaderSize = ExpandableHeader?.LayoutSize.Y ?? 0; + + float headerH = expandableHeaderSize + fixedHeaderSize; float footerH = Footer?.LayoutSize.Y ?? 0; if (headerH != headerHeight || footerH != footerHeight) @@ -200,13 +203,13 @@ namespace osu.Game.Graphics.Containers if (ExpandableHeader != null && FixedHeader != null) { - float offset = Math.Min(ExpandableHeader.LayoutSize.Y, currentScroll); + float offset = Math.Min(expandableHeaderSize, currentScroll); ExpandableHeader.Y = -offset; - FixedHeader.Y = -offset + ExpandableHeader.LayoutSize.Y; + FixedHeader.Y = -offset + expandableHeaderSize; } - headerBackgroundContainer.Height = (ExpandableHeader?.LayoutSize.Y ?? 0) + (FixedHeader?.LayoutSize.Y ?? 0); + headerBackgroundContainer.Height = expandableHeaderSize + fixedHeaderSize; headerBackgroundContainer.Y = ExpandableHeader?.Y ?? 0; var smallestSectionHeight = Children.Count > 0 ? Children.Min(d => d.Height) : 0; @@ -216,7 +219,7 @@ namespace osu.Game.Graphics.Containers // but the 5% can't be bigger than our smallest section height, otherwise it won't get selected correctly float sectionOrContent = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f); - float scrollOffset = (FixedHeader?.LayoutSize.Y ?? 0) + scrollContainer.DisplayableContent * scroll_target_multiplier + sectionOrContent; + float scrollOffset = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_target_multiplier + sectionOrContent; if (Precision.AlmostBigger(0, scrollContainer.Current)) SelectedSection.Value = lastClickedSection as T ?? Children.FirstOrDefault(); From c650cbd2a70f1f333d2fc3b159a1ca10ace61fca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 14:56:10 +0900 Subject: [PATCH 6116/6909] Rename variable to something slightly better --- osu.Game/Graphics/Containers/SectionsContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 5afe74db18..87c3007ca4 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -217,9 +217,9 @@ namespace osu.Game.Graphics.Containers // scroll offset is our fixed header height if we have it plus 20% of content height // plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards // but the 5% can't be bigger than our smallest section height, otherwise it won't get selected correctly - float sectionOrContent = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f); + float scrollIntoSectionAmount = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f); - float scrollOffset = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_target_multiplier + sectionOrContent; + float scrollOffset = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_target_multiplier + scrollIntoSectionAmount; if (Precision.AlmostBigger(0, scrollContainer.Current)) SelectedSection.Value = lastClickedSection as T ?? Children.FirstOrDefault(); From 8853ac04d97e2419da6b4f2db2a464d680173d3b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 15:08:36 +0900 Subject: [PATCH 6117/6909] Rename some variable and add xmldoc for scroll centre position --- 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 87c3007ca4..6ed161fa77 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -108,7 +108,10 @@ namespace osu.Game.Graphics.Containers private float? lastKnownScroll; - private const float scroll_target_multiplier = 0.2f; + /// + /// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section). + /// + private const float scroll_y_centre = 0.2f; public SectionsContainer() { @@ -144,7 +147,7 @@ namespace osu.Game.Graphics.Containers public void ScrollTo(Drawable section) { lastClickedSection = section; - scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - scrollContainer.DisplayableContent * scroll_target_multiplier - (FixedHeader?.BoundingBox.Height ?? 0)); + scrollContainer.ScrollTo(scrollContainer.GetChildPosInContent(section) - scrollContainer.DisplayableContent * scroll_y_centre - (FixedHeader?.BoundingBox.Height ?? 0)); } public void ScrollToTop() => scrollContainer.ScrollTo(0); @@ -217,9 +220,9 @@ namespace osu.Game.Graphics.Containers // scroll offset is our fixed header height if we have it plus 20% of content height // plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards // but the 5% can't be bigger than our smallest section height, otherwise it won't get selected correctly - float scrollIntoSectionAmount = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f); + float selectionLenienceAboveSection = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f); - float scrollOffset = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_target_multiplier + scrollIntoSectionAmount; + float scrollCentre = fixedHeaderSize + scrollContainer.DisplayableContent * scroll_y_centre + selectionLenienceAboveSection; if (Precision.AlmostBigger(0, scrollContainer.Current)) SelectedSection.Value = lastClickedSection as T ?? Children.FirstOrDefault(); @@ -228,7 +231,7 @@ namespace osu.Game.Graphics.Containers else { SelectedSection.Value = Children - .TakeWhile(section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollOffset <= 0) + .TakeWhile(section => scrollContainer.GetChildPosInContent(section) - currentScroll - scrollCentre <= 0) .LastOrDefault() ?? Children.FirstOrDefault(); } } From e6980688f60fd9d6b9e6489f2879eb397a50218f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 21 Jan 2021 15:42:23 +0900 Subject: [PATCH 6118/6909] Leave the multiplayer channel when leaving multiplayer --- osu.Game/Online/Chat/ChannelManager.cs | 10 +++++++--- .../OnlinePlay/Match/Components/MatchChatDisplay.cs | 6 ++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 62ae507419..036ec4d0f3 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -339,7 +339,7 @@ namespace osu.Game.Online.Chat } /// - /// Joins a channel if it has not already been joined. + /// Joins a channel if it has not already been joined. Must be called from the update thread. /// /// The channel to join. /// The joined channel. Note that this may not match the parameter channel as it is a backed object. @@ -399,7 +399,11 @@ namespace osu.Game.Online.Chat return channel; } - public void LeaveChannel(Channel channel) + /// + /// Leave the specified channel. Can be called from any thread. + /// + /// The channel to leave. + public void LeaveChannel(Channel channel) => Schedule(() => { if (channel == null) return; @@ -413,7 +417,7 @@ namespace osu.Game.Online.Chat api.Queue(new LeaveChannelRequest(channel)); channel.Joined.Value = false; } - } + }); private long lastMessageId; diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs index 8800215c2e..6da2866236 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs @@ -38,5 +38,11 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components Channel.Value = channelManager?.JoinChannel(new Channel { Id = channelId.Value, Type = ChannelType.Multiplayer, Name = $"#lazermp_{roomId.Value}" }); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + channelManager?.LeaveChannel(Channel.Value); + } } } From 9eb74e86edacd10d0f8fa8facf4f7f0045c4ca5c Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Thu, 21 Jan 2021 17:40:15 +0900 Subject: [PATCH 6119/6909] Apply comment suggestion 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/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 7df4f1ae7d..e215ecc17a 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/Timeline.cs @@ -151,7 +151,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline // 2) The user changes the track time through some other means (scrolling in the editor or overview timeline; clicking a hitobject etc.). We want the timeline to track the clock's time. // 3) An ongoing seek transform is running from an external seek. We want the timeline to track the clock's 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 + // The simplest way to cover the first two cases is by checking whether the scroll position has changed and the audio hasn't been changed externally // Checking IsSeeking covers the third case, where the transform may not have been applied yet. if (Current != lastScrollPosition && editorClock.CurrentTime == lastTrackTime && !editorClock.IsSeeking) seekTrackToCurrent(); From 153149554bdb5c7ba44b57d6bc75acda7cfc1763 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Thu, 21 Jan 2021 16:25:16 +0100 Subject: [PATCH 6120/6909] add more mime types --- osu.Android/OsuGameActivity.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index 788e5f82be..48b059b482 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -20,7 +20,8 @@ namespace osu.Android [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] - [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream" })] + [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeTypes = new[] { "application/x-osu-beatmap", "application/x-osu-skin" })] + [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/x-osu-beatmap", "application/x-osu-skin", "application/zip", "application/octet-stream", "application/x-zip", "application/x-zip-compressed" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { From e4b59c7317b7788f03b184ccc900aee717153658 Mon Sep 17 00:00:00 2001 From: vmaggioli Date: Thu, 21 Jan 2021 11:54:26 -0500 Subject: [PATCH 6121/6909] Test setup --- .../TestSceneTimelineHitObjectBlueprint.cs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs new file mode 100644 index 0000000000..ed427c2020 --- /dev/null +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +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.Screens.Edit.Compose.Components.Timeline; +using osuTK; + +namespace osu.Game.Tests.Visual.Editing +{ + public class TestSceneTimelineHitObjectBlueprint : TimelineTestScene + { + private Spinner spinner; + private TimelineHitObjectBlueprint blueprint; + + public TestSceneTimelineHitObjectBlueprint() + { + var spinner = new Spinner + { + Position = new Vector2(256, 256), + StartTime = -1000, + EndTime = 2000 + }; + + spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 }); + Add(new Container + { + RelativeSizeAxes = Axes.Both, + Size = new Vector2(0.5f), + Child = _ = new DrawableSpinner(spinner) + }); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + Clock.Seek(10000); + } + + public override Drawable CreateTestComponent() => blueprint = new TimelineHitObjectBlueprint(spinner); + + [Test] + public void TestDisallowZeroLengthSpinners() + { + + } + } +} From 05d3914fee2d9365577a2bf2fb8cd25fc0e11a17 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 21 Jan 2021 21:26:33 +0300 Subject: [PATCH 6122/6909] Rename friends tooltip to followers --- osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs b/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs index 6c2b2dc16a..7080a578d0 100644 --- a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs @@ -17,7 +17,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { public readonly Bindable User = new Bindable(); - public override string TooltipText => "friends"; + public override string TooltipText => "followers"; private OsuSpriteText followerText; From 2aa1df9ea4b5d95bdaa44b956d55f8de2eecf9ea Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 21 Jan 2021 21:38:52 +0300 Subject: [PATCH 6123/6909] Implement ProfileHeaderStatisticsButton component --- .../Header/Components/AddFriendButton.cs | 38 ++------------ .../ProfileHeaderStatisticsButton.cs | 52 +++++++++++++++++++ 2 files changed, 55 insertions(+), 35 deletions(-) create mode 100644 osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs diff --git a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs b/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs index 7080a578d0..ac05511132 100644 --- a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs @@ -3,58 +3,26 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; -using osu.Game.Graphics.Sprites; using osu.Game.Users; -using osuTK; namespace osu.Game.Overlays.Profile.Header.Components { - public class AddFriendButton : ProfileHeaderButton + public class AddFriendButton : ProfileHeaderStatisticsButton { public readonly Bindable User = new Bindable(); public override string TooltipText => "followers"; - private OsuSpriteText followerText; + protected override IconUsage CreateIcon() => FontAwesome.Solid.User; [BackgroundDependencyLoader] private void load() { - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Right = 10 }, - Children = new Drawable[] - { - new SpriteIcon - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Icon = FontAwesome.Solid.User, - FillMode = FillMode.Fit, - Size = new Vector2(50, 14) - }, - followerText = new OsuSpriteText - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Font = OsuFont.GetFont(weight: FontWeight.Bold) - } - } - }; - // todo: when friending/unfriending is implemented, the APIAccess.Friends list should be updated accordingly. - User.BindValueChanged(user => updateFollowers(user.NewValue), true); } - private void updateFollowers(User user) => followerText.Text = user?.FollowerCount.ToString("#,##0"); + private void updateFollowers(User user) => SetValue(user?.FollowerCount.ToString("#,##0")); } } diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs new file mode 100644 index 0000000000..7c1da503d1 --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.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 JetBrains.Annotations; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public abstract class ProfileHeaderStatisticsButton : ProfileHeaderButton + { + private readonly OsuSpriteText drawableText; + + protected ProfileHeaderStatisticsButton() + { + Child = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Direction = FillDirection.Horizontal, + Padding = new MarginPadding { Right = 10 }, + Children = new Drawable[] + { + new SpriteIcon + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Icon = CreateIcon(), + FillMode = FillMode.Fit, + Size = new Vector2(50, 14) + }, + drawableText = new OsuSpriteText + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Font = OsuFont.GetFont(weight: FontWeight.Bold) + } + } + }; + } + + [NotNull] + protected abstract IconUsage CreateIcon(); + + protected void SetValue(string value) => drawableText.Text = value; + } +} From 966440f109a452b46fa99b3fd2d3845650e02467 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 21 Jan 2021 22:02:19 +0300 Subject: [PATCH 6124/6909] Add MappingFollowerCount field to User --- osu.Game/Users/User.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index d7e78d5b35..518236755d 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -126,6 +126,9 @@ namespace osu.Game.Users [JsonProperty(@"follower_count")] public int FollowerCount; + [JsonProperty(@"mapping_follower_count")] + public int MappingFollowerCount; + [JsonProperty(@"favourite_beatmapset_count")] public int FavouriteBeatmapsetCount; From a7c22ebe88812ba1cf33bb931eceabcb11e83105 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 21 Jan 2021 22:02:54 +0300 Subject: [PATCH 6125/6909] Implement MappingSubscribersButton component --- .../Profile/Header/CentreHeaderContainer.cs | 7 ++++-- .../{AddFriendButton.cs => FriendsButton.cs} | 6 ++--- .../Components/MappingSubscribersButton.cs | 25 +++++++++++++++++++ .../ProfileHeaderStatisticsButton.cs | 14 +++++------ 4 files changed, 39 insertions(+), 13 deletions(-) rename osu.Game/Overlays/Profile/Header/Components/{AddFriendButton.cs => FriendsButton.cs} (75%) create mode 100644 osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs index 658cdb8ce3..1849b6d88a 100644 --- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs @@ -49,9 +49,12 @@ namespace osu.Game.Overlays.Profile.Header Spacing = new Vector2(10, 0), Children = new Drawable[] { - new AddFriendButton + new FriendsButton + { + User = { BindTarget = User } + }, + new MappingSubscribersButton { - RelativeSizeAxes = Axes.Y, User = { BindTarget = User } }, new MessageUserButton diff --git a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs b/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs similarity index 75% rename from osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs rename to osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs index ac05511132..f369874586 100644 --- a/osu.Game/Overlays/Profile/Header/Components/AddFriendButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs @@ -8,7 +8,7 @@ using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components { - public class AddFriendButton : ProfileHeaderStatisticsButton + public class FriendsButton : ProfileHeaderStatisticsButton { public readonly Bindable User = new Bindable(); @@ -20,9 +20,7 @@ namespace osu.Game.Overlays.Profile.Header.Components private void load() { // todo: when friending/unfriending is implemented, the APIAccess.Friends list should be updated accordingly. - User.BindValueChanged(user => updateFollowers(user.NewValue), true); + User.BindValueChanged(user => SetValue(user.NewValue?.FollowerCount ?? 0), true); } - - private void updateFollowers(User user) => SetValue(user?.FollowerCount.ToString("#,##0")); } } diff --git a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs new file mode 100644 index 0000000000..2fb53a0b9a --- /dev/null +++ b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.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.Bindables; +using osu.Framework.Graphics.Sprites; +using osu.Game.Users; + +namespace osu.Game.Overlays.Profile.Header.Components +{ + public class MappingSubscribersButton : ProfileHeaderStatisticsButton + { + public readonly Bindable User = new Bindable(); + + public override string TooltipText => "mapping subscribers"; + + protected override IconUsage CreateIcon() => FontAwesome.Solid.Bell; + + [BackgroundDependencyLoader] + private void load() + { + User.BindValueChanged(user => SetValue(user.NewValue?.MappingFollowerCount ?? 0), true); + } + } +} diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs index 7c1da503d1..84a6e351ea 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.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 JetBrains.Annotations; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -17,13 +16,14 @@ namespace osu.Game.Overlays.Profile.Header.Components protected ProfileHeaderStatisticsButton() { + RelativeSizeAxes = Axes.Y; Child = new FillFlowContainer { - AutoSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.X, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Direction = FillDirection.Horizontal, - Padding = new MarginPadding { Right = 10 }, Children = new Drawable[] { new SpriteIcon @@ -38,15 +38,15 @@ namespace osu.Game.Overlays.Profile.Header.Components { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Right = 10 }, Font = OsuFont.GetFont(weight: FontWeight.Bold) } } }; } - [NotNull] protected abstract IconUsage CreateIcon(); - protected void SetValue(string value) => drawableText.Text = value; + protected void SetValue(int value) => drawableText.Text = value.ToString("#,##0"); } } From 343166f158d09e74a9fee1df901989a80252c7be Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 21 Jan 2021 22:47:44 +0300 Subject: [PATCH 6126/6909] Make CreateIcon a property --- osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs | 2 +- .../Profile/Header/Components/MappingSubscribersButton.cs | 2 +- .../Header/Components/ProfileHeaderStatisticsButton.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs b/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs index f369874586..e0930b6a65 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Header.Components public override string TooltipText => "followers"; - protected override IconUsage CreateIcon() => FontAwesome.Solid.User; + protected override IconUsage CreateIcon => FontAwesome.Solid.User; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs index 2fb53a0b9a..ef290676d9 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Header.Components public override string TooltipText => "mapping subscribers"; - protected override IconUsage CreateIcon() => FontAwesome.Solid.Bell; + protected override IconUsage CreateIcon => FontAwesome.Solid.Bell; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs index 84a6e351ea..ff315727fa 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Icon = CreateIcon(), + Icon = CreateIcon, FillMode = FillMode.Fit, Size = new Vector2(50, 14) }, @@ -45,7 +45,7 @@ namespace osu.Game.Overlays.Profile.Header.Components }; } - protected abstract IconUsage CreateIcon(); + protected abstract IconUsage CreateIcon { get; } protected void SetValue(int value) => drawableText.Text = value.ToString("#,##0"); } From e87197c7fc80b9d40e85829b2120cfbbf8cca87d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 21 Jan 2021 22:48:31 +0300 Subject: [PATCH 6127/6909] Adjust text size --- .../Profile/Header/Components/ProfileHeaderStatisticsButton.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs index ff315727fa..118ea4c6aa 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs @@ -39,7 +39,7 @@ namespace osu.Game.Overlays.Profile.Header.Components Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Margin = new MarginPadding { Right = 10 }, - Font = OsuFont.GetFont(weight: FontWeight.Bold) + Font = OsuFont.GetFont(size: 14, weight: FontWeight.Bold) } } }; From 4555b9ff704bad005b094d0e5997ed1b29e9606c Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 21 Jan 2021 22:56:12 +0300 Subject: [PATCH 6128/6909] Make ProfileHeaderButton height defined --- osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs | 1 - osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs | 1 - .../Overlays/Profile/Header/Components/ProfileHeaderButton.cs | 1 + .../Profile/Header/Components/ProfileHeaderStatisticsButton.cs | 1 - 4 files changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs index 1849b6d88a..8f940cd0cc 100644 --- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs @@ -72,7 +72,6 @@ namespace osu.Game.Overlays.Profile.Header Width = UserProfileOverlay.CONTENT_X_MARGIN, Child = new ExpandDetailsButton { - RelativeSizeAxes = Axes.Y, Anchor = Anchor.Centre, Origin = Anchor.Centre, DetailsVisible = { BindTarget = DetailsVisible } diff --git a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs index cc6edcdd6a..228765ee1a 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MessageUserButton.cs @@ -33,7 +33,6 @@ namespace osu.Game.Overlays.Profile.Header.Components public MessageUserButton() { Content.Alpha = 0; - RelativeSizeAxes = Axes.Y; Child = new SpriteIcon { diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs index e14d73dd98..cea63574cf 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderButton.cs @@ -22,6 +22,7 @@ namespace osu.Game.Overlays.Profile.Header.Components protected ProfileHeaderButton() { AutoSizeAxes = Axes.X; + Height = 40; base.Content.Add(new CircularContainer { diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs index 118ea4c6aa..0b8f0b4d25 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs @@ -16,7 +16,6 @@ namespace osu.Game.Overlays.Profile.Header.Components protected ProfileHeaderStatisticsButton() { - RelativeSizeAxes = Axes.Y; Child = new FillFlowContainer { AutoSizeAxes = Axes.X, From a5f866d95ce0a6e97c8620b22266b5bf3f470173 Mon Sep 17 00:00:00 2001 From: vmaggioli Date: Thu, 21 Jan 2021 15:14:24 -0500 Subject: [PATCH 6129/6909] Test updates --- .../TestSceneTimelineHitObjectBlueprint.cs | 37 ++++++++----------- 1 file changed, 16 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index ed427c2020..1fa37470cb 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -1,28 +1,27 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Linq; using NUnit.Framework; 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.Beatmaps.ControlPoints; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; +using osuTK.Input; +using static osu.Game.Screens.Edit.Compose.Components.Timeline.TimelineHitObjectBlueprint; namespace osu.Game.Tests.Visual.Editing { public class TestSceneTimelineHitObjectBlueprint : TimelineTestScene { private Spinner spinner; - private TimelineHitObjectBlueprint blueprint; public TestSceneTimelineHitObjectBlueprint() { - var spinner = new Spinner + spinner = new Spinner { Position = new Vector2(256, 256), StartTime = -1000, @@ -30,26 +29,22 @@ namespace osu.Game.Tests.Visual.Editing }; spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 }); - Add(new Container - { - RelativeSizeAxes = Axes.Both, - Size = new Vector2(0.5f), - Child = _ = new DrawableSpinner(spinner) - }); } - protected override void LoadComplete() - { - base.LoadComplete(); - Clock.Seek(10000); - } - - public override Drawable CreateTestComponent() => blueprint = new TimelineHitObjectBlueprint(spinner); + public override Drawable CreateTestComponent() => new TimelineHitObjectBlueprint(spinner); [Test] public void TestDisallowZeroLengthSpinners() { - + DragBar dragBar = this.ChildrenOfType().First(); + Circle circle = this.ChildrenOfType().First(); + InputManager.MoveMouseTo(dragBar.ScreenSpaceDrawQuad.TopRight); + AddStep("drag dragbar to hit object", () => + { + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(circle.ScreenSpaceDrawQuad.TopLeft); + InputManager.ReleaseButton(MouseButton.Left); + }); } } } From c631354b57d39f165332f6482cce23fb1702c576 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 21 Jan 2021 23:39:19 +0300 Subject: [PATCH 6130/6909] Rename property --- osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs | 2 +- .../Profile/Header/Components/MappingSubscribersButton.cs | 2 +- .../Header/Components/ProfileHeaderStatisticsButton.cs | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs b/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs index e0930b6a65..09916997a4 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Header.Components public override string TooltipText => "followers"; - protected override IconUsage CreateIcon => FontAwesome.Solid.User; + protected override IconUsage Icon => FontAwesome.Solid.User; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs index ef290676d9..b4d7c9a05c 100644 --- a/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/MappingSubscribersButton.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays.Profile.Header.Components public override string TooltipText => "mapping subscribers"; - protected override IconUsage CreateIcon => FontAwesome.Solid.Bell; + protected override IconUsage Icon => FontAwesome.Solid.Bell; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs index 0b8f0b4d25..b65d5e2329 100644 --- a/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/ProfileHeaderStatisticsButton.cs @@ -29,7 +29,7 @@ namespace osu.Game.Overlays.Profile.Header.Components { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, - Icon = CreateIcon, + Icon = Icon, FillMode = FillMode.Fit, Size = new Vector2(50, 14) }, @@ -44,7 +44,7 @@ namespace osu.Game.Overlays.Profile.Header.Components }; } - protected abstract IconUsage CreateIcon { get; } + protected abstract IconUsage Icon { get; } protected void SetValue(int value) => drawableText.Text = value.ToString("#,##0"); } From 2eba2a9abf2b56f2637843846fab1988ea10aeb3 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Thu, 21 Jan 2021 23:40:23 +0300 Subject: [PATCH 6131/6909] Rename FriendsButton to FollowersButton --- osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs | 2 +- .../Header/Components/{FriendsButton.cs => FollowersButton.cs} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename osu.Game/Overlays/Profile/Header/Components/{FriendsButton.cs => FollowersButton.cs} (92%) diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs index 8f940cd0cc..04a1040e06 100644 --- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs @@ -49,7 +49,7 @@ namespace osu.Game.Overlays.Profile.Header Spacing = new Vector2(10, 0), Children = new Drawable[] { - new FriendsButton + new FollowersButton { User = { BindTarget = User } }, diff --git a/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs similarity index 92% rename from osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs rename to osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs index 09916997a4..bd8aa7b3bd 100644 --- a/osu.Game/Overlays/Profile/Header/Components/FriendsButton.cs +++ b/osu.Game/Overlays/Profile/Header/Components/FollowersButton.cs @@ -8,7 +8,7 @@ using osu.Game.Users; namespace osu.Game.Overlays.Profile.Header.Components { - public class FriendsButton : ProfileHeaderStatisticsButton + public class FollowersButton : ProfileHeaderStatisticsButton { public readonly Bindable User = new Bindable(); From 7046f64e7f7eaec8d9aa65cdac54aca13d01bc06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Jan 2021 22:07:08 +0100 Subject: [PATCH 6132/6909] Rewrite test scene --- .../TestSceneTimelineHitObjectBlueprint.cs | 54 ++++++++++--------- .../Visual/Editing/TimelineTestScene.cs | 10 ++-- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 1fa37470cb..15fa1d995b 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -1,13 +1,9 @@ using System.Linq; using NUnit.Framework; 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.Beatmaps.ControlPoints; +using osu.Game.Rulesets.Objects.Types; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Screens.Edit.Compose.Components.Timeline; using osuTK; using osuTK.Input; @@ -17,34 +13,40 @@ namespace osu.Game.Tests.Visual.Editing { public class TestSceneTimelineHitObjectBlueprint : TimelineTestScene { - private Spinner spinner; - - public TestSceneTimelineHitObjectBlueprint() - { - spinner = new Spinner - { - Position = new Vector2(256, 256), - StartTime = -1000, - EndTime = 2000 - }; - - spinner.ApplyDefaults(new ControlPointInfo(), new BeatmapDifficulty { CircleSize = 2 }); - } - - public override Drawable CreateTestComponent() => new TimelineHitObjectBlueprint(spinner); + public override Drawable CreateTestComponent() => new TimelineBlueprintContainer(Composer); [Test] - public void TestDisallowZeroLengthSpinners() + public void TestDisallowZeroDurationObjects() { - DragBar dragBar = this.ChildrenOfType().First(); - Circle circle = this.ChildrenOfType().First(); - InputManager.MoveMouseTo(dragBar.ScreenSpaceDrawQuad.TopRight); - AddStep("drag dragbar to hit object", () => + DragBar dragBar; + + AddStep("add spinner", () => { + EditorBeatmap.Clear(); + EditorBeatmap.Add(new Spinner + { + Position = new Vector2(256, 256), + StartTime = 150, + Duration = 500 + }); + }); + + AddStep("hold down drag bar", () => + { + // distinguishes between the actual drag bar and its "underlay shadow". + dragBar = this.ChildrenOfType().Single(bar => bar.HandlePositionalInput); + InputManager.MoveMouseTo(dragBar); InputManager.PressButton(MouseButton.Left); - InputManager.MoveMouseTo(circle.ScreenSpaceDrawQuad.TopLeft); + }); + + AddStep("try to drag bar past start", () => + { + var blueprint = this.ChildrenOfType().Single(); + InputManager.MoveMouseTo(blueprint.SelectionQuad.TopLeft - new Vector2(100, 0)); InputManager.ReleaseButton(MouseButton.Left); }); + + AddAssert("object has non-zero duration", () => EditorBeatmap.HitObjects.OfType().Single().Duration > 0); } } } diff --git a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs index 63bb018d6e..d6db171cf0 100644 --- a/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs +++ b/osu.Game.Tests/Visual/Editing/TimelineTestScene.cs @@ -23,22 +23,24 @@ namespace osu.Game.Tests.Visual.Editing protected HitObjectComposer Composer { get; private set; } + protected EditorBeatmap EditorBeatmap { get; private set; } + [BackgroundDependencyLoader] private void load(AudioManager audio) { Beatmap.Value = new WaveformTestBeatmap(audio); var playable = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); - var editorBeatmap = new EditorBeatmap(playable); + EditorBeatmap = new EditorBeatmap(playable); - Dependencies.Cache(editorBeatmap); - Dependencies.CacheAs(editorBeatmap); + Dependencies.Cache(EditorBeatmap); + Dependencies.CacheAs(EditorBeatmap); Composer = playable.BeatmapInfo.Ruleset.CreateInstance().CreateHitObjectComposer().With(d => d.Alpha = 0); AddRange(new Drawable[] { - editorBeatmap, + EditorBeatmap, Composer, new FillFlowContainer { From a71f769cce26eb9a010e2dd67d2ee637f74cd932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Jan 2021 22:09:42 +0100 Subject: [PATCH 6133/6909] Add missing license header --- .../Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs index 15fa1d995b..35f394fe1d 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneTimelineHitObjectBlueprint.cs @@ -1,4 +1,7 @@ -using System.Linq; +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; From e4c5e5ba17ae1b7359d26b628fc92a9565f4bcfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Jan 2021 22:11:51 +0100 Subject: [PATCH 6134/6909] Separate return statement with blank line --- .../Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 99edcd2149..508783a499 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -229,6 +229,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders float expectedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; if (expectedDistance < 1) return; + HitObject.Path.ExpectedDistance.Value = expectedDistance; editorBeatmap?.Update(HitObject); } From d0fd2ae432700dc8a7d125b827e6931ab69940c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 21 Jan 2021 22:20:07 +0100 Subject: [PATCH 6135/6909] Fix added zero-length slider test not working properly --- .../TestSceneSliderSelectionBlueprint.cs | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index ce1c13dac5..4edf778bfd 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -165,20 +165,12 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor public void TestZeroLengthSliderNotAllowed() { moveMouseToControlPoint(1); - AddStep("drag control point 1 to control point 0", () => - { - InputManager.PressButton(MouseButton.Left); - moveMouseToControlPoint(0); - InputManager.ReleaseButton(MouseButton.Left); - }); + dragMouseToControlPoint(0); + moveMouseToControlPoint(2); - AddStep("drag control point 2 to control point 0", () => - { - InputManager.PressButton(MouseButton.Left); - moveMouseToControlPoint(0); - InputManager.ReleaseButton(MouseButton.Left); - }); - checkPositions(); + dragMouseToControlPoint(0); + + AddAssert("slider has non-zero duration", () => slider.Duration > 0); } private void moveHitObject() @@ -209,6 +201,13 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } + private void dragMouseToControlPoint(int index) + { + AddStep("hold down mouse button", () => InputManager.PressButton(MouseButton.Left)); + moveMouseToControlPoint(index); + AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); + } + private void checkControlPointSelected(int index, bool selected) => AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected); From b220939650672b6e2056464f32082a31b8f0c531 Mon Sep 17 00:00:00 2001 From: Mysfit <8022806+Mysfit@users.noreply.github.com> Date: Thu, 21 Jan 2021 17:10:11 -0500 Subject: [PATCH 6136/6909] Fix storyboard samples continuing to play when the beatmap is paused or the intro is skipped. --- osu.Game/Screens/Play/Player.cs | 13 ++++++++++++- osu.Game/Skinning/PausableSkinnableSound.cs | 8 ++++---- .../Drawables/DrawableStoryboardSample.cs | 10 ++++++++++ 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 1fcbed7ef7..b622f11775 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -353,7 +353,7 @@ namespace osu.Game.Screens.Play }, skipOverlay = new SkipOverlay(DrawableRuleset.GameplayStartTime) { - RequestSkip = GameplayClockContainer.Skip + RequestSkip = performUserRequestedSkip }, FailOverlay = new FailOverlay { @@ -488,6 +488,17 @@ namespace osu.Game.Screens.Play this.Exit(); } + private void performUserRequestedSkip() + { + // user requested skip + // disable sample playback to stop currently playing samples and perform skip + samplePlaybackDisabled.Value = true; + GameplayClockContainer.Skip(); + + // return samplePlaybackDisabled.Value to what is defined by the beatmap's current state + updateSampleDisabledState(); + } + private void performUserRequestedExit() { if (ValidForResume && HasFailed && !FailOverlay.IsPresent) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index cb5234c847..e0891fbda2 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -32,7 +32,7 @@ namespace osu.Game.Skinning { } - private readonly IBindable samplePlaybackDisabled = new Bindable(); + protected readonly IBindable SamplePlaybackDisabled = new Bindable(); private ScheduledDelegate scheduledStart; @@ -42,8 +42,8 @@ namespace osu.Game.Skinning // if in a gameplay context, pause sample playback when gameplay is paused. if (samplePlaybackDisabler != null) { - samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); - samplePlaybackDisabled.BindValueChanged(disabled => + SamplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); + SamplePlaybackDisabled.BindValueChanged(disabled => { if (!RequestedPlaying) return; @@ -72,7 +72,7 @@ namespace osu.Game.Skinning cancelPendingStart(); RequestedPlaying = true; - if (samplePlaybackDisabled.Value) + if (SamplePlaybackDisabled.Value) return; base.Play(restart); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 218f051bf0..5b49e71daa 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -40,6 +40,16 @@ namespace osu.Game.Storyboards.Drawables foreach (var sample in DrawableSamples) mod.ApplyToSample(sample); } + + SamplePlaybackDisabled.BindValueChanged(disabled => + { + if (!RequestedPlaying) return; + + // Since storyboard samples can be very long we want to stop the playback regardless of + // whether or not the sample is looping or not + if (disabled.NewValue) + Stop(); + }); } protected override void Update() From 07ec0c0e0bbe7047442033bde8f23ea7c8533e4b Mon Sep 17 00:00:00 2001 From: Mysfit <8022806+Mysfit@users.noreply.github.com> Date: Thu, 21 Jan 2021 17:46:47 -0500 Subject: [PATCH 6137/6909] Updated DrawableStoryboardSample to use GetBoundCopy() --- osu.Game/Skinning/PausableSkinnableSound.cs | 6 ++-- .../Drawables/DrawableStoryboardSample.cs | 28 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index e0891fbda2..361360035d 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -18,6 +18,10 @@ namespace osu.Game.Skinning protected bool RequestedPlaying { get; private set; } + protected IBindable SamplePlaybackDisabled => samplePlaybackDisabled; + + private readonly Bindable samplePlaybackDisabled = new Bindable(); + public PausableSkinnableSound() { } @@ -32,8 +36,6 @@ namespace osu.Game.Skinning { } - protected readonly IBindable SamplePlaybackDisabled = new Bindable(); - private ScheduledDelegate scheduledStart; [BackgroundDependencyLoader(true)] diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 5b49e71daa..b924b0551f 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -21,11 +21,29 @@ namespace osu.Game.Storyboards.Drawables public override bool RemoveWhenNotAlive => false; + private readonly IBindable samplePlaybackDisabled; + public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo) : base(sampleInfo) { this.sampleInfo = sampleInfo; LifetimeStart = sampleInfo.StartTime; + + samplePlaybackDisabled = SamplePlaybackDisabled.GetBoundCopy(); + } + + [BackgroundDependencyLoader(true)] + private void load() + { + samplePlaybackDisabled.BindValueChanged(disabled => + { + if (!RequestedPlaying) return; + + // Since storyboard samples can be very long we want to stop the playback regardless of + // whether or not the sample is looping or not + if (disabled.NewValue) + Stop(); + }); } [Resolved] @@ -40,16 +58,6 @@ namespace osu.Game.Storyboards.Drawables foreach (var sample in DrawableSamples) mod.ApplyToSample(sample); } - - SamplePlaybackDisabled.BindValueChanged(disabled => - { - if (!RequestedPlaying) return; - - // Since storyboard samples can be very long we want to stop the playback regardless of - // whether or not the sample is looping or not - if (disabled.NewValue) - Stop(); - }); } protected override void Update() From b53ad50cd48461b564fafe30c3c6885b3a6b11dd Mon Sep 17 00:00:00 2001 From: Mysfit <8022806+Mysfit@users.noreply.github.com> Date: Thu, 21 Jan 2021 18:00:37 -0500 Subject: [PATCH 6138/6909] Remove redundant variable --- osu.Game/Skinning/PausableSkinnableSound.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 361360035d..0f97307372 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -18,9 +18,7 @@ namespace osu.Game.Skinning protected bool RequestedPlaying { get; private set; } - protected IBindable SamplePlaybackDisabled => samplePlaybackDisabled; - - private readonly Bindable samplePlaybackDisabled = new Bindable(); + protected readonly IBindable SamplePlaybackDisabled = new Bindable(); public PausableSkinnableSound() { From 5b1bdfbdc502a0500bb0cfdeb3b3075c8d530324 Mon Sep 17 00:00:00 2001 From: Mysfit <8022806+Mysfit@users.noreply.github.com> Date: Thu, 21 Jan 2021 20:06:24 -0500 Subject: [PATCH 6139/6909] Use callback method override --- osu.Game/Skinning/PausableSkinnableSound.cs | 59 ++++++++++--------- .../Drawables/DrawableStoryboardSample.cs | 31 ++++------ 2 files changed, 45 insertions(+), 45 deletions(-) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 0f97307372..b48cf5d448 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -18,7 +18,7 @@ namespace osu.Game.Skinning protected bool RequestedPlaying { get; private set; } - protected readonly IBindable SamplePlaybackDisabled = new Bindable(); + private readonly IBindable samplePlaybackDisabled = new Bindable(); public PausableSkinnableSound() { @@ -42,37 +42,32 @@ namespace osu.Game.Skinning // if in a gameplay context, pause sample playback when gameplay is paused. if (samplePlaybackDisabler != null) { - SamplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); - SamplePlaybackDisabled.BindValueChanged(disabled => - { - if (!RequestedPlaying) return; - - // let non-looping samples that have already been started play out to completion (sounds better than abruptly cutting off). - if (!Looping) return; - - cancelPendingStart(); - - if (disabled.NewValue) - base.Stop(); - else - { - // schedule so we don't start playing a sample which is no longer alive. - scheduledStart = Schedule(() => - { - if (RequestedPlaying) - base.Play(); - }); - } - }); + samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); + samplePlaybackDisabled.BindValueChanged(SamplePlaybackDisabledChanged); } } + protected virtual void SamplePlaybackDisabledChanged(ValueChangedEvent disabled) + { + if (!RequestedPlaying) return; + + // let non-looping samples that have already been started play out to completion (sounds better than abruptly cutting off). + if (!Looping) return; + + CancelPendingStart(); + + if (disabled.NewValue) + base.Stop(); + else + ScheduleStart(); + } + public override void Play(bool restart = true) { - cancelPendingStart(); + CancelPendingStart(); RequestedPlaying = true; - if (SamplePlaybackDisabled.Value) + if (samplePlaybackDisabled.Value) return; base.Play(restart); @@ -80,15 +75,25 @@ namespace osu.Game.Skinning public override void Stop() { - cancelPendingStart(); + CancelPendingStart(); RequestedPlaying = false; base.Stop(); } - private void cancelPendingStart() + protected void CancelPendingStart() { scheduledStart?.Cancel(); scheduledStart = null; } + + protected void ScheduleStart() + { + // schedule so we don't start playing a sample which is no longer alive. + scheduledStart = Schedule(() => + { + if (RequestedPlaying) + base.Play(); + }); + } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index b924b0551f..d5e1e19666 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -21,34 +21,29 @@ namespace osu.Game.Storyboards.Drawables public override bool RemoveWhenNotAlive => false; - private readonly IBindable samplePlaybackDisabled; - public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo) : base(sampleInfo) { this.sampleInfo = sampleInfo; LifetimeStart = sampleInfo.StartTime; - - samplePlaybackDisabled = SamplePlaybackDisabled.GetBoundCopy(); - } - - [BackgroundDependencyLoader(true)] - private void load() - { - samplePlaybackDisabled.BindValueChanged(disabled => - { - if (!RequestedPlaying) return; - - // Since storyboard samples can be very long we want to stop the playback regardless of - // whether or not the sample is looping or not - if (disabled.NewValue) - Stop(); - }); } [Resolved] private IBindable> mods { get; set; } + protected override void SamplePlaybackDisabledChanged(ValueChangedEvent disabled) + { + if (!RequestedPlaying) return; + + if (disabled.NewValue) + Stop(); + else + { + CancelPendingStart(); + ScheduleStart(); + } + } + protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); From 9f89b4e6d79822f7e7eb382767818add29bda6db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Jan 2021 14:25:21 +0900 Subject: [PATCH 6140/6909] Rewrite connection logic to better handle failure cases The main goal here is to ensure the connection is built each connection attempt. Previously, the access token would never be updated, leading to outdated tokens failing repeatedly (in the connection retry loop) and never being able to establish a new connection as a result. Due to threading considerations, this isn't as simple as I would hope it to be. I'm open to proposals as to a better way of handling this. Also, keep in mind that this logic will need to be abstracted and (re)used in `SpectatorClient` as well. I've intentionally not done that yet until we agree that this is a good direction forward. --- .../Online/Multiplayer/MultiplayerClient.cs | 141 ++++++++++++------ 1 file changed, 93 insertions(+), 48 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 50dc8f661c..34616a45a5 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -4,7 +4,6 @@ #nullable enable using System; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; @@ -30,6 +29,8 @@ namespace osu.Game.Online.Multiplayer private HubConnection? connection; + private CancellationTokenSource connectCancelSource = new CancellationTokenSource(); + private readonly string endpoint; public MultiplayerClient(EndpointConfiguration endpoints) @@ -50,8 +51,7 @@ namespace osu.Game.Online.Multiplayer { case APIState.Failing: case APIState.Offline: - connection?.StopAsync(); - connection = null; + Task.Run(Disconnect); break; case APIState.Online: @@ -60,70 +60,57 @@ namespace osu.Game.Online.Multiplayer } } - protected virtual async Task Connect() + private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1); + + public Task Disconnect() => disconnect(true); + + protected async Task Connect() { - if (connection != null) - return; + cancelExistingConnect(); - connection = new HubConnectionBuilder() - .WithUrl(endpoint, options => - { - options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); - }) - .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }) - .Build(); + await connectionLock.WaitAsync(); - // this is kind of SILLY - // https://github.com/dotnet/aspnetcore/issues/15198 - connection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); - connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); - connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); - connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); - connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); - connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); - connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); - connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); - connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); - - connection.Closed += async ex => + try { - isConnected.Value = false; + await disconnect(false); - Logger.Log(ex != null - ? $"Multiplayer client lost connection: {ex}" - : "Multiplayer client disconnected", LoggingTarget.Network); + // this token will be valid for the scope of this connection. + // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere. + var cancellationToken = connectCancelSource.Token; - if (connection != null) - await tryUntilConnected(); - }; - - await tryUntilConnected(); - - async Task tryUntilConnected() - { - Logger.Log("Multiplayer client connecting...", LoggingTarget.Network); - - while (api.State.Value == APIState.Online) + while (api.State.Value == APIState.Online && !cancellationToken.IsCancellationRequested) { + Logger.Log("Multiplayer client connecting...", LoggingTarget.Network); + try { - Debug.Assert(connection != null); + // importantly, rebuild the connection each attempt to get an updated access token. + connection = createConnection(cancellationToken); + + await connection.StartAsync(cancellationToken); - // reconnect on any failure - await connection.StartAsync(); Logger.Log("Multiplayer client connected!", LoggingTarget.Network); - - // Success. isConnected.Value = true; - break; + return; + } + catch (OperationCanceledException) + { + //connection process was cancelled. + return; } catch (Exception e) { Logger.Log($"Multiplayer client connection error: {e}", LoggingTarget.Network); - await Task.Delay(5000); + + // retry on any failure. + await Task.Delay(5000, cancellationToken); } } } + finally + { + connectionLock.Release(); + } } protected override Task JoinRoom(long roomId) @@ -189,5 +176,63 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); } + + private async Task disconnect(bool takeLock) + { + cancelExistingConnect(); + + if (takeLock) + await connectionLock.WaitAsync(); + + try + { + if (connection != null) + await connection.StopAsync(); + } + finally + { + connection = null; + if (takeLock) + connectionLock.Release(); + } + } + + private void cancelExistingConnect() + { + connectCancelSource.Cancel(); + connectCancelSource = new CancellationTokenSource(); + } + + private HubConnection createConnection(CancellationToken cancellationToken) + { + var newConnection = new HubConnectionBuilder() + .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }) + .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }) + .Build(); + + // this is kind of SILLY + // https://github.com/dotnet/aspnetcore/issues/15198 + newConnection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); + newConnection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); + newConnection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); + newConnection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); + newConnection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); + newConnection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); + newConnection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); + newConnection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); + newConnection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); + + newConnection.Closed += async ex => + { + isConnected.Value = false; + + Logger.Log(ex != null ? $"Multiplayer client lost connection: {ex}" : "Multiplayer client disconnected", LoggingTarget.Network); + + // make sure a disconnect wasn't triggered (and this is still the active connection). + if (!cancellationToken.IsCancellationRequested) + await Connect(); + }; + return newConnection; + } } } From d24d23646875b0bca4755c9d5b8a0caa24f30cae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Jan 2021 14:34:58 +0900 Subject: [PATCH 6141/6909] Make OperationCanceledException throwing behaviour consistent --- .../Online/Multiplayer/MultiplayerClient.cs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 34616a45a5..aa2305c991 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -12,6 +12,7 @@ using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; +using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -51,11 +52,11 @@ namespace osu.Game.Online.Multiplayer { case APIState.Failing: case APIState.Offline: - Task.Run(Disconnect); + Task.Run(Disconnect).CatchUnobservedExceptions(); break; case APIState.Online: - Task.Run(Connect); + Task.Run(Connect).CatchUnobservedExceptions(); break; } } @@ -78,8 +79,10 @@ namespace osu.Game.Online.Multiplayer // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere. var cancellationToken = connectCancelSource.Token; - while (api.State.Value == APIState.Online && !cancellationToken.IsCancellationRequested) + while (api.State.Value == APIState.Online) { + cancellationToken.ThrowIfCancellationRequested(); + Logger.Log("Multiplayer client connecting...", LoggingTarget.Network); try @@ -96,7 +99,7 @@ namespace osu.Game.Online.Multiplayer catch (OperationCanceledException) { //connection process was cancelled. - return; + throw; } catch (Exception e) { @@ -222,7 +225,7 @@ namespace osu.Game.Online.Multiplayer newConnection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); newConnection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); - newConnection.Closed += async ex => + newConnection.Closed += ex => { isConnected.Value = false; @@ -230,7 +233,9 @@ namespace osu.Game.Online.Multiplayer // make sure a disconnect wasn't triggered (and this is still the active connection). if (!cancellationToken.IsCancellationRequested) - await Connect(); + Task.Run(Connect, default).CatchUnobservedExceptions(); + + return Task.CompletedTask; }; return newConnection; } From 65b70759844c37ee4cdf723c615781934da9059e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Jan 2021 15:37:50 +0900 Subject: [PATCH 6142/6909] Limit the effect of parallax when outside the bounds of the ParallaxContainer This fixes the visual issues that still remain when mouse confining fails. I think it also feels more correct in general. --- .../Graphics/Containers/ParallaxContainer.cs | 22 +++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/ParallaxContainer.cs b/osu.Game/Graphics/Containers/ParallaxContainer.cs index 4cd3934cde..b501e68ba1 100644 --- a/osu.Game/Graphics/Containers/ParallaxContainer.cs +++ b/osu.Game/Graphics/Containers/ParallaxContainer.cs @@ -24,6 +24,10 @@ namespace osu.Game.Graphics.Containers private Bindable parallaxEnabled; + private const float parallax_duration = 100; + + private bool firstUpdate = true; + public ParallaxContainer() { RelativeSizeAxes = Axes.Both; @@ -60,17 +64,27 @@ namespace osu.Game.Graphics.Containers input = GetContainingInputManager(); } - private bool firstUpdate = true; - protected override void Update() { base.Update(); if (parallaxEnabled.Value) { - Vector2 offset = (input.CurrentState.Mouse == null ? Vector2.Zero : ToLocalSpace(input.CurrentState.Mouse.Position) - DrawSize / 2) * ParallaxAmount; + Vector2 offset = Vector2.Zero; - const float parallax_duration = 100; + if (input.CurrentState.Mouse != null) + { + var sizeDiv2 = DrawSize / 2; + + Vector2 relativeAmount = ToLocalSpace(input.CurrentState.Mouse.Position) - sizeDiv2; + + const float base_factor = 0.999f; + + relativeAmount.X = (float)(Math.Sign(relativeAmount.X) * Interpolation.Damp(0, 1, base_factor, Math.Abs(relativeAmount.X))); + relativeAmount.Y = (float)(Math.Sign(relativeAmount.Y) * Interpolation.Damp(0, 1, base_factor, Math.Abs(relativeAmount.Y))); + + offset = relativeAmount * sizeDiv2 * ParallaxAmount; + } double elapsed = Math.Clamp(Clock.ElapsedFrameTime, 0, parallax_duration); From fca6b15d2fa3fcfeec85cd3336aa181be6e1ffbb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Jan 2021 16:05:45 +0900 Subject: [PATCH 6143/6909] Fix local echo messages remaining permanently dimmed when chatting via multiplayer --- osu.Game/Overlays/Chat/ChatLine.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/ChatLine.cs b/osu.Game/Overlays/Chat/ChatLine.cs index 4eb348ae33..f43420e35e 100644 --- a/osu.Game/Overlays/Chat/ChatLine.cs +++ b/osu.Game/Overlays/Chat/ChatLine.cs @@ -190,13 +190,13 @@ namespace osu.Game.Overlays.Chat } } }; - - updateMessageContent(); } protected override void LoadComplete() { base.LoadComplete(); + + updateMessageContent(); FinishTransforms(true); } From bfabb1fdea25412c24088185fc0ed2c3e8a7f40c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Jan 2021 16:50:22 +0900 Subject: [PATCH 6144/6909] Change offset value to 10% --- osu.Game/Graphics/Containers/SectionsContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/SectionsContainer.cs b/osu.Game/Graphics/Containers/SectionsContainer.cs index 6ed161fa77..16bc6e4fc5 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -110,8 +110,8 @@ namespace osu.Game.Graphics.Containers /// /// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section). - /// - private const float scroll_y_centre = 0.2f; + /// + private const float scroll_y_centre = 0.1f; public SectionsContainer() { From a5f7ca485bfeeb38d88f113c6afe1bba6c49ba3c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Jan 2021 16:53:31 +0900 Subject: [PATCH 6145/6909] Fix unintended xmldoc tag edit --- 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 16bc6e4fc5..741fc17695 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -110,7 +110,7 @@ namespace osu.Game.Graphics.Containers /// /// The percentage of the container to consider the centre-point for deciding the active section (and scrolling to a requested section). - /// + /// private const float scroll_y_centre = 0.1f; public SectionsContainer() From a9c8f9bd4aebe9929007a89bdb5a938aa1541077 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Jan 2021 17:47:38 +0900 Subject: [PATCH 6146/6909] Fix a potential crash when exiting the editor before a new beatmap is added to the database --- osu.Game/Screens/Edit/Editor.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b7ebf0c0a4..66fe1a9507 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -109,7 +110,16 @@ namespace osu.Game.Screens.Edit if (Beatmap.Value is DummyWorkingBeatmap) { isNewBeatmap = true; - Beatmap.Value = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); + + var newBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); + + // this is a bit haphazard, but guards against setting the lease Beatmap bindable if + // the editor has already been exited. + if (!ValidForPush) + return; + + // this probably shouldn't be set in the asynchronous load method, but everything following relies on it. + Beatmap.Value = newBeatmap; } beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor; From b44bd8c4eea20cb3338c3f85e639a7472b6f5262 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 22 Jan 2021 18:03:33 +0900 Subject: [PATCH 6147/6909] Remove unused using statement --- osu.Game/Screens/Edit/Editor.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 66fe1a9507..a49de3b887 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using System.Threading; using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; From e0f8f6a23f64b584e5592a39e81e21e5e555195a Mon Sep 17 00:00:00 2001 From: Mysfit Date: Fri, 22 Jan 2021 12:09:40 -0500 Subject: [PATCH 6148/6909] introduce overrideable bool instead of copying event logic entirely --- osu.Game/Skinning/PausableSkinnableSound.cs | 58 +++++++++---------- .../Drawables/DrawableStoryboardSample.cs | 15 +---- 2 files changed, 30 insertions(+), 43 deletions(-) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index b48cf5d448..6245bcae02 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -18,7 +18,7 @@ namespace osu.Game.Skinning protected bool RequestedPlaying { get; private set; } - private readonly IBindable samplePlaybackDisabled = new Bindable(); + protected virtual bool AllowNonLoopingCutOff => false; public PausableSkinnableSound() { @@ -34,6 +34,8 @@ namespace osu.Game.Skinning { } + private readonly IBindable samplePlaybackDisabled = new Bindable(); + private ScheduledDelegate scheduledStart; [BackgroundDependencyLoader(true)] @@ -43,28 +45,34 @@ namespace osu.Game.Skinning if (samplePlaybackDisabler != null) { samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); - samplePlaybackDisabled.BindValueChanged(SamplePlaybackDisabledChanged); + samplePlaybackDisabled.BindValueChanged(disabled => + { + if (!RequestedPlaying) return; + + // if the sample is non-looping, and non-looping cut off is not allowed, + // let the sample play out to completion (sounds better than abruptly cutting off). + if (!Looping && !AllowNonLoopingCutOff) return; + + cancelPendingStart(); + + if (disabled.NewValue) + base.Stop(); + else + { + // schedule so we don't start playing a sample which is no longer alive. + scheduledStart = Schedule(() => + { + if (RequestedPlaying) + base.Play(); + }); + } + }); } } - protected virtual void SamplePlaybackDisabledChanged(ValueChangedEvent disabled) - { - if (!RequestedPlaying) return; - - // let non-looping samples that have already been started play out to completion (sounds better than abruptly cutting off). - if (!Looping) return; - - CancelPendingStart(); - - if (disabled.NewValue) - base.Stop(); - else - ScheduleStart(); - } - public override void Play(bool restart = true) { - CancelPendingStart(); + cancelPendingStart(); RequestedPlaying = true; if (samplePlaybackDisabled.Value) @@ -75,25 +83,15 @@ namespace osu.Game.Skinning public override void Stop() { - CancelPendingStart(); + cancelPendingStart(); RequestedPlaying = false; base.Stop(); } - protected void CancelPendingStart() + private void cancelPendingStart() { scheduledStart?.Cancel(); scheduledStart = null; } - - protected void ScheduleStart() - { - // schedule so we don't start playing a sample which is no longer alive. - scheduledStart = Schedule(() => - { - if (RequestedPlaying) - base.Play(); - }); - } } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index d5e1e19666..ebdf64e7ba 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -21,6 +21,8 @@ namespace osu.Game.Storyboards.Drawables public override bool RemoveWhenNotAlive => false; + protected override bool AllowNonLoopingCutOff => true; + public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo) : base(sampleInfo) { @@ -31,19 +33,6 @@ namespace osu.Game.Storyboards.Drawables [Resolved] private IBindable> mods { get; set; } - protected override void SamplePlaybackDisabledChanged(ValueChangedEvent disabled) - { - if (!RequestedPlaying) return; - - if (disabled.NewValue) - Stop(); - else - { - CancelPendingStart(); - ScheduleStart(); - } - } - protected override void SkinChanged(ISkinSource skin, bool allowFallback) { base.SkinChanged(skin, allowFallback); From 6379381f957c9194050f9ae5f47d15edba39f802 Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 22 Jan 2021 20:46:20 +0300 Subject: [PATCH 6149/6909] Make VotePill background transparent for own comments --- .../Visual/Online/TestSceneVotePill.cs | 17 +++++++++++++++-- osu.Game/Overlays/Comments/VotePill.cs | 14 ++++++++++---- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs index 9bb29541ec..420f6b1ab6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs @@ -7,6 +7,7 @@ using osu.Game.Overlays.Comments; using osu.Game.Online.API.Requests.Responses; using osu.Framework.Allocation; using osu.Game.Overlays; +using osu.Framework.Graphics.Shapes; namespace osu.Game.Tests.Visual.Online { @@ -16,13 +17,14 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); - private VotePill votePill; + private TestPill votePill; [Test] public void TestUserCommentPill() { AddStep("Log in", logIn); AddStep("User comment", () => addVotePill(getUserComment())); + AddAssert("Background is transparent", () => votePill.Background.Alpha == 0); AddStep("Click", () => votePill.Click()); AddAssert("Not loading", () => !votePill.IsLoading); } @@ -32,6 +34,7 @@ namespace osu.Game.Tests.Visual.Online { AddStep("Log in", logIn); AddStep("Random comment", () => addVotePill(getRandomComment())); + AddAssert("Background is not transparent", () => votePill.Background.Alpha == 1); AddStep("Click", () => votePill.Click()); AddAssert("Loading", () => votePill.IsLoading); } @@ -64,11 +67,21 @@ namespace osu.Game.Tests.Visual.Online private void addVotePill(Comment comment) { Clear(); - Add(votePill = new VotePill(comment) + Add(votePill = new TestPill(comment) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }); } + + private class TestPill : VotePill + { + public new Box Background => base.Background; + + public TestPill(Comment comment) + : base(comment) + { + } + } } } diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs index aa9723ea85..b6e6aa82c7 100644 --- a/osu.Game/Overlays/Comments/VotePill.cs +++ b/osu.Game/Overlays/Comments/VotePill.cs @@ -36,8 +36,10 @@ namespace osu.Game.Overlays.Comments [Resolved] private OverlayColourProvider colourProvider { get; set; } + protected Box Background { get; private set; } + private readonly Comment comment; - private Box background; + private Box hoverLayer; private CircularContainer borderContainer; private SpriteText sideNumber; @@ -62,8 +64,12 @@ namespace osu.Game.Overlays.Comments AccentColour = borderContainer.BorderColour = sideNumber.Colour = colours.GreenLight; hoverLayer.Colour = Color4.Black.Opacity(0.5f); - if (api.IsLoggedIn && api.LocalUser.Value.Id != comment.UserId) + var ownComment = api.LocalUser.Value.Id == comment.UserId; + + if (api.IsLoggedIn && !ownComment) Action = onAction; + + Background.Alpha = ownComment ? 0 : 1; } protected override void LoadComplete() @@ -71,7 +77,7 @@ namespace osu.Game.Overlays.Comments base.LoadComplete(); isVoted.Value = comment.IsVoted; votesCount.Value = comment.VotesCount; - isVoted.BindValueChanged(voted => background.Colour = voted.NewValue ? AccentColour : colourProvider.Background6, true); + isVoted.BindValueChanged(voted => Background.Colour = voted.NewValue ? AccentColour : colourProvider.Background6, true); votesCount.BindValueChanged(count => votesCounter.Text = $"+{count.NewValue}", true); } @@ -102,7 +108,7 @@ namespace osu.Game.Overlays.Comments Masking = true, Children = new Drawable[] { - background = new Box + Background = new Box { RelativeSizeAxes = Axes.Both }, From 61fcb486a8c7aea90c40b2c9f881fcc295f7b8de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Jan 2021 19:47:38 +0100 Subject: [PATCH 6150/6909] Trim unnecessary parentheses --- 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 741fc17695..624ec70cb6 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -181,7 +181,7 @@ namespace osu.Game.Graphics.Containers { base.UpdateAfterChildren(); - float fixedHeaderSize = (FixedHeader?.LayoutSize.Y ?? 0); + float fixedHeaderSize = FixedHeader?.LayoutSize.Y ?? 0; float expandableHeaderSize = ExpandableHeader?.LayoutSize.Y ?? 0; float headerH = expandableHeaderSize + fixedHeaderSize; From 20161aea6a472320f34ff0e8057d08a0cdea527d Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 22 Jan 2021 21:47:53 +0300 Subject: [PATCH 6151/6909] Show LoginOverlay if not logged-in when clicking on a pill --- .../Visual/Online/TestSceneVotePill.cs | 32 ++++++++++++++++--- osu.Game/Overlays/Comments/VotePill.cs | 11 ++++++- 2 files changed, 37 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs index 420f6b1ab6..e9e826e62f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs @@ -8,6 +8,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Framework.Allocation; using osu.Game.Overlays; using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Containers; namespace osu.Game.Tests.Visual.Online { @@ -17,11 +18,30 @@ namespace osu.Game.Tests.Visual.Online [Cached] private readonly OverlayColourProvider colourProvider = new OverlayColourProvider(OverlayColourScheme.Blue); + [Cached] + private LoginOverlay login; + private TestPill votePill; + private readonly Container pillContainer; + + public TestSceneVotePill() + { + AddRange(new Drawable[] + { + pillContainer = new Container + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AutoSizeAxes = Axes.Both + }, + login = new LoginOverlay() + }); + } [Test] public void TestUserCommentPill() { + AddStep("Hide login overlay", () => login.Hide()); AddStep("Log in", logIn); AddStep("User comment", () => addVotePill(getUserComment())); AddAssert("Background is transparent", () => votePill.Background.Alpha == 0); @@ -32,9 +52,10 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestRandomCommentPill() { + AddStep("Hide login overlay", () => login.Hide()); AddStep("Log in", logIn); AddStep("Random comment", () => addVotePill(getRandomComment())); - AddAssert("Background is not transparent", () => votePill.Background.Alpha == 1); + AddAssert("Background is visible", () => votePill.Background.Alpha == 1); AddStep("Click", () => votePill.Click()); AddAssert("Loading", () => votePill.IsLoading); } @@ -42,10 +63,11 @@ namespace osu.Game.Tests.Visual.Online [Test] public void TestOfflineRandomCommentPill() { + AddStep("Hide login overlay", () => login.Hide()); AddStep("Log out", API.Logout); AddStep("Random comment", () => addVotePill(getRandomComment())); AddStep("Click", () => votePill.Click()); - AddAssert("Not loading", () => !votePill.IsLoading); + AddAssert("Login overlay is visible", () => login.State.Value == Visibility.Visible); } private void logIn() => API.Login("localUser", "password"); @@ -66,12 +88,12 @@ namespace osu.Game.Tests.Visual.Online private void addVotePill(Comment comment) { - Clear(); - Add(votePill = new TestPill(comment) + pillContainer.Clear(); + pillContainer.Child = votePill = new TestPill(comment) { Anchor = Anchor.Centre, Origin = Anchor.Centre, - }); + }; } private class TestPill : VotePill diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs index b6e6aa82c7..cf3c470f96 100644 --- a/osu.Game/Overlays/Comments/VotePill.cs +++ b/osu.Game/Overlays/Comments/VotePill.cs @@ -33,6 +33,9 @@ namespace osu.Game.Overlays.Comments [Resolved] private IAPIProvider api { get; set; } + [Resolved(canBeNull: true)] + private LoginOverlay login { get; set; } + [Resolved] private OverlayColourProvider colourProvider { get; set; } @@ -66,7 +69,7 @@ namespace osu.Game.Overlays.Comments var ownComment = api.LocalUser.Value.Id == comment.UserId; - if (api.IsLoggedIn && !ownComment) + if (!ownComment) Action = onAction; Background.Alpha = ownComment ? 0 : 1; @@ -83,6 +86,12 @@ namespace osu.Game.Overlays.Comments private void onAction() { + if (!api.IsLoggedIn) + { + login?.Show(); + return; + } + request = new CommentVoteRequest(comment.Id, isVoted.Value ? CommentVoteAction.UnVote : CommentVoteAction.Vote); request.Success += onSuccess; api.Queue(request); From f3192877fee57db6f29d84faa208d10d1bdf41b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Jan 2021 19:48:33 +0100 Subject: [PATCH 6152/6909] Update outdated comment --- 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 624ec70cb6..8ab146efe7 100644 --- a/osu.Game/Graphics/Containers/SectionsContainer.cs +++ b/osu.Game/Graphics/Containers/SectionsContainer.cs @@ -217,7 +217,7 @@ namespace osu.Game.Graphics.Containers var smallestSectionHeight = Children.Count > 0 ? Children.Min(d => d.Height) : 0; - // scroll offset is our fixed header height if we have it plus 20% of content height + // scroll offset is our fixed header height if we have it plus 10% of content height // plus 5% to fix floating point errors and to not have a section instantly unselect when scrolling upwards // but the 5% can't be bigger than our smallest section height, otherwise it won't get selected correctly float selectionLenienceAboveSection = Math.Min(smallestSectionHeight / 2.0f, scrollContainer.DisplayableContent * 0.05f); From 3d42cc1f9199715990aa25c234a15885a87ec09a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 22 Jan 2021 22:27:26 +0300 Subject: [PATCH 6153/6909] Minor refactoring --- .../Visual/Online/TestSceneVotePill.cs | 19 ++----- osu.Game/Overlays/Comments/VotePill.cs | 53 ++++++++----------- 2 files changed, 26 insertions(+), 46 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs index e9e826e62f..6334c014c8 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs @@ -7,7 +7,6 @@ using osu.Game.Overlays.Comments; using osu.Game.Online.API.Requests.Responses; using osu.Framework.Allocation; using osu.Game.Overlays; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Containers; namespace osu.Game.Tests.Visual.Online @@ -21,7 +20,7 @@ namespace osu.Game.Tests.Visual.Online [Cached] private LoginOverlay login; - private TestPill votePill; + private VotePill votePill; private readonly Container pillContainer; public TestSceneVotePill() @@ -44,7 +43,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("Hide login overlay", () => login.Hide()); AddStep("Log in", logIn); AddStep("User comment", () => addVotePill(getUserComment())); - AddAssert("Background is transparent", () => votePill.Background.Alpha == 0); + AddAssert("Is disabled", () => !votePill.Enabled.Value); AddStep("Click", () => votePill.Click()); AddAssert("Not loading", () => !votePill.IsLoading); } @@ -55,7 +54,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("Hide login overlay", () => login.Hide()); AddStep("Log in", logIn); AddStep("Random comment", () => addVotePill(getRandomComment())); - AddAssert("Background is visible", () => votePill.Background.Alpha == 1); + AddAssert("Is enabled", () => votePill.Enabled.Value); AddStep("Click", () => votePill.Click()); AddAssert("Loading", () => votePill.IsLoading); } @@ -89,21 +88,11 @@ namespace osu.Game.Tests.Visual.Online private void addVotePill(Comment comment) { pillContainer.Clear(); - pillContainer.Child = votePill = new TestPill(comment) + pillContainer.Child = votePill = new VotePill(comment) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }; } - - private class TestPill : VotePill - { - public new Box Background => base.Background; - - public TestPill(Comment comment) - : base(comment) - { - } - } } } diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs index cf3c470f96..04a0508f3d 100644 --- a/osu.Game/Overlays/Comments/VotePill.cs +++ b/osu.Game/Overlays/Comments/VotePill.cs @@ -39,10 +39,9 @@ namespace osu.Game.Overlays.Comments [Resolved] private OverlayColourProvider colourProvider { get; set; } - protected Box Background { get; private set; } - + private bool isOwnComment; private readonly Comment comment; - + private Box background; private Box hoverLayer; private CircularContainer borderContainer; private SpriteText sideNumber; @@ -64,15 +63,14 @@ namespace osu.Game.Overlays.Comments [BackgroundDependencyLoader] private void load(OsuColour colours) { + isOwnComment = api.LocalUser.Value.Id == comment.UserId; + Action = onAction; + AccentColour = borderContainer.BorderColour = sideNumber.Colour = colours.GreenLight; hoverLayer.Colour = Color4.Black.Opacity(0.5f); + background.Alpha = isOwnComment ? 0 : 1; - var ownComment = api.LocalUser.Value.Id == comment.UserId; - - if (!ownComment) - Action = onAction; - - Background.Alpha = ownComment ? 0 : 1; + Enabled.Value = !isOwnComment; } protected override void LoadComplete() @@ -80,7 +78,7 @@ namespace osu.Game.Overlays.Comments base.LoadComplete(); isVoted.Value = comment.IsVoted; votesCount.Value = comment.VotesCount; - isVoted.BindValueChanged(voted => Background.Colour = voted.NewValue ? AccentColour : colourProvider.Background6, true); + isVoted.BindValueChanged(voted => background.Colour = voted.NewValue ? AccentColour : colourProvider.Background6, true); votesCount.BindValueChanged(count => votesCounter.Text = $"+{count.NewValue}", true); } @@ -117,7 +115,7 @@ namespace osu.Game.Overlays.Comments Masking = true, Children = new Drawable[] { - Background = new Box + background = new Box { RelativeSizeAxes = Axes.Both }, @@ -151,55 +149,48 @@ namespace osu.Game.Overlays.Comments protected override void OnLoadStarted() { votesCounter.FadeOut(duration, Easing.OutQuint); - updateDisplay(); + updateDisplay(false); } protected override void OnLoadFinished() { votesCounter.FadeIn(duration, Easing.OutQuint); - - if (IsHovered) - onHoverAction(); + updateDisplay(IsHovered); } protected override bool OnHover(HoverEvent e) { - onHoverAction(); + if (!isOwnComment && !IsLoading) + updateDisplay(true); + return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - updateDisplay(); + if (!isOwnComment && !IsLoading) + updateDisplay(false); + base.OnHoverLost(e); } - private void updateDisplay() + private void updateDisplay(bool isHovered) { - if (Action == null) - return; - if (isVoted.Value) { - hoverLayer.FadeTo(IsHovered ? 1 : 0); + hoverLayer.FadeTo(isHovered ? 1 : 0); sideNumber.Hide(); } else - sideNumber.FadeTo(IsHovered ? 1 : 0); + sideNumber.FadeTo(isHovered ? 1 : 0); - borderContainer.BorderThickness = IsHovered ? 3 : 0; - } - - private void onHoverAction() - { - if (!IsLoading) - updateDisplay(); + borderContainer.BorderThickness = isHovered ? 3 : 0; } protected override void Dispose(bool isDisposing) { - base.Dispose(isDisposing); request?.Cancel(); + base.Dispose(isDisposing); } } } From b692abd3c2574cacd5d41033e979f431069187e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 22 Jan 2021 20:17:21 +0100 Subject: [PATCH 6154/6909] Simplify condition from two to one operand --- osu.Game/Skinning/PausableSkinnableSound.cs | 13 ++++++++----- .../Drawables/DrawableStoryboardSample.cs | 6 +++++- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index 6245bcae02..d8149e76c0 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -18,7 +18,13 @@ namespace osu.Game.Skinning protected bool RequestedPlaying { get; private set; } - protected virtual bool AllowNonLoopingCutOff => false; + /// + /// Whether this is affected by + /// a higher-level 's state changes. + /// By default only looping samples are started/stopped on sample disable + /// to prevent one-time samples from cutting off abruptly. + /// + protected virtual bool AffectedBySamplePlaybackDisable => Looping; public PausableSkinnableSound() { @@ -48,10 +54,7 @@ namespace osu.Game.Skinning samplePlaybackDisabled.BindValueChanged(disabled => { if (!RequestedPlaying) return; - - // if the sample is non-looping, and non-looping cut off is not allowed, - // let the sample play out to completion (sounds better than abruptly cutting off). - if (!Looping && !AllowNonLoopingCutOff) return; + if (!AffectedBySamplePlaybackDisable) return; cancelPendingStart(); diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index ebdf64e7ba..5a800a71fd 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -21,7 +21,11 @@ namespace osu.Game.Storyboards.Drawables public override bool RemoveWhenNotAlive => false; - protected override bool AllowNonLoopingCutOff => true; + /// + /// Contrary to , all s are affected + /// by sample disables, as they are oftentimes longer-running sound effects. This also matches stable behaviour. + /// + protected override bool AffectedBySamplePlaybackDisable => true; public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo) : base(sampleInfo) From e9d10bb6e7bf9d9e48d25263cdc7c8f696b9a27a Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Fri, 22 Jan 2021 22:49:49 +0300 Subject: [PATCH 6155/6909] Revert "Minor refactoring" This reverts commit 3d42cc1f9199715990aa25c234a15885a87ec09a. --- .../Visual/Online/TestSceneVotePill.cs | 19 +++++-- osu.Game/Overlays/Comments/VotePill.cs | 53 +++++++++++-------- 2 files changed, 46 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs index 6334c014c8..e9e826e62f 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneVotePill.cs @@ -7,6 +7,7 @@ using osu.Game.Overlays.Comments; using osu.Game.Online.API.Requests.Responses; using osu.Framework.Allocation; using osu.Game.Overlays; +using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Containers; namespace osu.Game.Tests.Visual.Online @@ -20,7 +21,7 @@ namespace osu.Game.Tests.Visual.Online [Cached] private LoginOverlay login; - private VotePill votePill; + private TestPill votePill; private readonly Container pillContainer; public TestSceneVotePill() @@ -43,7 +44,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("Hide login overlay", () => login.Hide()); AddStep("Log in", logIn); AddStep("User comment", () => addVotePill(getUserComment())); - AddAssert("Is disabled", () => !votePill.Enabled.Value); + AddAssert("Background is transparent", () => votePill.Background.Alpha == 0); AddStep("Click", () => votePill.Click()); AddAssert("Not loading", () => !votePill.IsLoading); } @@ -54,7 +55,7 @@ namespace osu.Game.Tests.Visual.Online AddStep("Hide login overlay", () => login.Hide()); AddStep("Log in", logIn); AddStep("Random comment", () => addVotePill(getRandomComment())); - AddAssert("Is enabled", () => votePill.Enabled.Value); + AddAssert("Background is visible", () => votePill.Background.Alpha == 1); AddStep("Click", () => votePill.Click()); AddAssert("Loading", () => votePill.IsLoading); } @@ -88,11 +89,21 @@ namespace osu.Game.Tests.Visual.Online private void addVotePill(Comment comment) { pillContainer.Clear(); - pillContainer.Child = votePill = new VotePill(comment) + pillContainer.Child = votePill = new TestPill(comment) { Anchor = Anchor.Centre, Origin = Anchor.Centre, }; } + + private class TestPill : VotePill + { + public new Box Background => base.Background; + + public TestPill(Comment comment) + : base(comment) + { + } + } } } diff --git a/osu.Game/Overlays/Comments/VotePill.cs b/osu.Game/Overlays/Comments/VotePill.cs index 04a0508f3d..cf3c470f96 100644 --- a/osu.Game/Overlays/Comments/VotePill.cs +++ b/osu.Game/Overlays/Comments/VotePill.cs @@ -39,9 +39,10 @@ namespace osu.Game.Overlays.Comments [Resolved] private OverlayColourProvider colourProvider { get; set; } - private bool isOwnComment; + protected Box Background { get; private set; } + private readonly Comment comment; - private Box background; + private Box hoverLayer; private CircularContainer borderContainer; private SpriteText sideNumber; @@ -63,14 +64,15 @@ namespace osu.Game.Overlays.Comments [BackgroundDependencyLoader] private void load(OsuColour colours) { - isOwnComment = api.LocalUser.Value.Id == comment.UserId; - Action = onAction; - AccentColour = borderContainer.BorderColour = sideNumber.Colour = colours.GreenLight; hoverLayer.Colour = Color4.Black.Opacity(0.5f); - background.Alpha = isOwnComment ? 0 : 1; - Enabled.Value = !isOwnComment; + var ownComment = api.LocalUser.Value.Id == comment.UserId; + + if (!ownComment) + Action = onAction; + + Background.Alpha = ownComment ? 0 : 1; } protected override void LoadComplete() @@ -78,7 +80,7 @@ namespace osu.Game.Overlays.Comments base.LoadComplete(); isVoted.Value = comment.IsVoted; votesCount.Value = comment.VotesCount; - isVoted.BindValueChanged(voted => background.Colour = voted.NewValue ? AccentColour : colourProvider.Background6, true); + isVoted.BindValueChanged(voted => Background.Colour = voted.NewValue ? AccentColour : colourProvider.Background6, true); votesCount.BindValueChanged(count => votesCounter.Text = $"+{count.NewValue}", true); } @@ -115,7 +117,7 @@ namespace osu.Game.Overlays.Comments Masking = true, Children = new Drawable[] { - background = new Box + Background = new Box { RelativeSizeAxes = Axes.Both }, @@ -149,48 +151,55 @@ namespace osu.Game.Overlays.Comments protected override void OnLoadStarted() { votesCounter.FadeOut(duration, Easing.OutQuint); - updateDisplay(false); + updateDisplay(); } protected override void OnLoadFinished() { votesCounter.FadeIn(duration, Easing.OutQuint); - updateDisplay(IsHovered); + + if (IsHovered) + onHoverAction(); } protected override bool OnHover(HoverEvent e) { - if (!isOwnComment && !IsLoading) - updateDisplay(true); - + onHoverAction(); return base.OnHover(e); } protected override void OnHoverLost(HoverLostEvent e) { - if (!isOwnComment && !IsLoading) - updateDisplay(false); - + updateDisplay(); base.OnHoverLost(e); } - private void updateDisplay(bool isHovered) + private void updateDisplay() { + if (Action == null) + return; + if (isVoted.Value) { - hoverLayer.FadeTo(isHovered ? 1 : 0); + hoverLayer.FadeTo(IsHovered ? 1 : 0); sideNumber.Hide(); } else - sideNumber.FadeTo(isHovered ? 1 : 0); + sideNumber.FadeTo(IsHovered ? 1 : 0); - borderContainer.BorderThickness = isHovered ? 3 : 0; + borderContainer.BorderThickness = IsHovered ? 3 : 0; + } + + private void onHoverAction() + { + if (!IsLoading) + updateDisplay(); } protected override void Dispose(bool isDisposing) { - request?.Cancel(); base.Dispose(isDisposing); + request?.Cancel(); } } } From adcef19ab25953f003bc150654798f82e739a7d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jan 2021 15:44:30 +0100 Subject: [PATCH 6156/6909] Add coverage for operation tracker with failing tests --- .../NonVisual/OngoingOperationTrackerTest.cs | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs diff --git a/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs b/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs new file mode 100644 index 0000000000..b2be83d1f9 --- /dev/null +++ b/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs @@ -0,0 +1,62 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Testing; +using osu.Game.Screens.OnlinePlay; +using osu.Game.Tests.Visual; + +namespace osu.Game.Tests.NonVisual +{ + [HeadlessTest] + public class OngoingOperationTrackerTest : OsuTestScene + { + private OngoingOperationTracker tracker; + private IBindable operationInProgress; + + [SetUpSteps] + public void SetUp() + { + AddStep("create tracker", () => Child = tracker = new OngoingOperationTracker()); + AddStep("bind to operation status", () => operationInProgress = tracker.InProgress.GetBoundCopy()); + } + + [Test] + public void TestOperationTracking() + { + IDisposable firstOperation = null; + IDisposable secondOperation = null; + + AddStep("begin first operation", () => firstOperation = tracker.BeginOperation()); + AddAssert("operation in progress", () => operationInProgress.Value); + + AddStep("cannot start another operation", + () => Assert.Throws(() => tracker.BeginOperation())); + + AddStep("end first operation", () => firstOperation.Dispose()); + AddAssert("operation is ended", () => !operationInProgress.Value); + + AddStep("start second operation", () => secondOperation = tracker.BeginOperation()); + AddAssert("operation in progress", () => operationInProgress.Value); + + AddStep("dispose first operation again", () => firstOperation.Dispose()); + AddAssert("operation in progress", () => operationInProgress.Value); + + AddStep("dispose second operation", () => secondOperation.Dispose()); + AddAssert("operation is ended", () => !operationInProgress.Value); + } + + [Test] + public void TestOperationDisposalAfterTracker() + { + IDisposable operation = null; + + AddStep("begin operation", () => operation = tracker.BeginOperation()); + AddStep("dispose tracker", () => tracker.Expire()); + AddStep("end operation", () => operation.Dispose()); + AddAssert("operation is ended", () => !operationInProgress.Value); + } + } +} From 18b309a195e9aea1890a726dc1c77ed1f2e7afdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jan 2021 16:02:51 +0100 Subject: [PATCH 6157/6909] Make disposal of tracker operation idempotent --- osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs index 5c9e9ce90b..b834d4fa25 100644 --- a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -49,10 +49,7 @@ namespace osu.Game.Screens.OnlinePlay private void endOperation() { - if (leasedInProgress == null) - throw new InvalidOperationException("Cannot end operation multiple times."); - - leasedInProgress.Return(); + leasedInProgress?.Return(); leasedInProgress = null; } } From 7f89d9117d98f593209bc90271dd099a2febbcfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jan 2021 16:04:12 +0100 Subject: [PATCH 6158/6909] Make disposal of tracker idempotent for operations --- osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs index b834d4fa25..5d171e6e43 100644 --- a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -52,5 +52,13 @@ namespace osu.Game.Screens.OnlinePlay leasedInProgress?.Return(); leasedInProgress = null; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + // base call does an UnbindAllBindables(). + // clean up the leased reference here so that it doesn't get returned twice. + leasedInProgress = null; + } } } From d22f557a3bd074ff4ee29128f7d8a3375dece37d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jan 2021 16:14:58 +0100 Subject: [PATCH 6159/6909] Remove possibility of double-disposal interference --- .../OnlinePlay/OngoingOperationTracker.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs index 5d171e6e43..060f1d7b91 100644 --- a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay leasedInProgress.Value = true; // for extra safety, marshal the end of operation back to the update thread if necessary. - return new InvokeOnDisposal(() => Scheduler.Add(endOperation, false)); + return new OngoingOperation(() => Scheduler.Add(endOperation, false)); } private void endOperation() @@ -60,5 +60,26 @@ namespace osu.Game.Screens.OnlinePlay // clean up the leased reference here so that it doesn't get returned twice. leasedInProgress = null; } + + private class OngoingOperation : InvokeOnDisposal + { + private bool isDisposed; + + public OngoingOperation(Action action) + : base(action) + { + } + + public override void Dispose() + { + // base class does not check disposal state for performance reasons which aren't relevant here. + // track locally, to avoid interfering with other operations in case of a potential double-disposal. + if (isDisposed) + return; + + base.Dispose(); + isDisposed = true; + } + } } } From c30b700b3a320d070ceebac14e3f43cdd770661c Mon Sep 17 00:00:00 2001 From: yhsphd Date: Sun, 24 Jan 2021 00:26:52 +0900 Subject: [PATCH 6160/6909] "started" for past matches fixes grammar error at 'coming up next' section in schedule screen which displays schedule like "starting an hour ago" for past matches --- 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 88289ad6bd..b3fa9dc91c 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -194,7 +194,7 @@ namespace osu.Game.Tournament.Screens.Schedule { new TournamentSpriteText { - Text = "Starting ", + Text = match.NewValue.Date.Value.CompareTo(DateTimeOffset.Now) > 0 ? "Starting " : "Started ", Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) }, new DrawableDate(match.NewValue.Date.Value) From 899942611fcb085fc09226c84a7bbbd47500450f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jan 2021 17:01:47 +0100 Subject: [PATCH 6161/6909] Add tests for time display --- .../Screens/TestSceneScheduleScreen.cs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs index b240ef3ae5..0da8d1eb4a 100644 --- a/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.cs +++ b/osu.Game.Tournament.Tests/Screens/TestSceneScheduleScreen.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Tournament.Components; @@ -16,5 +18,23 @@ namespace osu.Game.Tournament.Tests.Screens Add(new TourneyVideo("main") { RelativeSizeAxes = Axes.Both }); Add(new ScheduleScreen()); } + + [Test] + public void TestCurrentMatchTime() + { + setMatchDate(TimeSpan.FromDays(-1)); + setMatchDate(TimeSpan.FromSeconds(5)); + setMatchDate(TimeSpan.FromMinutes(4)); + setMatchDate(TimeSpan.FromHours(3)); + } + + private void setMatchDate(TimeSpan relativeTime) + // Humanizer cannot handle negative timespans. + => AddStep($"start time is {relativeTime}", () => + { + var match = CreateSampleMatch(); + match.Date.Value = DateTimeOffset.Now + relativeTime; + Ladder.CurrentMatch.Value = match; + }); } } From a8fa09103cc0573dff3c0a3f2681293f40f45ab7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 23 Jan 2021 17:16:13 +0100 Subject: [PATCH 6162/6909] Update match start text prefix in real time --- .../Screens/Schedule/ScheduleScreen.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs index b3fa9dc91c..c1d8c8ddd3 100644 --- a/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs +++ b/osu.Game.Tournament/Screens/Schedule/ScheduleScreen.cs @@ -192,12 +192,7 @@ namespace osu.Game.Tournament.Screens.Schedule Origin = Anchor.CentreLeft, Children = new Drawable[] { - new TournamentSpriteText - { - Text = match.NewValue.Date.Value.CompareTo(DateTimeOffset.Now) > 0 ? "Starting " : "Started ", - Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) - }, - new DrawableDate(match.NewValue.Date.Value) + new ScheduleMatchDate(match.NewValue.Date.Value) { Font = OsuFont.Torus.With(size: 24, weight: FontWeight.Regular) } @@ -251,6 +246,18 @@ namespace osu.Game.Tournament.Screens.Schedule } } + public class ScheduleMatchDate : DrawableDate + { + public ScheduleMatchDate(DateTimeOffset date, float textSize = OsuFont.DEFAULT_FONT_SIZE, bool italic = true) + : base(date, textSize, italic) + { + } + + protected override string Format() => Date < DateTimeOffset.Now + ? $"Started {base.Format()}" + : $"Starting {base.Format()}"; + } + public class ScheduleContainer : Container { protected override Container Content => content; From eaa1519710577df734eb51e003777980fa434c81 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 24 Jan 2021 18:41:45 +0100 Subject: [PATCH 6163/6909] Implement native osu!lazer mod icons for tournament --- .../Components/TournamentBeatmapPanel.cs | 98 ++++++++++++++++--- 1 file changed, 86 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 477bf4bd63..7fac2bac71 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -16,6 +16,9 @@ using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Tournament.Models; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Screens.Play.HUD; using osuTK.Graphics; namespace osu.Game.Tournament.Components @@ -124,21 +127,11 @@ namespace osu.Game.Tournament.Components if (!string.IsNullOrEmpty(mods)) { - AddInternal(new Container + AddInternal(new ModSprite { - RelativeSizeAxes = Axes.Y, - Width = 60, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Margin = new MarginPadding(10), - Child = new Sprite - { - FillMode = FillMode.Fit, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Texture = textures.Get($"mods/{mods}"), - } + Mod = mods }); } } @@ -192,5 +185,86 @@ namespace osu.Game.Tournament.Components Alpha = 1; } } + + private class ModSprite : Container + { + public string Mod; + + public ModSprite() + { + Margin = new MarginPadding(10); + Width = 60; + RelativeSizeAxes = Axes.Y; + } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + var texture = textures.Get($"mods/{Mod}"); + + if (texture != null) + { + Child = new Sprite + { + FillMode = FillMode.Fit, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Texture = texture + }; + } + else + { + Mod selectedMod = null; + + switch (Mod) + { + case "DT": + selectedMod = new OsuModDoubleTime(); + break; + + case "FL": + selectedMod = new OsuModFlashlight(); + break; + + case "HT": + selectedMod = new OsuModHalfTime(); + break; + + case "HD": + selectedMod = new OsuModHidden(); + break; + + case "HR": + selectedMod = new OsuModHardRock(); + break; + + case "NF": + selectedMod = new OsuModNoFail(); + break; + + case "EZ": + selectedMod = new OsuModEasy(); + break; + } + + if (selectedMod != null) + { + Child = new ModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = + { + Value = new[] + { + selectedMod + } + } + }; + } + } + } + } } } From 9a5790cd31dbd9a60932fb16790fea3390db7ca8 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 24 Jan 2021 19:18:16 +0100 Subject: [PATCH 6164/6909] Implement StableStorage class. --- osu.Desktop/OsuGameDesktop.cs | 3 +- osu.Game/IO/StableStorage.cs | 64 +++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 osu.Game/IO/StableStorage.cs diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index d1515acafa..0dc659b120 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -18,6 +18,7 @@ using osu.Framework.Screens; using osu.Game.Screens.Menu; using osu.Game.Updater; using osu.Desktop.Windows; +using osu.Game.IO; namespace osu.Desktop { @@ -40,7 +41,7 @@ namespace osu.Desktop { string stablePath = getStableInstallPath(); if (!string.IsNullOrEmpty(stablePath)) - return new DesktopStorage(stablePath, desktopHost); + return new StableStorage(stablePath, desktopHost); } } catch (Exception) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs new file mode 100644 index 0000000000..a8665b5267 --- /dev/null +++ b/osu.Game/IO/StableStorage.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.IO; +using System.Linq; +using osu.Framework.Platform; + +namespace osu.Game.IO +{ + /// + /// A storage pointing to an osu-stable installation. + /// Provides methods for handling installations with a custom Song folder location. + /// + public class StableStorage : DesktopStorage + { + private const string stable_songs_path = "Songs"; + + private readonly DesktopGameHost host; + private string songs_path; + + public StableStorage(string path, DesktopGameHost host) + : base(path, host) + { + this.host = host; + songs_path = locateSongsDirectory(); + } + + /// + /// Returns a pointing to the osu-stable Songs directory. + /// + public Storage GetSongStorage() + { + if (songs_path.Equals(stable_songs_path, StringComparison.OrdinalIgnoreCase)) + return GetStorageForDirectory(stable_songs_path); + else + return new DesktopStorage(songs_path, host); + } + + private string locateSongsDirectory() + { + var configFile = GetStream(GetFiles(".", "osu!.*.cfg").First()); + var textReader = new StreamReader(configFile); + + var songs_directory_path = stable_songs_path; + + while (!textReader.EndOfStream) + { + var line = textReader.ReadLine(); + + if (line?.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase) == true) + { + var directory = line.Split('=')[1].TrimStart(); + if (Path.IsPathFullyQualified(directory) && !directory.Equals(stable_songs_path, StringComparison.OrdinalIgnoreCase)) + songs_directory_path = directory; + + break; + } + } + + return songs_directory_path; + } + } +} From d71ac834280c0873422a974dbc78bf73baa5ce71 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 24 Jan 2021 19:46:10 +0100 Subject: [PATCH 6165/6909] Use StableStorage in ArchiveModelManager. --- osu.Desktop/OsuGameDesktop.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 4 ++-- osu.Game/OsuGame.cs | 3 ++- osu.Game/Scoring/ScoreManager.cs | 3 ++- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Desktop/OsuGameDesktop.cs b/osu.Desktop/OsuGameDesktop.cs index 0dc659b120..5909b82c8f 100644 --- a/osu.Desktop/OsuGameDesktop.cs +++ b/osu.Desktop/OsuGameDesktop.cs @@ -33,7 +33,7 @@ namespace osu.Desktop noVersionOverlay = args?.Any(a => a == "--no-version-overlay") ?? false; } - public override Storage GetStorageForStableInstall() + public override StableStorage GetStorageForStableInstall() { try { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 9f69ad035f..7d22c51d0f 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -625,7 +625,7 @@ namespace osu.Game.Database /// /// Set a storage with access to an osu-stable install for import purposes. /// - public Func GetStableStorage { private get; set; } + public Func GetStableStorage { private get; set; } /// /// Denotes whether an osu-stable installation is present to perform automated imports from. @@ -640,7 +640,7 @@ namespace osu.Game.Database /// /// Select paths to import from stable. Default implementation iterates all directories in . /// - protected virtual IEnumerable GetStableImportPaths(Storage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath); + protected virtual IEnumerable GetStableImportPaths(StableStorage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath); /// /// Whether this specified path should be removed after successful import. diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5acd6bc73d..399bdda491 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -52,6 +52,7 @@ using osu.Game.Updater; using osu.Game.Utils; using LogLevel = osu.Framework.Logging.LogLevel; using osu.Game.Database; +using osu.Game.IO; namespace osu.Game { @@ -88,7 +89,7 @@ namespace osu.Game protected SentryLogger SentryLogger; - public virtual Storage GetStorageForStableInstall() => null; + public virtual StableStorage GetStorageForStableInstall() => null; public float ToolbarOffset => (Toolbar?.Position.Y ?? 0) + (Toolbar?.DrawHeight ?? 0); diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index cf1d123c06..11f31f7d59 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -16,6 +16,7 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; +using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -71,7 +72,7 @@ namespace osu.Game.Scoring } } - protected override IEnumerable GetStableImportPaths(Storage stableStorage) + protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) => stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)); public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); From f0fdad2f838ec4ab18a82841f4483cba66f715f0 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 24 Jan 2021 22:04:46 +0100 Subject: [PATCH 6166/6909] Construct a DesktopStorage pointing to the absolute path of the song directory. --- osu.Game/IO/StableStorage.cs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index a8665b5267..c7ca37a163 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -29,20 +29,14 @@ namespace osu.Game.IO /// /// Returns a pointing to the osu-stable Songs directory. /// - public Storage GetSongStorage() - { - if (songs_path.Equals(stable_songs_path, StringComparison.OrdinalIgnoreCase)) - return GetStorageForDirectory(stable_songs_path); - else - return new DesktopStorage(songs_path, host); - } + public Storage GetSongStorage() => new DesktopStorage(songs_path, host); private string locateSongsDirectory() { var configFile = GetStream(GetFiles(".", "osu!.*.cfg").First()); var textReader = new StreamReader(configFile); - var songs_directory_path = stable_songs_path; + var songs_directory_path = Path.Combine(BasePath, stable_songs_path); while (!textReader.EndOfStream) { @@ -51,7 +45,7 @@ namespace osu.Game.IO if (line?.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase) == true) { var directory = line.Split('=')[1].TrimStart(); - if (Path.IsPathFullyQualified(directory) && !directory.Equals(stable_songs_path, StringComparison.OrdinalIgnoreCase)) + if (Path.IsPathFullyQualified(directory)) songs_directory_path = directory; break; From 51d4da565c87192549f62fa94cf624818809a3d8 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Sun, 24 Jan 2021 22:25:49 +0100 Subject: [PATCH 6167/6909] Fix ArchiveModelManagers lookup paths. --- osu.Game/Beatmaps/BeatmapManager.cs | 8 +++++++- osu.Game/Database/ArchiveModelManager.cs | 12 +++++++++--- osu.Game/Scoring/ScoreManager.cs | 3 ++- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 42418e532b..a455f676b3 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -64,7 +64,13 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - protected override string ImportFromStablePath => "Songs"; + protected override bool CheckStableDirectoryExists(StableStorage stableStorage) => stableStorage.GetSongStorage().ExistsDirectory("."); + + protected override IEnumerable GetStableImportPaths(StableStorage stableStoage) + { + var songStorage = stableStoage.GetSongStorage(); + return songStorage.GetDirectories(".").Select(path => songStorage.GetFullPath(path)); + } private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 7d22c51d0f..516f70c700 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -637,10 +637,16 @@ namespace osu.Game.Database /// protected virtual string ImportFromStablePath => null; + /// + /// Checks for the existence of an osu-stable directory. + /// + protected virtual bool CheckStableDirectoryExists(StableStorage stableStorage) => stableStorage.ExistsDirectory(ImportFromStablePath); + /// /// Select paths to import from stable. Default implementation iterates all directories in . /// - protected virtual IEnumerable GetStableImportPaths(StableStorage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath); + protected virtual IEnumerable GetStableImportPaths(StableStorage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath) + .Select(path => stableStoage.GetFullPath(path)); /// /// Whether this specified path should be removed after successful import. @@ -662,14 +668,14 @@ namespace osu.Game.Database return Task.CompletedTask; } - if (!stable.ExistsDirectory(ImportFromStablePath)) + if (!CheckStableDirectoryExists(stable)) { // This handles situations like when the user does not have a Skins folder Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); return Task.CompletedTask; } - return Task.Run(async () => await Import(GetStableImportPaths(GetStableStorage()).Select(f => stable.GetFullPath(f)).ToArray())); + return Task.Run(async () => await Import(GetStableImportPaths(stable).ToArray())); } #endregion diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 11f31f7d59..6aa0a30a75 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -73,7 +73,8 @@ namespace osu.Game.Scoring } protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) - => stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)); + => stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) + .Select(path => stableStorage.GetFullPath(path)); public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); From d38db6eace2c6b00f5a3b92fe0f603302184f993 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 24 Jan 2021 23:29:05 +0100 Subject: [PATCH 6168/6909] Change ModSprite to use ruleset's mods directly. --- .../Components/TournamentBeatmapPanel.cs | 66 +++++-------------- 1 file changed, 16 insertions(+), 50 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 7fac2bac71..8fc52f8b4b 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -15,10 +15,9 @@ using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; -using osu.Game.Tournament.Models; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets; using osu.Game.Screens.Play.HUD; +using osu.Game.Tournament.Models; using osuTK.Graphics; namespace osu.Game.Tournament.Components @@ -190,6 +189,12 @@ namespace osu.Game.Tournament.Components { public string Mod; + [Resolved] + private LadderInfo ladderInfo { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + public ModSprite() { Margin = new MarginPadding(10); @@ -198,7 +203,7 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(TextureStore textures, IBindable ruleset) { var texture = textures.Get($"mods/{Mod}"); @@ -215,54 +220,15 @@ namespace osu.Game.Tournament.Components } else { - Mod selectedMod = null; - - switch (Mod) + Child = new ModDisplay { - case "DT": - selectedMod = new OsuModDoubleTime(); - break; - - case "FL": - selectedMod = new OsuModFlashlight(); - break; - - case "HT": - selectedMod = new OsuModHalfTime(); - break; - - case "HD": - selectedMod = new OsuModHidden(); - break; - - case "HR": - selectedMod = new OsuModHardRock(); - break; - - case "NF": - selectedMod = new OsuModNoFail(); - break; - - case "EZ": - selectedMod = new OsuModEasy(); - break; - } - - if (selectedMod != null) - { - Child = new ModDisplay + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Current = { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Current = - { - Value = new[] - { - selectedMod - } - } - }; - } + Value = rulesets.GetRuleset(ladderInfo.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods().Where(mod => mod.Acronym == Mod).ToArray() + } + }; } } } From c6d46129ad25567e2a3c9736de085e3669c78557 Mon Sep 17 00:00:00 2001 From: Shivam Date: Sun, 24 Jan 2021 23:33:02 +0100 Subject: [PATCH 6169/6909] Remove unneccessary ruleset parameter --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 8fc52f8b4b..92ac123097 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -203,7 +203,7 @@ namespace osu.Game.Tournament.Components } [BackgroundDependencyLoader] - private void load(TextureStore textures, IBindable ruleset) + private void load(TextureStore textures) { var texture = textures.Get($"mods/{Mod}"); From 304264046b0feba5236722652a81bd70ffe46628 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Sun, 24 Jan 2021 17:46:54 -0500 Subject: [PATCH 6170/6909] Added tests. --- .../TestSceneStoryboardSamplePlayback.cs | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs new file mode 100644 index 0000000000..1544f8fd35 --- /dev/null +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneStoryboardSamplePlayback.cs @@ -0,0 +1,74 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Osu; +using osu.Game.Storyboards; +using osu.Game.Storyboards.Drawables; + +namespace osu.Game.Tests.Visual.Gameplay +{ + public class TestSceneStoryboardSamplePlayback : PlayerTestScene + { + private Storyboard storyboard; + + [BackgroundDependencyLoader] + private void load(OsuConfigManager config) + { + config.Set(OsuSetting.ShowStoryboard, true); + + storyboard = new Storyboard(); + var backgroundLayer = storyboard.GetLayer("Background"); + backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -7000, volume: 20)); + backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: -5000, volume: 20)); + backgroundLayer.Add(new StoryboardSampleInfo("Intro/welcome.mp3", time: 0, volume: 20)); + } + + [Test] + public void TestStoryboardSamplesStopDuringPause() + { + checkForFirstSamplePlayback(); + + AddStep("player paused", () => Player.Pause()); + AddAssert("player is currently paused", () => Player.GameplayClockContainer.IsPaused.Value); + AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); + + AddStep("player resume", () => Player.Resume()); + AddUntilStep("any storyboard samples playing after resume", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + } + + [Test] + public void TestStoryboardSamplesStopOnSkip() + { + checkForFirstSamplePlayback(); + + AddStep("skip intro", () => InputManager.Key(osuTK.Input.Key.Space)); + AddAssert("all storyboard samples stopped immediately", () => allStoryboardSamples.All(sound => !sound.IsPlaying)); + + AddUntilStep("any storyboard samples playing after skip", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + } + + private void checkForFirstSamplePlayback() + { + AddUntilStep("storyboard loaded", () => Player.Beatmap.Value.StoryboardLoaded); + AddUntilStep("any storyboard samples playing", () => allStoryboardSamples.Any(sound => sound.IsPlaying)); + } + + private IEnumerable allStoryboardSamples => Player.ChildrenOfType(); + + protected override bool AllowFail => false; + + protected override Ruleset CreatePlayerRuleset() => new OsuRuleset(); + protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, false); + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) => + new ClockBackedTestWorkingBeatmap(beatmap, storyboard ?? this.storyboard, Clock, Audio); + } +} From bb8113fb5136b1c235693899ea11b58c649f289c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 14:47:47 +0900 Subject: [PATCH 6171/6909] Fix mod select footer not animating correctly on first reveal --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b93602116b..1258ba719d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -216,9 +216,9 @@ namespace osu.Game.Overlays.Mods }, new Drawable[] { - // Footer new Container { + Name = "Footer content", RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Origin = Anchor.TopCentre, @@ -237,10 +237,9 @@ namespace osu.Game.Overlays.Mods Anchor = Anchor.BottomCentre, AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, + RelativePositionAxes = 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, @@ -354,7 +353,7 @@ namespace osu.Game.Overlays.Mods { base.PopOut(); - footerContainer.MoveToX(footerContainer.DrawSize.X, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); + footerContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); footerContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); foreach (var section in ModSectionsContainer.Children) From 366f074f86eab5f0a665fe70843720b03fc9fb5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 16:53:38 +0900 Subject: [PATCH 6172/6909] Better describe test steps to discern on failures --- .../NonVisual/OngoingOperationTrackerTest.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs b/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs index b2be83d1f9..eef9582af9 100644 --- a/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs +++ b/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs @@ -30,22 +30,22 @@ namespace osu.Game.Tests.NonVisual IDisposable secondOperation = null; AddStep("begin first operation", () => firstOperation = tracker.BeginOperation()); - AddAssert("operation in progress", () => operationInProgress.Value); + AddAssert("first operation in progress", () => operationInProgress.Value); AddStep("cannot start another operation", () => Assert.Throws(() => tracker.BeginOperation())); AddStep("end first operation", () => firstOperation.Dispose()); - AddAssert("operation is ended", () => !operationInProgress.Value); + AddAssert("first operation is ended", () => !operationInProgress.Value); AddStep("start second operation", () => secondOperation = tracker.BeginOperation()); - AddAssert("operation in progress", () => operationInProgress.Value); + AddAssert("second operation in progress", () => operationInProgress.Value); AddStep("dispose first operation again", () => firstOperation.Dispose()); - AddAssert("operation in progress", () => operationInProgress.Value); + AddAssert("second operation still in progress", () => operationInProgress.Value); AddStep("dispose second operation", () => secondOperation.Dispose()); - AddAssert("operation is ended", () => !operationInProgress.Value); + AddAssert("second operation is ended", () => !operationInProgress.Value); } [Test] From 10e8b7082e5affb83670333a6115af16c34ee9c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 16:53:58 +0900 Subject: [PATCH 6173/6909] Rework logic to avoid custom disposal early return handling --- .../OnlinePlay/OngoingOperationTracker.cs | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs index 060f1d7b91..b7ee84eb9e 100644 --- a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -43,42 +42,45 @@ namespace osu.Game.Screens.OnlinePlay leasedInProgress = inProgress.BeginLease(true); leasedInProgress.Value = true; - // for extra safety, marshal the end of operation back to the update thread if necessary. - return new OngoingOperation(() => Scheduler.Add(endOperation, false)); + return new OngoingOperation(this, leasedInProgress); } - private void endOperation() + private void endOperationWithKnownLease(LeasedBindable lease) { - leasedInProgress?.Return(); - leasedInProgress = null; + if (lease != leasedInProgress) + return; + + // for extra safety, marshal the end of operation back to the update thread if necessary. + Scheduler.Add(() => + { + leasedInProgress?.Return(); + leasedInProgress = null; + }, false); } protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + // base call does an UnbindAllBindables(). // clean up the leased reference here so that it doesn't get returned twice. leasedInProgress = null; } - private class OngoingOperation : InvokeOnDisposal + private class OngoingOperation : IDisposable { - private bool isDisposed; + private readonly OngoingOperationTracker tracker; + private readonly LeasedBindable lease; - public OngoingOperation(Action action) - : base(action) + public OngoingOperation(OngoingOperationTracker tracker, LeasedBindable lease) { + this.tracker = tracker; + this.lease = lease; } - public override void Dispose() + public void Dispose() { - // base class does not check disposal state for performance reasons which aren't relevant here. - // track locally, to avoid interfering with other operations in case of a potential double-disposal. - if (isDisposed) - return; - - base.Dispose(); - isDisposed = true; + tracker.endOperationWithKnownLease(lease); } } } From c05ae3497afa8c5a7dc551496e189620ccbd11ad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 17:02:24 +0900 Subject: [PATCH 6174/6909] Make connect/disconnect private --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index aa2305c991..7dc4919d23 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -25,6 +25,8 @@ namespace osu.Game.Online.Multiplayer private readonly Bindable isConnected = new Bindable(); private readonly IBindable apiState = new Bindable(); + private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1); + [Resolved] private IAPIProvider api { get; set; } = null!; @@ -52,20 +54,16 @@ namespace osu.Game.Online.Multiplayer { case APIState.Failing: case APIState.Offline: - Task.Run(Disconnect).CatchUnobservedExceptions(); + Task.Run(() => disconnect(true)).CatchUnobservedExceptions(); break; case APIState.Online: - Task.Run(Connect).CatchUnobservedExceptions(); + Task.Run(connect).CatchUnobservedExceptions(); break; } } - private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1); - - public Task Disconnect() => disconnect(true); - - protected async Task Connect() + private async Task connect() { cancelExistingConnect(); @@ -233,7 +231,7 @@ namespace osu.Game.Online.Multiplayer // make sure a disconnect wasn't triggered (and this is still the active connection). if (!cancellationToken.IsCancellationRequested) - Task.Run(Connect, default).CatchUnobservedExceptions(); + Task.Run(connect, default).CatchUnobservedExceptions(); return Task.CompletedTask; }; From 994fb2667dc22d06d0fc7aca26387ef03ec04859 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 17:11:04 +0900 Subject: [PATCH 6175/6909] Call DisposeAsync instead of StopAsync --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 7dc4919d23..ffed2b57fe 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -188,7 +188,7 @@ namespace osu.Game.Online.Multiplayer try { if (connection != null) - await connection.StopAsync(); + await connection.DisposeAsync(); } finally { From 0f09a7feb9ee12c05c1d53464db9e6194e8818d0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 17:17:04 +0900 Subject: [PATCH 6176/6909] Avoid semaphore potentially getting held forever --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index ffed2b57fe..391658f0d0 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -67,7 +67,7 @@ namespace osu.Game.Online.Multiplayer { cancelExistingConnect(); - await connectionLock.WaitAsync(); + await connectionLock.WaitAsync(10000); try { @@ -183,7 +183,7 @@ namespace osu.Game.Online.Multiplayer cancelExistingConnect(); if (takeLock) - await connectionLock.WaitAsync(); + await connectionLock.WaitAsync(10000); try { @@ -237,5 +237,12 @@ namespace osu.Game.Online.Multiplayer }; return newConnection; } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + cancelExistingConnect(); + } } } From 91ce3df3a944244d73e18630f3dd26e3709df282 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 17:44:01 +0900 Subject: [PATCH 6177/6909] Bind MultiplayerGameplayLeaderboard to player updates later in load process --- osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs index d4ce542a67..a3d27c4e71 100644 --- a/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/MultiplayerGameplayLeaderboard.cs @@ -53,8 +53,6 @@ namespace osu.Game.Screens.Play.HUD [BackgroundDependencyLoader] private void load(OsuConfigManager config, IAPIProvider api) { - streamingClient.OnNewFrames += handleIncomingFrames; - foreach (var userId in playingUsers) { streamingClient.WatchUser(userId); @@ -90,6 +88,9 @@ namespace osu.Game.Screens.Play.HUD playingUsers.BindTo(multiplayerClient.CurrentMatchPlayingUserIds); playingUsers.BindCollectionChanged(usersChanged); + + // this leaderboard should be guaranteed to be completely loaded before the gameplay starts (is a prerequisite in MultiplayerPlayer). + streamingClient.OnNewFrames += handleIncomingFrames; } private void usersChanged(object sender, NotifyCollectionChangedEventArgs e) From 4ac362ee1acc899e1593ef548b981962e00916ec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 18:29:00 +0900 Subject: [PATCH 6178/6909] Move cloning local to editor --- osu.Game/Beatmaps/WorkingBeatmap.cs | 2 -- osu.Game/Screens/Edit/Editor.cs | 4 ++++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index d25adca92b..30382c444f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -111,8 +111,6 @@ namespace osu.Game.Beatmaps // Convert IBeatmap converted = converter.Convert(cancellationSource.Token); - converted.ControlPointInfo = converted.ControlPointInfo.CreateCopy(); - // Apply conversion mods to the result foreach (var mod in mods.OfType()) { diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index b7ebf0c0a4..0e04d1ea12 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -131,6 +131,10 @@ namespace osu.Game.Screens.Edit try { playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + + // clone these locally for now to avoid incurring overhead on GetPlayableBeatmap usages. + // eventually we will want to improve how/where this is done as there are issues with *not* cloning it in all cases. + playableBeatmap.ControlPointInfo = playableBeatmap.ControlPointInfo.CreateCopy(); } catch (Exception e) { From b489e92c9e1042e0d9254158f7da75a339142436 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 18:43:36 +0900 Subject: [PATCH 6179/6909] Fix TimelineParts not using correct beatmap --- .../Timelines/Summary/Parts/BookmarkPart.cs | 3 +-- .../Timelines/Summary/Parts/BreakPart.cs | 5 ++--- .../Summary/Parts/ControlPointPart.cs | 5 ++--- .../Timelines/Summary/Parts/MarkerPart.cs | 8 ++------ .../Timelines/Summary/Parts/TimelinePart.cs | 18 +++++++++++------- .../Timeline/TimelineControlPointDisplay.cs | 5 ++--- 6 files changed, 20 insertions(+), 24 deletions(-) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs index 103e39e78a..8298cf4773 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BookmarkPart.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; @@ -13,7 +12,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// public class BookmarkPart : TimelinePart { - protected override void LoadBeatmap(WorkingBeatmap beatmap) + protected override void LoadBeatmap(EditorBeatmap beatmap) { base.LoadBeatmap(beatmap); foreach (int bookmark in beatmap.BeatmapInfo.Bookmarks) diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs index ceccbffc9c..e8a4b5c8c7 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/BreakPart.cs @@ -2,7 +2,6 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Allocation; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.Timing; using osu.Game.Graphics; using osu.Game.Screens.Edit.Components.Timelines.Summary.Visualisations; @@ -14,10 +13,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts /// public class BreakPart : TimelinePart { - protected override void LoadBeatmap(WorkingBeatmap beatmap) + protected override void LoadBeatmap(EditorBeatmap beatmap) { base.LoadBeatmap(beatmap); - foreach (var breakPeriod in beatmap.Beatmap.Breaks) + foreach (var breakPeriod in beatmap.Breaks) Add(new BreakVisualisation(breakPeriod)); } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs index e76ab71e54..70afc1e308 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/ControlPointPart.cs @@ -4,7 +4,6 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Bindables; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts @@ -16,12 +15,12 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { private readonly IBindableList controlPointGroups = new BindableList(); - protected override void LoadBeatmap(WorkingBeatmap beatmap) + protected override void LoadBeatmap(EditorBeatmap beatmap) { base.LoadBeatmap(beatmap); controlPointGroups.UnbindAll(); - controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups); + controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((sender, args) => { switch (args.Action) 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 5a2214509c..d551333616 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/MarkerPart.cs @@ -2,15 +2,14 @@ // See the LICENCE file in the repository root for full licence text. using System; -using osuTK; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Threading; -using osu.Game.Beatmaps; using osu.Game.Graphics; +using osuTK; namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { @@ -54,9 +53,6 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts scheduledSeek?.Cancel(); scheduledSeek = Schedule(() => { - if (Beatmap.Value == null) - return; - float markerPos = Math.Clamp(ToLocalSpace(screenPosition).X, 0, DrawWidth); editorClock.SeekSmoothlyTo(markerPos / DrawWidth * editorClock.TrackLength); }); @@ -68,7 +64,7 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts marker.X = (float)editorClock.CurrentTime; } - protected override void LoadBeatmap(WorkingBeatmap beatmap) + protected override void LoadBeatmap(EditorBeatmap beatmap) { // block base call so we don't clear our marker (can be reused on beatmap change). } diff --git a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs index 5b8f7c747b..5aba81aa7d 100644 --- a/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs +++ b/osu.Game/Screens/Edit/Components/Timelines/Summary/Parts/TimelinePart.cs @@ -21,7 +21,10 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts ///
    public class TimelinePart : Container where T : Drawable { - protected readonly IBindable Beatmap = new Bindable(); + private readonly IBindable beatmap = new Bindable(); + + [Resolved] + protected EditorBeatmap EditorBeatmap { get; private set; } protected readonly IBindable Track = new Bindable(); @@ -33,10 +36,9 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts { AddInternal(this.content = content ?? new Container { RelativeSizeAxes = Axes.Both }); - Beatmap.ValueChanged += b => + beatmap.ValueChanged += b => { updateRelativeChildSize(); - LoadBeatmap(b.NewValue); }; Track.ValueChanged += _ => updateRelativeChildSize(); @@ -45,24 +47,26 @@ namespace osu.Game.Screens.Edit.Components.Timelines.Summary.Parts [BackgroundDependencyLoader] private void load(IBindable beatmap, EditorClock clock) { - Beatmap.BindTo(beatmap); + this.beatmap.BindTo(beatmap); + LoadBeatmap(EditorBeatmap); + Track.BindTo(clock.Track); } private void updateRelativeChildSize() { // the track may not be loaded completely (only has a length once it is). - if (!Beatmap.Value.Track.IsLoaded) + if (!beatmap.Value.Track.IsLoaded) { content.RelativeChildSize = Vector2.One; Schedule(updateRelativeChildSize); return; } - content.RelativeChildSize = new Vector2((float)Math.Max(1, Beatmap.Value.Track.Length), 1); + content.RelativeChildSize = new Vector2((float)Math.Max(1, beatmap.Value.Track.Length), 1); } - protected virtual void LoadBeatmap(WorkingBeatmap beatmap) + protected virtual void LoadBeatmap(EditorBeatmap beatmap) { content.Clear(); } diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs index 13191df13c..18600bcdee 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineControlPointDisplay.cs @@ -5,7 +5,6 @@ using System.Collections.Specialized; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Screens.Edit.Components.Timelines.Summary.Parts; @@ -23,12 +22,12 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline RelativeSizeAxes = Axes.Both; } - protected override void LoadBeatmap(WorkingBeatmap beatmap) + protected override void LoadBeatmap(EditorBeatmap beatmap) { base.LoadBeatmap(beatmap); controlPointGroups.UnbindAll(); - controlPointGroups.BindTo(beatmap.Beatmap.ControlPointInfo.Groups); + controlPointGroups.BindTo(beatmap.ControlPointInfo.Groups); controlPointGroups.BindCollectionChanged((sender, args) => { switch (args.Action) From f3061a8e837e4a59522d9a4faeac1b0e9f98aa00 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 18:47:41 +0900 Subject: [PATCH 6180/6909] Update squirrel to fix incorrect desktop icon creation on install --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 4554f8b83a..e201b250d4 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -25,7 +25,7 @@ - + From 439f03e3b3d78dbdbc8ba3d6db559c13043f1e86 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 25 Jan 2021 19:25:38 +0900 Subject: [PATCH 6181/6909] Fix failing test due to missing dependency --- .../Visual/Editing/TestSceneEditorSummaryTimeline.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs index 3adc1bd425..94a9fd7b35 100644 --- a/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs +++ b/osu.Game.Tests/Visual/Editing/TestSceneEditorSummaryTimeline.cs @@ -5,6 +5,8 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Game.Rulesets.Osu; +using osu.Game.Rulesets.Osu.Beatmaps; +using osu.Game.Screens.Edit; using osu.Game.Screens.Edit.Components.Timelines.Summary; using osuTK; @@ -13,6 +15,9 @@ namespace osu.Game.Tests.Visual.Editing [TestFixture] public class TestSceneEditorSummaryTimeline : EditorClockTestScene { + [Cached(typeof(EditorBeatmap))] + private readonly EditorBeatmap editorBeatmap = new EditorBeatmap(new OsuBeatmap()); + [BackgroundDependencyLoader] private void load() { From 964976f604e9071e929f90cdd934ab104cf20bdb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 25 Jan 2021 20:41:51 +0900 Subject: [PATCH 6182/6909] Use a task chain and fix potential misordering of events --- .../Online/Multiplayer/MultiplayerClient.cs | 11 +- .../Multiplayer/StatefulMultiplayerClient.cs | 123 +++++------------- .../Multiplayer/TestMultiplayerClient.cs | 2 + osu.Game/Utils/TaskChain.cs | 30 +++++ 4 files changed, 70 insertions(+), 96 deletions(-) create mode 100644 osu.Game/Utils/TaskChain.cs diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 5d18521eac..8573adc94a 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -134,17 +134,12 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoom), roomId); } - public override async Task LeaveRoom() + protected override Task LeaveRoomInternal() { if (!isConnected.Value) - { - // even if not connected, make sure the local room state can be cleaned up. - await base.LeaveRoom(); - return; - } + return Task.FromCanceled(new CancellationToken(true)); - await base.LeaveRoom(); - await connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); + return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); } public override Task TransferHost(int userId) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index f2b5a44fcf..94122aeff5 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -108,67 +108,38 @@ namespace osu.Game.Online.Multiplayer }); } - private readonly object joinOrLeaveTaskLock = new object(); - private Task? joinOrLeaveTask; + private readonly TaskChain joinOrLeaveTaskChain = new TaskChain(); /// /// Joins the for a given API . /// /// The API . - public async Task JoinRoom(Room room) + public async Task JoinRoom(Room room) => await joinOrLeaveTaskChain.Add(async () => { - Task? lastTask; - Task newTask; + if (Room != null) + throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); - lock (joinOrLeaveTaskLock) + Debug.Assert(room.RoomID.Value != null); + + // Join the server-side room. + var joinedRoom = await JoinRoom(room.RoomID.Value.Value); + Debug.Assert(joinedRoom != null); + + // Populate users. + Debug.Assert(joinedRoom.Users != null); + await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)); + + // Update the stored room (must be done on update thread for thread-safety). + await scheduleAsync(() => { - lastTask = joinOrLeaveTask; - joinOrLeaveTask = newTask = Task.Run(async () => - { - if (lastTask != null) - await lastTask; + Room = joinedRoom; + apiRoom = room; + playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; + }); - // Should be thread-safe since joinOrLeaveTask is locked on in both JoinRoom() and LeaveRoom(). - if (Room != null) - throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); - - Debug.Assert(room.RoomID.Value != null); - - // Join the server-side room. - var joinedRoom = await JoinRoom(room.RoomID.Value.Value); - Debug.Assert(joinedRoom != null); - - // Populate users. - Debug.Assert(joinedRoom.Users != null); - await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)); - - // Update the stored room (must be done on update thread for thread-safety). - await scheduleAsync(() => - { - Room = joinedRoom; - apiRoom = room; - playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; - }); - - // Update room settings. - await updateLocalRoomSettings(joinedRoom.Settings); - }); - } - - try - { - await newTask; - } - finally - { - // The task will be awaited in the future, so reset it so that the user doesn't get into a permanently faulted state if anything fails. - lock (joinOrLeaveTaskLock) - { - if (joinOrLeaveTask == newTask) - joinOrLeaveTask = null; - } - } - } + // Update room settings. + await updateLocalRoomSettings(joinedRoom.Settings); + }); /// /// Joins the with a given ID. @@ -177,48 +148,24 @@ namespace osu.Game.Online.Multiplayer /// The joined . protected abstract Task JoinRoom(long roomId); - public virtual async Task LeaveRoom() + public async Task LeaveRoom() => await joinOrLeaveTaskChain.Add(async () => { - Task? lastTask; - Task newTask; + if (Room == null) + return; - lock (joinOrLeaveTaskLock) + await scheduleAsync(() => { - lastTask = joinOrLeaveTask; - joinOrLeaveTask = newTask = Task.Run(async () => - { - if (lastTask != null) - await lastTask; + apiRoom = null; + Room = null; + CurrentMatchPlayingUserIds.Clear(); - // Should be thread-safe since joinOrLeaveTask is locked on in both JoinRoom() and LeaveRoom(). - if (Room == null) - return; + RoomUpdated?.Invoke(); + }); - await scheduleAsync(() => - { - apiRoom = null; - Room = null; - CurrentMatchPlayingUserIds.Clear(); + await LeaveRoomInternal(); + }); - RoomUpdated?.Invoke(); - }); - }); - } - - try - { - await newTask; - } - finally - { - // The task will be awaited in the future, so reset it so that the user doesn't get into a permanently faulted state if anything fails. - lock (joinOrLeaveTaskLock) - { - if (joinOrLeaveTask == newTask) - joinOrLeaveTask = null; - } - } - } + protected abstract Task LeaveRoomInternal(); /// /// Change the current settings. diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 7fbc770351..a79183fdab 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -98,6 +98,8 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.FromResult(room); } + protected override Task LeaveRoomInternal() => Task.CompletedTask; + public override Task TransferHost(int userId) => ((IMultiplayerClient)this).HostChanged(userId); public override async Task ChangeSettings(MultiplayerRoomSettings settings) diff --git a/osu.Game/Utils/TaskChain.cs b/osu.Game/Utils/TaskChain.cs new file mode 100644 index 0000000000..b397b0c45b --- /dev/null +++ b/osu.Game/Utils/TaskChain.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. + +#nullable enable + +using System; +using System.Threading.Tasks; + +namespace osu.Game.Utils +{ + /// + /// A chain of s that run sequentially. + /// + public class TaskChain + { + private readonly object currentTaskLock = new object(); + private Task? currentTask; + + public Task Add(Func taskFunc) + { + lock (currentTaskLock) + { + currentTask = currentTask == null + ? taskFunc() + : currentTask.ContinueWith(_ => taskFunc()).Unwrap(); + return currentTask; + } + } + } +} From bb44fcfe31c78f77321714c466724847d27492ce Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 25 Jan 2021 20:58:02 +0900 Subject: [PATCH 6183/6909] Prevent some data races --- .../Multiplayer/StatefulMultiplayerClient.cs | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 94122aeff5..0e736ed7c6 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -148,12 +149,15 @@ namespace osu.Game.Online.Multiplayer /// The joined . protected abstract Task JoinRoom(long roomId); - public async Task LeaveRoom() => await joinOrLeaveTaskChain.Add(async () => + public Task LeaveRoom() { if (Room == null) - return; + return Task.FromCanceled(new CancellationToken(true)); - await scheduleAsync(() => + // Leaving rooms is expected to occur instantaneously whilst the operation is finalised in the background. + // However a few members need to be reset immediately to prevent other components from entering invalid states whilst the operation hasn't yet completed. + // For example, if a room was left and the user immediately pressed the "create room" button, then the user could be taken into the lobby if the value of Room is not reset in time. + var scheduledReset = scheduleAsync(() => { apiRoom = null; Room = null; @@ -162,8 +166,12 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); }); - await LeaveRoomInternal(); - }); + return joinOrLeaveTaskChain.Add(async () => + { + await scheduledReset; + await LeaveRoomInternal(); + }); + } protected abstract Task LeaveRoomInternal(); From c17774e23c6b770d07cd2d9ea059ca1bcc4dff1a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 25 Jan 2021 20:58:05 +0900 Subject: [PATCH 6184/6909] Add xmldoc --- osu.Game/Utils/TaskChain.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Utils/TaskChain.cs b/osu.Game/Utils/TaskChain.cs index b397b0c45b..64d523bd3d 100644 --- a/osu.Game/Utils/TaskChain.cs +++ b/osu.Game/Utils/TaskChain.cs @@ -16,6 +16,11 @@ namespace osu.Game.Utils private readonly object currentTaskLock = new object(); private Task? currentTask; + /// + /// Adds a new task to the end of this . + /// + /// The task creation function. + /// The awaitable . public Task Add(Func taskFunc) { lock (currentTaskLock) From f89eb7d75db7982d3cdc6200b4737c630094eb87 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 25 Jan 2021 13:22:37 +0100 Subject: [PATCH 6185/6909] Split and rename TournamentModDisplay component --- .../Components/TournamentBeatmapPanel.cs | 64 +++---------------- .../Components/TournamentModDisplay.cs | 56 ++++++++++++++++ 2 files changed, 64 insertions(+), 56 deletions(-) create mode 100644 osu.Game.Tournament/Components/TournamentModDisplay.cs diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 92ac123097..2ed99d2fb5 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -9,14 +9,11 @@ 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.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; -using osu.Game.Rulesets; -using osu.Game.Screens.Play.HUD; using osu.Game.Tournament.Models; using osuTK.Graphics; @@ -25,7 +22,7 @@ namespace osu.Game.Tournament.Components public class TournamentBeatmapPanel : CompositeDrawable { public readonly BeatmapInfo Beatmap; - private readonly string mods; + private readonly string mod; private const float horizontal_padding = 10; private const float vertical_padding = 10; @@ -40,7 +37,7 @@ namespace osu.Game.Tournament.Components if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); Beatmap = beatmap; - this.mods = mods; + this.mod = mods; Width = 400; Height = HEIGHT; } @@ -124,13 +121,16 @@ namespace osu.Game.Tournament.Components }, }); - if (!string.IsNullOrEmpty(mods)) + if (!string.IsNullOrEmpty(mod)) { - AddInternal(new ModSprite + AddInternal(new TournamentModDisplay { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Mod = mods + Margin = new MarginPadding(10), + Width = 60, + RelativeSizeAxes = Axes.Y, + ModAcronym = mod }); } } @@ -184,53 +184,5 @@ namespace osu.Game.Tournament.Components Alpha = 1; } } - - private class ModSprite : Container - { - public string Mod; - - [Resolved] - private LadderInfo ladderInfo { get; set; } - - [Resolved] - private RulesetStore rulesets { get; set; } - - public ModSprite() - { - Margin = new MarginPadding(10); - Width = 60; - RelativeSizeAxes = Axes.Y; - } - - [BackgroundDependencyLoader] - private void load(TextureStore textures) - { - var texture = textures.Get($"mods/{Mod}"); - - if (texture != null) - { - Child = new Sprite - { - FillMode = FillMode.Fit, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Texture = texture - }; - } - else - { - Child = new ModDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Current = - { - Value = rulesets.GetRuleset(ladderInfo.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods().Where(mod => mod.Acronym == Mod).ToArray() - } - }; - } - } - } } } diff --git a/osu.Game.Tournament/Components/TournamentModDisplay.cs b/osu.Game.Tournament/Components/TournamentModDisplay.cs new file mode 100644 index 0000000000..a22969c20d --- /dev/null +++ b/osu.Game.Tournament/Components/TournamentModDisplay.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 System.Linq; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Framework.Graphics.Textures; +using osu.Game.Rulesets; +using osu.Game.Rulesets.UI; +using osu.Game.Tournament.Models; +using osuTK; + +namespace osu.Game.Tournament.Components +{ + public class TournamentModDisplay : CompositeDrawable + { + public string ModAcronym; + + [Resolved] + private LadderInfo ladderInfo { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + [BackgroundDependencyLoader] + private void load(TextureStore textures) + { + var texture = textures.Get($"mods/{ModAcronym}"); + + if (texture != null) + { + AddInternal(new Sprite + { + FillMode = FillMode.Fit, + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Texture = texture + }); + } + else + { + var mod = rulesets.GetRuleset(ladderInfo.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == ModAcronym); + + AddInternal(new ModIcon(mod) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.5f) + }); + } + } + } +} From 74310da7cf550af33367a4dd0e3d49eebacf32a5 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 25 Jan 2021 13:24:43 +0100 Subject: [PATCH 6186/6909] Change parameter to be singular mod instead of plural --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 2ed99d2fb5..e02709a045 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -32,12 +32,12 @@ namespace osu.Game.Tournament.Components private readonly Bindable currentMatch = new Bindable(); private Box flash; - public TournamentBeatmapPanel(BeatmapInfo beatmap, string mods = null) + public TournamentBeatmapPanel(BeatmapInfo beatmap, string mod = null) { if (beatmap == null) throw new ArgumentNullException(nameof(beatmap)); Beatmap = beatmap; - this.mod = mods; + this.mod = mod; Width = 400; Height = HEIGHT; } From ca08a19c409e4a454a0d6a66d7ce57d2ea845228 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 25 Jan 2021 13:28:46 +0100 Subject: [PATCH 6187/6909] Rename mod to modIcon --- osu.Game.Tournament/Components/TournamentModDisplay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentModDisplay.cs b/osu.Game.Tournament/Components/TournamentModDisplay.cs index a22969c20d..827b3d6a69 100644 --- a/osu.Game.Tournament/Components/TournamentModDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentModDisplay.cs @@ -42,9 +42,9 @@ namespace osu.Game.Tournament.Components } else { - var mod = rulesets.GetRuleset(ladderInfo.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == ModAcronym); + var modIcon = rulesets.GetRuleset(ladderInfo.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == ModAcronym); - AddInternal(new ModIcon(mod) + AddInternal(new ModIcon(modIcon) { Anchor = Anchor.Centre, Origin = Anchor.Centre, From 07bd9013585ebae924cf44c501b079bb9d9b01b0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 25 Jan 2021 14:20:58 +0100 Subject: [PATCH 6188/6909] Add visual test for Tournament Mod Display --- .../TestSceneTournamentModDisplay.cs | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs new file mode 100644 index 0000000000..9689ecd4ae --- /dev/null +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Rulesets; +using osu.Game.Tournament.Components; + +namespace osu.Game.Tournament.Tests.Components +{ + public class TestSceneTournamentModDisplay : TournamentTestScene + { + [Resolved] + private IAPIProvider api { get; set; } + + [Resolved] + private RulesetStore rulesets { get; set; } + + private FillFlowContainer fillFlow; + + private BeatmapInfo beatmap; + + [BackgroundDependencyLoader] + private void load() + { + var req = new GetBeatmapRequest(new BeatmapInfo { OnlineBeatmapID = 490154 }); + req.Success += success; + api.Queue(req); + + Add(fillFlow = new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Direction = FillDirection.Full, + Spacing = new osuTK.Vector2(10) + }); + } + + [Test] + public void TestModDisplay() + { + AddUntilStep("beatmap is available", () => beatmap != null); + AddStep("add maps with available mods for ruleset", () => displayForRuleset(Ladder.Ruleset.Value.ID ?? 0)); + } + + private void displayForRuleset(int rulesetId) + { + fillFlow.Clear(); + var mods = rulesets.GetRuleset(rulesetId).CreateInstance().GetAllMods(); + + foreach (var mod in mods) + { + fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym)); + } + } + + private void success(APIBeatmap apiBeatmap) => beatmap = apiBeatmap.ToBeatmap(rulesets); + } +} From 5e0ccb6c91d3e1d55968882b80d3ba0eca1971a0 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 25 Jan 2021 14:21:22 +0100 Subject: [PATCH 6189/6909] Remove unncessary test step --- .../TestSceneTournamentModDisplay.cs | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs index 9689ecd4ae..b4d9fa4222 100644 --- a/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.cs +++ b/osu.Game.Tournament.Tests/Components/TestSceneTournamentModDisplay.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 NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -43,24 +42,19 @@ namespace osu.Game.Tournament.Tests.Components }); } - [Test] - public void TestModDisplay() + private void success(APIBeatmap apiBeatmap) { - AddUntilStep("beatmap is available", () => beatmap != null); - AddStep("add maps with available mods for ruleset", () => displayForRuleset(Ladder.Ruleset.Value.ID ?? 0)); - } - - private void displayForRuleset(int rulesetId) - { - fillFlow.Clear(); - var mods = rulesets.GetRuleset(rulesetId).CreateInstance().GetAllMods(); + beatmap = apiBeatmap.ToBeatmap(rulesets); + var mods = rulesets.GetRuleset(Ladder.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods(); foreach (var mod in mods) { - fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym)); + fillFlow.Add(new TournamentBeatmapPanel(beatmap, mod.Acronym) + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre + }); } } - - private void success(APIBeatmap apiBeatmap) => beatmap = apiBeatmap.ToBeatmap(rulesets); } } From 6a85f5ca8bf2a9506ed55d9a60f870b820326e2b Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 25 Jan 2021 14:21:53 +0100 Subject: [PATCH 6190/6909] Add null checks to prevent nullrefexception in automated test --- osu.Game.Tournament/Components/TournamentModDisplay.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Components/TournamentModDisplay.cs b/osu.Game.Tournament/Components/TournamentModDisplay.cs index 827b3d6a69..e91c27345e 100644 --- a/osu.Game.Tournament/Components/TournamentModDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentModDisplay.cs @@ -42,7 +42,15 @@ namespace osu.Game.Tournament.Components } else { - var modIcon = rulesets.GetRuleset(ladderInfo.Ruleset.Value.ID ?? 0).CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == ModAcronym); + var ruleset = rulesets.AvailableRulesets.FirstOrDefault(r => r == ladderInfo.Ruleset.Value); + + if (ruleset == null) + return; + + var modIcon = ruleset.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == ModAcronym); + + if (modIcon == null) + return; AddInternal(new ModIcon(modIcon) { From a741d91aed91f338ffecd3a4596b551c8804b52e Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 25 Jan 2021 14:57:35 +0100 Subject: [PATCH 6191/6909] use null propragtor for Ruleset.Value and rulset instead of null checks --- .../Components/TournamentModDisplay.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentModDisplay.cs b/osu.Game.Tournament/Components/TournamentModDisplay.cs index e91c27345e..369a58858e 100644 --- a/osu.Game.Tournament/Components/TournamentModDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentModDisplay.cs @@ -18,14 +18,11 @@ namespace osu.Game.Tournament.Components { public string ModAcronym; - [Resolved] - private LadderInfo ladderInfo { get; set; } - [Resolved] private RulesetStore rulesets { get; set; } [BackgroundDependencyLoader] - private void load(TextureStore textures) + private void load(TextureStore textures, LadderInfo ladderInfo) { var texture = textures.Get($"mods/{ModAcronym}"); @@ -42,12 +39,8 @@ namespace osu.Game.Tournament.Components } else { - var ruleset = rulesets.AvailableRulesets.FirstOrDefault(r => r == ladderInfo.Ruleset.Value); - - if (ruleset == null) - return; - - var modIcon = ruleset.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == ModAcronym); + var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0); + var modIcon = ruleset?.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == ModAcronym); if (modIcon == null) return; From b036f0165a077bc49cb0fd9d7e8713fb6a18fd89 Mon Sep 17 00:00:00 2001 From: Shivam Date: Mon, 25 Jan 2021 15:47:31 +0100 Subject: [PATCH 6192/6909] move value set to constructor and make private readonly --- .../Components/TournamentBeatmapPanel.cs | 3 +-- .../Components/TournamentModDisplay.cs | 11 ++++++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index e02709a045..8cc4566c08 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -123,14 +123,13 @@ namespace osu.Game.Tournament.Components if (!string.IsNullOrEmpty(mod)) { - AddInternal(new TournamentModDisplay + AddInternal(new TournamentModDisplay(mod) { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding(10), Width = 60, RelativeSizeAxes = Axes.Y, - ModAcronym = mod }); } } diff --git a/osu.Game.Tournament/Components/TournamentModDisplay.cs b/osu.Game.Tournament/Components/TournamentModDisplay.cs index 369a58858e..3df8550667 100644 --- a/osu.Game.Tournament/Components/TournamentModDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentModDisplay.cs @@ -16,15 +16,20 @@ namespace osu.Game.Tournament.Components { public class TournamentModDisplay : CompositeDrawable { - public string ModAcronym; + private readonly string modAcronym; [Resolved] private RulesetStore rulesets { get; set; } + public TournamentModDisplay(string mod) + { + modAcronym = mod; + } + [BackgroundDependencyLoader] private void load(TextureStore textures, LadderInfo ladderInfo) { - var texture = textures.Get($"mods/{ModAcronym}"); + var texture = textures.Get($"mods/{modAcronym}"); if (texture != null) { @@ -40,7 +45,7 @@ namespace osu.Game.Tournament.Components else { var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0); - var modIcon = ruleset?.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == ModAcronym); + var modIcon = ruleset?.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == modAcronym); if (modIcon == null) return; From a4a7f0c5787a1fd0a5470a8af72c1ac5211e7d1b Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 25 Jan 2021 19:05:16 +0100 Subject: [PATCH 6193/6909] Address CI inspections. --- osu.Game/IO/StableStorage.cs | 12 ++++++------ osu.Game/OsuGame.cs | 1 - 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index c7ca37a163..85af92621b 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -17,26 +17,26 @@ namespace osu.Game.IO private const string stable_songs_path = "Songs"; private readonly DesktopGameHost host; - private string songs_path; + private readonly string songsPath; public StableStorage(string path, DesktopGameHost host) : base(path, host) { this.host = host; - songs_path = locateSongsDirectory(); + songsPath = locateSongsDirectory(); } /// /// Returns a pointing to the osu-stable Songs directory. /// - public Storage GetSongStorage() => new DesktopStorage(songs_path, host); + public Storage GetSongStorage() => new DesktopStorage(songsPath, host); private string locateSongsDirectory() { var configFile = GetStream(GetFiles(".", "osu!.*.cfg").First()); var textReader = new StreamReader(configFile); - var songs_directory_path = Path.Combine(BasePath, stable_songs_path); + var songsDirectoryPath = Path.Combine(BasePath, stable_songs_path); while (!textReader.EndOfStream) { @@ -46,13 +46,13 @@ namespace osu.Game.IO { var directory = line.Split('=')[1].TrimStart(); if (Path.IsPathFullyQualified(directory)) - songs_directory_path = directory; + songsDirectoryPath = directory; break; } } - return songs_directory_path; + return songsDirectoryPath; } } } diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 399bdda491..78c4d4ccad 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -28,7 +28,6 @@ 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; using osu.Game.Collections; From 9efce5717f40e2ade16c4dee2cb89501d6c27c0d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 25 Jan 2021 22:11:50 +0300 Subject: [PATCH 6194/6909] Fix beatmap listing placeholder disappearing on second time display --- osu.Game/Overlays/BeatmapListingOverlay.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 2f7f21e403..b65eaad0a2 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -176,6 +176,9 @@ namespace osu.Game.Overlays loadingLayer.Hide(); lastFetchDisplayedTime = Time.Current; + if (content == currentContent) + return; + var lastContent = currentContent; if (lastContent != null) From 9312de7c2387f4534090304550f3910c54b98e39 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 25 Jan 2021 23:40:26 +0300 Subject: [PATCH 6195/6909] Move online beatmap listing overlay to separate test scene --- ...ingOverlay.cs => TestSceneOnlineBeatmapListingOverlay.cs} | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) rename osu.Game.Tests/Visual/Online/{TestSceneBeatmapListingOverlay.cs => TestSceneOnlineBeatmapListingOverlay.cs} (80%) diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs similarity index 80% rename from osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs rename to osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs index 6cb1687d1f..fe1701a554 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneOnlineBeatmapListingOverlay.cs @@ -6,13 +6,14 @@ using NUnit.Framework; namespace osu.Game.Tests.Visual.Online { - public class TestSceneBeatmapListingOverlay : OsuTestScene + [Description("uses online API")] + public class TestSceneOnlineBeatmapListingOverlay : OsuTestScene { protected override bool UseOnlineAPI => true; private readonly BeatmapListingOverlay overlay; - public TestSceneBeatmapListingOverlay() + public TestSceneOnlineBeatmapListingOverlay() { Add(overlay = new BeatmapListingOverlay()); } From 75d6dbdbb7290c215505e408f15781b96128d929 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 25 Jan 2021 22:18:23 +0300 Subject: [PATCH 6196/6909] Fix beatmap listing placeholder potentially getting disposed --- osu.Game/Overlays/BeatmapListingOverlay.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 0c9c995dd6..2f7f21e403 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -180,18 +180,24 @@ namespace osu.Game.Overlays if (lastContent != null) { - lastContent.FadeOut(100, Easing.OutQuint).Expire(); + lastContent.FadeOut(100, Easing.OutQuint); // Consider the case when the new content is smaller than the last content. // 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); + + // the content may be reused again (e.g. notFoundContent), clear Y-axis bypass for displaying back properly. + lastContent.BypassAutoSizeAxes = Axes.None; + }); } if (!content.IsAlive) panelTarget.Add(content); - content.FadeIn(200, Easing.OutQuint); + content.FadeInFromZero(200, Easing.OutQuint); currentContent = content; } From c317d6016967a321c70d02ee81d012129e8b5767 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 25 Jan 2021 23:41:05 +0300 Subject: [PATCH 6197/6909] Add offline test scene for beatmap listing overlay --- .../Online/TestSceneBeatmapListingOverlay.cs | 81 +++++++++++++++++++ .../API/Requests/Responses/APIBeatmapSet.cs | 2 +- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs diff --git a/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.cs new file mode 100644 index 0000000000..1349264bf9 --- /dev/null +++ b/osu.Game.Tests/Visual/Online/TestSceneBeatmapListingOverlay.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 NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Graphics.Containers; +using osu.Framework.Testing; +using osu.Game.Beatmaps; +using osu.Game.Online.API; +using osu.Game.Online.API.Requests; +using osu.Game.Online.API.Requests.Responses; +using osu.Game.Overlays; +using osu.Game.Overlays.BeatmapListing; +using osu.Game.Rulesets; + +namespace osu.Game.Tests.Visual.Online +{ + public class TestSceneBeatmapListingOverlay : OsuTestScene + { + private readonly List setsForResponse = new List(); + + private BeatmapListingOverlay overlay; + + [BackgroundDependencyLoader] + private void load() + { + Child = overlay = new BeatmapListingOverlay { State = { Value = Visibility.Visible } }; + + ((DummyAPIAccess)API).HandleRequest = req => + { + if (req is SearchBeatmapSetsRequest searchBeatmapSetsRequest) + { + searchBeatmapSetsRequest.TriggerSuccess(new SearchBeatmapSetsResponse + { + BeatmapSets = setsForResponse, + }); + } + }; + } + + [Test] + public void TestNoBeatmapsPlaceholder() + { + AddStep("fetch for 0 beatmaps", () => fetchFor()); + AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + + AddStep("fetch for 1 beatmap", () => fetchFor(CreateBeatmap(Ruleset.Value).BeatmapInfo.BeatmapSet)); + AddUntilStep("placeholder hidden", () => !overlay.ChildrenOfType().Any()); + + AddStep("fetch for 0 beatmaps", () => fetchFor()); + AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + + // fetch once more to ensure nothing happens in displaying placeholder again when it already is present. + AddStep("fetch for 0 beatmaps again", () => fetchFor()); + AddUntilStep("placeholder shown", () => overlay.ChildrenOfType().SingleOrDefault()?.IsPresent == true); + } + + private void fetchFor(params BeatmapSetInfo[] beatmaps) + { + setsForResponse.Clear(); + setsForResponse.AddRange(beatmaps.Select(b => new TestAPIBeatmapSet(b))); + + // trigger arbitrary change for fetching. + overlay.ChildrenOfType().Single().Query.TriggerChange(); + } + + private class TestAPIBeatmapSet : APIBeatmapSet + { + private readonly BeatmapSetInfo beatmapSet; + + public TestAPIBeatmapSet(BeatmapSetInfo beatmapSet) + { + this.beatmapSet = beatmapSet; + } + + public override BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) => beatmapSet; + } + } +} diff --git a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs index bd1800e9f7..45d9c9405f 100644 --- a/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs +++ b/osu.Game/Online/API/Requests/Responses/APIBeatmapSet.cs @@ -81,7 +81,7 @@ namespace osu.Game.Online.API.Requests.Responses [JsonProperty(@"beatmaps")] private IEnumerable beatmaps { get; set; } - public BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) + public virtual BeatmapSetInfo ToBeatmapSet(RulesetStore rulesets) { var beatmapSet = new BeatmapSetInfo { diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index b65eaad0a2..c5cc0a9c85 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -211,7 +211,7 @@ namespace osu.Game.Overlays base.Dispose(isDisposing); } - private class NotFoundDrawable : CompositeDrawable + public class NotFoundDrawable : CompositeDrawable { public NotFoundDrawable() { From 0d8d0d685219e162e385f0bb506735d7fedcb2e0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 26 Jan 2021 01:03:29 +0300 Subject: [PATCH 6198/6909] Apply alternative way of fixing --- osu.Game/Overlays/BeatmapListingOverlay.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index 2f7f21e403..fc90968ec4 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -178,21 +178,18 @@ namespace osu.Game.Overlays var lastContent = currentContent; - if (lastContent != null) + // "not found" placeholder is reused, only remove without disposing through expire. + if (lastContent == notFoundContent) + lastContent.FadeOut(100, Easing.OutQuint).Schedule(() => panelTarget.Remove(lastContent)); + else if (lastContent != null) { - lastContent.FadeOut(100, Easing.OutQuint); + lastContent.FadeOut(100, Easing.OutQuint).Expire(); // Consider the case when the new content is smaller than the last content. // 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); - - // the content may be reused again (e.g. notFoundContent), clear Y-axis bypass for displaying back properly. - lastContent.BypassAutoSizeAxes = Axes.None; - }); + lastContent.Delay(25).Schedule(() => lastContent.BypassAutoSizeAxes = Axes.Y).Then().Schedule(() => panelTarget.Remove(lastContent)); } if (!content.IsAlive) From 3307e8357ff6f919ff649c6e8036b12c5d4ffb77 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Tue, 26 Jan 2021 00:36:32 -0500 Subject: [PATCH 6199/6909] DrawableStoryboardSample event method override for SamplePlaybackDisabledChanged --- osu.Game/Skinning/PausableSkinnableSound.cs | 46 +++++++++---------- .../Drawables/DrawableStoryboardSample.cs | 14 +++--- 2 files changed, 29 insertions(+), 31 deletions(-) diff --git a/osu.Game/Skinning/PausableSkinnableSound.cs b/osu.Game/Skinning/PausableSkinnableSound.cs index d8149e76c0..e2794938ad 100644 --- a/osu.Game/Skinning/PausableSkinnableSound.cs +++ b/osu.Game/Skinning/PausableSkinnableSound.cs @@ -18,14 +18,6 @@ namespace osu.Game.Skinning protected bool RequestedPlaying { get; private set; } - /// - /// Whether this is affected by - /// a higher-level 's state changes. - /// By default only looping samples are started/stopped on sample disable - /// to prevent one-time samples from cutting off abruptly. - /// - protected virtual bool AffectedBySamplePlaybackDisable => Looping; - public PausableSkinnableSound() { } @@ -51,24 +43,28 @@ namespace osu.Game.Skinning if (samplePlaybackDisabler != null) { samplePlaybackDisabled.BindTo(samplePlaybackDisabler.SamplePlaybackDisabled); - samplePlaybackDisabled.BindValueChanged(disabled => + samplePlaybackDisabled.BindValueChanged(SamplePlaybackDisabledChanged); + } + } + + protected virtual void SamplePlaybackDisabledChanged(ValueChangedEvent disabled) + { + if (!RequestedPlaying) return; + + // let non-looping samples that have already been started play out to completion (sounds better than abruptly cutting off). + if (!Looping) return; + + cancelPendingStart(); + + if (disabled.NewValue) + base.Stop(); + else + { + // schedule so we don't start playing a sample which is no longer alive. + scheduledStart = Schedule(() => { - if (!RequestedPlaying) return; - if (!AffectedBySamplePlaybackDisable) return; - - cancelPendingStart(); - - if (disabled.NewValue) - base.Stop(); - else - { - // schedule so we don't start playing a sample which is no longer alive. - scheduledStart = Schedule(() => - { - if (RequestedPlaying) - base.Play(); - }); - } + if (RequestedPlaying) + base.Play(); }); } } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 5a800a71fd..db8428f062 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -21,12 +21,6 @@ namespace osu.Game.Storyboards.Drawables public override bool RemoveWhenNotAlive => false; - /// - /// Contrary to , all s are affected - /// by sample disables, as they are oftentimes longer-running sound effects. This also matches stable behaviour. - /// - protected override bool AffectedBySamplePlaybackDisable => true; - public DrawableStoryboardSample(StoryboardSampleInfo sampleInfo) : base(sampleInfo) { @@ -48,6 +42,14 @@ namespace osu.Game.Storyboards.Drawables } } + protected override void SamplePlaybackDisabledChanged(ValueChangedEvent disabled) + { + if (!RequestedPlaying) return; + + if (disabled.NewValue) + Stop(); + } + protected override void Update() { base.Update(); From ca0242debef0c77bde87d7f4fecdc84f3b65a8b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Jan 2021 15:42:48 +0900 Subject: [PATCH 6200/6909] Tidy up logic to correctly expire in cases where expiry is expected --- osu.Game/Overlays/BeatmapListingOverlay.cs | 27 +++++++++++++--------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index fc90968ec4..de566c92cb 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -178,24 +178,29 @@ namespace osu.Game.Overlays var lastContent = currentContent; - // "not found" placeholder is reused, only remove without disposing through expire. - if (lastContent == notFoundContent) - lastContent.FadeOut(100, Easing.OutQuint).Schedule(() => panelTarget.Remove(lastContent)); - else if (lastContent != null) + if (lastContent != null) { - lastContent.FadeOut(100, Easing.OutQuint).Expire(); + var transform = lastContent.FadeOut(100, Easing.OutQuint); - // Consider the case when the new content is smaller than the last content. - // 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)); + if (lastContent == notFoundContent) + { + // not found display may be used multiple times, so don't expire/dispose it. + transform.Schedule(() => panelTarget.Remove(lastContent)); + } + else + { + // Consider the case when the new content is smaller than the last content. + // 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(() => lastContent.Expire()); + } } if (!content.IsAlive) panelTarget.Add(content); - content.FadeInFromZero(200, Easing.OutQuint); + content.FadeInFromZero(200, Easing.OutQuint); currentContent = content; } From 60ae87ec383645e61194bf51f19e94d55a342023 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Jan 2021 16:25:49 +0900 Subject: [PATCH 6201/6909] Add MessagePack package --- osu.Game/osu.Game.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 2b8f81532d..3e971d9d4f 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -22,6 +22,7 @@ + From e4fc6041635c4aebfbb791c6e671324ad9156abf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Jan 2021 16:26:03 +0900 Subject: [PATCH 6202/6909] Setup all multiplayer model classes for MessagePack support --- osu.Game/Online/API/APIMod.cs | 7 ++++++- osu.Game/Online/Multiplayer/MultiplayerRoom.cs | 10 +++++++++- osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs | 7 +++++++ osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 8 +++++++- osu.Game/Online/Rooms/BeatmapAvailability.cs | 6 +++++- 5 files changed, 34 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index c8b76b9685..69ce3825ee 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Humanizer; +using MessagePack; using Newtonsoft.Json; using osu.Framework.Bindables; using osu.Game.Configuration; @@ -13,16 +14,20 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Online.API { + [MessagePackObject] public class APIMod : IMod { [JsonProperty("acronym")] + [Key(0)] public string Acronym { get; set; } [JsonProperty("settings")] + [Key(1)] public Dictionary Settings { get; set; } = new Dictionary(); [JsonConstructor] - private APIMod() + [SerializationConstructor] + public APIMod() { } diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs index 12fcf25ace..c5fa6253ed 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoom.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoom.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using MessagePack; using Newtonsoft.Json; namespace osu.Game.Online.Multiplayer @@ -13,35 +14,42 @@ namespace osu.Game.Online.Multiplayer /// A multiplayer room. /// [Serializable] + [MessagePackObject] public class MultiplayerRoom { /// /// The ID of the room, used for database persistence. /// + [Key(0)] public readonly long RoomID; /// /// The current state of the room (ie. whether it is in progress or otherwise). /// + [Key(1)] public MultiplayerRoomState State { get; set; } /// /// All currently enforced game settings for this room. /// + [Key(2)] public MultiplayerRoomSettings Settings { get; set; } = new MultiplayerRoomSettings(); /// /// All users currently in this room. /// + [Key(3)] public List Users { get; set; } = new List(); /// /// The host of this room, in control of changing room settings. /// + [Key(4)] public MultiplayerRoomUser? Host { get; set; } [JsonConstructor] - public MultiplayerRoom(in long roomId) + [SerializationConstructor] + public MultiplayerRoom(long roomId) { RoomID = roomId; } diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 857b38ea60..0ead5db84c 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -7,22 +7,29 @@ using System; using System.Collections.Generic; using System.Linq; using JetBrains.Annotations; +using MessagePack; using osu.Game.Online.API; namespace osu.Game.Online.Multiplayer { [Serializable] + [MessagePackObject] public class MultiplayerRoomSettings : IEquatable { + [Key(0)] public int BeatmapID { get; set; } + [Key(1)] public int RulesetID { get; set; } + [Key(2)] public string BeatmapChecksum { get; set; } = string.Empty; + [Key(3)] public string Name { get; set; } = "Unnamed room"; [NotNull] + [Key(4)] public IEnumerable Mods { get; set; } = Enumerable.Empty(); public bool Equals(MultiplayerRoomSettings other) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 2590acbc81..b300be9f60 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using MessagePack; using Newtonsoft.Json; using osu.Game.Online.Rooms; using osu.Game.Users; @@ -11,21 +12,26 @@ using osu.Game.Users; namespace osu.Game.Online.Multiplayer { [Serializable] + [MessagePackObject] public class MultiplayerRoomUser : IEquatable { + [Key(0)] public readonly int UserID; + [Key(1)] public MultiplayerUserState State { get; set; } = MultiplayerUserState.Idle; /// /// The availability state of the current beatmap. /// + [Key(2)] public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable(); + [IgnoreMember] public User? User { get; set; } [JsonConstructor] - public MultiplayerRoomUser(in int userId) + public MultiplayerRoomUser(int userId) { UserID = userId; } diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index e7dbc5f436..38bd236718 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using MessagePack; using Newtonsoft.Json; namespace osu.Game.Online.Rooms @@ -9,20 +10,23 @@ namespace osu.Game.Online.Rooms /// /// The local availability information about a certain beatmap for the client. /// + [MessagePackObject] public class BeatmapAvailability : IEquatable { /// /// The beatmap's availability state. /// + [Key(0)] public readonly DownloadState State; /// /// The beatmap's downloading progress, null when not in state. /// + [Key(1)] public readonly double? DownloadProgress; [JsonConstructor] - private BeatmapAvailability(DownloadState state, double? downloadProgress = null) + public BeatmapAvailability(DownloadState state, double? downloadProgress = null) { State = state; DownloadProgress = downloadProgress; From 9537090d28bb29994e5a2902c4091e69bc15856b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Jan 2021 16:39:35 +0900 Subject: [PATCH 6203/6909] Setup all spectator model classes for MessagePack --- osu.Game/Online/Spectator/FrameDataBundle.cs | 4 ++++ osu.Game/Online/Spectator/FrameHeader.cs | 10 +++++++++- osu.Game/Online/Spectator/SpectatorState.cs | 5 +++++ osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 13 +++++++++++++ osu.Game/Rulesets/Replays/ReplayFrame.cs | 4 ++++ 5 files changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Spectator/FrameDataBundle.cs b/osu.Game/Online/Spectator/FrameDataBundle.cs index a8d0434324..0e59cdf4ce 100644 --- a/osu.Game/Online/Spectator/FrameDataBundle.cs +++ b/osu.Game/Online/Spectator/FrameDataBundle.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using MessagePack; using Newtonsoft.Json; using osu.Game.Replays.Legacy; using osu.Game.Scoring; @@ -12,10 +13,13 @@ using osu.Game.Scoring; namespace osu.Game.Online.Spectator { [Serializable] + [MessagePackObject] public class FrameDataBundle { + [Key(0)] public FrameHeader Header { get; set; } + [Key(1)] public IEnumerable Frames { get; set; } public FrameDataBundle(ScoreInfo score, IEnumerable frames) diff --git a/osu.Game/Online/Spectator/FrameHeader.cs b/osu.Game/Online/Spectator/FrameHeader.cs index 135b356eda..adfcbcd95a 100644 --- a/osu.Game/Online/Spectator/FrameHeader.cs +++ b/osu.Game/Online/Spectator/FrameHeader.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using MessagePack; using Newtonsoft.Json; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; @@ -12,31 +13,37 @@ using osu.Game.Scoring; namespace osu.Game.Online.Spectator { [Serializable] + [MessagePackObject] public class FrameHeader { /// /// The current accuracy of the score. /// + [Key(0)] public double Accuracy { get; set; } /// /// The current combo of the score. /// + [Key(1)] public int Combo { get; set; } /// /// The maximum combo achieved up to the current point in time. /// + [Key(2)] public int MaxCombo { get; set; } /// /// Cumulative hit statistics. /// + [Key(3)] public Dictionary Statistics { get; set; } /// /// The time at which this frame was received by the server. /// + [Key(4)] public DateTimeOffset ReceivedTime { get; set; } /// @@ -54,7 +61,8 @@ namespace osu.Game.Online.Spectator } [JsonConstructor] - public FrameHeader(int combo, int maxCombo, double accuracy, Dictionary statistics, DateTimeOffset receivedTime) + [SerializationConstructor] + public FrameHeader(double accuracy, int combo, int maxCombo, Dictionary statistics, DateTimeOffset receivedTime) { Combo = combo; MaxCombo = maxCombo; diff --git a/osu.Game/Online/Spectator/SpectatorState.cs b/osu.Game/Online/Spectator/SpectatorState.cs index 101ce3d5d5..96a875bc14 100644 --- a/osu.Game/Online/Spectator/SpectatorState.cs +++ b/osu.Game/Online/Spectator/SpectatorState.cs @@ -5,18 +5,23 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; +using MessagePack; using osu.Game.Online.API; namespace osu.Game.Online.Spectator { [Serializable] + [MessagePackObject] public class SpectatorState : IEquatable { + [Key(0)] public int? BeatmapID { get; set; } + [Key(1)] public int? RulesetID { get; set; } [NotNull] + [Key(2)] public IEnumerable Mods { get; set; } = Enumerable.Empty(); public bool Equals(SpectatorState other) => BeatmapID == other?.BeatmapID && Mods.SequenceEqual(other?.Mods) && RulesetID == other?.RulesetID; diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index 74bacae9e1..ab9ccda9b9 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.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 MessagePack; using Newtonsoft.Json; using osu.Game.Rulesets.Replays; using osuTK; namespace osu.Game.Replays.Legacy { + [MessagePackObject] public class LegacyReplayFrame : ReplayFrame { [JsonIgnore] + [IgnoreMember] public Vector2 Position => new Vector2(MouseX ?? 0, MouseY ?? 0); + [Key(1)] public float? MouseX; + + [Key(2)] public float? MouseY; [JsonIgnore] + [IgnoreMember] public bool MouseLeft => MouseLeft1 || MouseLeft2; [JsonIgnore] + [IgnoreMember] public bool MouseRight => MouseRight1 || MouseRight2; [JsonIgnore] + [IgnoreMember] public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1); [JsonIgnore] + [IgnoreMember] public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1); [JsonIgnore] + [IgnoreMember] public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2); [JsonIgnore] + [IgnoreMember] public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2); + [Key(3)] public ReplayButtonState ButtonState; public LegacyReplayFrame(double time, float? mouseX, float? mouseY, ReplayButtonState buttonState) diff --git a/osu.Game/Rulesets/Replays/ReplayFrame.cs b/osu.Game/Rulesets/Replays/ReplayFrame.cs index 85e068ae79..7de53211a2 100644 --- a/osu.Game/Rulesets/Replays/ReplayFrame.cs +++ b/osu.Game/Rulesets/Replays/ReplayFrame.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 MessagePack; + namespace osu.Game.Rulesets.Replays { + [MessagePackObject] public class ReplayFrame { + [Key(0)] public double Time; public ReplayFrame() From 20cfa991bffbe1cb4255b6cc45cda39b16cc28f9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Jan 2021 17:41:21 +0900 Subject: [PATCH 6204/6909] Switch clients to MessagePack mode --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 50dc8f661c..3221456e75 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online.Multiplayer { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }) - .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }) + .AddMessagePackProtocol() .Build(); // this is kind of SILLY diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 344b73f3d9..cc866b7ad9 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -121,7 +121,7 @@ namespace osu.Game.Online.Spectator { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }) - .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }) + .AddMessagePackProtocol() .Build(); // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) From 15885c17af58234c63ea154178f2156854e95bd5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Jan 2021 18:07:43 +0900 Subject: [PATCH 6205/6909] Remove unused usings --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 1 - osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 3221456e75..0d779232d0 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -9,7 +9,6 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index cc866b7ad9..dac2131035 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -9,7 +9,6 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; From b573c96c079f9065ee4690131f4677eeeb2998be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Jan 2021 18:59:42 +0900 Subject: [PATCH 6206/6909] Move disconnect logic inside connection loop to ensure previous connection is disposed --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 391658f0d0..cbb91c0832 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -71,14 +71,15 @@ namespace osu.Game.Online.Multiplayer try { - await disconnect(false); - // this token will be valid for the scope of this connection. // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere. var cancellationToken = connectCancelSource.Token; while (api.State.Value == APIState.Online) { + // ensure any previous connection was disposed. + await disconnect(false); + cancellationToken.ThrowIfCancellationRequested(); Logger.Log("Multiplayer client connecting...", LoggingTarget.Network); From a5f3418e561efcf05800d4d2d61f9182091467a9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Jan 2021 19:11:19 +0900 Subject: [PATCH 6207/6909] Avoid tooltip display --- .../Components/TournamentModDisplay.cs | 2 +- osu.Game/Overlays/Mods/ModButton.cs | 16 +++------------- osu.Game/Rulesets/UI/ModIcon.cs | 14 ++++++++++++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentModDisplay.cs b/osu.Game.Tournament/Components/TournamentModDisplay.cs index 3df8550667..fa9ee7edff 100644 --- a/osu.Game.Tournament/Components/TournamentModDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentModDisplay.cs @@ -50,7 +50,7 @@ namespace osu.Game.Tournament.Components if (modIcon == null) return; - AddInternal(new ModIcon(modIcon) + AddInternal(new ModIcon(modIcon, false) { Anchor = Anchor.Centre, Origin = Anchor.Centre, diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index ab8efdabcc..8e0d1f5bbd 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -236,13 +236,13 @@ namespace osu.Game.Overlays.Mods { iconsContainer.AddRange(new[] { - backgroundIcon = new PassThroughTooltipModIcon(Mods[1]) + backgroundIcon = new ModIcon(Mods[1], false) { Origin = Anchor.BottomRight, Anchor = Anchor.BottomRight, Position = new Vector2(1.5f), }, - foregroundIcon = new PassThroughTooltipModIcon(Mods[0]) + foregroundIcon = new ModIcon(Mods[0], false) { Origin = Anchor.BottomRight, Anchor = Anchor.BottomRight, @@ -252,7 +252,7 @@ namespace osu.Game.Overlays.Mods } else { - iconsContainer.Add(foregroundIcon = new PassThroughTooltipModIcon(Mod) + iconsContainer.Add(foregroundIcon = new ModIcon(Mod, false) { Origin = Anchor.Centre, Anchor = Anchor.Centre, @@ -297,15 +297,5 @@ namespace osu.Game.Overlays.Mods Mod = mod; } - - private class PassThroughTooltipModIcon : ModIcon - { - public override string TooltipText => null; - - public PassThroughTooltipModIcon(Mod mod) - : base(mod) - { - } - } } } diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 8ea6c74349..04a2e052fa 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -16,6 +16,9 @@ using osu.Framework.Bindables; namespace osu.Game.Rulesets.UI { + /// + /// Display the specified mod at a fixed size. + /// public class ModIcon : Container, IHasTooltip { public readonly BindableBool Selected = new BindableBool(); @@ -28,9 +31,10 @@ namespace osu.Game.Rulesets.UI private readonly ModType type; - public virtual string TooltipText => mod.IconTooltip; + public virtual string TooltipText => showTooltip ? mod.IconTooltip : null; private Mod mod; + private readonly bool showTooltip; public Mod Mod { @@ -42,9 +46,15 @@ namespace osu.Game.Rulesets.UI } } - public ModIcon(Mod mod) + /// + /// Construct a new instance. + /// + /// The mod to be displayed + /// Whether a tooltip describing the mod should display on hover. + public ModIcon(Mod mod, bool showTooltip = true) { this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); + this.showTooltip = showTooltip; type = mod.Type; From 64a3c712aa8be74bb2d32fddeb55fc364a3cb0fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Jan 2021 19:15:19 +0900 Subject: [PATCH 6208/6909] Rename class and add xmldoc --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 2 +- .../{TournamentModDisplay.cs => TournamentModIcon.cs} | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) rename osu.Game.Tournament/Components/{TournamentModDisplay.cs => TournamentModIcon.cs} (86%) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index 8cc4566c08..d1197b1a61 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -123,7 +123,7 @@ namespace osu.Game.Tournament.Components if (!string.IsNullOrEmpty(mod)) { - AddInternal(new TournamentModDisplay(mod) + AddInternal(new TournamentModIcon(mod) { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, diff --git a/osu.Game.Tournament/Components/TournamentModDisplay.cs b/osu.Game.Tournament/Components/TournamentModIcon.cs similarity index 86% rename from osu.Game.Tournament/Components/TournamentModDisplay.cs rename to osu.Game.Tournament/Components/TournamentModIcon.cs index fa9ee7edff..b53ecc02f8 100644 --- a/osu.Game.Tournament/Components/TournamentModDisplay.cs +++ b/osu.Game.Tournament/Components/TournamentModIcon.cs @@ -14,16 +14,19 @@ using osuTK; namespace osu.Game.Tournament.Components { - public class TournamentModDisplay : CompositeDrawable + /// + /// Mod icon displayed in tournament usages, allowing user overridden graphics. + /// + public class TournamentModIcon : CompositeDrawable { private readonly string modAcronym; [Resolved] private RulesetStore rulesets { get; set; } - public TournamentModDisplay(string mod) + public TournamentModIcon(string modAcronym) { - modAcronym = mod; + this.modAcronym = modAcronym; } [BackgroundDependencyLoader] From 81ab82fafe13df7aa513c5d64f3bb205feac6102 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 26 Jan 2021 19:16:38 +0900 Subject: [PATCH 6209/6909] Tidy up nesting --- .../Components/TournamentModIcon.cs | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentModIcon.cs b/osu.Game.Tournament/Components/TournamentModIcon.cs index b53ecc02f8..43ac92d285 100644 --- a/osu.Game.Tournament/Components/TournamentModIcon.cs +++ b/osu.Game.Tournament/Components/TournamentModIcon.cs @@ -32,9 +32,9 @@ namespace osu.Game.Tournament.Components [BackgroundDependencyLoader] private void load(TextureStore textures, LadderInfo ladderInfo) { - var texture = textures.Get($"mods/{modAcronym}"); + var customTexture = textures.Get($"mods/{modAcronym}"); - if (texture != null) + if (customTexture != null) { AddInternal(new Sprite { @@ -42,24 +42,24 @@ namespace osu.Game.Tournament.Components RelativeSizeAxes = Axes.Both, Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, - Texture = texture + Texture = customTexture }); + + return; } - else + + var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0); + var modIcon = ruleset?.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == modAcronym); + + if (modIcon == null) + return; + + AddInternal(new ModIcon(modIcon, false) { - var ruleset = rulesets.GetRuleset(ladderInfo.Ruleset.Value?.ID ?? 0); - var modIcon = ruleset?.CreateInstance().GetAllMods().FirstOrDefault(mod => mod.Acronym == modAcronym); - - if (modIcon == null) - return; - - AddInternal(new ModIcon(modIcon, false) - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.5f) - }); - } + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.5f) + }); } } } From 8c3b0a316737eb359c4d00e03001ab7167ed0633 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 Jan 2021 22:47:37 +0900 Subject: [PATCH 6210/6909] Fix TaskChain performing the action in-line, add test --- osu.Game.Tests/NonVisual/TaskChainTest.cs | 83 +++++++++++++++++++++++ osu.Game/Utils/TaskChain.cs | 28 ++++++-- 2 files changed, 106 insertions(+), 5 deletions(-) create mode 100644 osu.Game.Tests/NonVisual/TaskChainTest.cs diff --git a/osu.Game.Tests/NonVisual/TaskChainTest.cs b/osu.Game.Tests/NonVisual/TaskChainTest.cs new file mode 100644 index 0000000000..d561fb4c1b --- /dev/null +++ b/osu.Game.Tests/NonVisual/TaskChainTest.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.Threading; +using System.Threading.Tasks; +using NUnit.Framework; +using osu.Game.Utils; + +namespace osu.Game.Tests.NonVisual +{ + [TestFixture] + public class TaskChainTest + { + private TaskChain taskChain; + private int currentTask; + + [SetUp] + public void Setup() + { + taskChain = new TaskChain(); + currentTask = 0; + } + + [Test] + public async Task TestChainedTasksRunSequentially() + { + var task1 = addTask(); + var task2 = addTask(); + var task3 = addTask(); + + task3.mutex.Set(); + task2.mutex.Set(); + task1.mutex.Set(); + + await Task.WhenAll(task1.task, task2.task, task3.task); + + Assert.That(task1.task.Result, Is.EqualTo(1)); + Assert.That(task2.task.Result, Is.EqualTo(2)); + Assert.That(task3.task.Result, Is.EqualTo(3)); + } + + [Test] + public async Task TestChainedTaskWithIntermediateCancelRunsInSequence() + { + var task1 = addTask(); + var task2 = addTask(); + var task3 = addTask(); + + // Cancel task2, allow task3 to complete. + task2.cancellation.Cancel(); + task2.mutex.Set(); + task3.mutex.Set(); + + // Allow task3 to potentially complete. + Thread.Sleep(1000); + + // Allow task1 to complete. + task1.mutex.Set(); + + // Wait on both tasks. + await Task.WhenAll(task1.task, task3.task); + + Assert.That(task1.task.Result, Is.EqualTo(1)); + Assert.That(task2.task.IsCompleted, Is.False); + Assert.That(task3.task.Result, Is.EqualTo(2)); + } + + private (Task task, ManualResetEventSlim mutex, CancellationTokenSource cancellation) addTask() + { + var mutex = new ManualResetEventSlim(false); + var cancellationSource = new CancellationTokenSource(); + var completionSource = new TaskCompletionSource(); + + taskChain.Add(() => + { + mutex.Wait(CancellationToken.None); + completionSource.SetResult(Interlocked.Increment(ref currentTask)); + }, cancellationSource.Token); + + return (completionSource.Task, mutex, cancellationSource); + } + } +} diff --git a/osu.Game/Utils/TaskChain.cs b/osu.Game/Utils/TaskChain.cs index 64d523bd3d..2bc2c00e28 100644 --- a/osu.Game/Utils/TaskChain.cs +++ b/osu.Game/Utils/TaskChain.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Threading; using System.Threading.Tasks; namespace osu.Game.Utils @@ -19,15 +20,32 @@ namespace osu.Game.Utils /// /// Adds a new task to the end of this . /// - /// The task creation function. + /// The action to be executed. + /// The for this task. Does not affect further tasks in the chain. /// The awaitable . - public Task Add(Func taskFunc) + public Task Add(Action action, CancellationToken cancellationToken = default) { lock (currentTaskLock) { - currentTask = currentTask == null - ? taskFunc() - : currentTask.ContinueWith(_ => taskFunc()).Unwrap(); + // Note: Attaching the cancellation token to the continuation could lead to re-ordering of tasks in the chain. + // Therefore, the cancellation token is not used to cancel the continuation but only the run of each task. + if (currentTask == null) + { + currentTask = Task.Run(() => + { + cancellationToken.ThrowIfCancellationRequested(); + action(); + }, CancellationToken.None); + } + else + { + currentTask = currentTask.ContinueWith(_ => + { + cancellationToken.ThrowIfCancellationRequested(); + action(); + }, CancellationToken.None); + } + return currentTask; } } From 085115cba538483ed2bbbfdac848a70a6ffdabb8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 26 Jan 2021 22:49:01 +0900 Subject: [PATCH 6211/6909] Make threading even more thread safe --- osu.Game/Extensions/TaskExtensions.cs | 4 +- .../Multiplayer/StatefulMultiplayerClient.cs | 82 ++++++++++++------- 2 files changed, 55 insertions(+), 31 deletions(-) diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs index 4138c2757a..24f0188cf0 100644 --- a/osu.Game/Extensions/TaskExtensions.cs +++ b/osu.Game/Extensions/TaskExtensions.cs @@ -21,9 +21,9 @@ namespace osu.Game.Extensions /// Whether errors should be logged as errors visible to users, or as debug messages. /// Logging as debug will essentially silence the errors on non-release builds. /// - public static void CatchUnobservedExceptions(this Task task, bool logAsError = false) + public static Task CatchUnobservedExceptions(this Task task, bool logAsError = false) { - task.ContinueWith(t => + return task.ContinueWith(t => { Exception? exception = t.Exception?.AsSingular(); if (logAsError) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 0e736ed7c6..3d8ab4b4c7 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -110,37 +110,49 @@ namespace osu.Game.Online.Multiplayer } private readonly TaskChain joinOrLeaveTaskChain = new TaskChain(); + private CancellationTokenSource? joinCancellationSource; /// /// Joins the for a given API . /// /// The API . - public async Task JoinRoom(Room room) => await joinOrLeaveTaskChain.Add(async () => + public async Task JoinRoom(Room room) { - if (Room != null) - throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + var cancellationSource = new CancellationTokenSource(); - Debug.Assert(room.RoomID.Value != null); - - // Join the server-side room. - var joinedRoom = await JoinRoom(room.RoomID.Value.Value); - Debug.Assert(joinedRoom != null); - - // Populate users. - Debug.Assert(joinedRoom.Users != null); - await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)); - - // Update the stored room (must be done on update thread for thread-safety). await scheduleAsync(() => { - Room = joinedRoom; - apiRoom = room; - playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; - }); + joinCancellationSource?.Cancel(); + joinCancellationSource = cancellationSource; + }, CancellationToken.None); - // Update room settings. - await updateLocalRoomSettings(joinedRoom.Settings); - }); + await joinOrLeaveTaskChain.Add(async () => + { + if (Room != null) + throw new InvalidOperationException("Cannot join a multiplayer room while already in one."); + + Debug.Assert(room.RoomID.Value != null); + + // Join the server-side room. + var joinedRoom = await JoinRoom(room.RoomID.Value.Value); + Debug.Assert(joinedRoom != null); + + // Populate users. + Debug.Assert(joinedRoom.Users != null); + await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)); + + // Update the stored room (must be done on update thread for thread-safety). + await scheduleAsync(() => + { + Room = joinedRoom; + apiRoom = room; + playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; + }, cancellationSource.Token); + + // Update room settings. + await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token); + }, cancellationSource.Token); + } /// /// Joins the with a given ID. @@ -151,14 +163,15 @@ namespace osu.Game.Online.Multiplayer public Task LeaveRoom() { - if (Room == null) - return Task.FromCanceled(new CancellationToken(true)); - // Leaving rooms is expected to occur instantaneously whilst the operation is finalised in the background. // However a few members need to be reset immediately to prevent other components from entering invalid states whilst the operation hasn't yet completed. // For example, if a room was left and the user immediately pressed the "create room" button, then the user could be taken into the lobby if the value of Room is not reset in time. var scheduledReset = scheduleAsync(() => { + // The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled. + // This includes the setting of Room itself along with the initial update of the room settings on join. + joinCancellationSource?.Cancel(); + apiRoom = null; Room = null; CurrentMatchPlayingUserIds.Clear(); @@ -169,7 +182,7 @@ namespace osu.Game.Online.Multiplayer return joinOrLeaveTaskChain.Add(async () => { await scheduledReset; - await LeaveRoomInternal(); + await LeaveRoomInternal().CatchUnobservedExceptions(); }); } @@ -455,7 +468,8 @@ namespace osu.Game.Online.Multiplayer /// This updates both the joined and the respective API . /// /// The new to update from. - private Task updateLocalRoomSettings(MultiplayerRoomSettings settings) => scheduleAsync(() => + /// The to cancel the update. + private Task updateLocalRoomSettings(MultiplayerRoomSettings settings, CancellationToken cancellationToken = default) => scheduleAsync(() => { if (Room == null) return; @@ -473,10 +487,17 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => updatePlaylist(settings, res); + + req.Success += res => + { + if (cancellationToken.IsCancellationRequested) + return; + + updatePlaylist(settings, res); + }; api.Queue(req); - }); + }, cancellationToken); private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) { @@ -524,12 +545,15 @@ namespace osu.Game.Online.Multiplayer CurrentMatchPlayingUserIds.Remove(userId); } - private Task scheduleAsync(Action action) + private Task scheduleAsync(Action action, CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource(); Scheduler.Add(() => { + if (cancellationToken.IsCancellationRequested) + return; + try { action(); From 248989b3ebe9de5a1b24341774670be6533825d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 01:20:50 +0900 Subject: [PATCH 6212/6909] wip --- osu.Game.Tests/NonVisual/TaskChainTest.cs | 38 ++++++++++++++++-- .../Multiplayer/StatefulMultiplayerClient.cs | 2 +- osu.Game/Utils/TaskChain.cs | 39 +++++++------------ 3 files changed, 51 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tests/NonVisual/TaskChainTest.cs b/osu.Game.Tests/NonVisual/TaskChainTest.cs index d561fb4c1b..0a56468818 100644 --- a/osu.Game.Tests/NonVisual/TaskChainTest.cs +++ b/osu.Game.Tests/NonVisual/TaskChainTest.cs @@ -4,6 +4,7 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; +using osu.Game.Extensions; using osu.Game.Utils; namespace osu.Game.Tests.NonVisual @@ -13,14 +14,22 @@ namespace osu.Game.Tests.NonVisual { private TaskChain taskChain; private int currentTask; + private CancellationTokenSource globalCancellationToken; [SetUp] public void Setup() { + globalCancellationToken = new CancellationTokenSource(); taskChain = new TaskChain(); currentTask = 0; } + [TearDown] + public void TearDown() + { + globalCancellationToken?.Cancel(); + } + [Test] public async Task TestChainedTasksRunSequentially() { @@ -65,17 +74,40 @@ namespace osu.Game.Tests.NonVisual Assert.That(task3.task.Result, Is.EqualTo(2)); } + [Test] + public async Task TestChainedTaskDoesNotCompleteBeforeChildTasks() + { + var mutex = new ManualResetEventSlim(false); + + var task = taskChain.Add(async () => + { + await Task.Run(() => mutex.Wait(globalCancellationToken.Token)).CatchUnobservedExceptions(); + }); + + // Allow task to potentially complete + Thread.Sleep(1000); + + Assert.That(task.IsCompleted, Is.False); + + // Allow the task to complete. + mutex.Set(); + + await task; + } + private (Task task, ManualResetEventSlim mutex, CancellationTokenSource cancellation) addTask() { var mutex = new ManualResetEventSlim(false); - var cancellationSource = new CancellationTokenSource(); var completionSource = new TaskCompletionSource(); + var cancellationSource = new CancellationTokenSource(); + var token = CancellationTokenSource.CreateLinkedTokenSource(cancellationSource.Token, globalCancellationToken.Token); + taskChain.Add(() => { - mutex.Wait(CancellationToken.None); + mutex.Wait(globalCancellationToken.Token); completionSource.SetResult(Interlocked.Increment(ref currentTask)); - }, cancellationSource.Token); + }, token.Token); return (completionSource.Task, mutex, cancellationSource); } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 3d8ab4b4c7..5c6a0d34e0 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -182,7 +182,7 @@ namespace osu.Game.Online.Multiplayer return joinOrLeaveTaskChain.Add(async () => { await scheduledReset; - await LeaveRoomInternal().CatchUnobservedExceptions(); + await LeaveRoomInternal(); }); } diff --git a/osu.Game/Utils/TaskChain.cs b/osu.Game/Utils/TaskChain.cs index 2bc2c00e28..30aea7578f 100644 --- a/osu.Game/Utils/TaskChain.cs +++ b/osu.Game/Utils/TaskChain.cs @@ -14,8 +14,8 @@ namespace osu.Game.Utils /// public class TaskChain { - private readonly object currentTaskLock = new object(); - private Task? currentTask; + private readonly object finalTaskLock = new object(); + private Task? finalTask; /// /// Adds a new task to the end of this . @@ -23,31 +23,22 @@ namespace osu.Game.Utils /// The action to be executed. /// The for this task. Does not affect further tasks in the chain. /// The awaitable . - public Task Add(Action action, CancellationToken cancellationToken = default) + public async Task Add(Action action, CancellationToken cancellationToken = default) { - lock (currentTaskLock) - { - // Note: Attaching the cancellation token to the continuation could lead to re-ordering of tasks in the chain. - // Therefore, the cancellation token is not used to cancel the continuation but only the run of each task. - if (currentTask == null) - { - currentTask = Task.Run(() => - { - cancellationToken.ThrowIfCancellationRequested(); - action(); - }, CancellationToken.None); - } - else - { - currentTask = currentTask.ContinueWith(_ => - { - cancellationToken.ThrowIfCancellationRequested(); - action(); - }, CancellationToken.None); - } + Task? previousTask; + Task currentTask; - return currentTask; + lock (finalTaskLock) + { + previousTask = finalTask; + finalTask = currentTask = new Task(action, cancellationToken); } + + if (previousTask != null) + await previousTask; + + currentTask.Start(); + await currentTask; } } } From 9f9206726a0cd9cbae347ed26264434f37a86738 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 26 Jan 2021 18:11:54 +0100 Subject: [PATCH 6213/6909] Fix typos. --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++-- osu.Game/Database/ArchiveModelManager.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index a455f676b3..43b2486ac5 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -66,9 +66,9 @@ namespace osu.Game.Beatmaps protected override bool CheckStableDirectoryExists(StableStorage stableStorage) => stableStorage.GetSongStorage().ExistsDirectory("."); - protected override IEnumerable GetStableImportPaths(StableStorage stableStoage) + protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) { - var songStorage = stableStoage.GetSongStorage(); + var songStorage = stableStorage.GetSongStorage(); return songStorage.GetDirectories(".").Select(path => songStorage.GetFullPath(path)); } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 516f70c700..99301b6c68 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -645,8 +645,8 @@ namespace osu.Game.Database /// /// Select paths to import from stable. Default implementation iterates all directories in . /// - protected virtual IEnumerable GetStableImportPaths(StableStorage stableStoage) => stableStoage.GetDirectories(ImportFromStablePath) - .Select(path => stableStoage.GetFullPath(path)); + protected virtual IEnumerable GetStableImportPaths(StableStorage stableStorage) => stableStorage.GetDirectories(ImportFromStablePath) + .Select(path => stableStorage.GetFullPath(path)); /// /// Whether this specified path should be removed after successful import. From 043385f91928204cba2a292469eecfd65fd086a9 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 26 Jan 2021 18:26:01 +0100 Subject: [PATCH 6214/6909] Rename const and fix unintended tabbing. --- osu.Game/Database/ArchiveModelManager.cs | 2 +- osu.Game/IO/StableStorage.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 99301b6c68..ae1608d801 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -646,7 +646,7 @@ namespace osu.Game.Database /// Select paths to import from stable. Default implementation iterates all directories in . /// protected virtual IEnumerable GetStableImportPaths(StableStorage stableStorage) => stableStorage.GetDirectories(ImportFromStablePath) - .Select(path => stableStorage.GetFullPath(path)); + .Select(path => stableStorage.GetFullPath(path)); /// /// Whether this specified path should be removed after successful import. diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index 85af92621b..88a087087e 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -14,7 +14,7 @@ namespace osu.Game.IO /// public class StableStorage : DesktopStorage { - private const string stable_songs_path = "Songs"; + private const string stable_default_songs_path = "Songs"; private readonly DesktopGameHost host; private readonly string songsPath; @@ -36,7 +36,7 @@ namespace osu.Game.IO var configFile = GetStream(GetFiles(".", "osu!.*.cfg").First()); var textReader = new StreamReader(configFile); - var songsDirectoryPath = Path.Combine(BasePath, stable_songs_path); + var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); while (!textReader.EndOfStream) { From 2a2b6f347e7e8af533e3a9053497fe7ee77898eb Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 26 Jan 2021 19:07:05 +0100 Subject: [PATCH 6215/6909] Use a lazy for delegating Songs directory locating until it is actually used. --- osu.Game/IO/StableStorage.cs | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index 88a087087e..ebceba6ce0 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -17,38 +17,43 @@ namespace osu.Game.IO private const string stable_default_songs_path = "Songs"; private readonly DesktopGameHost host; - private readonly string songsPath; + private readonly Lazy songsPath; public StableStorage(string path, DesktopGameHost host) : base(path, host) { this.host = host; - songsPath = locateSongsDirectory(); + songsPath = new Lazy(locateSongsDirectory); } /// /// Returns a pointing to the osu-stable Songs directory. /// - public Storage GetSongStorage() => new DesktopStorage(songsPath, host); + public Storage GetSongStorage() => new DesktopStorage(songsPath.Value, host); private string locateSongsDirectory() { - var configFile = GetStream(GetFiles(".", "osu!.*.cfg").First()); - var textReader = new StreamReader(configFile); - var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); - while (!textReader.EndOfStream) + var configFile = GetFiles(".", "osu!.*.cfg").FirstOrDefault(); + + if (configFile == null) + return songsDirectoryPath; + + using (var textReader = new StreamReader(GetStream(configFile))) { - var line = textReader.ReadLine(); + string line; - if (line?.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase) == true) + while ((line = textReader.ReadLine()) != null) { - var directory = line.Split('=')[1].TrimStart(); - if (Path.IsPathFullyQualified(directory)) - songsDirectoryPath = directory; + if (line.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase)) + { + var directory = line.Split('=')[1].TrimStart(); + if (Path.IsPathFullyQualified(directory)) + songsDirectoryPath = directory; - break; + break; + } } } From 383c40b99268b350e35d955b638781c75e09e682 Mon Sep 17 00:00:00 2001 From: Lucas A Date: Tue, 26 Jan 2021 20:35:42 +0100 Subject: [PATCH 6216/6909] Address remaining reviews suggestions. --- osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 6 +++--- osu.Game/IO/StableStorage.cs | 5 +++-- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 43b2486ac5..4825569ee4 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -64,7 +64,7 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - protected override bool CheckStableDirectoryExists(StableStorage stableStorage) => stableStorage.GetSongStorage().ExistsDirectory("."); + protected override bool StableDirectoryExists(StableStorage stableStorage) => stableStorage.GetSongStorage().ExistsDirectory("."); protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index ae1608d801..fd94660a4b 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -640,10 +640,10 @@ namespace osu.Game.Database /// /// Checks for the existence of an osu-stable directory. /// - protected virtual bool CheckStableDirectoryExists(StableStorage stableStorage) => stableStorage.ExistsDirectory(ImportFromStablePath); + protected virtual bool StableDirectoryExists(StableStorage stableStorage) => stableStorage.ExistsDirectory(ImportFromStablePath); /// - /// Select paths to import from stable. Default implementation iterates all directories in . + /// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in . /// protected virtual IEnumerable GetStableImportPaths(StableStorage stableStorage) => stableStorage.GetDirectories(ImportFromStablePath) .Select(path => stableStorage.GetFullPath(path)); @@ -668,7 +668,7 @@ namespace osu.Game.Database return Task.CompletedTask; } - if (!CheckStableDirectoryExists(stable)) + if (!StableDirectoryExists(stable)) { // This handles situations like when the user does not have a Skins folder Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index ebceba6ce0..f86b18c724 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -35,12 +35,13 @@ namespace osu.Game.IO { var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); - var configFile = GetFiles(".", "osu!.*.cfg").FirstOrDefault(); + var configFile = GetFiles(".", "osu!.*.cfg").SingleOrDefault(); if (configFile == null) return songsDirectoryPath; - using (var textReader = new StreamReader(GetStream(configFile))) + using (var stream = GetStream(configFile)) + using (var textReader = new StreamReader(stream)) { string line; From 4d4d97661e7a8bbe924bdf4d2d5a414f017edb3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 26 Jan 2021 21:26:50 +0100 Subject: [PATCH 6217/6909] Fix connection loop always getting a cancelled token --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index cbb91c0832..319a8f7170 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -71,15 +71,16 @@ namespace osu.Game.Online.Multiplayer try { - // this token will be valid for the scope of this connection. - // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere. - var cancellationToken = connectCancelSource.Token; - while (api.State.Value == APIState.Online) { // ensure any previous connection was disposed. + // this will also create a new cancellation token source. await disconnect(false); + // this token will be valid for the scope of this connection. + // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere. + var cancellationToken = connectCancelSource.Token; + cancellationToken.ThrowIfCancellationRequested(); Logger.Log("Multiplayer client connecting...", LoggingTarget.Network); From 690feb1c1e60aa563cdabad181344fda1dc5d62d Mon Sep 17 00:00:00 2001 From: Mysfit Date: Tue, 26 Jan 2021 23:08:51 -0500 Subject: [PATCH 6218/6909] Allow looping storyboard samples to follow the base samplePlaybackDisabled event logic. --- .../Storyboards/Drawables/DrawableStoryboardSample.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index db8428f062..9041c10640 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -46,8 +46,15 @@ namespace osu.Game.Storyboards.Drawables { if (!RequestedPlaying) return; - if (disabled.NewValue) - Stop(); + // non-looping storyboard samples should be stopped immediately when sample playback is disabled + if (!Looping) + { + if (disabled.NewValue) + Stop(); + } + else + base.SamplePlaybackDisabledChanged(disabled); + } protected override void Update() From ee89aa159cdc905c1c279080ac3d0333b105cfa4 Mon Sep 17 00:00:00 2001 From: Mysfit Date: Tue, 26 Jan 2021 23:12:26 -0500 Subject: [PATCH 6219/6909] Removed blank line --- osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index 9041c10640..fcbffda227 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -54,7 +54,6 @@ namespace osu.Game.Storyboards.Drawables } else base.SamplePlaybackDisabledChanged(disabled); - } protected override void Update() From a800955bb1c9519732f1bf2d72ab3bd0d5840bfb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 19:46:25 +0900 Subject: [PATCH 6220/6909] Add mod validation utilities --- osu.Game.Tests/Mods/ModValidationTest.cs | 64 +++++++++++++ osu.Game.Tests/osu.Game.Tests.csproj | 1 + osu.Game/Utils/ModValidation.cs | 116 +++++++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 osu.Game.Tests/Mods/ModValidationTest.cs create mode 100644 osu.Game/Utils/ModValidation.cs diff --git a/osu.Game.Tests/Mods/ModValidationTest.cs b/osu.Game.Tests/Mods/ModValidationTest.cs new file mode 100644 index 0000000000..c7a7e242e9 --- /dev/null +++ b/osu.Game.Tests/Mods/ModValidationTest.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 Moq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; + +namespace osu.Game.Tests.Mods +{ + [TestFixture] + public class ModValidationTest + { + [Test] + public void TestModIsCompatibleByItself() + { + var mod = new Mock(); + Assert.That(ModValidation.CheckCompatible(new[] { mod.Object })); + } + + [Test] + public void TestIncompatibleThroughTopLevel() + { + var mod1 = new Mock(); + var mod2 = new Mock(); + + mod1.Setup(m => m.IncompatibleMods).Returns(new[] { mod2.Object.GetType() }); + + // Test both orderings. + Assert.That(ModValidation.CheckCompatible(new[] { mod1.Object, mod2.Object }), Is.False); + Assert.That(ModValidation.CheckCompatible(new[] { mod2.Object, mod1.Object }), Is.False); + } + + [Test] + public void TestIncompatibleThroughMultiMod() + { + var mod1 = new Mock(); + + // The nested mod. + var mod2 = new Mock(); + mod2.Setup(m => m.IncompatibleMods).Returns(new[] { mod1.Object.GetType() }); + + var multiMod = new MultiMod(new MultiMod(mod2.Object)); + + // Test both orderings. + Assert.That(ModValidation.CheckCompatible(new[] { multiMod, mod1.Object }), Is.False); + Assert.That(ModValidation.CheckCompatible(new[] { mod1.Object, multiMod }), Is.False); + } + + [Test] + public void TestAllowedThroughMostDerivedType() + { + var mod = new Mock(); + Assert.That(ModValidation.CheckAllowed(new[] { mod.Object }, new[] { mod.Object.GetType() })); + } + + [Test] + public void TestNotAllowedThroughBaseType() + { + var mod = new Mock(); + Assert.That(ModValidation.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index c0c0578391..d29ed94b5f 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -7,6 +7,7 @@ + WinExe diff --git a/osu.Game/Utils/ModValidation.cs b/osu.Game/Utils/ModValidation.cs new file mode 100644 index 0000000000..0c4d58ab2e --- /dev/null +++ b/osu.Game/Utils/ModValidation.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 System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.TypeExtensions; +using osu.Game.Rulesets.Mods; + +#nullable enable + +namespace osu.Game.Utils +{ + /// + /// A set of utilities to validate combinations. + /// + public static class ModValidation + { + /// + /// Checks that all s are compatible with each-other, and that all appear within a set of allowed types. + /// + /// + /// The allowed types must contain exact types for the respective s to be allowed. + /// + /// The s to check. + /// The set of allowed types. + /// Whether all s are compatible with each-other and appear in the set of allowed types. + public static bool CheckCompatibleAndAllowed(IEnumerable combination, IEnumerable allowedTypes) + { + // Prevent multiple-enumeration. + var combinationList = combination as ICollection ?? combination.ToArray(); + return CheckCompatible(combinationList) && CheckAllowed(combinationList, allowedTypes); + } + + /// + /// Checks that all s in a combination are compatible with each-other. + /// + /// The combination to check. + /// Whether all s in the combination are compatible with each-other. + public static bool CheckCompatible(IEnumerable combination) + { + var incompatibleTypes = new HashSet(); + var incomingTypes = new HashSet(); + + foreach (var mod in combination.SelectMany(flattenMod)) + { + // Add the new mod incompatibilities, checking whether any match the existing mod types. + foreach (var t in mod.IncompatibleMods) + { + if (incomingTypes.Contains(t)) + return false; + + incompatibleTypes.Add(t); + } + + // Add the new mod types, checking whether any match the incompatible types. + foreach (var t in mod.GetType().EnumerateBaseTypes()) + { + if (incomingTypes.Contains(t)) + return false; + + incomingTypes.Add(t); + } + } + + return true; + } + + /// + /// Checks that all s in a combination appear within a set of allowed types. + /// + /// + /// The set of allowed types must contain exact types for the respective s to be allowed. + /// + /// The combination to check. + /// The set of allowed types. + /// Whether all s in the combination are allowed. + public static bool CheckAllowed(IEnumerable combination, IEnumerable allowedTypes) + { + var allowedSet = new HashSet(allowedTypes); + + return combination.SelectMany(flattenMod) + .All(m => allowedSet.Contains(m.GetType())); + } + + /// + /// Determines whether a is in a set of incompatible types. + /// + /// + /// A can be incompatible through its most-declared type or any of its base types. + /// + /// The to test. + /// The set of incompatible types. + /// Whether the given is incompatible. + private static bool isModIncompatible(Mod mod, ICollection incompatibleTypes) + => flattenMod(mod) + .SelectMany(m => m.GetType().EnumerateBaseTypes()) + .Any(incompatibleTypes.Contains); + + /// + /// Flattens a , returning a set of s in-place of any s. + /// + /// The to flatten. + /// A set of singular "flattened" s + private static IEnumerable flattenMod(Mod mod) + { + if (mod is MultiMod multi) + { + foreach (var m in multi.Mods.SelectMany(flattenMod)) + yield return m; + } + else + yield return mod; + } + } +} From fcfb0d52c2d1d605ac3b8510cdffa01a3714e928 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jan 2021 19:50:16 +0900 Subject: [PATCH 6221/6909] Proposal to use extension method instead of TaskChain class --- osu.Game.Tests/NonVisual/TaskChainTest.cs | 19 ++++++++--- osu.Game/Extensions/TaskExtensions.cs | 39 +++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/NonVisual/TaskChainTest.cs b/osu.Game.Tests/NonVisual/TaskChainTest.cs index 0a56468818..bd4f15a6eb 100644 --- a/osu.Game.Tests/NonVisual/TaskChainTest.cs +++ b/osu.Game.Tests/NonVisual/TaskChainTest.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.Threading; using System.Threading.Tasks; using NUnit.Framework; @@ -12,15 +13,17 @@ namespace osu.Game.Tests.NonVisual [TestFixture] public class TaskChainTest { - private TaskChain taskChain; + private Task taskChain; + private int currentTask; private CancellationTokenSource globalCancellationToken; [SetUp] public void Setup() { + taskChain = Task.CompletedTask; + globalCancellationToken = new CancellationTokenSource(); - taskChain = new TaskChain(); currentTask = 0; } @@ -79,9 +82,15 @@ namespace osu.Game.Tests.NonVisual { var mutex = new ManualResetEventSlim(false); - var task = taskChain.Add(async () => + var task = taskChain.ContinueWithSequential(async () => { - await Task.Run(() => mutex.Wait(globalCancellationToken.Token)).CatchUnobservedExceptions(); + try + { + await Task.Run(() => mutex.Wait(globalCancellationToken.Token)); + } + catch (OperationCanceledException) + { + } }); // Allow task to potentially complete @@ -103,7 +112,7 @@ namespace osu.Game.Tests.NonVisual var cancellationSource = new CancellationTokenSource(); var token = CancellationTokenSource.CreateLinkedTokenSource(cancellationSource.Token, globalCancellationToken.Token); - taskChain.Add(() => + taskChain = taskChain.ContinueWithSequential(() => { mutex.Wait(globalCancellationToken.Token); completionSource.SetResult(Interlocked.Increment(ref currentTask)); diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs index 24f0188cf0..fd0274f39e 100644 --- a/osu.Game/Extensions/TaskExtensions.cs +++ b/osu.Game/Extensions/TaskExtensions.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; @@ -32,5 +33,43 @@ namespace osu.Game.Extensions Logger.Log($"Error running task: {exception}", LoggingTarget.Runtime, LogLevel.Debug); }, TaskContinuationOptions.NotOnRanToCompletion); } + + public static Task ContinueWithSequential(this Task task, Action continuationFunction, CancellationToken cancellationToken = default) + { + return task.ContinueWithSequential(() => Task.Run(continuationFunction, cancellationToken), cancellationToken); + } + + public static Task ContinueWithSequential(this Task task, Func continuationFunction, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + + task.ContinueWith(t => + { + if (cancellationToken.IsCancellationRequested) + { + tcs.SetCanceled(); + } + else + { + continuationFunction().ContinueWith(t2 => + { + if (cancellationToken.IsCancellationRequested || t2.IsCanceled) + { + tcs.TrySetCanceled(); + } + else if (t2.IsFaulted) + { + tcs.TrySetException(t2.Exception); + } + else + { + tcs.TrySetResult(true); + } + }, cancellationToken: default); + } + }, cancellationToken: default); + + return tcs.Task; + } } } From f2fa51bf5e31e79c799fcf8fd17cd785280cde94 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 19:59:28 +0900 Subject: [PATCH 6222/6909] Make ModSections overrideable --- osu.Game/Overlays/Mods/ModSection.cs | 18 +++++----- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 33 +++++++++++++++---- .../Mods/Sections/AutomationSection.cs | 19 ----------- .../Mods/Sections/ConversionSection.cs | 19 ----------- .../Sections/DifficultyIncreaseSection.cs | 19 ----------- .../Sections/DifficultyReductionSection.cs | 19 ----------- osu.Game/Overlays/Mods/Sections/FunSection.cs | 19 ----------- 7 files changed, 34 insertions(+), 112 deletions(-) delete mode 100644 osu.Game/Overlays/Mods/Sections/AutomationSection.cs delete mode 100644 osu.Game/Overlays/Mods/Sections/ConversionSection.cs delete mode 100644 osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs delete mode 100644 osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs delete mode 100644 osu.Game/Overlays/Mods/Sections/FunSection.cs diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 573d1e5355..d70013602e 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -11,26 +11,23 @@ using System; using System.Linq; using System.Collections.Generic; using System.Threading; +using Humanizer; using osu.Framework.Input.Events; using osu.Game.Graphics; namespace osu.Game.Overlays.Mods { - public abstract class ModSection : Container + public class ModSection : Container { private readonly OsuSpriteText headerLabel; public FillFlowContainer ButtonsContainer { get; } public Action Action; - protected abstract Key[] ToggleKeys { get; } - public abstract ModType ModType { get; } - public string Header - { - get => headerLabel.Text; - set => headerLabel.Text = value; - } + public Key[] ToggleKeys; + + public readonly ModType ModType; public IEnumerable SelectedMods => buttons.Select(b => b.SelectedMod).Where(m => m != null); @@ -153,7 +150,7 @@ namespace osu.Game.Overlays.Mods button.Deselect(); } - protected ModSection() + public ModSection(ModType type) { AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; @@ -168,7 +165,8 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.TopLeft, Anchor = Anchor.TopLeft, Position = new Vector2(0f, 0f), - Font = OsuFont.GetFont(weight: FontWeight.Bold) + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Text = type.Humanize(LetterCasing.Title) }, ButtonsContainer = new FillFlowContainer { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b93602116b..5775b08ae7 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -19,7 +19,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; -using osu.Game.Overlays.Mods.Sections; using osu.Game.Rulesets.Mods; using osu.Game.Screens; using osuTK; @@ -190,13 +189,31 @@ namespace osu.Game.Overlays.Mods Width = content_width, LayoutDuration = 200, LayoutEasing = Easing.OutQuint, - Children = new ModSection[] + Children = new[] { - new DifficultyReductionSection { Action = modButtonPressed }, - new DifficultyIncreaseSection { Action = modButtonPressed }, - new AutomationSection { Action = modButtonPressed }, - new ConversionSection { Action = modButtonPressed }, - new FunSection { Action = modButtonPressed }, + CreateModSection(ModType.DifficultyReduction).With(s => + { + s.ToggleKeys = new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }; + s.Action = modButtonPressed; + }), + CreateModSection(ModType.DifficultyIncrease).With(s => + { + s.ToggleKeys = new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }; + s.Action = modButtonPressed; + }), + CreateModSection(ModType.Automation).With(s => + { + s.ToggleKeys = new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }; + s.Action = modButtonPressed; + }), + CreateModSection(ModType.Conversion).With(s => + { + s.Action = modButtonPressed; + }), + CreateModSection(ModType.Fun).With(s => + { + s.Action = modButtonPressed; + }), } }, } @@ -455,6 +472,8 @@ namespace osu.Game.Overlays.Mods private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); + protected virtual ModSection CreateModSection(ModType type) => new ModSection(type); + #region Disposal protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/Mods/Sections/AutomationSection.cs b/osu.Game/Overlays/Mods/Sections/AutomationSection.cs deleted file mode 100644 index a2d7fec15f..0000000000 --- a/osu.Game/Overlays/Mods/Sections/AutomationSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class AutomationSection : ModSection - { - protected override Key[] ToggleKeys => new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }; - public override ModType ModType => ModType.Automation; - - public AutomationSection() - { - Header = @"Automation"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/ConversionSection.cs b/osu.Game/Overlays/Mods/Sections/ConversionSection.cs deleted file mode 100644 index 24fd8c30dd..0000000000 --- a/osu.Game/Overlays/Mods/Sections/ConversionSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class ConversionSection : ModSection - { - protected override Key[] ToggleKeys => null; - public override ModType ModType => ModType.Conversion; - - public ConversionSection() - { - Header = @"Conversion"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs b/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs deleted file mode 100644 index 0b7ccd1f25..0000000000 --- a/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class DifficultyIncreaseSection : ModSection - { - protected override Key[] ToggleKeys => new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }; - public override ModType ModType => ModType.DifficultyIncrease; - - public DifficultyIncreaseSection() - { - Header = @"Difficulty Increase"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs b/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs deleted file mode 100644 index 508e92508b..0000000000 --- a/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class DifficultyReductionSection : ModSection - { - protected override Key[] ToggleKeys => new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }; - public override ModType ModType => ModType.DifficultyReduction; - - public DifficultyReductionSection() - { - Header = @"Difficulty Reduction"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/FunSection.cs b/osu.Game/Overlays/Mods/Sections/FunSection.cs deleted file mode 100644 index af1f5836b1..0000000000 --- a/osu.Game/Overlays/Mods/Sections/FunSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class FunSection : ModSection - { - protected override Key[] ToggleKeys => null; - public override ModType ModType => ModType.Fun; - - public FunSection() - { - Header = @"Fun"; - } - } -} From a30aecbafeb7d45f87f1061a2ccc71ee757c91cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jan 2021 20:01:21 +0900 Subject: [PATCH 6223/6909] Comment and add xmldoc --- osu.Game.Tests/NonVisual/TaskChainTest.cs | 1 - osu.Game/Extensions/TaskExtensions.cs | 32 +++++++++++++++++------ 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/NonVisual/TaskChainTest.cs b/osu.Game.Tests/NonVisual/TaskChainTest.cs index bd4f15a6eb..342f137dfd 100644 --- a/osu.Game.Tests/NonVisual/TaskChainTest.cs +++ b/osu.Game.Tests/NonVisual/TaskChainTest.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using NUnit.Framework; using osu.Game.Extensions; -using osu.Game.Utils; namespace osu.Game.Tests.NonVisual { diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs index fd0274f39e..62b249b869 100644 --- a/osu.Game/Extensions/TaskExtensions.cs +++ b/osu.Game/Extensions/TaskExtensions.cs @@ -34,32 +34,46 @@ namespace osu.Game.Extensions }, TaskContinuationOptions.NotOnRanToCompletion); } - public static Task ContinueWithSequential(this Task task, Action continuationFunction, CancellationToken cancellationToken = default) - { - return task.ContinueWithSequential(() => Task.Run(continuationFunction, cancellationToken), cancellationToken); - } + /// + /// Add a continuation to be performed only after the attached task has completed. + /// + /// The previous task to be awaited on. + /// The action to run. + /// An optional cancellation token. Will only cancel the provided action, not the sequence. + /// A task representing the provided action. + public static Task ContinueWithSequential(this Task task, Action action, CancellationToken cancellationToken = default) => + task.ContinueWithSequential(() => Task.Run(action, cancellationToken), cancellationToken); + /// + /// Add a continuation to be performed only after the attached task has completed. + /// + /// The previous task to be awaited on. + /// The continuation to run. Generally should be an async function. + /// An optional cancellation token. Will only cancel the provided action, not the sequence. + /// A task representing the provided action. public static Task ContinueWithSequential(this Task task, Func continuationFunction, CancellationToken cancellationToken = default) { var tcs = new TaskCompletionSource(); task.ContinueWith(t => { + // the previous task has finished execution or been cancelled, so we can run the provided continuation. + if (cancellationToken.IsCancellationRequested) { tcs.SetCanceled(); } else { - continuationFunction().ContinueWith(t2 => + continuationFunction().ContinueWith(continuationTask => { - if (cancellationToken.IsCancellationRequested || t2.IsCanceled) + if (cancellationToken.IsCancellationRequested || continuationTask.IsCanceled) { tcs.TrySetCanceled(); } - else if (t2.IsFaulted) + else if (continuationTask.IsFaulted) { - tcs.TrySetException(t2.Exception); + tcs.TrySetException(continuationTask.Exception); } else { @@ -69,6 +83,8 @@ namespace osu.Game.Extensions } }, cancellationToken: default); + // importantly, we are not returning the continuation itself but rather a task which represents its status in sequential execution order. + // this will not be cancelled or completed until the previous task has also. return tcs.Task; } } From 0ff300628eff78d2d4c983d029137425b3209628 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 20:07:22 +0900 Subject: [PATCH 6224/6909] Fix type not being set --- osu.Game/Overlays/Mods/ModSection.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index d70013602e..df4d05daad 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -152,6 +152,8 @@ namespace osu.Game.Overlays.Mods public ModSection(ModType type) { + ModType = type; + AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; From 91d34d86f74ba1466323c3ee5f29c6b9541f1640 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 22:02:23 +0900 Subject: [PATCH 6225/6909] Abstractify ModSelectOverlay --- .../TestSceneModSelectOverlay.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 23 ++++++------------ .../Overlays/Mods/SoloModSelectOverlay.cs | 24 +++++++++++++++++++ osu.Game/Screens/Select/MatchSongSelect.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 5 files changed, 34 insertions(+), 19 deletions(-) create mode 100644 osu.Game/Overlays/Mods/SoloModSelectOverlay.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index bd4010a7f3..b03512ffde 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -265,7 +265,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void checkLabelColor(Func getColour) => AddAssert("check label has expected colour", () => modSelect.MultiplierLabel.Colour.AverageColour == getColour()); - private class TestModSelectOverlay : ModSelectOverlay + private class TestModSelectOverlay : SoloModSelectOverlay { public new Bindable> SelectedMods => base.SelectedMods; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 5775b08ae7..5709ca3b8d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -27,7 +27,7 @@ using osuTK.Input; namespace osu.Game.Overlays.Mods { - public class ModSelectOverlay : WaveOverlayContainer + public abstract class ModSelectOverlay : WaveOverlayContainer { private readonly Func isValidMod; public const float HEIGHT = 510; @@ -60,7 +60,7 @@ namespace osu.Game.Overlays.Mods private SampleChannel sampleOn, sampleOff; - public ModSelectOverlay(Func isValidMod = null) + protected ModSelectOverlay(Func isValidMod = null) { this.isValidMod = isValidMod ?? (m => true); @@ -346,19 +346,6 @@ namespace osu.Game.Overlays.Mods refreshSelectedMods(); } - /// - /// Deselect one or more mods. - /// - /// The types of s which should be deselected. - /// Set to true to bypass animations and update selections immediately. - private void deselectTypes(Type[] modTypes, bool immediate = false) - { - if (modTypes.Length == 0) return; - - foreach (var section in ModSectionsContainer.Children) - section.DeselectTypes(modTypes, immediate); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -458,7 +445,7 @@ namespace osu.Game.Overlays.Mods { if (State.Value == Visibility.Visible) sampleOn?.Play(); - deselectTypes(selectedMod.IncompatibleMods, true); + OnModSelected(selectedMod); if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show(); } @@ -470,6 +457,10 @@ namespace osu.Game.Overlays.Mods refreshSelectedMods(); } + protected virtual void OnModSelected(Mod mod) + { + } + private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); protected virtual ModSection CreateModSection(ModType type) => new ModSection(type); diff --git a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs new file mode 100644 index 0000000000..53d0c9fce9 --- /dev/null +++ b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Overlays.Mods +{ + public class SoloModSelectOverlay : ModSelectOverlay + { + public SoloModSelectOverlay(Func isValidMod = null) + : base(isValidMod) + { + } + + protected override void OnModSelected(Mod mod) + { + base.OnModSelected(mod); + + foreach (var section in ModSectionsContainer.Children) + section.DeselectTypes(mod.IncompatibleMods, true); + } + } +} diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index ed47b5d5ac..280f46f9ab 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Select item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); } - protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(isValidMod); + protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(isValidMod); private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 4fca77a176..ff49dd9f7e 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -301,7 +301,7 @@ namespace osu.Game.Screens.Select } } - protected virtual ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(); + protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(); protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) { From 4019cc38e5e34e8c900bed0e61176fdfa35dbb42 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 22:03:51 +0900 Subject: [PATCH 6226/6909] Allow footer buttons to be customised --- osu.Game/Screens/Select/SongSelect.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index ff49dd9f7e..4af96b7a29 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -263,9 +263,8 @@ namespace osu.Game.Screens.Select if (Footer != null) { - Footer.AddButton(new FooterButtonMods { Current = Mods }, ModSelect); - Footer.AddButton(new FooterButtonRandom { Action = triggerRandom }); - Footer.AddButton(new FooterButtonOptions(), BeatmapOptions); + foreach (var (button, overlay) in CreateFooterButtons()) + Footer.AddButton(button, overlay); BeatmapOptions.AddButton(@"Manage", @"collections", FontAwesome.Solid.Book, colours.Green, () => manageCollectionsDialog?.Show()); BeatmapOptions.AddButton(@"Delete", @"all difficulties", FontAwesome.Solid.Trash, colours.Pink, () => delete(Beatmap.Value.BeatmapSetInfo)); @@ -301,6 +300,13 @@ namespace osu.Game.Screens.Select } } + protected virtual IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() => new (FooterButton, OverlayContainer)[] + { + (new FooterButtonMods { Current = Mods }, ModSelect), + (new FooterButtonRandom { Action = triggerRandom }, null), + (new FooterButtonOptions(), BeatmapOptions) + }; + protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(); protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) From 45e41aaeacf930ce4b5e3b62f85acbc618ce73da Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 22:15:53 +0900 Subject: [PATCH 6227/6909] Initial implementation of freemod selection overlay --- .../TestSceneFreeModSelectOverlay.cs | 24 +++++ osu.Game/Overlays/Mods/ModButton.cs | 8 +- osu.Game/Overlays/Mods/ModSection.cs | 30 +++--- .../Multiplayer/FreeModSelectOverlay.cs | 101 ++++++++++++++++++ .../Multiplayer/MultiplayerMatchSongSelect.cs | 66 +++++++++++- osu.Game/Screens/Select/Footer.cs | 15 ++- 6 files changed, 216 insertions(+), 28 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs create mode 100644 osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs new file mode 100644 index 0000000000..960402df88 --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.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.Framework.Graphics.Containers; +using osu.Game.Overlays.Mods; +using osu.Game.Screens.OnlinePlay.Multiplayer; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneFreeModSelectOverlay : MultiplayerTestScene + { + private ModSelectOverlay overlay; + + [SetUp] + public new void Setup() => Schedule(() => + { + Child = overlay = new FreeModSelectOverlay + { + State = { Value = Visibility.Visible } + }; + }); + } +} diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index ab8efdabcc..9ea4f65eb2 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -174,7 +174,7 @@ namespace osu.Game.Overlays.Mods switch (e.Button) { case MouseButton.Right: - SelectNext(-1); + OnRightClick(e); break; } } @@ -183,10 +183,14 @@ namespace osu.Game.Overlays.Mods protected override bool OnClick(ClickEvent e) { SelectNext(1); - return true; } + protected virtual void OnRightClick(MouseUpEvent e) + { + SelectNext(-1); + } + /// /// Select the next available mod in a specified direction. /// diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index df4d05daad..5de629424b 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -19,7 +19,7 @@ namespace osu.Game.Overlays.Mods { public class ModSection : Container { - private readonly OsuSpriteText headerLabel; + private readonly Drawable header; public FillFlowContainer ButtonsContainer { get; } @@ -47,10 +47,7 @@ namespace osu.Game.Overlays.Mods if (m == null) return new ModButtonEmpty(); - return new ModButton(m) - { - SelectionChanged = Action, - }; + return CreateModButton(m).With(b => b.SelectionChanged = Action); }).ToArray(); modsLoadCts?.Cancel(); @@ -58,7 +55,7 @@ namespace osu.Game.Overlays.Mods if (modContainers.Length == 0) { ModIconsLoaded = true; - headerLabel.Hide(); + header.Hide(); Hide(); return; } @@ -73,7 +70,7 @@ namespace osu.Game.Overlays.Mods buttons = modContainers.OfType().ToArray(); - headerLabel.FadeIn(200); + header.FadeIn(200); this.FadeIn(200); } } @@ -160,16 +157,9 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.TopCentre; Anchor = Anchor.TopCentre; - Children = new Drawable[] + Children = new[] { - headerLabel = new OsuSpriteText - { - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - Position = new Vector2(0f, 0f), - Font = OsuFont.GetFont(weight: FontWeight.Bold), - Text = type.Humanize(LetterCasing.Title) - }, + header = CreateHeader(type.Humanize(LetterCasing.Title)), ButtonsContainer = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -185,5 +175,13 @@ namespace osu.Game.Overlays.Mods }, }; } + + protected virtual ModButton CreateModButton(Mod mod) => new ModButton(mod); + + protected virtual Drawable CreateHeader(string text) => new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Text = text + }; } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs new file mode 100644 index 0000000000..56e74a8460 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs @@ -0,0 +1,101 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.OnlinePlay.Multiplayer +{ + public class FreeModSelectOverlay : ModSelectOverlay + { + protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); + + private class FreeModSection : ModSection + { + private HeaderCheckbox checkbox; + + public FreeModSection(ModType type) + : base(type) + { + } + + protected override ModButton CreateModButton(Mod mod) => new FreeModButton(mod); + + protected override Drawable CreateHeader(string text) => new Container + { + AutoSizeAxes = Axes.Y, + Width = 175, + Child = checkbox = new HeaderCheckbox + { + LabelText = text, + Changed = onCheckboxChanged + } + }; + + private void onCheckboxChanged(bool value) + { + foreach (var button in ButtonsContainer.OfType()) + { + if (value) + // Note: Buttons where only part of the group has an implementation are not fully supported. + button.SelectAt(0); + else + button.Deselect(); + } + } + + protected override void Update() + { + base.Update(); + + // If any of the buttons aren't selected, deselect the checkbox. + foreach (var button in ButtonsContainer.OfType()) + { + if (button.Mods.Any(m => m.HasImplementation) && !button.Selected) + checkbox.Current.Value = false; + } + } + } + + private class HeaderCheckbox : OsuCheckbox + { + public Action Changed; + + protected override void OnUserChange(bool value) + { + base.OnUserChange(value); + Changed?.Invoke(value); + } + } + + private class FreeModButton : ModButton + { + public FreeModButton(Mod mod) + : base(mod) + { + } + + protected override bool OnClick(ClickEvent e) + { + onClick(); + return true; + } + + protected override void OnRightClick(MouseUpEvent e) => onClick(); + + private void onClick() + { + if (Selected) + Deselect(); + else + SelectNext(1); + } + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index ebc06d2445..5917ed3f49 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -6,17 +6,23 @@ using System.Linq; 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.Graphics.UserInterface; using osu.Framework.Logging; using osu.Framework.Screens; using osu.Game.Beatmaps; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; +using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer { @@ -33,6 +39,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private StatefulMultiplayerClient client { get; set; } private LoadingLayer loadingLayer; + private FreeModSelectOverlay freeModSelectOverlay; private WorkingBeatmap initialBeatmap; private RulesetInfo initialRuleset; @@ -43,6 +50,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer public MultiplayerMatchSongSelect() { Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; + + freeModSelectOverlay = new FreeModSelectOverlay(); } [BackgroundDependencyLoader] @@ -52,6 +61,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer initialBeatmap = Beatmap.Value; initialRuleset = Ruleset.Value; initialMods = Mods.Value.ToList(); + + FooterPanels.Add(freeModSelectOverlay); } protected override bool OnStart() @@ -111,8 +122,61 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(isValidMod); + protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(isValidMod); + + protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() + { + var buttons = base.CreateFooterButtons().ToList(); + buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods(), freeModSelectOverlay)); + return buttons; + } private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; } + + public class FooterButtonFreeMods : FooterButton, IHasCurrentValue> + { + public Bindable> Current + { + get => modDisplay.Current; + set => modDisplay.Current = value; + } + + private readonly ModDisplay modDisplay; + + public FooterButtonFreeMods() + { + ButtonContentContainer.Add(modDisplay = new ModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + DisplayUnrankedText = false, + Scale = new Vector2(0.8f), + ExpansionMode = ExpansionMode.AlwaysContracted, + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + SelectedColour = colours.Yellow; + DeselectedColour = SelectedColour.Opacity(0.5f); + Text = @"freemods"; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateModDisplay(), true); + } + + private void updateModDisplay() + { + if (Current.Value?.Count > 0) + modDisplay.FadeIn(); + else + modDisplay.FadeOut(); + } + } } diff --git a/osu.Game/Screens/Select/Footer.cs b/osu.Game/Screens/Select/Footer.cs index 689a11166a..ee13ebda44 100644 --- a/osu.Game/Screens/Select/Footer.cs +++ b/osu.Game/Screens/Select/Footer.cs @@ -28,19 +28,16 @@ namespace osu.Game.Screens.Select private readonly List overlays = new List(); - /// THe button to be added. + /// The button to be added. /// The to be toggled by this button. public void AddButton(FooterButton button, OverlayContainer overlay) { - overlays.Add(overlay); - button.Action = () => showOverlay(overlay); + if (overlay != null) + { + overlays.Add(overlay); + button.Action = () => showOverlay(overlay); + } - AddButton(button); - } - - /// Button to be added. - public void AddButton(FooterButton button) - { button.Hovered = updateModeLight; button.HoverLost = updateModeLight; From 4c256f1fb361f4a64d7cf3fa7a919467c56969b0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 22:23:38 +0900 Subject: [PATCH 6228/6909] Actually populate the playlist item --- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 5917ed3f49..4054d84540 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -62,6 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer initialRuleset = Ruleset.Value; initialMods = Mods.Value.ToList(); + freeModSelectOverlay.SelectedMods.Value = playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToList(); FooterPanels.Add(freeModSelectOverlay); } @@ -76,6 +77,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer item.RequiredMods.Clear(); item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); + item.AllowedMods.Clear(); + item.AllowedMods.AddRange(freeModSelectOverlay.SelectedMods.Value.Select(m => m.CreateCopy())); + // If the client is already in a room, update via the client. // Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation. if (client.Room != null) From c408b46a2158e251d82ce99997d65d2c58c7203f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 22:25:14 +0900 Subject: [PATCH 6229/6909] Add AllowedMods to MultiplayerRoomSettings model --- osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 857b38ea60..ad624f18ed 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -25,13 +25,21 @@ namespace osu.Game.Online.Multiplayer [NotNull] public IEnumerable Mods { get; set; } = Enumerable.Empty(); + [NotNull] + public IEnumerable AllowedMods { get; set; } = Enumerable.Empty(); + public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && BeatmapChecksum == other.BeatmapChecksum && Mods.SequenceEqual(other.Mods) + && AllowedMods.SequenceEqual(other.AllowedMods) && RulesetID == other.RulesetID && Name.Equals(other.Name, StringComparison.Ordinal); - public override string ToString() => $"Name:{Name} Beatmap:{BeatmapID} ({BeatmapChecksum}) Mods:{string.Join(',', Mods)} Ruleset:{RulesetID}"; + public override string ToString() => $"Name:{Name}" + + $" Beatmap:{BeatmapID} ({BeatmapChecksum})" + + $" Mods:{string.Join(',', Mods)}" + + $" AllowedMods:{string.Join(',', AllowedMods)}" + + $" Ruleset:{RulesetID}"; } } From ff8ee379fb9c1684cdd064a32361f27d8fb6105e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 22:27:31 +0900 Subject: [PATCH 6230/6909] Fix possible nullref --- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 4054d84540..70d3d128dd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.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; @@ -38,8 +39,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private StatefulMultiplayerClient client { get; set; } + private readonly FreeModSelectOverlay freeModSelectOverlay; private LoadingLayer loadingLayer; - private FreeModSelectOverlay freeModSelectOverlay; private WorkingBeatmap initialBeatmap; private RulesetInfo initialRuleset; @@ -62,7 +63,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer initialRuleset = Ruleset.Value; initialMods = Mods.Value.ToList(); - freeModSelectOverlay.SelectedMods.Value = playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToList(); + freeModSelectOverlay.SelectedMods.Value = playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); FooterPanels.Add(freeModSelectOverlay); } From b79d1c7b81ef900087608be126f96b6e49e6bdf0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 27 Jan 2021 22:33:03 +0900 Subject: [PATCH 6231/6909] Add mods to footer --- .../OnlinePlay/Multiplayer/FreeModSelectOverlay.cs | 5 +++++ .../Multiplayer/MultiplayerMatchSongSelect.cs | 11 +++++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs index 56e74a8460..10b68ec5a6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs @@ -14,6 +14,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class FreeModSelectOverlay : ModSelectOverlay { + public FreeModSelectOverlay(Func isValidMod = null) + : base(isValidMod) + { + } + protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); private class FreeModSection : ModSection diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 70d3d128dd..86b8f22d34 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -39,6 +39,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private StatefulMultiplayerClient client { get; set; } + private readonly Bindable> freeMods = new Bindable>(Array.Empty()); + private readonly FreeModSelectOverlay freeModSelectOverlay; private LoadingLayer loadingLayer; @@ -52,7 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; - freeModSelectOverlay = new FreeModSelectOverlay(); + freeModSelectOverlay = new FreeModSelectOverlay(isValidMod) { SelectedMods = { BindTarget = freeMods } }; } [BackgroundDependencyLoader] @@ -63,7 +65,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer initialRuleset = Ruleset.Value; initialMods = Mods.Value.ToList(); - freeModSelectOverlay.SelectedMods.Value = playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + freeMods.Value = playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + FooterPanels.Add(freeModSelectOverlay); } @@ -79,7 +82,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); item.AllowedMods.Clear(); - item.AllowedMods.AddRange(freeModSelectOverlay.SelectedMods.Value.Select(m => m.CreateCopy())); + item.AllowedMods.AddRange(freeMods.Value.Select(m => m.CreateCopy())); // If the client is already in a room, update via the client. // Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation. @@ -132,7 +135,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() { var buttons = base.CreateFooterButtons().ToList(); - buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods(), freeModSelectOverlay)); + buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = freeMods }, freeModSelectOverlay)); return buttons; } From 45395cb5e8fcf22d495908f7c77a3b5d90624d98 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 27 Jan 2021 23:00:14 +0900 Subject: [PATCH 6232/6909] 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 9ad5946311..f8ce6befd4 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 3e971d9d4f..97f4320c95 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 4732620085..301e7378a6 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 63f057a525e01e3c58ede431293b124c8b14d70d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 27 Jan 2021 20:45:48 +0300 Subject: [PATCH 6233/6909] Fix dotnet run/publish with runtime specified not working again --- osu.Desktop/osu.Desktop.csproj | 5 +---- osu.Game/osu.Game.csproj | 1 + 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index e201b250d4..cce7907c6c 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -24,16 +24,13 @@ + - - - - diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 97f4320c95..eb541c9de5 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,6 +26,7 @@ + From 2c08ce05fa18828248c1fe45aea502059917c0bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 27 Jan 2021 22:01:56 +0100 Subject: [PATCH 6234/6909] Remove game-local enum [Order] attribute In favour of the newly-added framework one. --- .../BeatmapListing/BeatmapSearchFilterRow.cs | 4 +- .../Overlays/BeatmapListing/SearchLanguage.cs | 2 +- .../Overlays/BeatmapSet/Scores/ScoreTable.cs | 4 +- osu.Game/Rulesets/Ruleset.cs | 6 +-- osu.Game/Rulesets/Scoring/HitResult.cs | 2 +- osu.Game/Utils/OrderAttribute.cs | 52 ------------------- 6 files changed, 9 insertions(+), 61 deletions(-) delete mode 100644 osu.Game/Utils/OrderAttribute.cs diff --git a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs index b429a5277b..01bcbd3244 100644 --- a/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs +++ b/osu.Game/Overlays/BeatmapListing/BeatmapSearchFilterRow.cs @@ -12,7 +12,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osuTK; using Humanizer; -using osu.Game.Utils; +using osu.Framework.Extensions.EnumExtensions; namespace osu.Game.Overlays.BeatmapListing { @@ -80,7 +80,7 @@ namespace osu.Game.Overlays.BeatmapListing if (typeof(T).IsEnum) { - foreach (var val in OrderAttributeUtils.GetValuesInOrder()) + foreach (var val in EnumExtensions.GetValuesInOrder()) AddItem(val); } } diff --git a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs b/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs index eee5d8f7e1..015cee8ce3 100644 --- a/osu.Game/Overlays/BeatmapListing/SearchLanguage.cs +++ b/osu.Game/Overlays/BeatmapListing/SearchLanguage.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.Utils; +using osu.Framework.Utils; namespace osu.Game.Overlays.BeatmapListing { diff --git a/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs b/osu.Game/Overlays/BeatmapSet/Scores/ScoreTable.cs index 324299ccba..ddd1dfa6cd 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.Framework.Extensions.EnumExtensions; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -15,7 +16,6 @@ using osu.Game.Rulesets.Scoring; using osu.Game.Rulesets.UI; using osu.Game.Scoring; using osu.Game.Users.Drawables; -using osu.Game.Utils; using osuTK; using osuTK.Graphics; @@ -105,7 +105,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores var ruleset = scores.First().Ruleset.CreateInstance(); - foreach (var result in OrderAttributeUtils.GetValuesInOrder()) + foreach (var result in EnumExtensions.GetValuesInOrder()) { if (!allScoreStatistics.Contains(result)) continue; diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index b3b3d11ab3..dbc2bd4d01 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -24,9 +24,9 @@ using osu.Game.Skinning; using osu.Game.Users; using JetBrains.Annotations; using osu.Framework.Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Testing; using osu.Game.Screens.Ranking.Statistics; -using osu.Game.Utils; namespace osu.Game.Rulesets { @@ -272,7 +272,7 @@ namespace osu.Game.Rulesets var validResults = GetValidHitResults(); // enumerate over ordered list to guarantee return order is stable. - foreach (var result in OrderAttributeUtils.GetValuesInOrder()) + foreach (var result in EnumExtensions.GetValuesInOrder()) { switch (result) { @@ -298,7 +298,7 @@ namespace osu.Game.Rulesets /// /// is implicitly included. Special types like are ignored even when specified. /// - protected virtual IEnumerable GetValidHitResults() => OrderAttributeUtils.GetValuesInOrder(); + protected virtual IEnumerable GetValidHitResults() => EnumExtensions.GetValuesInOrder(); /// /// Get a display friendly name for the specified result type. diff --git a/osu.Game/Rulesets/Scoring/HitResult.cs b/osu.Game/Rulesets/Scoring/HitResult.cs index 6a3a034fc1..eaa1f95744 100644 --- a/osu.Game/Rulesets/Scoring/HitResult.cs +++ b/osu.Game/Rulesets/Scoring/HitResult.cs @@ -3,7 +3,7 @@ using System.ComponentModel; using System.Diagnostics; -using osu.Game.Utils; +using osu.Framework.Utils; namespace osu.Game.Rulesets.Scoring { diff --git a/osu.Game/Utils/OrderAttribute.cs b/osu.Game/Utils/OrderAttribute.cs deleted file mode 100644 index aded7f9814..0000000000 --- a/osu.Game/Utils/OrderAttribute.cs +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using 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; - - throw new ArgumentException($"Not all values of {nameof(T)} have {nameof(OrderAttribute)} specified."); - }); - } - } - - [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 90a82f986bcd2ba59f0ebc3ccc95e942a3e5665a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Jan 2021 16:20:16 +0900 Subject: [PATCH 6235/6909] Fallback to using json for signalr communication if JIT is unavailable --- .../Online/Multiplayer/MultiplayerClient.cs | 22 +++++++++++++------ .../Spectator/SpectatorStreamingClient.cs | 21 ++++++++++++------ 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 0d779232d0..c10c4dd1b0 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -9,6 +9,8 @@ using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Logging; @@ -64,13 +66,19 @@ namespace osu.Game.Online.Multiplayer if (connection != null) return; - connection = new HubConnectionBuilder() - .WithUrl(endpoint, options => - { - options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); - }) - .AddMessagePackProtocol() - .Build(); + var builder = new HubConnectionBuilder() + .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); + + if (RuntimeInfo.SupportsJIT) + builder.AddMessagePackProtocol(); + else + { + // eventuall we will precompile resolvers for messagepack, but this isn't working currently + // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. + builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); + } + + connection = builder.Build(); // this is kind of SILLY // https://github.com/dotnet/aspnetcore/issues/15198 diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index dac2131035..7a28c179a8 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -9,6 +9,8 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -115,14 +117,19 @@ namespace osu.Game.Online.Spectator if (connection != null) return; - connection = new HubConnectionBuilder() - .WithUrl(endpoint, options => - { - options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); - }) - .AddMessagePackProtocol() - .Build(); + var builder = new HubConnectionBuilder() + .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); + if (RuntimeInfo.SupportsJIT) + builder.AddMessagePackProtocol(); + else + { + // eventuall we will precompile resolvers for messagepack, but this isn't working currently + // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. + builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); + } + + connection = builder.Build(); // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); From c3d40440170e98e026bfceaf3dbd25901704e7ca Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Jan 2021 16:53:56 +0900 Subject: [PATCH 6236/6909] Avoid using Dapper to fix iOS compatibility of beatmap lookup cache --- ...BeatmapManager_BeatmapOnlineLookupQueue.cs | 32 ++++++++++++------- osu.Game/osu.Game.csproj | 1 - 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index e90ccbb805..ea91f2d2e0 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -7,7 +7,6 @@ using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; -using Dapper; using Microsoft.Data.Sqlite; using osu.Framework.Development; using osu.Framework.IO.Network; @@ -154,20 +153,31 @@ namespace osu.Game.Beatmaps { 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); + db.Open(); - if (found != null) + using (var cmd = db.CreateCommand()) { - var status = (BeatmapSetOnlineStatus)found.approved; + cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path"; - beatmap.Status = status; - beatmap.BeatmapSet.Status = status; - beatmap.BeatmapSet.OnlineBeatmapSetID = found.beatmapset_id; - beatmap.OnlineBeatmapID = found.beatmap_id; + cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash)); + cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID)); + cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path)); - LogForModel(set, $"Cached local retrieval for {beatmap}."); - return true; + using (var reader = cmd.ExecuteReader()) + { + if (reader.Read()) + { + var status = (BeatmapSetOnlineStatus)reader.GetByte(2); + + beatmap.Status = status; + beatmap.BeatmapSet.Status = status; + beatmap.BeatmapSet.OnlineBeatmapSetID = reader.GetInt32(0); + beatmap.OnlineBeatmapID = reader.GetInt32(1); + + LogForModel(set, $"Cached local retrieval for {beatmap}."); + return true; + } + } } } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 97f4320c95..bfc5ff302e 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -18,7 +18,6 @@ - From a616688a47a1d2d1c952eb8fe173cd88f8a64c37 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 28 Jan 2021 23:55:03 +0900 Subject: [PATCH 6237/6909] 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 f8ce6befd4..7060e88026 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 97f4320c95..a18eef15fc 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 301e7378a6..48dc01f5de 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -88,7 +88,7 @@ - + From 386f9f78423f205490c817cab468d09a32d12339 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Thu, 28 Jan 2021 22:36:07 +0100 Subject: [PATCH 6238/6909] Fix typos in comments --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 2 +- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index c10c4dd1b0..b13d4fa899 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -73,7 +73,7 @@ namespace osu.Game.Online.Multiplayer builder.AddMessagePackProtocol(); else { - // eventuall we will precompile resolvers for messagepack, but this isn't working currently + // eventually we will precompile resolvers for messagepack, but this isn't working currently // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 7a28c179a8..b95e3f1297 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -124,7 +124,7 @@ namespace osu.Game.Online.Spectator builder.AddMessagePackProtocol(); else { - // eventuall we will precompile resolvers for messagepack, but this isn't working currently + // eventually we will precompile resolvers for messagepack, but this isn't working currently // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); } From da4c207a73b4beb25a59237a2e43ce674ed9ec99 Mon Sep 17 00:00:00 2001 From: Corentin PALLARD Date: Fri, 29 Jan 2021 02:53:26 +0100 Subject: [PATCH 6239/6909] Fix the ctb auto mod speedup in some occasions --- osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index 32e8ab5da7..ae6868aea5 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -45,6 +45,9 @@ namespace osu.Game.Rulesets.Catch.Replays float positionChange = Math.Abs(lastPosition - h.EffectiveX); double timeAvailable = h.StartTime - lastTime; + if (timeAvailable < 0) + return; + // So we can either make it there without a dash or not. // If positionChange is 0, we don't need to move, so speedRequired should also be 0 (could be NaN if timeAvailable is 0 too) // The case where positionChange > 0 and timeAvailable == 0 results in PositiveInfinity which provides expected beheaviour. From d168de0ae32f0877d225efc13942b96a57a9bba2 Mon Sep 17 00:00:00 2001 From: Corentin PALLARD Date: Fri, 29 Jan 2021 03:03:23 +0100 Subject: [PATCH 6240/6909] Formatting --- osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs index ae6868aea5..64ded8e94f 100644 --- a/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs +++ b/osu.Game.Rulesets.Catch/Replays/CatchAutoGenerator.cs @@ -46,7 +46,9 @@ namespace osu.Game.Rulesets.Catch.Replays double timeAvailable = h.StartTime - lastTime; if (timeAvailable < 0) + { return; + } // So we can either make it there without a dash or not. // If positionChange is 0, we don't need to move, so speedRequired should also be 0 (could be NaN if timeAvailable is 0 too) From 449f883be15956273c5271eef5236dbd95776792 Mon Sep 17 00:00:00 2001 From: Firmatorenio Date: Fri, 29 Jan 2021 11:48:51 +0600 Subject: [PATCH 6241/6909] add SV multiplier adjustment to TaikoModDifficultyAdjust --- .../Mods/TaikoModDifficultyAdjust.cs | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs index 56a73ad7df..1d1773fcbb 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -1,11 +1,45 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Linq; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModDifficultyAdjust : ModDifficultyAdjust { + [SettingSource("Slider Velocity", "Adjust a beatmap's set SV", LAST_SETTING_ORDER + 1)] + public BindableNumber SliderVelocity { get; } = new BindableFloat + { + Precision = 0.05f, + MinValue = 0.25f, + MaxValue = 4, + Default = 1, + Value = 1, + }; + + public override string SettingDescription + { + get + { + string sliderVelocity = SliderVelocity.IsDefault ? string.Empty : $"SV {SliderVelocity.Value:N1}"; + + return string.Join(", ", new[] + { + base.SettingDescription, + sliderVelocity + }.Where(s => !string.IsNullOrEmpty(s))); + } + } + + protected override void ApplySettings(BeatmapDifficulty difficulty) + { + base.ApplySettings(difficulty); + + ApplySetting(SliderVelocity, sv => difficulty.SliderMultiplier *= sv); + } } } From 1ec305e10d4ba70ee06a9f21bb3087e0d2e245e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Jan 2021 16:06:57 +0900 Subject: [PATCH 6242/6909] Update TaskChain to use ContinueWithSequential internally It turns out we may still want to use TaskChain for its locking behaviour, so I've made it internally use the refactored version I implemented, while keeping the general structure. --- osu.Game/Utils/TaskChain.cs | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/osu.Game/Utils/TaskChain.cs b/osu.Game/Utils/TaskChain.cs index 30aea7578f..df28faf9fb 100644 --- a/osu.Game/Utils/TaskChain.cs +++ b/osu.Game/Utils/TaskChain.cs @@ -6,6 +6,7 @@ using System; using System.Threading; using System.Threading.Tasks; +using osu.Game.Extensions; namespace osu.Game.Utils { @@ -14,8 +15,9 @@ namespace osu.Game.Utils /// public class TaskChain { - private readonly object finalTaskLock = new object(); - private Task? finalTask; + private readonly object taskLock = new object(); + + private Task lastTaskInChain = Task.CompletedTask; /// /// Adds a new task to the end of this . @@ -23,22 +25,22 @@ namespace osu.Game.Utils /// The action to be executed. /// The for this task. Does not affect further tasks in the chain. /// The awaitable . - public async Task Add(Action action, CancellationToken cancellationToken = default) + public Task Add(Action action, CancellationToken cancellationToken = default) { - Task? previousTask; - Task currentTask; + lock (taskLock) + return lastTaskInChain = lastTaskInChain.ContinueWithSequential(action, cancellationToken); + } - lock (finalTaskLock) - { - previousTask = finalTask; - finalTask = currentTask = new Task(action, cancellationToken); - } - - if (previousTask != null) - await previousTask; - - currentTask.Start(); - await currentTask; + /// + /// Adds a new task to the end of this . + /// + /// The task to be executed. + /// The for this task. Does not affect further tasks in the chain. + /// The awaitable . + public Task Add(Func task, CancellationToken cancellationToken = default) + { + lock (taskLock) + return lastTaskInChain = lastTaskInChain.ContinueWithSequential(task, cancellationToken); } } } From c3aec3bfe43da7c71c243f2188b39bf24c8233cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Jan 2021 16:20:25 +0900 Subject: [PATCH 6243/6909] Revert test changes to test original class/scope Importantly, this removes the call to CatchUnobservedExceptions(), which was outright incorrect (awaiting on the wrong task as a result) in the original test code. --- osu.Game.Tests/NonVisual/TaskChainTest.cs | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/NonVisual/TaskChainTest.cs b/osu.Game.Tests/NonVisual/TaskChainTest.cs index 342f137dfd..d83eaafe20 100644 --- a/osu.Game.Tests/NonVisual/TaskChainTest.cs +++ b/osu.Game.Tests/NonVisual/TaskChainTest.cs @@ -1,28 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Threading; using System.Threading.Tasks; using NUnit.Framework; -using osu.Game.Extensions; +using osu.Game.Utils; namespace osu.Game.Tests.NonVisual { [TestFixture] public class TaskChainTest { - private Task taskChain; - + private TaskChain taskChain; private int currentTask; private CancellationTokenSource globalCancellationToken; [SetUp] public void Setup() { - taskChain = Task.CompletedTask; - globalCancellationToken = new CancellationTokenSource(); + taskChain = new TaskChain(); currentTask = 0; } @@ -81,16 +78,7 @@ namespace osu.Game.Tests.NonVisual { var mutex = new ManualResetEventSlim(false); - var task = taskChain.ContinueWithSequential(async () => - { - try - { - await Task.Run(() => mutex.Wait(globalCancellationToken.Token)); - } - catch (OperationCanceledException) - { - } - }); + var task = taskChain.Add(async () => await Task.Run(() => mutex.Wait(globalCancellationToken.Token))); // Allow task to potentially complete Thread.Sleep(1000); @@ -111,7 +99,7 @@ namespace osu.Game.Tests.NonVisual var cancellationSource = new CancellationTokenSource(); var token = CancellationTokenSource.CreateLinkedTokenSource(cancellationSource.Token, globalCancellationToken.Token); - taskChain = taskChain.ContinueWithSequential(() => + taskChain.Add(() => { mutex.Wait(globalCancellationToken.Token); completionSource.SetResult(Interlocked.Increment(ref currentTask)); From a61444690ea8324719e4f04f1cb6e02b3706bb19 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Jan 2021 16:32:28 +0900 Subject: [PATCH 6244/6909] Remove all usage of CatchUnobservedExceptions This should no longer be required with the recent framework side change that stops a game from crashing on unobserved exceptions (https://github.com/ppy/osu-framework/pull/4171). --- osu.Game/Extensions/TaskExtensions.cs | 36 ------------------- .../Multiplayer/StatefulMultiplayerClient.cs | 3 +- .../OnlinePlay/Multiplayer/Multiplayer.cs | 3 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 8 +---- .../Multiplayer/MultiplayerRoomManager.cs | 3 +- .../Participants/ParticipantPanel.cs | 3 +- 6 files changed, 5 insertions(+), 51 deletions(-) delete mode 100644 osu.Game/Extensions/TaskExtensions.cs diff --git a/osu.Game/Extensions/TaskExtensions.cs b/osu.Game/Extensions/TaskExtensions.cs deleted file mode 100644 index 4138c2757a..0000000000 --- a/osu.Game/Extensions/TaskExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -#nullable enable - -using System; -using System.Threading.Tasks; -using osu.Framework.Extensions.ExceptionExtensions; -using osu.Framework.Logging; - -namespace osu.Game.Extensions -{ - public static class TaskExtensions - { - /// - /// Denote a task which is to be run without local error handling logic, where failure is not catastrophic. - /// Avoids unobserved exceptions from being fired. - /// - /// The task. - /// - /// Whether errors should be logged as errors visible to users, or as debug messages. - /// Logging as debug will essentially silence the errors on non-release builds. - /// - public static void CatchUnobservedExceptions(this Task task, bool logAsError = false) - { - task.ContinueWith(t => - { - Exception? exception = t.Exception?.AsSingular(); - if (logAsError) - Logger.Error(exception, $"Error running task: {exception?.Message ?? "(unknown)"}", LoggingTarget.Runtime, true); - else - Logger.Log($"Error running task: {exception}", LoggingTarget.Runtime, LogLevel.Debug); - }, TaskContinuationOptions.NotOnRanToCompletion); - } - } -} diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index f0e11b2b8b..48194d1f0f 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -15,7 +15,6 @@ using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; -using osu.Game.Extensions; using osu.Game.Online.API; using osu.Game.Online.API.Requests; using osu.Game.Online.API.Requests.Responses; @@ -104,7 +103,7 @@ namespace osu.Game.Online.Multiplayer if (!connected.NewValue && Room != null) { Logger.Log("Connection to multiplayer server was lost.", LoggingTarget.Runtime, LogLevel.Important); - LeaveRoom().CatchUnobservedExceptions(); + LeaveRoom(); } }); } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs index 76f5c74433..ae22e1fcec 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Multiplayer.cs @@ -4,7 +4,6 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Game.Extensions; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -23,7 +22,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.OnResuming(last); if (client.Room != null) - client.ChangeState(MultiplayerUserState.Idle).CatchUnobservedExceptions(true); + client.ChangeState(MultiplayerUserState.Idle); } protected override void UpdatePollingRate(bool isIdle) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 7c4b6d18ec..c071637b9b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -11,7 +11,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; -using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; @@ -237,7 +236,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer // accessing Exception here silences any potential errors from the antecedent task if (t.Exception != null) { - t.CatchUnobservedExceptions(true); // will run immediately. // gameplay was not started due to an exception; unblock button. endOperation(); } @@ -248,11 +246,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } client.ToggleReady() - .ContinueWith(t => - { - t.CatchUnobservedExceptions(true); // will run immediately. - endOperation(); - }); + .ContinueWith(t => endOperation()); void endOperation() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index 61d8896732..65d112a032 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -9,7 +9,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Logging; -using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; @@ -69,7 +68,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.PartRoom(); - multiplayerClient.LeaveRoom().CatchUnobservedExceptions(); + multiplayerClient.LeaveRoom(); // Todo: This is not the way to do this. Basically when we're the only participant and the room closes, there's no way to know if this is actually the case. // This is delayed one frame because upon exiting the match subscreen, multiplayer updates the polling rate and messes with polling. diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index f99655e305..b5533f49cc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -10,7 +10,6 @@ using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; -using osu.Game.Extensions; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -176,7 +175,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants if (Room.Host?.UserID != api.LocalUser.Value.Id) return; - Client.TransferHost(targetUser).CatchUnobservedExceptions(true); + Client.TransferHost(targetUser); }) }; } From 37ef5c70729c6f99cf51cc6841eb4833a728e51c Mon Sep 17 00:00:00 2001 From: Firmatorenio Date: Fri, 29 Jan 2021 15:04:55 +0600 Subject: [PATCH 6245/6909] rename SliderVelocity to ScrollSpeed --- .../Mods/TaikoModDifficultyAdjust.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs index 1d1773fcbb..4006652bd5 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModDifficultyAdjust.cs @@ -11,8 +11,8 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModDifficultyAdjust : ModDifficultyAdjust { - [SettingSource("Slider Velocity", "Adjust a beatmap's set SV", LAST_SETTING_ORDER + 1)] - public BindableNumber SliderVelocity { get; } = new BindableFloat + [SettingSource("Scroll Speed", "Adjust a beatmap's set scroll speed", LAST_SETTING_ORDER + 1)] + public BindableNumber ScrollSpeed { get; } = new BindableFloat { Precision = 0.05f, MinValue = 0.25f, @@ -25,12 +25,12 @@ namespace osu.Game.Rulesets.Taiko.Mods { get { - string sliderVelocity = SliderVelocity.IsDefault ? string.Empty : $"SV {SliderVelocity.Value:N1}"; + string scrollSpeed = ScrollSpeed.IsDefault ? string.Empty : $"Scroll x{ScrollSpeed.Value:N1}"; return string.Join(", ", new[] { base.SettingDescription, - sliderVelocity + scrollSpeed }.Where(s => !string.IsNullOrEmpty(s))); } } @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { base.ApplySettings(difficulty); - ApplySetting(SliderVelocity, sv => difficulty.SliderMultiplier *= sv); + ApplySetting(ScrollSpeed, scroll => difficulty.SliderMultiplier *= scroll); } } } From ab9a3e6dd05bd4a85ab5d1be0e7acc726148e029 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 29 Jan 2021 18:21:22 +0900 Subject: [PATCH 6246/6909] Pass allowed mods and consume on server callback --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index f0e11b2b8b..e5b07ddfb4 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -192,7 +192,8 @@ namespace osu.Game.Online.Multiplayer BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, RulesetID = item.GetOr(existingPlaylistItem).RulesetID, - Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods + Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods, + AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods }); } @@ -502,6 +503,7 @@ namespace osu.Game.Online.Multiplayer var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); var mods = settings.Mods.Select(m => m.ToMod(ruleset)); + var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); PlaylistItem playlistItem = new PlaylistItem { @@ -511,6 +513,7 @@ namespace osu.Game.Online.Multiplayer }; playlistItem.RequiredMods.AddRange(mods); + playlistItem.AllowedMods.AddRange(allowedMods); apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. apiRoom.Playlist.Add(playlistItem); From 18e6afbec06a2111275ba6415eb7d132f52f9ce2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Jan 2021 19:16:10 +0900 Subject: [PATCH 6247/6909] Ensure the item is present before trying to select it --- .../Overlays/Settings/Sections/SkinSection.cs | 33 ++++++++++++++----- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 56c677d5b5..7c8309fd56 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -121,7 +121,22 @@ namespace osu.Game.Overlays.Settings.Sections }); } - private void updateSelectedSkinFromConfig() => dropdownBindable.Value = skinDropdown.Items.Single(s => s.ID == configBindable.Value); + private void updateSelectedSkinFromConfig() + { + int id = configBindable.Value; + + var skin = skinDropdown.Items.FirstOrDefault(s => s.ID == id); + + if (skin == null) + { + // there may be a thread race condition where an item is selected that hasn't yet been added to the dropdown. + // to avoid adding complexity, let's just ensure the item is added so we can perform the selection. + skin = skins.Query(s => s.ID == id); + addItem(skin); + } + + dropdownBindable.Value = skin; + } private void updateItems() { @@ -134,14 +149,14 @@ namespace osu.Game.Overlays.Settings.Sections private void itemUpdated(ValueChangedEvent> weakItem) { if (weakItem.NewValue.TryGetTarget(out var item)) - { - Schedule(() => - { - List newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList(); - sortUserSkins(newDropdownItems); - skinDropdown.Items = newDropdownItems; - }); - } + Schedule(() => addItem(item)); + } + + private void addItem(SkinInfo item) + { + List newDropdownItems = skinDropdown.Items.Where(i => !i.Equals(item)).Append(item).ToList(); + sortUserSkins(newDropdownItems); + skinDropdown.Items = newDropdownItems; } private void itemRemoved(ValueChangedEvent> weakItem) From 16f3d1815f27a344ca2e9a662f960f97ef4db666 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Jan 2021 19:53:56 +0900 Subject: [PATCH 6248/6909] Fix SQLite exception thrown is a beatmap lookup is attempted without an OnlineBeatmapID present MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit It turns out the SQLite API isn't smart enough to handle nullables directly, so we need to help it out a bit. Stops the following from being thrown: ``` System.InvalidOperationException: Value must be set. at Microsoft.Data.Sqlite.SqliteParameter.Bind(sqlite3_stmt stmt) = 3 at at Microsoft.Data.Sqlite.SqliteParameterCollection.Bind(sqlite3_stmt stmt) = 3 at at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader(CommandBehavior behavior) at Microsoft.Data.Sqlite.SqliteCommand.ExecuteReader() at osu.Game.Beatmaps.BeatmapManager.BeatmapOnlineLookupQueue.checkLocalCache(BeatmapSetInfo set, BeatmapInfo beatmap) in /Users/dean/Projects/osu/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs:line 166 = 166 ``` --- osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs index ea91f2d2e0..7c4b344c9e 100644 --- a/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs +++ b/osu.Game/Beatmaps/BeatmapManager_BeatmapOnlineLookupQueue.cs @@ -160,7 +160,7 @@ namespace osu.Game.Beatmaps cmd.CommandText = "SELECT beatmapset_id, beatmap_id, approved FROM osu_beatmaps WHERE checksum = @MD5Hash OR beatmap_id = @OnlineBeatmapID OR filename = @Path"; cmd.Parameters.Add(new SqliteParameter("@MD5Hash", beatmap.MD5Hash)); - cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID)); + cmd.Parameters.Add(new SqliteParameter("@OnlineBeatmapID", beatmap.OnlineBeatmapID ?? (object)DBNull.Value)); cmd.Parameters.Add(new SqliteParameter("@Path", beatmap.Path)); using (var reader = cmd.ExecuteReader()) From f25809d35f9fd084f7486bab8066dc0da6b59347 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Jan 2021 19:55:51 +0900 Subject: [PATCH 6249/6909] Ensure spinners only handle input during their hittable time While this was already being enforced inside of `CheckForResult`, the internal tracking values of rotation were still being incremented as long as the `DrawableSpinner` was present. This resulted in incorrect SPM values being displayed if a user was to start spinning before the object's `StartTime`. Kind of annoying to write a test for (there's no setup for spinners yet) but am willing to do so if that is deemed necessary. Closes https://github.com/ppy/osu/issues/11600. --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 56aedebed3..43012563ae 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -18,6 +18,7 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Scoring; +using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Ranking; using osu.Game.Skinning; @@ -242,6 +243,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.Update(); + HandleUserInput = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime; + if (HandleUserInput) RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); @@ -255,6 +258,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!SpmCounter.IsPresent && RotationTracker.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); + SpmCounter.SetRotation(Result.RateAdjustedRotation); updateBonusScore(); From 5a306dfc2b261e5926817eb03352a11ec846c1ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 29 Jan 2021 20:22:25 +0900 Subject: [PATCH 6250/6909] Fix unused using --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 43012563ae..ab8156f4f2 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -18,7 +18,6 @@ using osu.Game.Rulesets.Osu.Judgements; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Scoring; -using osu.Game.Screens.Edit.Compose.Components; using osu.Game.Screens.Ranking; using osu.Game.Skinning; From d521bfc251195a4eb5560d9685181bf079602d46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 30 Jan 2021 02:35:11 +0900 Subject: [PATCH 6251/6909] Don't directly update HandleUserInput (as it is used by mods) --- .../Objects/Drawables/DrawableSpinner.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index ab8156f4f2..c58f703bef 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -242,10 +242,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { base.Update(); - HandleUserInput = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime; - if (HandleUserInput) - RotationTracker.Tracking = !Result.HasResult && (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); + { + bool isValidSpinningTime = Time.Current >= HitObject.StartTime && Time.Current <= HitObject.EndTime; + bool correctButtonPressed = (OsuActionInputManager?.PressedActions.Any(x => x == OsuAction.LeftButton || x == OsuAction.RightButton) ?? false); + + RotationTracker.Tracking = !Result.HasResult + && correctButtonPressed + && isValidSpinningTime; + } if (spinningSample != null && spinnerFrequencyModulate) spinningSample.Frequency.Value = spinning_sample_modulated_base_frequency + Progress; From ae08ef25437efd3e6aa6661a8d1d627cff338a19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Fri, 29 Jan 2021 20:32:45 +0100 Subject: [PATCH 6252/6909] Reset SPM counter state on DHO application --- .../Skinning/Default/SpinnerSpmCounter.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs index e5952ecf97..69355f624b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs @@ -4,16 +4,21 @@ 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.Utils; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; +using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Osu.Skinning.Default { public class SpinnerSpmCounter : Container { + [Resolved] + private DrawableHitObject drawableSpinner { get; set; } + private readonly OsuSpriteText spmText; public SpinnerSpmCounter() @@ -38,6 +43,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default }; } + protected override void LoadComplete() + { + base.LoadComplete(); + drawableSpinner.HitObjectApplied += resetState; + } + private double spm; public double SpinsPerMinute @@ -82,5 +93,19 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default records.Enqueue(new RotationRecord { Rotation = currentRotation, Time = Time.Current }); } + + private void resetState(DrawableHitObject hitObject) + { + SpinsPerMinute = 0; + records.Clear(); + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + if (drawableSpinner != null) + drawableSpinner.HitObjectApplied -= resetState; + } } } From c3ba92f057349d56c79906bd96ab0de8b67e0fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 30 Jan 2021 16:13:50 +0100 Subject: [PATCH 6253/6909] Set canceled result in scheduleAsync Was holding up the task completion source, and in consequence, potentially the entire task chain. --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 4bec327884..de51a4b117 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -551,7 +551,10 @@ namespace osu.Game.Online.Multiplayer Scheduler.Add(() => { if (cancellationToken.IsCancellationRequested) + { + tcs.SetCanceled(); return; + } try { From 0aaa62efc2d9f2d1e8f6d46632661b0e6ef8db03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 30 Jan 2021 20:55:56 +0100 Subject: [PATCH 6254/6909] Add failing test case --- .../NonVisual/OngoingOperationTrackerTest.cs | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs b/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs index eef9582af9..10216c3339 100644 --- a/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs +++ b/osu.Game.Tests/NonVisual/OngoingOperationTrackerTest.cs @@ -3,8 +3,12 @@ using System; using NUnit.Framework; +using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Screens; using osu.Framework.Testing; +using osu.Game.Screens; using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Visual; @@ -58,5 +62,45 @@ namespace osu.Game.Tests.NonVisual AddStep("end operation", () => operation.Dispose()); AddAssert("operation is ended", () => !operationInProgress.Value); } + + [Test] + public void TestOperationDisposalAfterScreenExit() + { + TestScreenWithTracker screen = null; + OsuScreenStack stack; + IDisposable operation = null; + + AddStep("create screen with tracker", () => + { + Child = stack = new OsuScreenStack + { + RelativeSizeAxes = Axes.Both + }; + + stack.Push(screen = new TestScreenWithTracker()); + }); + AddUntilStep("wait for loaded", () => screen.IsLoaded); + + AddStep("begin operation", () => operation = screen.OngoingOperationTracker.BeginOperation()); + AddAssert("operation in progress", () => screen.OngoingOperationTracker.InProgress.Value); + + AddStep("dispose after screen exit", () => + { + screen.Exit(); + operation.Dispose(); + }); + AddAssert("operation ended", () => !screen.OngoingOperationTracker.InProgress.Value); + } + + private class TestScreenWithTracker : OsuScreen + { + public OngoingOperationTracker OngoingOperationTracker { get; private set; } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = OngoingOperationTracker = new OngoingOperationTracker(); + } + } } } From 96f56d1c942505797f20cf1c166b5452c60ea592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 30 Jan 2021 21:00:13 +0100 Subject: [PATCH 6255/6909] Return tracker lease via UnbindAll() Improves reliability by being fail-safe in case of multiple returns, which can happen if the operation tracker is part of a screen being exited (as is the case with its current primary usage in multiplayer). --- .../Screens/OnlinePlay/OngoingOperationTracker.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs index b7ee84eb9e..9e88fabb3d 100644 --- a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -53,20 +53,15 @@ namespace osu.Game.Screens.OnlinePlay // for extra safety, marshal the end of operation back to the update thread if necessary. Scheduler.Add(() => { - leasedInProgress?.Return(); + // UnbindAll() is purposefully used instead of Return() - the two do roughly the same thing, with one difference: + // the former won't throw if the lease has already been returned before. + // this matters because framework can unbind the lease via the internal UnbindAllBindables(), which is not always detectable + // (it is in the case of disposal, but not in the case of screen exit - at least not cleanly). + leasedInProgress?.UnbindAll(); leasedInProgress = null; }, false); } - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - // base call does an UnbindAllBindables(). - // clean up the leased reference here so that it doesn't get returned twice. - leasedInProgress = null; - } - private class OngoingOperation : IDisposable { private readonly OngoingOperationTracker tracker; From 5f320cd4264ad444fe5cb3f7aa7bef74d530e932 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 30 Jan 2021 21:03:09 +0100 Subject: [PATCH 6256/6909] Move lease check inside schedule Theoretically safer due to avoiding a potential data race (change in `leasedInProgress` between the time of the check and start of schedule execution). --- osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs index 9e88fabb3d..aabeafe460 100644 --- a/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs +++ b/osu.Game/Screens/OnlinePlay/OngoingOperationTracker.cs @@ -47,12 +47,12 @@ namespace osu.Game.Screens.OnlinePlay private void endOperationWithKnownLease(LeasedBindable lease) { - if (lease != leasedInProgress) - return; - // for extra safety, marshal the end of operation back to the update thread if necessary. Scheduler.Add(() => { + if (lease != leasedInProgress) + return; + // UnbindAll() is purposefully used instead of Return() - the two do roughly the same thing, with one difference: // the former won't throw if the lease has already been returned before. // this matters because framework can unbind the lease via the internal UnbindAllBindables(), which is not always detectable From 90ba8ae234bbec60aab5b42f9cd625eb8e2d2f02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 30 Jan 2021 23:39:01 +0100 Subject: [PATCH 6257/6909] Don't part room if join task was cancelled --- .../Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs index 65d112a032..1e57847f04 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerRoomManager.cs @@ -87,7 +87,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { if (t.IsCompletedSuccessfully) Schedule(() => onSuccess?.Invoke(room)); - else + else if (t.IsFaulted) { const string message = "Failed to join multiplayer room."; From d7e5a212134e371838ee342ecb476bdcc0347c6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 15:23:53 +0100 Subject: [PATCH 6258/6909] Add failing test case --- .../Formats/LegacyStoryboardDecoderTest.cs | 20 +++++++++++++++++++ osu.Game.Tests/Resources/animation-types.osb | 9 +++++++++ 2 files changed, 29 insertions(+) create mode 100644 osu.Game.Tests/Resources/animation-types.osb diff --git a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs index 7bee580863..bcde899789 100644 --- a/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs +++ b/osu.Game.Tests/Beatmaps/Formats/LegacyStoryboardDecoderTest.cs @@ -129,5 +129,25 @@ namespace osu.Game.Tests.Beatmaps.Formats Assert.AreEqual(3456, ((StoryboardSprite)background.Elements.Single()).InitialPosition.X); } } + + [Test] + public void TestDecodeOutOfRangeLoopAnimationType() + { + var decoder = new LegacyStoryboardDecoder(); + + using (var resStream = TestResources.OpenResource("animation-types.osb")) + using (var stream = new LineBufferedReader(resStream)) + { + var storyboard = decoder.Decode(stream); + + StoryboardLayer foreground = storyboard.Layers.Single(l => l.Depth == 0); + Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[0]).LoopType); + Assert.AreEqual(AnimationLoopType.LoopOnce, ((StoryboardAnimation)foreground.Elements[1]).LoopType); + Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[2]).LoopType); + Assert.AreEqual(AnimationLoopType.LoopOnce, ((StoryboardAnimation)foreground.Elements[3]).LoopType); + Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[4]).LoopType); + Assert.AreEqual(AnimationLoopType.LoopForever, ((StoryboardAnimation)foreground.Elements[5]).LoopType); + } + } } } diff --git a/osu.Game.Tests/Resources/animation-types.osb b/osu.Game.Tests/Resources/animation-types.osb new file mode 100644 index 0000000000..82233b7d30 --- /dev/null +++ b/osu.Game.Tests/Resources/animation-types.osb @@ -0,0 +1,9 @@ +osu file format v14 + +[Events] +Animation,Foreground,Centre,"forever-string.png",330,240,10,108,LoopForever +Animation,Foreground,Centre,"once-string.png",330,240,10,108,LoopOnce +Animation,Foreground,Centre,"forever-number.png",330,240,10,108,0 +Animation,Foreground,Centre,"once-number.png",330,240,10,108,1 +Animation,Foreground,Centre,"undefined-number.png",330,240,10,108,16 +Animation,Foreground,Centre,"omitted.png",330,240,10,108 From b9a49d5589dfd91e651ad84c0872401e27ab192e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 15:33:07 +0100 Subject: [PATCH 6259/6909] Coerce undefined animation loop types to Forever --- osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs index 9a244c8bb2..b9bf6823b5 100644 --- a/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyStoryboardDecoder.cs @@ -139,7 +139,7 @@ namespace osu.Game.Beatmaps.Formats // this is random as hell but taken straight from osu-stable. frameDelay = Math.Round(0.015 * frameDelay) * 1.186 * (1000 / 60f); - var loopType = split.Length > 8 ? (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), split[8]) : AnimationLoopType.LoopForever; + var loopType = split.Length > 8 ? parseAnimationLoopType(split[8]) : AnimationLoopType.LoopForever; storyboardSprite = new StoryboardAnimation(path, origin, new Vector2(x, y), frameCount, frameDelay, loopType); storyboard.GetLayer(layer).Add(storyboardSprite); break; @@ -341,6 +341,12 @@ namespace osu.Game.Beatmaps.Formats } } + private AnimationLoopType parseAnimationLoopType(string value) + { + var parsed = (AnimationLoopType)Enum.Parse(typeof(AnimationLoopType), value); + return Enum.IsDefined(typeof(AnimationLoopType), parsed) ? parsed : AnimationLoopType.LoopForever; + } + private void handleVariables(string line) { var pair = SplitKeyVal(line, '='); From 81b052b866af6422434f96638dabace022618f95 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 20:18:12 +0100 Subject: [PATCH 6260/6909] Add failing test cases --- .../Rulesets/Mods/ModTimeRampTest.cs | 82 +++++++++++++++++++ osu.Game/Rulesets/Mods/ModTimeRamp.cs | 4 +- 2 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs diff --git a/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs b/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs new file mode 100644 index 0000000000..894648b8b3 --- /dev/null +++ b/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.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 NUnit.Framework; +using osu.Framework.Audio.Track; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Tests.Rulesets.Mods +{ + [TestFixture] + public class ModTimeRampTest + { + private const double start_time = 1000; + private const double duration = 9000; + + private TrackVirtual track; + private IBeatmap beatmap; + + [SetUp] + public void SetUp() + { + track = new TrackVirtual(20_000); + beatmap = new Beatmap + { + HitObjects = + { + new Spinner + { + StartTime = start_time, + Duration = duration + } + } + }; + } + + [TestCase(0, 1)] + [TestCase(start_time, 1)] + [TestCase(start_time + duration * ModTimeRamp.FINAL_RATE_PROGRESS / 2, 1.25)] + [TestCase(start_time + duration * ModTimeRamp.FINAL_RATE_PROGRESS, 1.5)] + [TestCase(start_time + duration, 1.5)] + [TestCase(15000, 1.5)] + public void TestModWindUp(double time, double expectedRate) + { + var mod = new ModWindUp(); + mod.ApplyToBeatmap(beatmap); + mod.ApplyToTrack(track); + + seekTrackAndUpdateMod(mod, time); + + Assert.That(mod.SpeedChange.Value, Is.EqualTo(expectedRate)); + } + + [TestCase(0, 1)] + [TestCase(start_time, 1)] + [TestCase(start_time + duration * ModTimeRamp.FINAL_RATE_PROGRESS / 2, 0.75)] + [TestCase(start_time + duration * ModTimeRamp.FINAL_RATE_PROGRESS, 0.5)] + [TestCase(start_time + duration, 0.5)] + [TestCase(15000, 0.5)] + public void TestModWindDown(double time, double expectedRate) + { + var mod = new ModWindDown + { + FinalRate = { Value = 0.5 } + }; + mod.ApplyToBeatmap(beatmap); + mod.ApplyToTrack(track); + + seekTrackAndUpdateMod(mod, time); + + Assert.That(mod.SpeedChange.Value, Is.EqualTo(expectedRate)); + } + + private void seekTrackAndUpdateMod(ModTimeRamp mod, double time) + { + track.Seek(time); + // update the mod via a fake playfield to re-calculate the current rate. + mod.Update(null); + } + } +} diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 4d43ae73d3..c691339a81 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mods /// /// The point in the beatmap at which the final ramping rate should be reached. /// - private const double final_rate_progress = 0.75f; + public const double FINAL_RATE_PROGRESS = 0.75f; [SettingSource("Initial rate", "The starting speed of the track")] public abstract BindableNumber InitialRate { get; } @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Mods SpeedChange.SetDefault(); beginRampTime = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0; - finalRateTime = final_rate_progress * (lastObject?.GetEndTime() ?? 0); + finalRateTime = FINAL_RATE_PROGRESS * (lastObject?.GetEndTime() ?? 0); } public virtual void Update(Playfield playfield) From 547b3d8bed86d1398da54ea3d6ffcca8f883763b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 20:34:56 +0100 Subject: [PATCH 6261/6909] Fix speed change calculation in time ramp mods --- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index c691339a81..a28a0351a6 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -66,17 +66,18 @@ namespace osu.Game.Rulesets.Mods public virtual void ApplyToBeatmap(IBeatmap beatmap) { - HitObject lastObject = beatmap.HitObjects.LastOrDefault(); - SpeedChange.SetDefault(); - beginRampTime = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0; - finalRateTime = FINAL_RATE_PROGRESS * (lastObject?.GetEndTime() ?? 0); + double firstObjectStart = beatmap.HitObjects.FirstOrDefault()?.StartTime ?? 0; + double lastObjectEnd = beatmap.HitObjects.LastOrDefault()?.GetEndTime() ?? 0; + + beginRampTime = firstObjectStart; + finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart); } public virtual void Update(Playfield playfield) { - applyRateAdjustment((track.CurrentTime - beginRampTime) / finalRateTime); + applyRateAdjustment((track.CurrentTime - beginRampTime) / (finalRateTime - beginRampTime)); } /// From a0de1cbfd07447789c4093d67c10e782e6463725 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 21:09:41 +0100 Subject: [PATCH 6262/6909] Handle no-duration single-object edge case --- .../Rulesets/Mods/ModTimeRampTest.cs | 55 +++++++++++++++---- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 +- 2 files changed, 44 insertions(+), 13 deletions(-) diff --git a/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs b/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs index 894648b8b3..4b9f2181dc 100644 --- a/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs +++ b/osu.Game.Tests/Rulesets/Mods/ModTimeRampTest.cs @@ -16,23 +16,11 @@ namespace osu.Game.Tests.Rulesets.Mods private const double duration = 9000; private TrackVirtual track; - private IBeatmap beatmap; [SetUp] public void SetUp() { track = new TrackVirtual(20_000); - beatmap = new Beatmap - { - HitObjects = - { - new Spinner - { - StartTime = start_time, - Duration = duration - } - } - }; } [TestCase(0, 1)] @@ -43,6 +31,7 @@ namespace osu.Game.Tests.Rulesets.Mods [TestCase(15000, 1.5)] public void TestModWindUp(double time, double expectedRate) { + var beatmap = createSingleSpinnerBeatmap(); var mod = new ModWindUp(); mod.ApplyToBeatmap(beatmap); mod.ApplyToTrack(track); @@ -60,6 +49,7 @@ namespace osu.Game.Tests.Rulesets.Mods [TestCase(15000, 0.5)] public void TestModWindDown(double time, double expectedRate) { + var beatmap = createSingleSpinnerBeatmap(); var mod = new ModWindDown { FinalRate = { Value = 0.5 } @@ -72,11 +62,52 @@ namespace osu.Game.Tests.Rulesets.Mods Assert.That(mod.SpeedChange.Value, Is.EqualTo(expectedRate)); } + [TestCase(0, 1)] + [TestCase(start_time, 1)] + [TestCase(2 * start_time, 1.5)] + public void TestZeroDurationMap(double time, double expectedRate) + { + var beatmap = createSingleObjectBeatmap(); + var mod = new ModWindUp(); + mod.ApplyToBeatmap(beatmap); + mod.ApplyToTrack(track); + + seekTrackAndUpdateMod(mod, time); + + Assert.That(mod.SpeedChange.Value, Is.EqualTo(expectedRate)); + } + private void seekTrackAndUpdateMod(ModTimeRamp mod, double time) { track.Seek(time); // update the mod via a fake playfield to re-calculate the current rate. mod.Update(null); } + + private static Beatmap createSingleSpinnerBeatmap() + { + return new Beatmap + { + HitObjects = + { + new Spinner + { + StartTime = start_time, + Duration = duration + } + } + }; + } + + private static Beatmap createSingleObjectBeatmap() + { + return new Beatmap + { + HitObjects = + { + new HitCircle { StartTime = start_time } + } + }; + } } } diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index a28a0351a6..b6916c838e 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -77,7 +77,7 @@ namespace osu.Game.Rulesets.Mods public virtual void Update(Playfield playfield) { - applyRateAdjustment((track.CurrentTime - beginRampTime) / (finalRateTime - beginRampTime)); + applyRateAdjustment((track.CurrentTime - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime)); } /// From 39d46d21e6e2453121e513a2025cd6c5a2bd9ba9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 31 Jan 2021 23:44:27 +0300 Subject: [PATCH 6263/6909] Add failing test case --- .../Online/TestSceneStandAloneChatDisplay.cs | 116 ++++++++++++------ 1 file changed, 80 insertions(+), 36 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index 492abdd88d..02ef024128 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -8,6 +8,7 @@ using osu.Game.Users; using osuTK; using System; using System.Linq; +using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Chat; @@ -16,8 +17,6 @@ namespace osu.Game.Tests.Visual.Online { public class TestSceneStandAloneChatDisplay : OsuTestScene { - private readonly Channel testChannel = new Channel(); - private readonly User admin = new User { Username = "HappyStick", @@ -46,78 +45,84 @@ namespace osu.Game.Tests.Visual.Online [Cached] private ChannelManager channelManager = new ChannelManager(); - private readonly TestStandAloneChatDisplay chatDisplay; - private readonly TestStandAloneChatDisplay chatDisplay2; + private TestStandAloneChatDisplay chatDisplay; + private TestStandAloneChatDisplay chatDisplay2; + private int messageIdSequence; + + private Channel testChannel; public TestSceneStandAloneChatDisplay() { Add(channelManager); - - Add(chatDisplay = new TestStandAloneChatDisplay - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding(20), - Size = new Vector2(400, 80) - }); - - Add(chatDisplay2 = new TestStandAloneChatDisplay(true) - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Margin = new MarginPadding(20), - Size = new Vector2(400, 150) - }); } - protected override void LoadComplete() + [SetUp] + public void SetUp() => Schedule(() => { - base.LoadComplete(); + messageIdSequence = 0; + channelManager.CurrentChannel.Value = testChannel = new Channel(); - channelManager.CurrentChannel.Value = testChannel; + Children = new[] + { + chatDisplay = new TestStandAloneChatDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding(20), + Size = new Vector2(400, 80), + Channel = { Value = testChannel }, + }, + chatDisplay2 = new TestStandAloneChatDisplay(true) + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding(20), + Size = new Vector2(400, 150), + Channel = { Value = testChannel }, + } + }; + }); - chatDisplay.Channel.Value = testChannel; - chatDisplay2.Channel.Value = testChannel; - - int sequence = 0; - - AddStep("message from admin", () => testChannel.AddNewMessages(new Message(sequence++) + [Test] + public void TestManyMessages() + { + AddStep("message from admin", () => testChannel.AddNewMessages(new Message(messageIdSequence++) { Sender = admin, Content = "I am a wang!" })); - AddStep("message from team red", () => testChannel.AddNewMessages(new Message(sequence++) + AddStep("message from team red", () => testChannel.AddNewMessages(new Message(messageIdSequence++) { Sender = redUser, Content = "I am team red." })); - AddStep("message from team red", () => testChannel.AddNewMessages(new Message(sequence++) + AddStep("message from team red", () => testChannel.AddNewMessages(new Message(messageIdSequence++) { Sender = redUser, Content = "I plan to win!" })); - AddStep("message from team blue", () => testChannel.AddNewMessages(new Message(sequence++) + AddStep("message from team blue", () => testChannel.AddNewMessages(new Message(messageIdSequence++) { Sender = blueUser, Content = "Not on my watch. Prepare to eat saaaaaaaaaand. Lots and lots of saaaaaaand." })); - AddStep("message from admin", () => testChannel.AddNewMessages(new Message(sequence++) + AddStep("message from admin", () => testChannel.AddNewMessages(new Message(messageIdSequence++) { Sender = admin, Content = "Okay okay, calm down guys. Let's do this!" })); - AddStep("message from long username", () => testChannel.AddNewMessages(new Message(sequence++) + AddStep("message from long username", () => testChannel.AddNewMessages(new Message(messageIdSequence++) { Sender = longUsernameUser, Content = "Hi guys, my new username is lit!" })); - AddStep("message with new date", () => testChannel.AddNewMessages(new Message(sequence++) + AddStep("message with new date", () => testChannel.AddNewMessages(new Message(messageIdSequence++) { Sender = longUsernameUser, Content = "Message from the future!", @@ -131,7 +136,7 @@ namespace osu.Game.Tests.Visual.Online { for (int i = 0; i < messages_per_call; i++) { - testChannel.AddNewMessages(new Message(sequence++) + testChannel.AddNewMessages(new Message(messageIdSequence++) { Sender = longUsernameUser, Content = "Many messages! " + Guid.NewGuid(), @@ -156,6 +161,45 @@ namespace osu.Game.Tests.Visual.Online AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom); } + /// + /// Tests that when a message gets wrapped by the chat display getting contracted while scrolled to bottom, the chat will still keep scrolling down. + /// + [Test] + public void TestMessageWrappingKeepsAutoScrolling() + { + AddStep("fill chat", () => + { + for (int i = 0; i < 10; i++) + { + testChannel.AddNewMessages(new Message(messageIdSequence++) + { + Sender = longUsernameUser, + Content = $"some stuff {Guid.NewGuid()}", + }); + } + }); + + AddAssert("ensure scrolled to bottom", () => chatDisplay.ScrolledToBottom); + + // send message with short words for text wrapping to occur when contracting chat. + AddStep("send lorem ipsum", () => testChannel.AddNewMessages(new Message(messageIdSequence++) + { + Sender = longUsernameUser, + Content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce et bibendum velit.", + })); + + AddStep("contract chat", () => chatDisplay.Width -= 100); + AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom); + + AddStep("send another message", () => testChannel.AddNewMessages(new Message(messageIdSequence++) + { + Sender = admin, + Content = "As we were saying...", + })); + + AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom); + } + private class TestStandAloneChatDisplay : StandAloneChatDisplay { public TestStandAloneChatDisplay(bool textbox = false) From e806e5bcd1580f1c85026e53d289d7cf933e4767 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 31 Jan 2021 23:37:52 +0300 Subject: [PATCH 6264/6909] Improve robustness of chat auto-scrolling logic Fix auto-scrolling state changing by old messages removal logic --- osu.Game/Overlays/Chat/DrawableChannel.cs | 47 ++++++++++++++++------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 5926d11c03..f1aa387c17 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -24,7 +24,7 @@ namespace osu.Game.Overlays.Chat { public readonly Channel Channel; protected FillFlowContainer ChatLineFlow; - private OsuScrollContainer scroll; + private ChannelScrollContainer scroll; private bool scrollbarVisible = true; @@ -56,7 +56,7 @@ namespace osu.Game.Overlays.Chat { RelativeSizeAxes = Axes.Both, Masking = true, - Child = scroll = new OsuScrollContainer + Child = scroll = new ChannelScrollContainer { ScrollbarVisible = scrollbarVisible, RelativeSizeAxes = Axes.Both, @@ -80,12 +80,6 @@ namespace osu.Game.Overlays.Chat Channel.PendingMessageResolved += pendingMessageResolved; } - protected override void LoadComplete() - { - base.LoadComplete(); - scrollToEnd(); - } - protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); @@ -113,8 +107,6 @@ namespace osu.Game.Overlays.Chat ChatLineFlow.Clear(); } - bool shouldScrollToEnd = scroll.IsScrolledToEnd(10) || !chatLines.Any() || newMessages.Any(m => m is LocalMessage); - // Add up to last Channel.MAX_HISTORY messages var displayMessages = newMessages.Skip(Math.Max(0, newMessages.Count() - Channel.MAX_HISTORY)); @@ -153,8 +145,10 @@ namespace osu.Game.Overlays.Chat } } - if (shouldScrollToEnd) - scrollToEnd(); + // due to the scroll adjusts from old messages removal above, a scroll-to-end must be enforced, + // to avoid making the container think the user has scrolled back up and unwantedly disable auto-scrolling. + if (scroll.ShouldAutoScroll || newMessages.Any(m => m is LocalMessage)) + ScheduleAfterChildren(() => scroll.ScrollToEnd()); }); private void pendingMessageResolved(Message existing, Message updated) => Schedule(() => @@ -178,8 +172,6 @@ namespace osu.Game.Overlays.Chat private IEnumerable chatLines => ChatLineFlow.Children.OfType(); - private void scrollToEnd() => ScheduleAfterChildren(() => scroll.ScrollToEnd()); - public class DaySeparator : Container { public float TextSize @@ -243,5 +235,32 @@ namespace osu.Game.Overlays.Chat }; } } + + /// + /// An with functionality to automatically scrolls whenever the maximum scrollable distance increases. + /// + private class ChannelScrollContainer : OsuScrollContainer + { + private const float auto_scroll_leniency = 10f; + + private float? lastExtent; + + /// + /// Whether this should automatically scroll to end on the next call to . + /// + public bool ShouldAutoScroll { get; private set; } = true; + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + if ((lastExtent == null || ScrollableExtent > lastExtent) && ShouldAutoScroll) + ScrollToEnd(); + else + ShouldAutoScroll = IsScrolledToEnd(auto_scroll_leniency); + + lastExtent = ScrollableExtent; + } + } } } From 230b347c1efbfc7baa7da6af8676356e47d7a4d2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 12:18:11 +0900 Subject: [PATCH 6265/6909] Move ModSelectOverlay.IsValidMod to a property --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 29 ++++++++++++++----- .../Overlays/Mods/SoloModSelectOverlay.cs | 6 ---- .../Multiplayer/FreeModSelectOverlay.cs | 5 ---- .../Multiplayer/MultiplayerMatchSongSelect.cs | 11 +++++-- osu.Game/Screens/Select/MatchSongSelect.cs | 5 +++- 5 files changed, 34 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 5709ca3b8d..c21b9ba409 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -29,7 +30,6 @@ namespace osu.Game.Overlays.Mods { public abstract class ModSelectOverlay : WaveOverlayContainer { - private readonly Func isValidMod; public const float HEIGHT = 510; protected readonly TriangleButton DeselectAllButton; @@ -46,6 +46,20 @@ namespace osu.Game.Overlays.Mods protected readonly ModSettingsContainer ModSettingsContainer; + [NotNull] + private Func isValidMod = m => true; + + [NotNull] + public Func IsValidMod + { + get => isValidMod; + set + { + isValidMod = value ?? throw new ArgumentNullException(nameof(value)); + updateAvailableMods(); + } + } + public readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); private Bindable>> availableMods; @@ -60,10 +74,8 @@ namespace osu.Game.Overlays.Mods private SampleChannel sampleOn, sampleOff; - protected ModSelectOverlay(Func isValidMod = null) + protected ModSelectOverlay() { - this.isValidMod = isValidMod ?? (m => true); - Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2"); Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2"); Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774"); @@ -350,7 +362,7 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - availableMods.BindValueChanged(availableModsChanged, true); + availableMods.BindValueChanged(_ => updateAvailableMods(), true); SelectedMods.BindValueChanged(selectedModsChanged, true); } @@ -405,12 +417,13 @@ namespace osu.Game.Overlays.Mods public override bool OnPressed(GlobalAction action) => false; // handled by back button - private void availableModsChanged(ValueChangedEvent>> mods) + private void updateAvailableMods() { - if (mods.NewValue == null) return; + if (availableMods.Value == null) + return; foreach (var section in ModSectionsContainer.Children) - section.Mods = mods.NewValue[section.ModType].Where(isValidMod); + section.Mods = availableMods.Value[section.ModType].Where(IsValidMod); } private void selectedModsChanged(ValueChangedEvent> mods) diff --git a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs index 53d0c9fce9..d039ad1f98 100644 --- a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs @@ -1,18 +1,12 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { public class SoloModSelectOverlay : ModSelectOverlay { - public SoloModSelectOverlay(Func isValidMod = null) - : base(isValidMod) - { - } - protected override void OnModSelected(Mod mod) { base.OnModSelected(mod); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs index 10b68ec5a6..56e74a8460 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs @@ -14,11 +14,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class FreeModSelectOverlay : ModSelectOverlay { - public FreeModSelectOverlay(Func isValidMod = null) - : base(isValidMod) - { - } - protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); private class FreeModSection : ModSection diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 86b8f22d34..d36ebeec0f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -54,7 +54,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; - freeModSelectOverlay = new FreeModSelectOverlay(isValidMod) { SelectedMods = { BindTarget = freeMods } }; + freeModSelectOverlay = new FreeModSelectOverlay + { + SelectedMods = { BindTarget = freeMods }, + IsValidMod = isValidMod, + }; } [BackgroundDependencyLoader] @@ -130,7 +134,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(isValidMod); + protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay + { + IsValidMod = isValidMod + }; protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() { diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 280f46f9ab..98e02a9294 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -81,7 +81,10 @@ namespace osu.Game.Screens.Select item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); } - protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(isValidMod); + protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay + { + IsValidMod = isValidMod + }; private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; } From 797a8102876808174924486af644de927d7123f4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 13:24:56 +0900 Subject: [PATCH 6266/6909] Allow unstacking mods --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 30 +++++++++++++++++-- .../Multiplayer/FreeModSelectOverlay.cs | 5 ++++ osu.Game/Utils/ModValidation.cs | 17 +++++++---- 3 files changed, 45 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index c21b9ba409..db9460c494 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -22,6 +22,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; using osu.Game.Rulesets.Mods; using osu.Game.Screens; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -46,9 +47,27 @@ namespace osu.Game.Overlays.Mods protected readonly ModSettingsContainer ModSettingsContainer; + private bool stacked = true; + + /// + /// Whether mod icons should be stacked, or appear as individual buttons. + /// + public bool Stacked + { + get => stacked; + set + { + stacked = value; + updateAvailableMods(); + } + } + [NotNull] private Func isValidMod = m => true; + /// + /// A function that checks whether a given mod is valid. + /// [NotNull] public Func IsValidMod { @@ -419,11 +438,18 @@ namespace osu.Game.Overlays.Mods private void updateAvailableMods() { - if (availableMods.Value == null) + if (availableMods?.Value == null) return; foreach (var section in ModSectionsContainer.Children) - section.Mods = availableMods.Value[section.ModType].Where(IsValidMod); + { + IEnumerable modEnumeration = availableMods.Value[section.ModType]; + + if (!stacked) + modEnumeration = ModValidation.FlattenMods(modEnumeration); + + section.Mods = modEnumeration.Where(IsValidMod); + } } private void selectedModsChanged(ValueChangedEvent> mods) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs index 56e74a8460..8334df1e44 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs @@ -14,6 +14,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class FreeModSelectOverlay : ModSelectOverlay { + public FreeModSelectOverlay() + { + Stacked = false; + } + protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); private class FreeModSection : ModSection diff --git a/osu.Game/Utils/ModValidation.cs b/osu.Game/Utils/ModValidation.cs index 0c4d58ab2e..3597396ec4 100644 --- a/osu.Game/Utils/ModValidation.cs +++ b/osu.Game/Utils/ModValidation.cs @@ -42,7 +42,7 @@ namespace osu.Game.Utils var incompatibleTypes = new HashSet(); var incomingTypes = new HashSet(); - foreach (var mod in combination.SelectMany(flattenMod)) + foreach (var mod in combination.SelectMany(FlattenMod)) { // Add the new mod incompatibilities, checking whether any match the existing mod types. foreach (var t in mod.IncompatibleMods) @@ -79,7 +79,7 @@ namespace osu.Game.Utils { var allowedSet = new HashSet(allowedTypes); - return combination.SelectMany(flattenMod) + return combination.SelectMany(FlattenMod) .All(m => allowedSet.Contains(m.GetType())); } @@ -93,20 +93,27 @@ namespace osu.Game.Utils /// The set of incompatible types. /// Whether the given is incompatible. private static bool isModIncompatible(Mod mod, ICollection incompatibleTypes) - => flattenMod(mod) + => FlattenMod(mod) .SelectMany(m => m.GetType().EnumerateBaseTypes()) .Any(incompatibleTypes.Contains); + /// + /// Flattens a set of s, returning a new set with all s removed. + /// + /// The set of s to flatten. + /// The new set, containing all s in recursively with all s removed. + public static IEnumerable FlattenMods(IEnumerable mods) => mods.SelectMany(FlattenMod); + /// /// Flattens a , returning a set of s in-place of any s. /// /// The to flatten. /// A set of singular "flattened" s - private static IEnumerable flattenMod(Mod mod) + public static IEnumerable FlattenMod(Mod mod) { if (mod is MultiMod multi) { - foreach (var m in multi.Mods.SelectMany(flattenMod)) + foreach (var m in multi.Mods.SelectMany(FlattenMod)) yield return m; } else From e02e3cf19a9ad9ace7d21e949ec30e6c9269771e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 13:35:48 +0900 Subject: [PATCH 6267/6909] Disallow selecting DT/HT/WU/WD as allowable freemods --- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index d36ebeec0f..98e3ca3358 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer freeModSelectOverlay = new FreeModSelectOverlay { SelectedMods = { BindTarget = freeMods }, - IsValidMod = isValidMod, + IsValidMod = isValidFreeMod, }; } @@ -147,6 +147,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; + + private bool isValidFreeMod(Mod mod) => isValidMod(mod) && !(mod is ModRateAdjust) && !(mod is ModTimeRamp); } public class FooterButtonFreeMods : FooterButton, IHasCurrentValue> From 4ae10b1e1c88f4d6b6a3fe5ec136abf1e3ead32b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 13:40:59 +0900 Subject: [PATCH 6268/6909] Add initial UI for selecting extra mods --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 29 ++++++-- .../Multiplayer/MultiplayerMatchSubScreen.cs | 69 +++++++++++++++++-- 2 files changed, 90 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 2449563c73..231fb58836 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.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.Audio; @@ -29,6 +30,11 @@ namespace osu.Game.Screens.OnlinePlay.Match [Resolved(typeof(Room), nameof(Room.Playlist))] protected BindableList Playlist { get; private set; } + /// + /// Any mods applied by/to the local user. + /// + protected readonly Bindable> ExtraMods = new Bindable>(Array.Empty()); + [Resolved] private MusicController music { get; set; } @@ -55,6 +61,8 @@ namespace osu.Game.Screens.OnlinePlay.Match managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); + + ExtraMods.BindValueChanged(_ => updateMods()); } public override void OnEntering(IScreen last) @@ -95,12 +103,17 @@ namespace osu.Game.Screens.OnlinePlay.Match { updateWorkingBeatmap(); - var item = SelectedItem.Value; + if (SelectedItem.Value == null) + return; - Mods.Value = item?.RequiredMods?.ToArray() ?? Array.Empty(); + // Remove any extra mods that are no longer allowed. + ExtraMods.Value = ExtraMods.Value + .Where(m => SelectedItem.Value.AllowedMods.Any(a => m.GetType() == a.GetType())) + .ToList(); - if (item?.Ruleset != null) - Ruleset.Value = item.Ruleset.Value; + updateMods(); + + Ruleset.Value = SelectedItem.Value.Ruleset.Value; } private void beatmapUpdated(ValueChangedEvent> weakSet) => Schedule(updateWorkingBeatmap); @@ -115,6 +128,14 @@ namespace osu.Game.Screens.OnlinePlay.Match Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } + private void updateMods() + { + if (SelectedItem.Value == null) + return; + + Mods.Value = ExtraMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList(); + } + private void beginHandlingTrack() { Beatmap.BindValueChanged(applyLoopingToTrack, true); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 7c4b6d18ec..95dda1a051 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -14,12 +14,15 @@ using osu.Framework.Screens; using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; +using osu.Game.Screens.Play.HUD; using osu.Game.Users; +using osuTK; using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.ParticipantsList; namespace osu.Game.Screens.OnlinePlay.Multiplayer @@ -37,7 +40,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } + private ModSelectOverlay extraModSelectOverlay; private MultiplayerMatchSettingsOverlay settingsOverlay; + private Drawable extraModsSection; private IBindable isConnected; @@ -129,10 +134,39 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Padding = new MarginPadding { Horizontal = 5 }, - Children = new Drawable[] + Spacing = new Vector2(0, 10), + Children = new[] { - new OverlinedHeader("Beatmap"), - new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } + new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new OverlinedHeader("Beatmap"), + new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } + } + }, + extraModsSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new OverlinedHeader("Extra mods"), + new ModDisplay + { + DisplayUnrankedText = false, + Current = ExtraMods + }, + new PurpleTriangleButton + { + RelativeSizeAxes = Axes.X, + Text = "Select", + Action = () => extraModSelectOverlay.Show() + } + } + } } } } @@ -174,6 +208,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Dimension(GridSizeMode.AutoSize), } }, + extraModSelectOverlay = new SoloModSelectOverlay + { + SelectedMods = { BindTarget = ExtraMods }, + Stacked = false, + IsValidMod = _ => false + }, settingsOverlay = new MultiplayerMatchSettingsOverlay { RelativeSizeAxes = Axes.Both, @@ -219,10 +259,31 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return true; } + if (extraModSelectOverlay.State.Value == Visibility.Visible) + { + extraModSelectOverlay.Hide(); + return true; + } + return base.OnBackButton(); } - private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => SelectedItem.Value = Playlist.FirstOrDefault(); + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) + { + SelectedItem.Value = Playlist.FirstOrDefault(); + + if (SelectedItem.Value?.AllowedMods.Any() != true) + { + extraModsSection.Hide(); + extraModSelectOverlay.Hide(); + extraModSelectOverlay.IsValidMod = _ => false; + } + else + { + extraModsSection.Show(); + extraModSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType()); + } + } private void onReadyClick() { From b846146f163a0dfe87d8d5e02f00730387b8de04 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 13:58:44 +0900 Subject: [PATCH 6269/6909] Update mods when resuming room subscreen --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 231fb58836..3c4c6ce040 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -81,6 +81,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.OnResuming(last); beginHandlingTrack(); + updateMods(); } public override bool OnExiting(IScreen next) From 426569c2a93244a7a31688a0d96529d752858ebc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 14:57:39 +0900 Subject: [PATCH 6270/6909] Move common song select implementation for online play --- .../Multiplayer/MultiplayerMatchSongSelect.cs | 91 +------------ .../OnlinePlay/OnlinePlaySongSelect.cs | 124 ++++++++++++++++++ osu.Game/Screens/Select/MatchSongSelect.cs | 34 +---- 3 files changed, 130 insertions(+), 119 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 98e3ca3358..e892570066 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -1,25 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Collections.Generic; -using System.Linq; -using 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.Graphics.UserInterface; using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Overlays.Mods; -using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; @@ -27,67 +20,21 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer { - public class MultiplayerMatchSongSelect : SongSelect, IOnlinePlaySubScreen + public class MultiplayerMatchSongSelect : OnlinePlaySongSelect { - public string ShortTitle => "song selection"; - - public override string Title => ShortTitle.Humanize(); - - [Resolved(typeof(Room), nameof(Room.Playlist))] - private BindableList playlist { get; set; } - [Resolved] private StatefulMultiplayerClient client { get; set; } - private readonly Bindable> freeMods = new Bindable>(Array.Empty()); - - private readonly FreeModSelectOverlay freeModSelectOverlay; private LoadingLayer loadingLayer; - private WorkingBeatmap initialBeatmap; - private RulesetInfo initialRuleset; - private IReadOnlyList initialMods; - - private bool itemSelected; - - public MultiplayerMatchSongSelect() - { - Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; - - freeModSelectOverlay = new FreeModSelectOverlay - { - SelectedMods = { BindTarget = freeMods }, - IsValidMod = isValidFreeMod, - }; - } - [BackgroundDependencyLoader] private void load() { AddInternal(loadingLayer = new LoadingLayer(true)); - initialBeatmap = Beatmap.Value; - initialRuleset = Ruleset.Value; - initialMods = Mods.Value.ToList(); - - freeMods.Value = playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); - - FooterPanels.Add(freeModSelectOverlay); } - protected override bool OnStart() + protected override void OnSetItem(PlaylistItem item) { - itemSelected = true; - var item = new PlaylistItem(); - - item.Beatmap.Value = Beatmap.Value.BeatmapInfo; - item.Ruleset.Value = Ruleset.Value; - - item.RequiredMods.Clear(); - item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); - - item.AllowedMods.Clear(); - item.AllowedMods.AddRange(freeMods.Value.Select(m => m.CreateCopy())); - // If the client is already in a room, update via the client. // Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation. if (client.Room != null) @@ -112,43 +59,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } else { - playlist.Clear(); - playlist.Add(item); + Playlist.Clear(); + Playlist.Add(item); this.Exit(); } - - return true; - } - - public override bool OnExiting(IScreen next) - { - if (!itemSelected) - { - Beatmap.Value = initialBeatmap; - Ruleset.Value = initialRuleset; - Mods.Value = initialMods; - } - - return base.OnExiting(next); } protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - - protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay - { - IsValidMod = isValidMod - }; - - protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() - { - var buttons = base.CreateFooterButtons().ToList(); - buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = freeMods }, freeModSelectOverlay)); - return buttons; - } - - private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; - - private bool isValidFreeMod(Mod mod) => isValidMod(mod) && !(mod is ModRateAdjust) && !(mod is ModTimeRamp); } public class FooterButtonFreeMods : FooterButton, IHasCurrentValue> diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs new file mode 100644 index 0000000000..7c64e00dc4 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.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 System.Linq; +using Humanizer; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Framework.Screens; +using osu.Game.Beatmaps; +using osu.Game.Online.Rooms; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.Select; + +namespace osu.Game.Screens.OnlinePlay +{ + public abstract class OnlinePlaySongSelect : SongSelect, IOnlinePlaySubScreen + { + public string ShortTitle => "song selection"; + + public override string Title => ShortTitle.Humanize(); + + public override bool AllowEditing => false; + + [Resolved(typeof(Room), nameof(Room.Playlist))] + protected BindableList Playlist { get; private set; } + + private readonly Bindable> freeMods = new Bindable>(Array.Empty()); + private readonly FreeModSelectOverlay freeModSelectOverlay; + + private WorkingBeatmap initialBeatmap; + private RulesetInfo initialRuleset; + private IReadOnlyList initialMods; + private bool itemSelected; + + protected OnlinePlaySongSelect() + { + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; + + freeModSelectOverlay = new FreeModSelectOverlay + { + SelectedMods = { BindTarget = freeMods }, + IsValidMod = IsValidFreeMod, + }; + } + + [BackgroundDependencyLoader] + private void load() + { + initialBeatmap = Beatmap.Value; + initialRuleset = Ruleset.Value; + initialMods = Mods.Value.ToList(); + + freeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + FooterPanels.Add(freeModSelectOverlay); + } + + protected sealed override bool OnStart() + { + itemSelected = true; + + var item = new PlaylistItem(); + + item.Beatmap.Value = Beatmap.Value.BeatmapInfo; + item.Ruleset.Value = Ruleset.Value; + + item.RequiredMods.Clear(); + item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); + + item.AllowedMods.Clear(); + item.AllowedMods.AddRange(freeMods.Value.Select(m => m.CreateCopy())); + + OnSetItem(item); + return true; + } + + protected abstract void OnSetItem(PlaylistItem item); + + public override bool OnBackButton() + { + if (freeModSelectOverlay.State.Value == Visibility.Visible) + { + freeModSelectOverlay.Hide(); + return true; + } + + return base.OnBackButton(); + } + + public override bool OnExiting(IScreen next) + { + if (!itemSelected) + { + Beatmap.Value = initialBeatmap; + Ruleset.Value = initialRuleset; + Mods.Value = initialMods; + } + + return base.OnExiting(next); + } + + protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay + { + IsValidMod = IsValidMod + }; + + protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() + { + var buttons = base.CreateFooterButtons().ToList(); + buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = freeMods }, freeModSelectOverlay)); + return buttons; + } + + protected virtual bool IsValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; + + protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod); + } +} diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 98e02a9294..23fe9620fe 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -1,48 +1,27 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; using System.Linq; -using Humanizer; using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; -using osu.Game.Overlays.Mods; -using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Components; namespace osu.Game.Screens.Select { - public class MatchSongSelect : SongSelect, IOnlinePlaySubScreen + public class MatchSongSelect : OnlinePlaySongSelect { - public Action Selected; - - public string ShortTitle => "song selection"; - public override string Title => ShortTitle.Humanize(); - - public override bool AllowEditing => false; - - [Resolved(typeof(Room), nameof(Room.Playlist))] - protected BindableList Playlist { get; private set; } - [Resolved] private BeatmapManager beatmaps { get; set; } - public MatchSongSelect() - { - Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }; - } - protected override BeatmapDetailArea CreateBeatmapDetailArea() => new MatchBeatmapDetailArea { CreateNewItem = createNewItem }; - protected override bool OnStart() + protected override void OnSetItem(PlaylistItem item) { switch (Playlist.Count) { @@ -56,8 +35,6 @@ namespace osu.Game.Screens.Select } this.Exit(); - - return true; } private void createNewItem() @@ -80,12 +57,5 @@ namespace osu.Game.Screens.Select item.RequiredMods.Clear(); item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); } - - protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay - { - IsValidMod = isValidMod - }; - - private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; } } From b43e529964b31ac40d5230aa41e68876985579d7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 15:06:50 +0900 Subject: [PATCH 6271/6909] Fix allowed mods being copied into required mods --- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index e892570066..0c22813e56 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.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 System.Collections.Generic; +using System.Linq; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; @@ -33,6 +35,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer AddInternal(loadingLayer = new LoadingLayer(true)); } + protected override void LoadComplete() + { + base.LoadComplete(); + + Mods.Value = Playlist.FirstOrDefault()?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + } + protected override void OnSetItem(PlaylistItem item) { // If the client is already in a room, update via the client. From 0909c73ead3d4c45495c8bb5d55dfac16386038a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 15:07:56 +0900 Subject: [PATCH 6272/6909] Once again disallow DT/etc as allowable mods --- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 0c22813e56..80bb7c7ac2 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -75,6 +75,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); + + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust); } public class FooterButtonFreeMods : FooterButton, IHasCurrentValue> From 05982f42ab8a991d9056a7ad38f947c0edc4c4cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Feb 2021 16:43:53 +0900 Subject: [PATCH 6273/6909] Add more comprehensive commenting and simplify base call logic We can call the base method regardless for better safety. Worst case it's just going to run `Stop()` twice anyway. --- .../Drawables/DrawableStoryboardSample.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs index fcbffda227..7b16009859 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSample.cs @@ -46,14 +46,15 @@ namespace osu.Game.Storyboards.Drawables { if (!RequestedPlaying) return; - // non-looping storyboard samples should be stopped immediately when sample playback is disabled - if (!Looping) + if (!Looping && disabled.NewValue) { - if (disabled.NewValue) - Stop(); + // the default behaviour for sample disabling is to allow one-shot samples to play out. + // storyboards regularly have long running samples that can cause this behaviour to lead to unintended results. + // for this reason, we immediately stop such samples. + Stop(); } - else - base.SamplePlaybackDisabledChanged(disabled); + + base.SamplePlaybackDisabledChanged(disabled); } protected override void Update() From 49e62c3a4b7118c1f3c81c6af357534bd541d8ee Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 1 Feb 2021 11:02:08 +0300 Subject: [PATCH 6274/6909] Apply documentation changes Co-authored-by: Dean Herbert --- osu.Game/Overlays/Chat/DrawableChannel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index f1aa387c17..4c8513b1d5 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -237,7 +237,7 @@ namespace osu.Game.Overlays.Chat } /// - /// An with functionality to automatically scrolls whenever the maximum scrollable distance increases. + /// An with functionality to automatically scroll whenever the maximum scrollable distance increases. /// private class ChannelScrollContainer : OsuScrollContainer { @@ -246,7 +246,7 @@ namespace osu.Game.Overlays.Chat private float? lastExtent; /// - /// Whether this should automatically scroll to end on the next call to . + /// Whether this container should automatically scroll to end on the next call to . /// public bool ShouldAutoScroll { get; private set; } = true; From fabb0eeb29a35c7e0d212d9c7be9f1aad8e90c35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Feb 2021 17:27:14 +0900 Subject: [PATCH 6275/6909] Add signalr messagepack key attribute --- osu.Game/Online/Rooms/BeatmapAvailability.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index 4ce797e583..2adeb9b959 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -22,6 +22,7 @@ namespace osu.Game.Online.Rooms /// /// The beatmap's downloading progress, null when not in state. /// + [Key(1)] public readonly float? DownloadProgress; [JsonConstructor] From 1d8de2f718916b907b24bf1b411ebb627258cbd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Feb 2021 17:32:54 +0900 Subject: [PATCH 6276/6909] Rename class to better match purpose --- ... TestSceneMultiplayerBeatmapAvailabilityTracker.cs} | 10 +++++----- ...er.cs => MultiplayerBeatmapAvailablilityTracker.cs} | 7 +++---- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 ++-- 3 files changed, 10 insertions(+), 11 deletions(-) rename osu.Game.Tests/Online/{TestSceneMultiplayerBeatmapTracker.cs => TestSceneMultiplayerBeatmapAvailabilityTracker.cs} (94%) rename osu.Game/Online/Rooms/{MultiplayerBeatmapTracker.cs => MultiplayerBeatmapAvailablilityTracker.cs} (93%) diff --git a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapAvailabilityTracker.cs similarity index 94% rename from osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs rename to osu.Game.Tests/Online/TestSceneMultiplayerBeatmapAvailabilityTracker.cs index 9caecc198a..3c3793670a 100644 --- a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapTracker.cs +++ b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapAvailabilityTracker.cs @@ -28,7 +28,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Online { [HeadlessTest] - public class TestSceneMultiplayerBeatmapTracker : OsuTestScene + public class TestSceneMultiplayerBeatmapAvailabilityTracker : OsuTestScene { private RulesetStore rulesets; private TestBeatmapManager beatmaps; @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Online private BeatmapSetInfo testBeatmapSet; private readonly Bindable selectedItem = new Bindable(); - private MultiplayerBeatmapTracker tracker; + private MultiplayerBeatmapAvailablilityTracker availablilityTracker; [BackgroundDependencyLoader] private void load(AudioManager audio, GameHost host) @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Online Ruleset = { Value = testBeatmapInfo.Ruleset }, }; - Child = tracker = new MultiplayerBeatmapTracker + Child = availablilityTracker = new MultiplayerBeatmapAvailablilityTracker { SelectedItem = { BindTarget = selectedItem, } }; @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Online }); addAvailabilityCheckStep("state still not downloaded", BeatmapAvailability.NotDownloaded); - AddStep("recreate tracker", () => Child = tracker = new MultiplayerBeatmapTracker + AddStep("recreate tracker", () => Child = availablilityTracker = new MultiplayerBeatmapAvailablilityTracker { SelectedItem = { BindTarget = selectedItem } }); @@ -129,7 +129,7 @@ namespace osu.Game.Tests.Online { // In DownloadTrackingComposite, state changes are scheduled one frame later, wait one step. AddWaitStep("wait for potential change", 1); - AddAssert(description, () => tracker.Availability.Value.Equals(expected.Invoke())); + AddAssert(description, () => availablilityTracker.Availability.Value.Equals(expected.Invoke())); } private static BeatmapInfo getTestBeatmapInfo(string archiveFile) diff --git a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs b/osu.Game/Online/Rooms/MultiplayerBeatmapAvailablilityTracker.cs similarity index 93% rename from osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs rename to osu.Game/Online/Rooms/MultiplayerBeatmapAvailablilityTracker.cs index 28e4872ad3..578c4db2f8 100644 --- a/osu.Game/Online/Rooms/MultiplayerBeatmapTracker.cs +++ b/osu.Game/Online/Rooms/MultiplayerBeatmapAvailablilityTracker.cs @@ -15,7 +15,7 @@ namespace osu.Game.Online.Rooms /// This differs from a regular download tracking composite as this accounts for the /// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap. /// - public class MultiplayerBeatmapTracker : DownloadTrackingComposite + public class MultiplayerBeatmapAvailablilityTracker : DownloadTrackingComposite { public readonly IBindable SelectedItem = new Bindable(); @@ -26,11 +26,10 @@ namespace osu.Game.Online.Rooms private readonly Bindable availability = new Bindable(); - public MultiplayerBeatmapTracker() + public MultiplayerBeatmapAvailablilityTracker() { State.BindValueChanged(_ => updateAvailability()); - Progress.BindValueChanged(_ => updateAvailability()); - updateAvailability(); + Progress.BindValueChanged(_ => updateAvailability(), true); } protected override void LoadComplete() diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index c049d4be20..f2cb1120cb 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -41,11 +41,11 @@ namespace osu.Game.Screens.OnlinePlay.Match private IBindable> managerUpdated; [Cached] - protected readonly MultiplayerBeatmapTracker BeatmapTracker; + protected readonly MultiplayerBeatmapAvailablilityTracker BeatmapAvailablilityTracker; protected RoomSubScreen() { - InternalChild = BeatmapTracker = new MultiplayerBeatmapTracker + InternalChild = BeatmapAvailablilityTracker = new MultiplayerBeatmapAvailablilityTracker { SelectedItem = { BindTarget = SelectedItem }, }; From ac2a995041590370e31b2b5ef9fb440dcb20d43c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 17:54:56 +0900 Subject: [PATCH 6277/6909] Add user and panel states --- .../TestSceneMultiplayerParticipantsList.cs | 25 ++++++++++++++ .../Online/Multiplayer/IMultiplayerClient.cs | 4 +++ .../Multiplayer/IMultiplayerRoomServer.cs | 4 +++ .../Online/Multiplayer/MultiplayerRoomUser.cs | 7 ++++ .../Multiplayer/StatefulMultiplayerClient.cs | 26 +++++++++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 11 +++++++ .../Participants/ParticipantPanel.cs | 33 ++++++++++++++++++- osu.Game/Screens/Play/HUD/ModDisplay.cs | 14 +++++--- .../Multiplayer/TestMultiplayerClient.cs | 17 ++++++++++ 9 files changed, 136 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 968a869532..9aa1f2cf99 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -8,6 +8,8 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Multiplayer.Participants; using osu.Game.Users; using osuTK; @@ -123,5 +125,28 @@ namespace osu.Game.Tests.Visual.Multiplayer } }); } + + [Test] + public void TestUserWithMods() + { + AddStep("add user", () => + { + Client.AddUser(new User + { + Id = 0, + Username = $"User 0", + CurrentModeRank = RNG.Next(1, 100000), + CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", + }); + + Client.ChangeUserExtraMods(0, new Mod[] + { + new OsuModHardRock(), + new OsuModDifficultyAdjust { ApproachRate = { Value = 1 } } + }); + }); + + AddToggleStep("toggle ready state", v => Client.ChangeUserState(0, v ? MultiplayerUserState.Ready : MultiplayerUserState.Idle)); + } } } diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 19dd473230..37f60ab036 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Threading.Tasks; +using osu.Game.Online.API; using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer @@ -55,6 +57,8 @@ namespace osu.Game.Online.Multiplayer /// The new beatmap availability state of the user. Task UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability); + Task UserExtraModsChanged(int userId, IEnumerable mods); + /// /// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point. /// diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 09816974a7..484acfe957 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -1,7 +1,9 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Threading.Tasks; +using osu.Game.Online.API; using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer @@ -47,6 +49,8 @@ namespace osu.Game.Online.Multiplayer /// The proposed new beatmap availability state. Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + Task ChangeExtraMods(IEnumerable newMods); + /// /// As the host of a room, start the match. /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 2590acbc81..d7f7f9135e 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -4,7 +4,11 @@ #nullable enable using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; using Newtonsoft.Json; +using osu.Game.Online.API; using osu.Game.Online.Rooms; using osu.Game.Users; @@ -22,6 +26,9 @@ namespace osu.Game.Online.Multiplayer /// public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable(); + [NotNull] + public IEnumerable ExtraMods { get; set; } = Enumerable.Empty(); + public User? User { get; set; } [JsonConstructor] diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index e5b07ddfb4..33dcf1e8b4 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -22,6 +22,7 @@ using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Rulesets; +using osu.Game.Rulesets.Mods; using osu.Game.Users; using osu.Game.Utils; @@ -231,6 +232,10 @@ namespace osu.Game.Online.Multiplayer public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + public Task ChangeExtraMods(IEnumerable newMods) => ChangeExtraMods(newMods.Select(m => new APIMod(m)).ToList()); + + public abstract Task ChangeExtraMods(IEnumerable newMods); + public abstract Task StartMatch(); Task IMultiplayerClient.RoomStateChanged(MultiplayerRoomState state) @@ -379,6 +384,27 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } + public Task UserExtraModsChanged(int userId, IEnumerable mods) + { + if (Room == null) + return Task.CompletedTask; + + Scheduler.Add(() => + { + var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); + + // errors here are not critical - user mods is mostly for display. + if (user == null) + return; + + user.ExtraMods = mods; + + RoomUpdated?.Invoke(); + }, false); + + return Task.CompletedTask; + } + Task IMultiplayerClient.LoadRequested() { if (Room == null) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 95dda1a051..c1025e73f8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.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.Collections.Specialized; using System.Diagnostics; using System.Linq; @@ -15,6 +16,7 @@ using osu.Game.Extensions; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; @@ -240,6 +242,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); Playlist.BindCollectionChanged(onPlaylistChanged, true); + ExtraMods.BindValueChanged(onExtraModsChanged); client.LoadRequested += onLoadRequested; @@ -285,6 +288,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } + private void onExtraModsChanged(ValueChangedEvent> extraMods) + { + if (client.Room == null) + return; + + client.ChangeExtraMods(extraMods.NewValue).CatchUnobservedExceptions(); + } + private void onReadyClick() { Debug.Assert(readyClickOperation == null); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index f99655e305..059e9e518d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.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.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -16,6 +17,8 @@ using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; +using osu.Game.Rulesets; +using osu.Game.Screens.Play.HUD; using osu.Game.Users; using osu.Game.Users.Drawables; using osuTK; @@ -30,6 +33,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants [Resolved] private IAPIProvider api { get; set; } + [Resolved] + private RulesetStore rulesets { get; set; } + + private ModDisplay extraModsDisplay; private StateDisplay userStateDisplay; private SpriteIcon crown; @@ -122,11 +129,32 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants } } }, - userStateDisplay = new StateDisplay + new FillFlowContainer { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, Margin = new MarginPadding { Right = 10 }, + Spacing = new Vector2(10), + Children = new Drawable[] + { + extraModsDisplay = new ModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Scale = new Vector2(0.5f), + ExpansionMode = ExpansionMode.AlwaysContracted, + DisplayUnrankedText = false, + ExpandOnAppear = false + }, + userStateDisplay = new StateDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + AlwaysPresent = true + } + } } } } @@ -143,7 +171,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; + var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance(); + userStateDisplay.Status = User.State; + extraModsDisplay.Current.Value = User.ExtraMods.Select(m => m.ToMod(ruleset)).ToList(); if (Room.Host?.Equals(User) == true) crown.FadeIn(fade_time); diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 68d019bf71..2d5b07f056 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -26,6 +26,8 @@ namespace osu.Game.Screens.Play.HUD public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; + public bool ExpandOnAppear = true; + private readonly Bindable> current = new Bindable>(); public Bindable> Current @@ -108,10 +110,14 @@ namespace osu.Game.Screens.Play.HUD else unrankedText.Hide(); - expand(); - - using (iconsContainer.BeginDelayedSequence(1200)) - contract(); + if (ExpandOnAppear) + { + expand(); + using (iconsContainer.BeginDelayedSequence(1200)) + contract(); + } + else + iconsContainer.TransformSpacingTo(new Vector2(-25, 0)); } private void expand() diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 7fbc770351..e699e7fb34 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading.Tasks; @@ -11,6 +12,7 @@ using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets.Mods; using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer @@ -122,6 +124,21 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } + public void ChangeUserExtraMods(int userId, IEnumerable newMods) + => ChangeUserExtraMods(userId, newMods.Select(m => new APIMod(m)).ToList()); + + public void ChangeUserExtraMods(int userId, IEnumerable newMods) + { + Debug.Assert(Room != null); + ((IMultiplayerClient)this).UserExtraModsChanged(userId, newMods.ToList()); + } + + public override Task ChangeExtraMods(IEnumerable newMods) + { + ChangeUserExtraMods(api.LocalUser.Value.Id, newMods); + return Task.CompletedTask; + } + public override Task StartMatch() { Debug.Assert(Room != null); From f53896360715341641dfc7971cb04f66d19fd91d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 17:57:32 +0900 Subject: [PATCH 6278/6909] Extra mods -> user mods --- .../TestSceneMultiplayerParticipantsList.cs | 2 +- .../Online/Multiplayer/IMultiplayerClient.cs | 2 +- .../Multiplayer/IMultiplayerRoomServer.cs | 2 +- .../Online/Multiplayer/MultiplayerClient.cs | 10 ++++++ .../Online/Multiplayer/MultiplayerRoomUser.cs | 2 +- .../Multiplayer/StatefulMultiplayerClient.cs | 8 ++--- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 12 +++---- .../Multiplayer/MultiplayerMatchSubScreen.cs | 34 +++++++++---------- .../Participants/ParticipantPanel.cs | 6 ++-- .../Multiplayer/TestMultiplayerClient.cs | 12 +++---- 10 files changed, 50 insertions(+), 40 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 9aa1f2cf99..8caba5d9c8 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -139,7 +139,7 @@ namespace osu.Game.Tests.Visual.Multiplayer CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); - Client.ChangeUserExtraMods(0, new Mod[] + Client.ChangeUserMods(0, new Mod[] { new OsuModHardRock(), new OsuModDifficultyAdjust { ApproachRate = { Value = 1 } } diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index 37f60ab036..f22b0e4e28 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -57,7 +57,7 @@ namespace osu.Game.Online.Multiplayer /// The new beatmap availability state of the user. Task UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability); - Task UserExtraModsChanged(int userId, IEnumerable mods); + Task UserModsChanged(int userId, IEnumerable mods); /// /// Signals that a match is to be started. This will *only* be sent to clients which are to begin loading at this point. diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 484acfe957..71555ae23d 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -49,7 +49,7 @@ namespace osu.Game.Online.Multiplayer /// The proposed new beatmap availability state. Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); - Task ChangeExtraMods(IEnumerable newMods); + Task ChangeUserMods(IEnumerable newMods); /// /// As the host of a room, start the match. diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 50dc8f661c..ecf314c1e5 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics; using System.Threading; using System.Threading.Tasks; @@ -84,6 +85,7 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); + connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); connection.Closed += async ex => { @@ -182,6 +184,14 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability); } + public override Task ChangeUserMods(IEnumerable newMods) + { + if (!isConnected.Value) + return Task.CompletedTask; + + return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserMods), newMods); + } + public override Task StartMatch() { if (!isConnected.Value) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index d7f7f9135e..4c9643bfce 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -27,7 +27,7 @@ namespace osu.Game.Online.Multiplayer public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable(); [NotNull] - public IEnumerable ExtraMods { get; set; } = Enumerable.Empty(); + public IEnumerable UserMods { get; set; } = Enumerable.Empty(); public User? User { get; set; } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 33dcf1e8b4..a0e903e89a 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -232,9 +232,9 @@ namespace osu.Game.Online.Multiplayer public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); - public Task ChangeExtraMods(IEnumerable newMods) => ChangeExtraMods(newMods.Select(m => new APIMod(m)).ToList()); + public Task ChangeUserMods(IEnumerable newMods) => ChangeUserMods(newMods.Select(m => new APIMod(m)).ToList()); - public abstract Task ChangeExtraMods(IEnumerable newMods); + public abstract Task ChangeUserMods(IEnumerable newMods); public abstract Task StartMatch(); @@ -384,7 +384,7 @@ namespace osu.Game.Online.Multiplayer return Task.CompletedTask; } - public Task UserExtraModsChanged(int userId, IEnumerable mods) + public Task UserModsChanged(int userId, IEnumerable mods) { if (Room == null) return Task.CompletedTask; @@ -397,7 +397,7 @@ namespace osu.Game.Online.Multiplayer if (user == null) return; - user.ExtraMods = mods; + user.UserMods = mods; RoomUpdated?.Invoke(); }, false); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 3c4c6ce040..6367aa54a7 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -33,7 +33,7 @@ namespace osu.Game.Screens.OnlinePlay.Match /// /// Any mods applied by/to the local user. /// - protected readonly Bindable> ExtraMods = new Bindable>(Array.Empty()); + protected readonly Bindable> UserMods = new Bindable>(Array.Empty()); [Resolved] private MusicController music { get; set; } @@ -62,7 +62,7 @@ namespace osu.Game.Screens.OnlinePlay.Match managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); - ExtraMods.BindValueChanged(_ => updateMods()); + UserMods.BindValueChanged(_ => updateMods()); } public override void OnEntering(IScreen last) @@ -108,9 +108,9 @@ namespace osu.Game.Screens.OnlinePlay.Match return; // Remove any extra mods that are no longer allowed. - ExtraMods.Value = ExtraMods.Value - .Where(m => SelectedItem.Value.AllowedMods.Any(a => m.GetType() == a.GetType())) - .ToList(); + UserMods.Value = UserMods.Value + .Where(m => SelectedItem.Value.AllowedMods.Any(a => m.GetType() == a.GetType())) + .ToList(); updateMods(); @@ -134,7 +134,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (SelectedItem.Value == null) return; - Mods.Value = ExtraMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList(); + Mods.Value = UserMods.Value.Concat(SelectedItem.Value.RequiredMods).ToList(); } private void beginHandlingTrack() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index c1025e73f8..a31a3e51ee 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -42,9 +42,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } - private ModSelectOverlay extraModSelectOverlay; + private ModSelectOverlay userModsSelectOverlay; private MultiplayerMatchSettingsOverlay settingsOverlay; - private Drawable extraModsSection; + private Drawable userModsSection; private IBindable isConnected; @@ -149,7 +149,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } } }, - extraModsSection = new FillFlowContainer + userModsSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -159,13 +159,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new ModDisplay { DisplayUnrankedText = false, - Current = ExtraMods + Current = UserMods }, new PurpleTriangleButton { RelativeSizeAxes = Axes.X, Text = "Select", - Action = () => extraModSelectOverlay.Show() + Action = () => userModsSelectOverlay.Show() } } } @@ -210,9 +210,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Dimension(GridSizeMode.AutoSize), } }, - extraModSelectOverlay = new SoloModSelectOverlay + userModsSelectOverlay = new SoloModSelectOverlay { - SelectedMods = { BindTarget = ExtraMods }, + SelectedMods = { BindTarget = UserMods }, Stacked = false, IsValidMod = _ => false }, @@ -242,7 +242,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.LoadComplete(); Playlist.BindCollectionChanged(onPlaylistChanged, true); - ExtraMods.BindValueChanged(onExtraModsChanged); + UserMods.BindValueChanged(onUserModsChanged); client.LoadRequested += onLoadRequested; @@ -262,9 +262,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return true; } - if (extraModSelectOverlay.State.Value == Visibility.Visible) + if (userModsSelectOverlay.State.Value == Visibility.Visible) { - extraModSelectOverlay.Hide(); + userModsSelectOverlay.Hide(); return true; } @@ -277,23 +277,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (SelectedItem.Value?.AllowedMods.Any() != true) { - extraModsSection.Hide(); - extraModSelectOverlay.Hide(); - extraModSelectOverlay.IsValidMod = _ => false; + userModsSection.Hide(); + userModsSelectOverlay.Hide(); + userModsSelectOverlay.IsValidMod = _ => false; } else { - extraModsSection.Show(); - extraModSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType()); + userModsSection.Show(); + userModsSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType()); } } - private void onExtraModsChanged(ValueChangedEvent> extraMods) + private void onUserModsChanged(ValueChangedEvent> mods) { if (client.Room == null) return; - client.ChangeExtraMods(extraMods.NewValue).CatchUnobservedExceptions(); + client.ChangeUserMods(mods.NewValue).CatchUnobservedExceptions(); } private void onReadyClick() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 059e9e518d..a782da4c39 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -36,7 +36,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants [Resolved] private RulesetStore rulesets { get; set; } - private ModDisplay extraModsDisplay; + private ModDisplay userModsDisplay; private StateDisplay userStateDisplay; private SpriteIcon crown; @@ -139,7 +139,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Spacing = new Vector2(10), Children = new Drawable[] { - extraModsDisplay = new ModDisplay + userModsDisplay = new ModDisplay { Anchor = Anchor.Centre, Origin = Anchor.Centre, @@ -174,7 +174,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance(); userStateDisplay.Status = User.State; - extraModsDisplay.Current.Value = User.ExtraMods.Select(m => m.ToMod(ruleset)).ToList(); + userModsDisplay.Current.Value = User.UserMods.Select(m => m.ToMod(ruleset)).ToList(); if (Room.Host?.Equals(User) == true) crown.FadeIn(fade_time); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index e699e7fb34..d0d41e56c3 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -124,18 +124,18 @@ namespace osu.Game.Tests.Visual.Multiplayer return Task.CompletedTask; } - public void ChangeUserExtraMods(int userId, IEnumerable newMods) - => ChangeUserExtraMods(userId, newMods.Select(m => new APIMod(m)).ToList()); + public void ChangeUserMods(int userId, IEnumerable newMods) + => ChangeUserMods(userId, newMods.Select(m => new APIMod(m)).ToList()); - public void ChangeUserExtraMods(int userId, IEnumerable newMods) + public void ChangeUserMods(int userId, IEnumerable newMods) { Debug.Assert(Room != null); - ((IMultiplayerClient)this).UserExtraModsChanged(userId, newMods.ToList()); + ((IMultiplayerClient)this).UserModsChanged(userId, newMods.ToList()); } - public override Task ChangeExtraMods(IEnumerable newMods) + public override Task ChangeUserMods(IEnumerable newMods) { - ChangeUserExtraMods(api.LocalUser.Value.Id, newMods); + ChangeUserMods(api.LocalUser.Value.Id, newMods); return Task.CompletedTask; } From 3cd30d284eb01689fab740342244ad33e37d66f7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 18:08:49 +0900 Subject: [PATCH 6279/6909] Renamespace --- .../TestSceneFreeModSelectOverlay.cs | 2 +- .../OnlinePlay/Match/FooterButtonFreeMods.cs | 63 +++++++++++++++++++ .../FreeModSelectOverlay.cs | 2 +- .../Multiplayer/MultiplayerMatchSongSelect.cs | 54 ---------------- .../OnlinePlay/OnlinePlaySongSelect.cs | 2 +- 5 files changed, 66 insertions(+), 57 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Match/FooterButtonFreeMods.cs rename osu.Game/Screens/OnlinePlay/{Multiplayer => Match}/FreeModSelectOverlay.cs (98%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index 960402df88..b1700d5b6e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -4,7 +4,7 @@ using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Game.Overlays.Mods; -using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Match; namespace osu.Game.Tests.Visual.Multiplayer { diff --git a/osu.Game/Screens/OnlinePlay/Match/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/Match/FooterButtonFreeMods.cs new file mode 100644 index 0000000000..ca2db877c3 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Match/FooterButtonFreeMods.cs @@ -0,0 +1,63 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Graphics; +using osu.Framework.Graphics.UserInterface; +using osu.Game.Graphics; +using osu.Game.Rulesets.Mods; +using osu.Game.Screens.Play.HUD; +using osu.Game.Screens.Select; +using osuTK; + +namespace osu.Game.Screens.OnlinePlay.Match +{ + public class FooterButtonFreeMods : FooterButton, IHasCurrentValue> + { + public Bindable> Current + { + get => modDisplay.Current; + set => modDisplay.Current = value; + } + + private readonly ModDisplay modDisplay; + + public FooterButtonFreeMods() + { + ButtonContentContainer.Add(modDisplay = new ModDisplay + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + DisplayUnrankedText = false, + Scale = new Vector2(0.8f), + ExpansionMode = ExpansionMode.AlwaysContracted, + }); + } + + [BackgroundDependencyLoader] + private void load(OsuColour colours) + { + SelectedColour = colours.Yellow; + DeselectedColour = SelectedColour.Opacity(0.5f); + Text = @"freemods"; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Current.BindValueChanged(_ => updateModDisplay(), true); + } + + private void updateModDisplay() + { + if (Current.Value?.Count > 0) + modDisplay.FadeIn(); + else + modDisplay.FadeOut(); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs similarity index 98% rename from osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs rename to osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs index 8334df1e44..aba86a3d72 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs @@ -10,7 +10,7 @@ using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; -namespace osu.Game.Screens.OnlinePlay.Multiplayer +namespace osu.Game.Screens.OnlinePlay.Match { public class FreeModSelectOverlay : ModSelectOverlay { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 80bb7c7ac2..e6b7656986 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -2,23 +2,15 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; -using osu.Framework.Extensions.Color4Extensions; -using osu.Framework.Graphics; -using osu.Framework.Graphics.UserInterface; using osu.Framework.Logging; using osu.Framework.Screens; -using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; -using osuTK; namespace osu.Game.Screens.OnlinePlay.Multiplayer { @@ -78,50 +70,4 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust); } - - public class FooterButtonFreeMods : FooterButton, IHasCurrentValue> - { - public Bindable> Current - { - get => modDisplay.Current; - set => modDisplay.Current = value; - } - - private readonly ModDisplay modDisplay; - - public FooterButtonFreeMods() - { - ButtonContentContainer.Add(modDisplay = new ModDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - DisplayUnrankedText = false, - Scale = new Vector2(0.8f), - ExpansionMode = ExpansionMode.AlwaysContracted, - }); - } - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - SelectedColour = colours.Yellow; - DeselectedColour = SelectedColour.Opacity(0.5f); - Text = @"freemods"; - } - - protected override void LoadComplete() - { - base.LoadComplete(); - - Current.BindValueChanged(_ => updateModDisplay(), true); - } - - private void updateModDisplay() - { - if (Current.Value?.Count > 0) - modDisplay.FadeIn(); - else - modDisplay.FadeOut(); - } - } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 7c64e00dc4..b75bf93204 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -15,7 +15,7 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.OnlinePlay.Multiplayer; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.Select; namespace osu.Game.Screens.OnlinePlay From 3e74f8fd9e260c48b4240a98bd7bfd2eef2c0598 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 18:11:20 +0900 Subject: [PATCH 6280/6909] Disable customisation of freemods, move stacking to property --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 28 ++++++++----------- .../OnlinePlay/Match/FreeModSelectOverlay.cs | 7 ++--- .../Multiplayer/MultiplayerMatchSubScreen.cs | 8 ++++-- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index db9460c494..56246a031d 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -39,6 +39,16 @@ namespace osu.Game.Overlays.Mods protected readonly OsuSpriteText MultiplierLabel; + /// + /// Whether to allow customisation of mod settings. + /// + protected virtual bool AllowCustomisation => true; + + /// + /// Whether mod icons should be stacked, or appear as individual buttons. + /// + protected virtual bool Stacked => true; + protected override bool BlockNonPositionalInput => false; protected override bool DimMainContent => false; @@ -47,21 +57,6 @@ namespace osu.Game.Overlays.Mods protected readonly ModSettingsContainer ModSettingsContainer; - private bool stacked = true; - - /// - /// Whether mod icons should be stacked, or appear as individual buttons. - /// - public bool Stacked - { - get => stacked; - set - { - stacked = value; - updateAvailableMods(); - } - } - [NotNull] private Func isValidMod = m => true; @@ -307,6 +302,7 @@ namespace osu.Game.Overlays.Mods CustomiseButton = new TriangleButton { Width = 180, + Alpha = AllowCustomisation ? 1 : 0, Text = "Customisation", Action = () => ModSettingsContainer.ToggleVisibility(), Enabled = { Value = false }, @@ -445,7 +441,7 @@ namespace osu.Game.Overlays.Mods { IEnumerable modEnumeration = availableMods.Value[section.ModType]; - if (!stacked) + if (!Stacked) modEnumeration = ModValidation.FlattenMods(modEnumeration); section.Mods = modEnumeration.Where(IsValidMod); diff --git a/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs index aba86a3d72..0d62cc3d37 100644 --- a/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs @@ -14,10 +14,9 @@ namespace osu.Game.Screens.OnlinePlay.Match { public class FreeModSelectOverlay : ModSelectOverlay { - public FreeModSelectOverlay() - { - Stacked = false; - } + protected override bool AllowCustomisation => false; + + protected override bool Stacked => false; protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index a31a3e51ee..22a58add70 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -210,10 +210,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Dimension(GridSizeMode.AutoSize), } }, - userModsSelectOverlay = new SoloModSelectOverlay + userModsSelectOverlay = new UserModSelectOverlay { SelectedMods = { BindTarget = UserMods }, - Stacked = false, IsValidMod = _ => false }, settingsOverlay = new MultiplayerMatchSettingsOverlay @@ -352,5 +351,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client != null) client.LoadRequested -= onLoadRequested; } + + private class UserModSelectOverlay : SoloModSelectOverlay + { + protected override bool Stacked => false; + } } } From e134af82f52373cd3b360ab31139271c5d91007a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 18:16:38 +0900 Subject: [PATCH 6281/6909] Stack freemods for the local user --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 +++++++++++- .../Multiplayer/MultiplayerMatchSubScreen.cs | 7 +------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 56246a031d..2e69f15ff5 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -444,10 +444,20 @@ namespace osu.Game.Overlays.Mods if (!Stacked) modEnumeration = ModValidation.FlattenMods(modEnumeration); - section.Mods = modEnumeration.Where(IsValidMod); + section.Mods = modEnumeration.Select(validModOrNull).Where(m => m != null); } } + [CanBeNull] + private Mod validModOrNull([NotNull] Mod mod) + { + if (!(mod is MultiMod multi)) + return IsValidMod(mod) ? mod : null; + + var validSubset = multi.Mods.Select(validModOrNull).Where(m => m != null).ToArray(); + return validSubset.Length == 0 ? null : new MultiMod(validSubset); + } + private void selectedModsChanged(ValueChangedEvent> mods) { foreach (var section in ModSectionsContainer.Children) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 22a58add70..659551abfd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -210,7 +210,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Dimension(GridSizeMode.AutoSize), } }, - userModsSelectOverlay = new UserModSelectOverlay + userModsSelectOverlay = new SoloModSelectOverlay { SelectedMods = { BindTarget = UserMods }, IsValidMod = _ => false @@ -351,10 +351,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client != null) client.LoadRequested -= onLoadRequested; } - - private class UserModSelectOverlay : SoloModSelectOverlay - { - protected override bool Stacked => false; - } } } From 51cb2887172fb330a8b5ff40961e458c08ef7024 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 18:18:59 +0900 Subject: [PATCH 6282/6909] Reduce mod selection height --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 659551abfd..87ae723c63 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -210,10 +210,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Dimension(GridSizeMode.AutoSize), } }, - userModsSelectOverlay = new SoloModSelectOverlay + new Container { - SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + Child = userModsSelectOverlay = new SoloModSelectOverlay + { + SelectedMods = { BindTarget = UserMods }, + IsValidMod = _ => false + } }, settingsOverlay = new MultiplayerMatchSettingsOverlay { From 3a906a89fce84a1dd7195af89aa7808276c1c70a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 18:25:09 +0900 Subject: [PATCH 6283/6909] Pin mod position in participant panels --- .../TestSceneMultiplayerParticipantsList.cs | 6 +++- .../Participants/ParticipantPanel.cs | 33 ++++++++----------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 8caba5d9c8..768dc6512c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -146,7 +146,11 @@ namespace osu.Game.Tests.Visual.Multiplayer }); }); - AddToggleStep("toggle ready state", v => Client.ChangeUserState(0, v ? MultiplayerUserState.Ready : MultiplayerUserState.Idle)); + for (var i = MultiplayerUserState.Idle; i < MultiplayerUserState.Results; i++) + { + var state = i; + AddStep($"set state: {state}", () => Client.ChangeUserState(0, state)); + } } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index a782da4c39..f6a60c8f57 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -129,32 +129,25 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants } } }, - new FillFlowContainer + new Container { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Margin = new MarginPadding { Right = 10 }, - Spacing = new Vector2(10), - Children = new Drawable[] + Margin = new MarginPadding { Right = 70 }, + Child = userModsDisplay = new ModDisplay { - userModsDisplay = new ModDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Scale = new Vector2(0.5f), - ExpansionMode = ExpansionMode.AlwaysContracted, - DisplayUnrankedText = false, - ExpandOnAppear = false - }, - userStateDisplay = new StateDisplay - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - AlwaysPresent = true - } + Scale = new Vector2(0.5f), + ExpansionMode = ExpansionMode.AlwaysContracted, + DisplayUnrankedText = false, + ExpandOnAppear = false } + }, + userStateDisplay = new StateDisplay + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Margin = new MarginPadding { Right = 10 }, } } } From 89a42d60fbc8f33c6cd28d2baa617b33c57b1312 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 18:50:32 +0900 Subject: [PATCH 6284/6909] General cleanup --- osu.Game.Tests/Mods/ModValidationTest.cs | 14 ++++----- .../TestSceneFreeModSelectOverlay.cs | 5 +-- .../Online/Multiplayer/IMultiplayerClient.cs | 5 +++ .../Multiplayer/IMultiplayerRoomServer.cs | 4 +++ .../Online/Multiplayer/MultiplayerRoomUser.cs | 3 ++ .../Multiplayer/StatefulMultiplayerClient.cs | 6 +++- osu.Game/Overlays/Mods/ModButton.cs | 8 ++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 +++++-- .../OnlinePlay/Match/FreeModSelectOverlay.cs | 31 ++----------------- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- .../Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- .../OnlinePlay/OnlinePlaySongSelect.cs | 18 +++++++++-- osu.Game/Screens/Play/HUD/ModDisplay.cs | 3 ++ osu.Game/Screens/Select/MatchSongSelect.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 4 +++ .../Utils/{ModValidation.cs => ModUtils.cs} | 24 +++----------- 16 files changed, 71 insertions(+), 72 deletions(-) rename osu.Game/Utils/{ModValidation.cs => ModUtils.cs} (79%) diff --git a/osu.Game.Tests/Mods/ModValidationTest.cs b/osu.Game.Tests/Mods/ModValidationTest.cs index c7a7e242e9..991adc221e 100644 --- a/osu.Game.Tests/Mods/ModValidationTest.cs +++ b/osu.Game.Tests/Mods/ModValidationTest.cs @@ -15,7 +15,7 @@ namespace osu.Game.Tests.Mods public void TestModIsCompatibleByItself() { var mod = new Mock(); - Assert.That(ModValidation.CheckCompatible(new[] { mod.Object })); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object })); } [Test] @@ -27,8 +27,8 @@ namespace osu.Game.Tests.Mods mod1.Setup(m => m.IncompatibleMods).Returns(new[] { mod2.Object.GetType() }); // Test both orderings. - Assert.That(ModValidation.CheckCompatible(new[] { mod1.Object, mod2.Object }), Is.False); - Assert.That(ModValidation.CheckCompatible(new[] { mod2.Object, mod1.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod2.Object, mod1.Object }), Is.False); } [Test] @@ -43,22 +43,22 @@ namespace osu.Game.Tests.Mods var multiMod = new MultiMod(new MultiMod(mod2.Object)); // Test both orderings. - Assert.That(ModValidation.CheckCompatible(new[] { multiMod, mod1.Object }), Is.False); - Assert.That(ModValidation.CheckCompatible(new[] { mod1.Object, multiMod }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new[] { multiMod, mod1.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, multiMod }), Is.False); } [Test] public void TestAllowedThroughMostDerivedType() { var mod = new Mock(); - Assert.That(ModValidation.CheckAllowed(new[] { mod.Object }, new[] { mod.Object.GetType() })); + Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { mod.Object.GetType() })); } [Test] public void TestNotAllowedThroughBaseType() { var mod = new Mock(); - Assert.That(ModValidation.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); + Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); } } } diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs index b1700d5b6e..e3342eb6a0 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs @@ -3,19 +3,16 @@ using NUnit.Framework; using osu.Framework.Graphics.Containers; -using osu.Game.Overlays.Mods; using osu.Game.Screens.OnlinePlay.Match; namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneFreeModSelectOverlay : MultiplayerTestScene { - private ModSelectOverlay overlay; - [SetUp] public new void Setup() => Schedule(() => { - Child = overlay = new FreeModSelectOverlay + Child = new FreeModSelectOverlay { State = { Value = Visibility.Visible } }; diff --git a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs index f22b0e4e28..6d7b9d24d6 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerClient.cs @@ -57,6 +57,11 @@ namespace osu.Game.Online.Multiplayer /// The new beatmap availability state of the user. Task UserBeatmapAvailabilityChanged(int userId, BeatmapAvailability beatmapAvailability); + /// + /// Signals that a user in this room changed their local mods. + /// + /// The ID of the user whose mods have changed. + /// The user's new local mods. Task UserModsChanged(int userId, IEnumerable mods); /// diff --git a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs index 71555ae23d..3527ce6314 100644 --- a/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs +++ b/osu.Game/Online/Multiplayer/IMultiplayerRoomServer.cs @@ -49,6 +49,10 @@ namespace osu.Game.Online.Multiplayer /// The proposed new beatmap availability state. Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + /// + /// Change the local user's mods in the currently joined room. + /// + /// The proposed new mods, excluding any required by the room itself. Task ChangeUserMods(IEnumerable newMods); /// diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 4c9643bfce..7de24826dd 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -26,6 +26,9 @@ namespace osu.Game.Online.Multiplayer /// public BeatmapAvailability BeatmapAvailability { get; set; } = BeatmapAvailability.LocallyAvailable(); + /// + /// Any mods applicable only to the local user. + /// [NotNull] public IEnumerable UserMods { get; set; } = Enumerable.Empty(); diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index a0e903e89a..597bee2764 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -232,6 +232,10 @@ namespace osu.Game.Online.Multiplayer public abstract Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability); + /// + /// Change the local user's mods in the currently joined room. + /// + /// The proposed new mods, excluding any required by the room itself. public Task ChangeUserMods(IEnumerable newMods) => ChangeUserMods(newMods.Select(m => new APIMod(m)).ToList()); public abstract Task ChangeUserMods(IEnumerable newMods); @@ -393,7 +397,7 @@ namespace osu.Game.Online.Multiplayer { var user = Room?.Users.SingleOrDefault(u => u.UserID == userId); - // errors here are not critical - user mods is mostly for display. + // errors here are not critical - user mods are mostly for display. if (user == null) return; diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 9ea4f65eb2..ab8efdabcc 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -174,7 +174,7 @@ namespace osu.Game.Overlays.Mods switch (e.Button) { case MouseButton.Right: - OnRightClick(e); + SelectNext(-1); break; } } @@ -183,12 +183,8 @@ namespace osu.Game.Overlays.Mods protected override bool OnClick(ClickEvent e) { SelectNext(1); - return true; - } - protected virtual void OnRightClick(MouseUpEvent e) - { - SelectNext(-1); + return true; } /// diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 2e69f15ff5..2950dc7489 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -61,7 +61,7 @@ namespace osu.Game.Overlays.Mods private Func isValidMod = m => true; /// - /// A function that checks whether a given mod is valid. + /// A function that checks whether a given mod is selectable. /// [NotNull] public Func IsValidMod @@ -442,12 +442,20 @@ namespace osu.Game.Overlays.Mods IEnumerable modEnumeration = availableMods.Value[section.ModType]; if (!Stacked) - modEnumeration = ModValidation.FlattenMods(modEnumeration); + modEnumeration = ModUtils.FlattenMods(modEnumeration); section.Mods = modEnumeration.Select(validModOrNull).Where(m => m != null); } } + /// + /// Returns a valid form of a given if possible, or null otherwise. + /// + /// + /// This is a recursive process during which any invalid mods are culled while preserving structures where possible. + /// + /// The to check. + /// A valid form of if exists, or null otherwise. [CanBeNull] private Mod validModOrNull([NotNull] Mod mod) { diff --git a/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs index 0d62cc3d37..3cdf25fad0 100644 --- a/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs @@ -5,13 +5,15 @@ using System; using System.Linq; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; namespace osu.Game.Screens.OnlinePlay.Match { + /// + /// A used for free-mod selection in online play. + /// public class FreeModSelectOverlay : ModSelectOverlay { protected override bool AllowCustomisation => false; @@ -29,8 +31,6 @@ namespace osu.Game.Screens.OnlinePlay.Match { } - protected override ModButton CreateModButton(Mod mod) => new FreeModButton(mod); - protected override Drawable CreateHeader(string text) => new Container { AutoSizeAxes = Axes.Y, @@ -47,7 +47,6 @@ namespace osu.Game.Screens.OnlinePlay.Match foreach (var button in ButtonsContainer.OfType()) { if (value) - // Note: Buttons where only part of the group has an implementation are not fully supported. button.SelectAt(0); else button.Deselect(); @@ -77,29 +76,5 @@ namespace osu.Game.Screens.OnlinePlay.Match Changed?.Invoke(value); } } - - private class FreeModButton : ModButton - { - public FreeModButton(Mod mod) - : base(mod) - { - } - - protected override bool OnClick(ClickEvent e) - { - onClick(); - return true; - } - - protected override void OnRightClick(MouseUpEvent e) => onClick(); - - private void onClick() - { - if (Selected) - Deselect(); - else - SelectNext(1); - } - } } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 6367aa54a7..f4136c895f 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -107,7 +107,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (SelectedItem.Value == null) return; - // Remove any extra mods that are no longer allowed. + // Remove any user mods that are no longer allowed. UserMods.Value = UserMods.Value .Where(m => SelectedItem.Value.AllowedMods.Any(a => m.GetType() == a.GetType())) .ToList(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index e6b7656986..08c00e8372 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -34,7 +34,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Mods.Value = Playlist.FirstOrDefault()?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); } - protected override void OnSetItem(PlaylistItem item) + protected override void SelectItem(PlaylistItem item) { // If the client is already in a room, update via the client. // Otherwise, update the playlist directly in preparation for it to be submitted to the API on match creation. diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index b75bf93204..46be5591a8 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -76,11 +76,15 @@ namespace osu.Game.Screens.OnlinePlay item.AllowedMods.Clear(); item.AllowedMods.AddRange(freeMods.Value.Select(m => m.CreateCopy())); - OnSetItem(item); + SelectItem(item); return true; } - protected abstract void OnSetItem(PlaylistItem item); + /// + /// Invoked when the user has requested a selection of a beatmap. + /// + /// The resultant . This item has not yet been added to the 's. + protected abstract void SelectItem(PlaylistItem item); public override bool OnBackButton() { @@ -117,8 +121,18 @@ namespace osu.Game.Screens.OnlinePlay return buttons; } + /// + /// Checks whether a given is valid for global selection. + /// + /// The to check. + /// Whether is a valid mod for online play. protected virtual bool IsValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; + /// + /// Checks whether a given is valid for per-player free-mod selection. + /// + /// The to check. + /// Whether is a selectable free-mod. protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod); } } diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 2d5b07f056..ce1a8a3205 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -26,6 +26,9 @@ namespace osu.Game.Screens.Play.HUD public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; + /// + /// Whether the mods should initially appear expanded, before potentially contracting into their final expansion state (depending on ). + /// public bool ExpandOnAppear = true; private readonly Bindable> current = new Bindable>(); diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 23fe9620fe..1de3e0e989 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -21,7 +21,7 @@ namespace osu.Game.Screens.Select CreateNewItem = createNewItem }; - protected override void OnSetItem(PlaylistItem item) + protected override void SelectItem(PlaylistItem item) { switch (Playlist.Count) { diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 4af96b7a29..ed6e0a1028 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -300,6 +300,10 @@ namespace osu.Game.Screens.Select } } + /// + /// Creates the buttons to be displayed in the footer. + /// + /// A set of and an optional which the button opens when pressed. protected virtual IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() => new (FooterButton, OverlayContainer)[] { (new FooterButtonMods { Current = Mods }, ModSelect), diff --git a/osu.Game/Utils/ModValidation.cs b/osu.Game/Utils/ModUtils.cs similarity index 79% rename from osu.Game/Utils/ModValidation.cs rename to osu.Game/Utils/ModUtils.cs index 3597396ec4..808dba2900 100644 --- a/osu.Game/Utils/ModValidation.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -12,9 +12,9 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Utils { /// - /// A set of utilities to validate combinations. + /// A set of utilities to handle combinations. /// - public static class ModValidation + public static class ModUtils { /// /// Checks that all s are compatible with each-other, and that all appear within a set of allowed types. @@ -25,11 +25,11 @@ namespace osu.Game.Utils /// The s to check. /// The set of allowed types. /// Whether all s are compatible with each-other and appear in the set of allowed types. - public static bool CheckCompatibleAndAllowed(IEnumerable combination, IEnumerable allowedTypes) + public static bool CheckCompatibleSetAndAllowed(IEnumerable combination, IEnumerable allowedTypes) { // Prevent multiple-enumeration. var combinationList = combination as ICollection ?? combination.ToArray(); - return CheckCompatible(combinationList) && CheckAllowed(combinationList, allowedTypes); + return CheckCompatibleSet(combinationList) && CheckAllowed(combinationList, allowedTypes); } /// @@ -37,7 +37,7 @@ namespace osu.Game.Utils /// /// The combination to check. /// Whether all s in the combination are compatible with each-other. - public static bool CheckCompatible(IEnumerable combination) + public static bool CheckCompatibleSet(IEnumerable combination) { var incompatibleTypes = new HashSet(); var incomingTypes = new HashSet(); @@ -83,20 +83,6 @@ namespace osu.Game.Utils .All(m => allowedSet.Contains(m.GetType())); } - /// - /// Determines whether a is in a set of incompatible types. - /// - /// - /// A can be incompatible through its most-declared type or any of its base types. - /// - /// The to test. - /// The set of incompatible types. - /// Whether the given is incompatible. - private static bool isModIncompatible(Mod mod, ICollection incompatibleTypes) - => FlattenMod(mod) - .SelectMany(m => m.GetType().EnumerateBaseTypes()) - .Any(incompatibleTypes.Contains); - /// /// Flattens a set of s, returning a new set with all s removed. /// From ee92ec0a5c2e6120f45b36f14b9a678ecab78e8a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 18:54:47 +0900 Subject: [PATCH 6285/6909] Disallow local user mod customisation --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 87ae723c63..f09c7168b3 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -216,7 +216,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, Height = 0.5f, - Child = userModsSelectOverlay = new SoloModSelectOverlay + Child = userModsSelectOverlay = new UserModSelectOverlay { SelectedMods = { BindTarget = UserMods }, IsValidMod = _ => false @@ -358,5 +358,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client != null) client.LoadRequested -= onLoadRequested; } + + private class UserModSelectOverlay : ModSelectOverlay + { + protected override bool AllowCustomisation => false; + } } } From 76ebb3811a03ea45fb5279fab26c9951fe199ba9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 19:05:02 +0900 Subject: [PATCH 6286/6909] Fix mod icons potentially having incorrect colours --- osu.Game/Rulesets/UI/ModIcon.cs | 49 +++++++++++++++++++-------------- 1 file changed, 29 insertions(+), 20 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 8ea6c74349..76fcb8e080 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -26,8 +26,6 @@ namespace osu.Game.Rulesets.UI private const float size = 80; - private readonly ModType type; - public virtual string TooltipText => mod.IconTooltip; private Mod mod; @@ -38,16 +36,22 @@ namespace osu.Game.Rulesets.UI set { mod = value; - updateMod(value); + + if (LoadState >= LoadState.Ready) + updateMod(value); } } + [Resolved] + private OsuColour colours { get; set; } + + private Color4 backgroundColour; + private Color4 highlightedColour; + public ModIcon(Mod mod) { this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); - type = mod.Type; - Size = new Vector2(size); Children = new Drawable[] @@ -79,10 +83,20 @@ namespace osu.Game.Rulesets.UI Icon = FontAwesome.Solid.Question }, }; + } + [BackgroundDependencyLoader] + private void load() + { updateMod(mod); } + protected override void LoadComplete() + { + base.LoadComplete(); + Selected.BindValueChanged(_ => updateColour(), true); + } + private void updateMod(Mod value) { modAcronym.Text = value.Acronym; @@ -92,20 +106,14 @@ namespace osu.Game.Rulesets.UI { modIcon.FadeOut(); modAcronym.FadeIn(); - return; + } + else + { + modIcon.FadeIn(); + modAcronym.FadeOut(); } - modIcon.FadeIn(); - modAcronym.FadeOut(); - } - - private Color4 backgroundColour; - private Color4 highlightedColour; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - switch (type) + switch (value.Type) { default: case ModType.DifficultyIncrease: @@ -139,12 +147,13 @@ namespace osu.Game.Rulesets.UI modIcon.Colour = colours.Yellow; break; } + + updateColour(); } - protected override void LoadComplete() + private void updateColour() { - base.LoadComplete(); - Selected.BindValueChanged(selected => background.Colour = selected.NewValue ? highlightedColour : backgroundColour, true); + background.Colour = Selected.Value ? highlightedColour : backgroundColour; } } } From e5ca9b1e500c06e1cc084bcf4f8d1cc8994a3474 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 19:28:33 +0900 Subject: [PATCH 6287/6909] Remove usage of removed method --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 3322c8ede1..9e1e9f3d69 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -298,7 +298,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client.Room == null) return; - client.ChangeUserMods(mods.NewValue).CatchUnobservedExceptions(); + client.ChangeUserMods(mods.NewValue); } private void onReadyClick() From b9832c1b2d2e55b06170e67c64c05e2f0cf016fc Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 19:37:19 +0900 Subject: [PATCH 6288/6909] Add ModUtils class for validating mod usages --- .../osu.Game.Tests.Android.csproj | 4 + osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj | 1 + osu.Game.Tests/Mods/ModUtilsTest.cs | 64 ++++++++++ osu.Game.Tests/osu.Game.Tests.csproj | 1 + osu.Game/Utils/ModUtils.cs | 109 ++++++++++++++++++ 5 files changed, 179 insertions(+) create mode 100644 osu.Game.Tests/Mods/ModUtilsTest.cs create mode 100644 osu.Game/Utils/ModUtils.cs diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj index c44ed69c4d..19e36a63f1 100644 --- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj +++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj @@ -69,5 +69,9 @@ osu.Game + + + + \ No newline at end of file diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj index ca68369ebb..67b2298f4c 100644 --- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj +++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj @@ -45,6 +45,7 @@ + \ No newline at end of file diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs new file mode 100644 index 0000000000..fdb441343a --- /dev/null +++ b/osu.Game.Tests/Mods/ModUtilsTest.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 Moq; +using NUnit.Framework; +using osu.Game.Rulesets.Mods; +using osu.Game.Utils; + +namespace osu.Game.Tests.Mods +{ + [TestFixture] + public class ModUtilsTest + { + [Test] + public void TestModIsCompatibleByItself() + { + var mod = new Mock(); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object })); + } + + [Test] + public void TestIncompatibleThroughTopLevel() + { + var mod1 = new Mock(); + var mod2 = new Mock(); + + mod1.Setup(m => m.IncompatibleMods).Returns(new[] { mod2.Object.GetType() }); + + // Test both orderings. + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod2.Object, mod1.Object }), Is.False); + } + + [Test] + public void TestIncompatibleThroughMultiMod() + { + var mod1 = new Mock(); + + // The nested mod. + var mod2 = new Mock(); + mod2.Setup(m => m.IncompatibleMods).Returns(new[] { mod1.Object.GetType() }); + + var multiMod = new MultiMod(new MultiMod(mod2.Object)); + + // Test both orderings. + Assert.That(ModUtils.CheckCompatibleSet(new[] { multiMod, mod1.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, multiMod }), Is.False); + } + + [Test] + public void TestAllowedThroughMostDerivedType() + { + var mod = new Mock(); + Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { mod.Object.GetType() })); + } + + [Test] + public void TestNotAllowedThroughBaseType() + { + var mod = new Mock(); + Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); + } + } +} diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index c0c0578391..d29ed94b5f 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -7,6 +7,7 @@ + WinExe diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs new file mode 100644 index 0000000000..808dba2900 --- /dev/null +++ b/osu.Game/Utils/ModUtils.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; +using System.Collections.Generic; +using System.Linq; +using osu.Framework.Extensions.TypeExtensions; +using osu.Game.Rulesets.Mods; + +#nullable enable + +namespace osu.Game.Utils +{ + /// + /// A set of utilities to handle combinations. + /// + public static class ModUtils + { + /// + /// Checks that all s are compatible with each-other, and that all appear within a set of allowed types. + /// + /// + /// The allowed types must contain exact types for the respective s to be allowed. + /// + /// The s to check. + /// The set of allowed types. + /// Whether all s are compatible with each-other and appear in the set of allowed types. + public static bool CheckCompatibleSetAndAllowed(IEnumerable combination, IEnumerable allowedTypes) + { + // Prevent multiple-enumeration. + var combinationList = combination as ICollection ?? combination.ToArray(); + return CheckCompatibleSet(combinationList) && CheckAllowed(combinationList, allowedTypes); + } + + /// + /// Checks that all s in a combination are compatible with each-other. + /// + /// The combination to check. + /// Whether all s in the combination are compatible with each-other. + public static bool CheckCompatibleSet(IEnumerable combination) + { + var incompatibleTypes = new HashSet(); + var incomingTypes = new HashSet(); + + foreach (var mod in combination.SelectMany(FlattenMod)) + { + // Add the new mod incompatibilities, checking whether any match the existing mod types. + foreach (var t in mod.IncompatibleMods) + { + if (incomingTypes.Contains(t)) + return false; + + incompatibleTypes.Add(t); + } + + // Add the new mod types, checking whether any match the incompatible types. + foreach (var t in mod.GetType().EnumerateBaseTypes()) + { + if (incomingTypes.Contains(t)) + return false; + + incomingTypes.Add(t); + } + } + + return true; + } + + /// + /// Checks that all s in a combination appear within a set of allowed types. + /// + /// + /// The set of allowed types must contain exact types for the respective s to be allowed. + /// + /// The combination to check. + /// The set of allowed types. + /// Whether all s in the combination are allowed. + public static bool CheckAllowed(IEnumerable combination, IEnumerable allowedTypes) + { + var allowedSet = new HashSet(allowedTypes); + + return combination.SelectMany(FlattenMod) + .All(m => allowedSet.Contains(m.GetType())); + } + + /// + /// Flattens a set of s, returning a new set with all s removed. + /// + /// The set of s to flatten. + /// The new set, containing all s in recursively with all s removed. + public static IEnumerable FlattenMods(IEnumerable mods) => mods.SelectMany(FlattenMod); + + /// + /// Flattens a , returning a set of s in-place of any s. + /// + /// The to flatten. + /// A set of singular "flattened" s + public static IEnumerable FlattenMod(Mod mod) + { + if (mod is MultiMod multi) + { + foreach (var m in multi.Mods.SelectMany(FlattenMod)) + yield return m; + } + else + yield return mod; + } + } +} From 97247b7a673fa552b9f8f5b66aa87e9dfe2293c0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 19:59:18 +0900 Subject: [PATCH 6289/6909] Fix unset key --- osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index a25c332b47..d0e19d9f37 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -33,6 +33,7 @@ namespace osu.Game.Online.Multiplayer public IEnumerable Mods { get; set; } = Enumerable.Empty(); [NotNull] + [Key(5)] public IEnumerable AllowedMods { get; set; } = Enumerable.Empty(); public bool Equals(MultiplayerRoomSettings other) From 97e3023df9cb680feedadd089b2eb326078a2e39 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Feb 2021 20:16:58 +0900 Subject: [PATCH 6290/6909] Renamespace/rename MatchSongSelect -> PlaylistsSongSelect --- .../Visual/Multiplayer/TestSceneMatchSongSelect.cs | 8 ++++---- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 3 +-- .../Playlists/PlaylistsSongSelect.cs} | 6 +++--- 3 files changed, 8 insertions(+), 9 deletions(-) rename osu.Game/Screens/{Select/MatchSongSelect.cs => OnlinePlay/Playlists/PlaylistsSongSelect.cs} (91%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs index e0fd7d9874..86429ac50e 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs @@ -19,7 +19,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Screens.Select; +using osu.Game.Screens.OnlinePlay.Playlists; namespace osu.Game.Tests.Visual.Multiplayer { @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private RulesetStore rulesets; - private TestMatchSongSelect songSelect; + private TestPlaylistsSongSelect songSelect; [BackgroundDependencyLoader] private void load(GameHost host, AudioManager audio) @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Beatmap.SetDefault(); }); - AddStep("create song select", () => LoadScreen(songSelect = new TestMatchSongSelect())); + AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect())); AddUntilStep("wait for present", () => songSelect.IsCurrentScreen()); } @@ -176,7 +176,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("item has rate 1.5", () => Precision.AlmostEquals(1.5, ((OsuModDoubleTime)Room.Playlist.First().RequiredMods[0]).SpeedChange.Value)); } - private class TestMatchSongSelect : MatchSongSelect + private class TestPlaylistsSongSelect : PlaylistsSongSelect { public new MatchBeatmapDetailArea BeatmapDetails => (MatchBeatmapDetailArea)base.BeatmapDetails; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 22580f0537..cedde373b3 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -13,7 +13,6 @@ using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; -using osu.Game.Screens.Select; using osu.Game.Users; using Footer = osu.Game.Screens.OnlinePlay.Match.Components.Footer; @@ -188,7 +187,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists settingsOverlay = new PlaylistsMatchSettingsOverlay { RelativeSizeAxes = Axes.Both, - EditPlaylist = () => this.Push(new MatchSongSelect()), + EditPlaylist = () => this.Push(new PlaylistsSongSelect()), State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden } } }; diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs similarity index 91% rename from osu.Game/Screens/Select/MatchSongSelect.cs rename to osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 1de3e0e989..0e8db6dfe5 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -6,12 +6,12 @@ using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Screens.Select; -namespace osu.Game.Screens.Select +namespace osu.Game.Screens.OnlinePlay.Playlists { - public class MatchSongSelect : OnlinePlaySongSelect + public class PlaylistsSongSelect : OnlinePlaySongSelect { [Resolved] private BeatmapManager beatmaps { get; set; } From ead8262257ed0749b9b81670aa76b534aa73874f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Feb 2021 20:20:10 +0900 Subject: [PATCH 6291/6909] Add function to check for (and return) invalid mods --- osu.Game/Utils/ModUtils.cs | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 808dba2900..9e638d4f2f 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -83,6 +83,48 @@ namespace osu.Game.Utils .All(m => allowedSet.Contains(m.GetType())); } + /// + /// Check the provided combination of mods are valid for a local gameplay session. + /// + /// The mods to check. + /// Invalid mods, if any where found. Can be null if all mods were valid. + /// Whether the input mods were all valid. If false, will contain all invalid entries. + public static bool CheckValidForGameplay(IEnumerable mods, out Mod[]? invalidMods) + { + mods = mods.ToArray(); + + List? foundInvalid = null; + + void addInvalid(Mod mod) + { + foundInvalid ??= new List(); + foundInvalid.Add(mod); + } + + foreach (var mod in mods) + { + bool valid = mod.Type != ModType.System + && mod.HasImplementation + && !(mod is MultiMod); + + if (!valid) + { + // if this mod was found as invalid, we can exclude it before potentially excluding more incompatible types. + addInvalid(mod); + continue; + } + + foreach (var type in mod.IncompatibleMods) + { + foreach (var invalid in mods.Where(m => type.IsInstanceOfType(m))) + addInvalid(invalid); + } + } + + invalidMods = foundInvalid?.ToArray(); + return foundInvalid == null; + } + /// /// Flattens a set of s, returning a new set with all s removed. /// From 425dc8a210d69c853c58f4d2a10ce0347914261f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 1 Feb 2021 20:20:19 +0900 Subject: [PATCH 6292/6909] Ensure mods are always in a valid state at a game level --- osu.Game/OsuGame.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 5acd6bc73d..a00cd5e6a0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -468,6 +468,12 @@ namespace osu.Game private void modsChanged(ValueChangedEvent> mods) { updateModDefaults(); + + if (!ModUtils.CheckValidForGameplay(mods.NewValue, out var invalid)) + { + // ensure we always have a valid set of mods. + SelectedMods.Value = mods.NewValue.Except(invalid).ToArray(); + } } private void updateModDefaults() From 286726feb0cb93c4f3d54e60d690979bba35c005 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 17:44:30 +0000 Subject: [PATCH 6293/6909] Bump SharpCompress from 0.26.0 to 0.27.1 Bumps [SharpCompress](https://github.com/adamhathcock/sharpcompress) from 0.26.0 to 0.27.1. - [Release notes](https://github.com/adamhathcock/sharpcompress/releases) - [Commits](https://github.com/adamhathcock/sharpcompress/compare/0.26...0.27.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 1552dff17d..d0aeb10c9c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 48dc01f5de..b40e6a3346 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -89,7 +89,7 @@ - + From 0560676236e2b73eb25726b10a07927912b2b201 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 18:09:05 +0000 Subject: [PATCH 6294/6909] Bump ppy.osu.Framework.NativeLibs from 2020.923.0 to 2021.115.0 Bumps [ppy.osu.Framework.NativeLibs](https://github.com/ppy/osu-framework) from 2020.923.0 to 2021.115.0. - [Release notes](https://github.com/ppy/osu-framework/releases) - [Commits](https://github.com/ppy/osu-framework/compare/2020.923.0...2021.115.0) Signed-off-by: dependabot-preview[bot] --- osu.iOS.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.iOS.props b/osu.iOS.props index b40e6a3346..dc3527c687 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -93,6 +93,6 @@ - + From 57213e630806a6e25973fa19588009217a0ad9ea Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 18:09:07 +0000 Subject: [PATCH 6295/6909] Bump DiscordRichPresence from 1.0.169 to 1.0.175 Bumps [DiscordRichPresence](https://github.com/Lachee/discord-rpc-csharp) from 1.0.169 to 1.0.175. - [Release notes](https://github.com/Lachee/discord-rpc-csharp/releases) - [Commits](https://github.com/Lachee/discord-rpc-csharp/commits) Signed-off-by: dependabot-preview[bot] --- osu.Desktop/osu.Desktop.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index cce7907c6c..3e0f0cb7f6 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -30,7 +30,7 @@ - + From 15fcabb1282c8c36c4f005e6acba36a7d239fb11 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 1 Feb 2021 22:04:44 +0300 Subject: [PATCH 6296/6909] Add documentation to auto-scroll leniency MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Overlays/Chat/DrawableChannel.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 4c8513b1d5..0cd5ecc05a 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -241,6 +241,11 @@ namespace osu.Game.Overlays.Chat /// private class ChannelScrollContainer : OsuScrollContainer { + /// + /// The chat will be automatically scrolled to end if and only if + /// the distance between the current scroll position and the end of the scroll + /// is less than this value. + /// private const float auto_scroll_leniency = 10f; private float? lastExtent; From 5c28c030c86a270cd6435955ac82de4daa8a196e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 1 Feb 2021 22:08:55 +0300 Subject: [PATCH 6297/6909] Unconditionally set "autoscroll" state --- osu.Game/Overlays/Chat/DrawableChannel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 0cd5ecc05a..db6a27bf8c 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -261,8 +261,8 @@ namespace osu.Game.Overlays.Chat if ((lastExtent == null || ScrollableExtent > lastExtent) && ShouldAutoScroll) ScrollToEnd(); - else - ShouldAutoScroll = IsScrolledToEnd(auto_scroll_leniency); + + ShouldAutoScroll = IsScrolledToEnd(auto_scroll_leniency); lastExtent = ScrollableExtent; } From 216b0d89a729cff6ad311ebb440f32dcb1e174e6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Feb 2021 19:16:51 +0000 Subject: [PATCH 6298/6909] Bump Sentry from 2.1.8 to 3.0.1 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 2.1.8 to 3.0.1. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Changelog](https://github.com/getsentry/sentry-dotnet/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/2.1.8...3.0.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 d0aeb10c9c..e2b506e187 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + From dcb1626e4d66649304d9307cdc63329f523192f9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 1 Feb 2021 22:38:42 +0300 Subject: [PATCH 6299/6909] Remove no longer necessary field --- osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index 02ef024128..8ea05784e9 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -46,7 +46,6 @@ namespace osu.Game.Tests.Visual.Online private ChannelManager channelManager = new ChannelManager(); private TestStandAloneChatDisplay chatDisplay; - private TestStandAloneChatDisplay chatDisplay2; private int messageIdSequence; private Channel testChannel; @@ -72,7 +71,7 @@ namespace osu.Game.Tests.Visual.Online Size = new Vector2(400, 80), Channel = { Value = testChannel }, }, - chatDisplay2 = new TestStandAloneChatDisplay(true) + new TestStandAloneChatDisplay(true) { Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, From f166c4c4148553e381f694cbdb7cca3fb65d7cf1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 11:04:09 +0900 Subject: [PATCH 6300/6909] Rename test --- ...tSceneMatchSongSelect.cs => TestScenePlaylistsSongSelect.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Tests/Visual/Multiplayer/{TestSceneMatchSongSelect.cs => TestScenePlaylistsSongSelect.cs} (99%) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs similarity index 99% rename from osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs rename to osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 86429ac50e..2f7e59f800 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -23,7 +23,7 @@ using osu.Game.Screens.OnlinePlay.Playlists; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMatchSongSelect : RoomTestScene + public class TestScenePlaylistsSongSelect : RoomTestScene { [Resolved] private BeatmapManager beatmapManager { get; set; } From 4cf52077b6dbb51bfb48f4589ed88f47cab886ca Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 11:11:28 +0900 Subject: [PATCH 6301/6909] Make checkbox also respond to all mods selected --- osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs index 3cdf25fad0..9a3c87f1ff 100644 --- a/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs @@ -57,12 +57,8 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.Update(); - // If any of the buttons aren't selected, deselect the checkbox. - foreach (var button in ButtonsContainer.OfType()) - { - if (button.Mods.Any(m => m.HasImplementation) && !button.Selected) - checkbox.Current.Value = false; - } + var validButtons = ButtonsContainer.OfType().Where(b => b.Mod.HasImplementation); + checkbox.Current.Value = validButtons.All(b => b.Selected); } } From b54f65c28279ffcc4d53b9eebdac6c1b73ac4d30 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 12:48:15 +0900 Subject: [PATCH 6302/6909] Exclude more mods from multiplayer --- .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 08c00e8372..f7f0402555 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -68,6 +68,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust); + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust) && !mod.RequiresConfiguration; } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 46be5591a8..755dbdb55e 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -126,7 +126,7 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a valid mod for online play. - protected virtual bool IsValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; + protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; /// /// Checks whether a given is valid for per-player free-mod selection. From 7a14e14e67db572f8ec1c8154d6e2e0111a0429b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 12:49:49 +0900 Subject: [PATCH 6303/6909] Refactor condition This won't make any noticeable difference, but is the more correct way to handle MultiMod because flattening works through infinite recursion levels. --- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 755dbdb55e..b0062720e0 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -17,6 +17,7 @@ using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.Select; +using osu.Game.Utils; namespace osu.Game.Screens.OnlinePlay { @@ -126,7 +127,7 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a valid mod for online play. - protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; + protected virtual bool IsValidMod(Mod mod) => mod.HasImplementation && !ModUtils.FlattenMod(mod).Any(m => m is ModAutoplay); /// /// Checks whether a given is valid for per-player free-mod selection. From 0d5353008c1d3e9520d7d12bf50d6f23b85d9712 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 13:34:34 +0900 Subject: [PATCH 6304/6909] Update sentry sdk usage --- osu.Game/Utils/SentryLogger.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index e8e41cdbbe..be9d01cde6 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -23,7 +23,7 @@ namespace osu.Game.Utils var options = new SentryOptions { - Dsn = new Dsn("https://5e342cd55f294edebdc9ad604d28bbd3@sentry.io/1255255"), + Dsn = "https://5e342cd55f294edebdc9ad604d28bbd3@sentry.io/1255255", Release = game.Version }; From 9c3c0895cf2a63d3a7517d53fecf8a0d64c149eb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 13:03:46 +0900 Subject: [PATCH 6305/6909] Hide customise button + multiplier label --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 3 ++- osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 25cb75f73a..a7bfac4088 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -37,6 +37,7 @@ namespace osu.Game.Overlays.Mods protected readonly TriangleButton CustomiseButton; protected readonly TriangleButton CloseButton; + protected readonly Drawable MultiplierSection; protected readonly OsuSpriteText MultiplierLabel; /// @@ -316,7 +317,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, - new FillFlowContainer + MultiplierSection = new FillFlowContainer { AutoSizeAxes = Axes.Both, Spacing = new Vector2(footer_button_spacing / 2, 0), diff --git a/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs index 9a3c87f1ff..f22c6603e5 100644 --- a/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs @@ -20,6 +20,12 @@ namespace osu.Game.Screens.OnlinePlay.Match protected override bool Stacked => false; + public FreeModSelectOverlay() + { + CustomiseButton.Alpha = 0; + MultiplierSection.Alpha = 0; + } + protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); private class FreeModSection : ModSection From 87f9e46b164c540d9f7a531510521087f3840a0c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 13:37:25 +0900 Subject: [PATCH 6306/6909] Add option to select all --- osu.Game/Overlays/Mods/ModSection.cs | 8 +++- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 13 +++--- .../OnlinePlay/Match/FreeModSelectOverlay.cs | 40 +++++++++++++++++++ 3 files changed, 53 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 5de629424b..993f4ef9d7 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -91,7 +91,13 @@ namespace osu.Game.Overlays.Mods return base.OnKeyDown(e); } - public void DeselectAll() => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); + public void SelectAll() + { + foreach (var button in buttons.Where(b => !b.Selected)) + button.SelectAt(0); + } + + public void DeselectAll(bool immediate = false) => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null), immediate); /// /// Deselect one or more mods in this section. diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index a7bfac4088..087990d3f8 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -33,6 +33,7 @@ namespace osu.Game.Overlays.Mods { public const float HEIGHT = 510; + protected readonly FillFlowContainer FooterContainer; protected readonly TriangleButton DeselectAllButton; protected readonly TriangleButton CustomiseButton; protected readonly TriangleButton CloseButton; @@ -85,8 +86,6 @@ namespace osu.Game.Overlays.Mods private const float content_width = 0.8f; private const float footer_button_spacing = 20; - private readonly FillFlowContainer footerContainer; - private SampleChannel sampleOn, sampleOff; protected ModSelectOverlay() @@ -275,7 +274,7 @@ namespace osu.Game.Overlays.Mods Colour = new Color4(172, 20, 116, 255), Alpha = 0.5f, }, - footerContainer = new FillFlowContainer + FooterContainer = new FillFlowContainer { Origin = Anchor.BottomCentre, Anchor = Anchor.BottomCentre, @@ -385,8 +384,8 @@ namespace osu.Game.Overlays.Mods { base.PopOut(); - footerContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); - footerContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); + FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); + FooterContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); foreach (var section in ModSectionsContainer.Children) { @@ -400,8 +399,8 @@ namespace osu.Game.Overlays.Mods { base.PopIn(); - footerContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint); - footerContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint); + FooterContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint); + FooterContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint); foreach (var section in ModSectionsContainer.Children) { diff --git a/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs index f22c6603e5..9a676930a0 100644 --- a/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/FreeModSelectOverlay.cs @@ -24,6 +24,46 @@ namespace osu.Game.Screens.OnlinePlay.Match { CustomiseButton.Alpha = 0; MultiplierSection.Alpha = 0; + DeselectAllButton.Alpha = 0; + + Drawable selectAllButton; + Drawable deselectAllButton; + + FooterContainer.AddRange(new[] + { + selectAllButton = new TriangleButton + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Width = 180, + Text = "Select All", + Action = selectAll, + }, + // Unlike the base mod select overlay, this button deselects mods instantaneously. + deselectAllButton = new TriangleButton + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Width = 180, + Text = "Deselect All", + Action = deselectAll, + }, + }); + + FooterContainer.SetLayoutPosition(selectAllButton, -2); + FooterContainer.SetLayoutPosition(deselectAllButton, -1); + } + + private void selectAll() + { + foreach (var section in ModSectionsContainer.Children) + section.SelectAll(); + } + + private void deselectAll() + { + foreach (var section in ModSectionsContainer.Children) + section.DeselectAll(true); } protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); From 1d3dff8c75988867dce9fd6124fdb08bcb84e344 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 13:41:01 +0900 Subject: [PATCH 6307/6909] Refactor ModDisplay flag usage --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index ce1a8a3205..052484afc4 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -113,14 +113,10 @@ namespace osu.Game.Screens.Play.HUD else unrankedText.Hide(); - if (ExpandOnAppear) - { - expand(); - using (iconsContainer.BeginDelayedSequence(1200)) - contract(); - } - else - iconsContainer.TransformSpacingTo(new Vector2(-25, 0)); + expand(); + + using (iconsContainer.BeginDelayedSequence(ExpandOnAppear ? 1200 : 0)) + contract(); } private void expand() From 53cfc3bc6ea4ddda9b8faf32a97b954e5a09c4f9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 13:42:45 +0900 Subject: [PATCH 6308/6909] Make ModIcon a bit more safe --- osu.Game/Rulesets/UI/ModIcon.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 981fe7f4a6..2ff59f4d1a 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -41,7 +41,7 @@ namespace osu.Game.Rulesets.UI { mod = value; - if (LoadState >= LoadState.Ready) + if (IsLoaded) updateMod(value); } } @@ -98,13 +98,15 @@ namespace osu.Game.Rulesets.UI [BackgroundDependencyLoader] private void load() { - updateMod(mod); } protected override void LoadComplete() { base.LoadComplete(); - Selected.BindValueChanged(_ => updateColour(), true); + + Selected.BindValueChanged(_ => updateColour()); + + updateMod(mod); } private void updateMod(Mod value) From 173e20938cd0b87fc47747cc13b8deba60bb9bea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 13:49:58 +0900 Subject: [PATCH 6309/6909] Revert changes to ModDisplay --- .../Multiplayer/Participants/ParticipantPanel.cs | 1 - osu.Game/Screens/Play/HUD/ModDisplay.cs | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 8b907066a8..8036e5f702 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -139,7 +139,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Scale = new Vector2(0.5f), ExpansionMode = ExpansionMode.AlwaysContracted, DisplayUnrankedText = false, - ExpandOnAppear = false } }, userStateDisplay = new StateDisplay diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 052484afc4..68d019bf71 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -26,11 +26,6 @@ namespace osu.Game.Screens.Play.HUD public ExpansionMode ExpansionMode = ExpansionMode.ExpandOnHover; - /// - /// Whether the mods should initially appear expanded, before potentially contracting into their final expansion state (depending on ). - /// - public bool ExpandOnAppear = true; - private readonly Bindable> current = new Bindable>(); public Bindable> Current @@ -115,7 +110,7 @@ namespace osu.Game.Screens.Play.HUD expand(); - using (iconsContainer.BeginDelayedSequence(ExpandOnAppear ? 1200 : 0)) + using (iconsContainer.BeginDelayedSequence(1200)) contract(); } From 4194c9308eedc24e5ad6cc6315eb945e0838a7a8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 13:50:05 +0900 Subject: [PATCH 6310/6909] Add xmldoc --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 087990d3f8..b20c2d9d19 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -288,7 +288,7 @@ namespace osu.Game.Overlays.Mods Vertical = 15, Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, - Children = new Drawable[] + Children = new[] { DeselectAllButton = new TriangleButton { @@ -509,6 +509,10 @@ namespace osu.Game.Overlays.Mods refreshSelectedMods(); } + /// + /// Invoked when a new has been selected. + /// + /// The that has been selected. protected virtual void OnModSelected(Mod mod) { } From 0bce9d68335d78f9c9286af6520e330f3d3f5c59 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 13:54:27 +0900 Subject: [PATCH 6311/6909] Clear freemods when ruleset is changed --- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index b0062720e0..c58632c500 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -60,6 +60,13 @@ namespace osu.Game.Screens.OnlinePlay freeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); FooterPanels.Add(freeModSelectOverlay); + + Ruleset.BindValueChanged(onRulesetChanged); + } + + private void onRulesetChanged(ValueChangedEvent ruleset) + { + freeMods.Value = Array.Empty(); } protected sealed override bool OnStart() From 80d88024d6bbca6db1f9ca2765340bb765f5ae07 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 14:13:50 +0900 Subject: [PATCH 6312/6909] Add basic test coverage of CheckValidForGameplay function --- osu.Game.Tests/Mods/ModUtilsTest.cs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index fdb441343a..88eee5449c 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.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 Moq; using NUnit.Framework; using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Utils; namespace osu.Game.Tests.Mods @@ -60,5 +64,29 @@ namespace osu.Game.Tests.Mods var mod = new Mock(); Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); } + + // test incompatible pair. + [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) }, new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) })] + // test incompatible pair with derived class. + [TestCase(new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) }, new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) })] + // test system mod. + [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModTouchDevice) }, new[] { typeof(OsuModTouchDevice) })] + // test valid. + [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModHardRock) }, null)] + public void TestInvalidModScenarios(Type[] input, Type[] expectedInvalid) + { + List inputMods = new List(); + foreach (var t in input) + inputMods.Add((Mod)Activator.CreateInstance(t)); + + bool isValid = ModUtils.CheckValidForGameplay(inputMods, out var invalid); + + Assert.That(isValid, Is.EqualTo(expectedInvalid == null)); + + if (isValid) + Assert.IsNull(invalid); + else + Assert.That(invalid?.Select(t => t.GetType()), Is.EquivalentTo(expectedInvalid)); + } } } From 1c645601d41491004c96903231c61ce5a163c7b2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 14:14:31 +0900 Subject: [PATCH 6313/6909] Fix typo in xmldoc --- osu.Game/Utils/ModUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 9e638d4f2f..2146abacb6 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -87,7 +87,7 @@ namespace osu.Game.Utils /// Check the provided combination of mods are valid for a local gameplay session. /// /// The mods to check. - /// Invalid mods, if any where found. Can be null if all mods were valid. + /// Invalid mods, if any were found. Can be null if all mods were valid. /// Whether the input mods were all valid. If false, will contain all invalid entries. public static bool CheckValidForGameplay(IEnumerable mods, out Mod[]? invalidMods) { From ed63b571d2185d4b3e86d77a08c0f9ce656f819c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 15:16:26 +0900 Subject: [PATCH 6314/6909] Add "new" override for ScrollToEnd To UserTrackingScrollContainer --- osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs index b8ce34b204..be33c231c9 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -45,5 +45,11 @@ namespace osu.Game.Graphics.Containers UserScrolling = false; base.ScrollTo(value, animated, distanceDecay); } + + public new void ScrollToEnd(bool animated = true, bool allowDuringDrag = false) + { + UserScrolling = false; + base.ScrollToEnd(animated, allowDuringDrag); + } } } From 398ab9c2c2283a8bad2dd2bf513103a606868e89 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 15:16:10 +0900 Subject: [PATCH 6315/6909] Use UserTrackingScrollContainer instead --- .../Online/TestSceneStandAloneChatDisplay.cs | 2 +- osu.Game/Overlays/Chat/DrawableChannel.cs | 32 ++++++++++++------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index 8ea05784e9..e7669262fe 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -208,7 +208,7 @@ namespace osu.Game.Tests.Visual.Online protected DrawableChannel DrawableChannel => InternalChildren.OfType().First(); - protected OsuScrollContainer ScrollContainer => (OsuScrollContainer)((Container)DrawableChannel.Child).Child; + protected UserTrackingScrollContainer ScrollContainer => (UserTrackingScrollContainer)((Container)DrawableChannel.Child).Child; public FillFlowContainer FillFlow => (FillFlowContainer)ScrollContainer.Child; diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index db6a27bf8c..1d021b331a 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -16,6 +16,7 @@ using osu.Framework.Graphics.Shapes; using osu.Game.Graphics; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics.Sprites; +using osu.Framework.Utils; using osu.Game.Graphics.Sprites; namespace osu.Game.Overlays.Chat @@ -147,8 +148,8 @@ namespace osu.Game.Overlays.Chat // due to the scroll adjusts from old messages removal above, a scroll-to-end must be enforced, // to avoid making the container think the user has scrolled back up and unwantedly disable auto-scrolling. - if (scroll.ShouldAutoScroll || newMessages.Any(m => m is LocalMessage)) - ScheduleAfterChildren(() => scroll.ScrollToEnd()); + if (newMessages.Any(m => m is LocalMessage)) + scroll.ScrollToEnd(); }); private void pendingMessageResolved(Message existing, Message updated) => Schedule(() => @@ -239,7 +240,7 @@ namespace osu.Game.Overlays.Chat /// /// An with functionality to automatically scroll whenever the maximum scrollable distance increases. /// - private class ChannelScrollContainer : OsuScrollContainer + private class ChannelScrollContainer : UserTrackingScrollContainer { /// /// The chat will be automatically scrolled to end if and only if @@ -250,21 +251,30 @@ namespace osu.Game.Overlays.Chat private float? lastExtent; - /// - /// Whether this container should automatically scroll to end on the next call to . - /// - public bool ShouldAutoScroll { get; private set; } = true; + protected override void OnUserScroll(float value, bool animated = true, double? distanceDecay = default) + { + base.OnUserScroll(value, animated, distanceDecay); + lastExtent = null; + } protected override void UpdateAfterChildren() { base.UpdateAfterChildren(); - if ((lastExtent == null || ScrollableExtent > lastExtent) && ShouldAutoScroll) - ScrollToEnd(); + // If the user has scrolled to the bottom of the container, we should resume tracking new content. + bool cancelUserScroll = UserScrolling && IsScrolledToEnd(auto_scroll_leniency); - ShouldAutoScroll = IsScrolledToEnd(auto_scroll_leniency); + // If the user hasn't overridden our behaviour and there has been new content added to the container, we should update our scroll position to track it. + bool requiresScrollUpdate = !UserScrolling && (lastExtent == null || Precision.AlmostBigger(ScrollableExtent, lastExtent.Value)); - lastExtent = ScrollableExtent; + if (cancelUserScroll || requiresScrollUpdate) + { + ScheduleAfterChildren(() => + { + ScrollToEnd(); + lastExtent = ScrollableExtent; + }); + } } } } From bb0753f68d79860b0631938dbbea7b9cd9ab2210 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 15:44:03 +0900 Subject: [PATCH 6316/6909] Use a better method of cancelling user scroll --- .../Containers/UserTrackingScrollContainer.cs | 2 ++ osu.Game/Overlays/Chat/DrawableChannel.cs | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs index be33c231c9..17506ce0f5 100644 --- a/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs +++ b/osu.Game/Graphics/Containers/UserTrackingScrollContainer.cs @@ -25,6 +25,8 @@ namespace osu.Game.Graphics.Containers /// public bool UserScrolling { get; private set; } + public void CancelUserScroll() => UserScrolling = false; + public UserTrackingScrollContainer() { } diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index 1d021b331a..de3057e9dc 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -262,17 +262,21 @@ namespace osu.Game.Overlays.Chat base.UpdateAfterChildren(); // If the user has scrolled to the bottom of the container, we should resume tracking new content. - bool cancelUserScroll = UserScrolling && IsScrolledToEnd(auto_scroll_leniency); + if (UserScrolling && IsScrolledToEnd(auto_scroll_leniency)) + CancelUserScroll(); // If the user hasn't overridden our behaviour and there has been new content added to the container, we should update our scroll position to track it. bool requiresScrollUpdate = !UserScrolling && (lastExtent == null || Precision.AlmostBigger(ScrollableExtent, lastExtent.Value)); - if (cancelUserScroll || requiresScrollUpdate) + if (requiresScrollUpdate) { ScheduleAfterChildren(() => { - ScrollToEnd(); - lastExtent = ScrollableExtent; + if (!UserScrolling) + { + ScrollToEnd(); + lastExtent = ScrollableExtent; + } }); } } From 3670bd40c2c8dee50cdbc4ab3a61cf412c603b15 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 15:44:11 +0900 Subject: [PATCH 6317/6909] Add test coverage of user scroll overriding --- .../Online/TestSceneStandAloneChatDisplay.cs | 82 +++++++++++++++---- 1 file changed, 66 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index e7669262fe..0e1c90f88e 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -12,10 +12,11 @@ using NUnit.Framework; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.Containers; using osu.Game.Overlays.Chat; +using osuTK.Input; namespace osu.Game.Tests.Visual.Online { - public class TestSceneStandAloneChatDisplay : OsuTestScene + public class TestSceneStandAloneChatDisplay : OsuManualInputManagerTestScene { private readonly User admin = new User { @@ -128,7 +129,7 @@ namespace osu.Game.Tests.Visual.Online Timestamp = DateTimeOffset.Now })); - AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom); + checkScrolledToBottom(); const int messages_per_call = 10; AddRepeatStep("add many messages", () => @@ -157,7 +158,7 @@ namespace osu.Game.Tests.Visual.Online return true; }); - AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom); + checkScrolledToBottom(); } /// @@ -165,6 +166,58 @@ namespace osu.Game.Tests.Visual.Online /// [Test] public void TestMessageWrappingKeepsAutoScrolling() + { + fillChat(); + + // send message with short words for text wrapping to occur when contracting chat. + sendMessage(); + + AddStep("contract chat", () => chatDisplay.Width -= 100); + checkScrolledToBottom(); + + AddStep("send another message", () => testChannel.AddNewMessages(new Message(messageIdSequence++) + { + Sender = admin, + Content = "As we were saying...", + })); + + checkScrolledToBottom(); + } + + [Test] + public void TestUserScrollOverride() + { + fillChat(); + + sendMessage(); + checkScrolledToBottom(); + + AddStep("User scroll up", () => + { + InputManager.MoveMouseTo(chatDisplay.ScreenSpaceDrawQuad.Centre); + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(chatDisplay.ScreenSpaceDrawQuad.Centre + new Vector2(0, chatDisplay.ScreenSpaceDrawQuad.Height)); + InputManager.ReleaseButton(MouseButton.Left); + }); + + checkNotScrolledToBottom(); + sendMessage(); + checkNotScrolledToBottom(); + + AddRepeatStep("User scroll to bottom", () => + { + InputManager.MoveMouseTo(chatDisplay.ScreenSpaceDrawQuad.Centre); + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(chatDisplay.ScreenSpaceDrawQuad.Centre - new Vector2(0, chatDisplay.ScreenSpaceDrawQuad.Height)); + InputManager.ReleaseButton(MouseButton.Left); + }, 5); + + checkScrolledToBottom(); + sendMessage(); + checkScrolledToBottom(); + } + + private void fillChat() { AddStep("fill chat", () => { @@ -178,27 +231,24 @@ namespace osu.Game.Tests.Visual.Online } }); - AddAssert("ensure scrolled to bottom", () => chatDisplay.ScrolledToBottom); + checkScrolledToBottom(); + } - // send message with short words for text wrapping to occur when contracting chat. + private void sendMessage() + { AddStep("send lorem ipsum", () => testChannel.AddNewMessages(new Message(messageIdSequence++) { Sender = longUsernameUser, Content = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce et bibendum velit.", })); - - AddStep("contract chat", () => chatDisplay.Width -= 100); - AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom); - - AddStep("send another message", () => testChannel.AddNewMessages(new Message(messageIdSequence++) - { - Sender = admin, - Content = "As we were saying...", - })); - - AddUntilStep("ensure still scrolled to bottom", () => chatDisplay.ScrolledToBottom); } + private void checkScrolledToBottom() => + AddUntilStep("is scrolled to bottom", () => chatDisplay.ScrolledToBottom); + + private void checkNotScrolledToBottom() => + AddUntilStep("not scrolled to bottom", () => !chatDisplay.ScrolledToBottom); + private class TestStandAloneChatDisplay : StandAloneChatDisplay { public TestStandAloneChatDisplay(bool textbox = false) From b3105fb2920ac3f3c24e144550cbc598bbcf0cf3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 15:46:26 +0900 Subject: [PATCH 6318/6909] Add coverage of local echo messages performing automatic scrolling --- .../Online/TestSceneStandAloneChatDisplay.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index 0e1c90f88e..ee01eb5f3a 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -217,6 +217,33 @@ namespace osu.Game.Tests.Visual.Online checkScrolledToBottom(); } + [Test] + public void TestLocalEchoMessageResetsScroll() + { + fillChat(); + + sendMessage(); + checkScrolledToBottom(); + + AddStep("User scroll up", () => + { + InputManager.MoveMouseTo(chatDisplay.ScreenSpaceDrawQuad.Centre); + InputManager.PressButton(MouseButton.Left); + InputManager.MoveMouseTo(chatDisplay.ScreenSpaceDrawQuad.Centre + new Vector2(0, chatDisplay.ScreenSpaceDrawQuad.Height)); + InputManager.ReleaseButton(MouseButton.Left); + }); + + checkNotScrolledToBottom(); + sendMessage(); + checkNotScrolledToBottom(); + + sendLocalMessage(); + checkScrolledToBottom(); + + sendMessage(); + checkScrolledToBottom(); + } + private void fillChat() { AddStep("fill chat", () => @@ -243,6 +270,15 @@ namespace osu.Game.Tests.Visual.Online })); } + private void sendLocalMessage() + { + AddStep("send local echo", () => testChannel.AddLocalEcho(new LocalEchoMessage() + { + Sender = longUsernameUser, + Content = "This is a local echo message.", + })); + } + private void checkScrolledToBottom() => AddUntilStep("is scrolled to bottom", () => chatDisplay.ScrolledToBottom); From a76314a8760f98474067decfa23c8ed980def836 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 15:57:17 +0900 Subject: [PATCH 6319/6909] Use Update instead of UpdateAfterChildren (no need for the latter) --- osu.Game/Overlays/Chat/DrawableChannel.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/DrawableChannel.cs b/osu.Game/Overlays/Chat/DrawableChannel.cs index de3057e9dc..86ce724390 100644 --- a/osu.Game/Overlays/Chat/DrawableChannel.cs +++ b/osu.Game/Overlays/Chat/DrawableChannel.cs @@ -257,9 +257,9 @@ namespace osu.Game.Overlays.Chat lastExtent = null; } - protected override void UpdateAfterChildren() + protected override void Update() { - base.UpdateAfterChildren(); + base.Update(); // If the user has scrolled to the bottom of the container, we should resume tracking new content. if (UserScrolling && IsScrolledToEnd(auto_scroll_leniency)) @@ -270,7 +270,8 @@ namespace osu.Game.Overlays.Chat if (requiresScrollUpdate) { - ScheduleAfterChildren(() => + // Schedule required to allow FillFlow to be the correct size. + Schedule(() => { if (!UserScrolling) { From 54c0bdf7d3174ec1b6ee5ba61abf096492d7fa2c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 16:04:42 +0900 Subject: [PATCH 6320/6909] Fix PlaylistLoungeTestScene appearing very narrow --- .../Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 008c862cc3..730bbbb397 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -28,12 +28,7 @@ namespace osu.Game.Tests.Visual.Playlists { base.SetUpSteps(); - AddStep("push screen", () => LoadScreen(loungeScreen = new PlaylistsLoungeSubScreen - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Width = 0.5f, - })); + AddStep("push screen", () => LoadScreen(loungeScreen = new PlaylistsLoungeSubScreen())); AddUntilStep("wait for present", () => loungeScreen.IsCurrentScreen()); } From 3002fef05eebf7c7102ff2e07838347b232d4749 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 16:11:13 +0900 Subject: [PATCH 6321/6909] Remove empty parenthesis --- osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs index ee01eb5f3a..01e67b1681 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneStandAloneChatDisplay.cs @@ -272,7 +272,7 @@ namespace osu.Game.Tests.Visual.Online private void sendLocalMessage() { - AddStep("send local echo", () => testChannel.AddLocalEcho(new LocalEchoMessage() + AddStep("send local echo", () => testChannel.AddLocalEcho(new LocalEchoMessage { Sender = longUsernameUser, Content = "This is a local echo message.", From 43052991f8120b51b6cce28f68f07f9250b09b47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 16:18:55 +0900 Subject: [PATCH 6322/6909] Remove unused using statement --- .../Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs index 730bbbb397..618447eae2 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsLoungeSubScreen.cs @@ -4,7 +4,6 @@ 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; From bdc05af4b7d3fcf43d919c594991f679410ae18d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 16:30:45 +0900 Subject: [PATCH 6323/6909] Make playlist settings area taller to better match screen aspect ratio --- .../OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index 01f9920609..ced6d1c5db 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -200,7 +200,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Child = new GridContainer { RelativeSizeAxes = Axes.X, - Height = 300, + Height = 500, Content = new[] { new Drawable[] From fb52ac8c69792e0101c7f612904cb562b3d1e8c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 16:57:08 +0900 Subject: [PATCH 6324/6909] Share remove from playlist button design with adjacent download button --- .../Graphics/UserInterface/DownloadButton.cs | 60 +++++++------------ osu.Game/Graphics/UserInterface/GrayButton.cs | 48 +++++++++++++++ .../OnlinePlay/DrawableRoomPlaylistItem.cs | 26 +++++++- 3 files changed, 93 insertions(+), 41 deletions(-) create mode 100644 osu.Game/Graphics/UserInterface/GrayButton.cs diff --git a/osu.Game/Graphics/UserInterface/DownloadButton.cs b/osu.Game/Graphics/UserInterface/DownloadButton.cs index 5168ff646b..7a8db158c1 100644 --- a/osu.Game/Graphics/UserInterface/DownloadButton.cs +++ b/osu.Game/Graphics/UserInterface/DownloadButton.cs @@ -4,54 +4,38 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; -using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Game.Online; using osuTK; namespace osu.Game.Graphics.UserInterface { - public class DownloadButton : OsuAnimatedButton + public class DownloadButton : GrayButton { - public readonly Bindable State = new Bindable(); - - private readonly SpriteIcon icon; - private readonly SpriteIcon checkmark; - private readonly Box background; - [Resolved] private OsuColour colours { get; set; } + public readonly Bindable State = new Bindable(); + + private SpriteIcon checkmark; + public DownloadButton() + : base(FontAwesome.Solid.Download) { - Children = new Drawable[] - { - background = new Box - { - RelativeSizeAxes = Axes.Both, - Depth = float.MaxValue - }, - icon = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Size = new Vector2(13), - Icon = FontAwesome.Solid.Download, - }, - checkmark = new SpriteIcon - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - X = 8, - Size = Vector2.Zero, - Icon = FontAwesome.Solid.Check, - } - }; } [BackgroundDependencyLoader] private void load() { + AddInternal(checkmark = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + X = 8, + Size = Vector2.Zero, + Icon = FontAwesome.Solid.Check, + }); + State.BindValueChanged(updateState, true); } @@ -60,27 +44,27 @@ namespace osu.Game.Graphics.UserInterface switch (state.NewValue) { case DownloadState.NotDownloaded: - background.FadeColour(colours.Gray4, 500, Easing.InOutExpo); - icon.MoveToX(0, 500, Easing.InOutExpo); + 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); + 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.Importing: - background.FadeColour(colours.Yellow, 500, Easing.InOutExpo); + 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); + Background.FadeColour(colours.Green, 500, Easing.InOutExpo); + Icon.MoveToX(-8, 500, Easing.InOutExpo); checkmark.ScaleTo(new Vector2(13), 500, Easing.InOutExpo); break; } diff --git a/osu.Game/Graphics/UserInterface/GrayButton.cs b/osu.Game/Graphics/UserInterface/GrayButton.cs new file mode 100644 index 0000000000..dd05701545 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/GrayButton.cs @@ -0,0 +1,48 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Shapes; +using osu.Framework.Graphics.Sprites; +using osuTK; + +namespace osu.Game.Graphics.UserInterface +{ + public class GrayButton : OsuAnimatedButton + { + protected SpriteIcon Icon; + protected Box Background; + + private readonly IconUsage icon; + + [Resolved] + private OsuColour colours { get; set; } + + public GrayButton(IconUsage icon) + { + this.icon = icon; + } + + [BackgroundDependencyLoader] + private void load() + { + Children = new Drawable[] + { + Background = new Box + { + Colour = colours.Gray4, + RelativeSizeAxes = Axes.Both, + Depth = float.MaxValue + }, + Icon = new SpriteIcon + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(13), + Icon = icon, + }, + }; + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index f8982582d5..4316a9508e 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -214,7 +214,8 @@ namespace osu.Game.Screens.OnlinePlay Origin = Anchor.CentreRight, Direction = FillDirection.Horizontal, AutoSizeAxes = Axes.Both, - X = -18, + Spacing = new Vector2(5), + X = -10, ChildrenEnumerable = CreateButtons() } } @@ -225,16 +226,35 @@ namespace osu.Game.Screens.OnlinePlay { new PlaylistDownloadButton(Item) { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, Size = new Vector2(50, 30) }, - new IconButton + new PlaylistRemoveButton { - Icon = FontAwesome.Solid.MinusSquare, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Size = new Vector2(30, 30), Alpha = allowEdit ? 1 : 0, Action = () => RequestDeletion?.Invoke(Model), }, }; + public class PlaylistRemoveButton : GrayButton + { + public PlaylistRemoveButton() + : base(FontAwesome.Solid.MinusSquare) + { + TooltipText = "Remove from playlist"; + } + + [BackgroundDependencyLoader] + private void load() + { + Icon.Scale = new Vector2(0.8f); + } + } + protected override bool OnClick(ClickEvent e) { if (allowSelection) From 6d9ac4d0f01bfefccd38b399ca9158bd4415ce23 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 16:57:27 +0900 Subject: [PATCH 6325/6909] Increase darkness of gradient on buttons to make text readability (slightly) better --- .../TestSceneDrawableRoomPlaylist.cs | 17 ++++++++++++++++- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 14 ++++---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 874c1694eb..16f6723e2d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -20,6 +20,7 @@ using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.OnlinePlay; using osu.Game.Tests.Beatmaps; +using osu.Game.Users; using osuTK; using osuTK.Input; @@ -278,7 +279,21 @@ namespace osu.Game.Tests.Visual.Multiplayer playlist.Items.Add(new PlaylistItem { ID = i, - Beatmap = { Value = new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo }, + Beatmap = + { + Value = i % 2 == 1 + ? new TestBeatmap(new OsuRuleset().RulesetInfo).BeatmapInfo + : new BeatmapInfo + { + Metadata = new BeatmapMetadata + { + Artist = "Artist", + Author = new User { Username = "Creator name here" }, + Title = "Long title used to check background colour", + }, + BeatmapSet = new BeatmapSetInfo() + } + }, Ruleset = { Value = new OsuRuleset().RulesetInfo }, RequiredMods = { diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 4316a9508e..844758b262 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -342,20 +342,14 @@ namespace osu.Game.Screens.OnlinePlay new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.9f)), - Width = 0.05f, + Colour = ColourInfo.GradientHorizontal(Color4.Black, new Color4(0f, 0f, 0f, 0.7f)), + Width = 0.4f, }, new Box { RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.9f), new Color4(0f, 0f, 0f, 0.1f)), - Width = 0.2f, - }, - new Box - { - RelativeSizeAxes = Axes.Both, - Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.1f), new Color4(0, 0, 0, 0)), - Width = 0.05f, + Colour = ColourInfo.GradientHorizontal(new Color4(0f, 0f, 0f, 0.7f), new Color4(0, 0, 0, 0.4f)), + Width = 0.4f, }, } } From 40233fb47cba94b9b48eafdf28944036c301e347 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 17:09:59 +0900 Subject: [PATCH 6326/6909] Make font bolder --- .../OnlinePlay/DrawableRoomPlaylistItem.cs | 155 +++++++++--------- 1 file changed, 80 insertions(+), 75 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 844758b262..a7015ba1c4 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -124,102 +124,107 @@ namespace osu.Game.Screens.OnlinePlay modDisplay.Current.Value = requiredMods.ToArray(); } - protected override Drawable CreateContent() => maskingContainer = new Container + protected override Drawable CreateContent() { - RelativeSizeAxes = Axes.X, - Height = 50, - Masking = true, - CornerRadius = 10, - Children = new Drawable[] + Action fontParameters = s => s.Font = OsuFont.Default.With(weight: FontWeight.SemiBold); + + return maskingContainer = new Container { - new Box // A transparent box that forces the border to be drawn if the panel background is opaque + RelativeSizeAxes = Axes.X, + Height = 50, + Masking = true, + CornerRadius = 10, + Children = new Drawable[] { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - AlwaysPresent = true - }, - new PanelBackground - { - RelativeSizeAxes = Axes.Both, - Beatmap = { BindTarget = beatmap } - }, - new FillFlowContainer - { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Left = 8 }, - Spacing = new Vector2(8, 0), - Direction = FillDirection.Horizontal, - Children = new Drawable[] + new Box // A transparent box that forces the border to be drawn if the panel background is opaque { - difficultyIconContainer = new Container + RelativeSizeAxes = Axes.Both, + Alpha = 0, + AlwaysPresent = true + }, + new PanelBackground + { + RelativeSizeAxes = Axes.Both, + Beatmap = { BindTarget = beatmap } + }, + new FillFlowContainer + { + RelativeSizeAxes = Axes.Both, + Padding = new MarginPadding { Left = 8 }, + Spacing = new Vector2(8, 0), + Direction = FillDirection.Horizontal, + Children = new Drawable[] { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Vertical, - Children = new Drawable[] + difficultyIconContainer = new Container { - beatmapText = new LinkFlowContainer { AutoSizeAxes = Axes.Both }, - new FillFlowContainer + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10f, 0), - Children = new Drawable[] + beatmapText = new LinkFlowContainer(fontParameters) { AutoSizeAxes = Axes.Both }, + new FillFlowContainer { - new FillFlowContainer + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10f, 0), + Children = new Drawable[] { - AutoSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(10f, 0), - Children = new Drawable[] + new FillFlowContainer { - authorText = new LinkFlowContainer { AutoSizeAxes = Axes.Both }, - explicitContentPill = new ExplicitContentBeatmapPill + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10f, 0), + Children = new Drawable[] { - Alpha = 0f, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Margin = new MarginPadding { Top = 3f }, - } + authorText = new LinkFlowContainer(fontParameters) { AutoSizeAxes = Axes.Both }, + explicitContentPill = new ExplicitContentBeatmapPill + { + Alpha = 0f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Margin = new MarginPadding { Top = 3f }, + } + }, }, - }, - new Container - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - AutoSizeAxes = Axes.Both, - Child = modDisplay = new ModDisplay + new Container { - Scale = new Vector2(0.4f), - DisplayUnrankedText = false, - ExpansionMode = ExpansionMode.AlwaysExpanded + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + AutoSizeAxes = Axes.Both, + Child = modDisplay = new ModDisplay + { + Scale = new Vector2(0.4f), + DisplayUnrankedText = false, + ExpansionMode = ExpansionMode.AlwaysExpanded + } } } } } } } + }, + new FillFlowContainer + { + Anchor = Anchor.CentreRight, + Origin = Anchor.CentreRight, + Direction = FillDirection.Horizontal, + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(5), + X = -10, + ChildrenEnumerable = CreateButtons() } - }, - new FillFlowContainer - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Direction = FillDirection.Horizontal, - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(5), - X = -10, - ChildrenEnumerable = CreateButtons() } - } - }; + }; + } protected virtual IEnumerable CreateButtons() => new Drawable[] From bc8a4f411159140371596546144fd56e34bc63d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 17:21:46 +0900 Subject: [PATCH 6327/6909] Update test handling --- .../Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs index 16f6723e2d..960aad10c6 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneDrawableRoomPlaylist.cs @@ -11,8 +11,8 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Platform; using osu.Framework.Testing; using osu.Game.Beatmaps; +using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics.Containers; -using osu.Game.Graphics.UserInterface; using osu.Game.Online.Rooms; using osu.Game.Overlays; using osu.Game.Rulesets; @@ -242,7 +242,7 @@ namespace osu.Game.Tests.Visual.Multiplayer } private void moveToItem(int index, Vector2? offset = null) - => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType>().ElementAt(index), offset)); + => AddStep($"move mouse to item {index}", () => InputManager.MoveMouseTo(playlist.ChildrenOfType().ElementAt(index), offset)); private void moveToDragger(int index, Vector2? offset = null) => AddStep($"move mouse to dragger {index}", () => { @@ -253,7 +253,7 @@ namespace osu.Game.Tests.Visual.Multiplayer private void moveToDeleteButton(int index, Vector2? offset = null) => AddStep($"move mouse to delete button {index}", () => { var item = playlist.ChildrenOfType>().ElementAt(index); - InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset); + InputManager.MoveMouseTo(item.ChildrenOfType().ElementAt(0), offset); }); private void assertHandleVisibility(int index, bool visible) @@ -261,7 +261,7 @@ namespace osu.Game.Tests.Visual.Multiplayer () => (playlist.ChildrenOfType.PlaylistItemHandle>().ElementAt(index).Alpha > 0) == visible); private void assertDeleteButtonVisibility(int index, bool visible) - => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible); + => AddAssert($"delete button {index} {(visible ? "is" : "is not")} visible", () => (playlist.ChildrenOfType().ElementAt(2 + index * 2).Alpha > 0) == visible); private void createPlaylist(bool allowEdit, bool allowSelection) { From 7c29386717cc7f8c96668dc9fb4d970c7b43a17a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:01:33 +0900 Subject: [PATCH 6328/6909] Add failing tests --- osu.Game.Tests/Mods/ModUtilsTest.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index fdb441343a..b602c082bf 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -47,6 +47,29 @@ namespace osu.Game.Tests.Mods Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, multiMod }), Is.False); } + [Test] + public void TestCompatibleMods() + { + var mod1 = new Mock(); + var mod2 = new Mock(); + + // Test both orderings. + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }), Is.True); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod2.Object, mod1.Object }), Is.True); + } + + [Test] + public void TestIncompatibleThroughBaseType() + { + var mod1 = new Mock(); + var mod2 = new Mock(); + mod2.Setup(m => m.IncompatibleMods).Returns(new[] { mod1.Object.GetType().BaseType }); + + // Test both orderings. + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new[] { mod2.Object, mod1.Object }), Is.False); + } + [Test] public void TestAllowedThroughMostDerivedType() { From 8232d9d2fe667a201aded46f4df1dba44b93ae7e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:01:38 +0900 Subject: [PATCH 6329/6909] Fix incorrect implementation --- osu.Game/Utils/ModUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 808dba2900..34bc0faca4 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -56,7 +56,7 @@ namespace osu.Game.Utils // Add the new mod types, checking whether any match the incompatible types. foreach (var t in mod.GetType().EnumerateBaseTypes()) { - if (incomingTypes.Contains(t)) + if (incompatibleTypes.Contains(t)) return false; incomingTypes.Add(t); From d0655c21c6db7e73b006a837b276d2efd0492e27 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:18:57 +0900 Subject: [PATCH 6330/6909] Simplify implementation of CheckCompatibleSet --- osu.Game/Utils/ModUtils.cs | 40 ++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 34bc0faca4..a9271db1b5 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using osu.Framework.Extensions.TypeExtensions; using osu.Game.Rulesets.Mods; #nullable enable @@ -29,7 +28,7 @@ namespace osu.Game.Utils { // Prevent multiple-enumeration. var combinationList = combination as ICollection ?? combination.ToArray(); - return CheckCompatibleSet(combinationList) && CheckAllowed(combinationList, allowedTypes); + return CheckCompatibleSet(combinationList, out _) && CheckAllowed(combinationList, allowedTypes); } /// @@ -38,32 +37,31 @@ namespace osu.Game.Utils /// The combination to check. /// Whether all s in the combination are compatible with each-other. public static bool CheckCompatibleSet(IEnumerable combination) + => CheckCompatibleSet(combination, out _); + + /// + /// Checks that all s in a combination are compatible with each-other. + /// + /// The combination to check. + /// Any invalid mods in the set. + /// Whether all s in the combination are compatible with each-other. + public static bool CheckCompatibleSet(IEnumerable combination, out List? invalidMods) { - var incompatibleTypes = new HashSet(); - var incomingTypes = new HashSet(); + invalidMods = null; - foreach (var mod in combination.SelectMany(FlattenMod)) + foreach (var mod in combination) { - // Add the new mod incompatibilities, checking whether any match the existing mod types. - foreach (var t in mod.IncompatibleMods) + foreach (var type in mod.IncompatibleMods) { - if (incomingTypes.Contains(t)) - return false; - - incompatibleTypes.Add(t); - } - - // Add the new mod types, checking whether any match the incompatible types. - foreach (var t in mod.GetType().EnumerateBaseTypes()) - { - if (incompatibleTypes.Contains(t)) - return false; - - incomingTypes.Add(t); + foreach (var invalid in combination.Where(m => type.IsInstanceOfType(m))) + { + invalidMods ??= new List(); + invalidMods.Add(invalid); + } } } - return true; + return invalidMods == null; } /// From 1df412a03cde9c1ef9363fc1759f8942ea73ec94 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:31:08 +0900 Subject: [PATCH 6331/6909] Fix incorrect handling of multi-mod incompatibilities --- osu.Game.Tests/Mods/ModUtilsTest.cs | 25 ++++++++++++++++++++++++- osu.Game/Utils/ModUtils.cs | 1 + 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index b602c082bf..7d3dea7ed5 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -32,7 +32,7 @@ namespace osu.Game.Tests.Mods } [Test] - public void TestIncompatibleThroughMultiMod() + public void TestMultiModIncompatibleWithTopLevel() { var mod1 = new Mock(); @@ -47,6 +47,21 @@ namespace osu.Game.Tests.Mods Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, multiMod }), Is.False); } + [Test] + public void TestTopLevelIncompatibleWithMultiMod() + { + // The nested mod. + var mod1 = new Mock(); + var multiMod = new MultiMod(new MultiMod(mod1.Object)); + + var mod2 = new Mock(); + mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(CustomMod1) }); + + // Test both orderings. + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { multiMod, mod2.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, multiMod }), Is.False); + } + [Test] public void TestCompatibleMods() { @@ -83,5 +98,13 @@ namespace osu.Game.Tests.Mods var mod = new Mock(); Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); } + + public abstract class CustomMod1 : Mod + { + } + + public abstract class CustomMod2 : Mod + { + } } } diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index a9271db1b5..41f7b1b45c 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -47,6 +47,7 @@ namespace osu.Game.Utils /// Whether all s in the combination are compatible with each-other. public static bool CheckCompatibleSet(IEnumerable combination, out List? invalidMods) { + combination = FlattenMods(combination); invalidMods = null; foreach (var mod in combination) From 9955e0289869be6a14cb67e762a75e9b49a599b1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:33:12 +0900 Subject: [PATCH 6332/6909] Make more tests use the custom mod classes For safety purposes... In implementing the previous tests, I found that using mod.Object.GetType() can lead to bad assertions since the same ModProxy class is used for all mocked classes. --- osu.Game.Tests/Mods/ModUtilsTest.cs | 40 ++++++++++++++--------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index 7d3dea7ed5..e4ded602aa 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -14,37 +14,37 @@ namespace osu.Game.Tests.Mods [Test] public void TestModIsCompatibleByItself() { - var mod = new Mock(); + var mod = new Mock(); Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object })); } [Test] public void TestIncompatibleThroughTopLevel() { - var mod1 = new Mock(); - var mod2 = new Mock(); + var mod1 = new Mock(); + var mod2 = new Mock(); mod1.Setup(m => m.IncompatibleMods).Returns(new[] { mod2.Object.GetType() }); // Test both orderings. - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }), Is.False); - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod2.Object, mod1.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False); } [Test] public void TestMultiModIncompatibleWithTopLevel() { - var mod1 = new Mock(); + var mod1 = new Mock(); // The nested mod. - var mod2 = new Mock(); + var mod2 = new Mock(); mod2.Setup(m => m.IncompatibleMods).Returns(new[] { mod1.Object.GetType() }); var multiMod = new MultiMod(new MultiMod(mod2.Object)); // Test both orderings. - Assert.That(ModUtils.CheckCompatibleSet(new[] { multiMod, mod1.Object }), Is.False); - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, multiMod }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { multiMod, mod1.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, multiMod }), Is.False); } [Test] @@ -65,37 +65,37 @@ namespace osu.Game.Tests.Mods [Test] public void TestCompatibleMods() { - var mod1 = new Mock(); - var mod2 = new Mock(); + var mod1 = new Mock(); + var mod2 = new Mock(); // Test both orderings. - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }), Is.True); - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod2.Object, mod1.Object }), Is.True); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.True); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.True); } [Test] public void TestIncompatibleThroughBaseType() { - var mod1 = new Mock(); - var mod2 = new Mock(); - mod2.Setup(m => m.IncompatibleMods).Returns(new[] { mod1.Object.GetType().BaseType }); + var mod1 = new Mock(); + var mod2 = new Mock(); + mod2.Setup(m => m.IncompatibleMods).Returns(new[] { typeof(Mod) }); // Test both orderings. - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }), Is.False); - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod2.Object, mod1.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod1.Object, mod2.Object }), Is.False); + Assert.That(ModUtils.CheckCompatibleSet(new Mod[] { mod2.Object, mod1.Object }), Is.False); } [Test] public void TestAllowedThroughMostDerivedType() { - var mod = new Mock(); + var mod = new Mock(); Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { mod.Object.GetType() })); } [Test] public void TestNotAllowedThroughBaseType() { - var mod = new Mock(); + var mod = new Mock(); Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); } From 12f52316cd2f198c0d649f41b46ef2975cfbb16d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:37:11 +0900 Subject: [PATCH 6333/6909] Prevent multiple enumeration --- osu.Game/Utils/ModUtils.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 41f7b1b45c..9336add465 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -47,7 +47,7 @@ namespace osu.Game.Utils /// Whether all s in the combination are compatible with each-other. public static bool CheckCompatibleSet(IEnumerable combination, out List? invalidMods) { - combination = FlattenMods(combination); + combination = FlattenMods(combination).ToArray(); invalidMods = null; foreach (var mod in combination) From 052cf1abaedec23cf44022aedc3324dacd5a8168 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:42:02 +0900 Subject: [PATCH 6334/6909] Reuse existing method --- osu.Game/Utils/ModUtils.cs | 30 ++++++------------------------ 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 05a07f0459..0eb30cbe36 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -88,40 +88,22 @@ namespace osu.Game.Utils /// The mods to check. /// Invalid mods, if any were found. Can be null if all mods were valid. /// Whether the input mods were all valid. If false, will contain all invalid entries. - public static bool CheckValidForGameplay(IEnumerable mods, out Mod[]? invalidMods) + public static bool CheckValidForGameplay(IEnumerable mods, out List? invalidMods) { mods = mods.ToArray(); - List? foundInvalid = null; - - void addInvalid(Mod mod) - { - foundInvalid ??= new List(); - foundInvalid.Add(mod); - } + CheckCompatibleSet(mods, out invalidMods); foreach (var mod in mods) { - bool valid = mod.Type != ModType.System - && mod.HasImplementation - && !(mod is MultiMod); - - if (!valid) + if (mod.Type == ModType.System || !mod.HasImplementation || mod is MultiMod) { - // if this mod was found as invalid, we can exclude it before potentially excluding more incompatible types. - addInvalid(mod); - continue; - } - - foreach (var type in mod.IncompatibleMods) - { - foreach (var invalid in mods.Where(m => type.IsInstanceOfType(m))) - addInvalid(invalid); + invalidMods ??= new List(); + invalidMods.Add(mod); } } - invalidMods = foundInvalid?.ToArray(); - return foundInvalid == null; + return invalidMods == null; } /// From 6fdaf025182f29e39243240dff05d85ac6820810 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 17:57:23 +0900 Subject: [PATCH 6335/6909] Hook up room-level max attempts to UI --- .../Screens/OnlinePlay/OnlinePlayComposite.cs | 3 ++ .../PlaylistsMatchSettingsOverlay.cs | 45 +++++++------------ 2 files changed, 18 insertions(+), 30 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index 64792a32f3..b2f3e4a1d9 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -39,6 +39,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable MaxParticipants { get; private set; } + [Resolved(typeof(Room))] + protected Bindable MaxAttempts { get; private set; } + [Resolved(typeof(Room))] protected Bindable EndDate { get; private set; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index 01f9920609..bf85ecf13d 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -42,15 +42,13 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public Action EditPlaylist; - public OsuTextBox NameField, MaxParticipantsField; + public OsuTextBox NameField, MaxParticipantsField, MaxAttemptsField; public OsuDropdown DurationField; public RoomAvailabilityPicker AvailabilityPicker; - public GameTypePicker TypePicker; public TriangleButton ApplyButton; public OsuSpriteText ErrorText; - private OsuSpriteText typeLabel; private LoadingLayer loadingLayer; private DrawableRoomPlaylist playlist; private OsuSpriteText playlistLength; @@ -134,6 +132,15 @@ namespace osu.Game.Screens.OnlinePlay.Playlists } } }, + new Section("Allowed attempts (across all playlist items)") + { + Child = MaxAttemptsField = new SettingsNumberTextBox + { + RelativeSizeAxes = Axes.X, + TabbableContentContainer = this, + PlaceholderText = "Unlimited", + }, + }, new Section("Room visibility") { Alpha = disabled_alpha, @@ -142,30 +149,6 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Enabled = { Value = false } }, }, - new Section("Game type") - { - Alpha = disabled_alpha, - Child = new FillFlowContainer - { - AutoSizeAxes = Axes.Y, - RelativeSizeAxes = Axes.X, - Direction = FillDirection.Vertical, - Spacing = new Vector2(7), - Children = new Drawable[] - { - TypePicker = new GameTypePicker - { - RelativeSizeAxes = Axes.X, - Enabled = { Value = false } - }, - typeLabel = new OsuSpriteText - { - Font = OsuFont.GetFont(size: 14), - Colour = colours.Yellow - }, - }, - }, - }, new Section("Max participants") { Alpha = disabled_alpha, @@ -294,10 +277,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists loadingLayer = new LoadingLayer(true) }; - TypePicker.Current.BindValueChanged(type => typeLabel.Text = type.NewValue?.Name ?? string.Empty, true); RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true); Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true); - Type.BindValueChanged(type => TypePicker.Current.Value = type.NewValue, true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); @@ -326,13 +307,17 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RoomName.Value = NameField.Text; Availability.Value = AvailabilityPicker.Current.Value; - Type.Value = TypePicker.Current.Value; if (int.TryParse(MaxParticipantsField.Text, out int max)) MaxParticipants.Value = max; else MaxParticipants.Value = null; + if (int.TryParse(MaxAttemptsField.Text, out max)) + MaxAttempts.Value = max; + else + MaxAttempts.Value = null; + Duration.Value = DurationField.Current.Value; manager?.CreateRoom(currentRoom.Value, onSuccess, onError); From 90acdd4361550d251ede8fc93db8edda2c5a38d2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 18:06:52 +0900 Subject: [PATCH 6336/6909] Display the correct error message on score submission failure The server will return a valid error message in most cases here. We may eventually want to add some fallback message for cases an error may occur that isn't of [InvariantException](https://github.com/ppy/osu-web/blob/3169b33ccc4c540be5f20136393ad5f00d635ff9/app/Exceptions/InvariantException.php#L9-L10) type, but I'm not 100% sure how to identify these just yet. --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 2c3e7a12e2..7936ab8ecd 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -65,7 +65,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { failed = true; - Logger.Error(e, "Failed to retrieve a score submission token.\n\nThis may happen if you are running an old or non-official release of osu! (ie. you are self-compiling)."); + Logger.Log($"You are not able to submit a score: {e.Message}", LoggingTarget.Information, LogLevel.Important); Schedule(() => { From 96d20bf6072aa181b6fb5662ddc32024be2d61bf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 18:24:51 +0900 Subject: [PATCH 6337/6909] Reduce height of ModeTypeInfo to match adjacent text sections --- osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs index 03b27b605c..2026106c42 100644 --- a/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/ModeTypeInfo.cs @@ -12,7 +12,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { public class ModeTypeInfo : OnlinePlayComposite { - private const float height = 30; + private const float height = 28; private const float transition_duration = 100; private Container drawableRuleset; From 9b209d67dc83568751994c1af8a0713f19339979 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 18:44:13 +0900 Subject: [PATCH 6338/6909] Match size of participants text with host display --- osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs index 0d5ce65d5a..bc4506b78e 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/ParticipantInfo.cs @@ -63,7 +63,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components summary = new OsuSpriteText { Text = "0 participants", - Font = OsuFont.GetFont(size: 14) } }, }, From fc3adaf6123e9f697b85d7c8fa566e111f1eb05b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 18:44:35 +0900 Subject: [PATCH 6339/6909] Show maximum attempt count in room display (when not unlimited) --- .../Components/RoomLocalUserInfo.cs | 50 +++++++++++++++++++ .../OnlinePlay/Lounge/Components/RoomInfo.cs | 39 ++++++--------- 2 files changed, 66 insertions(+), 23 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs b/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs new file mode 100644 index 0000000000..f52e59b0c8 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.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.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics; +using osu.Game.Graphics.Sprites; + +namespace osu.Game.Screens.OnlinePlay.Components +{ + public class RoomLocalUserInfo : OnlinePlayComposite + { + private OsuSpriteText attemptDisplay; + + public RoomLocalUserInfo() + { + AutoSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load() + { + InternalChild = new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Vertical, + Children = new Drawable[] + { + attemptDisplay = new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 14) + }, + } + }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + MaxAttempts.BindValueChanged(attempts => + { + attemptDisplay.Text = attempts.NewValue == null + ? string.Empty + : $"Maximum attempts: {attempts.NewValue:N0}"; + }, true); + } + } +} diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs index 0a17702f2a..a0a7f2dc28 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomInfo.cs @@ -20,41 +20,34 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components { AutoSizeAxes = Axes.Y; + RoomLocalUserInfo localUserInfo; RoomStatusInfo statusInfo; ModeTypeInfo typeInfo; ParticipantInfo participantInfo; InternalChild = new FillFlowContainer { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, RelativeSizeAxes = Axes.X, + Spacing = new Vector2(0, 10), AutoSizeAxes = Axes.Y, Direction = FillDirection.Vertical, - Spacing = new Vector2(0, 4), Children = new Drawable[] { + roomName = new OsuTextFlowContainer(t => t.Font = OsuFont.GetFont(size: 30)) + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + }, + participantInfo = new ParticipantInfo(), new Container { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, Children = new Drawable[] { - new FillFlowContainer - { - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Direction = FillDirection.Vertical, - Children = new Drawable[] - { - roomName = new OsuTextFlowContainer(t => t.Font = OsuFont.GetFont(size: 30)) - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - }, - statusInfo = new RoomStatusInfo(), - } - }, + statusInfo = new RoomStatusInfo(), typeInfo = new ModeTypeInfo { Anchor = Anchor.BottomRight, @@ -62,20 +55,21 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } } }, - participantInfo = new ParticipantInfo(), + localUserInfo = new RoomLocalUserInfo(), } }; - statusElements.AddRange(new Drawable[] { statusInfo, typeInfo, participantInfo }); + statusElements.AddRange(new Drawable[] + { + statusInfo, typeInfo, participantInfo, localUserInfo + }); } protected override void LoadComplete() { base.LoadComplete(); - if (RoomID.Value == null) statusElements.ForEach(e => e.FadeOut()); - RoomID.BindValueChanged(id => { if (id.NewValue == null) @@ -83,7 +77,6 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components else statusElements.ForEach(e => e.FadeIn(100)); }, true); - RoomName.BindValueChanged(name => { roomName.Text = name.NewValue ?? "No room selected"; From 0a9861d0abe19879a3d9772c248df7ed2b0c2e74 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:51:13 +0900 Subject: [PATCH 6340/6909] Use TestCaseSource and add multi-mod test --- osu.Game.Tests/Mods/ModUtilsTest.cs | 47 +++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index ff1af88bac..fbdb1e2f3d 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.cs @@ -103,20 +103,43 @@ namespace osu.Game.Tests.Mods Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); } - // test incompatible pair. - [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) }, new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) })] - // test incompatible pair with derived class. - [TestCase(new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) }, new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) })] - // test system mod. - [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModTouchDevice) }, new[] { typeof(OsuModTouchDevice) })] - // test valid. - [TestCase(new[] { typeof(OsuModDoubleTime), typeof(OsuModHardRock) }, null)] - public void TestInvalidModScenarios(Type[] input, Type[] expectedInvalid) + private static readonly object[] invalid_mod_test_scenarios = { - List inputMods = new List(); - foreach (var t in input) - inputMods.Add((Mod)Activator.CreateInstance(t)); + // incompatible pair. + new object[] + { + new Mod[] { new OsuModDoubleTime(), new OsuModHalfTime() }, + new[] { typeof(OsuModDoubleTime), typeof(OsuModHalfTime) } + }, + // incompatible pair with derived class. + new object[] + { + new Mod[] { new OsuModNightcore(), new OsuModHalfTime() }, + new[] { typeof(OsuModNightcore), typeof(OsuModHalfTime) } + }, + // system mod. + new object[] + { + new Mod[] { new OsuModDoubleTime(), new OsuModTouchDevice() }, + new[] { typeof(OsuModTouchDevice) } + }, + // multi mod. + new object[] + { + new Mod[] { new MultiMod(new OsuModHalfTime()), new OsuModHalfTime() }, + new[] { typeof(MultiMod) } + }, + // valid pair. + new object[] + { + new Mod[] { new OsuModDoubleTime(), new OsuModHardRock() }, + null + } + }; + [TestCaseSource(nameof(invalid_mod_test_scenarios))] + public void TestInvalidModScenarios(Mod[] inputMods, Type[] expectedInvalid) + { bool isValid = ModUtils.CheckValidForGameplay(inputMods, out var invalid); Assert.That(isValid, Is.EqualTo(expectedInvalid == null)); From 180af3c7f8311d0a4477ecb79e7f7403da9dc85a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 19:02:09 +0900 Subject: [PATCH 6341/6909] Add codeanalysis attribute --- osu.Game/Utils/ModUtils.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Utils/ModUtils.cs b/osu.Game/Utils/ModUtils.cs index 9336add465..8ac5bde65a 100644 --- a/osu.Game/Utils/ModUtils.cs +++ b/osu.Game/Utils/ModUtils.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using osu.Game.Rulesets.Mods; @@ -45,7 +46,7 @@ namespace osu.Game.Utils /// The combination to check. /// Any invalid mods in the set. /// Whether all s in the combination are compatible with each-other. - public static bool CheckCompatibleSet(IEnumerable combination, out List? invalidMods) + public static bool CheckCompatibleSet(IEnumerable combination, [NotNullWhen(false)] out List? invalidMods) { combination = FlattenMods(combination).ToArray(); invalidMods = null; From a2e3b1c0e454e81c6836b44ca6f29426040b6ac1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 18:56:57 +0900 Subject: [PATCH 6342/6909] Move Mods reset code to OnlinePlaySongSelect --- .../Multiplayer/MultiplayerMatchSongSelect.cs | 9 --------- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 11 ++++++++++- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index f7f0402555..84e8849726 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -1,8 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Screens; @@ -27,13 +25,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer AddInternal(loadingLayer = new LoadingLayer(true)); } - protected override void LoadComplete() - { - base.LoadComplete(); - - Mods.Value = Playlist.FirstOrDefault()?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); - } - protected override void SelectItem(PlaylistItem item) { // If the client is already in a room, update via the client. diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index c58632c500..1c345b883f 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -58,8 +58,17 @@ namespace osu.Game.Screens.OnlinePlay initialRuleset = Ruleset.Value; initialMods = Mods.Value.ToList(); - freeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); FooterPanels.Add(freeModSelectOverlay); + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. + // Similarly, freeMods is currently empty but should only contain the allowed mods. + Mods.Value = Playlist.FirstOrDefault()?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + freeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); Ruleset.BindValueChanged(onRulesetChanged); } From 41593ff09e4ef0ac405cbd0fdcdba0beff9a29b5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 19:14:44 +0900 Subject: [PATCH 6343/6909] Privatise protected property setters --- osu.Game/Graphics/UserInterface/GrayButton.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/GrayButton.cs b/osu.Game/Graphics/UserInterface/GrayButton.cs index dd05701545..88c46f29e0 100644 --- a/osu.Game/Graphics/UserInterface/GrayButton.cs +++ b/osu.Game/Graphics/UserInterface/GrayButton.cs @@ -11,8 +11,8 @@ namespace osu.Game.Graphics.UserInterface { public class GrayButton : OsuAnimatedButton { - protected SpriteIcon Icon; - protected Box Background; + protected SpriteIcon Icon { get; private set; } + protected Box Background { get; private set; } private readonly IconUsage icon; From 8e70a50af0836af6fb821c69a7239c7cad9e1eec Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Feb 2021 19:22:13 +0900 Subject: [PATCH 6344/6909] Remove unused using statement --- osu.Game.Tests/Mods/ModUtilsTest.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Mods/ModUtilsTest.cs b/osu.Game.Tests/Mods/ModUtilsTest.cs index fbdb1e2f3d..7dcaabca3d 100644 --- a/osu.Game.Tests/Mods/ModUtilsTest.cs +++ b/osu.Game.Tests/Mods/ModUtilsTest.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 Moq; using NUnit.Framework; From 2dece12a7c6041fb8ff9e5a4896c77b78865de3a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 19:57:42 +0900 Subject: [PATCH 6345/6909] Disable/disallow freemods on incompatible/selected mods --- osu.Game/Overlays/Mods/ModSection.cs | 6 +++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 11 ++++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 993f4ef9d7..728c726b82 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -130,13 +130,13 @@ namespace osu.Game.Overlays.Mods /// Updates all buttons with the given list of selected mods. /// /// The new list of selected mods to select. - public void UpdateSelectedMods(IReadOnlyList newSelectedMods) + public void UpdateSelectedButtons(IReadOnlyList newSelectedMods) { foreach (var button in buttons) - updateButtonMods(button, newSelectedMods); + updateButtonSelection(button, newSelectedMods); } - private void updateButtonMods(ModButton button, IReadOnlyList newSelectedMods) + private void updateButtonSelection(ModButton button, IReadOnlyList newSelectedMods) { foreach (var mod in newSelectedMods) { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index b20c2d9d19..56d6008b00 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -377,7 +377,7 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); availableMods.BindValueChanged(_ => updateAvailableMods(), true); - SelectedMods.BindValueChanged(selectedModsChanged, true); + SelectedMods.BindValueChanged(_ => updateSelectedButtons(), true); } protected override void PopOut() @@ -445,6 +445,8 @@ namespace osu.Game.Overlays.Mods section.Mods = modEnumeration.Select(validModOrNull).Where(m => m != null); } + + updateSelectedButtons(); } /// @@ -465,10 +467,13 @@ namespace osu.Game.Overlays.Mods return validSubset.Length == 0 ? null : new MultiMod(validSubset); } - private void selectedModsChanged(ValueChangedEvent> mods) + private void updateSelectedButtons() { + // Enumeration below may update the bindable list. + var selectedMods = SelectedMods.Value.ToList(); + foreach (var section in ModSectionsContainer.Children) - section.UpdateSelectedMods(mods.NewValue); + section.UpdateSelectedButtons(selectedMods); updateMods(); } From 3741f05ab335d5baeb755bd4e370310698c1fe7f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 20:11:40 +0900 Subject: [PATCH 6346/6909] Refactor mod sections and make them overridable --- osu.Game/Overlays/Mods/ModSection.cs | 39 +++++++++---------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 38 ++++++++++++++---- .../Mods/Sections/AutomationSection.cs | 19 --------- .../Mods/Sections/ConversionSection.cs | 19 --------- .../Sections/DifficultyIncreaseSection.cs | 19 --------- .../Sections/DifficultyReductionSection.cs | 19 --------- osu.Game/Overlays/Mods/Sections/FunSection.cs | 19 --------- 7 files changed, 50 insertions(+), 122 deletions(-) delete mode 100644 osu.Game/Overlays/Mods/Sections/AutomationSection.cs delete mode 100644 osu.Game/Overlays/Mods/Sections/ConversionSection.cs delete mode 100644 osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs delete mode 100644 osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs delete mode 100644 osu.Game/Overlays/Mods/Sections/FunSection.cs diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 573d1e5355..89a3e2f5cd 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -11,26 +11,23 @@ using System; using System.Linq; using System.Collections.Generic; using System.Threading; +using Humanizer; using osu.Framework.Input.Events; using osu.Game.Graphics; namespace osu.Game.Overlays.Mods { - public abstract class ModSection : Container + public class ModSection : CompositeDrawable { - private readonly OsuSpriteText headerLabel; + private readonly Drawable header; public FillFlowContainer ButtonsContainer { get; } public Action Action; - protected abstract Key[] ToggleKeys { get; } - public abstract ModType ModType { get; } - public string Header - { - get => headerLabel.Text; - set => headerLabel.Text = value; - } + public Key[] ToggleKeys; + + public readonly ModType ModType; public IEnumerable SelectedMods => buttons.Select(b => b.SelectedMod).Where(m => m != null); @@ -61,7 +58,7 @@ namespace osu.Game.Overlays.Mods if (modContainers.Length == 0) { ModIconsLoaded = true; - headerLabel.Hide(); + header.Hide(); Hide(); return; } @@ -76,7 +73,7 @@ namespace osu.Game.Overlays.Mods buttons = modContainers.OfType().ToArray(); - headerLabel.FadeIn(200); + header.FadeIn(200); this.FadeIn(200); } } @@ -153,23 +150,19 @@ namespace osu.Game.Overlays.Mods button.Deselect(); } - protected ModSection() + public ModSection(ModType type) { + ModType = type; + AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; Origin = Anchor.TopCentre; Anchor = Anchor.TopCentre; - Children = new Drawable[] + InternalChildren = new[] { - headerLabel = new OsuSpriteText - { - Origin = Anchor.TopLeft, - Anchor = Anchor.TopLeft, - Position = new Vector2(0f, 0f), - Font = OsuFont.GetFont(weight: FontWeight.Bold) - }, + header = CreateHeader(type.Humanize(LetterCasing.Title)), ButtonsContainer = new FillFlowContainer { AutoSizeAxes = Axes.Y, @@ -185,5 +178,11 @@ namespace osu.Game.Overlays.Mods }, }; } + + protected virtual Drawable CreateHeader(string text) => new OsuSpriteText + { + Font = OsuFont.GetFont(weight: FontWeight.Bold), + Text = text + }; } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 1258ba719d..fd6f771f16 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -19,7 +19,6 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; using osu.Game.Input.Bindings; -using osu.Game.Overlays.Mods.Sections; using osu.Game.Rulesets.Mods; using osu.Game.Screens; using osuTK; @@ -190,13 +189,31 @@ namespace osu.Game.Overlays.Mods Width = content_width, LayoutDuration = 200, LayoutEasing = Easing.OutQuint, - Children = new ModSection[] + Children = new[] { - new DifficultyReductionSection { Action = modButtonPressed }, - new DifficultyIncreaseSection { Action = modButtonPressed }, - new AutomationSection { Action = modButtonPressed }, - new ConversionSection { Action = modButtonPressed }, - new FunSection { Action = modButtonPressed }, + CreateModSection(ModType.DifficultyReduction).With(s => + { + s.ToggleKeys = new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }; + s.Action = modButtonPressed; + }), + CreateModSection(ModType.DifficultyIncrease).With(s => + { + s.ToggleKeys = new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }; + s.Action = modButtonPressed; + }), + CreateModSection(ModType.Automation).With(s => + { + s.ToggleKeys = new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }; + s.Action = modButtonPressed; + }), + CreateModSection(ModType.Conversion).With(s => + { + s.Action = modButtonPressed; + }), + CreateModSection(ModType.Fun).With(s => + { + s.Action = modButtonPressed; + }), } }, } @@ -454,6 +471,13 @@ namespace osu.Game.Overlays.Mods private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); + /// + /// Creates a that groups s with the same . + /// + /// The of s in the section. + /// The . + protected virtual ModSection CreateModSection(ModType type) => new ModSection(type); + #region Disposal protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Overlays/Mods/Sections/AutomationSection.cs b/osu.Game/Overlays/Mods/Sections/AutomationSection.cs deleted file mode 100644 index a2d7fec15f..0000000000 --- a/osu.Game/Overlays/Mods/Sections/AutomationSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class AutomationSection : ModSection - { - protected override Key[] ToggleKeys => new[] { Key.Z, Key.X, Key.C, Key.V, Key.B, Key.N, Key.M }; - public override ModType ModType => ModType.Automation; - - public AutomationSection() - { - Header = @"Automation"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/ConversionSection.cs b/osu.Game/Overlays/Mods/Sections/ConversionSection.cs deleted file mode 100644 index 24fd8c30dd..0000000000 --- a/osu.Game/Overlays/Mods/Sections/ConversionSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class ConversionSection : ModSection - { - protected override Key[] ToggleKeys => null; - public override ModType ModType => ModType.Conversion; - - public ConversionSection() - { - Header = @"Conversion"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs b/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs deleted file mode 100644 index 0b7ccd1f25..0000000000 --- a/osu.Game/Overlays/Mods/Sections/DifficultyIncreaseSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class DifficultyIncreaseSection : ModSection - { - protected override Key[] ToggleKeys => new[] { Key.A, Key.S, Key.D, Key.F, Key.G, Key.H, Key.J, Key.K, Key.L }; - public override ModType ModType => ModType.DifficultyIncrease; - - public DifficultyIncreaseSection() - { - Header = @"Difficulty Increase"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs b/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs deleted file mode 100644 index 508e92508b..0000000000 --- a/osu.Game/Overlays/Mods/Sections/DifficultyReductionSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class DifficultyReductionSection : ModSection - { - protected override Key[] ToggleKeys => new[] { Key.Q, Key.W, Key.E, Key.R, Key.T, Key.Y, Key.U, Key.I, Key.O, Key.P }; - public override ModType ModType => ModType.DifficultyReduction; - - public DifficultyReductionSection() - { - Header = @"Difficulty Reduction"; - } - } -} diff --git a/osu.Game/Overlays/Mods/Sections/FunSection.cs b/osu.Game/Overlays/Mods/Sections/FunSection.cs deleted file mode 100644 index af1f5836b1..0000000000 --- a/osu.Game/Overlays/Mods/Sections/FunSection.cs +++ /dev/null @@ -1,19 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using osu.Game.Rulesets.Mods; -using osuTK.Input; - -namespace osu.Game.Overlays.Mods.Sections -{ - public class FunSection : ModSection - { - protected override Key[] ToggleKeys => null; - public override ModType ModType => ModType.Fun; - - public FunSection() - { - Header = @"Fun"; - } - } -} From 6d620264f48369f807a79983da19bf2ff37773e9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 20:27:41 +0900 Subject: [PATCH 6347/6909] Allow mod buttons to not be stacked --- .../TestSceneModSelectOverlay.cs | 62 ++++++++++++------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 12 +++- 2 files changed, 51 insertions(+), 23 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index bd4010a7f3..71c549b433 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -38,28 +38,7 @@ namespace osu.Game.Tests.Visual.UserInterface } [SetUp] - public void SetUp() => Schedule(() => - { - SelectedMods.Value = Array.Empty(); - Children = new Drawable[] - { - modSelect = new TestModSelectOverlay - { - Origin = Anchor.BottomCentre, - Anchor = Anchor.BottomCentre, - SelectedMods = { BindTarget = SelectedMods } - }, - - modDisplay = new ModDisplay - { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - AutoSizeAxes = Axes.Both, - Position = new Vector2(-5, 25), - Current = { BindTarget = modSelect.SelectedMods } - } - }; - }); + public void SetUp() => Schedule(() => createDisplay(() => new TestModSelectOverlay())); [SetUpSteps] public void SetUpSteps() @@ -146,6 +125,18 @@ namespace osu.Game.Tests.Visual.UserInterface }); } + [Test] + public void TestNonStacked() + { + changeRuleset(0); + + AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay())); + + AddStep("show", () => modSelect.Show()); + + AddAssert("ensure all buttons are spread out", () => modSelect.ChildrenOfType().All(m => m.Mods.Length <= 1)); + } + private void testSingleMod(Mod mod) { selectNext(mod); @@ -265,6 +256,28 @@ namespace osu.Game.Tests.Visual.UserInterface private void checkLabelColor(Func getColour) => AddAssert("check label has expected colour", () => modSelect.MultiplierLabel.Colour.AverageColour == getColour()); + private void createDisplay(Func createOverlayFunc) + { + SelectedMods.Value = Array.Empty(); + Children = new Drawable[] + { + modSelect = createOverlayFunc().With(d => + { + d.Origin = Anchor.BottomCentre; + d.Anchor = Anchor.BottomCentre; + d.SelectedMods.BindTarget = SelectedMods; + }), + modDisplay = new ModDisplay + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + AutoSizeAxes = Axes.Both, + Position = new Vector2(-5, 25), + Current = { BindTarget = modSelect.SelectedMods } + } + }; + } + private class TestModSelectOverlay : ModSelectOverlay { public new Bindable> SelectedMods => base.SelectedMods; @@ -283,5 +296,10 @@ namespace osu.Game.Tests.Visual.UserInterface public new Color4 LowMultiplierColour => base.LowMultiplierColour; public new Color4 HighMultiplierColour => base.HighMultiplierColour; } + + private class TestNonStackedModSelectOverlay : TestModSelectOverlay + { + protected override bool Stacked => false; + } } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 1258ba719d..c7e856028a 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -22,6 +22,7 @@ using osu.Game.Input.Bindings; using osu.Game.Overlays.Mods.Sections; using osu.Game.Rulesets.Mods; using osu.Game.Screens; +using osu.Game.Utils; using osuTK; using osuTK.Graphics; using osuTK.Input; @@ -43,6 +44,11 @@ namespace osu.Game.Overlays.Mods protected override bool DimMainContent => false; + /// + /// Whether s underneath the same instance should appear as stacked buttons. + /// + protected virtual bool Stacked => true; + protected readonly FillFlowContainer ModSectionsContainer; protected readonly ModSettingsContainer ModSettingsContainer; @@ -405,7 +411,11 @@ namespace osu.Game.Overlays.Mods if (mods.NewValue == null) return; foreach (var section in ModSectionsContainer.Children) - section.Mods = mods.NewValue[section.ModType].Where(isValidMod); + { + section.Mods = Stacked + ? availableMods.Value[section.ModType] + : ModUtils.FlattenMods(availableMods.Value[section.ModType]); + } } private void selectedModsChanged(ValueChangedEvent> mods) From 75f81bfa062c9199759cd8257a10e64e543eb8ed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 20:35:31 +0900 Subject: [PATCH 6348/6909] Add back mod validation --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index c7e856028a..775b0de1c0 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -412,9 +412,12 @@ namespace osu.Game.Overlays.Mods foreach (var section in ModSectionsContainer.Children) { - section.Mods = Stacked - ? availableMods.Value[section.ModType] - : ModUtils.FlattenMods(availableMods.Value[section.ModType]); + IEnumerable modEnumeration = availableMods.Value[section.ModType]; + + if (!Stacked) + modEnumeration = ModUtils.FlattenMods(modEnumeration); + + section.Mods = modEnumeration.Where(isValidMod); } } From 10ceddf3ffcf861f71aee5f4a681441b15913226 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 20:47:50 +0900 Subject: [PATCH 6349/6909] Make IsValidMod adjustable --- .../TestSceneModSelectOverlay.cs | 16 ++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 54 ++++++++++++++++--- .../Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- osu.Game/Screens/Select/MatchSongSelect.cs | 2 +- 4 files changed, 64 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 71c549b433..9cf8b95ddf 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -137,6 +137,22 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("ensure all buttons are spread out", () => modSelect.ChildrenOfType().All(m => m.Mods.Length <= 1)); } + [Test] + public void TestChangeIsValidChangesButtonVisibility() + { + changeRuleset(0); + + AddAssert("double time visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModDoubleTime))); + + AddStep("make double time invalid", () => modSelect.IsValidMod = m => !(m is OsuModDoubleTime)); + AddAssert("double time not visible", () => modSelect.ChildrenOfType().All(b => !b.Mods.Any(m => m is OsuModDoubleTime))); + AddAssert("nightcore still visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModNightcore))); + + AddStep("make double time valid again", () => modSelect.IsValidMod = m => true); + AddAssert("double time visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModDoubleTime))); + AddAssert("nightcore still visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModNightcore))); + } + private void testSingleMod(Mod mod) { selectNext(mod); diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 775b0de1c0..61e4b45495 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; @@ -31,7 +32,6 @@ namespace osu.Game.Overlays.Mods { public class ModSelectOverlay : WaveOverlayContainer { - private readonly Func isValidMod; public const float HEIGHT = 510; protected readonly TriangleButton DeselectAllButton; @@ -49,6 +49,23 @@ namespace osu.Game.Overlays.Mods /// protected virtual bool Stacked => true; + [NotNull] + private Func isValidMod = m => true; + + /// + /// A function that checks whether a given mod is selectable. + /// + [NotNull] + public Func IsValidMod + { + get => isValidMod; + set + { + isValidMod = value ?? throw new ArgumentNullException(nameof(value)); + updateAvailableMods(); + } + } + protected readonly FillFlowContainer ModSectionsContainer; protected readonly ModSettingsContainer ModSettingsContainer; @@ -67,10 +84,8 @@ namespace osu.Game.Overlays.Mods private SampleChannel sampleOn, sampleOff; - public ModSelectOverlay(Func isValidMod = null) + public ModSelectOverlay() { - this.isValidMod = isValidMod ?? (m => true); - Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2"); Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2"); Waves.ThirdWaveColour = Color4Extensions.FromHex(@"005774"); @@ -351,7 +366,7 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - availableMods.BindValueChanged(availableModsChanged, true); + availableMods.BindValueChanged(_ => updateAvailableMods(), true); SelectedMods.BindValueChanged(selectedModsChanged, true); } @@ -406,9 +421,10 @@ namespace osu.Game.Overlays.Mods public override bool OnPressed(GlobalAction action) => false; // handled by back button - private void availableModsChanged(ValueChangedEvent>> mods) + private void updateAvailableMods() { - if (mods.NewValue == null) return; + if (availableMods?.Value == null) + return; foreach (var section in ModSectionsContainer.Children) { @@ -417,10 +433,32 @@ namespace osu.Game.Overlays.Mods if (!Stacked) modEnumeration = ModUtils.FlattenMods(modEnumeration); - section.Mods = modEnumeration.Where(isValidMod); + section.Mods = modEnumeration.Select(getValidModOrNull).Where(m => m != null); } } + /// + /// Returns a valid form of a given if possible, or null otherwise. + /// + /// + /// This is a recursive process during which any invalid mods are culled while preserving structures where possible. + /// + /// The to check. + /// A valid form of if exists, or null otherwise. + [CanBeNull] + private Mod getValidModOrNull([NotNull] Mod mod) + { + if (!(mod is MultiMod multi)) + return IsValidMod(mod) ? mod : null; + + var validSubset = multi.Mods.Select(getValidModOrNull).Where(m => m != null).ToArray(); + + if (validSubset.Length == 0) + return null; + + return validSubset.Length == 1 ? validSubset[0] : new MultiMod(validSubset); + } + private void selectedModsChanged(ValueChangedEvent> mods) { foreach (var section in ModSectionsContainer.Children) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index ebc06d2445..5bf9b1ee7e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(isValidMod); + protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay { IsValidMod = isValidMod }; private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; } diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index ed47b5d5ac..1bb7374ce3 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Select item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); } - protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(isValidMod); + protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay { IsValidMod = isValidMod }; private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; } From 50e92bd0ed729a2d4aa551bea13a66f37de6035c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 20:50:54 +0900 Subject: [PATCH 6350/6909] Fix selection not being preserved when IsValidMod changes --- .../UserInterface/TestSceneModSelectOverlay.cs | 12 ++++++++++++ osu.Game/Overlays/Mods/ModSection.cs | 6 +++--- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 11 ++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 9cf8b95ddf..81edcd8db8 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -153,6 +153,18 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("nightcore still visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModNightcore))); } + [Test] + public void TestChangeIsValidPreservesSelection() + { + changeRuleset(0); + + AddStep("select DT + HD", () => SelectedMods.Value = new Mod[] { new OsuModDoubleTime(), new OsuModHidden() }); + AddAssert("DT + HD selected", () => modSelect.ChildrenOfType().Count(b => b.Selected) == 2); + + AddStep("make NF invalid", () => modSelect.IsValidMod = m => !(m is ModNoFail)); + AddAssert("DT + HD still selected", () => modSelect.ChildrenOfType().Count(b => b.Selected) == 2); + } + private void testSingleMod(Mod mod) { selectNext(mod); diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 573d1e5355..4c629aef54 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -130,13 +130,13 @@ namespace osu.Game.Overlays.Mods /// Updates all buttons with the given list of selected mods. /// /// The new list of selected mods to select. - public void UpdateSelectedMods(IReadOnlyList newSelectedMods) + public void UpdateSelectedButtons(IReadOnlyList newSelectedMods) { foreach (var button in buttons) - updateButtonMods(button, newSelectedMods); + updateButtonSelection(button, newSelectedMods); } - private void updateButtonMods(ModButton button, IReadOnlyList newSelectedMods) + private void updateButtonSelection(ModButton button, IReadOnlyList newSelectedMods) { foreach (var mod in newSelectedMods) { diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 61e4b45495..fcec6f3926 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -367,7 +367,7 @@ namespace osu.Game.Overlays.Mods base.LoadComplete(); availableMods.BindValueChanged(_ => updateAvailableMods(), true); - SelectedMods.BindValueChanged(selectedModsChanged, true); + SelectedMods.BindValueChanged(_ => updateSelectedButtons(), true); } protected override void PopOut() @@ -435,6 +435,8 @@ namespace osu.Game.Overlays.Mods section.Mods = modEnumeration.Select(getValidModOrNull).Where(m => m != null); } + + updateSelectedButtons(); } /// @@ -459,10 +461,13 @@ namespace osu.Game.Overlays.Mods return validSubset.Length == 1 ? validSubset[0] : new MultiMod(validSubset); } - private void selectedModsChanged(ValueChangedEvent> mods) + private void updateSelectedButtons() { + // Enumeration below may update the bindable list. + var selectedMods = SelectedMods.Value.ToList(); + foreach (var section in ModSectionsContainer.Children) - section.UpdateSelectedMods(mods.NewValue); + section.UpdateSelectedButtons(selectedMods); updateMods(); } From e58ece9e108b757a72aaff635090ad00b245aa5a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 20:58:31 +0900 Subject: [PATCH 6351/6909] Make ModSelectOverlay abstract --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 2 +- .../Visual/UserInterface/TestSceneModSettings.cs | 2 +- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- osu.Game/Overlays/Mods/SoloModSelectOverlay.cs | 9 +++++++++ .../OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- osu.Game/Screens/Select/MatchSongSelect.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 7 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 osu.Game/Overlays/Mods/SoloModSelectOverlay.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 81edcd8db8..92104cfc72 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -306,7 +306,7 @@ namespace osu.Game.Tests.Visual.UserInterface }; } - private class TestModSelectOverlay : ModSelectOverlay + private class TestModSelectOverlay : SoloModSelectOverlay { public new Bindable> SelectedMods => base.SelectedMods; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index 8614700b15..3c889bdec4 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait for ready", () => modSelect.State.Value == Visibility.Visible && modSelect.ButtonsLoaded); } - private class TestModSelectOverlay : ModSelectOverlay + private class TestModSelectOverlay : SoloModSelectOverlay { public new VisibilityContainer ModSettingsContainer => base.ModSettingsContainer; public new TriangleButton CustomiseButton => base.CustomiseButton; diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index fcec6f3926..75ad90f065 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -30,7 +30,7 @@ using osuTK.Input; namespace osu.Game.Overlays.Mods { - public class ModSelectOverlay : WaveOverlayContainer + public abstract class ModSelectOverlay : WaveOverlayContainer { public const float HEIGHT = 510; @@ -84,7 +84,7 @@ namespace osu.Game.Overlays.Mods private SampleChannel sampleOn, sampleOff; - public ModSelectOverlay() + protected ModSelectOverlay() { Waves.FirstWaveColour = Color4Extensions.FromHex(@"19b0e2"); Waves.SecondWaveColour = Color4Extensions.FromHex(@"2280a2"); diff --git a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs new file mode 100644 index 0000000000..8f6819d7ff --- /dev/null +++ b/osu.Game/Overlays/Mods/SoloModSelectOverlay.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.Overlays.Mods +{ + public class SoloModSelectOverlay : ModSelectOverlay + { + } +} diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 5bf9b1ee7e..930f70d087 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -111,7 +111,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay { IsValidMod = isValidMod }; + protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay { IsValidMod = isValidMod }; private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; } diff --git a/osu.Game/Screens/Select/MatchSongSelect.cs b/osu.Game/Screens/Select/MatchSongSelect.cs index 1bb7374ce3..e181370cf7 100644 --- a/osu.Game/Screens/Select/MatchSongSelect.cs +++ b/osu.Game/Screens/Select/MatchSongSelect.cs @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Select item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); } - protected override ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay { IsValidMod = isValidMod }; + protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay { IsValidMod = isValidMod }; private bool isValidMod(Mod mod) => !(mod is ModAutoplay) && (mod as MultiMod)?.Mods.Any(mm => mm is ModAutoplay) != true; } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index 4fca77a176..ff49dd9f7e 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -301,7 +301,7 @@ namespace osu.Game.Screens.Select } } - protected virtual ModSelectOverlay CreateModSelectOverlay() => new ModSelectOverlay(); + protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(); protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) { From 728f8599b2bedbc721481c2bafb017a130177d25 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 21:06:32 +0900 Subject: [PATCH 6352/6909] Move incompatible mod deselection to SoloModOverlay --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 23 ++++++++----------- .../Overlays/Mods/SoloModSelectOverlay.cs | 9 ++++++++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 75ad90f065..c400e4cc43 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -349,19 +349,6 @@ namespace osu.Game.Overlays.Mods refreshSelectedMods(); } - /// - /// Deselect one or more mods. - /// - /// The types of s which should be deselected. - /// Set to true to bypass animations and update selections immediately. - private void deselectTypes(Type[] modTypes, bool immediate = false) - { - if (modTypes.Length == 0) return; - - foreach (var section in ModSectionsContainer.Children) - section.DeselectTypes(modTypes, immediate); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -496,7 +483,7 @@ namespace osu.Game.Overlays.Mods { if (State.Value == Visibility.Visible) sampleOn?.Play(); - deselectTypes(selectedMod.IncompatibleMods, true); + OnModSelected(selectedMod); if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show(); } @@ -508,6 +495,14 @@ namespace osu.Game.Overlays.Mods refreshSelectedMods(); } + /// + /// Invoked when a new has been selected. + /// + /// The that has been selected. + protected virtual void OnModSelected(Mod mod) + { + } + private void refreshSelectedMods() => SelectedMods.Value = ModSectionsContainer.Children.SelectMany(s => s.SelectedMods).ToArray(); #region Disposal diff --git a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs index 8f6819d7ff..d039ad1f98 100644 --- a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs @@ -1,9 +1,18 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Mods; + namespace osu.Game.Overlays.Mods { public class SoloModSelectOverlay : ModSelectOverlay { + protected override void OnModSelected(Mod mod) + { + base.OnModSelected(mod); + + foreach (var section in ModSectionsContainer.Children) + section.DeselectTypes(mod.IncompatibleMods, true); + } } } From 643c0605d85cf7808394c4b2fe3ea4f39b62906b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 21:14:38 +0900 Subject: [PATCH 6353/6909] Implement the freemod selection overlay --- .../TestSceneFreeModSelectOverlay.cs | 21 +++ osu.Game/Overlays/Mods/ModSection.cs | 15 ++- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 19 +-- .../OnlinePlay/FreeModSelectOverlay.cs | 120 ++++++++++++++++++ 4 files changed, 165 insertions(+), 10 deletions(-) create mode 100644 osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs create mode 100644 osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.cs new file mode 100644 index 0000000000..26a0301d8a --- /dev/null +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneFreeModSelectOverlay.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 NUnit.Framework; +using osu.Framework.Graphics.Containers; +using osu.Game.Screens.OnlinePlay; + +namespace osu.Game.Tests.Visual.Multiplayer +{ + public class TestSceneFreeModSelectOverlay : MultiplayerTestScene + { + [SetUp] + public new void Setup() => Schedule(() => + { + Child = new FreeModSelectOverlay + { + State = { Value = Visibility.Visible } + }; + }); + } +} diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index b3ddd30772..87a45ebf63 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -94,7 +94,20 @@ namespace osu.Game.Overlays.Mods return base.OnKeyDown(e); } - public void DeselectAll() => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); + /// + /// Selects all mods. + /// + public void SelectAll() + { + foreach (var button in buttons.Where(b => !b.Selected)) + button.SelectAt(0); + } + + /// + /// Deselects all mods. + /// + /// Set to true to bypass animations and update selections immediately. + public void DeselectAll(bool immediate = false) => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null), immediate); /// /// Deselect one or more mods in this section. diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index e064a6fb84..8225c1b6bb 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -37,8 +37,11 @@ namespace osu.Game.Overlays.Mods protected readonly TriangleButton CustomiseButton; protected readonly TriangleButton CloseButton; + protected readonly Drawable MultiplierSection; protected readonly OsuSpriteText MultiplierLabel; + protected readonly FillFlowContainer FooterContainer; + protected override bool BlockNonPositionalInput => false; protected override bool DimMainContent => false; @@ -79,8 +82,6 @@ namespace osu.Game.Overlays.Mods private const float content_width = 0.8f; private const float footer_button_spacing = 20; - private readonly FillFlowContainer footerContainer; - private SampleChannel sampleOn, sampleOff; protected ModSelectOverlay() @@ -269,7 +270,7 @@ namespace osu.Game.Overlays.Mods Colour = new Color4(172, 20, 116, 255), Alpha = 0.5f, }, - footerContainer = new FillFlowContainer + FooterContainer = new FillFlowContainer { Origin = Anchor.BottomCentre, Anchor = Anchor.BottomCentre, @@ -283,7 +284,7 @@ namespace osu.Game.Overlays.Mods Vertical = 15, Horizontal = OsuScreen.HORIZONTAL_OVERFLOW_PADDING }, - Children = new Drawable[] + Children = new[] { DeselectAllButton = new TriangleButton { @@ -310,7 +311,7 @@ namespace osu.Game.Overlays.Mods Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, - new FillFlowContainer + MultiplierSection = new FillFlowContainer { AutoSizeAxes = Axes.Both, Spacing = new Vector2(footer_button_spacing / 2, 0), @@ -378,8 +379,8 @@ namespace osu.Game.Overlays.Mods { base.PopOut(); - footerContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); - footerContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); + FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); + FooterContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); foreach (var section in ModSectionsContainer.Children) { @@ -393,8 +394,8 @@ namespace osu.Game.Overlays.Mods { base.PopIn(); - footerContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint); - footerContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint); + FooterContainer.MoveToX(0, WaveContainer.APPEAR_DURATION, Easing.OutQuint); + FooterContainer.FadeIn(WaveContainer.APPEAR_DURATION, Easing.OutQuint); foreach (var section in ModSectionsContainer.Children) { diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs new file mode 100644 index 0000000000..628199309a --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -0,0 +1,120 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Graphics.UserInterface; +using osu.Game.Overlays.Mods; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Screens.OnlinePlay +{ + /// + /// A used for free-mod selection in online play. + /// + public class FreeModSelectOverlay : ModSelectOverlay + { + protected override bool Stacked => false; + + public FreeModSelectOverlay() + { + CustomiseButton.Alpha = 0; + MultiplierSection.Alpha = 0; + DeselectAllButton.Alpha = 0; + + Drawable selectAllButton; + Drawable deselectAllButton; + + FooterContainer.AddRange(new[] + { + selectAllButton = new TriangleButton + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Width = 180, + Text = "Select All", + Action = selectAll, + }, + // Unlike the base mod select overlay, this button deselects mods instantaneously. + deselectAllButton = new TriangleButton + { + Origin = Anchor.CentreLeft, + Anchor = Anchor.CentreLeft, + Width = 180, + Text = "Deselect All", + Action = deselectAll, + }, + }); + + FooterContainer.SetLayoutPosition(selectAllButton, -2); + FooterContainer.SetLayoutPosition(deselectAllButton, -1); + } + + private void selectAll() + { + foreach (var section in ModSectionsContainer.Children) + section.SelectAll(); + } + + private void deselectAll() + { + foreach (var section in ModSectionsContainer.Children) + section.DeselectAll(true); + } + + protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); + + private class FreeModSection : ModSection + { + private HeaderCheckbox checkbox; + + public FreeModSection(ModType type) + : base(type) + { + } + + protected override Drawable CreateHeader(string text) => new Container + { + AutoSizeAxes = Axes.Y, + Width = 175, + Child = checkbox = new HeaderCheckbox + { + LabelText = text, + Changed = onCheckboxChanged + } + }; + + private void onCheckboxChanged(bool value) + { + foreach (var button in ButtonsContainer.OfType()) + { + if (value) + button.SelectAt(0); + else + button.Deselect(); + } + } + + protected override void Update() + { + base.Update(); + + var validButtons = ButtonsContainer.OfType().Where(b => b.Mod.HasImplementation); + checkbox.Current.Value = validButtons.All(b => b.Selected); + } + } + + private class HeaderCheckbox : OsuCheckbox + { + public Action Changed; + + protected override void OnUserChange(bool value) + { + base.OnUserChange(value); + Changed?.Invoke(value); + } + } + } +} From f25535548ae1910215fff01186cac34285cdac81 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 21:20:16 +0900 Subject: [PATCH 6354/6909] Fix buzzing on select all/deselect all --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 8225c1b6bb..c308dc2451 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -14,6 +14,7 @@ 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.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; @@ -495,11 +496,17 @@ namespace osu.Game.Overlays.Mods MultiplierLabel.FadeColour(Color4.White, 200); } + private ScheduledDelegate sampleOnDelegate; + private ScheduledDelegate sampleOffDelegate; + private void modButtonPressed(Mod selectedMod) { if (selectedMod != null) { - if (State.Value == Visibility.Visible) sampleOn?.Play(); + // Fixes buzzing when multiple mods are selected in the same frame. + sampleOnDelegate?.Cancel(); + if (State.Value == Visibility.Visible) + sampleOnDelegate = Scheduler.Add(() => sampleOn?.Play()); OnModSelected(selectedMod); @@ -507,7 +514,10 @@ namespace osu.Game.Overlays.Mods } else { - if (State.Value == Visibility.Visible) sampleOff?.Play(); + // Fixes buzzing when multiple mods are deselected in the same frame. + sampleOffDelegate?.Cancel(); + if (State.Value == Visibility.Visible) + sampleOffDelegate = Scheduler.Add(() => sampleOff?.Play()); } refreshSelectedMods(); From 5a56e2ba4b66ae8058157a3b32935f154d0f0c02 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 21:29:00 +0900 Subject: [PATCH 6355/6909] Fix sound duplication due to checkbox --- osu.Game/Graphics/UserInterface/OsuCheckbox.cs | 17 +++++++++++++---- .../Screens/OnlinePlay/FreeModSelectOverlay.cs | 2 ++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 6593531099..517f83daa9 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -18,6 +18,11 @@ namespace osu.Game.Graphics.UserInterface public Color4 UncheckedColor { get; set; } = Color4.White; public int FadeDuration { get; set; } + /// + /// Whether to play sounds when the state changes as a result of user interaction. + /// + protected virtual bool PlaySoundsOnUserChange => true; + public string LabelText { set @@ -96,10 +101,14 @@ namespace osu.Game.Graphics.UserInterface protected override void OnUserChange(bool value) { base.OnUserChange(value); - if (value) - sampleChecked?.Play(); - else - sampleUnchecked?.Play(); + + if (PlaySoundsOnUserChange) + { + if (value) + sampleChecked?.Play(); + else + sampleUnchecked?.Play(); + } } } } diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 628199309a..608e58b534 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -110,6 +110,8 @@ namespace osu.Game.Screens.OnlinePlay { public Action Changed; + protected override bool PlaySoundsOnUserChange => false; + protected override void OnUserChange(bool value) { base.OnUserChange(value); From 6ff8e8dd37c3a79ea7b6c122f596272708cd6e58 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 21:29:08 +0900 Subject: [PATCH 6356/6909] Disable a few mods by default --- osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 608e58b534..5b9a19897f 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -18,8 +18,16 @@ namespace osu.Game.Screens.OnlinePlay { protected override bool Stacked => false; + public new Func IsValidMod + { + get => base.IsValidMod; + set => base.IsValidMod = m => m.HasImplementation && !m.RequiresConfiguration && !(m is ModAutoplay) && value(m); + } + public FreeModSelectOverlay() { + IsValidMod = m => true; + CustomiseButton.Alpha = 0; MultiplierSection.Alpha = 0; DeselectAllButton.Alpha = 0; From 921f008217719bb6768b54c7746eef2d7d0f0ba7 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 21:35:08 +0900 Subject: [PATCH 6357/6909] Fix ModIcon not updating background colour correctly --- .../Visual/UserInterface/TestSceneModIcon.cs | 21 +++++++++ osu.Game/Rulesets/UI/ModIcon.cs | 46 +++++++++++-------- 2 files changed, 47 insertions(+), 20 deletions(-) create mode 100644 osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.cs new file mode 100644 index 0000000000..e7fa7d9235 --- /dev/null +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModIcon.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 NUnit.Framework; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Tests.Visual.UserInterface +{ + public class TestSceneModIcon : OsuTestScene + { + [Test] + public void TestChangeModType() + { + ModIcon icon = null; + + AddStep("create mod icon", () => Child = icon = new ModIcon(new OsuModDoubleTime())); + AddStep("change mod", () => icon.Mod = new OsuModEasy()); + } + } +} diff --git a/osu.Game/Rulesets/UI/ModIcon.cs b/osu.Game/Rulesets/UI/ModIcon.cs index 04a2e052fa..cae5da3d16 100644 --- a/osu.Game/Rulesets/UI/ModIcon.cs +++ b/osu.Game/Rulesets/UI/ModIcon.cs @@ -29,8 +29,6 @@ namespace osu.Game.Rulesets.UI private const float size = 80; - private readonly ModType type; - public virtual string TooltipText => showTooltip ? mod.IconTooltip : null; private Mod mod; @@ -42,10 +40,18 @@ namespace osu.Game.Rulesets.UI set { mod = value; - updateMod(value); + + if (IsLoaded) + updateMod(value); } } + [Resolved] + private OsuColour colours { get; set; } + + private Color4 backgroundColour; + private Color4 highlightedColour; + /// /// Construct a new instance. /// @@ -56,8 +62,6 @@ namespace osu.Game.Rulesets.UI this.mod = mod ?? throw new ArgumentNullException(nameof(mod)); this.showTooltip = showTooltip; - type = mod.Type; - Size = new Vector2(size); Children = new Drawable[] @@ -89,6 +93,13 @@ namespace osu.Game.Rulesets.UI Icon = FontAwesome.Solid.Question }, }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + Selected.BindValueChanged(_ => updateColour()); updateMod(mod); } @@ -102,20 +113,14 @@ namespace osu.Game.Rulesets.UI { modIcon.FadeOut(); modAcronym.FadeIn(); - return; + } + else + { + modIcon.FadeIn(); + modAcronym.FadeOut(); } - modIcon.FadeIn(); - modAcronym.FadeOut(); - } - - private Color4 backgroundColour; - private Color4 highlightedColour; - - [BackgroundDependencyLoader] - private void load(OsuColour colours) - { - switch (type) + switch (value.Type) { default: case ModType.DifficultyIncrease: @@ -149,12 +154,13 @@ namespace osu.Game.Rulesets.UI modIcon.Colour = colours.Yellow; break; } + + updateColour(); } - protected override void LoadComplete() + private void updateColour() { - base.LoadComplete(); - Selected.BindValueChanged(selected => background.Colour = selected.NewValue ? highlightedColour : backgroundColour, true); + background.Colour = Selected.Value ? highlightedColour : backgroundColour; } } } From aeb3ed8bb3bb12253eb2b4cf2092634e3c4c62ac Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Feb 2021 21:46:22 +0900 Subject: [PATCH 6358/6909] Renamespace footer button --- osu.Game/Screens/OnlinePlay/{Match => }/FooterButtonFreeMods.cs | 2 +- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) rename osu.Game/Screens/OnlinePlay/{Match => }/FooterButtonFreeMods.cs (97%) diff --git a/osu.Game/Screens/OnlinePlay/Match/FooterButtonFreeMods.cs b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs similarity index 97% rename from osu.Game/Screens/OnlinePlay/Match/FooterButtonFreeMods.cs rename to osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs index ca2db877c3..a3cc383b67 100644 --- a/osu.Game/Screens/OnlinePlay/Match/FooterButtonFreeMods.cs +++ b/osu.Game/Screens/OnlinePlay/FooterButtonFreeMods.cs @@ -13,7 +13,7 @@ using osu.Game.Screens.Play.HUD; using osu.Game.Screens.Select; using osuTK; -namespace osu.Game.Screens.OnlinePlay.Match +namespace osu.Game.Screens.OnlinePlay { public class FooterButtonFreeMods : FooterButton, IHasCurrentValue> { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 1c345b883f..0baa663578 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -15,7 +15,6 @@ using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; -using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.Select; using osu.Game.Utils; From 8e96ffd1e6ea4d848766c72e703503de6b23cffd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 2 Feb 2021 16:54:40 +0300 Subject: [PATCH 6359/6909] Fix "wait for import" until step potentially finishing early If not obvious, the issue with previous code is that it was checking for `IsAvailableLocally`, while the import is happening on a different thread, so that method could return `true` before the importing has finished and `ItemUpdated` event is called. --- .../TestSceneMultiplayerBeatmapAvailabilityTracker.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapAvailabilityTracker.cs index 3c3793670a..646c4139af 100644 --- a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapAvailabilityTracker.cs @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Online addAvailabilityCheckStep("state importing", BeatmapAvailability.Importing); AddStep("allow importing", () => beatmaps.AllowImport.SetResult(true)); - AddUntilStep("wait for import", () => beatmaps.IsAvailableLocally(testBeatmapSet)); + AddUntilStep("wait for import", () => beatmaps.CurrentImportTask?.IsCompleted == true); addAvailabilityCheckStep("state locally available", BeatmapAvailability.LocallyAvailable); } @@ -127,8 +127,6 @@ namespace osu.Game.Tests.Online private void addAvailabilityCheckStep(string description, Func expected) { - // In DownloadTrackingComposite, state changes are scheduled one frame later, wait one step. - AddWaitStep("wait for potential change", 1); AddAssert(description, () => availablilityTracker.Availability.Value.Equals(expected.Invoke())); } @@ -157,6 +155,8 @@ namespace osu.Game.Tests.Online { public TaskCompletionSource AllowImport = new TaskCompletionSource(); + public Task CurrentImportTask { get; private set; } + protected override ArchiveDownloadRequest CreateDownloadRequest(BeatmapSetInfo set, bool minimiseDownloadSize) => new TestDownloadRequest(set); @@ -168,7 +168,7 @@ namespace osu.Game.Tests.Online public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, CancellationToken cancellationToken = default) { await AllowImport.Task; - return await base.Import(item, archive, cancellationToken); + return await (CurrentImportTask = base.Import(item, archive, cancellationToken)); } } From 50d57a39317c32d59e30006679a030bedfbdb10b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 2 Feb 2021 17:07:16 +0300 Subject: [PATCH 6360/6909] Move tracker loading into BDL --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index f2cb1120cb..cdf889e4f1 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -41,11 +41,11 @@ namespace osu.Game.Screens.OnlinePlay.Match private IBindable> managerUpdated; [Cached] - protected readonly MultiplayerBeatmapAvailablilityTracker BeatmapAvailablilityTracker; + protected MultiplayerBeatmapAvailablilityTracker BeatmapAvailablilityTracker { get; } protected RoomSubScreen() { - InternalChild = BeatmapAvailablilityTracker = new MultiplayerBeatmapAvailablilityTracker + BeatmapAvailablilityTracker = new MultiplayerBeatmapAvailablilityTracker { SelectedItem = { BindTarget = SelectedItem }, }; @@ -54,6 +54,8 @@ namespace osu.Game.Screens.OnlinePlay.Match [BackgroundDependencyLoader] private void load(AudioManager audio) { + AddInternal(BeatmapAvailablilityTracker); + sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); } From 62d0036c819c0516005f9b5b3938d359e0cac987 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 2 Feb 2021 17:45:07 +0300 Subject: [PATCH 6361/6909] Fix using private constructor on MessagePack object --- osu.Game/Online/Rooms/BeatmapAvailability.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/BeatmapAvailability.cs b/osu.Game/Online/Rooms/BeatmapAvailability.cs index 2adeb9b959..a83327aad5 100644 --- a/osu.Game/Online/Rooms/BeatmapAvailability.cs +++ b/osu.Game/Online/Rooms/BeatmapAvailability.cs @@ -26,7 +26,7 @@ namespace osu.Game.Online.Rooms public readonly float? DownloadProgress; [JsonConstructor] - private BeatmapAvailability(DownloadState state, float? downloadProgress = null) + public BeatmapAvailability(DownloadState state, float? downloadProgress = null) { State = state; DownloadProgress = downloadProgress; From 181d2c672b5c93c1dfb84929b78a413dfe326db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Feb 2021 22:05:25 +0100 Subject: [PATCH 6362/6909] Fix outdated comment --- osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index a7015ba1c4..2d438bd96e 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -343,7 +343,7 @@ namespace osu.Game.Screens.OnlinePlay Colour = Color4.Black, Width = 0.4f, }, - // Piecewise-linear gradient with 3 segments to make it appear smoother + // Piecewise-linear gradient with 2 segments to make it appear smoother new Box { RelativeSizeAxes = Axes.Both, From fc84ec131347ee220c9c9df63561b3e6a099156f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Feb 2021 22:18:14 +0100 Subject: [PATCH 6363/6909] Move anchor specification to central place --- .../Screens/OnlinePlay/DrawableRoomPlaylistItem.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs index 2d438bd96e..23c713a2c1 100644 --- a/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs +++ b/osu.Game/Screens/OnlinePlay/DrawableRoomPlaylistItem.cs @@ -220,7 +220,11 @@ namespace osu.Game.Screens.OnlinePlay AutoSizeAxes = Axes.Both, Spacing = new Vector2(5), X = -10, - ChildrenEnumerable = CreateButtons() + ChildrenEnumerable = CreateButtons().Select(button => button.With(b => + { + b.Anchor = Anchor.Centre; + b.Origin = Anchor.Centre; + })) } } }; @@ -231,14 +235,10 @@ namespace osu.Game.Screens.OnlinePlay { new PlaylistDownloadButton(Item) { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, Size = new Vector2(50, 30) }, new PlaylistRemoveButton { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, Size = new Vector2(30, 30), Alpha = allowEdit ? 1 : 0, Action = () => RequestDeletion?.Invoke(Model), From 21d5f842fccb799ad83ae47ca868b4b3a9827cbb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 14:52:36 +0900 Subject: [PATCH 6364/6909] Re-layout to reduce movement --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 95 +++++++++++-------- 1 file changed, 54 insertions(+), 41 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 86982e2794..4664ac6bfe 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -97,18 +97,19 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, new Drawable[] { - new GridContainer + new Container { RelativeSizeAxes = Axes.Both, - Content = new[] + Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, + Child = new GridContainer { - new Drawable[] + RelativeSizeAxes = Axes.Both, + Content = new[] { - new Container + new Drawable[] { - RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding { Horizontal = 5, Vertical = 10 }, - Child = new GridContainer + // Main left column + new GridContainer { RelativeSizeAxes = Axes.Both, RowDimensions = new[] @@ -126,45 +127,57 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, } } - } - }, - new FillFlowContainer - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Padding = new MarginPadding { Horizontal = 5 }, - Spacing = new Vector2(0, 10), - Children = new[] + }, + // Main right column + new FillFlowContainer { - new FillFlowContainer + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new[] { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + new FillFlowContainer { - new OverlinedHeader("Beatmap"), - new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } - } - }, - userModsSection = new FillFlowContainer - { - RelativeSizeAxes = Axes.X, - AutoSizeAxes = Axes.Y, - Children = new Drawable[] + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Children = new Drawable[] + { + new OverlinedHeader("Beatmap"), + new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } + } + }, + userModsSection = new FillFlowContainer { - new OverlinedHeader("Extra mods"), - new ModDisplay + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Top = 10 }, + Children = new Drawable[] { - DisplayUnrankedText = false, - Current = UserMods - }, - new PurpleTriangleButton - { - RelativeSizeAxes = Axes.X, - Text = "Select", - Action = () => userModsSelectOverlay.Show() + new OverlinedHeader("Extra mods"), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new PurpleTriangleButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = () => userModsSelectOverlay.Show() + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + DisplayUnrankedText = false, + Current = UserMods, + Scale = new Vector2(0.8f), + }, + } + } } } } From 8bb13915152ef6c5ac4a96f684bdbece349409c8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 14:53:13 +0900 Subject: [PATCH 6365/6909] Fix inspection --- .../Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 768dc6512c..e2c98c0aad 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Client.AddUser(new User { Id = 0, - Username = $"User 0", + Username = "User 0", CurrentModeRank = RNG.Next(1, 100000), CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); From 8295fb908174b78cdf7803a28e5b5be4ae5346e9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 16:28:22 +0900 Subject: [PATCH 6366/6909] Implement mania constant speed mod --- osu.Game.Rulesets.Mania/ManiaRuleset.cs | 1 + .../Mods/ManiaModConstantSpeed.cs | 35 +++++++++++++++++++ .../UI/DrawableManiaRuleset.cs | 5 +++ .../UI/Scrolling/DrawableScrollingRuleset.cs | 8 ++--- 4 files changed, 45 insertions(+), 4 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 59c766fd84..4c729fef83 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -238,6 +238,7 @@ namespace osu.Game.Rulesets.Mania new ManiaModMirror(), new ManiaModDifficultyAdjust(), new ManiaModInvert(), + new ManiaModConstantSpeed() }; case ModType.Automation: diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.cs new file mode 100644 index 0000000000..078394b1d8 --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModConstantSpeed.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.Sprites; +using osu.Game.Configuration; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public class ManiaModConstantSpeed : Mod, IApplicableToDrawableRuleset + { + public override string Name => "Constant Speed"; + + public override string Acronym => "CS"; + + public override double ScoreMultiplier => 1; + + public override string Description => "No more tricky speed changes!"; + + public override IconUsage? Icon => FontAwesome.Solid.Equals; + + public override ModType Type => ModType.Conversion; + + public override bool Ranked => false; + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + var maniaRuleset = (DrawableManiaRuleset)drawableRuleset; + maniaRuleset.ScrollMethod = ScrollVisualisationMethod.Constant; + } + } +} diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 941ac9816c..6b34dbfa09 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Input; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; +using osu.Game.Configuration; using osu.Game.Input.Handlers; using osu.Game.Replays; using osu.Game.Rulesets.Mania.Beatmaps; @@ -49,6 +50,10 @@ namespace osu.Game.Rulesets.Mania.UI protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; + public ScrollVisualisationMethod ScrollMethod = ScrollVisualisationMethod.Sequential; + + protected override ScrollVisualisationMethod VisualisationMethod => ScrollMethod; + private readonly Bindable configDirection = new Bindable(); private readonly Bindable configTimeRange = new BindableDouble(); diff --git a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs index 0955f32790..6ffdad211b 100644 --- a/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs +++ b/osu.Game/Rulesets/UI/Scrolling/DrawableScrollingRuleset.cs @@ -91,7 +91,11 @@ namespace osu.Game.Rulesets.UI.Scrolling scrollingInfo = new LocalScrollingInfo(); scrollingInfo.Direction.BindTo(Direction); scrollingInfo.TimeRange.BindTo(TimeRange); + } + [BackgroundDependencyLoader] + private void load() + { switch (VisualisationMethod) { case ScrollVisualisationMethod.Sequential: @@ -106,11 +110,7 @@ namespace osu.Game.Rulesets.UI.Scrolling scrollingInfo.Algorithm = new ConstantScrollAlgorithm(); break; } - } - [BackgroundDependencyLoader] - private void load() - { double lastObjectTime = Objects.LastOrDefault()?.GetEndTime() ?? double.MaxValue; double baseBeatLength = TimingControlPoint.DEFAULT_BEAT_LENGTH; From c8f1126bd793cdf6169a1d27742fd0909cdd9c33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Feb 2021 19:44:39 +0900 Subject: [PATCH 6367/6909] Add failing test --- ...tion.cs => TestAPIModJsonSerialization.cs} | 2 +- .../TestAPIModMessagePackSerialization.cs | 139 ++++++++++++++++++ 2 files changed, 140 insertions(+), 1 deletion(-) rename osu.Game.Tests/Online/{TestAPIModSerialization.cs => TestAPIModJsonSerialization.cs} (99%) create mode 100644 osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs diff --git a/osu.Game.Tests/Online/TestAPIModSerialization.cs b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs similarity index 99% rename from osu.Game.Tests/Online/TestAPIModSerialization.cs rename to osu.Game.Tests/Online/TestAPIModJsonSerialization.cs index 5948582d77..aa6f66da81 100644 --- a/osu.Game.Tests/Online/TestAPIModSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModJsonSerialization.cs @@ -16,7 +16,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Tests.Online { [TestFixture] - public class TestAPIModSerialization + public class TestAPIModJsonSerialization { [Test] public void TestAcronymIsPreserved() diff --git a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs new file mode 100644 index 0000000000..4294f89397 --- /dev/null +++ b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.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.Collections.Generic; +using MessagePack; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Beatmaps; +using osu.Game.Configuration; +using osu.Game.Online.API; +using osu.Game.Rulesets; +using osu.Game.Rulesets.Difficulty; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Tests.Online +{ + [TestFixture] + public class TestAPIModMessagePackSerialization + { + [Test] + public void TestAcronymIsPreserved() + { + var apiMod = new APIMod(new TestMod()); + + var deserialized = MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(apiMod)); + + Assert.That(deserialized.Acronym, Is.EqualTo(apiMod.Acronym)); + } + + [Test] + public void TestRawSettingIsPreserved() + { + var apiMod = new APIMod(new TestMod { TestSetting = { Value = 2 } }); + + var deserialized = MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(apiMod)); + + Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(2.0)); + } + + [Test] + public void TestConvertedModHasCorrectSetting() + { + var apiMod = new APIMod(new TestMod { TestSetting = { Value = 2 } }); + + var deserialized = MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(apiMod)); + var converted = (TestMod)deserialized.ToMod(new TestRuleset()); + + 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 = MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(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 Mod[] + { + new TestMod(), + new TestModTimeRamp(), + }; + + 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 string Description { get; } = string.Empty; + public override string ShortName { get; } = string.Empty; + } + + private class TestMod : Mod + { + public override string Name => "Test Mod"; + public override string Acronym => "TM"; + public override double ScoreMultiplier => 1; + + [SettingSource("Test")] + public BindableNumber TestSetting { get; } = new BindableDouble + { + MinValue = 0, + MaxValue = 10, + Default = 5, + 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 + }; + } + } +} From 75f1ebd5f92ef4200f21f457bea58a23d4cdfa70 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Feb 2021 19:46:47 +0900 Subject: [PATCH 6368/6909] Add custom resolver for mod settings dictionary --- osu.Game/Online/API/APIMod.cs | 1 + .../API/ModSettingsDictionaryFormatter.cs | 96 +++++++++++++++++++ 2 files changed, 97 insertions(+) create mode 100644 osu.Game/Online/API/ModSettingsDictionaryFormatter.cs diff --git a/osu.Game/Online/API/APIMod.cs b/osu.Game/Online/API/APIMod.cs index 69ce3825ee..bff08b0515 100644 --- a/osu.Game/Online/API/APIMod.cs +++ b/osu.Game/Online/API/APIMod.cs @@ -23,6 +23,7 @@ namespace osu.Game.Online.API [JsonProperty("settings")] [Key(1)] + [MessagePackFormatter(typeof(ModSettingsDictionaryFormatter))] public Dictionary Settings { get; set; } = new Dictionary(); [JsonConstructor] diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs new file mode 100644 index 0000000000..a8ee9beca5 --- /dev/null +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -0,0 +1,96 @@ +using System; +using System.Collections.Generic; +using MessagePack; +using MessagePack.Formatters; +using osu.Framework.Bindables; + +namespace osu.Game.Online.API +{ + public class ModSettingsDictionaryFormatter : IMessagePackFormatter> + { + public int Serialize(ref byte[] bytes, int offset, Dictionary value, IFormatterResolver formatterResolver) + { + int startOffset = offset; + + offset += MessagePackBinary.WriteArrayHeader(ref bytes, offset, value.Count); + + foreach (var kvp in value) + { + offset += MessagePackBinary.WriteString(ref bytes, offset, kvp.Key); + + switch (kvp.Value) + { + case Bindable d: + offset += MessagePackBinary.WriteDouble(ref bytes, offset, d.Value); + break; + + case Bindable f: + offset += MessagePackBinary.WriteSingle(ref bytes, offset, f.Value); + break; + + case Bindable b: + offset += MessagePackBinary.WriteBoolean(ref bytes, offset, b.Value); + break; + + default: + throw new ArgumentException("A setting was of a type not supported by the messagepack serialiser", nameof(bytes)); + } + } + + return offset - startOffset; + } + + public Dictionary Deserialize(byte[] bytes, int offset, IFormatterResolver formatterResolver, out int readSize) + { + int startOffset = offset; + + var output = new Dictionary(); + + int itemCount = MessagePackBinary.ReadArrayHeader(bytes, offset, out readSize); + offset += readSize; + + for (int i = 0; i < itemCount; i++) + { + var key = MessagePackBinary.ReadString(bytes, offset, out readSize); + offset += readSize; + + switch (MessagePackBinary.GetMessagePackType(bytes, offset)) + { + case MessagePackType.Float: + { + // could be either float or double... + // see https://github.com/msgpack/msgpack/blob/master/spec.md#serialization-type-to-format-conversion + switch (MessagePackCode.ToFormatName(bytes[offset])) + { + case "float 32": + output[key] = MessagePackBinary.ReadSingle(bytes, offset, out readSize); + offset += readSize; + break; + + case "float 64": + output[key] = MessagePackBinary.ReadDouble(bytes, offset, out readSize); + offset += readSize; + break; + + default: + throw new ArgumentException("A setting was of a type not supported by the messagepack deserialiser", nameof(bytes)); + } + + break; + } + + case MessagePackType.Boolean: + output[key] = MessagePackBinary.ReadBoolean(bytes, offset, out readSize); + offset += readSize; + break; + + default: + throw new ArgumentException("A setting was of a type not supported by the messagepack deserialiser", nameof(bytes)); + } + } + + readSize = offset - startOffset; + return output; + } + } +} From d3f056f188ee7410f7603d71b6abec964755231b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Feb 2021 20:06:25 +0900 Subject: [PATCH 6369/6909] Add missing licence header --- osu.Game/Online/API/ModSettingsDictionaryFormatter.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index a8ee9beca5..1b381e7c98 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -1,3 +1,6 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + using System; using System.Collections.Generic; using MessagePack; From 1380717ebb5b51da488736df8cd30484ecc62532 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Feb 2021 20:19:27 +0900 Subject: [PATCH 6370/6909] Use PrimitiveObjectFormatter to simplify code --- .../API/ModSettingsDictionaryFormatter.cs | 43 +++---------------- 1 file changed, 7 insertions(+), 36 deletions(-) diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index 1b381e7c98..3dd8bff61b 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -15,6 +15,8 @@ namespace osu.Game.Online.API { int startOffset = offset; + var primitiveFormatter = PrimitiveObjectFormatter.Instance; + offset += MessagePackBinary.WriteArrayHeader(ref bytes, offset, value.Count); foreach (var kvp in value) @@ -24,15 +26,15 @@ namespace osu.Game.Online.API switch (kvp.Value) { case Bindable d: - offset += MessagePackBinary.WriteDouble(ref bytes, offset, d.Value); + offset += primitiveFormatter.Serialize(ref bytes, offset, d.Value, formatterResolver); break; case Bindable f: - offset += MessagePackBinary.WriteSingle(ref bytes, offset, f.Value); + offset += primitiveFormatter.Serialize(ref bytes, offset, f.Value, formatterResolver); break; case Bindable b: - offset += MessagePackBinary.WriteBoolean(ref bytes, offset, b.Value); + offset += primitiveFormatter.Serialize(ref bytes, offset, b.Value, formatterResolver); break; default: @@ -57,39 +59,8 @@ namespace osu.Game.Online.API var key = MessagePackBinary.ReadString(bytes, offset, out readSize); offset += readSize; - switch (MessagePackBinary.GetMessagePackType(bytes, offset)) - { - case MessagePackType.Float: - { - // could be either float or double... - // see https://github.com/msgpack/msgpack/blob/master/spec.md#serialization-type-to-format-conversion - switch (MessagePackCode.ToFormatName(bytes[offset])) - { - case "float 32": - output[key] = MessagePackBinary.ReadSingle(bytes, offset, out readSize); - offset += readSize; - break; - - case "float 64": - output[key] = MessagePackBinary.ReadDouble(bytes, offset, out readSize); - offset += readSize; - break; - - default: - throw new ArgumentException("A setting was of a type not supported by the messagepack deserialiser", nameof(bytes)); - } - - break; - } - - case MessagePackType.Boolean: - output[key] = MessagePackBinary.ReadBoolean(bytes, offset, out readSize); - offset += readSize; - break; - - default: - throw new ArgumentException("A setting was of a type not supported by the messagepack deserialiser", nameof(bytes)); - } + output[key] = PrimitiveObjectFormatter.Instance.Deserialize(bytes, offset, formatterResolver, out readSize); + offset += readSize; } readSize = offset - startOffset; From 65d45ec74cefcc4db80ef0d83d9bc598bc2e864b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 20:50:22 +0900 Subject: [PATCH 6371/6909] Unschedule cancellation --- .../Multiplayer/StatefulMultiplayerClient.cs | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index de51a4b117..7d729a3c11 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -117,13 +117,7 @@ namespace osu.Game.Online.Multiplayer /// The API . public async Task JoinRoom(Room room) { - var cancellationSource = new CancellationTokenSource(); - - await scheduleAsync(() => - { - joinCancellationSource?.Cancel(); - joinCancellationSource = cancellationSource; - }, CancellationToken.None); + var cancellationSource = joinCancellationSource = new CancellationTokenSource(); await joinOrLeaveTaskChain.Add(async () => { @@ -162,15 +156,15 @@ namespace osu.Game.Online.Multiplayer public Task LeaveRoom() { + // The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled. + // This includes the setting of Room itself along with the initial update of the room settings on join. + joinCancellationSource?.Cancel(); + // Leaving rooms is expected to occur instantaneously whilst the operation is finalised in the background. // However a few members need to be reset immediately to prevent other components from entering invalid states whilst the operation hasn't yet completed. // For example, if a room was left and the user immediately pressed the "create room" button, then the user could be taken into the lobby if the value of Room is not reset in time. var scheduledReset = scheduleAsync(() => { - // The join may have not completed yet, so certain tasks that either update the room or reference the room should be cancelled. - // This includes the setting of Room itself along with the initial update of the room settings on join. - joinCancellationSource?.Cancel(); - apiRoom = null; Room = null; CurrentMatchPlayingUserIds.Clear(); From 623b47f9af503a400facfea44e2db230db42a279 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 21:25:05 +0900 Subject: [PATCH 6372/6909] Add flag to toggle follow circle tracking for slider heads --- .../Objects/Drawables/DrawableSliderHead.cs | 14 ++++++++++---- osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs | 4 ++++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index acc95ab036..c051a9918d 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -12,6 +12,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { public class DrawableSliderHead : DrawableHitCircle { + public new SliderHeadCircle HitObject => (SliderHeadCircle)base.HitObject; + [CanBeNull] public Slider Slider => DrawableSlider?.HitObject; @@ -59,12 +61,16 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.Update(); Debug.Assert(Slider != null); + Debug.Assert(HitObject != null); - double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1); + if (HitObject.TrackFollowCircle) + { + double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1); - //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. - if (!IsHit) - Position = Slider.CurvePositionAt(completionProgress); + //todo: we probably want to reconsider this before adding scoring, but it looks and feels nice. + if (!IsHit) + Position = Slider.CurvePositionAt(completionProgress); + } } public Action OnShake; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index f6d46aeef5..5fc480883a 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -5,5 +5,9 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SliderHeadCircle : HitCircle { + /// + /// Makes the head circle track the follow circle when the start time is reached. + /// + public bool TrackFollowCircle = true; } } From 3fe190cfbe7c5fb3c6d2863ea53a33b370ea03cd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Feb 2021 22:00:16 +0900 Subject: [PATCH 6373/6909] Show original error message on web exceptions (or is no message is returned) --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 7936ab8ecd..dc98eb8687 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -4,6 +4,7 @@ using System; using System.Diagnostics; using System.Linq; +using System.Net; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; @@ -65,7 +66,10 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { failed = true; - Logger.Log($"You are not able to submit a score: {e.Message}", LoggingTarget.Information, LogLevel.Important); + if (e is WebException || string.IsNullOrEmpty(e.Message)) + Logger.Error(e, "Failed to retrieve a score submission token.\n\nThis may happen if you are running an old or non-official release of osu! (ie. you are self-compiling)."); + else + Logger.Log($"You are not able to submit a score: {e.Message}", level: LogLevel.Important); Schedule(() => { From 9d7164816cf073be96b07353d6d996c50086ad71 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Feb 2021 22:02:40 +0900 Subject: [PATCH 6374/6909] Add reverse binding for max attempts (currently unused but good for safety) --- .../OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index bf85ecf13d..56d3143f59 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -280,6 +280,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RoomName.BindValueChanged(name => NameField.Text = name.NewValue, true); Availability.BindValueChanged(availability => AvailabilityPicker.Current.Value = availability.NewValue, true); MaxParticipants.BindValueChanged(count => MaxParticipantsField.Text = count.NewValue?.ToString(), true); + MaxAttempts.BindValueChanged(count => MaxAttemptsField.Text = count.NewValue?.ToString(), true); Duration.BindValueChanged(duration => DurationField.Current.Value = duration.NewValue ?? TimeSpan.FromMinutes(30), true); playlist.Items.BindTo(Playlist); From e3d323989c6925304f1f8e126e3cab6aee31695a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Feb 2021 20:42:27 +0900 Subject: [PATCH 6375/6909] Switch to SignalR 5.0 and implement using better API --- .../API/ModSettingsDictionaryFormatter.cs | 39 ++++++++----------- osu.Game/osu.Game.csproj | 7 ++-- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index 3dd8bff61b..fc6b82a16b 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.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 System; +using System.Buffers; using System.Collections.Generic; +using System.Text; using MessagePack; using MessagePack.Formatters; using osu.Framework.Bindables; @@ -11,59 +12,51 @@ namespace osu.Game.Online.API { public class ModSettingsDictionaryFormatter : IMessagePackFormatter> { - public int Serialize(ref byte[] bytes, int offset, Dictionary value, IFormatterResolver formatterResolver) + public void Serialize(ref MessagePackWriter writer, Dictionary value, MessagePackSerializerOptions options) { - int startOffset = offset; - var primitiveFormatter = PrimitiveObjectFormatter.Instance; - offset += MessagePackBinary.WriteArrayHeader(ref bytes, offset, value.Count); + writer.WriteArrayHeader(value.Count); foreach (var kvp in value) { - offset += MessagePackBinary.WriteString(ref bytes, offset, kvp.Key); + var stringBytes = new ReadOnlySequence(Encoding.UTF8.GetBytes(kvp.Key)); + writer.WriteString(in stringBytes); switch (kvp.Value) { case Bindable d: - offset += primitiveFormatter.Serialize(ref bytes, offset, d.Value, formatterResolver); + primitiveFormatter.Serialize(ref writer, d.Value, options); break; case Bindable f: - offset += primitiveFormatter.Serialize(ref bytes, offset, f.Value, formatterResolver); + primitiveFormatter.Serialize(ref writer, f.Value, options); break; case Bindable b: - offset += primitiveFormatter.Serialize(ref bytes, offset, b.Value, formatterResolver); + primitiveFormatter.Serialize(ref writer, b.Value, options); break; default: - throw new ArgumentException("A setting was of a type not supported by the messagepack serialiser", nameof(bytes)); + // fall back for non-bindable cases. + primitiveFormatter.Serialize(ref writer, kvp.Value, options); + break; } } - - return offset - startOffset; } - public Dictionary Deserialize(byte[] bytes, int offset, IFormatterResolver formatterResolver, out int readSize) + public Dictionary Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { - int startOffset = offset; - var output = new Dictionary(); - int itemCount = MessagePackBinary.ReadArrayHeader(bytes, offset, out readSize); - offset += readSize; + int itemCount = reader.ReadArrayHeader(); for (int i = 0; i < itemCount; i++) { - var key = MessagePackBinary.ReadString(bytes, offset, out readSize); - offset += readSize; - - output[key] = PrimitiveObjectFormatter.Instance.Deserialize(bytes, offset, formatterResolver, out readSize); - offset += readSize; + output[reader.ReadString()] = + PrimitiveObjectFormatter.Instance.Deserialize(ref reader, options); } - readSize = offset - startOffset; return output; } } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index e2b506e187..eabd5c7d12 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,11 +20,12 @@ - - - + + + + From 03b7817887ea059719094f001a81c5b4f1c9ee36 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 22:12:20 +0900 Subject: [PATCH 6376/6909] Add flags to return to classic slider scoring --- .../Objects/Drawables/DrawableHitCircle.cs | 9 ++++++- .../Objects/Drawables/DrawableSlider.cs | 24 ++++++++++++++++++- .../Objects/Drawables/DrawableSliderHead.cs | 15 ++++++++++++ osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++++++- .../Objects/SliderHeadCircle.cs | 14 ++++++++++- 5 files changed, 66 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs index 3c0260f5f5..77094f928b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableHitCircle.cs @@ -123,7 +123,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return; } - var result = HitObject.HitWindows.ResultFor(timeOffset); + var result = ResultFor(timeOffset); if (result == HitResult.None || CheckHittable?.Invoke(this, Time.Current) == false) { @@ -146,6 +146,13 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables }); } + /// + /// Retrieves the for a time offset. + /// + /// The time offset. + /// The hit result, or if doesn't result in a judgement. + protected virtual HitResult ResultFor(double timeOffset) => HitObject.HitWindows.ResultFor(timeOffset); + protected override void UpdateInitialTransforms() { base.UpdateInitialTransforms(); diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 511cbc2347..7061ce59d0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -15,6 +15,7 @@ using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Skinning; using osu.Game.Rulesets.Osu.Skinning.Default; using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.Scoring; using osuTK.Graphics; using osu.Game.Skinning; @@ -249,7 +250,28 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < HitObject.EndTime) return; - ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); + if (HitObject.IgnoreJudgement) + { + ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); + return; + } + + // If not ignoring judgement, score proportionally based on the number of ticks hit, counting the head circle as a tick. + ApplyResult(r => + { + int totalTicks = NestedHitObjects.Count; + int hitTicks = NestedHitObjects.Count(h => h.IsHit); + double hitFraction = (double)totalTicks / hitTicks; + + if (hitTicks == totalTicks) + r.Type = HitResult.Great; + else if (hitFraction >= 0.5) + r.Type = HitResult.Ok; + else if (hitFraction > 0) + r.Type = HitResult.Meh; + else + r.Type = HitResult.Miss; + }); } public override void PlaySamples() diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index c051a9918d..08e9c5eb14 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Rulesets.Objects.Types; +using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Osu.Objects.Drawables { @@ -19,6 +20,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables protected DrawableSlider DrawableSlider => (DrawableSlider)ParentHitObject; + public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult; + private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; @@ -73,6 +76,18 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + protected override HitResult ResultFor(double timeOffset) + { + Debug.Assert(HitObject != null); + + if (HitObject.JudgeAsNormalHitCircle) + return base.ResultFor(timeOffset); + + // If not judged as a normal hitcircle, only track whether a hit has occurred (via IgnoreHit) rather than a scorable hit result. + var result = base.ResultFor(timeOffset); + return result.IsHit() ? HitResult.IgnoreHit : result; + } + public Action OnShake; public override void Shake(double maximumLength) => OnShake?.Invoke(maximumLength); diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 1670df24a8..e3365a8ccf 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -114,6 +114,12 @@ namespace osu.Game.Rulesets.Osu.Objects /// public double TickDistanceMultiplier = 1; + /// + /// Whether this 's judgement should be ignored. + /// If false, this will be judged proportionally to the number of ticks hit. + /// + public bool IgnoreJudgement = true; + [JsonIgnore] public HitCircle HeadCircle { get; protected set; } @@ -233,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Objects HeadCircle.Samples = this.GetNodeSamples(0); } - public override Judgement CreateJudgement() => new OsuIgnoreJudgement(); + public override Judgement CreateJudgement() => IgnoreJudgement ? new OsuIgnoreJudgement() : new OsuJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 5fc480883a..13eac60300 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -1,13 +1,25 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Osu.Judgements; + namespace osu.Game.Rulesets.Osu.Objects { public class SliderHeadCircle : HitCircle { /// - /// Makes the head circle track the follow circle when the start time is reached. + /// Makes this track the follow circle when the start time is reached. + /// If false, this will be pinned to its initial position in the slider. /// public bool TrackFollowCircle = true; + + /// + /// Whether to treat this as a normal for judgement purposes. + /// If false, judgement will be ignored. + /// + public bool JudgeAsNormalHitCircle = true; + + public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new OsuIgnoreJudgement(); } } From 2f22dbe06be3645ed9c5e8f99cc015414f578256 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 22:42:50 +0900 Subject: [PATCH 6377/6909] Make sliders display judgements when not ignored --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 2 +- 2 files 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 13f5960bd4..79655c33e4 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableOsuJudgement.cs @@ -39,7 +39,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (JudgedObject?.HitObject is OsuHitObject osuObject) { - Position = osuObject.StackedPosition; + Position = osuObject.StackedEndPosition; Scale = new Vector2(osuObject.Scale); } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 7061ce59d0..e607163b3e 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 public SliderBall Ball { get; private set; } public SkinnableDrawable Body { get; private set; } - public override bool DisplayResult => false; + public override bool DisplayResult => !HitObject.IgnoreJudgement; private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; From 3b5c67a0630681b6e1e2f0d52486e241772661d3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Feb 2021 23:08:59 +0900 Subject: [PATCH 6378/6909] Add OsuModClassic --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 43 +++++++++++++++++++++ osu.Game.Rulesets.Osu/OsuRuleset.cs | 1 + 2 files changed, 44 insertions(+) create mode 100644 osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs new file mode 100644 index 0000000000..5542580979 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.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.Linq; +using osu.Framework.Graphics.Sprites; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; + +namespace osu.Game.Rulesets.Osu.Mods +{ + public class OsuModClassic : Mod, IApplicableToHitObject + { + public override string Name => "Classic"; + + public override string Acronym => "CL"; + + public override double ScoreMultiplier => 1; + + public override IconUsage? Icon => FontAwesome.Solid.History; + + public override string Description => "Feeling nostalgic?"; + + public override bool Ranked => false; + + public void ApplyToHitObject(HitObject hitObject) + { + switch (hitObject) + { + case Slider slider: + slider.IgnoreJudgement = false; + + foreach (var head in slider.NestedHitObjects.OfType()) + { + head.TrackFollowCircle = false; + head.JudgeAsNormalHitCircle = false; + } + + break; + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index cba0c5be14..18324a18a8 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -163,6 +163,7 @@ namespace osu.Game.Rulesets.Osu { new OsuModTarget(), new OsuModDifficultyAdjust(), + new OsuModClassic() }; case ModType.Automation: From abdd417eb6ace553d81a11c32b6bc723247557b8 Mon Sep 17 00:00:00 2001 From: vmaggioli Date: Wed, 3 Feb 2021 10:03:38 -0500 Subject: [PATCH 6379/6909] Remove slider changes --- .../TestSceneSliderSelectionBlueprint.cs | 19 ------------------- .../Sliders/SliderSelectionBlueprint.cs | 6 +----- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs index 4edf778bfd..f6e1be693b 100644 --- a/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu.Tests/Editor/TestSceneSliderSelectionBlueprint.cs @@ -161,18 +161,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor checkControlPointSelected(1, false); } - [Test] - public void TestZeroLengthSliderNotAllowed() - { - moveMouseToControlPoint(1); - dragMouseToControlPoint(0); - - moveMouseToControlPoint(2); - dragMouseToControlPoint(0); - - AddAssert("slider has non-zero duration", () => slider.Duration > 0); - } - private void moveHitObject() { AddStep("move hitobject", () => @@ -201,13 +189,6 @@ namespace osu.Game.Rulesets.Osu.Tests.Editor }); } - private void dragMouseToControlPoint(int index) - { - AddStep("hold down mouse button", () => InputManager.PressButton(MouseButton.Left)); - moveMouseToControlPoint(index); - AddStep("release mouse button", () => InputManager.ReleaseButton(MouseButton.Left)); - } - private void checkControlPointSelected(int index, bool selected) => AddAssert($"control point {index} {(selected ? "selected" : "not selected")}", () => blueprint.ControlPointVisualiser.Pieces[index].IsSelected.Value == selected); diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs index 508783a499..3d3dff653a 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderSelectionBlueprint.cs @@ -226,11 +226,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders private void updatePath() { - float expectedDistance = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; - if (expectedDistance < 1) - return; - - HitObject.Path.ExpectedDistance.Value = expectedDistance; + HitObject.Path.ExpectedDistance.Value = composer?.GetSnappedDistanceFromDistance(HitObject.StartTime, (float)HitObject.Path.CalculatedDistance) ?? (float)HitObject.Path.CalculatedDistance; editorBeatmap?.Update(HitObject); } From db3f9e7cbee3ff3c719d80fc2943420b82a828a2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Feb 2021 02:20:18 +0300 Subject: [PATCH 6380/6909] Apply documentation suggestion --- osu.Game/Online/DownloadTrackingComposite.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 188cb9be7a..69c6ebd07c 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -67,10 +67,10 @@ namespace osu.Game.Online } /// - /// Verifies that the given databased model is in a correct state to be considered available. + /// Checks that a database model matches the one expected to be downloaded. /// /// - /// In the case of multiplayer/playlists, this has to verify that the databased beatmap set with the selected beatmap matches what's online. + /// In the case of multiplayer/playlists, this has to check that the databased beatmap set with the selected beatmap matches what's online. /// /// The model in database. protected virtual bool VerifyDatabasedModel([NotNull] TModel databasedModel) => true; From 76cfeae7e9cf844996ac9c53a54a1756034d1562 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 15:10:56 +0900 Subject: [PATCH 6381/6909] Add support for Bindable int in config --- osu.Game/Online/API/ModSettingsDictionaryFormatter.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index fc6b82a16b..99e87677fa 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -29,6 +29,10 @@ namespace osu.Game.Online.API primitiveFormatter.Serialize(ref writer, d.Value, options); break; + case Bindable i: + primitiveFormatter.Serialize(ref writer, i.Value, options); + break; + case Bindable f: primitiveFormatter.Serialize(ref writer, f.Value, options); break; From d165344070434f8e47af0d92c206b7a7fe9ee9ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 15:19:57 +0900 Subject: [PATCH 6382/6909] Force newer version of MessagePack for fixed iOS compatibility --- osu.Game/osu.Game.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index eabd5c7d12..f866b232d8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -20,6 +20,7 @@ + From 30dae5bf1c1188139fb683e235b15b16af5e83ec Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 4 Feb 2021 15:17:47 +0900 Subject: [PATCH 6383/6909] Add test to make sure the algorithm is passed down in time --- .../Mods/TestSceneManiaModConstantSpeed.cs | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.cs diff --git a/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.cs b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.cs new file mode 100644 index 0000000000..60363aaeef --- /dev/null +++ b/osu.Game.Rulesets.Mania.Tests/Mods/TestSceneManiaModConstantSpeed.cs @@ -0,0 +1,31 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Framework.Testing; +using osu.Game.Rulesets.Mania.Mods; +using osu.Game.Rulesets.Mania.Objects.Drawables; +using osu.Game.Rulesets.UI.Scrolling; +using osu.Game.Rulesets.UI.Scrolling.Algorithms; +using osu.Game.Tests.Visual; + +namespace osu.Game.Rulesets.Mania.Tests.Mods +{ + public class TestSceneManiaModConstantSpeed : ModTestScene + { + protected override Ruleset CreatePlayerRuleset() => new ManiaRuleset(); + + [Test] + public void TestConstantScroll() => CreateModTest(new ModTestData + { + Mod = new ManiaModConstantSpeed(), + PassCondition = () => + { + var hitObject = Player.ChildrenOfType().FirstOrDefault(); + return hitObject?.Dependencies.Get().Algorithm is ConstantScrollAlgorithm; + } + }); + } +} From a36b426b243e42062b93782ad5204dd35bd5ad88 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 15:46:50 +0900 Subject: [PATCH 6384/6909] Force iOS back to previous versions of messagepack --- osu.iOS.props | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.iOS.props b/osu.iOS.props index dc3527c687..22d104f2e1 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -80,6 +80,9 @@ + + + From b2f1e133f86d3cf26d9581cdb45c0beecbe2ab5f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 16:53:55 +0900 Subject: [PATCH 6385/6909] Allow checkbox nub to be moved to the left --- .../Graphics/UserInterface/OsuCheckbox.cs | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 517f83daa9..313962d9c6 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -48,7 +48,7 @@ namespace osu.Game.Graphics.UserInterface private SampleChannel sampleChecked; private SampleChannel sampleUnchecked; - public OsuCheckbox() + public OsuCheckbox(bool nubOnRight = true) { AutoSizeAxes = Axes.Y; RelativeSizeAxes = Axes.X; @@ -61,17 +61,24 @@ namespace osu.Game.Graphics.UserInterface { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, - Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding } - }, - Nub = new Nub - { - Anchor = Anchor.CentreRight, - Origin = Anchor.CentreRight, - Margin = new MarginPadding { Right = nub_padding }, }, + Nub = new Nub(), new HoverClickSounds() }; + if (nubOnRight) + { + Nub.Anchor = Anchor.CentreRight; + Nub.Origin = Anchor.CentreRight; + Nub.Margin = new MarginPadding { Right = nub_padding }; + labelText.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding }; + } + else + { + Nub.Margin = new MarginPadding { Left = nub_padding }; + labelText.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding }; + } + Nub.Current.BindTo(Current); Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1; From 3148bbda2a09c00f38aec261bcb4b78559c9412e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 16:54:17 +0900 Subject: [PATCH 6386/6909] Allow custom font to be used in OsuCheckbox --- osu.Game/Graphics/UserInterface/OsuCheckbox.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 313962d9c6..61a42dac23 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Graphics; +using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; using osu.Game.Graphics.Containers; @@ -57,7 +58,7 @@ namespace osu.Game.Graphics.UserInterface Children = new Drawable[] { - labelText = new OsuTextFlowContainer + labelText = new OsuTextFlowContainer(ApplyLabelParameters) { AutoSizeAxes = Axes.Y, RelativeSizeAxes = Axes.X, @@ -84,6 +85,13 @@ namespace osu.Game.Graphics.UserInterface Current.DisabledChanged += disabled => labelText.Alpha = Nub.Alpha = disabled ? 0.3f : 1; } + /// + /// A function which can be overridden to change the parameters of the label's text. + /// + protected virtual void ApplyLabelParameters(SpriteText text) + { + } + [BackgroundDependencyLoader] private void load(AudioManager audio) { From 48a58e790e2d2ec640d3273ee78e222ff6aed07e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 16:57:39 +0900 Subject: [PATCH 6387/6909] Don't specify arbitrary width --- osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 5b9a19897f..c60afeb2aa 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -86,7 +86,7 @@ namespace osu.Game.Screens.OnlinePlay protected override Drawable CreateHeader(string text) => new Container { AutoSizeAxes = Axes.Y, - Width = 175, + RelativeSizeAxes = Axes.X, Child = checkbox = new HeaderCheckbox { LabelText = text, From b32e10514d1f8f48d7e580947176a56faa497d5b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 16:58:02 +0900 Subject: [PATCH 6388/6909] Fix padding on label text not being double-applied (meaning no padding between nub and text) --- osu.Game/Graphics/UserInterface/OsuCheckbox.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index 61a42dac23..b80941c0bc 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -72,12 +72,12 @@ namespace osu.Game.Graphics.UserInterface Nub.Anchor = Anchor.CentreRight; Nub.Origin = Anchor.CentreRight; Nub.Margin = new MarginPadding { Right = nub_padding }; - labelText.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding }; + labelText.Padding = new MarginPadding { Right = Nub.EXPANDED_SIZE + nub_padding * 2 }; } else { Nub.Margin = new MarginPadding { Left = nub_padding }; - labelText.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding }; + labelText.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 }; } Nub.Current.BindTo(Current); From daf7ab942274a71298ff4caf3505cee52381aed0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 16:58:15 +0900 Subject: [PATCH 6389/6909] Apply the expected font to the checkbox's label --- .../Screens/OnlinePlay/FreeModSelectOverlay.cs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index c60afeb2aa..180f6079ac 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -3,8 +3,11 @@ using System; using System.Linq; +using System.Reflection.Emit; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Sprites; +using osu.Game.Graphics; using osu.Game.Graphics.UserInterface; using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; @@ -120,6 +123,19 @@ namespace osu.Game.Screens.OnlinePlay protected override bool PlaySoundsOnUserChange => false; + public HeaderCheckbox() + : base(false) + + { + } + + protected override void ApplyLabelParameters(SpriteText text) + { + base.ApplyLabelParameters(text); + + text.Font = OsuFont.GetFont(weight: FontWeight.Bold); + } + protected override void OnUserChange(bool value) { base.OnUserChange(value); From 4bfe3aabdc6c165e2bd368b9a70ab45ec4cc39b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 17:06:11 +0900 Subject: [PATCH 6390/6909] Simplify sound debounce logic --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index c308dc2451..97902d1c15 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -496,17 +496,12 @@ namespace osu.Game.Overlays.Mods MultiplierLabel.FadeColour(Color4.White, 200); } - private ScheduledDelegate sampleOnDelegate; - private ScheduledDelegate sampleOffDelegate; - private void modButtonPressed(Mod selectedMod) { if (selectedMod != null) { - // Fixes buzzing when multiple mods are selected in the same frame. - sampleOnDelegate?.Cancel(); if (State.Value == Visibility.Visible) - sampleOnDelegate = Scheduler.Add(() => sampleOn?.Play()); + Scheduler.AddOnce(playSelectedSound); OnModSelected(selectedMod); @@ -514,15 +509,16 @@ namespace osu.Game.Overlays.Mods } else { - // Fixes buzzing when multiple mods are deselected in the same frame. - sampleOffDelegate?.Cancel(); if (State.Value == Visibility.Visible) - sampleOffDelegate = Scheduler.Add(() => sampleOff?.Play()); + Scheduler.AddOnce(playDeselectedSound); } refreshSelectedMods(); } + private void playSelectedSound() => sampleOn?.Play(); + private void playDeselectedSound() => sampleOff?.Play(); + /// /// Invoked when a new has been selected. /// From f23ca7c7cfd7bbd752e50464189c2c84546beba2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 18:10:55 +0900 Subject: [PATCH 6391/6909] Centralise selection animation logic --- osu.Game/Overlays/Mods/ModSection.cs | 51 +++++++++++++------ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 1 - .../Overlays/Mods/SoloModSelectOverlay.cs | 2 +- .../OnlinePlay/FreeModSelectOverlay.cs | 25 +++++---- 4 files changed, 48 insertions(+), 31 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 87a45ebf63..495b1c05cd 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -33,6 +33,8 @@ namespace osu.Game.Overlays.Mods private CancellationTokenSource modsLoadCts; + protected bool SelectionAnimationRunning => pendingSelectionOperations.Count > 0; + /// /// True when all mod icons have completed loading. /// @@ -49,7 +51,11 @@ namespace osu.Game.Overlays.Mods return new ModButton(m) { - SelectionChanged = Action, + SelectionChanged = mod => + { + ModButtonStateChanged(mod); + Action?.Invoke(mod); + }, }; }).ToArray(); @@ -78,6 +84,10 @@ namespace osu.Game.Overlays.Mods } } + protected virtual void ModButtonStateChanged(Mod mod) + { + } + private ModButton[] buttons = Array.Empty(); protected override bool OnKeyDown(KeyDownEvent e) @@ -94,44 +104,53 @@ namespace osu.Game.Overlays.Mods return base.OnKeyDown(e); } + private const double initial_multiple_selection_delay = 100; + + private readonly Queue pendingSelectionOperations = new Queue(); + + protected override void LoadComplete() + { + base.LoadComplete(); + + Scheduler.AddDelayed(() => + { + if (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) + dequeuedAction(); + }, initial_multiple_selection_delay, true); + } + /// /// Selects all mods. /// public void SelectAll() { + pendingSelectionOperations.Clear(); + foreach (var button in buttons.Where(b => !b.Selected)) - button.SelectAt(0); + pendingSelectionOperations.Enqueue(() => button.SelectAt(0)); } /// /// Deselects all mods. /// - /// Set to true to bypass animations and update selections immediately. - public void DeselectAll(bool immediate = false) => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null), immediate); + public void DeselectAll() => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); /// /// Deselect one or more mods in this section. /// /// The types of s which should be deselected. - /// Set to true to bypass animations and update selections immediately. - public void DeselectTypes(IEnumerable modTypes, bool immediate = false) + public void DeselectTypes(IEnumerable modTypes) { - int delay = 0; + pendingSelectionOperations.Clear(); foreach (var button in buttons) { - Mod selected = button.SelectedMod; - if (selected == null) continue; + if (button.SelectedMod == null) continue; foreach (var type in modTypes) { - if (type.IsInstanceOfType(selected)) - { - if (immediate) - button.Deselect(); - else - Scheduler.AddDelayed(button.Deselect, delay += 50); - } + if (type.IsInstanceOfType(button.SelectedMod)) + pendingSelectionOperations.Enqueue(button.Deselect); } } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 97902d1c15..4f65a39ed3 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -14,7 +14,6 @@ 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.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; diff --git a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs index d039ad1f98..aa0e78c126 100644 --- a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Mods base.OnModSelected(mod); foreach (var section in ModSectionsContainer.Children) - section.DeselectTypes(mod.IncompatibleMods, true); + section.DeselectTypes(mod.IncompatibleMods); } } } diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 180f6079ac..7bc226bb3f 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -3,7 +3,6 @@ using System; using System.Linq; -using System.Reflection.Emit; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; @@ -72,7 +71,7 @@ namespace osu.Game.Screens.OnlinePlay private void deselectAll() { foreach (var section in ModSectionsContainer.Children) - section.DeselectAll(true); + section.DeselectAll(); } protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); @@ -99,21 +98,21 @@ namespace osu.Game.Screens.OnlinePlay private void onCheckboxChanged(bool value) { - foreach (var button in ButtonsContainer.OfType()) - { - if (value) - button.SelectAt(0); - else - button.Deselect(); - } + if (value) + SelectAll(); + else + DeselectAll(); } - protected override void Update() + protected override void ModButtonStateChanged(Mod mod) { - base.Update(); + base.ModButtonStateChanged(mod); - var validButtons = ButtonsContainer.OfType().Where(b => b.Mod.HasImplementation); - checkbox.Current.Value = validButtons.All(b => b.Selected); + if (!SelectionAnimationRunning) + { + var validButtons = ButtonsContainer.OfType().Where(b => b.Mod.HasImplementation); + checkbox.Current.Value = validButtons.All(b => b.Selected); + } } } From 223b858227ed2a0d2aa3af284acbe581dbab84be Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 18:56:40 +0900 Subject: [PATCH 6392/6909] Ramp the animation speed --- osu.Game/Overlays/Mods/ModSection.cs | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 495b1c05cd..b9640a6026 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -104,19 +104,31 @@ namespace osu.Game.Overlays.Mods return base.OnKeyDown(e); } - private const double initial_multiple_selection_delay = 100; + private const double initial_multiple_selection_delay = 120; + + private double selectionDelay = initial_multiple_selection_delay; + private double lastSelection; private readonly Queue pendingSelectionOperations = new Queue(); - protected override void LoadComplete() + protected override void Update() { - base.LoadComplete(); + base.Update(); - Scheduler.AddDelayed(() => + if (selectionDelay == initial_multiple_selection_delay || Time.Current - lastSelection >= selectionDelay) { if (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) + { dequeuedAction(); - }, initial_multiple_selection_delay, true); + + selectionDelay = Math.Max(30, selectionDelay * 0.8f); + lastSelection = Time.Current; + } + else + { + selectionDelay = initial_multiple_selection_delay; + } + } } /// From a2674f3c3ff5a99d81127aae980c0ed05614fc47 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 18:58:56 +0900 Subject: [PATCH 6393/6909] Add comments --- osu.Game/Overlays/Mods/ModSection.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index b9640a6026..353f779b4f 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -121,11 +121,14 @@ namespace osu.Game.Overlays.Mods { dequeuedAction(); + // each time we play an animation, we decrease the time until the next animation (to ramp the visual and audible elements). selectionDelay = Math.Max(30, selectionDelay * 0.8f); lastSelection = Time.Current; } else { + // reset the selection delay after all animations have been completed. + // this will cause the next action to be immediately performed. selectionDelay = initial_multiple_selection_delay; } } From bf239f8bef321dbb90e61ebe8e453a13a22f11c8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 19:12:37 +0900 Subject: [PATCH 6394/6909] Flush animation on closing mod overlay --- osu.Game/Overlays/Mods/ModSection.cs | 9 +++++++++ osu.Game/Overlays/Mods/ModSelectOverlay.cs | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 353f779b4f..440abfb1c0 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -231,5 +231,14 @@ namespace osu.Game.Overlays.Mods Font = OsuFont.GetFont(weight: FontWeight.Bold), Text = text }; + + /// + /// Play out all remaining animations immediately to leave mods in a good (final) state. + /// + public void FlushAnimation() + { + while (pendingSelectionOperations.TryDequeue(out var dequeuedAction)) + dequeuedAction(); + } } } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 4f65a39ed3..93fe693937 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -379,6 +379,11 @@ namespace osu.Game.Overlays.Mods { base.PopOut(); + foreach (var section in ModSectionsContainer) + { + section.FlushAnimation(); + } + FooterContainer.MoveToX(content_width, WaveContainer.DISAPPEAR_DURATION, Easing.InSine); FooterContainer.FadeOut(WaveContainer.DISAPPEAR_DURATION, Easing.InSine); From 15062cc63f71ae8ae64b9a5343da7585a5bb5fa0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 19:29:48 +0900 Subject: [PATCH 6395/6909] Fix intermittent test failures --- .../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 92104cfc72..7f4dfaa9b0 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -145,11 +145,11 @@ namespace osu.Game.Tests.Visual.UserInterface AddAssert("double time visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModDoubleTime))); AddStep("make double time invalid", () => modSelect.IsValidMod = m => !(m is OsuModDoubleTime)); - AddAssert("double time not visible", () => modSelect.ChildrenOfType().All(b => !b.Mods.Any(m => m is OsuModDoubleTime))); + AddUntilStep("double time not visible", () => modSelect.ChildrenOfType().All(b => !b.Mods.Any(m => m is OsuModDoubleTime))); AddAssert("nightcore still visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModNightcore))); AddStep("make double time valid again", () => modSelect.IsValidMod = m => true); - AddAssert("double time visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModDoubleTime))); + AddUntilStep("double time visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModDoubleTime))); AddAssert("nightcore still visible", () => modSelect.ChildrenOfType().Any(b => b.Mods.Any(m => m is OsuModNightcore))); } From 8f2f1a444f25058760583545c3390ce0562b3e5d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 19:55:09 +0900 Subject: [PATCH 6396/6909] Avoid resetting selection on deselecting incompatibile types --- osu.Game/Overlays/Mods/ModSection.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 440abfb1c0..3f93eec7df 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -148,7 +148,11 @@ namespace osu.Game.Overlays.Mods /// /// Deselects all mods. /// - public void DeselectAll() => DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); + public void DeselectAll() + { + pendingSelectionOperations.Clear(); + DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); + } /// /// Deselect one or more mods in this section. @@ -156,8 +160,6 @@ namespace osu.Game.Overlays.Mods /// The types of s which should be deselected. public void DeselectTypes(IEnumerable modTypes) { - pendingSelectionOperations.Clear(); - foreach (var button in buttons) { if (button.SelectedMod == null) continue; From cef16a9f61e6a9550f80b81f5c3cf6a2603c45e9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 19:55:15 +0900 Subject: [PATCH 6397/6909] Add test coverage of animation / selection flushing --- .../TestSceneModSelectOverlay.cs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 7f4dfaa9b0..44605f4994 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -8,6 +8,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Testing; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -46,6 +47,32 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("show", () => modSelect.Show()); } + [Test] + public void TestAnimationFlushOnClose() + { + changeRuleset(0); + + AddStep("Select all fun mods", () => + { + modSelect.ModSectionsContainer + .Single(c => c.ModType == ModType.DifficultyIncrease) + .SelectAll(); + }); + + AddUntilStep("many mods selected", () => modDisplay.Current.Value.Count >= 5); + + AddStep("trigger deselect and close overlay", () => + { + modSelect.ModSectionsContainer + .Single(c => c.ModType == ModType.DifficultyIncrease) + .DeselectAll(); + + modSelect.Hide(); + }); + + AddAssert("all mods deselected", () => modDisplay.Current.Value.Count == 0); + } + [Test] public void TestOsuMods() { @@ -312,6 +339,9 @@ namespace osu.Game.Tests.Visual.UserInterface public bool AllLoaded => ModSectionsContainer.Children.All(c => c.ModIconsLoaded); + public new FillFlowContainer ModSectionsContainer => + base.ModSectionsContainer; + public ModButton GetModButton(Mod mod) { var section = ModSectionsContainer.Children.Single(s => s.ModType == mod.Type); From f86f3236254019b99ae64e37b3628159c6758c94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 22:28:17 +0900 Subject: [PATCH 6398/6909] Add a basic guard against setting ScrollMethod too late in initialisation --- .../UI/DrawableManiaRuleset.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs index 6b34dbfa09..4ee060e91e 100644 --- a/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/UI/DrawableManiaRuleset.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; @@ -50,9 +51,21 @@ namespace osu.Game.Rulesets.Mania.UI protected new ManiaRulesetConfigManager Config => (ManiaRulesetConfigManager)base.Config; - public ScrollVisualisationMethod ScrollMethod = ScrollVisualisationMethod.Sequential; + public ScrollVisualisationMethod ScrollMethod + { + get => scrollMethod; + set + { + if (IsLoaded) + throw new InvalidOperationException($"Can't alter {nameof(ScrollMethod)} after ruleset is already loaded"); - protected override ScrollVisualisationMethod VisualisationMethod => ScrollMethod; + scrollMethod = value; + } + } + + private ScrollVisualisationMethod scrollMethod = ScrollVisualisationMethod.Sequential; + + protected override ScrollVisualisationMethod VisualisationMethod => scrollMethod; private readonly Bindable configDirection = new Bindable(); private readonly Bindable configTimeRange = new BindableDouble(); From 794f9e5e932294d8c77e9cdc5b0b995b8f8d9062 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 22:53:41 +0900 Subject: [PATCH 6399/6909] Add missing centre anchor/origin --- osu.Game/Graphics/UserInterface/OsuCheckbox.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index b80941c0bc..f6effa0834 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -76,6 +76,8 @@ namespace osu.Game.Graphics.UserInterface } else { + Nub.Anchor = Anchor.CentreLeft; + Nub.Origin = Anchor.CentreLeft; Nub.Margin = new MarginPadding { Left = nub_padding }; labelText.Padding = new MarginPadding { Left = Nub.EXPANDED_SIZE + nub_padding * 2 }; } From 0750c3cb6a3ae9f8bc614fe61f082e936ba4f705 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 4 Feb 2021 23:44:46 +0900 Subject: [PATCH 6400/6909] Add back immediate deselection flow to ensure user selections can occur without contention --- osu.Game/Overlays/Mods/ModSection.cs | 10 ++++++++-- osu.Game/Overlays/Mods/SoloModSelectOverlay.cs | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 3f93eec7df..ecbcba7ad3 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -158,7 +158,8 @@ namespace osu.Game.Overlays.Mods /// Deselect one or more mods in this section. /// /// The types of s which should be deselected. - public void DeselectTypes(IEnumerable modTypes) + /// Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow. + public void DeselectTypes(IEnumerable modTypes, bool immediate = false) { foreach (var button in buttons) { @@ -167,7 +168,12 @@ namespace osu.Game.Overlays.Mods foreach (var type in modTypes) { if (type.IsInstanceOfType(button.SelectedMod)) - pendingSelectionOperations.Enqueue(button.Deselect); + { + if (immediate) + button.Deselect(); + else + pendingSelectionOperations.Enqueue(button.Deselect); + } } } } diff --git a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs index aa0e78c126..d039ad1f98 100644 --- a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs @@ -12,7 +12,7 @@ namespace osu.Game.Overlays.Mods base.OnModSelected(mod); foreach (var section in ModSectionsContainer.Children) - section.DeselectTypes(mod.IncompatibleMods); + section.DeselectTypes(mod.IncompatibleMods, true); } } } From 18e50815232534bc456429b064ae3e71bc320a01 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 00:42:38 +0900 Subject: [PATCH 6401/6909] Fix test failures --- .../Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index a87b22affe..d76f354774 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.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.Testing; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Lounge.Components; @@ -50,5 +51,13 @@ namespace osu.Game.Tests.Visual.Multiplayer if (joinRoom) RoomManager.Schedule(() => RoomManager.CreateRoom(Room)); }); + + public override void SetUpSteps() + { + base.SetUpSteps(); + + if (joinRoom) + AddUntilStep("wait for room join", () => Client.Room != null); + } } } From dbea6d4ceed7504ff104683efcdf316d3c712d47 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 00:57:23 +0900 Subject: [PATCH 6402/6909] Remove unused using --- osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index d76f354774..2e8c834c65 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -6,7 +6,6 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Testing; using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Lounge.Components; From 4e530d2eaf45fc3c1b890b1016d2a6cbcec8658b Mon Sep 17 00:00:00 2001 From: Joehu Date: Wed, 3 Feb 2021 21:14:27 -0800 Subject: [PATCH 6403/6909] Remove old alpha hack from nub fill --- osu.Game/Graphics/UserInterface/Nub.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/Nub.cs b/osu.Game/Graphics/UserInterface/Nub.cs index 82b09e0821..18d8b880ea 100644 --- a/osu.Game/Graphics/UserInterface/Nub.cs +++ b/osu.Game/Graphics/UserInterface/Nub.cs @@ -42,13 +42,7 @@ namespace osu.Game.Graphics.UserInterface }, }; - Current.ValueChanged += filled => - { - if (filled.NewValue) - fill.FadeIn(200, Easing.OutQuint); - else - fill.FadeTo(0.01f, 200, Easing.OutQuint); //todo: remove once we figure why containers aren't drawing at all times - }; + Current.ValueChanged += filled => fill.FadeTo(filled.NewValue ? 1 : 0, 200, Easing.OutQuint); } [BackgroundDependencyLoader] From 9ef130cdccd46535c40db44b01b16f9624b4e36f Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 4 Feb 2021 13:28:35 -0800 Subject: [PATCH 6404/6909] Fix codefactor style issues --- osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs | 1 - osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs index 7308d6b499..8d8ee49af7 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/BorderPiece.cs @@ -29,4 +29,3 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default } } } - diff --git a/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs b/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs index d160956a6e..c8895f32f4 100644 --- a/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs +++ b/osu.Game.Rulesets.Catch/Skinning/Default/HyperBorderPiece.cs @@ -19,4 +19,3 @@ namespace osu.Game.Rulesets.Catch.Skinning.Default } } } - From d62bbbb7627673b4dc03729639a54ad9b864079e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 5 Feb 2021 00:38:56 +0300 Subject: [PATCH 6405/6909] Enhance documentation Co-authored-by: Dan Balasescu --- osu.Game/Online/DownloadTrackingComposite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/DownloadTrackingComposite.cs b/osu.Game/Online/DownloadTrackingComposite.cs index 69c6ebd07c..52042c266b 100644 --- a/osu.Game/Online/DownloadTrackingComposite.cs +++ b/osu.Game/Online/DownloadTrackingComposite.cs @@ -70,7 +70,7 @@ namespace osu.Game.Online /// Checks that a database model matches the one expected to be downloaded. /// /// - /// In the case of multiplayer/playlists, this has to check that the databased beatmap set with the selected beatmap matches what's online. + /// For online play, this could be used to check that the databased model matches the online beatmap. /// /// The model in database. protected virtual bool VerifyDatabasedModel([NotNull] TModel databasedModel) => true; From c0bd27fe832c0d05083107b0f488d7c979cce6d6 Mon Sep 17 00:00:00 2001 From: Joehu Date: Thu, 4 Feb 2021 13:55:15 -0800 Subject: [PATCH 6406/6909] Fix readme version badge not linking to correct page --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index efca075042..e09b4d86a5 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # osu! [![Build status](https://ci.appveyor.com/api/projects/status/u2p01nx7l6og8buh?svg=true)](https://ci.appveyor.com/project/peppy/osu) -[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)]() +[![GitHub release](https://img.shields.io/github/release/ppy/osu.svg)](https://github.com/ppy/osu/releases/latest) [![CodeFactor](https://www.codefactor.io/repository/github/ppy/osu/badge)](https://www.codefactor.io/repository/github/ppy/osu) [![dev chat](https://discordapp.com/api/guilds/188630481301012481/widget.png?style=shield)](https://discord.gg/ppy) From a2fdba3e5173441147761ace92894f54cc6f309f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 12:24:38 +0900 Subject: [PATCH 6407/6909] Rename to OnlinePlayBeatmapAvailabilityTracker --- ...s => TestSceneOnlinePlayBeatmapAvailabilityTracker.cs} | 8 ++++---- ...racker.cs => OnlinePlayBeatmapAvailablilityTracker.cs} | 4 ++-- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) rename osu.Game.Tests/Online/{TestSceneMultiplayerBeatmapAvailabilityTracker.cs => TestSceneOnlinePlayBeatmapAvailabilityTracker.cs} (96%) rename osu.Game/Online/Rooms/{MultiplayerBeatmapAvailablilityTracker.cs => OnlinePlayBeatmapAvailablilityTracker.cs} (95%) diff --git a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs similarity index 96% rename from osu.Game.Tests/Online/TestSceneMultiplayerBeatmapAvailabilityTracker.cs rename to osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 646c4139af..d3475de157 100644 --- a/osu.Game.Tests/Online/TestSceneMultiplayerBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -28,7 +28,7 @@ using osu.Game.Tests.Visual; namespace osu.Game.Tests.Online { [HeadlessTest] - public class TestSceneMultiplayerBeatmapAvailabilityTracker : OsuTestScene + public class TestSceneOnlinePlayBeatmapAvailabilityTracker : OsuTestScene { private RulesetStore rulesets; private TestBeatmapManager beatmaps; @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Online private BeatmapSetInfo testBeatmapSet; private readonly Bindable selectedItem = new Bindable(); - private MultiplayerBeatmapAvailablilityTracker availablilityTracker; + private OnlinePlayBeatmapAvailablilityTracker availablilityTracker; [BackgroundDependencyLoader] private void load(AudioManager audio, GameHost host) @@ -67,7 +67,7 @@ namespace osu.Game.Tests.Online Ruleset = { Value = testBeatmapInfo.Ruleset }, }; - Child = availablilityTracker = new MultiplayerBeatmapAvailablilityTracker + Child = availablilityTracker = new OnlinePlayBeatmapAvailablilityTracker { SelectedItem = { BindTarget = selectedItem, } }; @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Online }); addAvailabilityCheckStep("state still not downloaded", BeatmapAvailability.NotDownloaded); - AddStep("recreate tracker", () => Child = availablilityTracker = new MultiplayerBeatmapAvailablilityTracker + AddStep("recreate tracker", () => Child = availablilityTracker = new OnlinePlayBeatmapAvailablilityTracker { SelectedItem = { BindTarget = selectedItem } }); diff --git a/osu.Game/Online/Rooms/MultiplayerBeatmapAvailablilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs similarity index 95% rename from osu.Game/Online/Rooms/MultiplayerBeatmapAvailablilityTracker.cs rename to osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs index 578c4db2f8..ad4b3c5151 100644 --- a/osu.Game/Online/Rooms/MultiplayerBeatmapAvailablilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs @@ -15,7 +15,7 @@ namespace osu.Game.Online.Rooms /// This differs from a regular download tracking composite as this accounts for the /// databased beatmap set's checksum, to disallow from playing with an altered version of the beatmap. /// - public class MultiplayerBeatmapAvailablilityTracker : DownloadTrackingComposite + public class OnlinePlayBeatmapAvailablilityTracker : DownloadTrackingComposite { public readonly IBindable SelectedItem = new Bindable(); @@ -26,7 +26,7 @@ namespace osu.Game.Online.Rooms private readonly Bindable availability = new Bindable(); - public MultiplayerBeatmapAvailablilityTracker() + public OnlinePlayBeatmapAvailablilityTracker() { State.BindValueChanged(_ => updateAvailability()); Progress.BindValueChanged(_ => updateAvailability(), true); diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index cdf889e4f1..b1b3dde26e 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -41,11 +41,11 @@ namespace osu.Game.Screens.OnlinePlay.Match private IBindable> managerUpdated; [Cached] - protected MultiplayerBeatmapAvailablilityTracker BeatmapAvailablilityTracker { get; } + protected OnlinePlayBeatmapAvailablilityTracker BeatmapAvailablilityTracker { get; } protected RoomSubScreen() { - BeatmapAvailablilityTracker = new MultiplayerBeatmapAvailablilityTracker + BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker { SelectedItem = { BindTarget = SelectedItem }, }; From 85e63afcb48288f7838409c5b14380507ea3443d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 12:36:25 +0900 Subject: [PATCH 6408/6909] Rename Mods -> RequiredMods --- osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs | 6 +++--- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index d0e19d9f37..4fb9d724b5 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -30,7 +30,7 @@ namespace osu.Game.Online.Multiplayer [NotNull] [Key(4)] - public IEnumerable Mods { get; set; } = Enumerable.Empty(); + public IEnumerable RequiredMods { get; set; } = Enumerable.Empty(); [NotNull] [Key(5)] @@ -39,14 +39,14 @@ namespace osu.Game.Online.Multiplayer public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && BeatmapChecksum == other.BeatmapChecksum - && Mods.SequenceEqual(other.Mods) + && RequiredMods.SequenceEqual(other.RequiredMods) && AllowedMods.SequenceEqual(other.AllowedMods) && RulesetID == other.RulesetID && Name.Equals(other.Name, StringComparison.Ordinal); public override string ToString() => $"Name:{Name}" + $" Beatmap:{BeatmapID} ({BeatmapChecksum})" - + $" Mods:{string.Join(',', Mods)}" + + $" RequiredMods:{string.Join(',', RequiredMods)}" + $" AllowedMods:{string.Join(',', AllowedMods)}" + $" Ruleset:{RulesetID}"; } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 69df2a69cc..aedbe37d56 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -192,7 +192,7 @@ namespace osu.Game.Online.Multiplayer BeatmapID = item.GetOr(existingPlaylistItem).BeatmapID, BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, RulesetID = item.GetOr(existingPlaylistItem).RulesetID, - Mods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.Mods, + RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods, AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods }); } @@ -531,7 +531,7 @@ namespace osu.Game.Online.Multiplayer beatmap.MD5Hash = settings.BeatmapChecksum; var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); - var mods = settings.Mods.Select(m => m.ToMod(ruleset)); + var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset)); var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); PlaylistItem playlistItem = new PlaylistItem From 2e85ce5b824eeafe85f3b9b058b89601690314ea Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 12:40:16 +0900 Subject: [PATCH 6409/6909] Rename UserMods -> Mods for MultiplayerRoomUser --- osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs | 2 +- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 2 +- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs index 3271133bcd..c654127b94 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomUser.cs @@ -36,7 +36,7 @@ namespace osu.Game.Online.Multiplayer /// [Key(3)] [NotNull] - public IEnumerable UserMods { get; set; } = Enumerable.Empty(); + public IEnumerable Mods { get; set; } = Enumerable.Empty(); [IgnoreMember] public User? User { get; set; } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index aedbe37d56..f7c9193dfe 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -400,7 +400,7 @@ namespace osu.Game.Online.Multiplayer if (user == null) return; - user.UserMods = mods; + user.Mods = mods; RoomUpdated?.Invoke(); }, false); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 8036e5f702..2983d1268d 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance(); userStateDisplay.Status = User.State; - userModsDisplay.Current.Value = User.UserMods.Select(m => m.ToMod(ruleset)).ToList(); + userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList(); if (Room.Host?.Equals(User) == true) crown.FadeIn(fade_time); From 8004c19a8037bb4faece8646d604b1cb3c13ba97 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 12:40:42 +0900 Subject: [PATCH 6410/6909] Remove ModValidationTest --- osu.Game.Tests/Mods/ModValidationTest.cs | 64 ------------------------ 1 file changed, 64 deletions(-) delete mode 100644 osu.Game.Tests/Mods/ModValidationTest.cs diff --git a/osu.Game.Tests/Mods/ModValidationTest.cs b/osu.Game.Tests/Mods/ModValidationTest.cs deleted file mode 100644 index 991adc221e..0000000000 --- a/osu.Game.Tests/Mods/ModValidationTest.cs +++ /dev/null @@ -1,64 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using Moq; -using NUnit.Framework; -using osu.Game.Rulesets.Mods; -using osu.Game.Utils; - -namespace osu.Game.Tests.Mods -{ - [TestFixture] - public class ModValidationTest - { - [Test] - public void TestModIsCompatibleByItself() - { - var mod = new Mock(); - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod.Object })); - } - - [Test] - public void TestIncompatibleThroughTopLevel() - { - var mod1 = new Mock(); - var mod2 = new Mock(); - - mod1.Setup(m => m.IncompatibleMods).Returns(new[] { mod2.Object.GetType() }); - - // Test both orderings. - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, mod2.Object }), Is.False); - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod2.Object, mod1.Object }), Is.False); - } - - [Test] - public void TestIncompatibleThroughMultiMod() - { - var mod1 = new Mock(); - - // The nested mod. - var mod2 = new Mock(); - mod2.Setup(m => m.IncompatibleMods).Returns(new[] { mod1.Object.GetType() }); - - var multiMod = new MultiMod(new MultiMod(mod2.Object)); - - // Test both orderings. - Assert.That(ModUtils.CheckCompatibleSet(new[] { multiMod, mod1.Object }), Is.False); - Assert.That(ModUtils.CheckCompatibleSet(new[] { mod1.Object, multiMod }), Is.False); - } - - [Test] - public void TestAllowedThroughMostDerivedType() - { - var mod = new Mock(); - Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { mod.Object.GetType() })); - } - - [Test] - public void TestNotAllowedThroughBaseType() - { - var mod = new Mock(); - Assert.That(ModUtils.CheckAllowed(new[] { mod.Object }, new[] { typeof(Mod) }), Is.False); - } - } -} From df2da5950f2f7b1fd233f75900e3979a362204e3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 13:05:11 +0900 Subject: [PATCH 6411/6909] Add back vertical spacer --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4664ac6bfe..0466c8209f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -104,6 +104,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Child = new GridContainer { RelativeSizeAxes = Axes.Both, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 400), + new Dimension(), + new Dimension(GridSizeMode.Relative, size: 0.5f, maxSize: 600), + }, Content = new[] { new Drawable[] @@ -128,6 +134,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } }, + // Spacer + null, // Main right column new FillFlowContainer { From c5fa818630c25ee7b3912de0d1bfeca9194bc7a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 14:08:11 +0900 Subject: [PATCH 6412/6909] Actually handle case of failing to achieve lock on SemaphoreSlim --- osu.Game/Online/Multiplayer/MultiplayerClient.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 9067c9a738..36cdf3bc8f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -67,7 +67,8 @@ namespace osu.Game.Online.Multiplayer { cancelExistingConnect(); - await connectionLock.WaitAsync(10000); + if (!await connectionLock.WaitAsync(10000)) + throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck."); var builder = new HubConnectionBuilder() .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); @@ -199,7 +200,10 @@ namespace osu.Game.Online.Multiplayer cancelExistingConnect(); if (takeLock) - await connectionLock.WaitAsync(10000); + { + if (!await connectionLock.WaitAsync(10000)) + throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck."); + } try { From fc37d8b7df27a398f0343fcefbb6f15ce3b9450f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 14:25:19 +0900 Subject: [PATCH 6413/6909] Refactor content redirection logic to be easier to parse --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 19b68ee6d6..722b167387 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -45,18 +45,21 @@ namespace osu.Game.Screens.OnlinePlay.Match [Cached] protected OnlinePlayBeatmapAvailablilityTracker BeatmapAvailablilityTracker { get; } - private readonly Container content = new Container { RelativeSizeAxes = Axes.Both }; + private readonly Container content; protected RoomSubScreen() { - BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker + InternalChildren = new Drawable[] { - SelectedItem = { BindTarget = SelectedItem } + BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker + { + SelectedItem = { BindTarget = SelectedItem } + }, + content = new Container { RelativeSizeAxes = Axes.Both }, }; - - base.AddInternal(content); } + // Forward all internal management to content to ensure locally added components are not removed unintentionally. // This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690) protected override void AddInternal(Drawable drawable) => content.Add(drawable); protected override bool RemoveInternal(Drawable drawable) => content.Remove(drawable); @@ -65,8 +68,6 @@ namespace osu.Game.Screens.OnlinePlay.Match [BackgroundDependencyLoader] private void load(AudioManager audio) { - base.AddInternal(BeatmapAvailablilityTracker); - sampleStart = audio.Samples.Get(@"SongSelect/confirm-selection"); } From de8724b1f6b7d714260589dab5f5619afea0ca6f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 14:39:25 +0900 Subject: [PATCH 6414/6909] Use AddRangeInternal for simplicity, but disallow ClearInternal for safety --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 21 +++++-------------- .../Multiplayer/MultiplayerMatchSubScreen.cs | 4 ++-- .../Playlists/PlaylistsRoomSubScreen.cs | 4 ++-- 3 files changed, 9 insertions(+), 20 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 722b167387..8be0e89665 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -7,8 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -45,25 +43,16 @@ namespace osu.Game.Screens.OnlinePlay.Match [Cached] protected OnlinePlayBeatmapAvailablilityTracker BeatmapAvailablilityTracker { get; } - private readonly Container content; - protected RoomSubScreen() { - InternalChildren = new Drawable[] + AddInternal(BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker { - BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker - { - SelectedItem = { BindTarget = SelectedItem } - }, - content = new Container { RelativeSizeAxes = Axes.Both }, - }; + SelectedItem = { BindTarget = SelectedItem } + }); } - // Forward all internal management to content to ensure locally added components are not removed unintentionally. - // This is a bit ugly but we don't have the concept of InternalContent so it'll have to do for now. (https://github.com/ppy/osu-framework/issues/1690) - protected override void AddInternal(Drawable drawable) => content.Add(drawable); - protected override bool RemoveInternal(Drawable drawable) => content.Remove(drawable); - protected override void ClearInternal(bool disposeChildren = true) => content.Clear(disposeChildren); + protected override void ClearInternal(bool disposeChildren = true) => + throw new InvalidOperationException($"{nameof(RoomSubScreen)}'s children should not be cleared as it will remove required components"); [BackgroundDependencyLoader] private void load(AudioManager audio) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 8cc2e9061b..e1524067da 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -54,7 +54,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + AddRangeInternal(new Drawable[] { mainContent = new GridContainer { @@ -177,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RelativeSizeAxes = Axes.Both, State = { Value = client.Room == null ? Visibility.Visible : Visibility.Hidden } } - }; + }); if (client.Room == null) { diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 745e5a58bf..0b8026044b 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [BackgroundDependencyLoader] private void load() { - InternalChildren = new Drawable[] + AddRangeInternal(new Drawable[] { mainContent = new GridContainer { @@ -190,7 +190,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists EditPlaylist = () => this.Push(new MatchSongSelect()), State = { Value = roomId.Value == null ? Visibility.Visible : Visibility.Hidden } } - }; + }); if (roomId.Value == null) { From 730e66f0ee00aec2b6c9f2a2f03ca56ec152a136 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 5 Feb 2021 09:07:59 +0300 Subject: [PATCH 6415/6909] Make pausing on window focus lose instant --- .../Screens/Play/HUD/HoldForMenuButton.cs | 40 ------------------- osu.Game/Screens/Play/Player.cs | 21 +++++++--- 2 files changed, 16 insertions(+), 45 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs index 387c0e587b..284ac899ed 100644 --- a/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs +++ b/osu.Game/Screens/Play/HUD/HoldForMenuButton.cs @@ -88,11 +88,6 @@ namespace osu.Game.Screens.Play.HUD return base.OnMouseMove(e); } - public bool PauseOnFocusLost - { - set => button.PauseOnFocusLost = value; - } - protected override void Update() { base.Update(); @@ -120,8 +115,6 @@ namespace osu.Game.Screens.Play.HUD public Action HoverGained; public Action HoverLost; - private readonly IBindable gameActive = new Bindable(true); - [BackgroundDependencyLoader] private void load(OsuColour colours, Framework.Game game) { @@ -164,14 +157,6 @@ namespace osu.Game.Screens.Play.HUD }; bind(); - - gameActive.BindTo(game.IsActive); - } - - protected override void LoadComplete() - { - base.LoadComplete(); - gameActive.BindValueChanged(_ => updateActive(), true); } private void bind() @@ -221,31 +206,6 @@ namespace osu.Game.Screens.Play.HUD base.OnHoverLost(e); } - private bool pauseOnFocusLost = true; - - public bool PauseOnFocusLost - { - set - { - if (pauseOnFocusLost == value) - return; - - pauseOnFocusLost = value; - if (IsLoaded) - updateActive(); - } - } - - private void updateActive() - { - if (!pauseOnFocusLost || IsPaused.Value) return; - - if (gameActive.Value) - AbortConfirm(); - else - BeginConfirm(); - } - public bool OnPressed(GlobalAction action) { switch (action) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index b622f11775..556964bca4 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -59,6 +59,8 @@ namespace osu.Game.Screens.Play // We are managing our own adjustments (see OnEntering/OnExiting). public override bool AllowRateAdjustments => false; + private readonly IBindable gameActive = new Bindable(true); + private readonly Bindable samplePlaybackDisabled = new Bindable(); /// @@ -154,6 +156,9 @@ namespace osu.Game.Screens.Play // replays should never be recorded or played back when autoplay is enabled if (!Mods.Value.Any(m => m is ModAutoplay)) PrepareReplay(); + + // needs to be bound here as the last binding, otherwise starting a replay while not focused causes player to exit. + gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); } [CanBeNull] @@ -170,7 +175,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, OsuConfigManager config, OsuGame game) + private void load(AudioManager audio, OsuConfigManager config, OsuGame game, OsuGameBase gameBase) { Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray(); @@ -186,6 +191,8 @@ namespace osu.Game.Screens.Play mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); + gameActive.BindTo(gameBase.IsActive); + if (game != null) LocalUserPlaying.BindTo(game.LocalUserPlaying); @@ -420,10 +427,14 @@ namespace osu.Game.Screens.Play samplePlaybackDisabled.Value = DrawableRuleset.FrameStableClock.IsCatchingUp.Value || GameplayClockContainer.GameplayClock.IsPaused.Value; } - private void updatePauseOnFocusLostState() => - HUDOverlay.HoldToQuit.PauseOnFocusLost = PauseOnFocusLost - && !DrawableRuleset.HasReplayLoaded.Value - && !breakTracker.IsBreakTime.Value; + private void updatePauseOnFocusLostState() + { + if (!IsLoaded || !PauseOnFocusLost || DrawableRuleset.HasReplayLoaded.Value || breakTracker.IsBreakTime.Value) + return; + + if (gameActive.Value == false) + performUserRequestedExit(); + } private IBeatmap loadPlayableBeatmap() { From 0528469b440d33308cdc61c7d2a136027e4731e4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 14:04:04 +0900 Subject: [PATCH 6416/6909] Rename OrderedHitPolicy -> StartTimeOrderedHitPolicy --- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 ++-- .../UI/{OrderedHitPolicy.cs => StartTimeOrderedHitPolicy.cs} | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename osu.Game.Rulesets.Osu/UI/{OrderedHitPolicy.cs => StartTimeOrderedHitPolicy.cs} (97%) diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 975b444699..5c77112df7 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ProxyContainer spinnerProxies; private readonly JudgementContainer judgementLayer; private readonly FollowPointRenderer followPoints; - private readonly OrderedHitPolicy hitPolicy; + private readonly StartTimeOrderedHitPolicy hitPolicy; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; - hitPolicy = new OrderedHitPolicy(HitObjectContainer); + hitPolicy = new StartTimeOrderedHitPolicy(HitObjectContainer); var hitWindows = new OsuHitWindows(); diff --git a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs similarity index 97% rename from osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs rename to osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs index 8e4f81347d..1d9a5010ed 100644 --- a/osu.Game.Rulesets.Osu/UI/OrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs @@ -18,11 +18,11 @@ namespace osu.Game.Rulesets.Osu.UI /// The hit causes all previous s to missed otherwise. /// /// - public class OrderedHitPolicy + public class StartTimeOrderedHitPolicy { private readonly HitObjectContainer hitObjectContainer; - public OrderedHitPolicy(HitObjectContainer hitObjectContainer) + public StartTimeOrderedHitPolicy(HitObjectContainer hitObjectContainer) { this.hitObjectContainer = hitObjectContainer; } From df1df8184771f096a58acc095efc576485c7301c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 14:04:21 +0900 Subject: [PATCH 6417/6909] Better indicate ordering --- osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs index 1d9a5010ed..2b13a36e47 100644 --- a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI { /// - /// Ensures that s are hit in-order. Affectionately known as "note lock". + /// Ensures that s are hit in-order of their start times. 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 08aae011c10880a24204717cfea5eb5c2629571b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 14:04:32 +0900 Subject: [PATCH 6418/6909] Add IHitPolicy interface --- osu.Game.Rulesets.Osu/UI/IHitPolicy.cs | 25 +++++++++++++++++++ .../UI/StartTimeOrderedHitPolicy.cs | 13 ++-------- 2 files changed, 27 insertions(+), 11 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/IHitPolicy.cs diff --git a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs new file mode 100644 index 0000000000..fcc18b5f6f --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs @@ -0,0 +1,25 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.UI +{ + public interface IHitPolicy + { + /// + /// 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 . + bool IsHittable(DrawableHitObject hitObject, double time); + + /// + /// Handles a being hit. + /// + /// The that was hit. + void HandleHit(DrawableHitObject hitObject); + } +} diff --git a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs index 2b13a36e47..2b5a440f67 100644 --- a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.UI /// The hit causes all previous s to missed otherwise. /// /// - public class StartTimeOrderedHitPolicy + public class StartTimeOrderedHitPolicy : IHitPolicy { private readonly HitObjectContainer hitObjectContainer; @@ -27,12 +27,6 @@ namespace osu.Game.Rulesets.Osu.UI 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 blockingObject = null; @@ -54,10 +48,6 @@ namespace osu.Game.Rulesets.Osu.UI return blockingObject.Judged || time >= blockingObject.HitObject.StartTime; } - /// - /// Handles a being hit to potentially miss all earlier s. - /// - /// The that was hit. public void HandleHit(DrawableHitObject hitObject) { // Hitobjects which themselves don't block future hitobjects don't cause misses (e.g. slider ticks, spinners). @@ -67,6 +57,7 @@ namespace osu.Game.Rulesets.Osu.UI if (!IsHittable(hitObject, hitObject.HitObject.StartTime + hitObject.Result.TimeOffset)) throw new InvalidOperationException($"A {hitObject} was hit before it became hittable!"); + // Miss all hitobjects prior to the hit one. foreach (var obj in enumerateHitObjectsUpTo(hitObject.HitObject.StartTime)) { if (obj.Judged) From 8adf37d958abc4bb3b42e600149a4b0fdbbbaa16 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 14:09:25 +0900 Subject: [PATCH 6419/6909] Add SetHitObjects() to IHitPolicy instead of using ctor --- osu.Game.Rulesets.Osu/UI/IHitPolicy.cs | 7 +++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 3 ++- osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs | 10 +++------- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs index fcc18b5f6f..72c3d781bb 100644 --- a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; @@ -8,6 +9,12 @@ namespace osu.Game.Rulesets.Osu.UI { public interface IHitPolicy { + /// + /// Sets the s which this controls. + /// + /// An enumeration of the s. + void SetHitObjects(IEnumerable hitObjects); + /// /// Determines whether a can be hit at a point in time. /// diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 5c77112df7..c7900558a0 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -54,7 +54,8 @@ namespace osu.Game.Rulesets.Osu.UI approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; - hitPolicy = new StartTimeOrderedHitPolicy(HitObjectContainer); + hitPolicy = new StartTimeOrderedHitPolicy(); + hitPolicy.SetHitObjects(HitObjectContainer.AliveObjects); var hitWindows = new OsuHitWindows(); diff --git a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs index 2b5a440f67..38ba5fc490 100644 --- a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI { @@ -20,12 +19,9 @@ namespace osu.Game.Rulesets.Osu.UI /// public class StartTimeOrderedHitPolicy : IHitPolicy { - private readonly HitObjectContainer hitObjectContainer; + private IEnumerable hitObjects; - public StartTimeOrderedHitPolicy(HitObjectContainer hitObjectContainer) - { - this.hitObjectContainer = hitObjectContainer; - } + public void SetHitObjects(IEnumerable hitObjects) => this.hitObjects = hitObjects; public bool IsHittable(DrawableHitObject hitObject, double time) { @@ -77,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.UI private IEnumerable enumerateHitObjectsUpTo(double targetTime) { - foreach (var obj in hitObjectContainer.AliveObjects) + foreach (var obj in hitObjects) { if (obj.HitObject.StartTime >= targetTime) yield break; From c9481ebbafa4c28ba21730448dcdfbf2b71d59e6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 14:49:33 +0900 Subject: [PATCH 6420/6909] Rename test scene --- ...eOutOfOrderHits.cs => TestSceneStartTimeOrderedHitPolicy.cs} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename osu.Game.Rulesets.Osu.Tests/{TestSceneOutOfOrderHits.cs => TestSceneStartTimeOrderedHitPolicy.cs} (99%) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs similarity index 99% rename from osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs rename to osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index 296b421a11..b8c1217a73 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneOutOfOrderHits.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -25,7 +25,7 @@ using osuTK; namespace osu.Game.Rulesets.Osu.Tests { - public class TestSceneOutOfOrderHits : RateAdjustedBeatmapTestScene + public class TestSceneStartTimeOrderedHitPolicy : 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 64a2c7825e89811dec78f803507fdd4c8696cc52 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 14:53:47 +0900 Subject: [PATCH 6421/6909] Fix incorrect assert --- .../TestSceneStartTimeOrderedHitPolicy.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs index b8c1217a73..177a4f50a1 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneStartTimeOrderedHitPolicy.cs @@ -169,7 +169,7 @@ namespace osu.Game.Rulesets.Osu.Tests 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 + addJudgementOffsetAssert(hitObjects[1], -200); // time_second_circle - first_circle_time - 100 } /// From 4bc324f040b3bfaca1e57af88f00558a6237d6bc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 15:29:32 +0900 Subject: [PATCH 6422/6909] Rename parameter to make more sense --- osu.Game/Graphics/UserInterface/ProgressBar.cs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/ProgressBar.cs b/osu.Game/Graphics/UserInterface/ProgressBar.cs index 4ee1c73bf5..50367e600e 100644 --- a/osu.Game/Graphics/UserInterface/ProgressBar.cs +++ b/osu.Game/Graphics/UserInterface/ProgressBar.cs @@ -40,13 +40,18 @@ namespace osu.Game.Graphics.UserInterface set => CurrentNumber.Value = value; } - private readonly bool userInteractive; - public override bool HandlePositionalInput => userInteractive; - public override bool HandleNonPositionalInput => userInteractive; + private readonly bool allowSeek; - public ProgressBar(bool userInteractive) + public override bool HandlePositionalInput => allowSeek; + public override bool HandleNonPositionalInput => allowSeek; + + /// + /// Construct a new progress bar. + /// + /// Whether the user should be allowed to click/drag to adjust the value. + public ProgressBar(bool allowSeek) { - this.userInteractive = userInteractive; + this.allowSeek = allowSeek; CurrentNumber.MinValue = 0; CurrentNumber.MaxValue = 1; From d1f9aa52a4c528053d7ee45344ad5ec15dff4fe5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 15:33:48 +0900 Subject: [PATCH 6423/6909] Inline variable --- .../OnlinePlay/Multiplayer/Participants/StateDisplay.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index 4245628a59..c117c026dc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs @@ -101,9 +101,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants case DownloadState.Downloading: Debug.Assert(availability.DownloadProgress != null); - var progress = availability.DownloadProgress.Value; progressBar.FadeIn(fade_time); - progressBar.CurrentTime = progress; + progressBar.CurrentTime = availability.DownloadProgress.Value; text.Text = "downloading map"; icon.Icon = FontAwesome.Solid.ArrowAltCircleDown; From a4551dc1eeb2eb6afb9dec61752559eaa6e5632c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 14:31:22 +0900 Subject: [PATCH 6424/6909] Add object-ordered hit policy --- .../UI/ObjectOrderedHitPolicy.cs | 55 +++++++++++++++++++ osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 4 +- 2 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs diff --git a/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.cs new file mode 100644 index 0000000000..fdab241a9b --- /dev/null +++ b/osu.Game.Rulesets.Osu/UI/ObjectOrderedHitPolicy.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.Linq; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.Osu.Objects.Drawables; + +namespace osu.Game.Rulesets.Osu.UI +{ + /// + /// Ensures that s are hit in order of appearance. The classic note lock. + /// + /// Hits will be blocked until the previous s have been judged. + /// + /// + public class ObjectOrderedHitPolicy : IHitPolicy + { + private IEnumerable hitObjects; + + public void SetHitObjects(IEnumerable hitObjects) => this.hitObjects = hitObjects; + + public bool IsHittable(DrawableHitObject hitObject, double time) => enumerateHitObjectsUpTo(hitObject.HitObject.StartTime).All(obj => obj.AllJudged); + + public void HandleHit(DrawableHitObject hitObject) + { + } + + private IEnumerable enumerateHitObjectsUpTo(double targetTime) + { + foreach (var obj in hitObjects) + { + if (obj.HitObject.StartTime >= targetTime) + yield break; + + switch (obj) + { + case DrawableSpinner _: + continue; + + case DrawableSlider slider: + yield return slider.HeadCircle; + + break; + + default: + yield return obj; + + break; + } + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index c7900558a0..6cb890323b 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ProxyContainer spinnerProxies; private readonly JudgementContainer judgementLayer; private readonly FollowPointRenderer followPoints; - private readonly StartTimeOrderedHitPolicy hitPolicy; + private readonly IHitPolicy hitPolicy; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; - hitPolicy = new StartTimeOrderedHitPolicy(); + hitPolicy = new ObjectOrderedHitPolicy(); hitPolicy.SetHitObjects(HitObjectContainer.AliveObjects); var hitWindows = new OsuHitWindows(); From 6aece18f8dedc392a84996d1c7e4a906b72b827e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 15:23:03 +0900 Subject: [PATCH 6425/6909] Add OOHP tests --- .../TestSceneObjectOrderedHitPolicy.cs | 491 ++++++++++++++++++ osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 29 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 17 +- 3 files changed, 529 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs new file mode 100644 index 0000000000..039a4f142f --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -0,0 +1,491 @@ +// Copyright (c) ppy Pty Ltd . 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.Extensions.TypeExtensions; +using osu.Framework.Screens; +using osu.Framework.Utils; +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.Mods; +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 TestSceneObjectOrderedHitPolicy : 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 + + /// + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTime() + { + 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 = positionFirstCircle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle - 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + addJudgementOffsetAssert(hitObjects[0], late_miss_window); + } + + /// + /// Tests clicking a future circle at the first circle's start time, while the first circle HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAtFirstCircleTime() + { + 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 = positionFirstCircle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + addJudgementOffsetAssert(hitObjects[0], late_miss_window); + } + + /// + /// Tests clicking a future circle after the first circle's start time, while the first circle HAS NOT been judged. + /// + [Test] + public void TestClickSecondCircleAfterFirstCircleTime() + { + 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 = positionFirstCircle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle + 100, Position = positionSecondCircle, Actions = { OsuAction.LeftButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Miss); + addJudgementAssert(hitObjects[1], HitResult.Miss); + addJudgementOffsetAssert(hitObjects[0], late_miss_window); + } + + /// + /// Tests clicking a future circle before the first circle's start time, while the first circle HAS been judged. + /// + [Test] + public void TestClickSecondCircleBeforeFirstCircleTimeWithFirstCircleJudged() + { + 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 = positionFirstCircle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + 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); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 + addJudgementOffsetAssert(hitObjects[0], -200); // time_second_circle - first_circle_time - 100 + } + + /// + /// Tests clicking a future circle after the first circle's start time, while the first circle HAS been judged. + /// + [Test] + public void TestClickSecondCircleAfterFirstCircleTimeWithFirstCircleJudged() + { + 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 = positionFirstCircle + }, + new TestHitCircle + { + StartTime = time_second_circle, + Position = positionSecondCircle + } + }; + + performTest(hitObjects, new List + { + new OsuReplayFrame { Time = time_first_circle - 200, Position = positionFirstCircle, Actions = { OsuAction.LeftButton } }, + new OsuReplayFrame { Time = time_first_circle, Position = positionSecondCircle, Actions = { OsuAction.RightButton } } + }); + + addJudgementAssert(hitObjects[0], HitResult.Great); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementOffsetAssert(hitObjects[0], -200); // time_first_circle - 200 + addJudgementOffsetAssert(hitObjects[1], -100); // time_second_circle - first_circle_time + } + + /// + /// Tests clicking a future circle after a slider's start time, but hitting all slider ticks. + /// + [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.Miss); + addJudgementAssert(hitObjects[1], HitResult.Great); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.IgnoreHit); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); + } + + /// + /// Tests clicking hitting future slider ticks before a circle. + /// + [Test] + public void TestHitSliderTicksBeforeCircle() + { + const double time_slider = 1500; + const double time_circle = 1510; + Vector2 positionCircle = Vector2.Zero; + Vector2 positionSlider = new Vector2(30); + + 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.IgnoreHit); + addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); + } + + /// + /// Tests clicking a future circle before a spinner. + /// + [Test] + public void TestHitCircleBeforeSpinner() + { + const double time_spinner = 1500; + const double time_circle = 1800; + 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 - 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 } }, + 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); + } + + [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}", + () => 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}", + () => Precision.AlmostEquals(judgementResults.Single(r => r.HitObject == hitObject).TimeOffset, offset, 100)); + } + + private ScoreAccessibleReplayPlayer currentPlayer; + private List judgementResults; + + private void performTest(List hitObjects, List frames) + { + AddStep("load player", () => + { + Beatmap.Value = CreateWorkingBeatmap(new Beatmap + { + HitObjects = hitObjects, + BeatmapInfo = + { + BaseDifficulty = new BeatmapDifficulty { SliderTickRate = 3 }, + Ruleset = new OsuRuleset().RulesetInfo + }, + }); + + Beatmap.Value.Beatmap.ControlPointInfo.Add(0, new DifficultyControlPoint { SpeedMultiplier = 0.1f }); + + SelectedMods.Value = new[] { new OsuModClassic() }; + + var p = new ScoreAccessibleReplayPlayer(new Score { Replay = new Replay { Frames = frames } }); + + p.OnLoadComplete += _ => + { + p.ScoreProcessor.NewJudgement += result => + { + if (currentPlayer == p) judgementResults.Add(result); + }; + }; + + LoadScreen(currentPlayer = p); + judgementResults = new List(); + }); + + AddUntilStep("Beatmap at 0", () => Beatmap.Value.Track.CurrentTime == 0); + AddUntilStep("Wait until player is loaded", () => currentPlayer.IsCurrentScreen()); + AddUntilStep("Wait for completion", () => currentPlayer.ScoreProcessor.HasCompleted.Value); + } + + private class TestHitCircle : HitCircle + { + protected override HitWindows CreateHitWindows() => new TestHitWindows(); + } + + private class TestSlider : Slider + { + public TestSlider() + { + DefaultsApplied += _ => + { + HeadCircle.HitWindows = new TestHitWindows(); + TailCircle.HitWindows = new TestHitWindows(); + + HeadCircle.HitWindows.SetDifficulty(0); + TailCircle.HitWindows.SetDifficulty(0); + }; + } + } + + 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 = + { + new DifficultyRange(HitResult.Great, 500, 500, 500), + 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; + + 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, new PlayerConfiguration + { + AllowPause = false, + ShowResults = false, + }) + { + } + } + } +} diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 5542580979..6f41bcc0b0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -2,14 +2,18 @@ // See the LICENCE file in the repository root for full licence text. using System.Linq; +using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; +using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModClassic : Mod, IApplicableToHitObject + public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableRuleset { public override string Name => "Classic"; @@ -23,21 +27,38 @@ namespace osu.Game.Rulesets.Osu.Mods public override bool Ranked => false; + [SettingSource("Disable slider head judgement", "Scores sliders proportionally to the number of ticks hit.")] + public Bindable DisableSliderHeadJudgement { get; } = new BindableBool(true); + + [SettingSource("Disable slider head tracking", "Pins slider heads at their starting position, regardless of time.")] + public Bindable DisableSliderHeadTracking { get; } = new BindableBool(true); + + [SettingSource("Disable note lock lenience", "Applies note lock to the full hit window.")] + public Bindable DisableLenientNoteLock { get; } = new BindableBool(true); + public void ApplyToHitObject(HitObject hitObject) { switch (hitObject) { case Slider slider: - slider.IgnoreJudgement = false; + slider.IgnoreJudgement = !DisableSliderHeadJudgement.Value; foreach (var head in slider.NestedHitObjects.OfType()) { - head.TrackFollowCircle = false; - head.JudgeAsNormalHitCircle = false; + head.TrackFollowCircle = !DisableSliderHeadTracking.Value; + head.JudgeAsNormalHitCircle = !DisableSliderHeadJudgement.Value; } break; } } + + public void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + var osuRuleset = (DrawableOsuRuleset)drawableRuleset; + + if (!DisableLenientNoteLock.Value) + osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); + } } } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 6cb890323b..9bd1dc74b7 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -31,7 +31,6 @@ namespace osu.Game.Rulesets.Osu.UI private readonly ProxyContainer spinnerProxies; private readonly JudgementContainer judgementLayer; private readonly FollowPointRenderer followPoints; - private readonly IHitPolicy hitPolicy; public static readonly Vector2 BASE_SIZE = new Vector2(512, 384); @@ -54,11 +53,9 @@ namespace osu.Game.Rulesets.Osu.UI approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; - hitPolicy = new ObjectOrderedHitPolicy(); - hitPolicy.SetHitObjects(HitObjectContainer.AliveObjects); + HitPolicy = new ObjectOrderedHitPolicy(); 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, onJudgmentLoaded)); @@ -67,6 +64,18 @@ namespace osu.Game.Rulesets.Osu.UI NewResult += onNewResult; } + private IHitPolicy hitPolicy; + + public IHitPolicy HitPolicy + { + get => hitPolicy; + set + { + hitPolicy = value ?? throw new ArgumentNullException(nameof(value)); + hitPolicy.SetHitObjects(HitObjectContainer.AliveObjects); + } + } + protected override void OnNewDrawableHitObject(DrawableHitObject drawable) { ((DrawableOsuHitObject)drawable).CheckHittable = hitPolicy.IsHittable; From 1b6a05279847569be9d5568bc9812fa55b7fe503 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 15:46:03 +0900 Subject: [PATCH 6426/6909] Refactor logic to suck a bit less --- .../Multiplayer/Participants/StateDisplay.cs | 161 +++++++++--------- 1 file changed, 83 insertions(+), 78 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs index c117c026dc..c571b51c83 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/StateDisplay.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.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -87,85 +88,89 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants public void UpdateStatus(MultiplayerUserState state, BeatmapAvailability availability) { - if (availability.State != DownloadState.LocallyAvailable) - { - switch (availability.State) - { - case DownloadState.NotDownloaded: - progressBar.FadeOut(fade_time); - text.Text = "no map"; - icon.Icon = FontAwesome.Solid.MinusCircle; - icon.Colour = colours.RedLight; - break; - - case DownloadState.Downloading: - Debug.Assert(availability.DownloadProgress != null); - - progressBar.FadeIn(fade_time); - progressBar.CurrentTime = availability.DownloadProgress.Value; - - text.Text = "downloading map"; - icon.Icon = FontAwesome.Solid.ArrowAltCircleDown; - icon.Colour = colours.Blue; - break; - - case DownloadState.Importing: - progressBar.FadeOut(fade_time); - text.Text = "importing map"; - icon.Icon = FontAwesome.Solid.ArrowAltCircleDown; - icon.Colour = colours.Yellow; - break; - } - } - else - { - progressBar.FadeOut(fade_time); - - switch (state) - { - default: - this.FadeOut(fade_time); - return; - - case MultiplayerUserState.Ready: - text.Text = "ready"; - icon.Icon = FontAwesome.Solid.CheckCircle; - icon.Colour = Color4Extensions.FromHex("#AADD00"); - break; - - case MultiplayerUserState.WaitingForLoad: - text.Text = "loading"; - icon.Icon = FontAwesome.Solid.PauseCircle; - icon.Colour = colours.Yellow; - break; - - case MultiplayerUserState.Loaded: - text.Text = "loaded"; - icon.Icon = FontAwesome.Solid.DotCircle; - icon.Colour = colours.YellowLight; - break; - - case MultiplayerUserState.Playing: - text.Text = "playing"; - icon.Icon = FontAwesome.Solid.PlayCircle; - icon.Colour = colours.BlueLight; - break; - - case MultiplayerUserState.FinishedPlay: - text.Text = "results pending"; - icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; - icon.Colour = colours.BlueLighter; - break; - - case MultiplayerUserState.Results: - text.Text = "results"; - icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; - icon.Colour = colours.BlueLighter; - break; - } - } - + // the only case where the progress bar is used does its own local fade in. + // starting by fading out is a sane default. + progressBar.FadeOut(fade_time); this.FadeIn(fade_time); + + switch (state) + { + case MultiplayerUserState.Idle: + showBeatmapAvailability(availability); + break; + + case MultiplayerUserState.Ready: + text.Text = "ready"; + icon.Icon = FontAwesome.Solid.CheckCircle; + icon.Colour = Color4Extensions.FromHex("#AADD00"); + break; + + case MultiplayerUserState.WaitingForLoad: + text.Text = "loading"; + icon.Icon = FontAwesome.Solid.PauseCircle; + icon.Colour = colours.Yellow; + break; + + case MultiplayerUserState.Loaded: + text.Text = "loaded"; + icon.Icon = FontAwesome.Solid.DotCircle; + icon.Colour = colours.YellowLight; + break; + + case MultiplayerUserState.Playing: + text.Text = "playing"; + icon.Icon = FontAwesome.Solid.PlayCircle; + icon.Colour = colours.BlueLight; + break; + + case MultiplayerUserState.FinishedPlay: + text.Text = "results pending"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; + icon.Colour = colours.BlueLighter; + break; + + case MultiplayerUserState.Results: + text.Text = "results"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleUp; + icon.Colour = colours.BlueLighter; + break; + + default: + throw new ArgumentOutOfRangeException(nameof(state), state, null); + } + } + + private void showBeatmapAvailability(BeatmapAvailability availability) + { + switch (availability.State) + { + default: + this.FadeOut(fade_time); + break; + + case DownloadState.NotDownloaded: + text.Text = "no map"; + icon.Icon = FontAwesome.Solid.MinusCircle; + icon.Colour = colours.RedLight; + break; + + case DownloadState.Downloading: + Debug.Assert(availability.DownloadProgress != null); + + progressBar.FadeIn(fade_time); + progressBar.CurrentTime = availability.DownloadProgress.Value; + + text.Text = "downloading map"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleDown; + icon.Colour = colours.Blue; + break; + + case DownloadState.Importing: + text.Text = "importing map"; + icon.Icon = FontAwesome.Solid.ArrowAltCircleDown; + icon.Colour = colours.Yellow; + break; + } } } } From 98c4573240670b7992894d2ddaf84c8f9acdf476 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 15:52:49 +0900 Subject: [PATCH 6427/6909] Add assertions covering new test --- .../Multiplayer/TestSceneMultiplayerParticipantsList.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 4fec1c6dd8..b025440d04 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -7,6 +7,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Testing; using osu.Framework.Utils; +using osu.Game.Graphics.UserInterface; using osu.Game.Online; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -82,12 +83,20 @@ namespace osu.Game.Tests.Visual.Multiplayer { AddStep("set to no map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + + AddUntilStep("progress bar visible", () => this.ChildrenOfType().Single().IsPresent); + AddRepeatStep("increment progress", () => { var progress = this.ChildrenOfType().Single().User.BeatmapAvailability.DownloadProgress ?? 0; Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(progress + RNG.NextSingle(0.1f))); }, 25); + + AddAssert("progress bar increased", () => this.ChildrenOfType().Single().Current.Value > 0); + AddStep("set to importing map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Importing())); + AddUntilStep("progress bar not visible", () => !this.ChildrenOfType().Single().IsPresent); + AddStep("set to available", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); } From 3aa3692ed4be0a5b94e4bbb9b489063b6532b0da Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 15:56:13 +0900 Subject: [PATCH 6428/6909] Disable snaking out when tracking is disabled --- .../Sliders/SliderCircleSelectionBlueprint.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Slider.cs | 2 +- .../Skinning/Default/PlaySliderBody.cs | 22 ++++++++++++++++++- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs index a0392fe536..dec9cd8622 100644 --- a/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs +++ b/osu.Game.Rulesets.Osu/Edit/Blueprints/Sliders/SliderCircleSelectionBlueprint.cs @@ -27,7 +27,7 @@ namespace osu.Game.Rulesets.Osu.Edit.Blueprints.Sliders { base.Update(); - CirclePiece.UpdateFrom(position == SliderPosition.Start ? HitObject.HeadCircle : HitObject.TailCircle); + CirclePiece.UpdateFrom(position == SliderPosition.Start ? (HitCircle)HitObject.HeadCircle : HitObject.TailCircle); } // Todo: This is temporary, since the slider circle masks don't do anything special yet. In the future they will handle input. diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index e3365a8ccf..01694a838b 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -121,7 +121,7 @@ namespace osu.Game.Rulesets.Osu.Objects public bool IgnoreJudgement = true; [JsonIgnore] - public HitCircle HeadCircle { get; protected set; } + public SliderHeadCircle HeadCircle { get; protected set; } [JsonIgnore] public SliderTailCircle TailCircle { get; protected set; } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index e77c93c721..e9b4bb416c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -21,6 +21,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default [Resolved(CanBeNull = true)] private OsuRulesetConfigManager config { get; set; } + private readonly Bindable snakingOut = new Bindable(); + [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableObject) { @@ -35,11 +37,29 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default accentColour = drawableObject.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true); + SnakingOut.BindTo(snakingOut); config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn); - config?.BindWith(OsuRulesetSetting.SnakingOutSliders, SnakingOut); + config?.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1; BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; + + drawableObject.HitObjectApplied += onHitObjectApplied; + onHitObjectApplied(drawableObject); + } + + private void onHitObjectApplied(DrawableHitObject obj) + { + var drawableSlider = (DrawableSlider)obj; + if (drawableSlider.HitObject == null) + return; + + if (!drawableSlider.HitObject.HeadCircle.TrackFollowCircle) + { + // When not tracking the follow circle, force the path to not snake out as it looks better that way. + SnakingOut.UnbindFrom(snakingOut); + SnakingOut.Value = false; + } } private void updateAccentColour(ISkinSource skin, Color4 defaultAccentColour) From 1368d551529ac09ecb1d47c5d32c0ddbf5f313b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 15:58:27 +0900 Subject: [PATCH 6429/6909] Add test coverage of precedence of display --- .../TestSceneMultiplayerParticipantsList.cs | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index b025440d04..c3852fafd4 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -78,13 +78,27 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("single panel is for second user", () => this.ChildrenOfType().Single().User.User == secondUser); } + [Test] + public void TestGameStateHasPriorityOverDownloadState() + { + AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + checkProgressBarVisibility(true); + + AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Results)); + checkProgressBarVisibility(false); + AddUntilStep("ready mark visible", () => this.ChildrenOfType().Single().IsPresent); + + AddStep("make user ready", () => Client.ChangeState(MultiplayerUserState.Idle)); + checkProgressBarVisibility(true); + } + [Test] public void TestBeatmapDownloadingStates() { AddStep("set to no map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.NotDownloaded())); AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); - AddUntilStep("progress bar visible", () => this.ChildrenOfType().Single().IsPresent); + checkProgressBarVisibility(true); AddRepeatStep("increment progress", () => { @@ -95,7 +109,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("progress bar increased", () => this.ChildrenOfType().Single().Current.Value > 0); AddStep("set to importing map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Importing())); - AddUntilStep("progress bar not visible", () => !this.ChildrenOfType().Single().IsPresent); + checkProgressBarVisibility(false); AddStep("set to available", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.LocallyAvailable())); } @@ -197,5 +211,9 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep($"set state: {state}", () => Client.ChangeUserState(0, state)); } } + + private void checkProgressBarVisibility(bool visible) => + AddUntilStep($"progress bar {(visible ? "is" : "is not")}visible", () => + this.ChildrenOfType().Single().IsPresent == visible); } } From 9ba5ae3db7c7f235fab1d44fd75a2e2a2f0ded94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 16:17:02 +0900 Subject: [PATCH 6430/6909] Remove lots of unnecessary client side logic --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 20 ++----------------- 1 file changed, 2 insertions(+), 18 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 5bcb1d6dc8..bd1ec9c54a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -270,7 +270,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); UserMods.BindValueChanged(onUserModsChanged); - client.RoomUpdated += onRoomUpdated; client.LoadRequested += onLoadRequested; isConnected = client.IsConnected.GetBoundCopy(); @@ -323,24 +322,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.ChangeUserMods(mods.NewValue); } - private void updateBeatmapAvailability(ValueChangedEvent _ = null) + private void updateBeatmapAvailability(ValueChangedEvent availability) { if (client.Room == null) return; - client.ChangeBeatmapAvailability(BeatmapAvailability.Value); - - if (client.LocalUser?.State == MultiplayerUserState.Ready) - client.ChangeState(MultiplayerUserState.Idle); - } - - private void onRoomUpdated() - { - if (client.Room == null) - return; - - if (client.LocalUser?.BeatmapAvailability.Equals(BeatmapAvailability.Value) == false) - updateBeatmapAvailability(); + client.ChangeBeatmapAvailability(availability.NewValue); } private void onReadyClick() @@ -392,10 +379,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.Dispose(isDisposing); if (client != null) - { client.LoadRequested -= onLoadRequested; - client.RoomUpdated -= onRoomUpdated; - } } private class UserModSelectOverlay : ModSelectOverlay From be91f54349202c6a633f0de9728ede538b120c9d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 16:19:45 +0900 Subject: [PATCH 6431/6909] Add back edge case with comment --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index bd1ec9c54a..39e179262e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -328,6 +328,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; client.ChangeBeatmapAvailability(availability.NewValue); + + // while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap. + if (client.LocalUser?.State == MultiplayerUserState.Ready) + client.ChangeState(MultiplayerUserState.Idle); } private void onReadyClick() From e1789c29b1a1e01f235b8e6bb9bdcfd131e5d715 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 5 Feb 2021 10:28:13 +0300 Subject: [PATCH 6432/6909] Use `Pause()` instead of `performUserRequestedExit()` to avoid unexpected operations --- 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 556964bca4..542839f11d 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -433,7 +433,7 @@ namespace osu.Game.Screens.Play return; if (gameActive.Value == false) - performUserRequestedExit(); + Pause(); } private IBeatmap loadPlayableBeatmap() From 8d18c7e9299cc9c8e4a8b55563c3a9a22d1a99bd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 5 Feb 2021 10:28:33 +0300 Subject: [PATCH 6433/6909] Fix `BreakTracker.IsBreakTime` not updated properly on breaks set Causes a pause from focus lose when playing a beatmap that has a break section at the beginning, due to `IsBreakTime` incorrectly set to `false` --- osu.Game/Screens/Play/BreakTracker.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs index 51e21656e1..793b6b0ebe 100644 --- a/osu.Game/Screens/Play/BreakTracker.cs +++ b/osu.Game/Screens/Play/BreakTracker.cs @@ -23,16 +23,16 @@ namespace osu.Game.Screens.Play /// public IBindable IsBreakTime => isBreakTime; - private readonly BindableBool isBreakTime = new BindableBool(); + private readonly BindableBool isBreakTime = new BindableBool(true); public IReadOnlyList Breaks { set { - isBreakTime.Value = false; - breaks = new PeriodTracker(value.Where(b => b.HasEffect) .Select(b => new Period(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION))); + + updateBreakTime(); } } @@ -45,8 +45,12 @@ namespace osu.Game.Screens.Play protected override void Update() { base.Update(); + updateBreakTime(); + } - var time = Clock.CurrentTime; + private void updateBreakTime() + { + var time = Clock?.CurrentTime ?? 0; isBreakTime.Value = breaks?.IsInAny(time) == true || time < gameplayStartTime From 3e750feaa49cd9b0f7dea85965f657181b1c2eeb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 16:42:35 +0900 Subject: [PATCH 6434/6909] Subclass LocalPlayerModSelectOverlay to correctly deselect incompatible mods on free mod selection --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 2 +- osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs | 2 +- .../{SoloModSelectOverlay.cs => LocalPlayerModSelectOverlay.cs} | 2 +- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 2 +- osu.Game/Screens/Select/SongSelect.cs | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) rename osu.Game/Overlays/Mods/{SoloModSelectOverlay.cs => LocalPlayerModSelectOverlay.cs} (88%) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 44605f4994..37ebc72984 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -333,7 +333,7 @@ namespace osu.Game.Tests.Visual.UserInterface }; } - private class TestModSelectOverlay : SoloModSelectOverlay + private class TestModSelectOverlay : LocalPlayerModSelectOverlay { public new Bindable> SelectedMods => base.SelectedMods; diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs index 3c889bdec4..89f9b7381b 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSettings.cs @@ -151,7 +151,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddUntilStep("wait for ready", () => modSelect.State.Value == Visibility.Visible && modSelect.ButtonsLoaded); } - private class TestModSelectOverlay : SoloModSelectOverlay + private class TestModSelectOverlay : LocalPlayerModSelectOverlay { public new VisibilityContainer ModSettingsContainer => base.ModSettingsContainer; public new TriangleButton CustomiseButton => base.CustomiseButton; diff --git a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs b/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs similarity index 88% rename from osu.Game/Overlays/Mods/SoloModSelectOverlay.cs rename to osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs index d039ad1f98..78cd9bdae5 100644 --- a/osu.Game/Overlays/Mods/SoloModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/LocalPlayerModSelectOverlay.cs @@ -5,7 +5,7 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Overlays.Mods { - public class SoloModSelectOverlay : ModSelectOverlay + public class LocalPlayerModSelectOverlay : ModSelectOverlay { protected override void OnModSelected(Mod mod) { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 061e3b4d3f..f030879625 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -373,7 +373,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.LoadRequested -= onLoadRequested; } - private class UserModSelectOverlay : ModSelectOverlay + private class UserModSelectOverlay : LocalPlayerModSelectOverlay { public UserModSelectOverlay() { diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index 0baa663578..b201c62b7f 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -125,7 +125,7 @@ namespace osu.Game.Screens.OnlinePlay return base.OnExiting(next); } - protected override ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay + protected override ModSelectOverlay CreateModSelectOverlay() => new LocalPlayerModSelectOverlay { IsValidMod = IsValidMod }; diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index ed6e0a1028..b20effc67d 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -311,7 +311,7 @@ namespace osu.Game.Screens.Select (new FooterButtonOptions(), BeatmapOptions) }; - protected virtual ModSelectOverlay CreateModSelectOverlay() => new SoloModSelectOverlay(); + protected virtual ModSelectOverlay CreateModSelectOverlay() => new LocalPlayerModSelectOverlay(); protected virtual void ApplyFilterToCarousel(FilterCriteria criteria) { From 630c5bb74700ae3599013eddf70e2a1cda4a3642 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 16:46:21 +0900 Subject: [PATCH 6435/6909] Avoid potential crashes when lease is held on SelectedMods --- osu.Game/OsuGame.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index a00cd5e6a0..1a1f7bd233 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -469,6 +469,10 @@ namespace osu.Game { updateModDefaults(); + // a lease may be taken on the mods bindable, at which point we can't really ensure valid mods. + if (SelectedMods.Disabled) + return; + if (!ModUtils.CheckValidForGameplay(mods.NewValue, out var invalid)) { // ensure we always have a valid set of mods. From ee3367d7c549acecb778c652365c290e3e186e5b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 16:59:13 +0900 Subject: [PATCH 6436/6909] Add classic slider ball tracking --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 17 ++++++++++++++++- .../Skinning/Default/SliderBall.cs | 15 ++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 6f41bcc0b0..df3afb7063 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -1,19 +1,22 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Graphics.Sprites; using osu.Game.Configuration; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Objects.Drawables; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.Mods { - public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableRuleset + public class OsuModClassic : Mod, IApplicableToHitObject, IApplicableToDrawableHitObjects, IApplicableToDrawableRuleset { public override string Name => "Classic"; @@ -36,6 +39,9 @@ namespace osu.Game.Rulesets.Osu.Mods [SettingSource("Disable note lock lenience", "Applies note lock to the full hit window.")] public Bindable DisableLenientNoteLock { get; } = new BindableBool(true); + [SettingSource("Disable exact slider follow circle tracking", "Makes the slider follow circle track its final size at all times.")] + public Bindable DisableExactFollowCircleTracking { get; } = new BindableBool(true); + public void ApplyToHitObject(HitObject hitObject) { switch (hitObject) @@ -60,5 +66,14 @@ namespace osu.Game.Rulesets.Osu.Mods if (!DisableLenientNoteLock.Value) osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); } + + public void ApplyToDrawableHitObjects(IEnumerable drawables) + { + foreach (var obj in drawables) + { + if (obj is DrawableSlider slider) + slider.Ball.TrackVisualSize = !DisableExactFollowCircleTracking.Value; + } + } } } diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs index a96beb66d4..da3debbd42 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs @@ -31,6 +31,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default set => ball.Colour = value; } + /// + /// Whether to track accurately to the visual size of this . + /// If false, tracking will be performed at the final scale at all times. + /// + public bool TrackVisualSize = true; + private readonly Drawable followCircle; private readonly DrawableSlider drawableSlider; private readonly Drawable ball; @@ -94,7 +100,14 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default tracking = value; - followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint); + if (TrackVisualSize) + followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint); + else + { + // We need to always be tracking the final size, at both endpoints. For now, this is achieved by removing the scale duration. + followCircle.ScaleTo(tracking ? 2.4f : 1f); + } + followCircle.FadeTo(tracking ? 1f : 0, 300, Easing.OutQuint); } } From 791cbb7f03e9637a123b83b77f339c3d44abbf1f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 17:17:29 +0900 Subject: [PATCH 6437/6909] Don't reset ready state if the map is locally available --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 39e179262e..53c939115c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -330,7 +330,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.ChangeBeatmapAvailability(availability.NewValue); // while this flow is handled server-side, this covers the edge case of the local user being in a ready state and then deleting the current beatmap. - if (client.LocalUser?.State == MultiplayerUserState.Ready) + if (availability.NewValue != Online.Rooms.BeatmapAvailability.LocallyAvailable() + && client.LocalUser?.State == MultiplayerUserState.Ready) client.ChangeState(MultiplayerUserState.Idle); } From 110458612d730e449174fe533606faecc041785b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 17:19:23 +0900 Subject: [PATCH 6438/6909] Avoid handling null playlist items when updating avaialability display --- .../Rooms/OnlinePlayBeatmapAvailablilityTracker.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs index ad4b3c5151..dcb366ddab 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs @@ -36,7 +36,15 @@ namespace osu.Game.Online.Rooms { base.LoadComplete(); - SelectedItem.BindValueChanged(item => Model.Value = item.NewValue?.Beatmap.Value.BeatmapSet, true); + SelectedItem.BindValueChanged(item => + { + // the underlying playlist is regularly cleared for maintenance purposes (things which probably need to be fixed eventually). + // to avoid exposing a state change when there may actually be none, ignore all nulls for now. + if (item.NewValue == null) + return; + + Model.Value = item.NewValue.Beatmap.Value.BeatmapSet; + }, true); } protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet) From a5855f5d28f8e7db6b9d4038806e5d70b38c3a7f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 17:33:48 +0900 Subject: [PATCH 6439/6909] Move follow circle tracking to DrawableSliderHead --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 15 ++++++++++----- .../Objects/Drawables/DrawableSliderHead.cs | 8 +++++++- osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs | 6 ------ .../Skinning/Default/PlaySliderBody.cs | 2 +- 4 files changed, 18 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index df3afb7063..8cd6676f9d 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -50,10 +50,7 @@ namespace osu.Game.Rulesets.Osu.Mods slider.IgnoreJudgement = !DisableSliderHeadJudgement.Value; foreach (var head in slider.NestedHitObjects.OfType()) - { - head.TrackFollowCircle = !DisableSliderHeadTracking.Value; head.JudgeAsNormalHitCircle = !DisableSliderHeadJudgement.Value; - } break; } @@ -71,8 +68,16 @@ namespace osu.Game.Rulesets.Osu.Mods { foreach (var obj in drawables) { - if (obj is DrawableSlider slider) - slider.Ball.TrackVisualSize = !DisableExactFollowCircleTracking.Value; + switch (obj) + { + case DrawableSlider slider: + slider.Ball.TrackVisualSize = !DisableExactFollowCircleTracking.Value; + break; + + case DrawableSliderHead head: + head.TrackFollowCircle = !DisableSliderHeadTracking.Value; + break; + } } } } diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 08e9c5eb14..ee1df00ef7 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -22,6 +22,12 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables public override bool DisplayResult => HitObject?.JudgeAsNormalHitCircle ?? base.DisplayResult; + /// + /// Makes this track the follow circle when the start time is reached. + /// If false, this will be pinned to its initial position in the slider. + /// + public bool TrackFollowCircle = true; + private readonly IBindable pathVersion = new Bindable(); protected override OsuSkinComponents CirclePieceComponent => OsuSkinComponents.SliderHeadHitCircle; @@ -66,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables Debug.Assert(Slider != null); Debug.Assert(HitObject != null); - if (HitObject.TrackFollowCircle) + if (TrackFollowCircle) { double completionProgress = Math.Clamp((Time.Current - Slider.StartTime) / Slider.Duration, 0, 1); diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 13eac60300..28e57567cb 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -8,12 +8,6 @@ namespace osu.Game.Rulesets.Osu.Objects { public class SliderHeadCircle : HitCircle { - /// - /// Makes this track the follow circle when the start time is reached. - /// If false, this will be pinned to its initial position in the slider. - /// - public bool TrackFollowCircle = true; - /// /// Whether to treat this as a normal for judgement purposes. /// If false, judgement will be ignored. diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index e9b4bb416c..8eb2714c04 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -54,7 +54,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (drawableSlider.HitObject == null) return; - if (!drawableSlider.HitObject.HeadCircle.TrackFollowCircle) + if (!drawableSlider.HeadCircle.TrackFollowCircle) { // When not tracking the follow circle, force the path to not snake out as it looks better that way. SnakingOut.UnbindFrom(snakingOut); From dad32da4153e8cb18443c6c1f12c1a1847754936 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 17:34:05 +0900 Subject: [PATCH 6440/6909] Add rate limiting on sending download progress updates --- .../Rooms/OnlinePlayBeatmapAvailablilityTracker.cs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs index dcb366ddab..cfaf43451f 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using osu.Framework.Bindables; using osu.Framework.Logging; +using osu.Framework.Threading; using osu.Game.Beatmaps; namespace osu.Game.Online.Rooms @@ -24,12 +25,20 @@ namespace osu.Game.Online.Rooms /// public IBindable Availability => availability; - private readonly Bindable availability = new Bindable(); + private readonly Bindable availability = new Bindable(BeatmapAvailability.LocallyAvailable()); + + private ScheduledDelegate progressUpdate; public OnlinePlayBeatmapAvailablilityTracker() { State.BindValueChanged(_ => updateAvailability()); - Progress.BindValueChanged(_ => updateAvailability(), true); + Progress.BindValueChanged(_ => + { + // incoming progress changes are going to be at a very high rate. + // we don't want to flood the network with this, so rate limit how often we send progress updates. + if (progressUpdate?.Completed != false) + progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); + }); } protected override void LoadComplete() From 95ad7ea8f7667a5e3faacbcbe696ef09b5b38354 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 5 Feb 2021 18:44:26 +0900 Subject: [PATCH 6441/6909] Fix mods on participant panels flashing when changed --- .../Multiplayer/Participants/ParticipantPanel.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 2983d1268d..0ee1b6d684 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -162,15 +162,20 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; - var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance(); - userStateDisplay.Status = User.State; - userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList(); if (Room.Host?.Equals(User) == true) crown.FadeIn(fade_time); else crown.FadeOut(fade_time); + + // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 + // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. + Schedule(() => + { + var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance(); + userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList(); + }); } public MenuItem[] ContextMenuItems From 0679901e4d3048552b9e89ace1896958189c6d4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 22:53:40 +0900 Subject: [PATCH 6442/6909] Update error handling --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index dc98eb8687..33200ca076 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -66,8 +66,8 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { failed = true; - if (e is WebException || string.IsNullOrEmpty(e.Message)) - Logger.Error(e, "Failed to retrieve a score submission token.\n\nThis may happen if you are running an old or non-official release of osu! (ie. you are self-compiling)."); + if (string.IsNullOrEmpty(e.Message)) + Logger.Error(e, "Failed to retrieve a score submission token."); else Logger.Log($"You are not able to submit a score: {e.Message}", level: LogLevel.Important); From 7f82a06a61284b89a99a5b95cbae09cf428fa159 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 5 Feb 2021 23:08:31 +0900 Subject: [PATCH 6443/6909] Remove no longer used using directive --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 33200ca076..38eae2346a 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -4,7 +4,6 @@ using System; using System.Diagnostics; using System.Linq; -using System.Net; using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; From 5061231e599a2d04a40a2526872383987c9acddb Mon Sep 17 00:00:00 2001 From: vmaggioli Date: Fri, 5 Feb 2021 09:39:14 -0500 Subject: [PATCH 6444/6909] Switch to beat length --- .../Compose/Components/Timeline/TimelineHitObjectBlueprint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs index 301543b3c1..d24614299c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs +++ b/osu.Game/Screens/Edit/Compose/Components/Timeline/TimelineHitObjectBlueprint.cs @@ -388,7 +388,7 @@ namespace osu.Game.Screens.Edit.Compose.Components.Timeline case IHasDuration endTimeHitObject: var snappedTime = Math.Max(hitObject.StartTime, beatSnapProvider.SnapTime(time)); - if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, 1)) + if (endTimeHitObject.EndTime == snappedTime || Precision.AlmostEquals(snappedTime, hitObject.StartTime, beatmap.GetBeatLengthAtTime(snappedTime))) return; endTimeHitObject.Duration = snappedTime - hitObject.StartTime; From f29938e15d62bad02c0ff8d0f586bc2764f423a9 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 5 Feb 2021 20:39:57 +0300 Subject: [PATCH 6445/6909] Make last binding game activity more sensible --- osu.Game/Screens/Play/Player.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 542839f11d..f38eba3f27 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -80,6 +80,9 @@ namespace osu.Game.Screens.Play public int RestartCount; + [Resolved] + private OsuGameBase gameBase { get; set; } + [Resolved] private ScoreManager scoreManager { get; set; } @@ -157,7 +160,8 @@ namespace osu.Game.Screens.Play if (!Mods.Value.Any(m => m is ModAutoplay)) PrepareReplay(); - // needs to be bound here as the last binding, otherwise starting a replay while not focused causes player to exit. + // needs to be bound here as the last binding, otherwise cases like starting a replay while not focused causes player to exit, if activity is bound before checks. + gameActive.BindTo(gameBase.IsActive); gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); } @@ -191,8 +195,6 @@ namespace osu.Game.Screens.Play mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); - gameActive.BindTo(gameBase.IsActive); - if (game != null) LocalUserPlaying.BindTo(game.LocalUserPlaying); @@ -429,7 +431,7 @@ namespace osu.Game.Screens.Play private void updatePauseOnFocusLostState() { - if (!IsLoaded || !PauseOnFocusLost || DrawableRuleset.HasReplayLoaded.Value || breakTracker.IsBreakTime.Value) + if (!PauseOnFocusLost || DrawableRuleset.HasReplayLoaded.Value || breakTracker.IsBreakTime.Value) return; if (gameActive.Value == false) From f6d08f54e6e950f4784087012fe27cd3e566a22c Mon Sep 17 00:00:00 2001 From: Lucas A Date: Fri, 5 Feb 2021 21:19:13 +0100 Subject: [PATCH 6446/6909] Use the oldest user config file available when there happens to be multiple config files available. --- osu.Game/IO/StableStorage.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index f86b18c724..614e548d93 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -35,7 +35,11 @@ namespace osu.Game.IO { var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); - var configFile = GetFiles(".", "osu!.*.cfg").SingleOrDefault(); + // enumerate the user config files available in case the user migrated their files from another pc / operating system. + var foundConfigFiles = GetFiles(".", "osu!.*.cfg"); + + // if more than one config file is found, let's use the oldest one (where the username in the filename doesn't match the local username). + var configFile = foundConfigFiles.Count() > 1 ? foundConfigFiles.FirstOrDefault(filename => !filename[5..^4].Contains(Environment.UserName, StringComparison.Ordinal)) : foundConfigFiles.FirstOrDefault(); if (configFile == null) return songsDirectoryPath; From c9db0bf88651affc9bdd3a165984ad7577770149 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 6 Feb 2021 20:54:13 +0300 Subject: [PATCH 6447/6909] Call break time update when loaded --- osu.Game/Screens/Play/BreakTracker.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/BreakTracker.cs b/osu.Game/Screens/Play/BreakTracker.cs index 793b6b0ebe..2f3673e91f 100644 --- a/osu.Game/Screens/Play/BreakTracker.cs +++ b/osu.Game/Screens/Play/BreakTracker.cs @@ -32,7 +32,8 @@ namespace osu.Game.Screens.Play breaks = new PeriodTracker(value.Where(b => b.HasEffect) .Select(b => new Period(b.StartTime, b.EndTime - BreakOverlay.BREAK_FADE_DURATION))); - updateBreakTime(); + if (IsLoaded) + updateBreakTime(); } } @@ -50,7 +51,7 @@ namespace osu.Game.Screens.Play private void updateBreakTime() { - var time = Clock?.CurrentTime ?? 0; + var time = Clock.CurrentTime; isBreakTime.Value = breaks?.IsInAny(time) == true || time < gameplayStartTime From 40ddccf0c73904af580d3023b3d79d45a14868f3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 6 Feb 2021 20:55:55 +0300 Subject: [PATCH 6448/6909] Do not consider replays for "pause on focus lost" Replays are not pausable as can be seen in the `canPause` check. --- osu.Game/Screens/Play/Player.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index f38eba3f27..81401b08e8 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -160,7 +160,6 @@ namespace osu.Game.Screens.Play if (!Mods.Value.Any(m => m is ModAutoplay)) PrepareReplay(); - // needs to be bound here as the last binding, otherwise cases like starting a replay while not focused causes player to exit, if activity is bound before checks. gameActive.BindTo(gameBase.IsActive); gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); } @@ -267,8 +266,6 @@ namespace osu.Game.Screens.Play DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updateGameplayState()); - DrawableRuleset.HasReplayLoaded.BindValueChanged(_ => updatePauseOnFocusLostState(), true); - // bind clock into components that require it DrawableRuleset.IsPaused.BindTo(GameplayClockContainer.IsPaused); @@ -431,7 +428,7 @@ namespace osu.Game.Screens.Play private void updatePauseOnFocusLostState() { - if (!PauseOnFocusLost || DrawableRuleset.HasReplayLoaded.Value || breakTracker.IsBreakTime.Value) + if (!PauseOnFocusLost || breakTracker.IsBreakTime.Value) return; if (gameActive.Value == false) From d0ca2b99a850f9903eec2f7ac1956e15a73089a2 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 6 Feb 2021 20:57:01 +0300 Subject: [PATCH 6449/6909] Remove unnecessary injected dependency --- 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 81401b08e8..bd67d3f06a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -178,7 +178,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, OsuConfigManager config, OsuGame game, OsuGameBase gameBase) + private void load(AudioManager audio, OsuConfigManager config, OsuGame game) { Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray(); From 68c20a2a3705dcc0995303e83a4312164d9fd98d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 17:19:07 +0100 Subject: [PATCH 6450/6909] Allow autoplay score generation to access mod list --- osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs | 3 ++- osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs | 3 ++- osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs | 3 ++- .../TestSceneMissHitWindowJudgements.cs | 4 +++- osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs | 3 ++- osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs | 3 ++- osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs | 3 ++- osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs | 2 +- osu.Game/Rulesets/Mods/ModAutoplay.cs | 12 +++++++++++- osu.Game/Rulesets/Mods/ModCinema.cs | 4 +++- 12 files changed, 34 insertions(+), 12 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs index 692e63fa69..e1eceea606 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModAutoplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } }, Replay = new CatchAutoGenerator(beatmap).Generate(), diff --git a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs index 3bc1ee5bf5..d53d019e90 100644 --- a/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs +++ b/osu.Game.Rulesets.Catch/Mods/CatchModCinema.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Objects; using osu.Game.Rulesets.Catch.Replays; @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Catch.Mods { public class CatchModCinema : ModCinema { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "osu!salad!" } }, Replay = new CatchAutoGenerator(beatmap).Generate(), diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs index c05e979e9a..105d88129c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModAutoplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } }, Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs index 02c1fc1b79..064c55ed8d 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModCinema.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.Beatmaps; using osu.Game.Rulesets.Mania.Objects; @@ -13,7 +14,7 @@ namespace osu.Game.Rulesets.Mania.Mods { public class ManiaModCinema : ModCinema { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "osu!topus!" } }, Replay = new ManiaAutoGenerator((ManiaBeatmap)beatmap).Generate(), diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index 39deba2f57..f73649fcd9 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.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.Collections.Generic; using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Replays; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Osu.Objects; @@ -65,7 +67,7 @@ namespace osu.Game.Rulesets.Osu.Tests private class TestAutoMod : OsuModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, Replay = new MissingAutoGenerator(beatmap).Generate() diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index bea2bbcb32..454c94cd96 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, Replay = new OsuAutoGenerator(beatmap).Generate() diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs index 5d9a524577..99e5568bb3 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.Linq; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; @@ -16,7 +17,7 @@ namespace osu.Game.Rulesets.Osu.Mods { public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(OsuModAutopilot)).Append(typeof(OsuModSpunOut)).ToArray(); - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, Replay = new OsuAutoGenerator(beatmap).Generate() diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs index 5b890b3d03..64e59b64d0 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModAutoplay.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModAutoplay : ModAutoplay { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } }, Replay = new TaikoAutoGenerator(beatmap).Generate(), diff --git a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs index 71aa007d3b..00f0c8e321 100644 --- a/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs +++ b/osu.Game.Rulesets.Taiko/Mods/TaikoModCinema.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Objects; @@ -12,7 +13,7 @@ namespace osu.Game.Rulesets.Taiko.Mods { public class TaikoModCinema : ModCinema { - public override Score CreateReplayScore(IBeatmap beatmap) => new Score + public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "mekkadosu!" } }, Replay = new TaikoAutoGenerator(beatmap).Generate(), diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneReplay.cs index 3a71d4ca54..f94e122b30 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, Array.Empty())); } protected override void AddCheckSteps() diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 945dd444be..748c7272f4 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Beatmaps; using osu.Game.Graphics; @@ -15,7 +16,11 @@ namespace osu.Game.Rulesets.Mods public abstract class ModAutoplay : ModAutoplay, IApplicableToDrawableRuleset where T : HitObject { - public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) => drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap)); + public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); + drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, mods)); + } } public abstract class ModAutoplay : Mod, IApplicableFailOverride @@ -35,6 +40,11 @@ namespace osu.Game.Rulesets.Mods public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; + [Obsolete("Use the mod-supporting override")] // can be removed 20210731 public virtual Score CreateReplayScore(IBeatmap beatmap) => new Score { Replay = new Replay() }; + +#pragma warning disable 618 + public virtual Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => CreateReplayScore(beatmap); +#pragma warning restore 618 } } diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index bee9e56edd..16e6400f23 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; @@ -14,7 +15,8 @@ namespace osu.Game.Rulesets.Mods { public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap)); + var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); + drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, mods)); // AlwaysPresent required for hitsounds drawableRuleset.Playfield.AlwaysPresent = true; From 7daeacaff230b58a13847db3727918cf7da38d02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 17:43:16 +0100 Subject: [PATCH 6451/6909] Add and implement IApplicableToRate interface --- osu.Game/Rulesets/Mods/IApplicableToRate.cs | 20 ++++++++++++++++++++ osu.Game/Rulesets/Mods/ModRateAdjust.cs | 4 +++- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 19 ++++++++++++------- 3 files changed, 35 insertions(+), 8 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/IApplicableToRate.cs diff --git a/osu.Game/Rulesets/Mods/IApplicableToRate.cs b/osu.Game/Rulesets/Mods/IApplicableToRate.cs new file mode 100644 index 0000000000..f613867132 --- /dev/null +++ b/osu.Game/Rulesets/Mods/IApplicableToRate.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.Mods +{ + /// + /// Interface that should be implemented by mods that affect the track playback speed, + /// and in turn, values of the track rate. + /// + public interface IApplicableToRate : IApplicableToAudio + { + /// + /// Returns the playback rate at after this mod is applied. + /// + /// The time instant at which the playback rate is queried. + /// The playback rate before applying this mod. + /// The playback rate after applying this mod. + double ApplyToRate(double time, double rate = 1); + } +} diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index 2150b0fb68..b016a6d43b 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.cs @@ -8,7 +8,7 @@ using osu.Framework.Graphics.Audio; namespace osu.Game.Rulesets.Mods { - public abstract class ModRateAdjust : Mod, IApplicableToAudio + public abstract class ModRateAdjust : Mod, IApplicableToRate { public abstract BindableNumber SpeedChange { get; } @@ -22,6 +22,8 @@ namespace osu.Game.Rulesets.Mods sample.AddAdjustment(AdjustableProperty.Frequency, SpeedChange); } + public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value; + 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 b6916c838e..7e801c3024 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -14,7 +14,7 @@ using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mods { - public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToAudio + public abstract class ModTimeRamp : Mod, IUpdatableByPlayfield, IApplicableToBeatmap, IApplicableToRate { /// /// The point in the beatmap at which the final ramping rate should be reached. @@ -47,7 +47,7 @@ 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 => applyRateAdjustment(1), true); + FinalRate.BindValueChanged(val => applyRateAdjustment(double.PositiveInfinity), true); AdjustPitch.BindValueChanged(applyPitchAdjustment); } @@ -75,17 +75,22 @@ namespace osu.Game.Rulesets.Mods finalRateTime = firstObjectStart + FINAL_RATE_PROGRESS * (lastObjectEnd - firstObjectStart); } + public double ApplyToRate(double time, double rate = 1) + { + double amount = (time - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime); + double ramp = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1); + return rate * ramp; + } + public virtual void Update(Playfield playfield) { - applyRateAdjustment((track.CurrentTime - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime)); + applyRateAdjustment(track.CurrentTime); } /// - /// Adjust the rate along the specified ramp + /// Adjust the rate along the specified ramp. /// - /// The amount of adjustment to apply (from 0..1). - private void applyRateAdjustment(double amount) => - SpeedChange.Value = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1); + private void applyRateAdjustment(double time) => SpeedChange.Value = ApplyToRate(time); private void applyPitchAdjustment(ValueChangedEvent adjustPitchSetting) { From 3fabe247b09a32ccf1fb6a652356875ce1c63b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 17:59:35 +0100 Subject: [PATCH 6452/6909] Allow OsuModGenerator to accept a mod list --- .../TestSceneMissHitWindowJudgements.cs | 6 +++--- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 3 ++- osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs | 2 +- osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs | 2 +- osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs | 6 ++++-- osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs | 3 ++- 6 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs index f73649fcd9..af67ab5839 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneMissHitWindowJudgements.cs @@ -70,7 +70,7 @@ namespace osu.Game.Rulesets.Osu.Tests public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, - Replay = new MissingAutoGenerator(beatmap).Generate() + Replay = new MissingAutoGenerator(beatmap, mods).Generate() }; } @@ -78,8 +78,8 @@ namespace osu.Game.Rulesets.Osu.Tests { public new OsuBeatmap Beatmap => (OsuBeatmap)base.Beatmap; - public MissingAutoGenerator(IBeatmap beatmap) - : base(beatmap) + public MissingAutoGenerator(IBeatmap beatmap, IReadOnlyList mods) + : base(beatmap, mods) { } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 8c819c4773..59a5295858 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -62,7 +62,8 @@ namespace osu.Game.Rulesets.Osu.Mods inputManager.AllowUserCursorMovement = false; // Generate the replay frames the cursor should follow - replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap).Generate().Frames.Cast().ToList(); + var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); + replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, mods).Generate().Frames.Cast().ToList(); } } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs index 454c94cd96..3b1f271d41 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutoplay.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, - Replay = new OsuAutoGenerator(beatmap).Generate() + Replay = new OsuAutoGenerator(beatmap, mods).Generate() }; } } diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs index 99e5568bb3..df06988b70 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModCinema.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override Score CreateReplayScore(IBeatmap beatmap, IReadOnlyList mods) => new Score { ScoreInfo = new ScoreInfo { User = new User { Username = "Autoplay" } }, - Replay = new OsuAutoGenerator(beatmap).Generate() + Replay = new OsuAutoGenerator(beatmap, mods).Generate() }; } } diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index 954a217473..e4b6f6425d 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -6,10 +6,12 @@ using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Rulesets.Osu.Objects; using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using osu.Framework.Graphics; using osu.Game.Replays; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Beatmaps; using osu.Game.Rulesets.Osu.Scoring; @@ -49,8 +51,8 @@ namespace osu.Game.Rulesets.Osu.Replays #region Construction / Initialisation - public OsuAutoGenerator(IBeatmap beatmap) - : base(beatmap) + public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList mods) + : base(beatmap, mods) { // Already superhuman, but still somewhat realistic reactionTime = ApplyModsToRate(100); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index 3356a0fbe0..f88594a3ee 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -6,6 +6,7 @@ using osu.Game.Beatmaps; using System; using System.Collections.Generic; using osu.Game.Replays; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.UI; using osu.Game.Rulesets.Replays; @@ -34,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Replays protected Replay Replay; protected List Frames => Replay.Frames; - protected OsuAutoGeneratorBase(IBeatmap beatmap) + protected OsuAutoGeneratorBase(IBeatmap beatmap, IReadOnlyList mods) : base(beatmap) { Replay = new Replay(); From 0e1ec703d33907268dbb2acc49f3e8e19318ae2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 31 Jan 2021 17:56:03 +0100 Subject: [PATCH 6453/6909] Use IApplicableToRate in osu! auto generator --- .../Replays/OsuAutoGenerator.cs | 42 ++++++++++------- .../Replays/OsuAutoGeneratorBase.cs | 47 +++++++++++++++---- 2 files changed, 62 insertions(+), 27 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs index e4b6f6425d..693943a08a 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGenerator.cs @@ -35,11 +35,6 @@ namespace osu.Game.Rulesets.Osu.Replays #region Constants - /// - /// The "reaction time" in ms between "seeing" a new hit object and moving to "react" to it. - /// - private readonly double reactionTime; - private readonly HitWindows defaultHitWindows; /// @@ -54,9 +49,6 @@ namespace osu.Game.Rulesets.Osu.Replays public OsuAutoGenerator(IBeatmap beatmap, IReadOnlyList mods) : base(beatmap, mods) { - // Already superhuman, but still somewhat realistic - reactionTime = ApplyModsToRate(100); - defaultHitWindows = new OsuHitWindows(); defaultHitWindows.SetDifficulty(Beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); } @@ -242,7 +234,7 @@ namespace osu.Game.Rulesets.Osu.Replays OsuReplayFrame lastFrame = (OsuReplayFrame)Frames[^1]; // Wait until Auto could "see and react" to the next note. - double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - reactionTime); + double waitTime = h.StartTime - Math.Max(0.0, h.TimePreempt - getReactionTime(h.StartTime - h.TimePreempt)); if (waitTime > lastFrame.Time) { @@ -252,7 +244,7 @@ namespace osu.Game.Rulesets.Osu.Replays Vector2 lastPosition = lastFrame.Position; - double timeDifference = ApplyModsToTime(h.StartTime - lastFrame.Time); + double timeDifference = ApplyModsToTimeDelta(lastFrame.Time, h.StartTime); // Only "snap" to hitcircles if they are far enough apart. As the time between hitcircles gets shorter the snapping threshold goes up. if (timeDifference > 0 && // Sanity checks @@ -260,7 +252,7 @@ namespace osu.Game.Rulesets.Osu.Replays timeDifference >= 266)) // ... or the beats are slow enough to tap anyway. { // Perform eased movement - for (double time = lastFrame.Time + FrameDelay; time < h.StartTime; time += FrameDelay) + for (double time = lastFrame.Time + GetFrameDelay(lastFrame.Time); time < h.StartTime; time += GetFrameDelay(time)) { Vector2 currentPosition = Interpolation.ValueAt(time, lastPosition, targetPos, lastFrame.Time, h.StartTime, easing); AddFrameToReplay(new OsuReplayFrame((int)time, new Vector2(currentPosition.X, currentPosition.Y)) { Actions = lastFrame.Actions }); @@ -274,6 +266,14 @@ namespace osu.Game.Rulesets.Osu.Replays } } + /// + /// Calculates the "reaction time" in ms between "seeing" a new hit object and moving to "react" to it. + /// + /// + /// Already superhuman, but still somewhat realistic. + /// + private double getReactionTime(double timeInstant) => ApplyModsToRate(timeInstant, 100); + // Add frames to click the hitobject private void addHitObjectClickFrames(OsuHitObject h, Vector2 startPosition, float spinnerDirection) { @@ -343,17 +343,23 @@ namespace osu.Game.Rulesets.Osu.Replays float angle = radius == 0 ? 0 : MathF.Atan2(difference.Y, difference.X); double t; + double previousFrame = h.StartTime; - for (double j = h.StartTime + FrameDelay; j < spinner.EndTime; j += FrameDelay) + for (double nextFrame = h.StartTime + GetFrameDelay(h.StartTime); nextFrame < spinner.EndTime; nextFrame += GetFrameDelay(nextFrame)) { - t = ApplyModsToTime(j - h.StartTime) * spinnerDirection; + t = ApplyModsToTimeDelta(previousFrame, nextFrame) * spinnerDirection; + angle += (float)t / 20; - Vector2 pos = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); - AddFrameToReplay(new OsuReplayFrame((int)j, new Vector2(pos.X, pos.Y), action)); + Vector2 pos = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); + AddFrameToReplay(new OsuReplayFrame((int)nextFrame, new Vector2(pos.X, pos.Y), action)); + + previousFrame = nextFrame; } - t = ApplyModsToTime(spinner.EndTime - h.StartTime) * spinnerDirection; - Vector2 endPosition = SPINNER_CENTRE + CirclePosition(t / 20 + angle, SPIN_RADIUS); + t = ApplyModsToTimeDelta(previousFrame, spinner.EndTime) * spinnerDirection; + angle += (float)t / 20; + + Vector2 endPosition = SPINNER_CENTRE + CirclePosition(angle, SPIN_RADIUS); AddFrameToReplay(new OsuReplayFrame(spinner.EndTime, new Vector2(endPosition.X, endPosition.Y), action)); @@ -361,7 +367,7 @@ namespace osu.Game.Rulesets.Osu.Replays break; case Slider slider: - for (double j = FrameDelay; j < slider.Duration; j += FrameDelay) + for (double j = GetFrameDelay(slider.StartTime); j < slider.Duration; j += GetFrameDelay(slider.StartTime + j)) { Vector2 pos = slider.StackedPositionAt(j / slider.Duration); AddFrameToReplay(new OsuReplayFrame(h.StartTime + j, new Vector2(pos.X, pos.Y), action)); diff --git a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs index f88594a3ee..1cb3208c30 100644 --- a/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs +++ b/osu.Game.Rulesets.Osu/Replays/OsuAutoGeneratorBase.cs @@ -5,6 +5,7 @@ using osuTK; using osu.Game.Beatmaps; using System; using System.Collections.Generic; +using System.Linq; using osu.Game.Replays; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.UI; @@ -23,33 +24,61 @@ namespace osu.Game.Rulesets.Osu.Replays public const float SPIN_RADIUS = 50; - /// - /// The time in ms between each ReplayFrame. - /// - protected readonly double FrameDelay; - #endregion #region Construction / Initialisation protected Replay Replay; protected List Frames => Replay.Frames; + private readonly IReadOnlyList timeAffectingMods; protected OsuAutoGeneratorBase(IBeatmap beatmap, IReadOnlyList mods) : base(beatmap) { Replay = new Replay(); - // We are using ApplyModsToRate and not ApplyModsToTime to counteract the speed up / slow down from HalfTime / DoubleTime so that we remain at a constant framerate of 60 fps. - FrameDelay = ApplyModsToRate(1000.0 / 60.0); + timeAffectingMods = mods.OfType().ToList(); } #endregion #region Utilities - protected double ApplyModsToTime(double v) => v; - protected double ApplyModsToRate(double v) => v; + /// + /// Returns the real duration of time between and + /// after applying rate-affecting mods. + /// + /// + /// This method should only be used when and are very close. + /// That is because the track rate might be changing with time, + /// and the method used here is a rough instantaneous approximation. + /// + /// The start time of the time delta, in original track time. + /// The end time of the time delta, in original track time. + protected double ApplyModsToTimeDelta(double startTime, double endTime) + { + double delta = endTime - startTime; + + foreach (var mod in timeAffectingMods) + delta /= mod.ApplyToRate(startTime); + + return delta; + } + + protected double ApplyModsToRate(double time, double rate) + { + foreach (var mod in timeAffectingMods) + rate = mod.ApplyToRate(time, rate); + return rate; + } + + /// + /// Calculates the interval after which the next should be generated, + /// in milliseconds. + /// + /// The time of the previous frame. + protected double GetFrameDelay(double time) + => ApplyModsToRate(time, 1000.0 / 60); private class ReplayFrameComparer : IComparer { From 0229851c9ca93570e68dbe056cc15d20099b1cea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 7 Feb 2021 19:02:09 +0100 Subject: [PATCH 6454/6909] Apply rounding to ModTimeRamp to improve SPM consistency --- osu.Game/Rulesets/Mods/ModTimeRamp.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Rulesets/Mods/ModTimeRamp.cs b/osu.Game/Rulesets/Mods/ModTimeRamp.cs index 7e801c3024..330945d3d3 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -79,7 +79,9 @@ namespace osu.Game.Rulesets.Mods { double amount = (time - beginRampTime) / Math.Max(1, finalRateTime - beginRampTime); double ramp = InitialRate.Value + (FinalRate.Value - InitialRate.Value) * Math.Clamp(amount, 0, 1); - return rate * ramp; + + // round the end result to match the bindable SpeedChange's precision, in case this is called externally. + return rate * Math.Round(ramp, 2); } public virtual void Update(Playfield playfield) From 0df15b4d7a4361368d1504ec18695902b9969d82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sun, 7 Feb 2021 19:25:33 +0100 Subject: [PATCH 6455/6909] Add test coverage --- .../Mods/TestSceneOsuModAutoplay.cs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs diff --git a/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs new file mode 100644 index 0000000000..856b6554b9 --- /dev/null +++ b/osu.Game.Rulesets.Osu.Tests/Mods/TestSceneOsuModAutoplay.cs @@ -0,0 +1,65 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Linq; +using NUnit.Framework; +using osu.Framework.Testing; +using osu.Framework.Utils; +using osu.Game.Beatmaps; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.Osu.Mods; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.Skinning.Default; +using osu.Game.Rulesets.Osu.UI; + +namespace osu.Game.Rulesets.Osu.Tests.Mods +{ + public class TestSceneOsuModAutoplay : OsuModTestScene + { + [Test] + public void TestSpmUnaffectedByRateAdjust() + => runSpmTest(new OsuModDaycore + { + SpeedChange = { Value = 0.88 } + }); + + [Test] + public void TestSpmUnaffectedByTimeRamp() + => runSpmTest(new ModWindUp + { + InitialRate = { Value = 0.7 }, + FinalRate = { Value = 1.3 } + }); + + private void runSpmTest(Mod mod) + { + SpinnerSpmCounter spmCounter = null; + + CreateModTest(new ModTestData + { + Autoplay = true, + Mod = mod, + Beatmap = new Beatmap + { + HitObjects = + { + new Spinner + { + Duration = 2000, + Position = OsuPlayfield.BASE_SIZE / 2 + } + } + }, + PassCondition = () => Player.ScoreProcessor.JudgedHits >= 1 + }); + + AddUntilStep("fetch SPM counter", () => + { + spmCounter = this.ChildrenOfType().SingleOrDefault(); + return spmCounter != null; + }); + + AddUntilStep("SPM is correct", () => Precision.AlmostEquals(spmCounter.SpinsPerMinute, 477, 5)); + } + } +} From d74a1437beddf07f471254588a4609130a361cd2 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 7 Feb 2021 15:14:08 -0800 Subject: [PATCH 6456/6909] Fix player loader metadata not being centred --- .../Screens/Play/BeatmapMetadataDisplay.cs | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index b53141e8fb..eff06e26ee 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -23,32 +23,6 @@ namespace osu.Game.Screens.Play /// public class BeatmapMetadataDisplay : Container { - private class MetadataLine : Container - { - public MetadataLine(string left, string right) - { - AutoSizeAxes = Axes.Both; - Children = new Drawable[] - { - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopRight, - Margin = new MarginPadding { Right = 5 }, - Colour = OsuColour.Gray(0.8f), - Text = left, - }, - new OsuSpriteText - { - Anchor = Anchor.TopCentre, - Origin = Anchor.TopLeft, - Margin = new MarginPadding { Left = 5 }, - Text = string.IsNullOrEmpty(right) ? @"-" : right, - } - }; - } - } - private readonly WorkingBeatmap beatmap; private readonly Bindable> mods; private readonly Drawable facade; @@ -144,15 +118,34 @@ namespace osu.Game.Screens.Play Bottom = 40 }, }, - new MetadataLine("Source", metadata.Source) + new GridContainer { - Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, - }, - new MetadataLine("Mapper", metadata.AuthorString) - { Origin = Anchor.TopCentre, - Anchor = Anchor.TopCentre, + AutoSizeAxes = Axes.Both, + RowDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.AutoSize), + new Dimension(GridSizeMode.AutoSize), + }, + Content = new[] + { + new Drawable[] + { + new MetadataLineLabel("Source"), + new MetadataLineInfo(metadata.Source) + }, + new Drawable[] + { + new MetadataLineLabel("Mapper"), + new MetadataLineInfo(metadata.AuthorString) + } + } }, new ModDisplay { @@ -168,5 +161,26 @@ namespace osu.Game.Screens.Play Loading = true; } + + private class MetadataLineLabel : OsuSpriteText + { + public MetadataLineLabel(string text) + { + Anchor = Anchor.TopRight; + Origin = Anchor.TopRight; + Margin = new MarginPadding { Right = 5 }; + Colour = OsuColour.Gray(0.8f); + Text = text; + } + } + + private class MetadataLineInfo : OsuSpriteText + { + public MetadataLineInfo(string text) + { + Margin = new MarginPadding { Left = 5 }; + Text = string.IsNullOrEmpty(text) ? @"-" : text; + } + } } } From 2218247b21d09a8317ae84dd4dffa1bbf7a744c0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 11:07:50 +0900 Subject: [PATCH 6457/6909] Override mod type --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 8cd6676f9d..863dc05216 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Osu.Mods public override bool Ranked => false; + public override ModType Type => ModType.Conversion; + [SettingSource("Disable slider head judgement", "Scores sliders proportionally to the number of ticks hit.")] public Bindable DisableSliderHeadJudgement { get; } = new BindableBool(true); From d955200e0718db14fa4b5ea13e6355e5b7134983 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 11:10:07 +0900 Subject: [PATCH 6458/6909] Prevent invalid hit results for ignored slider heads --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index ee1df00ef7..87cfa47091 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // If not judged as a normal hitcircle, only track whether a hit has occurred (via IgnoreHit) rather than a scorable hit result. var result = base.ResultFor(timeOffset); - return result.IsHit() ? HitResult.IgnoreHit : result; + return result.IsHit() ? HitResult.IgnoreHit : HitResult.IgnoreMiss; } public Action OnShake; From 9e0724b138fd4c251dc61e5daa3e702dd2a77cee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Feb 2021 15:58:41 +0900 Subject: [PATCH 6459/6909] Remove unnecessary double resolution of OsuGame --- osu.Game/Screens/Play/Player.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index bd67d3f06a..669fa93298 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -80,9 +80,6 @@ namespace osu.Game.Screens.Play public int RestartCount; - [Resolved] - private OsuGameBase gameBase { get; set; } - [Resolved] private ScoreManager scoreManager { get; set; } @@ -160,7 +157,6 @@ namespace osu.Game.Screens.Play if (!Mods.Value.Any(m => m is ModAutoplay)) PrepareReplay(); - gameActive.BindTo(gameBase.IsActive); gameActive.BindValueChanged(_ => updatePauseOnFocusLostState(), true); } @@ -195,7 +191,10 @@ namespace osu.Game.Screens.Play mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); if (game != null) + { LocalUserPlaying.BindTo(game.LocalUserPlaying); + gameActive.BindTo(game.IsActive); + } DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); From 10142a44716882a4671d4cae2391a96348bd90ba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Feb 2021 16:59:21 +0900 Subject: [PATCH 6460/6909] Disable failing test temporarily pending resolution --- .../Visual/Multiplayer/TestScenePlaylistsSongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 2f7e59f800..1d13c6229c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -143,6 +143,7 @@ namespace osu.Game.Tests.Visual.Multiplayer /// Tests that the same instances are not shared between two playlist items. /// [Test] + [Ignore("Temporarily disabled due to a non-trivial test failure")] public void TestNewItemHasNewModInstances() { AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); From 42c169054afa0b0aaf6e84002d4f05fd80e63e17 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Feb 2021 17:46:34 +0900 Subject: [PATCH 6461/6909] Revert "Disable failing test temporarily pending resolution" This reverts commit 10142a44716882a4671d4cae2391a96348bd90ba. --- .../Visual/Multiplayer/TestScenePlaylistsSongSelect.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 1d13c6229c..2f7e59f800 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -143,7 +143,6 @@ namespace osu.Game.Tests.Visual.Multiplayer /// Tests that the same instances are not shared between two playlist items. /// [Test] - [Ignore("Temporarily disabled due to a non-trivial test failure")] public void TestNewItemHasNewModInstances() { AddStep("set dt mod", () => SelectedMods.Value = new[] { new OsuModDoubleTime() }); From fb8e31a30385636856e20ebdeb67d76ac6d815c8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 17:51:57 +0900 Subject: [PATCH 6462/6909] Fix incorrect connection building due to bad merges --- .../Online/Multiplayer/MultiplayerClient.cs | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 1b966ae1dc..6908795510 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -71,20 +71,6 @@ namespace osu.Game.Online.Multiplayer if (!await connectionLock.WaitAsync(10000)) throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck."); - var builder = new HubConnectionBuilder() - .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); - - if (RuntimeInfo.SupportsJIT) - builder.AddMessagePackProtocol(); - else - { - // eventually we will precompile resolvers for messagepack, but this isn't working currently - // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. - builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); - } - - connection = builder.Build(); - try { while (api.State.Value == APIState.Online) @@ -235,10 +221,19 @@ namespace osu.Game.Online.Multiplayer private HubConnection createConnection(CancellationToken cancellationToken) { - var newConnection = new HubConnectionBuilder() - .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }) - .AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }) - .Build(); + var builder = new HubConnectionBuilder() + .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); + + if (RuntimeInfo.SupportsJIT) + builder.AddMessagePackProtocol(); + else + { + // eventually we will precompile resolvers for messagepack, but this isn't working currently + // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. + builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); + } + + var newConnection = builder.Build(); // this is kind of SILLY // https://github.com/dotnet/aspnetcore/issues/15198 From 6b26a18a23162e784fad3c941ace78fa557bf7d4 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 8 Feb 2021 01:34:32 -0800 Subject: [PATCH 6463/6909] Fix attributes header not being aligned with content in editor timing mode --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index e4b9150df1..81b006e6c8 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -74,7 +74,8 @@ namespace osu.Game.Screens.Edit.Timing { new TableColumn(string.Empty, Anchor.Centre, new Dimension(GridSizeMode.AutoSize)), new TableColumn("Time", Anchor.Centre, new Dimension(GridSizeMode.AutoSize)), - new TableColumn("Attributes", Anchor.Centre), + new TableColumn(), + new TableColumn("Attributes", Anchor.CentreLeft), }; return columns.ToArray(); @@ -93,6 +94,7 @@ namespace osu.Game.Screens.Edit.Timing Text = group.Time.ToEditorFormattedString(), Font = OsuFont.GetFont(size: text_size, weight: FontWeight.Bold) }, + null, new ControlGroupAttributes(group), }; @@ -108,7 +110,6 @@ namespace osu.Game.Screens.Edit.Timing { RelativeSizeAxes = Axes.Both, Direction = FillDirection.Horizontal, - Padding = new MarginPadding(10), Spacing = new Vector2(2) }; From 5e7823b289a607731f253ec342c24fa9fcde7143 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 8 Feb 2021 01:37:34 -0800 Subject: [PATCH 6464/6909] Fix attributes content being zero size and disappearing after being half off-screen --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 81b006e6c8..8980c2019a 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -106,6 +106,7 @@ namespace osu.Game.Screens.Edit.Timing public ControlGroupAttributes(ControlPointGroup group) { + RelativeSizeAxes = Axes.Both; InternalChild = fill = new FillFlowContainer { RelativeSizeAxes = Axes.Both, From b40b159acb276d35bc553a597bc58980f3d3c1dd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 18:52:50 +0900 Subject: [PATCH 6465/6909] Round beatlength --- osu.Game/Beatmaps/Beatmap.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 51fdbce96d..434bff14b5 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.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.Timing; using osu.Game.Rulesets.Objects; using System.Collections.Generic; @@ -65,7 +66,7 @@ namespace osu.Game.Beatmaps return (beatLength: t.BeatLength, duration: nextTime - t.Time); }) // Aggregate durations into a set of (beatLength, duration) tuples for each beat length - .GroupBy(t => t.beatLength) + .GroupBy(t => Math.Round(t.beatLength * 1000) / 1000) .Select(g => (beatLength: g.Key, duration: g.Sum(t => t.duration))) // And if there are no timing points, use a default. .DefaultIfEmpty((TimingControlPoint.DEFAULT_BEAT_LENGTH, 0)); From 18e3f8c233da2ed3eb8b859944994d39f9f54512 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 19:03:19 +0900 Subject: [PATCH 6466/6909] Sort beat lengths rather than linear search --- osu.Game/Beatmaps/Beatmap.cs | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/osu.Game/Beatmaps/Beatmap.cs b/osu.Game/Beatmaps/Beatmap.cs index 434bff14b5..e5b6a4bc44 100644 --- a/osu.Game/Beatmaps/Beatmap.cs +++ b/osu.Game/Beatmaps/Beatmap.cs @@ -55,7 +55,7 @@ namespace osu.Game.Beatmaps // Note: This is more accurate and may present different results because osu-stable didn't have the ability to calculate slider durations in this context. double lastTime = HitObjects.LastOrDefault()?.GetEndTime() ?? ControlPointInfo.TimingPoints.LastOrDefault()?.Time ?? 0; - var beatLengthsAndDurations = + var mostCommon = // Construct a set of (beatLength, duration) tuples for each individual timing point. ControlPointInfo.TimingPoints.Select((t, i) => { @@ -68,23 +68,10 @@ namespace osu.Game.Beatmaps // Aggregate durations into a set of (beatLength, duration) tuples for each beat length .GroupBy(t => Math.Round(t.beatLength * 1000) / 1000) .Select(g => (beatLength: g.Key, duration: g.Sum(t => t.duration))) - // And if there are no timing points, use a default. - .DefaultIfEmpty((TimingControlPoint.DEFAULT_BEAT_LENGTH, 0)); + // Get the most common one, or 0 as a suitable default + .OrderByDescending(i => i.duration).FirstOrDefault(); - // Find the single beat length with the maximum aggregate duration. - double maxDurationBeatLength = double.NegativeInfinity; - double maxDuration = double.NegativeInfinity; - - foreach (var (beatLength, duration) in beatLengthsAndDurations) - { - if (duration > maxDuration) - { - maxDuration = duration; - maxDurationBeatLength = beatLength; - } - } - - return maxDurationBeatLength; + return mostCommon.beatLength; } IBeatmap IBeatmap.Clone() => Clone(); From f0dfa9f8f397269571b96d1165f03f36a555bc8e Mon Sep 17 00:00:00 2001 From: Lucas A Date: Mon, 8 Feb 2021 11:12:25 +0100 Subject: [PATCH 6467/6909] Use the newest config file available (where the local username matches the filename) --- osu.Game/IO/StableStorage.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index 614e548d93..ccc6f9c311 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -35,11 +35,7 @@ namespace osu.Game.IO { var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); - // enumerate the user config files available in case the user migrated their files from another pc / operating system. - var foundConfigFiles = GetFiles(".", "osu!.*.cfg"); - - // if more than one config file is found, let's use the oldest one (where the username in the filename doesn't match the local username). - var configFile = foundConfigFiles.Count() > 1 ? foundConfigFiles.FirstOrDefault(filename => !filename[5..^4].Contains(Environment.UserName, StringComparison.Ordinal)) : foundConfigFiles.FirstOrDefault(); + var configFile = GetFiles(".", $"osu!.{Environment.UserName}.cfg").SingleOrDefault(); if (configFile == null) return songsDirectoryPath; From a08c51f213594a02ea3d354c4e913f29723ab2fd Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 19:23:10 +0900 Subject: [PATCH 6468/6909] Remove duplicate code --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index e29fb658a3..ae32295676 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -164,9 +164,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); - var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance(); - userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList(); - if (Room.Host?.Equals(User) == true) crown.FadeIn(fade_time); else From d8c53e34ae5cba88b491d5379dec5d7ecd15e9f8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 19:42:17 +0900 Subject: [PATCH 6469/6909] Fix missing using --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 032493a5c6..f454fe619b 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; From 19368f87fb8c37eec5bf06f71b2d15959722cf08 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 19:59:07 +0900 Subject: [PATCH 6470/6909] Fix failing test --- osu.Game/Screens/Play/Player.cs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 669fa93298..7924e1390b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -174,7 +174,7 @@ namespace osu.Game.Screens.Play } [BackgroundDependencyLoader(true)] - private void load(AudioManager audio, OsuConfigManager config, OsuGame game) + private void load(AudioManager audio, OsuConfigManager config, OsuGameBase game) { Mods.Value = base.Mods.Value.Select(m => m.CreateCopy()).ToArray(); @@ -191,10 +191,9 @@ namespace osu.Game.Screens.Play mouseWheelDisabled = config.GetBindable(OsuSetting.MouseDisableWheel); if (game != null) - { - LocalUserPlaying.BindTo(game.LocalUserPlaying); gameActive.BindTo(game.IsActive); - } + if (game is OsuGame osuGame) + LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); DrawableRuleset = ruleset.CreateDrawableRulesetWith(playableBeatmap, Mods.Value); From 156f5bd5df715323e6dc227c7fb5be7e439ff72e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Feb 2021 20:05:16 +0900 Subject: [PATCH 6471/6909] Add newline between statements --- osu.Game/Screens/Play/Player.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 7924e1390b..5d06ac5b3a 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -192,6 +192,7 @@ namespace osu.Game.Screens.Play if (game != null) gameActive.BindTo(game.IsActive); + if (game is OsuGame osuGame) LocalUserPlaying.BindTo(osuGame.LocalUserPlaying); From f4a31287bfca31bf088f077e014f7af4d68b6232 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 8 Feb 2021 20:11:06 +0900 Subject: [PATCH 6472/6909] Add/use IHitObjectContainer interface instead of IEnumerables --- osu.Game.Rulesets.Osu/UI/IHitPolicy.cs | 7 +++--- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 3 +-- .../UI/StartTimeOrderedHitPolicy.cs | 7 +++--- osu.Game/Rulesets/UI/HitObjectContainer.cs | 11 +-------- osu.Game/Rulesets/UI/IHitObjectContainer.cs | 24 +++++++++++++++++++ 5 files changed, 32 insertions(+), 20 deletions(-) create mode 100644 osu.Game/Rulesets/UI/IHitObjectContainer.cs diff --git a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs index 72c3d781bb..5d8ea035a7 100644 --- a/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/IHitPolicy.cs @@ -1,19 +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.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI { public interface IHitPolicy { /// - /// Sets the s which this controls. + /// The containing the s which this applies to. /// - /// An enumeration of the s. - void SetHitObjects(IEnumerable hitObjects); + IHitObjectContainer HitObjectContainer { set; } /// /// Determines whether a can be hit at a point in time. diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index c7900558a0..e085714265 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -54,8 +54,7 @@ namespace osu.Game.Rulesets.Osu.UI approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; - hitPolicy = new StartTimeOrderedHitPolicy(); - hitPolicy.SetHitObjects(HitObjectContainer.AliveObjects); + hitPolicy = new StartTimeOrderedHitPolicy { HitObjectContainer = HitObjectContainer }; var hitWindows = new OsuHitWindows(); diff --git a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs index 38ba5fc490..0173156246 100644 --- a/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu/UI/StartTimeOrderedHitPolicy.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Osu.UI { @@ -19,9 +20,7 @@ namespace osu.Game.Rulesets.Osu.UI /// public class StartTimeOrderedHitPolicy : IHitPolicy { - private IEnumerable hitObjects; - - public void SetHitObjects(IEnumerable hitObjects) => this.hitObjects = hitObjects; + public IHitObjectContainer HitObjectContainer { get; set; } public bool IsHittable(DrawableHitObject hitObject, double time) { @@ -73,7 +72,7 @@ namespace osu.Game.Rulesets.Osu.UI private IEnumerable enumerateHitObjectsUpTo(double targetTime) { - foreach (var obj in hitObjects) + foreach (var obj in HitObjectContainer.AliveObjects) { if (obj.HitObject.StartTime >= targetTime) yield break; diff --git a/osu.Game/Rulesets/UI/HitObjectContainer.cs b/osu.Game/Rulesets/UI/HitObjectContainer.cs index 1972043ccb..11312a46df 100644 --- a/osu.Game/Rulesets/UI/HitObjectContainer.cs +++ b/osu.Game/Rulesets/UI/HitObjectContainer.cs @@ -17,19 +17,10 @@ using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.UI { - public class HitObjectContainer : LifetimeManagementContainer + public class HitObjectContainer : LifetimeManagementContainer, IHitObjectContainer { - /// - /// All currently in-use s. - /// public IEnumerable Objects => InternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); - /// - /// All currently in-use s that are alive. - /// - /// - /// If this uses pooled objects, this is equivalent to . - /// public IEnumerable AliveObjects => AliveInternalChildren.Cast().OrderBy(h => h.HitObject.StartTime); /// diff --git a/osu.Game/Rulesets/UI/IHitObjectContainer.cs b/osu.Game/Rulesets/UI/IHitObjectContainer.cs new file mode 100644 index 0000000000..4c784132e8 --- /dev/null +++ b/osu.Game/Rulesets/UI/IHitObjectContainer.cs @@ -0,0 +1,24 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Rulesets.Objects.Drawables; + +namespace osu.Game.Rulesets.UI +{ + public interface IHitObjectContainer + { + /// + /// All currently in-use s. + /// + IEnumerable Objects { get; } + + /// + /// All currently in-use s that are alive. + /// + /// + /// If this uses pooled objects, this is equivalent to . + /// + IEnumerable AliveObjects { get; } + } +} From 414e05affdf867f3bda1eb02a9639c965368b3d7 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 8 Feb 2021 02:41:07 -0800 Subject: [PATCH 6473/6909] Fix editor effect attribute tooltip having unnecessary whitespace when only one is enabled --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index 8980c2019a..cae7d5a021 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -151,7 +151,11 @@ namespace osu.Game.Screens.Edit.Timing return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x", colour); case EffectControlPoint effect: - return new RowAttribute("effect", () => $"{(effect.KiaiMode ? "Kiai " : "")}{(effect.OmitFirstBarLine ? "NoBarLine " : "")}", colour); + return new RowAttribute("effect", () => string.Join(" ", new[] + { + effect.KiaiMode ? "Kiai" : string.Empty, + effect.OmitFirstBarLine ? "NoBarLine" : string.Empty + }.Where(s => !string.IsNullOrEmpty(s))), colour); case SampleControlPoint sample: return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%", colour); From bebff61a9dde4ea61b27094ae9c4c4c214dababa Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Feb 2021 21:13:00 +0300 Subject: [PATCH 6474/6909] Add method for retrieving condensed user statistics --- osu.Game/Users/User.cs | 62 +++++++++++++++++++++++++++++++++++------- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 518236755d..2f8c6823c7 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -2,10 +2,16 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using JetBrains.Annotations; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using osu.Framework.Bindables; +using osu.Game.IO.Serialization; +using osu.Game.Online.API.Requests; +using osu.Game.Rulesets; namespace osu.Game.Users { @@ -178,6 +184,11 @@ namespace osu.Game.Users private UserStatistics statistics; + /// + /// The user statistics of the ruleset specified within the API request. + /// If the user is fetched from a or similar + /// (i.e. is a user compact instance), use instead. + /// [JsonProperty(@"statistics")] public UserStatistics Statistics { @@ -228,13 +239,35 @@ namespace osu.Game.Users [JsonProperty("replays_watched_counts")] public UserHistoryCount[] ReplaysWatchedCounts; - public class UserHistoryCount - { - [JsonProperty("start_date")] - public DateTime Date; + [UsedImplicitly] + [JsonExtensionData] + private readonly IDictionary otherProperties = new Dictionary(); - [JsonProperty("count")] - public long Count; + private readonly Dictionary statisticsCache = new Dictionary(); + + /// + /// Retrieves the user statistics for a certain ruleset. + /// If user is fetched from a , + /// this will always return null, use instead. + /// + /// The ruleset to retrieve statistics for. + // todo: this should likely be moved to a separate UserCompact class at some point. + public UserStatistics GetStatisticsFor(RulesetInfo ruleset) + { + if (statisticsCache.TryGetValue(ruleset, out var existing)) + return existing; + + return statisticsCache[ruleset] = parseStatisticsFor(ruleset); + } + + private UserStatistics parseStatisticsFor(RulesetInfo ruleset) + { + if (!(otherProperties.TryGetValue($"statistics_{ruleset.ShortName}", out var token))) + return null; + + var settings = JsonSerializableExtensions.CreateGlobalSettings(); + settings.DefaultValueHandling = DefaultValueHandling.Include; + return token.ToObject(JsonSerializer.Create(settings)); } public override string ToString() => Username; @@ -249,6 +282,14 @@ namespace osu.Game.Users Id = 0 }; + public bool Equals(User other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + + return Id == other.Id; + } + public enum PlayStyle { [Description("Keyboard")] @@ -264,12 +305,13 @@ namespace osu.Game.Users Touch, } - public bool Equals(User other) + public class UserHistoryCount { - if (ReferenceEquals(null, other)) return false; - if (ReferenceEquals(this, other)) return true; + [JsonProperty("start_date")] + public DateTime Date; - return Id == other.Id; + [JsonProperty("count")] + public long Count; } } } From d101add1591599c53f860c21e31901cbed220060 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Feb 2021 21:14:12 +0300 Subject: [PATCH 6475/6909] Display user global rank for selected ruleset in participants panel --- .../Participants/ParticipantPanel.cs | 21 ++++++++++++------- osu.Game/Users/UserStatistics.cs | 8 +++++++ 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 0ee1b6d684..f69a21918a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -35,9 +35,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants [Resolved] private RulesetStore rulesets { get; set; } + private SpriteIcon crown; + private OsuSpriteText userRankText; private ModDisplay userModsDisplay; private StateDisplay userStateDisplay; - private SpriteIcon crown; public ParticipantPanel(MultiplayerRoomUser user) { @@ -119,12 +120,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 18), Text = user?.Username }, - new OsuSpriteText + userRankText = new OsuSpriteText { Anchor = Anchor.CentreLeft, Origin = Anchor.CentreLeft, Font = OsuFont.GetFont(size: 14), - Text = user?.CurrentModeRank != null ? $"#{user.CurrentModeRank}" : string.Empty } } }, @@ -162,6 +162,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; + var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID); + + var currentModeRank = User.User?.GetStatisticsFor(ruleset)?.GlobalRank; + + // fallback to current mode rank for testing purposes. + currentModeRank ??= User.User?.CurrentModeRank; + + userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; + userStateDisplay.Status = User.State; if (Room.Host?.Equals(User) == true) @@ -171,11 +180,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => - { - var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance(); - userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList(); - }); + Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset.CreateInstance())).ToList()); } public MenuItem[] ContextMenuItems diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 8b7699d0ad..6c069f674e 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -3,6 +3,7 @@ using System; using Newtonsoft.Json; +using osu.Game.Online.API.Requests; using osu.Game.Scoring; using osu.Game.Utils; using static osu.Game.Users.User; @@ -26,6 +27,13 @@ namespace osu.Game.Users public int Progress; } + /// + /// This must only be used when coming from condensed user responses (e.g. from ), otherwise use Ranks.Global. + /// + // todo: this should likely be moved to a separate UserStatisticsCompact class at some point. + [JsonProperty(@"global_rank")] + public int? GlobalRank; + [JsonProperty(@"pp")] public decimal? PP; From cca1bac67d56d9d261149eb6f497fee824a69811 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 8 Feb 2021 22:00:01 +0300 Subject: [PATCH 6476/6909] Pass empty user statistics for consistency Realized `Statistics` was never giving null user statistics, so decided to pass an empty one here as well. --- osu.Game/Users/User.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 2f8c6823c7..467f00e409 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -248,7 +248,7 @@ namespace osu.Game.Users /// /// Retrieves the user statistics for a certain ruleset. /// If user is fetched from a , - /// this will always return null, use instead. + /// this will always return empty instance, use instead. /// /// The ruleset to retrieve statistics for. // todo: this should likely be moved to a separate UserCompact class at some point. @@ -263,7 +263,7 @@ namespace osu.Game.Users private UserStatistics parseStatisticsFor(RulesetInfo ruleset) { if (!(otherProperties.TryGetValue($"statistics_{ruleset.ShortName}", out var token))) - return null; + return new UserStatistics(); var settings = JsonSerializableExtensions.CreateGlobalSettings(); settings.DefaultValueHandling = DefaultValueHandling.Include; From af345ea5db05c056234e665f665b53b0a4912d1a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 01:52:35 +0300 Subject: [PATCH 6477/6909] Add a SignalR hub client connector component --- osu.Game/Online/HubClientConnector.cs | 209 ++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 osu.Game/Online/HubClientConnector.cs diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs new file mode 100644 index 0000000000..49b1ab639a --- /dev/null +++ b/osu.Game/Online/HubClientConnector.cs @@ -0,0 +1,209 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +#nullable enable + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.SignalR.Client; +using Microsoft.Extensions.DependencyInjection; +using Newtonsoft.Json; +using osu.Framework; +using osu.Framework.Allocation; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Logging; +using osu.Game.Online.API; + +namespace osu.Game.Online +{ + /// + /// A component that maintains over a hub connection between client and server. + /// + public class HubClientConnector : Component + { + /// + /// Invoked whenever a new hub connection is built. + /// + public Action? OnNewConnection; + + private readonly string clientName; + private readonly string endpoint; + + /// + /// The current connection opened by this connector. + /// + public HubConnection? CurrentConnection { get; private set; } + + /// + /// Whether this is connected to the hub, use to access the connection, if this is true. + /// + public IBindable IsConnected => isConnected; + + private readonly Bindable isConnected = new Bindable(); + private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1); + private CancellationTokenSource connectCancelSource = new CancellationTokenSource(); + + [Resolved] + private IAPIProvider api { get; set; } = null!; + + private readonly IBindable apiState = new Bindable(); + + /// + /// Constructs a new . + /// + /// The name of the client this connector connects for, used for logging. + /// The endpoint to the hub. + public HubClientConnector(string clientName, string endpoint) + { + this.clientName = clientName; + this.endpoint = endpoint; + } + + [BackgroundDependencyLoader] + private void load() + { + apiState.BindTo(api.State); + apiState.BindValueChanged(state => + { + switch (state.NewValue) + { + case APIState.Failing: + case APIState.Offline: + Task.Run(() => disconnect(true)); + break; + + case APIState.Online: + Task.Run(connect); + break; + } + }); + } + + private async Task connect() + { + cancelExistingConnect(); + + if (!await connectionLock.WaitAsync(10000)) + throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck."); + + try + { + while (apiState.Value == APIState.Online) + { + // ensure any previous connection was disposed. + // this will also create a new cancellation token source. + await disconnect(false); + + // this token will be valid for the scope of this connection. + // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere. + var cancellationToken = connectCancelSource.Token; + + cancellationToken.ThrowIfCancellationRequested(); + + Logger.Log($"{clientName} connecting...", LoggingTarget.Network); + + try + { + // importantly, rebuild the connection each attempt to get an updated access token. + CurrentConnection = createConnection(cancellationToken); + + await CurrentConnection.StartAsync(cancellationToken); + + Logger.Log($"{clientName} connected!", LoggingTarget.Network); + isConnected.Value = true; + return; + } + catch (OperationCanceledException) + { + //connection process was cancelled. + throw; + } + catch (Exception e) + { + Logger.Log($"{clientName} connection error: {e}", LoggingTarget.Network); + + // retry on any failure. + await Task.Delay(5000, cancellationToken); + } + } + } + finally + { + connectionLock.Release(); + } + } + + private HubConnection createConnection(CancellationToken cancellationToken) + { + var builder = new HubConnectionBuilder() + .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); + + if (RuntimeInfo.SupportsJIT) + builder.AddMessagePackProtocol(); + else + { + // eventually we will precompile resolvers for messagepack, but this isn't working currently + // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. + builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); + } + + var newConnection = builder.Build(); + + OnNewConnection?.Invoke(newConnection); + + newConnection.Closed += ex => + { + isConnected.Value = false; + + Logger.Log(ex != null ? $"{clientName} lost connection: {ex}" : $"{clientName} disconnected", LoggingTarget.Network); + + // make sure a disconnect wasn't triggered (and this is still the active connection). + if (!cancellationToken.IsCancellationRequested) + Task.Run(connect, default); + + return Task.CompletedTask; + }; + + return newConnection; + } + + private async Task disconnect(bool takeLock) + { + cancelExistingConnect(); + + if (takeLock) + { + if (!await connectionLock.WaitAsync(10000)) + throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck."); + } + + try + { + if (CurrentConnection != null) + await CurrentConnection.DisposeAsync(); + } + finally + { + CurrentConnection = null; + if (takeLock) + connectionLock.Release(); + } + } + + private void cancelExistingConnect() + { + connectCancelSource.Cancel(); + connectCancelSource = new CancellationTokenSource(); + } + + public override string ToString() => $"Connector for {clientName} ({(IsConnected.Value ? "connected" : "not connected")}"; + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + cancelExistingConnect(); + } + } +} From 28b815ffe13a5439241ccaeb763e26cb4e794ca6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 02:01:52 +0300 Subject: [PATCH 6478/6909] Clean up multiplayer client with new hub connector --- .../Online/Multiplayer/MultiplayerClient.cs | 209 +++--------------- .../Multiplayer/StatefulMultiplayerClient.cs | 7 +- 2 files changed, 31 insertions(+), 185 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 493518ac80..07036e7ffc 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -3,17 +3,11 @@ #nullable enable -using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using osu.Framework; -using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Logging; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -21,106 +15,37 @@ namespace osu.Game.Online.Multiplayer { public class MultiplayerClient : StatefulMultiplayerClient { - public override IBindable IsConnected => isConnected; + private readonly HubClientConnector connector; - private readonly Bindable isConnected = new Bindable(); - private readonly IBindable apiState = new Bindable(); + public override IBindable IsConnected => connector.IsConnected; - private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1); - - [Resolved] - private IAPIProvider api { get; set; } = null!; - - private HubConnection? connection; - - private CancellationTokenSource connectCancelSource = new CancellationTokenSource(); - - private readonly string endpoint; + private HubConnection? connection => connector.CurrentConnection; public MultiplayerClient(EndpointConfiguration endpoints) { - endpoint = endpoints.MultiplayerEndpointUrl; - } - - [BackgroundDependencyLoader] - private void load() - { - apiState.BindTo(api.State); - apiState.BindValueChanged(apiStateChanged, true); - } - - private void apiStateChanged(ValueChangedEvent state) - { - switch (state.NewValue) + InternalChild = connector = new HubClientConnector("Multiplayer client", endpoints.MultiplayerEndpointUrl) { - case APIState.Failing: - case APIState.Offline: - Task.Run(() => disconnect(true)); - break; - - case APIState.Online: - Task.Run(connect); - break; - } - } - - private async Task connect() - { - cancelExistingConnect(); - - if (!await connectionLock.WaitAsync(10000)) - throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck."); - - try - { - while (api.State.Value == APIState.Online) + OnNewConnection = newConnection => { - // ensure any previous connection was disposed. - // this will also create a new cancellation token source. - await disconnect(false); - - // this token will be valid for the scope of this connection. - // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere. - var cancellationToken = connectCancelSource.Token; - - cancellationToken.ThrowIfCancellationRequested(); - - Logger.Log("Multiplayer client connecting...", LoggingTarget.Network); - - try - { - // importantly, rebuild the connection each attempt to get an updated access token. - connection = createConnection(cancellationToken); - - await connection.StartAsync(cancellationToken); - - Logger.Log("Multiplayer client connected!", LoggingTarget.Network); - isConnected.Value = true; - return; - } - catch (OperationCanceledException) - { - //connection process was cancelled. - throw; - } - catch (Exception e) - { - Logger.Log($"Multiplayer client connection error: {e}", LoggingTarget.Network); - - // retry on any failure. - await Task.Delay(5000, cancellationToken); - } - } - } - finally - { - connectionLock.Release(); - } + // this is kind of SILLY + // https://github.com/dotnet/aspnetcore/issues/15198 + newConnection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); + newConnection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); + newConnection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); + newConnection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); + newConnection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); + newConnection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); + newConnection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); + newConnection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); + newConnection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); + newConnection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); + }, + }; } protected override Task JoinRoom(long roomId) { - if (!isConnected.Value) + if (!IsConnected.Value) return Task.FromCanceled(new CancellationToken(true)); return connection.InvokeAsync(nameof(IMultiplayerServer.JoinRoom), roomId); @@ -128,7 +53,7 @@ namespace osu.Game.Online.Multiplayer protected override Task LeaveRoomInternal() { - if (!isConnected.Value) + if (!IsConnected.Value) return Task.FromCanceled(new CancellationToken(true)); return connection.InvokeAsync(nameof(IMultiplayerServer.LeaveRoom)); @@ -136,7 +61,7 @@ namespace osu.Game.Online.Multiplayer public override Task TransferHost(int userId) { - if (!isConnected.Value) + if (!IsConnected.Value) return Task.CompletedTask; return connection.InvokeAsync(nameof(IMultiplayerServer.TransferHost), userId); @@ -144,7 +69,7 @@ namespace osu.Game.Online.Multiplayer public override Task ChangeSettings(MultiplayerRoomSettings settings) { - if (!isConnected.Value) + if (!IsConnected.Value) return Task.CompletedTask; return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeSettings), settings); @@ -152,7 +77,7 @@ namespace osu.Game.Online.Multiplayer public override Task ChangeState(MultiplayerUserState newState) { - if (!isConnected.Value) + if (!IsConnected.Value) return Task.CompletedTask; return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeState), newState); @@ -160,7 +85,7 @@ namespace osu.Game.Online.Multiplayer public override Task ChangeBeatmapAvailability(BeatmapAvailability newBeatmapAvailability) { - if (!isConnected.Value) + if (!IsConnected.Value) return Task.CompletedTask; return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeBeatmapAvailability), newBeatmapAvailability); @@ -168,7 +93,7 @@ namespace osu.Game.Online.Multiplayer public override Task ChangeUserMods(IEnumerable newMods) { - if (!isConnected.Value) + if (!IsConnected.Value) return Task.CompletedTask; return connection.InvokeAsync(nameof(IMultiplayerServer.ChangeUserMods), newMods); @@ -176,90 +101,10 @@ namespace osu.Game.Online.Multiplayer public override Task StartMatch() { - if (!isConnected.Value) + if (!IsConnected.Value) return Task.CompletedTask; return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); } - - private async Task disconnect(bool takeLock) - { - cancelExistingConnect(); - - if (takeLock) - { - if (!await connectionLock.WaitAsync(10000)) - throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck."); - } - - try - { - if (connection != null) - await connection.DisposeAsync(); - } - finally - { - connection = null; - if (takeLock) - connectionLock.Release(); - } - } - - private void cancelExistingConnect() - { - connectCancelSource.Cancel(); - connectCancelSource = new CancellationTokenSource(); - } - - private HubConnection createConnection(CancellationToken cancellationToken) - { - var builder = new HubConnectionBuilder() - .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); - - if (RuntimeInfo.SupportsJIT) - builder.AddMessagePackProtocol(); - else - { - // eventually we will precompile resolvers for messagepack, but this isn't working currently - // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. - builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); - } - - var newConnection = builder.Build(); - - // this is kind of SILLY - // https://github.com/dotnet/aspnetcore/issues/15198 - newConnection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); - newConnection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); - newConnection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); - newConnection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); - newConnection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); - newConnection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); - newConnection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); - newConnection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); - newConnection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); - newConnection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); - - newConnection.Closed += ex => - { - isConnected.Value = false; - - Logger.Log(ex != null ? $"Multiplayer client lost connection: {ex}" : "Multiplayer client disconnected", LoggingTarget.Network); - - // make sure a disconnect wasn't triggered (and this is still the active connection). - if (!cancellationToken.IsCancellationRequested) - Task.Run(connect, default); - - return Task.CompletedTask; - }; - return newConnection; - } - - protected override void Dispose(bool isDisposing) - { - base.Dispose(isDisposing); - - cancelExistingConnect(); - } } } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index f454fe619b..06f6754258 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -12,7 +12,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; @@ -28,7 +28,7 @@ using osu.Game.Utils; namespace osu.Game.Online.Multiplayer { - public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer + public abstract class StatefulMultiplayerClient : CompositeDrawable, IMultiplayerClient, IMultiplayerRoomServer { /// /// Invoked when any change occurs to the multiplayer room. @@ -97,7 +97,8 @@ namespace osu.Game.Online.Multiplayer // Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise. private int playlistItemId; - protected StatefulMultiplayerClient() + [BackgroundDependencyLoader] + private void load() { IsConnected.BindValueChanged(connected => { From f76f92515e7eb3588af91e0b3aac4c47fbc26731 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 02:15:51 +0300 Subject: [PATCH 6479/6909] Clean up spectator streaming client with new hub connector --- .../Visual/Gameplay/TestSceneSpectator.cs | 8 +- ...TestSceneMultiplayerGameplayLeaderboard.cs | 5 +- .../Spectator/SpectatorStreamingClient.cs | 159 +++++------------- 3 files changed, 48 insertions(+), 124 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 26524f07da..61b0961638 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -233,6 +232,8 @@ namespace osu.Game.Tests.Visual.Gameplay public class TestSpectatorStreamingClient : SpectatorStreamingClient { + protected override IBindable IsConnected { get; } = new BindableBool(false); + public readonly User StreamingUser = new User { Id = 55, Username = "Test user" }; public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; @@ -244,11 +245,6 @@ namespace osu.Game.Tests.Visual.Gameplay { } - protected override Task Connect() - { - return Task.CompletedTask; - } - public void StartPlay(int beatmapId) { this.beatmapId = beatmapId; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index d016accc25..6a777e2a78 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -4,7 +4,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Threading.Tasks; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -96,6 +95,8 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestMultiplayerStreaming : SpectatorStreamingClient { + protected override IBindable IsConnected { get; } = new BindableBool(false); + public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; private readonly int totalUsers; @@ -163,8 +164,6 @@ namespace osu.Game.Tests.Visual.Multiplayer ((ISpectatorClient)this).UserSentFrames(userId, new FrameDataBundle(header, Array.Empty())); } } - - protected override Task Connect() => Task.CompletedTask; } } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index b95e3f1297..7cea76c969 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -8,13 +8,9 @@ using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; using Microsoft.AspNetCore.SignalR.Client; -using Microsoft.Extensions.DependencyInjection; -using Newtonsoft.Json; -using osu.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; -using osu.Framework.Logging; +using osu.Framework.Graphics.Containers; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Replays.Legacy; @@ -27,14 +23,18 @@ using osu.Game.Screens.Play; namespace osu.Game.Online.Spectator { - public class SpectatorStreamingClient : Component, ISpectatorClient + public class SpectatorStreamingClient : CompositeDrawable, ISpectatorClient { /// /// The maximum milliseconds between frame bundle sends. /// public const double TIME_BETWEEN_SENDS = 200; - private HubConnection connection; + private readonly HubClientConnector connector; + + protected virtual IBindable IsConnected => connector.IsConnected; + + private HubConnection connection => connector.CurrentConnection; private readonly List watchingUsers = new List(); @@ -44,13 +44,6 @@ namespace osu.Game.Online.Spectator private readonly BindableList playingUsers = new BindableList(); - private readonly IBindable apiState = new Bindable(); - - private bool isConnected; - - [Resolved] - private IAPIProvider api { get; set; } - [CanBeNull] private IBeatmap currentBeatmap; @@ -82,114 +75,50 @@ namespace osu.Game.Online.Spectator /// public event Action OnUserFinishedPlaying; - private readonly string endpoint; - public SpectatorStreamingClient(EndpointConfiguration endpoints) { - endpoint = endpoints.SpectatorEndpointUrl; + InternalChild = connector = new HubClientConnector("Spectator client", endpoints.SpectatorEndpointUrl) + { + OnNewConnection = newConnection => + { + // until strong typed client support is added, each method must be manually bound + // (see https://github.com/dotnet/aspnetcore/issues/15198) + newConnection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); + newConnection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); + newConnection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + } + }; } [BackgroundDependencyLoader] private void load() { - apiState.BindTo(api.State); - apiState.BindValueChanged(apiStateChanged, true); - } - - private void apiStateChanged(ValueChangedEvent state) - { - switch (state.NewValue) + IsConnected.BindValueChanged(connected => { - case APIState.Failing: - case APIState.Offline: - connection?.StopAsync(); - connection = null; - break; - - case APIState.Online: - Task.Run(Connect); - break; - } - } - - protected virtual async Task Connect() - { - if (connection != null) - return; - - var builder = new HubConnectionBuilder() - .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); - - if (RuntimeInfo.SupportsJIT) - builder.AddMessagePackProtocol(); - else - { - // eventually we will precompile resolvers for messagepack, but this isn't working currently - // see https://github.com/neuecc/MessagePack-CSharp/issues/780#issuecomment-768794308. - builder.AddNewtonsoftJsonProtocol(options => { options.PayloadSerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore; }); - } - - connection = builder.Build(); - // until strong typed client support is added, each method must be manually bound (see https://github.com/dotnet/aspnetcore/issues/15198) - connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); - connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); - connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); - - connection.Closed += async ex => - { - isConnected = false; - playingUsers.Clear(); - - if (ex != null) + if (connected.NewValue) { - Logger.Log($"Spectator client lost connection: {ex}", LoggingTarget.Network); - await tryUntilConnected(); + // get all the users that were previously being watched + int[] users; + + lock (userLock) + { + users = watchingUsers.ToArray(); + watchingUsers.Clear(); + } + + // resubscribe to watched users. + foreach (var userId in users) + WatchUser(userId); + + // re-send state in case it wasn't received + if (isPlaying) + beginPlaying(); } - }; - - await tryUntilConnected(); - - async Task tryUntilConnected() - { - Logger.Log("Spectator client connecting...", LoggingTarget.Network); - - while (api.State.Value == APIState.Online) + else { - try - { - // reconnect on any failure - await connection.StartAsync(); - Logger.Log("Spectator client connected!", LoggingTarget.Network); - - // get all the users that were previously being watched - int[] users; - - lock (userLock) - { - users = watchingUsers.ToArray(); - watchingUsers.Clear(); - } - - // success - isConnected = true; - - // resubscribe to watched users - foreach (var userId in users) - WatchUser(userId); - - // re-send state in case it wasn't received - if (isPlaying) - beginPlaying(); - - break; - } - catch (Exception e) - { - Logger.Log($"Spectator client connection error: {e}", LoggingTarget.Network); - await Task.Delay(5000); - } + playingUsers.Clear(); } - } + }, true); } Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state) @@ -240,14 +169,14 @@ namespace osu.Game.Online.Spectator { Debug.Assert(isPlaying); - if (!isConnected) return; + if (!IsConnected.Value) return; connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState); } public void SendFrames(FrameDataBundle data) { - if (!isConnected) return; + if (!IsConnected.Value) return; lastSend = connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); } @@ -257,7 +186,7 @@ namespace osu.Game.Online.Spectator isPlaying = false; currentBeatmap = null; - if (!isConnected) return; + if (!IsConnected.Value) return; connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState); } @@ -271,7 +200,7 @@ namespace osu.Game.Online.Spectator watchingUsers.Add(userId); - if (!isConnected) + if (!IsConnected.Value) return; } @@ -284,7 +213,7 @@ namespace osu.Game.Online.Spectator { watchingUsers.Remove(userId); - if (!isConnected) + if (!IsConnected.Value) return; } From 3ce605b5e5174632bc78ec5792c75ea0e92be009 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 9 Feb 2021 12:00:03 +0900 Subject: [PATCH 6480/6909] Small refactoring to use .Trim() instead --- osu.Game/Screens/Edit/Timing/ControlPointTable.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs index cae7d5a021..a17b431fcc 100644 --- a/osu.Game/Screens/Edit/Timing/ControlPointTable.cs +++ b/osu.Game/Screens/Edit/Timing/ControlPointTable.cs @@ -151,11 +151,10 @@ namespace osu.Game.Screens.Edit.Timing return new RowAttribute("difficulty", () => $"{difficulty.SpeedMultiplier:n2}x", colour); case EffectControlPoint effect: - return new RowAttribute("effect", () => string.Join(" ", new[] - { + return new RowAttribute("effect", () => string.Join(" ", effect.KiaiMode ? "Kiai" : string.Empty, effect.OmitFirstBarLine ? "NoBarLine" : string.Empty - }.Where(s => !string.IsNullOrEmpty(s))), colour); + ).Trim(), colour); case SampleControlPoint sample: return new RowAttribute("sample", () => $"{sample.SampleBank} {sample.SampleVolume}%", colour); From 3133ccacfa79704dbf89faad3cb7c82fb242de32 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 13:09:38 +0900 Subject: [PATCH 6481/6909] Reset selected mods between each test method This doesn't actually fix or change behaviour, but does seem like something we probably want to do here. --- .../Visual/Multiplayer/TestScenePlaylistsSongSelect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs index 2f7e59f800..7d83ba569d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestScenePlaylistsSongSelect.cs @@ -87,6 +87,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Ruleset.Value = new OsuRuleset().RulesetInfo; Beatmap.SetDefault(); + SelectedMods.Value = Array.Empty(); }); AddStep("create song select", () => LoadScreen(songSelect = new TestPlaylistsSongSelect())); From be379e0e3cc910b7b3cf1fb6b48460aeaa2673d4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 13:44:11 +0900 Subject: [PATCH 6482/6909] Change CopyFrom to always overwrite all settings with incoming values --- osu.Game/Rulesets/Mods/Mod.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index 3a8717e678..dec72d94e5 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -134,7 +134,7 @@ namespace osu.Game.Rulesets.Mods } /// - /// Copies mod setting values from into this instance. + /// Copies mod setting values from into this instance, overwriting all existing settings. /// /// The mod to copy properties from. public void CopyFrom(Mod source) @@ -147,9 +147,7 @@ namespace osu.Game.Rulesets.Mods var targetBindable = (IBindable)prop.GetValue(this); var sourceBindable = (IBindable)prop.GetValue(source); - // we only care about changes that have been made away from defaults. - if (!sourceBindable.IsDefault) - CopyAdjustedSetting(targetBindable, sourceBindable); + CopyAdjustedSetting(targetBindable, sourceBindable); } } From 8204d360a8d84f5ac3fe2eec40155999c23a5ba2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 13:44:42 +0900 Subject: [PATCH 6483/6909] Always reset local user settings when a mod is deselected in ModSelectOverlay --- osu.Game/Overlays/Mods/ModButton.cs | 2 ++ osu.Game/Overlays/Mods/ModSection.cs | 4 +++- osu.Game/Rulesets/Mods/Mod.cs | 5 +++++ osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs | 11 +++++++++++ 4 files changed, 21 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 8e0d1f5bbd..06f2fea43f 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -69,6 +69,8 @@ namespace osu.Game.Overlays.Mods Mod newSelection = SelectedMod ?? Mods[0]; + newSelection.ResetSettingsToDefaults(); + Schedule(() => { if (beforeSelected != Selected) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index ecbcba7ad3..08bd3f8622 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -197,8 +197,10 @@ namespace osu.Game.Overlays.Mods continue; var buttonMod = button.Mods[index]; - buttonMod.CopyFrom(mod); button.SelectAt(index); + + // the selection above will reset settings to defaults, but as this is an external change we want to copy the new settings across. + buttonMod.CopyFrom(mod); return; } diff --git a/osu.Game/Rulesets/Mods/Mod.cs b/osu.Game/Rulesets/Mods/Mod.cs index dec72d94e5..2a11c92223 100644 --- a/osu.Game/Rulesets/Mods/Mod.cs +++ b/osu.Game/Rulesets/Mods/Mod.cs @@ -173,5 +173,10 @@ namespace osu.Game.Rulesets.Mods } public bool Equals(IMod other) => GetType() == other?.GetType(); + + /// + /// Reset all custom settings for this mod back to their defaults. + /// + public virtual void ResetSettingsToDefaults() => CopyFrom((Mod)Activator.CreateInstance(GetType())); } } diff --git a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs index a531e885db..dbc35569e7 100644 --- a/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModDifficultyAdjust.cs @@ -141,5 +141,16 @@ namespace osu.Game.Rulesets.Mods ApplySetting(DrainRate, dr => difficulty.DrainRate = dr); ApplySetting(OverallDifficulty, od => difficulty.OverallDifficulty = od); } + + public override void ResetSettingsToDefaults() + { + base.ResetSettingsToDefaults(); + + if (difficulty != null) + { + // base implementation potentially overwrite modified defaults that came from a beatmap selection. + TransferSettings(difficulty); + } + } } } From 71e564d399e617d6083d8540bdcdc2d89a18d2e3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 07:46:00 +0300 Subject: [PATCH 6484/6909] Revert clients to be `Component`s --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 4 ++-- osu.Game/Online/Spectator/SpectatorStreamingClient.cs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 06f6754258..18464a5f61 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -12,7 +12,7 @@ using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.ObjectExtensions; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; @@ -28,7 +28,7 @@ using osu.Game.Utils; namespace osu.Game.Online.Multiplayer { - public abstract class StatefulMultiplayerClient : CompositeDrawable, IMultiplayerClient, IMultiplayerRoomServer + public abstract class StatefulMultiplayerClient : Component, IMultiplayerClient, IMultiplayerRoomServer { /// /// Invoked when any change occurs to the multiplayer room. diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 7cea76c969..33ebe27937 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -10,7 +10,7 @@ using JetBrains.Annotations; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics; using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Replays.Legacy; @@ -23,7 +23,7 @@ using osu.Game.Screens.Play; namespace osu.Game.Online.Spectator { - public class SpectatorStreamingClient : CompositeDrawable, ISpectatorClient + public class SpectatorStreamingClient : Component, ISpectatorClient { /// /// The maximum milliseconds between frame bundle sends. From 848b81e952934ee90f3cc4e86428bb385c0fdde6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 07:53:22 +0300 Subject: [PATCH 6485/6909] Remove necessity of making hub client connector a component --- osu.Game/Online/HubClientConnector.cs | 51 ++++++++++++++------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index 49b1ab639a..b740aabb92 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -4,15 +4,14 @@ #nullable enable using System; +using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using Microsoft.Extensions.DependencyInjection; using Newtonsoft.Json; using osu.Framework; -using osu.Framework.Allocation; using osu.Framework.Bindables; -using osu.Framework.Graphics; using osu.Framework.Logging; using osu.Game.Online.API; @@ -21,7 +20,7 @@ namespace osu.Game.Online /// /// A component that maintains over a hub connection between client and server. /// - public class HubClientConnector : Component + public class HubClientConnector : IDisposable { /// /// Invoked whenever a new hub connection is built. @@ -30,6 +29,7 @@ namespace osu.Game.Online private readonly string clientName; private readonly string endpoint; + private readonly IAPIProvider? api; /// /// The current connection opened by this connector. @@ -45,9 +45,6 @@ namespace osu.Game.Online private readonly SemaphoreSlim connectionLock = new SemaphoreSlim(1); private CancellationTokenSource connectCancelSource = new CancellationTokenSource(); - [Resolved] - private IAPIProvider api { get; set; } = null!; - private readonly IBindable apiState = new Bindable(); /// @@ -55,30 +52,32 @@ namespace osu.Game.Online /// /// The name of the client this connector connects for, used for logging. /// The endpoint to the hub. - public HubClientConnector(string clientName, string endpoint) + /// The API provider for listening to state changes, or null to not listen. + public HubClientConnector(string clientName, string endpoint, IAPIProvider? api) { this.clientName = clientName; this.endpoint = endpoint; - } - [BackgroundDependencyLoader] - private void load() - { - apiState.BindTo(api.State); - apiState.BindValueChanged(state => + this.api = api; + + if (api != null) { - switch (state.NewValue) + apiState.BindTo(api.State); + apiState.BindValueChanged(state => { - case APIState.Failing: - case APIState.Offline: - Task.Run(() => disconnect(true)); - break; + switch (state.NewValue) + { + case APIState.Failing: + case APIState.Offline: + Task.Run(() => disconnect(true)); + break; - case APIState.Online: - Task.Run(connect); - break; - } - }); + case APIState.Online: + Task.Run(connect); + break; + } + }, true); + } } private async Task connect() @@ -137,6 +136,8 @@ namespace osu.Game.Online private HubConnection createConnection(CancellationToken cancellationToken) { + Debug.Assert(api != null); + var builder = new HubConnectionBuilder() .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); @@ -200,9 +201,9 @@ namespace osu.Game.Online public override string ToString() => $"Connector for {clientName} ({(IsConnected.Value ? "connected" : "not connected")}"; - protected override void Dispose(bool isDisposing) + public void Dispose() { - base.Dispose(isDisposing); + apiState.UnbindAll(); cancelExistingConnect(); } } From 0efad9ded10e03233ef2f14d644260178d9c746d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 13:54:13 +0900 Subject: [PATCH 6486/6909] Add test coverage of setting reset on deselection --- .../UserInterface/TestSceneModSelectOverlay.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 37ebc72984..85350c028c 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -47,6 +47,24 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("show", () => modSelect.Show()); } + [Test] + public void TestSettingsResetOnDeselection() + { + var osuModDoubleTime = new OsuModDoubleTime { SpeedChange = { Value = 1.2 } }; + + changeRuleset(0); + + AddStep("set dt mod with custom rate", () => { SelectedMods.Value = new[] { osuModDoubleTime }; }); + + AddAssert("selected mod matches", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.Value == 1.2); + + AddStep("deselect", () => modSelect.DeselectAllButton.Click()); + AddAssert("selected mods empty", () => SelectedMods.Value.Count == 0); + + AddStep("reselect", () => modSelect.GetModButton(osuModDoubleTime).Click()); + AddAssert("selected mod has default value", () => (SelectedMods.Value.Single() as OsuModDoubleTime)?.SpeedChange.IsDefault == true); + } + [Test] public void TestAnimationFlushOnClose() { From f04d6d5e5e98ddbdc0d94e4825d4d31392024be7 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 08:02:32 +0300 Subject: [PATCH 6487/6909] Update hub clients with changes to connecotr --- .../Visual/Gameplay/TestSceneSpectator.cs | 2 - ...TestSceneMultiplayerGameplayLeaderboard.cs | 2 - .../Online/Multiplayer/MultiplayerClient.cs | 46 +++++++++++++------ .../Spectator/SpectatorStreamingClient.cs | 45 ++++++++++-------- 4 files changed, 56 insertions(+), 39 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 61b0961638..4a0e1282c4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -232,8 +232,6 @@ namespace osu.Game.Tests.Visual.Gameplay public class TestSpectatorStreamingClient : SpectatorStreamingClient { - protected override IBindable IsConnected { get; } = new BindableBool(false); - public readonly User StreamingUser = new User { Id = 55, Username = "Test user" }; public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 6a777e2a78..aab69d687a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -95,8 +95,6 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestMultiplayerStreaming : SpectatorStreamingClient { - protected override IBindable IsConnected { get; } = new BindableBool(false); - public new BindableList PlayingUsers => (BindableList)base.PlayingUsers; private readonly int totalUsers; diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 07036e7ffc..6b67954351 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; +using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Game.Online.API; using osu.Game.Online.Rooms; @@ -15,32 +16,41 @@ namespace osu.Game.Online.Multiplayer { public class MultiplayerClient : StatefulMultiplayerClient { - private readonly HubClientConnector connector; + private readonly string endpoint; + private HubClientConnector? connector; - public override IBindable IsConnected => connector.IsConnected; + public override IBindable IsConnected { get; } = new BindableBool(); - private HubConnection? connection => connector.CurrentConnection; + private HubConnection? connection => connector?.CurrentConnection; public MultiplayerClient(EndpointConfiguration endpoints) { - InternalChild = connector = new HubClientConnector("Multiplayer client", endpoints.MultiplayerEndpointUrl) + endpoint = endpoints.MultiplayerEndpointUrl; + } + + [BackgroundDependencyLoader] + private void load(IAPIProvider api) + { + connector = new HubClientConnector(nameof(MultiplayerClient), endpoint, api) { - OnNewConnection = newConnection => + OnNewConnection = connection => { // this is kind of SILLY // https://github.com/dotnet/aspnetcore/issues/15198 - newConnection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); - newConnection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); - newConnection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); - newConnection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); - newConnection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); - newConnection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); - newConnection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); - newConnection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); - newConnection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); - newConnection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); + connection.On(nameof(IMultiplayerClient.RoomStateChanged), ((IMultiplayerClient)this).RoomStateChanged); + connection.On(nameof(IMultiplayerClient.UserJoined), ((IMultiplayerClient)this).UserJoined); + connection.On(nameof(IMultiplayerClient.UserLeft), ((IMultiplayerClient)this).UserLeft); + connection.On(nameof(IMultiplayerClient.HostChanged), ((IMultiplayerClient)this).HostChanged); + connection.On(nameof(IMultiplayerClient.SettingsChanged), ((IMultiplayerClient)this).SettingsChanged); + connection.On(nameof(IMultiplayerClient.UserStateChanged), ((IMultiplayerClient)this).UserStateChanged); + connection.On(nameof(IMultiplayerClient.LoadRequested), ((IMultiplayerClient)this).LoadRequested); + connection.On(nameof(IMultiplayerClient.MatchStarted), ((IMultiplayerClient)this).MatchStarted); + connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); + connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); }, }; + + IsConnected.BindTo(connector.IsConnected); } protected override Task JoinRoom(long roomId) @@ -106,5 +116,11 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + connector?.Dispose(); + } } } diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 33ebe27937..4ef59b5e47 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -30,9 +30,11 @@ namespace osu.Game.Online.Spectator /// public const double TIME_BETWEEN_SENDS = 200; - private readonly HubClientConnector connector; + private readonly string endpoint; - protected virtual IBindable IsConnected => connector.IsConnected; + private HubClientConnector connector; + + private readonly IBindable isConnected = new BindableBool(); private HubConnection connection => connector.CurrentConnection; @@ -77,23 +79,24 @@ namespace osu.Game.Online.Spectator public SpectatorStreamingClient(EndpointConfiguration endpoints) { - InternalChild = connector = new HubClientConnector("Spectator client", endpoints.SpectatorEndpointUrl) - { - OnNewConnection = newConnection => - { - // until strong typed client support is added, each method must be manually bound - // (see https://github.com/dotnet/aspnetcore/issues/15198) - newConnection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); - newConnection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); - newConnection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); - } - }; + endpoint = endpoints.SpectatorEndpointUrl; } [BackgroundDependencyLoader] - private void load() + private void load(IAPIProvider api) { - IsConnected.BindValueChanged(connected => + connector = CreateConnector(nameof(SpectatorStreamingClient), endpoint, api); + connector.OnNewConnection = connection => + { + // until strong typed client support is added, each method must be manually bound + // (see https://github.com/dotnet/aspnetcore/issues/15198) + connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); + connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); + connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + }; + + isConnected.BindTo(connector.IsConnected); + isConnected.BindValueChanged(connected => { if (connected.NewValue) { @@ -121,6 +124,8 @@ namespace osu.Game.Online.Spectator }, true); } + protected virtual HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) => new HubClientConnector(name, endpoint, api); + Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state) { if (!playingUsers.Contains(userId)) @@ -169,14 +174,14 @@ namespace osu.Game.Online.Spectator { Debug.Assert(isPlaying); - if (!IsConnected.Value) return; + if (!isConnected.Value) return; connection.SendAsync(nameof(ISpectatorServer.BeginPlaySession), currentState); } public void SendFrames(FrameDataBundle data) { - if (!IsConnected.Value) return; + if (!isConnected.Value) return; lastSend = connection.SendAsync(nameof(ISpectatorServer.SendFrameData), data); } @@ -186,7 +191,7 @@ namespace osu.Game.Online.Spectator isPlaying = false; currentBeatmap = null; - if (!IsConnected.Value) return; + if (!isConnected.Value) return; connection.SendAsync(nameof(ISpectatorServer.EndPlaySession), currentState); } @@ -200,7 +205,7 @@ namespace osu.Game.Online.Spectator watchingUsers.Add(userId); - if (!IsConnected.Value) + if (!isConnected.Value) return; } @@ -213,7 +218,7 @@ namespace osu.Game.Online.Spectator { watchingUsers.Remove(userId); - if (!IsConnected.Value) + if (!isConnected.Value) return; } From a0ead38496b9dcb9ead023729765c437e7902f95 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 08:02:51 +0300 Subject: [PATCH 6488/6909] Prevent test spectator clients from attempting hub connections --- osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs | 7 +++++++ .../Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 4a0e1282c4..1e499f20cb 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -12,6 +12,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu; @@ -243,6 +244,12 @@ namespace osu.Game.Tests.Visual.Gameplay { } + protected override HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) + { + // do not pass API to prevent attempting failing connections on an actual hub. + return base.CreateConnector(name, endpoint, null); + } + public void StartPlay(int beatmapId) { this.beatmapId = beatmapId; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index aab69d687a..b459cebdd7 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -13,6 +13,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Database; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu.Scoring; @@ -105,6 +106,12 @@ namespace osu.Game.Tests.Visual.Multiplayer this.totalUsers = totalUsers; } + protected override HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) + { + // do not pass API to prevent attempting failing connections on an actual hub. + return base.CreateConnector(name, endpoint, null); + } + public void Start(int beatmapId) { for (int i = 0; i < totalUsers; i++) From b96a594546b38a866c7e661b707de04518407baf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 15:11:58 +0900 Subject: [PATCH 6489/6909] Remove unnecessary initial call to HitObjectApplied bound method Was causing test failures. Looks to be unnecessary on a check of when HitObjectApplied is invoked. --- osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index 8eb2714c04..f9b8ffca7b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -45,7 +45,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; drawableObject.HitObjectApplied += onHitObjectApplied; - onHitObjectApplied(drawableObject); } private void onHitObjectApplied(DrawableHitObject obj) From 695e46a358ba1dd75da161ee70e09bd19d462334 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 15:31:55 +0900 Subject: [PATCH 6490/6909] Fix AutoPilot mod failing to block touch input --- osu.Game.Rulesets.Osu/OsuInputManager.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/OsuInputManager.cs b/osu.Game.Rulesets.Osu/OsuInputManager.cs index c8fe4f41ca..7314021a14 100644 --- a/osu.Game.Rulesets.Osu/OsuInputManager.cs +++ b/osu.Game.Rulesets.Osu/OsuInputManager.cs @@ -34,7 +34,7 @@ namespace osu.Game.Rulesets.Osu protected override bool Handle(UIEvent e) { - if (e is MouseMoveEvent && !AllowUserCursorMovement) return false; + if ((e is MouseMoveEvent || e is TouchMoveEvent) && !AllowUserCursorMovement) return false; return base.Handle(e); } From b87327841dec18cda4edcc9c6ee4a52d4b8ebf29 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 15:46:23 +0900 Subject: [PATCH 6491/6909] Add test covering initial state propagation --- .../TestSceneMultiplayerParticipantsList.cs | 24 +++++++++++-------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index c3852fafd4..0f7a9b442d 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -22,16 +22,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public class TestSceneMultiplayerParticipantsList : MultiplayerTestScene { [SetUp] - public new void Setup() => Schedule(() => - { - Child = new ParticipantsList - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - RelativeSizeAxes = Axes.Y, - Size = new Vector2(380, 0.7f) - }; - }); + public new void Setup() => Schedule(createNewParticipantsList); [Test] public void TestAddUser() @@ -92,6 +83,14 @@ namespace osu.Game.Tests.Visual.Multiplayer checkProgressBarVisibility(true); } + [Test] + public void TestCorrectInitialState() + { + AddStep("set to downloading map", () => Client.ChangeBeatmapAvailability(BeatmapAvailability.Downloading(0))); + AddStep("recreate list", createNewParticipantsList); + checkProgressBarVisibility(true); + } + [Test] public void TestBeatmapDownloadingStates() { @@ -212,6 +211,11 @@ namespace osu.Game.Tests.Visual.Multiplayer } } + private void createNewParticipantsList() + { + Child = new ParticipantsList { Anchor = Anchor.Centre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Y, Size = new Vector2(380, 0.7f) }; + } + private void checkProgressBarVisibility(bool visible) => AddUntilStep($"progress bar {(visible ? "is" : "is not")}visible", () => this.ChildrenOfType().Single().IsPresent == visible); From 04c243386b136359a81606c612686fe1a8754527 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 16:02:56 +0900 Subject: [PATCH 6492/6909] Fix initial state transfer regressing --- .../OnlinePlayBeatmapAvailablilityTracker.cs | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs index cfaf43451f..d6f4c45a75 100644 --- a/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs +++ b/osu.Game/Online/Rooms/OnlinePlayBeatmapAvailablilityTracker.cs @@ -29,18 +29,6 @@ namespace osu.Game.Online.Rooms private ScheduledDelegate progressUpdate; - public OnlinePlayBeatmapAvailablilityTracker() - { - State.BindValueChanged(_ => updateAvailability()); - Progress.BindValueChanged(_ => - { - // incoming progress changes are going to be at a very high rate. - // we don't want to flood the network with this, so rate limit how often we send progress updates. - if (progressUpdate?.Completed != false) - progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); - }); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -54,6 +42,16 @@ namespace osu.Game.Online.Rooms Model.Value = item.NewValue.Beatmap.Value.BeatmapSet; }, true); + + Progress.BindValueChanged(_ => + { + // incoming progress changes are going to be at a very high rate. + // we don't want to flood the network with this, so rate limit how often we send progress updates. + if (progressUpdate?.Completed != false) + progressUpdate = Scheduler.AddDelayed(updateAvailability, progressUpdate == null ? 0 : 500); + }); + + State.BindValueChanged(_ => updateAvailability(), true); } protected override bool VerifyDatabasedModel(BeatmapSetInfo databasedSet) From 5bd4f74ddf752f3ddc83d67d8ea48708a0248b13 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 16:24:29 +0900 Subject: [PATCH 6493/6909] Fix a potential crash when exiting play during the results screen transition --- osu.Game/Screens/Play/Player.cs | 48 +++++++++++++++++---------------- 1 file changed, 25 insertions(+), 23 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 5d06ac5b3a..dbee49b5dd 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -339,7 +339,7 @@ namespace osu.Game.Screens.Play { HoldToQuit = { - Action = performUserRequestedExit, + Action = () => PerformExit(true), IsPaused = { BindTarget = GameplayClockContainer.IsPaused } }, PlayerSettingsOverlay = { PlaybackSettings = { UserPlaybackRate = { BindTarget = GameplayClockContainer.UserPlaybackRate } } }, @@ -363,14 +363,14 @@ namespace osu.Game.Screens.Play FailOverlay = new FailOverlay { OnRetry = Restart, - OnQuit = performUserRequestedExit, + OnQuit = () => PerformExit(true), }, PauseOverlay = new PauseOverlay { OnResume = Resume, Retries = RestartCount, OnRetry = Restart, - OnQuit = performUserRequestedExit, + OnQuit = () => PerformExit(true), }, new HotkeyExitOverlay { @@ -487,14 +487,30 @@ namespace osu.Game.Screens.Play // if a restart has been requested, cancel any pending completion (user has shown intent to restart). completionProgressDelegate?.Cancel(); - ValidForResume = false; - - if (!this.IsCurrentScreen()) return; + if (!this.IsCurrentScreen()) + { + // there is a chance that the exit was performed after the transition to results has started. + // we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process). + ValidForResume = false; + this.MakeCurrent(); + } if (userRequested) - performUserRequestedExit(); - else - this.Exit(); + { + if (ValidForResume && HasFailed && !FailOverlay.IsPresent) + { + failAnimation.FinishTransforms(true); + return; + } + + if (canPause) + { + Pause(); + return; + } + } + + this.Exit(); } private void performUserRequestedSkip() @@ -508,20 +524,6 @@ namespace osu.Game.Screens.Play updateSampleDisabledState(); } - private void performUserRequestedExit() - { - if (ValidForResume && HasFailed && !FailOverlay.IsPresent) - { - failAnimation.FinishTransforms(true); - return; - } - - if (canPause) - Pause(); - else - this.Exit(); - } - /// /// Restart gameplay via a parent . /// This can be called from a child screen in order to trigger the restart process. From 61b9539864289b9ded799cac93187fc641c3db35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 17:14:16 +0900 Subject: [PATCH 6494/6909] Fix regression in quick exit logic --- osu.Game/Screens/Play/Player.cs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index dbee49b5dd..3f8651761e 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -478,11 +478,11 @@ namespace osu.Game.Screens.Play /// /// Exits the . /// - /// - /// Whether the exit is requested by the user, or a higher-level game component. - /// Pausing is allowed only in the former case. + /// + /// Whether the pause or fail dialog should be shown before performing an exit. + /// If true and a dialog is not yet displayed, the exit will be blocked the the relevant dialog will display instead. /// - protected void PerformExit(bool userRequested) + protected void PerformExit(bool showDialogFirst) { // if a restart has been requested, cancel any pending completion (user has shown intent to restart). completionProgressDelegate?.Cancel(); @@ -495,7 +495,7 @@ namespace osu.Game.Screens.Play this.MakeCurrent(); } - if (userRequested) + if (showDialogFirst) { if (ValidForResume && HasFailed && !FailOverlay.IsPresent) { @@ -503,7 +503,7 @@ namespace osu.Game.Screens.Play return; } - if (canPause) + if (canPause && !GameplayClockContainer.IsPaused.Value) { Pause(); return; @@ -540,10 +540,7 @@ namespace osu.Game.Screens.Play sampleRestart?.Play(); RestartRequested?.Invoke(); - if (this.IsCurrentScreen()) - PerformExit(true); - else - this.MakeCurrent(); + PerformExit(false); } private ScheduledDelegate completionProgressDelegate; From cba116ff090c650cbc4812f16e15ddfd13747e3f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 17:28:57 +0900 Subject: [PATCH 6495/6909] Fix incorrect call parameter for quick exit --- 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 3f8651761e..8a977b0498 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -379,7 +379,7 @@ namespace osu.Game.Screens.Play if (!this.IsCurrentScreen()) return; fadeOut(true); - PerformExit(true); + PerformExit(false); }, }, failAnimation = new FailAnimation(DrawableRuleset) { OnComplete = onFailComplete, }, From 2c052d70e8668bc9f64354fc14d3425b5f0e6552 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 17:29:18 +0900 Subject: [PATCH 6496/6909] Only trigger pause cooldown on pause (not exit) --- osu.Game/Screens/Play/Player.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8a977b0498..dda52f4dae 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -505,6 +505,10 @@ namespace osu.Game.Screens.Play if (canPause && !GameplayClockContainer.IsPaused.Value) { + if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value) + // still want to block if we are within the cooldown period and not already paused. + return; + Pause(); return; } @@ -808,14 +812,6 @@ namespace osu.Game.Screens.Play return true; } - // ValidForResume is false when restarting - if (ValidForResume) - { - if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value) - // still want to block if we are within the cooldown period and not already paused. - 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 94f35825ddb28b8bf2d3e86562afc47dbd30e4a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 17:29:27 +0900 Subject: [PATCH 6497/6909] Update test to cover changed exit/pause logic I think this makes more sense? --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 46dd91710a..ae806883b0 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -108,19 +108,19 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestExitTooSoon() + public void TestExitSoonAfterResumeSucceeds() { AddStep("seek before gameplay", () => Player.GameplayClockContainer.Seek(-5000)); pauseAndConfirm(); resume(); - AddStep("exit too soon", () => Player.Exit()); + AddStep("exit quick", () => Player.Exit()); confirmClockRunning(true); confirmPauseOverlayShown(false); - AddAssert("not exited", () => Player.IsCurrentScreen()); + AddAssert("exited", () => !Player.IsCurrentScreen()); } [Test] From b5fa9508006c76decac0752a6bdaf47598196d4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 18:30:05 +0900 Subject: [PATCH 6498/6909] Remove unnecessary depth specification --- osu.Game/Overlays/OnlineOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index 4a7318d065..b07f91b9ed 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - Header.With(h => h.Depth = -float.MaxValue), + Header, content = new Container { RelativeSizeAxes = Axes.X, From 178d88bcf197393fab133d3bd760d33c35b8e3c4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 18:32:44 +0900 Subject: [PATCH 6499/6909] Change BackgroundColour into a property --- osu.Game/Overlays/BeatmapListingOverlay.cs | 2 +- osu.Game/Overlays/BeatmapSetOverlay.cs | 2 +- osu.Game/Overlays/ChangelogOverlay.cs | 2 +- osu.Game/Overlays/FullscreenOverlay.cs | 6 +++--- osu.Game/Overlays/UserProfileOverlay.cs | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/BeatmapListingOverlay.cs b/osu.Game/Overlays/BeatmapListingOverlay.cs index cfa0ff00bc..5df7a4650e 100644 --- a/osu.Game/Overlays/BeatmapListingOverlay.cs +++ b/osu.Game/Overlays/BeatmapListingOverlay.cs @@ -85,7 +85,7 @@ namespace osu.Game.Overlays protected override BeatmapListingHeader CreateHeader() => new BeatmapListingHeader(); - protected override Color4 GetBackgroundColour() => ColourProvider.Background6; + protected override Color4 BackgroundColour => ColourProvider.Background6; private void onTypingStarted() { diff --git a/osu.Game/Overlays/BeatmapSetOverlay.cs b/osu.Game/Overlays/BeatmapSetOverlay.cs index 723b61bbc5..bdb3715e73 100644 --- a/osu.Game/Overlays/BeatmapSetOverlay.cs +++ b/osu.Game/Overlays/BeatmapSetOverlay.cs @@ -68,7 +68,7 @@ namespace osu.Game.Overlays protected override BeatmapSetHeader CreateHeader() => new BeatmapSetHeader(); - protected override Color4 GetBackgroundColour() => ColourProvider.Background6; + protected override Color4 BackgroundColour => ColourProvider.Background6; protected override void PopOutComplete() { diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 5200b567ff..05bad30107 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -55,7 +55,7 @@ namespace osu.Game.Overlays ListingSelected = ShowListing, }; - protected override Color4 GetBackgroundColour() => ColourProvider.Background4; + protected override Color4 BackgroundColour => ColourProvider.Background4; public void ShowListing() { diff --git a/osu.Game/Overlays/FullscreenOverlay.cs b/osu.Game/Overlays/FullscreenOverlay.cs index d0a0c994aa..735f0bcbd4 100644 --- a/osu.Game/Overlays/FullscreenOverlay.cs +++ b/osu.Game/Overlays/FullscreenOverlay.cs @@ -23,6 +23,8 @@ namespace osu.Game.Overlays public T Header { get; } + protected virtual Color4 BackgroundColour => ColourProvider.Background5; + [Resolved] protected IAPIProvider API { get; private set; } @@ -59,7 +61,7 @@ namespace osu.Game.Overlays new Box { RelativeSizeAxes = Axes.Both, - Colour = GetBackgroundColour() + Colour = BackgroundColour }, content = new Container { @@ -80,8 +82,6 @@ namespace osu.Game.Overlays [NotNull] protected abstract T CreateHeader(); - protected virtual Color4 GetBackgroundColour() => ColourProvider.Background5; - public override void Show() { if (State.Value == Visibility.Visible) diff --git a/osu.Game/Overlays/UserProfileOverlay.cs b/osu.Game/Overlays/UserProfileOverlay.cs index ccd9c291c4..299a14b250 100644 --- a/osu.Game/Overlays/UserProfileOverlay.cs +++ b/osu.Game/Overlays/UserProfileOverlay.cs @@ -36,7 +36,7 @@ namespace osu.Game.Overlays protected override ProfileHeader CreateHeader() => new ProfileHeader(); - protected override Color4 GetBackgroundColour() => ColourProvider.Background6; + protected override Color4 BackgroundColour => ColourProvider.Background6; public void ShowUser(int userId) => ShowUser(new User { Id = userId }); From 167076663304d009769df70fae7feccdf4128f0b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 18:42:15 +0900 Subject: [PATCH 6500/6909] Avoid unbinding external events --- osu.Game/Overlays/RankingsOverlay.cs | 44 +++++++++------------- osu.Game/Overlays/TabbableOnlineOverlay.cs | 2 +- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/osu.Game/Overlays/RankingsOverlay.cs b/osu.Game/Overlays/RankingsOverlay.cs index 6cd72d6e2c..a093969115 100644 --- a/osu.Game/Overlays/RankingsOverlay.cs +++ b/osu.Game/Overlays/RankingsOverlay.cs @@ -17,8 +17,6 @@ namespace osu.Game.Overlays { protected Bindable Country => Header.Country; - protected Bindable Scope => Header.Current; - private APIRequest lastRequest; [Resolved] @@ -42,31 +40,31 @@ namespace osu.Game.Overlays { // if a country is requested, force performance scope. if (Country.Value != null) - Scope.Value = RankingsScope.Performance; + Header.Current.Value = RankingsScope.Performance; - Scheduler.AddOnce(loadNewContent); - }); - - // Unbind events from scope so base class event will not be called - Scope.UnbindEvents(); - Scope.BindValueChanged(_ => - { - // country filtering is only valid for performance scope. - if (Scope.Value != RankingsScope.Performance) - Country.Value = null; - - Scheduler.AddOnce(loadNewContent); + Scheduler.AddOnce(triggerTabChanged); }); ruleset.BindValueChanged(_ => { - if (Scope.Value == RankingsScope.Spotlights) + if (Header.Current.Value == RankingsScope.Spotlights) return; - Scheduler.AddOnce(loadNewContent); + Scheduler.AddOnce(triggerTabChanged); }); } + protected override void OnTabChanged(RankingsScope tab) + { + // country filtering is only valid for performance scope. + if (Header.Current.Value != RankingsScope.Performance) + Country.Value = null; + + Scheduler.AddOnce(triggerTabChanged); + } + + private void triggerTabChanged() => base.OnTabChanged(Header.Current.Value); + protected override RankingsOverlayHeader CreateHeader() => new RankingsOverlayHeader(); public void ShowCountry(Country requested) @@ -79,17 +77,11 @@ namespace osu.Game.Overlays Country.Value = requested; } - public void ShowSpotlights() - { - Scope.Value = RankingsScope.Spotlights; - Show(); - } - protected override void CreateDisplayToLoad(RankingsScope tab) { lastRequest?.Cancel(); - if (Scope.Value == RankingsScope.Spotlights) + if (Header.Current.Value == RankingsScope.Spotlights) { LoadDisplay(new SpotlightsLayout { @@ -115,7 +107,7 @@ namespace osu.Game.Overlays private APIRequest createScopedRequest() { - switch (Scope.Value) + switch (Header.Current.Value) { case RankingsScope.Performance: return new GetUserRankingsRequest(ruleset.Value, country: Country.Value?.FlagName); @@ -153,8 +145,6 @@ namespace osu.Game.Overlays return null; } - private void loadNewContent() => OnTabChanged(Scope.Value); - protected override void Dispose(bool isDisposing) { lastRequest?.Cancel(); diff --git a/osu.Game/Overlays/TabbableOnlineOverlay.cs b/osu.Game/Overlays/TabbableOnlineOverlay.cs index cbcf3cd96e..8172e99c1b 100644 --- a/osu.Game/Overlays/TabbableOnlineOverlay.cs +++ b/osu.Game/Overlays/TabbableOnlineOverlay.cs @@ -68,7 +68,7 @@ namespace osu.Game.Overlays }, (cancellationToken = new CancellationTokenSource()).Token); } - protected void OnTabChanged(TEnum tab) + protected virtual void OnTabChanged(TEnum tab) { cancellationToken?.Cancel(); Loading.Show(); From 17475e60b0a733344a60a619a3c4fc16f2d9b95d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 18:48:50 +0900 Subject: [PATCH 6501/6909] Fix missed test scene update --- osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs index 626f545b91..aff510dd95 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankingsOverlay.cs @@ -25,7 +25,7 @@ namespace osu.Game.Tests.Visual.Online Add(rankingsOverlay = new TestRankingsOverlay { Country = { BindTarget = countryBindable }, - Scope = { BindTarget = scope }, + Header = { Current = { BindTarget = scope } }, }); } @@ -65,8 +65,6 @@ namespace osu.Game.Tests.Visual.Online private class TestRankingsOverlay : RankingsOverlay { public new Bindable Country => base.Country; - - public new Bindable Scope => base.Scope; } } } From 0a96f4d403cf3be814b10de8e2d7cc3e4e2c335e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 18:56:27 +0900 Subject: [PATCH 6502/6909] Avoid assigning null to a non-nullable property --- .../Visual/Online/TestSceneFullscreenOverlay.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs index f8b059e471..dc468bb62d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFullscreenOverlay.cs @@ -53,7 +53,16 @@ namespace osu.Game.Tests.Visual.Online }; } - protected override OverlayHeader CreateHeader() => null; + protected override OverlayHeader CreateHeader() => new TestHeader(); + + internal class TestHeader : OverlayHeader + { + protected override OverlayTitle CreateTitle() => new TestTitle(); + + internal class TestTitle : OverlayTitle + { + } + } } } } From d8d830db6e39a3d590dc3963f10e60a5919727fa Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Feb 2021 19:46:57 +0900 Subject: [PATCH 6503/6909] Defer playlist load to improve load time of the now playing overlay --- osu.Game/Overlays/NowPlayingOverlay.cs | 37 +++++++++++++++++++------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 9beb859f28..f94b41155a 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -84,11 +84,6 @@ namespace osu.Game.Overlays AutoSizeAxes = Axes.Y, Children = new Drawable[] { - playlist = new PlaylistOverlay - { - RelativeSizeAxes = Axes.X, - Y = player_height + 10, - }, playerContainer = new Container { RelativeSizeAxes = Axes.X, @@ -171,7 +166,7 @@ namespace osu.Game.Overlays Anchor = Anchor.CentreRight, Position = new Vector2(-bottom_black_area_height / 2, 0), Icon = FontAwesome.Solid.Bars, - Action = () => playlist.ToggleVisibility(), + Action = togglePlaylist }, } }, @@ -191,13 +186,35 @@ namespace osu.Game.Overlays }; } + private void togglePlaylist() + { + if (playlist == null) + { + LoadComponentAsync(playlist = new PlaylistOverlay + { + RelativeSizeAxes = Axes.X, + Y = player_height + 10, + }, _ => + { + dragContainer.Add(playlist); + + playlist.BeatmapSets.BindTo(musicController.BeatmapSets); + playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true); + + togglePlaylist(); + }); + + return; + } + + if (!beatmap.Disabled) + playlist.ToggleVisibility(); + } + protected override void LoadComplete() { base.LoadComplete(); - playlist.BeatmapSets.BindTo(musicController.BeatmapSets); - playlist.State.BindValueChanged(s => playlistButton.FadeColour(s.NewValue == Visibility.Visible ? colours.Yellow : Color4.White, 200, Easing.OutQuint), true); - beatmap.BindDisabledChanged(beatmapDisabledChanged, true); musicController.TrackChanged += trackChanged; @@ -306,7 +323,7 @@ namespace osu.Game.Overlays private void beatmapDisabledChanged(bool disabled) { if (disabled) - playlist.Hide(); + playlist?.Hide(); prevButton.Enabled.Value = !disabled; nextButton.Enabled.Value = !disabled; From d9dcf8a042bc99e0161a49088784e06553c8b27e Mon Sep 17 00:00:00 2001 From: Andrei Zavatski Date: Tue, 9 Feb 2021 20:30:31 +0300 Subject: [PATCH 6504/6909] Fix incorrect header depth in OnlineOverlay --- osu.Game/Overlays/OnlineOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index b07f91b9ed..4a7318d065 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - Header, + Header.With(h => h.Depth = -float.MaxValue), content = new Container { RelativeSizeAxes = Axes.X, From e44667e5e073016b471c13d7cce1427d0db89be5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 11:31:34 +0900 Subject: [PATCH 6505/6909] Use MinValue instead Co-authored-by: Salman Ahmed --- osu.Game/Overlays/OnlineOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index 4a7318d065..7c9f751d3b 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -32,7 +32,7 @@ namespace osu.Game.Overlays Direction = FillDirection.Vertical, Children = new Drawable[] { - Header.With(h => h.Depth = -float.MaxValue), + Header.With(h => h.Depth = float.MinValue), content = new Container { RelativeSizeAxes = Axes.X, From e9ef4aaf88a8908596231461420bc83d5ccc569f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 14:34:45 +0900 Subject: [PATCH 6506/6909] Add test covering expectations of external mod changes --- .../TestSceneModSelectOverlay.cs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 85350c028c..dec9e319ea 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -170,6 +170,31 @@ namespace osu.Game.Tests.Visual.UserInterface }); } + [Test] + public void TestExternallySetModIsReplacedByOverlayInstance() + { + Mod external = new OsuModDoubleTime(); + Mod overlayButtonMod = null; + + changeRuleset(0); + + AddStep("set mod externally", () => { SelectedMods.Value = new[] { external }; }); + + AddAssert("ensure button is selected", () => + { + var button = modSelect.GetModButton(SelectedMods.Value.Single()); + overlayButtonMod = button.SelectedMod; + return overlayButtonMod.GetType() == external.GetType(); + }); + + // Right now, when an external change occurs, the ModSelectOverlay will replace the global instance with its own + AddAssert("mod instance doesn't match", () => external != overlayButtonMod); + + AddAssert("one mod present in global selected", () => SelectedMods.Value.Count == 1); + AddAssert("globally selected matches button's mod instance", () => SelectedMods.Value.Contains(overlayButtonMod)); + AddAssert("globally selected doesn't contain original external change", () => !SelectedMods.Value.Contains(external)); + } + [Test] public void TestNonStacked() { From 52f0f3f3b212fb2d38057073138755b9e73c858b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 14:38:15 +0900 Subject: [PATCH 6507/6909] Add a note about SelectedMods behavioural quirks --- osu.Game/OsuGameBase.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 20d88d33f2..d3936ed27e 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -98,7 +98,14 @@ namespace osu.Game [Cached(typeof(IBindable))] protected readonly Bindable Ruleset = new Bindable(); - // todo: move this to SongSelect once Screen has the ability to unsuspend. + /// + /// The current mod selection for the local user. + /// + /// + /// If a mod select overlay is present, mod instances set to this value are not guaranteed to remain as the provided instance and will be overwritten by a copy. + /// In such a case, changes to settings of a mod will *not* propagate after a mod is added to this collection. + /// As such, all settings should be finalised before adding a mod to this collection. + /// [Cached] [Cached(typeof(IBindable>))] protected readonly Bindable> SelectedMods = new Bindable>(Array.Empty()); From de8a60435fca3f4644efa5b268848829b3812ae8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 14:44:37 +0900 Subject: [PATCH 6508/6909] Add failing test covering reported breaking case --- .../UserInterface/TestSceneModSelectOverlay.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index dec9e319ea..9ca1d4102a 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -170,6 +170,20 @@ namespace osu.Game.Tests.Visual.UserInterface }); } + [Test] + public void TestSettingsAreRetainedOnReload() + { + changeRuleset(0); + + AddStep("set customized mod externally", () => SelectedMods.Value = new[] { new OsuModDoubleTime { SpeedChange = { Value = 1.01 } } }); + + AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01); + + AddStep("create overlay", () => createDisplay(() => new TestNonStackedModSelectOverlay())); + + AddAssert("setting remains", () => (SelectedMods.Value.SingleOrDefault() as OsuModDoubleTime)?.SpeedChange.Value == 1.01); + } + [Test] public void TestExternallySetModIsReplacedByOverlayInstance() { From 75bc9f607e30495763ec305fe7f6fae608931754 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 14:55:15 +0900 Subject: [PATCH 6509/6909] Rename wrongly named method --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 93fe693937..21ed9af421 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -479,10 +479,10 @@ namespace osu.Game.Overlays.Mods foreach (var section in ModSectionsContainer.Children) section.UpdateSelectedButtons(selectedMods); - updateMods(); + updateMultiplier(); } - private void updateMods() + private void updateMultiplier() { var multiplier = 1.0; From a39263423c95dd25344dff4e4a5e56afb0842d3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 15:12:29 +0900 Subject: [PATCH 6510/6909] Fix externally changed settings from being reset when ModSelectOverlay is initialised --- osu.Game/Overlays/Mods/ModButton.cs | 16 ++++++++++++---- osu.Game/Overlays/Mods/ModSection.cs | 5 +++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModButton.cs b/osu.Game/Overlays/Mods/ModButton.cs index 06f2fea43f..5e3733cd5e 100644 --- a/osu.Game/Overlays/Mods/ModButton.cs +++ b/osu.Game/Overlays/Mods/ModButton.cs @@ -46,8 +46,9 @@ namespace osu.Game.Overlays.Mods /// Change the selected mod index of this button. /// /// The new index. + /// Whether any settings applied to the mod should be reset on selection. /// Whether the selection changed. - private bool changeSelectedIndex(int newIndex) + private bool changeSelectedIndex(int newIndex, bool resetSettings = true) { if (newIndex == selectedIndex) return false; @@ -69,7 +70,8 @@ namespace osu.Game.Overlays.Mods Mod newSelection = SelectedMod ?? Mods[0]; - newSelection.ResetSettingsToDefaults(); + if (resetSettings) + newSelection.ResetSettingsToDefaults(); Schedule(() => { @@ -211,11 +213,17 @@ namespace osu.Game.Overlays.Mods Deselect(); } - public bool SelectAt(int index) + /// + /// Select the mod at the provided index. + /// + /// The index to select. + /// Whether any settings applied to the mod should be reset on selection. + /// Whether the selection changed. + public bool SelectAt(int index, bool resetSettings = true) { if (!Mods[index].HasImplementation) return false; - changeSelectedIndex(index); + changeSelectedIndex(index, resetSettings); return true; } diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 08bd3f8622..71ecef2b82 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -197,9 +197,10 @@ namespace osu.Game.Overlays.Mods continue; var buttonMod = button.Mods[index]; - button.SelectAt(index); - // the selection above will reset settings to defaults, but as this is an external change we want to copy the new settings across. + button.SelectAt(index, false); + + // as this is likely coming from an external change, ensure the settings of the mod are in sync. buttonMod.CopyFrom(mod); return; } From 435c85a2e79b8682d093c261054c8d4ad67215b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 15:13:09 +0900 Subject: [PATCH 6511/6909] Avoid executing selection twice on ModSelectOverlay load --- 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 21ed9af421..f1bfa26a98 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -371,8 +371,8 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); + SelectedMods.BindValueChanged(_ => updateSelectedButtons()); availableMods.BindValueChanged(_ => updateAvailableMods(), true); - SelectedMods.BindValueChanged(_ => updateSelectedButtons(), true); } protected override void PopOut() From 98a83722ff0ad8fffdd426a55a32792d8ce8febd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 15:29:55 +0900 Subject: [PATCH 6512/6909] Move the point at which selected mods are reset in tests to allow mutliple creation test flow --- .../Visual/UserInterface/TestSceneModSelectOverlay.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs index 9ca1d4102a..2885dbee00 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneModSelectOverlay.cs @@ -39,7 +39,11 @@ namespace osu.Game.Tests.Visual.UserInterface } [SetUp] - public void SetUp() => Schedule(() => createDisplay(() => new TestModSelectOverlay())); + public void SetUp() => Schedule(() => + { + SelectedMods.Value = Array.Empty(); + createDisplay(() => new TestModSelectOverlay()); + }); [SetUpSteps] public void SetUpSteps() @@ -370,7 +374,6 @@ namespace osu.Game.Tests.Visual.UserInterface private void createDisplay(Func createOverlayFunc) { - SelectedMods.Value = Array.Empty(); Children = new Drawable[] { modSelect = createOverlayFunc().With(d => From 67c1c4c1ebdd89e8c63c76e071f9598c1db32250 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 15:30:17 +0900 Subject: [PATCH 6513/6909] Copy settings before applying selection --- osu.Game/Overlays/Mods/ModSection.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index 71ecef2b82..c3e56abd05 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -198,10 +198,10 @@ namespace osu.Game.Overlays.Mods var buttonMod = button.Mods[index]; - button.SelectAt(index, false); - // as this is likely coming from an external change, ensure the settings of the mod are in sync. buttonMod.CopyFrom(mod); + + button.SelectAt(index, false); return; } From b3b0d97354d7c57e554a69ea78f516e0e64833c3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 15:32:57 +0900 Subject: [PATCH 6514/6909] Avoid potential feedback from bindable event binds --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index f1bfa26a98..a6de0ad6b1 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -371,8 +371,11 @@ namespace osu.Game.Overlays.Mods { base.LoadComplete(); - SelectedMods.BindValueChanged(_ => updateSelectedButtons()); availableMods.BindValueChanged(_ => updateAvailableMods(), true); + + // intentionally bound after the above line to avoid a potential update feedback cycle. + // i haven't actually observed this happening but as updateAvailableMods() changes the selection it is plausible. + SelectedMods.BindValueChanged(_ => updateSelectedButtons()); } protected override void PopOut() From 806324b196607d6d918f792289d4043100944904 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 14:53:29 +0900 Subject: [PATCH 6515/6909] Allow overriding of Overlay pop-in and pop-out samples --- osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs index 41fd37a0d7..ee99a39523 100644 --- a/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs +++ b/osu.Game/Graphics/Containers/OsuFocusedOverlayContainer.cs @@ -20,6 +20,8 @@ namespace osu.Game.Graphics.Containers { private SampleChannel samplePopIn; private SampleChannel samplePopOut; + protected virtual string PopInSampleName => "UI/overlay-pop-in"; + protected virtual string PopOutSampleName => "UI/overlay-pop-out"; protected override bool BlockNonPositionalInput => true; @@ -40,8 +42,8 @@ namespace osu.Game.Graphics.Containers [BackgroundDependencyLoader(true)] private void load(AudioManager audio) { - samplePopIn = audio.Samples.Get(@"UI/overlay-pop-in"); - samplePopOut = audio.Samples.Get(@"UI/overlay-pop-out"); + samplePopIn = audio.Samples.Get(PopInSampleName); + samplePopOut = audio.Samples.Get(PopOutSampleName); } protected override void LoadComplete() From 3eda78c363def6589adc0a84aee6c7795cd72d76 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 14:57:46 +0900 Subject: [PATCH 6516/6909] Use unique samples for Dialog, NowPlaying, SettingsPanel and WaveOverlay pop-in/pop-out --- osu.Game/Overlays/DialogOverlay.cs | 3 +++ osu.Game/Overlays/NowPlayingOverlay.cs | 3 +++ osu.Game/Overlays/SettingsPanel.cs | 2 ++ osu.Game/Overlays/WaveOverlayContainer.cs | 2 ++ 4 files changed, 10 insertions(+) diff --git a/osu.Game/Overlays/DialogOverlay.cs b/osu.Game/Overlays/DialogOverlay.cs index 9f9dbdbaf1..4cc17a4c14 100644 --- a/osu.Game/Overlays/DialogOverlay.cs +++ b/osu.Game/Overlays/DialogOverlay.cs @@ -14,6 +14,9 @@ namespace osu.Game.Overlays { private readonly Container dialogContainer; + protected override string PopInSampleName => "UI/dialog-pop-in"; + protected override string PopOutSampleName => "UI/dialog-pop-out"; + public PopupDialog CurrentDialog { get; private set; } public DialogOverlay() diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 5c16a6e5c4..2866d2ad6d 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -51,6 +51,9 @@ namespace osu.Game.Overlays private Container dragContainer; private Container playerContainer; + protected override string PopInSampleName => "UI/now-playing-pop-in"; + protected override string PopOutSampleName => "UI/now-playing-pop-out"; + /// /// Provide a source for the toolbar height. /// diff --git a/osu.Game/Overlays/SettingsPanel.cs b/osu.Game/Overlays/SettingsPanel.cs index 7a5a586f67..f1270f750e 100644 --- a/osu.Game/Overlays/SettingsPanel.cs +++ b/osu.Game/Overlays/SettingsPanel.cs @@ -40,6 +40,8 @@ namespace osu.Game.Overlays private SeekLimitedSearchTextBox searchTextBox; + protected override string PopInSampleName => "UI/settings-pop-in"; + /// /// Provide a source for the toolbar height. /// diff --git a/osu.Game/Overlays/WaveOverlayContainer.cs b/osu.Game/Overlays/WaveOverlayContainer.cs index d0fa9987d5..52ae4dbdbb 100644 --- a/osu.Game/Overlays/WaveOverlayContainer.cs +++ b/osu.Game/Overlays/WaveOverlayContainer.cs @@ -18,6 +18,8 @@ namespace osu.Game.Overlays protected override bool StartHidden => true; + protected override string PopInSampleName => "UI/wave-pop-in"; + protected WaveOverlayContainer() { AddInternal(Waves = new WaveContainer From 22995c216deb26b16ad5e307b35cc5c69ff54260 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 14:59:30 +0900 Subject: [PATCH 6517/6909] Use unique sample for edit button click (ButtonSystem) --- osu.Game/Screens/Menu/ButtonSystem.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index f400b2114b..c6774127c1 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -129,7 +129,7 @@ namespace osu.Game.Screens.Menu buttonsPlay.ForEach(b => b.VisibleState = ButtonSystemState.Play); 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(@"edit", @"button-generic-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); + buttonsTopLevel.Add(new Button(@"edit", @"button-edit-select", OsuIcon.EditCircle, new Color4(238, 170, 0, 255), () => OnEdit?.Invoke(), 0, Key.E)); buttonsTopLevel.Add(new Button(@"browse", @"button-direct-select", OsuIcon.ChevronDownCircle, new Color4(165, 204, 0, 255), () => OnBeatmapListing?.Invoke(), 0, Key.D)); if (host.CanExit) From 73ab1b2b21f2eaaf40884d6d674d22c08031bfd7 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 15:01:22 +0900 Subject: [PATCH 6518/6909] Add pitch randomisation to HoverSounds on-hover sample playback --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index a1d06711db..21aae1b861 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; using osu.Game.Configuration; +using osu.Framework.Utils; namespace osu.Game.Graphics.UserInterface { @@ -49,9 +50,11 @@ namespace osu.Game.Graphics.UserInterface { bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; - if (enoughTimePassedSinceLastPlayback) + if (enoughTimePassedSinceLastPlayback && sampleHover != null) { - sampleHover?.Play(); + sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08); + sampleHover.Play(); + lastPlaybackTime.Value = Time.Current; } From 4e2ab0bad2c1aed6c9abc75250d1160ae87218f4 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 15:02:17 +0900 Subject: [PATCH 6519/6909] Use a separate sample set for Toolbar buttons --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 5 ++++- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index a1d06711db..4a79d1fbec 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -68,6 +68,9 @@ namespace osu.Game.Graphics.UserInterface Normal, [Description("-softer")] - Soft + Soft, + + [Description("-toolbar")] + Toolbar } } diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 49b9c62d85..83f2bdf6cb 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -77,7 +77,7 @@ namespace osu.Game.Overlays.Toolbar private KeyBindingStore keyBindings { get; set; } protected ToolbarButton() - : base(HoverSampleSet.Loud) + : base(HoverSampleSet.Toolbar) { Width = Toolbar.HEIGHT; RelativeSizeAxes = Axes.Y; From bc7f4a4f881b04feb4ac1c23b2d1fc5aba1fada5 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 20:16:22 +0900 Subject: [PATCH 6520/6909] Use a single sample for CarouselHeader on-hover with randomised pitch instead of multiple samples --- osu.Game/Screens/Select/Carousel/CarouselHeader.cs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index f1120f55a6..4f53a6e202 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -57,7 +57,7 @@ namespace osu.Game.Screens.Select.Carousel [BackgroundDependencyLoader] private void load(AudioManager audio, OsuColour colours) { - sampleHover = audio.Samples.Get($@"SongSelect/song-ping-variation-{RNG.Next(1, 5)}"); + sampleHover = audio.Samples.Get("SongSelect/song-ping"); hoverLayer.Colour = colours.Blue.Opacity(0.1f); } @@ -99,7 +99,11 @@ namespace osu.Game.Screens.Select.Carousel protected override bool OnHover(HoverEvent e) { - sampleHover?.Play(); + if (sampleHover != null) + { + sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2); + sampleHover.Play(); + } hoverLayer.FadeIn(100, Easing.OutQuint); return base.OnHover(e); From 625eb78a118566dc72161da36c9b5997d10309e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 10 Feb 2021 17:59:52 +0900 Subject: [PATCH 6521/6909] Simplify with an early exit for null sample --- osu.Game/Graphics/UserInterface/HoverSounds.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index 21aae1b861..5d6d0896fd 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -48,9 +48,12 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { + if (sampleHover == null) + return false; + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; - if (enoughTimePassedSinceLastPlayback && sampleHover != null) + if (enoughTimePassedSinceLastPlayback) { sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08); sampleHover.Play(); @@ -58,7 +61,7 @@ namespace osu.Game.Graphics.UserInterface lastPlaybackTime.Value = Time.Current; } - return base.OnHover(e); + return false; } } From 996f1098f6e7054e97150421ead8188147f5aa09 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Wed, 10 Feb 2021 18:14:32 +0900 Subject: [PATCH 6522/6909] Use alternate sample on the downbeat while hovering OsuLogo --- osu.Game/Screens/Menu/OsuLogo.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Menu/OsuLogo.cs b/osu.Game/Screens/Menu/OsuLogo.cs index 68d23e1a32..1d0af30275 100644 --- a/osu.Game/Screens/Menu/OsuLogo.cs +++ b/osu.Game/Screens/Menu/OsuLogo.cs @@ -45,6 +45,7 @@ namespace osu.Game.Screens.Menu private SampleChannel sampleClick; private SampleChannel sampleBeat; + private SampleChannel sampleDownbeat; private readonly Container colourAndTriangles; private readonly Triangles triangles; @@ -259,6 +260,7 @@ namespace osu.Game.Screens.Menu { sampleClick = audio.Samples.Get(@"Menu/osu-logo-select"); sampleBeat = audio.Samples.Get(@"Menu/osu-logo-heartbeat"); + sampleDownbeat = audio.Samples.Get(@"Menu/osu-logo-downbeat"); logo.Texture = textures.Get(@"Menu/logo"); ripple.Texture = textures.Get(@"Menu/logo"); @@ -281,7 +283,15 @@ namespace osu.Game.Screens.Menu if (beatIndex < 0) return; if (IsHovered) - this.Delay(early_activation).Schedule(() => sampleBeat.Play()); + { + this.Delay(early_activation).Schedule(() => + { + if (beatIndex % (int)timingPoint.TimeSignature == 0) + sampleDownbeat.Play(); + else + sampleBeat.Play(); + }); + } logoBeatContainer .ScaleTo(1 - 0.02f * amplitudeAdjust, early_activation, Easing.Out).Then() From cf06684ad121d58548d6cf0b9e87a439167dbc57 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:38:31 +0900 Subject: [PATCH 6523/6909] Judge heads as slider ticks instead --- .../Judgements/SliderTickJudgement.cs | 12 ++++++++++++ .../Objects/Drawables/DrawableSliderHead.cs | 2 +- osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs | 4 ++-- osu.Game.Rulesets.Osu/Objects/SliderTick.cs | 5 ----- 4 files changed, 15 insertions(+), 8 deletions(-) create mode 100644 osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs diff --git a/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.cs new file mode 100644 index 0000000000..a088696784 --- /dev/null +++ b/osu.Game.Rulesets.Osu/Judgements/SliderTickJudgement.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.Scoring; + +namespace osu.Game.Rulesets.Osu.Judgements +{ + public class SliderTickJudgement : OsuJudgement + { + public override HitResult MaxResult => HitResult.LargeTickHit; + } +} diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index 87cfa47091..c3759b6a34 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -91,7 +91,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables // If not judged as a normal hitcircle, only track whether a hit has occurred (via IgnoreHit) rather than a scorable hit result. var result = base.ResultFor(timeOffset); - return result.IsHit() ? HitResult.IgnoreHit : HitResult.IgnoreMiss; + return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss; } public Action OnShake; diff --git a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs index 28e57567cb..5672283230 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderHeadCircle.cs @@ -10,10 +10,10 @@ namespace osu.Game.Rulesets.Osu.Objects { /// /// Whether to treat this as a normal for judgement purposes. - /// If false, judgement will be ignored. + /// If false, this will be judged as a instead. /// public bool JudgeAsNormalHitCircle = true; - public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new OsuIgnoreJudgement(); + public override Judgement CreateJudgement() => JudgeAsNormalHitCircle ? base.CreateJudgement() : new SliderTickJudgement(); } } diff --git a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs index a427ee1955..725dbe81fb 100644 --- a/osu.Game.Rulesets.Osu/Objects/SliderTick.cs +++ b/osu.Game.Rulesets.Osu/Objects/SliderTick.cs @@ -33,10 +33,5 @@ namespace osu.Game.Rulesets.Osu.Objects protected override HitWindows CreateHitWindows() => HitWindows.Empty; public override Judgement CreateJudgement() => new SliderTickJudgement(); - - public class SliderTickJudgement : OsuJudgement - { - public override HitResult MaxResult => HitResult.LargeTickHit; - } } } From 6730c4c58b22ce2e753ac0f4b7055b5b87f62cde Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:41:28 +0900 Subject: [PATCH 6524/6909] Apply review comments (user explanations + property names) --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 26 ++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 863dc05216..642da87693 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -32,27 +32,27 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Conversion; - [SettingSource("Disable slider head judgement", "Scores sliders proportionally to the number of ticks hit.")] - public Bindable DisableSliderHeadJudgement { get; } = new BindableBool(true); + [SettingSource("No slider head accuracy requirement", "Scores sliders proportionally to the number of ticks hit.")] + public Bindable NoSliderHeadAccuracy { get; } = new BindableBool(true); - [SettingSource("Disable slider head tracking", "Pins slider heads at their starting position, regardless of time.")] - public Bindable DisableSliderHeadTracking { get; } = new BindableBool(true); + [SettingSource("No slider head movement", "Pins slider heads at their starting position, regardless of time.")] + public Bindable NoSliderHeadMovement { get; } = new BindableBool(true); - [SettingSource("Disable note lock lenience", "Applies note lock to the full hit window.")] - public Bindable DisableLenientNoteLock { get; } = new BindableBool(true); + [SettingSource("Apply classic note lock", "Applies note lock to the full hit window.")] + public Bindable ClassicNoteLock { get; } = new BindableBool(true); - [SettingSource("Disable exact slider follow circle tracking", "Makes the slider follow circle track its final size at all times.")] - public Bindable DisableExactFollowCircleTracking { get; } = new BindableBool(true); + [SettingSource("Use fixed slider follow circle hit area", "Makes the slider follow circle track its final size at all times.")] + public Bindable FixedFollowCircleHitArea { get; } = new BindableBool(true); public void ApplyToHitObject(HitObject hitObject) { switch (hitObject) { case Slider slider: - slider.IgnoreJudgement = !DisableSliderHeadJudgement.Value; + slider.IgnoreJudgement = !NoSliderHeadAccuracy.Value; foreach (var head in slider.NestedHitObjects.OfType()) - head.JudgeAsNormalHitCircle = !DisableSliderHeadJudgement.Value; + head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value; break; } @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var osuRuleset = (DrawableOsuRuleset)drawableRuleset; - if (!DisableLenientNoteLock.Value) + if (!ClassicNoteLock.Value) osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); } @@ -73,11 +73,11 @@ namespace osu.Game.Rulesets.Osu.Mods switch (obj) { case DrawableSlider slider: - slider.Ball.TrackVisualSize = !DisableExactFollowCircleTracking.Value; + slider.Ball.TrackVisualSize = !FixedFollowCircleHitArea.Value; break; case DrawableSliderHead head: - head.TrackFollowCircle = !DisableSliderHeadTracking.Value; + head.TrackFollowCircle = !NoSliderHeadMovement.Value; break; } } From 18a29dcb9664075e4fd1dadfc57157cbcc2fc21a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:42:13 +0900 Subject: [PATCH 6525/6909] Rename bindable member, reorder binds --- osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index f9b8ffca7b..b9cd176c63 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default [Resolved(CanBeNull = true)] private OsuRulesetConfigManager config { get; set; } - private readonly Bindable snakingOut = new Bindable(); + private readonly Bindable configSnakingOut = new Bindable(); [BackgroundDependencyLoader] private void load(ISkinSource skin, DrawableHitObject drawableObject) @@ -37,9 +37,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default accentColour = drawableObject.AccentColour.GetBoundCopy(); accentColour.BindValueChanged(accent => updateAccentColour(skin, accent.NewValue), true); - SnakingOut.BindTo(snakingOut); config?.BindWith(OsuRulesetSetting.SnakingInSliders, SnakingIn); - config?.BindWith(OsuRulesetSetting.SnakingOutSliders, snakingOut); + config?.BindWith(OsuRulesetSetting.SnakingOutSliders, configSnakingOut); + + SnakingOut.BindTo(configSnakingOut); BorderSize = skin.GetConfig(OsuSkinConfiguration.SliderBorderSize)?.Value ?? 1; BorderColour = skin.GetConfig(OsuSkinColour.SliderBorder)?.Value ?? Color4.White; @@ -56,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (!drawableSlider.HeadCircle.TrackFollowCircle) { // When not tracking the follow circle, force the path to not snake out as it looks better that way. - SnakingOut.UnbindFrom(snakingOut); + SnakingOut.UnbindFrom(configSnakingOut); SnakingOut.Value = false; } } From 9519b7f7c16e234d119ecaf65e3ff019ad84c400 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:43:14 +0900 Subject: [PATCH 6526/6909] Adjust comment --- osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs index b9cd176c63..4dd7b2d69c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/PlaySliderBody.cs @@ -54,9 +54,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default if (drawableSlider.HitObject == null) return; + // When not tracking the follow circle, unbind from the config and forcefully disable snaking out - it looks better that way. if (!drawableSlider.HeadCircle.TrackFollowCircle) { - // When not tracking the follow circle, force the path to not snake out as it looks better that way. SnakingOut.UnbindFrom(configSnakingOut); SnakingOut.Value = false; } From 2fcc4213e16f8a9ebc33d91474fe3c0a632cf36c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:46:26 +0900 Subject: [PATCH 6527/6909] Rename IgnoreJudgement -> OnlyJudgeNestedObjects --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 2 +- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 4 ++-- osu.Game.Rulesets.Osu/Objects/Slider.cs | 8 ++++---- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 642da87693..8e533854c0 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -49,7 +49,7 @@ namespace osu.Game.Rulesets.Osu.Mods switch (hitObject) { case Slider slider: - slider.IgnoreJudgement = !NoSliderHeadAccuracy.Value; + slider.OnlyJudgeNestedObjects = !NoSliderHeadAccuracy.Value; foreach (var head in slider.NestedHitObjects.OfType()) head.JudgeAsNormalHitCircle = !NoSliderHeadAccuracy.Value; diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index e607163b3e..13057d7a9a 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 public SliderBall Ball { get; private set; } public SkinnableDrawable Body { get; private set; } - public override bool DisplayResult => !HitObject.IgnoreJudgement; + public override bool DisplayResult => !HitObject.OnlyJudgeNestedObjects; private PlaySliderBody sliderBody => Body.Drawable as PlaySliderBody; @@ -250,7 +250,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < HitObject.EndTime) return; - if (HitObject.IgnoreJudgement) + if (HitObject.OnlyJudgeNestedObjects) { ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); return; diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 01694a838b..332163454a 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -115,10 +115,10 @@ namespace osu.Game.Rulesets.Osu.Objects public double TickDistanceMultiplier = 1; /// - /// Whether this 's judgement should be ignored. - /// If false, this will be judged proportionally to the number of ticks hit. + /// Whether this 's judgement is fully handled by its nested s. + /// If false, this will be judged proportionally to the number of nested s hit. /// - public bool IgnoreJudgement = true; + public bool OnlyJudgeNestedObjects = true; [JsonIgnore] public SliderHeadCircle HeadCircle { get; protected set; } @@ -239,7 +239,7 @@ namespace osu.Game.Rulesets.Osu.Objects HeadCircle.Samples = this.GetNodeSamples(0); } - public override Judgement CreateJudgement() => IgnoreJudgement ? new OsuIgnoreJudgement() : new OsuJudgement(); + public override Judgement CreateJudgement() => OnlyJudgeNestedObjects ? new OsuIgnoreJudgement() : new OsuJudgement(); protected override HitWindows CreateHitWindows() => HitWindows.Empty; } From a16f4cee3a0d44bbcdf69adb4949f2d3a425efd1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:52:39 +0900 Subject: [PATCH 6528/6909] Adjust DrawableSlider comment --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 13057d7a9a..921139c4e9 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -250,13 +250,15 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (userTriggered || Time.Current < HitObject.EndTime) return; + // If only the nested hitobjects are judged, then the slider's own judgement is ignored for scoring purposes. + // But the slider needs to still be judged with a reasonable hit/miss result for visual purposes (hit/miss transforms, etc). if (HitObject.OnlyJudgeNestedObjects) { ApplyResult(r => r.Type = NestedHitObjects.Any(h => h.Result.IsHit) ? r.Judgement.MaxResult : r.Judgement.MinResult); return; } - // If not ignoring judgement, score proportionally based on the number of ticks hit, counting the head circle as a tick. + // Otherwise, if this slider is also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. ApplyResult(r => { int totalTicks = NestedHitObjects.Count; From 6bf40170db22f31cea0c040824a218794d236718 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 18:53:23 +0900 Subject: [PATCH 6529/6909] Rename SliderBall flag --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 2 +- osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 8e533854c0..17b0b18b52 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -73,7 +73,7 @@ namespace osu.Game.Rulesets.Osu.Mods switch (obj) { case DrawableSlider slider: - slider.Ball.TrackVisualSize = !FixedFollowCircleHitArea.Value; + slider.Ball.InputTracksVisualSize = !FixedFollowCircleHitArea.Value; break; case DrawableSliderHead head: diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs index da3debbd42..82b677e12c 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SliderBall.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default /// Whether to track accurately to the visual size of this . /// If false, tracking will be performed at the final scale at all times. /// - public bool TrackVisualSize = true; + public bool InputTracksVisualSize = true; private readonly Drawable followCircle; private readonly DrawableSlider drawableSlider; @@ -100,7 +100,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default tracking = value; - if (TrackVisualSize) + if (InputTracksVisualSize) followCircle.ScaleTo(tracking ? 2.4f : 1f, 300, Easing.OutQuint); else { From 0dcdad98397453b439cab67421e29ecb3ca13f00 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 19:04:23 +0900 Subject: [PATCH 6530/6909] Adjust comment for DrawableSliderHead --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs index c3759b6a34..01c0d988ee 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSliderHead.cs @@ -89,7 +89,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (HitObject.JudgeAsNormalHitCircle) return base.ResultFor(timeOffset); - // If not judged as a normal hitcircle, only track whether a hit has occurred (via IgnoreHit) rather than a scorable hit result. + // If not judged as a normal hitcircle, judge as a slider tick instead. This is the classic osu!stable scoring. var result = base.ResultFor(timeOffset); return result.IsHit() ? HitResult.LargeTickHit : HitResult.LargeTickMiss; } From 393cd6c74a354919dc0b410d0bea38b8701bd032 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 19:39:47 +0900 Subject: [PATCH 6531/6909] Add helper class for tracking changes to mod settings --- .../Configuration/SettingSourceAttribute.cs | 27 ++++++++++++++++ .../Screens/Select/Details/AdvancedStats.cs | 31 ++++++------------- 2 files changed, 37 insertions(+), 21 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 50069be4b2..00c322065a 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -9,6 +9,7 @@ using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Mods; namespace osu.Game.Configuration { @@ -140,4 +141,30 @@ namespace osu.Game.Configuration return orderedRelative.Concat(unordered); } } + + public class ModSettingChangeTracker : IDisposable + { + public Action SettingChanged; + + private readonly List references = new List(); + + public ModSettingChangeTracker(IEnumerable mods) + { + foreach (var mod in mods) + { + foreach (var setting in mod.CreateSettingsControls().OfType()) + { + setting.SettingChanged += () => SettingChanged?.Invoke(mod); + references.Add(setting); + } + } + } + + public void Dispose() + { + foreach (var r in references) + r.Dispose(); + references.Clear(); + } + } } diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 44d908fc46..7966ec4240 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -18,7 +18,6 @@ using System.Threading; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; -using osu.Game.Overlays.Settings; using osu.Game.Rulesets; namespace osu.Game.Screens.Select.Details @@ -83,32 +82,22 @@ namespace osu.Game.Screens.Select.Details mods.BindValueChanged(modsChanged, true); } - private readonly List references = new List(); + private ModSettingChangeTracker settingChangeTracker; + private ScheduledDelegate debouncedStatisticsUpdate; private void modsChanged(ValueChangedEvent> mods) { - // TODO: find a more permanent solution for this if/when it is needed in other components. - // this is generating drawables for the only purpose of storing bindable references. - foreach (var r in references) - r.Dispose(); + settingChangeTracker?.Dispose(); - references.Clear(); - - ScheduledDelegate debounce = null; - - foreach (var mod in mods.NewValue.OfType()) + settingChangeTracker = new ModSettingChangeTracker(mods.NewValue); + settingChangeTracker.SettingChanged += m => { - foreach (var setting in mod.CreateSettingsControls().OfType()) - { - setting.SettingChanged += () => - { - debounce?.Cancel(); - debounce = Scheduler.AddDelayed(updateStatistics, 100); - }; + if (!(m is IApplicableToDifficulty)) + return; - references.Add(setting); - } - } + debouncedStatisticsUpdate?.Cancel(); + debouncedStatisticsUpdate = Scheduler.AddDelayed(updateStatistics, 100); + }; updateStatistics(); } From 7827e991b2dda30e265c3fc210e3828698d9d00b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 19:43:39 +0900 Subject: [PATCH 6532/6909] Also clear event on dispose --- osu.Game/Configuration/SettingSourceAttribute.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 00c322065a..04b8f8e962 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -162,6 +162,8 @@ namespace osu.Game.Configuration public void Dispose() { + SettingChanged = null; + foreach (var r in references) r.Dispose(); references.Clear(); From 822c66033f0d9cb2c2dbee0d138cdb67bc1fdd3c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 19:56:59 +0900 Subject: [PATCH 6533/6909] Add local-user freemod configuration --- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 9 +++++++- .../OnlinePlay/FreeModSelectOverlay.cs | 5 +++-- .../Multiplayer/MultiplayerMatchSongSelect.cs | 2 +- .../Multiplayer/MultiplayerMatchSubScreen.cs | 21 +++++++++++++++---- 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index 93fe693937..488c0659f4 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -51,6 +51,11 @@ namespace osu.Game.Overlays.Mods /// protected virtual bool Stacked => true; + /// + /// Whether configurable s can be configured by the local user. + /// + protected virtual bool AllowConfiguration => true; + [NotNull] private Func isValidMod = m => true; @@ -300,6 +305,7 @@ namespace osu.Game.Overlays.Mods Text = "Customisation", Action = () => ModSettingsContainer.ToggleVisibility(), Enabled = { Value = false }, + Alpha = AllowConfiguration ? 1 : 0, Origin = Anchor.CentreLeft, Anchor = Anchor.CentreLeft, }, @@ -509,7 +515,8 @@ namespace osu.Game.Overlays.Mods OnModSelected(selectedMod); - if (selectedMod.RequiresConfiguration) ModSettingsContainer.Show(); + if (selectedMod.RequiresConfiguration && AllowConfiguration) + ModSettingsContainer.Show(); } else { diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index 7bc226bb3f..ab7be13479 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -20,17 +20,18 @@ namespace osu.Game.Screens.OnlinePlay { protected override bool Stacked => false; + protected override bool AllowConfiguration => false; + public new Func IsValidMod { get => base.IsValidMod; - set => base.IsValidMod = m => m.HasImplementation && !m.RequiresConfiguration && !(m is ModAutoplay) && value(m); + set => base.IsValidMod = m => m.HasImplementation && !(m is ModAutoplay) && value(m); } public FreeModSelectOverlay() { IsValidMod = m => true; - CustomiseButton.Alpha = 0; MultiplierSection.Alpha = 0; DeselectAllButton.Alpha = 0; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index 84e8849726..f17d97c3fd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -59,6 +59,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override BeatmapDetailArea CreateBeatmapDetailArea() => new PlayBeatmapDetailArea(); - protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust) && !mod.RequiresConfiguration; + protected override bool IsValidFreeMod(Mod mod) => base.IsValidFreeMod(mod) && !(mod is ModTimeRamp) && !(mod is ModRateAdjust); } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 5f2f1366f7..7edba3231c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -12,6 +12,8 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; +using osu.Framework.Threading; +using osu.Game.Configuration; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; using osu.Game.Overlays.Mods; @@ -314,12 +316,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } + private ModSettingChangeTracker modSettingChangeTracker; + private ScheduledDelegate debouncedModSettingsUpdate; + private void onUserModsChanged(ValueChangedEvent> mods) { + modSettingChangeTracker?.Dispose(); + if (client.Room == null) return; client.ChangeUserMods(mods.NewValue); + + modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); + modSettingChangeTracker.SettingChanged += onModSettingsChanged; + } + + private void onModSettingsChanged(Mod mod) + { + // Debounce changes to mod settings so as to not thrash the network. + debouncedModSettingsUpdate?.Cancel(); + debouncedModSettingsUpdate = Scheduler.AddDelayed(() => client.ChangeUserMods(UserMods.Value), 500); } private void updateBeatmapAvailability(ValueChangedEvent availability) @@ -389,10 +406,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private class UserModSelectOverlay : LocalPlayerModSelectOverlay { - public UserModSelectOverlay() - { - CustomiseButton.Alpha = 0; - } } } } From 4a405bb8598a8dcdbc6772eaac801c9fa4907956 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:04:16 +0900 Subject: [PATCH 6534/6909] Split ModSettingChangeTracker into separate file --- .../Configuration/ModSettingChangeTracker.cs | 39 +++++++++++++++++++ .../Configuration/SettingSourceAttribute.cs | 29 -------------- 2 files changed, 39 insertions(+), 29 deletions(-) create mode 100644 osu.Game/Configuration/ModSettingChangeTracker.cs diff --git a/osu.Game/Configuration/ModSettingChangeTracker.cs b/osu.Game/Configuration/ModSettingChangeTracker.cs new file mode 100644 index 0000000000..f702a2fc22 --- /dev/null +++ b/osu.Game/Configuration/ModSettingChangeTracker.cs @@ -0,0 +1,39 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using System.Linq; +using osu.Game.Overlays.Settings; +using osu.Game.Rulesets.Mods; + +namespace osu.Game.Configuration +{ + public class ModSettingChangeTracker : IDisposable + { + public Action SettingChanged; + + private readonly List references = new List(); + + public ModSettingChangeTracker(IEnumerable mods) + { + foreach (var mod in mods) + { + foreach (var setting in mod.CreateSettingsControls().OfType()) + { + setting.SettingChanged += () => SettingChanged?.Invoke(mod); + references.Add(setting); + } + } + } + + public void Dispose() + { + SettingChanged = null; + + foreach (var r in references) + r.Dispose(); + references.Clear(); + } + } +} diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 04b8f8e962..50069be4b2 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -9,7 +9,6 @@ using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Game.Overlays.Settings; -using osu.Game.Rulesets.Mods; namespace osu.Game.Configuration { @@ -141,32 +140,4 @@ namespace osu.Game.Configuration return orderedRelative.Concat(unordered); } } - - public class ModSettingChangeTracker : IDisposable - { - public Action SettingChanged; - - private readonly List references = new List(); - - public ModSettingChangeTracker(IEnumerable mods) - { - foreach (var mod in mods) - { - foreach (var setting in mod.CreateSettingsControls().OfType()) - { - setting.SettingChanged += () => SettingChanged?.Invoke(mod); - references.Add(setting); - } - } - } - - public void Dispose() - { - SettingChanged = null; - - foreach (var r in references) - r.Dispose(); - references.Clear(); - } - } } From 6fff7c39daca9c27c39511b84223e7e98172325b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:09:45 +0900 Subject: [PATCH 6535/6909] Ensure tracker is disposed --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 ++ osu.Game/Screens/Select/Details/AdvancedStats.cs | 1 + 2 files changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 7edba3231c..59418cb348 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -402,6 +402,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (client != null) client.LoadRequested -= onLoadRequested; + + modSettingChangeTracker?.Dispose(); } private class UserModSelectOverlay : LocalPlayerModSelectOverlay diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 7966ec4240..4a03c5e614 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -162,6 +162,7 @@ namespace osu.Game.Screens.Select.Details protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); + settingChangeTracker?.Dispose(); starDifficultyCancellationSource?.Cancel(); } From 169acb42de35e88c8755a5140808cb3563cfb97d Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:11:36 +0900 Subject: [PATCH 6536/6909] Xmldoc + cleanup --- .../Configuration/ModSettingChangeTracker.cs | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/osu.Game/Configuration/ModSettingChangeTracker.cs b/osu.Game/Configuration/ModSettingChangeTracker.cs index f702a2fc22..e2ade7dc6a 100644 --- a/osu.Game/Configuration/ModSettingChangeTracker.cs +++ b/osu.Game/Configuration/ModSettingChangeTracker.cs @@ -9,12 +9,25 @@ using osu.Game.Rulesets.Mods; namespace osu.Game.Configuration { + /// + /// A helper class for tracking changes to the settings of a set of s. + /// + /// + /// Ensure to dispose when usage is finished. + /// public class ModSettingChangeTracker : IDisposable { + /// + /// Notifies that the setting of a has changed. + /// public Action SettingChanged; - private readonly List references = new List(); + private readonly List settings = new List(); + /// + /// Creates a new for a set of s. + /// + /// The set of s whose settings need to be tracked. public ModSettingChangeTracker(IEnumerable mods) { foreach (var mod in mods) @@ -22,7 +35,7 @@ namespace osu.Game.Configuration foreach (var setting in mod.CreateSettingsControls().OfType()) { setting.SettingChanged += () => SettingChanged?.Invoke(mod); - references.Add(setting); + settings.Add(setting); } } } @@ -31,9 +44,9 @@ namespace osu.Game.Configuration { SettingChanged = null; - foreach (var r in references) + foreach (var r in settings) r.Dispose(); - references.Clear(); + settings.Clear(); } } } From 86682cdb3480e711d2ecdeab04926c7488a17046 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:16:26 +0900 Subject: [PATCH 6537/6909] Add client/room null check --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 59418cb348..b7adb71e2f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -336,7 +336,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { // Debounce changes to mod settings so as to not thrash the network. debouncedModSettingsUpdate?.Cancel(); - debouncedModSettingsUpdate = Scheduler.AddDelayed(() => client.ChangeUserMods(UserMods.Value), 500); + debouncedModSettingsUpdate = Scheduler.AddDelayed(() => + { + if (client.Room == null) + return; + + client.ChangeUserMods(UserMods.Value); + }, 500); } private void updateBeatmapAvailability(ValueChangedEvent availability) From c458c4cfaee1d9a830e8ddafa512d30d35b5b4cf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:27:47 +0900 Subject: [PATCH 6538/6909] Fix unintended changes due to renaming or otherwise --- osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs | 2 +- osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs index 17b0b18b52..5470d0fcb4 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModClassic.cs @@ -62,7 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods { var osuRuleset = (DrawableOsuRuleset)drawableRuleset; - if (!ClassicNoteLock.Value) + if (ClassicNoteLock.Value) osuRuleset.Playfield.HitPolicy = new ObjectOrderedHitPolicy(); } diff --git a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs index 189ef2d76c..b1069149f3 100644 --- a/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs +++ b/osu.Game.Rulesets.Osu/UI/OsuPlayfield.cs @@ -53,7 +53,7 @@ namespace osu.Game.Rulesets.Osu.UI approachCircles = new ProxyContainer { RelativeSizeAxes = Axes.Both }, }; - HitPolicy = new ObjectOrderedHitPolicy(); + HitPolicy = new StartTimeOrderedHitPolicy(); var hitWindows = new OsuHitWindows(); foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => r > HitResult.None && hitWindows.IsHitResultAllowed(r))) From 321ca43b61a2a47857e04a117d622da6af385492 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 20:28:00 +0900 Subject: [PATCH 6539/6909] Update test --- .../TestSceneObjectOrderedHitPolicy.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs index 039a4f142f..77a68b714b 100644 --- a/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs +++ b/osu.Game.Rulesets.Osu.Tests/TestSceneObjectOrderedHitPolicy.cs @@ -248,7 +248,7 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[0], HitResult.Miss); addJudgementAssert(hitObjects[1], HitResult.Great); - addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.IgnoreHit); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); } @@ -291,7 +291,7 @@ namespace osu.Game.Rulesets.Osu.Tests addJudgementAssert(hitObjects[0], HitResult.Great); addJudgementAssert(hitObjects[1], HitResult.Great); - addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.IgnoreHit); + addJudgementAssert("slider head", () => ((Slider)hitObjects[1]).HeadCircle, HitResult.LargeTickHit); addJudgementAssert("slider tick", () => ((Slider)hitObjects[1]).NestedHitObjects[1] as SliderTick, HitResult.LargeTickHit); } From 4a391ce03d84adf76adb4214993c28e15cdebdbb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 21:24:41 +0900 Subject: [PATCH 6540/6909] Fix div-by-0 when 0 ticks are hit --- .../Objects/Drawables/DrawableSlider.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index 921139c4e9..d35da64ad5 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -263,16 +263,20 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables { int totalTicks = NestedHitObjects.Count; int hitTicks = NestedHitObjects.Count(h => h.IsHit); - double hitFraction = (double)totalTicks / hitTicks; if (hitTicks == totalTicks) r.Type = HitResult.Great; - else if (hitFraction >= 0.5) - r.Type = HitResult.Ok; - else if (hitFraction > 0) - r.Type = HitResult.Meh; - else + else if (hitTicks == 0) r.Type = HitResult.Miss; + else + { + double hitFraction = (double)totalTicks / hitTicks; + + if (hitFraction >= 0.5) + r.Type = HitResult.Ok; + else if (hitFraction > 0) + r.Type = HitResult.Meh; + } }); } From 1d425b83224ff7ac765b9af6d16389e637f1d840 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 21:25:31 +0900 Subject: [PATCH 6541/6909] Simplify case --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs index d35da64ad5..847011850c 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -271,11 +271,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables else { double hitFraction = (double)totalTicks / hitTicks; - - if (hitFraction >= 0.5) - r.Type = HitResult.Ok; - else if (hitFraction > 0) - r.Type = HitResult.Meh; + r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh; } }); } From bd2486e5a04bda5fb2a0ff32df6d2dce8681f2f3 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 21:27:12 +0900 Subject: [PATCH 6542/6909] Fix grammatical error in comment --- 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 847011850c..253b9800a6 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -258,7 +258,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables return; } - // Otherwise, if this slider is also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. + // Otherwise, if this slider also needs to be judged, apply judgement proportionally to the number of nested hitobjects hit. This is the classic osu!stable scoring. ApplyResult(r => { int totalTicks = NestedHitObjects.Count; From 20a6405fd20a6cef1251eebfd7435928344679a6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 22:06:19 +0900 Subject: [PATCH 6543/6909] Add explanatory comments + const --- .../Drawables/Connections/FollowPointConnection.cs | 6 ++++-- osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs | 14 ++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs index 40154ca84c..5541d0e790 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/Connections/FollowPointConnection.cs @@ -110,8 +110,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables.Connections double startTime = start.GetEndTime(); double duration = end.StartTime - startTime; - // For now, adjust the pre-empt for approach rates > 10. - double preempt = PREEMPT * Math.Min(1, start.TimePreempt / 450); + // Preempt time can go below 800ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. + // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear preempt function (see: OsuHitObject). + // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. + double preempt = PREEMPT * Math.Min(1, start.TimePreempt / OsuHitObject.PREEMPT_MIN); fadeOutTime = startTime + fraction * duration; fadeInTime = fadeOutTime - preempt; diff --git a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs index 6d28a576a4..22b64af3df 100644 --- a/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs +++ b/osu.Game.Rulesets.Osu/Objects/OsuHitObject.cs @@ -26,6 +26,11 @@ namespace osu.Game.Rulesets.Osu.Objects /// internal const float BASE_SCORING_DISTANCE = 100; + /// + /// Minimum preempt time at AR=10. + /// + public const double PREEMPT_MIN = 450; + public double TimePreempt = 600; public double TimeFadeIn = 400; @@ -113,8 +118,13 @@ namespace osu.Game.Rulesets.Osu.Objects { base.ApplyDefaultsToSelf(controlPointInfo, difficulty); - TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, 450); - TimeFadeIn = 400 * Math.Min(1, TimePreempt / 450); + TimePreempt = (float)BeatmapDifficulty.DifficultyRange(difficulty.ApproachRate, 1800, 1200, PREEMPT_MIN); + + // Preempt time can go below 450ms. Normally, this is achieved via the DT mod which uniformly speeds up all animations game wide regardless of AR. + // This uniform speedup is hard to match 1:1, however we can at least make AR>10 (via mods) feel good by extending the upper linear function above. + // Note that this doesn't exactly match the AR>10 visuals as they're classically known, but it feels good. + // This adjustment is necessary for AR>10, otherwise TimePreempt can become smaller leading to hitcircles not fully fading in. + TimeFadeIn = 400 * Math.Min(1, TimePreempt / PREEMPT_MIN); Scale = (1.0f - 0.7f * (difficulty.CircleSize - 5) / 5) / 2; } From 5d1d6ec1cbeef3ff0cf17b9e04d556e01772de1a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 22:09:24 +0900 Subject: [PATCH 6544/6909] Fix inverted calculation --- 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 253b9800a6..9122f347d0 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSlider.cs @@ -270,7 +270,7 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables r.Type = HitResult.Miss; else { - double hitFraction = (double)totalTicks / hitTicks; + double hitFraction = (double)hitTicks / totalTicks; r.Type = hitFraction >= 0.5 ? HitResult.Ok : HitResult.Meh; } }); From 07b661e28c2d2267dbe1a431f1a75e1d60fc6620 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 10 Feb 2021 23:44:06 +0900 Subject: [PATCH 6545/6909] Add Messagepack support for serialising unknown bindable types --- .../TestAPIModMessagePackSerialization.cs | 27 +++++++++++++++++++ .../API/ModSettingsDictionaryFormatter.cs | 8 ++++++ 2 files changed, 35 insertions(+) diff --git a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs index 4294f89397..74db477cfc 100644 --- a/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs +++ b/osu.Game.Tests/Online/TestAPIModMessagePackSerialization.cs @@ -68,6 +68,16 @@ namespace osu.Game.Tests.Online Assert.That(converted.FinalRate.Value, Is.EqualTo(0.25)); } + [Test] + public void TestDeserialiseEnumMod() + { + var apiMod = new APIMod(new TestModEnum { TestSetting = { Value = TestEnum.Value2 } }); + + var deserialized = MessagePackSerializer.Deserialize(MessagePackSerializer.Serialize(apiMod)); + + Assert.That(deserialized.Settings, Contains.Key("test_setting").With.ContainValue(1)); + } + private class TestRuleset : Ruleset { public override IEnumerable GetModsFor(ModType type) => new Mod[] @@ -135,5 +145,22 @@ namespace osu.Game.Tests.Online Value = true }; } + + private class TestModEnum : Mod + { + public override string Name => "Test Mod"; + public override string Acronym => "TM"; + public override double ScoreMultiplier => 1; + + [SettingSource("Test")] + public Bindable TestSetting { get; } = new Bindable(); + } + + private enum TestEnum + { + Value1 = 0, + Value2 = 1, + Value3 = 2 + } } } diff --git a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs index 99e87677fa..dd854acc32 100644 --- a/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs +++ b/osu.Game/Online/API/ModSettingsDictionaryFormatter.cs @@ -3,6 +3,7 @@ using System.Buffers; using System.Collections.Generic; +using System.Diagnostics; using System.Text; using MessagePack; using MessagePack.Formatters; @@ -41,6 +42,13 @@ namespace osu.Game.Online.API primitiveFormatter.Serialize(ref writer, b.Value, options); break; + case IBindable u: + // A mod with unknown (e.g. enum) generic type. + var valueMethod = u.GetType().GetProperty(nameof(IBindable.Value)); + Debug.Assert(valueMethod != null); + primitiveFormatter.Serialize(ref writer, valueMethod.GetValue(u), options); + break; + default: // fall back for non-bindable cases. primitiveFormatter.Serialize(ref writer, kvp.Value, options); From 97e799a26bc22316442500aefc79fad3f9c6d646 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Wed, 10 Feb 2021 18:10:31 +0100 Subject: [PATCH 6546/6909] add more MIME types to Android share intent filter --- 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 48b059b482..d3bb97973b 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -21,7 +21,7 @@ namespace osu.Android [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeTypes = new[] { "application/x-osu-beatmap", "application/x-osu-skin" })] - [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/x-osu-beatmap", "application/x-osu-skin", "application/zip", "application/octet-stream", "application/x-zip", "application/x-zip-compressed" })] + [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity { From fed2dea7353a380103772ba99edb4882a6639d73 Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Wed, 10 Feb 2021 18:13:59 +0100 Subject: [PATCH 6547/6909] remove unnecesary view intent filter --- osu.Android/OsuGameActivity.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index d3bb97973b..ad929bbac3 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -20,7 +20,6 @@ namespace osu.Android [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] - [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataMimeTypes = new[] { "application/x-osu-beatmap", "application/x-osu-skin" })] [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryBrowsable, Intent.CategoryDefault }, DataSchemes = new[] { "osu", "osump" })] public class OsuGameActivity : AndroidGameActivity From 63e6ec1c9fa3b0ecb12ecbd2f389edc0042de939 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 20:06:55 +0900 Subject: [PATCH 6548/6909] Add audio feedback for OSD value changes --- osu.Game/Overlays/OSD/TrackedSettingToast.cs | 46 ++++++++++++++++++-- 1 file changed, 43 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index 8e8a99a0a7..c5a289a85c 100644 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Configuration.Tracking; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; @@ -19,6 +21,15 @@ namespace osu.Game.Overlays.OSD { private const int lights_bottom_margin = 40; + private readonly int optionCount; + private readonly int selectedOption = -1; + + private SampleChannel sampleOn; + private SampleChannel sampleOff; + private SampleChannel sampleChange; + + private bool playedSample; + public TrackedSettingToast(SettingDescription description) : base(description.Name, description.Value, description.Shortcut) { @@ -46,9 +57,6 @@ namespace osu.Game.Overlays.OSD } }; - int optionCount = 0; - int selectedOption = -1; - switch (description.RawValue) { case bool val: @@ -69,6 +77,38 @@ namespace osu.Game.Overlays.OSD optionLights.Add(new OptionLight { Glowing = i == selectedOption }); } + protected override void Update() + { + base.Update(); + + if (playedSample) return; + + if (optionCount == 1) + { + if (selectedOption == 0) + sampleOn?.Play(); + else + sampleOff?.Play(); + } + else + { + if (sampleChange == null) return; + + sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f; + sampleChange.Play(); + } + + playedSample = true; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + sampleOn = audio.Samples.Get("UI/osd-on"); + sampleOff = audio.Samples.Get("UI/osd-off"); + sampleChange = audio.Samples.Get("UI/osd-change"); + } + private class OptionLight : Container { private Color4 glowingColour, idleColour; From c12c09ec4d497ad7e52a3c4b625e3793dc23d8a1 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 11 Feb 2021 09:57:14 +0900 Subject: [PATCH 6549/6909] Move logic to LoadComplete instead --- osu.Game/Overlays/OSD/TrackedSettingToast.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/osu.Game/Overlays/OSD/TrackedSettingToast.cs b/osu.Game/Overlays/OSD/TrackedSettingToast.cs index c5a289a85c..d61180baa2 100644 --- a/osu.Game/Overlays/OSD/TrackedSettingToast.cs +++ b/osu.Game/Overlays/OSD/TrackedSettingToast.cs @@ -28,8 +28,6 @@ namespace osu.Game.Overlays.OSD private SampleChannel sampleOff; private SampleChannel sampleChange; - private bool playedSample; - public TrackedSettingToast(SettingDescription description) : base(description.Name, description.Value, description.Shortcut) { @@ -77,11 +75,9 @@ namespace osu.Game.Overlays.OSD optionLights.Add(new OptionLight { Glowing = i == selectedOption }); } - protected override void Update() + protected override void LoadComplete() { - base.Update(); - - if (playedSample) return; + base.LoadComplete(); if (optionCount == 1) { @@ -97,8 +93,6 @@ namespace osu.Game.Overlays.OSD sampleChange.Frequency.Value = 1 + (double)selectedOption / (optionCount - 1) * 0.25f; sampleChange.Play(); } - - playedSample = true; } [BackgroundDependencyLoader] From a1ae739a6235f72995ec460d93874eac2a504aa6 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Fri, 15 Jan 2021 15:21:50 +0900 Subject: [PATCH 6550/6909] Add support for Notification sounds --- .../Overlays/Notifications/Notification.cs | 24 ++++++++++++++++++- .../Notifications/NotificationSection.cs | 7 +++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 2dc6b39a92..86e409d0f6 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -3,6 +3,8 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Audio.Sample; using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; @@ -40,6 +42,11 @@ namespace osu.Game.Overlays.Notifications /// public virtual bool DisplayOnTop => true; + private SampleChannel samplePopIn; + private SampleChannel samplePopOut; + protected virtual string PopInSampleName => "UI/notification-pop-in"; + protected virtual string PopOutSampleName => "UI/overlay-pop-out"; // TODO: replace with a unique sample? + protected NotificationLight Light; private readonly CloseButton closeButton; protected Container IconContent; @@ -120,6 +127,13 @@ namespace osu.Game.Overlays.Notifications }); } + [BackgroundDependencyLoader] + private void load(AudioManager audio) + { + samplePopIn = audio.Samples.Get(PopInSampleName); + samplePopOut = audio.Samples.Get(PopOutSampleName); + } + protected override bool OnHover(HoverEvent e) { closeButton.FadeIn(75); @@ -143,6 +157,9 @@ namespace osu.Game.Overlays.Notifications protected override void LoadComplete() { base.LoadComplete(); + + samplePopIn?.Play(); + this.FadeInFromZero(200); NotificationContent.MoveToX(DrawSize.X); NotificationContent.MoveToX(0, 500, Easing.OutQuint); @@ -150,12 +167,17 @@ namespace osu.Game.Overlays.Notifications public bool WasClosed; - public virtual void Close() + public virtual void Close() => Close(true); + + public virtual void Close(bool playSound) { if (WasClosed) return; WasClosed = true; + if (playSound) + samplePopOut?.Play(); + Closed?.Invoke(); this.FadeOut(100); Expire(); diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index c2a958b65e..c8cee22370 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -109,7 +109,12 @@ namespace osu.Game.Overlays.Notifications private void clearAll() { - notifications.Children.ForEach(c => c.Close()); + bool playSound = true; + notifications.Children.ForEach(c => + { + c.Close(playSound); + playSound = false; + }); } protected override void Update() From 2ee634d173647a97fa9e5ef40bfe1e262920162b Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 21 Jan 2021 18:42:53 +0900 Subject: [PATCH 6551/6909] Create subclass for "Error" notifications to allow them to have a unique pop-in sound --- .../TestSceneNotificationOverlay.cs | 20 ++++++++++++++++++- osu.Game/OsuGame.cs | 2 +- .../Notifications/SimpleErrorNotification.cs | 17 ++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 osu.Game/Overlays/Notifications/SimpleErrorNotification.cs diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs index 43ba23e6c6..d0f6f3fe47 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneNotificationOverlay.cs @@ -105,6 +105,15 @@ namespace osu.Game.Tests.Visual.UserInterface checkDisplayedCount(3); } + [Test] + public void TestError() + { + setState(Visibility.Visible); + AddStep(@"error #1", sendErrorNotification); + AddAssert("Is visible", () => notificationOverlay.State.Value == Visibility.Visible); + checkDisplayedCount(1); + } + [Test] public void TestSpam() { @@ -179,7 +188,7 @@ namespace osu.Game.Tests.Visual.UserInterface private void sendBarrage() { - switch (RNG.Next(0, 4)) + switch (RNG.Next(0, 5)) { case 0: sendHelloNotification(); @@ -196,6 +205,10 @@ namespace osu.Game.Tests.Visual.UserInterface case 3: sendDownloadProgress(); break; + + case 4: + sendErrorNotification(); + break; } } @@ -214,6 +227,11 @@ namespace osu.Game.Tests.Visual.UserInterface notificationOverlay.Post(new BackgroundNotification { Text = @"Welcome to osu!. Enjoy your stay!" }); } + private void sendErrorNotification() + { + notificationOverlay.Post(new SimpleErrorNotification { Text = @"Rut roh!. Something went wrong!" }); + } + private void sendManyNotifications() { for (int i = 0; i < 10; i++) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1a1f7bd233..0dc63dcd4b 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -778,7 +778,7 @@ namespace osu.Game if (recentLogCount < short_term_display_limit) { - Schedule(() => notifications.Post(new SimpleNotification + Schedule(() => notifications.Post(new SimpleErrorNotification { Icon = entry.Level == LogLevel.Important ? FontAwesome.Solid.ExclamationCircle : FontAwesome.Solid.Bomb, Text = entry.Message.Truncate(256) + (entry.Exception != null && IsDeployedBuild ? "\n\nThis error has been automatically reported to the devs." : string.Empty), diff --git a/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs b/osu.Game/Overlays/Notifications/SimpleErrorNotification.cs new file mode 100644 index 0000000000..13c9c5a02d --- /dev/null +++ b/osu.Game/Overlays/Notifications/SimpleErrorNotification.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; + +namespace osu.Game.Overlays.Notifications +{ + public class SimpleErrorNotification : SimpleNotification + { + protected override string PopInSampleName => "UI/error-notification-pop-in"; + + public SimpleErrorNotification() + { + Icon = FontAwesome.Solid.Bomb; + } + } +} From 72562070bcea0718bd6ddce220e354d934b6a987 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 14:18:00 +0900 Subject: [PATCH 6552/6909] Remove second overload of Close (makes the call structure hard to follow / invoke correctly) --- osu.Game/Overlays/Notifications/Notification.cs | 6 ++---- osu.Game/Overlays/Notifications/ProgressNotification.cs | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/osu.Game/Overlays/Notifications/Notification.cs b/osu.Game/Overlays/Notifications/Notification.cs index 86e409d0f6..daf931bc24 100644 --- a/osu.Game/Overlays/Notifications/Notification.cs +++ b/osu.Game/Overlays/Notifications/Notification.cs @@ -114,7 +114,7 @@ namespace osu.Game.Overlays.Notifications closeButton = new CloseButton { Alpha = 0, - Action = Close, + Action = () => Close(), Anchor = Anchor.CentreRight, Origin = Anchor.CentreRight, Margin = new MarginPadding @@ -167,9 +167,7 @@ namespace osu.Game.Overlays.Notifications public bool WasClosed; - public virtual void Close() => Close(true); - - public virtual void Close(bool playSound) + public virtual void Close(bool playSound = true) { if (WasClosed) return; diff --git a/osu.Game/Overlays/Notifications/ProgressNotification.cs b/osu.Game/Overlays/Notifications/ProgressNotification.cs index 3105ecd742..703c14af2b 100644 --- a/osu.Game/Overlays/Notifications/ProgressNotification.cs +++ b/osu.Game/Overlays/Notifications/ProgressNotification.cs @@ -150,12 +150,12 @@ namespace osu.Game.Overlays.Notifications colourCancelled = colours.Red; } - public override void Close() + public override void Close(bool playSound = true) { switch (State) { case ProgressNotificationState.Cancelled: - base.Close(); + base.Close(playSound); break; case ProgressNotificationState.Active: From 896f318b56866306e455d92c4443c252723d75f3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 14:18:40 +0900 Subject: [PATCH 6553/6909] Rename variable for clarity --- osu.Game/Overlays/Notifications/NotificationSection.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index c8cee22370..38ba712254 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -109,11 +109,11 @@ namespace osu.Game.Overlays.Notifications private void clearAll() { - bool playSound = true; + bool first = true; notifications.Children.ForEach(c => { - c.Close(playSound); - playSound = false; + c.Close(first); + first = false; }); } From 800f12a358b234f7d3409e843c5b913cc563f4b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 14:19:48 +0900 Subject: [PATCH 6554/6909] 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 7060e88026..a522a5f43d 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 f866b232d8..f69613cfd3 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 22d104f2e1..1c602e1584 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From c8899aff92f793ee03bf61ab026263d2e6bd4dd6 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Thu, 11 Feb 2021 14:36:41 +0900 Subject: [PATCH 6555/6909] Prevent the default on-click sample from playing for OsuCheckbox --- osu.Game/Graphics/UserInterface/OsuCheckbox.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs index f6effa0834..0d00bc0dce 100644 --- a/osu.Game/Graphics/UserInterface/OsuCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuCheckbox.cs @@ -64,7 +64,7 @@ namespace osu.Game.Graphics.UserInterface RelativeSizeAxes = Axes.X, }, Nub = new Nub(), - new HoverClickSounds() + new HoverSounds() }; if (nubOnRight) From f21a3c0c48abe02b8b949bd0881dfe02f5a143cc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 14:50:55 +0900 Subject: [PATCH 6556/6909] Decrease the game-wide track playback volume to give samples some head-room --- osu.Game/OsuGame.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1a1f7bd233..b77097a3f0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -174,6 +174,8 @@ namespace osu.Game protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); + private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(0.5f); + [BackgroundDependencyLoader] private void load() { @@ -230,6 +232,11 @@ namespace osu.Game Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade); + // drop track volume game-wide to leave some head-room for UI effects / samples. + // this means that for the time being, gameplay sample playback is louder relative to the audio track, compared to stable. + // we may want to revisit this if users notice or complain about the difference (consider this a bit of a trial). + Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust); + SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); } From eaa7b4cb93cdad7a27bc8958cacd860a7f2e3b10 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 14:54:50 +0900 Subject: [PATCH 6557/6909] Rename second usage variable name to match --- osu.Game/Screens/Select/Details/AdvancedStats.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index 4a03c5e614..ab4f3f4796 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -82,15 +82,15 @@ namespace osu.Game.Screens.Select.Details mods.BindValueChanged(modsChanged, true); } - private ModSettingChangeTracker settingChangeTracker; + private ModSettingChangeTracker modSettingChangeTracker; private ScheduledDelegate debouncedStatisticsUpdate; private void modsChanged(ValueChangedEvent> mods) { - settingChangeTracker?.Dispose(); + modSettingChangeTracker?.Dispose(); - settingChangeTracker = new ModSettingChangeTracker(mods.NewValue); - settingChangeTracker.SettingChanged += m => + modSettingChangeTracker = new ModSettingChangeTracker(mods.NewValue); + modSettingChangeTracker.SettingChanged += m => { if (!(m is IApplicableToDifficulty)) return; @@ -162,7 +162,7 @@ namespace osu.Game.Screens.Select.Details protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); - settingChangeTracker?.Dispose(); + modSettingChangeTracker?.Dispose(); starDifficultyCancellationSource?.Cancel(); } From df7aaa5c816e6064920c32028bb3689e31174c81 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 15:02:34 +0900 Subject: [PATCH 6558/6909] Move implementation to OsuGameBase to ensure it applies to test scenes This also removed a previous attempt at the same thing, which happened to not be applying due to the reference to the applied bindable not being held. Whoops. --- osu.Game/OsuGame.cs | 7 ------- osu.Game/OsuGameBase.cs | 9 ++++++--- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b77097a3f0..1a1f7bd233 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -174,8 +174,6 @@ namespace osu.Game protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) => dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); - private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(0.5f); - [BackgroundDependencyLoader] private void load() { @@ -232,11 +230,6 @@ namespace osu.Game Audio.AddAdjustment(AdjustableProperty.Volume, inactiveVolumeFade); - // drop track volume game-wide to leave some head-room for UI effects / samples. - // this means that for the time being, gameplay sample playback is louder relative to the audio track, compared to stable. - // we may want to revisit this if users notice or complain about the difference (consider this a bit of a trial). - Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust); - SelectedMods.BindValueChanged(modsChanged); Beatmap.BindValueChanged(beatmapChanged, true); } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index d3936ed27e..a1b66ba9c0 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -155,6 +155,8 @@ namespace osu.Game protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); + private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(0.5f); + [BackgroundDependencyLoader] private void load() { @@ -278,9 +280,10 @@ namespace osu.Game RegisterImportHandler(ScoreManager); RegisterImportHandler(SkinManager); - // tracks play so loud our samples can't keep up. - // this adds a global reduction of track volume for the time being. - Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, new BindableDouble(0.8)); + // drop track volume game-wide to leave some head-room for UI effects / samples. + // this means that for the time being, gameplay sample playback is louder relative to the audio track, compared to stable. + // we may want to revisit this if users notice or complain about the difference (consider this a bit of a trial). + Audio.Tracks.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust); Beatmap = new NonNullableBindable(defaultBeatmap); From 21f66a19fd21c492f8340cc70ef2934ff1d1033e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 15:55:08 +0900 Subject: [PATCH 6559/6909] Make server authoritative in which mods the client should be using when gameplay starts --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 8 +++--- .../Multiplayer/MultiplayerMatchSubScreen.cs | 25 +++++++++++++++++++ 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index f3972ab7f9..6d06e638c8 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.OnlinePlay.Match managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); - UserMods.BindValueChanged(_ => updateMods()); + UserMods.BindValueChanged(_ => UpdateMods()); } public override void OnEntering(IScreen last) @@ -97,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.OnResuming(last); beginHandlingTrack(); - updateMods(); + UpdateMods(); } public override bool OnExiting(IScreen next) @@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Match .Where(m => SelectedItem.Value.AllowedMods.Any(a => m.GetType() == a.GetType())) .ToList(); - updateMods(); + UpdateMods(); Ruleset.Value = SelectedItem.Value.Ruleset.Value; } @@ -145,7 +145,7 @@ namespace osu.Game.Screens.OnlinePlay.Match Beatmap.Value = beatmapManager.GetWorkingBeatmap(localBeatmap); } - private void updateMods() + protected virtual void UpdateMods() { if (SelectedItem.Value == null) return; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 5f2f1366f7..3dff291858 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -271,6 +271,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer UserMods.BindValueChanged(onUserModsChanged); client.LoadRequested += onLoadRequested; + client.RoomUpdated += onRoomUpdated; isConnected = client.IsConnected.GetBoundCopy(); isConnected.BindValueChanged(connected => @@ -367,6 +368,27 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer } } + private void onRoomUpdated() + { + UpdateMods(); // user mods may have changed. + } + + protected override void UpdateMods() + { + if (SelectedItem.Value == null || client.LocalUser == null) + return; + + // update local mods based on room's reported status for the local user (omitting the base call implementation). + // this makes the server authoritative, and avoids the local user potentially settings mods that the server is not aware of (ie. if the match was started during the selection being changed). + var localUserMods = client.LocalUser.Mods.ToList(); + + Schedule(() => + { + var ruleset = Ruleset.Value.CreateInstance(); + Mods.Value = localUserMods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); + }); + } + private void onLoadRequested() { Debug.Assert(client.Room != null); @@ -384,7 +406,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer base.Dispose(isDisposing); if (client != null) + { + client.RoomUpdated -= onRoomUpdated; client.LoadRequested -= onLoadRequested; + } } private class UserModSelectOverlay : LocalPlayerModSelectOverlay From 549e7520c520cb9b9e45773ad41a5d25b0d2fc7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 16:00:26 +0900 Subject: [PATCH 6560/6909] Move scheduler logic to client callback rather than inside the update method --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 3dff291858..1599936a51 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -281,6 +281,17 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, true); } + protected override void UpdateMods() + { + if (SelectedItem.Value == null || client.LocalUser == null) + return; + + // update local mods based on room's reported status for the local user (omitting the base call implementation). + // this makes the server authoritative, and avoids the local user potentially settings mods that the server is not aware of (ie. if the match was started during the selection being changed). + var ruleset = Ruleset.Value.CreateInstance(); + Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); + } + public override bool OnBackButton() { if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible) @@ -370,23 +381,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onRoomUpdated() { - UpdateMods(); // user mods may have changed. - } - - protected override void UpdateMods() - { - if (SelectedItem.Value == null || client.LocalUser == null) - return; - - // update local mods based on room's reported status for the local user (omitting the base call implementation). - // this makes the server authoritative, and avoids the local user potentially settings mods that the server is not aware of (ie. if the match was started during the selection being changed). - var localUserMods = client.LocalUser.Mods.ToList(); - - Schedule(() => - { - var ruleset = Ruleset.Value.CreateInstance(); - Mods.Value = localUserMods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); - }); + // user mods may have changed. + Scheduler.AddOnce(UpdateMods); } private void onLoadRequested() From 889a99c49cd153b67e8ad7bc04b77517fbd90ef4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 16:00:35 +0900 Subject: [PATCH 6561/6909] Use AddOnce everywhere to reduce potential call count --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 6d06e638c8..3668743720 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -78,7 +78,7 @@ namespace osu.Game.Screens.OnlinePlay.Match managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); - UserMods.BindValueChanged(_ => UpdateMods()); + UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods)); } public override void OnEntering(IScreen last) @@ -97,7 +97,7 @@ namespace osu.Game.Screens.OnlinePlay.Match { base.OnResuming(last); beginHandlingTrack(); - UpdateMods(); + Scheduler.AddOnce(UpdateMods); } public override bool OnExiting(IScreen next) From dddd776802ad1f77f085c9862c317f93a3d1b191 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 16:38:17 +0900 Subject: [PATCH 6562/6909] Add the ability for settings items to have tooltips --- osu.Game/Configuration/SettingSourceAttribute.cs | 6 ++++++ osu.Game/Overlays/Settings/SettingsItem.cs | 4 +++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 50069be4b2..70d67aaaa0 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -57,6 +57,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, + TooltipText = attr.Description, Current = bNumber, KeyboardStep = 0.1f, }; @@ -67,6 +68,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, + TooltipText = attr.Description, Current = bNumber, KeyboardStep = 0.1f, }; @@ -77,6 +79,7 @@ namespace osu.Game.Configuration yield return new SettingsSlider { LabelText = attr.Label, + TooltipText = attr.Description, Current = bNumber }; @@ -86,6 +89,7 @@ namespace osu.Game.Configuration yield return new SettingsCheckbox { LabelText = attr.Label, + TooltipText = attr.Description, Current = bBool }; @@ -95,6 +99,7 @@ namespace osu.Game.Configuration yield return new SettingsTextBox { LabelText = attr.Label, + TooltipText = attr.Description, Current = bString }; @@ -105,6 +110,7 @@ namespace osu.Game.Configuration var dropdown = (Drawable)Activator.CreateInstance(dropdownType); dropdownType.GetProperty(nameof(SettingsDropdown.LabelText))?.SetValue(dropdown, attr.Label); + dropdownType.GetProperty(nameof(SettingsDropdown.TooltipText))?.SetValue(dropdown, attr.Description); dropdownType.GetProperty(nameof(SettingsDropdown.Current))?.SetValue(dropdown, bindable); yield return dropdown; diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 278479e04f..27232d0a49 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -21,7 +21,7 @@ using osuTK; namespace osu.Game.Overlays.Settings { - public abstract class SettingsItem : Container, IFilterable, ISettingsItem, IHasCurrentValue + public abstract class SettingsItem : Container, IFilterable, ISettingsItem, IHasCurrentValue, IHasTooltip { protected abstract Drawable CreateControl(); @@ -214,5 +214,7 @@ namespace osu.Game.Overlays.Settings this.FadeColour(bindable.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint); } } + + public string TooltipText { get; set; } } } From 9fb41dc0b6f416e2232cffbeb576f0d9cdcf5955 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 16:41:21 +0900 Subject: [PATCH 6563/6909] Move property to a better place in the class --- osu.Game/Overlays/Settings/SettingsItem.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 27232d0a49..af225889da 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -37,6 +37,8 @@ namespace osu.Game.Overlays.Settings public bool ShowsDefaultIndicator = true; + public string TooltipText { get; set; } + public virtual string LabelText { get => labelText?.Text ?? string.Empty; @@ -214,7 +216,5 @@ namespace osu.Game.Overlays.Settings this.FadeColour(bindable.Disabled ? Color4.Gray : buttonColour, 200, Easing.OutQuint); } } - - public string TooltipText { get; set; } } } From 5fb99fdc52e12845dadc48a57f043e0da4696bbd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Feb 2021 10:49:16 +0300 Subject: [PATCH 6564/6909] Rename some members and extract connection closure to separate method --- osu.Game/Online/HubClientConnector.cs | 35 ++++++++++--------- .../Online/Multiplayer/MultiplayerClient.cs | 2 +- .../Spectator/SpectatorStreamingClient.cs | 2 +- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index b740aabb92..65285882d9 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -25,7 +25,7 @@ namespace osu.Game.Online /// /// Invoked whenever a new hub connection is built. /// - public Action? OnNewConnection; + public Action? ConfigureConnection; private readonly string clientName; private readonly string endpoint; @@ -106,7 +106,7 @@ namespace osu.Game.Online try { // importantly, rebuild the connection each attempt to get an updated access token. - CurrentConnection = createConnection(cancellationToken); + CurrentConnection = buildConnection(cancellationToken); await CurrentConnection.StartAsync(cancellationToken); @@ -134,7 +134,7 @@ namespace osu.Game.Online } } - private HubConnection createConnection(CancellationToken cancellationToken) + private HubConnection buildConnection(CancellationToken cancellationToken) { Debug.Assert(api != null); @@ -152,24 +152,25 @@ namespace osu.Game.Online var newConnection = builder.Build(); - OnNewConnection?.Invoke(newConnection); - - newConnection.Closed += ex => - { - isConnected.Value = false; - - Logger.Log(ex != null ? $"{clientName} lost connection: {ex}" : $"{clientName} disconnected", LoggingTarget.Network); - - // make sure a disconnect wasn't triggered (and this is still the active connection). - if (!cancellationToken.IsCancellationRequested) - Task.Run(connect, default); - - return Task.CompletedTask; - }; + ConfigureConnection?.Invoke(newConnection); + newConnection.Closed += ex => onConnectionClosed(ex, cancellationToken); return newConnection; } + private Task onConnectionClosed(Exception? ex, CancellationToken cancellationToken) + { + isConnected.Value = false; + + Logger.Log(ex != null ? $"{clientName} lost connection: {ex}" : $"{clientName} disconnected", LoggingTarget.Network); + + // make sure a disconnect wasn't triggered (and this is still the active connection). + if (!cancellationToken.IsCancellationRequested) + Task.Run(connect, default); + + return Task.CompletedTask; + } + private async Task disconnect(bool takeLock) { cancelExistingConnect(); diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index f025a5b429..ba2a8d7246 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -33,7 +33,7 @@ namespace osu.Game.Online.Multiplayer { connector = new HubClientConnector(nameof(MultiplayerClient), endpoint, api) { - OnNewConnection = connection => + ConfigureConnection = connection => { // this is kind of SILLY // https://github.com/dotnet/aspnetcore/issues/15198 diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 4ef59b5e47..532f717f2c 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -86,7 +86,7 @@ namespace osu.Game.Online.Spectator private void load(IAPIProvider api) { connector = CreateConnector(nameof(SpectatorStreamingClient), endpoint, api); - connector.OnNewConnection = connection => + connector.ConfigureConnection = connection => { // until strong typed client support is added, each method must be manually bound // (see https://github.com/dotnet/aspnetcore/issues/15198) From 18acd7f08032369f3c87d2327a3e1197b3c09d76 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Feb 2021 10:51:04 +0300 Subject: [PATCH 6565/6909] Apply documentation suggestions Co-authored-by: Dean Herbert --- osu.Game/Online/HubClientConnector.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index 65285882d9..71d9df84c4 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -18,7 +18,7 @@ using osu.Game.Online.API; namespace osu.Game.Online { /// - /// A component that maintains over a hub connection between client and server. + /// A component that manages the life cycle of a connection to a SignalR Hub. /// public class HubClientConnector : IDisposable { @@ -52,7 +52,7 @@ namespace osu.Game.Online /// /// The name of the client this connector connects for, used for logging. /// The endpoint to the hub. - /// The API provider for listening to state changes, or null to not listen. + /// An API provider used to react to connection state changes, or null to not establish connection at all (for testing purposes). public HubClientConnector(string clientName, string endpoint, IAPIProvider? api) { this.clientName = clientName; From db79080bc4deebef8277866157d3067b024ee533 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 17:14:49 +0900 Subject: [PATCH 6566/6909] Fix GetNodeSamples potentially returning a live reference and overwriting existing samples --- osu.Game.Rulesets.Osu/Objects/Slider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Slider.cs b/osu.Game.Rulesets.Osu/Objects/Slider.cs index 1670df24a8..6fb36e80bc 100644 --- a/osu.Game.Rulesets.Osu/Objects/Slider.cs +++ b/osu.Game.Rulesets.Osu/Objects/Slider.cs @@ -140,7 +140,8 @@ namespace osu.Game.Rulesets.Osu.Objects // The samples should be attached to the slider tail, however this can only be done after LegacyLastTick is removed otherwise they would play earlier than they're intended to. // For now, the samples are attached to and played by the slider itself at the correct end time. - Samples = this.GetNodeSamples(repeatCount + 1); + // ToArray call is required as GetNodeSamples may fallback to Samples itself (without it it will get cleared due to the list reference being live). + Samples = this.GetNodeSamples(repeatCount + 1).ToArray(); } protected override void CreateNestedHitObjects(CancellationToken cancellationToken) From e9730d4782e9e8716d61bcf3b7e91c5cb2d1573d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 17:16:17 +0900 Subject: [PATCH 6567/6909] Move default sample addition to inside PlacementBlueprint This isn't actually required to fix the behaviour but it does feel like a better place to put this logic. --- osu.Game/Rulesets/Edit/PlacementBlueprint.cs | 4 ++++ .../Edit/Compose/Components/ComposeBlueprintContainer.cs | 3 --- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs index c0eb891f5e..bfff93e7c5 100644 --- a/osu.Game/Rulesets/Edit/PlacementBlueprint.cs +++ b/osu.Game/Rulesets/Edit/PlacementBlueprint.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Events; +using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Rulesets.Objects; @@ -45,6 +46,9 @@ namespace osu.Game.Rulesets.Edit { HitObject = hitObject; + // adding the default hit sample should be the case regardless of the ruleset. + HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); + RelativeSizeAxes = Axes.Both; // This is required to allow the blueprint's position to be updated via OnMouseMove/Handle diff --git a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs index c09b935f28..79f457c050 100644 --- a/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/ComposeBlueprintContainer.cs @@ -211,9 +211,6 @@ namespace osu.Game.Screens.Edit.Compose.Components if (blueprint != null) { - // doing this post-creations as adding the default hit sample should be the case regardless of the ruleset. - blueprint.HitObject.Samples.Add(new HitSampleInfo(HitSampleInfo.HIT_NORMAL)); - placementBlueprintContainer.Child = currentPlacement = blueprint; // Fixes a 1-frame position discrepancy due to the first mouse move event happening in the next frame From f84ea3063768bac3d8850c62f187f681ece7bbba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 17:47:29 +0900 Subject: [PATCH 6568/6909] Expose Mods in DrawableRuleset to avoid using external DI --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 3 +-- osu.Game/Rulesets/Mods/ModAutoplay.cs | 3 +-- osu.Game/Rulesets/Mods/ModCinema.cs | 4 +--- osu.Game/Rulesets/UI/DrawableRuleset.cs | 4 ++-- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 59a5295858..77de0cb45b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -62,8 +62,7 @@ namespace osu.Game.Rulesets.Osu.Mods inputManager.AllowUserCursorMovement = false; // Generate the replay frames the cursor should follow - var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); - replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, mods).Generate().Frames.Cast().ToList(); + replayFrames = new OsuAutoGenerator(drawableRuleset.Beatmap, drawableRuleset.Mods).Generate().Frames.Cast().ToList(); } } } diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index 748c7272f4..d1d23def67 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -18,8 +18,7 @@ namespace osu.Game.Rulesets.Mods { public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); - drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, mods)); + drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); } } diff --git a/osu.Game/Rulesets/Mods/ModCinema.cs b/osu.Game/Rulesets/Mods/ModCinema.cs index 16e6400f23..eb0473016a 100644 --- a/osu.Game/Rulesets/Mods/ModCinema.cs +++ b/osu.Game/Rulesets/Mods/ModCinema.cs @@ -1,7 +1,6 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System.Collections.Generic; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Objects; @@ -15,8 +14,7 @@ namespace osu.Game.Rulesets.Mods { public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { - var mods = (IReadOnlyList)drawableRuleset.Dependencies.Get(typeof(IReadOnlyList)); - drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, mods)); + drawableRuleset.SetReplayScore(CreateReplayScore(drawableRuleset.Beatmap, drawableRuleset.Mods)); // AlwaysPresent required for hitsounds drawableRuleset.Playfield.AlwaysPresent = true; diff --git a/osu.Game/Rulesets/UI/DrawableRuleset.cs b/osu.Game/Rulesets/UI/DrawableRuleset.cs index 6940e43e5b..ca27e6b21a 100644 --- a/osu.Game/Rulesets/UI/DrawableRuleset.cs +++ b/osu.Game/Rulesets/UI/DrawableRuleset.cs @@ -92,7 +92,7 @@ namespace osu.Game.Rulesets.UI protected IRulesetConfigManager Config { get; private set; } [Cached(typeof(IReadOnlyList))] - protected override IReadOnlyList Mods { get; } + public sealed override IReadOnlyList Mods { get; } private FrameStabilityContainer frameStabilityContainer; @@ -434,7 +434,7 @@ namespace osu.Game.Rulesets.UI /// /// The mods which are to be applied. /// - protected abstract IReadOnlyList Mods { get; } + public abstract IReadOnlyList Mods { get; } /// ~ /// The associated ruleset. From ffd3caacb5167bf10d49587ef15cde22ae5896f7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Feb 2021 17:57:50 +0900 Subject: [PATCH 6569/6909] 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 a522a5f43d..d88a11257d 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 f69613cfd3..d68a8a515c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -30,7 +30,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 1c602e1584..87ebd41fee 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -71,7 +71,7 @@ - + From d3c1b475929227b761731ab2c0f7b5c0a1ae54cd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Feb 2021 12:32:54 +0300 Subject: [PATCH 6570/6909] Replace nullable API with null connector instead --- .../Visual/Gameplay/TestSceneSpectator.cs | 6 +- ...TestSceneMultiplayerGameplayLeaderboard.cs | 6 +- osu.Game/Online/HubClientConnector.cs | 36 +++++----- .../Spectator/SpectatorStreamingClient.cs | 67 ++++++++++--------- 4 files changed, 53 insertions(+), 62 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 1e499f20cb..36e7e1fb29 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -244,11 +244,7 @@ namespace osu.Game.Tests.Visual.Gameplay { } - protected override HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) - { - // do not pass API to prevent attempting failing connections on an actual hub. - return base.CreateConnector(name, endpoint, null); - } + protected override HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) => null; public void StartPlay(int beatmapId) { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index b459cebdd7..49abd62dba 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -106,11 +106,7 @@ namespace osu.Game.Tests.Visual.Multiplayer this.totalUsers = totalUsers; } - protected override HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) - { - // do not pass API to prevent attempting failing connections on an actual hub. - return base.CreateConnector(name, endpoint, null); - } + protected override HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) => null; public void Start(int beatmapId) { diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index 71d9df84c4..cfc4ff5d60 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -29,7 +29,7 @@ namespace osu.Game.Online private readonly string clientName; private readonly string endpoint; - private readonly IAPIProvider? api; + private readonly IAPIProvider api; /// /// The current connection opened by this connector. @@ -52,32 +52,28 @@ namespace osu.Game.Online /// /// The name of the client this connector connects for, used for logging. /// The endpoint to the hub. - /// An API provider used to react to connection state changes, or null to not establish connection at all (for testing purposes). - public HubClientConnector(string clientName, string endpoint, IAPIProvider? api) + /// An API provider used to react to connection state changes. + public HubClientConnector(string clientName, string endpoint, IAPIProvider api) { this.clientName = clientName; this.endpoint = endpoint; - this.api = api; - if (api != null) + apiState.BindTo(api.State); + apiState.BindValueChanged(state => { - apiState.BindTo(api.State); - apiState.BindValueChanged(state => + switch (state.NewValue) { - switch (state.NewValue) - { - case APIState.Failing: - case APIState.Offline: - Task.Run(() => disconnect(true)); - break; + case APIState.Failing: + case APIState.Offline: + Task.Run(() => disconnect(true)); + break; - case APIState.Online: - Task.Run(connect); - break; - } - }, true); - } + case APIState.Online: + Task.Run(connect); + break; + } + }, true); } private async Task connect() @@ -136,8 +132,6 @@ namespace osu.Game.Online private HubConnection buildConnection(CancellationToken cancellationToken) { - Debug.Assert(api != null); - var builder = new HubConnectionBuilder() .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 532f717f2c..7e61da9b87 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -32,11 +32,12 @@ namespace osu.Game.Online.Spectator private readonly string endpoint; + [CanBeNull] private HubClientConnector connector; private readonly IBindable isConnected = new BindableBool(); - private HubConnection connection => connector.CurrentConnection; + private HubConnection connection => connector?.CurrentConnection; private readonly List watchingUsers = new List(); @@ -86,42 +87,46 @@ namespace osu.Game.Online.Spectator private void load(IAPIProvider api) { connector = CreateConnector(nameof(SpectatorStreamingClient), endpoint, api); - connector.ConfigureConnection = connection => - { - // until strong typed client support is added, each method must be manually bound - // (see https://github.com/dotnet/aspnetcore/issues/15198) - connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); - connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); - connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); - }; - isConnected.BindTo(connector.IsConnected); - isConnected.BindValueChanged(connected => + if (connector != null) { - if (connected.NewValue) + connector.ConfigureConnection = connection => { - // get all the users that were previously being watched - int[] users; + // until strong typed client support is added, each method must be manually bound + // (see https://github.com/dotnet/aspnetcore/issues/15198) + connection.On(nameof(ISpectatorClient.UserBeganPlaying), ((ISpectatorClient)this).UserBeganPlaying); + connection.On(nameof(ISpectatorClient.UserSentFrames), ((ISpectatorClient)this).UserSentFrames); + connection.On(nameof(ISpectatorClient.UserFinishedPlaying), ((ISpectatorClient)this).UserFinishedPlaying); + }; - lock (userLock) + isConnected.BindTo(connector.IsConnected); + isConnected.BindValueChanged(connected => + { + if (connected.NewValue) { - users = watchingUsers.ToArray(); - watchingUsers.Clear(); + // get all the users that were previously being watched + int[] users; + + lock (userLock) + { + users = watchingUsers.ToArray(); + watchingUsers.Clear(); + } + + // resubscribe to watched users. + foreach (var userId in users) + WatchUser(userId); + + // re-send state in case it wasn't received + if (isPlaying) + beginPlaying(); } - - // resubscribe to watched users. - foreach (var userId in users) - WatchUser(userId); - - // re-send state in case it wasn't received - if (isPlaying) - beginPlaying(); - } - else - { - playingUsers.Clear(); - } - }, true); + else + { + playingUsers.Clear(); + } + }, true); + } } protected virtual HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) => new HubClientConnector(name, endpoint, api); From 37e3d95c35a67f24f66302977bb31f0906e6babc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Feb 2021 12:39:06 +0300 Subject: [PATCH 6571/6909] Slight reword in `ConfigureConnection`'s xmldoc --- osu.Game/Online/HubClientConnector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index cfc4ff5d60..fb76049446 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -23,7 +23,7 @@ namespace osu.Game.Online public class HubClientConnector : IDisposable { /// - /// Invoked whenever a new hub connection is built. + /// Invoked whenever a new hub connection is built, to configure it before it's started. /// public Action? ConfigureConnection; From f4a7ec57e982922928ed3f327c61b228457135cc Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Feb 2021 13:00:18 +0300 Subject: [PATCH 6572/6909] Remove unused using --- osu.Game/Online/HubClientConnector.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index fb76049446..2298ac4243 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -4,7 +4,6 @@ #nullable enable using System; -using System.Diagnostics; using System.Threading; using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; From 970039b7e3ff29804999cc0f28633bb308db0eee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 12:14:49 +0900 Subject: [PATCH 6573/6909] Split out hover sample debounce logic so it can be more easily used in other places --- .../HoverSampleDebounceComponent.cs | 46 +++++++++++++++++++ .../Graphics/UserInterface/HoverSounds.cs | 32 ++----------- 2 files changed, 50 insertions(+), 28 deletions(-) create mode 100644 osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs diff --git a/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs new file mode 100644 index 0000000000..f0c7c20fe8 --- /dev/null +++ b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs @@ -0,0 +1,46 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Framework.Allocation; +using osu.Framework.Audio; +using osu.Framework.Bindables; +using osu.Framework.Graphics.Containers; +using osu.Framework.Input.Events; +using osu.Game.Configuration; + +namespace osu.Game.Graphics.UserInterface +{ + /// + /// Handles debouncing hover sounds at a global level to ensure the effects are not overwhelming. + /// + public abstract class HoverSampleDebounceComponent : CompositeDrawable + { + /// + /// Length of debounce for hover sound playback, in milliseconds. + /// + public double HoverDebounceTime { get; } = 20; + + private Bindable lastPlaybackTime; + + [BackgroundDependencyLoader] + private void load(AudioManager audio, SessionStatics statics) + { + lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); + } + + protected override bool OnHover(HoverEvent e) + { + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; + + if (enoughTimePassedSinceLastPlayback) + { + PlayHoverSample(); + lastPlaybackTime.Value = Time.Current; + } + + return false; + } + + public abstract void PlayHoverSample(); + } +} diff --git a/osu.Game/Graphics/UserInterface/HoverSounds.cs b/osu.Game/Graphics/UserInterface/HoverSounds.cs index fa43d4543f..29238377c7 100644 --- a/osu.Game/Graphics/UserInterface/HoverSounds.cs +++ b/osu.Game/Graphics/UserInterface/HoverSounds.cs @@ -5,11 +5,8 @@ using System.ComponentModel; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; -using osu.Framework.Bindables; using osu.Framework.Extensions; using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Framework.Input.Events; using osu.Game.Configuration; using osu.Framework.Utils; @@ -19,19 +16,12 @@ namespace osu.Game.Graphics.UserInterface /// Adds hover sounds to a drawable. /// Does not draw anything. /// - public class HoverSounds : CompositeDrawable + public class HoverSounds : HoverSampleDebounceComponent { private SampleChannel sampleHover; - /// - /// Length of debounce for hover sound playback, in milliseconds. - /// - public double HoverDebounceTime { get; } = 20; - protected readonly HoverSampleSet SampleSet; - private Bindable lastPlaybackTime; - public HoverSounds(HoverSampleSet sampleSet = HoverSampleSet.Normal) { SampleSet = sampleSet; @@ -41,27 +31,13 @@ namespace osu.Game.Graphics.UserInterface [BackgroundDependencyLoader] private void load(AudioManager audio, SessionStatics statics) { - lastPlaybackTime = statics.GetBindable(Static.LastHoverSoundPlaybackTime); - sampleHover = audio.Samples.Get($@"UI/generic-hover{SampleSet.GetDescription()}"); } - protected override bool OnHover(HoverEvent e) + public override void PlayHoverSample() { - if (sampleHover == null) - return false; - - bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; - - if (enoughTimePassedSinceLastPlayback) - { - sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08); - sampleHover.Play(); - - lastPlaybackTime.Value = Time.Current; - } - - return false; + sampleHover.Frequency.Value = 0.96 + RNG.NextDouble(0.08); + sampleHover.Play(); } } From cd01591dda9ae57e66eddb0d5a80564a98f7ab20 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 12:14:57 +0900 Subject: [PATCH 6574/6909] Consume new debounce logic in carousel header --- .../Screens/Select/Carousel/CarouselHeader.cs | 69 +++++++++++-------- 1 file changed, 41 insertions(+), 28 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index 4f53a6e202..90eebfc05b 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; using osu.Framework.Utils; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; @@ -20,10 +21,6 @@ namespace osu.Game.Screens.Select.Carousel { public class CarouselHeader : Container { - private SampleChannel sampleHover; - - private readonly Box hoverLayer; - public Container BorderContainer; public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); @@ -44,23 +41,11 @@ namespace osu.Game.Screens.Select.Carousel Children = new Drawable[] { Content, - hoverLayer = new Box - { - RelativeSizeAxes = Axes.Both, - Alpha = 0, - Blending = BlendingParameters.Additive, - }, + new HoverLayer() } }; } - [BackgroundDependencyLoader] - private void load(AudioManager audio, OsuColour colours) - { - sampleHover = audio.Samples.Get("SongSelect/song-ping"); - hoverLayer.Colour = colours.Blue.Opacity(0.1f); - } - protected override void LoadComplete() { base.LoadComplete(); @@ -97,22 +82,50 @@ namespace osu.Game.Screens.Select.Carousel } } - protected override bool OnHover(HoverEvent e) + public class HoverLayer : HoverSampleDebounceComponent { - if (sampleHover != null) + private SampleChannel sampleHover; + + private Box box; + + public HoverLayer() { + RelativeSizeAxes = Axes.Both; + } + + [BackgroundDependencyLoader] + private void load(AudioManager audio, OsuColour colours) + { + InternalChild = box = new Box + { + Colour = colours.Blue.Opacity(0.1f), + Alpha = 0, + Blending = BlendingParameters.Additive, + RelativeSizeAxes = Axes.Both, + }; + + sampleHover = audio.Samples.Get("SongSelect/song-ping"); + } + + protected override bool OnHover(HoverEvent e) + { + box.FadeIn(100, Easing.OutQuint); + return base.OnHover(e); + } + + protected override void OnHoverLost(HoverLostEvent e) + { + box.FadeOut(1000, Easing.OutQuint); + base.OnHoverLost(e); + } + + public override void PlayHoverSample() + { + if (sampleHover == null) return; + sampleHover.Frequency.Value = 0.90 + RNG.NextDouble(0.2); sampleHover.Play(); } - - hoverLayer.FadeIn(100, Easing.OutQuint); - return base.OnHover(e); - } - - protected override void OnHoverLost(HoverLostEvent e) - { - hoverLayer.FadeOut(1000, Easing.OutQuint); - base.OnHoverLost(e); } } } From a2035a2e84d6a187331e0c56e2ffa906be673659 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 12:20:39 +0900 Subject: [PATCH 6575/6909] Stop hover sounds from playing when dragging (scrolling) --- .../Graphics/UserInterface/HoverSampleDebounceComponent.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs index f0c7c20fe8..55f43cfe46 100644 --- a/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs +++ b/osu.Game/Graphics/UserInterface/HoverSampleDebounceComponent.cs @@ -30,6 +30,10 @@ namespace osu.Game.Graphics.UserInterface protected override bool OnHover(HoverEvent e) { + // hover sounds shouldn't be played during scroll operations. + if (e.HasAnyButtonPressed) + return false; + bool enoughTimePassedSinceLastPlayback = !lastPlaybackTime.Value.HasValue || Time.Current - lastPlaybackTime.Value >= HoverDebounceTime; if (enoughTimePassedSinceLastPlayback) From 5f23bd725941a94304481d7aa37409162426e8ae Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 12:48:32 +0900 Subject: [PATCH 6576/6909] Revert most of the changes to ArchiveModeManager by using better code --- osu.Game/Beatmaps/BeatmapManager.cs | 8 ++------ osu.Game/Database/ArchiveModelManager.cs | 26 ++++++++++++++---------- osu.Game/Scoring/ScoreManager.cs | 7 +++---- 3 files changed, 20 insertions(+), 21 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 4825569ee4..f23e135c68 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -64,13 +64,9 @@ namespace osu.Game.Beatmaps protected override string[] HashableFileTypes => new[] { ".osu" }; - protected override bool StableDirectoryExists(StableStorage stableStorage) => stableStorage.GetSongStorage().ExistsDirectory("."); + protected override string ImportFromStablePath => "."; - protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) - { - var songStorage = stableStorage.GetSongStorage(); - return songStorage.GetDirectories(".").Select(path => songStorage.GetFullPath(path)); - } + protected override Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage.GetSongStorage(); private readonly RulesetStore rulesets; private readonly BeatmapStore beatmaps; diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index fd94660a4b..b55020c437 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -637,16 +637,11 @@ namespace osu.Game.Database /// protected virtual string ImportFromStablePath => null; - /// - /// Checks for the existence of an osu-stable directory. - /// - protected virtual bool StableDirectoryExists(StableStorage stableStorage) => stableStorage.ExistsDirectory(ImportFromStablePath); - /// /// Select paths to import from stable where all paths should be absolute. Default implementation iterates all directories in . /// - protected virtual IEnumerable GetStableImportPaths(StableStorage stableStorage) => stableStorage.GetDirectories(ImportFromStablePath) - .Select(path => stableStorage.GetFullPath(path)); + protected virtual IEnumerable GetStableImportPaths(Storage storage) => storage.GetDirectories(ImportFromStablePath) + .Select(path => storage.GetFullPath(path)); /// /// Whether this specified path should be removed after successful import. @@ -660,24 +655,33 @@ namespace osu.Game.Database /// public Task ImportFromStableAsync() { - var stable = GetStableStorage?.Invoke(); + var stableStorage = GetStableStorage?.Invoke(); - if (stable == null) + if (stableStorage == null) { Logger.Log("No osu!stable installation available!", LoggingTarget.Information, LogLevel.Error); return Task.CompletedTask; } - if (!StableDirectoryExists(stable)) + var storage = PrepareStableStorage(stableStorage); + + if (!storage.ExistsDirectory(ImportFromStablePath)) { // This handles situations like when the user does not have a Skins folder Logger.Log($"No {ImportFromStablePath} folder available in osu!stable installation", LoggingTarget.Information, LogLevel.Error); return Task.CompletedTask; } - return Task.Run(async () => await Import(GetStableImportPaths(stable).ToArray())); + return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray())); } + /// + /// Run any required traversal operations on the stable storage location before performing operations. + /// + /// The stable storage. + /// The usable storage. Return the unchanged if no traversal is required. + protected virtual Storage PrepareStableStorage(StableStorage stableStorage) => stableStorage; + #endregion /// diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 6aa0a30a75..a6beb19876 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -16,7 +16,6 @@ using osu.Framework.Platform; using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Database; -using osu.Game.IO; using osu.Game.IO.Archives; using osu.Game.Online.API; using osu.Game.Online.API.Requests; @@ -72,9 +71,9 @@ namespace osu.Game.Scoring } } - protected override IEnumerable GetStableImportPaths(StableStorage stableStorage) - => stableStorage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) - .Select(path => stableStorage.GetFullPath(path)); + protected override IEnumerable GetStableImportPaths(Storage storage) + => storage.GetFiles(ImportFromStablePath).Where(p => HandledExtensions.Any(ext => Path.GetExtension(p)?.Equals(ext, StringComparison.OrdinalIgnoreCase) ?? false)) + .Select(path => storage.GetFullPath(path)); public Score GetScore(ScoreInfo score) => new LegacyDatabasedScore(score, rulesets, beatmaps(), Files.Store); From 8ab7d07eab83befc7332496d20250fe54bf5ddc3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 12:57:57 +0900 Subject: [PATCH 6577/6909] Tidy up config parsing logic --- osu.Game/IO/StableStorage.cs | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/osu.Game/IO/StableStorage.cs b/osu.Game/IO/StableStorage.cs index ccc6f9c311..d4b0d300ff 100644 --- a/osu.Game/IO/StableStorage.cs +++ b/osu.Game/IO/StableStorage.cs @@ -23,6 +23,7 @@ namespace osu.Game.IO : base(path, host) { this.host = host; + songsPath = new Lazy(locateSongsDirectory); } @@ -33,32 +34,29 @@ namespace osu.Game.IO private string locateSongsDirectory() { - var songsDirectoryPath = Path.Combine(BasePath, stable_default_songs_path); - var configFile = GetFiles(".", $"osu!.{Environment.UserName}.cfg").SingleOrDefault(); - if (configFile == null) - return songsDirectoryPath; - - using (var stream = GetStream(configFile)) - using (var textReader = new StreamReader(stream)) + if (configFile != null) { - string line; - - while ((line = textReader.ReadLine()) != null) + using (var stream = GetStream(configFile)) + using (var textReader = new StreamReader(stream)) { - if (line.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase)) + string line; + + while ((line = textReader.ReadLine()) != null) { - var directory = line.Split('=')[1].TrimStart(); - if (Path.IsPathFullyQualified(directory)) - songsDirectoryPath = directory; + if (!line.StartsWith("BeatmapDirectory", StringComparison.OrdinalIgnoreCase)) continue; + + var customDirectory = line.Split('=').LastOrDefault()?.Trim(); + if (customDirectory != null && Path.IsPathFullyQualified(customDirectory)) + return customDirectory; break; } } } - return songsDirectoryPath; + return GetFullPath(stable_default_songs_path); } } } From 33c9ecac8a291657d19e4d4ba3cf7c55e15e3a5c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 14:54:19 +0900 Subject: [PATCH 6578/6909] Fix MessageFormatter not working for custom endpoints --- osu.Game/Online/Chat/MessageFormatter.cs | 7 ++++++- osu.Game/OsuGameBase.cs | 3 +++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index d2a117876d..8673d73be7 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -49,6 +49,11 @@ namespace osu.Game.Online.Chat // Unicode emojis private static readonly Regex emoji_regex = new Regex(@"(\uD83D[\uDC00-\uDE4F])"); + /// + /// The root URL for the website, used for chat link matching. + /// + public static string WebsiteRootUrl { get; set; } = "https://osu.ppy.sh"; + private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null, char[] escapeChars = null) { int captureOffset = 0; @@ -119,7 +124,7 @@ namespace osu.Game.Online.Chat case "http": case "https": // length > 3 since all these links need another argument to work - if (args.Length > 3 && args[1] == "osu.ppy.sh") + if (args.Length > 3 && url.StartsWith(WebsiteRootUrl, StringComparison.OrdinalIgnoreCase)) { switch (args[2]) { diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index a1b66ba9c0..174b5006a2 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -31,6 +31,7 @@ using osu.Game.Input; using osu.Game.Input.Bindings; using osu.Game.IO; using osu.Game.Online; +using osu.Game.Online.Chat; using osu.Game.Online.Multiplayer; using osu.Game.Online.Spectator; using osu.Game.Overlays; @@ -225,6 +226,8 @@ namespace osu.Game EndpointConfiguration endpoints = UseDevelopmentServer ? (EndpointConfiguration)new DevelopmentEndpointConfiguration() : new ProductionEndpointConfiguration(); + MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl; + dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints)); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient(endpoints)); From 6a42d312f62fd1ba45b2ffda559fbcb31b075c74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 14:56:46 +0900 Subject: [PATCH 6579/6909] Match using EndsWith to ignore protocol (and allow http) --- osu.Game/Online/Chat/MessageFormatter.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 8673d73be7..d8e02e5b6d 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -52,7 +52,14 @@ namespace osu.Game.Online.Chat /// /// The root URL for the website, used for chat link matching. /// - public static string WebsiteRootUrl { get; set; } = "https://osu.ppy.sh"; + public static string WebsiteRootUrl + { + set => websiteRootUrl = value + .Trim('/') // trim potential trailing slash/ + .Split('/').Last(); // only keep domain name, ignoring protocol. + } + + private static string websiteRootUrl; private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null, char[] escapeChars = null) { @@ -124,7 +131,7 @@ namespace osu.Game.Online.Chat case "http": case "https": // length > 3 since all these links need another argument to work - if (args.Length > 3 && url.StartsWith(WebsiteRootUrl, StringComparison.OrdinalIgnoreCase)) + if (args.Length > 3 && args[1].EndsWith(websiteRootUrl, StringComparison.OrdinalIgnoreCase)) { switch (args[2]) { From 1c5aaf3832e97a258bb764aa5dfd2f07200e4b35 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:03:53 +0900 Subject: [PATCH 6580/6909] Add back default value --- osu.Game/Online/Chat/MessageFormatter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index d8e02e5b6d..3c6df31462 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -59,7 +59,7 @@ namespace osu.Game.Online.Chat .Split('/').Last(); // only keep domain name, ignoring protocol. } - private static string websiteRootUrl; + private static string websiteRootUrl = "osu.ppy.sh"; private static void handleMatches(Regex regex, string display, string link, MessageFormatterResult result, int startIndex = 0, LinkAction? linkActionOverride = null, char[] escapeChars = null) { From 955c9a2dd3e47dab3752d62f4173bbde3b5ec0d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:17:39 +0900 Subject: [PATCH 6581/6909] Add test coverage of beatmap link resolution --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 600c820ce1..151e05b18d 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -21,6 +21,21 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(36, result.Links[0].Length); } + [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456")] + [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456?whatever")] + [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123/456")] + [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123")] + [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123/whatever")] + public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link) + { + Message result = MessageFormatter.FormatMessage(new Message { Content = link }); + + Assert.AreEqual(result.Content, result.DisplayContent); + Assert.AreEqual(1, result.Links.Count); + Assert.AreEqual(expectedAction, result.Links[0].Action); + Assert.AreEqual(expectedArg, result.Links[0].Argument); + } + [Test] public void TestMultipleComplexLinks() { From bb9123eecd231cde3b1ed240e7ffe3575b005fee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:17:54 +0900 Subject: [PATCH 6582/6909] Better handle fallback scenarios for beatmap links --- osu.Game/Online/Chat/MessageFormatter.cs | 33 ++++++++++++++++++------ 1 file changed, 25 insertions(+), 8 deletions(-) diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index d2a117876d..5aab476b6d 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -121,20 +121,37 @@ namespace osu.Game.Online.Chat // length > 3 since all these links need another argument to work if (args.Length > 3 && args[1] == "osu.ppy.sh") { + var mainArg = args[3]; + switch (args[2]) { + // old site only case "b": case "beatmaps": - return new LinkDetails(LinkAction.OpenBeatmap, args[3]); + { + string trimmed = mainArg.Split('?').First(); + if (int.TryParse(trimmed, out var id)) + return new LinkDetails(LinkAction.OpenBeatmap, id.ToString()); + + break; + } case "s": case "beatmapsets": case "d": - return new LinkDetails(LinkAction.OpenBeatmapSet, args[3]); + { + if (args.Length > 4 && int.TryParse(args[4], out var id)) + // https://osu.ppy.sh/beatmapsets/1154158#osu/2768184 + return new LinkDetails(LinkAction.OpenBeatmap, id.ToString()); + + // https://osu.ppy.sh/beatmapsets/1154158#whatever + string trimmed = mainArg.Split('#').First(); + return new LinkDetails(LinkAction.OpenBeatmapSet, trimmed); + } case "u": case "users": - return new LinkDetails(LinkAction.OpenUserProfile, args[3]); + return new LinkDetails(LinkAction.OpenUserProfile, mainArg); } } @@ -183,10 +200,9 @@ namespace osu.Game.Online.Chat case "osump": return new LinkDetails(LinkAction.JoinMultiplayerMatch, args[1]); - - default: - return new LinkDetails(LinkAction.External, null); } + + return new LinkDetails(LinkAction.External, null); } private static MessageFormatterResult format(string toFormat, int startIndex = 0, int space = 3) @@ -259,8 +275,9 @@ namespace osu.Game.Online.Chat public class LinkDetails { - public LinkAction Action; - public string Argument; + public readonly LinkAction Action; + + public readonly string Argument; public LinkDetails(LinkAction action, string argument) { From 3799493536907954559ed707d2edf07a1bd02c26 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:25:00 +0900 Subject: [PATCH 6583/6909] Add test coverage of int match failures --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 4 ++++ osu.Game/Online/Chat/MessageFormatter.cs | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 151e05b18d..11e94f0b89 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -24,8 +24,10 @@ namespace osu.Game.Tests.Chat [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456")] [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456?whatever")] [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123/456")] + [TestCase(LinkAction.External, null, "https://osu.ppy.sh/beatmapsets/abc/def")] [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123")] [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123/whatever")] + [TestCase(LinkAction.External, null, "https://osu.ppy.sh/beatmapsets/abc")] public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link) { Message result = MessageFormatter.FormatMessage(new Message { Content = link }); @@ -34,6 +36,8 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(1, result.Links.Count); Assert.AreEqual(expectedAction, result.Links[0].Action); Assert.AreEqual(expectedArg, result.Links[0].Argument); + if (expectedAction == LinkAction.External) + Assert.AreEqual(link, result.Links[0].Url); } [Test] diff --git a/osu.Game/Online/Chat/MessageFormatter.cs b/osu.Game/Online/Chat/MessageFormatter.cs index 5aab476b6d..8e92078c2c 100644 --- a/osu.Game/Online/Chat/MessageFormatter.cs +++ b/osu.Game/Online/Chat/MessageFormatter.cs @@ -146,7 +146,10 @@ namespace osu.Game.Online.Chat // https://osu.ppy.sh/beatmapsets/1154158#whatever string trimmed = mainArg.Split('#').First(); - return new LinkDetails(LinkAction.OpenBeatmapSet, trimmed); + if (int.TryParse(trimmed, out id)) + return new LinkDetails(LinkAction.OpenBeatmapSet, id.ToString()); + + break; } case "u": From a1be3c8bfdf71df5b0b0476b379d36a427ee0ee4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Feb 2021 15:27:37 +0900 Subject: [PATCH 6584/6909] Fix header background being invisible in multiplayer/playlists --- .../Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs index eb05cbaf85..3206f7b3ab 100644 --- a/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs +++ b/osu.Game/Beatmaps/Drawables/UpdateableBeatmapBackgroundSprite.cs @@ -34,8 +34,8 @@ namespace osu.Game.Beatmaps.Drawables /// protected virtual double UnloadDelay => 10000; - protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) - => new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay); + protected override DelayedLoadWrapper CreateDelayedLoadWrapper(Func createContentFunc, double timeBeforeLoad) => + new DelayedLoadUnloadWrapper(createContentFunc, timeBeforeLoad, UnloadDelay) { RelativeSizeAxes = Axes.Both }; protected override double TransformDuration => 400; From f7374703f00ee87f8865a51c5ef2fa5c3befad79 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:29:21 +0900 Subject: [PATCH 6585/6909] Update tests to match dev domain --- .../Visual/Online/TestSceneChatLink.cs | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs index 9e69530a77..74f53ebdca 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneChatLink.cs @@ -103,26 +103,26 @@ namespace osu.Game.Tests.Visual.Online private void testLinksGeneral() { addMessageWithChecks("test!"); - addMessageWithChecks("osu.ppy.sh!"); - addMessageWithChecks("https://osu.ppy.sh!", 1, expectedActions: LinkAction.External); + addMessageWithChecks("dev.ppy.sh!"); + addMessageWithChecks("https://dev.ppy.sh!", 1, expectedActions: LinkAction.External); addMessageWithChecks("00:12:345 (1,2) - Test?", 1, expectedActions: LinkAction.OpenEditorTimestamp); addMessageWithChecks("Wiki link for tasty [[Performance Points]]", 1, expectedActions: LinkAction.External); - addMessageWithChecks("(osu forums)[https://osu.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[https://osu.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[osu forums](https://osu.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[https://osu.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External); - addMessageWithChecks("is now listening to [https://osu.ppy.sh/s/93523 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmapSet); - addMessageWithChecks("is now playing [https://osu.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap); - addMessageWithChecks("Let's (try)[https://osu.ppy.sh/home] [https://osu.ppy.sh/b/252238 multiple links] https://osu.ppy.sh/home", 3, + addMessageWithChecks("(osu forums)[https://dev.ppy.sh/forum] (old link format)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[https://dev.ppy.sh/home New site] (new link format)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[osu forums](https://dev.ppy.sh/forum) (new link format 2)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[https://dev.ppy.sh/home This is only a link to the new osu webpage but this is supposed to test word wrap.]", 1, expectedActions: LinkAction.External); + addMessageWithChecks("is now listening to [https://dev.ppy.sh/s/93523 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmapSet); + addMessageWithChecks("is now playing [https://dev.ppy.sh/b/252238 IMAGE -MATERIAL- ]", 1, true, expectedActions: LinkAction.OpenBeatmap); + addMessageWithChecks("Let's (try)[https://dev.ppy.sh/home] [https://dev.ppy.sh/b/252238 multiple links] https://dev.ppy.sh/home", 3, expectedActions: new[] { LinkAction.External, LinkAction.OpenBeatmap, LinkAction.External }); - addMessageWithChecks("[https://osu.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External); - addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://osu.ppy.sh/home)", 1, expectedActions: LinkAction.External); - addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://osu.ppy.sh/home] and [[also a rogue wiki link]]", 2, expectedActions: new[] { LinkAction.External, LinkAction.External }); + addMessageWithChecks("[https://dev.ppy.sh/home New link format with escaped [and \\[ paired] braces]", 1, expectedActions: LinkAction.External); + addMessageWithChecks("[Markdown link format with escaped [and \\[ paired] braces](https://dev.ppy.sh/home)", 1, expectedActions: LinkAction.External); + addMessageWithChecks("(Old link format with escaped (and \\( paired) parentheses)[https://dev.ppy.sh/home] and [[also a rogue wiki link]]", 2, expectedActions: new[] { LinkAction.External, LinkAction.External }); // note that there's 0 links here (they get removed if a channel is not found) addMessageWithChecks("#lobby or #osu would be blue (and work) in the ChatDisplay test (when a proper ChatOverlay is present)."); addMessageWithChecks("I am important!", 0, false, true); addMessageWithChecks("feels important", 0, true, true); - addMessageWithChecks("likes to post this [https://osu.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External); + addMessageWithChecks("likes to post this [https://dev.ppy.sh/home link].", 1, true, true, expectedActions: LinkAction.External); addMessageWithChecks("Join my multiplayer game osump://12346.", 1, expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks("Join my [multiplayer game](osump://12346).", 1, expectedActions: LinkAction.JoinMultiplayerMatch); addMessageWithChecks("Join my [#english](osu://chan/#english).", 1, expectedActions: LinkAction.OpenChannel); @@ -136,9 +136,9 @@ namespace osu.Game.Tests.Visual.Online int echoCounter = 0; addEchoWithWait("sent!", "received!"); - addEchoWithWait("https://osu.ppy.sh/home", null, 500); - addEchoWithWait("[https://osu.ppy.sh/forum let's try multiple words too!]"); - addEchoWithWait("(long loading times! clickable while loading?)[https://osu.ppy.sh/home]", null, 5000); + addEchoWithWait("https://dev.ppy.sh/home", null, 500); + addEchoWithWait("[https://dev.ppy.sh/forum let's try multiple words too!]"); + addEchoWithWait("(long loading times! clickable while loading?)[https://dev.ppy.sh/home]", null, 5000); void addEchoWithWait(string text, string completeText = null, double delay = 250) { From a0733769206b58026ec0b044da3e2820b71a0c76 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 15:18:16 +0900 Subject: [PATCH 6586/6909] Show URLs in tooltips when custom text has replaced the link --- osu.Game/Graphics/Containers/LinkFlowContainer.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/Containers/LinkFlowContainer.cs b/osu.Game/Graphics/Containers/LinkFlowContainer.cs index e3a9a5fe9d..914c8ff78d 100644 --- a/osu.Game/Graphics/Containers/LinkFlowContainer.cs +++ b/osu.Game/Graphics/Containers/LinkFlowContainer.cs @@ -38,7 +38,12 @@ namespace osu.Game.Graphics.Containers foreach (var link in links) { AddText(text[previousLinkEnd..link.Index]); - AddLink(text.Substring(link.Index, link.Length), link.Action, link.Argument ?? link.Url); + + string displayText = text.Substring(link.Index, link.Length); + string linkArgument = link.Argument ?? link.Url; + string tooltip = displayText == link.Url ? null : link.Url; + + AddLink(displayText, link.Action, linkArgument, tooltip); previousLinkEnd = link.Index + link.Length; } @@ -52,7 +57,7 @@ namespace osu.Game.Graphics.Containers => createLink(AddText(text, creationParameters), new LinkDetails(LinkAction.Custom, null), tooltipText, action); public void AddLink(string text, LinkAction action, string argument, string tooltipText = null, Action creationParameters = null) - => createLink(AddText(text, creationParameters), new LinkDetails(action, argument), null); + => createLink(AddText(text, creationParameters), new LinkDetails(action, argument), tooltipText); public void AddLink(IEnumerable text, LinkAction action = LinkAction.External, string linkArgument = null, string tooltipText = null) { From 4ab16694d1ca145d2af3f6aa758504e9dba0a511 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 16:22:19 +0900 Subject: [PATCH 6587/6909] Fix classic "welcome" intro not looping as expected --- osu.Game/Screens/Menu/IntroWelcome.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Screens/Menu/IntroWelcome.cs b/osu.Game/Screens/Menu/IntroWelcome.cs index abb83f894a..d454d85d9e 100644 --- a/osu.Game/Screens/Menu/IntroWelcome.cs +++ b/osu.Game/Screens/Menu/IntroWelcome.cs @@ -67,6 +67,10 @@ namespace osu.Game.Screens.Menu { StartTrack(); + // this classic intro loops forever. + if (UsingThemedIntro) + Track.Looping = true; + const float fade_in_time = 200; logo.ScaleTo(1); From 725db5683731e9b193fb9d309910c88e8e6ba398 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 16:55:34 +0900 Subject: [PATCH 6588/6909] Add loading spinner while tournament bracket is loading / retrieving data --- .../TournamentTestBrowser.cs | 2 + .../TournamentTestScene.cs | 2 + osu.Game.Tournament/TournamentGame.cs | 43 +++++++++++++------ osu.Game.Tournament/TournamentGameBase.cs | 22 ++++++---- 4 files changed, 48 insertions(+), 21 deletions(-) diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs index f7ad757926..2f50ae4141 100644 --- a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs +++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs @@ -13,6 +13,8 @@ namespace osu.Game.Tournament.Tests { base.LoadComplete(); + BracketLoadTask.Wait(); + LoadComponentAsync(new Background("Menu/menu-background-0") { Colour = OsuColour.Gray(0.5f), diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index d22da25f9d..62882d7188 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -154,6 +154,8 @@ namespace osu.Game.Tournament.Tests protected override void LoadAsyncComplete() { + BracketLoadTask.Wait(); + // this has to be run here rather than LoadComplete because // TestScene.cs is checking the IsLoaded state (on another thread) and expects // the runner to be loaded at that point. diff --git a/osu.Game.Tournament/TournamentGame.cs b/osu.Game.Tournament/TournamentGame.cs index bbe4a53d8f..fadb821bef 100644 --- a/osu.Game.Tournament/TournamentGame.cs +++ b/osu.Game.Tournament/TournamentGame.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Colour; using osu.Game.Graphics.Cursor; using osu.Game.Tournament.Models; using osu.Game.Graphics; +using osu.Game.Graphics.UserInterface; using osuTK; using osuTK.Graphics; @@ -32,25 +33,24 @@ namespace osu.Game.Tournament private Drawable heightWarning; private Bindable windowSize; private Bindable windowMode; + private LoadingSpinner loadingSpinner; [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); - windowMode = frameworkConfig.GetBindable(FrameworkSetting.WindowMode); - windowMode.BindValueChanged(mode => ScheduleAfterChildren(() => - { - windowMode.Value = WindowMode.Windowed; - }), true); - AddRange(new[] + Add(loadingSpinner = new LoadingSpinner(true, true) + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Margin = new MarginPadding(40), + }); + + loadingSpinner.Show(); + + BracketLoadTask.ContinueWith(_ => LoadComponentsAsync(new[] { new Container { @@ -93,7 +93,24 @@ namespace osu.Game.Tournament RelativeSizeAxes = Axes.Both, Child = new TournamentSceneManager() } - }); + }, drawables => + { + loadingSpinner.Hide(); + loadingSpinner.Expire(); + + AddRange(drawables); + + windowSize.BindValueChanged(size => ScheduleAfterChildren(() => + { + var minWidth = (int)(size.NewValue.Height / 768f * TournamentSceneManager.REQUIRED_WIDTH) - 1; + heightWarning.Alpha = size.NewValue.Width < minWidth ? 1 : 0; + }), true); + + windowMode.BindValueChanged(mode => ScheduleAfterChildren(() => + { + windowMode.Value = WindowMode.Windowed; + }), true); + })); } } } diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 97c950261b..4dd072cf17 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -4,6 +4,8 @@ using System; using System.IO; using System.Linq; +using System.Threading; +using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Allocation; using osu.Framework.Graphics.Textures; @@ -29,6 +31,8 @@ namespace osu.Game.Tournament private DependencyContainer dependencies; private FileBasedIPC ipc; + protected Task BracketLoadTask { get; private set; } + protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { return dependencies = new DependencyContainer(base.CreateChildDependencies(parent)); @@ -46,14 +50,9 @@ namespace osu.Game.Tournament Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage))); - readBracket(); - - ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); + BracketLoadTask = Task.Run(readBracket); dependencies.CacheAs(new StableInfo(storage)); - - dependencies.CacheAs(ipc = new FileBasedIPC()); - Add(ipc); } private void readBracket() @@ -70,8 +69,6 @@ namespace osu.Game.Tournament Ruleset.BindTo(ladder.Ruleset); - dependencies.Cache(ladder); - bool addedInfo = false; // assign teams @@ -127,6 +124,15 @@ namespace osu.Game.Tournament if (addedInfo) SaveChanges(); + + ladder.CurrentMatch.Value = ladder.Matches.FirstOrDefault(p => p.Current.Value); + + Schedule(() => + { + dependencies.Cache(ladder); + dependencies.CacheAs(ipc = new FileBasedIPC()); + Add(ipc); + }); } /// From 0c3aef8645f234acdc8a4ceb4646af176ea8963b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Feb 2021 17:42:02 +0900 Subject: [PATCH 6589/6909] Fix potential race in looping sample As mentioned via GitHub comments. Very unlikely for this to happen unless: the sample takes a short amount of time to load, is very short itself, and the update thread stalls until the sample fully completes. --- osu.Game/Skinning/PoolableSkinnableSample.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index cff793e8d4..45880a8e1e 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -115,8 +115,7 @@ namespace osu.Game.Skinning if (Sample == null) return; - activeChannel = Sample.Play(); - activeChannel.Looping = Looping; + activeChannel = Sample.Play(Looping); Played = true; } From 9b5995f2f1f6710e3d26357e806f00168d559d01 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 12 Feb 2021 19:05:17 +0900 Subject: [PATCH 6590/6909] Update with removal of looping parameter --- osu.Game/Skinning/PoolableSkinnableSample.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 45880a8e1e..9025fdbd0f 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -115,7 +115,9 @@ namespace osu.Game.Skinning if (Sample == null) return; - activeChannel = Sample.Play(Looping); + activeChannel = Sample.GetChannel(); + activeChannel.Looping = Looping; + activeChannel.Play(); Played = true; } From 37a21cb192760c5f68a7842140d59d9421d681ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 21:30:02 +0900 Subject: [PATCH 6591/6909] Set static locally in test to ensure tests always run correctly --- osu.Game.Tests/Chat/MessageFormatterTests.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Chat/MessageFormatterTests.cs b/osu.Game.Tests/Chat/MessageFormatterTests.cs index 11e94f0b89..b80da928c8 100644 --- a/osu.Game.Tests/Chat/MessageFormatterTests.cs +++ b/osu.Game.Tests/Chat/MessageFormatterTests.cs @@ -21,15 +21,17 @@ namespace osu.Game.Tests.Chat Assert.AreEqual(36, result.Links[0].Length); } - [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456")] - [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123#osu/456?whatever")] - [TestCase(LinkAction.OpenBeatmap, "456", "https://osu.ppy.sh/beatmapsets/123/456")] - [TestCase(LinkAction.External, null, "https://osu.ppy.sh/beatmapsets/abc/def")] - [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123")] - [TestCase(LinkAction.OpenBeatmapSet, "123", "https://osu.ppy.sh/beatmapsets/123/whatever")] - [TestCase(LinkAction.External, null, "https://osu.ppy.sh/beatmapsets/abc")] + [TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123#osu/456")] + [TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123#osu/456?whatever")] + [TestCase(LinkAction.OpenBeatmap, "456", "https://dev.ppy.sh/beatmapsets/123/456")] + [TestCase(LinkAction.External, null, "https://dev.ppy.sh/beatmapsets/abc/def")] + [TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123")] + [TestCase(LinkAction.OpenBeatmapSet, "123", "https://dev.ppy.sh/beatmapsets/123/whatever")] + [TestCase(LinkAction.External, null, "https://dev.ppy.sh/beatmapsets/abc")] public void TestBeatmapLinks(LinkAction expectedAction, string expectedArg, string link) { + MessageFormatter.WebsiteRootUrl = "dev.ppy.sh"; + Message result = MessageFormatter.FormatMessage(new Message { Content = link }); Assert.AreEqual(result.Content, result.DisplayContent); From 7d057ab6ce9d81a6afe6fe79884d7705a40491dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Feb 2021 22:38:55 +0900 Subject: [PATCH 6592/6909] Fix two threading issues --- osu.Game.Tournament/TournamentGameBase.cs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 4dd072cf17..4224da4bbe 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -4,7 +4,6 @@ using System; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json; using osu.Framework.Allocation; @@ -31,7 +30,9 @@ namespace osu.Game.Tournament private DependencyContainer dependencies; private FileBasedIPC ipc; - protected Task BracketLoadTask { get; private set; } + protected Task BracketLoadTask => taskCompletionSource.Task; + + private readonly TaskCompletionSource taskCompletionSource = new TaskCompletionSource(); protected override IReadOnlyDependencyContainer CreateChildDependencies(IReadOnlyDependencyContainer parent) { @@ -50,9 +51,9 @@ namespace osu.Game.Tournament Textures.AddStore(new TextureLoaderStore(new StorageBackedResourceStore(storage))); - BracketLoadTask = Task.Run(readBracket); - dependencies.CacheAs(new StableInfo(storage)); + + Task.Run(readBracket); } private void readBracket() @@ -67,8 +68,6 @@ namespace osu.Game.Tournament ladder ??= new LadderInfo(); ladder.Ruleset.Value ??= RulesetStore.AvailableRulesets.First(); - Ruleset.BindTo(ladder.Ruleset); - bool addedInfo = false; // assign teams @@ -129,9 +128,13 @@ namespace osu.Game.Tournament Schedule(() => { + Ruleset.BindTo(ladder.Ruleset); + dependencies.Cache(ladder); dependencies.CacheAs(ipc = new FileBasedIPC()); Add(ipc); + + taskCompletionSource.SetResult(true); }); } From 13aaf766f93eadf358ea9d6b0d0bac2e71358a46 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Feb 2021 01:10:39 +0900 Subject: [PATCH 6593/6909] Fix regression in tournament test startup behaviour --- .../TournamentTestBrowser.cs | 19 ++++++++++--------- .../TournamentTestScene.cs | 13 +++++++------ 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs index 2f50ae4141..50bdcd86c5 100644 --- a/osu.Game.Tournament.Tests/TournamentTestBrowser.cs +++ b/osu.Game.Tournament.Tests/TournamentTestBrowser.cs @@ -13,17 +13,18 @@ namespace osu.Game.Tournament.Tests { base.LoadComplete(); - BracketLoadTask.Wait(); - - LoadComponentAsync(new Background("Menu/menu-background-0") + BracketLoadTask.ContinueWith(_ => Schedule(() => { - Colour = OsuColour.Gray(0.5f), - Depth = 10 - }, AddInternal); + LoadComponentAsync(new Background("Menu/menu-background-0") + { + Colour = OsuColour.Gray(0.5f), + Depth = 10 + }, AddInternal); - // 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()); + // 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.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index 62882d7188..025abfcbc6 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -154,12 +154,13 @@ namespace osu.Game.Tournament.Tests protected override void LoadAsyncComplete() { - BracketLoadTask.Wait(); - - // this has to be run here rather than LoadComplete because - // TestScene.cs is checking the IsLoaded state (on another thread) and expects - // the runner to be loaded at that point. - Add(runner = new TestSceneTestRunner.TestRunner()); + BracketLoadTask.ContinueWith(_ => Schedule(() => + { + // this has to be run here rather than LoadComplete because + // TestScene.cs is checking the IsLoaded state (on another thread) and expects + // the runner to be loaded at that point. + Add(runner = new TestSceneTestRunner.TestRunner()); + })); } public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test); From 52975c51854c665495a8384d5356829e90062bfc Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 12 Feb 2021 10:23:33 -0800 Subject: [PATCH 6594/6909] Remove hardcoded padding from main content --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b7adb71e2f..56882e0d38 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -77,7 +77,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer RelativeSizeAxes = Axes.Both, Padding = new MarginPadding { - Horizontal = 105, + Horizontal = HORIZONTAL_OVERFLOW_PADDING + 55, Vertical = 20 }, Child = new GridContainer From b28a906197eee66455c1201db6e3288afca2f571 Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 12 Feb 2021 10:29:29 -0800 Subject: [PATCH 6595/6909] Fix extra mod settings overflowing from screen --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 56882e0d38..c5130baa94 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -237,6 +237,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Origin = Anchor.BottomLeft, RelativeSizeAxes = Axes.Both, Height = 0.5f, + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }, Child = userModsSelectOverlay = new UserModSelectOverlay { SelectedMods = { BindTarget = UserMods }, From 982d8e35edc3efb9a05111b37596c94c44e182e8 Mon Sep 17 00:00:00 2001 From: Joehu Date: Fri, 12 Feb 2021 10:42:48 -0800 Subject: [PATCH 6596/6909] Fix mod settings showing scrollbar when screen is offset --- osu.Game/Overlays/Mods/ModSettingsContainer.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Mods/ModSettingsContainer.cs b/osu.Game/Overlays/Mods/ModSettingsContainer.cs index 1c57ff54ad..64d65cab3b 100644 --- a/osu.Game/Overlays/Mods/ModSettingsContainer.cs +++ b/osu.Game/Overlays/Mods/ModSettingsContainer.cs @@ -52,6 +52,7 @@ namespace osu.Game.Overlays.Mods new OsuScrollContainer { RelativeSizeAxes = Axes.Both, + ScrollbarVisible = false, Child = modSettingsContent = new FillFlowContainer { Anchor = Anchor.TopCentre, From a4dc54423531c77a0fb34023f8a634a9faaf108d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 14:23:59 +0900 Subject: [PATCH 6597/6909] Refactor some shared code in TestScenePause --- .../Visual/Gameplay/TestScenePause.cs | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index ae806883b0..1a0b594bb7 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -56,9 +56,7 @@ namespace osu.Game.Tests.Visual.Gameplay pauseAndConfirm(); resume(); - confirmClockRunning(false); - confirmPauseOverlayShown(false); - + confirmPausedWithNoOverlay(); AddStep("click to resume", () => InputManager.Click(MouseButton.Left)); confirmClockRunning(true); @@ -73,9 +71,7 @@ namespace osu.Game.Tests.Visual.Gameplay pauseAndConfirm(); resume(); - confirmClockRunning(false); - confirmPauseOverlayShown(false); - + confirmPausedWithNoOverlay(); pauseAndConfirm(); AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden); @@ -94,7 +90,7 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestPauseTooSoon() + public void TestPauseDuringCooldownTooSoon() { AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); @@ -103,8 +99,8 @@ namespace osu.Game.Tests.Visual.Gameplay resume(); pause(); - confirmClockRunning(true); - confirmPauseOverlayShown(false); + confirmResumed(); + AddAssert("not exited", () => Player.IsCurrentScreen()); } [Test] @@ -117,9 +113,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("exit quick", () => Player.Exit()); - confirmClockRunning(true); - confirmPauseOverlayShown(false); - + confirmResumed(); AddAssert("exited", () => !Player.IsCurrentScreen()); } @@ -133,9 +127,7 @@ namespace osu.Game.Tests.Visual.Gameplay pause(); - confirmClockRunning(false); - confirmPauseOverlayShown(false); - + confirmPausedWithNoOverlay(); AddAssert("fail overlay still shown", () => Player.FailOverlayVisible); exitAndConfirm(); @@ -277,6 +269,12 @@ namespace osu.Game.Tests.Visual.Gameplay confirmPauseOverlayShown(false); } + private void confirmPausedWithNoOverlay() + { + confirmClockRunning(false); + confirmPauseOverlayShown(false); + } + private void confirmExited() { AddUntilStep("player exited", () => !Player.IsCurrentScreen()); From 2b69c7b32530c4ab9ab712610b94664a2a9b9d43 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 14:00:30 +0900 Subject: [PATCH 6598/6909] Fix incorrect order of operation in pause blocking logic --- osu.Game/Screens/Play/Player.cs | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index dda52f4dae..c462786916 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -503,14 +503,17 @@ namespace osu.Game.Screens.Play return; } - if (canPause && !GameplayClockContainer.IsPaused.Value) + if (!GameplayClockContainer.IsPaused.Value) { - if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value) - // still want to block if we are within the cooldown period and not already paused. + // if we are within the cooldown period and not already paused, the operation should block completely. + if (pauseCooldownActive) return; - Pause(); - return; + if (canPause) + { + Pause(); + return; + } } } From 25f5120fdf51be4ca58c208f55198cdcbcf41d0d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 14:36:14 +0900 Subject: [PATCH 6599/6909] Add failing test coverage of user pausing or quick exiting during cooldown --- .../Visual/Gameplay/TestScenePause.cs | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 1a0b594bb7..8246e2c028 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -90,19 +90,47 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestPauseDuringCooldownTooSoon() + public void TestExternalPauseDuringCooldownTooSoon() { AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); pauseAndConfirm(); resume(); - pause(); + pauseExternally(); confirmResumed(); AddAssert("not exited", () => Player.IsCurrentScreen()); } + [Test] + public void TestUserPauseDuringCooldownTooSoon() + { + AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); + + pauseAndConfirm(); + + resume(); + AddStep("pause via exit key", () => Player.ExitViaPause()); + + confirmResumed(); + AddAssert("not exited", () => Player.IsCurrentScreen()); + } + + [Test] + public void TestQuickExitDuringCooldownTooSoon() + { + AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); + + pauseAndConfirm(); + + resume(); + AddStep("pause via exit key", () => Player.ExitViaQuickExit()); + + confirmResumed(); + AddAssert("exited", () => !Player.IsCurrentScreen()); + } + [Test] public void TestExitSoonAfterResumeSucceeds() { @@ -125,7 +153,7 @@ namespace osu.Game.Tests.Visual.Gameplay confirmClockRunning(false); - pause(); + pauseExternally(); confirmPausedWithNoOverlay(); AddAssert("fail overlay still shown", () => Player.FailOverlayVisible); @@ -237,7 +265,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void pauseAndConfirm() { - pause(); + pauseExternally(); confirmPaused(); } @@ -286,7 +314,7 @@ namespace osu.Game.Tests.Visual.Gameplay } private void restart() => AddStep("restart", () => Player.Restart()); - private void pause() => AddStep("pause", () => Player.Pause()); + private void pauseExternally() => AddStep("pause", () => Player.Pause()); private void resume() => AddStep("resume", () => Player.Resume()); private void confirmPauseOverlayShown(bool isShown) => @@ -305,6 +333,10 @@ namespace osu.Game.Tests.Visual.Gameplay public bool PauseOverlayVisible => PauseOverlay.State.Value == Visibility.Visible; + public void ExitViaPause() => PerformExit(true); + + public void ExitViaQuickExit() => PerformExit(false); + public override void OnEntering(IScreen last) { base.OnEntering(last); From ec37e1602d566920601a1c197757991099b2447f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 15:02:58 +0900 Subject: [PATCH 6600/6909] Add failing test coverage of retrying from the results screen --- .../Visual/Navigation/OsuGameTestScene.cs | 3 +++ .../Navigation/TestSceneScreenNavigation.cs | 26 +++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs b/osu.Game.Tests/Visual/Navigation/OsuGameTestScene.cs index c5038068ec..96393cc4c3 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.Rulesets.Mods; using osu.Game.Scoring; using osu.Game.Screens; using osu.Game.Screens.Menu; @@ -115,6 +116,8 @@ namespace osu.Game.Tests.Visual.Navigation public new Bindable Ruleset => base.Ruleset; + public new Bindable> SelectedMods => base.SelectedMods; + // 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"; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 8480e6eaaa..d8380b2dd3 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -11,7 +11,9 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Mods; using osu.Game.Overlays.Toolbar; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Screens.Play; +using osu.Game.Screens.Ranking; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Options; using osu.Game.Tests.Beatmaps.IO; @@ -41,6 +43,30 @@ namespace osu.Game.Tests.Visual.Navigation exitViaEscapeAndConfirm(); } + [Test] + public void TestRetryFromResults() + { + Player player = null; + ResultsScreen results = null; + + WorkingBeatmap beatmap() => Game.Beatmap.Value; + + PushAndConfirm(() => new TestSongSelect()); + + AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait()); + + AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); + + AddStep("set autoplay", () => Game.SelectedMods.Value = new[] { new OsuModAutoplay() }); + + AddStep("press enter", () => InputManager.Key(Key.Enter)); + AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); + AddStep("seek to end", () => beatmap().Track.Seek(beatmap().Track.Length)); + AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded); + AddStep("attempt to retry", () => results.ChildrenOfType().First().Action()); + AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player); + } + [TestCase(true)] [TestCase(false)] public void TestSongContinuesAfterExitPlayer(bool withUserPause) From 1aea840504aa337e24173ca701188e6fd88a6d1a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 14:03:41 +0900 Subject: [PATCH 6601/6909] Add missing return in early exit scenario (MakeCurrent isn't compatible with the following Exit) --- osu.Game/Screens/Play/Player.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index c462786916..88ca516440 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -493,6 +493,7 @@ namespace osu.Game.Screens.Play // we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process). ValidForResume = false; this.MakeCurrent(); + return; } if (showDialogFirst) From 83183a84da2a877e44a5472cc375e0e9b162793e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 15:31:51 +0900 Subject: [PATCH 6602/6909] Ensure the tournament test runner is ready before performing the test run --- osu.Game.Tournament.Tests/TournamentTestScene.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index 025abfcbc6..47d2160561 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -2,6 +2,7 @@ // 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.Platform; using osu.Framework.Testing; @@ -163,7 +164,13 @@ namespace osu.Game.Tournament.Tests })); } - public void RunTestBlocking(TestScene test) => runner.RunTestBlocking(test); + public void RunTestBlocking(TestScene test) + { + while (runner?.IsLoaded != true && Host.ExecutionState == ExecutionState.Running) + Thread.Sleep(10); + + runner?.RunTestBlocking(test); + } } } } From 4f264758a499fea09585cdd9403cba15d9a7777c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 15:57:34 +0900 Subject: [PATCH 6603/6909] Add test coverage of pause from resume overlay --- .../Visual/Gameplay/TestScenePause.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 8246e2c028..1ad1479cd4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -69,13 +69,14 @@ namespace osu.Game.Tests.Visual.Gameplay AddUntilStep("wait for hitobjects", () => Player.HealthProcessor.Health.Value < 1); pauseAndConfirm(); - resume(); + confirmPausedWithNoOverlay(); pauseAndConfirm(); AddUntilStep("resume overlay is not active", () => Player.DrawableRuleset.ResumeOverlay.State.Value == Visibility.Hidden); confirmPaused(); + confirmNotExited(); } [Test] @@ -100,7 +101,7 @@ namespace osu.Game.Tests.Visual.Gameplay pauseExternally(); confirmResumed(); - AddAssert("not exited", () => Player.IsCurrentScreen()); + confirmNotExited(); } [Test] @@ -114,7 +115,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("pause via exit key", () => Player.ExitViaPause()); confirmResumed(); - AddAssert("not exited", () => Player.IsCurrentScreen()); + confirmNotExited(); } [Test] @@ -277,7 +278,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void exitAndConfirm() { - AddUntilStep("player not exited", () => Player.IsCurrentScreen()); + confirmNotExited(); AddStep("exit", () => Player.Exit()); confirmExited(); confirmNoTrackAdjustments(); @@ -286,7 +287,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void confirmPaused() { confirmClockRunning(false); - AddAssert("player not exited", () => Player.IsCurrentScreen()); + confirmNotExited(); AddAssert("player not failed", () => !Player.HasFailed); AddAssert("pause overlay shown", () => Player.PauseOverlayVisible); } @@ -303,10 +304,8 @@ namespace osu.Game.Tests.Visual.Gameplay confirmPauseOverlayShown(false); } - private void confirmExited() - { - AddUntilStep("player exited", () => !Player.IsCurrentScreen()); - } + private void confirmExited() => AddUntilStep("player exited", () => !Player.IsCurrentScreen()); + private void confirmNotExited() => AddAssert("player not exited", () => Player.IsCurrentScreen()); private void confirmNoTrackAdjustments() { From 9cba350337484a3e1466063411daeb489c18abdc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 15:57:21 +0900 Subject: [PATCH 6604/6909] Refactor again to better cover cases where the pause dialog should definitely be shown --- osu.Game/Screens/Play/Player.cs | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 88ca516440..a844d3bcf7 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -487,35 +487,30 @@ namespace osu.Game.Screens.Play // if a restart has been requested, cancel any pending completion (user has shown intent to restart). completionProgressDelegate?.Cancel(); + // there is a chance that the exit was performed after the transition to results has started. + // we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process). if (!this.IsCurrentScreen()) { - // there is a chance that the exit was performed after the transition to results has started. - // we want to give the user what they want, so forcefully return to this screen (to proceed with the upwards exit process). ValidForResume = false; this.MakeCurrent(); return; } - if (showDialogFirst) + bool pauseDialogShown = PauseOverlay.State.Value == Visibility.Visible; + + if (showDialogFirst && !pauseDialogShown) { + // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). if (ValidForResume && HasFailed && !FailOverlay.IsPresent) { failAnimation.FinishTransforms(true); return; } - if (!GameplayClockContainer.IsPaused.Value) - { - // if we are within the cooldown period and not already paused, the operation should block completely. - if (pauseCooldownActive) - return; - - if (canPause) - { - Pause(); - return; - } - } + // in the case a dialog needs to be shown, attempt to pause and show it. + // this may fail (see internal checks in Pause()) at which point the exit attempt will be aborted. + Pause(); + return; } this.Exit(); From f664fca0ddf2ac466af1d27048f0d370c74ecb94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 16:11:17 +0900 Subject: [PATCH 6605/6909] Tidy up tests (and remove duplicate with new call logic) --- .../Visual/Gameplay/TestScenePause.cs | 22 ++++--------------- 1 file changed, 4 insertions(+), 18 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 1ad1479cd4..aa56c636ab 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -90,20 +90,6 @@ namespace osu.Game.Tests.Visual.Gameplay resumeAndConfirm(); } - [Test] - public void TestExternalPauseDuringCooldownTooSoon() - { - AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); - - pauseAndConfirm(); - - resume(); - pauseExternally(); - - confirmResumed(); - confirmNotExited(); - } - [Test] public void TestUserPauseDuringCooldownTooSoon() { @@ -112,7 +98,7 @@ namespace osu.Game.Tests.Visual.Gameplay pauseAndConfirm(); resume(); - AddStep("pause via exit key", () => Player.ExitViaPause()); + pauseFromUserExitKey(); confirmResumed(); confirmNotExited(); @@ -154,7 +140,7 @@ namespace osu.Game.Tests.Visual.Gameplay confirmClockRunning(false); - pauseExternally(); + pauseFromUserExitKey(); confirmPausedWithNoOverlay(); AddAssert("fail overlay still shown", () => Player.FailOverlayVisible); @@ -266,7 +252,7 @@ namespace osu.Game.Tests.Visual.Gameplay private void pauseAndConfirm() { - pauseExternally(); + pauseFromUserExitKey(); confirmPaused(); } @@ -313,7 +299,7 @@ namespace osu.Game.Tests.Visual.Gameplay } private void restart() => AddStep("restart", () => Player.Restart()); - private void pauseExternally() => AddStep("pause", () => Player.Pause()); + private void pauseFromUserExitKey() => AddStep("user pause", () => Player.ExitViaPause()); private void resume() => AddStep("resume", () => Player.Resume()); private void confirmPauseOverlayShown(bool isShown) => From 9ad38ab20e9bcc6b43aa8cab09ee6f14c8b34638 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 16:31:00 +0900 Subject: [PATCH 6606/6909] Move HubClientConnector retrieval to IAPIProvider --- .../Visual/Gameplay/TestSceneSpectator.cs | 3 -- ...TestSceneMultiplayerGameplayLeaderboard.cs | 3 -- osu.Game/Online/API/APIAccess.cs | 2 ++ osu.Game/Online/API/DummyAPIAccess.cs | 2 ++ osu.Game/Online/API/IAPIProvider.cs | 9 +++++ osu.Game/Online/HubClientConnector.cs | 7 ++-- osu.Game/Online/IHubClientConnector.cs | 34 +++++++++++++++++++ .../Online/Multiplayer/MultiplayerClient.cs | 15 ++++---- .../Spectator/SpectatorStreamingClient.cs | 6 ++-- 9 files changed, 60 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Online/IHubClientConnector.cs diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs index 36e7e1fb29..4a0e1282c4 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneSpectator.cs @@ -12,7 +12,6 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; using osu.Game.Online; -using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu; @@ -244,8 +243,6 @@ namespace osu.Game.Tests.Visual.Gameplay { } - protected override HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) => null; - public void StartPlay(int beatmapId) { this.beatmapId = beatmapId; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 49abd62dba..aab69d687a 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -13,7 +13,6 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Database; using osu.Game.Online; -using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu.Scoring; @@ -106,8 +105,6 @@ namespace osu.Game.Tests.Visual.Multiplayer this.totalUsers = totalUsers; } - protected override HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) => null; - public void Start(int beatmapId) { for (int i = 0; i < totalUsers; i++) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 2aaea22155..657487971b 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -243,6 +243,8 @@ namespace osu.Game.Online.API this.password = password; } + public IHubClientConnector GetHubConnector(string clientName, string endpoint) => new HubClientConnector(clientName, endpoint, this); + public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { Debug.Assert(State.Value == APIState.Offline); diff --git a/osu.Game/Online/API/DummyAPIAccess.cs b/osu.Game/Online/API/DummyAPIAccess.cs index 3e996ac97f..943b52db88 100644 --- a/osu.Game/Online/API/DummyAPIAccess.cs +++ b/osu.Game/Online/API/DummyAPIAccess.cs @@ -83,6 +83,8 @@ namespace osu.Game.Online.API state.Value = APIState.Offline; } + public IHubClientConnector GetHubConnector(string clientName, string endpoint) => null; + public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { Thread.Sleep(200); diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 1951dfaf40..34b7dc5f17 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.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. +#nullable enable + using System.Threading.Tasks; using osu.Framework.Bindables; using osu.Game.Users; @@ -95,6 +97,13 @@ namespace osu.Game.Online.API /// void Logout(); + /// + /// Constructs a new . May be null if not supported. + /// + /// The name of the client this connector connects for, used for logging. + /// The endpoint to the hub. + IHubClientConnector? GetHubConnector(string clientName, string endpoint); + /// /// Create a new user account. This is a blocking operation. /// diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index 2298ac4243..7884a294d3 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -16,15 +16,12 @@ using osu.Game.Online.API; namespace osu.Game.Online { - /// - /// A component that manages the life cycle of a connection to a SignalR Hub. - /// - public class HubClientConnector : IDisposable + public class HubClientConnector : IHubClientConnector { /// /// Invoked whenever a new hub connection is built, to configure it before it's started. /// - public Action? ConfigureConnection; + public Action? ConfigureConnection { get; set; } private readonly string clientName; private readonly string endpoint; diff --git a/osu.Game/Online/IHubClientConnector.cs b/osu.Game/Online/IHubClientConnector.cs new file mode 100644 index 0000000000..d2ceb1f030 --- /dev/null +++ b/osu.Game/Online/IHubClientConnector.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. + +#nullable enable + +using System; +using Microsoft.AspNetCore.SignalR.Client; +using osu.Framework.Bindables; +using osu.Game.Online.API; + +namespace osu.Game.Online +{ + /// + /// A component that manages the life cycle of a connection to a SignalR Hub. + /// Should generally be retrieved from an . + /// + public interface IHubClientConnector : IDisposable + { + /// + /// The current connection opened by this connector. + /// + HubConnection? CurrentConnection { get; } + + /// + /// Whether this is connected to the hub, use to access the connection, if this is true. + /// + IBindable IsConnected { get; } + + /// + /// Invoked whenever a new hub connection is built, to configure it before it's started. + /// + public Action? ConfigureConnection { get; set; } + } +} diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index ba2a8d7246..95d76f384f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -17,7 +17,8 @@ namespace osu.Game.Online.Multiplayer public class MultiplayerClient : StatefulMultiplayerClient { private readonly string endpoint; - private HubClientConnector? connector; + + private IHubClientConnector? connector; public override IBindable IsConnected { get; } = new BindableBool(); @@ -31,9 +32,11 @@ namespace osu.Game.Online.Multiplayer [BackgroundDependencyLoader] private void load(IAPIProvider api) { - connector = new HubClientConnector(nameof(MultiplayerClient), endpoint, api) + connector = api.GetHubConnector(nameof(MultiplayerClient), endpoint); + + if (connector != null) { - ConfigureConnection = connection => + connector.ConfigureConnection = connection => { // this is kind of SILLY // https://github.com/dotnet/aspnetcore/issues/15198 @@ -48,10 +51,10 @@ namespace osu.Game.Online.Multiplayer connection.On(nameof(IMultiplayerClient.ResultsReady), ((IMultiplayerClient)this).ResultsReady); connection.On>(nameof(IMultiplayerClient.UserModsChanged), ((IMultiplayerClient)this).UserModsChanged); connection.On(nameof(IMultiplayerClient.UserBeatmapAvailabilityChanged), ((IMultiplayerClient)this).UserBeatmapAvailabilityChanged); - }, - }; + }; - IsConnected.BindTo(connector.IsConnected); + IsConnected.BindTo(connector.IsConnected); + } } protected override Task JoinRoom(long roomId) diff --git a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs index 7e61da9b87..3a586874fe 100644 --- a/osu.Game/Online/Spectator/SpectatorStreamingClient.cs +++ b/osu.Game/Online/Spectator/SpectatorStreamingClient.cs @@ -33,7 +33,7 @@ namespace osu.Game.Online.Spectator private readonly string endpoint; [CanBeNull] - private HubClientConnector connector; + private IHubClientConnector connector; private readonly IBindable isConnected = new BindableBool(); @@ -86,7 +86,7 @@ namespace osu.Game.Online.Spectator [BackgroundDependencyLoader] private void load(IAPIProvider api) { - connector = CreateConnector(nameof(SpectatorStreamingClient), endpoint, api); + connector = api.GetHubConnector(nameof(SpectatorStreamingClient), endpoint); if (connector != null) { @@ -129,8 +129,6 @@ namespace osu.Game.Online.Spectator } } - protected virtual HubClientConnector CreateConnector(string name, string endpoint, IAPIProvider api) => new HubClientConnector(name, endpoint, api); - Task ISpectatorClient.UserBeganPlaying(int userId, SpectatorState state) { if (!playingUsers.Contains(userId)) From 55d5d8d5be4c3ad504681d1fa0d605429ccad684 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 14 Feb 2021 23:31:57 +0900 Subject: [PATCH 6607/6909] Send version hash on hub connection --- osu.Game/Online/API/APIAccess.cs | 7 +++++-- osu.Game/Online/HubClientConnector.cs | 11 +++++++++-- osu.Game/OsuGameBase.cs | 2 +- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 657487971b..8ffa0221c8 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -25,6 +25,8 @@ namespace osu.Game.Online.API { private readonly OsuConfigManager config; + private readonly string versionHash; + private readonly OAuth authentication; private readonly Queue queue = new Queue(); @@ -56,9 +58,10 @@ namespace osu.Game.Online.API private readonly Logger log; - public APIAccess(OsuConfigManager config, EndpointConfiguration endpointConfiguration) + public APIAccess(OsuConfigManager config, EndpointConfiguration endpointConfiguration, string versionHash) { this.config = config; + this.versionHash = versionHash; APIEndpointUrl = endpointConfiguration.APIEndpointUrl; WebsiteRootUrl = endpointConfiguration.WebsiteRootUrl; @@ -243,7 +246,7 @@ namespace osu.Game.Online.API this.password = password; } - public IHubClientConnector GetHubConnector(string clientName, string endpoint) => new HubClientConnector(clientName, endpoint, this); + public IHubClientConnector GetHubConnector(string clientName, string endpoint) => new HubClientConnector(clientName, endpoint, this, versionHash); public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index 7884a294d3..fdb21c5000 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -25,6 +25,7 @@ namespace osu.Game.Online private readonly string clientName; private readonly string endpoint; + private readonly string versionHash; private readonly IAPIProvider api; /// @@ -49,11 +50,13 @@ namespace osu.Game.Online /// The name of the client this connector connects for, used for logging. /// The endpoint to the hub. /// An API provider used to react to connection state changes. - public HubClientConnector(string clientName, string endpoint, IAPIProvider api) + /// The hash representing the current game version, used for verification purposes. + public HubClientConnector(string clientName, string endpoint, IAPIProvider api, string versionHash) { this.clientName = clientName; this.endpoint = endpoint; this.api = api; + this.versionHash = versionHash; apiState.BindTo(api.State); apiState.BindValueChanged(state => @@ -129,7 +132,11 @@ namespace osu.Game.Online private HubConnection buildConnection(CancellationToken cancellationToken) { var builder = new HubConnectionBuilder() - .WithUrl(endpoint, options => { options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); }); + .WithUrl(endpoint, options => + { + options.Headers.Add("Authorization", $"Bearer {api.AccessToken}"); + options.Headers.Add("OsuVersionHash", versionHash); + }); if (RuntimeInfo.SupportsJIT) builder.AddMessagePackProtocol(); diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 174b5006a2..00b436931a 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -228,7 +228,7 @@ namespace osu.Game MessageFormatter.WebsiteRootUrl = endpoints.WebsiteRootUrl; - dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints)); + dependencies.CacheAs(API ??= new APIAccess(LocalConfig, endpoints, VersionHash)); dependencies.CacheAs(spectatorStreaming = new SpectatorStreamingClient(endpoints)); dependencies.CacheAs(multiplayerClient = new MultiplayerClient(endpoints)); From 3562fddc27a2f4c31c3b93c93f70e660f8207dd4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 17:02:07 +0900 Subject: [PATCH 6608/6909] Add missing nullability flag on CreateAccount return value --- osu.Game/Online/API/IAPIProvider.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/IAPIProvider.cs b/osu.Game/Online/API/IAPIProvider.cs index 34b7dc5f17..3a77b9cfee 100644 --- a/osu.Game/Online/API/IAPIProvider.cs +++ b/osu.Game/Online/API/IAPIProvider.cs @@ -111,6 +111,6 @@ namespace osu.Game.Online.API /// The username to create the account with. /// The password to create the account with. /// Any errors encoutnered during account creation. - RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password); + RegistrationRequest.RegistrationRequestErrors? CreateAccount(string email, string username, string password); } } From de52b8a5ba0dc72f41d694c2ba7d1aa583276486 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 17:14:41 +0900 Subject: [PATCH 6609/6909] Fix test failures in PerformFromScreen tests --- osu.Game/Overlays/Volume/VolumeMeter.cs | 2 ++ osu.Game/Screens/BackgroundScreen.cs | 10 +++++++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Volume/VolumeMeter.cs b/osu.Game/Overlays/Volume/VolumeMeter.cs index 07accf8820..5b997bbd05 100644 --- a/osu.Game/Overlays/Volume/VolumeMeter.cs +++ b/osu.Game/Overlays/Volume/VolumeMeter.cs @@ -176,6 +176,7 @@ namespace osu.Game.Overlays.Volume } } }; + Bindable.ValueChanged += volume => { this.TransformTo("DisplayVolume", @@ -183,6 +184,7 @@ namespace osu.Game.Overlays.Volume 400, Easing.OutQuint); }; + bgProgress.Current.Value = 0.75f; } diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index c81362eebe..48c5523883 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -68,15 +68,19 @@ namespace osu.Game.Screens public override bool OnExiting(IScreen next) { - this.FadeOut(transition_length, Easing.OutExpo); - this.MoveToX(x_movement_amount, transition_length, Easing.OutExpo); + if (IsLoaded) + { + this.FadeOut(transition_length, Easing.OutExpo); + this.MoveToX(x_movement_amount, transition_length, Easing.OutExpo); + } return base.OnExiting(next); } public override void OnResuming(IScreen last) { - this.MoveToX(0, transition_length, Easing.OutExpo); + if (IsLoaded) + this.MoveToX(0, transition_length, Easing.OutExpo); base.OnResuming(last); } } From 6bfc7da671c355c5586cb556f473ae7773cecefb Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Mon, 15 Feb 2021 18:10:45 +0900 Subject: [PATCH 6610/6909] Fix sample potentially playing at the wrong frequency Co-authored-by: Dean Herbert --- .../OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs index 1888bf06bd..c9fb234ccc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerReadyButton.cs @@ -117,8 +117,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match if (sampleReadyCount == null) return; - var channel = sampleReadyCount.Play(); + var channel = sampleReadyCount.GetChannel(); channel.Frequency.Value = 0.77f + countReady * 0.06f; + channel.Play(); } private void updateButtonColour(bool green) From 1ac274e478b8fcb50d3cad3bf95f1c40d585231e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 21:22:18 +0900 Subject: [PATCH 6611/6909] 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 d88a11257d..e30416bc1c 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 d68a8a515c..cccebeb023 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 87ebd41fee..137c96a72d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -91,7 +91,7 @@ - + From 72b2123500f28257f140f6fc5f443e85973f99cf Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Feb 2021 21:42:35 +0900 Subject: [PATCH 6612/6909] Update nunit in line with framework --- osu.Game.Benchmarks/osu.Game.Benchmarks.csproj | 2 +- .../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 +- osu.Game/osu.Game.csproj | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj index 7805bfcefc..ea43d9a54c 100644 --- a/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj +++ b/osu.Game.Benchmarks/osu.Game.Benchmarks.csproj @@ -8,7 +8,7 @@ - + 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 54fddc297e..bf3aba5859 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 @@ -3,7 +3,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 d55b4fe08a..fcc0cafefc 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 @@ -3,7 +3,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 345c3e6d35..b4c686ccea 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 @@ -3,7 +3,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 2a5a2e2fdb..2b084f3bee 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 @@ -3,7 +3,7 @@ - + diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index d29ed94b5f..7e3868bd3b 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -4,7 +4,7 @@ - + diff --git a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj index 185b35e40d..77ae06d89c 100644 --- a/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj +++ b/osu.Game.Tournament.Tests/osu.Game.Tournament.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index cccebeb023..72f680f6f8 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -33,7 +33,7 @@ - + From a1496cd8f3afed32bd9c126a18dc8a3714ca6212 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 08:28:09 +0300 Subject: [PATCH 6613/6909] Remove necessity of using `CurrentModeRank` as a fallback --- .../TestSceneMultiplayerParticipantsList.cs | 10 ++++++++-- .../Multiplayer/Participants/ParticipantPanel.cs | 4 ---- osu.Game/Users/User.cs | 9 ++++++--- 3 files changed, 14 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 0f7a9b442d..5a0234e379 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -155,7 +155,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { Id = i, Username = $"User {i}", - CurrentModeRank = RNG.Next(1, 100000), + AllStatistics = + { + { Ruleset.Value, new UserStatistics { GlobalRank = RNG.Next(1, 100000) } } + }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); @@ -193,7 +196,10 @@ namespace osu.Game.Tests.Visual.Multiplayer { Id = 0, Username = "User 0", - CurrentModeRank = RNG.Next(1, 100000), + AllStatistics = + { + { Ruleset.Value, new UserStatistics { GlobalRank = RNG.Next(1, 100000) } } + }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 74bc86f279..e78264223e 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -165,10 +165,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID); var currentModeRank = User.User?.GetStatisticsFor(ruleset)?.GlobalRank; - - // fallback to current mode rank for testing purposes. - currentModeRank ??= User.User?.CurrentModeRank; - userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 467f00e409..621d70301d 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -243,7 +243,10 @@ namespace osu.Game.Users [JsonExtensionData] private readonly IDictionary otherProperties = new Dictionary(); - private readonly Dictionary statisticsCache = new Dictionary(); + /// + /// Map for ruleset with their associated user statistics, can be altered for testing purposes. + /// + internal readonly Dictionary AllStatistics = new Dictionary(); /// /// Retrieves the user statistics for a certain ruleset. @@ -254,10 +257,10 @@ namespace osu.Game.Users // todo: this should likely be moved to a separate UserCompact class at some point. public UserStatistics GetStatisticsFor(RulesetInfo ruleset) { - if (statisticsCache.TryGetValue(ruleset, out var existing)) + if (AllStatistics.TryGetValue(ruleset, out var existing)) return existing; - return statisticsCache[ruleset] = parseStatisticsFor(ruleset); + return AllStatistics[ruleset] = parseStatisticsFor(ruleset); } private UserStatistics parseStatisticsFor(RulesetInfo ruleset) From 1466f36649f12edf22942b01c5bf278b1ee55f17 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 08:55:50 +0300 Subject: [PATCH 6614/6909] Improve documentation on `Statistics` --- osu.Game/Users/User.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 621d70301d..58f25703fc 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -185,9 +185,8 @@ namespace osu.Game.Users private UserStatistics statistics; /// - /// The user statistics of the ruleset specified within the API request. - /// If the user is fetched from a or similar - /// (i.e. is a user compact instance), use instead. + /// User statistics for the requested ruleset (in the case of a response). + /// Otherwise empty. /// [JsonProperty(@"statistics")] public UserStatistics Statistics From 62514f23b59334ef3c6736941379a34c5ff0daf0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 08:56:01 +0300 Subject: [PATCH 6615/6909] Remove unnecessary json settings override --- osu.Game/Users/User.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 58f25703fc..b8ca345f5c 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -267,9 +267,7 @@ namespace osu.Game.Users if (!(otherProperties.TryGetValue($"statistics_{ruleset.ShortName}", out var token))) return new UserStatistics(); - var settings = JsonSerializableExtensions.CreateGlobalSettings(); - settings.DefaultValueHandling = DefaultValueHandling.Include; - return token.ToObject(JsonSerializer.Create(settings)); + return token.ToObject(JsonSerializer.Create(JsonSerializableExtensions.CreateGlobalSettings())); } public override string ToString() => Username; From d15ffff9a57e6a09a1ecb6e196165eccd16c72e4 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Feb 2021 09:54:17 +0300 Subject: [PATCH 6616/6909] Simplifiy user statistics retrieval to one-time on deserialization --- .../TestSceneMultiplayerParticipantsList.cs | 4 +- .../Participants/ParticipantPanel.cs | 3 +- osu.Game/Users/User.cs | 42 +++++++------------ 3 files changed, 20 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 5a0234e379..5da5ab74b2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -157,7 +157,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Username = $"User {i}", AllStatistics = { - { Ruleset.Value, new UserStatistics { GlobalRank = RNG.Next(1, 100000) } } + { Ruleset.Value.ShortName, new UserStatistics { GlobalRank = RNG.Next(1, 100000) } } }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); @@ -198,7 +198,7 @@ namespace osu.Game.Tests.Visual.Multiplayer Username = "User 0", AllStatistics = { - { Ruleset.Value, new UserStatistics { GlobalRank = RNG.Next(1, 100000) } } + { Ruleset.Value.ShortName, new UserStatistics { GlobalRank = RNG.Next(1, 100000) } } }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index e78264223e..49d3bfc2dc 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Extensions.Color4Extensions; @@ -164,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID); - var currentModeRank = User.User?.GetStatisticsFor(ruleset)?.GlobalRank; + var currentModeRank = User.User?.AllStatistics.GetValueOrDefault(ruleset.ShortName)?.GlobalRank; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index b8ca345f5c..2c2f293aac 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -5,13 +5,13 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using System.Runtime.Serialization; using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using osu.Framework.Bindables; using osu.Game.IO.Serialization; using osu.Game.Online.API.Requests; -using osu.Game.Rulesets; namespace osu.Game.Users { @@ -238,36 +238,26 @@ namespace osu.Game.Users [JsonProperty("replays_watched_counts")] public UserHistoryCount[] ReplaysWatchedCounts; + /// + /// All user statistics per ruleset's short name (in the case of a response). + /// Otherwise empty. Can be altered for testing purposes. + /// + // todo: this should likely be moved to a separate UserCompact class at some point. + [UsedImplicitly] + public readonly Dictionary AllStatistics = new Dictionary(); + [UsedImplicitly] [JsonExtensionData] private readonly IDictionary otherProperties = new Dictionary(); - /// - /// Map for ruleset with their associated user statistics, can be altered for testing purposes. - /// - internal readonly Dictionary AllStatistics = new Dictionary(); - - /// - /// Retrieves the user statistics for a certain ruleset. - /// If user is fetched from a , - /// this will always return empty instance, use instead. - /// - /// The ruleset to retrieve statistics for. - // todo: this should likely be moved to a separate UserCompact class at some point. - public UserStatistics GetStatisticsFor(RulesetInfo ruleset) + [OnDeserialized] + private void onDeserialized(StreamingContext context) { - if (AllStatistics.TryGetValue(ruleset, out var existing)) - return existing; - - return AllStatistics[ruleset] = parseStatisticsFor(ruleset); - } - - private UserStatistics parseStatisticsFor(RulesetInfo ruleset) - { - if (!(otherProperties.TryGetValue($"statistics_{ruleset.ShortName}", out var token))) - return new UserStatistics(); - - return token.ToObject(JsonSerializer.Create(JsonSerializableExtensions.CreateGlobalSettings())); + foreach (var kvp in otherProperties.Where(kvp => kvp.Key.StartsWith("statistics_", StringComparison.Ordinal))) + { + var shortName = kvp.Key.Replace("statistics_", string.Empty); + AllStatistics[shortName] = kvp.Value.ToObject(JsonSerializer.Create(JsonSerializableExtensions.CreateGlobalSettings())); + } } public override string ToString() => Username; From e838a5d9f09ae14738981a9d49ec002f353952e6 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 15 Feb 2021 18:38:54 -0800 Subject: [PATCH 6617/6909] Fix comment edited at date not showing tooltip --- osu.Game/Overlays/Comments/DrawableComment.cs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/osu.Game/Overlays/Comments/DrawableComment.cs b/osu.Game/Overlays/Comments/DrawableComment.cs index 31aa41e967..7c47ac655f 100644 --- a/osu.Game/Overlays/Comments/DrawableComment.cs +++ b/osu.Game/Overlays/Comments/DrawableComment.cs @@ -9,7 +9,6 @@ using osuTK; using osu.Game.Online.API.Requests.Responses; using osu.Game.Users.Drawables; using osu.Game.Graphics.Containers; -using osu.Game.Utils; using osu.Framework.Graphics.Cursor; using osu.Framework.Bindables; using System.Linq; @@ -245,11 +244,32 @@ namespace osu.Game.Overlays.Comments if (Comment.EditedAt.HasValue) { - info.Add(new OsuSpriteText + var font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular); + var colour = colourProvider.Foreground1; + + info.Add(new FillFlowContainer { - Font = OsuFont.GetFont(size: 12, weight: FontWeight.Regular), - Text = $@"edited {HumanizerUtils.Humanize(Comment.EditedAt.Value)} by {Comment.EditedUser.Username}", - Colour = colourProvider.Foreground1 + AutoSizeAxes = Axes.Both, + Children = new Drawable[] + { + new OsuSpriteText + { + Font = font, + Text = "edited ", + Colour = colour + }, + new DrawableDate(Comment.EditedAt.Value) + { + Font = font, + Colour = colour + }, + new OsuSpriteText + { + Font = font, + Text = $@" by {Comment.EditedUser.Username}", + Colour = colour + }, + } }); } From 02417697e994eb10183d4ba115c3085351a04a6d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Feb 2021 13:32:14 +0900 Subject: [PATCH 6618/6909] Display remaining attempts for playlist rooms with room-level attempt limits --- osu.Game/Online/Rooms/ItemAttemptsCount.cs | 19 +++++++++++++ .../Online/Rooms/PlaylistAggregateScore.cs | 16 +++++++++++ osu.Game/Online/Rooms/Room.cs | 5 ++++ .../Components/RoomLocalUserInfo.cs | 27 +++++++++++++++---- .../Screens/OnlinePlay/OnlinePlayComposite.cs | 3 +++ 5 files changed, 65 insertions(+), 5 deletions(-) create mode 100644 osu.Game/Online/Rooms/ItemAttemptsCount.cs create mode 100644 osu.Game/Online/Rooms/PlaylistAggregateScore.cs diff --git a/osu.Game/Online/Rooms/ItemAttemptsCount.cs b/osu.Game/Online/Rooms/ItemAttemptsCount.cs new file mode 100644 index 0000000000..298603d778 --- /dev/null +++ b/osu.Game/Online/Rooms/ItemAttemptsCount.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 Newtonsoft.Json; + +namespace osu.Game.Online.Rooms +{ + /// + /// Represents attempts on a specific playlist item. + /// + public class ItemAttemptsCount + { + [JsonProperty("id")] + public int PlaylistItemID { get; set; } + + [JsonProperty("attempts")] + public int Attempts { get; set; } + } +} diff --git a/osu.Game/Online/Rooms/PlaylistAggregateScore.cs b/osu.Game/Online/Rooms/PlaylistAggregateScore.cs new file mode 100644 index 0000000000..61e0951cd5 --- /dev/null +++ b/osu.Game/Online/Rooms/PlaylistAggregateScore.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 Newtonsoft.Json; + +namespace osu.Game.Online.Rooms +{ + /// + /// Represents aggregated score for the local user for a playlist. + /// + public class PlaylistAggregateScore + { + [JsonProperty("playlist_item_attempts")] + public ItemAttemptsCount[] PlaylistItemAttempts { get; set; } + } +} diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 763ba25d52..10a60ab374 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -72,6 +72,10 @@ namespace osu.Game.Online.Rooms [JsonIgnore] public readonly Bindable MaxParticipants = new Bindable(); + [Cached] + [JsonProperty("current_user_score")] + public readonly Bindable UserScore = new Bindable(); + [Cached] [JsonProperty("recent_participants")] public readonly BindableList RecentParticipants = new BindableList(); @@ -144,6 +148,7 @@ namespace osu.Game.Online.Rooms MaxParticipants.Value = other.MaxParticipants.Value; ParticipantCount.Value = other.ParticipantCount.Value; EndDate.Value = other.EndDate.Value; + UserScore.Value = other.UserScore.Value; if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs b/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs index f52e59b0c8..2206726beb 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.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.Linq; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; @@ -39,12 +41,27 @@ namespace osu.Game.Screens.OnlinePlay.Components { base.LoadComplete(); - MaxAttempts.BindValueChanged(attempts => + MaxAttempts.BindValueChanged(_ => updateAttempts()); + UserScore.BindValueChanged(_ => updateAttempts(), true); + } + + private void updateAttempts() + { + if (MaxAttempts.Value != null) { - attemptDisplay.Text = attempts.NewValue == null - ? string.Empty - : $"Maximum attempts: {attempts.NewValue:N0}"; - }, true); + attemptDisplay.Text = $"Maximum attempts: {MaxAttempts.Value:N0}"; + + if (UserScore.Value != null) + { + int remaining = MaxAttempts.Value.Value - UserScore.Value.PlaylistItemAttempts.Sum(a => a.Attempts); + attemptDisplay.Text += $" ({remaining} remaining)"; + } + } + + else + { + attemptDisplay.Text = string.Empty; + } } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index b2f3e4a1d9..f7a51230eb 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -42,6 +42,9 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable MaxAttempts { get; private set; } + [Resolved(typeof(Room))] + public Bindable UserScore { get; private set; } + [Resolved(typeof(Room))] protected Bindable EndDate { get; private set; } From 5b4999e8afd8779e0da7cd153f3a59beaa59ea3c Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 16 Feb 2021 04:51:21 +0300 Subject: [PATCH 6619/6909] Update user statistics retrieval with API changes --- .../TestSceneMultiplayerParticipantsList.cs | 5 +++-- .../Participants/ParticipantPanel.cs | 2 +- osu.Game/Users/User.cs | 22 +++---------------- 3 files changed, 7 insertions(+), 22 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 5da5ab74b2..a7398ebf02 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -1,6 +1,7 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System.Collections.Generic; using System.Linq; using NUnit.Framework; using osu.Framework.Graphics; @@ -155,7 +156,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Id = i, Username = $"User {i}", - AllStatistics = + RulesetsStatistics = new Dictionary { { Ruleset.Value.ShortName, new UserStatistics { GlobalRank = RNG.Next(1, 100000) } } }, @@ -196,7 +197,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Id = 0, Username = "User 0", - AllStatistics = + RulesetsStatistics = new Dictionary { { Ruleset.Value.ShortName, new UserStatistics { GlobalRank = RNG.Next(1, 100000) } } }, diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 49d3bfc2dc..25bc314f1b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID); - var currentModeRank = User.User?.AllStatistics.GetValueOrDefault(ruleset.ShortName)?.GlobalRank; + var currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 2c2f293aac..4a6fd540c7 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -5,12 +5,9 @@ using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; -using System.Runtime.Serialization; using JetBrains.Annotations; using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using osu.Framework.Bindables; -using osu.Game.IO.Serialization; using osu.Game.Online.API.Requests; namespace osu.Game.Users @@ -243,22 +240,9 @@ namespace osu.Game.Users /// Otherwise empty. Can be altered for testing purposes. /// // todo: this should likely be moved to a separate UserCompact class at some point. - [UsedImplicitly] - public readonly Dictionary AllStatistics = new Dictionary(); - - [UsedImplicitly] - [JsonExtensionData] - private readonly IDictionary otherProperties = new Dictionary(); - - [OnDeserialized] - private void onDeserialized(StreamingContext context) - { - foreach (var kvp in otherProperties.Where(kvp => kvp.Key.StartsWith("statistics_", StringComparison.Ordinal))) - { - var shortName = kvp.Key.Replace("statistics_", string.Empty); - AllStatistics[shortName] = kvp.Value.ToObject(JsonSerializer.Create(JsonSerializableExtensions.CreateGlobalSettings())); - } - } + [JsonProperty("statistics_rulesets")] + [CanBeNull] + public Dictionary RulesetsStatistics { get; set; } public override string ToString() => Username; From 0e7f52b5ccb1ae1a14147d1df9877d22024fde6e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 16 Feb 2021 07:28:51 +0300 Subject: [PATCH 6620/6909] Always use JSON property `global_rank` for global ranks instead --- .../TestSceneMultiplayerParticipantsList.cs | 16 ++++++++++-- .../Participants/ParticipantPanel.cs | 2 +- osu.Game/Users/UserStatistics.cs | 25 ++++++++----------- 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index a7398ebf02..1e14bbbbea 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -158,7 +158,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Username = $"User {i}", RulesetsStatistics = new Dictionary { - { Ruleset.Value.ShortName, new UserStatistics { GlobalRank = RNG.Next(1, 100000) } } + { + Ruleset.Value.ShortName, + new UserStatistics + { + Ranks = new UserStatistics.UserRanks { Global = RNG.Next(1, 100000) } + } + } }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); @@ -199,7 +205,13 @@ namespace osu.Game.Tests.Visual.Multiplayer Username = "User 0", RulesetsStatistics = new Dictionary { - { Ruleset.Value.ShortName, new UserStatistics { GlobalRank = RNG.Next(1, 100000) } } + { + Ruleset.Value.ShortName, + new UserStatistics + { + Ranks = new UserStatistics.UserRanks { Global = RNG.Next(1, 100000) } + } + } }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", }); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 25bc314f1b..c4d11676e7 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID); - var currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank; + var currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.Ranks.Global; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 6c069f674e..1fed908c39 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -3,7 +3,6 @@ using System; using Newtonsoft.Json; -using osu.Game.Online.API.Requests; using osu.Game.Scoring; using osu.Game.Utils; using static osu.Game.Users.User; @@ -27,24 +26,22 @@ namespace osu.Game.Users public int Progress; } - /// - /// This must only be used when coming from condensed user responses (e.g. from ), otherwise use Ranks.Global. - /// - // todo: this should likely be moved to a separate UserStatisticsCompact class at some point. + [JsonProperty(@"rank")] + public UserRanks Ranks; + + // eventually UserRanks object will be completely replaced with separate global and country rank properties, see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53. + // but for now, always point UserRanks.Global to the global_rank property, as that is included solely for requests like GetUsersRequest. [JsonProperty(@"global_rank")] - public int? GlobalRank; - - [JsonProperty(@"pp")] - public decimal? PP; - - [JsonProperty(@"pp_rank")] // the API sometimes only returns this value in condensed user responses - private int? rank + private int? globalRank { set => Ranks.Global = value; } - [JsonProperty(@"rank")] - public UserRanks Ranks; + [JsonProperty(@"pp")] + public decimal? PP; + + [JsonProperty(@"pp_rank")] + public int PPRank; [JsonProperty(@"ranked_score")] public long RankedScore; From e82922f8c545a6e82247c3fafb336094308f6c30 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Feb 2021 13:44:36 +0900 Subject: [PATCH 6621/6909] Add the ability to deselect the currently selected room via clicking away Always felt wrong that you couldn't do this until now. --- .../Multiplayer/TestSceneLoungeRoomsContainer.cs | 14 ++++++++++++++ .../OnlinePlay/Lounge/Components/RoomsContainer.cs | 10 ++++++++++ 2 files changed, 24 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs index 279dcfa584..5682fd5c3c 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneLoungeRoomsContainer.cs @@ -69,6 +69,20 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("last room joined", () => RoomManager.Rooms.Last().Status.Value is JoinedRoomStatus); } + [Test] + public void TestClickDeselection() + { + AddRooms(1); + + AddAssert("no selection", () => checkRoomSelected(null)); + + press(Key.Down); + AddAssert("first room selected", () => checkRoomSelected(RoomManager.Rooms.First())); + + AddStep("click away", () => InputManager.Click(MouseButton.Left)); + AddAssert("no selection", () => checkRoomSelected(null)); + } + private void press(Key down) { AddStep($"press {down}", () => InputManager.Key(down)); diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index f70c33babe..134758d023 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -11,6 +11,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Input.Bindings; +using osu.Framework.Input.Events; using osu.Framework.Threading; using osu.Game.Extensions; using osu.Game.Graphics.Cursor; @@ -42,6 +43,9 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components [Resolved(CanBeNull = true)] private LoungeSubScreen loungeSubScreen { get; set; } + // handle deselection + public override bool ReceivePositionalInputAt(Vector2 screenSpacePos) => true; + public RoomsContainer() { RelativeSizeAxes = Axes.X; @@ -159,6 +163,12 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components JoinRequested?.Invoke(selectedRoom.Value); } + protected override bool OnClick(ClickEvent e) + { + selectRoom(null); + return base.OnClick(e); + } + #region Key selection logic (shared with BeatmapCarousel) public bool OnPressed(GlobalAction action) From e969ca8974d621eb4a089231f65b684f1a3d4f60 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Feb 2021 13:52:42 +0900 Subject: [PATCH 6622/6909] Remove unused using statement that rider could not identify --- osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs b/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs index 2206726beb..c7d4ccd12e 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.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.Graphics; From 31a5cdd8ac3339ed680a948355c1087af3fa3fd7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Feb 2021 14:02:21 +0900 Subject: [PATCH 6623/6909] Fix current selection not updating visually after creating a new playlist --- .../Lounge/Components/RoomsContainer.cs | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs index f70c33babe..40102b1693 100644 --- a/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs +++ b/osu.Game/Screens/OnlinePlay/Lounge/Components/RoomsContainer.cs @@ -69,8 +69,16 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components rooms.BindTo(roomManager.Rooms); filter?.BindValueChanged(criteria => Filter(criteria.NewValue)); + + selectedRoom.BindValueChanged(selection => + { + updateSelection(); + }, true); } + private void updateSelection() => + roomFlow.Children.ForEach(r => r.State = r.Room == selectedRoom.Value ? SelectionState.Selected : SelectionState.NotSelected); + public void Filter(FilterCriteria criteria) { roomFlow.Children.ForEach(r => @@ -125,6 +133,8 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components } Filter(filter?.Value); + + updateSelection(); } private void removeRooms(IEnumerable rooms) @@ -146,11 +156,7 @@ namespace osu.Game.Screens.OnlinePlay.Lounge.Components roomFlow.SetLayoutPosition(room, room.Room.Position.Value); } - private void selectRoom(Room room) - { - roomFlow.Children.ForEach(r => r.State = r.Room == room ? SelectionState.Selected : SelectionState.NotSelected); - selectedRoom.Value = room; - } + private void selectRoom(Room room) => selectedRoom.Value = room; private void joinSelected() { From 9ed45ce1ca49c365a9a54cd7af71c7513ba5f233 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Feb 2021 14:31:00 +0900 Subject: [PATCH 6624/6909] Remove redundant double call to ValueChanged on UserMods change --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 2f50bee677..cc63f53ac0 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -271,7 +271,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Playlist.BindCollectionChanged(onPlaylistChanged, true); BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); - UserMods.BindValueChanged(onUserModsChanged); client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; From 52e544aa678a5a2e2d42160299d2bf9cdfd88170 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Feb 2021 14:42:31 +0900 Subject: [PATCH 6625/6909] Revert "Remove redundant double call to ValueChanged on UserMods change" This reverts commit 9ed45ce1ca49c365a9a54cd7af71c7513ba5f233. --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index cc63f53ac0..2f50bee677 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -271,6 +271,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Playlist.BindCollectionChanged(onPlaylistChanged, true); BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); + UserMods.BindValueChanged(onUserModsChanged); client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; From da42c6d2825d1a97d048c29f2bd4730b1dd989d5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Feb 2021 15:14:19 +0900 Subject: [PATCH 6626/6909] Expose FreeMods from OnlinePlaySongSelect --- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index b201c62b7f..3f2873cbc4 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -31,7 +31,8 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room), nameof(Room.Playlist))] protected BindableList Playlist { get; private set; } - private readonly Bindable> freeMods = new Bindable>(Array.Empty()); + protected readonly Bindable> FreeMods = new Bindable>(Array.Empty()); + private readonly FreeModSelectOverlay freeModSelectOverlay; private WorkingBeatmap initialBeatmap; @@ -45,7 +46,7 @@ namespace osu.Game.Screens.OnlinePlay freeModSelectOverlay = new FreeModSelectOverlay { - SelectedMods = { BindTarget = freeMods }, + SelectedMods = { BindTarget = FreeMods }, IsValidMod = IsValidFreeMod, }; } @@ -67,14 +68,14 @@ namespace osu.Game.Screens.OnlinePlay // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. // Similarly, freeMods is currently empty but should only contain the allowed mods. Mods.Value = Playlist.FirstOrDefault()?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); - freeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + FreeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); Ruleset.BindValueChanged(onRulesetChanged); } private void onRulesetChanged(ValueChangedEvent ruleset) { - freeMods.Value = Array.Empty(); + FreeMods.Value = Array.Empty(); } protected sealed override bool OnStart() @@ -90,7 +91,7 @@ namespace osu.Game.Screens.OnlinePlay item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); item.AllowedMods.Clear(); - item.AllowedMods.AddRange(freeMods.Value.Select(m => m.CreateCopy())); + item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy())); SelectItem(item); return true; @@ -133,7 +134,7 @@ namespace osu.Game.Screens.OnlinePlay protected override IEnumerable<(FooterButton, OverlayContainer)> CreateFooterButtons() { var buttons = base.CreateFooterButtons().ToList(); - buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = freeMods }, freeModSelectOverlay)); + buttons.Insert(buttons.FindIndex(b => b.Item1 is FooterButtonMods) + 1, (new FooterButtonFreeMods { Current = FreeMods }, freeModSelectOverlay)); return buttons; } From fff1cb0b355e9b8984dbc950dd8d2d4412e01a94 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Feb 2021 15:13:57 +0900 Subject: [PATCH 6627/6909] Fix allowed mods not being copied when populating playlist items --- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs index 0e8db6dfe5..21335fc90c 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsSongSelect.cs @@ -56,6 +56,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists item.RequiredMods.Clear(); item.RequiredMods.AddRange(Mods.Value.Select(m => m.CreateCopy())); + + item.AllowedMods.Clear(); + item.AllowedMods.AddRange(FreeMods.Value.Select(m => m.CreateCopy())); } } } From 97a7572cb8daebc642cabe44e4ef8bbd1a0c97b4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Feb 2021 15:14:48 +0900 Subject: [PATCH 6628/6909] Move UserModSelectOverlay to RoomSubScreen for Playlists consumption --- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 68 ++++++++++++++++++- .../Multiplayer/MultiplayerMatchSubScreen.cs | 49 +------------ 2 files changed, 67 insertions(+), 50 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index e755f8c405..24d42283f7 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -3,16 +3,20 @@ using System; using System.Collections.Generic; +using System.Collections.Specialized; using System.Linq; using osu.Framework.Allocation; using osu.Framework.Audio; using osu.Framework.Audio.Sample; using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Online.Rooms; using osu.Game.Overlays; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Play; @@ -25,6 +29,14 @@ namespace osu.Game.Screens.OnlinePlay.Match public override bool DisallowExternalBeatmapRulesetChanges => true; + private readonly ModSelectOverlay userModsSelectOverlay; + + /// + /// A container that provides controls for selection of user mods. + /// This will be shown/hidden automatically when applicable. + /// + protected Drawable UserModsSection; + private Sample sampleStart; [Resolved(typeof(Room), nameof(Room.Playlist))] @@ -53,9 +65,26 @@ namespace osu.Game.Screens.OnlinePlay.Match protected RoomSubScreen() { - AddInternal(BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker + AddRangeInternal(new Drawable[] { - SelectedItem = { BindTarget = SelectedItem } + BeatmapAvailablilityTracker = new OnlinePlayBeatmapAvailablilityTracker + { + SelectedItem = { BindTarget = SelectedItem } + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Depth = float.MinValue, + RelativeSizeAxes = Axes.Both, + Height = 0.5f, + Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }, + Child = userModsSelectOverlay = new UserModSelectOverlay + { + SelectedMods = { BindTarget = UserMods }, + IsValidMod = _ => false + } + }, }); } @@ -73,7 +102,8 @@ namespace osu.Game.Screens.OnlinePlay.Match base.LoadComplete(); SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); - SelectedItem.Value = Playlist.FirstOrDefault(); + + Playlist.BindCollectionChanged(onPlaylistChanged, true); managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); @@ -81,6 +111,22 @@ namespace osu.Game.Screens.OnlinePlay.Match UserMods.BindValueChanged(_ => Scheduler.AddOnce(UpdateMods)); } + public override bool OnBackButton() + { + if (userModsSelectOverlay.State.Value == Visibility.Visible) + { + userModsSelectOverlay.Hide(); + return true; + } + + return base.OnBackButton(); + } + + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) => + SelectedItem.Value = Playlist.FirstOrDefault(); + + protected void ShowUserModSelect() => userModsSelectOverlay.Show(); + public override void OnEntering(IScreen last) { base.OnEntering(last); @@ -131,6 +177,18 @@ namespace osu.Game.Screens.OnlinePlay.Match UpdateMods(); Ruleset.Value = SelectedItem.Value.Ruleset.Value; + + if (SelectedItem.Value?.AllowedMods.Any() != true) + { + UserModsSection?.Hide(); + userModsSelectOverlay.Hide(); + userModsSelectOverlay.IsValidMod = _ => false; + } + else + { + UserModsSection?.Show(); + userModsSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType()); + } } private void beatmapUpdated(ValueChangedEvent> weakSet) => Schedule(updateWorkingBeatmap); @@ -190,5 +248,9 @@ namespace osu.Game.Screens.OnlinePlay.Match track.RestartPoint = 0; } } + + private class UserModSelectOverlay : LocalPlayerModSelectOverlay + { + } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 2f50bee677..49ac9f64ff 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; @@ -16,7 +15,6 @@ using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; -using osu.Game.Overlays.Mods; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -43,9 +41,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer [Resolved] private OngoingOperationTracker ongoingOperationTracker { get; set; } - private ModSelectOverlay userModsSelectOverlay; private MultiplayerMatchSettingsOverlay settingsOverlay; - private Drawable userModsSection; private IBindable isConnected; @@ -155,7 +151,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new BeatmapSelectionControl { RelativeSizeAxes = Axes.X } } }, - userModsSection = new FillFlowContainer + UserModsSection = new FillFlowContainer { RelativeSizeAxes = Axes.X, AutoSizeAxes = Axes.Y, @@ -176,7 +172,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Origin = Anchor.CentreLeft, Width = 90, Text = "Select", - Action = () => userModsSelectOverlay.Show() + Action = ShowUserModSelect, }, new ModDisplay { @@ -231,19 +227,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer new Dimension(GridSizeMode.AutoSize), } }, - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.Both, - Height = 0.5f, - Padding = new MarginPadding { Horizontal = HORIZONTAL_OVERFLOW_PADDING }, - Child = userModsSelectOverlay = new UserModSelectOverlay - { - SelectedMods = { BindTarget = UserMods }, - IsValidMod = _ => false - } - }, settingsOverlay = new MultiplayerMatchSettingsOverlay { RelativeSizeAxes = Axes.Both, @@ -269,7 +252,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - Playlist.BindCollectionChanged(onPlaylistChanged, true); BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); UserMods.BindValueChanged(onUserModsChanged); @@ -303,32 +285,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return true; } - if (userModsSelectOverlay.State.Value == Visibility.Visible) - { - userModsSelectOverlay.Hide(); - return true; - } - return base.OnBackButton(); } - private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) - { - SelectedItem.Value = Playlist.FirstOrDefault(); - - if (SelectedItem.Value?.AllowedMods.Any() != true) - { - userModsSection.Hide(); - userModsSelectOverlay.Hide(); - userModsSelectOverlay.IsValidMod = _ => false; - } - else - { - userModsSection.Show(); - userModsSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType()); - } - } - private ModSettingChangeTracker modSettingChangeTracker; private ScheduledDelegate debouncedModSettingsUpdate; @@ -433,9 +392,5 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer modSettingChangeTracker?.Dispose(); } - - private class UserModSelectOverlay : LocalPlayerModSelectOverlay - { - } } } From fdcb6384cb808e3f616a73a8343c98c5956cacf7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 16 Feb 2021 15:14:56 +0900 Subject: [PATCH 6629/6909] Add user mod selection to playlists room screen --- .../Playlists/PlaylistsRoomSubScreen.cs | 46 ++++++++++++++++++- 1 file changed, 45 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 88731a10bc..31c441bcd2 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -13,7 +13,9 @@ using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Screens.OnlinePlay.Match.Components; +using osu.Game.Screens.Play.HUD; using osu.Game.Users; +using osuTK; using Footer = osu.Game.Screens.OnlinePlay.Match.Components.Footer; namespace osu.Game.Screens.OnlinePlay.Playlists @@ -140,13 +142,55 @@ namespace osu.Game.Screens.OnlinePlay.Playlists RelativeSizeAxes = Axes.Both, Content = new[] { - new Drawable[] { new OverlinedHeader("Leaderboard"), }, + new[] + { + UserModsSection = new FillFlowContainer + { + RelativeSizeAxes = Axes.X, + AutoSizeAxes = Axes.Y, + Margin = new MarginPadding { Bottom = 10 }, + Children = new Drawable[] + { + new OverlinedHeader("Extra mods"), + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(10, 0), + Children = new Drawable[] + { + new PurpleTriangleButton + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Width = 90, + Text = "Select", + Action = ShowUserModSelect, + }, + new ModDisplay + { + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + DisplayUnrankedText = false, + Current = UserMods, + Scale = new Vector2(0.8f), + }, + } + } + } + }, + }, + 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(GridSizeMode.AutoSize), new Dimension(), new Dimension(GridSizeMode.AutoSize), From f25b5147ef319c42329fd645490bf7ecc1907eb1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Feb 2021 15:37:45 +0900 Subject: [PATCH 6630/6909] Select last playlist item in match subscreen --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index c5130baa94..76608fb5c1 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -302,7 +302,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) { - SelectedItem.Value = Playlist.FirstOrDefault(); + SelectedItem.Value = Playlist.LastOrDefault(); if (SelectedItem.Value?.AllowedMods.Any() != true) { From 855d24dce769ae8ab65bd8f2daadfe638e6a76d8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Feb 2021 17:35:44 +0900 Subject: [PATCH 6631/6909] Cache selected item bindable from RoomSubScreen --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 1 + osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 10 ++++++++++ 2 files changed, 11 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 2946d07588..6a2844fa74 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -21,6 +21,7 @@ namespace osu.Game.Screens.OnlinePlay.Match [Cached(typeof(IPreviewTrackOwner))] public abstract class RoomSubScreen : OnlinePlaySubScreen, IPreviewTrackOwner { + [Cached(typeof(IBindable))] protected readonly Bindable SelectedItem = new Bindable(); public override bool DisallowExternalBeatmapRulesetChanges => true; diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index b2f3e4a1d9..c1aa93526f 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -2,10 +2,12 @@ // 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; using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Match; using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay @@ -50,5 +52,13 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room))] protected Bindable Duration { get; private set; } + + /// + /// The currently selected item in the . + /// May be null if this is not inside a . + /// + [CanBeNull] + [Resolved(typeof(Room), CanBeNull = true)] + protected IBindable SelectedItem { get; private set; } } } From 3ff9e14e35f851b32f40eb2339b6652beea37c2c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Feb 2021 18:56:13 +0900 Subject: [PATCH 6632/6909] Make StatefulMultiplayerClient control current playlist item --- .../Multiplayer/MultiplayerRoomSettings.cs | 12 +++++-- .../Multiplayer/StatefulMultiplayerClient.cs | 35 ++++++++++--------- .../Screens/OnlinePlay/Match/RoomSubScreen.cs | 4 --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 13 ++++--- .../Playlists/PlaylistsRoomSubScreen.cs | 7 ++-- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 4fb9d724b5..04752f4e6f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -36,18 +36,26 @@ namespace osu.Game.Online.Multiplayer [Key(5)] public IEnumerable AllowedMods { get; set; } = Enumerable.Empty(); + /// + /// Only used for client-side mutation. + /// + [Key(6)] + public int PlaylistItemId { get; set; } + public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID && BeatmapChecksum == other.BeatmapChecksum && RequiredMods.SequenceEqual(other.RequiredMods) && AllowedMods.SequenceEqual(other.AllowedMods) && RulesetID == other.RulesetID - && Name.Equals(other.Name, StringComparison.Ordinal); + && Name.Equals(other.Name, StringComparison.Ordinal) + && PlaylistItemId == other.PlaylistItemId; public override string ToString() => $"Name:{Name}" + $" Beatmap:{BeatmapID} ({BeatmapChecksum})" + $" RequiredMods:{string.Join(',', RequiredMods)}" + $" AllowedMods:{string.Join(',', AllowedMods)}" - + $" Ruleset:{RulesetID}"; + + $" Ruleset:{RulesetID}" + + $" Item:{PlaylistItemId}"; } } diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 18464a5f61..f5f4c3a8ba 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -66,6 +66,8 @@ namespace osu.Game.Online.Multiplayer /// public readonly BindableList CurrentMatchPlayingUserIds = new BindableList(); + public readonly Bindable CurrentMatchPlayingItem = new Bindable(); + /// /// The corresponding to the local player, if available. /// @@ -94,9 +96,6 @@ namespace osu.Game.Online.Multiplayer private Room? apiRoom; - // Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise. - private int playlistItemId; - [BackgroundDependencyLoader] private void load() { @@ -142,7 +141,6 @@ namespace osu.Game.Online.Multiplayer { Room = joinedRoom; apiRoom = room; - playlistItemId = room.Playlist.SingleOrDefault()?.ID ?? 0; }, cancellationSource.Token); // Update room settings. @@ -218,7 +216,8 @@ namespace osu.Game.Online.Multiplayer BeatmapChecksum = item.GetOr(existingPlaylistItem).Beatmap.Value.MD5Hash, RulesetID = item.GetOr(existingPlaylistItem).RulesetID, RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods, - AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods + AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods, + PlaylistItemId = Room.Settings.PlaylistItemId, }); } @@ -506,14 +505,13 @@ namespace osu.Game.Online.Multiplayer Room.Settings = settings; apiRoom.Name.Value = Room.Settings.Name; - // The playlist update is delayed until an online beatmap lookup (below) succeeds. - // In-order for the client to not display an outdated beatmap, the playlist is forcefully cleared here. - apiRoom.Playlist.Clear(); + // The current item update is delayed until an online beatmap lookup (below) succeeds. + // In-order for the client to not display an outdated beatmap, the current item is forcefully cleared here. + CurrentMatchPlayingItem.Value = null; RoomUpdated?.Invoke(); var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => { if (cancellationToken.IsCancellationRequested) @@ -540,18 +538,21 @@ namespace osu.Game.Online.Multiplayer var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset)); var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); - PlaylistItem playlistItem = new PlaylistItem - { - ID = playlistItemId, - Beatmap = { Value = beatmap }, - Ruleset = { Value = ruleset.RulesetInfo }, - }; + // Update an existing playlist item from the API room, or create a new item. + var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId); + if (playlistItem == null) + apiRoom.Playlist.Add(playlistItem = new PlaylistItem()); + + playlistItem.ID = settings.PlaylistItemId; + playlistItem.Beatmap.Value = beatmap; + playlistItem.Ruleset.Value = ruleset.RulesetInfo; + playlistItem.RequiredMods.Clear(); playlistItem.RequiredMods.AddRange(mods); + playlistItem.AllowedMods.Clear(); playlistItem.AllowedMods.AddRange(allowedMods); - apiRoom.Playlist.Clear(); // Clearing should be unnecessary, but here for sanity. - apiRoom.Playlist.Add(playlistItem); + CurrentMatchPlayingItem.Value = playlistItem; } /// diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 6a2844fa74..7740af9f63 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -28,9 +28,6 @@ namespace osu.Game.Screens.OnlinePlay.Match private Sample sampleStart; - [Resolved(typeof(Room), nameof(Room.Playlist))] - protected BindableList Playlist { get; private set; } - /// /// Any mods applied by/to the local user. /// @@ -74,7 +71,6 @@ namespace osu.Game.Screens.OnlinePlay.Match base.LoadComplete(); SelectedItem.BindValueChanged(_ => Scheduler.AddOnce(selectedItemChanged)); - SelectedItem.Value = Playlist.FirstOrDefault(); managerUpdated = beatmapManager.ItemUpdated.GetBoundCopy(); managerUpdated.BindValueChanged(beatmapUpdated); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 76608fb5c1..f55b5b9713 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -3,7 +3,6 @@ using System; using System.Collections.Generic; -using System.Collections.Specialized; using System.Diagnostics; using System.Linq; using JetBrains.Annotations; @@ -269,7 +268,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - Playlist.BindCollectionChanged(onPlaylistChanged, true); + SelectedItem.BindValueChanged(onSelectedItemChanged); + SelectedItem.BindTo(client.CurrentMatchPlayingItem); + BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); UserMods.BindValueChanged(onUserModsChanged); @@ -300,11 +301,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return base.OnBackButton(); } - private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) + private void onSelectedItemChanged(ValueChangedEvent item) { - SelectedItem.Value = Playlist.LastOrDefault(); - - if (SelectedItem.Value?.AllowedMods.Any() != true) + if (item.NewValue?.AllowedMods.Any() != true) { userModsSection.Hide(); userModsSelectOverlay.Hide(); @@ -313,7 +312,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer else { userModsSection.Show(); - userModsSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType()); + userModsSelectOverlay.IsValidMod = m => item.NewValue.AllowedMods.Any(a => a.GetType() == m.GetType()); } } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 88731a10bc..b4870ab580 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -27,6 +27,9 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved(typeof(Room), nameof(Room.RoomID))] private Bindable roomId { get; set; } + [Resolved(typeof(Room), nameof(Room.Playlist))] + private BindableList playlist { get; set; } + private MatchSettingsOverlay settingsOverlay; private MatchLeaderboard leaderboard; @@ -117,7 +120,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists new DrawableRoomPlaylistWithResults { RelativeSizeAxes = Axes.Both, - Items = { BindTarget = Playlist }, + Items = { BindTarget = playlist }, SelectedItem = { BindTarget = SelectedItem }, RequestShowResults = item => { @@ -222,7 +225,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists // Set the first playlist item. // This is scheduled since updating the room and playlist may happen in an arbitrary order (via Room.CopyFrom()). - Schedule(() => SelectedItem.Value = Playlist.FirstOrDefault()); + Schedule(() => SelectedItem.Value = playlist.FirstOrDefault()); } }, true); } From 2a1096a3c8ca7a3c5f656963dcbcb2c6e7770eed Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Feb 2021 19:02:16 +0900 Subject: [PATCH 6633/6909] Make BeatmapSelectionControl use the selected item --- .../Match/BeatmapSelectionControl.cs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index f17e04d4d4..769596956b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -1,14 +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.Specialized; -using System.Linq; +using System.Diagnostics; using osu.Framework.Allocation; +using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Online.API; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Match.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match @@ -60,7 +61,9 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - Playlist.BindCollectionChanged(onPlaylistChanged, true); + Debug.Assert(SelectedItem != null); + SelectedItem.BindValueChanged(onSelectedItemChanged, true); + Host.BindValueChanged(host => { if (RoomID.Value == null || host.NewValue?.Equals(api.LocalUser.Value) == true) @@ -70,12 +73,12 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }, true); } - private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) + private void onSelectedItemChanged(ValueChangedEvent selectedItem) { - if (Playlist.Any()) - beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(Playlist.Single(), false, false); - else + if (selectedItem.NewValue == null) beatmapPanelContainer.Clear(); + else + beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(selectedItem.NewValue, false, false); } } } From e24a5949c5a61c105e9f4670c07cdbccf0d7def6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Feb 2021 19:26:51 +0900 Subject: [PATCH 6634/6909] Fix resolve --- osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index c1aa93526f..4c4b1ff6d0 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -58,7 +58,7 @@ namespace osu.Game.Screens.OnlinePlay /// May be null if this is not inside a . /// [CanBeNull] - [Resolved(typeof(Room), CanBeNull = true)] + [Resolved(CanBeNull = true)] protected IBindable SelectedItem { get; private set; } } } From 3e802531d384761c0be4eeabed1d9c923ada7fc0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Feb 2021 19:29:40 +0900 Subject: [PATCH 6635/6909] Use long type where required in multiplayer --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 2 +- osu.Game/Online/Rooms/CreateRoomScoreRequest.cs | 6 +++--- osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs | 4 ++-- osu.Game/Online/Rooms/GetRoomRequest.cs | 4 ++-- osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs | 8 ++++---- osu.Game/Online/Rooms/MultiplayerScore.cs | 2 +- osu.Game/Online/Rooms/PlaylistItem.cs | 2 +- osu.Game/Online/Rooms/Room.cs | 2 +- osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs | 6 +++--- osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs | 8 ++++---- osu.Game/Screens/OnlinePlay/Components/RoomManager.cs | 2 +- .../OnlinePlay/Match/Components/MatchChatDisplay.cs | 2 +- .../OnlinePlay/Match/Components/MatchLeaderboard.cs | 2 +- .../Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs | 2 +- .../OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs | 2 +- osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs | 2 +- osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs | 2 +- .../OnlinePlay/Playlists/PlaylistsResultsScreen.cs | 4 ++-- .../OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs | 2 +- 19 files changed, 32 insertions(+), 32 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 18464a5f61..639dce9230 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -95,7 +95,7 @@ namespace osu.Game.Online.Multiplayer private Room? apiRoom; // Todo: This is temporary, until the multiplayer server returns the item id on match start or otherwise. - private int playlistItemId; + private long playlistItemId; [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs index afd0dadc7e..d4303e77df 100644 --- a/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/CreateRoomScoreRequest.cs @@ -9,11 +9,11 @@ namespace osu.Game.Online.Rooms { public class CreateRoomScoreRequest : APIRequest { - private readonly int roomId; - private readonly int playlistItemId; + private readonly long roomId; + private readonly long playlistItemId; private readonly string versionHash; - public CreateRoomScoreRequest(int roomId, int playlistItemId, string versionHash) + public CreateRoomScoreRequest(long roomId, long playlistItemId, string versionHash) { this.roomId = roomId; this.playlistItemId = playlistItemId; diff --git a/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs index 15f1221a00..67e2a2b27f 100644 --- a/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomLeaderboardRequest.cs @@ -7,9 +7,9 @@ namespace osu.Game.Online.Rooms { public class GetRoomLeaderboardRequest : APIRequest { - private readonly int roomId; + private readonly long roomId; - public GetRoomLeaderboardRequest(int roomId) + public GetRoomLeaderboardRequest(long roomId) { this.roomId = roomId; } diff --git a/osu.Game/Online/Rooms/GetRoomRequest.cs b/osu.Game/Online/Rooms/GetRoomRequest.cs index ce117075c7..853873901e 100644 --- a/osu.Game/Online/Rooms/GetRoomRequest.cs +++ b/osu.Game/Online/Rooms/GetRoomRequest.cs @@ -7,9 +7,9 @@ namespace osu.Game.Online.Rooms { public class GetRoomRequest : APIRequest { - public readonly int RoomId; + public readonly long RoomId; - public GetRoomRequest(int roomId) + public GetRoomRequest(long roomId) { RoomId = roomId; } diff --git a/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs b/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs index 43f80a2dc4..abce2093e3 100644 --- a/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs +++ b/osu.Game/Online/Rooms/IndexPlaylistScoresRequest.cs @@ -15,8 +15,8 @@ namespace osu.Game.Online.Rooms /// public class IndexPlaylistScoresRequest : APIRequest { - public readonly int RoomId; - public readonly int PlaylistItemId; + public readonly long RoomId; + public readonly long PlaylistItemId; [CanBeNull] public readonly Cursor Cursor; @@ -24,13 +24,13 @@ namespace osu.Game.Online.Rooms [CanBeNull] public readonly IndexScoresParams IndexParams; - public IndexPlaylistScoresRequest(int roomId, int playlistItemId) + public IndexPlaylistScoresRequest(long roomId, long playlistItemId) { RoomId = roomId; PlaylistItemId = playlistItemId; } - public IndexPlaylistScoresRequest(int roomId, int playlistItemId, [NotNull] Cursor cursor, [NotNull] IndexScoresParams indexParams) + public IndexPlaylistScoresRequest(long roomId, long playlistItemId, [NotNull] Cursor cursor, [NotNull] IndexScoresParams indexParams) : this(roomId, playlistItemId) { Cursor = cursor; diff --git a/osu.Game/Online/Rooms/MultiplayerScore.cs b/osu.Game/Online/Rooms/MultiplayerScore.cs index 677a3d3026..30c1d2f826 100644 --- a/osu.Game/Online/Rooms/MultiplayerScore.cs +++ b/osu.Game/Online/Rooms/MultiplayerScore.cs @@ -18,7 +18,7 @@ namespace osu.Game.Online.Rooms public class MultiplayerScore { [JsonProperty("id")] - public int ID { get; set; } + public long ID { get; set; } [JsonProperty("user")] public User User { get; set; } diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index ada2140ca6..61982101c1 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -15,7 +15,7 @@ namespace osu.Game.Online.Rooms public class PlaylistItem : IEquatable { [JsonProperty("id")] - public int ID { get; set; } + public long ID { get; set; } [JsonProperty("beatmap_id")] public int BeatmapID { get; set; } diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 763ba25d52..997f45ce52 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -17,7 +17,7 @@ namespace osu.Game.Online.Rooms { [Cached] [JsonProperty("id")] - public readonly Bindable RoomID = new Bindable(); + public readonly Bindable RoomID = new Bindable(); [Cached] [JsonProperty("name")] diff --git a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs index 3f728a5417..ba3e3c6349 100644 --- a/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs +++ b/osu.Game/Online/Rooms/ShowPlaylistUserScoreRequest.cs @@ -7,11 +7,11 @@ namespace osu.Game.Online.Rooms { public class ShowPlaylistUserScoreRequest : APIRequest { - private readonly int roomId; - private readonly int playlistItemId; + private readonly long roomId; + private readonly long playlistItemId; private readonly long userId; - public ShowPlaylistUserScoreRequest(int roomId, int playlistItemId, long userId) + public ShowPlaylistUserScoreRequest(long roomId, long playlistItemId, long userId) { this.roomId = roomId; this.playlistItemId = playlistItemId; diff --git a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs index 5a78b9fabd..9e432fa99e 100644 --- a/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs +++ b/osu.Game/Online/Rooms/SubmitRoomScoreRequest.cs @@ -11,12 +11,12 @@ namespace osu.Game.Online.Rooms { public class SubmitRoomScoreRequest : APIRequest { - private readonly int scoreId; - private readonly int roomId; - private readonly int playlistItemId; + private readonly long scoreId; + private readonly long roomId; + private readonly long playlistItemId; private readonly ScoreInfo scoreInfo; - public SubmitRoomScoreRequest(int scoreId, int roomId, int playlistItemId, ScoreInfo scoreInfo) + public SubmitRoomScoreRequest(long scoreId, long roomId, long playlistItemId, ScoreInfo scoreInfo) { this.scoreId = scoreId; this.roomId = roomId; diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs index 2ed259e2b8..227a772b2d 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomManager.cs @@ -116,7 +116,7 @@ namespace osu.Game.Screens.OnlinePlay.Components joinedRoom.Value = null; } - private readonly HashSet ignoredRooms = new HashSet(); + private readonly HashSet ignoredRooms = new HashSet(); private void onRoomsReceived(List received) { diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs index 6da2866236..a96d64cb5d 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchChatDisplay.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components public class MatchChatDisplay : StandAloneChatDisplay { [Resolved(typeof(Room), nameof(Room.RoomID))] - private Bindable roomId { get; set; } + private Bindable roomId { get; set; } [Resolved(typeof(Room), nameof(Room.ChannelId))] private Bindable channelId { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs index 50869f42ff..134e083c42 100644 --- a/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs +++ b/osu.Game/Screens/OnlinePlay/Match/Components/MatchLeaderboard.cs @@ -15,7 +15,7 @@ namespace osu.Game.Screens.OnlinePlay.Match.Components public class MatchLeaderboard : Leaderboard { [Resolved(typeof(Room), nameof(Room.RoomID))] - private Bindable roomId { get; set; } + private Bindable roomId { get; set; } [BackgroundDependencyLoader] private void load() diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs index f0064ae0b4..3199232f6f 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/MultiplayerMatchSettingsOverlay.cs @@ -357,7 +357,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match public class CreateOrUpdateButton : TriangleButton { [Resolved(typeof(Room), nameof(Room.RoomID))] - private Bindable roomId { get; set; } + private Bindable roomId { get; set; } protected override void LoadComplete() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs index e3b47b3254..140b3c45d8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerResultsScreen.cs @@ -9,7 +9,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { public class MultiplayerResultsScreen : PlaylistsResultsScreen { - public MultiplayerResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem) + public MultiplayerResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem) : base(score, roomId, playlistItem, false, false) { } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index b2f3e4a1d9..239db18a07 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -13,7 +13,7 @@ namespace osu.Game.Screens.OnlinePlay public class OnlinePlayComposite : CompositeDrawable { [Resolved(typeof(Room))] - protected Bindable RoomID { get; private set; } + protected Bindable RoomID { get; private set; } [Resolved(typeof(Room), nameof(Room.Name))] protected Bindable RoomName { get; private set; } diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index 38eae2346a..ddc88261f7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public Action Exited; [Resolved(typeof(Room), nameof(Room.RoomID))] - protected Bindable RoomId { get; private set; } + protected Bindable RoomId { get; private set; } protected readonly PlaylistItem PlaylistItem; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs index e13c8a9f82..2b252f9db7 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsResultsScreen.cs @@ -19,7 +19,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists { public class PlaylistsResultsScreen : ResultsScreen { - private readonly int roomId; + private readonly long roomId; private readonly PlaylistItem playlistItem; protected LoadingSpinner LeftSpinner { get; private set; } @@ -32,7 +32,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists [Resolved] private IAPIProvider api { get; set; } - public PlaylistsResultsScreen(ScoreInfo score, int roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true) + public PlaylistsResultsScreen(ScoreInfo score, long roomId, PlaylistItem playlistItem, bool allowRetry, bool allowWatchingReplay = true) : base(score, allowRetry, allowWatchingReplay) { this.roomId = roomId; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs index 88731a10bc..9ccf4775d0 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsRoomSubScreen.cs @@ -25,7 +25,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists public override string ShortTitle => "playlist"; [Resolved(typeof(Room), nameof(Room.RoomID))] - private Bindable roomId { get; set; } + private Bindable roomId { get; set; } private MatchSettingsOverlay settingsOverlay; private MatchLeaderboard leaderboard; From ffa90c1a2373b7d1c774f5d355a43452c9d19667 Mon Sep 17 00:00:00 2001 From: Dan Balasescu Date: Tue, 16 Feb 2021 20:23:19 +0900 Subject: [PATCH 6636/6909] Remove whitespace --- osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs b/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs index c7d4ccd12e..1fcf7f2277 100644 --- a/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs +++ b/osu.Game/Screens/OnlinePlay/Components/RoomLocalUserInfo.cs @@ -56,7 +56,6 @@ namespace osu.Game.Screens.OnlinePlay.Components attemptDisplay.Text += $" ({remaining} remaining)"; } } - else { attemptDisplay.Text = string.Empty; From 100097d78f0b28c66a86d9d8a4b8bbe309964429 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Feb 2021 21:32:35 +0900 Subject: [PATCH 6637/6909] Fix playlist not being handled correctly for non-joined cases --- .../Multiplayer/StatefulMultiplayerClient.cs | 8 ++++---- .../Multiplayer/Match/BeatmapSelectionControl.cs | 14 +++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index f5f4c3a8ba..e9eb80e6a1 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -539,10 +539,7 @@ namespace osu.Game.Online.Multiplayer var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); // Update an existing playlist item from the API room, or create a new item. - var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId); - - if (playlistItem == null) - apiRoom.Playlist.Add(playlistItem = new PlaylistItem()); + var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId) ?? new PlaylistItem(); playlistItem.ID = settings.PlaylistItemId; playlistItem.Beatmap.Value = beatmap; @@ -552,6 +549,9 @@ namespace osu.Game.Online.Multiplayer playlistItem.AllowedMods.Clear(); playlistItem.AllowedMods.AddRange(allowedMods); + if (!apiRoom.Playlist.Contains(playlistItem)) + apiRoom.Playlist.Add(playlistItem); + CurrentMatchPlayingItem.Value = playlistItem; } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index 769596956b..8d394f2c2b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -2,8 +2,8 @@ // 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.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; @@ -62,7 +62,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match base.LoadComplete(); Debug.Assert(SelectedItem != null); - SelectedItem.BindValueChanged(onSelectedItemChanged, true); + SelectedItem.BindValueChanged(_ => updateBeatmap()); + Playlist.BindCollectionChanged((_, __) => updateBeatmap(), true); Host.BindValueChanged(host => { @@ -73,12 +74,15 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match }, true); } - private void onSelectedItemChanged(ValueChangedEvent selectedItem) + private void updateBeatmap() { - if (selectedItem.NewValue == null) + Debug.Assert(SelectedItem != null); + PlaylistItem item = SelectedItem.Value ?? Playlist.FirstOrDefault(); + + if (item == null) beatmapPanelContainer.Clear(); else - beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(selectedItem.NewValue, false, false); + beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(item, false, false); } } } From f61b8e6154c596a33f43dceed8f55b35ce479f58 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Feb 2021 21:32:38 +0900 Subject: [PATCH 6638/6909] Change to long --- osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 04752f4e6f..473382cf5f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -40,7 +40,7 @@ namespace osu.Game.Online.Multiplayer /// Only used for client-side mutation. /// [Key(6)] - public int PlaylistItemId { get; set; } + public long PlaylistItemId { get; set; } public bool Equals(MultiplayerRoomSettings other) => BeatmapID == other.BeatmapID From 8f72631c314f576fb5e9b8ff0473ca23906a4f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 16 Feb 2021 21:48:19 +0100 Subject: [PATCH 6639/6909] Fix typo in comment --- .../Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 2f50bee677..3f3fee1b79 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -290,7 +290,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return; // update local mods based on room's reported status for the local user (omitting the base call implementation). - // this makes the server authoritative, and avoids the local user potentially settings mods that the server is not aware of (ie. if the match was started during the selection being changed). + // this makes the server authoritative, and avoids the local user potentially setting mods that the server is not aware of (ie. if the match was started during the selection being changed). var ruleset = Ruleset.Value.CreateInstance(); Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); } From 3b4e02e5c78fd77325f188c0379785e47b61d6aa Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Feb 2021 07:29:45 +0300 Subject: [PATCH 6640/6909] Fix user population not immediate on bracket loading --- osu.Game.Tournament/TournamentGameBase.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 4224da4bbe..327d8f67b8 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -152,7 +152,7 @@ namespace osu.Game.Tournament { if (string.IsNullOrEmpty(p.Username) || p.Statistics == null) { - PopulateUser(p); + PopulateUser(p, immediate: true); addedInfo = true; } } @@ -211,7 +211,7 @@ namespace osu.Game.Tournament return addedInfo; } - public void PopulateUser(User user, Action success = null, Action failure = null) + public void PopulateUser(User user, Action success = null, Action failure = null, bool immediate = false) { var req = new GetUserRequest(user.Id, Ruleset.Value); @@ -231,7 +231,10 @@ namespace osu.Game.Tournament failure?.Invoke(); }; - API.Queue(req); + if (immediate) + API.Perform(req); + else + API.Queue(req); } protected override void LoadComplete() From 705e9267497bc1eec01da9d73ec2630ad2be327d Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Feb 2021 07:48:23 +0300 Subject: [PATCH 6641/6909] Fix attempting to populate users with invalid IDs --- osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 582f72429b..263bbc533c 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -277,7 +277,8 @@ namespace osu.Game.Tournament.Screens.Editors userId.Value = user.Id.ToString(); userId.BindValueChanged(idString => { - int.TryParse(idString.NewValue, out var parsed); + if (!(int.TryParse(idString.NewValue, out var parsed))) + return; user.Id = parsed; From 85ebc8e06cd45be8e66f40c059dac6b20c926cc8 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Feb 2021 07:49:28 +0300 Subject: [PATCH 6642/6909] Fix potentially overwriting user ID from failed request --- osu.Game.Tournament/TournamentGameBase.cs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 327d8f67b8..0b101f050f 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -225,11 +225,7 @@ namespace osu.Game.Tournament success?.Invoke(); }; - req.Failure += _ => - { - user.Id = 1; - failure?.Invoke(); - }; + req.Failure += _ => failure?.Invoke(); if (immediate) API.Perform(req); From 9a7b6ebe5058ea4afb2fc17ad6b62b838caff187 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Feb 2021 14:30:52 +0900 Subject: [PATCH 6643/6909] Fix missed occurrence --- osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs index bcc8721400..172fa3a583 100644 --- a/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs +++ b/osu.Game/Online/API/Requests/Responses/APIUserScoreAggregate.cs @@ -22,7 +22,7 @@ namespace osu.Game.Online.API.Requests.Responses public double? PP { get; set; } [JsonProperty(@"room_id")] - public int RoomID { get; set; } + public long RoomID { get; set; } [JsonProperty("total_score")] public long TotalScore { get; set; } From a845e96b7a8a6eb55615c38567e5a1716b334008 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Feb 2021 08:50:48 +0300 Subject: [PATCH 6644/6909] Replace `Ranks.Global` completely with a `GlobalRank` property --- osu.Desktop/DiscordRichPresence.cs | 2 +- .../TestSceneMultiplayerParticipantsList.cs | 10 ++-------- .../Visual/Online/TestSceneRankGraph.cs | 10 +++++----- .../Visual/Online/TestSceneUserProfileOverlay.cs | 3 ++- osu.Game.Tournament.Tests/TournamentTestScene.cs | 10 +++++----- osu.Game.Tournament/Models/TournamentTeam.cs | 2 +- .../Screens/TeamIntro/SeedingScreen.cs | 2 +- osu.Game.Tournament/TournamentGameBase.cs | 2 +- .../Profile/Header/CentreHeaderContainer.cs | 2 +- .../Profile/Header/DetailHeaderContainer.cs | 2 +- .../Multiplayer/Participants/ParticipantPanel.cs | 2 +- osu.Game/Users/UserStatistics.cs | 16 +++++----------- 12 files changed, 26 insertions(+), 37 deletions(-) diff --git a/osu.Desktop/DiscordRichPresence.cs b/osu.Desktop/DiscordRichPresence.cs index 63b12fb84b..832d26b0ef 100644 --- a/osu.Desktop/DiscordRichPresence.cs +++ b/osu.Desktop/DiscordRichPresence.cs @@ -105,7 +105,7 @@ namespace osu.Desktop if (privacyMode.Value == DiscordRichPresenceMode.Limited) presence.Assets.LargeImageText = string.Empty; else - presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.Ranks.Global > 0 ? $" (rank #{user.Value.Statistics.Ranks.Global:N0})" : string.Empty); + presence.Assets.LargeImageText = $"{user.Value.Username}" + (user.Value.Statistics?.GlobalRank > 0 ? $" (rank #{user.Value.Statistics.GlobalRank:N0})" : string.Empty); // update ruleset presence.Assets.SmallImageKey = ruleset.Value.ID <= 3 ? $"mode_{ruleset.Value.ID}" : "mode_custom"; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs index 1e14bbbbea..e713cff233 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerParticipantsList.cs @@ -160,10 +160,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { { Ruleset.Value.ShortName, - new UserStatistics - { - Ranks = new UserStatistics.UserRanks { Global = RNG.Next(1, 100000) } - } + new UserStatistics { GlobalRank = RNG.Next(1, 100000), } } }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", @@ -207,10 +204,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { { Ruleset.Value.ShortName, - new UserStatistics - { - Ranks = new UserStatistics.UserRanks { Global = RNG.Next(1, 100000) } - } + new UserStatistics { GlobalRank = RNG.Next(1, 100000), } } }, CoverUrl = @"https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", diff --git a/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs b/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs index 3b31192259..5bf9e31309 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneRankGraph.cs @@ -70,7 +70,7 @@ namespace osu.Game.Tests.Visual.Online { graph.Statistics.Value = new UserStatistics { - Ranks = new UserStatistics.UserRanks { Global = 123456 }, + GlobalRank = 123456, PP = 12345, }; }); @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Online { graph.Statistics.Value = new UserStatistics { - Ranks = new UserStatistics.UserRanks { Global = 89000 }, + GlobalRank = 89000, PP = 12345, RankHistory = new User.RankHistoryData { @@ -92,7 +92,7 @@ namespace osu.Game.Tests.Visual.Online { graph.Statistics.Value = new UserStatistics { - Ranks = new UserStatistics.UserRanks { Global = 89000 }, + GlobalRank = 89000, PP = 12345, RankHistory = new User.RankHistoryData { @@ -105,7 +105,7 @@ namespace osu.Game.Tests.Visual.Online { graph.Statistics.Value = new UserStatistics { - Ranks = new UserStatistics.UserRanks { Global = 12000 }, + GlobalRank = 12000, PP = 12345, RankHistory = new User.RankHistoryData { @@ -118,7 +118,7 @@ namespace osu.Game.Tests.Visual.Online { graph.Statistics.Value = new UserStatistics { - Ranks = new UserStatistics.UserRanks { Global = 12000 }, + GlobalRank = 12000, PP = 12345, RankHistory = new User.RankHistoryData { diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index 7ade24f4de..b52cc6edb6 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -33,7 +33,8 @@ namespace osu.Game.Tests.Visual.Online ProfileOrder = new[] { "me" }, Statistics = new UserStatistics { - Ranks = new UserStatistics.UserRanks { Global = 2148, Country = 1 }, + GlobalRank = 2148, + Ranks = new UserStatistics.UserRanks { Country = 1, }, PP = 4567.89m, Level = new UserStatistics.LevelInfo { diff --git a/osu.Game.Tournament.Tests/TournamentTestScene.cs b/osu.Game.Tournament.Tests/TournamentTestScene.cs index 47d2160561..cdfd19c157 100644 --- a/osu.Game.Tournament.Tests/TournamentTestScene.cs +++ b/osu.Game.Tournament.Tests/TournamentTestScene.cs @@ -113,11 +113,11 @@ namespace osu.Game.Tournament.Tests }, 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 } } }, + new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 12 } }, + new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 16 } }, + new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 20 } }, + new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 24 } }, + new User { Username = "Hello", Statistics = new UserStatistics { GlobalRank = 30 } }, } } }, diff --git a/osu.Game.Tournament/Models/TournamentTeam.cs b/osu.Game.Tournament/Models/TournamentTeam.cs index 7fca75cea4..7074ae413c 100644 --- a/osu.Game.Tournament/Models/TournamentTeam.cs +++ b/osu.Game.Tournament/Models/TournamentTeam.cs @@ -36,7 +36,7 @@ namespace osu.Game.Tournament.Models { get { - var ranks = Players.Select(p => p.Statistics?.Ranks.Global) + var ranks = Players.Select(p => p.Statistics?.GlobalRank) .Where(i => i.HasValue) .Select(i => i.Value) .ToArray(); diff --git a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs index 55fc80dba2..4f66d89b7f 100644 --- a/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs +++ b/osu.Game.Tournament/Screens/TeamIntro/SeedingScreen.cs @@ -250,7 +250,7 @@ namespace osu.Game.Tournament.Screens.TeamIntro }; foreach (var p in team.Players) - fill.Add(new RowDisplay(p.Username, p.Statistics?.Ranks.Global?.ToString("\\##,0") ?? "-")); + fill.Add(new RowDisplay(p.Username, p.Statistics?.GlobalRank?.ToString("\\##,0") ?? "-")); } internal class RowDisplay : CompositeDrawable diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 0b101f050f..3a2a880811 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -150,7 +150,7 @@ namespace osu.Game.Tournament { foreach (var p in t.Players) { - if (string.IsNullOrEmpty(p.Username) || p.Statistics == null) + if (string.IsNullOrEmpty(p.Username) || p.Statistics?.GlobalRank == null) { PopulateUser(p, immediate: true); addedInfo = true; diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs index 04a1040e06..9285e2d875 100644 --- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs @@ -144,7 +144,7 @@ namespace osu.Game.Overlays.Profile.Header private void updateDisplay(User user) { - hiddenDetailGlobal.Content = user?.Statistics?.Ranks.Global?.ToString("\\##,##0") ?? "-"; + hiddenDetailGlobal.Content = user?.Statistics?.GlobalRank?.ToString("\\##,##0") ?? "-"; hiddenDetailCountry.Content = user?.Statistics?.Ranks.Country?.ToString("\\##,##0") ?? "-"; } } diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs index cf6ae1a3fc..05a0508e1f 100644 --- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs @@ -176,7 +176,7 @@ namespace osu.Game.Overlays.Profile.Header foreach (var scoreRankInfo in scoreRankInfos) scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; - detailGlobalRank.Content = user?.Statistics?.Ranks.Global?.ToString("\\##,##0") ?? "-"; + detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToString("\\##,##0") ?? "-"; detailCountryRank.Content = user?.Statistics?.Ranks.Country?.ToString("\\##,##0") ?? "-"; rankGraph.Statistics.Value = user?.Statistics; diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index c4d11676e7..25bc314f1b 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -165,7 +165,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID); - var currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.Ranks.Global; + var currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; userStateDisplay.UpdateStatus(User.State, User.BeatmapAvailability); diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 1fed908c39..e50ca57d90 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -26,17 +26,14 @@ namespace osu.Game.Users public int Progress; } + [JsonProperty(@"global_rank")] + public int? GlobalRank; + + // eventually UserRanks object will be completely replaced with separate global rank (exists) and country rank properties + // see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53. [JsonProperty(@"rank")] public UserRanks Ranks; - // eventually UserRanks object will be completely replaced with separate global and country rank properties, see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53. - // but for now, always point UserRanks.Global to the global_rank property, as that is included solely for requests like GetUsersRequest. - [JsonProperty(@"global_rank")] - private int? globalRank - { - set => Ranks.Global = value; - } - [JsonProperty(@"pp")] public decimal? PP; @@ -120,9 +117,6 @@ namespace osu.Game.Users public struct UserRanks { - [JsonProperty(@"global")] - public int? Global; - [JsonProperty(@"country")] public int? Country; } From fb0e9d6760c2877ae97b40ad62d601b9e6774369 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Feb 2021 16:44:39 +0900 Subject: [PATCH 6645/6909] Add played property to playlist item --- osu.Game/Online/Rooms/PlaylistItem.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/Rooms/PlaylistItem.cs b/osu.Game/Online/Rooms/PlaylistItem.cs index 61982101c1..1d409d4b56 100644 --- a/osu.Game/Online/Rooms/PlaylistItem.cs +++ b/osu.Game/Online/Rooms/PlaylistItem.cs @@ -23,6 +23,12 @@ namespace osu.Game.Online.Rooms [JsonProperty("ruleset_id")] public int RulesetID { get; set; } + /// + /// Whether this is still a valid selection for the . + /// + [JsonProperty("expired")] + public bool Expired { get; set; } + [JsonIgnore] public readonly Bindable Beatmap = new Bindable(); From 61bf9a64bb117483093318b99ce135cd50a2e8c0 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Wed, 17 Feb 2021 11:21:33 +0300 Subject: [PATCH 6646/6909] Revert failed user requests changes with returning user ID instead --- osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs | 3 +-- osu.Game.Tournament/TournamentGameBase.cs | 8 +++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs index 263bbc533c..582f72429b 100644 --- a/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs +++ b/osu.Game.Tournament/Screens/Editors/TeamEditorScreen.cs @@ -277,8 +277,7 @@ namespace osu.Game.Tournament.Screens.Editors userId.Value = user.Id.ToString(); userId.BindValueChanged(idString => { - if (!(int.TryParse(idString.NewValue, out var parsed))) - return; + int.TryParse(idString.NewValue, out var parsed); user.Id = parsed; diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index 0b101f050f..ffda101ee0 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -217,6 +217,8 @@ namespace osu.Game.Tournament req.Success += res => { + user.Id = res.Id; + user.Username = res.Username; user.Statistics = res.Statistics; user.Country = res.Country; @@ -225,7 +227,11 @@ namespace osu.Game.Tournament success?.Invoke(); }; - req.Failure += _ => failure?.Invoke(); + req.Failure += _ => + { + user.Id = 1; + failure?.Invoke(); + }; if (immediate) API.Perform(req); From 0d1149911c44f16b1017c96ed79638fbcfe00a5c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Feb 2021 17:33:10 +0900 Subject: [PATCH 6647/6909] Don't display expired playlist items --- osu.Game/Online/Rooms/Room.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index 997f45ce52..aaaa712860 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -148,6 +148,12 @@ namespace osu.Game.Online.Rooms if (EndDate.Value != null && DateTimeOffset.Now >= EndDate.Value) Status.Value = new RoomStatusEnded(); + // Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended, + // and display only the non-expired playlist items while the room is still active. + // In order to achieve this, all expired items are removed from the source Room. + if (!(Status.Value is RoomStatusEnded)) + other.Playlist.RemoveAll(i => i.Expired); + if (!Playlist.SequenceEqual(other.Playlist)) { Playlist.Clear(); From 70a995919cde7ae34501afac229872cf7317e693 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Feb 2021 17:58:24 +0900 Subject: [PATCH 6648/6909] Update comments --- osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs | 3 --- osu.Game/Online/Rooms/Room.cs | 4 ++-- .../OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs | 2 ++ 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs index 473382cf5f..7d6c76bc2f 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerRoomSettings.cs @@ -36,9 +36,6 @@ namespace osu.Game.Online.Multiplayer [Key(5)] public IEnumerable AllowedMods { get; set; } = Enumerable.Empty(); - /// - /// Only used for client-side mutation. - /// [Key(6)] public long PlaylistItemId { get; set; } diff --git a/osu.Game/Online/Rooms/Room.cs b/osu.Game/Online/Rooms/Room.cs index aaaa712860..00a7979f2c 100644 --- a/osu.Game/Online/Rooms/Room.cs +++ b/osu.Game/Online/Rooms/Room.cs @@ -149,8 +149,8 @@ namespace osu.Game.Online.Rooms Status.Value = new RoomStatusEnded(); // Todo: This is not the best way/place to do this, but the intention is to display all playlist items when the room has ended, - // and display only the non-expired playlist items while the room is still active. - // In order to achieve this, all expired items are removed from the source Room. + // and display only the non-expired playlist items while the room is still active. In order to achieve this, all expired items are removed from the source Room. + // More refactoring is required before this can be done locally instead - DrawableRoomPlaylist is currently directly bound to the playlist to display items in the room. if (!(Status.Value is RoomStatusEnded)) other.Playlist.RemoveAll(i => i.Expired); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index 8d394f2c2b..3cf0767cf8 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -77,6 +77,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void updateBeatmap() { Debug.Assert(SelectedItem != null); + + // When the selected item is null, the match hasn't yet been created. Use the playlist directly, which is mutated by song selection. PlaylistItem item = SelectedItem.Value ?? Playlist.FirstOrDefault(); if (item == null) From 604add04e495cbed44991962f3fa4e1bcb8b1451 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Feb 2021 19:06:37 +0900 Subject: [PATCH 6649/6909] Fix song select mods being reset incorrectly --- osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index b201c62b7f..c60743a226 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Linq; using Humanizer; +using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -31,6 +32,10 @@ namespace osu.Game.Screens.OnlinePlay [Resolved(typeof(Room), nameof(Room.Playlist))] protected BindableList Playlist { get; private set; } + [CanBeNull] + [Resolved(CanBeNull = true)] + private IBindable selectedItem { get; set; } + private readonly Bindable> freeMods = new Bindable>(Array.Empty()); private readonly FreeModSelectOverlay freeModSelectOverlay; @@ -66,8 +71,8 @@ namespace osu.Game.Screens.OnlinePlay // At this point, Mods contains both the required and allowed mods. For selection purposes, it should only contain the required mods. // Similarly, freeMods is currently empty but should only contain the allowed mods. - Mods.Value = Playlist.FirstOrDefault()?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); - freeMods.Value = Playlist.FirstOrDefault()?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + freeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); Ruleset.BindValueChanged(onRulesetChanged); } From c1620ce21b6e3194a200e980bb79891c0406be24 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Feb 2021 19:19:49 +0900 Subject: [PATCH 6650/6909] Fix intro beatmap always being imported even if already in a good state --- osu.Game/Screens/Menu/IntroScreen.cs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Menu/IntroScreen.cs b/osu.Game/Screens/Menu/IntroScreen.cs index b8b962be6c..71b83d4aab 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -111,12 +111,10 @@ namespace osu.Game.Screens.Menu { setInfo = beatmaps.QueryBeatmapSet(b => b.Hash == BeatmapHash); - if (setInfo != null) - { - initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0]); - } + if (setInfo == null) + return false; - return UsingThemedIntro; + return (initialBeatmap = beatmaps.GetWorkingBeatmap(setInfo.Beatmaps[0])) != null; } } From c1db33e0753e665334abd833330e13e5c3ee5c74 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Feb 2021 17:04:43 +0900 Subject: [PATCH 6651/6909] Improve some xmldoc on ArchiveModelManager for methods which are not going to trigger user interactive flow --- 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 232acba4a3..cc107d61c6 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -226,7 +226,7 @@ namespace osu.Game.Database public Action> PresentImport; /// - /// Import an item from an . + /// Silently import an item from an . /// /// The archive to be imported. /// An optional cancellation token. @@ -303,7 +303,7 @@ namespace osu.Game.Database } /// - /// Import an item from a . + /// Silently import an item from a . /// /// The model to be imported. /// An optional archive to use for model population. From 0196ee882a5c6a956827e84fc79fb824de51a07c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Feb 2021 19:09:38 +0900 Subject: [PATCH 6652/6909] Redirect batch imports to a separate task scheduler to avoid contention with interactive actions --- ...eneOnlinePlayBeatmapAvailabilityTracker.cs | 4 +-- osu.Game/Database/ArchiveModelManager.cs | 36 ++++++++++++++----- 2 files changed, 30 insertions(+), 10 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index d3475de157..3ffb512b7f 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -165,10 +165,10 @@ namespace osu.Game.Tests.Online { } - public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, CancellationToken cancellationToken = default) + public override async Task Import(BeatmapSetInfo item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) { await AllowImport.Task; - return await (CurrentImportTask = base.Import(item, archive, cancellationToken)); + return await (CurrentImportTask = base.Import(item, archive, lowPriority, cancellationToken)); } } diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index cc107d61c6..03b8db2cb8 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -38,6 +38,11 @@ namespace osu.Game.Database { private const int import_queue_request_concurrency = 1; + /// + /// The size of a batch import operation before considering it a lower priority operation. + /// + private const int low_priority_import_batch_size = 1; + /// /// A singleton scheduler shared by all . /// @@ -47,6 +52,13 @@ namespace osu.Game.Database /// private static readonly ThreadedTaskScheduler import_scheduler = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager)); + /// + /// A second scheduler for lower priority imports. + /// For simplicity, these will just run in parallel with normal priority imports, but a future refactor would see this implemented via a custom scheduler/queue. + /// See https://gist.github.com/peppy/f0e118a14751fc832ca30dd48ba3876b for an incomplete version of this. + /// + private static readonly ThreadedTaskScheduler import_scheduler_low_priority = new ThreadedTaskScheduler(import_queue_request_concurrency, nameof(ArchiveModelManager)); + /// /// Set an endpoint for notifications to be posted to. /// @@ -103,8 +115,11 @@ namespace osu.Game.Database /// /// Import one or more items from filesystem . - /// This will post notifications tracking progress. /// + /// + /// This will be treated as a low priority import if more than one path is specified; use to always import at standard priority. + /// This will post notifications tracking progress. + /// /// One or more archive locations on disk. public Task Import(params string[] paths) { @@ -133,13 +148,15 @@ namespace osu.Game.Database var imported = new List(); + bool isLowPriorityImport = tasks.Length > low_priority_import_batch_size; + await Task.WhenAll(tasks.Select(async task => { notification.CancellationToken.ThrowIfCancellationRequested(); try { - var model = await Import(task, notification.CancellationToken); + var model = await Import(task, isLowPriorityImport, notification.CancellationToken); lock (imported) { @@ -193,15 +210,16 @@ namespace osu.Game.Database /// Note that this bypasses the UI flow and should only be used for special cases or testing. /// /// The containing data about the to import. + /// Whether this is a low priority import. /// An optional cancellation token. /// The imported model, if successful. - internal async Task Import(ImportTask task, CancellationToken cancellationToken = default) + internal async Task Import(ImportTask task, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); TModel import; using (ArchiveReader reader = task.GetReader()) - import = await Import(reader, cancellationToken); + import = await Import(reader, lowPriority, cancellationToken); // We may or may not want to delete the file depending on where it is stored. // e.g. reconstructing/repairing database with items from default storage. @@ -229,8 +247,9 @@ namespace osu.Game.Database /// Silently import an item from an . /// /// The archive to be imported. + /// Whether this is a low priority import. /// An optional cancellation token. - public Task Import(ArchiveReader archive, CancellationToken cancellationToken = default) + public Task Import(ArchiveReader archive, bool lowPriority = false, CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); @@ -253,7 +272,7 @@ namespace osu.Game.Database return null; } - return Import(model, archive, cancellationToken); + return Import(model, archive, lowPriority, cancellationToken); } /// @@ -307,8 +326,9 @@ namespace osu.Game.Database /// /// The model to be imported. /// An optional archive to use for model population. + /// Whether this is a low priority import. /// An optional cancellation token. - public virtual async Task Import(TModel item, ArchiveReader archive = null, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => + public virtual async Task Import(TModel item, ArchiveReader archive = null, bool lowPriority = false, CancellationToken cancellationToken = default) => await Task.Factory.StartNew(async () => { cancellationToken.ThrowIfCancellationRequested(); @@ -383,7 +403,7 @@ namespace osu.Game.Database flushEvents(true); return item; - }, cancellationToken, TaskCreationOptions.HideScheduler, import_scheduler).Unwrap(); + }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap(); /// /// Exports an item to a legacy (.zip based) package. From 172e2e9b3ffabfcc7fb7017529f9eecd103e2d62 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Feb 2021 20:40:15 +0900 Subject: [PATCH 6653/6909] Fix audio previews not being adjusted in volume correctly --- osu.Game/Audio/PreviewTrackManager.cs | 3 +++ osu.Game/OsuGameBase.cs | 7 ++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Audio/PreviewTrackManager.cs b/osu.Game/Audio/PreviewTrackManager.cs index 8d02af6574..d88fd1e62b 100644 --- a/osu.Game/Audio/PreviewTrackManager.cs +++ b/osu.Game/Audio/PreviewTrackManager.cs @@ -27,6 +27,8 @@ namespace osu.Game.Audio protected TrackManagerPreviewTrack CurrentTrack; + private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(OsuGameBase.GLOBAL_TRACK_VOLUME_ADJUST); + [BackgroundDependencyLoader] private void load() { @@ -35,6 +37,7 @@ namespace osu.Game.Audio trackStore = new PreviewTrackStore(new OnlineStore()); audio.AddItem(trackStore); + trackStore.AddAdjustment(AdjustableProperty.Volume, globalTrackVolumeAdjust); trackStore.AddAdjustment(AdjustableProperty.Volume, audio.VolumeTrack); } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 00b436931a..3d24f245f9 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -156,7 +156,12 @@ namespace osu.Game protected override UserInputManager CreateUserInputManager() => new OsuUserInputManager(); - private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(0.5f); + /// + /// The maximum volume at which audio tracks should playback. This can be set lower than 1 to create some head-room for sound effects. + /// + internal const double GLOBAL_TRACK_VOLUME_ADJUST = 0.5; + + private readonly BindableNumber globalTrackVolumeAdjust = new BindableNumber(GLOBAL_TRACK_VOLUME_ADJUST); [BackgroundDependencyLoader] private void load() From 403536ef80c64d475f56bb6fc290d849b49a491c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Feb 2021 21:09:20 +0900 Subject: [PATCH 6654/6909] Fix ModDisplay potentially being operated on before loaded completely Closes https://github.com/ppy/osu/issues/11810. --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 68d019bf71..7359f04dcf 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -72,19 +72,6 @@ namespace osu.Game.Screens.Play.HUD } }, }; - - Current.ValueChanged += mods => - { - iconsContainer.Clear(); - - foreach (Mod mod in mods.NewValue) - { - iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); - } - - if (IsLoaded) - appearTransform(); - }; } protected override void Dispose(bool isDisposing) @@ -97,7 +84,16 @@ namespace osu.Game.Screens.Play.HUD { base.LoadComplete(); - appearTransform(); + Current.BindValueChanged(mods => + { + iconsContainer.Clear(); + + foreach (Mod mod in mods.NewValue) + iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); + + appearTransform(); + }, true); + iconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint); } From 2a1bb2f578ac8b74e6a7d5e7a949a7e995acc6c1 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Feb 2021 21:38:01 +0900 Subject: [PATCH 6655/6909] Fix selected item potentially changing during gameplay --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 51 ++++++++++++------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index c1930c525c..b5eff04532 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -46,7 +46,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private MultiplayerMatchSettingsOverlay settingsOverlay; private Drawable userModsSection; - private IBindable isConnected; + private readonly IBindable isConnected = new Bindable(); + private readonly IBindable matchCurrentItem = new Bindable(); [CanBeNull] private IDisposable readyClickOperation; @@ -268,8 +269,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - SelectedItem.BindValueChanged(onSelectedItemChanged); - SelectedItem.BindTo(client.CurrentMatchPlayingItem); + matchCurrentItem.BindTo(client.CurrentMatchPlayingItem); + matchCurrentItem.BindValueChanged(onCurrentItemChanged, true); BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); UserMods.BindValueChanged(onUserModsChanged); @@ -277,7 +278,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer client.LoadRequested += onLoadRequested; client.RoomUpdated += onRoomUpdated; - isConnected = client.IsConnected.GetBoundCopy(); + isConnected.BindTo(client.IsConnected); isConnected.BindValueChanged(connected => { if (!connected.NewValue) @@ -285,6 +286,33 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, true); } + private void onCurrentItemChanged(ValueChangedEvent item) + { + if (client?.LocalUser == null) + return; + + // If we're about to enter gameplay, schedule the item to be set at a later time. + if (client.LocalUser.State > MultiplayerUserState.Ready) + { + Schedule(() => onCurrentItemChanged(item)); + return; + } + + SelectedItem.Value = item.NewValue; + + if (item.NewValue?.AllowedMods.Any() != true) + { + userModsSection.Hide(); + userModsSelectOverlay.Hide(); + userModsSelectOverlay.IsValidMod = _ => false; + } + else + { + userModsSection.Show(); + userModsSelectOverlay.IsValidMod = m => item.NewValue.AllowedMods.Any(a => a.GetType() == m.GetType()); + } + } + protected override void UpdateMods() { if (SelectedItem.Value == null || client.LocalUser == null) @@ -313,21 +341,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return base.OnBackButton(); } - private void onSelectedItemChanged(ValueChangedEvent item) - { - if (item.NewValue?.AllowedMods.Any() != true) - { - userModsSection.Hide(); - userModsSelectOverlay.Hide(); - userModsSelectOverlay.IsValidMod = _ => false; - } - else - { - userModsSection.Show(); - userModsSelectOverlay.IsValidMod = m => item.NewValue.AllowedMods.Any(a => a.GetType() == m.GetType()); - } - } - private ModSettingChangeTracker modSettingChangeTracker; private ScheduledDelegate debouncedModSettingsUpdate; From 6ef235c4c5290f3254ed247b2f20054dde1aceaf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 17 Feb 2021 21:42:22 +0900 Subject: [PATCH 6656/6909] Fix beatmap panel flickering multiple times --- .../Match/BeatmapSelectionControl.cs | 17 ++------ .../Screens/OnlinePlay/OnlinePlayComposite.cs | 41 +++++++++++++++++-- 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index 3cf0767cf8..3af0d5b715 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.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.Diagnostics; -using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; using osu.Framework.Screens; using osu.Game.Online.API; -using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay.Match.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match @@ -61,10 +58,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { base.LoadComplete(); - Debug.Assert(SelectedItem != null); - SelectedItem.BindValueChanged(_ => updateBeatmap()); - Playlist.BindCollectionChanged((_, __) => updateBeatmap(), true); - + SelectedItem.BindValueChanged(_ => updateBeatmap(), true); Host.BindValueChanged(host => { if (RoomID.Value == null || host.NewValue?.Equals(api.LocalUser.Value) == true) @@ -76,15 +70,10 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match private void updateBeatmap() { - Debug.Assert(SelectedItem != null); - - // When the selected item is null, the match hasn't yet been created. Use the playlist directly, which is mutated by song selection. - PlaylistItem item = SelectedItem.Value ?? Playlist.FirstOrDefault(); - - if (item == null) + if (SelectedItem.Value == null) beatmapPanelContainer.Clear(); else - beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(item, false, false); + beatmapPanelContainer.Child = new DrawableRoomPlaylistItem(SelectedItem.Value, false, false); } } } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index a7058d0ede..f203ef927c 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -2,6 +2,8 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Collections.Specialized; +using System.Linq; using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; @@ -57,11 +59,44 @@ namespace osu.Game.Screens.OnlinePlay protected Bindable Duration { get; private set; } /// - /// The currently selected item in the . - /// May be null if this is not inside a . + /// The currently selected item in the , or the first item from + /// if this is not within a . /// + protected IBindable SelectedItem => selectedItem; + + private readonly Bindable selectedItem = new Bindable(); + [CanBeNull] [Resolved(CanBeNull = true)] - protected IBindable SelectedItem { get; private set; } + private IBindable subScreenSelectedItem { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + if (subScreenSelectedItem != null) + subScreenSelectedItem.BindValueChanged(onSelectedItemChanged, true); + else + Playlist.BindCollectionChanged(onPlaylistChanged, true); + } + + /// + /// Invoked when the selected item from within a changes. + /// Does not occur when this is outside a . + /// + private void onSelectedItemChanged(ValueChangedEvent item) + { + // If the room hasn't been created yet, fall-back to the first item from the playlist. + selectedItem.Value = RoomID.Value == null ? Playlist.FirstOrDefault() : item.NewValue; + } + + /// + /// Invoked when the playlist changes. + /// Does not occur when this is inside a . + /// + private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) + { + selectedItem.Value = Playlist.FirstOrDefault(); + } } } From 3208b2c5bf096bb25eab7946fb8475a1113b81e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Feb 2021 23:13:51 +0900 Subject: [PATCH 6657/6909] Fix potential nullref if mods are never set --- osu.Game/Screens/Play/HUD/ModDisplay.cs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/ModDisplay.cs b/osu.Game/Screens/Play/HUD/ModDisplay.cs index 7359f04dcf..cffdb21fb8 100644 --- a/osu.Game/Screens/Play/HUD/ModDisplay.cs +++ b/osu.Game/Screens/Play/HUD/ModDisplay.cs @@ -88,10 +88,13 @@ namespace osu.Game.Screens.Play.HUD { iconsContainer.Clear(); - foreach (Mod mod in mods.NewValue) - iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); + if (mods.NewValue != null) + { + foreach (Mod mod in mods.NewValue) + iconsContainer.Add(new ModIcon(mod) { Scale = new Vector2(0.6f) }); - appearTransform(); + appearTransform(); + } }, true); iconsContainer.FadeInFromZero(fade_duration, Easing.OutQuint); From e7308193e7bd33a8c6e38f90d7dca112bda6e084 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 13:03:29 +0900 Subject: [PATCH 6658/6909] Add xmldoc explaining how PreviewTime is intended to work --- osu.Game/Beatmaps/BeatmapMetadata.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/osu.Game/Beatmaps/BeatmapMetadata.cs b/osu.Game/Beatmaps/BeatmapMetadata.cs index 39b3c23ddd..367f612dc8 100644 --- a/osu.Game/Beatmaps/BeatmapMetadata.cs +++ b/osu.Game/Beatmaps/BeatmapMetadata.cs @@ -51,7 +51,12 @@ namespace osu.Game.Beatmaps [JsonProperty(@"tags")] public string Tags { get; set; } + /// + /// The time in milliseconds to begin playing the track for preview purposes. + /// If -1, the track should begin playing at 40% of its length. + /// public int PreviewTime { get; set; } + public string AudioFile { get; set; } public string BackgroundFile { get; set; } From 90dce5204218ac9132e929d374a123f3da7abe34 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 14:10:28 +0900 Subject: [PATCH 6659/6909] Fix potential crash from cross-thread drawable manipulation in CollectionFilterDropdown --- osu.Game/Collections/CollectionFilterDropdown.cs | 13 ++++++------- osu.Game/Screens/Select/FilterControl.cs | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index ec0e9d5a89..3e55ecb084 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -41,19 +41,18 @@ namespace osu.Game.Collections ItemSource = filters; } - [BackgroundDependencyLoader(permitNulls: true)] - private void load([CanBeNull] CollectionManager collectionManager) + [Resolved(CanBeNull = true)] + private CollectionManager collectionManager { get; set; } + + protected override void LoadComplete() { + base.LoadComplete(); + if (collectionManager != null) collections.BindTo(collectionManager.Collections); collections.CollectionChanged += (_, __) => collectionsChanged(); collectionsChanged(); - } - - protected override void LoadComplete() - { - base.LoadComplete(); Current.BindValueChanged(filterChanged, true); } diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 952a5d1eaa..eafd8a87d1 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -44,7 +44,7 @@ namespace osu.Game.Screens.Select Sort = sortMode.Value, AllowConvertedBeatmaps = showConverted.Value, Ruleset = ruleset.Value, - Collection = collectionDropdown?.Current.Value.Collection + Collection = collectionDropdown?.Current.Value?.Collection }; if (!minimumStars.IsDefault) From 49589b64c34792ac07d19f8f3d4efb97fd4c7aad Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 14:55:15 +0900 Subject: [PATCH 6660/6909] Intro track should not restart from preview point --- 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 71b83d4aab..71f3b60026 100644 --- a/osu.Game/Screens/Menu/IntroScreen.cs +++ b/osu.Game/Screens/Menu/IntroScreen.cs @@ -168,7 +168,7 @@ 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(); + Track.Start(); } protected override void LogoArriving(OsuLogo logo, bool resuming) From dfedea9ea2abf6c09dead571889915a37570c0ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 14:55:44 +0900 Subject: [PATCH 6661/6909] Move preview point logic to a specific method in WorkingBeatmap --- osu.Game/Beatmaps/WorkingBeatmap.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index 30382c444f..aab8ff6bd6 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -266,6 +266,26 @@ namespace osu.Game.Beatmaps [NotNull] public Track LoadTrack() => loadedTrack = GetBeatmapTrack() ?? GetVirtualTrack(1000); + /// + /// Reads the correct track restart point from beatmap metadata and sets looping to enabled. + /// + public void PrepareTrackForPreviewLooping() + { + Track.Looping = true; + Track.RestartPoint = Metadata.PreviewTime; + + if (Track.RestartPoint == -1) + { + if (!Track.IsLoaded) + { + // force length to be populated (https://github.com/ppy/osu-framework/issues/4202) + Track.Seek(Track.CurrentTime); + } + + Track.RestartPoint = 0.4f * Track.Length; + } + } + /// /// Transfer a valid audio track into this working beatmap. Used as an optimisation to avoid reload / track swap /// across difficulties in the same beatmap set. From 421cdb6650246d249d685812c8ea7dbd3c07a015 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 15:01:11 +0900 Subject: [PATCH 6662/6909] Consume new method in existing usages (and remove some unnecessary set/unset code) --- osu.Game/Screens/Menu/MainMenu.cs | 11 ++++++----- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 7 +------ osu.Game/Screens/Select/SongSelect.cs | 7 +++---- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 97fd58318b..424e6d2cd5 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -179,14 +179,15 @@ namespace osu.Game.Screens.Menu base.OnEntering(last); buttons.FadeInFromZero(500); - var metadata = Beatmap.Value.Metadata; - if (last is IntroScreen && musicController.TrackLoaded) { - if (!musicController.CurrentTrack.IsRunning) + var track = musicController.CurrentTrack; + + // presume the track is the current beatmap's track. not sure how correct this assumption is but it has worked until now. + if (!track.IsRunning) { - musicController.CurrentTrack.Seek(metadata.PreviewTime != -1 ? metadata.PreviewTime : 0.4f * musicController.CurrentTrack.Length); - musicController.CurrentTrack.Start(); + Beatmap.Value.PrepareTrackForPreviewLooping(); + track.Restart(); } } diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index e755f8c405..86422085a1 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -173,9 +173,7 @@ namespace osu.Game.Screens.OnlinePlay.Match if (track != null) { - track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - track.Looping = true; - + Beatmap.Value.PrepareTrackForPreviewLooping(); music?.EnsurePlayingSomething(); } } @@ -185,10 +183,7 @@ namespace osu.Game.Screens.OnlinePlay.Match var track = Beatmap?.Value?.Track; if (track != null) - { track.Looping = false; - track.RestartPoint = 0; - } } } } diff --git a/osu.Game/Screens/Select/SongSelect.cs b/osu.Game/Screens/Select/SongSelect.cs index edbab083cd..b7f7c40539 100644 --- a/osu.Game/Screens/Select/SongSelect.cs +++ b/osu.Game/Screens/Select/SongSelect.cs @@ -648,8 +648,9 @@ namespace osu.Game.Screens.Select { Debug.Assert(!isHandlingLooping); - music.CurrentTrack.Looping = isHandlingLooping = true; + isHandlingLooping = true; + ensureTrackLooping(Beatmap.Value, TrackChangeDirection.None); music.TrackChanged += ensureTrackLooping; } @@ -665,7 +666,7 @@ namespace osu.Game.Screens.Select } private void ensureTrackLooping(WorkingBeatmap beatmap, TrackChangeDirection changeDirection) - => music.CurrentTrack.Looping = true; + => beatmap.PrepareTrackForPreviewLooping(); public override bool OnBackButton() { @@ -719,8 +720,6 @@ namespace osu.Game.Screens.Select bool isNewTrack = !lastTrack.TryGetTarget(out var last) || last != track; - track.RestartPoint = Beatmap.Value.Metadata.PreviewTime; - if (!track.IsRunning && (music.UserPauseRequested != true || isNewTrack)) music.Play(true); From 56e9e10ff584742f80c21971771874c7f21ab745 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Feb 2021 15:30:31 +0900 Subject: [PATCH 6663/6909] Make server authoritative in playlist item id --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index e9eb80e6a1..416162779d 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -217,7 +217,6 @@ namespace osu.Game.Online.Multiplayer RulesetID = item.GetOr(existingPlaylistItem).RulesetID, RequiredMods = item.HasValue ? item.Value.AsNonNull().RequiredMods.Select(m => new APIMod(m)).ToList() : Room.Settings.RequiredMods, AllowedMods = item.HasValue ? item.Value.AsNonNull().AllowedMods.Select(m => new APIMod(m)).ToList() : Room.Settings.AllowedMods, - PlaylistItemId = Room.Settings.PlaylistItemId, }); } From 143e1456701ad4808c55cbb75788e81dc3df9c9a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 15:42:26 +0900 Subject: [PATCH 6664/6909] Update implementation of AdjustableAudioComponents --- .../Rulesets/TestSceneDrawableRulesetDependencies.cs | 4 ++-- osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs | 4 ++-- osu.Game/Skinning/PoolableSkinnableSample.cs | 4 ++-- osu.Game/Skinning/SkinnableSound.cs | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs index 787f72ba79..4aebed0d31 100644 --- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs +++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs @@ -118,9 +118,9 @@ namespace osu.Game.Tests.Rulesets public BindableNumber Frequency => throw new NotImplementedException(); public BindableNumber Tempo => throw new NotImplementedException(); - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException(); + public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => throw new NotImplementedException(); - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotImplementedException(); + public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) => throw new NotImplementedException(); public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotImplementedException(); diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index deec948d14..bbaca7c80f 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -110,9 +110,9 @@ namespace osu.Game.Rulesets.UI public IEnumerable GetAvailableResources() => throw new NotSupportedException(); - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); + public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => throw new NotSupportedException(); - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => throw new NotSupportedException(); + public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) => throw new NotSupportedException(); public void RemoveAllAdjustments(AdjustableProperty type) => throw new NotSupportedException(); diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 9025fdbd0f..abff57091b 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -165,9 +165,9 @@ namespace osu.Game.Skinning public BindableNumber Tempo => sampleContainer.Tempo; - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); + public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable); + public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable); public void RemoveAllAdjustments(AdjustableProperty type) => sampleContainer.RemoveAllAdjustments(type); diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index b3db2d6558..d3dfcb1dc0 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -176,10 +176,10 @@ namespace osu.Game.Skinning public BindableNumber Tempo => samplesContainer.Tempo; - public void AddAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => samplesContainer.AddAdjustment(type, adjustBindable); - public void RemoveAdjustment(AdjustableProperty type, BindableNumber adjustBindable) + public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) => samplesContainer.RemoveAdjustment(type, adjustBindable); public void RemoveAllAdjustments(AdjustableProperty type) From e911760318edfb87b86bd6f5b74da4e285c441f5 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Feb 2021 15:47:33 +0900 Subject: [PATCH 6665/6909] Split OnlinePlayComposite to remove if-statement --- .../Match/BeatmapSelectionControl.cs | 2 +- .../Screens/OnlinePlay/OnlinePlayComposite.cs | 36 ++++-------------- .../OnlinePlay/RoomSubScreenComposite.cs | 38 +++++++++++++++++++ 3 files changed, 46 insertions(+), 30 deletions(-) create mode 100644 osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs index 3af0d5b715..ebe63e26d6 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Match/BeatmapSelectionControl.cs @@ -11,7 +11,7 @@ using osu.Game.Screens.OnlinePlay.Match.Components; namespace osu.Game.Screens.OnlinePlay.Multiplayer.Match { - public class BeatmapSelectionControl : OnlinePlayComposite + public class BeatmapSelectionControl : RoomSubScreenComposite { [Resolved] private MultiplayerMatchSubScreen matchSubScreen { get; set; } diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs index f203ef927c..eb0b23f13f 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayComposite.cs @@ -2,9 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; -using System.Collections.Specialized; using System.Linq; -using JetBrains.Annotations; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics.Containers; @@ -14,6 +12,9 @@ using osu.Game.Users; namespace osu.Game.Screens.OnlinePlay { + /// + /// A that exposes bindables for properties. + /// public class OnlinePlayComposite : CompositeDrawable { [Resolved(typeof(Room))] @@ -62,41 +63,18 @@ namespace osu.Game.Screens.OnlinePlay /// The currently selected item in the , or the first item from /// if this is not within a . /// - protected IBindable SelectedItem => selectedItem; - - private readonly Bindable selectedItem = new Bindable(); - - [CanBeNull] - [Resolved(CanBeNull = true)] - private IBindable subScreenSelectedItem { get; set; } + protected readonly Bindable SelectedItem = new Bindable(); protected override void LoadComplete() { base.LoadComplete(); - if (subScreenSelectedItem != null) - subScreenSelectedItem.BindValueChanged(onSelectedItemChanged, true); - else - Playlist.BindCollectionChanged(onPlaylistChanged, true); + Playlist.BindCollectionChanged((_, __) => UpdateSelectedItem(), true); } - /// - /// Invoked when the selected item from within a changes. - /// Does not occur when this is outside a . - /// - private void onSelectedItemChanged(ValueChangedEvent item) + protected virtual void UpdateSelectedItem() { - // If the room hasn't been created yet, fall-back to the first item from the playlist. - selectedItem.Value = RoomID.Value == null ? Playlist.FirstOrDefault() : item.NewValue; - } - - /// - /// Invoked when the playlist changes. - /// Does not occur when this is inside a . - /// - private void onPlaylistChanged(object sender, NotifyCollectionChangedEventArgs e) - { - selectedItem.Value = Playlist.FirstOrDefault(); + SelectedItem.Value = Playlist.FirstOrDefault(); } } } diff --git a/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs b/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.cs new file mode 100644 index 0000000000..4cfd881aa3 --- /dev/null +++ b/osu.Game/Screens/OnlinePlay/RoomSubScreenComposite.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.Bindables; +using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Match; + +namespace osu.Game.Screens.OnlinePlay +{ + /// + /// An with additional logic tracking the currently-selected inside a . + /// + public class RoomSubScreenComposite : OnlinePlayComposite + { + [Resolved] + private IBindable subScreenSelectedItem { get; set; } + + protected override void LoadComplete() + { + base.LoadComplete(); + + subScreenSelectedItem.BindValueChanged(_ => UpdateSelectedItem(), true); + } + + protected override void UpdateSelectedItem() + { + if (RoomID.Value == null) + { + // If the room hasn't been created yet, fall-back to the base logic. + base.UpdateSelectedItem(); + return; + } + + SelectedItem.Value = subScreenSelectedItem.Value; + } + } +} From 46ba5de32c5e0dc63d94ab618121a61df655c2a9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Feb 2021 16:19:36 +0900 Subject: [PATCH 6666/6909] Fix collections being imported from BDL thread --- osu.Game/Collections/CollectionManager.cs | 48 +++++++++++++---------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index 569ac749a4..a65d9a415d 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -139,35 +139,43 @@ namespace osu.Game.Collections PostNotification?.Invoke(notification); var collection = readCollections(stream, notification); - bool importCompleted = false; - - Schedule(() => - { - importCollections(collection); - importCompleted = true; - }); - - while (!IsDisposed && !importCompleted) - await Task.Delay(10); + await importCollections(collection); notification.CompletionText = $"Imported {collection.Count} collections"; notification.State = ProgressNotificationState.Completed; } - private void importCollections(List newCollections) + private Task importCollections(List newCollections) { - foreach (var newCol in newCollections) - { - var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name); - if (existing == null) - Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } }); + var tcs = new TaskCompletionSource(); - foreach (var newBeatmap in newCol.Beatmaps) + Schedule(() => + { + try { - if (!existing.Beatmaps.Contains(newBeatmap)) - existing.Beatmaps.Add(newBeatmap); + foreach (var newCol in newCollections) + { + var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name); + if (existing == null) + Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } }); + + foreach (var newBeatmap in newCol.Beatmaps) + { + if (!existing.Beatmaps.Contains(newBeatmap)) + existing.Beatmaps.Add(newBeatmap); + } + } + + tcs.SetResult(true); } - } + catch (Exception e) + { + Logger.Error(e, "Failed to import collection."); + tcs.SetException(e); + } + }); + + return tcs.Task; } private List readCollections(Stream stream, ProgressNotification notification = null) From c3a98b6ad15475a86565c2501fe0faeb2e898b14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 16:59:43 +0900 Subject: [PATCH 6667/6909] Fix carousel items' borders getting blown out when selected and hovered I tried restructuring the hierarchy to avoid needing this added property (moving the hover layer out of the border container) but this leads to some subpixel leakage outside the borders which looks even worse. Closes #6915. --- .../Screens/Select/Carousel/CarouselHeader.cs | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index 947334c747..73324894ee 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -24,9 +24,13 @@ namespace osu.Game.Screens.Select.Carousel public Container BorderContainer; public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); + private HoverLayer hoverLayer; protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; + private const float corner_radius = 10; + private const float border_thickness = 2.5f; + public CarouselHeader() { RelativeSizeAxes = Axes.X; @@ -36,12 +40,12 @@ namespace osu.Game.Screens.Select.Carousel { RelativeSizeAxes = Axes.Both, Masking = true, - CornerRadius = 10, + CornerRadius = corner_radius, BorderColour = new Color4(221, 255, 255, 255), Children = new Drawable[] { Content, - new HoverLayer() + hoverLayer = new HoverLayer() } }; } @@ -59,6 +63,8 @@ namespace osu.Game.Screens.Select.Carousel { case CarouselItemState.Collapsed: case CarouselItemState.NotSelected: + hoverLayer.InsetForBorder = false; + BorderContainer.BorderThickness = 0; BorderContainer.EdgeEffect = new EdgeEffectParameters { @@ -70,7 +76,9 @@ namespace osu.Game.Screens.Select.Carousel break; case CarouselItemState.Selected: - BorderContainer.BorderThickness = 2.5f; + hoverLayer.InsetForBorder = true; + + BorderContainer.BorderThickness = border_thickness; BorderContainer.EdgeEffect = new EdgeEffectParameters { Type = EdgeEffectType.Glow, @@ -107,6 +115,26 @@ namespace osu.Game.Screens.Select.Carousel sampleHover = audio.Samples.Get("SongSelect/song-ping"); } + public bool InsetForBorder + { + set + { + if (value) + { + // apply same border as above to avoid applying additive overlay to it (and blowing out the colour). + Masking = true; + CornerRadius = corner_radius; + BorderThickness = border_thickness; + } + else + { + BorderThickness = 0; + CornerRadius = 0; + Masking = false; + } + } + } + protected override bool OnHover(HoverEvent e) { box.FadeIn(100, Easing.OutQuint); From b713eb2eae259e086ca3b62ff6e3d7b79bed06a4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 17:13:48 +0900 Subject: [PATCH 6668/6909] Make field readonly --- osu.Game/Screens/Select/Carousel/CarouselHeader.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs index 73324894ee..2fbf64de29 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselHeader.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselHeader.cs @@ -24,7 +24,8 @@ namespace osu.Game.Screens.Select.Carousel public Container BorderContainer; public readonly Bindable State = new Bindable(CarouselItemState.NotSelected); - private HoverLayer hoverLayer; + + private readonly HoverLayer hoverLayer; protected override Container Content { get; } = new Container { RelativeSizeAxes = Axes.Both }; From 668cc144f68c339ab03a4601c4d36faae86b051e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Feb 2021 17:39:01 +0900 Subject: [PATCH 6669/6909] Fix test failures + multiple filter operations firing --- .../Collections/CollectionFilterDropdown.cs | 22 ++++++++++++++----- .../Collections/CollectionFilterMenuItem.cs | 8 ++++++- 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index 3e55ecb084..aad8400faa 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -29,6 +29,14 @@ namespace osu.Game.Collections /// protected virtual bool ShowManageCollectionsItem => true; + private readonly BindableWithCurrent current = new BindableWithCurrent(); + + public new Bindable Current + { + get => current.Current; + set => current.Current = value; + } + private readonly IBindableList collections = new BindableList(); private readonly IBindableList beatmaps = new BindableList(); private readonly BindableList filters = new BindableList(); @@ -36,14 +44,15 @@ namespace osu.Game.Collections [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } + [Resolved(CanBeNull = true)] + private CollectionManager collectionManager { get; set; } + public CollectionFilterDropdown() { ItemSource = filters; + Current.Value = new AllBeatmapsCollectionFilterMenuItem(); } - [Resolved(CanBeNull = true)] - private CollectionManager collectionManager { get; set; } - protected override void LoadComplete() { base.LoadComplete(); @@ -51,9 +60,12 @@ namespace osu.Game.Collections if (collectionManager != null) collections.BindTo(collectionManager.Collections); - collections.CollectionChanged += (_, __) => collectionsChanged(); - collectionsChanged(); + // Dropdown has logic which triggers a change on the bindable with every change to the contained items. + // This is not desirable here, as it leads to multiple filter operations running even though nothing has changed. + // An extra bindable is enough to subvert this behaviour. + base.Current.BindTo(Current); + collections.BindCollectionChanged((_, __) => collectionsChanged(), true); Current.BindValueChanged(filterChanged, true); } diff --git a/osu.Game/Collections/CollectionFilterMenuItem.cs b/osu.Game/Collections/CollectionFilterMenuItem.cs index 4a489d2945..fe79358223 100644 --- a/osu.Game/Collections/CollectionFilterMenuItem.cs +++ b/osu.Game/Collections/CollectionFilterMenuItem.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 JetBrains.Annotations; using osu.Framework.Bindables; @@ -9,7 +10,7 @@ namespace osu.Game.Collections /// /// A filter. /// - public class CollectionFilterMenuItem + public class CollectionFilterMenuItem : IEquatable { /// /// The collection to filter beatmaps from. @@ -33,6 +34,11 @@ namespace osu.Game.Collections Collection = collection; CollectionName = Collection?.Name.GetBoundCopy() ?? new Bindable("All beatmaps"); } + + public bool Equals(CollectionFilterMenuItem other) + => other != null && CollectionName.Value == other.CollectionName.Value; + + public override int GetHashCode() => CollectionName.Value.GetHashCode(); } public class AllBeatmapsCollectionFilterMenuItem : CollectionFilterMenuItem From 71316bbee568c7ded598d502204054db85ffbb53 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 17:45:58 +0900 Subject: [PATCH 6670/6909] Allow using OnlineViewContainer without deriving it --- osu.Game/Online/OnlineViewContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/OnlineViewContainer.cs b/osu.Game/Online/OnlineViewContainer.cs index c9fb70f0cc..8868f90524 100644 --- a/osu.Game/Online/OnlineViewContainer.cs +++ b/osu.Game/Online/OnlineViewContainer.cs @@ -15,7 +15,7 @@ namespace osu.Game.Online /// A for displaying online content which require a local user to be logged in. /// Shows its children only when the local user is logged in and supports displaying a placeholder if not. /// - public abstract class OnlineViewContainer : Container + public class OnlineViewContainer : Container { protected LoadingSpinner LoadingSpinner { get; private set; } @@ -30,7 +30,7 @@ namespace osu.Game.Online [Resolved] protected IAPIProvider API { get; private set; } - protected OnlineViewContainer(string placeholderMessage) + public OnlineViewContainer(string placeholderMessage) { this.placeholderMessage = placeholderMessage; } From c3f66a0c745ce7c53fbf948625f5ad620dd2d730 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 17:46:07 +0900 Subject: [PATCH 6671/6909] Add login placeholder for chat overlay --- osu.Game/Overlays/ChatOverlay.cs | 73 ++++++++++++++++---------------- 1 file changed, 36 insertions(+), 37 deletions(-) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index 8bc7e21047..f5dd4fae2c 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -24,6 +24,7 @@ using osu.Game.Overlays.Chat.Tabs; using osuTK.Input; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; +using osu.Game.Online; namespace osu.Game.Overlays { @@ -118,40 +119,47 @@ namespace osu.Game.Overlays { RelativeSizeAxes = Axes.Both, }, - currentChannelContainer = new Container + new OnlineViewContainer("Sign in to chat") { RelativeSizeAxes = Axes.Both, - Padding = new MarginPadding - { - Bottom = textbox_height - }, - }, - new Container - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - RelativeSizeAxes = Axes.X, - Height = textbox_height, - Padding = new MarginPadding - { - Top = padding * 2, - Bottom = padding * 2, - Left = ChatLine.LEFT_PADDING + padding * 2, - Right = padding * 2, - }, Children = new Drawable[] { - textbox = new FocusedTextBox + currentChannelContainer = new Container { RelativeSizeAxes = Axes.Both, - Height = 1, - PlaceholderText = "type your message", - ReleaseFocusOnCommit = false, - HoldFocus = true, - } - } - }, - loading = new LoadingSpinner(), + Padding = new MarginPadding + { + Bottom = textbox_height + }, + }, + new Container + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + RelativeSizeAxes = Axes.X, + Height = textbox_height, + Padding = new MarginPadding + { + Top = padding * 2, + Bottom = padding * 2, + Left = ChatLine.LEFT_PADDING + padding * 2, + Right = padding * 2, + }, + Children = new Drawable[] + { + textbox = new FocusedTextBox + { + RelativeSizeAxes = Axes.Both, + Height = 1, + PlaceholderText = "type your message", + ReleaseFocusOnCommit = false, + HoldFocus = true, + } + } + }, + loading = new LoadingSpinner(), + }, + } } }, tabsArea = new TabsArea @@ -184,9 +192,7 @@ namespace osu.Game.Overlays }, }, }; - textbox.OnCommit += postMessage; - ChannelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue; ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden; ChannelSelectionOverlay.State.ValueChanged += state => @@ -203,10 +209,8 @@ namespace osu.Game.Overlays else textbox.HoldFocus = true; }; - ChannelSelectionOverlay.OnRequestJoin = channel => channelManager.JoinChannel(channel); ChannelSelectionOverlay.OnRequestLeave = channelManager.LeaveChannel; - ChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); ChatHeight.BindValueChanged(height => { @@ -214,9 +218,7 @@ namespace osu.Game.Overlays channelSelectionContainer.Height = 1f - height.NewValue; tabBackground.FadeTo(height.NewValue == 1f ? 1f : 0.8f, 200); }, true); - chatBackground.Colour = colours.ChatBlue; - loading.Show(); // This is a relatively expensive (and blocking) operation. @@ -226,13 +228,10 @@ namespace osu.Game.Overlays { // TODO: consider scheduling bindable callbacks to not perform when overlay is not present. channelManager.JoinedChannels.CollectionChanged += joinedChannelsChanged; - foreach (Channel channel in channelManager.JoinedChannels) ChannelTabControl.AddChannel(channel); - channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged; availableChannelsChanged(null, null); - currentChannel = channelManager.CurrentChannel.GetBoundCopy(); currentChannel.BindValueChanged(currentChannelChanged, true); }); From 58d8f0733cfed3f432879263e827fc4cc1128162 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 17:45:58 +0900 Subject: [PATCH 6672/6909] Allow using OnlineViewContainer without deriving it --- osu.Game/Online/OnlineViewContainer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/OnlineViewContainer.cs b/osu.Game/Online/OnlineViewContainer.cs index c9fb70f0cc..8868f90524 100644 --- a/osu.Game/Online/OnlineViewContainer.cs +++ b/osu.Game/Online/OnlineViewContainer.cs @@ -15,7 +15,7 @@ namespace osu.Game.Online /// A for displaying online content which require a local user to be logged in. /// Shows its children only when the local user is logged in and supports displaying a placeholder if not. /// - public abstract class OnlineViewContainer : Container + public class OnlineViewContainer : Container { protected LoadingSpinner LoadingSpinner { get; private set; } @@ -30,7 +30,7 @@ namespace osu.Game.Online [Resolved] protected IAPIProvider API { get; private set; } - protected OnlineViewContainer(string placeholderMessage) + public OnlineViewContainer(string placeholderMessage) { this.placeholderMessage = placeholderMessage; } From 0bd1964d8e7db31f84374aa079c16e43b2c33a24 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 18:04:41 +0900 Subject: [PATCH 6673/6909] Add login placeholder logic to OnlineOverlay A perfect implementation of this would probably leave the filter/header content visible, but that requires some re-thinking and restructuring to how the content is displayed in these overlays (ie. the header component shouldn't be inside the `ScrollContainer` as it is fixed). Supersedes and closes #10774. Closes #933. Addresses most pieces of #7417. --- osu.Game/Overlays/ChangelogOverlay.cs | 2 +- osu.Game/Overlays/NewsOverlay.cs | 2 +- osu.Game/Overlays/OnlineOverlay.cs | 15 ++++++++++++--- osu.Game/Overlays/TabbableOnlineOverlay.cs | 3 +-- 4 files changed, 15 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 593f59555a..537dd00727 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -30,7 +30,7 @@ namespace osu.Game.Overlays protected List Streams; public ChangelogOverlay() - : base(OverlayColourScheme.Purple) + : base(OverlayColourScheme.Purple, false) { } diff --git a/osu.Game/Overlays/NewsOverlay.cs b/osu.Game/Overlays/NewsOverlay.cs index 08e8331dd3..5beb285216 100644 --- a/osu.Game/Overlays/NewsOverlay.cs +++ b/osu.Game/Overlays/NewsOverlay.cs @@ -14,7 +14,7 @@ namespace osu.Game.Overlays private readonly Bindable article = new Bindable(null); public NewsOverlay() - : base(OverlayColourScheme.Purple) + : base(OverlayColourScheme.Purple, false) { } diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index 7c9f751d3b..0a5ceb1993 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -4,6 +4,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics.UserInterface; +using osu.Game.Online; namespace osu.Game.Overlays { @@ -16,10 +17,16 @@ namespace osu.Game.Overlays protected readonly LoadingLayer Loading; private readonly Container content; - protected OnlineOverlay(OverlayColourScheme colourScheme) + protected OnlineOverlay(OverlayColourScheme colourScheme, bool requiresSignIn = true) : base(colourScheme) { - base.Content.AddRange(new Drawable[] + var mainContent = requiresSignIn + ? new OnlineViewContainer($"Sign in to view the {Header.Title.Title}") + : new Container(); + + mainContent.RelativeSizeAxes = Axes.Both; + + mainContent.AddRange(new Drawable[] { ScrollFlow = new OverlayScrollContainer { @@ -41,8 +48,10 @@ namespace osu.Game.Overlays } } }, - Loading = new LoadingLayer(true) + Loading = new LoadingLayer() }); + + base.Content.Add(mainContent); } } } diff --git a/osu.Game/Overlays/TabbableOnlineOverlay.cs b/osu.Game/Overlays/TabbableOnlineOverlay.cs index 8172e99c1b..9ceab12d3d 100644 --- a/osu.Game/Overlays/TabbableOnlineOverlay.cs +++ b/osu.Game/Overlays/TabbableOnlineOverlay.cs @@ -61,8 +61,7 @@ namespace osu.Game.Overlays LoadComponentAsync(display, loaded => { - if (API.IsLoggedIn) - Loading.Hide(); + Loading.Hide(); Child = loaded; }, (cancellationToken = new CancellationTokenSource()).Token); From 990c1b1d6ef118c755b8cdff7150601542f47535 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 18:19:57 +0900 Subject: [PATCH 6674/6909] Revert accidental removal of newlines --- osu.Game/Overlays/ChatOverlay.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Overlays/ChatOverlay.cs b/osu.Game/Overlays/ChatOverlay.cs index f5dd4fae2c..28f2287514 100644 --- a/osu.Game/Overlays/ChatOverlay.cs +++ b/osu.Game/Overlays/ChatOverlay.cs @@ -192,7 +192,9 @@ namespace osu.Game.Overlays }, }, }; + textbox.OnCommit += postMessage; + ChannelTabControl.Current.ValueChanged += current => channelManager.CurrentChannel.Value = current.NewValue; ChannelTabControl.ChannelSelectorActive.ValueChanged += active => ChannelSelectionOverlay.State.Value = active.NewValue ? Visibility.Visible : Visibility.Hidden; ChannelSelectionOverlay.State.ValueChanged += state => @@ -209,8 +211,10 @@ namespace osu.Game.Overlays else textbox.HoldFocus = true; }; + ChannelSelectionOverlay.OnRequestJoin = channel => channelManager.JoinChannel(channel); ChannelSelectionOverlay.OnRequestLeave = channelManager.LeaveChannel; + ChatHeight = config.GetBindable(OsuSetting.ChatDisplayHeight); ChatHeight.BindValueChanged(height => { @@ -218,7 +222,9 @@ namespace osu.Game.Overlays channelSelectionContainer.Height = 1f - height.NewValue; tabBackground.FadeTo(height.NewValue == 1f ? 1f : 0.8f, 200); }, true); + chatBackground.Colour = colours.ChatBlue; + loading.Show(); // This is a relatively expensive (and blocking) operation. @@ -228,10 +234,13 @@ namespace osu.Game.Overlays { // TODO: consider scheduling bindable callbacks to not perform when overlay is not present. channelManager.JoinedChannels.CollectionChanged += joinedChannelsChanged; + foreach (Channel channel in channelManager.JoinedChannels) ChannelTabControl.AddChannel(channel); + channelManager.AvailableChannels.CollectionChanged += availableChannelsChanged; availableChannelsChanged(null, null); + currentChannel = channelManager.CurrentChannel.GetBoundCopy(); currentChannel.BindValueChanged(currentChannelChanged, true); }); From a01896a652ee042cc66149a99ccf5b76dddef535 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 18 Feb 2021 13:04:22 +0300 Subject: [PATCH 6675/6909] Fix misordered hit error in score meter types --- osu.Game/Configuration/ScoreMeterType.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Configuration/ScoreMeterType.cs b/osu.Game/Configuration/ScoreMeterType.cs index b9499c758e..ddbd2327c2 100644 --- a/osu.Game/Configuration/ScoreMeterType.cs +++ b/osu.Game/Configuration/ScoreMeterType.cs @@ -16,12 +16,12 @@ namespace osu.Game.Configuration [Description("Hit Error (right)")] HitErrorRight, - [Description("Hit Error (bottom)")] - HitErrorBottom, - [Description("Hit Error (left+right)")] HitErrorBoth, + [Description("Hit Error (bottom)")] + HitErrorBottom, + [Description("Colour (left)")] ColourLeft, From d85a4a22e525cc4914c12bbe067e79f7ef8ba8cb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 19:19:28 +0900 Subject: [PATCH 6676/6909] Allow beatmap imports from any derived version of SongSelect, rather than only PlaySongSelect --- osu.Game/OsuGame.cs | 2 +- osu.Game/PerformFromMenuRunner.cs | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 15785ea6bd..771bcd2310 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -383,7 +383,7 @@ namespace osu.Game Ruleset.Value = selection.Ruleset; Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); - }, validScreens: new[] { typeof(PlaySongSelect) }); + }, validScreens: new[] { typeof(SongSelect) }); } /// diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index 7999023998..3df9ca5305 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -82,7 +82,9 @@ namespace osu.Game game?.CloseAllOverlays(false); // we may already be at the target screen type. - if (validScreens.Contains(current.GetType()) && !beatmap.Disabled) + var type = current.GetType(); + + if (validScreens.Any(t => type.IsAssignableFrom(t)) && !beatmap.Disabled) { finalAction(current); Cancel(); @@ -91,13 +93,14 @@ namespace osu.Game while (current != null) { - if (validScreens.Contains(current.GetType())) + if (validScreens.Any(t => type.IsAssignableFrom(t))) { current.MakeCurrent(); break; } current = current.GetParentScreen(); + type = current?.GetType(); } } From 8a1a4ea2d42874d1dc752d4ec6a3371d02940054 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 18 Feb 2021 19:33:04 +0900 Subject: [PATCH 6677/6909] Set Current directly --- osu.Game/Collections/CollectionFilterDropdown.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index aad8400faa..bb743d4ccc 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -63,7 +63,7 @@ namespace osu.Game.Collections // Dropdown has logic which triggers a change on the bindable with every change to the contained items. // This is not desirable here, as it leads to multiple filter operations running even though nothing has changed. // An extra bindable is enough to subvert this behaviour. - base.Current.BindTo(Current); + base.Current = Current; collections.BindCollectionChanged((_, __) => collectionsChanged(), true); Current.BindValueChanged(filterChanged, true); From e14a59f272f7c5feaab972493c2fc8cf1a91c3e6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 18 Feb 2021 15:26:59 +0300 Subject: [PATCH 6678/6909] Fix creating ruleset instances per LINQ select --- .../OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs index 25bc314f1b..5bef934e6a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/Participants/ParticipantPanel.cs @@ -163,7 +163,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants const double fade_time = 50; - var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID); + var ruleset = rulesets.GetRuleset(Room.Settings.RulesetID).CreateInstance(); var currentModeRank = User.User?.RulesetsStatistics?.GetValueOrDefault(ruleset.ShortName)?.GlobalRank; userRankText.Text = currentModeRank != null ? $"#{currentModeRank.Value:N0}" : string.Empty; @@ -177,7 +177,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer.Participants // If the mods are updated at the end of the frame, the flow container will skip a reflow cycle: https://github.com/ppy/osu-framework/issues/4187 // This looks particularly jarring here, so re-schedule the update to that start of our frame as a fix. - Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset.CreateInstance())).ToList()); + Schedule(() => userModsDisplay.Current.Value = User.Mods.Select(m => m.ToMod(ruleset)).ToList()); } public MenuItem[] ContextMenuItems From a407bfe73bc0e4aefce7a30a00a4daf6cb30c481 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 18 Feb 2021 15:37:52 +0300 Subject: [PATCH 6679/6909] Privatize `UserRanks` and expose a similar `CountryRank` field instead --- .../Visual/Online/TestSceneUserProfileOverlay.cs | 2 +- .../Profile/Header/CentreHeaderContainer.cs | 2 +- .../Profile/Header/DetailHeaderContainer.cs | 2 +- osu.Game/Users/UserStatistics.cs | 13 +++++++++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs index b52cc6edb6..03d079261d 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneUserProfileOverlay.cs @@ -34,7 +34,7 @@ namespace osu.Game.Tests.Visual.Online Statistics = new UserStatistics { GlobalRank = 2148, - Ranks = new UserStatistics.UserRanks { Country = 1, }, + CountryRank = 1, PP = 4567.89m, Level = new UserStatistics.LevelInfo { diff --git a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs index 9285e2d875..62ebee7677 100644 --- a/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/CentreHeaderContainer.cs @@ -145,7 +145,7 @@ namespace osu.Game.Overlays.Profile.Header private void updateDisplay(User user) { hiddenDetailGlobal.Content = user?.Statistics?.GlobalRank?.ToString("\\##,##0") ?? "-"; - hiddenDetailCountry.Content = user?.Statistics?.Ranks.Country?.ToString("\\##,##0") ?? "-"; + hiddenDetailCountry.Content = user?.Statistics?.CountryRank?.ToString("\\##,##0") ?? "-"; } } } diff --git a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs index 05a0508e1f..574aef02fd 100644 --- a/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/DetailHeaderContainer.cs @@ -177,7 +177,7 @@ namespace osu.Game.Overlays.Profile.Header scoreRankInfo.Value.RankCount = user?.Statistics?.GradesCount[scoreRankInfo.Key] ?? 0; detailGlobalRank.Content = user?.Statistics?.GlobalRank?.ToString("\\##,##0") ?? "-"; - detailCountryRank.Content = user?.Statistics?.Ranks.Country?.ToString("\\##,##0") ?? "-"; + detailCountryRank.Content = user?.Statistics?.CountryRank?.ToString("\\##,##0") ?? "-"; rankGraph.Statistics.Value = user?.Statistics; } diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index e50ca57d90..90c1d40848 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -29,10 +29,15 @@ namespace osu.Game.Users [JsonProperty(@"global_rank")] public int? GlobalRank; - // eventually UserRanks object will be completely replaced with separate global rank (exists) and country rank properties - // see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53. + public int? CountryRank; + [JsonProperty(@"rank")] - public UserRanks Ranks; + private UserRanks ranks + { + // eventually that will also become an own json property instead of reading from a `rank` object. + // see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53. + set => CountryRank = value.Country; + } [JsonProperty(@"pp")] public decimal? PP; @@ -115,7 +120,7 @@ namespace osu.Game.Users } } - public struct UserRanks + private struct UserRanks { [JsonProperty(@"country")] public int? Country; From f6df5a9d2b67272e6a1e42a6cd71dad5777c0e02 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 18 Feb 2021 15:55:45 +0300 Subject: [PATCH 6680/6909] Suppress false warning --- osu.Game/Users/UserStatistics.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 90c1d40848..4b1e46d51a 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -122,8 +122,10 @@ namespace osu.Game.Users private struct UserRanks { +#pragma warning disable 649 [JsonProperty(@"country")] public int? Country; +#pragma warning restore 649 } public RankHistoryData RankHistory; From 10ec4cd8e07950b8a48e8da8931e3e39b16559b8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 18 Feb 2021 22:38:17 +0900 Subject: [PATCH 6681/6909] Revert change to loading layer's default state --- osu.Game/Overlays/OnlineOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/OnlineOverlay.cs b/osu.Game/Overlays/OnlineOverlay.cs index 0a5ceb1993..de33e4a1bc 100644 --- a/osu.Game/Overlays/OnlineOverlay.cs +++ b/osu.Game/Overlays/OnlineOverlay.cs @@ -48,7 +48,7 @@ namespace osu.Game.Overlays } } }, - Loading = new LoadingLayer() + Loading = new LoadingLayer(true) }); base.Content.Add(mainContent); From 4caca9653ab02ddbb9dea6105bb5cbeb807fae2b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 10:39:56 +0900 Subject: [PATCH 6682/6909] 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 e30416bc1c..bc5ba57d71 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 72f680f6f8..fef2f567df 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 137c96a72d..0d473290e6 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -91,7 +91,7 @@ - + From bc10fcafae2d57b6bb07d6bf666ee72577e5b8f9 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Feb 2021 13:23:01 +0900 Subject: [PATCH 6683/6909] Remove now unnecessary schedule --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index b5eff04532..cb2b6dda95 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -47,7 +47,6 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private Drawable userModsSection; private readonly IBindable isConnected = new Bindable(); - private readonly IBindable matchCurrentItem = new Bindable(); [CanBeNull] private IDisposable readyClickOperation; @@ -269,8 +268,8 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer { base.LoadComplete(); - matchCurrentItem.BindTo(client.CurrentMatchPlayingItem); - matchCurrentItem.BindValueChanged(onCurrentItemChanged, true); + SelectedItem.BindTo(client.CurrentMatchPlayingItem); + SelectedItem.BindValueChanged(onSelectedItemChanged, true); BeatmapAvailability.BindValueChanged(updateBeatmapAvailability, true); UserMods.BindValueChanged(onUserModsChanged); @@ -286,20 +285,11 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer }, true); } - private void onCurrentItemChanged(ValueChangedEvent item) + private void onSelectedItemChanged(ValueChangedEvent item) { if (client?.LocalUser == null) return; - // If we're about to enter gameplay, schedule the item to be set at a later time. - if (client.LocalUser.State > MultiplayerUserState.Ready) - { - Schedule(() => onCurrentItemChanged(item)); - return; - } - - SelectedItem.Value = item.NewValue; - if (item.NewValue?.AllowedMods.Any() != true) { userModsSection.Hide(); From 841c2c56d961e7eb76cb48f4fe2686f9250e6e7a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 13:30:42 +0900 Subject: [PATCH 6684/6909] Remove confusing pp_rank include (will be removed osu-web side too) --- osu.Game/Users/UserStatistics.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 4b1e46d51a..70969ea737 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -42,9 +42,6 @@ namespace osu.Game.Users [JsonProperty(@"pp")] public decimal? PP; - [JsonProperty(@"pp_rank")] - public int PPRank; - [JsonProperty(@"ranked_score")] public long RankedScore; From 183a481a345ea4fff4b55dd6df7fcdbc4e1dad4b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Feb 2021 13:32:32 +0900 Subject: [PATCH 6685/6909] Refactor playlist update to remove .Contains() check --- .../Multiplayer/StatefulMultiplayerClient.cs | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 416162779d..ed97307c95 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -537,21 +537,30 @@ namespace osu.Game.Online.Multiplayer var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset)); var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); - // Update an existing playlist item from the API room, or create a new item. - var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId) ?? new PlaylistItem(); + // Try to retrieve the existing playlist item from the API room. + var playlistItem = apiRoom.Playlist.FirstOrDefault(i => i.ID == settings.PlaylistItemId); - playlistItem.ID = settings.PlaylistItemId; - playlistItem.Beatmap.Value = beatmap; - playlistItem.Ruleset.Value = ruleset.RulesetInfo; - playlistItem.RequiredMods.Clear(); - playlistItem.RequiredMods.AddRange(mods); - playlistItem.AllowedMods.Clear(); - playlistItem.AllowedMods.AddRange(allowedMods); - - if (!apiRoom.Playlist.Contains(playlistItem)) + if (playlistItem != null) + updateItem(playlistItem); + else + { + // An existing playlist item does not exist, so append a new one. + updateItem(playlistItem = new PlaylistItem()); apiRoom.Playlist.Add(playlistItem); + } CurrentMatchPlayingItem.Value = playlistItem; + + void updateItem(PlaylistItem item) + { + item.ID = settings.PlaylistItemId; + item.Beatmap.Value = beatmap; + item.Ruleset.Value = ruleset.RulesetInfo; + item.RequiredMods.Clear(); + item.RequiredMods.AddRange(mods); + item.AllowedMods.Clear(); + item.AllowedMods.AddRange(allowedMods); + } } /// From 85a844a37820425e05df93d2d615593dbcc1989f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 13:40:12 +0900 Subject: [PATCH 6686/6909] Restructure class slightly --- osu.Game/Users/UserStatistics.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 70969ea737..78e6f5a05a 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -39,6 +39,9 @@ namespace osu.Game.Users set => CountryRank = value.Country; } + // populated via User model, as that's where the data currently lives. + public RankHistoryData RankHistory; + [JsonProperty(@"pp")] public decimal? PP; @@ -117,14 +120,12 @@ namespace osu.Game.Users } } +#pragma warning disable 649 private struct UserRanks { -#pragma warning disable 649 [JsonProperty(@"country")] public int? Country; -#pragma warning restore 649 } - - public RankHistoryData RankHistory; +#pragma warning restore 649 } } From c0e0bd4f421764ae6418597ed30bb64d501a69e8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Fri, 19 Feb 2021 13:57:04 +0900 Subject: [PATCH 6687/6909] Add compatibility with old server build --- osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index ed97307c95..bfd505fb19 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -94,6 +94,10 @@ namespace osu.Game.Online.Multiplayer [Resolved] private RulesetStore rulesets { get; set; } = null!; + // Only exists for compatibility with old osu-server-spectator build. + // Todo: Can be removed on 2021/02/26. + private long defaultPlaylistItemId; + private Room? apiRoom; [BackgroundDependencyLoader] @@ -141,6 +145,7 @@ namespace osu.Game.Online.Multiplayer { Room = joinedRoom; apiRoom = room; + defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0; }, cancellationSource.Token); // Update room settings. @@ -553,7 +558,7 @@ namespace osu.Game.Online.Multiplayer void updateItem(PlaylistItem item) { - item.ID = settings.PlaylistItemId; + item.ID = settings.PlaylistItemId == 0 ? defaultPlaylistItemId : settings.PlaylistItemId; item.Beatmap.Value = beatmap; item.Ruleset.Value = ruleset.RulesetInfo; item.RequiredMods.Clear(); From 87edf6787981a49c7160dac2d26beebf8b404416 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 14:07:39 +0900 Subject: [PATCH 6688/6909] 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 bc5ba57d71..bfdc8f6b3c 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 fef2f567df..4138fc8d6c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 0d473290e6..783b638aa0 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -91,7 +91,7 @@ - + From 1701d69a602520fb75c7f39f5c3fe7ad7d9f641d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 14:33:08 +0900 Subject: [PATCH 6689/6909] Fix calls to IsAssignableFrom being back-to-front --- osu.Game/PerformFromMenuRunner.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index 3df9ca5305..a4179c94da 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -84,7 +84,7 @@ namespace osu.Game // we may already be at the target screen type. var type = current.GetType(); - if (validScreens.Any(t => type.IsAssignableFrom(t)) && !beatmap.Disabled) + if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled) { finalAction(current); Cancel(); @@ -93,7 +93,7 @@ namespace osu.Game while (current != null) { - if (validScreens.Any(t => type.IsAssignableFrom(t))) + if (validScreens.Any(t => t.IsAssignableFrom(type))) { current.MakeCurrent(); break; From 39059ed82d57fa526018371089dc609c1fc0b7b3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 14:36:51 +0900 Subject: [PATCH 6690/6909] Remove unnecessary null coalesce check --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 15e12eac40..da516798c8 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -166,19 +166,21 @@ namespace osu.Game.Screens.OnlinePlay.Match { updateWorkingBeatmap(); - if (SelectedItem.Value == null) + var selected = SelectedItem.Value; + + if (selected == null) return; // Remove any user mods that are no longer allowed. UserMods.Value = UserMods.Value - .Where(m => SelectedItem.Value.AllowedMods.Any(a => m.GetType() == a.GetType())) + .Where(m => selected.AllowedMods.Any(a => m.GetType() == a.GetType())) .ToList(); UpdateMods(); - Ruleset.Value = SelectedItem.Value.Ruleset.Value; + Ruleset.Value = selected.Ruleset.Value; - if (SelectedItem.Value?.AllowedMods.Any() != true) + if (selected.AllowedMods.Any() != true) { UserModsSection?.Hide(); userModsSelectOverlay.Hide(); @@ -187,7 +189,7 @@ namespace osu.Game.Screens.OnlinePlay.Match else { UserModsSection?.Show(); - userModsSelectOverlay.IsValidMod = m => SelectedItem.Value.AllowedMods.Any(a => a.GetType() == m.GetType()); + userModsSelectOverlay.IsValidMod = m => selected.AllowedMods.Any(a => a.GetType() == m.GetType()); } } From 484968d797852ce71cb412ade2e43e11b267cd0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 14:46:10 +0900 Subject: [PATCH 6691/6909] Fix weird bool check Co-authored-by: Dan Balasescu --- osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs index 52705302aa..4a689314db 100644 --- a/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Match/RoomSubScreen.cs @@ -172,7 +172,7 @@ namespace osu.Game.Screens.OnlinePlay.Match Ruleset.Value = selected.Ruleset.Value; - if (selected.AllowedMods.Any() != true) + if (!selected.AllowedMods.Any()) { UserModsSection?.Hide(); userModsSelectOverlay.Hide(); From ee9e6fff402b146f2c99b8a872f3fe3f5cfc703f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 15:09:41 +0900 Subject: [PATCH 6692/6909] Add bindable flow for expanded leaderboard state --- .../Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs | 1 + osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs | 4 ++++ osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs | 2 ++ 3 files changed, 7 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index aab69d687a..026c302642 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -85,6 +85,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public void TestScoreUpdates() { AddRepeatStep("update state", () => streamingClient.RandomlyUpdateState(), 100); + AddToggleStep("switch compact mode", expanded => leaderboard.Expanded.Value = expanded); } [Test] diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 7b94bf19ec..20e24ed945 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -4,6 +4,7 @@ using System; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Bindables; using osu.Framework.Caching; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; @@ -16,6 +17,8 @@ namespace osu.Game.Screens.Play.HUD { private readonly Cached sorting = new Cached(); + public Bindable Expanded = new Bindable(); + public GameplayLeaderboard() { Width = GameplayLeaderboardScore.EXTENDED_WIDTH + GameplayLeaderboardScore.SHEAR_WIDTH; @@ -49,6 +52,7 @@ namespace osu.Game.Screens.Play.HUD { Anchor = Anchor.TopRight, Origin = Anchor.TopRight, + Expanded = { BindTarget = Expanded }, }; base.Add(drawable); diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index cb20deb272..f738f91e63 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -30,6 +30,8 @@ namespace osu.Game.Screens.Play.HUD private const float panel_shear = 0.15f; + public Bindable Expanded = new Bindable(); + private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; public BindableDouble TotalScore { get; } = new BindableDouble(); From 43c35c5118045f9c52a1755698073415f455d03b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 15:15:31 +0900 Subject: [PATCH 6693/6909] Show local user in test scene --- .../Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs index 026c302642..1ee848b902 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerGameplayLeaderboard.cs @@ -13,6 +13,7 @@ using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Database; using osu.Game.Online; +using osu.Game.Online.API; using osu.Game.Online.Spectator; using osu.Game.Replays.Legacy; using osu.Game.Rulesets.Osu.Scoring; @@ -50,6 +51,8 @@ namespace osu.Game.Tests.Visual.Multiplayer [SetUpSteps] public override void SetUpSteps() { + AddStep("set local user", () => ((DummyAPIAccess)API).LocalUser.Value = lookupCache.GetUserAsync(1).Result); + AddStep("create leaderboard", () => { leaderboard?.Expire(); From 691cfa5bc3bdceda779441dcd99a2437bd2beee8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 16:46:30 +0900 Subject: [PATCH 6694/6909] Add expanded/compact display modes for GameplayLeaderboard --- .../Screens/Play/HUD/GameplayLeaderboard.cs | 2 - .../Play/HUD/GameplayLeaderboardScore.cs | 347 +++++++++++------- 2 files changed, 210 insertions(+), 139 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs index 20e24ed945..34efeab54c 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboard.cs @@ -50,8 +50,6 @@ namespace osu.Game.Screens.Play.HUD { var drawable = new GameplayLeaderboardScore(user, isTracked) { - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, Expanded = { BindTarget = Expanded }, }; diff --git a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs index f738f91e63..10476e5565 100644 --- a/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs +++ b/osu.Game/Screens/Play/HUD/GameplayLeaderboardScore.cs @@ -20,16 +20,31 @@ namespace osu.Game.Screens.Play.HUD { public class GameplayLeaderboardScore : CompositeDrawable, ILeaderboardScore { - public const float EXTENDED_WIDTH = 255f; + public const float EXTENDED_WIDTH = regular_width + top_player_left_width_extension; private const float regular_width = 235f; + // a bit hand-wavy, but there's a lot of hard-coded paddings in each of the grid's internals. + private const float compact_width = 77.5f; + + private const float top_player_left_width_extension = 20f; + public const float PANEL_HEIGHT = 35f; public const float SHEAR_WIDTH = PANEL_HEIGHT * panel_shear; private const float panel_shear = 0.15f; + private const float rank_text_width = 35f; + + private const float score_components_width = 85f; + + private const float avatar_size = 25f; + + private const double panel_transition_duration = 500; + + private const double text_transition_duration = 200; + public Bindable Expanded = new Bindable(); private OsuSpriteText positionText, scoreText, accuracyText, comboText, usernameText; @@ -65,8 +80,15 @@ namespace osu.Game.Screens.Play.HUD private readonly bool trackedPlayer; private Container mainFillContainer; + private Box centralFill; + private Container backgroundPaddingAdjustContainer; + + private GridContainer gridContainer; + + private Container scoreComponents; + /// /// Creates a new . /// @@ -77,7 +99,8 @@ namespace osu.Game.Screens.Play.HUD User = user; this.trackedPlayer = trackedPlayer; - Size = new Vector2(EXTENDED_WIDTH, PANEL_HEIGHT); + AutoSizeAxes = Axes.X; + Height = PANEL_HEIGHT; } [BackgroundDependencyLoader] @@ -87,147 +110,167 @@ namespace osu.Game.Screens.Play.HUD InternalChildren = new Drawable[] { - mainFillContainer = new Container + new Container { - Width = regular_width, + AutoSizeAxes = Axes.X, RelativeSizeAxes = Axes.Y, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - Masking = true, - CornerRadius = 5f, - Shear = new Vector2(panel_shear, 0f), - Child = new Box + Margin = new MarginPadding { Left = top_player_left_width_extension }, + Children = new Drawable[] { - Alpha = 0.5f, - RelativeSizeAxes = Axes.Both, - } - }, - new GridContainer - { - Width = regular_width, - RelativeSizeAxes = Axes.Y, - Anchor = Anchor.TopRight, - Origin = Anchor.TopRight, - ColumnDimensions = new[] - { - new Dimension(GridSizeMode.Absolute, 35f), - new Dimension(), - new Dimension(GridSizeMode.Absolute, 85f), - }, - Content = new[] - { - new Drawable[] + backgroundPaddingAdjustContainer = new Container { - positionText = new OsuSpriteText + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] { - Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 }, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Colour = Color4.White, - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold), - Shadow = false, - }, - new Container - { - Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 }, - RelativeSizeAxes = Axes.Both, - Children = new Drawable[] + mainFillContainer = new Container { - new Container + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.Both, + Masking = true, + CornerRadius = 5f, + Shear = new Vector2(panel_shear, 0f), + Children = new Drawable[] { - Masking = true, - CornerRadius = 5f, - Shear = new Vector2(panel_shear, 0f), - RelativeSizeAxes = Axes.Both, - Children = new[] + new Box { - centralFill = new Box - { - Alpha = 0.5f, - RelativeSizeAxes = Axes.Both, - Colour = Color4Extensions.FromHex("3399cc"), - }, - } - }, - new FillFlowContainer - { - Padding = new MarginPadding { Left = SHEAR_WIDTH }, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - RelativeSizeAxes = Axes.Both, - Direction = FillDirection.Horizontal, - Spacing = new Vector2(4f, 0f), - Children = new Drawable[] - { - avatarContainer = new CircularContainer - { - Masking = true, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Size = new Vector2(25f), - Children = new Drawable[] - { - new Box - { - Name = "Placeholder while avatar loads", - Alpha = 0.3f, - RelativeSizeAxes = Axes.Both, - Colour = colours.Gray4, - } - } - }, - usernameText = new OsuSpriteText - { - RelativeSizeAxes = Axes.X, - Width = 0.6f, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = Color4.White, - Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), - Text = User?.Username, - Truncate = true, - Shadow = false, - } - } - }, - } - }, - new Container - { - Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f }, - RelativeSizeAxes = Axes.Both, - Anchor = Anchor.CentreLeft, - Origin = Anchor.CentreLeft, - Colour = Color4.White, - Children = new Drawable[] - { - scoreText = new OsuSpriteText - { - Spacing = new Vector2(-1f, 0f), - Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true), - Shadow = false, - }, - accuracyText = new OsuSpriteText - { - Anchor = Anchor.BottomLeft, - Origin = Anchor.BottomLeft, - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), - Spacing = new Vector2(-1f, 0f), - Shadow = false, - }, - comboText = new OsuSpriteText - { - Anchor = Anchor.BottomRight, - Origin = Anchor.BottomRight, - Spacing = new Vector2(-1f, 0f), - Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), - Shadow = false, + Alpha = 0.5f, + RelativeSizeAxes = Axes.Both, + }, }, }, } + }, + gridContainer = new GridContainer + { + RelativeSizeAxes = Axes.Y, + Width = compact_width, // will be updated by expanded state. + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + ColumnDimensions = new[] + { + new Dimension(GridSizeMode.Absolute, rank_text_width), + new Dimension(), + new Dimension(GridSizeMode.AutoSize, maxSize: score_components_width), + }, + Content = new[] + { + new Drawable[] + { + positionText = new OsuSpriteText + { + Padding = new MarginPadding { Right = SHEAR_WIDTH / 2 }, + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Colour = Color4.White, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.Bold), + Shadow = false, + }, + new Container + { + Padding = new MarginPadding { Horizontal = SHEAR_WIDTH / 3 }, + RelativeSizeAxes = Axes.Both, + Children = new Drawable[] + { + new Container + { + Masking = true, + CornerRadius = 5f, + Shear = new Vector2(panel_shear, 0f), + RelativeSizeAxes = Axes.Both, + Children = new[] + { + centralFill = new Box + { + Alpha = 0.5f, + RelativeSizeAxes = Axes.Both, + Colour = Color4Extensions.FromHex("3399cc"), + }, + } + }, + new FillFlowContainer + { + Padding = new MarginPadding { Left = SHEAR_WIDTH }, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + RelativeSizeAxes = Axes.Both, + Direction = FillDirection.Horizontal, + Spacing = new Vector2(4f, 0f), + Children = new Drawable[] + { + avatarContainer = new CircularContainer + { + Masking = true, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Size = new Vector2(avatar_size), + Children = new Drawable[] + { + new Box + { + Name = "Placeholder while avatar loads", + Alpha = 0.3f, + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray4, + } + } + }, + usernameText = new OsuSpriteText + { + RelativeSizeAxes = Axes.X, + Width = 0.6f, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.White, + Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), + Text = User?.Username, + Truncate = true, + Shadow = false, + } + } + }, + } + }, + scoreComponents = new Container + { + Padding = new MarginPadding { Top = 2f, Right = 17.5f, Bottom = 5f }, + AlwaysPresent = true, // required to smoothly animate autosize after hidden early. + Masking = true, + RelativeSizeAxes = Axes.Y, + Anchor = Anchor.CentreLeft, + Origin = Anchor.CentreLeft, + Colour = Color4.White, + Children = new Drawable[] + { + scoreText = new OsuSpriteText + { + Spacing = new Vector2(-1f, 0f), + Font = OsuFont.Torus.With(size: 16, weight: FontWeight.SemiBold, fixedWidth: true), + Shadow = false, + }, + accuracyText = new OsuSpriteText + { + Anchor = Anchor.BottomLeft, + Origin = Anchor.BottomLeft, + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), + Spacing = new Vector2(-1f, 0f), + Shadow = false, + }, + comboText = new OsuSpriteText + { + Anchor = Anchor.BottomRight, + Origin = Anchor.BottomRight, + Spacing = new Vector2(-1f, 0f), + Font = OsuFont.Torus.With(size: 12, weight: FontWeight.SemiBold, fixedWidth: true), + Shadow = false, + }, + }, + } + } + } } } - } + }, }; LoadComponentAsync(new DrawableAvatar(User), avatarContainer.Add); @@ -243,18 +286,43 @@ namespace osu.Game.Screens.Play.HUD base.LoadComplete(); updateState(); + Expanded.BindValueChanged(changeExpandedState, true); + FinishTransforms(true); } - private const double panel_transition_duration = 500; + private void changeExpandedState(ValueChangedEvent expanded) + { + scoreComponents.ClearTransforms(); + + if (expanded.NewValue) + { + gridContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutQuint); + + scoreComponents.ResizeWidthTo(score_components_width, panel_transition_duration, Easing.OutQuint); + scoreComponents.FadeIn(panel_transition_duration, Easing.OutQuint); + + usernameText.FadeIn(panel_transition_duration, Easing.OutQuint); + } + else + { + gridContainer.ResizeWidthTo(compact_width, panel_transition_duration, Easing.OutQuint); + + scoreComponents.ResizeWidthTo(0, panel_transition_duration, Easing.OutQuint); + scoreComponents.FadeOut(text_transition_duration, Easing.OutQuint); + + usernameText.FadeOut(text_transition_duration, Easing.OutQuint); + } + } private void updateState() { + bool widthExtension = false; + if (HasQuit.Value) { // we will probably want to display this in a better way once we have a design. // and also show states other than quit. - mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic); panelColour = Color4.Gray; textColour = Color4.White; return; @@ -262,22 +330,29 @@ namespace osu.Game.Screens.Play.HUD if (scorePosition == 1) { - mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); + widthExtension = true; panelColour = Color4Extensions.FromHex("7fcc33"); textColour = Color4.White; } else if (trackedPlayer) { - mainFillContainer.ResizeWidthTo(EXTENDED_WIDTH, panel_transition_duration, Easing.OutElastic); + widthExtension = true; panelColour = Color4Extensions.FromHex("ffd966"); textColour = Color4Extensions.FromHex("2e576b"); } else { - mainFillContainer.ResizeWidthTo(regular_width, panel_transition_duration, Easing.OutElastic); panelColour = Color4Extensions.FromHex("3399cc"); textColour = Color4.White; } + + this.TransformTo(nameof(SizeContainerLeftPadding), widthExtension ? -top_player_left_width_extension : 0, panel_transition_duration, Easing.OutElastic); + } + + public float SizeContainerLeftPadding + { + get => backgroundPaddingAdjustContainer.Padding.Left; + set => backgroundPaddingAdjustContainer.Padding = new MarginPadding { Left = value }; } private Color4 panelColour @@ -289,8 +364,6 @@ namespace osu.Game.Screens.Play.HUD } } - private const double text_transition_duration = 200; - private Color4 textColour { set From 772471a6d826a730ca17176f2c542b7e4d2ba44a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Feb 2021 09:34:39 +0300 Subject: [PATCH 6695/6909] Add failing test case --- .../Gameplay/TestScenePauseWhenInactive.cs | 63 ++++++++++++++++--- osu.Game/Screens/Play/Player.cs | 10 +-- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs index e43e5ba3ce..15412fea00 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs @@ -1,28 +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.Collections.Generic; +using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Platform; using osu.Framework.Testing; +using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Osu.Objects; +using osu.Game.Rulesets.Osu.UI; +using osu.Game.Storyboards; +using osu.Game.Tests.Beatmaps; +using osuTK.Input; namespace osu.Game.Tests.Visual.Gameplay { [HeadlessTest] // we alter unsafe properties on the game host to test inactive window state. public class TestScenePauseWhenInactive : OsuPlayerTestScene { - protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) - { - var beatmap = (Beatmap)base.CreateBeatmap(ruleset); - - beatmap.HitObjects.RemoveAll(h => h.StartTime < 30000); - - return beatmap; - } - [Resolved] private GameHost host { get; set; } @@ -33,10 +33,53 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("resume player", () => Player.GameplayClockContainer.Start()); AddAssert("ensure not paused", () => !Player.GameplayClockContainer.IsPaused.Value); + + AddStep("progress time to gameplay", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.GameplayStartTime)); + AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value); + } + + /// + /// Tests that if a pause from focus lose is performed while in pause cooldown, + /// the player will still pause after the cooldown is finished. + /// + [Test] + public void TestPauseWhileInCooldown() + { + AddStep("resume player", () => Player.GameplayClockContainer.Start()); + AddStep("skip to gameplay", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.GameplayStartTime)); + + AddStep("set inactive", () => ((Bindable)host.IsActive).Value = false); + AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value); + + AddStep("set active", () => ((Bindable)host.IsActive).Value = true); + + AddStep("resume player", () => Player.Resume()); + AddStep("click resume overlay", () => + { + InputManager.MoveMouseTo(this.ChildrenOfType().Single()); + InputManager.Click(MouseButton.Left); + }); + + AddAssert("pause cooldown active", () => Player.PauseCooldownActive); + AddStep("set inactive again", () => ((Bindable)host.IsActive).Value = false); AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value); - AddAssert("time of pause is after gameplay start time", () => Player.GameplayClockContainer.GameplayClock.CurrentTime >= Player.DrawableRuleset.GameplayStartTime); } protected override TestPlayer CreatePlayer(Ruleset ruleset) => new TestPlayer(true, true, true); + + protected override IBeatmap CreateBeatmap(RulesetInfo ruleset) + { + return new Beatmap + { + HitObjects = new List + { + new HitCircle { StartTime = 30000 }, + new HitCircle { StartTime = 35000 }, + }, + }; + } + + protected override WorkingBeatmap CreateWorkingBeatmap(IBeatmap beatmap, Storyboard storyboard = null) + => new TestWorkingBeatmap(beatmap, storyboard, Audio); } } diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 74059da21a..a7acda926b 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -667,6 +667,9 @@ namespace osu.Game.Screens.Play private double? lastPauseActionTime; + public bool PauseCooldownActive => + lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; + private bool canPause => // must pass basic screen conditions (beatmap loaded, instance allows pause) LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume @@ -675,10 +678,7 @@ namespace osu.Game.Screens.Play // cannot pause if we are already in a fail state && !HasFailed // cannot pause if already paused (or in a cooldown state) unless we are in a resuming state. - && (IsResuming || (GameplayClockContainer.IsPaused.Value == false && !pauseCooldownActive)); - - private bool pauseCooldownActive => - lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; + && (IsResuming || (GameplayClockContainer.IsPaused.Value == false && !PauseCooldownActive)); private bool canResume => // cannot resume from a non-paused state @@ -812,7 +812,7 @@ namespace osu.Game.Screens.Play // ValidForResume is false when restarting if (ValidForResume) { - if (pauseCooldownActive && !GameplayClockContainer.IsPaused.Value) + if (PauseCooldownActive && !GameplayClockContainer.IsPaused.Value) // still want to block if we are within the cooldown period and not already paused. return true; } From 4436585aa4dea5bec025e5bf816ed23008b4c87e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Feb 2021 09:35:29 +0300 Subject: [PATCH 6696/6909] Keep attempting to pause gameplay while window not active --- osu.Game/Screens/Play/Player.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index a7acda926b..72d9a60c91 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -427,11 +427,16 @@ namespace osu.Game.Screens.Play private void updatePauseOnFocusLostState() { - if (!PauseOnFocusLost || breakTracker.IsBreakTime.Value) + if (!PauseOnFocusLost || DrawableRuleset.HasReplayLoaded.Value || breakTracker.IsBreakTime.Value) return; if (gameActive.Value == false) - Pause(); + { + if (canPause) + Pause(); + else + Scheduler.AddDelayed(updatePauseOnFocusLostState, 200); + } } private IBeatmap loadPlayableBeatmap() From 9d02f589fe70bfa9b3d1b2ed9f46400b497250a1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 16:51:34 +0900 Subject: [PATCH 6697/6909] Compact leaderboard during gameplay --- .../Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index 04d9e0a72a..ffcf248575 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -89,6 +89,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Debug.Assert(client.Room != null); } + protected override void LoadComplete() + { + base.LoadComplete(); + + ((IBindable)leaderboard.Expanded).BindTo(IsBreakTime); + } + protected override void StartGameplay() { // block base call, but let the server know we are ready to start. From 52ebe343473007a06436cfe4c3969a129ef6c287 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 17:15:38 +0900 Subject: [PATCH 6698/6909] Update TestScenePause exit from fail test to actually fail --- .../Visual/Gameplay/TestScenePause.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index aa56c636ab..1214a33084 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -140,7 +140,7 @@ namespace osu.Game.Tests.Visual.Gameplay confirmClockRunning(false); - pauseFromUserExitKey(); + AddStep("pause via forced pause", () => Player.Pause()); confirmPausedWithNoOverlay(); AddAssert("fail overlay still shown", () => Player.FailOverlayVisible); @@ -149,11 +149,28 @@ namespace osu.Game.Tests.Visual.Gameplay } [Test] - public void TestExitFromFailedGameplay() + public void TestExitFromFailedGameplayAfterFailAnimation() { AddUntilStep("wait for fail", () => Player.HasFailed); - AddStep("exit", () => Player.Exit()); + AddUntilStep("wait for fail overlay shown", () => Player.FailOverlayVisible); + confirmClockRunning(false); + + AddStep("exit via user pause", () => Player.ExitViaPause()); + confirmExited(); + } + + [Test] + public void TestExitFromFailedGameplayDuringFailAnimation() + { + AddUntilStep("wait for fail", () => Player.HasFailed); + + // will finish the fail animation and show the fail/pause screen. + AddStep("attempt exit via pause key", () => Player.ExitViaPause()); + AddAssert("fail overlay shown", () => Player.FailOverlayVisible); + + // will actually exit. + AddStep("exit via pause key", () => Player.ExitViaPause()); confirmExited(); } From 82cc06ca57d1b8ba63739799c7db5120cfe3ae7e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 17:26:54 +0900 Subject: [PATCH 6699/6909] Fix new logic not considering fail overlay correctly --- osu.Game/Screens/Play/Player.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 8f2c1d1b92..e4fc92b5f2 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -496,12 +496,13 @@ namespace osu.Game.Screens.Play return; } - bool pauseDialogShown = PauseOverlay.State.Value == Visibility.Visible; + bool pauseOrFailDialogVisible = + PauseOverlay.State.Value == Visibility.Visible || FailOverlay.State.Value == Visibility.Visible; - if (showDialogFirst && !pauseDialogShown) + if (showDialogFirst && !pauseOrFailDialogVisible) { // if the fail animation is currently in progress, accelerate it (it will show the pause dialog on completion). - if (ValidForResume && HasFailed && !FailOverlay.IsPresent) + if (ValidForResume && HasFailed) { failAnimation.FinishTransforms(true); return; From ddd1dcff88428460cfcde74c963196a0518924fe Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Feb 2021 11:33:26 +0300 Subject: [PATCH 6700/6909] Attempt pausing every single frame --- 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 72d9a60c91..fa545859d4 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -435,7 +435,7 @@ namespace osu.Game.Screens.Play if (canPause) Pause(); else - Scheduler.AddDelayed(updatePauseOnFocusLostState, 200); + Scheduler.AddOnce(updatePauseOnFocusLostState); } } From 0771154dd2831f4d7de9d28913965f49df8909ce Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Feb 2021 11:42:30 +0300 Subject: [PATCH 6701/6909] Make `PauseCooldownActive` protected and expose on test class --- osu.Game/Screens/Play/Player.cs | 2 +- osu.Game/Tests/Visual/TestPlayer.cs | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index fa545859d4..8c816e8030 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -672,7 +672,7 @@ namespace osu.Game.Screens.Play private double? lastPauseActionTime; - public bool PauseCooldownActive => + protected bool PauseCooldownActive => lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; private bool canPause => diff --git a/osu.Game/Tests/Visual/TestPlayer.cs b/osu.Game/Tests/Visual/TestPlayer.cs index f47391ce6a..0addc9de75 100644 --- a/osu.Game/Tests/Visual/TestPlayer.cs +++ b/osu.Game/Tests/Visual/TestPlayer.cs @@ -34,6 +34,8 @@ namespace osu.Game.Tests.Visual public new HealthProcessor HealthProcessor => base.HealthProcessor; + public new bool PauseCooldownActive => base.PauseCooldownActive; + public readonly List Results = new List(); public TestPlayer(bool allowPause = true, bool showResults = true, bool pauseOnFocusLost = false) From fe5e45ea8180931f1c4c8d02a162e50b0000f186 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Feb 2021 11:43:33 +0300 Subject: [PATCH 6702/6909] Move gameplay cursor outside instead and fix potential failure --- .../Gameplay/TestScenePauseWhenInactive.cs | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs index 15412fea00..fa596c4823 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs @@ -2,21 +2,18 @@ // 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.Bindables; using osu.Framework.Platform; using osu.Framework.Testing; -using osu.Framework.Timing; using osu.Game.Beatmaps; using osu.Game.Rulesets; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Osu.Objects; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Storyboards; using osu.Game.Tests.Beatmaps; -using osuTK.Input; +using osuTK; namespace osu.Game.Tests.Visual.Gameplay { @@ -45,6 +42,8 @@ namespace osu.Game.Tests.Visual.Gameplay [Test] public void TestPauseWhileInCooldown() { + AddStep("move cursor outside", () => InputManager.MoveMouseTo(Player.ScreenSpaceDrawQuad.TopLeft - new Vector2(10))); + AddStep("resume player", () => Player.GameplayClockContainer.Start()); AddStep("skip to gameplay", () => Player.GameplayClockContainer.Seek(Player.DrawableRuleset.GameplayStartTime)); @@ -54,14 +53,15 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set active", () => ((Bindable)host.IsActive).Value = true); AddStep("resume player", () => Player.Resume()); - AddStep("click resume overlay", () => - { - InputManager.MoveMouseTo(this.ChildrenOfType().Single()); - InputManager.Click(MouseButton.Left); - }); - AddAssert("pause cooldown active", () => Player.PauseCooldownActive); - AddStep("set inactive again", () => ((Bindable)host.IsActive).Value = false); + bool pauseCooldownActive = false; + + AddStep("set inactive again", () => + { + pauseCooldownActive = Player.PauseCooldownActive; + ((Bindable)host.IsActive).Value = false; + }); + AddAssert("pause cooldown active", () => pauseCooldownActive); AddUntilStep("wait for pause", () => Player.GameplayClockContainer.IsPaused.Value); } From f6c279ab00d6af3231e09332e4d35b4c2ce7e106 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Fri, 19 Feb 2021 11:45:45 +0300 Subject: [PATCH 6703/6909] Add assert ensuring player resumed properly --- osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs index fa596c4823..49c1163c6c 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePauseWhenInactive.cs @@ -53,6 +53,7 @@ namespace osu.Game.Tests.Visual.Gameplay AddStep("set active", () => ((Bindable)host.IsActive).Value = true); AddStep("resume player", () => Player.Resume()); + AddAssert("unpaused", () => !Player.GameplayClockContainer.IsPaused.Value); bool pauseCooldownActive = false; From 362e4802f761980213893e30c2de0c038b1463db Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 17:58:04 +0900 Subject: [PATCH 6704/6909] Add the ability for PerformFromMenuRunner to inspect nested screen stacks --- osu.Game/PerformFromMenuRunner.cs | 21 ++++++++++++++++--- osu.Game/Screens/IHasSubScreenStack.cs | 15 +++++++++++++ .../Screens/OnlinePlay/OnlinePlayScreen.cs | 4 +++- 3 files changed, 36 insertions(+), 4 deletions(-) create mode 100644 osu.Game/Screens/IHasSubScreenStack.cs diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index a4179c94da..39889ea7fc 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -13,6 +13,7 @@ using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Notifications; +using osu.Game.Screens; using osu.Game.Screens.Menu; namespace osu.Game @@ -81,27 +82,41 @@ namespace osu.Game game?.CloseAllOverlays(false); - // we may already be at the target screen type. + findValidTarget(current); + } + + private bool findValidTarget(IScreen current) + { var type = current.GetType(); + // check if we are already at a valid target screen. if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled) { finalAction(current); Cancel(); - return; + return true; } while (current != null) { + // if this has a sub stack, recursively check the screens within it. + if (current is IHasSubScreenStack currentSubScreen) + { + if (findValidTarget(currentSubScreen.SubScreenStack.CurrentScreen)) + return true; + } + if (validScreens.Any(t => t.IsAssignableFrom(type))) { current.MakeCurrent(); - break; + return true; } current = current.GetParentScreen(); type = current?.GetType(); } + + return false; } /// diff --git a/osu.Game/Screens/IHasSubScreenStack.cs b/osu.Game/Screens/IHasSubScreenStack.cs new file mode 100644 index 0000000000..c5e2015109 --- /dev/null +++ b/osu.Game/Screens/IHasSubScreenStack.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.Screens; + +namespace osu.Game.Screens +{ + /// + /// A screen which manages a nested stack of screens within itself. + /// + public interface IHasSubScreenStack + { + ScreenStack SubScreenStack { get; } + } +} diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs index 71fd0d5c76..90e499c67f 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlayScreen.cs @@ -28,7 +28,7 @@ using osuTK; namespace osu.Game.Screens.OnlinePlay { [Cached] - public abstract class OnlinePlayScreen : OsuScreen + public abstract class OnlinePlayScreen : OsuScreen, IHasSubScreenStack { public override bool CursorVisible => (screenStack.CurrentScreen as IOnlinePlaySubScreen)?.CursorVisible ?? true; @@ -355,5 +355,7 @@ namespace osu.Game.Screens.OnlinePlay protected override double TransformDuration => 200; } } + + ScreenStack IHasSubScreenStack.SubScreenStack => screenStack; } } From 5eee46074cbe5821394539ed4812c3d1cc8af844 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 19 Feb 2021 19:45:29 +0900 Subject: [PATCH 6705/6909] Ensure the current screen is current when a sub screen is found as the target --- osu.Game/PerformFromMenuRunner.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index 39889ea7fc..fe75a3a607 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -103,7 +103,11 @@ namespace osu.Game if (current is IHasSubScreenStack currentSubScreen) { if (findValidTarget(currentSubScreen.SubScreenStack.CurrentScreen)) + { + // should be correct in theory, but currently untested/unused in existing implementations. + current.MakeCurrent(); return true; + } } if (validScreens.Any(t => t.IsAssignableFrom(type))) From 32556b1898cfb65e652a9bae2dabe827d663d27b Mon Sep 17 00:00:00 2001 From: Susko3 <16479013+Susko3@users.noreply.github.com> Date: Sat, 20 Feb 2021 02:32:44 +0100 Subject: [PATCH 6706/6909] add `Exported = true` to Activity manifest --- 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 ad929bbac3..d087c6218d 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -17,7 +17,7 @@ using osu.Game.Database; namespace osu.Android { - [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance)] + [Activity(Theme = "@android:style/Theme.NoTitleBar", MainLauncher = true, ScreenOrientation = ScreenOrientation.FullUser, SupportsPictureInPicture = false, ConfigurationChanges = ConfigChanges.Orientation | ConfigChanges.ScreenSize, HardwareAccelerated = false, LaunchMode = LaunchMode.SingleInstance, Exported = true)] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osz", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionView }, Categories = new[] { Intent.CategoryDefault }, DataScheme = "content", DataPathPattern = ".*\\\\.osk", DataHost = "*", DataMimeType = "*/*")] [IntentFilter(new[] { Intent.ActionSend, Intent.ActionSendMultiple }, Categories = new[] { Intent.CategoryDefault }, DataMimeTypes = new[] { "application/zip", "application/octet-stream", "application/download", "application/x-zip", "application/x-zip-compressed" })] From d2ec151c67d09a8a442960ffce216ac5f04a81e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Feb 2021 14:19:44 +0900 Subject: [PATCH 6707/6909] Add failing test for pausing when pause support is disabled --- osu.Game.Tests/Visual/Gameplay/TestScenePause.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs index 1214a33084..bddc7ab731 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestScenePause.cs @@ -90,6 +90,15 @@ namespace osu.Game.Tests.Visual.Gameplay resumeAndConfirm(); } + [Test] + public void TestUserPauseWhenPauseNotAllowed() + { + AddStep("disable pause support", () => Player.Configuration.AllowPause = false); + + pauseFromUserExitKey(); + confirmExited(); + } + [Test] public void TestUserPauseDuringCooldownTooSoon() { From 38a21249213912af02c88c08037026e94ab11de3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Feb 2021 13:35:25 +0900 Subject: [PATCH 6708/6909] Support instant exit if pausing is not allowed in the current game mode --- osu.Game/Screens/Play/Player.cs | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e4fc92b5f2..1e130b7f88 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -508,10 +508,14 @@ namespace osu.Game.Screens.Play return; } - // in the case a dialog needs to be shown, attempt to pause and show it. - // this may fail (see internal checks in Pause()) at which point the exit attempt will be aborted. - Pause(); - return; + // there's a chance the pausing is not supported in the current state, at which point immediate exit should be preferred. + if (pausingSupportedByCurrentState) + { + // in the case a dialog needs to be shown, attempt to pause and show it. + // this may fail (see internal checks in Pause()) but the fail cases are temporary, so don't fall through to Exit(). + Pause(); + return; + } } this.Exit(); @@ -670,15 +674,17 @@ namespace osu.Game.Screens.Play private double? lastPauseActionTime; - private bool canPause => + /// + /// A set of conditionals which defines whether the current game state and configuration allows for + /// pausing to be attempted via . If false, the game should generally exit if a user pause + /// is attempted. + /// + private bool pausingSupportedByCurrentState => // must pass basic screen conditions (beatmap loaded, instance allows pause) LoadedBeatmapSuccessfully && Configuration.AllowPause && ValidForResume // replays cannot be paused and exit immediately && !DrawableRuleset.HasReplayLoaded.Value - // cannot pause if we are already in a fail state - && !HasFailed - // cannot pause if already paused (or in a cooldown state) unless we are in a resuming state. - && (IsResuming || (GameplayClockContainer.IsPaused.Value == false && !pauseCooldownActive)); + && !HasFailed; private bool pauseCooldownActive => lastPauseActionTime.HasValue && GameplayClockContainer.GameplayClock.CurrentTime < lastPauseActionTime + pause_cooldown; @@ -693,7 +699,10 @@ namespace osu.Game.Screens.Play public void Pause() { - if (!canPause) return; + if (!pausingSupportedByCurrentState) return; + + if (!IsResuming && pauseCooldownActive) + return; if (IsResuming) { From 9d229a5ec2ae17f70f68ec9d77fb99b2a6ebeaf4 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Sat, 20 Feb 2021 16:27:58 +1100 Subject: [PATCH 6709/6909] Add tests for clockrate adjusted difficulty calculations --- .../CatchDifficultyCalculatorTest.cs | 5 +++++ .../ManiaDifficultyCalculatorTest.cs | 5 +++++ osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs | 6 ++++++ .../TaikoDifficultyCalculatorTest.cs | 6 ++++++ 4 files changed, 22 insertions(+) diff --git a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs index ee416e5a38..f4ee3f5a42 100644 --- a/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Catch.Tests/CatchDifficultyCalculatorTest.cs @@ -4,6 +4,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Catch.Difficulty; +using osu.Game.Rulesets.Catch.Mods; using osu.Game.Rulesets.Difficulty; using osu.Game.Tests.Beatmaps; @@ -17,6 +18,10 @@ namespace osu.Game.Rulesets.Catch.Tests public void Test(double expected, string name) => base.Test(expected, name); + [TestCase(5.0565038923984691d, "diffcalc-test")] + public void TestClockRateAdjusted(double expected, string name) + => Test(expected, name, new CatchModDoubleTime()); + protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new CatchDifficultyCalculator(new CatchRuleset(), beatmap); protected override Ruleset CreateRuleset() => new CatchRuleset(); diff --git a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs index a25551f854..09ca04be8a 100644 --- a/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Mania.Tests/ManiaDifficultyCalculatorTest.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Mania.Difficulty; +using osu.Game.Rulesets.Mania.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Mania.Tests @@ -17,6 +18,10 @@ namespace osu.Game.Rulesets.Mania.Tests public void Test(double expected, string name) => base.Test(expected, name); + [TestCase(2.7646128945056723d, "diffcalc-test")] + public void TestClockRateAdjusted(double expected, string name) + => Test(expected, name, new ManiaModDoubleTime()); + protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new ManiaDifficultyCalculator(new ManiaRuleset(), beatmap); protected override Ruleset CreateRuleset() => new ManiaRuleset(); diff --git a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs index 85a41137d4..a365ea10d4 100644 --- a/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Osu.Tests/OsuDifficultyCalculatorTest.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Osu.Difficulty; +using osu.Game.Rulesets.Osu.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Osu.Tests @@ -19,6 +20,11 @@ namespace osu.Game.Rulesets.Osu.Tests public void Test(double expected, string name) => base.Test(expected, name); + [TestCase(8.6228371119393064d, "diffcalc-test")] + [TestCase(1.2864585434597433d, "zero-length-sliders")] + public void TestClockRateAdjusted(double expected, string name) + => Test(expected, name, new OsuModDoubleTime()); + protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new OsuDifficultyCalculator(new OsuRuleset(), beatmap); protected override Ruleset CreateRuleset() => new OsuRuleset(); diff --git a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs index 71b3c23b50..eb21c02d5f 100644 --- a/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs +++ b/osu.Game.Rulesets.Taiko.Tests/TaikoDifficultyCalculatorTest.cs @@ -5,6 +5,7 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Taiko.Difficulty; +using osu.Game.Rulesets.Taiko.Mods; using osu.Game.Tests.Beatmaps; namespace osu.Game.Rulesets.Taiko.Tests @@ -18,6 +19,11 @@ namespace osu.Game.Rulesets.Taiko.Tests public void Test(double expected, string name) => base.Test(expected, name); + [TestCase(3.1473940254109078d, "diffcalc-test")] + [TestCase(3.1473940254109078d, "diffcalc-test-strong")] + public void TestClockRateAdjusted(double expected, string name) + => Test(expected, name, new TaikoModDoubleTime()); + protected override DifficultyCalculator CreateDifficultyCalculator(WorkingBeatmap beatmap) => new TaikoDifficultyCalculator(new TaikoRuleset(), beatmap); protected override Ruleset CreateRuleset() => new TaikoRuleset(); From 3b7ebfa2acad63c3a426610450d681eab9947450 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 20 Feb 2021 17:17:31 +0900 Subject: [PATCH 6710/6909] 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 bfdc8f6b3c..1513f6444d 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 4138fc8d6c..9c3d0c2020 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 783b638aa0..99ab88a064 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -91,7 +91,7 @@ - + From 66643a97b0af5b90793435d5b6abefae582ca163 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Sat, 6 Feb 2021 15:06:16 +1100 Subject: [PATCH 6711/6909] Add a list of mods to Skill class Although this isn't necessary for existing official rulesets and calculators, custom calculators can have use cases for accessing mods in difficulty calculation. For example, accounting for the effects of visual mods. --- .../Difficulty/CatchDifficultyCalculator.cs | 4 ++-- .../Difficulty/Skills/Movement.cs | 4 +++- .../Difficulty/ManiaDifficultyCalculator.cs | 4 ++-- osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs | 4 +++- .../Difficulty/OsuDifficultyCalculator.cs | 6 +++--- osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs | 6 ++++++ osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs | 6 ++++++ osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs | 6 ++++++ osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs | 6 ++++++ .../Difficulty/Skills/Stamina.cs | 5 ++++- .../Difficulty/TaikoDifficultyCalculator.cs | 10 +++++----- .../DifficultyAdjustmentModCombinationsTest.cs | 2 +- .../Rulesets/Difficulty/DifficultyCalculator.cs | 5 +++-- osu.Game/Rulesets/Difficulty/Skills/Skill.cs | 13 +++++++++++++ 14 files changed, 63 insertions(+), 18 deletions(-) diff --git a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs index a317ef252d..10aae70722 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/CatchDifficultyCalculator.cs @@ -69,7 +69,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty } } - protected override Skill[] CreateSkills(IBeatmap beatmap) + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) { halfCatcherWidth = Catcher.CalculateCatchWidth(beatmap.BeatmapInfo.BaseDifficulty) * 0.5f; @@ -78,7 +78,7 @@ namespace osu.Game.Rulesets.Catch.Difficulty return new Skill[] { - new Movement(halfCatcherWidth), + new Movement(mods, halfCatcherWidth), }; } diff --git a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs index e679231638..9ad719be1a 100644 --- a/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs +++ b/osu.Game.Rulesets.Catch/Difficulty/Skills/Movement.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Rulesets.Catch.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Catch.Difficulty.Skills { @@ -25,7 +26,8 @@ namespace osu.Game.Rulesets.Catch.Difficulty.Skills private float lastDistanceMoved; private double lastStrainTime; - public Movement(float halfCatcherWidth) + public Movement(Mod[] mods, float halfCatcherWidth) + : base(mods) { HalfCatcherWidth = halfCatcherWidth; } diff --git a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs index ade830764d..8c0b9ed8b7 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/ManiaDifficultyCalculator.cs @@ -68,9 +68,9 @@ namespace osu.Game.Rulesets.Mania.Difficulty // Sorting is done in CreateDifficultyHitObjects, since the full list of hitobjects is required. protected override IEnumerable SortObjects(IEnumerable input) => input; - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { - new Strain(((ManiaBeatmap)beatmap).TotalColumns) + new Strain(mods, ((ManiaBeatmap)beatmap).TotalColumns) }; protected override Mod[] DifficultyAdjustmentMods diff --git a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs index 7ebc1ff752..d6ea58ee78 100644 --- a/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs +++ b/osu.Game.Rulesets.Mania/Difficulty/Skills/Strain.cs @@ -6,6 +6,7 @@ using osu.Framework.Utils; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Mania.Difficulty.Preprocessing; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Objects; namespace osu.Game.Rulesets.Mania.Difficulty.Skills @@ -24,7 +25,8 @@ namespace osu.Game.Rulesets.Mania.Difficulty.Skills private double individualStrain; private double overallStrain; - public Strain(int totalColumns) + public Strain(Mod[] mods, int totalColumns) + : base(mods) { holdEndTimes = new double[totalColumns]; individualStrains = new double[totalColumns]; diff --git a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs index 6a7d76151c..75d6786d95 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/OsuDifficultyCalculator.cs @@ -79,10 +79,10 @@ namespace osu.Game.Rulesets.Osu.Difficulty } } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { - new Aim(), - new Speed() + new Aim(mods), + new Speed(mods) }; protected override Mod[] DifficultyAdjustmentMods => new Mod[] diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs index e74f4933b2..90cba13c7c 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Aim.cs @@ -4,6 +4,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -17,6 +18,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double angle_bonus_begin = Math.PI / 3; private const double timing_threshold = 107; + public Aim(Mod[] mods) + : base(mods) + { + } + protected override double SkillMultiplier => 26.25; protected override double StrainDecayBase => 0.15; diff --git a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs index 01f2fb8dc8..200bc7997d 100644 --- a/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs +++ b/osu.Game.Rulesets.Osu/Difficulty/Skills/Speed.cs @@ -4,6 +4,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu.Difficulty.Preprocessing; using osu.Game.Rulesets.Osu.Objects; @@ -27,6 +28,11 @@ namespace osu.Game.Rulesets.Osu.Difficulty.Skills private const double max_speed_bonus = 45; // ~330BPM private const double speed_balancing_factor = 40; + public Speed(Mod[] mods) + : base(mods) + { + } + protected override double StrainValueOf(DifficultyHitObject current) { if (current.BaseObject is Spinner) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 32421ee00a..cc0738e252 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -39,6 +40,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// private int currentMonoLength; + public Colour(Mod[] mods) + : base(mods) + { + } + protected override double StrainValueOf(DifficultyHitObject current) { // changing from/to a drum roll or a swell does not constitute a colour change. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs index 5569b27ad5..f2b8309ac5 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Rhythm.cs @@ -5,6 +5,7 @@ using System; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -47,6 +48,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// private int notesSinceRhythmChange; + public Rhythm(Mod[] mods) + : base(mods) + { + } + protected override double StrainValueOf(DifficultyHitObject current) { // drum rolls and swells are exempt. diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 0b61eb9930..c34cce0cd6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -5,6 +5,7 @@ using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; using osu.Game.Rulesets.Taiko.Objects; @@ -48,8 +49,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills /// /// Creates a skill. /// + /// Mods for use in skill calculations. /// Whether this instance is performing calculations for the right hand. - public Stamina(bool rightHand) + public Stamina(Mod[] mods, bool rightHand) + : base(mods) { hand = rightHand ? 1 : 0; } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index e5485db4df..fc198d2493 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -29,12 +29,12 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) => new Skill[] { - new Colour(), - new Rhythm(), - new Stamina(true), - new Stamina(false), + new Colour(mods), + new Rhythm(mods), + new Stamina(mods, true), + new Stamina(mods, false), }; protected override Mod[] DifficultyAdjustmentMods => new Mod[] diff --git a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs index 5c7adb3f49..1c0bfd56dd 100644 --- a/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs +++ b/osu.Game.Tests/NonVisual/DifficultyAdjustmentModCombinationsTest.cs @@ -212,7 +212,7 @@ namespace osu.Game.Tests.NonVisual throw new NotImplementedException(); } - protected override Skill[] CreateSkills(IBeatmap beatmap) + protected override Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods) { throw new NotImplementedException(); } diff --git a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs index f15e5e1df0..a25dc3e6db 100644 --- a/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs +++ b/osu.Game/Rulesets/Difficulty/DifficultyCalculator.cs @@ -64,7 +64,7 @@ namespace osu.Game.Rulesets.Difficulty private DifficultyAttributes calculate(IBeatmap beatmap, Mod[] mods, double clockRate) { - var skills = CreateSkills(beatmap); + var skills = CreateSkills(beatmap, mods); if (!beatmap.HitObjects.Any()) return CreateDifficultyAttributes(beatmap, mods, skills, clockRate); @@ -202,7 +202,8 @@ namespace osu.Game.Rulesets.Difficulty /// Creates the s to calculate the difficulty of an . /// /// The whose difficulty will be calculated. + /// Mods to calculate difficulty with. /// The s. - protected abstract Skill[] CreateSkills(IBeatmap beatmap); + protected abstract Skill[] CreateSkills(IBeatmap beatmap, Mod[] mods); } } diff --git a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs index 1063a24b27..95117be073 100644 --- a/osu.Game/Rulesets/Difficulty/Skills/Skill.cs +++ b/osu.Game/Rulesets/Difficulty/Skills/Skill.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.Linq; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Utils; +using osu.Game.Rulesets.Mods; namespace osu.Game.Rulesets.Difficulty.Skills { @@ -46,10 +47,22 @@ namespace osu.Game.Rulesets.Difficulty.Skills /// protected double CurrentStrain { get; private set; } = 1; + /// + /// Mods for use in skill calculations. + /// + protected IReadOnlyList Mods => mods; + private double currentSectionPeak = 1; // We also keep track of the peak strain level in the current section. private readonly List strainPeaks = new List(); + private readonly Mod[] mods; + + protected Skill(Mod[] mods) + { + this.mods = mods; + } + /// /// Process a and update current strain values accordingly. /// From 8d463987dd6c7260f25c90a31804d180c147b5df Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Feb 2021 13:21:50 +0900 Subject: [PATCH 6712/6909] Fix being able to select incompatible freemods --- .../Screens/OnlinePlay/OnlinePlaySongSelect.cs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs index f0c77b79bf..3f30ef1176 100644 --- a/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/OnlinePlaySongSelect.cs @@ -75,9 +75,18 @@ namespace osu.Game.Screens.OnlinePlay Mods.Value = selectedItem?.Value?.RequiredMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); FreeMods.Value = selectedItem?.Value?.AllowedMods.Select(m => m.CreateCopy()).ToArray() ?? Array.Empty(); + Mods.BindValueChanged(onModsChanged); Ruleset.BindValueChanged(onRulesetChanged); } + private void onModsChanged(ValueChangedEvent> mods) + { + FreeMods.Value = FreeMods.Value.Where(checkCompatibleFreeMod).ToList(); + + // Reset the validity delegate to update the overlay's display. + freeModSelectOverlay.IsValidMod = IsValidFreeMod; + } + private void onRulesetChanged(ValueChangedEvent ruleset) { FreeMods.Value = Array.Empty(); @@ -155,6 +164,10 @@ namespace osu.Game.Screens.OnlinePlay /// /// The to check. /// Whether is a selectable free-mod. - protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod); + protected virtual bool IsValidFreeMod(Mod mod) => IsValidMod(mod) && checkCompatibleFreeMod(mod); + + private bool checkCompatibleFreeMod(Mod mod) + => Mods.Value.All(m => m.Acronym != mod.Acronym) // Mod must not be contained in the required mods. + && ModUtils.CheckCompatibleSet(Mods.Value.Append(mod).ToArray()); // Mod must be compatible with all the required mods. } } From ca92ad715a9a11bd772b2f53d04f3cb5a8e13431 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Feb 2021 13:32:54 +0900 Subject: [PATCH 6713/6909] Add test --- osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs | 2 +- .../TestSceneMultiplayerMatchSongSelect.cs | 28 +++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs index df0a41455f..4b0939db16 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModTraceable.cs @@ -11,7 +11,7 @@ using osu.Game.Rulesets.Osu.Skinning.Default; namespace osu.Game.Rulesets.Osu.Mods { - internal class OsuModTraceable : ModWithVisibilityAdjustment + public class OsuModTraceable : ModWithVisibilityAdjustment { public override string Name => "Traceable"; public override string Acronym => "TC"; diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 95c333e9f4..faa5d9e6fc 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -7,17 +7,23 @@ using System.Linq; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Audio; +using osu.Framework.Bindables; using osu.Framework.Extensions; +using osu.Framework.Extensions.TypeExtensions; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Framework.Testing; using osu.Framework.Utils; using osu.Game.Beatmaps; +using osu.Game.Overlays.Mods; using osu.Game.Rulesets; using osu.Game.Rulesets.Catch; +using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.Osu; using osu.Game.Rulesets.Osu.Mods; using osu.Game.Rulesets.Taiko; using osu.Game.Rulesets.Taiko.Mods; +using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.Select; @@ -137,8 +143,30 @@ namespace osu.Game.Tests.Visual.Multiplayer AddAssert("mods not changed", () => SelectedMods.Value.Single() is TaikoModDoubleTime); } + [TestCase(typeof(OsuModHidden), typeof(OsuModHidden))] // Same mod. + [TestCase(typeof(OsuModHidden), typeof(OsuModTraceable))] // Incompatible. + public void TestAllowedModDeselectedWhenRequired(Type allowedMod, Type requiredMod) + { + AddStep($"select {allowedMod.ReadableName()} as allowed", () => songSelect.FreeMods.Value = new[] { (Mod)Activator.CreateInstance(allowedMod) }); + AddStep($"select {requiredMod.ReadableName()} as required", () => songSelect.Mods.Value = new[] { (Mod)Activator.CreateInstance(requiredMod) }); + + AddAssert("freemods empty", () => songSelect.FreeMods.Value.Count == 0); + assertHasFreeModButton(allowedMod, false); + assertHasFreeModButton(requiredMod, false); + } + + private void assertHasFreeModButton(Type type, bool hasButton = true) + { + AddAssert($"{type.ReadableName()} {(hasButton ? "displayed" : "not displayed")} in freemod overlay", + () => songSelect.ChildrenOfType().Single().ChildrenOfType().All(b => b.Mod.GetType() != type)); + } + private class TestMultiplayerMatchSongSelect : MultiplayerMatchSongSelect { + public new Bindable> Mods => base.Mods; + + public new Bindable> FreeMods => base.FreeMods; + public new BeatmapCarousel Carousel => base.Carousel; } } From e2c5dded7f4e5b4ac1a5123e70e54728f251bb2a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Feb 2021 14:14:36 +0900 Subject: [PATCH 6714/6909] 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 1513f6444d..183ac61c90 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 9c3d0c2020..37d730bf42 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 99ab88a064..ca11952cc8 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -91,7 +91,7 @@ - + From 63dd55c92c9f725926f89dfa14b4ba84d65760a3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Feb 2021 14:18:52 +0900 Subject: [PATCH 6715/6909] Add missing methods from updated audio component interface implementation --- .../TestSceneDrawableRulesetDependencies.cs | 4 ++++ .../Rulesets/UI/DrawableRulesetDependencies.cs | 4 ++++ osu.Game/Skinning/PoolableSkinnableSample.cs | 4 ++++ osu.Game/Skinning/SkinnableSound.cs | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+) diff --git a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs index 4aebed0d31..f421a30283 100644 --- a/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs +++ b/osu.Game.Tests/Rulesets/TestSceneDrawableRulesetDependencies.cs @@ -118,6 +118,10 @@ namespace osu.Game.Tests.Rulesets public BindableNumber Frequency => throw new NotImplementedException(); public BindableNumber Tempo => throw new NotImplementedException(); + public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException(); + + public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException(); + public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => throw new NotImplementedException(); public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) => throw new NotImplementedException(); diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index bbaca7c80f..b31884d246 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -134,6 +134,10 @@ namespace osu.Game.Rulesets.UI public IBindable AggregateTempo => throw new NotSupportedException(); + public void BindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException(); + + public void UnbindAdjustments(IAggregateAudioAdjustment component) => throw new NotImplementedException(); + public int PlaybackConcurrency { get => throw new NotSupportedException(); diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index abff57091b..5a0cf94d6a 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -165,6 +165,10 @@ namespace osu.Game.Skinning public BindableNumber Tempo => sampleContainer.Tempo; + public void BindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.BindAdjustments(component); + + public void UnbindAdjustments(IAggregateAudioAdjustment component) => sampleContainer.UnbindAdjustments(component); + public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => sampleContainer.AddAdjustment(type, adjustBindable); public void RemoveAdjustment(AdjustableProperty type, IBindable adjustBindable) => sampleContainer.RemoveAdjustment(type, adjustBindable); diff --git a/osu.Game/Skinning/SkinnableSound.cs b/osu.Game/Skinning/SkinnableSound.cs index d3dfcb1dc0..57e20a8d31 100644 --- a/osu.Game/Skinning/SkinnableSound.cs +++ b/osu.Game/Skinning/SkinnableSound.cs @@ -176,6 +176,16 @@ namespace osu.Game.Skinning public BindableNumber Tempo => samplesContainer.Tempo; + public void BindAdjustments(IAggregateAudioAdjustment component) + { + samplesContainer.BindAdjustments(component); + } + + public void UnbindAdjustments(IAggregateAudioAdjustment component) + { + samplesContainer.UnbindAdjustments(component); + } + public void AddAdjustment(AdjustableProperty type, IBindable adjustBindable) => samplesContainer.AddAdjustment(type, adjustBindable); @@ -192,6 +202,14 @@ namespace osu.Game.Skinning public bool IsPlayed => samplesContainer.Any(s => s.Played); + public IBindable AggregateVolume => samplesContainer.AggregateVolume; + + public IBindable AggregateBalance => samplesContainer.AggregateBalance; + + public IBindable AggregateFrequency => samplesContainer.AggregateFrequency; + + public IBindable AggregateTempo => samplesContainer.AggregateTempo; + #endregion } } From 541237ef16c62d259e4e84fa13943c32a7a54be0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Feb 2021 14:48:04 +0900 Subject: [PATCH 6716/6909] Use a shorter test beatmap for tests which need to run to completion --- .../Beatmaps/IO/ImportBeatmapTest.cs | 15 +++++++++++ ...241526 Soleily - Renatus_virtual_quick.osz | Bin 0 -> 89215 bytes osu.Game.Tests/Resources/TestResources.cs | 25 +++++++++++++++++- .../Navigation/TestSceneScreenNavigation.cs | 2 +- 4 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual_quick.osz diff --git a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs index c32e359de6..0c35e9471d 100644 --- a/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs +++ b/osu.Game.Tests/Beatmaps/IO/ImportBeatmapTest.cs @@ -852,6 +852,21 @@ namespace osu.Game.Tests.Beatmaps.IO } } + public static async Task LoadQuickOszIntoOsu(OsuGameBase osu) + { + var temp = TestResources.GetQuickTestBeatmapForImport(); + + var manager = osu.Dependencies.Get(); + + var importedSet = await manager.Import(new ImportTask(temp)); + + ensureLoaded(osu); + + waitForOrAssert(() => !File.Exists(temp), "Temporary file still exists after standard import", 5000); + + return manager.GetAllUsableBeatmapSets().Find(beatmapSet => beatmapSet.ID == importedSet.ID); + } + public static async Task LoadOszIntoOsu(OsuGameBase osu, string path = null, bool virtualTrack = false) { var temp = path ?? TestResources.GetTestBeatmapForImport(virtualTrack); diff --git a/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual_quick.osz b/osu.Game.Tests/Resources/Archives/241526 Soleily - Renatus_virtual_quick.osz new file mode 100644 index 0000000000000000000000000000000000000000..e9f5fb03282e189ffc23d77a0db40b3fa2488850 GIT binary patch literal 89215 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 + /// Retrieve a path to a copy of a shortened (~10 second) beatmap archive with a virtual track. + /// + /// + /// This is intended for use in tests which need to run to completion as soon as possible and don't need to test a full length beatmap. + /// A path to a copy of a beatmap archive (osz). Should be deleted after use. + public static string GetQuickTestBeatmapForImport() + { + var tempPath = Path.GetTempFileName() + ".osz"; + + using (var stream = GetTestBeatmapStream(true, true)) + using (var newFile = File.Create(tempPath)) + stream.CopyTo(newFile); + + Assert.IsTrue(File.Exists(tempPath)); + return tempPath; + } + + /// + /// Retrieve a path to a copy of a full-fledged beatmap archive. + /// + /// Whether the audio track should be virtual. + /// A path to a copy of a beatmap archive (osz). Should be deleted after use. public static string GetTestBeatmapForImport(bool virtualTrack = false) { var tempPath = Path.GetTempFileName() + ".osz"; diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index d8380b2dd3..e5959a3edf 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -53,7 +53,7 @@ namespace osu.Game.Tests.Visual.Navigation PushAndConfirm(() => new TestSongSelect()); - AddStep("import beatmap", () => ImportBeatmapTest.LoadOszIntoOsu(Game, virtualTrack: true).Wait()); + AddStep("import beatmap", () => ImportBeatmapTest.LoadQuickOszIntoOsu(Game).Wait()); AddUntilStep("wait for selected", () => !Game.Beatmap.IsDefault); From cdbf8de29db80994e903c1e92837d6208e78312f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Feb 2021 14:53:32 +0900 Subject: [PATCH 6717/6909] Update other tests which can benefit from using a shorter beatmap --- .../Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs | 2 +- osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs | 2 +- .../Visual/Collections/TestSceneManageCollectionsDialog.cs | 2 +- .../Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs | 2 +- osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs | 2 +- osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs | 2 +- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs index 3ffb512b7f..8c30802ce3 100644 --- a/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs +++ b/osu.Game.Tests/Online/TestSceneOnlinePlayBeatmapAvailabilityTracker.cs @@ -52,7 +52,7 @@ namespace osu.Game.Tests.Online { beatmaps.AllowImport = new TaskCompletionSource(); - testBeatmapFile = TestResources.GetTestBeatmapForImport(); + testBeatmapFile = TestResources.GetQuickTestBeatmapForImport(); testBeatmapInfo = getTestBeatmapInfo(testBeatmapFile); testBeatmapSet = testBeatmapInfo.BeatmapSet; diff --git a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs index 7ade7725d9..ba4d12b19f 100644 --- a/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs +++ b/osu.Game.Tests/Visual/Background/TestSceneUserDimBackgrounds.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Background Dependencies.Cache(manager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); Dependencies.Cache(new OsuConfigManager(LocalStorage)); - manager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + manager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); Beatmap.SetDefault(); } diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index fef1605f0c..1655adf811 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.Collections Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); - beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); base.Content.AddRange(new Drawable[] { diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs index 3b3b1bee86..b44e5b1e5b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerReadyButton.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmaps = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, audio, host, Beatmap.Default)); - beatmaps.Import(TestResources.GetTestBeatmapForImport(true)).Wait(); + beatmaps.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); Add(beatmapTracker = new OnlinePlayBeatmapAvailablilityTracker { diff --git a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs index 63bda08c88..0c199bfb62 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneDirectDownloadButton.cs @@ -42,7 +42,7 @@ namespace osu.Game.Tests.Visual.Online ensureSoleilyRemoved(); createButtonWithBeatmap(createSoleily()); AddAssert("button state not downloaded", () => downloadButton.DownloadState == DownloadState.NotDownloaded); - AddStep("import soleily", () => beatmaps.Import(TestResources.GetTestBeatmapForImport())); + AddStep("import soleily", () => beatmaps.Import(TestResources.GetQuickTestBeatmapForImport())); AddUntilStep("wait for beatmap import", () => beatmaps.GetAllUsableBeatmapSets().Any(b => b.OnlineBeatmapSetID == 241526)); createButtonWithBeatmap(createSoleily()); AddAssert("button state downloaded", () => downloadButton.DownloadState == DownloadState.LocallyAvailable); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs index 5d0fb248df..c13bdf0955 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneFilterControl.cs @@ -38,7 +38,7 @@ namespace osu.Game.Tests.Visual.SongSelect Dependencies.Cache(rulesets = new RulesetStore(ContextFactory)); Dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesets, null, Audio, host, Beatmap.Default)); - beatmapManager.Import(TestResources.GetTestBeatmapForImport()).Wait(); + beatmapManager.Import(TestResources.GetQuickTestBeatmapForImport()).Wait(); base.Content.AddRange(new Drawable[] { diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 81862448a8..d615f1f440 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -84,7 +84,7 @@ namespace osu.Game.Tests.Visual.UserInterface dependencies.Cache(beatmapManager = new BeatmapManager(LocalStorage, ContextFactory, rulesetStore, null, dependencies.Get(), dependencies.Get(), Beatmap.Default)); dependencies.Cache(scoreManager = new ScoreManager(rulesetStore, () => beatmapManager, LocalStorage, null, ContextFactory)); - beatmap = beatmapManager.Import(new ImportTask(TestResources.GetTestBeatmapForImport())).Result.Beatmaps[0]; + beatmap = beatmapManager.Import(new ImportTask(TestResources.GetQuickTestBeatmapForImport())).Result.Beatmaps[0]; for (int i = 0; i < 50; i++) { From fde026d44342534f7d06ddc0873e18f3f24e7070 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Feb 2021 14:54:48 +0900 Subject: [PATCH 6718/6909] Remove redundant interface specification --- osu.Game/Skinning/PoolableSkinnableSample.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Skinning/PoolableSkinnableSample.cs b/osu.Game/Skinning/PoolableSkinnableSample.cs index 5a0cf94d6a..9103a6a960 100644 --- a/osu.Game/Skinning/PoolableSkinnableSample.cs +++ b/osu.Game/Skinning/PoolableSkinnableSample.cs @@ -17,7 +17,7 @@ namespace osu.Game.Skinning /// /// A sample corresponding to an that supports being pooled and responding to skin changes. /// - public class PoolableSkinnableSample : SkinReloadableDrawable, IAggregateAudioAdjustment, IAdjustableAudioComponent + public class PoolableSkinnableSample : SkinReloadableDrawable, IAdjustableAudioComponent { /// /// The currently-loaded . From adf2dc36c9112200699ac8680b81a32bda9b937f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Feb 2021 15:43:58 +0900 Subject: [PATCH 6719/6909] Fix PlaylistResults tests performing delays in real-time when headless --- .../TestScenePlaylistsResultsScreen.cs | 87 +++++++------------ 1 file changed, 32 insertions(+), 55 deletions(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index cdcded8f61..e34da1ef0c 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.Playlists AddStep("bind user score info handler", () => { userScore = new TestScoreInfo(new OsuRuleset().RulesetInfo) { OnlineScoreID = currentScoreId++ }; - bindHandler(3000, userScore); + bindHandler(true, userScore); }); createResults(() => userScore); @@ -89,7 +89,7 @@ namespace osu.Game.Tests.Visual.Playlists [Test] public void TestShowNullUserScoreWithDelay() { - AddStep("bind delayed handler", () => bindHandler(3000)); + AddStep("bind delayed handler", () => bindHandler(true)); createResults(); waitForDisplay(); @@ -103,7 +103,7 @@ namespace osu.Game.Tests.Visual.Playlists createResults(); waitForDisplay(); - AddStep("bind delayed handler", () => bindHandler(3000)); + AddStep("bind delayed handler", () => bindHandler(true)); for (int i = 0; i < 2; i++) { @@ -134,7 +134,7 @@ namespace osu.Game.Tests.Visual.Playlists createResults(() => userScore); waitForDisplay(); - AddStep("bind delayed handler", () => bindHandler(3000)); + AddStep("bind delayed handler", () => bindHandler(true)); for (int i = 0; i < 2; i++) { @@ -169,70 +169,47 @@ namespace osu.Game.Tests.Visual.Playlists AddWaitStep("wait for display", 5); } - private void bindHandler(double delay = 0, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request => + private void bindHandler(bool delayed = false, ScoreInfo userScore = null, bool failRequests = false) => ((DummyAPIAccess)API).HandleRequest = request => { requestComplete = false; - if (failRequests) - { - triggerFail(request, delay); - return; - } + double delay = delayed ? 3000 : 0; - switch (request) + Scheduler.AddDelayed(() => { - case ShowPlaylistUserScoreRequest s: - if (userScore == null) - triggerFail(s, delay); - else - triggerSuccess(s, createUserResponse(userScore), delay); - break; + if (failRequests) + { + triggerFail(request); + return; + } - case IndexPlaylistScoresRequest i: - triggerSuccess(i, createIndexResponse(i), delay); - break; - } + switch (request) + { + case ShowPlaylistUserScoreRequest s: + if (userScore == null) + triggerFail(s); + else + triggerSuccess(s, createUserResponse(userScore)); + break; + + case IndexPlaylistScoresRequest i: + triggerSuccess(i, createIndexResponse(i)); + break; + } + }, delay); }; - private void triggerSuccess(APIRequest req, T result, double delay) + private void triggerSuccess(APIRequest req, T result) where T : class { - if (delay == 0) - success(); - else - { - Task.Run(async () => - { - await Task.Delay(TimeSpan.FromMilliseconds(delay)); - Schedule(success); - }); - } - - void success() - { - requestComplete = true; - req.TriggerSuccess(result); - } + requestComplete = true; + req.TriggerSuccess(result); } - private void triggerFail(APIRequest req, double delay) + private void triggerFail(APIRequest req) { - if (delay == 0) - fail(); - else - { - Task.Run(async () => - { - await Task.Delay(TimeSpan.FromMilliseconds(delay)); - Schedule(fail); - }); - } - - void fail() - { - requestComplete = true; - req.TriggerFailure(new WebException("Failed.")); - } + requestComplete = true; + req.TriggerFailure(new WebException("Failed.")); } private MultiplayerScore createUserResponse([NotNull] ScoreInfo userScore) From ccb83ef3a374f173b18473665c06c5002d68aecb Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Feb 2021 15:47:47 +0900 Subject: [PATCH 6720/6909] Fix checkbox not being updated --- osu.Game/Overlays/Mods/ModSection.cs | 20 +++++++++---------- osu.Game/Overlays/Mods/ModSelectOverlay.cs | 8 ++++++++ .../OnlinePlay/FreeModSelectOverlay.cs | 14 ++++++++++++- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Mods/ModSection.cs b/osu.Game/Overlays/Mods/ModSection.cs index c3e56abd05..aa8a5efd39 100644 --- a/osu.Game/Overlays/Mods/ModSection.cs +++ b/osu.Game/Overlays/Mods/ModSection.cs @@ -23,13 +23,15 @@ namespace osu.Game.Overlays.Mods public FillFlowContainer ButtonsContainer { get; } + protected IReadOnlyList Buttons { get; private set; } = Array.Empty(); + public Action Action; public Key[] ToggleKeys; public readonly ModType ModType; - public IEnumerable SelectedMods => buttons.Select(b => b.SelectedMod).Where(m => m != null); + public IEnumerable SelectedMods => Buttons.Select(b => b.SelectedMod).Where(m => m != null); private CancellationTokenSource modsLoadCts; @@ -77,7 +79,7 @@ namespace osu.Game.Overlays.Mods ButtonsContainer.ChildrenEnumerable = c; }, (modsLoadCts = new CancellationTokenSource()).Token); - buttons = modContainers.OfType().ToArray(); + Buttons = modContainers.OfType().ToArray(); header.FadeIn(200); this.FadeIn(200); @@ -88,8 +90,6 @@ namespace osu.Game.Overlays.Mods { } - private ModButton[] buttons = Array.Empty(); - protected override bool OnKeyDown(KeyDownEvent e) { if (e.ControlPressed) return false; @@ -97,8 +97,8 @@ namespace osu.Game.Overlays.Mods if (ToggleKeys != null) { var index = Array.IndexOf(ToggleKeys, e.Key); - if (index > -1 && index < buttons.Length) - buttons[index].SelectNext(e.ShiftPressed ? -1 : 1); + if (index > -1 && index < Buttons.Count) + Buttons[index].SelectNext(e.ShiftPressed ? -1 : 1); } return base.OnKeyDown(e); @@ -141,7 +141,7 @@ namespace osu.Game.Overlays.Mods { pendingSelectionOperations.Clear(); - foreach (var button in buttons.Where(b => !b.Selected)) + foreach (var button in Buttons.Where(b => !b.Selected)) pendingSelectionOperations.Enqueue(() => button.SelectAt(0)); } @@ -151,7 +151,7 @@ namespace osu.Game.Overlays.Mods public void DeselectAll() { pendingSelectionOperations.Clear(); - DeselectTypes(buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); + DeselectTypes(Buttons.Select(b => b.SelectedMod?.GetType()).Where(t => t != null)); } /// @@ -161,7 +161,7 @@ namespace osu.Game.Overlays.Mods /// Whether the deselection should happen immediately. Should only be used when required to ensure correct selection flow. public void DeselectTypes(IEnumerable modTypes, bool immediate = false) { - foreach (var button in buttons) + foreach (var button in Buttons) { if (button.SelectedMod == null) continue; @@ -184,7 +184,7 @@ namespace osu.Game.Overlays.Mods /// The new list of selected mods to select. public void UpdateSelectedButtons(IReadOnlyList newSelectedMods) { - foreach (var button in buttons) + foreach (var button in Buttons) updateButtonSelection(button, newSelectedMods); } diff --git a/osu.Game/Overlays/Mods/ModSelectOverlay.cs b/osu.Game/Overlays/Mods/ModSelectOverlay.cs index eef91deb4c..26b8632d7f 100644 --- a/osu.Game/Overlays/Mods/ModSelectOverlay.cs +++ b/osu.Game/Overlays/Mods/ModSelectOverlay.cs @@ -456,6 +456,7 @@ namespace osu.Game.Overlays.Mods } updateSelectedButtons(); + OnAvailableModsChanged(); } /// @@ -533,6 +534,13 @@ namespace osu.Game.Overlays.Mods private void playSelectedSound() => sampleOn?.Play(); private void playDeselectedSound() => sampleOff?.Play(); + /// + /// Invoked after has changed. + /// + protected virtual void OnAvailableModsChanged() + { + } + /// /// Invoked when a new has been selected. /// diff --git a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs index ab7be13479..66262e7dc4 100644 --- a/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/FreeModSelectOverlay.cs @@ -75,6 +75,14 @@ namespace osu.Game.Screens.OnlinePlay section.DeselectAll(); } + protected override void OnAvailableModsChanged() + { + base.OnAvailableModsChanged(); + + foreach (var section in ModSectionsContainer.Children) + ((FreeModSection)section).UpdateCheckboxState(); + } + protected override ModSection CreateModSection(ModType type) => new FreeModSection(type); private class FreeModSection : ModSection @@ -108,10 +116,14 @@ namespace osu.Game.Screens.OnlinePlay protected override void ModButtonStateChanged(Mod mod) { base.ModButtonStateChanged(mod); + UpdateCheckboxState(); + } + public void UpdateCheckboxState() + { if (!SelectionAnimationRunning) { - var validButtons = ButtonsContainer.OfType().Where(b => b.Mod.HasImplementation); + var validButtons = Buttons.Where(b => b.Mod.HasImplementation); checkbox.Current.Value = validButtons.All(b => b.Selected); } } From d985b8ab2aafd86c9f4d24fdcd39634de8a0b10c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 22 Feb 2021 17:14:39 +0900 Subject: [PATCH 6721/6909] Increase beatmapset download timeout --- osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs index 707c59436d..e8871bef05 100644 --- a/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.cs +++ b/osu.Game/Online/API/Requests/DownloadBeatmapSetRequest.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.IO.Network; using osu.Game.Beatmaps; namespace osu.Game.Online.API.Requests @@ -15,6 +16,13 @@ namespace osu.Game.Online.API.Requests this.noVideo = noVideo; } + protected override WebRequest CreateWebRequest() + { + var req = base.CreateWebRequest(); + req.Timeout = 60000; + return req; + } + protected override string Target => $@"beatmapsets/{Model.OnlineBeatmapSetID}/download{(noVideo ? "?noVideo=1" : "")}"; } } From 1fd76ea3fb9db1d7e80e92fbd9e9cdb40353683c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Feb 2021 17:14:00 +0900 Subject: [PATCH 6722/6909] Apply changes to UI components overriding functions with changing signatures --- osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs | 2 +- .../Ranking/TestSceneExpandedPanelMiddleContent.cs | 2 +- .../Visual/Settings/TestSceneKeyBindingPanel.cs | 4 ++-- .../Visual/SongSelect/TestSceneBeatmapInfoWedge.cs | 2 +- .../Visual/UserInterface/TestSceneDeleteLocalScore.cs | 2 +- osu.Game/Collections/CollectionFilterDropdown.cs | 5 +++-- osu.Game/Graphics/Sprites/GlowingSpriteText.cs | 3 ++- osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs | 3 ++- osu.Game/Graphics/UserInterface/OsuButton.cs | 5 +++-- osu.Game/Graphics/UserInterface/OsuDropdown.cs | 5 +++-- osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs | 3 ++- osu.Game/Graphics/UserInterface/ShowMoreButton.cs | 3 ++- osu.Game/Graphics/UserInterface/TriangleButton.cs | 2 +- .../Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs | 1 - .../Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs | 1 - osu.Game/Overlays/BeatmapSet/BasicStats.cs | 3 ++- .../BeatmapSet/Scores/TopScoreStatisticsSection.cs | 1 - osu.Game/Overlays/Chat/Selection/ChannelSection.cs | 9 ++------- .../Overlays/Chat/Selection/ChannelSelectionOverlay.cs | 6 +----- .../Overlays/Comments/Buttons/CommentRepliesButton.cs | 3 ++- osu.Game/Overlays/KeyBinding/KeyBindingRow.cs | 2 +- osu.Game/Overlays/Notifications/NotificationSection.cs | 7 ++++--- osu.Game/Overlays/NowPlayingOverlay.cs | 1 - osu.Game/Overlays/OverlaySortTabControl.cs | 3 ++- .../Sections/Historical/DrawableMostPlayedBeatmap.cs | 1 - .../Profile/Sections/Ranks/DrawableProfileScore.cs | 1 - .../Settings/Sections/Audio/AudioDevicesSettings.cs | 3 ++- .../Settings/Sections/Graphics/LayoutSettings.cs | 3 ++- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 2 +- osu.Game/Overlays/Settings/SettingsCheckbox.cs | 8 +++++--- osu.Game/Overlays/Settings/SettingsItem.cs | 5 +++-- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 7 ++++--- osu.Game/Screens/Menu/SongTicker.cs | 1 - osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs | 1 - .../Playlists/PlaylistsMatchSettingsOverlay.cs | 3 ++- osu.Game/Screens/Play/BeatmapMetadataDisplay.cs | 1 - .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 1 - osu.Game/Screens/Select/Carousel/SetPanelContent.cs | 1 - osu.Game/Screens/Select/Details/AdvancedStats.cs | 3 ++- osu.Game/Screens/Select/FooterButton.cs | 5 +++-- osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs | 5 +++-- osu.Game/Skinning/SkinnableSpriteText.cs | 5 +++-- 42 files changed, 68 insertions(+), 66 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs index bea6186501..43d8d1e27f 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/NumberPiece.cs @@ -18,7 +18,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default public string Text { - get => number.Text; + get => number.Text.ToString(); set => number.Text = value; } diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index 7be44a62de..f9fe42131f 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking })); AddAssert("mapped by text not present", () => - this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text, "mapped", "by"))); + this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by"))); } private void showPanel(ScoreInfo score) => Child = new ExpandedPanelMiddleContentContainer(score); diff --git a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs index 8330b9b360..f495e0fb23 100644 --- a/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs +++ b/osu.Game.Tests/Visual/Settings/TestSceneKeyBindingPanel.cs @@ -78,7 +78,7 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("first binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().First().Text.Text)); + AddAssert("first binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().First().Text.Text.ToString())); AddStep("click second binding", () => { @@ -90,7 +90,7 @@ namespace osu.Game.Tests.Visual.Settings clickClearButton(); - AddAssert("second binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().ElementAt(1).Text.Text)); + AddAssert("second binding cleared", () => string.IsNullOrEmpty(multiBindingRow.ChildrenOfType().ElementAt(1).Text.Text.ToString())); void clickClearButton() { diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index 0b2c0ce63b..fff4a9ba61 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -119,7 +119,7 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestNullBeatmap() { selectBeatmap(null); - AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text)); + AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text.ToString())); AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Title); AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Artist); AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any()); diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs index 81862448a8..1516a7d621 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneDeleteLocalScore.cs @@ -145,7 +145,7 @@ namespace osu.Game.Tests.Visual.UserInterface AddStep("click delete option", () => { - InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType().First(i => i.Item.Text.Value.ToLowerInvariant() == "delete")); + InputManager.MoveMouseTo(contextMenuContainer.ChildrenOfType().First(i => i.Item.Text.Value.ToString().ToLowerInvariant() == "delete")); InputManager.Click(MouseButton.Left); }); diff --git a/osu.Game/Collections/CollectionFilterDropdown.cs b/osu.Game/Collections/CollectionFilterDropdown.cs index bb743d4ccc..1eceb56e33 100644 --- a/osu.Game/Collections/CollectionFilterDropdown.cs +++ b/osu.Game/Collections/CollectionFilterDropdown.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -121,7 +122,7 @@ namespace osu.Game.Collections Current.TriggerChange(); } - protected override string GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value; + protected override LocalisableString GenerateItemText(CollectionFilterMenuItem item) => item.CollectionName.Value; protected sealed override DropdownHeader CreateHeader() => CreateCollectionHeader().With(d => { @@ -139,7 +140,7 @@ namespace osu.Game.Collections public readonly Bindable SelectedItem = new Bindable(); private readonly Bindable collectionName = new Bindable(); - protected override string Label + protected override LocalisableString Label { get => base.Label; set { } // See updateText(). diff --git a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs index 85df2d167f..fb273d7293 100644 --- a/osu.Game/Graphics/Sprites/GlowingSpriteText.cs +++ b/osu.Game/Graphics/Sprites/GlowingSpriteText.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Colour; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osuTK; namespace osu.Game.Graphics.Sprites @@ -14,7 +15,7 @@ namespace osu.Game.Graphics.Sprites { private readonly OsuSpriteText spriteText, blurredText; - public string Text + public LocalisableString Text { get => spriteText.Text; set => blurredText.Text = spriteText.Text = value; diff --git a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs index b499b26f38..8df2c1c2fd 100644 --- a/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs +++ b/osu.Game/Graphics/UserInterface/DrawableOsuMenuItem.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osuTK.Graphics; @@ -105,7 +106,7 @@ namespace osu.Game.Graphics.UserInterface protected class TextContainer : Container, IHasText { - public string Text + public LocalisableString Text { get => NormalText.Text; set diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index 9cf8f02024..d2114134cf 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -11,6 +11,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osuTK.Graphics; @@ -21,9 +22,9 @@ namespace osu.Game.Graphics.UserInterface /// public class OsuButton : Button { - public string Text + public LocalisableString Text { - get => SpriteText?.Text; + get => SpriteText.Text; set { if (SpriteText != null) diff --git a/osu.Game/Graphics/UserInterface/OsuDropdown.cs b/osu.Game/Graphics/UserInterface/OsuDropdown.cs index cc76c12975..15fb00ccb0 100644 --- a/osu.Game/Graphics/UserInterface/OsuDropdown.cs +++ b/osu.Game/Graphics/UserInterface/OsuDropdown.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Localisation; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; @@ -168,7 +169,7 @@ namespace osu.Game.Graphics.UserInterface protected new class Content : FillFlowContainer, IHasText { - public string Text + public LocalisableString Text { get => Label.Text; set => Label.Text = value; @@ -215,7 +216,7 @@ namespace osu.Game.Graphics.UserInterface { protected readonly SpriteText Text; - protected override string Label + protected override LocalisableString Label { get => Text.Text; set => Text.Text = value; diff --git a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs index bdc95ee048..b66a4a58ce 100644 --- a/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs +++ b/osu.Game/Graphics/UserInterface/OsuTabControlCheckbox.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics.Sprites; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -35,7 +36,7 @@ namespace osu.Game.Graphics.UserInterface } } - public string Text + public LocalisableString Text { get => text.Text; set => text.Text = value; diff --git a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs index 924c7913f3..615895074c 100644 --- a/osu.Game/Graphics/UserInterface/ShowMoreButton.cs +++ b/osu.Game/Graphics/UserInterface/ShowMoreButton.cs @@ -11,6 +11,7 @@ using osu.Game.Graphics.Sprites; using osu.Game.Overlays; using osuTK; using System.Collections.Generic; +using osu.Framework.Localisation; namespace osu.Game.Graphics.UserInterface { @@ -18,7 +19,7 @@ namespace osu.Game.Graphics.UserInterface { private const int duration = 200; - public string Text + public LocalisableString Text { get => text.Text; set => text.Text = value; diff --git a/osu.Game/Graphics/UserInterface/TriangleButton.cs b/osu.Game/Graphics/UserInterface/TriangleButton.cs index 5baf794227..003a81f562 100644 --- a/osu.Game/Graphics/UserInterface/TriangleButton.cs +++ b/osu.Game/Graphics/UserInterface/TriangleButton.cs @@ -27,7 +27,7 @@ namespace osu.Game.Graphics.UserInterface }); } - public virtual IEnumerable FilterTerms => new[] { Text }; + public virtual IEnumerable FilterTerms => new[] { Text.ToString() }; public bool MatchingFilter { diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs index c1d366bb82..97e7ce83a5 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; diff --git a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs index 76a30d1c11..4a887ed571 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs @@ -8,7 +8,6 @@ using osu.Framework.Graphics.Colour; 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; diff --git a/osu.Game/Overlays/BeatmapSet/BasicStats.cs b/osu.Game/Overlays/BeatmapSet/BasicStats.cs index a2464bef09..cf74c0d4d3 100644 --- a/osu.Game/Overlays/BeatmapSet/BasicStats.cs +++ b/osu.Game/Overlays/BeatmapSet/BasicStats.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Cursor; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -96,7 +97,7 @@ namespace osu.Game.Overlays.BeatmapSet public string TooltipText { get; } - public string Value + public LocalisableString Value { get => value.Text; set => this.value.Text = value; diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index 93744dd6a3..c281d7b432 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs index eac48ca5cb..e18302770c 100644 --- a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs @@ -4,12 +4,12 @@ using System; using System.Collections.Generic; using System.Linq; -using osuTK; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Online.Chat; +using osuTK; namespace osu.Game.Overlays.Chat.Selection { @@ -29,12 +29,6 @@ namespace osu.Game.Overlays.Chat.Selection public bool FilteringActive { get; set; } - public string Header - { - get => header.Text; - set => header.Text = value.ToUpperInvariant(); - } - public IEnumerable Channels { set => ChannelFlow.ChildrenEnumerable = value.Select(c => new ChannelListItem(c)); @@ -50,6 +44,7 @@ namespace osu.Game.Overlays.Chat.Selection header = new OsuSpriteText { Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), + Text = "All Channels".ToUpperInvariant() }, ChannelFlow = new FillFlowContainer { diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs index be9ecc6746..231d7ca63c 100644 --- a/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSelectionOverlay.cs @@ -131,11 +131,7 @@ namespace osu.Game.Overlays.Chat.Selection { sectionsFlow.ChildrenEnumerable = new[] { - new ChannelSection - { - Header = "All Channels", - Channels = channels, - }, + new ChannelSection { Channels = channels, }, }; foreach (ChannelSection s in sectionsFlow.Children) diff --git a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs index 57bf2af4d2..2f7f16dd6f 100644 --- a/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs +++ b/osu.Game/Overlays/Comments/Buttons/CommentRepliesButton.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.UserInterface; @@ -16,7 +17,7 @@ namespace osu.Game.Overlays.Comments.Buttons { public abstract class CommentRepliesButton : CompositeDrawable { - protected string Text + protected LocalisableString Text { get => text.Text; set => text.Text = value; diff --git a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs index b808d49fa2..300fce962a 100644 --- a/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs +++ b/osu.Game/Overlays/KeyBinding/KeyBindingRow.cs @@ -51,7 +51,7 @@ namespace osu.Game.Overlays.KeyBinding private FillFlowContainer cancelAndClearButtons; private FillFlowContainer buttons; - public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend((string)text.Text); + public IEnumerable FilterTerms => bindings.Select(b => b.KeyCombination.ReadableString()).Prepend(text.Text.ToString()); public KeyBindingRow(object action, IEnumerable bindings) { diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index 38ba712254..bc41311a6d 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; @@ -37,7 +38,7 @@ namespace osu.Game.Overlays.Notifications public NotificationSection(string title, string clearButtonText) { - this.clearButtonText = clearButtonText; + this.clearButtonText = clearButtonText.ToUpperInvariant(); titleText = title; } @@ -138,10 +139,10 @@ namespace osu.Game.Overlays.Notifications }; } - public string Text + public LocalisableString Text { get => text.Text; - set => text.Text = value.ToUpperInvariant(); + set => text.Text = value; } } diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 2866d2ad6d..74317a143c 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -13,7 +13,6 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; diff --git a/osu.Game/Overlays/OverlaySortTabControl.cs b/osu.Game/Overlays/OverlaySortTabControl.cs index b2212336ef..0ebabd424f 100644 --- a/osu.Game/Overlays/OverlaySortTabControl.cs +++ b/osu.Game/Overlays/OverlaySortTabControl.cs @@ -17,6 +17,7 @@ using osu.Game.Overlays.Comments; using JetBrains.Annotations; using System; using osu.Framework.Extensions; +using osu.Framework.Localisation; namespace osu.Game.Overlays { @@ -30,7 +31,7 @@ namespace osu.Game.Overlays set => current.Current = value; } - public string Title + public LocalisableString Title { get => text.Text; set => text.Text = value; diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index 5b7c5efbe2..e485802095 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -5,7 +5,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 2c20dcc0ef..859637485f 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -7,7 +7,6 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs index bed74542c9..b31e7dc45b 100644 --- a/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Audio/AudioDevicesSettings.cs @@ -6,6 +6,7 @@ using osu.Framework.Audio; using osu.Framework.Graphics; using System.Collections.Generic; using System.Linq; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings.Sections.Audio @@ -76,7 +77,7 @@ namespace osu.Game.Overlays.Settings.Sections.Audio private class AudioDeviceDropdownControl : DropdownControl { - protected override string GenerateItemText(string item) + protected override LocalisableString GenerateItemText(string item) => string.IsNullOrEmpty(item) ? "Default" : base.GenerateItemText(item); } } diff --git a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs index 7acbf038d8..4d5c2e06eb 100644 --- a/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Graphics/LayoutSettings.cs @@ -11,6 +11,7 @@ using osu.Framework.Extensions.IEnumerableExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Framework.Platform; using osu.Game.Configuration; using osu.Game.Graphics.Containers; @@ -234,7 +235,7 @@ namespace osu.Game.Overlays.Settings.Sections.Graphics private class ResolutionDropdownControl : DropdownControl { - protected override string GenerateItemText(Size item) + protected override LocalisableString GenerateItemText(Size item) { if (item == new Size(9999, 9999)) return "Default"; diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 7c8309fd56..75068bd611 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -178,7 +178,7 @@ namespace osu.Game.Overlays.Settings.Sections private class SkinDropdownControl : DropdownControl { - protected override string GenerateItemText(SkinInfo item) => item.ToString(); + protected override LocalisableString GenerateItemText(SkinInfo item) => item.ToString(); } } diff --git a/osu.Game/Overlays/Settings/SettingsCheckbox.cs b/osu.Game/Overlays/Settings/SettingsCheckbox.cs index 437b2e45b3..8b7ac80a5b 100644 --- a/osu.Game/Overlays/Settings/SettingsCheckbox.cs +++ b/osu.Game/Overlays/Settings/SettingsCheckbox.cs @@ -2,20 +2,22 @@ // See the LICENCE file in the repository root for full licence text. using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Graphics.UserInterface; namespace osu.Game.Overlays.Settings { public class SettingsCheckbox : SettingsItem { - private string labelText; + private LocalisableString labelText; protected override Drawable CreateControl() => new OsuCheckbox(); - public override string LabelText + public override LocalisableString LabelText { get => labelText; - set => ((OsuCheckbox)Control).LabelText = labelText = value; + // checkbox doesn't properly support localisation yet. + set => ((OsuCheckbox)Control).LabelText = (labelText = value).ToString(); } } } diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index af225889da..aafd7463a6 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -15,6 +15,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.UserInterface; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; @@ -39,7 +40,7 @@ namespace osu.Game.Overlays.Settings public string TooltipText { get; set; } - public virtual string LabelText + public virtual LocalisableString LabelText { get => labelText?.Text ?? string.Empty; set @@ -69,7 +70,7 @@ namespace osu.Game.Overlays.Settings set => controlWithCurrent.Current = value; } - public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText } : new List(Keywords) { LabelText }.ToArray(); + public virtual IEnumerable FilterTerms => Keywords == null ? new[] { LabelText.ToString() } : new List(Keywords) { LabelText.ToString() }.ToArray(); public IEnumerable Keywords { get; set; } diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 83f2bdf6cb..7790a21e0a 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -12,6 +12,7 @@ using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Bindings; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Backgrounds; using osu.Game.Graphics.Containers; @@ -43,19 +44,19 @@ namespace osu.Game.Overlays.Toolbar Texture = textures.Get(texture), }); - public string Text + public LocalisableString Text { get => DrawableText.Text; set => DrawableText.Text = value; } - public string TooltipMain + public LocalisableString TooltipMain { get => tooltip1.Text; set => tooltip1.Text = value; } - public string TooltipSub + public LocalisableString TooltipSub { get => tooltip2.Text; set => tooltip2.Text = value; diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs index c4943e77d5..fd9d9a3fac 100644 --- a/osu.Game/Screens/Menu/SongTicker.cs +++ b/osu.Game/Screens/Menu/SongTicker.cs @@ -9,7 +9,6 @@ using osuTK; using osu.Game.Graphics; using osu.Framework.Bindables; using osu.Game.Beatmaps; -using osu.Framework.Localisation; namespace osu.Game.Screens.Menu { diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index acb82360b3..b64ea37a59 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -4,7 +4,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; -using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs index 2a1efbc040..5062a296a8 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsMatchSettingsOverlay.cs @@ -10,6 +10,7 @@ using osu.Framework.Extensions.Color4Extensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -362,7 +363,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists Menu.MaxHeight = 100; } - protected override string GenerateItemText(TimeSpan item) => item.Humanize(); + protected override LocalisableString GenerateItemText(TimeSpan item) => item.Humanize(); } } } diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index eff06e26ee..bb82b00100 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -7,7 +7,6 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; -using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index ff6203bc25..85a9b06a70 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Allocation; 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; diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 4e8d27f14d..82704c24fb 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -6,7 +6,6 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; -using osu.Framework.Localisation; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index ab4f3f4796..1627d3ddfc 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using osu.Game.Rulesets.Mods; using System.Linq; using System.Threading; +using osu.Framework.Localisation; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; @@ -180,7 +181,7 @@ namespace osu.Game.Screens.Select.Details [Resolved] private OsuColour colours { get; set; } - public string Title + public LocalisableString Title { get => name.Text; set => name.Text = value; diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 35970cd960..7bdeacc91a 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -10,6 +10,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics.Sprites; using osu.Game.Graphics.Containers; @@ -21,9 +22,9 @@ namespace osu.Game.Screens.Select protected static readonly Vector2 SHEAR = new Vector2(SHEAR_WIDTH / Footer.HEIGHT, 0); - public string Text + public LocalisableString Text { - get => SpriteText?.Text; + get => SpriteText.Text; set { if (SpriteText != null) diff --git a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs index 6e2f3cc9df..845c0a914e 100644 --- a/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs +++ b/osu.Game/Screens/Select/Options/BeatmapOptionsButton.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; using osuTK; @@ -39,13 +40,13 @@ namespace osu.Game.Screens.Select.Options set => iconText.Icon = value; } - public string FirstLineText + public LocalisableString FirstLineText { get => firstLine.Text; set => firstLine.Text = value; } - public string SecondLineText + public LocalisableString SecondLineText { get => secondLine.Text; set => secondLine.Text = value; diff --git a/osu.Game/Skinning/SkinnableSpriteText.cs b/osu.Game/Skinning/SkinnableSpriteText.cs index 567dd348e1..06461127b1 100644 --- a/osu.Game/Skinning/SkinnableSpriteText.cs +++ b/osu.Game/Skinning/SkinnableSpriteText.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; namespace osu.Game.Skinning { @@ -21,9 +22,9 @@ namespace osu.Game.Skinning textDrawable.Text = Text; } - private string text; + private LocalisableString text; - public string Text + public LocalisableString Text { get => text; set From 8a97e2e28da1f7ad257c3bdf28c98f7c6bfe826f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 22 Feb 2021 17:14:13 +0900 Subject: [PATCH 6723/6909] Update LocalisedString usages to RomanisedString --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 4 ++-- .../Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs | 5 +++-- .../Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs | 5 +++-- .../BeatmapSet/Scores/TopScoreStatisticsSection.cs | 2 +- osu.Game/Overlays/Music/PlaylistItem.cs | 4 ++-- osu.Game/Overlays/NowPlayingOverlay.cs | 5 +++-- .../Sections/Historical/DrawableMostPlayedBeatmap.cs | 9 +++++---- .../Profile/Sections/Ranks/DrawableProfileScore.cs | 9 +++++---- osu.Game/Overlays/Settings/Sections/SkinSection.cs | 1 + osu.Game/Screens/Menu/SongTicker.cs | 5 +++-- osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs | 5 +++-- osu.Game/Screens/Play/BeatmapMetadataDisplay.cs | 5 +++-- .../Ranking/Expanded/ExpandedPanelMiddleContent.cs | 5 +++-- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 4 ++-- osu.Game/Screens/Select/Carousel/SetPanelContent.cs | 5 +++-- 15 files changed, 42 insertions(+), 31 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index d1197b1a61..e6d73c6e83 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -74,9 +74,9 @@ namespace osu.Game.Tournament.Components { new TournamentSpriteText { - Text = new LocalisedString(( + Text = new RomanisableString( $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", - $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}")), + $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}"), Font = OsuFont.Torus.With(weight: FontWeight.Bold), }, new FillFlowContainer diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs index 97e7ce83a5..ba4725b49a 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; @@ -83,14 +84,14 @@ namespace osu.Game.Overlays.BeatmapListing.Panels { new OsuSpriteText { - Text = new LocalisedString((SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title)), + Text = new RomanisableString(SetInfo.Metadata.Title, SetInfo.Metadata.TitleUnicode), Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true) }, } }, new OsuSpriteText { - Text = new LocalisedString((SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist)), + Text = new RomanisableString(SetInfo.Metadata.Artist, SetInfo.Metadata.ArtistUnicode), Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) }, }, diff --git a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs index 4a887ed571..624cb89d1e 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs @@ -8,6 +8,7 @@ using osu.Framework.Graphics.Colour; 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; @@ -106,14 +107,14 @@ namespace osu.Game.Overlays.BeatmapListing.Panels { new OsuSpriteText { - Text = new LocalisedString((SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title)), + Text = new RomanisableString(SetInfo.Metadata.Title, SetInfo.Metadata.TitleUnicode), Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true) }, } }, new OsuSpriteText { - Text = new LocalisedString((SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist)), + Text = new RomanisableString(SetInfo.Metadata.Artist, SetInfo.Metadata.ArtistUnicode), Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) }, } diff --git a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs index c281d7b432..5cb834b510 100644 --- a/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs +++ b/osu.Game/Overlays/BeatmapSet/Scores/TopScoreStatisticsSection.cs @@ -203,7 +203,7 @@ namespace osu.Game.Overlays.BeatmapSet.Scores this.text = text; } - public LocalisedString Text + public string Text { set => text.Text = value; } diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index 96dff39fae..dab9bc9629 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -48,8 +48,8 @@ namespace osu.Game.Overlays.Music artistColour = colours.Gray9; HandleColour = colours.Gray5; - title = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.TitleUnicode, Model.Metadata.Title))); - artist = localisation.GetLocalisedString(new LocalisedString((Model.Metadata.ArtistUnicode, Model.Metadata.Artist))); + title = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.Title, Model.Metadata.TitleUnicode)); + artist = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.Artist, Model.Metadata.ArtistUnicode)); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 74317a143c..9c17392e25 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Shapes; using osu.Framework.Graphics.Sprites; using osu.Framework.Graphics.Textures; using osu.Framework.Input.Events; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Containers; @@ -292,8 +293,8 @@ namespace osu.Game.Overlays else { BeatmapMetadata metadata = beatmap.Metadata; - title.Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)); - artist.Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)); + title.Text = new RomanisableString(metadata.Title, metadata.TitleUnicode); + artist.Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode); } }); diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index e485802095..48a0481b9e 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; using osuTK; using osu.Framework.Graphics.Cursor; +using osu.Framework.Localisation; namespace osu.Game.Overlays.Profile.Sections.Historical { @@ -128,14 +129,14 @@ namespace osu.Game.Overlays.Profile.Sections.Historical { new OsuSpriteText { - Text = new LocalisedString(( - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] ")), + Text = new RomanisableString( + $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] ", + $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] "), Font = OsuFont.GetFont(weight: FontWeight.Bold) }, new OsuSpriteText { - Text = "by " + new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)), + Text = "by " + new RomanisableString(beatmap.Metadata.Artist, beatmap.Metadata.ArtistUnicode), Font = OsuFont.GetFont(weight: FontWeight.Regular) }, }; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index 859637485f..ca9e19cd56 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -7,6 +7,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -255,16 +256,16 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = new LocalisedString(( - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} ", - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} ")), + Text = new RomanisableString( + $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} ", + $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} "), Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true) }, new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = "by " + new LocalisedString((beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist)), + Text = "by " + new RomanisableString(beatmap.Metadata.Artist, beatmap.Metadata.ArtistUnicode), Font = OsuFont.GetFont(size: 12, italics: true) }, }; diff --git a/osu.Game/Overlays/Settings/Sections/SkinSection.cs b/osu.Game/Overlays/Settings/Sections/SkinSection.cs index 75068bd611..316837d27d 100644 --- a/osu.Game/Overlays/Settings/Sections/SkinSection.cs +++ b/osu.Game/Overlays/Settings/Sections/SkinSection.cs @@ -8,6 +8,7 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Framework.Logging; using osu.Game.Configuration; using osu.Game.Graphics.UserInterface; diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs index fd9d9a3fac..2be446d71a 100644 --- a/osu.Game/Screens/Menu/SongTicker.cs +++ b/osu.Game/Screens/Menu/SongTicker.cs @@ -8,6 +8,7 @@ using osu.Game.Graphics.Sprites; using osuTK; using osu.Game.Graphics; using osu.Framework.Bindables; +using osu.Framework.Localisation; using osu.Game.Beatmaps; namespace osu.Game.Screens.Menu @@ -60,8 +61,8 @@ namespace osu.Game.Screens.Menu { var metadata = beatmap.Value.Metadata; - title.Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)); - artist.Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)); + title.Text = new RomanisableString(metadata.Title, metadata.TitleUnicode); + artist.Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode); this.FadeInFromZero(fade_duration / 2f) .Delay(4000) diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index b64ea37a59..299e3e3768 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -4,6 +4,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Graphics; using osu.Game.Graphics.Containers; using osu.Game.Graphics.Sprites; @@ -72,7 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { new OsuSpriteText { - Text = new LocalisedString((beatmap.Value.Metadata.ArtistUnicode, beatmap.Value.Metadata.Artist)), + Text = new RomanisableString(beatmap.Value.Metadata.Artist, beatmap.Value.Metadata.ArtistUnicode), Font = OsuFont.GetFont(size: TextSize), }, new OsuSpriteText @@ -82,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Components }, new OsuSpriteText { - Text = new LocalisedString((beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title)), + Text = new RomanisableString(beatmap.Value.Metadata.Title, beatmap.Value.Metadata.TitleUnicode), Font = OsuFont.GetFont(size: TextSize), } }, LinkAction.OpenBeatmap, beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap"); diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index bb82b00100..0779a9c637 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -7,6 +7,7 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; +using osu.Framework.Localisation; using osu.Game.Beatmaps; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -72,7 +73,7 @@ namespace osu.Game.Screens.Play }), new OsuSpriteText { - Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)), + Text = new RomanisableString(metadata.Title, metadata.TitleUnicode), Font = OsuFont.GetFont(size: 36, italics: true), Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, @@ -80,7 +81,7 @@ namespace osu.Game.Screens.Play }, new OsuSpriteText { - Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)), + Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode), Font = OsuFont.GetFont(size: 26, italics: true), Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 85a9b06a70..234e4f2023 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; 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; @@ -100,7 +101,7 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((metadata.TitleUnicode, metadata.Title)), + Text = new RomanisableString(metadata.Title, metadata.TitleUnicode), Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, Truncate = true, @@ -109,7 +110,7 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new LocalisedString((metadata.ArtistUnicode, metadata.Artist)), + Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, Truncate = true, diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 86cb561bc7..0c5b67026c 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -187,8 +187,8 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.Both; - titleBinding = localisation.GetLocalisedString(new LocalisedString((metadata.TitleUnicode, metadata.Title))); - artistBinding = localisation.GetLocalisedString(new LocalisedString((metadata.ArtistUnicode, metadata.Artist))); + titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.Title, metadata.TitleUnicode)); + artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.Artist, metadata.ArtistUnicode)); Children = new Drawable[] { diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 82704c24fb..0e99a4ce70 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -6,6 +6,7 @@ using System.Linq; using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Localisation; using osu.Game.Beatmaps.Drawables; using osu.Game.Graphics; using osu.Game.Graphics.Sprites; @@ -40,13 +41,13 @@ namespace osu.Game.Screens.Select.Carousel { new OsuSpriteText { - Text = new LocalisedString((beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title)), + Text = new RomanisableString(beatmapSet.Metadata.Title, beatmapSet.Metadata.TitleUnicode), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), Shadow = true, }, new OsuSpriteText { - Text = new LocalisedString((beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist)), + Text = new RomanisableString(beatmapSet.Metadata.Artist, beatmapSet.Metadata.ArtistUnicode), Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), Shadow = true, }, From 5e9040c29108cf423ddcf0d6ea418f4aa6690b46 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 22 Feb 2021 16:26:35 +0300 Subject: [PATCH 6724/6909] Use "pausing supported" conditional instead --- 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 5a86ac646a..0046eea91c 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -427,7 +427,7 @@ namespace osu.Game.Screens.Play private void updatePauseOnFocusLostState() { - if (!PauseOnFocusLost || DrawableRuleset.HasReplayLoaded.Value || breakTracker.IsBreakTime.Value) + if (!PauseOnFocusLost || pausingSupportedByCurrentState || breakTracker.IsBreakTime.Value) return; if (gameActive.Value == false) From 5493c55da7287bea7f77651ea0474587cc426626 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 22 Feb 2021 16:59:35 +0300 Subject: [PATCH 6725/6909] Fix silly mistake --- 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 0046eea91c..2ded1752da 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -427,7 +427,7 @@ namespace osu.Game.Screens.Play private void updatePauseOnFocusLostState() { - if (!PauseOnFocusLost || pausingSupportedByCurrentState || breakTracker.IsBreakTime.Value) + if (!PauseOnFocusLost || !pausingSupportedByCurrentState || breakTracker.IsBreakTime.Value) return; if (gameActive.Value == false) From f62120c66b6cbb852f96d2ede5e60b933214f08b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Mon, 22 Feb 2021 22:45:55 +0100 Subject: [PATCH 6726/6909] Remove unused using directive --- .../Visual/Playlists/TestScenePlaylistsResultsScreen.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs index e34da1ef0c..be8032cde8 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsResultsScreen.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Net; -using System.Threading.Tasks; using JetBrains.Annotations; using Newtonsoft.Json.Linq; using NUnit.Framework; From 6a5c6febc56567cd5acb43fdb274f9918c8ef230 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Feb 2021 13:23:32 +0900 Subject: [PATCH 6727/6909] Add inline comment explaining the retry loop --- osu.Game/Screens/Play/Player.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index 2ded1752da..e81efdac78 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -434,6 +434,8 @@ namespace osu.Game.Screens.Play { bool paused = Pause(); + // if the initial pause could not be satisfied, the pause cooldown may be active. + // reschedule the pause attempt until it can be achieved. if (!paused) Scheduler.AddOnce(updatePauseOnFocusLostState); } From 996c0897d1faa058fa12ea07b13799e01b67aab9 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Feb 2021 13:40:21 +0900 Subject: [PATCH 6728/6909] Seek via GameplayClockContainer for better reliability --- osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index e5959a3edf..5d070b424a 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -61,7 +61,7 @@ namespace osu.Game.Tests.Visual.Navigation AddStep("press enter", () => InputManager.Key(Key.Enter)); AddUntilStep("wait for player", () => (player = Game.ScreenStack.CurrentScreen as Player) != null); - AddStep("seek to end", () => beatmap().Track.Seek(beatmap().Track.Length)); + AddStep("seek to end", () => player.ChildrenOfType().First().Seek(beatmap().Track.Length)); AddUntilStep("wait for pass", () => (results = Game.ScreenStack.CurrentScreen as ResultsScreen) != null && results.IsLoaded); AddStep("attempt to retry", () => results.ChildrenOfType().First().Action()); AddUntilStep("wait for player", () => Game.ScreenStack.CurrentScreen != player && Game.ScreenStack.CurrentScreen is Player); From 672fd3f9d2935099f24ffa5f2a879295c6970f77 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Feb 2021 14:24:24 +0900 Subject: [PATCH 6729/6909] When disable mouse buttons during gameplay is selected, disable more globally Until now the disable setting would only apply to left/right buttons, and only in gameplay. This change will cause any global actions bound to mouse buttons to also not work during gameplay. Closes #11879. --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 07de2bf601..963c3427d0 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -109,9 +109,9 @@ namespace osu.Game.Rulesets.UI { switch (e) { - case MouseDownEvent mouseDown when mouseDown.Button == MouseButton.Left || mouseDown.Button == MouseButton.Right: + case MouseDownEvent _: if (mouseDisabled.Value) - return false; + return true; // importantly, block upwards propagation so global bindings also don't fire. break; From ec4b770cbac2260bfb731c64d8ba4f7990c9c02a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Feb 2021 14:56:03 +0900 Subject: [PATCH 6730/6909] Remove unused using statement --- osu.Game/Rulesets/UI/RulesetInputManager.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Rulesets/UI/RulesetInputManager.cs b/osu.Game/Rulesets/UI/RulesetInputManager.cs index 963c3427d0..d6f002ea2c 100644 --- a/osu.Game/Rulesets/UI/RulesetInputManager.cs +++ b/osu.Game/Rulesets/UI/RulesetInputManager.cs @@ -16,7 +16,6 @@ using osu.Game.Configuration; using osu.Game.Input.Bindings; using osu.Game.Input.Handlers; using osu.Game.Screens.Play; -using osuTK.Input; using static osu.Game.Input.Handlers.ReplayInputHandler; namespace osu.Game.Rulesets.UI From 664d243003b3fab7d4d6b008302b95fdf7ac12c0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Feb 2021 15:22:46 +0900 Subject: [PATCH 6731/6909] Disable multiplayer/spectator on iOS until it can be supported again --- osu.Game/Online/API/APIAccess.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 8ffa0221c8..ce01378b17 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -10,6 +10,7 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; +using osu.Framework; using osu.Framework.Bindables; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Extensions.ObjectExtensions; @@ -246,7 +247,14 @@ namespace osu.Game.Online.API this.password = password; } - public IHubClientConnector GetHubConnector(string clientName, string endpoint) => new HubClientConnector(clientName, endpoint, this, versionHash); + public IHubClientConnector GetHubConnector(string clientName, string endpoint) + { + // disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805. + if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS) + return null; + + return new HubClientConnector(clientName, endpoint, this, versionHash); + } public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { From c514233141756f9700bb3b51881480ddba98f8ab Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 23 Feb 2021 15:57:41 +0900 Subject: [PATCH 6732/6909] Fix importing collections twice from stable causing a hard crash Somehow a bindable equality check failure got through review. Not sure if there's some way to protect against this going forward, but we may want to. --- osu.Game/Collections/CollectionManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index a65d9a415d..fb9c230c7a 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -138,10 +138,10 @@ namespace osu.Game.Collections PostNotification?.Invoke(notification); - var collection = readCollections(stream, notification); - await importCollections(collection); + var collections = readCollections(stream, notification); + await importCollections(collections); - notification.CompletionText = $"Imported {collection.Count} collections"; + notification.CompletionText = $"Imported {collections.Count} collections"; notification.State = ProgressNotificationState.Completed; } @@ -155,7 +155,7 @@ namespace osu.Game.Collections { foreach (var newCol in newCollections) { - var existing = Collections.FirstOrDefault(c => c.Name == newCol.Name); + var existing = Collections.FirstOrDefault(c => c.Name.Value == newCol.Name.Value); if (existing == null) Collections.Add(existing = new BeatmapCollection { Name = { Value = newCol.Name.Value } }); From f45cedeb8524206a37f7dd7a59cadf5ed20fde21 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Tue, 23 Feb 2021 15:38:09 +0000 Subject: [PATCH 6733/6909] Adjust initial and final rate ranges and prevent them from overlapping --- osu.Game/Rulesets/Mods/ModWindDown.cs | 13 +++++++++++-- osu.Game/Rulesets/Mods/ModWindUp.cs | 13 +++++++++++-- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index 679b50057b..c47ec5fbde 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Initial rate", "The starting speed of the track")] public override BindableNumber InitialRate { get; } = new BindableDouble { - MinValue = 1, + MinValue = 0.5, MaxValue = 2, Default = 1, Value = 1, @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mods public override BindableNumber FinalRate { get; } = new BindableDouble { MinValue = 0.5, - MaxValue = 0.99, + MaxValue = 2, Default = 0.75, Value = 0.75, Precision = 0.01, @@ -45,5 +45,14 @@ namespace osu.Game.Rulesets.Mods }; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindUp)).ToArray(); + + public ModWindDown() + { + InitialRate.BindValueChanged(val => + InitialRate.Value = Math.Max(val.NewValue, FinalRate.Value + 0.01)); + + FinalRate.BindValueChanged(val => + FinalRate.Value = Math.Min(val.NewValue, InitialRate.Value - 0.01)); + } } } diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index b733bf423e..5a0fab5e67 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override BindableNumber InitialRate { get; } = new BindableDouble { MinValue = 0.5, - MaxValue = 1, + MaxValue = 2, Default = 1, Value = 1, Precision = 0.01, @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The speed increase to ramp towards")] public override BindableNumber FinalRate { get; } = new BindableDouble { - MinValue = 1.01, + MinValue = 0.5, MaxValue = 2, Default = 1.5, Value = 1.5, @@ -45,5 +45,14 @@ namespace osu.Game.Rulesets.Mods }; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModWindDown)).ToArray(); + + public ModWindUp() + { + InitialRate.BindValueChanged(val => + InitialRate.Value = Math.Min(val.NewValue, FinalRate.Value - 0.01)); + + FinalRate.BindValueChanged(val => + FinalRate.Value = Math.Max(val.NewValue, InitialRate.Value + 0.01)); + } } } From a6e840634b255ea86e21cfa8140df3794965ebe6 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Tue, 23 Feb 2021 15:52:53 +0000 Subject: [PATCH 6734/6909] Adjust scrubbing behaviour to allow dragging through rate values --- osu.Game/Rulesets/Mods/ModWindDown.cs | 8 ++++---- osu.Game/Rulesets/Mods/ModWindUp.cs | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index c47ec5fbde..f9e6854dd4 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -20,7 +20,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Initial rate", "The starting speed of the track")] public override BindableNumber InitialRate { get; } = new BindableDouble { - MinValue = 0.5, + MinValue = 0.51, MaxValue = 2, Default = 1, Value = 1, @@ -31,7 +31,7 @@ namespace osu.Game.Rulesets.Mods public override BindableNumber FinalRate { get; } = new BindableDouble { MinValue = 0.5, - MaxValue = 2, + MaxValue = 1.99, Default = 0.75, Value = 0.75, Precision = 0.01, @@ -49,10 +49,10 @@ namespace osu.Game.Rulesets.Mods public ModWindDown() { InitialRate.BindValueChanged(val => - InitialRate.Value = Math.Max(val.NewValue, FinalRate.Value + 0.01)); + FinalRate.Value = Math.Min(FinalRate.Value, val.NewValue - 0.01)); FinalRate.BindValueChanged(val => - FinalRate.Value = Math.Min(val.NewValue, InitialRate.Value - 0.01)); + InitialRate.Value = Math.Max(InitialRate.Value, val.NewValue + 0.01)); } } } diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index 5a0fab5e67..0d57bbb52d 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Mods public override BindableNumber InitialRate { get; } = new BindableDouble { MinValue = 0.5, - MaxValue = 2, + MaxValue = 1.99, Default = 1, Value = 1, Precision = 0.01, @@ -30,7 +30,7 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Final rate", "The speed increase to ramp towards")] public override BindableNumber FinalRate { get; } = new BindableDouble { - MinValue = 0.5, + MinValue = 0.51, MaxValue = 2, Default = 1.5, Value = 1.5, @@ -49,10 +49,10 @@ namespace osu.Game.Rulesets.Mods public ModWindUp() { InitialRate.BindValueChanged(val => - InitialRate.Value = Math.Min(val.NewValue, FinalRate.Value - 0.01)); + FinalRate.Value = Math.Max(FinalRate.Value, val.NewValue + 0.01)); FinalRate.BindValueChanged(val => - FinalRate.Value = Math.Max(val.NewValue, InitialRate.Value + 0.01)); + InitialRate.Value = Math.Min(InitialRate.Value, val.NewValue - 0.01)); } } } From 7394c62cc8ee4c30ce12543fa7c6609d7ee9dc58 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Tue, 23 Feb 2021 18:10:03 +0000 Subject: [PATCH 6735/6909] Make ModTimeRamp and ModRateAdjust incompatible --- osu.Game/Rulesets/Mods/ModRateAdjust.cs | 3 +++ osu.Game/Rulesets/Mods/ModTimeRamp.cs | 2 ++ 2 files changed, 5 insertions(+) diff --git a/osu.Game/Rulesets/Mods/ModRateAdjust.cs b/osu.Game/Rulesets/Mods/ModRateAdjust.cs index b016a6d43b..e66650f7b4 100644 --- a/osu.Game/Rulesets/Mods/ModRateAdjust.cs +++ b/osu.Game/Rulesets/Mods/ModRateAdjust.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.Audio; using osu.Framework.Audio.Track; using osu.Framework.Bindables; @@ -24,6 +25,8 @@ namespace osu.Game.Rulesets.Mods public double ApplyToRate(double time, double rate) => rate * SpeedChange.Value; + public override Type[] IncompatibleMods => new[] { typeof(ModTimeRamp) }; + 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 330945d3d3..b5cd64dafa 100644 --- a/osu.Game/Rulesets/Mods/ModTimeRamp.cs +++ b/osu.Game/Rulesets/Mods/ModTimeRamp.cs @@ -30,6 +30,8 @@ namespace osu.Game.Rulesets.Mods [SettingSource("Adjust pitch", "Should pitch be adjusted with speed")] public abstract BindableBool AdjustPitch { get; } + public override Type[] IncompatibleMods => new[] { typeof(ModRateAdjust) }; + public override string SettingDescription => $"{InitialRate.Value:N2}x to {FinalRate.Value:N2}x"; private double finalRateTime; From dbde47fe94e5c26270e4b124f7539c953f32b5b4 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Tue, 23 Feb 2021 19:43:04 +0000 Subject: [PATCH 6736/6909] Fix test failure --- 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 7a0dd5b719..650ae68ffc 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -101,7 +101,7 @@ namespace osu.Game.Tests.Gameplay break; case ModTimeRamp m: - m.InitialRate.Value = m.FinalRate.Value = expectedRate; + m.FinalRate.Value = m.InitialRate.Value = expectedRate; break; } From f6d3cd6413e55eb4f44dc87d66644da55ecb0699 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Tue, 23 Feb 2021 21:25:59 +0000 Subject: [PATCH 6737/6909] Change SamplePlaybackWithRateMods to use rate calulated from the sample Replace hardcoded numbers --- osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs | 9 +++++++-- osu.Game/Rulesets/Mods/ModWindDown.cs | 4 ++-- osu.Game/Rulesets/Mods/ModWindUp.cs | 4 ++-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs index 650ae68ffc..10a1a13ba0 100644 --- a/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs +++ b/osu.Game.Tests/Gameplay/TestSceneStoryboardSamples.cs @@ -13,6 +13,7 @@ using osu.Framework.Graphics.Audio; using osu.Framework.Graphics.Textures; using osu.Framework.IO.Stores; using osu.Framework.Testing; +using osu.Framework.Utils; using osu.Game.Audio; using osu.Game.IO; using osu.Game.Rulesets; @@ -90,6 +91,7 @@ namespace osu.Game.Tests.Gameplay public void TestSamplePlaybackWithRateMods(Type expectedMod, double expectedRate) { GameplayClockContainer gameplayContainer = null; + StoryboardSampleInfo sampleInfo = null; TestDrawableStoryboardSample sample = null; Mod testedMod = Activator.CreateInstance(expectedMod) as Mod; @@ -117,7 +119,7 @@ namespace osu.Game.Tests.Gameplay Child = beatmapSkinSourceContainer }); - beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(new StoryboardSampleInfo("test-sample", 1, 1)) + beatmapSkinSourceContainer.Add(sample = new TestDrawableStoryboardSample(sampleInfo = new StoryboardSampleInfo("test-sample", 1, 1)) { Clock = gameplayContainer.GameplayClock }); @@ -125,7 +127,10 @@ namespace osu.Game.Tests.Gameplay AddStep("start", () => gameplayContainer.Start()); - AddAssert("sample playback rate matches mod rates", () => sample.ChildrenOfType().First().AggregateFrequency.Value == expectedRate); + AddAssert("sample playback rate matches mod rates", () => + testedMod != null && Precision.AlmostEquals( + sample.ChildrenOfType().First().AggregateFrequency.Value, + ((IApplicableToRate)testedMod).ApplyToRate(sampleInfo.StartTime))); } private class TestSkin : LegacySkin diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index f9e6854dd4..9bd5b5eefd 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -49,10 +49,10 @@ namespace osu.Game.Rulesets.Mods public ModWindDown() { InitialRate.BindValueChanged(val => - FinalRate.Value = Math.Min(FinalRate.Value, val.NewValue - 0.01)); + FinalRate.Value = Math.Min(FinalRate.Value, val.NewValue - FinalRate.Precision)); FinalRate.BindValueChanged(val => - InitialRate.Value = Math.Max(InitialRate.Value, val.NewValue + 0.01)); + InitialRate.Value = Math.Max(InitialRate.Value, val.NewValue + InitialRate.Precision)); } } } diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index 0d57bbb52d..39d3c9c5d5 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -49,10 +49,10 @@ namespace osu.Game.Rulesets.Mods public ModWindUp() { InitialRate.BindValueChanged(val => - FinalRate.Value = Math.Max(FinalRate.Value, val.NewValue + 0.01)); + FinalRate.Value = Math.Max(FinalRate.Value, val.NewValue + FinalRate.Precision)); FinalRate.BindValueChanged(val => - InitialRate.Value = Math.Min(InitialRate.Value, val.NewValue - 0.01)); + InitialRate.Value = Math.Min(InitialRate.Value, val.NewValue - InitialRate.Precision)); } } } From 71182347d677be782005acaf1e227c6cd21a0275 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Feb 2021 11:30:13 +0900 Subject: [PATCH 6738/6909] Also add a notifiation when trying to enter the multiplayer screen Turns out the only check required to get into this screen was that the API was online, which it always is even if the multiplayer component isn't. This provides a better end-user experience. --- osu.Game/Screens/Menu/ButtonSystem.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 81b1cb0bf1..dd1e318aa0 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -172,6 +172,23 @@ namespace osu.Game.Screens.Menu return; } + // disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805. + if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS) + { + notifications?.Post(new SimpleNotification + { + Text = "Multiplayer is temporarily unavailable on iOS as we figure out some low level issues.", + Icon = FontAwesome.Solid.AppleAlt, + Activated = () => + { + loginOverlay?.Show(); + return true; + } + }); + + return; + } + OnMultiplayer?.Invoke(); } From e1f71038e39b09134ae2587692f7a9b9fa884d75 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Feb 2021 12:13:55 +0900 Subject: [PATCH 6739/6909] Remove unncessary action --- osu.Game/Screens/Menu/ButtonSystem.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index dd1e318aa0..f93bfd7705 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -179,11 +179,6 @@ namespace osu.Game.Screens.Menu { Text = "Multiplayer is temporarily unavailable on iOS as we figure out some low level issues.", Icon = FontAwesome.Solid.AppleAlt, - Activated = () => - { - loginOverlay?.Show(); - return true; - } }); return; From 7000132d034c6cf012b475ec44178c7202ca4c3a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Feb 2021 12:45:00 +0900 Subject: [PATCH 6740/6909] Specify full filename inline for quick beatmap --- osu.Game.Tests/Resources/TestResources.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index 14bc2c8733..c979b5c695 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -13,7 +13,7 @@ namespace osu.Game.Tests.Resources public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}"); - public static Stream GetTestBeatmapStream(bool virtualTrack = false, bool quick = false) => OpenResource($"Archives/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}{(quick ? "_quick" : "")}.osz"); + public static Stream GetTestBeatmapStream(bool virtualTrack = false) => OpenResource($"Archives/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz"); /// /// Retrieve a path to a copy of a shortened (~10 second) beatmap archive with a virtual track. @@ -24,8 +24,7 @@ namespace osu.Game.Tests.Resources public static string GetQuickTestBeatmapForImport() { var tempPath = Path.GetTempFileName() + ".osz"; - - using (var stream = GetTestBeatmapStream(true, true)) + using (var stream = OpenResource($"Archives/241526 Soleily - Renatus_virtual_quick.osz")) using (var newFile = File.Create(tempPath)) stream.CopyTo(newFile); From 59e6bad0b9cf128c4f208a67fface6ad82ff48bd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Feb 2021 12:46:35 +0900 Subject: [PATCH 6741/6909] Remove unnecessary interpolated string specification --- osu.Game.Tests/Resources/TestResources.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index c979b5c695..cef0532f9d 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -24,7 +24,7 @@ namespace osu.Game.Tests.Resources public static string GetQuickTestBeatmapForImport() { var tempPath = Path.GetTempFileName() + ".osz"; - using (var stream = OpenResource($"Archives/241526 Soleily - Renatus_virtual_quick.osz")) + using (var stream = OpenResource("Archives/241526 Soleily - Renatus_virtual_quick.osz")) using (var newFile = File.Create(tempPath)) stream.CopyTo(newFile); From dd702ccfd22ef251000985bdb72e71812855893e Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 24 Feb 2021 13:39:15 +0900 Subject: [PATCH 6742/6909] Make mania FI/HD incompatible with each other --- .../Mods/ManiaModFadeIn.cs | 10 +++-- .../Mods/ManiaModHidden.cs | 33 +------------- .../Mods/ManiaModPlayfieldCover.cs | 43 +++++++++++++++++++ 3 files changed, 51 insertions(+), 35 deletions(-) create mode 100644 osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs index cbdcd49c5b..f80c9e1f7c 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModFadeIn.cs @@ -1,18 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using osu.Framework.Graphics.Sprites; -using osu.Game.Graphics; +using System; +using System.Linq; using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModFadeIn : ManiaModHidden + public class ManiaModFadeIn : ManiaModPlayfieldCover { public override string Name => "Fade In"; public override string Acronym => "FI"; - public override IconUsage? Icon => OsuIcon.ModHidden; public override string Description => @"Keys appear out of nowhere!"; + public override double ScoreMultiplier => 1; + + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModHidden)).ToArray(); protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AlongScroll; } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index 4bdb15526f..a68f12cb84 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -3,43 +3,14 @@ using System; using System.Linq; -using osu.Framework.Graphics; -using osu.Framework.Graphics.Containers; -using osu.Game.Rulesets.Mania.Objects; -using osu.Game.Rulesets.Mania.UI; -using osu.Game.Rulesets.Mods; -using osu.Game.Rulesets.UI; namespace osu.Game.Rulesets.Mania.Mods { - public class ManiaModHidden : ModHidden, IApplicableToDrawableRuleset + public class ManiaModHidden : ManiaModPlayfieldCover { public override string Description => @"Keys fade out before you hit them!"; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; - /// - /// The direction in which the cover should expand. - /// - protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; - - public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) - { - ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; - - foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) - { - HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; - Container hocParent = (Container)hoc.Parent; - - hocParent.Remove(hoc); - hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c => - { - c.RelativeSizeAxes = Axes.Both; - c.Direction = ExpandDirection; - c.Coverage = 0.5f; - })); - } - } + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray(); } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs new file mode 100644 index 0000000000..78c3331fbf --- /dev/null +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.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.Linq; +using osu.Framework.Graphics; +using osu.Framework.Graphics.Containers; +using osu.Game.Rulesets.Mania.Objects; +using osu.Game.Rulesets.Mania.UI; +using osu.Game.Rulesets.Mods; +using osu.Game.Rulesets.UI; + +namespace osu.Game.Rulesets.Mania.Mods +{ + public abstract class ManiaModPlayfieldCover : ModHidden, IApplicableToDrawableRuleset + { + public override Type[] IncompatibleMods => new[] { typeof(ModFlashlight) }; + + /// + /// The direction in which the cover should expand. + /// + protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; + + public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) + { + ManiaPlayfield maniaPlayfield = (ManiaPlayfield)drawableRuleset.Playfield; + + foreach (Column column in maniaPlayfield.Stages.SelectMany(stage => stage.Columns)) + { + HitObjectContainer hoc = column.HitObjectArea.HitObjectContainer; + Container hocParent = (Container)hoc.Parent; + + hocParent.Remove(hoc); + hocParent.Add(new PlayfieldCoveringWrapper(hoc).With(c => + { + c.RelativeSizeAxes = Axes.Both; + c.Direction = ExpandDirection; + c.Coverage = 0.5f; + })); + } + } + } +} From 30a58691f04b48126fb8714331a6d84cf88b6cd6 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 24 Feb 2021 14:32:50 +0900 Subject: [PATCH 6743/6909] Make SD and PF incompatible with each other --- osu.Game/Rulesets/Mods/ModFailCondition.cs | 25 ++++++++++++++++++++++ osu.Game/Rulesets/Mods/ModPerfect.cs | 9 +++++++- osu.Game/Rulesets/Mods/ModSuddenDeath.cs | 15 ++++--------- 3 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 osu.Game/Rulesets/Mods/ModFailCondition.cs diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs new file mode 100644 index 0000000000..40a0843e06 --- /dev/null +++ b/osu.Game/Rulesets/Mods/ModFailCondition.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 osu.Game.Rulesets.Judgements; +using osu.Game.Rulesets.Scoring; + +namespace osu.Game.Rulesets.Mods +{ + public abstract class ModFailCondition : Mod, IApplicableToHealthProcessor, IApplicableFailOverride + { + public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; + + public bool PerformFail() => true; + + public bool RestartOnFail => true; + + public void ApplyToHealthProcessor(HealthProcessor healthProcessor) + { + healthProcessor.FailConditions += FailCondition; + } + + protected abstract bool FailCondition(HealthProcessor healthProcessor, JudgementResult result); + } +} diff --git a/osu.Game/Rulesets/Mods/ModPerfect.cs b/osu.Game/Rulesets/Mods/ModPerfect.cs index df0fc9c4b6..d0b09b50f2 100644 --- a/osu.Game/Rulesets/Mods/ModPerfect.cs +++ b/osu.Game/Rulesets/Mods/ModPerfect.cs @@ -1,6 +1,8 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. +using System; +using System.Linq; using osu.Framework.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; @@ -8,13 +10,18 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModPerfect : ModSuddenDeath + public abstract class ModPerfect : ModFailCondition { public override string Name => "Perfect"; public override string Acronym => "PF"; public override IconUsage? Icon => OsuIcon.ModPerfect; + public override ModType Type => ModType.DifficultyIncrease; + public override bool Ranked => true; + public override double ScoreMultiplier => 1; public override string Description => "SS or quit."; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModSuddenDeath)).ToArray(); + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => result.Type.AffectsAccuracy() && result.Type != result.Judgement.MaxResult; diff --git a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs b/osu.Game/Rulesets/Mods/ModSuddenDeath.cs index ae71041a64..617ae38feb 100644 --- a/osu.Game/Rulesets/Mods/ModSuddenDeath.cs +++ b/osu.Game/Rulesets/Mods/ModSuddenDeath.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.Graphics.Sprites; using osu.Game.Graphics; using osu.Game.Rulesets.Judgements; @@ -9,7 +10,7 @@ using osu.Game.Rulesets.Scoring; namespace osu.Game.Rulesets.Mods { - public abstract class ModSuddenDeath : Mod, IApplicableToHealthProcessor, IApplicableFailOverride + public abstract class ModSuddenDeath : ModFailCondition { public override string Name => "Sudden Death"; public override string Acronym => "SD"; @@ -18,18 +19,10 @@ namespace osu.Game.Rulesets.Mods public override string Description => "Miss and fail."; public override double ScoreMultiplier => 1; public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; - public bool PerformFail() => true; + public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ModPerfect)).ToArray(); - public bool RestartOnFail => true; - - public void ApplyToHealthProcessor(HealthProcessor healthProcessor) - { - healthProcessor.FailConditions += FailCondition; - } - - protected virtual bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) + protected override bool FailCondition(HealthProcessor healthProcessor, JudgementResult result) => result.Type.AffectsCombo() && !result.IsHit; } From 14160b897e238ebcba242f5fa09f6b237066c960 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 24 Feb 2021 14:42:04 +0900 Subject: [PATCH 6744/6909] Fix references to ModSuddenDeath --- osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs | 2 +- osu.Game/Rulesets/Mods/ModAutoplay.cs | 2 +- osu.Game/Rulesets/Mods/ModNoFail.cs | 2 +- osu.Game/Rulesets/Mods/ModRelax.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs index 77de0cb45b..aac830801b 100644 --- a/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs +++ b/osu.Game.Rulesets.Osu/Mods/OsuModAutopilot.cs @@ -22,7 +22,7 @@ namespace osu.Game.Rulesets.Osu.Mods public override ModType Type => ModType.Automation; public override string Description => @"Automatic cursor movement - just follow the rhythm."; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(OsuModSpunOut), typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail), typeof(ModAutoplay) }; public bool PerformFail() => false; diff --git a/osu.Game/Rulesets/Mods/ModAutoplay.cs b/osu.Game/Rulesets/Mods/ModAutoplay.cs index d1d23def67..d6e1d46b06 100644 --- a/osu.Game/Rulesets/Mods/ModAutoplay.cs +++ b/osu.Game/Rulesets/Mods/ModAutoplay.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Mods public bool RestartOnFail => false; - public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModNoFail) }; + public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModNoFail) }; public override bool HasImplementation => GetType().GenericTypeArguments.Length == 0; diff --git a/osu.Game/Rulesets/Mods/ModNoFail.cs b/osu.Game/Rulesets/Mods/ModNoFail.cs index b95ec7490e..c0f24e116a 100644 --- a/osu.Game/Rulesets/Mods/ModNoFail.cs +++ b/osu.Game/Rulesets/Mods/ModNoFail.cs @@ -16,6 +16,6 @@ namespace osu.Game.Rulesets.Mods public override string Description => "You can't fail, no matter what."; public override double ScoreMultiplier => 0.5; public override bool Ranked => true; - public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModSuddenDeath), typeof(ModAutoplay) }; + public override Type[] IncompatibleMods => new[] { typeof(ModRelax), typeof(ModFailCondition), typeof(ModAutoplay) }; } } diff --git a/osu.Game/Rulesets/Mods/ModRelax.cs b/osu.Game/Rulesets/Mods/ModRelax.cs index b6fec42f43..e5995ff180 100644 --- a/osu.Game/Rulesets/Mods/ModRelax.cs +++ b/osu.Game/Rulesets/Mods/ModRelax.cs @@ -14,6 +14,6 @@ namespace osu.Game.Rulesets.Mods public override IconUsage? Icon => OsuIcon.ModRelax; public override ModType Type => ModType.Automation; public override double ScoreMultiplier => 1; - public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModSuddenDeath) }; + public override Type[] IncompatibleMods => new[] { typeof(ModAutoplay), typeof(ModNoFail), typeof(ModFailCondition) }; } } From 0b44d2483b6f02dd415c461ab6d3081e96cd9971 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 24 Feb 2021 15:03:37 +0900 Subject: [PATCH 6745/6909] Make some properties virtual I think they were intended to be this way from the beginning. --- osu.Game/Rulesets/Mods/ModFailCondition.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModFailCondition.cs b/osu.Game/Rulesets/Mods/ModFailCondition.cs index 40a0843e06..c0d7bae2b2 100644 --- a/osu.Game/Rulesets/Mods/ModFailCondition.cs +++ b/osu.Game/Rulesets/Mods/ModFailCondition.cs @@ -11,9 +11,9 @@ namespace osu.Game.Rulesets.Mods { public override Type[] IncompatibleMods => new[] { typeof(ModNoFail), typeof(ModRelax), typeof(ModAutoplay) }; - public bool PerformFail() => true; + public virtual bool PerformFail() => true; - public bool RestartOnFail => true; + public virtual bool RestartOnFail => true; public void ApplyToHealthProcessor(HealthProcessor healthProcessor) { From 6b6811063b617b3cf5c0e38a6eae95193759dd18 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 24 Feb 2021 15:05:12 +0900 Subject: [PATCH 6746/6909] Make ExpandDirection abstract --- osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs | 3 +++ osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs index a68f12cb84..e3ac624a6e 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModHidden.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using osu.Game.Rulesets.Mania.UI; namespace osu.Game.Rulesets.Mania.Mods { @@ -12,5 +13,7 @@ namespace osu.Game.Rulesets.Mania.Mods public override double ScoreMultiplier => 1; public override Type[] IncompatibleMods => base.IncompatibleMods.Append(typeof(ManiaModFadeIn)).ToArray(); + + protected override CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; } } diff --git a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs index 78c3331fbf..87501d07a5 100644 --- a/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs +++ b/osu.Game.Rulesets.Mania/Mods/ManiaModPlayfieldCover.cs @@ -19,7 +19,7 @@ namespace osu.Game.Rulesets.Mania.Mods /// /// The direction in which the cover should expand. /// - protected virtual CoverExpandDirection ExpandDirection => CoverExpandDirection.AgainstScroll; + protected abstract CoverExpandDirection ExpandDirection { get; } public virtual void ApplyToDrawableRuleset(DrawableRuleset drawableRuleset) { From 165da3204454999cd8497d0f55987d871774b34c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Feb 2021 18:41:42 +0900 Subject: [PATCH 6747/6909] Fix dropdown crash on collection name collisions --- osu.Game/Collections/CollectionFilterMenuItem.cs | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/osu.Game/Collections/CollectionFilterMenuItem.cs b/osu.Game/Collections/CollectionFilterMenuItem.cs index fe79358223..0617996872 100644 --- a/osu.Game/Collections/CollectionFilterMenuItem.cs +++ b/osu.Game/Collections/CollectionFilterMenuItem.cs @@ -36,7 +36,19 @@ namespace osu.Game.Collections } public bool Equals(CollectionFilterMenuItem other) - => other != null && CollectionName.Value == other.CollectionName.Value; + { + if (other == null) + return false; + + // collections may have the same name, so compare first on reference equality. + // this relies on the assumption that only one instance of the BeatmapCollection exists game-wide, managed by CollectionManager. + if (Collection != null) + return Collection == other.Collection; + + // fallback to name-based comparison. + // this is required for special dropdown items which don't have a collection (all beatmaps / manage collections items below). + return CollectionName.Value == other.CollectionName.Value; + } public override int GetHashCode() => CollectionName.Value.GetHashCode(); } From 6e6fb31c050ad03ce1f064fb8077f8df4d0f7027 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Feb 2021 18:42:26 +0900 Subject: [PATCH 6748/6909] Add test coverage --- .../TestSceneManageCollectionsDialog.cs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs index 1655adf811..eca857f9e5 100644 --- a/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs +++ b/osu.Game.Tests/Visual/Collections/TestSceneManageCollectionsDialog.cs @@ -44,7 +44,7 @@ namespace osu.Game.Tests.Visual.Collections { manager = new CollectionManager(LocalStorage), Content, - dialogOverlay = new DialogOverlay() + dialogOverlay = new DialogOverlay(), }); Dependencies.Cache(manager); @@ -134,6 +134,27 @@ namespace osu.Game.Tests.Visual.Collections assertCollectionName(0, "2"); } + [Test] + public void TestCollectionNameCollisions() + { + AddStep("add dropdown", () => + { + Add(new CollectionFilterDropdown + { + Anchor = Anchor.TopRight, + Origin = Anchor.TopRight, + RelativeSizeAxes = Axes.X, + Width = 0.4f, + } + ); + }); + AddStep("add two collections with same name", () => manager.Collections.AddRange(new[] + { + new BeatmapCollection { Name = { Value = "1" } }, + new BeatmapCollection { Name = { Value = "1" }, Beatmaps = { beatmapManager.GetAllUsableBeatmapSets().First().Beatmaps[0] } }, + })); + } + [Test] public void TestRemoveCollectionViaButton() { From 5dc0aefb2bf36f7ab18e8c41a1643bcc31b05c98 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Feb 2021 19:54:52 +0900 Subject: [PATCH 6749/6909] Cancel request on leaving results screen --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 76b549da1a..4c35096910 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -15,6 +15,8 @@ namespace osu.Game.Screens.Ranking { public class SoloResultsScreen : ResultsScreen { + private GetScoresRequest getScoreRequest; + [Resolved] private RulesetStore rulesets { get; set; } @@ -28,9 +30,16 @@ namespace osu.Game.Screens.Ranking 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; + getScoreRequest = new GetScoresRequest(Score.Beatmap, Score.Ruleset); + getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != this.Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); + return getScoreRequest; + } + + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + + getScoreRequest?.Cancel(); } } } From 9ed8d902f7ca20f47179d5f6387e0c9583b8b320 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Feb 2021 19:57:42 +0900 Subject: [PATCH 6750/6909] Fix requests being indefinitely queued when user is offline --- osu.Game/Online/API/APIAccess.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index ce01378b17..569481d491 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -381,7 +381,13 @@ namespace osu.Game.Online.API public void Queue(APIRequest request) { - lock (queue) queue.Enqueue(request); + lock (queue) + { + if (state.Value == APIState.Offline) + return; + + queue.Enqueue(request); + } } private void flushQueue(bool failOldRequests = true) @@ -402,8 +408,6 @@ namespace osu.Game.Online.API public void Logout() { - flushQueue(); - password = null; authentication.Clear(); @@ -415,6 +419,7 @@ namespace osu.Game.Online.API }); state.Value = APIState.Offline; + flushQueue(); } private static User createGuestUser() => new GuestUser(); From fa6d797adf9860bbde472efffcca6fa77256fb14 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 24 Feb 2021 20:30:17 +0900 Subject: [PATCH 6751/6909] Remove redundant prefix --- osu.Game/Screens/Ranking/SoloResultsScreen.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/Ranking/SoloResultsScreen.cs b/osu.Game/Screens/Ranking/SoloResultsScreen.cs index 4c35096910..9bc696948f 100644 --- a/osu.Game/Screens/Ranking/SoloResultsScreen.cs +++ b/osu.Game/Screens/Ranking/SoloResultsScreen.cs @@ -31,7 +31,7 @@ namespace osu.Game.Screens.Ranking return null; getScoreRequest = new GetScoresRequest(Score.Beatmap, Score.Ruleset); - getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != this.Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); + getScoreRequest.Success += r => scoresCallback?.Invoke(r.Scores.Where(s => s.OnlineScoreID != Score.OnlineScoreID).Select(s => s.CreateScoreInfo(rulesets))); return getScoreRequest; } From 73d6a3687eacd25e26501cc2b9ea061b86512c38 Mon Sep 17 00:00:00 2001 From: Ronnie Moir <7267697+H2n9@users.noreply.github.com> Date: Wed, 24 Feb 2021 14:40:56 +0000 Subject: [PATCH 6752/6909] Change rate correction logic to be more explicit --- osu.Game/Rulesets/Mods/ModWindDown.cs | 10 ++++++++-- osu.Game/Rulesets/Mods/ModWindUp.cs | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/osu.Game/Rulesets/Mods/ModWindDown.cs b/osu.Game/Rulesets/Mods/ModWindDown.cs index 9bd5b5eefd..c8d79325a3 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -49,10 +49,16 @@ namespace osu.Game.Rulesets.Mods public ModWindDown() { InitialRate.BindValueChanged(val => - FinalRate.Value = Math.Min(FinalRate.Value, val.NewValue - FinalRate.Precision)); + { + if (val.NewValue <= FinalRate.Value) + FinalRate.Value = val.NewValue - FinalRate.Precision; + }); FinalRate.BindValueChanged(val => - InitialRate.Value = Math.Max(InitialRate.Value, val.NewValue + InitialRate.Precision)); + { + if (val.NewValue >= InitialRate.Value) + InitialRate.Value = val.NewValue + FinalRate.Precision; + }); } } } diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index 39d3c9c5d5..4fc1f61e02 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -49,10 +49,16 @@ namespace osu.Game.Rulesets.Mods public ModWindUp() { InitialRate.BindValueChanged(val => - FinalRate.Value = Math.Max(FinalRate.Value, val.NewValue + FinalRate.Precision)); + { + if (val.NewValue >= FinalRate.Value) + FinalRate.Value = val.NewValue + FinalRate.Precision; + }); FinalRate.BindValueChanged(val => - InitialRate.Value = Math.Min(InitialRate.Value, val.NewValue - InitialRate.Precision)); + { + if (val.NewValue <= InitialRate.Value) + InitialRate.Value = val.NewValue - FinalRate.Precision; + }); } } } From 421b7877d4eb9eed06942666c37d60d962d78b27 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 24 Feb 2021 19:16:10 +0100 Subject: [PATCH 6753/6909] Avoid mixing precision across time ramp bindables Bears no functional difference, it's just a bit less of an eyesore. --- 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 c8d79325a3..08bd44f7bd 100644 --- a/osu.Game/Rulesets/Mods/ModWindDown.cs +++ b/osu.Game/Rulesets/Mods/ModWindDown.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mods FinalRate.BindValueChanged(val => { if (val.NewValue >= InitialRate.Value) - InitialRate.Value = val.NewValue + FinalRate.Precision; + InitialRate.Value = val.NewValue + InitialRate.Precision; }); } } diff --git a/osu.Game/Rulesets/Mods/ModWindUp.cs b/osu.Game/Rulesets/Mods/ModWindUp.cs index 4fc1f61e02..df8f781148 100644 --- a/osu.Game/Rulesets/Mods/ModWindUp.cs +++ b/osu.Game/Rulesets/Mods/ModWindUp.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Mods FinalRate.BindValueChanged(val => { if (val.NewValue <= InitialRate.Value) - InitialRate.Value = val.NewValue - FinalRate.Precision; + InitialRate.Value = val.NewValue - InitialRate.Precision; }); } } From a362382d381e6128b2eabc55ff7a6717eb1722ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 14:06:21 +0900 Subject: [PATCH 6754/6909] Add back more correct null checks --- osu.Game/Graphics/UserInterface/OsuButton.cs | 2 +- osu.Game/Screens/Select/FooterButton.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Graphics/UserInterface/OsuButton.cs b/osu.Game/Graphics/UserInterface/OsuButton.cs index d2114134cf..a22c837080 100644 --- a/osu.Game/Graphics/UserInterface/OsuButton.cs +++ b/osu.Game/Graphics/UserInterface/OsuButton.cs @@ -24,7 +24,7 @@ namespace osu.Game.Graphics.UserInterface { public LocalisableString Text { - get => SpriteText.Text; + get => SpriteText?.Text ?? default; set { if (SpriteText != null) diff --git a/osu.Game/Screens/Select/FooterButton.cs b/osu.Game/Screens/Select/FooterButton.cs index 7bdeacc91a..cd7c1c449f 100644 --- a/osu.Game/Screens/Select/FooterButton.cs +++ b/osu.Game/Screens/Select/FooterButton.cs @@ -24,7 +24,7 @@ namespace osu.Game.Screens.Select public LocalisableString Text { - get => SpriteText.Text; + get => SpriteText?.Text ?? default; set { if (SpriteText != null) From 63d48f0c7d786ea069da6ac88fcc1d7e053356e0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 14:06:29 +0900 Subject: [PATCH 6755/6909] Fix incorrect unicode/romanised string order --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index e6d73c6e83..a86699a9b5 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -75,8 +75,8 @@ namespace osu.Game.Tournament.Components new TournamentSpriteText { Text = new RomanisableString( - $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", - $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}"), + $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}", + $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}"), Font = OsuFont.Torus.With(weight: FontWeight.Bold), }, new FillFlowContainer From 4cdde422280004f4013124ba29a78cf871f52bc0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 14:08:01 +0900 Subject: [PATCH 6756/6909] Remove unnecessary backing field --- osu.Game/Overlays/Chat/Selection/ChannelSection.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs index e18302770c..537ac975ac 100644 --- a/osu.Game/Overlays/Chat/Selection/ChannelSection.cs +++ b/osu.Game/Overlays/Chat/Selection/ChannelSection.cs @@ -15,8 +15,6 @@ namespace osu.Game.Overlays.Chat.Selection { public class ChannelSection : Container, IHasFilterableChildren { - private readonly OsuSpriteText header; - public readonly FillFlowContainer ChannelFlow; public IEnumerable FilterableChildren => ChannelFlow.Children; @@ -41,7 +39,7 @@ namespace osu.Game.Overlays.Chat.Selection Children = new Drawable[] { - header = new OsuSpriteText + new OsuSpriteText { Font = OsuFont.GetFont(size: 15, weight: FontWeight.Bold), Text = "All Channels".ToUpperInvariant() From e82eaffaed6097e59262aa6106b4784edcb29157 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 14:12:59 +0900 Subject: [PATCH 6757/6909] Flip order back to original for romanisable strings --- osu.Game.Tournament/Components/TournamentBeatmapPanel.cs | 4 ++-- osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs | 4 ++-- osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs | 4 ++-- osu.Game/Overlays/Music/PlaylistItem.cs | 4 ++-- osu.Game/Overlays/NowPlayingOverlay.cs | 4 ++-- .../Sections/Historical/DrawableMostPlayedBeatmap.cs | 6 +++--- .../Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs | 6 +++--- osu.Game/Screens/Menu/SongTicker.cs | 4 ++-- osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs | 4 ++-- osu.Game/Screens/Play/BeatmapMetadataDisplay.cs | 4 ++-- .../Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs | 4 ++-- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 4 ++-- osu.Game/Screens/Select/Carousel/SetPanelContent.cs | 4 ++-- 13 files changed, 28 insertions(+), 28 deletions(-) diff --git a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs index a86699a9b5..e6d73c6e83 100644 --- a/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs +++ b/osu.Game.Tournament/Components/TournamentBeatmapPanel.cs @@ -75,8 +75,8 @@ namespace osu.Game.Tournament.Components new TournamentSpriteText { Text = new RomanisableString( - $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}", - $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}"), + $"{Beatmap.Metadata.ArtistUnicode ?? Beatmap.Metadata.Artist} - {Beatmap.Metadata.TitleUnicode ?? Beatmap.Metadata.Title}", + $"{Beatmap.Metadata.Artist} - {Beatmap.Metadata.Title}"), Font = OsuFont.Torus.With(weight: FontWeight.Bold), }, new FillFlowContainer diff --git a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs index ba4725b49a..4d5c387c4a 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/GridBeatmapPanel.cs @@ -84,14 +84,14 @@ namespace osu.Game.Overlays.BeatmapListing.Panels { new OsuSpriteText { - Text = new RomanisableString(SetInfo.Metadata.Title, SetInfo.Metadata.TitleUnicode), + Text = new RomanisableString(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true) }, } }, new OsuSpriteText { - Text = new RomanisableString(SetInfo.Metadata.Artist, SetInfo.Metadata.ArtistUnicode), + Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) }, }, diff --git a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs index 624cb89d1e..00ffd168c1 100644 --- a/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs +++ b/osu.Game/Overlays/BeatmapListing/Panels/ListBeatmapPanel.cs @@ -107,14 +107,14 @@ namespace osu.Game.Overlays.BeatmapListing.Panels { new OsuSpriteText { - Text = new RomanisableString(SetInfo.Metadata.Title, SetInfo.Metadata.TitleUnicode), + Text = new RomanisableString(SetInfo.Metadata.TitleUnicode, SetInfo.Metadata.Title), Font = OsuFont.GetFont(size: 18, weight: FontWeight.Bold, italics: true) }, } }, new OsuSpriteText { - Text = new RomanisableString(SetInfo.Metadata.Artist, SetInfo.Metadata.ArtistUnicode), + Text = new RomanisableString(SetInfo.Metadata.ArtistUnicode, SetInfo.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.Bold, italics: true) }, } diff --git a/osu.Game/Overlays/Music/PlaylistItem.cs b/osu.Game/Overlays/Music/PlaylistItem.cs index dab9bc9629..571b14428e 100644 --- a/osu.Game/Overlays/Music/PlaylistItem.cs +++ b/osu.Game/Overlays/Music/PlaylistItem.cs @@ -48,8 +48,8 @@ namespace osu.Game.Overlays.Music artistColour = colours.Gray9; HandleColour = colours.Gray5; - title = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.Title, Model.Metadata.TitleUnicode)); - artist = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.Artist, Model.Metadata.ArtistUnicode)); + title = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.TitleUnicode, Model.Metadata.Title)); + artist = localisation.GetLocalisedString(new RomanisableString(Model.Metadata.ArtistUnicode, Model.Metadata.Artist)); } protected override void LoadComplete() diff --git a/osu.Game/Overlays/NowPlayingOverlay.cs b/osu.Game/Overlays/NowPlayingOverlay.cs index 9c17392e25..81bf71cdec 100644 --- a/osu.Game/Overlays/NowPlayingOverlay.cs +++ b/osu.Game/Overlays/NowPlayingOverlay.cs @@ -293,8 +293,8 @@ namespace osu.Game.Overlays else { BeatmapMetadata metadata = beatmap.Metadata; - title.Text = new RomanisableString(metadata.Title, metadata.TitleUnicode); - artist.Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode); + title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title); + artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); } }); diff --git a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs index 48a0481b9e..20e40569e8 100644 --- a/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs +++ b/osu.Game/Overlays/Profile/Sections/Historical/DrawableMostPlayedBeatmap.cs @@ -130,13 +130,13 @@ namespace osu.Game.Overlays.Profile.Sections.Historical new OsuSpriteText { Text = new RomanisableString( - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] ", - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] "), + $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} [{beatmap.Version}] ", + $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} [{beatmap.Version}] "), Font = OsuFont.GetFont(weight: FontWeight.Bold) }, new OsuSpriteText { - Text = "by " + new RomanisableString(beatmap.Metadata.Artist, beatmap.Metadata.ArtistUnicode), + Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.Regular) }, }; diff --git a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs index ca9e19cd56..713303285a 100644 --- a/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs +++ b/osu.Game/Overlays/Profile/Sections/Ranks/DrawableProfileScore.cs @@ -257,15 +257,15 @@ namespace osu.Game.Overlays.Profile.Sections.Ranks Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, Text = new RomanisableString( - $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} ", - $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} "), + $"{beatmap.Metadata.TitleUnicode ?? beatmap.Metadata.Title} ", + $"{beatmap.Metadata.Title ?? beatmap.Metadata.TitleUnicode} "), Font = OsuFont.GetFont(size: 14, weight: FontWeight.SemiBold, italics: true) }, new OsuSpriteText { Anchor = Anchor.BottomLeft, Origin = Anchor.BottomLeft, - Text = "by " + new RomanisableString(beatmap.Metadata.Artist, beatmap.Metadata.ArtistUnicode), + Text = "by " + new RomanisableString(beatmap.Metadata.ArtistUnicode, beatmap.Metadata.Artist), Font = OsuFont.GetFont(size: 12, italics: true) }, }; diff --git a/osu.Game/Screens/Menu/SongTicker.cs b/osu.Game/Screens/Menu/SongTicker.cs index 2be446d71a..237fe43168 100644 --- a/osu.Game/Screens/Menu/SongTicker.cs +++ b/osu.Game/Screens/Menu/SongTicker.cs @@ -61,8 +61,8 @@ namespace osu.Game.Screens.Menu { var metadata = beatmap.Value.Metadata; - title.Text = new RomanisableString(metadata.Title, metadata.TitleUnicode); - artist.Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode); + title.Text = new RomanisableString(metadata.TitleUnicode, metadata.Title); + artist.Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist); this.FadeInFromZero(fade_duration / 2f) .Delay(4000) diff --git a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs index 299e3e3768..e5a5e35897 100644 --- a/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs +++ b/osu.Game/Screens/OnlinePlay/Components/BeatmapTitle.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.OnlinePlay.Components { new OsuSpriteText { - Text = new RomanisableString(beatmap.Value.Metadata.Artist, beatmap.Value.Metadata.ArtistUnicode), + Text = new RomanisableString(beatmap.Value.Metadata.ArtistUnicode, beatmap.Value.Metadata.Artist), Font = OsuFont.GetFont(size: TextSize), }, new OsuSpriteText @@ -83,7 +83,7 @@ namespace osu.Game.Screens.OnlinePlay.Components }, new OsuSpriteText { - Text = new RomanisableString(beatmap.Value.Metadata.Title, beatmap.Value.Metadata.TitleUnicode), + Text = new RomanisableString(beatmap.Value.Metadata.TitleUnicode, beatmap.Value.Metadata.Title), Font = OsuFont.GetFont(size: TextSize), } }, LinkAction.OpenBeatmap, beatmap.Value.OnlineBeatmapID.ToString(), "Open beatmap"); diff --git a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs index 0779a9c637..c56344a8fb 100644 --- a/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs +++ b/osu.Game/Screens/Play/BeatmapMetadataDisplay.cs @@ -73,7 +73,7 @@ namespace osu.Game.Screens.Play }), new OsuSpriteText { - Text = new RomanisableString(metadata.Title, metadata.TitleUnicode), + Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), Font = OsuFont.GetFont(size: 36, italics: true), Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, @@ -81,7 +81,7 @@ namespace osu.Game.Screens.Play }, new OsuSpriteText { - Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode), + Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), Font = OsuFont.GetFont(size: 26, italics: true), Origin = Anchor.TopCentre, Anchor = Anchor.TopCentre, diff --git a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs index 234e4f2023..6a6b39b61c 100644 --- a/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs +++ b/osu.Game/Screens/Ranking/Expanded/ExpandedPanelMiddleContent.cs @@ -101,7 +101,7 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new RomanisableString(metadata.Title, metadata.TitleUnicode), + Text = new RomanisableString(metadata.TitleUnicode, metadata.Title), Font = OsuFont.Torus.With(size: 20, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, Truncate = true, @@ -110,7 +110,7 @@ namespace osu.Game.Screens.Ranking.Expanded { Anchor = Anchor.TopCentre, Origin = Anchor.TopCentre, - Text = new RomanisableString(metadata.Artist, metadata.ArtistUnicode), + Text = new RomanisableString(metadata.ArtistUnicode, metadata.Artist), Font = OsuFont.Torus.With(size: 14, weight: FontWeight.SemiBold), MaxWidth = ScorePanel.EXPANDED_WIDTH - padding * 2, Truncate = true, diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 0c5b67026c..1c1623e334 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -187,8 +187,8 @@ namespace osu.Game.Screens.Select RelativeSizeAxes = Axes.Both; - titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.Title, metadata.TitleUnicode)); - artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.Artist, metadata.ArtistUnicode)); + titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); + artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist)); Children = new Drawable[] { diff --git a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs index 0e99a4ce70..23a02547b2 100644 --- a/osu.Game/Screens/Select/Carousel/SetPanelContent.cs +++ b/osu.Game/Screens/Select/Carousel/SetPanelContent.cs @@ -41,13 +41,13 @@ namespace osu.Game.Screens.Select.Carousel { new OsuSpriteText { - Text = new RomanisableString(beatmapSet.Metadata.Title, beatmapSet.Metadata.TitleUnicode), + Text = new RomanisableString(beatmapSet.Metadata.TitleUnicode, beatmapSet.Metadata.Title), Font = OsuFont.GetFont(weight: FontWeight.Bold, size: 22, italics: true), Shadow = true, }, new OsuSpriteText { - Text = new RomanisableString(beatmapSet.Metadata.Artist, beatmapSet.Metadata.ArtistUnicode), + Text = new RomanisableString(beatmapSet.Metadata.ArtistUnicode, beatmapSet.Metadata.Artist), Font = OsuFont.GetFont(weight: FontWeight.SemiBold, size: 17, italics: true), Shadow = true, }, From a08a3d44c796bafb3fac49b8612869b3f8131bd8 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 14:51:23 +0900 Subject: [PATCH 6758/6909] Add failing test coverage for using hotkeys from main menu before toolbar displayed --- .../Navigation/TestSceneScreenNavigation.cs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index 5d070b424a..fc49517cdf 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -214,6 +214,21 @@ namespace osu.Game.Tests.Visual.Navigation AddAssert("Options overlay still visible", () => songSelect.BeatmapOptionsOverlay.State.Value == Visibility.Visible); } + [Test] + public void TestSettingsViaHotkeyFromMainMenu() + { + AddAssert("toolbar not displayed", () => Game.Toolbar.State.Value == Visibility.Hidden); + + AddStep("press settings hotkey", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.O); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + AddUntilStep("settings displayed", () => Game.Settings.State.Value == Visibility.Visible); + } + private void pushEscape() => AddStep("Press escape", () => InputManager.Key(Key.Escape)); From 2c8e62ae3589a28fd00dd68a72c20e38f53ca60b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 14:52:51 +0900 Subject: [PATCH 6759/6909] Fix toolbar not completing enough of layout to propagate hotkeys to buttons before initial display --- osu.Game/Overlays/Toolbar/Toolbar.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 393e349bd0..0ccb22df3a 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -37,6 +37,15 @@ namespace osu.Game.Overlays.Toolbar { RelativeSizeAxes = Axes.X; Size = new Vector2(1, HEIGHT); + AlwaysPresent = true; + } + + protected override void UpdateAfterChildren() + { + base.UpdateAfterChildren(); + + // this only needed to be set for the initial LoadComplete/Update, so layout completes and gets buttons in a state they can correctly handle keyboard input for hotkeys. + AlwaysPresent = false; } [BackgroundDependencyLoader(true)] From 154dc03a8c9f3c0eb4b880cc9c25f0baaf0939a0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Feb 2021 15:31:50 +0900 Subject: [PATCH 6760/6909] Update analyser package --- Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Build.props b/Directory.Build.props index 2e1873a9ed..53ad973e47 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -18,7 +18,7 @@ - + $(MSBuildThisFileDirectory)CodeAnalysis\osu.ruleset From 996b6a1e57c639617fa4f6d897b75dd3fbe47845 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Feb 2021 15:38:43 +0900 Subject: [PATCH 6761/6909] Add Enum.HasFlag to banned symbols --- .editorconfig | 5 ++++- CodeAnalysis/BannedSymbols.txt | 1 + 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index a5f7795882..0cdf3b92d3 100644 --- a/.editorconfig +++ b/.editorconfig @@ -194,4 +194,7 @@ dotnet_diagnostic.IDE0068.severity = none dotnet_diagnostic.IDE0069.severity = none #Disable operator overloads requiring alternate named methods -dotnet_diagnostic.CA2225.severity = none \ No newline at end of file +dotnet_diagnostic.CA2225.severity = none + +# Banned APIs +dotnet_diagnostic.RS0030.severity = error \ No newline at end of file diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 47839608c9..60cce39176 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -7,3 +7,4 @@ M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods. +M:System.Enum.HasFlagFast(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead. \ No newline at end of file From dff1d80f3943c705cb4dfdb8b67123b5e80e1592 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Feb 2021 15:38:56 +0900 Subject: [PATCH 6762/6909] Update HasFlag usages to HasFlagFast --- osu.Game.Rulesets.Catch/CatchRuleset.cs | 27 +++++----- .../TestSceneNotes.cs | 3 +- .../Legacy/DistanceObjectPatternGenerator.cs | 17 ++++--- .../Legacy/HitObjectPatternGenerator.cs | 33 ++++++------ osu.Game.Rulesets.Mania/ManiaRuleset.cs | 51 ++++++++++--------- osu.Game.Rulesets.Osu/OsuRuleset.cs | 35 ++++++------- osu.Game.Rulesets.Taiko/TaikoRuleset.cs | 29 ++++++----- .../Beatmaps/Formats/LegacyBeatmapDecoder.cs | 5 +- osu.Game/Graphics/UserInterface/BarGraph.cs | 9 ++-- .../Graphics/UserInterface/TwoLayerButton.cs | 11 ++-- osu.Game/Overlays/Toolbar/ToolbarButton.cs | 5 +- osu.Game/Replays/Legacy/LegacyReplayFrame.cs | 9 ++-- .../Objects/Legacy/ConvertHitObjectParser.cs | 19 +++---- .../Drawables/DrawableStoryboardAnimation.cs | 9 ++-- .../Drawables/DrawableStoryboardSprite.cs | 9 ++-- 15 files changed, 143 insertions(+), 128 deletions(-) diff --git a/osu.Game.Rulesets.Catch/CatchRuleset.cs b/osu.Game.Rulesets.Catch/CatchRuleset.cs index 0a817eca0d..f4ddbd3021 100644 --- a/osu.Game.Rulesets.Catch/CatchRuleset.cs +++ b/osu.Game.Rulesets.Catch/CatchRuleset.cs @@ -21,6 +21,7 @@ using osu.Game.Rulesets.Difficulty; using osu.Game.Rulesets.Scoring; using osu.Game.Scoring; using System; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Rulesets.Catch.Skinning.Legacy; using osu.Game.Skinning; @@ -50,40 +51,40 @@ namespace osu.Game.Rulesets.Catch public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlag(LegacyMods.Nightcore)) + if (mods.HasFlagFast(LegacyMods.Nightcore)) yield return new CatchModNightcore(); - else if (mods.HasFlag(LegacyMods.DoubleTime)) + else if (mods.HasFlagFast(LegacyMods.DoubleTime)) yield return new CatchModDoubleTime(); - if (mods.HasFlag(LegacyMods.Perfect)) + if (mods.HasFlagFast(LegacyMods.Perfect)) yield return new CatchModPerfect(); - else if (mods.HasFlag(LegacyMods.SuddenDeath)) + else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) yield return new CatchModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Cinema)) + if (mods.HasFlagFast(LegacyMods.Cinema)) yield return new CatchModCinema(); - else if (mods.HasFlag(LegacyMods.Autoplay)) + else if (mods.HasFlagFast(LegacyMods.Autoplay)) yield return new CatchModAutoplay(); - if (mods.HasFlag(LegacyMods.Easy)) + if (mods.HasFlagFast(LegacyMods.Easy)) yield return new CatchModEasy(); - if (mods.HasFlag(LegacyMods.Flashlight)) + if (mods.HasFlagFast(LegacyMods.Flashlight)) yield return new CatchModFlashlight(); - if (mods.HasFlag(LegacyMods.HalfTime)) + if (mods.HasFlagFast(LegacyMods.HalfTime)) yield return new CatchModHalfTime(); - if (mods.HasFlag(LegacyMods.HardRock)) + if (mods.HasFlagFast(LegacyMods.HardRock)) yield return new CatchModHardRock(); - if (mods.HasFlag(LegacyMods.Hidden)) + if (mods.HasFlagFast(LegacyMods.Hidden)) yield return new CatchModHidden(); - if (mods.HasFlag(LegacyMods.NoFail)) + if (mods.HasFlagFast(LegacyMods.NoFail)) yield return new CatchModNoFail(); - if (mods.HasFlag(LegacyMods.Relax)) + if (mods.HasFlagFast(LegacyMods.Relax)) yield return new CatchModRelax(); } diff --git a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs index 6b8f5d5d9d..706268e478 100644 --- a/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs +++ b/osu.Game.Rulesets.Mania.Tests/TestSceneNotes.cs @@ -6,6 +6,7 @@ using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Shapes; @@ -97,7 +98,7 @@ namespace osu.Game.Rulesets.Mania.Tests } private bool verifyAnchors(DrawableHitObject hitObject, Anchor expectedAnchor) - => hitObject.Anchor.HasFlag(expectedAnchor) && hitObject.Origin.HasFlag(expectedAnchor); + => hitObject.Anchor.HasFlagFast(expectedAnchor) && hitObject.Origin.HasFlagFast(expectedAnchor); private bool verifyAnchors(DrawableHoldNote holdNote, Anchor expectedAnchor) => verifyAnchors((DrawableHitObject)holdNote, expectedAnchor) && holdNote.NestedHitObjects.All(n => verifyAnchors(n, expectedAnchor)); diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs index 30d33de06e..c81710ed18 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/DistanceObjectPatternGenerator.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Audio; using osu.Game.Beatmaps; using osu.Game.Rulesets.Mania.MathUtils; @@ -141,7 +142,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 6.5) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.78, 0.3, 0); return generateNRandomNotes(StartTime, 0.85, 0.36, 0.03); @@ -149,7 +150,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 4) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.43, 0.08, 0); return generateNRandomNotes(StartTime, 0.56, 0.18, 0); @@ -157,13 +158,13 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 2.5) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.3, 0, 0); return generateNRandomNotes(StartTime, 0.37, 0.08, 0); } - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateNRandomNotes(StartTime, 0.17, 0, 0); return generateNRandomNotes(StartTime, 0.27, 0, 0); @@ -221,7 +222,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); int lastColumn = nextColumn; @@ -373,7 +374,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy static bool isDoubleSample(HitSampleInfo sample) => sample.Name == HitSampleInfo.HIT_CLAP || sample.Name == HitSampleInfo.HIT_FINISH; - bool canGenerateTwoNotes = !convertType.HasFlag(PatternType.LowProbability); + bool canGenerateTwoNotes = !convertType.HasFlagFast(PatternType.LowProbability); canGenerateTwoNotes &= HitObject.Samples.Any(isDoubleSample) || sampleInfoListAt(StartTime).Any(isDoubleSample); if (canGenerateTwoNotes) @@ -406,7 +407,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int endTime = startTime + SegmentDuration * SpanCount; int nextColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) nextColumn = FindAvailableColumn(nextColumn, PreviousPattern); for (int i = 0; i < columnRepeat; i++) @@ -435,7 +436,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy var pattern = new Pattern(); int holdColumn = GetColumn((HitObject as IHasXPosition)?.X ?? 0, true); - if (convertType.HasFlag(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) + if (convertType.HasFlagFast(PatternType.ForceNotStack) && PreviousPattern.ColumnWithObjects < TotalColumns) holdColumn = FindAvailableColumn(holdColumn, PreviousPattern); // Create the hold note diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index bc4ab55767..8e9020ee13 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osuTK; using osu.Game.Audio; using osu.Game.Beatmaps; @@ -78,7 +79,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy else convertType |= PatternType.LowProbability; - if (!convertType.HasFlag(PatternType.KeepSingle)) + if (!convertType.HasFlagFast(PatternType.KeepSingle)) { if (HitObject.Samples.Any(s => s.Name == HitSampleInfo.HIT_FINISH) && TotalColumns != 8) convertType |= PatternType.Mirror; @@ -101,7 +102,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int lastColumn = PreviousPattern.HitObjects.FirstOrDefault()?.Column ?? 0; - if (convertType.HasFlag(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) + if (convertType.HasFlagFast(PatternType.Reverse) && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by copying the last hit objects in reverse-column order for (int i = RandomStart; i < TotalColumns; i++) @@ -113,7 +114,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlag(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 + if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 // If we convert to 7K + 1, let's not overload the special key && (TotalColumns != 8 || lastColumn != 0) // Make sure the last column was not the centre column @@ -126,7 +127,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlag(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) + if (convertType.HasFlagFast(PatternType.ForceStack) && PreviousPattern.HitObjects.Any()) { // Generate a new pattern by placing on the already filled columns for (int i = RandomStart; i < TotalColumns; i++) @@ -140,7 +141,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (PreviousPattern.HitObjects.Count() == 1) { - if (convertType.HasFlag(PatternType.Stair)) + if (convertType.HasFlagFast(PatternType.Stair)) { // Generate a new pattern by placing on the next column, cycling back to the start if there is no "next" int targetColumn = lastColumn + 1; @@ -151,7 +152,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy return pattern; } - if (convertType.HasFlag(PatternType.ReverseStair)) + if (convertType.HasFlagFast(PatternType.ReverseStair)) { // Generate a new pattern by placing on the previous column, cycling back to the end if there is no "previous" int targetColumn = lastColumn - 1; @@ -163,10 +164,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } } - if (convertType.HasFlag(PatternType.KeepSingle)) + if (convertType.HasFlagFast(PatternType.KeepSingle)) return generateRandomNotes(1); - if (convertType.HasFlag(PatternType.Mirror)) + if (convertType.HasFlagFast(PatternType.Mirror)) { if (ConversionDifficulty > 6.5) return generateRandomPatternWithMirrored(0.12, 0.38, 0.12); @@ -178,7 +179,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 6.5) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateRandomPattern(0.78, 0.42, 0, 0); return generateRandomPattern(1, 0.62, 0, 0); @@ -186,7 +187,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 4) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateRandomPattern(0.35, 0.08, 0, 0); return generateRandomPattern(0.52, 0.15, 0, 0); @@ -194,7 +195,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy if (ConversionDifficulty > 2) { - if (convertType.HasFlag(PatternType.LowProbability)) + if (convertType.HasFlagFast(PatternType.LowProbability)) return generateRandomPattern(0.18, 0, 0, 0); return generateRandomPattern(0.45, 0, 0, 0); @@ -207,9 +208,9 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy foreach (var obj in p.HitObjects) { - if (convertType.HasFlag(PatternType.Stair) && obj.Column == TotalColumns - 1) + if (convertType.HasFlagFast(PatternType.Stair) && obj.Column == TotalColumns - 1) StairType = PatternType.ReverseStair; - if (convertType.HasFlag(PatternType.ReverseStair) && obj.Column == RandomStart) + if (convertType.HasFlagFast(PatternType.ReverseStair) && obj.Column == RandomStart) StairType = PatternType.Stair; } @@ -229,7 +230,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy { var pattern = new Pattern(); - bool allowStacking = !convertType.HasFlag(PatternType.ForceNotStack); + bool allowStacking = !convertType.HasFlagFast(PatternType.ForceNotStack); if (!allowStacking) noteCount = Math.Min(noteCount, TotalColumns - RandomStart - PreviousPattern.ColumnWithObjects); @@ -249,7 +250,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy int getNextColumn(int last) { - if (convertType.HasFlag(PatternType.Gathered)) + if (convertType.HasFlagFast(PatternType.Gathered)) { last++; if (last == TotalColumns) @@ -296,7 +297,7 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy /// The containing the hit objects. private Pattern generateRandomPatternWithMirrored(double centreProbability, double p2, double p3) { - if (convertType.HasFlag(PatternType.ForceNotStack)) + if (convertType.HasFlagFast(PatternType.ForceNotStack)) return generateRandomPattern(1 / 2f + p2 / 2, p2, (p2 + p3) / 2, p3); var pattern = new Pattern(); diff --git a/osu.Game.Rulesets.Mania/ManiaRuleset.cs b/osu.Game.Rulesets.Mania/ManiaRuleset.cs index 4c729fef83..d624e094ad 100644 --- a/osu.Game.Rulesets.Mania/ManiaRuleset.cs +++ b/osu.Game.Rulesets.Mania/ManiaRuleset.cs @@ -9,6 +9,7 @@ using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Sprites; using osu.Framework.Input.Bindings; @@ -59,76 +60,76 @@ namespace osu.Game.Rulesets.Mania public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlag(LegacyMods.Nightcore)) + if (mods.HasFlagFast(LegacyMods.Nightcore)) yield return new ManiaModNightcore(); - else if (mods.HasFlag(LegacyMods.DoubleTime)) + else if (mods.HasFlagFast(LegacyMods.DoubleTime)) yield return new ManiaModDoubleTime(); - if (mods.HasFlag(LegacyMods.Perfect)) + if (mods.HasFlagFast(LegacyMods.Perfect)) yield return new ManiaModPerfect(); - else if (mods.HasFlag(LegacyMods.SuddenDeath)) + else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) yield return new ManiaModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Cinema)) + if (mods.HasFlagFast(LegacyMods.Cinema)) yield return new ManiaModCinema(); - else if (mods.HasFlag(LegacyMods.Autoplay)) + else if (mods.HasFlagFast(LegacyMods.Autoplay)) yield return new ManiaModAutoplay(); - if (mods.HasFlag(LegacyMods.Easy)) + if (mods.HasFlagFast(LegacyMods.Easy)) yield return new ManiaModEasy(); - if (mods.HasFlag(LegacyMods.FadeIn)) + if (mods.HasFlagFast(LegacyMods.FadeIn)) yield return new ManiaModFadeIn(); - if (mods.HasFlag(LegacyMods.Flashlight)) + if (mods.HasFlagFast(LegacyMods.Flashlight)) yield return new ManiaModFlashlight(); - if (mods.HasFlag(LegacyMods.HalfTime)) + if (mods.HasFlagFast(LegacyMods.HalfTime)) yield return new ManiaModHalfTime(); - if (mods.HasFlag(LegacyMods.HardRock)) + if (mods.HasFlagFast(LegacyMods.HardRock)) yield return new ManiaModHardRock(); - if (mods.HasFlag(LegacyMods.Hidden)) + if (mods.HasFlagFast(LegacyMods.Hidden)) yield return new ManiaModHidden(); - if (mods.HasFlag(LegacyMods.Key1)) + if (mods.HasFlagFast(LegacyMods.Key1)) yield return new ManiaModKey1(); - if (mods.HasFlag(LegacyMods.Key2)) + if (mods.HasFlagFast(LegacyMods.Key2)) yield return new ManiaModKey2(); - if (mods.HasFlag(LegacyMods.Key3)) + if (mods.HasFlagFast(LegacyMods.Key3)) yield return new ManiaModKey3(); - if (mods.HasFlag(LegacyMods.Key4)) + if (mods.HasFlagFast(LegacyMods.Key4)) yield return new ManiaModKey4(); - if (mods.HasFlag(LegacyMods.Key5)) + if (mods.HasFlagFast(LegacyMods.Key5)) yield return new ManiaModKey5(); - if (mods.HasFlag(LegacyMods.Key6)) + if (mods.HasFlagFast(LegacyMods.Key6)) yield return new ManiaModKey6(); - if (mods.HasFlag(LegacyMods.Key7)) + if (mods.HasFlagFast(LegacyMods.Key7)) yield return new ManiaModKey7(); - if (mods.HasFlag(LegacyMods.Key8)) + if (mods.HasFlagFast(LegacyMods.Key8)) yield return new ManiaModKey8(); - if (mods.HasFlag(LegacyMods.Key9)) + if (mods.HasFlagFast(LegacyMods.Key9)) yield return new ManiaModKey9(); - if (mods.HasFlag(LegacyMods.KeyCoop)) + if (mods.HasFlagFast(LegacyMods.KeyCoop)) yield return new ManiaModDualStages(); - if (mods.HasFlag(LegacyMods.NoFail)) + if (mods.HasFlagFast(LegacyMods.NoFail)) yield return new ManiaModNoFail(); - if (mods.HasFlag(LegacyMods.Random)) + if (mods.HasFlagFast(LegacyMods.Random)) yield return new ManiaModRandom(); - if (mods.HasFlag(LegacyMods.Mirror)) + if (mods.HasFlagFast(LegacyMods.Mirror)) yield return new ManiaModMirror(); } diff --git a/osu.Game.Rulesets.Osu/OsuRuleset.cs b/osu.Game.Rulesets.Osu/OsuRuleset.cs index 18324a18a8..838d707d64 100644 --- a/osu.Game.Rulesets.Osu/OsuRuleset.cs +++ b/osu.Game.Rulesets.Osu/OsuRuleset.cs @@ -29,6 +29,7 @@ using osu.Game.Scoring; using osu.Game.Skinning; using System; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Rulesets.Osu.Objects; using osu.Game.Rulesets.Osu.Skinning.Legacy; using osu.Game.Rulesets.Osu.Statistics; @@ -58,52 +59,52 @@ namespace osu.Game.Rulesets.Osu public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlag(LegacyMods.Nightcore)) + if (mods.HasFlagFast(LegacyMods.Nightcore)) yield return new OsuModNightcore(); - else if (mods.HasFlag(LegacyMods.DoubleTime)) + else if (mods.HasFlagFast(LegacyMods.DoubleTime)) yield return new OsuModDoubleTime(); - if (mods.HasFlag(LegacyMods.Perfect)) + if (mods.HasFlagFast(LegacyMods.Perfect)) yield return new OsuModPerfect(); - else if (mods.HasFlag(LegacyMods.SuddenDeath)) + else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) yield return new OsuModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Autopilot)) + if (mods.HasFlagFast(LegacyMods.Autopilot)) yield return new OsuModAutopilot(); - if (mods.HasFlag(LegacyMods.Cinema)) + if (mods.HasFlagFast(LegacyMods.Cinema)) yield return new OsuModCinema(); - else if (mods.HasFlag(LegacyMods.Autoplay)) + else if (mods.HasFlagFast(LegacyMods.Autoplay)) yield return new OsuModAutoplay(); - if (mods.HasFlag(LegacyMods.Easy)) + if (mods.HasFlagFast(LegacyMods.Easy)) yield return new OsuModEasy(); - if (mods.HasFlag(LegacyMods.Flashlight)) + if (mods.HasFlagFast(LegacyMods.Flashlight)) yield return new OsuModFlashlight(); - if (mods.HasFlag(LegacyMods.HalfTime)) + if (mods.HasFlagFast(LegacyMods.HalfTime)) yield return new OsuModHalfTime(); - if (mods.HasFlag(LegacyMods.HardRock)) + if (mods.HasFlagFast(LegacyMods.HardRock)) yield return new OsuModHardRock(); - if (mods.HasFlag(LegacyMods.Hidden)) + if (mods.HasFlagFast(LegacyMods.Hidden)) yield return new OsuModHidden(); - if (mods.HasFlag(LegacyMods.NoFail)) + if (mods.HasFlagFast(LegacyMods.NoFail)) yield return new OsuModNoFail(); - if (mods.HasFlag(LegacyMods.Relax)) + if (mods.HasFlagFast(LegacyMods.Relax)) yield return new OsuModRelax(); - if (mods.HasFlag(LegacyMods.SpunOut)) + if (mods.HasFlagFast(LegacyMods.SpunOut)) yield return new OsuModSpunOut(); - if (mods.HasFlag(LegacyMods.Target)) + if (mods.HasFlagFast(LegacyMods.Target)) yield return new OsuModTarget(); - if (mods.HasFlag(LegacyMods.TouchDevice)) + if (mods.HasFlagFast(LegacyMods.TouchDevice)) yield return new OsuModTouchDevice(); } diff --git a/osu.Game.Rulesets.Taiko/TaikoRuleset.cs b/osu.Game.Rulesets.Taiko/TaikoRuleset.cs index f2b5d195b4..56f58f404b 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.Extensions.EnumExtensions; using osu.Game.Rulesets.Edit; using osu.Game.Rulesets.Taiko.Edit; using osu.Game.Rulesets.Taiko.Objects; @@ -57,43 +58,43 @@ namespace osu.Game.Rulesets.Taiko public override IEnumerable ConvertFromLegacyMods(LegacyMods mods) { - if (mods.HasFlag(LegacyMods.Nightcore)) + if (mods.HasFlagFast(LegacyMods.Nightcore)) yield return new TaikoModNightcore(); - else if (mods.HasFlag(LegacyMods.DoubleTime)) + else if (mods.HasFlagFast(LegacyMods.DoubleTime)) yield return new TaikoModDoubleTime(); - if (mods.HasFlag(LegacyMods.Perfect)) + if (mods.HasFlagFast(LegacyMods.Perfect)) yield return new TaikoModPerfect(); - else if (mods.HasFlag(LegacyMods.SuddenDeath)) + else if (mods.HasFlagFast(LegacyMods.SuddenDeath)) yield return new TaikoModSuddenDeath(); - if (mods.HasFlag(LegacyMods.Cinema)) + if (mods.HasFlagFast(LegacyMods.Cinema)) yield return new TaikoModCinema(); - else if (mods.HasFlag(LegacyMods.Autoplay)) + else if (mods.HasFlagFast(LegacyMods.Autoplay)) yield return new TaikoModAutoplay(); - if (mods.HasFlag(LegacyMods.Easy)) + if (mods.HasFlagFast(LegacyMods.Easy)) yield return new TaikoModEasy(); - if (mods.HasFlag(LegacyMods.Flashlight)) + if (mods.HasFlagFast(LegacyMods.Flashlight)) yield return new TaikoModFlashlight(); - if (mods.HasFlag(LegacyMods.HalfTime)) + if (mods.HasFlagFast(LegacyMods.HalfTime)) yield return new TaikoModHalfTime(); - if (mods.HasFlag(LegacyMods.HardRock)) + if (mods.HasFlagFast(LegacyMods.HardRock)) yield return new TaikoModHardRock(); - if (mods.HasFlag(LegacyMods.Hidden)) + if (mods.HasFlagFast(LegacyMods.Hidden)) yield return new TaikoModHidden(); - if (mods.HasFlag(LegacyMods.NoFail)) + if (mods.HasFlagFast(LegacyMods.NoFail)) yield return new TaikoModNoFail(); - if (mods.HasFlag(LegacyMods.Relax)) + if (mods.HasFlagFast(LegacyMods.Relax)) yield return new TaikoModRelax(); - if (mods.HasFlag(LegacyMods.Random)) + if (mods.HasFlagFast(LegacyMods.Random)) yield return new TaikoModRandom(); } diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs index 37ab489da5..99dffa7041 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapDecoder.cs @@ -6,6 +6,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; using osu.Framework.Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Beatmaps.Legacy; using osu.Game.Beatmaps.Timing; @@ -348,8 +349,8 @@ namespace osu.Game.Beatmaps.Formats if (split.Length >= 8) { LegacyEffectFlags effectFlags = (LegacyEffectFlags)Parsing.ParseInt(split[7]); - kiaiMode = effectFlags.HasFlag(LegacyEffectFlags.Kiai); - omitFirstBarSignature = effectFlags.HasFlag(LegacyEffectFlags.OmitFirstBarLine); + kiaiMode = effectFlags.HasFlagFast(LegacyEffectFlags.Kiai); + omitFirstBarSignature = effectFlags.HasFlagFast(LegacyEffectFlags.OmitFirstBarLine); } string stringSampleSet = sampleSet.ToString().ToLowerInvariant(); diff --git a/osu.Game/Graphics/UserInterface/BarGraph.cs b/osu.Game/Graphics/UserInterface/BarGraph.cs index 953f3985f9..407bf6a923 100644 --- a/osu.Game/Graphics/UserInterface/BarGraph.cs +++ b/osu.Game/Graphics/UserInterface/BarGraph.cs @@ -6,6 +6,7 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using System.Collections.Generic; using System.Linq; +using osu.Framework.Extensions.EnumExtensions; namespace osu.Game.Graphics.UserInterface { @@ -24,11 +25,11 @@ namespace osu.Game.Graphics.UserInterface set { direction = value; - base.Direction = direction.HasFlag(BarDirection.Horizontal) ? FillDirection.Vertical : FillDirection.Horizontal; + base.Direction = direction.HasFlagFast(BarDirection.Horizontal) ? FillDirection.Vertical : FillDirection.Horizontal; foreach (var bar in Children) { - bar.Size = direction.HasFlag(BarDirection.Horizontal) ? new Vector2(1, 1.0f / Children.Count) : new Vector2(1.0f / Children.Count, 1); + bar.Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, 1.0f / Children.Count) : new Vector2(1.0f / Children.Count, 1); bar.Direction = direction; } } @@ -56,14 +57,14 @@ namespace osu.Game.Graphics.UserInterface if (bar.Bar != null) { bar.Bar.Length = length; - bar.Bar.Size = direction.HasFlag(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1); + bar.Bar.Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1); } else { Add(new Bar { RelativeSizeAxes = Axes.Both, - Size = direction.HasFlag(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1), + Size = direction.HasFlagFast(BarDirection.Horizontal) ? new Vector2(1, size) : new Vector2(size, 1), Length = length, Direction = Direction, }); diff --git a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs index 120149d8c1..8f03c7073c 100644 --- a/osu.Game/Graphics/UserInterface/TwoLayerButton.cs +++ b/osu.Game/Graphics/UserInterface/TwoLayerButton.cs @@ -12,6 +12,7 @@ using osu.Game.Graphics.Containers; using osu.Game.Beatmaps.ControlPoints; using osu.Framework.Audio.Track; using System; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Shapes; using osu.Framework.Input.Events; @@ -56,15 +57,15 @@ namespace osu.Game.Graphics.UserInterface set { base.Origin = value; - c1.Origin = c1.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight; - c2.Origin = c2.Anchor = value.HasFlag(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft; + c1.Origin = c1.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopLeft : Anchor.TopRight; + c2.Origin = c2.Anchor = value.HasFlagFast(Anchor.x2) ? Anchor.TopRight : Anchor.TopLeft; - X = value.HasFlag(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; + X = value.HasFlagFast(Anchor.x2) ? SIZE_RETRACTED.X * shear.X * 0.5f : 0; Remove(c1); Remove(c2); - c1.Depth = value.HasFlag(Anchor.x2) ? 0 : 1; - c2.Depth = value.HasFlag(Anchor.x2) ? 1 : 0; + c1.Depth = value.HasFlagFast(Anchor.x2) ? 0 : 1; + c2.Depth = value.HasFlagFast(Anchor.x2) ? 1 : 0; Add(c1); Add(c2); } diff --git a/osu.Game/Overlays/Toolbar/ToolbarButton.cs b/osu.Game/Overlays/Toolbar/ToolbarButton.cs index 83f2bdf6cb..5939f7a42f 100644 --- a/osu.Game/Overlays/Toolbar/ToolbarButton.cs +++ b/osu.Game/Overlays/Toolbar/ToolbarButton.cs @@ -4,6 +4,7 @@ using osu.Framework.Allocation; using osu.Framework.Caching; using osu.Framework.Extensions.Color4Extensions; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Effects; @@ -127,9 +128,9 @@ namespace osu.Game.Overlays.Toolbar { Direction = FillDirection.Vertical, RelativeSizeAxes = Axes.Both, // stops us being considered in parent's autosize - Anchor = TooltipAnchor.HasFlag(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight, + Anchor = TooltipAnchor.HasFlagFast(Anchor.x0) ? Anchor.BottomLeft : Anchor.BottomRight, Origin = TooltipAnchor, - Position = new Vector2(TooltipAnchor.HasFlag(Anchor.x0) ? 5 : -5, 5), + Position = new Vector2(TooltipAnchor.HasFlagFast(Anchor.x0) ? 5 : -5, 5), Alpha = 0, Children = new Drawable[] { diff --git a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs index ab9ccda9b9..f6abf259e8 100644 --- a/osu.Game/Replays/Legacy/LegacyReplayFrame.cs +++ b/osu.Game/Replays/Legacy/LegacyReplayFrame.cs @@ -3,6 +3,7 @@ using MessagePack; using Newtonsoft.Json; +using osu.Framework.Extensions.EnumExtensions; using osu.Game.Rulesets.Replays; using osuTK; @@ -31,19 +32,19 @@ namespace osu.Game.Replays.Legacy [JsonIgnore] [IgnoreMember] - public bool MouseLeft1 => ButtonState.HasFlag(ReplayButtonState.Left1); + public bool MouseLeft1 => ButtonState.HasFlagFast(ReplayButtonState.Left1); [JsonIgnore] [IgnoreMember] - public bool MouseRight1 => ButtonState.HasFlag(ReplayButtonState.Right1); + public bool MouseRight1 => ButtonState.HasFlagFast(ReplayButtonState.Right1); [JsonIgnore] [IgnoreMember] - public bool MouseLeft2 => ButtonState.HasFlag(ReplayButtonState.Left2); + public bool MouseLeft2 => ButtonState.HasFlagFast(ReplayButtonState.Left2); [JsonIgnore] [IgnoreMember] - public bool MouseRight2 => ButtonState.HasFlag(ReplayButtonState.Right2); + public bool MouseRight2 => ButtonState.HasFlagFast(ReplayButtonState.Right2); [Key(3)] public ReplayButtonState ButtonState; diff --git a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs index 72025de131..8419dd66de 100644 --- a/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs +++ b/osu.Game/Rulesets/Objects/Legacy/ConvertHitObjectParser.cs @@ -10,6 +10,7 @@ using osu.Game.Beatmaps.Formats; using osu.Game.Audio; using System.Linq; using JetBrains.Annotations; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Utils; using osu.Game.Beatmaps.Legacy; using osu.Game.Skinning; @@ -54,7 +55,7 @@ namespace osu.Game.Rulesets.Objects.Legacy int comboOffset = (int)(type & LegacyHitObjectType.ComboOffset) >> 4; type &= ~LegacyHitObjectType.ComboOffset; - bool combo = type.HasFlag(LegacyHitObjectType.NewCombo); + bool combo = type.HasFlagFast(LegacyHitObjectType.NewCombo); type &= ~LegacyHitObjectType.NewCombo; var soundType = (LegacyHitSoundType)Parsing.ParseInt(split[4]); @@ -62,14 +63,14 @@ namespace osu.Game.Rulesets.Objects.Legacy HitObject result = null; - if (type.HasFlag(LegacyHitObjectType.Circle)) + if (type.HasFlagFast(LegacyHitObjectType.Circle)) { result = CreateHit(pos, combo, comboOffset); if (split.Length > 5) readCustomSampleBanks(split[5], bankInfo); } - else if (type.HasFlag(LegacyHitObjectType.Slider)) + else if (type.HasFlagFast(LegacyHitObjectType.Slider)) { double? length = null; @@ -141,7 +142,7 @@ namespace osu.Game.Rulesets.Objects.Legacy result = CreateSlider(pos, combo, comboOffset, convertPathString(split[5], pos), length, repeatCount, nodeSamples); } - else if (type.HasFlag(LegacyHitObjectType.Spinner)) + else if (type.HasFlagFast(LegacyHitObjectType.Spinner)) { double duration = Math.Max(0, Parsing.ParseDouble(split[5]) + Offset - startTime); @@ -150,7 +151,7 @@ namespace osu.Game.Rulesets.Objects.Legacy if (split.Length > 6) readCustomSampleBanks(split[6], bankInfo); } - else if (type.HasFlag(LegacyHitObjectType.Hold)) + else if (type.HasFlagFast(LegacyHitObjectType.Hold)) { // Note: Hold is generated by BMS converts @@ -436,16 +437,16 @@ namespace osu.Game.Rulesets.Objects.Legacy new LegacyHitSampleInfo(HitSampleInfo.HIT_NORMAL, bankInfo.Normal, bankInfo.Volume, bankInfo.CustomSampleBank, // if the sound type doesn't have the Normal flag set, attach it anyway as a layered sample. // None also counts as a normal non-layered sample: https://osu.ppy.sh/help/wiki/osu!_File_Formats/Osu_(file_format)#hitsounds - type != LegacyHitSoundType.None && !type.HasFlag(LegacyHitSoundType.Normal)) + type != LegacyHitSoundType.None && !type.HasFlagFast(LegacyHitSoundType.Normal)) }; - if (type.HasFlag(LegacyHitSoundType.Finish)) + if (type.HasFlagFast(LegacyHitSoundType.Finish)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_FINISH, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); - if (type.HasFlag(LegacyHitSoundType.Whistle)) + if (type.HasFlagFast(LegacyHitSoundType.Whistle)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_WHISTLE, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); - if (type.HasFlag(LegacyHitSoundType.Clap)) + if (type.HasFlagFast(LegacyHitSoundType.Clap)) soundTypes.Add(new LegacyHitSampleInfo(HitSampleInfo.HIT_CLAP, bankInfo.Add, bankInfo.Volume, bankInfo.CustomSampleBank)); return soundTypes; diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs index 7eac994e07..81623a9307 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardAnimation.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Animations; using osu.Framework.Graphics.Textures; @@ -80,17 +81,17 @@ namespace osu.Game.Storyboards.Drawables if (FlipH) { - if (origin.HasFlag(Anchor.x0)) + if (origin.HasFlagFast(Anchor.x0)) origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); - else if (origin.HasFlag(Anchor.x2)) + else if (origin.HasFlagFast(Anchor.x2)) origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); } if (FlipV) { - if (origin.HasFlag(Anchor.y0)) + if (origin.HasFlagFast(Anchor.y0)) origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); - else if (origin.HasFlag(Anchor.y2)) + else if (origin.HasFlagFast(Anchor.y2)) origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); } diff --git a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs index 7b1a6d54da..eb877f3dff 100644 --- a/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs +++ b/osu.Game/Storyboards/Drawables/DrawableStoryboardSprite.cs @@ -3,6 +3,7 @@ using System; using osu.Framework.Allocation; +using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Textures; @@ -80,17 +81,17 @@ namespace osu.Game.Storyboards.Drawables if (FlipH) { - if (origin.HasFlag(Anchor.x0)) + if (origin.HasFlagFast(Anchor.x0)) origin = Anchor.x2 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); - else if (origin.HasFlag(Anchor.x2)) + else if (origin.HasFlagFast(Anchor.x2)) origin = Anchor.x0 | (origin & (Anchor.y0 | Anchor.y1 | Anchor.y2)); } if (FlipV) { - if (origin.HasFlag(Anchor.y0)) + if (origin.HasFlagFast(Anchor.y0)) origin = Anchor.y2 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); - else if (origin.HasFlag(Anchor.y2)) + else if (origin.HasFlagFast(Anchor.y2)) origin = Anchor.y0 | (origin & (Anchor.x0 | Anchor.x1 | Anchor.x2)); } From 9f3ceb99eba64a20a600474963b84ac35b256147 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 16:05:08 +0900 Subject: [PATCH 6763/6909] Fix the star rating display at song select flashing to zero when changing mods Due to the use of bindable flow provided by `BeatmapDifficultyCache` in this usage, the display would briefly flash to zero while difficulty calculation was still running (as there is no way for a consumer of the provided bindable to know whether the returned 0 is an actual 0 SR or a "pending" calculation). While I hope to fix this by making the bindable flow return nullable values, I think this particular use case works better with non-bindable flow so have switched across to that. --- osu.Game/Screens/Select/Details/AdvancedStats.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game/Screens/Select/Details/AdvancedStats.cs b/osu.Game/Screens/Select/Details/AdvancedStats.cs index ab4f3f4796..0c2cce0bb1 100644 --- a/osu.Game/Screens/Select/Details/AdvancedStats.cs +++ b/osu.Game/Screens/Select/Details/AdvancedStats.cs @@ -15,6 +15,7 @@ using System.Collections.Generic; using osu.Game.Rulesets.Mods; using System.Linq; using System.Threading; +using System.Threading.Tasks; using osu.Framework.Threading; using osu.Framework.Utils; using osu.Game.Configuration; @@ -137,8 +138,6 @@ namespace osu.Game.Screens.Select.Details updateStarDifficulty(); } - private IBindable normalStarDifficulty; - private IBindable moddedStarDifficulty; private CancellationTokenSource starDifficultyCancellationSource; private void updateStarDifficulty() @@ -150,13 +149,13 @@ namespace osu.Game.Screens.Select.Details starDifficultyCancellationSource = new CancellationTokenSource(); - normalStarDifficulty = difficultyCache.GetBindableDifficulty(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); - moddedStarDifficulty = difficultyCache.GetBindableDifficulty(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); + var normalStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, null, starDifficultyCancellationSource.Token); + var moddedStarDifficulty = difficultyCache.GetDifficultyAsync(Beatmap, ruleset.Value, mods.Value, starDifficultyCancellationSource.Token); - normalStarDifficulty.BindValueChanged(_ => updateDisplay()); - moddedStarDifficulty.BindValueChanged(_ => updateDisplay(), true); - - void updateDisplay() => starDifficulty.Value = ((float)normalStarDifficulty.Value.Stars, (float)moddedStarDifficulty.Value.Stars); + Task.WhenAll(normalStarDifficulty, moddedStarDifficulty).ContinueWith(_ => Schedule(() => + { + starDifficulty.Value = ((float)normalStarDifficulty.Result.Stars, (float)moddedStarDifficulty.Result.Stars); + }), starDifficultyCancellationSource.Token, TaskContinuationOptions.OnlyOnRanToCompletion, TaskScheduler.Current); } protected override void Dispose(bool isDisposing) From dcda7f62dff49a69cd1c0fdc8e38c937a490c02b Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Feb 2021 16:10:27 +0900 Subject: [PATCH 6764/6909] Fix incorrect banned symbol --- CodeAnalysis/BannedSymbols.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CodeAnalysis/BannedSymbols.txt b/CodeAnalysis/BannedSymbols.txt index 60cce39176..46c50dbfa2 100644 --- a/CodeAnalysis/BannedSymbols.txt +++ b/CodeAnalysis/BannedSymbols.txt @@ -7,4 +7,4 @@ M:osu.Framework.Graphics.Sprites.SpriteText.#ctor;Use OsuSpriteText. M:osu.Framework.Bindables.IBindableList`1.GetBoundCopy();Fails on iOS. Use manual ctor + BindTo instead. (see https://github.com/mono/mono/issues/19900) T:Microsoft.EntityFrameworkCore.Internal.EnumerableExtensions;Don't use internal extension methods. T:Microsoft.EntityFrameworkCore.Internal.TypeExtensions;Don't use internal extension methods. -M:System.Enum.HasFlagFast(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead. \ No newline at end of file +M:System.Enum.HasFlag(System.Enum);Use osu.Framework.Extensions.EnumExtensions.HasFlagFast() instead. \ No newline at end of file From 03771ce8ecedc5adcf405e28f7b07531f5da8f1c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 16:19:01 +0900 Subject: [PATCH 6765/6909] Allow determining a BeatmapDifficultyCache's bindable return's completion state via nullability --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 8 ++++---- osu.Game/Beatmaps/Drawables/DifficultyIcon.cs | 8 ++++++-- osu.Game/Scoring/ScoreManager.cs | 8 ++++++-- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 4 ++-- .../Screens/Select/Carousel/DrawableCarouselBeatmap.cs | 7 +++++-- 5 files changed, 23 insertions(+), 12 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 37d262abe5..72a9b36c6f 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -70,7 +70,7 @@ namespace osu.Game.Beatmaps /// The to get the difficulty of. /// An optional which stops updating the star difficulty for the given . /// A bindable that is updated to contain the star difficulty when it becomes available. - public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) + public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) { var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); @@ -91,8 +91,8 @@ namespace osu.Game.Beatmaps /// The s to get the difficulty with. If null, no mods will be assumed. /// An optional which stops updating the star difficulty for the given . /// A bindable that is updated to contain the star difficulty when it becomes available. - public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, - CancellationToken cancellationToken = default) + public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, + CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); /// @@ -313,7 +313,7 @@ namespace osu.Game.Beatmaps } } - private class BindableStarDifficulty : Bindable + private class BindableStarDifficulty : Bindable { public readonly BeatmapInfo Beatmap; public readonly CancellationToken CancellationToken; diff --git a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs index 96e18f120a..c62b803d1a 100644 --- a/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs +++ b/osu.Game/Beatmaps/Drawables/DifficultyIcon.cs @@ -151,7 +151,7 @@ namespace osu.Game.Beatmaps.Drawables this.mods = mods; } - private IBindable localStarDifficulty; + private IBindable localStarDifficulty; [BackgroundDependencyLoader] private void load() @@ -160,7 +160,11 @@ namespace osu.Game.Beatmaps.Drawables localStarDifficulty = ruleset != null ? difficultyCache.GetBindableDifficulty(beatmap, ruleset, mods, difficultyCancellation.Token) : difficultyCache.GetBindableDifficulty(beatmap, difficultyCancellation.Token); - localStarDifficulty.BindValueChanged(difficulty => StarDifficulty.Value = difficulty.NewValue); + localStarDifficulty.BindValueChanged(d => + { + if (d.NewValue is StarDifficulty diff) + StarDifficulty.Value = diff; + }); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index a6beb19876..96ec9644b5 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -137,7 +137,7 @@ namespace osu.Game.Scoring ScoringMode.BindValueChanged(onScoringModeChanged, true); } - private IBindable difficultyBindable; + private IBindable difficultyBindable; private CancellationTokenSource difficultyCancellationSource; private void onScoringModeChanged(ValueChangedEvent mode) @@ -168,7 +168,11 @@ namespace osu.Game.Scoring // We can compute the max combo locally after the async beatmap difficulty computation. difficultyBindable = difficulties().GetBindableDifficulty(score.Beatmap, score.Ruleset, score.Mods, (difficultyCancellationSource = new CancellationTokenSource()).Token); - difficultyBindable.BindValueChanged(d => updateScore(d.NewValue.MaxCombo), true); + difficultyBindable.BindValueChanged(d => + { + if (d.NewValue is StarDifficulty diff) + updateScore(diff.MaxCombo); + }, true); return; } diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 86cb561bc7..3b3ed88ccb 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -43,7 +43,7 @@ namespace osu.Game.Screens.Select [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } - private IBindable beatmapDifficulty; + private IBindable beatmapDifficulty; protected BufferedWedgeInfo Info; @@ -132,7 +132,7 @@ namespace osu.Game.Screens.Select return; } - LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value) + LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value ?? new StarDifficulty()) { Shear = -Shear, Depth = Info?.Depth + 1 ?? 0 diff --git a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs index e66469ff8d..633ef9297e 100644 --- a/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/DrawableCarouselBeatmap.cs @@ -63,7 +63,7 @@ namespace osu.Game.Screens.Select.Carousel [Resolved(CanBeNull = true)] private ManageCollectionsDialog manageCollectionsDialog { get; set; } - private IBindable starDifficultyBindable; + private IBindable starDifficultyBindable; private CancellationTokenSource starDifficultyCancellationSource; public DrawableCarouselBeatmap(CarouselBeatmap panel) @@ -217,7 +217,10 @@ namespace osu.Game.Screens.Select.Carousel { // We've potentially cancelled the computation above so a new bindable is required. starDifficultyBindable = difficultyCache.GetBindableDifficulty(beatmap, (starDifficultyCancellationSource = new CancellationTokenSource()).Token); - starDifficultyBindable.BindValueChanged(d => starCounter.Current = (float)d.NewValue.Stars, true); + starDifficultyBindable.BindValueChanged(d => + { + starCounter.Current = (float)(d.NewValue?.Stars ?? 0); + }, true); } base.ApplyState(); From 5fa9bf61b6a8d2abfd374759da0553d8e807bc27 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 16:22:40 +0900 Subject: [PATCH 6766/6909] Update xmldoc --- osu.Game/Beatmaps/BeatmapDifficultyCache.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs index 72a9b36c6f..53d82c385d 100644 --- a/osu.Game/Beatmaps/BeatmapDifficultyCache.cs +++ b/osu.Game/Beatmaps/BeatmapDifficultyCache.cs @@ -69,7 +69,7 @@ namespace osu.Game.Beatmaps /// /// The to get the difficulty of. /// An optional which stops updating the star difficulty for the given . - /// A bindable that is updated to contain the star difficulty when it becomes available. + /// A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state (but not during updates to ruleset and mods if a stale value is already propagated). public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, CancellationToken cancellationToken = default) { var bindable = createBindable(beatmapInfo, currentRuleset.Value, currentMods.Value, cancellationToken); @@ -90,7 +90,7 @@ namespace osu.Game.Beatmaps /// The to get the difficulty with. If null, the 's ruleset is used. /// The s to get the difficulty with. If null, no mods will be assumed. /// An optional which stops updating the star difficulty for the given . - /// A bindable that is updated to contain the star difficulty when it becomes available. + /// A bindable that is updated to contain the star difficulty when it becomes available. Will be null while in an initial calculating state. public IBindable GetBindableDifficulty([NotNull] BeatmapInfo beatmapInfo, [CanBeNull] RulesetInfo rulesetInfo, [CanBeNull] IEnumerable mods, CancellationToken cancellationToken = default) => createBindable(beatmapInfo, rulesetInfo, mods, cancellationToken); From 31c52bd585a55f925676313671c23c437aff28e2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 17:00:42 +0900 Subject: [PATCH 6767/6909] Update the displayed BPM at song select with rate adjust mods This only covers constant rate rate adjust mods. Mods like wind up/wind down will need a more complex implementation which we haven't really planned yet. --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 86cb561bc7..13ec106694 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -383,10 +383,18 @@ namespace osu.Game.Screens.Select return labels.ToArray(); } + [Resolved] + private IBindable> mods { get; set; } + private string getBPMRange(IBeatmap beatmap) { - double bpmMax = beatmap.ControlPointInfo.BPMMaximum; - double bpmMin = beatmap.ControlPointInfo.BPMMinimum; + // this doesn't consider mods which apply variable rates, yet. + double rate = 1; + foreach (var mod in mods.Value.OfType()) + rate = mod.ApplyToRate(0, rate); + + double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate; + double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate; if (Precision.AlmostEquals(bpmMin, bpmMax)) return $"{bpmMin:0}"; From 2db4b793d7ab2c064c1a6a1b924ef379b71a86ee Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 17:04:39 +0900 Subject: [PATCH 6768/6909] Also handle most common BPM display --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 13ec106694..311ed6ffb9 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -395,11 +395,12 @@ namespace osu.Game.Screens.Select double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate; double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate; + double mostCommonBPM = 60000 / beatmap.GetMostCommonBeatLength() * rate; if (Precision.AlmostEquals(bpmMin, bpmMax)) return $"{bpmMin:0}"; - return $"{bpmMin:0}-{bpmMax:0} (mostly {60000 / beatmap.GetMostCommonBeatLength():0})"; + return $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})"; } private OsuSpriteText[] getMapper(BeatmapMetadata metadata) From 6d1c5979eafaf2dac1182b49c79c0e3a9e369c4d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 17:28:59 +0900 Subject: [PATCH 6769/6909] 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 183ac61c90..8ea7cfac5b 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 37d730bf42..6ff08ae63c 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index ca11952cc8..d7a1b7d692 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -91,7 +91,7 @@ - + From 3802cb29a42056927da2f2c5535cd55bbfc5cf0f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 25 Feb 2021 17:46:35 +0900 Subject: [PATCH 6770/6909] Fix failing tests doing reference comparisons between string and LocalisedString --- .../Ranking/TestSceneExpandedPanelMiddleContent.cs | 4 ++-- .../Visual/SongSelect/TestSceneBeatmapInfoWedge.cs | 14 +++++++------- .../UserInterface/TestSceneFooterButtonMods.cs | 2 +- osu.Game/Configuration/SettingSourceAttribute.cs | 3 ++- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs index f9fe42131f..2f558a6379 100644 --- a/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs +++ b/osu.Game.Tests/Visual/Ranking/TestSceneExpandedPanelMiddleContent.cs @@ -37,7 +37,7 @@ namespace osu.Game.Tests.Visual.Ranking Beatmap = createTestBeatmap(author) })); - AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Text == "mapper_name")); + AddAssert("mapper name present", () => this.ChildrenOfType().Any(spriteText => spriteText.Current.Value == "mapper_name")); } [Test] @@ -49,7 +49,7 @@ namespace osu.Game.Tests.Visual.Ranking })); AddAssert("mapped by text not present", () => - this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Text.ToString(), "mapped", "by"))); + this.ChildrenOfType().All(spriteText => !containsAny(spriteText.Current.Value, "mapped", "by"))); } private void showPanel(ScoreInfo score) => Child = new ExpandedPanelMiddleContentContainer(score); diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index fff4a9ba61..07b67ca3ad 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -103,10 +103,10 @@ namespace osu.Game.Tests.Visual.SongSelect private void testBeatmapLabels(Ruleset ruleset) { - AddAssert("check version", () => infoWedge.Info.VersionLabel.Text == $"{ruleset.ShortName}Version"); - AddAssert("check title", () => infoWedge.Info.TitleLabel.Text == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title"); - AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Text == $"{ruleset.ShortName}Artist"); - AddAssert("check author", () => infoWedge.Info.MapperContainer.Children.OfType().Any(s => s.Text == $"{ruleset.ShortName}Author")); + AddAssert("check version", () => infoWedge.Info.VersionLabel.Current.Value == $"{ruleset.ShortName}Version"); + AddAssert("check title", () => infoWedge.Info.TitleLabel.Current.Value == $"{ruleset.ShortName}Source — {ruleset.ShortName}Title"); + AddAssert("check artist", () => infoWedge.Info.ArtistLabel.Current.Value == $"{ruleset.ShortName}Artist"); + AddAssert("check author", () => infoWedge.Info.MapperContainer.Children.OfType().Any(s => s.Current.Value == $"{ruleset.ShortName}Author")); } private void testInfoLabels(int expectedCount) @@ -119,9 +119,9 @@ namespace osu.Game.Tests.Visual.SongSelect public void TestNullBeatmap() { selectBeatmap(null); - AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Text.ToString())); - AddAssert("check default title", () => infoWedge.Info.TitleLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Title); - AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Text == Beatmap.Default.BeatmapInfo.Metadata.Artist); + AddAssert("check empty version", () => string.IsNullOrEmpty(infoWedge.Info.VersionLabel.Current.Value)); + AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); + AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist); AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any()); AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any()); } diff --git a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs index 1e3b1c2ffd..546e905ded 100644 --- a/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs +++ b/osu.Game.Tests/Visual/UserInterface/TestSceneFooterButtonMods.cs @@ -76,7 +76,7 @@ namespace osu.Game.Tests.Visual.UserInterface var multiplier = mods.Aggregate(1.0, (current, mod) => current * mod.ScoreMultiplier); var expectedValue = multiplier.Equals(1.0) ? string.Empty : $"{multiplier:N2}x"; - return expectedValue == footerButtonMods.MultiplierText.Text; + return expectedValue == footerButtonMods.MultiplierText.Current.Value; } private class TestFooterButtonMods : FooterButtonMods diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 70d67aaaa0..65a5a6d1b4 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -8,6 +8,7 @@ using System.Reflection; using JetBrains.Annotations; using osu.Framework.Bindables; using osu.Framework.Graphics; +using osu.Framework.Localisation; using osu.Game.Overlays.Settings; namespace osu.Game.Configuration @@ -24,7 +25,7 @@ namespace osu.Game.Configuration [AttributeUsage(AttributeTargets.Property)] public class SettingSourceAttribute : Attribute { - public string Label { get; } + public LocalisableString Label { get; } public string Description { get; } From cf4c88c647f2bbfc03984218b01d8dc81d396bbe Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Feb 2021 21:38:21 +0900 Subject: [PATCH 6771/6909] Fix spacing --- .../Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs index 8e9020ee13..54c37e9742 100644 --- a/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs +++ b/osu.Game.Rulesets.Mania/Beatmaps/Patterns/Legacy/HitObjectPatternGenerator.cs @@ -115,10 +115,10 @@ namespace osu.Game.Rulesets.Mania.Beatmaps.Patterns.Legacy } if (convertType.HasFlagFast(PatternType.Cycle) && PreviousPattern.HitObjects.Count() == 1 - // If we convert to 7K + 1, let's not overload the special key - && (TotalColumns != 8 || lastColumn != 0) - // Make sure the last column was not the centre column - && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2)) + // If we convert to 7K + 1, let's not overload the special key + && (TotalColumns != 8 || lastColumn != 0) + // Make sure the last column was not the centre column + && (TotalColumns % 2 == 0 || lastColumn != TotalColumns / 2)) { // Generate a new pattern by cycling backwards (similar to Reverse but for only one hit object) int column = RandomStart + TotalColumns - lastColumn - 1; From 98313a98bf4e0b89deb9b47219fc4e0fd3aaef4a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Feb 2021 21:48:02 +0900 Subject: [PATCH 6772/6909] DI mods in parent class and pass them down --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 311ed6ffb9..37808f6e94 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -39,6 +39,7 @@ namespace osu.Game.Screens.Select private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0); private readonly IBindable ruleset = new Bindable(); + private readonly IBindable> mods = new Bindable>(Array.Empty()); [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } @@ -64,9 +65,11 @@ namespace osu.Game.Screens.Select } [BackgroundDependencyLoader(true)] - private void load([CanBeNull] Bindable parentRuleset) + private void load([CanBeNull] Bindable parentRuleset, [CanBeNull] Bindable> parentMods) { ruleset.BindTo(parentRuleset); + mods.BindTo(parentMods); + ruleset.ValueChanged += _ => updateDisplay(); } @@ -132,7 +135,7 @@ namespace osu.Game.Screens.Select return; } - LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, beatmapDifficulty.Value) + LoadComponentAsync(loadingInfo = new BufferedWedgeInfo(beatmap, ruleset.Value, mods.Value, beatmapDifficulty.Value) { Shear = -Shear, Depth = Info?.Depth + 1 ?? 0 @@ -167,13 +170,15 @@ namespace osu.Game.Screens.Select private readonly WorkingBeatmap beatmap; private readonly RulesetInfo ruleset; + private readonly IReadOnlyList mods; private readonly StarDifficulty starDifficulty; - public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, StarDifficulty difficulty) + public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList mods, StarDifficulty difficulty) : base(pixelSnapping: true) { this.beatmap = beatmap; ruleset = userRuleset ?? beatmap.BeatmapInfo.Ruleset; + this.mods = mods; starDifficulty = difficulty; } @@ -383,14 +388,11 @@ namespace osu.Game.Screens.Select return labels.ToArray(); } - [Resolved] - private IBindable> mods { get; set; } - private string getBPMRange(IBeatmap beatmap) { // this doesn't consider mods which apply variable rates, yet. double rate = 1; - foreach (var mod in mods.Value.OfType()) + foreach (var mod in mods.OfType()) rate = mod.ApplyToRate(0, rate); double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate; From de417a660d7121589abb9c0b0fe635f4e2f44eb0 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Feb 2021 21:51:32 +0900 Subject: [PATCH 6773/6909] Make BPM update with changes in mod settings --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 113 ++++++++++++-------- 1 file changed, 68 insertions(+), 45 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 37808f6e94..9084435f44 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -25,6 +25,7 @@ using osu.Framework.Graphics.Effects; using osu.Framework.Graphics.Sprites; using osu.Framework.Localisation; using osu.Framework.Logging; +using osu.Game.Configuration; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Rulesets.UI; @@ -167,12 +168,15 @@ namespace osu.Game.Screens.Select private ILocalisedBindableString titleBinding; private ILocalisedBindableString artistBinding; + private Container bpmLabelContainer; private readonly WorkingBeatmap beatmap; private readonly RulesetInfo ruleset; private readonly IReadOnlyList mods; private readonly StarDifficulty starDifficulty; + private ModSettingChangeTracker settingChangeTracker; + public BufferedWedgeInfo(WorkingBeatmap beatmap, RulesetInfo userRuleset, IReadOnlyList mods, StarDifficulty difficulty) : base(pixelSnapping: true) { @@ -189,9 +193,11 @@ namespace osu.Game.Screens.Select var metadata = beatmapInfo.Metadata ?? beatmap.BeatmapSetInfo?.Metadata ?? new BeatmapMetadata(); CacheDrawnFrameBuffer = true; - RelativeSizeAxes = Axes.Both; + settingChangeTracker = new ModSettingChangeTracker(mods); + settingChangeTracker.SettingChanged += _ => updateBPM(); + titleBinding = localisation.GetLocalisedString(new LocalisedString((metadata.TitleUnicode, metadata.Title))); artistBinding = localisation.GetLocalisedString(new LocalisedString((metadata.ArtistUnicode, metadata.Artist))); @@ -312,7 +318,25 @@ namespace osu.Game.Screens.Select Margin = new MarginPadding { Top = 20 }, Spacing = new Vector2(20, 0), AutoSizeAxes = Axes.Both, - Children = getInfoLabels() + Children = new Drawable[] + { + new InfoLabel(new BeatmapStatistic + { + Name = "Length", + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), + Content = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToString(@"m\:ss"), + }), + bpmLabelContainer = new Container + { + AutoSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(20, 0), + Children = getRulesetInfoLabels() + } + } } } } @@ -324,6 +348,8 @@ namespace osu.Game.Screens.Select // no difficulty means it can't have a status to show if (beatmapInfo.Version == null) StatusPill.Hide(); + + updateBPM(); } private static Drawable createStarRatingDisplay(StarDifficulty difficulty) => difficulty.Stars > 0 @@ -340,69 +366,60 @@ namespace osu.Game.Screens.Select ForceRedraw(); } - private InfoLabel[] getInfoLabels() + private InfoLabel[] getRulesetInfoLabels() { - var b = beatmap.Beatmap; - - List labels = new List(); - - if (b?.HitObjects?.Any() == true) + try { - labels.Add(new InfoLabel(new BeatmapStatistic - { - Name = "Length", - CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), - Content = TimeSpan.FromMilliseconds(b.BeatmapInfo.Length).ToString(@"m\:ss"), - })); - - labels.Add(new InfoLabel(new BeatmapStatistic - { - Name = "BPM", - CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), - Content = getBPMRange(b), - })); + IBeatmap playableBeatmap; try { - 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()); - } - - labels.AddRange(playableBeatmap.GetStatistics().Select(s => new InfoLabel(s))); + // Try to get the beatmap with the user's ruleset + playableBeatmap = beatmap.GetPlayableBeatmap(ruleset, Array.Empty()); } - catch (Exception e) + catch (BeatmapInvalidForRulesetException) { - Logger.Error(e, "Could not load beatmap successfully!"); + // Can't be converted to the user's ruleset, so use the beatmap's own ruleset + playableBeatmap = beatmap.GetPlayableBeatmap(beatmap.BeatmapInfo.Ruleset, Array.Empty()); } + + return playableBeatmap.GetStatistics().Select(s => new InfoLabel(s)).ToArray(); + } + catch (Exception e) + { + Logger.Error(e, "Could not load beatmap successfully!"); } - return labels.ToArray(); + return Array.Empty(); } - private string getBPMRange(IBeatmap beatmap) + private void updateBPM() { + var b = beatmap.Beatmap; + if (b == null) + return; + // this doesn't consider mods which apply variable rates, yet. double rate = 1; foreach (var mod in mods.OfType()) rate = mod.ApplyToRate(0, rate); - double bpmMax = beatmap.ControlPointInfo.BPMMaximum * rate; - double bpmMin = beatmap.ControlPointInfo.BPMMinimum * rate; - double mostCommonBPM = 60000 / beatmap.GetMostCommonBeatLength() * rate; + double bpmMax = b.ControlPointInfo.BPMMaximum * rate; + double bpmMin = b.ControlPointInfo.BPMMinimum * rate; + double mostCommonBPM = 60000 / b.GetMostCommonBeatLength() * rate; - if (Precision.AlmostEquals(bpmMin, bpmMax)) - return $"{bpmMin:0}"; + string labelText = Precision.AlmostEquals(bpmMin, bpmMax) + ? $"{bpmMin:0}" + : $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})"; - return $"{bpmMin:0}-{bpmMax:0} (mostly {mostCommonBPM:0})"; + bpmLabelContainer.Child = new InfoLabel(new BeatmapStatistic + { + Name = "BPM", + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Bpm), + Content = labelText + }); + + ForceRedraw(); } private OsuSpriteText[] getMapper(BeatmapMetadata metadata) @@ -425,6 +442,12 @@ namespace osu.Game.Screens.Select }; } + protected override void Dispose(bool isDisposing) + { + base.Dispose(isDisposing); + settingChangeTracker?.Dispose(); + } + public class InfoLabel : Container, IHasTooltip { public string TooltipText { get; } From 649ce20e354b2f30b08a3c60db94695e24aa5e34 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Feb 2021 22:01:53 +0900 Subject: [PATCH 6774/6909] Fix up super weird and super wrong DI --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index d1b28e6607..97fe099975 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -5,7 +5,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading; -using JetBrains.Annotations; using osuTK; using osuTK.Graphics; using osu.Framework.Allocation; @@ -39,8 +38,11 @@ namespace osu.Game.Screens.Select private static readonly Vector2 wedged_container_shear = new Vector2(shear_width / SongSelect.WEDGE_HEIGHT, 0); - private readonly IBindable ruleset = new Bindable(); - private readonly IBindable> mods = new Bindable>(Array.Empty()); + [Resolved] + private IBindable ruleset { get; set; } + + [Resolved] + private IBindable> mods { get; set; } [Resolved] private BeatmapDifficultyCache difficultyCache { get; set; } @@ -65,13 +67,10 @@ namespace osu.Game.Screens.Select }; } - [BackgroundDependencyLoader(true)] - private void load([CanBeNull] Bindable parentRuleset, [CanBeNull] Bindable> parentMods) + protected override void LoadComplete() { - ruleset.BindTo(parentRuleset); - mods.BindTo(parentMods); - - ruleset.ValueChanged += _ => updateDisplay(); + base.LoadComplete(); + ruleset.BindValueChanged(_ => updateDisplay(), true); } protected override void PopIn() From c3eb44137bfd109006f826fc196a2394a2675196 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Feb 2021 22:09:41 +0900 Subject: [PATCH 6775/6909] Move ValueChanged bind back to load() --- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index 97fe099975..fe2b7b7525 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -67,10 +67,10 @@ namespace osu.Game.Screens.Select }; } - protected override void LoadComplete() + [BackgroundDependencyLoader] + private void load() { - base.LoadComplete(); - ruleset.BindValueChanged(_ => updateDisplay(), true); + ruleset.BindValueChanged(_ => updateDisplay()); } protected override void PopIn() From 01a48154126fbc2ca75cea6632349689ea41ce4f Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 25 Feb 2021 23:36:02 +0900 Subject: [PATCH 6776/6909] Make labels disappear on null beatmap/no hitobjects --- .../SongSelect/TestSceneBeatmapInfoWedge.cs | 7 ++- osu.Game/Screens/Select/BeatmapInfoWedge.cs | 61 +++++++++++-------- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs index 07b67ca3ad..7ea6373763 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapInfoWedge.cs @@ -7,6 +7,7 @@ using JetBrains.Annotations; using NUnit.Framework; using osu.Framework.Allocation; using osu.Framework.Graphics; +using osu.Framework.Testing; using osu.Game.Beatmaps; using osu.Game.Graphics.Sprites; using osu.Game.Rulesets; @@ -111,8 +112,8 @@ namespace osu.Game.Tests.Visual.SongSelect private void testInfoLabels(int expectedCount) { - AddAssert("check info labels exists", () => infoWedge.Info.InfoLabelContainer.Children.Any()); - AddAssert("check info labels count", () => infoWedge.Info.InfoLabelContainer.Children.Count == expectedCount); + AddAssert("check info labels exists", () => infoWedge.Info.ChildrenOfType().Any()); + AddAssert("check info labels count", () => infoWedge.Info.ChildrenOfType().Count() == expectedCount); } [Test] @@ -123,7 +124,7 @@ namespace osu.Game.Tests.Visual.SongSelect AddAssert("check default title", () => infoWedge.Info.TitleLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Title); AddAssert("check default artist", () => infoWedge.Info.ArtistLabel.Current.Value == Beatmap.Default.BeatmapInfo.Metadata.Artist); AddAssert("check empty author", () => !infoWedge.Info.MapperContainer.Children.Any()); - AddAssert("check no info labels", () => !infoWedge.Info.InfoLabelContainer.Children.Any()); + AddAssert("check no info labels", () => !infoWedge.Info.ChildrenOfType().Any()); } [Test] diff --git a/osu.Game/Screens/Select/BeatmapInfoWedge.cs b/osu.Game/Screens/Select/BeatmapInfoWedge.cs index fe2b7b7525..36cc19cce3 100644 --- a/osu.Game/Screens/Select/BeatmapInfoWedge.cs +++ b/osu.Game/Screens/Select/BeatmapInfoWedge.cs @@ -163,10 +163,10 @@ namespace osu.Game.Screens.Select public OsuSpriteText ArtistLabel { get; private set; } public BeatmapSetOnlineStatusPill StatusPill { get; private set; } public FillFlowContainer MapperContainer { get; private set; } - public FillFlowContainer InfoLabelContainer { get; private set; } private ILocalisedBindableString titleBinding; private ILocalisedBindableString artistBinding; + private FillFlowContainer infoLabelContainer; private Container bpmLabelContainer; private readonly WorkingBeatmap beatmap; @@ -194,9 +194,6 @@ namespace osu.Game.Screens.Select CacheDrawnFrameBuffer = true; RelativeSizeAxes = Axes.Both; - settingChangeTracker = new ModSettingChangeTracker(mods); - settingChangeTracker.SettingChanged += _ => updateBPM(); - titleBinding = localisation.GetLocalisedString(new RomanisableString(metadata.TitleUnicode, metadata.Title)); artistBinding = localisation.GetLocalisedString(new RomanisableString(metadata.ArtistUnicode, metadata.Artist)); @@ -312,30 +309,11 @@ namespace osu.Game.Screens.Select AutoSizeAxes = Axes.Both, Children = getMapper(metadata) }, - InfoLabelContainer = new FillFlowContainer + infoLabelContainer = new FillFlowContainer { Margin = new MarginPadding { Top = 20 }, Spacing = new Vector2(20, 0), AutoSizeAxes = Axes.Both, - Children = new Drawable[] - { - new InfoLabel(new BeatmapStatistic - { - Name = "Length", - CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), - Content = TimeSpan.FromMilliseconds(beatmapInfo.Length).ToString(@"m\:ss"), - }), - bpmLabelContainer = new Container - { - AutoSizeAxes = Axes.Both, - }, - new FillFlowContainer - { - AutoSizeAxes = Axes.Both, - Spacing = new Vector2(20, 0), - Children = getRulesetInfoLabels() - } - } } } } @@ -348,7 +326,7 @@ namespace osu.Game.Screens.Select if (beatmapInfo.Version == null) StatusPill.Hide(); - updateBPM(); + addInfoLabels(); } private static Drawable createStarRatingDisplay(StarDifficulty difficulty) => difficulty.Stars > 0 @@ -365,6 +343,37 @@ namespace osu.Game.Screens.Select ForceRedraw(); } + private void addInfoLabels() + { + if (beatmap.Beatmap?.HitObjects?.Any() != true) + return; + + infoLabelContainer.Children = new Drawable[] + { + new InfoLabel(new BeatmapStatistic + { + Name = "Length", + CreateIcon = () => new BeatmapStatisticIcon(BeatmapStatisticsIconType.Length), + Content = TimeSpan.FromMilliseconds(beatmap.BeatmapInfo.Length).ToString(@"m\:ss"), + }), + bpmLabelContainer = new Container + { + AutoSizeAxes = Axes.Both, + }, + new FillFlowContainer + { + AutoSizeAxes = Axes.Both, + Spacing = new Vector2(20, 0), + Children = getRulesetInfoLabels() + } + }; + + settingChangeTracker = new ModSettingChangeTracker(mods); + settingChangeTracker.SettingChanged += _ => refreshBPMLabel(); + + refreshBPMLabel(); + } + private InfoLabel[] getRulesetInfoLabels() { try @@ -392,7 +401,7 @@ namespace osu.Game.Screens.Select return Array.Empty(); } - private void updateBPM() + private void refreshBPMLabel() { var b = beatmap.Beatmap; if (b == null) From 254f9bb58be27c0981dd32df8a1d6038a6a090fb Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Feb 2021 13:37:58 +0900 Subject: [PATCH 6777/6909] Show API human readable error message when chat posting fails Closes #11902. --- osu.Game/Online/Chat/ChannelManager.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/osu.Game/Online/Chat/ChannelManager.cs b/osu.Game/Online/Chat/ChannelManager.cs index 036ec4d0f3..a980f4c54b 100644 --- a/osu.Game/Online/Chat/ChannelManager.cs +++ b/osu.Game/Online/Chat/ChannelManager.cs @@ -152,7 +152,7 @@ namespace osu.Game.Online.Chat createNewPrivateMessageRequest.Failure += exception => { - Logger.Error(exception, "Posting message failed."); + handlePostException(exception); target.ReplaceMessage(message, null); dequeueAndRun(); }; @@ -171,7 +171,7 @@ namespace osu.Game.Online.Chat req.Failure += exception => { - Logger.Error(exception, "Posting message failed."); + handlePostException(exception); target.ReplaceMessage(message, null); dequeueAndRun(); }; @@ -184,6 +184,14 @@ namespace osu.Game.Online.Chat dequeueAndRun(); } + private static void handlePostException(Exception exception) + { + if (exception is APIException apiException) + Logger.Log(apiException.Message, level: LogLevel.Important); + else + Logger.Error(exception, "Posting message failed."); + } + /// /// Posts a command locally. Commands like /help will result in a help message written in the current channel. /// From cd1c1bf534947585db53eb6b4641acef41f1a12f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Feb 2021 14:15:12 +0900 Subject: [PATCH 6778/6909] Centralise cases of performing actions on the current selection By moving this to a central location, we can avoid invoking the EditorChangeHandler when there is no selection made. This helps alleviate the issue pointed out in https://github.com/ppy/osu/issues/11901, but not fix it completely. --- .../Edit/ManiaSelectionHandler.cs | 8 +++-- .../Edit/TaikoSelectionHandler.cs | 32 +++++++------------ .../Compose/Components/BlueprintContainer.cs | 3 +- .../Compose/Components/SelectionHandler.cs | 27 ++++------------ osu.Game/Screens/Edit/EditorBeatmap.cs | 16 ++++++++++ 5 files changed, 42 insertions(+), 44 deletions(-) diff --git a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs index 50629f41a9..2689ed4112 100644 --- a/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs +++ b/osu.Game.Rulesets.Mania/Edit/ManiaSelectionHandler.cs @@ -45,6 +45,7 @@ namespace osu.Game.Rulesets.Mania.Edit int minColumn = int.MaxValue; int maxColumn = int.MinValue; + // find min/max in an initial pass before actually performing the movement. foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType()) { if (obj.Column < minColumn) @@ -55,8 +56,11 @@ namespace osu.Game.Rulesets.Mania.Edit columnDelta = Math.Clamp(columnDelta, -minColumn, maniaPlayfield.TotalColumns - 1 - maxColumn); - foreach (var obj in EditorBeatmap.SelectedHitObjects.OfType()) - obj.Column += columnDelta; + EditorBeatmap.PerformOnSelection(h => + { + if (h is ManiaHitObject maniaObj) + maniaObj.Column += columnDelta; + }); } } } diff --git a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs index 3fbcee44af..ac2dd4bdb6 100644 --- a/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs +++ b/osu.Game.Rulesets.Taiko/Edit/TaikoSelectionHandler.cs @@ -52,32 +52,24 @@ namespace osu.Game.Rulesets.Taiko.Edit public void SetStrongState(bool state) { - var hits = EditorBeatmap.SelectedHitObjects.OfType(); - - EditorBeatmap.BeginChange(); - - foreach (var h in hits) + EditorBeatmap.PerformOnSelection(h => { - if (h.IsStrong != state) - { - h.IsStrong = state; - EditorBeatmap.Update(h); - } - } + if (!(h is Hit taikoHit)) return; - EditorBeatmap.EndChange(); + if (taikoHit.IsStrong != state) + { + taikoHit.IsStrong = state; + EditorBeatmap.Update(taikoHit); + } + }); } public void SetRimState(bool state) { - var hits = EditorBeatmap.SelectedHitObjects.OfType(); - - EditorBeatmap.BeginChange(); - - foreach (var h in hits) - h.Type = state ? HitType.Rim : HitType.Centre; - - EditorBeatmap.EndChange(); + EditorBeatmap.PerformOnSelection(h => + { + if (h is Hit taikoHit) taikoHit.Type = state ? HitType.Rim : HitType.Centre; + }); } protected override IEnumerable GetContextMenuItemsForSelection(IEnumerable selection) diff --git a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs index 5371beac60..051d0766bf 100644 --- a/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs +++ b/osu.Game/Screens/Edit/Compose/Components/BlueprintContainer.cs @@ -495,8 +495,7 @@ namespace osu.Game.Screens.Edit.Compose.Components // Apply the start time at the newly snapped-to position double offset = result.Time.Value - movementBlueprints.First().HitObject.StartTime; - foreach (HitObject obj in Beatmap.SelectedHitObjects) - obj.StartTime += offset; + Beatmap.PerformOnSelection(obj => obj.StartTime += offset); } return true; diff --git a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs index 788b485449..018d4d081c 100644 --- a/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs +++ b/osu.Game/Screens/Edit/Compose/Components/SelectionHandler.cs @@ -320,18 +320,14 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void AddHitSample(string sampleName) { - EditorBeatmap.BeginChange(); - - foreach (var h in EditorBeatmap.SelectedHitObjects) + EditorBeatmap.PerformOnSelection(h => { // Make sure there isn't already an existing sample if (h.Samples.Any(s => s.Name == sampleName)) - continue; + return; h.Samples.Add(new HitSampleInfo(sampleName)); - } - - EditorBeatmap.EndChange(); + }); } /// @@ -341,19 +337,15 @@ namespace osu.Game.Screens.Edit.Compose.Components /// Throws if any selected object doesn't implement public void SetNewCombo(bool state) { - EditorBeatmap.BeginChange(); - - foreach (var h in EditorBeatmap.SelectedHitObjects) + EditorBeatmap.PerformOnSelection(h => { var comboInfo = h as IHasComboInformation; - if (comboInfo == null || comboInfo.NewCombo == state) continue; + if (comboInfo == null || comboInfo.NewCombo == state) return; comboInfo.NewCombo = state; EditorBeatmap.Update(h); - } - - EditorBeatmap.EndChange(); + }); } /// @@ -362,12 +354,7 @@ namespace osu.Game.Screens.Edit.Compose.Components /// The name of the hit sample. public void RemoveHitSample(string sampleName) { - EditorBeatmap.BeginChange(); - - foreach (var h in EditorBeatmap.SelectedHitObjects) - h.SamplesBindable.RemoveAll(s => s.Name == sampleName); - - EditorBeatmap.EndChange(); + EditorBeatmap.PerformOnSelection(h => h.SamplesBindable.RemoveAll(s => s.Name == sampleName)); } #endregion diff --git a/osu.Game/Screens/Edit/EditorBeatmap.cs b/osu.Game/Screens/Edit/EditorBeatmap.cs index 174ff1478b..4f1b0484d2 100644 --- a/osu.Game/Screens/Edit/EditorBeatmap.cs +++ b/osu.Game/Screens/Edit/EditorBeatmap.cs @@ -100,6 +100,22 @@ namespace osu.Game.Screens.Edit private readonly HashSet batchPendingUpdates = new HashSet(); + /// + /// Perform the provided action on every selected hitobject. + /// Changes will be grouped as one history action. + /// + /// The action to perform. + public void PerformOnSelection(Action action) + { + if (SelectedHitObjects.Count == 0) + return; + + BeginChange(); + foreach (var h in SelectedHitObjects) + action(h); + EndChange(); + } + /// /// Adds a collection of s to this . /// From 3e65dfb9e7df4fe11c7d884e85efb30e461041b6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Feb 2021 17:11:47 +0900 Subject: [PATCH 6779/6909] Reduce allocation overhead when notification overlay has visible notifications --- .../Notifications/NotificationSection.cs | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/Notifications/NotificationSection.cs b/osu.Game/Overlays/Notifications/NotificationSection.cs index bc41311a6d..2316199049 100644 --- a/osu.Game/Overlays/Notifications/NotificationSection.cs +++ b/osu.Game/Overlays/Notifications/NotificationSection.cs @@ -10,9 +10,9 @@ 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 osuTK; -using osu.Game.Graphics.Containers; namespace osu.Game.Overlays.Notifications { @@ -122,7 +122,20 @@ namespace osu.Game.Overlays.Notifications { base.Update(); - countDrawable.Text = notifications.Children.Count(c => c.Alpha > 0.99f).ToString(); + countDrawable.Text = getVisibleCount().ToString(); + } + + private int getVisibleCount() + { + int count = 0; + + foreach (var c in notifications) + { + if (c.Alpha > 0.99f) + count++; + } + + return count; } private class ClearAllButton : OsuClickableContainer From 7e6bd0e995fe8ec1f33b5cbfa510ad7cac69c04e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Feb 2021 17:30:59 +0900 Subject: [PATCH 6780/6909] Fix "failed to import" message showing when importing from a stable install with no beatmaps --- osu.Game/Database/ArchiveModelManager.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 03b8db2cb8..daaba9098e 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -141,6 +141,13 @@ namespace osu.Game.Database protected async Task> Import(ProgressNotification notification, params ImportTask[] tasks) { + if (tasks.Length == 0) + { + notification.CompletionText = $"No {HumanisedModelName}s were found to import!"; + notification.State = ProgressNotificationState.Completed; + return Enumerable.Empty(); + } + notification.Progress = 0; notification.Text = $"{HumanisedModelName.Humanize(LetterCasing.Title)} import is initialising..."; From 1ab449b73e081284f88125c696845c51c35ae984 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Feb 2021 17:54:51 +0900 Subject: [PATCH 6781/6909] Add test scene for drawings screen --- .../Screens/TestSceneDrawingsScreen.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs diff --git a/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs b/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.cs new file mode 100644 index 0000000000..e2954c8f10 --- /dev/null +++ b/osu.Game.Tournament.Tests/Screens/TestSceneDrawingsScreen.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.IO; +using osu.Framework.Allocation; +using osu.Framework.Graphics; +using osu.Framework.Platform; +using osu.Game.Graphics.Cursor; +using osu.Game.Tournament.Screens.Drawings; + +namespace osu.Game.Tournament.Tests.Screens +{ + public class TestSceneDrawingsScreen : TournamentTestScene + { + [BackgroundDependencyLoader] + private void load(Storage storage) + { + using (var stream = storage.GetStream("drawings.txt", FileAccess.Write)) + using (var writer = new StreamWriter(stream)) + { + writer.WriteLine("KR : South Korea : KOR"); + writer.WriteLine("US : United States : USA"); + writer.WriteLine("PH : Philippines : PHL"); + writer.WriteLine("BR : Brazil : BRA"); + writer.WriteLine("JP : Japan : JPN"); + } + + Add(new OsuContextMenuContainer + { + RelativeSizeAxes = Axes.Both, + Child = new DrawingsScreen() + }); + } + } +} From 1ac82af19abce0e1e2ce97facf22808652d9d305 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Feb 2021 17:58:21 +0900 Subject: [PATCH 6782/6909] Adjust flag size to fit again --- .../Screens/Drawings/Components/ScrollingTeamContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs index 3ff4718b75..c7060bd538 100644 --- a/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs +++ b/osu.Game.Tournament/Screens/Drawings/Components/ScrollingTeamContainer.cs @@ -345,7 +345,7 @@ namespace osu.Game.Tournament.Screens.Drawings.Components Flag.Anchor = Anchor.Centre; Flag.Origin = Anchor.Centre; - Flag.Scale = new Vector2(0.9f); + Flag.Scale = new Vector2(0.7f); InternalChildren = new Drawable[] { From 98d525d1dbb6f8b854b682a45d5ba600f30da6ea Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Feb 2021 19:56:10 +0900 Subject: [PATCH 6783/6909] 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 8ea7cfac5b..5d83bb9583 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 6ff08ae63c..84a74502c2 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index d7a1b7d692..2cea2e4b13 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -91,7 +91,7 @@ - + From 4fd8501c860989e09f1fdfedc9405bdde39aa70c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 26 Feb 2021 20:03:03 +0900 Subject: [PATCH 6784/6909] Remove unnecessary using (underlying enumerator change) --- osu.Game/Screens/Play/PlayerLoader.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/Screens/Play/PlayerLoader.cs b/osu.Game/Screens/Play/PlayerLoader.cs index 5b4bd11216..7d906cdc5b 100644 --- a/osu.Game/Screens/Play/PlayerLoader.cs +++ b/osu.Game/Screens/Play/PlayerLoader.cs @@ -3,7 +3,6 @@ using System; using System.Diagnostics; -using System.Linq; using System.Threading.Tasks; using JetBrains.Annotations; using osu.Framework.Allocation; From 52e81385a6ba594cd325e24cf71c6580a7922727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Feb 2021 11:33:08 +0100 Subject: [PATCH 6785/6909] Fix restore default button mutating transforms during load --- osu.Game/Overlays/Settings/SettingsItem.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index aafd7463a6..4cb8d7f83c 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -207,7 +207,9 @@ namespace osu.Game.Overlays.Settings UpdateState(); } - public void UpdateState() + public void UpdateState() => Scheduler.AddOnce(updateState); + + private void updateState() { if (bindable == null) return; From 87b73da73edddc47f93d5c1a5edc6f8bae3ce6fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Feb 2021 14:46:48 +0100 Subject: [PATCH 6786/6909] Add failing test case --- .../Mods/SettingsSourceAttributeTest.cs | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs new file mode 100644 index 0000000000..240d617dc7 --- /dev/null +++ b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.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.Linq; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Game.Configuration; + +namespace osu.Game.Tests.Mods +{ + [TestFixture] + public class SettingsSourceAttributeTest + { + [Test] + public void TestOrdering() + { + var objectWithSettings = new ClassWithSettings(); + + var orderedSettings = objectWithSettings.GetOrderedSettingsSourceProperties().ToArray(); + + Assert.That(orderedSettings, Has.Length.EqualTo(3)); + } + + private class ClassWithSettings + { + [SettingSource("Second setting", "Another description", 2)] + public BindableBool SecondSetting { get; set; } = new BindableBool(); + + [SettingSource("First setting", "A description", 1)] + public BindableDouble FirstSetting { get; set; } = new BindableDouble(); + + [SettingSource("Third setting", "Yet another description", 3)] + public BindableInt ThirdSetting { get; set; } = new BindableInt(); + } + } +} From 528de5869e305ec377d75098496ecaeadb949b69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Feb 2021 14:47:09 +0100 Subject: [PATCH 6787/6909] Fix multiple enumerations when ordering setting sources This was not spotted previously, because the base `Attribute` overrides `Equals()` to have semantics similar to structs (per-field equality) by using reflection. That masked the issue when strings were used, and migrating to `LocalisableString` revealed it, as that struct's implementation of equality currently uses instance checks. Whether `LocalisableString.Equals()` is the correct implementation may still be up for discussion, but allowing multiple enumeration is wrong anyway, since the underlying enumerables are live (one especially is a yield iterator, causing new object instances to be allocated). --- osu.Game/Configuration/SettingSourceAttribute.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 65a5a6d1b4..d0d2480e62 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -139,9 +139,12 @@ namespace osu.Game.Configuration public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj) { - var original = obj.GetSettingsSourceProperties(); + var original = obj.GetSettingsSourceProperties().ToArray(); - var orderedRelative = original.Where(attr => attr.Item1.OrderPosition != null).OrderBy(attr => attr.Item1.OrderPosition); + var orderedRelative = original + .Where(attr => attr.Item1.OrderPosition != null) + .OrderBy(attr => attr.Item1.OrderPosition) + .ToArray(); var unordered = original.Except(orderedRelative); return orderedRelative.Concat(unordered); From dd2f63f3137c8a9923509f04b2bb5656eda0093a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Feb 2021 14:57:37 +0100 Subject: [PATCH 6788/6909] Add assertions to actually check order --- osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs index 240d617dc7..7fce1a6ce5 100644 --- a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs +++ b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs @@ -19,6 +19,10 @@ namespace osu.Game.Tests.Mods var orderedSettings = objectWithSettings.GetOrderedSettingsSourceProperties().ToArray(); Assert.That(orderedSettings, Has.Length.EqualTo(3)); + + Assert.That(orderedSettings[0].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.FirstSetting))); + Assert.That(orderedSettings[1].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.SecondSetting))); + Assert.That(orderedSettings[2].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.ThirdSetting))); } private class ClassWithSettings From 7b6e53680c6035f6b7843f3ac5bd65b90e41128d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Feb 2021 15:14:25 +0100 Subject: [PATCH 6789/6909] Add coverage for the unordered case --- osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs index 7fce1a6ce5..883c9d1ac2 100644 --- a/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs +++ b/osu.Game.Tests/Mods/SettingsSourceAttributeTest.cs @@ -18,15 +18,19 @@ namespace osu.Game.Tests.Mods var orderedSettings = objectWithSettings.GetOrderedSettingsSourceProperties().ToArray(); - Assert.That(orderedSettings, Has.Length.EqualTo(3)); + Assert.That(orderedSettings, Has.Length.EqualTo(4)); Assert.That(orderedSettings[0].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.FirstSetting))); Assert.That(orderedSettings[1].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.SecondSetting))); Assert.That(orderedSettings[2].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.ThirdSetting))); + Assert.That(orderedSettings[3].Item2.Name, Is.EqualTo(nameof(ClassWithSettings.UnorderedSetting))); } private class ClassWithSettings { + [SettingSource("Unordered setting", "Should be last")] + public BindableFloat UnorderedSetting { get; set; } = new BindableFloat(); + [SettingSource("Second setting", "Another description", 2)] public BindableBool SecondSetting { get; set; } = new BindableBool(); From 1e56d2cbba16edec86632b5c1f780f1abaac2d1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Feb 2021 15:30:05 +0100 Subject: [PATCH 6790/6909] Make `SettingSourceAttribute` implement `IComparable` --- .../Configuration/SettingSourceAttribute.cs | 29 +++++++++++++------ 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index d0d2480e62..4cc31e14ac 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -23,7 +23,7 @@ namespace osu.Game.Configuration /// [MeansImplicitUse] [AttributeUsage(AttributeTargets.Property)] - public class SettingSourceAttribute : Attribute + public class SettingSourceAttribute : Attribute, IComparable { public LocalisableString Label { get; } @@ -42,6 +42,21 @@ namespace osu.Game.Configuration { OrderPosition = orderPosition; } + + public int CompareTo(SettingSourceAttribute other) + { + if (OrderPosition == other.OrderPosition) + return 0; + + // unordered items come last (are greater than any ordered items). + if (OrderPosition == null) + return 1; + if (other.OrderPosition == null) + return -1; + + // ordered items are sorted by the order value. + return OrderPosition.Value.CompareTo(other.OrderPosition); + } } public static class SettingSourceExtensions @@ -137,17 +152,13 @@ namespace osu.Game.Configuration } } - public static IEnumerable<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj) + public static ICollection<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj) { var original = obj.GetSettingsSourceProperties().ToArray(); - var orderedRelative = original - .Where(attr => attr.Item1.OrderPosition != null) - .OrderBy(attr => attr.Item1.OrderPosition) - .ToArray(); - var unordered = original.Except(orderedRelative); - - return orderedRelative.Concat(unordered); + return original + .OrderBy(attr => attr.Item1) + .ToArray(); } } } From 7e17c5ab7180c460f6fe142a37a2b0fdf3b8c987 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 27 Feb 2021 15:46:18 +0100 Subject: [PATCH 6791/6909] Trim yet another array copy --- osu.Game/Configuration/SettingSourceAttribute.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game/Configuration/SettingSourceAttribute.cs b/osu.Game/Configuration/SettingSourceAttribute.cs index 4cc31e14ac..cfce615130 100644 --- a/osu.Game/Configuration/SettingSourceAttribute.cs +++ b/osu.Game/Configuration/SettingSourceAttribute.cs @@ -153,12 +153,8 @@ namespace osu.Game.Configuration } public static ICollection<(SettingSourceAttribute, PropertyInfo)> GetOrderedSettingsSourceProperties(this object obj) - { - var original = obj.GetSettingsSourceProperties().ToArray(); - - return original - .OrderBy(attr => attr.Item1) - .ToArray(); - } + => obj.GetSettingsSourceProperties() + .OrderBy(attr => attr.Item1) + .ToArray(); } } From 41b43dd39a8b0b5e76a0f82e2d06b19ee633d696 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Feb 2021 21:32:56 +0300 Subject: [PATCH 6792/6909] Add nested legacy-simulating coordinates container --- .../Skinning/Legacy/LegacySpinner.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index ec7ecb0d28..94b6a906d0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -127,5 +127,33 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (DrawableSpinner != null) DrawableSpinner.ApplyCustomUpdateState -= UpdateStateTransforms; } + + /// + /// A simulating osu!stable's absolute screen-space, + /// for perfect placements of legacy spinner components with legacy coordinates. + /// + protected class LegacyCoordinatesContainer : Container + { + /// + /// An offset that simulates stable's spinner top offset, + /// for positioning some legacy spinner components perfectly as in stable. + /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners) + /// + public const float SPINNER_TOP_OFFSET = 29f; + + public LegacyCoordinatesContainer() + { + // legacy spinners relied heavily on absolute screen-space coordinate values. + // wrap everything in a container simulating absolute coords to preserve alignment + // as there are skins that depend on it. + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Size = new Vector2(640, 480); + + // since legacy coordinates were on screen-space, they were accounting for the playfield shift offset. + // therefore cancel it from here. + Position = new Vector2(0, -8f); + } + } } } From d528ef426fac4f30f380e35d12a2b4a99f59b69f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 28 Feb 2021 21:41:11 +0300 Subject: [PATCH 6793/6909] Reposition legacy spinner components in-line with osu!stable --- .../Skinning/Legacy/LegacyOldStyleSpinner.cs | 44 ++++++++----------- .../Skinning/Legacy/LegacySpinner.cs | 40 +++++++++-------- 2 files changed, 39 insertions(+), 45 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs index 4e07cb60b3..7e9f73a89b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs @@ -33,39 +33,31 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { spinnerBlink = source.GetConfig(OsuSkinConfiguration.SpinnerNoBlink)?.Value != true; - AddInternal(new Container + AddRangeInternal(new Drawable[] { - // the old-style spinner relied heavily on absolute screen-space coordinate values. - // wrap everything in a container simulating absolute coords to preserve alignment - // as there are skins that depend on it. - Width = 640, - Height = 480, - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Children = new Drawable[] + new Sprite { - new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-background"), - Scale = new Vector2(SPRITE_SCALE) - }, - disc = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-circle"), - Scale = new Vector2(SPRITE_SCALE) - }, - metre = new Container + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-background"), + Scale = new Vector2(SPRITE_SCALE) + }, + disc = new Sprite + { + Anchor = Anchor.Centre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-circle"), + Scale = new Vector2(SPRITE_SCALE) + }, + new LegacyCoordinatesContainer + { + Child = metre = new Container { AutoSizeAxes = Axes.Both, // this anchor makes no sense, but that's what stable uses. Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - // adjustment for stable (metre has additional offset) - Margin = new MarginPadding { Top = 20 }, + Margin = new MarginPadding { Top = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET }, Masking = true, Child = metreSprite = new Sprite { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 94b6a906d0..1f1fd1fbd9 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -30,27 +30,29 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy DrawableSpinner = (DrawableSpinner)drawableHitObject; - AddRangeInternal(new[] + AddInternal(new LegacyCoordinatesContainer { - spin = new Sprite + Depth = float.MinValue, + Children = new Drawable[] { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Depth = float.MinValue, - Texture = source.GetTexture("spinner-spin"), - Scale = new Vector2(SPRITE_SCALE), - Y = 120 - 45 // offset temporarily to avoid overlapping default spin counter - }, - clear = new Sprite - { - Anchor = Anchor.Centre, - Origin = Anchor.Centre, - Depth = float.MinValue, - Alpha = 0, - Texture = source.GetTexture("spinner-clear"), - Scale = new Vector2(SPRITE_SCALE), - Y = -60 - }, + spin = new Sprite + { + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-spin"), + Scale = new Vector2(SPRITE_SCALE), + Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 335, + }, + clear = new Sprite + { + Alpha = 0, + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Texture = source.GetTexture("spinner-clear"), + Scale = new Vector2(SPRITE_SCALE), + Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 115, + }, + } }); } From 97bb217830e2f2e28942bb04083d3682c0c31c31 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Mar 2021 17:24:05 +0900 Subject: [PATCH 6794/6909] Fix test room playlist items not getting ids --- .../Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index 5e12156f3c..022c297ccd 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -35,6 +35,7 @@ namespace osu.Game.Tests.Visual.Multiplayer int currentScoreId = 0; int currentRoomId = 0; + int currentPlaylistItemId = 0; ((DummyAPIAccess)api).HandleRequest = req => { @@ -46,6 +47,9 @@ namespace osu.Game.Tests.Visual.Multiplayer createdRoom.CopyFrom(createRoomRequest.Room); createdRoom.RoomID.Value ??= currentRoomId++; + for (int i = 0; i < createdRoom.Playlist.Count; i++) + createdRoom.Playlist[i].ID = currentPlaylistItemId++; + rooms.Add(createdRoom); createRoomRequest.TriggerSuccess(createdRoom); break; From f7e4cfa4d0dce04258aad29b03917048954b93c4 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Mar 2021 17:24:32 +0900 Subject: [PATCH 6795/6909] Fix initial room settings not being returned correctly --- .../Multiplayer/TestMultiplayerClient.cs | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 379bb758c5..67679b2659 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -25,6 +25,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private IAPIProvider api { get; set; } = null!; + [Resolved] + private Room apiRoom { get; set; } = null!; + public void Connect() => isConnected.Value = true; public void Disconnect() => isConnected.Value = false; @@ -89,13 +92,28 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Task JoinRoom(long roomId) { - var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { User = api.LocalUser.Value }; + Debug.Assert(apiRoom != null); - var room = new MultiplayerRoom(roomId); - room.Users.Add(user); + var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) + { + User = api.LocalUser.Value + }; - if (room.Users.Count == 1) - room.Host = user; + var room = new MultiplayerRoom(roomId) + { + Settings = + { + Name = apiRoom.Name.Value, + BeatmapID = apiRoom.Playlist.Last().BeatmapID, + RulesetID = apiRoom.Playlist.Last().RulesetID, + BeatmapChecksum = apiRoom.Playlist.Last().Beatmap.Value.MD5Hash, + RequiredMods = apiRoom.Playlist.Last().RequiredMods.Select(m => new APIMod(m)).ToArray(), + AllowedMods = apiRoom.Playlist.Last().AllowedMods.Select(m => new APIMod(m)).ToArray(), + PlaylistItemId = apiRoom.Playlist.Last().ID + }, + Users = { user }, + Host = user + }; return Task.FromResult(room); } From 7adb33f40e352137e26b9cb82fc1ad675da881af Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Mar 2021 17:24:54 +0900 Subject: [PATCH 6796/6909] Fix beatmap getting nulled due to failing web request --- .../Online/Multiplayer/MultiplayerClient.cs | 25 ++++++++++++ .../Multiplayer/StatefulMultiplayerClient.cs | 38 ++++++++++--------- .../Multiplayer/TestMultiplayerClient.cs | 20 ++++++++++ 3 files changed, 65 insertions(+), 18 deletions(-) diff --git a/osu.Game/Online/Multiplayer/MultiplayerClient.cs b/osu.Game/Online/Multiplayer/MultiplayerClient.cs index 95d76f384f..4529dfd0a7 100644 --- a/osu.Game/Online/Multiplayer/MultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/MultiplayerClient.cs @@ -9,7 +9,9 @@ using System.Threading.Tasks; using Microsoft.AspNetCore.SignalR.Client; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Online.API; +using osu.Game.Online.API.Requests; using osu.Game.Online.Rooms; namespace osu.Game.Online.Multiplayer @@ -121,6 +123,29 @@ namespace osu.Game.Online.Multiplayer return connection.InvokeAsync(nameof(IMultiplayerServer.StartMatch)); } + protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) + { + var tcs = new TaskCompletionSource(); + var req = new GetBeatmapSetRequest(beatmapId, BeatmapSetLookupType.BeatmapId); + + req.Success += res => + { + if (cancellationToken.IsCancellationRequested) + { + tcs.SetCanceled(); + return; + } + + tcs.SetResult(res.ToBeatmapSet(Rulesets)); + }; + + req.Failure += e => tcs.SetException(e); + + API.Queue(req); + + return tcs.Task; + } + protected override void Dispose(bool isDisposing) { base.Dispose(isDisposing); diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index bfd505fb19..73100be505 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -17,8 +17,6 @@ using osu.Framework.Logging; using osu.Game.Beatmaps; using osu.Game.Database; using osu.Game.Online.API; -using osu.Game.Online.API.Requests; -using osu.Game.Online.API.Requests.Responses; using osu.Game.Online.Rooms; using osu.Game.Online.Rooms.RoomStatuses; using osu.Game.Rulesets; @@ -71,7 +69,7 @@ namespace osu.Game.Online.Multiplayer /// /// The corresponding to the local player, if available. /// - public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == api.LocalUser.Value.Id); + public MultiplayerRoomUser? LocalUser => Room?.Users.SingleOrDefault(u => u.User?.Id == API.LocalUser.Value.Id); /// /// Whether the is the host in . @@ -85,15 +83,15 @@ namespace osu.Game.Online.Multiplayer } } + [Resolved] + protected IAPIProvider API { get; private set; } = null!; + + [Resolved] + protected RulesetStore Rulesets { get; private set; } = null!; + [Resolved] private UserLookupCache userLookupCache { get; set; } = null!; - [Resolved] - private IAPIProvider api { get; set; } = null!; - - [Resolved] - private RulesetStore rulesets { get; set; } = null!; - // Only exists for compatibility with old osu-server-spectator build. // Todo: Can be removed on 2021/02/26. private long defaultPlaylistItemId; @@ -515,30 +513,26 @@ namespace osu.Game.Online.Multiplayer RoomUpdated?.Invoke(); - var req = new GetBeatmapSetRequest(settings.BeatmapID, BeatmapSetLookupType.BeatmapId); - req.Success += res => + GetOnlineBeatmapSet(settings.BeatmapID, cancellationToken).ContinueWith(set => Schedule(() => { if (cancellationToken.IsCancellationRequested) return; - updatePlaylist(settings, res); - }; - - api.Queue(req); + updatePlaylist(settings, set.Result); + }), TaskContinuationOptions.OnlyOnRanToCompletion); }, cancellationToken); - private void updatePlaylist(MultiplayerRoomSettings settings, APIBeatmapSet onlineSet) + private void updatePlaylist(MultiplayerRoomSettings settings, BeatmapSetInfo beatmapSet) { if (Room == null || !Room.Settings.Equals(settings)) return; Debug.Assert(apiRoom != null); - var beatmapSet = onlineSet.ToBeatmapSet(rulesets); var beatmap = beatmapSet.Beatmaps.Single(b => b.OnlineBeatmapID == settings.BeatmapID); beatmap.MD5Hash = settings.BeatmapChecksum; - var ruleset = rulesets.GetRuleset(settings.RulesetID).CreateInstance(); + var ruleset = Rulesets.GetRuleset(settings.RulesetID).CreateInstance(); var mods = settings.RequiredMods.Select(m => m.ToMod(ruleset)); var allowedMods = settings.AllowedMods.Select(m => m.ToMod(ruleset)); @@ -568,6 +562,14 @@ namespace osu.Game.Online.Multiplayer } } + /// + /// Retrieves a from an online source. + /// + /// The beatmap set ID. + /// A token to cancel the request. + /// The retrieval task. + protected abstract Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default); + /// /// For the provided user ID, update whether the user is included in . /// diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 67679b2659..6a901fc45b 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -3,12 +3,15 @@ #nullable enable +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Threading; using System.Threading.Tasks; using osu.Framework.Allocation; using osu.Framework.Bindables; +using osu.Game.Beatmaps; using osu.Game.Online.API; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; @@ -28,6 +31,9 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private Room apiRoom { get; set; } = null!; + [Resolved] + private BeatmapManager beatmaps { get; set; } = null!; + public void Connect() => isConnected.Value = true; public void Disconnect() => isConnected.Value = false; @@ -168,5 +174,19 @@ namespace osu.Game.Tests.Visual.Multiplayer return ((IMultiplayerClient)this).LoadRequested(); } + + protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) + { + Debug.Assert(Room != null); + Debug.Assert(apiRoom != null); + + var set = apiRoom.Playlist.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet + ?? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId)?.BeatmapSet; + + if (set == null) + throw new InvalidOperationException("Beatmap not found."); + + return Task.FromResult(set); + } } } From 5cfaf1de1b6d72cbbf900d0667bf2c2e48e6f77c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 1 Mar 2021 17:43:03 +0900 Subject: [PATCH 6797/6909] Fix duplicate ongoing operation tracker --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs index 2344ebea0e..8869718fd1 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSubScreen.cs @@ -3,12 +3,10 @@ using System.Linq; using NUnit.Framework; -using osu.Framework.Allocation; using osu.Framework.Screens; using osu.Framework.Testing; using osu.Game.Online.Rooms; using osu.Game.Rulesets.Osu; -using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Multiplayer; using osu.Game.Screens.OnlinePlay.Multiplayer.Match; using osu.Game.Tests.Beatmaps; @@ -20,9 +18,6 @@ namespace osu.Game.Tests.Visual.Multiplayer { private MultiplayerMatchSubScreen screen; - [Cached] - private OngoingOperationTracker ongoingOperationTracker = new OngoingOperationTracker(); - public TestSceneMultiplayerMatchSubScreen() : base(false) { From fe54a51b5a9c5995db488e1c8873fd1691463a3a Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 1 Mar 2021 22:41:09 +0300 Subject: [PATCH 6798/6909] Remove `UserRanks` object and move to outer `country_rank` property --- osu.Game/Users/UserStatistics.cs | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/osu.Game/Users/UserStatistics.cs b/osu.Game/Users/UserStatistics.cs index 78e6f5a05a..dc926898fc 100644 --- a/osu.Game/Users/UserStatistics.cs +++ b/osu.Game/Users/UserStatistics.cs @@ -29,16 +29,9 @@ namespace osu.Game.Users [JsonProperty(@"global_rank")] public int? GlobalRank; + [JsonProperty(@"country_rank")] public int? CountryRank; - [JsonProperty(@"rank")] - private UserRanks ranks - { - // eventually that will also become an own json property instead of reading from a `rank` object. - // see https://github.com/ppy/osu-web/blob/cb79bb72186c8f1a25f6a6f5ef315123decb4231/app/Transformers/UserStatisticsTransformer.php#L53. - set => CountryRank = value.Country; - } - // populated via User model, as that's where the data currently lives. public RankHistoryData RankHistory; @@ -119,13 +112,5 @@ namespace osu.Game.Users } } } - -#pragma warning disable 649 - private struct UserRanks - { - [JsonProperty(@"country")] - public int? Country; - } -#pragma warning restore 649 } } From 51a5652666d7d5e653fa28ddc0c74e23ddafe0f6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 1 Mar 2021 22:42:53 +0300 Subject: [PATCH 6799/6909] Refetch tournament users on null country rank --- osu.Game.Tournament/TournamentGameBase.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tournament/TournamentGameBase.cs b/osu.Game.Tournament/TournamentGameBase.cs index d506724017..2ee52c35aa 100644 --- a/osu.Game.Tournament/TournamentGameBase.cs +++ b/osu.Game.Tournament/TournamentGameBase.cs @@ -150,7 +150,9 @@ namespace osu.Game.Tournament { foreach (var p in t.Players) { - if (string.IsNullOrEmpty(p.Username) || p.Statistics?.GlobalRank == null) + if (string.IsNullOrEmpty(p.Username) + || p.Statistics?.GlobalRank == null + || p.Statistics?.CountryRank == null) { PopulateUser(p, immediate: true); addedInfo = true; From 2d3c3c18d4c1c3e1174079f2363a5d2e03b29c16 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:05:35 +0000 Subject: [PATCH 6800/6909] Bump SharpCompress from 0.27.1 to 0.28.1 Bumps [SharpCompress](https://github.com/adamhathcock/sharpcompress) from 0.27.1 to 0.28.1. - [Release notes](https://github.com/adamhathcock/sharpcompress/releases) - [Commits](https://github.com/adamhathcock/sharpcompress/compare/0.27.1...0.28.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 84a74502c2..4d086844e4 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -32,7 +32,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 2cea2e4b13..c0cfb7a96d 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -92,7 +92,7 @@ - + From 9db37e62d8dc33bd19ef35861dab19b5f861af86 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 1 Mar 2021 20:05:53 +0000 Subject: [PATCH 6801/6909] Bump Microsoft.AspNetCore.SignalR.Protocols.MessagePack Bumps [Microsoft.AspNetCore.SignalR.Protocols.MessagePack](https://github.com/dotnet/aspnetcore) from 5.0.2 to 5.0.3. - [Release notes](https://github.com/dotnet/aspnetcore/releases) - [Commits](https://github.com/dotnet/aspnetcore/compare/v5.0.2...v5.0.3) 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 84a74502c2..ca39c160a4 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 2cea2e4b13..c854ae7dff 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -80,7 +80,7 @@ - + From 2609b22d53626ff13206a88e70714b952ff5ff35 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Mon, 1 Mar 2021 23:07:25 +0300 Subject: [PATCH 6802/6909] Replace usage of `CurrentModeRank` in line with API change --- osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs | 4 ++-- .../Visual/Playlists/TestScenePlaylistsParticipantsList.cs | 2 +- osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs | 2 +- osu.Game/Users/User.cs | 5 +---- 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs index 9bece39ca0..e8d9ff72af 100644 --- a/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs +++ b/osu.Game.Tests/Visual/Online/TestSceneFriendDisplay.cs @@ -51,7 +51,7 @@ namespace osu.Game.Tests.Visual.Online Username = "flyte", Id = 3103765, IsOnline = true, - CurrentModeRank = 1111, + Statistics = new UserStatistics { GlobalRank = 1111 }, Country = new Country { FlagName = "JP" }, CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c6.jpg" }, @@ -60,7 +60,7 @@ namespace osu.Game.Tests.Visual.Online Username = "peppy", Id = 2, IsOnline = false, - CurrentModeRank = 2222, + Statistics = new UserStatistics { GlobalRank = 2222 }, Country = new Country { FlagName = "AU" }, CoverUrl = "https://osu.ppy.sh/images/headers/profile-covers/c3.jpg", IsSupporter = true, diff --git a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs index 8dd81e02e2..255f147ec9 100644 --- a/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs +++ b/osu.Game.Tests/Visual/Playlists/TestScenePlaylistsParticipantsList.cs @@ -20,7 +20,7 @@ namespace osu.Game.Tests.Visual.Playlists Room.RecentParticipants.Add(new User { Username = "peppy", - CurrentModeRank = 1234, + Statistics = new UserStatistics { GlobalRank = 1234 }, Id = 2 }); } diff --git a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs index e6fe6ac749..0922ce5ecc 100644 --- a/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs +++ b/osu.Game/Overlays/Dashboard/Friends/FriendDisplay.cs @@ -244,7 +244,7 @@ namespace osu.Game.Overlays.Dashboard.Friends return unsorted.OrderByDescending(u => u.LastVisit).ToList(); case UserSortCriteria.Rank: - return unsorted.OrderByDescending(u => u.CurrentModeRank.HasValue).ThenBy(u => u.CurrentModeRank ?? 0).ToList(); + return unsorted.OrderByDescending(u => u.Statistics.GlobalRank.HasValue).ThenBy(u => u.Statistics.GlobalRank ?? 0).ToList(); case UserSortCriteria.Username: return unsorted.OrderBy(u => u.Username).ToList(); diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 4a6fd540c7..4d537b91bd 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -72,9 +72,6 @@ namespace osu.Game.Users [JsonProperty(@"support_level")] public int SupportLevel; - [JsonProperty(@"current_mode_rank")] - public int? CurrentModeRank; - [JsonProperty(@"is_gmt")] public bool IsGMT; @@ -182,7 +179,7 @@ namespace osu.Game.Users private UserStatistics statistics; /// - /// User statistics for the requested ruleset (in the case of a response). + /// User statistics for the requested ruleset (in the case of a or response). /// Otherwise empty. /// [JsonProperty(@"statistics")] From d6925d09609c81bc8b8dc426d66440f7f25cedad Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 2 Mar 2021 00:43:44 +0000 Subject: [PATCH 6803/6909] Bump Moq from 4.16.0 to 4.16.1 Bumps [Moq](https://github.com/moq/moq4) from 4.16.0 to 4.16.1. - [Release notes](https://github.com/moq/moq4/releases) - [Changelog](https://github.com/moq/moq4/blob/main/CHANGELOG.md) - [Commits](https://github.com/moq/moq4/compare/v4.16.0...v4.16.1) Signed-off-by: dependabot-preview[bot] --- osu.Game.Tests.Android/osu.Game.Tests.Android.csproj | 2 +- osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj | 2 +- osu.Game.Tests/osu.Game.Tests.csproj | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj index 19e36a63f1..543f2f35a7 100644 --- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj +++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj @@ -71,7 +71,7 @@ - + \ No newline at end of file diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj index 67b2298f4c..e83bef4a95 100644 --- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj +++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj @@ -45,7 +45,7 @@ - + \ No newline at end of file diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 7e3868bd3b..877f41fbff 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -7,7 +7,7 @@ - + WinExe From b03efd69402995a6bc4ce62cf3b903ace5de396b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 2 Mar 2021 00:43:45 +0000 Subject: [PATCH 6804/6909] Bump Microsoft.NET.Test.Sdk from 16.8.3 to 16.9.1 Bumps [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest) from 16.8.3 to 16.9.1. - [Release notes](https://github.com/microsoft/vstest/releases) - [Commits](https://github.com/microsoft/vstest/compare/v16.8.3...v16.9.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 bf3aba5859..728af5124e 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 fcc0cafefc..af16f39563 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 b4c686ccea..3d2d1f3fec 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 2b084f3bee..fa00922706 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 7e3868bd3b..6c5ca937e2 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 77ae06d89c..b20583dd7e 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 7829a0636e5c021db48d058df16a6554313182d6 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 2 Mar 2021 00:43:47 +0000 Subject: [PATCH 6805/6909] Bump Sentry from 3.0.1 to 3.0.7 Bumps [Sentry](https://github.com/getsentry/sentry-dotnet) from 3.0.1 to 3.0.7. - [Release notes](https://github.com/getsentry/sentry-dotnet/releases) - [Changelog](https://github.com/getsentry/sentry-dotnet/blob/main/CHANGELOG.md) - [Commits](https://github.com/getsentry/sentry-dotnet/compare/3.0.1...3.0.7) 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 5ec7fb81fc..9916122a2a 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -31,7 +31,7 @@ - + From fa959291216feeb9e24174d74f9c3bc9a3882f36 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Mar 2021 16:07:09 +0900 Subject: [PATCH 6806/6909] Remove easy to remove finalizers --- osu.Game/Database/DatabaseWriteUsage.cs | 5 ----- osu.Game/Utils/SentryLogger.cs | 5 ----- 2 files changed, 10 deletions(-) diff --git a/osu.Game/Database/DatabaseWriteUsage.cs b/osu.Game/Database/DatabaseWriteUsage.cs index ddafd77066..84c39e3532 100644 --- a/osu.Game/Database/DatabaseWriteUsage.cs +++ b/osu.Game/Database/DatabaseWriteUsage.cs @@ -54,10 +54,5 @@ namespace osu.Game.Database Dispose(true); GC.SuppressFinalize(this); } - - ~DatabaseWriteUsage() - { - Dispose(false); - } } } diff --git a/osu.Game/Utils/SentryLogger.cs b/osu.Game/Utils/SentryLogger.cs index be9d01cde6..8f12760a6b 100644 --- a/osu.Game/Utils/SentryLogger.cs +++ b/osu.Game/Utils/SentryLogger.cs @@ -86,11 +86,6 @@ namespace osu.Game.Utils #region Disposal - ~SentryLogger() - { - Dispose(false); - } - public void Dispose() { Dispose(true); From c4ba045df158275175e0a84675106986ae96b946 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Mar 2021 16:07:51 +0900 Subject: [PATCH 6807/6909] Add note about finalizers required for audio store clean-up --- osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs | 1 + osu.Game/Skinning/Skin.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs index b31884d246..14aa3fe99a 100644 --- a/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs +++ b/osu.Game/Rulesets/UI/DrawableRulesetDependencies.cs @@ -63,6 +63,7 @@ namespace osu.Game.Rulesets.UI ~DrawableRulesetDependencies() { + // required to potentially clean up sample store from audio hierarchy. Dispose(false); } diff --git a/osu.Game/Skinning/Skin.cs b/osu.Game/Skinning/Skin.cs index e8d84b49f9..6b435cff0f 100644 --- a/osu.Game/Skinning/Skin.cs +++ b/osu.Game/Skinning/Skin.cs @@ -36,6 +36,7 @@ namespace osu.Game.Skinning ~Skin() { + // required to potentially clean up sample store from audio hierarchy. Dispose(false); } From 103dd4a6cea72ec399ac995acfe5270bd1da0de7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Mar 2021 16:14:43 +0900 Subject: [PATCH 6808/6909] Remove WorkingBeatmap's finalizer --- osu.Game/Beatmaps/BeatmapManager.cs | 4 ++++ osu.Game/Beatmaps/WorkingBeatmap.cs | 10 ---------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 3c6a6ba302..d653e5386b 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -20,6 +20,7 @@ using osu.Framework.IO.Stores; using osu.Framework.Lists; using osu.Framework.Logging; using osu.Framework.Platform; +using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Beatmaps.Formats; using osu.Game.Database; @@ -311,6 +312,9 @@ namespace osu.Game.Beatmaps workingCache.Add(working = new BeatmapManagerWorkingBeatmap(beatmapInfo, this)); + // best effort; may be higher than expected. + GlobalStatistics.Get(nameof(Beatmaps), $"Cached {nameof(WorkingBeatmap)}s").Value = workingCache.Count(); + return working; } } diff --git a/osu.Game/Beatmaps/WorkingBeatmap.cs b/osu.Game/Beatmaps/WorkingBeatmap.cs index aab8ff6bd6..f7f276230f 100644 --- a/osu.Game/Beatmaps/WorkingBeatmap.cs +++ b/osu.Game/Beatmaps/WorkingBeatmap.cs @@ -12,7 +12,6 @@ using osu.Framework.Audio; using osu.Framework.Audio.Track; using osu.Framework.Graphics.Textures; using osu.Framework.Logging; -using osu.Framework.Statistics; using osu.Framework.Testing; using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; @@ -34,8 +33,6 @@ namespace osu.Game.Beatmaps protected AudioManager AudioManager { get; } - private static readonly GlobalStatistic total_count = GlobalStatistics.Get(nameof(Beatmaps), $"Total {nameof(WorkingBeatmap)}s"); - protected WorkingBeatmap(BeatmapInfo beatmapInfo, AudioManager audioManager) { AudioManager = audioManager; @@ -47,8 +44,6 @@ namespace osu.Game.Beatmaps waveform = new RecyclableLazy(GetWaveform); storyboard = new RecyclableLazy(GetStoryboard); skin = new RecyclableLazy(GetSkin); - - total_count.Value++; } protected virtual Track GetVirtualTrack(double emptyLength = 0) @@ -331,11 +326,6 @@ namespace osu.Game.Beatmaps protected virtual ISkin GetSkin() => new DefaultSkin(); private readonly RecyclableLazy skin; - ~WorkingBeatmap() - { - total_count.Value--; - } - public class RecyclableLazy { private Lazy lazy; From 6372a0265af98f48afa15d4c4499a1a33250db6c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Mar 2021 17:44:56 +0900 Subject: [PATCH 6809/6909] Fix confine mode dropdown becoming visible again after filtering Changes from a hidden to a disabled state, with a tooltip explaining why. Closes #11851. --- .../Settings/Sections/Input/MouseSettings.cs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 455e13711d..768a18cca0 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -68,7 +68,21 @@ namespace osu.Game.Overlays.Settings.Sections.Input }; windowMode = config.GetBindable(FrameworkSetting.WindowMode); - windowMode.BindValueChanged(mode => confineMouseModeSetting.Alpha = mode.NewValue == WindowMode.Fullscreen ? 0 : 1, true); + windowMode.BindValueChanged(mode => + { + var isFullscreen = mode.NewValue == WindowMode.Fullscreen; + + if (isFullscreen) + { + confineMouseModeSetting.Current.Disabled = true; + confineMouseModeSetting.TooltipText = "Not applicable in full screen mode"; + } + else + { + confineMouseModeSetting.Current.Disabled = false; + confineMouseModeSetting.TooltipText = string.Empty; + } + }, true); if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) { From 0300a554476c72fb0e07774350f0fc79687718c2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 2 Mar 2021 18:00:50 +0900 Subject: [PATCH 6810/6909] 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 5d83bb9583..c428cd2546 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 9916122a2a..2528292e17 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index b4f981162a..56a24bea12 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -91,7 +91,7 @@ - + From 30ff0b83c199a20ddcd2e86beb643842f1263daf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 2 Mar 2021 19:06:21 +0900 Subject: [PATCH 6811/6909] Fix test failures due to unpopulated room --- .../Tests/Visual/Multiplayer/MultiplayerTestScene.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs index 2e8c834c65..7775c2bd24 100644 --- a/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs +++ b/osu.Game/Tests/Visual/Multiplayer/MultiplayerTestScene.cs @@ -7,8 +7,10 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Game.Online.Multiplayer; +using osu.Game.Online.Rooms; using osu.Game.Screens.OnlinePlay; using osu.Game.Screens.OnlinePlay.Lounge.Components; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Multiplayer { @@ -48,7 +50,16 @@ namespace osu.Game.Tests.Visual.Multiplayer RoomManager.Schedule(() => RoomManager.PartRoom()); if (joinRoom) + { + Room.Name.Value = "test name"; + Room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + }); + RoomManager.Schedule(() => RoomManager.CreateRoom(Room)); + } }); public override void SetUpSteps() From 711cf3e5111e46f293b8aba2216c082378944eb3 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 2 Mar 2021 17:25:36 +0300 Subject: [PATCH 6812/6909] Add mobile logs location to issue templates --- .github/ISSUE_TEMPLATE/01-bug-issues.md | 2 ++ .github/ISSUE_TEMPLATE/02-crash-issues.md | 2 ++ 2 files changed, 4 insertions(+) diff --git a/.github/ISSUE_TEMPLATE/01-bug-issues.md b/.github/ISSUE_TEMPLATE/01-bug-issues.md index 0b80ce44dd..6050036cbf 100644 --- a/.github/ISSUE_TEMPLATE/01-bug-issues.md +++ b/.github/ISSUE_TEMPLATE/01-bug-issues.md @@ -13,4 +13,6 @@ about: Issues regarding encountered bugs. *please attach logs here, which are located at:* - `%AppData%/osu/logs` *(on Windows),* - `~/.local/share/osu/logs` *(on Linux & 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. (https://support.apple.com/en-us/HT201301#copy-to-computer) --> diff --git a/.github/ISSUE_TEMPLATE/02-crash-issues.md b/.github/ISSUE_TEMPLATE/02-crash-issues.md index ada8de73c0..04170312d1 100644 --- a/.github/ISSUE_TEMPLATE/02-crash-issues.md +++ b/.github/ISSUE_TEMPLATE/02-crash-issues.md @@ -13,6 +13,8 @@ about: Issues regarding crashes or permanent freezes. *please attach logs here, which are located at:* - `%AppData%/osu/logs` *(on Windows),* - `~/.local/share/osu/logs` *(on Linux & 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. (https://support.apple.com/en-us/HT201301#copy-to-computer) --> **Computer Specifications:** From 40a28367c63a2cf7e3c87eeccaf617b1f25564a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Mar 2021 18:50:33 +0100 Subject: [PATCH 6813/6909] Fix restore-to-default buttons never showing if initially hidden --- osu.Game/Overlays/Settings/SettingsItem.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 4cb8d7f83c..c5890a6fbb 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -147,6 +147,7 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.Y; Width = SettingsPanel.CONTENT_MARGINS; Alpha = 0f; + AlwaysPresent = true; } [BackgroundDependencyLoader] From 3b125a26a863e61d20a9a5018ab10383c2486611 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Mar 2021 19:18:01 +0100 Subject: [PATCH 6814/6909] Add test coverage --- .../Visual/Settings/TestSceneSettingsItem.cs | 43 +++++++++++++++++++ osu.Game/Overlays/Settings/SettingsItem.cs | 2 +- 2 files changed, 44 insertions(+), 1 deletion(-) create mode 100644 osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs diff --git a/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.cs new file mode 100644 index 0000000000..8f1c17ed29 --- /dev/null +++ b/osu.Game.Tests/Visual/Settings/TestSceneSettingsItem.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.Linq; +using NUnit.Framework; +using osu.Framework.Bindables; +using osu.Framework.Graphics; +using osu.Framework.Testing; +using osu.Game.Overlays.Settings; + +namespace osu.Game.Tests.Visual.Settings +{ + [TestFixture] + public class TestSceneSettingsItem : OsuTestScene + { + [Test] + public void TestRestoreDefaultValueButtonVisibility() + { + TestSettingsTextBox textBox = null; + + AddStep("create settings item", () => Child = textBox = new TestSettingsTextBox + { + Current = new Bindable + { + Default = "test", + Value = "test" + } + }); + AddAssert("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0); + + AddStep("change value from default", () => textBox.Current.Value = "non-default"); + AddUntilStep("restore button shown", () => textBox.RestoreDefaultValueButton.Alpha > 0); + + AddStep("restore default", () => textBox.Current.SetDefault()); + AddUntilStep("restore button hidden", () => textBox.RestoreDefaultValueButton.Alpha == 0); + } + + private class TestSettingsTextBox : SettingsTextBox + { + public new Drawable RestoreDefaultValueButton => this.ChildrenOfType().Single(); + } + } +} diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index c5890a6fbb..8631b8ac7b 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -121,7 +121,7 @@ namespace osu.Game.Overlays.Settings labelText.Alpha = controlWithCurrent.Current.Disabled ? 0.3f : 1; } - private class RestoreDefaultValueButton : Container, IHasTooltip + protected internal class RestoreDefaultValueButton : Container, IHasTooltip { private Bindable bindable; From 26736d990f792f15bd0c92f7f5a100a8f800816d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Mar 2021 19:42:47 +0100 Subject: [PATCH 6815/6909] Enable filter parsing extensibility --- osu.Game/Screens/Select/FilterQueryParser.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 4b6b3be45c..3f1b80ee1c 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -11,7 +11,7 @@ namespace osu.Game.Screens.Select internal static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( - @"\b(?stars|ar|dr|hp|cs|divisor|length|objects|bpm|status|creator|artist)(?[=:><]+)(?("".*"")|(\S*))", + @"\b(?\w+)(?[=:><]+)(?("".*"")|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static void ApplyQueries(FilterCriteria criteria, string query) @@ -22,15 +22,14 @@ namespace osu.Game.Screens.Select var op = match.Groups["op"].Value; var value = match.Groups["value"].Value; - parseKeywordCriteria(criteria, key, value, op); - - query = query.Replace(match.ToString(), ""); + if (tryParseKeywordCriteria(criteria, key, value, op)) + query = query.Replace(match.ToString(), ""); } criteria.SearchText = query; } - private static void parseKeywordCriteria(FilterCriteria criteria, string key, string value, string op) + private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, string op) { switch (key) { @@ -75,7 +74,12 @@ namespace osu.Game.Screens.Select case "artist": updateCriteriaText(ref criteria.Artist, op, value); break; + + default: + return false; } + + return true; } private static int getLengthScale(string value) => From e46543a4a924d6bab3b7ef67fa8d3d992d6504bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Mar 2021 19:56:36 +0100 Subject: [PATCH 6816/6909] Constrain operator parsing better --- .../Filtering/FilterQueryParserTest.cs | 9 ++ osu.Game/Screens/Select/Filter/Operator.cs | 17 +++ osu.Game/Screens/Select/FilterQueryParser.cs | 131 ++++++++++-------- 3 files changed, 99 insertions(+), 58 deletions(-) create mode 100644 osu.Game/Screens/Select/Filter/Operator.cs diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index d15682b1eb..e121cb835c 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -194,5 +194,14 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(1, filterCriteria.SearchTerms.Length); Assert.AreEqual("double\"quote", filterCriteria.Artist.SearchTerm); } + + [Test] + public void TestOperatorParsing() + { + const string query = "artist=>. Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +namespace osu.Game.Screens.Select.Filter +{ + /// + /// Defines logical operators that can be used in the song select search box keyword filters. + /// + public enum Operator + { + Less, + LessOrEqual, + Equal, + GreaterOrEqual, + Greater + } +} diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index 3f1b80ee1c..d2d33b13f5 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -5,13 +5,14 @@ using System; using System.Globalization; using System.Text.RegularExpressions; using osu.Game.Beatmaps; +using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.Select { internal static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( - @"\b(?\w+)(?[=:><]+)(?("".*"")|(\S*))", + @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*"")|(\S*))", RegexOptions.Compiled | RegexOptions.IgnoreCase); internal static void ApplyQueries(FilterCriteria criteria, string query) @@ -19,7 +20,7 @@ namespace osu.Game.Screens.Select foreach (Match match in query_syntax_regex.Matches(query)) { var key = match.Groups["key"].Value.ToLower(); - var op = match.Groups["op"].Value; + var op = parseOperator(match.Groups["op"].Value); var value = match.Groups["value"].Value; if (tryParseKeywordCriteria(criteria, key, value, op)) @@ -29,57 +30,72 @@ namespace osu.Game.Screens.Select criteria.SearchText = query; } - private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, string op) + private static bool tryParseKeywordCriteria(FilterCriteria criteria, string key, string value, Operator op) { switch (key) { case "stars" when parseFloatWithPoint(value, out var stars): - updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2); - break; + return updateCriteriaRange(ref criteria.StarDifficulty, op, stars, 0.01f / 2); case "ar" when parseFloatWithPoint(value, out var ar): - updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2); - break; + return updateCriteriaRange(ref criteria.ApproachRate, op, ar, 0.1f / 2); case "dr" when parseFloatWithPoint(value, out var dr): case "hp" when parseFloatWithPoint(value, out dr): - updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); - break; + return updateCriteriaRange(ref criteria.DrainRate, op, dr, 0.1f / 2); case "cs" when parseFloatWithPoint(value, out var cs): - updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2); - break; + return updateCriteriaRange(ref criteria.CircleSize, op, cs, 0.1f / 2); case "bpm" when parseDoubleWithPoint(value, out var bpm): - updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2); - break; + return updateCriteriaRange(ref criteria.BPM, op, bpm, 0.01d / 2); case "length" when parseDoubleWithPoint(value.TrimEnd('m', 's', 'h'), out var length): var scale = getLengthScale(value); - updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0); - break; + return updateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0); case "divisor" when parseInt(value, out var divisor): - updateCriteriaRange(ref criteria.BeatDivisor, op, divisor); - break; + return updateCriteriaRange(ref criteria.BeatDivisor, op, divisor); case "status" when Enum.TryParse(value, true, out var statusValue): - updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue); - break; + return updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue); case "creator": - updateCriteriaText(ref criteria.Creator, op, value); - break; + return updateCriteriaText(ref criteria.Creator, op, value); case "artist": - updateCriteriaText(ref criteria.Artist, op, value); - break; + return updateCriteriaText(ref criteria.Artist, op, value); default: return false; } + } - return true; + private static Operator parseOperator(string value) + { + switch (value) + { + case "=": + case ":": + return Operator.Equal; + + case "<": + return Operator.Less; + + case "<=": + case "<:": + return Operator.LessOrEqual; + + case ">": + return Operator.Greater; + + case ">=": + case ">:": + return Operator.GreaterOrEqual; + + default: + throw new ArgumentOutOfRangeException(nameof(value), $"Unsupported operator {value}"); + } } private static int getLengthScale(string value) => @@ -97,120 +113,119 @@ namespace osu.Game.Screens.Select private static bool parseInt(string value, out int result) => int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result); - private static void updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, string op, string value) + private static bool updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value) { switch (op) { - case "=": - case ":": + case Operator.Equal: textFilter.SearchTerm = value.Trim('"'); - break; + return true; + + default: + return false; } } - private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, float value, float tolerance = 0.05f) + private static bool updateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, float value, float tolerance = 0.05f) { switch (op) { default: - return; + return false; - case "=": - case ":": + case Operator.Equal: range.Min = value - tolerance; range.Max = value + tolerance; break; - case ">": + case Operator.Greater: range.Min = value + tolerance; break; - case ">=": - case ">:": + case Operator.GreaterOrEqual: range.Min = value - tolerance; break; - case "<": + case Operator.Less: range.Max = value - tolerance; break; - case "<=": - case "<:": + case Operator.LessOrEqual: range.Max = value + tolerance; break; } + + return true; } - private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, double value, double tolerance = 0.05) + private static bool updateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, double value, double tolerance = 0.05) { switch (op) { default: - return; + return false; - case "=": - case ":": + case Operator.Equal: range.Min = value - tolerance; range.Max = value + tolerance; break; - case ">": + case Operator.Greater: range.Min = value + tolerance; break; - case ">=": - case ">:": + case Operator.GreaterOrEqual: range.Min = value - tolerance; break; - case "<": + case Operator.Less: range.Max = value - tolerance; break; - case "<=": - case "<:": + case Operator.LessOrEqual: range.Max = value + tolerance; break; } + + return true; } - private static void updateCriteriaRange(ref FilterCriteria.OptionalRange range, string op, T value) + private static bool updateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, T value) where T : struct { switch (op) { default: - return; + return false; - case "=": - case ":": + case Operator.Equal: range.IsLowerInclusive = range.IsUpperInclusive = true; range.Min = value; range.Max = value; break; - case ">": + case Operator.Greater: range.IsLowerInclusive = false; range.Min = value; break; - case ">=": - case ">:": + case Operator.GreaterOrEqual: range.IsLowerInclusive = true; range.Min = value; break; - case "<": + case Operator.Less: range.IsUpperInclusive = false; range.Max = value; break; - case "<=": - case "<:": + case Operator.LessOrEqual: range.IsUpperInclusive = true; range.Max = value; break; } + + return true; } } } From 14e249a13405e834a7ea90b32cc3e8246efc37be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Mar 2021 20:07:11 +0100 Subject: [PATCH 6817/6909] Add ruleset interface for extending filter criteria --- .../Rulesets/Filter/IRulesetFilterCriteria.cs | 44 +++++++++++++++++++ osu.Game/Rulesets/Ruleset.cs | 7 +++ 2 files changed, 51 insertions(+) create mode 100644 osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs new file mode 100644 index 0000000000..a83f87d72b --- /dev/null +++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using osu.Game.Beatmaps; +using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; + +namespace osu.Game.Rulesets.Filter +{ + /// + /// Allows for extending the beatmap filtering capabilities of song select (as implemented in ) + /// with ruleset-specific criteria. + /// + public interface IRulesetFilterCriteria + { + /// + /// Checks whether the supplied satisfies ruleset-specific custom criteria, + /// in addition to the ones mandated by song select. + /// + /// The beatmap to test the criteria against. + /// + /// true if the beatmap matches the ruleset-specific custom filtering criteria, + /// false otherwise. + /// + bool Matches(BeatmapInfo beatmap); + + /// + /// Attempts to parse a single custom keyword criterion, given by the user via the song select search box. + /// The format of the criterion is: + /// + /// {key}{op}{value} + /// + /// + /// The key (name) of the criterion. + /// The operator in the criterion. + /// The value of the criterion. + /// + /// true if the keyword criterion is valid, false if it has been ignored. + /// Valid criteria are stripped from , + /// while ignored criteria are included in . + /// + bool TryParseCustomKeywordCriteria(string key, Operator op, string value); + } +} diff --git a/osu.Game/Rulesets/Ruleset.cs b/osu.Game/Rulesets/Ruleset.cs index dbc2bd4d01..38d30a2e31 100644 --- a/osu.Game/Rulesets/Ruleset.cs +++ b/osu.Game/Rulesets/Ruleset.cs @@ -26,6 +26,7 @@ using JetBrains.Annotations; using osu.Framework.Extensions; using osu.Framework.Extensions.EnumExtensions; using osu.Framework.Testing; +using osu.Game.Rulesets.Filter; using osu.Game.Screens.Ranking.Statistics; namespace osu.Game.Rulesets @@ -306,5 +307,11 @@ namespace osu.Game.Rulesets /// The result type to get the name for. /// The display name. public virtual string GetDisplayNameForHitResult(HitResult result) => result.GetDescription(); + + /// + /// Creates ruleset-specific beatmap filter criteria to be used on the song select screen. + /// + [CanBeNull] + public virtual IRulesetFilterCriteria CreateRulesetFilterCriteria() => null; } } From c375be6b07b7d4bc19d29683c61a9f4da6529d52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Mar 2021 20:10:03 +0100 Subject: [PATCH 6818/6909] Instantiate ruleset criteria --- osu.Game/Screens/Select/FilterControl.cs | 8 ++++++++ osu.Game/Screens/Select/FilterCriteria.cs | 4 ++++ 2 files changed, 12 insertions(+) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index eafd8a87d1..983928ac51 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -2,6 +2,7 @@ // See the LICENCE file in the repository root for full licence text. using System; +using System.Diagnostics; using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Graphics; @@ -34,8 +35,13 @@ namespace osu.Game.Screens.Select private Bindable groupMode; + [Resolved] + private RulesetStore rulesets { get; set; } + public FilterCriteria CreateCriteria() { + Debug.Assert(ruleset.Value.ID != null); + var query = searchTextBox.Text; var criteria = new FilterCriteria @@ -53,6 +59,8 @@ namespace osu.Game.Screens.Select if (!maximumStars.IsDefault) criteria.UserStarDifficulty.Max = maximumStars.Value; + criteria.RulesetCriteria = rulesets.GetRuleset(ruleset.Value.ID.Value).CreateInstance().CreateRulesetFilterCriteria(); + FilterQueryParser.ApplyQueries(criteria, query); return criteria; } diff --git a/osu.Game/Screens/Select/FilterCriteria.cs b/osu.Game/Screens/Select/FilterCriteria.cs index 7bddb3e51b..208048380a 100644 --- a/osu.Game/Screens/Select/FilterCriteria.cs +++ b/osu.Game/Screens/Select/FilterCriteria.cs @@ -8,6 +8,7 @@ using JetBrains.Annotations; using osu.Game.Beatmaps; using osu.Game.Collections; using osu.Game.Rulesets; +using osu.Game.Rulesets.Filter; using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.Select @@ -69,6 +70,9 @@ namespace osu.Game.Screens.Select [CanBeNull] public BeatmapCollection Collection; + [CanBeNull] + public IRulesetFilterCriteria RulesetCriteria { get; set; } + public struct OptionalRange : IEquatable> where T : struct { From 42c3309d4918db8044c312d28f3efbc7422caae1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Mar 2021 20:11:21 +0100 Subject: [PATCH 6819/6909] Use ruleset criteria in parsing and filtering --- osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs | 3 +++ osu.Game/Screens/Select/FilterQueryParser.cs | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs index 1aab50037a..521b90202d 100644 --- a/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs +++ b/osu.Game/Screens/Select/Carousel/CarouselBeatmap.cs @@ -73,6 +73,9 @@ namespace osu.Game.Screens.Select.Carousel if (match) match &= criteria.Collection?.Beatmaps.Contains(Beatmap) ?? true; + if (match && criteria.RulesetCriteria != null) + match &= criteria.RulesetCriteria.Matches(Beatmap); + Filtered.Value = !match; } diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index d2d33b13f5..c81a72d938 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -67,7 +67,7 @@ namespace osu.Game.Screens.Select return updateCriteriaText(ref criteria.Artist, op, value); default: - return false; + return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false; } } From bf72f9ad1e988f14dbd8ca5b87f55c64b52d9c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Mar 2021 20:22:56 +0100 Subject: [PATCH 6820/6909] Add tests for custom parsing logic --- .../Filtering/FilterQueryParserTest.cs | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index e121cb835c..d835e58b29 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -4,7 +4,9 @@ using System; using NUnit.Framework; using osu.Game.Beatmaps; +using osu.Game.Rulesets.Filter; using osu.Game.Screens.Select; +using osu.Game.Screens.Select.Filter; namespace osu.Game.Tests.NonVisual.Filtering { @@ -203,5 +205,43 @@ namespace osu.Game.Tests.NonVisual.Filtering FilterQueryParser.ApplyQueries(filterCriteria, query); Assert.AreEqual("> true; + + public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) + { + if (key == "custom" && op == Operator.Equal) + { + CustomValue = value; + return true; + } + + return false; + } + } } } From faf5fbf49b5a940b41ba3e7b1336fbefd868895a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Tue, 2 Mar 2021 20:27:50 +0100 Subject: [PATCH 6821/6909] Add tests for custom matching logic --- .../NonVisual/Filtering/FilterMatchingTest.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs index 24a0a662ba..8ff2743b6a 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterMatchingTest.cs @@ -4,8 +4,10 @@ using NUnit.Framework; using osu.Game.Beatmaps; using osu.Game.Rulesets; +using osu.Game.Rulesets.Filter; using osu.Game.Screens.Select; using osu.Game.Screens.Select.Carousel; +using osu.Game.Screens.Select.Filter; namespace osu.Game.Tests.NonVisual.Filtering { @@ -214,5 +216,31 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual(filtered, carouselItem.Filtered.Value); } + + [Test] + public void TestCustomRulesetCriteria([Values(null, true, false)] bool? matchCustomCriteria) + { + var beatmap = getExampleBeatmap(); + + var customCriteria = matchCustomCriteria is bool match ? new CustomCriteria(match) : null; + var criteria = new FilterCriteria { RulesetCriteria = customCriteria }; + var carouselItem = new CarouselBeatmap(beatmap); + carouselItem.Filter(criteria); + + Assert.AreEqual(matchCustomCriteria == false, carouselItem.Filtered.Value); + } + + private class CustomCriteria : IRulesetFilterCriteria + { + private readonly bool match; + + public CustomCriteria(bool shouldMatch) + { + match = shouldMatch; + } + + public bool Matches(BeatmapInfo beatmap) => match; + public bool TryParseCustomKeywordCriteria(string key, Operator op, string value) => false; + } } } From 6e75ebbb06fb6bf394e257bc44877b3f7171923f Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 14:02:01 +0900 Subject: [PATCH 6822/6909] Add interface to handle local beatmap presentation logic --- osu.Game/Screens/IHandlePresentBeatmap.cs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 osu.Game/Screens/IHandlePresentBeatmap.cs diff --git a/osu.Game/Screens/IHandlePresentBeatmap.cs b/osu.Game/Screens/IHandlePresentBeatmap.cs new file mode 100644 index 0000000000..b94df630ef --- /dev/null +++ b/osu.Game/Screens/IHandlePresentBeatmap.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.Rulesets; + +namespace osu.Game.Screens +{ + /// + /// Denotes a screen which can handle beatmap / ruleset selection via local logic. + /// This is used in the flow to handle cases which require custom logic, + /// for instance, if a lease is held on the Beatmap. + /// + public interface IHandlePresentBeatmap + { + /// + /// Invoked with a requested beatmap / ruleset for selection. + /// + /// The beatmap to be selected. + /// The ruleset to be selected. + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset); + } +} From 36e1fb6da80a1416900d07ae9c69c9b12e8876ff Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 14:04:00 +0900 Subject: [PATCH 6823/6909] Add flow to allow MatchSubScreen to handle beatmap presentation locally --- osu.Game/OsuGame.cs | 13 ++++++++++--- osu.Game/PerformFromMenuRunner.cs | 7 +------ .../Multiplayer/MultiplayerMatchSongSelect.cs | 19 +++++++++++++++++++ .../Multiplayer/MultiplayerMatchSubScreen.cs | 12 +++++++++++- 4 files changed, 41 insertions(+), 10 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 771bcd2310..1e0cb587e9 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -381,9 +381,16 @@ namespace osu.Game ?? beatmaps.FirstOrDefault(b => b.Ruleset.Equals(Ruleset.Value)) ?? beatmaps.First(); - Ruleset.Value = selection.Ruleset; - Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); - }, validScreens: new[] { typeof(SongSelect) }); + if (screen is IHandlePresentBeatmap presentableScreen) + { + presentableScreen.PresentBeatmap(BeatmapManager.GetWorkingBeatmap(selection), selection.Ruleset); + } + else + { + Ruleset.Value = selection.Ruleset; + Beatmap.Value = BeatmapManager.GetWorkingBeatmap(selection); + } + }, validScreens: new[] { typeof(SongSelect), typeof(IHandlePresentBeatmap) }); } /// diff --git a/osu.Game/PerformFromMenuRunner.cs b/osu.Game/PerformFromMenuRunner.cs index fe75a3a607..6f979b8dc8 100644 --- a/osu.Game/PerformFromMenuRunner.cs +++ b/osu.Game/PerformFromMenuRunner.cs @@ -5,11 +5,9 @@ using System; using System.Collections.Generic; using System.Linq; using osu.Framework.Allocation; -using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Screens; using osu.Framework.Threading; -using osu.Game.Beatmaps; using osu.Game.Overlays; using osu.Game.Overlays.Dialog; using osu.Game.Overlays.Notifications; @@ -30,9 +28,6 @@ namespace osu.Game [Resolved] private DialogOverlay dialogOverlay { get; set; } - [Resolved] - private IBindable beatmap { get; set; } - [Resolved(canBeNull: true)] private OsuGame game { get; set; } @@ -90,7 +85,7 @@ namespace osu.Game var type = current.GetType(); // check if we are already at a valid target screen. - if (validScreens.Any(t => t.IsAssignableFrom(type)) && !beatmap.Disabled) + if (validScreens.Any(t => t.IsAssignableFrom(type))) { finalAction(current); Cancel(); diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs index f17d97c3fd..c9f0f6de90 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSongSelect.cs @@ -4,9 +4,11 @@ using osu.Framework.Allocation; using osu.Framework.Logging; using osu.Framework.Screens; +using osu.Game.Beatmaps; using osu.Game.Graphics.UserInterface; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.Select; @@ -19,6 +21,23 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer private LoadingLayer loadingLayer; + /// + /// Construct a new instance of multiplayer song select. + /// + /// An optional initial beatmap selection to perform. + /// An optional initial ruleset selection to perform. + public MultiplayerMatchSongSelect(WorkingBeatmap beatmap = null, RulesetInfo ruleset = null) + { + if (beatmap != null || ruleset != null) + { + Schedule(() => + { + if (beatmap != null) Beatmap.Value = beatmap; + if (ruleset != null) Ruleset.Value = ruleset; + }); + } + } + [BackgroundDependencyLoader] private void load() { diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4fbea4e3be..06d83e495c 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -12,9 +12,11 @@ using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Screens; using osu.Framework.Threading; +using osu.Game.Beatmaps; using osu.Game.Configuration; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Rulesets; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -29,7 +31,7 @@ using ParticipantsList = osu.Game.Screens.OnlinePlay.Multiplayer.Participants.Pa namespace osu.Game.Screens.OnlinePlay.Multiplayer { [Cached] - public class MultiplayerMatchSubScreen : RoomSubScreen + public class MultiplayerMatchSubScreen : RoomSubScreen, IHandlePresentBeatmap { public override string Title { get; } @@ -394,5 +396,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer modSettingChangeTracker?.Dispose(); } + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + if (!this.IsCurrentScreen()) + return; + + this.Push(new MultiplayerMatchSongSelect(beatmap, ruleset)); + } } } From fcea900a5327cc3d421c7332ed001026f6521388 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 14:06:39 +0900 Subject: [PATCH 6824/6909] Move main menu (song select) presentation logic to a local implementation Reduces cross-dependencies between OsuGame and MainMenu. --- osu.Game/OsuGame.cs | 4 ---- osu.Game/Screens/Menu/MainMenu.cs | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 1e0cb587e9..6f760a1aa7 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -361,10 +361,6 @@ namespace osu.Game PerformFromScreen(screen => { - // we might already be at song select, so a check is required before performing the load to solo. - if (screen is MainMenu) - menuScreen.LoadToSolo(); - // we might even already be at the song if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true)) return; diff --git a/osu.Game/Screens/Menu/MainMenu.cs b/osu.Game/Screens/Menu/MainMenu.cs index 424e6d2cd5..baeb86c976 100644 --- a/osu.Game/Screens/Menu/MainMenu.cs +++ b/osu.Game/Screens/Menu/MainMenu.cs @@ -9,12 +9,14 @@ using osu.Framework.Bindables; using osu.Framework.Graphics; using osu.Framework.Platform; using osu.Framework.Screens; +using osu.Game.Beatmaps; 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.Rulesets; using osu.Game.Screens.Backgrounds; using osu.Game.Screens.Edit; using osu.Game.Screens.OnlinePlay.Multiplayer; @@ -23,7 +25,7 @@ using osu.Game.Screens.Select; namespace osu.Game.Screens.Menu { - public class MainMenu : OsuScreen + public class MainMenu : OsuScreen, IHandlePresentBeatmap { public const float FADE_IN_DURATION = 300; @@ -104,7 +106,7 @@ namespace osu.Game.Screens.Menu Beatmap.SetDefault(); this.Push(new Editor()); }, - OnSolo = onSolo, + OnSolo = loadSoloSongSelect, OnMultiplayer = () => this.Push(new Multiplayer()), OnPlaylists = () => this.Push(new Playlists()), OnExit = confirmAndExit, @@ -160,9 +162,7 @@ namespace osu.Game.Screens.Menu LoadComponentAsync(songSelect = new PlaySongSelect()); } - public void LoadToSolo() => Schedule(onSolo); - - private void onSolo() => this.Push(consumeSongSelect()); + private void loadSoloSongSelect() => this.Push(consumeSongSelect()); private Screen consumeSongSelect() { @@ -289,5 +289,13 @@ namespace osu.Game.Screens.Menu this.FadeOut(3000); return base.OnExiting(next); } + + public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset) + { + Beatmap.Value = beatmap; + Ruleset.Value = ruleset; + + Schedule(loadSoloSongSelect); + } } } From 7c5904008247a113525396f9c4e1603ef470a464 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 14:17:06 +0900 Subject: [PATCH 6825/6909] Re-present even when already the current beatmap This feels better and closer to what a user would expect. --- osu.Game/OsuGame.cs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 6f760a1aa7..7db85d0d66 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -361,10 +361,6 @@ namespace osu.Game PerformFromScreen(screen => { - // we might even already be at the song - if (Beatmap.Value.BeatmapSetInfo.Hash == databasedSet.Hash && (difficultyCriteria?.Invoke(Beatmap.Value.BeatmapInfo) ?? true)) - return; - // Find beatmaps that match our predicate. var beatmaps = databasedSet.Beatmaps.Where(b => difficultyCriteria?.Invoke(b) ?? true).ToList(); From 7dce9b04fa0a7870d7576c803c8130a095124484 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 14:50:45 +0900 Subject: [PATCH 6826/6909] Add a more basic ConfirmDialog implementation --- osu.Game/Overlays/Dialog/ConfirmDialog.cs | 45 ++++++++++++++++++++++ osu.Game/Screens/Menu/ConfirmExitDialog.cs | 26 +++---------- 2 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 osu.Game/Overlays/Dialog/ConfirmDialog.cs diff --git a/osu.Game/Overlays/Dialog/ConfirmDialog.cs b/osu.Game/Overlays/Dialog/ConfirmDialog.cs new file mode 100644 index 0000000000..6f160daf97 --- /dev/null +++ b/osu.Game/Overlays/Dialog/ConfirmDialog.cs @@ -0,0 +1,45 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using osu.Framework.Graphics.Sprites; + +namespace osu.Game.Overlays.Dialog +{ + /// + /// A dialog which confirms a user action. + /// + public class ConfirmDialog : PopupDialog + { + protected PopupDialogOkButton ButtonConfirm; + protected PopupDialogCancelButton ButtonCancel; + + /// + /// Construct a new dialog. + /// + /// The description of the action to be displayed to the user. + /// An action to perform on confirmation. + /// An optional action to perform on cancel. + public ConfirmDialog(string description, Action onConfirm, Action onCancel = null) + { + HeaderText = $"Are you sure you want to {description}?"; + BodyText = "Last chance to back out."; + + Icon = FontAwesome.Solid.ExclamationTriangle; + + Buttons = new PopupDialogButton[] + { + ButtonConfirm = new PopupDialogOkButton + { + Text = @"Yes", + Action = onConfirm + }, + ButtonCancel = new PopupDialogCancelButton + { + Text = @"Cancel", + Action = onCancel + }, + }; + } + } +} diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs index d120eb21a8..41cc7b480c 100644 --- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs @@ -2,33 +2,17 @@ // 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 class ConfirmExitDialog : ConfirmDialog { - public ConfirmExitDialog(Action confirm, Action cancel) + public ConfirmExitDialog(Action confirm, Action onCancel = null) + : base("exit osu!", confirm, onCancel) { - 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 - }, - }; + ButtonConfirm.Text = "Let me out!"; + ButtonCancel.Text = "Just a little more..."; } } } From d332fd2414c131a363437e1c71fb27047eaa48c1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 14:53:47 +0900 Subject: [PATCH 6827/6909] Handle case where local user tries to change beatmap while not the host --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 06d83e495c..e09e1fc3d4 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -402,6 +402,14 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!this.IsCurrentScreen()) return; + if (!client.IsHost) + { + // todo: should handle this when the request queue is implemented. + // if we decide that the presentation should exit the user from the multiplayer game, the PresentBeatmap + // flow may need to change to support an "unable to present" return value. + return; + } + this.Push(new MultiplayerMatchSongSelect(beatmap, ruleset)); } } From cb4c3503a01da92d0a68222702fa3b958da05fe1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 14:50:54 +0900 Subject: [PATCH 6828/6909] Confirm exiting a multiplayer match --- .../Multiplayer/MultiplayerMatchSubScreen.cs | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 4fbea4e3be..51445b0668 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -15,6 +15,8 @@ using osu.Framework.Threading; using osu.Game.Configuration; using osu.Game.Online.Multiplayer; using osu.Game.Online.Rooms; +using osu.Game.Overlays; +using osu.Game.Overlays.Dialog; using osu.Game.Rulesets.Mods; using osu.Game.Screens.OnlinePlay.Components; using osu.Game.Screens.OnlinePlay.Match; @@ -279,14 +281,36 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); } + [Resolved] + private DialogOverlay dialogOverlay { get; set; } + + private bool exitConfirmed; + public override bool OnBackButton() { - if (client.Room != null && settingsOverlay.State.Value == Visibility.Visible) + if (client.Room == null) + { + // room has not been created yet; exit immediately. + return base.OnBackButton(); + } + + if (settingsOverlay.State.Value == Visibility.Visible) { settingsOverlay.Hide(); return true; } + if (!exitConfirmed) + { + dialogOverlay.Push(new ConfirmDialog("leave this multiplayer match", () => + { + exitConfirmed = true; + this.Exit(); + })); + + return true; + } + return base.OnBackButton(); } From 0ede28da2f0f79b11cf2355cda4264f4d686ad12 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 15:24:55 +0900 Subject: [PATCH 6829/6909] Fix test failures due to missing dependency --- .../OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index 51445b0668..f1d8bf97fd 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -281,7 +281,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer Mods.Value = client.LocalUser.Mods.Select(m => m.ToMod(ruleset)).Concat(SelectedItem.Value.RequiredMods).ToList(); } - [Resolved] + [Resolved(canBeNull: true)] private DialogOverlay dialogOverlay { get; set; } private bool exitConfirmed; @@ -300,7 +300,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer return true; } - if (!exitConfirmed) + if (!exitConfirmed && dialogOverlay != null) { dialogOverlay.Push(new ConfirmDialog("leave this multiplayer match", () => { From 002646370ccd589f94bf1d6947982961393003e6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 16:47:42 +0900 Subject: [PATCH 6830/6909] Move bindable logic in MouseSettings to LoadComplete --- .../Settings/Sections/Input/MouseSettings.cs | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index 768a18cca0..7599a748ab 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -17,7 +17,11 @@ 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 configSensitivity; + + private Bindable localSensitivity = new BindableDouble(); + private Bindable ignoredInputHandlers; private Bindable windowMode; @@ -26,12 +30,12 @@ namespace osu.Game.Overlays.Settings.Sections.Input [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); + configSensitivity = config.GetBindable(FrameworkSetting.CursorSensitivity); + localSensitivity = configSensitivity.GetUnboundCopy(); + + windowMode = config.GetBindable(FrameworkSetting.WindowMode); + ignoredInputHandlers = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); Children = new Drawable[] { @@ -43,7 +47,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input new SensitivitySetting { LabelText = "Cursor sensitivity", - Current = sensitivityBindable + Current = localSensitivity }, new SettingsCheckbox { @@ -66,8 +70,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input Current = osuConfig.GetBindable(OsuSetting.MouseDisableButtons) }, }; + } + + protected override void LoadComplete() + { + base.LoadComplete(); + + configSensitivity.BindValueChanged(val => localSensitivity.Value = val.NewValue, true); + localSensitivity.BindValueChanged(val => configSensitivity.Value = val.NewValue); - windowMode = config.GetBindable(FrameworkSetting.WindowMode); windowMode.BindValueChanged(mode => { var isFullscreen = mode.NewValue == WindowMode.Fullscreen; @@ -87,7 +98,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) { rawInputToggle.Disabled = true; - sensitivityBindable.Disabled = true; + localSensitivity.Disabled = true; } else { @@ -100,12 +111,11 @@ namespace osu.Game.Overlays.Settings.Sections.Input ignoredInputHandlers.Value = enabled.NewValue ? standard_mouse_handlers : raw_mouse_handler; }; - ignoredInputHandlers = config.GetBindable(FrameworkSetting.IgnoredInputHandlers); ignoredInputHandlers.ValueChanged += handler => { bool raw = !handler.NewValue.Contains("Raw"); rawInputToggle.Value = raw; - sensitivityBindable.Disabled = !raw; + localSensitivity.Disabled = !raw; }; ignoredInputHandlers.TriggerChange(); From 012b48dbe51e972b291f37f88dfe0788cd9adb84 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Mar 2021 19:03:44 +0900 Subject: [PATCH 6831/6909] Remove explicit public definition Interface members are public by default. --- osu.Game/Screens/IHandlePresentBeatmap.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Screens/IHandlePresentBeatmap.cs b/osu.Game/Screens/IHandlePresentBeatmap.cs index b94df630ef..60801fb3eb 100644 --- a/osu.Game/Screens/IHandlePresentBeatmap.cs +++ b/osu.Game/Screens/IHandlePresentBeatmap.cs @@ -18,6 +18,6 @@ namespace osu.Game.Screens /// /// The beatmap to be selected. /// The ruleset to be selected. - public void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset); + void PresentBeatmap(WorkingBeatmap beatmap, RulesetInfo ruleset); } } From 6affe33fb275acb9d3feee55d08433f88fb1e25a Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Mar 2021 19:40:19 +0900 Subject: [PATCH 6832/6909] Fix another test scene --- .../TestSceneMultiplayerRoomManager.cs | 46 +++++++++++++------ .../Multiplayer/TestMultiplayerClient.cs | 14 ++++-- .../TestMultiplayerRoomContainer.cs | 10 ++-- .../Multiplayer/TestMultiplayerRoomManager.cs | 8 ++-- 4 files changed, 52 insertions(+), 26 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs index 6de5704410..91c15de69f 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerRoomManager.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; using NUnit.Framework; using osu.Framework.Graphics; using osu.Framework.Testing; using osu.Game.Online.Rooms; +using osu.Game.Screens.OnlinePlay.Components; +using osu.Game.Tests.Beatmaps; namespace osu.Game.Tests.Visual.Multiplayer { @@ -21,15 +24,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room { Name = { Value = "1" } }); + roomManager.CreateRoom(createRoom(r => r.Name.Value = "1")); roomManager.PartRoom(); - roomManager.CreateRoom(new Room { Name = { Value = "2" } }); + roomManager.CreateRoom(createRoom(r => r.Name.Value = "2")); roomManager.PartRoom(); roomManager.ClearRooms(); }); }); - AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2); AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); } @@ -40,16 +43,16 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); }); }); AddStep("disconnect", () => roomContainer.Client.Disconnect()); - AddAssert("rooms cleared", () => roomManager.Rooms.Count == 0); + AddAssert("rooms cleared", () => ((RoomManager)roomManager).Rooms.Count == 0); AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); } @@ -60,9 +63,9 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); }); }); @@ -70,7 +73,7 @@ namespace osu.Game.Tests.Visual.Multiplayer AddStep("disconnect", () => roomContainer.Client.Disconnect()); AddStep("connect", () => roomContainer.Client.Connect()); - AddAssert("manager polled for rooms", () => roomManager.Rooms.Count == 2); + AddAssert("manager polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 2); AddAssert("initial rooms received", () => roomManager.InitialRoomsReceived.Value); } @@ -81,12 +84,12 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.ClearRooms(); }); }); - AddAssert("manager not polled for rooms", () => roomManager.Rooms.Count == 0); + AddAssert("manager not polled for rooms", () => ((RoomManager)roomManager).Rooms.Count == 0); AddAssert("initial rooms not received", () => !roomManager.InitialRoomsReceived.Value); } @@ -97,7 +100,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); }); }); @@ -111,7 +114,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - roomManager.CreateRoom(new Room()); + roomManager.CreateRoom(createRoom()); roomManager.PartRoom(); }); }); @@ -126,7 +129,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { createRoomManager().With(d => d.OnLoadComplete += _ => { - var r = new Room(); + var r = createRoom(); roomManager.CreateRoom(r); roomManager.PartRoom(); roomManager.JoinRoom(r); @@ -136,6 +139,21 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("multiplayer room joined", () => roomContainer.Client.Room != null); } + private Room createRoom(Action initFunc = null) + { + var room = new Room(); + + room.Name.Value = "test room"; + room.Playlist.Add(new PlaylistItem + { + Beatmap = { Value = new TestBeatmap(Ruleset.Value).BeatmapInfo }, + Ruleset = { Value = Ruleset.Value } + }); + + initFunc?.Invoke(room); + return room; + } + private TestMultiplayerRoomManager createRoomManager() { Child = roomContainer = new TestMultiplayerRoomContainer diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index 6a901fc45b..c03364a391 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -28,12 +28,16 @@ namespace osu.Game.Tests.Visual.Multiplayer [Resolved] private IAPIProvider api { get; set; } = null!; - [Resolved] - private Room apiRoom { get; set; } = null!; - [Resolved] private BeatmapManager beatmaps { get; set; } = null!; + private readonly TestMultiplayerRoomManager roomManager; + + public TestMultiplayerClient(TestMultiplayerRoomManager roomManager) + { + this.roomManager = roomManager; + } + public void Connect() => isConnected.Value = true; public void Disconnect() => isConnected.Value = false; @@ -98,7 +102,7 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Task JoinRoom(long roomId) { - Debug.Assert(apiRoom != null); + var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == roomId); var user = new MultiplayerRoomUser(api.LocalUser.Value.Id) { @@ -178,8 +182,8 @@ namespace osu.Game.Tests.Visual.Multiplayer protected override Task GetOnlineBeatmapSet(int beatmapId, CancellationToken cancellationToken = default) { Debug.Assert(Room != null); - Debug.Assert(apiRoom != null); + var apiRoom = roomManager.Rooms.Single(r => r.RoomID.Value == Room.RoomID); var set = apiRoom.Playlist.FirstOrDefault(p => p.BeatmapID == beatmapId)?.Beatmap.Value.BeatmapSet ?? beatmaps.QueryBeatmap(b => b.OnlineBeatmapID == beatmapId)?.BeatmapSet; diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs index 860caef071..e57411d04d 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomContainer.cs @@ -32,11 +32,15 @@ namespace osu.Game.Tests.Visual.Multiplayer { RelativeSizeAxes = Axes.Both; + RoomManager = new TestMultiplayerRoomManager(); + Client = new TestMultiplayerClient(RoomManager); + OngoingOperationTracker = new OngoingOperationTracker(); + AddRangeInternal(new Drawable[] { - Client = new TestMultiplayerClient(), - RoomManager = new TestMultiplayerRoomManager(), - OngoingOperationTracker = new OngoingOperationTracker(), + Client, + RoomManager, + OngoingOperationTracker, content = new Container { RelativeSizeAxes = Axes.Both } }); } diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs index 022c297ccd..7e824c4d7c 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerRoomManager.cs @@ -27,7 +27,7 @@ namespace osu.Game.Tests.Visual.Multiplayer [Cached] public readonly Bindable Filter = new Bindable(new FilterCriteria()); - private readonly List rooms = new List(); + public new readonly List Rooms = new List(); protected override void LoadComplete() { @@ -50,7 +50,7 @@ namespace osu.Game.Tests.Visual.Multiplayer for (int i = 0; i < createdRoom.Playlist.Count; i++) createdRoom.Playlist[i].ID = currentPlaylistItemId++; - rooms.Add(createdRoom); + Rooms.Add(createdRoom); createRoomRequest.TriggerSuccess(createdRoom); break; @@ -65,7 +65,7 @@ namespace osu.Game.Tests.Visual.Multiplayer case GetRoomsRequest getRoomsRequest: var roomsWithoutParticipants = new List(); - foreach (var r in rooms) + foreach (var r in Rooms) { var newRoom = new Room(); @@ -79,7 +79,7 @@ namespace osu.Game.Tests.Visual.Multiplayer break; case GetRoomRequest getRoomRequest: - getRoomRequest.TriggerSuccess(rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId)); + getRoomRequest.TriggerSuccess(Rooms.Single(r => r.RoomID.Value == getRoomRequest.RoomId)); break; case GetBeatmapSetRequest getBeatmapSetRequest: From 0f5bce70ad4eec310f54113341e747e552b2a4e4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 20:34:36 +0900 Subject: [PATCH 6833/6909] Split confirmation dialog classes apart --- osu.Game/Overlays/Dialog/ConfirmDialog.cs | 17 +++++----- osu.Game/Screens/Menu/ConfirmExitDialog.cs | 31 ++++++++++++++++--- .../Multiplayer/MultiplayerMatchSubScreen.cs | 2 +- 3 files changed, 34 insertions(+), 16 deletions(-) diff --git a/osu.Game/Overlays/Dialog/ConfirmDialog.cs b/osu.Game/Overlays/Dialog/ConfirmDialog.cs index 6f160daf97..a87c06ffdf 100644 --- a/osu.Game/Overlays/Dialog/ConfirmDialog.cs +++ b/osu.Game/Overlays/Dialog/ConfirmDialog.cs @@ -11,30 +11,27 @@ namespace osu.Game.Overlays.Dialog /// public class ConfirmDialog : PopupDialog { - protected PopupDialogOkButton ButtonConfirm; - protected PopupDialogCancelButton ButtonCancel; - /// - /// Construct a new dialog. + /// Construct a new confirmation dialog. /// - /// The description of the action to be displayed to the user. + /// The description of the action to be displayed to the user. /// An action to perform on confirmation. /// An optional action to perform on cancel. - public ConfirmDialog(string description, Action onConfirm, Action onCancel = null) + public ConfirmDialog(string message, Action onConfirm, Action onCancel = null) { - HeaderText = $"Are you sure you want to {description}?"; - BodyText = "Last chance to back out."; + HeaderText = message; + BodyText = "Last chance to turn back"; Icon = FontAwesome.Solid.ExclamationTriangle; Buttons = new PopupDialogButton[] { - ButtonConfirm = new PopupDialogOkButton + new PopupDialogOkButton { Text = @"Yes", Action = onConfirm }, - ButtonCancel = new PopupDialogCancelButton + new PopupDialogCancelButton { Text = @"Cancel", Action = onCancel diff --git a/osu.Game/Screens/Menu/ConfirmExitDialog.cs b/osu.Game/Screens/Menu/ConfirmExitDialog.cs index 41cc7b480c..6488a2fd63 100644 --- a/osu.Game/Screens/Menu/ConfirmExitDialog.cs +++ b/osu.Game/Screens/Menu/ConfirmExitDialog.cs @@ -2,17 +2,38 @@ // 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 : ConfirmDialog + public class ConfirmExitDialog : PopupDialog { - public ConfirmExitDialog(Action confirm, Action onCancel = null) - : base("exit osu!", confirm, onCancel) + /// + /// Construct a new exit confirmation dialog. + /// + /// An action to perform on confirmation. + /// An optional action to perform on cancel. + public ConfirmExitDialog(Action onConfirm, Action onCancel = null) { - ButtonConfirm.Text = "Let me out!"; - ButtonCancel.Text = "Just a little more..."; + HeaderText = "Are you sure you want to exit osu!?"; + BodyText = "Last chance to turn back"; + + Icon = FontAwesome.Solid.ExclamationTriangle; + + Buttons = new PopupDialogButton[] + { + new PopupDialogOkButton + { + Text = @"Let me out!", + Action = onConfirm + }, + new PopupDialogCancelButton + { + Text = @"Just a little more...", + Action = onCancel + }, + }; } } } diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs index f1d8bf97fd..5a9a26d997 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerMatchSubScreen.cs @@ -302,7 +302,7 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer if (!exitConfirmed && dialogOverlay != null) { - dialogOverlay.Push(new ConfirmDialog("leave this multiplayer match", () => + dialogOverlay.Push(new ConfirmDialog("Are you sure you want to leave this multiplayer match?", () => { exitConfirmed = true; this.Exit(); From 534e16237a5e74dd448af1d37e72d580393e5ba3 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 3 Mar 2021 20:36:41 +0900 Subject: [PATCH 6834/6909] Remove unnecessary intial construction of 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 7599a748ab..c3deb385cd 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -20,7 +20,7 @@ namespace osu.Game.Overlays.Settings.Sections.Input private Bindable configSensitivity; - private Bindable localSensitivity = new BindableDouble(); + private Bindable localSensitivity; private Bindable ignoredInputHandlers; From 1ecb1d122a55500204eaea01d2321a1a9c71c707 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Mar 2021 21:54:34 +0900 Subject: [PATCH 6835/6909] Fix up TestSceneMultiplayer --- .../Multiplayer/TestSceneMultiplayer.cs | 36 ++++++------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index 2e39471dc0..bb5db5b803 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -1,13 +1,13 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using NUnit.Framework; +using osu.Framework.Allocation; +using osu.Game.Online.Multiplayer; using osu.Game.Screens.OnlinePlay.Components; -using osu.Game.Users; namespace osu.Game.Tests.Visual.Multiplayer { - public class TestSceneMultiplayer : MultiplayerTestScene + public class TestSceneMultiplayer : ScreenTestScene { public TestSceneMultiplayer() { @@ -17,30 +17,16 @@ namespace osu.Game.Tests.Visual.Multiplayer AddUntilStep("wait for loaded", () => multi.IsLoaded); } - [Test] - public void TestOneUserJoinedMultipleTimes() - { - var user = new User { Id = 33 }; - - AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); - - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); - } - - [Test] - public void TestOneUserLeftMultipleTimes() - { - var user = new User { Id = 44 }; - - AddStep("add user", () => Client.AddUser(user)); - AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); - - AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); - AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); - } - private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer { + [Cached(typeof(StatefulMultiplayerClient))] + public readonly TestMultiplayerClient Client; + + public TestMultiplayer() + { + AddInternal(Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager)); + } + protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); } } From 0f83b66cdabb1aad42d7f9d1c205b38089d7b2c2 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Mar 2021 22:01:03 +0900 Subject: [PATCH 6836/6909] Add separate test for stateful multiplayer client --- .../StatefulMultiplayerClientTest.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs diff --git a/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.cs new file mode 100644 index 0000000000..82ce588c6f --- /dev/null +++ b/osu.Game.Tests/OnlinePlay/StatefulMultiplayerClientTest.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 NUnit.Framework; +using osu.Framework.Testing; +using osu.Game.Tests.Visual.Multiplayer; +using osu.Game.Users; + +namespace osu.Game.Tests.OnlinePlay +{ + [HeadlessTest] + public class StatefulMultiplayerClientTest : MultiplayerTestScene + { + [Test] + public void TestUserAddedOnJoin() + { + var user = new User { Id = 33 }; + + AddRepeatStep("add user multiple times", () => Client.AddUser(user), 3); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + } + + [Test] + public void TestUserRemovedOnLeave() + { + var user = new User { Id = 44 }; + + AddStep("add user", () => Client.AddUser(user)); + AddAssert("room has 2 users", () => Client.Room?.Users.Count == 2); + + AddRepeatStep("remove user multiple times", () => Client.RemoveUser(user), 3); + AddAssert("room has 1 user", () => Client.Room?.Users.Count == 1); + } + } +} From 77607c06eba37de48cb0670a4e7e09d9a9c8e4ae Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Wed, 3 Mar 2021 22:07:39 +0900 Subject: [PATCH 6837/6909] Fix not being able to enter gameplay in TestSceneMultiplayer --- .../Visual/Multiplayer/TestSceneMultiplayer.cs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs index bb5db5b803..78bc51e47b 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayer.cs @@ -9,12 +9,21 @@ namespace osu.Game.Tests.Visual.Multiplayer { public class TestSceneMultiplayer : ScreenTestScene { + private TestMultiplayer multiplayerScreen; + public TestSceneMultiplayer() { - var multi = new TestMultiplayer(); + AddStep("show", () => + { + multiplayerScreen = new TestMultiplayer(); - AddStep("show", () => LoadScreen(multi)); - AddUntilStep("wait for loaded", () => multi.IsLoaded); + // Needs to be added at a higher level since the multiplayer screen becomes non-current. + Child = multiplayerScreen.Client; + + LoadScreen(multiplayerScreen); + }); + + AddUntilStep("wait for loaded", () => multiplayerScreen.IsLoaded); } private class TestMultiplayer : Screens.OnlinePlay.Multiplayer.Multiplayer @@ -24,7 +33,7 @@ namespace osu.Game.Tests.Visual.Multiplayer public TestMultiplayer() { - AddInternal(Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager)); + Client = new TestMultiplayerClient((TestMultiplayerRoomManager)RoomManager); } protected override RoomManager CreateRoomManager() => new TestMultiplayerRoomManager(); From f9148eec206b1126ed0af01f24610d2eca2ab00d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Mar 2021 21:33:41 +0100 Subject: [PATCH 6838/6909] Refactor filter query parsing helper methods In preparation for exposition as public. --- .../Filtering/FilterQueryParserTest.cs | 11 +++ osu.Game/Screens/Select/FilterQueryParser.cs | 75 ++++++++++++------- 2 files changed, 59 insertions(+), 27 deletions(-) diff --git a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs index d835e58b29..49389e67aa 100644 --- a/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs +++ b/osu.Game.Tests/NonVisual/Filtering/FilterQueryParserTest.cs @@ -215,6 +215,17 @@ namespace osu.Game.Tests.NonVisual.Filtering Assert.AreEqual("unrecognised=keyword", filterCriteria.SearchText); } + [TestCase("cs=nope")] + [TestCase("bpm>=bad")] + [TestCase("divisor(value, true, out var statusValue): - return updateCriteriaRange(ref criteria.OnlineStatus, op, statusValue); + case "status": + return tryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value, + (string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val)); case "creator": - return updateCriteriaText(ref criteria.Creator, op, value); + return tryUpdateCriteriaText(ref criteria.Creator, op, value); case "artist": - return updateCriteriaText(ref criteria.Artist, op, value); + return tryUpdateCriteriaText(ref criteria.Artist, op, value); default: return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false; @@ -104,16 +104,16 @@ namespace osu.Game.Screens.Select value.EndsWith('m') ? 60000 : value.EndsWith('h') ? 3600000 : 1000; - private static bool parseFloatWithPoint(string value, out float result) => + private static bool tryParseFloatWithPoint(string value, out float result) => float.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result); - private static bool parseDoubleWithPoint(string value, out double result) => + private static bool tryParseDoubleWithPoint(string value, out double result) => double.TryParse(value, NumberStyles.AllowDecimalPoint, CultureInfo.InvariantCulture, out result); - private static bool parseInt(string value, out int result) => + private static bool tryParseInt(string value, out int result) => int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result); - private static bool updateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value) + private static bool tryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value) { switch (op) { @@ -126,7 +126,10 @@ namespace osu.Game.Screens.Select } } - private static bool updateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, float value, float tolerance = 0.05f) + private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string val, float tolerance = 0.05f) + => tryParseFloatWithPoint(val, out float value) && tryUpdateCriteriaRange(ref range, op, value, tolerance); + + private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, float value, float tolerance = 0.05f) { switch (op) { @@ -158,7 +161,10 @@ namespace osu.Game.Screens.Select return true; } - private static bool updateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, double value, double tolerance = 0.05) + private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string val, double tolerance = 0.05) + => tryParseDoubleWithPoint(val, out double value) && tryUpdateCriteriaRange(ref range, op, value, tolerance); + + private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, double value, double tolerance = 0.05) { switch (op) { @@ -190,7 +196,13 @@ namespace osu.Game.Screens.Select return true; } - private static bool updateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, T value) + private delegate bool TryParseFunction(string val, out T value); + + private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string value, TryParseFunction conversionFunc) + where T : struct + => conversionFunc.Invoke(value, out var converted) && tryUpdateCriteriaRange(ref range, op, converted); + + private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, T value) where T : struct { switch (op) @@ -227,5 +239,14 @@ namespace osu.Game.Screens.Select return true; } + + private static bool tryUpdateLengthRange(FilterCriteria criteria, Operator op, string val) + { + if (!tryParseDoubleWithPoint(val.TrimEnd('m', 's', 'h'), out var length)) + return false; + + var scale = getLengthScale(val); + return tryUpdateCriteriaRange(ref criteria.Length, op, length * scale, scale / 2.0); + } } } From f733d1ec1fcf08a147d24aeab20d3e8187936c71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Wed, 3 Mar 2021 21:58:34 +0100 Subject: [PATCH 6839/6909] Expose and document query parser and helpers --- .../Rulesets/Filter/IRulesetFilterCriteria.cs | 11 +++ osu.Game/Screens/Select/FilterQueryParser.cs | 89 +++++++++++++++---- 2 files changed, 84 insertions(+), 16 deletions(-) diff --git a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs index a83f87d72b..13cc41f8e0 100644 --- a/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs +++ b/osu.Game/Rulesets/Filter/IRulesetFilterCriteria.cs @@ -31,6 +31,17 @@ namespace osu.Game.Rulesets.Filter /// {key}{op}{value} /// /// + /// + /// + /// For adding optional string criteria, can be used for matching, + /// along with for parsing. + /// + /// + /// For adding numerical-type range criteria, can be used for matching, + /// along with + /// and - and -typed overloads for parsing. + /// + /// /// The key (name) of the criterion. /// The operator in the criterion. /// The value of the criterion. diff --git a/osu.Game/Screens/Select/FilterQueryParser.cs b/osu.Game/Screens/Select/FilterQueryParser.cs index ce937d07b1..ea7f233bea 100644 --- a/osu.Game/Screens/Select/FilterQueryParser.cs +++ b/osu.Game/Screens/Select/FilterQueryParser.cs @@ -9,7 +9,10 @@ using osu.Game.Screens.Select.Filter; namespace osu.Game.Screens.Select { - internal static class FilterQueryParser + /// + /// Utility class used for parsing song select filter queries entered via the search box. + /// + public static class FilterQueryParser { private static readonly Regex query_syntax_regex = new Regex( @"\b(?\w+)(?(:|=|(>|<)(:|=)?))(?("".*"")|(\S*))", @@ -35,36 +38,36 @@ namespace osu.Game.Screens.Select switch (key) { case "stars": - return tryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2); + return TryUpdateCriteriaRange(ref criteria.StarDifficulty, op, value, 0.01d / 2); case "ar": - return tryUpdateCriteriaRange(ref criteria.ApproachRate, op, value); + return TryUpdateCriteriaRange(ref criteria.ApproachRate, op, value); case "dr": case "hp": - return tryUpdateCriteriaRange(ref criteria.DrainRate, op, value); + return TryUpdateCriteriaRange(ref criteria.DrainRate, op, value); case "cs": - return tryUpdateCriteriaRange(ref criteria.CircleSize, op, value); + return TryUpdateCriteriaRange(ref criteria.CircleSize, op, value); case "bpm": - return tryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2); + return TryUpdateCriteriaRange(ref criteria.BPM, op, value, 0.01d / 2); case "length": return tryUpdateLengthRange(criteria, op, value); case "divisor": - return tryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt); + return TryUpdateCriteriaRange(ref criteria.BeatDivisor, op, value, tryParseInt); case "status": - return tryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value, + return TryUpdateCriteriaRange(ref criteria.OnlineStatus, op, value, (string s, out BeatmapSetOnlineStatus val) => Enum.TryParse(value, true, out val)); case "creator": - return tryUpdateCriteriaText(ref criteria.Creator, op, value); + return TryUpdateCriteriaText(ref criteria.Creator, op, value); case "artist": - return tryUpdateCriteriaText(ref criteria.Artist, op, value); + return TryUpdateCriteriaText(ref criteria.Artist, op, value); default: return criteria.RulesetCriteria?.TryParseCustomKeywordCriteria(key, op, value) ?? false; @@ -113,7 +116,18 @@ namespace osu.Game.Screens.Select private static bool tryParseInt(string value, out int result) => int.TryParse(value, NumberStyles.None, CultureInfo.InvariantCulture, out result); - private static bool tryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value) + /// + /// Attempts to parse a keyword filter with the specified and textual . + /// If the value indicates a valid textual filter, the function returns true and the resulting data is stored into + /// . + /// + /// The to store the parsed data into, if successful. + /// + /// The operator for the keyword filter. + /// Only is valid for textual filters. + /// + /// The value of the keyword filter. + public static bool TryUpdateCriteriaText(ref FilterCriteria.OptionalTextFilter textFilter, Operator op, string value) { switch (op) { @@ -126,7 +140,20 @@ namespace osu.Game.Screens.Select } } - private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string val, float tolerance = 0.05f) + /// + /// Attempts to parse a keyword filter of type + /// from the specified and . + /// If can be parsed as a , the function returns true + /// and the resulting range constraint is stored into . + /// + /// + /// The -typed + /// to store the parsed data into, if successful. + /// + /// The operator for the keyword filter. + /// The value of the keyword filter. + /// Allowed tolerance of the parsed range boundary value. + public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string val, float tolerance = 0.05f) => tryParseFloatWithPoint(val, out float value) && tryUpdateCriteriaRange(ref range, op, value, tolerance); private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, float value, float tolerance = 0.05f) @@ -161,7 +188,20 @@ namespace osu.Game.Screens.Select return true; } - private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string val, double tolerance = 0.05) + /// + /// Attempts to parse a keyword filter of type + /// from the specified and . + /// If can be parsed as a , the function returns true + /// and the resulting range constraint is stored into . + /// + /// + /// The -typed + /// to store the parsed data into, if successful. + /// + /// The operator for the keyword filter. + /// The value of the keyword filter. + /// Allowed tolerance of the parsed range boundary value. + public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string val, double tolerance = 0.05) => tryParseDoubleWithPoint(val, out double value) && tryUpdateCriteriaRange(ref range, op, value, tolerance); private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, double value, double tolerance = 0.05) @@ -196,11 +236,28 @@ namespace osu.Game.Screens.Select return true; } - private delegate bool TryParseFunction(string val, out T value); + /// + /// Used to determine whether the string value can be converted to type . + /// If conversion can be performed, the delegate returns true + /// and the conversion result is returned in the out parameter . + /// + /// The string value to attempt parsing for. + /// The parsed value, if conversion is possible. + public delegate bool TryParseFunction(string val, out T parsed); - private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string value, TryParseFunction conversionFunc) + /// + /// Attempts to parse a keyword filter of type , + /// from the specified and . + /// If can be parsed into using , the function returns true + /// and the resulting range constraint is stored into . + /// + /// The to store the parsed data into, if successful. + /// The operator for the keyword filter. + /// The value of the keyword filter. + /// Function used to determine if can be converted to type . + public static bool TryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, string val, TryParseFunction parseFunction) where T : struct - => conversionFunc.Invoke(value, out var converted) && tryUpdateCriteriaRange(ref range, op, converted); + => parseFunction.Invoke(val, out var converted) && tryUpdateCriteriaRange(ref range, op, converted); private static bool tryUpdateCriteriaRange(ref FilterCriteria.OptionalRange range, Operator op, T value) where T : struct From fe64c3dbd4de6ada5be2ca5112c65c2f17abb607 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Mar 2021 14:59:08 +0300 Subject: [PATCH 6840/6909] Refrain from disabling cursor sensitivity at config-level --- osu.Game/OsuGame.cs | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 7db85d0d66..203cc458e0 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -880,13 +880,8 @@ namespace osu.Game switch (action) { case GlobalAction.ResetInputSettings: - var sensitivity = frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity); - - sensitivity.Disabled = false; - sensitivity.Value = 1; - sensitivity.Disabled = true; - - frameworkConfig.Set(FrameworkSetting.IgnoredInputHandlers, string.Empty); + frameworkConfig.GetBindable(FrameworkSetting.IgnoredInputHandlers).SetDefault(); + frameworkConfig.GetBindable(FrameworkSetting.CursorSensitivity).SetDefault(); frameworkConfig.GetBindable(FrameworkSetting.ConfineMouseMode).SetDefault(); return true; From 132fcda08987f880767bf20d2eb4a785f09dd9dd Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 4 Mar 2021 15:00:46 +0300 Subject: [PATCH 6841/6909] Force config sensitivity value to local setting bindable Re-enable the local bindable to update the sensitivity value then change back to whatever state it was in previously. --- .../Overlays/Settings/Sections/Input/MouseSettings.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs index c3deb385cd..3a78cff890 100644 --- a/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs +++ b/osu.Game/Overlays/Settings/Sections/Input/MouseSettings.cs @@ -76,7 +76,15 @@ namespace osu.Game.Overlays.Settings.Sections.Input { base.LoadComplete(); - configSensitivity.BindValueChanged(val => localSensitivity.Value = val.NewValue, true); + configSensitivity.BindValueChanged(val => + { + var disabled = localSensitivity.Disabled; + + localSensitivity.Disabled = false; + localSensitivity.Value = val.NewValue; + localSensitivity.Disabled = disabled; + }, true); + localSensitivity.BindValueChanged(val => configSensitivity.Value = val.NewValue); windowMode.BindValueChanged(mode => From 12b7d9e06d16b52a600c5506913c5569ee12b2ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 6 Mar 2021 12:16:01 +0100 Subject: [PATCH 6842/6909] Simplify custom filter criteria retrieval --- osu.Game/Screens/Select/FilterControl.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Screens/Select/FilterControl.cs b/osu.Game/Screens/Select/FilterControl.cs index 983928ac51..298b6e49bd 100644 --- a/osu.Game/Screens/Select/FilterControl.cs +++ b/osu.Game/Screens/Select/FilterControl.cs @@ -35,9 +35,6 @@ namespace osu.Game.Screens.Select private Bindable groupMode; - [Resolved] - private RulesetStore rulesets { get; set; } - public FilterCriteria CreateCriteria() { Debug.Assert(ruleset.Value.ID != null); @@ -59,7 +56,7 @@ namespace osu.Game.Screens.Select if (!maximumStars.IsDefault) criteria.UserStarDifficulty.Max = maximumStars.Value; - criteria.RulesetCriteria = rulesets.GetRuleset(ruleset.Value.ID.Value).CreateInstance().CreateRulesetFilterCriteria(); + criteria.RulesetCriteria = ruleset.Value.CreateInstance().CreateRulesetFilterCriteria(); FilterQueryParser.ApplyQueries(criteria, query); return criteria; From 06e42b4b4c2bab783e277fcbc86ef5f26efc50aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 6 Mar 2021 16:02:20 +0100 Subject: [PATCH 6843/6909] Fix taiko leaving behind empty judgements on legacy skins --- .../Skinning/Legacy/TaikoLegacySkinTransformer.cs | 2 +- osu.Game/Rulesets/Judgements/DrawableJudgement.cs | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 9f29675230..40dc149ec9 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { // if a taiko skin is providing explosion sprites, hide the judgements completely if (hasExplosion.Value) - return Drawable.Empty(); + return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue); } if (!(component is TaikoSkinComponent taikoComponent)) diff --git a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs index da9bb8a09d..feeafb7151 100644 --- a/osu.Game/Rulesets/Judgements/DrawableJudgement.cs +++ b/osu.Game/Rulesets/Judgements/DrawableJudgement.cs @@ -150,17 +150,13 @@ namespace osu.Game.Rulesets.Judgements } if (JudgementBody.Drawable is IAnimatableJudgement animatable) - { - var drawableAnimation = (Drawable)animatable; - animatable.PlayAnimation(); - // a derived version of DrawableJudgement may be proposing a lifetime. - // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime. - double lastTransformTime = drawableAnimation.LatestTransformEndTime; - if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd) - LifetimeEnd = lastTransformTime; - } + // a derived version of DrawableJudgement may be proposing a lifetime. + // if not adjusted (or the skinned portion requires greater bounds than calculated) use the skinned source's lifetime. + double lastTransformTime = JudgementBody.Drawable.LatestTransformEndTime; + if (LifetimeEnd == double.MaxValue || lastTransformTime > LifetimeEnd) + LifetimeEnd = lastTransformTime; } } From 1525480e73196e9cbbef3128b012e04e130c84b6 Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sat, 6 Mar 2021 19:18:40 +0300 Subject: [PATCH 6844/6909] Demonstrate value of `SPINNER_TOP_OFFSET` to being more sensible --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 1f1fd1fbd9..5df8f8a485 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -141,7 +141,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy /// for positioning some legacy spinner components perfectly as in stable. /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners) /// - public const float SPINNER_TOP_OFFSET = 29f; + public static readonly float SPINNER_TOP_OFFSET = (float)Math.Ceiling(45f * SPRITE_SCALE); public LegacyCoordinatesContainer() { From 8f4dadb06a393f760565ee61c3066644975b8c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 6 Mar 2021 15:06:16 +0100 Subject: [PATCH 6845/6909] Enable pooling for taiko judgements --- .../UI/DrawableTaikoJudgement.cs | 11 -------- osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs | 25 ++++++++++++++----- 2 files changed, 19 insertions(+), 17 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs index b5e35f88b5..1ad1e4495c 100644 --- a/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs +++ b/osu.Game.Rulesets.Taiko/UI/DrawableTaikoJudgement.cs @@ -3,7 +3,6 @@ using osu.Framework.Graphics; using osu.Game.Rulesets.Judgements; -using osu.Game.Rulesets.Objects.Drawables; namespace osu.Game.Rulesets.Taiko.UI { @@ -12,16 +11,6 @@ namespace osu.Game.Rulesets.Taiko.UI /// public class DrawableTaikoJudgement : DrawableJudgement { - /// - /// Creates a new judgement text. - /// - /// The object which is being judged. - /// The judgement to visualise. - public DrawableTaikoJudgement(JudgementResult result, DrawableHitObject judgedObject) - : base(result, judgedObject) - { - } - protected override void ApplyHitAnimations() { this.MoveToY(-100, 500); diff --git a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs index 148ec7755e..d2e7b604bb 100644 --- a/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs +++ b/osu.Game.Rulesets.Taiko/UI/TaikoPlayfield.cs @@ -2,10 +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.Graphics; using osu.Framework.Graphics.Containers; +using osu.Framework.Graphics.Pooling; using osu.Game.Beatmaps.ControlPoints; using osu.Game.Graphics; using osu.Game.Rulesets.Objects.Drawables; @@ -17,6 +19,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.Rulesets.Taiko.Scoring; using osu.Game.Skinning; using osuTK; @@ -38,6 +41,8 @@ namespace osu.Game.Rulesets.Taiko.UI internal Drawable HitTarget; private SkinnableDrawable mascot; + private readonly IDictionary> judgementPools = new Dictionary>(); + private ProxyContainer topLevelHitContainer; private Container rightArea; private Container leftArea; @@ -159,6 +164,12 @@ namespace osu.Game.Rulesets.Taiko.UI RegisterPool(5); RegisterPool(100); + + var hitWindows = new TaikoHitWindows(); + foreach (var result in Enum.GetValues(typeof(HitResult)).OfType().Where(r => hitWindows.IsHitResultAllowed(r))) + judgementPools.Add(result, new DrawablePool(15)); + + AddRangeInternal(judgementPools.Values); } protected override void LoadComplete() @@ -283,13 +294,15 @@ namespace osu.Game.Rulesets.Taiko.UI break; default: - judgementContainer.Add(new DrawableTaikoJudgement(result, judgedObject) + judgementContainer.Add(judgementPools[result.Type].Get(j => { - Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft, - Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre, - RelativePositionAxes = Axes.X, - X = result.IsHit ? judgedObject.Position.X : 0, - }); + j.Apply(result, judgedObject); + + j.Anchor = result.IsHit ? Anchor.TopLeft : Anchor.CentreLeft; + j.Origin = result.IsHit ? Anchor.BottomCentre : Anchor.Centre; + j.RelativePositionAxes = Axes.X; + j.X = result.IsHit ? judgedObject.Position.X : 0; + })); var type = (judgedObject.HitObject as Hit)?.Type ?? HitType.Centre; addExplosion(judgedObject, result.Type, type); From ad1b86e33a566b009ee115812c72461d87388669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Dach?= Date: Sat, 6 Mar 2021 18:54:25 +0100 Subject: [PATCH 6846/6909] Change `LifetimeEnd` idiom to `Expire()` for readability --- .../Skinning/Legacy/TaikoLegacySkinTransformer.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs index 40dc149ec9..d97da40ef2 100644 --- a/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs +++ b/osu.Game.Rulesets.Taiko/Skinning/Legacy/TaikoLegacySkinTransformer.cs @@ -35,7 +35,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy { // if a taiko skin is providing explosion sprites, hide the judgements completely if (hasExplosion.Value) - return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue); + return Drawable.Empty().With(d => d.Expire()); } if (!(component is TaikoSkinComponent taikoComponent)) @@ -118,7 +118,7 @@ namespace osu.Game.Rulesets.Taiko.Skinning.Legacy // suppress the default kiai explosion if the skin brings its own sprites. // the drawable needs to expire as soon as possible to avoid accumulating empty drawables on the playfield. if (hasExplosion.Value) - return Drawable.Empty().With(d => d.LifetimeEnd = double.MinValue); + return Drawable.Empty().With(d => d.Expire()); return null; From 3e4dfdb6755f7ea4bb721deb22029edb3dd408d0 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 6 Mar 2021 20:37:27 -0800 Subject: [PATCH 6847/6909] Fix pop out count being above displayed count on legacy combo counter --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 4784bca7dd..81b22b68b2 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -84,14 +84,14 @@ namespace osu.Game.Screens.Play.HUD { InternalChildren = new[] { - displayedCountSpriteText = createSpriteText().With(s => - { - s.Alpha = 0; - }), popOutCount = createSpriteText().With(s => { s.Alpha = 0; s.Margin = new MarginPadding(0.05f); + }), + displayedCountSpriteText = createSpriteText().With(s => + { + s.Alpha = 0; }) }; From 413cbb30a0f45b757941c053db0e983d1053d83f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 7 Mar 2021 13:39:46 +0300 Subject: [PATCH 6848/6909] Reword playfield shift counteract comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 5df8f8a485..9ce9fb9fd0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -152,8 +152,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.Centre; Size = new Vector2(640, 480); - // since legacy coordinates were on screen-space, they were accounting for the playfield shift offset. - // therefore cancel it from here. + // counteracts the playfield shift from OsuPlayfieldAdjustmentContainer. Position = new Vector2(0, -8f); } } From 503f29609a69451ee2cf0de0f5473bd1939495dd Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 7 Mar 2021 23:40:09 +0900 Subject: [PATCH 6849/6909] Also set additive mode to match stable --- osu.Game/Screens/Play/HUD/LegacyComboCounter.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs index 81b22b68b2..81183a425a 100644 --- a/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs +++ b/osu.Game/Screens/Play/HUD/LegacyComboCounter.cs @@ -88,6 +88,7 @@ namespace osu.Game.Screens.Play.HUD { s.Alpha = 0; s.Margin = new MarginPadding(0.05f); + s.Blending = BlendingParameters.Additive; }), displayedCountSpriteText = createSpriteText().With(s => { From fbfaa378fc25ce640eb809a0b3817511edb1042e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 7 Mar 2021 20:47:16 +0300 Subject: [PATCH 6850/6909] Move spinner top offset constant outside --- .../Skinning/Legacy/LegacyOldStyleSpinner.cs | 2 +- .../Skinning/Legacy/LegacySpinner.cs | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs index 7e9f73a89b..5c25c38504 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs @@ -57,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy // this anchor makes no sense, but that's what stable uses. Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Margin = new MarginPadding { Top = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET }, + Margin = new MarginPadding { Top = SPINNER_TOP_OFFSET }, Masking = true, Child = metreSprite = new Sprite { diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 9ce9fb9fd0..421c43fd7a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -16,6 +16,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public abstract class LegacySpinner : CompositeDrawable { + /// + /// An offset that simulates stable's spinner top offset, can be used with + /// for positioning some legacy spinner components perfectly as in stable. + /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners) + /// + public static readonly float SPINNER_TOP_OFFSET = (float)Math.Ceiling(45f * SPRITE_SCALE); + protected const float SPRITE_SCALE = 0.625f; protected DrawableSpinner DrawableSpinner { get; private set; } @@ -41,7 +48,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.Centre, Texture = source.GetTexture("spinner-spin"), Scale = new Vector2(SPRITE_SCALE), - Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 335, + Y = SPINNER_TOP_OFFSET + 335, }, clear = new Sprite { @@ -50,7 +57,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.Centre, Texture = source.GetTexture("spinner-clear"), Scale = new Vector2(SPRITE_SCALE), - Y = LegacyCoordinatesContainer.SPINNER_TOP_OFFSET + 115, + Y = SPINNER_TOP_OFFSET + 115, }, } }); @@ -136,13 +143,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy /// protected class LegacyCoordinatesContainer : Container { - /// - /// An offset that simulates stable's spinner top offset, - /// for positioning some legacy spinner components perfectly as in stable. - /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners) - /// - public static readonly float SPINNER_TOP_OFFSET = (float)Math.Ceiling(45f * SPRITE_SCALE); - public LegacyCoordinatesContainer() { // legacy spinners relied heavily on absolute screen-space coordinate values. From 0ad3073c1aa3863edbfb4c4f485ad970e507127e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Sun, 7 Mar 2021 21:21:44 +0300 Subject: [PATCH 6851/6909] Use MathF utility class instead Co-authored-by: Berkan Diler --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 421c43fd7a..406c19e76a 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -21,7 +21,7 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy /// for positioning some legacy spinner components perfectly as in stable. /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners) /// - public static readonly float SPINNER_TOP_OFFSET = (float)Math.Ceiling(45f * SPRITE_SCALE); + public static readonly float SPINNER_TOP_OFFSET = MathF.Ceiling(45f * SPRITE_SCALE); protected const float SPRITE_SCALE = 0.625f; From d961d110bf5294e26d7d51987dce30098a53e168 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 8 Mar 2021 02:58:52 +0000 Subject: [PATCH 6852/6909] Bump Microsoft.Extensions.Configuration.Abstractions from 2.2.0 to 5.0.0 Bumps [Microsoft.Extensions.Configuration.Abstractions](https://github.com/dotnet/runtime) from 2.2.0 to 5.0.0. - [Release notes](https://github.com/dotnet/runtime/releases) - [Commits](https://github.com/dotnet/runtime/commits/v5.0.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 2528292e17..c7aa6a8e11 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -26,7 +26,7 @@ - + From 74fc5d5b8cdb3452a69b70c59fa9c8c394103d3d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Mar 2021 13:29:09 +0900 Subject: [PATCH 6853/6909] Fix potential cross-thread drawable mutation in IntroTriangles --- osu.Game/Screens/BackgroundScreen.cs | 2 ++ osu.Game/Screens/Menu/IntroTriangles.cs | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/osu.Game/Screens/BackgroundScreen.cs b/osu.Game/Screens/BackgroundScreen.cs index 48c5523883..a6fb94b151 100644 --- a/osu.Game/Screens/BackgroundScreen.cs +++ b/osu.Game/Screens/BackgroundScreen.cs @@ -13,6 +13,8 @@ namespace osu.Game.Screens { private readonly bool animateOnEnter; + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + protected BackgroundScreen(bool animateOnEnter = true) { this.animateOnEnter = animateOnEnter; diff --git a/osu.Game/Screens/Menu/IntroTriangles.cs b/osu.Game/Screens/Menu/IntroTriangles.cs index ffe6882a72..abe6c62461 100644 --- a/osu.Game/Screens/Menu/IntroTriangles.cs +++ b/osu.Game/Screens/Menu/IntroTriangles.cs @@ -170,7 +170,7 @@ namespace osu.Game.Screens.Menu rulesets.Hide(); lazerLogo.Hide(); - background.Hide(); + background.ApplyToBackground(b => b.Hide()); using (BeginAbsoluteSequence(0, true)) { @@ -231,7 +231,8 @@ namespace osu.Game.Screens.Menu lazerLogo.Dispose(); // explicit disposal as we are pushing a new screen and the expire may not get run. logo.FadeIn(); - background.FadeIn(); + + background.ApplyToBackground(b => b.Show()); game.Add(new GameWideFlash()); From 7763e1dbe130233a61493ddf1cfacab1be0f3063 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sun, 7 Mar 2021 12:39:46 +0900 Subject: [PATCH 6854/6909] Apply workaround for runtime iOS failures See https://github.com/mono/mono/issues/20805#issuecomment-791440473. --- osu.iOS.props | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/osu.iOS.props b/osu.iOS.props index 56a24bea12..729d692e0e 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -77,12 +77,14 @@ $(NoWarn);NU1605 + - - - - - + + none + + + none + From 765cc5cf37120b9892e9233127573c5189b92456 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Mar 2021 13:29:47 +0900 Subject: [PATCH 6855/6909] Remove iOS multiplayer blocking code --- osu.Game/Online/API/APIAccess.cs | 11 ++--------- osu.Game/Screens/Menu/ButtonSystem.cs | 12 ------------ 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/osu.Game/Online/API/APIAccess.cs b/osu.Game/Online/API/APIAccess.cs index 569481d491..ede64c0340 100644 --- a/osu.Game/Online/API/APIAccess.cs +++ b/osu.Game/Online/API/APIAccess.cs @@ -10,7 +10,6 @@ using System.Net.Sockets; using System.Threading; using System.Threading.Tasks; using Newtonsoft.Json.Linq; -using osu.Framework; using osu.Framework.Bindables; using osu.Framework.Extensions.ExceptionExtensions; using osu.Framework.Extensions.ObjectExtensions; @@ -247,14 +246,8 @@ namespace osu.Game.Online.API this.password = password; } - public IHubClientConnector GetHubConnector(string clientName, string endpoint) - { - // disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805. - if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS) - return null; - - return new HubClientConnector(clientName, endpoint, this, versionHash); - } + public IHubClientConnector GetHubConnector(string clientName, string endpoint) => + new HubClientConnector(clientName, endpoint, this, versionHash); public RegistrationRequest.RegistrationRequestErrors CreateAccount(string email, string username, string password) { diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index f93bfd7705..81b1cb0bf1 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -172,18 +172,6 @@ namespace osu.Game.Screens.Menu return; } - // disabled until the underlying runtime issue is resolved, see https://github.com/mono/mono/issues/20805. - if (RuntimeInfo.OS == RuntimeInfo.Platform.iOS) - { - notifications?.Post(new SimpleNotification - { - Text = "Multiplayer is temporarily unavailable on iOS as we figure out some low level issues.", - Icon = FontAwesome.Solid.AppleAlt, - }); - - return; - } - OnMultiplayer?.Invoke(); } From b1cd01ceb82b9d04ea0c20b6a15392b2413f6bc4 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Mar 2021 12:57:16 +0900 Subject: [PATCH 6856/6909] Apply ConfigureAwait changes to game side --- CodeAnalysis/osu.ruleset | 2 +- osu.Desktop/Updater/SquirrelUpdateManager.cs | 14 ++++++------ osu.Game/Beatmaps/BeatmapManager.cs | 2 +- osu.Game/Collections/CollectionManager.cs | 4 ++-- osu.Game/Database/ArchiveModelManager.cs | 15 ++++++------- .../DownloadableArchiveModelManager.cs | 2 +- osu.Game/Database/MemoryCachingComponent.cs | 2 +- osu.Game/Database/UserLookupCache.cs | 2 +- osu.Game/Graphics/ScreenshotManager.cs | 6 ++--- osu.Game/IO/Archives/ArchiveReader.cs | 2 +- osu.Game/IPC/ArchiveImportIPCChannel.cs | 4 ++-- osu.Game/Online/HubClientConnector.cs | 12 +++++----- .../Multiplayer/StatefulMultiplayerClient.cs | 22 +++++++++---------- osu.Game/OsuGame.cs | 8 +++---- osu.Game/OsuGameBase.cs | 4 ++-- osu.Game/Overlays/ChangelogOverlay.cs | 4 ++-- osu.Game/Scoring/ScorePerformanceCache.cs | 2 +- osu.Game/Screens/Import/FileImportScreen.cs | 2 +- .../Multiplayer/MultiplayerPlayer.cs | 6 ++--- .../OnlinePlay/Playlists/PlaylistsPlayer.cs | 4 ++-- osu.Game/Screens/Play/Player.cs | 4 ++-- osu.Game/Skinning/SkinManager.cs | 2 +- .../Multiplayer/TestMultiplayerClient.cs | 2 +- osu.Game/Updater/SimpleUpdateManager.cs | 2 +- osu.Game/Updater/UpdateManager.cs | 2 +- 25 files changed, 65 insertions(+), 66 deletions(-) diff --git a/CodeAnalysis/osu.ruleset b/CodeAnalysis/osu.ruleset index d497365f87..6a99e230d1 100644 --- a/CodeAnalysis/osu.ruleset +++ b/CodeAnalysis/osu.ruleset @@ -30,7 +30,7 @@ - + diff --git a/osu.Desktop/Updater/SquirrelUpdateManager.cs b/osu.Desktop/Updater/SquirrelUpdateManager.cs index 71f9fafe57..47cd39dc5a 100644 --- a/osu.Desktop/Updater/SquirrelUpdateManager.cs +++ b/osu.Desktop/Updater/SquirrelUpdateManager.cs @@ -42,7 +42,7 @@ namespace osu.Desktop.Updater Splat.Locator.CurrentMutable.Register(() => new SquirrelLogger(), typeof(Splat.ILogger)); } - protected override async Task PerformUpdateCheck() => await checkForUpdateAsync(); + protected override async Task PerformUpdateCheck() => await checkForUpdateAsync().ConfigureAwait(false); private async Task checkForUpdateAsync(bool useDeltaPatching = true, UpdateProgressNotification notification = null) { @@ -51,9 +51,9 @@ namespace osu.Desktop.Updater try { - 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).ConfigureAwait(false); - var info = await updateManager.CheckForUpdate(!useDeltaPatching); + var info = await updateManager.CheckForUpdate(!useDeltaPatching).ConfigureAwait(false); if (info.ReleasesToApply.Count == 0) { @@ -79,12 +79,12 @@ namespace osu.Desktop.Updater try { - await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f); + await updateManager.DownloadReleases(info.ReleasesToApply, p => notification.Progress = p / 100f).ConfigureAwait(false); notification.Progress = 0; notification.Text = @"Installing update..."; - await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f); + await updateManager.ApplyReleases(info, p => notification.Progress = p / 100f).ConfigureAwait(false); notification.State = ProgressNotificationState.Completed; updatePending = true; @@ -97,7 +97,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. - await checkForUpdateAsync(false, notification); + await checkForUpdateAsync(false, notification).ConfigureAwait(false); scheduleRecheck = false; } else @@ -116,7 +116,7 @@ namespace osu.Desktop.Updater if (scheduleRecheck) { // check again in 30 minutes. - Scheduler.AddDelayed(async () => await checkForUpdateAsync(), 60000 * 30); + Scheduler.AddDelayed(async () => await checkForUpdateAsync().ConfigureAwait(false), 60000 * 30); } } diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index d653e5386b..29b3f5d3a3 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -156,7 +156,7 @@ namespace osu.Game.Beatmaps bool hadOnlineBeatmapIDs = beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0); if (onlineLookupQueue != null) - await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken); + await onlineLookupQueue.UpdateAsync(beatmapSet, cancellationToken).ConfigureAwait(false); // ensure at least one beatmap was able to retrieve or keep an online ID, else drop the set ID. if (hadOnlineBeatmapIDs && !beatmapSet.Beatmaps.Any(b => b.OnlineBeatmapID > 0)) diff --git a/osu.Game/Collections/CollectionManager.cs b/osu.Game/Collections/CollectionManager.cs index fb9c230c7a..9723409c79 100644 --- a/osu.Game/Collections/CollectionManager.cs +++ b/osu.Game/Collections/CollectionManager.cs @@ -124,7 +124,7 @@ namespace osu.Game.Collections return Task.Run(async () => { using (var stream = stable.GetStream(database_name)) - await Import(stream); + await Import(stream).ConfigureAwait(false); }); } @@ -139,7 +139,7 @@ namespace osu.Game.Collections PostNotification?.Invoke(notification); var collections = readCollections(stream, notification); - await importCollections(collections); + await importCollections(collections).ConfigureAwait(false); notification.CompletionText = $"Imported {collections.Count} collections"; notification.State = ProgressNotificationState.Completed; diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index daaba9098e..d809dbcb01 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -22,7 +22,6 @@ using osu.Game.IO.Archives; using osu.Game.IPC; using osu.Game.Overlays.Notifications; using SharpCompress.Archives.Zip; -using FileInfo = osu.Game.IO.FileInfo; namespace osu.Game.Database { @@ -163,7 +162,7 @@ namespace osu.Game.Database try { - var model = await Import(task, isLowPriorityImport, notification.CancellationToken); + var model = await Import(task, isLowPriorityImport, notification.CancellationToken).ConfigureAwait(false); lock (imported) { @@ -183,7 +182,7 @@ namespace osu.Game.Database { Logger.Error(e, $@"Could not import ({task})", LoggingTarget.Database); } - })); + })).ConfigureAwait(false); if (imported.Count == 0) { @@ -226,7 +225,7 @@ namespace osu.Game.Database TModel import; using (ArchiveReader reader = task.GetReader()) - import = await Import(reader, lowPriority, cancellationToken); + import = await Import(reader, lowPriority, cancellationToken).ConfigureAwait(false); // We may or may not want to delete the file depending on where it is stored. // e.g. reconstructing/repairing database with items from default storage. @@ -358,7 +357,7 @@ namespace osu.Game.Database item.Files = archive != null ? createFileInfos(archive, Files) : new List(); item.Hash = ComputeHash(item, archive); - await Populate(item, archive, cancellationToken); + await Populate(item, archive, cancellationToken).ConfigureAwait(false); using (var write = ContextFactory.GetForWrite()) // used to share a context for full import. keep in mind this will block all writes. { @@ -410,7 +409,7 @@ namespace osu.Game.Database flushEvents(true); return item; - }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap(); + }, cancellationToken, TaskCreationOptions.HideScheduler, lowPriority ? import_scheduler_low_priority : import_scheduler).Unwrap().ConfigureAwait(false); /// /// Exports an item to a legacy (.zip based) package. @@ -621,7 +620,7 @@ namespace osu.Game.Database } /// - /// Create all required s for the provided archive, adding them to the global file store. + /// Create all required s for the provided archive, adding them to the global file store. /// private List createFileInfos(ArchiveReader reader, FileStore files) { @@ -699,7 +698,7 @@ namespace osu.Game.Database return Task.CompletedTask; } - return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray())); + return Task.Run(async () => await Import(GetStableImportPaths(storage).ToArray()).ConfigureAwait(false)); } /// diff --git a/osu.Game/Database/DownloadableArchiveModelManager.cs b/osu.Game/Database/DownloadableArchiveModelManager.cs index 50b022f9ff..da3144e8d0 100644 --- a/osu.Game/Database/DownloadableArchiveModelManager.cs +++ b/osu.Game/Database/DownloadableArchiveModelManager.cs @@ -82,7 +82,7 @@ namespace osu.Game.Database Task.Factory.StartNew(async () => { // This gets scheduled back to the update thread, but we want the import to run in the background. - var imported = await Import(notification, new ImportTask(filename)); + var imported = await Import(notification, new ImportTask(filename)).ConfigureAwait(false); // for now a failed import will be marked as a failed download for simplicity. if (!imported.Any()) diff --git a/osu.Game/Database/MemoryCachingComponent.cs b/osu.Game/Database/MemoryCachingComponent.cs index d913e66428..a1a1279d71 100644 --- a/osu.Game/Database/MemoryCachingComponent.cs +++ b/osu.Game/Database/MemoryCachingComponent.cs @@ -29,7 +29,7 @@ namespace osu.Game.Database if (CheckExists(lookup, out TValue performance)) return performance; - var computed = await ComputeValueAsync(lookup, token); + var computed = await ComputeValueAsync(lookup, token).ConfigureAwait(false); if (computed != null || CacheNullValues) cache[lookup] = computed; diff --git a/osu.Game/Database/UserLookupCache.cs b/osu.Game/Database/UserLookupCache.cs index 568726199c..19cc211709 100644 --- a/osu.Game/Database/UserLookupCache.cs +++ b/osu.Game/Database/UserLookupCache.cs @@ -28,7 +28,7 @@ namespace osu.Game.Database public Task GetUserAsync(int userId, CancellationToken token = default) => GetAsync(userId, token); protected override async Task ComputeValueAsync(int lookup, CancellationToken token = default) - => await queryUser(lookup); + => await queryUser(lookup).ConfigureAwait(false); private readonly Queue<(int id, TaskCompletionSource)> pendingUserTasks = new Queue<(int, TaskCompletionSource)>(); private Task pendingRequestTask; diff --git a/osu.Game/Graphics/ScreenshotManager.cs b/osu.Game/Graphics/ScreenshotManager.cs index f7914cbbca..fb7fe4947b 100644 --- a/osu.Game/Graphics/ScreenshotManager.cs +++ b/osu.Game/Graphics/ScreenshotManager.cs @@ -103,7 +103,7 @@ namespace osu.Game.Graphics } } - using (var image = await host.TakeScreenshotAsync()) + using (var image = await host.TakeScreenshotAsync().ConfigureAwait(false)) { if (Interlocked.Decrement(ref screenShotTasks) == 0 && cursorVisibility.Value == false) cursorVisibility.Value = true; @@ -116,13 +116,13 @@ namespace osu.Game.Graphics switch (screenshotFormat.Value) { case ScreenshotFormat.Png: - await image.SaveAsPngAsync(stream); + await image.SaveAsPngAsync(stream).ConfigureAwait(false); break; case ScreenshotFormat.Jpg: const int jpeg_quality = 92; - await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }); + await image.SaveAsJpegAsync(stream, new JpegEncoder { Quality = jpeg_quality }).ConfigureAwait(false); break; default: diff --git a/osu.Game/IO/Archives/ArchiveReader.cs b/osu.Game/IO/Archives/ArchiveReader.cs index f74574e60c..679ab40402 100644 --- a/osu.Game/IO/Archives/ArchiveReader.cs +++ b/osu.Game/IO/Archives/ArchiveReader.cs @@ -41,7 +41,7 @@ namespace osu.Game.IO.Archives return null; byte[] buffer = new byte[input.Length]; - await input.ReadAsync(buffer); + await input.ReadAsync(buffer).ConfigureAwait(false); return buffer; } } diff --git a/osu.Game/IPC/ArchiveImportIPCChannel.cs b/osu.Game/IPC/ArchiveImportIPCChannel.cs index 029908ec9d..d9d0e4c0ea 100644 --- a/osu.Game/IPC/ArchiveImportIPCChannel.cs +++ b/osu.Game/IPC/ArchiveImportIPCChannel.cs @@ -33,12 +33,12 @@ namespace osu.Game.IPC if (importer == null) { // we want to contact a remote osu! to handle the import. - await SendMessageAsync(new ArchiveImportMessage { Path = path }); + await SendMessageAsync(new ArchiveImportMessage { Path = path }).ConfigureAwait(false); return; } if (importer.HandledExtensions.Contains(Path.GetExtension(path)?.ToLowerInvariant())) - await importer.Import(path); + await importer.Import(path).ConfigureAwait(false); } } diff --git a/osu.Game/Online/HubClientConnector.cs b/osu.Game/Online/HubClientConnector.cs index fdb21c5000..3839762e46 100644 --- a/osu.Game/Online/HubClientConnector.cs +++ b/osu.Game/Online/HubClientConnector.cs @@ -79,7 +79,7 @@ namespace osu.Game.Online { cancelExistingConnect(); - if (!await connectionLock.WaitAsync(10000)) + if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false)) throw new TimeoutException("Could not obtain a lock to connect. A previous attempt is likely stuck."); try @@ -88,7 +88,7 @@ namespace osu.Game.Online { // ensure any previous connection was disposed. // this will also create a new cancellation token source. - await disconnect(false); + await disconnect(false).ConfigureAwait(false); // this token will be valid for the scope of this connection. // if cancelled, we can be sure that a disconnect or reconnect is handled elsewhere. @@ -103,7 +103,7 @@ namespace osu.Game.Online // importantly, rebuild the connection each attempt to get an updated access token. CurrentConnection = buildConnection(cancellationToken); - await CurrentConnection.StartAsync(cancellationToken); + await CurrentConnection.StartAsync(cancellationToken).ConfigureAwait(false); Logger.Log($"{clientName} connected!", LoggingTarget.Network); isConnected.Value = true; @@ -119,7 +119,7 @@ namespace osu.Game.Online Logger.Log($"{clientName} connection error: {e}", LoggingTarget.Network); // retry on any failure. - await Task.Delay(5000, cancellationToken); + await Task.Delay(5000, cancellationToken).ConfigureAwait(false); } } } @@ -174,14 +174,14 @@ namespace osu.Game.Online if (takeLock) { - if (!await connectionLock.WaitAsync(10000)) + if (!await connectionLock.WaitAsync(10000).ConfigureAwait(false)) throw new TimeoutException("Could not obtain a lock to disconnect. A previous attempt is likely stuck."); } try { if (CurrentConnection != null) - await CurrentConnection.DisposeAsync(); + await CurrentConnection.DisposeAsync().ConfigureAwait(false); } finally { diff --git a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs index 73100be505..0f7050596f 100644 --- a/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs +++ b/osu.Game/Online/Multiplayer/StatefulMultiplayerClient.cs @@ -131,12 +131,12 @@ namespace osu.Game.Online.Multiplayer Debug.Assert(room.RoomID.Value != null); // Join the server-side room. - var joinedRoom = await JoinRoom(room.RoomID.Value.Value); + var joinedRoom = await JoinRoom(room.RoomID.Value.Value).ConfigureAwait(false); Debug.Assert(joinedRoom != null); // Populate users. Debug.Assert(joinedRoom.Users != null); - await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)); + await Task.WhenAll(joinedRoom.Users.Select(PopulateUser)).ConfigureAwait(false); // Update the stored room (must be done on update thread for thread-safety). await scheduleAsync(() => @@ -144,11 +144,11 @@ namespace osu.Game.Online.Multiplayer Room = joinedRoom; apiRoom = room; defaultPlaylistItemId = apiRoom.Playlist.FirstOrDefault()?.ID ?? 0; - }, cancellationSource.Token); + }, cancellationSource.Token).ConfigureAwait(false); // Update room settings. - await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token); - }, cancellationSource.Token); + await updateLocalRoomSettings(joinedRoom.Settings, cancellationSource.Token).ConfigureAwait(false); + }, cancellationSource.Token).ConfigureAwait(false); } /// @@ -178,8 +178,8 @@ namespace osu.Game.Online.Multiplayer return joinOrLeaveTaskChain.Add(async () => { - await scheduledReset; - await LeaveRoomInternal(); + await scheduledReset.ConfigureAwait(false); + await LeaveRoomInternal().ConfigureAwait(false); }); } @@ -237,11 +237,11 @@ namespace osu.Game.Online.Multiplayer switch (localUser.State) { case MultiplayerUserState.Idle: - await ChangeState(MultiplayerUserState.Ready); + await ChangeState(MultiplayerUserState.Ready).ConfigureAwait(false); return; case MultiplayerUserState.Ready: - await ChangeState(MultiplayerUserState.Idle); + await ChangeState(MultiplayerUserState.Idle).ConfigureAwait(false); return; default: @@ -307,7 +307,7 @@ namespace osu.Game.Online.Multiplayer if (Room == null) return; - await PopulateUser(user); + await PopulateUser(user).ConfigureAwait(false); Scheduler.Add(() => { @@ -486,7 +486,7 @@ namespace osu.Game.Online.Multiplayer /// Populates the for a given . /// /// The to populate. - protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID); + protected async Task PopulateUser(MultiplayerRoomUser multiplayerUser) => multiplayerUser.User ??= await userLookupCache.GetUserAsync(multiplayerUser.UserID).ConfigureAwait(false); /// /// Updates the local room settings with the given . diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index 203cc458e0..b7398efdc2 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -440,7 +440,7 @@ namespace osu.Game public override Task Import(params ImportTask[] imports) { // encapsulate task as we don't want to begin the import process until in a ready state. - var importTask = new Task(async () => await base.Import(imports)); + var importTask = new Task(async () => await base.Import(imports).ConfigureAwait(false)); waitForReady(() => this, _ => importTask.Start()); @@ -831,7 +831,7 @@ namespace osu.Game asyncLoadStream = Task.Run(async () => { if (previousLoadStream != null) - await previousLoadStream; + await previousLoadStream.ConfigureAwait(false); try { @@ -845,7 +845,7 @@ namespace osu.Game // The delegate won't complete if OsuGame has been disposed in the meantime while (!IsDisposed && !del.Completed) - await Task.Delay(10); + await Task.Delay(10).ConfigureAwait(false); // Either we're disposed or the load process has started successfully if (IsDisposed) @@ -853,7 +853,7 @@ namespace osu.Game Debug.Assert(task != null); - await task; + await task.ConfigureAwait(false); Logger.Log($"Loaded {component}!", level: LogLevel.Debug); } diff --git a/osu.Game/OsuGameBase.cs b/osu.Game/OsuGameBase.cs index 3d24f245f9..e1c7b67a8c 100644 --- a/osu.Game/OsuGameBase.cs +++ b/osu.Game/OsuGameBase.cs @@ -434,7 +434,7 @@ namespace osu.Game foreach (var importer in fileImporters) { if (importer.HandledExtensions.Contains(extension)) - await importer.Import(paths); + await importer.Import(paths).ConfigureAwait(false); } } @@ -445,7 +445,7 @@ namespace osu.Game { var importer = fileImporters.FirstOrDefault(i => i.HandledExtensions.Contains(taskGroup.Key)); return importer?.Import(taskGroup.ToArray()) ?? Task.CompletedTask; - })); + })).ConfigureAwait(false); } public IEnumerable HandledExtensions => fileImporters.SelectMany(i => i.HandledExtensions); diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 537dd00727..2da5be5e6c 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -160,9 +160,9 @@ namespace osu.Game.Overlays tcs.SetException(e); }; - await API.PerformAsync(req); + await API.PerformAsync(req).ConfigureAwait(false); - await tcs.Task; + return tcs.Task; }); } diff --git a/osu.Game/Scoring/ScorePerformanceCache.cs b/osu.Game/Scoring/ScorePerformanceCache.cs index 5f66c13d2f..bb15983de3 100644 --- a/osu.Game/Scoring/ScorePerformanceCache.cs +++ b/osu.Game/Scoring/ScorePerformanceCache.cs @@ -34,7 +34,7 @@ namespace osu.Game.Scoring { var score = lookup.ScoreInfo; - var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token); + var attributes = await difficultyCache.GetDifficultyAsync(score.Beatmap, score.Ruleset, score.Mods, token).ConfigureAwait(false); // Performance calculation requires the beatmap and ruleset to be locally available. If not, return a default value. if (attributes.Attributes == null) diff --git a/osu.Game/Screens/Import/FileImportScreen.cs b/osu.Game/Screens/Import/FileImportScreen.cs index 329623e03a..ee8ef6926d 100644 --- a/osu.Game/Screens/Import/FileImportScreen.cs +++ b/osu.Game/Screens/Import/FileImportScreen.cs @@ -154,7 +154,7 @@ namespace osu.Game.Screens.Import Task.Factory.StartNew(async () => { - await game.Import(path); + await game.Import(path).ConfigureAwait(false); // some files will be deleted after successful import, so we want to refresh the view. Schedule(() => diff --git a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs index ffcf248575..b3cd44d55a 100644 --- a/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Multiplayer/MultiplayerPlayer.cs @@ -137,13 +137,13 @@ namespace osu.Game.Screens.OnlinePlay.Multiplayer protected override async Task SubmitScore(Score score) { - await base.SubmitScore(score); + await base.SubmitScore(score).ConfigureAwait(false); - await client.ChangeState(MultiplayerUserState.FinishedPlay); + await client.ChangeState(MultiplayerUserState.FinishedPlay).ConfigureAwait(false); // Await up to 60 seconds for results to become available (6 api request timeouts). // This is arbitrary just to not leave the player in an essentially deadlocked state if any connection issues occur. - await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))); + await Task.WhenAny(resultsReady.Task, Task.Delay(TimeSpan.FromSeconds(60))).ConfigureAwait(false); } protected override ResultsScreen CreateResults(ScoreInfo score) diff --git a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs index ddc88261f7..a75e4bdc07 100644 --- a/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs +++ b/osu.Game/Screens/OnlinePlay/Playlists/PlaylistsPlayer.cs @@ -108,7 +108,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists protected override async Task SubmitScore(Score score) { - await base.SubmitScore(score); + await base.SubmitScore(score).ConfigureAwait(false); Debug.Assert(Token != null); @@ -128,7 +128,7 @@ namespace osu.Game.Screens.OnlinePlay.Playlists }; api.Queue(request); - await tcs.Task; + await tcs.Task.ConfigureAwait(false); } protected override void Dispose(bool isDisposing) diff --git a/osu.Game/Screens/Play/Player.cs b/osu.Game/Screens/Play/Player.cs index e81efdac78..0e221351aa 100644 --- a/osu.Game/Screens/Play/Player.cs +++ b/osu.Game/Screens/Play/Player.cs @@ -592,7 +592,7 @@ namespace osu.Game.Screens.Play try { - await SubmitScore(score); + await SubmitScore(score).ConfigureAwait(false); } catch (Exception ex) { @@ -601,7 +601,7 @@ namespace osu.Game.Screens.Play try { - await ImportScore(score); + await ImportScore(score).ConfigureAwait(false); } catch (Exception ex) { diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 2826c826a5..fcde9f041b 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -120,7 +120,7 @@ namespace osu.Game.Skinning protected override async Task Populate(SkinInfo model, ArchiveReader archive, CancellationToken cancellationToken = default) { - await base.Populate(model, archive, cancellationToken); + await base.Populate(model, archive, cancellationToken).ConfigureAwait(false); if (model.Name?.Contains(".osk") == true) populateMetadata(model); diff --git a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs index c03364a391..09fcc1ff47 100644 --- a/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs +++ b/osu.Game/Tests/Visual/Multiplayer/TestMultiplayerClient.cs @@ -136,7 +136,7 @@ namespace osu.Game.Tests.Visual.Multiplayer { Debug.Assert(Room != null); - await ((IMultiplayerClient)this).SettingsChanged(settings); + await ((IMultiplayerClient)this).SettingsChanged(settings).ConfigureAwait(false); foreach (var user in Room.Users.Where(u => u.State == MultiplayerUserState.Ready)) ChangeUserState(user.UserID, MultiplayerUserState.Idle); diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 4ebf2a7368..6eded7ce53 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -37,7 +37,7 @@ namespace osu.Game.Updater { var releases = new OsuJsonWebRequest("https://api.github.com/repos/ppy/osu/releases/latest"); - await releases.PerformAsync(); + await releases.PerformAsync().ConfigureAwait(false); var latest = releases.ResponseObject; diff --git a/osu.Game/Updater/UpdateManager.cs b/osu.Game/Updater/UpdateManager.cs index f772c6d282..9a0454bc95 100644 --- a/osu.Game/Updater/UpdateManager.cs +++ b/osu.Game/Updater/UpdateManager.cs @@ -69,7 +69,7 @@ namespace osu.Game.Updater lock (updateTaskLock) waitTask = (updateCheckTask ??= PerformUpdateCheck()); - bool hasUpdates = await waitTask; + bool hasUpdates = await waitTask.ConfigureAwait(false); lock (updateTaskLock) updateCheckTask = null; From d2bc48e57650d0ff2c1ec9cf840f4258f90b786b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Mar 2021 12:57:30 +0900 Subject: [PATCH 6857/6909] Exclude tests from ConfigureAwait rule --- osu.Game.Tests/osu.Game.Tests.csproj | 3 +++ osu.Game.Tests/tests.ruleset | 6 ++++++ 2 files changed, 9 insertions(+) create mode 100644 osu.Game.Tests/tests.ruleset diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index 32ccb5b699..e36b3cdc74 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -13,6 +13,9 @@ WinExe net5.0 + + tests.ruleset + diff --git a/osu.Game.Tests/tests.ruleset b/osu.Game.Tests/tests.ruleset new file mode 100644 index 0000000000..a0abb781d3 --- /dev/null +++ b/osu.Game.Tests/tests.ruleset @@ -0,0 +1,6 @@ + + + + + + From 6cb0db9c33c41312e9a380168d41327794a2288c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Mar 2021 14:45:11 +0900 Subject: [PATCH 6858/6909] Apply override rules to iOS/Android test projects --- osu.Game.Tests.Android/osu.Game.Tests.Android.csproj | 5 ++++- osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj index 543f2f35a7..c3d9cb5875 100644 --- a/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj +++ b/osu.Game.Tests.Android/osu.Game.Tests.Android.csproj @@ -20,6 +20,9 @@ + + $(NoWarn);CA2007 + %(RecursiveDir)%(Filename)%(Extension) @@ -74,4 +77,4 @@ - \ No newline at end of file + diff --git a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj index e83bef4a95..97df9b2cd5 100644 --- a/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj +++ b/osu.Game.Tests.iOS/osu.Game.Tests.iOS.csproj @@ -21,6 +21,9 @@ %(RecursiveDir)%(Filename)%(Extension) + + $(NoWarn);CA2007 + {2A66DD92-ADB1-4994-89E2-C94E04ACDA0D} @@ -48,4 +51,4 @@ - \ No newline at end of file + From 02194a93cb324e9a3781e82a73ce47ae6ce40f45 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 8 Mar 2021 15:17:10 +0900 Subject: [PATCH 6859/6909] Apply missing additions to android project --- osu.Android/OsuGameActivity.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/osu.Android/OsuGameActivity.cs b/osu.Android/OsuGameActivity.cs index d087c6218d..cffcea22c2 100644 --- a/osu.Android/OsuGameActivity.cs +++ b/osu.Android/OsuGameActivity.cs @@ -100,15 +100,15 @@ namespace osu.Android // copy to an arbitrary-access memory stream to be able to proceed with the import. var copy = new MemoryStream(); using (var stream = ContentResolver.OpenInputStream(uri)) - await stream.CopyToAsync(copy); + await stream.CopyToAsync(copy).ConfigureAwait(false); lock (tasks) { tasks.Add(new ImportTask(copy, filename)); } - })); + })).ConfigureAwait(false); - await game.Import(tasks.ToArray()); + await game.Import(tasks.ToArray()).ConfigureAwait(false); }, TaskCreationOptions.LongRunning); } } From bb79da1aacfd45dc73d1f493bd5e0400327dc61f Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Mar 2021 00:33:43 +0300 Subject: [PATCH 6860/6909] Correct playfield shift counteract comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 406c19e76a..896c3f4a3e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -152,7 +152,9 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.Centre; Size = new Vector2(640, 480); - // counteracts the playfield shift from OsuPlayfieldAdjustmentContainer. + // stable applies this adjustment conditionally, locally in the spinner. + // in lazer this is handled at a higher level in OsuPlayfieldAdjustmentContainer, + // therefore it's safe to apply it unconditionally in this component. Position = new Vector2(0, -8f); } } From dc9028d24acd3df3152057f36f0aa0217b9c954a Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Mar 2021 14:27:20 +0900 Subject: [PATCH 6861/6909] Update framework --- osu.Android.props | 2 +- osu.Game/Updater/SimpleUpdateManager.cs | 2 +- osu.Game/osu.Game.csproj | 2 +- osu.iOS.props | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/osu.Android.props b/osu.Android.props index c428cd2546..5b700224db 100644 --- a/osu.Android.props +++ b/osu.Android.props @@ -52,6 +52,6 @@ - + diff --git a/osu.Game/Updater/SimpleUpdateManager.cs b/osu.Game/Updater/SimpleUpdateManager.cs index 6eded7ce53..50572a7867 100644 --- a/osu.Game/Updater/SimpleUpdateManager.cs +++ b/osu.Game/Updater/SimpleUpdateManager.cs @@ -77,7 +77,7 @@ namespace osu.Game.Updater bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".exe", StringComparison.Ordinal)); break; - case RuntimeInfo.Platform.MacOsx: + case RuntimeInfo.Platform.macOS: bestAsset = release.Assets?.Find(f => f.Name.EndsWith(".app.zip", StringComparison.Ordinal)); break; diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index c7aa6a8e11..90c8b98f42 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -29,7 +29,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index 729d692e0e..ccd33bf88c 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -70,7 +70,7 @@ - + @@ -93,7 +93,7 @@ - + From 05493958696d488c4a8b76582ee0f92d5e7cf75b Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Tue, 9 Mar 2021 08:55:32 +0300 Subject: [PATCH 6862/6909] Inline "legacy coordinates container" and add "spinner Y centre" const --- .../Skinning/Legacy/LegacyNewStyleSpinner.cs | 3 +- .../Skinning/Legacy/LegacyOldStyleSpinner.cs | 33 ++++---- .../Skinning/Legacy/LegacySpinner.cs | 82 ++++++++----------- 3 files changed, 50 insertions(+), 68 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs index efeca53969..22fb3aab86 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyNewStyleSpinner.cs @@ -37,9 +37,10 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy AddInternal(scaleContainer = new Container { Scale = new Vector2(SPRITE_SCALE), - Anchor = Anchor.Centre, + Anchor = Anchor.TopCentre, Origin = Anchor.Centre, RelativeSizeAxes = Axes.Both, + Y = SPINNER_Y_CENTRE, Children = new Drawable[] { glow = new Sprite diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs index 5c25c38504..19cb55c16e 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacyOldStyleSpinner.cs @@ -37,35 +37,34 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { new Sprite { - Anchor = Anchor.Centre, + Anchor = Anchor.TopCentre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-background"), - Scale = new Vector2(SPRITE_SCALE) + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_Y_CENTRE, }, disc = new Sprite { - Anchor = Anchor.Centre, + Anchor = Anchor.TopCentre, Origin = Anchor.Centre, Texture = source.GetTexture("spinner-circle"), - Scale = new Vector2(SPRITE_SCALE) + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_Y_CENTRE, }, - new LegacyCoordinatesContainer + metre = new Container { - Child = metre = new Container + AutoSizeAxes = Axes.Both, + // this anchor makes no sense, but that's what stable uses. + Anchor = Anchor.TopLeft, + Origin = Anchor.TopLeft, + Margin = new MarginPadding { Top = SPINNER_TOP_OFFSET }, + Masking = true, + Child = metreSprite = new Sprite { - AutoSizeAxes = Axes.Both, - // this anchor makes no sense, but that's what stable uses. + Texture = source.GetTexture("spinner-metre"), Anchor = Anchor.TopLeft, Origin = Anchor.TopLeft, - Margin = new MarginPadding { Top = SPINNER_TOP_OFFSET }, - Masking = true, - Child = metreSprite = new Sprite - { - Texture = source.GetTexture("spinner-metre"), - Anchor = Anchor.TopLeft, - Origin = Anchor.TopLeft, - Scale = new Vector2(SPRITE_SCALE) - } + Scale = new Vector2(SPRITE_SCALE) } } }); diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 896c3f4a3e..1738003390 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -16,12 +16,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public abstract class LegacySpinner : CompositeDrawable { - /// - /// An offset that simulates stable's spinner top offset, can be used with - /// for positioning some legacy spinner components perfectly as in stable. - /// (e.g. 'spin' sprite, 'clear' sprite, metre in old-style spinners) - /// - public static readonly float SPINNER_TOP_OFFSET = MathF.Ceiling(45f * SPRITE_SCALE); + protected static readonly float SPINNER_TOP_OFFSET = MathF.Ceiling(45f * SPRITE_SCALE); + protected static readonly float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f; protected const float SPRITE_SCALE = 0.625f; @@ -33,33 +29,41 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject, ISkinSource source) { - RelativeSizeAxes = Axes.Both; + // legacy spinners relied heavily on absolute screen-space coordinate values. + // wrap everything in a container simulating absolute coords to preserve alignment + // as there are skins that depend on it. + Anchor = Anchor.Centre; + Origin = Anchor.Centre; + Size = new Vector2(640, 480); + + // stable applies this adjustment conditionally, locally in the spinner. + // in lazer this is handled at a higher level in OsuPlayfieldAdjustmentContainer, + // therefore it's safe to apply it unconditionally in this component. + Position = new Vector2(0, -8f); DrawableSpinner = (DrawableSpinner)drawableHitObject; - AddInternal(new LegacyCoordinatesContainer + AddRangeInternal(new[] { - Depth = float.MinValue, - Children = new Drawable[] + spin = new Sprite { - spin = new Sprite - { - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-spin"), - Scale = new Vector2(SPRITE_SCALE), - Y = SPINNER_TOP_OFFSET + 335, - }, - clear = new Sprite - { - Alpha = 0, - Anchor = Anchor.TopCentre, - Origin = Anchor.Centre, - Texture = source.GetTexture("spinner-clear"), - Scale = new Vector2(SPRITE_SCALE), - Y = SPINNER_TOP_OFFSET + 115, - }, - } + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Depth = float.MinValue, + Texture = source.GetTexture("spinner-spin"), + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_TOP_OFFSET + 335, + }, + clear = new Sprite + { + Alpha = 0, + Anchor = Anchor.TopCentre, + Origin = Anchor.Centre, + Depth = float.MinValue, + Texture = source.GetTexture("spinner-clear"), + Scale = new Vector2(SPRITE_SCALE), + Y = SPINNER_TOP_OFFSET + 115, + }, }); } @@ -136,27 +140,5 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy if (DrawableSpinner != null) DrawableSpinner.ApplyCustomUpdateState -= UpdateStateTransforms; } - - /// - /// A simulating osu!stable's absolute screen-space, - /// for perfect placements of legacy spinner components with legacy coordinates. - /// - protected class LegacyCoordinatesContainer : Container - { - public LegacyCoordinatesContainer() - { - // legacy spinners relied heavily on absolute screen-space coordinate values. - // wrap everything in a container simulating absolute coords to preserve alignment - // as there are skins that depend on it. - Anchor = Anchor.Centre; - Origin = Anchor.Centre; - Size = new Vector2(640, 480); - - // stable applies this adjustment conditionally, locally in the spinner. - // in lazer this is handled at a higher level in OsuPlayfieldAdjustmentContainer, - // therefore it's safe to apply it unconditionally in this component. - Position = new Vector2(0, -8f); - } - } } } From a5b3ac7ef8101c867bec8c1188ec4595ccb1c919 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Mar 2021 15:45:03 +0900 Subject: [PATCH 6863/6909] Add failing test covering alpha commands proceeding non-alpha (but ignored) commands --- .../Visual/Gameplay/TestSceneLeadIn.cs | 40 ++++++++++++++++++- 1 file changed, 39 insertions(+), 1 deletion(-) diff --git a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs index 563d6be0da..dccde366c2 100644 --- a/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs +++ b/osu.Game.Tests/Visual/Gameplay/TestSceneLeadIn.cs @@ -46,11 +46,12 @@ namespace osu.Game.Tests.Visual.Gameplay [TestCase(0, 0)] [TestCase(-1000, -1000)] [TestCase(-10000, -10000)] - public void TestStoryboardProducesCorrectStartTime(double firstStoryboardEvent, double expectedStartTime) + public void TestStoryboardProducesCorrectStartTimeSimpleAlpha(double firstStoryboardEvent, double expectedStartTime) { var storyboard = new Storyboard(); var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); + sprite.TimelineGroup.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1); storyboard.GetLayer("Background").Add(sprite); @@ -64,6 +65,43 @@ namespace osu.Game.Tests.Visual.Gameplay }); } + [TestCase(1000, 0, false)] + [TestCase(0, 0, false)] + [TestCase(-1000, -1000, false)] + [TestCase(-10000, -10000, false)] + [TestCase(1000, 0, true)] + [TestCase(0, 0, true)] + [TestCase(-1000, -1000, true)] + [TestCase(-10000, -10000, true)] + public void TestStoryboardProducesCorrectStartTimeFadeInAfterOtherEvents(double firstStoryboardEvent, double expectedStartTime, bool addEventToLoop) + { + var storyboard = new Storyboard(); + + var sprite = new StoryboardSprite("unknown", Anchor.TopLeft, Vector2.Zero); + + // these should be ignored as we have an alpha visibility blocker proceeding this command. + sprite.TimelineGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1); + var loopGroup = sprite.AddLoop(-20000, 50); + loopGroup.Scale.Add(Easing.None, -20000, -18000, 0, 1); + + var target = addEventToLoop ? loopGroup : sprite.TimelineGroup; + target.Alpha.Add(Easing.None, firstStoryboardEvent, firstStoryboardEvent + 500, 0, 1); + + // these should be ignored due to being in the future. + sprite.TimelineGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1); + loopGroup.Alpha.Add(Easing.None, 18000, 20000, 0, 1); + + storyboard.GetLayer("Background").Add(sprite); + + loadPlayerWithBeatmap(new TestBeatmap(new OsuRuleset().RulesetInfo), storyboard); + + AddAssert($"first frame is {expectedStartTime}", () => + { + Debug.Assert(player.FirstFrameClockTime != null); + return Precision.AlmostEquals(player.FirstFrameClockTime.Value, expectedStartTime, lenience_ms); + }); + } + private void loadPlayerWithBeatmap(IBeatmap beatmap, Storyboard storyboard = null) { AddStep("create player", () => From 8aaba324314c4cfc628de35a3b6ab438227103a5 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Mar 2021 15:55:05 +0900 Subject: [PATCH 6864/6909] Fix storyboard commands occurring before the earliest point of visibility delaying gameplay In osu-stable, storyboard intros start from the first command, but in the case of storyboard drawables which have an initial hidden state, all commands before the time at which they become visible (ie. the first command where `Alpha` increases to a non-zero value) are ignored. This brings lazer in line with that behaviour. It also removes several unnecessary LINQ calls. Note that the alpha check being done in its own pass is important, as it must be the "minimum present alpha across all command groups, including loops". This is what makes the implementation slightly complex. Closes #11981. --- osu.Game/Storyboards/CommandTimelineGroup.cs | 19 +++++++++ osu.Game/Storyboards/StoryboardSprite.cs | 45 +++++++++++++++++--- 2 files changed, 58 insertions(+), 6 deletions(-) diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 6ce3b617e9..617455cf0b 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -45,11 +45,30 @@ namespace osu.Game.Storyboards }; } + /// + /// Returns the earliest visible time. Will be null unless this group has an command with a start value of zero. + /// + public double? EarliestDisplayedTime + { + get + { + var first = Alpha.Commands.FirstOrDefault(); + + return first?.StartValue == 0 ? first.StartTime : (double?)null; + } + } + [JsonIgnore] public double CommandsStartTime { get { + // if the first alpha command starts at zero it should be given priority over anything else. + // this is due to it creating a state where the target is not present before that time, causing any other events to not be visible. + var earliestDisplay = EarliestDisplayedTime; + if (earliestDisplay != null) + return earliestDisplay.Value; + double min = double.MaxValue; for (int i = 0; i < timelines.Length; i++) diff --git a/osu.Game/Storyboards/StoryboardSprite.cs b/osu.Game/Storyboards/StoryboardSprite.cs index f411ad04f3..fdaa59d7d9 100644 --- a/osu.Game/Storyboards/StoryboardSprite.cs +++ b/osu.Game/Storyboards/StoryboardSprite.cs @@ -24,13 +24,46 @@ namespace osu.Game.Storyboards public readonly CommandTimelineGroup TimelineGroup = new CommandTimelineGroup(); - public double StartTime => Math.Min( - TimelineGroup.HasCommands ? TimelineGroup.CommandsStartTime : double.MaxValue, - loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Min(l => l.StartTime) : double.MaxValue); + public double StartTime + { + get + { + // check for presence affecting commands as an initial pass. + double earliestStartTime = TimelineGroup.EarliestDisplayedTime ?? double.MaxValue; - public double EndTime => Math.Max( - TimelineGroup.HasCommands ? TimelineGroup.CommandsEndTime : double.MinValue, - loops.Any(l => l.HasCommands) ? loops.Where(l => l.HasCommands).Max(l => l.EndTime) : double.MinValue); + foreach (var l in loops) + { + if (!(l.EarliestDisplayedTime is double lEarliest)) + continue; + + earliestStartTime = Math.Min(earliestStartTime, lEarliest); + } + + if (earliestStartTime < double.MaxValue) + return earliestStartTime; + + // if an alpha-affecting command was not found, use the earliest of any command. + earliestStartTime = TimelineGroup.StartTime; + + foreach (var l in loops) + earliestStartTime = Math.Min(earliestStartTime, l.StartTime); + + return earliestStartTime; + } + } + + public double EndTime + { + get + { + double latestEndTime = TimelineGroup.EndTime; + + foreach (var l in loops) + latestEndTime = Math.Max(latestEndTime, l.EndTime); + + return latestEndTime; + } + } public bool HasCommands => TimelineGroup.HasCommands || loops.Any(l => l.HasCommands); From 5a6864eb7826502cc74132275206834bf81532fe Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Mar 2021 16:43:44 +0900 Subject: [PATCH 6865/6909] Fix SPM counter immediately disappearing on completion of spinners --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 3 +++ osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index d02376b6c3..69095fd160 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -109,6 +109,9 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnFree(); spinningSample.Samples = null; + + // the counter handles its own fade in (when spinning begins) so we should only be responsible for resetting it here, for pooling. + SpmCounter.Hide(); } protected override void LoadSamples() diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs index 69355f624b..f3e013c759 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs @@ -21,6 +21,11 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private readonly OsuSpriteText spmText; + public override void ApplyTransformsAt(double time, bool propagateChildren = false) + { + // handles own fade in state. + } + public SpinnerSpmCounter() { Children = new Drawable[] From 4e8bcc92659b47bb1223e43458f8159475e78209 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Tue, 9 Mar 2021 16:15:44 +0900 Subject: [PATCH 6866/6909] Fix SPM counter decreasing after spinner has already been completed --- osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index 69095fd160..e6940f0985 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -267,7 +267,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables if (!SpmCounter.IsPresent && RotationTracker.Tracking) SpmCounter.FadeIn(HitObject.TimeFadeIn); - SpmCounter.SetRotation(Result.RateAdjustedRotation); + // don't update after end time to avoid the rate display dropping during fade out. + // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period. + if (Time.Current <= HitObject.EndTime) + SpmCounter.SetRotation(Result.RateAdjustedRotation); updateBonusScore(); } From 3f349816649689b519f3ed93942c311c3881a90e Mon Sep 17 00:00:00 2001 From: Salman Ahmed Date: Thu, 11 Mar 2021 05:40:18 +0300 Subject: [PATCH 6867/6909] Fix incorrect spinner top offset calculation with clarification --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 1738003390..ab7d265f67 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -9,6 +9,7 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; +using osu.Game.Rulesets.Osu.UI; using osu.Game.Skinning; using osuTK; @@ -16,7 +17,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy { public abstract class LegacySpinner : CompositeDrawable { - protected static readonly float SPINNER_TOP_OFFSET = MathF.Ceiling(45f * SPRITE_SCALE); + /// + /// osu!stable applies this adjustment conditionally, locally in the spinner. + /// in lazer this is handled at a higher level in , + /// therefore it's safe to apply it unconditionally in this component. + /// + protected static readonly float SPINNER_TOP_OFFSET = 45f - 16f; + protected static readonly float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f; protected const float SPRITE_SCALE = 0.625f; From efb4a366d42600b5217e574f40c5d53438a8d3c7 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Mar 2021 12:15:59 +0900 Subject: [PATCH 6868/6909] Fix xmldoc explaining incorrect behaviour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bartłomiej Dach --- osu.Game/Storyboards/CommandTimelineGroup.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Storyboards/CommandTimelineGroup.cs b/osu.Game/Storyboards/CommandTimelineGroup.cs index 617455cf0b..c478b91c22 100644 --- a/osu.Game/Storyboards/CommandTimelineGroup.cs +++ b/osu.Game/Storyboards/CommandTimelineGroup.cs @@ -46,7 +46,7 @@ namespace osu.Game.Storyboards } /// - /// Returns the earliest visible time. Will be null unless this group has an command with a start value of zero. + /// Returns the earliest visible time. Will be null unless this group's first command has a start value of zero. /// public double? EarliestDisplayedTime { From 1591d593e26095201cedb9d2e1fde2f2761a09a6 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Thu, 11 Mar 2021 12:58:15 +0900 Subject: [PATCH 6869/6909] Move spin start time to inside result and switch to standard state handling --- .../Judgements/OsuSpinnerJudgementResult.cs | 5 +++++ .../Objects/Drawables/DrawableSpinner.cs | 21 +++++++++++++++---- .../Skinning/Default/SpinnerSpmCounter.cs | 5 ----- 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs index e58aacd86e..9f77175398 100644 --- a/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs +++ b/osu.Game.Rulesets.Osu/Judgements/OsuSpinnerJudgementResult.cs @@ -38,6 +38,11 @@ namespace osu.Game.Rulesets.Osu.Judgements /// public float RateAdjustedRotation; + /// + /// Time instant at which the spin was started (the first user input which caused an increase in spin). + /// + public double? TimeStarted; + /// /// Time instant at which the spinner has been completed (the user has executed all required spins). /// Will be null if all required spins haven't been completed. diff --git a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs index e6940f0985..3d614c2dbd 100644 --- a/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs +++ b/osu.Game.Rulesets.Osu/Objects/Drawables/DrawableSpinner.cs @@ -109,9 +109,6 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.OnFree(); spinningSample.Samples = null; - - // the counter handles its own fade in (when spinning begins) so we should only be responsible for resetting it here, for pooling. - SpmCounter.Hide(); } protected override void LoadSamples() @@ -161,6 +158,17 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables } } + protected override void UpdateStartTimeStateTransforms() + { + base.UpdateStartTimeStateTransforms(); + + if (Result?.TimeStarted is double startTime) + { + using (BeginAbsoluteSequence(startTime)) + fadeInCounter(); + } + } + protected override void UpdateHitStateTransforms(ArmedState state) { base.UpdateHitStateTransforms(state); @@ -265,7 +273,10 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables base.UpdateAfterChildren(); if (!SpmCounter.IsPresent && RotationTracker.Tracking) - SpmCounter.FadeIn(HitObject.TimeFadeIn); + { + Result.TimeStarted ??= Time.Current; + fadeInCounter(); + } // don't update after end time to avoid the rate display dropping during fade out. // this shouldn't be limited to StartTime as it causes weirdness with the underlying calculation, which is expecting updates during that period. @@ -275,6 +286,8 @@ namespace osu.Game.Rulesets.Osu.Objects.Drawables updateBonusScore(); } + private void fadeInCounter() => SpmCounter.FadeIn(HitObject.TimeFadeIn); + private int wholeSpins; private void updateBonusScore() diff --git a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs index f3e013c759..69355f624b 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Default/SpinnerSpmCounter.cs @@ -21,11 +21,6 @@ namespace osu.Game.Rulesets.Osu.Skinning.Default private readonly OsuSpriteText spmText; - public override void ApplyTransformsAt(double time, bool propagateChildren = false) - { - // handles own fade in state. - } - public SpinnerSpmCounter() { Children = new Drawable[] From 8bc494b224639f79ab4bb6f0a31e18d89da5cfcf Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 11 Mar 2021 20:57:00 +0900 Subject: [PATCH 6870/6909] Adjust explanatory comments --- .../Skinning/Legacy/LegacySpinner.cs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index ab7d265f67..acaec9cbc0 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -18,13 +18,13 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy public abstract class LegacySpinner : CompositeDrawable { /// - /// osu!stable applies this adjustment conditionally, locally in the spinner. - /// in lazer this is handled at a higher level in , - /// therefore it's safe to apply it unconditionally in this component. + /// All constant spinner coordinates are in osu!stable's gamefield space, which is shifted 16px downwards. + /// This offset is negated in both osu!stable and osu!lazer to bring all constant coordinates into window-space. + /// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable) /// - protected static readonly float SPINNER_TOP_OFFSET = 45f - 16f; + protected const float SPINNER_TOP_OFFSET = 45f - 16f; - protected static readonly float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f; + protected const float SPINNER_Y_CENTRE = SPINNER_TOP_OFFSET + 219f; protected const float SPRITE_SCALE = 0.625f; @@ -43,9 +43,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy Origin = Anchor.Centre; Size = new Vector2(640, 480); - // stable applies this adjustment conditionally, locally in the spinner. - // in lazer this is handled at a higher level in OsuPlayfieldAdjustmentContainer, - // therefore it's safe to apply it unconditionally in this component. + // osu!stable positions components of the spinner in window-space (as opposed to gamefield-space). + // in lazer, the gamefield-space transformation is applied in OsuPlayfieldAdjustmentContainer, which is inverted here to bring coordinates back into window-space. Position = new Vector2(0, -8f); DrawableSpinner = (DrawableSpinner)drawableHitObject; From b5bdf235cad7a638ad0fc8ce62fd7f6a31edf094 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 11 Mar 2021 21:21:44 +0900 Subject: [PATCH 6871/6909] Slightly improve comments more --- .../Skinning/Legacy/LegacySpinner.cs | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index acaec9cbc0..1cc25bf053 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -18,8 +18,8 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy public abstract class LegacySpinner : CompositeDrawable { /// - /// All constant spinner coordinates are in osu!stable's gamefield space, which is shifted 16px downwards. - /// This offset is negated in both osu!stable and osu!lazer to bring all constant coordinates into window-space. + /// All constants are in osu!stable's gamefield space, which is shifted 16px downwards. + /// This offset is negated in both osu!stable and osu!lazer to bring all constants into window-space. /// Note: SPINNER_Y_CENTRE + SPINNER_TOP_OFFSET - Position.Y = 240 (=480/2, or half the window-space in osu!stable) /// protected const float SPINNER_TOP_OFFSET = 45f - 16f; @@ -36,15 +36,12 @@ namespace osu.Game.Rulesets.Osu.Skinning.Legacy [BackgroundDependencyLoader] private void load(DrawableHitObject drawableHitObject, ISkinSource source) { - // legacy spinners relied heavily on absolute screen-space coordinate values. - // wrap everything in a container simulating absolute coords to preserve alignment - // as there are skins that depend on it. Anchor = Anchor.Centre; Origin = Anchor.Centre; - Size = new Vector2(640, 480); - // osu!stable positions components of the spinner in window-space (as opposed to gamefield-space). - // in lazer, the gamefield-space transformation is applied in OsuPlayfieldAdjustmentContainer, which is inverted here to bring coordinates back into window-space. + // osu!stable positions spinner components in window-space (as opposed to gamefield-space). This is a 640x480 area taking up the entire screen. + // In lazer, the gamefield-space positional transformation is applied in OsuPlayfieldAdjustmentContainer, which is inverted here to make this area take up the entire window space. + Size = new Vector2(640, 480); Position = new Vector2(0, -8f); DrawableSpinner = (DrawableSpinner)drawableHitObject; From ea9b48d17d08444c2e7e458fd874cac580ec388c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Thu, 11 Mar 2021 21:21:48 +0900 Subject: [PATCH 6872/6909] Remove unused using --- osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs index 1cc25bf053..513888db53 100644 --- a/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs +++ b/osu.Game.Rulesets.Osu/Skinning/Legacy/LegacySpinner.cs @@ -9,7 +9,6 @@ using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.Sprites; using osu.Game.Rulesets.Objects.Drawables; using osu.Game.Rulesets.Osu.Objects.Drawables; -using osu.Game.Rulesets.Osu.UI; using osu.Game.Skinning; using osuTK; From f1302d16006b567343c12a2624eeb544c7eae9bf Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Thu, 11 Mar 2021 19:23:56 +0300 Subject: [PATCH 6873/6909] Update Microsoft.EntityFrameworkCore --- osu.Desktop/osu.Desktop.csproj | 7 +++++-- osu.Game/Database/OsuDbContext.cs | 3 --- osu.Game/osu.Game.csproj | 6 +++--- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 3e0f0cb7f6..4af69c573d 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -27,8 +27,11 @@ - - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 2aae62edea..d27da50448 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -111,9 +111,6 @@ namespace osu.Game.Database { base.OnConfiguring(optionsBuilder); optionsBuilder - // this is required for the time being due to the way we are querying in places like BeatmapStore. - // if we ever move to having consumers file their own .Includes, or get eager loading support, this could be re-enabled. - .ConfigureWarnings(warnings => warnings.Ignore(CoreEventId.IncludeIgnoredWarning)) .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10)) .UseLoggerFactory(logger.Value); } diff --git a/osu.Game/osu.Game.csproj b/osu.Game/osu.Game.csproj index 90c8b98f42..fa1b0a95c3 100644 --- a/osu.Game/osu.Game.csproj +++ b/osu.Game/osu.Game.csproj @@ -24,10 +24,10 @@ - - + + - + From 47b80d2474f6c2a371dd91aa48fda003a60e808e Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Thu, 11 Mar 2021 20:51:54 +0300 Subject: [PATCH 6874/6909] Workaround InvalidOperation exceptions --- osu.Game/Beatmaps/BeatmapManager.cs | 16 ++++++++++++++++ osu.Game/Database/ArchiveModelManager.cs | 8 +++++++- osu.Game/Skinning/SkinManager.cs | 12 ++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 29b3f5d3a3..3254f53574 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -174,6 +174,22 @@ namespace osu.Game.Beatmaps if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null)) throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}."); + var dbContext = ContextFactory.Get(); + + // Workaround System.InvalidOperationException + // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. + foreach (var beatmap in beatmapSet.Beatmaps) + { + beatmap.Ruleset = dbContext.RulesetInfo.Find(beatmap.RulesetID); + } + + // Workaround System.InvalidOperationException + // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. + foreach (var file in beatmapSet.Files) + { + file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); + } + // check if a set already exists with the same online id, delete if it does. if (beatmapSet.OnlineBeatmapSetID != null) { diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index d809dbcb01..fe2caaa0b7 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -462,6 +462,10 @@ namespace osu.Game.Database // Dereference the existing file info, since the file model will be removed. if (file.FileInfo != null) { + // Workaround System.InvalidOperationException + // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. + file.FileInfo = usage.Context.FileInfo.Find(file.FileInfoID); + Files.Dereference(file.FileInfo); // This shouldn't be required, but here for safety in case the provided TModel is not being change tracked @@ -635,10 +639,12 @@ namespace osu.Game.Database { using (Stream s = reader.GetStream(file)) { + var fileInfo = files.Add(s); fileInfos.Add(new TFileModel { Filename = file.Substring(prefix.Length).ToStandardisedPath(), - FileInfo = files.Add(s) + FileInfo = fileInfo, + FileInfoID = fileInfo.ID }); } } diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index fcde9f041b..2bb27b60d6 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -142,6 +142,18 @@ namespace osu.Game.Skinning } } + protected override void PreImport(SkinInfo model) + { + var dbContext = ContextFactory.Get(); + + // Workaround System.InvalidOperationException + // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. + foreach (var file in model.Files) + { + file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); + } + } + /// /// Retrieve a instance for the provided /// From c6c616f244eb08e664c04937234ffcd6dd84b9c0 Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Thu, 11 Mar 2021 21:02:40 +0300 Subject: [PATCH 6875/6909] Actualize tests --- .../Multiplayer/TestSceneMultiplayerMatchSongSelect.cs | 1 + .../SongSelect/TestSceneBeatmapRecommendations.cs | 1 + .../Visual/SongSelect/TestScenePlaySongSelect.cs | 4 +++- osu.Game/Scoring/ScoreInfo.cs | 10 ++-------- 4 files changed, 7 insertions(+), 9 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index faa5d9e6fc..4a9eaa1842 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -56,6 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer beatmaps.Add(new BeatmapInfo { Ruleset = rulesets.GetRuleset(i % 4), + RulesetID = i % 4, OnlineBeatmapID = beatmapId, Length = length, BPM = bpm, diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 53a956c77c..223ace6ca5 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -186,6 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect Metadata = metadata, BaseDifficulty = new BeatmapDifficulty(), Ruleset = ruleset, + RulesetID = ruleset.ID.GetValueOrDefault(), StarDifficulty = difficultyIndex + 1, Version = $"SR{difficultyIndex + 1}" }).ToList() diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 35c6d62cb7..4b402d0c54 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -911,9 +911,11 @@ namespace osu.Game.Tests.Visual.SongSelect int length = RNG.Next(30000, 200000); double bpm = RNG.NextSingle(80, 200); + var ruleset = getRuleset(); beatmaps.Add(new BeatmapInfo { - Ruleset = getRuleset(), + Ruleset = ruleset, + RulesetID = ruleset.ID.GetValueOrDefault(), OnlineBeatmapID = beatmapId, Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})", Length = length, diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index f5192f3a40..c5ad43abba 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -73,7 +73,7 @@ namespace osu.Game.Scoring } set { - modsJson = null; + modsJson = JsonConvert.SerializeObject(value.Select(m => new DeserializedMod { Acronym = m.Acronym })); mods = value; } } @@ -88,13 +88,7 @@ namespace osu.Game.Scoring { get { - if (modsJson != null) - return modsJson; - - if (mods == null) - return null; - - return modsJson = JsonConvert.SerializeObject(mods.Select(m => new DeserializedMod { Acronym = m.Acronym })); + return modsJson; } set { From d2f943395d349e08ae8e5a72b1f2996ea0d1d539 Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Thu, 11 Mar 2021 22:12:47 +0300 Subject: [PATCH 6876/6909] Hotfix importing scores from stable --- osu.Game/Scoring/ScoreManager.cs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index 96ec9644b5..a97c516a1b 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -52,6 +52,23 @@ namespace osu.Game.Scoring this.configManager = configManager; } + protected override void PreImport(ScoreInfo model) + { + var dbContext = ContextFactory.Get(); + + // Workaround System.InvalidOperationException + // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. + foreach (var file in model.Files) + { + file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); + } + + foreach (var file in model.Beatmap.BeatmapSet.Files) + { + file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); + } + } + protected override ScoreInfo CreateModel(ArchiveReader archive) { if (archive == null) From 5a4b0174b187e649b9fb739fafd076ea19817f23 Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Thu, 11 Mar 2021 22:40:35 +0300 Subject: [PATCH 6877/6909] Ignore MultipleCollectionIncludeWarning --- osu.Game/Database/OsuDbContext.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index d27da50448..689f248de8 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -112,6 +112,7 @@ namespace osu.Game.Database base.OnConfiguring(optionsBuilder); optionsBuilder .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10)) + .ConfigureWarnings(w => w.Ignore(RelationalEventId.MultipleCollectionIncludeWarning)) .UseLoggerFactory(logger.Value); } From a60ff80c04850c1f09ad9f741197378e691fc78d Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Fri, 12 Mar 2021 00:02:29 +0300 Subject: [PATCH 6878/6909] Use expression body in ModsJson get accessor --- osu.Game/Scoring/ScoreInfo.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Game/Scoring/ScoreInfo.cs b/osu.Game/Scoring/ScoreInfo.cs index c5ad43abba..78101991f6 100644 --- a/osu.Game/Scoring/ScoreInfo.cs +++ b/osu.Game/Scoring/ScoreInfo.cs @@ -86,10 +86,7 @@ namespace osu.Game.Scoring [Column("Mods")] public string ModsJson { - get - { - return modsJson; - } + get => modsJson; set { modsJson = value; From e7707eee94335149c2c284dfba421a93b11c0f69 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Fri, 12 Mar 2021 15:23:11 +0900 Subject: [PATCH 6879/6909] Switch RestoreDefaultsValueButton to use HasPendingTasks to avoid tooltip always showing --- osu.Game/Overlays/Settings/SettingsItem.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/osu.Game/Overlays/Settings/SettingsItem.cs b/osu.Game/Overlays/Settings/SettingsItem.cs index 8631b8ac7b..85765bf991 100644 --- a/osu.Game/Overlays/Settings/SettingsItem.cs +++ b/osu.Game/Overlays/Settings/SettingsItem.cs @@ -123,6 +123,8 @@ namespace osu.Game.Overlays.Settings protected internal class RestoreDefaultValueButton : Container, IHasTooltip { + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + private Bindable bindable; public Bindable Bindable @@ -147,7 +149,6 @@ namespace osu.Game.Overlays.Settings RelativeSizeAxes = Axes.Y; Width = SettingsPanel.CONTENT_MARGINS; Alpha = 0f; - AlwaysPresent = true; } [BackgroundDependencyLoader] From d0644221ff74801457b2b17efa728de723ddea20 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 9 Nov 2020 14:43:06 -0800 Subject: [PATCH 6880/6909] Add test showing toolbar behavior change --- .../Navigation/TestSceneScreenNavigation.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs index fc49517cdf..f2bb518b2e 100644 --- a/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs +++ b/osu.Game.Tests/Visual/Navigation/TestSceneScreenNavigation.cs @@ -229,6 +229,35 @@ namespace osu.Game.Tests.Visual.Navigation AddUntilStep("settings displayed", () => Game.Settings.State.Value == Visibility.Visible); } + [Test] + public void TestToolbarHiddenByUser() + { + AddStep("Enter menu", () => InputManager.Key(Key.Enter)); + + AddUntilStep("Wait for toolbar to load", () => Game.Toolbar.IsLoaded); + + AddStep("Hide toolbar", () => + { + InputManager.PressKey(Key.ControlLeft); + InputManager.Key(Key.T); + InputManager.ReleaseKey(Key.ControlLeft); + }); + + pushEscape(); + + AddStep("Enter menu", () => InputManager.Key(Key.Enter)); + + AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden); + + AddStep("Enter song select", () => + { + InputManager.Key(Key.Enter); + InputManager.Key(Key.Enter); + }); + + AddAssert("Toolbar is hidden", () => Game.Toolbar.State.Value == Visibility.Hidden); + } + private void pushEscape() => AddStep("Press escape", () => InputManager.Key(Key.Escape)); From 6c0734a09ff87754eb91917623f717476ee400e6 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 9 Nov 2020 14:45:20 -0800 Subject: [PATCH 6881/6909] Handle global action in toolbar instead of osugame --- osu.Game/OsuGame.cs | 4 ---- osu.Game/Overlays/Toolbar/Toolbar.cs | 22 ++++++++++++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index b7398efdc2..dd775888a1 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -885,10 +885,6 @@ namespace osu.Game frameworkConfig.GetBindable(FrameworkSetting.ConfineMouseMode).SetDefault(); return true; - case GlobalAction.ToggleToolbar: - Toolbar.ToggleVisibility(); - return true; - case GlobalAction.ToggleGameplayMouseButtons: LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get(OsuSetting.MouseDisableButtons)); return true; diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 0ccb22df3a..011f5a03c9 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -13,10 +13,12 @@ using osu.Framework.Allocation; using osu.Framework.Bindables; using osu.Framework.Input.Events; using osu.Game.Rulesets; +using osu.Framework.Input.Bindings; +using osu.Game.Input.Bindings; namespace osu.Game.Overlays.Toolbar { - public class Toolbar : VisibilityContainer + public class Toolbar : VisibilityContainer, IKeyBindingHandler { public const float HEIGHT = 40; public const float TOOLTIP_HEIGHT = 30; @@ -30,7 +32,7 @@ namespace osu.Game.Overlays.Toolbar protected readonly IBindable OverlayActivationMode = new Bindable(OverlayActivation.All); - // Toolbar components like RulesetSelector should receive keyboard input events even when the toolbar is hidden. + // Toolbar and its components need keyboard input even when hidden. public override bool PropagateNonPositionalInputSubTree => true; public Toolbar() @@ -164,5 +166,21 @@ namespace osu.Game.Overlays.Toolbar this.MoveToY(-DrawSize.Y, transition_time, Easing.OutQuint); this.FadeOut(transition_time, Easing.InQuint); } + + public bool OnPressed(GlobalAction action) + { + switch (action) + { + case GlobalAction.ToggleToolbar: + ToggleVisibility(); + return true; + } + + return false; + } + + public void OnReleased(GlobalAction action) + { + } } } From 62f2a823f6be7d27ec7c6db751193023927bb130 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 9 Nov 2020 14:46:08 -0800 Subject: [PATCH 6882/6909] Hide toolbar forever when the user hides it --- osu.Game/OsuGame.cs | 2 +- osu.Game/Overlays/Toolbar/Toolbar.cs | 6 ++++++ osu.Game/Screens/Menu/ButtonSystem.cs | 3 ++- 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index dd775888a1..fa9a0d4eb5 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -983,7 +983,7 @@ namespace osu.Game if (newOsuScreen.HideOverlaysOnEnter) CloseAllOverlays(); - else + else if (!Toolbar.HiddenByUser) Toolbar.Show(); if (newOsuScreen.AllowBackButton) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 011f5a03c9..7f77e5add9 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -23,6 +23,8 @@ namespace osu.Game.Overlays.Toolbar public const float HEIGHT = 40; public const float TOOLTIP_HEIGHT = 30; + public bool HiddenByUser; + public Action OnHome; private ToolbarUserButton userButton; @@ -169,10 +171,14 @@ namespace osu.Game.Overlays.Toolbar public bool OnPressed(GlobalAction action) { + if (OverlayActivationMode.Value == OverlayActivation.Disabled) + return false; + switch (action) { case GlobalAction.ToggleToolbar: ToggleVisibility(); + HiddenByUser = State.Value == Visibility.Hidden; return true; } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 81b1cb0bf1..8f1fd627f5 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -352,7 +352,8 @@ namespace osu.Game.Screens.Menu if (impact) logo.Impact(); - game?.Toolbar.Show(); + if (game?.Toolbar.HiddenByUser == false) + game.Toolbar.Show(); }, 200); break; From 5999e4ba3361f54828eaa25f1539ca0e2b991279 Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 9 Nov 2020 15:16:35 -0800 Subject: [PATCH 6883/6909] Add xmldoc for hiddenbyuser bool --- osu.Game/Overlays/Toolbar/Toolbar.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 7f77e5add9..483d82200b 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -23,6 +23,9 @@ namespace osu.Game.Overlays.Toolbar public const float HEIGHT = 40; public const float TOOLTIP_HEIGHT = 30; + /// + /// Whether the user hid this with . + /// public bool HiddenByUser; public Action OnHome; From 0ba5312a4063e3308c666e17ab4590bfe2f38267 Mon Sep 17 00:00:00 2001 From: Joehu Date: Sat, 13 Mar 2021 00:05:26 -0800 Subject: [PATCH 6884/6909] Move blocking show logic to UpdateState --- osu.Game/OsuGame.cs | 2 +- osu.Game/Overlays/Toolbar/Toolbar.cs | 12 +++++++++--- osu.Game/Screens/Menu/ButtonSystem.cs | 3 +-- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index fa9a0d4eb5..dd775888a1 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -983,7 +983,7 @@ namespace osu.Game if (newOsuScreen.HideOverlaysOnEnter) CloseAllOverlays(); - else if (!Toolbar.HiddenByUser) + else Toolbar.Show(); if (newOsuScreen.AllowBackButton) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 483d82200b..7497f4d210 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -26,7 +26,9 @@ namespace osu.Game.Overlays.Toolbar /// /// Whether the user hid this with . /// - public bool HiddenByUser; + private bool hiddenByUser; + + private bool userToggled; public Action OnHome; @@ -149,7 +151,9 @@ namespace osu.Game.Overlays.Toolbar protected override void UpdateState(ValueChangedEvent state) { - if (state.NewValue == Visibility.Visible && OverlayActivationMode.Value == OverlayActivation.Disabled) + var blockShow = !userToggled && hiddenByUser; + + if (state.NewValue == Visibility.Visible && (OverlayActivationMode.Value == OverlayActivation.Disabled || blockShow)) { State.Value = Visibility.Hidden; return; @@ -180,8 +184,10 @@ namespace osu.Game.Overlays.Toolbar switch (action) { case GlobalAction.ToggleToolbar: + userToggled = true; ToggleVisibility(); - HiddenByUser = State.Value == Visibility.Hidden; + hiddenByUser = State.Value == Visibility.Hidden; + userToggled = false; return true; } diff --git a/osu.Game/Screens/Menu/ButtonSystem.cs b/osu.Game/Screens/Menu/ButtonSystem.cs index 8f1fd627f5..81b1cb0bf1 100644 --- a/osu.Game/Screens/Menu/ButtonSystem.cs +++ b/osu.Game/Screens/Menu/ButtonSystem.cs @@ -352,8 +352,7 @@ namespace osu.Game.Screens.Menu if (impact) logo.Impact(); - if (game?.Toolbar.HiddenByUser == false) - game.Toolbar.Show(); + game?.Toolbar.Show(); }, 200); break; From b13f193c8dd60573aa579bb47b06d1d02ed0b5ef Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Mar 2021 19:26:38 +0900 Subject: [PATCH 6885/6909] Fix incorrect task being returned for changelog continuations --- osu.Game/Overlays/ChangelogOverlay.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index 2da5be5e6c..eda7748367 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -163,7 +163,7 @@ namespace osu.Game.Overlays await API.PerformAsync(req).ConfigureAwait(false); return tcs.Task; - }); + }).Unwrap(); } private CancellationTokenSource loadContentCancellation; From 4afbccfcff2544fa4a4e80d765a2bccb270c1658 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Mar 2021 19:30:40 +0900 Subject: [PATCH 6886/6909] Fix initial operation potentially running before DI is completed --- osu.Game/Overlays/ChangelogOverlay.cs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/osu.Game/Overlays/ChangelogOverlay.cs b/osu.Game/Overlays/ChangelogOverlay.cs index eda7748367..e7d68853ad 100644 --- a/osu.Game/Overlays/ChangelogOverlay.cs +++ b/osu.Game/Overlays/ChangelogOverlay.cs @@ -21,6 +21,8 @@ namespace osu.Game.Overlays { public class ChangelogOverlay : OnlineOverlay { + public override bool IsPresent => base.IsPresent || Scheduler.HasPendingTasks; + public readonly Bindable Current = new Bindable(); private Sample sampleBack; @@ -126,8 +128,11 @@ namespace osu.Game.Overlays private Task initialFetchTask; - private void performAfterFetch(Action action) => fetchListing()?.ContinueWith(_ => - Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion); + private void performAfterFetch(Action action) => Schedule(() => + { + fetchListing()?.ContinueWith(_ => + Schedule(action), TaskContinuationOptions.OnlyOnRanToCompletion); + }); private Task fetchListing() { From e70ba2d005c45bb485d3c985873fbecf99a1c71c Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Mar 2021 23:29:01 +0900 Subject: [PATCH 6887/6909] Remove unnecessary second variable --- osu.Game/Overlays/Toolbar/Toolbar.cs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 7497f4d210..5e2280e2fc 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -28,8 +28,6 @@ namespace osu.Game.Overlays.Toolbar /// private bool hiddenByUser; - private bool userToggled; - public Action OnHome; private ToolbarUserButton userButton; @@ -151,9 +149,9 @@ namespace osu.Game.Overlays.Toolbar protected override void UpdateState(ValueChangedEvent state) { - var blockShow = !userToggled && hiddenByUser; + bool blockShow = hiddenByUser || OverlayActivationMode.Value == OverlayActivation.Disabled; - if (state.NewValue == Visibility.Visible && (OverlayActivationMode.Value == OverlayActivation.Disabled || blockShow)) + if (state.NewValue == Visibility.Visible && blockShow) { State.Value = Visibility.Hidden; return; @@ -184,10 +182,8 @@ namespace osu.Game.Overlays.Toolbar switch (action) { case GlobalAction.ToggleToolbar: - userToggled = true; + hiddenByUser = State.Value == Visibility.Visible; // set before toggling to allow the operation to always succeed. ToggleVisibility(); - hiddenByUser = State.Value == Visibility.Hidden; - userToggled = false; return true; } From a227b0a581103004c10e767f0511f01bb8af2fbc Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Sat, 13 Mar 2021 23:29:47 +0900 Subject: [PATCH 6888/6909] Build on xmldoc with rationale --- osu.Game/Overlays/Toolbar/Toolbar.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/osu.Game/Overlays/Toolbar/Toolbar.cs b/osu.Game/Overlays/Toolbar/Toolbar.cs index 5e2280e2fc..d049c2d3ec 100644 --- a/osu.Game/Overlays/Toolbar/Toolbar.cs +++ b/osu.Game/Overlays/Toolbar/Toolbar.cs @@ -25,6 +25,7 @@ namespace osu.Game.Overlays.Toolbar /// /// Whether the user hid this with . + /// In this state, automatic toggles should not occur, respecting the user's preference to have no toolbar. /// private bool hiddenByUser; From 0a1e325fc774f371785ba95b618f71bd0637bb2e Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Sun, 14 Mar 2021 19:34:53 +0300 Subject: [PATCH 6889/6909] Extract requerying of navigational properties from DbContext --- osu.Game/Beatmaps/BeatmapManager.cs | 16 +---------- osu.Game/Database/Extensions.cs | 44 +++++++++++++++++++++++++++++ osu.Game/Scoring/ScoreManager.cs | 14 +-------- osu.Game/Skinning/SkinManager.cs | 9 +----- 4 files changed, 47 insertions(+), 36 deletions(-) create mode 100644 osu.Game/Database/Extensions.cs diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index 3254f53574..f42fba79cb 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -174,21 +174,7 @@ namespace osu.Game.Beatmaps if (beatmapSet.Beatmaps.Any(b => b.BaseDifficulty == null)) throw new InvalidOperationException($"Cannot import {nameof(BeatmapInfo)} with null {nameof(BeatmapInfo.BaseDifficulty)}."); - var dbContext = ContextFactory.Get(); - - // Workaround System.InvalidOperationException - // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. - foreach (var beatmap in beatmapSet.Beatmaps) - { - beatmap.Ruleset = dbContext.RulesetInfo.Find(beatmap.RulesetID); - } - - // Workaround System.InvalidOperationException - // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. - foreach (var file in beatmapSet.Files) - { - file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); - } + beatmapSet.Requery(ContextFactory); // check if a set already exists with the same online id, delete if it does. if (beatmapSet.OnlineBeatmapSetID != null) diff --git a/osu.Game/Database/Extensions.cs b/osu.Game/Database/Extensions.cs new file mode 100644 index 0000000000..3af26c348e --- /dev/null +++ b/osu.Game/Database/Extensions.cs @@ -0,0 +1,44 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Scoring; + +namespace osu.Game.Database +{ + public static class Extensions + { + public static void Requery(this BeatmapSetInfo beatmapSetInfo, IDatabaseContextFactory databaseContextFactory) + { + var dbContext = databaseContextFactory.Get(); + + foreach (var beatmap in beatmapSetInfo.Beatmaps) + { + // Workaround System.InvalidOperationException + // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. + beatmap.Ruleset = dbContext.RulesetInfo.Find(beatmap.RulesetID); + } + + beatmapSetInfo.Files.Requery(databaseContextFactory); + } + + public static void Requery(this ScoreInfo scoreInfo, IDatabaseContextFactory databaseContextFactory) + { + scoreInfo.Files.Requery(databaseContextFactory); + scoreInfo.Beatmap.BeatmapSet.Files.Requery(databaseContextFactory); + } + + public static void Requery(this List files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo + { + var dbContext = databaseContextFactory.Get(); + + foreach (var file in files) + { + // Workaround System.InvalidOperationException + // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. + file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); + } + } + } +} diff --git a/osu.Game/Scoring/ScoreManager.cs b/osu.Game/Scoring/ScoreManager.cs index a97c516a1b..1e90ee1ac7 100644 --- a/osu.Game/Scoring/ScoreManager.cs +++ b/osu.Game/Scoring/ScoreManager.cs @@ -54,19 +54,7 @@ namespace osu.Game.Scoring protected override void PreImport(ScoreInfo model) { - var dbContext = ContextFactory.Get(); - - // Workaround System.InvalidOperationException - // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. - foreach (var file in model.Files) - { - file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); - } - - foreach (var file in model.Beatmap.BeatmapSet.Files) - { - file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); - } + model.Requery(ContextFactory); } protected override ScoreInfo CreateModel(ArchiveReader archive) diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index 2bb27b60d6..c25f00eccb 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -144,14 +144,7 @@ namespace osu.Game.Skinning protected override void PreImport(SkinInfo model) { - var dbContext = ContextFactory.Get(); - - // Workaround System.InvalidOperationException - // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. - foreach (var file in model.Files) - { - file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); - } + model.Files.Requery(ContextFactory); } /// From 61d5a6cc57941cab31e2abb0acee00c8fad3f80f Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Sun, 14 Mar 2021 19:47:14 +0300 Subject: [PATCH 6890/6909] Simplify Microsoft.EntityFrameworkCore.Design PackageReference --- osu.Desktop/osu.Desktop.csproj | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/osu.Desktop/osu.Desktop.csproj b/osu.Desktop/osu.Desktop.csproj index 4af69c573d..d9d23dea6b 100644 --- a/osu.Desktop/osu.Desktop.csproj +++ b/osu.Desktop/osu.Desktop.csproj @@ -28,10 +28,7 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + From 28ef64b62a5d873110a163c4275fb48c9c47b262 Mon Sep 17 00:00:00 2001 From: Roman Kapustin Date: Sun, 14 Mar 2021 21:43:27 +0300 Subject: [PATCH 6891/6909] Explicitly specify SingleQuery behavior --- osu.Game/Database/OsuDbContext.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index 689f248de8..e5ae530018 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -3,7 +3,6 @@ using System; using Microsoft.EntityFrameworkCore; -using Microsoft.EntityFrameworkCore.Diagnostics; using Microsoft.Extensions.Logging; using osu.Framework.Logging; using osu.Framework.Statistics; @@ -111,8 +110,7 @@ namespace osu.Game.Database { base.OnConfiguring(optionsBuilder); optionsBuilder - .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10)) - .ConfigureWarnings(w => w.Ignore(RelationalEventId.MultipleCollectionIncludeWarning)) + .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10).UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery)) .UseLoggerFactory(logger.Value); } From 900da7b891c3a74faa3ebbe0f030e9cf3350b273 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Mar 2021 12:42:41 +0900 Subject: [PATCH 6892/6909] Rename and refactor extenion methods to be easier to read --- .../Database/DatabaseWorkaroundExtensions.cs | 48 +++++++++++++++++++ osu.Game/Database/Extensions.cs | 44 ----------------- 2 files changed, 48 insertions(+), 44 deletions(-) create mode 100644 osu.Game/Database/DatabaseWorkaroundExtensions.cs delete mode 100644 osu.Game/Database/Extensions.cs diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs new file mode 100644 index 0000000000..07ce7e8529 --- /dev/null +++ b/osu.Game/Database/DatabaseWorkaroundExtensions.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.Collections.Generic; +using osu.Game.Beatmaps; +using osu.Game.Scoring; + +namespace osu.Game.Database +{ + public static class DatabaseWorkaroundExtensions + { + public static void Requery(this IHasPrimaryKey model, IDatabaseContextFactory contextFactory) + { + switch (model) + { + case ScoreInfo scoreInfo: + scoreInfo.Beatmap.BeatmapSet.Requery(contextFactory); + scoreInfo.Files.RequeryFiles(contextFactory); + break; + + case BeatmapSetInfo beatmapSetInfo: + var context = contextFactory.Get(); + + foreach (var beatmap in beatmapSetInfo.Beatmaps) + { + // Workaround System.InvalidOperationException + // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. + beatmap.Ruleset = context.RulesetInfo.Find(beatmap.RulesetID); + } + + requeryFiles(beatmapSetInfo.Files, contextFactory); + break; + } + } + + public static void RequeryFiles(this List files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo + { + var dbContext = databaseContextFactory.Get(); + + foreach (var file in files) + { + // Workaround System.InvalidOperationException + // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. + file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); + } + } + } +} diff --git a/osu.Game/Database/Extensions.cs b/osu.Game/Database/Extensions.cs deleted file mode 100644 index 3af26c348e..0000000000 --- a/osu.Game/Database/Extensions.cs +++ /dev/null @@ -1,44 +0,0 @@ -// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. -// See the LICENCE file in the repository root for full licence text. - -using System.Collections.Generic; -using osu.Game.Beatmaps; -using osu.Game.Scoring; - -namespace osu.Game.Database -{ - public static class Extensions - { - public static void Requery(this BeatmapSetInfo beatmapSetInfo, IDatabaseContextFactory databaseContextFactory) - { - var dbContext = databaseContextFactory.Get(); - - foreach (var beatmap in beatmapSetInfo.Beatmaps) - { - // Workaround System.InvalidOperationException - // The instance of entity type 'RulesetInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. - beatmap.Ruleset = dbContext.RulesetInfo.Find(beatmap.RulesetID); - } - - beatmapSetInfo.Files.Requery(databaseContextFactory); - } - - public static void Requery(this ScoreInfo scoreInfo, IDatabaseContextFactory databaseContextFactory) - { - scoreInfo.Files.Requery(databaseContextFactory); - scoreInfo.Beatmap.BeatmapSet.Files.Requery(databaseContextFactory); - } - - public static void Requery(this List files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo - { - var dbContext = databaseContextFactory.Get(); - - foreach (var file in files) - { - // Workaround System.InvalidOperationException - // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. - file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); - } - } - } -} From 2bdffd10044984ea0a5639754b8ecff8f4fc979b Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Mar 2021 12:47:58 +0900 Subject: [PATCH 6893/6909] Move skin requery logic into extension methods --- .../Database/DatabaseWorkaroundExtensions.cs | 19 +++++++++++++++++-- osu.Game/Skinning/SkinManager.cs | 2 +- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs index 07ce7e8529..39bf358071 100644 --- a/osu.Game/Database/DatabaseWorkaroundExtensions.cs +++ b/osu.Game/Database/DatabaseWorkaroundExtensions.cs @@ -1,21 +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.Game.Beatmaps; using osu.Game.Scoring; +using osu.Game.Skinning; namespace osu.Game.Database { + /// + /// Extension methods which contain workarounds to make EFcore 5.x work with our existing (incorrect) thread safety. + /// The intention is to avoid blocking package updates while we consider the future of the database backend, with a potential backend switch imminent. + /// public static class DatabaseWorkaroundExtensions { + /// + /// Re-query the provided model to ensure it is in a sane state. This method requires explicit implementation per model type. + /// + /// + /// public static void Requery(this IHasPrimaryKey model, IDatabaseContextFactory contextFactory) { switch (model) { + case SkinInfo skinInfo: + requeryFiles(skinInfo.Files, contextFactory); + break; + case ScoreInfo scoreInfo: scoreInfo.Beatmap.BeatmapSet.Requery(contextFactory); - scoreInfo.Files.RequeryFiles(contextFactory); + requeryFiles(scoreInfo.Files, contextFactory); break; case BeatmapSetInfo beatmapSetInfo: @@ -33,7 +48,7 @@ namespace osu.Game.Database } } - public static void RequeryFiles(this List files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo + private static void requeryFiles(List files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo { var dbContext = databaseContextFactory.Get(); diff --git a/osu.Game/Skinning/SkinManager.cs b/osu.Game/Skinning/SkinManager.cs index c25f00eccb..601b77e782 100644 --- a/osu.Game/Skinning/SkinManager.cs +++ b/osu.Game/Skinning/SkinManager.cs @@ -144,7 +144,7 @@ namespace osu.Game.Skinning protected override void PreImport(SkinInfo model) { - model.Files.Requery(ContextFactory); + model.Requery(ContextFactory); } /// From 8a3553388972349a6833afcf915f44a7dae19995 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Mar 2021 12:48:23 +0900 Subject: [PATCH 6894/6909] Add fall-through case to catch a potential requery for unsupported model type --- osu.Game/Database/DatabaseWorkaroundExtensions.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs index 39bf358071..1d5c98ed8d 100644 --- a/osu.Game/Database/DatabaseWorkaroundExtensions.cs +++ b/osu.Game/Database/DatabaseWorkaroundExtensions.cs @@ -45,6 +45,9 @@ namespace osu.Game.Database requeryFiles(beatmapSetInfo.Files, contextFactory); break; + + default: + throw new ArgumentException($"{nameof(Requery)} does not have support for the provided model type", nameof(model)); } } From 1573298e682329d133059a74dd56c8c849b86bba Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Mar 2021 13:12:10 +0900 Subject: [PATCH 6895/6909] Update remaining package references to point to efcore5 --- .../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.iOS.props | 2 -- 6 files changed, 5 insertions(+), 7 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 728af5124e..42f70151ac 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 @@ -5,7 +5,7 @@ - + WinExe 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 af16f39563..e51b20c9fe 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 @@ -5,7 +5,7 @@ - + WinExe 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 3d2d1f3fec..f1f75148ef 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 @@ -5,7 +5,7 @@ - + WinExe 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 fa00922706..c9a320bdd5 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 @@ -5,7 +5,7 @@ - + WinExe diff --git a/osu.Game.Tests/osu.Game.Tests.csproj b/osu.Game.Tests/osu.Game.Tests.csproj index e36b3cdc74..6f8e0fac6f 100644 --- a/osu.Game.Tests/osu.Game.Tests.csproj +++ b/osu.Game.Tests/osu.Game.Tests.csproj @@ -6,7 +6,7 @@ - + diff --git a/osu.iOS.props b/osu.iOS.props index ccd33bf88c..71fcdd45f3 100644 --- a/osu.iOS.props +++ b/osu.iOS.props @@ -90,8 +90,6 @@ - - From 79d3379f55b2b23dbfdd613c58adc8a8b3259768 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Mar 2021 13:20:22 +0900 Subject: [PATCH 6896/6909] Reformat application of configuration --- osu.Game/Database/OsuDbContext.cs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/osu.Game/Database/OsuDbContext.cs b/osu.Game/Database/OsuDbContext.cs index e5ae530018..2342ab07d4 100644 --- a/osu.Game/Database/OsuDbContext.cs +++ b/osu.Game/Database/OsuDbContext.cs @@ -110,7 +110,10 @@ namespace osu.Game.Database { base.OnConfiguring(optionsBuilder); optionsBuilder - .UseSqlite(connectionString, sqliteOptions => sqliteOptions.CommandTimeout(10).UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery)) + .UseSqlite(connectionString, + sqliteOptions => sqliteOptions + .CommandTimeout(10) + .UseQuerySplittingBehavior(QuerySplittingBehavior.SingleQuery)) .UseLoggerFactory(logger.Value); } From 2904f479c65bf9f2cd76a3164ab51d13ff53dc96 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Mar 2021 13:26:14 +0900 Subject: [PATCH 6897/6909] Share file lookup workaround in ArchiveModelManager with workaround extensions class --- osu.Game/Database/ArchiveModelManager.cs | 4 +--- .../Database/DatabaseWorkaroundExtensions.cs | 23 +++++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index fe2caaa0b7..31c365b478 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -462,9 +462,7 @@ namespace osu.Game.Database // Dereference the existing file info, since the file model will be removed. if (file.FileInfo != null) { - // Workaround System.InvalidOperationException - // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. - file.FileInfo = usage.Context.FileInfo.Find(file.FileInfoID); + file.Requery(usage.Context); Files.Dereference(file.FileInfo); diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs index 1d5c98ed8d..8ac05f78e0 100644 --- a/osu.Game/Database/DatabaseWorkaroundExtensions.cs +++ b/osu.Game/Database/DatabaseWorkaroundExtensions.cs @@ -49,18 +49,23 @@ namespace osu.Game.Database default: throw new ArgumentException($"{nameof(Requery)} does not have support for the provided model type", nameof(model)); } + + void requeryFiles(List files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo + { + var dbContext = databaseContextFactory.Get(); + + foreach (var file in files) + { + Requery(file, dbContext); + } + } } - private static void requeryFiles(List files, IDatabaseContextFactory databaseContextFactory) where T : class, INamedFileInfo + public static void Requery(this INamedFileInfo file, OsuDbContext dbContext) { - var dbContext = databaseContextFactory.Get(); - - foreach (var file in files) - { - // Workaround System.InvalidOperationException - // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. - file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); - } + // Workaround System.InvalidOperationException + // The instance of entity type 'FileInfo' cannot be tracked because another instance with the same key value for {'ID'} is already being tracked. + file.FileInfo = dbContext.FileInfo.Find(file.FileInfoID); } } } From fce21f23d687ab72be391c23987ad1edff8740f0 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Mar 2021 13:29:26 +0900 Subject: [PATCH 6898/6909] Add comments marking workarounds required for EFcore 5 --- .../Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs | 2 +- .../Visual/SongSelect/TestSceneBeatmapRecommendations.cs | 2 +- osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs | 2 +- osu.Game/Database/ArchiveModelManager.cs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs index 4a9eaa1842..8cfe5d8af2 100644 --- a/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs +++ b/osu.Game.Tests/Visual/Multiplayer/TestSceneMultiplayerMatchSongSelect.cs @@ -56,7 +56,7 @@ namespace osu.Game.Tests.Visual.Multiplayer beatmaps.Add(new BeatmapInfo { Ruleset = rulesets.GetRuleset(i % 4), - RulesetID = i % 4, + RulesetID = i % 4, // workaround for efcore 5 compatibility. OnlineBeatmapID = beatmapId, Length = length, BPM = bpm, diff --git a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs index 223ace6ca5..9b8b74e6f6 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestSceneBeatmapRecommendations.cs @@ -186,7 +186,7 @@ namespace osu.Game.Tests.Visual.SongSelect Metadata = metadata, BaseDifficulty = new BeatmapDifficulty(), Ruleset = ruleset, - RulesetID = ruleset.ID.GetValueOrDefault(), + RulesetID = ruleset.ID.GetValueOrDefault(), // workaround for efcore 5 compatibility. StarDifficulty = difficultyIndex + 1, Version = $"SR{difficultyIndex + 1}" }).ToList() diff --git a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs index 4b402d0c54..2d192ae207 100644 --- a/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs +++ b/osu.Game.Tests/Visual/SongSelect/TestScenePlaySongSelect.cs @@ -915,7 +915,7 @@ namespace osu.Game.Tests.Visual.SongSelect beatmaps.Add(new BeatmapInfo { Ruleset = ruleset, - RulesetID = ruleset.ID.GetValueOrDefault(), + RulesetID = ruleset.ID.GetValueOrDefault(), // workaround for efcore 5 compatibility. OnlineBeatmapID = beatmapId, Version = $"{beatmapId} (length {TimeSpan.FromMilliseconds(length):m\\:ss}, bpm {bpm:0.#})", Length = length, diff --git a/osu.Game/Database/ArchiveModelManager.cs b/osu.Game/Database/ArchiveModelManager.cs index 31c365b478..64428882ac 100644 --- a/osu.Game/Database/ArchiveModelManager.cs +++ b/osu.Game/Database/ArchiveModelManager.cs @@ -642,7 +642,7 @@ namespace osu.Game.Database { Filename = file.Substring(prefix.Length).ToStandardisedPath(), FileInfo = fileInfo, - FileInfoID = fileInfo.ID + FileInfoID = fileInfo.ID // workaround for efcore 5 compatibility. }); } } From 6d4c1ba2aee43c9fdab49f83ba16c0bcdedcce78 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Mar 2021 13:35:08 +0900 Subject: [PATCH 6899/6909] Fix a couple of new inspections introduced in Rider EAPs --- osu.Game/Beatmaps/BeatmapConverter.cs | 10 +++++----- osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs | 3 --- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapConverter.cs b/osu.Game/Beatmaps/BeatmapConverter.cs index cb0b3a8d09..b291edd19d 100644 --- a/osu.Game/Beatmaps/BeatmapConverter.cs +++ b/osu.Game/Beatmaps/BeatmapConverter.cs @@ -17,12 +17,12 @@ namespace osu.Game.Beatmaps public abstract class BeatmapConverter : IBeatmapConverter where T : HitObject { - private event Action> ObjectConverted; + private event Action> objectConverted; event Action> IBeatmapConverter.ObjectConverted { - add => ObjectConverted += value; - remove => ObjectConverted -= value; + add => objectConverted += value; + remove => objectConverted -= value; } public IBeatmap Beatmap { get; } @@ -92,10 +92,10 @@ namespace osu.Game.Beatmaps var converted = ConvertHitObject(obj, beatmap, cancellationToken); - if (ObjectConverted != null) + if (objectConverted != null) { converted = converted.ToList(); - ObjectConverted.Invoke(obj, converted); + objectConverted.Invoke(obj, converted); } foreach (var c in converted) diff --git a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs index df940e8c8e..d06478b9de 100644 --- a/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs +++ b/osu.Game/Beatmaps/Formats/LegacyBeatmapEncoder.cs @@ -471,9 +471,6 @@ namespace osu.Game.Beatmaps.Formats private string toLegacyCustomSampleBank(HitSampleInfo hitSampleInfo) { - if (hitSampleInfo == null) - return "0"; - if (hitSampleInfo is ConvertHitObjectParser.LegacyHitSampleInfo legacy) return legacy.CustomSampleBank.ToString(CultureInfo.InvariantCulture); From 1e519f0d31125a3bb508be2dd97777556f69f0b1 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 15 Mar 2021 14:20:59 +0900 Subject: [PATCH 6900/6909] Fix seemingly innocent logic change causing breakage in score imports --- osu.Game/Database/DatabaseWorkaroundExtensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/osu.Game/Database/DatabaseWorkaroundExtensions.cs b/osu.Game/Database/DatabaseWorkaroundExtensions.cs index 8ac05f78e0..a3a982f232 100644 --- a/osu.Game/Database/DatabaseWorkaroundExtensions.cs +++ b/osu.Game/Database/DatabaseWorkaroundExtensions.cs @@ -29,7 +29,7 @@ namespace osu.Game.Database break; case ScoreInfo scoreInfo: - scoreInfo.Beatmap.BeatmapSet.Requery(contextFactory); + requeryFiles(scoreInfo.Beatmap.BeatmapSet.Files, contextFactory); requeryFiles(scoreInfo.Files, contextFactory); break; From 3dd72d6f7d229bd7cb8311e707ace797fed31d3e Mon Sep 17 00:00:00 2001 From: Joehu Date: Sun, 14 Mar 2021 22:47:05 -0700 Subject: [PATCH 6901/6909] Fix disable mouse buttons setting not showing default indicator when using keybind --- osu.Game/OsuGame.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index dd775888a1..eb34ba4a37 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -886,7 +886,9 @@ namespace osu.Game return true; case GlobalAction.ToggleGameplayMouseButtons: - LocalConfig.Set(OsuSetting.MouseDisableButtons, !LocalConfig.Get(OsuSetting.MouseDisableButtons)); + var mouseDisableButtons = LocalConfig.GetBindable(OsuSetting.MouseDisableButtons); + + mouseDisableButtons.Value = !mouseDisableButtons.Value; return true; case GlobalAction.RandomSkin: From 393f1fbd3f7f4169fefb1df16b689e66314e6fdf Mon Sep 17 00:00:00 2001 From: Joehu Date: Mon, 15 Mar 2021 10:07:50 -0700 Subject: [PATCH 6902/6909] Remove skype --- osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs | 1 - osu.Game/Users/User.cs | 3 --- 2 files changed, 4 deletions(-) diff --git a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs index 2925107766..662f55317b 100644 --- a/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs +++ b/osu.Game/Overlays/Profile/Header/BottomHeaderContainer.cs @@ -138,7 +138,6 @@ namespace osu.Game.Overlays.Profile.Header if (!string.IsNullOrEmpty(user.Twitter)) anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Twitter, "@" + user.Twitter, $@"https://twitter.com/{user.Twitter}"); anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Discord, user.Discord); - anyInfoAdded |= tryAddInfo(FontAwesome.Brands.Skype, user.Skype, @"skype:" + user.Skype + @"?chat"); anyInfoAdded |= tryAddInfo(FontAwesome.Solid.Link, websiteWithoutProtocol, user.Website); // If no information was added to the bottomLinkContainer, hide it to avoid unwanted padding diff --git a/osu.Game/Users/User.cs b/osu.Game/Users/User.cs index 4d537b91bd..6c45417db0 100644 --- a/osu.Game/Users/User.cs +++ b/osu.Game/Users/User.cs @@ -111,9 +111,6 @@ namespace osu.Game.Users [JsonProperty(@"twitter")] public string Twitter; - [JsonProperty(@"skype")] - public string Skype; - [JsonProperty(@"discord")] public string Discord; From da3dc61aae29202b7a0ebb498c274852c1d955ab Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Tue, 16 Mar 2021 10:58:42 +0900 Subject: [PATCH 6903/6909] Remove newline --- osu.Game/OsuGame.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/osu.Game/OsuGame.cs b/osu.Game/OsuGame.cs index eb34ba4a37..7d11029a9c 100644 --- a/osu.Game/OsuGame.cs +++ b/osu.Game/OsuGame.cs @@ -887,7 +887,6 @@ namespace osu.Game case GlobalAction.ToggleGameplayMouseButtons: var mouseDisableButtons = LocalConfig.GetBindable(OsuSetting.MouseDisableButtons); - mouseDisableButtons.Value = !mouseDisableButtons.Value; return true; From c7740d1181a165466b9960ed54f10c7e14e85075 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Mar 2021 15:52:24 +0900 Subject: [PATCH 6904/6909] Fix opening the editor occasionally causing a hard crash due to incorrect threading logic Setting one of the global screen `Bindable`s (in this case, `Beatmap`) is not valid from anywhere but the update thread. This changes the order in which things happen during the editor startup process to ensure correctness. Closes #11968. --- osu.Game/Screens/Edit/Editor.cs | 67 +++++++++++++++++++++------------ 1 file changed, 42 insertions(+), 25 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 0ba202b082..3a4c3491ff 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -106,26 +106,29 @@ namespace osu.Game.Screens.Edit [BackgroundDependencyLoader] private void load(OsuColour colours, GameHost host, OsuConfigManager config) { - if (Beatmap.Value is DummyWorkingBeatmap) + var loadableBeatmap = Beatmap.Value; + + if (loadableBeatmap is DummyWorkingBeatmap) { isNewBeatmap = true; - var newBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); + loadableBeatmap = beatmapManager.CreateNew(Ruleset.Value, api.LocalUser.Value); + + // required so we can get the track length in EditorClock. + // this is safe as nothing has yet got a reference to this new beatmap. + loadableBeatmap.LoadTrack(); // this is a bit haphazard, but guards against setting the lease Beatmap bindable if // the editor has already been exited. if (!ValidForPush) return; - - // this probably shouldn't be set in the asynchronous load method, but everything following relies on it. - Beatmap.Value = newBeatmap; } - beatDivisor.Value = Beatmap.Value.BeatmapInfo.BeatDivisor; - beatDivisor.BindValueChanged(divisor => Beatmap.Value.BeatmapInfo.BeatDivisor = divisor.NewValue); + beatDivisor.Value = loadableBeatmap.BeatmapInfo.BeatDivisor; + beatDivisor.BindValueChanged(divisor => loadableBeatmap.BeatmapInfo.BeatDivisor = divisor.NewValue); // Todo: should probably be done at a DrawableRuleset level to share logic with Player. - clock = new EditorClock(Beatmap.Value, beatDivisor) { IsCoupled = false }; + clock = new EditorClock(loadableBeatmap, beatDivisor) { IsCoupled = false }; UpdateClockSource(); @@ -139,7 +142,7 @@ namespace osu.Game.Screens.Edit try { - playableBeatmap = Beatmap.Value.GetPlayableBeatmap(Beatmap.Value.BeatmapInfo.Ruleset); + playableBeatmap = loadableBeatmap.GetPlayableBeatmap(loadableBeatmap.BeatmapInfo.Ruleset); // clone these locally for now to avoid incurring overhead on GetPlayableBeatmap usages. // eventually we will want to improve how/where this is done as there are issues with *not* cloning it in all cases. @@ -153,13 +156,21 @@ namespace osu.Game.Screens.Edit return; } - AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, Beatmap.Value.Skin)); + AddInternal(editorBeatmap = new EditorBeatmap(playableBeatmap, loadableBeatmap.Skin)); dependencies.CacheAs(editorBeatmap); changeHandler = new EditorChangeHandler(editorBeatmap); dependencies.CacheAs(changeHandler); updateLastSavedHash(); + Schedule(() => + { + // we need to avoid changing the beatmap from an asynchronous load thread. it can potentially cause weirdness including crashes. + // this assumes that nothing during the rest of this load() method is accessing Beatmap.Value (loadableBeatmap should be preferred). + // generally this is quite safe, as the actual load of editor content comes after menuBar.Mode.ValueChanged is fired in its own LoadComplete. + Beatmap.Value = loadableBeatmap; + }); + OsuMenuItem undoMenuItem; OsuMenuItem redoMenuItem; @@ -167,17 +178,6 @@ namespace osu.Game.Screens.Edit EditorMenuItem copyMenuItem; EditorMenuItem pasteMenuItem; - var fileMenuItems = new List - { - new EditorMenuItem("Save", MenuItemType.Standard, Save) - }; - - if (RuntimeInfo.IsDesktop) - fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap)); - - fileMenuItems.Add(new EditorMenuItemSpacer()); - fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); - AddInternal(new OsuContextMenuContainer { RelativeSizeAxes = Axes.Both, @@ -209,7 +209,7 @@ namespace osu.Game.Screens.Edit { new MenuItem("File") { - Items = fileMenuItems + Items = createFileMenuItems() }, new MenuItem("Edit") { @@ -242,7 +242,11 @@ namespace osu.Game.Screens.Edit Height = 60, Children = new Drawable[] { - bottomBackground = new Box { RelativeSizeAxes = Axes.Both }, + bottomBackground = new Box + { + RelativeSizeAxes = Axes.Both, + Colour = colours.Gray2 + }, new Container { RelativeSizeAxes = Axes.Both, @@ -299,8 +303,6 @@ namespace osu.Game.Screens.Edit clipboard.BindValueChanged(content => pasteMenuItem.Action.Disabled = string.IsNullOrEmpty(content.NewValue)); menuBar.Mode.ValueChanged += onModeChanged; - - bottomBackground.Colour = colours.Gray2; } /// @@ -681,6 +683,21 @@ namespace osu.Game.Screens.Edit lastSavedHash = changeHandler.CurrentStateHash; } + private List createFileMenuItems() + { + var fileMenuItems = new List + { + new EditorMenuItem("Save", MenuItemType.Standard, Save) + }; + + if (RuntimeInfo.IsDesktop) + fileMenuItems.Add(new EditorMenuItem("Export package", MenuItemType.Standard, exportBeatmap)); + + fileMenuItems.Add(new EditorMenuItemSpacer()); + fileMenuItems.Add(new EditorMenuItem("Exit", MenuItemType.Standard, this.Exit)); + return fileMenuItems; + } + public double SnapTime(double time, double? referenceTime) => editorBeatmap.SnapTime(time, referenceTime); public double GetBeatLengthAtTime(double referenceTime) => editorBeatmap.GetBeatLengthAtTime(referenceTime); From eda891223c469cdb23a07fa8faeac7331db2aa33 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Mar 2021 16:47:12 +0900 Subject: [PATCH 6905/6909] Start the editor with empty artist/creator/difficulty name fields --- osu.Game/Beatmaps/BeatmapManager.cs | 3 --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 8 ++++---- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/osu.Game/Beatmaps/BeatmapManager.cs b/osu.Game/Beatmaps/BeatmapManager.cs index f42fba79cb..115d1b33bb 100644 --- a/osu.Game/Beatmaps/BeatmapManager.cs +++ b/osu.Game/Beatmaps/BeatmapManager.cs @@ -113,8 +113,6 @@ namespace osu.Game.Beatmaps { var metadata = new BeatmapMetadata { - Artist = "artist", - Title = "title", Author = user, }; @@ -128,7 +126,6 @@ namespace osu.Game.Beatmaps BaseDifficulty = new BeatmapDifficulty(), Ruleset = ruleset, Metadata = metadata, - Version = "difficulty" } } }; diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index e812c042fb..2b10be0423 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -28,25 +28,25 @@ namespace osu.Game.Screens.Edit.Setup }, artistTextBox = new LabelledTextBox { - Label = "Artist", + PlaceholderText = "Artist", Current = { Value = Beatmap.Metadata.Artist }, TabbableContentContainer = this }, titleTextBox = new LabelledTextBox { - Label = "Title", + PlaceholderText = "Title", Current = { Value = Beatmap.Metadata.Title }, TabbableContentContainer = this }, creatorTextBox = new LabelledTextBox { - Label = "Creator", + PlaceholderText = "Creator", Current = { Value = Beatmap.Metadata.AuthorString }, TabbableContentContainer = this }, difficultyTextBox = new LabelledTextBox { - Label = "Difficulty Name", + PlaceholderText = "Difficulty Name", Current = { Value = Beatmap.BeatmapInfo.Version }, TabbableContentContainer = this }, From 26d6f96c4e2decdba1836a98280a2ba6381c379d Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Mar 2021 16:56:58 +0900 Subject: [PATCH 6906/6909] Fix LabelledTextBox not correctly forwarding focus to its underlying TextBox component --- osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs index 4aeda74be8..266eb11319 100644 --- a/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs +++ b/osu.Game/Graphics/UserInterfaceV2/LabelledTextBox.cs @@ -5,6 +5,7 @@ using osu.Framework.Allocation; using osu.Framework.Graphics; using osu.Framework.Graphics.Containers; using osu.Framework.Graphics.UserInterface; +using osu.Framework.Input.Events; using osu.Game.Graphics.UserInterface; namespace osu.Game.Graphics.UserInterfaceV2 @@ -53,6 +54,14 @@ namespace osu.Game.Graphics.UserInterfaceV2 CornerRadius = CORNER_RADIUS, }; + public override bool AcceptsFocus => true; + + protected override void OnFocus(FocusEvent e) + { + base.OnFocus(e); + GetContainingInputManager().ChangeFocus(Component); + } + protected override OsuTextBox CreateComponent() => CreateTextBox().With(t => { t.OnCommit += (sender, newText) => OnCommit?.Invoke(sender, newText); From 5adc675862afb015ad737db47ebda917fc24c2f2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Mar 2021 16:57:14 +0900 Subject: [PATCH 6907/6909] Focus artist textbox on entering song setup if fields are empty --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index 2b10be0423..c5a2b77ab4 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -56,6 +56,14 @@ namespace osu.Game.Screens.Edit.Setup item.OnCommit += onCommit; } + protected override void LoadComplete() + { + base.LoadComplete(); + + if (string.IsNullOrEmpty(artistTextBox.Current.Value)) + GetContainingInputManager().ChangeFocus(artistTextBox); + } + private void onCommit(TextBox sender, bool newText) { if (!newText) return; From 3b6a1180b68515b82a9e970f84a3e51d5f908f4e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Mar 2021 17:02:11 +0900 Subject: [PATCH 6908/6909] Remove non-accessed field --- osu.Game/Screens/Edit/Editor.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/osu.Game/Screens/Edit/Editor.cs b/osu.Game/Screens/Edit/Editor.cs index 3a4c3491ff..0c24eb6a4d 100644 --- a/osu.Game/Screens/Edit/Editor.cs +++ b/osu.Game/Screens/Edit/Editor.cs @@ -74,7 +74,6 @@ namespace osu.Game.Screens.Edit private string lastSavedHash; - private Box bottomBackground; private Container screenContainer; private EditorScreen currentScreen; @@ -242,7 +241,7 @@ namespace osu.Game.Screens.Edit Height = 60, Children = new Drawable[] { - bottomBackground = new Box + new Box { RelativeSizeAxes = Axes.Both, Colour = colours.Gray2 From d0e61e5b4d609c342e55e2b8a2ed86c66977d11e Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Wed, 17 Mar 2021 17:14:04 +0900 Subject: [PATCH 6909/6909] Put back the label --- osu.Game/Screens/Edit/Setup/MetadataSection.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/osu.Game/Screens/Edit/Setup/MetadataSection.cs b/osu.Game/Screens/Edit/Setup/MetadataSection.cs index c5a2b77ab4..f429164ece 100644 --- a/osu.Game/Screens/Edit/Setup/MetadataSection.cs +++ b/osu.Game/Screens/Edit/Setup/MetadataSection.cs @@ -28,25 +28,25 @@ namespace osu.Game.Screens.Edit.Setup }, artistTextBox = new LabelledTextBox { - PlaceholderText = "Artist", + Label = "Artist", Current = { Value = Beatmap.Metadata.Artist }, TabbableContentContainer = this }, titleTextBox = new LabelledTextBox { - PlaceholderText = "Title", + Label = "Title", Current = { Value = Beatmap.Metadata.Title }, TabbableContentContainer = this }, creatorTextBox = new LabelledTextBox { - PlaceholderText = "Creator", + Label = "Creator", Current = { Value = Beatmap.Metadata.AuthorString }, TabbableContentContainer = this }, difficultyTextBox = new LabelledTextBox { - PlaceholderText = "Difficulty Name", + Label = "Difficulty Name", Current = { Value = Beatmap.BeatmapInfo.Version }, TabbableContentContainer = this },

    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 Ni{DEH3<&Xe{{o+^{JH=D literal 0 HcmV?d00001 diff --git a/osu.Game.Tests/Resources/TestResources.cs b/osu.Game.Tests/Resources/TestResources.cs index e882229570..14bc2c8733 100644 --- a/osu.Game.Tests/Resources/TestResources.cs +++ b/osu.Game.Tests/Resources/TestResources.cs @@ -13,8 +13,31 @@ namespace osu.Game.Tests.Resources public static Stream OpenResource(string name) => GetStore().GetStream($"Resources/{name}"); - public static Stream GetTestBeatmapStream(bool virtualTrack = false) => OpenResource($"Archives/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}.osz"); + public static Stream GetTestBeatmapStream(bool virtualTrack = false, bool quick = false) => OpenResource($"Archives/241526 Soleily - Renatus{(virtualTrack ? "_virtual" : "")}{(quick ? "_quick" : "")}.osz"); + ///

    /// 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 1112/6909] 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 1113/6909] 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 1114/6909] 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 1115/6909] 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 1116/6909] 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 1117/6909] 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 1118/6909] 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 1119/6909] 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 1120/6909] 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 1121/6909] 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 1122/6909] 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 1123/6909] 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 1124/6909] 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 1125/6909] 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 1126/6909] 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 1127/6909] 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 1128/6909] 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 1129/6909] 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 1130/6909] 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 1131/6909] 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 1132/6909] 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 1133/6909] 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 1134/6909] 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 1135/6909] 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 1136/6909] 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 1137/6909] 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 1138/6909] 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 1139/6909] 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 1140/6909] 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 1141/6909] 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 d61388880364b45b49ac037c9d04bd0427fcf80c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 11 May 2020 14:50:02 +0900 Subject: [PATCH 1142/6909] Add initial changes --- .../Preprocessing/StaminaCheeseDetector.cs | 95 ++++++++++++ .../Preprocessing/TaikoDifficultyHitObject.cs | 30 +++- .../TaikoDifficultyHitObjectRhythm.cs | 124 +++++++++++++++ .../Difficulty/Skills/Colour.cs | 144 ++++++++++++++++++ .../Difficulty/Skills/SpeedInvariantRhythm.cs | 133 ++++++++++++++++ .../Difficulty/Skills/Stamina.cs | 103 +++++++++++++ .../Difficulty/Skills/Strain.cs | 95 ------------ .../Difficulty/TaikoDifficultyCalculator.cs | 99 +++++++++++- .../Difficulty/TaikoPerformanceCalculator.cs | 10 +- 9 files changed, 726 insertions(+), 107 deletions(-) create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs create mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs delete mode 100644 osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs new file mode 100644 index 0000000000..ffdf4cb82a --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.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; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing +{ + public class StaminaCheeseDetector + { + + private const int roll_min_repetitions = 12; + private const int tl_min_repetitions = 16; + + private List hitObjects; + + public void FindCheese(List difficultyHitObjects) + { + this.hitObjects = difficultyHitObjects; + findRolls(3); + findRolls(4); + findTLTap(0, true); + findTLTap(1, true); + findTLTap(0, false); + findTLTap(1, false); + } + + private void findRolls(int patternLength) + { + List history = new List(); + + int repititionStart = 0; + + for (int i = 0; i < hitObjects.Count; i++) + { + history.Add(hitObjects[i]); + if (history.Count < 2 * patternLength) continue; + if (history.Count > 2 * patternLength) history.RemoveAt(0); + + bool isRepeat = true; + for (int j = 0; j < patternLength; j++) + { + if (history[j].IsKat != history[j + patternLength].IsKat) + { + isRepeat = false; + } + } + + if (!isRepeat) + { + repititionStart = i - 2 * patternLength; + } + + int repeatedLength = i - repititionStart; + + if (repeatedLength >= roll_min_repetitions) + { + // Console.WriteLine("Found Roll Cheese.\tStart: " + repititionStart + "\tEnd: " + i); + for (int j = repititionStart; j < i; j++) + { + (hitObjects[i]).StaminaCheese = true; + } + } + + } + } + + private void findTLTap(int parity, bool kat) + { + int tl_length = -2; + for (int i = parity; i < hitObjects.Count; i += 2) + { + if (kat == hitObjects[i].IsKat) + { + tl_length += 2; + } + else + { + tl_length = -2; + } + + if (tl_length >= tl_min_repetitions) + { + // Console.WriteLine("Found TL Cheese.\tStart: " + (i - tl_length) + "\tEnd: " + i); + for (int j = i - tl_length; j < i; j++) + { + (hitObjects[i]).StaminaCheese = true; + } + } + } + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 6807142327..abad494e62 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.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.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -10,11 +11,36 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public class TaikoDifficultyHitObject : DifficultyHitObject { public readonly bool HasTypeChange; + public readonly bool HasTimingChange; + public readonly TaikoDifficultyHitObjectRhythm Rhythm; + public readonly bool IsKat; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, double clockRate) + public bool StaminaCheese = false; + + public readonly int RhythmID; + + public readonly double NoteLength; + + public readonly int n; + private int counter = 0; + + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate) : base(hitObject, lastObject, clockRate) { - HasTypeChange = (lastObject as Hit)?.Type != (hitObject as Hit)?.Type; + NoteLength = DeltaTime; + double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; + Rhythm = TaikoDifficultyHitObjectRhythm.GetClosest(NoteLength / prevLength); + RhythmID = Rhythm.ID; + HasTypeChange = lastObject is RimHit != hitObject is RimHit; + IsKat = lastObject is RimHit; + HasTimingChange = !TaikoDifficultyHitObjectRhythm.IsRepeat(RhythmID); + + n = counter; + counter++; } + + public const int CONST_RHYTHM_ID = 0; + + } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs new file mode 100644 index 0000000000..74b3d285aa --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.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; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing +{ + public class TaikoDifficultyHitObjectRhythm + { + + private static TaikoDifficultyHitObjectRhythm[] commonRhythms; + private static TaikoDifficultyHitObjectRhythm constRhythm; + private static int constRhythmID; + + public int ID = 0; + public readonly double Difficulty; + private readonly double ratio; + + private static void initialiseCommonRhythms() + { + + /* + + ALCHYRS CODE + + If (change < 0.48) Then 'sometimes gaps are slightly different due to position rounding + Return 0.65 'This number increases value of anything that more than doubles speed. Affects doubles. + ElseIf (change < 0.52) Then + Return 0.5 'speed doubling - this one affects pretty much every map other than stream maps + ElseIf change <= 0.9 Then + Return 1.0 'This number increases value of 1/4 -> 1/6 and other weird rhythms. + ElseIf change < 0.95 Then + Return 0.25 '.9 + ElseIf change > 1.95 Then + Return 0.3 'half speed or more - this affects pretty much every map + ElseIf change > 1.15 Then + Return 0.425 'in between - this affects (mostly) 1/6 -> 1/4 + ElseIf change > 1.05 Then + Return 0.15 '.9, small speed changes + + */ + + + commonRhythms = new TaikoDifficultyHitObjectRhythm[] + { + new TaikoDifficultyHitObjectRhythm(1, 1, 0.1), + new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), + new TaikoDifficultyHitObjectRhythm(1, 2, 0.5), + new TaikoDifficultyHitObjectRhythm(3, 1, 0.3), + new TaikoDifficultyHitObjectRhythm(1, 3, 0.35), + new TaikoDifficultyHitObjectRhythm(3, 2, 0.6), + new TaikoDifficultyHitObjectRhythm(2, 3, 0.4), + new TaikoDifficultyHitObjectRhythm(5, 4, 0.5), + new TaikoDifficultyHitObjectRhythm(4, 5, 0.7) + }; + + for (int i = 0; i < commonRhythms.Length; i++) + { + commonRhythms[i].ID = i; + } + + constRhythmID = 0; + constRhythm = commonRhythms[constRhythmID]; + + } + + public bool IsRepeat() + { + return ID == constRhythmID; + } + + public static bool IsRepeat(int id) + { + return id == constRhythmID; + } + + public bool IsSpeedup() + { + return ratio < 1.0; + } + + public bool IsLargeSpeedup() + { + return ratio < 0.49; + } + + private TaikoDifficultyHitObjectRhythm(double ratio, double difficulty) + { + this.ratio = ratio; + this.Difficulty = difficulty; + } + + private TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) + { + this.ratio = ((double)numerator) / ((double)denominator); + this.Difficulty = difficulty; + } + + // Code is inefficient - we are searching exhaustively through the sorted list commonRhythms + public static TaikoDifficultyHitObjectRhythm GetClosest(double ratio) + { + if (commonRhythms == null) + { + initialiseCommonRhythms(); + } + + TaikoDifficultyHitObjectRhythm closestRhythm = commonRhythms[0]; + double closestDistance = Double.MaxValue; + + foreach (TaikoDifficultyHitObjectRhythm r in commonRhythms) + { + if (Math.Abs(r.ratio - ratio) < closestDistance) + { + closestRhythm = r; + closestDistance = Math.Abs(r.ratio - ratio); + } + } + + return closestRhythm; + + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs new file mode 100644 index 0000000000..6ed826f345 --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.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.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + public class Colour : Skill + { + protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0.3; + + private ColourSwitch lastColourSwitch = ColourSwitch.None; + private int sameColourCount = 1; + + private int[] previousDonLengths = {0, 0}, previousKatLengths = {0, 0}; + private int sameTypeCount = 1; + // TODO: make this smarter (dont initialise with "Don") + private bool previousIsKat = false; + + protected override double StrainValueOf(DifficultyHitObject current) + { + return StrainValueOfNew(current); + } + + protected double StrainValueOfNew(DifficultyHitObject current) + { + + double returnVal = 0.0; + double returnMultiplier = 1.0; + + if (previousIsKat != ((TaikoDifficultyHitObject) current).IsKat) + { + returnVal = 1.5 - (1.75 / (sameTypeCount + 0.65)); + + if (previousIsKat) + { + if (sameTypeCount % 2 == previousDonLengths[0] % 2) + { + returnMultiplier *= 0.8; + } + + if (previousKatLengths[0] == sameTypeCount) + { + returnMultiplier *= 0.525; + } + + if (previousKatLengths[1] == sameTypeCount) + { + returnMultiplier *= 0.75; + } + + previousKatLengths[1] = previousKatLengths[0]; + previousKatLengths[0] = sameTypeCount; + } + else + { + if (sameTypeCount % 2 == previousKatLengths[0] % 2) + { + returnMultiplier *= 0.8; + } + + if (previousDonLengths[0] == sameTypeCount) + { + returnMultiplier *= 0.525; + } + + if (previousDonLengths[1] == sameTypeCount) + { + returnMultiplier *= 0.75; + } + + previousDonLengths[1] = previousDonLengths[0]; + previousDonLengths[0] = sameTypeCount; + } + + + sameTypeCount = 1; + previousIsKat = ((TaikoDifficultyHitObject) current).IsKat; + + } + + else + { + sameTypeCount += 1; + } + + return Math.Min(1.25, returnVal) * returnMultiplier; + } + + protected double StrainValueOfOld(DifficultyHitObject current) + { + + double addition = 0; + + // We get an extra addition if we are not a slider or spinner + if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) + { + if (hasColourChange(current)) + addition = 0.75; + } + else + { + lastColourSwitch = ColourSwitch.None; + sameColourCount = 1; + } + + return addition; + } + + + private bool hasColourChange(DifficultyHitObject current) + { + var taikoCurrent = (TaikoDifficultyHitObject) current; + + if (!taikoCurrent.HasTypeChange) + { + sameColourCount++; + return false; + } + + var oldColourSwitch = lastColourSwitch; + var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd; + + lastColourSwitch = newColourSwitch; + sameColourCount = 1; + + // We only want a bonus if the parity of the color switch changes + return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch; + } + + private enum ColourSwitch + { + None, + Even, + Odd + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs new file mode 100644 index 0000000000..b48cfc675f --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -0,0 +1,133 @@ +// Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. +// See the LICENCE file in the repository root for full licence text. + +using System; +using System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + public class Rhythm : Skill + { + + protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0; + private const double strain_decay = 0.96; + private double currentStrain = 0.0; + + private readonly List ratioObjectHistory = new List(); + private int ratioHistoryLength = 0; + private const int ratio_history_max_length = 8; + + private int rhythmLength = 0; + + // Penalty for repeated sequences of rhythm changes + private double repititionPenalty(double timeSinceRepititionMS) + { + double t = Math.Atan(timeSinceRepititionMS / 3000) / (Math.PI / 2); + return t; + } + + private double repititionPenalty(int notesSince) + { + double t = notesSince * 150; + t = Math.Atan(t / 3000) / (Math.PI / 2); + return t; + } + + // Penalty for short patterns + // Must be low to buff maps like wizodmiot + // Must not be too low for maps like inverse world + private double patternLengthPenalty(int patternLength) + { + double shortPatternPenalty = Math.Min(0.15 * patternLength, 1.0); + double longPatternPenalty = Math.Max(Math.Min(2.5 - 0.15 * patternLength, 1.0), 0.0); + return Math.Min(shortPatternPenalty, longPatternPenalty); + } + + // Penalty for notes so slow that alting is not necessary. + private double speedPenalty(double noteLengthMS) + { + if (noteLengthMS < 80) return 1; + if (noteLengthMS < 160) return Math.Max(0, 1.4 - 0.005 * noteLengthMS); + if (noteLengthMS < 300) return 0.6; + return 0.0; + } + + // Penalty for the first rhythm change in a pattern + private const double first_burst_penalty = 0.1; + private bool prevIsSpeedup = true; + + protected override double StrainValueOf(DifficultyHitObject dho) + { + currentStrain *= strain_decay; + + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject) dho; + rhythmLength += 1; + if (!currentHO.HasTimingChange) + { + return 0.0; + } + + double objectDifficulty = currentHO.Rhythm.Difficulty; + + // find repeated ratios + + ratioObjectHistory.Add(currentHO); + ratioHistoryLength += 1; + if (ratioHistoryLength > ratio_history_max_length) + { + ratioObjectHistory.RemoveAt(0); + ratioHistoryLength -= 1; + } + + for (int l = 2; l <= ratio_history_max_length / 2; l++) + { + for (int start = ratioHistoryLength - l - 1; start >= 0; start--) + { + bool samePattern = true; + for (int i = 0; i < l; i++) + { + if (ratioObjectHistory[start + i].RhythmID != ratioObjectHistory[ratioHistoryLength - l + i].RhythmID) + { + samePattern = false; + } + } + + if (samePattern) // Repitition found! + { + int notesSince = currentHO.n - ratioObjectHistory[start].n; + objectDifficulty *= repititionPenalty(notesSince); + break; + } + } + } + + + if (currentHO.Rhythm.IsSpeedup()) + { + objectDifficulty *= 1; + if (currentHO.Rhythm.IsLargeSpeedup()) objectDifficulty *= 1; + if (prevIsSpeedup) objectDifficulty *= 1; + + prevIsSpeedup = true; + } + else + { + prevIsSpeedup = false; + } + + objectDifficulty *= patternLengthPenalty(rhythmLength); + objectDifficulty *= speedPenalty(currentHO.NoteLength); + + rhythmLength = 0; + + currentStrain += objectDifficulty; + return currentStrain; + + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs new file mode 100644 index 0000000000..349f4c29fa --- /dev/null +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.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 System.Collections.Generic; +using osu.Game.Rulesets.Difficulty.Preprocessing; +using osu.Game.Rulesets.Difficulty.Skills; +using osu.Game.Rulesets.Objects; +using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; +using osu.Game.Rulesets.Taiko.Objects; + +namespace osu.Game.Rulesets.Taiko.Difficulty.Skills +{ + public class Stamina : Skill + { + + private int hand; + private int noteNumber = 0; + + protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0.4; + // i only add strain every second note so its kind of like using 0.16 + + private readonly int maxHistoryLength = 2; + private List noteDurationHistory = new List(); + + private List lastHitObjects = new List(); + + private double offhandObjectDuration = double.MaxValue; + + // Penalty for tl tap or roll + private double cheesePenalty(double last2NoteDuration) + { + if (last2NoteDuration > 125) return 1; + if (last2NoteDuration < 100) return 0.6; + + return 0.6 + (last2NoteDuration - 100) * 0.016; + } + + private double speedBonus(double last2NoteDuration) + { + // note that we are only looking at every 2nd note, so a 300bpm stream has a note duration of 100ms. + if (last2NoteDuration >= 200) return 0; + double bonus = 200 - last2NoteDuration; + bonus *= bonus; + return bonus / 100000; + } + + protected override double StrainValueOf(DifficultyHitObject current) + { + noteNumber += 1; + + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject) current; + + if (noteNumber % 2 == hand) + { + lastHitObjects.Add(currentHO); + noteDurationHistory.Add(currentHO.NoteLength + offhandObjectDuration); + + if (noteNumber == 1) + return 1; + + if (noteDurationHistory.Count > maxHistoryLength) + noteDurationHistory.RemoveAt(0); + + double shortestRecentNote = min(noteDurationHistory); + double bonus = 0; + bonus += speedBonus(shortestRecentNote); + + double objectStaminaStrain = 1 + bonus; + if (currentHO.StaminaCheese) objectStaminaStrain *= cheesePenalty(currentHO.NoteLength + offhandObjectDuration); + + return objectStaminaStrain; + } + + offhandObjectDuration = currentHO.NoteLength; + return 0; + } + + private static double min(List l) + { + double minimum = double.MaxValue; + + foreach (double d in l) + { + if (d < minimum) + minimum = d; + } + return minimum; + } + + public Stamina(bool rightHand) + { + hand = 0; + if (rightHand) + { + hand = 1; + } + } + + } +} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.cs deleted file mode 100644 index c6fe273b50..0000000000 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Strain.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 osu.Game.Rulesets.Difficulty.Preprocessing; -using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; - -namespace osu.Game.Rulesets.Taiko.Difficulty.Skills -{ - public class Strain : Skill - { - private const double rhythm_change_base_threshold = 0.2; - private const double rhythm_change_base = 2.0; - - protected override double SkillMultiplier => 1; - protected override double StrainDecayBase => 0.3; - - private ColourSwitch lastColourSwitch = ColourSwitch.None; - - private int sameColourCount = 1; - - protected override double StrainValueOf(DifficultyHitObject current) - { - double addition = 1; - - // We get an extra addition if we are not a slider or spinner - if (current.LastObject is Hit && current.BaseObject is Hit && current.DeltaTime < 1000) - { - if (hasColourChange(current)) - addition += 0.75; - - if (hasRhythmChange(current)) - addition += 1; - } - else - { - lastColourSwitch = ColourSwitch.None; - sameColourCount = 1; - } - - double additionFactor = 1; - - // Scale the addition factor linearly from 0.4 to 1 for DeltaTime from 0 to 50 - if (current.DeltaTime < 50) - additionFactor = 0.4 + 0.6 * current.DeltaTime / 50; - - return additionFactor * addition; - } - - private bool hasRhythmChange(DifficultyHitObject current) - { - // We don't want a division by zero if some random mapper decides to put two HitObjects at the same time. - if (current.DeltaTime == 0 || Previous.Count == 0 || Previous[0].DeltaTime == 0) - return false; - - double timeElapsedRatio = Math.Max(Previous[0].DeltaTime / current.DeltaTime, current.DeltaTime / Previous[0].DeltaTime); - - if (timeElapsedRatio >= 8) - return false; - - double difference = Math.Log(timeElapsedRatio, rhythm_change_base) % 1.0; - - return difference > rhythm_change_base_threshold && difference < 1 - rhythm_change_base_threshold; - } - - private bool hasColourChange(DifficultyHitObject current) - { - var taikoCurrent = (TaikoDifficultyHitObject)current; - - if (!taikoCurrent.HasTypeChange) - { - sameColourCount++; - return false; - } - - var oldColourSwitch = lastColourSwitch; - var newColourSwitch = sameColourCount % 2 == 0 ? ColourSwitch.Even : ColourSwitch.Odd; - - lastColourSwitch = newColourSwitch; - sameColourCount = 1; - - // We only want a bonus if the parity of the color switch changes - return oldColourSwitch != ColourSwitch.None && oldColourSwitch != newColourSwitch; - } - - private enum ColourSwitch - { - None, - Even, - Odd - } - } -} diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 32d49ea39c..68da0f0e02 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; @@ -19,39 +20,121 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double star_scaling_factor = 0.04125; + + private const double rhythmSkillMultiplier = 0.15; + private const double colourSkillMultiplier = 0.01; + private const double staminaSkillMultiplier = 0.02; public TaikoDifficultyCalculator(Ruleset ruleset, WorkingBeatmap beatmap) : base(ruleset, beatmap) { } + private double readingPenalty(double staminaDifficulty) + { + return Math.Max(0, 1 - staminaDifficulty / 14); + // return 1; + } + + private double norm(double p, double v1, double v2, double v3) + { + return Math.Pow( + Math.Pow(v1, p) + + Math.Pow(v2, p) + + Math.Pow(v3, p) + , 1 / p); + } + + private double rescale(double sr) + { + if (sr <= 1) return sr; + sr -= 1; + sr = 1.5 * Math.Pow(sr, 0.76); + sr += 1; + return sr; + } + + private double combinedDifficulty(Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) + { + + double staminaRating = (stamina1.DifficultyValue() + stamina2.DifficultyValue()) * staminaSkillMultiplier; + double readingPenalty = this.readingPenalty(staminaRating); + + + double difficulty = 0; + double weight = 1; + List peaks = new List(); + for (int i = 0; i < colour.StrainPeaks.Count; i++) + { + double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier * readingPenalty; + double rhythmPeak = rhythm.StrainPeaks[i] * rhythmSkillMultiplier; + double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier; + peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); + } + foreach (double strain in peaks.OrderByDescending(d => d)) + { + difficulty += strain * weight; + weight *= 0.9; + } + + return difficulty; + } + protected override DifficultyAttributes CreateDifficultyAttributes(IBeatmap beatmap, Mod[] mods, Skill[] skills, double clockRate) { if (beatmap.HitObjects.Count == 0) return new TaikoDifficultyAttributes { Mods = mods, Skills = skills }; + double staminaRating = (skills[2].DifficultyValue() + skills[3].DifficultyValue()) * staminaSkillMultiplier; + double readingPenalty = this.readingPenalty(staminaRating); + + double colourRating = skills[0].DifficultyValue() * colourSkillMultiplier * readingPenalty; + double rhythmRating = skills[1].DifficultyValue() * rhythmSkillMultiplier; + double combinedRating = combinedDifficulty(skills[0], skills[1], skills[2], skills[3]); + + // Console.WriteLine("colour\t" + colourRating); + // Console.WriteLine("rhythm\t" + rhythmRating); + // Console.WriteLine("stamina\t" + staminaRating); + double separatedRating = norm(1.5, colourRating, rhythmRating, staminaRating); + // Console.WriteLine("combinedRating\t" + combinedRating); + // Console.WriteLine("separatedRating\t" + separatedRating); + double starRating = 1.4 * separatedRating + 0.5 * combinedRating; + starRating = rescale(starRating); + HitWindows hitWindows = new TaikoHitWindows(); hitWindows.SetDifficulty(beatmap.BeatmapInfo.BaseDifficulty.OverallDifficulty); return new TaikoDifficultyAttributes { - StarRating = skills.Single().DifficultyValue() * star_scaling_factor, + StarRating = starRating, Mods = mods, // Todo: This int cast is temporary to achieve 1:1 results with osu!stable, and should be removed in the future GreatHitWindow = (int)(hitWindows.WindowFor(HitResult.Great)) / clockRate, MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; + } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { - for (int i = 1; i < beatmap.HitObjects.Count; i++) - yield return new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], clockRate); + List taikoDifficultyHitObjects = new List(); + for (int i = 2; i < beatmap.HitObjects.Count; i++) + { + taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate)); + } + new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); + for (int i = 0; i < taikoDifficultyHitObjects.Count; i++) + yield return taikoDifficultyHitObjects[i]; } - protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] { new Strain() }; + protected override Skill[] CreateSkills(IBeatmap beatmap) => new Skill[] + { + new Colour(), + new Rhythm(), + new Stamina(true), + new Stamina(false), + }; protected override Mod[] DifficultyAdjustmentMods => new Mod[] { @@ -60,5 +143,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty new TaikoModEasy(), new TaikoModHardRock(), }; + + /* + protected override DifficultyAttributes VirtualCalculate(IBeatmap beatmap, Mod[] mods, double clockRate) + => taikoCalculate(beatmap, mods, clockRate); + */ + } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs index 3a0fb64622..70249db0f6 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoPerformanceCalculator.cs @@ -31,10 +31,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 = Convert.ToInt32(Score.Statistics[HitResult.Great]); + countGood = Convert.ToInt32(Score.Statistics[HitResult.Good]); + countMeh = Convert.ToInt32(Score.Statistics[HitResult.Meh]); + countMiss = Convert.ToInt32(Score.Statistics[HitResult.Miss]); // Don't count scores made with supposedly unranked mods if (mods.Any(m => !m.Ranked)) @@ -71,7 +71,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double strainValue = Math.Pow(5.0 * Math.Max(1.0, Attributes.StarRating / 0.0075) - 4.0, 2.0) / 100000.0; // Longer maps are worth more - double lengthBonus = 1 + 0.1 * Math.Min(1.0, totalHits / 1500.0); + double lengthBonus = 1 + 0.1f * Math.Min(1.0, totalHits / 1500.0); strainValue *= lengthBonus; // Penalize misses exponentially. This mainly fixes tag4 maps and the likes until a per-hitobject solution is available From 779af48802ad135529b9e60e0d9df58871fc03f8 Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 11 May 2020 14:53:42 +0900 Subject: [PATCH 1143/6909] Resolve errors + auto-format --- .../Preprocessing/StaminaCheeseDetector.cs | 7 ++----- .../Preprocessing/TaikoDifficultyHitObject.cs | 10 +++++----- .../Difficulty/Skills/Colour.cs | 16 ++++++---------- .../Difficulty/Skills/SpeedInvariantRhythm.cs | 9 ++++----- .../Difficulty/Skills/Stamina.cs | 11 ++++------- .../Difficulty/TaikoDifficultyCalculator.cs | 9 ++++----- 6 files changed, 25 insertions(+), 37 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs index ffdf4cb82a..4f645d7e51 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/StaminaCheeseDetector.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 osu.Game.Rulesets.Difficulty.Preprocessing; namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public class StaminaCheeseDetector { - private const int roll_min_repetitions = 12; private const int tl_min_repetitions = 16; @@ -39,6 +36,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing if (history.Count > 2 * patternLength) history.RemoveAt(0); bool isRepeat = true; + for (int j = 0; j < patternLength; j++) { if (history[j].IsKat != history[j + patternLength].IsKat) @@ -62,13 +60,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing (hitObjects[i]).StaminaCheese = true; } } - } } private void findTLTap(int parity, bool kat) { int tl_length = -2; + for (int i = parity; i < hitObjects.Count; i += 2) { if (kat == hitObjects[i].IsKat) @@ -90,6 +88,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } } } - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index abad494e62..42c23a3d14 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.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.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Objects; @@ -27,12 +26,15 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate) : base(hitObject, lastObject, clockRate) { + var lastHit = lastObject as Hit; + var currentHit = hitObject as Hit; + NoteLength = DeltaTime; double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; Rhythm = TaikoDifficultyHitObjectRhythm.GetClosest(NoteLength / prevLength); RhythmID = Rhythm.ID; - HasTypeChange = lastObject is RimHit != hitObject is RimHit; - IsKat = lastObject is RimHit; + HasTypeChange = lastHit?.Type != currentHit?.Type; + IsKat = lastHit?.Type == HitType.Rim; HasTimingChange = !TaikoDifficultyHitObjectRhythm.IsRepeat(RhythmID); n = counter; @@ -40,7 +42,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } public const int CONST_RHYTHM_ID = 0; - - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 6ed826f345..8b3cc0bb8f 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -17,8 +17,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private ColourSwitch lastColourSwitch = ColourSwitch.None; private int sameColourCount = 1; - private int[] previousDonLengths = {0, 0}, previousKatLengths = {0, 0}; + private int[] previousDonLengths = { 0, 0 }, previousKatLengths = { 0, 0 }; + private int sameTypeCount = 1; + // TODO: make this smarter (dont initialise with "Don") private bool previousIsKat = false; @@ -29,11 +31,10 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected double StrainValueOfNew(DifficultyHitObject current) { - double returnVal = 0.0; double returnMultiplier = 1.0; - if (previousIsKat != ((TaikoDifficultyHitObject) current).IsKat) + if (previousIsKat != ((TaikoDifficultyHitObject)current).IsKat) { returnVal = 1.5 - (1.75 / (sameTypeCount + 0.65)); @@ -78,10 +79,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills previousDonLengths[0] = sameTypeCount; } - sameTypeCount = 1; - previousIsKat = ((TaikoDifficultyHitObject) current).IsKat; - + previousIsKat = ((TaikoDifficultyHitObject)current).IsKat; } else @@ -94,7 +93,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected double StrainValueOfOld(DifficultyHitObject current) { - double addition = 0; // We get an extra addition if we are not a slider or spinner @@ -112,10 +110,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills return addition; } - private bool hasColourChange(DifficultyHitObject current) { - var taikoCurrent = (TaikoDifficultyHitObject) current; + var taikoCurrent = (TaikoDifficultyHitObject)current; if (!taikoCurrent.HasTypeChange) { @@ -139,6 +136,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills Even, Odd } - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs index b48cfc675f..cdd1d2d5d0 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -11,7 +11,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Rhythm : Skill { - protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0; private const double strain_decay = 0.96; @@ -64,8 +63,9 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { currentStrain *= strain_decay; - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject) dho; + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)dho; rhythmLength += 1; + if (!currentHO.HasTimingChange) { return 0.0; @@ -77,6 +77,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills ratioObjectHistory.Add(currentHO); ratioHistoryLength += 1; + if (ratioHistoryLength > ratio_history_max_length) { ratioObjectHistory.RemoveAt(0); @@ -88,6 +89,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills for (int start = ratioHistoryLength - l - 1; start >= 0; start--) { bool samePattern = true; + for (int i = 0; i < l; i++) { if (ratioObjectHistory[start + i].RhythmID != ratioObjectHistory[ratioHistoryLength - l + i].RhythmID) @@ -105,7 +107,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills } } - if (currentHO.Rhythm.IsSpeedup()) { objectDifficulty *= 1; @@ -126,8 +127,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills currentStrain += objectDifficulty; return currentStrain; - } - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs index 349f4c29fa..1ecca886df 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Stamina.cs @@ -1,24 +1,20 @@ // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. // See the LICENCE file in the repository root for full licence text. -using System; -using System.Linq; using System.Collections.Generic; using osu.Game.Rulesets.Difficulty.Preprocessing; using osu.Game.Rulesets.Difficulty.Skills; -using osu.Game.Rulesets.Objects; using osu.Game.Rulesets.Taiko.Difficulty.Preprocessing; -using osu.Game.Rulesets.Taiko.Objects; namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { public class Stamina : Skill { - private int hand; private int noteNumber = 0; protected override double SkillMultiplier => 1; + protected override double StrainDecayBase => 0.4; // i only add strain every second note so its kind of like using 0.16 @@ -51,7 +47,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills { noteNumber += 1; - TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject) current; + TaikoDifficultyHitObject currentHO = (TaikoDifficultyHitObject)current; if (noteNumber % 2 == hand) { @@ -87,17 +83,18 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills if (d < minimum) minimum = d; } + return minimum; } public Stamina(bool rightHand) { hand = 0; + if (rightHand) { hand = 1; } } - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 68da0f0e02..26e92a1ea1 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -20,7 +20,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty { public class TaikoDifficultyCalculator : DifficultyCalculator { - private const double rhythmSkillMultiplier = 0.15; private const double colourSkillMultiplier = 0.01; private const double staminaSkillMultiplier = 0.02; @@ -56,14 +55,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty private double combinedDifficulty(Skill colour, Skill rhythm, Skill stamina1, Skill stamina2) { - double staminaRating = (stamina1.DifficultyValue() + stamina2.DifficultyValue()) * staminaSkillMultiplier; double readingPenalty = this.readingPenalty(staminaRating); - double difficulty = 0; double weight = 1; List peaks = new List(); + for (int i = 0; i < colour.StrainPeaks.Count; i++) { double colourPeak = colour.StrainPeaks[i] * colourSkillMultiplier * readingPenalty; @@ -71,6 +69,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty double staminaPeak = (stamina1.StrainPeaks[i] + stamina2.StrainPeaks[i]) * staminaSkillMultiplier; peaks.Add(norm(2, colourPeak, rhythmPeak, staminaPeak)); } + foreach (double strain in peaks.OrderByDescending(d => d)) { difficulty += strain * weight; @@ -113,16 +112,17 @@ namespace osu.Game.Rulesets.Taiko.Difficulty MaxCombo = beatmap.HitObjects.Count(h => h is Hit), Skills = skills }; - } protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List taikoDifficultyHitObjects = new List(); + for (int i = 2; i < beatmap.HitObjects.Count; i++) { taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate)); } + new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); for (int i = 0; i < taikoDifficultyHitObjects.Count; i++) yield return taikoDifficultyHitObjects[i]; @@ -148,6 +148,5 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override DifficultyAttributes VirtualCalculate(IBeatmap beatmap, Mod[] mods, double clockRate) => taikoCalculate(beatmap, mods, clockRate); */ - } } From b0ed39f32baafa5de158cf95b7dc025bf6ce4d6c Mon Sep 17 00:00:00 2001 From: smoogipoo Date: Mon, 11 May 2020 14:57:47 +0900 Subject: [PATCH 1144/6909] Do not use statics --- .../Preprocessing/TaikoDifficultyHitObject.cs | 6 +- .../TaikoDifficultyHitObjectRhythm.cs | 67 +++++++------------ .../Difficulty/Skills/Colour.cs | 5 +- .../Difficulty/Skills/SpeedInvariantRhythm.cs | 6 +- .../Difficulty/TaikoDifficultyCalculator.cs | 3 +- 5 files changed, 36 insertions(+), 51 deletions(-) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs index 42c23a3d14..75b1b3e268 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObject.cs @@ -23,7 +23,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing public readonly int n; private int counter = 0; - public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate) + public TaikoDifficultyHitObject(HitObject hitObject, HitObject lastObject, HitObject lastLastObject, double clockRate, TaikoDifficultyHitObjectRhythm rhythm) : base(hitObject, lastObject, clockRate) { var lastHit = lastObject as Hit; @@ -31,11 +31,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing NoteLength = DeltaTime; double prevLength = (lastObject.StartTime - lastLastObject.StartTime) / clockRate; - Rhythm = TaikoDifficultyHitObjectRhythm.GetClosest(NoteLength / prevLength); + Rhythm = rhythm.GetClosest(NoteLength / prevLength); RhythmID = Rhythm.ID; HasTypeChange = lastHit?.Type != currentHit?.Type; IsKat = lastHit?.Type == HitType.Rim; - HasTimingChange = !TaikoDifficultyHitObjectRhythm.IsRepeat(RhythmID); + HasTimingChange = !rhythm.IsRepeat(RhythmID); n = counter; counter++; diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs index 74b3d285aa..8a6f0e5bfe 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Preprocessing/TaikoDifficultyHitObjectRhythm.cs @@ -7,18 +7,36 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing { public class TaikoDifficultyHitObjectRhythm { - - private static TaikoDifficultyHitObjectRhythm[] commonRhythms; - private static TaikoDifficultyHitObjectRhythm constRhythm; - private static int constRhythmID; + private readonly TaikoDifficultyHitObjectRhythm[] commonRhythms; + private readonly TaikoDifficultyHitObjectRhythm constRhythm; + private int constRhythmID; public int ID = 0; public readonly double Difficulty; private readonly double ratio; - private static void initialiseCommonRhythms() + public bool IsRepeat() { + return ID == constRhythmID; + } + public bool IsRepeat(int id) + { + return id == constRhythmID; + } + + public bool IsSpeedup() + { + return ratio < 1.0; + } + + public bool IsLargeSpeedup() + { + return ratio < 0.49; + } + + public TaikoDifficultyHitObjectRhythm() + { /* ALCHYRS CODE @@ -40,8 +58,7 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing */ - - commonRhythms = new TaikoDifficultyHitObjectRhythm[] + commonRhythms = new[] { new TaikoDifficultyHitObjectRhythm(1, 1, 0.1), new TaikoDifficultyHitObjectRhythm(2, 1, 0.3), @@ -61,33 +78,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing constRhythmID = 0; constRhythm = commonRhythms[constRhythmID]; - - } - - public bool IsRepeat() - { - return ID == constRhythmID; - } - - public static bool IsRepeat(int id) - { - return id == constRhythmID; - } - - public bool IsSpeedup() - { - return ratio < 1.0; - } - - public bool IsLargeSpeedup() - { - return ratio < 0.49; - } - - private TaikoDifficultyHitObjectRhythm(double ratio, double difficulty) - { - this.ratio = ratio; - this.Difficulty = difficulty; } private TaikoDifficultyHitObjectRhythm(int numerator, int denominator, double difficulty) @@ -97,13 +87,8 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } // Code is inefficient - we are searching exhaustively through the sorted list commonRhythms - public static TaikoDifficultyHitObjectRhythm GetClosest(double ratio) + public TaikoDifficultyHitObjectRhythm GetClosest(double ratio) { - if (commonRhythms == null) - { - initialiseCommonRhythms(); - } - TaikoDifficultyHitObjectRhythm closestRhythm = commonRhythms[0]; double closestDistance = Double.MaxValue; @@ -117,8 +102,6 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Preprocessing } return closestRhythm; - } - } } diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs index 8b3cc0bb8f..da255dcdd7 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/Colour.cs @@ -17,12 +17,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills private ColourSwitch lastColourSwitch = ColourSwitch.None; private int sameColourCount = 1; - private int[] previousDonLengths = { 0, 0 }, previousKatLengths = { 0, 0 }; + private readonly int[] previousDonLengths = { 0, 0 }; + private readonly int[] previousKatLengths = { 0, 0 }; private int sameTypeCount = 1; // TODO: make this smarter (dont initialise with "Don") - private bool previousIsKat = false; + private bool previousIsKat; protected override double StrainValueOf(DifficultyHitObject current) { diff --git a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs index cdd1d2d5d0..2d99bac7a9 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/Skills/SpeedInvariantRhythm.cs @@ -14,13 +14,13 @@ namespace osu.Game.Rulesets.Taiko.Difficulty.Skills protected override double SkillMultiplier => 1; protected override double StrainDecayBase => 0; private const double strain_decay = 0.96; - private double currentStrain = 0.0; + private double currentStrain; private readonly List ratioObjectHistory = new List(); - private int ratioHistoryLength = 0; + private int ratioHistoryLength; private const int ratio_history_max_length = 8; - private int rhythmLength = 0; + private int rhythmLength; // Penalty for repeated sequences of rhythm changes private double repititionPenalty(double timeSinceRepititionMS) diff --git a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs index 26e92a1ea1..6e1fae01ee 100644 --- a/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs +++ b/osu.Game.Rulesets.Taiko/Difficulty/TaikoDifficultyCalculator.cs @@ -117,10 +117,11 @@ namespace osu.Game.Rulesets.Taiko.Difficulty protected override IEnumerable CreateDifficultyHitObjects(IBeatmap beatmap, double clockRate) { List taikoDifficultyHitObjects = new List(); + var rhythm = new TaikoDifficultyHitObjectRhythm(); for (int i = 2; i < beatmap.HitObjects.Count; i++) { - taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate)); + taikoDifficultyHitObjects.Add(new TaikoDifficultyHitObject(beatmap.HitObjects[i], beatmap.HitObjects[i - 1], beatmap.HitObjects[i - 2], clockRate, rhythm)); } new StaminaCheeseDetector().FindCheese(taikoDifficultyHitObjects); From 77041bdbb571a738da0c22f10948059642502bb2 Mon Sep 17 00:00:00 2001 From: Dean Herbert Date: Mon, 11 May 2020 16:19:47 +0900 Subject: [PATCH 1145/6909] 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 1146/6909] 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 1147/6909] 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 1148/6909] 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 1149/6909] 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 1150/6909] 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 1151/6909] 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 1152/6909] 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 1153/6909] 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 1154/6909] 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 1155/6909] 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 1156/6909] 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 1157/6909] 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 1158/6909] 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 1159/6909] 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 1160/6909] 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 1161/6909] 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 1162/6909] 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 1163/6909] 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 1164/6909] 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 1165/6909] 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 1166/6909] 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 1167/6909] 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 1168/6909] 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 1169/6909] 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 1170/6909] 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 1171/6909] 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 1172/6909] 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 1173/6909] 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 1174/6909] 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 1175/6909] 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 1176/6909] 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 1177/6909] 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 1178/6909] 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 1179/6909] 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 1180/6909] 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 1181/6909] 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 1182/6909] 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 1183/6909] 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 1184/6909] 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 1185/6909] 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 1186/6909] 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 1187/6909] 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 1188/6909] 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 1189/6909] 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 1190/6909] 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 1191/6909] 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 1192/6909] 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 1193/6909] 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 1194/6909] 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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Ɛ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 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 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 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 zcmV)uK$gFWP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D|D{PpK~#8N?41Q* zQ&$(q|4AivZz&au7k77e_YF358ygIpz!z>8W4POJcXy|>NZmDQd{yn9W(SV8xOZ3ICV& z38F2W4>@0?tjPG1$_fcLmE!ZnC>H8s3IoE0rz^U;!YdR3;W4`DGb@B31i}q36kX9K zvJwK}Il^CIo8E)S>UHlM!nvVyLc&bNiWO^CY*?}73)M-mNv!F)1RYBf<`h*CLz@oT46sG!Q!I7y4IKQeBB~EU$ENfukadrTo6^d z$%UI;*HX*u#uw`J7$H1Yu=<3eE8J9sZDGGqi1eTFt{|KT;T#C!Y{wTTRvdMT11t16 zohv#=bX@3skR?q59mh&6D>3|Eswc1_h&DY>IDf>udR#N5Sg4CJ1j0>A;+bB2g(8Tr zAgF?<3PpH~Ze*bd&lBE5cwhP}JVsU;iG=QJ=sAMO3f*a;WM!ezwr-)v>Gkw{dY#ZU z7hX?~3GX3%7VS^}xbPg|J%zuzg`oXK0pT3bai-%<;!74Y2{DPY3t!y%;>t=hR!Xo! z=T|s?gmCOgsPD7#kd+8l!uh|^d=Z43M4PN{L9m76`p#1<)WzrnT^Pa*npM#iTj6hd z5hhU;iXf`G&lg0OitrpN^d9sX!gmnflOFSA#e)@RR;a5@`$+r*(WD!jEHkpy=%$Zi zh4!ZxN!^>C_6x#G;!GADy^pSCCy237goZ%32#?YG(R&MZ;r03mIz29I6SQ4W(6JYe zI~{Amawah*Q6@2_Lg%Cv|Ci1cL9fdWIu`WX=osH(3j&s zxFC;-NhlWTVhG{H3!*ECp{_6rMORqqudrVbRb3&b=TH$IBkN1I2+t8-mzn4apF!V) zzK`%-buZSOL_mPVAT#JD5DFn|lL(L)ka&<#yypMvcEjmCX`A*5VooK!SPMniPeu2A zs0ad0+k%h_MR;6z4i#aW{?g;ZHbL8lf{wLtj_JJFvqI;H#F@^SH(wgCQk@myd}KH; zq%nfdD3!uj%>eyg`u=1=pJ3%VD|GDNvO>Zg$qETJHNe6#q~{y*Hy0|3h5G$MIKhJO zr59nku>^6YZ7RZkDs-djx*4bl&ljv=5?H#K1*=#PZQ56c6}q9Rn@o=j-y<)gTM&?d z2+)lq2qo<&!60EFArEAQgoO49LQ5sRpwr*Ira=mYf-9Lo zzlVPN6;`gXLdW(ME5WRgi-g=FblipGqt906 z=LqjXj| zBrDfhAuBtG6|%C)oh4Y;^c;ho$ihOgP`?`pH? zl(CWwr96q%6PaYuJ+DS1f=~)VO}<$2(~=+wf-ZE?lwer2KZtN>Z*N>EtQ$D0<5_-IICLZFI{U>)i(Bu0lLJ~jpk32}%|PC`OT z3X+)kC(2Th!h}CX!5RW4@Nz1t2}qSiv%ir{?Blh9&WciHa6uPDT@YUqE)sNd3Funr zf=CNOOk$o@AuFGXAmRjFGbGaFho%O)A}jq^A@@gGfwp7Bc?2Ik2LYJrc6fO%pBRB@3A4-ZE~Of(|n;t`XSh&X8?;-yJQmd7F`={4)xhcW?D z7+cWk4XMz6vNQ!j4`77^jp}5z$Mc0mp6Vpl^m@8B$lc+~N)1*zvy#S&Hk1XO6C~IQ zxq|hKbbliEyT}bfg8hgU`d#F^rttwmtn+3B5#F?Q3M#u!R#ORwZC%;|^PECpcS5P}158-i|gX z?N$ORM^8?O<-Y!qx=>HKnDmI47`yLZh>Z{K;mNb3P{~PCGKaG!NyLrgauI}_M42Kf zZm>ep6vE&fJx$)+h=vvEyDC@c`NQmhsA|VzExh+VPDLjg{b7du)k)PMh5*89K z*w}i*&fWvgw$>;(_HcG^h0@*~GG{-?eJUWu!w)Kv@b<;%gVTjwIC=g6t_IwK zESU^I!ypJ)8bN0zmlnDp-Xv^KSs~G;6I_cGy52O`kg2TbtYE(mQYp1B8tA)IPhlG? zG;!t`D{oj41{sUISQiVmX`us5g>HbX+!%r=3&Jc|$huZC2{9ECWg6h2=hFKT*@`mf zrVvYDVd)MVJ8w8Tm4us}J^bx0;P2!JOFK`bx|D&eYz-uN(zsDE`Nj6mIb1uo9_It@ zAtm7szt{ATz;Oc3EOMoSm8^xFgijaEH6@Wq3QVANq2EH5wT^z#cc$LJW>)U8LXC|O z_fGB{5^M6=7fFhR+PDa!oS85SqD+4UQ6?){*Nveo%H%7o!wSs;qt^@OY?i`IR|4oCrnK`?b)KNEu1^Vu(sT8rJX>jNVkT3pUR_?MwgNzh0L%jpCtjT9W zPu7ZF5usS94To^>1W^`*nJi^Nm~|~?L6iky))i$c6sKC56%uTEFM9thMVZc>nVAi& zZOg*Rximc8T+qo7=%m;sk}Q3uF#nPZ`?qKyh_>KDA?UiwDNem0q3|Vof1@B-RwZCx|sYxk!n1u~6p^ zI$*livS2Z%AN11|X2Fe-UYKcAo!l7oe0o13TWeWtZUtNW%J5(UT)KoEn%mpM+@Taw zeCi>&Y9l5fS-$+eLX;Q})$MbTzq*Hnn74?MB_WoHZ~{AfsS3K$l}Kep!4)~HE7Wn; zY+EVBTPw(NOJxGa{wk=;&Hl=z>@O9nOWDOhz3XC34T&`gH3>EkZIBpmW#u+2f>=}d z9!0m1gS$wHb+J(A4mvHFBU44e5bqWw*5tmS2~IkY^?ZO8nto2B zw=@%!#Jb2$j42lC>=6zK39}&1nF+J5Ag8yMDJ+hrR@3M?)#*LxeKQqhf+mPi_><7( zG`B1TcZVuyR=ygVR4za@{Uznrd0gJV1<&5zL{joyc5pS8 zRh|%=mw>Y?b#Q>_$UDq(rhtONg-GlITZf8pawq{WYY8ei*`ktLNtoL@v1>-mHSX&B zSqvq+j#PtMu2USl#v<81E+B-WWsjv?iy8|E}CLaa5NP#Sn7 zv8Klhy;v6ubw=nQsIPFfnCiMR$Wl%(%v1zHrb1?IC01w_7d!Y9 zRD`E<1+=SJ0ewdeMvb9?4>Y$iNCfEvlph#v5(`c2*=$hRE3e*-RDIfcNe zhmfVv{a2);uX09Y2YaWQ@U}032EM-VuUHfCtPYD(mALCAH>AeI!aVRIq5`gB)4lt6 z6?&ymHb!}cG)*eY5WYuo;xsXaVy#K6i`2xJVxiVTKVGn$NtlIb3=&%sXkAezixy&;rO#IUlfj>_)-=v=cV zBsF`%+OxFgiZKKyRMe6P@o z_4_W=^n(R~7A$AMzpQI1(_a#0@-0)LcsBAeQ*xqgg_({ixiPZJBxYg7EN9nB=vLkb zoqBdcn@`(j5NBOt*(J5=07VW<53QI|r26zV6P1%OY8&+`<3Ey-XfXEJ``A6Oj#EMi%j;zC> zE9dYu{1K#ybWgn>SlQJ;uNoat<>MK!RtYwMX-SC=!{fbku>I~8)^nl7R*Ehp)|pI< zp}|Co2%!s0cVY~M?-k03koQff=?4ndv#x7HaAlCSOi9rxTNst*tWd~YwpO!)Q)M{2 z`my6@g_!7IBquyzACzYF8Dg`NaCNVV=H4FY-MT5-eAydL?)1^Q=jM$>d_44Xym-C? zEo%-!jq$7AiRs*um{5q0{)vBY-o@LHGwfEONUHY)u>=*%cEd;QKZI3{PW;Y-gH$2$ z$vJG?x*X4f4>CJ5%>bS85m`fGQ%8r4Cw(f#3D(Ul!%{t0;`DFyhp`xyLnejRKb8(>ne5r}Rz zgnzW?VH^Am;?t|J@$p-{3OWOol6s==5h4dPs@VnIT8)InzX`vyaKN+>_t#IywrhJJ zmyxHySg@X`qzs%~%A<^{Gn$vMg^O)Th^&|mWyLzV*3J;wxql9+?xc1M?PacvWHA@73s6@(FQ5^epYU>BE(vgYeY9y zYN7D|yvssOKZXhkv=Fr+gwB!mOyW$|Gl?^m#;j15IeLBuadvjCj>bOa(Vht-y@d->h`4LJ zv0~5Ph=@4GY6j9S){C%rstZ4dl4$1V4R627Q2I56l~=i3#<66Cd>2wC*s8ln5&Gyn z{=NHv^>$COYfO0GLz$p%iYO;Dq{ z4?5a;q4DGys9H%3`@DHKa^&d7+ZZ)t79xU=unW99KI-`?5^B?;8$|=x<7=?z+Iidy zxC*(H?v3{mGc;&25Pixvg;~>K{Jz3Va>z>@-~J=cK0cAZa6XYW++3TXUp0St*Bb!y z@^xWuL?;6^E3$+zT;B8>4&B?$dcJ{~Ekr=#ypt75%0gWS%Dz`G zCieyjw6Iz-t?@=#=Nhp>e(DU?bA@spFv!0mTJ`CJMjy7s%eSn#UAz@bc5lb)2oh-O z8S$`-&efwCN_x5C<;y^nck{sy%h#a|v(JrrI1`e9?kz{)%Ka@cv-HK(Hr)|Da4!F7 z;l<|uC4^pHk8QUfASsR_3*KK!_;kU9x=f@xP2u+xeq>5H!nRMrs_R#f97l#|E)dzn z%dHvux99}-HbWs18@QX91EjQZp-=G7^67XPb_CKCaz|?*u_kv1Xt)x$ukYce@BZ(jDHu8TL)elXZ$#*{&F%L+ zemK0GT|QFOtu+YEKm8qYYigScC$0wAXR?MB7`Q$ z&`c^Jp_3ri1>y!N>OxIlmPc4|U zM=254oy%ePYt~$By3H=e_^bT0*@uZ;QMyD^^lH@!aj|D`E=Y+-tUE<3p!Bcb8?!d9 zKxq?Y4c)omC_edhCQ=e^!@;FFCU)wL$o8M|j}{&*<02s1`#-F^7lg=A@^Kc3QBtU= zUS$9VeKa4IPIRdjJt(ta1QY9}SFYzE)?$<^-4i3)^@C;IUi`j-hpxcjlk2f`&q5?8 zJiK_nnvEYYkXxg=Wwi8wiNf@)_n>YRtuivt3N@RtxEk6Q=F619A z9N67FiE9`C#i=`&A(y`3_rBjyE|DL*je|wU3H+WS3KQ$F9be&(t2eV0YbUps`0B&S zu&PXfRE3X-=XbH_kI#^lcr&vcA%a+w-&$9!1viMvD2hT&Us7;$2m^(} z(scq95*v8zorldY9^=7n>LDrN>d_v*kNq4yCU(@kpD9RTZTqKf$6?2fHC(-nUoUi> z^$)jQ7;roA8`j;tiI)Kshnr@G*n5pvi7uEh{a=XqJk~`Tg&d)~CSlpt+nI`WgNg&u zXVO}j72-}*yetumr}sm2?CJD5LP)G>fblXbWLeWn=R#gKp{xvMC5=dzEZH10yMKj^f3CxnoeR*kvsR>26~M;H|6=rn8Q6Sv zmqw&%kgi+J5g7K}A4u#ol@n?E>ql@PS&ySwq|?x~cC&XwqzN-~c=ld^(e-P< z#(~zrP0OQ@w@5DJ=ilDa7GKZ)7bV?0aZ8QqKqsj;E0p)JJ(P82w)Nbqs>vZE7K~JOc z@3%i<#oYjezoq0_bUeiHaBYi0L%xBsmUaSyBq|t>cTdNG^T)W@X_43+V_OcwtewAb zqA76VBKuP8o0VhVOrJyg_{<}0TDS@aoLV*Zhwxp6HHU3&FJd=%@Ui7hd$ z_YA~zE4VJTNU4C+_NiEU?IEJWPw;z-Hez#kb_4st&BhaDN|Z(k&r+~;D-F9c6e#>v4KR{rEiCUPta5Jj{z6d@YWh-wNYji$isqnL zXlmU^pedzje^xU1p?lQnjs8QwLXQ!RVMnjjfqwSECI4Z|p>5cI?LMT5v}%vqitbdY z9m;?H3lvt`AZQXG`G)cM^WquK*Q#P4*Rf_l{B>|SYYftSEdH6c1F!F2Koe%6H)-m~ z4N#_s2AXf3e~YtAm*a}!Efkg_n6qEm$*n$y{QepLtelCAVODj+A$aE|?JM90ATZ*;!ZsFz)3KgSJNikgA+hRnU&PeW* zF>=CX+jPu3dkV?%YS%*fGJUXN`TtO(MQN_COUN53M)dyzH=e)3>=B=0=)b-y+sdn{+e9t`jgDwuW#egIQ@tke0DG@^hfil ze{tKUgk~8kw||G#*Y4^avQ_BdXUf%bFO4?Mx{`n)EQUfJhi=tcqg(yzOu%2^R8RzN28SU! z@+QnJol&PoL-g}0jSqgG#NIde!PiQ5`o?sgh4p9Vadl6hE*SXzUuk*+#)fi5tJY7# zpI4a`l1MA{7bPg?b2a#Qlt-VkesEyD6NwLX((+H3SdPULnz0*R-CHA?)R>6gpa00$ z-c4k2k=XaoH@I|n8>^}HOJZ9U-}n9$)=fuo^}^08)>B?`@K@F=3elZ%PGU`wAFEj* zi;a@|P;L;4u{Np;#BA2AS^Oq2d-iO8tH2RNng$KYA59^5WJS{*RG$?ZEF@=!Mx;x5 zb;W{lGcaTABzXJTW-8Keo<(E+ND9=yjfjxr>_e1X{PDO(t>8ZF7o@mRJCGLJ+o!Sj z(m}juzm;B>pvZK)jPr5!EnA~%0V7-<1)fxwL)FJQcskr){w8go0v&# zP{G<34@2V-AFIw{r$`OQ%UCPatksekM(O;Jg%HOI_2Ci~jr*_fuq!2%TZx`QE?Bar zNR+9&Ko&N+KFCKx?hm6`)g~LY%mwL1nx=k}65(o%Uwr+J6r*j*0ZL^T&}8bBWvM=FtHYx(!0o;9t0ij`UcE2jSt3e{k<1bzha- zpwh>+8{)ISCTA)4R}AUT&)S!5UbR2QltGiwbogpmwjRsG!|**2 zD|YLa_bboW)Y#h;-kGC@#G+a|jHy+NJJtdLK}Ya>_s_=l5g3r#LuR9OrT#E8x8Rn_ z<00Llpm0jyCPabINu~Yyy_K`1FbcKc?w~ZJf+bC^j?S#KXGLRrN~~Nku<1DLy|Nmm zN~a6w&|&}5qxkasx%lteZYX3l1|Wj3cXy2Za6F<~Q#4;jXkaY)=p6Q%+uW)mG{#Ds))i0=Bw>ds2Zw>#f>`8lwQNo@%# zQr{poid<*L$L_{aY`Am|QDKyLtng!LT?uWg4#CvXi_rAbWw5N#$#{1bgWkJB?Pl!! zYkNy}_GEj!A4ocViw|m5=L{SURT4Jcxr~%MY5n$v6}mk;d_E7QJiEf&BF#-t8byOi zq)jwtMxnxFDH%A<#PBBY`zx?SrSc3VkUk0?wB}g zI7(O5ziaHke#L+<#>2&}1y{d+?=gItwr`%` z?1e3O5kQl-3O8mJeyCG%0A>#U9v#Q7gjHFsm1k@${p-TgN;}}}=t%eU`+-Vih6*hw zp$?>Ci^V<#aC=ek@o|>*Eh5vA^YEX~9PXs0EpfNOHxJkC9p;GA!u{ z@4QgccOfCBJ}9Xn9RNBIU0I-I6K1)UiZb1T?c3CUkXuy+s|)b|5(@ABwXy%oGm(`~HgQQ%Po?kG9~ zs*V*KqDTD!5Q)vwyFe(KfM!9{38oWkG^@I>2sQmML8Qs&Ln2M#cfuqVPL3%m#o4(D zcK`J^MtUo>M~nPiWQA$7Vq9sd)TAJJNI z%p@1t;KQob@WH~*bM#AR1~nuf&HWDkzLatVxcK4$cF$hV#c>-75^6J@l#x+M#uaLZ z1B>zRjq6a#X#l(sBC>{$Uk^-e^C5X?O)Ib;%8tpiz$J1$>_ESX~30bkEH82G~vu+GB0q7M}+S!2eeiQK>nWxCsT z3v1iv(&?KRvfHl*V4<{{PLhX~@1wvv( zzG|8!Z8WR8poN-#4D}_b6(Z*y{a7KwV<0OUGtt?p5sn?+h6**wb)7jR(*Lc$fu$Q) z;_|J1oKKqklS3Q!g!dPVGmY#>kIjjVc<}NJw=FjF!k689qfWj2MFh}~>^!^2LLe27oMTv5rtHMC?EqiATGBLpYbJv~WQKcBM*o$J7s}qt3A9uq<78 zX2JQ>RC9*doZZ8=@1I3YnAzB&a@~>e_R@|E-3~mD9;j<~LVOyt% zVeb$F;8vj_mpDr-wt}h%#1I<`%LM4gZ`?@6@&!NP?9D@LM?7~ijB3yx$|1QMv@kQSPyZBC>uC9e3o*BA_$(ka_y{7+s2 zE54eHL$?p9*E1B^qgU-tFdsUHe}?{q)5-xnrAm0fjlP`$Y1qsai+=hM4NEygYM?>C z!)q=wlU;j&?2I5-ii6AX{AJ71)N)b06%wTRa>U9swqN&j7^S`DNWNN)P#qf!pr}P&f1Z-3ZPrRG3;JWGu}X2N4{oj{0gN7xruU5vq}_j zqCPASql0nxaz2*D&jRWlRIFfuik0#xPW zkT~V<(0rE$l?LIPRg2-8&D;PQFWa@|J{Ip@#4Jm-n0lA!f=*+9g4{OmlM0+$_JPEf z#xsG)h-?IC9q$rqz52Nz->V_|R42NaM$^4$8gCj3Y8}2_u zL|DNpH7zo5<8%GcQ?KFq!#`MAY!FVP9#bpITF`p>VEJoY#r2@1Svz^z89|`y;n5y#?be7Y&R61?#EzdIgvF4?fj_+qdvAT-rT{-WjnZSQ$qs%;8rxI_n3yBAAN~MXZ}P)$Pp&c>H$wm zGSso=2N*SaCgQt};r1C4+u&z7`;um87A$U_9Wc6iN7xn-e{?bWVP)@yhB~>wBg4+& z#oet%$EsGCVL+`~%&JZka!Fc7`H9G?CaZ}a%(qo-DxnsvX1UE46fIJFr7J10te4Wy<##lMG6Ac@v+ zFcIW~4t$Y}--gUX_dcKE_me9S6>*Fyw|Z$HbIWpQ=syTw51WMw6ILR=wA$BaXaaVx zL~=sGE|zO)QyJsx)`ex$%(EO8Q*nhh)mN{V3L> zZIRxpruakZ+6z`Sxd-xPRhvSn>3q{Z*h!^D6@VIkonSlIus%z&%X81wH^pHSDL@Kd*%%AAd^%Kn6 zy96O22e}KFp!FPUmFthmAAgVfQ&uCcLDnnGtDJL zrVN;j=HIVDY*QM0H7?0}S0k2RV4S+OJh`~Sq(ig_2IJGXIV?R`&i)i=Xf=kavW zm|9NX->X)4xVSY-Z&i~aO552m>LPcvNiY`aN@xkqeXwjCjx(r zT!fFOev4~QwnC9gtI;z=Ht;Oj6<>E8g^p8KBDU=?qclpHvG?*RcJef9#hDx%hq@S8 zqdv^u!)$Z!8+C?!4Y3(XgPPZ`!f?y{B^2jOzesalkW_1fhQ1}z=KvwiqfkBF2{3uK zs*Qcu)B6iC)8wzA0mQnjXhIVzQhw-8AETsC_HoZwPKRQ}!3`QWK%?>OJ z=aG8g2X@aFd}wHP&}_?Q73#oJQVA9onjtq5Wt!1O?hoBiy*yz|p%$!Z8ub(g5XqXR zp?l7nmROa*$jar=Ya;a$vW6_#spF<2Dd8$tcW`QfW+T7nwhf8Hm4mpy@f$3-a0T~o zZDE#e1WK3fgE`%L;>W!|=P4g`c07C}#q{yh@Ym&iNKJXh?W0DaTfIT}?4!vrA2A;( z?&K*lEywq*XE%p|r)1^=XZMz9UcC#qLP#<1J|sfT{I!u@N_jXgT#Chm#|r%YG5{|& zk4JL!TmI3)iFuQu@GV&dw4kvLWJwD%6{%~O=Rrkd2{lO?eIYUfg)d7p5NXXr%@=K0 zr7Z@{{VqoV+%S6&E(aV@S40jN*04#A$^UbMd2#~GHhhm2J67VzvlnSqZ~XgKiCq-i*O_W&~(!dbV;9d)V>#5X;Mq4bzlNUTvXp=job(Flmp zu9|IfM7HQ%sT9oW_u+RG^FC0mX?JeYmO$w1gLrgzD<0q5isffFVe!H-NI12k&}Ofu z#gPY8@MBtLt1H%|Y&23z?vH$B)ib703j>JcOebraCbv*11!qk=xzxcIy#~O$Oq!WJ z83>P1VE)E`xmK1aP1?`X7KUpfjPZ~Vd3soX!~KE z+#gjvIK1KnHa`ziCy`=^MCNGgZH1_|Be;4&5wmk8Ql*din(1*X(G=zD8g>eNF@+yV znOg90p)~ZFph`wW_!0biU_D-K9tTw+U2oB`-e5SnHh{S{IT3A8>?{d2xj#tkNEZu* zP}7&twWew0lLWdaE1Zu;#JaTotJg=rsoklXB{bn<<+$Gw6ncuQQ;R#OWjllTYpueA zAYC&7zinNL`_Ff9D~(hB>V9=Tz@NYUijgb6fHQr2Bas*!h!keY3n&%AxF?fwE6fxm zryroU>j3v~%;do?C(*Pnl(>Z*7J7ePSKji50xOX)IE6<+M_)8m> zZG`xC!@2FeLPF?zXfbxLoQ^-wZ{d;|(P8i`(HXxCo`i*am!U-%tM z^+ZUcBlx|AhbDE}!IBw#I#xB&h7}5?5MruxJyX%pLQNk|-;i1j`ci^5O*3`4Z*FPn zhVhLX!oNoLna^lht+msCMP#IUSu7ie`e@!he_diq*O|O-Eav^U1J9Th%`Gs%E{x9A zhvKhQ%katXAHyY0K5*mF?c1K{=I9K27i|n_;LDq^d!%0ZydVhQzX5XDbG~MJDtVWK zMg4w!t(f->MTNGow#|9$iQI*!?(e{V|4c?|s&N-`&Mjv3`=XRbHC;b7=^lw4MSlqC zPjfw}XlS9P_akeX!tn%anv_WMrK+%#F?ilszLqsd|G9xJ5AHDKmqEqevS+1gNT}VK ze>C^l-8_!#+ke2?^T(JKt+rZi?Hgj+;MrLD-%_+_@4+n+Ybxlo#{N4OLmPHx&70a! zFPFZ+u4m66RuoM9^10guH^GJ0r|ez6baA9d7LY?s#>}83c6j31tsi5qLX?FHacrCR zrLxBgZVstoF!!klXKVeIRH7LFa}N*T=K62B-(Fat6?}SEtqNMmSO?NY%4$tkawmPQt`4o|D^5^2oEnd}kViJQOlLfQA+OZ6=*d7u~ci2l@;AHEFDka=dyK;4yH|P%wi!_2!=sC3H zbDVgvp08;Un|nYh594cEXrjy)ZHB?O&oqNuoq0tXABK6q4n=hMDacYa)0rvf#Zp!- zv+|G?I*Ey_=rt&1$lsQkHBAmj&1W>JRtNol$$1jPwpm+njT32F0%%aZ53yy>3UnG$up&+PdI4X}`4#pK>UHF#iFa|~?nQ3fl!WYF zUtl6lH1u$Sq*$agAiuT~^XCo6gaf=j_G6ootxNlDib8hr$hUvI;% z>&r1?;}rZfy*swd`vkEUw?L*W*i^mxQo9u8GRkN$dX#JcM<-gAFfB9)zw*pZL|rO8 zS@$gMJe~?DzYD&4Ggqpq)Ngg%dH0zLj)8uTT#3Y;)x_UImkh+cF z=#qU4PfK5IJaz=hynCXYUn|Ia=4}R|`>r4H$BoAbeRGi2RIqn$il6(8!s7pypn|`x zUj2^XaA5z63rJQct}_blJJ?}dhhAI~BH}{8bJ*TF$8DR6E7wl)Tc*d%(g$@aH`Ko$ z_#Q!SF#qyp;p*9*OKVH<_O#quj;P|07<&oVUi^z6H;l);X}xfE<#;6BImEe83M)?D z{_J|lm{VTNMDKsSyTQhWvLdI2!bz9xSdN%Nkdl|6!`rlOGxnW5U~ZBCR&1f8Ndduh#JO)exb0y4xqA&HHrdajTsvt4zWV!jE?K>W zRYiQ)V<@6JkLMrNAJ-!*@Yj{|2ztItU3cq%Uq2m+{@?Y=RYb?RYrwpzqcEh)2=pA& z1eST81e!f}@5|Az=U}{kyPws>7*u~a+`cefVwzQ-H-s#uU{)A+W9Mevtbbu`mjAHJ zcah}K*O14(Vpg#fA|~7-xeTc)5n`F*%4I1qdwT~7Nx^t5ljCi89HOIwAWeSE%*~8h z#3;no%e6YXRjv&8mP29VUfN*RnjuJh^8^e27>oGm^ZXt)K4p4h(Kq7}t#HH_-wt8= zKt1lXxVyB&nCa_aYh~EQGxCCzovQ^i`XVyoIAk)-c_4Fd2`kiPrtm77Zb>0+Il4a# zagIpc(_7P&(}VtUB3-IXd-SN+I!BQX2~%RtarVRb6-DY+sEwHRqq*(eVDtC}w%@y`DE)}UUs8+=sBWl;=Ts?H&4nBJV zNff!UOu(%RtV>;RlUy2l`$AGI(ph3%!Uy&h8pFlE6>O`vhgp@Du#J8Ji-7$&e|-270Z}(x@@dcPSe<*e?7>T&^{~$$GI9ww%py*^r z!4YW@DMj#ZSQph>RKj+s5YZ@ty1}70iuF4O9Jk%?p)PR~S*S>mxqo86vk$!{*<=z}!RI z@yF>+c>Hi1Vk1vOA$!d~Q(qKFio1r(&o<-7&6BZkT3=jU`wbEU?{J@0P*^ZSsJxSA z%7q3iCe$K6Ho41}12L@eAdRa~m)n8+@pS$7g(4E9X|pa6n>nSAsiw8Z1V1(Dre3CF ze(w)D9D+4XA1+waw9S3Ha{isrsYw@RnP&e{&jJ)!ed;6^&`w{gk-tBp+kdKlFqfPP z2vA3NFxqq+ji%iyBCokRJXqsiw?$W2u;uO98)s$m8A&K>GLV?HZD^Wh} z>}cGk1U{2A_O>E?rUY*de*@tj1 z*^z&RVW5Gz`$0SK%g)(YFnbU#ubF`4Cl~aKAu|w84kckONt-p&LY&4bA||zX{BqJ)7Us z3;J|w4X9d6|_M6I*nl6 zcLr4HPL93=o!W<(geTl~CC930+oLWd=^EbL;9$poP>2|ZZ(K)Y=q22Vx5vf*PT=Y}oVee6FxO9()$Z9IJCj_5u06F9l&EJ!|8eQ;^V zNxTTV%d7(hLL$vjseOO`QNxqW8iK2jcJeh7BeKSjHiKbPI^#(G`%IE74zIV*#;U!m z@F4II6VEqnui8HJ^zDjvZ7ZOZuQlx1bty}=g2bjgoLnly$+a?^U8}&^jTQFS$+detX^q{J7HMBbsir&krY$?l9eb{C}|0;$ePuO;K)zZ2dOAI#A#|I zDRBcv38!apyF}6)F%s-I5MMxjNV)P13*}8-q=y369i!WQXMP%o3{4;V-(Bg5} zdsF?nwMu`0z2{fMHJ<~)p%K6bZAReovn~9dED+hi+O86u9lg=e!wmxle~2!hG(lQ7 zRrdV#^}iUmat0S8N`d~9#%)AO_3W1^%PVJRkHGDhTlt!4v2$vKZ>Md7wJi;86hm$f z`MK5Dc>Xk=ygbTo4)waAy3o3r?b^41jkOYPNh)|M9pLO-1`-!nnA_OF(#{@Mj*hT& zw1>5;3+z3dkPsP<#NZGlg+*}rG34>_kR`+-AwCh8gI?iocr+psBaoEv5{gu66tX~M z4|k8I7*Me+yqb@IrIF&^6M_SJeU?@>eJ zA7k2ofl|G`D=?`hU-{2pw?ka~dEMv_5@`A{v{DF-;!}%6zFMsqrz5XIO;02nl}vbw zm!>tVDDem-5#}VRa`gdNJbDD$jHVRSS@Z0L0w4DoiQ7+TRdF%8HXev_BbMgb?LoNj zorhU_mV(wCbaZZs0Hi=$ze9)jqGq+cS?2<@3i;XHPcZQFL21%}(kj_is@8&ojR=0M z1LG&Lfv0Chv>ZDKWh%R}W6D1<7X-ahpjGpM2n{>L)mv8{imKoK!EGB7acVOD`ldJH zV@(^gEl{&ee+-(u4(3^2p6?F{FK^-D>1Eh^|2|UUGl>Ugy^oUKZ85w`DR`Bs2Crr< z;Z>uw!2~Gm$y>ZWdj|LJ-NvDZFA)@VH|q&dVhOiKV2^TTP>~6?MHy|kqL?*Up?{Q`6UoyXTQa%S-cOy9B$DG7J@J!-7&>R{6N<#6($iP8nbj^D=O;?uQ| zN;P8&v_X*q)DqDO($qSVWu2vmDX%_idL1b#ja-sroML6#vBIe#t=!VJv^`pVn!QL< z8*$>d^>`4ZzA$Z_>Z2Yzp!pGLBB^XsI5?|MOaikKZf&C2^?zd0mPHu8Yz)d)$(u+MUM0=YvAlK_^sB*7Ac-;jD(JMLO+vgn z6w=hF*rn2@lK*=z(sFihYx6v8y}UD1k+!m{iH{l$#QZ7K(R1EB)c;~AybUPQ zghxd$)EPMlA1|DPfB)Nrf7cwrpvDv6>e3D(vvl22rIgFv{@2BI_+|BETwOC3sm~0T z^^qb!tXX5AOHS*lwix#j#y9T@GYg7k(2|^R16%%@gJiv8D~v;nmZTXHT|=0@4aFdm zpPE8k$mq>hi}NDX^l2o>u<}2vz+svsOeV* z(H(LhH=UluYE4kuP2HWQg<>DyJO^1m^BmDJc6I=!t^ObTt4+muH#iiH+IpdC1v4%} zM3RqzO966|bnM=Z%a2XO$6SZ}*gxF1A$fM^JU1P=Ah5Qp3j5kU_}cr4G$|6d*N(-{ z`_>^Sa5rnc(j~-aRaTHT#TQ6@Vwl8UHWq1x+FX=#i{A66X&6$EH7wj*1TD`( z4&nLcU$S=s8xiNa9bsczU3Zou(UcYXqC%J+J(y*4lvkl9Iii+;K8SqOWIA&nVrFKE z2IXzhdFl|VX3Nn(4&ml2viw<-VqFRCE9O1YBRw5Tm1Mt;E-Dp{heqO6V7i+i50rFg zT?eNBi{{MLi0Jj88`w2#L)OlvJkoMF>)N`ieSnlf@Zc@Ok6_;jFSvZZ-cDs8DV4>F z9`87Gt;F9wjFt1pVDq(&kS1oF5o%>u1D`bi6sxzbL#;s_v%Z6kjD@8bjXoWOh1*tR z?cAmKs?8X9cywYHrN%B}sJMlh_uXEPpVv*p?Z3Z-?6rC@Kd;zZrX8>})8hb8)YOol zenso*9ofCFqa&;bu=&y+Bt;fXbdx!g*$QsuSp%Z^5hTh&-VY&6FIy8YFG5YPp`$<_ zAhfjGNl zGq;^zTv%tN!+>fS-TDJ|u{v=6>IDb>!{a+S1WD%-Cw3q7s6g(2^(lH8`2e=ThP%*g z?p;J!f?>aNZfIS`mkVEd-=MI&xcxt3{y#tCRq#G8+%7$y9-T4w^BEYmv9J_-E5UnAv{@{5-p-vtCt5h`xvomo{L*s&5dyZ5m|Jk^E!1!_h{a`d7eA z&)J3!uGSd&+1DuNm)1ooO}c|!fBuM+yeOUlXj`Wt>y@YRztD*lgqm8VY~3HZ{o2$d zkn)oX3vLezN8sK^#Qv2nTOLhEFcHgvUKg27j-OV zV(c|sxOa(5pK1u~Szlt@^2uoCPZxt2&x1~2cpG>`rG+)d7Lv?KSe z7Bf9u9|Gl@R>FiX-C^rUfoNLpy*`4_{gi6M^td!101HWJ-C2sHrBzrVp%(nqS$fj* zD%AAGG{QiRV9GzreY(WP16@5FP^&F{e%8>8y&W6=LP~;q;KaK`L&SB=IsIvFP@0*e zjgN5gC~!0=5+^s(vRj6sB>UxGfA=j)dUaujL>$)MzJn7RZ}5*AhBm`n!q1J?1SV8C z{nT(4bwYFyWGPvU>Khq*C-$5CGp4M3myt#v66&n@fm%fLyfRJL!LmBcB$@Xecj`0?O!to!{Fq@Mqqe;{X^oXG~% zVwKB|Mg30);Pd(|n8mNn8$v6?{C##G5+hA-pjcQt!@$;w;v#T8U@_uDNN=j&vV?)+^wA?LbiM8`!@z z%;K|`Bjed`D46^nK8~)ia7ue4zRM&=yvCjFUt{f+Q)xmwHHd7`q3TEYWy1V&2S^HfWB+%4#AU!2_GKqJZCkW)` zs-QQZ!$pcpZ%)m=Cj5a+`r5vpXg{$RzbA7jB=7k0KM@y8vEJ+(I@X1GJqo`zJc+D( zU8$tHi%kjEw>%3*z+>&o-}!|y@qM`Hb4+Mh3liJPIR5lH4*a?m3IkpR^*VKcm94sm zk`Q|z=GP4nY8W1bz&M%=RWLO6bb;A>;-5|oyoaj?XJOmLQ{05E^iW=xzI8sr%)ghw zCi`KXLXVxZHCoP?gx?n}!Ibu+;O5bR>nR9O=;7uo+p*%G*^nMy$R5l3Mse~f9l>O6+HR-QlQIoFH!7`Xo3;t;u zK%|Hdu2W(u@kN)i{;;>p{w?3WmSWTWC)`LZQPraqk}J_7<;I~$^Hv)F*~>Tgaca>9 zZre~;F=O}B)j#3W`VC=c?~VHpjv3Q2lWPH!iwMWm5-*d;ZfQdz2?ru zl3$l%XycKvanMN-pi0K;puPCv@IN@SW;A3kGItkR_>|?h)YwpPbsq4m;f?Qyj!qL_ zO*4L19p8tP$c#&S8I#JbnL#SDgBgXVs3A=yaiS0!L8vno(A)?$J(v36g1Jr>wZ?Pg z;ZYNfKmC}mWzF%0Tks}K9co6_M7tWs9aKtaH5g@m)zhyj&-LaTR}uWyu#3k!JDXwh z`nmY3K{Z&a*kLlDwP?#uUoCfP7u7tDiO=`qx8^ZH+;riBz}}%8l-~9E+Ph0iY9cP| z{Rt-i{`>o?b&+gC|k!HqyJcd-zLmKlgfkA_^;_++;?p&7H^z` zq=R!=X+zwkBFVD8`!iKJY5KQ+zj z79u~${gG+cAvZ!zZy@-mX&{jnuH`T{w_w(^FZ}Ce?{}nW*xSyY;$U+( ze^b6AW23Uq>0m_Mo!wyWPbqodT_|VA)wSQ^=Yj* zEh5w?(;HJ4FNBS4ZtSF@jg7rI+RXkIKTn*39<}>%i%bioraZx}Ydf)K%P&YdxrCEs zUHe(YWJjgWJeOyjoXjxkgQ1YvYA0DqlkVZ*jzxy-0Ov@>5`OIWq|ss0P!d5Q@*`6x zSYCvh)SrY}Bg^GIs-opkTDLK4QrVF|@%JXgMX5vN$X8jlnQ5jF%dH(SuudH=UYmFv za31@9SPZ2B2mOo*%?>j&Q(GZ2=^H3g@;6Z>D4}2m62)eoDChicXUdDewhOB+9ONP$ zGQ-ciJ7)d#Gh96BzI^}iZ&L$bEn9`@-ABU3tu4Qop-RTffL)k#cqd-`I|lNztN91m zbz^P~b5Z{NL-huCMmK+QyJ=fWT8SYxcvmk}aA1fkAg$>=N8 z^gLbvG$|g}ESOtZBxPC5^0iHrec(@!7uU|q^JRd`?WT(P5?{T(fDT#=YWoGFaA1gawh}|mjwRaC` zax|9hUZ-BOFcaK8I^ySVC&RmH#`XT*C&=B?ary+zo4E*0D}I&B?z~T}#*QD?0NpN&gFLy_f8+OyazfG10#I<1uW~BP)`l$)>Qmd@3|k^}sAAf*gXF93*R-|9 zSN-PADeRrQ7z$IJp*(QsQYd~MIt~jjU+2OL2|LHiP&lQ{5SAOPqJxo?6vNj{i@m)& zl;yPvt==W5bGC2U6huZIQ}0NJjeULmIN}qOZ&i!m^L|5eQLRN8%v|#)#&?vh;+H<}2L+9j zOPPdP@KdKjAE8d~_MqlZ6U|=Vw+xze)~4jl3=+c~M^7_LR&BwOpU9=#2yWYyB(@)p z>VA~8ilK_f-}fKl#5P*By>LNO-8cU5AEr;9%DT}fkQk$0qKw={AJ(XV__}#teo2%n zWtN|5!;%C_+So$kO3P}#dxW1{k1H<@^0l;(aWJuM57hdg@J)tQ_5WcK=8T*Q zuadNaiy%o_&oR0E%&kaz3`6(tCd1QRJ2RHnj9hnpI}bk@%UlOdK|m9M-w6Az`AQA8n%aI@H{9ad7l}fNJ>Gf zv|wC5)m&`#?!px%q~Rf0ePoNK*P{!CgmtUl30-GQ;`bDj9&$-E9^M6WCQnDxYW>qC zdSZ_=p%&Ycju{3AHye!a(k0F3ghic1z|Q%6&3MrDU#SEWYW7de^oWP_H2r{Lnp;Jm z`-YxFLQU#GM~jAQIkk6juMXdOnkc<2xw?KIq9gcJj$%u+Zc5WS3x??C1JSI4x|>D* z^1V-VZY4fr3%&z_A1r}yA>z#ev&Cv#SG&02#nsc{2X4_%P&srKQa zq1^kL9FaXLxa2U9QA9bv?FYofoaSq3Deu!2qksN|3sWm5dn&cAhABVJ!|=vESg%{1 zjEpcdw}ef)2{8Gk)%X#p?9-aBF_bcFzIKuuXD~LE{m6AG(w+E12s!=f2G5gFlNqk- zpC-pQ=R+y$<%d=yvL6tn>D0$=-2e^X5w^Bow}X8rd!0t0qKDG%owG+M}XVvA1b+~+Gq^!tJHv*(xBL23)#)JSan zU}2TkQhFDVUfqeKPpF~K802N=)ELtzd<9o;!}wR<2RzESV&uX__^w53xO-9DvxrN$ zU!tC2CrQy*-h|J_q?v>hAA14UHvY`ljEALXMP|)Ar}tCSpdDQ(B-9xO6mt@4dM90> zCX+~W)phVJ2`6Vxv$N#k{bXExtDUY~$EPY(+R&5&A+~W(xLONIUz9ld>?sb+HSJ2= zH2G)q{8Q-JVi+d>I}c9+c4`_kE4y0gQ?EaU^%;$n!3&XKz(qtOBMrZ#mp<6qc|v7p zz?ndW0Fg?8%_sLjE`7o8(Ly8pUw0mW%I#@U{$g^2xx^fuf0=`+Et{cgtpRYhuz|fx z4*u0VP@`KzG%u@TRkOzS@Z-x!c>A2M8wYXe>M*x(Pw%JJ&G|t>okk>c5^8#!UPMAo zW;yv$xHm8}w??^gb#sjNxUuOVBBRt9!)VHQqZS3{_RytldF`s+v^vGPD~I`-agoV^ zvj>ClLGv$Imv}sGz1RkYOr3U-d}h_l^~2W##=`6Kl}M|$@nE*~d# ze(-mNaZovV!P1gh)Z91FK)R}HQBz&>1bu~?{L>`VLULqgN&BNppDuhYYpz~B z54lX;LOD6sg|wdOVyCksCYVpN%ArDZf^oa$G`Z&K(`1bAG7Y_l4aTwieRLbQ50$q0SJZ zr>{`ci|I`$T!MRpDjua!tx@)o8*xk%j=xlQ=ZS_sB@MPFVt#NvvI37nwD~fe?3-cH z?iqZ|@R0v|@t5n+vPB=PKevcm8Ck&0!WSLueTAV5c0<+Ub1pK)n4}btP&-+RU|E9C z0{9M)a$*sl2W56Mi!r3*FgUu>EmMpkaN(AuGY(|3_WEor9Gv;ET}DFmW!%oiA_uvJ zR^E5ERtt5yi7(`_5GK9QfK(b4&kho0U5lCuN87TMxy1I_aqge92#Hbq?`ZZ>ld=VC zQHhiaIBcDPMaK^Cxq(ISE72WSuIz$~vvF>$`)8hDWV_EW`~*w3R5wi>;_23k||w_a8tQQ_#i5p*eh+cO04!r}tn7+Lbnx`?x zOE+krqksCz`74knKIFD-?ER5gTI&}!HIkSx#I7EP`KJydCj2D(b=GKH;R75$w*lUM z^z)2EqRaySJ-nDETb-qC6%49B6g}r|hRDUVxi4g?1z=ILV_h;2iQfw=3FnsJ)$8+o zO^b@zG+$2q`JKuw{l3t;|1fxXYR80PA}%1{X8srH$cmDl>hYR%1B#TIgT#n5S8t)F z$4Hn--AU!in&zHwVd;zt?Xyq9Bv$|zo|11`JxANk!4pZ<$rogLY#v;}nbqH6!{vQk z9$c}x2m00?jBTe^prp5P@2cv}%b>2Wc9Birvc1r?b4`@$H-X<{TI5R8ThwOe?ywQj zB~6Gv!npZ8XPX*Rz*Mj?zxlGbBEx z)N$eI1{CSTscWAri<%xIw}&9q8b^_pWeJq@(iqp7lAHoOh|=bisO#ZmbpLB+IGk9A zbsOg4>iuop%rTmkIHvg^tT?s^&Q8X+@~m0g_(`8GoE1$ZC8>o{WnM65mps+& zfy9E`Z$%r5ln%SG4zFI43o9M$2LEBCTJ?d8ht`;k3_pj+GukYEhQ!XZ z9J^*To@4Q%Q=Z#46(W@q zR{ze#f}JZ77IKK~QNqr-F{ZWajURT;fermC6VZEWA2cghliQ9AIffT6*#W(>o2#1| zmD2Q?^CS`nh^>qMtVHR}ec16LBGakj<-B^JdbM{uh3mUcm7cB8zBH{a$jhQC8C!4N zLW)Q|u4G8eJ$;!0qrP5r1{A5CC!rQBY7%O$?axlA=}9EiLd1tg{=FPZu$zysWWl2& zXOR>i#BJNyR)nmiTBu9`4H}yLGZ9NJ9M+_qbarop>8(3r{KjdpG_f%&VV^g0&_LKa zQPM#b?%q0%z|cEfV`6gBjxw7BGa4whFWSkNl`-S&$(Smms~oov#@Vk(DDXEcgLMt)E&~s7ZUtFy?O9tnONR0X<1qs5QQh zGVUc|si|eot49x^NL9y}y1IHH#fS^orpIpURII#ulZmu?nVk~eT`;|EYkaZ(8!iD% zcDxA#Vskm(mMRCLW7VFL=64tf%A#j&!H4Lj zbqP~woUT=!YoX?zO99&crE<=Ecah5}p;oRkPFCD1E2h9>?>sEKb{EO<6ss&ksdByW zU90B!Z0#hyjLDNrcH+;GKO!+IEt%;>dBgEdpT?sc2NLPDNiHFv^ZKo<6duNejsSVt?8RkocrM2|8y>H4X& z6>8c~FQO;Yi!`g&ip?dcShXTw%N$uM@HARGRk?-(X}$3w(cAgrF=8XBdn!VO%Kh+l zvnKfDPyJ^$c{i+we_uSqvY}riG&Ji~1*GgZo;&gye~cK5eJ@TRB{m8+IsD8f-78~w zjT-E$sUsksy*z}7%_ip#NZfhBNEf0<>P-)j_R`JD4Jc+O z)THKQev?p>InLeT78V|GuT+6Hvm`n)717D+CGtgLSGd{ded(y&V0B^*?mkqrDG z!WVU$V8pU9TwPykG%JOsPPSNf;{pB}Jq6+F@z6BTjLS{ax8j#yW?|#wP*jnbq2sLa zIl6<$2lnCrCc?{;Pg+ic6}L~r_SsE!-2{krmf<%QW@ch`F>=>i(L<%gv(zVe{w%Wv zCdBC9sv~z^iz&R2z{Uq&K|<^^Y`=XHcTNYWAIJhXH!}>XQi*97??Z+Rd!M|3RAkuY zpq#CEq2}ys4MG41=`Y=^+=Dq;)Pn0nlL$j%?Fqly*{|sG^wKj(lhl0-OY4$QnkX61 z-Wzw-iS|Vf7+S47`px~62?YP3p13#yZTpRYt*rxozj7OEKADOL9q+`W$EjE|Vg}|N z+K0;zpP;M16MB6;6Qz9h6TnKfTrsv;Q`R$4XOfd9-pBFHOSx@RAX2xtCPmB%buLdt zAwhoo5bnjBWx5Wcl}%0bm{dFqv9Kd&z26&&;ara>G4?XfZ<~k29IR^ZR^8#^t{qo< z9q|~-+ei4CVQ{d~&MnZnKLjJ03Yh?$MrS9~^h7ehNvO%H=CHAFgR@gsFH;lp_yrUS zwYphZyCTU;o1My-1VyW-^x8RAL6zE#a||KO3_4zIht$PqH7X%FDHwClp1@Co#$eCv zf3afNJWL!q8b6;ogRrpI7*W46Mos?-H5z&9-5}8O2K+Jx6@9f6hHkt(1Dn&@HR6nk zxBy%qW@0f^Mbx5B4m^p6_tnu$>8a;e4-%{F?}cIto>ylt<8Fws%$5qLLc;L;q_BQ# z=2UE14fVaXo&Z_WLp;58oUa)Mdt0qQ=?o~6k_ZB=*_qWETRbhD({lhxfLs5;%%m|OojokF zG0G?sBH+~<_6BmU?qO{OrOEuF>PQWFstj=v`CDnbR&87KZQ2=LrD&B2CFIfolrC2f zU$yFjA3psO<2L;QA3tr^CAajQ^)Z^1ugBMbz~GZm?OepyjEAMA>BHpY6u(#i@!R<& z_3Cz9dmPKIWf{LmO9_VtC|B#<$p-i?v-jE+q^4-o8Y@x*aWWkVuxbo@H8znro zgNk91w~+eiEMLn%4j#hdB^e<7)h(F{wII@hP?JTi5$e)zZrR>*wDi{NNbLbAWluV9 z8YB|+4^NRMA}%1~svEh1e&fK|eep?UKh0ve4pKApo&N{=ec1?B`TSXqcEGSc{b1`z zlVBLC1pIsPG|ZFZ)O90aXY0yunT}Msf?Yeg>~<6aV&7gtSa9YG1N3Oum)|O;uo3Yz z2zOWigWcbLhwq1u!pSEY-5if!$K&yq?A?u}>y<%UA97Eq1&?~ZPcI$dwhe)Wiw`J~ zS0>QSLLW*cN1+x(nnoZw$d6K`9EJI14NdxZo1hIS_OsW#+~_#l(DY9rA?5)>BcDK# zPfr4g@u2xL(6L5sc2ulzE9fYW{kDSZH5h{K6FZ@MC4Y9Z)b5C=m~)VApT}(*3kQ4C z3$;R?$}XzRF15moG&%zRMI>c9UED%a5g$w(!qEK!*wPab_6Sj7*)O?8##$}Es<5)tTGbbVf*|LlGyj-- zmWJ3|?MF@*`9Us-G-93E-9!86iGoO%WQ9W{wnK>=a!Qj$9i1$670uwphoG2(QZDMR zN;lW>2q{7|0|T3F_&I&~;6h%y^L!<8_HY$_&cCiE> z^yto4iWYRaUSGZ(tBLhoZ*QGxxCZ>(4Rw;2z95c@%fWKz!+F8{pNe>{sYnVLMB+X(o-l9%BJCVYnNys#obyh+xSk&|`nzU!u z)}HK(XCF;L?uJBJGPf-j+d?VHkjl?!sG9eHt(`g_a8%d@Jh^%qN&~qmMy&V}V;aC~)DLWS##|!N3>!JC_MN_B)??>YAYnKq87>Ur^_xZg=gQzGaj?G$#$#Z|i z(o=`={L%JISAWk9Vc}2l`dY^LcwI<7TU4pb1Xb-;g;ItS&z|L>S;!o*NbMRp zlKzsxOhTQl>w^@Xo~-d**h^fvL9px~*N1`+GZULzLusR#D`Rvhx}=gD@0w91Vbk3w zh|XW|hXxA4o4R!#zG%=C=9aG5e)}QzO`D0RSpEt5pzWvaQQkx2VM1{5RXCpghp!nH zH!qK@1~80Ec8a--@u56s_I?K|S5a|17}Ex3?fqO+doi z%zs~DgY2xkM;~I*gwHVZ;A*^iwJ*~}8}foI=@Fh>%b33vP^a}#lgjF4QPYEh`-7g$;bLu*V?GPAs8!tIWpBcST5f0Bu6FBs4cV_& z|M*wom#}yKE^gZpSX!B3#?JW|->eO+tekKvIRV;Wt49H?CY z*N`YAHmuPx?SP^}o&a%N(GUhA;mr-iMQ3z-h|Rpvvpqlkzld>X`*Ca!4&|&+d1@S_ zk%iy&k(vq||KB1^oj(o#-QL2lb81LbICk?g;wlh(E^huvl;=H`S|BPyju+u+V><7L81PH$pp;m{ z!pgJ*it^NO*1^dkZCN3Ky0ixZL)kr?F*L&6y%wquD0*&>lmsbmUfYC+53g}`MQSXP z6EjI-Q;3Lo~VVotN5>wEnO@#;pGuAAXd-l}n!7j>NAYkH_uT7x+C4ku}PdGS1q2WKrAL zs`HGP3Y8)jF^NS(sAU1S@H{*Vw}+QIEN#deXk6$XdVDDeXMb9RUq^h7K5hGMKi|**(eh^0W7=;C+XX4btb-4b|e|UWQ9wOf)XhcLImEz3Lv+&;w zDHrL$p^8U*igDxh^Mf==jtx^LVC$`Y+(a{DB2B)JTbnlWwXCVwwmq!vwC;~fp>ar6 zsK?^+gN1pPLM@0i30bB-YVHBPXk?juD9%i%&9ahq9+^}D6(0^knc7qZFxp?H{5 z*XFk6vgcT~aFx0NDMV0M(4Li7F>lHgth#i8n=M7q!U8S)d&6g_aTdQ-TH3(TF%L;l z^2v*s0$BY#FFZMpgqVy2Od_#6+SJo7Qfw?+%&Jn~fXP^*PnE!gUpo06nzjZxP} zn=|{&%bR#`+W@0}2I5sjIkf}NpX`7vDYMk7 zS)fYf7Rv|@+J_sLHeuVrJ=k#O0S;V^!m*=gGQ<_XN|6;h<3%9u?L#CbnXX}f5gUkr zOAq;4=8%dt@zJ_*lj0vBB{1iK$egguT&PK9=_57bnVIa;gCx*`pM-;EmbuyH-0f45 z70u5EQ7M^qt&I;fDQ)UEhqSyyYAF-bOi!M{sNrCGp$<=x@>_)$xhw&-|@#mhK(QmA)z ze$ii;zk4H&U)yPz?!GDoDG7HF8FEye`ZpaBG8GErDM?OdBB)G2QlcrWX*Ef45AgE8 zO?)kDS~hCL^{hBl$vFP%t;WTaSFAGkC6j=rdF%8-Em+bN=|O6talneqtYB%8=?G;S zxK^4EURJ3TOyCPfsAF0TM!gC`(jygiKe&psdwCyS0r2E*G`{XK31b&8z{Pu;H8G$x zK;h}#1>dyq1B>BvS;Lm;i8ClAY#q5Jq)bVIECmV^T1k09dHW!uVlt*QrEtd1wT+Xi zmqhyT%wI8c*Lnm7>|$TSH0qfoS0Xkz`=mq#!JB{tT#A=*dD=||>x~=>$~h{jLia9k zaH_)BfTvMWNDj?B1+KnGG8bx2Mft*sX?me1RTV^9JPuU zY9$kDRoXyqlR;hl_VwGs-lYjwkB_;8%}W*{$;7U?c&1IiefB3hb{mO5&#&WVHVV+H zj8$IxNlA-c!c>{w;i!7)4{TTe)AOq=;FB{?37V8gCEXJ`^%-9SoG#=H>LwpCEQ zS3kaHILN3u_}zE-_2f<@#$Ms~nue4V8R7#AUu^rE0~>MU5sla6*H+CPkE7xc{ycjt z-l2p8%Cnx8KuqKlm_N#2k|wjv^-&ONItZBxHPuO&sc%d|t(mQ2Zo!0FlCwokQZMG; zR;5y!OnfpUy5)!HRk<7~6RRn(|9K#G{kq>^vlX+0){Wf0>?&H+9)cO0W+5#6Fgx^W zYl*V))v5adCbw*hQlBnEVtMtzr>T&6_`}Z5v>|%Zlt>^pf7XXFp-bZUTXv&nV$u3K zm4;WO7m)Z)Z0D#AVxfn;tge;_pgjy(^U}q-O^esrJ$$Zw#L$t7TfVox9nP|+J zP+OR*EpKkxZ~O8hN;uW6A3Hlz(w@^9@8rq-L5qFN)pcCoTfRJI+D3zU^`0 z@j5Oro>wrwBu{`h-D4fq9#CCjTHD*&WUCE#ag+yToBs8aAC(6}+N`QpVX z)|Jip{md+V(4=EWzGfKi9=(TOwyj1|oO%LEL6Dl71Vy}o@+%mKfTKGR8Jcl59aEr? zB{K^-XX~v@>$-5a*RJ{!67v@FhiCbk!N^gl$#5W{7W~wjyV=6h3KE^g7_-8PX}LtO zsL4kiX_$HW`5~d(_vltzy{d3T=vi!g6N9;5e}wNo`2b@^^~dC;KVtg9J(zp;B7Q%8 z8H>(b#^Tc#xiaU}aZKI63FFqS#L%ffpjpe|=+a;$zU}rM{+YfLTernx&A&jfx(~sfP@czEl`2hrGYc+EO9O&({Vqv;IDKF#kb807Kfd~23|U0dU5w5e{qhl<(@2DAC&D)5Ni@@WoAMxSk)xfntr*Zr39AtId_+7 z5YvhubX24!!XlyIZ_ZR(M^yD|iptg7vm=v?$M?1}>ux8W2k%8h_+i9EoZ?m|CSPHy z^f4FLOiTBu%X3zqan=uoI?>4d*$12P$BD)G?6)uQ+2}SH_emLCx;C4ej6jIQZtyDE z4HG&}z{k_qLiy28g+`zSES<{0%2vH*qp5h47=_eAm_?87gwr8pU1sbm)$}Lqhk;GUEf{REaqE zDjzvWGFNI6YMOW;Sk;+Y)Ph(Gg+pR#sh3bwE)ZKwb>=38JR0Hx&ha2pDdBqHH~f3) zG#=g0>;@6T!m1+doEpKwwFR6#+Q7YJM|k>lg{xrxO^Pi^IP=ub`!>rnZZN!xd{U&9V@6sK1j@s$u4`LIL z8lAULM?t5BNrF5iD7qGP2B9W&#hU(dm|K{^$|>i7f0|-$E3E3mgiM}La6)bW@-7}N z{0wsr{EMegw`-=P(x9G`TPyfi8Gw;3hv0vmdg0eT{qV;pqp)_yWbFCtckEri5P#43 z3M)UKfW<>5VoHZe=v;j$yga*P8-pAfeibnaKO}i(zY^^@hcK3yZcveYQi3&|sYOi)Lah;Lg5Jp5Sr4IR7PY<3 zIz^!orVpwYDHV`y_z_DNeT}1cH*3~Rvyk|sZnc4!)v-T*8}c!>{QVo|?pcm6|DB0p zOFu`S8GX@VL^ITEQ3_QXd875P=IA%OKSrz=kLkPrhpiWv;qr|wSUGVXn)?riwOw8I zu4xY;E!VZ_>?$N~Gu||CL(#;IeAJm;0!Bh9!<~FCI&2J*W1d5rl(F0G>Fk4Y?Hcj5 z{1F$Y#EB;bH+YyCZ)EH~DI`KIGI6+QdT1pob8|hyk9lgA)b>V`;-4TXEN{zmDS3CB zF!zK(0xek7Sq>&;$<-Y#d5-2xJ=l2v&N;vz%T%uhM&J^6&S)sl#K)1)OxOgtZUs^-=&8=bWSCg;hO!J1?y!SK!dM=Ooy;FIY zM>>m|((-AnY1*OpRj5*^U}dL?*2@x3sC8T<5s4=BgWbA-KW7fZjThV47iKpf%u(LA z7v_zag{?=|VdRQ0P`R>Aj;UJpfi9^UjXjw~J`Ml=y@|81DXK((Li1Lg-vh}%Q~5oG zk0jqZuye?MRe3`bniLILOyLo0*{g?m7N5~KYAz{*Dqh+!to*Wf?_RFARhUSWsSxwZ zbU8J$!^*o1BogD~Qqc!9u{E3;w&80zQ@UMyNNltL0#Bo)PzL9ILYJJ+-Ji}!Er>K( z)48*#RjEo&gmZ#?)E@5IZcI!{{&TkK!>bL8v3&Um*2Uyw8b!9~P<0rVty_&z^9R7& zgSARVXs~HMPwNiO*mP+fMo`=KcuQ_*2VI!$V3%EI&ZmBdPG5!ri zFV66_f=B%P6e2T8#AIh*3NAJ3^R@hQ^@U+pt~Mme$#T|ZFB~hqIFlM$zP&sIUJz5xY;!&`dB9n|ok*w_mXG;370_Y^_%VlV8YvKXc1BSoPg3R4Ln+ z?NNUyEnUCr&I`EgoW<1(6O^T|o~vnNscD9y?5*jiOlO5OHUJ5k3?#bPyP$L%b)J~~ zLIZZmio%zuKP4p#@xda)bK2X@xp5_6ZbG&-t2u z$im)e^qKUPhjt0pwD52)gqqWxoUKB#Fm0S&U~ZufX-iThb0KoZLbZE2ERp0V0Br;GgF~u)ju23KcH=eJemDHr*PE zG&}U#>%x=sBUTlQl*Eh!oYkFdVQXiY7M?QXiA-TFd9d6kPjt3 zRKJyrU7XEeWv(`9Rmv#Dy*7LvV(X(DaOSBtEupP#Lu}Z;j7thtSom;8cYNESC+ChK zRLTgffAAQNmkhJiyrF2}ZQ2;t#Dv!n1?nv3TR@23-b8d%#>7N+HeT>;)r_y@9}9B- z>2DL64266!mtXZJ7d82-*8b}xy6!cjjxl|Pn{6^45}Bo z$Sa-_52-Yo{oXWkOJQ0OLdMjqW$azx;9xkPvc0<_%rakXx@-LG=M6jwe zb$!q)1d-O5-2}Nlq%r#WS8c2SPn~7;U*>Thd{)>TTZYhZZ757-zZMufe-K|QT$q~! zQ~sKRnq}2KFdFncd;cMv4=?5Fg@L46o#E(gx};Hb46cL~e7=kLln}@=nUPrA*M~cA zhQh|h7Opl07bBb%3MSMAoGv~C?jqasaF#X#}4E71HKTh9^O&vvhrsK$EFWN-kt?;!r9TkS6LU)YK*LanzYco@ls{ zWefIRyIurwE)_y#=7`~q>*wpnqSZENru*H;z;V_iKY9*0!+J1RZUGOU12l#z(;SK1 z477a(_{ST9qRb}&a| z?u4q9)#u5ORC3a+!BMEN&qD@lNu=q?ntoAoN;IU&`Xx@GA79CXT153> zNRmYuEZV{O#2UPKtF?k$oExC$j1hb-cf`dhuzun?^lLN*4Qus9+ZJunr%Mg=>ga~f zt=-V3nI9V0uZwmK24Y(8?{VX3_DjlVN1rL((SciRSA9&LJU;{T{|b*)-Kumcc4L^X zdmbGdj?~8m=clHKj~9^{=TBQ%mxW_x!!M0i&)e0c$5$V$ZQNj6R=ZehVSyqUzh3z} z8kHZ&ZWqJGR#T?T;q~ib;J3eUJ)QjG>g50@8*SuAoHUG`=QP5bC9f0IhKi2sgEj>J zH0iMBjj|GI5^0iqjZ<2h9159K{|r}d9v%>h)qYuN(i=#g+~l_N%Y*QTNQ%9|*F@;< z?+-8E+#T>__Urzdz7s7Q48@o0=HtlybqERn4~g-YAeS){nyf5VXz(vJk6KdK? z3Qm?Zl`LH!g4=^eA89#YuG60?3xh0G|A7Y=KOfDc7ZN<_)BLZpY#kbatKsz3xZVJ) z?5d+fkIs7g+30wLgaaS6_zGYCJ`PWUc0n#xTjy4GHR0vg9i=PuL79qu`CQaB*dyvU zr&#S9w{~FB^_!U2ZxUjna^G#Q-KHFd)o;pNE`0EzXOD%){)(cd9#4Yh2m4DC%$dg2t(#CgpHH_Q^j+F)S-TE4%6MUCMa5=SU}>$LyMH$z|Eo9H+&+xRNNs-W z(%$}PJW#(dD)K{LyB33Hb^74w-IbhB5SBKT(V^N1%oy|y#ti)hpN?9J(c{))(l=}H z?Sysc+hPW+tkhYxM63-9c)SD4?*wAe@QDa|k=DPd4-=M6K?&E^>I$>kw?7MlUC@1X zy#S%<>+XfxW&}5DR`oZa-gk|;1 zSAtGP;z9|zL0XRPi*=i~V0zby@b&GPW|B!xm|6Owaf5ysSGy{@{4$@_4L`}EbU9k) zPis}*f06SX0V%uwl9Drxp(gDWtWizZJ-twqg3}X(7m!$Us!k5?7qR+V)aC1!ft8td z^8CX*rUBGN6%&YrI5P4H5juF=!PzzE15YA-?8Fk=9F1oCJ3?`h=)aY7jDC8l-d z(Sj0wC4BgrVIg^L-nJWT9O|fd6fmwHEm5~mKfYG@adEdmx1YYlnzeso!LaYSY?Yqg zUEtu-oEsdZ_yEB`KEHnQPiQ&wd-WR`0jJ8fnNg~> zs$WFq9MNPRau2f1W2njcBkd&(&Q_=etD2suF`>yk4@pSn+z0wV6R|w3wJV53CxyT& zGIw*~(-WT}gIua!pG;)#2w!J!Sme-iBayy*Ee?YQ_GCBaV{RW=6O~H$!PrlJgmv`} z+`jCvVn?LI=kqYL-voAnx99dH#a+efn~(AL=oy^PTVKdc(|5*a@bhiK)hS27j=*qO zhZlVQi};4U;N(p19@9YUQNF#lpRbt~QDQjcsc9`58;8=c@iAVQV%6U5(ZJVmOMB%R zku3&Ssen?oOy}w{6pkLYXfkdX=5Jkv6^mEkryhOqP4mY1u6+-D-*GsmcNmQ6?fT=# zt^=`P@fvjbX&OAr8+YcdXYIp~pR%17;NZs!Y3c3ON zi<`Oaf<$3ui#i^rT}Scs^8hXa!{j7ILMcz{x^i;#fOAdtlz(GDNq;^ZJO&mL!w**G zhLS$5(0uS`e68qGvb-xAO#Bo*eqD^7bC;mU+(qa%_cwI;bsm~c8G};gY)q0l)0W-F zE>_w&`D9rrWZ@YzGU^f=_nKXQ5zJ822sJ4yErU-YN3M%(Eowqns0C%`h-Y2Jh%nl% zAIESFh5YS6;_zyQ&tJPN>qYC11;&7-leaY5M_^l7!hpW@zc(3mwNT zhlNevMtv=7cE(pdKGnoThrBv~RkyBT=ez@4T^}gsyVbbQ;OMNLfknN9y^jh$_XW|I z36*ArWfKq-8-$dQ0_aUh$(l~vLb%yT;9AzW4OP{SwJ@ko7xvNG-li!bH^HaXE5NTo z!8R7}5;Sk5q@DJB%2I-$3VXoUGUrAF%`H(vDWuO+L#DXk`XJra_*F9#YLa+*5-Bn% zyT&n{oDzxqNA#Z}S+-?8*g0yudbfkKZ)I{KUMIJ;mDnDRu35)eQfkS`!CX{5_=J-pSWYhf0yk8l<$Be<>>qSm!d;Wp1HHdf?0%Xj-`|=YlgO z=9cB~QT;mTIBN#Krx-&~-d$(Hm?{Y`pJzSiLavbFX`EUt6xszvNU;TLS`catSy|MA zNRu+AiKPy{p}$pJyKP03)XB{DHX#gRc7x=VM5#72mA%9++a(k*aQ!P$kgj*JaWlm6oC|DlqfHfOo9F|+hR z=VtZMYve?jNpfCNubA9X)sOCD?e+8|NCp(Ii&{7JsK^E--AxzgT)wUszMDFSThmjsNUD*b z6^}YKXpW(MK7~gO(@ko6pK$W^V?Atbi-Q zp5$v{w69nj){Teob;DuqTNzy{XlHiaV}f&hh5qgf2WH&&s#1Y%sau@L=x1;^yPn%F zNTf~r!NFPo6`Au(Qv7wiJ-mvqnFf`TY!B__Z)WZe%UldD8-mJ>eDJ^7bI_+=UoM{C z7?69edxN(4{Ijp%+uU$Dwu?!NWBH1(w9-O&a~7QptI-R%2o142Pt7h7S#REa&e?jcqGb_~@z)P%o>kOM-6)338`iA_oCDpgn?&9Dh^ zvg1wUO{Cs7-C(LpsbFHQ9h{a}IC3i!6$BLC)oRM;_-*cDeAD_fF2#KCkCA9UlBMJ-Hr z$y2413B|=7rSx;OXffhd6kgzw=LHTIN42QhM zhp=<1#n+4vtssB)tm#(1A&Xk6&c{}R1Ee97n?M;6B$P7E7^trr#?oEX)DdhHaR2r1o=@@9NP#!=!}GFHIY_gV@}33*W}Y zTt)1;HGIvuB&X87plx7Xtu0|$2&2|0L6j5gb$OQrDwYRo*8m#SVM1OVsN&DWp)62} z*&`lq>}D?3=K`gyE6q;iPl74|sc+RTSY47MULZ9!fN4Y;kr2EVx}kY#$WEw9aN z4ah|0UQEfE9!5nWCMLssWeJH8mGPAb;$pp1)pUwjqHuJa57iKOU)>w|PR!wry?j?uf& z8<6VJ8_;}5dJ~SQ=$E*%CwEET2lVNz>E5R#K7#aV?&GONI`O7Tr96Y{Ln&7vE>-5j&*FUUy}+xhx$B;{9@!cW4(jL&THN$_V8+#-bHdC_oA`uWFlm<> zV_!}thnZL%3PPU|7G?Yhiqu>CS?}VGx+1bhr-~(6gQBkL3yndPbJd3SB`B)_Fb`uYi?6N1>a ztr9Ug42v@Q4S(^K}Vz+C9t5l{9=m95A4EH$hE!UWTnnHh)RLGk!n{+I{!5JP02D7CcC8b6R62fsOfs9 zcci;T@Kb9NG$q8pz>}RjXj5;>S8+xKXKmt}YoXz}n%rTh(>;w%iONEcX(1QKM}~ZacS7-1X~NI=pHet+WG+g#m@c2JOqy zdVi)PB>WoW=Z%;5L@L+bEFzI5%xsH=x|sYC8UBh1we~)iDz))TvIl1nlMu*6HI09o zHU$4Pncrj>r%`c%oGfZVs0E8!Gk07ri^SPycafyAv2%u|eD|nPm3>w9>Y-5)w_tl^ z7q_hses0>BUa2AmZ=ZzmH7#ww?1SpQG!v4cO2oRe`yr3a_`Ljb^S|E_9(sb?7Kz<4 z#J?0eeW6~aE4L)Fw)9biw!!kYFPN#!+2Pl$AD49ARM5nzL)R}tZUomASs+qMSk5C7 zTSDTX?THmrOkN1243H{q4_={Sx+_!Hlb)Z0G^2l-ENI<&`6EjbA_DL%cJ)Sm3u zG=A!{uR?J3sQzhBi3a_;!p2^0@hatEIDbd~S&Ee_YO|ZAN+S?_HDib%1^y4HRTt(G zwFQ_Ee;y|`&EmEVh2rQ!?0s~Di!323S8Rp;)4pbw8CT6U*EStOT*58B#(Ex}rd#S~M-NQlL`(6qi-g;Ivp2z7F} zqb~wDz0k3y>B;1wAWbEI2?;eB#%YvXASa=w7YPH3B-G?&(b&)tVb^hgXYN+#XxOz1 ze4YIG8Z*&DUc)R!JDV^&tjg8mR*EJRsUZk@e2@Kbu9}7>V}_%mmv*AY@kd8+W%YRW zqO=Dxw{Vu&;otDf+2cq_P&Y6(4)yRw<0h!yC~x9Q;|iO0?$u0t=kg*q>a5-_WOS`r zlYKwa4Z0=A-@=OvTQa|L^9fDd5+IA(vRJ5#$s;r)^!2Ipc>U%TzekNiE`vBB0`hw& z@F-lJe_F0%ATY=ZA)yu`J~DNC5c&u;y|b=R)0=ajCX3;>GEQeQ^)BofFdX)tEIdg!n0N?A{Y3rX5)2s9o06C0Z8z8BuU|(% z7M!~tfPgGE5>b19z|SZ4ASRs8buF^S$90-x@b90h>v`bTl`x!nCgjcrd_7ttwR$7I zR=7~K`4~|cuDD`~$qODXz=JC{kr1Qp6%z`D z6w0J9#5~@ExaeEy-5w;$6xT~GWpaIFIgm)?B-8}GAxSa48Qn27-BIKIxEUOXD~ECy zz0vK9k6~%UyD?O8ID6@=dOHV{<`Otrx$`yPVR!%n0y4Lf2aT?T?}mH^pHe+pjoq58 z8}i`Q4*a}mBto`NflSW@RaFnqB5v0=_-EZ$Sa5PXlH!ClBgLpwx-Y)l_#?NHOII#}%mVS66}E8&)paliS~58y?ZcJ=bIek~wxJCd&z zPO3NR1anDQzGi&LCA8<(Eu`kr|DPH3R=U;=3AHNKIH7>W7zZgkUGeI1x&oynvm5W7 zdx^)985cxRDw7dn2L!zi&dlv`jukNzbzt{BG+X8kr!10w^kPw^J?LnxN zDct(4aZwrE9(2=_sF2}H%?()zS*VyAeT142+#lqprdEJvx@b(`XJHRI5-Xw#z<)*M@bxqUxEy&3~yX|0VO zA^&x9{0-JKID_!F{~Sc1lt>mjo9SarP+qY{bHy-G#)olUS@H&3ecF|ME8p0&A=2XylRt5gcs zUaA|+VhTUcZ@opptHVf0xQKtRn|=j*f+D<1GMh@uE}Lz)A8;C$9;G54*Ka1W$KtqU z40*`Feas4lg9rnObn#_-dFw0Gg8PHsl-`^^gi=FsR&{d1Bb?s4ja~3m(~okM9N^`s z4$~v>>Lz~z+ zQ_nky%FViW2E@%t-D5ww@hYB0rH%H`z{LB_%e|aYpd-}u6KJ4|3|{h3kd;Y4!XQFT zZ<5~qq3Mgt<*C?p?;hUfVqOVZy8YW~^J1hV-$BYH^#qcfs9Cp#W@hV*QzA`1h~HSn$O-%pEeGS=&?azfK?Fr>;XV_ruSy>iZvXXye2ujnac41m=udo2KS;Ez*LB9g$?fs!;$axGej+Ap{Q+Y(e2bC4 z4@J)}nxIT2M=nOv5JWOv{tx)w=Evip%5f99haM9ND=QiMi-OdbpdRl7}`N?P;if zLa#J4$ADUOm{154XO!4-{S@w=e#X}e7lChqVO^(V_nA5DD5^tv?3^0mvzmSg?=zjN z7d2v=4MGXG+I-D4yotC1>5<=azN+;=q%)0_7JpE&Ug7)7!6_4QXT5$#lD^zowGM+i z4#Jp$Un5kV1vuZ(AmYQ*8xbC?4(Bzq^g$ng(?uW=G!KLp`XCO^--s6>*@q^lv!+Qa z$v-Vb&XILZ2Cz|tnv{kr6K(x1J7dSFVH@(8gP zj&a*L5#4P(YL(JX3`vZ;jHzGEW7ZMBqYyw{qw6=4@o|SwapLA8&MG7*Y1ELq<&oGc zcfEkZ1o_jum8XU>pZ=g+K+!jAFI`4*VlI4aSzu+M%^R##$e_wB72|tAz}Z_^b}JCy zuiuQw$ei7W`fz{yf0(`hFakpM;o76+7}2*oE**cvML^_-n@g5s%OhrtsmN9Y{Ja~W z%77uY&jbmAUPmRj9eeVn zkex5MKgg=q*w9f?7jb;aA566Q9eU7jd`H;Z)!}O@oZqK^9^!T%PttLn04E=S4XL7u7tqtIL}NCaT-+Ttwo_ z{H@e(M^nmpR4OSG>i5(2A^dDEKm4_mS!tdL@!uWwB zasMRs{`Kd<-m}=UcO~vW*w1S03yLk#yY5G5{Aqvn6HEa4q{%l;#3cZm4<1Bv!cEp2 zQ>M=^%?UK^OBa$y9tc{0kp9vj6&bjC3uRt}n%)4m~^OF3nH-_Uo#CUiMMcY>k`QFVO6_W zYuC0@shCCmp8BY*J-lFTtM>Dr50Al<1E;y|+!D>yVEXXSa5s>wa_!AeDS&AvFNZ1~ zcY-$K^Y1^x!ck-K>@ZEv%bH}SG8g8p!kRC#zUxyhxEPnMW!YsE37DBUXe>s=Ogl#X7wdh8e?V;>?pNsiJ?46AiiCo|KR z$Veq74;zIW0b5v2Fv-R6E71*~HLVGek3WJ#lAx4RC!CD2aNK))2Zt^{z^Oy0AiaDX zvDfY)@@WuWU%iPp7p^1l%q2WLxCMXSe~3G`_cDuL9qHlY-UZ(;Tnle+Bi+!MK!GK` zo}APmp0ySKn>-f}Umb=_8`4Q4O)g=I8}wl%eJBc9(8pP!kw?n$K|dzP*@(L2MW_Yn zNYNLf=90vloRwUglPn8?OMDqL?V_E~k{#x(Wqf?%C2qe|`=1o@Pun(ZyZH(S*B?W|oiNChwy^hf;|3h54alo_llA=I zb}aVIUXQ>3Sc8Sz58&vn9Y{%`^N_BQ6xpLq?arvvdkPZ0OuI;|DNtHk!|T}v+8;U1yDc!O9+82pk(6Q=^| zdfe!XHoOe527zhkWZ6;HV26~`D|!L zA6cSg#gRpwtNy-7=fl{S(%Uv6i6eogG0^g?&}iTwR;W+UeMvvhF1T=WA3XI;T@m;q z3Ds-YXRWO|4XLAjGkmo8KWytjw7-sxh3DCe+Md00tY;Ar6hPkSen za`ixkk}mLZbAg$S1lIPpkT}@E#>o!0j@C$wibX<13{s-vkP^pAVgh9Bf=f(^!haD7 zc>LlO;-ar=qB%0d!KE2S)vf_ix33^|(ymbRE@6ND2o|oJmF)<)aS)+;#eNt#YAz)9 zxyz9feQghZUh^Se14c9*hR>G$!Pnj|UOf!MH{VV~K#)4YnWs+|Ol{i=4JVD^>REC3 z&^3HLX9i+oweAvH1!!dLRv5Hk9<0*oX~^AYIJ53=Y|HN*Sy6U8A~aNvPkN8W(Hk4n$3~L~3&Gt@SkcX|K(3HwtdNjDWQEdi zm`teYiDXUFPaspjDk~H#H;NVRNLpIjW7^=~F?IDY?#VepR&J9jpWtpFMMzM4Yk}cI zH^Qz}?z&-w^yFUbJNP$3qi%2mj5#8*f|-RI%q^VQ_jY6#m>sOFt=YCY#9|RQkU+;K zAzq5;m=GkzJ?92r(nJ0@OY0IyO^F2M5YfP_YIpah=vTEOY`T33sf&8?gLjG3KMup) z=iB+32@%<#Z{2>VKWaJ5w3l*@#J+xjAAfGcE;I^Y0-Dwsh~G9CXMN40z{ORYF?ro@ zkS5ZQ6NQjg$AIe1(B;!{aQ8383>?-2Wj$XSe2fWC!qq?j!2DzTker~M`{3l>8ei6_ zhW7va1(Iy^G$|?(_x|31Be!qj(whjxCj~Pjh#8ywxgaQH*V?`cJUz=|c!knvJ$)u5 zHoAtY@o{zlym{M-+|8>-{)%ab_8=xgv+59uEk!<%g+klZG&N;KaDC7|N?=D8HFY`3 zq9z|T4J78`YGs#s9)+49tD2StC*L#;{nL^v{aDesKzuwq;q2wTDCMJ{*ZDt5n4L_pv@$dbry#lso|FR%6(-mo6xx=rK~9K9>p@0yEW4ldTNRAWLYnr1@V zkKx&4D!(UZ;zX(Vd0aIpWaKggm-l7U_P^OVdUwuAZrS(U|1kIDZpf0=V?896&aO>R z-q9O1ef{9!XoH}zFq{m0f#A@~8Y75|sxNJ>=T`OyoAx_<>X z0s?S9Jf3xaMX2Lr32#elRBX}>{yk0alA_)~Ic9-ksqNJtZJI4qia=v!U}Iy+hKB#8JM~G zE6oqi4r;X;R33^)uX)Q%_Zg3bQgqmgK9ZPFtef*WBEydGdrXUseSJ(BG8xu2^jpR( zk=YKvPHN69LiM_v?p~d+?)L?7^EYh{lfuS-zbwPgd;W$rNxvj%G&``Be{&2O`4uX+ zuB=z@xv)b$hjOKue(`b}9SaO^KOBc{Z)5kidS)?$#P%>N6l+NC4JTG8d66Ji^!Qm; z1Z$f7JY<2=8|2EGCh{ZHgy5?ttD1b(WKH*Bg;J|=KeMD~M;tx86_u;$w;cb@m6!4H zSN$~BZx!DGX!C<{x}>yNfn{6-RN-$CcIP@?zqpD(c?tqwoMXqr91`o2u(olAox~2_ zHWHL$7DZJbKlrukfVy34aI=)M%LPJ5Hac2?o!>6S@~b!T_{C8ji&G32)>Rzds5xZ4 zC%qHab@=ixvE_>4S7Xr^PtPtG`|5Sr5e$AoWDsdq=N z{yy{U#5K&GzXVT$_c8k=^E?(}Gf!0VX@cP$x}f1FA7tz5C~Ek5sk3bo6yiI5&~I3L za4w`$jT@Cjn8KWBRu9GC(#=aLJ<0V;f=!S$y_6NQ1jwHxSkrlm1I@2c(+fzDb**X| zE~Z5UxuFD0i7h6yn}DBo|B$Vy2~fCO{mO&!?5%oWgIesbrffx0nHpT(n7Ew$8>>(M zhlKbTjILJ}6)HDJ#nyFEqjg1Y)HnBsM-3g~+h>EY@u!7Ycl#!i;&iNQNon-1*&M!u ze}v4TXfLp8f9*f~wr&=ri3O7*nC_RlmHVORz}c`ak^O`s>OXB?@CmLyQ;&(z!VSL< z8IO8jj^OI=I}~|#?aUJ#Sic$%UcW$O;#*i+*}=iW5v815F}QnY)cvTZUiWlSg4RFu z;BP~^ZtuzJ+haT{qZI}{ z-g<}R>5`xDJY7h_=#bI zeg5OeTD|I94d{HBE`8)l4II%^joZ|5uBogf4R~*Z<~4NWut93=siQ^f^R#^@zR_&u z=Iqph4J(ziYNgsYZl&xh92t_`sEv}RPtw*zCb#zo^0aaL8bu{dE@>!8jzZB<7DPCHh^FBqzeF1tkMUv!>^oj+8AhMug( z-P-R{o2ZOnQA_=RwOgM(K32EB_Lvb?^NWV!ePRqXf=Xt{0XY*`K_I@lbI!TXjjc3B zvTEz{C=*G8^5_rw1;nZla>501@yLC86$d-EPSV>CKkfK#OM_vz4^i`a1&a@6Y?!TT zbN9T8XIE%b@r|P6y6DVPFHy%X@#U@-qD?n8?5@H3@ZBdhyw~XgJKUsb z+QKjN-W!jo#?*ow2~`oNbnf6xuJ9O`oR*U9$;E*va$=uuxUj(u5ooP3e0#rO1|&DHw# z-|P9$UsUWj`)?n(eMR=s7pYe5irxLCD45^)YUa;c{Qk{*lrWyQ$0({=!Q|oML|>k8 z7ylg~#+o&fY6WPEaUago&7)pW;*Rk}e{DKTsk3T{WjDA#g>~w%1>&-3SVt&=0?ldE#^&35!wSfMrWW<>-ObzN5Okkz#n5?JAeki^BKPSYns)0p41DgAH-`S5KCF*|CIBY|{>hVkd(f7{~(NZ!RGz{0J zy?f*jqf-;7YU<=&vvTbVYJK~JKAttlH0sUHq~#sMFH(=*yGEysd*m&BdDAE@UFI#; zeY0Urnw~i8ew}jcC7SsbuBp<2rSqqb?5i)oeosAG4L7CX^Qn3=m*|x#W0dsf4GPbo zL#|qA)B8k4#qR&G!yM0gz53VZT2Fkp|6Cv!Ki^lKr-_SasOE%{ zH?l1a@Oi&FbG9;5W|>cp)X5zO>WU|atGEf0otsu_)Z}RzcH)Ja@XeC^tbCbSrU(9E zxgNay|Mb*ff9R+t)phEfBYb}>CHT5--Q)E6_}6v%A(!O4d%Rhz^!~K5TKnEjPK5DYuo2orW;G$&UT5V;Nw2hMn ze~#a#hwr^t<7bRfjyJWaUz+ebqBocvM1b(=r{+yh*3&OOpeDPCO7TQC(x>15sP&tE*4+8qR5Pc!k>d^Z z?}Rma>dA-n!}QSZhz1NhPDwLEHEZ=e<>jWC9z9iyHf&V< z_SI_Iq@z65Yx`ea`O#(jV*Rz0IR8S!$Nhp_?{-aJyHVXjJ&I}7%c-0e)tWZdUlYjA z#->GiN=~bz!DpZ1`=g5k3b;BNH!!1h|DJ~yzyGjaci-|qO-N}Zls8dZ&=%|u!2^VBA%rE=>)ELl+rxGG zS*I#4j!2f>19h9w?qK~gZjq9beQ}K2leg-KkTB)6FW81_Z&B;pC-up+G0MwHQnebr z^zhK(I{uQ~r?_m~HbGm*&Cs+pi?wyfBF$g6O-ujYsL}5}uSKhVQgn11J@eq>I{f$+ z`xGfcR7n3*`^vk%iKZ=>Zpg!Q%6aKpzImW#seY-J;9D+u1BT%gmd!G>u zqUORdet=cg^ek-S;xKb^+BVf+7ok3<^)0bMBs8d|@LB8i`?6KOe6|qR_6T)7(x~e2 zf@RyKMbyqs`ta)yv@>y}sxH`!)`{vcgXKW3Vbb`mYhTx$AH1ja8-FnEET`xu0B{y0 zjmv{xoUC_{uwArbKaJnuDr#U=HI8l}iC zu(xD1Q_a|=N~qC6p6Zo<6(cL2{$4Q85HK#g;!(3sKV3cWIQ1HGt=2E!s>EbEzj7HZ z77LQr|GR3H4$AYWdfPsZa)`;^uBnTbD96indX=&+M3-KFf-~8hWi(O2oRW*G0)$t? z1XBm=`@~Nmc5e6Th4b&!BftKt#BINuwv}6yq&XlSn5=lLK4iWPcLoH^-vcP$sjR4N z2;361^;?%)&Dn>6`X0f>bHNM~kV$Kdv7|D;1#a9tK_A`uw9Gv!5w&WC>HHz5t45t8 zon!Jpe>P)+B9n0kl^(I{7wf|Xi_PszcGUFgE$ge}A-*lZNOU91`j#D7RW2 zr8e)Zp;z3i-W^Xf1?*%W2pT+@OZD>DSG4;3=M-X^Qs%+Ot3ktp$m&94%L~di26y^*GxX)!rCK!iNBX3W=M^ytuIvJw2 ztN)5Ybac2G9r!xGw8rgp)cJSmuuenFdrBVV>Xd(PjI zljrKkQ6D=Sz2f$&WBT%TEquv(VuO2GF3Jd89H{4mOvl>5bg)HJtuiY5{^dz}{GogF z=G4EGkvdCW58<{B9RW+ygq1ODW>O572K)sPTavcmwVbnL&7)P)Z#iv(05ZNDKCdfW>fK^TI#|J9#T%Tsr&OO|0$&k*Zbw8`;1ER zZLqm*d!%aD$W@1~hpJ}X((S^_WMNIyNLzNSqi!ekP!~fewbpIcg2Wn1HJwgE_EL4} z)J@gv6*K1A3%q{WlX`yARAp!QlN*J^YQ&M3D{4@|)=^aj&ZOev$y&6r_({d)ExW2= zpP|Z)U@F50Z?$+e?a*7Xo7QW^X1r#MCW8aQAm^^0qp+A*g~!*{R{uQ6rX8DAZCRpv zpL*;rZV!CdtY#yUAkthmBdLnq@&3OK)G`T_*l?EiSkwm{h&5}ydh^n|^uoWhG}nwy zb8~hmE4xsAqU4yI2=qy_@(_|{4an|DxeDYg z)oB2G0=;EImmZq?_bjd6Kx}oM64N%SPiCg_I+r}Ln6hxKwr)pJd*#X6s5xs=G;_*g z#irCy+g^JYTF152ZyK-u!v|^jdFSZNvo6;u=M7Q4X8T6c6F#1<|GoRU61TB~j}Lv@ z4pqx*2+^#XXcCgEsWyV82--I5tc2cd3{)5pe4~zstLEmlTE5kYn_L7>kYE(_%8hH( zyhVF$+O)+XZ=#FlY|m56%$n-lzxYgXoI}NtC!&T?c2pnI^s=p-i}D8}8e*WE)R$tb znftwm_R1viS5N(>#~yl2AN)H(JGTC1$~7x6cL#u^S#8KpB{sSPlE&S!Cna6ss3@Wa z+*YULf%RC_g3ry~m{&US~|36b*5O4O8f8OlsE!|A*%E!r8T!&-He zr-Z!-TOK|@?YF0C!R8$fX|Tv)tx-@D=WkZ@!kubycrT{{>inB+uN zQ~l0IDt_&1E#E?lg<#6oENxi7Tv64UJ5e^kV?@NPH6FE#Ypm9XG%YmMUlfRpIKRq< zyqP42v|a2wXjhHu`Zk*7d2yYl1@j8Crfkux*W9Pa$BfbZ6=US}Y;#;40jmuGrSrQn zmV6T@&14yKX@NfJN6KruroMAb0$L|X`4ie*Hjvk1{Forwsld>A!`VlUW;v89EZn_v_v)C8m~i6 z>b^(i0`M8;q}9~Sm5JK1-M4w{=AEn6KH00NgG)~Qp53Cedben+oWwM(+e+@JY^Q-v zUAtb1e=ky(BaTw7+NG!&<%n;d{Z03L`o6N#eJe28(fo>|M#wwxT<5r|BO)|L^Ovqr zW;$)ckC1pB-MX_pUG}`wd~QUPnwxS>UARhHcKSA6tw^M#EYa`Nx2jEaQ?=<{GFe+d z(u~k*8^R`J48=`;3Eoe~>`p`$j~a14|8qjE(uGAbsr%~(GxYea_v)nyKWoz_Vx!ZG zn=qui#Kpmm+14Ko0T4n=C@}=i>ndHzRJ5W75Hq(QRn4TL(H;u*dQ-G$dxTCu^<>4@ zD7kdnbZ@4RDN8hQO`=}~d9+~TCLP|TpK@vzj&&{$xJg=e?4#P7H)z=wX8O||SIE50 ztF&<9G97jN09CJ1F=V}X_I6!!$D`V`nPh|1l5h>`e46T9_Ly^gAiz_vi6R#Mu9ce# zw&@CsXrQB7cTi?$_5|HCaQ(FF-cK85FIQ43?WRs)eb4CU6yBad#qW6hVcPWDY|Y+eR9bGPGo!nF&T4fTaGLT$OD<_ABBA?0 zwcD~*^S5ksw*BIBR&8CW^^-Sf;8`aqDyoDF*+-_@${&q+dKm{jYw^ zTwm#;QK||$ZJejS)@=2!05rrE(@gzabx?Z8qn+cDfX}*Xk0Z47p9MMCv8=5v9hWvo=x(k-fg#GZ%K7I|S2itc%WdM0kr%#AG7 z#!cnX;;kE%X@z8SVzpP^7TI+~59hG*_Va{=BKhg@fu#w^_LdJM8AFa=ep9U=#T$+OPja< zW5{`@qoRxCk47C+K~T|bjL8H?po+Gx4olLN8vBE&?cJpBG)3wh=P!cE%vdYW(o`Kc z;=~dc!GQWr;#4y=PLmdHS9+>%mE)S7t8{Qqtin6kZLUg2$iV~DVbfC0*|^p;QrsYE zhPbyYZdIOooY<>W*Rr?xW>ia#=gI5Wh5s|l}YN6K6yZc=@I=)yiDh?hbmH7QD(z<9#$ z@I_NutcqqdUuj((MX^7Lnre+!MOA~=#zJ#V16jHwQ-`)bT&+4Iv`fZ8hqhAK^tGC} zVxyc`-P}ygU%OHLI`)>QR_Q(iM0Gn>4c9EyqD>IKsQ}&`n!0MITIM!Vhr?Qzn{oo1 zHhFZ-)%R${+Jf!H4sAO`jjwug?<7VQLxa_eG~YC%w3KD4S*Me_b?BwcrhCo@QXF#Q z>!?Tjo|-vrk|}c{s`eFm3XAHg(1@ny{)a0px{o5_h8ZKqJ50IRyDr$t&Rna1*Ur;V zzb{qV9}Cs!;O?qjx9ska@&Hu_qpl(VmOyF0E>P(ZJS2(XQh_uvR5`8|h_sp^>3lQC zoU?8iUgx~WyqEW5_Vf5`z9(2!cJ_qjy6yZ&blv?=YvSCGl%7G`^q3Abueg|Ke9i3q zFwPj0A-FkM5^KZm$ZlsR46D?P!2Td=09jMCsA`ln##kTX4$aQmqJRF`tPvLvEmi7J zDoK|U4%fQhW@+vQe3#iy%FNXZH>%T-CzU=OYDlPRbvs&7D^_UL*2U%mbLGiiuRrJS z&=Eb3RFf7)CX@qkqm91mX?-&J6LTks1~TfUW69A*4DPhr*SsZpD{K_jmN9ck&(x6l5n>#u%PkG}bW=C1z9v_saR z6o_9!?)+lAew`h_nyrLw!cL*`N&aeyJz@V2FLJJfGd zO|>#>X~LY1{v<$oTCrojdgqvX-+|rSO9!G0I=5(}#S7LsY6+}QTBqt8tE=ar9;NCy z_Jon=8GCZ9A!9a;O%s zV*NsX1Kw-kkB*GL&^+6nmXOs20|m8H)g_(XT#`J$eC{~i52{d4U|o@3PVo<~Rl+hyU( zATNZC=wJd7HwOwD)_{Of0tv?OgR*?t8poU`paIf`u;C&2h)P*$N^5@-HLBVwYi=mR z4|>4v{^zD3ll9l^)jF>KU^O%(TpG0R(?a31Hf!RF)us_8n#Px|x$AeTUz0ZS)Nk&8 zRmouD>#(qNOHEk7F<_4FPoT;m>ztB`feyTx8v>Cp4Q;j~y z`;Pwu$gSI4Nvp=In&H%_P7vO{qsGC9s>YgRE#ElZw63zcA3a7f4|K@5B=}q+eH9UB z343W_!Zx7Z>@cIdNh=oV*O{v|>GR*TbMi7p)NQ4NhH>RO=TTKay!f{5Px8L}* z9(n#{eK+}it=aakv%=UXfQv(@!lDjUWX&s;lQBM!s988i4;@I-40RxkTE-w_d}}R? zu{9ENVLWmSA+Wy%qzz&7diyEt)m^W&e>uNW9MMF%@{PThFv$2QV-Ryu5~gQoD29IrMzzX|QB| zvd$ZNt>&#HF%}3_|Bl1e^`_B#HPjCEWa!^#uGQp)gk$EK%2G@BUG$VLe{`6WjHxVO zk;@TX&e!7gU;5AZ8t}00Lsj?6QOXNB@TFFrAD(vdXn4+k#eb&!@XKB2Ysvbr{6~8S zeAH33AM_vXJ($$=rhcUCG3wtTZYAk!gX$p2eo_)n4fd7+qdYw0@bgRcxiLhI<9 zUZ<({6;B`VWF5PHi9#m6sMNEc_n#?0gnF~|`Tx$*#_hiH+8cy~HC44nM0o8R@{FqX zrhX}J`WWSA6UJLQ&|ybLHBq&gda4&)L#^v1s8^@<>T!I3weQzKwG0K>Rg0AgB%Vq! z1BoTev-SDA6EyYLF^{tM(lKP+LRuE!hWvmtG}EtieMt}wE$3`7mA3j`k# z{-ln^AnS{a1(Wl?Op^jZjmb3h{H*Wh(x4!wfvi%VjQL&wL;~Cc}<>(R4 zacPnF?zMV((pSpMNiwt=t_SWKrz@`S?F99g6|3fN(V1u7q=lY)cNTJZ9m~!|55pZ{WRaadxVmb|MZ_JJ*qVxZ-|1bSU$-0tdKY3XG7d$ zP5ogbguMqMBdS^x#YWUs{n#36Sfi$z*Q=|J2enbNuAS7VTYJ^46Xh&~VUY>@FhJ(z z*`jGrsXH^YWyMx)TDeVYmTb_3$#XP!#VV~!+@R#lb;{0KZN77Ne&*s48r9Vh^azE= z4CKgnayQ8JELHlZvy_#UpZuQ?ufL3;rtwWvaO5O_LkcPAu&^-4;5q(T7^*fSp}fG5 zKMM$(Uglk6cHs=_+J^j9m>ZV#ZpJvh_uX6Cu=;!FxLxC+ z`sMGpot0qwf@QPTYs9&?Yu<{l&5@t1!y4*>J|`;fk^`Hp!_yMwjV(VyEQ-i=OEmiN zOO=u8Tlra9L?=9}u$VIJa9;{Q+PR)ZMrAK^B1TZ#c{z-#%MSJt4Ub4rM0jmONSqpTt8 zLhK$4R2HHJd^tB7e>EHE;hdrb2N`4g@%$SPS=Xs`j2^!EUY&DouM%@{kf-*-0as|; z{Ey9%uVNk2_AK?k{^?Q?MbyqsO8x#deYb3(Hm>mzb)8xR^zWSSO6@b>6BbThsf(|? zPqUYOV{QWV!u0-;O?5?|lNEaLlg_b=vLR&lpL%8V_3~uVzDkFP_=}vp9~A|%Hs*Q^ zaU0S`F*@d5W>~#+E`ySgXLL?jSY1U#*D-CUrlMow6dN0(xVT88zC)Fnou`cS9A#v9 zl$DvKtn3U!glY16QkCOLQjT}0`H%=%q-l4dBGp~dG90oN1dChIj%#^-=Zmgd9^j*zgHb&W5g*qrm z`88t<(O3;fVGw14!#t>kQrUBqwd!c zgPJ)7usi(t?rdFj+s#T%rV}2 z3*|Aqu_0mz*${I$k#ND`lJpHH$eX=BP*L-v2}H;z=~@ZBblstc>gv(=E1^-@!XAHl zXPRz(_9ZP@{Dm1|gno?&57qT8=j7b6? zS(|^6QHHF|KXPTjE*^L?-L5`3Dd^R(;W{#ttoTHnCtNM=GAK*uKyc+wssuN zsyP>B#fDe!)mMMNT&nVoY4DLDo$@W);3LNP^&kZR5&;iwKFheF0aV)jk6Ryg?JQ;G@n5{ME9_DSGO<%Qg7M zLHm4#Z(TK75B)Y?8`l_RUEsHdtZja4eAX+C;jgAr1x-~~Mxq9mtnoY1C0ep(f|12A z?Ti%+bE2Xgvc|7_YL{bm)k6=cZ~x+b&mv|;A-6SBv@q4 z+ni7_7nKK|P zQ+cws2ksh!dLC_z6_M5jLPHIjs;&&VK6VFwFzkuZi*XY$h8~bNtB#ylo}8QwgQ=auK(cWjRA!Gvg?v6>FCFz~;%ZFY)A z_Uxd3&6=uz%XV6rT1P1$hWZ8>bJx|rAey+HAzbba&M8z)pQ&C&i(hJHPQ$DI^ zuVE@e%fpY;#=jRRDXnOk7J!}LBdQ-58w(!@ATlO9@d=*!P6VJu!@+uS^t0-7BWDQYcM7uN8;3nY) z0ikxM2WQn$t|7J^^YzoTgvZS!7Az__CeyT=nIZjuhaUe&zVQ**lwLQapG2OODenrc0_n94LWnMv)e^!uroz^^}-vqVWmgC zk7EJmp3$shL&fJd(V9)$wSMC~XXDP;nhjK|$0`0-R6ay+U88Dap3&EzzoFkJeyNEI zr)m6xsrr5KpZasvO8vWhi552tIePMN7pCl|ng3cVCtKGWI6BIsYt)^`bRYqFDrmDFi z^>x;{XDiC^3@(*DfkaX7T>5}MU$aqXUU{8bx5{6`vs+LnrhPX{AB-F0kTf0niEVl+ z@8Hts*RDuZANQs{`r-}!IP-fY?wq0Q%yr7~Y&GSJh)W-E%UHe(+oR$3By(M_)!(L^;H6QNl7P<{F@dS}u&ty}Z0xwpk9doPN-+^yQU z@dv#){&{`PSdvp$RDnfyH?^#)&oKI4GQ2RfrlCC2N+ATCyg23b`#= z+UbxraL76!s)dipaV8K0*)&8)A2d|=Ju|}DBW`!7-n6OdN%KG2L0G`@?!R{~(~I}q zqHmrV=eSlZ`1*Hmd0#J2pQh~eY#o2aP0q%mB_ZAX%D3;hUyD|~f5DqE-ZZ4b zS@%d@bxDsyN|p1cJaE)mrzkwCg)>TL0y6-N=5cjWK zS$|@TC7uOjot@<2Tg!R*A8saZpS;9gJ3T)fDYI^&(@w)x3 zxAo-EJM_(?-|B~#f7g>|+@d$8PSC1VGj&a$UK(&p@q|zmpWXejKAQcH`Nh7SE^5_1 zMkfrpq*CVoh)7A)_w%QkDjd*58IZ!WPJ?4rx6vR~i)SywJy*BP^jUiC)7!M^mAe#@ z7Zg`tv4}kWY>gN=(x|0fC!@S+-zq0#qW{Rn{(v{NU;}82xVTR0b7{p*_8u~*vueZ= zA#7?f6J0()01!13i}V;cK!egis~S;T(28LNn3D-bWHK|bY!yu)I?^tmH3(KG?s8Xz z>X1&YRO5(~i*;E9V$7zCw`=r zi@#LE`u%j#1Wcz#T?uWH!s`5mT3N%L_&PQY9|adHGVhu!;B(+n)8jOzjK$9 zuC$_1>yrC)R*zxMu(}X(GnKLVTGNTxxO*3+1(^b~cRuCBKbHmG40Kmf6&0#e`uBHy z#}x%cbb|!n?ohZt0>D*3(pF8oL|qJE2Z$J&WipZ4z>E}xjXMOwF6^(c2&t|kJ2qBq zud8MF*2&|$J1qQnixzGznu;=1M|SHZJGT&Fim2ZaF?#&77j?@KebunpVCOvfuWvc? zJe@RxjV4OQSEGK_$8&Lc_uz$@G&2Xj8MfE_?SuMa8kO zv;aiLoN!)OJ@(@$ef!a9-8}Fb)k!$k6k;KtpAUJdnlR@(&3WT?^Buc(vcW?As~*>o zuBV&&QRIH-B`R~v)rN3fvMvd}O%uK_o!dFe*mSlLIGau9SGMeB-n3oa9x<`qb?!YC zALHJ%37f|F5a9j*C}~I6?s2Nu_yk>l^jSLl z?vqQ^`>$K;(Npigr;HTb?dB>Yo9KcL&6L!)d^Ug$2BMbER!RzknF1WvD#Z~)9_vwD>9~-j?6UpU*~g8KXwF?YB@>j2aObgZ&1oa=> z_HcOz|IdF^IT5pQp7M%B!-PgxFIC6HScNmP_85M;T8EP4&pSA5~|0=nPQ~H$^c2nPsO{bd?$3;c~e{XJ} zU7rxDs5%cQB(!X!L2oLXX%q(H<+3Z!-=nY!7FzZ^*!*8CWd{9f4iusWPy<5{9G^%b zLS{&jTBxPfsdubyA2wXWZaHeVG1GyV|Ia2(+f>x;5u)Q;H&yIzT&%H%nET)HqAq*q zKGiGbs@CyeEYfRJ|H$`)lQHka-UE~!U;fKt%M)RVYfNh`T#aVUC8=2H!F?Qh>k0bc z^Vc=J=S5<%rISj#ZaSAJvhaPcg;4Ye9QbKQ=@AidHvy5i8WjnvQ7&)pGuQLriKdq;?6Cu@gcBc;7dE`01<4TO| z>_juXEqpUqZQtnZw7xe$fp_g%O{2aar3bElPEpaQmjZY)mumFSAFKL|asD&G2c2Y} zbMI2GZn!~qoy5q^p0D&x!U&!GtPrHUU%T;5 zOK%9qgsJ&IyO}rhTLA z_SYE21iXtMb*>wC*7h4KEBbJ$SN&< z`*@B<+;WRjb`;*px?b%;`g8i1YFO9wboBE5c(>R#fe~&9f2`zmySqxhcx?tTK;@h7hyO zi7aCtbId$C$Jyq*d7kTkZp;_KYC6q4ayQd?8gu3pj1mqp=bY!}F~@vn4)ZW&=bEsX z!xUNbR)vN)_n#>nyy;_=wSCvQnUfE`K<`g_S&@6uhRYLI4!lF}PkzpMo|%iy7i5{BWe1tsh@wT zltdOf7RHtxKkJarN9&GZPig6*G-XrQ%YY11#E)Eik0I#^{xcyu@}TZjhNJ@K5t?>B#Y%<&UvC%q_7((6WzzU%nV zy=h-MzGQFOPlo))`7_W>GKzVMv4545Im=PVx!H@%HM6*=Y$P2L-o)9tJv!l~eI{v8 z+_L@fs8%}T#DV*bq`~4Xz8p41Ryh;609oS$qQ(bBRabSQMorsYVDL+`dnRM9{Nn7= z<}}^?(i5sx%2cElDd|Q;EnIF0JHuIm&TbMOkH;~drx=y@{ErW6z;Wm5mJ@H*cdt%U z@?P|#M0t$9^&|Z}|7UaLEBNZQ`>O9D3Yq`!>*=3=)L(!2cA(!Aa;OZg% zqlyQM-qt4iiMp`c14p{uH~^xCxXT??+4Vsu14-kTW&;8CR6Nuek?@Z6sad&OGN7WoP>Rt)Y=Rz0VnHbouMP<0=GpY=7a=OO=sQkl&jLrJIktQ4f4` zum4Qhk!z~xm~U6>ybA}*o6V*gKCnysRX4n;Tr&#U8$|8gq_7QZ6p^%Exrv*!e$!@c zNKDk0)Ku+EPgPP@sVTq{$6&+TUKnbX-xj?%$^gwcQ~vFI_q5KgyH#o#_~; zKS6Vi?gI8q46k;glm0a{qOJdI`5|-LjgIm!1amKMKkgdc@x=rC>}o74R;`t~cRNg3 z8M7Us7O)FCez{MLk;RkH7ubB)8P*3ZLctjDA1#89v)`W&A7O`>dssuY>-N&&jT`CcZoSm`oZ&jC zW2`Si#Jmf4=luCe8g}|nC8ZSjzDZnk^)V-C`+!UQXQ~#aJb0<5uP8hL)TY^~`gy|p zYS>`kTNkNX-hFABZn*0_<#-BK2S&#nqVpd8UQsn_I=jfnu3x6mjVlzMv{s=RJLUBx zYg2NjmZxNBV_LQnGc%N)k)o`u9m>wwB40|@LJFxwF!ZI}kUz#I{9<<@a-g$fEpUl( zMEoU+tTEC&+uK@LImCqW?ISn@!4_z56|Tgb@&75uFWJ9;~paJ_?I2Ik{F@ zk&`u3nOiRQ9~B_J_7VE>rKfb*kk0-y6^~CxP0+>no^9@Tl9QFw4>;i+W0Q;#5<-GF zR!En-s;;a=Z7+lxQPlVww8vN*zcfy+4l}ec%uQZ7Kd#s*${o?#Ma1b8ZTF+f}yDoh4Z2y^k10v?>JHOR~ zqi;~Iw;;AXBC4%gH|Z*GM2H!!b&O!B0G9<@R^fln&HUFqPAhh0-=2VC4vXySC+2R3r`d_UP!$gn@|3ap z{9@f6{X3qmcYk?9bxX)_87x4NXC69IKhJ%q=nm*qtmlmpSIY`$g6eUAmSa!pLuDaq zz8HguE&1WTu#fgRf3tt5)-#L6@<1XzU;}4aTU_;4dgJCtbk<#Gak$rb_x6u9@`c-! zq7mU_R-#?^0FXAP*a0uEAu>$HrAn3elK>U$kSZKD< z?v4*aBiblB;TdP*ts;?^yWJ4>EN5X3Q5|g7Kt?*6TS$0Qg@!j%NCb~fo#T*V;>9Zh z-t?c8wY~TW;``5fKv#~wX^;7Mf{B%LH|X#aE>+TwKZ?3N@WZ`f44)tQg$SG3%M}Er z!~P~}zR;2{6&;lf*%EHYBgZ_q*{Ns<_O;cGkMD1wdy_U7#N1 z*MWSO7-b()L(z3d8Kv6O|5q0d^0Jk_afs<>IQ;!)l4 z&9g>X<1#D63GW(%7v>nVZV!jW%NzTPsQDtRj3GSwN`hh`Uwq8=IjWWCkTqt>n)h%X zqU1Gf%k8L{dJPZPSC2ocz9SCjaIdj^L8AH|bB0oOP6<>|u)$l#=s8y!V}}{8-L8UU z2UHp&s)dCsE<93kMy1C_S5sVU4aL>0 zV}{cW6NBZa8>2Cj)~QnAT0ePdY`5-$Aq`XOTkzY6Yyl~UuT7<`) zsVGC(ds#IOIFL6aeZwi{i5-~iiY5(D*7!*usc|E;UB%*+>)+KaZ#`&)$cCb78Xq6Y zNZ8#2@kXv+Ua+fDDF;+EQB!dtThuWG4KZ7SgVaCV&V}dru~FPKVq4RK z+BzW`c)VzNI`keSvUh0D5~2C$e0u)(gz4fVuF~^kpD@S%H(lhzTaT+J$^JwQ3)u31i<^+fLk;^2Ov|*6552u26DP;k^xu z@h^kG#zYxHwDu8oOlx%F3n13rd`5BLbNL(yorY`E>UA|Z$7S`11jb|A=+*`>3Y5L=SPMM(u%5SQ~}w+$kt}n zwB>}LprVL6kfc$acBwZZZaC`-O~6=`W+E;SeY*|U4}ZK_s`RDB;g(bG*NfvG3#>3S zTAZM);}49Jde12Z8OT7+BKgdx-{cW>SkIY6l_J z6kX?Gg~c4@KXKqA&%0Uan}(Vv$>v7o6(vIM9knFCo<>Di*U_EM)Th6VR$TQmHtwUSSB8b}FaO=-SP2#L1di{K3oX;;1 z{CF%JAm%h8s(~URYAGfxUa^tYlsNy=_yAW`^OlCLPCVyO&IX47-UIJvIcQmLgu-(K$69ZBWY`p zFs~{48JPWy;uJY%VLzYGfaFp4JaYf(IQT4kEqqqs=ULdw!ei0r+!s1AI|O9pKxfS7 zIjX2Z-4itFuTRyuiF2x4!6KzsZ}~*GzxAj*nQ&tvqQ-4SYa@J$KSH2%wse{#zec8U zKd6(8f}9K7EwKi`8K|2!#NrqK!l>QipmxKxYW0uih8CwB&B&w1)ryF(I(c5oj$CDK zxx$I}=Ke9M9T1yNxsW#&L02e@#S4By6M8f>N=pSS{_3Bjsg%qmzt8Jc@q|E^^8SIUc1)Z&@D>y?lj}p9NquTEB;sR z9e)30hK5{uuF_KAV+Rz>n*Hvg;P+#J?gGm`lr{c&;@gpkbO1h89ad8h zpZkEWf90|=b*3f3Yp&$Mb4KauA4ePF+!RREJYja8a60B0P(fTXBNOX-BXWp3%Dr$# zb3|(F4J>JUJzi%hUlgaGd9l9z@*U+A`5WKmh!gXVCFfDK?>Beexorm|JSlH0d#5kT zqevtLZ4LrN5b>N&l(KQk1Vlw8=!7mO>AIV**O6x)T(0#fEybgkE`CtYOqr&n9TSyf zDpWw{%M&&3`ERvg+0V9vSO>a$lKCsddYsQ2Dd(|^xK#(VD zF~95IOW2!h3Dpv(eY92-_BGbHXP6~ZY;QluzOU%#ShJsHAB(~gHOBqGY-L5IGMP$Q z4>Fdoicr;U8lS3Pr+%b*=%pZl80#Ab-KzKgeqWiH2xkXe8|vo|W5m4TL*|-sB_SM6 zHkKcU4?z>9k?(E`ja0R`uBuU^z7nEh)G9tk^{T}xC$zed54y4-J(s&>y(jU zI{G}S$*u^CXr{DO`ikOV7s#+0FuD$lV=7+^{l0=^Rq30~H^0*BbZkXj9<;e{jjc6C zM+$G)RmTVk2NB^>YSeU)uIEYr>*G<;`B>jF3ED zF2MoUMi2pNnkWzEs^9}$X{8Zv`;1@`H6KRBXLwDGV|YoneHQZ*1>rOb)I@khLtS#@ zxw`LzdsM4-etA?FaC@|GbE;Oa%g>HR!BAny4r9Y_@M~5`p%epKFhvQ}J_cHGu;+O_ zuw-nFV_svA?epDzo(0k{kgR#VRn)k*hG8nX^tDZAu7aXi!k*48;FVjDZKR1mMPIf@+vuOahUOE&=+NpWeemE9{ zM;G_i{TgI9*`QIN%)DRQCPOgEq0{IHXx&gVB(Y0?^ zcz!MCTMzC>hbRss&1Cq}F5y3<-iJF_q1Pzl)41-w!aGf#8+g}-D zbaMA$dTq=rs#%lHa{u5pb|ZQ34l@Lz?KlO);s%1*nM9-qWtbDx@S6h!R3OS7m58g0 zB+``omByTM_sv7J(5nxBqTwUE?b0KZC;t6oy+&Moy_T&iw3{^q;NP6mxwia&M zHiLCR+q$~=g$I<-5S}PIRxH@Ak>~wSla~INpO++nsHtZhpzz@nls)WRh=fqr5r(Lv zYTa(`ZdLVL=lIi?PISnymEPD>d^o8&dY&TAZWr{(3_RdwKnzulRCcZ@x>|kp>2Z=S?$%xBJad(!z{&&3$zI)d+D&@@pD!G; zE@tu!^=Ol9<1RvB=aZ0KejXOtPEmD6IZ2eN8nSlWD{tC&4j~qa@Zk4D&bS$wpyNI; zk!Zu+xDU*EK-MU0xC9sK0Ap-Lp6?2=Kmd}xu%xhFOU^8?32<{*@ZFFxB+d3(*4=^o z6E_G2SfN}%qY4JNJE()eo6IDlb5k8(Wkhq`cJc}8d(!?T>2yQCn^P&5d?0;LS&)ql ztK+(@AJ@g6K?SDf<0>NAGj1c?M|6M=m`$Iv_E(Mnab3`B`|;;?*E82&=WHV32W3gQ z=7$bO$28TceFo^x3op{S&z2uagZTJxJ^sxzIabJ5E~+J zDka2*(#y$S;%qR>B9W?u?48f;iKKBO{A3K50&11|LcOuSpE_gf5r1df92TCD`21yK zxJnRE`3;zk5aQ*Y;R9y`Q=IWPUQ>jm`5aDgJ$$}(cRuZ;21li znNSHFJD{@mY}Hr89=_D{dj6C930uwWZ&#;~tYZ{_yT&d9r=#I_aQ!Y>mkcT~6`o-| zogQu@LK|_`It_}DryTEQJ^8@B&IDYran>y-=z+5@Q;k~8Nfky!WNlq^_%XWek!LjE zLR{(PkNE1Ldj8X=ba1Ov9EFEMM)~;xS>u1DUZLW+SL_s=Pe3R?h$T0N9nWu3cH$HE zPn8;;l=qFj<0EO$E+h@{kv|9WW%Vy1Iw)yK8ovnjh)0N#`@-w#^ofJV6+x)nH^vwW z69quT2*l23@6FAzujjptoSBW-F14J;UB3>H?h{t}xsn9EYNO64N)SZG(a8)|+Y)JW$7a9^XSXmS$_} z`ZVR`Y;ja=e4V};c|?C*^w21E?HRRN z5ApJwIY&F=rUbSxOW2&J4MM0WTZohQK%x*h5g8CT3K;hZ^^Lm5d-+b@YmLr;&!NFX z#tc)4@Zyp8b4@6CK8Nq1)8u{og``2SeAJK}iWHw46E-a9r_wpI>T&VCHSULR)U9Wb z9h9X+k}394mprRSzqrk)m;BC{C-|u8t?{GLaEkW&E_M$p6D|e{8Q&stW=Pg^jM0Fc zy3o4eX__?iqrKjIxGaDxo;&9uJ^cGvL)0<4yhl5oa?j)H(Id9P1jDD#Ox4wo{+}{a z=Qt!*B#!tuW7IY384Z)(gNb-n2Nd$h`Xn$8)kRcRUrtLqDDcXO7Xw3kbuPgP9;Ohw5fNvZoThpCxoUlfIPR| z1|BNUtPK6Vf`9<0Oi(dMJqnaUqfm<~YZoPh#${b$G%RNBX*{7Q%9PB_Y7}vZdi7`E z6h^uqojQQ1drgF2!nh(kGZ$`i;?oZ?@JMr;+cLzS;03it3@tfEP7LrD_ zGNDWOMM)%W!QU}H3j)P>FYVJ3Eef0aMrX(O@f{d`X51JkWegV+#~7W39m#ON`7BG) zMHT*jK_Nw5UNFj>-i(kMd~c{IRw72A*rOt&H1yEx^!E5i&8f>e-&1dXaDTmeL9xDrC-t@HA#biFnvW zU57_^a}qmM^j2R5@F;8h-SVc7E&7`P?F2%^Crw9WN!qp}2(c_gWJLhR{lKVKfyO!h zZhJAlhn~^8Qmk3!j5*{C%wwg1(>n(N_6FJt<=iAEPqIPHdcpNcmKF69q?=zIrIUM| z>p$aw!e+RR8wavM^`H#-ZMN`T6b~vomEKMU(KsO>+(=F*N2uERZ4>m`?ev8egNUdw zb!@kPNE#e=R6U*D^JsH(B1{GHO)mODeInBySH`_#f((MgkILj0u93Hmp&0W;0{1X0 zZMxDnoh45yn+yu{W^<1dFHvmCh2j4}MyUZ^BxL%854Zs_FWhZ`UDRhWgJqpd}46 zhOZUhJN_^xD}s6d4iwOkXt@k08Hz$Y5Dw=ZH{{0a zRI3giK~uR3h#FCYyAY2(^^1E1i9l=+9I?jao*_RvJB)B&9m2*Hl9jnrncE*y#^%eF z=Y^w!kGx#-om(zbPS(`?=#&6(ZICm)2anb#jl09T7Rr+l`7C6#XH)UtaIsrZ>NFf02Mv^liqDmaT8Cs%gxhxhq35oB)|v48 zFQ8|iI2}?Sj|QDap(+GxGl__*rhajcP}VRnqY)Nh5K9bE??zouQQF2coGf*092+d; zdKN2V(|O9tp6~eCv$A&~Y2uv8HdmCSAxTKU&j>C`LNJM%CPz<|ubgb^$8^%&RdS4Y zWVlXl-@&LIgpC6l&6&m+4&eTvA>kHrQ8Fla7z0(!kQ7yoGR$X@98ad+o%o{`&WFzr ze8ibs1$}d(Sfh+>cPf43 zP$%|!pOKfTtQ~h5dq8=)Sxzi?kxX;6cM>d-QkKbgmW2nBG_P?e7P}r%s>@Ar$SAMcCODB}J=Q8Ue5_g53l(pG9jdapw0H4r)|RjL#T5J03y z(9l}~(t8PnCPgABgd!w>pfmwNKq4TZM0yA5O{pp(e0ksR-v8n5PkYYJ*>m=pojJ3! zQ{3Kw-=B{nk&V^!)n3Z37t?l~ZB&d%gPDj5zfN7QXR-*Sjl}gp4gAob&q|(W)Is1* zk!5}P;nBXRN9ktHsrxR>+2)DB;DEGy4!O+T!k+zw0n&LWnzdZ<&YC^=ry_^?JU-Ji z+TAIA*X7^%iARdUVy>@~lM-eEQrm(7!rXWo$oMb0VuFd-bIf6;Q}hxzTk=@juogq? zK8Y~?o+iu?S5k{1h%wuJtN}>k>VO7gO8ZpJnZ2?d*Ii$}Jg+!5E8x6&e}Q&xt|BNk z^lP-dutrbTNnmpMy?)CF#;R*)&%ri;KpBT8jsgTktMEGp0Q-Q2I4iwp>fnQ~choiN z#lrKVA|B>knywkNC}coSFg2t7)aZU3UqRJRdPQraq%hZl3u4LRea1UTWIjQ2jfjb>$SaR@`l`KnLeN&2wHZ^@zzpSk zDfJ2pHmbnL>YYsM^?ZDh-krD4-3@#$-B^!*ug@h84hR^sQd)bDcsST0({R{(KO96H z%}GE~NQ#lo7Mhl4CL*6;C(Kr-yefWjr4BFFQ$!teY_ZhXlKX$K_H z8N7)4W9EaM=6_$6Z(&=SUv(Ykx5Hj;ywsIcU$`4zpXvqDn__Gj z{+51O=x-0eMDubS7=K%ESs{wrzQ)tRk5Uv1%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 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 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 1195/6909] 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 1196/6909] 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 1197/6909] 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 1198/6909] 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 1199/6909] 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 1200/6909] 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 1201/6909] 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 1202/6909] 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 1203/6909] 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 1204/6909] 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 1205/6909] 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 1206/6909] 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 1207/6909] 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 1208/6909] 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 1209/6909] 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 1210/6909] 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 1211/6909] 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 1212/6909] 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 1213/6909] 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 1214/6909] 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 1215/6909] 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 1216/6909] 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 1217/6909] 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 1218/6909] 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 1219/6909] 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 1220/6909] 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 1221/6909] 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 1222/6909] 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 1223/6909] 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 1224/6909] 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 1225/6909] 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 1226/6909] 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 1227/6909] 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 1228/6909] 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 1229/6909] 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 1230/6909] 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 1231/6909] 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 1232/6909] 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 1233/6909] 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 1234/6909] 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 1235/6909] 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 1236/6909] 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 1237/6909] 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 1238/6909] 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 1239/6909] 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 1240/6909] 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 1241/6909] 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 1242/6909] 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 1243/6909] 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 1244/6909] 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 1245/6909] 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 1246/6909] 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 1247/6909] 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 1248/6909] 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 1249/6909] 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 1250/6909] 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 1251/6909] 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 1252/6909] 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 1253/6909] 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 1254/6909] 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 1255/6909] 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 1256/6909] 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 1257/6909] 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 1258/6909] 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 1259/6909] 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 1260/6909] 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 1261/6909] 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 1262/6909] 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 1263/6909] 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 1264/6909] 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 1265/6909] 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 1266/6909] 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 1267/6909] 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 1268/6909] 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 1269/6909] 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 1270/6909] 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 1271/6909] 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 1272/6909] 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 1273/6909] 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 1274/6909] 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 1275/6909] 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 1276/6909] 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 1277/6909] 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 1278/6909] 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 1279/6909] 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 1280/6909] 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 1281/6909] 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 1282/6909] 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 1283/6909] 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 1284/6909] 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 1285/6909] 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 1286/6909] 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 1287/6909] 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 1288/6909] 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 1289/6909] 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 1290/6909] 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 1291/6909] 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 1292/6909] 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 1293/6909] 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 1294/6909] 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 1295/6909] 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 1296/6909] 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 1297/6909] 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 1298/6909] 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 1299/6909] 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 1300/6909] 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 1301/6909] 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 1302/6909] 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 1303/6909] 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 1304/6909] 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 1305/6909] 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 1306/6909] 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 1307/6909] 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 1308/6909] 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 1309/6909] 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 1310/6909] 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 1311/6909] 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 1312/6909] 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 ///